Webpack使用总结

2018-05-04

Webpack简介

  • 当我们使用Webpack来打包一个项目的时候,Webpack首先从配置中的entries属性定义的入口开始,历遍整个项目,根据import的递归地历遍整个项目的文件,构建模块之间的依赖图(dependency graph)。然后再根据webpack的配置信息对项目进行打包。

Webpack执行过程

Webpack执行

  • 解析过程:
    • Webpack自entry起递归地历遍整个项目。可以设置entryresolve属性来设置查找路径
    • 每次Webpack成功地解析一个模块, 都会通过对应的Loader对模块进行处理。
    • 对于每个Loader也会发生类似策解析过程。
  • 执行过程:
    • Webpack按照从右往左从上到下的顺序执行模块匹配的Loader。每个Loader依次处理该模块。
    • 执行的结果最终会被注入到打包后的文件中。
  • 完成:
    • 在每个模块都被处理之后,Webpack根据output的设定生成最终打包文件。

Developing

简单的 Webpack 配置

创建项目并安装Webpack

1
2
3
4
mkdir webpack-car
cd webpack-car
npm init
npm install webpack webpack-cli --save-dev

运行webpack

1
2
3
4
5
6
7
8
9
10
$ node_modules/.bin/webpack
Hash: 6736210d3313db05db58
Version: webpack 4.1.1
Time: 88ms
Built at: 3/16/2018 3:35:07 PM

WARNING in configuration
The 'mode' option has not been set. Set 'mode' option to 'development' or 'production' to enable defaults for this environment.

ERROR in Entry module not found: Error: Can't resolve './src' in '.../webpack-car'
  • 输出提示我们未设置mode属性, 并且由于没有找到相关文件来解析。通过如下步骤解决
    • 创建src/index.js文件。 写入console.log('Doom!');
    • 执行 node_modules/.bin/webpack --mode development。 这次webpack会发现文件来解析打包。
    • 刚才的命令会在项目根目录下生成dist/main.js文件。 其中包含webpack的启动代码,还有项目打包后的输出。

更复杂一点

  • src/component.js

    1
    2
    3
    4
    5
    6
    7
    export default (text = "Hello world") => {
    const element = document.createElement("div");

    element.innerHTML = text;

    return element;
    };
  • src/index.js

    1
    2
    3
    import component from "./component";

    document.body.appendChild(component());

配置html-webpack-plugin测试本项目

1
2
npm install html-webpack-plugin --save-dev
npm install serve -g
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const HtmlWebpackPlugin = require('html-webpack-plugin');

    module.exports = {
    plugins: [
    new HtmlWebpackPlugin({
    title: "Webpack demo",
    }),
    ],
    };
  • 输入如下命令,打开浏览器到提示网址查看结果。

    1
    2
    3
    node_modules/.bin/webpack --mode production
    cd dist
    serve

查看输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Hash: d172aaf21a4ccd6a3124
Version: webpack 4.6.0
Time: 618ms
Built at: 2018-05-04 20:46:45
Asset Size Chunks Chunk Names
main.js 679 bytes 0 [emitted] main
index.html 181 bytes [emitted]
Entrypoint main = main.js
[0] ./src/index.js + 1 modules 217 bytes {0} [built]
| ./src/index.js 77 bytes [built]
| ./src/component.js 140 bytes [built]
Child html-webpack-plugin for "index.html":
1 asset
Entrypoint undefined = index.html
[0] (webpack)/buildin/module.js 497 bytes {0} [built]
[1] (webpack)/buildin/global.js 489 bytes {0} [built]
+ 2 hidden modules
  • Hash: d172aaf21a4ccd6a3124: 这次build的哈希值,可以将这个hash值添加到打包后输出的文件名中。
  • main.js 679 bytes 0 [emitted] main: 生成文件的名字,大小, 分块的ID,状态信息,分块的名字。
  • Child html-webpack-plugin for "index.html": 与Plugin有关的输出。

    npm scripts快捷方式

  • package.json
    1
    2
    3
    "scripts": {
    "build": "webpack --mode production"
    }

配置webpack-dev-server

Webpack watch模式和 webpack-dev-server

  • 传入--watch选项开启watch mode, webpack在watch模式下会检测任何文件变更,并重新编译文件。

    1
    2
    3
    4
    5
    npm run build -- --watch
    ```
    - `webpack-dev-server`(WDS)也实现了watch模式,WDS运行在内存当中。当WDS运行时,所有对项目文件的更改生成的结果都是直接写入到打包文件中,而是存储在内存中。默认状态下,WDS会自动刷新浏览器中内容。
    ```bash
    npm install webpack-dev-server --save-dev
  • package.json

    1
    2
    3
    4
    "srcipt": {
    "start": "webpack-dev-server --mode development",
    "build": "webpack --mode production"
    },
  • 现在运行npm start后, http://localhost:8080中会显示内容, 代码更改后会强制刷新

配置 webpack-dev-server

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
// ...
devServer: {
// 只展示错误信息
stats: "errors-only",
host: process.env.HOST, // 默认为 `localhost`
port: process.env.PORT, // 默认为 8080
open: true, // 在浏览器中打开页面
overlay: true, // 在浏览器中显示错误和警告的相关信息
}
}
  • 可以通过命令行设置HOSTPORT的值(PORT=3000 npm start)。

