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 仍是会改变。

那么到底该如何解决这个问题呢?在官方文档上没有找到解决方案。后面我会继续探索这一问题,找到解决方案后会及时更新到这里,若是你有解决方案,还请不吝赐教,谢谢。

相关文章
相关标签/搜索