webpack的打包和性能優(yōu)化

webpack的打包和性能優(yōu)化

tree shaking

tree shaking 是一個(gè)術(shù)語,通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴于 ES2015 模塊系統(tǒng)中的靜態(tài)結(jié)構(gòu)特性,例如 importexport。

所謂的“未引用代碼(dead code)”,也就是說,應(yīng)該刪除掉未被引用的 export,但是它仍然被包含在 bundle 中,優(yōu)化體積

解決方法

將文件標(biāo)記為無副作用(side-effect-free)

通過 package.json 的 "sideEffects" 屬性來實(shí)現(xiàn)的

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

// 如果所有代碼都不包含副作用,我們就可以簡單地將該屬性標(biāo)記為 false,來告知 webpack,它可以安全地刪除未用到的 export 導(dǎo)出
{
  "name": "your-project",
  "sideEffects": false
}

// 如果你的代碼確實(shí)有一些副作用,那么可以改為提供一個(gè)數(shù)組

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}

壓縮輸出

通過如上方式,我們已經(jīng)可以通過 importexport 語法,找出那些需要?jiǎng)h除的“未使用代碼(dead code)”,然而,我們不只是要找出,還需要在 bundle 中刪除它們。為此,我們將使用 -p(production) 這個(gè) webpack 編譯標(biāo)記,來啟用 uglifyjs 壓縮插件

注意,--optimize-minimize 標(biāo)記也會(huì)在 webpack 內(nèi)部調(diào)用 UglifyJsPlugin。

從 webpack 4 開始,也可以通過 "mode" 配置選項(xiàng)輕松切換到壓縮輸出,只需設(shè)置為 "production"

為了學(xué)會(huì)使用 tree shaking,你必須……

  • 使用 ES2015 模塊語法(即 importexport)。
  • 在項(xiàng)目 package.json 文件中,添加一個(gè) "sideEffects" 入口。
  • 引入一個(gè)能夠刪除未引用代碼(dead code)的壓縮工具(minifier)(例如 UglifyJSPlugin)。

代碼分離

代碼分離是 webpack 中最引人注目的特性之一。此特性能夠把代碼分離到不同的 bundle 中,然后可以按需加載或并行加載這些文件。代碼分離可以用于獲取更小的 bundle,以及控制資源加載優(yōu)先級(jí),如果使用合理,會(huì)極大影響加載時(shí)間。(優(yōu)化加載時(shí)間)

入口起點(diǎn)(entry points)

這是迄今為止最簡單、最直觀的分離代碼的方式。

project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

console.log(
  _.join(['Another', 'module', 'loaded!'], ' ')
);

webpack.config.js

const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

這種方法存在一些問題:

  • 如果入口 chunks 之間包含重復(fù)的模塊,那些重復(fù)模塊都會(huì)被引入到各個(gè) bundle 中。
  • 這種方法不夠靈活,并且不能將核心應(yīng)用程序邏輯進(jìn)行動(dòng)態(tài)拆分代碼。

以上兩點(diǎn)中,第一點(diǎn)對我們的示例來說無疑是個(gè)問題,因?yàn)橹拔覀冊?./src/index.js 中也引入過 lodash,這樣就在兩個(gè) bundle 中造成重復(fù)引用。接著,我們通過使用 CommonsChunkPlugin 來移除重復(fù)的模塊。

防止重復(fù)(prevent duplication)

SplitChunksPlugin允許我們共同的依賴提取到一個(gè)現(xiàn)有的條目塊或一個(gè)全新的塊。讓我們用它來重復(fù)lodash上一個(gè)例子的依賴:

webpack.config.js

  const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
   optimization: {
     splitChunks: {
       chunks: 'all'
     }
   }
  };

有了optimization.splitChunks配置選項(xiàng),我們現(xiàn)在應(yīng)該看到從我們的index.bundle.js和中刪除了重復(fù)的依賴項(xiàng)another.bundle.js。該插件應(yīng)該注意到我們已經(jīng)分離lodash出一個(gè)單獨(dú)的塊并從我們的主包中移除了自重。

