如何优雅地编写一个高逼格的JS插件?
在一个风和日丽的早晨,我正悠闲地喝着Coffe,突然领导向我走来,我赶紧熟练地切出VSCode,淡定自若地问:领导,什么事?领导拍了拍我的肩膀:你上次封装的方法同事跟我反馈使用起来很不错啊,你不如做成JS插件给大家用吧。我放下了手中的马克杯,甩了一下眼前仅剩的几根刘海:没问题啊,小Case!随即开始摸鱼。。。。原型链写法
要开始编写插件就得先了解JS模块化,早期的模块化是利用了函数自执行来实现的,在单独的函数作用域中执行代码可以避免插件中定义的变量污染到全局变量,举个栗子,以下代码实现了一个简单随机数生成的插件:;(function(global){usestrict;varMyPluginfunction(name){this。namename};MyPlugin。prototype{say:function(){console。log(欢迎你:,this。name)},random:function(min0,max1){if(minNumber。MAXSAFEINTEGERmaxNumber。MAXSAFEINTEGER){returnMath。floor(Math。random()(maxmin1))min}}};函数自执行将this(全局下为window)传入,并在其下面挂载方法global。MyPluginMyPlugin;兼容CommonJs规范导出if(typeofmodule!undefinedmodule。exports)module。exportsMyPlugin;})(this);
直接使用script标签引入该插件,接着new一个实例就能使用插件啦:varaFnnewMyPlugin()varnumaFn。random(10,20)console。log(num)打印一个1020之间的随机数闭包式写法
上面的插件使用时如果调用say方法,会打印方法中的欢迎字样,并显示初始化的name值:varaFnnewMyPlugin(呀哈哈)aFn。say()欢迎你:呀哈哈
但由于属性能被直接访问,插件中的变量就可以随意修改,这可能是我们不想看到的:varaFnnewMyPlugin(呀哈哈)aFn。namenullaFn。say()欢迎你:null
那么如果要创建私有变量,可以利用JS闭包原理来编写插件,我们使用工厂模式来创建函数,再举个栗子,如下代码实现了一个简单正则校验的插件:;(function(global){usestrict;varMyPluginfunction(value){varvalvaluevarreg{phone:1〔3456789〕d{9},number:?d。?d};return{getRegs(){returnreg},setRegs(params){reg{。。。reg,。。。params}},isPhone(){reg。phone。test(val)console。log(这是手机号)returnthis},isNumber(){reg。number。test(val)console。log(这是数字)returnthis}};};函数自执行将this(全局下为window)传入,并在其下面挂载方法global。MyPluginMyPlugin;兼容CommonJs规范导出if(typeofmodule!undefinedmodule。exports)module。exportsMyPlugin;})(this);
这时我们再调用插件,其内部的变量是不可访问的,只能通过插件内部的方法查看修改:varaFnnewMyPlugin()console。log(aFn。reg)undefinedvarregaFn。getRegs()console。log(reg){phone:{。。。。},number:{。。。。。}}
上面代码中我们在isPhoneisNumber方法的最后都返回了this,这是为了实现如下的链式调用:varaFnnewMyPlugin(13800138000)aFn。isPhone()。isNumber()log:这是手机号这是数字仿JQuery写法
这种写法是仿造JQ实现的一种编写模式,可以省去调用时new实例化的步骤,并实现类似(xxx)。someFn(。。。。)这样的调用方法,在需要频繁DOM操作的时候就很适合这么编写插件。笔者以前会在小项目中自己实现一些类JQ选择器操作的功能插件,来避免引入整个JQ,实现插件的核心思路如下:varFnFunction(params){returnnewFn。prototype。init(params)}Fn。prototype{init:function(){}}Fn。prototype。init。prototypeFn。prototype
可以看出核心是对JS原型链的极致利用,首先主动对其原型上的init方法进行实例化并返回,init相当于构造函数的效果,而此时返回的实例里并没有包含Fn的方法,我们调用时JS自然就会从init的原型对象上去查找,于是最终init下的原型才又指向了Fn的原型,通过这种套娃的手法,使得我们能够不通过实例化Fn又能正确地访问到Fn下的原型对象。
说了这么多,还是举个栗子,以下代码实现了一个简单的样式操作插件:;(function(global){usestrict;varMyPluginfunction(el){returnnewMyPlugin。prototype。init(el)};MyPlugin。prototype{init:function(el){this。eltypeofelstring?document。querySelector(el):el;},setBg:function(bg){this。el。style。backgroundbg;returnthis},setWidth:function(w){this。el。style。widthw;returnthis},setHeight:function(h){this。el。style。heighth;returnthis}};MyPlugin。prototype。init。prototypeMyPlugin。prototypescript标签引入插件后全局下挂载一个的方法global。MyPlugin;})(thiswindow);
使用演示:!页面元素helloworld
为元素设置背景:(app)。setBg(ff0)
为元素设置背景并改变宽高:(app)。setBg(ff0)。setHeight(100px)。setWidth(200px)
工程化插件
假设以后会有多人同时开发的情况,仅靠一个JS维护大型插件肯定是独木难支,这时候就需要组件化把颗粒度打细,将插件拆分成多个文件,分别负责各自的功能,最终再打包成一个文件引用。如今ES模块化已经可以轻松应对功能拆分了,所以我们只需要一个打包器,Rollup。js就是不错的选择,有了它我们可以更优雅地编写插件,它会帮我们打包。许多大型框架例如Vue、React都是用它打包的。
Rollup是一个用于JavaScript的模块打包器,它将小段代码编译成更大更复杂的东西,例如库或应用程序。Rollup官网〔1〕创建一个示例
下面我们一步步实现这个工程化的插件,没有那么复杂,先创建一个目录:mkdirpmyprojectsrc
接着运行npminit进行项目初始化,一路回车,接着为项目安装Rollup:npminstallsavedevrollup
根目录下创建入口文件index。js,以及src下的main。js用于等下测试:index。jsimportmainfrom。srcmain。js;console。log(main);srcmain。jsexportdefaulthelloworld!;
根目录下创建rollup。config。jsimportbabelfromrolluppluginbabelbabel:将最终代码编译成es5,我们的开发代码可以不用处理兼容性。importcommonjsfromrollupplugincommonjsimportresolvefromrolluppluginnoderesolveresolve、commonjs:用于兼容可以依赖commonjs规范的包。exportdefault{input:index。js,output:〔{file:distmain。umd。js,format:umd,name:bundlename,},{file:distmain。es。js,format:es,},{file:distmain。cjs。js,format:cjs,},〕,plugins:〔babel({exclude:nodemodules,}),resolve({jsnext:true,main:true,browser:true,}),commonjs(),〕,}
把上面的依赖安装一下,运行:npminstallsavedevbabelcorebabelpresetenvrolluppluginbabellatestrolluppluginnoderesolverollupplugincommonjs
修改package。json,增加一条脚本命令:。。。。。。。scripts:{。。。。。。dev:rollupcw},
最后运行npmrundev看看效果吧:
示例结果
打包最终文件位置:
运行nodedistmain。cjs。js:
打包文件格式说明1。umd
集合了CommonJS、AMD、CMD、IIFE为一体的打包模式,看看上面的helloworld会被打包成什么:(function(global,factory){typeofexportsobjecttypeofmodule!undefined?module。exportsfactory():typeofdefinefunctiondefine。amd?define(factory):(globaltypeofglobalThis!undefined?globalThis:globalself,global〔bundlename〕factory());})(this,(function(){usestrict;。。。。。代码省略。。。。。returnxxxxxxxx;}));
可以看出导出的文件就是我们前面一直使用的函数自执行开发方式,其中加了各种兼容判断代码将在哪个环境下导入。2。es
现代JS的标准,导出的文件只能使用ES模块化方式导入。3。cjs
这个是指CommonJS规范导出的格式,只可在Node环境下导入。补充:模块化的发展早期利用函数自执行实现,在单独的函数作用域中执行代码(如JQuery)AMD:引入require。js编写模块化,引用依赖必须提前声明CMD:引入sea。js编写模块化,特点是可以动态引入依赖CommonJS:NodeJs中的模块化,只在服务端适用,是同步加载ESModules:ES6中新增的模块化,是目前的主流
本文前三种插件编写方式均属于利用函数自执行(IIFE)实现的插件,同时在向全局注入插件时兼容了CommonJS规范,但并未兼容AMDCMD,是因为目前基本没有项目会使用到这两种模块化。自动化API文档
一个JS插件如果没有一份文档,如同一台精密的仪器没有说明书。当别人使用你的插件时,他不可能去查看源码才知道这个插件有哪些方法、用途如何、要传哪些参数等。
所以这里我们使用JSDoc来创建API文档,它使用简单,只需要在代码中编写规范的注释,即能根据注释自动生成文档,一举多得,十分优雅!npminstallsavedevjsdocopen
修改package。json,增加一条脚本命令:。。。。。。。scripts:{。。。。。。doc:jsdocdistmain。es。jsnodeserver。js},
根目录下创建文件server。js:varopenrequire(open);open(outindex。html);这是apidoc默认生成的路径,这里只是为了自动打开网页
好了,现在可以使用npmrundoc命令来生成文档了,依然是举个栗子,我们在src目录下添加一个文件ArrayDelSome。js:desc对象数组去重param{Array}arrparam{String}对象中相同的关键字(如id)return{Array}返回新数组,eg:ArrayDelSome(〔{id:1},{id:2},{id:1}〕,id)返回:〔{id:1},{id:2}〕functionArrayDelSome(arr,key){constmapnewMap()returnarr。filter((x)!map。has(x〔key〕)map。set(x〔key〕,true))}exportdefaultArrayDelSome
本例只演示最基础的用法,JSDoc有许多类型注释大家可以自行搜索学习下,不过本例最基本的这几个注释依旧是够用的。
运行npmrundoc,将会打开一个网页,可以查看我们刚写的工具函数:
注意:在生成文档前需要先进行过rollup的打包,且不能开启去注释之类的插件,因为上面的例子实际是对dist目录下的最终文件进行文档编译的。发布插件
还没发布过npm包?限于篇幅就不展开,可以参考这篇文章如何发布npm包〔2〕。私有源发布
如果你的公司有私域npm管理源,或者平时喜欢用淘宝源,推荐使用nrm进行切换:npminrmg1。查看源:nrmls2。添加源:nrmaddnamehttp:xxx。xxx。xxx。xxx:48733。删除源:nrmdelname4。使用指定源:nrmusenpm总结
功能较简单的JS插件我们可以直接采用前三种方式开发,如果涉及DOM操作较多,可以编写仿JQ的插件更好用,如果插件功能较多,有可能形成长期维护的大型插件,那么可以采用工程化的方式开发,方便多人协作,配套生成文档也利于维护。
以上就是文章的全部内容,希望对你有所帮助!如果觉得文章写得不错,可以点赞收藏,也欢迎关注,我会持续更新更多前端有用的知识与实用技巧,我是茶无味de一天〔3〕,希望与你共同成长引用链接
〔1〕Rollup官网:https:rollupjs。orgguideen
〔2〕如何发布npm包:https:juejin。cnpost6979531144043692039
〔3〕关于作者:https:book。palxp。com