当我谈 Rax 按端拆分代码的时候我谈些什么:代码规范相关
前言在跨端开发领域Rax 作为一个备受关注的框架凭借其“一次编写多端运行”的理念为开发者带来了巨大的效率提升。然而随着业务规模的扩大和终端形态的多样化Web、Weex、小程序、Node 等如何优雅地管理不同端的代码差异成为摆在团队面前的一道必答题。“按端拆分代码”不只是一个技术手段更是一套贯穿开发全流程的工程规范。本文将从代码规范的角度出发详细探讨在 Rax 项目中如何系统化地拆分、组织、维护多端代码帮助团队在保证开发效率的同时兼顾代码的可读性、可维护性和可扩展性。第一章为什么需要按端拆分代码1.1 Rax 的跨端能力与挑战Rax 是阿里巴巴开源的一套跨端解决方案它提供了一套与 React 兼容的 API使得开发者可以用组件化的方式编写应用并通过不同的渲染引擎将同一份代码渲染到 Web、Weex、阿里小程序、微信小程序等多个平台上。这种“写一次跑多处”的理想很丰满但现实往往存在诸多差异渲染引擎差异Web 端基于 DOM/CSSOMWeex 使用原生渲染小程序则是 WebView 与原生混合。API 能力差异不同端提供的原生能力如路由、存储、网络接口不同。样式支持差异CSS 属性在各端支持程度不同例如 Weex 只支持 Flexbox 布局且部分属性有差异。生命周期差异不同端的页面生命周期、组件生命周期存在细微差别。性能特性差异Web 端可以充分利用浏览器缓存而小程序对代码包大小有严格限制。如果不对这些差异做有效管理代码中就会充斥着if (isWeb) ... else if (isWeex) ...这样的条件分支导致代码难以阅读和维护。因此按端拆分代码成为必然选择。1.2 按端拆分的价值清晰度将不同端的实现物理隔离使得开发者一眼就能看出某个模块在各端的行为。可维护性修改某一端的逻辑时只需关注对应的文件不会意外影响其他端。构建优化在打包时可以只打包当前端所需的代码从而减小包体积。团队协作不同端的开发者可以并行工作减少代码冲突。测试效率可以针对不同端编写独立的测试用例降低测试复杂度。第二章代码拆分的基本原则在制定具体规范之前我们需要先明确几个核心原则它们将指导我们如何设计目录结构、如何划分模块、如何选择技术方案。2.1 DRYDon‘t Repeat Yourself原则DRY 原则要求尽量减少重复代码。在按端拆分的场景中我们应当将跨端通用的逻辑、组件、工具函数抽取出来放在公共目录中避免在每个端目录下重复实现。只有那些真正依赖特定端能力的部分才进行拆分。2.2 单一职责原则每个模块文件、组件、函数应当只有一个职责。对于按端拆分的代码一个文件要么是纯通用的要么是特定于某个端的不要混合。例如一个组件文件如果同时包含 Web 和 Weex 的实现那么这个文件就承担了两种职责违背了单一职责。2.3 平台抽象层Platform Abstraction Layer构建一个统一的平台抽象层封装不同端的 API 差异。业务代码通过这个抽象层调用能力而不是直接使用各端原生 API。抽象层内部按端拆分实现对外暴露统一接口。这是降低耦合、提升可维护性的关键。2.4 编译时优于运行时由于运行时的环境判断如if (process.env.RAX_END web)会增加代码体积并且无法被 tree-shaking 完全优化因此优先使用编译时技术如文件后缀区分、构建宏来隔离不同端的代码。这样可以在构建阶段就剔除无关端的代码确保最终产物干净。2.5 渐进式拆分不要一开始就追求完美的拆分。随着业务的发展当某个模块在不同端的差异逐渐增大时再进行拆分。过早的拆分可能导致过度设计增加维护成本。第三章目录结构规范合理的目录结构是代码规范的基础。Rax 项目通常使用rax-app脚手架创建其默认结构如下textmy-app/ ├── src/ │ ├── app.js # 应用入口 │ ├── app.json # 应用配置 │ ├── pages/ # 页面目录 │ │ └── Home/ │ │ ├── index.js │ │ └── index.css │ └── components/ # 公共组件 ├── build/ # 构建输出 ├── public/ # 静态资源 ├── package.json └── ...为了支持按端拆分我们需要对上述结构进行扩展。以下是推荐的多端目录结构规范。3.1 按平台分目录推荐将不同端的代码放在独立的顶级目录下通用代码放在common或根目录下。例如textsrc/ ├── common/ # 通用代码所有端共享 │ ├── components/ # 通用组件 │ ├── utils/ # 通用工具函数 │ ├── services/ # 通用服务API 调用等 │ └── constants/ # 常量定义 ├── web/ # Web 端特有代码 │ ├── components/ # Web 端专用组件 │ ├── pages/ # Web 端专用页面或覆盖通用页面 │ ├── utils/ # Web 端专用工具 │ └── index.js # Web 端入口可选 ├── weex/ # Weex 端特有代码 │ ├── components/ │ ├── pages/ │ └── ... ├── miniapp/ # 小程序端特有代码阿里小程序、微信小程序等 │ ├── components/ │ ├── pages/ │ └── ... └── node/ # Node 端SSR特有代码 ├── components/ └── ...优点物理隔离清晰构建时只需指定入口目录。缺点如果页面较多可能会产生大量重复的页面骨架但可以通过继承或组合方式复用。3.2 按功能/模块内拆分另一种常见的做法是在功能模块内部使用文件后缀来区分不同端的实现。例如textsrc/ ├── components/ │ ├── Button/ │ │ ├── index.js # 通用入口根据环境引入对应实现 │ │ ├── index.web.js │ │ ├── index.weex.js │ │ └── index.miniapp.js │ └── ... ├── pages/ │ └── Home/ │ ├── index.js │ ├── index.web.js │ └── index.weex.js └── ...这种结构更加贴近组件化思想每个组件自包含不同端的实现文件放在同一目录下通过构建工具根据目标平台选择正确的文件。优点组件内聚性高便于维护。缺点如果平台数量多组件目录下文件会变多。3.3 混合结构在实际项目中可以结合上述两种方式对于通用的、与平台无关的代码放在common目录对于核心组件使用后缀方式拆分对于整个页面或模块使用按平台分目录的方式。建议团队在项目初期就明确采用哪种模式并写入规范文档。第四章文件命名规范文件命名是代码可读性的重要一环。在按端拆分时我们需要有一套统一的后缀约定。4.1 平台后缀约定Rax 生态中通常使用以下后缀来标识不同端的文件.web.js/.web.jsx/.web.tsxWeb 端.weex.js/.weex.jsx/.weex.tsxWeex 端.miniapp.js/.miniapp.jsx/.miniapp.tsx阿里小程序也可用.aliapp.js.wx.js/.wx.jsx微信小程序如果使用统一构建工具可能需要区分.node.jsNode.js 端SSR.common.js通用可选但通常不需要显式标注因为默认就是通用需要注意的是有些构建工具如 Webpack会根据文件后缀自动解析因此在使用import时可以省略后缀构建工具会自动尝试添加后缀并选择匹配的版本。例如javascript// 引入 Button 组件构建工具会根据当前目标平台自动加载 Button.web.js 或 Button.weex.js import Button from ./components/Button;4.2 目录与文件命名风格组件目录使用大驼峰PascalCase如Button/、UserCard/。页面目录同样使用大驼峰如Home/、Profile/。工具函数文件使用小驼峰camelCase或短横线kebab-case如formatDate.js、request.js。常量文件使用大写加下划线如API_URLS.js但实际内容导出对象时也可以用小驼峰命名风格统一即可。4.3 入口文件约定每个模块组件、页面的入口文件统一命名为index.js或index.ts然后在入口文件中根据环境引入对应的实现。例如javascript// components/Button/index.js let Button; if (process.env.RAX_END web) { Button require(./index.web).default; } else if (process.env.RAX_END weex) { Button require(./index.weex).default; } else { Button require(./index.miniapp).default; } export default Button;或者更优雅地利用构建工具的 resolve 规则直接import Button from ./Button然后配置 webpack 的resolve.extensions优先级使得不同端优先匹配对应后缀的文件。这将在第六章详细说明。第五章代码实现规范5.1 组件拆分规范5.1.1 公共组件与平台组件将组件分为三类通用组件完全与平台无关只依赖 Rax 核心 API如createElement、useState等。这类组件放在common/components中所有端直接复用。平台适配组件核心逻辑通用但渲染或交互存在平台差异。这类组件使用单一入口内部通过条件判断或文件后缀拆分实现细节。平台专用组件完全为某一端定制例如 Web 端使用div、span等原生标签而 Weex 端必须使用text、image等组件。这类组件放在对应平台的目录下不暴露给其他端使用。5.1.2 组件实现示例通用组件示例common/components/Loading/index.jsjsximport { createElement, useState, useEffect } from rax; export default function Loading({ text 加载中... }) { return div classNameloading{text}/div; }平台适配组件示例components/Image/index.js作为入口jsx// components/Image/index.js import { createElement } from rax; // 运行时判断不推荐仅作示例 let ImageComponent; if (typeof window ! undefined window.document) { ImageComponent require(./index.web).default; } else if (typeof weex ! undefined) { ImageComponent require(./index.weex).default; } else { ImageComponent require(./index.miniapp).default; } export default ImageComponent;更好的做法是依赖构建工具我们稍后会介绍。5.2 样式处理规范不同端的样式差异是跨端开发中最棘手的问题之一。Rax 提供了createStyle方法但底层实现不同端各异。5.2.1 样式文件拆分原则通用样式放在common/styles或组件同目录的index.css文件中。平台特定样式如果某个组件在不同端的样式差异较大可以为该组件创建多个样式文件如index.web.css、index.weex.css并在组件中按端引入。5.2.2 使用 CSS Modules 还是全局样式建议使用 CSS Modules 来避免样式冲突。在 Rax 项目中可以通过配置 webpack 的 css-loader 开启 modules 模式。组件内使用示例jsximport { createElement } from rax; import styles from ./index.css; export default function Card({ title }) { return div className{styles.card} h2 className{styles.title}{title}/h2 /div; }5.2.3 处理平台样式差异对于需要平台特定样式的组件可以采用以下两种方式不同端使用不同的样式文件textCard/ ├── index.js ├── index.web.css ├── index.weex.css └── index.miniapp.css在组件中根据环境动态引入jsximport { createElement } from rax; let styles; if (process.env.RAX_END web) { styles require(./index.web.css); } else if (process.env.RAX_END weex) { styles require(./index.weex.css); } else { styles require(./index.miniapp.css); }使用内联样式对于差异较小的场景可以使用内联样式并在组件内部根据端进行微调。但内联样式不利于维护不推荐大量使用。5.2.4 样式属性兼容性在编写样式时需要查阅各端支持的 CSS 属性清单。例如Weex 仅支持 flexbox 布局不支持position: fixed有特殊实现、z-index等。小程序对某些属性有前缀要求如-webkit-。Web 端最自由但也要考虑不同浏览器。建议团队维护一份《跨端样式兼容性手册》常见样式写法统一规定。5.3 API 调用规范Rax 本身提供了一些跨端 API如Storage、Router等但实际项目中往往需要调用各端特有的能力如支付宝支付、微信登录等。为此我们需要封装一个统一的平台服务层。5.3.1 定义统一的 API 接口在common/services中定义抽象接口javascript// common/services/storage.js export const storage { get(key) { throw new Error(Not implemented); }, set(key, value) { throw new Error(Not implemented); }, remove(key) { throw new Error(Not implemented); }, };5.3.2 各端实现具体逻辑在对应平台目录下实现javascript// web/services/storage.js export const storage { get(key) { return localStorage.getItem(key); }, set(key, value) { localStorage.setItem(key, value); }, remove(key) { localStorage.removeItem(key); }, };javascript// weex/services/storage.js import { storage as weexStorage } from weex-module/storage; export const storage { get(key) { return new Promise((resolve, reject) { weexStorage.getItem(key, resolve, reject); }); }, // ... };5.3.3 在业务代码中使用业务代码通过统一入口引入不感知底层实现javascriptimport { storage } from /common/services/storage; storage.set(token, xxx);在构建时通过 webpack 的 resolve.alias 或类似机制将/common/services/storage映射到对应平台的实际文件。这样运行时无任何判断代码干净。5.4 业务逻辑中的平台判断有些场景下我们无法通过抽象层完全消除平台判断比如某个功能的流程在不同端差异巨大。这时可以采用以下策略将不同端的逻辑拆分成独立的函数/模块避免在同一个函数内写大量if语句。使用策略模式定义一个策略对象根据平台选择不同的执行函数。示例javascript// common/utils/platform.js export const platform process.env.RAX_END; // web, weex, miniapp 等 // 业务代码 import { platform } from /common/utils/platform; import { webLogin, weexLogin, miniappLogin } from ./loginStrategies; const loginStrategies { web: webLogin, weex: weexLogin, miniapp: miniappLogin, }; export function login() { const strategy loginStrategies[platform]; if (strategy) { return strategy(); } throw new Error(Unsupported platform: ${platform}); }这样即便有多个平台业务代码也保持简洁。5.5 条件编译宏编译时优化为了彻底剔除不相关平台的代码我们可以使用构建时的宏替换。例如使用babel-plugin-transform-define或webpack.DefinePlugin定义全局常量然后在代码中通过if (__PLATFORM__ web)来编写条件分支构建时对不满足条件的分支进行 dead code elimination。Rax 项目通常已经内置了process.env.RAX_END这个变量我们可以在代码中使用它并且在构建时如使用 webpack 的 terser 插件会移除死代码。示例jsximport { createElement } from rax; export default function PlatformSpecific() { if (process.env.RAX_END web) { return divWeb Only/div; } else if (process.env.RAX_END weex) { return textWeex Only/text; } return null; }经过构建后只有当前平台的代码会保留。这种方式简单直接适合小范围的条件分支。但对于大型组件还是建议拆分为独立文件。第六章构建工具配置规范合理的构建配置是实现按端拆分的基石。Rax 项目通常使用rax-app基于 icejs或自己搭建的 webpack 配置。本节介绍如何通过配置实现文件后缀自动匹配、别名映射、多端打包等。6.1 文件后缀解析配置在 webpack 中通过配置resolve.extensions可以控制模块解析时的文件后缀优先级。为了支持按端拆分我们通常将平台相关的后缀放在前面这样当存在index.web.js和index.js时会优先加载index.web.js。示例配置webpack.config.js 片段javascriptmodule.exports { resolve: { extensions: [.web.js, .weex.js, .miniapp.js, .js, .jsx, .json], // 注意顺序很重要越靠前优先级越高 }, };然后在构建 Web 端时我们使用这套配置构建 Weex 端时将.weex.js放在最前面。然而由于构建配置通常是静态的我们可以通过环境变量动态生成配置。在rax-app中可以通过插件或自定义配置文件实现。例如在build.json中配置json{ targets: [web, weex, miniapp], web: { resolveExtensions: [.web.js, .js, .jsx] }, weex: { resolveExtensions: [.weex.js, .js, .jsx] } }6.2 路径别名配置为了简化跨端引入我们通常会设置路径别名使得无论是通用代码还是平台代码都可以通过统一的路径访问。推荐别名/common指向src/common/web指向src/web/weex指向src/weex/miniapp指向src/miniapp但在实际代码中应该尽量使用/common来引入通用模块避免直接引入平台特定模块除非是在平台专用代码中。业务代码不应该依赖/web等别名否则就失去了跨端性。在 webpack 中配置别名javascriptresolve: { alias: { : path.resolve(__dirname, src), /common: path.resolve(__dirname, src/common), /web: path.resolve(__dirname, src/web), /weex: path.resolve(__dirname, src/weex), }, },6.3 多端打包策略一个项目可能需要同时产出多个端的产物例如 Web 端、Weex 端、小程序端。我们可以使用同一个配置文件通过传入不同的目标参数来分别构建。或者使用 monorepo 方式每个端独立构建。在rax-app中可以通过--targets参数指定构建目标bashnpm run build -- --targetsweb,weex,miniapp构建后产物会分别输出到build/web、build/weex、build/miniapp等目录。6.4 按端分离公共资源不同端对资源文件图片、字体等的处理方式不同。例如Web 端可以直接使用图片 URL而小程序可能需要将图片放在指定目录并引用。建议将资源文件按端分别存放或者使用统一的资源管理模块根据平台返回正确的路径。第七章测试规范多端代码的测试是一个挑战。我们需要确保每个端的代码都能正确运行并且通用逻辑在各端表现一致。7.1 单元测试对于通用逻辑应该编写单元测试使用 Jest 或 Rax Test Utils。对于平台特定代码可以分别针对不同端编写测试或者使用模拟mock环境。示例通用组件测试javascriptimport { createElement } from rax; import renderer from rax-test-renderer; import Loading from ./Loading; test(Loading renders correctly, () { const component renderer.create(Loading texttest /); const tree component.toJSON(); expect(tree).toMatchSnapshot(); });7.2 端到端测试对于不同端建议编写独立的端到端测试Web 端使用 Puppeteer 或 Cypress。Weex 端使用 Weex 官方提供的测试工具。小程序端使用小程序开发者工具的自动化测试能力。由于成本较高端到端测试可以只覆盖核心业务流程。7.3 测试覆盖率要求团队应设定测试覆盖率阈值如 80%尤其是通用模块和平台抽象层确保代码质量。第八章文档与注释规范代码拆分后清晰准确的文档尤为重要。8.1 组件文档每个通用组件和平台适配组件都应有 README 文件说明组件的功能和使用方式支持的平台各端差异说明Props 列表示例代码可以使用 Storybook 或类似的工具来展示组件。8.2 平台差异说明在项目的根目录下可以维护一个PLATFORM_DIFF.md文件记录各个模块在不同平台上的实现差异方便新成员快速了解。8.3 代码注释规范对于使用了条件编译或文件后缀拆分的代码注释应说明为何拆分、各端实现的位置。对于平台抽象层的接口注释应说明该接口在各端的预期行为。使用 JSDoc 为函数和组件添加类型注释。第九章团队协作与代码审查按端拆分代码引入了新的复杂度团队协作时需遵循以下流程9.1 分支管理建议使用 Git Flow 或 GitHub Flow。对于涉及多端的大改动可创建 feature 分支并在合并前确保所有端的构建通过。9.2 代码审查要点在 Code Review 时需重点关注是否引入了不必要的平台判断平台特定代码是否放在了正确的目录/文件中通用代码是否真的通用还是隐含了平台假设构建配置是否同步更新9.3 持续集成设置 CI 流水线对每次提交进行所有端的构建和测试至少单元测试。确保新增代码不会破坏其他端的构建。第十章实践案例从零搭建一个跨端组件库为了更直观地展示上述规范的应用我们以搭建一个跨端组件库为例逐步演示如何按端拆分代码。10.1 项目初始化使用rax-cli初始化一个组件库项目bashnpx rax-cli init my-components选择“组件库”模板。10.2 目录结构设计我们决定采用“功能内拆分 公共抽象”的混合结构textmy-components/ ├── src/ │ ├── common/ # 通用工具、样式等 │ │ ├── styles/ │ │ └── utils/ │ ├── components/ │ │ ├── Button/ # Button 组件 │ │ │ ├── index.js # 入口根据平台导出 │ │ │ ├── index.web.js │ │ │ ├── index.weex.js │ │ │ └── index.miniapp.js │ │ ├── Image/ # 图片组件 │ │ │ ├── index.js │ │ │ ├── index.web.js │ │ │ └── index.weex.js │ │ └── Modal/ # 模态框仅 Web 和 Weex 支持 │ │ ├── index.js │ │ ├── index.web.js │ │ └── index.weex.js │ └── index.js # 组件库统一导出 ├── build/ # 构建输出 ├── test/ # 测试 ├── package.json └── ...10.3 实现 Button 组件Button 组件的通用逻辑接收children、onClick、type等 props样式可能在各端略有差异。index.js 入口javascript// src/components/Button/index.js import { createElement } from rax; let ButtonComponent; if (process.env.RAX_END web) { ButtonComponent require(./index.web).default; } else if (process.env.RAX_END weex) { ButtonComponent require(./index.weex).default; } else { ButtonComponent require(./index.miniapp).default; } export default ButtonComponent;Web 端实现index.web.jsjsximport { createElement } from rax; import styles from ./index.css; export default function Button({ children, onClick, type default }) { return ( button className{${styles.button} ${styles[type]}} onClick{onClick} {children} /button ); }Weex 端实现index.weex.jsjsximport { createElement } from rax; import styles from ./index.weex.css; export default function Button({ children, onClick, type default }) { return ( div className{${styles.button} ${styles[type]}} onClick{onClick} text className{styles.text}{children}/text /div ); }小程序端实现index.miniapp.js小程序中按钮使用原生button组件需要适配其属性。jsximport { createElement } from rax; import styles from ./index.miniapp.css; export default function Button({ children, onClick, type default }) { // 小程序按钮的 type 映射 const btnType type primary ? primary : default; return ( button className{styles.button} type{btnType} onClick{onClick} {children} /button ); }10.4 配置构建工具在组件库的build.json中配置多端构建json{ targets: [web, weex, miniapp], web: { resolveExtensions: [.web.js, .js, .jsx, .json] }, weex: { resolveExtensions: [.weex.js, .js, .jsx, .json] }, miniapp: { resolveExtensions: [.miniapp.js, .js, .jsx, .json] } }10.5 测试与发布编写单元测试针对各端模拟环境。例如使用jest配合jest-rax模拟 Rax 环境。由于组件库需要发布到 npm我们可以在package.json中设置main指向src/index.js这样使用者在构建时会根据自身项目的配置解析正确的端文件。第十一章常见问题与解决方案11.1 第三方库的跨端兼容性有些 npm 包只支持 Web 端如果直接使用会导致其他端报错。解决方案使用platform条件导入如if (process.env.RAX_END web) { require(some-web-lib); }。在 webpack 配置中为不同端设置externals或alias将特定库替换为空模块或模拟实现。11.2 代码重复问题有时不同端的实现有大量相似代码如果直接复制粘贴会导致维护困难。可以考虑提取公共部分到common中或者使用继承/组合模式。例如定义一个BaseButton包含通用逻辑各端实现继承它。11.3 调试困难当出现某个端特有的 bug 时调试可能困难。建议为每个端搭建独立的开发环境如npm run dev:web、npm run dev:weex。使用端特定的调试工具如 Chrome DevTools for Web, Weex Devtools。增加详细的日志输出并利用条件编译只在调试模式下输出。11.4 包体积过大如果按端拆分不当可能导致每个端的包中都包含了不必要的代码。解决方案确保使用了 tree-shaking 和 dead code elimination。对于大型依赖使用动态导入import()按需加载。检查构建产物使用webpack-bundle-analyzer分析。第十二章未来展望与演进随着 Rax 生态的发展按端拆分代码的规范和工具也在不断演进。我们可以关注以下几个方向更智能的构建工具自动检测平台差异并优化打包。统一的跨端 API 标准Rax 可能会提供更完善的跨端 API减少开发者封装成本。类型系统支持通过 TypeScript 定义平台抽象接口确保各端实现符合契约。组件平台声明在组件库的package.json中声明支持的平台让构建工具自动处理。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2452998.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!