写点什么

如何优雅地编写一个高逼格的 JS 插件?

  • 2022 年 10 月 09 日
    广东
  • 本文字数:4930 字

    阅读完需:约 16 分钟

在一个风和日丽的早晨,我正悠闲地喝着 Coffe,突然领导向我走来,我赶紧熟练地切出 VSCode,淡定自若地问:领导,什么事?领导拍了拍我的肩膀:你上次封装的方法同事跟我反馈使用起来很不错啊,你不如做成 JS 插件给大家用吧。我放下了手中的马克杯,甩了一下眼前仅剩的几根刘海:没问题啊,小 Case!随即开始摸鱼....

原型链写法

要开始编写插件就得先了解 JS 模块化,早期的模块化是利用了函数自执行来实现的,在单独的函数作用域中执行代码可以避免插件中定义的变量污染到全局变量,举个栗子🌰,以下代码实现了一个简单随机数生成的插件:


;(function (global) {    "use strict";
var MyPlugin = function (name) { this.name = name };
MyPlugin.prototype = { say: function () { console.log('欢迎你:', this.name) }, random: function (min = 0, max = 1) { if (min <= Number.MAX_SAFE_INTEGER && max <= Number.MAX_SAFE_INTEGER) { return Math.floor(Math.random() * (max - min + 1)) + min } } }; // 函数自执行将 this(全局下为window)传入,并在其下面挂载方法 global.MyPlugin = MyPlugin; // 兼容CommonJs规范导出 if (typeof module !== 'undefined' && module.exports) module.exports = MyPlugin; })(this);
复制代码


直接使用 script 标签引入该插件,接着 new 一个实例就能使用插件啦:


var aFn = new MyPlugin()
var num = aFn.random(10, 20)console.log(num) // 打印一个 10~20 之间的随机数
复制代码

闭包式写法

上面的插件使用时如果调用 say 方法,会打印方法中的欢迎字样,并显示初始化的 name 值:


var aFn = new MyPlugin('呀哈哈')aFn.say() // 欢迎你: 呀哈哈
复制代码


但由于属性能被直接访问,插件中的变量就可以随意修改,这可能是我们不想看到的:


var aFn = new MyPlugin('呀哈哈')aFn.name = nullaFn.say() // 欢迎你: null
复制代码


那么如果要创建私有变量,可以利用 JS 闭包原理来编写插件,我们使用工厂模式来创建函数,再举个栗子🌰,如下代码实现了一个简单正则校验的插件:


; (function (global) {    "use strict";
var MyPlugin = function (value) { var val = value var reg = { phone: /^1[3456789]\d{9}$/, number: /^-?\d*\.?\d+$/ }; return { getRegs() { return reg }, setRegs(params) { reg = { ...reg, ...params } }, isPhone() { reg.phone.test(val) && console.log('这是手机号') return this }, isNumber() { reg.number.test(val) && console.log('这是数字') return this } }; };
// 函数自执行将 this(全局下为window)传入,并在其下面挂载方法 global.MyPlugin = MyPlugin; // 兼容CommonJs规范导出 if (typeof module !== 'undefined' && module.exports) module.exports = MyPlugin;})(this);
复制代码


这时我们再调用插件,其内部的变量是不可访问的,只能通过插件内部的方法查看/修改


var aFn = new MyPlugin()
console.log( aFn.reg ) // undefined
var reg = aFn.getRegs()console.log( reg ) // {"phone":{....},"number":{.....}}
复制代码


上面代码中我们在 isPhone isNumber 方法的最后都返回了 this,这是为了实现如下的链式调用:


var aFn = new MyPlugin(13800138000)
aFn.isPhone().isNumber() // log: > 这是手机号 > 这是数字
复制代码

仿 JQuery 写法

这种写法是仿造 JQ 实现的一种编写模式,可以省去调用时new实例化的步骤,并实现类似 $(xxx).someFn(....) 这样的调用方法,在需要频繁 DOM 操作的时候就很适合这么编写插件。笔者以前会在小项目中自己实现一些类 JQ 选择器操作的功能插件,来避免引入整个 JQ,实现插件的核心思路如下:


var Fn = Function(params) {    return new Fn.prototype.init(params)}
Fn.prototype = { init: function() {}}
Fn.prototype.init.prototype = Fn.prototype
复制代码


可以看出核心是对 JS 原型链的极致利用,首先主动对其原型上的init方法进行实例化并返回,init相当于构造函数的效果,而此时返回的实例里并没有包含Fn的方法,我们调用时 JS 自然就会从init的原型对象上去查找,于是最终init下的原型才又指向了Fn的原型,通过这种"套娃"的手法,使得我们能够不通过实例化Fn又能正确地访问到Fn下的原型对象。


说了这么多,还是举个栗子🌰,以下代码实现了一个简单的样式操作插件:


;(function (global) {  "use strict";
var MyPlugin = function (el) { return new MyPlugin.prototype.init(el) };
MyPlugin.prototype = { init: function (el) { this.el = typeof el === "string" ? document.querySelector(el) : el; }, setBg: function (bg) { this.el.style.background = bg; return this }, setWidth: function (w) { this.el.style.width = w; return this }, setHeight: function (h) { this.el.style.height = h; return this } };
MyPlugin.prototype.init.prototype = MyPlugin.prototype // script标签引入插件后全局下挂载一个_$的方法 global._$ = MyPlugin;})(this || window);
复制代码


使用演示:


<!-- 页面元素 --><div id="app">hello world</div>
复制代码


为元素设置背景:


_$('#app').setBg('#ff0')
复制代码



为元素设置背景并改变宽高:


_$('#app').setBg('#ff0').setHeight('100px').setWidth('200px')
复制代码


工程化插件

假设以后会有多人同时开发的情况,仅靠一个 JS 维护大型插件肯定是独木难支,这时候就需要组件化把颗粒度打细,将插件拆分成多个文件,分别负责各自的功能,最终再打包成一个文件引用。如今 ES 模块化已经可以轻松应对功能拆分了,所以我们只需要一个打包器,Rollup.js 就是不错的选择,有了它我们可以更优雅地编写插件,它会帮我们打包。许多大型框架例如 VueReact 都是用它打包的。


Rollup 是一个用于 JavaScript 的模块打包器,它将小段代码编译成更大更复杂的东西,例如库或应用程序。官网链接

创建一个示例

下面我们一步步实现这个工程化的插件,没有那么复杂,先创建一个目录:


mkdir -p my-project/src
复制代码


接着运行 npm init 进行项目初始化,一路回车,接着为项目安装 Rollup


npm install --save-dev rollup
复制代码


根目录下创建入口文件 index.js,以及 src 下的 main.js 用于等下测试:


// index.jsimport main from './src/main.js';
console.log(main);
复制代码


// src/main.jsexport default 'hello world!';
复制代码


根目录下创建 rollup.config.js


import babel from 'rollup-plugin-babel'// babel:将最终代码编译成 es5,我们的开发代码可以不用处理兼容性。import commonjs from 'rollup-plugin-commonjs'import resolve from 'rollup-plugin-node-resolve'// resolve、commonjs:用于兼容可以依赖 commonjs 规范的包。
export default { input: 'index.js', output: [ { file: 'dist/main.umd.js', format: 'umd', name: 'bundle-name', }, { file: 'dist/main.es.js', format: 'es', }, { file: 'dist/main.cjs.js', format: 'cjs', }, ], plugins: [ babel({ exclude: 'node_modules/**', }), resolve({ jsnext: true, main: true, browser: true, }), commonjs(), ],}
复制代码


把上面的依赖安装一下,运行:


npm install --save-dev @babel/core @babel/preset-env rollup-plugin-babel@latest rollup-plugin-node-resolve rollup-plugin-commonjs
复制代码


修改 package.json,增加一条脚本命令:


......."scripts": {    ......    "dev": "rollup -c -w"},
复制代码


最后运行 npm run dev 看看效果吧:


示例结果

打包最终文件位置:



运行 node dist/main.cjs.js


打包文件格式说明

1. umd

集合了 CommonJSAMDCMDIIFE 为一体的打包模式,看看上面的 hello world 会被打包成什么:


(function (global, factory) {    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :    typeof define === 'function' && define.amd ? define(factory) :    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global["bundle-name"] = factory());})(this, (function () { 'use strict';
.....代码省略..... return xxxxxxxx;}));
复制代码


可以看出导出的文件就是我们前面一直使用的函数自执行开发方式,其中加了各种兼容判断代码将在哪个环境下导入。

2. es

现代 JS 的标准,导出的文件只能使用 ES 模块化 方式导入。

3. cjs

这个是指 CommonJS 规范导出的格式,只可在 Node 环境下导入。

补充:模块化的发展

  • 早期利用函数自执行实现,在单独的函数作用域中执行代码(如 JQuery )

  • AMD:引入 require.js 编写模块化,引用依赖必须提前声明

  • CMD:引入 sea.js 编写模块化,特点是可以动态引入依赖

  • CommonJS:NodeJs 中的模块化,只在服务端适用,是同步加载

  • ES Modules:ES6 中新增的模块化,是目前的主流


本文前三种插件编写方式均属于利用函数自执行(IIFE)实现的插件,同时在向全局注入插件时兼容了 CommonJS 规范,但并未兼容 AMD CMD,是因为目前基本没有项目会使用到这两种模块化。

自动化 API 文档

一个 JS 插件如果没有一份文档,如同一台精密的仪器没有说明书。当别人使用你的插件时,他不可能去查看源码才知道这个插件有哪些方法、用途如何、要传哪些参数等。


所以这里我们使用 JSDoc 来创建 API 文档,它使用简单,只需要在代码中编写规范的注释,即能根据注释自动生成文档,一举多得,十分优雅!


npm install --save-dev jsdoc open
复制代码


修改 package.json,增加一条脚本命令:


......."scripts": {    ......    "doc": "jsdoc dist/main.es.js && node server.js"},
复制代码


根目录下创建文件 server.js


var open = require('open');open(`out/index.html`); // 这是apidoc默认生成的路径,这里只是为了自动打开网页
复制代码


好了,现在可以使用 npm run doc 命令来生成文档了,依然是举个栗子🌰,我们在 src 目录下添加一个文件 ArrayDelSome.js


/** * * @desc 对象数组去重 * @param {Array} arr * @param {String} 对象中相同的关键字(如id) * @return {Array} 返回新数组,eg: ArrayDelSome([{id: 1},{id: 2},{id: 1}], 'id') -> 返回: [{id: 1},{id: 2}] */function ArrayDelSome(arr, key) {  const map = new Map()  return arr.filter((x) => !map.has(x[key]) && map.set(x[key], true))}
export default ArrayDelSome
复制代码


本例只演示最基础的用法,JSDoc 有许多类型注释大家可以自行搜索学习下,不过本例最基本的这几个注释依旧是够用的。


运行 npm run doc,将会打开一个网页,可以查看我们刚写的工具函数:



注意:在生成文档前需要先进行过 rollup 的打包,且不能开启去注释之类的插件,因为上面的例子实际是对 dist/ 目录下的最终文件进行文档编译的。

发布插件

还没发布过 npm 包?参考这篇文章

私有源发布

如果你的公司有私域 npm 管理源,或者平时喜欢用淘宝源,推荐使用 nrm 进行切换:


npm i nrm -g
复制代码


  1. 查看源: nrm ls

  2. 添加源: nrm add name http//:xxx.xxx.xxx.xxx:4873/

  3. 删除源: nrm del name

  4. 使用指定源: nrm use npm

总结

功能较简单的 JS 插件我们可以直接采用前三种方式开发,如果涉及 DOM 操作较多,可以编写仿 JQ 的插件更好用,如果插件功能较多,有可能形成长期维护的大型插件,那么可以采用工程化的方式开发,方便多人协作,配套生成文档也利于维护。


以上就是文章的全部内容,希望对你有所帮助!如果觉得文章写得不错,可以点赞收藏,也欢迎关注,我会持续更新更多前端有用的知识与实用技巧,我是茶无味de一天,希望与你共同成长~

发布于: 刚刚阅读数: 5
用户头像

公众号:品味前端 2022.09.22 加入

一介前端,卖码为生。很惭愧,只希望在学习和分享的道路上能做一点微小的贡献。

评论

发布
暂无评论
如何优雅地编写一个高逼格的JS插件?_JavaScript_茶无味的一天_InfoQ写作社区