开源库架构实战——从0到1搭建属于你本身的开源库

需求分析

最近在 H5 开发与 APP 客户端工程师的联调过程当中, 常常须要实现一些经常使用的移动端事件封装成接口提供给客户端,例如用户的单击 tap 事件、双击事件、长按事件以及拖动事件。但因为浏览器默认只提供了 touchstarttouchmovetouchend 三个原生事件,在实际的开发过程当中,咱们经常使用的解决方案即是经过监听touchstarttouchend 事件配合定时器来实现咱们的自定义移动端事件,为了实现经常使用自定义事件的复用,咱们对其进行了封装,并提供方便用户使用的工具函数,这也是咱们实现 mt-events 的初衷。javascript

mt-events 全名是 Mobile Terminal Events。最初咱们对这个库的定位是但愿封装一些经常使用的移动端事件来方便用户进行更为便捷的移动端开发,例如双击事件、长按事件、滑动事件等等。后来,随着项目的迭代,mt-events 的功能更倾向往前端事件绑定工具的趋势发展,由于咱们集成了事件委托等,您能够像使用 JQuery 的 on 方法那样使用咱们的 mt-events,更加便捷事件绑定和委托,让移动端事件如原生事件般友好。 这是咱们项目的 Github 地址:github.com/jerryOnlyZR…html

接下来,咱们将带您体验一款工具库的搭建流程,ES6 新特性 Map、Proxy、Reflect 以及 WeakMap 在咱们的工具库中发挥的做用,以及咱们开源的工具库mt-events所拥有的魅力。前端


mt-events 初探

先看看 mt-events 这款工具库具备哪些特性:vue

  • 广泛性:封装经常使用的移动端事件
    • 单击
    • 双击
    • 长按
    • 滑动
    • 拖拽
    • 缩放
    • 旋转
  • 便捷性:在全局挂载工具函数,绑定事件如 $.on() 般丝滑;封装事件代理,只须要像 JQuery 那样传入被代理元素的选择器便可。
  • 新语法:使用 Map 映射事件回调,便捷事件移除;使用 WeakMap 实现 DOM 元素与回调的弱引用,预防内存泄漏。
  • 轻量级:代码压缩 + gzip,只有大约 2KB

那么,咱们又该如何使用它呢?这里提供了两种引用工具库的方式,最经常使用的固然是从 HTML 里使用 script 引入:java

<script src="http://mtevents.jerryonlyzrj.com/mtevents.min.js"></script>
复制代码

而后,咱们的工具函数 mtEvents 将会被挂载在 window 对象上,您能够在浏览器的开发者工具里的 console 面板输入并执行 mtEvents,若是打印出以下文本说明您已经成功引入咱们的工具库了:node

mtEvents-console

或者您是 VUE 等前端框架的开发者,您也能够经过 npm 依赖的方式引入咱们的工具,咱们的工具库会跟随您们的 VUE 文件被打包进 bundle 里。webpack

首先,将咱们的工具库以上线依赖的形式安装:nginx

npm i mt-events --save
复制代码

而后就能够在咱们的 .vue 等文件里直接引入使用:git

//test.vue
<script> const mtEvents = require('mt-events') export default { ..., mounted(){ mtEvents('#bindTarget', 'click', e => console.log('click')) } } </script>
复制代码

具体的使用方法,您能够参照咱们 Github 为您提供的用户文档哦~github

如何搭建一款属于咱们本身的开源库

选择一款合适的测试工具

​ 没有通过测试的代码不具有任何说服性。相信你们在浏览别人开源的工具库代码时,都能在根目录下见到一个名为 test 的文件夹,其中就放置着项目的测试文件。特别对于工具库来讲,测试更是一个不可或缺的环节。

