愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Webpack使用总结

一、核心概念

webpack 是一个现代 JavaScript 应用程序的模块打包器。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成少量的 bundle(通常只有一个),由浏览器加载。
其核心概念有4个:入口(entry)、出口(output)、loader、插件

1、入口(entry)

webpack为应用程序创建依赖关系图,图的起点被称之为入口起点。入口起点告诉 webpack 从哪里开始,并根据依赖关系图确定需要打包的内容。可将应用程序的入口起点认为是根上下文app第一个启动文件

module.exports = {
    entry: './path/to/app.js'
}


2、出口

告诉 webpack 在哪里打包应用程序:

const path = require('path');
module.exports = {
    entry: './path/to/app.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'app.bundle.js'
    }
}


3、Loader

webpack 把每个文件(.css, .html, .scss, .jpg, etc.) 都看作模块处理。然而 webpack 自身只理解 JavaScript。为了让webpack理解这些模块,就需要引入loader,loader能够达成的事情是:
1)识别出应该被对应的 loader 进行转换的那些文件。(test属性)
2)转换这些文件,从而使其能够被添加到依赖图中(并且最终添加到 bundle 中)(use属性)
如:

const path = require('path');
module.exports = {
    entry: './path/to/app.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'app.bundle.js'
    },
    module: {
        rules: [
            { test: /\.txt$/, use: 'raw-loader' }
        ]
    }
}

这个配置告诉webpack:

当你遇到在require()或者import语句中被解析为.txt的路径时,在对它打包前,先试用raw-loader转化一下

注意:定义loader时,应定义在module.rules中,而不是rules


4、插件

loader是在每个文件的基础上执行转化,而插件则更常用于在打包模块的compilationchunk生命周期执行操作与自定义功能。使用一个插件的步骤为:

require()这个插件,然后提交到plugins数组中

如果在一个配置文件中,需要因为不同的目的而多次使用同一个插件,那么需要使用new来创建它的一个实例。如:

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

module.exports = {
    entry: './path/to/app.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'app.bundle.js'
    },
    module: {
        rules: [
            { test: /\.txt$/, use: 'raw-loader' }
        ]
    },
    plugins: [
        new webpack.optimize.UglifyJsPlugin(),
        new HtmlWebpackPlugin({template: './src/index.html'})
    ]
}


二、使用Webpack

1、基本使用

安装:

mkdir webpack-demo && cd webpack-demo
npm init -y
npm install --save-dev webpack

配置:
Webpack通过webpack.config.js来进行配置
使用:

  • 可以使用webpack命令直接执行,它将默认读取并使用webpack.config.js的配置
  • 也可以使用webpack --config configFileName来指定使用特定的配置文件


2、NPM 脚本

用 CLI 方式来运行本地的 webpack 不是特别方便,我们可以设置一个快捷方式。在 package.json 添加一个 npm 脚本,如:

{
    ...
    "script": {
        "build": "webpack"
    },
    ...
}

此后,用npm run build来替代较长的命令运行webpack,而不是去调用 ./node_modules/.bin/webpack


三、管理资源

webpack将动态打包所有依赖项(创建所谓的依赖图),这是极好的创举,因为现在每个模块都可以明确表述它自身的依赖,将避免打包未使用的模块。除了 JavaScript,还可以通过 loader 引入任何其他类型的文件,以下为示例:

1、加载CSS

安装style-loadercss-loader

npm install --save-dev style-loader css-loader

配置webpack.config.js

// 省略 ...
module: {
    rules: [
        {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        }
    ]
}
// 省略 ...

如此一来,就可以直接使用import './style.css'来引入文件,而当模块运行的时候,css文件内的CSS字符串将被以<style>标签的形式插入到<head>

2、加载图片

可以使用file-loader模块来加载图像:

npm install --save-dev file-loader

配置webpack.config.js

// 省略 ...
module: {
    rules: [
        {
            test: /\.(png|svg|jpg|gif)$/,
            use: ['file-loader']
        }
    ]
}

如此一来,就可以使用import avatar from './avatar.jpg'的形式,来引用一个图片文件了。打包后,会自动将图片文件复制到输出目录中且重命名(如:2c32dbbbb617e0f940893ea81a82ec34.jpg),然后avatar保存的则是这个最终路径。此外:

  • 使用css-loader,CSS中的url(图片地址)会被替换地址
  • 使用html-loader,HTML中的<img src="图片地址">会被替换为最终地址

3、加载字体

file-loader 和 url-loader 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,包括字体。
配置webpack.config.js

// 省略 ...
module: {
    rules: [
        {
            test: /\.(woff|woff2|eot|ttf|otf)$/,
            use: ['file-loader']
        }
    ]
}

4、加载数据

