webpack plugin源码解析(二) copy-webpack-plugin
文章目录
- 作用
- 涉及 webpack API
- 实现
-
- 初始化选项
- apply
- 异步执行拷贝任务
- runPattern 匹配拷贝文件信息
- 限制 runPattern 并发量 throttleAll
- transformAll
- 输出拷贝文件 compilation.emitAsset
作用
- 拷贝文件到另一个文件
new CopyPlugin({patterns: [{ from: "src/css", to: "css" ,transform(content, absoluteFrom) {console.log('trans',content,absoluteFrom)return content;},// transformAll(assets) {// console.log('transAll',assets)// return assets;// }},],}),
涉及 webpack API
compiler.context
- 获取项目上下文路径(项目根目录)
webpack 接收编译错误 compilation.errors
compilation.errors.push(new Error(`Invalid "pattern.to" for the "pattern.from": "${normalizedPattern.from}" and "pattern.transformAll" function. The "to" option must be specified.`));
compilation.getLogger(“copy-webpack-plugin”);
- Webpack Logger 可以用在 loader 和 plugin。生成的 Logger 将作为 stats 的一部分进行输出
const logger = compilation.getLogger("copy-webpack-plugin");
logger.log("starting to add additional assets...");
compilation.getCache(“CopyWebpackPlugin”);
- 用于获取指定插件的缓存对象,后续可以使用该对象来读取和写入缓存数据,下次构建时直接从缓存中读取,避免重复计算
const cache = compilation.getCache("CopyWebpackPlugin");
-
获取|设置缓存
// 获取
const source = await cache.getPromise(key, etag);// 设置
await cache.storePromise(key, etag, data);
-
根据文件内容计算hash
const etag = cache.getLazyHashedEtag(rawSource)
-
合并hash
const mergedEtag = cache.mergeEtags(cache.getLazyHashedEtag(rawSource),cache.getLazyHashedEtag(rawSource))
-
获取|新建缓存对象
const cacheItem = cache.getItemCache(key,etag)
// 获取
const content = await cacheItem.getPromise();
// 设置
await cacheItem.storePromise(transformedAsset);
文件快照snapshot
-
用于创建文件系统的快照。快照是文件系统状态的一份副本,包含了文件的元数据以及每个文件的最后修改时间戳
- 在开始构建时:webpack 会在开始构建时创建一个快照,用于记录构建开始时文件系统的状态。
- 在构建结束时:webpack 会在构建结束时创建一个快照,用于记录构建结束时文件系统的状态。
- 在监听模式下:如果 webpack 在监听模式下运行,那么每次文件系统发生变化时,webpack 都会创建一个快照,用于记录文件系统的最新状态。
-
创建快照
/ @param {number} 开始时间戳* @param {Iterable<string>} 文件列表,绝对路径,可选* @param {Iterable<string>} 文件夹列表,同上* @param {Iterable<string>} 配置忽略文件* @param {Object} 其他选项配置* @param {boolean=} 是否使用 hash 对于快照* @param {boolean=} 是否使用时间戳对于快照* @param {function((WebpackError | null)=, (Snapshot | null)=): void} callback callback function* node 风格回调*/
new Promise((resolve, reject) => {compilation.fileSystemInfo.createSnapshot(startTime, [dependency], // "/xxx/Desktop/webpack/wb/src/css/1.css"undefined,undefined, null, (error, snapshot) => {if (error) {reject(error);return;}resolve(snapshot);});});
-
检查快照是否有效
static async checkSnapshotValid(compilation, snapshot) {// eslint-disable-next-line consistent-returnreturn new Promise((resolve, reject) => {compilation.fileSystemInfo.checkSnapshotValid(snapshot, (error, isValid) => {if (error) {reject(error);return;}resolve(isValid);});});
}
webpack 文件读入系统 inputFileSystem
- webpack 中用于读取各种类型的文件,包括 JavaScript、CSS、图片等等,默认值是 fs 模块
const { inputFileSystem } = compiler;
- 读取文件 stats 源信息
function stat(inputFileSystem, path) {return new Promise((resolve, reject) => {inputFileSystem.stat(path, // "/xxx/Desktop/webpack/wb/src/css" 绝对路径(err, stats) => {if (err) {reject(err);return;}resolve(stats);});});
}
- 读取文件内容
function readFile(inputFileSystem, path) {return new Promise((resolve, reject) => {inputFileSystem.readFile(path, // 绝对路径(err, data) => {if (err) {reject(err);return;}resolve(data);});});
}
生成 webpack 格式文件 RawSource
const { RawSource } = compiler.webpack.sources; // 或者直接webpack.sources.RawSourceconst source = new RawSource(data); // buffer | string
source.buffer(); // 返回 buffer 内容
实现
初始化选项
class CopyPlugin {constructor(options = {patterns: []}) {validate(schema, options, {name: "Copy Plugin",baseDataPath: "options"});this.patterns = options.patterns;this.options = options.options || {};}}
}
apply
apply(compiler) {const pluginName = this.constructor.name;// 在初始化编译时执行compiler.hooks.thisCompilation.tap(pluginName, compilation => {const logger = compilation.getLogger("copy-webpack-plugin");const cache = compilation.getCache("CopyWebpackPlugin"); // 用于获取指定插件的缓存对象,后续可以使用该对象来读取和写入缓存数据,下次构建时直接从缓存中读取,避免重复计算let globby;compilation.hooks.processAssets.tapAsync({name: "copy-webpack-plugin",stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL // 该阶段 webpack 已经完成了所有的文件处理操作}, async (unusedAssets, callback) => {if (typeof globby === "undefined") {try {// 动态导入 globby 库({globby} = await import("globby"));} catch (error) {callback(error);return;}}logger.log("starting to add additional assets...");const copiedResultMap = new Map(); // 拷贝内容const scheduledTasks = [];// 存放拷贝任务this.patterns.map((item, index) => scheduledTasks.push(async () => {// ... 拷贝任务})// 限制任务并发量,默认最多一次处理100个拷贝任务await throttleAll(this.options.concurrency || 100, scheduledTasks);const copiedResult = [...copiedResultMap.entries()].sort((a, b) => a[0] - b[0]); // 拷贝结果进行优先级排序// 拷贝文件输出//...})
}
异步执行拷贝任务
this.patterns.map((item, index) => scheduledTasks.push(async () => {// 格式化配置const normalizedPattern = typeof item === "string" ? {from: item} : { ...item};// 设置上下文路径(项目根目录):// "/xxx/Desktop/webpack/wb"const context = typeof normalizedPattern.context === "undefined" ? compiler.context : path.isAbsolute(normalizedPattern.context) ? normalizedPattern.context : path.join(compiler.context, normalizedPattern.context);let copiedResult;try {// 返回匹配的文件信息以及文件内容 ( rawSource ) 列表copiedResult = await CopyPlugin.runPattern(globby, compiler, compilation, logger, cache,normalizedPattern, index);} catch (error) {compilation.errors.push(error);return;}if (!copiedResult) {return;}// 过滤无效值let filteredCopiedResult = copiedResult.filter(result => Boolean(result));// 如果配置了 transformAll 修改要拷贝的所有资源if (typeof normalizedPattern.transformAll !== "undefined") {}){// ...filteredCopiedResult = [transformedAsset];}// 开发者传入了任务优先级配置相关const priority = normalizedPattern.priority || 0;if (!copiedResultMap.has(priority)) { copiedResultMap.set(priority, []);}copiedResultMap.get(priority).push(...filteredCopiedResult);
})
runPattern 匹配拷贝文件信息
CopyPlugin.runPattern
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {// 格式化配置和路径const {RawSource} = compiler.webpack.sources; // 或者直接webpack.sources.RawSourceconst pattern = { ...inputPattern };const originalFrom = pattern.from;const normalizedOriginalFrom = path.normalize(originalFrom);logger.log(`starting to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context`);let absoluteFrom;if (path.isAbsolute(normalizedOriginalFrom)) {absoluteFrom = normalizedOriginalFrom;} else {absoluteFrom = path.resolve(pattern.context, normalizedOriginalFrom); // "/xxx/Desktop/webpack/wb/src/css"}logger.debug(`getting stats for '${absoluteFrom}'...`);// 根据决定路径 from 读取文件 stats 源信息,判断文件类型const {inputFileSystem // webpack 中用于读取各种类型的文件,包括 JavaScript、CSS、图片等等,默认值是 fs 模块} = compiler;let stats;// 返回文件的一些属性:时间戳、大小、uid、判断文件类型方法等stats = await stat(inputFileSystem, absoluteFrom); // from 属性的绝对路径let fromType;if (stats) {if (stats.isDirectory()) {fromType = "dir"; // 本例为 dirlogger.debug(`determined '${absoluteFrom}' is a directory`);} else if (stats.isFile()) {fromType = "file";logger.debug(`determined '${absoluteFrom}' is a file`);} else {// FallbackfromType = "glob";logger.debug(`determined '${absoluteFrom}' is unknown`);}} else {fromType = "glob";logger.debug(`determined '${absoluteFrom}' is a glob`);}// ...
}
stat
function stat(inputFileSystem, path) {return new Promise((resolve, reject) => {inputFileSystem.stat(path,(err, stats) => {if (err) {reject(err);return;}resolve(stats);});});
}
根据 from 得出的文件类型添加对应的依赖,更改时重新出发编译
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {// ...let fromType; // 本例为 dir// ...const globOptions = { ...{followSymbolicLinks: true},...(pattern.globOptions || {}),...{cwd: pattern.context, // "/xxx/Desktop/webpack/wb"objectMode: true}}; globOptions.fs = inputFileSystem; // 更改 globby 库的读取方式let glob;switch (fromType) {case "dir":compilation.contextDependencies.add(absoluteFrom); // 文件夹依赖,文件夹变更时重新触发编译logger.debug(`added '${absoluteFrom}' as a context dependency`);pattern.context = absoluteFrom; // "/xxx/Desktop/webpack/wb/src/css"// escapePath 转译 *?|(){}[] 等字符转,避免影响 glob 匹配规则// glob: "/xxx/Desktop/webpack/wb/src/css//*"glob = path.posix.join(fastGlob.escapePath(normalizePath(path.resolve(absoluteFrom))), "/*");// absoluteFrom: "/xxx/Desktop/webpack/wb/src/css//*"absoluteFrom = path.join(absoluteFrom, "/*");if (typeof globOptions.dot === "undefined") {globOptions.dot = true;}break;case "file":compilation.fileDependencies.add(absoluteFrom);logger.debug(`added '${absoluteFrom}' as a file dependency`);pattern.context = path.dirname(absoluteFrom);glob = fastGlob.escapePath(normalizePath(path.resolve(absoluteFrom)));if (typeof globOptions.dot === "undefined") {globOptions.dot = true;}break;case "glob":default:{const contextDependencies = path.normalize(globParent(absoluteFrom));compilation.contextDependencies.add(contextDependencies);logger.debug(`added '${contextDependencies}' as a context dependency`);glob = path.isAbsolute(originalFrom) ? originalFrom : path.posix.join(fastGlob.escapePath(normalizePath(path.resolve(pattern.context))), originalFrom);}}logger.log(`begin globbing '${glob}'...`);
}
根据 globby 库匹配出要拷贝的文件列表
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {// ...let globEntries;try {// glob:/xxx/Desktop/webpack/wb/src/css//*globEntries = await globby(glob, globOptions); // 返回匹配到的文件列表( 文件名 + 文件绝对路径 )} catch (error) {compilation.errors.push(/ @type {WebpackError} */error);return;}// 没有匹配结果的处理if (globEntries.length === 0) {if (pattern.noErrorOnMissing) { // 如果配置了无结果不报错logger.log(`finished to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context to '${pattern.to}'`);return;}const missingError = new Error(`unable to locate '${glob}' glob`);compilation.errors.push(missingError);return;}
}
根据匹配的文件列表,读取文件真实内容
- 格式化各种文件路径
- 优先从缓存里读取文件内容,没有则通过 inputFileSystem 读取
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {// ...let globEntries;/[{dirent: { // 还有其他判断文件类型的方法name: "1.css",...},name: "1.css",path: "/xxx/webpack/wb/src/css/1.css",},...]*/// ...let copiedResult;try {copiedResult = await Promise.all(globEntries.map(async globEntry => {// 如果是非文件类型退出比如(文件夹)if (!globEntry.dirent.isFile()) {return;}// 如果配置了 filter 不拷贝某些文件if (pattern.filter) {let isFiltered;try {isFiltered = await pattern.filter(globEntry.path);} catch (error) {compilation.errors.push(error);return;}if (!isFiltered) {logger.log(`skip '${globEntry.path}', because it was filtered`);return;}}})const from = globEntry.path; // "/xxx/Desktop/webpack/wb/src/css/1.css"logger.debug(`found '${from}'`); // `globby`/`fast-glob` return the relative path when the path contains special characters on windowsconst absoluteFilename = path.resolve(pattern.context, from); // "/xxx/webpack/wb/src/css/1.css"// 拿到要去到的路径// to: "css"const to = typeof pattern.to === "function" ? await pattern.to({context: pattern.context,absoluteFilename}) : path.normalize(typeof pattern.to !== "undefined" ? pattern.to : "");// 根据 to 结尾是否是'\\'或者"" 判断是文件还是文件夹// toType: "dir",path.sep: "/"const toType = pattern.toType ? pattern.toType : template.test(to) ? "template" : path.extname(to) === "" || to.slice(-1) === path.sep ? "dir" : "file";logger.log(`'to' option '${to}' determinated as '${toType}'`);const relativeFrom = path.relative(pattern.context, absoluteFilename); // 1.css// filename 转化成相对路径let filename = toType === "dir" ? path.join(to, relativeFrom) : to; // css/1.cssif (path.isAbsolute(filename)) {filename = path.relative(compiler.options.output.path, filename);}logger.log(`determined that '${from}' should write to '${filename}'`);const sourceFilename = normalizePath(path.relative(compiler.context, absoluteFilename)); // "src/css/1.css"if (fromType === "dir" || fromType === "glob") {compilation.fileDependencies.add(absoluteFilename); // 将源文件加入文件依赖 fileDependencies 中, "/xxx/Desktop/webpack/wb/src/css/1.css"logger.debug(`added '${absoluteFilename}' as a file dependency`);}// ...}
从缓存中读取内容
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {// ...copiedResult = await Promise.all(globEntries.map(async globEntry => {// ... 各种路径转换let cacheEntry;logger.debug(`getting cache for '${absoluteFilename}'...`);try {cacheEntry = await cache.getPromise(`${sourceFilename}|${index}`, null); // 用于获取指定键名对应的缓存数据,第一个参数为 key ,第二个参数为etag} catch (error) {compilation.errors.push(error);return;}let source;if (cacheEntry) {logger.debug(`found cache for '${absoluteFilename}'...`);let isValidSnapshot;logger.debug(`checking snapshot on valid for '${absoluteFilename}'...`);try {isValidSnapshot = await CopyPlugin.checkSnapshotValid(compilation, cacheEntry.snapshot); // cacheEntry 里会存放文件快照 snapshot} catch (error) {compilation.errors.push(error);return;}// 如果快照有效,则直接获取缓存中的内容if (isValidSnapshot) {logger.debug(`snapshot for '${absoluteFilename}' is valid`);({source} = cacheEntry);} else {logger.debug(`snapshot for '${absoluteFilename}' is invalid`);}} else {logger.debug(`missed cache for '${absoluteFilename}'`);}})
}
checkSnapshotValid
- 检查文件快照是否有效
static async checkSnapshotValid(compilation, snapshot) {return new Promise((resolve, reject) => {compilation.fileSystemInfo.checkSnapshotValid(snapshot, (error, isValid) => {if (error) {reject(error);return;}resolve(isValid);});});
}
无缓存通过 inputFileSystem 读取
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {// ...copiedResult = await Promise.all(globEntries.map(async globEntry => {// ...if (!source) {const startTime = Date.now();logger.debug(`reading '${absoluteFilename}'...`);let data;try {// data 是 buffer 类型data = await readFile(inputFileSystem, absoluteFilename); // inputFileSystem.readFile 读取文件内容} catch (error) {compilation.errors.push(error);return;}logger.debug(`read '${absoluteFilename}'`);source = new RawSource(data); // 生成 webpack 类型文件let snapshot;logger.debug(`creating snapshot for '${absoluteFilename}'...`);try {snapshot = await CopyPlugin.createSnapshot(compilation, startTime, absoluteFilename); // 文件的快照信息} catch (error) {compilation.errors.push(error);return;}if (snapshot) {logger.debug(`created snapshot for '${absoluteFilename}'`);logger.debug(`storing cache for '${absoluteFilename}'...`);try {// 将文件相关信息存入缓存await cache.storePromise(`${sourceFilename}|${index}`, null, {source,snapshot});} catch (error) {compilation.errors.push(error);return;}logger.debug(`stored cache for '${absoluteFilename}'`);}}})
}
readFile
function readFile(inputFileSystem, path) {return new Promise((resolve, reject) => {inputFileSystem.readFile(path,(err, data) => {if (err) {reject(err);return;}resolve(data);});});
}
createSnapshot
- 创建文件快照
static async createSnapshot(compilation, startTime, dependency) {// dependency: "/xxx/Desktop/webpack/wb/src/css/1.css"return new Promise((resolve, reject) => {compilation.fileSystemInfo.createSnapshot(startTime, [dependency],undefined,undefined, null, (error, snapshot) => {if (error) {reject(error);return;}resolve(snapshot);});});
}
如果配置了 transform,拷贝过程中修改源文件内容
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {// ...copiedResult = await Promise.all(globEntries.map(async globEntry => {// ...if (pattern.transform) {const transformObj = typeof pattern.transform === "function" ? {transformer: pattern.transform} : pattern.transform;if (transformObj.transformer) {logger.log(`transforming content for '${absoluteFilename}'...`);// 拿到源文件 bufferconst buffer = source.buffer();if (transformObj.cache) { // 如果配置了cache,没怎么看懂// TODO: remove in the next major releaseconst hasher = compiler.webpack && compiler.webpack.util && compiler.webpack.util.createHash ? compiler.webpack.util.createHash("xxhash64") : // eslint-disable-next-line global-requirerequire("crypto").createHash("md4");const defaultCacheKeys = {version,sourceFilename,transform: transformObj.transformer,contentHash: hasher.update(buffer).digest("hex"),index};const cacheKeys = `transform|${serialize(typeof transformObj.cache === "boolean" ? defaultCacheKeys : typeof transformObj.cache.keys === "function" ? await transformObj.cache.keys(defaultCacheKeys, absoluteFilename) : { ...defaultCacheKeys,...transformObj.cache.keys})}`;logger.debug(`getting transformation cache for '${absoluteFilename}'...`);const cacheItem = cache.getItemCache(cacheKeys, cache.getLazyHashedEtag(source));source = await cacheItem.getPromise();logger.debug(source ? `found transformation cache for '${absoluteFilename}'` : `no transformation cache for '${absoluteFilename}'`);if (!source) {const transformed = await transformObj.transformer(buffer, absoluteFilename);source = new RawSource(transformed);logger.debug(`caching transformation for '${absoluteFilename}'...`);await cacheItem.storePromise(source);logger.debug(`cached transformation for '${absoluteFilename}'`);}} else {// transformer(buffer, absoluteFilename) 将源文件 buffer 和 文件绝对路径传递给 transformer,然后将返回内容重新生成 RawSourcesource = new RawSource(await transformObj.transformer(buffer, absoluteFilename));}}}})
}
最后转换输出文件信息
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {// ...copiedResult = await Promise.all(globEntries.map(async globEntry => {// ...let info = typeof pattern.info === "undefined" ? {} : typeof pattern.info === "function" ? pattern.info({absoluteFilename,sourceFilename,filename,toType}) || {} : pattern.info || {};if (toType === "template") { // toType 为 'dir',这里未进入logger.log(`interpolating template '${filename}' for '${sourceFilename}'...`);const contentHash = CopyPlugin.getContentHash(compiler, compilation, source.buffer());const ext = path.extname(sourceFilename);const base = path.basename(sourceFilename);const name = base.slice(0, base.length - ext.length);const data = {filename: normalizePath(path.relative(pattern.context, absoluteFilename)),contentHash,chunk: {name,id:/ @type {string} */sourceFilename,hash: contentHash}};const {path: interpolatedFilename,info: assetInfo} = compilation.getPathWithInfo(normalizePath(filename), data);info = { ...info,...assetInfo};filename = interpolatedFilename;logger.log(`interpolated template '${filename}' for '${sourceFilename}'`);} else {filename = normalizePath(filename); // 序列化'\\\\'} return {sourceFilename, // "src/css/1.css"absoluteFilename, // "/xxx/Desktop/webpack/wb/src/css/1.css"filename, // "css/1.css"source, // rawSourceinfo, // {}force: pattern.force // undefined};})
}
限制 runPattern 并发量 throttleAll
apply(compiler) {const pluginName = this.constructor.name;// 在初始化编译时执行compiler.hooks.thisCompilation.tap(pluginName, compilation => {const logger = compilation.getLogger("copy-webpack-plugin");const cache = compilation.getCache("CopyWebpackPlugin"); // 用于获取指定插件的缓存对象,后续可以使用该对象来读取和写入缓存数据,下次构建时直接从缓存中读取,避免重复计算let globby;compilation.hooks.processAssets.tapAsync({name: "copy-webpack-plugin",stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL // 该阶段 webpack 已经完成了所有的文件处理操作}, async (unusedAssets, callback) => {// ... const copiedResultMap = new Map(); // 拷贝内容const scheduledTasks = [];// 存放拷贝任务this.patterns.map((item, index) => scheduledTasks.push(async () => {// ... 拷贝任务})// 限制任务并发量,默认最多一次处理100个拷贝任务await throttleAll(this.options.concurrency || 100, scheduledTasks);const copiedResult = [...copiedResultMap.entries()].sort((a, b) => a[0] - b[0]); // 拷贝结果进行优先级排序// 拷贝文件输出//...})
}
function throttleAll(limit, tasks) {if (!Number.isInteger(limit) || limit < 1) {throw new TypeError(`Expected \\`limit\\` to be a finite number > 0, got \\`${limit}\\` (${typeof limit})`);}if (!Array.isArray(tasks) || !tasks.every(task => typeof task === `function`)) {throw new TypeError(`Expected \\`tasks\\` to be a list of functions returning a promise`);}return new Promise((resolve, reject) => {const result = Array(tasks.length).fill(notSettled);const entries = tasks.entries();const next = () => {const {done,value} = entries.next();if (done) {const isLast = !result.includes(notSettled); // result 已经全部解析if (isLast) {resolve(result);}return;}const [index, task] = value;const onFulfilled = x => { // 异步任务完成后,执行下一个任务result[index] = x;next();};task().then(onFulfilled, reject);};Array(limit).fill(0).forEach(next);// 执行 100 次 next});
}
transformAll
- 如果开发者配置了transformAll,在资源全部解析匹配后,输出前统一修改资源文件
apply(compiler) {const pluginName = this.constructor.name;// 在初始化编译时执行compiler.hooks.thisCompilation.tap(pluginName, compilation => {const logger = compilation.getLogger("copy-webpack-plugin");const cache = compilation.getCache("CopyWebpackPlugin"); // 用于获取指定插件的缓存对象,后续可以使用该对象来读取和写入缓存数据,下次构建时直接从缓存中读取,避免重复计算let globby;compilation.hooks.processAssets.tapAsync({name: "copy-webpack-plugin",stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL // 该阶段 webpack 已经完成了所有的文件处理操作}, async (unusedAssets, callback) => {// ... const copiedResultMap = new Map(); // 拷贝内容const scheduledTasks = [];// 存放拷贝任务this.patterns.map((item, index) => scheduledTasks.push(async () => {// 返回匹配的文件信息以及文件内容 ( rawSource ) 列表copiedResult = await CopyPlugin.runPattern(globby, compiler, compilation, logger, cache,normalizedPattern, index);let filteredCopiedResult = copiedResult.filter(result => Boolean(result));if (typeof normalizedPattern.transformAll !== "undefined") {filteredCopiedResult.sort((a, b) => a.absoluteFilename > b.absoluteFilename ? 1 : a.absoluteFilename < b.absoluteFilename ? -1 : 0);// cache.getLazyHashedEtag 计算指定源代码的哈希值,已经存在则返回// 合并所有文件的hash值const mergedEtag = filteredCopiedResult.length === 1 ? cache.getLazyHashedEtag(filteredCopiedResult[0].source) : filteredCopiedResult.reduce((accumulator, asset, i) => {// cache.mergeEtags 将多个哈希值合并为一个哈希值。通过合并多个哈希值为一个哈希值accumulator = cache.mergeEtags(i === 1 ? cache.getLazyHashedEtag(accumulator.source) : accumulator, cache.getLazyHashedEtag(asset.source));return accumulator;});// 为所有文件 创建|返回 对应的 cache ,传入 key 和 etag,存储合并后的 hash 值const cacheItem = cache.getItemCache(`transformAll|${serialize({version,from: normalizedPattern.from,to: normalizedPattern.to,transformAll: normalizedPattern.transformAll})}`, mergedEtag);let transformedAsset = await cacheItem.getPromise();// 如果缓存中没有值if (!transformedAsset) {transformedAsset = {filename: normalizedPattern.to};try {// 将已经获取到的所有文件信息传递给 transformAlltransformedAsset.data = await normalizedPattern.transformAll(filteredCopiedResult.map(asset => {return {data: asset.source.buffer(),sourceFilename: asset.sourceFilename,absoluteFilename: asset.absoluteFilename};}));} catch (error) {compilation.errors.push(error);return;}const filename = typeof normalizedPattern.to === "function" ? await normalizedPattern.to({context}) : normalizedPattern.to;const {RawSource} = compiler.webpack.sources;transformedAsset.source = new RawSource(transformedAsset.data);transformedAsset.force = normalizedPattern.force;// 缓存存储转换结果await cacheItem.storePromise(transformedAsset);}filteredCopiedResult = [transformedAsset];}const priority = normalizedPattern.priority || 0;if (!copiedResultMap.has(priority)) { // 开发者传入了优先级配置相关copiedResultMap.set(priority, []);}copiedResultMap.get(priority).push(...filteredCopiedResult);})// ...})
}
输出拷贝文件 compilation.emitAsset
// .../* [{sourceFilename: "src/css/1.css",absoluteFilename: "/xxx/Desktop/webpack/wb/src/css/1.css",filename: "css/1.css",source: RawSourceinfo: {},force: undefined,},...]*/
copiedResult.reduce((acc, val) => acc.concat(val[1]), []).filter(Boolean).forEach( result => {const {absoluteFilename,sourceFilename,filename,source,force} = result;const existingAsset = compilation.getAsset(filename);if (existingAsset) { // 判断是否已存在文件,已存在则根据开发者传入配置来更新或跳过if (force) { // 已存在更新const info = {copied: true,sourceFilename};logger.log(`force updating '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists...`);compilation.updateAsset(filename, source, { ...info,...result.info});logger.log(`force updated '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists`);return;}// 否则跳过logger.log(`skip adding '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists`);return;}const info = {copied: true,sourceFilename};logger.log(`writing '${filename}' from '${absoluteFilename}' to compilation assets...`);// 通过 emitAsset 输出拷贝文件compilation.emitAsset(filename, source, { ...info,...result.info});logger.log(`written '${filename}' from '${absoluteFilename}' to compilation assets`);// ...
logger.log("finished to adding additional assets");
callback();
});// ...