react-native從項目搭建到發佈上線項目實戰(基於expo)

文檔導航

項目結構

|-- 項目結構
    |-- .gitignore
    |-- app.json
    |-- App.tsx
    |-- babel.config.js
    |-- images.d.ts
    |-- package-lock.json
    |-- package.json
    |-- tsconfig.json
    |-- yarn.lock
    |-- .expo-shared
    |   |-- assets.json
    |-- assets
    |   |-- icon.png
    |   |-- splash.png
    |   |-- images
    |       |-- cookbook-active.png
    |       |-- cookbook.png
    |       |-- location-active.png
    |       |-- location.png
    |       |-- menu-active.png
    |       |-- menu.png
    |       |-- more-active.png
    |       |-- more.png
    |       |-- search.png
    |       |-- swiper-1.png
    |       |-- swiper-2.jpeg
    |       |-- swiper-3.jpeg
    |-- context
    |   |-- navigation.js
    |-- mock
    |   |-- cookbook-category.json
    |   |-- cookbook-detail.json
    |   |-- cookbook-hotcate.json
    |   |-- cookbook-list-json.json
    |   |-- cookbook-list.json
    |   |-- mock.js
    |   |-- route.json
    |-- pages
    |   |-- cate
    |   |   |-- Cate.tsx
    |   |   |-- style_cate.js
    |   |-- detail
    |   |   |-- Detail.tsx
    |   |   |-- style_detail.js
    |   |-- home
    |   |   |-- Home.tsx
    |   |   |-- HotCate.tsx
    |   |   |-- style_home.js
    |   |   |-- Swiper.tsx
    |   |   |-- Top10.tsx
    |   |-- index
    |   |   |-- Index.tsx
    |   |   |-- styled_index.js
    |   |   |-- style_index.js
    |   |-- map
    |   |   |-- Map.tsx
    |   |-- more
    |       |-- More.tsx
    |-- store
    |   |-- index.ts
    |-- utils
        |-- http.js

環境搭建

本項目是應用 ReactNativeTypeScriptMobx等技術開發的一個“美食大全”的項目,基本的環境搭建,大家參照本文基礎部分。

expo init rn-cookbooks

然後選擇 blank (TypeScript):

? Choose a template: 
  ----- Managed workflow -----
  blank                 a minimal app as clean as an empty canvas 
❯ blank (TypeScript)    same as blank but with TypeScript configuration 
  tabs                  several example screens and tabs using react-navigation 
  ----- Bare workflow -----
  minimal               bare and minimal, just the essentials to get you started 
  minimal (TypeScript)  same as minimal but with TypeScript configuration

啓動項目:

cd rn-cookbooks
yarn start

Index組件初始化

在根目錄下創建 pages/index 文件夾,在裏面創建一個 Index.tsx 文件,編輯內容:

// pages/index/Index.tsx
import React, { Component } from 'react'

import {
  View,
  Text,
  StyleSheet
} from 'react-native'

interface Props {
  
}

interface State {
  
}

export default class Index extends Component<Props, State> {
  constructor(props) {
    super(props)
  }
  
  state: State = {
    
  }

  componentDidMount() {
    
  }

  render() {
    return (
      <View style={styles.container}>
        <Text>
          Index 組件內容
        </Text>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
})

修改根目錄下的 App.tsx

import React from 'react'
import Index from './pages/index/Index'

export default function App() {
  return (
    <Index></Index>
  )
}

引入tabbar導航器

在項目環境命令行裏安裝 tabbar 導航器,詳細內容可參見 react-native-tab-navigator 官網

yarn add react-native-tab-navigator -S

修改 index.tsx, 引入 tab-navigator 代碼:

import React, { Component } from 'react'
import TabNavigator from 'react-native-tab-navigator'

import {
  View,
  Text
} from 'react-native'

import {
  Img
} from './styled_index'
import styles from './style_index'

import cookbook from '../../assets/images/cookbook.png'
import cookbookActive from '../../assets/images/cookbook-active.png'
import category from '../../assets/images/menu.png'
import categoryActive from '../../assets/images/menu-active.png'
import map from '../../assets/images/location.png'
import mapActive from '../../assets/images/location-active.png'
import more from '../../assets/images/more.png'
import moreActive from '../../assets/images/more-active.png'

interface Props {

}

interface State {
  selectedTab: string
}

class Index extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
  }

  state: State = {
    selectedTab: 'home'
  }

  componentDidMount() {

  }

  render() {
    return (
      <TabNavigator
        tabBarStyle={styles.tabBarStyle}
      >
        <TabNavigator.Item
          selected={this.state.selectedTab === 'home'}
          title="美食大全"
          titleStyle={styles.titleStyle}
          selectedTitleStyle={styles.selectedTitleStyle}
          renderIcon={() => <Img source={cookbook} />}
          renderSelectedIcon={() => <Img source={cookbookActive} />}
          onPress={() => this.setState({ selectedTab: 'home' })}
        >
          {<View><Text>美食大全</Text></View>}
        </TabNavigator.Item>
        <TabNavigator.Item
          selected={this.state.selectedTab === 'category'}
          title="分類"
          titleStyle={styles.titleStyle}
          selectedTitleStyle={styles.selectedTitleStyle}
          renderIcon={() => <Img source={category} />}
          renderSelectedIcon={() => <Img source={categoryActive} />}
          onPress={() => this.setState({ selectedTab: 'category' })}
        >
          {<View><Text>分類</Text></View>}
        </TabNavigator.Item>
        <TabNavigator.Item
          selected={this.state.selectedTab === 'map'}
          title="地圖"
          titleStyle={styles.titleStyle}
          selectedTitleStyle={styles.selectedTitleStyle}
          renderIcon={() => <Img source={map} />}
          renderSelectedIcon={() => <Img source={mapActive} />}
          onPress={() => this.setState({ selectedTab: 'map' })}
        >
          {<View><Text>地圖</Text></View>}
        </TabNavigator.Item>
        <TabNavigator.Item
          selected={this.state.selectedTab === 'more'}
          title="更多"
          titleStyle={styles.titleStyle}
          selectedTitleStyle={styles.selectedTitleStyle}
          renderIcon={() => <Img source={more} />}
          renderSelectedIcon={() => <Img source={moreActive} />}
          onPress={() => this.setState({ selectedTab: 'more' })}
        >
          {<View><Text>更多</Text></View>}
        </TabNavigator.Item>
      </TabNavigator>
    )
  }
}

export default Index

問題:

  • ts 提示引入的 png 不能識別,飄紅了。解決方案是在項目跟目錄下創建 images.d.ts 文件,內容如下:
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'

pages/index 下創建樣式文件:

下面使用兩種形式書寫樣式表,到時候自行開發按需選擇

  • 安裝 styled-components 模塊
npm i styled-components -D
  • 創建 styled_index.js, 內容如下:
import styled from 'styled-components'

const Img = styled.Image `
  width: 25px;
  height: 25px;
`

export {
  Img
}

再創建一個樣式文件 style_index.js, 內容如下:

import { StyleSheet } from 'react-native'

export default StyleSheet.create({
  titleStyle: {
    color: '#666'
  },
  
  tabBarStyle: {
    paddingBottom: 34, 
    height: 80
  },

  selectedTitleStyle: {
    color: '#000'
  }
})

tabbar 的兼容處理

安裝 expo-device

npm i expo-device -S

修改 index.ts, 根據您設備情況引入不同的樣式,此處只是測試性代碼,只做了iphone XR 和 其他非 “齊劉海” iPhone 手機:

// 載入模塊
import * as Device from 'expo-device'

// 在 TabNavigator 上修改 tabBarStyle
<TabNavigator
  tabBarStyle={
    Device.deviceName === 'iPhone Xʀ' ? styles.tabBarStyle : null
  }
>

搭建antd-mobile-rn環境

在開始之前,推薦先學習 ReactES2015。使用了 babel,試試用 ES2015 的寫法來提升編碼的愉悅感。

確認 Node.js 已經升級到 v4.x 或以上。

1. 創建一個項目

可以是已有項目,或者是使用 create-react-native-app 新創建的空項目,你也可以從 官方示例 腳手架裏拷貝並修改

參考更多官方示例集
或者你可以利用 React 生態圈中的 各種腳手架

完整步驟請查看此處文檔: antd-mobile-sample/create-react-native-app

2. 安裝

npm install @ant-design/react-native --save

or

yarn add @ant-design/react-native

鏈接字體圖標

react-native link @ant-design/icons-react-native

3. 使用

按需加載

下面兩種方式都可以只加載用到的組件,選擇其中一種方式即可。

  • 使用 babel-plugin-import(推薦)。

    // .babelrc or babel-loader option
    {
      "plugins": [
        ["import", { libraryName: "@ant-design/react-native" }] // 與 Web 平臺的區別是不需要設置 style
      ]
    }
    

    然後改變從 @ant-design/react-native 引入模塊方式即可。

    import { Button } from '@ant-design/react-native';
    

創建Home組件

在項目根目錄下創建 pages/home 文件夾,在這個文件夾下創建 Home.tsx 文件,內容如下:

import React, { Component } from 'react'
import Swiper from './Swiper'
import HotCate from './HotCate'

interface Props {

}

interface State {
  
}

class Home extends Component<Props, State> {
  render() {
    return (
      <>
        <Swiper></Swiper>
        <HotCate></HotCate>
      </>
    )
  }
}

export default Home

此時在 Home.tsx 中引入 Swiper 和 HotCate 兩個組價。

創建Swiper組件

在根目錄下創建 utils 文件夾,在這個文件夾裏創建 http.js 文件,內容如下:

// utils/http.js
export const get = (url) => {
  return fetch(url, {
    method: 'get'
  })
  .then(response => response.json())
  .then(result => {
    return result.data
  })
}

在 pages/home 文件夾裏再創建一個 Swiper.tsx 組件,內容如下:

import React, { Component } from 'react'
import { Carousel } from '@ant-design/react-native'
import { get } from '../../utils/http'

import {
  View,
  Image
} from 'react-native'

import styles from './style_home'

interface Props {

}

interface State {
  list: Array<any>
}

class Swiper extends Component<Props, State> {
  state = {
    list: []
  }
  async componentDidMount() {
    let list = await get('http://localhost:9000/api/swiper')
    this.setState({
      list
    })
  }

  render() {
    return (
      <Carousel
        style={styles.wrapper}
        selectedIndex={0}
        autoplay
        infinite
      >
        {
          this.state.list.slice(0, 5).map((value, index) => {
            return (
              <View
                style={styles.containerHorizontal}
                key={value.id}
              >
                <Image
                  source={{uri: value.img}}
                  style={styles.slideImg}
                  resizeMode='cover'
                ></Image>
              </View>
            )
          })
        }
      </Carousel>
    )
  }
}

export default Swiper

在 page/home 文件裏創建 style_home.js 文件,編輯樣式如下:

import { StyleSheet } from 'react-native'

export default StyleSheet.create({
  // swiper
  wrapper: {
    height: 170
  },

  containerHorizontal: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    height: 170
  },

  slideImg: {
    height: 170,
    width: '100%'
  },
})

創建HotCate組件

pages/home 文件夾裏構建 HotCate.tsx 文件,內容爲:

import React, { Component } from 'react'
import { Grid } from '@ant-design/react-native'
import { get } from '../../utils/http'

import styles from './style_home'

import {
  View,
  Text,
  Image,
  StyleSheet
} from 'react-native'

interface Props {
  
}
interface State {
  hotCate: Array<object>
}

export default class HotCate extends Component<Props, State> {
  state = {
    hotCate: []
  }

  _renderItem(el, index) {
    return (
      <View
        style={styles.container}
      >
        {el.img ? <Image source={{uri: el.img}} style={styles.gridImg}></Image> : null}
        <Text style={styles.gridText}>{el.title}</Text>
      </View>
    )
  }

  async componentDidMount() {
    let hotCate = await get('http://localhost:9000/api/hotcate')

    // 補全最後一項數據
    hotCate.push({
      img: '',
      title: '更多...'
    })

    this.setState({
      hotCate
    })
  }

  render() {
    return (
      <View>
        <Grid
          data={this.state.hotCate}
          renderItem={this._renderItem}
          hasLine={false}
        ></Grid>
      </View>
    )
  }
}

修改 pages/home/style_home.js 文件,樣式如下:

import { StyleSheet } from 'react-native'

export default StyleSheet.create({
  // hotcate
  container: {
    paddingTop: 20,
    paddingBottom: 10
  },

  gridContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },

  gridText: {
    fontSize: 16,
    margin: 6
  },

  gridImg: {
    width: 70,
    height: 70,
    borderRadius: 5
  },
})

創建Top10組件

Top10組件渲染的數據和Swiper組件可以使用同一個接口的數據,因此我們決定應用Mobx來管理這個數據。

安裝 Mobx 相關模塊

npm i mobx mobx-react -S

構建 store

在項目根目錄下創建 store 文件夾,在這個文件下創建 index.js 文件:

// store/index.js
import {
  observable,
  action,
  computed
} from 'mobx'

class Store {
  // swiper 與 top10 共享的數據
  @observable
  list = []

  // swiper 數據過濾
  @computed
  get swiper() {
    return this.list.slice(0, 5).map((value, index) => {
      return {
        img: value.img
      }
    })
  }

  // top10 數據過濾
  @computed
  get top10() {
    return this.list.slice(0, 10).map((value, index) => {
      return {
        img: value.img,
        all_click: value.all_click,
        favorites: value.favorites,
        name: value.name
      }
    })
  }

