// 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
// 通过一个工厂模式
module.exports = makeLoader();
module.exports.custom = makeLoader;
function makeLoader(callback) {const overrides = callback ? callback(babel) : undefined;return function (source, inputSourceMap) {// Make the loader asyncconst 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监听依赖文件,依赖文件变化后重新执行loaderconfig.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-loaderexternalDependencies == null ? void 0 : externalDependencies.forEach(dep => this.addDependency(dep));// 开发者传入的loader的this上下文中的钩子name,会在转译完成后触发metadataSubscribers.forEach(subscriber => {subscribe(subscriber, metadata, this);});// 返回转译后的代码和sourceMapreturn [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
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
// 读文件操作
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;
};
//打包后
index.js
const parse = require('./parse');module.exports = function(src, map, meta) {//const options = loaderUtils.getOptions(this);const callback = this.async();try {const product = `module.exports = ${parse(src, ref => `require("${ref}")`)}`;callback(null, product, map, meta);} catch (e) {callback(e);}
};
parse
'use strict';const startMark = '@ref('
const endMark = ')'module.exports = parse;/*** * @param {string} src */
function parse(src, pred) {let ret = '\"\"', pos = 0;let leg=src.length;// 替换所有@refwhile (pos < src.length) {// 找到 '@ref('的起始位置const k = src.indexOf(startMark, pos);// 如果文件中已经没有'@ref('这样的字符串,直接退出if (k === -1) {break;}// '@ref('之前的内容ret += ' + ' + JSON.stringify(src.slice(pos, k));// 找到 '@ref('的结束位置const u = k + startMark.length;// 找到')'的起始位置const v = src.indexOf(endMark, u)if (v === -1) {throw new Error(`@ref should close with ')'`)}// 找到@ref引用的模块内容const ref = src.slice(u, v);if (ret.length === 0) {throw new Error('@ref path should not be empty')}// 将模块内的@ref替换成'require("xx")',注意这里的require是字符串,所以不会拿到require内容,后续还会交给webpack替换成__webpack__require,使得能够引入任意模块ret += ' + ' + pred(ref);pos = v + 1;}if (pos < src.length) {ret += ' + ' + JSON.stringify(src.slice(pos, src.length));}return ret;
}
基本使用
import o3 from './o3.ff'
console.log(o3); // 42
// o3.ff
module.exports = (options, loaderContext) => {return { code: "module.exports = 42;" };
};
{test:/o3.ff$/,use: [{loader: 'val-loader',},]
}
loader
async function loader(content) {const options = this.getOptions(_options.default);const {executableFile} = options;const callback = this.async();let exports;// ...如果指定了executableFile逻辑// 默认逻辑try {/** exports 导出结果为:* (options, loaderContext) => {* return { code: "module.exports = 42;" };* }*/exports = execute(content, this);} catch (error) {callback(new Error(`Unable to execute "${this.resource}": ${error}`));return;}// 如果是import()导入,那么需要.default获取默认导出const func = exports && exports.default ? exports.default : exports;if (typeof func !== "function") {callback(new Error(`Module "${this.resource}" does not export a function as default`));return;}let result;try {result = func(options, this, content);} catch (error) {callback(new Error(`Module "${this.resource}" throw error: ${error}`));return;}// 针对返回结果是Promise的情况if (result && typeof result.then === "function") {result.then(res => processResult(this, res)).catch(error => {callback(new Error(`Module "${this.resource}" throw error: ${error}`));});return;}processResult(this, result);
}
execute
处理任意模块原理:
function execute(code, loaderContext) {// 通过 node 的 module构造器,将外部内容转换成 node 模块const module = new _module.default(loaderContext.resource, parentModule);// eslint-disable-next-line no-underscore-dangle// 指定模块向上的查找路径,loaderContext.context返回模块所在的文件夹module.paths = _module.default._nodeModulePaths(loaderContext.context);// Use the path without webpack-specific parts (`resourceQuery` or `resourceFragment`)module.filename = loaderContext.resourcePath;// eslint-disable-next-line no-underscore-dangle// 手动编译给定内容,按照node格式module._compile(code, loaderContext.resource);// 返回代码中module.exports导出的结果return module.exports;
}
processResult
if (!result || typeof result !== "object" || "code" in result === false) {loaderContext.callback(new Error(`The returned result of module "${loaderContext.resource}" is not an object with a "code" property`));return;}if (typeof result.code !== "string" && result.code instanceof Buffer === false) {loaderContext.callback(new Error(`The returned code of module "${loaderContext.resource}" is neither a string nor an instance of Buffer`));return;}// 文件依赖(result.dependencies || []).forEach(dep => loaderContext.addDependency(dep));// 文件夹依赖(result.contextDependencies || []).forEach(dep => loaderContext.addContextDependency(dep));// 文件依赖(result.buildDependencies || []).forEach(dep => loaderContext.addBuildDependency(dep));// 指定是否缓存loader结果loaderContext.cacheable(Boolean(result.cacheable));loaderContext.callback(null, result.code, result.sourceMap || null, result.ast || null);