Vite简介
引用原作者的话:Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,
完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。
针对生产环境则可以把同一份代码用 rollup 打包。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,
做得好可以彻底解决改一行代码等半天热更新的问题。
正如尤大说的在开发环境下,跳过了打包,直接通过ESM方式加载文件。
基于webpack 开发环境加载逻辑
这样的工程可能会遇到这样的问题
vite基于ESM 加载逻辑
10个相同文件 vue-cli VS vite
很明显,vite在开发启动时间远远领先与vue-cli。
那么vite做了什么呢?
ESModule
ES 2015在语言层面上实现了模块功能,且实现简单,可以替代CommonJS和AMD规范,成为在服务器和浏览器通用的解决方案。
<script type="module"> // 标识为ESM
import {test} from './test.js'
</script>
浏览器会自动发起请求,请求test.js文件。
但ESM不支持“裸”模块引入,会直接报错。例如
import {defineComponent} from 'san'
Vite会检测到当前服务中所以的.js文件,并重写他们的路径例如/@modules/san
。
Vite使用了es-module-lexer
用来解析import语法,通过替换的方式将 'san'替换为'/@modules/san'。
Vite1.0浅析(暂无热更新,编译模块)
启动KOA服务
koa-server
const Koa = require('koa');
const {staticPlugin} = require('./plugins/serverPluginStatic');
const {moduleRewritePlugin} = require('./plugins/serverPluginModuleRewrite');
const {moduleResolvePlugin} = require('./plugins/serverPluginModuleResolve');
const {htmlRewritePlugin} = require('./plugins/serverPluginHtml');
const {vueComplitePlugin} = require('./plugins/serverPluginCompliteVue');
function createServer() {
const app = new Koa(); // 创建一个koa实例
const root = process.cwd();
const context = {
app,
root // 当前根位置
};
// 插件集合 依此进行安装
const resolvePlugins = [
htmlRewritePlugin,
// 4) 解析import 重写路径
moduleRewritePlugin,
// 3) 解析以/@modules文件
moduleResolvePlugin,
// 2) vue文件编译
vueComplitePlugin,
// 1) 实现静态服务的功能。 最后
staticPlugin
];
resolvePlugins.forEach(plugin => plugin(context));
return app; // 返回app 中有listen方法
}
module.exports = createServer;
Vite 通过 Koa 启动了一个 http 服务,并且加载了一些插件。通过添加插件来对不同类型的文件做不同的逻辑处理。 模块的解析机制的相关插件是 serverRewritePlugin 和 serverResolvePlugin,静态文件处理用了koa-static
模块路径重写
需要先解析import 语法,用来替换引入的包路径,上面我们也提到,Vite使用的是es-module-lexer用来解析import语法
中间件serverPluginModuleRewrite
moduleRewritePlugin
const {readBody} = require('./utils');
const {parse} = require('es-module-lexer'); // 解析 import 语法
const MagicString = require('magic-string'); // 字符串具有不变性
function rewriteImports(source) {
// let imports = parse(source); // [[],[],boolean]
let imports = parse(source)[0];
let magicString = new MagicString(source); // overwrite();
// n 匹配到的 from 'vue', s=>start e=>end ss=>整条语句的开始 se=>整条语句结束, d => dynamic >-1 是否是动态引入
// [
// [
// { n: 'vue', s: 27, e: 30, ss: 0, se: 31, d: -1 },
// { n: './App.vue', s: 49, e: 58, ss: 32, se: 59, d: -1 },
// { n: './index.css', s: 68, e: 79, ss: 60, se: 80, d: -1 }
// ],
// []
// ]
if (imports.length) {
imports.forEach(i => {
let {s: start, e: end} = i;
let id = source.substring(start, end);
// 当前开头是 / 或者 . 的不需要重写
if (/^[^/.]/.test(id)) {
id = `/@modules/${id}`;
magicString.overwrite(start, end, id);
}
});
}
return magicString.toString();
}
function moduleRewritePlugin({app, root}) {
app.use(async (ctx, next) => {
await next(); // 静态服务
if (ctx.body && ctx.response.is('js')) {
let content = await readBody(ctx.body);
// 重写内容 将重写后的结果返回回去
const result = rewriteImports(content);
ctx.body = result;
}
});
}
exports.moduleRewritePlugin = moduleRewritePlugin;
模块路径引入
处理/@modules/xxx/路径,返回正确的
moduleResolvePlugin
const moduleReg = /^/@modules//;
const fs = require('fs').promises;
const path = require('path');
function resolveVue(root) {
// vue3 有几部分组成
// 运行时相关代码 runtime-dom runtime-core
// compiler compiler-sfc(单文件编译部分) reactivity(:Vue3 响应式部分) shared(Vue3 工具库)
const resolvePath = (name) => path.resolve(root, 'node_modules', `@vue/${name}/dist/${name}.esm-bundler.js`);
const runtimeDomPath = resolvePath('runtime-dom');
const runtimeCorePath = resolvePath('runtime-core');
const reactivityPath = resolvePath('reactivity');
const sharedPath = resolvePath('shared');
return {
'@vue/runtime-dom': runtimeDomPath,
'@vue/runtime-core': runtimeCorePath,
'@vue/reactivity': reactivityPath,
'@vue/shared': sharedPath,
'vue': runtimeDomPath
};
}
function moduleResolvePlugin({app, root}) {
const vueResolved = resolveVue(root);
app.use(async (ctx, next) => {
if (!moduleReg.test(ctx.path)) { // 处理当前请求的路径是否以 /@modules/开头的
return next();
}
// 将/@modules/替换掉 指向到当前项目中
const id = ctx.path.replace(moduleReg, '');
ctx.type = 'js'; // 设置响应js类型
// 去当前项目下查找 vue对应的真实文件
const content = await fs.readFile(vueResolved[id], 'utf8');
ctx.body = content;
});
}
exports.moduleResolvePlugin = moduleResolvePlugin;
vue文件处理
vueCompilePlugin
const path = require('path');
const fs = require('fs').promises;
function vueCompilePlugin ({root, app}) {
app.use(async (ctx, next) => {
if (!ctx.path.endsWith('.vue')) {
return next();
}
const filePath = path.join(root, ctx.path);
const content = await fs.readFile(filePath, 'utf8');
// 引入.vue文件解析模板
const {compileTemplate, parse} = require(path.resolve(root, 'node_modules', '@vue/compiler-sfc/dist/compiler-sfc.cjs'));
let {descriptor} = parse(content);
if (!ctx.query.type) {
// App.vue
let code = '';
if (descriptor.script) {
let content = descriptor.script.content;
code += content.replace(/((?:^|\n|;)\s*)export default/, '$1const __script=');
}
if (descriptor.template) {
const requestPath = ctx.path + `?type=template`;
code += `\nimport { render as __render } from "${requestPath}"`;
code += `\n__script.render = __render`;
}
code += `\nexport default __script`;
ctx.type = 'js';
ctx.body = code;
}
if (ctx.query.type === 'template') {
ctx.type = 'js';
let content = descriptor.template.content;
const {code} = compileTemplate({source: content}); // 将app.vue中的模板 转换成render函数
ctx.body = code;
}
})
};
exports.vueCompilePlugin = vueCompilePlugin;
参考资料: