以前我没得选,现在我只想做个坏人

ReactNative实战之社交网络客户端(一:基础篇)

    大前端     ReactNative·Mastodon·App·Android

  1. 前言
  2. 项目结构
  3. 介绍下目录规划
  4. 状态管理
  5. UI库的选择
  6. 组件重用
  7. 封装请求

前言


完整项目地址:https://github.com/shuiRong/Gakki 🌟🌟🌟

系列文章:

对了,这里有一篇介绍Mastodon (长毛象)的文章,毕竟我是在为它开发客户端🤣

项目结构


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
.
├── App.js // 项目路由配置,因为我这个项目需要登陆,所以我就根据登陆/未登陆,定义了两个路由入口
├── LICENSE // 证书
├── README.md // 介绍文档
├── android // 原生Android工程
├── app.json // 用来配置原生App需要的基本信息
├── index.js // 入口文件
├── ios // 原生iOS工程
├── node_modules // 依赖项目的源代码
├── package.json // 定义项目所需要的各种依赖/模块,以及项目的配置信息
├── preview // App预览图
├── rn-cli.config.js // 顾名思义,是命令行工具:react-native的配置信息
├── src // 项目源代码。你写的业务代码基本都在这里
├── Main.js // 配置了“登陆”状态下各页面的路由信息
├── SignedOutNavigator.js // 配置了“未登陆”状态下各页面的路由信息
├── assets // 会被RN打包工具打包的静态资源
├── pages // App的页面级文件
│ ├── About.js // 关于页面
│ ├── Auth.js // 授权认证页面(Mastodon客户端登录时不是常规的账号密码登录,而是输入账号密码然后弹出授权认证,想想微信的授权认证)
│ ├── BlockedUsers.js // 被屏蔽用户页面
│ ├── Envelope.js // 私信页面
│ ├── FollowRequestList.js // 关注请求页面
│ ├── Followers.js // 关注者页面
│ ├── Following.js // 关注页面
│ ├── Home.js // 首页
│ ├── Launcher.js // 启动页面,从存储系统中加载主题、用户Token等基础数据
│ ├── Login.js // 登陆页面,即输入账号密码的地方
│ ├── MutedUsers.js // 被隐藏用户页面
│ ├── Notifications.js // 消息通知页面
│ ├── OpenSource.js // 开源声明页面,列出了项目所有用到的开源软件及开源协议
│ ├── Profile.js // 个人详情页面
│ ├── Search.js // 搜索页面
│ ├── SendToot.js // 发送嘟文(类似于微博的博文)的页面
│ ├── SideBar.js // 主页侧栏组件
│ ├── Tag.js // 标签页面
│ ├── Test.js // 测试页面,开发项目时需要测试某模块时可以在这个页面单独测试
│ ├── TootDetail.js // 嘟文详情页面
│ ├── common // 项目中会多处使用的公共组件
│ │ ├── Context.js // 评论组件
│ │ ├── DefaultTabBar.js // Tab组件的顶部Bar组件
│ │ ├── Divider.js // 分割线组件
│ │ ├── Fab.js // Floating Action Button (FAB)组件,一般是漂浮在右下角的那个圆形按钮
│ │ ├── HTMLView.js // 把渲染HTML的部分提取出来,自成一个组件
│ │ ├── Header.js // 顶部的Header组件
│ │ ├── ListFooterComponent.js FlatList组件的尾部组件
│ │ ├── Loading.js // Loading加载效果组件
│ │ ├── MediaBox.js // 媒体文件(图片、视频)展示组件
│ │ ├── Notice.js // 提示框、确认框等组件
│ │ ├── ReplyInput.js // 封装的单行、多行输入框组件
│ │ ├── Spruce.js // 骨架屏组件,自定了几个常用骨架屏
│ │ ├── TootBox.js // 嘟文组件
│ │ ├── UserItem.js // 用户列组件
│ └── screen // Tab组件下的页面
│ ├── HomeScreen.js // 主页时间轴Tab下的组件
│ ├── LocalScreen.js // 本站时间轴Tab下的组件
│ ├── MediaScreen.js // 媒体文件Tab下的组件
│ ├── PublicScreen.js // 跨站公共时间轴Tab下的组件
│ └── TootScreen.js // (个人详情页面下)个人嘟文Tab下的组件
└── utils // 放置一些公共方法
├── api.js // 定义用到的所有接口
├── color.js // 统一管理项目颜色
├── config.js // 配置信息,如开发时使用的Token
├── locale.js // 国际化文件,目前只有中文的时间字符,未来会集成所有的国际化数据
├── mobx.js // 项目全局状态管理中心
├── request.js // 封装axios,增加全局请求、响应拦截器
└── store.js // 封装AsyncStorage:本地存储系统
└── yarn.lock // yarn为了跨机器安装得到一致的结果而生成的文件

