编写webpack loader和插件

时间:2020-7-5 作者:admin

webpack简介

基本概念

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情

工作流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

编写webpack loader和插件

编写loader

职责:一个 Loader 的职责是单一的,只需要完成一种转换。

初始化

module.exports = function(source) {  
    // source 为 compiler 传递给 Loader 的一个文件的原内容  
    // 对source进行一些操作 之后返回给下一个loader
    return source;
};
  • 获得 Loader 的 options

    const loaderUtils = require('loaderutils');
    module.exports = function(source) {  
        // 获取到用户给当前 Loader 传入的 options 
        const options = loaderUtils.getOptions(this);
        // 根据不同的options 进行不同的操作
        return source;
    };

返回其它结果

例如以用 babel-loader转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码。 为了把 Source Map 也一起随着 ES5 代码返回给 Webpack

module.exports = function(source) { 
    this.callback(null, source, sourceMaps); 
    // 通过 this.callback 告诉 Webpack 返回的结果
    //当使用this.callback返回内容时,该 Loader 必须返回undefined以让 Webpack 知道该 Loader 返回的结果this.callback 中,而不是 return 中   
    return;
};

其中的 this.callback是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。 this.callback的详细使用方法如下:

this.callback(    
    // 当无法转换原内容时,给 Webpack 返回一个 Error   
    err: Error | null,    
    // 原内容转换后的内容    
    content: string | Buffer,    
    // 用于把转换后的内容得出原内容的 Source Map,方便调试    sourceMap?: SourceMap,    
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能    
    abstractSyntaxTree?: AST
);

同步与异步

但在有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢。

module.exports = function(source) {    
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果    
    var callback = this.async();    
    someAsyncOperation(
    source, 
    function(err, result, sourceMaps, ast) {  
    // 通过 callback 返回异步执行后的结果
    callback(err, result, sourceMaps, ast);   
    });
};

处理二进制数据

在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据。

module.exports = function(source) {    
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的    
    source instanceof Buffer === true;    
    // Loader 返回的类型也可以是 Buffer 类型的    
    // 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果    
    return source;
    };
    // 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据 
    module.exports.raw = true;

其它 Loader API(Loader API地址)

  • this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context就等于 /src
  • this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1
  • this.resourcePath:当前处理文件的路径,例如 /src/main.js
  • this.resourceQuery:当前处理文件的 querystring
  • this.target:等于 Webpack 配置中的 Target。
  • this.loadModule:但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 this.loadModule(request:string,callback:function(err,source,sourceMap,module))去获得 request对应文件的处理结果。
  • this.resolve:像 require语句一样获得指定文件的完整路径,使用方法为 resolve(context:string,request:string,callback:function(err,result:string))
  • this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file:string)
  • this.addContextDependency:和 addDependency类似,但 addContextDependency是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory:string)
  • this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()
  • this.emitFile:输出一个文件,使用方法为 emitFile(name:string,content:Buffer|string,sourceMap:{...})

加载本地 Loader

Npmlink

Npm link 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules目录下,让项目可以直接使用本地的 Npm 模块。 由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 Npm link 的步骤如下:

  • 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json已经正确配置好;
  • 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
  • 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels下,其中的 loader-name是指在第1步中的 package.json文件中配置的模块名称。

链接好 Loader 到项目后你就可以像使用一个真正的 Npm 模块一样使用本地的 Loader 了。

ResolveLoader

ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认情况下只会去 node_modules目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules

假如本地的 Loader 在项目目录中的 ./loaders/loader-name中,则需要如下配置:

module.exports = {  
    resolveLoader:{    
    // 去哪些目录下寻找 Loader,有先后顺序之分   
    modules: ['node\_modules','./loaders/'\],  }
}

加上以上配置后, Webpack 会先去 node_modules项目下寻找 Loader,如果找不到,会再去 ./loaders/目录下寻找。

编写插件

Webpack 插件组成

在自定义插件之前,我们需要了解,一个 Webpack 插件由哪些构成,下面摘抄文档:

  • 一个具名 JavaScript 函数;
  • 在它的原型上定义 apply 方法;
  • 指定一个触及到 Webpack 本身的事件钩子
  • 操作 Webpack 内部的实例特定数据;
  • 在实现功能后调用 Webpack 提供的 callback。

Webpack 插件基本架构

插件由一个构造函数实例化出来。构造函数定义 apply方法,在安装插件时,apply方法会被 Webpack compiler调用一次。apply方法可以接收一个 Webpack compiler对象的引用,从而可以在回调函数中访问到 compiler对象。

官方文档提供一个简单的插件结构:

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Hello World Plugin', (
      stats /* 在 hook 被触及时,会将 stats 作为参数传入。 */
    ) => {
      console.log('Hello World!');
    });
  }
}
module.exports = HelloWorldPlugin;

使用插件:

// webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
  // ... 这里是其他配置 ...
  plugins: [new HelloWorldPlugin({ options: true })]
};

插件触发时机

