1. 前言

大家好,我是若川。最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12

想学源码,极力推荐之前我写的《学习源码整体架构系列》jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4koa-composevue-next-releasevue-thiscreate-vue等10余篇源码文章。

最近组织了源码共读活动

在 vuejs组织[1] 下,找到了尤雨溪几年前写的“玩具 vite”vue-dev-server[2],发现100来行代码,很值得学习。于是有了这篇文章。

阅读本文,你将学到:

1. 学会 vite 简单原理
2. 学会使用 VSCode 调试源码
3. 学会如何编译 Vue 单文件组件
4. 学会如何使用 recast 生成 ast 转换文件
5. 如何加载包文件
6. 等等

2. vue-dev-server 它的原理是什么

vue-dev-server#how-it-works[3]README 文档上有四句英文介绍。

发现谷歌翻译[4]的还比较准确,我就原封不动的搬运过来。

  • 浏览器请求导入作为原生 ES 模块导入 - 没有捆绑。

  • 服务器拦截对 *.vue 文件的请求,即时编译它们,然后将它们作为 JavaScript 发回。

  • 对于提供在浏览器中工作的 ES 模块构建的库,只需直接从 CDN 导入它们。

  • 导入到 .js 文件中的 npm 包(仅包名称)会即时重写以指向本地安装的文件。 目前,仅支持 vue 作为特例。 其他包可能需要进行转换才能作为本地浏览器目标 ES 模块公开。

也可以看看vitejs 文档[5],了解下原理,文档中图画得非常好。

看完本文后,我相信你会有一个比较深刻的理解。

3. 准备工作

3.1 克隆项目

本文仓库 vue-dev-server-analysis,求个star^_^[6]

# 推荐克隆我的仓库
git clone https://github.com/lxchuan12/vue-dev-server-analysis.git
cd vue-dev-server-analysis/vue-dev-server
# npm i -g yarn
# 安装依赖
yarn# 或者克隆官方仓库
git clone https://github.com/vuejs/vue-dev-server.git
cd vue-dev-server
# npm i -g yarn
# 安装依赖
yarn

一般来说,我们看源码先从package.json文件开始:

// vue-dev-server/package.json
{"name": "@vue/dev-server","version": "0.1.1","description": "Instant dev server for Vue single file components","main": "middleware.js",// 指定可执行的命令"bin": {"vue-dev-server": "./bin/vue-dev-server.js"},"scripts": {// 先跳转到 test 文件夹,再用 Node 执行 vue-dev-server 文件"test": "cd test && node ../bin/vue-dev-server.js"}
}

根据 scripts test 命令。我们来看 test 文件夹。

3.2 test 文件夹

vue-dev-server/test 文件夹下有三个文件,代码不长。

  • index.html

  • main.js

  • text.vue

如图下图所示。

test文件夹三个文件

接着我们找到 vue-dev-server/bin/vue-dev-server.js 文件,代码也不长。

3.3 vue-dev-server.js

// vue-dev-server/bin/vue-dev-server.js
#!/usr/bin/env nodeconst express = require('express')
const { vueMiddleware } = require('../middleware')const app = express()
const root = process.cwd();app.use(vueMiddleware())app.use(express.static(root))app.listen(3000, () => {console.log('server running at http://localhost:3000')
})

原来就是express启动了端口3000的服务。重点在 vueMiddleware 中间件。接着我们来调试这个中间件。

鉴于估计很多小伙伴没有用过VSCode调试,这里详细叙述下如何调试源码。学会调试源码后,源码并没有想象中的那么难

3.4 用 VSCode 调试项目

vue-dev-server/bin/vue-dev-server.js 文件中这行 app.use(vueMiddleware()) 打上断点。

找到 vue-dev-server/package.jsonscripts,把鼠标移动到 test 命令上,会出现运行脚本调试脚本命令。如下图所示,选择调试脚本。

调试

VSCode 调试 Node.js 说明

点击进入函数(F11)按钮可以进入 vueMiddleware 函数。如果发现断点走到不是本项目的文件中,不想看,看不懂的情况,可以退出或者重新来过可以用浏览器无痕(隐私)模式(快捷键Ctrl + Shift + N,防止插件干扰)打开 http://localhost:3000,可以继续调试 vueMiddleware 函数返回的函数

