关于webpack:爪哇学习笔记Webpack原理

前端工程化

  • 技术选型
  • 对立标准——eslint、husky
  • 测试、部署、监控——ut、e2e、mock
  • 性能优化
  • 模块化重构

webpack流程

webpack的构建流程能够分为以下三大阶段:

  • 初始化:启动构建,读取与合并配置参数,加载Plugin,实例化Compiler
  • 编译:从Entry登程,针对每个Module串行调用对应Loader去翻译文件的内容,再找到该Module依赖的Module,递归地进行编译解决。
  • 输入:将编译后的Module组合成Chunk,将Chunk转换成文件,输入到文件系统中。

Loader

Loader就像一个翻译员,能将源文件通过转化后输入新的后果,并且一个文件还能够链式地通过多个翻译员翻译。

在开发一个Loader时,请确保其职责的单一性,咱们只须要关怀输出和输入。

根底

Webpack是运行在Node.js上的,一个Loader其实就是一个Node.js模块,这个模块须要导出一个函数。这个导出的函数的工作就是取得解决前的原内容,对原内容执行解决后,返回解决后的内容。
一个最简的的Loader的源码如下:

// source为compiler传递给Loader的一个文件的原内容
module.exports = function(source) {
    // TODO: 对文件内容进行解决
    return source;
}

因为Loader运行在Node.js中,所以咱们能够调用任意Node.js自带的API,或者装置第三方模块进行调用:

const sass = require('node-sass');
module.exports = function(source) {
    return sass(source);
}

进阶

取得Loader的options

const loaderUtils = require('loader-utils'); // getOptions办法要求loader-utils版本为2.x
module.exports = function(source) {
    // 获取用户为以后Loader传入的options
    const options = loaderUtils.getOptions(this);
    return source;
}

返回其余后果

下面的Loader都只是返回了原内容转换后的内容,然而在某些场景下还须要返回除了内容之外的货色。
以用babel-loader转换ES6为例,它还须要输入转换后的ES5代码对应的Source Map,以不便调试源码。

module.exports = function(source) {
    this.callback(null, source, sourceMaps);
    return;
}

其中的this.callback是Webpack向Loader注入的API(The Loader Context),以不便Loader和Webpack之间通信。

this.callback
能够同步或者异步调用的并返回多个后果的函数。预期的参数是:

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);

如果这个函数被调用的话,你应该返回 undefined 从而防止含混的 loader 后果。

异步

module.exports = function(source) {
    let callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, meta) {
        callback(err, result, sourceMaps, meta);
    });
}

this.async
通知loader-runner这个loader将会异步地回调。返回this.callback

解决二进制数据

在默认状况下,Webpack传给Loader的原内容都是UTF-8格局编码的字符串。但在某些场景下Loader不会解决文本文件,而会解决二进制文件如file-loader,这时就须要Webpack为Loader传入二进制格局的数据。

module.exports = function(source) {
    if(source instanceof Buffer === true) {
        // TODO: 解决二进制内容
    }

    // Loader返回的类型也能够是Buffer类型
    return source;
}
// 通过exports.raw属性通知webpack该Loader是否须要二进制数据
module.exports.raw = true;

缓存减速

Webpack会默认缓存所有Loader的处理结果,以防止每次构建都从新执行反复的转换操作,从而放慢构建速度。
敞开缓存性能:

module.exports = function(source) {
    this.cacheable(false);
    return source;
}

其余Loader API

详见Loader Interface

加载本地Loader

Npm link

Npm link专门用于开发和调试本地的Npm模块,能做到在不公布模块的状况下,将本地的一个正在开发的模块的源码链接到我的项目的node_modules目录下,这让我的项目能够间接应用本地的Npm模块。
步骤如下:

  • 确保正在开发的本地Npm模块的package.json已配置好;
  • 在本地的Npm根目录下执行npm link,将本地模块注册到全局;
  • 在我的项目根目录下执行npm link loader-name,将第二步注册到全局的本地Npm模块链接到我的项目的node_modules下,其中loader-name是指在第一步的package.json中配置的模块名称。

ResolveLoader

ResolveLoader用于配置Webpack如何寻找Loader,它在默认状况下只会去node_modules目录下寻找。

module.exports = {
  //...
  resolveLoader: {
    modules: ['node_modules'],
    extensions: ['.js', '.json'],
    mainFields: ['loader', 'main'],
  },
};

Plugin

Webpack通过Plugin机制让其更灵便,以适应各种场景。在Webpack运行的生命周期中会播送许多事件,Plugin能够监听这些事件,在适合的机会通过Webpack提供的API扭转输入后果。
一个最根底的Plugin的代码是这样的:

// webpack3
class BasicPlugin {
    constructor(options) {}

    apply(compiler) {
        compiler.plugin('compilation', function(compilation) {});
    }
}

module.exports = BasicPlugin;

在webpack5的官网文档中对plugin的解释如下
plugin的目标在于解决loader无奈实现的其余事,是对于webpack性能的扩大。
webpack plugin是一个具备apply办法的JavaScript对象。apply办法会被webpack compiler调用,并且在整个编译生命周期都能够拜访compiler对象。

// webpack4,webpack5
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
    constructor(options) {}

    apply(compiler) {
        compiler.hooks.run.tap(pluginName, (compilation) => {
            console.log('webpack 构建正在启动!');
        });
    }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

Compiler和Compilation

  • Compiler对象蕴含了Webpack环境的所有配置信息,蕴含options、loaders、plugins等信息。这个对象在Webpack启动时被实例化,它是全局惟一的,能够简略地将它了解为Webpack实例。
  • Compilation对象蕴含了以后的模块资源、编译生成资源、变动的文件等。当Webpack以开发模式运行时,每当检测到一个文件发生变化,便有一次新的Compilation被创立。Compilation对象也提供了很多事件回调供插件进行扩大。通过Compilation也能读取到Compiler对象。

Compiler和Compilation的区别在于:Compiler代表了整个Webpack从启动到敞开的生命周期,而Compilation只代表一次新的编译。

Compiler钩子

Compiler模块是webpack的次要引擎,它通过CLI或者Node API传递的所有选项创立出一个compilation实例。它扩大(extends)自Tapable类,用来注册和调用插件。 大多数面向用户的插件会首先在Compiler上注册。

// 钩子函数调用形式
compiler.hooks.someHook.tap('MyPlugin', (params) => {
  /* ... */
});

// 示例
compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => {
  /* ... */
});

Compilation钩子

Compilation模块会被Compiler用来创立新的compilation对象(或新的 build 对象)。 compilation 实例可能拜访所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和从新创立(restore)。

// 钩子函数调用形式
compilation.hooks.someHook.tap(/* ... */);

// 示例
compilation.hooks.buildModule.tap(
  'SourceMapDevToolModuleOptionsPlugin',
  (module) => {
    module.useSourceMap = true;
  }
);

事件流

Webpack就像一条生产线,要通过一系列解决流程后能力将源文件转换成输入后果。Webpack通过Tabable来组织这条简单的生产线。Webpack在运行的过程中会播送事件,插件只须要监听它关怀的事件,就能退出这条生产线中,去扭转生产线的运作。

// webpack3
// 播送事件,留神不要和现有事件重名
compiler.apply('event-name', params);
compilation.apply('event-name', params);

// 监听事件
compiler.plugin('event-name', function(params) {});
compilation.plugin('event-name', function(params) {});

Tabable

这个小型库是webpack的一个外围工具,但也可用于其余中央,以提供相似的插件接口。 在webpack中的许多对象都扩大自Tapable类。 它对外裸露了taptapAsynctapPromise等办法, 插件能够应用这些办法向webpack中注入自定义构建的步骤,这些步骤将在构建过程中触发。

依据应用不同的钩子(hooks)和 tap 办法, 插件能够以多种不同的形式运行。 这个工作形式与 Tapable 提供的钩子(hooks)密切相关。 compiler hooks 别离记录了 Tapable 外在的钩子, 并指出哪些 tap 办法可用。

自定义钩子

// 须要简略的从`tapable`中require所需的hook类,并创立
const SyncHook = require('tapable').SyncHook;

if (compiler.hooks.myCustomHook) throw new Error('已存在该钩子');
compiler.hooks.myCustomHook = new SyncHook(['a', 'b', 'c']);

// 在你想要触发钩子的地位/机会下调用……
compiler.hooks.myCustomHook.call(a, b, c);

All Hook constructors take one optional argument, which is a list of argument names as strings.

const hook = new SyncHook(["arg1", "arg2", "arg3"]);

罕用的钩子

  1. 读取Webpack的处理结果/批改输入资源:emit钩子,输入asset到output目录之前执行

    compiler.hooks.emit.tap('MyPlugin', (compilation, callback) => {
        // do something
        callback();
    });
  2. 监听文件的变动:afterCompile钩子,compilation 完结和封印之后执行

    compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => {
        // 将指定文件增加到文件依赖列表中
        commpilation.fileDependencies.push(filePath);
        callback();
    });

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理