写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。本博客的Github地址。
[TOC]
babel-loader
// 装包
yarn add @babel/core @babel/preset-env loader-utils
1
2
2
实现步骤:
- 获取loader参数
- 基于babel转换代码
- 返回转换后的代码
// 思路:从webpack.config.js中拿到babel的预设,通过预设来转换模块
// 先引入babel
const babel = require('@babel/core');
// 拿到babel的参数,使用loader-utils
const loaderUtils = require('loader-utils');
function loader(source) {
// this是loader的上下文
const options = loaderUtils.getOptions(this);
// console.log(Object.keys(this));
// console.log(options); // { presets: [ '@babel/preset-env' ] }
// console.log(this.resourcePath); // /Users/xxxx/study/Blog/前端相关/Webpack学习总结/webpack4-study/code/webpack手写/babel-loader-demo/src/index.js
// babel的转换是异步的,同步的返回是不行的,不能用return
// 同步就是直接调用,异步会在async中
// 这里采用异步,不阻塞其它编译流程
const callback = this.async();
// console.log(this.resourcePath.split('/').pop());
babel.transform(source, {
...options,
sourceMap: true, // 设置生成sourceMap,还需要再webpack.config.js中配置devtool: 'source-map'
// pop方法删除数组的最后一个元素,并返回该元素,会影响原数组
// 给生成的source-map指定名字
filename: this.resourcePath.split('/').pop()
}, (err, result) => {
// console.log(Object.keys(result)); // [ 'metadata', 'options', 'ast', 'code', 'map', 'sourceType' ]
// result.map是sourceMap
callback(err, result.code, result.map);
});
// return只能返回一个值,callback可以返回多个值
// return source; // 不起作用了
}
module.exports = loader;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
file-loader和url-loader
file-loader
// 装包
yarn add mime loader-utils
1
2
2
mime主要作用是:设置某种扩展名的文件的响应程序类型。
创建my-file-loader.js:
实现步骤:
- 获取文件名
- 文件输出目录兼容处理,默认直接输出到dist目录下
- 原来的路径变成编译后的路径
需要注意二进制的处理:
loader.raw = true;
1
const loaderUtils = require('loader-utils');
const path = require('path');
// file-loader只是对文件进行复制移动到固定目录
function loader(source) {
// '[name].[ext]'
const {output} = loaderUtils.getOptions(this);
// console.log(output); // { output: '/assets' }
// interpolateName获取文件的hash值,并插入值,生成唯一的文件名
// const interpolatedName = loaderUtils.interpolateName(loaderContext, name, options);
// this代表loader上下文
const filename = loaderUtils.interpolateName(this, '[name].[hash:8].[ext]', {content: source});
if (output) {
this.emitFile(path.resolve(__dirname, output, filename), source);
} else {
this.emitFile(filename, source);
}
// console.log(filename); // avatar.9a70fede.jpg
// console.log('my-file-loader');
// 把原来的路径变成编译后的路径
return `module.exports='${filename}'`;
}
loader.raw = true; // 二进制
module.exports = loader;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
url-loader
my-url-loader.js:
// 拿到loader的参数 需要工具包loaderUtils
const loaderUtils = require('loader-utils');
const mime = require('mime'); // 作用是设置某种扩展名的文件的响应程序类型
// 这里source是Buffer
function loader(source) { // loader的参数就是源代码
const {limit} = loaderUtils.getOptions(this);
// console.log(this.resourcePath);
// 如果图片小于limit,则使用base64编码
// console.log(source); Buffer
// console.log(source.toString('base64'));
// mime.getType(this.resourcePath) 是文件类型
if (limit && limit > source.length) {
return `module.exports="data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"`
} else {
// limit小于文件大小时还是走file-loader
return require('./my-file-loader').call(this, source);
}
}
loader.raw = true; // 二进制
module.exports = loader;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
webpack配置如下:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}, {
test: /\.(ico|gif|png|jpg|jpeg|webp)$/i,
use: {
loader: 'my-file-loader',
options: {
output: '/assets'
}
}
// use: {
// loader: 'url-loader',
// options: {
// limit: 100 * 1024 // 小于100k的图片采用base64编码
// }
// }
}
]
},
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
less-loader/css-loader/style-loader
style-loader
// 装包
yarn add less
1
2
2
在loaders目录下,分别创建my-style-loader.js、my-css-loader.js、my-less-loader.js。 my-style-loader.js:
const loaderUtils = require('loader-utils');
function loader(source) {
// 最后一个loader要返回一个脚本
const str = `
const style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`;
return str;
}
// 在style-loader上写了pitch,有返回,后面的跳过,自己的写也不会执行
// remainingRequest:剩余的请求
loader.pitch = function (remainingRequest) {
// /Users/xxxx/study/Blog/前端相关/Webpack学习总结/webpack4-study/code/webpack手写/less-loader&css-loader/loaders/my-css-loader.js!/Users/xxxx/study/Blog/前端相关/Webpack学习总结/webpack4-study/code/webpack手写/less-loader&css-loader/loaders/my-less-loader.js!/Users/liujie26/study/Blog/前端相关/Webpack学习总结/webpack4-study/code/webpack手写/less-loader&css-loader/src/style/index.less
// 剩余的请求 my-css-loader!my-less-loader!./index.less
// console.log(remainingRequest);
// require路径 返回的就是css-loader处理好的结果require('!!css-loader!less-loader!./index.less')
console.log(loaderUtils.stringifyRequest(this, '!!' + remainingRequest)); // "!!../../loaders/my-css-loader.js!../../loaders/my-less-loader.js!./index.less"
const str = `
let style = document.createElement('style')
style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)})
document.head.appendChild(style)
`;
// stringifyRequest方法用来将绝对路径转为相对路径
return str;
}
module.exports = loader;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
css-loader
css-loader:主要用来处理css中的图片链接,需要把url转换成require。 my-css-loader.js:
// css-loader 用来解析@import或者url()语法,包括css中引入的图片
function loader(source) {
const reg = /url\((.+?)\)/g;
let pos = 0;
let current;
let arr = ['let list = []'];
while(current = reg.exec(source)) {
const [matchUrl, g] = current;
// console.log(matchUrl, g);// url('./avatar.jpg') './avatar.jpg'
// 拿到css从开始到地址链接之前的部分的索引值
let lastIndex = reg.lastIndex - matchUrl.length;
arr.push(`list.push(${JSON.stringify(source.slice(pos, lastIndex))})`); // 获取css开始和地址之前的代码
pos = reg.lastIndex;
// 把g替换成require的写法
arr.push(`list.push('url('+ require(${g}) +')')`); // 拼入图片地址
}
arr.push(`list.push(${JSON.stringify(source.slice(pos))})`); // 拼入地址到结尾的代码
arr.push(`module.exports = list.join('')`);
// console.log(arr.join('\r\n'));
/**
let list = []
list.push("body {\n background-color: green;\n background: ")
list.push('url('+ require('./avatar.jpg') +')')
list.push(";\n}\n")
module.exports = list.join('')
*/
return arr.join('\r\n');
}
module.exports = loader;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
正则表达式对象的lastIndex属性
var regexp = /abcd/g;
var str = 'abcdefg';
alert(regexp.test(str)); // true
alert(regexp.test(str)); // false
alert(regexp.test(str)); // true
1
2
3
4
5
2
3
4
5
lastIndex从字面上来讲就是最后一个索引,实际上它的意思是正则表达式开始下一次查找的索引位置,第一次的时候总是为0的,第一次查找完了的时候会把lastIndex的值设为匹配到得字符串的最后一个字符的索引位置加1,第二次查找的时候会从lastIndex这个位置开始,后面的以此类推。如果没有找到,则会把lastIndex重置为0。要注意的是,lastIndex属性只有在有全局标志正则表达式中才有作用,如果我们把上面代码中正则表达式的g标志去掉,那么三次弹出的就都是true了。
less-loader
less-loader实现比较简单,直接调用less.render
方法将less语法转为css语法并返回。
my-less-loader.js:
const less = require('less');
function loader(source) {
let css = '';
less.render(source, (err, output) => {
css = output.css;
});
return css;
}
module.exports = loader;
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
webpack配置如下:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}, {
test: /\.less$/,
use: ['my-style-loader', 'my-css-loader', 'my-less-loader']
}, {
test: /\.(ico|gif|png|jpg|jpeg|webp)$/i,
use: {
loader: 'url-loader',
options: {
limit: 100 * 1024 // 小于100k的图片采用base64编码
}
}
}
]
},
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
index.js
import './style/index.less';
1
index.less
@color: green;
body {
background-color: @color;
background: url('./avatar.jpg')
}
1
2
3
4
5
2
3
4
5