流星和反应本地 - 创建本机移动应用程序
#javascript #教程 #meteor #reactnative

这是一个全面的研讨会。估计时间:整体60分钟至120分钟。我们将创建一个有效的移动应用程序,其中包括身份验证,配置文件页面和托管待办事项。该应用程序是用反应生态开发的,并使用了由流星实现的后端。

Rami Al-zayatUnsplash

上使用的照片

快速链接到最终研讨会代码:

GitHub logo jankapunkt / meteor-react-native-workshop

我们研讨会的代码回购

Meteor React Native Starter

This is the final code repo for our workshop "Meteor and React Native" @ Meteor Impact 2022 After post-editing it resulted in a complete starter repo. ?

JavaScript Style Guide GitHub

请注意,我无法覆盖所有操作系统。

大约

流星和反应本机默认不集成 但是,那里有很棒的包装,可以帮助我们整合它们 最好的是,实际上并不困难!

这个起动器为流星项目带来了最基本的集成,作为您的React Native应用程序的后端。 只需按照此读书中的说明即可立即获取startet。

preview

安装

您需要在系统上安装流星。 按照the Meteor website上的流星安装说明。

克隆回购并检查车间分支


背景

这个研讨会是my session at Meteor Impact 2022的结果,由于时间限制,它错过了最后一部分。因此,我审查了研讨会和代码,并改进了所有内容。 ð

准备好终端和IDES,抓住热饮料,让自己在接下来的两个小时内进行完整的动手课程。

在研讨会期间,我们将介绍这些主要主题:

  • 开发环境的安装
  • 创建流星后端
  • 创建React Native App
  • 将RN应用程序连接/重新连接到后端
  • 使用React Navigation的完整身份验证工作流
  • crud个人Tasks集合(使用流星方法和出版物/订阅)

如果您是流星或反应本地人的新手,请让我尽快将其介绍给您(如果您已经知道,请跳到下一节)​​。

Meteor icon

Meteor是一个全面的跨平台框架,用于开发现代JavaScript应用程序。它可以与您喜欢的前端引擎/库一起使用,以创建Webockets和Publish/Subscribe的SPA(单页应用程序)。但是,它还支持SSR,HTTP路线,并与NPM安装的绝大多数软件包集成在一起,因为在引擎盖下都是Nodejs。

react logo

React Native也是一个跨平台框架,但使用世界上最著名的JavaScript库之一来开发本地移动应用程序:React。但是不要弄错!这不仅仅是构建一些符合WebView的HTML5应用程序。它将使用移动平台的本机引擎功能渲染您的所有代码。

ðü共同为开发移动应用程序提供了出色的开发人员体验。他们都在许多平台上运行,并同时部署到不同的平台。例如,我们可以在Ubuntu Linux上运行我们的开发环境,但在iOS设备上运行开发构建。

您也可以在GitHub上查看并为其存储库做出贡献:

GitHub logo meteor / meteor

流星,JavaScript应用程序平台


TravisCI Status CircleCI Status built with Meteor


流星是 Ultra-simple 构建 Modern Web应用程序的环境。



使用现代JavaScript创建应用程序

受益于最新技术更新,以快速原型和开发您的应用程序。


â 整合您已经使用的技术

使用流行的框架和工具直接在框外。专注于构建功能,而不是自己配置不同的组件。


为任何设备构建应用程序

使用相同的代码,无论您是为Web,iOS,Android还是桌面开发的,可为用户提供无缝的更新体验。


入门

尝试在您喜欢的技术中尝试入门教程?

接下来,阅读documentation并获得一些examples

快速启动

在您的平台上,使用此行:

 >  npm安装-g流星

ð创建一个项目:

 > 流星创建my-app 

â!运行它:

开发人员资源

用流星构建应用程序?

  • 部署

GitHub logo facebook / react-native

使用React构建本机应用程序的框架

React Native

学习一次,在任何地方写:
用React构建移动应用程序

React Native is released under the MIT license. Current CircleCI build status. Current npm package version. PRs welcome! Follow @reactnative

Getting Started - · Learn the Basics - · Showcase - · Contribute - · Community - · Support

反应本机将React's声明性UI框架带到iOS和Android。使用React Native,您可以使用本机UI控件并可以完全访问本机平台。

  • 声明性。反应使创建交互式UIS无痛。声明的视图使您的代码更容易预测和更易于调试。
  • 基于组件的。构建封装的组件来管理其状态,然后构成它们以制造复杂的UI。
  • 开发人员的速度。请参阅秒的局部变化。可以对JavaScript代码进行更改,而无需重建本机应用程序。
  • 可移植性。重复使用iOS,Android和other platforms

React Native是由许多公司和个人核心贡献者开发和支持的。在我们的ecosystem overview中找到更多信息。

内容


架构概述

在我们开始动手研讨会之前,还有一件事。让我为喜欢视觉的人提供概述。将流星应用程序视为服务器(后端)和React本机应用程序作为客户端之一:

architecture overview

您可以看到两者都将使用@meteorrn/core进行通信。该软件包将大部分API实现为网络上的流星客户端捆绑包。但是,它还提供了一种简单的方法,可以将某些较低级别的API事件连接到与useEffect结合使用的情况下,非常容易对服务器的更改作用。

后端还自动管理authentication via koude3,而我们将使用带有React上下文的导航。请注意,身份验证还将使用@meteorrn/core的功能,使我们能够检索登录令牌以实现“自动login”功能

现在,让我们最终开始实际的研讨会。 -


1.先决条件ð§ðout

为了充分利用车间,您应该至少收集一个物理移动设备。没关系,无论是Android还是iOS,甚至两者兼而有之,React Native和Expo都会照顾捆绑的细节。

1.1安装expo GO(或使用模拟器)

您应该在这些设备上安装"Expo Go"应用程序,因为当我们预览开发构建时,这将是一个巨大的帮助。您可以通过搜索“ expo go”或直接在这些链接下在商店中找到该应用程序:

博览会去iOS

‎Expo Go on the App Store

仅使用您的iOS设备和计算机开始使用Web技术构建项目。 Expo是使用JavaScript和React创建交互式手势和图形的经验的开发工具。 注意:建议一些编程经验。 技术规格:这个版本的Expo US!

apps.apple.com

博览会去Android

Expo - Apps on Google Play

博览会是一个免费的开源平台,可使用JavaScript和React构建应用程序。

favicon play.google.com

如果其中一个链接无法正常工作,您也可以get them from the Expo website

注意,即使看上去像是乍一看,您也不必注册使用Expo Go应用程序。

安装仿真器作为替代方案

如果您确实无法访问实体设备,或者在此研讨会期间遇到设备的某些问题,则可能会考虑安装各自的仿真器。

devs为您提供了有关安装Android emulatoriOS simulator的综合指南。

1.2安装流星

您可以通过NPM在各种OS'和平台上安装流星(如果您已经安装了节点> = 14,并且已经安装了NPM)

$ npm install -g meteor

或在Linux或MacOS上使用他们提供的Shell脚本:

$ curl https://install.meteor.com/ | sh

如果要在带有M1,Docker容器或Windows的Mac上安装它,请read the details from the official installation guide