  // 裝載 list 數據
  @action.bound
  setList(data) {
    this.list = data
  }  
}

export default new Store()

開始構建 Top.tsx 組件

pages/home 下創建 Top.tsx 文件:

// pages/home/Top.tsx
import React, { Component } from 'react'
import { Grid } from '@ant-design/react-native'
import { observer, inject } from 'mobx-react'

import {
  View,
  Text,
  Image
} from 'react-native'

import styles from './style_home.js'

interface Props {
  // store 作爲組件的 props
  store?: any
}

interface State {
  
}

// 注入 store 與 將類變爲可觀察的對象
@inject('store')
@observer
class Top10 extends Component<Props, State> {

  renderTop10(el, index) {
    return (
      <View style={styles.top10ItemContainer}>
        <View style={styles.top10ImgContainer}>
          <Image style={styles.Top10Img} source={{uri: el.img}}></Image>
        </View>
        <View style={styles.top10DesContainter}>
          <Text style={styles.top10Titie}>{el.name}</Text>
          <Text style={styles.Top10Desc}>{el.all_click} {el.favorites}</Text>
        </View>
      </View>
    )
  }

  render() {   
    return (
      <View style={styles.top10Container}>
        <View style={styles.top10Head}>
          <Text style={styles.top10HeadText}>精品好菜</Text>
        </View>
        <View style={styles.gridContainer}>
          <Grid
            data={this.props.store.top10}
            columnNum={2}
            hasLine={false}
            renderItem={this.renderTop10}
          />
        </View>
      </View>
    )
  }
}

export default Top10

💣注意:expo-cli 構建的項目,默認 ts 配置不支持裝飾器,會給出如下警告:

Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.

需要修改項目根目錄下的 tsconfig.json,添加:

"experimentalDecorators": true

如果不能起作用,重新啓動VSCode即可。

添加 top10 樣式

// pages/home/style_home.js
import { StyleSheet } from 'react-native'
export default StyleSheet.create({
  // top10
  top10Container: {
    paddingBottom: 44,
    backgroundColor: '#eee'
  },

  top10Head: {
    height: 50,
    paddingLeft: 10,
    justifyContent: 'flex-end',
  },

  top10HeadText: {
    fontSize: 18
  },

  top10ItemContainer: {
    flex: 1,
    paddingRight: 10
  },

  top10DesContainter: {
    marginLeft: 10,
    paddingTop: 10,
    paddingBottom: 10,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#fff'
  },

  top10ImgContainer: {
    paddingLeft: 10,
    paddingTop: 10,
    flex: 1
  },

  Top10Img: {
    width: '100%',
    height: '100%',
  },

  top10Titie: {
    fontSize: 20
  },

  Top10Desc: {
    color: '#666'
  }
})

更改Swiper和Home組件

Swiper 組件和 Top10 組件共享了數據,因此在 store 構建好後,需要改造一下:

// pages/home/Swiper.tsx
import React, { Component } from 'react'
import { Carousel } from '@ant-design/react-native'
import { get } from '../../utils/http'

import { observer, inject } from 'mobx-react'

import {
  View,
  Image
} from 'react-native'

import styles from './style_home'

interface Props {
  // store 作爲組件的 props
  store?: any
}

interface State {
  
}

// 注入 store 與 將類變爲可觀察的對象
@inject('store')
@observer
class Swiper extends Component<Props, State> {
  state = {
    list: []
  }
  async componentDidMount() {
    let list = await get('http://localhost:9000/api/swiper')
    this.props.store.setList(list)
  }

  render() {
    return (
      <Carousel
        style={styles.wrapper}
        selectedIndex={0}
        autoplay
        infinite
      >
        {
          this.props.store.swiper.map((value, index) => {
            return (
              <View
                style={styles.containerHorizontal}
                key={value.id}
              >
                <Image
                  source={{uri: value.img}}
                  style={styles.slideImg}
                  resizeMode='cover'
                ></Image>
              </View>
            )
          })
        }
      </Carousel>
    )
  }
}

export default Swiper

改造 Home.tsx 組件

在 Home.tsx 組件引入 Top10 組件,同時添加 ScrollView 組件,實現頁面滾動效果。

// page/home/Home.tsx

import React, { Component } from 'react'
import { ScrollView } from 'react-native'

import Swiper from './Swiper'
import HotCate from './HotCate'
import Top10 from './Top10'

interface Props {

}

interface State {
  
}

class Home extends Component<Props, State> {
  render() {
    return (
      <ScrollView>
        <Swiper></Swiper>
        <HotCate></HotCate>
        <Top10></Top10>
      </ScrollView>
    )
  }
}

export default Home

創建List組件

接下來構建另一個頁面,首先在 pages 目錄下創建 list 文件夾,在此文件夾裏創建 List.tsx 組件文件和 style_list.js 樣式文件。

List.tsx

// pages/list/List
import React, { Component, createRef } from 'react'

import {
  inject,
  observer
} from 'mobx-react'

import {
  View,
  Text,
  Image,
  FlatList
} from 'react-native'

import styles from './style_list'

interface Props {
  store?: any
}

interface State {
  // 記錄上拉加載更多的當前頁碼
  curPage: number, 

  // 頁面顯示的數據
  datalist: Array<object>, 

  // 控制下拉刷新的開關
  refresh: boolean 
}

let pageSize = 10

@inject('store')
@observer
export default class List extends Component<Props, State> {
  constructor (
    public props: Props, 
    public flatlist,
  ) {
    super(props)
    this.flatlist = createRef()
  }

  state = {
    curPage: 1,
    datalist: [],
    refresh: false
  }

  // 渲染 Flatlist 組件數據
  _renderItem(item) {
    let {img, name, burdens, all_click, favorites} = item.item.data   
    return (
      <View style={styles.listWrap}>
        <View style={styles.imgWrap}>
          <Image style={styles.image} source={{uri: img}}></Image>
        </View>
        <View style={styles.descWrap}>
          <Text style={styles.title}>{name}</Text>
          <Text style={styles.subtitle} numberOfLines={1}>{burdens}</Text>
          <Text style={styles.desc}>{all_click} {favorites}</Text>
        </View>
      </View>
    )
  }

  // 處理用戶拉到底端的響應函數
  _handleReachEnd() {
    // 如果還有數據,一直加載
    if (this.state.curPage < Math.ceil(this.props.store.list.length / pageSize)) {
      this.setState((state) => {
        return {
          curPage: state.curPage + 1
        }
      }, () => {
        this._loadData()
      })
    }
  }

  // 下拉刷新的響應函數
  _handleRefresh() {
    this.setState({
      refresh: true
    })

    // 此處可以異步獲取後端接口數據,具體實現思路見上拉加載。
    setTimeout(() => {
      this.setState({
        refresh: false
      })
    }, 2000)
  }

  // 加載數據
  // 注:這裏的 key: value.id 由於模擬接口會出現重複的情況
  _loadData() {
    let data = this.props.store.list.slice(0, this.state.curPage * pageSize)
    let flatListData = data.map((value, index) => ({
        data: value,
        key: value.id
      })
    )
    this.setState({
      datalist: flatListData
    })
  }

  // 執行第一次數據加載
  componentDidMount() {
    setTimeout((params) => {
      this._loadData()
    }, 0)
  }

  render() {
    return (
      <FlatList
        ref={this.flatlist}
        renderItem={this._renderItem.bind(this)}
        data={this.state.datalist}
        refreshing={this.state.refresh}
        onEndReached={this._handleReachEnd.bind(this)}
        onEndReachedThreshold={1}
        onRefresh={this._handleRefresh.bind(this)}
      ></FlatList>
    )
  }
}

style_list.js 樣式

import { StyleSheet } from 'react-native'

export default StyleSheet.create({
  listWrap: {
    flexDirection: 'row',
    padding: 10,
    borderBottomWidth: 1,
    borderStyle: 'solid',
    borderBottomColor: '#eee'
  },

  imgWrap: {
    width: 135,
    paddingRight: 10
  },

  image: {
    width: 115,
    height: 75
  },

  descWrap: {
    flex: 1
  },

  title: {
    fontSize: 20,
    lineHeight: 30
  },

  subtitle: {
    fontSize: 16,
    color: '#666',
    lineHeight: 30,
    overflow: 'hidden'
  },

  desc: {
    fontSize: 12,
    color: '#666'
  }
})

react-navigation

本項目應用 React Navigation 構建路由系統。

安裝 React Navigation 環境

npm install @react-navigation/native

npm install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

npm install @react-navigation/stack

給App.tsx配置路由

import React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'

import { Provider } from 'mobx-react'
import store from './store/'

import Index from './pages/index/Index'
import List from './pages/list/List'
import Detail from './pages/detail/Detail'

const Stack = createStackNavigator()

export default function App() {

  // 這裏配置了三個頁面
  return (
    <NavigationContainer>
      <Provider store={store}>
        <Stack.Navigator
          screenOptions={{ 
            headerStyle: {
              backgroundColor: '#ee7530'
            },
            headerTintColor: '#fff',
            headerTitleStyle: {
              fontWeight: 'bold',
              fontSize: 20
            }
          }}
        >
          <Stack.Screen 
            name="Index" 
            component={Index}
            options={{ 
              title: '首頁'
            }}
          />
          <Stack.Screen
            name="List"
            component={List}
            options={{
              title: '熱門'
            }}
          />
          <Stack.Screen
            name="Detail"
            component={Detail}
            options={{
              title: '詳情'
            }}
          />
        </Stack.Navigator>
      </Provider>
    </NavigationContainer>
  )
}

創建 Context

爲了讓組件能收到路由的信息,這裏我們自己構建了一個 Context。

在根目錄下創建一個context目錄,在此目錄下創建一個 navigation.js 文件,內容如下:

// context/navigations.js

import { createContext } from 'react'

const navigationContext = createContext()

let { Provider, Consumer } = navigationContext

export {
  navigationContext,
  Provider,
  Consumer
}

修改 index.tsx

1.解構出Provider
import { Provider } from '../../context/navigation'
2.通過Context 的Provider,將props遞交給後代組件
<Provider value={{...this.props}}>
  <Home></Home>
</Provider>

<Provider value={{...this.props}}>
  <List></List>
</Provider>
3.全部內容
// pages/index/Index.tsx

import React, { Component, ContextType } from 'react'
import TabNavigator from 'react-native-tab-navigator'
import * as Device from 'expo-device'

// 解構出 Provider
import { Provider } from '../../context/navigation'

import {
  View,
  Text
} from 'react-native'

import {
  Img
} from './styled_index'
import styles from './style_index'

import cookbook from '../../assets/images/cookbook.png'
import cookbookActive from '../../assets/images/cookbook-active.png'
import category from '../../assets/images/menu.png'
import categoryActive from '../../assets/images/menu-active.png'
import map from '../../assets/images/location.png'
import mapActive from '../../assets/images/location-active.png'
import more from '../../assets/images/more.png'
import moreActive from '../../assets/images/more-active.png'

import Home from '../home/Home'
import List from '../list/List'
import Detail from '../detail/Detail'

interface Props {
  navigation?: any
}

interface State {
  selectedTab: string
}

class Index extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
  }

  state: State = {
    selectedTab: 'home'
  }


  componentDidMount() {
    
  }

  render() {
    return (
      <>
        <TabNavigator
          tabBarStyle={Device.deviceName === 'iPhone Xʀ' ? styles.tabBarStyle : null}
        >
          <TabNavigator.Item
            selected={this.state.selectedTab === 'home'}
            title="美食大全"
            titleStyle={styles.titleStyle}
            selectedTitleStyle={styles.selectedTitleStyle}
            renderIcon={() => <Img source={cookbook} />}
            renderSelectedIcon={() => <Img source={cookbookActive} />}
            onPress={() => {
              this.setState({ selectedTab: 'home' })
              this.props.navigation.setOptions({ title: '美食大全' })
            }}
          >
            {/* 通過Context 的Provider,將props遞交給後代組件 */}
            <Provider value={{...this.props}}>
              <Home></Home>
            </Provider>
          </TabNavigator.Item>
          <TabNavigator.Item
            selected={this.state.selectedTab === 'category'}
            title="熱門"
            titleStyle={styles.titleStyle}
            selectedTitleStyle={styles.selectedTitleStyle}
            renderIcon={() => <Img source={category} />}
            renderSelectedIcon={() => <Img source={categoryActive} />}
            onPress={
              () => {
                this.setState({ selectedTab: 'category' })
                this.props.navigation.setOptions({ title: '熱門' })
              }
            }
          >
            {/* 通過Context 的Provider,將props遞交給後代組件 */}
            <Provider value={{...this.props}}>
              <List></List>
            </Provider>
          </TabNavigator.Item>
          <TabNavigator.Item
            selected={this.state.selectedTab === 'map'}
            title="地圖"
            titleStyle={styles.titleStyle}
            selectedTitleStyle={styles.selectedTitleStyle}
            renderIcon={() => <Img source={map} />}
            renderSelectedIcon={() => <Img source={mapActive} />}
            onPress={() => {
              this.setState({ selectedTab: 'map' })
              this.props.navigation.setOptions({ title: '地圖' })
            }}
          >
            {<View><Text>地圖</Text></View>}
          </TabNavigator.Item>
          <TabNavigator.Item
            selected={this.state.selectedTab === 'more'}
            title="更多"
            titleStyle={styles.titleStyle}
            selectedTitleStyle={styles.selectedTitleStyle}
            renderIcon={() => <Img source={more} />}
            renderSelectedIcon={() => <Img source={moreActive} />}
            onPress={() => {
              this.setState({ selectedTab: 'more' })
              this.props.navigation.setOptions({ title: '更多' })
            }}
          >
            {<View><Text>更多</Text></View>}
          </TabNavigator.Item>
        </TabNavigator>
      </>
    )
  }
}

export default Index

修改 home.tsx

1. 將路由信息傳給HotCate
<HotCate { ...this.props }></HotCate>
2.定義Props
interface Props {
  navigation?: any
}
3.全部內容
// pages/home/Home.tsx

import React, { Component } from 'react'
import { ScrollView, StatusBar } from 'react-native'

import Swiper from './Swiper'
import HotCate from './HotCate'
import Top10 from './Top10'

interface Props {
  navigation?: any
}

interface State {
  
}

class Home extends Component<Props, State> {
  render() {
    return (
      <ScrollView>
        <StatusBar backgroundColor="blue" barStyle="light-content" />
        <Swiper></Swiper>
        {/* 將路由信息傳給HotCate */}
        <HotCate { ...this.props }></HotCate>
        <Top10></Top10>
      </ScrollView>
    )
  }
}

export default Home

修改 HotCate.tsx

1. 導入
import { Consumer } from '../../context/navigation'
2. 路由到“熱門”頁面
_onPress = (navigation) => {
  return () => {
    navigation.push('List')
  }
}

<View style={styles.container}>
  <Consumer>
    {
      ({navigation}) => {
        return (
          <Grid
            data={this.state.hotCate}
            renderItem={this._renderItem}
            hasLine={false}
            onPress={this._onPress(navigation)}
          ></Grid>
        )
      }
    }
  </Consumer>
</View>
3. 全部代碼
// pages/home/HotCate.tsx

import React, { Component } from 'react'
import { Grid } from '@ant-design/react-native'
import { get } from '../../utils/http'
import { Consumer } from '../../context/navigation'

import styles from './style_home'

import {
  View,
  Text,
  Image
} from 'react-native'

interface Props {
  
}
interface State {
  hotCate: Array<object>
}

export default class HotCate extends Component<Props, State> {
  state = {
    hotCate: []
  }

  _renderItem(el, index) {
    return (
      <View
        style={styles.gridContainer}
      >
        {el.img ? <Image source={{uri: el.img}} style={styles.gridImg}></Image> : null}
        <Text style={styles.gridText}>{el.title}</Text>
      </View>
    )
  }

  _onPress = (navigation) => {
    return () => {
      navigation.push('List')
    }
  }
  
  async componentDidMount() {
    let hotCate = await get('http://localhost:9000/api/hotcate')

    // 補全最後一項數據
    hotCate.push({
      img: '',
      title: '更多...'
    })

    this.setState({
      hotCate
    })
  }

  render() {
    return (
      <View style={styles.container}>
        <Consumer>
          {
            ({navigation}) => {
              return (
                <Grid
                  data={this.state.hotCate}
                  renderItem={this._renderItem}
                  hasLine={false}
                  onPress={this._onPress(navigation)}
                ></Grid>
              )
            }
          }
        </Consumer>
      </View>
    )
  }
}

修改 Top10.tsx

1. 通過 contextType 定義 context
import { navigationContext } from '../../context/navigation'
2. 導航到詳情頁,並傳參
import { navigationContext } from '../../context/navigation'

static contextType = navigationContext

_onPress = (e) => {
  this.context.navigation.push('Detail', { name: e.name })
}

<Grid
  data={this.props.store.top10}
  columnNum={2}
  hasLine={false}
  renderItem={this._renderTop10}
  onPress={this._onPress}
/>
3. 全部代碼
// pages/home/Top10.tsx

import React, { Component } from 'react'
import { Grid } from '@ant-design/react-native'
import { observer, inject } from 'mobx-react'
import { navigationContext } from '../../context/navigation'

import {
  View,
  Text,
  Image
} from 'react-native'

import styles from './style_home.js'

interface Props {
  // store 作爲組件的 props
  store?: any
}

interface State {
  
}

// 注入 store 與 將類變爲可觀察的對象
@inject('store')
@observer
class Top10 extends Component<Props, State> {

  static contextType = navigationContext

  _renderTop10(el, index) {
    return (
      <View style={styles.top10ItemContainer}>
        <View style={styles.top10ImgContainer}>
          <Image style={styles.Top10Img} source={{uri: el.img}}></Image>
        </View>
        <View style={styles.top10DesContainter}>
          <Text style={styles.top10Titie}>{el.name}</Text>
          <Text style={styles.Top10Desc}>{el.all_click} {el.favorites}</Text>
        </View>
      </View>
    )
  }

  _onPress = (e) => {
    this.context.navigation.push('Detail', { name: e.name })
  }

  render() {   
    return (
      <View style={styles.top10Container}>
        <View style={styles.top10Head}>
          <Text style={styles.top10HeadText}>精品好菜</Text>
        </View>
        <View style={styles.gridContainer}>
          <Grid
            data={this.props.store.top10}
            columnNum={2}
            hasLine={false}
            renderItem={this._renderTop10}
            onPress={this._onPress}
          />
        </View>
      </View>
    )
  }
}

export default Top10

修改 List.tsx

1. 載入路由相關模塊,實現路由到詳情頁的功能,主要代碼:
// 1. 載入Context
import { navigationContext } from '../../context/navigation'

// 2. 在 Props 裏定義 navigation
interface Props {
  store?: any,
  navigation?: any
}

// 3. 在類裏定義 contextType 靜態變量
static contextType = navigationContext

// 4. 在組件類裏定義路由跳轉響應方法
_onPress = (name: string) => {
  return () => {
    // 鑑於此頁面從 TabBar 和 首頁兩個入口進入
    // 路由跳轉的方式也不同
    if (this.context) {
      // 從Tabbar進入
      this.context.navigation.push('Detail', {name})
    } else {
      // 從首頁進入
      this.props.navigation.push('Detail', {name})
    }
  }
}

// 5. 應用 TouchableOpacity 組件綁定路由跳轉事件
<TouchableOpacity
  onPress={this._onPress(name)}
>
  <View style={styles.listWrap}>
    <View style={styles.imgWrap}>
      <Image style={styles.image} source={{uri: img}}></Image>
    </View>
    <View style={styles.descWrap}>
      <Text style={styles.title}>{name}</Text>
      <Text style={styles.subtitle} numberOfLines={1}>{burdens}</Text>
      <Text style={styles.desc}>{all_click} {favorites}</Text>
    </View>
  </View>
</TouchableOpacity>
2. 全部代碼
import React, { Component, createRef } from 'react'
import { navigationContext } from '../../context/navigation'

import {
  inject,
  observer
} from 'mobx-react'

import {
  View,
  Text,
  Image,
  FlatList,
  TouchableOpacity
} from 'react-native'

import styles from './style_list'

interface Props {
  store?: any,
  navigation?: any
}

interface State {
  // 記錄上拉加載更多的當前頁碼
  curPage: number, 

  // 頁面顯示的數據
  datalist: Array<object>, 

  // 控制下拉刷新的開關
  refresh: boolean 
}

let pageSize = 10

@inject('store')
@observer
export default class List extends Component<Props, State> {
  constructor (
    public props: Props, 
    public flatlist,
  ) {
    super(props)
    this.flatlist = createRef()
  }

  state = {
    curPage: 1,
    datalist: [],
    refresh: false
  }

  static contextType = navigationContext

  _onPress = (name: string) => {
    return () => {
      if (this.context) {
        this.context.navigation.push('Detail', {name})
      } else {
        this.props.navigation.push('Detail', {name})
      }
    }
  }
  
  // 渲染 Flatlist 組件數據
  _renderItem(item) {
    let {img, name, burdens, all_click, favorites} = item.item.data   
    return (
      <TouchableOpacity
        onPress={this._onPress(name)}
      >
        <View style={styles.listWrap}>
          <View style={styles.imgWrap}>
            <Image style={styles.image} source={{uri: img}}></Image>
          </View>
          <View style={styles.descWrap}>
            <Text style={styles.title}>{name}</Text>
            <Text style={styles.subtitle} numberOfLines={1}>{burdens}</Text>
            <Text style={styles.desc}>{all_click} {favorites}</Text>
          </View>
        </View>
      </TouchableOpacity>
    )
  }

  // 處理用戶拉到底端的響應函數
  _handleReachEnd() {
    // 如果還有數據,一直加載
    // 加載更多,由於Mock數據問題,有ID重複問題
    // if (this.state.curPage < Math.ceil(this.props.store.list.length / pageSize)) {
    //   console.log(this.state.curPage)
    //   this.setState((state) => {
    //     return {
    //       curPage: state.curPage + 1
    //     }
    //   }, () => {
    //     this._loadData()
    //   })
    // }
  }

  // 下拉刷新的響應函數
  _handleRefresh() {
    this.setState({
      refresh: true
    })

    // 此處可以異步獲取後端接口數據,具體實現思路見上拉加載。
    setTimeout(() => {
      this.setState({
        refresh: false
      })
    }, 2000)
  }

  // 加載數據
  _loadData() {
    let data = this.props.store.list.slice(0, this.state.curPage * pageSize)    
    let flatListData = data.map((value, index) => ({
        data: value,
        key: value.id
      })
    )
    this.setState({
      datalist: flatListData
    })
  }

  // 執行第一次數據加載
  componentDidMount() {
    setTimeout((params) => {
      this._loadData()
    }, 0)
  }

  render() {
    return (
      <View style={styles.container}>
        <FlatList
          ref={this.flatlist}
          renderItem={this._renderItem.bind(this)}
          data={this.state.datalist}
          refreshing={this.state.refresh}
          onEndReached={this._handleReachEnd.bind(this)}
          onEndReachedThreshold={1}
          onRefresh={this._handleRefresh.bind(this)}
        ></FlatList>
      </View>
    )
  }
}

創建詳情頁

在路由信息定義好後,就可以構建詳情頁了。

Detail.tsx

// pages/detail/Detail.tsx

import React, { Component } from 'react'
import { get } from '../../utils/http'
import {
  View,
  ScrollView,
  Text,
  Image,
  StatusBar,
  TouchableOpacity,
  Alert
} from 'react-native'

import styles from './style_detail'

interface Props {
  navigation?: any,
  route?: any
}
interface State {
  detail: {}
}

export default class Detail extends Component<Props, State> {
  state = {
    detail: null
  }

  async componentDidMount() {
    let result = await get('http://localhost:9000/api/detail')
    this.setState({
      detail: result
    })
    // 根據路由傳遞過來參數,修改本頁的 title
    this.props.navigation.setOptions({ title: this.props.route.params.name })
  }
  
  render() {
    let detail = this.state.detail
    return (
      <>
        {
          detail && (
            <ScrollView>
              <View style={styles.container}>
                <StatusBar backgroundColor="blue" barStyle="light-content" />
                <Image
                  source={{uri: detail.img}}
                  style={styles.mainImg}
                ></Image>
                <View style={styles.mainInfo}>
                  <View>
                    <Text style={styles.mainInfoName}>{detail.name}</Text>
                  </View>
                  <View>
                    <Text style={styles.mainInfoSubTitle}>{detail.all_click}瀏覽/{detail.favorites}收藏</Text>
                  </View>
                  <TouchableOpacity
                    onPress={() => Alert.alert('已經收藏.')}
                  >
                    <View style={styles.mainInfoButtonWrap}>
                      <Text style={styles.mainInfoButton}>收藏</Text>
                    </View>
                  </TouchableOpacity>
                </View>
                <View style={styles.infoWrap}>
                  <View>
                    <Text style={styles.infoTitle}>心得</Text>
                  </View>
                  <View>
                    <Text style={styles.infoText}>
                      {detail.info}
                    </Text>
                  </View>

                  <View>
                    <Text style={[styles.infoTitle, {marginTop: 20}]}>做法</Text>
                  </View>
                  <View>
                    {
                      detail.makes.map((value) => {
                        return (
                          <View
                            key={value.num}
                          >
                            <View>
                              <Text style={styles.makesTitle}>{value.num} {value.info}</Text>
                            </View>
                            <View>
                              <Image 
                                source={{uri: value.img}}
                                style={styles.makesImg}
                              ></Image>
                            </View>
                          </View>
                        )
                      })
                    }
                  </View>
                </View>
              </View>
            </ScrollView>
          )
        }
      </>
    )
  }
}

style_detail.js 頁面樣式

// pages/detail/style_detail.js

import { StyleSheet } from 'react-native'

export default StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#eee',
    paddingBottom: 34
  },

  // main
    mainImg: {
      width: '100%',
      height: 250
    },

    mainInfo: {
      height: 170,
      justifyContent: 'center',
      alignItems: 'center',
      backgroundColor: '#fff'
    },

    mainInfoName: {
      fontSize: 24
    },
    
    mainInfoSubTitle: {
      marginTop: 10,
      fontSize: 12,
      color: '#666'
    },

    // button
    mainInfoButtonWrap: {
      width: 140,
      height: 40,
      backgroundColor: '#df7b42',
      marginTop: 20,
      borderRadius: 6
    },

    mainInfoButton: {
      textAlign: 'center',
      lineHeight: 40,
      color: '#fff',
      fontSize: 16
    },

  // info
    infoWrap: {
      marginTop: 15,
      backgroundColor: '#fff',
      paddingTop: 25,
      paddingLeft: 15,
      paddingBottom: 25
    },
    
    infoTitle: {
      fontSize: 20,
      fontWeight: 'bold',
      marginBottom: 20
    },

    infoText: {
      fontSize: 16,
      lineHeight: 20,
      paddingRight: 20
    },
  
  // makes
    makesTitle: {
      fontSize: 16,
      paddingRight: 30
    },

    makesImg: {
      width: 300,
      height: 210,
      margin: 20
    }
})

