成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

攜程商旅在 Remix 模塊預加載中的探索與優化實踐

開發 前端
本文總結了攜程商旅大前端團隊在將框架從 Remix 1.0 升級至 Remix 2.0 過程中遇到的問題和解決方案,特別是針對 Vite 在動態模塊加載優化中引發的資源加載問題。文章詳細探討了 Vite 優化 DynamicImport 的機制,并介紹了團隊為解決動態引入導致 404 問題所做的定制化處理。

一、引言

去年,商旅大前端團隊成功嘗試將部分框架從 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 的標簽:

圖片


簡單來說,這便是 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 加載順序可以用下面的草圖來表達:

圖片


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(自身)PhoneName 三個 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 執行順序不了解的同學,可以參考下面這張圖。

圖片


此時 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 的改造。

責任編輯:張燕妮 來源: 攜程技術
相關推薦

2024-12-18 10:03:30

2023-12-29 09:42:28

攜程開發

2023-08-18 10:49:14

開發攜程

2023-06-06 11:49:24

2022-06-17 10:44:49

實體鏈接系統旅游AI知識圖譜攜程

2024-03-22 15:09:32

2022-03-30 18:39:51

TiDBHTAPCDP

2024-04-18 09:41:53

2022-07-15 09:20:17

性能優化方案

2022-07-08 09:38:27

攜程酒店Flutter技術跨平臺整合

2023-07-07 12:26:39

攜程開發

2017-02-23 21:17:00

致遠

2022-04-28 09:36:47

Redis內存結構內存管理

2024-07-05 15:05:00

2023-06-06 16:01:00

Web優化

2023-11-06 09:56:10

研究代碼

2023-11-13 11:27:58

攜程可視化

2023-07-07 14:18:57

攜程實踐

2020-12-04 14:32:33

AndroidJetpackKotlin

2024-11-05 09:56:30

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品美女一区二区三区 | 美日韩一区二区 | 欧美日韩国产一区二区三区 | 久久久久久久久久久福利观看 | 国产精品一级在线观看 | 日韩免费在线观看视频 | 欧美精品日韩精品国产精品 | 国产精品成av人在线视午夜片 | www.久久| 国产成人精品一区二区在线 | 日韩高清国产一区在线 | 精品电影 | 国产精品国产三级国产aⅴ无密码 | 国产精品视频久久 | 国产成人免费视频网站视频社区 | 在线日韩不卡 | 久久av综合 | 天堂色区| 国产综合精品一区二区三区 | 欧美精品乱码久久久久久按摩 | 69堂永久69tangcom| 在线观看国产视频 | 亚洲午夜精品一区二区三区 | 天天射影院| 精品少妇一区二区三区日产乱码 | 极品粉嫩国产48尤物在线播放 | 久久精品视频网站 | 国产黄a一级 | 国产亚洲一区二区三区 | 亚洲高清在线观看 | av影音| 91精品久久久久久久久久入口 | 亚洲精品免费在线 | 亚洲精品日韩综合观看成人91 | 亚洲a一区 | 日韩一区二区三区视频 | 成人看片在线观看 | 亚洲成人免费在线 | 成人av免费在线观看 | 欧美在线a | 欧美极品在线观看 |