RN从0到1系统精讲与小红书APP实战(2023版)万山春色归
ReactNative介绍
RN从0到1系统精讲与小红书APP实战
下栽の地止:https://lexuecode.com/6659.html
React Native 是一个由 Facebook 于 2015 年 9 月发布的一款开源的 JavaScript 框架,它可以让开发者使用 JavaScript 和 React 来开发跨平台的移动应用。它既保留了 React 的开发效率,又同时拥有 Native 应用的良好体验,加上 Virtual DOM 跨平台的优势,实现了真正意义上的:Learn Once,Write Anywhere.
注:非高清 logo,这不是原子结构模型吗?暗示 React (Native)是万恶之源?
React Native 的特点
跨平台
React Native 使用了 Virtual DOM(虚拟 DOM),只需编写一套代码,便可以将代码打包成不同平台的 App,极大提高了开发效率,并且相对全部原生开发的应用来说,维护成本也相对更低。
上手快
相比于原生开发,JavaScript 学习成本低、语法灵活。允许让 Web 开发者更多地基于现有经验开发 App。React Native 只需使用 JavaScript 就能编写移动原生应用,它和 React 的设计理念是一样的,因此可以毫不夸张地说:你如果会写 React,就会写 React Native!
原生体验
由于 React Native 提供的组件是对原生 API 的暴露,虽然我们使用的是 JavaScript 语言编写的代码,但是实际上是调用了原生的 API 和原生的 UI 组件。因此,体验和性能足以媲美原生应用。
热更新
React Native 开发的应用支持热更新,因为 React Native 的产物是 bundle 文件,其实本质上就是 JS 代码,在 App 启动的时候就会去服务器上获取 bundle 文件,我们只需要更新 bundle 文件,从而使得 App 不需要重新前往商店下载包体就可以进行版本更新,开发者可以在用户无感知的情况下进行功能迭代或者 bug 修复。但是值得注意的是,AppStore 禁止热更新的功能中有调用私有 API、篡改原生代码和改变 App 的行为。
React Native 原理
JavaScriptCore
JavaScriptCore 是 JavaScript 引擎,通常会被叫做虚拟机,专门设计来解释和执行 JavaScript 代码。在 React Native 里面,JavaScriptCore 负责 bundle 产出的 JS 代码的解析和执行。
JS Engine
React Native 需要一个 JS 的运行环境,因为 React Native 会把应用的 JS 代码编译成一个 JS 文件(x x.bundle),React Native 框架的目标就是解释运行这个 JS 脚本文件,如果是 Native 拓展的 API,则直接通过 bridge 调用 Native 方法,最基础的比如绘制 UI 界面,映射 Virtual DOM 到真实的 UI 组件中。
绿色的是我们应用开发的部分,我们写的代码基本上都是在这一层。
蓝色代表公用的跨平台的代码和工具引擎,一般我们不会动蓝色部分的代码。
黄色代表平台相关的 bridge 代码,做定制化的时候会添加修改代码。
红色代表系统平台的功能,另外红色上面有一个虚线,表示所有平台相关的东西都通过 bridge 隔离开来了,红色部分是独立于 React Native 的。
脱离 React Native,纯原生端是如何与 JS 交互的?来看下 iOS 里面是如何实现的。
在 Native 创建一个 JS 上下文:
// 创建一个ctx的JS上下文 JSContent *ctx = [[JSContent alloc] init]; // 创建一个变量name [ctx evaluateScript:@"var name = 'Hellen'"]; // 创建一个方法 [ctx evaluateScript:@"var hello = function(name) { return 'hello ' + name }"];
Native 调用 JavaScript 方法:
// 通过ctx上下文对象,获取到hello方法 JSValue *helloFUnction = ctx[@"hello"]; // 运行js方法 JSValue *greetings = [helloFunction callWithArguments:@[@"bytedancers"]; // hello bytedancers
所以,JavaScript 代码只要将变量暴露在 JS 上下文全局,Native 就能获取到,并运行 JS 的代码。
JavaScript 调用 Native,首先需要在 Native 端,将一个变量暴露在 JS 上下文全局,在 JavaScript 全局变量里面就能获取到并执行这个方法:
ctx[@"createdByNative"] = ^(NSString *name) {
// do something
return someResult
}
RN从0到1系统精讲与实战小红书APP - 实战
Navigator使用和封装
点击查看官方文档
0.44版本后Navigator已经从react-native库中移除,如需导入可按如下操作:
// install
$npm install React-native-deprecated-custom-components --save
// import API
import CustomerComponents, {Navigator} from 'react-native-deprecated-custom-components';
实际项目中对于单页面应用,我们可以把Navigator封装成一个组件,把各页面当作Navigator的一个个场景转换,在页面中实现跳转,返回,动画等的各种操作时只需要调用相应方法即可。
class APP extends Component {
constructor(props) {
super(props);
this._renderScene = this._renderScene.bind(this);
this.state = {};
}
/* eslint-disable */
_renderScene(route, navigator) {
let Component = route.component;
return (
<Component
{...route}
navigator={navigator}
passProps={route.passProps}
callback={route.callback}
/>
);
}
render() {
return (
<View style={{ flex: 1 }}>
<Navigator
ref="navigator"
renderScene={this._renderScene}
configureScene={(route) => ({
...route.sceneConfig || Navigator.SceneConfigs.HorizontalSwipeJump,
gestures: route.gestures
})}
initialRoute={{
component: Login
}}
/>
<LoadingView isVisible={this.props.showLoading} />
</View>
)
}
}
除了场景转换等操作,还可以在这个组件中集成控制App全局的一些操作,比如说,Loading的设置,网络状态检查等设置,在各页面就无须再单独设置。尽量在一个地方里面实现控制app的一些相近的默认操作
实际页面中跳转或其他操作:
_jumpPage() {
const { navigator } = this.props;
if (navigator) {
navigator.push({
component: TabBarList, //next route
sceneConfig: Navigator.SceneConfigs.FloatFromBottomAndroid, // animated config
callback: () => {} //callback
passProps: { //transfer parameters
tabs: 'home',
activeTab: 'home',
onPressHandler: this.props.goToPage
}
});
}
}
React Navigation理解和使用
点击查看官方文档
react-native 0.44版本之前路由控制使用的Navigator虽然非常稳定,基本没出现过什么BUG,但是跳转效果一直被人诟病,跳转时候的动画和原生App的效果相比,非常明显差一等,在0.44版本后Facebook推荐使用react-navigation库来实现页面跳转,tab转换,侧边栏滑动等功能。
react-navigation主要包括导航,底部tab,顶部tab
,侧滑等,功能很强大,而且体验接近原生。接下来会一一介绍:
导航 -> StackNavigator
底部或者顶部tab -> TabNavigator
关于侧滑DrawerNavigator的使用,笔者不在本文介绍,但可以看这篇附带Demo的推荐博客
StackNavigator
StackNavigator在功能上就是相当于原来使用Navigator,但是他有着不一样的实现和非常好的跳转体验,使用上也非常简单,其实也就是三部曲:
路由配置(页面注册):
const routeConfigs = {
Login: { screen: Login },
TabBar: { screen: TabBarContainer },
Feedback: { screen: Feedback },
};
默认场景配置:
const stackNavigatorConfig = {
initialRouteName: 'Login',
navigationOptions: {
headerBackTitle: null,
headerTintColor: 'white',
showIcon: true,
swipeEnabled: false,
animationEnabled: false,
headerStyle: {
backgroundColor: '#f2f2f2'
}
},
mode: 'card',
paths: 'rax/: Login',
headerMode: 'float',
transitionConfig: (() => ({
screenInterpolator: CardStackStyleInterpolator.forHorizontal // android's config about jump to next page
})),
onTransitionStart: () => {},
onTransitionEnd: () => {}
};
容器生成与初始化:
const Nav = StackNavigator(routeConfigs, stackNavigatorConfig);
export default class QQDrawerHome extends Component {
render() {
return(
<Nav/>
);
}
}
这样就简单完成了路由的配置,开发时只需要把新页面添加到注册对象routeConfigs中,StackNavigator会对里面的的注册页面和注册时使用的KEY值形成对应关系,当你在页面时跳转时,只需要这样:
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
const { navigation } = this.props;
navigation.navigate('TabBar');
}
}
带参数跳转时:
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
const { navigation } = this.props;
navigation.navigate('TabBar', {
visible: false,
title: '首页'
});
}
}
在下个页面就可以拿到参数并设置头部或其他参数:
static navigationOptions = ({ navigation }) => {
const { state } = navigation;
const { title } = state.params;
return {
title: title,
};
};
其他reset,setParams等操作将可以学着本文后面封装到组件中去使用,当然你也可以直接在页面跳转函数中重置路由,就像这样:
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Login'})
]
})
this.props.navigation.dispatch(resetAction)
TabNavigator
0.44版本之前我们实现Tab页面通常都选择使用框架react-native-tab-navigator或者react-native-scrollable-tab-view,现在0.44版本后react-navigation库中推荐使用TabNavigator,同样的使用方式,类似StackNavigator三部曲:
const routeConfigs = {
Message:{
screen:QQMessage,
navigationOptions: {
tabBarLabel: '消息',
tabBarIcon: ({ tintColor }) => (
<Image
source={require('./notif-icon.png')}
style={[styles.icon, {tintColor: tintColor}]}
/>),
}
},
Contact:{
screen:QQContact,
navigationOptions: {
tabBarLabel: '联系人',
tabBarIcon: ({ tintColor }) => (
<Image
source={require('./notif-icon.png')}
style={[styles.icon, {tintColor: tintColor}]}
/>),
}
},
};
const tabNavigatorConfig = {
tabBarComponent:TabBarBottom,
tabBarPosition:'bottom',
swipeEnabled:false,
animationEnabled:false,
lazy:true,
initialRouteName:'Message',
backBehavior:'none',
tabBarOptions:{
activeTintColor:'rgb(78,187,251)',
activeBackgroundColor:'white',
inactiveTintColor:'rgb(127,131,146)',
inactiveBackgroundColor:'white',
labelStyle:{
fontSize:12
}
}
}
export default TabNavigator(routeConfigs, tabNavigatorConfig);
关于使用TabNavigator的一些注意点和当前问题:
如你甚至未使用StackNavigator,而想直接使用TabNavigator,还是用其他第三方框架吧,他和StackNavigator是配套使用的,你必须保证TabNavigator存在于StackNavigator中,TabNavigator才能良好工作。
当你当前页面使用了TabNavigator,那么TabNavigator所形成的容器组件应该是当前页面的顶层组件,否则报错,将会无法获取到tab中的router数组。
关于嵌套使用TabNavigator,即在TabNavigator的一个screen中再次使用了TabNavigator形成页面,安卓平台下无法渲染子组件,页面空白,且内层Tab基本失效,或者你的内层Tab容器使用其他第三方框架如react-native-tab-view等类似框架,问题依然存在,关于此问题可关注公关BUG#1796。
StackNavigator路由的集中封装
此部分集成了一部分Redux知识,建议可以看一下redux官方文档了解一下redux。StackNavigator本身就集成了Redux来进行路由数据的管理,如你想要将你自己的redux管理集成到StackNavigator中,官方同样提供接口addNavigationHelpers,这里我们关注的是如何把reset,setParams等Navigator中的Action直接封装到组件中形成页面调用接口。
以下是笔者的封装组件,类似之前封装Navigator组件封装集中管理组件的思路代码,我们把StackNavigator同样封装为一个组件作为管理中心
......
const AppNavigator = StackNavigator(RouteConfigs, stackNavigatorConfig);// eslint-disable-line
class MainContainer extends Component {
constructor(props) {
super(props);
this.resetRouteTo = this.resetRouteTo.bind(this);
this.resetActiveRouteTo = this.resetActiveRouteTo.bind(this);
this.backTo = this.backTo.bind(this);
this.setParamsWrapper = this.setParamsWrapper.bind(this);
this.state = {};
}
resetRouteTo(route, params) {
const { dispatch } = this.props;
if (dispatch) {
dispatch(
NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: route, params: params })],
})
);
}
}
resetActiveRouteTo(routeArray, activeIndex) {
const { dispatch } = this.props;
if (dispatch) {
const actionsArray = [];
for (let i = 0; i < routeArray.length; i++) {
actionsArray.push(NavigationActions.navigate({ routeName: routeArray[i] }));
}
const resetAction = NavigationActions.reset({
index: activeIndex,
actions: actionsArray,
});
dispatch(resetAction);
}
}
backTo(key) {
const { dispatch } = this.props;
if (dispatch) {
dispatch(
NavigationActions.reset({
key: key
})
);
}
}
setParamsWrapper(params, key) {
const { dispatch } = this.props;
if (dispatch) {
const setParamsAction = NavigationActions.setParams({
params: params,
key: key,
});
dispatch(setParamsAction);
}
}
render() {
const { dispatch, navigationState, screenProps } = this.props;
return (
<View
style={{ flex: 1 }}
onStartShouldSetResponder={() => dismissKeyboard()}
>
<StatusBar barStyle="light-content" />
<AppNavigator
navigation={addNavigationHelpers({
dispatch: dispatch,
state: navigationState,
resetRouteTo: (route, params) => this.resetRouteTo(route, params),
resetActiveRouteTo: (routeArray, activeIndex) => this.resetActiveRouteTo(routeArray, activeIndex),
backTo: (key) => this.backTo(key),
setParamsWrapper: (params, key) => this.setParamsWrapper(params, key)
})}
screenProps={screenProps}
/>
<Loading isVisible={true} mode="alipay" />
</View>
);
}
}
const mapStateToProps = (state) => {
const newNavigationState = state.navReducer;
if (state.screenProps) {
newNavigationState.params = {
...state.params,
...state.screenProps
};
}
return {
navigationState: newNavigationState,
screenProps: state.screenProps
};
};
export default connect(mapStateToProps)(MainContainer);
......
其中绑定navReducer文件的数据,可参考redux和react-navigation官网文档,此文不再列出
这样封装后,各页面使用reset,setParams等操作时,就可以像以前一样直接使用相关操作,如重置路由:
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
navigation.resetRouteTo('TabBar', { title: '首页', selectedTab: 'home' });
}
}
状态分析
前几天刚好看到一篇文章前端状态管理请三思,觉得挺有意思的,原文作者利用状态机的思想,预先设想好所有状态和状态的迁移,优雅的管理页面登录状态避免过多变量的使用。本文参考作者的思想和代码,实现一个简单的登录页面。状态分析如下:
初始登录页面是展示登录的表单(login form)
当提交(submit)数据过程后,页面变为等待数据响应状态(loading)
数据响应有两种状态,成功(success)页面跳转到首页;失败(failure)页面提示错误
当登录成功,只有先退出登录(logout)之后才能重新登录
当登录失败,重新提交(submit)回到加载状态(loading)
logout之后回到login form状态
依旧是模仿掘金app登录页面的一个实现效果:
定义状态机
const machine = {
states: {
'login form': {
submit: 'loading'
},
loading: {
success: 'profile',
failure: 'error'
},
profile: {
viewProfile: 'profile',
logout: 'login form'
},
error: {
submit: 'loading'
}
}
}
复制代码实现一个状态控制函数,返回下一个状态
const stateTransformer = function(currentState, stepUp) {
let nextState
if (machine.states[currentState][stepUp]) {
nextState = machine.states[currentState][stepUp]
}
console.log(`${currentState} + ${stepUp} --> ${nextState}`)
return nextState || currentState
}
复制代码我们把状态控制的变量存储在redux中,定义一个简单的auth模块如下,stateChanger纯函数用于控制currentState的状态迁移,每次操作结果返回进行状态变换
export default {
namespace: 'auth',
state: {
currentState: 'login form'
},
reducers: {
stateChanger(state, {stepUp}) {
return {
...state,
currentState: stateTransformer(state.currentState, stepUp)
}
}
},
effects: dispatch => ({
async loginByPhoneNumber(playload, state) {
dispatch.auth.stateChanger({stepUp: 'submit'})
let {data} = await api.auth.loginByPhoneNumber(playload)
if (data.s === 0) {
dispatch.auth.stateChanger({stepUp: 'success'})
saveData('juejin_token', data.token)
} else {
dispatch.auth.stateChanger({stepUp: 'failure'})
Toast.info('用户名或密码错误', 2)
}
}
})
}
复制代码那么在组件中,我们很容易写一个控制状态变化的组件
render() {
let {currentState} = this.props
return (
<>
{(() => {
switch (currentState) {
case 'loading':
return (
//加载中展示组件
)
case 'profile':
return <Redirect to={'/'} />//返回首页
default:
return (
//登录表单
)
}
})()}
</>
)
}
复制代码具体配置补充
为了配合项目的用户登录验证,我们重新搭建一个本地服务,在react配置路由的代理转发,具体地,在根目录下新建文件src/setupProxy.js,将/api开头请求转发到服务器
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(proxy('/api', {target: 'http://localhost:8989/', changeOrigin: true}))
}
复制代码services/api定义数据接口
export async function loginByPhoneNumber({phoneNumber, password}) {
return post('/api/auth/type/phoneNumber', {
body: {
phoneNumber,
password
}
})
}
复制代码后端实现一个简单的中间件路由
const Koa = require('koa')
const router = require('./router')
router.post('/auth/type/phoneNumber', async (ctx, next) => {
var {phoneNumber, password} = await parse.json(ctx.req)
if (phoneNumber === '15111111111' && password === '123456') {
let token = generateToken({uid: phoneNumber, password})
ctx.response.body = JSON.stringify({
s: 0,
m: `账号登录成功错误`,
d: '',
token
})
} else {
ctx.response.body = JSON.stringify({s: 1, m: '账号信息错误', d: ''})
}
})