重要
在此研讨会中,您必须从现在开始在终端中使用meteor npm,而不仅仅是npm。有必要始终使用正确的链接二进制文件。
这是由于流星工具的船舶带有捆绑的NPM,该NPM在您的系统上具有自己的路径。删除流星后,它将被删除,包括所有已安装的依赖。

通过
验证流星,它是npm

$ meteor --version
$ meteor npm -v

1.3安装Expo CLI

没有博览会,反应本地发展是完全有可能的。但是,有了博览会,它才开始感到舒适。不用担心构建,没有安装Android SDK或XCode。地狱,您可以在Linux机器上为iOS制造开发构建。那时他们已经得到了我。

ð通知

但是,还有一些缺点。首先,您必须坚持使用SDK并定期更新SDK。
其次,如果您想为应用商店 /播放商店部署应用程序,则可能需要使用其cloud services (EAS)。尽管他们提供了免费的层,但它可能会引入成本,最后,这仍然是对您部署的第三方的依赖。不过,好的一面是,此服务负责移动应用程序部署的真正令人讨厌的一面,因此可能还不错。

足够的谈话,让我们在全球安装Expo CLI工具:

$ meteor npm install -g expo-cli

重要
这会导致CLI仅在流星名称空间中可用,因此您需要在以后致电meteor expo,而不是仅在。

1.4创建项目存储库或本地文件夹

安装工具后,我们实际上可以创建新项目。为了跟踪所有内容,您可以创建一个新的空GitHub存储库,并在本地克隆它,或者创建一个本地文件夹,以后您将其与现有存储库连接。从这两种方面,让我们命名该研讨会的项目mrntodos

1.4.一个从github通过SSH克隆的克隆

$ git clone git@github.com:myusername/mrntodos.git

1.4.b通过https从github克隆

$ git clone https://github.com/myusername/mrntodos.git

1.4.C创建本地文件夹

$ mkdir -p mrntodos

Meteor icon

2.创建并设置流星后端

创建一个新的流星项目是直接使用meteor create [options] [name]的。但是,有了流星,我们有许多选择的前端选择:反应,vue,svelte,solid(以2.8为2.8),大火,阿波罗,无头等(read the docs for all options)。流星将它们自动在项目创建上自动集成。

ð通知
“后端”一词在这里有些误导。流星应用程序是一个Fullstack应用程序,为您提供服务器和客户端构建。但是,对于本研讨会,我们将仅使用服务器环境,使流星应用程序作为移动应用程序的“后端”。

2.1创建一个新的流星应用程序

对于研讨会,我们使用流星的默认create命令(使用react作为前端):

$ cd mrntodos # if you are not already in the project folder
$ meteor create backend
$ cd backend
$ echo "{}" > settings.json

我们可以使用json文件(backend/settings.json)在流星中配置全局应用程序设置,该文件被starting meteor with koude13注入过程环境。当您必须管理其配置不同的多个部署时,它非常方便。

为了使流星应用程序自动从自定义端口开始并使用我们的settings.json,让我们更改backend/package.json中的scripts之一:

...
"start": "meteor --port=8000 --settings=setting.json"

现在通过
启动流星应用

$ meteor npm start

如果您看到以下消息输出,一切都很好,您可以继续到下一节:

=> App running at: http://localhost:8000/

2.2将身份验证层添加到流星后端

流星提供了零至最小值身份验证的核心软件包,名为Accounts。使用accounts-password,您将获得开箱即用的OAuth2身份验证,服务器还使用bcrypt来加密服务器上的密码。

帐户还集成了third-party authentication with Facebook, Twitter, Apple, GitHub或您的custom OAuth2 provider。但是,在此研讨会中,我们使用accounts-passwords
添加和配置最基本的身份验证方法

$ meteor add accounts-password

帐户也是可配置的(documentation),我们应该根据需要更改其默认值。为此,请在backend/imports/startup/server/accounts.js下添加一个新文件(如果不存在,则创建这些文件夹)。然后将以下内容添加到此文件:

import { Accounts } from 'meteor/accounts-base'
import { Meteor } from 'meteor/meteor'

// Here we define the fields that are automatically 
// available to clients via Meteor.user().
// This extends the defaults (_id, username, emails) 
// by our profile fields.
// If you want your custom fields to be immediately 
// available then place them here.
const defaultFieldSelector = {
  _id: 1,
  username: 1,
  emails: 1,
  firstName: 1,
  lastName: 1
}


// merge our config from settings.json with fixed code
// and pass them to Accounts.config
Accounts.config({ 
  ...Meteor.settings.accounts.config,
  defaultFieldSelector 
})

现在,将以下部分添加到您的settings.json文件:

{
  "accounts": {
    "config": {
      "forbidClientAccountCreation": true,
      "ambiguousErrorMessages": true,
      "sendVerificationEmail": true,
      "loginExpirationInDays": null
    }
  }
}

所有parameters of the configuration are documented,以防您要进一步调查它们。

ð通知
添加accounts-password时,将自动添加accounts-base软件包。在代码中导入Accounts时,您将使用import { Accounts } from 'meteor/accounts-base'

最后,确保我们的启动文件在backend/server/main.js中导入。打开此文件并用以下方式替换其默认内容

import '../imports/startup/server/accounts'

2.3添加注册方法端点

默认情况下,流星帐户允许客户通过Accounts.createUserdocs)注册自己。这也是@meteorrn/core软件包中支持的1:1。

但是,我们希望在注册过程中具有其他功能,例如发送注册电子邮件并向用户文档添加默认配置文件字段。

因此,我们创建一个新的注册端点作为流星方法。我们在backend/imports/accounts/methods.js中创建一个新文件,然后添加以下代码:

import { Accounts } from 'meteor/accounts-base'
import { check, Match } from 'meteor/check'

export const registerNewUser = function (options) {
  check(options, Match.ObjectIncluding({
    email: String,
    password: String,
    firstName: String,
    lastName: String,
    loginImmediately: Match.Maybe(Boolean)
  }))

  const { email, password, firstName, lastName, loginImmediately } = options

  if (Accounts.findUserByEmail(email)) {
    throw new Meteor.Error('permissionDenied', 'userExists', { email })
  }

  const userId = Accounts.createUser({ email, password })

  // we add the firstName and lastName as toplevel fields
  // which allows for better handling in publications
  Meteor.users.update(userId, { $set: { firstName, lastName } })

  // let them verify their new account, so
  // they can use the full app functionality
  Accounts.sendVerificationEmail(userId, email)

  if (loginImmediately) {
    // signature: { id, token, tokenExpires }
    return Accounts._loginUser(this, userId)
  }

  // keep the same return signature here to let clients
  // better handle the response
  return { id: userId, token: undefined, tokenExpires: undefined }
}

最后,通过在我们已经创建的backend/imports/startup/server/accounts.js文件中创建新的Meteor方法来确保在启动中导入此文件:

import { Accounts } from 'meteor/accounts-base'
import { Meteor } from 'meteor/meteor'
import { registerNewUser } from '../../accounts/methods'

Accounts.config(Meteor.settings.accounts.config)
Meteor.methods({ registerNewUser })

2.6添加更多端点以管理帐户

如果您还希望用户更新其个人资料或删除其帐户,则您也需要为他们提供端点方法。

让我们通过这两种方法扩展backend/imports/accounts/methods.js文件:

// ... registerNewUser

export const updateUserProfile = function ({ firstName, lastName }) {
  check(firstName, Match.Maybe(String))
  check(lastName, Match.Maybe(String))

  // in a meteor Method we can access the current user
  // via this.userId which is only present when an
  // authenticated user calls a Method
  const { userId } = this

  if (!userId) {
    throw new Meteor.Error('permissionDenied', 'notAuthenticated', { userId })
  }

  const updateDoc = { $set: {} }

  if (firstName) {
    updateDoc.$set.firstName = firstName
  }

  if (lastName) {
    updateDoc.$set.lastName = lastName
  }

  return !!Meteor.users.update(userId, updateDoc)
}

export const deleteAccount = function () {
  const { userId } = this

  if (!userId) {
    throw new Meteor.Error('permissionDenied', 'notAuthenticated', { userId })
  }

  return !!Meteor.users.remove(userId)
}

最后,backend/imports/startup/server/accounts.js上更新启动文件:

import { Accounts } from 'meteor/accounts-base'
import { Meteor } from 'meteor/meteor'
import { 
  registerNewUser,
  updateUserProfile,
  deleteAccount
 } from '../../accounts/methods'

// ... other code

Meteor.methods({
  registerNewUser,
  deleteAccount,
  updateUserProfile
})

此时,您的应用程序定义了最小的API,可以注册,登录,登录(内置,通过Meteor.logout()),删除帐户和更新配置文件。让我们立即构建移动应用程序。


react logo

3.创建并设置React Native应用程序

在本节中,我们将创建一个带有博览会管理工作流程的新React Native应用程序。确保您已经设置了第1节中的所有必需工具。如果您没有物理设备,也可以安装Android模拟器或iOS模拟器(也介绍在第1节中)。

3.1创建新的React本机应用程序

首先,打开一个新的终端,以安装和运行应用程序。后端和应用程序将使用单独的节点进程。因此,要有效地管理它们,您应该在单独的终端中处理它们。

如果您尚未安装expo-cli 或您已经更新了流星版本,则需要通过:

$ meteor npm install -g expo-cli
$ meteor expo --version
6.0.6 # at the time of this workshop

在项目根文件夹(mrntodos/)中,创建一个新的博览会项目,并回答以下问题:

$ cd mrntodos # if not already there
$ meteor expo init
? What would you like to name your app? › app
? Choose a template: › - Use arrow-keys. Return to submit.
    ----- Managed workflow -----
❯   blank a minimal app as clean as an empty canvas

ð请注意,您还可以使用不同的工作流程,例如,如果您喜欢使用Typescript。独自选择或坚持此研讨会的偏好。

然后,我们还需要在研讨会期间在这里安装一些依赖项:

$ cd app
$ meteor expo install @meteorrn/core @react-navigation/native @react-navigation/native-stack @react-navigation/stack expo-secure-store expo-status-bar react-native-screens react-native-safe-area-context react-native-gesture-handler

重要
我们需要使用meteor expo install来安装我们的依赖项。这是因为Expo为我们解决当前SDK的正确依赖性版本。通过这样做,我们无需摆弄可能破坏我们的构建或从根本上不兼容的包装版本。

添加包装后,您可能会从npm audit中收到一些警告。如果您像我一样关心,请帮助我们通过测试您本地构建的最新提交,以更新的依赖关系发布下一个版本的@meteorrn/core 。将您的评论或问题添加到:https://github.com/meteorrn/meteor-react-native

3.2设置正确的网络设置

将应用程序连接到后端需要进一步的配置。您需要获取本地网络IP 才能使RN应用程序连接。流星典型的localhost在这里无法使用,因为当代码在移动设备上运行时,这将无法解析为同一本地IP。

您通常可以通过以下命令之一获取本地IP:

OS
命令
linux ip addr show
macOS ifconfig
Windows ipconfig

获得IP后(在家中通常是192.168.178.XXX或类似),请创建一个带有此值的配置文件:

$ cd app # if not already there
$ echo "{}" > config.json

将以下内容放在其中(xxx.xxx.xxx.xxx被您获得的本地IP替换):

{
  "backend": {
    "url": "ws://xxx.xxx.xxx.xxx:8000/websocket"
  }
}

在这一点上,让我们第一次运行该应用程序让Expo通过以下方式生成一些文件:

$ meteor npm start

使用您的移动设备,并使用Expo Go应用程序扫描QR码。您现在应该看到捆绑正在进行中:

bundling in progress image

完成此操作后,您的设备应在app/App.js中显示默认内容:

initial screen

运行后,检查新创建的app/.expo/settings.json文件,并确保其看起来如下:

{
  "hostType": "lan",
  "lanType": "ip",
  "dev": true,
  "minify": false,
  "urlRandomness": "mc-y7b",
  "https": false,
  "scheme": null,
  "devClient": false
}

3.3将爱马仕设置为JavaScript引擎

我们可以使用名为Hermes的JavaScript引擎,该引擎完全针对React Native进行了优化。实际上,我们应该。咨询React Native docs page on Hermes以了解原因。

为了使用Expo激活它,我们需要在app/app.json中编辑设置:

 "expo": {
    "name": "app",
    "slug": "app",
    "version": "1.0.0",
    "assetBundlePatterns": [
      "**/*"
    ],
    "jsEngine": "hermes"
  }

这就是全部,博览会为我们服务。


4.将应用程序连接到流星后端

我们应用程序的几乎所有功能都需要与我们的后端建立联系。 Meteor通过DDP, a custom protocol建立连接并交换数据,该数据建立在WebSocket上。很棒的是,您不必担心整个运输层,因为流星为您抽象所有这些逻辑,因此您可以专注于重要的事情。

4.1写一个连接钩

我们希望在连接和处理状态的方式上保持灵活性。这就是为什么我们将其抽象成自定义的React hook,我们将其命名为useConnection

app/src/hooks/useConnection.js上创建一个新文件,并在此处添加以下代码:

import { useEffect, useState } from 'react'
import Meteor from '@meteorrn/core'
import * as SecureStore from 'expo-secure-store'
import config from '../../config.json'

// get detailed info about internals
Meteor.isVerbose = true

// connect with Meteor and use a secure store
// to persist our received login token, so it's encrypted
// and only readable for this very app
// read more at: https://docs.expo.dev/versions/latest/sdk/securestore/
Meteor.connect(config.backend.url, {
  AsyncStorage: {
    getItem: SecureStore.getItemAsync,
    setItem: SecureStore.setItemAsync,
    removeItem: SecureStore.deleteItemAsync
  }
})

export const useConnection = () => {
  const [connected, setConnected] = useState(null)
  const [connectionError, setConnectionError] = useState(null)

  // we use separate functions as the handlers, so they get removed
  // on unmount, which happens on auto-reload and would cause errors
  // if not handled
  useEffect(() => {
    const onError = (e) => setConnectionError(e)
    Meteor.ddp.on('error', onError)

    const onConnected = () => connected !== true && setConnected(true)
    Meteor.ddp.on('connected', onConnected)

    // if the connection is lost, we not only switch the state
    // but also force to reconnect to the server
    const onDisconnected = () => {
      Meteor.ddp.autoConnect = true
      if (connected !== false) {
        setConnected(false)
      }
      Meteor.reconnect()
    }
    Meteor.ddp.on('disconnected', onDisconnected)

    // remove all of these listeners on unmount
    return () => {
      Meteor.ddp.off('error', onError)
      Meteor.ddp.off('connected', onConnected)
      Meteor.ddp.off('disconnected', onDisconnected)
    }
  }, [])

  return { connected, connectionError }
}

经典的流星 +反应提供了一个useTracker钩子,以实现反应性。在这种情况下,我们直接将其连接到DDP事件中以更新连接状态。

4.3将useConnection钩集成到App.js

让我们向上移动两个级别并打开app/App.js,这是我们的React应用程序的主要切入点。在这里,我们现在基于连接状态集成了useConnection钩子和渲染消息:

import React from 'react'
import { MainNavigator } from './src/screens/MainNavigator'
import { StyleSheet, View, Text, ActivityIndicator } from 'react-native'
import { useConnection } from './src/hooks/useConnection'

export default function App () {
  const { connected, connectionError } = useConnection()

  // use splashscreen here, if you like
  if (!connected) {
    return (
      <View style={styles.container}>
        <ActivityIndicator />
        <Text>Connecting to our servers...</Text>
      </View>
    )
  }

  // use alert or other things here, if you like
  if (connectionError) {
    return (
      <View style={styles.container}>
        <Text>Error, while connecting to our servers!</Text>
        <Text>{connectionError.message}</Text>
      </View>
    )
  }

  return (
      <View style={styles.container}>
        <Text>We are connected!</Text>
      </View>
  )
}

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

启动应用程序,然后在屏幕上查看We are connected!消息。现在,切换到另一个终端以进行我们的后端,然后击中ctrl+c以取消该过程。看看您的设备 - 它将在尝试重新连接时立即显示Connecting to our servers...。现在,再次启动后端服务器,然后查看即时更改回连接的状态。

connecting to servers

您现在有一个反应性连接状态ð¥³您可以进一步使用此钩子来构建一个脱机的应用程序,但我们的研讨会不会涵盖。

从这里我们可以继续进行身份验证工作流程,但是在此之前,我们需要做简短的重构!

4.4整理代码

我们将在此应用程序中再重复使用几次元素。不重复再次编写相同的代码是一个很好的做法。

一个潜在的节省是创建默认样式表,您可以将其与网络上的main.css文件进行比较。它有助于在我们的应用程序上提供一致的布局,并有一个来管理此布局的点。

让我们在app/src/styles/defaultStyles.js上创建一个新文件,然后在此处放置以下内容:

import { StyleSheet } from 'react-native'

export const defaultColors = {
  placeholder: '#8a8a8a',
  danger: '#981111',
  white: '#eee',
  black: '#1a1a1a',
  primary: '#0B52AF'
}

export const defaultStyles = StyleSheet.create({
  text: {
    height: 40,
    margin: 12,
    borderWidth: 1,
    padding: 10,
    alignSelf: 'stretch',
    color: defaultColors.black,
    backgroundColor: defaultColors.white
  },
  panel: {
    margin: 20
  },
  container: {
    margin: 20,
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center'
  },
  danger: {
    color: defaultColors.danger
  },
  dangerBorder: {
    borderWidth: 1,
    borderColor: defaultColors.danger
  },
  bold: {
    fontWeight: 'bold'
  },
  row: {
    flexDirection: 'row',
    alignItems: 'center'
  },
  flex1: {
    flex: 1
  }
})

第二,让我们创建一个新的ErrorMessage组件。它还将使用一些默认样式。在app/src/components/ErrorMessage.js上创建新文件,然后向其添加以下代码:

import React from 'react'
import { Text, View } from 'react-native'
import { defaultStyles } from '../styles/defaultStyles'

export const ErrorMessage = ({ error, message }) => {
  if (!error && !message) { return null }

  return (
    <View style={defaultStyles.container}>
      <Text style={defaultStyles.danger}>{message || error.message}</Text>
    </View>
  )
}

使用这两个抽象,我们可以再次更新app/App.js并替换

// use alert or other things here, if you like
if (connectionError) {
  return (
    <View style={styles.container}>
      <Text>Error, while connecting to our servers!</Text>
      <Text>{connectionError.message}</Text>
    </View>
  )
}


// use alert or other things here, if you like
if (connectionError) {
  return (<ErrorMessage error={connectionError} />)
}

5.实施身份验证工作流程

我们应用程序中最重要的部分之一是提供流利且可访问的身份验证工作流程。用户不必每次使用该应用程序登录。而是应安全存储登录令牌(这就是为什么我们安装expo-secure-store)并使用直到到期(我们在第2节中使用Accounts.config定义)。

如果用户没有帐户,则应通过提供电子邮件,密码,名字和姓氏来注册。创建帐户后,他们应接收登录令牌并自动登录。最后,用户应该能够签署并删除其帐户。

以下图形从更抽象的角度汇总了auth工作流程,包括要导航的屏幕:

authentication workflow

此工作流程受React Nativation authentication workflow的启发,我们将使用此库在我们自己的环境边界内实现工作流程。

5.1创建身份验证上下文和API层

与连接一样,我们希望在React中尽可能地将身份验证从渲染逻辑中解脱出来。这也有助于使项目与任何流星逻辑保持紧密结合。同时,我们希望拥有一个统一的位置,以管理身份验证状态。

5.1.1创建一个新的身份验证上下文

React上下文提供了一种访问我们的验证层的方法,而无需将其传递到整个组件树中,并有助于防止紧密的耦合。在React docs on contexts中阅读更多信息,如果您想了解其详细信息的工作方式。

由于将有多个屏幕,可以利用我们的身份验证,因此我们在app/src/contexts/AuthContext.js上创建和导出我们的身份验证上下文:

import { createContext } from 'react'

export const AuthContext = createContext()

5.2.2创建一个useAuth钩子

整个“魔术”将在这里发生。在这里,由于DDP请求到后端,来自组件的调用将通过,并且响应将相应地更新状态。相反,错误将传递回组件以使其呈现错误消息。

由于所有这些导致了更复杂的状态,我们应该使用React reducer。我们的身份验证功能也应仅创建一次,这就是为什么我们将它们包裹在useMemo钩中。

创建文件app/src/hooks/useAuth.js并添加以下内容:

import { useReducer, useEffect, useMemo } from 'react'
import Meteor from '@meteorrn/core'

const initialState = {
  isLoading: true,
  isSignout: false,
  userToken: null
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'RESTORE_TOKEN':
      return {
        ...state,
        userToken: action.token,
        isLoading: false
      }
    case 'SIGN_IN':
      return {
        ...state,
        isSignOut: false,
        userToken: action.token
      }
    case 'SIGN_OUT':
      return {
        ...state,
        isSignout: true,
        userToken: null
      }
  }
}

const Data = Meteor.getData()


export const useAuth = () => {
  const [state, dispatch] = useReducer(reducer, initialState, undefined)

  // Case 1: restore token already exists
  // MeteorRN loads the token on connection automatically,
  // in case it exists, but we need to "know" that for our auth workflow
  useEffect(() => {
    const handleOnLogin = () => dispatch({ type: 'RESTORE_TOKEN', token: Meteor.getAuthToken() })
    Data.on('onLogin', handleOnLogin)
    return () => Data.off('onLogin', handleOnLogin)
  }, [])


  const authContext = useMemo(() => ({
    signIn: ({ email, password, onError }) => {
      Meteor.loginWithPassword(email, password, async (err) => {
        if (err) {
          if (err.message === 'Match failed [400]') {
            err.message = 'Login failed, please check your credentials and retry.'
          }
          return onError(err)
        }
        const token = Meteor.getAuthToken()
        const type = 'SIGN_IN'
        dispatch({ type, token })
      })
    },
    signOut: ({ onError }) => {
      Meteor.logout(err => {
        if (err) {
          return onError(err)
        }
        dispatch({ type: 'SIGN_OUT' })
      })
    },
    signUp: ({ email, password, firstName, lastName, onError }) => {
      const signupArgs = { email, password, firstName, lastName, loginImmediately: true }

      Meteor.call('registerNewUser', signupArgs, (err, credentials) => {
        if (err) {
          return onError(err)
        }

        // this sets the { id, token } values internally to make sure
        // our calls to Meteor endpoints will be authenticated
        Meteor._handleLoginCallback(err, credentials)

        // from here this is the same routine as in signIn
        const token = Meteor.getAuthToken()
        const type = 'SIGN_IN'
        dispatch({ type, token })
      })
    },
    deleteAccount: ({ onError }) => {
      Meteor.call('deleteAccount', (err) => {
        if (err) {
          return onError(err)
        }

        // removes all auth-based data from client
        // as if we would call signOut
        Meteor.handleLogout()
        dispatch({ type: 'SIGN_OUT' })
      })
    }
  }), [])

  return { state, authContext }
}

ð请注意,此文件当然需要一些解释。

  • reducer处理内部状态,通过dispatch调用操纵
  • @meteorrn/core软件包将(一旦连接)自动检查提供的安全存储中的现有令牌;如果发现,它将尝试使用令牌登录并发出“ Onlogin”事件,如果成功的话;我们可以在useEffect钩中利用这一点来实现我们的“自动login”功能
  • authContext在以前创建的AuthContextuseContext的屏幕中检索到您将在即将到来的部分中看到的
  • 几个功能signInsignOutsignUpdeleteAccounts主要利用@meteorrn/core软件包在Meteor.loginWithPasswordMeteor.logout中内部使用的功能82
  • 如果您需要迁移流星,可以完全替换他们的代码(我希望您不要)

5.2创建屏幕

在以下步骤中,我们将创建工作流中涉及的所有必要屏幕。

所有屏幕都应在app/src/screens中创建,您可以通过
创建该屏幕

$ cd app # if not already in app folder
$ mkdir -p src/screens

5.2.1房屋屏幕

app/src/screens/HomeScreen.js的主屏幕现在只包含一条消息:

import React from 'react'
import { View, Text } from 'react-native'
import { defaultStyles } from '../styles/defaultStyles'

export const HomeScreen = () => {
  return (
    <View style={defaultStyles.container}>
      <Text>Welcome home!</Text>
    </View>
  )
}

5.2.2登录屏幕

登录屏幕提供了一个简单的电子邮件和密码输入,密码字段使用secureTextEntry隐藏字符。

如果身份验证失败,则应显示一个错误消息。如果用户尚未帐户,则“注册”按钮应触发导航以注册:

import React, { useState, useContext } from 'react'
import { View, Text, TextInput, Button } from 'react-native'
import { AuthContext } from '../contexts/AuthContext'
import { defaultColors, defaultStyles } from '../styles/defaultStyles'
import { ErrorMessage } from '../components/ErrorMessage'

export const LoginScreen = ({ navigation }) => {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState(null)
  const { signIn } = useContext(AuthContext)

  // handlers
  const onError = err => setError(err)
  const onSignIn = () => signIn({ email, password, onError })
  const onSignUp = () => navigation.navigate('SignUp')

  // render login form
  return (
    <View style={defaultStyles.container}>
      <TextInput
        placeholder='Your Email'
        placeholderTextColor={defaultColors.placeholder}
        style={defaultStyles.text}
        value={email}
        onChangeText={setEmail}
      />
      <TextInput
        placeholder='Password'
        placeholderTextColor={defaultColors.placeholder}
        style={defaultStyles.text}
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <ErrorMessage error={error} />
      <Button title='Sign in' color={defaultColors.primary} onPress={onSignIn} />
      <View style={defaultStyles.panel}>
        <Text>or</Text>
      </View>
      <Button title='Sign up' onPress={onSignUp} color={defaultColors.placeholder} />
    </View>
  )
}

ð请注意两件事。

首先,signIn方法将由useContext提供,其中AuthContext是使react返回正确值的“键”。其次,组件道具将包含一个navigation属性,当我们使用React Navigation时,它总是会注入。

5.2.3注册屏幕

寄存器屏幕提供了类似的表格,但将使用其他验证方法:

import React, { useContext, useState } from 'react'
import { TextInput, Button, View } from 'react-native'
import { defaultColors, defaultStyles } from '../styles/defaultStyles'
import { AuthContext } from '../contexts/AuthContext'
import { ErrorMessage } from '../components/ErrorMessage'

export const RegistrationScreen = () => {
  const [email, setEmail] = useState()
  const [firstName, setFirstName] = useState()
  const [lastName, setLastName] = useState()
  const [password, setPassword] = useState()
  const [error, setError] = useState()
  const { signUp } = useContext(AuthContext)

  const onError = err => setError(err)
  const onSignUp = () => signUp({ email, password, firstName, lastName, onError })

  return (
    <View style={defaultStyles.container}>
      <TextInput
        placeholder='Your Email'
        placeholderTextColor={defaultColors.placeholder}
        style={defaultStyles.text}
        value={email}
        onChangeText={setEmail}
      />
      <TextInput
        placeholder='Your password'
        placeholderTextColor={defaultColors.placeholder}
        style={defaultStyles.text}
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <TextInput
        placeholder='Your first name (optional)'
        placeholderTextColor={defaultColors.placeholder}
        style={defaultStyles.text}
        value={firstName}
        onChangeText={setFirstName}
      />
      <TextInput
        placeholder='Your last name (optional)'
        placeholderTextColor={defaultColors.placeholder}
        style={defaultStyles.text}
        value={lastName}
        onChangeText={setLastName}
      />
      <ErrorMessage error={error} />
      <Button title='Create new account' onPress={onSignUp} />
    </View>
  )
}

5.2.4 profilescreen

一旦经过身份验证,配置文件屏幕才能可用。目前,我们仅在此屏幕上创建登录和删除字幕功能。

import { defaultColors, defaultStyles } from '../styles/defaultStyles'
import { Button, Text, View } from 'react-native'
import { useState } from 'react'
import { ErrorMessage } from '../components/ErrorMessage'

export const ProfileScreen = () => {
  const [error, setError] = useState(null)
  const handleSignOut = () => console.log('sign out')
  const handleDelete = () => console.log('delete account')


  return (
    <View style={defaultStyles.container}>
      <View style={{ ...defaultStyles.dangerBorder, padding: 10, marginTop: 10, alignSelf: 'stretch' }}>
        <Text style={defaultStyles.bold}>Danger Zone</Text>
        <Button title='Sign out' color={defaultColors.danger} onPress={handleSignOut} />
        <Button title='Delete account' color={defaultColors.danger} onPress={handleDelete} />
        <ErrorMessage error={error} />
      </View>
    </View>
  )
}

5.3实施导航

您可能已经意识到屏幕上没有“取消”或“背面”按钮,也没有呈现“标题”。这一切都将由我们的React Navigation库来处理。它使我们能够保持屏幕相互连接的方式灵活。

最重要的是,它有助于我们根据我们的身份验证状态呈现不同的屏幕,并提供有关如何渲染导航栏的选项。

5.3.1创建主要导航

让我们在app/src/screens/MainNavigator.js上创建导航,并添加以下代码:

import React from 'react'
import { CardStyleInterpolators } from '@react-navigation/stack'
import { AuthContext } from '../contexts/AuthContext'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { useAuth } from '../hooks/useAuth'
import { HomeScreen } from './HomeScreen'
import { LoginScreen } from './LoginScreen'
import { RegistrationScreen } from './RegistrationScreen'
import { ProfileScreen } from './ProfileScreen'
import { NavigateButton } from '../components/NavigateButton'

const Stack = createNativeStackNavigator()

export const MainNavigator = () => {
  const { state, authContext } = useAuth()
  const { userToken } = state

  const renderScreens = () => {
    if (userToken) {
      // only authenticated users can visit these screens
      const headerRight = () => (<NavigateButton title='My profile' route='Profile' />)
      return (
        <>
          <Stack.Screen name='Home' component={HomeScreen} options={{ title: 'Welcome home', headerRight }} />
          <Stack.Screen name='Profile' component={ProfileScreen} options={{ title: 'Your profile' }} />
        </>
      )
    }

    // non authenticated users need to sign in or register
    // and can only switch between the two screens below:
    return (
      <>
        <Stack.Screen
          name='SignIn'
          component={LoginScreen}
          options={{ title: 'Sign in to awesome-app' }}
        />
        <Stack.Screen
          name='SignUp'
          component={RegistrationScreen}
          options={{ title: 'Register to awesome-app' }}
        />
      </>
    )
  }

  return (
    <AuthContext.Provider value={authContext}>
      <NavigationContainer>
        <Stack.Navigator screenOptions={{ cardStyleInterpolator: CardStyleInterpolators.forVerticalIOS }}>
          {renderScreens()}
        </Stack.Navigator>
      </NavigationContainer>
    </AuthContext.Provider>
  )
}

再次,此代码需要进一步的解释:

  • Stack是一个导航器,在来回导航时会呈现本机推送/流行动画。它看起来真的很好开箱即用
  • stateuseAuthreducer的当前状态,而authContext通过<AuthContext.Provider value={authContext}>注入了我们的组件树;没有这个
  • userToken才存在,当@meteorrn/core收到令牌或从安全商店中加载它并与之成功签名

5.3.2创建导航按钮组件

添加导航后,请在app/src/components/NavigateButton.js上添加一个新的NavigateButton组件,然后向其添加以下代码:

import { Button } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { defaultColors } from '../styles/defaultStyles'

export const NavigateButton = ({ title, route }) => {
  const navigation = useNavigation()

  return (
    <Button
      title={title}
      color={defaultColors.primary}
      onPress={() => navigation.navigate(route)}
    />
  )
}

它可以帮助我们避免通过整个组件树传递navigation并在内部处理路由。

5.3.3将导航集成到App.js

如果我们在此阶段启动该应用程序,就不会有新的。这是因为我们需要将MainNavigation集成到我们的App中。因此,将app/App.js更新为以下最终代码:

import React from 'react'
import { MainNavigator } from './src/screens/MainNavigator'
import { View, Text, ActivityIndicator } from 'react-native'
import { useConnection } from './src/hooks/useConnection'
import { ErrorMessage } from './src/components/ErrorMessage'
import { defaultStyles } from './src/styles/defaultStyles'

export default function App () {
  const { connected, connectionError } = useConnection()

  // use splashscreen here, if you like
  if (!connected) {
    return (
      <View style={defaultStyles.container}>
        <ActivityIndicator />
        <Text>Connecting to our servers...</Text>
      </View>
    )
  }

  // use alert or other things here, if you like
  if (connectionError) {
    return (<ErrorMessage error={connectionError} />)
  }

  return (<MainNavigator />)
}

5.4测试所有工作流程

此时,您应该能够运行该应用程序并测试整个身份验证工作流程。这是一些期望的屏幕截图:

5.4.1登录屏幕

plain on Enter:

sign in screen

登录失败时:

sign in failed

5.4.2注册屏幕

register screen

5.4.3房屋屏幕

home screen

5.4.4 profilescreen

profile screen

5.5添加最小用户配置文件

此步骤提供了对从流星后端处理数据反应性的重要见解,以使其与渲染相关。

我们想更新用户的配置文件(现在只是firstNamelastName字段),并立即反映这些更改而无需手动获取更新的配置文件

5.5.1创建一个useAccount钩子

为了做到这一点,我们首先创建了一个新的钩子,在app/src/hooks/useAccount.js上名为useAccount并添加以下代码:

import Meteor from '@meteorrn/core'
import { useMemo, useState } from 'react'

const { useTracker } = Meteor

export const useAccount = () => {
  const [user, setUser] = useState(Meteor.user())

  useTracker(() => {
    const reactiveUser = Meteor.user()
    if (reactiveUser !== user) {
      setUser(reactiveUser)
    }
  })

  const api = useMemo(() => ({
    updateProfile: ({ options, onError, onSuccess }) => {
      Meteor.call('updateUserProfile', options, (err) => {
        return err
          ? onError(err)
          : onSuccess()
      })
    }
  }), [])

  return { user, ...api }
}

使用此钩子,我们创建了一种与我们的auth上下文相似(但不同)的方法。但是,在这种情况下,我们要保持updateProfile功能靠近用户配置文件,因此不会在此处使用上下文。

注意useTracker,这是流星反应性模型的基本支柱之一。结合出版/订阅,我们可以在服务器在DB级上更新时接收更新的用户配置文件。

ð请注意,您尚未订阅任何出版物,但是在服务器上更改时,用户文档会自动更新。这是由于流星总是自动发布对用户自己的文档的更改,以及为什么我们在第2节中为Accounts.config设置defaultPublishFields

5.5.2将useAccount钩集成到ProfileScreen

为了更新配置文件字段,我们需要提供另一种简单的表单并处理某些状态。我不会太深入这里的细节,因为这仅适用于干燥原理以具有多个字段的形式。

现在是更新的app/screens/ProfileScreen.js文件:

import { AuthContext } from '../contexts/AuthContext'
import { defaultColors, defaultStyles } from '../styles/defaultStyles'
import { Button, Text, TextInput, View, StyleSheet } from 'react-native'
import { useContext, useState } from 'react'
import { ErrorMessage } from '../components/ErrorMessage'
import { useAccount } from '../hooks/useAccount'

export const ProfileScreen = () => {
  const [editMode, setEditMode] = useState('')
  const [editValue, setEditValue] = useState('')
  const [error, setError] = useState(null)
  const { signOut, deleteAccount } = useContext(AuthContext)
  const { user, updateProfile } = useAccount()
  const onError = err => setError(err)

  if (!user) {
    return null // if sign our or delete
  }

  /**
   * Updates a profile field from given text input state
   * by sending update data to the server and let hooks
   * reactively sync with the updated user document. *magic*
   * @param fieldName {string} name of the field to update
   */
  const updateField = ({ fieldName }) => {
    const options = {}
    options[fieldName] = editValue
    const onSuccess = () => {
      setError(null)
      setEditValue('')
      setEditMode('')
    }
    updateProfile({ options, onError, onSuccess })
  }

  const renderField = ({ title, fieldName }) => {
    const value = user[fieldName] || ''

    if (editMode === fieldName) {
      return (
        <>
          <Text style={styles.headline}>{title}</Text>
          <View style={defaultStyles.row}>
            <TextInput
              placeholder={title}
              autoFocus
              placeholderTextColor={defaultColors.placeholder}
              style={{ ...defaultStyles.text, ...defaultStyles.flex1 }}
              value={editValue}
              onChangeText={setEditValue}
            />
            <ErrorMessage error={error} />
            <Button title='Update' onPress={() => updateField({ fieldName })} />
            <Button title='Cancel' onPress={() => setEditMode('')} />
          </View>
        </>
      )
    }

    return (
      <>
        <Text style={styles.headline}>{title}</Text>
        <View style={{ ...defaultStyles.row, alignSelf: 'stretch' }}>
          <Text style={{ ...defaultStyles.text, flexGrow: 1 }}>{user[fieldName] || 'Not yet defined'}</Text>
          <Button
            title='Edit' onPress={() => {
              setEditValue(value)
              setEditMode(fieldName)
            }}
          />
        </View>
      </>
    )
  }

  return (
    <View style={defaultStyles.container}>
      <Text style={styles.headline}>Email</Text>
      <Text style={{ ...defaultStyles.text, alignSelf: 'stretch' }}>{user.emails[0].address}</Text>

      {renderField({ title: 'First Name', fieldName: 'firstName' })}
      {renderField({ title: 'Last Name', fieldName: 'lastName' })}

      <Text style={styles.headline}>Danger Zone</Text>
      <View style={{ ...defaultStyles.dangerBorder, padding: 10, marginTop: 10, alignSelf: 'stretch' }}>
        <Button title='Sign out' color={defaultColors.danger} onPress={() => signOut({ onError })} />
        <Button title='Delete account' color={defaultColors.danger} onPress={() => deleteAccount({ onError })} />
        <ErrorMessage error={error} />
      </View>
    </View>
  )
}

const styles = StyleSheet.create({
  headline: {
    ...defaultStyles.bold,
    alignSelf: 'flex-start'
  }
})

6.带有CRUD功能的简单招待

现在,如果您打算走另一条路径并从这里继续使用自己的代码,这可能是研讨会的早期结束。但是,对于那些想要留下来并实施一个简单的应用程序的人,请紧紧挂。这只是另一个步骤。

6.1将任务功能添加到后端

我们应用程序功能的核心始终是后端提供的。因此,我们首先在后端定义哪些方法和出版物实际上可用。

6.1.1添加一个新任务集合

我们首先创建一个新的Mongo集合,该集合仅在流星中,只有一行代码。将以下代码添加到backend/imports/tasks/TasksCollection.js

import { Mongo } from 'meteor/mongo'

export const TasksCollection = new Mongo.Collection('tasks')

6.1.2添加任务方法终点和出版物

客户端应该能够创建新任务,更新其checked状态或删除它们。数据获取应通过发布/订阅来实现。

一个简单的实现可以查看以下backend/imports/tasks/methods.js文件:

import { Meteor } from 'meteor/meteor'
import { TasksCollection } from './TasksCollection'

export const getMyTasks = function () {
  const userId = this.userId
  checkUser(userId)
  return TasksCollection.find({ userId })
}

export const insertTask = function ({ text }) {
  const userId = this.userId
  checkUser(userId)
  const checked = false
  const createdAt = new Date()
  return TasksCollection.insert({ text, userId, checked, createdAt })
}

export const checkTask = function ({ _id, checked }) {
  const userId = this.userId
  checkUser(userId)
  return TasksCollection.update({ _id, userId }, { $set: { checked } })
}

export const removeTask = function ({ _id }) {
  const userId = this.userId
  checkUser(userId)
  return TasksCollection.remove({ _id, userId })
}

const checkUser = userId => {
  if (!userId) {
    throw new Meteor.Error('permissionDenied', 'notSignedIn', { userId })
  }
}

6.1.3将任务添加到启动

首先,在新文件中注册方法和出版物,backend/imports/startup/server/tasks.js

import { Meteor } from 'meteor/meteor'
import { checkTask, insertTask, getMyTasks, removeTask } from '../../tasks/methods'

Meteor.methods({
  'tasks.insert': insertTask,
  'tasks.setIsChecked': checkTask,
  'tasks.remove': removeTask
})

Meteor.publish('tasks.my', getMyTasks)

最后,确保此文件已在backend/server/main.js中导入:

import '../imports/startup/server/accounts'
import '../imports/startup/server/tasks'

在这一点

6.2将任务功能添加到应用程序

本节涉及与任务的后端和渲染的通信。它包含的代码比上一个代码更多。但是,我们将仅引入一个新概念,即Meteor.subscribe机制。除此之外,这是您现在已经知道的一切。

6.2.1安装复选框组件

在此研讨会之前,React Antial包含自己的Checkbox组件,现在已弃用。尽管有多个UI组件系统提供出色的复选框,但让我们坚持使用当前的环境并使用一个Expo提供:

$ meteor expo install expo-checkbox

6.2.2添加客户Mongo Collection

流星的核心方面之一是同构 - 某些概念在客户端上与服务器上实现相同的API和行为。数据处理也是如此,这是由“ Minimongo”与服务器Mongo集合的轻量级对应物所实现的。它是流星发布订阅系统的核心部分,也是数据反应性的来源。

为了利用这些效果,必须以与服务器出版物中的集合相同的方式命名客户收集。因此,将以下代码添加到新文件app/src/tasks/TasksCollection.js

import { Mongo } from '@meteorrn/core'

export const TasksCollection = new Mongo.Collection('tasks')

6.2.3创建任务组件

由于我们正在创建一个任务列表,因此最好使用自己的组件呈现每个任务。它应该显示任务的文本和检查状态,并提供一种向父母传递某些动作的方法。对于复选框,我们将使用先前安装的expo-checkbox

创建一个带有以下内容的新文件app/src/tasks/Task.js

import React from 'react'
import { View, Text, Button, StyleSheet } from 'react-native'
import Checkbox from 'expo-checkbox'
import { defaultColors } from '../styles/defaultStyles'

export const Task = ({ task, onCheckboxClick, onDeleteClick }) => {
  const handleCheck = (checked) => onCheckboxClick({ _id: task._id, checked })

  return (
    <View style={{ display: 'flex', flexDirection: 'row', width: '100%', padding: 5, justifyContent: 'space-between' }}>
      <View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
        <Checkbox
          value={task.checked}
          onValueChange={handleCheck}
          style={{ padding: 12 }}
          color={task.checked && defaultColors.placeholder}
          readOnly
        />
        <Text style={task.checked ? styles.checked : styles.unchecked}>{task.text}</Text>
      </View>
      <Button title='X' onPress={() => onDeleteClick(task)} style={{ justifySelf: 'flex-end' }} />
    </View>
  )
}

const styles = StyleSheet.create({
  checked: {
    color: defaultColors.placeholder,
    marginLeft: 10
  },
  unchecked: {
    marginLeft: 10
  }
})

在这种情况下,我们将把处理程序弹出给父母,因为我们将在列表中渲染任务项。

6.2.4创建任务表格

我们还需要一个表格来创建新任务。这与我们以前已经做过的事情非常相似。在app/src/tasks/TaskForm.js上创建一个新文件,并添加以下内容:

import Meteor from '@meteorrn/core'
import React, { useState } from 'react'
import { View, Button, TextInput } from 'react-native'
import { defaultColors, defaultStyles } from '../styles/defaultStyles'
import { ErrorMessage } from '../components/ErrorMessage'

export const TaskForm = () => {
  const [text, setText] = useState('')
  const [error, setError] = useState('')
  const handleSubmit = e => {
    e.preventDefault()
    if (!text) return
    Meteor.call('tasks.insert', { text }, (err) => {
      if (err) {
        return setError(err)
      }
      setError(null)
    })
    setText('')
  }

  return (
    <View>
      <View style={defaultStyles.row}>
        <TextInput
          placeholder='Type to add new tasks'
          value={text}
          place
          onChangeText={setText}
          placeholderTextColor={defaultColors.placeholder}
          style={defaultStyles.text}
        />
        <Button title='Add Task' onPress={handleSubmit} />
      </View>
      <ErrorMessage error={error} />
    </View>
  )
}

6.2.5将它们结合在任务列表组件中

现在是时候在app/src/tasks/TaskList.js上创建一个新组件并使所有先前创建的文件一起工作,为我们的简单杂物提供完整的CRUD功能。

import Meteor from '@meteorrn/core'
import React, { useState } from 'react'
import { Text, View, SafeAreaView, FlatList, Button, ActivityIndicator } from 'react-native'
import { TasksCollection } from './TasksCollection'
import { Task } from './Task'
import { TaskForm } from './TaskForm'
import { useAccount } from '../hooks/useAccount'
import { defaultColors, defaultStyles } from '../styles/defaultStyles'

const { useTracker } = Meteor
const toggleChecked = ({ _id, checked }) => Meteor.call('tasks.setIsChecked', { _id, checked })
const deleteTask = ({ _id }) => Meteor.call('tasks.remove', { _id })

export const TaskList = () => {
  const { user } = useAccount()
  const [hideCompleted, setHideCompleted] = useState(false)

  // prevent errors when authentication is complete but user is not yet set
  if (!user) { return null }

  const hideCompletedFilter = { checked: { $ne: true } }
  const userFilter = { userId: user._id }
  const pendingOnlyFilter = { ...hideCompletedFilter, ...userFilter }

  const { tasks, pendingTasksCount, isLoading } = useTracker(() => {
    const tasksData = { tasks: [], pendingTasksCount: 0 }

    if (!user) {
      return tasksData
    }

    const handler = Meteor.subscribe('tasks.my')

    if (!handler.ready()) {
      return { ...tasksData, isLoading: true }
    }

    const filter = hideCompleted
      ? pendingOnlyFilter
      : userFilter
    const tasks = TasksCollection.find(filter, { sort: { createdAt: -1 } }).fetch()
    const pendingTasksCount = TasksCollection.find(pendingOnlyFilter).count()

    return { tasks, pendingTasksCount }
  }, [hideCompleted])

  if (isLoading) {
    return (
      <View style={defaultStyles.container}>
        <ActivityIndicator />
        <Text>Loading tasks...</Text>
      </View>
    )
  }

  const pendingTasksTitle = `${pendingTasksCount ? ` (${pendingTasksCount})` : ''}`

  return (
    <SafeAreaView style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
      <View style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'scroll' }}>
        <View style={defaultStyles.row}>
          <Text>My Tasks {pendingTasksTitle}</Text>
          <TaskForm />
        </View>
        <Button
          title={hideCompleted ? 'Show All' : 'Hide Completed Tasks'}
          color={defaultColors.placeholder}
          onPress={() => setHideCompleted(!hideCompleted)}
        />
        <FlatList
          data={tasks}
          renderItem={({ item: task }) => (
            <Task
              task={task}
              onCheckboxClick={toggleChecked}
              onDeleteClick={deleteTask}
            />
          )}
          keyExtractor={task => task._id}
        />

      </View>
    </SafeAreaView>
  )
}

流星魔法发生在useTracker钩中。在那里,我们订阅了后端的出版物tasks.my,并将可选后过滤器应用于检索到的数据。我们通过操纵现场同步的Mongo。 useTracker还将在数据更新上触发一个新的渲染周期,这提供了实时更新的“自动”感觉。

最后一步,我们终于完成了!确保TaskList包含在HomeScreen中:

import React from 'react'
import { View } from 'react-native'
import { defaultStyles } from '../styles/defaultStyles'
import { TaskList } from '../tasks/TaskList'

export const HomeScreen = () => {
  return (
    <View style={defaultStyles.container}>
      <TaskList />
    </View>
  )
}

6.3终于庆祝您的第一个应用程序ð¥³

final image


7.摘要和前景

在这个研讨会中,我们创建了一个功能齐全的移动应用程序,该应用程序是本机和流星的后端。它解决了使它们俩一起工作的最大挑战 - 连接,身份验证和沟通。

从这里开始有许多主题需要继续,因为这只是一个最小的应用程序原型。许多主题不能仅在一个研讨会中涵盖,这就是为什么我试图将它们缩小到“ Essentials”的原因。如果您认为有任何未发现的主题也是必不可少的,请发表评论。

如果您喜欢研讨会,请喜欢并订阅我的频道。


更多资源和链接

流星

反应天然


关于我

我在Dev.to上定期发布文章,以大约流星 javascript 。如果您喜欢您正在阅读并想支持我的内容,则可以sponsor me on GitHubsend me a tip via PayPal

您还可以在GitHubTwitterLinkedIn上找到(并与我联系)。

通过访问their blog来跟上流星的最新发展,如果您像我一样进入流星并想向世界展示,则应查看Meteor merch store