news 2026/10/20 16:35:44

Rea:React企业级应用的前端工程化与组件复用实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rea:React企业级应用的前端工程化与组件复用实践

我一直觉得,一个看似简单的项目名背后,往往藏着开发者最真实的需求和思考。这次要聊的“Rea”,初看只是一个缩写,但真正落地时,它牵扯出的是关于前端工程化、组件封装、跨端复用、性能优化等一系列绕不开的问题。这篇文章我会把整个项目的来龙去脉、技术选型逻辑、核心代码实现,以及我踩过的坑和排查过程,全都摊开来讲。不管你是想做一个类似的工具,还是想了解一个前端项目从0到1的完整思考路径,这篇文章都应该能给你一些参考。

1. 项目整体设计与思路拆解

1.1 核心需求解析:从一个模糊缩写到功能边界

“Rea”这个名字,最初并不是一个深思熟虑的品牌名,更多是“React Enterprise Application”的缩写灵感。我的核心诉求不是做一个简单的Demo,而是一套能支撑中型团队、多业务线并行开发的前端基础架构。这类架构在行业内常被称作“脚手架”或“工程化底座”,但实际做起来,它远比“npm create”生成一个模板要复杂得多。

我把需求拆解成四个层面:

  • 业务层面:团队同时维护着三个以上的Web应用,涉及数据看板、内容管理后台、用户权限中心,这些应用有大量相似的页面和交互逻辑。如果每个应用都独立维护一套UI组件和工具函数,人力浪费会非常明显。
  • 工程层面:需要统一的技术栈(React + TypeScript)、统一的代码规范(ESLint + Prettier)、统一的构建流程(Vite或Webpack)。更重要的是,组件和工具函数必须能跨项目复用,而不是通过复制粘贴。
  • 体验层面:权限控制要细化到按钮级别;主题切换不能“一闪而过”;大数据列表页面滚动必须流畅;国际化文案要能从后端动态拉取。
  • 维护层面:代码提交要有规范(Commitlint);发布版本要有清晰的Changelog;组件库的文档和Demo必须和代码同步更新,避免“文档过期”。

所以,“Rea”在项目初期,本质上是一个决策框架:它决定了我们后续所有开发的边界和准则。当你把这些问题想清楚,后面的一切都是执行问题。

1.2 技术选型背后的为什么

技术选型是这类项目最容易被争论的部分。我的原则是“团队熟悉度优先,性能其次,生态兜底”。

  • React 18 + TypeScript:团队主力栈,TypeScript的静态类型检查在多人协作时能避免大量低级错误。React 18的并发特性(如startTransition)对于后续优化大数据列表渲染非常有用。
  • Vite:选Vite而不是Webpack,核心原因是开发体验。Vite基于ESModule的按需编译,冷启动速度是Webpack的几倍以上,这在项目体积变大后体验差异极其明显。虽然Vite在大型项目构建时可能遇到一些兼容性问题(下文会提到),但整体利大于弊。
  • Monorepo(pnpm workspace):这是“Rea”区别于普通脚手架的关键。我把packages/ui(组件库)、packages/utils(工具函数)、apps/admin(后台应用)、apps/portal(门户应用)放在同一个仓库里。好处是跨项目复用代码时,不再需要npm link或者发布到私有npm仓库,直接引用workspace包即可,改动一处,所有引用的地方立刻生效。
  • 状态管理:没有选择Redux,而是用了Zustand。理由很简单:Redux的样板代码太多,对于一个需要快速迭代的业务团队来说,心智负担太重。Zustand的API极其简洁,且基于useSyncExternalStore,与React 18的并发特性完美兼容。
  • CSS方案:最终选了CSS Modules + CSS Variables。没有用Tailwind或CSS-in-JS,因为我们的业务需要运行时动态换肤(不同客户有不同品牌色),CSS变量是最轻量的方案,而CSS Modules能保证样式隔离,避免组件库样式污染宿主应用。

提示: 技术选型没有“最好的”,只有“最合适的”。如果你是一个3人小团队做内部工具,完全没必要搞Monorepo和复杂的权限模型;但如果你的目标是沉淀一套可复用的资产,那这些复杂度是值得提前投入的。

