使用 rslib 开发一个 排班表组件 并实现自动发版更新到 npm 上

1,107 阅读4分钟

前言

工作中派了个任务需要做一个排班表功能,搜了下没有现成的组件,发现可以自己来开发一个发到 npm 上给大家使用,成品如下

20250210172015_rec_.gif

源码地址: github.com/LLmoskk/tim…

开发过程

开发一个组件,可以自己搭一下 rollup 然后修改一些杂七杂八的配置最后 build 出一个组件,开源社区已经有帮我做好这些事情的脚手架了。 例如:

我这里就是个小的事例组件,所以我选择使用 rslib

按照官网的教程操作

pnpm create rslib@latest

然后我们选一下一些自定义配置项,根据个人爱好选择就好

image.png

创建后的项目结构我们可以看到给了个 Button 做示例

image.png

然后我们按照教程的流程

pnpm install

pnpm run dev

启动后和我一样第一次开发组件的同学可能会一脸懵逼😳,怎么没有预览地址呀,那我该去哪看一下这个组件啊!!!

image.png

原来其实大佬们可能以为我们都知道 pnpm link 的概念了,所以没给预览。

在开发包中 pnpm link --global 将当前包link到全局node_modules中

image.png

然后在你运行的项目中pnpm link --global @xxx(package.json中的name) 即可

由于启动命令是 --watch 的 "dev": "rslib build --watch" 所以你修改这个组件会热更新立刻看到变化的。

但是这样好不方便啊!我还要另外 vite 起一个项目来看,那么我们就改一下,让 pnpm dev启动后返回一个可以预览的地址。

改一下脚手架

我们需要启动一个 React ,写完组件后又不要把 React 打包起来,可以模仿 React router 仓库,把要展示的内容收到 Exmple 目录下,然后打包指向需要打包的 src 目录就可以了。

image.png

我这里就使用 rsbuild 了,创建好 exmple 文件夹在根目录,然后一个常见的 React App.tsx 和 index.tsx 文件

image.png

再安装下相关要用到的依赖

pnpm i react react-dom @rsbuild/core

然后修改启动命令为 rsbuild

"dev": "rsbuild dev --open",

再创建一个 rsbuild.config.ts 来配置一些必要的选项

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
  plugins: [pluginReact()],
  source: {
    entry: {
      index: './example/index.tsx', // 指向入口为 example 的 index
    },
  },
  output: {
    distPath: {
      root: './distExample',
    },
    assetPrefix: './',
  },
});

再把 src 下的组件导入到 exmple/index.tsx文件展示,现在我们就可以在一个仓库里面边修改边看效果啦~

这里 output 我也修改了一下,后面会使用到,目的是为了可以在 github 仓库右侧贴一个预览使用的 demo 展示。

image.png

组件主要逻辑

const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const;
type WeekDay = (typeof weekDays)[number];
export type Time = Record<WeekDay, number[]>;

// 数据结构是这样的星期几为键,值为一个 0-12 的数组
const [selectedSlots, setSelectedSlots] = useState<Time>(
    value || {
      Mon: [],
      Tue: [],
      Wed: [],
      Thu: [],
      Fri: [],
      Sat: [],
      Sun: [],
    }
);

主体是一个 table 然后一个个小格子表示时间

image.png
 <table className='time-period-table'>
    <thead className='time-period-thead'>
      <tr>
        <th rowSpan={2} className='time-period-th'>
          {language === 'zh' ? '日期/时间' : 'Date/Time'}
        </th>
        <th colSpan={12} className='time-period-th'>
          00:00~12:00
        </th>
        <th colSpan={12} className='time-period-th'>
          12:00~24:00
        </th>
      </tr>
      <tr>
        {Array.from({ length: 24 }).map((_, i) => (
          <th key={i} className='time-period-hour-th'>
            {i}
          </th>
        ))}
      </tr>
    </thead>
    <tbody>
      {weekDays.map((day) => (
        <tr key={day}>
          <td className='time-period-day'>{weekDayDisplay[day]}</td>
          {Array.from({ length: 24 }).map((_, hourIndex) => (
            <td
              key={hourIndex}
              className={`time-period-hour ${
                selectedSlots[day].includes(hourIndex) ? 'selected' : ''
              }`}
              onMouseDown={() => handleMouseDown(day, hourIndex)}
              onMouseEnter={() => handleMouseEnter(day, hourIndex)}
              onMouseUp={handleMouseUp}
              onClick={() => handleClick(day, hourIndex)}
            />
          ))}
        </tr>
      ))}
    </tbody>
  </table>

主要在每个小格格上使用这几个事件处理点击、按压滑动、滑动结束后选中

  • onMouseDown={() => handleMouseDown(day, hourIndex)}
  • onMouseEnter={() => handleMouseEnter(day, hourIndex)}
  • onMouseUp={handleMouseUp}
  • onClick={() => handleClick(day, hourIndex)}
// 选中确认的值
const [selectedSlots, setSelectedSlots] = useState<Time>(
    value || {
      Mon: [],
      Tue: [],
      Wed: [],
      Thu: [],
      Fri: [],
      Sat: [],
      Sun: [],
    }
);
// 记录是否是在 Selecting
const [isSelecting, setIsSelecting] = useState(false);
// 选中的范围值
const [selectionStart, setSelectionStart] = useState<{
    day: WeekDay;
    hour: number;
} | null>(null);


