干货 | 携程商旅在 Remix 模块预加载中的探索与优化实践

御网杯2025线下赛 reverse wp(0解题赛后复现) 本文分析了去年七月份的re题目,主要思路是通过逆向工程和密码学知识解密隐藏的flag。文章首先观察了主页面逻辑,发现flag校验并不在输入框提交路径上,而是隐藏在/__check路由中。接着分析了关键文件Check-BG23wlCi.js,发现其中包含AES解密函数deAes,密文是一个固定的Base64字符串。通过还原核心逻辑,发现解密密钥来自URL的query.method参数。由于常见HTTP方法被禁用,最终选择OPTIONS方法作为密钥成功解密出flag。 阅读详情

作者简介

19组清风,携程资深前端开发工程师,负责商旅前端公共基础平台建设,关注NodeJs、研究效能领域。

团队热招岗位:高级/资深前端开发工程师

本文总结了携程商旅大前端团队在将框架从 Remix 1.0 升级至 Remix 2.0 过程中遇到的问题和解决方案,特别是针对 Vite 在动态模块加载优化中引发的资源加载问题。文章详细探讨了 Vite 优化 DynamicImport 的机制,并介绍了团队为解决动态引入导致 404 问题所做的定制化处理。

  • 一、引言

  • 二、模块懒加载

  • 三、Vite 中如何处理懒加载模块

  • 3.1 表象

  • 3.2 机制

  • 3.3 原理

  • 四、商旅对于 DynamicImport 的内部改造

  • 五、结尾

一、引言

去年,商旅大前端团队成功尝试将部分框架从 Next.js 迁移至 Remix,并显著提升了用户体验。由于 Remix 2.0 版本在构建工具和新功能方面进行了大量升级,我们最近决定将 Remix 1.0 版本同步升级至 Remix 2.0。

目前,商旅内部所有 Remix 项目在浏览器中均已使用 ESModule 进行资源加载。

在 Remix 1.0 版本中,我们通过在服务端渲染生成静态资源模板时,为所有静态资源动态添加 CDN 前缀来处理资源加载。简单来说,原始的 HTML 模板如下:

<script type="module">
  import init from 'assets/contact-GID3121.js';
  init();
  // ...
</script>

在每次生成模板时,我们会动态地为所有生成的 <script> 标签注入一个变量:

<script type="module">
  import init from 'https://aw-s.tripcdn.com/assets/contact-GID3121.js';
  init();
  // ...
</script>

在 Remix 1.0 下,这种工作机制完全满足我们的需求,并且运行良好。然而,在商旅从 Remix 1.0 升级到 2.0 后,我们发现某些 CSS 资源以及 modulePreload 的 JavaScript 资源仍然会出现 404 响应。

经过排查,我们发现这些 404 响应的静态资源实际上是由于在 1.0 中动态注入的 Host 变量未能生效。实际上,这是由于 Remix 升级过程中,Vite 对懒加载模块(DynamicImport)进行了优化,以提升页面性能。然而,这些优化手段在我们的应用中使用动态加载的静态资源时引发了新的问题。

这篇文章总结了我们在 Vite Preload 改造过程中的经验和心得。接下来,我们将从表象、实现和源码三个层面详细探讨 Vite 如何优化 DynamicImport,并进一步介绍携程商旅在 Remix 升级过程中对 Vite DynamicImport 所进行的定制化处理。

二、模块懒加载

懒加载(Lazy Load)是前端开发中的一种优化技术,旨在提高页面加载性能和用户体验。

懒加载的核心思想是在用户需要时才加载某些资源,而不是在页面初始加载时就加载所有资源。

除了常见的图像懒加载、路由懒加载外还有一种模块懒加载。

广义上路由懒加载可以看作是模块懒加载的子集。

所谓的模块懒加载表示页面中某些模块通过动态导入(dynamic import),在需要时才加载某些 JavaScript 模块。

目前绝大多数前端构建工具中会将通过动态导入的模块进行 split chunk(代码拆分),只有在需要时才加载这些模块的 JavaScript、Css 等静态资源内容。

我们以 React 来看一个简单的例子:

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;

在这个示例中:

1)Travelers 组件是立即加载并显示的。

2)Contact 组件使用 React.lazy 以及 DynamicImport 进行懒加载,只有在用户点击“添加联系人”按钮后才会加载并显示。

