写一个自定义loader,看完,就会

2022-07-28 12:42:20 浏览数 (1)

webpackloader本质上是一个导出的函数,loader runner[1]会调用该函数,在loader函数内部,this的上下文指向是webpack,通常loader内部返回的是一个string或者Buffer。当前loader返回的结果,会传递给下一个执行的loader

今天一起学习一下webpack5中的loader,让我们进一步加深对webpack的理解

正文开始...

开始一个loader

首先我们看下,通常情况下loader是怎么使用的

代码语言:javascript复制
  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

代码语言:javascript复制
module.exports = function (source) {
  console.log('hello world')
  return source;
}

rules中我们修改下

代码语言:javascript复制
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了,这个是文件名,文件后缀名默认可以省略。

代码语言:javascript复制
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

代码语言:javascript复制
// loader/test-loader.js
module.exports = function (source) {
  const options = this.getOptions();
  console.log(options);
  console.log('hello world')
  return source
}

我们可以看到以下options传入的参数

代码语言:javascript复制
  ...
  use: [
          {
            loader: 'test-loader',
            options: {
              name: 'Maic',
               age: 18
             }
          }
   ]

在官方提供了一个简单的例子,主要是用schema-utils验证options传入的数据格式是否正确

安装schema-utils

代码语言:javascript复制
npm i schema-utils --save-dev

test-loader中引入schema-utils

代码语言:javascript复制
// 定义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
}

当我把rulesoptions修改类型时

代码语言:javascript复制
{
  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插件

代码语言:javascript复制
...
{
  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

代码语言:javascript复制
// 引入@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代码

代码语言:javascript复制
const sayhello = () => {
  const str = 'hello world';
  console.log(str)
}
sayhello();

然后在package.json写入打包命令

代码语言:javascript复制
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "start": "webpack server --port=8081",
    "build": "webpack"
  },

我们执行npm run build

test-loadertest-babel-loader都会执行,而且生成的main.js源代码的es6已经被转换成es5了。

写一个自定义markdown-loader

首先我们在loader目录下新建一个markdown-loader.js

代码语言:javascript复制
// markdown-loader.js
module.exports = function (content) {
  console.log(content)
  return content;
}

然后在rules中加入自定义loader

代码语言:javascript复制
  {
      test: /.md$/,
      loader: 'markdown-loader'
  }
  ...

我们需要在src/index.js中引入md文件

代码语言:javascript复制
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]

代码语言:javascript复制
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

代码语言:javascript复制
npm i html-loader --save-dev

然后添加html-loader

代码语言:javascript复制
 {
  test: /.md$/,
  use: ['html-loader', 'markdown-loader']
 }

我们在看下index.js

代码语言:javascript复制
import md from '../doc/index.md';
console.log(md)
const sayhello = () => {
  const str = 'hello world';
  console.log(str)
}
sayhello();

我们在index.js打印引入的md就一段html-loader转换过的最终代码

代码语言:javascript复制
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

0 人点赞