如何快速创建一个高质量的 js 库/组件

243 阅读4分钟

引言

在从事的前端工作中,我们会引入各种各样的第三方 js 库来提高工作效率。如果我们使用 react 框架,可能还会需要阿里出品的 UI - antd,用于处理时间的 moment,用于管理全局状态的 redux,用于提升性能的 immutable-js...等等。

虽然现有的库很多,但是当需要完成某些特定的功能并在多个项目中应用时,我们还是要自己动手封装。

本文将笔者自己快速创建一个高质量 js 库/组件的经验分享给大家,希望能有所帮助。

首先,笔者认为一个高质量的库应该满足以下条件:

  1. 文档完善的。需要让使用者以及后面维护的人能快速的上手
  2. 自测试通过的。库源代码有自己的测试用例,并且有较高的覆盖率
  3. 支持 tree-shaking。如果库将在客户端使用,我认为支持 tree-shaking 是有效提升网页性能的一个方式,因为它能减小 bundle 的大小

在达成这个共识之后,接下来笔者将带着大家一步一步创建一个组件 - react-input 组件

示例 react-input

目录:

  1. 需求描述
  2. 初始化库代码
  3. 编写代码逻辑
  4. 编写测试用例

需求描述

基于 react,笔者希望 react-input 组件能做到以下几点:

  1. 实现自定义格式化。比如:在某种场景下,开发者会希望 input 元素中只能输入中国手机号
  2. 实现自动检查合法性。比如:开发者会希望 input 失焦时,自动检查输入的值得合法性

初始化库代码

使用 cli 工具: livelybone/npm-module-generator

如果 react-input 存在,运行:

cd react-input

# 运行下面的命令之前,请将 react-input 文件夹内部的文件清空并备份
npx @livelybone/npm-module-generator --react-ts

如果 react-input 不存在,运行:

npx @livelybone/npm-module-generator react-input --react-ts

运行后出现下面的窗口:

按照引导填写完信息之后,cli 会自动初始化 git, 并运行 npm i,如下:

npm i 如果安装比较慢,建议 使用 control + c 停止之后,再使用淘宝源安装:

cd react-input
npm i --registry=http://registry.npm.taobao.org

成功初始化 react-input 的目录,如下:

初始化之后,代码库特点:

  1. 使用 typescript 编写,并在将来的打包中可以使用 npm run build:dts 生成 index.d.ts 声明文件,支持 typescript 项目对它的使用
  2. 使用 eslint + prettier + husky + lint-staged 自动管理代码风格。你只需要关心代码逻辑的编写即可,在你提交 commit 之前会自动格式化代码
  3. 代码库将使用 rollup 打包编译,不用修改配置就能实现打包
  4. 支持 tree-shaking。rollup 打包结果会生成一个 es module 版本和 umd 版本,es module 版本可支持 tree-shaking
  5. 支持开发模式。使用 npm run dev 进入开发模式,通过 http://127.0.0.1:3000/examples/test.html 访问 demo,组件代码改动会做即时的编译,刷新页面就能看到相应的改动
  6. 支持测试,并支持覆盖率文档生成。编写好测试用例(/test/index.spec.js)之后,就可以运行 npm run test 开始测试,测试完成后将自动生成覆盖率文档
  7. README.md 文档范例化。只要根据现有 README 文档做相应的补充和修改,就能等到一个较为清晰可用的使用文档

编写代码逻辑

  1. 实现自定义格式化。
class ReactInput extends React.Component {
  function onChange(ev) {
    const { preFormatter, onChange } = this.props
    const oldVal = this.value
    if (preFormatter) {
      // 每次输入都对 input 的值做格式化,并重新赋值
      ev.target.value = preFormatter(ev.target.value)
    }
    if (oldVal !== ev.target.value) {
      if (onChange) onChange(ev)
    }
  }

  render() {
    return <input onChange={onChange}>
  }
}

通过 preFormatter 来控制格式化的输出,就能实现自定义格式化

e.g. 实现只能输入中国手机号:

function preFormatter(value) {
    const sample = '18888888888'
    const phoneReg = /^1[3-9]\d{9}$/
    let val = value
    let phoneNum = val + sample.substr(val.length)

    while(!phoneReg.test(phoneNum) && val !== '') {
        val = val.substr(0, val.length - 1)
        phoneNum = val + sample.substr(val.length)
    }

    return val
}

// 将 preFormatter 作为 props 传入: <ReactInput preFormatter={preFormatter} />
// 
// 输入:'1', 得到:'1'
// 输入:'2', 得到:''
// 输入:'123', 得到:'1'
// 输入:'132', 得到:'132'
  1. 实现自动检查合法性
class ReactInput extends React.Component {
  function validator(val) {
    const { validator, onCheck } = this.props
    this.pristine = false
    this.valid = validator ? validator(val) : true
    this.value = val

    if (onCheck) {
      const { pristine, valid } = this
      onCheck({ pristine, valid })
    }
  }

  function onBlur(ev) {
    const { onBlur } = this.props
    this.validator(ev.target.value)
    if (onBlur) onBlur(ev)
  }

  render() {
    return <input onBlur={onBlur}>
  }
}

通过 validator 检验值的合法性,并通过 onCheck 属性通知父组件

e.g. 检查手机号的合法性

function validator(val) {
    const phoneReg = /^1[3-9]\d{9}$/
    return phoneReg.test(val)
}

function onCheck(...args) {
    console.log(...args)
}
// 将 validator 作为 props 传入: <ReactInput validator={validator} onCheck={onCheck} />
// 
// 输入:1 -> 失焦:{ pristine: false, valid: false }
// 输入:18888888888 -> 失焦:{ pristine: false, valid: true }

编写测试用例

如下:

describe('index', () => {
  let container

  beforeEach(() => {
    container = document.createElement('div')
    document.body.appendChild(container)
  })

  afterEach(() => {
    document.body.removeChild(container)
    container = null
  })

  it('render success', () => {
    act(() => {
      ReactDOM.render(ReactInput(), container)
    })
    expect(container.querySelector('input')).to.not.equal(null)
  })

  it('other test', () => {
    // ...
  })
})

运行 npm run test,得到测试结果和测试覆盖率,通过修改、增加测试用例,能提高测试覆盖率

这样我们就开发了一个完整可复用的组件 react-input. 源代码请看 livelybone/react-input

结束

上面具体讲述了如何快速开发一个组件,接下来的问题就是如何做到多处复用,一般有三种方式:

  1. 建立 CDN,在 html 中直接引入打包之后的 umd 文件
  2. 发布到 npm,然后通过 npm i your-module-name 的安装方式引入
  3. 上传到 git 远程仓库,让后通过 npm i git+ssh://git@[origin]:xxx/xxx.git[#tagName] 的方式引入