浅尝webpack

栏目: JavaScript · 发布时间: 6年前

内容简介:webpack 自出现时,一直备受青睐。作为强大的打包工具,它只是出现在项目初始或优化的阶段。如果没有参与项目的构建,接触的机会几乎为零。即使是参与了,但也会因为项目的周期短,从网上东拼西凑草草了事。纵观网上的 webpack 教程,要么是蜻蜓点水,科普了一些常规配置项;要么是过于深入原理,于实际操作无益。最近一段时间,我把 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 告一段落,浅尝辄止。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Analytics 2.0

Web Analytics 2.0

Avinash Kaushik / Sybex / 2009-10-26 / USD 39.99

The bestselling book Web Analytics: An Hour A Day was the first book in the analytics space to move beyond clickstream analysis. Web Analytics 2.0 will significantly evolve the approaches from the fir......一起来看看 《Web Analytics 2.0》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器