为什么会有Loader?
默认情况下,即在不安装其它Loader时,webpack仅支持处理JS和JSON这两种文件类型,没办法处理其它类型的文件比如css文件、png图片等。因此,在打包时,需要一种工具来处理此类文件,loader就产生了。webpack就是通过Loader去支持其他文件类型并且把他们转化成有效的模块,并且可以添加到依赖图中。
给Loader下个定义?
定义:Loader是一个导出为函数的JS模块,接受源代码,返回处理后的代码。
也就是说Loader本身是一个函数,将源文件作为参数,返回转换后的文件,比如将TS的代码转成JS代码,将图像转成base64的字符串 。
以下就是一个最简单的loader,接受源码返回源码
module.exports = function(source) {
return source
}
Loader的执行顺序是怎样的?
多个Loader串行执行,顺序是从后到前
从后到前,从下到上,从右到左,这些说的都是从数组的尾loader执行到首loader
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.css$/, // test 指定匹配规则
use: [ // use 指定使用的loader名称
'style-loader',
'css-loader',
'less-loader'
]
}
]
}
}
以上webpack的配置文件,会先执行less-loader,然后将执行的结果传给css-loader,执行css-loader,然后将执行结果传递给style-loader,然后执行style-loader。处理less文件,为什么需要这么loader呢,这是因为webpack提倡保持loader的功能单一,loader应该只做单一的任务,不仅使loader已维护,也方便在更多场景中进行链式调用。
您可能会问怎样设计这样的顺序?既然每一个loader本质上是一个函数,那么多个loader就是函数的组合。函数的组合可以参考以下这种形式(webpack采取的就是这种)
compose(f, g) => (...args) => f(g(...args))
常用的loader
名称 | 描述 |
---|---|
babel-loader | 转换ES6/ES7等JS新特性语法 |
css-loader | 支持.css文件的加载和解析 |
less-loader | 将less文件转换成css |
ts-loader | 将ts转换成JS |
file-loader | 进行图片、字体等的打包 |
raw-loader | 将文件以字符串的形式导入 |
thread-loader | 多进程打包JS和CSS |
loader-runner
loader-runner 模块提供了方法,让我们方便的运行和测试loader,它允许您在不安装webpack的情况下运行loader。
loader-runner也作为webpack的依赖,webpack中就是使用它执行了loader,我们也可以使用loader-running进行loader的开发和调试。
loader-runner的使用
import { runLoaders } from "loader-runner";
runLoaders({
resource: "/abs/path/to/file.txt?query",
// 字符串:资源的绝对路径(查询字符串可选)
loaders: ["/abs/path/to/loader.js?query"],
// 字符串数组:loader的绝对路径(查询字符串可选)
// {loader, options}[]: 带有options对象的加载器的绝对路径
context: { minimize: true },
// 除了基本的上下文,添加额外的loader上下文
processResource: (loaderContext, resourcePath, callback) => { ... },
// 可选:处理资源的函数
// Must have signature 必须是具名函数 有方法名,有参数 function(context, path, function(err, buffer))
// By default readResource is used and the resource is added a fileDependency
readResource: fs.readFile.bind(fs)
// 可选:读取资源的函数
// 仅当processResource没有被提供时使用
// Must have signature function(path, function(err, buffer))
// 默认是s.readFile
}, function(err, result) {
// err: Error?
// result.result: Buffer | String
// The result
// only available when no error occurred
// result.resourceBuffer: Buffer
// The raw resource as Buffer (useful for SourceMaps)
// only available when no error occurred
// result.cacheable: Bool
// Is the result cacheable or do it require reexecution?
// result.fileDependencies: String[]
// An array of paths (existing files) on which the result depends on
// result.missingDependencies: String[]
// An array of paths (not existing files) on which the result depends on
// result.contextDependencies: String[]
// An array of paths (directories) on which the result depends on
})
以开发测试一个raw-loader为例
raw-loader.js
module.exports = function(source) {
const json = JSON.stringify(source)
.replace('foo', '')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
return `export default ${json}`; // loader的返回值
}
使用run-loader.js测试
const { runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');
runLoaders({
resource: path.join(__dirname, './src/demo.txt'), // 源文件
loaders: [
{
loader: path.join(__dirname, './src/raw-loader.js'), // 指定loader位置
options: { // 传递给loader携带的额外参数
name: 'test'
}
}
],
context: {
emitFile: () => {}
},
readResource: fs.readFile.bind(fs)
}, (err, result) => {
err ? console.log(err) : console.log(result);
});
运行run-loader得到结果
{
result: [ 'export default "bar"' ],
resourceBuffer: <Buffer 66 6f 6f 62 61 72>,
cacheable: true,
fileDependencies: [
'e:\\project\\learn-webpack\\geektime-webpack-course\\code\\chapter07\\raw-loader\\src\\demo.txt'
],
contextDependencies: []
}
获取传递给loader的参数
如果需要获取传递给loader参数,如run-loader.js代码中的第10行的参数name: test,可以记住loader-utils模块中的方法获取
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const { name } = loaderUtils.getOptions(this) // 获取options中传递的name参数
console.log(this.query.name) // 这种方式也可以获取到name参数
}
loader中的异常处理-抛出错误
loader内直接可以通过throw抛出
throw new Error('Error')
或者通过this.callback抛出
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap? : SourceMap
meta?: any
)
this.callback(new Error('Error', json))
// 或者
this.callback(null, json, 2, 3, 4) // 第一个参数必须是null, 后面的参数是返回的其它参数
借助callback还有另外一个好处,可以返回多个值,而return 只能返回一项
loader中的异步处理
有些耗时的操作使得loader变成异步的loader,比如文件的读取或者图像的处理
通过this.async来返回一个异步函数,第1个参数是Error,第2个参数是处理后的结果
以读取文件的内容并返回为例
const fs = require('fs')
const path = reqiure('path')
module.exports = function (source) {
const callback = this.async()
// 读取文件async.txt中的文件并返回
fs.readFile(path.join(__dirname, './async.txt'), 'utf-8', (err, data) => {
if (err) {
callback(err, '')
return
}
callback(null, data)
})
}
开启loader缓存
webpack中默认开启loader缓存, 也可以关掉缓存
this.cacheable(false) // 关闭loader缓存
缓存条件:loader的结果在相同的输入下有确定的输出,也就是说做了记忆化的处理
有依赖的loadr无法使用缓存?
loader输出文件
通过this.emitFile将文件到写入到其它文件中
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const url = loaderUtils.interpolateName(this, '[name].[ext]', source);
console.log(url); // 获取到了源文件的名称
this.emitFile(url, source); // 把源文件写到dist文件下以源文件名称命名的文件中
return source;
}
Loader编写案例
让我们开发一个Loader,功能是将css中的px值转换成相对根元素以rem为单位的相对值,标准尺寸下即开发时的根元素大小,以参数unit的形式传入,类似px2rem-loader的功能
入口文件index.js
import './index.css'
const a = 1
样式文件index.css
.box {
width: 100px;
height: 200px;
font-size: 40px;
line-height: 40px;
color: #0000ff;
background-color: #deb8e7;
}
webpack配置文件
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'main.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
// px2rem-loader处理后,传递给css-loader将css的样式内容以字符串的形式拼接,并将其作为js的模块导出内容
'css-loader',
{
loader: path.resolve('./loaders/px2rem-loader'),
options: {
unit: 20, // 开发时,标准尺寸的根元素大小
}
},
]
}
]
}
}
开发的px2rem-loader文件
const loaderUtils = require('loader-utils');
module.exports = function (source) {
const pxRegExp = /\b(\d+(\.\d+)?)px\b/g; // 匹配所有px单位的正则
const { unit } = loaderUtils.getOptions(this); // 获取传递给loader的参数 unit
const callback = this.async(); // 异步回调,当然这里可以不用异步的形式,为了演示上面讲解的loader中的异步处理
const url = loaderUtils.interpolateName(this, '[name].[ext]', {content: source});
// 将px单位大小转成以rem为单位的大小
let result = source.replace(pxRegExp, (match, $1) => {
return `${Number($1) / unit}rem`
})
// 保存转换后的文件
this.emitFile(url, result);
// 返回处理后的文件
callback(null, result)
}
执行npm run build
命令即运行webpack打包,打包成功后会在dist目录下得到index.css,main.js文件
Index.css文件内容如下,可以看到所有px大小都转换成了rem大小
.box {
width: 5rem;
height: 10rem;
font-size: 2rem;
line-height: 2rem;
color: #0000ff;
background-color: #deb8e7;
}
转换之后的文件会传递给css-loader,css-loader会将css以字符串的形式放到main.js中,打开main.js可以看到如下内容
o.push([n.i,".box {\r\n width: 5rem;\r\n height: 10rem;\r\n font-size: 2rem;\r\n line-height: 2rem;\r\n color: #0000ff;\r\n background-color: #deb8e7;\r\n}",""])
以上是px2rem-loader的简写版,完整实现可以参考实现源码px2rem-loader
思考:
在webpack中,loader和plugin有什么区别?常见的loader有哪些?说一说loader的执行流程?