loader源码
loader是用来加载处理各种形式的资源,本质上是一个函数, 接受文件作为参数,返回转化后的结构。
- loader 用于对模块的源代码进行转换
- loader 可以使你在 import 或"加载"模块时预处理文件
1.1 loader核心代码 [#](#t11.1 loader核心代码)
webpack/lib/NormalModule.js:263
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;
}
1.2 LoaderRunner.js [#](#t21.2 LoaderRunner.js)
loader-runner/lib/LoaderRunner.js
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
callback(null, {
result: result,//结果
resourceBuffer: processOptions.resourceBuffer,
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies
});
}
1.3 LoaderRunner.js:155 [#](#t31.3 LoaderRunner.js:155)
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
var fn = currentLoaderObject.pitch;
runSyncOrAsync(
fn,
loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {}
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true;
context.async = function async() {
isSync = false;
return innerCallback;
};
var innerCallback = context.callback = function() {
isSync = false;
callback.apply(null, arguments);
};
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
}());
if(isSync) {
return callback(null, result);
}
1.4 loadLoader.js:13 [#](#t41.4 loadLoader.js:13)
loader-runner/lib/loadLoader.js:13
var module = require(loader.path);
loader.normal = typeof module === "function" ? module : module.default;
loader.pitch = module.pitch;
loader.raw = module.raw;
1.5 LoaderRunner.js [#](#t51.5 LoaderRunner.js)
loader-runner/lib/LoaderRunner.js
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);
}
}
);
1.6 pitch [#](#t61.6 pitch)
The loaders are called from right to left. But in some cases loaders do not care about the results of the previous loader or the resource. They only care for metadata. The pitch method on the loaders is called from left to right before the loaders are called. If a loader delivers a result in the pitch method the process turns around and skips the remaining loaders, continuing with the calls to the more left loaders. data can be passed between pitch and normal call.
In the complex case, when multiple loaders are chained, only the last loader gets the resource file and only the first loader is expected to give back one or two values (JavaScript and SourceMap). Values that any other loader give back are passed to the previous loader.
2. loader配置 [#](#t72. loader配置)
loader
是导出为一个函数的node
模块。该函数在loader
转换资源的时候调用。给定的函数将调用loader API
,并通过this
上下文访问。
2.1 匹配(test)单个 loader [#](#t82.1 匹配(test)单个 loader)
匹配(test)单个 loader,你可以简单通过在 rule 对象设置 path.resolve 指向这个本地文件
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}
2.2 匹配(test)多个 loaders [#](#t92.2 匹配(test)多个 loaders)
你可以使用 resolveLoader.modules 配置,webpack 将会从这些目录中搜索这些 loaders。
resolveLoader: {
modules: [path.resolve('node_modules'), path.resolve(__dirname, 'src', 'loaders')]
},
2.1 3 npm link [#](#t102.1 3 npm link)
确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好; 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 - loader-name 是指在第1步中的 package.json 文件中配置的模块名称。
npm link
3. loader用法 [#](#t113. loader用法)
3.1 单个loader用法 [#](#t123.1 单个loader用法)
当一个 loader 在资源中使用,这个 loader 只能传入一个参数 - 这个参数是一个包含包含资源文件内容的字符串
同步 loader 可以简单的返回一个代表模块转化后的值。
在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么扔进同步 loader 中。
loader只能传入一个包含包含资源文件内容的字符串
同步 loader 可以简单的返回一个代表模块转化后的值
loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值
loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个参数值是 SourceMap,它是个 JavaScript 对象
loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个参数值是 SourceMap,它是个 JavaScript 对象。
3.2 多个loader [#](#t133.2 多个loader)
当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。
- 最后的 loader 最早调用,将会传入原始资源内容。
- 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。
- 中间的 loader 执行时,会传入前一个 loader 传出的结果。
3.3 单个loader用法 [#](#t143.3 单个loader用法)
- 最后的 loader 最早调用,将会传入原始资源内容。
- 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。
- 中间的 loader 执行时,会传入前一个 loader 传出的结果。
4 用法准则 [#](#t154 用法准则)
4.1 简单 [#](#t164.1 简单)
- loaders 应该只做单一任务。这不仅使每个 loader 易维护,也可以在更多场景链式调用。
4.2 链式(Chaining) [#](#t174.2 链式(Chaining))
- 利用 loader 可以链式调用的优势。写五个简单的 loader 实现五项任务,而不是一个 loader 实现五项任务
4.3 模块化(Modular) [#](#t184.3 模块化(Modular))
保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。
4.4 无状态(Stateless) [#](#t194.4 无状态(Stateless))
确保 loader 在不同模块转换之间不保存状态。每次运行都应该独立于其他编译模块以及相同模块之前的编译结果。
4.5 loader 工具库(Loader Utilities) [#](#t204.5 loader 工具库(Loader Utilities))
https://github.com/webpack/loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项
https://github.com/webpack-contrib/schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验
4.6 loader 依赖(Loader Dependencies) [#](#t214.6 loader 依赖(Loader Dependencies))
如果一个 loader 使用外部资源(例如,从文件系统读取),必须声明它。这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重编译。
4.7 模块依赖(Module Dependencies) [#](#t224.7 模块依赖(Module Dependencies))
根据模块类型,可能会有不同的模式指定依赖关系。例如在 CSS 中,使用 @import 和 url(...) 语句来声明依赖。这些依赖关系应该由模块系统解析。
4.8 绝对路径(Absolute Paths) [#](#t234.8 绝对路径(Absolute Paths))
不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。loader-utils
中的 stringifyRequest
方法,可以将绝对路径转化为相对路径。
4.9 同等依赖(Peer Dependencies) [#](#t244.9 同等依赖(Peer Dependencies))
- 如果你的 loader 简单包裹另外一个包,你应该把这个包作为一个 peerDependency 引入。
- 这种方式允许应用程序开发者在必要情况下,在 package.json 中指定所需的确定版本。
5. API [#](#t255. API)
5.1 缓存结果 [#](#t265.1 缓存结果)
webpack充分地利用缓存来提高编译效率
this.cacheable();
5..2 异步 [#](#t275..2 异步)
当一个 Loader 无依赖,可异步的时候我想都应该让它不再阻塞地去异步
// 让 Loader 缓存
module.exports = function(source) {
var callback = this.async();
// 做异步的事
doSomeAsyncOperation(content, function(err, result) {
if(err) return callback(err);
callback(null, result);
});
};
5.3 raw loader [#](#t285.3 raw loader)
默认的情况源文件是以 UTF-8
字符串的形式传入给 Loader,设置module.exports.raw = true
可使用 buffer 的形式进行处理
module.exports.raw = true;
5.4 获得 Loader 的 options [#](#t295.4 获得 Loader 的 options)
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取到用户给当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
};
5.5 返回其它结果 [#](#t305.5 返回其它结果)
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
);
5.6 同步与异步 [#](#t315.6 同步与异步)
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);
});
};
1.4.7 处理二进制数据 [#](#t321.4.7 处理二进制数据)
在默认的情况下,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;
5.8 缓存 [#](#t335.8 缓存)
在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。
module.exports = function(source) {
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};
5.9 其它 Loader API [#](#t345.9 其它 Loader API)
方法名 | 含义 |
---|---|
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)) |
his.addDependency | 给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string) |
this.addContextDependency | 和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string) |
this.clearDependencies | 清除当前正在处理文件的所有依赖,使用方法为 clearDependencies() |
this.emitFile | 输出一个文件,使用方法为 emitFile(name: string, content: Buffer/string, sourceMap: {...}) |
#
6.loader实战loader-utils,schema-utils,this.async,this.cacheable,getOptions,validateOptions,addDependency
6.1 webpack.BannerLoader [#](#t366.1 webpack.BannerLoader)
const path = require('path');
const fs=require('fs');
const { getOptions } = require('loader-utils');
const validateOptions = require('schema-utils');
const schema={
type: 'object',
properties: {
template: {
type:'string'
}
}
}
module.exports=function (source) {
let callback = this.async();
this.cacheable&&this.cacheable();
const options=getOptions(this);
validateOptions(schema,options,'Banner-Loader');
let template=path.resolve(options.template);
this.addDependency(template);
fs.readFile(template,'utf8',(err,data) => {
if (err) callback(err);
callback(null,data+"\n"+source);
});
}
6.2 pitch [#](#t376.2 pitch)
6.2.1 md-loader1.js [#](#t386.2.1 md-loader1.js)
function normal(source) {
return `console.log("1${source}")`;
}
function pitch() {
console.log(1);
}
module.exports=normal;
exports.pitch=pitch;
6.2.2 md-loader2.js [#](#t396.2.2 md-loader2.js)
function normal(source) {
return `2${source}`;
}
function pitch() {
console.log(2);
}
module.exports=normal;
exports.pitch=pitch;
6.2.3 md-loader3.js [#](#t406.2.3 md-loader3.js)
function normal(source) {
return `3${source}`;
}
function pitch() {
console.log(3);
}
module.exports=normal;
exports.pitch=pitch;
{
test: /\.md$/,
use:['md-loader','md-loader','md-loader']
}
6.3 css [#](#t416.3 css)
6.3.1 less-loader.js [#](#t426.3.1 less-loader.js)
let less=require('less');
module.exports=function (source) {
less.render(source,(err,output) => {
this.callback(err,output.css);
});
}
6.3.2 style-loader [#](#t436.3.2 style-loader)
let less=require('less');
let loaderUtils=require("loader-utils");
module.exports=function (source) {
let script=(`
let style = document.createElement("style");
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`);
return script;
}
/*
module.exports.pitch=function (request,preceding,data) {
let script=(`
let style = document.createElement("style");
let source=require(${loaderUtils.stringifyRequest(this, '!!' + request)});
style.innerHTML = source;
document.head.appendChild(style);
`);
return script;
}
*/
6.4 file [#](#t446.4 file)
6.4.1 file-loader [#](#t456.4.1 file-loader)
const { getOptions,interpolateName } = require('loader-utils');
function loader(content) {
let options=getOptions(this)||{};
let {name}=options;
let url = interpolateName(this, name?name:"[hash]", {
content
});
this.emitFile(url,content);
return `module.exports = ${JSON.stringify(url)};`;
}
loader.raw=true
module.exports=loader;
6.4.2 url-loader [#](#t466.4.2 url-loader)
const {getOptions}=require('loader-utils');
var mime = require('mime');
function loader(src) {
let options=getOptions(this)||{};
let {limit}=options;
if (limit) {
limit = parseInt(limit, 10);
}
const file=this.resourcePath;
debugger;
const mimetype = options.mimetype || mime.lookup(file);
if (!limit || src.length < limit) {
if (typeof src === 'string') {
src = Buffer.from(src);
}
return `module.exports = ${JSON.stringify(`data:${mimetype || ''};base64,${src.toString('base64')}`)}`;
}
const fallback = require(options.fallback ? options.fallback : 'file-loader');
return fallback.call(this, src);
}
module.exports=loader;
6.4 html-layout-loader [#](#t476.4 html-layout-loader)
6.4.1 webpack.config.js [#](#t486.4.1 webpack.config.js)
{
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'
})
]
6.4.2 html-layout-loader [#](#t496.4.2 html-layout-loader)
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)}`);
})
}
#
7.loader测试7.1 安装依赖 [#](#t517.1 安装依赖)
cnpm install --save-dev jest babel-jest babel-preset-env
cnpm install --save-dev webpack memory-fs
7.2 .babelrc [#](#t527.2 .babelrc)
{
"presets": [[
"env"
]]
}
7.3 src/loader.js [#](#t537.3 src/loader.js)
import { getOptions } from 'loader-utils';
export default function loader(source) {
const options = getOptions(this);
source = source.replace(/\[name\]/g, options.name);
return `export default ${ JSON.stringify(source) }`;
};
7.4 test/example.txt [#](#t547.4 test/example.txt)
hello [name]
7.5 test/compiler.js [#](#t557.5 test/compiler.js)
import path from 'path';
import webpack from 'webpack';
import memoryfs from 'memory-fs';
export default (fixture, options = {}) => {
const compiler = webpack({
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((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err);
resolve(stats);
});
});
}
7.6 test/loader.test.js [#](#t567.6 test/loader.test.js)
import compiler from './compiler.js';
test('Inserts name and outputs JavaScript', async () => {
const stats=await compiler('example.txt');
const output = stats.toJson().modules[0].source;
expect(output).toBe(`export default \"Hey Alice!\"`);
});
7.7 package.json [#](#t577.7 package.json)
"scripts": {
"test": "jest"
}
Gitalking ...
Be the first guy leaving a comment!