Webpack2初识

3,704 阅读7分钟

本文为腾讯移动分析MTA 产品UI工程师linji的分享。

聚焦webpack2的部分目录:

温故或知新

简单地概括--webpack是一个智能模块依赖分析的打包工具,它通过入口js文件以及一系列的插件能把各种资源文件编译、打包为可发布使用的静态资源。如官网中的视图:万物皆模块。

安装

文章撰写时webpack最新正式版版本号为2.5.1。

命令行工具中安装:

# 全局安装
npm install -g webpack
# 初始化项目
npm init
#安装到项目目录中
npm install --save-dev webpack

快速开始

在根目录创建如下文件:
entry.js -- 入口文件

import fn from './fn';
fn();

fn.js -- 被引用的模块

export default function fn() {
  document.write('I am a Function');
}

webpack.config.js -- webpack配置文件

module.exports = {
  entry: './entry.js',
  output: {
    filename: 'bundle.js'
  }
}

index.html -- 静态页面

<!DOCTYPE html>
<html>
  <body>
    <script src="./bundle.js"></script>
  </body>
</html>

在项目目录中执行命令 webpack,然后在浏览器打开上面的静态页面就可以看到效果了

变化

现在简单列出我认为对比webpack1版比较明显的变化

  1. ES6模块的静态导入(import等部分ES6语法的原生支持): 更快捷的初始化工程

  2. ES6模块的动态导入:按官方文档描述,这种动态导入方式正尝试加入到ES的规范之中

    • 创建async.js
      export default function async() {
        console.log('异步引用');
      }
    • entry.js 加入代码

      import('./async').then(async => {
        async.default();
      });
      // 等价于
      /*
      require.ensure([], (require) => {
        const async = require('./async');
        async.default();
      });
      */

      import()返回的是promise实例,所以我们还可以通过Promise.all([import('./async')]).then(([async]) => {})的形式去引用多个模块;不过,当你需要兼容IE8-的浏览器时,你需要额外导入promise-polyfill

      System.import()和import()方法作用相同,在本文撰写期间的2.5.x版本前者依然可以使用,但官方文档有指出这个方法不建议使用

  3. 修复ugulifyJS压缩css文件时误删除[dpr]问题:当然我们可以通过别的工具去代替

  4. 函数方式导出配置:可以通过参数来代替设置复杂的环境变量

    • 修改 webpack.config.js, 并执行 webpack --env.dev
      module.exports = function(env) {
      return {
       entry: './entry.js',
       output: {
         filename: 'bundle.js'
       },
       // 新的“环境变量”使用方式 (此处新增了sourcemap)
       devtool: env.dev
         ? 'cheap-module-eval-source-map'
         : false
       };
      };
  5. 配置更清晰简洁, 更多查看后文配置“升级”
  6. Tree shaking:去除无用代码,适用于文件目录结构复杂的项目,更多查看后文了解 Tree shaking

配置“升级”

  • module.loaders --> modules.rules
    使用loader时需要把loader名字写全,如 style 需要写为 style-loader

  • 移除 module.preLoaders, module.postLoaders
    现在通过module.rules.enforce属性来配置

  • 不再需要手动加入 DedupePlugin, OccurrenceOrderPlugin, json-loader, devServer.inline 等配置
    毕竟几乎大家都需要用到它们

  • webpack-dev-server, extract-text-webpack-plugin, html-webpack-plugin 这三个常用的工具必须升级到2.x版本,否则无法和webpack2作用

  • 合并分散的resolve配置 resolve.root, resolve.fallback, resolve.modulesDirectories
    通过配置好 resolve.modules 有助于提高定位引用文件的速度

    const path = require('path');
    module.exports = function(env) {
      return {
        // ...
        resolve: {
          modules: [
            path.resolve(__dirname, 'node_modules'),
          ],
        }
       };
    };

下图为以前一次把个迷你项目从webpack1升级到webpack2的基本配置变更

