国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

[萬字總結(jié)] 一文吃透 Webpack 核心原理

共 21626字,需瀏覽 44分鐘

 ·

2021-05-10 12:51


(給前端大學(xué)加星標(biāo),提升前端技能.

作者: Tecvan

https://juejin.cn/post/6949040393165996040

背景

Webpack 特別難學(xué)?。。?/p>

時(shí)至 5.0 版本之后,Webpack 功能集變得非常龐大,包括:模塊打包、代碼分割、按需加載、HMR、Tree-shaking、文件監(jiān)聽、sourcemap、Module Federation、devServer、DLL、多進(jìn)程等等,為了實(shí)現(xiàn)這些功能,webpack 的代碼量已經(jīng)到了驚人的程度:

  • 498 份JS文件
  • 18862 行注釋
  • 73548 行代碼
  • 54 個(gè) module 類型
  • 69 個(gè) dependency 類型
  • 162 個(gè)內(nèi)置插件
  • 237 個(gè)hook

在這個(gè)數(shù)量級(jí)下,源碼的閱讀、分析、學(xué)習(xí)成本非常高,加上 webpack 官網(wǎng)語焉不詳?shù)奈臋n,導(dǎo)致 webpack 的學(xué)習(xí)、上手成本極其高。為此,社區(qū)圍繞著 Webpack 衍生出了各種手腳架,比如 vue-cli、create-react-app,解決“用”的問題。

但這又導(dǎo)致一個(gè)新的問題,大部分人在工程化方面逐漸變成一個(gè)配置工程師,停留在“會(huì)用會(huì)配”但是不知道黑盒里面到底是怎么轉(zhuǎn)的階段,遇到具體問題就瞎了:

  • 想給基礎(chǔ)庫做個(gè)升級(jí),出現(xiàn)兼容性問題跑不動(dòng)了,直接放棄
  • 想優(yōu)化一下編譯性能,但是不清楚內(nèi)部原理,無從下手

究其原因還是對(duì) webpack 內(nèi)部運(yùn)行機(jī)制沒有形成必要的整體認(rèn)知,無法迅速定位問題 —— 對(duì),連問題的本質(zhì)都常??床怀?,所謂的不能透過現(xiàn)象看本質(zhì),那本質(zhì)是啥?我個(gè)人將 webpack 整個(gè)龐大的體系抽象為三方面的知識(shí):

  1. 構(gòu)建的核心流程
  2. loader 的作用
  3. plugin 架構(gòu)與常用套路

三者協(xié)作構(gòu)成 webpack 的主體框架:

理解了這三塊內(nèi)容就算是入了個(gè)門,對(duì) Webpack 有了一個(gè)最最基礎(chǔ)的認(rèn)知了,工作中再遇到問題也就能按圖索驥了。補(bǔ)充一句,作為一份入門教程,本文不會(huì)展開太多 webpack 代碼層面的細(xì)節(jié) —— 我的精力也不允許,所以讀者也不需要看到一堆文字就產(chǎn)生特別大的心理負(fù)擔(dān)。

核心流程解析

首先,我們要理解一個(gè)點(diǎn),Webpack 最核心的功能:

At its core, webpack is a static module bundler for modern JavaScript applications.

也就是將各種類型的資源,包括圖片、css、js等,轉(zhuǎn)譯、組合、拼接、生成 JS 格式的 bundler 文件。官網(wǎng)首頁的動(dòng)畫很形象地表達(dá)了這一點(diǎn):

1d66a833-2841-4a8a-a91a-0da800fab306.png

這個(gè)過程核心完成了 內(nèi)容轉(zhuǎn)換 + 資源合并 兩種功能,實(shí)現(xiàn)上包含三個(gè)階段:

  1. 初始化階段:
    1. 初始化參數(shù):從配置文件、 配置對(duì)象、Shell 參數(shù)中讀取,與默認(rèn)配置結(jié)合得出最終的參數(shù)
    2. 創(chuàng)建編譯器對(duì)象:用上一步得到的參數(shù)創(chuàng)建 Compiler 對(duì)象
    3. 初始化編譯環(huán)境:包括注入內(nèi)置插件、注冊(cè)各種模塊工廠、初始化 RuleSet 集合、加載配置的插件等
    4. 開始編譯:執(zhí)行 compiler 對(duì)象的 run 方法
    5. 確定入口:根據(jù)配置中的 entry 找出所有的入口文件,調(diào)用 compilition.addEntry 將入口文件轉(zhuǎn)換為 dependence 對(duì)象
  2. 構(gòu)建階段:
    1. 編譯模塊(make):根據(jù) entry 對(duì)應(yīng)的 dependence 創(chuàng)建 module 對(duì)象,調(diào)用 loader 將模塊轉(zhuǎn)譯為標(biāo)準(zhǔn) JS 內(nèi)容,調(diào)用 JS 解釋器將內(nèi)容轉(zhuǎn)換為 AST 對(duì)象,從中找出該模塊依賴的模塊,再 遞歸 本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理
    2. 完成模塊編譯:上一步遞歸處理所有能觸達(dá)到的模塊后,得到了每個(gè)模塊被翻譯后的內(nèi)容以及它們之間的 依賴關(guān)系圖
  3. 生成階段:
    1. 輸出資源(seal):根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個(gè)個(gè)包含多個(gè)模塊的 Chunk,再把每個(gè) Chunk 轉(zhuǎn)換成一個(gè)單獨(dú)的文件加入到輸出列表,這步是可以修改輸出內(nèi)容的最后機(jī)會(huì)
    2. 寫入文件系統(tǒng)(emitAssets):在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)

單次構(gòu)建過程自上而下按順序執(zhí)行,下面會(huì)展開聊聊細(xì)節(jié),在此之前,對(duì)上述提及的各類技術(shù)名詞不太熟悉的同學(xué),可以先看看簡介:

  • Entry:編譯入口,webpack 編譯的起點(diǎn)
  • Compiler:編譯管理器,webpack 啟動(dòng)后會(huì)創(chuàng)建 compiler 對(duì)象,該對(duì)象一直存活知道結(jié)束退出
  • Compilation:單次編輯過程的管理器,比如 watch = true 時(shí),運(yùn)行過程中只有一個(gè) compiler 但每次文件變更觸發(fā)重新編譯時(shí),都會(huì)創(chuàng)建一個(gè)新的 compilation 對(duì)象
  • Dependence:依賴對(duì)象,webpack 基于該類型記錄模塊間依賴關(guān)系
  • Module:webpack 內(nèi)部所有資源都會(huì)以“module”對(duì)象形式存在,所有關(guān)于資源的操作、轉(zhuǎn)譯、合并都是以 “module” 為基本單位進(jìn)行的
  • Chunk:編譯完成準(zhǔn)備輸出時(shí),webpack 會(huì)將 module 按特定的規(guī)則組織成一個(gè)一個(gè)的 chunk,這些 chunk 某種程度上跟最終輸出一一對(duì)應(yīng)
  • Loader:資源內(nèi)容轉(zhuǎn)換器,其實(shí)就是實(shí)現(xiàn)從內(nèi)容 A 轉(zhuǎn)換 B 的轉(zhuǎn)換器
  • Plugin:webpack構(gòu)建過程中,會(huì)在特定的時(shí)機(jī)廣播對(duì)應(yīng)的事件,插件監(jiān)聽這些事件,在特定時(shí)間點(diǎn)介入編譯過程

webpack 編譯過程都是圍繞著這些關(guān)鍵對(duì)象展開的,更詳細(xì)完整的信息,可以參考 Webpack 知識(shí)圖譜 。

初始化階段

學(xué)習(xí)一個(gè)項(xiàng)目的源碼通常都是從入口開始看起,按圖索驥慢慢摸索出套路的,所以先來看看 webpack 的初始化過程:

解釋一下:

  1. process.args + webpack.config.js 合并成用戶配置
  2. 調(diào)用 validateSchema 校驗(yàn)配置
  3. 調(diào)用 getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最終配置
  4. 創(chuàng)建 compiler 對(duì)象
  5. 遍歷用戶定義的 plugins 集合,執(zhí)行插件的 apply 方法
  6. 調(diào)用 new WebpackOptionsApply().process 方法,加載各種內(nèi)置插件

