非大厂的我们,如何去搞Vue/React Hooks和Utils的企业开源工具库?

lxf2023-02-17 01:51:59

作者:易师傅 、github

声明:文章为稀土AdminJS技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

大家好,我是易师傅,一个专门搞前端的搬(touch)砖(fish)师傅 ~

在上两篇文章中《如何去搞基建》和《如何去搞前端团队规范》中主要给大家介绍前端基建前端规范的理论篇;

下面就给大家带来代码实战篇 — 《如何去搞企业工具库》;

至于为什么搞工具库,可以看看 前端基建这里的介绍

长话短说,咱们直接开干 ~

快速体验:

  • 工具库模板地址:链接

  • Github 开源地址:链接

一、初始化项目

因为我们的目的是想做成一个 monorepo 仓库,而为什么用 monorepo 仓库,一句话解释就是想把 多个项目 放在 一个仓库中 管理,不懂的可自行搜索或参考《现代前端工程为什么越来越离不开 Monorepo》;

而使用 monorepo 仓库使用较多的一般就是yarn workspace 和 lerna 包管理工具 之类的,但是我们要使用的是 pnpm workspace ,至于为啥?

一句话解释就是:速度快,节省磁盘,安全性高

1. 初始化

pnpm init

2. 创建 pnpm-workspace.yaml

touch pnpm-workspace.yaml

3. 修改 pnpm-workspace.yaml

