【一】尤大神都說Vite香,讓我來手把手分析Vite原理

一.什麼是Vite?

法語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;
  • 經過 es-module-lexer 解析資源 ast 並拿到 import 內容;
  • 判斷 import 的資源是不是 npm 模塊;
  • 返回處理後的資源路徑: "vue" => "/@modules/vue"

將要處理的template,script,style等所需依賴以http請求的形式、經過query參數的形式區分,並加載SFC(vue單文件)文件各個模塊內容。es6

接下來將本身手寫一個Vite來實現相同的功能:web

三.手把手實現Vite

1.安裝依賴

實現Vite的環境須要es-module-lexerkoakoa-staticmagic-string模塊搭建:npm

npm install es-module-lexer koa koa-static magic-string
複製代碼

這些模塊的功能是:json

  • koakoa-staticvite內部使用的服務框架;
  • es-module-lexer 用於分析ES6 import語法;
  • 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 = 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
複製代碼

html的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熱更新原理~

前端優選
歡迎關注【前端優選】公衆號或添加我的VX:abcdarin1992,獲取更多前端技能!

本文使用 mdnice 排版

相關文章
相關標籤/搜索