主要邏輯集中在 WebpackOptionsApply 類,webpack 內(nèi)置了數(shù)百個(gè)插件,這些插件并不需要我們手動(dòng)配置,WebpackOptionsApply 會(huì)在初始化階段根據(jù)配置內(nèi)容動(dòng)態(tài)注入對(duì)應(yīng)的插件,包括:

  • 注入 EntryOptionPlugin 插件,處理 entry 配置
  • 根據(jù) devtool 值判斷后續(xù)用那個(gè)插件處理 sourcemap,可選值:EvalSourceMapDevToolPlugin、SourceMapDevToolPlugin、EvalDevToolModulePlugin
  • 注入 RuntimePlugin ,用于根據(jù)代碼內(nèi)容動(dòng)態(tài)注入 webpack 運(yùn)行時(shí)

到這里,compiler 實(shí)例就被創(chuàng)建出來了,相應(yīng)的環(huán)境參數(shù)也預(yù)設(shè)好了,緊接著開始調(diào)用 compiler.compile 函數(shù):

// 取自 webpack/lib/compiler.js 
compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      // ...
      const compilation = this.newCompilation(params);
      this.hooks.make.callAsync(compilation, err => {
        // ...
        this.hooks.finishMake.callAsync(compilation, err => {
          // ...
          process.nextTick(() => {
            compilation.finish(err => {
              compilation.seal(err => {...});
            });
          });
        });
      });
    });
  }

Webpack 架構(gòu)很靈活,但代價(jià)是犧牲了源碼的直觀性,比如說上面說的初始化流程,從創(chuàng)建 compiler 實(shí)例到調(diào)用 make 鉤子,邏輯鏈路很長:

  • 啟動(dòng) webpack ,觸發(fā) lib/webpack.js 文件中 createCompiler 方法
  • createCompiler 方法內(nèi)部調(diào)用 WebpackOptionsApply 插件
  • WebpackOptionsApply 定義在 lib/WebpackOptionsApply.js 文件,內(nèi)部根據(jù) entry 配置決定注入 entry 相關(guān)的插件,包括:DllEntryPluginDynamicEntryPlugin、EntryPluginPrefetchPlugin、ProgressPlugin、ContainerPlugin
  • Entry 相關(guān)插件,如 lib/EntryPlugin.jsEntryPlugin 監(jiān)聽 compiler.make 鉤子
  • lib/compiler.jscompile 函數(shù)內(nèi)調(diào)用 this.hooks.make.callAsync
  • 觸發(fā) EntryPluginmake 回調(diào),在回調(diào)中執(zhí)行 compilation.addEntry 函數(shù)
  • compilation.addEntry 函數(shù)內(nèi)部經(jīng)過一坨與主流程無關(guān)的 hook 之后,再調(diào)用 handleModuleCreate 函數(shù),正式開始構(gòu)建內(nèi)容

這個(gè)過程需要在 webpack 初始化的時(shí)候預(yù)埋下各種插件,經(jīng)歷 4 個(gè)文件,7次跳轉(zhuǎn)才開始進(jìn)入主題,前戲太足了,如果讀者對(duì) webpack 的概念、架構(gòu)、組件沒有足夠了解時(shí),源碼閱讀過程會(huì)很痛苦。

關(guān)于這個(gè)問題,我在文章最后總結(jié)了一些技巧和建議,有興趣的可以滑到附錄閱讀模塊。

構(gòu)建階段

基本流程

你有沒有思考過這樣的問題:

  • Webpack 編譯過程會(huì)將源碼解析為 AST 嗎?webpack 與 babel 分別實(shí)現(xiàn)了什么?
  • Webpack 編譯過程中,如何識(shí)別資源對(duì)其他資源的依賴?
  • 相對(duì)于 grunt、gulp 等流式構(gòu)建工具,為什么 webpack 會(huì)被認(rèn)為是新一代的構(gòu)建工具?

這些問題,基本上在構(gòu)建階段都能看出一些端倪。構(gòu)建階段從 entry 開始遞歸解析資源與資源的依賴,在 compilation 對(duì)象內(nèi)逐步構(gòu)建出 module 集合以及 module 之間的依賴關(guān)系,核心流程:

解釋一下,構(gòu)建階段從入口文件開始:

  1. 調(diào)用 handleModuleCreate ,根據(jù)文件類型構(gòu)建 module 子類
  2. 調(diào)用 loader-runner 倉庫的 runLoaders 轉(zhuǎn)譯 module 內(nèi)容,通常是從各類資源類型轉(zhuǎn)譯為 JavaScript 文本
  3. 調(diào)用 acorn 將 JS 文本解析為AST
  4. 遍歷 AST,觸發(fā)各種鉤子
    1. HarmonyExportDependencyParserPlugin 插件監(jiān)聽 exportImportSpecifier 鉤子,解讀 JS 文本對(duì)應(yīng)的資源依賴
    2. 調(diào)用 module 對(duì)象的 addDependency 將依賴對(duì)象加入到 module 依賴列表中
  5. AST 遍歷完畢后,調(diào)用 module.handleParseResult 處理模塊依賴
  6. 對(duì)于 module 新增的依賴,調(diào)用 handleModuleCreate ,控制流回到第一步
  7. 所有依賴都解析完畢后,構(gòu)建階段結(jié)束

這個(gè)過程中數(shù)據(jù)流 module => ast => dependences => module ,先轉(zhuǎn) AST 再從 AST 找依賴。這就要求 loaders 處理完的最后結(jié)果必須是可以被 acorn 處理的標(biāo)準(zhǔn) JavaScript 語法,比如說對(duì)于圖片,需要從圖像二進(jìn)制轉(zhuǎn)換成類似于 export default "data:image/png;base64,xxx" 這類 base64 格式或者 export default "http://xxx" 這類 url 格式。

compilation 按這個(gè)流程遞歸處理,逐步解析出每個(gè)模塊的內(nèi)容以及 module 依賴關(guān)系,后續(xù)就可以根據(jù)這些內(nèi)容打包輸出。

示例:層級(jí)遞進(jìn)

假如有如下圖所示的文件依賴樹:

其中 index.jsentry 文件,依賴于 a/b 文件;a 依賴于 c/d 文件。初始化編譯環(huán)境之后,EntryPlugin 根據(jù) entry 配置找到 index.js 文件,調(diào)用 compilation.addEntry 函數(shù)觸發(fā)構(gòu)建流程,構(gòu)建完畢后內(nèi)部會(huì)生成這樣的數(shù)據(jù)結(jié)構(gòu):

此時(shí)得到 module[index.js] 的內(nèi)容以及對(duì)應(yīng)的依賴對(duì)象 dependence[a.js] 、dependence[b.js] 。OK,這就得到下一步的線索:a.js、b.js,根據(jù)上面流程圖的邏輯繼續(xù)調(diào)用 module[index.js]handleParseResult 函數(shù),繼續(xù)處理 a.js、b.js 文件,遞歸上述流程,進(jìn)一步得到 a、b 模塊:

從 a.js 模塊中又解析到 c.js/d.js 依賴,于是再再繼續(xù)調(diào)用 module[a.js]handleParseResult ,再再遞歸上述流程:

到這里解析完所有模塊后,發(fā)現(xiàn)沒有更多新的依賴,就可以繼續(xù)推進(jìn),進(jìn)入下一步。

總結(jié)

回顧章節(jié)開始時(shí)提到的問題:

  • Webpack 編譯過程會(huì)將源碼解析為 AST 嗎?webpack 與 babel 分別實(shí)現(xiàn)了什么?
    • 構(gòu)建階段會(huì)讀取源碼,解析為 AST 集合。
    • Webpack 讀出 AST 之后僅遍歷 AST 集合;babel 則對(duì)源碼做等價(jià)轉(zhuǎn)換
  • Webpack 編譯過程中,如何識(shí)別資源對(duì)其他資源的依賴?
    • Webpack 遍歷 AST 集合過程中,識(shí)別 require/ import 之類的導(dǎo)入語句,確定模塊對(duì)其他資源的依賴關(guān)系
  • 相對(duì)于 grant、gulp 等流式構(gòu)建工具,為什么 webpack 會(huì)被認(rèn)為是新一代的構(gòu)建工具?
    • Grant、Gulp 僅執(zhí)行開發(fā)者預(yù)定義的任務(wù)流;而 webpack 則深入處理資源的內(nèi)容,功能上更強(qiáng)大

生成階段

基本流程

構(gòu)建階段圍繞 module 展開,生成階段則圍繞 chunks 展開。經(jīng)過構(gòu)建階段之后,webpack 得到足夠的模塊內(nèi)容與模塊關(guān)系信息,接下來開始生成最終資源了。代碼層面,就是開始執(zhí)行 compilation.seal 函數(shù):