動(dòng)態(tài)導(dǎo)入(dynamic imports)

當(dāng)涉及到動(dòng)態(tài)代碼拆分時(shí),webpack 提供了兩個(gè)類似的技術(shù)。對于動(dòng)態(tài)導(dǎo)入,第一種,也是優(yōu)先選擇的方式是,使用符合 ECMAScript 提案import() 語法。第二種,則是使用 webpack 特定的 require.ensure。

import() 調(diào)用會(huì)在內(nèi)部用到 promises。如果在舊有版本瀏覽器中使用 import(),記得使用 一個(gè) polyfill 庫(例如 es6-promisepromise-polyfill),來 shim Promise。

現(xiàn)在,我們不再使用靜態(tài)導(dǎo)入 lodash,而是通過使用動(dòng)態(tài)導(dǎo)入來分離一個(gè) chunk:

src/index.js


 function getComponent() {
   return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
     var element = document.createElement('div');

     element.innerHTML = _.join(['Hello', 'webpack'], ' ');

     return element;

   }).catch(error => 'An error occurred while loading the component');
  }


 getComponent().then(component => {
   document.body.appendChild(component);
 })

注意,在注釋中使用了 webpackChunkName。這樣做會(huì)導(dǎo)致我們的 bundle 被命名為 lodash.bundle.js ,而不是 [id].bundle.js 。想了解更多關(guān)于 webpackChunkName 和其他可用選項(xiàng),請查看 import() 相關(guān)文檔。讓我們執(zhí)行 webpack,查看 lodash 是否會(huì)分離到一個(gè)單獨(dú)的 bundle:

由于 import() 會(huì)返回一個(gè) promise,因此它可以和 async 函數(shù)一起使用。但是,需要使用像 Babel 這樣的預(yù)處理器和Syntax Dynamic Import Babel Plugin。下面是如何通過 async 函數(shù)簡化代碼:

src/index.js


 async function getComponent() {
   var element = document.createElement('div');
   const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');

   element.innerHTML = _.join(['Hello', 'webpack'], ' ');

   return element;
  }

  getComponent().then(component => {
    document.body.appendChild(component);
  });

緩存

輸出文件的文件名(Output Filenames)

通過使用 output.filename 進(jìn)行文件名替換,可以確保瀏覽器獲取到修改后的文件。[hash] 替換可以用于在文件名中包含一個(gè)構(gòu)建相關(guān)(build-specific)的 hash,但是更好的方式是使用 [chunkhash] 替換,在文件名中包含一個(gè) chunk 相關(guān)(chunk-specific)的哈希

project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
|- /node_modules

webpack.config.js

  const path = require('path');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
       title: 'Caching'
      })
    ],
    output: {
    filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    }
  };

bundle 的名稱是它內(nèi)容(通過 hash)的映射。如果我們不做修改,然后再次運(yùn)行構(gòu)建,我們以為文件名會(huì)保持不變。然而,如果我們真的運(yùn)行,可能會(huì)發(fā)現(xiàn)情況并非如此:(譯注:這里的意思是,如果不做修改,文件名可能會(huì)變,也可能不會(huì)。)

提取模板(Extracting Boilerplate)

SplitChunksPlugin可以使用它將模塊拆分為單獨(dú)的包。webpack提供了一個(gè)優(yōu)化功能,它根據(jù)提供的選項(xiàng)將運(yùn)行時(shí)代碼拆分為單獨(dú)的塊,只需使用optimization.runtimeChunkset來single創(chuàng)建一個(gè)運(yùn)行時(shí)包:

webpack.config.js

  const path = require('path');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Caching'
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    },
   optimization: {
     runtimeChunk: 'single'
  }
  };

