taro多端小程序:HelloWorld Rank项目全面复盘!

项目基本介绍

技术选型

采用微信小程序开发框架— Taro,截止目前 taro 框架在 github 上的 star 数已经突破 18k,该框架采用 react 语法开发微信小程序,在一定程度上解决了小程序原生的开发方式的一些不足。

开发环境

  • 环境:node v11.0,taro 官方脚手架模块化及打包
  • 编辑器:vscode
  • 调试与测试:微信开发者工具稳定版

第三方框架与组件

  1. 开发框架:Taro
  2. ui 框架:taro-ui
  3. 数据流框架:tarojs/redux
  4. 图表渲染框架:echarts-for-weixin

项目目录如下所示

42d4ca156ea10a37f7a1ea942a323e3e.png

整体架构

baf615b0fcedb4280abbf41944eeb81a.png

项目数据流状态的管理

  • 数据状态管理是前端开发中较为困难的一部分,若前期数据状态管理十分混乱,会导致在后续产品迭代,需求变化的过程中,出现难以维护的情况。
  • 由于此小程序使用的 taro 开发框架应用 react 语法,而 redux 作为一种可预测的状态容器,在 react 生态中十分的流行,加之 taro 对 redux 的支持非常好,taro 官方也十分支持在 taro 框架中使用 redux 进行管理数据,此小程序采用 tarojs/redux 来进行管理数据状态。
    36b18bd005e51675a1e548746f9d1af8.png

项目中对工具函数的封装

对小程序 request 请求的封装

首先看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Taro from "@tarojs/taro";
const HOST = "https://pgrk.wizzstudio.com";

export default async function myApi(url, method = "GET", data = {}) {
Taro.showLoading({
title: "loading..."
});
try {
const response = await Taro.request({
url: HOST + url,
method,
data
});
Taro.hideLoading();
const res = response.data;
return res;
} catch (e) {
Taro.hideLoading();
Taro.showToast({
title: "加载失败,请查看网络环境",
icon: "none"
});
}
}

简单介绍一下~

  • 暴露出 url, method, data 基本可以满足整个项目所有的 request 请求
  • 设置统一的域名,方便项目由前期的测试域名迁移到生产环境下的域名,如果有多个域名下请求的话,便可以新建一个配置文件,使得更改更方便。
  • 统一的在 request 请求中加入 loading 效果,做到网速慢的情况有一定的反馈,省的每次请求时都需要手动加上。
  • 返回一个 promise 对象,在其他文件引用的时候可以通过.then 来处理请求之后的逻辑。

redux 中封装创建 action 的函数

show the code !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Taro from "@tarojs/taro";
import { HOST } from "./config";
const createActionSucess = (TYPE, data) => {
return {
type: TYPE,
payload: data
};
};
const createActionFail = data => {
return {
type: "GET_FAIL",
payload: data
};
};
export default function fetchData(option, TYPE) {
return async dispatch => {
Taro.showLoading({
title: "loading..."
});
const response = await Taro.request({
url: HOST + option.url,
method: option.method || "GET",
data: option.data || ""
});
const res = response.data;
if (res.code === 0) {
Taro.hideLoading();
dispatch(createActionSucess(TYPE, res.data));
} else {
Taro.hideLoading();
dispatch(createActionFail(res.data));
}
return res;
};
}

具体分析在另一篇博文中有详细介绍。

项目中遇到的困难(都解决了哈哈哈)

小程序无法获取用户微信好友,但是产品同学觉得微信好友 pk 是一个非常重要的事情,那怎么才能获取微信好友呢?

这里和后端同学讨论了一段时间,最终想到的解决方案是通过用户分享小程序的 url 上携带用户标识,在新用户点击加入额小程序完成登陆操作之后立即获取该用户以及源用户的 id,将其发送到后端,后端建立好友关系网,从而实现小程序内部好友的建立。
并且此方法依赖于用户的主动分享,所以也不算上打扰用户或者窥探用户隐私啦~~并且在小程序内部已有相关的引导说明

首先在页面中携带用户标识参数

1
2
3
4
5
6
onShareAppMessage = res => {
return {
title: "小程序标题",
path: `/pages/index/index?shareId=${myUserId}`
};
};

