皮皮网
皮皮网

【开放源码平台】【即食燕窝源码图片】【大名门源码】手写webpack源码_手写webpack loader

来源:指标源码dma 发表时间:2024-12-22 16:54:45

1.手写webpacktapable源码,手写官方tapable的码手性能真的就一定是好的吗?
2.手写loader并不难
3.umi4发布,有哪些技术亮点?
4.webpack5loader和plugin原理解析
5.手摸手写个webpack plugin
6.详解webpackplugin的原理及编写一个plugin

手写webpack源码_手写webpack loader

手写webpacktapable源码,官方tapable的手写性能真的就一定是好的吗?

       完整的手写源码仓库

       tapable是Webpack?插件机制核心。?码手mini-tapable?不仅解读官方?tapable?的源码,还用自己的手写思路去实现一遍,并且和官方的码手开放源码平台运行时间做了个比较,我和webpack作者相关的手写讨论可以点击查看。webpacktapable源码内部根据newFunction动态生成函数执行体这种优化方式不一定是码手好的。当我们熟悉了tapable后,手写就基本搞懂了webpackplugin的码手底层逻辑,再回头看webpack源码就轻松很多

目录

       src目录。手写这个目录下是码手手写所有的tapablehook的源码,每个hook都用自己的手写思路实现一遍,并且和官方的码手hook执行时间做个对比。

tapable的手写设计理念:单态、多态及内联缓存

       由于在webpack打包构建的过程中,会有上千(数量其实是取决于自身业务复杂度)个插件钩子执行,同时同类型的钩子在执行时,函数参数固定,函数体相同,因此tapable针对这些业务场景进行了相应的优化。这其中最重要的是运用了单态性及多态性概念,内联缓存的原理,也可以看这个issue。为了达到这个目标,tapable采用newFunction动态生成函数执行体的方式,主要逻辑在源码的HookCodeFactory.js文件中。

如何理解tapable的设计理念

       思考下面两种实现方法,哪一种执行效率高,哪一种实现方式简洁?

//方法一:constcallFn=(...tasks)=>(...args)=>{ for(constfnoftasks){ fn(...args)}}//方法二:constcallFn2=(a,b,c)=>(x,y)=>{ a(x,y);b(x,y);c(x,y);}

       callFn及callFn2的目的都是为了实现将一组方法以相同的参数调用,依次执行。很显然,方法一效率明显更高,并且容易扩展,能支持传入数量不固定的一组方法。但是,如果根据单态性以及内联缓存的说法,很明显方法二的执行效率更高,同时也存在一个问题,即只支持传入a,b,c三个方法,参数形态也固定,这种方式显然没有方法一灵活,那能不能同时兼顾效率以及灵活性呢?答案是可以的。我们可以借助newFunction动态生成函数体的方式。

