Webpack源码解析
// 使用webpack版本
"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
打包主流程分析
上一篇文章 doResolve最终回调链路 分析了模块的 Loader和 Dependencies 经过插件解析最终经过层层回调到 _addModuleChain 中,此时一个完整的 module 信息已经形成,后续就是对这个 module 进行最终的 build。
_addModuleChain 中 module 创建完毕后的 buildModule
build 的流程不复杂,主要是加载 Loader,执行 Loader 的 pitch 函数和 normal 函数,对资源进行解析处理,最终将解析结果返回到 build 中的回调中,经过this.parser.parse()进行最终的解析,此时解析结果包含资源内容和资源其它依赖集合,将结果交由 addModuleChain 中 afterBuild 处理时会查找 module 是否存在其它依赖集合,存在的话会递归解析依赖,最终返回整个 module 的解析结果。
// node_modules/webpack/lib/Compilation.js
/**
* Builds the module object
*
* @param {Module} module module to be built
* @param {boolean} optional optional flag
* @param {Module=} origin origin module this module build was requested from
* @param {Dependency[]=} dependencies optional dependencies from the module to be built
* @param {TODO} thisCallback the callback
* @returns {TODO} returns the callback function with results
*/
buildModule(module, optional, origin, dependencies, thisCallback) {
// 取出当前module打包的回调,这里是第一次打包,所以为undefined
let callbackList = this._buildingModules.get(module);
if (callbackList) {
callbackList.push(thisCallback);
return;
}
// 设置building回调列表
this._buildingModules.set(module, (callbackList = [thisCallback]));
// build后的最终回调,build已经完成,删除 module 对应的 building 回调
// 然后依次执行回调
const callback = (err) => {
this._buildingModules.delete(module);
for (const cb of callbackList) {
// 执行回调,回到_addModuleChain
cb(err);
}
};
// 执行挂载在buildModule hook上的函数
this.hooks.buildModule.call(module);
// 进行build
module.build(
this.options, // 配置选项
this, // Compilation实例
this.resolverFactory.get("normal", module.resolveOptions), // NormalResolver {"normal|{}" => Resolver}
this.inputFileSystem,
(error) => {
// 当回到这个回调的时候说明文件的AST树已经生成,并经过每个分支的解析,所有依赖全部添加至module,下面就可以进行依赖的加载处理
// 错误处理
const errors = module.errors;
for (let indexError = 0; indexError < errors.length; indexError++) {
const err = errors[indexError];
err.origin = origin;
err.dependencies = dependencies;
if (optional) {
this.warnings.push(err);
} else {
this.errors.push(err);
}
}
// 警告处理
const warnings = module.warnings;
for (
let indexWarning = 0;
indexWarning < warnings.length;
indexWarning++
) {
const war = warnings[indexWarning];
war.origin = origin;
war.dependencies = dependencies;
this.warnings.push(war);
}
// 依赖排序
const originalMap = module.dependencies.reduce((map, v, i) => {
map.set(v, i);
return map;
}, new Map());
module.dependencies.sort((a, b) => {
const cmp = compareLocations(a.loc, b.loc);
if (cmp) return cmp;
return originalMap.get(a) - originalMap.get(b);
});
if (error) {
this.hooks.failedModule.call(module, error);
return callback(error);
}
this.hooks.succeedModule.call(module);
// 最终执行_addModuleChain中的afterBuild,存在依赖的话会递归加载依赖,加载过程和普通file类似,都是从NormalModule create开始
return callback();
}
);
}
module.build 首先会到 ResolverFactory 模块中通过 get 方法找到 build 的模块:
// node_modules/webpack/lib/ResolverFactory.js
get(type, resolveOptions) {
resolveOptions = resolveOptions || EMTPY_RESOLVE_OPTIONS;
const ident = `${type}|${JSON.stringify(resolveOptions)}`;
// 这里的ident是normal,在这里就是从cache2中取出NormalModule并返回,如果没有就创建一个
// 此时再调用 module.build 是其实就是在调用 NormalModule 的 build 方法
const resolver = this.cache2.get(ident);
if (resolver) return resolver;
const newResolver = this._create(type, resolveOptions);
this.cache2.set(ident, newResolver);
return newResolver;
}
获取到 NormalModule 后执行它的 build 方法,build 经过打包信息整合后调用 doBuild 进行打包:
// node_modules/webpack/lib/NormalModule.js
build(options, compilation, resolver, fs, callback) {
// 打包时间戳
this.buildTimestamp = Date.now();
// 建成标识
this.built = true;
// 资源
this._source = null;
// 资源大小
this._sourceSize = null;
// AST树
this._ast = null;
// 打包hash
this._buildHash = "";
// 错误和告警
this.error = null;
this.errors.length = 0;
this.warnings.length = 0;
// 打包元信息
this.buildMeta = {};
// 打包信息
this.buildInfo = {
cacheable: false,
fileDependencies: new Set(),
contextDependencies: new Set(),
assets: undefined,
assetsInfo: undefined,
};
/**
* options webpack 配置项
* compilation Compilation实例
* resolver NormalModuleResolve
*/
return this.doBuild(options, compilation, resolver, fs, (err) => {
this._cachedSources.clear();
// 如果返回错误信息,表示创建module失败,退出打包
if (err) {
this.markModuleAsErrored(err);
this._initBuildHash(compilation);
return callback();
}
// 检测request是否是防止被解析的请求,如果是直接创建build hash,并执行回调终止解析
// webpack可以通过配置 noParse 阻止一些资源被重复解析,有的第三方依赖本来就是一个独立的依赖模块
// 已经被编译打包好的,无需再次经过编译解析、再次打包,可以节省资源消耗
const noParseRule = options.module && options.module.noParse;
if (this.shouldPreventParsing(noParseRule, this.request)) {
this._initBuildHash(compilation);
return callback();
}
// 解析失败结果
const handleParseError = (e) => {
const source = this._source.source();
const loaders = this.loaders.map((item) =>
contextify(options.context, item.loader)
);
const error = new ModuleParseError(this, source, e, loaders);
this.markModuleAsErrored(error);
this._initBuildHash(compilation);
return callback();
};
// 解析成功结果,生成build hash,执行buildModule回调
const handleParseResult = (result) => {
this._lastSuccessfulBuildMeta = this.buildMeta;
this._initBuildHash(compilation);
return callback();
};
// 此时文件内容已经经过Loader解析,此时调用Module的parse方法进行最终的解析
try {
const result = this.parser.parse(
// this._source:
// _name:'/Users/xueyong/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js??ref--4!/Users/xueyong/lagou-edu/webpack-entry/src/index.js'
// _value:'import vue from 'vue';\nconsole.log(vue);'
// __proto__:Source
// this._source 的 __proto__ 继承至 node_modules/webpack-sources/lib/OriginalSource.js
// 它的source 方法返回 _value,即Loader处理后的文件内容
this._ast || this._source.source(),
{
current: this, // 当前module
module: this, // 当前module
compilation: compilation, // 当前Compilation实例
options: options, // 当前webpack参数选项
},
(err, result) => {
if (err) {
handleParseError(err);
} else {
handleParseResult(result);
}
}
);
// 经过各个分支的处理解析,最终将所有依赖全部添加至 module 的 depend 上:
// lastHarmonyImportOrder:1
// harmonySpecifier:Map(1) {vue => {source: 'vue', …}}
// harmonyParserScope:{}
// current:NormalModule {dependencies: Array(5), blocks: Array(0), variables: …}
// compilation:Compilation {_pluginCompat: SyncBailHook, hooks: {…}, name: undefined, …}
// module:NormalModule {dependencies: Array(5), blocks: Array(0), variables: Array(0), type: 'javascript/auto', …}
// options:{entry: './src/index.js', output: {…}, mode: 'development', devtool: 'none', plugins: Array(1), …}
// __proto__:Object
// 然后调用handleParseResult将最终结果返回
if (result !== undefined) {
// parse is sync
handleParseResult(result);
}
} catch (e) {
handleParseError(e);
}
});
}
doBuild(options, compilation, resolver, fs, callback) {
// 生成Loader对象
// _compilation:Compilation {_pluginCompat: SyncBailHook, hooks: {…}, name: ... }
// _compiler:Compiler {_pluginCompat: SyncBailHook, hooks: {…}, name: undefined, parentCompilation: ...}
// _module:NormalModule {dependencies: Array(0), blocks: Array(0), variables: Array(0), type: 'javascript/auto', ...}
// emitError:error => {...}
// emitFile:(name, content, sourceMap, assetInfo) => {...}
// emitWarning:warning => {...}
// exec:(code, filename) => {...}
// fs:CachedInputFileSystem {fileSystem: NodeJsInputFileSystem,_statStorage:...}
// getResolve:getResolve(options) {...}
// getLogger:name => {..}
// loadModule:(request, callback) => {…}
// mode:'development'
// resolve:ƒ resolve(context, request, callback) {\n\t\t\t\tresolver.resolve({}, context, request, {}, callback);\n\t\t\t}
// rootContext:'/Users/---/lagou-edu/webpack-entry'
// sourceMap:false
// target:'web'
// version:2
// webpack:true
// __proto__:Object
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
// 运行Loaders
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs),
},
(err, result) => {
// loader pitch 和 normal处理结果 result:
// cacheable:true
// fileDependencies:(1) ['/Users/---/lagou-edu/webpack-entry/src/index.js']
// resourceBuffer:Buffer(39) [...]
// result:(2) ['import vue from 'vue';\nconsole.log(vue);', null]
// contextDependencies:(0) []
// __proto__:Object
// 设置缓存和依赖信息
if (result) {
this.buildInfo.cacheable = result.cacheable;
this.buildInfo.fileDependencies = new Set(result.fileDependencies);
this.buildInfo.contextDependencies = new Set(
result.contextDependencies
);
}
// 接收到runLoaders的错误信息,反馈至build函数回调,终止打包,抛出错误信息
if (err) {
if (!(err instanceof Error)) {
err = new NonErrorEmittedError(err);
}
const currentLoader = this.getCurrentLoader(loaderContext);
const error = new ModuleBuildError(this, err, {
from:
currentLoader &&
compilation.runtimeTemplate.requestShortener.shorten(
currentLoader.loader
),
});
return callback(error);
}
// 资源buffer
const resourceBuffer = result.resourceBuffer;
// 资源内容
const source = result.result[0];
// 设置sourceMap
const sourceMap = result.result.length >= 1 ? result.result[1] : null;
// 设置额外的信息
const extraInfo = result.result.length >= 2 ? result.result[2] : null;
// 如果返回的解析内容既不是一个buffer,也不是一个string,返回错误信息
if (!Buffer.isBuffer(source) && typeof source !== "string") {
const currentLoader = this.getCurrentLoader(loaderContext, 0);
const err = new Error(
`Final loader (${
currentLoader
? compilation.runtimeTemplate.requestShortener.shorten(
currentLoader.loader
)
: "unknown"
}) didn't return a Buffer or String`
);
const error = new ModuleBuildError(this, err);
return callback(error);
}
// 创建一个source对象,继承至OriginSource
// this._source:
// _name:'/Users/---/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js??ref--4!/Users/---/lagou-edu/webpack-entry/src/index.js'
// _value:'import vue from 'vue';\nconsole.log(vue);'
// __proto__:Source
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
this._sourceSize = null;
// 如果额外信息有AST信息的话取出来
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
return callback();
}
);
}
module.noParse防止 webpack 解析那些任何与给定正则表达式相匹配的文件。忽略的文件中不应该含有 import, require, define 的调用,或任何其他导入机制。忽略大型的 library 可以提高构建性能。
// webpack.config.js
module.exports = {
//...
module: {
noParse: /jquery|lodash/,
},
};
module.exports = {
//...
module: {
noParse: (content) => /jquery|lodash/.test(content),
},
};
下面就是递归执行 Loader 的 pitch、normal 函数对资源进行加载:
// node_modules/loader-runner/lib/LoaderRunner.js
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true;
var isDone = false;
var isError = false; // internal error
var reportedError = false;
// 提供异步解析,上下文上绑定async方法,提供一个内部回调
context.async = function async() {
if (isDone) {
if (reportedError) return; // ignore
throw new Error("async(): The callback was already called.");
}
isSync = false;
return innerCallback;
};
// 内部回调修改上下文回调,内部执行最终回调将解析结果返回
var innerCallback = (context.callback = function () {
if (isDone) {
if (reportedError) return; // ignore
throw new Error("callback(): The callback was already called.");
}
isDone = true;
isSync = false;
try {
callback.apply(null, arguments);
} catch (e) {
isError = true;
throw e;
}
});
try {
// 执行pitch\normal函数
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
})();
if (isSync) {
isDone = true;
if (result === undefined) return callback();
if (
result &&
typeof result === "object" &&
typeof result.then === "function"
) {
return result.then(function (r) {
callback(null, r);
}, callback);
}
return callback(null, result);
}
} catch (e) {
if (isError) throw e;
if (isDone) {
// loader is already "done", so we cannot use the callback function
// for better debugging we print the error on the console
if (typeof e === "object" && e.stack) console.error(e.stack);
else console.error(e);
return;
}
isDone = true;
reportedError = true;
callback(e);
}
}
// 默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。
// 每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。
function convertArgs(args, raw) {
// 如果raw设置为false的话,就转换为utf-8字符串
if(!raw && Buffer.isBuffer(args[0]))
args[0] = utf8BufferToString(args[0]);
else if(raw && typeof args[0] === "string")
args[0] = new Buffer(args[0], "utf-8"); // eslint-disable-line
}
// pitching-loader 相关请查看 https://webpack.docschina.org/api/loaders/#pitching-loader
// 比如”use: ["style-loader", "css-loader"],“,css-loader 最终会导出一段包含css代码的js字符串,
// 里面可能还包含一些动态的导入,将其他的css样式代码文件导入,这些在style-loader中都是无法直接处理的,
// style-loader本身只会创建一个style标签,将最终的css代码嵌入进html中,而在嵌入之前的处理就需要交由pitch函数处理
/**
* 递归执行Loader的pitch函数
*/
function iteratePitchingLoaders(options, loaderContext, callback) {
// loaderIndex 记录loader pitch函数的执行索引,当执行到最后一个之后就执行Loader
if (loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
// 取出当前的Loader对象
// normal:null
// ident:'ref--4'
// data:null
// normalExecuted:false
// options:{presets: Array(1)}
// path:'/Users/---/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js'
// pitch:null
// pitchExecuted:false
// query:'??ref--4'
// raw:null
// request (get):ƒ () {\n\t\t\treturn obj.path + obj.query;\n\t\t}
// request (set):ƒ (value) {...}
// __proto__:Object
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 如果当前Loader的pitch函数已经执行过了,递归下一个Loader
if (currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 加载Loader模块
loadLoader(currentLoaderObject, function (err) {
if (err) {
loaderContext.cacheable(false);
return callback(err);
}
// 取pitch函数
var fn = currentLoaderObject.pitch;
// 标注当前Loader的pitch执行
currentLoaderObject.pitchExecuted = true;
// 如果不存在pitch函数就直接递归下一个Loader
if (!fn) return iteratePitchingLoaders(options, loaderContext, callback);
// 异步或者同步执行pitch函数,数组中的选项会作为pitch函数的执行实参传入
// remainingRequest前置请求
// previousRequest后置请求
// currentLoaderObject.data 当前loader处理数据
runSyncOrAsync(
fn,
loaderContext,
[
loaderContext.remainingRequest,
loaderContext.previousRequest,
(currentLoaderObject.data = {}),
],
function (err) {
if (err) return callback(err);
// 如果当前pitch函数返回结果不为undefined,那么就终止后续Loader的pitch和normal的执行
// 直接执行上一个Loader的normal
// 这里需要区别的是:pitch的执行是从上(左)往下(右),normal的执行是从下(右)往上(左)(我们一般配置是按normal执行顺序配置的)
var args = Array.prototype.slice.call(arguments, 1);
if (args.length > 0) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
// 返回结果为undefined的话就继续进行递归,执行下一个Loader的pitch
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
}
/**
* 加载请求资源,获取buffer,然后交由Loader处理
* @param options {
* resourceBuffer: null,
* readResource: readResource,
* }
* @param loaderContext loaders控制对象
*/
function processResource(options, loaderContext, callback) {
// 设置最后一个Loader,因为Loader normal从右往左执行
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
var resourcePath = loaderContext.resourcePath;
if (resourcePath) {
// runLoader中fileDependencies中加入依赖文件
loaderContext.addDependency(resourcePath);
// 读取文件buffer(var readResource = options.readResource || readFile;)
options.readResource(resourcePath, function (err, buffer) {
if (err) return callback(err);
// 设置buffer并交由normal处理
options.resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
} else {
iterateNormalLoaders(options, loaderContext, [null], callback);
}
}
/**
* normal loader处理文件buffer
* @param options {
* resourceBuffer: null,
* readResource: readResource,
* }
* @param loaderContext loaders控制对象
* @param args 文件buffer
*/
function iterateNormalLoaders(options, loaderContext, args, callback) {
// loaderIndex < 0 表示所有Loader的normal执行完毕,执行回调,返回处理后的args
// iteratePitchingLoaders callback -> doBuild runLoaders callback -> build callback handleParseResult(result);
if (loaderContext.loaderIndex < 0) return callback(null, args);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 如果当前Loader normal执行过就递归执行下一个 normal 函数
if (currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
// 取出normal函数
var fn = currentLoaderObject.normal;
// 执行标识位
currentLoaderObject.normalExecuted = true;
// 不存在normal直接进入下一个Loader normal
if (!fn) {
return iterateNormalLoaders(options, loaderContext, args, callback);
}
// 转换为String还是Buffer
convertArgs(args, currentLoaderObject.raw);
// 执行normal,以我们的babel-loader加载index.js为例:
// runSyncOrAsync 中 ”fn.apply(context, args)“,执行fn时其实执行的是babel-loader的makeLoader函数,this指向context
// function makeLoader(callback) {
// const overrides = callback ? callback(babel) : undefined;
// return function (source, inputSourceMap) {
// // Make the loader async
// const callback = this.async();
// loader.call(this, source, inputSourceMap, overrides).then(args => callback(null, ...args), err => callback(err));
// };
// }
// 此时contest上是挂载有async函数的,就是执行自定义的innerCallback回调,将解析结果args返回
runSyncOrAsync(fn, loaderContext, args, function (err) {
if (err) return callback(err);
// 将执行结果传递给下一个Loader的normal递归执行
var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
exports.runLoaders = function runLoaders(options, callback) {
// 读取配置项
var resource = options.resource || ""; // 需要处理的资源
var loaders = options.loaders || []; // loaders配置项
var loaderContext = options.context || {}; // 所有loader工作时共享的一份数据
var readResource = options.readResource || readFile; // 读取文件方法
// 获取路径和路径参数
var splittedResource = resource && splitQuery(resource);
var resourcePath = splittedResource ? splittedResource[0] : undefined;
var resourceQuery = splittedResource ? splittedResource[1] : undefined;
var contextDirectory = resourcePath ? dirname(resourcePath) : null;
// 执行状态
var requestCacheable = true; // 缓存标识位
var fileDependencies = []; // 文件依赖的缓存
var contextDependencies = []; // 目录依赖的缓存
// 准备Loader对象
loaders = loaders.map(createLoaderObject);
loaders 如下
// [
// {
// data:null
// ident:'ref--4'
// normal:null
// normalExecuted:false
// options:{presets: Array(1)}
// path:'/Users/---/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js'
// pitch:null
// pitchExecuted:false
// query:'??ref--4'
// raw:null
// request (get):ƒ () {\n\t\t\treturn obj.path + obj.query;\n\t\t}
// request (set):ƒ (value) {...}
// __proto__:Object
// }
// ]
...
// loaderContext上扩展一些属性和方法
// 其中 loaderIndex 是一个指针,它控制了所有 loaders 的 pitch 与 normal 函数的执行
...
// Object.preventExtensions()(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions)方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
// 到这里Loader的上下文环境已经全部整合完毕,闭合LoaderContext,解析过程中它不能够再被扩展修改
if (Object.preventExtensions) {
Object.preventExtensions(loaderContext);
}
var processOptions = {
resourceBuffer: null,
readResource: readResource,
};
// 迭代 PitchingLoaders
iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
// 如果出错会执行回调,通过doBuild最终到build函数的回调中终止解析抛出错误信息
if (err) {
return callback(err, {
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies,
});
}
// 不出错的情况下将结果通过回调传给doBuild
callback(null, {
result: result,
resourceBuffer: processOptions.resourceBuffer,
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies,
});
});
};
加载 Loader 信息:
// node_modules/loader-runner/lib/loadLoader.js
// LoaderRunner.js 中的 iteratePitchingLoaders 方法会调用 loadLoader 来加载 load module
// loader是一个Loader对象
// 这里主要是获取Loader的三个属性,normalLoader,pitchLoader和raw
module.exports = function loadLoader(loader, callback) {
if (typeof System === "object" && typeof System.import === "function") {
// System.js加载模块
System.import(loader.path)
.catch(callback)
.then(function (module) {
...
callback();
});
} else {
try {
var module = require(loader.path);
} catch (e) {
...
return callback(e);
}
// module必须是一个function或者标准的es6模块
if (typeof module !== "function" && typeof module !== "object") {
return callback(
new LoaderLoadingError(
"Module '" +
loader.path +
"' is not a loader (export function or es6 module)"
)
);
}
// babel-loader部分源码:
/**
* module.exports = makeLoader();
* module.exports.custom = makeLoader;
*
* function makeLoader(callback) {
* const overrides = callback ? callback(babel) : undefined;
* return function (source, inputSourceMap) {
* // Make the loader async
* const callback = this.async();
* loader.call(this, source, inputSourceMap, overrides).then(args => callback(null, ...args), err => callback(err));
* };
* }
*
* function loader(_x, _x2, _x3) {
* return _loader.apply(this, arguments);
* }
*
* function _loader() {
* ...
* }
*/
// 最终的normal就对应于_loader函数
loader.normal = typeof module === "function" ? module : module.default;
loader.pitch = module.pitch;
loader.raw = module.raw;
// normalLoader和pitch只要存在就必须是函数
if (
typeof loader.normal !== "function" &&
typeof loader.pitch !== "function"
) {
return callback(
new LoaderLoadingError(
"Module '" +
loader.path +
"' is not a loader (must have normal or pitch function)"
)
);
}
// 这里的loader是引用类型数据,会直接修改值,无需回传loader,直接执行iteratePitchingLoaders中loadLoader的回调
callback();
}
};
可以看到上面的整个流程的核心就是 Loader 的 pitch 和 normal 方法的执行,这个处理完毕会生成一个文件被 Loader 解析的结果:
cacheable:true
▶ fileDependencies:(1) ['/Users/---/lagou-edu/webpack-entry/src/index.js']
▶ resourceBuffer:Buffer(39) [...]
▶ result:(2) ['import vue from 'vue';\nconsole.log(vue);', null]
▶ contextDependencies:(0) []
▶ __proto__:Object
这就是 LoaderRunner.js 中 runLoaders 的结果,最终通过执行回调,将结果传回 NormalModule.js 中 doBuild,在 runLoaders 的回调中通过 this.createSource(...) 创建一个 OriginSource 实例:
_name:'/Users/xueyong/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js??ref--4!/Users/xueyong/lagou-edu/ webpack-entry/src/index.js'
_value:'import vue from 'vue';\nconsole.log(vue);'
▶ __proto__:Source
然后再次执行回调,此时进入 build 中进行最终的 parse:
// 目前这里的source就是文件内容_value:'import vue from 'vue';\nconsole.log(vue);'
parse(source, initialState) {
let ast;
let comments;
if (typeof source === "object" && source !== null) {
ast = source;
comments = source.comments;
} else {
// 注释
comments = [];
// 生成AST树,结构如下:
// body:(2) [Node, Node]
// 0:Node {type: 'ImportDeclaration', start: 0, end: 22, loc: SourceLocation, range: Array(2), …}
// 1:Node {type: 'ExpressionStatement', start: 23, end: 40, loc: SourceLocation, range: Array(2), …}
// length:2
// start:0
// end:40
// range:(2) [0, 40]
// loc:SourceLocation {start: Position, end: Position}
// sourceType:'module'
// type:'Program'
ast = Parser.parse(source, {
sourceType: this.sourceType,
onComment: comments,
});
}
// 缓存parse的初始信息
const oldScope = this.scope;
const oldState = this.state;
const oldComments = this.comments;
// 每次parse都会记录当前parse的一些信息,保存在scope中
this.scope = {
// 是否是顶级作用域,解析模块中的函数、class类时,它们就属于非顶级作用域
topLevelScope: true,
// 当前解析是否在try语句中
inTry: false,
// 对象表达式中是否是缩减形式,即 {x: x, y: y, fun: () => {}} 等价于 { x, y, () => {} }
inShorthand: false,
// 是否是严格模式
isStrict: false,
// 是否是asm.js
isAsmJs: false,
// 模块内定义的变量
definitions: new StackedSetMap(),
// 模块内可以重命名的变量
renames: new StackedSetMap(),
};
// 保存此次parse的module、compilation、webpack options信息
const state = (this.state = initialState || {});
this.comments = comments;
// 执行program钩子上注册的两个函数:
// 0:{type: 'sync', fn: ƒ, name: 'HarmonyDetectionParserPlugin'}
// 1:{type: 'sync', fn: ƒ, name: 'UseStrictPlugin'}
if (this.hooks.program.call(ast, comments) === undefined) {
// program hook没有返回值时顺序执行下面四个函数
// 判断是isStrict,还是isAsmjs
this.detectMode(ast.body);
// 迭代普通变量声明(主要解析块级作用域之类的,比如条件语句、循环语句、export、函数、try/catch等)
this.prewalkStatements(ast.body);
// 迭代块变量的声明(变量、export、class)
this.blockPrewalkStatements(ast.body);
// 迭代迭代对象、表达式(主要解决迭代语句、表达式等的解析)
this.walkStatements(ast.body);
}
// 重置parse信息
this.scope = oldScope;
this.state = oldState;
this.comments = oldComments;
return state;
}
parse 主要就是对文件内容解析生成 AST 树,然后就是执行 program hook 上的两个函数 HarmonyDetectionParserPlugin、UseStrictPlugin,进行依赖收集,和严格模式标注。
在 node_modules/webpack/lib/dependencies 目录下包含很多 webpack 的依赖模板,包含 AMD、CommonJS、ESModule 三种不同的依赖导入的解析,这里因为我们是使用的 ESModule 导入的 Vue,所以这里会采用 HarmonyDetectionParserPlugin 插件来解析依赖,下面的 loc 指的是 location,代码的位置:
// node_modules/webpack/lib/dependencies/HarmonyDetectionParserPlugin.js
module.exports = class HarmonyDetectionParserPlugin {
// program 上注册的HarmonyDetectionParserPlugin函数
apply(parser) {
parser.hooks.program.tap("HarmonyDetectionParserPlugin", (ast) => {
/**
* import vue from 'vue'
*
* console.log(vue)
*/
// ast body里面就是两个node:
/**
* 1. 'ImportDeclaration' 一个import声明
* 2. 'ExpressionStatement' 一个表达式
*/
// 创建module的时候type设置成功`javascript/auto`,没有使用严格模式
const isStrictHarmony = parser.state.module.type === "javascript/esm";
// 判断是否是ESModule
const isHarmony =
isStrictHarmony ||
ast.body.some(
(statement) =>
statement.type === "ImportDeclaration" ||
statement.type === "ExportDefaultDeclaration" ||
statement.type === "ExportNamedDeclaration" ||
statement.type === "ExportAllDeclaration"
);
// 如果是ESModule,创建一个兼容的ESModule依赖模板
if (isHarmony) {
const module = parser.state.module;
const compatDep = new HarmonyCompatibilityDependency(module);
compatDep.loc = {
start: {
line: -1,
column: 0,
},
end: {
line: -1,
column: 0,
},
index: -3,
};
// 添加到module的依赖中
module.addDependency(compatDep);
// 创建一个初始依赖模板
const initDep = new HarmonyInitDependency(module);
initDep.loc = {
start: {
line: -1,
column: 0,
},
end: {
line: -1,
column: 0,
},
index: -2,
};
// 添加到module的依赖中
module.addDependency(initDep);
parser.state.harmonyParserScope = parser.state.harmonyParserScope || {};
parser.scope.isStrict = true;
// 打包信息中修改导出方式和模块信息为CommonJS
module.buildMeta.exportsType = "namespace";
module.buildInfo.strict = true;
module.buildInfo.exportsArgument = "__webpack_exports__";
if (isStrictHarmony) {
module.buildMeta.strictHarmonyModule = true;
module.buildInfo.moduleArgument = "__webpack_module__";
}
}
});
}
};
然后就是标注是否是严格模式:
// node_modules/webpack/lib/UseStrictPlugin.js
class UseStrictPlugin {
/**
* @param {Compiler} compiler Webpack Compiler
* @returns {void}
*/
apply(compiler) {
compiler.hooks.compilation.tap(
"UseStrictPlugin",
(compilation, { normalModuleFactory }) => {
const handler = (parser) => {
parser.hooks.program.tap("UseStrictPlugin", (ast) => {
const firstNode = ast.body[0];
if (
firstNode &&
firstNode.type === "ExpressionStatement" &&
firstNode.expression.type === "Literal" &&
firstNode.expression.value === "use strict"
) {
// 删除“use strict”表达式, 稍后渲染器将再次添加它。
// 为了在 webpack 预先添加代码时不破坏严格模式,这是必要的。
// @see https://github.com/webpack/webpack/issues/1970
const dep = new ConstDependency("", firstNode.range);
dep.loc = firstNode.loc;
parser.state.current.addDependency(dep);
// 在打包信息中标注它是严格模式
parser.state.module.buildInfo.strict = true;
}
});
};
}
);
}
}
依赖模板生成完毕,如果没有返回值就会进入条件内部执行四个函数,第一个是判断 isStrict\isAsmJs 的,没有特别的意义,暂且不看,第二个函数 prewalkStatements 迭代 AST body 内容,然后执行 prewalkStatement 函数进行类别分支控制,进入不同类型语句的解析:
// node_modules/webpack/lib/Parser.js
function prewalkStatement(statement) {
switch (statement.type) {
case "BlockStatement":
this.prewalkBlockStatement(statement);
break;
case "DoWhileStatement":
this.prewalkDoWhileStatement(statement);
break;
...
case "IfStatement":
this.prewalkIfStatement(statement);
break;
case "ImportDeclaration":
this.prewalkImportDeclaration(statement);
break;
case "LabeledStatement":
this.prewalkLabeledStatement(statement);
break;
...
}
}
这里第一行是 import vue from 'vue',所以会走 ImportDeclaration,执行 prewalkImportDeclaration 执行 import 声明的处理:
// node_modules/webpack/lib/Parser.js
prewalkImportDeclaration(statement) {
// 取出导入变量,这里就是 vue
const source = statement.source.value;
// 执行import钩子上注册的函数
this.hooks.import.call(statement, source);
// 其它dep添加完毕后,判断是默认导入、导入某个特定的导出、导入命名空间
for (const specifier of statement.specifiers) {
const name = specifier.local.name;
// `import vue from 'vue'` 这里的导入变量vue是可以重命名的,添加入renames中
this.scope.renames.set(name, null);
this.scope.definitions.add(name);
switch (specifier.type) {
// 这里是默认导入,走这个分支
case "ImportDefaultSpecifier":
this.hooks.importSpecifier.call(statement, source, "default", name);
break;
case "ImportSpecifier":
this.hooks.importSpecifier.call(
statement,
source,
specifier.imported.name,
name
);
break;
case "ImportNamespaceSpecifier":
this.hooks.importSpecifier.call(statement, source, null, name);
break;
}
}
}
prewalkImportDeclaration 第二行在取出 import 变量后执行了挂载在 import 钩子上的函数,主要就是添加两个额外的依赖,用来清除 import 语句和处理特殊的模块引用方式:
// node_modules/webpack/lib/dependencies/HarmonyImportDependencyParserPlugin.js
parser.hooks.import.tap(
"HarmonyImportDependencyParserPlugin",
(statement, source) => {
parser.state.lastHarmonyImportOrder =
(parser.state.lastHarmonyImportOrder || 0) + 1;
// clearDep 清除依赖导入,import 最终会被替换为真实的依赖模块,所有原有的import语句要被删除
const clearDep = new ConstDependency("", statement.range);
clearDep.loc = statement.loc;
parser.state.module.addDependency(clearDep);
// sideEffectDep 用来处理 ”import b from 'b'; a.use(b);“
// 最终将a.use(b) 替换为b的代码片段。
const sideEffectDep = new HarmonyImportSideEffectDependency(
source,
parser.state.module,
parser.state.lastHarmonyImportOrder,
parser.state.harmonyParserScope
);
sideEffectDep.loc = statement.loc;
parser.state.module.addDependency(sideEffectDep);
return true;
}
);
上面执行完毕就会进入 switch/case 分支进行导入判断,区分默认导入、特定导入和命名空间导入,我们这里是默认导入,所以执行 importSpecifier 钩子时传入的 id 就是 default:
// node_modules/webpack/lib/dependencies/HarmonyImportDependencyParserPlugin.js
parser.hooks.importSpecifier.tap(
"HarmonyImportDependencyParserPlugin",
(statement, source, id, name) => {
// 将定义的import变量删除
parser.scope.definitions.delete(name);
// 重新命名为 imported var
parser.scope.renames.set(name, "imported var");
// 设置一个依赖标识符
if (!parser.state.harmonySpecifier) {
parser.state.harmonySpecifier = new Map();
}
// parser.state 最终结果:
// options:{entry: './src/index.js', output: {…}, mode: 'development', devtool: 'none', plugins: Array(1), …}
// module:NormalModule {dependencies: Array(4), blocks: Array(0), …}
// compilation:Compilation {_pluginCompat: SyncBailHook, hooks: {…}, …}
// current:NormalModule {dependencies: Array(4), blocks: Array(0), …}
// harmonyParserScope:{}
// harmonySpecifier:Map(1) {vue => {source: 'vue', …}}
// lastHarmonyImportOrder:1
// __proto__:Object
parser.state.harmonySpecifier.set(name, {
source,
id,
sourceOrder: parser.state.lastHarmonyImportOrder,
});
return true;
}
);
以上 import 语句处理完毕,迭代处理第二个 node,这时第二个 node 是一个表达式 console.log(vue),不符合 prewalkStatement 任何一个分支,所以退出来执行第三个函数 blockPrewalkStatements,同样进行 AST body 迭代然后执行 blockPrewalkStatement 函数进行分支处理:
// node_modules/webpack/lib/Parser.js
blockPrewalkStatement(statement) {
switch (statement.type) {
// 变量
case "VariableDeclaration":
this.blockPrewalkVariableDeclaration(statement);
break;
// 默认导出
case "ExportDefaultDeclaration":
this.blockPrewalkExportDefaultDeclaration(statement);
break;
// 特定导出
case "ExportNamedDeclaration":
this.blockPrewalkExportNamedDeclaration(statement);
break;
// class类
case "ClassDeclaration":
this.blockPrewalkClassDeclaration(statement);
break;
}
}
我们这里的两个 node 皆不满足条件,所以不会执行任何一个分支,直接结束迭代,然后执行第四个函数 walkStatements,迭代 AST body 执行 walkStatement 函数进行分支处理:
// node_modules/webpack/lib/Parser.js
function walkStatement(statement) {
// 如果自定义处理了,且返回结果不为undefined,就return掉
if (this.hooks.statement.call(statement) !== undefined) return;
switch (statement.type) {
case "BlockStatement":
this.walkBlockStatement(statement);
break;
...
case "ExpressionStatement":
this.walkExpressionStatement(statement);
break;
...
case "FunctionDeclaration":
this.walkFunctionDeclaration(statement);
break;
case "IfStatement":
this.walkIfStatement(statement);
break;
...
case "WhileStatement":
this.walkWhileStatement(statement);
break;
case "WithStatement":
this.walkWithStatement(statement);
break;
}
}
第一条语句不满足任何分支条件,第二条语句 console.log() 是一个表达式,会执行 walkExpressionStatement 函数,该函数内部直接调用了 this.walkExpression 进行表达式判断:
// node_modules/webpack/lib/Parser.js
function walkExpression(expression) {
switch (expression.type) {
case "ArrayExpression":
this.walkArrayExpression(expression);
break;
case "ArrowFunctionExpression":
this.walkArrowFunctionExpression(expression);
break;
...
case "AwaitExpression":
this.walkAwaitExpression(expression);
break;
case "BinaryExpression":
this.walkBinaryExpression(expression);
break;
case "CallExpression":
this.walkCallExpression(expression);
break;
case "Identifier":
this.walkIdentifier(expression);
break;
...
case "YieldExpression":
this.walkYieldExpression(expression);
break;
}
}
console 也相当于函数调用,会进入 CallExpression 分支执行 walkCallExpression 函数:
// node_modules/webpack/lib/Parser.js
function walkCallExpression(expression) {
if (
expression.callee.type === "MemberExpression" &&
expression.callee.object.type === "FunctionExpression" &&
!expression.callee.computed &&
(expression.callee.property.name === "call" ||
expression.callee.property.name === "bind") &&
expression.arguments.length > 0
) {
// (function(…) { }.call/bind(?, …))
this._walkIIFE(
expression.callee.object,
expression.arguments.slice(1),
expression.arguments[0]
);
} else if (expression.callee.type === "FunctionExpression") {
// (function(…) { }(…))
this._walkIIFE(expression.callee, expression.arguments, null);
} else if (expression.callee.type === "Import") {
let result = this.hooks.importCall.call(expression);
if (result === true) return;
if (expression.arguments) this.walkExpressions(expression.arguments);
} else {
// console.log()都不符合前两种调用方式,也不是import,所以执行下面的代码
// 鉴定表达式
// BasicEvaluatedExpression {type: 9, identifier:'console.log', range: Array(2), falsy: false, truthy: false ... }
const callee = this.evaluateExpression(expression.callee);
// 判断表达式有没有标识符
if (callee.isIdentifier()) {
// 看看表达式有没有注册hook,有就执行
const callHook = this.hooks.call.get(callee.identifier);
if (callHook !== undefined) {
let result = callHook.call(expression);
if (result === true) return;
}
// 截取 . 符号之前的表达式
let identifier = callee.identifier.replace(/\.[^.]+$/, "");
// 如果截取的表达式和源表达式不同,找出截取的表达式注册的钩子,有就执行它
if (identifier !== callee.identifier) {
const callAnyHook = this.hooks.callAnyMember.get(identifier);
if (callAnyHook !== undefined) {
let result = callAnyHook.call(expression);
if (result === true) return;
}
}
}
// 最终将表达式和参数进行处理
// 这里如果存在多个点调用的话会递归依次处理
if (expression.callee) this.walkExpression(expression.callee);
// 每次调用都需要对参数进行处理
if (expression.arguments) this.walkExpressions(expression.arguments);
}
}
递归 点调用(a.b.c()) 的逻辑重复,我们暂且不提,只看一下对于参数的处理,这里会和前面的 import 变量联通,walkExpressions 对传入的参数进行迭代,然后交由函数 walkExpression 处理,参数的 type 对应 Identifier,会走 walkIdentifier 这个函数处理参数标识符:
// node_modules/webpack/lib/Parser.js
function walkIdentifier(expression) {
// 我们以上在处理import声明的时候已经将导入变量修改了:
// parser.scope.definitions.delete(name);
// parser.scope.renames.set(name, "imported var");
// 这里判断主要是保证import语句已经被处理过了,以便进行后面的参数替换
if (!this.scope.definitions.has(expression.name)) {
// 此时的name其实已经被改变为 `imported va`
// 所以是取出 `imported va` hook
const hook = this.hooks.expression.get(
this.scope.renames.get(expression.name) || expression.name
);
// 然后就是执行 `imported va` hook
if (hook !== undefined) {
const result = hook.call(expression);
if (result === true) return;
}
}
}
// node_modules/webpack/lib/dependencies/HarmonyImportDependencyParserPlugin.js
parser.hooks.expression
.for("imported var")
.tap("HarmonyImportDependencyParserPlugin", (expr) => {
// 取出参数名称,这里就是vue
const name = expr.name;
// 取出标识符设置:{source: 'vue', id: 'default', sourceOrder: 1}
const settings = parser.state.harmonySpecifier.get(name);
// 创建一个依赖模板
const dep = new HarmonyImportSpecifierDependency(
settings.source,
parser.state.module,
settings.sourceOrder,
parser.state.harmonyParserScope,
settings.id,
name,
expr.range,
this.strictExportPresence
);
// 设置填充范围并添加到module的依赖中
dep.shorthand = parser.scope.inShorthand;
dep.directImport = true;
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
return true;
});
到这里内部的四个函数全部执行完毕了,后续就对本次的parse信息进行重置,然后返回结果至最上面的parse:
// result:
// lastHarmonyImportOrder:1
// harmonySpecifier:Map(1) {vue => {source: 'vue', …}}
// harmonyParserScope:{}
// current:NormalModule {dependencies: Array(5), blocks: Array(0), variables: …}
// compilation:Compilation {_pluginCompat: SyncBailHook, hooks: {…}, name: undefined, …}
// module:NormalModule {dependencies: Array(5), blocks: Array(0), variables: Array(0), type: 'javascript/auto', …}
// options:{entry: './src/index.js', output: {…}, mode: 'development', devtool: 'none', plugins: Array(1), …}
// __proto__:Object
到此整个webpack的核心打包流程就分析完毕了,总结在上篇文章已经说明,可以去看看。