然后在新用户登陆的时候调用写好的“添加好友”函数进行判断以及处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Taro from "@tarojs/taro";
import myApi from "../service/api";
export function addUserRelation(userOne, userTwo) {
console.log("进入addUser收到的两个user", userOne, userTwo);
if (userOne != 0 && userTwo != 0) {
const data = {
userOne,
userTwo
};
myApi("/updateuserrelationship", "POST", data).then(res => {
if (res.code === 0) {
console.log("添加成功");
}
});
}
}
产品中间经历了一次大的迭代,登陆策略上都出现了比较大的变化,但是由于开发版,体验版,正式版三个版本共用一个缓存,又由于登陆策略的特殊性(这个就不细说啦,详见上个问题),版本更替测试的时候,第一版用户使用的依然是第一版的数据缓存,用户 id 分配便出现两个版本的混乱,如何解决呢?

其实这个也不算是什么大问题,主要问题的起源来自版本更新的时候涉及登录策略的改变(这个是由于上个问题导致的,即在登录过程中需建立好友关系网),再加上两个版本的开发版体验版正式版全部共用一个缓存!!
当然这个问题在最后也得到了完美的解决,即在登录函数中添加一个对 version 的维护,使得第一版中各个版本的用户更新 storage 的内容
这里要注意,由于项目的特殊性,和小程序的版本更新 api 想要解决的不是同一个问题,这个项目最关键的主要是开发版体验版正式版共用一个缓存导致的,而非小程序迭代更新导致的,故无法直接通过官方的 api 来解决
结合上个问题,可以看到最终的登录代码是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import Taro from "@tarojs/taro";
import myApi from "../service/api";
import { addUserRelation } from "./addUserRelation";
const againLogin = shareId => {
console.log("进入againLogin");
Taro.login().then(res => {
if (res.code) {
const userCode = res.code;
Taro.getUserInfo()
.then(userInfoRes => {
const data = {
code: userCode,
iv: userInfoRes.iv,
encryptedData: userInfoRes.encryptedData
};
myApi("/login", "POST", data).then(loginRes => {
if (loginRes.code === 0) {
console.log("重新登陆成功");
Taro.setStorageSync("login", {
userId: loginRes.data.userId,
openId: loginRes.data.openId,
session_key: loginRes.data.session_key
});
Taro.setStorageSync("version", {
ver: "2.1"
});
if (shareId) {
addUserRelation(shareId, loginRes.data.userId);
}
Taro.hideLoading();
Taro.switchTab({
url: "/pages/index/index"
});
} else {
Taro.hideLoading();
Taro.showToast({
title: "登录失败,请查看网络环境",
icon: "none"
});
}
});
})
.catch(e => {
Taro.redirectTo({
url: `/pages/login/login?shareId=${shareId}`
});
});
} else {
Taro.hideLoading();
Taro.showToast({
title: "登录失败,请检查网络环境",
icon: "none"
});
}
});
};
export default function checkToLogin(shareId = 0) {
Taro.showLoading({
title: "loading...",
icon: "loading"
});
const version = Taro.getStorageSync("version") || null;
if (!version) {
Taro.hideLoading();
console.log("version", version);
Taro.redirectTo({
url: `/pages/login/login?shareId=${shareId}`
});
} else if (version.ver !== "2.1") {
//手动在缓存中维护一个版本号,解决小程序的版本缓存问题
Taro.hideLoading();
Taro.redirectTo({
url: `/pages/login/login?shareId=${shareId}`
});
} else {
Taro.checkSession({
success: function() {
const basicInfo = Taro.getStorageSync("basicInfo") || null;
const loginInfo = Taro.getStorageSync("login") || null;
if (!basicInfo || !loginInfo) {
Taro.hideLoading();
Taro.navigateTo({
url: `/pages/login/login?shareId=${shareId}`
});
} else {
Taro.hideLoading();
if (shareId) {
const userId = Taro.getStorageSync("login").userId;
addUserRelation(shareId, userId);
}
}
},
fail: function() {
console.log("登录态过期");
againLogin(shareId);
}
});
}
}
再谈一下利用 redux 方便的实现不同组件间数据的联动

首先将 redux 状态树绑定到组件内部

1
2
3
4
5
6
7
8
9
10
11
@connect(
({ classInfo, cmtInfo }) => ({ classInfo, cmtInfo }),
dispatch => ({
getClassMsg(data) {
return dispatch(getClassMsg(data));
},
ajaxGetUserClass(userId) {
return dispatch(ajaxGetUserClass(userId));
}
})
)

然后需要在 componentDidUpdate 周期函数内对状态树进行监听,并发送请求更新状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 componentDidUpdate(prevProps) {
const { clazzId } = this.$router.params;
if (this.props.classInfo.userClassId != prevProps.classInfo.userClassId) {
this.checkIsAdded();
this.getClassMessage(clazzId).then(res => {
if (res.code === 0) {
this.setState({
isPunched: res.data.isPunchCard ? 1 : 0,
classMsgState: res.data
});
}
});
}
}