【若川视野 x 源码共读】11期之一 - Vue-dev-server源码解析

323 阅读10分钟

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

1 vue-dev-server

代码所在仓库

这是一个simple版的vite,不过它只是最最简单的vite实现,vite在实际转译js、jsx、typescript的过程中,应用了esbuild,对vue的SFC标准支持也是通过vite插件实现支持的。而这个精简版的vite,并不支持那么多其它文件类型,只对vue做了支持,对其它框架的支持能力也并没有那么强。

2 分支结构

我读了举办活动的若川大佬的笔记,也学(tou)袭(kan)了另外两位同学hua_bangNewName 的笔记,我发现这三位都不约而同地只分析了 archive 分支上的代码,真的只有我看见还有个master分支吗?

是的,vue-dev-server有两个分支的代码,分别是:

  • archive
  • master

这两个分支到目前我都看了初步,archive分支的代码,最后维护已经是5年前了,而master分支,最后维护是3年前。

我总结的archive和master的不同有:

  • archive的实现语言是javascript,而master的实现语言是typescript;
  • archive没有使用websocket而master具有WebSocket,所以 master 有hmr能力
  • archive和master使用的sfc编译器不同,分别是@vue/component-compiler@vue/compiler-sfc,这两个编译器后者相对新一点;

从我个人的角度看,更推荐master分支,如果有读过一下vite2源码的朋友,不难发现其实master分支的代码已经有vite的一些样子了;我选择master的另一个理由是,archive这个单词是"归档"的意思,归档就是作为档案存在了(不再维护)。

当然不看master,也不用担心失去了一片大陆,两个分支的实现原理以及启动方式都比较类似,archive事实上比master简单非常多。阅读vite源码以前,直接选择archive做个“垫脚凳”也是不错的。


本文将尽量讨论两个分支都通用的内容,我计划多写一篇笔记进行分支代码的深入分析。


3 基本原理

11期-vue-dev-server-基本原理.png

如果读者搜索过Vite原理,你一定不会对我下面这段话感到陌生:

vue-dev-server的原理就是创建一个Http服务,并加载一个中间件拦截所有的Http请求并返回文件,对其中特定的文件格式和路径进行处理,比如vue的sfc文件,解析处理为JS文件后,JS的解析(编译)和运行的过程交给了浏览器,Vite本身不需要进行任何的打包动作,因此速度非常快。

这个原理也是vue-dev-server为什么确实就是微缩版的vite的原因。

整个server的工作原理,也在Readme.md的How it works中有跟大家说明,听若川大佬说,google机翻还挺准确的,建议你可以直接机翻。

4 Http服务和中间件

在这一点上,archivemaster分支稍有不同,archive分支的server服务和请求拦截,依托于expressexpress中间件来实现,阅读它的实现,重心也是VueMiddleware这个中间件;而master分支则脱离了框架上的依赖,直接使用的Nodejshttp.CreateServer建立的http服务。

可以点击下面的链接去查看它们的文档:

这个部分,master和vite的做法几乎一致。

我不希望引导阅读文章的你去对比使用express和原生http.Server两种做法的异同,因为并没有什么意义,这个无意义的动作让我来替你承担就好,我在书写到此处的时候专门去读了Experss的http服务实现,然后发现Express其实也就是http.CreateServer而已,当然如果有兴趣亦可以看看

📌支线任务:或许你会和我一样,对Express如何挂载一个中间件,中间件的执行,路由的实现感兴趣,那有空可以深入探讨一下express哦。

5 启动

两个分支,都由 bin\*.js这个文件负责启动开发服务器。

5.1 archive分支的启动

在archive分支的package.json中,只有一个自定义脚本:

  "scripts": {
    "test": "cd test && node ../bin/vue-dev-server.js"
  },

test文件夹(正在被开发的文件):

这个就是启动archive分支的指令了。它分成两个部分:cd testnode运行vue-dev-server.js,这个应该不难的。

