基于react框架的项目组需要具备的知识统计

通过基础篇的入门,对于react基本语法和插件都可以灵活运用,这篇文章说说实战中进阶的知识点吧~

image.png

1. 项目前期准备

1.1 开发环境搭建

1.2 开发流程

推荐以敏捷开发方法进行软件开发

需求评审 -> 技术评审 -> UI评审 -> 用例评审 -> 开发拆解需求 -> 截止日期评审 -> 开发 -> 测试 -> 验收 -> 发布

1.2.3 开发阶段需要系统

  • 测试bug单 (如tapd,jira等
  • 开发设计与问题文档维护 (wiki,confluence,语雀,有道等
  • UI给稿平台(蓝湖等
  • 提测平台(公司内部提供
  • 日志排查(公司提供或者阿里云日志等
  • 接口平台 (有道,Middleman等)

1.3 代码提交流程

  • 向组长申请(f / bg)分支,f为当天迭代内容分支,bg为bug紧急上线分支, 均在master代码上操作
  • 在本地分支开发自测,提到dev分支进行与后端联调
  • dev自测验证后,提测交付测试

1.4 联调流程

  • 公司内部使用bff,请查看文章
  • 只是前后端联调,询问后端接口文档地址,确定参数信息即可。

1.5 问题排查原则

  • 抓包定位问题模块
  • 寻求对应模块开发人员解决
  • 与组长确定是否为紧急bug,紧急bug当天修复上线,其余随迭代
  • 开发不可擅自接需求、bug,需经过组内产品或组长确认

1.6 需求依赖系统

  • 测试bug单 (如tapd,jira等
  • 开发设计与问题文档维护 (wiki,confluence,语雀,有道等
  • UI给稿平台(蓝湖等
  • 提测平台(公司内部提供
  • 日志排查(公司提供或者阿里云日志等
  • 接口平台 (有道,Middleman等)

2. 项目整体保障机制

以下为各个阶段需要确保的内容

2.1 需求评审

  • 确认对接其他业务组的需求确认其他业务组已提前上线
  • 确认其他业务组需求对接的各岗位负责人
  • 确认风控规则内容
  • 确认业务监控内容
  • 确认核心业务流程内容

2.2 技术评审

  • 梳理业务影响范围
  • 预评估开发周期
  • 针对历史活动数据如何兼容处理
  • 现有的技术框架能否实现业务需求
  • 是否有领域模型
  • 是否有第三方对接
  • 是否有跨部门对接协助
  • 需求冲突
  • 数据处理
  • 权益类逻辑评审
  • 确认性能要求指标
  • 数据是否需要脱敏

2.3 UI评审

  • 确认UI设计调整范围:新旧设计稿对比
  • 确认UI切图是否已提供
  • 确认SVG图能否直接从蓝湖导出
  • 确认交互细节UI设计师是否特殊讲解说明
  • 确认产品是否有定稿,确保不再大范围改动

2.4 用例评审

  • 评估用例覆盖业务范围

2.5 开发

  • 协程泄露
  • 内存泄露
  • 安全性(用户态,防刷
  • 确认日志记录正常
  • 确认数据一致性(数据上报,传播链路
  • 接口幂等
  • 变量竞争
  • 数据库 ddl 与索引 *
  • Code Review :
    • 评估代码是否在各层职责范围内
    • 有无低级设计缺陷
    • 通用安全校验等是否遗漏
    • 是否存在相同可复用逻辑
    • 错误异常等处理是否合理
    • 代码可读性(命名明确,代码是否简单易懂
    • 集成外部服务设计合理性
    • 适配不同场景的设计合理性
    • c端接口是否加分布式锁
    • 数据库查询是否使用了索引、是否有join 表情况(一般情况下不建议join表,特殊数据统计可以join
    • 业务状态常量是否 抽离到常量文件统一维护
    • 代码是否有一定量的注释。复杂实现最好有一定的讲解内容
    • 缓存是否有被击穿的可能性。(empty值需要也需要设置empty的缓存
    • 高频查询接口是否有缓存
    • 第三方对接接口是否有足够的日志记录

2.6 测试

  • 登录态处理
  • 接口核心数据校验,不信任外部输入,如 用户中心 ID , 手机号
  • 操作类是否需要加入人机防刷
  • 前端网络协议问题,统一https协议,特别像 图片地址
  • 前端兼容适配问题,如 px未转换成rem
  • 事件移除问题,如 监听事件未remove , 定时器setInterval
  • c端bff调用接口尽量并行处理
  • 代码逻辑是否做了容错处理,防止页面乱码(特别关注点:空指针)
  • 是否存在相同可复用逻辑
  • 代码可读性(命名明确,代码是否简单易懂)
  • react组件性能问题(props未改变情况下避免不必要的渲染hook组件合理使用useCallback,useMemo
  • 网页性能:图片压缩处理,超过500KB的图片需要压缩
  • 网页性能:列表页面图片量大的情况,图片需要做懒加载处理
  • 网页性能:能用样式实现的效果,尽量用样式,减少图片调用
  • alert,debug,console等调试代码删除
  • 浮点数计算是否有进行精度问题处理(特别关注金额相关精度问题)
  • 代码逻辑业务解耦

2.7 验收

发布当天组织会议全员集中验收
参与人员:迭代相关的产品、测试、前端,后端,ui设计师, 产品主导,携带电脑。
!!!如有第三方参与,需加上第三方人员一同参会

  • 提前一天定好会议室,会议在发版当天4点进行,历时1个小时。
  • 产品在后台创建含所有功能点的活动,发布,以及复制更换准入再次发布,验收至少两个活动。
  • 创建临时微信群聊,用于分享活动数据。
  • 参会人员扫码以'新用户' 、'被邀请用户'、'老用户' 三种身份玩转活动。
  • 全员参与完毕后,在后台数据明细中,检验数据是否正确。
  • 无异议,迭代需求无明显bug,测试准备发布。
  • 有体验优化点,产品记录,后续迭代优化。

2.8 发布

  • 根据发布清单列表确认发布分支
  • 检查确认发布清单列表:
    需求名,分支名,sql,apollo,灰度租户,缓存处理,开发责任人,遗留问题(bug链接),发布依赖,备注
  • 产品确认遗留问题。

2.9 发布后

  • 上线后关注 error log 日志
  • 上线后关注数据库负载
  • 上线后关注慢 sql 日志
  • 上线后关注 grafana 监控
  • 生产环境特性验收,已发布后同步团队
  • 出现问题立即回滚,回滚后关注功能是否恢复正常。

3. AOP编程思路 (面向切面编程)

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后, 再通过“动态织入”的方式掺入业务逻辑模块中。

3.1 应用AOP思想的好处

AOP的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模
块。

3.2 应用例子 - 通过修饰器实现编程思想

4. 项目文档维护

持久化的项目必定需要标志的文档记录,基本必备如下:

4.1 开发流程文档

  • code review清单
  • 提测流程

4.2 前端文档

  • 环境搭建
  • 代码规范
  • 项目仓库 & 权限账户
  • 组件文档
  • 采坑记录
  • 代码设计(关键业务信息
  • 第三方对接
  • 与其他组对接
  • 知识笔记
  • 调研文档

4.3 后端文档

  • 环境搭建
  • 项目仓库 & 权限账户
  • 代码规范
  • SQL注意事项
  • 日志规范
  • 采坑记录
  • 性能相关
  • 代码设计(模块设计 / 数据库表设计

4.4 测试文档

  • 测试用例
  • 自动化测试
  • 发布流程
  • 采坑记录

4.5 入职文档(新人

4.6 离职交接文档(旧人

4.7 质量记录

  • 事故记录
  • 质量保障机制

4.8 其他业务相关文档记录

5. 项目组件开发

5.1 组件设计

在一个项目中,好的组件封装决定一个迭代的工作效率,复用就是重中之重。
组件封装一般分为两个部分:业务组件 and 公共组件 。

公共组件, 用于全局引用

先看下我最近项目优化后的组件目录


image.png

共有五层内容:

  • constant.ts 用于存储常量, 例如: 图片的url地址,固定数据结构等

    image.png
  • index.tsx 组件核心代码


    image.png
  • README.md 组件文档!!!为什么加了重点号,因为随着项目的复杂化加深,开发人员的增多,组件需要维护以及真正用上,避免重复开发,我们需要有一个持续化更新的文档去查看以及校验,方便开发人员的使用。


    image.png
  • styled.tsx 用于编写样式,为了方便实现组件化,解决了css全局命名空间,避免样式冲突的问题,维护起来更加方便。在大型项目中,开发人员可以方便的组合组件。使用方法查看此篇文章:


    image.png
  • types.ts 用于ts定义

image.png

业务组件

我们一般放置的目录结构 - 具体层级参考上述内容


image.png

5.2 项目中的功能组件

5.2.1 表格

export default class MainTable extends Component<Props> {
    state = {};
    render() {
        const {
            list,
            columns,
            total,
            page,
            pageSize,
            onShowSizeChange,
            rowKey,
            rowSelection,
            scroll = {}
        } = this.props;
        let tabProps = { columns, dataSource: list, rowKey, rowSelection, pagination: false, scroll };
        return (
            <div className={`${styles.table_wrap} ${this.props.className || ''}`}>
                <Table {...tabProps} pagination={false} />
                <div className={`${styles.page_wrap} tl-list flex-end`}>
                    <Pagination
                        showTotal={total => `共计 ${total} 条`}
                        showSizeChanger
                        showQuickJumper
                        defaultCurrent={page}
                        current={page}
                        pageSize={pageSize}
                        total={total}
                        onChange={onShowSizeChange}
                    />
                </div>
            </div>
        );
    }
}

5.2.2 表单

<Form {...formItemLayoutSignUp}>
                {items.map((item, index) => {
                    const { viewVisible, itemLayout, label, name, options, FormComponent, isElement } = item as any;
                    if (viewVisible !== undefined && !viewVisible) return null;
                    if (isElement && React.isValidElement(FormComponent)) return FormComponent;
                    return (
                        <Form.Item key={index} label={label} {...itemLayout}>
                            {form.getFieldDecorator(name as never, options)(FormComponent)}
                        </Form.Item>
                    );
                })}
            </Form>

5.2.3 提示框

import { Modal } from 'antd';
import React from 'react';
import './style.less';
import styled from 'styled-components';
import { countPrizeNum, numFormat } from '@/pages/Component/ActivityCreate/Prize/constant';

interface Props {
    [propName: string]: any;
}
interface State {
    value: any;
    [propName: string]: any;
}
class AmountTip extends React.Component<Props, State> {
    reportConfirm = () => {
        this.props.confirm();
    };
    cancel = () => {
        this.props.handleCancel();
    };
    render() {
        const { visable, drawParams } = this.props;
        const dataSource = drawParams.awards || [];
        const { award_quota_rule, hasRed = false } = drawParams;
        return (
            <ModalMain
                centered
                title="温馨提示"
                visible={visable}
                className="amount-num"
                onCancel={this.cancel}
                onOk={this.reportConfirm}
                width={400}
            >

            </ModalMain>
        );
    }
}
const ModalMain = styled(Modal)``;
export default AmountTip;

5.2.4 缩略图

import React from 'react';
import { QuestionCircleFilled } from '@ant-design/icons';
import { ThumbnailConfig } from './constant';
import { ThumbnailWrap, ContengWrap } from './styled';
import { Tooltip } from 'antd';
import { ThumbnailProps } from './types';

const ThumbnailContent = (props: ThumbnailProps) => {
    const { src, width, height, content } = ThumbnailConfig[props.type];

    return (
        <ContengWrap widthW={width} heightH={height}>
            <h3>效果展示</h3>
            <p>{content}</p>
            <img src={src} alt=" " />
        </ContengWrap>
    );
};
const Thumbnail = (props: ThumbnailProps) => {
    return (
        <ThumbnailWrap>
            <Tooltip title={() => ThumbnailContent(props)} color={'#fff'}>
                <QuestionCircleFilled className="mark" />
            </Tooltip>
        </ThumbnailWrap>
    );
};
export default Thumbnail;

5.2.5 上传图片

import { YkProjectSelect } from '@yunke/yunked';
import { queryHandOut } from '@/services/oss';
import Loading from '../component/loading';
import XLSX from 'xlsx';
import { getToken as getSubjectId } from '@yunke/yunked/lib/esm/utils/app';
import { message } from 'antd';
import * as R from 'ramda';
import qs from 'qs';
import utils from '@/utils/utils';
const coreUtil = require('@yunke/core/util').default;
//上传图片公用方法封装
export const uploadImg = (file: File, maxSize?: any) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.addEventListener('load', function () {});
    return new Promise((resolve, reject) => {
        let max = utils.getByteSize(maxSize || '2mb');
        if (maxSize && file.size > max) {
            reject('文件大小超过限制');
            return message.warn(`文件大小超过限制: ${maxSize}`);
        }
        Loading.init();
        let maxMb = Math.ceil(max / (1 << 20)); //转换为最大Mb 并向上取整
        queryHandOut('', {}, maxMb)
            .then(res => {
                if (!res) {
                    message.error('上传失败');
                    return;
                }
                // 开始直传
                const { accessid, host, policy, signature, callback, dir } = res.data;
                // 验证格式
                if (!/(jpg|jpeg|png|gif|mp4)/.test(file.type)) {
                    message.error('格式不支持,只支持jpg、jpeg、png、gif、mp4格式文件上传');
                    return;
                }
                // 准备数据
                const data = new FormData();
                data.append('key', `${dir}${randomName() + ('封面图' || ['.jpg'])[0]}`);
                data.append('policy', policy);
                data.append('OSSAccessKeyId', accessid);
                data.append('success_action_status', '200');
                data.append('signature', signature);
                data.append('callback', callback);
                data.append('file', file);
                // 上传数据
                const xhr = new XMLHttpRequest();
                xhr.open('POST', `https:${host}`);
                xhr.onload = e => {
                    try {
                        if (xhr.status === 200) {
                            let data = JSON.parse(xhr.response).data;
                            data.url = data.url.replace(/http:\/\//, 'https://');
                            resolve(data);
                        } else {
                            message.error('文件或尺寸过大,请重新上传');
                            reject(xhr);
                        }
                    } catch (e) {
                        reject(e);
                        message.error('文件或尺寸过大,请重新上传');
                    }
                };
                xhr.onerror = e => {
                    reject(e);
                    message.error('文件或尺寸过大,请重新上传');
                };
                xhr.send(data);
            })
            .finally(() => {
                Loading.destroy();
            });
    });
    function randomName() {
        var str = '';
        var arr = [
            '0',
            '1',
            '2',
            '3',
            '4',
            '5',
            '6',
            '7',
            '8',
            '9',
            'a',
            'b',
            'c',
            'd',
            'e',
            'f',
            'g',
            'h',
            'i',
            'j',
            'k',
            'l',
            'm',
            'n',
            'o',
            'p',
            'q',
            'r',
            's',
            't',
            'u',
            'v',
            'w',
            'x',
            'y',
            'z'
        ];

        for (let i = 0; i < 32; i++) {
            str += arr[Math.floor(Math.random() * arr.length)];
        }

        return str;
    }
};

5.2.6 文本编辑器

import 'braft-editor/dist/index.css';
import React from 'react';
import BraftEditor from 'braft-editor';
import { ContentUtils } from 'braft-utils';
import UploadImg from '@/component/editor/upload';
interface Props {
    type: string; //编辑 详情 新增
    onChange: (editorVal: any) => void;
    val?: string; //编辑详情时传入
    controls?: Array<any>;
    imageControls?: Array<any>;
    [propName: string]: any;
}
export default class Editor extends React.Component<Props> {
    state = {
        editorVal: BraftEditor.createEditorState(null)
    };
    isInit = false; //是否初始化完成
    render() {
        const extendControls: any = [
            {
                key: 'antd-uploader',
                type: 'component',
                component: <UploadImg onChange={this.editorUpload} />
            }
        ];
        let { val, type, cover_url, imageControls = [], controls = [] } = this.props;
        let value = this.state.editorVal;
        if (type !== 'new' && !this.isInit && val) {
            this.isInit = true;
            let data = val.includes('p') ? val: `<p>${val}</p>`
            value = BraftEditor.createEditorState(data);
        }
        if (type === 'new' && !this.isInit && cover_url) {
            this.isInit = true;
            let src = encodeURI(cover_url);
            let img = `<p><p><img src='${src}' style="max-width: 100%;"/></p>${val}</p>`;
            value = BraftEditor.createEditorState(img);
        }
        return (
            <BraftEditor
                style={type === 'look' ? { color: '#ddd' } : {padding: 0}}
                readOnly={type === 'look'}
                value={value}
                defaultValue='<p></p>'
                onChange={this.editorChange}
                extendControls={extendControls}
                imageResizable={false}
                imageControls={imageControls}
                controls={controls}
            />
        );
    }
    //富文本编辑器图片上传
    editorUpload = url => {
        this.setState(
            {
                editorVal: ContentUtils.insertMedias(this.state.editorVal, [
                    {
                        type: 'IMAGE',
                        url
                    }
                ])
            },
            () => {}
        );
    };
    //富文本编辑器内容改变
    editorChange = editorVal => {
        this.setState({ editorVal });
        this.props.onChange(editorVal);
    };
}


import React from 'react';
import { PictureFilled } from '@ant-design/icons';
import { Upload } from 'antd';
import { uploadImg } from '@/utils';
interface Props {
    maxSize?: string;
    onChange: (url: string) => void;
    [propName: string]: any;
}
export default class ImageUploader extends React.Component<Props> {
    customRequest = config => {};
    /**
     * 上传中、完成、失败都会调用
     * 坑的一批,如果beforeUpload返回了一个Promise,file.originFileObj instanceof File === true
     * 如果beforeUpload返回了一个false,file instanceof File === true
     */
    onChangeWrap: any = ({ file, fileList, event }) => {
        const { maxSize, onChange } = this.props;
        uploadImg(file, maxSize)
            .then((res: any) => {
                onChange(res.url);
            })
            .catch(() => {});
    };

    render() {
        return (
            <Upload
                accept=".jpg,.png,.jpeg"
                showUploadList={false}
                beforeUpload={() => {
                    return false;
                }}
                onChange={this.onChangeWrap}
            >
                <button type="button" className="control-item button upload-button" data-title="插入图片">
                    <PictureFilled />
                </button>
            </Upload>
        );
    }
}

5.2.7 日历

5.2.8 svg

import React from 'react';
import { SvgDiv } from './style';
const RadianSVG = (props: any) => {
    const { themeColor, width = '30px' } = props;
    const svgStr = `<?xml version="1.0" encoding="UTF-8"?>
    <svg style="margin: 0 auto; display: block;" width="375px" height="237px" viewBox="0 0 375 237" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
        <!-- Generator: Sketch 61 (89581) - https://sketch.com -->
        <title>Path 3</title>
        <desc>Created with Sketch.</desc>
        <defs>
            <path d="M0,0.0370471208 C64.9638617,16.3320105 129.468751,24.4794922 193.514668,24.4794922 C257.560585,24.4794922 318.055696,16.3320105 375,0.0370471208 L375,236.056883 L0,236.056883 L0,0.0370471208 Z" id="path-1"></path>
        </defs>
        <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
            <g id="Path-3">
                <mask id="mask-2" fill="white">
                    <use xlink:href="#path-1"></use>
                </mask>
                <use id="Mask" fill="${themeColor}" xlink:href="#path-1"></use>
                <path d="M0,8.5 C135.12239,41.6333333 260.197584,38.8 375.225582,-1.0658141e-14 C375.075194,-6.66666667 375.075194,-15.4666667 375.225582,-26.4 L0,-26.4 L0,8.5 Z" id="Path-75" fill-opacity="0.2" fill="#FFFFFF" mask="url(#mask-2)"></path>
            </g>
        </g>
    </svg>`;
    return (
        <SvgDiv width={width} dangerouslySetInnerHTML={{ __html: svgStr }} />
    );
};

export default RadianSVG

6. 项目应用技术

7. React技术

8. h5与小程序交互

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容