packages:
  - packages/*
  - playground
  - docs

4. 新增 packages/core 目录

mkdir packages/core

cd packages/core

pnpm init

5. 新增 typescript 依赖

pnpm i typescript @types/node -Dw 

# 初始化 
npx tsc --init

到这里一个简单的 pnpm monorepo 仓库就搭建的差不多了;

你的目录应该就是如下图这样子的(其中 LICENSE 和 README.md 为手动创建):

非大厂的我们,如何去搞Vue/React Hooks和Utils的企业开源工具库?

好家伙,一顿操作下来是不是很简单呢 ~

二、pnpm workspace 指南

1. 安装根目录依赖

pnpm 提供了 -w, --workspace-root 参数,可以将依赖包安装到工程的根目录下,作为所有 packages 的公共依赖。

pnpm i typescript -w 

2. 安装开发依赖,需加上 -D 参数

pnpm i typescript -Dw 

3. 单独给 packages/core 安装指定依赖

pnpm 提供了 --filter 参数;

可以对指定的 packages/core 进行操作,其中 --filter 跟着的参数是 <package_selector> 也就是初始化的 packages/corepackage.json 的 name 字段:

# 使用
pnpm add <package_name> --filter <package_selector>

# 例子
pnpm add typescript -D --filter @vmejs/core

4. 如何运行 packages/core 中的 scripts 脚本

因为我们想直接运行指定某个包 packages/* 下的某个脚本,那么你可以这么做:

# 运行 @vmejs/core 包中的 dev 命令
pnpm dev --filter @vmejs/core

# 运行 @vmejs/core 包中的 build 命令
pnpm build --filter @vmejs/core

5. 各个 packages/* 模块包间的相互依赖

在实际开发中,不可能只存在一个 packages/core 包,可能还有 packages/shared 等,那么我们如何在 packages/core 中依赖 packages/shared 呢?

直接运行:

pnpm install @vmejs/shared -r --filter @vmejs/core

但是安装后的包会带上具体版本,这里是不推荐的,所以需要我们手动更改 packages/core 目录 package.json 下的 "@vmejs/shared": "workspace:^1.0.0”"@vmejs/shared": "workspace:*",如下图

"@vmejs/shared": "workspace:*"

当然,pnpm workspace 的使用远不止于此,但是对于开发咱们的工具库足矣;

如若您想了解更多,可前往官网查看更多

三、配置 eslint + prettier + husky + commitlint

eslint + prettier代码质量代码风格 是我们必须要做的项任务,正所谓没有规矩不成方圆;

husky + commitlintgit hooks提交检测 我们也是必须要集成的;

如果你还有疑问,可以看看我的上一篇文章《前端规范都有哪些》;

1. 配置 eslint:

  1. 安装

    pnpm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -Dw
    
  2. 新建 .eslintrc.eslintignore 文件

    编辑 .eslintrc 文件:

    {
      "root": true,
      "env": {
        "browser": true,
        "es2021": true,
        "es6": true,
        "node": true
      },
      "parser": "@typescript-eslint/parser",
      "parserOptions": {
        "sourceType": "module",
        "ecmaVersion": 12,
        "ecmaFeatures": {
          "jsx": true,
          "tsx": true
        }
      },
      "plugins": ["@typescript-eslint"],
      "rules": {
        "no-console": "error",
        "no-debugger": "error"
      }
    }
    
  3. package.jsonscript 添加脚本

    "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx,.json --max-warnings 0 --cache",
    "lint:fix": "pnpm run lint --fix",
    

2. 配置 prettier:

  1. 安装

    pnpm i prettier eslint-config-prettier eslint-plugin-prettier -Dw
    
  2. 新建 .prettierrc(非必须,可不填)

    {
      "bracketSpacing": true,
      "jsxBracketSameLine": true,
      "jsxSingleQuote": false,
      "printWidth": 140,
      "semi": true,
      "useTabs": false,
      "singleQuote": true,
      "tabWidth": 2,
      "endOfLine": "auto",
      "trailingComma": "all"
    }
    
  3. package.jsonscript 添加脚本

    "format": "prettier --write --cache .",
    

到这里你可以自己新建一个 test.ts 去测试是否生效;

非大厂的我们,如何去搞Vue/React Hooks和Utils的企业开源工具库?

3. 配置 husky:

  1. 安装

    pnpm i husky lint-staged -Dw
    
  2. package.jsonscript 添加脚本

    script:{
      "prepare": "husky install",
    },
    "lint-staged": {
      "*.{vue,js,ts,jsx,tsx,md,json}": [
        "pnpm run lint",
        "pnpm run format"
      ]
    }
    
  3. 初始化 husky(按顺序运行以下命令)

    # 按顺序运行以下命令
    npx husky install
    npx husky add .husky/pre-commit "npx --no-install lint-staged"
    

4. 配置 commitlint

  1. 安装

    pnpm i @commitlint/config-conventional @commitlint/cli  -Dw
    
  2. 创建 commitlint.config.ts

    module.exports = {
      extends: ['@commitlint/config-conventional'],
    };
    
  3. 运行

    npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
    

好的,到这里我们就基本安装完成了,下面我们一起 git push 验证一下吧!

5. 验证

  1. 新建 .gitignore 文件

    .DS_Store
    .history
    .vscode
    .idea
    .eslintcache
    .pnpm-debug.log
    *.local
    dist
    node_modules
    types
    coverage
    
  2. git 提交

    git add . && git commit -m "init(init): init"    
    
  3. 执行效果如下图

    非大厂的我们,如何去搞Vue/React Hooks和Utils的企业开源工具库?

好的呢,到这里就搭建好基本架子了,下面就开始动手吧 ~

四、添加共享函数集合(@vmejs/shared)

@vmejs/shared 是一个共享的函数包,包含了其他包所使用的的一些公共方法

1. 初始化 packages/shared 包(已有可忽略)

# 1.新建
mkdir packages/shared

# 2.初始化
cd packages/shared && pnpm init

2. 添加常见的共享工具函数:

  1. 新建 packages/shared/is 目录

  2. 新建 packages/shared/is/index.ts(不细诉详细过程):

    export const isClient = typeof window !== 'undefined'
    export const isDef = <T = any>(val?: T): val is T => typeof val !== 'undefined'
    export const assert = (condition: boolean, ...infos: any[]) => {
      if (!condition)
        console.warn(...infos)
    }
    const toString = Object.prototype.toString
    export const isBoolean = (val: any): val is boolean => typeof val === 'boolean'
    export const isFunction = <T extends Function> (val: any): val is T => typeof val === 'function'
    export const isNumber = (val: any): val is number => typeof val === 'number'
    export const isString = (val: unknown): val is string => typeof val === 'string'
    export const isObject = (val: any): val is object =>
      toString.call(val) === '[object Object]'
    export const isWindow = (val: any): val is Window =>
      typeof window !== 'undefined' && toString.call(val) === '[object Window]'
    export const now = () => Date.now()
    export const timestamp = () => +Date.now()
    export const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n))
    export const noop = () => {}
    export const rand = (min: number, max: number) => {
      min = Math.ceil(min)
      max = Math.floor(max)
      return Math.floor(Math.random() * (max - min + 1)) + min
    }
    export const isIOS = /* #__PURE__ */ isClient && window?.navigator?.userAgent && /iP(ad|hone|od)/.test(window.navigator.userAgent)
    export const hasOwn = <T extends object, K extends keyof T>(val: T, key: K): key is K => Object.prototype.hasOwnProperty.call(val, key)
    

3. 导出

  1. 新建 packages/shared/index.ts 文件
    export * from './is';
    

4. 在 packages/core 包中使用

import { isString } from '@vmejs/shared';

五、开始第一个函数(@vmejs/core)

1. 初始化 packages/core 包(已有可忽略)

# 1.新建
mkdir packages/core

# 2.初始化
cd packages/core && pnpm init