classHookCodeFactory{ constructor(args){ this._argNames=args;this.tasks=[];}tap(task){ this.tasks.push(task);}createCall(){ letcode="";//注意思考这里是即食燕窝源码图片如何拼接参数已经函数执行体的constparams=this._argNames.join(",");for(leti=0;i<this.tasks.length;i++){ code+=`varcallback${ i}=this.tasks[${ i}];callback${ i}(${ params})`;}returnnewFunction(params,code);}call(...args){ constfinalCall=this.createCall();//将函数打印出来,方便观察最终拼接后的结果console.log(finalCall);returnfinalCall.apply(this,args);}}//构造函数接收的arg数组里面的参数,就是taska、b、c三个函数的参数constcallFn=newHookCodeFactory(["x","y","z"]);consta=(x,y,z)=>{ console.log("taska:",x,y,z);};constb=(x,y,z)=>{ console.log("taskb:",x,y,z);};constc=(x,y,z)=>{ console.log("taskc:",x,y,z);};callFn.tap(a);callFn.tap(b);callFn.tap(c);callFn.call(4,5,6);

       当我们在浏览器控制台执行上述代码时:

       拼接后的完整函数执行体:

       可以看到,通过这种动态生成函数执行体的方式,我们能够同时兼顾性能及灵活性。我们可以通过tap方法添加任意数量的任务,同时通过在初始化构造函数时newHookCodeFactory(['x','y',...,'n'])传入任意参数。

       实际上,这正是官方tapable的HookCodeFactory.js的简化版本。这是tapable的精华所在。

tapable源码解读

       tapable最主要的源码在Hook.js以及HookCodeFactory.js中。Hook.js主要是提供了tap、tapAsync、tapPromise等方法,每个Hook都在构造函数内部调用consthook=newHook()初始化hook实例。HookCodeFactory.js主要是根据newFunction动态生成函数执行体。

demo

       以SyncHook.js为例,SyncHook钩子使用如下:

const{ SyncHook}=require("tapable");debugger;consttesthook=newSyncHook(["compilation","name"]);//注册plugin1testhook.tap("plugin1",(compilation,name)=>{ console.log("plugin1",name);compilation.sum=compilation.sum+1;});//注册plugin2testhook.tap("plugin2",(compilation,name)=>{ console.log("plugin2..",name);compilation.sum=compilation.sum+2;});//注册plugin3testhook.tap("plugin3",(compilation,name)=>{ console.log("plugin3",compilation,name);compilation.sum=compilation.sum+3;});constcompilation={ sum:0};//第一次调用testhook.call(compilation,"mytest1");//第二次调用testhook.call(compilation,"mytest2");//第三次调用testhook.call(compilation,"mytest3");...//第n次调用testhook.call(compilation,"mytestn");

       我们用这个demo做为用例,一步步debug。

SyncHook.js源码

       主要逻辑如下:

constHook=require("./Hook");constHookCodeFactory=require("./HookCodeFactory");//继承HookCodeFactoryclassSyncHookCodeFactoryextendsHookCodeFactory{ }constfactory=newSyncHookCodeFactory();constCOMPILE=function(options){ factory.setup(this,options);returnfactory.create(options);};functionSyncHook(args=[],name=undefined){ //初始化Hookconsthook=newHook(args,name);//注意这里修改了hook的constructorhook.constructor=SyncHook;...//每个钩子都必须自行实现自己的compile方法!!!hook.compile=COMPILE;returnhook;}Hook.js源码

       主要逻辑如下:

//问题一:思考一下为什么需要CALL_DELEGATEconstCALL_DELEGATE=function(...args){ //当第一次调用时,实际上执行的是CALL_DELEGATE方法this.call=this._createCall("sync");//当第二次或者第n次调用时,此时this.call方法已经被设置成this._createCall的返回值returnthis.call(...args);};...classHook{ constructor(args=[],name=undefined){ this._args=args;this.name=name;this.taps=[];//存储我们通过hook.tap注册的插件this.interceptors=[];this._call=CALL_DELEGATE;//初始化时,this.call被设置成CALL_DELEGATEthis.call=CALL_DELEGATE;...//问题三:this._x=undefined是什么this._x=undefined;//this._x实际上就是this.taps中每个插件的回调//问题四:为什么需要在构造函数中绑定这些函数this.compile=this.compile;this.tap=this.tap;this.tapAsync=this.tapAsync;this.tapPromise=this.tapPromise;}//每个钩子必须自行实现自己的compile方法。compile方法根据this.taps以及this._args动态生成函数执行体compile(options){ thrownewError("Abstract:shouldbeoverridden");}//生成函数执行体_createCall(type){ returnthis.compile({ taps:this.taps,interceptors:this.interceptors,args:this._args,type:type});}..._tap(type,options,fn){ ...this._insert(options);}tap(options,fn){ this._tap("sync",options,fn);}_resetCompilation(){ this.call=this._call;this.callAsync=this._callAsync;this.promise=this._promise;}_insert(item){ //问题二:为什么每次调用testhook.tap()注册插件时,都需要重置this.call等方法?this._resetCompilation();...}}思考Hook.js源码中的几个问题

       问题一:为什么需要CALL_DELEGATE

       问题二:为什么每次调用testhook.tap()注册插件时,都需要重置this.call等方法?

       问题三:this._x=undefined是什么

       问题四:为什么需要在构造函数中绑定this.compile、this.tap、this.tapAsync以及this.tapPromise等方法

       当我们每次调用testhook.tap方法注册插件时,流程如下:

       方法往this.taps数组中添加一个插件。this.__insert方法逻辑比较简单,但这里有一个细节需要注意一下,为什么每次注册插件时,都需要调用this._resetCompilation()重置this.call等方法?我们稍后再看下这个问题。先继续debug。

       当我们第一次(注意是第一次)调用testhook.call时,实际上调用的是CALL_DELEGATE方法

constCALL_DELEGATE=function(...args){ //当第一次调用时,实际上执行的是CALL_DELEGATE方法this.call=this._createCall("sync");//当第二次或者第n次调用时,此时this.call方法已经被缓存成this._createCall的返回值returnthis.call(...args);};

       CALL_DELEGATE调用this._createCall函数根据注册的this.taps动态生成函数执行体。并且this.call被设置成this._createCall的返回值缓存起来,如果this.taps改变了,则需要重新生成。

       此时如果我们第二次调用testhook.call时,大名门源码就不需要再重新动态生成一遍函数执行体。这也是tapable的优化技巧之一。这也回答了问题一:为什么需要CALL_DELEGATE。

       如果我们调用了n次testhook.call,然后又调用testhook.tap注册插件,此时this.call已经不能重用了,需要再根据CALL_DELEGATE重新生成一次函数执行体,这也回答了问题二:为什么每次调用testhook.tap()注册插件时,都需要重置this.call等方法。可想而知重新生成的过程是很耗时的。因此我们在使用tapable时,最好一次性注册完所有插件,再调用call

testhook.tap("plugin1");testhook.tap("plugin2");testhook.tap("plugin3");testhook.call(compilation,"mytest1");//第一次调用call时,会调用CALL_DELEGATE动态生成函数执行体并缓存起来testhook.call(compilation,"mytest2");//不会重新生成函数执行体,使用第一次的testhook.call(compilation,"mytest3");//不会重新生成函数执行体,使用第一次的

       避免下面的调用方式:

testhook.tap("plugin1");testhook.call(compilation,"mytest1");//第一次调用call时,会调用CALL_DELEGATE动态生成函数执行体并缓存起来testhook.tap("plugin2");testhook.call(compilation,"mytest2");//重新调用CALL_DELEGATE生成函数执行体testhook.tap("plugin3");testhook.call(compilation,"mytest3");//重新调用CALL_DELEGATE生成函数执行体

       现在让我们看看第三个问题,调用this.compile方法时,实际上会调用HookCodeFacotry.js中的setup方法:

setup(instance,options){ instance._x=options.taps.map(t=>t.fn);}

       对于问题四,实际上这和V8引擎的HiddenClass有关,通过在构造函数中绑定这些方法,类中的属性形态固定,这样在查找这些方法时就能利用V8引擎中HiddenClass属性查找机制,提高性能。

HookCodeFactory.js

       主要逻辑:

classHookCodeFactory{ constructor(config){ this.config=config;this.options=undefined;this._args=undefined;}create(options){ this.init(options);letfn;switch(this.options.type){ case'sync':fn=newFunction(...)breakcase'async':fn=newFunction(...)breakcase'promise':fn=newFunction(...)break}this.deinit();returnfn;}setup(instance,options){ instance._x=options.taps.map(t=>t.fn);}...}手写tapable每个Hook

       手写tapable中所有的hook,并比较我们自己实现的hook和官方的执行时间

       这里面每个文件都会实现一遍官方的hook,并比较执行时间,以SyncHook为例,批量注册个插件时,我们自己手写的MySyncHook执行时间0.ms,而官方的需要6ms,这中间整整倍的差距!!!

       具体可以看我的仓库

原文:/post/

手写loader并不难

       理解并掌握webpack的loader配置并非难事。loader在webpack中扮演关键角色,用于处理模块源代码,例如将不同语言转换为JavaScript,或在JavaScript中引入CSS文件。loader支持链式传递,按照相反顺序执行,且遵循单一原则,即每个loader只负责特定任务。配置loader时,确保遵循最佳实践,如使用绝对路径,来源论坛源码 php并配置指向自定义loader的路径。

       loader本质上是一个函数,负责生成预期的JavaScript输出。它们应保持无状态,确保不同模块之间独立运行。遵循这些原则,合理配置loader,即可实现模块预处理。通过`use`属性指向自定义loader,如`use: ['loader/loader.js']`,实现链式调用。

       在`loader.js`文件中,处理传入的代码并返回有效输出至关重要。若返回值非Buffer或String,则可能导致构建失败。例如,`use`属性配置确保正确使用loader路径。

       loader运行原理相对简单,但实际应用时,需关注代码处理细节。了解loader的不同模式(如`pre`、`normal`、`inline`、`post`)有助于优化配置。通过调整`use`属性,可以控制不同loader之间的执行顺序和优先级。

       loader通常由两部分组成:pitch和normal。pitch部分在执行前先运行,若返回值则跳过后续loader。通过添加`pitch`方法,可实现自定义逻辑。loader还支持异步处理,使用`this.async()`和`this.callback`来处理复杂的场景。

       探索更多loader特性,如通过`loader-utils`获取配置参数,以及API文档,能进一步提升loader应用能力。理解loader的工作机制,结合实际需求进行定制,是编写高效loader的关键。

umi4发布,有哪些技术亮点?

       前言

       在业务调整后,我负责的多个中后台系统的研发范式不统一,导致上手成本高,资源调配困难。为了解决这个问题,我们考虑采用应用框架实现范式的源码再次来袭原版统一下调。近期,我们关注了开源应用框架,包括umi,发现其4.0版本的发布带来了技术亮点。

       原文链接: umijs.org/docs/introduc...

       在深入研究后,我们发现了一个有趣的功能:import all from 'umi'。这意味着所有能力都可从umi中导入获取,并支持通过插件扩展此功能。接下来,我们将通过源码分析来探讨如何实现这一功能。

       源码分析 umi从 demo 说起

       首先,我们以创建的Umi项目为例进行分析。在页面中通过import { useModel } from '@umijs/max' 来使用useModel方法。这让我们追踪到 @umijs/max 实际上导出了umi包的全部方法。

       继续深入,我们查看了Umi的入口文件,发现其未实际导出useModel方法。由此推测,@umijs/max可能通过某种机制在构建时注入了这些方法。

       webpack构建时

       根据Umi官方说明,@umijs/max是一个插件集,内置了数据流管理插件。通过配置文件.umirc.ts证实了这一点。进一步分析,我们发现构建过程中的关键在于@umijs/max基于Umi插件协议修改了webpack配置,引入了alias别名@@/exports。

       通过进一步查看源码,我们发现@@/exports最终指向了/.umi/exports文件,这是Umi项目中生成的临时文件。

       模板

       在Umi项目中,.umi目录下存放的是临时文件,那么这个/.umi/exports文件是如何生成的呢?答案在packages/preset-umi/src/features/tmpFiles/tmpFiles.ts中。这里向插件register了一个临时文件生产方法hooks,供applyPlugins使用,实现项目dev或build时的文件生成。

       总结,Umi通过上述机制实现了"import all from '@umijs/max'"。

       接着,我们分析了类似实现的vitekit。同样地,从官方使用案例入手,我们发现vitekit利用了依赖包的方式,而非alias别名。在.vitekit-package中间接引用了@vitekit/framework-vue。

       vite构建时

       vitekit的实现依赖于构建时自动引入依赖包,这得益于其源码/vitekit/src/node.ts中对构建配置的处理。

       综上,umi和vitekit采用相似的实现思路,通过构建时插件协议或依赖包管理来支持扩展导入功能。

       手写实践

       为了验证理解,我们基于上述原理,编写了一个简单的demo。首先,我们创建了临时文件exports,并修改了webpack配置引入alias别名。接着,准备了一个测试demo,展示了函数的导入与使用。最后,执行pnpm start启动项目,验证了临时文件的生成与页面输出,确保实现符合预期。

       总结,通过分析和实践,我们深入理解了Umi和vitekit实现"import all"功能的原理,以及构建时插件和依赖包管理在其中的作用。对于想要理解和实现类似功能的开发者,本文提供的方法和步骤提供了参考。

webpack5loader和plugin原理解析

       大家好,今天为大家解析下loader和plugin

一、区别

       loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中

       plugin赋予了Webpack各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决loader无法实现的其他事从整个运行时机上来看,如下图所示:

       可以看到,两者在运行时机上的区别:

       loader运行在打包文件之前plugins在整个编译周期都起作用在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果

       对于loader,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程

       下面我们来看看loader和plugin实现的原理

Loader原理loader概念

       帮助webpack将不同类型的文件转换为webpack可识别的模块。

loader执行顺序

       分类

       pre:前置loader

       normal:普通loader

       inline:内联loader

       post:后置loader

       执行顺序

       4类loader的执行优级为:pre>normal>inline>post。

       相同优先级的loader执行顺序为:从右到左,从下到上。

       例如:

//此时loader执行顺序:loader3-loader2-loader1module:{ rules:[{ test:/\.js$/,loader:"loader1",},{ test:/\.js$/,loader:"loader2",},{ test:/\.js$/,loader:"loader3",},],},//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},

       使用loader的方式

       配置方式:在webpack.config.js文件中指定loader。(pre、normal、postloader)

       内联方式:在每个import语句中显式指定loader。(inlineloader)

开发一个loader1.最简单的loader//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};

       它接受要处理的源码作为参数,输出转换后的js代码。

2.loader接受的参数

       content源文件的内容

       mapSourceMap数据

       meta数据,可以是任何内容

loader分类1.同步loadermodule.exports=function(content,map,meta){ returncontent;};

       this.callback方法则更灵活,因为它允许传递多个参数,而不仅仅是content。

module.exports=function(content,map,meta){ //传递map,让source-map不中断//传递meta,让下一个loader接收到其他参数this.callback(null,content,map,meta);return;//当调用callback()函数时,总是返回undefined};2.异步loadermodule.exports=function(content,map,meta){ constcallback=this.async();//进行异步操作setTimeout(()=>{ callback(null,result,map,meta);},);};

       由于同步计算过于耗时,在Node.js这样的单线程环境下进行此操作并不是好的方案,我们建议尽可能地使你的loader异步化。但如果计算量很小,同步loader也是可以的。

3.RawLoader

       默认情况下,资源文件会被转化为UTF-8字符串,然后传给loader。通过设置raw为true,loader可以接收原始的Buffer。

module.exports=function(content){ //content是一个Buffer数据returncontent;};module.exports.raw=true;//开启RawLoader4.PitchingLoadermodule.exports=function(content){ returncontent;};module.exports.pitch=function(remainingRequest,precedingRequest,data){ console.log("dosomethings");};

       webpack会先从左到右执行loader链中的每个loader上的pitch方法(如果有),然后再从右到左执行loader链中的每个loader上的普通loader方法。

       在这个过程中如果任何pitch有返回值,则loader链被阻断。webpack会跳过后面所有的的pitch和loader,直接进入上一个loader。

loaderAPI方法名含义用法this.async异步回调loader。返回this.callbackconstcallback=this.async()this.callback可以同步或者异步调用的并返回多个结果的函数this.callback(err,content,sourceMap?,meta?)this.getOptions(schema)获取loader的optionsthis.getOptions(schema)this.emitFile产生一个文件this.emitFile(name,content,sourceMap)this.utils.contextify返回一个相对路径this.utils.contextify(context,request)this.utils.absolutify返回一个绝对路径this.utils.absolutify(context,request)

       更多文档,请查阅webpack官方loaderapi文档

手写clean-log-loader

       作用:用来清理js代码中的console.log

//loaders/clean-log-loader.jsmodule.exports=functioncleanLogLoader(content){ //将console.log替换为空returncontent.replace(/console\.log\(.*\);?/g,"");};手写banner-loader

       作用:给js代码添加文本注释

       loaders/banner-loader/index.js

constschema=require("./schema.json");module.exports=function(content){ //获取loader的options,同时对options内容进行校验//schema是options的校验规则(符合JSONschema规则)constoptions=this.getOptions(schema);constprefix=`/**Author:${ options.author}*/`;return`${ prefix}\n${ content}`;};

       loaders/banner-loader/schema.json

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},0手写babel-loader

       作用:编译js代码,将ES6+语法编译成ES5-语法。

       下载依赖

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},1

       loaders/babel-loader/index.js

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},2

       loaders/banner-loader/schema.json

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},3手写file-loader

       作用:将文件原封不动输出出去

       下载包

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},4

       loaders/file-loader.js

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},5

       loader配置

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},6手写style-loader

       作用:动态创建style标签,插入js中的样式代码,使样式生效。

       loaders/style-loader.js

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},7Plugin原理Plugin的作用

       通过插件我们可以扩展webpack,加入自定义的构建行为,使webpack可以执行更广泛的任务,拥有更强的构建能力。