3)Suspense 组件用于在懒加载的组件尚未加载完成时显示一个回退内容(例如“加载中...”)。

这样,当用户点击“添加联系人”按钮时,Contact 组件才会被动态加载并显示在页面上。

所以上边的 Contact 联系人组件就可以认为是被当前页面懒加载。

三、Vite 中如何处理懒加载模块

3.1 表象

首先,我们先来通过 npm create vite@latest react -- --template react 创建一个基于 Vite 的 React 项目。

无论是 React、Vue 还是源生 JavaScript ,LazyLoad 并不局限于任何框架。这里为了方便演示我就使用 React 来举例。

想跳过简单 Demo 编写环节的小伙伴可以直接在这里 Clone Demo 仓库。

首先我们通过 vite 命令行初始化一个代码仓库,之后我们对新建的代码稍做修改:

// 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;

上边的 Demo 中,我们在 App.tsx 中编写了一个简单的页面。

页面中使用 dynamicImport 引入了三个模块,分别为:

  • Contact 联系人模块

  • Phone 手机模块

  • Name 姓名模块

对于 App.tsx 中动态引入的 Phone 和 Name 模块,我们仅仅是利用动态引入实现在构建时的代码拆分。所以这里在 App.tsx 中完全可以忽略这两个模块。

简单来说 vite 中对于使用 dynamicImport 的模块会在构建时单独拆分成为一个 chunk (通常情况下一个 chunk 就代表构建后的一个单独 javascript 文件)。

重点在于 App.tsx 中动态引入的联系人模块,我们在 App.tsx 中使用 dynamicImport 引入了 Contact 模块。

同时,在 Contact 模块中我们又引入了 Phone、Name 两个模块。

由于在 App.tsx 中我们已经使用 dynamicImport 将 Phone 和 Name 强制拆分为两个独立的 chunk,自然 Contact 在构建时相当于依赖了 Phone 和 Name 这两个模块的独立 chunk。

此时,让我们直接直接运行 npm run build && npm run start 启动应用(只有在生产构建模式下才会开启对于 dynamicImport 的优化)。

打开浏览器后我们会发现,在 head 标签中多出了 3 个 moduleprealod 的标签:

92899e7130fdfc2ade9365f0b681d332.jpeg

简单来说,这便是 vite 对于使用 dynamicImport 异步引入模块的优化方式,默认情况下 Vite 会对于使用 dynamicImport 的模块收集当前模块的依赖进行 modulepreload 进行预加载。

当然,对于 dynamicImport,Vite 内部不仅对 JS 模块进行了依赖模块的 modulePreload 处理,同时也对 dynamicImport 依赖的 CSS 模块进行了处理。

不过,让我们先聚焦于 dynamicImport 的 JavaScript 优化上吧。

3.2 机制

在探讨源码实现之前,我们先从编译后的 JavaScript 代码角度来分析 Vite 对 DynamicImport 模块的优化方式。

首先,我们先查看浏览器 head 标签中的 modulePreload 标签可以发现,声明 modulePreload 的资源分别为 Contact 联系人模块、Phone 手机模块以及 Name 姓名模块。

从表现上来说,简单来说可以用这段话来描述 Vite 内部对于动态模块加载的优化:

项目在构建时,首次访问页面会加载 App.tsx 对应生成的 chunk 代码。App.tsx 对应的页面在渲染时会依赖 dynamicImport 的 Contact 联系人模块。

此时,Vite 内部会对使用 dynamicImport 的 Contact 进行模块分析,发现联系人模块内部又依赖了 Phone 以及 Name 两个 chunk。

简单来讲我们网页的 JS 加载顺序可以用下面的草图来表达:

599e747ff834487f65d139acb0b6a360.jpeg

App.tsx 构建后生成的 Js Assets 会使用 dynamicImport 加载 Contact.tsx 对应的 assets。

而 Contact.tsx 中则依赖了 name-[hash].jsx 和 phone-[hash].js 这两个 assets。

Vite 对于 App.tsx 进行静态扫描时,会发现内部存在使用 dynamicImport 语句。此时会将所有的 dynamicImport 语句进行优化处理,简单来说会将

const Contact = React.lazy(() => import('./components/Contact'))