將第三方庫(library)(例如 lodashreact)提取到單獨(dú)的 vendor chunk 文件中,是比較推薦的做法,這是因?yàn)?,它們很少像本地的源代碼那樣頻繁修改。因此通過實(shí)現(xiàn)以上步驟,利用客戶端的長效緩存機(jī)制,可以通過命中緩存來消除請求,并減少向服務(wù)器獲取資源,同時(shí)還能保證客戶端代碼和服務(wù)器端代碼版本一致。

webpack.config.js

  var path = require('path');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Caching'
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    },
    optimization: {

     runtimeChunk: 'single',
     splitChunks: {
      cacheGroups: {
         vendor: {
           test: /[\\/]node_modules[\\/]/,
           name: 'vendors',
           chunks: 'all'
         }
       }
     }
    }
  }

模塊標(biāo)識(shí)符(Module Identifiers)

  • main捆綁包因其新內(nèi)容而發(fā)生變化。
  • vendor包更改,因?yàn)樗?code>module.id改變了。
  • 而且,runtime捆綁包已更改,因?yàn)樗F(xiàn)在包含對新模塊的引用。

第一個(gè)和最后一個(gè)是預(yù)期的 - 這是vendor我們想要解決的哈希值。幸運(yùn)的是,我們可以使用兩個(gè)插件來解決此問題。第一個(gè)是NamedModulesPlugin,它將使用模塊的路徑而不是數(shù)字標(biāo)識(shí)符。雖然此插件在開發(fā)期間對于更易讀的輸出非常有用,但運(yùn)行起來需要更長的時(shí)間。第二個(gè)選項(xiàng)是HashedModuleIdsPlugin,建議用于生產(chǎn)構(gòu)建:

  const path = require('path');
+ const webpack = require('webpack');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Caching'
      }),
+      new webpack.HashedModuleIdsPlugin()
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    },
    optimization: {
      runtimeChunk: 'single',
      splitChunks: {
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      }
    }
  };

如果改變項(xiàng)目代碼,依賴不變的保持runtime 和vendor的id不變,緩存

shimming

webpack 編譯器(compiler)能夠識(shí)別遵循 ES2015 模塊語法、CommonJS 或 AMD 規(guī)范編寫的模塊。然而,一些第三方的庫(library)可能會(huì)引用一些全局依賴(例如 jQuery 中的 $)。這些庫也可能創(chuàng)建一些需要被導(dǎo)出的全局變量。這些“不符合規(guī)范的模塊”就是 shimming 發(fā)揮作用的地方。

我們不推薦使用全局的東西!在 webpack 背后的整個(gè)概念是讓前端開發(fā)更加模塊化。也就是說,需要編寫具有良好的封閉性(well contained)、彼此隔離的模塊,以及不要依賴于那些隱含的依賴模塊(例如,全局變量)。請只在必要的時(shí)候才使用本文所述的這些特性。

shimming 另外一個(gè)使用場景就是,當(dāng)你希望 polyfill 瀏覽器功能以支持更多用戶時(shí)。在這種情況下,你可能只想要將這些 polyfills 提供給到需要修補(bǔ)(patch)的瀏覽器(也就是實(shí)現(xiàn)按需加載)。

shimming 全局變量

使用 ProvidePlugin 后,能夠在通過 webpack 編譯的每個(gè)模塊中,通過訪問一個(gè)變量來獲取到 package 包。如果 webpack 知道這個(gè)變量在某個(gè)模塊中被使用了,那么 webpack 將在最終 bundle 中引入我們給定的 package。讓我們先移除 lodashimport 語句,并通過插件提供它:

src/index.js

- import _ from 'lodash';
-
  function component() {
    var element = document.createElement('div');

-   // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

webpack.config.js

  const path = require('path');
+ const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
-   }
+   },
+   plugins: [
+     new webpack.ProvidePlugin({
+       _: 'lodash'
+     })
+   ]
  };

本質(zhì)上,我們所做的,就是告訴 webpack……

