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

Webpack 5 核心打包原理全流程解析,看這一篇就夠了

共 35568字,需瀏覽 72分鐘

 ·

2021-12-05 13:12

e3f21980a308d15ced0c707908b8617e.webp

寫在前邊

Webpack在前端前端構(gòu)建工具中可以堪稱中流砥柱般的存在,日常業(yè)務(wù)開發(fā)、前端基建工具、高級(jí)前端面試...任何場景都會(huì)出現(xiàn)它的身影。

也許對(duì)于它的內(nèi)部實(shí)現(xiàn)機(jī)制你也許會(huì)感到疑惑,日常工作中基于Webpack Plugin/Loader之類查閱API仍然不明白各個(gè)參數(shù)的含義和應(yīng)用方式。

其實(shí)這一切原因本質(zhì)上都是基于Webpack工作流沒有一個(gè)清晰的認(rèn)知導(dǎo)致了所謂的“面對(duì)API無從下手”開發(fā)。

文章中我們會(huì)從如何實(shí)現(xiàn)模塊分析項(xiàng)目打包的角度出發(fā),使用最通俗,最簡潔,最明了的代碼帶你揭開Webpack背后的神秘面紗,帶你實(shí)現(xiàn)一個(gè)簡易版Webpack,從此對(duì)于任何webpack相關(guān)底層開發(fā)了然于胸。

這里我們只講「干貨」,用最通俗易懂的代碼帶你走進(jìn)webpack的工作流。

我希望你能掌握的前置知識(shí)
  • Tapable[2]

Tapable[3]包本質(zhì)上是為我們更方面創(chuàng)建自定義事件和觸發(fā)自定義事件的庫,類似于Nodejs中的EventEmitter Api。

Webpack中的插件機(jī)制就是基于Tapable實(shí)現(xiàn)與打包流程解耦,插件的所有形式都是基于Tapable實(shí)現(xiàn)。

  • Webpack Node Api[4]

基于學(xué)習(xí)目的我們會(huì)著重于Webpack Node Api流程去講解,實(shí)際上我們?cè)谇岸巳粘J褂玫?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">npm run build命令也是通過環(huán)境變量調(diào)用bin腳本去調(diào)用Node Api去執(zhí)行編譯打包。

  • Babel[5]

Webpack內(nèi)部的AST分析同樣依賴于Babel進(jìn)行處理,如果你對(duì)Babel不是很熟悉。我建議你可以先去閱讀下這兩篇文章「前端基建」帶你在Babel的世界中暢游[6]、\# 從Tree Shaking來走進(jìn)Babel插件開發(fā)者的世界[7]。

當(dāng)然后續(xù)我也會(huì)去詳解這些內(nèi)容在Webpack中的應(yīng)用,但是我更加希望在閱讀文章之前你可以去點(diǎn)一點(diǎn)上方的文檔稍微了解一下前置知識(shí)。

流程梳理

在開始之前我們先對(duì)于整個(gè)打包流程進(jìn)行一次梳理。

這里僅僅是一個(gè)全流程的梳理,現(xiàn)在你沒有必要非常詳細(xì)的去思考每一個(gè)步驟發(fā)生了什么,我們會(huì)在接下來的步驟中去一步一步帶你串聯(lián)它們。

7662c8e512814b6c207c0655d10a07d3.webpimage.png

整體我們將會(huì)從上邊5個(gè)方面來分析Webpack打包流程:

  1. 初始化參數(shù)階段。

    這一步會(huì)從我們配置的webpack.config.js中讀取到對(duì)應(yīng)的配置參數(shù)和shell命令中傳入的參數(shù)進(jìn)行合并得到最終打包配置參數(shù)。

  2. 開始編譯準(zhǔn)備階段

    這一步我們會(huì)通過調(diào)用webpack()方法返回一個(gè)compiler方法,創(chuàng)建我們的compiler對(duì)象,并且注冊(cè)各個(gè)Webpack Plugin。找到配置入口中的entry代碼,調(diào)用compiler.run()方法進(jìn)行編譯。

  3. 模塊編譯階段

    從入口模塊進(jìn)行分析,調(diào)用匹配文件的loaders對(duì)文件進(jìn)行處理。同時(shí)分析模塊依賴的模塊,遞歸進(jìn)行模塊編譯工作。

  4. 完成編譯階段

    在遞歸完成后,每個(gè)引用模塊通過loaders處理完成同時(shí)得到模塊之間的相互依賴關(guān)系。

  5. 輸出文件階段

    整理模塊依賴關(guān)系,同時(shí)將處理后的文件輸出到ouput的磁盤目錄中。

接下來讓我們?cè)敿?xì)的去探索每一步究竟發(fā)生了什么。

創(chuàng)建目錄

工欲善其事,必先利其器。首先讓我們創(chuàng)建一個(gè)良好的目錄來管理我們需要實(shí)現(xiàn)的Packing tool吧!

讓我們來創(chuàng)建這樣一個(gè)目錄:

51c34adce21cca44631d4d9d32bcadf3.webpimage.png
  • webpack/core存放我們自己將要實(shí)現(xiàn)的webpack核心代碼。
  • webpack/example存放我們將用來打包的實(shí)例項(xiàng)目。
    • webpack/example/webpak.config.js配置文件.
    • webpack/example/src/entry1第一個(gè)入口文件
    • webpack/example/src/entry1第二個(gè)入口文件
    • webpack/example/src/index.js模塊文件
  • webpack/loaders存放我們的自定義loader。
  • webpack/plugins存放我們的自定義plugin。
初始化參數(shù)階段

往往,我們?cè)谌粘J褂秒A段有兩種方式去給webpack傳遞打包參數(shù),讓我們先來看看如何傳遞參數(shù):

Cli命令行傳遞參數(shù)

通常,我們?cè)谑褂谜{(diào)用webpack命令時(shí),有時(shí)會(huì)傳入一定命令行參數(shù),比如:

webpack?--mode=production
#?調(diào)用webpack命令執(zhí)行打包?同時(shí)傳入mode為production
復(fù)制代碼

webpack.config.js傳遞參數(shù)

另一種方式,我相信就更加老生常談了。

我們?cè)陧?xiàng)目根目錄下使用webpack.config.js導(dǎo)出一個(gè)對(duì)象進(jìn)行webpack配置:

const?path?=?require('path')

//?引入loader和plugin?...
module.exports?=?{
??mode:?'development',
??entry:?{
????main:?path.resolve(__dirname,?'./src/entry1.js'),
????second:?path.resolve(__dirname,?'./src/entry2.js'),
??},
??devtool:?false,
??//?基礎(chǔ)目錄,絕對(duì)路徑,用于從配置中解析入口點(diǎn)(entry point)和?加載器(loader)。
??//?換而言之entry和loader的所有相對(duì)路徑都是相對(duì)于這個(gè)路徑而言的
??context:?process.cwd(),
??output:?{
????path:?path.resolve(__dirname,?'./build'),
????filename:?'[name].js',
??},
??plugins:?[new?PluginA(),?new?PluginB()],
??resolve:?{
????extensions:?['.js',?'.ts'],
??},
??module:?{
????rules:?[
??????{
????????test:?/\.js/,
????????use:?[
??????????//?使用自己loader有三種方式?這里僅僅是一種
??????????path.resolve(__dirname,?'../loaders/loader-1.js'),
??????????path.resolve(__dirname,?'../loaders/loader-2.js'),
????????],
??????},
????],
??},
};
復(fù)制代碼

同時(shí)這份配置文件也是我們需要作為實(shí)例項(xiàng)目example下的實(shí)例配置,接下來讓我們修改example/webpack.config.js中的內(nèi)容為上述配置吧。

當(dāng)然這里的loaderplugin目前你可以不用理解,接下來我們會(huì)逐步實(shí)現(xiàn)這些東西并且添加到我們的打包流程中去。

實(shí)現(xiàn)合并參數(shù)階段

這一步,讓我們真正開始動(dòng)手實(shí)現(xiàn)我們的webpack吧!

首先讓我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">webpack/core下新建一個(gè)index.js文件作為核心入口文件。

同時(shí)建立一個(gè)webpack/core下新建一個(gè)webpack.js文件作為webpack()方法的實(shí)現(xiàn)文件。

首先,我們清楚在NodeJs Api中是通過webpack()方法去得到compiler對(duì)象的。

59b56591aac702739181230958a182f2.webpimage.png

此時(shí)讓我們按照原本的webpack接口格式來補(bǔ)充一下index.js中的邏輯:

  • 我們需要一個(gè)webpack方法去執(zhí)行調(diào)用命令。
  • 同時(shí)我們引入webpack.config.js配置文件傳入webpack方法。
//?index.js
const?webpack?=?require('./webpack');
const?config?=?require('../example/webpack.config');
//?步驟1:?初始化參數(shù)?根據(jù)配置文件和shell參數(shù)合成參數(shù)
const?compiler?=?webpack(config);
復(fù)制代碼

嗯,看起來還不錯(cuò)。接下來讓我們?nèi)?shí)現(xiàn)一下webpack.js:

function?webpack(options)?{
??//?合并參數(shù)?得到合并后的參數(shù)?mergeOptions
??const?mergeOptions?=?_mergeOptions(options);
}

//?合并參數(shù)
function?_mergeOptions(options)?{
??const?shellOptions?=?process.argv.slice(2).reduce((option,?argv)?=>?{
????//?argv?->?--mode=production
????const?[key,?value]?=?argv.split('=');
????if?(key?&&?value)?{
??????const?parseKey?=?key.slice(2);
??????option[parseKey]?=?value;
????}
????return?option;
??},?{});
??return?{?...options,?...shellOptions?};
}

module.export?=?webpack;
復(fù)制代碼

這里我們需要額外說明的是

webpack文件中需要導(dǎo)出一個(gè)名為webpack的方法,同時(shí)接受外部傳入的配置對(duì)象。這個(gè)是我們?cè)谏鲜鲋v述過的。

當(dāng)然關(guān)于我們合并參數(shù)的邏輯,是將外部傳入的對(duì)象和執(zhí)行shell時(shí)的傳入?yún)?shù)進(jìn)行最終合并。

Node Js中我們可以通過process.argv.slice(2)來獲得shell命令中傳入的參數(shù),比如:

0820dab0d9962e57c4484327506c39c6.webpimage.png

當(dāng)然_mergeOptions方法就是一個(gè)簡單的合并配置參數(shù)的方法,相信對(duì)于大家來說就是小菜一碟。

恭喜大家??,千里之行始于足下。這一步我們已經(jīng)完成了打包流程中的第一步:合并配置參數(shù)。

編譯階段

在得到最終的配置參數(shù)之后,我們需要在webpack()函數(shù)中做以下幾件事情:

  • 通過參數(shù)創(chuàng)建compiler對(duì)象。我們看到官方案例中通過調(diào)用webpack(options)方法返回的是一個(gè)compiler對(duì)象。并且同時(shí)調(diào)用compiler.run()方法啟動(dòng)的代碼進(jìn)行打包。

  • 注冊(cè)我們定義的webpack plugin插件。

  • 根據(jù)傳入的配置對(duì)象尋找對(duì)應(yīng)的打包入口文件。

創(chuàng)建compiler對(duì)象

讓我們先來完成index.js中的邏輯代碼補(bǔ)全:

//?index.js
const?webpack?=?require('./webpack');
const?config?=?require('../example/webpack.config');
//?步驟1:?初始化參數(shù)?根據(jù)配置文件和shell參數(shù)合成參數(shù)
//?步驟2:?調(diào)用Webpack(options)?初始化compiler對(duì)象??
//?webpack()方法會(huì)返回一個(gè)compiler對(duì)象

const?compiler?=?webpack(config);

//?調(diào)用run方法進(jìn)行打包
compiler.run((err,?stats)?=>?{
??if?(err)?{
????console.log(err,?'err');
??}
??//?...
});
復(fù)制代碼

可以看到,核心編譯實(shí)現(xiàn)在于webpack()方法返回的compiler.run()方法上。

一步一步讓我們來完善這個(gè)webpack()方法:

//?webpack.js
function?webpack(options)?{
??//?合并參數(shù)?得到合并后的參數(shù)?mergeOptions
??const?mergeOptions?=?_mergeOptions(options);
??//?創(chuàng)建compiler對(duì)象
??const?compiler?=?new?Compiler(mergeOptions)
??
??return?compiler
}