转化为

const Contact = React.lazy(() =>
    __vitePreload(() => import('./Contact-BGa5hZNp.js'), __vite__mapDeps([0, 1, 2])))
  • __vitePreload 是构建时 Vite 对于使用 dynamicImport 插入的动态加载的优化方法。

  • __vite__mapDeps([0, 1, 2])则是传递给 __vitePreload 的第二个参数,它表示当前动态引入的 dynamicImport 包含的所有依赖 chunk,也就是 Contact(自身)、Phone、Name 三个 chunk。

简单来说 __vitePreload 方法首先会将 __vite__mapDeps 中所有依赖的模块使用 document.head.appendChild 插入所有 modulePreload 标签之后返回真实的 import('./Contact-BGa5hZNp.js')。

最终,Vite 通过该方式就会对于动态模块内部引入的所有依赖模块实现对于动态加载模块的深层 chunk 使用 modulePreload 进行动态加载优化。

3.3 原理

在了解了 Vite 内部对 modulePreload 的基本原理和机制后,接下来我们将深入探讨 Vite 的构建过程,详细分析其动态模块加载优化的实现方式。

Vite 在构建过程中对 dynamicImport 的优化主要体现在 vite:build-import-analysis 插件中。

接下来,我们将通过分析 build-import-analysis 插件的源代码,深入探讨 Vite 是如何实现 modulePreload 优化的。

3.3.1 扫描/替换模块代码 - transform

首先,build-import-analysis 中存在 transform hook。

简单来说,transform 钩子用于在每个模块被加载和解析之后,对模块的代码进行转换。这个钩子允许我们对模块的内容进行修改或替换,比如进行代码转换、编译、优化等操作。

上边我们讲过,vite 在构建时扫描源代码中的所有 dynamicImport 语句同时会将所有 dynamicImport 语句增加 __vitePreload的 polyfill 优化方法。

所谓的 transform Hook 就是扫描每一个模块,对于模块内部的所有 dynamicImport 使用 __vitePreload 进行包裹。

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,
        }
      }
    },

上面的代码展示了 build-import-analysis 插件中 transform 钩子的全部内容,并在关键环节添加了相应的注释说明。简而言之,transform 钩子的作用可以归纳为以下几点:

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,
            ''
          )

其中,__VITE_IS_MODERN__ 和 __VITE_PRELOAD__ 是 Vite 内部的固定字符串占位符,在 transform 钩子中不会处理这两个字符串变量,目前仅用作占位。而 __vitePreload 则是外层包裹的 Polyfill 方法。

3)引入预加载方法:transform 钩子会检查该模块中是否引入了 preloadMethod (__vitePreload),如果未引入,则会在模块顶部添加对 preloadMethod 的引入。例如:

import { ${preloadMethod} } from "${preloadHelperId}"
// ...

经过 vite:build-import-analysis 插件的 transform 钩子处理后,动态导入的优化机制已经初具雏形。

3.3.2 增加 preload 辅助语句 - resolveId/load

接下来,我们将针对 transform 钩子中添加的 import { ${preloadMethod} } from "${preloadHelperId}" 语句进行分析。

当转换后的模块中不存在 preloadMethod 声明时,Vite 会在构建过程中自动插入 preloadMethod 的引入语句。当模块内部引入 preloadHelperId 时,Vite 会在解析该模块(例如 App.tsx)的过程中,通过 moduleParse 钩子逐步分析 App.tsx 中的依赖关系。

由于我们在 App.tsx 顶部插入了 import { ${preloadMethod} } from "${preloadHelperId}" 语句,因此在 App.tsx 的 moduleParse 阶段,Vite 会递归分析 App.tsx 中引入的 preloadHelperId 模块。

关于 Rollup Plugin 执行顺序不了解的同学,可以参考下面这张图。

e9d3b7f842728ad454214c2007b74267.jpeg

此时 vite:build-import-analysis 插件的 resolveId 和 load hook 就会派上用场:

// ...


    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)
  })
}

对于引入 preloadHelperId 的模块,build-import-analysis 会在 resolveId 和 load 阶段识别并添加 preload 方法的静态声明。preload 方法支持三个参数:

1)第一个参数是原始的模块引入语句,例如 import('./Phone')。