如果你遇到了至少一處用到 lodash 變量的模塊實(shí)例,那請你將 lodash package 包引入進(jìn)來,并將其提供給需要用到它的模塊。

我們還可以使用 ProvidePlugin 暴露某個(gè)模塊中單個(gè)導(dǎo)出值,只需通過一個(gè)“數(shù)組路徑”進(jìn)行配置(例如 [module, child, ...children?])。所以,讓我們做如下設(shè)想,無論 join 方法在何處調(diào)用,我們都只會(huì)得到的是 lodash 中提供的 join 方法。

src/index.js

  function component() {
    var element = document.createElement('div');

-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    plugins: [
      new webpack.ProvidePlugin({
-       _: 'lodash'
+       join: ['lodash', 'join']
      })
    ]
  };

這樣就能很好的與 tree shaking 配合(壓縮),將 lodash 庫中的其他沒用到的部分去除。

細(xì)粒度 shimming

一些傳統(tǒng)的模塊依賴的 this 指向的是 window 對象。在接下來的用例中,調(diào)整我們的 index.js

  function component() {
    var element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');
+
+   // Assume we are in the context of `window`
+   this.alert('Hmmm, this probably isn\'t a great idea...')

    return element;
  }

  document.body.appendChild(component());

當(dāng)模塊運(yùn)行在 CommonJS 環(huán)境下這將會(huì)變成一個(gè)問題,也就是說此時(shí)的 this 指向的是 module.exports。在這個(gè)例子中,你可以通過使用 imports-loader 覆寫 this

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
+   module: {
+     rules: [
+       {
+         test: require.resolve('index.js'),
+         use: 'imports-loader?this=>window'
+       }
+     ]
+   },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join']
      })
    ]
  };

全局 exports

讓我們假設(shè),某個(gè)庫(library)創(chuàng)建出一個(gè)全局變量,它期望用戶使用這個(gè)變量。為此,我們可以在項(xiàng)目配置中,添加一個(gè)小模塊來演示說明:

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
  |- /src
    |- index.js
+   |- globals.js
  |- /node_modules

src/globals.js

var file = 'blah.txt';
var helpers = {
  test: function() { console.log('test something'); },
  parse: function() { console.log('parse something'); }
}

你可能從來沒有在自己的源碼中做過這些事情,但是你也許遇到過一個(gè)老舊的庫(library),和上面所展示的代碼類似。在這個(gè)用例中,我們可以使用 exports-loader,將一個(gè)全局變量作為一個(gè)普通的模塊來導(dǎo)出。例如,為了將 file 導(dǎo)出為 file 以及將 helpers.parse 導(dǎo)出為 parse,做如下調(diào)整:

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    module: {
      rules: [
        {
          test: require.resolve('index.js'),
          use: 'imports-loader?this=>window'
-       }
+       },
+       {
+         test: require.resolve('globals.js'),
+         use: 'exports-loader?file,parse=helpers.parse'
+       }
      ]
    },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join']
      })
    ]
  };

現(xiàn)在從我們的 entry 入口文件中(即 src/index.js),我們能 import { file, parse } from './globals.js';,然后一切將順利進(jìn)行。

加載 polyfills

目前為止我們所討論的所有內(nèi)容都是處理那些遺留的 package 包,讓我們進(jìn)入到下一個(gè)話題:polyfills。

有很多方法來載入 polyfills。例如,要引入 babel-polyfill 我們只需要如下操作:

npm install --save babel-polyfill

然后使用 import 將其添加到我們的主 bundle 文件:

src/index.js

+ import 'babel-polyfill';
+
  function component() {
    var element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

請注意,我們沒有將 import 綁定到變量。這是因?yàn)橹恍柙诨A(chǔ)代碼(code base)之外,再額外執(zhí)行 polyfills,這樣我們就可以假定代碼中已經(jīng)具有某些原生功能。

