自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

攜程商旅在 Remix 模塊預(yù)加載中的探索與優(yōu)化實踐

開發(fā) 前端
本文總結(jié)了攜程商旅大前端團(tuán)隊在將框架從 Remix 1.0 升級至 Remix 2.0 過程中遇到的問題和解決方案,特別是針對 Vite 在動態(tài)模塊加載優(yōu)化中引發(fā)的資源加載問題。文章詳細(xì)探討了 Vite 優(yōu)化 DynamicImport 的機制,并介紹了團(tuán)隊為解決動態(tài)引入導(dǎo)致 404 問題所做的定制化處理。

一、引言

去年,商旅大前端團(tuán)隊成功嘗試將部分框架從 Next.js 遷移至 Remix,并顯著提升了用戶體驗。由于 Remix 2.0 版本在構(gòu)建工具和新功能方面進(jìn)行了大量升級,我們最近決定將 Remix 1.0 版本同步升級至 Remix 2.0。

目前,商旅內(nèi)部所有 Remix 項目在瀏覽器中均已使用 ESModule 進(jìn)行資源加載。

在 Remix 1.0 版本中,我們通過在服務(wù)端渲染生成靜態(tài)資源模板時,為所有靜態(tài)資源動態(tài)添加 CDN 前綴來處理資源加載。簡單來說,原始的 HTML 模板如下:

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

在每次生成模板時,我們會動態(tài)地為所有生成的 <script> 標(biāo)簽注入一個變量:

<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 后,我們發(fā)現(xiàn)某些 CSS 資源以及 modulePreload 的 JavaScript 資源仍然會出現(xiàn) 404 響應(yīng)。

經(jīng)過排查,我們發(fā)現(xiàn)這些 404 響應(yīng)的靜態(tài)資源實際上是由于在 1.0 中動態(tài)注入的 Host 變量未能生效。實際上,這是由于 Remix 升級過程中,Vite 對懶加載模塊(DynamicImport)進(jìn)行了優(yōu)化,以提升頁面性能。然而,這些優(yōu)化手段在我們的應(yīng)用中使用動態(tài)加載的靜態(tài)資源時引發(fā)了新的問題。

這篇文章總結(jié)了我們在 Vite Preload 改造過程中的經(jīng)驗和心得。接下來,我們將從表象、實現(xiàn)和源碼三個層面詳細(xì)探討 Vite 如何優(yōu)化 DynamicImport,并進(jìn)一步介紹攜程商旅在 Remix 升級過程中對 Vite DynamicImport 所進(jìn)行的定制化處理。

二、模塊懶加載

懶加載(Lazy Load)是前端開發(fā)中的一種優(yōu)化技術(shù),旨在提高頁面加載性能和用戶體驗。

懶加載的核心思想是在用戶需要時才加載某些資源,而不是在頁面初始加載時就加載所有資源。

除了常見的圖像懶加載、路由懶加載外還有一種模塊懶加載。

廣義上路由懶加載可以看作是模塊懶加載的子集。

所謂的模塊懶加載表示頁面中某些模塊通過動態(tài)導(dǎo)入(dynamic import),在需要時才加載某些 JavaScript 模塊。

目前絕大多數(shù)前端構(gòu)建工具中會將通過動態(tài)導(dǎo)入的模塊進(jìn)行 split chunk(代碼拆分),只有在需要時才加載這些模塊的 JavaScript、Css 等靜態(tài)資源內(nèi)容。

我們以 React 來看一個簡單的例子:

import React, { Suspense, useState } from 'react';


// 出行人組件,立即加載
const Travelers = () => {
  return <div>出行人組件內(nèi)容</div>;
};


// 聯(lián)系人組件,使用 React.lazy 進(jìn)行懶加載
const Contact = React.lazy(() => import('./Contact'));


const App = () => {
  const [showContact, setShowContact] = useState(false);


  const handleAddContactClick = () => {
    setShowContact(true);
  };


  return (
    <div>
      <h1>頁面標(biāo)題</h1>


      {/* 出行人組件立即展示 */}
      <Travelers />


      {/* 添加按鈕 */}
      <button onClick={handleAddContactClick}>添加聯(lián)系人</button>


      {/* 懶加載的聯(lián)系人組件 */}
      {showContact && (
        <Suspense fallback={<div>加載中...</div>}>
          <Contact />
        </Suspense>
      )}
    </div>
  );
};


export default App;

在這個示例中:

1)Travelers 組件是立即加載并顯示的。

