乐趣区

关于前端:如何优雅地编写一个高逼格的JS插件

在一个风和日丽的晚上,我正悠闲地喝着 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 = null
aFn.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.js
import main from './src/main.js';

console.log(main);
// src/main.js
export 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 [email protected] 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 一天,心愿与你独特成长~

退出移动版