介绍下目录规划

使用react-native-cli工具初始化完项目的时候,你就会发现,它生成的所谓源码文件只有index.jsApp.js,而这基本不足以支持任何复杂点的项目。所以就需要自己来:先新建一个src文件夹,然后规划文件。

首先需要一个入口/路由文件(我这里根据“登陆”和“未登陆”两种状态,规划了两个入口文件:Main.jsSignedOutNavigator.js),然后还需要一个 pages文件夹放置所有的“页级”文件,一个common用来放置那些会被引用多次的组件,比如Header组件(基本每个页面都要用到Heeader组件)。然后我还建了一个screen组件用来放置每个Tab对应的文件,因为严格来说,一个Tab并不属于一个页面。首页的Tab是这样的:

首页Tab

然后介绍下utils文件夹。

utils

统一管理很重要!所以我接口、本地存储、国际化、颜色搭配、状态管理都封装到了单独文件中,任何组件需要使用的时候都可以自行调用。

最开始开发的时候,其实我并没有把颜色这一块给封装成单个文件,这导致后面我要实现夜间模式时直接懵逼,颜色写死在项目中的各处,完全没法实现夜间模式!因此希望大家谨记我的教训,任何重复两次以上的代码都要考虑下是否可以声明成变量或者封装成组件!

状态管理


我在项目开发初期并没有使用状态管理工具,因为当时项目复杂度还没有上来,使用state足矣。

就像React社区中流传甚广的一句话:don’t use Redux until you have problems with vanilla React.

意思是:除非你遇到了只使用React解决不了的问题,不要使用Redux。

P.S. React Native将会在下一个稳定版本中支持Hooks:是一个用起来非常舒服的功能,建议了解一下。

直到我遇到了“主题切换功能”,非引入一个状态管理功能不能解决,然后我就引入了Mobx(上手快)

关于ReduxMobx我们应该如何选择的问题,这里推荐一篇文章:Redux or MobX: An attempt to dissolve the Confusion中译文

作者观点比较中立,并没有偏向任何一方。(所以看完之后,你的选择也没必要非要和我一样)

UI库的选择


首先你需要知道的是,你不能奢求某一款UI库能完全满足你的需求。实际也不会存在这么一款UI库。

因此项目开发时基本是这样:一个流行组件库 + N个“单UI”组件(根据需要自行Google) + 自行实现Style

常见的流行UI库我基本都看过,包括不限于:

最后选择了Teaset,理由简单实在(笑):它由“纯JS”实现,不含任何原生代码。(这意味着,如果遇到问题需要去看源码的话,我能看懂🤣)

组件重用


路由我用的是[react navigation]

设想下这样的场景:在水溶的个人主页点击小明的头像。我们预期的是页面会跳转到小明的个人主页,但实际呢?实际是页面不会有任何变化(当然,如果你没有针对这种情况做处理的话。)

个人详情页面

这是因为当跳转前后的两个路由相同(也即组件复用)的话,默认不会重新渲染组件。那么componentDidMount钩子(一般我们在这个钩子里面请求接口数据)就不会触发,那么页面就不会有任何变化。

P.S. 如果你在开发ReactNative之前曾写过Vue/React/Angular项目的话,可能就会遇到过这样的问题。

解决方法就是在componentWillReceiveProps钩子里重新请求接口数据,因为前后两个路由相同会触发此钩子。

1
2
3
4
5
6
7
componentDidMount() {
this.init(this.props.navigation.getParam('data'))
}

componentWillReceiveProps({ navigation }) {
this.init(navigation.getParam('data'))
}

封装请求


用来请求接口数据的HTTP库,我是用的是axios,因为之前在写Vue、React项目时就有在用,已经比较熟悉了。如果你是用了其他的库也无妨,思想都是想通的,可以借鉴。

为了开发测试方便,有必要将axios封装一下,加上请求、响应拦截等逻辑。这样的话,未来增加特定Header或增加接口错误时提示信息也方便,在这里加就可以了。

代码贴出来了,简单,都能看懂:

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
import axios from 'axios'
import { Toast } from 'teaset'

const service = axios.create({
baseURL: 'https://cmx.im', // 统一Host
timeout: 10000 // 请求超时时间限制
})

// 请求拦截器
service.interceptors.request.use(
config => {
// 如果未来需要增加统一Header,加在这里
return config
},
err => {
Promise.reject(err)
}
)

// 响应拦截器
service.interceptors.response.use(
response => {
// 如果你需要对接口返回的数据特殊处理一下的话,写在这里
return response.data
},
err => {
console.log('拦截器err:', err)
if (err && err.error) {
// 服务器异常,统一展示出来
Toast.message(err.error)
}

return Promise.reject(error)
}
)

export default service
page PV:  ・  site PV:  ・  site UV: 
知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可