​ 市面上的测试工具种类繁多,例如 Jest,Karma,Mocha,Tape等,并不须要局限与哪一款,下面咱们对这几种框架进行了一些对比。

  • Jest
    • facebook 开源的 js 单元测试框架
    • 集成 JSDOM,mt-events 库主要适用于移动端,集成 JSDOM 可以让咱们更好地去模拟移动端事件
    • 基于 Istanbul 的测试覆盖率工具,会在项目下生产一个 coverage 目录,内附一个优雅的测试覆盖率报告,让咱们能够清晰看到优雅的测试状况
    • 开箱即用,配置不多,只须要 npm 命令安装便可运行,UI 层面清晰,并且操做简单
    • 基于并行测试多文件,在大项目中的运行速度很快
    • 内置 Jasmine 语法,以及添加了不少新特性
    • 内置 auto mock,自带 mock API
    • 支持断言和仿真,不须要引入第三方断言库
    • 在隔离环境下测试,支持快照测试
    • 较多用于 React 项目(但普遍支持各类项目)
    • 比较新,社区还不是很成熟
  • Karma
    • Google Angular 团队开源的 JavaScript测试执行过程管理工具
    • 配置简单方便
    • 强大适配器,能够在 karma 上面配置 jasmine,mocha 等单元测试框架
    • 可提供真实的模拟环境,能够在 chrome,firefox 等各类浏览器环境进行配置
    • 开发者能够本身把控整个自动化测试流程,实现更加自动化,当咱们编辑保存的时候,便可运行所有的测试用例
    • 高扩展性,支持插件开发
    • 支持 ci 服务
    • 执行速度快
    • 支持远程控制以及支持调试
  • Mocha
    • 学习成本比较高,但随之带来的是它能提供更好的灵活性和可扩展性
    • 社区成熟,在社区上能够找到各类的特殊场景下可用的插件或者扩展
    • 须要较多的配置,配置相对比较麻烦
    • 自身集成度不高,须要和第三方库结合(一般是 Enzyme 和 Chai)才能有断言、mocks、spies 的功能
    • 默认建立全局的测试结构
    • 终端显示友好
    • 目前使用最普遍的库
  • Tape
    • 开发者只须要用 node 执行一个 js 脚本,直接调用 API 便可
    • 最精简,体积最小,提供简单的结构和断言
    • 只提供最底层最基础的 API
    • 没有定义全局变量,开发者能够随意更改测试代码
    • 不须要 CLI 客户端环境,只须要可以运行 js 环境,便可运行 Tape

综上所述,Jest 开箱即用;若须要为大型项目配备足以快速上手的框架,建议使用Karma;Mocha 用的人最多,社区最成熟,灵活,可配置性强易拓展;Tape 最精简,提供最基础的东西最底层的API。

下面咱们举个例子如何使用 Jest

  • 安装Jest
$ npm i jest -D
复制代码
  • 添加配置文件:
// jest.config.js # 在 jest.config.js 配置测试用例路径,以及覆盖率输出文档的目录等等信息
module.exports = {
    testURL: 'http://localhost',
    testMatch: ['<rootDir>/test/*.js'],             // 测试文件匹配路径(拿到根目录下test文件夹里的全部JS文件)
    coverageDirectory: '<rootDir>/test/coverage',   // 测试覆盖率文档生成路径
    coverageThreshold: {			    // 测试覆盖率经过阈值
        global: {
            branches: 90,
            functions: 90,
            lines: 90,
            statements: 90
        }
    }
}
复制代码
  • 配置package测试scripts
// package.json
{
    ...,
    "scripts": {
        ...,
        "test": "jest"
    }
}
复制代码
  • 编写测试用例
// test/index.js                              # 编写测试用例
describe('test dbtap events', () => {         # 测试组
	test('test 1+1', () => {	      # 测试用例
        expect(1+1).toBe(2)           	      # 断言
	})
})
复制代码
  • 执行测试
$ npm t
复制代码
  • 结果输出

test-result

这就是配置测试的基本流程。

使用 eslint 规范团队代码

在团队开发的工做中,代码维护所占的时间比重每每大于新功能的开发。所以制定符合团队的代码规范是相当重要的,这样不只仅能够很大程度地避免基本语法错误,也保证了代码的可读性,方便维护。

程序是写给人读的,只是偶尔让计算机执行一下。 --Donald Knuth

众所周知,eslint 是一个开源的 JavaScript 代码检查工具,能够用来校验咱们的代码,给代码定义一个规范,团队成员按照这个代码规范进行开发,这保证了代码的规范。使用 eslint 能够带来不少好处,能够帮助咱们避免一些低级错误,可能一个小小的语法问题,让您定位了好久才发现问题所在,并且在团队合做的过程当中,能够保证你们都按照同一种风格去开发,这样更方便你们看懂彼此的代码,提升开发效率。

