RN從0到1系統(tǒng)精講與小紅書APP實(shí)戰(zhàn)(2023版)萬山春色歸
ReactNative介紹
RN從0到1系統(tǒng)精講與小紅書APP實(shí)戰(zhàn)
下栽の地止:https://lexuecode.com/6659.html
React Native 是一個(gè)由 Facebook 于 2015 年 9 月發(fā)布的一款開源的 JavaScript 框架,它可以讓開發(fā)者使用 JavaScript 和 React 來開發(fā)跨平臺的移動應(yīng)用。它既保留了 React 的開發(fā)效率,又同時(shí)擁有 Native 應(yīng)用的良好體驗(yàn),加上 Virtual DOM 跨平臺的優(yōu)勢,實(shí)現(xiàn)了真正意義上的:Learn Once,Write Anywhere.
注:非高清 logo,這不是原子結(jié)構(gòu)模型嗎?暗示 React (Native)是萬惡之源?
React Native 的特點(diǎn)
跨平臺
React Native 使用了 Virtual DOM(虛擬 DOM),只需編寫一套代碼,便可以將代碼打包成不同平臺的 App,極大提高了開發(fā)效率,并且相對全部原生開發(fā)的應(yīng)用來說,維護(hù)成本也相對更低。
上手快
相比于原生開發(fā),JavaScript 學(xué)習(xí)成本低、語法靈活。允許讓 Web 開發(fā)者更多地基于現(xiàn)有經(jīng)驗(yàn)開發(fā) App。React Native 只需使用 JavaScript 就能編寫移動原生應(yīng)用,它和 React 的設(shè)計(jì)理念是一樣的,因此可以毫不夸張地說:你如果會寫 React,就會寫 React Native!
原生體驗(yàn)
由于 React Native 提供的組件是對原生 API 的暴露,雖然我們使用的是 JavaScript 語言編寫的代碼,但是實(shí)際上是調(diào)用了原生的 API 和原生的 UI 組件。因此,體驗(yàn)和性能足以媲美原生應(yīng)用。
熱更新
React Native 開發(fā)的應(yīng)用支持熱更新,因?yàn)?React Native 的產(chǎn)物是 bundle 文件,其實(shí)本質(zhì)上就是 JS 代碼,在 App 啟動的時(shí)候就會去服務(wù)器上獲取 bundle 文件,我們只需要更新 bundle 文件,從而使得 App 不需要重新前往商店下載包體就可以進(jìn)行版本更新,開發(fā)者可以在用戶無感知的情況下進(jìn)行功能迭代或者 bug 修復(fù)。但是值得注意的是,AppStore 禁止熱更新的功能中有調(diào)用私有 API、篡改原生代碼和改變 App 的行為。
React Native 原理
JavaScriptCore
JavaScriptCore 是 JavaScript 引擎,通常會被叫做虛擬機(jī),專門設(shè)計(jì)來解釋和執(zhí)行 JavaScript 代碼。在 React Native 里面,JavaScriptCore 負(fù)責(zé) bundle 產(chǎn)出的 JS 代碼的解析和執(zhí)行。
JS Engine
React Native 需要一個(gè) JS 的運(yùn)行環(huán)境,因?yàn)?React Native 會把應(yīng)用的 JS 代碼編譯成一個(gè) JS 文件(x x.bundle),React Native 框架的目標(biāo)就是解釋運(yùn)行這個(gè) JS 腳本文件,如果是 Native 拓展的 API,則直接通過 bridge 調(diào)用 Native 方法,最基礎(chǔ)的比如繪制 UI 界面,映射 Virtual DOM 到真實(shí)的 UI 組件中。
綠色的是我們應(yīng)用開發(fā)的部分,我們寫的代碼基本上都是在這一層。
藍(lán)色代表公用的跨平臺的代碼和工具引擎,一般我們不會動藍(lán)色部分的代碼。
黃色代表平臺相關(guān)的 bridge 代碼,做定制化的時(shí)候會添加修改代碼。
紅色代表系統(tǒng)平臺的功能,另外紅色上面有一個(gè)虛線,表示所有平臺相關(guān)的東西都通過 bridge 隔離開來了,紅色部分是獨(dú)立于 React Native 的。
脫離 React Native,純原生端是如何與 JS 交互的?來看下 iOS 里面是如何實(shí)現(xiàn)的。
在 Native 創(chuàng)建一個(gè) JS 上下文:
// 創(chuàng)建一個(gè)ctx的JS上下文 JSContent *ctx = [[JSContent alloc] init]; // 創(chuàng)建一個(gè)變量name [ctx evaluateScript:@"var name = 'Hellen'"]; // 創(chuàng)建一個(gè)方法 [ctx evaluateScript:@"var hello = function(name) { return 'hello ' + name }"];
Native 調(diào)用 JavaScript 方法:
// 通過ctx上下文對象,獲取到hello方法 JSValue *helloFUnction = ctx[@"hello"]; // 運(yùn)行js方法 JSValue *greetings = [helloFunction callWithArguments:@[@"bytedancers"]; // hello bytedancers
所以,JavaScript 代碼只要將變量暴露在 JS 上下文全局,Native 就能獲取到,并運(yùn)行 JS 的代碼。
JavaScript 調(diào)用 Native,首先需要在 Native 端,將一個(gè)變量暴露在 JS 上下文全局,在 JavaScript 全局變量里面就能獲取到并執(zhí)行這個(gè)方法:
ctx[@"createdByNative"] = ^(NSString *name) {
// do something
return someResult
}
RN從0到1系統(tǒng)精講與實(shí)戰(zhàn)小紅書APP - 實(shí)戰(zhàn)
Navigator使用和封裝
點(diǎn)擊查看官方文檔
0.44版本后Navigator已經(jīng)從react-native庫中移除,如需導(dǎo)入可按如下操作:
// install
$npm install React-native-deprecated-custom-components --save
// import API
import CustomerComponents, {Navigator} from 'react-native-deprecated-custom-components';
實(shí)際項(xiàng)目中對于單頁面應(yīng)用,我們可以把Navigator封裝成一個(gè)組件,把各頁面當(dāng)作Navigator的一個(gè)個(gè)場景轉(zhuǎn)換,在頁面中實(shí)現(xiàn)跳轉(zhuǎn),返回,動畫等的各種操作時(shí)只需要調(diào)用相應(yīng)方法即可。
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>
)
}
}
除了場景轉(zhuǎn)換等操作,還可以在這個(gè)組件中集成控制App全局的一些操作,比如說,Loading的設(shè)置,網(wǎng)絡(luò)狀態(tài)檢查等設(shè)置,在各頁面就無須再單獨(dú)設(shè)置。盡量在一個(gè)地方里面實(shí)現(xiàn)控制app的一些相近的默認(rèn)操作
實(shí)際頁面中跳轉(zhuǎn)或其他操作:
_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理解和使用
點(diǎn)擊查看官方文檔
react-native 0.44版本之前路由控制使用的Navigator雖然非常穩(wěn)定,基本沒出現(xiàn)過什么BUG,但是跳轉(zhuǎn)效果一直被人詬病,跳轉(zhuǎn)時(shí)候的動畫和原生App的效果相比,非常明顯差一等,在0.44版本后Facebook推薦使用react-navigation庫來實(shí)現(xiàn)頁面跳轉(zhuǎn),tab轉(zhuǎn)換,側(cè)邊欄滑動等功能。
react-navigation主要包括導(dǎo)航,底部tab,頂部tab
,側(cè)滑等,功能很強(qiáng)大,而且體驗(yàn)接近原生。接下來會一一介紹:
導(dǎo)航 -> StackNavigator
底部或者頂部tab -> TabNavigator
關(guān)于側(cè)滑DrawerNavigator的使用,筆者不在本文介紹,但可以看這篇附帶Demo的推薦博客
StackNavigator
StackNavigator在功能上就是相當(dāng)于原來使用Navigator,但是他有著不一樣的實(shí)現(xiàn)和非常好的跳轉(zhuǎn)體驗(yàn),使用上也非常簡單,其實(shí)也就是三部曲:
路由配置(頁面注冊):
const routeConfigs = {
Login: { screen: Login },
TabBar: { screen: TabBarContainer },
Feedback: { screen: Feedback },
};
默認(rèn)場景配置:
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/>
);
}
}
這樣就簡單完成了路由的配置,開發(fā)時(shí)只需要把新頁面添加到注冊對象routeConfigs中,StackNavigator會對里面的的注冊頁面和注冊時(shí)使用的KEY值形成對應(yīng)關(guān)系,當(dāng)你在頁面時(shí)跳轉(zhuǎn)時(shí),只需要這樣:
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
const { navigation } = this.props;
navigation.navigate('TabBar');
}
}
帶參數(shù)跳轉(zhuǎn)時(shí):
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
const { navigation } = this.props;
navigation.navigate('TabBar', {
visible: false,
title: '首頁'
});
}
}
在下個(gè)頁面就可以拿到參數(shù)并設(shè)置頭部或其他參數(shù):
static navigationOptions = ({ navigation }) => {
const { state } = navigation;
const { title } = state.params;
return {
title: title,
};
};
其他reset,setParams等操作將可以學(xué)著本文后面封裝到組件中去使用,當(dāng)然你也可以直接在頁面跳轉(zhuǎn)函數(shù)中重置路由,就像這樣:
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Login'})
]
})
this.props.navigation.dispatch(resetAction)
TabNavigator
0.44版本之前我們實(shí)現(xiàn)Tab頁面通常都選擇使用框架react-native-tab-navigator或者react-native-scrollable-tab-view,現(xiàn)在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: '聯(lián)系人',
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);
關(guān)于使用TabNavigator的一些注意點(diǎn)和當(dāng)前問題:
如你甚至未使用StackNavigator,而想直接使用TabNavigator,還是用其他第三方框架吧,他和StackNavigator是配套使用的,你必須保證TabNavigator存在于StackNavigator中,TabNavigator才能良好工作。
當(dāng)你當(dāng)前頁面使用了TabNavigator,那么TabNavigator所形成的容器組件應(yīng)該是當(dāng)前頁面的頂層組件,否則報(bào)錯(cuò),將會無法獲取到tab中的router數(shù)組。
關(guān)于嵌套使用TabNavigator,即在TabNavigator的一個(gè)screen中再次使用了TabNavigator形成頁面,安卓平臺下無法渲染子組件,頁面空白,且內(nèi)層Tab基本失效,或者你的內(nèi)層Tab容器使用其他第三方框架如react-native-tab-view等類似框架,問題依然存在,關(guān)于此問題可關(guān)注公關(guān)BUG#1796。
StackNavigator路由的集中封裝
此部分集成了一部分Redux知識,建議可以看一下redux官方文檔了解一下redux。StackNavigator本身就集成了Redux來進(jìn)行路由數(shù)據(jù)的管理,如你想要將你自己的redux管理集成到StackNavigator中,官方同樣提供接口addNavigationHelpers,這里我們關(guān)注的是如何把reset,setParams等Navigator中的Action直接封裝到組件中形成頁面調(diào)用接口。
以下是筆者的封裝組件,類似之前封裝Navigator組件封裝集中管理組件的思路代碼,我們把StackNavigator同樣封裝為一個(gè)組件作為管理中心
......
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文件的數(shù)據(jù),可參考redux和react-navigation官網(wǎng)文檔,此文不再列出
這樣封裝后,各頁面使用reset,setParams等操作時(shí),就可以像以前一樣直接使用相關(guān)操作,如重置路由:
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
navigation.resetRouteTo('TabBar', { title: '首頁', selectedTab: 'home' });
}
}
狀態(tài)分析
前幾天剛好看到一篇文章前端狀態(tài)管理請三思,覺得挺有意思的,原文作者利用狀態(tài)機(jī)的思想,預(yù)先設(shè)想好所有狀態(tài)和狀態(tài)的遷移,優(yōu)雅的管理頁面登錄狀態(tài)避免過多變量的使用。本文參考作者的思想和代碼,實(shí)現(xiàn)一個(gè)簡單的登錄頁面。狀態(tài)分析如下:
初始登錄頁面是展示登錄的表單(login form)
當(dāng)提交(submit)數(shù)據(jù)過程后,頁面變?yōu)榈却龜?shù)據(jù)響應(yīng)狀態(tài)(loading)
數(shù)據(jù)響應(yīng)有兩種狀態(tài),成功(success)頁面跳轉(zhuǎn)到首頁;失?。╢ailure)頁面提示錯(cuò)誤
當(dāng)?shù)卿洺晒Γ挥邢韧顺龅卿洠╨ogout)之后才能重新登錄
當(dāng)?shù)卿浭。匦绿峤唬╯ubmit)回到加載狀態(tài)(loading)
logout之后回到login form狀態(tài)
依舊是模仿掘金app登錄頁面的一個(gè)實(shí)現(xiàn)效果:
定義狀態(tài)機(jī)
const machine = {
states: {
'login form': {
submit: 'loading'
},
loading: {
success: 'profile',
failure: 'error'
},
profile: {
viewProfile: 'profile',
logout: 'login form'
},
error: {
submit: 'loading'
}
}
}
復(fù)制代碼實(shí)現(xiàn)一個(gè)狀態(tài)控制函數(shù),返回下一個(gè)狀態(tài)
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
}
復(fù)制代碼我們把狀態(tài)控制的變量存儲在redux中,定義一個(gè)簡單的auth模塊如下,stateChanger純函數(shù)用于控制currentState的狀態(tài)遷移,每次操作結(jié)果返回進(jìn)行狀態(tài)變換
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('用戶名或密碼錯(cuò)誤', 2)
}
}
})
}
復(fù)制代碼那么在組件中,我們很容易寫一個(gè)控制狀態(tài)變化的組件
render() {
let {currentState} = this.props
return (
<>
{(() => {
switch (currentState) {
case 'loading':
return (
//加載中展示組件
)
case 'profile':
return <Redirect to={'/'} />//返回首頁
default:
return (
//登錄表單
)
}
})()}
</>
)
}
復(fù)制代碼具體配置補(bǔ)充
為了配合項(xiàng)目的用戶登錄驗(yàn)證,我們重新搭建一個(gè)本地服務(wù),在react配置路由的代理轉(zhuǎn)發(fā),具體地,在根目錄下新建文件src/setupProxy.js,將/api開頭請求轉(zhuǎn)發(fā)到服務(wù)器
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(proxy('/api', {target: 'http://localhost:8989/', changeOrigin: true}))
}
復(fù)制代碼services/api定義數(shù)據(jù)接口
export async function loginByPhoneNumber({phoneNumber, password}) {
return post('/api/auth/type/phoneNumber', {
body: {
phoneNumber,
password
}
})
}
復(fù)制代碼后端實(shí)現(xiàn)一個(gè)簡單的中間件路由
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: `賬號登錄成功錯(cuò)誤`,
d: '',
token
})
} else {
ctx.response.body = JSON.stringify({s: 1, m: '賬號信息錯(cuò)誤', d: ''})
}
})