轮询来替代 watch mode

  • 当watch mode在某些系统上是小事。可以启动WDS的轮询选项。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const path = require("path");

    module.exports = {
    devServer: {
    watchOptions: {
    aggregateTimeout: 300,
    poll: 1000,
    },
    },
    plugins: [
    new webpack.WatchIgnorePlugin([
    path.join(__dirname, "node_module")
    ]),
    ],
    };

更改配置后自动重启WDS

1
npm install nodemon --save-dev
  • package.json

    1
    2
    3
    "scripts": {
    "start": "nodemon --watch webpack.config.js --exec \"webpack-dev-server --mode development\""
    }
  • webpack-dev-server的一些其他选项

    • devServer.contentBase:假如我们没有自动生成index.html,可以提供过设置contentBase来指定内容路径。
    • devServer.proxy:默认禁用, 接受一个代理映射对象, 将对应的查询映射到对应的服务器地址。
    • devServer.headers:在请求中加入自定义头部。

配置管理

配置管理的几种常见方法

  • 常见方法

    • 创建多个配置文件, 分别对应每个运行环境。 通过--config参数来控制。
    • 将配置作为一个库发布,之后再使用它,例如: hjs-webpack
    • 在一个文件中管理所有配置, 通过--env参数来控制配置的使用。

      通过webpack-merge管理配置

      1
      npm install webpack-merge --save-dev
  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    exports.devServer = ({ host, port } = {}) => ({
    devServer: {
    stats: "errors-only",
    host, // Defaults to `localhost`
    port, // Defaults to 8080
    open: true,
    overlay: true,
    },
    });
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    const merge = require("webpack-merge");
    const HtmlWebpackPlugin = require("html-webpack-plugin");

    const parts = require("./webpack.parts");

    const commonConfig = merge([
    {
    plugins: [
    new HtmlWebpackPlugin({
    title: "Webpack demo",
    }),
    ],
    },
    ]);

    const productionConfig = merge([]);

    const developmentConfig = merge([
    parts.devServer({
    // Customize host/port here if needed
    host: process.env.HOST,
    port: process.env.PORT,
    }),
    ]);

    module.exports = mode => {
    if (mode === "production") {
    return merge(commonConfig, productionConfig, { mode });
    }

    return merge(commonConfig, developmentConfig, { mode });
    };
  • package.json

    1
    2
    3
    4
    "scripts": {
    "build": "webpack --env production",
    "start": "nodemon --watch webpack.config.js --exec \" webpack-dev-server --env development\""
    },

Styling

  • Webpack通过loader和plugin来处理加载样式文件。

    加载CSS样式

  • css-loader:历遍每个匹配文件的@importurl(), 并将其作为ES6的import处理来构建依赖图, 如果@import指向外部资源的话,css-loader会跳过该语句。
  • style-loader: 创建style元素, 并将所有样式内容写入。
1
npm install css-loader style-loader --save-dev
  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    exports.loadCSS = ({ include, exclude} = {}) => ({
    module: {
    rules: [
    {
    test: /\.css$/,
    include,
    exclude,
    use: ["style-loader", "css-loader"], // evaluated from right to left
    }
    ]
    }
    })
  • webpack.config.js

    1
    2
    3
    4
    const commonConfig = merge([
    // ...
    parts.loadCSS(),
    ]);
  • src/main.css

    1
    2
    3
    body {
    background: lime;
    }
  • src/index.js

    1
    2
    import './main.css';
    //...

加载LESS样式

  • 通过less-loader处理LESS样式
    1
    2
    3
    4
    {
    test:/\.less$/,
    use: ["style-loader", "css-loader", "less-loader"],
    }

加载SASS样式

  • 通过sass-loader处理SASS样式
    1
    2
    3
    4
    {
    test: /\.scss$/,
    use: ["style-loader", "css-loader", "sass-loader"],
    }

加载Stylus样式

  • 通过stylus-loader处理Stylus样式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    // ...
    module: {
    rules: [
    {
    test: /\.styl$/,
    use: [
    "style-loader",
    "css-loader",
    "stylus-loader",
    ],
    },
    ],
    },
    },

PostCSS

  • PostCSS允许你通过JavaScript插件对CSS执行转化。 PostCSS之于CSS如Babel之于JavaScript。
  • 通过postcss-loader配置PostCSS
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    test: /\.css$/,
    use: [
    "style-loader",
    "css-loader",
    {
    loader: "postcss-loader",
    options: {
    plugins: () => ([
    require("autoprerfixer"),
    require("precss"),
    ]),
    },
    },
    ],
    }

其他

  • css-loader的文件查找,只能处理相对路径引用,不能处理绝对路径引用。 如果想使用绝对路径引入文件,需要把文件复制到项目下。
  • node_modules中引入文件: @import "~bootstrap/less/bootstrap";, 通过在路径前添加~符号,告知webpack在node_modules中查询文件。

