• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

若川视野 x 源码共读11期:二 - Vue-dev-server master分支源码详析

武飞扬头像
wandbond
帮助3

本文源码,欢迎勘误,交流。

1 Guide

两个分支的启动和基本代码结构已经在前一篇文章中谈到了,本文计划围绕几个问题开展master分支的代码解析:

  1. master分支对 代码依赖,vue文件,js文件的处理,其它请求的默认处理;
  2. hmr的实现原理;
  3. lodash-es的引入和测试;

本文将不谈及master代码的调试方法,做功课时,我感觉typescript调试起来配置比较复杂(可能是我找到的教程太详细了)(实际上是作者这个人比较懒),要花比较多时间进去,导致偏离了原来的目标。

🏷 说明: 当然,通过调试tsc编译输出后的javascript代码(dist文件夹)来观察运行时的形态也是可以的,编译后的代码对可读性也没有特别大的影响。限于篇幅太长了,就先不说了吧。 行吧,其实就是我不想浪费你宝贵的时间(画外音:偷懒就偷懒吧)

2 入口-index.html

在前一篇文章提到,通过http的拦截器,Server端会拦截几种文件的http请求:vuejs请求路径前缀有'/__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文件的核心处理:sendJSrewrite

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个插件:bigIntoptionalChainingnullishCoalescingOperator,这三个都是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'的情况的处理
	}
}

看到此处,我不禁有下面的疑问:

  1. query.type是在什么地方赋值的?
  2. 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-sfccompileTemplate方法编译为一个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-sfccompileStyle方法解析后的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-sumhash方法计算一个哈希码,以达到scoped的效果。

4.4 VueMiddleWare-compileSFCTemplate函数

有了Style编译函数(compileSFCStyle)的基础,这个函数读起来也是小儿科,函数的具体实现,不是本文重点,但是要知道的是,它把.vue中的<template>编译为一个render函数了,这个也是vue的一个基础知识,所有的vue最后都会被编译为一个render函数,进行渲染。

4.5 VueMiddleWare-parseSFC函数

这个函数实际没有什么可以谈论的事物,它最关键的两个东西是:

  • 缓存历史版本到内存中,在hmr(热模块替换)对比时,进行新旧版本的对比;
  • 利用nodejsreadFile方法读取磁盘上对应的文件,并且将它送入@vue/compiler-sfcparse函数中解析;
// 读取文件并送入"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: []
}

留意scripttemplatestyle三个字段的值和结构,以了解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.tsjavascript文件处理中谈到,如果import语句引入的依赖名,没有使用路径的标识,就会被识别为一个模块依赖。

这样有个很明显的问题就是:人懒。webpack允许我们通过resolve配置节使得进行引入的时候,不写后缀:

// 引入a.vue
import AVueComponent from "./a"

但是,vue-dev-server就不行,vite也一样。

5.2 解析模块的过程

函数大体通过下面几个步骤来解析模块依赖:

  • 针对source-map请求做处理;
  • 借助resolve-form的能力,找到模块的package.json路径;
  • 针对vue做特殊处理;
  • 其它依赖,依据package.jsonmodulemain配置节拼接模块路径,并后续根据这个模块依赖进行后续引入;

5.3 resolve-form

这个包的能力很特殊,通过给定的两个路径参数,它会给你返回正确的文件路径,哪怕这个路径是错误的。

比如引入vue的时候,传入的参数分别是: -【我的代码路径】/vue-dev-server-master/test/fixtures

  • vue/package.json

它仍然能解析出vuepackage.json的正确路径:【我的代码路径】/vue-dev-server-master/node_modules/vue/package.json

这是了不得的,具体的实现原理真是让人好奇。

5.4 vue

对于 vue,会直接拿到vue.runtime.esm-browser.js这个vue打包发布后的版本。

5.5 其它依赖

对于其它文件,会根据模块package.jsonmodulemain配置节来拼接出最后引入的文件的路径:

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-servervite的距离。

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
系列文章
更多 icon
同类精品
更多 icon
继续加载