下面会从基本使用开始出发,探究一个loader怎么写,并实现raw-loader、json-loader、url-loader、bundle-loader
准备工作:先安装webpack、webpack-cli、webpack-dev-server,后面的实践用到什么再装什么
importtxtfrom"raw-loader!./1.txt";//txt就是这个文件里面所有的内容其实使用webpack.config文件统一配置loader后,最终也是会转成这种方式使用loader再引入的。支持多个loader,语法:loader1!loader2!yourfilename
query替代options
使用loadername!前缀语法:raw-loadera=1&b=2!./1.txt,等价于webpack配置:
const{getOptions}=require("loader-utils");module.exports=function(content){constoptions=getOptions(this)||{};//如果是配置,返回的是options;如果是loadername!语法,返回根据query字符串生成的对象//...};复制代码下文为了方便演示,会多次使用此方法配置loader。如果没用过这种方法的,就当作入门学习吧。搞起~
一个loader是一个导出为函数的js模块,这个函数有三个参数:content,map,meta
我们实现一个最最最简单的,给代码加上一句console的loader:
这两个loader就是读取文件内容,然后可以使用import或者require导入原始文件所有的内容。很明显,原文件被当作js使用的时候,缺少了一个导出语句,loader做的事情就是加上导出语句。
比如有一个这样的txt
thisisatxtfile假如你把它当作js来用,import或者require进来的时候,执行thisisatxtfile这句js,肯定会报错。如果想正常使用,那么这个txt文件需要改成:
//自己写的raw-loaderconst{getOptions}=require("loader-utils");//获取webpack配置的options,写loader的固定套路第一步module.exports=function(content,map,meta){constopts=getOptions(this)||{};constcode=JSON.stringify(content);constisESM=typeofopts.esModule!=="undefined"options.esModule:true;//直接返回原文件内容return`${isESM"exportdefault":"module.exports="}${code}`;};复制代码raw-loader和json-loader几乎都是一样的,他们的目的就是把原文件所有的内容作为一个字符串导出,而json-loader多了一个json.parse的过程
注意:看了一下官方的loader源码,发现它们还会多一个步骤
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为truemodule.exports.raw=trueurl-loader的流程就是,读取配置,是否可以转、怎么转=>读取原文件buffer=>buffer转base64输出=>无法转换的走fallback流程。我们下面实现一个简易版本的url-loader,仅仅实现核心功能
const{getOptions}=require("loader-utils");module.exports=function(content){constoptions=getOptions(this)||{};constmimetype=options.mimetype;constesModule=typeofoptions.esModule!=="undefined"options.esModule:true;//base编码组成:data:[mime类型];base64,[文件编码后内容]return`${esModule"exportdefault":"module.exports="}${JSON.stringify(`data:${mimetype||""};base64,${content.toString("base64")}`)}`;};module.exports.raw=true;复制代码然后,我们随便弄一张图片,import进来试一下:
//loader路径自行修改//img就是一个base64的图片路径,可以直接放img标签使用importimgfrom"../../loaders/my-url-loadermimetype=image!./1.png";至于file-loader,相信大家也有思路了吧,流程就是:读取配置里面的publicpath=>确定最终输出路径=>文件名称加上MD5哈希值=>搬运一份文件,文件名改新的名=>新文件名拼接前面的path=>输出最终文件路径
pitch方法的三个参数:
会经历这样的过程:
如果b-loader里面有一个pitch方法,而且这个pitch方法有返回结果,那么上面这个过程自从经过了b-loader后,就不会再将c-loader入栈
//b-loadermodule.exports=function(content){returncontent;};//没做什么,就透传import进来再export出去module.exports.pitch=function(remainingRequest){//remainingRequest路径要加-!前缀return`importsfrom${JSON.stringify(`-!${remainingRequest}`)};exportdefaults`;};复制代码b-loader的pitch方法有返回结果,会经历这样的过程:
什么情况下需要跳过剩下的loader呢?最常见的,就是动态加载和缓存读取了,要跳过后面loader的计算。bundle-loader是一个典型的例子
bundle-loader实现的是动态按需加载,怎么使用呢?我们可以对react最终ReactDom.render那一步改造一下,换成动态加载react-dom,再体会一下区别
-importReactDomfrom"react-dom";+importLazyReactDomfrom"bundle-loaderlazy&name=reactDom!react-dom";+LazyReactDom(ReactDom=>{+console.log(ReactDom,"ReactDom");ReactDom.render(,document.getElementById("root"));+});可以看见reactdom被隔离开来,动态引入
//获取ChunkNamefunctiongetChunkNameFromRemainingRequest(r){constpaths=r.split("/");letcursor=paths.length-1;if(/^index\./.test(paths[cursor])){cursor--;}returnpaths[cursor];}//原loader不需要做什么了module.exports=function(){};module.exports.pitch=function(remainingRequest,r){//带loadername!前缀的依赖路径consts=JSON.stringify(`-!${remainingRequest}`);//使用注释webpackChunkName来定义chunkname的语法return`exportdefaultfunction(cb){returncb(import(/*webpackChunkName:"my-lazy-${getChunkNameFromRemainingRequest(this.resource)}"*/${s}));}`;};复制代码用法和官方的bundle-loader基本差不多,只是动态import返回一个promise,需要改一下使用方法:
一堆上下文的属性中,我们拿其中一个来实践一下:this.loadModule
loadModule(request:string,callback:function(err,source,sourceMap,module))
loadModule方法作用是,解析给定的request到一个模块,应用所有配置的loader,并且在回调函数中传入生成的source、sourceMap和webpack内部的NormalModule实例。如果你需要获取其他模块的源代码来生成结果的话,你可以使用这个函数。
很明显,这个方法其中一个应用场景就是,在已有代码上注入其他依赖
背景:已有一个api文件api.js
constapi0={log(...args){console.log("apilog>>>",...args);}};module.exports=api0;希望效果:我们使用下面这个a.jsjs文件的时候,可以直接使用api,且不报错
//a.jsexportdefaultfunctiona(){return1;}//其他代码//...api.log("a","b");复制代码因此,我们需要构建的时候loader把api打进去我们的代码里面:
//addapi的loadermodule.exports=function(content,map,meta){//涉及到加载模块,异步loaderconstcallback=this.async();this.loadModule("../src/api.js",(err,source,sourceMap,module)=>{//source是一个module.exports=require(xxx)的字符串,我们需要require那部分callback(null,`constapi=${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语句,就可以用这种方式瞬间完成。这种骚操作的前提是,保证后续同事接手项目难度低、代码无坑。注释、文档、优雅命名都搞起来