創建美食地圖頁

“美食地圖” 主要應用的知識點是 WebView, 根據官網推薦,使用 react-native-webview 項目來實現在RN裏嵌入Web頁面。

模塊安裝

npm install --save react-native-webview

react-native link react-native-webview

Map.tsx 文件構建

在項目根目錄 pages 下創建目錄 map, 在 map 目錄下創建 Map.tsx 文件,文件內容如下:

import React, { Component } from 'react'
import { View } from 'react-native'
import { WebView } from 'react-native-webview'

interface Props {
  
}
interface State {
  
}

export default class Map extends Component<Props, State> {
  state = {}

  render() {
    return (
      <View
        style={{
          width: '100%',
          flex: 1
        }}
      >
        <WebView
          source={{ uri: 'https://map.baidu.com/search/%E7%BE%8E%E9%A3%9F/@12959238.56,4825347.47,12z?querytype=s&da_src=shareurl&wd=%E7%BE%8E%E9%A3%9F&c=131&src=0&pn=0&sug=0&l=12&b=(12905478.56,4795011.47;13012998.56,4855683.47)&from=webmap&biz_forward=%7B%22scaler%22:2,%22styles%22:%22pl%22%7D&device_ratio=2' }}
          style={{ 
            width: '100%',
            height: '100%'
          }}
        />
      </View>
    )
  }
}

是否顯示地圖頁籤

更多頁面實現了兩個功能:

1、是否顯示地圖頁籤

2、拍照功能

是否顯示地圖頁籤

更新 store

添加了 isShow 屬性,和 setVisible 方法。

// store/index.js

import {
  observable,
  action,
  computed
} from 'mobx'

class Store {
  // swiper 與 top10 共享的數據
  @observable
  list = []

  // 定義是否顯示地圖按鈕
  @observable
  isShow = true

  // swiper 數據過濾
  @computed
  get swiper() {
    return this.list.slice(0, 5).map((value, index) => {
      return {
        img: value.img
      }
    })
  }

  // top10 數據過濾
  @computed
  get top10() {
    return this.list.slice(0, 10).map((value, index) => {
      return {
        img: value.img,
        all_click: value.all_click,
        favorites: value.favorites,
        name: value.name
      }
    })
  }

  // 裝載 list 數據
  @action.bound
  setList(data) {
    this.list = data
  }

  // 修改是否顯示地圖按鈕
  @action.bound
  setVisible(status) {
    this.isShow = status
  }
}

export default new Store()

添加 More.tsx 文件

在根目錄pages下創建 more 文件夾,再創建 More.tsx 文件,內容如下

// pages/more/More.tsx

import React, { Component } from 'react'
import { View, Text, Switch, AsyncStorage } from 'react-native'
import { observer, inject } from 'mobx-react'

interface Props {
  store?: any
}

interface State {

}

@inject('store')
@observer
export default class Profile extends Component<Props, State> {
  state = {

  }

  async componentDidMount() {
    
  }

  render() {
    return (
      <View>
        <View style={{
          flexDirection: 'row', 
          justifyContent: 'flex-start', 
          alignItems: 'flex-start', 
          padding: 20
        }}>
          <View style={{
            height: 30, 
            justifyContent: 'center', 
            alignItems: 'center'
          }}>
            <Text>是否顯示地圖:</Text>
          </View>
          <Switch
            value={this.props.store.isShow}
            onValueChange={(value) => {
              this.props.store.setVisible(value)
              AsyncStorage.setItem('isShow', value.toString())
            }}
          ></Switch>
        </View>
      </View>
    )
  }
}

修改 pages/index/Index.tsx 文件

1、修改代碼的要點
  // 定義 store
  interface Props {
    navigation?: any
    store?: any
  }

  // 記錄用戶緩存
  async componentDidMount() {
    let isShow = await AsyncStorage.getItem('isShow')
    this.props.store.setVisible(JSON.parse(isShow))
  }

  // 在 tabbar 裏修改
  {
    this.props.store.isShow
      ? (
        <TabNavigator.Item
          selected={this.state.selectedTab === 'map'}
          title="地圖"
          titleStyle={styles.titleStyle}
          selectedTitleStyle={styles.selectedTitleStyle}
          renderIcon={() => <Img source={map} />}
          renderSelectedIcon={() => <Img source={mapActive} />}
          onPress={() => {
            this.setState({ selectedTab: 'map' })
            this.props.navigation.setOptions({ title: '地圖' })
          }}
        >
          <Map></Map>
        </TabNavigator.Item>
      )
      : null
  }
2. 全部代碼
// pages/index/Index.tsx

import React, { Component, ContextType } from 'react'
import TabNavigator from 'react-native-tab-navigator'
import * as Device from 'expo-device'
import { observer, inject } from 'mobx-react'

import { Provider } from '../../context/navigation'

import {
  View,
  Text,
  AsyncStorage
} from 'react-native'

import {
  Img
} from './styled_index'
import styles from './style_index'

import cookbook from '../../assets/images/cookbook.png'
import cookbookActive from '../../assets/images/cookbook-active.png'
import category from '../../assets/images/menu.png'
import categoryActive from '../../assets/images/menu-active.png'
import map from '../../assets/images/location.png'
import mapActive from '../../assets/images/location-active.png'
import more from '../../assets/images/more.png'
import moreActive from '../../assets/images/more-active.png'

import Home from '../home/Home'
import List from '../list/List'
import Map from '../map/Map'
import More from '../more/More'

interface Props {
  navigation?: any
  store?: any
}

interface State {
  selectedTab: string
}

@inject('store')
@observer
class Index extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
  }

  state: State = {
    selectedTab: 'home'
  }

  async componentDidMount() {
    let isShow = await AsyncStorage.getItem('isShow')
    this.props.store.setVisible(JSON.parse(isShow))
  }

  render() {
    return (
      <>
        <TabNavigator
          tabBarStyle={Device.deviceName === 'iPhone Xʀ' ? styles.tabBarStyle : null}
        >
          <TabNavigator.Item
            selected={this.state.selectedTab === 'home'}
            title="美食大全"
            titleStyle={styles.titleStyle}
            selectedTitleStyle={styles.selectedTitleStyle}
            renderIcon={() => <Img source={cookbook} />}
            renderSelectedIcon={() => <Img source={cookbookActive} />}
            onPress={() => {
              this.setState({ selectedTab: 'home' })
              this.props.navigation.setOptions({ title: '美食大全' })
            }}
          >
            <Provider value={{...this.props}}>
              <Home></Home>
            </Provider>
          </TabNavigator.Item>
          <TabNavigator.Item
            selected={this.state.selectedTab === 'category'}
            title="熱門"
            titleStyle={styles.titleStyle}
            selectedTitleStyle={styles.selectedTitleStyle}
            renderIcon={() => <Img source={category} />}
            renderSelectedIcon={() => <Img source={categoryActive} />}
            onPress={
              () => {
                this.setState({ selectedTab: 'category' })
                this.props.navigation.setOptions({ title: '熱門' })
              }
            }
          >
            <Provider value={{...this.props}}>
              <List></List>
            </Provider>
          </TabNavigator.Item>
          {
            this.props.store.isShow
              ? (
                <TabNavigator.Item
                  selected={this.state.selectedTab === 'map'}
                  title="地圖"
                  titleStyle={styles.titleStyle}
                  selectedTitleStyle={styles.selectedTitleStyle}
                  renderIcon={() => <Img source={map} />}
                  renderSelectedIcon={() => <Img source={mapActive} />}
                  onPress={() => {
                    this.setState({ selectedTab: 'map' })
                    this.props.navigation.setOptions({ title: '地圖' })
                  }}
                >
                  <Map></Map>
                </TabNavigator.Item>
              )
              : null
          }
          <TabNavigator.Item
            selected={this.state.selectedTab === 'more'}
            title="更多"
            titleStyle={styles.titleStyle}
            selectedTitleStyle={styles.selectedTitleStyle}
            renderIcon={() => <Img source={more} />}
            renderSelectedIcon={() => <Img source={moreActive} />}
            onPress={() => {
              this.setState({ selectedTab: 'more' })
              this.props.navigation.setOptions({ title: '更多' })
            }}
          >
            <More></More>
          </TabNavigator.Item>
        </TabNavigator>
      </>
    )
  }
}

export default Index

拍照功能

更多頁面實現了兩個功能:

1、是否顯示地圖頁籤

2、拍照功能

安裝模塊

npm install expo-camera -S

改寫 More.tsx 代碼

以下代碼是 拍照 和 切換顯示地圖按鈕 的全部代碼。

import React, { Component } from 'react'
import { View, Text, Switch, AsyncStorage, TouchableOpacity, Image } from 'react-native'
import { observer, inject } from 'mobx-react'

import * as Permissions from 'expo-permissions'
import { Camera } from 'expo-camera'

interface Props {
  store?: any
}

interface State {
  hasCameraPermission: boolean
  type: boolean
  isTakePic: boolean,
  picUri: string
}

@inject('store')
@observer
export default class Profile extends Component<Props, State> {
  camera = null

  state = {
    hasCameraPermission: null,
    type: Camera.Constants.Type.back,
    isTakePic: false,
    picUri: 'http://placehold.it/240x180'
  }

  async componentDidMount() {
    const { status } = await Permissions.askAsync(Permissions.CAMERA);
    this.setState({ 
      hasCameraPermission: status === 'granted' 
    })
  }

  showTakePicScene() {
    this.setState({
      isTakePic: true
    })
  }

  async takePicture() {
    let result = await this.camera.takePictureAsync()
    this.setState({
      isTakePic: false,
      picUri: result.uri
    })
  }

  render() {
    return (
      <>
        {
          this.state.isTakePic
            ? (
              <Camera
                style={{ flex: 1 }} 
                type={this.state.type}
                ref={ref => {
                  this.camera = ref
                }}
              >
                <TouchableOpacity
                  onPress={this.takePicture.bind(this)}
                >
                  <View style={{
                    marginLeft: 20, 
                    width: 80, 
                    height: 40, 
                    backgroundColor: '#f9efd4', 
                    justifyContent: 'center', 
                    alignItems: 'center'
                  }}>
                    <Text>拍照</Text>
                  </View>
                </TouchableOpacity>
              </Camera>
            )
            : (
              <View>
                <View style={{
                  flexDirection: 'row', 
                  justifyContent: 'flex-start', 
                  alignItems: 'flex-start', 
                  padding: 20
                }}>
                  <View style={{
                    height: 30, 
                    justifyContent: 'center', 
                    alignItems: 'center'
                  }}>
                    <Text>是否顯示地圖:</Text>
                  </View>
                  <Switch
                    value={this.props.store.isShow}
                    onValueChange={(value) => {
                      this.props.store.setVisible(value)
                      AsyncStorage.setItem('isShow', value.toString())
                    }}
                  ></Switch>
                </View>
                <TouchableOpacity
                  onPress={this.showTakePicScene.bind(this)}
                >
                  <View style={{
                    marginLeft: 20,
                    width: 80, 
                    height: 40, 
                    backgroundColor: '#df7b42', 
                    justifyContent: 'center', 
                    alignItems: 'center'
                  }}>
                    <Text style={{color: '#fff'}}>拍照</Text>
                  </View>
                </TouchableOpacity>
                <View>
                  <Image style={{marginLeft: 20, marginTop: 20, width: 240, height: 180}} source={{uri: this.state.picUri}}></Image>
                </View>
              </View>
            )
        }
      </>
    )
  }
}

項目發佈

本項目發佈利用expo發佈功能,詳細可參考 構建獨立的應用程序

安裝 Expo CLI

此步驟已經完成。

配置 app.json

{
  "expo": {
    "name": "rn-cookbooks",
    "slug": "rn-cookbooks",
    "privacy": "public",
    "sdkVersion": "36.0.0",
    "platforms": [
      "ios",
      "android",
      "web"
    ],
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "bundleIdentifier": "com.qianfeng.felixlu",
      "buildNumber": "1.0.0"
    },
    "android": {
      "package": "com.qianfeng.felixlu",
      "versionCode": 1
    }
  }
}

開始Build

expo build:android

或:

expo build:ios

build 過程監測

輸入以上命令後,會在控制檯看到下邊信息:

Build started, it may take a few minutes to complete.
You can check the queue length at https://expo.io/turtle-status

You can monitor the build at

https://expo.io/dashboard/felixlurt/builds/15b2ae11-c98d-48dc-879e-9ff05fb0b9f1

可以通過訪問 https://expo.io/dashboard/felixlurt/builds/15b2ae11-c98d-48dc-879e-9ff05fb0b9f1 來監控build過程。(注意鏈接是終端打印的,這個鏈接只是個示例)

build 成功後,點擊 “Download” 按鈕即可下載打完的APP安裝包了。

注:iOS 需要有開發者賬號,沒有賬號的童鞋建議運行 expo build:android進行試驗

項目源碼下載

👉點擊下載 👈

🎉感謝

❀感謝felix的幫助,想了解、學習更多前端知識的童鞋可以加他qq:2518121701 古藝散人,學前端找他準沒錯!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章