polyfills 雖然是一種模塊引入方式,但是并不推薦在主 bundle 中引入 polyfills,因?yàn)檫@不利于具備這些模塊功能的現(xiàn)代瀏覽器用戶,會(huì)使他們下載體積很大、但卻不需要的腳本文件。

讓我們把 import 放入一個(gè)新文件,并加入 whatwg-fetch polyfill:

npm install --save whatwg-fetch

src/index.js

- import 'babel-polyfill';
-
  function component() {
    var element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
  |- /src
    |- index.js
    |- globals.js
+   |- polyfills.js
  |- /node_modules

src/polyfills.js

import 'babel-polyfill';
import 'whatwg-fetch';

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
-   entry: './src/index.js',
+   entry: {
+     polyfills: './src/polyfills.js',
+     index: './src/index.js'
+   },
    output: {
-     filename: 'bundle.js',
+     filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    module: {
      rules: [
        {
          test: require.resolve('index.js'),
          use: 'imports-loader?this=>window'
        },
        {
          test: require.resolve('globals.js'),
          use: 'exports-loader?file,parse=helpers.parse'
        }
      ]
    },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join']
      })
    ]
  };

如此之后,我們可以在代碼中添加一些邏輯,根據(jù)條件去加載新的 polyfills.bundle.js 文件。你該如何決定,依賴于那些需要支持的技術(shù)以及瀏覽器。我們將做一些簡單的試驗(yàn),來確定是否需要引入這些 polyfills:

dist/index.html

  <!doctype html>
  <html>
    <head>
      <title>Getting Started</title>
+     <script>
+       var modernBrowser = (
+         'fetch' in window &&
+         'assign' in Object
+       );
+
+       if ( !modernBrowser ) {
+         var scriptElement = document.createElement('script');
+
+         scriptElement.async = false;
+         scriptElement.src = '/polyfills.bundle.js';
+         document.head.appendChild(scriptElement);
+       }
+     </script>
    </head>
    <body>
      <script src="index.bundle.js"></script>
    </body>
  </html>

現(xiàn)在,我們能在 entry 入口文件中,通過 fetch 獲取一些數(shù)據(jù):

src/index.js

  function component() {
    var element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());
+
+ fetch('https://jsonplaceholder.typicode.com/users')
+   .then(response => response.json())
+   .then(json => {
+     console.log('We retrieved some data! AND we\'re confident it will work on a variety of browser distributions.')
+     console.log(json)
+   })
+   .catch(error => console.error('Something went wrong when fetching this data: ', error))

當(dāng)我們開始執(zhí)行構(gòu)建時(shí),polyfills.bundle.js 文件將會(huì)被載入到瀏覽器中,然后所有代碼將正確無誤的在瀏覽器中執(zhí)行。請注意,以上的這些設(shè)定可能還會(huì)有所改進(jìn),我們只是對于如何解決「將 polyfills 提供給那些需要引入它的用戶」這個(gè)問題,向你提供一個(gè)很棒的想法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Depthwise separable convolution(深度可分離卷積) 核心:深度可分離卷積其實(shí)是一種可...
    LaLa_2539閱讀 2,480評論 0 2
  • 如果你有面子,那我就給你備上里子,讓你的幸福盡最大限度飽滿! ——題記 好模好樣的怎么就睡不著了呢? 很長時(shí)間了,...
    春箋素心閱讀 256評論 0 0
  • (2017年3月11日) 等待援藏結(jié)束,我再擁你入懷。那不是奇跡出現(xiàn),而是用六年時(shí)間等來的那片為你盛開的花海。--...
    老葫蘆閱讀 243評論 0 1
  • 自從在表姐家住下后,我本以為從此就和過去一刀兩斷,可是,心始終隱隱作痛。那個(gè)男人的影子無論如何都趕不走。愛恨撕扯著...
    7af8eaec95e9閱讀 223評論 0 1

友情鏈接更多精彩內(nèi)容