// 取自 webpack/lib/compiler.js 
compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      // ...
      const compilation = this.newCompilation(params);
      this.hooks.make.callAsync(compilation, err => {
        // ...
        this.hooks.finishMake.callAsync(compilation, err => {
          // ...
          process.nextTick(() => {
            compilation.finish(err => {
              **compilation.seal**(err => {...});
            });
          });
        });
      });
    });
  }

seal 原意密封、上鎖,我個(gè)人理解在 webpack 語境下接近于 “將模塊裝進(jìn)蜜罐”seal 函數(shù)主要完成從 modulechunks 的轉(zhuǎn)化,核心流程:

簡單梳理一下:

  1. 構(gòu)建本次編譯的 ChunkGraph 對(duì)象;
  2. 遍歷 compilation.modules 集合,將 moduleentry/動(dòng)態(tài)引入 的規(guī)則分配給不同的 Chunk 對(duì)象;
  3. compilation.modules 集合遍歷完畢后,得到完整的 chunks 集合對(duì)象,調(diào)用 createXxxAssets 方法
  4. createXxxAssets 遍歷 module/chunk ,調(diào)用 compilation.emitAssets 方法將資 assets 信息記錄到 compilation.assets 對(duì)象中
  5. 觸發(fā) seal 回調(diào),控制流回到 compiler 對(duì)象

這一步的關(guān)鍵邏輯是將 module 按規(guī)則組織成 chunks ,webpack 內(nèi)置的 chunk 封裝規(guī)則比較簡單:

  • entry 及 entry 觸達(dá)到的模塊,組合成一個(gè) chunk
  • 使用動(dòng)態(tài)引入語句引入的模塊,各自組合成一個(gè) chunk

chunk 是輸出的基本單位,默認(rèn)情況下這些 chunks 與最終輸出的資源一一對(duì)應(yīng),那按上面的規(guī)則大致上可以推導(dǎo)出一個(gè) entry 會(huì)對(duì)應(yīng)打包出一個(gè)資源,而通過動(dòng)態(tài)引入語句引入的模塊,也對(duì)應(yīng)會(huì)打包出相應(yīng)的資源,我們來看個(gè)示例。

示例:多入口打包

假如有這樣的配置:

const path = require("path");

module.exports = {
  mode: "development",
  context: path.join(__dirname),
  entry: {
    a: "./src/index-a.js",
    b: "./src/index-b.js",
  },
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "./dist"),
  },
  devtool: false,
  target: "web",
  plugins: [],
};

實(shí)例配置中有兩個(gè)入口,對(duì)應(yīng)的文件結(jié)構(gòu):

index-a 依賴于c,且動(dòng)態(tài)引入了 e;index-b 依賴于 c/d 。根據(jù)上面說的規(guī)則:

  • entry 及entry觸達(dá)到的模塊,組合成一個(gè) chunk
  • 使用動(dòng)態(tài)引入語句引入的模塊,各自組合成一個(gè) chunk

生成的 chunks 結(jié)構(gòu)為:

也就是根據(jù)依賴關(guān)系,chunk[a] 包含了 index-a/c 兩個(gè)模塊;chunk[b] 包含了 c/index-b/d 三個(gè)模塊;chunk[e-hash] 為動(dòng)態(tài)引入 e 對(duì)應(yīng)的 chunk。

不知道大家注意到?jīng)]有,chunk[a]chunk[b] 同時(shí)包含了 c,這個(gè)問題放到具體業(yè)務(wù)場(chǎng)景可能就是,一個(gè)多頁面應(yīng)用,所有頁面都依賴于相同的基礎(chǔ)庫,那么這些所有頁面對(duì)應(yīng)的 entry 都會(huì)包含有基礎(chǔ)庫代碼,這豈不浪費(fèi)?為了解決這個(gè)問題,webpack 提供了一些插件如 CommonsChunkPlugin 、SplitChunksPlugin,在基本規(guī)則之外進(jìn)一步優(yōu)化 chunks 結(jié)構(gòu)。

SplitChunksPlugin 的作用

SplitChunksPlugin 是 webpack 架構(gòu)高擴(kuò)展的一個(gè)絕好的示例,我們上面說了 webpack 主流程里面是按 entry / 動(dòng)態(tài)引入 兩種情況組織 chunks 的,這必然會(huì)引發(fā)一些不必要的重復(fù)打包,webpack 通過插件的形式解決這個(gè)問題。

回顧 compilation.seal 函數(shù)的代碼,大致上可以梳理成這么4個(gè)步驟:

  1. 遍歷 compilation.modules ,記錄下模塊與 chunk 關(guān)系
  2. 觸發(fā)各種模塊優(yōu)化鉤子,這一步優(yōu)化的主要是模塊依賴關(guān)系
  3. 遍歷 module 構(gòu)建 chunk 集合
  4. 觸發(fā)各種優(yōu)化鉤子
image (3).png

上面 1-3 都是預(yù)處理 + chunks 默認(rèn)規(guī)則的實(shí)現(xiàn),不在我們討論范圍,這里重點(diǎn)關(guān)注第4個(gè)步驟觸發(fā)的 optimizeChunks 鉤子,這個(gè)時(shí)候已經(jīng)跑完主流程的邏輯,得到 chunks 集合,SplitChunksPlugin 正是使用這個(gè)鉤子,分析 chunks 集合的內(nèi)容,按配置規(guī)則增加一些通用的 chunk :

module.exports = class SplitChunksPlugin {
  constructor(options = {}) {
    // ...
  }

  _getCacheGroup(cacheGroupSource) {
    // ...
  }

  apply(compiler) {
    // ...
    compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => {
      // ...
      compilation.hooks.optimizeChunks.tap(
        {
          name"SplitChunksPlugin",
          stage: STAGE_ADVANCED,
        },
        (chunks) => {
          // ...
        }
      );
    });
  }
};

理解了嗎?webpack 插件架構(gòu)的高擴(kuò)展性,使得整個(gè)編譯的主流程是可以固化下來的,分支邏輯和細(xì)節(jié)需求“外包”出去由第三方實(shí)現(xiàn),這套規(guī)則架設(shè)起了龐大的 webpack 生態(tài),關(guān)于插件架構(gòu)的更多細(xì)節(jié),下面 plugin 部分有詳細(xì)介紹,這里先跳過。

寫入文件系統(tǒng)

經(jīng)過構(gòu)建階段后,compilation 會(huì)獲知資源模塊的內(nèi)容與依賴關(guān)系,也就知道“輸入”是什么;而經(jīng)過 seal 階段處理后, compilation 則獲知資源輸出的圖譜,也就是知道怎么“輸出”:哪些模塊跟那些模塊“綁定”在一起輸出到哪里。seal 后大致的數(shù)據(jù)結(jié)構(gòu):

compilation = {
  // ...
  modules: [
    /* ... */
  ],
  chunks: [
    {
      id: "entry name",
      files: ["output file name"],
      hash"xxx",
      runtime: "xxx",
      entryPoint: {xxx}
      // ...
    },
    // ...
  ],
};

seal 結(jié)束之后,緊接著調(diào)用 compiler.emitAssets 函數(shù),函數(shù)內(nèi)部調(diào)用 compiler.outputFileSystem.writeFile 方法將 assets 集合寫入文件系統(tǒng),實(shí)現(xiàn)邏輯比較曲折,但是與主流程沒有太多關(guān)系,所以這里就不展開講了。

資源形態(tài)流轉(zhuǎn)

OK,上面已經(jīng)把邏輯層面的構(gòu)造主流程梳理完了,這里結(jié)合資源形態(tài)流轉(zhuǎn)的角度重新考察整個(gè)過程,加深理解:

  • compiler.make 階段:
    • entry 文件以 dependence 對(duì)象形式加入 compilation 的依賴列表,dependence 對(duì)象記錄有 entry 的類型、路徑等信息
    • 根據(jù) dependence 調(diào)用對(duì)應(yīng)的工廠函數(shù)創(chuàng)建 module 對(duì)象,之后讀入 module 對(duì)應(yīng)的文件內(nèi)容,調(diào)用 loader-runner 對(duì)內(nèi)容做轉(zhuǎn)化,轉(zhuǎn)化結(jié)果若有其它依賴則繼續(xù)讀入依賴資源,重復(fù)此過程直到所有依賴均被轉(zhuǎn)化為 module
  • compilation.seal 階段:
    • 遍歷 module 集合,根據(jù) entry 配置及引入資源的方式,將 module 分配到不同的 chunk
    • 遍歷 chunk 集合,調(diào)用 compilation.emitAsset 方法標(biāo)記 chunk 的輸出規(guī)則,即轉(zhuǎn)化為 assets 集合
  • compiler.emitAssets 階段:
    • assets 寫入文件系統(tǒng)

