vite中的dev-dev-server源码解读

dev-dev-server

从文档中的注释,引用谷歌翻译。作用大致如下:

  • 浏览器请求导入作为原生 ES 模块导入 - 没有捆绑。
  • 服务器拦截对 *.vue 文件的请求,即时编译它们,然后将它们作为 JavaScript 发回。
  • 对于提供在浏览器中工作的 ES 模块构建的库,只需直接从 CDN 导入它们。
  • 导入到 .js 文件中的 npm 包(仅包名称)会即时重写以指向本地安装的文件。 目前,仅支持 vue 作为特例。 其他包可能需要进行转换才能作为本地浏览器目标 ES 模块公开。

image.png 通过图上内容,简单分析:通过一个server服务,拦截浏览器对资源的请求,也就是route,然后对各个模块module进行处理,最后响应资源文件。

vueMiddleware

cache

为了优化加载速度,当浏览器第二次请求资源文件时,vue-dev-server都是直接从内存中拿到缓存文件直接响应给浏览器。缓存主要是通过lru-cache这个库,用于在内存中管理缓存数据,并且支持LRU算法。可以让程序不依赖任何外部数据库实现缓存管理。

const LRU = require('lru-cache')
const cache = new LRU({
  max: 500, // 指定缓存大小
  length: function (n, key) { return n * 2 + key.length }
})
复制代码

简单说下LRU,引用百度百科的原话:

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

下面很多对缓存资源的增、删、改、查都是基于cache进行。

拦截.js文件

vue项目中的入口文件是main.js,而main.js文件中一般第一行都是下面这行代码。所以首先就要对.js文件进行处理。

import Vue from 'vue'
复制代码

这里其实就是对vue的引用做的一个优化。先来看源代码

 else if (req.path.endsWith('.js')) {
      const key = parseUrl(req).pathname // main.js
      let out = await tryCache(key) // 读取页面缓存

      if (!out) {
        // transform import statements
        const result = await readSource(req) // 读取文件资源,返回filepath、内容source、updateTime
        out = transformModuleImports(result.source) // 改变引用路径,从import vue 改变成 import /__modules/vue
        cacheData(key, out, result.updateTime) // 缓存
      }

      send(res, out, 'application/javascript') // 响应浏览器资源
    }
复制代码

也就是拿到当前的main.js文件,然后改变vue的引用路径,如下

// old
import vue from 'vue'
// new
import Vue from "/__modules/vue"
复制代码

而转换函数readSourcetransformModuleImports作用已经在注释中标注出了,内部就是使用了一些node模块正则处理文件资源

拦截 import vue

既然转换了vue的引用,那这个/__modules/vue路径下的vue又怎么去获取呢?简单来说,vue-dev-server是通过拦截这个路径资源请求,从而做出资源更改,然后响应浏览器,返回正确的vue文件。

  // 对/__modules/请求的拦截
  else if (req.path.startsWith('/__modules/')) {
      const key = parseUrl(req).pathname // '/__modules/vue'
      const pkg = req.path.replace(/^\/__modules\//, '') // vue

      // 这里打印出来out是vue缓存资源,第一次没缓存通过loadPkg加载vue
      let out = await tryCache(key, false)
      if (!out) {
        out = (await loadPkg(pkg)).toString()
        cacheData(key, out, false)
      }

      send(res, out, 'application/javascript')
    }
复制代码

重点在于loadPkg这个模块是干嘛的,还是先看下源码

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')) // 返回vue目录
    const filepath = path.join(dir, 'vue.esm.browser.js') // 拼接路径
    // 返回vue的es module完整版本,可以直接用于浏览器
    return readFile(filepath)
  }
  else {
    // TODO
    // check if the package has a browser es module that can be used
    // otherwise bundle it with rollup on the fly?
    throw new Error('npm imports support are not ready yet.')
  }
}

exports.loadPkg = loadPkg
复制代码

