浅尝webpack

吐槽一下

webpack 自出现时,一向备受喜爱。作为壮大的打包东西,它只是出如今项目初始或优化的阶段。假如没有介入项目的构建,打仗的时机险些为零。纵然是介入了,但也会由于项目的周期短,从网上东拼西凑敷衍了事。

纵观网上的 webpack 教程,要么是走马观花,科普了一些通例设置项;要么是过于深切道理,于实际操作无益。近来一段时刻,我把 webpack 的官方文档来来回回地看了几遍,效果发明,真香。中文版的官方文档,通俗易懂,很谢谢翻译组的辛劳贡献。看完以后,虽然达不到出神入化的田地,但也不会左支右绌,疲于敷衍。

关于这类东西类的博文,依旧因循 用Type驯化JavaScript 的作风,串连各个观点。至于细节,就是官方文档的事了。

本文基于 webpack v4.31.0 版本。

Tapable

Tapable 是一个小型的库,许可你对一个 javascript 模块增加和运用插件。它能够被继续或混入到其他模块中。相似于 NodeJS 的 EventEmitter 类,专注于自定义事宜的触发和处置惩罚。除此之外,Tapable 还许可你经由历程回调函数的参数,接见事宜的“触发者(emittee)”或“供应者(producer)”。

tapable 是 webpack 的中心,webpack 中的许多对象(compile, compilation等)都扩大自tapable,包含 webpack 也是 tapable 的实例。扩大自 tapable 的对象内部会有许多钩子,它们贯串了 webpack 构建的悉数历程。我们能够应用这些钩子,在其被触发时,做一些我们想做的事变。

抛开 webpack 不谈,先看看 tapable 的简朴运用。

// 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 中的某个钩子处,插进去自定义的事。

理清观点

  • 依靠图
    在单页面运用中,只需有一个进口文件,就能够把散落在项面前目今的各个文件整合到一同。何谓依靠,当前文件须要什么,什么就是当前文件的依靠。依靠引入的情势有以下:

    • ES2015 import 语句
    • CommonJS require() 语句
    • AMD definerequire 语句
    • 款式(url(...))或 HTML 文件(<img src=...>)中的图片链接
  • 进口(entry)
    进口出发点(entry point)指导 webpack 应当运用哪一个模块,来作为构建其内部依靠图(dependency graph)的最先。
  • 输出(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: {
    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 告一段落,浅尝辄止。

    原文作者:夜暁宸
    原文地址: https://segmentfault.com/a/1190000019228467
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