Webpack Loader学习与使用webpack 介绍 学习loader之前,先了解下webpack,Webpack
webpack 介绍
学习loader之前,先了解下webpack,Webpack是一个强大的模块打包工具,它可以帮助开发者将多个模块打包成一个或多个bundle文件,并提供了很多文件处理能力。通过webpack可以将我们的开发工程化。 但是webpack本身只能处理js和json文件,对于css、less、图片等文件是无法处理的,所以webpack提供了loader机制,让webpack可以处理其他格式的文件。
基于loader机制,在社区中提供了丰富的loader插件,我们平时也主要是使用一些现成的loader在webpack中进行配置。本文主要是和大家一起学习loader的入门配置以及不同的loader的区别,在最后实现了一个自定义的loader功能,供大家参考学习。
什么是loader
官网的解释:loader用于对模块的源代码进行转换。loader可以使你在import或"load(加载)" 模块时预处理文件。因此,loader类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader可以将文件从不同的语言(如 TypeScript)转换为JavaScript或将内联图像转换为data URL。loader甚至允许你直接在JavaScript模块中import CSS 文件。
loader本质上是导出一个函数模块。loader-runner会调用此函数,然后将上一个loader产生的结果或者资源文件传入进去。函数中的this作为上下文会被webpack填充,并且loader-runner中包含一些实用的方法,比如可以把loader调用方式变为异步。在LoaderRunner源码 中可以看到loader-runner是如何去执行这些loader方法的,感兴趣的可以学习一下。
我们先来看下loader的最简单的使用样例,在下列代码中对于css与ts文件分别进行了loader处理配置:
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' },
{ test: /\.ts$/, use: 'ts-loader' },
],
},
};
如何配置一个loader
从上面的例子中可以看到一些配置参数,在module下进行rules配置,rules是模块的处理规则,rules允许你在webpack配置中指定多个loader。其中每一个对象都是一项规则,具体参数的介绍如下:
-
test:可以是字符串、正则表达式、函数、数组,只有正则匹配上的模块才会使用这条规则。 如以/.css$/来匹配所有以.css结尾的文件。
-
include:符合条件的资源让这项规则中的loader处理,用法和test一样。
-
exclude:符合条件的资源要排除在外,不能让这项规则中的loader处理,用法和test一样。例如排除一些目录中的文件来提升loader的处理速度。
-
use:可以接收一个数组,数组包含该规则所使用的loader,也可以是字符串,对象等。对应文件使用的loader,比如use: ['css-loader']其实是use: [ { loader: 'css-loader'} ]的简写。
-
loader:具体使用的loader的name,也可以是一个文件地址。
-
oneOf:当规则匹配时,只使用第一个匹配规则。
大多数情况下,在处理某一类资源时我们都需要同时使用多个loader来处理文件。比如对于less文件得处理,我们需要less-loader来处理其语法,并将其编译为css;接着再用css-loader处理css的各类加载语法;最后使用style-loader来将样式字符串包装成style标签插入页面。
loader的分类
按照配置方式loader可以分为四类:
- pre:前置loader,通过enforce指定。
- normal:普通loader,默认loader即为普通loader
- inline:内联loader,这种loader不在webpack配置文件中进行配置,一般很少使用。通过在引入文件时直接指定相应的loader,多个loader之家通过!相连。
- post:后置loader,通过enforce指定。
import common from 'loader-a!loader-b!loader-c?type=abc!./common.js'
rules: [
{
test: /\.(jsx|tsx)$/,
loader: './loader/post.loader.js',
enforce: "post",
},
{
test: /\.(jsx|tsx)$/,
loader: './loader/pre.loader.js',
enforce: "pre",
},
{
test: /\.(jsx|tsx)$/,
loader: './loader/normal.loader.js',
}
]
其中前置loader和后置loader,可以通过rule对象的enforce属性来指定,enforce只接收pre或post两种字符串类型的值。 如果不指定enforce属性,默认为普通loader。我们一般不会去enforce来指定loader的是顺序,只需要保证loader的顺序正确即可。
按照内容执行方式,可以将loader分为四种:
- 同步loader
同步loader在整个loader的执行流程中为同步执行。默认loader就是使用同步的方式,可以直接return内容或者通过调用this.callback来返回内容,this.callback相较于直接return可以多传入一些参数,使用更加灵活方便, 比如异常情况的处理等,或者有时会需要传递给下一个loader一些参数,此时就需要通过调用this.callback进行内容传递。
const loader = function (content, map, meta) {
console.log('loader');
/*
param1:error 是否有错误
param2:content 处理后的内容
param3:source-map 信息可继续传递 source-map
param4:meta 给下一个 loader 传递的参数
*/
this.callback(null, content, map, meta);
// 调用this.callback或者直接return都可以
// return content;
}
module.exports = loader;
- 异步loader 异步loader并不是让渡当前loader的执行权力给下一个loader先执行。而是卡住当前的执行进程,方便我们做一些异步的操作处理,等这些异步操作完成后再返回处理内容,将任务进程交给下一个loader。 主要是通过调用this.async获取到一个callback函数,然后在操作执行完毕后在调用callback来通知loader执行下一个任务。
const asyncLoader = function (content, map, meta) {
// this.async 告诉 loader-runner 这个 loader 将会异步地回调。返
// 回callback 在需要通知下一步操作时进行调用
const callback = this.async();
setTimeout(() => {
console.log('asyncLoader');
// 调用 callback 后,才会执行下一个 loader
callback(null, content, map, meta);
}, 500);
}
module.exports = asyncLoader;
- raw loader
raw loader一般用于处理Buffer数据流的文件。在处理图片,字体图标等经常会使用它。
const rawLoader = function(content) {
console.log('content', content);
const publicPath = `__webpack_public_path__ + '/a.png'`;
return `module.exports = ${publicPath};`;
}
rawLoader.raw = true;
module.exports = rawLoader;
输出结果:可以看到入参内容是buffer格式。
编译结果:可以看到图片内容被替换成了loader内返回的内容。
- pitch loader
loader模块中导出函数的pitch属性指向的函数就叫pitch loader。它的使用场景是:当前loader依赖上个loader的输出结果,且该结果为js而非webpack处理后的资源。其中data参数,可以用于数据传递。
const ptichLoader = function(content) {
console.log('ptichLoader', this.data);
return content;
}
/**
* @remainingRequest 剩余请求
* @precedingRequest 前置请求
* @data 数据对象
*/
ptichLoader.pitch = function (remainingRequest, precedingRequest, data) {
console.log('pitch', data);
console.log('remainingRequest', remainingRequest);
console.log('precedingRequest', precedingRequest);
data.pitch = true;
};
module.exports = ptichLoader;
可以看到在pitch函数中往data对象上添加数据,之后在normal函数中通过this.data的方式读取已添加的数据。pitch函数也会先于loader函数执行。
loader的执行顺序
首先我们要知道,loader执行过程分为两个阶段,分别是pitching和normal阶段。 1、相同优先级情况下:先按从上到下,从左到右的顺序执行每个loader上的pitch方法。再按从右到左,从下到上的顺序执行每个loader函数。 2、在不同优先级情况下:执行优先级为 pre(前置) -> normal(普通) -> inline(内联) -> post(后置)。
pitching阶段: webpack规定,在每个loader上可以有一个pitch属性,该属性指向一个函数。
1、当loader.pitch没有返回值时,按照上面的执行顺序执 2、当loader.pitch有返回值,就直接结束当前loader的Pitching阶段,并直接跳到当前Loader执行pitching阶段时的前一个loader的normal阶段,然后继续执行。
让我们一起来看下面不同loader得执行顺序:
rules: [
{
test: /\.(jsx|tsx)$/,
loader: './loader/post.loader.js',
enforce: "post",
},
{
test: /\.(jsx|tsx)$/,
loader: './loader/pre.loader.js',
enforce: "pre",
},
{
test: /\.(jsx|tsx)$/,
loader: './loader/normal.loader.js',
},
{
test: /\.(jsx|tsx)$/,
loader: './loader/ptich1.loader.js',
},
{
test: /\.(jsx|tsx)$/,
loader: './loader/ptich2.loader.js',
},
{
test: /\.(jsx|tsx)$/,
loader: './loader/ptich3.loader.js',
},
]
const normalLoader = function(content) {
console.log('normalLoader');
return content;
}
module.exports = normalLoader;
const postLoader = function(content) {
console.log('postLoader');
return content;
}
module.exports = postLoader;
const preLoader = function(content) {
console.log('preLoader');
return content;
}
module.exports = preLoader;
const ptichLoader = function(content) {
console.log('ptich1Loader');
return content;
}
/**
* @remainingRequest 剩余请求
* @precedingRequest 前置请求
* @data 数据对象
*/
ptichLoader.pitch = function (remainingRequest, precedingRequest, data) {
// some code
console.log('pitch1');
};
module.exports = ptichLoader;
const ptichLoader2 = function(content) {
console.log('ptich2Loader');
return content;
}
/**
* @remainingRequest 剩余请求
* @precedingRequest 前置请求
* @data 数据对象
*/
ptichLoader2.pitch = function (remainingRequest, precedingRequest, data) {
// some code
console.log('pitch2');
};
module.exports = ptichLoader2;
const ptichLoader3 = function(content) {
console.log('ptich3Loader');
return content;
}
/**
* @remainingRequest 剩余请求
* @precedingRequest 前置请求
* @data 数据对象
*/
ptichLoader3.pitch = function (remainingRequest, precedingRequest, data) {
// some code
console.log('pitch3');
};
module.exports = ptichLoader3;
可以看到执行结果如下:
常见的loader以及配置
babel-loader:可以将ES6+代码转换为ES5代码,它使我们能够在工程中使用最新的语言特性,同时不必特别关注这些特性在不同平台的兼容问题。 babel-preset-env是Babel官方推荐的预置器,可根据用户设置的目标环境自动添加所需的插件和补丁来编译ES6+代码。 使用exclude排除掉不需要处理的目录比如node_modules, cacheDirectory配置项,它会启用缓存机制,在重复打包未改变过的模块时防止二次编译,同样也会加快打包的速度。
module.exports = {
module: {
rules: [
{
test: /.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
// 按需加载
useBuiltIns: "usage",
// 指定core-js版本
corejs: 3
}
]
]
}
}
}
]
}
}
less-loader、css-loader、style-loader:先通过less-loader将less语法编译为css,之后再通过css-loader处理css的各类加载语法,再之后通过style-loader来将样式字符串包装成style标签插入页面。
rules: [
{
test: /.less$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
// 启用/禁用 url() 处理
url: true,
// 启用/禁用 @import 处理
import: true,
// 启用/禁用 Sourcemap
sourceMap: false
}
},
"less-loader"
]
}
]
file-loader:将打包过程中遇到的文件类型的资源,复制到对应输出目录,并返回文件的路径。
rules: [
{
test: /.(png|jpe?g|gif)$/,
use: {
loader: "file-loader",
options: {
// placeholder 占位符 [name] 源资源模块的名称
// [ext] 源资源模块的后缀
name: "[name]_[hash].[ext]",
//打包后的存放位置
outputPath: "./images",
// 打包后文件的 url
publicPath: './images',
}
}
}
]
ts-loader:ts-loader与babel-loader的性质类似,它是用于连接Webpack与Typescript的模块。
实现一个自定义loader
当我们有一个自定义的组件库,此时我们希望去统计每个组件的使用情况,如果我们有很多项目都在使用这个组件库,并且随时都在增加,那么这个时候使用loader去进行统计就会简单很多,在每个项目中都配置一个loader即可,我们可以对代码文件进行loader配置,然后将这些文件的内容进行解析统计,然后就可以得到我们想要的数据了。下面我们来一起实现吧。
首先统计一个组件库中每个组件的使用次数需要将每个文件中的代码解析成ast语法树,这样我们就可以把文件中使用组件库的每个组件解析出来,然后再进行汇总。可以使用ast在线预览试试代码解析的ast结果。
1、下面的图片是一个简单的结果。通过分析ast语法树:
可以发现包名在source中,需要统计的每个组件在specifiers数组中
2、在配置loader的时候配置一个pkgName参数,用来统计pkgName的组件库中的组件使用情况。
{
test: /\.(jsx|tsx)$/,
loader: './loader/js.loader.js',
options: {
pkgName: 'my-component',
}
}
3、在我们实现loader内部,可以通过loader-utils工具将上一步配置的pkgName参数获取到, 然后通过@babel/parser工具将jsx、tsx文件转换为ast语法树,然后再参照之前语法树分析的结果进行逻辑处理: 1、先定义初始对象,对进行组件使用情况进行统计; 2、先通过节点类型ImportDeclaration以及之前设置的pkgName可以将我们需要的数据过滤出来; 3、当统计完毕后通过callback调用来进行下一个任务;
const parser = require('@babel/parser');
const loaderUtils = require('loader-utils');
const total = {
len: 0,
components: {},
};
module.exports = function (source) {
// 拿到loader得配置
const options = loaderUtils.getOptions(this);
const callback = this.async();
try {
// 解析成 ast
const ast = parser.parse(source, {
sourceType: 'module',
plugins: ['jsx', 'tsx'],
});
if (ast) {
setTimeout(() => {
const getImport = 'ImportDeclaration';
// 通过节点类型和pkgName过滤出组件库被引入的
const importAst = ast.program.body.filter(
(i) => i.type === getImport && i.source.value.includes(options.pkgName),
);
total.len = total.len + importAst.length;
// 统计每个组件被使用的次数
for (let i of importAst) {
const { specifiers = [] } = i;
for (let s of specifiers) {
if (s.local) {
const { name } = s.local;
total.components[name] = total.components[name] ? total.components[name] + 1 : 1;
}
}
}
console.log(total, 'total');
callback(null, source);
}, 0);
} else {
callback(null, source);
};
} catch (error) {
callback(null, source);
}
}
通过以上,我们可以对webpack-loader有了一个基本的了解,再日常工作中也可去实现一个自己的loader,通过loader去实现一些个性化的能力。
参考链接: webpack-loader官网 LoaderRunner源代码
作者:洞窝-昌军
转载自:https://juejin.cn/post/7396542465299316799