2)第二个参数是被 dynamicImport 加载的模块的所有依赖,这些依赖需要被添加为 modulepreload。

3)第三个参数是 import.meta.url(生成的资源的 JavaScript 路径)或空字符串,这取决于 renderBuiltUrl 或 isRelativeBase 的值。在这里,我们并没有传入 renderBuiltUrl 或 isRelativeBase。

也就说,在 vite:build-import-analysis 的 resolveId 以及 load 阶段为会存在 __vite_preload 的模块添加对于 preloadMethod 的声明。

3.3.3 开启预加载优化 - renderChunk

经过了 resolveId、load 以及 transform 阶段的分析,build-import-analysis 插件已经可以为使用了 dynamicImport 的模块中包裹 __vitePreload 的方法调用以及在模块内部引入 __vitePreload 的声明。

renderChunk 是 Rollup(Vite) 插件钩子之一,用于在生成每个代码块(chunk)时进行自定义处理。它的主要功能是在代码块被转换为最终输出格式之前,对其进行进一步的操作或修改。

build-import-analysis 会在渲染每一个 chunk 时,通过 renderChunk hook 来最终确定是否需要开启 modulePrealod 。

// ...


    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
    },

简单来说,在渲染每一个时会判断源代码中是否存在 isModernFlag (code.indexOf(isModernFlag) > -1 ):

  • 如果存在,则会判断生成的 chunk 是否为 esm 格式。如果是的话,则会将 isModernFlag 全部替换为 true,否则会全部替换为 false。

  • 如果不存在则不会进行任何处理。

isModernFlag 这个标记位,在上边的 transform hook 中我们已经生成了:

// transform 后对于 dynamicImport 的处理
__vitePreload(
  async () => {
    const { Contact } = await import('./Contact.tsx')
    return { Contact }
  },
  __VITE_IS_MODERN__ ? __VITE_PRELOAD__ : void 0,
)

此时,经过 renderChunk 的处理会变为:

__vitePreload(
  async () => {
    const { Contact } = await import('./Contact.tsx')
    return { Contact }
  },
  true ? __VITE_PRELOAD__ : void 0,
  ''
)

3.3.4 寻找/加载需要预加载模块 - generateBundle

经过上述各个阶段的处理,vite 内部会将 import ('Contact.tsx') 转化为:

__vitePreload(
  async () => {
    const { Contact } = await import('./Contact.tsx')
    return { Contact }
  },
  __VITE_PRELOAD__,
  ''
)

对于 __vitePreload 方法,唯一尚未解决的变量是 __VITE_PRELOAD__。

如前所述,Vite 内部对动态导入(dynamicImport)的优化会对被动态加载模块的所有依赖进行 modulePreload。在 __vitePreload 方法中,第一个参数是原始被动态加载的 baseModule,第二个参数目前是占位符 __VITE_PRELOAD__,第三个参数是对引入资源路径的额外处理参数,在当前配置下为空字符串。

结合 preload 方法的定义,可以推测接下来的步骤是将 __VITE_PRELOAD__ 转化为每个 dynamicImport 的深层依赖,从而使 preload 方法在加载 baseModule 时能够对所有依赖进行 modulePreload。

generateBundle 是 Rollup(Vite) 插件钩子之一,用于在生成最终输出文件之前对整个构建结果进行处理。

它的主要作用是在所有代码块(chunks)和资产(assets)都生成之后,对这些输出进行进一步的操作或修改。

这里 build-import-analysis 插件中的 generateBundle 钩子正是用于实现对于最终生成的 assets 中的内容进行修改,寻找当前生成的 assets 中所有 dynamicImport 的深层依赖文件从而替换 __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()
                }
              }
            }
          }
        }
      }
    },

上边的代码中,我对于 generateBundle hook 每一行都进行了详细的注释。

在 generateBundle hook 中,简单来说就是遍历每一个生成的 chunk ,通过检查每个 chunk 中的 js assets 中是否包含 preloadMarker 标记来检查生成的资源中是否需要被处理。

如果当前文件存在 preloadMarker 标记的话,此时会解析出生成的 js 文件中所有的 dynamicImport 语句,遍历每一个 dynamicImport 语句。

同时将 dynamicImport 的模块以及依赖的模块全部通过 addDeps 方法加入到 deps 的 Set 中。