了解TreeShaking

  • Tree shaking 示例
    修改fn.js

    // ...
    export function unused(){
      document.write('I am an unused Function');
    }

    修改entry.js

    import fn, {unused} from './fn';
    fn();
    // ...

    执行 webpack --env(这里不使用env.dev来查看较为美观的打包代码)后,我们会发现生成的bundle代码中出现如下片段


    此时再执行 webpack --optimize-minimize --env ,打包的代码经过压缩处理后,unused()这个没有被调用的函数被彻底删掉了。
    简单而然,webpack2 中的 tree shaking就是一个按需打包模块一种实现。但需要tree shaking生效,被引用的模块必须遵循es6模块导出的规范,详看后文。

  • Tree shaking 起源
    在前端圈子中,该解释 “起源”于 rollup.js 作者。
    Rollup中的 Tree-shaking 是无用代码移除(DCE, dead code elimination)的一种实现。利用AST(Abstract Syntax Tree,译作 抽象语法树),找到被使用的代码块然后注入到输出文件中。而Webpack2“原生”支持的ES6模块静态性引用,可以帮助标记编译后的多余代码,并在压缩时"甩掉"它们。

    更多关于抽象语法树的理解可以通过在线抽象语法树解释工具去自行体验

  • ES6模块的静态性的作用:

    1. DCE
    2. 快速定位代码

      • 使用Commonjs去引用库的时候,调用其子属性会触发对象属性查表,这会降低编译的效率
        const lib = require('lib'); lib.someFunc(); // 属性查表

      • 而import的静态特性使其跳过这个步骤;
        import * as lib from 'lib'; lib.someFunc(); // 静态处理

    3. 提高变量校验效率

    4. 为macro(宏)做准备,现有macro库 sweetjs.org/
  • Tree shaking 横向对比
    babel-plugin-import 是一个类tree shaking的babel插件,它能够把 import { Button } from ‘antd’; 这种代码转换为 import Button ‘antd/lib/button’; 的形式。

    • 优点在于
      o 不硬性要求引用ES6模块
      o 构建速度较快
    • 缺点在于
      x 需指定按需打包的模块
      x 自己创建的模块使用不便
      x 无法把同一个文件里的多个export分离
      x 配置错误将会导致依赖丢失

    如今的webpack2的tree shaking 主要适用范围如下
    import {lastName} form 'name'; -- named import
    import name from 'name'; -- default import
    import * as all from 'name;
    const name = require('name');

    如今并不是所有开源组件都提供ES6模块的代码,所以如果想最有效的去除多余代码,把 babel-plugin-import 和 webpack2 Tree shaking组合使用来打包压缩生产环境的代码为当下最优选择。

  • Webpack2 和 Babel 组合的短暂演变
    在使用webpack的同时,我们大多都需要使用到babel来支持转换ES6、ES7、TS等一系列语法糖,那一旦使用babel,务必要防止babel对import语法先行处理。为了达到这个目的,babel的配置也经历过比较明显的转变最终简单易懂:

1-2速度以及打包大小对比

该速度对比数据为同样代码每项执行5次后取最大时长的数据,代码如下:

import react from 'react'; // react15.5.4 不提供ES6模块
import {compose} from 'refux'; // redux3.6.0 提供ES6模块

速度

webpack1 webpack2
dev-server 启动服务 2187ms 2252ms
dev-server 文件更改 41ms 27ms
production 1991ms 2057ms

打包大小

webpack1 webpack2
dev-server 1.38MB 1.75MB
production 53.3kB 48.5kB

由此可见,在开发过程中,webpack2打包速度较快但需加载的代码文件大小较大;生产环境代码webpack2生成的文件体积更小(但这要求开发者遵循使用ES6模块的写法)

Webpack2优缺点汇总

个人最后总结的优缺点如下

  • 优点
    o change文件打包效率提高
    o 升级快捷
    o 加强code splitting时chunk文件的错误定位(这个真心棒)
    o 官方文档更加清晰
    o 和babel6完美契合
  • 缺点
    x 自带的tree shaking对非ES6模块和对象动态属性无效
    x 动态import实现方式不标准,还需要 bable-plugin-syntax-dynamic-import
    x Chrome下devtool为cheap-module-source-map等时源码定
位有数行误差
    x 后文中的Issue

    随着webpack2版本迭代,以上缺点正慢慢改正

结语

我想不到任何一个不升级的理由,毕竟这是正式版了。:) 如文中有与实际不对等的情况,务必告诉我好让我能撇除误解~~~

Issue

webpack2处理原生ES6 Module时,无法使用babel-plugin-add-module-exports插件。该插件能让babel6处理export时,如同babel5一样,生成语句 module.exports = exports["default"];webpack2中export default转换后为 __webpack_exports__["default"] 而非 exports["default"],故执行代码时会报错。

参考

What's new in webpack 2