Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
maicFir opened this issue Aug 11, 2022 · 0 comments
Open

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

maicFir opened this issue Aug 11, 2022 · 0 comments

Comments

@maicFir
Copy link
Owner

maicFir commented Aug 11, 2022

webpackloader本质上是一个导出的函数,loader runner会调用该函数,在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

我们在新建一个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

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

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

已经获取到了doc/index.md的内容了

在 loader 中我需要解析md的内容,此时我们需要借助一个第三方的md解析器marked

npm i marked --save-dev

详细使用文档参考markedjs

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant