Rumah > Artikel > hujung hadapan web > Analisis Mendalam Vite Learning 'Pengimbasan Kebergantungan'
Artikel ini akan memberi anda penjelasan yang mendalam tentang butiran pelaksanaan pengimbasan kebergantungan dalam Vite Hasil pengimbasan akhir ialah objek yang mengandungi nama berbilang modul Ia tidak melibatkan proses pra-pembinaan atau cara pra -produk binaan digunakan.
Apabila kami menjalankan Vite buat kali pertama, Vite akan melaksanakan pra-pembinaan pergantungan agar serasi dengan CommonJS dan UMD dan Meningkatkan prestasi. [Cadangan berkaitan: tutorial video vuejs]
Untuk pra-bina kebergantungan, anda mesti terlebih dahulu memahami dua isu ini:
Pra-bina Perkara adakah kandungan ? / Modul yang manakah perlu diprabina?
Bagaimana untuk mencari modul yang perlu pra-dibina?
Dua isu ini sebenarnya bergantung pada kandungan dan kaedah pelaksanaan pengimbasan.
Artikel ini akan menerangkan secara mendalam butiran pelaksanaan pengimbasan kebergantungan Hasil pengimbasan akhir ialah objek yang mengandungi nama berbilang modul, yang tidak melibatkan proses pra-pembinaan atau apa. produk pra-bina adalah. Jika anda berminat dengan bahagian kandungan ini, anda boleh mengikuti saya dan tunggu artikel seterusnya.
Terdapat banyak modul dalam projek, dan bukan semua modul akan pra-bina. Hanya import kosong (kebergantungan kosong) akan melaksanakan prapembinaan kebergantungan
Apakah import kosong?
Lihat terus pada contoh di bawah
// vue 是 bare import import xxx from "vue" import xxx from "vue/xxx" // 以下不是裸依赖 import xxx from "./foo.ts" import xxx from "/foo.ts"
Anda boleh membahagikannya secara ringkas:
Malah, Vite juga menilai perkara ini.
Berikut ialah pepohon pergantungan modul bagi projek Vue biasa
Keputusan imbasan pergantungan adalah seperti berikut:
[ "vue", "axios" ]
Mengapa hanya import kosong pra-bina?
Node.js mentakrifkan mekanisme pengalamatan import kosong - Cari di bawah node_modules dalam direktori semasa Jika tidak dijumpai, pergi ke node_modules dalam direktori atas sehingga Direktori ialah laluan akar dan tidak boleh pergi lebih tinggi.
import kosong biasanya merupakan modul yang dipasang oleh npm Ia adalah modul pihak ketiga, bukan kod yang kami tulis sendiri Secara amnya, ia adalah dan tidak akan diubah suai Oleh itu, melaksanakan pembinaan modul ini lebih awal akan membantu meningkatkan prestasi.
Sebaliknya, jika anda pra-membina kod yang ditulis oleh pembangun dan membungkus projek ke dalam fail ketulan, apabila pembangun mengubah suai kod, anda perlu melaksanakan semula binaan dan bungkusnya ke dalam fail ketulan, proses ini sebenarnya akan menjejaskan prestasi.
Adakah modul di bawah monorepo juga akan dibina terlebih dahulu?
Tidak. Kerana dalam kes monorepo, walaupun beberapa modul adalah import kosong, modul ini juga ditulis oleh pembangun sendiri dan bukan modul pihak ketiga, jadi Vite tidak membuat pra-membina modul ini.
Malah, Vite akan menentukan sama ada laluan sebenar modul berada dalam node_modules :
Idea Pelaksanaan
Mari kita lihat pepohon pergantungan modul ini sekali lagi :
Untuk mengimbas semua import kosong, anda perlu melintasi keseluruhan pepohon pergantungan, yang melibatkan pelintasan mendalam bagi pokok
Apabila kita membincangkan rentas pokok, secara amnya kita menumpukan pada dua perkara ini:
Nod daun semasa tidak perlu dilalui secara mendalam:
Apabila semua nod daun dilalui, objek import kosong yang direkodkan ialah hasil imbasan kebergantungan.
Idea pelaksanaan bergantung pada pengimbasan sebenarnya sangat mudah difahami, tetapi pemprosesan sebenar tidak mudah.
Mari kita lihat pemprosesan nod daun:
Ia boleh dinilai oleh id modul Modul yang id modulnya bukan laluan adalah import kosong. Apabila menemui modul ini, rekod kebergantungan dan tidak lagi merentasi secara mendalam.
menilai sebagai contoh, jika anda menemui modul, *.css
tidak diperlukan Sebarang pemprosesan tidak akan dilalui secara mendalam .
menukar kod kepada AST dan mendapatkan modul yang diperkenalkan oleh penyata import , atau Padankan dengan kerap semua modul yang diimport , dan kemudian terus merentasi modul ini secara mendalam
mengekstrak bahagian kod JS ini, dan kemudian menganalisis dan memprosesnya mengikut modul JS merentasi modul ini secara mendalam. Kami hanya perlu mengambil berat tentang bahagian JS di sini, dan modul tidak akan diperkenalkan di bahagian lain.
Pelaksanaan khususKami sudah mengetahui idea pelaksanaan pengimbasan kebergantungan,
Idea sebenarnya Ia tidak rumit, apa yang rumit ialah pemprosesan, terutamanya pemprosesan HTML, Vue dan modul lain. Vite menggunakan kaedah yang bijak di sini -
Gunakan alat esbuild untuk pembungkusanMengapakah pembungkusan esbuild boleh digunakan untuk menggantikan proses traversal dalam?
Pada asasnyaproses pembungkusan juga merupakan proses lintasan modul yang mendalam
Kaedah alternatif adalah seperti berikut: melalui pemalam dan menambah beberapa logik khas semasa proses penghuraian Tidak mengapa jika anda tidak memahami ini buat masa ini, akan ada contoh nanti 最后 dep 对象中收集到的依赖就是依赖扫描的结果,而这次 esbuild 的打包产物,其实是没有任何作用的,在依赖扫描过程中,我们只关心每个模块的处理过程,不关心构建产物 用 Rollup 处理可以吗? 其实也可以,打包工具基本上都会有解析和加载的流程,也能对模块进行 external 但是 esbuild 性能更好 这类文件有 由于依赖扫描过程,只关注引入的 JS 模块,因此可以直接丢弃掉其他不需要的内容,直接取其中 JS html 类型文件(包括 vue)的转换,有两种情况: 什么是虚拟模块? 是模块的内容并非直接从磁盘中读取,而是编译时生成。 举个例子, 为什么需要虚拟模块? 因为一个 html 类型文件中,允许有多个 script 标签,多个内联的 script 标签,其内容无法处理成一个 JS 文件 (因为可能会有命名冲突等原因) 既然无法将多个内联 script,就只能将它们分散成多个虚拟模块,然后分别引入了。 依赖扫描的入口 下面是扫描依赖的入口函数(为了便于理解,有删减和修改): 主要流程如下: 将项目内所有的 html 作为入口文件(排除 node_modules)。 将每个入口文件,用 esbuild 进行打包 这里的核心其实是 esbuild 插件 很多同学可能不知道 esbuild 插件是如何编写的,这里简单介绍一下: 每个模块都会经过解析(resolve)和加载(load)的过程: 插件可以定制化解析和加载的过程,下面是一些插件示例代码: 扫描插件的实现 部分参数解析: 例如 esbuild 默认能将模块路径转成真实路径,为什么还要用 因为 Vite/Rollup 的插件,也能扩展解析的流程,例如 alias 的能力,我们常常会在项目中用 因此不能用 esbuild 原生的解析流程进行解析。 这里 那么接下来就是插件的实现了,先回顾一下之前写的各类模块的处理: esbuild 本身就能处理 JS 语法,因此 JS 是不需要任何处理的,esbuild 能够分析出 JS 文件中的依赖,并进一步深入处理这些依赖。 这部分处理非常简单,直接匹配,然后 external 就行了 如: 解析过程很简单,只是用于过滤掉一些不需要的模块,并且标记 namespace 为 html 真正的处理在加载阶段: 加载阶段的主要做有以下流程: srcMatch[1] || srcMatch[2] || srcMatch[3] 是干嘛? 我们来看看匹配的表达式: 因为 src 可以有以下三种写法: 三种情况会出现其中一种,因此是三个捕获组 虚拟模块是如何加载成对应的 script 代码的? 虚拟模块的加载很简单,直接从 script 对象中,读取之前缓存起来的内容即可。 这样之后,我们就可以把 html 类型的模块,转换成 JS 了 扫描结果 下面是一个 depImport 对象的例子: 依赖扫描是预构建前的一个非常重要的步骤,这决定了 Vite 需要对哪些依赖进行预构建。 本文介绍了 Vite 会对哪些内容进行依赖预构建,然后分析了实现依赖扫描的基本思路 —— 深度遍历依赖树,并对各种类型的模块进行处理。然后介绍了 Vite 如何巧妙的使用 esbuild 实现这一过程。最后对这部分的源码进行了解析: 最后获取到的 depImport 是一个记录依赖以及其真实路径的对象
Depth traversal
esbuild packaging
Pemprosesan nod daun
esbuild bolehMenghuraikan dan memuatkan setiap modul (nod daun)
Kedua-dua proses ini boleh
dilanjutkan
深度遍历
esbuild 打包
叶子节点的处理
esbuild 可以对每个模块(叶子节点)进行解析和加载
可以通过插件对这两个过程进行扩展,加入一些特殊的逻辑
例如将 html 在加载过程中转换为 js
不深入处理模块
esbuild 可以在解析过程,指定当前解析的模块为 external
则 esbuild 不再深入解析和加载该模块。
深入遍历模块
正常解析模块(什么都不做,esbuild 默认行为),返回模块的文件真实路径
Sebagai contoh, letakkan html dalam Tukar kepada js semasa proses pemuatan
Jangan proses modul secara mendalam
esbuild boleh menentukan modul yang sedang dihuraikan sebagai luaran
Kemudian esbuild tidak lagi akan menghuraikan dan memuatkan modul
Melintasi modul secara mendalam
Menghuraikan modul secara normal (tidak melakukan apa-apa, esbuild tingkah laku lalai), kembalikan laluan sebenar fail modul tr>
Pemprosesan pelbagai modul
例子
处理
bare import
vue
在解析过程中,将裸依赖保存到 deps 对象中,设置为 external
其他 JS 无关的模块
less文件
在解析过程中,设置为 external
JS 模块
./mian.ts
正常解析和加载即可,esbuild 本身能处理 JS
html 类型模块
index.html
、app.vue
在加载过程中,将这些模块加载成 JS
Contoh
Pemprosesan
import kosong
vue
Semasa proses penghuraian, simpan kebergantungan kosong pada objek deps dan tetapkannya kepada luaran
Modul JS lain yang tidak berkaitan
fail kurang
td>semasa proses penghuraian , Tetapkan kepada
modul JS
Hanya menghuraikan dan memuatkan seperti biasa esbuild sendiri boleh mengendalikan JS luar./mian.ts
tbody>modul jenis html
index. .html
, app.vueSemasa proses pemuatan, muatkan modul ini ke dalam JS
html 类型的模块处理
html
、vue
等。之前我们提到了要将它们转换成 JS,那么到底要如何转换呢?
import
语句,引入外部 scriptsrc/main.ts
是磁盘中实际存在的文件,而 virtual-module:D:/project/index.html?id=0
在磁盘中是不存在的,需要借助打包工具(如 esbuild),在编译过程生成。源码解析
import { build } from 'esbuild'
export async function scanImports(config: ResolvedConfig): Promise
missing: Record<string>
}> {
// 将项目中所有的 html 文件作为入口,会排除 node_modules
let entries: string[] = await globEntries('**/*.html', config)
// 扫描到的依赖,会放到该对象
const deps: Record<string> = {}
// 缺少的依赖,用于错误提示
const missing: Record<string> = {}
// esbuild 扫描插件,这个是重点!!!
const plugin = esbuildScanPlugin(config, container, deps, missing, entries)
// 获取用户配置的 esbuild 自定义配置,没有配置就是空的
const { plugins = [], ...esbuildOptions } =
config.optimizeDeps?.esbuildOptions ?? {}
await Promise.all(
// 入口可能不止一个,分别用 esbuid 打包
entries.map((entry) =>
// esbuild 打包
build({
absWorkingDir: process.cwd(),
write: false,
entryPoints: [entry],
bundle: true,
format: 'esm',
// 使用插件
plugins: [...plugins, plugin],
...esbuildOptions
})
)
)
return {
deps,
missing
}
}</string></string></string>
esbuildScanPlugin
插件的实现,它定义了各类模块(叶子节点)的处理方式。function esbuildScanPlugin(config, container, deps, missing, entries){}
dep
、missing
对象被当做入参传入,在函数中,这两个对象的内容会在打包(插件运行)过程中被修改
vue
,会解析到实际 node_modules 中的 vue 的入口 js 文件const plugin = {
name: 'xxx',
setup(build) {
// 定制解析过程,所有的 http/https 的模块,都会被 external
build.onResolve({ filter: /^(https?:)?\/\// }, ({ path }) => ({
path,
external: true
}))
// 定制解析过程,给所有 less 文件 namespace: less 标记
build.onResolve({ filter: /.*\.less/ }, args => ({
path: args.path,
namespace: 'less',
}))
// 定义加载过程:只处理 namespace 为 less 的模块
build.onLoad({ filter: /.*/, namespace: 'less' }, () => {
const raw = fs.readFileSync(path, 'utf-8')
const content = // 省略 less 处理,将 less 处理成 css
return {
contents,
loader: 'css'
}
})
}
}
onResolve
、onLoad
定义解析和加载过程onResolve
的第一个参数为过滤条件,第二个参数为回调函数,解析时调用,返回值可以给模块做标记,如 external
、namespace
(用于过滤),还需要返回模块的路径
onResolve
会被依次调用,直到回调函数返回有效的值,后面的不再调用。如果都没有有效返回,则使用默认的解析方式onLoad
的第一个参数为过滤条件,第二个参数为回调函数,加载时调用,可以读取文件的内容,然后进行处理,最后返回加载的内容。onLoad
会被依次调用,直到回调函数返回有效的值,后面的不再调用。如果都没有有效返回,则使用默认的加载方式。function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string>,
missing: Record<string>,
entries: string[]
): Plugin</string></string>
config
:Vite 的解析好的用户配置container
:这里只会用到 container.resolveId
的方法,这个方法能将模块路径转成真实路径。vue
转成 xxx/node_modules/dist/vue.esm-bundler.js
。depImports
:用于存储扫描到的依赖对象,插件执行过程中会被修改missing
:用于存储缺少的依赖的对象,插件执行过程中会被修改entries
:存储所有入口文件的数组container.resolveId
?@
的别名代表项目的 src
路径。container
(插件容器)用于兼容 Rollup 插件生态,用于保证 dev 和 production 模式下,Vite 能有一致的表现。感兴趣的可查看《Vite 是如何兼容 Rollup 插件生态的》container.resolveId
会被再次包装一成 resolve
函数(多了缓存能力)const seen = new Map<string>()
const resolve = async (
id: string,
importer?: string,
options?: ResolveIdOptions
) => {
const key = id + (importer && path.dirname(importer))
// 如果有缓存,就直接使用缓存
if (seen.has(key)) {
return seen.get(key)
}
// 将模块路径转成真实路径
const resolved = await container.resolveId(
id,
importer && normalizePath(importer),
{
...options,
scan: true
}
)
// 缓存解析过的路径,之后可以直接获取
const res = resolved?.id
seen.set(key, res)
return res
}</string>
例子
处理
bare import
vue
在解析过程中,将裸依赖保存到 deps 对象中,设置为 external
其他 JS 无关的模块
less文件
在解析过程中,设置为 external
JS 模块
./mian.ts
正常解析和加载即可,esbuild 本身能处理 JS
html 类型模块
index.html
、app.vue
在加载过程中,将这些模块加载成 JS
JS 模块
其他 JS 无关的模块
// external urls
build.onResolve({ filter: /^(https?:)?\/\// }, ({ path }) => ({
path,
external: true
}))
// external css 等文件
build.onResolve(
{
filter: /\.(css|less|sass|scss|styl|stylus|pcss|postcss|json|wasm)$/
},
({ path }) => ({
path,
external: true
}
)
// 省略其他 JS 无关的模块
bare import
build.onResolve(
{
// 第一个字符串为字母或 @,且第二个字符串不是 : 冒号。如 vite、@vite/plugin-vue
// 目的是:避免匹配 window 路径,如 D:/xxx
filter: /^[\w@][^:]/
},
async ({ path: id, importer, pluginData }) => {
// depImports 为
if (depImports[id]) {
return externalUnlessEntry({ path: id })
}
// 将模块路径转换成真实路径,实际上调用 container.resolveId
const resolved = await resolve(id, importer, {
custom: {
depScan: { loader: pluginData?.htmlType?.loader }
}
})
// 如果解析到路径,证明找得到依赖
// 如果解析不到路径,则证明找不到依赖,要记录下来后面报错
if (resolved) {
if (shouldExternalizeDep(resolved, id)) {
return externalUnlessEntry({ path: id })
}
// 如果模块在 node_modules 中,则记录 bare import
if (resolved.includes('node_modules')) {
// 记录 bare import
depImports[id] = resolved
return {
path,
external: true
}
}
// isScannable 判断该文件是否可以扫描,可扫描的文件有 JS、html、vue 等
// 因为有可能裸依赖的入口是 css 等非 JS 模块的文件
else if (isScannable(resolved)) {
// 真实路径不在 node_modules 中,则证明是 monorepo,实际上代码还是在用户的目录中
// 是用户自己写的代码,不应该 external
return {
path: path.resolve(resolved)
}
} else {
// 其他模块不可扫描,直接忽略,external
return {
path,
external: true
}
}
} else {
// 解析不到依赖,则记录缺少的依赖
missing[id] = normalizePath(importer)
}
}
)
html 类型模块
index.html
、app.vue
const htmlTypesRE = /\.(html|vue|svelte|astro)$/
// html types: 提取 script 标签
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
// 将模块路径,转成文件的真实路径
const resolved = await resolve(path, importer)
if (!resolved) return
// 不处理 node_modules 内的
if (resolved.includes('node_modules'){
return
}
return {
path: resolved,
// 标记 namespace 为 html
namespace: 'html'
}
})
// 正则,匹配例子: <script></script>
const scriptModuleRE = /(<script>]*type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gims
// 正则,匹配例子: <script></script>
export const scriptRE = /(<script>]*>|>))(.*?)<\/script>/gims
build.onLoad(
{ filter: htmlTypesRE, namespace: 'html' },
async ({ path }) => {
// 读取源码
let raw = fs.readFileSync(path, 'utf-8')
// 去掉注释,避免后面匹配到注释
raw = raw.replace(commentRE, '<!---->')
const isHtml = path.endsWith('.html')
// scriptModuleRE: <script type=module></script>
// scriptRE: <script></script>
// html 模块,需要匹配 module 类型的 script,因为只有 module 类型的 script 才能使用 import
const regex = isHtml ? scriptModuleRE : scriptRE
// 重置正则表达式的索引位置,因为同一个正则表达式对象,每次匹配后,lastIndex 都会改变
// regex 会被重复使用,每次都需要重置为 0,代表从第 0 个字符开始正则匹配
regex.lastIndex = 0
// load 钩子返回值,表示加载后的 js 代码
let js = ''
let scriptId = 0
let match: RegExpExecArray | null
// 匹配源码的 script 标签,用 while 循环,因为 html 可能有多个 script 标签
while ((match = regex.exec(raw))) {
// openTag: 它的值的例子: <script>
// content: script 标签的内容
const [, openTag, content] = match
// 正则匹配出 openTag 中的 type 和 lang 属性
const typeMatch = openTag.match(typeRE)
const type =
typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3])
const langMatch = openTag.match(langRE)
const lang =
langMatch && (langMatch[1] || langMatch[2] || langMatch[3])
// 跳过 type="application/ld+json" 和其他非 non-JS 类型
if (
type &&
!(
type.includes('javascript') ||
type.includes('ecmascript') ||
type === 'module'
)
) {
continue
}
// esbuild load 钩子可以设置 应的 loader
let loader: Loader = 'js'
if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {
loader = lang
} else if (path.endsWith('.astro')) {
loader = 'ts'
}
// 正则匹配出 script src 属性
const srcMatch = openTag.match(srcRE)
// 有 src 属性,证明是外部 script
if (srcMatch) {
const src = srcMatch[1] || srcMatch[2] || srcMatch[3]
// 外部 script,改为用 import 用引入外部 script
js += `import ${JSON.stringify(src)}\n`
} else if (content.trim()) {
// 内联的 script,它的内容要做成虚拟模块
// 缓存虚拟模块的内容
// 一个 html 可能有多个 script,用 scriptId 区分
const key = `${path}?id=${scriptId++}`
scripts[key] = {
loader,
content,
pluginData: {
htmlType: { loader }
}
}
// 虚拟模块的路径,如 virtual-module:D:/project/index.html?id=0
const virtualModulePath = virtualModulePrefix + key
js += `export * from ${virtualModulePath}\n`
}
}
return {
loader: 'js',
contents: js
}
}
)</script>
const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
export const virtualModuleRE = /^virtual-module:.*/
// 匹配所有的虚拟模块,namespace 标记为 script
build.onResolve({ filter: virtualModuleRE }, ({ path }) => {
return {
// 去掉 prefix
// virtual-module:D:/project/index.html?id=0 => D:/project/index.html?id=0
path: path.replace(virtualModulePrefix, ''),
namespace: 'script'
}
})
// 之前的内联 script 内容,保存到 script 对象,加载虚拟模块的时候取出来
build.onLoad({ filter: /.*/, namespace: 'script' }, ({ path }) => {
return scripts[path]
})
{
"vue": "D:/app/vite/node_modules/.pnpm/vue@3.2.37/node_modules/vue/dist/vue.runtime.esm-bundler.js",
"vue/dist/vue.d.ts": "D:/app/vite/node_modules/.pnpm/vue@3.2.37/node_modules/vue/dist/vue.d.ts",
"lodash-es": "D:/app/vite/node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/lodash.js"
}
总结
Atas ialah kandungan terperinci Analisis Mendalam Vite Learning 'Pengimbasan Kebergantungan'. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!