//?...
復(fù)制代碼

讓我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">webpack/core目錄下同樣新建一個(gè)compiler.js文件,作為compiler的核心實(shí)現(xiàn)文件:

//?compiler.js
//?Compiler類進(jìn)行核心編譯實(shí)現(xiàn)
class?Compiler?{
??constructor(options)?{
????this.options?=?options;
??}

??//?run方法啟動(dòng)編譯?
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{
??}
}

module.exports?=?Compiler
復(fù)制代碼

此時(shí)我們的Compiler類就先搭建一個(gè)基礎(chǔ)的骨架代碼。

目前,我們擁有了:

  • webpack/core/index.js作為打包命令的入口文件,這個(gè)文件引用了我們自己實(shí)現(xiàn)的webpack同時(shí)引用了外部的webpack.config.js(options)。調(diào)用webpack(options).run()開始編譯。

  • webpack/core/webpack.js這個(gè)文件目前處理了參數(shù)的合并以及傳入合并后的參數(shù)new Compiler(mergeOptions),同時(shí)返回創(chuàng)建的Compiler實(shí)力對(duì)象。

  • webpack/core/compiler,此時(shí)我們的compiler僅僅是作為一個(gè)基礎(chǔ)的骨架,存在一個(gè)run()啟動(dòng)方法。

編寫Plugin

還記得我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">webpack.config.js中使用了兩個(gè)plugin---pluginA、pluginB插件嗎。接下來讓我們來依次實(shí)現(xiàn)它們:

在實(shí)現(xiàn)Plugin前,我們需要先來完善一下compiler方法:

const?{?SyncHook?}?=?require('tapable');

class?Compiler?{
??constructor(options)?{
????this.options?=?options;
????//?創(chuàng)建plugin?hooks
????this.hooks?=?{
??????//?開始編譯時(shí)的鉤子
??????run:?new?SyncHook(),
??????//?輸出?asset?到?output?目錄之前執(zhí)行?(寫入文件之前)
??????emit:?new?SyncHook(),
??????//?在?compilation?完成時(shí)執(zhí)行?全部完成編譯執(zhí)行
??????done:?new?SyncHook(),
????};
??}

??//?run方法啟動(dòng)編譯
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{}
}

module.exports?=?Compiler;
復(fù)制代碼

這里,我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">Compiler這個(gè)類的構(gòu)造函數(shù)中創(chuàng)建了一個(gè)屬性hooks,它的值是三個(gè)屬性runemit、done

關(guān)于這三個(gè)屬性的值就是我們上文提到前置知識(shí)的tapableSyncHook方法,本質(zhì)上你可以簡單將SyncHook()方法理解稱為一個(gè)Emitter Event類。

當(dāng)我們通過new SyncHook()返回一個(gè)對(duì)象實(shí)例后,我們可以通過this.hook.run.tap('name',callback)方法為這個(gè)對(duì)象上添加事件監(jiān)聽,然后在通過this.hook.run.call()執(zhí)行所有tap注冊(cè)的事件。

當(dāng)然webpack真實(shí)源碼中,這里有非常多的hook。以及分別存在同步/異步鉤子,我們這里更多的是為大家講解清楚流程,所以僅列舉了三個(gè)常見且簡單的同步鉤子。

此時(shí),我們需要明白,我們可以通過Compiler類返回的實(shí)例對(duì)象上compiler.hooks.run.tap注冊(cè)鉤子。

接下來讓我們切回到webpack.js中,讓我們來填充關(guān)于插件注冊(cè)的邏輯:

const?Compiler?=?require('./compiler');

function?webpack(options)?{
??//?合并參數(shù)
??const?mergeOptions?=?_mergeOptions(options);
??//?創(chuàng)建compiler對(duì)象
??const?compiler?=?new?Compiler(mergeOptions);
??//?加載插件
??_loadPlugin(options.plugins,?compiler);
??return?compiler;
}

//?合并參數(shù)
function?_mergeOptions(options)?{
??const?shellOptions?=?process.argv.slice(2).reduce((option,?argv)?=>?{
????//?argv?->?--mode=production
????const?[key,?value]?=?argv.split('=');
????if?(key?&&?value)?{
??????const?parseKey?=?key.slice(2);
??????option[parseKey]?=?value;
????}
????return?option;
??},?{});
??return?{?...options,?...shellOptions?};
}

//?加載插件函數(shù)
function?_loadPlugin(plugins,?compiler)?{
??if?(plugins?&&?Array.isArray(plugins))?{
????plugins.forEach((plugin)?=>?{
??????plugin.apply(compiler);
????});
??}
}

module.exports?=?webpack;
復(fù)制代碼

這里我們?cè)趧?chuàng)建完成compiler對(duì)象后,調(diào)用了_loadPlugin方法進(jìn)行注冊(cè)插件。

有接觸過webpack插件開發(fā)的同學(xué),或多或少可能都有了解過。任何一個(gè)webpack插件都是一個(gè)類(當(dāng)然類本質(zhì)上都是funciton的語法糖),每個(gè)插件都必須存在一個(gè)apply方法。

這個(gè)apply方法會(huì)接受一個(gè)compiler對(duì)象。我們上邊做的就是依次調(diào)用傳入的pluginapply方法并且傳入我們的compiler對(duì)象。

這里我請(qǐng)你記住上邊的流程,日常我們編寫webpack plugin時(shí)本質(zhì)上就是操作compiler對(duì)象從而影響打包結(jié)果進(jìn)行。

也許此時(shí)你并不是很理解這句話的含義,在我們串聯(lián)完成整個(gè)流程之后我會(huì)為大家揭曉這個(gè)答案。

接下來讓我們?nèi)ゾ帉戇@些個(gè)插件:

不了解插件開發(fā)的同學(xué)可以去稍微看一下官方的介紹[8],其實(shí)不是很難,我個(gè)人強(qiáng)烈建議如果不了解可以先去看看再回來結(jié)合上變講的內(nèi)容你一定會(huì)有所收獲的。

首先讓我們先創(chuàng)建文件:

ab341edea6f9ab2c86ccd9d7e52e6655.webpimage.png
//?plugin-a.js
//?插件A
class?PluginA?{
??apply(compiler)?{
????//?注冊(cè)同步鉤子
????//?這里的compiler對(duì)象就是我們new?Compiler()創(chuàng)建的實(shí)例哦
????compiler.hooks.run.tap('Plugin?A',?()?=>?{
??????//?調(diào)用
??????console.log('PluginA');
????});
??}
}

module.exports?=?PluginA;
復(fù)制代碼
//?plugin-b.js
class?PluginB?{
??apply(compiler)?{
????compiler.hooks.done.tap('Plugin?B',?()?=>?{
??????console.log('PluginB');
????});
??}
}

module.exports?=?PluginB;
復(fù)制代碼

看到這里我相信大部分同學(xué)都已經(jīng)反應(yīng)過來了,compiler.hooks.done.tap不就是我們上邊講到的通過tapable創(chuàng)建一個(gè)SyncHook實(shí)例然后通過tap方法注冊(cè)事件嗎?

沒錯(cuò)!的確是這樣,關(guān)于webpack插件本質(zhì)上就是通過發(fā)布訂閱的模式,通過compiler上監(jiān)聽事件。然后再打包編譯過程中觸發(fā)監(jiān)聽的事件從而添加一定的邏輯影響打包結(jié)果

我們?cè)诿總€(gè)插件的apply方法上通過tap在編譯準(zhǔn)備階段(也就是調(diào)用webpack()函數(shù)時(shí))進(jìn)行訂閱對(duì)應(yīng)的事件,當(dāng)我們的編譯執(zhí)行到一定階段時(shí)發(fā)布對(duì)應(yīng)的事件告訴訂閱者去執(zhí)行監(jiān)聽的事件,從而達(dá)到在編譯階段的不同生命周期內(nèi)去觸發(fā)對(duì)應(yīng)的plugin。

所以這里你應(yīng)該清楚,我們?cè)谶M(jìn)行webpack插件開發(fā)時(shí),compiler對(duì)象上存放著本次打包的所有相關(guān)屬性,比如options打包的配置,以及我們會(huì)在之后講到的各種屬性。

尋找entry入口

這之后,我們的絕大多數(shù)內(nèi)容都會(huì)放在compiler.js中去實(shí)現(xiàn)Compiler這個(gè)類實(shí)現(xiàn)打包的核心流程。

任何一次打包都需要入口文件,接下來讓我們就從真正進(jìn)入打包編譯階段。首當(dāng)其沖的事情就是,我們需要根據(jù)入口配置文件路徑尋找到對(duì)應(yīng)入口文件。

//?compiler.js
const?{?SyncHook?}?=?require('tapable');
const?{?toUnixPath?}?=?require('./utils');

class?Compiler?{
??constructor(options)?{
????this.options?=?options;
????//?相對(duì)路徑跟路徑?Context參數(shù)
????this.rootPath?=?this.options.context?||?toUnixPath(process.cwd());
????//?創(chuàng)建plugin?hooks
????this.hooks?=?{
??????//?開始編譯時(shí)的鉤子
??????run:?new?SyncHook(),
??????//?輸出?asset?到?output?目錄之前執(zhí)行?(寫入文件之前)
??????emit:?new?SyncHook(),
??????//?在?compilation?完成時(shí)執(zhí)行?全部完成編譯執(zhí)行
??????done:?new?SyncHook(),
????};
??}

??//?run方法啟動(dòng)編譯
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{
????//?當(dāng)調(diào)用run方式時(shí)?觸發(fā)開始編譯的plugin
????this.hooks.run.call();
????//?獲取入口配置對(duì)象
????const?entry?=?this.getEntry();
??}

??//?獲取入口文件路徑
??getEntry()?{
????let?entry?=?Object.create(null);
????const?{?entry:?optionsEntry?}?=?this.options;
????if?(typeof?optionsEntry?===?'string')?{
??????entry['main']?=?optionsEntry;
????}?else?{
??????entry?=?optionsEntry;
????}
????//?將entry變成絕對(duì)路徑
????Object.keys(entry).forEach((key)?=>?{
??????const?value?=?entry[key];
??????if?(!path.isAbsolute(value))?{
????????//?轉(zhuǎn)化為絕對(duì)路徑的同時(shí)統(tǒng)一路徑分隔符為?/
????????entry[key]?=?toUnixPath(path.join(this.rootPath,?value));
??????}
????});
????return?entry;
??}
}

module.exports?=?Compiler;
復(fù)制代碼
//?utils/index.js
/**
?*
?*?統(tǒng)一路徑分隔符?主要是為了后續(xù)生成模塊ID方便
?*?@param?{*}?path
?*?@returns
?*/

function?toUnixPath(path)?{
??return?path.replace(/\\/g,?'/');
}
復(fù)制代碼

這一步我們通過options.entry處理獲得入口文件的絕對(duì)路徑。

這里有幾個(gè)需要注意的小點(diǎn):

  • this.hooks.run.call()

在我們_loadePlugins函數(shù)中對(duì)于每一個(gè)傳入的插件在compiler實(shí)例對(duì)象中進(jìn)行了訂閱,那么當(dāng)我們調(diào)用run方法時(shí),等于真正開始執(zhí)行編譯。這個(gè)階段相當(dāng)于我們需要告訴訂閱者,發(fā)布開始執(zhí)行的訂閱。此時(shí)我們通過this.hooks.run.call()執(zhí)行關(guān)于run的所有tap監(jiān)聽方法,從而觸發(fā)對(duì)應(yīng)的plugin邏輯。

  • this.rootPath:

在上述的外部webpack.config.js中我們配置了一個(gè) context: process.cwd(),其實(shí)真實(shí)webpack中這個(gè)context值默認(rèn)也是process.cwd()。

關(guān)于它的詳細(xì)解釋你可以在這里看到Context[9]。

簡而言之,這個(gè)路徑就是我們項(xiàng)目啟動(dòng)的目錄路徑,任何entryloader中的相對(duì)路徑都是針對(duì)于context這個(gè)參數(shù)的相對(duì)路徑。

這里我們使用this.rootPath在構(gòu)造函數(shù)中來保存這個(gè)變量。

  • toUnixPath工具方法:

因?yàn)椴煌僮飨到y(tǒng)下,文件分隔路徑是不同的。這里我們統(tǒng)一使用\來替換路徑中的//來替換模塊路徑。后續(xù)我們會(huì)使用模塊相對(duì)于rootPath的路徑作為每一個(gè)文件的唯一ID,所以這里統(tǒng)一處理下路徑分隔符。

  • entry的處理方法:

關(guān)于entry配置,webpack中其實(shí)有很多種。我們這里考慮了比較常見的兩種配置方式:

entry:'entry1.js'

//?本質(zhì)上這段代碼在webpack中會(huì)被轉(zhuǎn)化為
entry:?{
????main:'entry1.js
}
復(fù)制代碼
entry:?{
???'entry1':'./entry1.js',
???'entry2':'/user/wepback/example/src/entry2.js'
}
復(fù)制代碼

這兩種方式任何方式都會(huì)經(jīng)過getEntry方法最終轉(zhuǎn)化稱為{ [模塊名]:[模塊絕對(duì)路徑]... }的形式,關(guān)于geEntry()方法其實(shí)非常簡單,這里我就不過于累贅這個(gè)方法的實(shí)現(xiàn)過程了。

這一步,我們就通過getEntry方法獲得了一個(gè)keyentryName,valueentryAbsolutePath的對(duì)象了,接來下就讓我們從入口文件出發(fā)進(jìn)行編譯流程吧。

模塊編譯階段

上邊我們講述了關(guān)于編譯階段的準(zhǔn)備工作:

  • 目錄/文件基礎(chǔ)邏輯補(bǔ)充。
  • 通過hooks.tap注冊(cè)webpack插件。
  • getEntry方法獲得各個(gè)入口的對(duì)象。

接下來讓我們繼續(xù)完善compiler.js

在模塊編譯階段,我們需要做的事件:

  • 根據(jù)入口文件路徑分析入口文件,對(duì)于入口文件進(jìn)行匹配對(duì)應(yīng)的loader進(jìn)行處理入口文件。
  • loader處理完成的入口文件使用webpack進(jìn)行編譯。
  • 分析入口文件依賴,重復(fù)上邊兩個(gè)步驟編譯對(duì)應(yīng)依賴。
  • 如果嵌套文件存在依賴文件,遞歸調(diào)用依賴模塊進(jìn)行編譯。
  • 遞歸編譯完成后,組裝一個(gè)個(gè)包含多個(gè)模塊的chunk

首先,我們先來給compiler.js的構(gòu)造函數(shù)中補(bǔ)充一下對(duì)應(yīng)的邏輯:

class?Compiler?{
??constructor(options)?{
????this.options?=?options;
????//?創(chuàng)建plugin?hooks
????this.hooks?=?{
??????//?開始編譯時(shí)的鉤子
??????run:?new?SyncHook(),
??????//?輸出?asset?到?output?目錄之前執(zhí)行?(寫入文件之前)
??????emit:?new?SyncHook(),
??????//?在?compilation?完成時(shí)執(zhí)行?全部完成編譯執(zhí)行
??????done:?new?SyncHook(),
????};
????//?保存所有入口模塊對(duì)象
????this.entries?=?new?Set();
????//?保存所有依賴模塊對(duì)象
????this.modules?=?new?Set();
????//?所有的代碼塊對(duì)象
????this.chunks?=?new?Set();
????//?存放本次產(chǎn)出的文件對(duì)象
????this.assets?=?new?Set();
????//?存放本次編譯所有產(chǎn)出的文件名
????this.files?=?new?Set();
??}
??//?...
?}
復(fù)制代碼

這里我們通過給compiler構(gòu)造函數(shù)中添加一些列屬性來保存關(guān)于編譯階段生成的對(duì)應(yīng)資源/模塊對(duì)象。

關(guān)于entries\modules\chunks\assets\files這幾個(gè)Set對(duì)象是貫穿我們核心打包流程的屬性,它們各自用來儲(chǔ)存編譯階段不同的資源從而最終通過對(duì)應(yīng)的屬性進(jìn)行生成編譯后的文件。

根據(jù)入口文件路徑分析入口文件

上邊說到我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">run方法中已經(jīng)可以通過this.getEntry();獲得對(duì)應(yīng)的入口對(duì)象了~

接下來就讓我們從入口文件開始去分析入口文件吧!

class?Compiler?{
????//?run方法啟動(dòng)編譯
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{
????//?當(dāng)調(diào)用run方式時(shí)?觸發(fā)開始編譯的plugin
????this.hooks.run.call();
????//?獲取入口配置對(duì)象
????const?entry?=?this.getEntry();
????//?編譯入口文件
????this.buildEntryModule(entry);
??}

??buildEntryModule(entry)?{
????Object.keys(entry).forEach((entryName)?=>?{
??????const?entryPath?=?entry[entryName];
??????const?entryObj?=?this.buildModule(entryName,?entryPath);
??????this.entries.add(entryObj);
????});
??}
??
??
??//?模塊編譯方法
??buildModule(moduleName,modulePath)?{
????//?...
????return?{}
??}
}
復(fù)制代碼

這里我們添加了一個(gè)名為buildEntryModule方法作為入口模塊編譯方法。循環(huán)入口對(duì)象,得到每一個(gè)入口對(duì)象的名稱和路徑。

比如如假使我們?cè)陂_頭傳入entry:{ main:'./src/main.js' }的話,buildEntryModule獲得的形參entry{ main: "/src...[你的絕對(duì)路徑]" }, 此時(shí)我們buildModule方法接受的entryName為main,entryPath為入口文件main對(duì)應(yīng)的的絕對(duì)路徑。

單個(gè)入口編譯完成后,我們會(huì)在buildModule方法中返回一個(gè)對(duì)象。這個(gè)對(duì)象就是我們編譯入口文件后的對(duì)象。

buildModule模塊編譯方法

在進(jìn)行代碼編寫之前,我們先來梳理一下buildModule方法它需要做哪些事情:

  • buildModule接受兩個(gè)參數(shù)進(jìn)行模塊編譯,第一個(gè)為模塊所屬的入口文件名稱,第二個(gè)為需要編譯的模塊路徑。

  • buildModule方法要進(jìn)行代碼編譯的前提就是,通過fs模塊根據(jù)入口文件路徑讀取文件源代碼。

  • 讀取文件內(nèi)容之后,調(diào)用所有匹配的loader對(duì)模塊進(jìn)行處理得到返回后的結(jié)果。

  • 得到loader處理后的結(jié)果后,通過babel分析loader處理后的代碼,進(jìn)行代碼編譯。(這一步編譯主要是針對(duì)require語句,修改源代碼中require語句的路徑)。

  • 如果該入口文件沒有依賴與任何模塊(require語句),那么返回編譯后的模塊對(duì)象。

  • 如果該入口文件存在依賴的模塊,遞歸buildModule方法進(jìn)行模塊編譯。

讀取文件內(nèi)容

  1. 我們先調(diào)用`fs`模塊讀取文件內(nèi)容。
const?fs?=?require('fs');
//?...
class?Compiler?{
??????//...
??????//?模塊編譯方法
??????buildModule(moduleName,?modulePath)?{
????????//?1.?讀取文件原始代碼
????????const?originSourceCode?=
??????????((this.originSourceCode?=?fs.readFileSync(modulePath,?'utf-8'));
????????//?moduleCode為修改后的代碼
????????this.moduleCode?=?originSourceCode;
??????}
??????
??????//?...
?}
復(fù)制代碼

調(diào)用loader處理匹配后綴文件

  1. 接下來我們獲得了文件的具體內(nèi)容之后,就需要匹配對(duì)應(yīng)loader對(duì)我們的源代碼進(jìn)行編譯了。

實(shí)現(xiàn)簡單自定義loader

在進(jìn)行loader編譯前,我們先來實(shí)現(xiàn)一下我們上方傳入的自定義loader吧。

055ddd4ccb855f691c3e337de80f7e91.webpimage.png

webpack/loader目錄下新建loader-1.js,loader-2.js:

首先我們需要清楚簡單來說loader本質(zhì)上就是一個(gè)函數(shù),接受我們的源代碼作為入?yún)⑼瑫r(shí)返回處理后的結(jié)果。

關(guān)于loader的特性,更加詳細(xì)你可以在這里看到[10],因?yàn)槲恼轮饕v述打包流程所以loader我們簡單的作為倒序處理。更加具體的loader/plugin開發(fā)我會(huì)在后續(xù)的文章詳細(xì)補(bǔ)充。

// loader本質(zhì)上就是一個(gè)函數(shù),接受原始內(nèi)容,返回轉(zhuǎn)換后的內(nèi)容。
function?loader1(sourceCode)?{
??console.log('join?loader1');
??return?sourceCode?+?`\n?const?loader1?=?'https://github.com/19Qingfeng'`;
}

module.exports?=?loader1;
復(fù)制代碼
function?loader2(sourceCode)?{
??console.log('join?loader2');
??return?sourceCode?+?`\n?const?loader2?=?'19Qingfeng'`;
}

module.exports?=?loader2;
復(fù)制代碼

使用loader處理文件

在搞清楚了loader就是一個(gè)單純的函數(shù)之后,讓我們?cè)谶M(jìn)行模塊分析之前將內(nèi)容先交給匹配的loader去處理下吧。

//?模塊編譯方法
??buildModule(moduleName,?modulePath)?{
????//?1.?讀取文件原始代碼
????const?originSourceCode?=
??????((this.originSourceCode?=?fs.readFileSync(modulePath)),?'utf-8');
????//?moduleCode為修改后的代碼
????this.moduleCode?=?originSourceCode;
????//??2.?調(diào)用loader進(jìn)行處理
????this.handleLoader(modulePath);
??}

??//?匹配loader處理
??handleLoader(modulePath)?{
????const?matchLoaders?=?[];
????//?1.?獲取所有傳入的loader規(guī)則
????const?rules?=?this.options.module.rules;
????rules.forEach((loader)?=>?{
??????const?testRule?=?loader.test;
??????if?(testRule.test(modulePath))?{
????????if?(loader.loader)?{
??????????//?僅考慮loader?{?test:/\.js$/g,?use:['babel-loader']?},?{?test:/\.js$/,?loader:'babel-loader'?}
??????????matchLoaders.push(loader.loader);
????????}?else?{
??????????matchLoaders.push(...loader.use);
????????}
??????}
??????//?2.?倒序執(zhí)行l(wèi)oader傳入源代碼
??????for?(let?i?=?matchLoaders.length?-?1;?i?>=?0;?i--)?{
????????//?目前我們外部僅支持傳入絕對(duì)路徑的loader模式
????????//?require引入對(duì)應(yīng)loader
????????const?loaderFn?=?require(matchLoaders[i]);
????????//?通過loader同步處理我的每一次編譯的moduleCode
????????this.moduleCode?=?loaderFn(this.moduleCode);
??????}
????});
??}
復(fù)制代碼

這里我們通過handleLoader函數(shù),對(duì)于傳入的文件路徑匹配到對(duì)應(yīng)后綴的loader后,依次倒序執(zhí)行l(wèi)oader處理我們的代碼this.moduleCode并且同步更新每次moduleCode。

最終,在每一個(gè)模塊編譯中this.moduleCode都會(huì)經(jīng)過對(duì)應(yīng)的loader處理。

webpack模塊編譯階段

上一步我們經(jīng)歷過loader處理了我們的入口文件代碼,并且得到了處理后的代碼保存在了this.moduleCode中。

此時(shí),經(jīng)過loader處理后我們就要進(jìn)入webpack內(nèi)部的編譯階段了。

這里我們需要做的是:針對(duì)當(dāng)前模塊進(jìn)行編譯,將當(dāng)前模塊所有依賴的模塊(require())語句引入的路徑變?yōu)橄鄬?duì)于跟路徑(this.rootPath)的相對(duì)路徑

總之你需要搞明白的是,我們這里編譯的結(jié)果是期望將源代碼中的依賴模塊路徑變?yōu)橄鄬?duì)跟路徑的路徑,同時(shí)建立基礎(chǔ)的模塊依賴關(guān)系。后續(xù)我會(huì)告訴你為什么針對(duì)路徑進(jìn)行編譯。

讓我們繼續(xù)來完善buildModule方法吧:

const?parser?=?require('@babel/parser');
const?traverse?=?require('@babel/traverse').default;
const?generator?=?require('@babel/generator').default;
const?t?=?require('@babel/types');
const?tryExtensions?=?require('./utils/index')
//?...
??class?Compiler?{
?????//?...
??????
?????//?模塊編譯方法
??????buildModule(moduleName,?modulePath)?{
????????//?1.?讀取文件原始代碼
????????const?originSourceCode?=
??????????((this.originSourceCode?=?fs.readFileSync(modulePath)),?'utf-8');
????????//?moduleCode為修改后的代碼
????????this.moduleCode?=?originSourceCode;
????????//??2.?調(diào)用loader進(jìn)行處理
????????this.handleLoader(modulePath);
????????//?3.?調(diào)用webpack?進(jìn)行模塊編譯?獲得最終的module對(duì)象
????????const?module?=?this.handleWebpackCompiler(moduleName,?modulePath);
????????//?4.?返回對(duì)應(yīng)module
????????return?module
??????}

??????//?調(diào)用webpack進(jìn)行模塊編譯
??????handleWebpackCompiler(moduleName,?modulePath)?{
????????//?將當(dāng)前模塊相對(duì)于項(xiàng)目啟動(dòng)根目錄計(jì)算出相對(duì)路徑?作為模塊ID
????????const?moduleId?=?'./'?+?path.posix.relative(this.rootPath,?modulePath);
????????//?創(chuàng)建模塊對(duì)象
????????const?module?=?{
??????????id:?moduleId,
??????????dependencies:?new?Set(),?//?該模塊所依賴模塊絕對(duì)路徑地址
??????????name:?[moduleName],?//?該模塊所屬的入口文件
????????};
????????//?調(diào)用babel分析我們的代碼
????????const?ast?=?parser.parse(this.moduleCode,?{
??????????sourceType:?'module',
????????});
????????//?深度優(yōu)先?遍歷語法Tree
????????traverse(ast,?{
??????????//?當(dāng)遇到require語句時(shí)
??????????CallExpression:(nodePath)?=>?{
????????????const?node?=?nodePath.node;
????????????if?(node.callee.name?===?'require')?{
??????????????//?獲得源代碼中引入模塊相對(duì)路徑
??????????????const?moduleName?=?node.arguments[0].value;
??????????????//?尋找模塊絕對(duì)路徑?當(dāng)前模塊路徑+require()對(duì)應(yīng)相對(duì)路徑
??????????????const?moduleDirName?=?path.posix.dirname(modulePath);
??????????????const?absolutePath?=?tryExtensions(
????????????????path.posix.join(moduleDirName,?moduleName),
????????????????this.options.resolve.extensions,
????????????????moduleName,
????????????????moduleDirName
??????????????);
??????????????//?生成moduleId?-?針對(duì)于跟路徑的模塊ID?添加進(jìn)入新的依賴模塊路徑
??????????????const?moduleId?=
????????????????'./'?+?path.posix.relative(this.rootPath,?absolutePath);
??????????????//?通過babel修改源代碼中的require變成__webpack_require__語句
??????????????node.callee?=?t.identifier('__webpack_require__');
??????????????//?修改源代碼中require語句引入的模塊?全部修改變?yōu)橄鄬?duì)于跟路徑來處理
??????????????node.arguments?=?[t.stringLiteral(moduleId)];
??????????????//?為當(dāng)前模塊添加require語句造成的依賴(內(nèi)容為相對(duì)于根路徑的模塊ID)
??????????????module.dependencies.add(moduleId);
????????????}
??????????},
????????});
????????//?遍歷結(jié)束根據(jù)AST生成新的代碼
????????const?{?code?}?=?generator(ast);
????????//?為當(dāng)前模塊掛載新的生成的代碼
????????module._source?=?code;
????????//?返回當(dāng)前模塊對(duì)象
????????return?module
??????}
??}
復(fù)制代碼

這一步我們關(guān)于webpack編譯的階段就完成了。

需要注意的是:

  • 這里我們使用babel相關(guān)的API針對(duì)于require語句進(jìn)行了編譯,如果對(duì)于babel相關(guān)的api不太了解的朋友可以在前置知識(shí)中查看我的另兩篇文章。這里我就不在累贅了

  • 同時(shí)我們代碼中引用了一個(gè)tryExtensions()工具方法,這個(gè)方法是針對(duì)于后綴名不全的工具方法,稍后你就可以看到這個(gè)方法的具體內(nèi)容。

  • 針對(duì)于每一次文件編譯,我們都會(huì)返回一個(gè)module對(duì)象,這個(gè)對(duì)象是重中之重。

    • id屬性,表示當(dāng)前模塊針對(duì)于this.rootPath的相對(duì)目錄。
    • dependencies屬性,它是一個(gè)Set內(nèi)部保存了該模塊依賴的所有模塊的模塊ID。
    • name屬性,它表示該模塊屬于哪個(gè)入口文件。
    • _source屬性,它存放模塊自身經(jīng)過babel編譯后的字符串代碼。

tryExtensions方法實(shí)現(xiàn)

我們?cè)谏衔牡?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">webpack.config.js有這么一個(gè)配置:

101ab74d9d2a70823a1c495dc60fdb6b.webpimage.png

熟悉webpack配置的同學(xué)可能清楚,resolve.extensions是針對(duì)于引入依賴時(shí),在沒有書寫文件后綴的情況下,webpack會(huì)自動(dòng)幫我們按照傳入的規(guī)則為文件添加后綴。

在清楚了原理后我們來一起看看utils/tryExtensions方法的實(shí)現(xiàn):


/**
?*
?*
?*?@param?{*}?modulePath?模塊絕對(duì)路徑
?*?@param?{*}?extensions?擴(kuò)展名數(shù)組
?*?@param?{*}?originModulePath?原始引入模塊路徑
?*?@param?{*}?moduleContext?模塊上下文(當(dāng)前模塊所在目錄)
?*/

function?tryExtensions(
??modulePath,
??extensions,
??originModulePath,
??moduleContext
)?
{
??//?優(yōu)先嘗試不需要擴(kuò)展名選項(xiàng)
??extensions.unshift('');
??for?(let?extension?of?extensions)?{
????if?(fs.existsSync(modulePath?+?extension))?{
??????return?modulePath?+?extension;
????}
??}
??//?未匹配對(duì)應(yīng)文件
??throw?new?Error(
????`No?module,?Error:?Can't?resolve?${originModulePath}?in??${moduleContext}`
??);
}
復(fù)制代碼

這個(gè)方法很簡單,我們通過fs.existsSync檢查傳入文件結(jié)合extensions依次遍歷尋找對(duì)應(yīng)匹配的路徑是否存在,如果找到則直接返回。如果未找到則給予用于一個(gè)友好的提示錯(cuò)誤。

需要注意 extensions.unshift('');是防止用戶如果已經(jīng)傳入了后綴時(shí),我們優(yōu)先嘗試直接尋找,如果可以找到文件那么就直接返回。找不到的情況下才會(huì)依次嘗試。

遞歸處理

經(jīng)過上一步處理,針對(duì)入口文件我們調(diào)用buildModule可以得到這樣的返回對(duì)象。

我們先來看看運(yùn)行webpack/core/index.js得到的返回結(jié)果吧。

ac608d317de26a4086dc95330bb546af.webpimage.png

我在buildEntryModule中打印了處理完成后的entries對(duì)象??梢钥吹秸缥覀冎八诖?

  • id為每個(gè)模塊相對(duì)于跟路徑的模塊.(這里我們配置的context:process.cwd())為webpack目錄。
  • dependencies為該模塊內(nèi)部依賴的模塊,這里目前還沒有添加。
  • name為該模塊所屬的入口文件名稱。
  • _source為該模塊編譯后的源代碼。

目前_source中的內(nèi)容是基于

此時(shí)讓我們打開src目錄為我們的兩個(gè)入口文件添加一些依賴和內(nèi)容吧:

//?webpack/example/entry1.js
const?depModule?=?require('./module');

console.log(depModule,?'dep');
console.log('This?is?entry?1?!');


//?webpack/example/entry2.js
const?depModule?=?require('./module');

console.log(depModule,?'dep');
console.log('This?is?entry?2?!');

//?webpack/example/module.js
const?name?=?'19Qingfeng';

module.exports?=?{
??name,
};
復(fù)制代碼

此時(shí)讓我們重新運(yùn)行webpack/core/index.js:

d50fbc46c1d7a759fd355c2706d4b08d.webpimage.png

OK,目前為止我們針對(duì)于entry的編譯可以暫時(shí)告一段落了。

總之也就是,這一步我們通過``方法將entry進(jìn)行分析編譯后得到一個(gè)對(duì)象。將這個(gè)對(duì)象添加到this.entries中去。

接下來讓我們?nèi)ヌ幚硪蕾嚨哪K吧。

其實(shí)對(duì)于依賴的模塊無非也是相同的步驟:

  • 檢查入口文件中是否存在依賴。
  • 存在依賴的話,遞歸調(diào)用buildModule方法編譯模塊。傳入moduleName為當(dāng)前模塊所屬的入口文件。modulePath為當(dāng)前被依賴模塊的絕對(duì)路徑。
  • 同理檢查遞歸檢查被依賴的模塊內(nèi)部是否仍然存在依賴,存在的話遞歸依賴進(jìn)行模塊編譯。這是一個(gè)深度優(yōu)先的過程。
  • 將每一個(gè)編譯后的模塊保存進(jìn)入this.modules中去。

接下來我們只要稍稍在handleWebpackCompiler方法中稍稍改動(dòng)就可以了:

?//?調(diào)用webpack進(jìn)行模塊編譯
??handleWebpackCompiler(moduleName,?modulePath)?{
????//?將當(dāng)前模塊相對(duì)于項(xiàng)目啟動(dòng)根目錄計(jì)算出相對(duì)路徑?作為模塊ID
????const?moduleId?=?'./'?+?path.posix.relative(this.rootPath,?modulePath);
????//?創(chuàng)建模塊對(duì)象
????const?module?=?{
??????id:?moduleId,
??????dependencies:?new?Set(),?//?該模塊所依賴模塊絕對(duì)路徑地址
??????name:?[moduleName],?//?該模塊所屬的入口文件
????};
????//?調(diào)用babel分析我們的代碼
????const?ast?=?parser.parse(this.moduleCode,?{
??????sourceType:?'module',
????});
????//?深度優(yōu)先?遍歷語法Tree
????traverse(ast,?{
??????//?當(dāng)遇到require語句時(shí)
??????CallExpression:?(nodePath)?=>?{
????????const?node?=?nodePath.node;
????????if?(node.callee.name?===?'require')?{
??????????//?獲得源代碼中引入模塊相對(duì)路徑
??????????const?moduleName?=?node.arguments[0].value;
??????????//?尋找模塊絕對(duì)路徑?當(dāng)前模塊路徑+require()對(duì)應(yīng)相對(duì)路徑
??????????const?moduleDirName?=?path.posix.dirname(modulePath);
??????????const?absolutePath?=?tryExtensions(
????????????path.posix.join(moduleDirName,?moduleName),
????????????this.options.resolve.extensions,
????????????moduleName,
????????????moduleDirName
??????????);
??????????//?生成moduleId?-?針對(duì)于跟路徑的模塊ID?添加進(jìn)入新的依賴模塊路徑
??????????const?moduleId?=
????????????'./'?+?path.posix.relative(this.rootPath,?absolutePath);
??????????//?通過babel修改源代碼中的require變成__webpack_require__語句
??????????node.callee?=?t.identifier('__webpack_require__');
??????????//?修改源代碼中require語句引入的模塊?全部修改變?yōu)橄鄬?duì)于跟路徑來處理
??????????node.arguments?=?[t.stringLiteral(moduleId)];
??????????//?為當(dāng)前模塊添加require語句造成的依賴(內(nèi)容為相對(duì)于根路徑的模塊ID)
??????????module.dependencies.add(moduleId);
????????}
??????},
????});
????//?遍歷結(jié)束根據(jù)AST生成新的代碼
????const?{?code?}?=?generator(ast);
????//?為當(dāng)前模塊掛載新的生成的代碼
????module._source?=?code;
????//?遞歸依賴深度遍歷?存在依賴模塊則加入
????module.dependencies.forEach((dependency)?=>?{
??????const?depModule?=?this.buildModule(moduleName,?dependency);
??????//?將編譯后的任何依賴模塊對(duì)象加入到modules對(duì)象中去
??????this.modules.add(depModule);
????});
????//?返回當(dāng)前模塊對(duì)象
????return?module;
??}
復(fù)制代碼

這里我們添加了這樣一段代碼:

????//?遞歸依賴深度遍歷?存在依賴模塊則加入
????module.dependencies.forEach((dependency)?=>?{
??????const?depModule?=?this.buildModule(moduleName,?dependency);
??????//?將編譯后的任何依賴模塊對(duì)象加入到modules對(duì)象中去
??????this.modules.add(depModule);
????});
復(fù)制代碼

這里我們對(duì)于依賴的模塊進(jìn)行了遞歸調(diào)用buildModule,將輸出的模塊對(duì)象添加進(jìn)入了this.modules中去。

此時(shí)讓我們重新運(yùn)行webpack/core/index.js進(jìn)行編譯,這里我在buildEntryModule編譯結(jié)束后打印了assetsmodules:

49195ed7d7ebedc82154a2690a5214c6.webpimage.png
Set?{
??{
????id:?'./example/src/entry1.js',
????dependencies:?Set?{?'./example/src/module.js'?},
????name:?[?'main'?],
????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
??????'\n'?+
??????"console.log(depModule,?'dep');\n"?+
??????"console.log('This?is?entry?1?!');\n"?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??},
??{
????id:?'./example/src/entry2.js',
????dependencies:?Set?{?'./example/src/module.js'?},
????name:?[?'second'?],
????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
??????'\n'?+
??????"console.log(depModule,?'dep');\n"?+
??????"console.log('This?is?entry?2?!');\n"?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??}
}?entries
Set?{
??{
????id:?'./example/src/module.js',
????dependencies:?Set?{},
????name:?[?'main'?],
????_source:?"const?name?=?'19Qingfeng';\n"?+
??????'module.exports?=?{\n'?+
??????'??name\n'?+
??????'};\n'?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??},
??{
????id:?'./example/src/module.js',
????dependencies:?Set?{},
????name:?[?'second'?],
????_source:?"const?name?=?'19Qingfeng';\n"?+
??????'module.exports?=?{\n'?+
??????'??name\n'?+
??????'};\n'?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??}
}?modules
復(fù)制代碼

可以看到我們已經(jīng)將module.js這個(gè)依賴如愿以償加入到modules中了,同時(shí)它也經(jīng)過loader的處理。但是我們發(fā)現(xiàn)它被重復(fù)加入了兩次。

這是因?yàn)?strong style="color:#000000;">module.js這個(gè)模塊被引用了兩次,它被entry1entry2都已進(jìn)行了依賴,在進(jìn)行遞歸編譯時(shí)我們進(jìn)行了兩次buildModule相同模塊。

讓我們來處理下這個(gè)問題:

????handleWebpackCompiler(moduleName,?modulePath)?{
???????...
????????//?通過babel修改源代碼中的require變成__webpack_require__語句
??????????node.callee?=?t.identifier('__webpack_require__');
??????????//?修改源代碼中require語句引入的模塊?全部修改變?yōu)橄鄬?duì)于跟路徑來處理
??????????node.arguments?=?[t.stringLiteral(moduleId)];
??????????//?轉(zhuǎn)化為ids的數(shù)組?好處理
??????????const?alreadyModules?=?Array.from(this.modules).map((i)?=>?i.id);
??????????if?(!alreadyModules.includes(moduleId))?{
????????????//?為當(dāng)前模塊添加require語句造成的依賴(內(nèi)容為相對(duì)于根路徑的模塊ID)
????????????module.dependencies.add(moduleId);
??????????}?else?{
????????????//?已經(jīng)存在的話?雖然不進(jìn)行添加進(jìn)入模塊編譯?但是仍要更新這個(gè)模塊依賴的入口
????????????this.modules.forEach((value)?=>?{
??????????????if?(value.id?===?moduleId)?{
????????????????value.name.push(moduleName);
??????????????}
????????????});
??????????}
????????}
??????},
????});
????...
????}
復(fù)制代碼

這里在每一次代碼分析的依賴轉(zhuǎn)化中,首先判斷this.module對(duì)象是否已經(jīng)存在當(dāng)前模塊了(通過唯一的模塊id路徑判斷)。

如果不存在則添加進(jìn)入依賴中進(jìn)行編譯,如果該模塊已經(jīng)存在過了就證明這個(gè)模塊已經(jīng)被編譯過了。所以此時(shí)我們不需要將它再次進(jìn)行編譯,我們僅僅需要更新這個(gè)模塊所屬的chunk,為它的name屬性添加當(dāng)前所屬的chunk名稱。

重新運(yùn)行,讓我們?cè)趤砜纯创蛴〗Y(jié)果:

Set?{
??{
????id:?'./example/src/entry1.js',
????dependencies:?Set?{?'./example/src/module.js'?},
????name:?[?'main'?],
????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
??????'\n'?+
??????"console.log(depModule,?'dep');\n"?+
??????"console.log('This?is?entry?1?!');\n"?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??},
??{
????id:?'./example/src/entry2.js',
????dependencies:?Set?{},
????name:?[?'second'?],
????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
??????'\n'?+
??????"console.log(depModule,?'dep');\n"?+
??????"console.log('This?is?entry?2?!');\n"?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??}
}?entries
Set?{
??{
????id:?'./example/src/module.js',
????dependencies:?Set?{},
????name:?[?'main',?'./module'?],
????_source:?"const?name?=?'19Qingfeng';\n"?+
??????'module.exports?=?{\n'?+
??????'??name\n'?+
??????'};\n'?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??}
}?modules
復(fù)制代碼

此時(shí)針對(duì)我們的“模塊編譯階段”基本已經(jīng)結(jié)束了,這一步我們對(duì)于所有模塊從入口文件開始進(jìn)行分析。

  • 從入口出發(fā),讀取入口文件內(nèi)容調(diào)用匹配loader處理入口文件。
  • 通過babel分析依賴,并且同時(shí)將所有依賴的路徑更換為相對(duì)于項(xiàng)目啟動(dòng)目錄options.context的路徑。
  • 入口文件中如果存在依賴的話,遞歸上述步驟編譯依賴模塊。
  • 將每個(gè)依賴的模塊編譯后的對(duì)象加入this.modules。
  • 將每個(gè)入口文件編譯后的對(duì)象加入this.entries。
編譯完成階段

在上一步我們完成了模塊之間的編譯,并且為moduleentry分別填充了內(nèi)容。

在將所有模塊遞歸編譯完成后,我們需要根據(jù)上述的依賴關(guān)系,組合最終輸出的chunk模塊。

讓我們來繼續(xù)改造我們的Compiler吧:

class?Compiler?{

????//?...
????buildEntryModule(entry)?{
????????Object.keys(entry).forEach((entryName)?=>?{
??????????const?entryPath?=?entry[entryName];
??????????//?調(diào)用buildModule實(shí)現(xiàn)真正的模塊編譯邏輯
??????????const?entryObj?=?this.buildModule(entryName,?entryPath);
??????????this.entries.add(entryObj);
??????????//?根據(jù)當(dāng)前入口文件和模塊的相互依賴關(guān)系,組裝成為一個(gè)個(gè)包含當(dāng)前入口所有依賴模塊的chunk
??????????this.buildUpChunk(entryName,?entryObj);
????????});
????????console.log(this.chunks,?'chunks');
????}
????
?????//?根據(jù)入口文件和依賴模塊組裝chunks
??????buildUpChunk(entryName,?entryObj)?{
????????const?chunk?=?{
??????????name:?entryName,?//?每一個(gè)入口文件作為一個(gè)chunk
??????????entryModule:?entryObj,?//?entry編譯后的對(duì)象
??????????modules:?Array.from(this.modules).filter((i)?=>
????????????i.name.includes(entryName)
??????????),?//?尋找與當(dāng)前entry有關(guān)的所有module
????????};
????????//?將chunk添加到this.chunks中去
????????this.chunks.add(chunk);
??????}
??????
??????//?...
}
復(fù)制代碼

這里,我們根據(jù)對(duì)應(yīng)的入口文件通過每一個(gè)模塊(module)的name屬性查找對(duì)應(yīng)入口的所有依賴文件。

我們先來看看this.chunks最終會(huì)輸出什么:

Set?{
??{
????name:?'main',
????entryModule:?{
??????id:?'./example/src/entry1.js',
??????dependencies:?[Set],
??????name:?[Array],
??????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
????????'\n'?+
????????"console.log(depModule,?'dep');\n"?+
????????"console.log('This?is?entry?1?!');\n"?+
????????"const?loader2?=?'19Qingfeng';\n"?+
????????"const?loader1?=?'https://github.com/19Qingfeng';"
????},
????modules:?[?[Object]?]
??},
??{
????name:?'second',
????entryModule:?{
??????id:?'./example/src/entry2.js',
??????dependencies:?Set?{},
??????name:?[Array],
??????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
????????'\n'?+
????????"console.log(depModule,?'dep');\n"?+
????????"console.log('This?is?entry?2?!');\n"?+
????????"const?loader2?=?'19Qingfeng';\n"?+
????????"const?loader1?=?'https://github.com/19Qingfeng';"
????},
????modules:?[]
??}
}?
復(fù)制代碼

這一步,我們得到了Webpack中最終輸出的兩個(gè)chunk。

它們分別擁有:

  • name:當(dāng)前入口文件的名稱
  • entryModule: 入口文件編譯后的對(duì)象。
  • modules: 該入口文件依賴的所有模塊對(duì)象組成的數(shù)組,其中每一個(gè)元素的格式和entryModule是一致的。

此時(shí)編譯完成我們拼裝chunk的環(huán)節(jié)就圓滿完成。

輸出文件階段

我們先放一下上一步所有編譯完成后拼裝出來的this.chunks。

分析原始打包輸出結(jié)果

這里,我把webpack/core/index.js中做了如下修改:

-?const?webpack?=?require('./webpack');
+?const?webpack?=?require('webpack')

...
復(fù)制代碼

運(yùn)用原本的webpack代替我們自己實(shí)現(xiàn)的webpack先進(jìn)行一次打包。

運(yùn)行webpack/core/index.js后,我們會(huì)在webpack/src/build中得到兩個(gè)文件:main.jssecond.js,我們以其中一個(gè)main.js來看看它的內(nèi)容:

(()?=>?{
??var?__webpack_modules__?=?{
????'./example/src/module.js':?(module)?=>?{
??????const?name?=?'19Qingfeng';

??????module.exports?=?{
????????name,
??????};

??????const?loader2?=?'19Qingfeng';
??????const?loader1?=?'https://github.com/19Qingfeng';
????},
??};
??//?The?module?cache
??var?__webpack_module_cache__?=?{};

??//?The?require?function
??function?__webpack_require__(moduleId)?{
????//?Check?if?module?is?in?cache
????var?cachedModule?=?__webpack_module_cache__[moduleId];
????if?(cachedModule?!==?undefined)?{
??????return?cachedModule.exports;
????}
????//?Create?a?new?module?(and?put?it?into?the?cache)
????var?module?=?(__webpack_module_cache__[moduleId]?=?{
??????//?no?module.id?needed
??????//?no?module.loaded?needed
??????exports:?{},
????});

????//?Execute?the?module?function
????__webpack_modules__[moduleId](module,?module.exports,?__webpack_require__);

????//?Return?the?exports?of?the?module
????return?module.exports;
??}

??var?__webpack_exports__?=?{};
??//?This?entry?need?to?be?wrapped?in?an?IIFE?because?it?need?to?be?isolated?against?other?modules?in?the?chunk.
??(()?=>?{
????const?depModule?=?__webpack_require__(
??????/*!?./module?*/?'./example/src/module.js'
????);

????console.log(depModule,?'dep');
????console.log('This?is?entry?1?!');

????const?loader2?=?'19Qingfeng';
????const?loader1?=?'https://github.com/19Qingfeng';
??})();
})();

復(fù)制代碼

這里我手動(dòng)刪除了打包生成后的多余注釋,精簡了代碼。

我們來稍微分析一下原始打包生成的代碼:

webpack打包后的代碼內(nèi)部定義了一個(gè)__webpack_require__的函數(shù)代替了NodeJs內(nèi)部的require方法。

同時(shí)底部的

0650c216247ae69e1dea8fc81f2e657e.webpimage.png

這塊代碼相比大家都很熟悉吧,這就是我們編譯后的入口文件代碼。同時(shí)頂部的代碼是該入口文件依賴的所有模塊定義的一個(gè)對(duì)象:

7ea6eff24a395334ce1a51c105ec9225.webpimage.png

這里定義了一個(gè)__webpack__modules的對(duì)象,**對(duì)象的key為該依賴模塊相對(duì)于跟路徑的相對(duì)路徑,對(duì)象的value該依賴模塊編譯后的代碼。`

輸出文件階段

接下里在分析完webpack原始打包后的代碼之后,上我們來繼續(xù)上一步。通過我們的this.chunks來嘗試輸出最終的效果吧。

讓我們回到Compiler上的run方法中:

???class?Compiler?{
???
???}
??//?run方法啟動(dòng)編譯
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{
????//?當(dāng)調(diào)用run方式時(shí)?觸發(fā)開始編譯的plugin
????this.hooks.run.call();
????//?獲取入口配置對(duì)象
????const?entry?=?this.getEntry();
????//?編譯入口文件
????this.buildEntryModule(entry);
????//?導(dǎo)出列表;之后將每個(gè)chunk轉(zhuǎn)化稱為單獨(dú)的文件加入到輸出列表assets中
????this.exportFile(callback);
??}
復(fù)制代碼

我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">buildEntryModule模塊編譯完成之后,通過this.exportFile方法實(shí)現(xiàn)導(dǎo)出文件的邏輯。

讓我們來一起看看this.exportFile方法:

?//?將chunk加入輸出列表中去
??exportFile(callback)?{
????const?output?=?this.options.output;
????//?根據(jù)chunks生成assets內(nèi)容
????this.chunks.forEach((chunk)?=>?{
??????const?parseFileName?=?output.filename.replace('[name]',?chunk.name);
??????//?assets中?{?'main.js':?'生成的字符串代碼...'?}
??????this.assets.set(parseFileName,?getSourceCode(chunk));
????});
????//?調(diào)用Plugin?emit鉤子
????this.hooks.emit.call();
????//?先判斷目錄是否存在?存在直接fs.write?不存在則首先創(chuàng)建
????if?(!fs.existsSync(output.path))?{
??????fs.mkdirSync(output.path);
????}
????//?files中保存所有的生成文件名
????this.files?=?Object.keys(this.assets);
????//?將assets中的內(nèi)容生成打包文件?寫入文件系統(tǒng)中
????Object.keys(this.assets).forEach((fileName)?=>?{
??????const?filePath?=?path.join(output.path,?fileName);
??????fs.writeFileSync(filePath,?this.assets[fileName]);
????});
????//?結(jié)束之后觸發(fā)鉤子
????this.hooks.done.call();
????callback(null,?{
??????toJson:?()?=>?{
????????return?{
??????????entries:?this.entries,
??????????modules:?this.modules,
??????????files:?this.files,
??????????chunks:?this.chunks,
??????????assets:?this.assets,
????????};
??????},
????});
??}
復(fù)制代碼

exportFile做了如下幾件事:

  • 首先獲取配置參數(shù)的輸出配置,迭代我們的this.chunks,將output.filename中的[name]替換稱為對(duì)應(yīng)的入口文件名稱。同時(shí)根據(jù)chunks的內(nèi)容為this.assets中添加需要打包生成的文件名和文件內(nèi)容。

  • 將文件寫入磁盤前調(diào)用pluginemit鉤子函數(shù)。

  • 判斷output.path文件夾是否存在,如果不存在,則通過fs新建這個(gè)文件夾。

  • 將本次打包生成的所有文件名(this.assetskey值組成的數(shù)組)存放進(jìn)入files中去。

  • 循環(huán)this.assets,將文件依次寫入對(duì)應(yīng)的磁盤中去。

  • 所有打包流程結(jié)束,觸發(fā)webpack插件的done鉤子。

  • 同時(shí)為NodeJs Webpack APi呼應(yīng),調(diào)用run方法中外部傳入的callback傳入兩個(gè)參數(shù)。

總的來說,this.assets做的事情也比較簡單,就是通過分析chunks得到assets然后輸出對(duì)應(yīng)的代碼到磁盤中。

仔細(xì)看過上邊代碼,你會(huì)發(fā)現(xiàn)。this.assets這個(gè)Map中每一個(gè)元素的value是通過調(diào)用getSourceCode(chunk)方法來生成模塊對(duì)應(yīng)的代碼的。

那么getSourceCode這個(gè)方法是如何根據(jù)chunk來生成我們最終編譯后的代碼呢?讓我們一起來看看吧!

getSourceCode方法

首先我們來簡單明確一下這個(gè)方法的職責(zé),我們需要getSourceCode方法接受傳入的chunk對(duì)象。從而返回該chunk的源代碼。

廢話不多說,其實(shí)這里我用了一個(gè)比較偷懶的辦法,但是完全不妨礙你理解Webpack流程,上邊我們分析過原本webpack打包后的代碼僅僅只有入口文件和模塊依賴是每次打包不同的地方,關(guān)于require方法之類都是相通的。

把握每次的不同點(diǎn),我們直接先來看看它的實(shí)現(xiàn)方式:

//?webpack/utils/index.js

...


/**
?*
?*
?*?@param?{*}?chunk
?*?name屬性入口文件名稱
?*?entryModule入口文件module對(duì)象
?*?modules?依賴模塊路徑
?*/

function?getSourceCode(chunk)?{
??const?{?name,?entryModule,?modules?}?=?chunk;
??return?`
??(()?=>?{
????var?__webpack_modules__?=?{
??????${modules
????????.map((module)?=>?{
??????????return?`
??????????'${module.id}':?(module)?=>?{
????????????${module._source}
??????}
????????`
;
????????}
)
????????.join(',')}
????};
????//?The?module?cache
????var?__webpack_module_cache__?=?{};

????//?The?require?function
????function?__webpack_require__(moduleId)?{
??????//?Check?if?module?is?in?cache
??????var?cachedModule?=?__webpack_module_cache__[moduleId];
??????if?(cachedModule?!==?undefined)?{
????????return?cachedModule.exports;
??????}
??????//?Create?a?new?module?(and?put?it?into?the?cache)
??????var?module?=?(__webpack_module_cache__[moduleId]?=?{
????????//?no?module.id?needed
????????//?no?module.loaded?needed
????????exports:?{},
??????});

??????//?Execute?the?module?function
??????__webpack_modules__[moduleId](module,?module.exports,?__webpack_require__);

??????//?Return?the?exports?of?the?module
??????return?module.exports;
????}

????var?__webpack_exports__?=?{};
????//?This?entry?need?to?be?wrapped?in?an?IIFE?because?it?need?to?be?isolated?against?other?modules?in?the?chunk.
????(()?=>?{
??????${entryModule._source}
????})();
??})();
??`
;
}
...
復(fù)制代碼

這段代碼其實(shí)非常非常簡單,遠(yuǎn)遠(yuǎn)沒有你想象的多難!有點(diǎn)返璞歸真的感覺是嗎哈哈。

getSourceCode方法中,我們通過組合而來的chunk獲得對(duì)應(yīng)的:

  • name: 該入口文件對(duì)應(yīng)輸出文件的名稱。
  • entryModule: 存放該入口文件編譯后的對(duì)象。
  • modules:存放該入口文件依賴的所有模塊的對(duì)象。

我們通過字符串拼接的方式去實(shí)現(xiàn)了__webpack__modules對(duì)象上的屬性,同時(shí)也在底部通過${entryModule._source}拼接出入口文件的代碼。

這里我們上文提到過為什么要將模塊的require方法的路徑轉(zhuǎn)化為相對(duì)于跟路徑(context)的路徑,看到這里我相信為什么這么做大家都已經(jīng)了然于胸了。因?yàn)槲覀冏罱K實(shí)現(xiàn)的__webpack_require__方法全都是針對(duì)于模塊跟路徑的相對(duì)路徑自己實(shí)現(xiàn)的require方法。

同時(shí)如果不太清楚require方法是如何轉(zhuǎn)變稱為__webpack_require__方法的同學(xué)可以重新回到我們的編譯章節(jié)仔細(xì)復(fù)習(xí)熬~我們通過babelAST轉(zhuǎn)化階段將require方法調(diào)用變成了__webpack_require__

大功告成

至此,讓我們回到webpack/core/index.js中去。重新運(yùn)行這個(gè)文件,你會(huì)發(fā)現(xiàn)webpack/example目錄下會(huì)多出一個(gè)build目錄。

fcc579842f81ec7235cfcc3a95d11504.webpimage.png

這一步我們就完美的實(shí)現(xiàn)屬于我們自己的webpack。

實(shí)質(zhì)上,我們對(duì)于實(shí)現(xiàn)一個(gè)簡單版的webpack核心我還是希望大家可以在理解它的工作流的同時(shí)徹底理解compiler這個(gè)對(duì)象。

在之后的任何關(guān)于webpack相關(guān)底層開發(fā)中,真正做到對(duì)于compiler的用法了然于胸。了解compiler上的各種屬性是如何影響到編譯打包結(jié)果的。

讓我們用一張流程圖來進(jìn)行一個(gè)完美的收尾吧:

e003b1fb7afb2784934bc0978c67e888.webpimage.png寫在最后

首先,感謝每一位可以看到這里的同學(xué)。

這篇文章相對(duì)有一定的知識(shí)門檻并且代碼部分居多,敬佩每一位可以讀到結(jié)尾的同學(xué)。

文章中對(duì)于實(shí)現(xiàn)一個(gè)簡易版的Webpack在這里就要和大家告一段落了,這其實(shí)只是一個(gè)最基礎(chǔ)版本的webpack工作流。

但是正是通過這樣一個(gè)小??可以帶我們真正入門webpack的核心工作流,希望這篇文章對(duì)于大家理解webpack時(shí)可以起到更好的輔助作用。

其實(shí)在理解清楚基礎(chǔ)的工作流之后,針對(duì)于loaderplugin開發(fā)都是信手拈來的部分,文章中對(duì)于這兩部分內(nèi)容的開發(fā)介紹比較膚淺,后續(xù)我會(huì)分別更新有關(guān)loaderplugin的詳細(xì)開發(fā)流程。有興趣的同學(xué)可以及時(shí)關(guān)注??。

文章中的代碼你可以在這里下載[11],這份簡易版的webpack我也會(huì)持續(xù)在代碼庫中完善更多工作流的邏輯處理。

同時(shí)這里這里的代碼我想強(qiáng)調(diào)的是源碼流程的講解,真實(shí)的webpack會(huì)比這里復(fù)雜很多很多。這里為了方便大家理解刻意進(jìn)行了簡化,但是核心工作流是和源碼中基本一致的。

作者:19組清風(fēng)

https://juejin.cn/post/7031546400034947108

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

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

手機(jī)掃一掃分享

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

手機(jī)掃一掃分享

分享
舉報(bào)

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

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 丁香六月天| 精品网站| 国产伦子伦一级A片免费看老牛| 欧美成综合| 另类老妇奶性BBWBBwBBw| 七六十路の高齢熟妇无码| 欧美三级电影在线观看| 日本a片| 久久大香蕉| 按摩忍不住BD中文字幕| 亚洲成人第一网站| AV偷拍| 伊人77| 日韩性爱网| 成人小视频十八禁免费观看| 婷婷久久久久久| 无码入口| 杨贵妃一级婬片90分钟| 国产激情无码视频| 二区AV| 亚洲色婷婷在线| 天天干无码| 亚洲激情性爱| 国产自慰一区| 色妞视频| 国产一区在线视频| www.色在线观看| 一本一道波多野结衣潮喷视频| 日本中文字幕在线观看视频| 色三区| 国产黃色AAA片| 特级丰满少妇免费观看| 日皮视频| 91在线无码精品秘入口国战| 波多野结衣网址| 国产一区二区免费在线观看| 中文字幕在线视频第一页| 豆花视频成人精品视频| 午夜无码在线观看视频| 亚洲高清无码中字| 最新中文字幕免费MV第一季歌词| 国产成人亚洲日韩| 日韩乱轮小说与视频| 国产99久久| 国产精品在线观看视频| 国产日韩欧美在线| 日韩国产三级| 人人艹人人干| 黄色电影免费网站| 内射一区二区| 黄色电影天堂网| 久久午夜福利电影| 大香蕉性爱| 婷婷五月天激情俺来也| 亲子乱婬-一级A片| 亚洲激情片| 高H视频在线观看| 春色激情| 日韩电影免费在线观看| 欧美日韩久久久| 不卡三区| 日皮免费视频| 99大香蕉视频| 欧美国产精品一区二区三区| caopor在线| 国产亚洲视频完整在线观看 | 色九九| av影音先锋在线| 欧美日本国产| 黄色自拍视频| 亚洲精品久久久久avwww潮水| 欧美无人区码suv| 麻豆精品在线播放| 国产一区二区三区在线观看免费视频免费视频免费视频 | 久久久999精品日韩一区二区| 国产成人V在线精品一区| 亚洲日本黄色视频| 深爱婷婷网| 三级网址在线| 波多野结衣高清无码| 爱爱视频天天操| 大香蕉网伊人| 亚洲无码你懂的| 久久精品99久久久久久| 欧美人妻视频在线| www.豆花视频成人版| 国产一级在线观看| 无码电影免费观看| 中文字幕乱码亚洲无线码在线日噜噜 | av天堂小说网| 亚洲视频免费完整版在线播放| 亚洲精品国产精品乱码不卡√香蕉| 色九九九九| 不卡无码中文字幕一区| 亚洲男人天堂视频| 91就要爱爱视频| 国产av资源网| 人人摸人人草| 中文字幕高清无码在线| 国产成人无码永久免费| 超碰超碰| 日本三级片网址| 中文字幕视频| 91麻豆精品无码人妻| 国产精品揄拍一区二区| 色视频免费在线观看| 色婷婷精品| 在线播放91灌醉迷J高跟美女 | 美女天天日| 久久性视频| 一本加勒比HEZYO东京热无码| 91香蕉在线| 91人妻人人澡人人添人人爽| 热逼视频| 日韩AV无码专区亚洲AV| 成人先锋AV| 日日日操| 未满十八18禁止免费无码网站| 91av无码| 国产成人综合自拍| 午夜久久视频| 少妇福利| 无码波多野结衣| 污污污www精品国产网站| 一级成人电影| 免费a片在线观看| 久久久久久久久黄色| 国产一级片免费观看| 久久久aaa| 影音先锋国产精品| 欧美一区二区丁香五月天激情| 99久久影院| 精品一区二区三区三区| 久久大陆| 国产精品HongKong麻豆 | 日本91视频| 欧美V在线| 国产黄色影院| 亚洲AV成人无码精品区| 2019中文字幕在线| 亚洲天堂2025| 艹逼视频| 在线不卡免费Av| 黑巨茎大战欧美白妞小说| 人人艹在线观看| 无码精品一区二区| 91射区| 青娱乐AV| 亚洲AV成人无码精品直播在线| 午夜福利日本| 性爱综合网| 黄片免费看网站| 国产男女无套免费| 波多野结衣网址| 蜜芽成人网站| 少妇人妻一级A毛片| 日韩在线免费观看视频| 中文字幕av第一页| 久久国产高清视频| 在线不卡视频| 成人AV影院| 精品国内视频| 激情片AAA| 中文字幕AV播放| 午夜人妻AV| 一品国精和二品国精的文化意义 | 午夜福利区| 激情小视频国产在线播放| 九色PORNY国产成人| 久草视频这里只有精品| 男人的天堂av网站| 日韩中文字幕av| 无码123区| A色片| 内射午夜福利在线免费观看视频 | 老妇槡BBBB| 欧美精品一区二区少妇免费A片| 久久久福利视频| www.簧片| 狠狠做深爱婷婷久久综合一区| 亚洲www| 男男做受A片AAAA| 免费观看在线无码视频| 无码主播| 久草黄色电影在线观看| 伊人久久大香线蕉av一区| 欧美一级欧美三级在线观看| 成人婷婷网| 黄色特级aaa片| 69黄色视频| 人人爽久久涩噜噜噜网站| 69亚洲| 毛片在线看片| 春色激情| 国产高清一区二区| 欧美日韩中文字幕视频| 99精品六月婷婷综合在线| 大鸡巴久久久久久久| 黃色A片一級二級三級免費久久久| 欧美中出| 亚洲.无码.制服.日韩.中文字幕| 欧美一区在线视频| 免费日逼| 再深点好爽灬轻点久久国产| 亚洲国产中文字幕在线播放| 免费黄色在线观看| 国产91在线观看| 日韩毛片大全| 俺去搞| 久热国产视频| 天天撸天天干| 这里只有精品久久| 欧美成人AA| 高潮无码在线观看| 精品视频在线播放| 蜜桃91视频| 伊人77| 91一级特黄大片| 欧美精品成人网站| 影音先锋女人aV鲁色资源网站| AAAA毛片视频| 99视频免费| 在线国产小视频| 可以在线观看的av| 久热在线精品视频| 日韩,变态,另类,中文,人妻| 日韩少妇AV| 爆菊花综合网| 菊花插综合网| 日韩精品高清中文| 精品人妻一区二区三区含羞草 | 内射无码视频| 中文无码观看| 九色91PORNY国产| 久久久老熟女一区二区三区91| 在线成人亚洲| 久久夜色精品噜噜亚洲AV| 亚洲无码免费看| 超碰在线中文字幕| 中文字幕乱码中文字幕| 丁香六月综合激情| 欧美亚洲国产日韩| 无码电影在线播放| 中文无码日本高潮喷水| 久久99视频| 人妻大香蕉| 无码视频免费在线观看| 天天插天天插| 国产精品无码一区二区在线欢| 黄色免费在线观看视频| 加勒比无码在线播放| 好男人WWW一区二区三区| 天堂资源中文在线| 超碰福利在线| 蜜桃久久久亚洲精品| 日韩在线播放视频| 欧美偷拍精品| 欧美人妻激情| AV免费在线播放| 无码操逼视频| 欧美一道本| 亚洲免费成人网站| 色播婷婷五月天| 亚洲无码A片在线观看| 亚洲无码自拍偷拍| 黄片福利| 色老板在线观看永久免费视频| 国产一级AAAAA片免费| 1024在线视频| 69av在线观看| 日本AI高清无码在线观看网址| 日本三级片视频不卡| 精品无码一区二区三区四区久久久软件 | 黄片网站免费观看| 日韩欧美在线观看| 成人毛片在线播放免费| 丁香五月天在线播放| 人人操在线公开| 久久久久久久| 亚洲AV免费| 俺也操| 日韩无码久久久| 91人人澡人人爽人人看| 日本黄色视频在线| 成人网视频| 女人的天堂AV在线观看| 日韩成人片无码| 在线A片免费观看| 成人三级片网| 欧美黄色精品| 亚洲精品久久久久久久久豆丁网 | 黄色精品视频| 国产剧情一区二区av在线观看| 成人国产AV网站| 五月丁香综合在线| 亚洲AV黄片| 最新中文字幕免费MV第一季歌词| 色色色99| 操逼视频在线观看| 国产精品久久久91| 国产小福利| 成人V| 仓井空一区二区| 国产一级女婬乱免费看| 麻豆国产视频| 免费操逼| 日韩一级性爱| 国产91www| 国产女人十八水真多| 欧洲三级网观看| 免费黄色一级电影| 国产麻豆三级片| 69国产精品成人无码| 人人射人人操| AV黄色| 最新va在线观看| 中国人妻HDbute熟睡| 日韩第一色| 青春草在线免费视频| 欧美精品第一页| 日韩综合精品中文字幕66| 伊人大香蕉在线| V天堂在线视频| 日韩A毛片| 无码日韩人妻精品久久蜜桃| 91精品视频网站| 春色激情| 拍拍AV| 男女福利视频| 91人妻人人爽人人澡人人爽| 97超碰人妻| 日韩一级免费| 51亚洲精品| 久久四区| 亚洲性爱一级片| 逼特逼在线观看| 亚洲精品一区中文字幕乱码| 91精品人妻一区二区三区四区| 天天干天天日天天射| ww国产| 色天使青青草| 久草网视频| 亚洲.欧美.丝袜.中文.综合 | 日本黄色视频网| 91蜜桃网| 搡BBBB搡BBBB搡BBB| 黄色视频网站亚洲| 亚欧一区二区| 北条麻妃无码播放| 日韩欧美综合| 中文字幕在线无码视频| 久久做爱视频| 日韩AV在线电影| 成人无码高清在线观看| 国产丰满乱子伦无码| 亚洲熟女一区| 日韩一级一片| 四虎综合网| 俺也去啦WWW色官网| 天天爽夜夜爽| 欧美黄色一级网站| 国产三级午夜理伦三级| 久操网站| 一本一道波多野结衣潮喷视频| 国产精品93333333| 无码高清视频| 噜噜色小说| 日韩欧美小视频| 日韩一级一片内射视频4K| 成人777777免费视频色| 亚洲免费三级| 性爱精品视频| 亚洲无码AV在线观看| 色色色99| 人人人人人妻| 亚州中文字幕| 内射网站| 国产精品色综合| 国产精品国产精品国产专区不卡| 99久久黄色| 成人无码国产| 97欧美精品人妻系列| 国产在线观看mv免费全集电视剧大全| 国产成人无码精品久在线观看| 欧美爱爱网| 色草视频| 黄色成人视频在线免费观看| 国模一区二区三区| 国产AV小电影| 日韩一级视频| 一区二区在线免费观看| 精品中文字幕在线观看| 日本高清视频网站| 国产免费无码视频| 中文字幕在线观看免费高清完整版在线观看 | 成人免费看AA片| 国产在线观看一区| 一道本一区二区三区| 韩国午夜激情| 国产av一区二区三区四区| AV资源在线| A免费在线观看| 青春草在线观看视频| 亚洲人人色| 国产熟女| 欧美不卡在线播放| 做爰视频毛片下载蜜桃视频| 色综合天天| 黄色中文字幕| 五月婷婷六月激情| 国产成人自拍视频在线| www.亚洲精品| 大香蕉伊人免费| 真实野外打野视频| 少妇456| 99re伊人| 人人操人人看人人摸| 丰满人妻一区二区三区免费| 性色网站| 亚洲丝袜av| 五月婷婷视频| 久久精品视频免费看| 日韩欧美爱爱| 一区二区三区四区成人| 亚洲成人视频在线| 伊人久久大香蕉国产| 五月激情六月丁香| 在线一区观看| 国产在线小电影| 久久国产精品电影| 国产精品久久视频| 人人摸在线视频| 俺来也AV| 亚洲成人在线免费| 日韩三级小说| av女人的天堂| AV一区二区三区| 风流老熟女一区二区三区| 无码-ThePorn| 国产av三级| 成人免费A片| 加勒比日韩| 少妇无码一区| 人人摸人人色| 国产主播一区二区| 男人天堂网在线| 中文字幕无码观看| 91福利在线观看| 欧美特大黄| 日韩一级电影在线| 综合色区| 少妇搡BBBB搡BBB搡18禁| 日本黄色视频免费观看| 翔田千里无码播放| 91就要爱爱视频| 韩国精品一区二区三区| 国内自拍欧美| 东北嫖老熟女一区二区视频网站| 国产视频成人| 黄色视频免费看| 嫩BBB槡BBBB槡BBBB视频-百度 | 高清视频一区| 日本大胆中出| 欧美日韩在线电影| 色播网址| 西西444WWW大胆无| 国产秘精品一区二区三区免费| 亚洲国产成人精品激情在线| 色欲插插| japanese在线观看| 亚洲天堂av在线观看| 操欧美美女| 无码高清视频在线观看| 色久综合| 久久99久久99久久99| 丰满的人妻一区二区10| 囯产精品久久久久久久久久| 黄色视频毛片一一| 久久久久久国产免费A片| 黄片在线免费观看视频| 韩国无码一区二区| 日韩精品一区二区三区四区| 在线免费观看黄色片| 日韩一级片在线| 乳揉みま痴汉电车羽月希免费观看 | 夜夜操天天日| 一区二区三区三级片| 国产内射在线观看| 国产在线毛片| 2022天天干| 中文字幕无码在线观看| 国产做受91一片二片老头| 色哟哟视频在线观看| 国产三级无码| 东北老女人操逼视频| 大肉大捧一进一出免费阅读| 国产高清秘成人久久| 免费看黄片的网站| 91蜜桃视频在线观看| 国产AV无遮挡| 一区二区三区视频在线观看| 天天日天天拍| 中文无码一区二区三区四区| 久久99久久视频| 无码av在线观看| 三级片网站在线播放| 欧美性性性| 日韩AV三级片| 日韩A视频| 青娱乐国产在线视频| 黄色成人在线观看视频| 99久久免费网| 人人澡人人澡人人澡| 麻豆成人精品国产免费| 国产91嫩草乱婬A片2蜜臀| 91在线观看视频| 内射无码专区久久亚洲| 日本成人高清视频| 精品人妻在线| 成人黄色免费视频| 日韩综合网| 美国黄色A片| 久久久久久久久久久国产精品| 99久久婷婷国产综合精品草原| 在线天堂19| 国产精品一区二区三区不卡| A级毛片网站| 黄色www| 国产精品免费观看视频| 国产网站免费| 微拍福利一区二区| 最新中文字幕在线视频| 成人午夜在线视频| 午夜视频成人| 国产精品无码永久免费A片| 国产一区二区无码| 色综合综合色| 自拍视频一区| 国产精品人妻无码一区牛牛影视| 亚洲中文免费视频| 无码黄页| 久久另类TS人妖一区二区| 国产成人精品一区二区三区视频| av三级网站| 久久久九九九| 亚洲欧美网站| 亚洲.无码.制服.日韩.中文字幕| 婷婷五月天激情电影| 国产乱子伦一区二区三| 无码中文综合成熟精品AV电影| 成人在线18禁| 91爱爱com| 四川少妇bbbbbbbbb| 四川性BBB搡BBB爽爽爽小说| 午夜伦理福利| 呦呦av| 欧美亚洲系列| 天天扣天天操| 日韩99在线观看| 操你啦无码日韩| 2019中文字幕在线免费观看| 在线观看无码视频| 国产美女高潮| 黄色成人网站在线| 日韩大屌| 一级黄色网| 一道本一区二区三区| 午夜福利影视| 国产一级片在线| 不卡a12| 精品乱子伦| 色综合大香蕉| 爱草在线| 人人干日日干| 九九九无码| 国产毛片精品一区二区色欲黄A片| 欧美日韩一级二级三级| 加勒比久久88| 亚洲精品色婷婷| 国产AV无码影院| 老太色HD色老太HD.| 翔田千里53歳在线播放| 激情黄色毛片| AA精品| 色色网站视频| 影音先锋av网| 亚洲日韩国产中文字幕| 先锋av资源在线| 亚洲国产色婷婷| 91久久久久久久久久| 国精产品一区一区三区有限公司杨 | 日韩高清一区二区| 亚洲精品一区二区三区在线观看 | 亚洲AV成人无码精品区| 自拍偷拍亚洲无码| 日韩中出视频| 黄色视频a| 97在线视频免费观看| 色色色777| 91天天射| 思思99热| 成年人毛片| 婷婷五月在线| 韩国午夜福利视频| 一本色道久久88亚洲精品综合| 人人人妻人人人操| 青青草在线观看免费| 国产91在线拍揄自揄拍无码九色| 国产秘精品区二区三区日本| 麻豆精品传媒2021md| 黄色AV免费观看| 人人操人人妻人人爽| 黄色视频免费网站| 成人激情免费视频| 波多野结衣无码一区二区| 亚洲在线视频免费观看| 亚洲色逼| 久久性爱网| 国产免费小视频| 中文字幕在线观看第一页| 成人小视频十八禁免费观看 | 欧美精品99| 老司机AV91| 久久成人影音先锋| 亚洲最新AV网站| 亚洲无码视频在线免费观看| 五月天激情小说网| 免费看一区二区三区| 2025中文字幕| 国产嫩草久久久一二三久久免费观看| 一线av| 国产一区二区三区成人| 日韩精品一区二区三| 骚逼视频聊天记录| 人人妻人人玩澡人人爽| 亚洲少妇人妻| 能看的av网站| 91成人做爰A片| 国产精品爽爽久久久| 人人妻人人躁人人DVD| 北京熟妇搡BBBB搡BBBB电影| 国产在线免费视频| 黄色A级毛片| 大鸡巴视频在线| 国产精品无码激情| 国产精品国产精品国产专区不| 你懂的视频在线观看| 草久精品| 欧美级黑寡妇毛片app| 青娱乐av在线| 五月天操逼| 日本熟妇HD| 黄色视频a| 久久精品成人电影| 日韩不卡精品| 东京热一区二区三区四区| 成人AV影院| 蜜臀在线视频| 六月丁香综合| 狠狠操电影| 91免费| 亚洲高清无码免费| 亚洲小电影在线观看| av网站免费在线观看| 欧美日韩国产中文字幕| 狠狠色婷婷7777| 亚洲日韩中文字幕| 婷婷五月综合激情| 国产91精品在线观看| 成人国产精品秘在线看| 懂色av懂色av粉嫩av| 男人日女人视频| 国产性色| 亚洲情在线| a无码视频在线观看| 99爱在线观看| 日日夜夜爽歪歪| 国产精品福利在线播放| 亚洲小电影在线| 东京热精品| 性欧美xxxx| 无码人妻丰满熟妇区蜜桃| 无码免费一区二区| 在线观看免费欧美操逼视频| 免费成人在线看片黄| 久久黄色网址| 日本一级婬片A片免费看| 屁屁影院CCYYCOM国产| 无码无遮挡| 人妻无码免费视频| www.777熟女人妻| 欧美精品在线观看视频| 亚洲无码免费观看视频| 国产精品无码乱伦| 国产精品视频免费在线观看| 免费黄色av| 亚洲Av无码午夜国产精品色软件| 久久久久久久久久8888| 欧美三级片在线观看| xxx一区二区| 中文字幕亚洲一区| 日韩中文字幕一区二区三区| 精品人妻一区二区三区阅读全文| 91成人视频18| 五月天激情啪啪| 国产成人精品av| 日韩精品欧美一区二区三区| 人妻在线免费视频| 成人黄网免费观看视频| 91亚洲精品久久久久蜜桃| 中文字幕av免费在线观看| 中文字幕二区| 在线观看无码高清视频| 国产激情在线播放| 大肉大捧一进一出免费阅读| 懂色av,蜜臀AV粉嫩av| 欧一美一婬一伦一区二区三区自慰国| 日本高潮视频| 国产高清黑人| 久久一区二区三区四区五区 | 国精产品一区二区三区在线观看 | 色哟哟无码精品一区二区三区| 精品国产91| 天堂网www| 日韩大屌| 三级片在线看片AV| 欧美第一色| 韩国GOGOGO高清| 天码人妻一区二区三区在线看| 激情六月| 欧美激情区| 日韩无码高清视频| 亚洲家庭乱伦| 操逼精品| 91丨九色丨国产在线| 一区二区免费视频| 国产嫩苞又嫩又紧AV在线| 国产婷婷久久Av免费高清 | 91吴梦梦无码一区二区| 三级片久久久| 亚洲精品在线视频观看| 欧美日韩a| 亚洲少妇免费| 国产亚洲久一区二区| 亚洲一区三区| 欧美大鸡吧视频| 欧美黑吊大战白妞欧美大片| 黄色生活片| 亚洲激情在线| 无码中文一区| 亚洲任你操超碰在线| 国产精品婷婷午夜在线观看| 翔田千里53歳在线播放| 成人天堂| 美女天天日| 色综合视频| 蜜臀久久| 日本一区二区在线视频| 日韩在线中文字幕| 五月丁香999| 日产精品久久久| 国产精品秘久久久久久免费播放 | 天天干免费视频| 天天干天天日天天操| 色秘乱码一区二区三区唱戏| 中文字幕理论片| 亚洲黄色视频网站| 特级艺体西西444WWw| 日韩在线视频二区| 亚洲无码高清在线观看| 亚洲精品无码a片| 99精品亚洲| 这里只有精品视频| 老妇槡BBBB槡BBBB槡| 激情五月天在线视频| 91人妻人人澡人人精品| 大陆一级片| 亚洲欧美日韩中文字幕在线观看| 免费在线观看av| 亚洲久久久久久| 在线se| 粉嫩AV蜜乳AV蜜臀AV蜂腰AV | 欧美一级AA| 男女av在线观看| 日韩高清无码一区| 国产经典午夜福利视频合集| 亚洲成人第一网站| 欧美成人无码片免费看A片秀色| 波多野结衣亚洲视频| 苍井空精毛片精品久久久| 88av在线| 北条麻妃无码一区二区| 高H网站| 欧美一级婬片免费视频黄| 在线播放内射| 97av在线| 天天爽夜夜爽夜夜爽精品视频| 99热最新在线| 小明成人免费视频| 欧美一级片免费看| 精品欧美无人区乱码毛片| 中文字幕乱码中文字幕电视剧| 天天撸天天日| 波多野结衣无码流出| 人人妻人人澡人人爽人人DVD | 欧美精品久久久| 亚洲国产激情| 国产综合久久久777777色胡同| 五月天青青草超碰免费公开在线观看 | 91无码一区二区三区在线| 国模精品无码一区二区免费蜜桃| 久草在线播放| 国产A片大全| 成人影片亚洲| 99视频精品| 久久艹大香蕉| 一本加勒比HEZYO东京热无码| 国产精品探花熟女AV| 日本A片| 亚洲免费在线视频观看| 先锋av资源在线| 成人做爰100片免费看| 四川BBB搡BBB搡多人乱| 91精品成人电影| 国产精品欧美激情| 波多野结衣久久| 无码视频在线观看免费| 真人一级片| 久久国产激情| 精品一区二区三区四区五区六区七区八区九区 | 日本免费在线观看视频| 亚洲欧美综合| www.色五月| 91AV成人| 在线播放国产精品| 亚洲天堂在线观看视频网站| 丁香六月婷婷久久综合| 波多野结衣在线无码| 国产精品久久久久精| 91影音先锋| 97视频福利| 免费在线观看黄片| a免费视频| 无码三| 七十路の高齢熟女千代子下载| 亚洲国产免费视频| 抠骚逼| 中文字幕在线观看网站| 成人视频免费| 国产精品一级a毛一级a| 老师搡BBBB搡BBB| 日本一区二区精品| 天天日很很操| 77Q视频| 五月丁香伊人| 亚洲天堂欧美| 久草性爱| 国产成人精品久久| 91免费观看网站| 中文字幕自拍偷拍| 亚洲第一区欧美日韩| 日韩无码影院| 黄片二区| 丰满人妻精品一区二区在线 | 欧美日韩操逼视频| 欧美亚洲日韩一区二区三区| 国产欧美日韩在线视频| 国产午夜精品一区二区三区四区| 俺也来俺也去WWW色| 无码三级AV| 亚洲日色| 韩国毛片| 成人香蕉网| 岛国av在线播放| 日老女人的逼| 一道本高清无码视频| 人人妻人人玩人人澡人人爽| 大香蕉在8线| 午夜ww| 秋霞丝鲁片一区二区三区手机在绒免| 尤物视频入口| 日韩av电影免费在线观看| 国产盗摄AV| 精品人妻一区二区三区四区 | 东京热一区二区三区| 逼特逼视频在线观看| 人人艹人人| 2025精品视频| 国产精品视频网站| 亚洲视频网站在线观看| 天干夜天干天天天爽视频| 国产一级a一片成人AV| 日韩AV无码成人精品| 91人妻人人澡人人添人人爽| 天天日天天干天天草| 欧美乱伦一区| 在线观看无码高清视频| 超碰九九热| 天天干,夜夜爽| www国产在线|