Plugin工作原理

       webpack就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack通过Tapable来组织这条复杂的生产线。webpack在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack的事件流机制保证了插件的有序性,使得整个系统扩展性很好。——「深入浅出Webpack」

       站在代码逻辑的角度就是:webpack在编译代码过程中,会触发一系列Tapable钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当webpack构建的时候,插件注册的事件就会随着钩子的触发而执行了。

Webpack内部的钩子什么是钩子

       钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。

Tapable

       Tapable为webpack提供了统一的插件接口(钩子)类型定义,它是webpack的核心功能库。webpack中目前有十种hooks,在Tapable源码中可以看到,他们是:

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},8

       Tapable还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

       tap:可以注册同步钩子和异步钩子。

       tapAsync:回调方式注册异步钩子。

       tapPromise:Promise方式注册异步钩子。

Plugin构建对象Compiler

       compiler对象中保存着完整的Webpack环境配置,每次启动webpack构建时它都是一个独一无二,仅仅会创建一次的对象。

       这个对象会在首次启动Webpack时创建,我们可以通过compiler对象上访问到Webapck的主环境配置,比如loader、plugin等等配置信息。

       它有以下主要属性:

       compiler.options可以访问本次启动webpack时候所有的配置文件,包括但不限于loaders、entry、output、plugin等等完整配置信息。

       compiler.inputFileSystem和compiler.outputFileSystem可以进行文件操作,相当于Nodejs中fs。

       compiler.hooks可以注册tapable的不同种类Hook,从而可以在compiler生命周期中植入不同的逻辑。

       compilerhooks文档