可以加载的有用资源还有数据,如 JSON 文件,CSV、TSV 和 XML。其中,JSON支持是内置的,而要导入CSV、TSV和XML的haul,则可以使用csv-loaderxml-loader,而这么一来,所导入的文件,都会被解析为直接可用的JSON对象:

import xmlData from './data.xml'

console.log(xmlData) // 此时输出的是一个解析后的对象


四、管理输出

1、多入口文件

对于有多个入口的文件,我们可以像下面这样子来配置:

{
    entry: {
        app1: './src/app1.js',
        app2: './src/app2.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js'
    }
}

那么输出的dist文件夹中将得到app1.jsapp2.js两个文件,简单情况下,我们可以自己在html文件中引用这两个文件,如:

<script src="dist/app1.bundle.js"></script>
<script src="dist/app2.bundle.js"></script>

但是我们如果为入口文件换个名称,那么还得再回去修改index.html文件,很是不方便。所以,这种情况下我们可以采用HtmlWebpackPlugin来解决这个问题:
安装:

npm install --save-dev html-webpack-plugin

使用:

const HtmlWebpackPlugin = require('html-webpack-plugin')
// ... 省略
{
    plugins: [
        new HtmlWebpackPlugin({ title: 'Hello, world' })
    ]
}

这样子打包后,就会生成一个index.html文件,自动包含两个bundle文件

2、清理/dist文件夹

在每次构建前清理 /dist 文件夹,是比较推荐的一种做法,因为这样子做只会生成用到的文件。保持/dist文件夹的纯粹。而完成这种需求,可以使用clean-webpack-plugin,安装如:

npm install clean-webpack-plugin --save-dev

配置webpack.config.js

const CleanWebpackPlugin = require('clean-webpack-plugin')
// 省略 ...
{
    plugins: [
        new CleanWebpackPlugin(['dist'])
    ]
}


五、开发

1、使用source-map

打包后的源码,通常很难以最终到源文件及其初始位置,这种情况下,使用source-map将会大有裨益。为了更容易地追踪错误和警告,JavaScript 提供了 source map 功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你。并指出是在错误文件里的哪一行。如:

greet.js:1 Uncaught ReferenceError: consoxle is not defined
at Object.8 (greet.js:1)
at webpack_require (bootstrap 654f809…?0fa9:19)
at bootstrap 654f809…?0fa9:65
at bootstrap 654f809…?0fa9:65

配置方法:加入devtool: 'inline-source-map',如:

{
    // entry: ...
    // output: ...
    devtool: 'inline-source-map'
    // ...
}

2、使用观察模式

package.json里,可以添加一句用于启动webpack观察模式的npm script

"scripts": {
    "watch": "webpack --watch"
}

观察模式下,webpack将自动监测修改后的模块,并自动重新编译。通常情况下,我们还希望修改后自动刷新浏览器,以查看修改后的效果。那么,可以使用webpack-dev-serverwebpack-dev-server为我们提供了一个简单的web服务器,同时能够实时重新加载(live reloading
安装:

npm install --save-dev webpack-dev-server

配置:

{
    devServer: {
        contentBase: './dist'
    }
}

该配置告知webpack-dev-server,在localhost:8080下建立服务,将 dist 目录下的文件,作为可访问文件。
同时,修改package.json文件里的配置:

"script": {
    "start": "webpack-dev-server --open"
}

现在,我们可以在命令行中运行npm start,就会看到浏览器自动加载页面。如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码


六、代码分离

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
有三种常用的代码分离方法:
1)入口起点:使用 entry 选项手动分离代码
2)防止重复:使用 CommonsChunkPlugin 去重和分离 chunk
3)动态导入:通过模块的内联函数调用来分离代码

1、入口起点

目前为止最简单和最直观的分离代码的方式。然而,这种方式是比较手动的并且会遇到一些陷阱:
1)如果两个入口起点文件中都引用了同一个模块(如lodash),那么打包后的两个打包文件中,也都会包含两个模块(造成重复)
2)不能被用来进行动态代码分离,从而不能够结合应用的逻辑进行分离

2、防止重复

为了防止重复,我们可以使用CommonsChunkPlugin插件来去重和分离chunk,用法如:
配置文件:

const webpack = require('webpack')

// ... 省略
module.exports = {
    entry: {
        app1: './src/app1.js',
        app2: './src/app2.js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common'
        })
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

如此一来,公共部分将会被抽离出来,成一个common.bundle.js模块。而app1.bundle.jsapp2.bundle.js中将只含有各自的逻辑代码。
此外,还有一些有用的插件和loader可以用来分离代码:

  • ExtractTextPlugin 从主程序中分离出有用的CSS代码
  • bundle-loader 用来分离代码和懒加载打包代码
  • promise-loaderbundle-loader类似,但是使用Promise