打开test文件夹,即可以看见用于测试和演示的一组文件:

└──  test
     ├── main.js 演示项目的入口文件
     ├── index.html 演示项目的index.html
     ├── test.vue 演示加载的test.vue文件

这个其实就是最简单的一个Vue项目。

vue-dev-server.js(开发服务器): 顾名思义,开发服务器,就是写代码时使用的服务器了。所以你可以在vue-dev-server.js中找到它启动了一个express http服务:

const express = require('express')

app.listen(3000, () => {
  console.log('server running at http://localhost:3000')
})

我们前面也说了,它会加载一个中间件,拦截所有的html请求,所以也能找到:

const { vueMiddleware } = require('../middleware')
app.use(vueMiddleware())

最后剩下的这两句,就是启动一个静态文件服务器:

const root = process.cwd();
app.use(express.static(root))

什么?你问我root是什么?刚才已经cd进了test了哦!

5.2 master分支的启动

emm.. 这个分支的启动确实要复杂一些。但是,还是从package.json开始吧,但是你只能找到这三个句子:

"scripts": {
	"dev": "run-p dev-client dev-server",
	"dev-client": "tsc -w --p src/client",
	"dev-server": "tsc -w --p src/server",
},

我来描述一下当时的心情吧,我高高兴兴地执行了npm i,又高高兴兴执行了npm run dev,然后只见屏幕上输出了四句话:

[9:30:21 AM] Starting compilation in watch mode...

[9:30:21 AM] Starting compilation in watch mode...

[9:30:25 AM] Found 0 errors. Watching for file changes.

[9:30:25 AM] Found 0 errors. Watching for file changes.

嗯?好像哪里不对。因为这个时候你启动的,并不是那个dev-server,而是tsc的监听文件的服务,这是另一个故事。

但是,master和archive分支最大的不同在于,master提供了单元测试,单元测试的代码就不在此处分析了,我直接说结果吧,我找到了里面的一段代码:

server = execa(path.resolve(__dirname, '../bin/vds.js'), {
	cwd: tempDir
})

答案就在这里,上面也说了,所有的启动都是bin\*.js负责的,那么我们要看一下bin\vds.js的代码了,这个文件只有9行代码,其中两句特别重要:

const { createServer } = require('../dist/server')
// ...
argv.cwd = require('path').resolve(process.cwd(), argv._[0])

通过这两句代码:

  • 我们知道这个server在dist/server这个东西里实现
  • 我们知道这个vds.js会接收一个参数,这个参数是一个路径,不传这个参数就会从process.cwd()目录启动。

这个时候,不妨对比一下archive分支,我们刚才看到,在archive分支中,vue-dev-server.js和当前分支的vds.js承担的都是启动开发服务器的工作,但是现在。

  • 需要启动的server——dist/server并不存在(全新拉取的分支还没有编译出dist)
  • 需要依托server的客户端——正在开发的代码不存在

执行编译:

现在我们应当回头看下刚才找的的自定义脚本devdev-clientdev-server,它们正是应用typescript编译器tsc对代码执行编译的关键脚本。所以,执行dev脚本,就可以看见项目根目录多出来一个dist文件夹:

npm run dev

了解一下tsc-w参数,修改一下src中的代码,你应该就能感觉到这个dev脚本实际上有什么作用了,这里我就不说了,因为我觉得“画公仔都画出肠子了”(粤语)。

客户端

编译以后,server是有了,客户端还没有。

还记得那个传递给server的那个参数吗?对,它就是关键先生,它就是客户端(你正在开发的)代码的所在路径

那么现在怎么启动master分支,综合来说就是这样一个指令了:

node bin/vds.js 【客户端代码路径】

你可以用单元测试中的 test/fixtures这个文件夹来做这个实验:

node node bin/vds.js test/fixtures

为了方便,我在scripts里面加了一个自定义指令:

"test2": "node bin/vds.js test/fixtures"

这样执行npm run test2就启动了项目,这个时候可以改动一下test/fixtures中的文件,可以看见HMR的效果。

6 文件请求拦截

实现文件拦截分别在两个分支这些文件中:

  • archive分支的middleware.js
  • master分支的server.ts

📌 亲爱的读者,如果让你实现或者改造这个部分,你会考虑用什么设计模式

6.1 archive分支的文件请求拦截

archive分支的文件拦截,主要是最后return中间件的代码部分,在此处前面的,是中间件依赖的文件函数。

return的部分,可以精简为:

return async (req, res, next) => {
	if (req.path.endsWith('.vue')) {
		// 处理 去往后缀是 .vue 的文件请求
	} else if (req.path.endsWith('.js')) { 
		// 处理 去往后缀是 .js 的文件请求
	} else if (req.path.startsWith('/__modules/')) {
		// 处理去往 基础包 的请求
	} else {
      next() // 不做任何处理的情况直接返回
    }
}

不去关注每个文件的具体处理,应该不是特别难。

6.2 master分支的文件请求拦截

// ...

const server = http.createServer(async (req, res) => {
	const pathname = url.parse(req.url!) .pathname!

	if (pathname === '/__hmrClient') {
		// WebSocket客户端代码的发送
	} else if (pathname.startsWith('/__modules/')) {
		// 处理去往 基础包 的请求
	} else if (pathname.endsWith('.vue')) {
		// 处理去往 后缀是.vue 的文件请求
	} else if (pathname.endsWith('.js')) {
		// 处理 去往后缀是 .js 的文件请求
	
		// 读取js文件
		const filename = path.join(cwd, pathname.slice(1))
		try {
			// 处理并返回 js文件
		} catch (e) {
			// 错误的处理
		}
	}

	// 访问不存在的路径做这个处理
	serve(req, res, {
		public: cwd ? path.relative(process.cwd(), cwd) : '/',
		rewrites: [{
			source: '**',
			destination: '/index.html'
		}]
	})
})

// 预告一下,这下面做了这些, 下一篇文章来做详细的分析:
// WebSocket服务器的加载
// 文件监听、HMR的实现
// 向http.Server注入事件响应,并启动Server

typescript是javascript的超集,它给js带来了强类型语法,刚刚开始读的时候,会有一点不适感。

① 两个感叹号:

url.parse(req.url!) .pathname!这一句中,可能最让人费解的是两个感叹号!吧?它意味着,url和pathname两个从对象中取出来的值是必须有值的,即对象中一定存在这个字段,如果不存在,则需要抛出错误,而不是返回一个undefined了事。

② hmrClient:

相对于archive分支,master分支的处理多了一个/__hmrClient的if分支,也是顾名思义吧,它实际上就是clent文件夹中实现的WebSocket客户端。

这个client是自动注入到代码中去的。

③ serve:

这个函数,来自serve-handler这个包,它是一个最终返回结果,如果我没有理解错,它就是个响应结果打包器,可以通过配置做更多不同的响应,比如说重定向到某个路径(包括外链)改写为其它路径等等,你可以读一下它的文档了解它的更多信息。

server.ts中的这段代码,意味着请求不存在的路径时都会把你回转到 index.html,不过我做实验时,只有在浏览器地址栏乱输地址才会重写到index.html,感觉还需要多做一些实验来把它理解清楚了。

7 总结

我们从项目的基本层分析了vue-dev-server,包括它的基本分支结构、代码的基本结构和工作原理,又从服务器的启动,了解的程序的入口层面。

我们接下来还要抽时间继续深入了解:

  • 分支的代码怎么调试?
  • master和archive分支是怎么处理vue单文件组件的?js文件呢?
  • hmr的websocket是怎么实现的呢?

我计划在下一篇笔记继续深入探讨这些问题。

活动相关

链接

欢迎加入,共同进步哟!