分离CSS样式

  • 尽管通过刚才的设置我们已经基本完成了对CSS样式的处理, 但还有一些问题。 首先我们的CSS样式是直接行内置在JavaScript脚本中的。这样的话浏览器端无法缓存CSS样式。并且由于浏览器会先下载并加载JavaScript,CSS样式在JavaScript执行后才会被渲染,这就会导致无风格内容闪烁(Flash of Un styled Content)。

配置MiniCssExtractPlugin

1
npm install mini-css-extract-plugin --save-dev
  • MiniCssExtractPlugin中包含loader。loader提取CSS内容, MiniCssExtractPlugin把loader提取的内容打包到一个单独的CSS文件中。

  • webpack.part.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");

    exports.extractCSS = ({ include, exclude, use = [] }) => {
    // Output extracted CSS to a file
    const plugin = new MiniCssExtractPlugin({
    filename: "[name].css",
    });

    return {
    module: {
    rules: [
    {
    test: /\.css$/,
    include,
    exclude,

    use: [
    MiniCssExtractPlugin.loader,
    ].concat(use),
    },
    ],
    },
    plugins: [plugin],
    };
    }
  • [name]占位符的值最终会被引用CSS样式的文件的名字取代。

  • webpack.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const commonConfig = merge([
    //parts.loadCSS(),
    ]);
    // const productionConfig = merge([]);
    const productionConfig = merge([
    parts.extractCSS({
    use: "css-loader",
    }),
    ]);
    const developmentConfig = merge([
    // ...
    parts.loadCSS(),
    ]);

删除无用的CSS

配置PurifyCSS

1
npm install glob purifycss-webpack purify-css --save-dev
  • webpack.parts.js

    1
    2
    3
    4
    5
    const PurifyCSSPlugin = require("purifycss-webpack");

    exports.purifyCSS = ({ paths }) => ({
    plugins: [ new PurifyCSSPlugin({ paths })],
    });
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // ...
    const path = require("path");
    const glob = require("glob");


    const parts = require("./webpack.parts");


    const PATHS = {
    app: path.join(__dirname, "src"),
    };


    //...

    const productionConfig = merge([
    //...

    parts.purifyCSS({
    paths: glob.sync(`${PATHS.app}/**/*.js`, { nodir: true }),
    }),

    ]);
  • PurifyCSS 必须放在 MiniCssExtractPlugin后执行。

Autoprefixing

  • 有些CSS规则在某些浏览器上表现异常,需要使用浏览器供应商特定的前缀来处理CSS规则异常。记住这些浏览器供应商前缀和异常CSS规则之间的对应关系很难。可以通过Autoprefix插件来完成

    配置Autoprefixing

    1
    npm install postcss-loader autoprefixer --save-dev
  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    exports.autoprefix = () => ({
    loader: "postcss-loader",
    options: {
    plugins: () => [require("autoprefixer")()],
    },
    });
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const productionConfig = merge([
    parts.extractCSS({

    use: "css-loader",


    use: ["css-loader", parts.autoprefix()],

    }),
    // ...
    ]);
  • app/main.css

    1
    2
    3
    4
    .pure-button {
    -webkit-border-radius: 1em;
    border-radius: 1em;
    }
  • 创建.browserslistrc文件,来规定需要支持的浏览器版本

    1
    2
    3
    > 1% # Browser usage over 1%
    Last 2 versions # Or last two versions
    IE 8 # Or IE 8

加载 Assets

Loader的定义

  • webpack.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    module.exports = {
    // ...
    module: {
    rules: [
    {
    // 条件判断: 通过正则表达式判断匹配文件。
    test: /\.js$/

    // 路径限制条件
    include: path.join(__dirname, "app"),
    exclude(path) {
    return path.match(/node_modules/);
    }
    // 需要执行的loader
    user: "babel-loader",
    }
    ]
    }
    }

Loader的执行顺序

  • 从下到上, 从右到左。例如:use:["style-loader", "css-loader"]可以看做style(css(input))
  • 通过指定enfore的值(prepost)来强制改变loader执行顺序。pre在其他loader之前执行,post在其他loader执行之后执行。

    向Loader传递参数

  • 通过查询格式传递参数给loader:

    1
    2
    3
    4
    5
    {
    test: /\.js$/,
    include: PATHS.app,
    use: "babel-loader?presets[]=env",
    },
  • 通过options属性传递参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    test: /\.js$/,
    include: PATHS.app,
    use: {
    loader: "babel-loader",
    options: {
    presets: ["env"],
    },
    },
    }
  • 向多个loader传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    test: /\.js$/,
    include: PATHS.app,
    use: [
    {
    loader: "babel-loader",
    options: {
    presets: ["env"],
    },
    },
    {
    loader: "react-loader",
    options: {
    presets: ["env"],
    },
    },
    // Add more loaders
    ]
    }

通过 use 分支来管理配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
test: /\.css$/,
use: ({ resource, resourceQuery, issuer }) => {
if (env === "development") {
return {
use: {
loader: "css-loader",
rules: [
"style-loader",
]
}
}
}
}
}

行内置定义loader

1
2
3
4
// process foo.png through url-loader
import "url-loader!./foo.png";
// orverride possible higher level match
import "!!url-loader!./bar.png";

