搭建Taro项目 编译多端小程序

809 阅读8分钟

我正在参加跨端技术专题征文活动,详情查看:juejin.cn/post/710123…

前言

Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ / 飞书 小程序 / H5 / RN 等应用。

在 Taro 3 中可以使用完整的 React / Vue / Vue3 / Nerv 开发体验。 具体的请参考官方文档

我使用Taro的时候,当时Taro 的版本还是 1.3.1版本的配置的形式还是在入口文件中的组件中新增一个 config配置对小程序的路径等进行配置。然后又升级了到了2.0版本我用的使用已经是2.2.8版本了1.0版本和2.0 版本的差距不大,升级也还行。

1.0、2.0 和 3.0 的比较

说说我的感受

1.0 版本

  • 配置路径在App入口文件中配置 新增字段进行config配置具体的路径等,界面的配置同样也是在页面组件中增加参数config进行单页界面的配置
  • 接收参数也是使用的 this.$router.params 获取到路径中携带的参数
  • 还需要引用@tarojs/async-await异步的包

2.0 版本

  • 移除了@tarojs/async-await
  • 同时还有一些项目配置也进行了修改

3.0 版本

  • 项目配置方式改变,具体就不多说了,官方文档上有:文档地址
  • 小程序配置方式改变,采用单个文件的方式进行配置
  • 界面获取小程序路径参数等的方式也进行了调整:通过getCurrentInstance来获取一些参数等。
  • 3.4之后版本和 3.0 ~ 3.4 这个区间的版本有差别,更新了一些新的东西。
import Taro, { getCurrentPages, getCurrentInstance } from '@tarojs/taro'

接下来开始项目搭建吧

技术栈

  • React (17版本)
  • Mobx (6.0 版本)
  • TypeScript 4.0

Mobx 状态管理,我这边是用来做对接口Api的请求处理和一些全局状态的保存。Mobx5.0 和 6.0 的写法稍微存在一些区别。最开始在Taro 1.0 的时候使用的是redux但是一些状态的改变太麻烦了,所以之后就切换成了mobx, 配置方便一些,使用也更简单。

项目搭建

首先安装一个全局的脚手架工具, 我习惯性的使用的是npm 来进行安装的,你们可以使用 cnpm 或者yarn 都行。我目前的Taro的版本是3.4.10 在3.4之前的版本可能会有一些细微的差别

npm install -g @tarojs/cli

安装完成之后的我们进入目录中开始初始化项目,我选择的是React的版本,用了TypeScript, mobx 的话,后面进行集成

image.png

需要注意一点的是: Taro 项目安装依赖的时候最好使用yarn 来进行安装对应的依赖,使用npm的话可能有的依赖会安装不上或者安装的过程中报错。还有就是需要注意当前的node的版本,有的依赖包低版本的可能安装不上,或者需要制定的版本(多次安装得出的经验)

项目添加@tarojs/cli

为什么需要在对应的项目依赖添加脚手架包呢?
因为Taro在运行的时候是依靠 @tarojs/cli来进行编译的。当项目中的@tarojs/taro版本和全局@tarojs/cli的版本不一致时,运行编译会有点问题。
所以我们在当前项目中安装上对应的版本的@tarojs/cli,这样在运行编译的时候就会优先从当前项目的依赖包中去寻找对应的依赖,而不是使用全局的。

(ps: 经验教训得到的,因为我一开始使用的是1.0的版本,后来项目的版本升到了2.0 但是全局的@tarojs/cli还是1.0 的, 项目中没有添加@tarojs/cli,然后项目成员在使用的时候发现运行报错了。最后发现是这个问题)

npm install --save-dev @tarojs/cli

or

yarn add --save-dev @tarojs/cli

安装完成

安装完成之后目录结构如下

├── dist                        编译结果目录
|
├── config                      项目编译配置目录
|   ├── index.js                默认配置
|   ├── dev.js                  开发环境配置
|   └── prod.js                 生产环境配置
|
├── src                         源码目录
|   ├── pages                   页面文件目录
|   |   └── index               index 页面目录
|   |       ├── index.tsx       index 页面逻辑
|   |       ├── index.css       index 页面样式
|   |       └── index.config.ts index 页面配置
|   |
|   ├── app.tsx                 项目入口文件
|   ├── app.css                 项目总通用样式
|   └── app.config.ts           项目入口配置
|
├── project.config.json         微信小程序项目配置 project.config.json
|
├── babel.config.js             Babel 配置
├── global.d.ts                 TypeScript 全局的一些定义
├── tsconfig.json               TypeScript 配置
├── .eslintrc                   ESLint 配置
|
└── package.json

需要开发百度和其他的小程序的话,需要加入配置文件,其中需要加配置文件的有 字节跳动 百度小程序 QQ 小程序 其他的小程序不用添加,是在相应的开发者工具中有相应的配置。

├── project.tt.json             字节跳动小程序项目配置 project.tt.json
├── project.swan.json           百度小程序项目配置 project.swan.json
├── project.qq.json             QQ 小程序项目配置 project.qq.json

添加Taro UI

接下来添加 TaroUI, 虽然我在项目中大部分时候都是用不到的,我基本直接用的都是自己手写的样式或者说原声的组件然后自定义样式。
那你可能在想,为啥不直接不用了用别的移动端的框架,我也想啊,但是这不是说 TaroUI 它做了多个平台的编译么,然后还有就是有的时候还是能用到一些些的。

安装依赖

npm install taro-ui@3.1.0-beta.2

按照文档的加上了这个。

image.png

image.png

勉强凑合用着吧,好吧运行一下,报错啦。

Deprecation Warning: Using / for division outside of calc() is deprecated and will be removed in Dart Sass 2.0.0.

Recommendation: math.div($at-button-height, 2) or calc($at-button-height / 2)

More info and automated migrator: https://sass-lang.com/d/slash-div87 │     border-radius: $at-button-height / 2;
   │                    ^^^^^^^^^^^^^^^^^^^^^
   ╵
    node_modules/taro-ui/dist/style/components/button.scss 87:20  @import
    node_modules/taro-ui/dist/style/components/index.scss 13:9    @import
    node_modules/taro-ui/dist/style/index.scss 12:9               @import
    src/app.scss 2:9                                              root stylesheet

Warning: 16 repetitive deprecation warnings omitted.

解决方案: 地址

如果感觉不行的可以试试用Taroify

增加mobx

安装依赖

npm install mobx mobx-react

src 下新建文件和文件夹 store store/index.ts store/moudle store/moudle/counter.ts

├── src                         源码目录
|   ├── store                   页面文件目录
|   |   └── index.ts            输出汇总store
|   |   └── moudle              存放对应的模块的文件目录: 可以按照项目来区分
|   |       ├── counter.ts      计数器的mobx模块

counter.ts 中新增一个字段保存计数,并且操作增加或减少的函数

// counter.ts
import { makeAutoObservable } from 'mobx'

/** 定义当前模块的接口,方便使用的时候直接获取到 类型提示 */
export interface CounterProps {
  /** 数量 */
  counterNum: number
  /**
   * 操作函数
   * @param {add|reduce} type 操作类型
   */
  onOperationCounter: (type: 'add'|'reduce') => void
}

class Counter {
  constructor () {
    // mobx6 可以直接在constructor中定义这个,然后不用去用action 和 observable 绑定字段和函数
    makeAutoObservable(this)
  }

  counterNum: number = 0

  onOperationCounter = (type: 'add'|'reduce') => {
    if (type == 'add') {
      this.counterNum++
    } else {
      this.counterNum--
    }
  }

}
// 导出
export default new Counter()

store/index.ts 导出对应的store

// store/index.ts
import counter, { CounterProps } from './moudle/counter'

export interface StoreProps {
  /** 计数函数 */
  counter: CounterProps
}

export default {
counter
}
 

修改入口文件, 将mobx注入

import { Component } from 'react'
import { Provider } from 'mobx-react'
import store from './store/index'
import './app.scss'

class App extends Component {

  // this.props.children 是将要会渲染的页面
  render () {
    return (
      <Provider store={store}>
        {this.props.children}
      </Provider>
    )
  }
}

export default App

界面中使用 src/pages/index/index.tsx

import { Component } from 'react'
import { View, Text, Button } from '@tarojs/components'
import { observer, inject } from 'mobx-react'
import { StoreProps } from '../../store/index'
import './index.scss'

interface IndexProps {
  store: StoreProps
}

@inject('store')
@observer
export default class Index extends Component<IndexProps> {

  render () {
    const { counter } = this.props.store
    return (
      <View className='index'>
        <Button onClick={() => counter.onOperationCounter('add')}>加</Button>
        <Text>{counter.counterNum}</Text>
        <Button onClick={() => counter.onOperationCounter('reduce')}>减</Button>
      </View>
    )
  }
}

然后运行一波。

pic.gif

封装请求

新建文件夹service存放封装的请求新建两个文件

  • interceptors.ts 存放请求的拦截的封装
  • index.ts 请求的封装
// interceptors.ts
import Taro from '@tarojs/taro'

export const HTTP_STATUS = {
  SUCCESS: 200,
  CLIENT_ERROR: 400,
  AUTHENTICATE: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
  BAD_GATEWAY: 502,
  SERVICE_UNAVAILABLE: 503,
  GATEWAY_TIMEOUT: 504,
  PASSWORD_ERROR: 426
}

/** 请求参数 */
export interface RequestProps extends Taro.request.Option<any, any> {
  /** 域名的配置 */
  baseURL?: string
}

/** 封装请求的拦截器 */
const customInterceptor = (chain) => {
  const requestParams = chain.requestParams
  return chain.proceed(requestParams).then(res => {
    if (res.statusCode === HTTP_STATUS.NOT_FOUND) {
      return Promise.reject({ message: '请求资源不存在', ...res })
    } else if (res.statusCode === HTTP_STATUS.BAD_GATEWAY) {
      return Promise.reject({ message: '服务端出现了问题', ...res })
    } else if (res.statusCode === HTTP_STATUS.FORBIDDEN) {
      return Promise.reject({ message: '没有权限访问', ...res })
    } else if (res.statusCode === HTTP_STATUS.GATEWAY_TIMEOUT) {
      return Promise.reject({ message: '请求超时', ...res })
    } else if (res.statusCode === HTTP_STATUS.AUTHENTICATE) {
      return Promise.reject({ message: '没有访问的权限', ...res })
    } else if (res.statusCode === HTTP_STATUS.SUCCESS) {
      return Promise.resolve(res)
    } else {
      return Promise.reject(res)
    }
  })
}

// Taro 提供了两个内置拦截器
// logInterceptor - 用于打印请求的相关信息
// timeoutInterceptor - 在请求超时时抛出错误。

const interceptors = [customInterceptor]
// 打印请求的数据
interceptors.push(Taro.interceptors.logInterceptor)

export default interceptors

index.ts 中新增request函数用于发起请求

// index.ts
import Taro from '@tarojs/taro'
import interceptors, { RequestProps } from './interceptors'

interceptors.forEach(interceptorItem => Taro.addInterceptor(interceptorItem))

/**
 * 基础的请求配置
 * @param {RequestProps} option
 * @return {Promise<any>}
 */
export const request = (option: RequestProps) => {
  const { url, baseURL, data, method = 'GET', header, ...otherConfig } = option
  return new Promise((resolve, reject) => {
    Taro.request({
      url: baseURL ? baseURL + url : url,
      data,
      method,
      header: {
        'Content-Type': 'application/json;charset=UTF-8',
        ...header
      },
      ...otherConfig
    }).then((res) => {
      // 业务逻辑的处理
      resolve(res)
    }).catch(e => {
      // 报错的处理
      reject(e)
    })
  })
}

项目优化

引用路径定义

修改 config/index.jstsconfig.json 文件

config/index.js 新增 alias 配置路径的引用

// config/index.js
import path from 'path'

alias: {
    '@pages': path.resolve(__dirname, '..', 'src/pages'),
    '@store': path.resolve(__dirname, '..', 'src/store'),
    '@service': path.resolve(__dirname, '..', 'src/service')
},

同时 tsconfig.json 也需要增加一个引用配置

// tsconfig.json
"compilerOptions": {
    "paths": {
      "@pages/*": ["./src/pages/*"],
      "@store/*": ["./src/store/*"],
      "@service/*": ["./src/service/*"]
    }
}

