NormalModuleFactory
和ContextModuleFactory
。它们相当于是两个类工厂,通过其可以创建相应的NormalModule
和ContextModule
loader
是导出为一个函数的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 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')
}
},
在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么扔进同步 loader 中。
loader只能传入一个包含包含资源文件内容的字符串
当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。
保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。
确保 loader 在不同模块转换之间不保存状态。每次运行都应该独立于其他编译模块以及相同模块之前的编译结果。
loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项
schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验
如果一个 loader 使用外部资源(例如,从文件系统读取),必须声明它。这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重编译。
根据模块类型,可能会有不同的模式指定依赖关系。例如在 CSS 中,使用 @import 和 url(...) 语句来声明依赖。这些依赖关系应该由模块系统解析。
不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。loader-utils
中的 stringifyRequest
方法,可以将绝对路径转化为相对路径。
webpack充分地利用缓存来提高编译效率
this.cacheable();
当一个 Loader 无依赖,可异步的时候我想都应该让它不再阻塞地去异步
// 让 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);
});
};
this.callback(
err:Error|null,
content:string|Buffer,
sourceMap?:SourceMap,
meta?:any
);
在默认的情况下,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;
};
方法名 | 含义 |
---|---|
this.context |
当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src |
this.resource |
当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1。 |
this.resourcePath |
当前处理文件的路径,例如 /src/main.js |
this.resourceQuery |
当前处理文件的 querystring |
this.target |
等于 Webpack 配置中的 Target |
this.loadModule |
但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时,就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果 |
this.resolve |
像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string)) |
this.addDependency |
给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string) |
this.addContextDependency |
和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string) |
this.clearDependencies |
清除当前正在处理文件的所有依赖,使用方法为 clearDependencies() |
this.emitFile |
输出一个文件,使用方法为 emitFile(name: string, content: Buffer/string, sourceMap: {...}) |
loader-utils.stringifyRequest |
Turns a request into a string that can be used inside require() or import while avoiding absolute paths. Use it instead of JSON.stringify(...) if you're generating code inside a loader 把一个请求字符串转成一个字符串,以便能在require或者import中使用以避免绝对路径。如果你在一个loder中生成代码的话请使用这个而不要用JSON.stringify() |
loader-utils.interpolateName |
Interpolates a filename template using multiple placeholders and/or a regular expression. The template and regular expression are set as query params called name and regExp on the current loader's context. 使用多个占位符或一个正则表达式转换一个文件名的模块。这个模板和正则表达式被设置为查询参数,在当前loader的上下文中被称为name或者regExp |
属性 | 值 |
---|---|
this.request | /loaders/babel-loader.js!/src/index.js' |
this.userRequest | /src/index.js |
this.rawRequest | ./src/index.js |
this.resourcePath | /src/index.js |
$ cnpm i @babel/preset-env @babel/core -D
const babel = require("@babel/core");
function loader(source,inputSourceMap) {
//C:\webpack-analysis2\loaders\babel-loader.js!C:\webpack-analysis2\src\index.js
console.log(this.request);
const options = {
presets: ['@babel/preset-env'],
inputSourceMap:inputSourceMap,
sourceMaps: true,//ourceMaps: true 是告诉 babel 要生成 sourcemap
filename:this.request.split('!')[1].split('/').pop()
}
//在webpack.config.js中 增加devtool: 'eval-source-map'
let {code,map,ast}=babel.transform(source,options);
return this.callback(null,code,map,ast);
}
module.exports = loader;
resolveLoader: {
alias: {//可以配置别名
"babel-loader": resolve('./build/babel-loader.js')
},//也可以配置loaders加载目录
modules: [path.resolve('./loaders'), 'node_modules']
},
{
test: /\.js$/,
use:['babel-loader']
}
loaders\banner-loader.js
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);
let {filename } = options;
fs.readFile(filename, 'utf8', (err, text) => {
cb(err, text + source);
});
}
module.exports = loader;
loaders\banner.js
/**copyright: zhufengjiagu*/
webpack.config.js
{
test: /\.js$/,
use:[{
loader:'banner-loader',
options:{filename:path.resolve(__dirname,'loaders/banner.js')}
},'babel-loader']
}
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
loaders\loader1.js
function loader(source) {
console.log('loader1',this.data);
return source+"//loader1";
}
loader.pitch = function (remainingRequest,previousRequest,data) {
data.name = 'pitch1';
console.log('pitch1');
}
module.exports = loader;
loaders\loader2.js
function loader(source) {
console.log('loader2');
return source+"//loader2";
}
loader.pitch = function (remainingRequest,previousRequest,data) {
console.log('remainingRequest=',remainingRequest);
console.log('previousRequest=',previousRequest);
console.log('pitch2');
//return 'console.log("pitch2")';
}
module.exports = loader;
loaders\loader3.js
function loader(source) {
console.log('loader3');
return source+"//loader3";
}
loader.pitch = function () {
console.log('pitch3');
}
module.exports = loader;
{
test: /\.js$/,
use: ['loader1', 'loader2', 'loader3']
}
let readFile = require("fs");
let path = require("path");
function createLoaderObject(loader) {
let obj = { data: {} };
obj.request = loader;
obj.normal = require(loader);
obj.pitch = obj.normal.pitch;
return obj;
}
function runLoaders(options, callback) {
let loaderContext = {};
let resource = options.resource;
let loaders = options.loaders;
loaders = loaders.map(createLoaderObject);
loaderContext.loaderIndex = 0;
loaderContext.readResource = readFile;
loaderContext.resource = resource;
loaderContext.loaders = loaders;
let isSync = true;
var innerCallback = (loaderContext.callback = function(err, args) {
loaderContext.loaderIndex--;
iterateNormalLoaders(loaderContext, args, callback);
});
loaderContext.async = function async() {
isSync = false;
return innerCallback;
};
Object.defineProperty(loaderContext, "request", {
get: function() {
return loaderContext.loaders
.map(function(o) {
return o.request;
})
.concat(loaderContext.resource)
.join("!");
}
});
Object.defineProperty(loaderContext, "remainingRequest", {
get: function() {
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("!");
}
});
Object.defineProperty(loaderContext, "previousRequest", {
get: function() {
return loaderContext.loaders
.slice(0, loaderContext.loaderIndex)
.map(function(o) {
return o.request;
})
.join("!");
}
});
Object.defineProperty(loaderContext, "data", {
get: function() {
return loaderContext.loaders[loaderContext.loaderIndex].data;
}
});
iteratePitchingLoaders(loaderContext, callback);
function iteratePitchingLoaders(loaderContext, callback) {
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
loaderContext.loaderIndex--;
return processResource(loaderContext, callback);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
let fn = currentLoaderObject.pitch;
if (!fn) return iteratePitchingLoaders(options, loaderContext, callback);
let args = fn.apply(loaderContext, [
loaderContext.remainingRequest,
loaderContext.previousRequest,
currentLoaderObject.data
]);
if (args) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(loaderContext, args, callback);
} else {
loaderContext.loaderIndex++;
iteratePitchingLoaders(loaderContext, callback);
}
function processResource(loaderContext, callback) {
let buffer = loaderContext.readResource.readFileSync(
loaderContext.resource,
"utf8"
);
iterateNormalLoaders(loaderContext, buffer, callback);
}
}
function iterateNormalLoaders(loaderContext, args, callback) {
if (loaderContext.loaderIndex < 0) return callback(null, args);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
var fn = currentLoaderObject.normal;
if (!fn) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(loaderContext, args, callback);
}
args = fn.apply(loaderContext, [args]);
if (isSync) {
loaderContext.loaderIndex--;
iterateNormalLoaders(loaderContext, args, callback);
}
}
}
let entry = "./src/world.js";
let options = {
resource: path.join(__dirname, entry),
loaders: [
path.join(__dirname, "loaders/loader1.js"),
path.join(__dirname, "loaders/loader2.js"),
path.join(__dirname, "loaders/loader3.js")
]
};
runLoaders(options, (err, result) => {
console.log(result);
});
let result = [useLoadersPost,useLoaders,useLoadersPre];
loaders = results[0].concat(loaders, results[1], results[2]);
useLoadersPost+inlineLoader+useLoaders(normal loader)+useLoadersPre
符号 | 变量 | 含义 | |
---|---|---|---|
-! |
noPreAutoLoaders | 不要前置和普通loader | Prefixing with -! will disable all configured preLoaders and loaders but not postLoaders |
! |
noAutoLoaders | 不要普通loader | Prefixing with ! will disable all configured normal loaders |
!! |
noPrePostAutoLoaders | 不要前后置和普通loader,只要内联loader | Prefixing with !! will disable all configured loaders (preLoaders, loaders, postLoaders) |
let less = require('less');
function loader(source) {
let callback = this.async();
less.render(source, { filename: this.resource }, (err, output) => {
callback(err, output.css);
});
}
module.exports = loader;
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;
}
module.exports = loader;
let less = require('less');
function loader(source) {
let callback = this.async();
less.render(source, { filename: this.resource }, (err, output) => {
callback(err, `module.exports = ${JSON.stringify(output.css)}`);
});
}
module.exports = loader;
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;
}
//remainingRequest 剩下的路径
//previousRequest 之前的路径
// !! noPrePostAutoLoaders 不要前后置和普通loader
//https://github.com/webpack/webpack/blob/v4.39.3/lib/NormalModuleFactory.js#L339
loader.pitch = function (remainingRequest,previousRequest,data) {
//C:\webpack-analysis2\loaders\less-loader.js!C:\webpack-analysis2\src\index.less
console.log('remainingRequest',remainingRequest);
console.log('previousRequest',previousRequest);
console.log('data',data);
let style = `
var style = document.createElement("style");
style.innerHTML = require(${loaderUtils.stringifyRequest(this, "!!" + remainingRequest)});
document.head.appendChild(style);
`;
return style;
}
module.exports = loader;
var postcss = require("postcss");
var loaderUtils = require("loader-utils");
var Tokenizer = require("css-selector-tokenizer");
function plugin(options) {
return function(css) {
css.walkAtRules(/^import$/i, function(rule) {
console.log('rule.params',rule.params)
var values = Tokenizer.parseValues(rule.params);
console.log('values',values);
console.log('values.nodes',values.nodes);
var url = values.nodes[0].nodes[0];
console.log('values.nodes[0].nodes[0]',values.nodes[0].nodes[0]);
});
css.walkDecls(function(decl) {
var values = Tokenizer.parseValues(decl.value);
console.log('values',values)
values.nodes.forEach(function(value) {
console.log('value',value)
value.nodes.forEach(item => {
console.log('item',item)
});
});
});
};
}
var pipeline = postcss([plugin({})]);
pipeline.process('@import "index.css"').then(function(result) {
//console.log(result);
});
/**
rule.params "index.css"
values { type: 'values', nodes: [ { type: 'value', nodes: [Array] } ] }
values.nodes [ { type: 'value', nodes: [ [Object] ] } ]
*/
var postcss = require("postcss");
var loaderUtils = require("loader-utils");
var Tokenizer = require("css-selector-tokenizer");
function plugin(options) {
return function(css) {
css.walkDecls(function(decl) {
var values = Tokenizer.parseValues(decl.value);
console.log('values',values)
values.nodes.forEach(function(value) {
console.log('value',value)
value.nodes.forEach(item => {
console.log('item',item)
});
});
});
};
}
var pipeline = postcss([plugin({})]);
pipeline.process('border:1px solid red;').then(function(result) {
//console.log(result);
});
/**
values { type: 'values', nodes: [ { type: 'value', nodes: [Array] } ] }
value {
type: 'value',
nodes: [
{ type: 'item', name: '1px', after: ' ' },
{ type: 'item', name: 'solid', after: ' ' },
{ type: 'item', name: 'red' }
]
}
item { type: 'item', name: '1px', after: ' ' }
item { type: 'item', name: 'solid', after: ' ' }
item { type: 'item', name: 'red' }
*/
index.js
require('./style.css');
style.css
@import './global.css';
h1 {
color: #f00;
}
.avatar {
width: 100px;
height: 100px;
background-image: url('./avatar.jpg');
background-size: cover;
}
global.css
body {
background-color: green;
}
var postcss = require("postcss");
var loaderUtils = require("loader-utils");
var Tokenizer = require("css-selector-tokenizer");
function plugin(options) {
return function(css) {
let importItems = options.importItems;
let urlItems = options.urlItems;
css.walkAtRules(/^import$/i, function(rule) {
var values = Tokenizer.parseValues(rule.params);
var url = values.nodes[0].nodes[0];
importItems.push({ url: url.value });
});
css.walkDecls(function(decl) {
var values = Tokenizer.parseValues(decl.value);
values.nodes.forEach(function(value) {
value.nodes.forEach(item => {
if (item.type === "url") {
var url = item.url;
item.url = "___CSS_LOADER_URL___" + urlItems.length + "___";
urlItems.push({ url });
}
});
});
decl.value = Tokenizer.stringifyValues(values);
});
};
}
module.exports = function(inputSource) {
var callback = this.async();
let options = { importItems: [], urlItems: [] };
var pipeline = postcss([plugin(options)]);
pipeline.process(inputSource).then(function(result) {
let importJs = options.importItems
.map(function(imp) {
return (
"require(" +loaderUtils.stringifyRequest(this, imp.url) +")"
);
})
.join("\n");
var cssAsString = JSON.stringify(result.css);
cssAsString = cssAsString.replace(/@import\s+['"]([^'"]+?)['"]/g,'');
var URLREG_G = /___CSS_LOADER_URL___(\d+)___/g;
var URLREG = /___CSS_LOADER_URL___(\d+)___/;
cssAsString = cssAsString.replace(URLREG_G, function(item) {
var match = URLREG.exec(item);
if (!match) return item;
const url = options.urlItems[+match[1]].url;
return '" + require("' + url + '") + "';
});
callback(
null,
`
${importJs}
module.exports= ${cssAsString};
`
);
});
};
{
"./loaders/css-loader.js!./src/global.css":
(function(module, exports) {
module.exports= "body {\r\n background-color: green;\r\n }";
}),
"./loaders/css-loader.js!./src/style.css":
(function(module, exports, __webpack_require__) {
__webpack_require__(/*! ./global.css */ "./src/global.css")
module.exports= ";\r\nh1 {\r\n color: #f00;\r\n}\r\n\r\n.avatar {\r\n width: 100px;\r\n height: 100px;\r\n background-image: url('" + __webpack_require__(/*! ./avatar.jpg */ "./src/avatar.jpg") + "');\r\n background-size: cover;\r\n}";
}),
"./src/avatar.jpg":
(function(module, exports, __webpack_require__) {
module.exports = __webpack_require__.p + "be95ec5844e2eb652fd8775b0915a9c2.jpg";
}),
"./src/global.css":
(function(module, exports, __webpack_require__) {
var style = document.createElement("style");
style.innerHTML = __webpack_require__("./loaders/css-loader.js!./src/global.css");
document.head.appendChild(style);
}),
"./src/index.js":
(function(module, exports, __webpack_require__) {
__webpack_require__("./src/style.css");
}),
"./src/style.css":
(function(module, exports, __webpack_require__) {
var style = document.createElement("style");
style.innerHTML = __webpack_require__("./loaders/css-loader.js!./src/style.css");
document.head.appendChild(style);
})
});
link
标签去引入function loader(source) {
//发射或者说输出一个文件,这个文件的内容 就是css文件的内容
this.emitFile('main.css', source);
let script = `
let link = document.createElement('link');
link.setAttribute('rel','stylesheet');
link.setAttribute('href','main.css');
document.head.appendChild(link);
`;
return script;
}
module.exports = loader;
file-loader
并不会对文件内容进行任何转换,只是复制一份文件内容,并根据配置为他生成一个唯一的文件名。
const { getOptions, interpolateName } = require('loader-utils');
function loader(content) {
let options=getOptions(this)||{};
let url = interpolateName(this, options.filename || "[hash].[ext]", {content});
this.emitFile(url, content);
return `module.exports = ${JSON.stringify(url)}`;
}
loader.raw = true;
module.exports = loader;
loaderUtils.interpolateName
方法可以根据 options.name 以及文件内容生成一个唯一的文件名 url(一般配置都会带上hash,否则很可能由于文件重名而冲突)this.emitFile(url, content)
告诉 webpack 我需要创建一个文件,webpack会根据参数创建对应的文件,放在 public path
目录下module.exports = ${JSON.stringify(url)}
,这样就会把原来的文件路径替换为编译后的路径let { getOptions } = require('loader-utils');
var mime = require('mime');
function loader(source) {
let options=getOptions(this)||{};
let { limit, fallback='file-loader' } = options;
if (limit) {
limit = parseInt(limit, 10);
}
const mimetype=mime.getType(this.resourcePath);
if (!limit || source.length < limit) {
let base64 = `data:${mimetype};base64,${source.toString('base64')}`;
return `module.exports = ${JSON.stringify(base64)}`;
} else {
let fileLoader = require(fallback || 'file-loader');
return fileLoader.call(this, source);
}
}
loader.raw = true;
module.exports = loader;
const path = require('path');
module.exports = {
module:{
rules:[
{//css-loader是用来处理解析@import "base.css" url(./bg.jpg)
test:/\.css$/,
use:['style-loader','sprite-loader']
},
{
test:/\.(jpg|png|gif)$/,
use:[
{
loader:'url-loader',
options:{
limit:0
}
}
]
}
]
}
}
loaders\sprite-loader.js
const postcss = require('postcss');
const path = require('path');
const loaderUtils = require('loader-utils');
const SpriteSmith = require('spritesmith');
const Tokenizer = require('css-selector-tokenizer');
function loader(inputSource){
let callback = this.async();
let that = this;//this.context代表被加载资源的上下文目录
function createPlugin(postcssOptions){
return function(css){//代表CSS文件本身
css.walkDecls(function(decl){
let values = Tokenizer.parseValues(decl.value);
values.nodes.forEach(value=>{
value.nodes.forEach(item=>{
if(item.type == 'url' && item.url.endsWith('?sprite')){
//拼一个路径,找到是这个图片绝对路径
let url = path.resolve(that.context,item.url);
item.url = postcssOptions.spriteFilename;
//按理说我要在当前规则下面添一条background-position
postcssOptions.rules.push({
url,//就是原始图片的绝对路径,未来要用来合并雪碧图用
rule:decl.parent //当前的规则
});
}
});
});
decl.value = Tokenizer.stringifyValues(values);//直接把url地址改成了雪碧图的名字
});
postcssOptions.rules.map(item=>item.rule).forEach((rule,index)=>{
rule.append(
postcss.decl({
prop:'background-position',
value:`_BACKGROUND_POSITION_${index}_`
})
);
});
}
}
const postcssOptions = {spriteFilename:'sprite.jpg',rules:[]}
let pipeline = postcss([createPlugin(postcssOptions)]);
pipeline.process(inputSource,{from:undefined}).then(cssResult=>{
let cssStr = cssResult.css;
let sprites = postcssOptions.rules.map(item=>item.url.slice(0,item.url.lastIndexOf('?')));
SpriteSmith.run({src:sprites},(err,spriteResult)=>{
let coordinates = spriteResult.coordinates;
Object.keys(coordinates).forEach((key,index)=>{
cssStr= cssStr.replace(`_BACKGROUND_POSITION_${index}_`,`-${coordinates[key].x}px -${coordinates[key].y}px`);
});
that.emitFile(postcssOptions.spriteFilename,spriteResult.image);
callback(null,`module.exports = ${JSON.stringify(cssStr)}`);
});
});
}
loader.raw = true;
module.exports = loader;
src\sprit.css
*{
padding: 0;
margin: 0;
}
.one{
background: url(./images/1.jpg?sprite);
width:500px;
height:422px;
}
.two{
background: url(./images/2.jpg?sprite);
width:500px;
height:332px;
}
.three{
background: url(./images/3.jpg?sprite);
width:500px;
height:352px;
}
{
test: /\.html$/,
use: {
loader: 'html-layout-loader',
options: {
layout: path.join(__dirname, 'src', 'layout.html'),
placeholder: '{{__content__}}'
}
}
}
plugins: [
new HtmlWebpackPlugin({
template: './src/login.html',
filename: 'login.html'
}),
new HtmlWebpackPlugin({
template: './src/home.html',
filename: 'home.html'
})
]
{{__content__}}
替换成HTML内容const path = require('path');
const fs = require('fs');
const loaderUtils = require('loader-utils');
const defaultOptions = {
placeholder: '{{__content__}}',
decorator: 'layout'
}
module.exports = function (source) {
let callback = this.async();
this.cacheable && this.cacheable();
const options = Object.assign(loaderUtils.getOptions(this), defaultOptions);
const { placeholder, decorator, layout } = options;
fs.readFile(layout, 'utf8', (err, html) => {
html = html.replace(placeholder, source);
callback(null, `module.exports = ${JSON.stringify(html)}`);
})
}
const path = require('path');
const fs = require('fs');
const loaderUtils = require('loader-utils');
const defaultOptions = {
placeholder:'{{__content__}}',
decorator:'layout'
}
module.exports = function(source){
let callback = this.async();
this.cacheable&& this.cacheable();
const options = {...loaderUtils.getOptions(this),...defaultOptions};
const {placeholder,layout,decorator} = options;
const layoutReg = new RegExp(`@${decorator}\\((.+?)\\)`);
let result = source.match(layoutReg);
if(result){
source = source.replace(result[0],'');
render(path.resolve(this.context,result[1]), placeholder, source, callback)
}else{
render(layout, placeholder, source, callback);
}
}
function render(layout, placeholder, source, callback) {
fs.readFile(layout, 'utf8', (err, html) => {
html = html.replace(placeholder, source);
callback(null, `module.exports = ${JSON.stringify(html)}`);
})
}
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"`);
});
"scripts": {
"test":"jest"
}