1.3 整体架构的层次划分

架构上,我参考了“洋葱模型”的分层思想,但做了简化。整体分成四层,每层职责单一,依赖方向从上到下:

层级模块核心职责典型依赖
应用层apps/admin、apps/portal页面组装、路由配置、业务逻辑编排业务组件、领域Hooks
业务组件层packages/ui/src/biz带业务语义的组件,如UserTable、AuthButton基础组件、Utils
基础组件层packages/ui/src/base无业务语义的通用组件,如Button、Modal、TableUtils、仅React
基础设施层packages/utils、packages/hooks纯函数工具、通用Hooks(如usePagination)仅TypeScript

这样的分层带来一个显著的收益:当你需要修改一个通用Table组件的样式时,你不会意外影响到另一个业务模块的特定逻辑。同时,依赖方向是单向的,循环依赖问题会大幅减少。

2. 核心细节解析与实操要点

2.1 权限控制:从路由守卫到按钮级控制

权限是所有中后台系统绕不开的痛点。“Rea”的权限模型没有用复杂的CASL或其他权限库,而是基于“用户 -> 角色 -> 权限点”的经典RBAC模型,结合前端路由和指令级组件实现。

路由级权限的实现核心在于三层:

  1. 后端在用户登录时返回当前用户拥有的权限码数组,如['dashboard:view', 'user:create', 'user:delete']。
  2. 前端路由表在Meta中声明permission字段,例如:
// apps/admin/src/router/routes.ts const routes: RouteRecordRaw[] = [ { path: '/user', name: 'UserManagement', component: () => import('@/views/user/index.vue'), // 示例,实际是TSX meta: { title: '用户管理', permission: 'user:view', // 访问该路由需要的权限码 }, }, ];
  1. 在全局路由守卫中匹配meta.permission,如果没有对应权限码,则重定向到403页面。

按钮级权限我用了一个封装好的Auth组件,内部通过一个usePermission的Hook判断。这样业务代码里就不会到处是if (hasPermission('user:create'))这样的条件判断,代码会干净很多。

// packages/hooks/src/usePermission.ts import { useAuthStore } from '@/store/auth'; export function usePermission() { const permissionCodes = useAuthStore((state) => state.permissionCodes); const hasPermission = (code: string) => { if (permissionCodes.includes('*')) return true; // 超级管理员 return permissionCodes.includes(code); }; return { hasPermission }; }

注意:前端权限控制永远只是体验层面的优化,真正的安全校验必须以后端接口为准。不要因为前端隐藏了某个按钮,就觉得后端不需要校验权限了。攻击者完全可以绕过前端直接调用接口。

2.2 主题切换:CSS变量与运行时替换

主题切换是另一个经常被低估复杂度的模块。很多团队用切换className的方式实现,但那样会带来样式覆盖的优先级灾难。我的方案是彻底拥抱CSS变量(Custom Properties)。