然后就可以将页面中的路径替换成如下使用方式

import { StoreProps } from '@store/index'
import { request } from '@service/index'
// import { StoreProps } from '../../store/index'
// import { request } from '../../service/index'

更改打包文件存放位置

目前的无论是微信还是其他的平台的打包都是存放在dist 文件下的,这样可能我们有的时候打包不是很方便,所以就设置一个参数放入对应的打包中

安装依赖

npm install --save-dev cross-env

修改 package.json 中的 scripts命令, 命令前增加一个文件夹参数folder, 只需要在build的命令中加入即可

"scripts": {
    "build:weapp": "cross-env folder=weapp taro build --type weapp",
    "build:swan": "cross-env folder=swan taro build --type swan",
    "build:alipay": "cross-env folder=alipay taro build --type alipay",
    "build:tt": "cross-env folder=tt taro build --type tt",
    "build:h5": "cross-env folder=h5 taro build --type h5",
    "build:rn": "cross-env folder=rn taro build --type rn",
    "build:qq": "cross-env folder=qq taro build --type qq",
    "build:jd": "cross-env folder=jd taro build --type jd",
    "build:quickapp": "cross-env folder=quickapp taro build --type quickapp",
    "dev:weapp": "npm run build:weapp -- --watch",
    "dev:swan": "npm run build:swan -- --watch",
    "dev:alipay": "npm run build:alipay -- --watch",
    "dev:tt": "npm run build:tt -- --watch",
    "dev:h5": "npm run build:h5 -- --watch",
    "dev:rn": "npm run build:rn -- --watch",
    "dev:qq": "npm run build:qq -- --watch",
    "dev:jd": "npm run build:jd -- --watch",
    "dev:quickapp": "npm run build:quickapp -- --watch"
  },

接下来修改配置文件index.js

const config = {
  outputRoot: `dist/${process.env.folder}`,
}

好了,运行打包就可以看到输出在不同的文件目录下了

image.png

小程序机器人上传

为什么需要自动上传?

  1. 当人员多的时候,每个开发者可能都会上传一个版本的代码到测试,但是小程序设置体验版本一次只能是一个的,这样就会频繁的去切换体验版本。
  2. 可以用来控制版本的环境: 如开发, 预发, 正式,在提交审核的时候,只用该版本的。

这个需要借助于miniprogram-ci ,先安装

npm install --save-dev  miniprogram-ci

在 config 中增加upload文件夹。并创建以下文件

├── config                               项目编译配置目录
|   ├── upload                           默认配置
|   |       ├── private.微信小程序的appid.key   微信小程序 上传密钥
|   |       ├── upload.pro.js            正式环境配置
|   |       ├── upload.tes.js            测试环境配

private.微信小程序的appid.key 文件的获取方式 登录 微信公众平台
找到 开发 => 开发管理 => 开发设置

找到小程序代码上传, 然后生成上传密钥

image.png

注意:IP 白名单关闭的话,那么只要拿到这个key,然后是当前的开发者就能传递代码了,也可以选择配置IP的形式来限制

upload.pro.js 中增加上传的内容

const ci = require('miniprogram-ci')
const { version } = require('../../package.json')
const dayjs = require('dayjs')

const project = new ci.Project({
  appid: '微信小程序的appid',
  type: 'miniProgram', // 项目的类型,有效值 miniProgram/miniProgramPlugin/miniGame/miniGamePlugin
  projectPath: process.cwd() + '/dist/weapp', // 项目的路径,即 project.config.json 所在的目录
  privateKeyPath: process.cwd() + '/config/upload/private.微信小程序的appid.key', // 私钥文件地址
  ignores: ['node_modules/**/*'],
})
ci.upload({
  project,
  version,
  desc: dayjs().format('YYYY年MM月DD日 HH:mm:ss') + '提交上传',
  setting: {
    urlCheck: true,
    es6: false,
    postcss: false,
    minified: false
  },
  robot: 2
}).then(res => {
  console.log(res)
  console.log('上传成功')
}).catch(error => {
  if (error.errCode == -1) {
    console.log('上传成功')
  }
  console.log(error)
  console.log('上传失败')
  process.exit(-1)
})

修改 package.json 中的 scripts命令,增加上传的命令

"scripts": {
    "upload:tes": "npm run build:weapp && node config/upload/upload.tes.js",
    "upload:pro": "npm run build:weapp && node config/upload/upload.pro.js"
 },

上传配置中的 robot 代表不同的机器人, 运行之后就看到上传的代码是这样的, 可以指定一个机器人为体验版本,不同的成员之间上传的也都是用一个机器人上传的。

image.png

小程序分包加载

小程序的一个包的大小就只有2M,当包的大小大于2M之后,我们还在加需求加代码,这个时候就可以考虑分包了, 分包其实很简单。

新增文件夹shadowAshadowB 存放分包的界面

image.png

src/shadowA/pages/index/index.tsx界面中我们增加互相跳转的路由, shadowB 的文件也增加类似的。

// src/shadowA/pages/index/index.tsx
import React from 'react'
import { View, Button } from '@tarojs/components'
import Taro from '@tarojs/taro'
import './index.scss'

export default () => {
  const onTrunLink = (type?: '/shadowB') => {
    let url = '/pages/index/index'
    if (type) {
      url = type + url
    }
    Taro.navigateTo({ url })
  }

  return (
    <View className='index'>
      <View>
        <Button onClick={() => onTrunLink('/shadowB')}>跳转shadowB</Button>
        <Button onClick={() => onTrunLink()}>跳转主包</Button>
      </View>
    </View>
  )
}

然后我们修改 app.config.ts 的配置,新增subpackages 配置,

注意: 跳转到分包的时候,需要在路径前带上分包的存放文件夹(shadowA)

image.png

代码规范

eslint 代码检查

代码规范的使用eslintstandard 规范

npm install --save-dev  eslint-config-standard eslint-friendly-formatter eslint-plugin-jsx-a11y eslint-plugin-node eslint-plugin-promise eslint-plugin-standard eslint-plugin-typescript 

修改.eslintrc的配置文件

{
  "root": true,
  "extends": ["taro/react", "standard"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "globals": {
    "__static": true,
    "document": true,
    "navigator": true,
    "window":true,
    "node":true
  },
  "env": {
    "browser": true,
    "node": true
  },
  "plugins": [
    "@typescript-eslint",
    "typescript",
    "react"
  ],
  "rules":{
    "arrow-parens": "off",
    "no-useless-return":"off",
    "generator-star-spacing": "off",
    "no-debugger": "off",
    "no-extra-semi": "error",
    "no-unreachable": "error",
    "no-dupe-class-members": "off",
    "no-useless-constructor": "off",
    "no-unused-vars": "off",
    "no-useless-escape": "off",
    "prefer-promise-reject-errors": "off",
    "eqeqeq": [
      "error",
      "always",
      {
        "null": "ignore"
      }
    ],
    "typescript/class-name-casing": "error",
    "standard/array-bracket-even-spacing": "off",
    "react/jsx-uses-react": "off",
    "react/react-in-jsx-scope": "off"
  }
}

添加完文件发现这个我们不能直接关闭掉。

image.png

当存在这个我们没法取到具体类型的时候,我们进行让eslint忽略它,不去检查它。新增.eslintignore 文件进行配置需要忽略检查的目录和特定的文件

// .eslintignore

test/*
static/*.js
config/*
node_modules/*
src/**/*.config.ts

这样就不会报错了。

image.png

代码格式化 Prettier

省略...... 之后补上😂

完结

这边文章大概就先这样了, 还有些没有完善的地方

  • 请求的封装,重复的请求,取消请求这些没有写进去。
  • 代码格式化,git 提交规范
  • 小程序中的iphone机型的一些适配
  • 自定义导航栏:自定义顶部导航栏之后,下来刷新是整个屏一起下拉,所以还会衍生出下拉刷新上拉加载的问题。
  • oss 文件上传的封装
  • 错误日志的打印
  • 还有个没有解决的问题, TaroUI的sass文件引用的问题。

注意: 微信小程序包的大小只有2M

image.png

项目源码: GitHub

完结 🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