匹配资源的其他方法

  • 其他常见方法:
    • resource: /inline/: 匹配资源路径中包含inline的资源,包括查询语句。例如:/path/foo.inline.js,/path/bar.png?inline.
    • issuer: /bar.js/: 匹配被bar.js文件引入的文件。
    • resourcePath: /inline/: 匹配路径中包含inline的资源。不包含查询语句。
    • resourceQuery: /inline/: 匹配资源查询语句中包含inline的资源。
  • 通过 rsourceQuery匹配文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    test: /\.png$/,
    oneOf: [
    {
    resourceQuery: /inline/,
    use: "url-loader",
    },
    {
    resourceQuery: /external/,
    use: "file-loader",
    },
    ],
    }
  • 通过issuer 匹配文件。

    • 通过issuer属性根据匹配文件被引入位置的不同作出不同的处理。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      {
      test:/\.css$/,
      rules: [
      {
      issuer: /\.js$/,
      use: "style-loader",
      },
      {
      use: "css-loader",
      },
      ],
      }

加载图像

配置 url-loader

  • url-loader 将图片以base64编码并以行内置的方式插入到打包的JavaScript文件中。这可以减少HTTP请求数量, 但是会增加打包后文件的大小。所有url-loader常用于development阶段。
  • url-loader的limit属性规定了url-loader可以处理文件的大小上限,当文件大小超过limit时,会被url-loader跳过。
  • 通过url-loader和file-loader组合使用, 可以在打包文件中内置小与limit的文件, 而大的文件则通过file-loader单独生成文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    test: /\.(jpg|png)$/,
    use: {
    loader: "url-loader",
    options: {
    limit: 25000,
    },
    },
    }

配置 file-loader

1
2
3
4
5
6
7
8
9
{
test: /\.(jpg|png)$/,
use: {
loader: "file-loader",
options: {
name: "[path][name].[hash].[ext]",
},
},
}
  • [hash]为文件内容的MD5哈希值。

    项目中加入图片

  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    exports.loadImages = ({ include, exclude, options } = {}) => ({
    module: {
    rules: [
    {
    test: /\.(png|jpg)$/,
    include,
    exclude,
    use: {
    loader: "url-loader",
    options,
    },
    },
    ],
    },
    });
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const productionConfig = merge([
    // ...
    parts.loadImages({
    options: {
    limit: 15000,
    name: "[name].[ext]",
    },
    }),

    ]);

    const developmentConfig = merge([
    // ...
    parts.loadImages(),

    ]);

加载SVGs

  • 通过file-loader处理SVGs
    1
    2
    3
    4
    {
    test: /\.svg$/,
    use: "file-loader",
    }

图片加载优化

  • 可以使用image-webpack-loader, svgo-loader或者imagemin-webpack-plugin,来压缩图片或SVG. 这些插件应该首先作用在图片数据上,所以他们作为use属性的数组的最后一项。
  • webpack.parts.js 配置image-webpack-loader
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    module: {
    rules: [
    {
    test: /\.(png|jpg)$/,
    use: [
    // ...
    {
    loader: 'image-webpack-loader',
    options: {
    mozjpeg: {
    progressive: true,
    quality: 65,
    },
    optipng: {
    enabled: false,
    },
    pngquant: {
    quality: '65-90',
    speed: 4,
    },
    gifsicle: {
    interlaced: false,
    },
    webp: {
    quality: 75,
    }
    }
    ]
    }
    ]
    }

其他

  • 通过image-trace-loaderlqip-loader在图片加载时使用占位符。
  • webpack-spritesmith 创建图片精灵 sprites.

加载字体

  • 加载字体的配置和image的配置差不多,也是通过url-loader和file-loader完成, 但是由于不同浏览器支持不同的字体格式,字体格式的匹配查找模式test变得更为复杂。
  • 除了Opera Mini外,所有浏览器都支持.woff格式.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    test: /\.woff$/,
    use: {
    loader: "url-loader",
    options: {
    limit: 5000,
    },
    },
    }
  • 所有现代浏览器都支持.woff2格式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    test: /\.(woff|woff20)(\?v=\d+\.\d+)?$/,
    use: {
    loader: "url-loader",
    options: {
    limit: 50000,
    mimetype: "application/font-wolf",
    name: "./fonts/[name].[ext]",
    },
    },
    }
  • CSS: 把.woff2格式的字体放在首位,新的格式版本相比旧版有很多优势。

    1
    2
    3
    4
    5
    6
    7
    8
    @font-face {
    font-family: "myfontfamily";
    src: url("./fonts/myfontfile.woff2") format("woff2"),
    url("./fonts/myfontfile.woff") format("woff"),
    url("./fonts/myfontfile.eot") format("embedded-opentype"),
    url("./fonts/myfontfile.ttf") format("truetype");
    /* Add other formats as you see fit */
    }

加载javascript

  • Webpack支持ES6的模块机制,但是不会转译ES6的新语法,这需要Babel来完成。

配置babel-loader