Compilation

       compilation对象代表一次资源的构建,compilation实例能够访问所有的模块和它们的依赖。

       一个compilation对象会对构建依赖图中所有模块,进行编译。在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、分块(chunk)、哈希(hash)和重新创建(restore)。

       它有以下主要属性:

       compilation.modules可以访问所有模块,打包的每一个文件都是一个模块。

       compilation.chunkschunk即是多个modules组成而来的一个代码块。入口文件引入的资源组成一个chunk,通过代码分割的模块又是另外的chunk。

       compilation.assets可以访问本次打包生成所有文件的结果。

       compilation.hooks可以注册tapable的不同种类Hook,用于在compilation编译模块阶段进行逻辑添加以及修改。

       compilationhooks文档

生命周期简图开发一个插件最简单的插件

       plugins/test-plugin.js

//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},9注册hook//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};0启动调试

       通过调试查看compiler和compilation对象数据情况。

       package.json配置指令

//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};1

       运行指令

//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};2

       此时控制台输出以下内容:

PSC:\Users\\Desktop\source>//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};2>source@1.0.0debug>node--inspect-brk./node_modules/webpack-cli/bin/cli.jsDebuggerlisteningonws://.0.0.1:/ea-7b--a7-fccForhelp,see:/post/

       开发思路:

       我们需要借助html-webpack-plugin来实现

       在html-webpack-plugin输出index.html前将内联runtime注入进去

       删除多余的runtime文件

       如何操作html-webpack-plugin?官方文档

       实现:

//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};7

手摸手写个webpack plugin

       本周的任务是深入了解和编写webpack插件。插件是什么?它实际上是一个具有 apply 方法的JavaScript对象,更准确地说,就是一种函数。它可以是普通的函数,也可以是特殊的函数,用于实现特定功能。

       相比loader,插件可以解决一些loader无法处理的场景,比如在打包开始前进行环境配置、打包结束后发送通知邮件等。例如,HtmlWebpackPlugin插件可以帮助我们在dist文件夹下自动生成index.html文件,并在其中自动引入打包后的css和js脚本文件。安装并配置插件后,运行打包指令,可以看到生成的index.html文件自动引入了生成的main.js脚本文件。

       接下来,我们深入探讨HtmlWebpackPlugin的源码,了解如何编写插件。通过观察源码,我们可以发现插件是通过apply方法在特定生命周期内执行操作。以HtmlWebpackPlugin为例,其在初始化阶段会利用Compiler提供的hook函数,实现生成html文件并引入资源的功能。

       为了更深入理解插件的工作原理,我们可以执行yarn run webpack命令,跟踪webpack的运行过程,了解不同hook函数的调用时机和作用。例如,afterDone钩子函数会在打包完成后触发,用于执行发送通知邮件等操作。通过观察Compiler类的实现,我们可以理解如何在插件中访问和利用Compiler实例对象,以及如何在特定钩子函数的回调中实现自定义逻辑。

       接下来,我们以一句话需求为例,实现一个在webpack打包后发送通知邮件的插件。首先,我们需要安装nodemailer库以实现邮件发送功能。然后,根据需求编写插件代码,利用Compiler提供的hook函数,如afterDone,实现邮件发送逻辑。运行webpack后,我们可以看到通知邮件被成功发送。为了方便他人使用,我们还可以将自定义插件发布到npm仓库。

       在插件开发中,还有一些重要的概念,如SyncHook和AsyncHook,它们分别用于同步和异步操作。另外,了解如何在插件中正确使用这些概念,对于提高插件的可复用性和功能性至关重要。官方文档提供了关于编写插件的详细指导,建议开发者深入学习。

       总之,通过本篇文章的学习,我们不仅掌握了webpack插件的基本概念和使用方法,还深入探讨了其原理和实战应用。希望这些知识能帮助开发者在实际项目中灵活运用插件,提升开发效率。

