携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情
loader
的作用就是帮助webpack
来识别计算机的文件,把webpack
本身不识别的文件资源类型转换成webpack
可以识别的js
处理方式,然后再交由webpack
来处理。
loader 是什么?
从计算机的世界来看,文件资源格式实在是太多了,webpack
如果将所有文件资源都替你做好那工作量庞大且不说,还有很多开发商喜欢用自定义类型,你可以自定义那webpack
就直接把解析也自定义不就ok了,你把你的东西处理成我能识别的javascript
格式,其他的你爱咋咋地。
下面就是一个loader
函数形式,具体可以参考loaders:
/** * @param source {string|Buffer} 资源输入,对于第一个执行的 Loader 为资源文件的内容;后续执行的 Loader 则为前一个 Loader 的执行结果,可能是字符串,也可能是代码的 AST 结构; * @param sourceMap? {Object} 可选参数,代码的 [sourcemap](https://github.com/mozilla/source-map) 结构 * @param data? {any} 数据,可以是任何内容 * @returns {*} */ module.exports = (source, sourceMap, data) => { return source; }
注:上面是示例代码写了剪头函数,真实的
loader
不要使用剪头函数,因为webpack
会通过this
注入上下文,剪头函数的this
指向的是外层的this
,会导致注入失败。
Loader 简单示例
上面的loader
是啥也没做,所以我们现在就写一个能做点啥的loader
,代码目录结构如下
├─ src │ ├─ loader │ ├─ └─ index.js │ └─ main.js ├─ package.json └─ webpack.config.js
首先src/loader/index.js
里面的内容就是上面的那个,main.js
里面什么都没有,所以现在只需要配置一下webpack.config.js
就好了:
const path = require('path'); module.exports = { mode: "development", entry: "./src/main.js", output: { path: path.resolve(__dirname, "./dist"), filename: "[name].js", clean: true }, module: { rules: [{ test: /.js$/, use: ['./src/loader/index.js'], // loader 直接指向本地地址就好 }] } }
当然现在我们的loader
是啥也干不了,那我们就加点功能,就比如说我要在文件头部自动加上一些信息,修改src/loader/index.js
代码如下:
module.exports = function(source) { return ( `/** * @author 田八 */ ${source} ` ) }
接下来就执行一下npx webpack
来看一下构建的结果吧。
使用上下文接口
上面的代码是将对应的文件进行转置,其实在loader
的运行过程中,webpack
会自动主动上下文this
,用以使用更加丰富的webpack
的能力,上面提供的loader
链接文档中有这一部分内容。
我们可以直接通过this
关键字来访问webpack
的上下文,上面的示例修改如下可以达到同样的效果:
module.exports = function(source, sourceMap, data) { this.callback( null, `/** \n * @author 田八\n */\n${source}`, sourceMap, data ) }
this.callback
参数解释如下
- 第一个参数必须是
Error
或者null
- 第二个参数是一个
string
或者Buffer
。- 可选的:第三个参数必须是一个可以被 this module 解析的 source map。
- 可选的:第四个参数,会被 webpack 忽略,可以是任何东西(例如一些元数据)。
直接调用this.callback
和使用return source
的效果是相同的,但是使用this.callback
的好处是可以传递多个参数。
官方文档中的内容已经很详细了,下来列举的是比较常用的接口:
fs
:Compilation 对象的inputFileSystem
属性,我们可以通过这个对象获取更多资源文件的内容;resource
:当前文件路径;resourceQuery
:文件请求参数,例如import "./a?foo=bar"
的resourceQuery
值为?foo=bar
;callback
:可用于返回多个结果;getOptions
:用于获取当前 Loader 的配置对象;async
:用于声明这是一个异步 Loader,开发者需要通过async
接口返回的callback
函数传递处理结果;emitWarning
:添加警告;emitError
:添加错误信息,注意这不会中断 Webpack 运行;emitFile
:用于直接写出一个产物文件,例如file-loader
依赖该接口写出 Chunk 之外的产物;addDependency
:将dep
文件添加为编译依赖,当dep
文件内容发生变化时,会触发当前文件的重新构建;
取消 Loader 缓存
默认情况下,webpack
对loader
的处理结果进行缓存,这是因为loader
处理文件是CPU
密集型操作,很耗费性能,使用this.cacheable
方法,并传入false
可以关闭缓存能力:
module.exports = function (source, sourceMap, data) { this.cacheable(false); this.callback(null, `/** \n * @author 田八\n */\n${source}`, sourceMap, data ) }
在 Loader 返回异步结果
webpack
还可以异步返回处理结果,对于需要使用计算机其他硬件或者软件协助实现的功能,处理时间和时机往往都是不确定的,这个时候就需要使用异步返回(this.async
)了,它返回一个this.callback
,我们就可以使用这个callback
返回结果:
module.exports = function (source, sourceMap, data) { const callback = this.async(); // 显示调用,返回一个 callback setTimeout(() => { // 异步返回结果,同this.callback的使用方式一致 callback(null, `/** \n * @author 田八\n */\n${source}`, sourceMap, data ) }, 1000) }
在 Loader 中直接写出文件
this.emitFile
用于直接写入内容到新的产物中,还是上面的示例,可以将注释内容写到一个单独的文件中:
module.exports = function (source, sourceMap, data) { this.emitFile('author.txt', '/** \n * @author 田八\n */'); return source; }
在 Loader 中添加额外依赖
首先需要理解一下什么叫额外依赖,额外依赖就是一开始webpack
管不着的依赖,但是我需要webpack
来帮我管理起来,不然这些文件发生改变了,webpack
不知道,也就无法通知给对应的loader
让其进行处理,这个时候就可以通过this.addDependency
进行添加了:
module.exports = function (source, sourceMap, data) { this.addDependency(path); // 这个 path 是虚拟的,参数类型为 string return source; }
由于我的
loader
示例太过于简单,所以这一块不太方便尝试验证,应该使用其他类型的文件匹配规则的loader
,现成的示例就是sass-loader
和less-loader
就是使用这种方式添加的。
sass-loader
源码片段
此外,Loader Context 还提供了下面几个与依赖处理相关的接口:
addContextDependency(directory: String)
:添加文件目录依赖,目录下内容变更时会触发文件变更;addMissingDependency(file: String)
:用于添加文件依赖,效果与addDependency
类似;clearDependencies()
:清除所有文件依赖。
处理二进制资源
默认情况下,loader
的第一个参数是string
类型,这通常对标的是文本类型,例如.js
、.css
、.html
、.vue
等,但是如果是.png
、.mp3
、.avi
这种通常都是使用二进制流来处理的,string
肯定是不行的,通过设置 raw
为 true
,loader
可以接收原始的 Buffer
,如:
module.exports = function (source, sourceMap, data) { console.log(source); return source; } module.exports.raw = true;
在 Loader 中正确处理日志
webpack
内置了infrastructure logging,使用方式很简单,调用this.getLogger()
可以获得一个logger
的实例,下面是官网原文:
- Loaders 最好使用
this.getLogger()
进行日志记录,这是指向compilation.getLogger()
具有 loader 路径和已处理的文件。这种日志记录被存储到 Stats 中并相应地格式化。它可以被 webpack 用户过滤和导出。- Loaders 可以使用
this.getLogger('name')
获取具有子名称的独立记录器。仍会添加 loader 路径和已处理的文件。- Loaders 可以使用特殊的回退逻辑来检测日志支持
this.getLogger() ? this.getLogger() : console
。在使用不支持getLogger
方法的旧 webpack 版本时提供回退方法。
- 代码示例
module.exports = function (source, sourceMap, data) { const logger = this.getLogger("xxx-loader"); logger.info('这是info级别的日志') return source; }
在 Loader 中正确上报异常
上面写到了日志,日志分级别,那么可能就会直接想到使用日志上报异常,想法很ok,但是loader
中对异常的处理还有其他的方式,日志只是其中一种。
使用this.emitError
,emit 一个错误,也可以在输出中显示,示例:
module.exports = function (source, sourceMap, data) { this.emitError(new Error('出错啦!!!')) return source; } module.exports.raw = true;
- 运行效果
this.emitError
和this.getLogger
的区别:
- 相同点
- 都可以在控制台输出内容。
- 都不会打断正常编译流程。
- 不同点
this.Logger
可以通过用户配置来控制控制台显示的信息级别。this.emtiError
没有级别,一定会打印在控制台中。
使用this.callback
来提交错误信息:
module.exports = function (source, sourceMap, data) { this.callback( new Error('出错啦!!!'), source, sourceMap, data ); }
- 运行效果
对于上面的两种方式,这种方式会直接导致编译失败,之后,Webpack 会将 callback
传递过来的错误信息当做模块内容,打包进产物文件,如上图。
总结
loader
就是用于帮助webpack
识别更多的计算机文件类型,因为loader
的存在,使webpack
处理的文件不局限在javascript
上,只要有对应的loader
理论上可以处理任何文件,同时配合使用webpack
为loader
提供的上下文,我们可以实现各种各样的需求。
这一篇其实只写了一半,一般说到
loader
就会提到loader
的加载顺序,还有loader
都在开发了,但是怎么测试?这些准备下一篇写。