2. 新增获取当前浏览器设备信息函数

  1. 新增 packages/core/getDevice/index.ts 目录

    mkdir getDevice && touch index.ts
    
  2. 编写代码(不细述实现过程)

    import { isString } from '@vmejs/shared'
    
    export const DEVICES = [
      {
        regs: [
          /\b(sch-i[89]0\d|shw-m380s|sm-[pt]\w{2,4}|gt-[pn]\d{2,4}|sgh-t8[56]9|nexus 10)/i,
          /\b((?:s[cgp]h|gt|sm)-\w+|galaxy nexus)/i,
          /samsung[- ]([-\w]+)/i,
          /sec-(sgh\w+)/i,
        ],
        vendor: 'Samsung',
      },
      {
        regs: [
          /\((ip(?:hone|od)[\w ]*);/i,
          /\((ipad);[-\w\),; ]+apple/i,
          /applecoremedia\/[\w\.]+ \((ipad)/i,
          /\b(ipad)\d\d?,\d\d?[;\]].+ios/i,
        ],
        vendor: 'Apple',
      },
      {
        regs: [
          /(pixel c)\b/i,
          /droid.+; (pixel[\daxl ]{0,6})(?: bui|\))/i,
        ],
        vendor: 'Google',
      },
      {
        regs: [
          /\b((?:ag[rs][23]?|bah2?|sht?|btv)-a?[lw]\d{2})\b(?!.+d\/s)/i,
          /(?:huawei|honor)([-\w ]+)[;\)]/i,
          /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][012359c][adn]?)\b(?!.+d\/s)/i,
        ],
        vendor: 'Huawei',
      },
      {
        regs: [
          /\b(poco[\w ]+)(?: bui|\))/i, // Xiaomi POCO
          /\b; (\w+) build\/hm\1/i, // Xiaomi Hongmi 'numeric' models
          /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, // Xiaomi Hongmi
          /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, // Xiaomi Redmi
          /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite)?)(?: bui|\))/i, // Xiaomi Mi
          /\b(mi[-_ ]?(?:pad)(?:[\w_ ]+))(?: bui|\))/i, // Mi Pad tablets
        ],
        vendor: 'Xiaomi',
      },
      {
        regs: [
          /; (\w+) bui.+ oppo/i,
          /\b(cph[12]\d{3}|p(?:af|c[al]|d\w|e[ar])[mt]\d0|x9007|a101op)\b/i,
        ],
        vendor: 'OPPO',
      },
      {
        regs: [
          /vivo (\w+)(?: bui|\))/i,
          /\b(v[12]\d{3}\w?[at])(?: bui|;)/i,
        ],
        vendor: 'Vivo',
      },
      {
        regs: [
          /(Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i,
        ],
        vendor: 'other',
      },
    ]
    
    /**
      * 获取设备类型与供应商
      * @param ua window.navigator.userAgent
      * @returns { model: '', vendor: '' }
      */
    export const getDevice = (ua?: string) => {
      const device = {
        model: '',
        vendor: '',
      }
    
      if (!isString(ua)) {
        // node runtimes env
        if (global) return device
    
        ua = window.navigator.userAgent
      }
    
      device.model = 'pc'
      device.vendor = 'other'
    
      for (let i = 0; i <= DEVICES.length; i++) {
        if (!DEVICES[i]) break
    
        const { regs, vendor } = DEVICES[i]
        const findVal = regs.find(item => item.exec(ua as string))
    
        if (findVal) {
          device.model = 'mobile'
          device.vendor = vendor
          break
        }
      }
    
      return device
    }
    
    

3. 导出:新建 packages/core/index.ts 文件

export * from './getDevice';

那写到这里其实咱们的第一个工具函数就完成了~

看了之后是不是觉得超简单呢?

是的,没错,的确简单!

但是我们在上篇文章《非大厂的我们,如何去卷一套标准的前端团队规范?》中有说明,前端测试的重要性;

所以咱们不可避免的要使用单元测试来对我们的工具函数来进行 自动化测试 保证它的完整度;

六、单元测试

1. 测试工具的选择:

  • mocha:Mocha 是一个功能丰富的 JavaScript 测试框架
  • jest:facebook 出的一款 JavaScript 自动化测试框架,专注于简单性。
  • vitest:尤大团队打造,一个由 Vite 打造的单元测试框架,而且它很快!

无可厚非,我选择 vitest 来进行单元测试 ~

当然,你对其它测试框架比较熟悉,你亦可以选择,选择权在你 ~

2. 安装 vitest

pnpm i vitest -Dw

3. 新建 vitest.config.ts

import { resolve } from 'path'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true
  },
  resolve: {
    alias: {
      '@vmejs/shared': resolve(__dirname, 'packages/shared/index.ts'),
      '@vmejs/core': resolve(__dirname, 'packages/core/index.ts'),
    },
  },
})