Plugin 解析

網(wǎng)上不少資料將 webpack 的插件架構(gòu)歸類為“事件/訂閱”模式,我認(rèn)為這種歸納有失偏頗。訂閱模式是一種松耦合架構(gòu),發(fā)布器只是在特定時(shí)機(jī)發(fā)布事件消息,訂閱者并不或者很少與事件直接發(fā)生交互,舉例來說,我們平常在使用 HTML 事件的時(shí)候很多時(shí)候只是在這個(gè)時(shí)機(jī)觸發(fā)業(yè)務(wù)邏輯,很少調(diào)用上下文操作。而 webpack 的鉤子體系是一種強(qiáng)耦合架構(gòu),它在特定時(shí)機(jī)觸發(fā)鉤子時(shí)會(huì)附帶上足夠的上下文信息,插件定義的鉤子回調(diào)中,能也只能與這些上下文背后的數(shù)據(jù)結(jié)構(gòu)、接口交互產(chǎn)生 side effect,進(jìn)而影響到編譯狀態(tài)和后續(xù)流程。

學(xué)習(xí)插件架構(gòu),需要理解三個(gè)關(guān)鍵問題:

  • WHAT: 什么是插件
  • WHEN: 什么時(shí)間點(diǎn)會(huì)有什么鉤子被觸發(fā)
  • HOW: 在鉤子回調(diào)中,如何影響編譯狀態(tài)

What: 什么是插件

從形態(tài)上看,插件通常是一個(gè)帶有 apply 函數(shù)的類:

class SomePlugin {
    apply(compiler) {
    }
}

apply 函數(shù)運(yùn)行時(shí)會(huì)得到參數(shù) compiler ,以此為起點(diǎn)可以調(diào)用 hook 對(duì)象注冊(cè)各種鉤子回調(diào),例如:compiler.hooks.make.tapAsync ,這里面 make 是鉤子名稱,tapAsync 定義了鉤子的調(diào)用方式,webpack 的插件架構(gòu)基于這種模式構(gòu)建而成,插件開發(fā)者可以使用這種模式在鉤子回調(diào)中,插入特定代碼。webpack 各種內(nèi)置對(duì)象都帶有 hooks 屬性,比如 compilation 對(duì)象:

class SomePlugin {
    apply(compiler) {
        compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
            compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{});
        })
    }
}

鉤子的核心邏輯定義在 Tapable 倉庫,內(nèi)部定義了如下類型的鉤子:

const {
        SyncHook,
        SyncBailHook,
        SyncWaterfallHook,
        SyncLoopHook,
        AsyncParallelHook,
        AsyncParallelBailHook,
        AsyncSeriesHook,
        AsyncSeriesBailHook,
        AsyncSeriesWaterfallHook
 } = require("tapable");

不同類型的鉤子根據(jù)其并行度、熔斷方式、同步異步,調(diào)用方式會(huì)略有不同,插件開發(fā)者需要根據(jù)這些的特性,編寫不同的交互邏輯,這部分內(nèi)容也特別多,回頭展開聊聊。

When: 什么時(shí)候會(huì)觸發(fā)鉤子

了解 webpack 插件的基本形態(tài)之后,接下來需要弄清楚一個(gè)問題:webpack 會(huì)在什么時(shí)間節(jié)點(diǎn)觸發(fā)什么鉤子?這一塊我認(rèn)為是知識(shí)量最大的一部分,畢竟源碼里面有237個(gè)鉤子,但官網(wǎng)只介紹了不到100個(gè),且官網(wǎng)對(duì)每個(gè)鉤子的說明都太簡短,就我個(gè)人而言看完并沒有太大收獲,所以有必要展開聊一下這個(gè)話題。先看幾個(gè)例子:

  • compiler.hooks.compilation
    • 時(shí)機(jī):啟動(dòng)編譯創(chuàng)建出 compilation 對(duì)象后觸發(fā)
    • 參數(shù):當(dāng)前編譯的 compilation 對(duì)象
    • 示例:很多插件基于此事件獲取 compilation 實(shí)例
  • compiler.hooks.make
    • 時(shí)機(jī):正式開始編譯時(shí)觸發(fā)
    • 參數(shù):同樣是當(dāng)前編譯的 compilation 對(duì)象
    • 示例:webpack 內(nèi)置的 EntryPlugin 基于此鉤子實(shí)現(xiàn) entry 模塊的初始化
  • compilation.hooks.optimizeChunks
    • 時(shí)機(jī):seal 函數(shù)中,chunk 集合構(gòu)建完畢后觸發(fā)
    • 參數(shù):chunks 集合與 chunkGroups 集合
    • 示例:SplitChunksPlugin 插件基于此鉤子實(shí)現(xiàn) chunk 拆分優(yōu)化
  • compiler.hooks.done
    • 時(shí)機(jī):編譯完成后觸發(fā)
    • 參數(shù):stats 對(duì)象,包含編譯過程中的各類統(tǒng)計(jì)信息
    • 示例:webpack-bundle-analyzer 插件基于此鉤子實(shí)現(xiàn)打包分析

這是我總結(jié)的鉤子的三個(gè)學(xué)習(xí)要素:觸發(fā)時(shí)機(jī)、傳遞參數(shù)、示例代碼。

觸發(fā)時(shí)機(jī)

觸發(fā)時(shí)機(jī)與 webpack 工作過程緊密相關(guān),大體上從啟動(dòng)到結(jié)束,compiler 對(duì)象逐次觸發(fā)如下鉤子:

image.png

compilation 對(duì)象逐次觸發(fā):

image (1).png

所以,理解清楚前面說的 webpack 工作的主流程,基本上就可以捋清楚“什么時(shí)候會(huì)觸發(fā)什么鉤子”。

參數(shù)

傳遞參數(shù)與具體的鉤子強(qiáng)相關(guān),官網(wǎng)對(duì)這方面沒有做出進(jìn)一步解釋,我的做法是直接在源碼里面搜索調(diào)用語句,例如對(duì)于 compilation.hooks.optimizeTree ,可以在 webpack 源碼中搜索 hooks.optimizeTree.call 關(guān)鍵字,就可以找到調(diào)用代碼:

// lib/compilation.js#2297
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
});

結(jié)合代碼所在的上下文,可以判斷出此時(shí)傳遞的是經(jīng)過優(yōu)化的 chunksmodules 集合。

找到示例

Webpack 的鉤子復(fù)雜程度不一,我認(rèn)為最好的學(xué)習(xí)方法還是帶著目的去查詢其他插件中如何使用這些鉤子。例如,在 compilation.seal 函數(shù)內(nèi)部有 optimizeModulesafterOptimizeModules 這一對(duì)看起來很對(duì)偶的鉤子,optimizeModules 從字面上可以理解為用于優(yōu)化已經(jīng)編譯出的 modules ,那 afterOptimizeModules 呢?

從 webpack 源碼中唯一搜索到的用途是 ProgressPlugin ,大體上邏輯如下:

compilation.hooks.afterOptimizeModules.intercept({
  name"ProgressPlugin",
  call() {
    handler(percentage, "sealing", title);
  },
  done() {
    progressReporters.set(compiler, undefined);
    handler(percentage, "sealing", title);
  },
  result() {
    handler(percentage, "sealing", title);
  },
  error() {
    handler(percentage, "sealing", title);
  },
  tap(tap) {
    // p is percentage from 0 to 1
    // args is any number of messages in a hierarchical matter
    progressReporters.set(compilation.compiler, (p, ...args) => {
      handler(percentage, "sealing", title, tap.name, ...args);
    });
    handler(percentage, "sealing", title, tap.name);
  }
});

基本上可以猜測(cè)出,afterOptimizeModules 的設(shè)計(jì)初衷就是用于通知優(yōu)化行為的結(jié)束。

apply 雖然是一個(gè)函數(shù),但是從設(shè)計(jì)上就只有輸入,webpack 不 care 輸出,所以在插件中只能通過調(diào)用類型實(shí)體的各種方法來或者更改實(shí)體的配置信息,變更編譯行為。例如:

  • compilation.addModule :添加模塊,可以在原有的 module 構(gòu)建規(guī)則之外,添加自定義模塊
  • compilation.emitAsset:直譯是“提交資產(chǎn)”,功能可以理解將內(nèi)容寫入到特定路徑

到這里,插件的工作機(jī)理和寫法已經(jīng)有一個(gè)很粗淺的介紹了,回頭單拎出來細(xì)講吧。

How: 如何影響編譯狀態(tài)

解決上述兩個(gè)問題之后,我們就能理解“如何將特定邏輯插入 webpack 編譯過程”,接下來才是重點(diǎn) —— 如何影響編譯狀態(tài)?強(qiáng)調(diào)一下,webpack 的插件體系與平常所見的 訂閱/發(fā)布 模式差別很大,是一種非常強(qiáng)耦合的設(shè)計(jì),hooks 回調(diào)由 webpack 決定何時(shí),以何種方式執(zhí)行;而在 hooks 回調(diào)內(nèi)部可以通過修改狀態(tài)、調(diào)用上下文 api 等方式對(duì) webpack 產(chǎn)生 side effect

比如,EntryPlugin 插件:

class EntryPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "EntryPlugin",
      (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
          EntryDependency,
          normalModuleFactory
        );
      }
    );

    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      const { entry, options, context } = this;

      const dep = EntryPlugin.createDependency(entry, options);
      compilation.addEntry(context, dep, options, (err) => {
        callback(err);
      });
    });
  }
}

上述代碼片段調(diào)用了兩個(gè)影響 compilation 對(duì)象狀態(tài)的接口:

  • compilation.dependencyFactories.set
  • compilation.addEntry

操作的具體含義可以先忽略,這里要理解的重點(diǎn)是,webpack 會(huì)將上下文信息以參數(shù)或 this (compiler 對(duì)象) 形式傳遞給鉤子回調(diào),在回調(diào)中可以調(diào)用上下文對(duì)象的方法或者直接修改上下文對(duì)象屬性的方式,對(duì)原定的流程產(chǎn)生 side effect。所以想純熟地編寫插件,除了要理解調(diào)用時(shí)機(jī),還需要了解我們可以用哪一些api,例如:

  • compilation.addModule:添加模塊,可以在原有的 module 構(gòu)建規(guī)則之外,添加自定義模塊
  • compilation.emitAsset:直譯是“提交資產(chǎn)”,功能可以理解將內(nèi)容寫入到特定路徑
  • compilation.addEntry:添加入口,功能上與直接定義 entry 配置相同
  • module.addError:添加編譯錯(cuò)誤信息
  • ...

Loader 介紹

Loader 的作用和實(shí)現(xiàn)比較簡單,容易理解,所以簡單介紹一下就行了?;仡?loader 在編譯流程中的生效的位置:

流程圖中, runLoaders 會(huì)調(diào)用用戶所配置的 loader 集合讀取、轉(zhuǎn)譯資源,此前的內(nèi)容可以千奇百怪,但轉(zhuǎn)譯之后理論上應(yīng)該輸出標(biāo)準(zhǔn) JavaScript 文本或者 AST 對(duì)象,webpack 才能繼續(xù)處理模塊依賴。

理解了這個(gè)基本邏輯之后,loader 的職責(zé)就比較清晰了,不外乎是將內(nèi)容 A 轉(zhuǎn)化為內(nèi)容 B,但是在具體用法層面還挺多講究的,有 pitch、pre、post、inline 等概念用于應(yīng)對(duì)各種場(chǎng)景。

為了幫助理解,這里補(bǔ)充一個(gè)示例:Webpack 案例 -- vue-loader 原理分析。

附錄

源碼閱讀技巧

  • 避重就輕: 挑軟柿子捏,比如初始化過程雖然繞,但是相對(duì)來說是概念最少、邏輯最清晰的,那從這里入手摸清整個(gè)工作過程,可以習(xí)得 webpack 的一些通用套路,例如鉤子的設(shè)計(jì)與作用、編碼規(guī)則、命名習(xí)慣、內(nèi)置插件的加載邏輯等,相當(dāng)于先入了個(gè)門
  • 學(xué)會(huì)調(diào)試: 多用 ndb 單點(diǎn)調(diào)試功能追蹤程序的運(yùn)行,雖然 node 的調(diào)試有很多種方法,但是我個(gè)人更推薦 ndb ,靈活、簡單,配合 debugger 語句是大殺器
  • 理解架構(gòu): 某種程度上可以將 webpack 架構(gòu)簡化為 compiler + compilation + plugins ,webpack 運(yùn)行過程中只會(huì)有一個(gè) compiler ;而每次編譯 —— 包括調(diào)用 compiler.run 函數(shù)或者 watch = true 時(shí)文件發(fā)生變更,都會(huì)創(chuàng)建一個(gè) compilation 對(duì)象。理解這三個(gè)核心對(duì)象的設(shè)計(jì)、職責(zé)、協(xié)作,差不多就能理解 webpack 的核心邏輯了
  • 抓大放?。?/strong> plugin 的關(guān)鍵是“鉤子”,我建議戰(zhàn)略上重視,戰(zhàn)術(shù)上忽視!鉤子畢竟是 webpack 的關(guān)鍵概念,是整個(gè)插件機(jī)制的根基,學(xué)習(xí) webpack 根本不可能繞過鉤子,但是相應(yīng)的邏輯跳轉(zhuǎn)實(shí)在太繞太不直觀了,看代碼的時(shí)候一直揪著這個(gè)點(diǎn)的話,復(fù)雜性會(huì)劇增,我的經(jīng)驗(yàn)是:
    • 認(rèn)真看一下 tapable 倉庫的文檔,或者粗略看一下 tapable 的源碼,理解同步鉤子、異步鉤子、promise 鉤子、串行鉤子、并行鉤子等概念,對(duì) tapable 提供的事件模型有一個(gè)較為精細(xì)的認(rèn)知,這叫戰(zhàn)略上重視
    • 遇到不懂的鉤子別慌,我的經(jīng)驗(yàn)我連這個(gè)類都不清楚干啥的,要去理解這些鉤子實(shí)在太難了,不如先略過鉤子本身的含義,去看那些插件用到了它,然后到插件哪里去加 debugger 語句單點(diǎn)調(diào)試,等你縷清后續(xù)邏輯的時(shí)候,大概率你也知道鉤子的含義了,這叫戰(zhàn)術(shù)上忽視
  • 保持好奇心: 學(xué)習(xí)過程保持旺盛的好奇心和韌性,善于 & 敢于提出問題,然后基于源碼和社區(qū)資料去總結(jié)出自己的答案,問題可能會(huì)很多,比如:
    • loader 為什么要設(shè)計(jì) pre、pitch、post、inline?
    • compilation.seal 函數(shù)內(nèi)部設(shè)計(jì)了很多優(yōu)化型的鉤子,為什么需要區(qū)分的這么細(xì)?webpack 設(shè)計(jì)者對(duì)不同鉤子有什么預(yù)期?
    • 為什么需要那么多 module 子類?這些子類分別在什么時(shí)候被使用?

ModuleModule 子類

從上文可以看出,webpack 構(gòu)建階段的核心流程基本上都圍繞著 module 展開,相信接觸過、用過 Webpack 的讀者對(duì) module 應(yīng)該已經(jīng)有一個(gè)感性認(rèn)知,但是實(shí)現(xiàn)上 module 的邏輯是非常復(fù)雜繁重的。

[email protected] 為例,直接或間接繼承自 Module (webpack/lib/Module.js 文件) 的子類有54個(gè):

module 體系.png

要一個(gè)一個(gè)捋清楚這些類的作用實(shí)在太累了,我們需要抓住本質(zhì):module 的作用是什么?

module 是 webpack 資源處理的基本單位,可以認(rèn)為 webpack 對(duì)資源的路徑解析、讀入、轉(zhuǎn)譯、分析、打包輸出,所有操作都是圍繞著 module 展開的。有很多文章會(huì)說 module = 文件, 其實(shí)這種說法并不準(zhǔn)確,比如子類 AsyncModuleRuntimeModule 就只是一段內(nèi)置的代碼,是一種資源而不能簡單等價(jià)于實(shí)際文件。

