webpack loader源码解析系列(二)-转译|转换相关

文章目录

babel-loader 转译

  • 通过babel的核心包进行转译,babel-loader主要利用了webpack的打包流程对匹配的js文件进行转译
// index.js
// 引入基础库等一系列操作
let babel;
try {
  babel = require("@babel/core");
} catch (err) {
  if (err.code === "MODULE_NOT_FOUND") {
    err.message += "\n babel-loader@9 requires Babel 7.12+ (the package '@babel/core'). " + "If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.";
  }
  throw err;
}

// Since we've got the reverse bridge package at @babel/core@6.x, give
// people useful feedback if they try to use it alongside babel-loader.
if (/^6\./.test(babel.version)) {
  throw new Error("\n babel-loader@9 will not work with the '@babel/core@6' bridge package. " + "If you want to use Babel 6.x, install 'babel-loader@7'.");
}

const {
  version
} = require("../package.json");
const cache = require("./cache");
const transform = require("./transform");
const injectCaller = require("./injectCaller");
const schema = require("./schema");
const {
  isAbsolute
} = require("path");
const validateOptions = require("schema-utils").validate;

// ...

导出loader

  • 通过工厂函数,导出给webpack使用的loader,并且导出的loader是一个异步loader,采用的是Promise风格
// 通过一个工厂模式
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();
    // 这里的调用格式是promise风格
    loader.call(this, source, inputSourceMap, overrides).then(args => callback(null, ...args), err => callback(err));
  };
}

loader

async function loader(source, inputSourceMap, overrides) {
  // loader提供的API,获取当前的文件名
  const filename = this.resourcePath;
  // 获取开发者传递的参数
  let loaderOptions = this.getOptions();
  // 校验开发者传递的option格式
  validateOptions(schema, loaderOptions, {
    name: "Babel loader"
  });
  // ... 一系列合并、注入、校验参数等处理
  
  // 最终处理完成的参数配置
  const config = await babel.loadPartialConfigAsync(injectCaller(programmaticOptions, this.target));
  
  if (config) {
    // ...参数的一些转换
    
    const {
      cacheDirectory = null,
      cacheIdentifier = JSON.stringify({
        options,
        "@babel/core": transform.version,
        "@babel/loader": version
      }),
      cacheCompression = true,
      metadataSubscribers = []
    } = loaderOptions;
    
    let result;
    // 如果开发者配置了cacheDirectory缓存babel处理结果
    if (cacheDirectory) {
      // 写入缓存位置,实际调用通过 handleCache ,读取缓存中结果
      result = await cache({
        source,
        options,
        transform,
        cacheDirectory,
        cacheIdentifier,
        cacheCompression
      });
    } else {
      // 如果不开启缓存,每次进行编译,核心就是通过babel.transform进行转译
      result = await transform(source, options);
    }
    
   // 通过this.addDependency监听依赖文件,依赖文件变化后重新执行loader
    config.files.forEach(configFile => this.addDependency(configFile));
    if (result) {
      if (overrides && overrides.result) {
        result = await overrides.result.call(this, result, {
          source,
          map: inputSourceMap,
          customOptions,
          config,
          options
        });
      }
      const {
        code,
        map,
        metadata,
        externalDependencies
      } = result;
      // 通过this.addDependency添加依赖,当依赖项改变时重新执行babel-loader
      externalDependencies == null ? void 0 : externalDependencies.forEach(dep => this.addDependency(dep));
      
      // 开发者传入的loader的this上下文中的钩子name,会在转译完成后触发
      metadataSubscribers.forEach(subscriber => {
        subscribe(subscriber, metadata, this);
      });
      // 返回转译后的代码和sourceMap
      return [code, map];
    }
  }

  // If the file was ignored, pass through the original content.
  // 如果文件设置了忽略,那么直接直接返回源文件内容
  return [source, inputSourceMap];
}

缓存loader结果

// ...
if (cacheDirectory) {
  // 写入缓存位置,实际调用通过 handleCache ,读取缓存中结果
  result = await cache({
    source,
    options,
    transform,
    cacheDirectory,
    cacheIdentifier,
    cacheCompression
  });
} else {
  // 如果不开启缓存,每次进行编译,核心就是通过babel.transform进行转译
  result = await transform(source, options);
}
// ...

cache

  • 通过find-cache-dir库,查找babel默认的存储路径,否则使用操作系统的默认临时文件路径
const os = require("os");
const path = require("path");
const zlib = require("zlib");
const crypto = require("crypto");
const findCacheDir = require("find-cache-dir");
const {
  promisify
} = require("util");
const {
  readFile,
  writeFile,
  mkdir
} = require("fs/promises");
const transform = require("./transform");

// ...

module.exports = async function (params) {
  let directory;
  if (typeof params.cacheDirectory === "string") {
    directory = params.cacheDirectory;
  } else {
    if (defaultCacheDirectory === null) {
      //默认缓存位置在 node_modules/.cache/babel-loader'
      //os.tmpdir() 指定操作系统临时文件的默认目录的路径
      defaultCacheDirectory = findCacheDir({
        name: "babel-loader"
      }) || os.tmpdir();
    }
    directory = defaultCacheDirectory;
  }
  return await handleCache(directory, params);
};

handleCache

  • 返回cache结果,会将文件进行hash避免冲突
// 读文件操作
const read = async function (filename, compress) {
  const data = await readFile(filename + (compress ? ".gz" : ""));
  const content = compress ? await gunzip(data) : data;
  return JSON.parse(content.toString());
};

// 写文件操作
const write = async function (filename, compress, result) {
  const content = JSON.stringify(result);
  const data = compress ? await gzip(content) : content;
  return await writeFile(filename + (compress ? ".gz" : ""), data);
};

// 对文件进行hash操作
const filename = function (source, identifier, options) {
  const hash = crypto.createHash(hashType);
  const contents = JSON.stringify({
    source,
    options,
    identifier
  });
  hash.update(contents);
  return hash.digest("hex") + ".json";
};

const handleCache = async function (directory, params) {
  const {
    source,
    options = {},
    cacheIdentifier,
    cacheDirectory,
    cacheCompression
  } = params;
  // 获取缓存文件绝对路径
  const file = path.join(directory, filename(source, cacheIdentifier, options));
  try {
    // 读取缓存位置内容 "/node_modules/.cache/babel-loader/c1d0c71205535b96755f65b2a39f16601016d58d8a61022ba4ad0c91bf41714e.json" 如果开启了压缩会添加.gz后缀
    // 如果有返回值,直接返回,不再执行后续逻辑
    return await read(file, cacheCompression);
  } catch (err) {}
  
  const fallback = typeof cacheDirectory !== "string" && directory !== os.tmpdir();

  try {
    // "/node_modules/.cache/babel-loader"
    // 首次创建缓存文件夹
    await mkdir(directory, {
      recursive: true
    });
  } catch (err) {
    if (fallback) {
      return handleCache(os.tmpdir(), params);
    }
    throw err;
  }

  // 通过 babel 进行转译
  const result = await transform(source, options);


  if (!result.externalDependencies.length) {
    try {
      // 将内容写入缓存位置 "/node_modules/.cache/babel-loader/c1d0c71205535b96755f65b2a39f16601016d58d8a61022ba4ad0c91bf41714e.json"
      await write(file, cacheCompression, result);
    } catch (err) {
      if (fallback) {
        // Fallback to tmpdir if node_modules folder not writable
        // 如果出错写入操作系统的临时文件位置
        return handleCache(os.tmpdir(), params);
      }
      throw err;
    }
  }
  return result;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值