创建你的第一个loader

99 阅读3分钟

简单使用

当一个 loader 在资源中使用,这个 loader 只能传入一个参数 - 这个参数是一个包含包含资源文件内容的字符串。

⚠️ 默认情况下,Webpack 传递给 Loader 的原始内容是一个 UTF-8 格式编码的字符串。但是在某些场景下,加载器处理的不是文本文件,而是二进制文件,官网例子 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据。

同步 loader 可以简单的返回一个代表模块转化后的值。在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么扔进同步 loader 中。loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个参数值是 SourceMap,它是个 JavaScript 对象。

多个loader

⚠️ 当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。

  • 最后的 loader 最早调用,将会传入原始资源内容。
  • 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。
  • 中间的 loader 执行时,会传入前一个 loader 传出的结果。

所以,在接下来的例子,foo-loader 被传入原始资源,bar-loader 将接收 foo-loader 的产出,返回最终转化后的模块和一个 source map(可选)

{                                   ⬆️
  test: /\.js/,                     | 
  use: [                            |
    'bar-loader',                   |            
    'foo-loader'                    |           从下向上 
  ]                                 |
}

设计原则

编写 loader 时应该遵循以下准则。它们按重要程度排序,有些仅适用于某些场景,请阅读下面详细的章节以获得更多信息。

  • 简单易用
  • 使用链式传递。
  • 模块化的输出。
  • 确保无状态
  • 使用 loader utilities
  • 记录 loader 的依赖
  • 解析模块依赖关系
  • 提取通用代码
  • 避免绝对路径
  • 使用 peer dependencies

简单(Simple)

loaders 应该只做单一任务。这不仅使每个 loader 易维护,也可以在更多场景链式调用。

缓存

如果为每个构建重新执行重复的转换操作,这样Webpack构建可能会变得非常慢。

Webpack 默认会缓存所有loader的处理结果,也就是说,当待处理的文件或者依赖的文件没有变化时,不会再次调用对应的loader进行转换操作

module.exports = function (source) {
    // 开始缓存
    this.cacheable && this.cacheable();
    // 在这里按照你的需求处理 source
    return source.replace('word''嘿嘿')
}

一般默认开启缓存,如果不想Webpack这个loader进行缓存,也可以关闭缓存

module.exports = function (source) {
    // 关闭缓存
    this.cacheable(false);
    // 在这里按照你的需求处理 source
    return source.replace('word''嘿嘿')
}

创建项目 loader

Webpack-demo
 ├── dist
 │   └── index.html
 ├── package-lock.json
 ├── package.json
 ├── src
 │   └── index.js
 └── Webpack.config.js

初始化

npm init -y

添加依赖

npm install webpack@4.39.2 webpack-cli@3.3.6 webpack-dev-server@3.11.0 -D

dist/index.html

<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title></title>
    </head>
    <body>
        <script src="./bundle.js"></script>
    </body>
</html>

入口src/index.js

document.write('hello app');

webpack.config.js

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    devServer: {
        contentBase: './dist',
        overlay: {
            warnings: true,
            errors: true,
        },
        open: true,
    },
}

package.json 中配置启动命令

"scripts": {
    "dev": "Webpack-dev-server"
},

创建loader

src/loader/text-loader.js

module.exports = function (source) {
    // 在这里按照你的需求处理 source
    return source.replace('app', '嘿嘿')
}

引用自己的loader

module.exports = {
	//...
    module: {
        rules: [
            {
                test: /\.js$/,
                use: ['text-loader'],
            },
        ],
    },
    resolveLoader: {
        modules: ['node_modules', './src/loader'], // node_modules 找loader,如果找不到就 ./src/loader 找
    },
}

**option参数

module: {
    rules: [
        {
            test: /.js$/,
            use: [
                {
                    loader: 'my-loader',
                    options: {
                        flag: true,
                    },
                },
            ],
        },
    ],
},

如何在loader中获取这个写入配置信息呢?

const loaderUtils = require('loader-utils')
module.exports = function (source) {
    // 获取到用户给当前 Loader 传入的 options
    const options = loaderUtils.getOptions(this)
    console.log('options-->', options)
    // 在这里按照你的需求处理 source
    return source.replace('word', ', 哈哈')
}

实现渲染markdown文档loader

安装依赖: 安装依赖 md 转 html 的依赖,使用的 markdown-it

npm install markdown-it@12.0.6 -D

辅助工具 md.js 用来添加 div和 class

  • markdown-it 处理完成的html 增加类
module.exports = function ModifyStructure(html) {
    // 分块, 把h3和h2开头的切成数组
    const htmlList = html.replace(/<h3/g, '$*(<h3').replace(/<h2/g, '$*(<h2').split('$*(')

    // div套上 .card 类名
    return htmlList
        .map(item => {
            if (item.indexOf('<h3') !== -1) {
                return `<div class="card card-3">${item}</div>`
            } else if (item.indexOf('<h2') !== -1) {
                return `<div class="card card-2">${item}</div>`
            }
            return item
        })
        .join('')
}

创建loader

  • /src/myLoader/md-loader.js
const { getOptions } = require('loader-utils')
const MarkdownIt = require('markdown-it')
const beautify = require('./md')

module.exports = function (source) {
    const options = getOptions(this) || {}
    const md = new MarkdownIt({
        html: true,
        ...options,
    })
    // 这里解析的结果是一个 HTML 字符串。如果直接返回,也会面临Webpack无法解析模块的问题。
    // 正确的做法是把这个HTML字符串拼接成一段JS代码。通过module.exports导出这个HTML字符串,
    let html = beautify(md.render(source))
    // 这样外界在导入模块的时候就可以接收到这个HTML字符串。
    console.log("🚀 ~ file: md-trans-loader.js ~ line 16 ~ html", JSON.stringify(html));
    html = `module.exports = ${JSON.stringify(html)}`
    this.callback(null, html)
}

使用md-trans-loader

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    module: {
        rules: [
            {
                test:/.md$/,
                use:[
                    {
                        loader: 'md-trans-loader'
                    }
                ]
            },
            {
                test: /\.js$/,
                use: [
                    {
                        loader: 'text-trans-loader',
                        options: {
                            测试: true,
                        },
                    },
                ],
            }
       
        ],
    },
    resolveLoader: {
        modules: ['node_modules', './src/loader'], // node_modules 找loader,如果找不到就 ./src/loader 找
    },

    devServer: {
        contentBase: './dist',
        overlay: {
            warnings: true,
            errors: true,
        },
        open: true,
    },
};

选取一个md文件测试渲染

  • index.js 引入home.md文件
document.write('hello world');

import mdHtml from './home.md'
const content = document.createElement('div')
content.className = 'content'
content.innerHTML = mdHtml
document.body.appendChild(content)