4. 配置运行脚本:package.json

"script": {
    ...
    "test": "vitest test", // 执行测试
    "coverage": "vitest run --coverage" // 执行测试覆盖率,需要安装 @vitest/coverage-c8
    ...
}

5. 编写 packages/core/getDevice 函数的测试用例

  1. 新建 packages/core/getDevice/index.test.ts 文件

    import { expect, describe, it } from 'vitest'
    import { getDevice } from '.'
    
    describe('device test', () => {
      it('The device should return {}', () => {
        const browser = getDevice()
        expect(browser).toEqual({
          model: '',
          vendor: '',
        })
      })
    
      it('The device should return mobile/Samsung', () => {
        const uaStr = 'Mozilla/5.0 (Linux; U; Android 2.3.5; de-de; SAMSUNG GT-S5830/S5830BUKS2 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1'
        const device = getDevice(uaStr)
        expect(device).toEqual({
          model: 'mobile',
          vendor: 'Samsung',
        })
      })
    
      it('The device should return mobile/Apple', () => {
        const uaStr = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
        const device = getDevice(uaStr)
        expect(device).toEqual({
          model: 'mobile',
          vendor: 'Apple',
        })
      })
    
      it('The device should return mobile/Apple', () => {
        const uaStr = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
        const device = getDevice(uaStr)
        expect(device).toEqual({
          model: 'mobile',
          vendor: 'Apple',
        })
      })
    
      it('The device should return mobile/Huawei', () => {
        const uaStr = 'Mozilla/5.0 (Linux; U; Android 7.0; zh-cn; KNT-AL20 Build/HUAWEIKNT-AL20) AppleWebKit/537.36 (KHTML, like Gecko) MQQBrowser/7.3 Chrome/37.0.0.0 Mobile Safari/537.36'
        const device = getDevice(uaStr)
        expect(device).toEqual({
          model: 'mobile',
          vendor: 'Huawei',
        })
      })
    
      it('The device should return mobile/other', () => {
        const uaStr = 'Mozilla/5.0 (Linux; U; Android 2.3.5; en-gb; HTC Desire HD A9191 Build/GRJ90) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1'
        const device = getDevice(uaStr)
        expect(device).toEqual({
          model: 'mobile',
          vendor: 'other',
        })
      })
    
      it('The device should return pc/other', () => {
        const uaStr = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de-CH) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2'
        const device = getDevice(uaStr)
        expect(device).toEqual({
          model: 'pc',
          vendor: 'other',
        })
      })
    })
    

6. 执行 pnpm test 如下图:

非大厂的我们,如何去搞Vue/React Hooks和Utils的企业开源工具库?

7. 执行 pnpm coverage:测试覆盖率

  1. 安装 @vitest/coverage-c8

    pnpm i @vitest/coverage-c8 -Dw
    
  2. 执行结果:

    非大厂的我们,如何去搞Vue/React Hooks和Utils的企业开源工具库?

8. 其它测试工具的搭配(文章篇幅原因就不细述):

  • happy-dom:在测试环境模拟 Web 浏览器,以便执行自动化测试脚本;
  • @vitest/coverage-c8:展示测试覆盖率;

到这里其实就已经可以大致的去使用了;

但是我们试想一下,要想做一个适合企业或者开源的一个工具库,那么配套的文档是必不可少的;

毕竟你写的代码不知道如何去使用那将完全失去了它的价值,所以项目文档是标配;

七、搭建文档

搭建文档的方法有很多,有开源的库,也有一些现成的在线工具,或者你有时间去自研也是一个不错的选择;

为了专注于代码的实现层面,咱们可以使用现有的文档生成框架,那么 Vitepress 就是一个很不错的选择;

由于 Vitepress 是基于 Vite 的,所以它也很好的继承了 Bundless 特性,开发的代码能以秒级速度在文档中看到运行效果,完全可以充当调试工具来使用。

1. 安装

pnpm i vitepress -Dw

2. 配置 vitepress.config.ts:默认不需要配置

3. 配置运行脚本:package.json

"script": {
    ...
    "docs:dev": "vitepress dev packages",
    "docs:build": "vitepress build packages",
    ...
}

4. 运行 pnpm docs:dev 后如下图:

非大厂的我们,如何去搞Vue/React Hooks和Utils的企业开源工具库?

因为还未做任何配置,所以会是 404 页面

5. 基本配置:新建 packages/index.md 文件

---
layout: home
sidebar: false

title: vmejs
titleTemplate: 一个疯狂的开源前端工具库

hero:
  name: vmejs
  text: 一个疯狂的开源前端工具库
  tagline: