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

Webpack 實現 Tree shaking 的前世今生

開發 前端
Tree-shaking 實現機制快速瀏覽完官方文檔和一眾文章后,發現 webpack 實現 tree-shaking 的方式還不止一種!但是,都與 rollup 不同。

[[407597]]

前言

如果看過 rollup 系列的這篇文章 - 無用代碼去哪了?項目減重之 rollup 的 Tree-shaking,那你一定對 tree-shaking 不陌生了。如果對 tree-shaking 相關知識不熟悉,請先點開上面這篇文章花 5 分鐘了解一下:什么是 tree-shaking。

眾所周知,原本不支持 tree-shaking 的 Webpack 在它的 2.x 版本也實現了 tree-shaking,好奇心又來了,rollup 從一開始就自實現了 tree-shaking,而 Webpack 則是看到 rollup 的打包瘦身效果之后,到了 2.x 才實現,那么二者實現 tree-shaking 的原理是一樣的嗎?

因為這樣的疑問,就有了眼前這篇文章。

Tree-shaking 實現機制

快速瀏覽完官方文檔和一眾文章后,發現 webpack 實現 tree-shaking 的方式還不止一種!但是,都與 rollup 不同。

早期 webpack 的配置使用并不簡單,也因此曾有 webpack 配置工程師的戲稱,雖然現在 webpack 的配置被極大簡化了,webpack4 也宣稱 0 配置,但如果涉及復雜全面的打包功能,并非是 0 配置可以實現的。了解其功能原理及配置還是極為有用的,接下來就來了解一下 webpack 實現 tree-shaking 的原理吧。

Tree-shaking -- rollup VS Webpack

  • rollup 是在編譯打包過程中分析程序流,得益于于 ES6 靜態模塊(exports 和 imports 不能在運行時修改),我們在打包時就可以確定哪些代碼時我們需要的。
  • webpack 本身在打包時只能標記未使用的代碼而不移除,而識別代碼未使用標記并完成 tree-shaking 的 其實是 UglifyJS、babili、terser 這類壓縮代碼的工具。簡單來說,就是壓縮工具讀取 webpack 打包結果,在壓縮之前移除 bundle 中未使用的代碼。

我們提到了標記未使用代碼,也提到了 UglifyJS、babili、terser 等壓縮工具,那么 webpack 與壓縮工具是怎么實現 tree-shaking 的呢?先來了解下 webpack 中實現 tree-shaking 的前世今生吧!

Webpack 實現 tree-shaking 的 3 個階段

第一階段:UglifyJS

webpack 標記代碼 + babel 轉譯 ES5 --> UglifyJS 壓縮刪除無用代碼 關于最早版本的 Webpack 實現 tree-shaking 可以參考這篇文章 如何在 Webpack 2 中使用 tree-shaking(鏈接地址見文末參考),掘金也有翻譯版,當然如果不愿意花時間考古,也可以看下面這一段總結:

  • UglifyJS 不支持 ES6 及以上,需要用 Babel 將代碼編譯為 ES5,然后再用 UglifyJS 來清除無用代碼;
  • 通過 Babel 將代碼編譯為 ES5,但又要讓 ES6 模塊不受 Babel 預設(preset)的影響:配置 Babel 預設不轉換 module,對應地配置 Webpack 的 plugins 配置;
  • 為避免副作用,將其標記為 pure(無副作用),以便 UglifyJS 能夠處理,主要是 webpack 的編譯過程阻止了對類進行 tree-shaking,它僅對函數起作用,后來通過支持將類編譯后的賦值標記為 @__PURE__解決了這個問題。
  1. // .babelrc 
  2.   "presets": [ 
  3.     ["env", { 
  4.       "loose"true, // 寬松模式 
  5.       "modules"false // 不轉換 module,保持 ES6 語法 
  6.     }] 
  7.   ] 
  1. // webpack.config.js 
  2. module: { 
  3.   rules: [ 
  4.     { test: /\.js$/, loader: 'babel-loader' } 
  5.   ] 
  6. }, 
  7.  
  8. plugins: [ 
  9.   new webpack.LoaderOptionsPlugin({ 
  10.     minimize: true
  11.     debug: false 
  12.   }), 
  13.   new webpack.optimize.UglifyJsPlugin({ 
  14.     compress: { 
  15.       warnings: true 
  16.     }, 
  17.     output: { 
  18.       comments: false 
  19.     }, 
  20.     sourceMap: false 
  21.   }) 

第二階段:BabelMinify

webpack 標記代碼 --> Babili(即 BabelMinify)壓縮刪除無用代碼 Babili 后來被重命名為 BabelMinify,是基于 Babel 的代碼壓縮工具,而 Babel 已經通過我們的解析器 Babylon 理解了新語法,同時又在 babili 中集成了 UglifyJS 的壓縮功能,本質上實現了和 UglifyJS 一樣的功能,但使用 babili 插件又不必再轉譯,而是直接壓縮,使代碼體積更小。

一般使用 Babili 替代 uglify 有 Babili 插件式和 babel-loader 預設兩種方式。在官方文檔最后有說明,Babel Minify 最適合針對最新的瀏覽器(具有完整的 ES6+ 支持),也可以與通常的 Babel es2015 預設一起使用,以首先向下編譯代碼。

在 webpack 中使用 babel-loader,然后再引入 minify 作為一個 preset 會比直接使用 BabelMinifyWebpackPlugin 插件(下一個就講到)執行得更快。因為 babel-minify 處理的文件體積會更小。

第三階段:Terser

webpack 標記代碼 --> Terser 壓縮刪除無用代碼 (webpack5 已內置) terser 是一個用于 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。如果你看過這個 issue(https://github.com/webpack-contrib/terser-webpack-plugin/issues/15),就會知道放棄 uglify 而投向 terser 懷抱的人越來越多,其原因也很清楚:

  • uglify 不再進行維護且不支持 ES6+ 語法
  • webpack 默認內置配置了 terser 插件實現代碼壓縮 關于副作用,從 webpack 4 正式版本擴展了未使用模塊檢測能力,通過 package.json 的 "sideEffects" 屬性作為標記,向 compiler 提供提示,表明項目中的哪些文件是 "pure(純正 ES2015 模塊)",由此可以安全地刪除文件中未使用的部分。

webpack4 的時候還要手動配置一下壓縮插件,但最新的 webpack5 已經內置實現 tree-shaking 啦!在生產環境下無需配置即可實現 tree-shaking !

Webpack 的 Tree-shaking 流程

Webpack 標記代碼

總的來說,webpack 對代碼進行標記,主要是對 import & export 語句標記為 3 類:

  • 所有 import 標記為 /* harmony import */
  • 所有被使用過的 export 標記為/* harmony export ([type]) */,其中 [type] 和 webpack 內部有關,可能是 binding, immutable 等等
  • 沒被使用過的 export 標記為/* unused harmony export [FuncName] */,其中 [FuncName] 為 export 的方法名稱

首先我們要知道,為了正常運行業務項目,Webpack 需要將開發者編寫的業務代碼以及支撐、調配這些業務代碼的運行時一并打包到產物(bundle)中。落到 Webpack 源碼實現上,運行時的生成邏輯可以劃分為打包階段中的兩個步驟:

  • 依賴收集:遍歷代碼模塊并收集模塊的特性依賴,從而確定整個項目對 Webpack runtime 的依賴列表;
  • 生成:合并 runtime 的依賴列表,打包到最終輸出的 bundle。

顯然,對代碼的語句標記就發生在依賴收集的過程中。

在運行時環境標記所有 import:

  1. const exportsType = module.getExportsType( 
  2.  chunkGraph.moduleGraph, 
  3.  originModule.buildMeta.strictHarmonyModule 
  4. ); 
  5. runtimeRequirements.add(RuntimeGlobals.require); 
  6. const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`; 
  7.  
  8. // 動態導入語法分析 
  9. if (exportsType === "dynamic") { 
  10.  runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport); 
  11.  return [ 
  12.   importContent, // 標記/* harmony import */ 
  13.   `/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar});\n` // 通過 /*#__PURE__*/ 注釋可以告訴 webpack 一個函數調用是無副作用的 
  14.  ]; // 返回 import 語句和 compat 語句 

在運行時環境標記所有被使用過的和未被使用的 export:

  1. // 在運行時狀態定義 property getters 
  2.  generate() { 
  3.  const { runtimeTemplate } = this.compilation; 
  4.  const fn = RuntimeGlobals.definePropertyGetters; 
  5.  return Template.asString([ 
  6.   "// define getter functions for harmony exports"
  7.   `${fn} = ${runtimeTemplate.basicFunction("exports, definition", [ 
  8.    `for(var key in definition) {`, 
  9.    Template.indent([ 
  10.     `if(${RuntimeGlobals.hasOwnProperty}(definition, key) && !${RuntimeGlobals.hasOwnProperty}(exports, key)) {`, 
  11.     Template.indent([ 
  12.      "Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });" 
  13.     ]), 
  14.     "}" 
  15.    ]), 
  16.    "}" 
  17.   ])};` 
  18.  ]); 
  19.   
  20.  // 輸入為 generate 上下文 
  21.  getContent({ runtimeTemplate, runtimeRequirements }) { 
  22.  runtimeRequirements.add(RuntimeGlobals.exports); 
  23.  runtimeRequirements.add(RuntimeGlobals.definePropertyGetters); 
  24.  
  25.  const unusedPart = 
  26.   this.unusedExports.size > 1 
  27.    ? `/* unused harmony exports ${joinIterableWithComma( 
  28.      this.unusedExports 
  29.      )} */\n` 
  30.    : this.unusedExports.size > 0 
  31.    ? `/* unused harmony export ${first(this.unusedExports)} */\n` 
  32.    : ""
  33.  const definitions = []; 
  34.  for (const [key, value] of this.exportMap) { 
  35.   definitions.push( 
  36.    `\n/* harmony export */   ${JSON.stringify( 
  37.     key 
  38.    )}: ${runtimeTemplate.returningFunction(value)}` 
  39.   ); 
  40.  } 
  41.  const definePart = 
  42.   this.exportMap.size > 0 
  43.    ? `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${ 
  44.      this.exportsArgument 
  45.      }, {${definitions.join(",")}\n/* harmony export */ });\n` 
  46.    : ""
  47.  return `${definePart}${unusedPart}`; // 作為初始化代碼包含的源代碼 

壓縮清除大法

UglifyJS

以 UglifyJS 為例,UglifyJS 是一個 js 解釋器、最小化器、壓縮器、美化器工具集(parser, minifier, compressor or beautifier toolkit)。具體介紹可以查看下 UglifyJS 中文手冊。

如果不想瀏覽這么一大長篇文檔,可以看干凈利落、直指 tree-shaking 的壓縮配置參數總結吧!

  1. dead_code -- 移除沒被引用的代碼 // 是不是很眼熟!無用代碼!
  2. drop_debugger -- 移除 debugger
  3. unused -- 干掉沒有被引用的函數和變量。(除非設置"keep_assign",否則變量的簡單直接賦值也不算被引用。)
  4. toplevel -- 干掉頂層作用域中沒有被引用的函數 ("funcs")和/或變量("vars") (默認是 false , true 的話即函數變量都干掉)
  5. warnings -- 當刪除沒有用處的代碼時,顯示警告 // 還挺貼心有么有~
  6. pure_getters -- 默認是 false. 如果你傳入 true,UglifyJS 會假設對象屬性的引用(例如 foo.bar 或 foo["bar"])沒有函數副作用。
  7. pure_funcs -- 默認 null. 你可以傳入一個名字的數組,UglifyJS 會假設這些函數沒有函數副作用。

舉個栗子:

  1. plugins: [ 
  2.   new UglifyJSPlugin({ 
  3.     uglifyOptions: { 
  4.       compress: { 
  5.           // 這樣該函數會被認為沒有函數副作用,整個聲明會被廢棄。在目前的執行情況下,會增加開銷(壓縮會變慢)。 
  6.           pure_funcs: ['Math.floor'
  7.       } 
  8.     } 
  9.   }) 
  10. ], 

Tip:假如名字在作用域中重新定義,不會再次檢測。例如 var q = Math.floor(a/b),假如變量 q 沒有被引用,UglifyJS 會干掉它,但 Math.floor(a/b)會被保留,沒有人知道它是干嘛的。

  • side_effects -- 默認 true. 傳 false 禁用丟棄純函數。如果一個函數被調用前有一段/@PURE/ or /#PURE/ 注釋,該函數會被標注為純函數。例如 /@PURE/foo();

事實上,在這么多的壓縮配置中,除了要解決副作用問題要手動配置以外,僅使用 UglifyJS 默認配置即可去除無用標記代碼以實現 tree-shaking。

terser

以 terser 為例,terser 是一個用于 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。具體可查看官方文檔。雖然沒有中文文檔,但是一眼掃過去也可以看出來配置參數和 UglifyJS 沒有太大區別。當然很明顯地多了一些參數:

  • arrows -- 如果轉換后的代碼更短,類和對象字面量方法也將被轉換為箭頭表達式
  • ecma -- 通過 ES2015 或 更高版本來啟用壓縮選項,將 ES5 代碼轉換為更小的 ES6+等效形式 顯然是因為 terser 支持 ES6+ 語法,這也是它淘汰 UglifyJS 的優勢之一。

壓縮性能 PK

目前 Webpack 已經更新到了版本 5.X,已經將 terser 插件默認內置且無需配置,雖然生產環境下默認使用 TerserPlugin ,并且也是代碼壓縮方面比較好的選擇,但是還有一些其他可選擇項。等等,我們的主題不是 tree-shaking 嗎?怎么在壓縮工具的路上突然越走越遠...

本質上,實現 tree-shaking 的還是壓縮工具,所以我們來看壓縮工具的性能好像也沒毛病!

TIP:壓縮是在生產環境中生效的,所以生產環境下才能 tree-shaking。下面 3 個可配置插件要求 webpack 版本至少在 V4+。

UglifyjsWebpackPlugin

基本的使用方式也更加簡單:

  1. // webpack.config.js 
  2. const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 
  3.  
  4. module.exports = { 
  5.   optimization: { 
  6.     minimizer: [new UglifyJsPlugin()], 
  7.   }, 
  8. }; 
  9.  
  10. const UglifyJsPlugin = require('uglifyjs-webpack-plugin'
  11.  
  12. module.exports = { 
  13.   plugins: [ 
  14.     new UglifyJsPlugin() 
  15.   ] 

 

BabelMinifyWebpackPlugin

一般使用 babili 替代 UglifyJS 有 Babili 插件式和 babel-loader 預設兩種方式。

Babili 插件式

只要用 Babili 插件替代 uglify 即可,此時也不需要 babel-loader 了:

  1. // webpack.config.js 
  2. const MinifyPlugin = require("babel-minify-webpack-plugin"); 
  3. module.exports = { 
  4.   plugins: [ 
  5.     new MinifyPlugin(minifyOpts, pluginOpts) 
  6.   ] 

babel-loader 預設

在官方文檔最后有說明,Babel Minify 最適合針對最新的瀏覽器(具有完整的 ES6+ 支持),也可以與通常的 Babel es2015 預設一起使用,以首先向下編譯代碼。

在 webpack 中使用 babel-loader,然后再引入 minify 作為一個 preset 會比直接使用 BabelMinifyWebpackPlugin 插件執行得更快。因為 babel-minify 處理的文件體積會更小。

即在.babelrc 中配置如下:

  1.   "presets": ["es2015"], 
  2.   "env": { 
  3.     "production": { 
  4.       "presets": ["minify"
  5.     } 
  6.   } 

但 BabelMinifyWebpackPlugin 插件存在必定有其無法替代的作用:

  • webpack loader 對單個文件進行操作, minify preset 作為一個 webpack loader 會把每個文件視為在瀏覽器全局范圍內直接執行(默認情況下),并不會優化頂級作用域內的某些內容;
  • 當排除 node_modules 不通過 babel-loader 運行時,babel-minify 優化不會應用于被排除的文件;
  • 當使用 babel-loader 時,由 webpack 為模塊系統生成的代碼不會通過 babel-minify 進行優化;
  • webpack 插件可以在整個 chunk/bundle 輸出上運行,并且可以優化整個 bundle。

采用第一種方式:

TerserWebpackPlugin

同 uglify 和 babelMinify 插件一樣,terser 插件配置使用也十分簡單。

  1. webpack.config.js 
  2. const TerserPlugin = require("terser-webpack-plugin"); 
  3.  
  4. module.exports = { 
  5.   optimization: { 
  6.     minimize: true
  7.     minimizer: [new TerserPlugin()], 
  8.   }, 
  9. }; 

 

企業微信截圖_16247735356260.png

看上去結果是符合預期的,又因為我的文件代碼本身體積就小,所以壓縮包體積上的優勢其實并不明顯,但壓縮時間上還是比較明顯的。

官方數據性能對比

再來康康 bableMinify 文檔 中給出的對比吧:

打包 react:

圖片

打包 vue:

打包 lodash:

圖片

打包 three.js:

圖片

小結

先讓我們來看看 issue 區網友們是怎么說的:

圖片

大意是 terser 壓縮性能相較于 uglify 提升了三倍!Nice!

大意是說:鑒于 terser-webpack-plugin 得到維護并且有更多的正確性修復,絕對是首選 -- 即使沒有性能改進(事實上還是有所改進的),也值得切換。最后一句話總結:webpack 打包 + terser 壓縮才是最終的不二之選!webpack5 內置 terser 說明了一切!

處理 Side Effects

「副作用」的定義是,在導入時會執行特殊行為的代碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全局作用域,并且通常不提供 export。

關于副作用在 rollup 中也已經介紹過。有些模塊導入,只要被引入,就會對應用程序產生重要的影響。比如全局樣式表,或者設置全局配置的 JavaScript 文件就是很好的例子。

Webpack 認為這樣的文件有“副作用”,具有副作用的文件不應該做 tree-shaking,因為這將破壞整個應用程序。webpack 的 tree-shaking 在副作用處理方面稍顯遜色,它可以簡單的判斷變量后續是否被引用、修改,但是不能判斷一個變量完整的修改過程,不知道它是否已經指向了外部變量,所以很多有可能會產生副作用的代碼,都只能保守的不刪除。

幸運的是,我們可以通過配置項目,告訴 Webpack 哪些代碼是沒有副作用的,可以進行 tree-shaking。

配置參數

在項目的 package.json 文件中,添加 "sideEffects" 屬性。package.json 有一個特殊的屬性 sideEffects,就是為處理副作用而存在的 -- 向 webpack 的 compiler 提供提示哪些代碼是“純粹部分”。它有三個可能的值:

  • true 是默認值,如果不指定其他值的話。這意味著所有的文件都有副作用,也就是沒有一個文件可以 tree-shaking。
  • false 告訴 Webpack 沒有文件有副作用,所有文件都可以 tree-shaking。
  • 第三個值 […] 是文件路徑數組。它告訴 webpack,除了數組中包含的文件外,你的任何文件都沒有副作用。因此,除了指定的文件之外,其他文件都可以安全地進行 tree-shaking。
  1.   "name""your-project"
  2.   "sideEffects"false 
  3.   // "sideEffects": [ // 數組方式支持相關文件的相對路徑、絕對路徑和 glob 模式 
  4.   //  "./src/some-side-effectful-file.js"
  5.   //  "*.css" 
  6.   //] 

每個項目都必須將 sideEffects 屬性設置為 false 或文件路徑數組,如果你的代碼確實有一些副作用,那么可以改為提供一個數組,在工作中需要正確配置 sideEffects 標記。

代碼中標記

可以通過 /#PURE/ 注釋可以告訴 webpack 一個函數調用是無副作用的。在函數調用之前,用來標記它們是無副作用的(pure)。傳到函數中的入參是無法被剛才的注釋所標記,需要單獨每一個標記才可以。如果一個沒被使用的變量定義的初始值被認為是無副作用的(pure),它會被標記為死代碼,不會被執行且會被壓縮工具清除掉。當 optimization.innerGraph 被設置成 true 這個行為被會開啟,而在 webpack5.x 中optimization.innerGraph 默認為 true。

語法使用層面

  • 首先,mode 為 production 模式下才會啟用更多優化項,包括我們本文講的壓縮代碼與 tree shaking;
  • 使用 ES2015 模塊語法(即 import 和 export);
  • 確保沒有編譯器將 ES2015 模塊語法轉換為 CommonJS 的,把 presets 中的 modules 設置為 false,告訴 babel 不要編譯模塊代碼。

總結

  • 如果是開發 JavaScript 庫,使用 rollup!并且提供 ES6 module 的版本,入口文件地址設置到 package.json 的 module 字段;
  • 使用 webpack 哪怕是舊版本可以優先考慮 terser 插件作為壓縮工具;
  • 為避免副作用,盡量不寫帶有副作用的代碼,使用 ES2015 模塊語法;
  • 在項目 package.json 文件中,添加一個 sideEffects 入口,設置 sideEffects 屬性為 false,也可以通過 /#PURE/ 注釋強制刪除一些認為不會產生副作用的代碼;
  • 在 Webpack 中還要額外引入一個能夠刪除未引用代碼(dead code)的壓縮工具(eg. Terser)。

參考資料

  • 如何在 Webpack 2 中使用 tree-shaking()https://blog.craftlab.hu/how-to-do-proper-tree-shaking-in-webpack-2-e27852af8b21
  • 你的 Tree-Shaking 并沒什么卵用(https://zhuanlan.zhihu.com/p/32831172)
  • UglifyJS 中文手冊(https://github.com/LiPinghai/UglifyJSDocCN/blob/master/README.md)
  • Webpack 4 Tree Shaking 終極優化指南(https://juejin.cn/post/6844903998634328072#heading-5)
  • Webpack 中文文檔 Tree-shaking(https://www.webpackjs.com/guides/tree-shaking/)

 

責任編輯:姜華 來源: 微醫大前端技術
相關推薦

2022-02-10 14:23:16

WebpackJavaScript

2021-08-26 10:30:29

WebpackTree-Shakin前端

2011-08-23 09:52:31

CSS

2015-11-18 14:14:11

OPNFVNFV

2014-07-30 10:55:27

2025-02-12 11:25:39

2014-07-21 12:57:25

諾基亞微軟裁員

2019-06-04 09:00:07

Jenkins X開源開發人員

2014-07-15 10:31:07

asyncawait

2016-12-29 18:21:01

2021-06-17 07:08:19

Tapablewebpack JavaScript

2016-12-29 13:34:04

阿爾法狗圍棋計算機

2012-05-18 16:54:21

FedoraFedora 17

2013-05-23 16:23:42

Windows Azu微軟公有云

2016-11-03 13:33:31

2016-11-08 19:19:06

2021-04-15 07:01:28

區塊鏈分布式DLT

2011-05-13 09:43:27

產品經理PM

2015-06-11 11:10:09

對象存儲云存儲

2019-08-05 10:08:25

軟件操作系統程序員
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产人久久人人人人爽 | 丁香久久 | 中国一级毛片免费 | 狠狠色狠狠色综合日日92 | 国产精品国产精品国产专区不片 | 91网站在线看 | 99精品一区二区三区 | 九九热国产精品视频 | 欧美自拍日韩 | 日韩中文字幕在线视频观看 | 欧美日韩一区在线 | 91精品国产高清久久久久久久久 | 久久精品亚洲国产 | 国产一区二区黑人欧美xxxx | 91精品久久久久久久久 | 国产精品久久久久aaaa九色 | 精品在线一区 | 欧美极品视频在线观看 | 日日干干| 国产精品3区 | 国产黄色大片在线免费观看 | 亚洲精品乱码久久久久久按摩观 | 亚洲一区二区在线播放 | 国产丝袜一区二区三区免费视频 | 欧洲精品码一区二区三区免费看 | www.精品一区 | 免费观看色 | 国产精品久久久久久久久久免费 | 国产精品欧美一区二区 | 国产在线精品一区二区 | 日日操操| 日日日操 | 午夜男人的天堂 | 日韩中文一区二区三区 | 一区二区在线不卡 | 999精品在线观看 | 欧美激情一区二区三区 | 一区二区在线不卡 | 韩日在线观看视频 | 一区二区三区在线 | 狠狠干天天干 |