vue-dev-server是尤大前些年写的“玩具vite”, 理解其源码对于理解Vite非常有帮助,开始学习吧!
1.学习准备工作
clone代码
git clone https://github.com/vuejs/vue-dev-server.git
了解功能:
如何工作:
1.浏览器请求导入作为原生 ES 模块导入 - 没有捆绑。
2.服务器拦截对 *.vue 文件的请求,即时编译它们,然后将它们作为 JavaScript 发回。
3.对于提供在浏览器中工作的 ES 模块构建的库,只需直接从 CDN 导入它们。
4.导入到 .js 文件中的 npm 包(仅包名称)会即时重写以指向本地安装的文件。
目前,仅支持 vue 作为特例。 其他包可能需要进行转换才能作为本地浏览器目标 ES 模块公开。
2.猜测实现方法
要实现的功能是在浏览器中导入vue的但文件组件,不需要build这个步骤。
因为浏览器能够识别的就是html还有js文件,css文件。所以vue要被转换成js, 有点像webpack那样。但是这里说不需要build,那么肯定需要别的翻译过程,在一个vue文件中可以导入其他依赖,可以写各种样式。。。剩下的不知道了,看源码吧!
3.大概了解代码流程
package.json中:
"bin": {
"vue-dev-server": "./bin/vue-dev-server.js"
}
所以看bin目录下的vue-dev-server.js文件
#!/usr/bin/env node
const express = require('express')
const { vueMiddleware } = require('../middleware')
const app = express()
// 方法返回 Node.js 进程的当前工作目录
const root = process.cwd();
// 使用vueMiddleware中间件
app.use(vueMiddleware())
//为了提供对静态资源文件(图片、csss文件、javascript文件)的服务,
//请使用Express内置的中间函数 express.static 。
//传递一个包含静态资源的目录给 express.static 中间件用于立刻开始提供文件
app.use(express.static(root))
//监听3000端口
app.listen(3000, () => {
console.log('server running at http://localhost:3000')
})
看定义vueMiddleware的文件,在这个文件中最后返回了一个函数,在这个函数里面有4个分支判断:
1.对.vue文件的处理
2.对js文件的处理
3.对__modules开头路径处理
4.其他
4.详细读代码
4.1 vue-dev-server.js
// vue-dev-server/bin/vue-dev-server.js
#!/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')
})
vue-dev-server启动了端口为3000的服务,应用了vueMiddleware中间件,下面重点看这个中间件的实现。
4.2 vueMiddleware
vue-dev-server/middleware.js
//vueCompiler 这个模块将一个像下面这样的单个文件 Vue 组件编译成一个 CommonJS 模块,
//可以在 Browserify/Webpack/Component/Duo 构建中使用。
const vueCompiler = require('@vue/component-compiler')
const fs = require('fs')
const stat = require('util').promisify(fs.stat)
const root = process.cwd()
const path = require('path')
const parseUrl = require('parseurl')
const { transformModuleImports } = require('./transformModuleImports')
const { loadPkg } = require('./loadPkg')
const { readSource } = require('./readSource')
// 默认需要缓存
const defaultOptions = {
cache: true
}
const vueMiddleware = (options = defaultOptions) => {
let cache
let time = {}
// 如果需要缓存
if (options.cache) {
// 引入LRU
const LRU = require('lru-cache')
// 新建一个LRU缓存实例
cache = new LRU({
max: 500,
length: function (n, key) { return n * 2 + key.length }
})
}
// 创建一个默认的编译器
const compiler = vueCompiler.createDefaultCompiler()
function send(res, source, mime) {
res.setHeader('Content-Type', mime)
res.end(source)
}
function injectSourceMapToBlock (block, lang) {
// 转成base64格式
const map = Base64.toBase64(
JSON.stringify(block.map)
)
let mapInject
// 判断是JS 还是 css
switch (lang) {
case 'js': mapInject = `//# sourceMappingURL=data:application/json;base64,${map}\n`; break;
case 'css': mapInject = `/*# sourceMappingURL=data:application/json;base64,${map}*/\n`; break;
default: break;
}
return {
// 解构
...block,
// 增加code字段
code: mapInject + block.code
}
}
// 调用injectSourceMapToBlock,lang参数传js
function injectSourceMapToScript (script) {
return injectSourceMapToBlock(script, 'js')
}
// 调用injectSourceMapToBlock,lang参数传css
function injectSourceMapsToStyles (styles) {
return styles.map(style => injectSourceMapToBlock(style, 'css'))
}
return async (req, res, next) => {
// 处理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')
} else if (req.path.endsWith('.js')) {
const key = parseUrl(req).pathname
let out = await tryCache(key)
if (!out) {
// transform import statements
const result = await readSource(req)
//对js中import的内容处理一下
out = transformModuleImports(result.source)
cacheData(key, out, result.updateTime)
}
send(res, out, 'application/javascript')
} else 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')
} else {
// 都不是则进入到下一个中间件的处理环节?
next()
}
}
}
exports.vueMiddleware = vueMiddleware
vueMiddleware最终返回一个函数,这个函数里主要做了4件事情:
对.vue结尾的文件进行处理;
对.js结尾买的文件进行处理;
对/__modules/开头的文件进行处理;
否则执行next方法交给下一个中间件处理。
4.3 readSource
readSource文件内容:
const path = require('path')
const fs = require('fs')
// require('util').promisify 把原来的异步回调方法改成返回 Promise 实例的方法
// 读取文件内容
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的作用是读取文件资源,返回文件的路径,文件的内容,以及文件的更新时间。
4.4 transformModuleImports
transformModuleImports文件内容:
const recast = require('recast')
const isPkg = require('validate-npm-package-name')
function transformModuleImports(code) {
// 获得代码对应的抽象语法树
const ast = recast.parse(code)
recast.types.visit(ast, {
// 处理import语句
visitImportDeclaration(path) {
const source = path.node.source.value
// 如果不是以./开头的 并且是一个合法的包名
if (!/^./?/.test(source) && isPkg(source)) {
// 将路径修改为__modules下面的?
path.node.source = recast.types.builders.literal(`/__modules/${source}`)
}
// ?指向?
this.traverse(path)
}
})
return recast.print(ast).code
}
exports.transformModuleImports = transformModuleImports
可以看到此方法是针对npm包进行转换,如对Vue的转换:
import Vue from 'vue' => import Vue from "/__modules/vue"
4.5 loadPkg
loadPkg的内容:
const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)
async function loadPkg(pkg) {
// 如果是vue则读取vue的浏览器版本
if (pkg === 'vue') {
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
此方法用于加载包,可以看到对vue进行了判断说明只支持vue文件。
5.调试验证
调试代码:(记录一些变量)
.js文件
处理__modules ( import Vue from 'vue' 被翻译成 __modules/....)
处理.vue (import App from './test.vue')
6.总结
vue-dev-server的主要逻辑在vueMiddleware中,此中间件的作用是对.js文件、.vue文件以及.css文件进行处理,最终可以让浏览器运行vue项目。
7.参考资料
node-npm发布包-package.json中bin的用法
川哥的解析本源码的文章:
尤雨溪几年前开发的“玩具 vite”,才100多行代码,却十分有助于理解 vite 原理
学完本期源码,你可以思考如下问题:
1.谈谈你你对node.js中间件的理解
2.vue-dev-server如何处理vue文件?
3.对于能够在浏览器中运行的ES模块构建的库,vue-dev-server如何处理?
4.vue-dev-server如何处理js文件中引入的npm包?
5.@vue/component-compiler的作用是什么?