一、loader 在webpack 中的作用
loader 用于对模块的源代码进行转换。
loader 可以使你在 import
或 "load(加载)" 模块时预处理文件。
因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。
loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。
loader 甚至允许你直接在 JavaScript 模块中 import
CSS文件!
二、loader 运行的总体流程
webpack-loader.png1、默认配置
webpack 入口文件 webpack.js ,根据配置文件 设置配置的options
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
WebpackOptionsDefaulter 加载 默认配置
// WebpackOptionsDefaulter.js
this.set("module.defaultRules", "make", options => [
{
type: "javascript/auto",
resolve: {}
},
{
test: /\.mjs$/i,
type: "javascript/esm",
resolve: {
mainFields:
options.target === "web" ||
options.target === "webworker" ||
options.target === "electron-renderer"
? ["browser", "main"]
: ["main"]
}
},
{
test: /\.json$/i,
type: "json"
},
{
test: /\.wasm$/i,
type: "webassembly/experimental"
}
]);
//...
this.set("optimization.splitChunks.cacheGroups.default", {
automaticNamePrefix: "",
reuseExistingChunk: true,
minChunks: 2,
priority: -20
});
this.set("optimization.splitChunks.cacheGroups.vendors", {
automaticNamePrefix: "vendors",
test: /[\\/]node_modules[\\/]/,
priority: -10
});
// ...
image2020-12-28_11-8-42.png
上图可以看出一些默认配置已经加载,WebpackOptionsApply 模块主要是根据options选项的配置,设置compile的相应的插件,属性,里面写了大量的 apply(compiler);
使得模块的this指向compiler
2、创建NormalModuleFactory
处理完一些webpac.config 和一些内部配置,在一个module 构建的过程中,首先根据module 的依赖类型,调用对应的构造函数来创建对应的模块
// Compiler.js
createNormalModuleFactory() {
const normalModuleFactory = new NormalModuleFactory(
this.options.context,
this.resolverFactory,
this.options.module || {}
);
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
return params;
}
image2020-12-28_11-35-30.png当NormalModuleFactory 实例化完成之后,在compilation 内部调用 这个实例create 创建normalModule
3、解析loader 路径
这里需要用到上一个阶段讲到的 NormalModuleFactory 实例, NormalModuleFactory 的 create 方法是创建 NormalModule 实例的入口, 内部需要解析一写module ,其中就包含 loaders ,资源路径 resource 等等,最终将解析完毕的参数传给 NormalModule 构建函数直接实例化
在NormalModuleFactory中,创建出NormalModule实例之前会涉及到四个钩子:
- beforeResolve
- factory: 负责来基于resolve钩子返回值来创建NormalModule
- resolver : 负责解析loader 模块路径 (例如css-loader这个loader的模块路径是什么)
- afterResolve
resolve钩子上注册的方法较长,其中还包括了模块资源本身的路径解析。resolver有两种,分别是loaderResolver和normalResolver
// NormalModuleFactory.js
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
const contextInfo = data.contextInfo;
const context = data.context;
const request = data.request;
const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);
....
}
loader 路径解析也分为两种:inline loader和config文件中的loader。resolver钩子中会先处理inline loader
resolver钩子中会先处理inline loader
比如: import Styles from 'style-loader!css-loader?modules!./styles.css';
通过 上面的 request 解析出来 所需要的loader路径
// NormalModuleFactory.js
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
const contextInfo = data.contextInfo;
const context = data.context;
const request = data.request;
// 假如 import Styles from 'style-loader!css-loader?modules!./styles.css'
// 这时候 request 就是
const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);
let matchResource = undefined;
let requestWithoutMatchResource = request;
...
// 是否忽略perLoader
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
// 是否忽略normalLoader
const noAutoLoaders =
noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
// 忽略所有的 perLoader、normalLoader、postLoader
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
// 首先解析出所需要的 loader,这种loader 为内联loader
let elements = requestWithoutMatchResource
.replace(/^-?!+/, "")
.replace(/!!+/g, "!")
.split("!");
let resource = elements.pop(); // 获取资源路径
// 获取每个loader对应的options 配置
elements = elements.map(identToLoaderRequest);
....
}
从上面可知 request :style-loader!css-loader?modules!./styles.css
let elements = requestWithoutMatchResource
.replace(/^-?!+/, "")
.replace(/!!+/g, "!")
.split("!");
image.png从style-loader!css-loader?modules!./styles.css中可以取出
两个loader:style-loader和css-loader
webpack首先解析了inline loader的绝对路径与配置。接下来则是解析config文件中的loader (source code),
// NormalModuleFactory.js
const result = this.ruleSet.exec({
resource: resourcePath,
realResource:
matchResource !== undefined
? resource.replace(/\?.*/, "")
: resourcePath,
resourceQuery,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler
});
image.png webpack-loader.pngruleSet: 它可以根据模块路径名,匹配出模块所需的loader
返回的result就是当前模块匹配出的config中的loader。
const settings = {};
const useLoadersPost = []; // post loader
const useLoaders = []; // normal loader
const useLoadersPre = []; // pre loader
for (const r of result) {
if (r.type === "use") {
if (r.enforce === "post" && !noPrePostAutoLoaders) {
useLoadersPost.push(r.value);
} else if (
r.enforce === "pre"
&&!noPreAutoLoaders
&&!noPrePostAutoLoaders) {
useLoadersPre.push(r.value); // perLoader
} else if (!r.enforce &&!noAutoLoaders &&!noPrePostAutoLoaders) {
useLoaders.push(r.value); // normal loader
}
} else if (
typeof r.value === "object" &&r.value !== null &&settings[r.type] !== null
) {
settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
} else {
settings[r.type] = r.value;
}
}
最后,使用neo-aysnc来并行解析三类loader数组
asyncLib.parallel(
[
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoadersPost,
loaderResolver,
),
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoaders,
loaderResolver,
),
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoadersPre,
loaderResolver,
),
]
不同类型 loader 上的 pitch 方法执行的顺序为:
postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch
最终 loader 所执行的顺序对应为:
preLoader -> normalLoader -> inlineLoader -> postLoader
loader是从右至左执行的,真实的loader执行顺序是倒过来的,因此inlineLoader是整体后于config中normal loader执行的
经过上面一下步骤,loader 解析工作基本完成。
补充: 前面提到 RuleSet - 会将resourcePath应用于所有的module.rules规则,从而筛选出所需的loader
- RuleSet - 含有类静态方法.normalizeRule() 和实例方法.exec()
- 通过其上的静态方法.normalizeRule()将配置值转换为标准化的test对象,会存储一个this.references属性是一个map类型的存储,key是loader在配置中的类型和位置。例如,ref-2表示loader配置数组中的第三个。
- this.ruleSet.exec()中传入源码模块路径,返回的result就是当前模块匹配出的config中的loader
4、loader的运行
loader的绝对路径解析完毕后,在NormalModuleFactory的factory钩子中会创建当前模块的NormalModule对象
在创建完 NormalModule 实例之后会调用 build 方法继续进行内部的构建。我们熟悉的 loaders 将会在这里开始应用
4.1、 loader-runner - loader的执行库
loader-runner分为了两个部分:loadLoader.js与LoaderRunner.js。
- module 开始构建的过程中,会先创建一个 loaderContext 对象。
所有的 loader 会共享这个 loaderContext 对象,每个loader 执行的时候,上下文 是这个对象
doBuild(options, compilation, resolver, fs, callback) {
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext, // loaderContext 上下文
readResource: fs.readFile.bind(fs) // 读取文件的
},
....
)
}
- 初始化 loaderContext ,完成之后,开始调用runLoader 方法,到了loaders 执行。
//
exports.runLoaders = function runLoaders(options, callback) {
// read options
var resource = options.resource || ""; // 模块路径
var loaders = options.loaders || []; // 所需要的loader
var loaderContext = options.context || {}; // 在 normalModule 里创建的loaderContext
var readResource = options.readResource || readFile;
//
var splittedResource = resource && splitQuery(resource);
var resourcePath = splittedResource ? splittedResource[0] : undefined; // 模块实际路径
var resourceQuery = splittedResource ? splittedResource[1] : undefined; // 模块路径 query 参数
var contextDirectory = resourcePath ? dirname(resourcePath) : null; //
// execution state
var requestCacheable = true;
var fileDependencies = [];
var contextDependencies = [];
// prepare loader objects
loaders = loaders.map(createLoaderObject); // 处理loader
loaderContext.context = contextDirectory;
loaderContext.loaderIndex = 0;
loaderContext.loaders = loaders;
loaderContext.resourcePath = resourcePath;
loaderContext.resourceQuery = resourceQuery;
loaderContext.async = null;
loaderContext.callback = null;
loaderContext.cacheable = function cacheable(flag) {
if(flag === false) {
requestCacheable = false;
}
};
loaderContext.dependency = loaderContext.addDependency = function addDependency(file) {
fileDependencies.push(file);
};
loaderContext.addContextDependency = function addContextDependency(context) {
contextDependencies.push(context);
};
loaderContext.getDependencies = function getDependencies() {
return fileDependencies.slice();
};
loaderContext.getContextDependencies = function getContextDependencies() {
return contextDependencies.slice();
};
loaderContext.clearDependencies = function clearDependencies() {
fileDependencies.length = 0;
contextDependencies.length = 0;
requestCacheable = true;
};
// 被构建的模块 路径, loaderContext.resource -> getter/setter
Object.defineProperty(loaderContext, "resource", {
enumerable: true,
get: function() {
if(loaderContext.resourcePath === undefined)
return undefined;
return loaderContext.resourcePath + loaderContext.resourceQuery;
},
set: function(value) {
var splittedResource = value && splitQuery(value);
loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined;
loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined;
}
});
// 所有的loader以及 这个模块的resource 所组成request 字符串
Object.defineProperty(loaderContext, "request", {
enumerable: true,
get: function() {
return loaderContext.loaders.map(function(o) {
return o.request;
}).concat(loaderContext.resource || "").join("!");
}
});
// 执行loader 提供的 pitch 函数阶段传入函数,未被调用的 loader.pitch 组成的request 字符串
Object.defineProperty(loaderContext, "remainingRequest", {
enumerable: true,
get: function() {
if(loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource)
return "";
return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(function(o) {
return o.request;
}).concat(loaderContext.resource || "").join("!");
}
});
Object.defineProperty(loaderContext, "currentRequest", {
enumerable: true,
get: function() {
return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) {
return o.request;
}).concat(loaderContext.resource || "").join("!");
}
});
// 包含已经执行的loader.pitch 所组成的request 字符串
Object.defineProperty(loaderContext, "previousRequest", {
enumerable: true,
get: function() {
return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) {
return o.request;
}).join("!");
}
});
// 获取当前正在执行loader 的query参数
//
Object.defineProperty(loaderContext, "query", {
enumerable: true,
get: function() {
var entry = loaderContext.loaders[loaderContext.loaderIndex];
return entry.options && typeof entry.options === "object" ? entry.options : entry.query;
}
});
// 每个loader 在pitch 阶段和正常执行都可以共享的 data 数据
Object.defineProperty(loaderContext, "data", {
enumerable: true,
get: function() {
return loaderContext.loaders[loaderContext.loaderIndex].data;
}
});
// finish loader context
if(Object.preventExtensions) {
Object.preventExtensions(loaderContext);
}
var processOptions = {
resourceBuffer: null,
readResource: readResource
};
// 开始执行每个loader 上的pitch 函数
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
if(err) {
return callback(err, {
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies
});
}
callback(null, {
result: result,
resourceBuffer: processOptions.resourceBuffer,
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies
});
});
};
- 从上面 runLoader 方法来看,loader-runner中对应的
iteratePitchingLoaders()和iterateNormalLoaders()两个方法 - iteratePitchingLoaders()会递归执行,并记录loader的pitch状态与当前执行到的loaderIndex(loaderIndex++)
从下图可以知道 iteratePitchingLoaders 中 通过pitchExecuted 属性来判断 是否执行过pitch
- 当loaderContext.loaderIndex值达到整体loader数组长度时,表明所有pitch都被执行完毕。所有pitch都被执行完毕(执行到了最后的loader),这时会调用processResource()来处理模块资源
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
image.png
- processResource 会递归执行iterateNormalLoaders()并进行loaderIndex--操作,因此loader会“反向”执行。
function processResource(options, loaderContext, callback) {
// set loader index to last loader
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
var resourcePath = loaderContext.resourcePath;
if(resourcePath) {
loaderContext.addDependency(resourcePath);
options.readResource(resourcePath, function(err, buffer) {
if(err) return callback(err);
options.resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
} else {
iterateNormalLoaders(options, loaderContext, [null], callback);
}
}
function iterateNormalLoaders(options, loaderContext, args, callback) {
...
if(currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
...
}
- loader 中的this其实是一个叫loaderContext的对象,那么this.data的实现其实就是loaderContext.data
// 每个loader 在pitch 阶段和正常执行都可以共享的 data 数据
Object.defineProperty(loaderContext, "data", {
enumerable: true,
get: function() {
return loaderContext.loaders[loaderContext.loaderIndex].data;
}
});
调用this.data时,不同的normal loader由于loaderIndex不同,会得到不同的值;而pitch方法的形参data也是不同的loader下的data
4.2、 pitch 和 normal 执行 都是在 runSyncOrAsync
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true; // 是否为同步
var isDone = false;
var isError = false; // internal error
var reportedError = false;
// 给loaderContext 上下文赋值 async 函数,用来将loader 异步并返回 异步回调
context.async = function async() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("async(): The callback was already called.");
}
isSync = false;
return innerCallback;
};
// callback 这种形式,可以向下一个loader 传递多个参数
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 {
var result = (function LOADER_EXECUTION() { // 开始执行loader
return fn.apply(context, args);
}());
if(isSync) {
isDone = true;
if(result === undefined) // 如果没有返回值,执行callback 开始下一个loader 执行
return callback();
if(result && typeof result === "object" && typeof result.then === "function") {
return result.then(function(r) { // loader 返回一个promise 实例,待实例被resolve 或者reject后执行下一个loader
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);
}
}
pitch:
// runLoader.js
// 开始执行 pitch 函数
runSyncOrAsync(
fn,
loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
if(args.length > 0) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
下图显示 : loaderContext.remainingRequest 表示剩余 loader 拼接成字符 (未被调用的 loader.pitch 组成的request 字符串)
image.png
normal:
runSyncOrAsync(fn, loaderContext, args, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
4.3、webpack 中loader 四种类型
- pre (前置)
{
test: /test\.js$/,
loader: 'loader1',
enforce: 'pre'
}
rules: [{
test: /\.js$/,
use: ['eslint-loader'],
enforce: 'pre',
exclude: /node_modules/,
}]
Pre表示这个loader在所有的loader之前执行。配置表示在所有处理js文件模块loader之前执行eslint-loader,这样我们可以在js代码未被处理的时候就进行eslint代码规范校验
- normal : 是普通loader ,没有配置就是普通的loader
- inline :
import 'loader1!loader2!./test.js';
- post:
{
test: /test\.js$/,
loader: 'loader5',
enforce: 'post'
},
调用顺序:
不同类型 loader 上的 pitch 方法执行的顺序为:
postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch
最终 loader 所执行的顺序对应为:
preLoader -> normalLoader -> inlineLoader -> postLoader
- 比如: a!b!c!module, 正常调用顺序应该是c、b、a,但是真正调用顺序是
a(pitch)、b(pitch)、c(pitch)、c、b、a,比如如果b返回了字符串"result b", 接下来只有a会被系统执行,且a的loader收到的参数是result b
三、 常用loader的执行机制分析
style-loader
style-loader 中pitch 中的作用:
我们知道css-loader最后会导出一段js字符串,里面可能包含需要动态执行的函数。按照正常的执行顺序,style-loader只能拿到这些字符串而并不能把他们转成真正的css代码
// style-loader .js
loaderApi.pitch = function loader(request) {
// // 获取webpack配置的options
const options = _loaderUtils.default.getOptions(this);
(0, _schemaUtils.validate)(_options.default, options, {
name: 'Style Loader',
baseDataPath: 'options'
});
// // 定义了两个变量,不难看出insert的默认值为head,injectType默认值为styleTag
const insert = typeof options.insert === 'undefined' ? '"head"' : typeof options.insert === 'string' ? JSON.stringify(options.insert) : options.insert.toString();
const injectType = options.injectType || 'styleTag';
const esModule = typeof options.esModule !== 'undefined' ? options.esModule : true;
const namedExport = esModule && options.modules && options.modules.namedExport;
const runtimeOptions = {
injectType: options.injectType,
attributes: options.attributes,
insert: options.insert,
base: options.base
};
switch(injectType){
case 'linkTag':
{
// ...
}
case 'lazyStyleTag':
case 'lazySingletonStyleTag':
{
// ...
}
case 'styleTag':
case 'singletonStyleTag':
default:
{
// ...
}
}
}
switch(){
....
case:...
..
default:
{
const isSingleton = injectType === 'singletonStyleTag';
const hmrCode = this.hot ? `
if (module.hot) {
if (!content.locals || module.hot.invalidate) {
var isEqualLocals = ${_isEqualLocals.default.toString()};
var oldLocals = ${namedExport ? 'locals' : 'content.locals'};
module.hot.accept(
${_loaderUtils.default.stringifyRequest(this, `!!${request}`)},
function () {
${esModule ? `if (!isEqualLocals(oldLocals, ${namedExport ? 'locals' : 'content.locals'}, ${namedExport})) {
module.hot.invalidate();
return;
}
oldLocals = ${namedExport ? 'locals' : 'content.locals'};
update(content);` : `content = require(${_loaderUtils.default.stringifyRequest(this, `!!${request}`)});
content = content.__esModule ? content.default : content;
if (typeof content === 'string') {
content = [[module.id, content, '']];
}
if (!isEqualLocals(oldLocals, content.locals)) {
module.hot.invalidate();
return;
}
oldLocals = content.locals;
update(content);`}
}
)
}
module.hot.dispose(function() {
update();
});
}` : '';
return `${esModule ? `import api from ${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)};
import content${namedExport ? ', * as locals' : ''} from ${_loaderUtils.default.stringifyRequest(this, `!!${request}`)};` : `var api = require(${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)});
var content = require(${_loaderUtils.default.stringifyRequest(this, `!!${request}`)});
content = content.__esModule ? content.default : content;`}
var options = ${JSON.stringify(runtimeOptions)};
options.insert = ${insert};
options.singleton = ${isSingleton};
var update = api(content, options);
${hmrCode}
${esModule ? namedExport ? `export * from ${_loaderUtils.default.stringifyRequest(this, `!!${request}`)};` : 'export default content.locals || {};' : 'module.exports = content.locals || {};'}`;
}
}
简化 大概是:pitch 方法 返回一个字符串
const isSingleton = injectType === 'singletonStyleTag';
const hmrCode = this.hot ? `
// ...
` : '';
return `
// _loaderUtils.default.stringifyRequest这里就不叙述了,主要作用是将绝对路径转换为相对路径
var content = require(${_loaderUtils.default.stringifyRequest(this, `!!${request}`)});
if (typeof content === 'string') {
content = [[module.id, content, '']];
}
var options = ${JSON.stringify(options)}
options.insert = ${insert};
options.singleton = ${isSingleton};
var update = require(${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)})(content, options);
if (content.locals) {
module.exports = content.locals;
}
${hmrCode}
`;`
- 首先调用require方法获取css文件的内容,将其赋值给content
- 如果content是字符串,则将content赋值为数组,即:[[module.id], content, ''],接着我们覆盖了options的insert、singleton属性
- 又使用require方法引用了runtime/injectStyleIntoStyleTag.js,它返回一个函数,我们将content和options传递给该函数,并立即执行它:
module.exports = function (list, options) {
options = options || {};
options.attributes = typeof options.attributes === 'object' ? options.attributes : {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of <style>
// tags it will allow on a page
if (!options.singleton && typeof options.singleton !== 'boolean') {
options.singleton = isOldIE();
}
var styles = listToStyles(list, options);
addStylesToDom(styles, options);
return function update(newList) {
// ...
};
};
主要的 是 modulesToDom 和addStyle
function addStyle(obj, options) {
var style;
var update;
var remove;
if (options.singleton) {
var styleIndex = singletonCounter++;
style = singleton || (singleton = insertStyleElement(options));
update = applyToSingletonTag.bind(null, style, styleIndex, false);
remove = applyToSingletonTag.bind(null, style, styleIndex, true);
} else {
style = insertStyleElement(options);
update = applyToTag.bind(null, style, options);
remove = function remove() {
removeStyleElement(style);
};
}
update(obj);
return function updateStyle(newObj) {
if (newObj) {
if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) {
return;
}
update(obj = newObj);
} else {
remove();
}
};
}
function insertStyleElement(options) {
var style = document.createElement('style');
var attributes = options.attributes || {};
if (typeof attributes.nonce === 'undefined') {
var nonce = typeof __webpack_nonce__ !== 'undefined' ? __webpack_nonce__ : null;
if (nonce) {
attributes.nonce = nonce;
}
}
Object.keys(attributes).forEach(function (key) {
style.setAttribute(key, attributes[key]);
});
if (typeof options.insert === 'function') {
options.insert(style);
} else {
var target = getTarget(options.insert || 'head');
if (!target) {
throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
}
target.appendChild(style);
}
return style;
}
style-loader会返回一个字符串,而在浏览器中调用时,会将创建一个style标签,将其加入head中,并将css的内容放入style中,同时每次该文件更新也会相应的更新Style结构,如果该css文件内容被删除,则style的内容也会被相应的删除,总体来说,style-loader做了一件非常简单的事:在 DOM 里插入一个 <style> 标签,并且将 CSS 写入这个标签内
而style-loader的pitch方法里面调用了require('!!.../x.css'),这就会把require的css文件当作新的入口文件,重新链式调用剩余的loader函数进行处理。(值得注意的是'!!'是一个标志,表示不会再重复递归调用style-loader,而只会调用css-loader处理了)
file-loader 和url-loader
file-loader和url-loader都可以辅助webpack将图片打包为js文件
- file-loader会将图片打包成一个单独的js文件
- url-loader会将图片打包成一个base64格式的图片字符串,并将这个图片字符串放到bundle.js里,可以减少http请求,但是如果图片大小非常大那么打包后的bundle.js会非常大加载的时候会非常慢,甚至会有空白,所以可以做一个限制,limit:2048 limit的单位是字节,在小于这个单位的时候就使用者个loader,否则就不使用这loader
- 由于url-loader包含了file-loader所以,file-loader内的option在url-loader中均能使用
- url-loader 会将引入的文件进行编码,生成 DataURL,相当于把文件翻译成了一串字符串,再把这个字符串打包到 JavaScript
file-loader:
function loader(content) {
const options = (0, _loaderUtils.getOptions)(this);
(0, _schemaUtils.validate)(_options.default, options, {
name: 'File Loader',
baseDataPath: 'options'
});
const context = options.context || this.rootContext;
const name = options.name || '[contenthash].[ext]';
const url = (0, _loaderUtils.interpolateName)(this, name, {
context,
content,
regExp: options.regExp
});
let outputPath = url;
....
}
处理outpath:
if (options.outputPath) {
if (typeof options.outputPath === 'function') {
outputPath = options.outputPath(url, this.resourcePath, context);
} else {
outputPath = _path.default.posix.join(options.outputPath, url);
}
}
image.png
处理publicpath:·
let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;
if (options.publicPath) {
if (typeof options.publicPath === 'function') {
publicPath = options.publicPath(url, this.resourcePath, context);
} else {
publicPath = `${options.publicPath.endsWith('/') ? options.publicPath : `${options.publicPath}/`}${url}`;
}
publicPath = JSON.stringify(publicPath);
}
if (options.postTransformPublicPath) {
publicPath = options.postTransformPublicPath(publicPath);
}
emitFile 是由 loader 上下文提供的方法,用于输出一个文件
assetInfo.sourceFilename = (0, _utils.normalizePath)(_path.default.relative(this.rootContext, this.resourcePath));
// 把文件输出到指定的outputPath路径
this.emitFile(outputPath, content, null, assetInfo);
image.png
image.png
最终输出路径:
return
module.exports = ${publicPath};
;
emitFile: (name, content, sourceMap, assetInfo) => {
if (!this.buildInfo.assets) {
this.buildInfo.assets = Object.create(null);
this.buildInfo.assetsInfo = new Map();
}
this.buildInfo.assets[name] = this.createSourceForAsset(
name,
content,
sourceMap
);
this.buildInfo.assetsInfo.set(name, assetInfo);
},
rootContext: options.context, // 项目的根路径
webpack: true,
sourceMap: !!this.useSourceMap,
mode: options.mode || "production",
_module: this,
_compilation: compilation,
_compiler: compilation.compiler,
fs: fs
};
四、修改代码
根据上面 loader 的介绍分析,可以看出很多loader 都用到了loader-utils,
loader-utils是一个webpack工具类,通过一些方法配合loader处理文件
- getOptions(检索被调用loader的配置选项)
- stringifyRequest(将一个请求转化为非绝对路径的可被require或import的字符串;)
- urlToRequest(将url转换成适合webpack环境的模块请求)
- getHashDigest(通过限制字符长度获取文件部分哈希值)
- isUrlRequest(判断是否是路径)
- getCurrentRequest(获取当前请求)
4.1、实现一个loader 替换某一个类名
- webpack.config.js 引入自己的loader
module:{
rule:[
...
{
test: /\.js?$/,
exclude: /node_modules/,
use: [
'babel-loader',
{
// 因为该loader还没有上传的npm,所以要指定该loader的路径,才能使用
loader: 'test-loader',
options: {
text: !isProd?'wechat-test-wrap':'wechat-wrap'
},
}
]
},
]
},
resolveLoader: {
modules: ['node_modules', './src/loader']
},
- test-loader
// src/loader/test-loader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
console.log('dasdasd',source,'test') // 'import less from './index.less';\n\nconsole.log('index.js file load');'
const options = loaderUtils.getOptions(this); // 此时 this 就是 context
console.log('options',options,options.text)
return source.replace(/wrap/g, options.text);
};
4.2 异步loader (简单实现一个less-loader)
- webpack.config.js
{
test: /\.(le|c)ss$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
// modules: true 启用或者禁用css 模块
}
},
{
loader:"test-less-loader",
options:{
sourceMap:false
}
},
]
},
- test-less-loader
const less = require('less');
// webpack 提供的模块
const loaderUtils = require('loader-utils');
// source 为源代码
module.exports = function (source) {
console.log('--------- begin handle less file:');
console.log(source);
// 获取 webpack.config.js 中配置的 options
const options = loaderUtils.getOptions(this);
console.log('--------- options: ');
console.log(options);
// loader 的异步回调
const callback = this.async();
// less 转换函数
less.render(source).then(({css, map, imports}) => {
console.log('less render finished, css: ');
console.log(css);
callback(null, css);
});
}
网友评论