浅尝webpack

吐槽一下

webpack 自出现时,一直备受青睐。做为强大的打包工具,它只是出如今项目初始或优化的阶段。若是没有参与项目的构建,接触的机会几乎为零。即便是参与了,但也会由于项目的周期短,从网上东拼西凑草草了事。javascript

纵观网上的 webpack 教程,要么是走马观花,科普了一些常规配置项;要么是过于深刻原理,于实际操做无益。最近一段时间,我把 webpack 的官方文档来来回回地看了几遍,结果发现,真香。中文版的官方文档,通俗易懂,很感谢翻译组的辛勤奉献。看完以后,虽然达不到炉火纯青的地步,但也不会捉襟见肘,疲于应付。前端

对于这种工具类的博文,依然沿袭 用Type驯化JavaScript 的风格,串联各个概念。至于细节,就是官方文档的事了。vue

本文基于 webpack v4.31.0 版本。java

Tapable

Tapable 是一个小型的库,容许你对一个 javascript 模块添加和应用插件。它能够被继承或混入到其余模块中。相似于 NodeJS 的 EventEmitter 类,专一于自定义事件的触发和处理。除此以外,Tapable 还容许你经过回调函数的参数,访问事件的“触发者(emittee)”或“提供者(producer)”。node

tapable 是 webpack 的核心,webpack 中的不少对象(compile, compilation等)都扩展自tapable,包括 webpack 也是 tapable 的实例。扩展自 tapable 的对象内部会有不少钩子,它们贯穿了 webpack 构建的整个过程。咱们能够利用这些钩子,在其被触发时,作一些咱们想作的事情。react

抛开 webpack 不谈,先看看 tapable 的简单使用。jquery

// Main.js
const {
  SyncHook
} = require("tapable");
class Main {
  constructor(options) {
    this.hooks = {
      init: new SyncHook(['init'])
    };
    this.plugins = options.plugins;
    this.init();
  }
  init() {
    this.beforeInit();
    if (Array.isArray(this.plugins)) {
      this.plugins.forEach(plugin => {
        plugin.apply(this);
      })
    }
    this.hooks.init.call('初始化中。。。');
    this.afterInit();
  }
  beforeInit() {
    console.log('初始化前。。。');
  }
  afterInit() {
    console.log('初始化后。。。');
  }
}
module.exports = Main;
// MyPlugin.js
class MyPlugin {
  apply(main) {
    main.hooks.init.tap('MyPlugin', param => {
      console.log('init 钩子,作些啥;', param);
    });
  }
};
module.exports = MyPlugin;
// index.js
const Main = require('./Main');
const MyPlugin = require('./MyPlugin');
let myPlugin = new MyPlugin();
new Main({ plugins: [myPlugin] });

// 初始化前。。。
// init 钩子,作些啥; 初始化中。。。
// 初始化后。。。
复制代码

理解起来很简单,就是在 init 处触发钩子,this.hooks.init.call(params) 相似于咱们熟悉的 EventEmitter.emit('init', params)main.hooks.init.tap 相似于 EventEmitter.on('init', callback),在 init钩子上绑定一些咱们想作的事情。在后面将要说的 webpack 自定义插件,就是在 webpack 中的某个钩子处,插入自定义的事。webpack

