詳解webpack code splitting

webpack代碼分割

什麼是代碼分割

在最開始使用Webpack的時候, 都是將全部的js文件所有打包到一個build.js文件中(文件名取決與在webpack.config.js文件中output.filename), 可是在大型項目中, build.js可能過大, 致使頁面加載時間過長. 這個時候就須要code splitting, code splitting就是將文件分割成塊(chunk), 咱們能夠定義一些分割點(split point), 根據這些分割點對文件進行分塊, 並實現按需加載。html

代碼分割,也就是Code Splitting通常須要作這些事情:vue

  • 爲 Vendor 單獨打包(Vendor 指第三方的庫或者公共的基礎組件,由於 Vendor 的變化比較少,單獨打包利於緩存)
  • 爲 Manifest (Webpack 的 Runtime 代碼)單獨打包
  • 爲不一樣入口的業務代碼打包,也就是代碼分割異步加載(同理,也是爲了緩存和加載速度)
  • 爲異步公共加載的代碼打一個的包

來自螞蟻金服數據體驗技術Webpack 4 配置最佳實踐,這裏雖然用的webpack3,也一樣試用。react

下面就用Code Splitting來實現上面幾點。webpack

code splitting

webpack的代碼分割Code Splitting,主要有兩種方式:git

第一種:webpack1經過require.ensure定義分割點,將代碼進行分割打包,異步加載。

第二種:在動態代碼拆分方面,webpack支持符合ECMAScript提議的import()語法進行代碼分割和異步加載。es6

require.ensure代碼分割

webpack 在編譯時,會靜態地解析代碼中的 require.ensure(),同時將模塊添加到一個分開的 chunk 當中,這個新的 chunk 會被 webpack 經過 jsonp 來異步加載,其餘包則會同步加載。github

  • 語法:
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
  • 參數:

第一個參數是dependencies依賴列表,webpack會加載模塊,但不會執行。web

第二個參數是一個回調,當全部的依賴都加載完成後,webpack會執行這個回調函數,在其中可使用require導入模塊,導入的模塊會被代碼分割到一個分開的chunk中。json

第三個參數指定第二個參數中代碼分割的chunkname。數組

將下面代碼拷貝到webpack.config.RequireEnsure.js:

var webpack = require('webpack');
var path = require('path');
module.exports = {
    entry: {
        'pageA': './src/pageA',
        'vendor': ['lodash'] // 指定單獨打包的第三方庫(和CommonsChunkPlugin結合使用),能夠用數組指定多個
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].bundle.js',
        chunkFilename: '[name].chunk.js', // code splitting的chunk是異步(動態)加載,須要指定chunkFilename(具體能夠了解和filename的區別)
    },
    plugins: [
        // 爲第三方庫和和manifest(webpack runtime)單獨打包
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor', 'manifest'],
            minChunks: Infinity
        }),
    ]
}

src/pageA.js、src/subPageA.js、src/subPageB.js、src/module.js代碼以下:

// src/pageA.js
import * as _ from 'lodash';
import subPageA from './subPageA';
import subPageB from './subPageB';
console.log('this is pageA');
export default 'pageA';

// src/subPageA.js
import module from './module';
console.log('this is subPageA');
export default 'subPageA';

// src/subPageB.js
import module from './module';
console.log('this is subPageB');
export default 'subPageB';

// src/module.js
const s = 'this is module'
export default s;

其中subPageA和subPageB模塊使用共同模塊module.js,命令行運行webpack --config webpack.config.RequireEnsure.js,打包生成:

pageA.bundle.js
vendor.bundle.js // 爲 Vendor 單獨打包
manifest.bundle.js // 爲Manifest單獨打包

知足咱們文檔一剛開始說的代碼分割的兩點要求,可是咱們想要subPageA和subPageB單獨打包:

修改src/pageA.js,把import導入方式改爲require.ensure的方式就能夠代碼分割:

// import subPageA from './subPageA';
// import subPageB from './subPageB';
require.ensure([], function() {
    // 分割./subPageA模塊
    var subPageA = require('./subPageA');
}, 'subPageA');
require.ensure([], function () {
    var subPageB = require('./subPageB');
}, 'subPageB');

再次打包,生成:

pageA.bundle.js
subPageA.chunk.js // 代碼分割
subPageB.chunk.js
vendor.bundle.js // 爲 Vendor 單獨打包
manifest.bundle.js // 爲Manifest單獨打包

會發現用了require.ensure的模塊被代碼分割了,達到了咱們想要的目的,可是因爲subPageA和subPageB有公共模塊module.js,打開subPageA.chunk.js和subPageB.chunk.js發現都有公共模塊module.js,這時候就須要在require.ensure代碼前面加上require.include('./module')

src/pageA.js:

require.include('./module'); // 加在require.ensure代碼前

再次打包,公共模塊modle.js被打包在了pageA.bundle.js,解決了爲異步加載的代碼打一個公共的包問題。

最後測試一下webpack打包後的動態加載/異步加載:

在index.html裏面引入打包文件:

<html>
    <body>
        <script src="./dist/manifest.bundle.js"></script>
        <script src="./dist/vendor.bundle.js"></script>
        <script src="./dist/pageA.bundle.js"></script>
    </body>
