精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

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

一、引言

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

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

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

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

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

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

在 Remix 1.0 下,這種工作機(jī)制完全滿足我們的需求,并且運(yùn)行良好。然而,在商旅從 Remix 1.0 升級(jí)到 2.0 后,我們發(fā)現(xiàn)某些 CSS 資源以及 modulePreload 的 JavaScript 資源仍然會(huì)出現(xiàn) 404 響應(yīng)。

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

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

二、模塊懶加載

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

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

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

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

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

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

我們以 React 來看一個(gè)簡(jiǎn)單的例子:

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>頁(yè)面標(biāo)題</h1>


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


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


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


export default App;

在這個(gè)示例中:

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

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

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

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

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

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

3.1 表象

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

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

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

首先我們通過 vite 命令行初始化一個(gè)代碼倉(cāng)庫(kù),之后我們對(duì)新建的代碼稍做修改:

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


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


// 這里的手機(jī)號(hào)組件、姓名組件可以忽略
// 實(shí)際上特意這么寫是為了利用 dynamicImport 的 splitChunk 特性
// vite 在構(gòu)建時(shí)對(duì)于 dynamicImport 的模塊是會(huì)進(jìn)行 splitChunk 的
// 自然 Phone、Name 模塊在構(gòu)建時(shí)會(huì)被拆分為兩個(gè) 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>頁(yè)面標(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)系人組件依賴的手機(jī)號(hào)以及姓名組件 */}
    <Phone></Phone>
    <Name></Name>
  </div>;
};


export default Contact;
// components/Phone.tsx
import React from 'react';


const Phone = () => {
  return <div>手機(jī)號(hào)組件</div>;
};


export default Phone;
// components/Name.tsx
import React from 'react';


const Name = () => {
  return <div>姓名組件</div>;
};


export default Name;

上邊的 Demo 中,我們?cè)?App.tsx 中編寫了一個(gè)簡(jiǎn)單的頁(yè)面。

頁(yè)面中使用 dynamicImport 引入了三個(gè)模塊,分別為:

  • Contact 聯(lián)系人模塊
  • Phone 手機(jī)模塊
  • Name 姓名模塊

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

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

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

同時(shí),在 Contact 模塊中我們又引入了 Phone、Name 兩個(gè)模塊。

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

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

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

圖片


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

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

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

3.2 機(jī)制

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

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

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

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

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

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

圖片


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

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

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

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)建時(shí) Vite 對(duì)于使用 dynamicImport 插入的動(dòng)態(tài)加載的優(yōu)化方法。
  • __vite__mapDeps([0, 1, 2])則是傳遞給 __vitePreload 的第二個(gè)參數(shù),它表示當(dāng)前動(dòng)態(tài)引入的 dynamicImport 包含的所有依賴 chunk,也就是 Contact(自身)、PhoneName 三個(gè) chunk。

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

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

3.3 原理

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

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

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

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

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

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

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

所謂的 transform Hook 就是掃描每一個(gè)模塊,對(duì)于模塊內(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 會(huì)在每一個(gè) module 上執(zhí)行
    async transform(source, importer) {
    
      // 如果當(dāng)前模塊是在 node_modules 中,且代碼中沒有任何動(dòng)態(tài)導(dǎo)入語(yǔ)法,則直接返回。不進(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 語(yǔ)法
        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)建時(shí)(非 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 時(shí)
      // Rollup 無法分析訪問的變量是否存在 TreeShaking
      // 下面的代碼主要作用為試圖匹配常見的訪問語(yǔ)法,以將其“復(fù)制”到由預(yù)加載幫助程序包裝的動(dòng)態(tài)導(dǎo)入中
      // 例如:`const {foo} = await import('foo')` 會(huì)被轉(zhuǎn)換為 `const {foo} = await __vitePreload(async () => { const {foo} = await import('foo');return {foo}}, ...)` 簡(jiǎn)單說就是防止直接使用 __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 引入語(yǔ)句
      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 語(yǔ)句的屬性導(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 語(yǔ)句為 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,同時(shí)該模塊代碼中不存在 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 鉤子的全部?jī)?nèi)容,并在關(guān)鍵環(huán)節(jié)添加了相應(yīng)的注釋說明。簡(jiǎn)而言之,transform 鉤子的作用可以歸納為以下幾點(diǎn):

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

2)注入預(yù)加載 Polyfill:對(duì)于所有的動(dòng)態(tài)導(dǎo)入語(yǔ)句,使用 magic-string 克隆一份源代碼,然后結(jié)合第一步掃描出的 dynamicImport 語(yǔ)句進(jìn)行字符串拼接,注入預(yù)加載 Polyfill。例如,import ('./Contact.tsx') 經(jīng)過 transform 鉤子處理后會(huì)被轉(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 鉤子中不會(huì)處理這兩個(gè)字符串變量,目前僅用作占位。而 __vitePreload 則是外層包裹的 Polyfill 方法。

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

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

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

3.3.2 增加 preload 輔助語(yǔ)句 - resolveId/load

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

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

由于我們?cè)?nbsp;App.tsx 頂部插入了 import { ${preloadMethod} } from "${preloadHelperId}" 語(yǔ)句,因此在 App.tsx 的 moduleParse 階段,Vite 會(huì)遞歸分析 App.tsx 中引入的 preloadHelperId 模塊。

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

圖片


此時(shí) vite:build-import-analysis 插件的 resolveId 和 load hook 就會(huì)派上用場(chǎng):

// ...


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


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


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


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


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


        // 我們暫時(shí)忽略 renderBuiltUrl ,因?yàn)槲覀儤?gòu)建時(shí)并未傳入該配置
        
        // 自然 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)
  })
}

對(duì)于引入 preloadHelperId 的模塊,build-import-analysis 會(huì)在 resolveId 和 load 階段識(shí)別并添加 preload 方法的靜態(tài)聲明。preload 方法支持三個(gè)參數(shù):

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

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

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

也就說,在 vite:build-import-analysis 的 resolveId 以及 load 階段為會(huì)存在 __vite_preload 的模塊添加對(duì)于 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) 插件鉤子之一,用于在生成每個(gè)代碼塊(chunk)時(shí)進(jìn)行自定義處理。它的主要功能是在代碼塊被轉(zhuǎn)換為最終輸出格式之前,對(duì)其進(jìn)行進(jìn)一步的操作或修改。

build-import-analysis 會(huì)在渲染每一個(gè) chunk 時(shí),通過 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
    },

簡(jiǎn)單來說,在渲染每一個(gè)時(shí)會(huì)判斷源代碼中是否存在 isModernFlag (code.indexOf(isModernFlag) > -1 ):

  • 如果存在,則會(huì)判斷生成的 chunk 是否為 esm 格式。如果是的話,則會(huì)將 isModernFlag 全部替換為 true,否則會(huì)全部替換為 false。
  • 如果不存在則不會(huì)進(jìn)行任何處理。

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

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

此時(shí),經(jīng)過 renderChunk 的處理會(huì)變?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)過上述各個(gè)階段的處理,vite 內(nèi)部會(huì)將 import ('Contact.tsx') 轉(zhuǎn)化為:

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

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

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

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

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

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

這里 build-import-analysis 插件中的 generateBundle 鉤子正是用于實(shí)現(xiàn)對(duì)于最終生成的 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ù)加載功能未啟用的情況下,移除對(duì)純 CSS 文件的無效 dynamicImport 導(dǎo)入,以確保生成的包(bundle)中沒有無效的導(dǎo)入語(yǔ)句,從而避免運(yùn)行時(shí)錯(cuò)誤。


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


                      // 遍歷當(dāng)前代碼塊導(dǎo)入的 CSS 文件
                      // 確保當(dāng)前代碼塊導(dǎo)入的 CSS 在其依賴項(xiàng)之后加載。
                      // 這樣可以防止當(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)


                    // 如果是的話,則會(huì)將 css 文件加入到依賴中
                    // 同時(shí)更新 dynamicImport 的 css 為 promise.resolve({}) 防止找不到 css 文件導(dǎo)致的運(yùn)行時(shí)錯(cuò)誤
                    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') 會(huì)將 [root]/assets/Contact.tsx 添加到 deps 中
                addDeps(normalizedFile)
              }


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


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




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


                 // 具體可以參考 https://vite.dev/config/build-options.html#build-modulepreload
                 // resolveDependencies 是一個(gè)函數(shù),用于確定給定模塊的依賴關(guān)系。在 Vite 的構(gòu)建過程中,Vite 會(huì)調(diào)用這個(gè)函數(shù)來獲取每個(gè)模塊的依賴項(xiàng),并生成相應(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 選項(xiàng)
                // 簡(jiǎn)單來說 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 中對(duì)于 depsArray 會(huì)走到這個(gè)的邏輯處理
                  // 首先會(huì)根據(jù) isRelativeBase 判斷構(gòu)建時(shí)的 basename 是否為相對(duì)路徑


                  // 如果為相對(duì)路徑,調(diào)用 toRelativePath 將每個(gè)依賴想相較于 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),
                  )
                }


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


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


            // 將 fileDeps 對(duì)象轉(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()
                }
              }
            }
          }
        }
      }
    },

上邊的代碼中,我對(duì)于 generateBundle hook 每一行都進(jìn)行了詳細(xì)的注釋。

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

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

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

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

比如 import('./Contact.tsx') 模塊就會(huì)尋找到 Contact、Phone、Name 這三個(gè) chunk 對(duì)應(yīng)的 js asset 文件路徑。

之后,會(huì)將上述生成的

__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(',')}],
  ''
)

對(duì)于我們 Demo 中的 Contact 模塊,renderedDeps 則是 Contact、Phone 以及 Name 對(duì)應(yīng)構(gòu)建后生成的 js 資源路徑。

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

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

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

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

最終,代碼中的 await import('./Contact.tsx') 經(jīng)過 vite 的構(gòu)建后會(huì)變?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ù)加載的全部源碼實(shí)現(xiàn)。

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

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

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

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

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

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

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

五、結(jié)尾

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

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

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

2024-12-18 10:03:30

2023-12-29 09:42:28

攜程開發(fā)

2023-08-18 10:49:14

開發(fā)攜程

2023-06-06 11:49:24

2022-06-17 10:44:49

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

2022-03-30 18:39:51

TiDBHTAPCDP

2024-03-22 15:09:32

2024-04-18 09:41:53

2022-07-08 09:38:27

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

2022-07-15 09:20:17

性能優(yōu)化方案

2023-07-07 12:26:39

攜程開發(fā)

2022-04-28 09:36:47

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

2017-02-23 21:17:00

致遠(yuǎn)

2024-07-05 15:05:00

2023-06-06 16:01:00

Web優(yōu)化

2023-11-06 09:56:10

研究代碼

2023-11-13 11:27:58

攜程可視化

2023-07-07 14:18:57

攜程實(shí)踐

2024-11-05 09:56:30

2020-12-04 14:32:33

AndroidJetpackKotlin
點(diǎn)贊
收藏

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

