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

11,575 阅读9分钟

作者:易师傅github

声明:文章为稀土掘金技术社区首发签约文章,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 为手动创建):

Untitled.png

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

二、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 去测试是否生效;

image.png

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. 执行效果如下图

    image.png

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

四、添加共享函数集合(@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 如下图:

Untitled2.png

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

  1. 安装 @vitest/coverage-c8

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

    Untitled3.png

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 后如下图:

Untitled4.png

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

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

---
layout: home
sidebar: false

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

hero:
  name: vmejs
  text: 一个疯狂的开源前端工具库
  tagline: 🎉 一个疯狂的开源前端工具库
  actions:
    - theme: brand
      text: 快速开始
      link: /guide/
    - theme: alt
      text: 工具函数集合
      link: /core/getDevice/
    - theme: alt
      text: Vue Hooks
      link: /vue
    - theme: alt
      text: React Hooks
      link: /react
    - theme: alt
      text: View on GitHub
      link: https://github.com/jeddygong

features:
  - title: 功能丰富
    details: 众多工具函数任你选择
    icon: 🎛
  - title: React Hooks(建设中)
    details: 集成你想要的 React hooks
    icon: 🚀
  - title: Vue Hooks(建设中)
    details:  集成你想要的 Vue hooks
    icon: 
  - title: 强类型支持
    details: 使用TypeScript编写,良好类型支持
    icon: 🦾
  - title: SSR 支持(建设中)
    details: 支持服务端渲染,无需额外配置
    icon: 🛠
  - title: 轻量级
    details: 不依赖任何第三方库,体积小巧
    icon: ☁️
---

6. 编写 packages/core/getDevice 函数的使用文档

  1. 新建 packages/core/getDevice/index.md 文件

        ---
        category: UA
        ---
    
        # getDevice
        用于从 User-Agent(用户代理数据) 中解析出 `设备类型与供应商`,在浏览器(客户端)或 node.js(服务器端)中使用。
    
        ## Usage
    
        ``` ts
          import { getDevice } from "@vmejs/core"
    
          const browser = getDevice()
    
          // do something
        ```
    
        ## 文档
    
        ### 参数 [ua]:可选]
    
        -   浏览器(客户端)中使用 [ua可选]
    
            ```
              import { getDevice } from "@vmejs/core"
              // 未带参数
              const browser = getDevice()
    
              // 使用参数
              const browser = getDevice(window.navigator.userAgent)
            ```
    
        -   node.js(服务器端)中使用 [ua必传]
    
            ```
              import http from 'http'
              import { getDevice } from "@vmejs/core"
    
              http.createServer(function (req, res) {
                  // get user-agent header
                  const browser = getDevice(req.headers['user-agent']);
    
                  // write the result as response
                  res.end(JSON.stringify(ua, null, '  '));
              })
              .listen(3000, '127.0.0.1');
            ```
    
        ### 返回值:`{model: '', vendor: ''}`
    
        -   device.model:常见设备类型
    
            ```
            # Chrome/其它
            pc
    
            # ios/android/平板
            mobile
            ```
    
        -   device.vendor:常见供应商
    
            ```
            # mobile 常见供应商:
            Samsung, Apple, Coogle, Huawei, Xiaomi, OPPO, Vivo
    
            # 其它
            other
            ```
    

7. 配置指导页:packages/guide/index.md

    # 快速开始

    ## 安装

    ```bash
    npm i @vmejs/core
    ```


    ## 简单使用

    ``` ts
      import { getDevice } from "@vmejs/core"

      const browser = getDevice()

      // do something
    ```

    <!-- 更多功能列表,请参阅 [functions list](/core/getDevice/)。 -->

8. 配置 vitepress

  1. 新建 packages/.vitepresspackages/.vitepress/config.ts 文件:

    mkdir .vitepress && touch .vitepress/config.ts
    
  2. 配置 packages/.vitepress/config.ts 文件:

    const Guide = [
      { text: 'Get Started', link: '/guide/' },
    ]
    
    const functions = [
      Guide,
      { text: 'getDevice', link: '/core/getDevice/' },
    ]
    
    const vueHooks = [
      functions,
      { text: '建设中', link: '' },
    ]
    
    const reactHooks = [
      functions,
      { text: '建设中', link: '' },
    ]
    
    const DefaultSideBar = [
      { text: '指导', items: Guide },
      { text: "工具函数集合", items: functions },
      { text: "Vue Hooks集合", items: vueHooks },
      { text: "React Hooks集合", items: reactHooks },
    ]
    
    export default {
      base: '/',
      title: 'velvet',
      lang: 'zh-CN',
      themeConfig: {
        logo: '/logo.png',
        lastUpdated: true,
        lastUpdatedText: '最后修改时间',
        socialLinks: [{ icon: 'github', link: 'https://github.com/vmejs/vmejs' }],
        nav: [
          { text: '指南', link: '/guide/' },
          { text: '函数集合', link: '/core/getDevice/' },
        ],
        // 侧边栏
        sidebar: {
          '/guide/': [
            {
              text: '',
              items: DefaultSideBar
            }
          ],
          '/core/': [
            {
              text: '',
              items: DefaultSideBar
            },
          ],
        },
      },
    };
    
  3. 添加主题:

    mkdir packages/.vitepress/theme && touch packages/.vitepress/theme/index.ts
    
  4. 配置 packages/.vitepress/theme/index.ts 文件:

    import DefaultTheme from 'vitepress/theme';
    
    export default {
      ...DefaultTheme,
    };
    

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

WX20221111-141721@2x.png

到这里一个简单的文档库就搭建完成了,但是上面只能满足一些最基本的情况;

咱们试想一下后续函数库肯定会越来越多,那么显然上面手动添加每个路径和修改配置是不可取的;

秉持自动化的原则,针对工具库文档咱们后续会集成一个自动化生成的方法,贡献者只需要关注代码层面、单元测试和单个文档,降低代码的耦合度。

具体文章如果您感兴趣的话会在后续输出 ~

八、文档部署(Github/Gitlab Pages)

1. 文档部署的多种方式:

  • 企业:一般都会部署在内网上,这个就需要运维童鞋做 CI/CD 了;
  • 个人网站:最原始的办法就是 pnpm run docs:build 后把生成的包直接 copy 过去,当然你也可以做自动化;
  • GitHub/Gitlab:可用自动化来部署到对应的 GitHub Pages,下面就以 GitHub 的部署来讲解;
  • 当然还有国外的一些免费的部署平台也很好用,这个可自行搜索;

2. 使用 GitHub 操作:

  1. 在配置文件 packages/.vitepress/config.js,将base属性设置为您的 GitHub 存储库的名称。如果你计划将站点部署到https://jeddygong.github.io/vmejs/,那么你应该将 base 设置为'/vmejs/',以斜线开头和结尾。
  2. 创建 .github/workflows/docs-deploy.yml 文件:
    name: docs-deploy
    
    on: # 触发条件
      # 每当 push 到 main 分支时触发部署
      push:
        branches: [main]
    
    jobs:
      docs:
        runs-on: ubuntu-latest # 指定运行所需要的虚拟机环境(必填)
    
        steps:
          - uses: actions/checkout@v2
            with:
              # “最近更新时间” 等 git 日志相关信息,需要拉取全部提交记录
              fetch-depth: 0
    
          - name: Install pnpm
            uses: pnpm/action-setup@v2
            with:
              version: 7
    
          - name: Setup Node.js
            uses: actions/setup-node@v2
            with:
              # 选择要使用的 node 版本
              node-version: '16'
              cache: 'pnpm'
    
          # 如果缓存没有命中,安装依赖
          - name: Install dependencies
            run: pnpm install --no-frozen-lockfile --ignore-scripts
    
          # 运行构建脚本
          - name: Build vitepress site
            run: pnpm docs:build
            env:
              DOC_ENV: preview
              NODE_OPTIONS: --max-old-space-size=4096
    
          # 查看 workflow 的文档来获取更多信息
          # @see https://github.com/crazy-max/ghaction-github-pages
          - name: Deploy to GitHub Pages
            uses: crazy-max/ghaction-github-pages@v2
            # 环境变量
            env:
              GITHUB_TOKEN: ${{ secrets.ACTION_SECRET }}
            with:
              # 部署到 gh-pages 分支
              target_branch: gh-pages
              # 部署目录为 vitepress 的默认输出目录
              build_dir: packages/.vitepress/dist
    

到这里如果你 git push 代码后会发现 GitHub 的 Actions 有运行,但是会一直构建失败;

image.png

这是为什么呢?

主要是由于权限的问题,所以下面我们生成对应的密钥才能操作;

3. 生成 GitHub 的 Secerts:

  1. 进入 GitHub 设置页面生成一个密钥:github.com/settings/to…

    Untitled7.png

  2. 设置当前仓库的 Secerts

    Untitled8.png

  3. 再重新去运行当前仓库的 Action

    image.png

  4. 设置当前仓库的 pages 地址

    image.png

  5. 访问:vmejs.github.io/vmejs/ 即可

到这里,咱们对工具库的搭配基本已经完成,下面就要开始着重输出环节了,毕竟开箱即用咱们还是要配置好的 ~

九、构建打包

要想别人开箱即用,那么构建打包少不了;

其实现在想要构建出 esm、cjs、iife 格式的构建工具有许多,像 Rollup、esbuild、webpack、vite、tsup、unbuild 都可以实现;

1. 什么是 esm、cjs、iife 格式

  • esm 格式:ECMAScript Module,现在使用的模块方案,使用 import export 来管理依赖;
  • cjs 格式:CommonJS,只能在 NodeJS 上运行,使用 require("module") 读取并加载模块;
  • iife 格式:通过 <script> 标签引入的自执行函数;

为了方便大家,让大家可以更快速的、更方便的、开箱即用的构建出 esm、cjs、iife 格式的包,咱们这里就以 tsup 为例;

2. 安装 tsup

pnpm add tsup -Dw

3. 在根目录下配置文件:tsup.config.ts

import { defineConfig } from 'tsup';

export default defineConfig([
  {
    entry: ['packages/core/index.ts'],
    format: ['cjs', 'esm', 'iife'],
    outDir: 'packages/core/dist',
    dts: true, // 添加 .d.ts 文件
    metafile: true, // 添加 meta 文件
    minify: true, // 压缩
    splitting: false,
    sourcemap: true, // 添加 sourcemap 文件
    clean: true, // 是否先清除打包的目录,例如 dist
  },
  {
    entry: ['packages/shared/index.ts'],
    format: ['cjs', 'esm', 'iife'],
    outDir: 'packages/shared/dist',
    dts: true, // 添加 .d.ts 文件
    metafile: true, // 添加 meta 文件
    minify: true, // 压缩
    splitting: false,
    sourcemap: true, // 添加 sourcemap 文件
    clean: true, // 是否先清除打包的目录,例如 dist
  },
]);

更多 defineConfig 参数详见

4. 在 package.json 下添加脚本

"scripts": {
  "dev": "tsup --watch",
  "build": "tsup"
},

5. 执行 pnpm dev 后如下图

image.png

6. 添加 packages/core、packaes/shared 子包的 package.json 入口:

{
  ...
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "unpkg": "./dist/index.global.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    },
    "./*": "./*"
  },
  "engines": {
    "node": ">=14",
    "pnpm": ">=7.0.0"
  },
  "publishConfig": {
    "access": "public"
  },
  ...
}