理清概念

  • 依赖图 在单页面应用中,只要有一个入口文件,就能够把散落在项目下的各个文件整合到一块儿。何谓依赖,当前文件须要什么,什么就是当前文件的依赖。依赖引入的形式有以下:web

    • ES2015 import 语句
    • CommonJS require() 语句
    • AMD definerequire 语句
    • 样式(url(...))或 HTML 文件(<img src=...>)中的图片连接
  • 入口(entry) 入口起点(entry point)指示 webpack 应该使用哪一个模块,来做为构建其内部依赖图(dependency graph)的开始。json

  • 输出(output) output 属性告诉 webpack 在哪里输出它所建立的 bundle,以及如何命名这些文件。

  • 模块(module) 决定了如何处理项目中的不一样类型的模块。好比设置 loader,处理各类模块。设置 noParse,忽略无需 webpack 解析的模块。

  • 解析(resolve) 设置模块如何被解析。引用依赖时,须要知道依赖间的路径关系,应遵循何种解析规则。好比给路径设置别名(alias),解析模块的搜索目录(modules),解析 loader 包路径(resolveLoader)等。

  • 外部扩展(externals) 防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖。好比说,项目中引用了 jQuery 的CDN资源,在使用 import $ from 'jquery';时,webpack 会把 jQuery 打包进 bundle,其实这是没有必要的,此时须要配置 externals: {jquery: 'jQuery'},将其剔除 bundle。

  • 插件(plugins) 用于以各类方式自定义 webpack 构建过程。能够利用 webpack 中的钩子,作些优化或者搞些小动做。

  • 开发设置(devServer) 顾名思义,就是开发时用到的选项。好比,开发服务根路径(contentBase),模块热替换(hot,需配合 HotModuleReplacementPlugin 使用),代理(proxy)等。

  • 模式(mode) 提供 mode 配置选项,告知 webpack 使用相应环境的内置优化。具体可见 模式(mode)

  • 优化(optimization) 从 webpack 4 开始,会根据你选择的 mode 来执行不一样的优化,不过全部的优化仍是能够手动配置和重写。好比,CommonsChunkPluginoptimization.splitChunks 取代。

webpack 差很少就是这几个配置项,搞清楚这几个概念,上手仍是比较容易的。

代码分离

如今的前端项目愈来愈复杂,若是最终导出为一个 bundle,会极大地影响加载速度。切割 bundle,控制资源加载优先级,按需加载或并行加载,合理应用就会大大缩短加载时间。官方文档提供了三种常见的代码分离方法:

  • 入口起点 配置多个入口文件,而后将最终生成的过个 bundle 出入到 HTML 中。

    // webpack.config.js
    entry: {
        index: './src/index.js',
        vendor: './src/vendor.js'
    }
    output: {
        filename: '[name].bundle.js',
    },
    plugins: [
    new HtmlWebpackPlugin({
        chunks: ['vendor', 'index']
    })
    ]
    复制代码

    不过若是这两个文件中存在相同的模块,这就意味着相同的模块被加载了两次。此时,咱们就须要提取出重复的模块。

  • 防止重复 在 webpack 老的版本中,CommonsChunkPlugin 经常使用来提取公共的模块。新版本中 SplitChunksPlugin 取而代之,能够经过 optimization.splitChunks 设置,多见于多页面应用。

  • 动态导入 就是在须要时再去加载模块,而不是一股脑的所有加载。webpack 还提供了预取和预加载的方式。非入口 chunk,咱们能够经过 chunkFilename 为其命名。常见的如,vue 路由动态导入。

    // webpack.config.js
    output: {
      chunkFilename: '[name].bundle.js',
    }
    // index.js
    import(/* webpackChunkName: "someJs" */ 'someJs');
    import(/* webpackPrefetch: true */ 'someJs');
    import(/* webpackPreload: true */ 'someJs');
    复制代码

缓存

基于浏览器的缓存策略,咱们知道若是本地缓存命中,则无需再次请求资源。对于改动不频繁或基本不会再作改动的模块,能够剥离出来。

// webpack.config.js
  output: {
    filename: '[name].[contenthash].js',
  }
复制代码

按照咱们的想法,只要模块的内容没有变化,对应的名字也就不会发生变化,这样缓存就会起做用了。事实上并不是如此,webpack 打包后的文件,并不是只有用户本身的代码,还包括管理用户代码的代码,如 runtime 和 manifest。

模块依赖间的整合并非简单的代码拼接,其中包括模块的加载和解析逻辑。注入的 runtime 和 manifest 在每次构建后都会发生变化。这就致使了即便用户代码没有变化,某些 hash 仍是发生了改变。经过 optimization.runtimeChunk 提取 runtime 代码。经过 optimization.splitChunks 剥离第三方库。好比, react,react-dom。

