maicFir / lessonNote

JS学习笔记
33 stars 11 forks source link

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

Open maicFir opened 2 years ago

maicFir commented 2 years ago

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标签,然后再渲染到页面上了

总结