也就说,每个 chunk 中的每个 asset 的每一个 dynamicImport 都存在一个名为 deps 的 Set ,它会收集到当前 dynamicImport 模块的所有依赖(从被动态导入的自身模块开始递归寻找)。

比如 import('./Contact.tsx') 模块就会寻找到 Contact、Phone、Name 这三个 chunk 对应的 js asset 文件路径。

之后,会将上述生成的

__vitePreload(
  async () => {
    const { Contact } = await import('./Contact.tsx')
    return { Contact }
  },
  __VITE_PRELOAD__,
  ''
)

中的 __VITE_PRELOAD__ 替换成为

__vitePreload(
  async () => {
    const { Contact } = await import('./Contact.tsx')
    return { Contact }
  },
  __vite__mapDeps([${renderedDeps.join(',')}],
  ''
)

对于我们 Demo 中的 Contact 模块,renderedDeps 则是 Contact、Phone 以及 Name 对应构建后生成的 js 资源路径。

之后,又会在生成的 js 文件中插入这样一段代码:

const __vite__mapDeps = (i, m = __vite__mapDeps, d = m.f || (m.f = ${fileDepsCode})) =>
  i.map((i) => d[i])

在我们的 Demo 中 fileDepsCode 即为 fileDeps 中每一项依赖的静态资源地址(也就是执行 dynamicImport Contact 时需要依赖的 js 模块)转化为 JSON 字符串之后的路径。

Tips: fileDeps 是 asset (资源文件) 纬度的,也就是一个 JS 资源中所有 dynamicImport 的资源都会被加入到 fileDeps 数组中,而 deps 是每个 dynamicImport 语句维护的。最终在调用 preload 时,每个 preload 语句的 deps 是一个索引的数组,我们会通过 deps 中的索引去 fileDeps 中寻找对应下标的资源路径。

最终,代码中的 await import('./Contact.tsx') 经过 vite 的构建后会变为:

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]),
  ),
)

至此,我们已经详细讲解了 Vite 内部 modulePreload 预加载的全部源码实现。

四、商旅对于 DynamicImport 的内部改造

目前,商旅内部对 Remix 2.0 的升级优化工作已接近尾声。相比于 Remix 1.0 的运行方式,2.0 中如果仅在服务端模板生成时为所有 ES 模块动态添加 AresHost,对于某些动态导入(DynamicImport)的模块,构建后代码发布时可能会出现 modulePreload 标签和 CSS 资源加载 404 的问题。这些 404 资源问题正是由于 Vite 中 build-import-analysis 对 DynamicImport 的优化所导致的。

为了解决这一问题,我们不仅对 Remix 进行了改造,还对 Vite 中处理 DynamicImport 的逻辑进行了优化,以支持在 modulePreload 开启时以及 DynamicImport 模块中的静态资源实现 Ares 的运行时 CDN Host 注入。

实际上,Vite 中存在一个实验性属性 experimental.renderBuiltUrl,也支持为静态资源添加动态 Host。然而,renderBuiltUrl 的局限性在于它无法获取服务端的运行变量。由于我们的前端应用在服务端运行时将 AresHost 挂载在每次请求的 request 中,而 renderBuiltUrl 属性无法访问每次请求的 request。

我们期望不仅在客户端运行时,还能在服务端 SSR 应用模板生成时通过 request 获取动态的 Ares 前缀并挂载在静态资源上,显然 renderBuiltUrl 无法满足这一需求。

简单来说,对于修改后的 Remix 框架,我们将所有携程相关的通用框架属性集成到 RemixContext 中,并通过传统 SSR 应用服务端和客户端传递数据的方式(script 脚本)在 window 上挂载 __remixContext.aresHost 属性。

之后,我们在 Vite 内部的 build-import-analysis 插件中的 preload 函数中增加了一段代码,为所有链接添加 window.__remixContext.aresHost 属性,从而确保 dynamicImport 模块中依赖的 CSS 和 modulePreload 脚本能够正确携带当前应用的 AresHost。

五、结尾

商旅大前端团队在携程内部是较早采用 Streaming 和 ESModule 技术的。相比集团的 NFES(携程内部一款基于 React 18 + Next.js 13.1.5 + Webpack 5 的前端框架),Remix 在开发友好度和服务端 Streaming 处理方面具有独特优势。目前,Remix 已在商旅的大流量页面中得到了验证,并取得了良好效果。

