所有依赖已经收集完毕, 接下来需要做的对收集完的依赖进行一些处理, esbuild 生成具有最低公共祖先基的嵌套目录输出,这是不可预测的,因此很难分析输入和输出。所以展平所有 id 以消除斜杠, 在插件中,将条目作为虚拟文件读取以保留路径。
# 展平所有入口 id
const flatIdDeps: Record<string, string> = {}
export const flattenId = (id: string): string => id.replace(/[\/\.]/g, '_')
const flatIdDeps: Record<string, string> = {}
for (const id in deps) {
const flatId = flattenId(id)
flatIdDeps[flatId] = deps[id]
}
声明flatIdDeps为展平化的依赖映射,把 / 与 . 重写为 _
# 转化示例:
@ant-design-vue/use => @ant-design-vue_use
lodash.debounce => lodash_debounce
flatIdDeps 与 deps 不同的就是一个是展平后的key值
# 为什么需要展平 id
假设不进行展平, 在构建元信息中 optimized 对于构建依赖的模块信息描述将会以下结果:
"@ant-design/icons-vue": {
"file": "/Users/admin/crm/node_modules/.vite/@ant-design/icons-vue.js",
"src": "/Users/admin/crm/node_modules/@ant-design/icons-vue/es/index.js",
"needsInterop": false
},
对于 file 是构建结果的文件路径, @ant-design/icons-vue.js 是一个完整的 NPM 模块名称, 当加载时会当路径进行路径, 并不会达到预期效果, 所以最后通过展平 id 让 / 和 . 转换成 _ 保证正确的路径读取。正确结果如下:
"@ant-design/icons-vue": {
"file": "/Users/admin/crm/node_modules/.vite/@ant-design_icons-vue.js",
"src": "/Users/admin/crm/node_modules/@ant-design/icons-vue/es/index.js",
"needsInterop": false
},
esbuild 构建时候会在 entryPoint 以数组的方式传入传入收集来的构建依赖 id 模块名称。构建完后会以这些模块名称作为构建结果的文件名。通过展平之后, 生成的文件名称就与路径寻找的文件名称保持一致。
# es-module-lexer 对依赖的模块内容进行词法解析
通过es-module-lexer (opens new window)需要对所有构建依赖的入口文件进行词法分析解决两个问题:
- 防止构建生成重复代码, 对构建入口文件进行模块代理, 判断是
cjs模块还是esm模块, 生成不同的代理方法。 - 对于
cjs与esm的导入导出方式混用, 导致 esbuild 构建导出结果变异, 需要判断是否是cjs模块,以needsInterop作为标识, 可能会存在需要进行强制操作提取导出结果。
import { init, parse } from 'es-module-lexer'
const idToExports: Record<string, ExportsData> = {}
const flatIdToExports: Record<string, ExportsData> = {}
await init
for (const id in deps) {
const entryContent = fs.readFileSync(deps[id], 'utf-8')
const exportsData = parse(entryContent) as ExportsData
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
}
# 词法分析构建依赖入口文件中的内容
对依赖进行循环, entryContent变量收集依赖入口文件的文本内容。
exportsData 得到需要构建依赖的入口进行 paser 词法分析的结果。
构建入口文件内容词法分析示例
import one from './one.js';
export { two } from './two.js';
export const three = 3
console.log(1)
function demo() {
console.log(2)
}
exportsData 得到的词法分析结果为:
[
{ n: './one.js', s: 22, e: 30, ss: 5, se: 31, d: -1, a: -1 },
{ n: './two.js', s: 58, e: 66, ss: 37, se: 67, d: -1, a: -1 }
] [ 'two', 'three' ] true
- 数组中第一个下标为对
import词法分析的内容。 - 数组中第二个下标为
export词法分析的内容。
# 截取 import 内容
对 import 的词法进行循环,截取每行 import 的全部内容。
截取的内容
import one from './one.js';
export { two } from './two.js';
将对截取的条目进行匹配, 匹配方式 /export\s+\*\s+from/,如果有同时进行导入导出 export 语法,则在解析后的对象 exportsData 上设置 hasReExports:true。
存储词法分析后的内容
对每个依赖的词法解析后的结果进行对象存储
idToExports原始依赖模块名称的存储。flatIdToExports展平后依赖模块名称的存储。
# 定义全局常量替换
在构建时候, 代码块的内容往往存在一些全局的常量,有些根据项目封装 node_modules 包模块模,或者通过 include 手动收集的业务模块, 会存在以下代码内容。
if (process.env.NODE_ENV === 'development') {
...
}
if (process.env.TEST_ENV === 'test') {
...
}
在构建中 process.env.TEST_ENV 和 process.env.NODE_ENV 并不会读取环境变量中的内容,通过 define 可以把环境变量作为常量进行对待,启动时可以进行对环境量变进行获取,写入 define 之后, esbuild 在构建时传入 define,会进行自动替换这些环境变量。
定义内容的环境常量
const define: Record<string, string> = {
'process.env.NODE_ENV': JSON.stringify(config.mode)
}
对 pocess.env.NODE_ENV 定义为设置的 mode 模式。
寻找自定义常量
for (const key in config.define) {
const value = config.define[key]
define[key] = typeof value === 'string' ? value : JSON.stringify(value)
}
在 config 配置中有 define API 选项,通过此选项可以自定义一些常量名和值,同时把这些定义的常量赋值给 esbuild 需要的 define 变量上。这样 esbuild 在构建时就可以进行未能解析的常量替换。
开始构建
const { plugins = [], ...esbuildOptions } =
config.optimizeDeps?.esbuildOptions ?? {}
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: Object.keys(flatIdDeps),
bundle: true,
format: 'esm',
external: config.optimizeDeps?.exclude,
logLevel: 'error',
splitting: true,
sourcemap: true,
outdir: cacheDir,
treeShaking: 'ignore-annotations',
metafile: true,
define,
plugins: [
...plugins,
esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr)
],
...esbuildOptions
})
通过 esbuild 的 build 方法进行开始构建。
entryPoints构建入口,写入的则是展平化的依赖模块路径。bundle进行递归的方式进行解析。format构建成esm导出的模块。external过滤optimizeDeps.exclude强制排出的依赖。logLevel提示级别。splitting对构建时对相同的依赖做代码分隔。sourcemap产生sourcemap文件。outdir输出目录,为设置缓存文件的目录。treeShaking构建时去除注释内容。metafile生成文件元信息。define常量替换的内容。plugins构建插件。