如果你的VSCode不是中文(不习惯英文),可以安装简体中文插件[7]
如果 VSCode 没有这个调试功能。建议更新到最新版的 VSCode(目前最新版本 v1.61.2)。

接着我们来跟着调试学习 vueMiddleware 源码。可以先看主线,在你觉得重要的地方继续断点调试。

4. vueMiddleware 源码

4.1 有无 vueMiddleware 中间件对比

不在调试情况状态下,我们可以在 vue-dev-server/bin/vue-dev-server.js 文件中注释 app.use(vueMiddleware()),执行 npm run test 打开 http://localhost:3000

没有执行 vueMiddleware 中间件的原始情况

再启用中间件后,如下图。

执行了 vueMiddleware 中间文件变化

看图我们大概知道了有哪些区别。

4.2 vueMiddleware 中间件概览

我们可以找到vue-dev-server/middleware.js,查看这个中间件函数的概览。

// vue-dev-server/middleware.jsconst vueMiddleware = (options = defaultOptions) => {// 省略return async (req, res, next) => {// 省略// 对 .vue 结尾的文件进行处理if (req.path.endsWith('.vue')) {// 对 .js 结尾的文件进行处理} else if (req.path.endsWith('.js')) {// 对 /__modules/ 开头的文件进行处理} else if (req.path.startsWith('/__modules/')) {} else {next()}}
}
exports.vueMiddleware = vueMiddleware

vueMiddleware 最终返回一个函数。这个函数里主要做了四件事:

  • .vue 结尾的文件进行处理

  • .js 结尾的文件进行处理

  • /__modules/ 开头的文件进行处理

  • 如果不是以上三种情况,执行 next 方法,把控制权交给下一个中间件

接着我们来看下具体是怎么处理的。

我们也可以断点这些重要的地方来查看实现。比如:

重要断点

4.3 对 .vue 结尾的文件进行处理

