理解Vite原理 - vue-dev-server源码浅析
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第11期 | 尤雨溪几年前写的100多行的玩具 vite
前言
在Vue官方Github组织下有一个仓库: vue-dev-server, 总计代码不到200行,展示了通过ES Module导入的方式是如何解析Vue单文件组件的,也算Vite最初的雏形
Vite官方文档:Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理
源码
入口文件
#!/usr/bin/env node
const 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作为服务端,Use了一个中间件, 这个中间件就是解析Vue单文件组件的核心
promisify
const stat = require('util').promisify(fs.stat)
在14期有说过 promisify的用法,juejin.cn/post/713974…, 这里就是将 fs.stat这个方法转为Promise的形式调用
loadPkg
const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)
async function loadPkg(pkg) {
//只有传入的值为 vue字符 才处理
if (pkg === 'vue') {
const dir = path.dirname(require.resolve('vue'))
//dir 取vue包路径, 拼接vue.esm.browser.js 拼接浏览器能识别的esm格式产物文件
const filepath = path.join(dir, 'vue.esm.browser.js')
//返回 读取这个文件的promise
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
loadPkg 的作用就是读取了 node_modules下 vue的产物文件 并返回
readSource
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
readSource 根据传入的文件路径, 拼接执行命名的目录 得到完整路径进行文件的读取 并返回
transformModuleImports
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
//将import 的路径 不包含. 并且符合 package name格式的 替换为 /_modules/前缀
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
这里使用了recast
将 文件代码中的import 路径 进行了处理,比如 main.js 内部的import Vue from 'vue'
会替换为 import Vue from '/__modules/vue'
文件的处理
if (req.path.endsWith('.vue')) {
//得到文件名
const key = parseUrl(req).pathname
let out = await tryCache(key)
//从缓存取出 时间对比 不存在就 通过 complier 编译这个vue单文件
if (!out) {
// Bundle Single-File Component
const result = await bundleSFC(req)
out = result
cacheData(key, out, result.updateTime)
}
send(res, out.code, 'application/javascript')
// js文件的处理
}
最后看到 vueMiddleware 会返回一个 Async函数, 内部会对不同类型下进行处理
如果是 Vue单文件, 会通过 bundleSFC 也就是Complier
进行代码编译,转为JS能处理的格式,最后Send 函数 将结果以及 文件类型 返回交给 Res, 浏览器就会拿到对应的代码内容
if (req.path.endsWith('.js')) {
const key = parseUrl(req).pathname
let out = await tryCache(key)
if (!out) {
// transform import statements
//得到js文件内容
const result = await readSource(req)
//通过recast 转换内部的 import 路径
out = transformModuleImports(result.source)
cacheData(key, out, result.updateTime)
}
//将处理后的结果返回Res
send(res, out, 'application/javascript')
}
这里js 主要做的就是 将 import 的非.字符和符合package name的路径替换为_modules/ 开头的路径
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) {
//loadPkg 就是从node_modules 里面读取 vue的产物文件
out = (await loadPkg(pkg)).toString()
//不需要时间对比 默认缓存
cacheData(key, out, false) // Do not outdate modules
}
//将产物代码返回,识别为JS格式
send(res, out, 'application/javascript')
}
经过前面JS文件的处理后,Express读取到前面JS文件内部替换的路径格式,再将内容返回给浏览器,至此就结束了,此时浏览器就能读取和识别Vue的单文件组件了
总结
- 浏览器访问 JS Vue文件时候,通过 recast处理 js 内部的 非文件路径的包引入 转为__modules开头的路径、通过Vue Complier处理 Vue文件的代码, 返回给浏览器
- 浏览器访问 modules路径的文件,则通过 loadPkg 读取Vue内的产物文件代码返回给浏览器使用,用于加载Vue
- 核心就是通过Exporess 的中间件 针对不同格式的文件进行了处理,使得客户端可以正确的加载到文件对应代码
转载自:https://juejin.cn/post/7246955055109324837