另外,eslint 的初衷是为了让开发者建立本身的代码检测规则,使其能够在编码过程当中发现问题,扩展性强。为了方便使用,eslint 也内置了一些规则,也能够在这基础上去增长自定义规则。

eslint --init  
复制代码

选择您最熟悉的构建工具

bundle-tools

在开发阶段咱们常常会使用一些语法糖像ES6的新特性来方便咱们的开发,或者 ES6 Modules 来衔接咱们的模块化工做,可是有些新特性是 Node.js 或者浏览器还未能支持的,因此咱们须要对开发代码进行编译及打包,为了提炼自动化工程,咱们能够选择许多优良的自动化构建工具,例如前端巨头 Webpack,或是流式构建工具 Gulp,亦或是具备优良 Tree-shaking 特性的Rollup,每款构建工具都有本身的闪光点,咱们能够根据业务需求选择最合适的构建工具。 构建工具作的事情就是将一系列流程用代码去实现,自动化地去执行一系列复杂的操做,最终实现将源代码转换成能够执行的 JavaScript、CSS、HTML 代码。构建工具层出不穷,例如 Grunt,Gulp,Webpack,Rollup 等等。下面咱们对这几种工具进行一些对比。

  • Grunt
    • Grunt 有大量可复用的插件,封装成经常使用的构建任务
    • 灵活性高
    • 集成度不高,配置麻烦,没法作到开箱即用
    • 至关于 Npm scripts 的进化版
    • 模式与Webpack相似,随着Webpack的不断迭代以及功能的完善,能够优先考虑使用Webpack
  • Gulp
    • 基于流的自动化构建工具
    • 能够管理任务和执行任务
    • 能够监听文件的变化以及读写文件,流式处理任务
    • 能够搭配其余工具一块儿使用
    • 集成度不高,配置麻烦,没法作到开箱即用
  • Webpack
    • 一款打包模块化的 JavaScript 工具
    • 经过 loader 转换文件,经过 Plugin 注入钩子,最后输出由多个模块组合成的文件。
    • 专一处理模块化的项目,不适用于非模块化项目
    • 丰富完整,同时也可经过 Plugin 扩展
    • 开箱即用,开发体验不错
    • 社区成熟活跃,能够在社区中找到各类特殊场景的插件扩展
  • Rollup
    • 相似 webpack 但专一于 ES6 模块的打包工具
    • 针对 ES6 源码进行 Tree Shaking,移除只被定义但没有被使用的代码
    • 针对 ES6 源码进行 Scope Hoisting,以减小输出文件的大小和提高运行性能
    • 配置和使用简单,但不如 webpack 那么完善
    • 社区生态链还不够成熟,不少特殊场景下没法找到解决方案

咱们的 mt-events 项目选择了 Rollup 和 Webpack 两款构建工具是由于咱们须要对“同构”后的JS代码裁剪分支,所以咱们须要利用 Rollup 优良的 Tree-shaking 特性;而且为了上线 min.js 文件的压缩打包,咱们使用 Webpack 来方便咱们的构建工做。

配置 JSDoc 为后来之人扫清障碍

​ 项目的维护工做是延伸项目生命周期的最关键手段,阅读别人的源码相信对你们来讲都是一件费力的事情,特别是当原做者不在您身边或者没法给您提供任何信息的时候,那就更是悲从中来。因此,书写完善的注释是开发过程当中须要养成的良好习惯。为了提高代码的可维护性,咱们都会在主干代码上完善咱们的注释,而且,市面上有一款工具,它可以自动将咱们的注释转化成 API 文档,生成可视化页面,听起来是很神奇吧,先别着急,听我娓娓道来。

​ 这款工具名为 JSDoc,它是一款根据 Javascript 文件中注释信息,生成 JavaScript 应用程序或库、模块的 API 文档的工具。JSDoc 分析的源代码是咱们书写的符合 Docblock 格式的代码注释,它会智能帮咱们生成美观的 API 文档页面,咱们要作的,只是简单的跑一句jsdoc命令就能够了。

下面是 mt-events 的 API 文档页面(很美观不是吗?这些都是JSDoc自动生成的):

