Taro+React多端开发实践总结

1,957 阅读6分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

本文是笔者实践工作经验总结的Taro+React+TypeScript+Redux+自研UI库Tard+自研lint规范模版仓库

文章速览: image.png

一、工程目录

在开发中,首先第一步就是确定目录结构,一个好的目录结构往往让人神清气爽

文件目录的思想是预定大于配置

约定好哪个文件下放什么,在一个团队中保持一致,打开一个接手的项目,一个工程目录一目了然,而不用去某个文件大概是干什么的 举几个例子

  • 静态资源static文件夹里面放不需要经过打包,比如说下载的第三方lib文件,而需要打包的资源放在asstes文件夹里面
  • 常量统一都放在constants文件夹下

具体的文件目录如下

taro-react-typescript-redux-template
├─ dist                            // 编辑结果目录
├─ config                          // taro编译配置目录
│  ├─ dev.js                       // 开发环境配置
│  ├─ index.js                     // 默认配置
│  └─ prod.js                      // 生产环境配置
├─ src                             // 源码目录
│  ├─ apis                         // 全局接口
│  ├─ assets                       // 需要打包的静态资源
│  │  └─ iconfont                  // 字体图标
│  │  └─ css                       // css
│  │  └─ img                       // 图片
│  ├─ components                   // 全局通用组件 
│  ├─ constants                    // 全局常量
│  ├─ pages                        // 页面文件目录
│  │  └─ tab                       // tab-bar页面模块,后面小程序分包会讲到
│  │  │  ├─ apis                   // tab-bar页面下的接口
│  │  │  ├─ home                   // home页面
│  │  │  │  ├─ index.config.ts     // 页面配置
│  │  │  │  ├─ index.less          // 页面样式
│  │  │  └─ └─ index.tsx           // 页面逻辑
│  │  ├─ user                      // 个人中心页面模块
│  │  │  ├─ apis                   // 个人中心下的接口
│  │  │  ├─ settings               // 个人信息设置页
│  │  │  └─ address                // 个人收获地址信息页
│  │  ├─ order                     // 订单页面模块
│  │  │  ├─ apis                   // 订单下的接口
│  │  │  ├─ list                   // 订单列表页
│  │  │  └─ detail                 // 订单详情页
│  ├─ types                        // ts类型文件
│  ├─ utils                        // 工具文件目录
│  │  ├─ cookie.ts                 // cookie模块  
│  │  ├─ https.ts                  // 网络请求模块
│  │  └─ index.ts                  // 基础模块
│  ├─ app.config.ts                // taro项目入口配置
│  ├─ app.less                     // 项目总样式
│  ├─ app.ts                       // 项目入口文件
│  ├─ index.html                   // 
├─ .editorconfig
├─ .env                            // 开发环境环境变量
├─ .env.pre                        // 预发环境环境变量
├─ .env.prod                       // 生产环境环境变量
├─ .env.test                       // 测试环境环境变量
├─ .eslintrc.js                    // esLint规则配置
├─ .stylelintrc.js                 // styleLint规则配置
├─ .gitignore                      // git忽略文件    
├─ babel.config.js                 // Babel配置
├─ tsconfig.json                   // ts配置文件
├─ global.d.ts                      
├─ iconfont.json                   // 图片处理配置文件
├─ package.json 
├─ project.config.json
├─ project.tt.json
└─ tsconfig.json

二、代码规范

代码规范是必不可少的前置配置

使用ESLint+StyleLint+CommitLint,分别去约束js代码、css代码、git提交

模版仓库里面用的是自研的规范库,具体可参考之前写的文章如何在团队快速落地代码规范,当然你也可以直接换成你所在部门的使用的规范,直接修改相应的配置文件即可

1、ESLint

npm i eslint-config-selling -D
// .eslintrc.js
module.exports = {
  "extends": ['selling/react']
}

2、StyleLint

npm i stylelint-config-selling -D
// .stylelintrc.js
module.exports = {
  "extends": "stylelint-config-selling"
}

3、CommitLint

npm i commitlint-config-selling husky -D
// .commitlintrc.js
module.exports = {
   "extends": "commitlint-config-selling"
}
// package.json
{
  "lint-staged": {
    "*.{js,jsx,json,ts,tsx,vue}": [
      "eslint --fix",
      "git add"
    ],
    "src/**/*.less": [
      "stylelint --config  ./.stylelintrc --fix",
      "git add"
    ]
  },
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

如果用VScode,需要下载以下插件

  • eslint
  • stylelint

三、环境变量

在开发中,通常有多套环境去配置不同的环境变量

方案:将变量写在 .env.{NODE_ENV} 文件中,利用 cross-env 注入环境标识,dotenv-flow.env.{NODE_ENV} 文件加载到 process.env,最后将 process.env 写入 taro 配置文件,环境变量统一用 APP_ 开头

1、将变量写在 .env.{NODE_ENV} 文件

模版仓库里面,以有开发-dev预发-pre生产-prod测试-test为例,分别对应文件.env.env.pre.env.prod.env.test image.png

2、利用 cross-env 注入环境标识

组合环境(dev/pre/prod/test) * 类型(dev/build) * 平台(小程序/H5) 比较多...但是比手动在命令行输入变量强

// package.json
"scripts": {
    "build:weapp": "taro build --type weapp",
    "build:h5": "taro build --type h5",
    "dev:weapp": "npm run build:weapp -- --watch",
    "dev:h5": "npm run build:h5 -- --watch",
    "build-pre:weapp": "cross-env APP_ENV=pre taro build --type weapp",
    "build-pre:h5": "cross-env APP_ENV=pre taro build --type h5",
    "dev-pre:weapp": "cross-env APP_ENV=pre npm run build:weapp -- --watch",
    "dev-pre:h5": "cross-env APP_ENV=pre npm run build:h5 -- --watch",
    "build-test:weapp": "cross-env APP_ENV=test taro build --type weapp",
    "build-test:h5": "cross-env APP_ENV=test taro build --type h5",
    "dev-test:weapp": "cross-env APP_ENV=test npm run build:weapp -- --watch",
    "dev-test:h5": "cross-env APP_ENV=test npm run build:h5 -- --watch",
    "build-prod:weapp": "cross-env APP_ENV=prod taro build --type weapp",
    "build-prod:h5": "cross-env APP_ENV=prod taro build --type h5",
    "dev-prod:weapp": "cross-env APP_ENV=prod npm run build:weapp -- --watch",
    "dev-prod:h5": "cross-env APP_ENV=prod npm run build:h5 -- --watch"
}

3、利用dotenv-flow.env.{NODE_ENV} 文件加载到 process.env并写入taro配置文件

默认注入dev标识,起h5 dev服务执行yarn dev:h5即可

// config/dev.js | config/prod.js
process.env.APP_ENV = process.env.APP_ENV || 'dev'

require('dotenv-flow').config({
  // node_dev表示.env.{node_env}文件
  node_env: process.env.APP_ENV
})

module.exports = {
  env: {
    APP_VERSION: process.env.npm_package_version,
    APP_ENV: process.env.APP_ENV,
    APP_API: process.env.APP_API
  }
};

四、图片处理

方案,小图标用 iconfont,借助 taro-iconfont-cli 这个包将图片处理成 svg;大图标用CDN

1、小图标

(1)安装taro-iconfont-cli

npm i taro-iconfont-cli -D

(2)初始化配置文件

npx iconfont-init

可以看到根目录生成了配置文件iconfont.json,具体参数说明可查看taro-iconfont-cli官网

{
    // 直接复制[iconfont](http://iconfont.cn/)官网提供的项目链接
    "symbol_url": "http://at.alicdn.com/t/font_1373348_kk9y3jk2omq.js",
    // 根据iconfont图标生成的组件存放的位置。每次生成组件之前,该文件夹都会被清空。
    "save_dir": "./src/assets/iconfont",
    // 如果您的项目使用Typescript编写,请设置为true。这个选项将决定生成的图标组件是`.tsx`还是`.js`后缀。
    "use_typescript": true,
    // 选择需要支持的平台,默认是`*`,意味着所有平台都需要支持(如果有)。如果你只想支持部分平台,也可以设置成数组:
    "platforms": ["weapp", "h5"],
    // 是否使用[尺寸单位rpx]还是普通的像素单位`px`。默认值为true,与Taro保持一致的缩放。您也可以设置为false,强制使用`px`
    "use_rpx": true,
    "trim_icon_prefix": "icon",
    "default_icon_size": 18,
    "design_width": 750
}

(3)生成Taro标准组件 执行yarn icon

// package.json
"scripts": {
    "icon": "npx iconfont-taro"
}

(4)使用图标

//xx.tsx
import { resolve } from 'path';
<IconFont name='user' size={40} color="#333" />

图标多了,会出现图片体积增大的问题,如果你的项目体积非常紧张(小程序体积有要求),没有多余的体积给图标,可以考虑 字体图标,直接使用字体图标的话可能会对首屏时间有一点影响,如果关注这个的话,可以直接使用 CDN

2、大图标

就比较简单了,将CDN前缀抽离为常量 CDN_IMG 写在 constants 文件夹下,使用时,拼接图片地址

// src/constants/index.ts
export const CDN_IMG = 'https://www.baidu.com/img/'
// xx.tsx
import { Image } from "@tarojs/components";
import { CDN_IMG } from "@/constants";

<Image src={`${CDN_IMG}PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png`} />

五、网络请求封装

方案:基于API Taro.request 封装,H5携带cookie配置credentials=include,而小程序需要自己实现传递cookie,可实现类似H5cookie通用方案微信小程序cookies方案

封装如下

import { request } from "@tarojs/taro";

// 请求头类型
const contentTypes = {
    json: 'application/json; charset=utf-8',
    urlencoded: 'application/x-www-form-urlencoded; charset=utf-8',
    multipart: 'multipart/form-data',
}

// 默认参数
const defaultOptions = {
    timeout: 3000,
    credentials: "include", // 设置H5端携带Cookie
    dataType: "json" // 返回的数据结构
}

interface OptionParams {
    url: string,
    method?: "OPTIONS" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "TRACE" | "CONNECT",
    data?: object,
    options?: any,
    contentType: 'json' | 'urlencoded' | 'multipart',
    prefixUrl: string
}

const callApi = ({
    url,
    method = 'GET',
    data,
    options,
    contentType = 'json',
    prefixUrl = 'api'
}: OptionParams) => {
    if (!url) {
        const error = new Error('请传入url')
        return Promise.reject(error)
    }

    const fullUrl = `/${prefixUrl}/${url}`

    const newOptions = {
        ...defaultOptions,
        header: {
            'content-type':
                (options.header && options.header['content-type']) ||
                contentTypes[contentType],
            'X-Requested-With': 'XMLHttpRequest',
        },
        ...options,
        data,
        method
    }

    return request({
        url: fullUrl,
        ...newOptions
    })
        .then((response) => {
            const { data } = response
            if (data.code === 'xxx') {
                // 与服务端约定
                // 登录校验失败
            } else if (data.code === 'xxx') {
                // 与服务端约定
                // 无权限
            } else if (data.code === 'xxx') {
                // 与服务端约定
                return Promise.resolve(data)
            } else {
                return Promise.reject(data)
            }
        })
        .catch((error) => {
            return Promise.reject(error)
        })

}

export default callApi

六、样式适配

1、确定设计稿尺寸

  • Taro 默认以 750px 作为换算尺寸标准;如果设计搞的尺寸不是750,可通过 designWidth配置
  • 默认支持 750640828 三种尺寸设计稿;如果需要支持自定义设计稿,可通过 deviceRatio 自定义;比如'375':2/1

方案:要求UI设计稿750px,无需更改config/index.js配置,直接使用默认配置

// config/index.js
const config = {
  ...
  // 设计稿尺寸
  designWidth: 750,
  // 设计稿尺寸换算规则
  deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    828: 1.81 / 2,
    350: 2 / 1
  },
  ...
}