Webpack 提供钩子有很多,完整具体可参考文档《Compiler Hooks

  • entryOption: 在 webpack 选项中的 entry配置项 处理过之后,执行插件。
  • afterPlugins: 设置完初始插件之后,执行插件。
  • compilation: 编译创建之后,生成文件之前,执行插件。。
  • emit: 生成资源到 output目录之前。
  • done: 编译完成。

compiler.hooks下指定事件钩子函数,便会触发钩子时,执行回调函数。
Webpack 提供三种触发钩子的方法:

  • tap:以同步方式触发钩子;
  • tapAsync:以异步方式触发钩子;
  • tapPromise:以异步方式触发钩子,返回 Promise;

compiler和compilation介绍

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

compiler

webpack的compiler模块是其核心部分。其包含了webpack配置文件传递的所有选项,包含了诸如loader、plugins等信息。

我们可以看看Compiler类中定义的一些核心方法。

//继承自Tapable类,使得自身拥有发布订阅的能力
class Compiler extends Tapable {
  //构造函数,context实际传入值为process.cwd(),代表当前的工作目录
  constructor(context) {
    super();
    // 定义了一系列的事件钩子,分别在不同的时刻触发
    this.hooks = {
      shouldEmit: new SyncBailHook(["compilation"]),
      done: new AsyncSeriesHook(["stats"]),
      //....更多钩子
    };
    this.running = true;
    //其他一些变量声明
  }

  //调用该方法之后会监听文件变更,一旦变更则重新执行编译
  watch(watchOptions, handler) {
    this.running = true;
    return new Watching(this, watchOptions, handler)
  }

  //用于触发编译时所有的工作
  run(callback) {
    //编译之后的处理,省略了部分代码
    const onCompiled = (err, compilation) => {
      this.emitAssets(compilation, err => {...})
    }
  }

  //负责将编译输出的文件写入本地
  emitAssets(compilation, callback) {}

  //创建一个compilation对象,并将compiler自身作为参数传递
  createCompilation() {
    return new Compilation(this);
  }

  //触发编译,在内部创建compilation实例并执行相应操作
  compile() {}


  //以上核心方法中很多会通过this.hooks.someHooks.call来触发指定的事件

}

可以看到,compiler中设置了一系列的事件钩子和各种配置参数,并定义了webpack诸如启动编译、观测文件变动、将编译结果文件写入本地等一系列核心方法。在plugin执行的相应工作中我们肯定会需要通过compiler拿到webpack的各种信息。

compilation

如果把compiler算作是总控制台,那么compilation则专注于编译处理这件事上。

在启用Watch模式后,webpack将会监听文件是否发生变化,每当检测到文件发生变化,将会执行一次新的编译,并同时生成新的编译资源和新的compilation对象。
compilation对象中包含了模块资源、编译生成资源以及变化的文件和被跟踪依赖的状态信息等等,以供插件工作时使用。如果我们在插件中需要完成一个自定义的编译过程,那么必然会用到这个对象。

常用 API(全部API)

插件可以用来修改输出文件、增加输出文件、甚至可以提升 Webpack 性能、等等,总之插件通过调用 Webpack 提供的 API 能完成很多事情。

读取输出资源、代码块、模块及其依赖

有些插件可能需要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。

emit事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。

监听文件变化

Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation。

在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation

默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码:

compiler.plugin('after-compile', 
    (compilation, callback) => {  
    // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译 
    compilation.fileDependencies.push(filePath);   
    callback();}
);

修改输出资源

有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit事件,因为发生 emit事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit事件是修改 Webpack 输出资源的最后时机。

所有需要输出的资源会存放在 compilation.assets中, compilation.assets是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

设置 compilation.assets的代码如下:

compiler.plugin('emit',
(compilation, callback) => {  
    // 设置名称为 fileName 的输出资源  
    compilation.assets[fileName] = {    
        // 返回文件内容    
        source: () => {      
            // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer      
            return fileContent;      
        },    
        // 返回文件大小      
        size: () => {      
            return Buffer.byteLength(fileContent, 'utf8');    
        }  
    };  
    callback();
}
);

读取 compilation.assets的代码如下:

compiler.plugin('emit', 
(compilation, callback) => {  
    // 读取名称为 fileName 的输出资源  
    const asset = compilation.assets[fileName];  
    // 获取输出资源的内容 
    asset.source();  
    // 获取输出资源的文件大小 
    asset.size(); 
    callback();
 });

判断 Webpack 使用了哪些插件

在开发一个插件时可能需要根据当前配置是否使用了其它某个插件而做下一步决定,因此需要读取 Webpack 当前的插件配置情况。 以判断当前是否使用了 ExtractTextPlugin 为例,可以使用如下代码:

// 判断当前配置使用了 ExtractTextPlugin,compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {  
// 当前配置所有使用的插件列表  
const plugins = compiler.options.plugins;  
// 去 plugins 中寻找有没有 ExtractTextPlugin 的实例  
return plugins.find(plugin=>plugin.\_\_proto\_\_.constructor === ExtractTextPlugin) != null;}

写在最后

参考文章

推荐阅读

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。