mtEvents-docs

​ 简约的风格让人看起来心旷神怡,想一想若是有后来的维护者想要快速了解您的项目的大致架构和具体方法的功能,献上这样一份开发者文档可不是要比直接丢给他一份源代码要来的好得多对吧。

使用 Git 钩子对提交的代码进行 lint 和测试

为了确保线上的代码不被污染,咱们配置了eslint,因此在团队里每位成员push代码以前,都须要进行一次lint和test,这样才能确保线上代码的整洁性和有效性,可是这一繁琐的工做可否自动化去完成呢?

解决方案就是使用git钩子来实现自动化lint和test:

  1. 首先,咱们先安装git的钩子管理工具——husky:

    npm install -D husky
    复制代码
  2. 接着,在咱们项目的package.json里添加咱们的钩子命令:

    在mt-events项目里,咱们在commit钩子上执行lint,在push钩子上执行test,配置以下:

    {
      ...,
      "scripts": {
        ...,
        "precommit": "lint-staged",
        "prepush": "npm t"
      }
    }
    复制代码
  3. 优化 :只lint改动的文件—— lint-staged

    每次钩子执行lint都须要遍历全部的文件,不免拉低效率,lint-staged这款插件就优化了这一个问题,自带diff的特性让eslint只去lint变动文件,您只须要在package.json里添加相关配置便可。mt-events示例:

    首先,先安装lint-staged依赖:

    npm install -D lint-staged
    复制代码

    接着,在package里添加相应配置:

    {
      ...,
      "lint-staged": {
        "core/*.js": [ 						// lint监听的变动文件
          "prettier --write",				// 自动格式化全部代码
          "eslint --fix",					// lint并自动修改不符合规范的代码
          "git add"						    // 将全部的修改添加进暂存池
        ]
      }
    }
    复制代码

    配上钩子以后,咱们就能看到这样的额输出结果了:

    • commit钩子

    commit-husky

    • push钩子

    push-husky

让持续集成工具帮您实现自动化部署

每次咱们在本地跑完构建生成了上线文件以后,咱们都须要经过scp或者rsync等方式上传到咱们的服务器上,每次若是都须要手动执行相关命令完成上线操做确定是违背了咱们工程自动化的思想,为了实现自动化部署,咱们可使用持续集成工具来协助咱们完成上线操做。

市面上成熟的持续集成工具也很多,可是口碑最盛的也当属 Travis CIJenkins 了。做为Github的标配,Travis CI 在开源领域有着不可颠覆的地位,若是咱们是在Github上对项目进行版本控制管理,选择这款工具天然再合适不过了。Jenkins由于内容较多,这里就不作过多介绍了,本文的重点,主要是谈谈Travis CI在咱们的自动化工程中该如何运用。

ci-tools

Travis CI 的特性:
  • Travis CI 提供的是持续集成服务,它仅支持 Github,不支持其余代码托管。
  • 它须要绑定 Github 上面的项目,还须要该项目含有构建或者测试脚本。
  • 只要有新的代码,就会自动抓取。而后,提供一个虚拟机环境,执行测试,完成构建,还能部署到服务器。
  • 只要代码有变动,就自动运行构建和测试,反馈运行结果。确保符合预期之后,再将新代码集成到主干。
  • 每次代码的小幅变动,就能看到运行结果,从而不断累积小的变动,而不是在开发周期结束时,一会儿合并一大块代码,这大大提升了开发 mt-events 库的效率,只要一更新,用户便可拉取到最新的 js 代码。这就是增量上线。

其实Travis CI的使用方法能够简单的归纳为3步,就像官网首页的那样图片介绍的同样:

travis-guide

  1. 在 Travis CI 的仪表盘里勾选您须要持续集成的项目
  2. 在您的项目根目录下添加一个名为 .travis.yml 的配置文件
  3. 最后您要作的,就是 push 您的代码,而后静观其变

其实难点也就是 .travis.yml 配置文件的书写和具体持续集成的梳理的,先 po 一张咱们项目的配置文件:

language: node_js               # 项目语言,node 项目就按照这种写法就OK了
node_js:
- 8.11.2 			# 项目环境
cache:				# 缓存 node_js 依赖,提高第二次构建的效率
 directories:
 - node_modules
