webpack只能处理 js 、json 模块,对于css、img 等其他非js模块是无法默认处理的。因此,就需要loader了,它能将非js模块的资源打包成js文件,进行后续处理。
Loader源码
它是在 doBuild方法中执行的,具体可看webpack源码03中的第二部分。
内部主要两部分:
- 创建loader上下文对象
- 通过loader对源代码进行编译
doBuild(options, compilation, resolver, fs, callback) {// 创建loader上下文对象const loaderContext = this.createLoaderContext(resolver,options,compilation,fs);// 通过loader进行编译runLoaders({resource: this.resource,loaders: this.loaders,context: loaderContext,readResource: fs.readFile.bind(fs)},// 此时loader已经加载结束了// result中有一个result数组,包含了经过loader处理后的源代码(err, result) => {...})}
01 => loader上下文对象
createLoaderContext(resolver, options, compilation, fs) {...const loaderContext = {version: 2,...resolve(context, request, callback) {...},getResolve(options) {...},emitFile: (name, content, sourceMap, assetInfo) => {...},rootContext: options.context,webpack: true,sourceMap: !!this.useSourceMap,mode: options.mode || "production",_module: this,_compilation: compilation,_compiler: compilation.compiler,fs: fs}// 调用Tapable的normalModuleLoader钩子compilation.hooks.normalModuleLoader.call(loaderContext, this);// 和传入的loader进行合并if (options.loader) {Object.assign(loaderContext, options.loader);}return loaderContext;}
02 => 编译loader
内部会调用 iteratePitchingLoaders 函数,当该函数出发回调时,loader流程便结束了
function runLoaders(options, callback) {// 设置属性...// 内部对loader进行处理iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {if(err) {...}// 最终返回一些属性// result => loader编译后的源代码callback(null, {result: result,resourceBuffer: processOptions.resourceBuffer,cacheable: requestCacheable,fileDependencies: fileDependencies,contextDependencies: contextDependencies});});}// 当loader进入到了最后一个时候,会进入 processResource 函数function iteratePitchingLoaders(options, loaderContext, callback) {// loader传递的是一个数组// loaderContext.loaderIndex => 当前loader的长度,初始为0// 意味着即使只有一个loader,也会重新进入 iteratePitchingLoaders 函数if(loaderContext.loaderIndex >= loaderContext.loaders.length)return processResource(options, loaderContext, callback);// 获取当前项的loadervar currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];// currentLoaderObject.pitchExecuted初始为false,后续就将其改为trueif(currentLoaderObject.pitchExecuted) {loaderContext.loaderIndex++;// 重新进入return iteratePitchingLoaders(options, loaderContext, callback);}// loadLoader => 加载对应的模块// var module = require(loader.path)// 执行第二个参数loadLoader(currentLoaderObject, function(err) {if (err) {...}var fn = currentLoaderObject.pitch;// 将currentLoaderObject.pitchExecuted设为truecurrentLoaderObject.pitchExecuted = true;// 没有设置pitch时候为trueif(!fn) return iteratePitchingLoaders(options, loaderContext, callback);// 如果设置了pitch,会执行fn并传入参数...var result = (function LOADER_EXECUTION() {return fn.apply(context,[loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}]);}());...}}// 加载对应的loader// 最终进入 iterateNormalLoaders 函数function processResource() {// 设置loaderIndex为最后一个下标loaderContext.loaderIndex = loaderContext.loaders.length - 1;// 获取资源路径var resourcePath = loaderContext.resourcePath;// 不论怎样,都会执行 iterateNormalLoaders 函数if(resourcePath) {// 往loader中增加依赖loaderContext.addDependency(resourcePath);// 读取资源// options.readResource => fs.readFileoptions.readResource(resourcePath, function(err, buffer) {if(err) return callback(err);options.resourceBuffer = buffer;iterateNormalLoaders(options, loaderContext, [buffer], callback);});} else {iterateNormalLoaders(options, loaderContext, [null], callback);}}function iterateNormalLoaders(options, loaderContext, args, callback) {// 在 processResource 函数中,loaderContext.loaderIndex被设置成了loaders数组的最后一项// 后续会递减,因此,loader是从后往前执行的if(loaderContext.loaderIndex < 0) return callback(null, args);// 获取当前项loadervar currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];// currentLoaderObject.normalExecuted初始为false// 将loaderContext.loaderIndex-- ,并重新执行自身if(currentLoaderObject.normalExecuted) {loaderContext.loaderIndex--;return iterateNormalLoaders(options, loaderContext, args, callback);}var fn = currentLoaderObject.normal;// 将currentLoaderObject.normalExecuted设置truecurrentLoaderObject.normalExecuted = true;// 将args从Buffer转换成utf-8格式convertArgs(args, currentLoaderObject.raw);// 之后,执行loader...// 内部,会通过babel.transform对source进行编译// 然后会执行callback,重新进入 processResource 函数// 最后,执行 processResource 函数中的callback,返回loader处理后的结果loader.call(this, source, inputSourceMap, overrides).then(args => callback(null, ...args), err => callback(err));}
自定义Loader
// webpack.config.js
const path = require('path');
module.exports = {
...
module: {
rules: [{
test: /\.js/,
use: [
{
loader: path.resolve(__dirname, './myLoader.js'),
options: {
name: 'oh'
}
}
]
}]
}
}
// myLoader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// getOptions可以拿到webpack.config.js中的option
const options = loaderUtils.getOptions(this);
// 替换文件中所有 "test" 的字符串为 "oh"
const result = source.replace('test', options.name);
// 因为使用的function不是箭头函数,因此this指向是正确的
this.callback(null, result);
}
Plugin源码
严格来说,plugin并没有源码,只是在一开始进入的时候,调用了apply方法,因此,核心就是绑定Tapable的钩子,之后在编译的过程中适当时候触发即可。
compiler对象身上绑定了webpack的几乎所有属性,因此,插件能做的时候很多
// 在webpack.js入口文件,就挂载了plugins
// 会调用内部的apply方法,并传入compiler
// compiler中有许多 Tapable 的钩子,在自定义插件时,挂载 Tabpable 的钩子,等待适时的触发即可
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
// 如果是函数类型,则将this指向compiler,并传参compiler
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
// 调用apply方法并传参compiler
// 此处也是为什么,写插件的时候需要提供一个apply方法
plugin.apply(compiler);
}
}
}
自定义Plugin
根据传入的compiler对象,传入Tapable钩子即可
// MyPlugin.js
module.exports = class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tap('emit', () => {
console.log('这是我的emit')
})
}
}
// webpack.config.js
const MyPlugin = require('./util');
module.exports = {
...
plugins: [
new MyPlugin()
]
}
