百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

手把手教你在Webpack写一个Loader

myzbx 2025-07-03 18:18 49 浏览

前言

有的时候,你可能在从零搭建 Webpack 项目很熟悉,配置过各种 loader ,面试官在 Webpack 方面问你,是否自己实现过一个loader?如果没有去了解过如果去实现,确实有点尴尬,其实呢,loader实现其实很简单的。下面说下loader是什么?

为什么需要Loader?

Webpack 它只能处理 js 和 JSON 文件。面对 css 文件还有一些图片等等,Webpack 它自己是不能够处理的,它需要loader 处理其他类型的文件并将它们转换为有效的模块以供应用程序使用并添加到依赖关系图中,

Loader是什么?

loader本质上是一个node模块,符合Webpack中一切皆模块的思想。由于它是一个 node 模块,它必须导出一些东西。loader本身就是一个函数,在该函数中对接收到的内容进行转换,然后返回转换后的结果

下面小浪为你简单介绍下webpack中的loader

常见的loader

我们先来回顾下常见的 Loader 基础的配置和使用吧(仅仅只是常见的,npm上面开发者大佬们发布的太多了)

那么开始吧,首先先介绍 处理 CSS 相关的 Loader

css-loader 和 style-loader

安装依赖

npm install css-loader style-loader
复制代码

使用加载器

module.exports = {
    // ...
    module: {
        rules: [{
            test: /.css$/,
            use: ['style-loader', 'css-loader'],
        }],
    },
};
复制代码

其中module.rules代表模块的处理规则。每个规则可以包含很多配置项

test 可以接收正则表达式或元素为正则表达式的数组。只有与正则表达式匹配的模块才会使用此规则。在此示例中,/.css$/ 匹配所有以 .css 结尾的文件。

use 可以接收一个包含规则使用的加载器的数组。如果只配置了一个css-loader,当只有一个loader时也可以为字符串

css-loader 的作用只是处理 CSS 的各种加载语法(@importurl() 函数等),如果样式要工作,则需要 style-loader 将样式插入页面

style-loader加到了css-loader前面,这是因为在Webpack打包时是按照数组从后往前的顺序将资源交给loader处理的,因此要把最后生效的放在前面

还可以这样写成对象的形式,里面options传入配置

module.exports = {
    // ...
    module: {
        rules: [{
            test: /.css$/,
            use: [
                'style-loader',
                  {
                    loader: 'css-loader',
                    options: {
                        // css-loader 配置项
                 },
               }
            ],
        }],
    },
};
复制代码

excludeinclude

include代表该规则只对正则匹配到的模块生效

exclude的含义是,所有被正则匹配到的模块都排除在该规则之外

rules: [
    {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
        exclude: /node_modules/,
        include: /src/,
    }
],

复制代码

是否都还记得呢,现在有现成的脚手架,很多人都很少自己去配置这些了,欸~当然还有相关的 sass/less等等预处理器loader这里就不一一介绍了。

babel-loader

babel-loader 这个loader十分的重要,把高级语法转为ES5,常用于处理 ES6+ 并将其编译为 ES5。它允许我们在项目中使用最新的语言特性(甚至在提案中),而无需特别注意这些特性在不同平台上的兼容性。

介绍下主要的三个模块

  • babel-loader:使 BabelWebpack 一起工作的模块
  • @babel/core:Babel核心模块。
  • @babel/preset-env:是Babel官方推荐的preseter,可以根据用户设置的目标环境,自动添加编译ES6+代码所需的插件和补丁

安装

npm install babel-loader @babel/core @babel/preset-env
复制代码

配置

rules: [
  {
    test: /.js$/,
    exclude: /node_modules/, //排除掉,不排除拖慢打包的速度
    use: {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true, // 启用缓存机制以防止在重新打包未更改的模块时进行二次编译
        presets: [[
          'env', {
            modules: false, // 将ES6 Module的语法交给Webpack本身处理
          }
        ]],
      },
    },
  }
],
复制代码

html-loader

Webpack 可不认识 html,直接报错,需要loader转化

html-loader 用于将 HTML 文件转换为字符串并进行格式化,它允许我们通过 JS 加载一个 HTML 片段。

安装

npm install html-loader
复制代码

配置

rules: [
    {
        test: /.html$/,
        use: 'html-loader',
    }
],
复制代码
// index.js
import otherHtml from './other.html';
document.write(otherHtml);
复制代码

这样你可以在js中加载另一个页面,写刀当前index.html里面

file-loader

用于打包文件类型的资源,比如对pngjpggif等图片资源使用file-loader,然后就可以在JS中加载图片了

安装

npm install file-loader
复制代码

配置

const path = require('path');
module.exports = {
    entry: './index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    module: {
        rules: [
            {
                test: /.(png|jpg|gif)$/,
                use: 'file-loader',
            }
        ],
    },
};
复制代码

url-loader

既然介绍了 file-loader 就不得不介绍 url-loader,它们很相似,但是唯一的区别是用户可以设置文件大小阈值。大于阈值时返回与file-loader相同的publicPath,小于阈值时返回文件base64编码。

安装

npm install url-loader
复制代码

配置

rules: [
    {
        test: /.(png|jpg|gif)$/,
        use: {
            loader: 'url-loader',
            options: {
                limit: 1024,
                name: '[name].[ext]',
                publicPath: './assets/',
            },
        },
    }
],
复制代码

ts-loader

TypeScript使用得越来越多,对于我们平时写代码有了更好的规范,项目更加利于维护...等等好处,我们也在Webpack中来配置loader,本质上类似于 babel-loader,是一个连接 WebpackTypescript 的模块

安装

npm install ts-loader typescript
复制代码

loader配置,主要的配置还是在 tsconfig.json

rules: [
    {
        test: /.ts$/,
        use: 'ts-loader',
    }
],
复制代码

vue-loader

用来处理vue组件,还要安装vue-template-compiler来编译Vue模板,估计大家大部分都用脚手架了

安装

npm install vue-loader  vue-template-compiler 
复制代码
rules: [
    {
        test: /.vue$/,
        use: 'vue-loader',
    }
],
复制代码

写一个简单的Loader

介绍了几个常见的loader的安装配置,我们在具体的业务的实现的时候,可能遇到各种需求,上面介绍的或者npm上都没有的加载器都不适合当前的业务场景,那我们可以自己去实现一个自己的loader来满足自己的需求,小浪下面介绍一下如何自定义一个loader

1.初始化项目

初始化项目

先创建一个项目文件夹(名字可以随意,当然肯定是英文名)后进行初始化

npm init -y
复制代码

安装依赖

安装依赖:WebpackWebpack脚手架 和 热更新服务器

不同的版本 Webpack 可能有些差异,如果你跟着我的这个例子写的话,小浪建议和我装一样的版本

npm install webpack@4.39.2 webpack-cli@3.3.6 webpack-dev-server@3.11.0 -D
复制代码

新建一个index.html文件

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>

复制代码

新建一个入口文件 index.js 文件

src/index.js

document.write('hello world')
复制代码

创建 webpack.config.js 配置文件

配置出口和入口文件

配置devServer服务

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"
  },
复制代码

启动 npm run dev

devServer帮我们启动一个服务器,每次修改index.js不需要自己在去打包,而是自动帮我们完成这项任务

页面内容就是我们index.js编写的内容被打包成在dist/bundle.js引入到index.html

image.png

当前的文件目录

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

2.实现一个简单的 loader

src/MyLoader/my-loader.js

module.exports = function (source) {
    // 在这里按照你的需求处理 source
    return source.replace('word', ', I am Xiaolang')
}
复制代码

返回其它结果 this.callback

this.callback(    
    // 当无法转换原内容时,给 Webpack 返回一个 Error   
    err: Error | null,    
    // 原内容转换后的内容    
    content: string | Buffer,    
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,    
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能 
    abstractSyntaxTree?: AST
);
复制代码

打开代码对应的source-map,方便调试源代码。source-map 可以方便实际开发者在浏览器控制台查看源代码。如果不处理source-map,最终将无法生成正确的map文件,在浏览器的开发工具中可能会看到混乱的源代码。

为了在使用 this.callback 返回内容时将 source-map 返回给 Webpack

loader 必须返回 undefinedWebpack 知道 loader 返回的结果在 this.callback 中,而不是在 return

module.exports = function(source) { 
    // 通过 this.callback 告诉 Webpack 返回的结果
    this.callback(null, source.replace('word', ', I am Xiaolang'), sourceMaps);   
    return;
};
复制代码

常用加载本地 loader 两种方式

1.path.resolve

使用 path.resolve 指向这个本地文件

const path = require('path')

module.exports = {
    module: {
        rules: [
            {
                test: /.js$/,
                use: path.resolve('./src/myLoader/my-loader.js'),
            },
        ],
    },
}

复制代码

2.ResolveLoader

先去 node_modules 项目下寻找 my-loader,如果找不到,会再去 ./src/myLoader/ 目录下寻找。


module.exports = {
 //...
    module: {
        rules: [
            {
                test: /.js$/,
                use: ['my-loader'],
            },
        ],
    },
    resolveLoader: {
        modules: ['node_modules', './src/myLoader'],
    },
}

复制代码

一个 loader的职责是单一的,使每个loader易维护。

如果源文件需要分多步转换才能正常使用,通过多个Loader进行转换。当调用多个loader进行文件转换时,每个loader都会链式执行。

第一个loader会得到要处理的原始内容,将前一个loader处理的结果传递给下一个。处理完毕,最终的Loader会将处理后的最终结果返回给 Webpack

所以,当你写loader记得保持它的职责单一,你只关心输入和输出。

image-20220522142823507

3.option参数

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

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

Webpack 提供了loader-utils工具

在之前写的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', ', I am Xiaolang')
}
复制代码

控制台也打印了出来

image-20220522143828316

4.缓存

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

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

module.exports = function (source) {
    // 开始缓存
    this.cacheable && this.cacheable();
    // 在这里按照你的需求处理 source
    return source.replace('word', ', I am Xiaolang')
}
复制代码

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