const handleClick = (day: WeekDay, hour: number) => {
    if (isSelecting) return;

    const newSelectedSlots = { ...selectedSlots };
    const currentHours = selectedSlots[day];

    if (currentHours.includes(hour)) {
      newSelectedSlots[day] = currentHours.filter((h) => h !== hour);
    } else {
      newSelectedSlots[day] = [...currentHours, hour].sort((a, b) => a - b);
    }

    setSelectedSlots(newSelectedSlots);
    onChange?.(newSelectedSlots);
 };

const handleMouseDown = (day: WeekDay, hour: number) => {
    setIsSelecting(true);
    setSelectionStart({ day, hour });
 };

const handleMouseEnter = (day: WeekDay, hour: number) => {
    if (!isSelecting || !selectionStart || selectionStart.day !== day) return;

    const minHour = Math.min(selectionStart.hour, hour);
    const maxHour = Math.max(selectionStart.hour, hour);
    const hours = Array.from(
      { length: maxHour - minHour + 1 },
      (_, i) => minHour + i
    );

    const newSelectedSlots = {
      ...selectedSlots,
      [day]: Array.from(new Set([...selectedSlots[day], ...hours])).sort(
        (a, b) => a - b
      ),
    };

    setSelectedSlots(newSelectedSlots);
    onChange?.(newSelectedSlots);
 };

const handleMouseUp = () => {
    setIsSelecting(false);
    setSelectionStart(null);
};

这样子就可以做到零依赖完成滑动选中的效果了 具体完整代码与样式可以查看源码

github.com/LLmoskk/tim…

自动发布到 npm

注册 npm 账号与传统的手动 npm publish 发布到 npm 的流程这里先不提了,可以参考其他优秀文章,这里讲一下怎么 git push 后自动更新最新的代码到 npm 包上。

自动发版使用 semantic-release

我使用 semantic-release 进行自动版本控制和发布。当更改推送到主分支时,会自动触发发布流程。

提交消息应遵循 Conventional Commits 规范:

  • feat: ... - 新功能(次要版本发布)
  • fix: ... - 错误修复(补丁版本发布)
  • BREAKING CHANGE: ... - 破坏性更改(主要版本发布)

对 semantic-release 做对应的 config 配置,根目录下创建 release.config.mjs文件

const config = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    '@semantic-release/changelog',
    '@semantic-release/github',
    '@semantic-release/npm',
    '@semantic-release/git',
  ],
};

export default config;

在.github/workflows 创建对应的 CI 流程

name: Release
on:
  push:
    branches:
      - main
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Install dependencies
        run: pnpm install

      - name: Build
        run: pnpm build

      - name: Run semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release

这里需要在你的仓库设置两个环境变量 GH_TOKEN(github 的 token),NPM_TOKEN(npm 账号的 token),具体的流程我就不补充图片了用文字描述,大家要是找不到我再补充图片

设置令牌

此模板使用 GitHub Actions 进行自动发布。您需要设置以下令牌:

1. GitHub 令牌 (GH_TOKEN)

  1. 创建 Github 个人访问令牌

  2. 点击 Generate new token

  3. 生成具有以下权限的新令牌:

    • Actions - read and write
    • Commit statuses - read and write
    • Contents - read and write
    • Deployments - read and write
    • Issues - read and write
  4. 复制令牌并将其添加到您的仓库密钥中:

    • 进入仓库设置
    • 导航到 Secrets and variables > Actions
    • 创建一个名为 GH_TOKEN 的新密钥

2. NPM 令牌 (NPM_TOKEN)

  1. 访问 npmjs.com

  2. 导航到您的个人设置

  3. 选择 "Access Tokens"

  4. 创建新的访问令牌(发布权限)

  5. 复制令牌并将其添加到您的仓库密钥中:

    • 进入仓库设置
    • 导航到 Secrets and variables > Actions
    • 创建一个名为 NPM_TOKEN 的新密钥

3. 开启 GitHub Actions

  1. 进入仓库设置,点击 Pages
  2. 构建和部署来源:选择 GitHub Actions

组件成品效果展示使用 github-pages

目的是把我们之前打包预览的 exmple 文件展示在 github pages 让访问的用户可以直接看到与体验组件效果, 完整的 script 修改如下,在 CI 中执行 pnpm build:example并部署展示 path: "distExample",可以根据自己的需求自行更改。

"scripts": {
    "build": "rslib build",
    "build:example": "rsbuild build",
    "preview:example": "rsbuild preview --open",
    "dev": "rsbuild dev --open",
    "semantic-release": "semantic-release"
},
image.png
name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Install dependencies
        run: pnpm install

      - name: Build example
        run: pnpm build:example

      - name: Setup Pages
        uses: actions/configure-pages@v4

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: "distExample"

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

抽离成一个快速启动模版

上面这些步骤开发每个组件都做一次太麻烦了,我就抽离成一个 Template 方便自己使用,有需要的同学自取,只需要根据 readme 中的教程配置下两个 token 即可。可以直接点击右上角的 use Template 使用

image.png

模版地址: github.com/LLmoskk/rsl…

结语

这样的流程我们就可以达成在一个仓库中开发、预览、发版、部署预览页了。

文章中可能有些地方讲的不够细致,有的步骤不清晰或有疑问的小伙伴评论区提一下反馈我再进行修改。感谢🙏🏻