本文源码,欢迎勘误,交流。
1 vue-dev-server
这是一个simple版的vite,不过它只是最最简单的vite实现,vite在实际转译js、jsx、typescript的过程中,应用了esbuild,对vue的SFC标准支持也是通过vite插件实现支持的。而这个精简版的vite,并不支持那么多其它文件类型,只对vue做了支持,对其它框架的支持能力也并没有那么强。
2 分支结构
我读了举办活动的若川大佬的笔记,也学(tou)袭(kan)了另外两位同学hua_bang和 NewName 的笔记,我发现这三位都不约而同地只分析了 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 基本原理
如果读者搜索过Vite原理,你一定不会对我下面这段话感到陌生:
vue-dev-server的原理就是创建一个Http服务,并加载一个中间件拦截所有的Http请求并返回文件,对其中特定的文件格式和路径进行处理,比如vue的sfc文件,解析处理为JS文件后,JS的解析(编译)和运行的过程交给了浏览器,Vite本身不需要进行任何的打包动作,因此速度非常快。
这个原理也是vue-dev-server为什么确实就是微缩版的vite的原因。
整个server的工作原理,也在Readme.md的How it works中有跟大家说明,听若川大佬说,google机翻还挺准确的,建议你可以直接机翻。
4 Http服务和中间件
在这一点上,archive和master分支稍有不同,archive分支的server服务和请求拦截,依托于express和express中间件来实现,阅读它的实现,重心也是VueMiddleware这个中间件;而master分支则脱离了框架上的依赖,直接使用的Nodejs的http.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 test 和 node运行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的客户端——正在开发的代码不存在
执行编译:
现在我们应当回头看下刚才找的的自定义脚本dev、dev-client、dev-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是怎么实现的呢?
我计划在下一篇笔记继续深入探讨这些问题。
活动相关
欢迎加入,共同进步哟!