2)Contact 組件使用 React.lazy 以及 DynamicImport 進(jìn)行懶加載,只有在用戶點擊“添加聯(lián)系人”按鈕后才會加載并顯示。

3)Suspense 組件用于在懶加載的組件尚未加載完成時顯示一個回退內(nèi)容(例如“加載中...”)。

這樣,當(dāng)用戶點擊“添加聯(lián)系人”按鈕時,Contact 組件才會被動態(tài)加載并顯示在頁面上。

所以上邊的 Contact 聯(lián)系人組件就可以認(rèn)為是被當(dāng)前頁面懶加載。

三、Vite 中如何處理懶加載模塊

3.1 表象

首先,我們先來通過 npm create vite@latest react -- --template react 創(chuàng)建一個基于 Vite 的 React 項目。

無論是 React、Vue 還是源生 JavaScript ,LazyLoad 并不局限于任何框架。這里為了方便演示我就使用 React 來舉例。

想跳過簡單 Demo 編寫環(huán)節(jié)的小伙伴可以直接在這里 Clone Demo 倉庫。

首先我們通過 vite 命令行初始化一個代碼倉庫,之后我們對新建的代碼稍做修改:

// app.tsx
import React, { Suspense } from 'react';


// 聯(lián)系人組件,使用 React.lazy 進(jìn)行懶加載
const Contact = React.lazy(() => import('./components/Contact'));


