前言
工作中派了个任务需要做一个排班表功能,搜了下没有现成的组件,发现可以自己来开发一个发到 npm 上给大家使用,成品如下
源码地址: github.com/LLmoskk/tim…
开发过程
开发一个组件,可以自己搭一下 rollup 然后修改一些杂七杂八的配置最后 build 出一个组件,开源社区已经有帮我做好这些事情的脚手架了。 例如:
- dumi - 为组件研发而生的静态站点框架 用来开发有界面的组件
- father - NPM 包研发工具 用来开发工具函数之类的不需要界面的 npm 包
- rslib - 基于 Rsbuild 实现的 library 开发工具 基于 Rsbuild 精心设计的配置和插件,使库开发者得以复用 webpack 和 Rspack 繁荣的知识和生态系统
我这里就是个小的事例组件,所以我选择使用 rslib
按照官网的教程操作
pnpm create rslib@latest
然后我们选一下一些自定义配置项,根据个人爱好选择就好
创建后的项目结构我们可以看到给了个 Button 做示例
然后我们按照教程的流程
pnpm install
pnpm run dev
启动后和我一样第一次开发组件的同学可能会一脸懵逼😳,怎么没有预览地址呀,那我该去哪看一下这个组件啊!!!
原来其实大佬们可能以为我们都知道 pnpm link 的概念了,所以没给预览。
在开发包中 pnpm link --global 将当前包link到全局node_modules中
然后在你运行的项目中pnpm link --global @xxx(package.json中的name) 即可
由于启动命令是 --watch 的 "dev": "rslib build --watch" 所以你修改这个组件会热更新立刻看到变化的。
但是这样好不方便啊!我还要另外 vite 起一个项目来看,那么我们就改一下,让 pnpm dev启动后返回一个可以预览的地址。
改一下脚手架
我们需要启动一个 React ,写完组件后又不要把 React 打包起来,可以模仿 React router 仓库,把要展示的内容收到 Exmple 目录下,然后打包指向需要打包的 src 目录就可以了。
我这里就使用 rsbuild 了,创建好 exmple 文件夹在根目录,然后一个常见的 React App.tsx 和 index.tsx 文件
再安装下相关要用到的依赖
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 展示。
组件主要逻辑
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 然后一个个小格子表示时间
<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);
};
这样子就可以做到零依赖完成滑动选中的效果了 具体完整代码与样式可以查看源码
自动发布到 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)
-
点击
Generate new token -
生成具有以下权限的新令牌:
- Actions - read and write
- Commit statuses - read and write
- Contents - read and write
- Deployments - read and write
- Issues - read and write
-
复制令牌并将其添加到您的仓库密钥中:
- 进入仓库设置
- 导航到 Secrets and variables > Actions
- 创建一个名为
GH_TOKEN的新密钥
2. NPM 令牌 (NPM_TOKEN)
-
访问 npmjs.com
-
导航到您的个人设置
-
选择 "Access Tokens"
-
创建新的访问令牌(发布权限)
-
复制令牌并将其添加到您的仓库密钥中:
- 进入仓库设置
- 导航到 Secrets and variables > Actions
- 创建一个名为
NPM_TOKEN的新密钥
3. 开启 GitHub Actions
- 进入仓库设置,点击 Pages
- 构建和部署来源:选择 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"
},
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 使用
模版地址: github.com/LLmoskk/rsl…
结语
这样的流程我们就可以达成在一个仓库中开发、预览、发版、部署预览页了。
文章中可能有些地方讲的不够细致,有的步骤不清晰或有疑问的小伙伴评论区提一下反馈我再进行修改。感谢🙏🏻