引言
vite简易版,vue-dev-server阅读源码记录
从代码里面摘出来几个重点,部分已经梳理完毕,还有一些陆续会梳理掉。主要介绍的点有:
- vite冷启动流程
- readSource函数
- parseUrl依赖
- recast依赖
- recast实践
- validate-npm-package-name依赖
- transformModuleImports函数
- cacheData函数
- @vue/component-compiler依赖(单独拎出来讲吧)
- esbuild生产环境用之后再说
重点介绍
vite冷启动流程
通过梳理vue-dev-server源码,理解了vite在本地开发时的流程。首先,启动开发服务器,然后利用新一代浏览器的ESM能力,直接可以识别import,让浏览器去分析模块之间的依赖关系(webpack本地启动会自己去分析import的依赖关系),无需打包,直接请求所需模块并实时编译(将.vue文件转成浏览器认识的js文件)
readSource parseUrl
readSource.js源码,导出一个readSource方法,该方法接收express捕获的请求req对象作为入参,返回文件绝对路径和readFile后的文件内容,以及更新时间,应该是用来缓存用。这个文件比较简单
const path = require('path')
const fs = require('fs')
// promisify Node.js 内置的 util 模块有一个 promisify() 方法
// 该方法将基于回调的函数转换为基于 Promise 的函数。
// 这使您可以将 Promise 链和 async/await 与基于回调的 API 结合使用
const readFile = require('util').promisify(fs.readFile)
const stat = require('util').promisify(fs.stat)
// 解析url,返回url param
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
recast validate-npm-package-name
recast
这个内容比较多了,涉及到了ast,仓库链接github.com/benjamn/rec…
主要就是把js和ast之前转来转去,在转的时候做一些事情,具体使用可以看看参考文章里面的recast实际使用,方便理解,最好动手实践一下
validate-npm-package-name
判断是不是npm市场的依赖,字面意思就能理解
transformModuleImports
导出一个transformModuleImports方法,接收readFile获取的code,这个方法就一个作用,将main.js 中的 import 语句 import Vue from 'vue' 通过 recast 生成 ast 转换成 import Vue from "/__modules/vue" 。
这么做的目的就是最后会调用loadPkg方法将 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js返回给浏览器
import Vue from 'vue' => import Vue from "/__modules/vue"
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
cacheData、tryCache函数
举个例子,在核心文件middleware.js中有这么一段代码
// 判断浏览器请求的文件是否为js
if (req.path.endsWith('.js')) {
// 取文件路径作为缓存的key
const key = parseUrl(req).pathname
// 读取缓存
let out = await tryCache(key)
// 没有找到的话就写入缓存
if (!out) {
// readSource上面介绍了,返回文件信息对象
const result = await readSource(req)
// transformModuleImports也不多说了
out = transformModuleImports(result.source)
// 写入缓存
cacheData(key, out, result.updateTime)
}
// 请求成功
send(res, out, 'application/javascript')
}
里面用到了两个重要的函数,tryCache读取缓存,cacheData写入缓存。缓存的实现原理是使用了lru-cache库(最近最少使用),听说面试会问这个,之后看看源码到底是怎么做的。
const vueMiddleware = (options = { cache = true }) => {
let cache
// 存放文件的更新时间,如果文件没有更新则不去读写缓存
let time = {}
if (options.cache) {
// 核心库
const LRU = require('lru-cache')
cache = new LRU({
// 缓存最大数量(应该是文件数量没仔细看解释)
max: 500,
// 不知道干啥的
length: function (n, key) { return n * 2 + key.length }
})
}
function cacheData (key, data, updateTime) {
// peek与get类似,应该都是读取,不知道这里为什么还要再判断一下
const old = cache.peek(key)
if (old != data) {
cache.set(key, data)
if (updateTime) time[key] = updateTime
return true
} else return false
}
async function tryCache (key, checkUpdateTime = true) {
// 读取缓存,没有的话返回undefined
const data = cache.get(key)
// 这里判断了一下如果文件的更新时间晚于缓存更新时间,那么就直接return出去不管有没有缓存,重新写入缓存
if (checkUpdateTime) {
const cacheUpdateTime = time[key]
const fileUpdateTime = (await stat(path.resolve(root, key.replace(/^\//, '')))).mtime.getTime()
if (cacheUpdateTime < fileUpdateTime) return null
}
return data
}
}