before_install:                 # 这些是咱们加密密钥后自动生成,两行命令的做用就是获得一个有效密钥
- openssl aes-256-cbc -K $encrypted_81d1fc7fdfa5_key -iv $encrypted_81d1fc7fdfa5_iv
 -in mtevents_travis_key.enc -out mtevents_travis_key -d
- chmod 600 mtevents_travis_key
after_success:			# 构建成功后的自定义操做
- npm run codecov		# 生成 Github 首页的 codecov 图标
- scp -i mtevents_travis_key -P $DEPLOY_PORT -o stricthostkeychecking=no -r dist/mtevents.min.js
  $DEPLOY_USER@$DEPLOY_HOST:/usr/local/nginx/html   		#将生成的上线文件 scp 到服务器
复制代码

先梳理一下持续集成的流程,首先,咱们更新开源项目而后 push,Travis 会监听到咱们的 push 操做并自动拉取项目代码到 Travis 的虚拟机上,执行构建流程。思路就是这样,其实咱们使用 Shelljs 也能实现一个简单的持续集成工具。

一般,咱们在CI大型项目例如网站、Web APP 之类的项目时,更多地会使用 rsync 命令代替咱们暴力的 scp,由于 scp 会上传全部的文件,而 rsync 自带 diff 功能,因此功能如其名,它的做用就是“同步”变动文件,这样能极大提高咱们的CI效率。可是因为咱们的工具库项目只有一个 min.js 文件,因此 scp 就已经足够解决问题了。

为您的项目添加开源许可证

每一个开源项目都须要配置一份合适的开源许可证来告知全部浏览过咱们的项目的用户他们拥有哪些权限,具体许可证的选取能够参照阮一峰前辈绘制的这张图表:

licenses

那咱们又该怎样为咱们的项目添加许可证了?其实 Github 已经为咱们提供了很是简便的可视化操做: ​ 咱们平时在逛 github 网站的时候,发现很多项目都在 README.md 中添加徽标,对项目进行标记和说明,这些小图标给项目增色很多,不只简单美观,并且还包含清晰易懂的信息。

  1. 打开咱们的开源项目并切换至 Insights 面板
  2. 点击 Community 标签
  3. 若是您的项目没有添加 License,在 Checklist 里会提示您添加许可证,点击 Add 按钮就进入可视化操做流程了

添加一些您喜欢的 Icon 来修饰您的项目吧

​ 当咱们花费了不少精力去构建完善咱们的项目后,但愿有更多的人来关注以及使用咱们的项目。此时咱们如何更好地向其余人展现本身的项目呢?给本身的项目添加一些好看的徽标是一种不错的选择,让人耳目一新。

​ 点开 mt-events 的README文件,您能够看到在开头部分有不少漂亮的小图标,不少大型项目都会使用这些小图标来装饰本身的项目,既能展现项目的一些主要信息,也能体现项目的专业性。

readme-icons

​ 那么,咱们又该如何为咱们本身的开源项目添加这样的小图标呢?GitHub 小图标的官方网站是 shields.io/ ,能够在上面选择喜欢的徽标来为本身的项目润色,常见的徽标主要有持续集成状态,代码测试覆盖率,项目版本信息,项目下载量,开源协议类型,项目语言等,下面根据咱们项目简单罗列几个图标讲一讲如何生成。

add-licenses

  • 持续集成状态

    • 持续集成按照前面的模块推荐使用 Travis CI,在项目中添加一个 .travis.yml 配置文件,告诉 Travis CI 怎样对您的项目进行编译或测试,具体配置关注上一个模块。

    • 而后徽标图片地址是

      http://img.shields.io/travis/{GitHub 用户名}/{项目名称}.svg
      复制代码

      将上面 URL 中的 {GitHub 用户名} 和 {项目名称} 替换为本身项目的便可,最后能够将集成完成后的 markdown 代码贴在本身的项目上

    • 效果图是:

      Travis (.org)

  • 测试覆盖率

辛辛苦苦把项目的测试覆盖率提升到了100%,不把它show出来确定很憋屈吧。若是您但愿在您的Github上添加项目测试覆盖率小图标,这里咱们推荐使用 codecov 这套解决方案(图片来自官网截图)。

