如果你在一个项目目录里看到“rea”这种只有一个单词的代号,别急着按常规思路去搜框架文档,它未必是某个开源框架的名字。我手里这套系统就把“rea”作为内部代号,全称是 Refined Execution Architecture,意思是一套被反复打磨过的执行结构。它不做宏大宣言,只解决一类很实际的问题:当业务代码散落在多个端、多套工程里时,怎么统一请求、状态、路由和构建配置,让开发者把注意力放回业务本身。适合正在做跨端应用、频繁切换工程、或者维护一堆历史代码的开发者参考。
1. 项目定位与整体设计思路
1.1 这个项目到底解决什么问题
聊聊我实际接手的经历。某跨平台系统内部有 Web、小程序、桌面端三套代码,每套代码里都有自己一套请求、拦截器、store 初始化逻辑。看起来都是“发个请求、拿个数据”,但写法五花八门。有的页面把 loading 状态挂在全局变量里,有的请求模块把 token 写死在 header 中,有的接口路径前缀直接硬编码在业务代码里。后来我重构时,发现不能继续堆功能,得先立规矩。
rea 的设计目标不是做一个新框架,它的核心是“约定优先”。项目边界非常清楚:不动业务代码的写法,只统一基础设施层;所有模块通过一个 rea.json 做配置入口;运行、构建、发布命令收敛成 rea 前缀的子命令;模块可以独立抽取,不留隐式依赖。
它解决的是团队协作里的“配置没人敢动”问题。大量时间浪费在环境变量、代理规则、接口前缀上,rea 把这些自上而下收敛,出现问题了直接去 rea.json 找答案,比翻代码历史高效得多。
1.2 为什么不做成一个重框架
可能有人问:既然是统一,为什么不直接做一个自研框架?我的回答是,业务变化太快,框架重了就很难适应。
rea 建立在几个成熟库之上,只覆盖四类能力:请求封装、状态加载、路由注册、构建命令。它自己不做虚拟 DOM 调度,不重复实现依赖注入。这种轻量设计有个直观收益:rea 的核心代码很少,出现问题能直接读完源码;同时它不影响按需升级,底层依赖版本升级时,rea 的接口保持稳定。
另一个原因是换人成本。新人接手项目,不需要搞清楚整套内部机制,只需要按目录约定放代码、在 rea.json 里配参数,五天之内能开始写业务。这比我之前经历的“先啃两周框架源码再动手”要友好得多。
2. 环境准备与项目初始化
2.1 前置环境清单
在动手之前,把环境对齐是第一步,避免到联调阶段才暴露版本差异。下面是我实际记录的版本组合:
| 项目 | 版本区间 | 说明 |
|---|---|---|
| Node.js | 18.0 及以上 | 匹配 rea 内置的 fetch、AbortController 等 API |
| pnpm | 8.15 及以上 | 默认包管理器,也可以使用 npm,但不建议混用 |
| 浏览器 | 现代浏览器 | 小程序端不依赖 DOM API |
如果 Node 版本低于 18,rea 的请求层会回退到 polyfill 方案,虽然也能跑,但重试场景下的超时处理就有偏差。我建议直接用当前 LTS 版本,省心。另外,页面开发时如果用了很新的 CSS 特性,最好在构建配置里加上目标浏览器字段,避免上线后被反馈“样式不对”。
2.2 从零初始化一个rea项目
初始化命令:
npm create rea-app@latest demo-app cd demo-app rea install rea dev第一步会生成一个最小的项目骨架,里面包含 rea.json、package.json、src 目录。rea install 和 npm install 的差异在于它会额外检查 rea.json 里的依赖声明,提前报出模块不匹配的问题,而不是等到运行时才报错。rea dev 启动本地开发服务,默认端口是 4173,如果端口冲突它不会自动跳过,而是明确提示你修改配置,这个设计我觉得很实用,避免“服务启动了但并不是自己想要的环境”的错觉。
2.3 目录结构说明
初始化后的标准目录:
demo-app/ rea.json package.json src/ core/ # 请求层、配置解析 features/ # 业务模块,每个模块自包含 store、api、组件 shared/ # 公共组件与工具 scripts/ build.mjs # 构建脚本为什么要按 features 分目录?因为 rea 的状态和请求是按业务模块加载的,如果全部堆在 core 里,模块之间的边界会模糊,做按需加载就很被动。实际协作中,features 目录让多人开发时冲突概率明显降低,大家各自管自己的目录,合并代码时非常顺。
3. 核心实现原理与关键配置
3.1 统一配置入口 rea.json
rea.json 是项目里最关键的静态文件。它保存了环境、接口前缀、超时时间、重试策略、构建目标等信息。
{ "app": { "name": "demo-app", "version": "0.1.0" }, "network": { "baseURL": "/api", "timeout": 15000, "retry": { "maxTimes": 2, "enableOnGET": true, "enableOnPOST": false } }, "build": { "target": ["web", "miniapp"], "sourcemap": false, "bundleLimit": 120 } }这里面的 timeout、retry、bundleLimit 都是之后性能优化要惦记的参数。配置解析的顺序是环境变量优先于 rea.json,命令行参数优先于环境变量:
| 来源 | 优先级 |
|---|---|
| 命令行参数 | 最高 |
| 环境变量 | 中 |
| rea.json | 最低 |
这么设计的好处是可复现。CI 环境里如果需要覆盖接口前缀,直接通过环境变量注入,不用改仓库里的配置文件,避免“本地正常、线上异常”的经典问题。
3.2 请求层封装的关键点
rea 的请求层核心不是简单包装 fetch,它做了两件很实际的事:超时中断和重试策略。
超时用 AbortController 实现:
function requestWithTimeout(url, options = {}, timeout = 15000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); return fetch(url, { ...options, signal: controller.signal }) .finally(() => clearTimeout(timer)); }很多人写超时会忽略清理定时器,结果请求完成后、定时器还在,造成无谓的 tick。这里用 finally 清掉。
重试策略则是按请求方式区分:GET 因为幂等,可以安全重试;POST 不默认重试,防止订单、写入类接口被重复提交。这个决策看似简单,实际救了不少次线上事故。我建议任何请求层都严格按照 HTTP 语义来定重试策略,别一刀切。
3.3 状态加载与多端复用
状态层我只做一件事:引入统一的数据加载状态。用一句话概括:业务代码只管触发数据和读数据,loading、error、success 的流转由 rea 处理。
function createLoadable(asyncFn) { let state = 'idle'; let data = null; let error = null; const callbacks = new Set(); async function run(...args) { state = 'loading'; notify(); try { data = await asyncFn(...args); state = 'success'; } catch (e) { error = e; state = 'error'; } notify(); return data; } function subscribe(fn) { callbacks.add(fn); } function notify() { callbacks.forEach(fn => fn({ state, data, error })); } return { run, subscribe }; }这么做最大的收益不是省代码,而是让所有模块的加载表现一致。不管在 Web、小程序还是桌面端,loading 状态在同一个位置读取,页面的骨架屏、错误提示、重试按钮都能复用同一套逻辑。它避免了我以前见过的一类问题:每个页面都自己维护 isLoading 和 errorMessage,状态不一致导致 UI 闪烁。
3.4 路由注册与懒加载
路由不复杂,但有一个细节值得说:rea 支持按模块注册路由,并默认懒加载。路由配置写在 features 目录下的同名文件里。开发模式下,路由懒加载带来的体感提升不大;但到生产构建,首屏差异非常明显。如果所有页面都同步加载,产物体积会暴涨,尤其当项目页面超过二十个时,打包时间也会显著增加。按路由拆包是构建优化里性价比最高的一步。
4. 实操过程:把rea接入一个旧项目
4.1 接入前的问题清单
我实际接入旧项目时,并没有直接跑脚手架,前期先做了评估:
- 是否有统一构建工具链?没有的话需要先引入构建入口。
- 请求逻辑是否分散在几个工具文件里?需要收敛。
- 状态管理是否多个模块重复实现?需要包一层加载状态。
- 接口路径前缀是否已经统一?没有要先把路径整理出来。
先列问题清单,再动手,能省一半时间。因为我见过一些人直接套用脚手架,结果旧代码里的全局对象调用、相对路径、全局 store 全都不兼容,最后回滚。评估阶段不需要写任何代码,只做盘点,能极大降低迁移风险。
4.2 老项目接入的三步迁移
第一步,配置对齐。拷贝 rea.json,把 baseURL、timeout、retry 按照旧项目原有习惯设置。这一步不改业务代码。
第二步,请求替换。把旧代码里零散的 fetch、axios 调用替换为 rea.request。替换时注意旧接口可能存在 mock 字段,rea 里可以在 network 层配一个 mock 开关。
第三步,状态包一层。把原来散落的 loading、error 改成 createLoadable 包装。这一步是机械的,但做完后页面加载逻辑会统一很多。
三步迁移的原则是:每一步都要能单独提交、单独验证,不要一把梭。某一步出问题了,可以立即回滚到上一步。
4.3 联调阶段要注意的细节
联调最容易踩坑的是跨域代理。rea dev 内置代理配置,通常在 rea.json 的 proxy 字段里写目标地址。实际经验:前端代理路径别写绝对路径,统一用相对路径,配合环境变量动态切换。这样联调环境、预发环境、生产环境同一套代码不会因为前缀不一致炸掉。
还有一点,接口返回的结构不统一时,建议在统一响应拦截层做转换,不要在业务代码里到处判断 res.code === 0。把成功判断逻辑下沉一次,后续维护量降低很多。特别是老项目里,同一个字段在 A 接口叫 data、在 B 接口叫 result 的情况太常见了,拦截层做一层映射比到处补兼容代码干净得多。
5. 性能优化与构建细节
5.1 构建产物体积优化
rea 构建默认按路由拆包,再把第三方模块单独打成一个 vendor 包。这还不够,实际可以按几个方向继续压缩:
- 依赖库尽量用支持按需加载的版本,避免把整个组件库打进主包。
- 图片资源超出阈值自动转 base64,但阈值别设太高,设到 4KB 以下比较合适,否则 js 文件体积会膨胀。
- 开启 gzip 或 brotli 压缩,配合服务端开启对应压缩策略。
构建预算我通常会设置 120KB 的告警线。rea 会自动对比上一次构建的产物,如果新增依赖超过预算,命令行会标红提示。这个提示不是用来卡发布的,而是提醒团队:新增的依赖到底值不值这个体积。很多项目就是在这类提示下砍掉了不必要的图表库和工具库。
5.2 高频场景实测表现
记忆比较深的一次优化:某列表页,每次进来要拉三个接口,原实现里这三个接口是顺序发起的。rea 提供 request 并发方法后,改成并行发起,首屏时间从 1.8 秒降到了 0.9 秒左右。这种优化不是某个神奇手段,而是把不必要的串行改成并行,收益却非常直接。
另外,长列表场景下,状态层避免在每次数据更新时重建整个数组,用结构共享方式更新局部数据。这个能力 rea 的状态层已经内置。实测滚动加载 500 条数据时,页面帧率比直接 setState 的方案稳定一些,尤其在低端安卓机上表现明显。
6. 常见问题与排查方法
6.1 经典问题速查表
我整理了一张表格,基本覆盖了平时提问最多的几个问题:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 本地请求一直404 | 代理路径没匹配 | 检查 rea.json 的 proxy 字段 |
| 超时后接口自动重试 | POST 的 retry 误开 | 设置 enableOnPOST=false |
| 构建后打不开页面 | 路由 basename 没配对 | 在 rea.json 里设置 basePath |
| 小程序真机无法联网 | 合法域名没配置 | 在后台把前缀加进白名单 |
| 环境变量不生效 | key 拼写不一致 | 统一使用 REA_ 前缀 |
这张表不是一次性总结出来的,是实际被问到好多次之后整理出来的。每次有新同学问类似问题,直接甩表格,比自己解释半天高效。
6.2 几个踩过的坑
第一个坑:Node 版本与内置 fetch 的兼容性。旧版本环境里 AbortController 不存在,rea 请求层会直接报错,而不是优雅降级。后来我确定最低运行版本,并在启动时做一次能力检测,不满足就打印清晰的提示,不再让人猜。
第二个坑:环境变量大小写。Linux 环境变量区分大小写,我曾用 rea_base_url 写了半天不生效,实际上是环境变量必须用 REA_BASE_URL。这个问题一旦踩到,视觉上极难发现,因为本地和 CI 的 shell 配置往往不同。
第三个坑:小程序端的网络请求没有标准的 fetch,需要进行适配。如果直接在 Web 端代码里调用 fetch,小程序运行时接口会全部失败。rea 在初始化时检测运行环境,自动切换到对应的 request 实现。这里也要提醒:真机调试和开发者工具的行为可能不一致,测接口一定要用真机跑一遍。
7. 插件扩展与后续演进
7.1 注册一个自定义命令
rea 提供插件机制,可以新增命令或替换默认命令。
export function defineReaPlugin() { return { commands: [ { name: 'inspect', async run(ctx) { console.log(ctx.config); } } ] }; }这种扩展方式很轻量,不侵入核心。实际用下来,团队可以做自己的代码生成、部署脚本,统一挂在 rea 命令树之下,使用体验比散落的 npm scripts 好。维护成本也低,因为插件接口足够稳定,核心升级不会频繁破坏外部命令。
7.2 预留的服务端渲染能力
服务端渲染不一定要在项目初期接入,但 rea 的配置里预留了 SSR 的标记字段。做好数据预取后,页面可以先加载骨架再填充数据,SEO 场景也能覆盖。项目走到需要 SSR 那一步时,不需要推翻重来,只需要按 rea 文档把数据预取逻辑抽出来,再在构建命令里加一个 ssr 参数即可。
我在实际使用 rea 的过程中,最大的感受不是它有多少功能,而是它减少了很多“基础设施自治”的争吵。以前每个端都有自己的请求层、状态标记、代理规则,出了问题互相推诿,现在配置集中在一处,排查路径也单一了。如果你正被多端、多工程的代码组织问题困扰,不妨先按 rea 的思路整理一个最小化约定:统一超时、统一重试语义、统一加载状态读取位置、统一构建命令。这四个点做到位,项目复杂度会肉眼可见地下降。最后再分享一个小技巧:任何一个新模式要在老项目落地,不要一步到位全量替换,挑一个边缘模块试运行,跑顺了再铺开,风险和阻力都会小很多。