vite实现原理-js加载和裸模块加载

写在前面

接上一章内容,我们搭建了一个koa服务,通过fs模块和path模块读取并返回了一个index.html文件,浏览器成功读取到了html的内容,接下来同样的原理,实现一下js的加载和裸模块的路径改写。

快速开始

js加载

根据行业统一共识,我们继续在项目中创建一个src作为项目的主要入口,创建main.js,最终项目结构如下

text 复制代码
├── index.html
├── index.js
├── nodemon.json
├── package.json
└── src
    └── main.js
js 复制代码
// main.js
import { createApp, h } from 'vue';
const app = createApp({
    render() {
        return h('div', 'hello world');
    }
});
app.mount('#app');

回头看下上一篇文章的页面请求

我们发现,不仅localhost返回了index.html内容,浏览器自动发起的./main.js请求也返回了html内容,这显然不是我们预期想要的效果,原因是因为koa没有判断路由,讲所有的请求内容都返回index.html

js 复制代码
app.use(async (ctx) => {
    // !!! 没有判断路由,所有路由默认执行此逻辑
    // 读取html内容并返回
    const htmlFile = fs.readFileSync(path.join(__dirname, '/index.html'), 'utf8');
    ctx.type = htmlCtxType;
    ctx.body = htmlFile;
});

因此我们需要进行路由判断,修改index.js文件,判断路由url参数

  1. url等于/时,默认返回index.html
  2. url等于XXX.js时,需要返回对应的js文件。
js 复制代码
...省略代码
const jsCtxType = 'application/javascript'
app.use(async (ctx) => {
    const { url } = ctx.request;
    if (url === '/') {
        // 读取html内容并返回
        const htmlFile = fs.readFileSync(path.join(__dirname, '/index.html'), 'utf8');
        ctx.type = htmlType;
        ctx.body = htmlFile;
    } else if (url.endsWith('.js')) {
        // 处理.js结尾的js文件请求
        const jsFile = fs.readFileSync(path.join(__dirname, '/src/', url), 'utf8');
        ctx.type = jsCtxType;
        ctx.body = jsFile;
    }
});
...省略代码

也是利用同样的原理,浏览器请求main.js时,实际上url="/main.js",判断.js结尾的所有路由,fs读取src目录下的对应js文件,设置Content-typeapplication/javascript并返回,打开http://localhost:3000/查看

此时,main.js已经完全按照预期进行返回,但是浏览器抛出了一个错误

这句话的意思是,vue模块加载失败,相对路径必须以/./../开头,浏览器只能识别相对路径的模块加载,对于vue这种裸模块无法识别,因此抛出了错误。

裸模块路径重写

路径重写,字面意思就是改写vue这种裸模块的引入路径,将其改写为/./../这种形式的引入,我们看看vite官方是如何做的。

vite将裸模块改成了/node_modules/.vite,其中.vite目录是vite对模块进行了预打包,这里不具体阐述,我们只需要按这个思路改写index.js

js 复制代码
// index.js
// 重写import,加载裸模块
... 省略代码
app.use(async (ctx) => {
    const { url } = ctx.request;
    if (url === '/') {
        // 读取html内容并返回
        const htmlFile = fs.readFileSync(path.join(__dirname, '/index.html'), 'utf8');
        ctx.type = htmlType;
        ctx.body = htmlFile;
    } else if (url.endsWith('.js')) {
        // 处理.js结尾的js文件请求
        const jsFile = fs.readFileSync(path.join(__dirname, '/src/', url), 'utf8');
        ctx.type = jsCtxType;
        ctx.body = transformModuleImport(jsFile);
    }
});
function transformModuleImport(content) {
    content = content.replace(/\s+from\s+['"](.*)['"]/g, (s1, s2) => {
        if (s2.startsWith('/') || s2.startsWith('./') || s2.startsWith('../')) {
            // 相对路径的文件读取,无需处理
            return s1;
        } else {
            // 裸模块,处理成/@modules的形式
            return ` from '/node_modules/${s2}';`
        }
    });
    return content;
}
... 省略代码