从源码中可以看到,这个函数作用是当浏览器请求vue资源时,vue-dev-server将vue目录下的vue.esm.browser.js返回给浏览器。。而这个vue.esm.browser.js文件,从名字就能看出来是在浏览器中工作的,通过官方文档也能印证猜想。这个文件不需要编译,浏览器直接就能使用。这样优化了对vue资源的请求。并且第二次访问资源时,是直接通过tryCache函数从缓存中拿。不仅是vue文件,vue-dev-server中所有处理过后的资源文件都是缓存在内存中的,第二次直接从内存中拿。具体可以查看cache

image.png

拦截 .vue文件

处理了main.js过后,之后就是对.vue文件的处理了。主要是通过@vue/component-compiler这个包

@vue/component-compiler

.vue文件浏览器是不可识别的,所以就需要这个编译器,将.vue单文件转化为浏览器可识别的js文件github文档

compiler.createDefaultCompiler

获取编译器实例

const compiler = vueCompiler.createDefaultCompiler()
复制代码

compiler.compileToDescriptor(filename: string, source: string)

参数为文件地址以及源代码内容,根据源代码编译输出每个模块,然后输出如下格式

interface DescriptorCompileResult {
  customBlocks: SFCBlock[]
  scopeId: string
  script?: CompileResult
  styles: StyleCompileResult[]
  template?: TemplateCompileResult & { functional: boolean }
}
// script编译后内容
interface CompileResult {
  code: string
  map?: any
}
// style编译后内容
interface StyleCompileResult {
  code: string
  map?: any
  scoped?: boolean
  media?: string
  moduleName?: string
  module?: any
}
// template模板编译后内容
interface TemplateCompileResult {
  code: string;
  source: string;
  tips: string[];
  errors: string[];
  functional: boolean;
}
复制代码

上面的templatescriptstyles也就是vue文件中模板编译过后的返回内容,再查看当前text.vue文件输出,可知道上面DescriptorCompileResult中的我们关心的各个返回参数的内容

image.png 拷贝出来内容如下

template

'var render = function() {\n  var _vm = this\n  var _h = _vm.$createElement\n  var _c = _vm._self._c || _h\n  return _c("div", [_vm._v(_vm._s(_vm.msg))])\n}\nvar staticRenderFns = []\nrender._withStripped = true\n'
复制代码

script

'//\n//\n//\n//\n\nexport default {\n  data() {\n    return {\n      msg: 'Hi from the Vue file!'\n    }\n  }\n}\n'
复制代码

styles中的code

'\ndiv[data-v-a941da2c] {\n  color: red;\n}\n'
复制代码

通过这些编译后的js,应该可以大致理解@vue/component-compiler的作用了,也就是将vue文件中的template、script、style转化为浏览器能识别的js

vueCompiler.assemble()

assemble 组装输出,会将之前编译的template、script、style组装成一个字符串,返回的是一个对象{ code: string, map?: any }。然后通过send方法响应给浏览器。

  function send(res, source, mime) {
    res.setHeader('Content-Type', mime)
    res.end(source)
  }
  send(res, out.code, 'application/javascript')
复制代码

base64注入的作用

在通过vueCompiler.assemble合并模块时,对script、style重新做了处理。

const assembledResult = vueCompiler.assemble(compiler, filepath, {
      ...descriptorResult,
      // 这里是重新为script和style中的内容注入了一段base64注释
      script: injectSourceMapToScript(descriptorResult.script),
      styles: injectSourceMapsToStyles(descriptorResult.styles)
    })
    return { ...assembledResult, updateTime }
}
function injectSourceMapToScript (script) {
    return injectSourceMapToBlock(script, 'js')
}

function injectSourceMapsToStyles (styles) {
    return styles.map(style => injectSourceMapToBlock(style, 'css'))
}
复制代码

那么这个injectSourceMapToBlock有什么作用呢?查看源代码

