切图编码规范
前端开发代码开发的过程中,其中将近一半的时间会用在切图上。切图规范的制定,可以提高开发效率,减少后期修改的工作量。以下是一些切图规范的建议:
关于规范优先级分为以下几种:
- 必须:必须遵守的规范,不遵守在code-review或者后期审查中会进行扣分。
- 推荐:推荐遵守的规范,不遵守不会导致开发无法进行,但会降低开发效率或增加后期修改的工作量。虽不会扣分,但是会降低项目整体评价
UI设计稿的尺寸为 375
【必须】使用flex布局
项目切图部署方案始终遵循flex 布局优先 block(块级)/postion(定位) 布局为辅 抛弃浮动布局
的理念。
几个要点:
- 显式声明
flex-direction
、justify-content
、align-items
等属性。尤其是在 跨端框架涉及RN时 - 思考那个组件使用
flex: 1
来占满剩余空间 - 有固定宽高的组件需要使用
flex-shrink :0
来防止被压缩变形
<View className={styles.rightView}>
// 图标icon 设置固定宽高 防止被挤压
<Image src={address_icon} className={styles.iconStyle} />
// 使用flex:1占满剩余空间。以保证右侧图片始终在最右侧
<View className={styles.levelView}>{lanchText(data?.address, data?.enAddress)}</View>
<Image src={hang_icon} className={styles.rightViewIcon} />
</View>
解释:
由于在移动端设备种类繁多,屏幕尺寸不一,使用flex
布局可以更好的适配不同尺寸的屏幕。并且flex布局也可以更好的控制子元素的排列方式,使其在不同尺寸的屏幕上都能保持良好的布局效果。
【必须】不要写死宽度
布局类型组件没有无法解决的理由。不要用写死的宽度。例如
// ts
<Button className={styles.payButton} onClick={handlePayment}>
{isTimeout ? '已超时' : '去支付'}
</Button>
// css
.payButton {
width: 345px;
height: 48px;
background: #C9B48F;
border-radius: 4px;
...省略其他
}
解释:
在ui设计稿中。实际要求是按钮距离左右有15px的间距。但是实际开发中,如果你像上述那样直接拷贝蓝湖样式。写死宽度。那么在不同手机上就会出现样式异常的情况。
解决方法:
用 padding 或者 margin 属性来设置左右间距。
【推荐】不要拷贝蓝湖无用样式
.info {
// 默认样式。多余
font-style: normal;
// 无效字体。虽然影响微乎其微。但是为了代码的整洁。建议不要拷贝无用样式。避免不必要的样式污染和维护的疑问
font-family: SourceHanSansCN, SourceHanSansCN;
// 默认颜色
color: #333;
// 可能是无效的高度值。在切图是需要考虑是否需要设置高度。如果不需要设置高度。那么就不要设置高度。避免不必要的样式。
height: 30px;
font-weight: 400;
font-size: 12px;
}
解释:
参考上述注释
【必须】透明导航头提前处理滚动逻辑
解释:
需要UI设计画稿时。为了设计美观会考虑采用初始的透明导航头。并且在页面长度较长时在滚动需要导航头变为不透明。此时在切图时需要提前处理滚动逻辑。
解决方法:
监听页面滚动动态修改导航背景色:
- 方案一: 瞬时改变
import Taro, { usePageScroll } from '@tarojs/taro'
const [elementTop, setElementTop] = useState(false)
usePageScroll((height) => {
setElementTop(height?.scrollTop > 300)
})
<MMNavigation shadow={false} type={elementTop ? 'Default' : 'Transparent'} place={false} contentStyle={{ backgroundColor: elementTop ? '#000' : 'transparent' }} />
- 方案一: 渐变改变
// 具体使用查看api
// usePageScrollNav.tsx
import { usePageScroll } from '@tarojs/taro'
import MMNavigation from '@wmeimob/taro-design/src/components/navigation'
import { CSSProperties, useRef, useState } from 'react'
export interface Option {
/**
* 顶部偏移量
* 距离顶部多少时开始计算
* @default 20
*/
offset?: number
/** 背景色 */
background?: [number, number, number]
/** 字体颜色 */
color?: [number, number, number, number?]
}
export function usePageScrollNav(option: Option) {
const { background = [255, 255, 255], color = [0, 0, 0, 0], offset = 20 } = option
const [top, setTop] = useState(0)
const [contentStyle, setContentStyle] = useState<CSSProperties>(() => {
return {
background: `rgb(${background.join(' ')} / 0)`,
color: `rgb(${color.join(' ')} / ${1})`
}
})
const navHeight = useRef(MMNavigation.navigationHeight * 2)
usePageScroll(({ scrollTop }) => {
hanelScroll(scrollTop)
})
const hanelScroll = (top) => {
if (typeof top === 'number') {
// 把顶部拉出来时是 > 0
if (top <= 0) return
setTop(top)
const offsetTop = Math.abs(top) - offset
let opa = parseFloat((offsetTop / navHeight.current).toFixed(2))
// eslint-disable-next-line no-nested-ternary
opa = opa >= 1 ? 1 : opa <= 0.2 ? 0 : opa
setContentStyle({ background: `rgb(${background.join(' ')} / ${opa})`, color: `rgb(${color.join(' ')} / ${color[3] || opa})` })
}
}
return {
top,
contentStyle,
setContentStyle
}
}
const { top, contentStyle } = usePageScrollNav({
background: [13, 9, 8],
color: [255, 255, 255, 1]
})
<MMNavigation title={t('onlineReservation')} contentStyle={contentStyle} />
【推荐】文本类需要提前考虑样式
解释:
如果页面中存在文本类元素,需要提前考虑多行样式。
- 文本最大长度是多少
- 文本最大行数是多少
- 文本溢出时如何处理(省略号、换行等)
解决方法:
切图时多尝试边界情况。在切图阶段就把多行文本、长文本、短文本、空文本等边界情况都考虑进去。
【必须】图片裁剪
解释:
许多系统在后台上传图片时都只会上传一套图片。并且在前端展示时会根据最大尺寸上传。比如说商品详情页/列表页/首页入口等使用同一张主图。 那么在商品列表页渲染如果使用原始图片渲染会造成: 下载耗时较长/渲染慢/内存占用高/流量费用高等问题。严重甚至导致小程序崩溃退出。
解决方法:
图片尺寸较大在加载时如果采用了 OSS 的必须走缩放
// 使用 OSS 裁剪图片API
import { assembleResizeUrl } from '@wmeimob/aliyun'
// import { assembleResizeUrl } from '@wmeimob/tencentyun'
<Avatar src={assembleResizeUrl(item.skuImg, { width: 64 })} size={64} shape="square" />
注意
不同对象存储参数格式不一样。请自行查看文档进行修改
【推荐】图片资源就近维护
解释: 许多开发者甚至前端框架设计时。都会倾向于将UI静态图片资源维护类似在名为 src/assets/imgs
这样的文件夹内。进行统一管理。理论上统一维护可以方便统一修改。但是实际开发中。如果图片资源较多。那么会存在以下问题:
- 查找困难: 图片太多,查找困难,容易遗漏或者重复
- 移除困难: 如果图片不再使用,需要手动删除,容易遗漏或者误删。很多时候开发者也不会去删除。久而久之就会变成一个巨大的图片资源库,难以管理。
解决方法:
实际上关于资源管理。可以参考组件化概念。将单个组件使用的资源集中在一个文件夹内。比如:tsx/hook/css/img/...都放在一个文件夹内。并且保持高内聚。其他组件使用时避免使用此组件内的任何资源。这样在修改/移除资源时,只需要修改/删除对应的文件夹即可,不会影响到其他组件。
【必须】图标样式
明确写上以下样式
.icon {
flex: none;
width: 10px;
height: 10px;
}
【推荐】使用useMemo这种计算属性而不是定义state
解释: 许多开发在编码时对于衍生数据会这么处理
const [storeData, setStoreData] = useState<ResStoreDto>()
// 定义衍生数据
const [storeImg, setStoreImg] = useState<string[]>([])
const [firstImg, setFirstImg] = useState('')
// 获取门店详情
useEffect(() => {
const fetchStoreDetails = async () => {
try {
// ...忽略其他
const { data } = await api['/api/member/v1/store/detail_GET']({ storeId });
if (data) {
setStoreData(data)
// 接口调用后赋值衍生数据
const storeImgList = data?.environmentImgs?.split(',')
setFirstImg(storeImgList?.[0])
setStoreImg(storeImgList || [])
}
} catch (error) {
}
};
fetchStoreDetails();
}, []);
看起来除了手动定义略显憨态之外。似乎,也没什么太大问题。但是如果说此处的衍生数据需要修改或者需要重新初始化。就容易出现遗漏或者修改错误
const [storeData, setStoreData] = useState<ResStoreDto>()
const [storeImg, setStoreImg] = useState<string[]>([])
const [firstImg, setFirstImg] = useState('')
const onChange = (url: string[]) => {
setStoreImg(url)
setFirstImg(url[0])
setStoreData(pre => ({...pre, environmentImgs: url.join(',')}))
}
const onInit = () => {
setStoreImg([])
setFirstImg('')
setStoreData({})
}
解决方法:
只修改一份数据。其他数据通过计算属性推导
const [storeData, setStoreData] = useState<ResStoreDto>()
const storeImg = usememo(()=> storeData?.environmentImgs?.split(',') || [], [storeData])
const firstImg = useMemo(()=> storeImg[0] || '', [storeImg])
// 获取门店详情
useEffect(() => {
const fetchStoreDetails = async () => {
// ...忽略其他
const { data } = await api['/api/member/v1/store/detail_GET']({ storeId });
data && setStoreData(data)
};
fetchStoreDetails();
}, []);
const onChange = (url: string[]) => {
setStoreData(pre => ({...pre, environmentImgs: url.join(',')}))
}
const onInit = () => {
setStoreData({})
}
【推荐】小程序所有页面都在project.private.config.json中定义出来
切小程序项目时。将project.private.config.json
文件移除 gitignore 并且将每个页面声明出来。这样可以方便的进行页面跳转和页面对接管理。
【推荐】避免无意义的try/catch
解释:
许多开发者在写异步函数时会这么写。
const storeList = async () => {
try {
const { data } = await api['/api/member/v1/reservationOrder/{orderNo}_GET'](orderNo);
setStoreData(data)
} catch (error) {}
}
解决方法:
考虑await之后是否有存在进一步的处理逻辑。避免写这种看起来有用实际没作用的代码
【推荐】使用公司组件库
解释:
许多同学尤其是刚入职的同学。在切图时总是忽略公司组件库 @wmeimob/taro-design
。自行去实现 弹窗/按钮/列表等基础组件。但是基本上都存在问题。
解决方法:
使用公司组件库。尤其是 toast/dialog/button/modal/popup等基础或者复杂组件。这样可以避免重复造轮子。并且可以保证组件的样式和功能的一致性。
如果不知道使用。可以查看 @wmeimob/taro-design/src/pages
内的代码示例
```tsx
const [toast] = useToast()
const dialog = useDialog()
return (
<PageContainer>
<MMNavigation title={i18n.t._('myAccount')} type="Transparent" />
<MMButton text="确认" />
</PageContainer>
)
【推荐】积极封装组件/抽离逻辑
严禁出现单页面代码量超过800行。当单个组件代码行数超过500.你就应该思考逻辑封装和组件拆分的事情了。
【推荐】提前思考业务
切图时,提前思考业务逻辑和可能的交互
- 比如提交按钮,进行事件绑定并尽可能补充逻辑
- 列表项,不要只单纯切静态DOM。考虑使用 list.map 进行渲染,考虑使用 key 属性。并将渲染项抽离成独立组件
【推荐】提交操作提前考虑防抖/提示处理
// 保存
const [handleSave, saveLoading] = useSuperLock(async () => {
const formData = await form.validateFields()
const { term, receive, ...value } = formData
let param: any = {
...value,
...imgList
}
try {
id ? await api['/api/sys/v1/brand/update/{id}_PUT'](id, { ...param }) : await api['/api/sys/v1/brand_POST']({ ...param })
message.success('保存成功')
history.goBack()
} catch (error) { }
})