1
npm install babel-loader babel-core --save-dev
  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    exports.loadJavaScript = ({ include, exclude } = {}) => ({
    module: {
    rules: [
    {
    test: /\.js$/,
    include,
    exclude,
    use: "babel-loader",
    },
    ],
    },
    });
  • webpack.config.js

    1
    2
    3
    4
    const commonConfig = merge([
    // ...
    parts.loadJavaScript({ include: PATHS.app }),
    ]);

通过.babelrc配置Babel

  • babel-preset-env 是Babel的预设配置,根据运行环境启用响应的插件。

    1
    npm install babel-preset-env --save-dev
  • .babelrc 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "presets": [
    [
    "env",
    {
    "modules": false,
    }
    ]
    ]
    }
  • Webpack支持ES6模块机制,如果再经过Babel处理,Webpack的HMR机制就会失效,所以设置"modules": false

Building 构建

Source Maps

  • Source Map 的作用:当我们的代码被打包转译之后,代码debug成了一个问题。 当我们在浏览器中之中发现问题所在之后如何在源代码中确定出现问题的代码的位置呢? 我们可以通过Source Map来解决。

    行内置的Source Map和分离的 Source Map

  • Inline Source Map: 适合在Development阶段使用, 拥有更好的性能, 但是打包后文件更大。

  • Seperate Source Map: 适合在Production阶段使用, 分离文件不会增加打包后文件大小。
  • Hidden Source Map: 只提供调用栈信息。

开启Source Map

  • Webpack配置
  • Webpack.parts.js

    1
    2
    3
    exports.generateSourceMaps = ({ type }) => ({
    devtool: type,
    })
  • Webpack.config.js

    1
    2
    3
    4
    5
    // enable source-map for production and  let webpack use the default for development.
    const productionConfig = merge([
    parts.generateSourceMaps({ type: "source-map"}),
    // ...
    ]);
  • Output

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    Hash: b59445cb2b9ae4cea11b
    Version: webpack 4.1.1
    Time: 1347ms
    Built at: 3/16/2018 4:58:14 PM
    Asset Size Chunks Chunk Names
    main.js 838 bytes 0 [emitted] main
    main.css 3.49 KiB 0 [emitted] main
    main.js.map 3.75 KiB 0 [emitted] main
    main.css.map 85 bytes 0 [emitted] main
    index.html 220 bytes [emitted]
    Entrypoint main = main.js main.css main.js.map main.css.map
    ...

行内置Source Map

  • devtool: “eval”
    • 模块内容被包裹在eval()中.
    • Source Map的相关信息则以注释的方式表示.
  • devtool: “cheap-eval-source-map”
    • 模块内容被包裹在eval()中。
    • Source Map的相关信息则以Base64编码注释的方式保存。
  • devtool: “cheap-module-eval-source-map”
    • 与”cheap-eval-source-map”类似, 只不过生成source map内容更详细, 性能有所下降。
  • devtool: “eval-source-map”
    • 生成最详细的source map, 但也是最慢的。

分离的Source Map

  • 生成的分离Source Map文件的扩展名为.map。只有被浏览器要求时才会加载, 既保证了性能,又方便debug。

  • devtool: “cheap-source-map”

    • 类似于上述的cheap类型的source map。
    • 没有列的映射关系。
    • 从loader中生成的 source map 也不会被显示。
  • devtool: “cheap-module-source-map”
    • 类似于 “cheap-source-map”。
    • 从loader中产生的source map将被简化显示。
  • devtool: “hidden-source-map”
    • 不会将 Source Map 暴露给浏览器,但是会显示调用栈的信息。
  • devtool: “nosource-source-map”
    • 生成的source map中不包含sourcesContent, 但会得到调用栈的信息。
  • devtool: “source-map”
    • 生成质量最好的source-map, 但性能最差。

打包分割

  • 如果没有打包分割的话, 那么production模式下打包后的文件就只生成一个文件, 每次发生文件修改, 客户端都需要重新下载整个文件。 打包分割后, 客户端只需要下载发生更改后的那部分文件就可以了。
  • Webpack 4提供了一些开箱即用的打包分割。
  • 通过打包分割, 可以将应用的依赖单独打包到一个文件当中,如:vender.js, 这样可以利用客户端缓存。 应用的大小不会改变,但在初次使用时发送的HTTP请求数会增多。

增加需要分割的内容

1
npm install react react-dom --save
  • src/index.js
    1
    2
    3
    import 'react';
    import 'react-dom';
    //...

