oader是导出为一个函数的node模块。该函数在loader转换资源的时候调用。给定的函数将调用loader API,并通过this上下文访问。
匹配(test)单个 loader,你可以简单通过在 rule 对象设置 path.resolve 指向这个本地文件
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}
你可以使用 resolveLoader.modules 配置,webpack 将会从这些目录中搜索这些 loaders。
resolveLoader: {
modules: [path.resolve('node_modules'), path.resolve(__dirname, 'src', 'loaders')]
},
确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好; 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局; 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 - loader-name 是指在第1步中的 package.json 文件中配置的模块名称。
npm link
resolveLoader: {
alias: {
"babel-loader": resolve('./loaders/babel-loader.js'),
"css-loader": resolve('./loaders/css-loader.js'),
"style-loader": resolve('./loaders/style-loader.js'),
"file-loader": resolve('./loaders/file-loader.js'),
"url-loader": resolve('./loaders/url-loader.js')
}
},
ebpack充分地利用缓存来提高编译效率
this.cacheable();
当一个 Loader 无依赖,可异步的时候我想都应该让它不再阻塞地去异步 module.exports = function(source) { var callback = this.async(); // 做异步的事 doSomeAsyncOperation(content, function(err, result) { if(err) return callback(err); callback(null, result); }); };
默认的情况源文件是以 UTF-8 字符串的形式传入给 Loader,设置module.exports.raw = true可使用 buffer 的形式进行处理
module.exports.raw = true;
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取到用户给当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
};
Loader有些场景下还需要返回除了内容之外的东西。
module.exports = function(source) {
// 通过 this.callback 告诉 Webpack 返回的结果
this.callback(null, source, sourceMaps);
// 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
// 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中
return;
};
完整格式
this.callback(
// 当无法转换原内容时,给 Webpack 返回一个 Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 用于把转换后的内容得出原内容的 Source Map,方便调试
sourceMap?: SourceMap,
// 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
// 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
abstractSyntaxTree?: AST
);
Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢。
module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};
在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据。 为此,你需要这样编写 Loader:
module.exports = function(source) {
// 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
source instanceof Buffer === true;
// Loader 返回的类型也可以是 Buffer 类型的
// 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports.raw = true;
在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。
module.exports = function(source) {
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};
loader-utils schema-utils this.async this.cacheable getOptions validateOptions addDependency
babel-core babel-loader babel-plugin-transform-react-jsx this.request=/loaders/babel-loader.js!/src/index.js' this.userRequest /src/index.js this.rawRequest ./src/index.js this.resourcePath /src/index.js
const babel=require('babel-core');
const path=require('path');
module.exports=function (source) {
const options = {
presets: ['env'],
sourceMap: true,
filename:this.request.split('/').pop()
}
let result=babel.transform(source,options);
return this.callback(null,result.code,result.map);
}
const loaderUtils = require('loader-utils');
const validateOptions = require('schema-utils');
const fs = require('fs');
function loader(source) {
//把loader改为异步,任务完成后需要手工执行callback
let cb = this.async();
//启用loader缓存
this.cacheable && this.cacheable();
//用来验证options的合法性
let schema = {
type: 'object',
properties: {
filename: {
type: 'string'
},
text: {
type: 'string'
}
}
}
//通过工具方法获取options
let options = loaderUtils.getOptions(this);
//用来验证options的合法性
validateOptions(schema, options, 'Banner-Loader');
let { text, filename } = options;
if (text) {
cb(null, text + source);
} else if (filename) {
fs.readFile(filename, 'utf8', (err, text) => {
cb(err, text + source);
});
}
}
module.exports = loader;
比如a!b!c!module, 正常调用顺序应该是c、b、a,但是真正调用顺序是 a(pitch)、b(pitch)、c(pitch)、c、b、a,如果其中任何一个pitching loader返回了 值就相当于在它以及它右边的loader已经执行完毕
比如如果b返回了字符串"result b", 接下来只有a会被系统执行,且a的loader收到的参数是result b
loader根据返回值可以分为两种,一种是返回js代码(一个module的代码,含有类似module.export语句)的loader,还有不能作为最左边loader的其他loader
有时候我们想把两个第一种loader chain起来,比如style-loader!css-loader! 问题是css-loader的返回值是一串js代码,如果按正常方式写style-loader的参数就是一串代码字符串
为了解决这种问题,我们需要在style-loader里执行require(css-loader!resouce)
pitch与loader本身方法的执行顺序图
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
//source就是接收到的源文件的内容
let loader = function (source, sourceMaps, extra) {
let cb = this.async();
console.log('loader1');
cb(null, source);
}
module.exports = loader;
loader.pitch = function (remainingRequest,previousRequest,data) {
console.log('pitch1');
}
//source就是接收到的源文件的内容
let loader = function (source, sourceMaps, extra) {
let cb = this.async();
console.log('loader2');
cb(null, source);
}
module.exports = loader;
loader.pitch = function (remainingRequest,previousRequest,data) {
console.log('pitch2');
return "2";
}
4.3.3 log-loader3.js
//source就是接收到的源文件的内容
const loaderUtils = require('loader-utils');
let loader = function (source) {
let cb = this.async();
console.log('loader3');
cb(null, source);
}
module.exports = loader;
loader.pitch = function () {
console.log('pitch3');
}
{
test: /\.less$/,
use:[path.resolve('src/loaders/log-loader1'),path.resolve('src/loaders/log-loader2'),path.resolve('src/loaders/log-loader3')]
}
let less = require('less');
module.exports = function (source) {
let callback = this.async();
less.render(source, { filename: this.resource }, (err, output) => {
this.callback(err, output.css);
});
}
有些时间我们希望less-loader可以放在use数组最左边,最左边要求返回一个JS脚本
let less=require('less');
module.exports=function (source) {
let callback = this.async();
less.render(source,(err,output) => {
callback(err, `module.exports = ${JSON.stringify(output.css)}`);
});
}
let loaderUtils=require("loader-utils");
function loader(source) {
let script=(`
let style = document.createElement("style");
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`);
return script;
}
//pitch里的参数可不是文件内容,而是文件的请求路径
//pitch request就是你要加载的文件路径 //index.less
loader.pitch = function (request) {
let style = `
var style = document.createElement("style");
style.innerHTML = require(${loaderUtils.stringifyRequest(this, "!!" + request)});
document.head.appendChild(style);
`;
return style;
}
module.exports = loader;
function loader(source) {
let reg = /url\((.+?)\)/g;
let current;
let pos = 0;
let arr = [`let lists = [];`];
while (current = reg.exec(source)) {
let [matchUrl, p] = current;
let index = reg.lastIndex - matchUrl.length;
arr.push(`lists.push(${JSON.stringify(source.slice(pos, index))})`);
pos = reg.lastIndex;
arr.push(`lists.push("url("+require(${p})+")")`);
}
arr.push(`lists.push(${JSON.stringify(source.slice(pos))})`);
arr.push(`module.exports = lists.join('')`);
return arr.join('\r\n');
}
module.exports = loader;
{
"./loaders/css-loader.js!./loaders/less-loader.js!./src/index.less":
/*!*************************************************************************!*\
!*** ./loaders/css-loader.js!./loaders/less-loader.js!./src/index.less ***!
\*************************************************************************/
(function(module, exports, __webpack_require__) {
eval("let lists = [];\r\nlists.push(\"div {\\n color: red;\\n}\\nbody {\\n background: \")\r\nlists.push(\"url(\"+__webpack_require__(/*! ./baidu.png */ \"./src/baidu.png\")+\")\")\r\nlists.push(\";\\n}\\n\")\r\nmodule.exports = lists.join('')\n\n//# sourceURL=webpack:///./src/index.less?./loaders/css-loader.js!./loaders/less-loader.js");
}),
"./src/baidu.png":
(function(module, exports, __webpack_require__) {
eval("module.exports = __webpack_require__.p + \"b15c113aeddbeb606d938010b88cf8e6.png\";\n\n//# sourceURL=webpack:///./src/baidu.png?");
}),
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _index_less__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index.less */ \"./src/index.less\");\n/* harmony import */ var _index_less__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_index_less__WEBPACK_IMPORTED_MODULE_0__);\n\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/index.less":
(function(module, exports, __webpack_require__) {
eval("\n var style = document.createElement(\"style\");\n style.innerHTML = __webpack_require__(/*! !../loaders/css-loader.js!../loaders/less-loader.js!./index.less */ \"./loaders/css-loader.js!./loaders/less-loader.js!./src/index.less\");\n document.head.appendChild(style);\n \n\n//# sourceURL=webpack:///./src/index.less?");
}
cnpm install --save-dev jest babel-jest babel-preset-env
cnpm install --save-dev webpack memory-fs
let {getOptions} = require('loader-utils');
function loader(source){
const options = getOptions(this);
source=source.replace(/\[name\]/g,options.name);
return `module.exports = ${JSON.stringify(source)}`;
}
module.exports=loader;
hello [name]
const path=require('path');
const webpack=require('webpack');
let MemoryFs=require('memory-fs');
module.exports = function(fixture,options={}) {
const compiler=webpack({
mode:'development',
context: __dirname,
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename:'bundle.js'
},
module: {
rules: [
{
test: /\.txt$/,
use: {
loader: path.resolve(__dirname,'../src/loader.js'),
options:{name:'Alice'}
}
}
]
}
});
compiler.outputFileSystem=new MemoryFs();
return new Promise(function (resolve,reject) {
compiler.run((err,stats) => {
if (err) reject(err);
else resolve(stats);
});
});
}
let compile=require('./compile');
test('replace name',async () => {
const stats=await compile('example.txt');
const data=stats.toJson();
const source=data.modules[0].source;
expect(source).toBe(`module.exports = "hello Alice"`);
});
5.6 package.json
"scripts": {
"test":"jest"
}
loader是用来加载处理各种形式的资源,本质上是一个函数, 接受文件作为参数,返回转化后的结构。
loader 用于对模块的源代码进行转换
loader 可以使你在 import 或"加载"模块时预处理文件
! noAutoLoaders 所有的loader都不要执行
!! noPrePostAutoLoaders 不要前后置loader
-! noPreAutoLoaders 不要前置loader
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
const contextInfo = data.contextInfo;
const context = data.context;
const request = data.request;
debugger /*resolve钩子上注册的方法较长,其中还包括了模块资源本身的路径解析。resolver有两种,分别是loaderResolver和normalResolver。*/
const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);
//匹配的资源
let matchResource = undefined;
let requestWithoutMatchResource = request;//这是原始的请求
const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request);//"^([^!]+)!=!"
if (matchResourceMatch) {//如果能匹配上
matchResource = matchResourceMatch[1];//取得匹配到的资源
if (/^\.\.?\//.test(matchResource)) {//如果是一个相对路径,则转成绝对路径
matchResource = path.join(context, matchResource);
}//把匹配到的部分截取掉
requestWithoutMatchResource = request.substr(
matchResourceMatch[0].length
);
}
debugger /*noPreAuto指的是只用行内loader,禁用配置文件中的loader配置*/
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
const noAutoLoaders =
noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");//!表示不走配置
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");//表示禁用前后loader
let elements = requestWithoutMatchResource
.replace(/^-?!+/, "")//把-!替换成空
.replace(/!!+/g, "!")//把!!替换成一个!
.split("!"); debugger /*webpack会从request中解析出所需的loader,包括资源本身 */
let resource = elements.pop();//取得资源
elements = elements.map(identToLoaderRequest);//剩下的全转成loader对象
6.2 webpack/lib/NormalModule.js
runLoaders({
resource: this.resource,
loaders: this.loaders,
context: loaderContext
},
(err, result) => {
const resourceBuffer = result.resourceBuffer;
const source = result.result[0];
const sourceMap = result.result.length >= 1 ? result.result[1] : null;
const extraInfo = result.result.length >= 2 ? result.result[2] : null;//ast
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
this._ast = typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined? extraInfo.webpackAST: null;
}
loader-runner/lib/LoaderRunner.js
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
callback(null, {
result: result,//结果
resourceBuffer: processOptions.resourceBuffer,
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies
});
}
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
var fn = currentLoaderObject.pitch;
runSyncOrAsync(fn,loaderContext);
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true;
var isDone = false;
var isError = false; // internal error
var reportedError = false;
context.async = function async() {
if (isDone) {
if (reportedError) return; // ignore
throw new Error("async(): The callback was already called.");
}
isSync = false;
return innerCallback;
};
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() {
return fn.apply(context, args);
}());
if (isSync) {
isDone = true;
if (result === undefined)
return callback();
if (result && typeof result === "object" && typeof result.then === "function") {
return result.catch(callback).then(function (r) {
callback(null, r);
});
}
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);
}
}
function runSyncOrAsync(fn, context, callback) {
var isSync = true;
context.callback = callback;
context.async = function async() {
isSync = false;
return context.callback;
};
var result = fn.apply(context);
if (isSync) {
return callback(null, result);
}
}
function say() {
return this.name;
}
function say2() {
let cb = this.async();
cb(null);
}
let context = { name: 'zfpx' };
runSyncOrAsync(say2, context, function () {
console.log('over');
});
#6.5 loadLoader.js:13
var module = require(loader.path);
loader.normal = typeof module === "function" ? module : module.default;
loader.pitch = module.pitch;
loader.raw = module.raw;
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);
}
}
);