本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第11期 | 尤雨溪几年前写的100多行的玩具 vite
前言
在Vue官方Github组织下有一个仓库: vue-dev-server, 总计代码不到200行,展示了通过ES Module导入的方式是如何解析Vue单文件组件的,也算Vite最初的雏形
Vite官方文档:Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理
源码
入口文件
#!/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')})#!/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') })#!/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)const stat = require('util').promisify(fs.stat)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')//返回 读取这个文件的promisereturn 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 = loadPkgconst 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 = loadPkgconst 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 = readSourceconst 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 = readSourceconst 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 = transformModuleImportsconst 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 = transformModuleImportsconst 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).pathnamelet out = await tryCache(key)//从缓存取出 时间对比 不存在就 通过 complier 编译这个vue单文件if (!out) {// Bundle Single-File Componentconst result = await bundleSFC(req)out = resultcacheData(key, out, result.updateTime)}send(res, out.code, 'application/javascript')// js文件的处理}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文件的处理 }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).pathnamelet 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)}//将处理后的结果返回Ressend(res, out, 'application/javascript')}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') }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).pathnameconst pkg = req.path.replace(/^\/__modules\//, '')let out = await tryCache(key, false) // Do not outdate modulesif (!out) {//loadPkg 就是从node_modules 里面读取 vue的产物文件out = (await loadPkg(pkg)).toString()//不需要时间对比 默认缓存cacheData(key, out, false) // Do not outdate modules}//将产物代码返回,识别为JS格式send(res, out, 'application/javascript')}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') }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 的中间件 针对不同格式的文件进行了处理,使得客户端可以正确的加载到文件对应代码