3、动态导入

Webpack提供两种动态代码分离的方法:
1)使用ES提案的import()语法
2)使用require.ensure
配置方法:

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

module.exports = {
    entry: {
        index: './src/index.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Code splitting'
        })
    ],
    output: {
        filename: '[name].bundle.js',
        chunkFilename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

使用如:

function getComponent() {
    return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
        const elem = document.createElement('div')
        elem.innerHTML = _.join(['Hello', 'webpack'], ' ')

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

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

注意在注释中的:webpackChunkName,这可以使得我们的分离模块被命名为lodash.bundle.js,而不是[id].bundle.js
如果使用环境下允许使用async/await语法,那么还可以这么写:

async function getComponent() {
    const elem = document.createElement('div')
    const _ = await import(/* webpackChunkName: "lodash" */ 'lodash')
    elem.innerHTML = _.join(['Hello', 'webpack'], ' ')
    return elem
}

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


七、生产环境构建

1、自动方式

运行webpack -p (也可以运行webpack --optimize-minimize --define process.env.NODE_ENV="'production'",他们是等效的)。它会执行如下步骤:

  • 使用 UglifyJsPlugin 进行 JS 文件压缩
  • 运行LoaderOptionsPlugin
  • 设置 NodeJS 环境变量,触发某些 package 包,以不同的方式进行编译

2、JS文件压缩

webpack自带了UglifyJsPlugin,它运行 UglifyJS 来压缩输出文件。此插件支持所有的 UglifyJS 选项。在命令行中指定 --optimize-minimize,或在 plugins 配置中添加:

module.exports = {
    // ...
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            sourceMap: options.devtool &&
            (
                options.devtool.indexOf('sourcemap') >= 0 ||
                options.devtool.indexOf('source-map') >= 0
            )
        })
    ]
    // ...
}

因此,通过设置devtool options可以生成Source Maps

3、Source Maps

推荐在生产环境中使用 source map,因为 Source Maps 对于 debug 和运行基准测试(benchmark tests)非常有用。webpack 可以在 bundle 中生成内联的 source map 或生成到独立文件。

4、Node 环境变量

运行webpack -p或者加参数--define process.env.NODE_ENV="'production'",就会通过以下的方式调用DefinePlugin

const webpack = require('webpack')
module.exports = {
    // ...
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('production')
        })
    ]
}

那么,DefinePlugin就会在源码中执行查找和替换操作。将任何存现process.env.NODE_ENV的地方全部替换为'production'。因此,如果有如下的代码:

if (process.env.NODE_ENV !== 'production') {
    // ...
}

就会等价于:

if (false) {
    // ...
}

并最终被UglifyJS等价替换掉

NODE_ENV是一个Node.js暴露给运行脚本的系统环境变量。服务端的工具/构建脚本以及客户端库都可以方便的使用该环境变量确定自己的开发-生产行为。然而与期望的相反,构建脚本 webpack.config.js 中的 process.env.NODE_ENV 并不会被设置为 "production"。 因此,条件判定,形如 process.env.NODE_ENV === 'production' ? '[name].[hash].bundle.js' : '[name].bundle.js' 并不会按预想的起作用

解决方法为:使用 Node.js 模块的标准方式:在运行 webpack 时设置环境变量,并且使用 Node.js 的 process.env 来引用变量
webpack.config.js:

module.exports = {
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: process.env.NODE_ENV === 'production'
        })
    ]
}

然后使用cross-env包来跨平台设置环境变量:
package.json

{
    "scripts": {
        "build": "cross-env NODE_ENV=production PLATFORM=web webpack"
    }
}

5、为多种环境分别配置打包

解决途径:为不同的环境编写独立的webpack配置文件

简单方式

编写两个独立的webpack配置文件,如:webpack.dev.jswebpack.prod.js
package.json里调整scripts,如:

"scripts": {
    ...
    "build:dev": "webpack --env=dev --progress --profile --colors",
    "build:dist": "webpack --env=prod --progress --profile --colors"
}

现在,执行webpack命令的时候,就带上了多个参数,包含env参数,那么我们可以获得env参数,并决定该调用哪个配置文件,所以webpack.config.js可以编写如下:

module.exports = function(env) {
    return require(`./webpack.${env}.js`)
}

高级途径

有一个基本配置文件,其中包含两个环境通用的配置,然后将其与特定于环境的配置进行合并。这将为每个环境产生完整配置,并防止重复公共部分代码。
而解决这个问题,可以使用webpack-merge工具。可以先编写一个包含有公共配置的文件webpack.common.js,然后,像下面这样子来合并得到webpack.prod.js文件:

const Merge = require('webpack-merge')
const CommonConfig = require('./webpack.common.js')

module.exports = Merge(CommonConfig, {
    // 额外配置
})