ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

打造团队基础工程底座:Basis目录设计全解析

打造团队基础工程底座:Basis目录设计全解析 这段时间在整理团队的基础工程资产发现很多东西散落在各个仓库和个人项目里时间一长就变成了“只有作者能看懂”的黑盒。于是我干脆把所有沉淀下来的公共能力、配置规范、组件和工具全部收拢到一个叫Basis的项目里并且用一个目录串起所有模块让新同学进来以后不用靠口口相传直接把整个基础层看一遍就能上手。今天这篇内容就是把“Basis 目录”本身拿出来拆一遍聊聊它为什么这样设计、每个目录背后解决了什么问题以及我实际搭建过程中踩过的坑。这篇文章适合两类人看一是团队里有基础能力建设需求、准备搭一套公共工程底座的人二是正在做个人项目整理、想把自己零散的工具链变成一个结构化工程的同学。我会尽量说人话能给出参考命令和文件结构的地方也会直接贴出来方便你照着落。1. Basis 项目定位这个目录到底在管什么1.1 为什么叫 Basis 而不是“公共库”或“基础模块”项目名定为 Basis就是因为这个词在英文里本身就带“基石、基准、基底”的意思。我要做的不是一个 UI 组件库也不是一套后端框架而是一个介于“工程规范”和“业务代码”之间的基础能力集合。它要解决的是每接一个新项目、每来一个新成员都要重复处理的那部分事环境怎么配、代码怎么写、请求怎么发、报错怎么记、权限怎么接、组件怎么抽象、文档怎么组织。叫 Basis 的好处是概念边界特别清晰。团队里不管是谁只要一听这个项目名就知道“这不是业务代码这是所有业务代码长出之前要先铺好的底层”。一旦项目名变得明确目录结构反而好设计了因为它不需要刻意照顾某个具体业务方向只需要回答一个问题一个正经工程从初始化到稳定运行到底需要哪些基础设施。1.2 目录服务的对象和边界我在设计目录前先明确了这个 Basis 目录服务的对象有三类业务研发同学他们需要快速找到公共组件、请求封装、权限逻辑、工具函数并且知道“什么场景该用什么模块”。前端基建同学他们需要维护和扩展这些基础能力同时保证版本稳定和文档完整。新入职成员他们需要一个自解释的工程入口顺着目录就能理解团队约定而不是到处找群聊记录和旧项目源码。边界也在目录里写得很死Basis 不做具体业务功能不允许出现“订单列表”“用户管理”这类业务页面Basis 可以放“用户登录状态管理”但不会放“用户注册流程”。边界画清楚以后目录自然就稳定了不用三天两头因为业务变化去动基础层。2. Basis 目录的整体设计一张地基图是怎么铺开的2.1 顶层模块划分的三条原则Basis 的顶层目录不是拍脑袋定的我按三条原则划分。第一条是“按职责分层不按技术栈分”。你可能会想前端项目一般按components、utils、hooks这样分不就行了但实际跑一段时间就会发现单纯按技术类型分会让目录变成一个杂物仓库components里既有图表组件又有布局组件utils里既有日期格式化又有请求拦截谁都能往里面扔东西最后谁都不好找。Basis 改成按职责分比如configs只管配置、core只管运行时能力、packages只管可复用资产每个模块的目标用户和使用场景非常清楚。第二条是“目录即流程”。一个新项目接入 Basis 时第一步看配置、第二步引入运行环境、第三步接入公共能力、第四步引用组件和工具、最后看文档和示例。这个流程的顺序就对应了目录从上到下的排列顺序你顺着目录读一遍就等于走了一遍工程初始化流程。第三条是“一切要可删减”。基础工程最忌讳大而全。Basis 虽然目录多但每个目录之间没有强依赖比如你的项目不需要国际化那直接把i18n相关部分删掉不影响整体运行。所以目录里的每个模块在文档开头都要注明“可选”还是“必选”避免团队照单全收后背上沉重包袱。2.2 顶层目录树与模块职责下面是我实际在用的 Basis 项目中目录结构的简化版basis/ ├── configs/ # 工程配置lint、构建、环境变量、提交规范 ├── core/ # 运行时核心入口、容器、插件机制 ├── services/ # 公共服务请求、日志、存储、埋点、权限 ├── packages/ # 可复用资产组件、工具函数、Hooks ├── docs/ # 文档中心使用指南、架构说明、API 文档 ├── examples/ # 示例工程最小可运行案例 ├── scripts/ # 工程脚本发布、初始化、代码生成 ├── tests/ # 基础层自测单元测试、集成测试 └── package.json # 工程入口模块职责对应表目录核心职责关键词configs统一工程规范和编译参数标准化core提供运行容器和插件扩展机制内核services对接外部能力的公共封装能力集成packages面向业务的复用资产开箱即用docs记录设计与用法自解释examples提供最小实践样例可复现scripts提升自动化效率工程化tests守住基础层质量底线稳定性这套目录结构看起来并不炫技但它恰恰解决了我最头疼的问题以前项目里utils越来越大、components越来越乱现在职责一旦划分清楚新增代码只需要想清楚“这属于配置、能力还是资产”就能在十秒内找到应该放的位置。3. 核心子模块拆解与实操要点3.1 configs把“约定”固化成“配置”configs这个目录是 Basis 里最容易被低估的模块。很多人觉得配置不就是.eslintrc、tsconfig.json、prettierrc嘛直接复制不就行了。但真正维护过后你会发现配置散落在各项目里就会出现“A 项目 lint 严一些、B 项目 build 有私有脚本、C 项目完全没有 test 配置”的混乱局面。Basis 的做法是把所有通用配置收拢到configs下并且区分成两类标准配置eslint.config.js、prettier.config.js、tsconfig.base.json、commitlint.config.js这些是团队约定不允许业务项目随意改动。模板配置build.base.js、env.development.ts、env.production.ts业务项目可以基于模板覆盖部分字段但核心参数不能动。这里有一个很实在的细节tsconfig.base.json里的路径别名/指向哪个目录直接影响业务代码的导入方式。我一开始没统一结果有的项目/components指到src/components有的指到packages/components后面 Basis 里所有组件引用时路径非常混乱。后来我把别名规则统一为“所有/开头的内容都必须先经过 Basis 的路由解析默认指向packages目录”并且写进了基础配置这个问题才彻底消失。3.2 core运行时内核和插件机制Basis 的core不承载具体业务功能它只做三件事初始化应用容器、管理应用生命周期、提供插件挂载点。为什么需要这东西因为没有内核的话Basis 里所有 services 和 packages 都是平铺的散件业务项目接入时要自己约定初始化顺序先建请求实例还是先初始化日志存储模块要不要在登录前就加载这些顺序问题如果不在内核里统一处理几十个项目会有几十种初始化顺序排查问题的时候非常痛苦。Basis 的做法是内置一个极简插件系统所有 services 都是插件。核心代码大概长这样// core/plugin.ts export type Plugin { name: string; setup(context: AppContext): void | Promisevoid; }; export class AppKernel { private plugins: Plugin[] []; use(plugin: Plugin) { this.plugins.push(plugin); return this; } async bootstrap() { for (const plugin of this.plugins) { if (plugin.setup) { await plugin.setup(this.context); } console.info([basis] plugin loaded: ${plugin.name}); } } }业务项目接入时只需要一行一列地注册插件比如import { AppKernel } from basis/core; import { httpService } from basis/services/http; import { loggerService } from basis/services/logger; const kernel new AppKernel(); kernel.use(httpService).use(loggerService); kernel.bootstrap().then(() { // 启动业务应用 });这种设计最直接的好处是你可以“按需取用”不需要埋点就把埋点插件注释掉内核和其余插件完全不受影响。我在实际落地中体会到插件化不是为了炫技而是为了让基础层和业务层彻底解耦真正实现“基础层换模块不影响业务”的目标。3.3 services公共能力要的是“默认可用”services目录收的是面对外部能力的公共封装包括请求模块、日志模块、存储模块、埋点模块、权限模块。做这层封装时我的原则只有一个默认可用必要可配。以请求模块为例。Basis 里的http不是简单包一层 axios 完事它内置了统一的超时设置、错误码映射、登录态失效自动跳转、接口日志输出等能力。新项目接进来以后不发一行配置也能正常发起请求只有遇到特殊接口时才需要传入覆盖参数。// services/http/index.ts import axios, { AxiosRequestConfig } from axios; const service axios.create({ timeout: 10000, }); service.interceptors.response.use( (response) { // 统一处理业务状态码 if (response.data.code ! 0) { return Promise.reject(new Error(response.data.message)); } return response.data.data; }, (error) { // 统一处理登录失效等场景 if (error.response?.status 401) { window.location.href /login; } return Promise.reject(error); } ); export function requestT(config: AxiosRequestConfig): PromiseT { return service.request(config); }这里我想特别提一个常被忽略的模块权限。Basis 的permission不只做按钮级权限控制它把权限抽象成“资源点”统一管理“用户是否有权访问某个资源”。目录里有个resource.ts专门维护所有权限标识业务项目不用自行硬编码字符串判断权限降低了被绕过或写错的风险。3.4 packages组件、工具和 Hooks 的正确打开方式packages是业务同学使用频率最高的目录里面分成三类components/布局类、业务通用类、数据展示类组件。hooks/与业务状态相关的组合式函数如useTable、useForm、usePermission。utils/与框架无关的纯函数如日期格式化、对象深拷贝、字节格式化。在目录设计上每个 package 内部都遵循统一规范packages/ └── components/ └── Button/ ├── index.ts ├── types.ts ├── button.tsx ├── style.css └── README.md我发现少一个 README 都会带来许多困扰。组件有人改了 API 但不知道其他人继续按老参数用报错了还要花时间排查。所以从 Basis 开始我就强行规定凡进入packages的资产必须有 README至少要写清楚 props、事件、使用示例。没有文档的资产不允许合并到主分支这个约定靠 Code Review 执行效果很好。3.5 docs 和 examples目录里的“说明书”与“跑通路径”一个项目只有代码没有文档等于没做基建。docs目录采用分层结构guide/给新人看的快速开始、目录说明、开发流程。architecture/基础层的架构决策记录为什么这么设计。api/各个模块的 API 文档由代码注释自动生成。examples则是最能反映“目录可用性”的部分。我里面放了一个最小可运行项目它不包含任何业务逻辑只是把所有 Basis 的必选模块接好并对跑通做验证。新人 clone 下来执行npm install和npm run dev就能看到一个干净可用的工程接下来要做什么业务在示例基础上加代码就行。我自己的经验是写完 examples 以后一定要执行一遍“从零接入流程”删掉node_modules、清空缓存、按 README 操作一遍看有没有遗漏步骤。很多时候文档看起来完美但实际操作时才发现某个环境变量没配、某个依赖没写进package.json这个坑我至少踩过三次。4. 从 0 到 1 搭建 Basis 目录的实操过程4.1 落地顺序先跑通骨架再填模块如果让我重新做一遍 Basis我会严格遵守下面这个顺序先建仓库和目录骨架只建空的目录结构、package.json、README.md不做任何功能。配好 configs把 Lint、Prettier、TypeScript、Commitlint 基础配置放进去确保所有模块在统一规范下开发。实现 core 内核先写插件机制和生命周期因为所有 services 都要挂到内核上。实现最核心的 services优先做http和logger因为其他模块大多依赖它们。抽一个最小示例用 core http logger 跑通一个完整页面确认链路可用。逐步补齐 packages每加一个资产前先写文档再写代码。最后写 docs有了实际运行经验后写文档比凭空写靠谱得多。4.2 搭建过程中我踩过的典型坑坑一目录一开始分得太细导致“不知道该放哪”。我最早把packages下面直接分了几十个二级目录比如date、format、dom、image。结果业务同学要加一个图片压缩函数时在image和utils之间犹豫半天最后还是按自己的理解乱放。后来我把二级目录收敛成只有components、hooks、utils三类只有当某个分类下的资产超过十个以上才允许新建子分类。分类越少归属越清晰。坑二过早优化配置让项目看起来很重。我在 v1 版本里加了非常多的 eslint 规则和 TS 严格检查刚接入的头两天团队里抱怨声一片因为明明业务代码没问题lint 却因为一个自定义规则报了错。后来我把那些“建议级”的约束全关掉只保留“错误级”并且把规则调整流程写进 README先记录在adr文档里评审通过后再全局开启。这个方式平衡了规范性和推进效率。坑三更新基础层时没有考虑兼容性。有一次我升级了logger模块的初始化参数但只改了自己正在做的项目结果其他三个业务项目重新安装依赖后直接报错。从那以后我在 Basis 的scripts里加了一个pre-publish脚本每次发布前自动扫描所有使用 Basis 的工程项目用npm outdated检查依赖版本并在核心 API 变更时输出迁移提示。基础层的兼容性永远比内部实现优雅重要。4.3 scripts 工具链让目录“活”起来纯静态目录是没有生命力的所以我写了一批自动化脚本放在scripts下保证目录和实际工程一致npm run create:package --name Button自动生成包目录的模板文件省去手写结构的时间。npm run check:circular检测 packages 之间的循环依赖避免基础层互相牵扯。npm run docs:build根据代码注释生成 API 文档并输出到 docs/api。npm run release自动更新版本号、生成 CHANGELOG、发布 npm 包。自动化脚本里我最看重check:circular。基础层的包一旦循环依赖淡感觉不到项目一大就容易出现内存溢出或者加载顺序崩溃。这个脚本利用madge扫一遍目录结构直接输出异常依赖链基本每次运行都能筛出一两个问题。这种自检脚本是可以长期复利的投入强烈建议做基建的同学都配上。5. 常见问题与排查技巧实录5.1 新项目接入后白屏或加载顺序异常现象新项目接入 Basis 的 services 后页面一直白屏控制台报某个 plugin 的setup阶段未完成。排查思路这类问题九成是插件初始化顺序问题。比如permission插件依赖http已经创建了实例如果permission注册在http前面就会拿到undefined。解决办法在core/plugin.ts中增加依赖声明字段注册时按依赖关系排序。export type Plugin { name: string; deps?: string[]; setup(context: AppContext): void | Promisevoid; }; function sortPlugins(plugins: Plugin[]): Plugin[] { return plugins.sort((a, b) { if (a.deps?.includes(b.name)) return 1; if (b.deps?.includes(a.name)) return -1; return 0; }); }有了显式依赖声明后即使注册顺序写错了内核也能自动调整白屏问题基本绝迹。5.2 公共组件更新后业务项目看不到最新效果现象Basis 的Button组件更新了样式但业务项目装了最新包之后样式没变。排查思路绝大多数是缓存问题可能是 npm 缓存、webpack 的持久化缓存或者是项目内自己覆盖了同名的 CSS 类名。解决办法先清 npm 缓存再清构建缓存如果仍然无效就去业务项目里搜索是否覆盖了button相关样式。我在做 Basis 时所有组件样式都用>
返回列表