codecov-index

您要作的,只是像在Travis CI里添加项目那样把您须要跑收集测试覆盖率的项目添加进codecov的仪表盘,而后在您的项目里安装codecov依赖:

$ npm install codecov --save-dev
复制代码

codecov的原理就是在您执行完项目测试以后,它会自动去寻找并收集项目内的测试覆盖率文档,而后呈如今页面上,并生成小图标,因此,您只要在项目测试以后执行codecov命令就好了。由于咱们的codedev是安装在本地,因此咱们须要进入package.json内配置一下咱们的codecov执行命令:

// package.json
{
    ...,
    "scripts": {
        ...,
        "codecov": "codecov"
    }
}
复制代码

如今,您终于知道咱们的.travis.yml配置文件里的npm run codecov是作什么用了的吧~

codecov

接下来,就能够把咱们的效果图添加进Github首页了。

  • 项目版本信息

    • 项目版本信息,是根据不一样的发布工具来制定的。shields.io/#/examples/… 在这个网站上能够找到不一样的发布工具的徽标图片地址。

    • 这里以咱们的库作示例,以 npm 方式发布出去的,因此图标的地址就是:

      https://img.shields.io/npm/v/{项目名称}.svg
      复制代码
    • 效果图是:

      npm

  • 项目下载量

    • 项目被下载的次数,是根据不一样的平台独立统计的。shields.io/#/examples/… 在这个网站上能够找到各类统计平台的徽标图片地址。

    • 这里以咱们的库作示例,以 npm 方式发布出去的,且以每周下载量的维度来看:

      https://img.shields.io/npm/dw/{您的项目}.svg 
      复制代码
    • 效果图是:

      npm

mt-events从0到1

目录结构

mt-events
├── core                   # 源代码文件夹
│   ├── event.js           # 自定义事件处理句柄生成器,包含长按,双击,滑动,拖拽事件
│   ├── index.js           # mtEvents 类以及绑定,移除事件方法
│   ├── proxy.js           # 事件代理 Proxy 生成器
│   ├── touch.js           # 模拟浏览器原生 touch 事件,供test使用,未对外发布
│   ├── weakmap.js         # 创建用户定义回调与事件绑定元素的弱引用,预防内存泄漏
├── dist
│   ├── mtevents.min.js    # mt-events 工具库最终生成的 JS 上线压缩文件
├── docs                   
│   ├── developer          # 为开发者提供的mt-events开发文档,使用命令`$npm run docs`便可生成
│   ├── user               # 为用户提供的mt-events的中英文使用文档
├── lib                    # 上线待构建代码临时文件夹
│   ├── event.js           
│   ├── index-Browser.js   # 上线压缩JS源文件
│   ├── index-npm.js       # npm package入口文件
│   ├── proxy.js                    
│   ├── weakmap.js                  
├── test
│   ├── coverage           # 测试覆盖率参考文件
│   ├── index.js           # 测试用例
├── .travis.yml            # Travis-ci配置文件
├── jest.config.js         # Jest 配置文件
├── package.json           
├── rollup.config.js       # rollup 配置文件
├── webpack.config.js      # webpack配置文件 
复制代码

这是一份平平无奇的项目目录,你们必定能看到不少熟悉的字眼,咱们都对其中的文件的用途进行了解释说明,具体关键细节和重点,咱们会在后文中提炼出来。

工程化实践

images

工具选型

构建: webpack4 Rollup
测试工具: Jest
持续集成: Travis CI 
API 文档生成工具: JSDoc
代码规范: eslint  prettier  lint-staged
项目版本控制工具: git
复制代码
JavaScript 模块打包器 Rollup

​ Rollup 已被许多主流的 JavaScript 库使用,它对代码模块使用新的标准化格式,这些标准都包含在 JavaScript 的 ES6 版本中,这可让您自由无缝地使用您须要的 lib 中最有用的独立函数。Rollup 还帮助 mt-events实现了简单的“同构”,经过区分用户的引用方式,咱们将上线文件区分为 index-npm.js 和 index-Browser.js 文件,既能够经过 script 在 HTML 引入,也可使用 npm 方式 require 依赖。