</html>

webpack.config.RequireEnsure.js加上動態加載路徑的配置後再次打包:

output: {
    ...
    publicPath: './dist/' // 動態加載的路徑
}

瀏覽器中打開index.html文件,會發現subPageA.chunk.js和subPageB.chunk.js沒有引入也被導入了進來。其實這兩個文件是webpack runtime打包文件根據代碼分割的文件自動異步加載的。

<img src="https://user-gold-cdn.xitu.io...;h=115&f=png&s=12335">

dynamic import 代碼分割

requre.ensure好像已被webpack4廢棄。es6提供了一種更好的代碼分割方案也就是dynamic import(動態加載)的方式,webpack打包時會根據import()自動代碼分割;雖然在提案中,能夠安裝babel插件兼容,推薦用這種方式:

  • 語法:
import(/* webpackChunkName: chunkName */ chunk)
.then( res => {
    // handle something
})
.catch(err => {
    // handle err
});

其中/ chunkName /爲指定代碼分割包名,chunk指定須要代碼分割的文件入口。注意不要把 import關鍵字和import()方法弄混了,該方法是爲了進行動態加載。

import()調用內部使用promises。若是您使用import()較舊的瀏覽器,請記住Promise使用諸如es6-promise或promise-polyfill之類的polyfill進行填充。

下面修改pageA.js:

import * as _ from 'lodash';
import(/* webpackChunkName: 'subPageA' */'./subPageA').then(function(res){
    console.log('import()', res)
})
import(/* webpackChunkName: 'subPageB' */'./subPageB').then(function(res){
    console.log('import()', res)
})

console.log('this is pageA');
export default 'pageA';

將下面代碼拷貝到webpack.config.import.js:

var webpack = require('webpack');
var path = require('path');
module.exports = {
    entry: {
        'pageA': './src/pageA',
        'vendor': ['lodash'] // 指定單獨打包的第三方庫(和CommonsChunkPlugin結合使用),能夠用數組指定多個
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].bundle.js',
        chunkFilename: '[name].chunk.js', // code splitting的chunk是異步(動態)加載,須要指定chunkFilename(具體能夠了解和filename的區別)
        publicPath: './dist/' // 動態加載的路徑
    },
    plugins: [
        // 爲第三方庫和和manifest(webpack runtime)單獨打包
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor', 'manifest'],
            minChunks: Infinity
        }),
    ]
}

命令行webpack --config webpack.config.import.js,打包發現和require.ensure同樣的結果:

pageA.bundle.js
subPageA.chunk.js // 代碼分割
subPageB.chunk.js
vendor.bundle.js // 爲 Vendor 單獨打包
manifest.bundle.js // 爲Manifest單獨打包

爲了分離出subPageA.chunk.js和subPageB.chunk.js的公共模塊module.js,能夠用CommonsChunkPlugin的async方式給異步加載的打包提取公共模塊:

在webpack.config.import.js加上下面配置:(其實這種方式能夠替代require.ensure中的require.include方式提取公共代碼,更自動和簡單)

plugins: [
    // 爲異步公共加載的代碼打一個的包
    new webpack.optimize.CommonsChunkPlugin({
        async: 'async-common', // 異步公共的代碼
        children: true, // 要加上children,會從入口的子依賴開始找
        minChunks: 2 // 出現2次或以上相同代碼就打包
    }),
    ...
]

從新打包,打包文件以下:

pageA.bundle.js
subPageA.chunk.js // 代碼分割
subPageB.chunk.js
async-common-pageA.chunk.js // 爲異步公共加載的代碼打的包
vendor.bundle.js // 爲 Vendor 單獨打包
manifest.bundle.js // 爲Manifest單獨打包

會發現多了一個異步加載包subPageA.chunk.js和subPageB.chunk.js的公共模塊async-common-pageA.chunk.js包,配置成功!

一樣,仍是進行測試,index.html引入同步加載的包:

<html>
    <body>
        <script src="./dist/manifest.bundle.js"></script>
        <script src="./dist/vendor.bundle.js"></script>
        <script src="./dist/pageA.bundle.js"></script>
    </body>
</html>

瀏覽器中打開index.html文件,會發現subPageA.chunk.js、subPageB.chunk.js和async-common-pageA.chunk.js被自動異步加載了。

<img src="https://user-gold-cdn.xitu.io...;h=136&f=png&s=15038">

關於異步加載和按需加載

關於文檔中屢次提到webpack代碼分割的包,在瀏覽器中webpack runtime的包會自動異步加載代碼分割的包,那麼在react和vue應用中,若是這些代碼分割包在頁面初始化也會自動異步加載,那不是分包的做用不大?緣由實際上是咱們上面例子執行了import()或require.ensure,而在應用中,寫法是當請求路由的時候才執行import()或require.ensure,而後再異步加載,webpack遇到import()或require.ensure的配置的時候只會進行代碼切割,這種思路就是按需加載的基礎。

部分參考連接:

代碼分割 - 使用 require.ensure

文檔中出現的源代碼在個人github,感興趣的歡迎star。

相關文章
相關標籤/搜索