webpack loader
1、loader
webpack只能处理JavaScript的模块,如果要处理其他类型的文件就需要使用loader进行转换。
loader是webpack中一个重要的概念,它是指用来将一段代码转换成另一段代码的webpack加载器。
2、一个简易loader
初始化一个npm项目,安装webpack,配置webpack.config.js
let path = require('path');
module.exports = {
mode: 'development',
entry: "./src/index.js",
output: {
filename: "build.js",
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: path.resolve(__dirname, 'loaders','jsLoader.js')
}
]
}
}
// 加载loader还可以通过配置resolveLoader 别名alias的配置,不需要在配置loader的地方写path.resolve(__dirname, 'loaders','jsLoader.js') 这么长的内容
let path = require('path');
module.exports = {
mode: 'development',
entry: "./src/index.js",
output: {
filename: "build.js",
path: path.resolve(__dirname, 'dist')
},
resolveLoader: {
alias: {
myJsLoader: path.resolve(__dirname, 'loaders', 'jsLoader.js')
}
},
module: {
rules: [{
test: /\.js$/,
use: 'myJsLoader'
}]
}
}
// 还可以配置成自己寻找 配置modules,设置如果`node_modules`里面找不到就去`./loaders`里面找,这样就可以自动寻找我们自定义的loader了
module.exports = {
mode: 'development',
entry: "./src/index.js",
output: {
filename: "build.js",
path: path.resolve(__dirname, 'dist')
},
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
},
module: {
rules: [{
test: /\.js$/,
use: 'jsLoader'
}]
}
}
加载一个我们自己的loader,在/loaders/jsLoader.js
function loader(source){
return source;
}
module.exports = loader;
loader就是一个函数,接受一个参数,参数是源代码,多个loader的时候从下自上依次执行,前面执行的loader的返回值是下一个loader执行的参数。
输入以上代码执行npx webpack
打包,dist
目录内的build.js
内容主要如下
eval("console.log('loader 1 转换的js');\n\n//# sourceURL=webpack://loader/./src/index.js?");
会发现源代码会被eval()
方法包裹。
这就是一个简易的loader了
3、配置多个loader
多个loader的执行顺序默认是 从下到上,从右到左。
先创建多个loader
// loaders/loader2.js
function loader(source) {
source = source.replace(/loader1/, 'loader2')
return source;
}
module.exports = loader;
// loaders/loader3.js
function loader(source) {
source = source.replace(/loader2/, 'loader3')
return source;
}
module.exports = loader;
修改webpack.config.js
的配置,可以加载多个loader
module.exports = {
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
},
module: {
rules: [{
test: /\.js$/,
use: [
'loader3',
'loader2',
'jsLoader'
]
}]
}
}
然后运行npx webpack
来看结果, 打开dist/build.js
运行代码后输出
loader3 转换的js
可以发现loader的确是从右到左从下到上执行的,而且前面loader的返回值是后面loader的参数。
4、loader的分类
分为4类
- pre:在前面的loader
- post:在后面的loader
- normal:正常的loader
- inline: 行内的loader
执行顺序是: pre -> normal -> inline -> post
。其中inline
loader不需要写在配置里,可以在行内引入使用。
当我们loader的书写顺序需要调整,但是又不想改代码顺序的时候可以配置loader的分类,改变loader的加载顺序
module.exports = {
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'jsLoader',
},
enforce: 'pre',
},
{
test: /\.js$/,
use: {
loader: 'loader2',
}
},
{
test: /\.js$/,
use: {
loader: 'loader3',
},
enforce: 'post',
}
]
}
}
这样写不看enforce
配置单纯看执行顺序是loader3 -> loader2 -> jsLoader
, 实际的顺序还是之前的jsLoader -> loader2 -> loader3
。
4.1 inline-loader
使用inline-loader
可以在代码里直接使用。
let str = require('-!inline-loader!./a.js');
创建inline-loader.js
和 a.js
// a.js
module.exports = 'a.js'
// inline-loader.js
function loader(source) {
console.log('inline loader')
return source;
}
module.exports = loader;
在index.js
里引入a.js
let str = require('-!inline-loader!./a.js');
执行npx webpack
看控制台会打印出inline loader
标识符的含义:
-!
标识符(-!inline-loader.js
)禁用前置和正常loader;会让当前引入的文件不需要通过pre + normal
状态的loader进行处理,直接跳过这两种loader交给inline-loader
来处理,然后处理完交给post
loader处理。!
标识符(inline-loader!./a.js
)是前置的意思,意思是让前面的loader来处理,inline-loader!./a.js
:意思是让inline-loader.js
处理./a.js
!
标识符(!inline-loader.js
)禁用普通loader;会跳过normal
状态的loader,只使用pre
的loader处理完成之后直接给inline-loader
处理,然后处理完交给post
loader处理。!!
标识符(!!inline-loader.js
)禁用前置后置和正常loader; 意思是只通过inline-loader
来处理,不需要其他loader处理。
5、loader的组成
每个loader都有两个部分组成pitch & normal
, pitch和normal的执行顺序相反,normal是正常的loader,当pitch没有定义或者没有返回值的时候,会先依次执行pitch在获取资源执行loader,如果定义的某个pitch有返回值则会跳过读取资源和自己的loader。
loader配置pitch的执行的顺序:loader3.pitch -> loader2.pitch -> loader1.pitch -> 获取到资源 -> loader1 -> loader2 -> loader3
。
如果pitch没有返回值就按上面正常流程执行。
先把index.js
里面引用a.js
的代码注释掉,代码展示:
// 每个loader里面加如下代码
// jsLoader.js
function loader(source){
console.log('jsLoader 执行')
return source;
}
loader.pitch = function(){
console.log('loader1 jsLoader pitch 执行')
}
module.exports = loader;
// loader2.js
function loader(source) {
source = source.replace(/loader1/, 'loader2')
console.log('loader2 执行')
return source;
}
loader.pitch = function() {
console.log('loader2 pitch 执行')
}
module.exports = loader;
// loader3.js
function loader(source) {
source = source.replace(/loader2/, 'loader3')
console.log('loader3 执行')
return source;
}
loader.pitch = function() {
console.log('loader3 pitch 执行')
}
module.exports = loader;
然后运行npx webpack
,可以看loader的加载执行顺序如下
loader3 pitch 执行
loader2 pitch 执行
loader1 jsLoader pitch 执行
jsLoader 执行
loader2 执行
loader3 执行
这是pitch没有返回值的情况下,走的正常的加载流程。如果pitch
有返回值,则会打断这个顺序,假设loader2
的pitch
有返回值,其他代码依旧
// loader2.js
function loader(source) {
console.log('loader2 执行')
source = source.replace(/loader1/, 'loader2')
return source;
}
loader.pitch = function() {
console.log('loader2 pitch 执行')
return 'console.log("loader2 执行的pitch返回值")';
}
module.exports = loader;
然后运行npx webpack
,打印出来结果如下
loader3 pitch 执行
loader2 pitch 执行
loader3 执行
如果loader2.pitch
有返回值的话的执行顺序: loader3.pitch -> loader2.pitch -> loader3
然后运行dist/build.js
,打印结果如下
loader3 执行的pitch返回值
6、loader的特点
- 第一个loader要返回js脚本
- 每个loader只做一件事,可以兼容更多场景,然后链式调用
- 每个loader都是一个模块
- 每个loader都是无状态的,确保loader在不同模块转换之间不保存状态。
7、babel-loader源码
安装@babel-core @babel/preset-env
,@babel-loader
我们自己写。
在loaders
创建文件babel-loader.js
,输入代码
let babel = require('@babel/core');
// 获取在 `webpack.config.js`里面配置loader地方传入的options
let loaderUtils = require('loader-utils');
function loader(source) {
let options = loaderUtils.getOptions(this);
// cb 内置的回调,在异步执行的时候调用它就相当于return了,调用它无需再写 `return source`
let cb = this.async();
// 使用babel来转化源代码
babel.transform(source, {
...options,
sourceMap: true,
// 开启sourcemap 可以在浏览器里面看到源码,方便debug
filename: this.resourcePath.split('/').pop()
}, function(err, result) {
cb(err, result.code, result.map);
})
}
module.exports = loader;
检验我们代码有没有问题,在src/index.js
里面输入一段ES6的箭头函数
const a = () => console.log('2222');
运行npx webpack
,看dist/build.js
贴一下webpack.config.js
里的配置
module.exports = {
devtool: 'source-map',
module: {
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
// 这里就是可以通过 loader-utils 拿到的参数,想传什么传什么,比如一行文本,或者一个文件
presets: [
'@babel/preset-env'
]
}
}
}]
}
}
另外当我们写loader需要对参数进行验证格式的时候可以使用schema-utils
当我们引用了其他文件作为依赖的时候,依赖文件更新需要重新触发编译可以添加
this.addDependency(filePath);
8、file-loader
简单写一下file-loader
的源码
// file-loader.js
// file-loader 处理图片是需要把一个图片转成一个MD5,放到dist目录下, 然后返回一个url
let loaderUtils = require('loader-utils');
function loader(source) {
// interpolateName: 根据当前提供的东西生成一个url
// '[hash].[ext]' :生成hash的名字+原来的格式
let filename = loaderUtils.interpolateName(this, '[hash].[ext]', { content: source });
this.emitFile(filename, source);
return `module.exports = "${filename}"`;
}
loader.raw = true; // 改成二进制
module.exports = loader;
9、url-loader
在处理图片之类的时候,会用到url-loader
,如果文件不大就转换成base64,如果太大就继续调用file-loader
处理
// url-loader.js
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('./file-loader').call(this, source);
}
}
loader.raw = true; // buffer
module.exports = loader;
10、less-loader
处理.less
的文件需要less-loader css-loader style-loader
// less-loader.js 负责转换.less文件为css,传给css-loader处理,然后再由css-loader传给style-loader 插入到HTML中
let less = require('less'); // 解析less 转成 css
function loader(source) {
let css = null;
less.render(source, function(err, res) {
css = res.css;
})
return css;
}
module.exports = loader;
11、css-loader
css-loader
负责处理css
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;
let last = reg.lastIndex - matchUrl.length;
// 存放字符串匹配到的前面部分
arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`);
pos = reg.lastIndex;
// 存放匹配到的部分
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;
12、style-loader
style-loader
负责把处理好的css代码插入到文档里面
function loader(source){
// webpack loader的最后一个loader必须返回一个可执行的js字符串
let str = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.body.appendChild(style);
`;
return str;
}
module.exports = loader;