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

写在前面

源码地址

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

快速开始

js加载

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

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

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

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

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文件。
...省略代码
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/查看

vite实现原理-js加载和裸模块加载
此时,main.js已经完全按照预期进行返回,但是浏览器抛出了一个错误

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

裸模块路径重写

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

vite实现原理-js加载和裸模块加载
vite将裸模块改成了/node_modules/.vite,其中.vite目录是vite对模块进行了预打包,这里不具体阐述,我们只需要按这个思路改写index.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官方的一样的效果,运行看看

vite实现原理-js加载和裸模块加载
刚刚的报错已经不见了,变成了404 Not Found,因为我们并没有处理/node_modules/XX这种路由的请求。

裸模块加载

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

... 代码省略
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

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

<!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>

最后:

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

原文链接:https://juejin.cn/post/7325730345839099956 作者:Infatuation

(0)
上一篇 2024年1月20日 下午4:21
下一篇 2024年1月20日 下午4:31

相关推荐

发表回复

登录后才能评论