对于webpack,一切皆模块。所以,不管什么文件,都须要转换成js可识别模块。你能够理解为,不管什么后缀的文件,都看成js来使用(即便是img、ppt、txt文件等等)。可是直接看成js使用确定是不行的,需转换为一种能被js理解的方式才能看成js模块来使用——这个转换的过程由webpack的loader来处理。一个webpack loader 是一个导出为函数的 js 模块。webpack内部的
loader runner
会调用这个函数,而后把上一个 loader 产生的结果或者资源文件传入进去,而后返回处理后的结果前端
下面会从基本使用开始出发,探究一个loader怎么写,并实现raw-loader
、json-loader
、url-loader
、bundle-loader
node
准备工做: 先安装webpack
、webpack-cli
、webpack-dev-server
,后面的实践用到什么再装什么react
module.exports = {
module: {
rules: [
{
test: /\.js$/, // 匹配规则
use: ['babel-loader'] // require的loader路径数组
}
]
}
}
复制代码
写了这个规则,只要匹配的文件名以.js
为结尾的,那就会通过use里面全部的loader处理webpack
raw-loader
来获取整个txt文件里面的字符串内容。除了使用统一webpack config配置的方式以外,咱们还能够在引入的时候,用这样的语法来引入:import txt from "raw-loader!./1.txt";
// txt就是这个文件里面全部的内容
复制代码
其实使用webpack.config文件统一配置loader后,最终也是会转成这种方式使用loader再引入的。支持多个loader,语法: loader1!loader2!yourfilename
git
query替代optionsgithub
使用loadername! 前缀语法:raw-loader?a=1&b=2!./1.txt
,等价于webpack配置:web
{
test: /^1\.txt$/,
exclude: /node_modules/,
use: [
{ loader: "raw-loader", options: { a: '1', b: '2' } },
]
},
复制代码
在写本身的loader的时候,常常会使用loader-utils
(不须要特意安装,装了webpack一套就自带)来获取传入参数json
const { getOptions } = require("loader-utils");
module.exports = function(content) {
const options = getOptions(this) || {};
// 若是是配置,返回的是options;若是是loadername!语法,返回根据query字符串生成的对象
// ...
};
复制代码
下文为了方便演示,会屡次使用此方法配置loader。若是没用过这种方法的,就看成入门学习吧😊。搞起~api
一个loader是一个导出为函数的 js 模块,这个函数有三个参数:content, map, meta数组
咱们实现一个最最最简单的,给代码加上一句console的loader:
// console.js
module.exports = function(content, map, meta) {
return `${content}; console.log('loader exec')`;
};
复制代码
webpack配置
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{ loader: "./loaders/console" }, // 加上本身写的loader
]
}
]
},
复制代码
'loader exec'
这两个loader就是读取文件内容,而后可使用import或者require导入原始文件全部的内容。很明显,原文件被看成js使用的时候,缺乏了一个导出语句,loader作的事情就是加上导出语句。
好比有一个这样的txt
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-loader
和json-loader
几乎都是同样的,他们的目的就是把原文件全部的内容做为一个字符串导出,而json-loader多了一个json.parse的过程
注意:看了一下官方的loader源码,发现它们还会多一个步骤
JSON.stringify(content)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
复制代码
\u2028
和\u2029
是特殊字符,和\n
、\b
之类的相似,但它们特殊之处在于——转义后直观上看仍是一个空字符串。能够看见它特殊之处:
即便你看得见中间有一个奇怪的字符,可是你再按下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 | 字节顺序标记 | 空白 |
咱们前面已经实现了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;
复制代码
而后,咱们随便弄一张图片,import进来试一下:
// loader路径自行修改
// img就是一个base64的图片路径,能够直接放img标签使用
import img from "../../loaders/my-url-loader?mimetype=image!./1.png";
复制代码
至于file-loader
,相信你们也有思路了吧,流程就是:读取配置里面的publicpath=>肯定最终输出路径=>文件名称加上MD5 哈希值=>搬运一份文件,文件名改新的名=>新文件名拼接前面的path=>输出最终文件路径
官网对pitching loader介绍是: loader 老是从右到左地被调用。有些状况下,loader 只关心 request 后面的元数据(metadata),而且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 以前,会先从左到右调用 loader 上的 pitch 方法。其次,若是某个 loader 在 pitch 方法中返回一个结果,那么这个过程会跳过剩下的 loader
pitch方法的三个参数:
loader从后往前执行这个过程,你能够视为顺序入栈倒序出栈。好比命中某种规则A的文件,会经历3个loader: ['a-loader', 'b-loader', 'c-loader']
会经历这样的过程:
pitch
方法pitch
方法pitch
方法若是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方法有返回结果,会经历这样的过程:
pitch
方法pitch
方法(有返回结果,跳过c-loader)什么状况下须要跳过剩下的loader呢?最多见的,就是动态加载和缓存读取了,要跳事后面loader的计算。
bundle-loader
是一个典型的例子
bundle-loader
实现的是动态按需加载,怎么使用呢?咱们能够对react最终ReactDom.render那一步改造一下,换成动态加载react-dom
,再体会一下区别
- 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被隔离开来,动态引入
点开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); 复制代码
上文咱们看见有在写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
背景:已有一个api文件api.js
const api0 = {
log(...args) {
console.log("api log>>>", ...args);
}
};
module.exports = api0;
复制代码
但愿效果:咱们使用下面这个a.js
js文件的时候,能够直接使用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
平时也有一些熟悉的场景,某某某api、某某某sdk、公共utils方法、每个index页面的pvuv上报等等,须要先把这些js加载执行完或者导入。若是咱们懒得一个个文件加import/require
语句,就能够用这种方式瞬间完成。这种骚操做的前提是,保证后续同事接手项目难度低、代码无坑。注释、文档、优雅命名都搞起来
loader的做用就是,让一切文件,转化为本身所须要、能使用的js模块运行起来。babel和loader双剑合璧更增强大,能够随心所欲的修改代码、偷懒等等。后续还会出webpack插件、babel相关的文章,你们一块儿来学习交流~
关注公众号《不同的前端》,以不同的视角学习前端,快速成长,一块儿把玩最新的技术、探索各类黑科技