一.什么是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 = __renderexport 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 排版