Entry
:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。Module
:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出全部依赖的模块。Chunk
:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。Loader
:模块转换器,用于把模块原内容按照需求转换成新内容。Plugin
:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件能够监听这些事件的发生,在特定时机作对应的事情Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行如下流程:node
在以上过程当中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,而且插件能够调用 Webpack 提供的 API 改变 Webpack 的运行结果。webpack
职责:一个 Loader 的职责是单一的,只须要完成一种转换。git
module.exports = function(source) { // source 为 compiler 传递给 Loader 的一个文件的原内容 // 对source进行一些操做 以后返回给下一个loader return source; };
得到 Loader 的 options
github
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;
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:{...})
。Npm link 专门用于开发和调试本地 Npm 模块,能作到在不发布模块的状况下,把本地的一个正在开发的模块的源码连接到项目的 node_modules
目录下,让项目能够直接使用本地的 Npm 模块。 因为是经过软连接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。babel
完成 Npm link 的步骤以下:网络
package.json
已经正确配置好;npm link
,把本地模块注册到全局;npm link loader-name
,把第2步注册到全局的本地 Npm 模块连接到项目的 node_moduels
下,其中的 loader-name
是指在第1步中的 package.json
文件中配置的模块名称。连接好 Loader 到项目后你就能够像使用一个真正的 Npm 模块同样使用本地的 Loader 了。
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 插件由哪些构成,下面摘抄文档:
插件由一个构造函数实例化出来。构造函数定义 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 表明了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是表明了一次新的编译。
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的各类信息。
若是把compiler算做是总控制台,那么compilation则专一于编译处理这件事上。
在启用Watch模式后,webpack将会监听文件是否发生变化,每当检测到文件发生变化,将会执行一次新的编译,并同时生成新的编译资源和新的compilation对象。
compilation对象中包含了模块资源、编译生成资源以及变化的文件和被跟踪依赖的状态信息等等,以供插件工做时使用。若是咱们在插件中须要完成一个自定义的编译过程,那么必然会用到这个对象。
插件能够用来修改输出文件、增长输出文件、甚至能够提高 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 当前的插件配置状况。 以判断当前是否使用了 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;}