webpack 代碼拆分

若是利用 webpack 將項目中的全部代碼打包在一塊兒,不少時候是不適用的,由於代碼中有些東西咱們老是但願將其拆分出來。好比:css

  • 樣式表,但願利用 link 標籤引入html

  • 使用機率較低的模塊,但願後期須要的時候異步加載node

  • 框架代碼,但願能利用瀏覽器緩存下部分不易變更的代碼react

下面是我在閱讀 webpack 的官方文檔時候,記錄的一些筆記,部分地方使用了本身的話來說,力圖讓它顯得更易懂。webpack

按需加載拆分

webpack 能夠幫助咱們將代碼分紅不一樣的邏輯塊,在須要的時候加載這些代碼。git

使用 require.ensure() 來拆分代碼

require.ensure() 是一種使用 CommonJS 的形式來異步加載模塊的策略。在代碼中經過 require.ensure([<fileurl>]) 引用模塊,其使用方法以下:github

require.ensure(dependencies: String[], callback: function(require), chunkName: String)

第一個參數指定依賴的模塊,第二個參數是一個函數,在這個函數裏面你可使用 require 來加載其餘的模塊,webpack 會收集 ensure 中的依賴,將其打包在一個單獨的文件中,在後續用到的時候使用 jsonp 異步地加載進去。web

require.ensure(['./a'], function(require){
    let b = require('./b');
    let a = require('./a');
    console.log(a+b)
});

以上代碼,a 和 b 會被打包在一塊兒,在代碼中執行到這段代碼的時候,會異步地去加載,加載完成後執行函數裏面的邏輯。npm

let a = require('./a');
require.ensure(['./a'], function(require){
    let b = require('./b');
    console.log(a+b)
});

若是這樣寫,那麼 a 不會和 b 打包在一塊兒,由於 a 已經被打包在主代碼中了。json

require.ensure(['./c'], function(require){
    let a = require('./a');
    console.log(a)
});

require.ensure(['./c'], function(require){
    let b = require('./b');
    console.log(b)
});

以上代碼中兩個模塊都依賴了 c 模塊,這個時候會拆分出兩個模塊,其中都包含了 c 模塊,由於在實際運用中,以上兩個 require.ensure 的執行順序不肯定,執行與否也不肯定,所以須要將 c 模塊都打包進去。

require.ensure 還能夠傳入第三個參數,這個參數用來指定打包的包名,對於上面這種狀況,c 模塊被打包入了兩個包中,若是事先明確這兩個包都會被使用,那麼不妨將這兩個包合併爲一個,這樣就不會有 c 模塊被打包兩次的問題了,因此能夠將 chunkName 指定爲同一個名字。

require.ensure(['./c'], function(require){
    let a = require('./a');
    console.log(a)
}, 'd');

require.ensure(['./c'], function(require){
    let b = require('./b');
    console.log(b)
}, 'd');

ok,這樣以上兩個 require.ensure 拆出來的包就合併爲同一個了。

CSS 拆分

開發者,可能但願能將工程中的全部引入的 CSS 拆分爲單個文件,這樣能夠利用緩存,且利用 CSS 和 JavaScript 並行加載,來加速 web 應用。

使用 css-loader

爲了加載 css,這裏須要用到 css-loader,配置方法以下:

module: {
    loaders: [{
        test: /\.css$/,
        exclude: /node_modules/,
        loader: 'css-loader'
    }]
}

這樣在代碼中就能夠寫以下代碼:

let css = require('./main.css');
console.log('' + css);

經過 require 一個 css 獲得其內容,固然了這裏 require('./main.css') 實際獲得的是一個對象,須要調用其 toString 方法將其轉換爲字符串。在代碼中引用一段 css,這經常不是咱們想要的。爲此可使用 style-loader 在代碼執行起來的時候,會將這些 css 插入到 style 標籤中,只是這裏 css 仍是存在於 js 中的,是後來動態插入到頁面中的:

module: {
    loaders: [{
        test: /\.css$/,
        exclude: /node_modules/,
        loader: 'style-loader!css-loader'
    }]
}

更多時候,是但願將 css 拆分爲單個文件,而後使用 link 標籤嵌入到 html 中,CSS 和 JavaScript 能夠並行加載,css 還能夠被緩存下來。

使用 extract-text-webpack-plugin 來拆分 css

爲了使用這個插件首先須要經過 npm 來安裝它:

npm i --save-dev extract-text-webpack-plugin

而後在 webpack 的配置文件中使用該插件:

var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = function () {
    return {
        entry: './index.js',
        output: {
            path: './build',
            filename: 'bundle.js'
        },
        module: {
            loaders: [{
                test: /\.css$/,
                exclude: /node_modules/,
                // 在 loader 中使用該插件
                loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
            }]
        },
        plugins: [
            // 將其添加在插件中
            new ExtractTextPlugin({ filename: 'bundle.css', disable: false, allChunks: true })
        ]
    }
}

須要注意的是,對於 webpack1 和 webpack2 這個插件的配置方法是不一樣的,差異比較細微,詳情請看官方文檔 extract text plugin for webpack 2

拆分業務代碼與框架代碼

一般一個 web 應用都會引用若干第三方庫,這些第三方庫一般比較穩定不會常常變更,可是若是將業務代碼和框架代碼打包在了一塊兒,這樣業務代碼每次變更打包獲得的結果都會變更,及時只改變了一個字符,瀏覽器也沒法利用緩存,必須所有從新加載。所以,何不將第三方庫單獨打包在一塊兒呢?

這裏舉個案例,一個 react 項目中使用了 reactreact-dom 這兩個包,我但願將他們打包在一塊兒,將業務代碼打包在一塊兒。

下面一步一步來:

1. 安裝 reactreact-dom:

npm i react react-dom --save

2. 配置 entry,output 和 loader

先使用單入口,讓代碼工做起來。另外由於使用了 react 因此要使用 babel-loader 來加載 js

// webpack.config.js

module.exports = {
    entry: 'index.js',
    output: {
        path: 'build/',
        filname: '[name]@[chunkhash].js'
    },
    module:{
        loaders:[{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel'
        }]
    }
}

3. 編寫業務代碼

index.js:

import React from 'react';
import ReactDOM from 'react-dom';


var Hello = React.createClass({
    render: function() {
        return <div>Hello {this.props.name}</div>;
    }
});

ReactDOM.render(<Hello name={'world'} />, document.getElementById('app'));

index.html:

<div id="app"></div>
<!--entry 爲一個字符串,這個 chunk 的名字會是 main, 所以這裏引入 main.js -->
<script src="build/main.js"></script>

啓動 webpack-dev-server,打開瀏覽器這個時候應該能在頁面上看到 hello world,這說明工做正常。

4. 拆分框架代碼

爲了拆分框架代碼,咱們須要增長一個入口,在這個入口中要包含 reactreact-dom

module.exports = {
    entry: {
        main: 'index.js',
        vendor: ['react', 'react-dom']
    }
    //...
}

單單像上面這樣配置,打包後會獲得 main.jsvendor.js,但會發如今 main.js 中依然包含了 react 和 react-dom 的代碼,這是由於指定了入口後,webpack 就會從入口文件開始講整個依賴打包進來,index.js 中引用了 react 和 react-dom 天然會被打包進去。要想達到以前所說的那個效果,還須要藉助一個插件 —— CommonsChunkPlugin

5. 使用 CommonsChunkPlugin

這個插件的功能是將多個打包結果中公共的部分抽取出來,做爲一個單獨的文件。它符合目前的場景,由於 main.jsvendor.js 中存在一份公共的代碼,那就是 vendor.js 中的內容。(這個說法並不許確,這裏只是指 react 和 react-dom 都被打包進了這兩個文件)

let webpack = require('webpack');

module.exports = {
    entry: {
        main: 'index.js',
        vendor: ['react', 'react-dom']
    },
    //...

    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor' // 指定一個但願做爲公共包的入口
        })
    ]
}

再進行打包,這個時候會發現 main.js 中的代碼不在包含 react 的代碼了。看似實現了咱們的需求,但真實應用下並無這麼簡單,在實際項目中 js 腳本一般都會給添加一個 MD5 的 hash 在後面,形如 app@709d9850745a4c8ba1d4.js 這樣每次打包後,若是文件內容變了,後面的 hash 也會變更。就以上場景,會發現當咱們修改了業務代碼後,獲得的 hash 是不一樣的,所以每次都會獲得兩個不一樣的打包結果。業務代碼改變了,拆分出來的框架包也變了,這顯然不符合初衷 —— 利用瀏覽器緩存。

這是由於 webpack 在打包的時候會產生一些運行時代碼,好比 __webpack_require__webpackJsonp 等等,這些函數是用來幫助 webpack 完成模塊加載等功能的,業務代碼的改變會致使業務代碼打包後的 hash 值改變,而在 webpack 的運行時代碼中其實是保存了打包後的結果的文件名的,由於它在異步加載模塊的時候須要用到。所以,下面須要作的是將 webpack 的運行時代碼拆分出來。

修改 plugins 以下,將 name 修改成 names,並增長一個 init 的包名,執行打包,會發現 webpack 的運行時代碼都被入該包內。

plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        names: ['vendor', 'init']
    })
]

這樣以來,修改了業務代碼後,vendor 由於只引用了 react 和 react-dom 所以,業務代碼的改變不會改變 vendor 這個包的內容,hash 也保持不變。但,也僅僅如此 若是你引用了其餘模塊,webpack 收集依賴的時候會給每一個模塊編一個號,引入其餘模塊會致使模塊數改變,也就會致使編號改變,這個時候打包出來的 vendor 仍是會改變。

那麼到底該如何解決這個問題呢?在官方文檔上沒有找到解決方案。後面我會繼續探索這一問題,找到解決方案後會及時更新到這裏,若是你有解決方案,還請不吝賜教,謝謝。

相關文章
相關標籤/搜索