webpack
的loader
本质上是一个导出的函数,loader runner[1]会调用该函数,在loader
函数内部,this
的上下文指向是webpack
,通常loader
内部返回的是一个string
或者Buffer
。当前loader
返回的结果,会传递给下一个执行的loader
今天一起学习一下webpack5
中的loader
,让我们进一步加深对webpack
的理解
正文开始...
开始一个loader
首先我们看下,通常情况下loader
是怎么使用的
module.exports = {
...
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/env']
}
},
]
}
]
},
}
在module.rules
下,use
是一个数组,数组中是可以有多个loader
默认情况loader:'babel-loader'
会从node_modules
中的lib/index.js
中执行内部的_loader
函数,然后通过内部@babel/core
这个核心库对源代码进行ast
转换,最终编译成es5
的代码
现在需要自己写个loader
,参考官方文档writing loader[2]
我们在新建一个loader
目录,然后新建test-loader
module.exports = function (source) {
console.log('hello world')
return source;
}
在rules
中我们修改下
const path = require('path')
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: path.resolve(__dirname, 'loader/test-loader.js'),
}
]
}
]
}
}
当我运行npm run start
时,我们会发现loader
中加载的自定义test-loader
已经触发了。
但是官方提供另外一种方式
在resolveLoader
中可以给加载loader
快捷的注册路径,这样就可以像官方一样直接写test-loader
了,这个是文件名,文件后缀名默认可以省略。
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: 'test-loader',
}
]
}
]
},
resolveLoader: {
modules: ['node_modules', './loader']
},
}
我们知道loader
中可以设置options
,而在自定义loader
是如何获取options
的参数呢?
官方提供了loader
的一些接口api-loader[3]
getOptions
获取loader传过来的options
// loader/test-loader.js
module.exports = function (source) {
const options = this.getOptions();
console.log(options);
console.log('hello world')
return source
}
我们可以看到以下options
传入的参数
...
use: [
{
loader: 'test-loader',
options: {
name: 'Maic',
age: 18
}
}
]
在官方提供了一个简单的例子,主要是用schema-utils
验证options
传入的数据格式是否正确
安装schema-utils
npm i schema-utils --save-dev
在test-loader
中引入schema-utils
// 定义schema字段数据类型
const schema = {
type: 'object',
properties: {
name: {
type: 'string',
description: 'name is require string'
},
age: {
type: 'number',
description: 'age is require number'
}
}
}
// 引入validate
const { validate } = require('schema-utils');
module.exports = function (source) {
// 获取loader传入的options
const options = this.getOptions();
validate(schema, options);
console.log(options);
console.log('hello world')
return source
}
当我把rules
中options
修改类型时
{
use: [
{
loader: 'test-loader',
options: {
name: 'Maic',
age: '18'
}
}
]
}
运行npm run start
直接提示报错了,相当于validate
这个方法帮我们验证了loader
传过来的options
,如果传入的options
类型不对,那么直接报错了,我们可以用此来检验参数的类型。
自定义babel-loader
在之前的所有项目中,我们都会使用这个babel-loader
,那我们能不能自己实现一个自定义的babel-loader
呢?
首先我们要确定,babel
转换es6
,我们需要安装依赖两个插件,一个是@babel/core
核心插件,另一个是@babel/preset-env
预设插件
修改rules
,我们现在使用一个test-babel-loader
插件
...
{
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: 'test-babel-loader',
options: {
presets: ['@babel/preset-env'] // 预设
}
},
{
loader: 'test-loader',
options: {
name: 'Maic',
age: 18
}
}
]
}
]
},
resolveLoader: {
modules: ['node_modules', './loader']
},
}
修改test-babel-loader
// 引入@babel/core核心库
const babelCore = require('@babel/core');
module.exports = function (content) {
// 获取options
const options = this.getOptions();
// 必须异步方式
const callback = this.async();
// 转换es6
babelCore.transform(content, options, (err, res) => {
if (err) {
callback(err);
} else {
callback(null, res.code);
}
})
在index.js
中写入一些es6代码
const sayhello = () => {
const str = 'hello world';
console.log(str)
}
sayhello();
然后在package.json
写入打包命令
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start": "webpack server --port=8081",
"build": "webpack"
},
我们执行npm run build
test-loader
与test-babel-loader
都会执行,而且生成的main.js
源代码的es6
已经被转换成es5
了。
写一个自定义markdown-loader
首先我们在loader
目录下新建一个markdown-loader.js
// markdown-loader.js
module.exports = function (content) {
console.log(content)
return content;
}
然后在rules
中加入自定义loader
{
test: /.md$/,
loader: 'markdown-loader'
}
...
我们需要在src/index.js
中引入md
文件
import md from '../doc/index.md';
const sayhello = () => {
const str = 'hello world';
console.log(str)
}
sayhello();
我们运行npm run build
已经获取到了doc/index.md
的内容了
在loader中我需要解析md
的内容,此时我们需要借助一个第三方的md
解析器marked[4]
npm i marked --save-dev
详细使用文档参考markedjs[5]
代码语言:javascript复制const { marked } = require('marked');
module.exports = function (content) {
// 解析md
const ret = marked.parse(content)
console.log(ret);
return ret;
}
我们运行npm run build
此时依然报错,错误提示You may need an additional loader to handle the result of these loaders.
所以需要解析html
,那么此时需要另外一个loader
来解决,html-loader
npm i html-loader --save-dev
然后添加html-loader
{
test: /.md$/,
use: ['html-loader', 'markdown-loader']
}
我们在看下index.js
import md from '../doc/index.md';
console.log(md)
const sayhello = () => {
const str = 'hello world';
console.log(str)
}
sayhello();
我们在index.js
打印引入的md
就一段html-loader
转换过的最终代码
import md from '../doc/index.md';
const sayhello = () => {
const str = 'hello world';
console.log(str)
}
sayhello();
const renderMd = () => {
const app = document.getElementById('app');
const div = document.createElement('div');
div.innerHTML = md;
app.appendChild(div);
}
renderMd();
我么最终就看到md
文件就成功通过我们自己写的loader给转换了
本质上就是将md
转换成html
标签,然后再渲染到页面上了
总结
- 了解
loader
的本质,实际上就是一个导出的函数,该函数只能返回字符串
或者Buffer
,内部提供了很多钩子,比如getOptions
可以获取loader
中的options
loader
的执行顺序是从下往上或者从右往左,在后一个loader中的content
是前一个loader
返回的结果- loader有两种类型,一种是同步
this.callback
,另一种是异步this.async
- 了解自定义
babel
转换,通过@bable/core
,@babel/preset-env
实现es6转换 - 实现了一个自定义
markdown
转换器,主要是利用marked.js
这个对md
文件转换成html,但是html
标签进一步需要html-loader
- 本文示例code-example[6]
参考示例
[1]loader runner: https://github.com/webpack/loader-runner
[2]writing loader: https://webpack.docschina.org/contribute/writing-a-loader/
[3]api-loader: https://webpack.docschina.org/api/loaders/
[4]marked: https://github.com/markedjs/marked
[5]markedjs: https://marked.js.org/
[6]code-example: https://github.com/maicFir/lessonNote/tree/master/webpack/webpack-12-loader