[toc] 「时光不负,创作不停,本文正在参加2022年中总结征文大赛」
旧版
技术选型
一开始,移动端项目采用的技术栈是:React + Taro + Dva + Sass + ES6/ES7。
当时选用 React + Dva + ES6/ES7 是因为这是惯用的开发模式,而选用 Taro 则是看中了它的可拓展性。能够编译出 微信小程序、H5 小程序,支付宝小程序等。当时 Taro 已经出到 v3 了,我实践后,选择了 v1 版本。
有两个原因,第一是因为 v3 版本比较新,我当时遇到一个问题完全找不到相关解法;第二是因为 v3 版本包含了 react-dom 相关内容,打包出来的入口文件较大,最后选用了 v1 版本,最开始的设计是,尽可能地封装公共组件,完成初始版本后,渐进 Taro 版本。
问题
开发
我们的开发模式是敏捷开发,需求驱动。即,功能处于快速增和变更长之中,由于没有完整的设计图,开发过程中,对于公共组件的判断出现了失误,速度至上的时候,开发流程也不够规范,导致虽然业务组件多次出现,但并没有得到相对规范的封装。
兼容性
尽管想要做多种小程序,但目前主页依然集中在 H5 小程序的开发。Taro v1 版本在客户端上存在一些无法解决的问题[当然也可能是版本原因],比如输入框无法在文字中断任意插入;多个弹框共同出现时、滑动会出现问题;企微的 h5 有时需要网页展示,Taro 时间组件只适用于移动端,等。
新版
最终决定重构是因为,Taro 并不支持网页上的操作,暂时也并无发展其他平台第三方的需求,而我们的业务存在较大量的交互趋势,因而选用更加支持 H5 的开发模式。
技术选型
选用 React + TS + Less + Mobx 进行开发。
由于移动端之间共享的数据较少,移动端界面限制,不存在过多交互,因此认为采用 Mobx 更加轻量。
由于不少界面中会调用相同的 API , 因此 API 集中放置于一个文件中,避免代码冗余。
规范
文件结构规范
大致文件结构如下:
├── config // 开放配置文件
├── dist // 编译文件
├── src // 源码目录
│ ├── api // 接口文件
│ ├── assets // 静态资源
│ ├── components // 公用组件
│ ├── page // 页面
│ ├── router // 路由配置文件
│ ├── store // 状态管理文件
│ └── utils
│ ├── interface.ts // TS 接口文件
│ ├── tools.ts // 公共方法文件
基础命名规范
文件夹、function、css 采用小驼峰命名。
interface 文件大写开头,Type 结尾。
公共组件、公共方法界定
在项目中使用两次以上的重复模块代码需要封装成公共组件。
在项目中使用两次以上的重复功能需要放置到公共方法文件中。
公共样式规范
常用色、主题色设置在同一个样式文件中。
外部组件库使用规范
不直接在界面中使用,在公共组件中封一层导出。防止对组件组组件变更的时候需要多点修改。
文字和图片展示规范
为了避免移动端可能出现的展示问题,图片和文字必须用统一的组件包括,方便后期的统一修改。
<Text align="left" color="#666" level="level3">
示例文字
</Text>
页面拆分
一个页面的代码行数不能吵过 300 行,超过 300 行则对界面进行拆分。
格式规范
统一完成 eslint、prettierrc 进行规范设定,下载相关插件,尽可能使功能一致。
提交规范
提议配置 commitlint.config.js ,明确提交格式。
分支规范
分支分为 master - 正式环境、 release - 测试环境、 dev - 开发环境三种。
开发流程规范
提交的工单需明确至少三种类型 线上紧急bug修复、新需求开发、下一版本必须完成的功能和修复的bug。
其他
如果是开发 pc 端,系统复杂,页面多,可以为公共组件单独分配一个路由,只用于开发环境。这样,新加入的开发人员进行开发时,可以快速的了解现有的公共组件,避免重复工作。
避坑
如果创建文件时,将文件名大小写写错了,从小写修改为大写或者从大写修改为小写,git 可能不是别。在 windows 环境下打包部署没有问题,但是 Linux 会显示文件不存在。
实践优化
在实际操作过程中,我总结了一些优化的实操过程,总结了一些其他的可以优化的点。
全局 css 变量
@primary-button: #2f6acf; // 按钮主色
@primary-page: #2f6ad0; // 页面主色
@import '../Style/colors';
.overwriteButton {
--adm-color-primary: @primary-button;
}
第三方组件引入
只修改部分样式
包括一层组件,重写相关组件的样式即可。
index.tsx
import React from 'react';
import { Button as AntButton } from 'antd-mobile';
import type { ButtonProps } from 'antd-mobile/cjs/components/button';
import './style.less';
export const Button: React.FC<ButtonProps> = ({ children, ...props }) => {
return (
<div className="overwriteButton">
<AntButton {...props}>{children}</AntButton>
</div>
);
};
export default Button;
index.less
@import '../Style/colors';
.overwriteButton {
--adm-color-primary: @primary-button;
}
在原有的组件的传参上添加新的属性传参
import React from 'react';
import { Tag as AntTag } from 'antd-mobile';
import type { TagProps } from 'antd-mobile/cjs/components/tag';
import './index.less';
interface CurTagProps extends TagProps {
round?: boolean;
}
const Tag: React.FC<CurTagProps> = ({ round = true, children, ...props }) => {
return (
<AntTag {...props} className={round ? 'tag-style' : 'tag-tangle-style'}>
{children}
</AntTag>
);
};
export default Tag;
包含子组件对象的组件修改样式
import React, { FC } from 'react';
import { Tabs as AntTabs } from 'antd-mobile';
import type { TabsProps, TabProps } from 'antd-mobile/cjs/components/tabs';
import './index.less';
export interface TabsFC<T> extends FC<T> {
Tab: FC<TabProps>;
}
interface CurTabsProps extends TabsProps {
mode?: 'light' | 'dark';
}
const Tabs: TabsFC<CurTabsProps> = ({ mode = 'dark', children, ...props }) => {
return (
<div className="overwriteTab">
<AntTabs {...props} className={mode === 'dark' ? 'tab-style-dark' : 'tag-style-light'}>
{children}
</AntTabs>
</div>
);
};
Tabs.Tab = AntTabs.Tab;
export default Tabs;
自定义组件优化
支持修改组件样式、支持行间样式
import React, { ReactNode } from 'react';
import classnames from 'classnames';
import './index.less';
interface SimpleCardProps {
bgColor?: string;
anglePos?: 'top' | 'bottom' | 'default';
shadow?: boolean;
style?: any;
children?: string | ReactNode;
}
const SimpleCard: React.FC<SimpleCardProps> = ({
bgColor = 'white',
anglePos = 'default',
shadow = false,
children,
style,// 拿到 style ,合入代码
...props
}) => {
const rightAngle =
anglePos === 'default' ? '' : anglePos === 'top' ? 'angle-top' : 'angle-bottom';
return (
<div
className={classnames({
'simple-card': true,
shadow: shadow,
[rightAngle]: true
})}
style={{ backgroundColor: bgColor, ...style }}
{...props}
>
{children}
</div>
);
};
export default SimpleCard;
减少状态管理和代码量
修改 Modal, 使它调用的时候像使用 Toast 一样方便。
核心思想:在 dom 上直接挂载和清除。
import React, { ReactNode, cloneElement, useState, useEffect } from 'react';
import Styles from './style.module.less';
import { Button, Popup } from 'antd-mobile';
import ReactDOM from 'react-dom';
import PopupFooter, { PopupFooterProps } from './PopupFooter';
import type { Router } from 'react-router-dom';
import { useMount } from '@/utils/hooks';
interface Option {
height?: string;
maskClosable?: boolean; //是否点击蒙层来关闭弹窗
title?: string;
content: React.FC | ReactNode;
}
interface ConfirmPopupType {
open: (opt: Option) => Promise<any>;
Footer: typeof PopupFooter;
}
export interface ExtraContentProps {
confirm: (data) => void;
cancel: (data) => void;
}
const open: ConfirmPopupType['open'] = (options) => {
let { content: Content, title, height, maskClosable = false } = options;
//区分渲染内容为组件还是reactElement
Content = typeof Content === 'function' ? <Content /> : Content;
//设置渲染根节点
const contentRootNode = document.createElement('div');
contentRootNode.className = 'confirm-popup-root';
document.body.appendChild(contentRootNode);
return new Promise<any>((resolve, reject) => {
//完成操作后点确定,弹窗关闭
const handleOk = async (data) => {
render(false);
resolve(data);
};
//取消操作,弹窗关闭
const handleCancel = (reason) => {
render(false);
reject(reason);
};
const afterClose = () => {
document.body.removeChild(contentRootNode);
};
//传递confirm和cancel给content组件用于控制关闭弹窗
const extraContentProps: ExtraContentProps = {
confirm: handleOk,
cancel: handleCancel
};
const handleMaskClick = () => {
if (maskClosable) {
handleCancel(null);
afterClose();
}
};
const render = (visible: boolean) => {
//新建Transition过渡组件,处理打开组件时动画效果消失的问题
const Transition: React.FC<{ visible: boolean }> = ({ visible }) => {
const [visible1, setVisible] = useState(!visible);
useEffect(() => {
if (visible) {
setVisible(true);
} else {
setVisible(false);
}
}, []);
return (
<Popup
afterClose={afterClose}
bodyClassName={Styles.popupBodyStyle}
bodyStyle={height ? { height } : undefined}
destroyOnClose
getContainer={contentRootNode}
onMaskClick={() => {
new Promise(() => {
setVisible(false);
}).then(() => {
handleMaskClick();
});
}}
visible={visible1}
>
{title && <div className={Styles.title}>{title}</div>}
{React.Children.map(Content as React.ReactNode, (child) =>
cloneElement(child as any, extraContentProps)
)}
</Popup>
);
};
ReactDOM.render(<Transition visible={visible} />, contentRootNode);
};
render(true);
});
};
const ConfirmPopup: ConfirmPopupType = {
open,
Footer: PopupFooter
};
export default ConfirmPopup;
更加优雅的自定义组件写法-组合组件
结语
以上就是我近期重构移动端的一点总结。
完善的脚手架和完备的约束能够降低开发门槛,使代码的复用性、可维护性更高,代码最终都会朝着更加规范化的方向前进。
重构是自我进步中重要的一环,站在更高的角度,才能轻易的看清楚问题,才能接着前进。如有后续总结,我会继续更新。
如果对你有帮助,记得给我点赞哦~