1. 前言

大家好,我是若川。最近组织了源码共读流动,感兴趣的能够加我微信 ruochuan12 参加,已进行两个多月,大家一起交流学习,共同进步。

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

最近组织了源码共读流动,大家一起学习源码。于是各种搜查值得咱们学习,且代码行数不多的源码。

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

浏览本文,你将学到:

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

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

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

发现谷歌翻译的还比拟精确,我就一成不变的搬运过去。

  • 浏览器申请导入作为原生 ES 模块导入 - 没有捆绑。
  • 服务器拦挡对 *.vue 文件的申请,即时编译它们,而后将它们作为 JavaScript 发回。
  • 对于提供在浏览器中工作的 ES 模块构建的库,只需间接从 CDN 导入它们。
  • 导入到 .js 文件中的 npm 包(仅包名称)会即时重写以指向本地装置的文件。 目前,仅反对 vue 作为特例。 其余包可能须要进行转换能力作为本地浏览器指标 ES 模块公开。

也能够看看vitejs 文档,理解下原理,文档中图画得十分好。

看完本文后,我置信你会有一个比拟粗浅的了解。

3. 筹备工作

3.1 克隆我的项目

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

# 举荐克隆我的仓库git clone https://github.com/lxchuan12/vue-dev-server-analysis.gitcd vue-dev-server-analysis/vue-dev-server# npm i -g yarn# 装置依赖yarn# 或者克隆官网仓库git clone https://github.com/vuejs/vue-dev-server.gitcd 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

如图下图所示。

接着咱们找到 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 命令上,会呈现运行脚本调试脚本命令。如下图所示,抉择调试脚本。

点击进入函数(F11)按钮能够进入 vueMiddleware 函数。如果发现断点走到不是本我的项目的文件中,不想看,看不懂的状况,能够退出或者从新来过能够用浏览器无痕(隐衷)模式(快捷键Ctrl + Shift + N,避免插件烦扰)关上 http://localhost:3000,能够持续调试 vueMiddleware 函数返回的函数

如果你的VSCode不是中文(不习惯英文),能够装置简体中文插件。

如果 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

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

看图咱们大略晓得了有哪些区别。

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).pathname  let out = await tryCache(key)  if (!out) {    // Bundle Single-File Component    const result = await bundleSFC(req)    out = result    cacheData(key, out, result.updateTime)  }  send(res, out.code, 'application/javascript')}

4.3.1 bundleSFC 编译单文件组件

这个函数,依据 @vue/component-compiler 转换单文件组件,最终返回浏览器可能辨认的文件。

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).pathname  let 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

validate-npm-package-name

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.value      if (!/^\.\/?/.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).pathname  const pkg = req.path.replace(/^\/__modules\//, '')  let out = await tryCache(key, false) // Do not outdate modules  if (!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.jsconst 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/dist    const 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 中间件的两张图总结一下:

启用中间件后,如下图。

浏览器反对原生 type=module 模块申请加载。vue-dev-server 对其拦挡解决,返回浏览器反对内容,因为无需打包构建,所以速度很快。

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

5.1 import Vue from 'vue' 转换

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

main.js 中的 import 语句
import Vue from 'vue'
通过 recast 生成 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 转换成浏览器反对的文件。

5.3 后续还能做什么?

鉴于文章篇幅无限,缓存 tryCache 局部目前没有剖析。简略说就是应用了 node-lru-cache 最近起码应用 来做缓存的(这个算法常考)。后续应该会剖析这个仓库的源码,欢送继续关注我@若川。

十分倡议读者敌人依照文中办法应用VSCode调试 vue-dev-server 源码。源码中还有很多细节文中因为篇幅无限,未全面开展讲述。

值得一提的是这个仓库的 master 分支,是尤雨溪两年前写的,绝对本文会比较复杂,有余力的读者能够学习。

也能够间接去看 vite 源码。

看完本文,兴许你就能发现其实前端能做的事件越来越多,不禁感叹:前端水深不可测,唯有继续学习。

最初欢送加我微信 ruochuan12 交换,参加 源码共读 流动,大家一起学习源码,共同进步。