通过重构一个移动端项目,我悟到了这些东西。

197 阅读7分钟

[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;

更加优雅的自定义组件写法-组合组件

juejin.cn/post/711410…

结语

以上就是我近期重构移动端的一点总结。

完善的脚手架和完备的约束能够降低开发门槛,使代码的复用性、可维护性更高,代码最终都会朝着更加规范化的方向前进。

重构是自我进步中重要的一环,站在更高的角度,才能轻易的看清楚问题,才能接着前进。如有后续总结,我会继续更新。

如果对你有帮助,记得给我点赞哦~