详解webpackplugin的原理及编写一个plugin

       plugin解决了什么问题?

       plugin解决了Webpack构建生命周期过程中的功能定制问题,可以利用plugin参与到webpack构建流程中的各个阶段并劫持做一些代码处理。

       æ¯”如,打包后需要生成一个html文件,那么就可以使用html-webpack-plugin。还有,在打包之前把dist文件删除,就可以使用clean-webpack-plugin。

       webpack本身就是一个构建过程的状态机,其自身的核心功能也是构建在loader和plugin的机制上的。

compiler和compilation具体是干什么的?

       é¦–先我们来看一个webpack自带的插件BannerPlugin代码,其实webpack的很多核心公司就是利用插件来实现的。

       æ’件的格式

       ä¸€ä¸ªJavaScript函数或JavaScriptç±»;

       åœ¨å®ƒåŽŸåž‹ä¸Šå®šä¹‰çš„apply方法,会在安装插件时被调用,并被webpackcompiler调用一次;

       æŒ‡å®šä¸€ä¸ªè§¦åŠåˆ°webpack本身的事件钩子,即hooks,用于特定时机处理额外的逻辑;

classBannerPlugin{ constructor(options){ this.options=options;}apply(compiler){ constoptions=this.options;constbanner=this.banner;compiler.hooks.compilation.tap("BannerPlugin",compilation=>{ compilation.hooks.processAssets.tap({ name:"BannerPlugin"},()=>{ for(constchunkofcompilation.chunks){ for(constfileofchunk.files){ constdata={ chunk,filename:file};//生成注释constcomment=compilation.getPath(banner,data);//把注释加入到文件中compilation.updateAsset(file,old=>{ constsource=options.footer?newConcatSource(old,"\n",comment):newConcatSource(comment,"\n",old);returnsource;}});}}}}}}

       ä»Žä»£ç ä¸­å‡ºçŽ°äº†compiler和compilation,那它们到底是什么呢?

compiler

       compiler模块是Webpack最核心的模块。每次执行Webpack构建的时候,在Webpack内部,会首先实例化一个Compiler对象,然后调用它的run方法来开始一次完整的编译过程。compiler对象代表了完整的webpack环境配置,插件可以通过它获取到webpack的配置信息,如entry、output、module等配置。

       compiler钩子compiler有很多钩子,下面只介绍常用的几个:钩子名?|Tapable类型|触发时机|传入callback的参数?||--------------------|-----------------|------------------------------------------|---------------------------------||entryOption?|SyncBailHook|在webpack中的entry配置处理过之后|context,entry||afterPlugins|SyncHook?|初始化完内置插件之后|compiler||environment?|SyncHook?|准备编译环境,webpackplugins配置初始化完成之后?|compiler||beforeRun?|AsyncSeriesHook?|开始正式编译之前?|compiler||run|AsyncSeriesHook?|开始编译之后,读取records之前;|compiler||compile|SyncHook?|一次compilation编译创建之前|compilationParams||compilation?|SyncHook?|compilation创建成功之后?|compilation,compilationParams||emit|AsyncSeriesHook?|生成资源到output目录之前?|compilation?||done|AsyncSeriesHook?|compilation完成之后?|stats?||failed|SyncHook?|compilation失败|

       æ•´ä¸ªCompiler完整地展现了Webpack的构建流程:

       å‡†å¤‡é˜¶æ®µï¼šmake之前做的事情都属于准备阶段,这阶段的calback入参以compiler为主;

       ç¼–译阶段:这阶段以compilation的钩子为主,calback入参以compilation为主;

       äº§å‡ºé˜¶æ®µï¼šè¿™é˜¶æ®µä»Žcompilation开始,最后回到Compiler钩子上,calback传入参数是跟结果相关的数据,包括stats、error。

compilation

       åœ¨compilation阶段,模块会被加载(loaded)、封存(sealed)、优化(optimized)、分块(chunked)、哈希(hashed)和重新创建(restored),Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation对象也提供了很多事件回调供插件做扩展,通过Compilation也能读取到Compiler对象。

       Compilation钩子

       åœ¨Compilation中处理的对象分别是module、chunk、assets,由modules组成chunks,由chunks生成assets,处理顺序是:module→modules→chunks→assets,先从单个module开始处理,查找依赖关系,最后完成单个module处理,完成全部modules之后,开始chunks阶段处理,最后在根据优化配置,按需生成assets。

       æ‰€ä»¥æ•´ä¸ªCompilation的生命周期钩子虽然比较多,但是大规律上是围绕这个顺序进行的,具体的钩子可以查看webpack官网。

       Stats对象在Webpack的回调函数中会得到stats对象。这个对象实际来自于Compilation.getStats(),返回的是主要含有modules、chunks和assets三个属性值的对象。

       modules:记录了所有解析后的模块;

       chunks:记录了所有chunk;

       assets:记录了所有要生成的文件。

       æœ‰äº†å¯¹compilercompilation的理解,那现在来看看BannerPlugin的实现,这个插件的功能是在最后生成的文件的头部加上一段我们自定义的注释,那么它的执行时机肯定是在编译完成之后,生成打包文件之间,也就是在compiler.hooks.compilation这个大钩子下面的processAssets钩子里面执行我们的逻辑。

编写pluginplugin:在文件尾部插入一段注释

       é¦–先创建一个plugins/FootPlugin.js,代码如下:

const{ ConcatSource}=require('webpack-sources')classFootPlugin{ constructor(options){ this.options=options}apply(compiler){ compiler.hooks.compilation.tap('FootPlugin',compilation=>{ compilation.hooks.processAssets.tap('FootPlugin',()=>{ for(constchunkofcompilation.chunks){ for(constfileofchunk.files){ console.log('file--',file)//bundle.js//定义注释的内容constcomment=`/*${ this.options.banner}*/`compilation.updateAsset(file,old=>{ //把注释和旧代码进行拼接returnnewConcatSource(old,'\n',comment)})}}})})}}module.exports=FootPlugin

       webpack.config.js

constFootPlugin=require('./plugins/FootPlugin')module.exports={ plugins:[newwebpack.BannerPlugin({ banner:'欢迎学习'}),newFootPlugin({ banner:'结束学习'})]}

       å¯ä»¥çœ‹åˆ°åœ¨bundle.js的开头和结尾都有对应的注释。

plugin:文件超过一定大小时给出警告const{ resolve}=require('path')constfs=require('fs')classBundleSizeWebpackPlugin{ constructor(options){ this.options=options}apply(compiler){ const{ sizeLimit}=this.optionsconsole.log('bundlesizeplugin')//在编译完成后,执行回调,拿到打包后文件路径,然后读取文件信息获取文件大小,然后定义一些逻辑compiler.hooks.done.tap('BundleSizePlugin',stats=>{ const{ path,filename}=stats.compilation.outputOptionsconstbundlePath=resolve(path,filename)const{ size}=fs.statSync(bundlePath)constbundleSize=size/if(bundleSize<sizeLimit){ console.log('safe:bundle-size',bundleSize,'\nsizelimit:',sizeLimit)}else{ console.warn('unsafe:bundle-size',bundleSize,'\nsizelimit:',sizeLimit)}})}}module.exports=BundleSizeWebpackPlugin

       æœ¬ç« åˆ°è¿™é‡Œå°±ç»“束了,我们开始介绍了webpack的核心概念,有了对webpack的基本配置的了解;接着利用css-loader和style-loader对webpack的loader机制进行了详细分析;最后,对webpackplugin的工作机制和流程进行了梳理,并手写了两个plugin,让你对plugin不再觉得遥不可及。有了这些前置知识,就可以对我们原生项目进行工程化的改造了。期待你的学习。

原文:/post/

相关栏目:百科