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 {
  constructor(props) {
    super(props)
  }
  
  state: State = {
    
  }

  componentDidMount() {
    
  }

  render() {
    return (
      
        
          Index 组件内容
        
      
    )
  }
}

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 (
    
  )
}

引入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 {
  constructor(props: Props) {
    super(props)
  }

  state: State = {
    selectedTab: 'home'
  }

  componentDidMount() {

  }

  render() {
    return (
      
         }
          renderSelectedIcon={() => }
          onPress={() => this.setState({ selectedTab: 'home' })}
        >
          {美食大全}
        
         }
          renderSelectedIcon={() => }
          onPress={() => this.setState({ selectedTab: 'category' })}
        >
          {分类}
        
         }
          renderSelectedIcon={() => }
          onPress={() => this.setState({ selectedTab: 'map' })}
        >
          {地图}
        
         }
          renderSelectedIcon={() => }
          onPress={() => this.setState({ selectedTab: 'more' })}
        >
          {更多}
        
      
    )
  }
}

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

搭建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 {
  render() {
    return (
      <>
        
        
      
    )
  }
}

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
}

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

  render() {
    return (
      
        {
          this.state.list.slice(0, 5).map((value, index) => {
            return (
              
                
              
            )
          })
        }
      
    )
  }
}

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
}

export default class HotCate extends Component {
  state = {
    hotCate: []
  }

  _renderItem(el, index) {
    return (
      
        {el.img ?  : null}
        {el.title}
      
    )
  }

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

    // 补全最后一项数据
    hotCate.push({
      img: '',
      title: '更多...'
    })

    this.setState({
      hotCate
    })
  }

  render() {
    return (
      
        
      
    )
  }
}

修改 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 {

  renderTop10(el, index) {
    return (
      
        
          
        
        
          {el.name}
          {el.all_click} {el.favorites}
        
      
    )
  }

  render() {   
    return (
      
        
          精品好菜
        
        
          
        
      
    )
  }
}

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 {
  state = {
    list: []
  }
  async componentDidMount() {
    let list = await get('http://localhost:9000/api/swiper')
    this.props.store.setList(list)
  }

  render() {
    return (
      
        {
          this.props.store.swiper.map((value, index) => {
            return (
              
                
              
            )
          })
        }
      
    )
  }
}

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 {
  render() {
    return (
      
        
        
        
      
    )
  }
}

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, 

  // 控制下拉刷新的开关
  refresh: boolean 
}

let pageSize = 10

@inject('store')
@observer
export default class List extends Component {
  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 (
      
        
          
        
        
          {name}
          {burdens}
          {all_click} {favorites}
        
      
    )
  }

  // 处理用户拉到底端的响应函数
  _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 (
      
    )
  }
}

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 (
    
      
        
          
          
          
        
      
    
  )
}

创建 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递交给后代组件

  



  
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 {
  constructor(props: Props) {
    super(props)
  }

  state: State = {
    selectedTab: 'home'
  }


  componentDidMount() {
    
  }

  render() {
    return (
      <>
        
           }
            renderSelectedIcon={() => }
            onPress={() => {
              this.setState({ selectedTab: 'home' })
              this.props.navigation.setOptions({ title: '美食大全' })
            }}
          >
            {/* 通过Context 的Provider,将props递交给后代组件 */}
            
              
            
          
           }
            renderSelectedIcon={() => }
            onPress={
              () => {
                this.setState({ selectedTab: 'category' })
                this.props.navigation.setOptions({ title: '热门' })
              }
            }
          >
            {/* 通过Context 的Provider,将props递交给后代组件 */}
            
              
            
          
           }
            renderSelectedIcon={() => }
            onPress={() => {
              this.setState({ selectedTab: 'map' })
              this.props.navigation.setOptions({ title: '地图' })
            }}
          >
            {地图}
          
           }
            renderSelectedIcon={() => }
            onPress={() => {
              this.setState({ selectedTab: 'more' })
              this.props.navigation.setOptions({ title: '更多' })
            }}
          >
            {更多}
          
        
      
    )
  }
}

export default Index

修改 home.tsx

1. 将路由信息传给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 {
  render() {
    return (
      
        
        
        {/* 将路由信息传给HotCate */}
        
        
      
    )
  }
}

export default Home

修改 HotCate.tsx

1. 导入
import { Consumer } from '../../context/navigation'
2. 路由到“热门”页面
_onPress = (navigation) => {
  return () => {
    navigation.push('List')
  }
}


  
    {
      ({navigation}) => {
        return (
          
        )
      }
    }
  
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
}

export default class HotCate extends Component {
  state = {
    hotCate: []
  }

  _renderItem(el, index) {
    return (
      
        {el.img ?  : null}
        {el.title}
      
    )
  }

  _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 (
      
        
          {
            ({navigation}) => {
              return (
                
              )
            }
          }
        
      
    )
  }
}

修改 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 })
}

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 {

  static contextType = navigationContext

  _renderTop10(el, index) {
    return (
      
        
          
        
        
          {el.name}
          {el.all_click} {el.favorites}
        
      
    )
  }

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

  render() {   
    return (
      
        
          精品好菜
        
        
          
        
      
    )
  }
}

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 组件绑定路由跳转事件

  
    
      
    
    
      {name}
      {burdens}
      {all_click} {favorites}
    
  
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, 

  // 控制下拉刷新的开关
  refresh: boolean 
}

let pageSize = 10

@inject('store')
@observer
export default class List extends Component {
  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 (
      
        
          
            
          
          
            {name}
            {burdens}
            {all_click} {favorites}
          
        
      
    )
  }

  // 处理用户拉到底端的响应函数
  _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 (
      
        
      
    )
  }
}

创建详情页


在路由信息定义好后,就可以构建详情页了。

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 {
  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 && (
            
              
                
                
                
                  
                    {detail.name}
                  
                  
                    {detail.all_click}浏览/{detail.favorites}收藏
                  
                   Alert.alert('已经收藏.')}
                  >
                    
                      收藏
                    
                  
                
                
                  
                    心得
                  
                  
                    
                      {detail.info}
                    
                  

                  
                    做法
                  
                  
                    {
                      detail.makes.map((value) => {
                        return (
                          
                            
                              {value.num} {value.info}
                            
                            
                              
                            
                          
                        )
                      })
                    }
                  
                
              
            
          )
        }
      
    )
  }
}

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 {
  state = {}

  render() {
    return (
      
        
      
    )
  }
}

是否显示地图页签

更多页面实现了两个功能:

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 {
  state = {

  }

  async componentDidMount() {
    
  }

  render() {
    return (
      
        
          
            是否显示地图:
          
           {
              this.props.store.setVisible(value)
              AsyncStorage.setItem('isShow', value.toString())
            }}
          >
        
      
    )
  }
}

修改 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
      ? (
         }
          renderSelectedIcon={() => }
          onPress={() => {
            this.setState({ selectedTab: 'map' })
            this.props.navigation.setOptions({ title: '地图' })
          }}
        >
          
        
      )
      : 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 {
  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 (
      <>
        
           }
            renderSelectedIcon={() => }
            onPress={() => {
              this.setState({ selectedTab: 'home' })
              this.props.navigation.setOptions({ title: '美食大全' })
            }}
          >
            
              
            
          
           }
            renderSelectedIcon={() => }
            onPress={
              () => {
                this.setState({ selectedTab: 'category' })
                this.props.navigation.setOptions({ title: '热门' })
              }
            }
          >
            
              
            
          
          {
            this.props.store.isShow
              ? (
                 }
                  renderSelectedIcon={() => }
                  onPress={() => {
                    this.setState({ selectedTab: 'map' })
                    this.props.navigation.setOptions({ title: '地图' })
                  }}
                >
                  
                
              )
              : null
          }
           }
            renderSelectedIcon={() => }
            onPress={() => {
              this.setState({ selectedTab: 'more' })
              this.props.navigation.setOptions({ title: '更多' })
            }}
          >
            
          
        
      
    )
  }
}

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 {
  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
            ? (
               {
                  this.camera = ref
                }}
              >
                
                  
                    拍照
                  
                
              
            )
            : (
              
                
                  
                    是否显示地图:
                  
                   {
                      this.props.store.setVisible(value)
                      AsyncStorage.setItem('isShow', value.toString())
                    }}
                  >
                
                
                  
                    拍照
                  
                
                
                  
                
              
            )
        }
      
    )
  }
}

项目发布

本项目发布利用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 古艺散人,学前端找他准没错!

你可能感兴趣的:(react-native,mobx,app,前端)