通过基础篇的入门,对于react基本语法和插件都可以灵活运用,这篇文章说说实战中进阶的知识点吧~
1. 项目前期准备
1.1 开发环境搭建
- 安装git https://git-scm.com/downloads
- 安装node https://nodejs.org/zh-cn/
- 安装nvm https://github.com/nvm-sh/nvm
- 安装postman https://www.postman.com/
- 安装抓包工具 教程指引 -- whistle安卓手机端抓包方法 - 简书 (jianshu.com)
- 安装vscode https://code.visualstudio.com/
vscode 必备插件
- Gitlens - 最强git增强插件
- Prettier – 代码格式化工具
- Code Spell Checker – 变量拼写检查
- git-commit-plugin – Git提交信息格式化
- Document This – 注释生成
- Todo Tree – 重要注释高亮
- Eslint – 代码风格约束
- Turbo Console Log - console日志输出
- 配置常用hosts(
根据自身项目而定
)
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 应用例子 - 通过修饰器实现编程思想
-
读懂ES7中javascript修饰器 - SegmentFault 思否
上面的文章通俗易懂,适合初学者理解
4. 项目文档维护
持久化的项目必定需要标志的文档记录,基本必备如下:
4.1 开发流程文档
- code review清单
- 提测流程
4.2 前端文档
- 环境搭建
- 代码规范
- 项目仓库 & 权限账户
- 组件文档
- 采坑记录
- 代码设计(
关键业务信息
) - 第三方对接
- 与其他组对接
- 知识笔记
- 调研文档
4.3 后端文档
- 环境搭建
- 项目仓库 & 权限账户
- 代码规范
- SQL注意事项
- 日志规范
- 采坑记录
- 性能相关
- 代码设计(
模块设计 / 数据库表设计
)
4.4 测试文档
- 测试用例
- 自动化测试
- 发布流程
- 采坑记录
4.5 入职文档(新人
)
4.6 离职交接文档(旧人
)
4.7 质量记录
- 事故记录
- 质量保障机制
4.8 其他业务相关文档记录
5. 项目组件开发
5.1 组件设计
在一个项目中,好的组件封装决定一个迭代的工作效率,复用就是重中之重。
组件封装一般分为两个部分:业务组件 and 公共组件 。
公共组件, 用于全局引用
先看下我最近项目优化后的组件目录
共有五层内容:
-
constant.ts 用于存储常量, 例如: 图片的url地址,固定数据结构等
-
index.tsx 组件核心代码
-
README.md 组件文档!!!为什么加了重点号,因为随着项目的复杂化加深,开发人员的增多,组件需要维护以及真正用上,避免重复开发,我们需要有一个持续化更新的文档去查看以及校验,方便开发人员的使用。
-
styled.tsx 用于编写样式,为了方便实现组件化,解决了css全局命名空间,避免样式冲突的问题,维护起来更加方便。在大型项目中,开发人员可以方便的组合组件。使用方法查看此篇文章:
types.ts 用于ts定义
业务组件
我们一般放置的目录结构 - 具体层级参考上述内容
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