若川视野 x 源码共读11期:二 - Vue-dev-server master分支源码详析
本文源码,欢迎勘误,交流。
1 Guide
两个分支的启动和基本代码结构已经在前一篇文章中谈到了,本文计划围绕几个问题开展master分支的代码解析:
- master分支对
代码依赖
,vue文件,js文件的处理,其它请求的默认处理; - hmr的实现原理;
- lodash-es的引入和测试;
本文将不谈及master代码的调试方法,做功课时,我感觉typescript调试起来配置比较复杂(可能是我找到的教程太详细了)(实际上是作者这个人比较懒),要花比较多时间进去,导致偏离了原来的目标。
🏷 说明: 当然,通过调试
tsc
编译输出后的javascript代码(dist文件夹)
来观察运行时的形态也是可以的,编译后的代码对可读性也没有特别大的影响。限于篇幅太长了,就先不说了吧。 行吧,其实就是我不想浪费你宝贵的时间(画外音:偷懒就偷懒吧)
2 入口-index.html
在前一篇文章提到,通过http的拦截器,Server端会拦截几种文件的http请求:vue
,js
,请求路径前缀有'/__modules/'、'/__hmrClient'的文件
。
📌一个技巧:
平常我们只是使用webpack-dev-server、vite直接进行开发,我们往往只需要关注
/src
里面的代码写对了没有,而现在,一定要区分清楚,你正在面对的是哪一个部分的代码,dev-server
开发服务器会为你处理你写的代码,而你向dev-server
请求的,是处理好的,你写的那部分代码
2.1 那个真正的入口
在考虑这些文件解析的if
分支以前,我建议先考虑这些问题:
- 这些
/__hmrClient
,/__modules/
的前缀是谁加上去的?是默认有的吗? - 如果阅读过
main.js
,知道Comp.vue
这些文件是从main.js
引入的,而main.js
又是index.html
引入的,但是,server源码中,没有很明显的路径读取index.html
,为什么自动跳入了index.html
了呢?
以上问题的答案就在于第一个请求的路径,究竟是什么?它应该是 /
这个根路径,它并不在上面的几个分支拦截范围以内,因此它执行的是重定向代码:
serve(req, res, {
public: cwd ? path.relative(process.cwd(), cwd) : '/',
rewrites: [{ source: '**', destination: '/index.html' }]
})
转向 index.html 文件,浏览器接收到这个文件并解析,最终发起一个新的请求,请求main.js
。
2.2 不难想到的弊端
这里其实会有一个弊端,就是,它只允许你请求index.html
并从index.html
转向其它文件。若有兴趣,可以添加一个html文件(我是用的是test文件夹中的fixtures文件夹),然后访问http://localhost:3000/[你的文件].html
,会自动转向index.html
。
3 javascript文件的处理
既然入口是main.js文件,那么就从js
文件入手吧!
对js
文件的处理,主要关注这几句代码:
// 拼接路径
const filename = path.join(cwd, pathname.slice(1))
try {
// 读入文件内容
const content = await fs.readFile(filename, 'utf-8')
return sendJS(res, rewrite(content))
} catch (e) {
// 发生问题的处理
}
在这些关键代码当中,又要特别关注下面一句:
return sendJS(res, rewrite(content))
不难看出,这两个函数组成了对js文件的核心处理:sendJS
,rewrite
。
3.1 moduleRewriter.ts和rewirte(content)
rewrite
这个函数,在moduleRewriter.ts
里实现,这个文件中也只实现了这个函数。
打开moduleRewriter
文件,映入眼帘是它的两个import:
import { parse } from '@babel/parser'
import MagicString from 'magic-string'
引用了@babel/parser
,意味着,下面的代码将要用到ast
,引用了magic-string
,意味着,下面的代码需要对字符串进行改写和替换操作。
3.2 ast树的获取
rewirte函数的第一步,就是使用@babel/parser
将传入的代码,转换为ast
语法树:
const ast = parse(source, {
sourceType: 'module',
plugins: [
// by default we enable proposals slated for ES2020.
// full list at https://babeljs.io/docs/en/next/babel-parser#plugins
// this will need to be updated as the spec moves forward.
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
]
}).program.body
① 注释: 可以留意到这里的一段注释:默认来说,调用编译器时,是按ES2020
语法标准对代码进行编译的。这里引用了3个插件:bigInt
、optionalChaining
、nullishCoalescingOperator
,这三个都是ES2020的新特性。如果想要了解这个plugins配置能传入什么值,可以打开注释中提供的地址了解它的详细传值。
② ast语法树: 如果要详细了解ast是怎么编译出来的,需要比较深入地研究编译原理知识(这里有本《武林秘笈》推荐给你),就不在本文中深入探讨了。
③ @babel/parser编译的结果 : 可以通过babel提供的一个线上工具来查看:
3.3 MagicString构造和ast树的处理
关于MagicString的部分,其实不是很重要。这里最重要的,是ast的处理过程:
// 构造MagicString
const s = new MagicString(source)
// 轮询 ast树
ast.forEach((node) => {
if (node.type === 'ImportDeclaration') {
// 针对Import语句的处理
} else if (asSFCScript && node.type === 'ExportDefaultDeclaration') {
// 针对 “形似vue SFC脚本” 的 Export语句的处理
}
})
使用上面babel
提供的工具,你可以跟着它一步一步处理js文件。
① ImportDeclarationa
的处理,会先正则检查你import的目标,是不是由/
、.
这些路径表示的元素开头,如果是,则会将它替换成 "/_modules/"
开头的路径。那么现在就可以知道刚才提到的/_modules/
来自哪里了。
// 正则判断
if (/^[^\.\/]/.test(node.source.value)) {
// 目的: import { foo } from 'vue' --> import { foo } from '/__modules/vue'
/*
从Babel Praser 中返回的结构:
"source": {
"type": "Literal",
"start": 26,
"end": 31,
"value": "vue",
"raw": "'vue'"
}
*/
// MagicString 替换为 `"/__modules/${node.source.value}"`
}
② 形似SFC脚本的Export语句
的处理,顾名思义地想,这段代码应该用于处理vue SFC
文件的script部分,但是我没有理解它这样换一个"壳"的目的是什么,或许答案藏在.vue
文件的处理里吧:
let __script; export default (__script =
// SFC sript
)
4 Vue文件的处理
既然产生了问题,也事不宜迟,马上开始看.vue
的处理吧!这个部分是整个master中最复杂的部分。
目光回到server.ts
,vue文件的拦截处理由下面的代码完成:
else if (pathname.endsWith('.vue')) {
return vueMiddleware(cwd, req, res)
}
而vueMiddleware
函数由vueCompiler.ts
提供,这个文件有179行代码。
4.1 vueMiddleware
这个函数,最关键的部分是:
export async function vueMiddleware(
// 函数的参数
) {
// ...
// 此处代码分析请求的url
// ...
// 使用@vue/compiler-sfc对文件进行解析
const [descriptor] = await parseSFC(
filename,
true /* save last accessed descriptor on the client */
)
if (!query.type) {
// 对请求的url没有type参数的.vue文件的处理
}
if (query.type === 'template') {
// 对请求的url-若存在参数type是'template'的情况的处理
}
if (query.type === 'style') {
// 对请求的url-若存在参数type是'style'的情况的处理
}
}
看到此处,我不禁有下面的疑问:
- query.type是在什么地方赋值的?
- parseSFC返回的descriptor是什么结构?
① 通过开发者工具看.vue
文件请求,在这里,如果能进行代码调试,结合浏览器的F12开发者工具来看文件请求的过程,更容易理解,这是我用test/fixtures
做的一个请求过程的截图:
第一个和第二个请求,刚才已经解释过了,请求了/
根路径后,直接转向了index.html
,并继续请求main.js
,第三个请求,是请求vue
依赖的,第四个请求,是我们需要关心的.vue
文件请求。
从Comp.vue
开始,一共请求了6个文件:
- Comp.vue
- __hmrClient
- Child.vue
- apple.js
- Comp.vue?type=template
- Child.vue?type=template
先来看下Comp.vue
文件的源码:
<template>
<button @click="count ">{{ count }}</button>
<h1>{{b}}</h1>
<Child/>
</template>
<script>
import Child from './Child.vue'
import apple from "./apple.js"
export default {
components: { Child },
setup() {
return {
count: 0,
b: apple.b
}
}
}
</script>
综合上面说到的信息,可以知道:Comp.vue的第一次请求,进入了if(!query.type)
,即没有type的这个分支。它并没有返回Comp.vue这个文件本身的正文或者正文的一部分,而是根据Comp.vue的编译结果,拼接出一个新的JS代码并返回。
✨ 虽说不谈调试运行的具体方法,如果能调试运行的话,会更加清晰一些。
4.2 VueMiddleWare-compileSFCMain
函数
这个函数拼接了一段全新的Javascript代码,并且返回。
前面谈到,第一次的vue文件请求,转入了:
if (!query.type) {
return compileSFCMain(res, descriptor, pathname, query.t as string)
}
这一分支中,调用的正是compileSFCMain
这一函数,这个函数,关键的部分为:
function compileSFCMain(
// 若干参数
) {
// 注入热模块更新客户端
let code = `import "/__hmrClient"\n`
// script部分
if (descriptor.script) {
code = ... // 处理后的、script部分的代码
} else{
code = ... // 另一段代码
}
// styles部分
if (descriptor.styles) {
descriptor.styles.forEach(()=>{
// 处理每个样式
code = ...
})
// 此处处理scoped的样式
}
// template部分
if (descriptor.template) {
code = ...
}
}
添加了注释,比较凌乱,请细心阅读。
从上面的代码可以看出,descriptor正是理解vue文件处理的关键,这个对象是由parseSFC
提供的,而parseSFC
又调用了@vue/compiler-sfc来对代码进行解析。这个descriptor的具体结构在第一小节继续详细讨论。
4.2.1 descriptor的各个部分
虽然我们不知道descriptor到底返回了什么,但是.vue
文件总是有<template>
、<style>
、<script>
三个部分,只要接触过vue开发,都是知道的。
4.2.2 script部分的处理
script的处理代码是这样的:
if (descriptor.script) {
code = rewrite(
descriptor.script.content,
true /* rewrite default export to `script` */
)
} else {
code = `const __script = {}; export default __script`
}
这里调用了我们刚才读过的moduleRewriter.ts
中的rewrite
函数(本文章节:2.2.1),它的asSFCStrpt
参数传入了true
。
读到这里,我仍然不能理解添加这个“壳”:__script = {}; export default __script
的目的是什么。
4.2.3 style部分的处理
对于style部分,代码会自动加入一段import语句,告知浏览器发起请求,请求css部分的正文,值得留意的是?type=style
这个传参,<style>
部分和另外两个不同,可以有多个,因此需要循环:
descriptor.styles.forEach((s, i) => {
// ... 此处判断是不是有标识为scoped(局部) css的判断
// 加入import语句
code = `\nimport ${JSON.stringify(pathname `?type=style&index=${i}${timestamp}`)}`
})
// ... 此处为若有scoped css(局部css)的标识的处理
// 同时通过pathname和hash计算出 scopedId,用于css局部影响
4.2.4 template部分的处理
这个部分和style部分基本一致,也会自动加入一段import,但是它的type传参是?type=template
。这个部分是html部分,会被@vue/compiler-sfc
的compileTemplate
方法编译为一个render函数。
4.2.5 最后
最后,它组装出下面这一段全新的JS,返回给浏览器(以 test/fixture/Comp.vue
为例):
// hmr客户端代码
import "/__hmrClient"
// Comp.vue的script部分
import Child from './Child.vue'
import apple from "./apple.js"
let __script; export default (__script = {
// ...
})
// 告知浏览器,请求Comp.vue的template部分
import { render as __render } from "/Comp.vue?type=template"
__script.render = __render
__script.__hmrId = "/Comp.vue"
4.3 VueMiddleWare-compileSFCStyle
函数
经过前面compileSFCMain
函数之后,浏览器会得到<script>
部分和另外两个部分(<template>
和<style>
)的请求(如果有的话)。
为了实验这个部分,我在test/fixture/Comp.vue
中加入了两段CSS:
<style>
.a {
font-size:14px;
font-weight:bolder;
}
</style>
<style scoped>
.b {
font-weight:bolder;
}
</style>
加入后,再次请求,可以从开发者工具中发现,多出了两个type=style
的http请求,另外,对 Comp.vue
请求的返回,也增加了一些新的代码:
// 在返回的Comp.vue中新增的代码:
import "/Comp.vue?type=style&index=0"
import "/Comp.vue?type=style&index=1"
__script.__scopeId = "data-v-92a6df80"
这里compileSFCStyle
接收到CSS代码后,会依据是否有scoped处理css,会往最后返回的html上,插入一段javascript代码,这段代码会在html上创建一个style标签,并设置它的textContent为@vue/compiler-sfc
的compileStyle
方法解析后的css,我自己给这段javascript加了注释:
const id = "vue-style-${id}-${index}" // style的标签
let style = document.getElementById(id) // 先判断Style
if (!style) {
style = document.createElement('style') // 创建style元素
style.id = id // 放入计算好的id属性
document.head.appendChild(style) // 注入元素
}
style.textContent = ${JSON.stringify(code)} // 注入编译好的css代码
和compileSFCMain
中的style处理(本文2.3.2的style部分)是成对的,它会使用hash-sum
的hash
方法计算一个哈希码,以达到scoped
的效果。
4.4 VueMiddleWare-compileSFCTemplate
函数
有了Style编译函数(compileSFCStyle
)的基础,这个函数读起来也是小儿科,函数的具体实现,不是本文重点,但是要知道的是,它把.vue
中的<template>
编译为一个render函数了,这个也是vue的一个基础知识,所有的vue
最后都会被编译为一个render函数,进行渲染。
4.5 VueMiddleWare-parseSFC
函数
这个函数实际没有什么可以谈论的事物,它最关键的两个东西是:
- 缓存历史版本到内存中,在hmr(热模块替换)对比时,进行新旧版本的对比;
- 利用
nodejs
的readFile
方法读取磁盘上对应的文件,并且将它送入@vue/compiler-sfc
的parse
函数中解析;
// 读取文件并送入"compiler-sfc"
content = await fs.readFile(filename, 'utf-8')
const { descriptor, errors } = parse(content, {
filename
})
// 缓存的读取和返回,用于HMR
const prev = cache.get(filename)
if (saveCache) {
cache.set(filename, descriptor)
}
但是如果往深一步走,探索@vue/comiler-sfc
实际返回的数据结构是什么样子的,初步掌握vue sfc的处理能力,更让人收获满满,为此我搭建了一个最简的实验环境(我在wsl下运行的):
mkdir vue-compiler-sfc
cd vue-compiler-sfc/
yarn init # 此处一直按回车键到结束
yarn add -D @vue/compiler-sfc
※ 📌注意: 这个实验环境使用的
@vue/compiler-sfc
是当时最新的3.2.36
版本,而master分支使用的,是3.0.0-beta.3
,返回值格式会稍有不同。
分别创建两个文件:
- Comp.vue(实际是master分支的
test/fixture/Comp.vue
) - index.js
const fs = require('fs');
const {
parse
} = require('@vue/compiler-sfc');
let content;
try {
// 使用同步API读入文件
content = fs.readFileSync('./Comp.vue', 'utf-8');
}
catch(ex) {
console.error(ex);
}
const parseResult = parse(content);
console.log(parseResult);
运行node index.js
后可以在终端得到:
{
descriptor: {
filename: 'anonymous.vue',
source: '<template>\n'
' <button @click="count ">{{ count }}</button>\n'
' <h1>{{b}}</h1>\n'
' <Child/>\n'
'</template>\n'
'\n'
'<script>\n'
"import Child from './Child.vue'\n"
'import apple from "./apple.js"\n'
'\n'
'export default {\n'
' components: { Child },\n'
' setup() {\n'
' return {\n'
' count: 0,\n'
' b: apple.b\n'
' }\n'
' }\n'
'}\n'
'</script>\n'
'\n'
'<style>\n'
'.a {\n'
' font-size:14px;\n'
'}\n'
'</style>\n'
'\n'
'<style scoped>\n'
' .b {\n'
' font-weight:bolder;\n'
' }\n'
'</style>\n',
template: {
type: 'template',
content: '\n'
' <button @click="count ">{{ count }}</button>\n'
' <h1>{{b}}</h1>\n'
' <Child/>\n',
loc: [Object],
attrs: {},
ast: [Object],
map: [Object]
},
script: {
type: 'script',
content: '\n'
"import Child from './Child.vue'\n"
'import apple from "./apple.js"\n'
'\n'
'export default {\n'
' components: { Child },\n'
' setup() {\n'
' return {\n'
' count: 0,\n'
' b: apple.b\n'
' }\n'
' }\n'
'}\n',
loc: [Object],
attrs: {},
map: [Object]
},
scriptSetup: null,
styles: [ [Object], [Object] ],
customBlocks: [],
cssVars: [],
slotted: false,
shouldForceReload: [Function: shouldForceReload]
},
errors: []
}
留意script
、template
、style
三个字段的值和结构,以了解descriptor
的结构,以及vueMiddleware
的分析过程。
5 读取项目依赖
这是最后一个分支了,这时将目光放回server.ts
,项目依赖的解析部分分支是:
else if (pathname.startsWith('/__modules/')) {
return resolveModule(pathname.replace('/__modules/', ''), cwd, res)
}
读到这里的时候,我开始思考这几个问题:
- 为什么需要这个部分的解析?加入
/__modules/
前缀的目的是什么? - 我们自己开发的代码(src)和开发服务器(devServer)是什么关系?它存在的价值只有热模块替换吗?
- 同为开发server,vue-dev-server的做法和webpack-dev-server的做法有什么不同?在vite中,又是怎么进行这种模块依赖的解析的?
5.1 resolveModule
函数
这个函数接收了3个参数(我自己写的JDoc注释,源代码里没有):
/**
* @param {string} id 被移除了`/__modules/`部分的一个依赖包名
* @param {string} cwd “你的代码”(可以理解为你的src)所在的目录
* @param {ServerResponse} res 当前server对当前请求的响应流
*/
前面在 server.ts
和javascript文件处理
中谈到,如果import
语句引入的依赖名,没有使用路径的标识,就会被识别为一个模块依赖。
这样有个很明显的问题就是:人懒。webpack允许我们通过resolve配置节
使得进行引入的时候,不写后缀:
// 引入a.vue
import AVueComponent from "./a"
但是,vue-dev-server就不行,vite也一样。
5.2 解析模块的过程
函数大体通过下面几个步骤来解析模块依赖:
- 针对
source-map
请求做处理; - 借助
resolve-form
的能力,找到模块的package.json
路径; - 针对
vue
做特殊处理; - 其它依赖,依据
package.json
的module
和main
配置节拼接模块路径,并后续根据这个模块依赖进行后续引入;
5.3 resolve-form
这个包的能力很特殊,通过给定的两个路径参数,它会给你返回正确的文件路径,哪怕这个路径是错误的。
比如引入vue
的时候,传入的参数分别是: -【我的代码路径】/vue-dev-server-master/test/fixtures
vue/package.json
它仍然能解析出vue
的package.json
的正确路径:【我的代码路径】/vue-dev-server-master/node_modules/vue/package.json
。
这是了不得的,具体的实现原理真是让人好奇。
5.4 vue
对于 vue,会直接拿到vue.runtime.esm-browser.js
这个vue打包发布后的版本。
5.5 其它依赖
对于其它文件,会根据模块package.json
的module
和main
配置节来拼接出最后引入的文件的路径:
const pkg = require(modulePath) // 拉取package.json文件
modulePath = path.join(path.dirname(modulePath), pkg.module || pkg.main) // 得到最后的文件路径
一般来说,这是一个js文件,这个js文件会返回给浏览器处理。如果js文件中 import
了其它文件,会继续向devServer发起后续文件的请求,这里是存在不少问题的,我也做了引入 lodash-es
的引入实验,在本文的最后一节会讨论。
6 小小总结-拦截、解析和返回部分
终于结束了server对文件请求拦截和解析部分的阅读,不知道作为读者的你有没有和我一样获益良多的感受。
回过头来看看,你是否思考过作为 “不完全体” 的vue-dev-server(master),和作为 “相对完全体” 的 vite,有多少差距呢?
即使不讨论框架的支持能力,各种语言的支持能力,我觉得至少是有下面这些问题的:
- 假设你引入的依赖,不支持
ESM
语法,只能通过require引入的依赖怎么办? - 不能通过import引入图片(jpg)、样式文件(css),没有拦截处理;
- 还有 5.5 小节略有谈到的,如果你引入的是一个依赖的部分,会造成大量的无用请求,详见最后一节lodash-es的引入实现;
📌 下面的内容可以说是可选的,它们对你理解Server的实现没有很大的影响,它们可以加深你的理解;
7 热模块替换(Hot Moudule Replace)的实现
这个热模块替换当然比不得大型的前端开发者服务器,从总体的实现来看,它分为 WebSocket Server和WebSocket Client两个部分,从功能上分,它分为监听、处理、通知、接收处理这几个部分。
我建议先从Server部分开始看起。
7.1 Server部分
主要有这样的功能:
- 构造WebSocket服务器
- 建立文件侦听器
- 对比文件的变化
- 通知Client端
7.1.1 WebSocket Server的建立
主要由下面的代码进行:
// server为建立的Http Server
const wss = new WebSocket.Server({ server })
const sockets = new Set<WebSocket>()
wss.on('connection', (socket) => {
// connection 事件的处理
// ※ 注意下面这一句,向客户端发送事件:
// socket.send(JSON.stringify({ type: 'connected' }))
})
wss.on('error', (e: Error & { code: string }) => {
// error 事件的处理
})
这个部分比较简单,调用的是ws 提供的相关实现,可以打开链接了解它的具体实现。
7.1.2 文件变更侦听
在server.ts
中,能找到这个代码:
createFileWatcher(cwd, (payload) =>
sockets.forEach((s) => s.send(JSON.stringify(payload)))
)
createFileWatcher
函数在watcher.ts
里实现,接收两个参数:
- 你的src所在的目录
- 发生改变时的回调函数
createFileWatcher
基于chokidar
这个文件监听器来实现的文件监听和响应,这个监听器不知道和nodemon
有什么具体的区别。
7.1.3 文件变更的处理
vue-dev-server
只有两种针对文件的处理:
- vue文件的处理
- 不是vue文件的处理
(emmm, 有点废话。。。)
对于不是Vue的文件变更的处理,只有一种:刷新全部重新加载。
对于vue的文件变更的处理,则会调用parseSFC
函数解析它,经过 “check which part of the file changed”(代码中原有注释,检查哪个部分发生了变化) ,最后向client发出一个通知,client那边的浏览器会重新发起一个 **.vue?type=【类型】&&index=?
型的请求,将最新的代码请求倒浏览器中。在这里你需要回头看看vueCompiler.ts
的实现。
针对vue sfc的部分更新实现
对于<script>
和<template>
部分的比较还是相对比较简单的,比较字符串的异同即可,而对于块属性的变化,则比较@vue/complier-sfc
提供的attrs
字段就可以了:
if (!isEqual(descriptor.script, prevDescriptor.script)) {
// script部分的比较
}
if (!isEqual(descriptor.template, prevDescriptor.template)) {
// template部分的比较
}
而对 样式 <style>
的比较,通过@vue/complier-sfc
返回的,是一个数组,它的处理也相对复杂:
- 查找是不是存在有至少1个
<style>
标签从局部样式转换为全局样式,也可以查找是不是全部<style>
变成了全局样式或者局部样式; - 查找是否存在
<style>
的单个变化更新,这个对比弥补了前面不能查找单个<style>
局部样式、全局样式变化的问题; - 查找是不是移除了一个或多个
<style>
标签,并向Client发送更新通知;
📌对样式的比较,我认为这三个部分紧密地关联在了一起,比较巧妙,建议要多加深一些理解。
文件对比的实现
现在,要仔细读一下isEqual
这个函数:
function isEqual(a: SFCBlock | null, b: SFCBlock | null) {
// ...
}
isEqual函数可以实现<script>
、<style>
、<template>
三个部分的对比,对比的方式可以说非常直接:
if (a.content !== b.content) return false
但是,只对比每个块的正文是不够的,<script>
、<style>
、<template>
还有可能带有一些属性,因此还需要针对一些属性做简单的比较,非常简洁:
const keysA = Object.keys(a.attrs) // 取出属性
const keysB = Object.keys(b.attrs) // 取出属o性
// 直接取出长度对比,避免后面的循环影响性能
if (keysA.length !== keysB.length) {
return false
}
// 这种情况就要对比每一个属性的值是不是一样
return keysA.every((key) => a.attrs[key] === b.attrs[key])
7.2 Client部分
这个部分,在浏览器中运行,它的代码在:\src\client\client.ts
。
HMR运行时
import { HMRRuntime } from 'vue'
declare var __VUE_HMR_RUNTIME__: HMRRuntime
这个事物对我来说比较新鲜,从代码实现的应用上来说,我认为它主要用于在HMR事件发生时,对某个特定的部分进行局部渲染、更新
。
事件的拦截
这个结构应该很清晰的:
socket.addEventListener('message', ({ data }) => {
const { type, path, id, index } = JSON.parse(data)
switch (type) {
case 'connected': // ...
case 'reload': // ...
case 'rerender': // ...
case 'style-update': // ...
case 'style-remove': // ...
}
});
这里 将Server发送过来的message
进行了处理。
缓存避免
如果要替换浏览器环境的一个js、img资源,必然要考虑缓存问题
,可以留意它们的import
语句,每个路径都加上了当前的时间戳来避免利用缓存:
import(`${path}?t=${Date.now()}`) // script部分的重载
import(`${path}?type=template&t=${Date.now()}`) // template部分的重载
import(`${path}?type=style&index=${index}&t=${Date.now()}`) // style部分的重载
8 lodash-es
引用实验
vue-dev-server
提供了引入vue
的例子,那么其它依赖的表现怎样呢?
我进行这个实验,并且选择loadash-es
的原因很大一部分是vite文档中的这一段话:
路径 性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。 > 一些包将它们的 ES 模块构建作为许多单独的文件相互导入。例如,
lodash-es
有超过 600 个内置模块!当我们执行import { debounce } from 'lodash-es'
时,浏览器同时发出 600 多个 HTTP 请求!尽管服务器在处理这些请求时没有问题,但大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度相当慢。 > 通过预构建lodash-es
成为一个模块,我们就只需要一个 HTTP 请求了!
不过,这个实验不是特别成功,它也让我确切体会到了vue-dev-server
与vite
的距离。
8.1 lodash-es的引入
我仍然使用test\fixtures
作为实验环境,在命令行执行:
yarn add lodash-es
安装结束后,打开test\fixtures\Comp.vue
,将script部分改为:
import { cloneDeep } from "lodash-es";
import Child from './Child.vue'
export default {
components: { Child },
setup() {
return {
count: 0,
clone: null,
}
},
mounted(){
this.doClone();
},
methods:{
doClone(){
let ob = cloneDeep({ apple: 1 });
ob.count ;
this.clone = ob;
}
}
}
8.2 实验运行结果
如无意外,应该是看到什么都没有的浏览器窗口和报错,当然,我的目的在于看看600个请求的盛况,我打开了开发者工具:
确实发起了海量的请求,这是和我想的一样的,但是却没有请求到这些文件,为什么呢?
8.3 为什么
请看lodash-es
依赖的引入请求返回了什么:
/**
* @license
* Lodash (Custom Build) <https://lodash.com/>
* Build: `lodash modularize exports="es" -o ./`
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
export { default as add } from './add.js';
export { default as after } from './after.js';
export { default as ary } from './ary.js';
export { default as assign } from './assign.js';
// ...
// ... 下面是其它模块的引入,格式一样的
// ...
为什么产生了巨量的请求?
每当遇到模块依赖的引入,事实上是引入依赖package.json中main和moudule配置节指定的js文件,如果这个js文件中引入了其它js文件,会发起一个新的请求获取它们。
通过上面提到的lodash-es的结构,里面有超过300个import,这造成了海量的请求。
为什么引入vue没有产生巨量请求?
因为vue做了特殊处理,统一引入node_modules/vue/dist/vue.runtime.esm-browser.js
这个版本。而vue的这个版本没有引入其它模块,它已经打包并且压缩好了。(注意引入的路径,和@vue
没有关系。)
为什么所有的请求都404失败了?
综合运用我对server.ts
的知识,我知道,这些模块的引入,没有跑到依赖的处理中去,而进入了javascript文件处理的分支中去了,于是,这些文件会从用户代码中去寻找,明显,这些文件不存在于用户代码中。由此产生大量404请求。
活动相关
欢迎加入,共同进步哟!
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhffjjff
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
怎样阻止微信小程序自动打开
PHP中文网 06-13 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01