Webpack相关知识点总结(基础,性能优化)

418 阅读6分钟

随着前端的不断发展,前端进入了模块化开发的时代,同时也产生了各种高阶的支持能力,比如TS(Typescript),Less,Sass等,这些是不能直接被浏览器识别的,我们需要借助工具将其转化为浏览器能识别的代码再进行部署,由此便产生了编译打包工具,例如Webpack,rollup,gulp等。

今天我们这里讨论的就是webpack这一款主流的编译打包工具,注意本文将重心放在了如何优化的思路上,而关于Webpack必备的基础知识讲一笔带过,不会做过多的分析和解释,有兴趣的可以自己前往webpack中文站阅读。

Webpack基础

本质上,webpack 是一个用于现代 JavaScript 应用程序的_静态模块打包工具_。当 webpack 处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle

入口(entry)

入口就是webpack开始编译的起点,设置入口之后webpack就会根据入口的文件来开始解析和入口文件有关的库和依赖。这里一般默认值为./src/index.js,如果存在多种入口编译的也是通过修改这里实现的。

module.exports = {
  entry: './src/index.js',
};

这里的entry可以是一个或者多个。

module.exports = {
  entry: {
    pc: './src/pc/index.js',
    mobile: './src/mobile/index.js',
    pad: './src/pad/index.js'
  }
};

当涉及到不同平台加载的依赖不同的时候就可以使用上述方式进行配置。

输出(output)

指定编译过后的bundle输出在何处,以及如何命名。

const path = require('path');

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

一般大家都会默认把编译结果放到dist目录下。

loader

这是webpack最核心的能力之一,因为webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。

module.exports = {
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};

上图就是一个关于解析txt文件的loader,基本使用格式就如同上诉代码所示。

这么写的含义是让webpack打包遇到import了.txt文件的时候,优先使用raw-loader去解析加载。

插件(Plugin)

这也是webpack的特色之一,被广大开发者追捧。loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建一个插件实例。

const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

模式(mode)

通过选择 development, productionnone 之中的一个,来设置 mode 参数。这个一般是用来区分测试环境和生产环境的。一般来说我们会在生成环境对代码进行压缩混淆,而dev环境不做这些操作。

Webpack优化

我们将webpack的优化分为一下几步。

1)缩小文件搜索范围

2)开启多进程操作

3)代码压缩

4)代码分包

5)使用Tree Shaking

6)提取公共代码

7)按需加载

缩小文件搜索访问

Webpack 启动后会从配置的 Entry 出发,解析出文件中的导入语句,再递归的解析。 在遇到导入语句时 Webpack 会做两件事情:

  1. 根据导入语句去寻找对应的要导入的文件。例如 require('react')`导入语句对应的文件是 ./node_modules/react/react.js,require('./util')对应的文件是 ./util.js。
  2. 根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件。例如使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理。

上述操作如果只是单纯的一个文件还好,但是在一个庞大的项目中这造成的时间损耗就非常庞大了。

Loader

由于 Loader 对文件的转换操作很耗时,需要让尽可能少的文件被 Loader 处理。

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx$/,
        use: ['ts-loader?cacheDirectory'],
        include: path.resolve(__dirname, 'src/typescript'),
      },
    ]
  },
};

尽可能的细化需要转换的路径(路径得写明确),减少loader访问的时间。

第三方模块统一管理

resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块。

resolve.modules的默认值是 ['node_modules'],含义是先去当前目录下的 ./node_modules目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules`中找,以此类推,这和 Node.js 的模块寻找机制很相似。

这里我们也需要尽可能的去写明白该去何处寻找第三方依赖。

module.exports = {
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    // 其中 __dirname 表示当前工作目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')]
  },
};

开启多进程操作

由于有大量文件需要解析和处理,构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack 构建慢的问题会显得严重。 运行在 Node.js 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。

但是文件读写是不可避免的,那么这里我们是否可以转变一下思路,让 Webpack 同一时刻处理多个任务,发挥多核 CPU 电脑的威力,以提升构建速度呢?

由于 JavaScript 是单线程模型,要想发挥多核 CPU 的能力,只能通过多进程去实现,而无法通过多线程实现。

答案是可以的,我们可以通过HappyPack这个插件去使任务,进行多进程操作,代码如下图所示。

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
        use: ['happypack/loader?id=babel'],
        // 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        // 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css'],
        }),
      },
    ]
  },
  plugins: [
    new HappyPack({
      // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // 如何处理 .js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory'],
      // ... 其它配置项
    }),
    new HappyPack({
      id: 'css',
      // 如何处理 .css 文件,用法和 Loader 配置中一样
      loaders: ['css-loader'],
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,
    }),
  ],
};

以上代码有两处修改点。

1.在loader中表明本类型的文件交给HappyPack处理。

2.在Plugins中实例化对应的对象进行处理。

开启代码压缩

在使用 Webpack 构建出用于发布到线上的代码时,都会有压缩代码这一流程。 最常见的压缩工具是UglifyJS,在webpack中内置了该插件。

由于压缩 JavaScript 代码需要先把代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST,导致这个过程计算量巨大,耗时非常多。

这里就可以接上我们上一点了,为什么我们不多进程操作的思想引入代码压缩之中呢?

ParallelUglifyPlugin 就做了这个事情。 当 Webpack 有多个 JavaScript 文件需要输出和压缩时,原本会使用 UglifyJS 去一个个挨着压缩再输出, 但是 ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。 所以 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工作。

具体代码如下所示:

const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  plugins: [
    // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS 的参数
      uglifyJS: {
        output: {
          // 最紧凑的输出
          beautify: false,
          // 删除所有的注释
          comments: false,
        },
        compress: {
          // 在UglifyJs删除没有用到的代码时不输出警告
          warnings: false,
          // 删除所有的 `console` 语句,可以兼容ie浏览器
          drop_console: true,
          // 内嵌定义了但是只用到一次的变量
          collapse_vars: true,
          // 提取出出现多次但是没有定义成变量去引用的静态值
          reduce_vars: true,
        }
      },
    }),
  ],
};

压缩代码

js代码是前端优化的非常重要的一点,在前一章节,我们已经说过了。

这里主要看压缩CSS。

CSS 代码也可以像 JavaScript 那样被压缩,以达到提升加载速度和代码混淆的作用。 目前比较成熟可靠的 CSS 压缩工具是 cssnano,基于 PostCSS,通常压缩率能达到 60%。。

const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,// 增加对 CSS 文件的支持
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          // 通过 minimize 选项压缩 CSS 代码
          use: ['css-loader?minimize']
        }),
      },
    ]
  },
  plugins: [
    // 用 WebPlugin 生成对应的 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路径
      filename: 'index.html' // 输出的 HTML 的文件名称
    }),
    new ExtractTextPlugin({
      filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 Hash 值
    }),
  ],
};

其他优化

其实代码的优化还有很多可以做的,比如Tree Shaking 可以用来剔除 JavaScript 中用不上的死代码,使用CDN加速网络访问,开启按需加载,动态加载。

参考文献

1)深入浅出 Webpack:webpack.wuhaolin.cn/

2)Webpack官方文档:webpack.docschina.org/concepts/