Webpack 擴(kuò)展性很強(qiáng),包括模塊的處理邏輯上,比如說入口文件是一個(gè)普通的 js,此時(shí)首先創(chuàng)建 NormalModule 對(duì)象,在解析 AST 時(shí)發(fā)現(xiàn)這個(gè)文件里還包含了異步加載語句,例如 requere.ensure ,那么相應(yīng)地會(huì)創(chuàng)建 AsyncModuleRuntimeModule 模塊,注入異步加載的模板代碼。上面類圖的 54 個(gè) module 子類都是為適配各種場(chǎng)景設(shè)計(jì)的。

點(diǎn)贊和在看就是最大的支持??

瀏覽 99
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 婷婷狠狠| 久久精品福利视频| 日韩毛片在线播放| 国产肏屄| 国精产品一区一区三区有限公司杨| 污污污污污www网站免费民国| 一级av| 怡红院男人的天堂| 国产三级午夜理伦三级| 乌克兰性爱视频| 91av一区二区三区| av无码免费在线观看| 高潮喷水AⅤ| 一级免费毛片| 91人妻中文字幕在线精品| 在线观看视频91| 黄色免费在线观看| 欧美成人精品网站| 黄色一级视频在线观看| 在线观看中文字幕无码| 99视频在线免费| 国产欧美二区综合中文字幕精品一 | 97爱| 久草福利在线观看| 亚洲人妻少妇| 久久AV影院| 免费在线观看黄色片| 18禁污网站| 国产精品成人在线视频| 91婷婷在线| 亚洲砖区| 日本久久精品| 免费一级婬片AAA片毛片A级 | 亚洲无码激情| 色婷婷导航| 毛片视频网站| av天堂中文字幕| 日韩人妻斩| 日啪| 丁香婷婷激情五月| 91爱在线| 国产av三级| 人人综合网| PORNY九色视频9l自拍| 北条麻妃无码av| 中文字幕高清无码在线播放| 成人A电影| 日本天堂Tv视频在线观看| 中文字幕视频一区| 大香蕉伊人视频| 操逼网页| 亚洲一区中文字幕| 青草91| 欧美日韩日逼| 婷婷五月天成人电影| 伊人大香蕉电影| 日日操夜夜爽| 又大又粗又爽| 99久久99久久久精品棕色圆| 东北老女人操逼| 黄色免费在线网站| 亚洲超碰在线观看| 在线免费看黄视频| 国产av网站大全| 蜜桃久久av一区| 99精品视频国产| 91亚洲国产成人久久精品网站| 91av一区二区三区| 色视频免费观看| 日韩无码AV中文字幕| www.91在线看| 精品无码AV一区二区三区| 91丨精品丨国产丨丝袜| 黄色草莓视频| 亚洲无码理论片| 国产日韩91| 艹逼中文字幕| 亚洲色激情| 精品国产一级A片黄毛网站| 在线91网站| 久久高清亚洲| 丁香五月激情啪啪| 天天舔九色婷婷| AV无码一区| 黄色国产视频在线观看| 日本中文字幕视频| 大香蕉伊人色| 国产成人主播| 婷婷综合一区| 欧美亚洲成人电影| 探花在线综合| 国产8区| 影音先锋av资源网站| 国产日产亚洲精品| 亚洲欧美国产日韩字幕| 精品久久久久久久久久久| 国产成人无码一区二区| 中文字幕免费高清在线观看| 成人黄片18| jizzjizzjizzjizz| 91牛| 亚洲天堂在线播放| 无码人妻日本| 西西4444WWW无视频| 国产精品视频一区二区三区在线观看 | 午色婷婷国产无码| 日韩性AV| 色99网站| 欧美日韩a片| 五月激情婷婷网| 在线观看一区二区三区四区| 日韩在线你懂的| 中文不卡在线| 国产男女AV| 国产操美女| 麻豆视频国产| 婷婷五月天综合网| 欧美丰满人妻免费视频人| 超碰在线99| 亚洲日本黄色网址| 影音先锋无码AV| 99久操| 囯产精品久久| 国产免费av网站| 在线99热| 狠狠色婷婷777| 国产精品色视频| 四川女人毛多水多A片| 一级aa片| 综合伊人| 无码成人视频| 久草大香蕉在线视频| 一本色道无码人妻精品| 天天综合天天| 肏屄视频在线播放| 一级无码视频| 亚洲三级片视频| 激情五月天成人| 亚洲AV无码乱码| 操美女影院| 欧美色图亚洲另类| 国产黄片在线播放| 国产乱伦一区| 高清无码视频观看| 亚洲天堂色| 一区二区三区电影高清电影免费观看 | 精品国产国产没封| 中文字幕成人| 亚洲国产av电影| 99精品999| 无码人妻少妇| 国产成人性爱| 婷婷视频在线观看| 日本三级视频| 日韩中字无码黄片| 婷婷五月天免费视频| 免费人成在线观看视频播放| 久久久精品欧美| 中文字幕不卡无码| 久久五月天婷婷| 51无码| 黄色片免费视频网站| 亚欧美日韩| 免费在线观看无码| 天天色色色| 丁香成人五月天| 口爆在线| 免费无码| 中文资源在线观看| 九九午夜| 日韩熟妇无码中文字幕| 免费a在线| 国产精品秘入口18禁网站| 中国老少配BBwBBwBBW| 少妇中文字幕| 午夜精品视频在线观看| 亚洲,制服,综合,中文| 黄色福利| 91香蕉国产在线观看| 色婷婷狠| 亚洲一区欧美| 男人天堂无码| 国内精品一区二区| 亚洲一本色道中文无码| 国产三级片91| 男女啪啪免费视频| 免费无码一区二区三区| 久操视频在线观看| 欧美日韩国产三级| 国产精品一色哟哟哟| 亚洲无码观看视频| 天天干天天添| 黄片91| 欧美在线亚洲| 九九精品视频在线播放| 国产91无码精品秘入口新欢| 成人a级网站| 久久蜜桃成人| www.日批| 先锋影音资源av| 免费黄色网址啊不卡| 伊人大香蕉在线网| 国产乱子伦一区二区三区在线观看 | 2025av天堂网| 亚洲激情网| 国产无码一| 年轻女教师高潮2| 成人亚洲AV| 亚洲有码在线观看| 成人午夜精品福利免费| 美女被操面费网站| 欧美综合第一页| 免费黄色一级片| 精品国产乱码一区二区| 人人射人人射| 国产中文在线观看| 黄色三级视频在线观看| 国产一区在线看| 欧美日韩无码视频| 成人性爱在线播放| 国产肏逼视频| 久久久久久久人妻丝袜| 天天日天天干天天爽| 北条麻妃一区二区三区在线播放| 日韩熟女视频| 欧美A片视频| 欧美大香蕉伊人网| 超碰黄片| 视频在线一区| 三级片欧美| 国产无码一| 日韩无码精品一区| 黄色电影一级片| 亚洲激情婷婷| 2014亚洲天堂| A级视频免费观看| 日本无码成人| 婷婷中文字幕| 婷婷色色五月天图片| 欧美一级特黄真人做受| a片视频网站| 熟妇自拍| 国产精品久久久久久久久久两年半 | 国产久久精品视频| 一本到免费视频| 操欧美老女人| 天天爽夜夜爽AA片免费| 黄色成人在线观看视频| 少妇搡BBBB搡BBB搡造水爽| 国产剧情一区二区av在线观看| 最近中文字幕无码| 亚洲v欧美| 空姐白洁| 天堂a在线8| 亚洲中文字幕影院| 欧美熟妇精品一二三区| 中文字幕+乱码+中文乱码91在线观看| 国产区AV| gogogo高清在线观看免费直播中国 | www.俺去了| 插进去综合网| 日韩在线观看免| 国产成人视频免费| 操逼视频网| 四虎成人精品永久免费AV九九 | 91成人做爰A片| 秘蜜桃色一区二区三区在线观看| 免费看成人A片无码照片88hⅤ| 操逼视频免费观看| 欧美视频区| 婷婷色在线视频| footjobvk| 欧美色图另类图片| 亚洲无码成人网站| 国产传媒av| 亚洲精品一二三| 黄色三级片网站| 91精品少妇高潮一区二区三区不卡 | 人人色在线观看| 日韩成人无码专区| 强伦轩人妻一区二区三区70后| 三级视频国产| 成人特级毛片全部免费播放| 特特级毛片| 国产无码AV在线| 日本黄色视频在线观看| 在线观看免费a片| www.黄色片| 中文AV第一页| 亚洲欧美成人在线视频| 综合天堂AV久久久久久久| 天堂中文在线a| 无码精品久久| www.天天射视频| 国产日本在线观看| 亚洲高清视频在线| 亚洲欧美在线成人| 亚洲精品二| 国产欧美成人| 天天躁狠狠躁夜躁2024| 午夜成人无码视频| 永久免费AV无码| 黑人人妻黑人ThePorn| 91熟女首页| 成人午夜在线观看| 免费A片视频| 免费的一级片| 国产丨熟女丨国产熟女视频| 天堂网影音先锋| 一级欧美一级日韩片| 久草视频在线资源| 国产成人高清视频| 中文字幕1区| 在线观看一级片| v在线| 成人午夜视频精品一区| 国产做爱| 欧美国产综合在线| 东京热无码一区| 在线观看中文字幕| 日韩免费AV电影| 无码人妻一区二区三区| 五月激情婷婷网| 你懂的在线播放| 人人上人人操| 色色毛片| 日本v片| 超碰97资源| 西西人体大胆ww4444图片| 久久蝌蚪窝| 综合欧美国产视频二区| 国内精品一区二区三区| 亚洲AV无码成人精品区天堂小说| 成人午夜精品无码区| 伊人午夜| 就要干就要操| 亚洲色图欧美| 一区二区三区久久久久| 亚洲无码视频观看| 少妇人妻av| 8050午夜网| 影音先锋无码专区| 高清无码在线观看视频| 婷婷五月综合中文字幕| 3D动漫啪啪精品一区二| 青青草在线观看视频| 精品人妻一区二区| 翔田千里一区二区三区精品播放| 自拍偷拍国产| 爱爱综合| 久久免费精品视频| 亚洲三级无码在线| 波多野结衣一区| 无码导航| 中文在线视频| 五月丁香色播| 黄色成人网站在线观看| 色吧| 黄色视频在线观看亚洲一区二区三区免费 | 91人妻人人人| 人人操免费| 在线免费观看黄色网址| 日韩城人免费| 免费在线观看一区| 五月婷婷丁香五月| 人妻精品无码| 免费黄色小视频| 免费无码在线播放| 色婷婷久久久久swag精品| 白嫩外女BBwBBwBBw| 成人性视频Aⅴ| 亚洲国产三级片| 日本人妻在线播放| 91色婷婷综合久久中文字幕二区| 亚洲在线观看免费| 激情五月天网站| 激情久久AV一区AV二区AV三区| 色色一区| 激情成人五月天| 中文在线最新版天堂8| 91九色91蝌蚪91窝成人| 蜜乳av红桃嫩久久| 欧美老妇操逼| 69国产精品视频免费观看| 免费v片| 国产美女一级真毛片酒店| 欧美亚洲国产精品| 最新中文字幕免费MV第一季歌词| 欧美日韩岛国| 色婷婷大香蕉| 边添小泬边狠狠躁视频| 尤物视频在线播放| 99国产热| 51一区二区三区| 97人妻视频| 久久久久亚洲AV无码专区| 婷婷丁香激情| 国产秘精品区二区三区日本| 黄色视频在线观看网站| 精品久久一区二区三区四区| 中文无码精品欧美日韩| 欧美亚洲日韩国产| 91亚洲一区| 黄网在线看| 男女av免费| 粉嫩一区| www.6969成人片亚洲| 色逼视频| 日本a在线免费观看| 殴殴美日韩在线| 亚洲黄色成人| AV麻豆| 想要xx视频| 九七影院第二页| 手机在线观看av| 大香蕉综合伊人| 乱伦性爱视频| 中文字幕亚洲无码视频| 亚洲成人中文字幕在线| 中文字幕第6页| 91久久无码一区人妻A片蜜桃 | 奇米色色| 日本少妇做爱| 亚洲三级国产| 双飞人妻13p| 免费观看高清无码| 西西www444无码免费视频| 狠狠干大香蕉| 天天爽夜夜爽夜夜爽精品| www.色999| 婷婷综合五月| 69色色| 久久久久久久| 婷婷五月影院| 国产美女免费视频| 国产免费黄色| 国产精品在线观看视频| 天天透天天干| 精品一区二区三区三区| 韩国精品一区二区三区| 91就去干| 精品九九| 乖我硬了让老子cao你小视频| 先锋AV资源网| 亚洲精品91| 99久久99久久| 手机av网站| 福利一区二区视频网| 日韩AV综合| av在线三级| 另类老妇性BBBWBBW| 国产精品爽爽久久久久| 国产精品久久久久久无人区| 日韩视频一二三| 免费婷婷| 久久蜜桃视频| 天堂久久av| 高清无码在线观看视频| 色色A| 2022天天干| 黄片网站免费观看| 色婷婷日韩精品一区二区三区| 成人理论片| 蜜桃精品一区二区三区美女| 一区二区三区成人电影| 亚洲AV无码成人精品区在线欢看| 亚洲AV无码国产精品二区| 成人精品久久久| 欧美日韩性爱网站| 精品人人操| 久久久一区二区| 18禁网站免费| 欧美性猛交XXXXⅩXX| 在线观看三级网址| 久久爱成人| 翔田千里AV| www.毛片| 香蕉操逼小视频| 麻豆mdapp03.tⅴ| 天堂网在线播放| 91麻豆一区二区| 欧美中文字幕在线观看| 亚洲视频成人| 粉嫩av一区二区白浆| 亚洲福利天堂| 一区二区三区在线观看视频| 奇米影视狠狠干| 就爱av| 亚洲va在线∨a天堂va欧美va| 人妻少妇无码| 亚州操逼片| 国产美女在线观看| 国产欧美一区二区三区视频| 国产精品一区二区毛片A片婊下载| 在线观看老湿视频福利| 水果派红桃AV解说| 午夜一级性爱片| 另类罕见稀奇videos| 欧美AⅤ在线| 91精品国产乱码久久| 抽插视频欧美| 无码人妻丰满熟妇| 高清无码三级片在线观看| 亚洲天堂无码av| 在线中文字幕在线观看| 无码免费在线视频| 激情国产视频| 88在线无码精品秘入口九色| 国产福利美女网站| 中文字幕第一页在线| 亚洲中文字幕免费| 免费无码又爽又黄又刺激网站| 色婷婷狠狠操| 亚洲啪啪网站| 激情婷婷五月| 人妻人人澡| 操操av| 97精品超碰一区二区三区| 精品无码一区二区人妻久久蜜桃| 国产无码激情视频| 日韩一卡二卡| 成人伊人电影| 在线中文AV| 2025最新偷拍| 久草福利在线观看| 内射久久| 日韩一级免费电影| 人人射人人射| 老司机在线免费视频| 亚洲色影院| 亚洲精品在线观看视频| 北条麻妃人妻中文字幕91影视| 日本一级黄| 午夜操B| 骚白虎一区| 婷婷99| 91小视频在线观看| 国产TS变态重口人妖| 懂色av懂色av粉嫩av| 3D动漫精选啪啪一期二期三期| 99re国产| 精品国产91| 九九热精| www.a片| 蜜芽视频| 成人午夜小电影| 最新97色黄色精品高清网站| 亚洲无码免费观看视频| 日韩高清无码不卡| 色色播播| 欧美日韩在线视频免费| 国产亚洲网| av大片在线观看| 天堂VA蜜桃一区二区三区| 日本免费黄色电影| 蜜桃av秘无码一区二区三欧| 三级片高清无码| 18啪啪网站| 91成人电影院| 黄片免费无码| 婷婷色综合视频二区| 久久av一区二区三区| 极品久久久| 欧美成人毛片一级A片| 国内不卡一卡二视频| 99热99re6国产线播放| 作爱免费视频| 无码一区二区三区四季| 中文字幕A片无码免费看| 成年人黄色片| 日本成人三级片| 精品国产午夜福利在线观看| V天堂| 色老板综合| 国产女人18毛片水18精品软件| 午夜成人福利视频在线观看| 操操操操操操| 无码av在线播放| 国产成人无码免费看片| 日本熟妇高潮BBwBBwBBw| www.91AV| eeuss在线| 四虎亚洲无码| 久久亚洲影视| 黄片91| 青娱乐在线成人| 亚洲高清无码免费| 安徽妇女BBBWBBBwm| 翔田千里AV在线| av资源免费观看| 五月天婷婷激情网| 乱伦激情| 午夜特片| 豆花视频免费| 久久久久一区二区三区| 51午夜福利| 人人操人人超碰| 日韩性爱视屏| 人人操人人| 亚洲AV无码乱码精品| 人妻无码免费视频| 成人天堂| 欧美亚洲天堂网| 亚洲无码999| 亚洲日韩欧美一区二区| 男人资源网| 黄片视频在线| 亚洲有码中文字幕| 日韩一| 就爱搞搞| 二区视频在线| 无码高清视频在线观看| 久碰人妻人妻人妻| 一级黄色影院| 能看的黄色视频| 操干视频| 亚洲永久在线| 91最新在线播放| 无码免费视频| 久久精品网| 99精品视频免费观看| 黄色成人18| 日本A片在线观看| 成人三级在线观看| 亚洲免费网| 精品欧美一区二区精品久久| 欧美一区二区三曲的| 超碰97资源| 国产棈品久久久久久久久久九秃| 国产毛片在线视频| www.俺去了| 波多野结衣日韩| 国产91精品久久久天天| 免费A片在线播放| 在线国产91| 69欧美| 亚洲欧美成人网站| 青青草原网站在线观看| 自拍偷拍| 欧美艹逼视频| 日韩性爱A片| 五月天综合视频| 久操免费观看| 九九热视频在线观看| 大地影视中文第三页最新在线观看| 国产操逼大片| 在线免费看黄视频| 天堂网在线播放| 欧美日韩国产性爱| 乱子伦毛片国产| 欧美色视频一区二区三区在线观看| 五月天婷婷激情视频| 一区二区三区观看| 北京熟妇搡BBBB搡BBBB| 日韩一级片免费观看| 无码黄漫| A片动漫| 淫色五月| 亚洲成人观看| 天天肏天天干| 欧美成人三级在线观看| 亚洲精品成人在线| 婷婷视频在线观看| 91插逼| 亚洲理论片| 夜夜操夜夜爽| 精品视频一区二区三区| 欧美在线视频99| 免费av在线| 久操视频一区二区三区| 一级a片激情啪啪免费观| 青青操国产乱伦| 久草网大香蕉| 麻豆疯狂做受XXXX高潮视频| 国产91小视频| 操逼操逼操逼操逼操逼操逼| 欧美日韩国产成人在线观看| 先锋AV资源在线| 天天草天天草| 国产激情在线观看视频| 欧美区亚洲区| A片网站在线观看| 欧美嗯啊| 伊人中文在线| 欧美熟妇精品黑人巨大一二三区 | 三级片无码视频| 日韩免费高清视频| www国产亚洲精品久久网站| 久热精品视频| 中文字幕av久久爽一区| 91在线无码精品国产三年| 日本亚洲欧洲免费| 超碰997| 伊人久久大香蕉国产| 99热激情在线| 婷婷伊人中文字幕| 国产成人高清视频| 亚洲电影无码| 久久成人网站| 丁香五月AV| 男女激情网站| 操逼逼视频| 伊人春色网| 粉嫩小泬BBBB免费看| AV777777| 日韩免费高清无码视频| 国产午夜激情视频| 久久黄色小视频| 2025AV天堂网| 国产h视频在线观看| 国产精品色哟哟| 亚洲免费视频在线观看| 91人妻无码一区二区久久| 北条麻妃精品视频| www.色色网| 91九色TS另类国产人妖| 偷拍亚洲天堂| 操骚屄视频| 免费亲子乱婬一级A片| 自拍超碰| 小草久久95| 欧美一级特黄AAAAAA片在线视频| 夜夜夜夜撸| 老司机午夜视频| 少妇婷婷| 夜夜撸网站| 嫩草国产| 欧美日韩国产在线观看| a亚洲天堂| 91精品国产综合久久久蜜臀图片| 国产AV电影网| 日韩图色| 久久久久久久久国产精品| 亚洲精品乱码久久久久久蜜桃91| 日韩一区二区三区在线| 影音先锋一区| 444444免费高清在线观看电视剧的注意 | 夜夜夜撸| 欧美日韩国产高清| 91人妻人人澡人人精品| 美日韩一区二区| 91精品国产综合久久久蜜臀粉嫩| A级毛片视频| 欧美亚洲成人视频| AV免费在线播放| 日韩中文字幕无码| 狼人亚洲伊人| 噜噜色av| 男女成人视频| 精品人伦一区二区三区| 无码人妻中文字幕| 丰满欧美熟妇免费视频| 91中文字幕| 豆花视频成人网站入口| 手机av网站| 99热国产在线观看| 一区二区三区黄色| 免费国产黄色| 久久肏屄视频| 国产性播放| 九九人妻| 有码一区二区三区| 97国产在线视频| 99九九网| 二区三区无码| 国产c区| 无码国产一区二区三区四区五区| 九九成人视频| 亚洲天堂在线播放| 嫩BBB搡BBBB搡BBBB| BBw日本熟妇BBwHD| 婷婷三级片| 黑人久久| 欧美夜夜草视频| 亚洲三级无码在线观看| 黄色免费无码| 能看的AV网站| 爽好紧别夹喷水网站| 亚洲人内射片又| 悠悠色导航| 久久精品国产亚洲AV成人婷婷| 黄片免费看视频| 天天爽日日澡AAAA片| 极品少妇视频| 久久久久久久久久久久高清毛片一级 | 一级大黄色毛片| 二级黄色毛片| 一区在线免费观看| 哪里可以看毛片| 欧美视频在线观看| 色臀av| 精品av在线观看| 无码区一区二区三区| 91乱子伦国产乱子伦!| 日韩午夜欧美精品一二三区| 丁香婷婷在线| 成人久久久久一级大黄毛片中国| 免费高清无码在线观看| 久久肥妞操| 国产无码电影网| www.豆花视频成人版| 午夜黄色影院| 日韩精品| A∨无码| 日韩无码高清一区| 国产视频99| 在线观看免费完整版中文字幕视频| 日韩无码A级片| BBB搡BBB搡BBB搡BBB| 国产乱国产乱300精品| 不迷路福利视频| 欧美在线视频你懂的| 国产欧美在线看| 91看片| 激情小视频国产在线播放| 人人妻人人躁人人DVD| 美女自慰网站免费| 亚洲高清视频在线| 西西www444无码免费视频| 国产成人主播| www.黄色片| 天天色播| 五月丁香成人电影| 波多野结衣无码AV| 加勒比色综合| 黑丝一区| 91看片看婬黄大片Videos| 99免费在线观看视频| 四虎永久www成人影院| 欧美日韩岛国| 俺来射| 免费看黃色AAAAAA片| 亚洲最新视频| 黄色视频免费网站| 久久精品成人| 成人免费A片| 少妇高潮一区二区三区99| 成人v| 东京热高清无码| 伊人av网| 国产成人午夜精品无码区久久麻豆| 怡春院成人| 狠狠躁日日躁夜夜躁A片视频| 国产一区二区不卡亚洲涩情 | 青青视频网| 欧美老妇另类老屁XXX| 91精品国产乱码久久久| 日本免费在线视频| 成人性爱视频在线观看| 一起操逼| 69久久久久| 丁香成人五月天| 久久精品视频观看| 国产c区| 特级毛片www| 日本一区二区三区免费看| 成人免费黄色网| 人妻一区| 亚洲第一狼人综合网| 色先锋资源站| 欧美大香蕉在线视频| 不卡无码中文字幕一区| 日本免费版网站nba| 日本親子亂子倫XXXX50路| 无码a区天堂| 日本国产在线视频| 18毛片| 91蝌蚪久久| 免费观看一级毛一片| 欧美日韩一级视频| 青青青青操| 黄色成人视频免费看| 国产免费AV片| 在线观看中文字幕视频| 欧美日韩色图| 中文字幕av在线观看| 91伊人久热精品| 老太色HD色老太HD| 波多野结衣网站| 影音先锋AV资源在线| 成人视频无码| 日本黄A级A片国产免费| 国产成人av在线观看| 精品人无码一区二区三区下载| 男人操女人网站| 91成人在线播放| 99操| 国产av小电影| 色欲亚洲| 黄色视频在线免费观| 久久AV网站| 日韩精品一区二区三区使用方法| 欧美成人免费精品| 人人草人人澡| 欧美三级在线| 免费人成视频在线| 亚洲sese| 亚洲无码伊人| 男人天堂视频在线观看| 一线天嫩穴少妇|