揭祕 Vite 的原理

前言

Vite 如今正在瘋狂的更新,目前在 Beta 中,可能很快就會發布 1.0。javascript

Vite 是什麼

Vite,一個基於瀏覽器原生 ES imports 的開發服務器。利用瀏覽器去解析 imports,在服務器端按需編譯返回,徹底跳過了打包這個概念,服務器隨起隨用。同時不只有 Vue 文件支持,還搞定了熱更新,並且熱更新的速度不會隨着模塊增多而變慢。針對生產環境則能夠把同一份代碼用 rollup 打包。雖然如今還比較粗糙,但這個方向我以爲是有潛力的,作得好能夠完全解決改一行代碼等半天熱更新的問題。css

  • 快速冷啓動服務器
  • 即時熱模塊更換(HMR)
  • 真正的按需編譯

天生的懶加載呀!html

Javascript 模塊

首先,你須要把 type="module" 放到 <script> 標籤中, 來聲明這個腳本是一個模塊:vue

<script type="module" src="main.js"></script>
複製代碼

script.typemodule 時,經過 srcimport 導入的文件會發送 http 請求。java

攔截請求

Vite 會攔截這些請求,並對請求的文件進行特殊的處理。node

import Vue from 'vue'
複製代碼

當經過 import 試圖導入 node_modules內的文件時,Vite 會對路徑進行替換,由於在瀏覽器中只有 相對路徑絕對路徑webpack

import Vue from '/@modules/vue'
複製代碼

代碼實現

// server.js
 const Koa = require('koa');  const fs = require('fs');  const path = require('path');   const app = new Koa();   app.use(async (ctx) => {  const {  request: { url, query }  } = ctx;   if (url == '/') {  // 返回靜態資源  ctx.type = 'text/html';  ctx.body = fs.readFileSync('./index.html', 'utf-8');  }   if (url.endsWith('.js')) {  // 處理 js 文件  const p = path.resolve(__dirname, url.slice(1));  const res = fs.readFileSync(p, 'utf-8');  ctx.type = 'application/javascript';  // 返回替換路徑後的文件  ctx.body = rewriteImports(res);  }  });   function rewriteImports(content) {  return content.replace(/from ['|"]([^'"]+)['|"]/g, function ($0, $1) {  // 要訪問 node_modules 裏的文件  if ($1[0] !== '.' && $1[1] !== '/') {  return `from '/@modules/${$1}'`;  } else {  return $0;  }  });  }   app.listen(3000, function () {  console.log('success listen 3000');  });  複製代碼

解析 /@modules

接下來就是要把 /@modules 開頭的路徑解析爲真正的文件地址,而且返回給瀏覽器。以前是 webpack 幫咱們作了這件事。git

經過 import 導入的文件 webpack 會去 package.json 文件內找 moduel 屬性。github

{
 "license": "MIT",  "main": "index.js",  "module": "dist/vue.runtime.esm-bundler.js",  "name": "vue",  "repository": {  "type": "git",  "url": "git+https://github.com/vuejs/vue-next.git"  },  "types": "dist/vue.d.ts",  "unpkg": "dist/vue.global.js",  "version": "3.0.0-beta.15"  } 複製代碼

咱們只須要把這個 dist/vue.runtime.esm-bundler.js 地址的文件返回就好。web

代碼實現

if (url.startsWith('/@modules/')) {
 // 找到 node_modules 內的文件夾  const prefix = path.resolve(  __dirname,  'node_modules',  url.replace('/@modules/', '')  );  // 獲取 package.json 內的 module 屬性  const module = require(prefix + '/package.json').module;  const p = path.resolve(prefix, module);  // 讀取文件  const res = fs.readFileSync(p, 'utf-8');  ctx.type = 'application/javascript';  // 讀取的文件內還經過 import 導入了其餘的依賴,繼續把路徑替換爲 /@modules/  ctx.body = rewriteImports(res);  } 複製代碼

解析單文件組件

你們都知道 vue 文件包含了三個部分,分別是 template script style

Vite 對這幾個部分分別進行了處理。

script
script
css
css
template
template

接下來咱們就要實現對這幾個部分的處理。

處理 script

@vue/compiler-sfc 是用來解析單文件組件的,就像是 vue-loader 作的事情。

它解析的結果就像是下面這樣。

代碼實現

const compilerSfc = require('@vue/compiler-sfc');
  if (url.includes('.vue')) {  const p = path.resolve(__dirname, url.slice(1));  const { descriptor } = compilerSfc.parse(fs.readFileSync(p, 'utf-8'));  if (!query.type) {  ctx.type = 'application/javascript';  ctx.body = `  // 拿到 script 的內容  const __script = ${descriptor.script.content.replace('export default ', '')}   // 若是有 style 就發送請求獲取 style 的部分  ${descriptor.styles.length ? `import "${url}?type=style"` : ''}   // 發送請求獲取 template 的部分  import { render as __render } from "${url}?type=template"   // 渲染 template 的內容  __script.render = __render;  export default __script;  `;  }  } 複製代碼

處理 template

@vue/compiler-dom 是用來編譯 template 的。

由於返回給瀏覽器的 vueruntime 版本,是沒有 編譯器 的,全部要在服務端編譯後返回給瀏覽器。

const compilerDom = require('@vue/compiler-dom');
  ...  if (query.type === 'template') {  const template = descriptor.template;  // 在服務端編譯 template 而且返回  const render = compilerDom.compile(template.content, {  mode: 'module',  }).code;  ctx.type = 'application/javascript';  ctx.body = render;  } 複製代碼

處理 style

style 的處理有一丟丟特殊,能夠看到返回的內容中調用了 updateStyle 方法,在 Vite 中是把它放在了 熱更新 的模塊中,在這裏咱們尚未實現熱更新,因此先 hash 下,在 client 實現該功能。

// server.js
 if (query.type === 'style') {  const styleBlock = descriptor.styles[0];  ctx.type = 'application/javascript';  ctx.body = `  const css = ${JSON.stringify(styleBlock.content)};  updateStyle(css);  export default css;  `;  } 複製代碼

方法1使用了 可構造樣式表 在這裏放兩個資料供你們參考。

方法2就不 balabala 了。

<body>
 <div id="app"></div>  <script>  // hash: 規避 shared 文件內的環境判斷  window.process = {  env: {  NODE_ENV: 'dev',  }  };   function updateStyle(content) {  // 方法1   let style = new CSSStyleSheet();  style.replaceSync(content);  document.adoptedStyleSheets = [  ...document.adoptedStyleSheets,  style,  ];   // 方法2  let style = document.createElement('style')  style.setAttribute('type', 'text/css')  style.innerHTML = content  document.head.appendChild(style)  }  </script>  <script type="module" src="./main.js"></script>  </body> 複製代碼

完整代碼

github.com/18zili/vite…

結語

終於等到你~ 還好我沒放棄~

往期文章

本文使用 mdnice 排版

相關文章
相關標籤/搜索