写在前面
接上一章内容,我们搭建了一个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
参数
- 当
url
等于/
时,默认返回index.html
。 - 当
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-type
为application/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.json
的module
字段找到该模块最终打包的输出路径,看看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
单文件。