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

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

webpackloader本质上是一个导出的函数,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已经触发了。

image.png

但是官方提供另外一种方式


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
}

当我把rulesoptions修改类型时

{
  use: [
      {
        loader: 'test-loader',
        options: {
          name: 'Maic',
          age: '18'
        }
      }
  ]
}

运行npm run start

7b0c68a8726fc42dd5a62385fb943e5c.png

直接提示报错了,相当于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

920572bbaf94cc3ddd1a26de08cc32da.png

79acd17472addfcbe43a2067c3fbebd4.png

test-loadertest-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

2be11335f6093f989f087264d2ba18cf.png

已经获取到了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

5ea9081b0fa6b537bf9b164b9c7b7d7e.png

此时依然报错,错误提示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']
 }

37338188383bfaf91bc88d9c92e34db9.png

我们在看下index.js

import md from '../doc/index.md';
console.log(md)
const sayhello = () => {
  const str = 'hello world';
  console.log(str)
}
sayhello();

96c8890f20f8cc2f0259b97f030a5f5b.png

我们在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();

da7b6b344089f8ce89bd5e585d0cc70a.png

我么最终就看到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月前
|
前端开发 Java 开发者
class loader
【8月更文挑战第4】
40 7
|
4月前
|
前端开发 JavaScript
常见loader
常见loader:
57 0
|
10月前
|
XML Java PHP
AS3 Loader与URLLoader的区别与比较
AS3 Loader与URLLoader的区别与比较
36 0
|
Go API C++
实现第一个shellcod Loader
上一篇文章说了,我们说到了什么是shellcode,为了使我们的shellcode加载到内存并执行,我们需要shellcode加载器,也就是我们的shellcode loader。当然编写Loader不止局限于C/C++,你也可以使用Python、Golang等语言编写加载器
202 0
|
缓存 前端开发 JavaScript
【前端】style-loader和MiniCssExtractPlugin.loader
【前端】style-loader和MiniCssExtractPlugin.loader
189 0
|
前端开发 开发者
loader - 配置处理 less 文件的 loader| 学习笔记
快速学习 loader - 配置处理 less 文件的 loader
loader - 配置处理 less 文件的 loader| 学习笔记
|
前端开发 开发者
loader - 配置处理 scss 文件的 loader|学习笔记
快速学习 loader - 配置处理 scss 文件的 loader
loader - 配置处理 scss 文件的 loader|学习笔记
|
JavaScript 前端开发 开发者
loader - 分析 webpack 调用第三方 loader 的过程| 学习笔记
快速学习 loader - 分析 webpack 调用第三方 loader 的过程
loader  - 分析 webpack 调用第三方 loader 的过程| 学习笔记
|
资源调度 JavaScript
5、图片的加载(url-loader)
5、图片的加载(url-loader)
88 0
|
前端开发 JavaScript 开发者
loader - 配置处理 css 样式表的第三方 loader| 学习笔记
快速学习 loader - 配置处理 css 样式表的第三方 loader