之前我们已经实现了一个不带依赖预构建版本的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() {
// 第一步:设置缓存存储的位置
const cacheDir = 'node_modules/.vite';
// _metadata.json中存储了所有预构建的依赖信息, 也存储到.vite文件夹中
const dataPath = path.join(cacheDir, '_metadata.json');
// getDepHash函数 将此项目的xxx.lock.json文件生成一个hash值(作用是如果我的依赖发生变化那我的lock文件也会发生更改,将来我的依赖预构建程序会在hash值发生变化的时候重新执行预构建程序)
const mainHash = getDepHash();
// 定义 _metadata.json中存储的数据格式
const data = {
hash: mainHash,
browserHash: mainHash, // 浏览器存储的hash值
optimized: {}, // 依赖包的信息
};
// 首先判断_metadata.json是否存在 如果存在则对下之前存储的hash值是否跟现在的hash一样,如果一样则依赖没有发生变化 不用执行预构建动作
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;
}
}
// 判断node_modules/.vite是否存在
if (fs.existsSync(cacheDir)) {
// 如果node_modules/.vite这个文件存在则清空.vite下的所有文件
emptyDir(cacheDir);
} else {
fs.mkdirSync(cacheDir, { recursive: true });
}
// scanImports 收集所有依赖模块的绝对路径
const { deps } = await scanImports();
// {
// 'vue': ''C:\\Users\\dftd\\desktop\\vite\\node-vue\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js''
// }
console.log('deps', deps);
// 更新浏览器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);
// 使用esbuild对收集的依赖进行编译
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: Object.values(flatIdDeps),
bundle: true,
format: 'esm',
outdir: cacheDir, // 配置打完包的文件存储的位置 cacheDir默认为
treeShaking: true,
metafile: true,
define,
});
const metafile = result.metafile;
// 将 _metadata.json 写入 .vite
const cacheDirOutputPath = path.relative(process.cwd(), cacheDir);
// _metadata.json中的依赖数据填充上
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,
};
}
// 将数据写入_metadata.json
fs.writeFileSync(dataPath, JSON.stringify(data, null, 2));
return data;
}
optimizeDeps函数中所涉及到工具函数
// 转化路径格式
function normalizePath(id) {
const isWindows = os.platform() === 'win32';
return path.posix.normalize(isWindows ? id.replace(/\\/g, '/') : id);
}
// 将此项目的xxx.lock.json文件内容生成一个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;
}
}
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 (作用是esbuild在打包过程中处理不同的文件,大致分为处理html、js、第三方包解析,目前是对main.js做单独的处理)
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 =
/(