我在:root选择器下定义了一套默认的主题变量,然后通过>/* packages/ui/src/styles/theme.css */ :root { --primary-color: #1677ff; --text-color: rgba(0, 0, 0, 0.88); --bg-color: #ffffff; } :root[data-theme='dark'] { --primary-color: #4c9aff; --text-color: rgba(255, 255, 255, 0.85); --bg-color: #141414; }

所有组件里的颜色都不再写死,而是引用这些变量:

/* packages/ui/src/base/Button/style.css */ .btn { background-color: var(--primary-color); color: var(--text-color); }

切换主题时,我只需要动态改变根节点的>// packages/utils/src/theme.ts export function applyTheme(theme: 'light' | 'dark') { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('app-theme', theme); }

为了配合动态换肤,后端同学配合提供了一个接口,允许不同租户配置自己的品牌色。前端在应用启动时拉取配置,合并到--primary-color之上。这就是CSS变量最大的价值:把视觉定义权从代码中剥离出来,变成运行时数据。

2.3 国际化:动态文案与组件解耦

国际化(i18n)不仅仅是把中文替换成英文,它会导致一个非常棘手的问题:组件内部如果带有文案,就无法和文案资源解耦。

我的做法是,所有的文案资源都被抽成独立文件,并以“命名空间”区分:

// packages/ui/src/locales/zh-CN.ts export default { 'ui.button.confirm': '确定', 'ui.table.empty': '暂无数据', 'ui.modal.close': '关闭', }; // packages/ui/src/locales/en-US.ts export default { 'ui.button.confirm': 'Confirm', 'ui.table.empty': 'No Data', 'ui.modal.close': 'Close', };

组件内不直接写中文,而是通过一个useLocale的Hook获取当前语言环境下的文案:

// packages/ui/src/base/Modal/index.tsx import { useLocale } from '@/hooks/useLocale'; export function Modal({ open, onClose, children }) { const { t } = useLocale(); return ( <div className={...}> <button onClick={onClose}>{t('ui.modal.close')}</button> {children} </div> ); }

应用层的业务文案则放在各自的语言包里。由于采用Monorepo,组件库的语言包和应用层的语言包可以独立维护,互不干扰。切换语言时,需要同时变更两个层级的文案资源,这在实践中需要格外细心,很容易出现“组件变成英文了,但业务菜单还是中文”的尴尬局面。我的解决方案是提供一个统一的LocaleProvider,它负责收集所有层级的语言包,然后下发到各个模块。

2.4 通用组件封装原则:保持扁平,避免“上帝组件”

做组件库最忌讳的是把所有功能塞进一个组件里。比如一个Table组件,如果既要支持排序、过滤、分页、树形展示、拖拽列宽,又要支持单元格合并、行选择、自定义操作列,那这个组件的props会膨胀到难以维护,最终变成一个谁也不敢动的“上帝组件”。

我的封装原则有三条:

  • 单一职责:组件只做一件事,不要尝试在一次迭代中满足所有人的要求。
  • 组合优于配置:与其给Table加一个enableRowSelection布尔值,不如提供一个SelectionTable组件,它内部组合了Table和Checkbox。
  • 受控与半受控:尽量让组件支持受控模式(外部传入value和onChange),这样业务代码可以灵活控制组件状态。

举一个搜索表单的例子。业务里经常有“筛选条件”区域,输入框、下拉框、日期选择器一应俱全。如果我把这些全部封装成一个庞大的SearchForm组件,配置化渲染,那业务方确实省了代码,但代价是灵活性丧失殆尽。我的做法是只提供一个FormLayout组件,负责处理栅格布局和间距,具体的表单控件由业务侧自由组合。这样既保证了视觉上的整洁统一,又没有限制业务方的发挥空间。

3. 实操过程与核心环节实现

3.1 从零初始化Monorepo工程

Monorepo的搭建是整个项目的基石。我使用的是pnpm作为包管理器,配合pnpm-workspace.yaml文件定义工作区。

# pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*'

根目录的package.json里,我只保留了公共的开发依赖和脚本命令,实际应用和包的依赖都由各子包自行管理。

{ "name": "rea-monorepo", "private": true, "scripts": { "dev:admin": "pnpm --filter @rea/admin dev", "build:admin": "pnpm --filter @rea/admin build", "build:ui": "pnpm --filter @rea/ui build", "lint": "eslint \"{apps,packages}/**/*.{ts,tsx}\"" }, "devDependencies": { "typescript": "^5.3.3", "eslint": "^8.56.0", "prettier": "^3.1.1" } }

这里有个关键细节:子包之间的依赖声明。比如@rea/admin依赖@rea/ui,在apps/admin/package.json中需要注明:

{ "dependencies": { "@rea/ui": "workspace:*" } }

pnpm的workspace:*协议会自动将本地的@rea/ui包链接过来,而不用每次都执行pnpm publish。开发时,改动@rea/ui的源码,admin应用能够即时生效(因为Vite直接编译了源码,而不是编译后的产物)。

注意: 在Monorepo中,组件库的构建模式需要特别留意。如果你改的是@rea/ui的src目录,而admin应用引用的是@rea/ui/dist目录(即构建产物),那么开发时改动不会生效。因此,在开发环境下,我会把组件库的package.json里main字段指向src/index.ts,而在发布时再用构建脚本切换到dist目录。这是一个非常容易踩的坑。

3.2 构建配置:Vite插件与微调

Vite的配置是“Rea”工程链中比较关键的一环。除了常规的@vitejs/plugin-react之外,我还做了几个微调。

  • 路径别名:在apps/admin/vite.config.ts中配置@指向src。
// apps/admin/vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, 'src'), }, }, });
  • 组件库样式按需加载:为了减少首屏样式体积,我没有让组件库一次性引入所有样式。而是在组件库内部,每个基础组件都import自己的style.css。Vite在构建时会自动分析依赖,将用到的组件样式单独打包,实现样式层面的“按需加载”。

  • 构建缓存与CHUNK拆分:生产构建时,我会手动配置rollupOptions.output.manualChunks,把react、react-dom、zustand等稳定依赖拆分到独立Chunk,这样浏览器缓存命中率能提升不少。

build: { rollupOptions: { output: { manualChunks: { react: ['react', 'react-dom'], zustand: ['zustand'], }, }, }, },

3.3 核心代码实现:一个优雅的usePagination Hook

分页是几乎所有列表页面的刚需。我封装了一个usePaginationHook,它统一管理了分页状态、请求参数和列表数据。这个Hook是“Rea”里复用率最高的工具函数之一。

// packages/hooks/src/usePagination.ts import { useState, useCallback, useEffect } from 'react'; interface PaginationOptions<T> { fetcher: (params: { page: number; pageSize: number }) => Promise<{ list: T[]; total: number }>; initialPage?: number; initialPageSize?: number; } export function usePagination<T>({ fetcher, initialPage = 1, initialPageSize = 10 }: PaginationOptions<T>) { const [data, setData] = useState<T[]>([]); const [loading, setLoading] = useState(false); const [page, setPage] = useState(initialPage); const [pageSize, setPageSize] = useState(initialPageSize); const [total, setTotal] = useState(0); const loadData = useCallback(async () => { setLoading(true); try { const res = await fetcher({ page, pageSize }); setData(res.list); setTotal(res.total); } finally { setLoading(false); } }, [fetcher, page, pageSize]); useEffect(() => { loadData(); }, [loadData]); const refresh = useCallback(() => { loadData(); }, [loadData]); const setCurrentPage = (p: number) => setPage(p); const setCurrentPageSize = (s: number) => { setPageSize(s); setPage(1); // 改变每页条数时,重置到第一页 }; return { data, loading, total, page, pageSize, setCurrentPage, setCurrentPageSize, refresh }; }

使用的时候:

const { data, total, loading, page, pageSize, setCurrentPage, setCurrentPageSize } = usePagination({ fetcher: (params) => fetchUserList(params), });

这个Hook的精髓在于:它把请求状态与UI状态彻底解耦。业务页面不需要再关心loading如何赋值等问题,只需专注于渲染data。

3.4 核心代码实现:高性能虚拟列表

中后台系统经常遇到上千条数据渲染的场景,直接map渲染会卡死页面。我实现了一个轻量级的虚拟列表组件,只渲染可视区域内的行,大幅提升滚动性能。

核心思路是基于scrollTop和容器高度计算起始索引和结束索引。

// packages/ui/src/base/VirtualList/index.tsx import { useRef, useState, useEffect, useCallback, ReactNode } from 'react'; interface VirtualListProps<T> { items: T[]; height: number; itemHeight: number; renderItem: (item: T) => ReactNode; } export function VirtualList<T>({ items, height, itemHeight, renderItem }: VirtualListProps<T>) { const containerRef = useRef<HTMLDivElement>(null); const [scrollTop, setScrollTop] = useState(0); const totalHeight = items.length * itemHeight; const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 2); // buffer const endIndex = Math.min(items.length - 1, Math.floor((scrollTop + height) / itemHeight) + 2); const visibleItems = items.slice(startIndex, endIndex + 1); const handleScroll = useCallback(() => { if (containerRef.current) { setScrollTop(containerRef.current.scrollTop); } }, []); useEffect(() => { const container = containerRef.current; if (!container) return; container.addEventListener('scroll', handleScroll, { passive: true }); return () => container.removeEventListener('scroll', handleScroll); }, [handleScroll]); return ( <div ref={containerRef} style={{ overflowY: 'auto', height }} className="virtual-list" > <div style={{ position: 'relative', height: totalHeight }}> <div style={{ position: 'absolute', top: startIndex * itemHeight, left: 0, right: 0, }} > {visibleItems.map((item, index) => ( <div key={index} style={{ height: itemHeight }}> {renderItem(item)} </div> ))} </div> </div> </div> ); }

这里有几个细节值得解释:

  • 动态高度问题:如果列表项高度不固定,虚拟列表的复杂度会直线上升。我的策略是:给每个列表项一个预估高度,渲染后再通过ResizeObserver修正实际值。但为了简单,这个版本假设了固定高度。实际业务中,如果高度变化很大,建议直接用成熟的方案(如社区内的虚拟滚动库)。
  • 缓冲区域:在计算起始索引时往前多渲染2个、往后多渲染2个,避免快速滚动时出现短暂白屏。

3.5 状态驱动UI:基于角色的菜单生成

菜单在“Rea”里不是静态配置的,而是根据登录用户的权限动态生成的。后端在登录接口中返回了菜单树结构,前端拿到之后动态注册路由和渲染菜单。

// apps/admin/src/store/auth.ts interface UserInfo { name: string; roles: string[]; menus: MenuItem[]; } interface MenuItem { id: string; title: string; path: string; icon: string; children?: MenuItem[]; }

前端在路由初始化时,不再使用静态的routes,而是将后端的菜单数据映射为Vue/React路由结构。这样,菜单和路由由同一个数据源驱动,不会出现“菜单有显示但路由未注册”的不一致问题。

在渲染侧,我用了一个递归组件主要是为了处理多级菜单。菜单每一项的显示与否,完全取决于它是否出现在用户数据中。

4. 常见问题与排查技巧实录

4.1 启动报错:Module not found: ESM packages

这是使用Vite + Monorepo最常见的痛点。当你在本地开发时,某个依赖包(通常是React或Zustand)的版本在两个子包中不一致,会导致“Duplicate React”或者莫名其妙的ESM错误。

我遇到的报错是:

[vite] Internal server error: Failed to resolve import "react/jsx-runtime" from "..."

排查过程:

  1. 先检查node_modules中是否出现了多个react副本。
  2. 使用pnpm why react命令查看依赖树,发现admin应用依赖react@18.2.0,而ui包某个间接依赖锁定了react@18.2.0的另一个版本(实际上是因为peerDependency导致重复安装)。
  3. 解决方案:在根目录的package.json中添加resolutions字段(或使用pnpm.overrides)来强制统一React版本。
{ "pnpm": { "overrides": { "react": "18.2.0", "react-dom": "18.2.0" } } }

在Monorepo中,统一核心依赖版本是铁律。不要在子包里随意安装不同版本的React、Vue或其它框架级依赖,否则你在排查问题时会被各种诡异错误折磨。

4.2 样式隔离失效:CSS Modules与全局样式纠缠

在使用CSS Modules的过程中,我遇到过组件库样式被外部某个全局CSS覆盖的问题。原因是一位同事在应用层写了一个全局样式:

/* apps/admin/src/styles/global.css */ button { border-radius: 0; }

这个全局选择器直接影响了组件库中Button组件的圆角。排查困难是因为组件库单独开发环境下没有这个全局样式,一切正常;联调时问题才暴露。

解决办法有两个方向:

  1. 约定:应用层的全局样式只用于重置基础样式(Reset),不允许对具体HTML标签定义视觉样式。
  2. 加固:组件库的每个根节点都有自己独一无二的className前缀,例如rea-btn,以此增加选择器权重,并在最前面引入Css Baseline(类似Reset)。

最终我两者都做了。在组件库里,我引入了一份精简的base-reset.css,确保组件在不同宿主环境下的渲染一致性;同时要求应用层不要写全局标签选择器。

4.3 数据缓存陷阱:列表页返回时状态丢失

另一个高频问题是:从列表页点击“详情”跳转,再返回时,列表页的页码、筛选条件、滚动位置全部丢失,用户必须重新从头排查,体验很糟糕。

这其实是一个“前端路由缓存”问题。React Router默认情况下,组件卸载后状态就会被销毁。我的解决方案是使用keepalive思想。

在React中实现Keepalive比Vue要麻烦一些。我没有采用复杂的缓存库,而是用zustand把列表页的关键状态(筛选参数、分页信息、滚动位置)提升到全局Store中,在组件卸载时保存、重新挂载时恢复。

// apps/admin/src/store/userList.ts interface UserListState { filters: Record<string, any>; page: number; pageSize: number; scrollTop: number; setFilters: (filters: Record<string, any>) => void; setPage: (page: number) => void; setScrollTop: (scrollTop: number) => void; }

这样一来,列表页的组件本身是无状态的,它只是全局状态的“投影”。这种方法对业务侵入小,且可维护性强,比引入一个重型的缓存路由库要轻量得多。

4.4 构建速度优化:从15分钟到3分钟

项目运转一段时间之后,生产构建越来越慢,最后到了15分钟级别,严重拖累发布效率。我做了三步优化:

  1. 构建缓存:开启Vite的build.cacheDir,让构建过程中依赖的二次编译结果可以被缓存。
  2. 多进程压缩:使用vite-plugin-compression插件,并指定terserOptions的多进程参数。后来换用esbuild作为压缩器,比Terser快了好几倍。
  3. 忽略无用文件:在tsconfig中设置include字段,确保TypeScript编译器只处理应用的源码路径,不去扫描node_modules里的类型声明。

优化后的构建时间稳定在3分钟以内。核心收益来自于减少工作量,而不是单纯提升单次计算速度。

4.5 网络请求竞态:快速切换Tab时响应错乱

这是个大坑。在“数据看板”页面,用户快速切换Tab,每个Tab都有对应的异步请求。但异步响应回来的顺序不一定和请求顺序一致,导致先发出去的请求后返回,把后发出的数据覆盖了。

排查时发现,组件卸载后,网络请求的回调仍然触发了setState。React 18中这不会直接报错(旧版本会警告),但会导致状态污染。

解决思路:

  • 将数据请求与组件生命周期解耦,请求的结果放入全局Store,页面渲染时从Store读取某个Tab对应的数据。
  • 或者,在请求层增加一个“请求序列号”校验,只有最新序列号的请求才能写入状态。

我最终选择了方案二,封装了一个简单的useRequestSequence:

// packages/hooks/src/useRequestSequence.ts import { useRef, useCallback } from 'react'; export function useRequestSequence<T>() { const seqRef = useRef(0); const run = useCallback(async (request: () => Promise<T>, onSuccess: (data: T) => void) => { const seq = ++seqRef.current; try { const data = await request(); if (seq === seqRef.current) { onSuccess(data); } } catch (error) { if (seq === seqRef.current) { throw error; } } }, []); return { run }; }

在Tab切换时,每次请求都会递增序列号,旧请求即使返回了,也会被丢弃。

5. 经验沉淀与扩展思考

5.1 组件库文档的自动同步

组件库如果没有良好的文档,团队成员很快就会弃用,或者重复造轮子。“Rea”使用了一种“文档即Demo”的方式。每个组件的examples目录就是一个可运行的Demo,同时它被自动抓取到文档站的对应位置。每次修改组件代码或Demo,文档站更新后显示的内容始终与最新代码保持一致。这个思路源于Storybook,但实现起来更轻,只要约定目录结构和自动扫描规则即可。

5.2 代码生成的辅助工具

在一个大型项目里,重复劳动不可避免。写一个新页面,要新建路由、创建Store、写API请求函数、写列表页模板。为了快速生成这些样板代码,我写了一个名为generate-page的脚本。运行如node scripts/generate-page.ts user/list,它就会自动生成页面文件夹、对应的路由配置、初始Store文件和API封装。它不只是简单的“复制粘贴模板”,而会根据页面类型(列表页/详情页/表单页)预置不同的初始结构。

这种自动化脚本的价值在于:它把团队规范直接固化在工具里,新同事加入时,不需要读长篇大论的设计文档,而是直接运行命令,生成符合规范的模板代码,极大降低了上手门槛。

5.3 关于“React Enterprise Application”的一些思考

做完整个项目,我的体会是:前端架构不是一蹴而就的,它是在一次次重构和换血中逐步长出来的。“Rea”这个项目最大的收获不是某个UI组件写得多漂亮,而是一个可演进的架构底座。它允许我们在不推翻整体的前提下,持续加入新能力(比如将来接入微前端、或替换构建工具)。

如果让我重新做一次,我可能会在更早的时间点引入设计Tokens体系。现在虽然使用了CSS变量管理视觉风格,但业务组件里仍然有一些硬编码的颜色值隐藏在某个角落。设计Tokens可以在设计侧和代码侧形成一一对应关系,让视觉变更的工程量进一步降低。这是一个值得后续继续深挖的方向。

“Rea”目前还在持续迭代。从最初的一个模糊缩写,到现在组件数量超过40个、覆盖3个业务系统的基础设施,这条路走下来踩过的坑几乎都写在了这篇文章里。希望这些经验能帮你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 16:35:32

特征提取、特征选择与聚类分析:机器学习数据挖掘完整链路解析

在模式识别与机器学习这条路上&#xff0c;特征提取、特征选择与聚类分析这三个名字经常一起出现&#xff0c;但真要问它们各自解决什么问题&#xff0c;很多刚开始接触的同学都会卡壳。L10这一讲把三个主题放在一起&#xff0c;我最早以为只是课程章节的安排&#xff0c;直到自…

作者头像 李华
网站建设 2026/10/20 16:35:09

AI短剧工具实测方法论:用标准化测试镜头量化评估6大核心指标

1. 为什么“用同一条测试镜头先测再买”是新手做AI短剧最不该跳过的一步刚入局AI短剧的朋友&#xff0c;十有八九会卡在第一步&#xff1a;工具怎么选&#xff1f;MidJourney、DALLE、Stable Diffusion、Runway、Pika、Kaedim、Suno、Udio……光是名字列出来就让人眼花。更别提…

作者头像 李华
网站建设 2026/10/20 16:33:53

自然语言到优化求解:构建可解释的语义解析与约束建模系统

1. 项目概述&#xff1a;这不是一个“调包”任务&#xff0c;而是一次对NLP与优化底层逻辑的重新握手“从零构建一个自己的自然语言优化求解系统”&#xff0c;这个标题里没有“大模型”“微调”“RAG”这些高频热词&#xff0c;却藏着当前NLP工程实践中最被忽视的一环——当语…

作者头像 李华
网站建设 2026/10/20 16:33:12

基于预设性能的无模型自适应分数阶快速终端滑模控制与仿真

1. 控制难题拆解与现代方案的整体思路做非线性系统控制的人&#xff0c;不管是搞学术研究还是做工程落地的&#xff0c;迟早都会撞上同一堵墙&#xff1a;被控对象数学模型的精确表达式越来越难拿到。实际系统里存在未建模动态、参数摄动、外部扰动、耦合项&#xff0c;强行依赖…

作者头像 李华
网站建设 2026/10/20 16:33:10

一个RAR里的商业HIS:部署验证与改造指南

简介&#xff1a;这是一套面向小型诊所与基层医疗机构的轻量级HIS系统源码包&#xff0c;基于ASP.NET技术构建&#xff0c;以C#源代码、aspx页面、JavaScript与CSS前端文件为核心&#xff0c;同时包含DLL库、配置文件及少量可执行程序&#xff0c;便于开发者直接编译部署或进行…

作者头像 李华
网站建设 2026/10/20 16:32:01

std::mutex静态初始化顺序:启动期崩溃的排查与规避

某天凌晨&#xff0c;一个已经稳定运行了几个月的服务程序突然崩溃。拉下转储&#xff0c;调用栈只有三四帧&#xff0c;最深的地方是一个std::mutex::lock()。从符号看&#xff0c;这个锁是某个模块里一个不起眼的全局静态std::mutex&#xff0c;代码里天天在用。但就是这么个…

作者头像 李华