免费a级在线播放| 欧美日韩在线视频播放| 99亚洲乱人伦aⅴ精品| 亚洲一区电影777| 欧美久久综合性欧美| 在线观看中文字幕网站| 激情欧美亚洲| 在线播放日韩专区| 国产麻豆剧传媒精品国产| 综合毛片免费视频| 亚洲欧美一区二区三区极速播放| 国产中文一区二区| 国产一区二区三区成人| 亚洲一区网站| 插插插亚洲综合网| 91成年人网站| 91精品丝袜国产高跟在线| 91成人网在线| 奇米影视亚洲色图| 菠萝蜜视频国产在线播放| 久久五月婷婷丁香社区| 亚洲xxx视频| 中文字字幕在线观看| 国产精品亚洲欧美| 欧美大片在线看| 一级片黄色录像| 亚洲人亚洲人色久| 精品久久人人做人人爰| 午夜一区二区视频| 另类中文字幕国产精品| 婷婷国产在线综合| www.好吊操| 最新黄网在线观看| 国产精品久久久久久久久久久免费看 | 欧美一级淫片007| 日韩一级片播放| 亚洲妇女成熟| 午夜精品一区二区三区电影天堂| 黄色小视频大全| 欧美精品videos另类| 国产亚洲欧洲997久久综合| 精品日韩美女| 性xxxxbbbb| 97精品久久久久中文字幕| 99国产超薄丝袜足j在线观看| 91丨porny丨在线中文 | 婷婷亚洲久悠悠色悠在线播放| 影音先锋成人资源网站| 老司机在线视频二区| 国产精品色眯眯| 天堂√在线观看一区二区| 男人天堂网在线| 久久理论电影网| 任我爽在线视频精品一| 黄色av网站在线| 国产视频一区在线播放| 日韩欧美一区二区三区四区| 成人福利在线| 中文在线免费一区三区高中清不卡| 日本精品二区| 永久免费av在线| 1区2区3区精品视频| 一区二区三区日韩视频| 超碰在线caoporen| 亚洲一区二区三区四区在线| 亚洲精品蜜桃久久久久久| 国产偷倩在线播放| 天天免费综合色| wwwxxx黄色片| 国产一区高清| 日韩午夜电影在线观看| 亚洲乱妇老熟女爽到高潮的片| 荡女精品导航| 亚洲欧美日韩天堂| 成年人视频软件| 欧美高清日韩| 17婷婷久久www| 亚洲精品一区二三区| 精品在线亚洲视频| av日韩中文字幕| 涩涩视频在线观看免费| 中文字幕精品—区二区四季| 欧美xxxx吸乳| 韩国成人免费视频| 色婷婷狠狠综合| 特级西西444www| 日韩精品导航| 日韩在线www| 日本三级中文字幕| 三级不卡在线观看| 亚洲最大的网站| 日韩av资源站| 亚洲乱码一区二区三区在线观看| 最新91在线视频| 国产亚洲精品久久久久久豆腐| 亚洲午夜精品一区 二区 三区| 午夜精品久久久99热福利| 97人妻一区二区精品视频| 国产一区二区三区美女| 久久久精彩视频| 看黄网站在线观看| 色呦呦国产精品| 在线播放国产视频| 视频一区欧美| 国模私拍视频一区| 亚洲无码精品在线播放| 91在线播放网址| 桥本有菜av在线| 在线观看涩涩| 欧美成人激情免费网| 永久免费av无码网站性色av| 激情综合在线| 国产日韩欧美电影在线观看| 亚州av在线播放| 亚洲精品日日夜夜| 日本激情综合网| 日韩高清成人在线| 久久久久国产精品一区| 国产免费高清视频| 国产欧美一区二区三区沐欲 | 男人的天堂av网| 影音先锋国产精品| 99re在线观看| av毛片在线免费| 欧美日韩一区二区在线观看视频| 在线观看日韩精品视频| 黄色成人av网站| 91麻豆国产精品| 欧美一区二区三区在线观看免费| 日韩欧美高清在线视频| 朝桐光av一区二区三区| 欧美日韩在线大尺度| 91在线观看免费网站| 日本中文字幕在线播放| 欧美午夜片在线观看| 亚洲精品成人无码| 视频在线观看91| 欧美成人蜜桃| 欧美黑人疯狂性受xxxxx野外| 日韩成人在线电影网| a v视频在线观看| 成人h版在线观看| 日韩av中文字幕第一页| 高清精品视频| 97视频在线观看免费高清完整版在线观看 | av电影院在线看| 亚洲第一精品夜夜躁人人爽| 精品在线免费观看视频| 高清不卡一二三区| www.好吊操| 欧美成人一区在线观看| 欧美在线一区二区三区四| 你懂的在线观看| 91黄视频在线观看| 国产精品av久久久久久无| 免费高清不卡av| 特级黄色录像片| 一区二区三区四区高清视频 | 国产欧美日产一区| 欧美一级特黄a| 999国产精品视频| 91在线网站视频| 久草免费在线色站| 亚洲精品国产精品久久清纯直播| 欧美日韩一二三四区| 国产亚洲欧美一级| 五月天中文字幕在线| 亚洲乱码精品| 国产欧美一区二区三区另类精品| 亚洲天堂免费电影| 国产一区二区三区视频在线观看| 一区二区视频免费观看| 亚洲一区二区三区三| 国产制服丝袜在线| 免费成人性网站| av中文字幕av| 蜜臀91精品国产高清在线观看| 国产不卡av在线免费观看| 日本激情视频在线观看| 日韩免费电影一区| 国产99免费视频| 亚洲欧美日韩一区| 少妇光屁股影院| 黄色资源网久久资源365| 99热久久这里只有精品| 精品国产乱码| 成人在线看片| 成人网ww555视频免费看| 欧美日韩高清区| 国产综合在线观看| 欧美大片在线观看| 黄色av网站免费观看| 亚洲精品日日夜夜| www久久久久久久| 成人黄色av电影| 亚洲美女性囗交| 99精品国产在热久久婷婷| 亚洲图片小说在线| 日韩欧美四区| 3d动漫啪啪精品一区二区免费| 免费成人动漫| 久久久久久久网站| 一本一道波多野毛片中文在线| 亚洲国产精品高清久久久| 中文字幕一区二区三区波野结 | 风间由美性色一区二区三区四区| 国产精品观看在线亚洲人成网| 牛牛精品视频在线| 搡老女人一区二区三区视频tv| 欧美 日韩 中文字幕| 在线电影欧美成精品| 国产免费av一区| 夜夜精品浪潮av一区二区三区| 免费看的黄色录像| 久久综合一区二区| 亚洲免费观看在线| 国产在线精品一区二区夜色 | 色综合.com| 日本精品视频在线| 日本а中文在线天堂| 欧美激情xxxx| 91精品久久| 日日噜噜噜夜夜爽亚洲精品| 国产资源在线播放| 日韩电影在线观看永久视频免费网站 | 日韩黄色一级片| 亚洲中文字幕无码中文字| 国产在线不卡| 黄色特一级视频| 国产精品久久久久9999赢消| 天天人人精品| 禁果av一区二区三区| 蜜桃臀一区二区三区| 精品丝袜久久| 黑人另类av| 老司机aⅴ在线精品导航| 国产成人精品福利一区二区三区 | 久久久国产精品黄毛片| 亚洲视频 欧洲视频| 中国美女黄色一级片| 中文字幕 久热精品 视频在线 | 国产精品久久久久9999| 免费日韩电影| 国产精品久久久久久网站 | 国产美女一区| 国内自拍中文字幕| 亚洲视频中文| 免费无码毛片一区二三区| 一区二区国产精品| 黄色一级一级片| 免费在线看一区| 中文字幕国产免费| 国产一区二区三区在线看麻豆| 国产999免费视频| 国产99久久久国产精品潘金 | 国产人妻精品一区二区三区| 欧美一区二区视频在线观看| 国内精品国产成人国产三级| 欧美成人午夜电影| 五月婷婷免费视频| 国产亚洲精品日韩| 里番在线观看网站| 欧美激情一区二区久久久| 九色porny丨首页入口在线| 热99精品里视频精品| 成人激情视屏| 99国产视频| 日本网站在线免费观看| 日韩精品每日更新| 国产三级国产精品国产专区50| 麻豆精品一二三| 国产又黄又嫩又滑又白| 91在线丨porny丨国产| 超薄肉色丝袜一二三| 亚洲免费av高清| 国产精品第9页| 欧美色窝79yyyycom| 精品女同一区二区三区| 日韩经典中文字幕在线观看| 波多野结衣在线影院| 欧美大片在线影院| 日韩精品专区| 亚洲一区二区三区视频| 五月综合久久| 国产精品jizz在线观看老狼| 日韩午夜一区| 国产无遮挡猛进猛出免费软件| 成人网页在线观看| jizz18女人高潮| 亚洲影院免费观看| 自拍偷拍精品视频| 亚洲第一视频在线观看| 91社区在线观看播放| 国内伊人久久久久久网站视频| se69色成人网wwwsex| 国产一区二区黄色| 色偷偷综合网| 久久综合九色综合88i| 狠狠网亚洲精品| 欧美亚一区二区三区| 亚洲精品老司机| 国产天堂第一区| 亚洲精品久久久久久久久久久久| 最新国产在线观看| 欧美极品欧美精品欧美视频| 国产黄色精品| 日产中文字幕在线精品一区| 国产精品99久久精品| 男人舔女人下面高潮视频| 高清在线不卡av| 久久国产高清视频| 色婷婷av一区二区三区gif | 国产精品久久久久久妇女| 国产日韩欧美精品| 欧美 日韩 国产一区二区在线视频 | 欧美第一区第二区| 粉嫩一区二区三区国产精品| 97在线视频免费观看| 久久久久久亚洲精品美女| 亚洲日本精品| 日韩二区三区四区| 久久国产精品无码一级毛片| 亚洲国产中文字幕在线视频综合| 国产精品视频无码| www.精品av.com| 精品肉辣文txt下载| 欧美日韩一区在线播放| 亚洲免费精品| 精品熟女一区二区三区| 一区二区三区成人在线视频| 国产孕妇孕交大片孕| 在线色欧美三级视频| 亚洲黄色免费av| 久久久久一区二区| 国产精品视区| xxx在线播放| 在线观看欧美日本| 国产高清在线观看| 国产精品女人久久久久久| 国产亚洲电影| 日韩在线第三页| 国产欧美日韩在线视频| 国产在线一级片| 色多多国产成人永久免费网站 | 超级碰在线观看| 韩国三级中文字幕hd久久精品| 三级黄色片在线观看| 欧美夫妻性生活| free性欧美hd另类精品| 91观看网站| 亚洲激情国产| 大地资源二中文在线影视观看| 黑人与娇小精品av专区| 欧美日韩免费做爰大片| 国产精品户外野外| 欧美hd在线| 黑人巨大猛交丰满少妇| 亚洲国产日韩在线一区模特| 天堂中文在线观看视频| 日本精品视频在线观看| 日韩精品欧美| 99精品视频免费版的特色功能| 亚洲综合免费观看高清在线观看| 男人的天堂a在线| 国产99久久精品一区二区永久免费 | 亚洲欧美在线观看| 99免费在线视频| 久久久日本电影| 国产成人一区二区三区影院| 日日干夜夜操s8| 伊人色综合久久天天人手人婷| 免费看av毛片| 国产精品男人的天堂| 午夜天堂精品久久久久| 国产人妻人伦精品1国产丝袜 | 国 产 黄 色 大 片| 秋霞成人午夜鲁丝一区二区三区| 中文文字幕一区二区三三| 一区二区三区影院| 四虎精品成人影院观看地址| 国产精品久久久久久亚洲影视| 国产精品s色| 毛片网站免费观看| 7878成人国产在线观看| 国产精品xx| 在线观看亚洲视频啊啊啊啊| 懂色av中文一区二区三区| 亚洲大尺度在线观看| 欧美精品在线网站| 亚洲婷婷丁香| 国内精品国产三级国产aⅴ久| 色94色欧美sute亚洲线路二| 国产精品一区二区三区视频网站| 精品国产乱码久久久久| 久久精品99国产精品日本| 中文字幕精品三级久久久| 久久夜精品va视频免费观看| 亚洲精品小区久久久久久| 亚洲精品一二三四| 欧美日韩电影在线播放|