7. 执行 pnpm build 打包构建即可

这里之所以选择 tsup,主要是因为开箱即用、快速、易上手等优点;虽然不及 rollup 的生态丰富,但是做一些工具库的代码构建足矣;

当然如果你对 rollup、webpack 或 vite 更熟悉,这里也可以用它们构建,可自行配置;

十、发布(npm/私有)

1. 登录 npm(按照提示输入用户名密码邮箱即可)

npm login

2. 注意:

  • 如果发布的 npm 包名为:@xxx/yyy 格式,需要先在 npm 注册名为:xxx 的 organization,否则会出现提交不成功;
  • 发布到 npm group 时默认为 private,所以我们需要手动在每个 packages 子包中的 package.json 中添加如下配置;
    "publishConfig": {
         "access": "public"
     },
    

3. 发布:npm publish

你是不是以为直接 npm publish 就发布成功了?

其实的确发布成功了,但是发布的是一个 monorepo 仓库的代码,这也没办法用呢?

但是咱们的目的要分包发布,所以这时候搭配 pnpm workspace 的工具 changesets 就出现了

4. 安装 changesets

pnpm i @changesets/cli -Dw

5. 初始化 changesets

pnpm changeset init

6. 配置 package.json 的发布脚本

{
    "script": {
        "release": "changeset publish",
    }
}

