webpack plugin源码解析(二) copy-webpack-plugin

文章目录

  • 作用
  • 涉及 webpack API
    • compiler.context
    • webpack 接收编译错误 compilation.errors
    • compilation.getLogger("copy-webpack-plugin");
    • compilation.getCache("CopyWebpackPlugin");
      • 获取|设置缓存
      • 根据文件内容计算hash
      • 合并hash
      • 获取|新建缓存对象
    • 文件快照snapshot
      • 创建快照
      • 检查快照是否有效
    • webpack 文件读入系统 inputFileSystem
    • 生成 webpack 格式文件 RawSource
  • 实现
    • 初始化选项
    • 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} 文件列表,绝对路径,可选
	 * @param {Iterable} 文件夹列表,同上
	 * @param {Iterable} 配置忽略文件
	 * @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-return
  return 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.RawSource

const 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.RawSource
    
    const 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";  // 本例为 dir
        logger.debug(`determined '${absoluteFrom}' is a directory`);
      } else if (stats.isFile()) {
        fromType = "file";
        logger.debug(`determined '${absoluteFrom}' is a file`);
      } else {
        // Fallback
        fromType = "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 windows
      
      const 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.css
	  if (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}'...`);
            
            // 拿到源文件 buffer
            const buffer = source.buffer();

            if (transformObj.cache) { // 如果配置了cache,没怎么看懂
              // TODO: remove in the next major release
              const hasher = compiler.webpack && compiler.webpack.util && compiler.webpack.util.createHash ? compiler.webpack.util.createHash("xxhash64") : // eslint-disable-next-line global-require
              require("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,然后将返回内容重新生成 RawSource
              source = 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, // rawSource
          info, // {}
          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 {
	                // 将已经获取到的所有文件信息传递给 transformAll
	                transformedAsset.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: RawSource
      info: {
      },
      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();
});


// ...

你可能感兴趣的:(webpack,javascript,前端)