Vite是一个基于浏览器原生 ES Modules 开发的服务器。利用浏览器去解析模块,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。
vite运行原理
通过此流程图可得: vite是借助了node的koa模块,启动一个静态服务器,通过Koa的中间件, 完成了设置静态资源文件, 解析import语句,解析@module模块,解析vue文件等功能, 本文讲会带领大家手写一个toyVite, 完成上述类似的功能。
2.创建一个koa服务器
const Koa = require('Koa')
const { serverStaticPlugin } = require('./plugins/serverStaticPlugin')
const { moduleRewritePlugin } = require('./plugins/moduleRewritePlugin')
const { moduleResolvePlugin } = require('./plugins/moduleResolvePlugin')
const { htmlRewritePlugin } = require('./plugins/htmlRewritePlugin')
const { vuePlugin } = require('./plugins/vuePlugin')
function createServer() {
const app = new Koa()
// 进程当前工作路径
const root = process.cwd()
const context = {
app, // koa实例
root,
}
const resolvedPlugins = [
htmlRewritePlugin, // Html文件重写
moduleRewritePlugin, // 导入模块重写
moduleResolvePlugin, // 模块解析
vuePlugin, // .vue文件解析
serverStaticPlugin, // 静态服务器启动
]
resolvedPlugins.forEach(fn => fn && fn(context)) // 遍历执行中间件
return app
}
module.exports = createServer
3. serverStaticPlugin 设置静态服务器
const static = require("koa-static");
const path = require("path");
function serverStaticPlugin({ app, root }) {
app.use(static(root));
app.use(static(path.join(root, "public")));
}
exports.serverStaticPlugin = serverStaticPlugin;
通过koa-static设置当前目录下和当前目录下public文件作为静态服务。
4.moduleRewritePlugin 模块解析
先看看两张对比图, 图一是项目文件中我们手写的main.js, 经过vite的模块重写,
import { createApp } from 'vue' 就变成了 import { createApp } from '/@modules/vue'
由此我们可以得出,此模块的主要功能就是重写 import 导入, 把非项目内的导入重新成以/@modules为开头的导入,具体代码如下。
const { readBody } = require("./utils");
const MagicString = require("magic-string");
const { parse } = require("es-module-lexer");
// 重写import
function rewriteImport(source) {
let imports = parse(source)[0]; // 获取字符串里面的import内容
let magicString = new MagicString(source); // 字符串转换成魔法字符串
if (imports.length) {
for (let i = 0; i < imports.length; i++) {
let { s, e } = imports[i]; // 如果 import a from 'bc'; {s: 起始位置, e: 结束位置}
let id = source.substring(s, e);
if (/^[^\/\.]/.test(id)) { // 如果非/或者非.开头
id = `/@modules/${id}`;
magicString.overwrite(s, e, 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 = rewriteImport(content);
ctx.body = result;
}
});
}
exports.moduleRewritePlugin = moduleRewritePlugin;
async function readBody(stream) {
if (stream instanceof Stream) {
return new Promise((resolve, reject) => {
let res = "";
stream.on("data", (data) => (res += data));
stream.on("end", () => resolve(res));
});
}
else {
return stream.toString();
}
}
此中间件判断如果是js文件的话,获取content字符串, 然后通过rewriteImport方法重写字符, 主要使用了es-module-lexer获取js文件中import的具体位置,通过magic-string重写字符串。vite官方也是使用这两个库。
5.moduleResolvePlugin - 解析@modules开头的文件
-
正则匹配拿到**@modules/后面具体需要的文件类型id****。**
-
拿到node_modules中全都的解析模块的集合
-
根据文件类型去解析集合中找到对应的模块都去并返回文件内容
const moduleREG = /^/@modules//; const fs = require("fs"); const { resolveVue } = require("./utils") // function moduleResolvePlugin({ app, root }) { const vueResolved = resolveVue(root); app.use(async (ctx, next) => { if (!moduleREG.test(ctx.path)) { return next(); } const id = ctx.path.replace(moduleREG, ""); ctx.type = "js"; const content = fs.readFileSync(vueResolved[id], "utf8"); ctx.body = content; }); }
exports.moduleResolvePlugin = moduleResolvePlugin;
function resolveVue(root) { const compilerPkgPath = path.join( root, "node_modules", "@vue/compiler-sfc/package.json" ); const compilerPkg = require(compilerPkgPath); const compilerPath = path.join( path.dirname(compilerPkgPath), compilerPkg.main ); 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 { compiler: compilerPath, "@vue/runtime-dom": runtimeDomPath, "@vue/runtime-core": runtimeCorePath, "@vue/reactivity": reactivityPath, "@vue/shared": sharedPath, vue: runtimeDomPath, }; }
6. vuePlugins
const fs = require("fs");
const { resolveVue } = require("./utils");
const defaultExportRE = /((?:^|\n|;)\s*)export default/;
function vuePlugin({ app, root }) {
app.use(async (ctx, next) => {
if (!ctx.path.endsWith(".vue")) {
return next();
}
const filePath = path.join(root, ctx.path);
const content = fs.readFileSync(filePath, "utf8");
let { parse, compileTemplate } = require(resolveVue(root).compiler); //获取vue的解析和模版编译
let { descriptor } = parse(content);
if (!ctx.query.type) {
let code = "";
if (descriptor.script) {
let content = descriptor.script.content;
let replaced = content.replace(defaultExportRE, '$1const __script =')
code += replaced
}
if (descriptor.template) { 将路径加上type=template参数,再次发起请求
const templateRequest = ctx.path + '?type=template'
code += `\nimport {render as __render} from ${JSON.stringify(templateRequest)}`
code += `\n__script.render = __render`
}
ctx.type = 'js'
code += `\nexport default __script`
ctx.body = code
}
if (ctx.query.type == 'template') { type=template时利用compileTemplate将模版解析成render函数
ctx.type = 'js'
let content = descriptor.template.content
const { code } = compileTemplate({ source: content })
ctx.body = code
}
});
}
exports.vuePlugin = vuePlugin;
7. htmlRewritePlugin
const { readBody } = require('./utils')
function htmlRewritePlugin({ app, root }) {
const inject = `
<script>
window.process = {}
process.env = {
NODE_ENV: "development"
}
</script> `
app.use(async (ctx, next) => {
await next()
if (ctx.response.is('html')) {
const html = await readBody(ctx.body)
ctx.body = html.replace(/<head>/, `$&${inject}`)
}
})
}
exports.htmlRewritePlugin = htmlRewritePlugin
此模块主要是增加环境变量, 然后vite源码中HRM热更新也是在此处替换进去。热更新主要就是借助websocket通过监听源文件的变动,通过不同的命令通知浏览器进行对应的改变。
8. 执行文件
/bin/index.js
#!/usr/bin/env node //设置执行环境
const createServer = require('../index')
createServer().listen(4000, () => {
console.log('cx-vite: start')
})
然后在package.json里面增加一条命令
"bin": {
"cx-vite": './bin/index.js'
}
在通过yarn link 关联到全局,这样你就可以在你的vite-app通过此cx-vite 命令启动。