1. 首页
  2. webpack

看完这篇webpack-loader,不再怕面试官问了

看完这篇webpack-loader,不再怕面试官问了

对于webpack,一切皆模块。因此,无论什么文件,都需要转换成js可识别模块。你可以理解为,无论什么后缀的文件,都当作js来使用(即使是img、ppt、txt文件等等)。但是直接当作js使用肯定是不行的,需转换为一种能被js理解的方式才能当作js模块来使用——这个转换的过程由webpack的loader来处理。一个webpack loader 是一个导出为函数的 js 模块。webpack内部的loader runner会调用这个函数,然后把上一个 loader 产生的结果或者资源文件传入进去,然后返回处理后的结果

下面会从基本使用开始出发,探究一个loader怎么写,并实现raw-loaderjson-loaderurl-loaderbundle-loader

准备工作: 先安装webpackwebpack-cliwebpack-dev-server,后面的实践用到什么再装什么

loader使用

  1. 常规方法:webpack.config里面配置rules

module.exports = { module: { rules: [ { test: /\.js$/, // 匹配规则 use: ['babel-loader'] // require的loader路径数组 } ] } } 复制代码

写了这个规则,只要匹配的文件名以.js为结尾的,那就会经过use里面所有的loader处理

  1. loadername! 前缀方式 比如有一个txt文件,我们想通过raw-loader来获取整个txt文件里面的字符串内容。除了使用统一webpack config配置的方式之外,我们还可以在引入的时候,用这样的语法来引入:

import txt from "raw-loader!./1.txt"; // txt就是这个文件里面所有的内容 复制代码

其实使用webpack.config文件统一配置loader后,最终也是会转成这种方式使用loader再引入的。支持多个loader,语法: loader1!loader2!yourfilename

query替代options

使用loadername! 前缀语法:raw-loader?a=1&b=2!./1.txt,等价于webpack配置:


{ test: /^1\.txt$/, exclude: /node_modules/, use: [ { loader: "raw-loader", options: { a: '1', b: '2' } }, ] }, 复制代码

在写自己的loader的时候,经常会使用loader-utils(不需要特地安装,装了webpack一套就自带)来获取传入参数


const { getOptions } = require("loader-utils"); module.exports = function(content) { const options = getOptions(this) || {}; // 如果是配置,返回的是options;如果是loadername!语法,返回根据query字符串生成的对象 // ... }; 复制代码

下文为了方便演示,会多次使用此方法配置loader。如果没用过这种方法的,就当作入门学习吧😊。搞起~

一个loader一般是怎样的

一个loader是一个导出为函数的 js 模块,这个函数有三个参数:content, map, meta

  • content: 表示源文件字符串或者buffer
  • map: 表示sourcemap对象
  • meta: 表示元数据,辅助对象

我们实现一个最最最简单的,给代码加上一句console的loader:

// console.js
module.exports = function(content, map, meta) {
  return `${content}; console.log('loader exec')`;
};
复制代码

```javascript 

webpack配置


module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: [
{ loader: “./loaders/console” }, // 加上自己写的loader
]
}
]
},
复制代码

![](https://static.javascriptc.com/frontend/webpack/20200408/webpack-188189ff0888189ff0.webp)

我们发现,重新跑构建后,每一个js都打印一下`'loader exec'`

# 最简单的loader——raw-loader和json-loader

这两个loader就是读取文件内容,然后可以使用import或者require导入原始文件所有的内容。很明显,原文件被当作js使用的时候,缺少了一个导出语句,loader做的事情就是加上导出语句。

比如有一个这样的txt


```javascript 

this is a txt file
复制代码

假如你把它当作js来用,import或者require进来的时候,执行this is a txt file这句js,肯定会报错。如果想正常使用,那么这个txt文件需要改成:


export default 'this is a txt file' 复制代码

最终的效果就是,无论是什么文件,txt、md、json等等,都当作一个js文件来用,原文件内容相当于一个字符串,被导出了:

// 自己写的raw-loader
const { getOptions } = require("loader-utils");
// 获取webpack配置的options,写loader的固定套路第一步

module.exports = function(content, map, meta) {
  const opts = getOptions(this) || {};

  const code = JSON.stringify(content);
  const isESM = typeof opts.esModule !== "undefined" ? options.esModule : true;
// 直接返回原文件内容
  return `${isESM ? "export default" : "module.exports ="} ${code}`;
};
复制代码

raw-loaderjson-loader几乎都是一样的,他们的目的就是把原文件所有的内容作为一个字符串导出,而json-loader多了一个json.parse的过程

注意:看了一下官方的loader源码,发现它们还会多一个步骤


JSON.stringify(content) .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029'); 复制代码

\u2028\u2029是特殊字符,和\n\b之类的类似,但它们特殊之处在于——转义后直观上看还是一个空字符串。可以看见它特殊之处:

看完这篇webpack-loader,不再怕面试官问了

即使你看得见中间有一个奇怪的字符,但是你再按下enter,还是'ab'\u2028字符串在直观上来看相当于空字符串(实际上字符是存在的,却没有它的带来的效果)。而对于除了2028和2029,比如\u000A\n,是有换行的效果的(字符存在,也有它带来的效果)。因此,对于低概率出现的字符值为2028和2029的转义是有必要的

Unicode 字符值

转义序列

含义

类别

\u0008

\b

Backspace

\u0009

\t

Tab

空白

\u000A

\n

换行符(换行)

行结束符

\u000B

\v

垂直制表符

空白

\u000C

\f

换页

空白

\u000D

\r

回车

行结束符

\u0022

双引号 (“)

\u0027

\‘

单引号 (‘)

\u005C

\

反斜杠 ()

\u00A0

不间断空格

空白

\u2028

行分隔符

行结束符

\u2029

段落分隔符

行结束符

\uFEFF

字节顺序标记

空白

看完这篇webpack-loader,不再怕面试官问了

raw模式与url-loader

我们前面已经实现了raw-loader,这个loader是把原文件里面的内容以字符串形式返回。但是问题来了,有的文件并不是一个字符串就可以解决的了的,比如图片、视频、音频。此时,我们需要直接利用原文件的buffer。恰好,loader函数的第一个参数content,支持string/buffer

如何开启buffer类型的content?


// 只需要导出raw为true module.exports.raw = true 复制代码

url-loader的流程就是,读取配置,是否可以转、怎么转=>读取原文件buffer=>buffer转base64输出 => 无法转换的走fallback流程。我们下面实现一个简易版本的url-loader,仅仅实现核心功能

const { getOptions } = require("loader-utils");

module.exports = function(content) {
  const options = getOptions(this) || {};
  const mimetype = options.mimetype;

  const esModule =
    typeof options.esModule !== "undefined" ? options.esModule : true;

// base编码组成:data:[mime类型];base64,[文件编码后内容]
  return `${esModule ? "export default" : "module.exports ="} ${JSON.stringify(
    `data:${mimetype || ""};base64,${content.toString("base64")}`
  )}`;
};

module.exports.raw = true;
复制代码

```javascript 

然后,我们随便弄一张图片,import进来试一下:


// loader路径自行修改
// img就是一个base64的图片路径,可以直接放img标签使用
import img from “../../loaders/my-url-loader?mimetype=image!./1.png”;
复制代码

至于`file-loader`,相信大家也有思路了吧,流程就是:读取配置里面的publicpath=>确定最终输出路径=>文件名称加上MD5 哈希值=>搬运一份文件,文件名改新的名=>新文件名拼接前面的path=>输出最终文件路径

# pitch与bundle-loader

[官网对pitching loader介绍][1]是: loader 总是从右到左地被调用。有些情况下,loader 只关心 request 后面的元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先从左到右调用 loader 上的 pitch 方法。**其次,如果某个 loader 在 pitch 方法中返回一个结果,那么这个过程会跳过剩下的 loader**

pitch方法的三个参数:

+   remainingRequest: 后面的loader+资源路径,loadername!的语法
+   precedingRequest: 资源路径
+   metadata: 和普通的loader函数的第三个参数一样,辅助对象,而且loader执行的全程用的是同一个对象哦

loader从后往前执行这个过程,你可以视为顺序入栈倒序出栈。比如命中某种规则A的文件,会经历3个loader: `['a-loader', 'b-loader', 'c-loader']`

会经历这样的过程:

+   执行a-loader的`pitch`方法
+   执行b-loader `pitch`方法
+   执行c-loader `pitch`方法
+   根据import/require路径获取资源内容
+   c-loader 执行
+   b-loader 执行
+   a-loader 执行

如果`b-loader`里面有一个pitch方法,而且这个pitch方法有返回结果,那么上面这个过程自从经过了`b-loader`后,就不会再将`c-loader`入栈

// b-loader
module.exports = function(content) {
return content;
};

// 没做什么,就透传import进来再export出去
module.exports.pitch = function(remainingRequest) {
// remainingRequest路径要加-! 前缀
return import s from ${JSON.stringify(-!${remainingRequest})}; export default s;
};
复制代码

b-loader的pitch方法有返回结果,会经历这样的过程:

+   执行a-loader的`pitch`方法
+   执行b-loader `pitch`方法(有返回结果,跳过c-loader)
+   根据import/require路径获取资源内容
+   b-loader 执行
+   a-loader 执行

> 什么情况下需要跳过剩下的loader呢?最常见的,就是动态加载和缓存读取了,要跳过后面loader的计算。`bundle-loader`是一个典型的例子

`bundle-loader`实现的是动态按需加载,怎么使用呢?我们可以对react最终ReactDom.render那一步改造一下,换成动态加载`react-dom`,再体会一下区别


```javascript 

- import ReactDom from "react-dom";
+ import LazyReactDom from "bundle-loader?lazy&name=reactDom!react-dom";

+ LazyReactDom(ReactDom => {
+   console.log(ReactDom, "ReactDom");
ReactDom.render(<S />, document.getElementById("root"));
+});
复制代码

可以看见reactdom被隔离开来,动态引入

看完这篇webpack-loader,不再怕面试官问了

点开bundle-loader源码,发现它利用的是require.ensure来动态引入,具体的实现也很简单,具体看bundle-loader源码。时代在变化,新时代的动态引入应该是动态import,下面我们自己基于动态import来实现一个新的bundle-loader。(仅实现lazy引入的核心功能)

// 获取ChunkName
function getChunkNameFromRemainingRequest(r) {
  const paths = r.split("/");
  let cursor = paths.length - 1;
  if (/^index\./.test(paths[cursor])) {
    cursor--;
  }
  return paths[cursor];
}

// 原loader不需要做什么了
module.exports = function() {};

module.exports.pitch = function(remainingRequest, r) {
  // 带loadername!前缀的依赖路径
  const s = JSON.stringify(`-!${remainingRequest}`);
  // 使用注释webpackChunkName来定义chunkname的语法
  return `export default function(cb) {
  return cb(import(/* webpackChunkName: "my-lazy-${getChunkNameFromRemainingRequest(
    this.resource
  )}" */${s}));
}`;
};

复制代码

用法和官方的bundle-loader基本差不多,只是动态import返回一个promise,需要改一下使用方法:


import LazyReactDom from "../loaders/my-bundle!react-dom"; setTimeout(() => { LazyReactDom(r => { r.then(({ default: ReactDom }) => { ReactDom.render(<S />, document.getElementById("root")); }); }); }, 1000); 复制代码

看完这篇webpack-loader,不再怕面试官问了

loader上下文

上文我们看见有在写loader的时候使用this,这个this就是loader的上下文。具体可见官网

一堆上下文的属性中,我们拿其中一个来实践一下: this.loadModule

loadModule(request: string, callback: function(err, source, sourceMap, module))

loadModule方法作用是,解析给定的 request 到一个模块,应用所有配置的 loader ,并且在回调函数中传入生成的 source 、sourceMap和webpack内部的NormalModule实例。如果你需要获取其他模块的源代码来生成结果的话,你可以使用这个函数。

很明显,这个方法其中一个应用场景就是,在已有代码上注入其他依赖

let’s coding

看完这篇webpack-loader,不再怕面试官问了

背景:已有一个api文件api.js


const api0 = { log(...args) { console.log("api log>>>", ...args); } }; module.exports = api0; 复制代码

希望效果:我们使用下面这个a.jsjs文件的时候,可以直接使用api,且不报错


// a.js export default function a() { return 1; } // 其他代码 // ... api.log("a", "b"); 复制代码

因此,我们需要构建的时候loader把api打进去我们的代码里面:

// addapi的loader
module.exports = function(content, map, meta) {
// 涉及到加载模块,异步loader
  const callback = this.async();
  this.loadModule("../src/api.js", (err, source, sourceMap, module) => {
// source是一个module.exports = require(xxx)的字符串,我们需要require那部分
    callback(
      null,
      `const api = ${source.split("=")[1]};
${content};`,
      sourceMap,
      meta
    );
  });
  return;
};
复制代码

loader写好了,记得去webpack配置里面加上,或者使用loadername!的语法引入a.js(./loaders/addapi!./a.js)

最后我们可以看见成功运行了api.js的log

看完这篇webpack-loader,不再怕面试官问了

平时也有一些熟悉的场景,某某某api、某某某sdk、公共utils方法、每一个index页面的pvuv上报等等,需要先把这些js加载执行完或者导入。如果我们懒得一个个文件加import/require语句,就可以用这种方式瞬间完成。这种骚操作的前提是,保证后续同事接手项目难度低、代码无坑。注释、文档、优雅命名都搞起来

最后

loader的作用就是,让一切文件,转化为自己所需要、能使用的js模块运行起来。babel和loader双剑合璧更加强大,可以为所欲为的修改代码、偷懒等等。后续还会出webpack插件、babel相关的文章,大家一起来学习交流~

关注公众号《不一样的前端》,以不一样的视角学习前端,快速成长,一起把玩最新的技术、探索各种黑科技

看完这篇webpack-loader,不再怕面试官问了

作者:lhyt
链接:https://juejin.im/post/5e3389436fb9a02fef3a707a

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「IT平头哥联盟」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:看完这篇webpack-loader,不再怕面试官问了

链接:https://www.javascriptc.com/3948.html

« React hook + Typescript +Redux +AntD搭一个admin后台管理系统
JavasScript中的异步和事件轮询详解»
Flutter 中文教程资源

相关推荐

QR code