编写webpack loader和插件

webpack简介

基本概念

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

工做流程

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

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

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

编写loader

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

初始化

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

    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 代码返回给 Webpackweb

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 的详细使用方法以下:npm

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

同步与异步

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

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 传入二进制格式的数据。api

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 模块代码,在项目中也能使用到编辑后的代码。babel

完成 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;}

写在最后

参考文章

推荐阅读

相关文章
相关标签/搜索