vite中的dev-dev-server源码解读
dev-dev-server
从文档中的注释,引用谷歌翻译。作用大致如下:
浏览器请求导入作为原生 ES 模块导入 - 没有捆绑。
服务器拦截对 *.vue 文件的请求,即时编译它们,然后将它们作为 JavaScript 发回。
对于提供在浏览器中工作的 ES 模块构建的库,只需直接从 CDN 导入它们。
导入到 .js 文件中的 npm 包(仅包名称)会即时重写以指向本地安装的文件。 目前,仅支持 vue 作为特例。 其他包可能需要进行转换才能作为本地浏览器目标 ES 模块公开。
通过图上内容,简单分析:通过一个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" 复制代码
而转换函数readSource
和transformModuleImports
作用已经在注释中标注出了,内部就是使用了一些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
拦截 .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; } 复制代码
上面的template
、script
、styles
也就是vue文件中模板编译过后的返回内容,再查看当前text.vue
文件输出,可知道上面DescriptorCompileResult
中的我们关心的各个返回参数的内容
拷贝出来内容如下
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
就是将script
和style
中的source code
转化成的base64
。可以通过解码的形式拿到源文件代码。比如下面这段
eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkQ6XFxjb2RlXFxkZW1vXFxzb3VyY2VDb2RlXFx2dWUtZGV2LXNlcnZlci1hbmFseXNpc1xcdnVlLWRldi1zZXJ2ZXJcXHRlc3RcXHRlc3QudnVlIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFlQTtFQUNBLFVBQUE7QUFDQSIsImZpbGUiOiJ0ZXN0LnZ1ZSIsInNvdXJjZXNDb250ZW50IjpbIjx0ZW1wbGF0ZT5cbiAgPGRpdj57eyBtc2cgfX08L2Rpdj5cbjwvdGVtcGxhdGU+XG5cbjxzY3JpcHQ+XG5leHBvcnQgZGVmYXVsdCB7XG4gIGRhdGEoKSB7XG4gICAgcmV0dXJuIHtcbiAgICAgIG1zZzogJ0hpIGZyb20gdGhlIFZ1ZSBmaWxlISdcbiAgICB9XG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48c3R5bGUgc2NvcGVkPlxuZGl2IHtcbiAgY29sb3I6IHJlZDtcbn1cbjwvc3R5bGU+XG4iXX0= 复制代码
然后可以到base64解码去进行解码查看内容
有没有猜出来是干嘛用的?既然都能拿到vue文件中的template
、script
、style
,就可以进行sourcemap输出了呀。因为浏览器本身是识别不了vue文件的,但是我们又看不懂编译过后的文件,所以chrome
的devtool
对这里做了特殊处理。devtool
会将//# sourceMappingURL
作为特殊注释,自动生成sourceMap文件。方便我们查看。
通过删除这段注释也能发现source
资源中的文件变少了
而加上//# sourceMappingURL base64
注释,则又自动生成了文件。
在chrome devtool文档上也找到了具体的说明
而/*# sourceMappingURL
是 Source 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对不同构建版本的解释
最后
对源码感兴趣的小伙伴也可以加入若川组织的源码共读活动
作者:Wailen
链接:https://juejin.cn/post/7042611367433469983
玩站网免费分享SEO网站优化 技术及文章 伪原创工具 https://www.237it.com/