vite工作原理

80 阅读2分钟

前言

我们知道webpack是根据文件依赖生成依赖树,并将资源打包后放到内存来提供服务,而vite则可以秒级启动项目,那它是如何做到的呢,今天我们来一探究竟。

vue-dev-server

原理

import Vue from 'vue'
import App from './test.vue'
import './index.css'

new Vue({
  render: h => h(App)
}).$mount('#app')

熟悉ES6的同学应该知道,浏览器对于type="module"类型的脚本中的import会发起一个http请求,但是浏览器并不认识Vue,也不认识*.vue文件,对于*.css等类型的资源也不认识,那怎么办呢?

解决方案

vue-dev-server文档,其中提到下面几点:

  1. 原生ES module直接导入,无需build
  2. 拦截*.vue请求并实时编译
  3. 有ES module的库直接从CDN获取
  4. *.js文件中导入的npm包,即时重写路径以指向本地(本地即node_modules里)

由此我们也可以看出,对于npm包需要重写路径指向本地node_modules,对于js文件需要对里面的import进行处理,对于vue文件则需要即时编译,对于css则动态创建style标签。

实施

后台启动一个express服务器,拦截前端请求

  • js
const recast = require('recast')
const isPkg = require('validate-npm-package-name')

function transformModuleImports(code) {
  const ast = recast.parse(code)
  recast.types.visit(ast, {
    visitImportDeclaration(path) {
      const source = path.node.source.value
      if (!/^\.\/?/.test(source) && isPkg(source)) {
        path.node.source = recast.types.builders.literal(`/__modules/${source}`)
      }
      this.traverse(path)
    }
  })
  return recast.print(ast).code
}

exports.transformModuleImports = transformModuleImports

对于js里的npm包会加上/__modules前缀(比如import Vue from "/__modules/vue"),后续拦截从本地node_modules里找

  • npm包
if (req.path.startsWith('/__modules/')) {
  const key = parseUrl(req).pathname
  const pkg = req.path.replace(/^\/__modules\//, '')

  let out = await tryCache(key, false) // Do not outdate modules
  if (!out) {
    out = (await loadPkg(pkg)).toString()
    cacheData(key, out, false) // Do not outdate modules
  }

  send(res, out, 'application/javascript')
}
 ---------------------------------------------------------------   
const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)

async function loadPkg(pkg) {
  if (pkg === 'vue') {
    const dir = path.dirname(require.resolve('vue'))
    const filepath = path.join(dir, 'vue.esm.browser.js')
    return readFile(filepath)
  }
}

exports.loadPkg = loadPkg

对于npm包需要到node_modules下vue/package.json中拿到modules字段值的值,并读取其中的文件内容

  • vue文件
async function bundleSFC (req) {
    const { filepath, source, updateTime } = await readSource(req)
    const descriptorResult = compiler.compileToDescriptor(filepath, source)
    const assembledResult = vueCompiler.assemble(compiler, filepath, {
      ...descriptorResult,
      script: injectSourceMapToScript(descriptorResult.script),
      styles: injectSourceMapsToStyles(descriptorResult.styles)
    })
    return { ...assembledResult, updateTime }
}

对于vue文件,需要@vue/component-compiler进行即时编译为浏览器支持的js

  • css文件
if(url.endsWith('.css')){
    const p = path.resolve(__dirname,url.slice(1))
    const file = fs.readFileSync(p,'utf-8')
    const content = `
    const css = "${file.replace(/\n/g,'')}"
    let link = document.createElement('style')
    link.setAttribute('type', 'text/css')
    document.head.appendChild(link)
    link.innerHTML = css
    export default css
    `
    ctx.type = 'application/javascript'
    ctx.body = content
}

热更新

为了实现HMR需要在客户端和服务端各启动一个websocket,服务端通过chokidar监听文件变化并通知客户端 server:

const watcher = chokidar.watch(appRoot, {
    ignored: ['**/node_modules/**', '**/.git/**'],
    ignoreInitial: true,
    ignorePermissionErrors: true,
    disableGlobbing: true,
});

 ws.send({
    type: 'update',
    updates: [
      {
        type: 'js-update',
        timestamp,
        path: getShortName(file, appRoot),
        acceptedPath: getShortName(file, appRoot),
      },
    ],
  });

客户端收到消息重新获取变更的文件 client:

async function handleMessage(payload: any) {
  switch (payload.type) {
    case 'connected':
      console.log(`[vite] connected.`);
      setInterval(() => socket.send('ping'), 30000);
      break;
    case 'update':
      payload.updates.forEach((update: Update) => {
        if (update.type === 'js-update') {
          fetchUpdate(update);
        } 
      });
      break;
  }
}

预构建

对于分散的模块,为了减少http请求数量,vite通过 预构建合并成一个模块,从而只会有一个请求。已预构建的依赖请求使用 HTTP 头 max-age=31536000, immutable 进行强缓存。

总结

vite对比webpack确实有更好的开发体验,充分利用了浏览器对es6新特性的支持,当然其中也不乏esbuild解析js时的高性能,通过对vite工作原理的简单分析,我们对整个vue项目怎么在浏览器里呈现有了更感性的认识。