什么是loader
loader 用于对模块的源代码进行转换
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
在更高层面,在 webpack 的配置中 loader 有两个目标:
- test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
- use 属性,表示进行转换时,应该使用哪个 loader。
使用loader
在你的应用程序中,有三种使用 loader 的方式:
- 配置(推荐):在 webpack.config.js 文件中指定 loader。
- 内联:在每个 import 语句中显式指定 loader。
- CLI:在 shell 命令中指定它们。
配置
module.rules 允许你在 webpack 配置中指定多个 loader。 这是展示 loader 的一种简明方式,并且有助于使代码变得简洁。同时让你对各个 loader 有个全局概览:module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
}
]
}
]
}
内联
可以在 import 语句或任何等效于 “import” 的方式中指定 loader。使用 ! 将资源中的 loader 分开。分开的每个部分都相对于当前目录解析。
尽可能使用 module.rules,因为这样可以减少源码中的代码量,并且可以在出错时,更快地调试和定位 loader 中的问题。
import Styles from 'style-loader!css-loader?modules!./styles.css';
// 例子
import Styles from 'inline-loader?modules!./a.js'; // 内联loader写法1
require('inline-loader!./a') // 内联loader写法2
// -! 不会让文件 再去通过pre + normal loader来处理了
require('-!inline-loader!./a') // 内联loader写法2
// ! 没有normal
require('!inline-loader!./a')
// !! 什么都不要
CLI
你也可以通过 CLI 使用 loader:
webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
这会对 .jade 文件使用 jade-loader,对 .css 文件使用 style-loader 和 css-loader。
loader 特性
- loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
- loader 可以是同步的,也可以是异步的。
- loader 运行在 Node.js 中,并且能够执行任何可能的操作。
- loader 接收查询参数。用于对 loader 传递配置。
- loader 也能够使用 options 对象进行配置。
- 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
- 插件(plugin)可以为 loader 带来更多特性。
- loader 能够产生额外的任意文件。
loader 通过(loader)预处理函数,为 JavaScript 生态系统提供了更多能力。 用户现在可以更加灵活地引入细粒度逻辑,例如压缩、打包、语言翻译和其他更多。
loader的api
https://webpack.docschina.org/api/loaders/
loaderapi之 emitFile
this.emitFile(name: string, content: Buffer|string, sourceMap: {...})
loaderapi之 resource,resourcePath,resourceQuery
this.resource = this.resourcePath + this.resourceQuery
request 中的资源部分,包括 query 参数。
在示例中:’/abc/resource.js?rrr’
- webpack.config.js
```javascript function loader(source) { // G:\github_project\webpack-study-2022\src\index.js resource console.log(this.resource, ‘resource’) // ‘’ console.log(this.resourceQuery) // G:\github_project\webpack-study-2022\src\index.js resource console.log(this.resourcePath) return source }{
test: /\.js$/,
use: {
loader: "test-api-loader",
}
},
module.exports = loader
![image.png](https://cdn.nlark.com/yuque/0/2022/png/866396/1643079913804-bc1cc9cf-a22e-4220-b74d-70d609947505.png#clientId=u1d003981-8e62-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=59&id=CjjAQ&margin=%5Bobject%20Object%5D&name=image.png&originHeight=118&originWidth=652&originalType=binary&ratio=1&rotation=0&showTitle=false&size=19139&status=done&style=none&taskId=ub31f628f-36da-4f73-bf0b-1c675820be3&title=&width=326)
<a name="RFXGw"></a>
# loader帮助函数之loaderUtils
- 'getOptions',
- 'parseQuery',
- 'stringifyRequest',
- 'getRemainingRequest',
- 'getCurrentRequest',
- 'isUrlRequest',
- 'urlToRequest',
- 'parseString',
- 'getHashDigest',
- 'interpolateName'
<a name="v4q8F"></a>
### 1)getOptions
获取options里面的数据
```javascript
{
test: /\.js$/,
use: {
loader: "test-api-loader",
options: {
customProps: 'wangling'
}
}
let loaderUtils = require('loader-utils')
function loader(source) {
console.log(loaderUtils.getOptions(this)) // { customProps: 'wangling' }
return source
}
module.exports = loader
如果没有options就返回null
{
test: /\.js$/,
use: 'test-api-loader'
}
loader的执行顺序
loader执行顺序如下 pre > normal > inline > post
默认执行顺序
从右向左,从下到上
从右到左
rules:[
{
test:/\.js$/,
use:['loader1','loader1','loader3']
},
]
从下到上
rules:[
{
test:/\.js$/,
use: {
loader: 'loader1'
}
},
{
test:/\.js$/,
use: {
loader: 'loader2'
}
},
{
test:/\.js$/,
use: {
loader: 'loader3'
}
},
]
enforce 改变loader执行顺序
- pre:在前面执行
- post:在后面执行
- normal: 默认是normal
module: { rules: [ { test: /\.js$/, use: { loader: 'loader1' } }, { test: /\.js$/, use: { loader: 'loader2' }, enforce: "pre" }, { test: /\.js$/, use: { loader: 'loader3' } } ] // 执行属性 loader2,loader3,loader1
最简单loader - 执行顺序例子
先看下例子,具体怎么写loader后面再说 ```javascript CustomLoader是自定义的loader的文件夹 |— undefined |— package-lock.json |— package.json |— README.md |— webpack.config.js |— CustomLoader | |— inline-loader.js | |— loader1.js | |— loader2.js | |— loader3.js |— src|-- a.js |-- index.js
```javascript
// loader1.js
function loader(source) {
console.log('loader1')
return source
}
module.exports = loader
// loader2.js
function loader(source) {
console.log('loader2')
return source
}
module.exports = loader
// loader3.js
function loader(source) {
console.log('loader1')
return source
}
module.exports = loader
// index.js
console.log('haha')
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
resolveLoader: {
// 找loader的位置
modules: ['node_modules', path.resolve(__dirname, 'CustomLoader')]
},
module: {
rules: [
{
test: /\.js$/,
use: ['loader3', 'loader2', 'loader1']
}
]
}
}
运行 npx webpack
结果如下
loader1
loader2
loader3
改变下
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
resolveLoader: {
// 找loader的位置
modules: ['node_modules', path.resolve(__dirname, 'CustomLoader')]
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'loader3'
},
enforce: "post"
},
{
test: /\.js$/,
use: {
loader: 'loader2'
},
enforce: "pre"
},
{
test: /\.js$/,
use: {
loader: 'loader1'
}
}
]
}
}
结果如下
loader2
loader1
loader3
Loader的pitch方法
在定义一个loader函数时,可以导出一个pitch方法,这个方法会在loader函数执行前执行。
另外,如果存在多个loader串行的情况,这些loader的pitch函数会从左到右依次执行,其示意图如下:
// loader1.js
function loader(source) {
console.log('loader1')
return source
}
loader.pitch = function() {
console.log('loader1 pitch')
}
module.exports = loader
// loader2.js
function loader(source) {
console.log('loader2')
return source
}
loader.pitch = function() {
console.log('loader2 pitch')
}
module.exports = loader
// loader3.js
function loader(source) {
console.log('loader3')
return source
}
loader.pitch = function() {
console.log('loader3 pitch')
}
module.exports = loader
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'CustomLoader')]
},
module: {
rules: [
{
test: /\.js$/,
use: ['loader3', 'loader2', 'loader1']
}
]
}
}
运行 npx webpack
如果给loader2的pitch一个返回值呢?
// loader2.js
function loader(source) {
console.log('loader2')
return source
}
loader.pitch = function() {
console.log('loader2 pitch')
// 返回任意值都是一样的效果,除了undefined
return 'xxx'
}
module.exports = loader
只输出了一个loader3
手写loader
babel-loader实现
npm i @babel/core@7.2 @babel/preset-env@7.2 loader-utils@1.1 -S
babel-loader实现
let babel = require('@babel/core');
let loaderUtils = require('loader-utils')
function loader(source) {
/*
loaderUtils.getOptions(this) 获取options这部分
* options: {
presets: ['@babel/preset-env']
}
* */
let options = loaderUtils.getOptions(this);
let cb = this.async();
babel.transform(source,{
...options,
// 源代码调试
sourceMap:true,
// 主要是为了sourceMap显示文件名名称,不然会显示undefined
filename: this.resourcePath.split('/').pop() // 文件名
},function (err,result) {
cb(err,result.code,result.map); // 异步
});
}
module.exports = loader;
测试代码
// webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'CustomLoader')]
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "custom-babel-loader",
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
}
// index.js
class MyName {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
const name = new MyName('wangling')
console.log(name.getName())
file-loader实现
let loaderUtils = require('loader-utils');
function loader(source) {
// file-loader 需要返回一个路径
let filename = loaderUtils.interpolateName(this, '[name].[hash].[ext]', { content: source});
// zly.4d069064177f2de284d771e6ff46df22.jpg filename
console.log(filename, 'filename')
this.emitFile(filename, source); // 发射文件
return `module.exports="${filename}"`
}
loader.raw = true; // 二进制
module.exports = loader;
url-loader实现
let loaderUtils = require('loader-utils');
let mime = require('mime');
function loader(source) {
let { limit } = loaderUtils.getOptions(this) || {};
if (limit && limit > source.length) {
return `module.exports="data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"`
} else {
return require('./custom-file-loader').call(this, source)
}
}
loader.raw = true;
module.exports = loader;
banner-loader
let loaderUtils = require('loader-utils');
let validateOptions = require('schema-utils');
let fs = require('fs');
function loader(source) {
this.cacheable && this.cacheable()
let options = loaderUtils.getOptions(this);
let cb = this.async();
let schema = {
type:'object',
properties:{
text:{
type:'string',
},
filename:{
type:'string'
}
}
}
validateOptions(schema, options,'banner-loader');
if(options.filename){
this.addDependency(options.filename); // 自动的添加文件依赖
fs.readFile(options.filename,'utf8',function (err,data) {
cb(err, `/**${data}**/${source}`);
});
}else{
cb(null, `/**${options.text}**/${source}`);
}
}
module.exports = loader;
style-loader,css-loader,less-loader实现
// style-loader
let loaderUtils = require('loader-utils');
function loader(source) {
// 我们可以在style-loader中导出一个 脚本
let str = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`
return str;
}
// 在style-loader上 写了pitch
// style-loader less-loader!css-loader!./index.less
loader.pitch = function (remainingRequest) {
// 剩余的请求
// 让style-loader 去处理less-loader!css-loader/./index.less
// require路径 返回的就是css-loader处理好的结果 require('!!css-loader!less-loader!index.less')
let str = `
let style = document.createElement('style');
style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});
document.head.appendChild(style);
`
return str;
}
module.exports = loader;
// css-loader
function loader(source) {
let reg = /url\((.+?)\)/g;
let pos = 0;
let current;
let arr = ['let list = []'];
while (current = reg.exec(source)) { // [matchUrl,g]
let [matchUrl, g] = current;
//console.log(matchUrl, g)
let last = reg.lastIndex - matchUrl.length;
arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`);
pos = reg.lastIndex;
// 把 g 替换成 require的写法 => url(require('xxx'))
arr.push(`list.push('url('+require(${g})+')')`);
}
arr.push(`list.push(${JSON.stringify(source.slice(pos))})`)
arr.push(`module.exports = list.join('')`);
return arr.join('\r\n');
}
module.exports = loader;
// less-loader
let less = require('less');
function loader(source) {
let css;
less.render(source,function (err,r) { // r.css
css = r.css;
});
console.log(css)
return css
}
module.exports = loader;
参考链接
webpack 中如何自定义loader
手写清除console的loader
https://www.webpackjs.com/concepts/loaders/ webpack-loader介绍
https://www.webpackjs.com/contribute/writing-a-loader/ 编写一个loader
https://www.npmjs.com/package/loader-utils/v/0.2.15?activeTab=versions loader-utils