2、外联样式写法

  • 根据设计搞 1:1 直接写px、或者%即可,Taro默认会对所有的单位进行转换;当转成微信小程序的时候,尺寸将默认转换为 rpx,当转成 H5 时将默认转换为以 rem 为单位的值
  • 不希望被转换的单位,在 px 单位中增加一个大写字母,例如 Px 或者 PX ,还有一些其他方案,不过不常用,具体可见官网
  • 配置 1px 不转换,通过配置 onePxTransform 字段为 false,如下
// config/index.js
const config = {
  ...
  h5: {
    publicPath: '/',
    staticDirectory: 'static',
    postcss: {
      autoprefixer: {
        enable: true
      },
      pxtransform: {
        enable: true,
        config: {
          onePxTransform: false // H5配置 1px 不需要被转换
        }
      }
    }
  },
  ...
}

3、内联样式写法

在编译时,Taro 会帮你对样式做尺寸转换操作,但是如果是在 JS 中书写了行内样式,那么编译时就无法做替换了,针对这种情况,Taro 提供了 Taro.pxTransform 来做运行时的尺寸转换

七、样式隔离

方案,用lessbem规范,每个样式文件的前缀确保唯一,为什么不用CSS module写法比较复杂,不符合常规开发习惯 代码示例

<View className="buyer-show-detail" >
  {
    showDetail?.mediaInfos?.length > 1 && isShowInfo &&
    <View className="buyer-show-detail__num">
      {currentId} / {imgList.length}
    </View>
  }

  <Swiper className="buyer-show-detail__swiper">
    {
      imgList.map((img, index) => (
        <SwiperItem className="buyer-show-detail__swiper-item" key={ index }>
          <HsImage
            className="buyer-show-detail__img" src={ img.picUrl }
            style={ { height: `${showLoadList[index]?.height}` } }
            mode="widthFix"
          />
        </SwiperItem>
      ))
    }
  </Swiper>

  {
    isShowInfo && <View className="buyer-show-detail-bottom">
      ...
    </View>
  }
</View>
.buyer-show-detail {
  height: 100vh;
  overflow: hidden;
  font-size: 24px;
  color: #fff;
  background: #222;

  &__num {
    position: fixed;
    top: 0;
    z-index: 99;
    width: 100%;
    padding-top: 24px;
    font-size: 28px;
    text-align: center;
  }

  &__swiper {
    height: 100%;
    &-item {
      display: flex;
      align-items: center;
    }
  }

  &__img {
    width: 100%;
  }

  &-bottom {
    position: fixed;
    bottom: 0;
    display: flex;
    width: 100%;
  }
}

八、小程序分包

1、解释什么是分包

官方文档说明如下,简而言之就说分包是一个优化项,可以优化小程序首次启动的下载时间 image.png

2、怎么做分包

配置文件如下

export default {
  // 主包
  pages: [
    "pages/tab/home/index",        // 首页
    "pages/tab/mine/index"         // 我的
  ],
  // 子包
  subpakages: [                    // 子包:user,个人中心
    {
      root: 'pages/user/',         
      pages: [
        "address/index",           // 个人地址信息页
        "settings/index"           // 个人信息设置页
      ]
    },
    {
      root: 'pages/order/',        // 子包:order,订单中心
      pages: [
        "list/index",              // 订单列表
        "detail/index"             // 订单详情
      ]
    }
  ],
  window: {
    backgroundTextStyle: "light",
    navigationBarBackgroundColor: "#fff",
    navigationBarTitleText: "WeChat",
    navigationBarTextStyle: "black",
  },
};

写在最后

本文所有的内容都总结为一个模版仓库,GitHub地址,感兴趣的可以点个star,有问题欢迎提issue,后续如果有更好的方案会持续更新~