7. 运行 pnpm release 的最终发布

image.png

至此搞定 ~

总结

其实真要一篇文章详细的讲完所有的配置已经逻辑是不显示的,我想也没有多少人能愿意看完的下去;

但是上面的配置已经全部趋于一个完整的工具开源库;

只是其中有许多是需要我们优化的,这篇文章就不做详细的介绍了,因为每个优化都是一副长篇概论;

当然,既然做我们就要做好了,所以下面有几个自动化的优化是需要持续跟进的:

  • 文档自动化打包,不需手动添加对应的路径;
  • 文档抽离成一个单独的 docs 目录(可忽略);
  • changeset 的更多功能使用,让开源更加轻松;
  • Github 自动部署发布配置,自动 Tag/Release 等;
  • 开源贡献指导文档;
  • 更多 cli 脚本;

目前该开源库还是一个雏形,如果您想提升自己、对开源感兴趣、对 Vue Hooks 感兴趣、对 React Hooks 感兴趣,可以加入我们的 Team(vx: JeddyGong),一起 crazy 吧!

工具库模板地址:链接

Github 开源地址:链接

最后

该系列会是一个持续更新系列,关于 前端基建,笔者主要会从如下图几个方面讲解,如果您想第一时间看到我的更新文章,可以关注我和我的《前端要搞基建》专栏

前端基建之路.png

如果你对 Vite 感兴趣,可以看看我的专栏:《Vite 从入门到精通》

如果你对微前端感兴趣,可以看看我的专栏:《微前端从入门到精通》

如果想跟我一起讨论技术吹水聊球, 欢迎加入前端学习群聊(群人数太多,只能加vx,望谅解) vx: JeddyGong

感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏 哦 ~

关注我,带您一起搞基建 ~