​ 除了使用 ES6 模块,Rollup 独树一帜的 Tree Shaking 特性,能够静态分析导入模块,移除冗余,帮助咱们完成了代码无用分支的裁剪:

// index.js 
if (process.env.PLATFORM === 'Browser') {
  window.mtEvents = mtEventsFun
} else {
  module.exports = mtEventsFun
}
// rollup.config.js 
export default {
    entry: './core/index.js',
    output: {
        file: `lib/index-${platform}.js`,
        format: 'cjs'
    },
    plugins: [
        replace({
            "process.env.PLATFORM": JSON.stringify(platform)
        }),
        copy({
            './core/events.js': 'lib/events.js',
            './core/proxy.js': 'lib/proxy.js',
            './core/weakmap.js': 'lib/weakmap.js',
        })
    ]
};

// package.json 根据传入的参数生成对应的 index-npm.js 和 index-Browser.js 文件
// 在相应的 index-${platform}.js 文件移除没用到的代码
{
    "build:browser": "rollup --config --platform Browser",
    "build:npm": "rollup --config --platform npm"
}

// index-npm.js
{
  module.exports = mtEventsFun;
}

// index-Browser.js
{
  window.mtEvents = mtEventsFun;
}
复制代码
单元测试工具 Jest

​ 随着项目迭代的过程,依赖人工去回归测试容易出错和遗漏,为了保证 mt-events 库的质量,以及实现自动化测试,咱们引入了 Jest,由于它集成了 JSDOM,用它模拟咱们的事件库在浏览器环境中执行的效果再合适不过了。而且 Jest 容易上手,开箱即用,几乎零配置,功能全面。

​ 可是在测试的开始阶段就遇到了一个问题,在浏览器原生移动端事件中,并无一个像 click() 那样的方法能够供咱们直接调用来模拟事件触发,这个问题又该如何解决呢?

​ 利用挂载在全局的 TouchEvent 构造函数,咱们尝试着建立用户的 touch 事件,最终实践证实,这个方法可行,下方即是咱们模拟touch事件的核心代码:

// touch.js
createTouchEvent (type) {
    return new window.TouchEvent(type, {
        bubbles: true,
        cancelable: true
    })
}
dispatchTouchEvent (eventTarget, event) {
    if (typeof eventTarget === 'string') {
        eventTarget = document.querySelector(eventTarget)
    }
    eventTarget.dispatchEvent(event)
    return eventTarget
}
复制代码

​ 下面是咱们使用 Jest 测试代码的覆盖率及结果:

mtEvents-test

持续集成

根据前文提到的配置,咱们就能够在Travis CI首页看到咱们的项目的持续集成结果:

travis-result

线上的min.js文件也同时被更新到最新的版本了。

源码剖析

mt-events 源码都是按照 ES6 代码规范来写,下面从几个方面来体验 mt-events 源码的魅力:

一个既是 Function 又是 Object 的工具函数

​ 如此奇葩的数据类型看起来彷佛很陌生,但我敢保证您以前必定有见过,只是没注意到它罢了,并且是多年之前咱们最常常打交道的老朋友。还记得 JQuery 里面的$符号嘛?您必定用过这种写法去获取元素 $("#myDom"),也用过挂在 $ 上的 ajax 方法来发送请求就像这样:$.ajax(...),是否是被我这么一说突然发现,以前最经常使用的 $ 竟然既是个函数又是个对象,不多见这样的状况对吧,其实实现原理很简单,只须要把类实例的原型挂载到 Function 上就搞定了,之因此这么作,是为了让用户绑定事件时,直接使用mtEvents这个 Function 就能够了,就不须要再去拿到 mtEvents 上的 bind 方法了,可以优化体验。具体实现代码以下:

// index.js
let mtEvents = new MTEvents()
const mtEventsPrototype = Object.create(MTEvents.prototype)
const mtEventsFun = mtEvents.bind.bind(mtEvents)
Object.setPrototypeOf(mtEventsFun, mtEventsPrototype)
Object.keys(mtEvents).map(keyItem => {
  mtEventsFun[keyItem] = mtEvents[keyItem]
})
复制代码

mtEvents-bind

移除事件时须要传递指针,怎么让用户的回调和咱们绑定在元素上的事件回调造成映射?

​ 在自定义事件中,咱们是经过同时监听 touchstarttouchend 两个事件来判断用户触发的事件类型,而且在指定的位置执行用户传入的回调。那么,当用户须要移除以前绑定的事件时,咱们又该如何处理呢?用户传入的确定是须要执行的回调,而不是咱们绑定在元素上的事件回调。

​ 这时候,咱们就须要对用户传入的执行回调和咱们绑定在事件监听上的回调创建映射关系了,这样咱们就能够依据用户传入的执行回调找到咱们所须要移除的事件绑定回调函数了。对于映射关系,咱们首先想到的确定就是对象了,可是在传统的 JS 里,对象的键只能是字符串,可是咱们须要让它是一个函数,这回就该想到咱们 ES6 里新增的数据类型 Map 了,他的键能够不限于字符串,正合我意。

​ 咱们定义 userCallback2Handler 为一个 Map,将用户自定义的 callback 与事件处理器 eventHandler 绑定起来,相应的 remove 的时候也是根据 callback 来进行移除事件绑定,自定义事件中也是同理。

用户移除 DOM 元素时忘了移除绑定的事件怎么办?让 WeakMap 弱引用和内存泄漏 Say goodbye!

WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。所以,只要所引用的对象的其余引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦再也不须要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 --摘自 阮一峰《ECMAScript 6 入门》

​ weakmap.js 的意义在于创建 DOM 元素与对应 callback 的弱引用,在移除 DOM 元素时绑定在该元素上的回调也会被 GC 回收,这样就能起到防止内存泄漏的做用。

// weakmap.js

/** * weakMapCreator WeakMap生成器 * @param {HTMLElement} htmlElement DOM元素 * @param {Function} callback 事件监听回调 * @return {WeakMap} WeakMap实例 */
function weakMapCreator (htmlElement, callback) {
    let weakMap = new WeakMap()
    weakMap.set(htmlElement, callback)
    return weakMap
}
复制代码

事件委托的代码每次绑定事件都得写一次,用 Proxy—Reflect 快速去重

​ 在开发的过程当中咱们发现,为了实现事件委托相关操做,咱们常常要书写重复的代码,为了下降代码的重复率,咱们想到了使用 ES6 里的 Proxy 和 Reflect 对事件回调进行代理,在这过程当中执行事件委托相关操做。

​ 在 proxy.js 源码中,定义了事件委托处理的方法:_delegateEvent,以及事件委托 Proxy 生成器:delegateProxyCreator,这样在执行事件监听回调时,通过咱们的事件委托 Proxy,进行相应的事件委托处理,这样不只能够大大减小代码重复率,使代码看起来更加精简美观,同时这样定位问题 bug 也变得简单不少,只须要从根源处去定位 bug 便可。

/** * _delegateEvent 事件代理处理 * @param {String(Selector) | HTMLElement} bindTarget 事件绑定元素 * @param {String(Selector)} delegateTarget 事件代理元素 * @param {Object} target 原生事件对象上的target对象,即(e.target) * @return {Object | null} 若是存在代理,则调用此方法,事件发生在代理对象上则返回代理对象 */
function _delegateEvent (bindTarget, delegateTarget, target) {
  if (!delegateTarget) return null
  const delegateTargets = new Set(document.querySelectorAll(delegateTarget))
  while (target !== bindTarget) {
    if (delegateTargets.has(target)) {
      return target
    } else {
      target = target.parentNode
    }
  }
  return null
}

/** * delegateProxyCreator 事件代理Proxy生成器 * @param {String(Selector) | HTMLElement} bindTarget 事件绑定元素 * @param {String(Selector)} delegateTarget 事件代理元素 * @param {Object} target 原生事件对象 * @param {Function} callback proxy拦截回调 * @return {Function} 过Proxy的callback */
function delegateProxyCreator (bindTarget, delegateTarget, e, callback) {
  const handler = {
    apply (callback, ctx, args) {
      const target = _delegateEvent(bindTarget, delegateTarget, e.target)
      if ((delegateTarget && target) || !delegateTarget) {
        return Reflect.apply(...arguments)
      }
    }
  }
  return new Proxy(callback, handler)
}
复制代码
相关文章
相关标签/搜索