打包分割

  • Webpack4之前,打包分割需要通过CommonsChunkPlugin插件来完成, 而在Webpack4之后,可以通过配置来完成。
  • webpack.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const productionConfig = merge([
    //...
    {
    optimization: {
    splitChunks: {
    chunks: "initial",
    },
    },
    },
    ]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
npm run build

Hash: 6c499f10237fdbb07378
Version: webpack 4.1.1
Time: 3172ms
Built at: 3/16/2018 5:00:03 PM
Asset Size Chunks Chunk Names

vendors~main.js 96.8 KiB 0 [emitted] vendors~main

main.js 1.35 KiB 1 [emitted] main
main.css 1.27 KiB 1 [emitted] main

vendors~main.css 2.27 KiB 0 [emitted] vendors~main
vendors~main.js.map 235 KiB 0 [emitted] vendors~main
vendors~main.css.map 93 bytes 0 [emitted] vendors~main

main.js.map 7.11 KiB 1 [emitted] main
main.css.map 85 bytes 1 [emitted] main
index.html 329 bytes [emitted]
Entrypoint main = vendors~main.js vendors~main.css ...
...

分割和合并代码块

  • AggressiveSplitingPlugin: 可以生成更多更小的代码块。可以结合HTTP/2标志使用。
  • AggressiveMergingPlugin: 将小的模块合并。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Aggresive Splitting
    {
    plugins: {
    new Webpack.optimize.AggressiveSplittingPlugin({
    minSize: 10000,
    maxSize: 30000,
    }),
    },
    }
1
2
3
4
5
6
7
8
9
// Aggresive Merging 
{
plugins: {
new AggressiveMerginPlugin({
minSizeReduce: 2,
moveToParents: true,
}),
},
}

Webpack中代码块的种类

  • Entry Chunks: 包含webpack runtime 和之后需要加载的模块.
  • Normal Chunks: 不包含Webpack Runtime。 这些代码动态加载到应用中。
  • Initial Chunks: Normal Chunk 中的一种,影响应用的初次加载时间。

Code分割(懒加载 lazy loading)

  • 当Web应用慢慢发展,具有更多特性之后,其代码文件的大小也难免越来越大,应用首次加载时需要下载的代码文件过大,应用首次加载的时间就会很长。
  • 懒加载允许客户端从服务端下载目前需要的代码块。当用户进入应用的另外一个视图层时,则再下载该视图层对应的代码。

    代码分割格式

  • 代码分割通常使用两种方法: 动态importrequire.ensure
  • 代码分割的目的: 通过在代码中设置分割点,使对应的代码只在需要的时候被加载。

Dynamic import

  • Dynamic import语法并不是正式语言标准。需要设置Babel配置来支持这一特性。
  • Dynamic import 使用Promise:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import(/* webpackChunkName: "optional-name"*/ "./modules").then(
    module => {
    // ...
    }
    ).catch(
    error => {
    // ...
    }
    )
  • Optional Name 允许你将多个分割代码合并到一个打包后文件中,只要它们具有相同的optional-name。 默认的是每个分割点打包到单独的文件中。

  • 也可以多个 dynamic import 组合在一起。
    1
    2
    3
    4
    5
    6
    Promise.all([
    import("moduleA"),
    import("moduleB")
    ]).then(((moduleA, moduleB]) => {
    // do something
    });

配置Babel

1
npm install babel-plugin-syntax-dynamic-import --save-dev
  • .babelrc

    1
    2
    3
    {
    "plugins": ["syntax-dynamic-import"],
    }
  • 如果在使用ESlint, 需要在ESlint配置文件中设置"parser":"babel-eslint"parserOptions.allowImportExportEveryWhere: true

    通过 Dynamic import 来设置分割点

  • src/lazy.js

    1
    export default 'Hello, Lazy loading`;
  • src/component.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    export default (text = 'Hello world') => {
    const element = document.createElement('div');

    element.className = "pure-button";
    element.innerHTML = text;

    element.onClick = () => {
    import('./lazy')
    .then(lazy => {
    element.textContent = lazy.default;
    })
    .catch(err => {
    console.error(err);
    });
    }
    return element;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
npm run build

Hash: 6c8b751621f73fb39ec7
Version: webpack 4.8.1
Time: 5187ms
Built at: 2018-05-16 00:28:09
Asset Size Chunks Chunk Names
0.js.map 197 bytes 0 [emitted]
0.js 159 bytes 0 [emitted]
vendors~main.js 105 KiB 1 [emitted] vendors~main
main.css 9.22 KiB 2 [emitted] main
main.js 2.21 KiB 2 [emitted] main
vendors~main.css 1.99 KiB 1 [emitted] vendors~main
vendors~main.css.map 93 bytes 1 [emitted] vendors~main
vendors~main.js.map 251 KiB 1 [emitted] vendors~main
main.css.map 85 bytes 2 [emitted] main
main.js.map 11.3 KiB 2 [emitted] main
index.html 329 bytes [emitted]
Entrypoint main = vendors~main.css vendors~main.js vendors~main.css.map vendors~main.js.map main.css main.js main.css.map main.js.map

React 中的代码分割方法

  • React 中通过将分割点包裹在React中完成
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import React from 'react';

    <AsyncComponent loader = {() => import('./SomeComponent'))}/>

    class AsyncComponent extends React.Component{
    constructor(props) {
    super(props);
    this.state = { Component: null };
    }
    componentDidMount(){
    this.props.loader().then(Component => this.setState({ Component }));
    }
    render() {
    const { Component } = this.state;
    const { Placeholder, ...props } = this.props;

    return Component ? <Component {...props} />:<Placeholder />;
    }
    }
    AsyncComponent.propTypes = {
    loader: PropTypes.func.isRequired,
    Placeholder: PropTypes.node.isRequired,
    }

禁用代码分割

1
2
3
4
5
6
7
8
9
const webpack = require('webpack');
// ...
module.exports = {
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
};
  • 代码分割需要我们决定哪些代码需要分割, 我们可以参考router设置。

清除Build路径: 在每次build之前,清除之前build生成的文件。

设置 CleanWebpackPlugin

1
npm install clean-webpack-plugin --save-dev
  • webpack.parts.js

    1
    2
    3
    4
    const CleanWebpackPlugin = require('clean-webpack-plugin');
    exports.clean = path => ({
    plugins: [new CleanWebpackPlugin([path])],
    });
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const PATHS = {
    app: path.join(__dirname, "src"),
    build: path.join(__dirname, "dist"),
    };
    // ...
    const productionConfig = merge([
    parts.clean(PATHS.build);
    // ...
    ]);

添加Build的相关信息到Build之后的文件

配置 BannerPlugin 和 GitRevisionPlugin

1
npm install git-revision-webpack-plugin --save-dev

优化

Minifying JavaScript

  • Minification 将代码文件转化到更小的形式。安全的转化过程不会影响代码的正常工作。
  • 在Webpack 4中, Minification 通过两个配置选项: optimization.minimizeoptimization.minimizer来完成。
  • 通过 uglifyjs-webpack-plugin 插件可以修改默认设置。

    设置 Minify JavaScript

    1
    npm install uglifyjs-webpack-plugin --save-dev
  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    const UglifyWebpackPlugin = require("uglifyjs-webpack-plugin");

    exports.minifyJavaScript = () => ({
    optimization: {
    minimizer: [ new UglifyWebpackPlugin({ sourceMap: ture })],
    },
    });
  • webpack.config.js

    1
    2
    3
    4
    const productionConfig = merge([
    parts.clean(PATHS.build),
    parts.minifyJavaScript(),
    ]);

设置 Minify CSS

1
npm install optimize-css-assets-webpack-plugin cssnano --save-dev
  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    const cssnano = require('cssnano');

    exports.minifyCSS = ({ options }) => ({
    plugins: [
    new OptimizeCSSAssetsPlugin({
    cssProcessor: cssnano,
    cssProcessorOptions: options,
    canPrint: false,
    })
    ]
    })
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const productionConfig = merge([
    // ...
    parts.minifyJavaScript(),
    parts.minifyCSS({
    options: {
    discardComments: {
    removeAll: true,
    },
    safe: true,
    },
    })
    ]);

Tree Shaking

  • Tree Shaking是ES6的新特性, 它可以静态检测代码, 分析出代码的哪一部分被使用, 哪一部分没有被使用。
  • src/shake.js

    1
    2
    3
    4
    const shake = () => console.log("shake");
    const bake = () => console.log("bake");

    export { shake, bake };
  • src/index.js

    1
    2
    3
    4
    // ...
    import { bake } from "./shake";
    bake();
    // ...
  • 运行npm run build后,会发现dist/main.js中只包含console.log("bake")但不包括console.log("shake")

环境变量 Environment Variables

  • 通过设置环境变量, 我们可以控制代码的执行, 例如if (process.env.NODE_ENV === "development") { // do something },Webpack通过DefinePlugin会将环境变量转化为实际值,如if(false){ // do something },而JavaScript Minifiers 会最终移除if(false){}这样的代码。
  • Webpack 4根据mode自动设置process.env.NODE_ENV

    DefinePlugin的工作原理

    1
    2
    3
    4
    5
    6
    7
    var foo
    if (foo === 'bar') {
    console.log('bar');
    }
    if ( EnvirVar === 'bar') {
    console.log('bar');
    }
  • DefinePlugin 会替换环境变量的值, 如果EnvirVar的值为foobar则代码如下:

    1
    2
    3
    4
    5
    6
    7
    var foo;
    if (foo === 'bar') {
    console.log('bar');
    }
    if ('foobar' === 'bar') {
    console.log('bar');
    }
  • DefinePlugin 进一步分析发现 'foobar' === 'bar'结果为false。则Minifier 处理后代码如下:

    1
    2
    3
    4
    var foo;
    if (foo === 'bar') {
    console.log('bar');
    }

设置 process.env.NODE_ENV

  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const webpack = require('webpack');

    exports.setFreeVariable = (key, value) => {
    const env = {};
    env[key] = JSON.stringfy(value);
    return {
    plugins: [new webpack.DefinePlugin(env)],
    };
    };
  • webpack.config.js

    1
    2
    3
    4
    const commonConfig = merge([
    // ...
    parts.setFreeVariable("Hello", "Hello from config"),
    ]);

环境变量控制模块使用

1
2
3
4
5
6
// src/store/index.js
if (process.env.NODE_ENV === "production") {
module.exports = require('./store.prod');
} else {
module.exports = require('./store.dev');
}
  • 通过环境变量控制模块使用时,只能使用CommonJS模块机制,ES6的模块机制不支持动态加载。

    在文件名中加入哈希值

  • 尽管每次build之后生成的文件都被自动命名, 但这并不能有效利用客户端缓存, 因为客户端并不知道哪些文件有效,而哪些文件已经经过修改,其对应的缓存文件已经失效。可以在文件名中加入哈希值解决这个问题。

    常用占位符

  • [id]: 返回 chunk id。
  • [path]: 返回文件路径。
  • [name]: 返回文件名。
  • [ext]: 返回文件扩展名。
  • [hash]: 返回本次build的哈希值。
  • [chunkhash]: 返回特定entry的哈希值, 如果配置文件中的entry改变, 该值也改变。
  • [contenthash]: 返回根据内容生成的哈希值。

    设置哈希值

  • 图片和字体使用[hash]占位符, 代码块则使用[chunkhash]占位符。

  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const productionConfig = merge([
    {
    output: {
    chunkFilename: '[name].[chunkhash:4].js',
    filename: '[name].[chunkhash:4].js',
    },
    }
    // ...
    parts.loadImages({
    options: {
    limit: 15000,
    name: '[name].[hash:4].[ext]',
    },
    }),
    // ...
    ]);
  • 如果生成的CSS也是用[chunkhash]的话,则会产生一些问题,由于具有相同的entry, 所以当JavaScript(或CSS)改变时,CSS(或JavaScript)也被认定为失效。由此我们使用[contentHash]对应CSS文件。

分离Minifest文件

  • Minifest文件包含在生成的vendor bundle中, Minifest描述Webpack需要加载的文件。如果生成文件的哈希值改变了, Minifest文件将会失效。 这将导致vendor bundle文件也失效了。

    从Vendor bundle提取Minifest文件

  • webpack.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const productionConfig = merge([
    // ...
    {
    optimization: {
    splitChunks: {
    // ...
    },
    runtimeChunk: {
    name: 'manifest','
    },
    },
    },
    // ...
    ]);

Build Analysis

配置Webpack

  • package.json
    1
    2
    3
    4
    5
    6
    "scripts": {

    "build:stats": "webpack --env production --json > stats.json",

    // ...
    },

Output

Build Targets

  • 我们通过target的值来控制webpack的输出目标(output target)。

    Web Targets

  • Webpack默认使用Web作为output target。
  • 初始化需要加载的模块在manifest中标明。

    Web Workers Targets

  • 设置为Web Workers Targets可以将你的应用包装为一个Web worker。
  • Web Workers可以在主线程之外执行运算。但不可以操控DOM。

    Node Targets

  • Webpack提供了两种Node特定的Target: nodeasync-node
  • node模式下,使用require语句加载模块。通常在使用服务端渲染的情况下使用。
  • async-node模式下, 可以通过Node的fsvm加载模块。

    Desktop Targets

  • node-webkit: 生成NW.js应用。
  • atom, electron, electron-main生成Electron主进程。
  • electron-render生成Electron 渲染进程。

Multiple Pages

Generating Multiple Pages through multi-complier mode.

  • 为每个页面都设置配置文件,然后在 multi-complier模式下处理文件。
  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const HtmlWebpackPlugin = require('html-webpack-plugin');

    exports.page = ({
    path = '',
    template = require.resolve(
    'html-webpack-plugin/default_index.ejs'
    ),
    title,
    } = {}) => ({
    plugins: [
    new HtmlWebpackPlugin({
    filename: `${path && `${path}/`}index.html`,
    template,
    title,
    }),
    ],
    });
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // const HtmlWebpackPlugin = require('html-webpack-plugin');

    const commonConfig = merge([
    // {
    // plugins: [
    // new HtmlWebpackPlugin({
    // title: 'Webpack demo',
    // }),
    // ],
    // }
    // ...
    ]);

    module.exports = mode => {
    // if (mode === 'production') {
    // return merge(commonConfig, productionConfig, { mode });
    // }

    // return merge(commonConfig, developmentConfig, { mode });
    const pages = [
    parts.page({ title: 'Webpack demo'}),
    parts.page({ title: 'Another demo', path: 'another'}),
    ];
    const config = mode === 'production' ? productionConfig : developmentConfig;

    return pages.map(page => merge(commonConfig, config, page, { mode }));
    }
  • 现在构建后的页面加载相同的代码, 为了使每个页面能单独加载代码。 我们需要在每个页面的配置文件中单独的设置entry的值。

  • src/another.js

    1
    2
    3
    4
    5
    6
    import './main.css';
    import component from './component';

    const demoComponent = component('Another');

    document.body.appendChild(demoComponent);
  • webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    const commonConfig = merge([
    {
    output: {
    publicPath: '/',
    },
    },
    // ...
    ]);

    module.exports = mode => {
    const pages = [
    parts.page({
    title: 'Webpack demo',
    entry: {
    app: PATHS.app,
    },
    }),
    parts.page({
    title: 'Another demo',
    path: 'another',
    entry: {
    another: path.join(PATHS.app, 'another.js'),
    },
    }),
    ];
    const config = mode === 'production' ? productionConfig : developmentConfig;

    return pages.map(page => merge(commonConfig, config, page, { mode }));
    }
  • webpack.parts.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    exports.page = (
    {
    path = "",
    template = require.resolve(
    "html-webpack-plugin/default_index.ejs"
    ),
    title,

    entry,

    } = {}
    ) => ({

    entry,

    plugins: [
    new HtmlWebpackPlugin({
    filename: `${path && path + "/"}index.html`,
    title,
    }),
    ],
    });