作者简介
19组清风,携程资深前端开发工程师,负责商旅前端公共基础平台建设,关注NodeJs、研究效能领域。
团队热招岗位:高级/资深前端开发工程师
本文总结了携程商旅大前端团队在将框架从 Remix 1.0 升级至 Remix 2.0 过程中遇到的问题和解决方案,特别是针对 Vite 在动态模块加载优化中引发的资源加载问题。文章详细探讨了 Vite 优化 DynamicImport 的机制,并介绍了团队为解决动态引入导致 404 问题所做的定制化处理。
<script type="module">
import init from 'assets/contact-GID3121.js';
init();
// ...
</script>
<script type="module">
import init from 'https://aw-s.tripcdn.com/assets/contact-GID3121.js';
init();
// ...
</script>
import React, { Suspense, useState } from 'react';
// 出行人组件,立即加载
const Travelers = () => {
return <div>出行人组件内容</div>;
};
// 联系人组件,使用 React.lazy 进行懒加载
const Contact = React.lazy(() => import('./Contact'));
const App = () => {
const [showContact, setShowContact] = useState(false);
const handleAddContactClick = () => {
setShowContact(true);
};
return (
<div>
<h1>页面标题</h1>
{/* 出行人组件立即展示 */}
<Travelers />
{/* 添加按钮 */}
<button onClick={handleAddContactClick}>添加联系人</button>
{/* 懒加载的联系人组件 */}
{showContact && (
<Suspense fallback={<div>加载中...</div>}>
<Contact />
</Suspense>
)}
</div>
);
};
export default App;
// app.tsx
import React, { Suspense } from 'react';
// 联系人组件,使用 React.lazy 进行懒加载
const Contact = React.lazy(() => import('./components/Contact'));
// 这里的手机号组件、姓名组件可以忽略
// 实际上特意这么写是为了利用 dynamicImport 的 splitChunk 特性
// vite 在构建时对于 dynamicImport 的模块是会进行 splitChunk 的
// 自然 Phone、Name 模块在构建时会被拆分为两个 chunk 文件
const Phone = () => import('./components/Phone');
const Name = () => import('./components/Name');
// 防止被 sharking
console.log(Phone,'Phone')
console.log(Name,'Name')
const App = () => {
return (
<div>
<h1>页面标题</h1>
{/* 懒加载的联系人组件 */}
(
<Suspense fallback={<div>加载中...</div>}>
<Contact />
</Suspense>
)
</div>
);
};
export default App;
// components/Contact.tsx
import React from 'react';
import Phone from './Phone';
import Name from './Name';
const Contact = () => {
return <div>
<h3>联系人组件</h3>
{/* 联系人组件依赖的手机号以及姓名组件 */}
<Phone></Phone>
<Name></Name>
</div>;
};
export default Contact;
// components/Phone.tsx
import React from 'react';
const Phone = () => {
return <div>手机号组件</div>;
};
export default Phone;
// components/Name.tsx
import React from 'react';
const Name = () => {
return <div>姓名组件</div>;
};
export default Name;
页面中使用 dynamicImport 引入了三个模块,分别为:
对于 App.tsx 中动态引入的 Phone 和 Name 模块,我们仅仅是利用动态引入实现在构建时的代码拆分。所以这里在 App.tsx 中完全可以忽略这两个模块。
简单来说,这便是 vite 对于使用 dynamicImport 异步引入模块的优化方式,默认情况下 Vite 会对于使用 dynamicImport 的模块收集当前模块的依赖进行 modulepreload 进行预加载。
const Contact = React.lazy(() => import('./components/Contact'))
const Contact = React.lazy(() =>
__vitePreload(() => import('./Contact-BGa5hZNp.js'), __vite__mapDeps([0, 1, 2])))
__vite__mapDeps([0, 1, 2])则是传递给 __vitePreload 的第二个参数,它表示当前动态引入的 dynamicImport 包含的所有依赖 chunk,也就是 Contact(自身)、Phone、Name 三个 chunk。
export const isModernFlag = `__VITE_IS_MODERN__`
export const preloadMethod = `__vitePreload`
export const preloadMarker = `__VITE_PRELOAD__`
export const preloadBaseMarker = `__VITE_PRELOAD_BASE__`
//...
// transform hook 会在每一个 module 上执行
async transform(source, importer) {
// 如果当前模块是在 node_modules 中,且代码中没有任何动态导入语法,则直接返回。不进行任何处理
if (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) {
return
}
// 初始化 es-module-lexer
await init
let imports: readonly ImportSpecifier[] = []
try {
// 调用 es-module-lexer 的 parse 方法,解析 source 中所有的 import 语法
imports = parseImports(source)[0]
} catch (_e: unknown) {
const e = _e as EsModuleLexerParseError
const { message, showCodeFrame } = createParseErrorInfo(
importer,
source,
)
this.error(message, showCodeFrame ? e.idx : undefined)
}
if (!imports.length) {
return null
}
// environment.config.consumer === 'client' && !config.isWorker && !config.build.lib
// 客户端构建时(非 worker 非 lib 模式下)为 true
const insertPreload = getInsertPreload(this.environment)
// when wrapping dynamic imports with a preload helper, Rollup is unable to analyze the
// accessed variables for treeshaking. This below tries to match common accessed syntax
// to "copy" it over to the dynamic import wrapped by the preload helper.
// 当使用预加载助手(__vite_preload 方法)包括 dynamicImport 时
// Rollup 无法分析访问的变量是否存在 TreeShaking
// 下面的代码主要作用为试图匹配常见的访问语法,以将其“复制”到由预加载帮助程序包装的动态导入中
// 例如:`const {foo} = await import('foo')` 会被转换为 `const {foo} = await __vitePreload(async () => { const {foo} = await import('foo');return {foo}}, ...)` 简单说就是防止直接使用 __vitePreload 包裹后的模块无法被 TreeShaking
const dynamicImports: Record<
number,
{ declaration?: string; names?: string }
> = {}
if (insertPreload) {
let match
while ((match = dynamicImportTreeshakenRE.exec(source))) {
/* handle `const {foo} = await import('foo')`
*
* match[1]: `const {foo} = await import('foo')`
* match[2]: `{foo}`
* import end: `const {foo} = await import('foo')_`
* ^
*/
if (match[1]) {
dynamicImports[dynamicImportTreeshakenRE.lastIndex] = {
declaration: `const ${match[2]}`,
names: match[2]?.trim(),
}
continue
}
/* handle `(await import('foo')).foo`
*
* match[3]: `(await import('foo')).foo`
* match[4]: `.foo`
* import end: `(await import('foo'))`
* ^
*/
if (match[3]) {
let names = /\.([^.?]+)/.exec(match[4])?.[1] || ''
// avoid `default` keyword error
if (names === 'default') {
names = 'default: __vite_default__'
}
dynamicImports[
dynamicImportTreeshakenRE.lastIndex - match[4]?.length - 1
] = { declaration: `const {${names}}`, names: `{ ${names} }` }
continue
}
/* handle `import('foo').then(({foo})=>{})`
*
* match[5]: `.then(({foo})`
* match[6]: `foo`
* import end: `import('foo').`
* ^
*/
const names = match[6]?.trim()
dynamicImports[
dynamicImportTreeshakenRE.lastIndex - match[5]?.length
] = { declaration: `const {${names}}`, names: `{ ${names} }` }
}
}
let s: MagicString | undefined
const str = () => s || (s = new MagicString(source))
let needPreloadHelper = false
// 遍历当前模块中的所有 import 引入语句
for (let index = 0; index < imports.length; index++) {
const {
s: start,
e: end,
ss: expStart,
se: expEnd,
d: dynamicIndex,
a: attributeIndex,
} = imports[index]
// 判断是否为 dynamicImport
const isDynamicImport = dynamicIndex > -1
// 删除 import 语句的属性导入
// import { someFunction } from './module.js' with { type: 'json' };
// => import { someFunction } from './module.js';
if (!isDynamicImport && attributeIndex > -1) {
str().remove(end + 1, expEnd)
}
// 如果当前 import 语句为 dynamicImport 且需要插入预加载助手
if (
isDynamicImport &&
insertPreload &&
// Only preload static urls
(source[start] === '"' ||
source[start] === "'" ||
source[start] === '`')
) {
needPreloadHelper = true
// 获取本次遍历到的 dynamic 的 declaration 和 names
const { declaration, names } = dynamicImports[expEnd] || {}
// 之后的逻辑就是纯字符串拼接,将 __vitePreload(preloadMethod) 变量进行拼接
// import ('./Phone.tsx')
// __vitePreload(
// async () => {
// const { Phone } = await import('./Phone.tsx')
// return { Phone }
// },
// __VITE_IS_MODERN__ ? __VITE_PRELOAD__ : void 0,
// )
if (names) {
/* transform `const {foo} = await import('foo')`
* to `const {foo} = await __vitePreload(async () => { const {foo} = await import('foo');return {foo}}, ...)`
*
* transform `import('foo').then(({foo})=>{})`
* to `__vitePreload(async () => { const {foo} = await import('foo');return { foo }},...).then(({foo})=>{})`
*
* transform `(await import('foo')).foo`
* to `__vitePreload(async () => { const {foo} = (await import('foo')).foo; return { foo }},...)).foo`
*/
str().prependLeft(
expStart,
`${preloadMethod}(async () => { ${declaration} = await `,
)
str().appendRight(expEnd, `;return ${names}}`)
} else {
str().prependLeft(expStart, `${preloadMethod}(() => `)
}
str().appendRight(
expEnd,
// renderBuiltUrl 和 isRelativeBase 可以参考 vite base 配置以及 renderBuildUrl 配置
`,${isModernFlag}?${preloadMarker}:void 0${
renderBuiltUrl || isRelativeBase ? ',import.meta.url' : ''
})`,
)
}
}
// 如果该模块标记饿了 needPreloadHelper 并且当前执行环境 insertPreload 为 true,同时该模块代码中不存在 preloadMethod 的引入,则在该模块的顶部引入 preloadMethod
if (
needPreloadHelper &&
insertPreload &&
!source.includes(`const ${preloadMethod} =`)
) {
str().prepend(`import { ${preloadMethod} } from "${preloadHelperId}";`)
}
if (s) {
return {
code: s.toString(),
map: this.environment.config.build.sourcemap
? s.generateMap({ hires: 'boundary' })
: null,
}
}
},
1)扫描动态导入语句:在每个模块中使用 es-module-lexer 扫描所有的 dynamicImport 语句。例如,对于 app.tsx 文件,会扫描到 import ('./Contact.tsx') 这样的动态导入语句。
2)注入预加载 Polyfill:对于所有的动态导入语句,使用 magic-string 克隆一份源代码,然后结合第一步扫描出的 dynamicImport 语句进行字符串拼接,注入预加载 Polyfill。例如,import ('./Contact.tsx') 经过 transform 钩子处理后会被转换为:
__vitePreload(
async () => {
const { Contact } = await import('./Contact.tsx')
return { Contact }
},
__VITE_IS_MODERN__ ? __VITE_PRELOAD__ : void 0,
''
)
import { ${preloadMethod} } from "${preloadHelperId}"
// ...
// ...
resolveId(id) {
if (id === preloadHelperId) {
return id
}
},
load(id) {
// 当检测到引入的模块路径为 ${preloadHelperId} 时
if (id === preloadHelperId) {
// 判断是否开启了 modulePreload 配置
const { modulePreload } = this.environment.config.build
// 判断是否需要 polyfill
const scriptRel =
modulePreload && modulePreload.polyfill
? `'modulepreload'`
: `/* @__PURE__ */ (${detectScriptRel.toString()})()`
// 声明对于 dynamicImport 模块深层依赖的路径处理方式
// 比如对于使用了 dynamicImport 引入的 Contact 模块,模块内部又依赖了 Phone 和 Name 模块
// 这里 assetsURL 方法就是在执行对于 Phone 和 Name 模块 preload 时是否需要其他特殊处理
// 关于 renderBuiltUrl 可以参考 Vite 文档说明 https://vite.dev/guide/build.html#advanced-base-options
// 我们暂时忽略 renderBuiltUrl ,因为我们构建时并未传入该配置
// 自然 assetsURL = `function(dep) { return ${JSON.stringify(config.base)}+dep }`
const assetsURL =
renderBuiltUrl || isRelativeBase
? // If `experimental.renderBuiltUrl` is used, the dependencies might be relative to the current chunk.
// If relative base is used, the dependencies are relative to the current chunk.
// The importerUrl is passed as third parameter to __vitePreload in this case
`function(dep, importerUrl) { return new URL(dep, importerUrl).href }`
: // If the base isn't relative, then the deps are relative to the projects `outDir` and the base
// is appended inside __vitePreload too.
`function(dep) { return ${JSON.stringify(config.base)}+dep }`
// 声明 assetsURL 方法,声明 preloadMethod 方法
const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}`
return { code: preloadCode, moduleSideEffects: false }
}
},
// ...
function detectScriptRel() {
const relList =
typeof document !== 'undefined' && document.createElement('link').relList
return relList && relList.supports && relList.supports('modulepreload')
? 'modulepreload'
: 'preload'
}
declare const scriptRel: string
declare const seen: Record<string, boolean>
function preload(
baseModule: () => Promise<unknown>,
deps?: string[],
importerUrl?: string,
) {
let promise: Promise<PromiseSettledResult<unknown>[] | void> =
Promise.resolve()
// @ts-expect-error __VITE_IS_MODERN__ will be replaced with boolean later
if (__VITE_IS_MODERN__ && deps && deps.length > 0) {
const links = document.getElementsByTagName('link')
const cspNonceMeta = document.querySelector<HTMLMetaElement>(
'meta[property=csp-nonce]',
)
// `.nonce` should be used to get along with nonce hiding (https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce#accessing_nonces_and_nonce_hiding)
// Firefox 67-74 uses modern chunks and supports CSP nonce, but does not support `.nonce`
// in that case fallback to getAttribute
const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute('nonce')
promise = Promise.allSettled(
deps.map((dep) => {
// @ts-expect-error assetsURL is declared before preload.toString()
dep = assetsURL(dep, importerUrl)
if (dep in seen) return
seen[dep] = true
const isCss = dep.endsWith('.css')
const cssSelector = isCss ? '[rel="stylesheet"]' : ''
const isBaseRelative = !!importerUrl
// check if the file is already preloaded by SSR markup
if (isBaseRelative) {
// When isBaseRelative is true then we have `importerUrl` and `dep` is
// already converted to an absolute URL by the `assetsURL` function
for (let i = links.length - 1; i >= 0; i--) {
const link = links[i]
// The `links[i].href` is an absolute URL thanks to browser doing the work
// for us. See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-domstring-5
if (link.href === dep && (!isCss || link.rel === 'stylesheet')) {
return
}
}
} else if (
document.querySelector(`link[href="${dep}"]${cssSelector}`)
) {
return
}
const link = document.createElement('link')
link.rel = isCss ? 'stylesheet' : scriptRel
if (!isCss) {
link.as = 'script'
}
link.crossOrigin = ''
link.href = dep
if (cspNonce) {
link.setAttribute('nonce', cspNonce)
}
document.head.appendChild(link)
if (isCss) {
return new Promise((res, rej) => {
link.addEventListener('load', res)
link.addEventListener('error', () =>
rej(new Error(`Unable to preload CSS for ${dep}`)),
)
})
}
}),
)
}
function handlePreloadError(err: Error) {
const e = new Event('vite:preloadError', {
cancelable: true,
}) as VitePreloadErrorEvent
e.payload = err
window.dispatchEvent(e)
if (!e.defaultPrevented) {
throw err
}
}
return promise.then((res) => {
for (const item of res || []) {
if (item.status !== 'rejected') continue
handlePreloadError(item.reason)
}
return baseModule().catch(handlePreloadError)
})
}
1)第一个参数是原始的模块引入语句,例如 import('./Phone')。
2)第二个参数是被 dynamicImport 加载的模块的所有依赖,这些依赖需要被添加为 modulepreload。
3)第三个参数是 import.meta.url(生成的资源的 JavaScript 路径)或空字符串,这取决于 renderBuiltUrl 或 isRelativeBase 的值。在这里,我们并没有传入 renderBuiltUrl 或 isRelativeBase。
// ...
renderChunk(code, _, { format }) {
// make sure we only perform the preload logic in modern builds.
if (code.indexOf(isModernFlag) > -1) {
const re = new RegExp(isModernFlag, 'g')
const isModern = String(format === 'es')
if (this.environment.config.build.sourcemap) {
const s = new MagicString(code)
let match: RegExpExecArray | null
while ((match = re.exec(code))) {
s.update(match.index, match.index + isModernFlag.length, isModern)
}
return {
code: s.toString(),
map: s.generateMap({ hires: 'boundary' }),
}
} else {
return code.replace(re, isModern)
}
}
return null
},
如果存在,则会判断生成的 chunk 是否为 esm 格式。如果是的话,则会将 isModernFlag 全部替换为 true,否则会全部替换为 false。
如果不存在则不会进行任何处理。
// transform 后对于 dynamicImport 的处理
__vitePreload(
async () => {
const { Contact } = await import('./Contact.tsx')
return { Contact }
},
__VITE_IS_MODERN__ ? __VITE_PRELOAD__ : void 0,
)
__vitePreload(
async () => {
const { Contact } = await import('./Contact.tsx')
return { Contact }
},
true ? __VITE_PRELOAD__ : void 0,
''
)
__vitePreload(
async () => {
const { Contact } = await import('./Contact.tsx')
return { Contact }
},
__VITE_PRELOAD__,
''
)
generateBundle({ format }, bundle) {
// 检查生成模块规范如果不为 es 则直接返回
if (format !== 'es') {
return
}
// 如果当前环境并为开启 modulePreload 的优化
// if (!getInsertPreload(this.environment)) 中的主要目的是在预加载功能未启用的情况下,移除对纯 CSS 文件的无效 dynamicImport 导入,以确保生成的包(bundle)中没有无效的导入语句,从而避免运行时错误。
// 在 Vite 中,纯 CSS 文件可能会被单独处理,并从最终的 JavaScript 包中移除。这是因为 CSS 通常会被提取到单独的 CSS 文件中,以便浏览器可以并行加载 CSS 和 JavaScript 文件,从而提高加载性能。
// 当纯 CSS 文件被移除后,任何对这些 CSS 文件的导入语句将变成无效的导入。如果不移除这些无效的导入语句,运行时会出现错误,因为这些 CSS 文件已经不存在于生成的包中。
// 默认情况下,modulePreload 都是开启的。同时,我们的 Demo 中并不涉及 CSS 文件的处理,所以这里的逻辑并不会执行。
if (!getInsertPreload(this.environment)) {
const removedPureCssFiles = removedPureCssFilesCache.get(config)
if (removedPureCssFiles && removedPureCssFiles.size > 0) {
for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'chunk' && chunk.code.includes('import')) {
const code = chunk.code
let imports!: ImportSpecifier[]
try {
imports = parseImports(code)[0].filter((i) => i.d > -1)
} catch (e: any) {
const loc = numberToPos(code, e.idx)
this.error({
name: e.name,
message: e.message,
stack: e.stack,
cause: e.cause,
pos: e.idx,
loc: { ...loc, file: chunk.fileName },
frame: generateCodeFrame(code, loc),
})
}
for (const imp of imports) {
const {
n: name,
s: start,
e: end,
ss: expStart,
se: expEnd,
} = imp
let url = name
if (!url) {
const rawUrl = code.slice(start, end)
if (rawUrl[0] === `"` && rawUrl[rawUrl.length - 1] === `"`)
url = rawUrl.slice(1, -1)
}
if (!url) continue
const normalizedFile = path.posix.join(
path.posix.dirname(chunk.fileName),
url,
)
if (removedPureCssFiles.has(normalizedFile)) {
// remove with Promise.resolve({}) while preserving source map location
chunk.code =
chunk.code.slice(0, expStart) +
`Promise.resolve({${''.padEnd(expEnd - expStart - 19, ' ')}})` +
chunk.code.slice(expEnd)
}
}
}
}
}
return
}
const buildSourcemap = this.environment.config.build.sourcemap
const { modulePreload } = this.environment.config.build
// 遍历 bundle 中的所有 assets
for (const file in bundle) {
const chunk = bundle[file]
// 如果生成的文件类型为 chunk 同时源文件内容中包含 preloadMarker
if (chunk.type === 'chunk' && chunk.code.indexOf(preloadMarker) > -1) {
const code = chunk.code
let imports!: ImportSpecifier[]
try {
// 获取模块中所有的动态 dynamicImport 语句
imports = parseImports(code)[0].filter((i) => i.d > -1)
} catch (e: any) {
const loc = numberToPos(code, e.idx)
this.error({
name: e.name,
message: e.message,
stack: e.stack,
cause: e.cause,
pos: e.idx,
loc: { ...loc, file: chunk.fileName },
frame: generateCodeFrame(code, loc),
})
}
const s = new MagicString(code)
const rewroteMarkerStartPos = new Set() // position of the leading double quote
const fileDeps: FileDep[] = []
const addFileDep = (
url: string,
runtime: boolean = false,
): number => {
const index = fileDeps.findIndex((dep) => dep.url === url)
if (index === -1) {
return fileDeps.push({ url, runtime }) - 1
} else {
return index
}
}
if (imports.length) {
// 遍历当前模块中所有的 dynamicImport 语句
for (let index = 0; index < imports.length; index++) {
const {
n: name,
s: start,
e: end,
ss: expStart,
se: expEnd,
} = imports[index]
// check the chunk being imported
let url = name
if (!url) {
const rawUrl = code.slice(start, end)
if (rawUrl[0] === `"` && rawUrl[rawUrl.length - 1] === `"`)
url = rawUrl.slice(1, -1)
}
const deps = new Set<string>()
let hasRemovedPureCssChunk = false
let normalizedFile: string | undefined = undefined
if (url) {
// 获取当前动态导入 dynamicImport 的模块路径(相较于应用根目录而言)
normalizedFile = path.posix.join(
path.posix.dirname(chunk.fileName),
url,
)
const ownerFilename = chunk.fileName
// literal import - trace direct imports and add to deps
const analyzed: Set<string> = new Set<string>()
const addDeps = (filename: string) => {
if (filename === ownerFilename) return
if (analyzed.has(filename)) return
analyzed.add(filename)
const chunk = bundle[filename]
if (chunk) {
// 将依赖添加到 deps 中
deps.add(chunk.fileName)
// 递归当前依赖 chunk 的所有 import 静态依赖
if (chunk.type === 'chunk') {
// 对于所有 chunk.imports 进行递归 addDeps 加入到 deps 中
chunk.imports.forEach(addDeps)
// 遍历当前代码块导入的 CSS 文件
// 确保当前代码块导入的 CSS 在其依赖项之后加载。
// 这样可以防止当前代码块的样式被意外覆盖。
chunk.viteMetadata!.importedCss.forEach((file) => {
deps.add(file)
})
}
} else {
// 如果当前依赖的 chunk 并没有被生成,检查当前 chunk 是否为纯 CSS 文件的 dynamicImport
const removedPureCssFiles =
removedPureCssFilesCache.get(config)!
const chunk = removedPureCssFiles.get(filename)
// 如果是的话,则会将 css 文件加入到依赖中
// 同时更新 dynamicImport 的 css 为 promise.resolve({}) 防止找不到 css 文件导致的运行时错误
if (chunk) {
if (chunk.viteMetadata!.importedCss.size) {
chunk.viteMetadata!.importedCss.forEach((file) => {
deps.add(file)
})
hasRemovedPureCssChunk = true
}
s.update(expStart, expEnd, 'Promise.resolve({})')
}
}
}
// 将当前 dynamicImport 的模块路径添加到 deps 中
// 比如 import('./Contact.tsx') 会将 [root]/assets/Contact.tsx 添加到 deps 中
addDeps(normalizedFile)
}
// 寻找当前 dynamicImport 语句中的 preloadMarker 的位置
let markerStartPos = indexOfMatchInSlice(
code,
preloadMarkerRE,
end,
)
// 边界 case 处理,我们可以忽略这个判断。找不到的清咖滚具体参考相关 issue #3051
if (markerStartPos === -1 && imports.length === 1) {
markerStartPos = indexOfMatchInSlice(code, preloadMarkerRE)
}
// 如果找到了 preloadMarker
// 判断 vite 构建时是否开启了 modulePreload
// 如果开启则将当前 dynamicImport 的所有依赖项添加到 deps 中
// 否则仅会添加对应 css 文件
if (markerStartPos > 0) {
// the dep list includes the main chunk, so only need to reload when there are actual other deps.
let depsArray =
deps.size > 1 ||
// main chunk is removed
(hasRemovedPureCssChunk && deps.size > 0)
? modulePreload === false
?
// 在 Vite 中,CSS 依赖项的处理机制与模块预加载(module preloads)的机制是相同的。
// 所以,及时没有开启 dynamicImport 的 modulePreload 优化,仍然需要通过 vite_preload 处理 dynamicImport 的 CSS 依赖项。
[...deps].filter((d) => d.endsWith('.css'))
: [...deps]
: []
// 具体可以参考 https://vite.dev/config/build-options.html#build-modulepreload
// resolveDependencies 是一个函数,用于确定给定模块的依赖关系。在 Vite 的构建过程中,Vite 会调用这个函数来获取每个模块的依赖项,并生成相应的预加载指令。
// 在 vite 构建过程中我们可以通过 resolveDependencies 函数来自定义修改模块的依赖关系从而响应 preload 的声明
// 我们这里并没有开启,所以为 undefined
const resolveDependencies = modulePreload
? modulePreload.resolveDependencies
: undefined
if (resolveDependencies && normalizedFile) {
// We can't let the user remove css deps as these aren't really preloads, they are just using
// the same mechanism as module preloads for this chunk
const cssDeps: string[] = []
const otherDeps: string[] = []
for (const dep of depsArray) {
;(dep.endsWith('.css') ? cssDeps : otherDeps).push(dep)
}
depsArray = [
...resolveDependencies(normalizedFile, otherDeps, {
hostId: file,
hostType: 'js',
}),
...cssDeps,
]
}
let renderedDeps: number[]
// renderBuiltUrl 可以参考 Vite 文档说明
// 这里我们也没有开启 renderBuiltUrl 选项
// 简单来说 renderBuiltUrl 用于在构建过程中自定义处理资源 URL 的生成
if (renderBuiltUrl) {
renderedDeps = depsArray.map((dep) => {
const replacement = toOutputFilePathInJS(
this.environment,
dep,
'asset',
chunk.fileName,
'js',
toRelativePath,
)
if (typeof replacement === 'string') {
return addFileDep(replacement)
}
return addFileDep(replacement.runtime, true)
})
} else {
// 最终,我们的 Demo 中对于 depsArray 会走到这个的逻辑处理
// 首先会根据 isRelativeBase 判断构建时的 basename 是否为相对路径
// 如果为相对路径,调用 toRelativePath 将每个依赖想相较于 basename 的地址进行转换之后调用 addFileDep
// 否则,直接将依赖地址调用 addFileDep
renderedDeps = depsArray.map((d) =>
// Don't include the assets dir if the default asset file names
// are used, the path will be reconstructed by the import preload helper
isRelativeBase
? addFileDep(toRelativePath(d, file))
: addFileDep(d),
)
}
// 最终这里会将当前 import 语句中的 __VITE_PRELOAD__ 替换为 __vite__mapDeps([${renderedDeps.join(',')}])
// renderedDeps 则为当前 dynamicImport 模块所有需要被优化的依赖项的 FileDep 类型对象
s.update(
markerStartPos,
markerStartPos + preloadMarker.length,
renderedDeps.length > 0
? `__vite__mapDeps([${renderedDeps.join(',')}])`
: `[]`,
)
rewroteMarkerStartPos.add(markerStartPos)
}
}
}
// 这里的逻辑主要用于生成 __vite__mapDeps 方法
if (fileDeps.length > 0) {
// 将 fileDeps 对象转化为字符串
const fileDepsCode = `[${fileDeps
.map((fileDep) =>
// 检查是否存在 runtime
// 关于 runtime 的逻辑,可以参考 vite 文档 https://vite.dev/config/build-options.html#build-modulepreload
// Demo 中并没有定义任何 runtime 逻辑,所以这里的 runtime 为 false
// 如果存在,则直接使用 fileDep.url 的字符串
// 否则使用 fileDep.url 的 JSON 字符串
fileDep.runtime ? fileDep.url : JSON.stringify(fileDep.url),
)
.join(',')}]`
const mapDepsCode = `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${fileDepsCode})))=>i.map(i=>d[i]);\n`
// 将生成的 __vite__mapDeps 声明插入到生成的文件顶部
if (code.startsWith('#!')) {
s.prependLeft(code.indexOf('\n') + 1, mapDepsCode)
} else {
s.prepend(mapDepsCode)
}
}
// 看上去像是为了确保所有的预加载标记都被正确移除。
// 不过上述的 case 理论上来说已经处理了所有的 dynamicImport ,这里具体为什么在检查一遍,我也不是很清楚
// But it's not important! 😊 这并不妨碍我们理解 preload 优化的原理,我们可以将它标记为兜底的异常边界处理
let markerStartPos = indexOfMatchInSlice(code, preloadMarkerRE)
while (markerStartPos >= 0) {
if (!rewroteMarkerStartPos.has(markerStartPos)) {
s.update(
markerStartPos,
markerStartPos + preloadMarker.length,
'void 0',
)
}
markerStartPos = indexOfMatchInSlice(
code,
preloadMarkerRE,
markerStartPos + preloadMarker.length,
)
}
// 修改最终生成的文件内容
if (s.hasChanged()) {
chunk.code = s.toString()
if (buildSourcemap && chunk.map) {
const nextMap = s.generateMap({
source: chunk.fileName,
hires: 'boundary',
})
const map = combineSourcemaps(chunk.fileName, [
nextMap as RawSourceMap,
chunk.map as RawSourceMap,
]) as SourceMap
map.toUrl = () => genSourceMapUrl(map)
chunk.map = map
if (buildSourcemap === 'inline') {
chunk.code = chunk.code.replace(
convertSourceMap.mapFileCommentRegex,
'',
)
chunk.code += `\n//# sourceMappingURL=${genSourceMapUrl(map)}`
} else if (buildSourcemap) {
const mapAsset = bundle[chunk.fileName + '.map']
if (mapAsset && mapAsset.type === 'asset') {
mapAsset.source = map.toString()
}
}
}
}
}
}
},
__vitePreload(
async () => {
const { Contact } = await import('./Contact.tsx')
return { Contact }
},
__VITE_PRELOAD__,
''
)
__vitePreload(
async () => {
const { Contact } = await import('./Contact.tsx')
return { Contact }
},
__vite__mapDeps([${renderedDeps.join(',')}],
''
)
const __vite__mapDeps = (i, m = __vite__mapDeps, d = m.f || (m.f = ${fileDepsCode})) =>
i.map((i) => d[i])
const __vite__mapDeps = (
i,
m = __vite__mapDeps,
d = m.f ||
(m.f = [
'assets/Contact-BGa5hZNp.js',
'assets/Phone-CqabSd3V.js',
'assets/Name-Blg-G5Um.js',
]),
) => i.map((i) => d[i])
const Contact = React.lazy(() =>
__vitePreload(
() => import('./Contact-BGa5hZNp.js'),
__vite__mapDeps([0, 1, 2]),
),
)
【推荐阅读】
“携程技术”公众号
分享,交流,成长