我们来总结一下vite的原理。
- vite是在浏览器端使用的export import 方式导入和导出模块
- vite支持热更新
- vite依赖es module的特性
ES Modules
ES Modules 是浏览器支持的一种模块化方案。
import vue from './src/App.vue';
当浏览器解析上面代码时,会向服务器发送一个请求来获取对应的资源。
这里我们需要注意浏览器只支持相对路径和绝对路径的解析,所以我们要对第三方的包进行拦截处理。
Vite 实现
拦截资源请求
我们了解es modules后,需要做的就是实现一个服务端,拦截这些资源请来求实现一个迷你vite。
准备工作
- 用koa实现服务端
- 用nodemon实现node热启动
- vue文件转ast语法
如果没有nodemon ,建议全局安装。yarn add koa @vue/compiler-dom @vue/compiler-sfc
npm i -g nodemon
启动服务端
新建qvite.js。代码如下:
const fs = require('fs');
const path = require('path');
const koa = require('koa');
const DOM = require('@vue/compiler-dom');
const SFC = require('@vue/compiler-sfc');
const app = new koa();
app.use((ctx) => {
const {
request: { url, query }
} = ctx;
if (url == '/') {
ctx.type = 'text/html';
ctx.body = readFile('./index.html');
}
// 下面我们的代码都写在这里
// ...
})
app.listen(3000, () => {
console.log('Koa listen 3000');
})
function rewriteImports(content){
return content.replace(/from ['"](.*)['"]/g, function ($0, $1) {
if ($1[0] !== '.' && $1[1] !== '/') {
return `from '/@modules/${$1}'`;
} else {
return $0;
}
});
}
function readFile(srcPath){
return fs.readFileSync(path.join(__dirname, srcPath), 'utf-8');
}
function updateStyle(content) {
let style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.innerHTML = content
document.head.appendChild(style)
}
启动代码:
nodemon qvite.js
实现了上面的功能,我们就可以启动并预览到效果啦。
拦截第三方包
我们知道第三方的包,大概长这样 import vue from ‘vue’
通过import视图导入时,vite会对路径进行替换,我们看到的结果是这样的:
import vue from '/@modules/vue'
在koa中新增对js的拦截。
else if(url.endsWith('.js')){
ctx.type = 'application/javascript';
const res = readFile(url);
ctx.body = rewriteImports(res);
}
如图所示,我们会看到效果就是这样的:
解析第三方包
完成上面步骤后,浏览器会向服务器发送一个 http://localhost:3000/@modules/vue 请求。
我们要做的就是找到那个真正文件地址,并返回给浏览器。
文件本地路径: node_modules/vue/package.json 文件内找 module 属性。
else if (url.startsWith('/@modules')){
// 解析module
const prefix = path.resolve(
__dirname,
'node_modules',
url.replace('/@modules/', '')
);
// 获取 package.json 内的 module 属性
const module = require(prefix + '/package.json').module;
const p = path.resolve(prefix, module);
// 读取文件
const res = fs.readFileSync(p, 'utf-8');
ctx.type = 'application/javascript';
// 读取的文件可能还会有依赖,递归替换。
ctx.body = rewriteImports(res);
}
解析vue文件
完成上面步骤后,我们需要解析开发的vue文件,
我们的应用文件大概是这样的,按照上面的思路我们继续开干:
import App from './App.vue';
拦截 .vue 文件
// 解析vue的模板和样式
else if(url.includes('.vue') && query.type){
ctx.type = 'application/javascript';
const newUrl = url.substr(0, url.indexOf('?'));
const { descriptor } = SFC.parse(readFile(newUrl))
if (query.type === 'template'){
const render = DOM.compile(descriptor.template.content, { mode: 'module'}).code;
ctx.type = 'application/javascript';
ctx.body = rewriteImports(render);
}
else if(query.type === 'style'){
const style = descriptor.styles[0];
ctx.type = 'application/javascript';
ctx.body = `${updateStyle.toString()};
const __css = ${JSON.stringify(style.content)};
updateStyle(__css);
export default __css;
`;
}
}
// 解析vue
else if (url.endsWith('.vue')) {
// vue 文件包含了三个部分,template script style。我们需要多每个进行单独处理
const { descriptor } = SFC.parse(readFile(url))
ctx.type = 'application/javascript';
const script = descriptor.script.content.replace('export default ', 'const __script = ');
ctx.body = `
${rewriteImports(script)}
// 将vue的template单独作为一个请求来解析
import { render as __render } from '${url}?type=template';
// 将vue的style单独作为一个请求来解析
${descriptor.styles.length > 0 ? `import '${url}?type=style'`: ''}
__script.render = __render;
export default __script;
`
}
实现了上面的功能后,我们打开浏览器,就已经能看到内容了。
哦吼,能看到vue文件已经解析出来了。
vite的原理解析就到这里了。
完!