module.exports = function (source) {
    // 关闭缓存
    this.cacheable(false);
    // 在这里按照你的需求处理 source
    return source.replace('word', ', I am Xiaolang')
}
复制代码

5.同步与异步

在某些情况下,转换步骤只能异步完成。

例如,您需要发出网络请求以获取结果。如果使用同步方式,网络请求会阻塞整个构建,导致构建非常缓慢。

module.exports = function(source) {    
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    var callback = this.async()
    // someAsyncOperation 代表一些异步的方法
    someAsyncOperation(source, function (err, result, sourceMaps, ast) {
        // 通过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast)
    })
};
复制代码

6.处理二进制数据

默认情况下,Webpack 传递给 Loader 的原始内容是一个 UTF-8 格式编码的字符串。但是在某些场景下,加载器处理的不是文本文件,而是二进制文件

官网例子 通过 exports.raw 属性告诉 WebpackLoader 是否需要二进制数据

module.exports = function(source) {    
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的    
    source instanceof Buffer === true;    
    // Loader 返回的类型也可以是 Buffer 类型的    
    // 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果    
    return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据 
module.exports.raw = true;
复制代码

7.实现一个渲染markdown文档loader

安装依赖 mdhtml 的依赖,当然可以选择另外一个模块 marked

我这里使用的 markdown-it

npm install markdown-it@12.0.6 -D 
复制代码

辅助工具 用来添加 divclass

module.exports = function ModifyStructure(html) {
    // 把h3和h2开头的切成数组
    const htmlList = html.replace(/<h3/g, '$*(<h3').replace(/<h2/g, '$*(<h2').split('$*(')

    // 给他们套上 .card 类名的 div
    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('./beautify')
module.exports = function (source) {
    const options = getOptions(this) || {}
    const md = new MarkdownIt({
        html: true,
        ...options,
    })
    let html = beautify(md.render(source))
    html = `module.exports = ${JSON.stringify(html)}`
    this.callback(null, html)
}
复制代码

这样loader也写完了,this.callback(null, html)return 在这里差不多哈。

html = `module.exports = ${JSON.stringify(html)}`
复制代码

这里解析的结果是一个 HTML 字符串。如果直接返回,也会面临Webpack无法解析模块的问题。正确的做法是把这个HTML字符串拼接成一段JS代码。

这时候我们要返回的代码就是通过module.exports导出这个HTML字符串,这样外界在导入模块的时候就可以接收到这个HTML字符串。

然后在webpack.config.js使用这个加载器

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: [
                    {
                        loader: 'my-loader',
                        options: {
                            flag: true,
                        },
                    },
                ],
            },
            {
                test: /.md$/,
                use: [
                    {
                        loader: 'md-loader',
                    },
                ],
            },
        ],
    },
    resolveLoader: {
        modules: ['node_modules', './src/myLoader'],
    },
    devServer: {
        contentBase: './dist',
        overlay: {
            warnings: true,
            errors: true,
        },
        open: true,
    },
}

复制代码

使用

最后在index.js中加载一个md文件,我这里随便整个,新建githubreadme.md

document.write('hello word')

import mdHtml from './test.md'
const content = document.createElement('div')
content.className = 'content'
content.innerHTML = mdHtml
document.body.appendChild(content)
复制代码

结果图

image-20220522165928553

目录结构

Webpack-demo
 ├── dist
 │   └── index.html
 ├── package-lock.json
 ├── package.json
 ├── src
 │   ├── index.js
 │   ├── myLoader
 │   │   ├── beautify.js
 │   │   ├── md-loader.js
 │   │   └── my-loader.js
 │   └── test.md
 └── webpack.config.js
复制代码

github仓库地址[1]

结语

感谢大家能看到这里哈~ ,现在打包构建工具也慢慢增多了vue-clivite等等,但是 webpack 仍然有一席之地,很多值得学习的地方,继续努力学习~~

来源:小浪努力学前端,
https://juejin.cn/post/7100534685134454815

同时收录于小程序-互联网小兵,涵盖前端、后端、移动端、算法、人工智能等领域的优秀技术文章技术人小程序,希望支持支持!

相关推荐

如何设计一个优秀的电子商务产品详情页

加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...

怎么在JS中使用Ajax进行异步请求?

大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...

中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革

前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...

前端监控 SDK 开发分享_前端监控系统 开源

一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...

Ajax 会被 fetch 取代吗?Axios 怎么办?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...

前端面试题《AJAX》_前端面试ajax考点汇总

1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...

Ajax 详细介绍_ajax

1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...

6款可替代dreamweaver的工具_替代powerdesigner的工具

dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...

我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊

接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...

福斯《死侍》发布新剧照 &quot;小贱贱&quot;韦德被改造前造型曝光

时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...

2021年超详细的java学习路线总结—纯干货分享

本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...

不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!

Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...

2025 年 Python 爬虫四大前沿技术:从异步到 AI

作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...

最贱超级英雄《死侍》来了!_死侍超燃

死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...

停止javascript的ajax请求,取消axios请求,取消reactfetch请求

一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...