法語Vite(輕量,輕快)vite
是一個基於 Vue3
單文件組件的非打包開發服務器,它作到了本地快速開發啓動、實現按需編譯、再也不等待整個應用編譯完成的功能做用。html
對於
Vite
的描述:針對Vue
單頁面組件的無打包開發服務器,能夠直接在瀏覽器運行請求的vue
文件。前端
面向現代瀏覽器,
Vite
基於原生模塊系統ESModule
實現了按需編譯,而在webpack
的開發環境卻很慢,是由於其開發時須要將進行的編譯放到內存中,打包全部文件。vue
Vite
有如此多的優勢,那麼它是如何實現的呢?node
Vite
的實現原理咱們先來總結下Vite
的實現原理:react
Vite
在瀏覽器端使用的是 export import 方式導入和導出的模塊;
vite
同時實現了按需加載;
Vite
高度依賴module script特性。
實現過程以下:webpack
koa
中間件中獲取請求 body;
ast
並拿到 import 內容;
npm
模塊;
"vue" => "/@modules/vue"
將要處理的template,script,style等所需依賴以http
請求的形式、經過query參數的形式區分,並加載SFC
(vue單文件)文件各個模塊內容。es6
接下來將本身手寫一個Vite
來實現相同的功能:web
Vite
實現Vite
的環境須要es-module-lexer
、koa
、koa-static
、magic-string
模塊搭建:npm
npm install es-module-lexer koa koa-static magic-string
複製代碼
這些模塊的功能是:json
koa
、
koa-static
是
vite
內部使用的服務框架;
es-module-lexer
用於分析ES6
import
語法;
magic-string
用來實現重寫字符串內容。
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); 複製代碼
用於處理項目中的靜態資源:
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目錄下的文件能夠直接被訪問
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 複製代碼
/@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
模塊
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 = htmlRewritePlugin
複製代碼
amp;${devInjection}`) } }) } exports.htmlRewritePlugin = htmlRewritePlugin 複製代碼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 = htmlRewritePlugin
複製代碼
在
htm
l的head標籤中注入腳本
.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
方法中進行使用
到這裏,基本的一個Vite
就實現了。總結一下就是:經過Koa服務,實現了按需讀取文件,省掉了打包步驟,以此來提高項目啓動速度,這中間包含了一系列的處理,諸如解析代碼內容、靜態文件讀取、瀏覽器新特性實踐等等。
其實Vite
的內容遠不止於此,這裏咱們實現了非打包開發服務器,那它是如何作到熱更新的呢,下次將手把手實現Vite
熱更新原理~
本文使用 mdnice 排版