创建一个transformModuleImport函数方便后续调用,接收一个content即js的字符串代码,通过正则/\s+from\s+['"](.*)['"]/匹配所有from "xx",捕获括号内()的内容替换成/node_modules/,具体可以了解一下正则。在返回js内容时,将读取到的文件内容通过transformModuleImport函数将所有的import替换一遍,就实现了vite官方的一样的效果,运行看看

刚刚的报错已经不见了,变成了404 Not Found,因为我们并没有处理/node_modules/XX这种路由的请求。

裸模块加载

我们需要先安装vue模块,运行命令npm install vue -S,接下来分析一下到底需要读取裸模块的哪个文件,因为裸模块是不确定的,所有的打包文件名称没有一个规范定义。可以通过package.jsonmodule字段找到该模块最终打包的输出路径,看看vue包的package.json

json 复制代码
{
  "name": "vue",
  "version": "3.4.14",
  "description": "The progressive JavaScript framework for building modern web UI.",
  "main": "index.js",
  "module": "dist/vue.runtime.esm-bundler.js",
  "types": "dist/vue.d.ts",
  "unpkg": "dist/vue.global.js",
  "jsdelivr": "dist/vue.global.js",
  "files": [
    "index.js",
    "index.mjs",
    "dist",
    "compiler-sfc",
    "server-renderer",
    "jsx-runtime",
    "jsx.d.ts"
  ],
  ... 省略代码

思路就是我们需要读取package.json文件,并取到module字段的路径,修改index.js

js 复制代码
... 代码省略
app.use(async (ctx) => {
    const { url, query } = ctx.request;
    if (url === '/') {
        // 读取html内容并返回
        const htmlFile = fs.readFileSync(path.join(__dirname, '/index.html'), 'utf8');
        ctx.type = htmlType;
        ctx.body = htmlFile;
    } else if (url.endsWith('.js')) {
        // 处理.js结尾的js文件请求
        const jsFile = fs.readFileSync(path.join(__dirname, '/src/', url), 'utf8');
        ctx.type = jsCtxType;
        ctx.body = transformModuleImport(jsFile);
    } else if (url.startsWith('/node_modules/')) {
        // 加载node_module下的裸模块
        // 读取模块下package.json的module字段,即该模块打包之后的输出文件
        const prefix = path.join(__dirname, url);
        const module = require(path.join(prefix, '/package.json')).module;
        const filePath = fs.readFileSync(path.join(prefix, module), 'utf8');
        ctx.type = jsCtxType;
        ctx.body = transformModuleImport(filePath);
    }
});
... 代码省略

新增一个/node_modules/路由,用require导入package.json文件,并获取到module字段内容,path拼接路径,得到路径为/node_modules/vue/dist/vue.runtime.esm-bundler.js,同上,fs读取该文件,需要注意vue.runtime.esm-bundler.js文件内也可能会有import from 'xxx的裸模块加载,需要再次用transformModuleImport函数处理之后再返回,如果有报错@vue/runtime-dom is Not Found,npm安装对应模块即可。例如: npm install @vue/runtime-dom @vue/runtime-core @vue/shared @vue/reactivity -S

运行项目后又有了新的报错,Uncaught ReferenceError: process is not defined,这是因为在node中会有环境变量的判断,在浏览器中并没有这种变量,我们只需要手动创建这些变量即可解决报错。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>
<script>
    window.process = {
        env: {
            NODE_ENV: 'development'
        }
    }
</script>
<script type="module" src="./main.js"></script>

最后:

将环境变量process挂在到window下,默认是开发环境development即可,到此js加载和裸模块的路径重写和加载就完成了,已经成功将一个vue实例展示在浏览器端,下一章将讲述如何编译vue SFC单文件。

相关推荐
y先森17 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy17 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891120 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端