function injectSourceMapToBlock (block, lang) {
  const map = Base64.toBase64(
    JSON.stringify(block.map)
  )
  let mapInject

  switch (lang) {
    case 'js': mapInject = `//# sourceMappingURL=data:application/json;base64,${map}\n`; break;
    case 'css': mapInject = `/*# sourceMappingURL=data:application/json;base64,${map}*/\n`; break;
    default: break;
  }
  return {
    ...block,
    code: mapInject + block.code
  }
}
复制代码

简单来说就是为之前转化的script和style注入了一段base64注释,也就是下面这样

// js注入的注释
//# sourceMappingURL=data:application/json;base64, mapData
// css注入的注释
/*# sourceMappingURL=data:application/json;base64, mapData */
复制代码

而其中的mapData就是将scriptstyle中的source code转化成的base64。可以通过解码的形式拿到源文件代码。比如下面这段

eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkQ6XFxjb2RlXFxkZW1vXFxzb3VyY2VDb2RlXFx2dWUtZGV2LXNlcnZlci1hbmFseXNpc1xcdnVlLWRldi1zZXJ2ZXJcXHRlc3RcXHRlc3QudnVlIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFlQTtFQUNBLFVBQUE7QUFDQSIsImZpbGUiOiJ0ZXN0LnZ1ZSIsInNvdXJjZXNDb250ZW50IjpbIjx0ZW1wbGF0ZT5cbiAgPGRpdj57eyBtc2cgfX08L2Rpdj5cbjwvdGVtcGxhdGU+XG5cbjxzY3JpcHQ+XG5leHBvcnQgZGVmYXVsdCB7XG4gIGRhdGEoKSB7XG4gICAgcmV0dXJuIHtcbiAgICAgIG1zZzogJ0hpIGZyb20gdGhlIFZ1ZSBmaWxlISdcbiAgICB9XG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48c3R5bGUgc2NvcGVkPlxuZGl2IHtcbiAgY29sb3I6IHJlZDtcbn1cbjwvc3R5bGU+XG4iXX0=
复制代码

然后可以到base64解码去进行解码查看内容

image.png

有没有猜出来是干嘛用的?既然都能拿到vue文件中的templatescriptstyle,就可以进行sourcemap输出了呀。因为浏览器本身是识别不了vue文件的,但是我们又看不懂编译过后的文件,所以chromedevtool对这里做了特殊处理。devtool会将//# sourceMappingURL作为特殊注释,自动生成sourceMap文件。方便我们查看。

通过删除这段注释也能发现source资源中的文件变少了

image.png

而加上//# sourceMappingURL base64注释,则又自动生成了文件。 image.png

在chrome devtool文档上也找到了具体的说明

image.png

/*# sourceMappingURLSource Map V3的标准

最新的可以使用//# sourceURL=source.coffee

总结

vite-dev-serve内部是通过express启动了一个服务器,然后通过中间件vueMiddleware对资源文件进行处理,先是获取main.js,通过拦截对vue的资源请求,改变返回的资源为vue.esm.browser.js,因为它是能直接在浏览器中访问。之后对.vue文件进行编译,将浏览器识别不了的vue文件中的template script style通过@vue/component-compiler这个包进行各个模块编译,最后通过编译器的assemble方法组装编译过后的template、script、style,组装的同时还做了base64注入处理,方便chrome做sourceMap,最后将资源文件响应给浏览器。并且这些资源都进行了缓存处理,缓存是缓存在内存中。

参考

尤雨溪几年前开发的“玩具 vite”,才100多行代码,却十分有助于理解 vite 原理

Chrome devtool Map Preprocessed Code to Source Code

vue-component-compiler

vue对不同构建版本的解释

最后

对源码感兴趣的小伙伴也可以加入若川组织的源码共读活动

猜你喜欢

转载自juejin.im/post/7042611367433469983
Dev