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]
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]