if (req.path.endsWith('.vue')) {const key = parseUrl(req).pathnamelet out = await tryCache(key)if (!out) {// Bundle Single-File Componentconst result = await bundleSFC(req)out = resultcacheData(key, out, result.updateTime)}send(res, out.code, 'application/javascript')
}

4.3.1 bundleSFC 编译单文件组件

这个函数,根据 @vue/component-compiler[8] 转换单文件组件,最终返回浏览器能够识别的文件。

const vueCompiler = require('@vue/component-compiler')
async function bundleSFC (req) {const { filepath, source, updateTime } = await readSource(req)const descriptorResult = compiler.compileToDescriptor(filepath, source)const assembledResult = vueCompiler.assemble(compiler, filepath, {...descriptorResult,script: injectSourceMapToScript(descriptorResult.script),styles: injectSourceMapsToStyles(descriptorResult.styles)})return { ...assembledResult, updateTime }
}

接着我们来看 readSource 函数实现。

4.3.2 readSource 读取文件资源

这个函数主要作用:根据请求获取文件资源。返回文件路径 filepath、资源 source、和更新时间 updateTime

const path = require('path')
const fs = require('fs')
const readFile = require('util').promisify(fs.readFile)
const stat = require('util').promisify(fs.stat)
const parseUrl = require('parseurl')
const root = process.cwd()async function readSource(req) {const { pathname } = parseUrl(req)const filepath = path.resolve(root, pathname.replace(/^\//, ''))return {filepath,source: await readFile(filepath, 'utf-8'),updateTime: (await stat(filepath)).mtime.getTime()}
}exports.readSource = readSource

接着我们来看对 .js 文件的处理

4.4 对 .js 结尾的文件进行处理

if (req.path.endsWith('.js')) {const key = parseUrl(req).pathnamelet out = await tryCache(key)if (!out) {// transform import statements// 转换 import 语句 // import Vue from 'vue'// => import Vue from "/__modules/vue"const result = await readSource(req)out = transformModuleImports(result.source)cacheData(key, out, result.updateTime)}send(res, out, 'application/javascript')
}

针对 vue-dev-server/test/main.js 转换

import Vue from 'vue'
import App from './test.vue'new Vue({render: h => h(App)
}).$mount('#app')// 公众号:若川视野
// 加微信 ruochuan12
// 参加源码共读,一起学习源码
import Vue from "/__modules/vue"
import App from './test.vue'new Vue({render: h => h(App)
}).$mount('#app')// 公众号:若川视野
// 加微信 ruochuan12
// 参加源码共读,一起学习源码

4.4.1 transformModuleImports 转换 import 引入

recast[9]

validate-npm-package-name[10]

const recast = require('recast')
const isPkg = require('validate-npm-package-name')function transformModuleImports(code) {const ast = recast.parse(code)recast.types.visit(ast, {visitImportDeclaration(path) {const source = path.node.source.valueif (!/^\.\/?/.test(source) && isPkg(source)) {path.node.source = recast.types.builders.literal(`/__modules/${source}`)}this.traverse(path)}})return recast.print(ast).code
}exports.transformModuleImports = transformModuleImports

也就是针对 npm 包转换。 这里就是 "/__modules/vue"

import Vue from 'vue' => import Vue from "/__modules/vue"

4.5 对 /__modules/ 开头的文件进行处理

import Vue from "/__modules/vue"

这段代码最终返回的是读取路径 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js 下的文件。

if (req.path.startsWith('/__modules/')) {// const key = parseUrl(req).pathnameconst pkg = req.path.replace(/^\/__modules\//, '')let out = await tryCache(key, false) // Do not outdate modulesif (!out) {out = (await loadPkg(pkg)).toString()cacheData(key, out, false) // Do not outdate modules}send(res, out, 'application/javascript')
}

4.5.1 loadPkg 加载包(这里只支持Vue文件)

目前只支持 Vue 文件,也就是读取路径 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js 下的文件返回。

// vue-dev-server/loadPkg.js
const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)async function loadPkg(pkg) {if (pkg === 'vue') {// 路径// vue-dev-server/node_modules/vue/distconst dir = path.dirname(require.resolve('vue'))const filepath = path.join(dir, 'vue.esm.browser.js')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

至此,我们就基本分析完毕了主文件和一些引入的文件。对主流程有个了解。

5. 总结

最后我们来看上文中有无 vueMiddleware 中间件的两张图总结一下:

没有执行 vueMiddleware 中间件的原始情况

启用中间件后,如下图。

执行了 vueMiddleware 中间文件变化

浏览器支持原生 type=module 模块请求加载。vue-dev-server 对其拦截处理,返回浏览器支持内容,因为无需打包构建,所以速度很快。

<script type="module">import './main.js'
</script>

5.1 import Vue from 'vue' 转换

// vue-dev-server/test/main.js
import Vue from 'vue'
import App from './test.vue'new Vue({render: h => h(App)
}).$mount('#app')

main.js 中的 import 语句 import Vue from 'vue' 通过 recast[11] 生成 ast 转换成 import Vue from "/__modules/vue"而最终返回给浏览器的是 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js

5.2 import App from './test.vue' 转换

main.js 中的引入 .vue 的文件,import App from './test.vue'则用 @vue/component-compiler[12] 转换成浏览器支持的文件。

5.3 后续还能做什么?

鉴于文章篇幅有限,缓存 tryCache 部分目前没有分析。简单说就是使用了 node-lru-cache[13]最近最少使用 来做缓存的(这个算法常考)。后续应该会分析这个仓库的源码,欢迎持续关注我@若川。

非常建议读者朋友按照文中方法使用VSCode调试 vue-dev-server 源码。源码中还有很多细节文中由于篇幅有限,未全面展开讲述。

值得一提的是这个仓库的 `master` 分支[14],是尤雨溪两年前写的,相对本文会比较复杂,有余力的读者可以学习。

也可以直接去看 `vite`[15] 源码。

看完本文,也许你就能发现其实前端能做的事情越来越多,不由感慨:前端水深不可测,唯有持续学习。

最后欢迎加我微信 ruochuan12源码共读 活动,大家一起学习源码,共同进步。

参考资料

[1]

vuejs组织: https://github.com/vuejs

[2]

vue-dev-server: https://github.com/vuejs/vue-dev-server

[3]

更多链接可以点击阅读原文查看


最近组建了一个江西人的前端交流群,如果你是江西人可以加我微信 ruochuan12 私信 江西 拉你进群。

推荐阅读

1个月,200+人,一起读了4周源码
我历时3年才写了10余篇源码文章,但收获了100w+阅读

老姚浅谈:怎么学JavaScript?

我在阿里招前端,该怎么帮你(可进面试群)

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动

识别方二维码加我微信、拉你进源码共读

今日话题

略。欢迎分享、收藏、点赞、在看我的公众号文章~

尤雨溪写的100多行的“玩具 vite”,十分有助于理解 vite 原理相关推荐

  1. 学习尤雨溪写的 Vue3 源码中的简单工具函数

    大家好,我是若川.最近组织了源码共读活动.每周读 200 行左右的源码.很多第一次读源码的小伙伴都感觉很有收获,感兴趣可以加我微信ruochuan12,拉你进群学习. 初学者也能看懂的 Vue3 源码 ...

  2. 尤雨溪-写一个mini vue

    render 主题: render 解决问题: 1.h是什么?有什么用? h是hyperScript. 作用: 使用JS去写html.因为html最后也是render成JS 1,when to use ...

  3. Vue.js 作者尤雨溪:TypeScript 与 JavaScript 并行才切合实际!

    作者 | Evrone 译者 | 弯月,责编 | 杨碧玉 头图 | CSDN 下载自视觉中国 出品 | CSDN(ID:CSDNnews) 以下为译文: 简介 尤雨溪是一名优秀的软件开发人员,同时是开 ...

  4. 【译】听尤雨溪聊:下一代前端构建工具 ViteJS 中英双语字幕

    原视频地址:Next generation frontend tooling with ViteJS ✨ Open Source Friday ✨[1] 中英文字幕视频地址(B站):[译]下一代前端工 ...

  5. 尤雨溪:下一代前端构建工具 ViteJS 技术解读

    作者:@清秋 https://juejin.cn/post/6937176680251424775 Vite 作者 尤雨溪 在 2021 年 2 月 12 日在 Twitch 上做客 GitHub O ...

  6. 尤雨溪推荐神器 ni ,能替代 npm/yarn/pnpm ?简单好用!源码揭秘!

    1. 前言 大家好,我是若川.最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 想学源码,极力推荐之前我写的<学习源码整体架构系列>jQuery.underscore.l ...

  7. 尤雨溪携手字节前端专家,畅聊 Vue 3.0 前端技术新趋势

    前端这个技术领域,在应用化以后,涵盖的内容越来越广--纯表现层.应用实现层.应用架构层.基础设施层到改进开发范式的理念层,都有太多可以去钻研的技术点,衍生出了无数前端开发的发展路线."别更新 ...

  8. 竟然被尤雨溪点赞了:我给Vue生态贡献代码的这一年

    大家好,我是若川.最近组织了源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列> ...

  9. Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?

    1. 前言 大家好,我是若川.最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12,长期交流学习. 之前写的<学习源码整体架构系列> 包含jQuery.underscore. ...

最新文章

  1. 金蝶显示中间服务器忙,金蝶显示云服务器繁忙怎么回事
  2. golang包math/rand使用示例
  3. wxWidgets:wxListItem类用法
  4. 负责域名解析的DNS服务
  5. 【Altium Designer】DatabaseLib的使用方法
  6. lucene创建索引_Lucene概述第一部分:创建索引
  7. Android官方开发文档Training系列课程中文版:手势处理之滚动动画及Scroller
  8. class传参 python_Python类的概念、定义、属性、继承
  9. MongoDB---之---可视化客户端
  10. python ioc框架_轻松理解 Spring 中的 IOC
  11. 五个未来最吃香的技能 数据分析排第一
  12. iOS开发UI篇—简单的浏览器查看程序
  13. WinDbg分析蓝屏dump教程
  14. 12个从小到超级成功的博客案例研究
  15. 315/433MHZ无线遥控接收解码源程序 Keil源程序 含AD格式电路图
  16. 图扑数字孪生智慧隧道,新基建带来新生活
  17. 元编程: is_floating_point<Tp>
  18. nodejs -- 使用seneca模块搭建为服务
  19. 项目管理系统Cynthia
  20. 蒙古文字在线翻译_内蒙古语言免费中文转换蒙文翻译_中蒙互译电脑版在线

热门文章

  1. 实验楼 1. k-近邻算法实现手写数字识别系统--《机器学习实战 》
  2. vm+ubuntu联网
  3. css样式图片、渐变、相关小知识
  4. [递推] hihocoder 1239 Fibonacci
  5. 大话设计模式笔记 享元模式
  6. The mook jong 计数DP
  7. 用 PS 调整服务器时间
  8. css3高级和低级样式属性先后顺序
  9. 家庭组计算机无法,【求助】Windows无法从该家庭组中删除你的计算机
  10. srs 服务关闭命令_【经验总结】如何做到网络版工作站与服务器时间同步?