本文主要从 preload 细节入手,分享我们在这方面遇到的问题和心得。后续我们将继续分享更多关于 Remix 的技术细节,并为大家介绍更多商旅对 Remix 的改造。

【推荐阅读】

bc6eca493115e988deca64661c246432.jpeg

 “携程技术”公众号

  分享,交流,成长

如何在 React 中实现组件懒加载? 组件懒加载(Lazy Loading)是一种在应用程序中仅在需要时(如用户访问特定路由或触发某个事件时)才加载组件的方法。这种方式可以显著减少初始加载时的 JavaScript 文件大小,从而提高应用的加载速度。组件懒加载是提高 React 应用性能的重要手段,通过React.lazy和Suspense,我们可以轻松地实现懒加载。 阅读详情

相关推荐

【电力负荷预测】项目介绍 MATLAB实现基于BO-Transformer-LSTM贝叶斯优化算法(BO)优化Transformer-LSTM组合模型进行居民用户多步电力负荷预测(含模型描述及部分示例

内容概要:本文介绍了一个基于BO-Transformer-LSTM的组合模型,用于实现居民用户多步电力负荷预测的完整项目实例。该模型融合Transformer的全局注意力机制与LSTM的局部时序记忆能力,构建能够捕捉长时依赖与周期性特征的深度学习架构,并引入贝叶斯优化(BO)自动搜索最优超参数组合,提升预测精度与训练效率。项目涵盖从原始数据清洗、特征工程、滑动窗口样本构建、模型设计到超参数优化、反归一化及多步误差评估的全流程,强调时间顺序划分以防止信息泄露,并通过周期编码、外生变量融合等方式增强数据表达能力。文中提供了详细的MATLAB代码示例,包括数据预处理、网络搭建、贝叶斯优化配置和预测结果评估等关键步骤。 适合人群:具备一定机器学习和时间序列分析基础,熟悉MATLAB编程环境,从事电力系统预测、智能电网、负荷管理等相关领域的研究人员、工程师及研究生。 使用场景及目标:①应用于居民级或多用户聚合负荷的短期至中期多步预测(如未来24小时);②服务于配电系统运行调度、需求响应基线设定、家庭能源管理系统优化、分布式能源协调控制等实际工程场景;③为科研提供可复现的深度学习+贝叶斯优化的技术路线参考。 阅读建议:此资源以实际项目为导向,建议读者结合提供的代码逐步实现并调试模型,重点关注数据质量控制、时间特征编码、组合网络结构设计与贝叶斯优化稳定性之间的协同关系,在实践中深入理解多步预测中的误差累积机制与超参数敏感性问题。

为什么写了懒加载,构建产物里仍有“静态 import”?

明明写了“动态导入”,但构建里仍看到“静态 import”,是因为“被动态导入的那个模块内部存在顶层静态依赖”,构建器抽出了共享块并在别的 chunk 顶部“静态引用”它。这不等于首屏同步加载。把这些顶层静态依赖改为“函数内动态 import”,即可从依赖图层面断开那条“静态边”,让 rongcloud 真正只在需要时加载。模板里通过结构性v-if隔离 computed 访问,避免无关场景触发懒加载。

2501_92822955的博客 808

国央企创新负责人如何通过平台实现科研成果的快速识别与转化?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

vue3 vite 环境配置 (开发环境 线上环境)

变量名开头必须是VITE。变量名开头必须是VITE。变量名开头必须是VITE。变量名开头必须是VITE。变量名开头必须是VITE。变量名开头必须是VITE。env文件中配置的变量为全局变量。# 开发环境配置。

weixin_60592115的博客 845

PotPlayer自用皮肤

自用 PotPlayer 皮肤,仅微调了字体大小与颜色,是按个人习惯调整,未做适配性调整。

2-shell及常用命令-续.pdf

2-shell及常用命令-续.pdf

对vite的简单了解

vite 是一种新型的前端构建工具,能够显著的提升前端开发者的体验。它主要有俩部分组成: 一个开发服务器:它基于原生的es模块,提供了丰富的内建功能,如速度快到惊人的模块热更新HMR. 一套构建指令:使用Rollup打包代码,并且它是预构建的,可输出用于生产环境的高度优化过的静态资源。 vite旨在提供开箱即用的配置,但同时它也提供插件API和JavaSccript API 带来高度的可扩展性并且有完整的类型支持。

汉堡请不要欺负面条 8400

BitLocker磁盘锁定解决方法

摘要:本文介绍了BitLocker加密磁盘被锁的两种情况及解决方法。对于系统盘锁定,建议在微软账户中查找48位恢复密钥;数据盘锁定可尝试密码或恢复密钥解锁。若密钥丢失,可通过IT支持或数据恢复服务尝试解决。预防措施包括妥善备份密钥到微软账户和离线位置,避免随意更改系统硬件配置。BitLocker安全性高,恢复密钥是解锁关键,提醒用户务必做好备份以防数据丢失。(148字)

yueke06442300的专栏 2674

vite 依赖预构建-依赖扫描

vite 依赖预构建-依赖扫描

weixin_43294560的博客 2150

部署资源管理系统chemx

资源管理系统的部署教程

weixin_40784215的博客 822

从FPGA/ASIC工程师视角看DDR:手把手拆解Memory Controller、PHY与DRAM芯片的协作流程

本文从FPGA/ASIC工程师视角深入解析DDR内存子系统,详细拆解Memory Controller、PHY与DRAM芯片的协作流程。通过系统架构分解、接口标准解析和实战调试技巧,帮助工程师优化DDR性能并解决常见问题,涵盖地址映射、命令调度、时序校准等关键技术要点。

weixin_42548888的博客 457

资源预加载可能会拖慢网站速度

因此,我们搭建了真实用户监测(Real User Monitoring,RUM),跟踪主要图片的加载时间和第一个预加载的 JavaScript 文件的完成时间。在 Firefox 的测试中,我们发现它的预加载机制更为合理 —— 预加载请求会被延迟更长时间,从而确保主要页面内容优先渲染。然而,在很多情况下,页面性能并不会受到带宽的限制,因此预加载对页面速度的影响可能并不明显。然而,在分析网站资源加载的请求瀑布图时,我们发现预加载请求往往发生得太早,导致它们与主要内容的图片争夺带宽。

程序员成长指北 133

OrangePi Kunpeng Pro 安装 ROS2 + Gazebo

OrangePi Kunpeng Pro 的优点很明显,内置openEuler,配备PD电源,做到开箱即用;板子设计也不错,风扇噪音不大,散热良好,昇腾芯片又确保小板子的性能,接口丰富是这类板子共同的优点,也很好的继承了下来,甚至还可以外接固态,可靠性大大提升。使用起来,哪怕不用openEuler的话,也可以很方便的刷Ubuntu来实现一些需要依赖的开发工作,这次没有跑AI模型,所以对算力的优势还没有很明显的感受,下次可以试一下yolo跑跑看。

Arcan 1824

Vite构建中哈希值连锁更新问题分析与解决方案

在使用Vite构建项目时,开发者发现一个值得关注的现象:当修改项目中某个文件后,不仅该文件对应的输出文件哈希值会变化,其他多个未修改文件的哈希值也会随之改变。这种连锁反应导致浏览器缓存机制失效,用户需要重新下载大量实际上并未变更的资源文件。 ## 技术背景 现代前端构建工具通常使用内容哈希作为静态资源文件名的一部分,这种技术被称为"文件指纹"。理想情况下,只有当文件内容真正发生变化时,其哈希值...

gitblog_00776的博客 429

Dataset - DeepFashion 服装数据集

Dataset - DeepFashion 服装数据集 [Dataset - DeepFashion] [Project - DeepFashion] 1. 服装类别和属性预测集 [Category - Attribute 下载] [百度网盘] 289,222 张服装图片 clothes images; 50 个服装类别 clothing categories 1,000 ...

长风破浪会有时,直挂云帆济沧海 2万+

大数据知识面试题-通用(2026版)

序列号 内容 链接 1 大数据知识面试题-通用(2022版) 待续… 2 大数据知识面试题-Hadoop(2022版) 待续… 3 大数据知识面试题-MapReduce和yarn(2022版) 待续… 4 大数据知识面试题-Zookeepr (2022版) 待续… 5 大数据知识面试题-Hive (2022版) 待续… 6 大数据知识面试题-Flume(2022版) 待续… 7 大数据知识面试题-Hbase(2022版) 待续… 8 大数据知识面试题-sqoop(....

拉格朗日的学习笔记 2万+

Vite原理学习之按需编译

前言 当Vite启动开发服务器之前会完成依赖预构建工作,这个工作整个流程简单来说是通过入口文件扫描所有源码分析相关import语句得到使用的第三方依赖包名,之后使用esbuild对依赖进行编译,至此完成整个预编译过程。之后会启动开发服务器并在相关端口进行监听,当启动开发服务器后,Vite会如何处理源码呢?整个过程的执行逻辑具体是什么样的?这篇文章就是来学习Vite开发服务器启动后整个的处理过程。

玉案轩窗的博客 5394

ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory解决方案

本文主要介绍了ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory解决方案,希望能对使用Python的同学们有所帮助。 文章目录 1. 问题描述 2. 解决方案

weixin_43178406的博客 11万+

vite编程-依赖预构建

1. optimizeDeps是针对本地研发环境的依赖进行优化2. optimizeDeps.include能够解决的问题:a. vite的依赖启发式算法无法发现的依赖(例如,源码中没有import,但是babel转换成esModule后又有了import),可以添加到include中b. 如果依赖是commonJS,且非npm安装的,可以通过include进行转码。

Mercuryyjs的博客 2962

26、机器学习模型的成员推断攻击与隐私保护

本文探讨了机器学习模型面临的成员推断攻击及其对隐私的潜在威胁。通过使用CIFAR-10数据集,详细介绍了如何利用影子模型和攻击模型推断特定数据点是否属于模型的训练集。实验表明,即使无法直接访问模型参数和原始数据,攻击者仍能以高于随机猜测的概率进行推断,从而构成隐私风险。此外,文章还分析了实验的关键技术点,并提出了多种隐私保护建议,包括数据脱敏、差分隐私、模型正则化等方法。

coffee的博客 123

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

解决Vite项目入口分块源映射难题:从原理到实战修复

你是否在Vite项目中遇到过生产环境调试困境?当用户报告错误时,浏览器控制台只显示压缩后的代码位置,无法定位原始源码?本文将深入解析Vite中入口分块(Entry Chunk)源映射问题的技术根源,并提供一套完整的解决方案,帮助开发者在保持构建性能的同时,获得精确的错误定位能力。 读完本文你将掌握: - 入口分块与源映射的工作原理 - 生产环境调试的最佳配置方案 - 分块策略与源映射质量的平衡技...

gitblog_01055的博客 534

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

在Remix中运行合约JS脚本

Remix 接受 async/await 脚本来运行 web3.js 或 ethers.js 命令。脚本需要包装在一个自执行函数中。如下: // Right click on the script name and hit "Run" to execute (async () => { try { console.log('Running deployWithWeb3 script...') const contractName = 'St

CamphorBlossom的博客 5361

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

React 并发 API 实战,这几个例子看懂你就明白了

点击上方前端Q,关注公众号 回复加群,加入前端Q技术交流群本文适合对React 并发 API感兴趣的小伙伴阅读。目录什么是并发它和 React 有什么关系中断和切换是如何工作的那 Suspense 呢?如何启动 transition结束语什么是并发并发是一种执行模型,它允许程序的不同部分可以不按顺序执行,而不影响最终结果。你可能听说过多线程或多进程。由于浏览器中的 JavaScript 只能访...

LuckyWinty的博客 129

【路径规划】蚁群算法的优化计算——旅行商问题(TSP)优化

旅行商问题(TSP)是一种经典的组合优化问题,其目标是找到一条遍历所有城市且总路程最短的环路。由于其计算复杂度高,求解大规模TSP问题往往依赖于启发式算法。本文研究了基于蚁群算法(Ant Colony Optimization, ACO)的TSP优化问题,通过实验验证了蚁群算法在求解TSP问题中的有效性。结果表明,经过若干次迭代后,蚁群算法能够找到一个较优的解决方案。

2401_84610415的博客 1378
上一篇: 干货 | RN框架在携程旅行鸿蒙应用的全业务适配实践
下一篇: 干货 | 携程从零构建多端一致的设计研发体系实践
携程技术
博客等级 码龄10年 3716粉丝 324原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值