Skip to content

切图编码规范

前端开发代码开发的过程中,其中将近一半的时间会用在切图上。切图规范的制定,可以提高开发效率,减少后期修改的工作量。以下是一些切图规范的建议:

关于规范优先级分为以下几种:

  • 必须:必须遵守的规范,不遵守在code-review或者后期审查中会进行扣分。
  • 推荐:推荐遵守的规范,不遵守不会导致开发无法进行,但会降低开发效率或增加后期修改的工作量。虽不会扣分,但是会降低项目整体评价

UI设计稿的尺寸为 375

【必须】使用flex布局

项目切图部署方案始终遵循flex 布局优先 block(块级)/postion(定位) 布局为辅 抛弃浮动布局 的理念。

几个要点:

  • 显式声明 flex-directionjustify-contentalign-items 等属性。尤其是在 跨端框架涉及RN时
  • 思考那个组件使用 flex: 1来占满剩余空间
  • 有固定宽高的组件需要使用flex-shrink :0来防止被压缩变形
tsx
<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布局也可以更好的控制子元素的排列方式,使其在不同尺寸的屏幕上都能保持良好的布局效果。

【必须】不要写死宽度

布局类型组件没有无法解决的理由。不要用写死的宽度。例如

不要写死宽度

code
// ts
<Button className={styles.payButton} onClick={handlePayment}>
  {isTimeout ? '已超时' : '去支付'}
</Button>

// css
.payButton {
  width: 345px;
  height: 48px;
  background: #C9B48F;
  border-radius: 4px;
  ...省略其他
}

解释:

在ui设计稿中。实际要求是按钮距离左右有15px的间距。但是实际开发中,如果你像上述那样直接拷贝蓝湖样式。写死宽度。那么在不同手机上就会出现样式异常的情况。

解决方法:

用 padding 或者 margin 属性来设置左右间距。

【推荐】不要拷贝蓝湖无用样式

code
.info {
  // 默认样式。多余
  font-style: normal;
  // 无效字体。虽然影响微乎其微。但是为了代码的整洁。建议不要拷贝无用样式。避免不必要的样式污染和维护的疑问
  font-family: SourceHanSansCN, SourceHanSansCN;
  // 默认颜色
  color: #333;
  // 可能是无效的高度值。在切图是需要考虑是否需要设置高度。如果不需要设置高度。那么就不要设置高度。避免不必要的样式。
  height: 30px;
  font-weight: 400;
  font-size: 12px;
}

解释:

参考上述注释

【必须】透明导航头提前处理滚动逻辑

nav

解释:

需要UI设计画稿时。为了设计美观会考虑采用初始的透明导航头。并且在页面长度较长时在滚动需要导航头变为不透明。此时在切图时需要提前处理滚动逻辑。

解决方法:

监听页面滚动动态修改导航背景色:

  • 方案一: 瞬时改变
tsx
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

tsx
// 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
  }
}
tsx

  const { top, contentStyle } = usePageScrollNav({
    background: [13, 9, 8],
    color: [255, 255, 255, 1]
  })


  <MMNavigation title={t('onlineReservation')} contentStyle={contentStyle} />

【推荐】文本类需要提前考虑样式

text

解释:

如果页面中存在文本类元素,需要提前考虑多行样式。

  • 文本最大长度是多少
  • 文本最大行数是多少
  • 文本溢出时如何处理(省略号、换行等)

解决方法:

切图时多尝试边界情况。在切图阶段就把多行文本、长文本、短文本、空文本等边界情况都考虑进去。

【必须】图片裁剪

解释:

许多系统在后台上传图片时都只会上传一套图片。并且在前端展示时会根据最大尺寸上传。比如说商品详情页/列表页/首页入口等使用同一张主图。 那么在商品列表页渲染如果使用原始图片渲染会造成: 下载耗时较长/渲染慢/内存占用高/流量费用高等问题。严重甚至导致小程序崩溃退出

解决方法:

图片尺寸较大在加载时如果采用了 OSS 的必须走缩放

tsx
// 使用 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/...都放在一个文件夹内。并且保持高内聚。其他组件使用时避免使用此组件内的任何资源。这样在修改/移除资源时,只需要修改/删除对应的文件夹即可,不会影响到其他组件。

【必须】图标样式

明确写上以下样式

css
  .icon {
    flex: none;
    width: 10px;
    height: 10px;
  }

【推荐】使用useMemo这种计算属性而不是定义state

解释: 许多开发在编码时对于衍生数据会这么处理

tsx
  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();
  }, []);

看起来除了手动定义略显憨态之外。似乎,也没什么太大问题。但是如果说此处的衍生数据需要修改或者需要重新初始化。就容易出现遗漏或者修改错误

tsx
  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({})
  }

解决方法:

只修改一份数据。其他数据通过计算属性推导

tsx
  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 并且将每个页面声明出来。这样可以方便的进行页面跳转和页面对接管理。

project.private.config.json1project.private.config.json2

【推荐】避免无意义的try/catch

解释:

许多开发者在写异步函数时会这么写。

tsx
  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
```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 属性。并将渲染项抽离成独立组件

【推荐】提交操作提前考虑防抖/提示处理

tsx
// 保存
  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) { }
  })