module.exports = {
  //...
  optimization: {
  runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'vendor',
          chunks: 'all',
        }
      }
    }
  }
};
复制代码

最后使用 HashedModuleIdsPlugin 来消除因模块 ID 变更带来的影响。

loader

loader 用于对模块的源代码进行转换。loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 loader API,并经过 this 上下文访问。

// loader API;
this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);
// sync loader
module.exports = function(content, map, meta){
  this.callback(null, syncOperation(content, map, meta));
  return;
}
// async loader
module.exports = function(content, map, meta){
  let callback = this.async();
  asyncOperation(content, (error, result) => {
    if(error) callback(error);
    callback(null, result, map, meta);
    return;
  })
}
复制代码

多个 loader 串行时,在从右向左执行 loader 以前,会向从左到右调用 loader 上的 pitch 方法。若是在 pitch 中返回告终果,则会跳事后续 loader。

|- a-loader `pitch`
  |- b-loader `pitch`
 |- c-loader `pitch`
 |- requested module is picked up as a dependency
 |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

<!-- pitch 中返回结果 -->

|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution
复制代码

plugins

webpack 的自定义插件和本文开头 Tapable 中的差很少。webpack 插件是一个具备 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,而且 compiler 对象可在整个编译生命周期访问。钩子有同步的,也有异步的,这须要根据 webpack 提供的 API 文档。

// 官方例子
class FileListPlugin {
  apply(compiler) {
    // emit 是异步 hook,使用 tapAsync 触及它,还可使用 tapPromise/tap(同步)
    compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
      // 在生成文件中,建立一个头部字符串:
      var filelist = 'In this build:\n\n';
      // 遍历全部编译过的资源文件,
      // 对于每一个文件名称,都添加一行内容。
      for (var filename in compilation.assets) {
        filelist += '- ' + filename + '\n';
      }
      // 将这个列表做为一个新的文件资源,插入到 webpack 构建中:
      compilation.assets['filelist.md'] = {
        source: function() {
          return filelist;
        },
        size: function() {
          return filelist.length;
        }
      };
      callback();
    });
  }
}
module.exports = FileListPlugin;
复制代码
  • ProvidePlugin 自动加载模块,无需到处引用。有点相似 expose-loader

    // webpack.config.js
    new webpack.ProvidePlugin({
      $: 'jquery',
    })
    // some.js
    $('#item');
    复制代码
  • DllPlugin 将基础模块打包进动态连接库,当依赖的模块存在于动态连接库中时,无需再次打包,而是直接从动态连接库中获取。DLLPlugin 负责打包出动态连接库,DllReferencePlugin 负责从主要配置文件中引入 DllPlugin 插件打包好的动态连接库文件。

    // webpack-dll-config.js
    // 先执行该配置文件
    output: {
      path: path.join(__dirname, "dist"),
      filename: "MyDll.[name].js",
      library: "[name]_[hash]"
    },
    plugins: [
      new webpack.DllPlugin({
        path: path.join(__dirname, "dist", "[name]-manifest.json"),
        name: "[name]_[hash]"
      })
    ]
    // webpack-config.js
    // 后执行该配置文件
    plugins: [
      new webpack.DllReferencePlugin({
        manifest: require("../dll/dist/alpha-manifest.json")
      }),
    ]
    复制代码
  • HappyPack 启动子进程处理任务,充分利用资源。不过进程间的通信比较耗资源,要酌情处理。

    const HappyPack = require('happypack');
    // loader
    {
      test: /\.js$/,
      use: ['happypack/loader?id=babel'],
      exclude: path.resolve(__dirname, 'node_modules'),
    },
    // plugins
    new HappyPack({
      id: 'babel',
      loaders: ['babel-loader?cacheDirectory'],
    }),
    复制代码
  • webpack-bundle-analyzer
    webpack 打包后的分析工具。

webpack 告一段落,浅尝辄止。

相关文章
相关标签/搜索