规范背景 随着市面上的需求越来越复杂. 个人开发者已无法满足项目版本的开发速度. 那么团队开发是复杂项目的必然选择. 不过如何管理研发团队的开发风格, 确保每一行代码都像是同一个人
编写的. 从而减少团队中代码的沟通成本. 这是团队开发中重要的一环.
目录结构规范 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 . └── src ├── api ├── components ├── config ├── hooks ├── images ├── less ├── pages │ └── Home │ ├── components │ ├── data │ └── utils ├── redux │ ├── actions │ ├── constants │ ├── reducers │ └── store ├── service └── utils
组件设计
每个组件都是一个文件夹. 组件名即为文件夹名. 文件夹内包括 index.tsx(.js) 、 index.less 以及 README.md(如组件内部业务过于复杂需要书写 README, 功能清晰的组件只需在代码上方注释书写组件功能、传参含义即可). 组件内尽量不要使用 redux. 所有依赖数据尽量使用 props 传值. 内部逻辑实现尽量抽象, 不要依赖业务元素. 这样以保证其复用性. 组建内部尽量不要使用页面级生命周期. (待实践)
组件传入的每个参数都要注释其意义, 以及是否必传.公共组件
放到./src/components局部组件
放到对应页面下的 components 文件夹内
页面设计
每个页面都是一个文件夹. 页面名即为文件夹名. 文件夹内包括 components、index.tsx(.js) 、 index.less、 index.config.ts
页面传入的每个参数都要注释其意义, 以及是否必传.
git 管理规范
保证 master 分支代码, 是没有风险的, 随时可以打包上线. 禁止直接在 master 分支修改代码.
dev 分支为预发环境, 保证该分支代码是健康的, 也为即将可以合并 master 分支状态.
stage 分支为测试环境, 大杂烩分支, 想要部署到测试环境上的代码都可以往里放, 该分支可能随时会删除重新从 dev 开辟出来.
dev-something 即为对应需求的开发分支, 从 dev 分支开辟出来. 功能开发完毕后, 需要合并到 dev 分支, 同步书写该版本的文档. 在回归测试没问题后, 通过提交 PR 请求, 经过其他人代码 review 之后, 合并到 master 提单发版.
可通过 gitlab runner 搭建 CI 持续集成.
提交代码时候, 尽量按照功能的原子性提交, 不要多个事情放到一个 commit 里面去.
请按照一定语法去 commit, 每一条 commit 由以下几部分构成.
1 2 3 4 5 6 7 8 9 10 修改类型+(影响模块)+:+[bug单号]+问题描述 如: fix(会员购买页面):[7405-7405]会员等级购买ios购买规避政策 修改类型分为以下几种: feat: 开发新功能 style: 调整样式 fix: bug修复 refactor: 代码重构 merge: 代码合并 doc: 书写文档 config: 调整配置
PS: 定制版本过多的话, 不建议以分支去隔离. 随着业务分叉严重, 版本更迭时间过长. 合代码的时候, 任务重, 风险大. 可考虑通过 webpack 打包不同文件后缀名, 进行文件隔离. (待实践)
代码编写规范
统一开发环境
建议 VSCode
+ Prettier
+ ESLint
+ Stylelint
+ Tailwind
.vscode/setting.json
(仅针对该项目)
1 2 3 4 { "editor.formatOnSave" : true , "files.autoSave" : "onFocusChange" }
.editorconfig
1 2 3 4 5 6 7 8 9 10 11 12 # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false
.prettierrc.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 module .exports = { tailwindConfig: "./tailwind.config.js" , tailwindAttributes: ["customClass" , "className" ], jsxSingleQuote: false , singleQuote: false , printWidth: 140 , tabWidth: 2 , useTabs: false , semi: true , trailingComma: "es5" , jsxBracketSameLine: false , bracketSpacing: true , arrowParens: "always" , quoteProps: "preserve" , proseWrap: "preserve" , htmlWhitespaceSensitivity: "css" , organizeImportsSkipDestructiveCodeActions: false , stylelintIntegration: true , importOrder: [ "<THIRD_PARTY_MODULES>" , "^@/(.*)$" , "^../(.*)" , "^./((?!less).)*$" , "^./(.*)" , ], importOrderSeparation: false , importOrderSortSpecifiers: true , plugins: ["prettier-plugin-organize-imports" , "prettier-plugin-tailwindcss" ], };
.eslintrc.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 module .exports = { extends: ["taro/react" ], parserOptions: { ecmaFeatures: { jsx: true , tsx: true , }, useJSXTextNode: true , useTSXTextNode: true , }, rules: { "react/jsx-uses-react" : "off" , "react/react-in-jsx-scope" : "off" , "react/jsx-key" : "error" , "jsx-quotes" : ["error" , "prefer-double" ], "no-const-assign" : 2 , "no-fallthrough" : 1 , "no-func-assign" : 2 , "no-multiple-empty-lines" : [1 , { max : 2 }], "no-param-reassign" : 2 , "no-mixed-spaces-and-tabs" : [2 , false ], "no-sequences" : 0 , "no-unneeded-ternary" : 2 , "no-unused-vars" : [1 , { vars : "all" , args : "after-used" }], "no-undef" : "error" , "no-var" : 0 , "arrow-parens" : 0 , "arrow-spacing" : 0 , curly: [2 , "all" ], "default-case" : 2 , eqeqeq: 2 , "init-declarations" : 0 , "import/order" : 0 , "import/no-commonjs" : 0 , "import/no-named-as-default" : 0 , }, };
.stylelintrc.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 module .exports = { processors: [], plugins: ["stylelint-order" ], extends: ["stylelint-config-standard" , "stylelint-config-css-modules" ], globals: { wx: true , App: true , Page: true , getApp: true , Component: true , }, rules: { "selector-class-pattern" : [ "^([a-z][a-z0-9]*)(-[a-z0-9]*_?[a-z0-9]+)*$" , { message: "Expected class selector to be kebab-case" , }, ], "no-invalid-double-slash-comments" : null , "block-no-empty" : null , "at-rule-empty-line-before" : null , "at-rule-no-unknown" : null , "length-zero-no-unit" : true , "shorthand-property-no-redundant-values" : true , "declaration-block-no-duplicate-properties" : true , "declaration-block-no-redundant-longhand-properties" : null , "no-descending-specificity" : true , "selector-max-id" : 0 , "max-nesting-depth" : 3 , "order/properties-order" : [ "box-sizing" , "position" , "top" , "left" , "right" , "bottom" , "z-index" , "flex" , "display" , "flex-direction" , "flex-wrap" , "justify-content" , "align-items" , "grid-template-columns" , "grid-gap" , "float" , "width" , "height" , "max-width" , "max-height" , "min-width" , "min-height" , "padding" , "padding-top" , "padding-right" , "padding-bottom" , "padding-left" , "margin" , "margin-top" , "margin-right" , "margin-bottom" , "margin-left" , "margin-collapse" , "margin-top-collapse" , "margin-right-collapse" , "margin-bottom-collapse" , "margin-left-collapse" , "transform" , "overflow" , "overflow-x" , "overflow-y" , "clip" , "clear" , "font" , "font-family" , "font-size" , "font-smoothing" , "osx-font-smoothing" , "font-style" , "font-weight" , "line-height" , "letter-spacing" , "word-spacing" , "color" , "text-align" , "text-decoration" , "text-indent" , "text-overflow" , "text-rendering" , "text-size-adjust" , "text-shadow" , "text-transform" , "word-break" , "word-wrap" , "white-space" , "vertical-align" , "list-style" , "list-style-type" , "list-style-position" , "list-style-image" , "pointer-events" , "cursor" , "background" , "background-color" , "border" , "border-color" , "border-radius" , "box-shadow" , "content" , "outline" , "outline-offset" , "opacity" , "filter" , "visibility" , "size" , "transition" , ], }, };
事件绑定函数命名规范
采用小驼峰命名法.
开头: 自身实现前缀 handle, 组件暴露出来的前缀 on
中间模块名称: 如: Cell、Item、Title 等
尾部事件名称: 如: Click、TouchMove、Change 等
1 2 3 4 5 6 7 const handleCellClick = () => { console .log("handleCellClick" ); }; render() { <ListCell onCellClick={handleCellClick} /> }
自定义函数命名规范
采用小驼峰命名法, 见名知意, 通过函数名来知道意义.
校验类: check 开头
处理类: deal 开头
方法类: process 开头
格式化类: format 开头
渲染类: render 开头
组件命名规范
采用大驼峰命名法. 使用名词开头, 后接形容词. 如:
ListSelect(实现可选择的列表)
ModuleTitle(拥有标题的模块)
PanelBottom(位于底部的面板)
css 样式命名规范
采用小驼峰命名法. 每个 class 中间要空一行,且注释不要使用双斜杠,而应使用 /**/
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 .pageWrap {} .pageContent {} .page-wrap {} .page-content {}
路由传值
由于路由传值都被转为 string 类型. 传值 undefined, false, true 等歧义变量, 很容易引发隐蔽性 bug.
1 2 `/pages/Index/index?isAdmin=false&isOwner=true` `/pages/Index/index?role=owner` ;
封装方法传入的参数尽量是对象
1 2 3 4 5 6 7 8 9 10 11 dealDateInfo(data, show, tip, success); const params = { data: "1" , show: true , tip: 99 , success: () => {}, }; dealDateInfo(params);
如果一个函数需要多个参数实现其逻辑, 尽量将这些参数组成一个对象. 这样的好处在于:
a) 方便定义类型. b) 如果部分参数是非必传的情况, 方便处理. c) 对于编译器减少 push 函数参数的操作
不过要注意, 通过对象入参, 会让参数变为引用传参, 在函数内切记不要直接修改入参的值, 否则会改变入参的原数据, 以免引发其他问题.
开发规范
import 书写顺序
a) 首先引入第三方库 b) 次之引入设置别名的绝对路径 c) 最后引入相对路径
而同级则以引入库的字母顺序排列.
引用公共组件/公共方法应当使用别名路径
或者绝对路径
, 专属于自己组件的引用文件可考虑使用相对路径.
严禁如: ../../../../../../../xxx.js
类开发内书写顺序
优先书写变量相关
优先书写 构造函数: 自定义变量
其次书写 state、data 的定义
次之书写 computed
最后书写 watch
次之书写函数相关
优先书写 methods
其次书写 events 等自定义事件
次之书写 生命周期
最后书写自定义函数
如为 tsx 文件
自定义 render 函数
返回的 render 函数
TSX 文件书写顺序(待实践)
自定义变量
自定义函数
生命周期
绑定事件函数
渲染函数
主渲染函数
详细:
useRouter
useRef
useState
useMemo
自定义变量
自定义方法
useDidShow
useEffect
usePullDownRefresh
useReachBottom
handleXXX 绑定事件
renderXXX 渲染函数
render return() 主渲染函数
善用 CSS 变量 / CSS 原子化
颜色、字号、边距、边角已经有规定尺寸, 常规情况下, 不要直接去写数值. 会造成 UI 风格不一致.
数据管理
前端要有自己的数据管理能力. 由于组件设计过程中, 组件内的变量命名应该是抽象的. 那么在接口获取数据之后, 都需要将得到的数据, 转换为组件内对应的变量位置装好. 这样, 数据处理位置比较居中, 同时也能倒逼设计组件更加抽象化.
生命周期的运用(仅参考, 根据具体业务来运用)
useEffect
, 只用来处理变量
useDidShow
, 用来请求接口数据
组件抽象化 在组件的设计过程中, 组件内的变量命名应该是抽象的. 不要把轻易业务变量丢到组件内部. (除非放弃该组件的复用性)
1 2 3 4 5 6 7 8 <Header isGM={true } isAM={false } isBM={false } isCM={true } /> <Header isShowEdit={true } />
可自闭合标签, 采取自闭合方式书写
为防止标签内的内容过多时, 闭合标签匹配混乱的问题.
1 2 3 4 5 <Video></Video> / / good <Video / >
标签属性书写顺序
应当按照以下给出的顺序依次排列, 确保代码的易读性 a) class b) id, name c) data-_ d) src, for, type, href, value e) title, alt f) role, aria-_
1 2 3 4 5 6 7 <img className="imgContent" id="img_apple" data-info="aaa" src="../aa/bb/cc/dd.jpg" title="图片" />
switch 的相关操作
一定要有 default 的情况作为兜底, 每个 case 的作用域应当一定要被大括号包裹, 以免变量声明提前导致一些隐形 bug.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const test1 = (type ) => { let a = 999 ; switch (type) { case 1 : console .log("case 1: a = " , a); break ; case 2 : const a = 8 ; console .log("case 2: a =" , a); break ; default : break ; } }; const test2 = (type ) => { let a = 999 ; switch (type) { case 1 : { console .log("case 1: a = " , a); break ; } case 2 : { const a = 8 ; console .log("case 2: a =" , a); break ; } default : { break ; } } };
套路场景
多条件判断可配置化
简单判断可如下:
1 2 3 4 5 6 7 8 9 10 11 if ( strMySelfRole === "GM" || strMySelfRole === "DM" || strMySelfRole === "AM" ) { } if (["GM" , "DM" , "AM" ].includes(strMySelfRole)) {}
复杂判断可如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const TAB_LIST = [ { type: "APPLE" , title: "Apple" , }, { type: "BANANA" , title: "Banana" , supportRules: [ { isAM: [true ], }, { nBM: [100 , 200 ], }, { isAM: [false ], strCM: ["ok" , "sure" ], }, ], }, { type: "CHERRY" , title: "Cherry" , supportRules: (matchRules ) => { const { isAM, nBM, strCM, arrDM, objEM } = matchRules || {}; return !!objEM?.a && nBM > 1.23 && nBM < 12.34 && arrDM.includes("happy" ); }, } { type: "ORANGE" , title: "Orange" , supportRules: true , }, ];
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 const updateTabList = (params ) => { const { aDetail, bDetail, cDetail, dDetail } = params || {}; const { am } = aDetail || {}; const { bm } = bDetail || {}; const { cm } = cDetail || {}; const { dm } = dDetail || {}; const { em } = eDetail || {}; const matchRules = { isAM: am, nBM: bm, strCM: cm, arrDM: dm, objEM: em, }; const tabList = TAB_LIST.filter((item ) => { if (item.supportRules === undefined ) { return true ; } if (item.supportRules) { return item.supportRules.some((rule: any ) => { return Object .keys(rule).every((key ) => { return rule[key].includes(nowRule[key]); }); }); } if (item.supportRules) { return item.supportRules.some((rule: any ) => { return Object .keys(rule).every((key ) => { return rule[key].includes(nowRule[key]); }); }); } return false ; }); return tabList; };
后期只需要维护 TAB_LIST 常量数组的配置, 不再需要调整 updateTabList 方法, 即可清晰应对每一项的调整需求.
另外, 一般不要在带代码中直接去使用常量, 建议将所有的常量抽出来, 统一整理到一个 config.js 文件中,
同时遵循开闭原则
, 即对于功能的扩展是开放的, 对功能的修改是关闭的. 即使新增需求, 也不需要去改动老代码. 以提高其拓展性和可维护性. 方便后续需求变动的时候, 能够快捷找到对应的位置.
复杂条件渲染场景
通过抽象枚举值, 善用表驱动法, 实现条件渲染. 不过要注意的是, key 值要保证是已有字段, 不然无法渲染. 例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const renderPageContent = () => { return { VPEmpty: <VPEmpty pageInfo ={objTabBarCurrent} /> , VPHome: <VPHome pageInfo ={objTabBarCurrent} /> , VPHomeCarbon: <VPHomeCarbon pageInfo ={objTabBarCurrent} /> , VPMine: ( <VPMine pageInfo={objTabBarCurrent} mineMemberEditRefresh={mineMemberEditRefresh} onMineMemberEditClick={handleMineMemberEditClick} /> ), VPNew: <VPNew pageInfo ={objTabBarCurrent} /> , VPNull: <VPNull pageInfo ={objTabBarCurrent} /> , }[code || "VPNull" ]; };
复杂场景权限校验
简单逻辑情况, 可以通过“与”运算符实现. 复杂逻辑的情况, 可通过装饰器模式、自定义指令、高阶函数或者 hook, 对业务逻辑进行装饰. 达成目的是, 降低耦合, 校验逻辑、业务逻辑互相不会污染.
1 2 3 <Permission strCheckPosition={"LEADER" }> <View>Leader Content</View> </ Permission>
减少冗余的变量声明
对变量的声明尽量精简. 不要多个变量去控制一个事物的状态, 声明越多的变量, 需要维护的成本就会越大.
比如新建 / 编辑等场景:
组件传参只需要传递 id 即可, 是新增还是编辑通过判断 id 是否传值即可. 如果有 id 则说明是编辑场景, 没有则说明是新建场景。
这样即可少维护一个冗余字段
组件的抽象实现
为保证组件的复用性, 对暴露出来的方法要遵循单一职责原则
保证方法的高内聚, 不要携带其他副作用. 比如, tab 切换组件返回的事件, 应当只是纯粹的告诉调用者(父组件), tab 的哪一项被点击, 至于被点击之后的处理逻辑, 应由调用者(父组件)去实现. 业务逻辑不在组件内实现的好处, 在于方便在其他位置复用该组件样式.
同时, 这也是遵循单向数据流, 数据由上向下,事件由下向上. 有助于简化数据的管理和状态的维护, 提高代码的可维护性和可预测性.
PS: 只有数据的拥有者,才能有资格修改这个数据(唯一责任人)
内聚业务逻辑
把校验逻辑、限制逻辑其他杂七杂八的逻辑,尽量剥离在业务逻辑之外。让开发同学更专注于业务逻辑实现。
如:指令、装饰器。
1 2 3 4 5 6 7 8 9 10 11 12 import { Debounce } from '@/utils' ;@Debounce(200 , { leading : false , trailing : true }) handleBtnClick() { }
判断以字符串开头/结尾
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export const startsWith = (source, start ) => { return source.slice(0 , start.length) === start; }; export const endsWith = (source, end ) => { return source.indexOf(end, source.length - end.length) !== -1 ; };
返回跳转问题
Close
按钮是 back 还是 navigateTo ?
个人倾向于是使用 back, 优势在于: 因为这样操作的路由栈是纯粹的, 不会因为魔改而污染路由栈. 比如点击左上角返回, 或者移动端手势操作返回的时候, 都不会有影响.
缺点在于: 如果是通过外链, 直接跳转到该页面, 那么就需要判断. 此时的关闭是返回外链来源页面, 还是返回自身应用的首页. 需要单独用代码来处理. (或者通过携带参数, 来做判断处理)
Dropdown 下拉菜单实现
下拉菜单的触发区, 尽量采用 listPopup.length > 0 来判断. 这样方便后续某些场景没有下拉菜单项的时候, 就自动不会展示下拉菜单触发区(如…), 这样也省心省力.
Select 的组件操作
如果有条件的话, 在从接口拿到 value 对其进行初始化的时候, 可以考虑根据 options list 的数据(如不是分页加载)做一下筛选.
这样可以避免后端给出一些可能已经不存在于 options 的 value, 从而影响到前端的空值表单校验.
分页列表刷新场景
下方加载中状态展示逻辑: 只要当前数据数量
小于数据数量总数
, 那就会一直展示(屏幕外也是如此)
如果滚动到底部, 触发请求分页操作. 请求分页操作做 请求锁 + 防抖处理.
如果当前请求页数不是第一页, 那么就拼接老列表数据.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 export default function MemberScoreList ( ) { const isLoading = useRef(false ); const pageNum = useRef(0 ); const pageSize = useRef(15 ); const pageTotalCount = useRef(999999 ); const [listScore, setListScore] = useState([]); const getList = async () => { isLoading.current = true ; const res = await Api.Score.getList({ pageNum: pageNum.current, pageSize: pageSize.current, }); if (res?.body) { const { totalCount = 0 , list = [] } = res?.body || {}; pageTotalCount.current = totalCount; const listNew = (list || [] ).map ((item ) => ({ ...item, createTimeStr: dayjs(item.sys_createTime).format("YYYY-MM-DD HH:mm:ss" ), })); let listOld = pageNum.current === 0 ? [] : listScore; setListScore(listOld.concat(listNew)); } setTimeout(() => { isLoading.current = false ; }, 600 ); }; const init = async () => { getQueryScoreList(); }; useEffect(() => { init(); }, []); const handleRefreshScore = () => { init(); }; const hanldePageScrollToLower = useDebounce( () => { if (isLoading.current) { return ; } if (listScore.length >= pageTotalCount.current) { return ; } pageNum.current += 1 ; getQueryScoreList(); }, 500 , { leading : false , trailing : true } ); return ( <SAPageCore isShowLoadingBottom={listScore.length < pageTotalCount.current} renderPageHeader={() => { return ( <View className="flex flex-col" > <SANavHeader isShowBtnLeft title="积分记录" /> </View> ); }} onScrollToLower={hanldePageScrollToLower} > <View className="relative box-border px-2 pb-2"> <ListScore list={listScore} / > </View> </ SAPageCore> ); }
善用 Promise.all 并行请求接口
Promise.all 兼容性更好, Promise.allSettled 更安全.(注意返回值结构不一样) 另注意, Promise.all 如果其中一个异步操作抛出错误, 那么会全部直接返回. 如果为了避免该情况, 可以在每个异步处理中, 使用 try catch 进行包裹, 如果报错异常, 也去手动正常返回一个值, 即可避免该问题.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 async queryList() { try { const resA = await queryA(); this .listA = resA ?.filter((item ) => item?.total > 80 ) .map((item ) => { return { ...item, label: item.name.toUpperCase(), }; }); if (this .listA?.length > 1 ) { const [resB = [], resC = []] = await Promise .all([queryB(), queryC()]); this .listResult = [...resB, ...resC]; } else { this .listResult = await queryD(); } } catch (error) { console .log(error); } }; async init() { await queryList(); }
常见风险操作注意
1 2 3 4 const { a, b, c } = obj;const [x, y, z] = arr;
1 2 3 4 const { data: { status, body, message }, } = await queryList();
1 2 3 4 5 JSON .stringify(obj);JSON .parse(str);
1 2 3 4 5 6 7 8 arr.map((item ) => { item.a = 1 ; return item; }); arr.filter(() => { return true ; });
1 2 3 4 5 6 init() { const { id } = this .props || {} }
代码优化
慎用 redux. 由于每次变化会触发全局刷新. 建议只存储唯一性的值. a) redux 是响应式, 每次更新都会触发 diff 算法, 全局刷新渲染 DOM, 影响性能. b) redux 是全局纬度, 其生命周期很难把握. 如果不做处理, 只是一味的存储, 不去主动销毁, 最终就会内存泄漏. 建议只存储全局唯一状态性的值. 如: 品牌、菜单列表等.
搞清楚数据纬度: 组件纬度、页面纬度、全局纬度. 各自纬度的数据, 存储到各自纬度中, 方便代码维护.
与渲染无关的数据尽量不要放在 state 中, 可以放在 useRef 中. 与渲染无关的数据尽量不要放在 state 中, 可以考虑放在 useRef 或 this 中. 每次渲染会对 state 对象进行遍历 diff 算法. 减少 state 的内容, 可提升渲染效率. 且对 useRef 和 this 的修改是同步的, 能处理一些需要属性及时生效的业务场景.
善用节流、防抖. 可对 usePageScroll 等场景装饰节流、对 input 的 value 变化场景装饰防抖.
封装组件的时候, 留意不要套多余无意义 View . 以免页面 DOM 层级太深, 影响渲染性能. 可适当用<Block>
或者<Fragment>
代替
Input 输入框跳位问题. 尽量不要对 Input 的 value 二次 setState.
尽量减少页面的跳转交互. 页面跳转体验奇差. 会有白屏闪现. 在业务能满足的情况下, 尽量单页去实现功能. 可提出建议反馈产品设计.
尝试页面处于 loading 状态时候使用骨架屏.
Input 的输入内容要进行 trim()处理.
点击等用户操作, 一定要有反馈效果. 比如: 适当的 loading / toast, 按钮灰度大小变化, 弹窗动画, 展开收起动画等. 这样不仅对用户友好, 也方便后续自身定位问题.
移动端点击区域要舒适. 不要太老实直接把事件绑定到 span / text 上面. 点击区域应适当放大一些, 以方便用户的操作.
sss
项目管理
通过 jsDoc 完善 api 文档
通过 jscpd 判断代码重复率
工具化代码 review 平台
项目管理系统平台
自动化测试
后记 本篇部分内容为实际项目已经用到且取得相应成效的方法. 部分内容为个人见解尚在理论部分, 待实践. 项目规范是一个不断优化、不断完善的长期过程. 要做到因地制宜, 慢慢尝试, 找到最适合自己团队的方式, 这样才能真正提升团队开发效率. 我也会不断更新该篇文章.