// 這里的手機號組件、姓名組件可以忽略
// 實際上特意這么寫是為了利用 dynamicImport 的 splitChunk 特性
// vite 在構(gòu)建時對于 dynamicImport 的模塊是會進(jìn)行 splitChunk 的
// 自然 Phone、Name 模塊在構(gòu)建時會被拆分為兩個 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>頁面標(biāo)題</h1>
      {/* 懶加載的聯(lián)系人組件 */}
       (
        <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>聯(lián)系人組件</h3>
    {/* 聯(lián)系人組件依賴的手機號以及姓名組件 */}
    <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 聯(lián)系人模塊
  • Phone 手機模塊
  • Name 姓名模塊

對于 App.tsx 中動態(tài)引入的 Phone 和 Name 模塊,我們僅僅是利用動態(tài)引入實現(xiàn)在構(gòu)建時的代碼拆分。所以這里在 App.tsx 中完全可以忽略這兩個模塊。

簡單來說 vite 中對于使用 dynamicImport 的模塊會在構(gòu)建時單獨拆分成為一個 chunk (通常情況下一個 chunk 就代表構(gòu)建后的一個單獨 javascript 文件)。

重點在于 App.tsx 中動態(tài)引入的聯(lián)系人模塊,我們在 App.tsx 中使用 dynamicImport 引入了 Contact 模塊。

同時,在 Contact 模塊中我們又引入了 Phone、Name 兩個模塊。

由于在 App.tsx 中我們已經(jīng)使用 dynamicImport 將 Phone 和 Name 強制拆分為兩個獨立的 chunk,自然 Contact 在構(gòu)建時相當(dāng)于依賴了 Phone 和 Name 這兩個模塊的獨立 chunk。

此時,讓我們直接直接運行 npm run build && npm run start 啟動應(yīng)用(只有在生產(chǎn)構(gòu)建模式下才會開啟對于 dynamicImport 的優(yōu)化)。

打開瀏覽器后我們會發(fā)現(xiàn),在 head 標(biāo)簽中多出了 3 個 moduleprealod 的標(biāo)簽:

圖片


簡單來說,這便是 vite 對于使用 dynamicImport 異步引入模塊的優(yōu)化方式,默認(rèn)情況下 Vite 會對于使用 dynamicImport 的模塊收集當(dāng)前模塊的依賴進(jìn)行 modulepreload 進(jìn)行預(yù)加載。

當(dāng)然,對于 dynamicImport,Vite 內(nèi)部不僅對 JS 模塊進(jìn)行了依賴模塊的 modulePreload 處理,同時也對 dynamicImport 依賴的 CSS 模塊進(jìn)行了處理。

不過,讓我們先聚焦于 dynamicImport 的 JavaScript 優(yōu)化上吧。

3.2 機制

在探討源碼實現(xiàn)之前,我們先從編譯后的 JavaScript 代碼角度來分析 Vite 對 DynamicImport 模塊的優(yōu)化方式。

首先,我們先查看瀏覽器 head 標(biāo)簽中的 modulePreload 標(biāo)簽可以發(fā)現(xiàn),聲明 modulePreload 的資源分別為 Contact 聯(lián)系人模塊、Phone 手機模塊以及 Name 姓名模塊。

從表現(xiàn)上來說,簡單來說可以用這段話來描述 Vite 內(nèi)部對于動態(tài)模塊加載的優(yōu)化:

項目在構(gòu)建時,首次訪問頁面會加載 App.tsx 對應(yīng)生成的 chunk 代碼。App.tsx 對應(yīng)的頁面在渲染時會依賴 dynamicImport 的 Contact 聯(lián)系人模塊。

此時,Vite 內(nèi)部會對使用 dynamicImport 的 Contact 進(jìn)行模塊分析,發(fā)現(xiàn)聯(lián)系人模塊內(nèi)部又依賴了 Phone 以及 Name 兩個 chunk。

簡單來講我們網(wǎng)頁的 JS 加載順序可以用下面的草圖來表達(dá):

圖片


App.tsx 構(gòu)建后生成的 Js Assets 會使用 dynamicImport 加載 Contact.tsx 對應(yīng)的 assets。

而 Contact.tsx 中則依賴了 name-[hash].jsx 和 phone-[hash].js 這兩個 assets。

Vite 對于 App.tsx 進(jìn)行靜態(tài)掃描時,會發(fā)現(xiàn)內(nèi)部存在使用 dynamicImport 語句。此時會將所有的 dynamicImport 語句進(jìn)行優(yōu)化處理,簡單來說會將

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

轉(zhuǎn)化為

const Contact = React.lazy(() =>
    __vitePreload(() => import('./Contact-BGa5hZNp.js'), __vite__mapDeps([0, 1, 2])))
  • __vitePreload 是構(gòu)建時 Vite 對于使用 dynamicImport 插入的動態(tài)加載的優(yōu)化方法。
  • __vite__mapDeps([0, 1, 2])則是傳遞給 __vitePreload 的第二個參數(shù),它表示當(dāng)前動態(tài)引入的 dynamicImport 包含的所有依賴 chunk,也就是 Contact(自身)、Phone、Name 三個 chunk。

簡單來說 __vitePreload 方法首先會將 __vite__mapDeps 中所有依賴的模塊使用 document.head.appendChild 插入所有 modulePreload 標(biāo)簽之后返回真實的 import('./Contact-BGa5hZNp.js')。

最終,Vite 通過該方式就會對于動態(tài)模塊內(nèi)部引入的所有依賴模塊實現(xiàn)對于動態(tài)加載模塊的深層 chunk 使用 modulePreload 進(jìn)行動態(tài)加載優(yōu)化。

3.3 原理

在了解了 Vite 內(nèi)部對 modulePreload 的基本原理和機制后,接下來我們將深入探討 Vite 的構(gòu)建過程,詳細(xì)分析其動態(tài)模塊加載優(yōu)化的實現(xiàn)方式。

Vite 在構(gòu)建過程中對 dynamicImport 的優(yōu)化主要體現(xiàn)在 vite:build-import-analysis 插件中。

接下來,我們將通過分析 build-import-analysis 插件的源代碼,深入探討 Vite 是如何實現(xiàn) modulePreload 優(yōu)化的。

3.3.1 掃描/替換模塊代碼 - transform

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

簡單來說,transform 鉤子用于在每個模塊被加載和解析之后,對模塊的代碼進(jìn)行轉(zhuǎn)換。這個鉤子允許我們對模塊的內(nèi)容進(jìn)行修改或替換,比如進(jìn)行代碼轉(zhuǎn)換、編譯、優(yōu)化等操作。

上邊我們講過,vite 在構(gòu)建時掃描源代碼中的所有 dynamicImport 語句同時會將所有 dynamicImport 語句增加 __vitePreload的 polyfill 優(yōu)化方法。

所謂的 transform Hook 就是掃描每一個模塊,對于模塊內(nèi)部的所有 dynamicImport 使用 __vitePreload 進(jìn)行包裹。

export const isModernFlag = `__VITE_IS_MODERN__`
export const preloadMethod = `__vitePreload`
export const preloadMarker = `__VITE_PRELOAD__`
export const preloadBaseMarker = `__VITE_PRELOAD_BASE__`


//...


  // transform hook 會在每一個 module 上執(zhí)行
    async transform(source, importer) {
    
      // 如果當(dāng)前模塊是在 node_modules 中,且代碼中沒有任何動態(tài)導(dǎo)入語法,則直接返回。不進(jìn)行任何處理
      if (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) {
        return
      }
      
      // 初始化 es-module-lexer
      await init


      let imports: readonly ImportSpecifier[] = []
      try {
        // 調(diào)用 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
      // 客戶端構(gòu)建時(非 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.
      
      // 當(dāng)使用預(yù)加載助手(__vite_preload 方法)包括 dynamicImport 時
      // Rollup 無法分析訪問的變量是否存在 TreeShaking
      // 下面的代碼主要作用為試圖匹配常見的訪問語法,以將其“復(fù)制”到由預(yù)加載幫助程序包裝的動態(tài)導(dǎo)入中
      // 例如:`const {foo} = await import('foo')` 會被轉(zhuǎn)換為 `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


      // 遍歷當(dāng)前模塊中的所有 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 語句的屬性導(dǎo)入
        // import { someFunction } from './module.js' with { type: 'json' };
        // => import { someFunction } from './module.js';
        if (!isDynamicImport && attributeIndex > -1) {
          str().remove(end + 1, expEnd)
        }
        
        // 如果當(dāng)前 import 語句為 dynamicImport 且需要插入預(yù)加載助手
        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) 變量進(jìn)行拼接
          // 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' : ''
            })`,
          )
        }
      }


      // 如果該模塊標(biāo)記餓了 needPreloadHelper 并且當(dāng)前執(zhí)行環(huán)境 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 鉤子的全部內(nèi)容,并在關(guān)鍵環(huán)節(jié)添加了相應(yīng)的注釋說明。簡而言之,transform 鉤子的作用可以歸納為以下幾點:

1)掃描動態(tài)導(dǎo)入語句:在每個模塊中使用 es-module-lexer 掃描所有的 dynamicImport 語句。例如,對于 app.tsx 文件,會掃描到 import ('./Contact.tsx') 這樣的動態(tài)導(dǎo)入語句。

2)注入預(yù)加載 Polyfill:對于所有的動態(tài)導(dǎo)入語句,使用 magic-string 克隆一份源代碼,然后結(jié)合第一步掃描出的 dynamicImport 語句進(jìn)行字符串拼接,注入預(yù)加載 Polyfill。例如,import ('./Contact.tsx') 經(jīng)過 transform 鉤子處理后會被轉(zhuǎn)換為:

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

其中,__VITE_IS_MODERN__ 和 __VITE_PRELOAD__ 是 Vite 內(nèi)部的固定字符串占位符,在 transform 鉤子中不會處理這兩個字符串變量,目前僅用作占位。而 __vitePreload 則是外層包裹的 Polyfill 方法。

3)引入預(yù)加載方法:transform 鉤子會檢查該模塊中是否引入了 preloadMethod (__vitePreload),如果未引入,則會在模塊頂部添加對 preloadMethod 的引入。例如:

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

經(jīng)過 vite:build-import-analysis 插件的 transform 鉤子處理后,動態(tài)導(dǎo)入的優(yōu)化機制已經(jīng)初具雛形。

3.3.2 增加 preload 輔助語句 - resolveId/load

接下來,我們將針對 transform 鉤子中添加的 import { ${preloadMethod} } from "${preloadHelperId}" 語句進(jìn)行分析。

當(dāng)轉(zhuǎn)換后的模塊中不存在 preloadMethod 聲明時,Vite 會在構(gòu)建過程中自動插入 preloadMethod 的引入語句。當(dāng)模塊內(nèi)部引入 preloadHelperId 時,Vite 會在解析該模塊(例如 App.tsx)的過程中,通過 moduleParse 鉤子逐步分析 App.tsx 中的依賴關(guān)系。

由于我們在 App.tsx 頂部插入了 import { ${preloadMethod} } from "${preloadHelperId}" 語句,因此在 App.tsx 的 moduleParse 階段,Vite 會遞歸分析 App.tsx 中引入的 preloadHelperId 模塊。

關(guān)于 Rollup Plugin 執(zhí)行順序不了解的同學(xué),可以參考下面這張圖。

圖片


此時 vite:build-import-analysis 插件的 resolveId 和 load hook 就會派上用場:

// ...


    resolveId(id) {
      if (id === preloadHelperId) {
        return id
      }
    },


    load(id) {
      // 當(dāng)檢測到引入的模塊路徑為 ${preloadHelperId} 時
      if (id === preloadHelperId) {
      
        // 判斷是否開啟了 modulePreload 配置
        const { modulePreload } = this.environment.config.build
        
        // 判斷是否需要 polyfill
        const scriptRel =
          modulePreload && modulePreload.polyfill
            ? `'modulepreload'`
            : `/* @__PURE__ */ (${detectScriptRel.toString()})()`


        // 聲明對于 dynamicImport 模塊深層依賴的路徑處理方式
        // 比如對于使用了 dynamicImport 引入的 Contact 模塊,模塊內(nèi)部又依賴了 Phone 和 Name 模塊 


        // 這里 assetsURL 方法就是在執(zhí)行對于 Phone 和 Name 模塊 preload 時是否需要其他特殊處理


        // 關(guān)于 renderBuiltUrl 可以參考 Vite 文檔說明 https://vite.dev/guide/build.html#advanced-base-options


        // 我們暫時忽略 renderBuiltUrl ,因為我們構(gòu)建時并未傳入該配置
        
        // 自然 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 方法的靜態(tài)聲明。preload 方法支持三個參數(shù):

1)第一個參數(shù)是原始的模塊引入語句,例如 import('./Phone')。

2)第二個參數(shù)是被 dynamicImport 加載的模塊的所有依賴,這些依賴需要被添加為 modulepreload。

3)第三個參數(shù)是 import.meta.url(生成的資源的 JavaScript 路徑)或空字符串,這取決于 renderBuiltUrl 或 isRelativeBase 的值。在這里,我們并沒有傳入 renderBuiltUrl 或 isRelativeBase。

也就說,在 vite:build-import-analysis 的 resolveId 以及 load 階段為會存在 __vite_preload 的模塊添加對于 preloadMethod 的聲明。

3.3.3 開啟預(yù)加載優(yōu)化 - renderChunk

經(jīng)過了 resolveId、load 以及 transform 階段的分析,build-import-analysis 插件已經(jīng)可以為使用了 dynamicImport 的模塊中包裹 __vitePreload 的方法調(diào)用以及在模塊內(nèi)部引入 __vitePreload 的聲明。

renderChunk 是 Rollup(Vite) 插件鉤子之一,用于在生成每個代碼塊(chunk)時進(jìn)行自定義處理。它的主要功能是在代碼塊被轉(zhuǎn)換為最終輸出格式之前,對其進(jìn)行進(jìn)一步的操作或修改。

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。
  • 如果不存在則不會進(jìn)行任何處理。

isModernFlag 這個標(biāo)記位,在上邊的 transform hook 中我們已經(jīng)生成了:

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

此時,經(jīng)過 renderChunk 的處理會變?yōu)椋?/span>

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

3.3.4 尋找/加載需要預(yù)加載模塊 - generateBundle

經(jīng)過上述各個階段的處理,vite 內(nèi)部會將 import ('Contact.tsx') 轉(zhuǎn)化為:

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

對于 __vitePreload 方法,唯一尚未解決的變量是 __VITE_PRELOAD__。

如前所述,Vite 內(nèi)部對動態(tài)導(dǎo)入(dynamicImport)的優(yōu)化會對被動態(tài)加載模塊的所有依賴進(jìn)行 modulePreload。在 __vitePreload 方法中,第一個參數(shù)是原始被動態(tài)加載的 baseModule,第二個參數(shù)目前是占位符 __VITE_PRELOAD__,第三個參數(shù)是對引入資源路徑的額外處理參數(shù),在當(dāng)前配置下為空字符串。

結(jié)合 preload 方法的定義,可以推測接下來的步驟是將 __VITE_PRELOAD__ 轉(zhuǎn)化為每個 dynamicImport 的深層依賴,從而使 preload 方法在加載 baseModule 時能夠?qū)λ幸蕾囘M(jìn)行 modulePreload。

generateBundle 是 Rollup(Vite) 插件鉤子之一,用于在生成最終輸出文件之前對整個構(gòu)建結(jié)果進(jìn)行處理。

它的主要作用是在所有代碼塊(chunks)和資產(chǎn)(assets)都生成之后,對這些輸出進(jìn)行進(jìn)一步的操作或修改。

這里 build-import-analysis 插件中的 generateBundle 鉤子正是用于實現(xiàn)對于最終生成的 assets 中的內(nèi)容進(jìn)行修改,尋找當(dāng)前生成的 assets 中所有 dynamicImport 的深層依賴文件從而替換 __VITE_PRELOAD__ 變量。

generateBundle({ format }, bundle) {


      // 檢查生成模塊規(guī)范如果不為 es 則直接返回
      if (format !== 'es') {
        return
      }




      // 如果當(dāng)前環(huán)境并為開啟 modulePreload 的優(yōu)化
      // if (!getInsertPreload(this.environment)) 中的主要目的是在預(yù)加載功能未啟用的情況下,移除對純 CSS 文件的無效 dynamicImport 導(dǎo)入,以確保生成的包(bundle)中沒有無效的導(dǎo)入語句,從而避免運行時錯誤。


      // 在 Vite 中,純 CSS 文件可能會被單獨處理,并從最終的 JavaScript 包中移除。這是因為 CSS 通常會被提取到單獨的 CSS 文件中,以便瀏覽器可以并行加載 CSS 和 JavaScript 文件,從而提高加載性能。
      // 當(dāng)純 CSS 文件被移除后,任何對這些 CSS 文件的導(dǎo)入語句將變成無效的導(dǎo)入。如果不移除這些無效的導(dǎo)入語句,運行時會出現(xiàn)錯誤,因為這些 CSS 文件已經(jīng)不存在于生成的包中。
      
      // 默認(rèn)情況下,modulePreload 都是開啟的。同時,我們的 Demo 中并不涉及 CSS 文件的處理,所以這里的邏輯并不會執(zhí)行。
      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 同時源文件內(nèi)容中包含 preloadMarker
        if (chunk.type === 'chunk' && chunk.code.indexOf(preloadMarker) > -1) {
          const code = chunk.code
          let imports!: ImportSpecifier[]
          try {
            // 獲取模塊中所有的動態(tài) 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) {
            // 遍歷當(dāng)前模塊中所有的 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) {
                // 獲取當(dāng)前動態(tài)導(dǎo)入 dynamicImport 的模塊路徑(相較于應(yīng)用根目錄而言)
                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)


                    // 遞歸當(dāng)前依賴 chunk 的所有 import 靜態(tài)依賴
                    if (chunk.type === 'chunk') {
                      // 對于所有 chunk.imports 進(jìn)行遞歸 addDeps 加入到 deps 中
                      chunk.imports.forEach(addDeps)


                      // 遍歷當(dāng)前代碼塊導(dǎo)入的 CSS 文件
                      // 確保當(dāng)前代碼塊導(dǎo)入的 CSS 在其依賴項之后加載。
                      // 這樣可以防止當(dāng)前代碼塊的樣式被意外覆蓋。
                      chunk.viteMetadata!.importedCss.forEach((file) => {
                        deps.add(file)
                      })
                    }
                  } else {
                    // 如果當(dāng)前依賴的 chunk 并沒有被生成,檢查當(dāng)前 chunk 是否為純 CSS 文件的 dynamicImport 


                    const removedPureCssFiles =
                      removedPureCssFilesCache.get(config)!
                    const chunk = removedPureCssFiles.get(filename)


                    // 如果是的話,則會將 css 文件加入到依賴中
                    // 同時更新 dynamicImport 的 css 為 promise.resolve({}) 防止找不到 css 文件導(dǎo)致的運行時錯誤
                    if (chunk) {
                      if (chunk.viteMetadata!.importedCss.size) {
                        chunk.viteMetadata!.importedCss.forEach((file) => {
                          deps.add(file)
                        })
                        hasRemovedPureCssChunk = true
                      }


                      s.update(expStart, expEnd, 'Promise.resolve({})')
                    }
                  }
                }




                // 將當(dāng)前 dynamicImport 的模塊路徑添加到 deps 中
                // 比如 import('./Contact.tsx') 會將 [root]/assets/Contact.tsx 添加到 deps 中
                addDeps(normalizedFile)
              }


              // 尋找當(dāng)前 dynamicImport 語句中的 preloadMarker 的位置
              let markerStartPos = indexOfMatchInSlice(
                code,
                preloadMarkerRE,
                end,
              )


              // 邊界 case 處理,我們可以忽略這個判斷。找不到的清咖滾具體參考相關(guān) issue #3051
              if (markerStartPos === -1 && imports.length === 1) {
                markerStartPos = indexOfMatchInSlice(code, preloadMarkerRE)
              }




              // 如果找到了 preloadMarker
              // 判斷 vite 構(gòu)建時是否開啟了 modulePreload
              // 如果開啟則將當(dāng)前 dynamicImport 的所有依賴項添加到 deps 中
              // 否則僅會添加對應(yīng) 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 依賴項的處理機制與模塊預(yù)加載(module preloads)的機制是相同的。
                        // 所以,及時沒有開啟 dynamicImport 的 modulePreload 優(yōu)化,仍然需要通過 vite_preload 處理 dynamicImport 的 CSS 依賴項。
                        [...deps].filter((d) => d.endsWith('.css'))
                      : [...deps]
                    : []


                 // 具體可以參考 https://vite.dev/config/build-options.html#build-modulepreload
                 // resolveDependencies 是一個函數(shù),用于確定給定模塊的依賴關(guān)系。在 Vite 的構(gòu)建過程中,Vite 會調(diào)用這個函數(shù)來獲取每個模塊的依賴項,并生成相應(yīng)的預(yù)加載指令。


                 // 在 vite 構(gòu)建過程中我們可以通過 resolveDependencies 函數(shù)來自定義修改模塊的依賴關(guān)系從而響應(yīng) 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 用于在構(gòu)建過程中自定義處理資源 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 會走到這個的邏輯處理
                  // 首先會根據(jù) isRelativeBase 判斷構(gòu)建時的 basename 是否為相對路徑


                  // 如果為相對路徑,調(diào)用 toRelativePath 將每個依賴想相較于 basename 的地址進(jìn)行轉(zhuǎn)換之后調(diào)用 addFileDep


                  // 否則,直接將依賴地址調(diào)用 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),
                  )
                }


                // 最終這里會將當(dāng)前 import 語句中的 __VITE_PRELOAD__ 替換為 __vite__mapDeps([${renderedDeps.join(',')}])
                // renderedDeps 則為當(dāng)前 dynamicImport 模塊所有需要被優(yōu)化的依賴項的 FileDep 類型對象
                s.update(
                  markerStartPos,
                  markerStartPos + preloadMarker.length,
                  renderedDeps.length > 0
                    ? `__vite__mapDeps([${renderedDeps.join(',')}])`
                    : `[]`,
                )
                rewroteMarkerStartPos.add(markerStartPos)
              }
            }
          }


          // 這里的邏輯主要用于生成 __vite__mapDeps 方法
          if (fileDeps.length > 0) {


            // 將 fileDeps 對象轉(zhuǎn)化為字符串
            const fileDepsCode = `[${fileDeps
              .map((fileDep) =>
                // 檢查是否存在 runtime 
                // 關(guān)于 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)
            }
          }




          // 看上去像是為了確保所有的預(yù)加載標(biāo)記都被正確移除。
          // 不過上述的 case 理論上來說已經(jīng)處理了所有的 dynamicImport ,這里具體為什么在檢查一遍,我也不是很清楚
          // But it's not important! ?? 這并不妨礙我們理解 preload 優(yōu)化的原理,我們可以將它標(biāo)記為兜底的異常邊界處理
          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,
            )
          }


          // 修改最終生成的文件內(nèi)容
          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 每一行都進(jìn)行了詳細(xì)的注釋。

在 generateBundle hook 中,簡單來說就是遍歷每一個生成的 chunk ,通過檢查每個 chunk 中的 js assets 中是否包含 preloadMarker 標(biāo)記來檢查生成的資源中是否需要被處理。

如果當(dāng)前文件存在 preloadMarker 標(biāo)記的話,此時會解析出生成的 js 文件中所有的 dynamicImport 語句,遍歷每一個 dynamicImport 語句。

同時將 dynamicImport 的模塊以及依賴的模塊全部通過 addDeps 方法加入到 deps 的 Set 中。

也就說,每個 chunk 中的每個 asset 的每一個 dynamicImport 都存在一個名為 deps 的 Set ,它會收集到當(dāng)前 dynamicImport 模塊的所有依賴(從被動態(tài)導(dǎo)入的自身模塊開始遞歸尋找)。

比如 import('./Contact.tsx') 模塊就會尋找到 Contact、Phone、Name 這三個 chunk 對應(yīng)的 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 對應(yīng)構(gòu)建后生成的 js 資源路徑。

之后,又會在生成的 js 文件中插入這樣一段代碼:

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

在我們的 Demo 中 fileDepsCode 即為 fileDeps 中每一項依賴的靜態(tài)資源地址(也就是執(zhí)行 dynamicImport Contact 時需要依賴的 js 模塊)轉(zhuǎn)化為 JSON 字符串之后的路徑。

Tips: fileDeps 是 asset (資源文件) 緯度的,也就是一個 JS 資源中所有 dynamicImport 的資源都會被加入到 fileDeps 數(shù)組中,而 deps 是每個 dynamicImport 語句維護(hù)的。最終在調(diào)用 preload 時,每個 preload 語句的 deps 是一個索引的數(shù)組,我們會通過 deps 中的索引去 fileDeps 中尋找對應(yīng)下標(biāo)的資源路徑。

最終,代碼中的 await import('./Contact.tsx') 經(jīng)過 vite 的構(gòu)建后會變?yōu)椋?/span>

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

至此,我們已經(jīng)詳細(xì)講解了 Vite 內(nèi)部 modulePreload 預(yù)加載的全部源碼實現(xiàn)。

四、商旅對于 DynamicImport 的內(nèi)部改造

目前,商旅內(nèi)部對 Remix 2.0 的升級優(yōu)化工作已接近尾聲。相比于 Remix 1.0 的運行方式,2.0 中如果僅在服務(wù)端模板生成時為所有 ES 模塊動態(tài)添加 AresHost,對于某些動態(tài)導(dǎo)入(DynamicImport)的模塊,構(gòu)建后代碼發(fā)布時可能會出現(xiàn) modulePreload 標(biāo)簽和 CSS 資源加載 404 的問題。這些 404 資源問題正是由于 Vite 中 build-import-analysis 對 DynamicImport 的優(yōu)化所導(dǎo)致的。

為了解決這一問題,我們不僅對 Remix 進(jìn)行了改造,還對 Vite 中處理 DynamicImport 的邏輯進(jìn)行了優(yōu)化,以支持在 modulePreload 開啟時以及 DynamicImport 模塊中的靜態(tài)資源實現(xiàn) Ares 的運行時 CDN Host 注入。

實際上,Vite 中存在一個實驗性屬性 experimental.renderBuiltUrl,也支持為靜態(tài)資源添加動態(tài) Host。然而,renderBuiltUrl 的局限性在于它無法獲取服務(wù)端的運行變量。由于我們的前端應(yīng)用在服務(wù)端運行時將 AresHost 掛載在每次請求的 request 中,而 renderBuiltUrl 屬性無法訪問每次請求的 request。

我們期望不僅在客戶端運行時,還能在服務(wù)端 SSR 應(yīng)用模板生成時通過 request 獲取動態(tài)的 Ares 前綴并掛載在靜態(tài)資源上,顯然 renderBuiltUrl 無法滿足這一需求。

簡單來說,對于修改后的 Remix 框架,我們將所有攜程相關(guān)的通用框架屬性集成到 RemixContext 中,并通過傳統(tǒng) SSR 應(yīng)用服務(wù)端和客戶端傳遞數(shù)據(jù)的方式(script 腳本)在 window 上掛載 __remixContext.aresHost 屬性。

之后,我們在 Vite 內(nèi)部的 build-import-analysis 插件中的 preload 函數(shù)中增加了一段代碼,為所有鏈接添加 window.__remixContext.aresHost 屬性,從而確保 dynamicImport 模塊中依賴的 CSS 和 modulePreload 腳本能夠正確攜帶當(dāng)前應(yīng)用的 AresHost。

五、結(jié)尾

商旅大前端團(tuán)隊在攜程內(nèi)部是較早采用 Streaming 和 ESModule 技術(shù)的。相比集團(tuán)的 NFES(攜程內(nèi)部一款基于 React 18 + Next.js 13.1.5 + Webpack 5 的前端框架),Remix 在開發(fā)友好度和服務(wù)端 Streaming 處理方面具有獨特優(yōu)勢。目前,Remix 已在商旅的大流量頁面中得到了驗證,并取得了良好效果。

本文主要從 preload 細(xì)節(jié)入手,分享我們在這方面遇到的問題和心得。后續(xù)我們將繼續(xù)分享更多關(guān)于 Remix 的技術(shù)細(xì)節(jié),并為大家介紹更多商旅對 Remix 的改造。

責(zé)任編輯:張燕妮 來源: 攜程技術(shù)
相關(guān)推薦

2024-12-26 09:27:51

2023-12-29 09:42:28

攜程開發(fā)

2023-08-18 10:49:14

開發(fā)攜程

2023-06-06 11:49:24

2024-04-18 09:41:53

2022-06-17 10:44:49

實體鏈接系統(tǒng)旅游AI知識圖譜攜程

2022-03-30 18:39:51

TiDBHTAPCDP

2022-07-15 09:20:17

性能優(yōu)化方案

2022-07-08 09:38:27

攜程酒店Flutter技術(shù)跨平臺整合

2024-03-22 15:09:32

2023-07-07 12:26:39

攜程開發(fā)

2022-04-28 09:36:47

Redis內(nèi)存結(jié)構(gòu)內(nèi)存管理

2024-07-05 15:05:00

2017-02-23 21:17:00

致遠(yuǎn)

2023-06-06 16:01:00

Web優(yōu)化

2023-07-07 14:18:57

攜程實踐

2023-11-13 11:27:58

攜程可視化

2024-11-05 09:56:30

2023-11-06 09:56:10

研究代碼

2024-04-17 07:21:52

物化視圖查詢加速器數(shù)據(jù)倉庫
點贊
收藏

51CTO技術(shù)棧公眾號