共计 7308 个字符,预计需要花费 19 分钟才能阅读完成。
一. 什么是 Vite?
法语 Vite(轻量,轻快)vite
是一个基于 Vue3
单文件组件的非打包开发服务器,它做到了本地疾速开发启动、实现按需编译、不再期待整个利用编译实现的性能作用。
对于
Vite
的形容:针对Vue
单页面组件的无打包开发服务器,能够间接在浏览器运行申请的vue
文件。面向古代浏览器,
Vite
基于原生模块零碎ESModule
实现了按需编译,而在webpack
的开发环境却很慢,是因为其开发时须要将进行的编译放到内存中,打包所有文件。
Vite
有如此多的长处,那么它是如何实现的呢?
二.Vite
的实现原理
咱们先来总结下 Vite
的实现原理:
Vite
在浏览器端应用的是 export import 形式导入和导出的模块;vite
同时实现了按需加载;Vite
高度依赖 module script 个性。
实现过程如下:
- 在
koa
中间件中获取申请 body; - 通过 es-module-lexer 解析资源
ast
并拿到 import 内容; - 判断 import 的资源是否是
npm
模块; - 返回解决后的资源门路:
"vue" => "/@modules/vue"
将要解决的 template,script,style 等所需依赖以 http
申请的模式、通过 query 参数的模式辨别,并加载 SFC
(vue 单文件) 文件各个模块内容。
接下来将本人手写一个 Vite
来实现雷同的性能:
三. 手把手实现Vite
1. 装置依赖
实现 Vite
的环境须要 es-module-lexer
、koa
、koa-static
、magic-string
模块搭建:
npm install es-module-lexer koa koa-static magic-string
这些模块的性能是:
koa
、koa-static
是vite
外部应用的服务框架;es-module-lexer
用于剖析 ES6import
语法;magic-string
用来实现重写字符串内容。
2. 根本构造搭建
Vite
须要搭建一个 koa
服务:
const Koa = require('koa');
function createServer() {const app = new Koa();
const root = process.cwd();
// 构建上下文对象
const context = {
app,
root
}
app.use((ctx, next) => {
// 扩大 ctx 属性
Object.assign(ctx, context);
return next();});
const resolvedPlugins = [ ];
// 顺次注册所有插件
resolvedPlugins.forEach(plugin => plugin(context));
return app;
}
createServer().listen(4000);
3.Koa 动态服务配置
用于解决我的项目中的动态资源:
const {serveStaticPlugin} = require('./serverPluginServeStatic');
const resolvedPlugins = [serveStaticPlugin];
const path = require('path');
function serveStaticPlugin({app,root}){
// 以以后根目录作为动态目录
app.use(require('koa-static')(root));
// 以 public 目录作为根目录
app.use(require('koa-static')(path.join(root,'public')))
}
exports.serveStaticPlugin = serveStaticPlugin;
目标是让当前目录下的文件和 public 目录下的文件能够间接被拜访
4. 重写模块门路
const {moduleRewritePlugin} = require('./serverPluginModuleRewrite');
const resolvedPlugins = [
moduleRewritePlugin,
serveStaticPlugin
];
const {readBody} = require("./utils");
const {parse} = require('es-module-lexer');
const MagicString = require('magic-string');
function rewriteImports(source) {let imports = parse(source)[0];
const magicString = new MagicString(source);
if (imports.length) {for (let i = 0; i < imports.length; i++) {const { s, e} = imports[i];
let id = source.substring(s, e);
if (/^[^\/\.]/.test(id)) {id = `/@modules/${id}`;
// 批改门路减少 /@modules 前缀
magicString.overwrite(s, e, id);
}
}
}
return magicString.toString();}
function moduleRewritePlugin({app, root}) {app.use(async (ctx, next) => {await next();
// 对类型是 js 的文件进行拦挡
if (ctx.body && ctx.response.is('js')) {
// 读取文件中的内容
const content = await readBody(ctx.body);
// 重写 import 中无奈辨认的门路
const r = rewriteImports(content);
ctx.body = r;
}
});
}
exports.moduleRewritePlugin = moduleRewritePlugin;
对
js
文件中的import
语法进行门路的重写,改写后的门路会再次向服务器拦挡申请
读取文件内容:
const {Readable} = require('stream')
async function readBody(stream) {if (stream instanceof Readable) { //
return new Promise((resolve, reject) => {
let res = '';
stream
.on('data', (chunk) => res += chunk)
.on('end', () => resolve(res));
})
}else{return stream.toString()
}
}
exports.readBody = readBody
5. 解析 /@modules
文件
const {moduleResolvePlugin} = require('./serverPluginModuleResolve');
const resolvedPlugins = [
moduleRewritePlugin,
moduleResolvePlugin,
serveStaticPlugin
];
const fs = require('fs').promises;
const path = require('path');
const {resolve} = require('path');
const moduleRE = /^\/@modules\//;
const {resolveVue} = require('./utils')
function moduleResolvePlugin({app, root}) {const vueResolved = resolveVue(root)
app.use(async (ctx, next) => {
// 对 /@modules 结尾的门路进行映射
if(!moduleRE.test(ctx.path)){return next();
}
// 去掉 /@modules/ 门路
const id = ctx.path.replace(moduleRE,'');
ctx.type = 'js';
const content = await fs.readFile(vueResolved[id],'utf8');
ctx.body = content
});
}
exports.moduleResolvePlugin = moduleResolvePlugin;
将 /@modules 结尾的门路解析成对应的实在文件,并返回给浏览器
const path = require('path');
function resolveVue(root) {const compilerPkgPath = path.resolve(root, 'node_modules', '@vue/compiler-sfc/package.json');
const compilerPkg = require(compilerPkgPath);
// 编译模块的门路 node 中编译
const compilerPath = path.join(path.dirname(compilerPkgPath), compilerPkg.main);
const resolvePath = (name) => path.resolve(root, 'node_modules', `@vue/${name}/dist/${name}.esm-bundler.js`);
// dom 运行
const runtimeDomPath = resolvePath('runtime-dom')
// 外围运行
const runtimeCorePath = resolvePath('runtime-core')
// 响应式模块
const reactivityPath = resolvePath('reactivity')
// 共享模块
const sharedPath = resolvePath('shared')
return {
vue: runtimeDomPath,
'@vue/runtime-dom': runtimeDomPath,
'@vue/runtime-core': runtimeCorePath,
'@vue/reactivity': reactivityPath,
'@vue/shared': sharedPath,
compiler: compilerPath,
}
}
编译的模块应用
commonjs
标准, 其余文件均应用es6
模块
6. 解决 process
的问题
浏览器中并没有 process 变量,所以咱们须要在 html
中注入 process 变量
const {htmlRewritePlugin} = require('./serverPluginHtml');
const resolvedPlugins = [
htmlRewritePlugin,
moduleRewritePlugin,
moduleResolvePlugin,
serveStaticPlugin
];
const {readBody} = require("./utils");
function htmlRewritePlugin({root,app}){
const devInjection = `
<script>
window.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>/,`const { readBody} = require("./utils");function htmlRewritePlugin({root,app}){const devInjection = ` <script> window.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>/,`$&${devInjection}`) } })}exports.htmlRewritePlugin = htmlRewritePluginamp;${devInjection}`)
}
})
}
exports.htmlRewritePlugin = htmlRewritePlugin
在
htm
l 的 head 标签中注入脚本
7. 解决 .vue
后缀文件
const {vuePlugin} = require('./serverPluginVue')
const resolvedPlugins = [
htmlRewritePlugin,
moduleRewritePlugin,
moduleResolvePlugin,
vuePlugin,
serveStaticPlugin
];
const path = require('path');
const fs = require('fs').promises;
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();
}
// vue 文件解决
const filePath = path.join(root, ctx.path);
const content = await fs.readFile(filePath, 'utf8');
// 获取文件内容
let {parse, compileTemplate} = require(resolveVue(root).compiler);
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) {
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') {
ctx.type = 'js';
let content = descriptor.template.content;
const {code} = compileTemplate({source: content});
ctx.body = code;
}
})
}
exports.vuePlugin = vuePlugin;
在后端将.vue 文件进行解析成如下后果
import {reactive} from '/@modules/vue';
const __script = {setup() {let state = reactive({count:0});
function click(){state.count+= 1}
return {
state,
click
}
}
}
import {render as __render} from "/src/App.vue?type=template"
__script.render = __render
export default __script
import {toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue"
export function render(_ctx, _cache) {return (_openBlock(), _createBlock(_Fragment, null, [_createVNode("div", null, "计数器:" + _toDisplayString(_ctx.state.count), 1 /* TEXT */),
_createVNode("button", {onClick: _cache[1] || (_cache[1] = $event => (_ctx.click($event)))
}, "+")
], 64 /* STABLE_FRAGMENT */))
}
解析后的后果能够间接在
createApp
办法中进行应用
8. 小结
到这里,根本的一个 Vite
就实现了。总结一下就是:通过 Koa 服务,实现了按需读取文件,省掉了打包步骤,以此来晋升我的项目启动速度,这两头蕴含了一系列的解决,诸如解析代码内容、动态文件读取、浏览器新个性实际等等。
其实 Vite
的内容远不止于此,这里咱们实现了非打包开发服务器,那它是如何做到热更新的呢,下次将手把手实现 Vite
热更新原理~
本文应用 mdnice 排版