资讯专栏INFORMATION COLUMN

浅尝webpack

villainhr / 2000人阅读

摘要:用于对模块的源代码进行转换。将基础模块打包进动态链接库,当依赖的模块存在于动态链接库中时,无需再次打包,而是直接从动态链接库中获取。负责打包出动态链接库,负责从主要配置文件中引入插件打包好的动态链接库文件。告一段落,浅尝辄止。

吐槽一下

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 文件()中的图片链接

入口(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



|- 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:

";
      // 遍历所有编译过的资源文件,
      // 对于每个文件名称,都添加一行内容。
      for (var filename in compilation.assets) {
        filelist += "- " + filename + "
";
      }
      // 将这个列表作为一个新的文件资源,插入到 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://www.ucloud.cn/yun/109637.html

相关文章

  • webpack开发环境配置

    摘要:本次的重点主要集中在开发环境上,生产环境则是使用的默认模式。开发环境开发环境没什么好说的了,简单易配置,官网很详细。 日常吐槽 经过不断的调整和测试,关于 react 的 webpack 配置终于新鲜出炉。本次的重点主要集中在开发环境上,生产环境则是使用 webpack 的 production 默认模式。 本次配置主要有: eslint+prettier; optimizati...

    FuisonDesign 评论0 收藏0
  • 2017-08-02 前端日报

    摘要:前端日报精选浮点数精度之谜前端面试必备基本排序算法从贺老微博引出的遍历器加速那些奥秘进阶之深入理解数据双向绑定全栈天中文深入理解笔记用模块封装代码前端架构经验分享周二放送自制知乎专栏译在大型应用中使用的五个技巧掘金开发指南众成 2017-08-02 前端日报 精选 JavaScript 浮点数精度之谜前端面试必备——基本排序算法从贺老微博引出的遍历器(Iterators)加速那些奥秘J...

    Worktile 评论0 收藏0
  • 翻译 | 上手 Webpack ? 这篇就够了!

    摘要:最后,我们在控制台中打印这个新数组。也可以借助简单的将其跑在浏览器上,之后可在控制台中看到同样的运行结果。使用配置文件虽然会更占位置,但与此同时增加了可读性,因为它是由写成的。例如,规定后缀的文件要先通过检查,再通过把语法转换为语法。 译者:小 boy (沪江前端开发工程师) 本文原创,转载请注明作者及出处。 原文地址:https://www.smashingmag...

    codercao 评论0 收藏0
  • graphql-js 浅尝

    摘要:系列文章核心概念浅尝本文常言道,实践是检验真理的唯一标准。遵循传统,第一个例子必须是。官方提供这个中间件来支持基于的查询,所以,这里选用作为服务器。首先是,这里对做了一点小修改,给几个字段添加了不能为空的设计。 系列文章: GraphQL 核心概念 graphql-js 浅尝(本文) 常言道,实践是检验真理的唯一标准。 上一篇文章讲了 GraphQL 的核心概念,所提到的一些例...

    gyl_coder 评论0 收藏0
  • 浅尝正则表达式

    摘要:同样的你也可以测试第四次执行的时候就会是了,需要知道的是,只有在全局检索时才会生效,否则的话只会返回哦方法二使用正则表达式模式对字符串执行搜索,并将更新全局对象的属性以反映匹配结果。 之前写正则都是各种上网搜索,还是没有系统的学习过正则表达式的用法,今天稍稍研究了一下下,感觉还是收获颇丰的,分享给各位,希望对于你们有所帮助~~ 修饰符 g --全局匹配 i --不区分大小写,默认...

    HelKyle 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<