共计 12927 个字符,预计需要花费 33 分钟才能阅读完成。
之前咱们曾经实现了一个不带依赖预构建版本的 vite, 在 vite2.0 中减少了一个代表性优化策略 依赖预构建
明天咱们也来手写一个。
咱们要实现的性能如下图所示:
- 依赖预构建性能
- 实现对 SFC 的解析
- 实现对 vue3 语法的解析
- 实现对 html 和 js 的解析
- 最初实现一个对数字的加减操作性能
代码分两局部
- 依赖预构建局部
- 本地服务局部
先开始编写依赖预构建局部
先把所须要的依赖引入
const http = require('http'); | |
const path = require('path'); | |
const url = require('url'); | |
const querystring = require('querystring'); | |
const glob = require('fast-glob'); // 在规定范畴内查问指定的文件,并返回绝对路径 | |
const {build} = require('esbuild'); // 打包编译 esm 模块 | |
const fs = require('fs'); | |
const os = require('os'); // 获取以后的零碎信息 | |
const {createHash} = require('crypto'); // 加密应用 | |
const {init, parse} = require('es-module-lexer'); // 查问出代码中应用 import 局部信息 | |
const MagicString = require('magic-string');// 替换代码中门路 | |
const compilerSfc = require('@vue/compiler-sfc');// 将 sfc 转化为 json 数据 | |
const compilerDom = require('@vue/compiler-dom');// 将 template 转化为 render 函数 |
编写依赖预构建主函数
async function optimizeDeps() { | |
// 第一步: 设置缓存存储的地位 | |
let cacheDir = 'node_modules/.vite'; | |
const dataPath = path.join(cacheDir, '_metadata.json'); | |
const mainHash = getDepHash(); | |
const data = { | |
hash: mainHash, | |
browserHash: mainHash, | |
optimized: {},}; | |
if (fs.existsSync(dataPath)) { | |
let prevData; | |
try { | |
// 解析.vite 下的_metadata.json 文件 | |
prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8')); | |
} catch (error) {console.log(error); | |
} | |
// 哈希是统一的,不须要从新绑定 | |
if (prevData && prevData.hash === data.hash) {return prevData;} | |
} | |
if (fs.existsSync(cacheDir)) { | |
// 如果 node_modules/.vite 这个文件存在则清空.vite 下的所有文件 | |
emptyDir(cacheDir); | |
} else {fs.mkdirSync(cacheDir, { recursive: true}); | |
} | |
// 第二步: 收集依赖模块门路 | |
const {deps, missing} = await scanImports(); | |
// { | |
// 'vue': ''C:\\Users\\dftd\\desktop\\vite\\node-vue\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js'' | |
// } | |
console.log('deps', deps); | |
// update browser hash | |
data.browserHash = createHash('sha256') | |
.update(data.hash + JSON.stringify(deps)) | |
.digest('hex') | |
.substr(0, 8); | |
const qualifiedIds = Object.keys(deps); | |
// 如果没有找到任何依赖,那么间接把 data 数据写入.vite/_metadata.json | |
if (!qualifiedIds.length) {fs.writeFileSync(dataPath, data); | |
return data; | |
} | |
// 第三步: 对收集的依赖进行解决 | |
// 比方 deps 的数据是 {'plamat/byte': 'C:\\Users\\dftd\\node_modules\\vue\\dist\\byte.js'} | |
const flatIdDeps = {}; // 这个对象存储的是 { plamat_byte: 'path'} 的模式 | |
const idToExports = {}; // 这个对象存储的是{ plamat/byte: 'souce'} 的模式 | |
const flatIdToExports = {}; // 这个对象存储的是{ plamat/byte: 'souce'} 的模式 | |
await init; | |
// 将 例如 node/example ==> node_example | |
const flattenId = (id) => id.replace(/[\/\.]/g, '_'); | |
for (const id in deps) {const flatId = flattenId(id); | |
flatIdDeps[flatId] = deps[id]; | |
const entryContent = fs.readFileSync(deps[id], 'utf-8'); | |
const exportsData = parse(entryContent); | |
for (const { ss, se} of exportsData[0]) {const exp = entryContent.slice(ss, se); | |
if (/export\s+\*\s+from/.test(exp)) {exportsData.hasReExports = true;} | |
} | |
idToExports[id] = exportsData; | |
flatIdToExports[flatId] = exportsData; | |
} | |
const define = {'process.env.NODE_ENV': 'development',}; | |
console.log('flatIdDeps', flatIdDeps); | |
function esbuildVitePlugin(flatIdDeps) { | |
return { | |
name: 'vite-scan', | |
setup(build) {build.onResolve({ filter: /\*/}, (args) => {console.log('打印后果', args); | |
}); | |
}, | |
}; | |
} | |
const result = await build({absWorkingDir: process.cwd(), | |
// entryPoints: Object.keys(flatIdDeps), | |
entryPoints: Object.values(flatIdDeps), | |
bundle: true, | |
format: 'esm', | |
outdir: cacheDir, // 配置打完包的文件存储的地位 cacheDir 默认为 | |
treeShaking: true, | |
metafile: true, | |
define, | |
// plugins: [esbuildVitePlugin(flatIdDeps)], | |
}); | |
// console.log('result', result); | |
const metafile = result.metafile; | |
// 将 _metadata.json 写入 .vite | |
const cacheDirOutputPath = path.relative(process.cwd(), cacheDir); | |
// console.log(cacheDirOutputPath); | |
for (const id in deps) { | |
// p ==> C:\Users\dftd\desktop\vite\node-vue\node_modules\.vite\vue.js | |
// normalizePath(p) ==> C:/Users/dftd/desktop/vite/node-vue/node_modules/.vite/vue.js | |
const p = path.resolve(cacheDir, flattenId(id) + '.js'); | |
const entry = deps[id]; | |
data.optimized[id] = {file: normalizePath(p), | |
src: normalizePath(entry), | |
needsInterop: false, | |
}; | |
} | |
fs.writeFileSync(dataPath, JSON.stringify(data, null, 2)); | |
return data; | |
} | |
function normalizePath(id) {const isWindows = os.platform() === 'win32'; | |
return path.posix.normalize(isWindows ? id.replace(/\\/g, '/') : id); | |
} | |
// 将此我的项目的 xxx.lock.json 文件和局部 vite 配置内容生成一个 hash | |
function getDepHash() { | |
// 读取 xxx.lock.json 文件的内容 | |
const content = lookupFile() || ''; | |
const cryptographicStr = createHash('sha256') | |
.update(content) | |
.digest('hex') | |
.substring(0, 8); | |
return cryptographicStr; | |
} | |
// 读取 xxx.lock.json 文件的内容 | |
function lookupFile() {const lockfileFormats = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']; | |
let content = null; | |
for (let index = 0; index < lockfileFormats.length; index++) {const lockPath = path.resolve(__dirname, lockfileFormats[index]); | |
const isExist = fs.existsSync(lockPath, 'utf-8'); | |
if (isExist) {content = fs.readFileSync(lockPath); | |
break; | |
} | |
} | |
// for (const index of lockfileFormats) {// console.log(index); | |
// const lockPath = path.resolve(__dirname, index); | |
// const isExist = fs.existsSync(lockPath, 'utf-8'); | |
// if (isExist) {// content = fs.readFileSync(lockPath); | |
// return content; | |
// } | |
// } | |
return content; | |
} | |
function emptyDir(dir) {for (const file of fs.readdirSync(dir)) {const abs = path.resolve(dir, file); | |
if (fs.lstatSync(abs).isDirectory()) {emptyDir(abs); | |
fs.rmdirSync(abs); | |
} else {fs.unlinkSync(abs); | |
} | |
} | |
} | |
// esbuild plugin | |
function esbuildScanPlugin(deps) { | |
return { | |
name: 'dep-scan', | |
setup(build) { | |
// 解析 index.html | |
build.onResolve({filter: /\.(html|vue)$/ }, (args) => {// console.log(args); | |
// const path1 = path.resolve(__dirname, args.path); | |
return { | |
path: args.path, | |
namespace: 'html', | |
}; | |
}); | |
// 加载以后 index.html 文件 返回出 main.js | |
build.onLoad({ filter: /\.(html|vue)$/, namespace: 'html' }, | |
async ({path: ids}) => { | |
const scriptModuleRE = | |
/(<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gims; | |
const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im; | |
const langRE = /\blang\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im; | |
const scriptRE = /(<script\b(?:\s[^>]*>|>))(.*?)<\/script>/gims; | |
const importsRE = | |
/(?:^|;|\*\/)\s*import(?!\s+type)(?:[\w*{}\n\r\t, ]+from\s*)?\s*("[^"]+"|'[^']+')\s*(?:$|;|\/\/|\/\*)/gm; | |
let raw = fs.readFileSync(ids, 'utf-8'); | |
raw = raw.replace(/<!--(.|[\r\n])*?-->/, '<!---->'); | |
const isHtml = ids.endsWith('.html'); | |
const regex = isHtml ? scriptModuleRE : scriptRE; | |
regex.lastIndex = 0; | |
let js = ''; | |
let loader = 'js'; | |
let match; | |
while ((match = regex.exec(raw))) {const [, openTag, content] = match; | |
const srcMatch = openTag.match(srcRE); | |
const langMatch = openTag.match(langRE); | |
const lang = | |
langMatch && (langMatch[1] || langMatch[2] || langMatch[3]); | |
if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {loader = lang;} | |
if (srcMatch) {const src = srcMatch[1] || srcMatch[2] || srcMatch[3]; | |
js += `import ${JSON.stringify(src)}\n`; | |
} else if (content.trim()) {js += content + '\n';} | |
} | |
if (loader.startsWith('ts') && | |
(ids.endsWith('.svelte') || | |
(ids.endsWith('.vue') && /<script\s+setup/.test(raw))) | |
) {// when using TS + (Vue + <script setup>) or Svelte, imports may seem | |
// unused to esbuild and dropped in the build output, which prevents | |
// esbuild from crawling further. | |
// the solution is to add `import 'x'` for every source to force | |
// esbuild to keep crawling due to potential side effects. | |
let m; | |
const original = js; | |
while ((m = importsRE.exec(original)) !== null) { | |
// This is necessary to avoid infinite loops with zero-width matches | |
if (m.index === importsRE.lastIndex) {importsRE.lastIndex++;} | |
js += `\nimport ${m[1]}`; | |
} | |
} | |
if (!js.includes(`export default`)) {js += `\nexport default {}`; | |
} | |
return { | |
loader, | |
contents: js, | |
}; | |
}, | |
); | |
// 解析第三方库的 esm js 模块文件 间接走打包 | |
build.onResolve( | |
{ | |
// avoid matching windows volume | |
filter: /\.js\?v=1$/, | |
}, | |
({path: id, importer}) => { | |
return { | |
path: id, | |
external: true, | |
}; | |
}, | |
); | |
// 解析.js 文件 | |
build.onResolve( | |
{ | |
// avoid matching windows volume | |
filter: /main\.js$/, | |
}, | |
({path: id, importer}) => { | |
return { | |
path: id, | |
namespace: 'mianJs', | |
}; | |
}, | |
); | |
// 加载.js 文件的内容 | |
build.onLoad({ filter: /main\.js$/, namespace: 'mianJs'}, | |
({path: id}) => {const c = fs.readFileSync(id, 'utf-8'); | |
const magicString = new MagicString(c); | |
let imports = parse(c)[0]; | |
imports.forEach((i) => {const { s, e, n} = i; | |
let absolutePath = path.resolve(__dirname, 'node_modules', n); | |
const isExist = fs.existsSync(absolutePath); | |
if (isExist) {const modulePath = require(absolutePath + '/package.json').module; | |
const esmPath = path.resolve(absolutePath, modulePath); | |
magicString.overwrite(s, e, `${esmPath}?v=1`); | |
deps[n] = esmPath; | |
} else {// let aa = path.resolve(__dirname, n); | |
// magicString.overwrite(s, e, aa); | |
} | |
}); | |
return { | |
loader: 'js', | |
contents: magicString.toString(),}; | |
}, | |
); | |
}, | |
}; | |
} | |
// 收集依赖模块门路 返回一个依赖合集对象 | |
async function scanImports() {const deps = {}; | |
const missing = {}; | |
let entries; | |
// 查问出当前目录下后缀为 html 的文件 | |
entries = await glob('**/*.html', {cwd: process.cwd(), | |
ignore: ['**/node_modules/**'], | |
absolute: true, | |
}); | |
// entries => ['C:/Users/dftd/Desktop/vite/node-vue/index.html'] | |
// console.log('entries', entries); | |
if (!entries.length) { | |
return {deps: {}, | |
missing: {},}; | |
} | |
const scanPath = esbuildScanPlugin(deps); | |
// 应用 esbuild 进行一次打包,打包过程中就找到了 deps 和 missing, 最初返回 deps 和 missing | |
// Promise.all(// entries.map((entry) => {// console.log(222); | |
// await build({// absWorkingDir: process.cwd(), // 工作的目录 | |
// entryPoints: [entry], // 入口文件(个别为 index.html) | |
// write: false, // build API 能够间接写入文件系统 默认状况下,JavaScript API 会写入文件系统 | |
// bundle: true, // 应用 analyze 性能生成一个对于 bundle 内容的易于浏览的报告 | |
// format: 'esm', // 输入的类型(iife, cjs, esm)// plugins: [scanPath], | |
// }); | |
// }), | |
// ); | |
await build({absWorkingDir: process.cwd(), // 工作的目录 | |
entryPoints: entries, // 入口文件(个别为 index.html) | |
write: false, // build API 能够间接写入文件系统 默认状况下,JavaScript API 会写入文件系统 | |
bundle: true, // 应用 analyze 性能生成一个对于 bundle 内容的易于浏览的报告 | |
format: 'esm', // 输入的类型(iife, cjs, esm)plugins: [scanPath], | |
}); | |
return { | |
deps, | |
missing, | |
}; | |
} |
依赖预构建总结:
依赖预构建产生在在本地服务启动之前执行, 大抵过程分为两步,第一步就是依赖的收集(这部分用到了 esbuild build 办法来收集所有的依赖门路),第二部是把这些收集的依赖门路再次用 esbuild build 办法进行编译,编译的后果存储到 node_modules/.vite 中, 并且还创立了一份_metadata.json 文件对构建出的依赖做记录。
接下来编写本地服务局部
function createServer() {optimizeDeps(); | |
const serve = {}; | |
let httpServe = http.createServer((req, res) => {let pathName = url.parse(req.url).pathname; | |
// console.log(22, pathName); | |
if (pathName == '/') {pathName = '/index.html';} | |
let extName = path.extname(pathName); | |
let extType = ''; | |
switch (extName) { | |
case '.html': | |
extType = 'text/html'; | |
break; | |
case '.js': | |
extType = 'application/javascript'; | |
break; | |
case '.css': | |
extType = 'text/css'; | |
break; | |
case '.ico': | |
extType = 'image/x-icon'; | |
break; | |
case '.vue': | |
extType = 'application/javascript'; | |
break; | |
default: | |
extType = 'text/html'; | |
} | |
if (/.vite/.test(pathName)) { | |
// 间接加载预构建好的包 | |
resolveViteModules(pathName, extType, res); | |
} else if (/\/@modules\//.test(pathName)) { | |
// 加载那些没有预构建好的包第三方包 | |
resolveNodeModules(pathName, res); | |
} else { | |
// 加载非第三方包文件 | |
resolveModules(pathName, extName, extType, res, req); | |
} | |
}); | |
serve.listen = () => {httpServe.listen(7777); | |
}; | |
return serve; | |
} | |
// 重写门路 | |
function rewriteImports(source) {let imports = parse(source)[0]; | |
const magicString = new MagicString(source); | |
if (imports.length) {for (let index = 0; index < imports.length; index++) {const { s, e} = imports[index]; | |
// 失去以后引入的第三方库的 name | |
const id = source.substring(s, e); | |
if (/^[^\.\/]/.test(id)) { | |
// 依据 id 到 node_modules/.vite/_metadata.json 中查问以后这个包是否存在 | |
const _metadataPath = path.resolve( | |
__dirname, | |
'node_modules/.vite/_metadata.json', | |
); | |
const _metaContent = require(_metadataPath).optimized; | |
console.log('_metaContent', _metaContent); | |
if (_metaContent[id]) {magicString.overwrite(s, e, `/node_modules/.vite/${id}`); | |
} else {magicString.overwrite(s, e, `/@modules/${id}`); | |
} | |
} | |
} | |
} | |
return magicString.toString();} | |
// 解决加载非第三方包文件 | |
function resolveModules(pathName, extName, extType, res, req) {fs.readFile(`.${pathName}`, 'utf-8', (err, data) => {if (err) {throw err;} | |
console.log('extName', extName); | |
console.log('extType', extType); | |
res.writeHead(200, {'Content-Type': `${extType}; charset=utf-8`, | |
}); | |
if (extName == '.vue') {const query = querystring.parse(url.parse(req.url).query); | |
const ret = compilerSfc.parse(data); | |
const {descriptor} = ret; | |
if (!query.type) { | |
// 解析出 vue 文件 script 局部 | |
const scriptBlock = descriptor.script.content; | |
const newScriptBlock = rewriteImports(scriptBlock.replace('export default', 'const __script ='), | |
); | |
const newRet = ` | |
${newScriptBlock} | |
import {render as __render} from '.${pathName}?type=template' | |
__script.render = __render | |
export default __script | |
`; | |
res.write(newRet); | |
} else { | |
// 解析出 vue 文件 template 局部 生成 render 函数 | |
const templateBlock = descriptor.template.content; | |
const compilerTemplateBlockRender = rewriteImports( | |
compilerDom.compile(templateBlock, {mode: 'module',}).code, | |
); | |
res.write(compilerTemplateBlockRender); | |
} | |
} else if (extName == '.js') {const r = rewriteImports(data); | |
res.write(r); | |
} else {res.write(data); | |
} | |
res.end();}); | |
} | |
// 在 node_modules 中读取没有预构建好的资源 | |
function resolveNodeModules(pathName, res) {const id = pathName.replace(/\/@modules\//, ''); | |
// 如果是加载的是第三方包 | |
// 获取第三方包的相对地址 | |
let absolutePath = path.resolve(__dirname, 'node_modules', id); | |
// console.log('absolutePath', absolutePath); | |
// 获取第三方包的 esm 的包地址 | |
const modulePath = require(absolutePath + '/package.json').module; | |
const esmPath = path.resolve(absolutePath, modulePath); | |
// console.log('esmPath', esmPath); | |
// const pkgPath = `./node_modules/${id}/${modulePath}`; | |
fs.readFile(esmPath, 'utf-8', (err, data) => {if (err) {throw err;} | |
res.writeHead(200, {'Content-Type': `application/javascript; charset=utf-8`,}); | |
// const b = cjsEs6(data); | |
// console.log(b); | |
const r = rewriteImports(data); | |
// console.log(r); | |
res.write(r); | |
res.end();}); | |
} | |
// 加载.vite 预构建好的包 | |
function resolveViteModules(pathName, extType, res) {const id = pathName.replace(/\/node_modules\/.vite\//, ''); | |
// 依据 id 到 node_modules/.vite/_metadata.json 中查问以后这个包是否存在 | |
const _metadataPath = path.resolve( | |
__dirname, | |
'node_modules/.vite/_metadata.json', | |
); | |
const _metaContent = require(_metadataPath).optimized; | |
const {src, file} = _metaContent[id]; | |
// 提取以后文件门路中的文件名称 | |
const absFileName = path.basename(src); | |
const absFile = path.resolve(__dirname, 'node_modules/.vite', absFileName); | |
const content = fs.readFileSync(absFile, 'utf-8'); | |
// console.log(1111, content); | |
// console.log(222, absFile); | |
res.writeHead(200, {'Content-Type': `application/javascript; charset=utf-8`,}); | |
res.write(content); | |
res.end();} | |
const serve = createServer(); | |
serve.listen(); |
本地服务局部总结:
我在上一篇中写过一篇不带预构建版本的 vite,这个版本是在之前的根底重写了 rewriteImports 办法把门路重写改为 /node_modules/.vite 能够间接加载预构建好的文件, 如果你还没有看我的不带预构建版本的 vite 倡议先看下,由浅入深能力更好的了解 vite 的实质原理。
正文完