十五、结合 Node.js 和前端

现在我们已经了解了前端框架和 Node.js,让我们将两端连接起来。 我们将构建三个(几乎)全栈功能的小应用来演示我们的知识。 毕竟,前端和后端想要了解对方! 这将是我们首次尝试使用这些技术,所以一定要给自己足够的空间和时间来学习,因为这些都是沉重但极其重要的话题。

本章将涵盖以下主题:

  • 理解架构握手
  • 前端和 Node.js: React 和图像上传
  • 使用 api 和 JSON 创建一本菜谱
  • 用 Yelp 和 Firebase 建立一个餐厅数据库

技术要求

准备使用存储库的Chapter-15目录中提供的代码:https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-15。 由于我们将使用命令行工具,还需要提供终端或命令行 shell。 我们需要一个现代化的浏览器和本地代码编辑器。

理解架构握手

现在我们已经有了在前端和后端使用 Node.js 的 JavaScript 的经验,让我们讨论一下将这两个部分捆绑在一起的真正意味着。 我们知道 JavaScript 在用户交互、视觉效果、数据验证和其他与用户体验相关的方面都很出色。 后台的 Node.js 是一种强大的服务器端语言,它可以帮助我们从大多数其他服务器端语言中完成几乎所有我们需要的事情。 那么,结合这两个端点在理论上是什么样子的呢?

您可能想知道为什么一个应用有两个端点。 我们知道 PythonNode.js 和 JavaScript 都执行不同的任务,并在前端或后端执行,但这背后的理论是什么? 答案是:有一个软件工程的原则被称为关注点分离,它基本上说明了一个程序的每个部分应该做一个或几个任务,并把它们做好。 与单片应用不同,模块化系统的想法在实践中是一个更高效的系统。 在本章中,我们将创建三个使用该原则的应用。

*# 前端和 Node.js - React 和图像上传

让我们从结合 React 和 Node 开始。 准备好跟随https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-15/photo-album的解决方案代码。 我们将构建一个像这样的相册应用:

Figure 15.1 - Our photo gallery

我们将从研究架构布局开始,然后回顾 React 代码,最后检查 Express 后端。

体系结构

这个应用将在后端使用 Node.js 来存储我们上传的文件,并在前端使用 React。 但我们该怎么做呢? 从概念上讲,我们需要告诉 React 使用 Express 应用来提供 React 信息,并使用我们发送的文件。 为此,我们使用package.json文件中定义的代理。 它基本上是这样的:

Figure 15.2 - Proxying

如果你不熟悉代理的概念,在计算中,它本质上意味着,它在英语中所做的事情:一个参与者代表另一个参与者执行动作。 从本质上讲,它是一个中间商,从这张图可以看出它是一个中间商。 因为反应和前端 JavaScript 不能与文件系统交互或做其他重要的事情我们知道在 12 章,node . js 和 Python,和第十三章,使用表达,我们需要使用我们的能力将【显示】前端和后端。 因此,有了代理的概念。

让我们看一下package.json中的一行:

"proxy": "http://localhost:3001",

这告诉 React 要做的是将某些请求路由到我们的 Express 应用。 如果你遵循 GitHub 的代码,这意味着我们实际上必须执行一些不同的npm命令:

  1. 首先,安装 Express 的包。 在photo-album目录下启动:npm install
  2. 启动 Express 服务器:npm start
  3. 在另一个终端窗口中,cd进入client目录,运行npm install
  4. 现在,使用npm start开始 React 应用。

当我们访问http://localhost:3000时,就可以使用我们的相册应用了。 尝试通过选择一个文件并点击上传来上传照片。 用户界面也会刷新并显示你刚刚上传的照片。 恭喜你! 这是一个端到端的应用!

这段代码在做什么呢? 让我们仔细分析它。

首先,我们将看看 JavaScript。

调查 React JSX

打开client/src/components/upload/Upload.jsx。 我们先来看看render()方法的内容:

<p><button id="upload" onClick={this.upload}>Upload Photo</button></p>
<div id="uploadForm" className="w3-modal">   <form method="post"
 encType="multipart/form-data">
     <p><input type="file" name="filetoupload" /></p>
     <p><button type="submit" onClick={this.uploadForm}>Upload</button></p>
   </form>
</div>

很好,这是一个基本的 HTML 表单。 惟一与 react 相关的部分是单击处理程序。 让我们看看表单this.uploadFormonClick方法。 如果我们看看这个方法,我们会看到上传表单的真正功能:

 uploadForm(e) {
 e.preventDefault();
 const formData = new FormData()

 formData.append('file', document.querySelector('input').files[0]);

 fetch("http://localhost:3000/upload", {
   method: 'POST',
   body: formData
 })
   .then(() => {
     this.props.reload()
   })
}

准备好查看 Node.js Express 路由了吗?

解密 Express 应用

打开routes/upload.js。 这是相当简单的:

const express = require('express');
const formidable = require('formidable');
const router = express.Router();
const fs = require('fs');

router.post('/', (req, res, next) => {
  const form = new formidable.IncomingForm().parse(req)
    .on('fileBegin', (name, file) => {
      file.path = __dirname + '/../publimg/' + file.name
    })
    .on('file', () => {
      res.sendStatus(200)
    })
});

module.exports = router;

为了简化我们的工作,我们使用了一个名为 Formidable 的表单处理程序包。 当一个 POST 请求到达/upload端点时,它将运行以下代码。 当通过 Ajax 接收到表单时,我们的承诺将侦听文件,并将触发fileBeginfile事件,分别将文件写入磁盘,然后发出成功信号。 这是我们的上传表单在Upload.jsx中使用的方法,也是我们的应用的两个方面如何绑定在一起,以完成一些 JavaScript 在前端无法单独完成的事情——访问服务器的文件系统。

上传一些图片与前端。 您会注意到它们将存储在public/images中,就像我们在代码中读取的那样。 注意,这个系统是非常简单的:它不会检查它是否是一个映像文件,而是盲目地接受我们发送给它的内容并将其存储在文件系统中。 实际上,这是危险的。 当处理用户输入时,总是需要来预测攻击和可能的恶意文件。 虽然保护 web 应用的方法有些超出了本书的范围,但要记住的一般原则是:不要信任用户。 我们已经检查了在前端验证输入的方法,虽然这很有用,但在后端也检查它是至关重要的。 减少威胁的一些可能的方法是将某些文件扩展名列入白名单,将其他的列入黑名单,并使用沙箱环境对上传的文件运行分析代码,以确定它实际上是否是无害的图像文件。

*现在我们已经上传了图像,接下来让我们进入应用的检索方面。 开放routes/gallery.js:

var express = require('express');
const fs = require('fs');

var router = express.Router();

router.get('/', (req, res, next) => {
 fs.readdir(`${__dirname}/../public/images`, (err, files) => {
     if (err) {
       res.json({
         path: '',
         files: []
       });
       return;
     }

     const data = {
       path: 'img/',
       files: files.splice(1,files.length) // remove the .gitignore
     };
     res.json(data);
 });
});

router.delete('/:name', (req, res) => {
 fs.unlink(`${__dirname}/../publimg/${req.params.name}`, (err) => {
   res.json(1)
 });
});

module.exports = router;

希望这不难理解。 在 GET 路由中,我们首先检查文件系统,看看是否有我们可以访问的文件。 如果由于某些原因出现了错误,例如不正确的权限,我们将向前端发送错误并终止。 否则,我们将格式化返回数据并发送它! 容易 peasy。

我们的下一个方法定义了 DELETE 功能,它是一个简单的文件系统解除链接方法。 它的前端并不复杂:如果你点击我们图库中的一张图片,它就会删除这张照片。 当然,在实践中,您可能需要某种更好的用户界面和确认消息,但对于我们的目的来说,这就足够了。

欢迎使用您的第一个端到端应用!

继续我们的下一个应用!

使用 api 和 JSON 创建一本菜谱

使用后端的好处之一是便于应用、文件系统和 api 之间的通信。 以前,我们所做的所有工作都被限制在前端,没有持久性。 现在,我们将创建一个以 JSON 格式保存信息的菜谱应用。 不要担心,我们将在第 18 章Node.js 和 MongoDB中使用数据库。 现在,我们将使用本地文件。 下面是我们将要构建的内容:

Figure 15.3 - Our recipe book

首先,我们将使用第三方 API 设置凭证,然后继续编写代码。

设置应用

克隆https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-15/recipe-book/的启动代码。 请确保在该目录和client内都执行npm install。 我们还需要做一些设置来访问我们的 API。 要访问 Edamam API,请在https://developer.edamam.com/上注册一个免费的 API 密钥。

在我们的项目的根级,创建一个.env文件,并填充它如下:

APPLICATION_ID=<your id>
APPLICATION_KEY=<your key>

注意,这些是作为环境变量构造的,没有分号或空格。

下一步我们要做的是确保我们的应用可以读取这些变量。 在app.js的结尾,你会看到:

console.log(process.env.APPLICATION_ID, process.env.APPLICATION_KEY);

process.env.<variable name>的构造就是我们如何访问.env中的环境变量。 提供这种访问的机制是dotenv包; 你可以看到它包含在package.json; 默认情况下,不包含文件中的环境变量。

为什么要使用环境文件? 我们将学习第 17 章,安全和键,我们不想公开我们的 API 密钥我们可以承诺 GitHub 或类似的代码,因为这将允许任何人使用和滥用我们的钥匙。 我们必须确保它们的安全,如果你注意到在.gitignore文件中,我在 Git 中列出了.env而不是,这就是为什么你必须自己创建文件的原因。 这是敏感信息的最佳实践。 虽然它会使开发人员之间的代码共享变得有点棘手,但最好将敏感信息与代码分开。

让我们测试一下 API。

测试 API

如果你通读routes/tests.js,你就会明白我们到底在做什么:

const https = require('https');

require('dotenv').config();

https.get(`https://api.edamam.com/search?app_id=${process.env.APPLICATION_ID}&app_key=${process.env.APPLICATION_KEY}&q=cheesecake`, (res) => {
 console.log("Got response: " + res.statusCode)

 res.setEncoding('utf8')
  res.on("data", (chunk) => {
   console.log(chunk)
 })
}).on('error', (e) => {
 console.log("Got error: " + e.message);
})

我们的fetch调用被硬编码为搜索cheesecake(我最喜欢的甜点…问我我的食谱),如果我们用node routes/tests.js运行它,我们会在控制台看到一堆 JSON 返回。 如果您有任何问题,请务必检查您的 API 密钥。

深入研究代码

现在我们知道 API 调用正在工作,让我们切换到前端。 client/src/components/search/Search.jsx及其render功能:

render() {
 return (
   <h2>Search for: <input type="text" id="searchTerm" />
     <button onClick={this.submitSearch}>Search!</button></h2>
 )
}

到目前为止,这是一个简单的表格。 接下来,我们来看看submitSearch方法:

 submitSearch(e) {
 e.preventDefault()

 fetch(`http://localhost:3000/search?q=${document.querySelector('#searchTerm').value}`)
   .then(data => data.json())
   .then((json) => {
     this.props.handleSearchResults(json)
   })
}

同样,我们将使用代理从表单提交搜索。 在我们得到结果之后,我们将 JSON 传递给来自父组件RecipeBookprops方法handleSearchResults。 我们将在稍后讨论它,但现在,让我们切换回 Express 应用,看看我们的搜索路由在做什么。 看一眼routes/search.js

GET 路由实际上非常简单:

router.get('/', (req, res, next) => {
 https.get(`https://api.edamam.com/search?app_id=${process.env.APPLICATION_ID}&app_key=${process.env.APPLICATION_KEY}&q=${req.query.q}`, (data) => {

   let chunks = '';

   data.on("data", (chunk) => {
     chunks += chunk
   })

   data.on("end", () => {
     res.send(JSON.parse(chunks))
   })

   data.on('error', (e) => {
     console.log("Got error: " + e.message);
   })
 })
});

这看起来应该与我们的测试文件有点相似。 我们使用我们的.env文件再次为我们的搜索查询,但这一次,我们在我们的搜索和处理错误的查询字符串参数。 我们的data.on("end")处理程序将我们的结果传递回 React,以便在handleSearchResults方法的RecipeBook.jsx中使用:

handleSearchResults(data) {
 const recipes = []

 data.hits.forEach( (item) => {
   const recipe = item.recipe

   recipes.push({
     "title": recipe.label,
     "url": recipe.url,
     "image": recipe.image
   })
 })

 this.setState({
   recipes: recipes
 })
}

我们解析出应用所需的数据,并将其分配给组件的状态。 到目前为止一切顺利!

接下来是食谱书的render方法,用于显示搜索结果:

<Search handleSearchResults={this.handleSearchResults} />

{
 recipes.length > 0 ? (
   <>
     <p>Search Results</p>
     <div className="card-columns">
       {
         recipes.map((recipe, i) => (
           <Recipe recipe={recipe} key={i} search="true" 
            refresh={this.refresh} />
         ))
       }
     </div>
   </>
 ) : <p></p>

我们使用另一个三元运算符来有条件地呈现我们的结果,如果有的话,作为一个<Recipe>组件。 我们的 key 属性只是 React 希望道具拥有的唯一标识符,但refresh道具是一个有趣的标识符。 让我们看一下Recipe组件,看看它在哪里使用。

我们的Recipe组件的render方法是相当标准的:它使用了一些 Bootstrap 组件来渲染我们的漂亮的小卡片,但除此之外,它没什么特别的。 save方法是我们真正想研究的:

save(e) {
   e.preventDefault()

   const recipe = { [this.props.recipe.title]: this.props.recipe }

   fetch('http://localhost:3000/recipes', {
     method: 'POST',
     headers: {
       'Accept': 'application/json',
       'Content-Type': 'application/json'
     },
     body: JSON.stringify(recipe)
   })
   .then(json => json.json())
   .then( (data) => {
     this.props.refresh(data)
   })
 }

const recipe声明可能看起来有点奇怪,所以让我们把它拆开。 这是创建一个对象键/值对,对于键,我们使用 recipe 的标题。 因为它是一个变量,我们想用方括号来表示它应该被解释。 我们不能使用点属性作为键,所以标题将是一个字符串。

这里有一个例子,说明在这种结构下的配方可能是什么样子的:

{"Strawberry Cheesecake Parfaits": {"title":"Strawberry Cheesecake Parfaits", "image":"https://www.edamam.com/web-img/d4c/d4c3a4f1db4e8c413301ae1f324cf32a.jpg", "url":"http://honestcooking.com/strawberry-cheesecake-parfaits/"}}

它有我们之前在RecipeBook.jsx中一起映射对象时指定的所有信息。 我们流程中的下一步是将菜谱保存到文件系统中,同时向 Express 服务器发送另一个fetch请求。

回到 Express we go,这次是routes/recipes.js!

让我们逐部分地查看该文件。 在 Express 方法之外,我们有一个readData方法,它检查我们的recipes.json文件是否存在:

const readData = () => {
 if (!fs.existsSync(__dirname + "/../data/recipes.json")) {
   fs.writeFileSync(__dirname + "/../data/recipes.json", '[]')
 }

 return JSON.parse(fs.readFileSync(__dirname + "/../data/recipes.json"))
}

如果没有,则创建一个包含空数组的文件。 然后它将文件的内容(无论是否为空)返回给调用函数。

GET 方法使用来自readData的数据,并将其发送到响应中,在本例中是发送到RecipeBook.jsx:

router.get('/', (req, res, next) => {
 const recipes = readData()
 res.json(recipes)
})

RecipeBook.render方法的第二部分(我们没有看到)类似于搜索结果 JSX,它使用这个 JSON。

我们的save方法与readData方法有相似之处:

router.post('/', (req, res) => {
 let recipes = readData()
 const data = req.body
 recipes.push(data)
 fs.writeFileSync(__dirname + "/../data/recipes.json",JSON.stringify(recipes))
 res.json(recipes)
})

注意,它还将 JSON 发送给响应,以便当保存条目时,它还会在RecipeBook.jsx中填充保存的食谱。 这可能是不言而喻的,但请注意,我们再次使用了readData方法,而不是重写相同的逻辑,使代码保持 DRY。

这就是我们应用的逻辑! 我们已经成功地将 API、Node.js、Express 和 React 组合成一个端到端应用。 接下来,我们将创建一个更真实的应用:我们将创建一个餐馆搜索应用,它将保存到云数据库中,可以通过 JavaScript 访问。

用 Yelp 和 Firebase 建立一个餐厅数据库

到目前为止,我们的应用相当简单,只是在文件系统上存储信息。 然而,在大多数情况下,您希望它是某种类型的数据库,而不是静态文件。 我们将使用 Firebase,这是一个基于云的 NoSQL 数据库,可以很好地使用 JavaScript,但首先,让我们设置 React 脚手架。

起跑线-创建一个 React 应用

我们之前已经经历过这种设置好几次了,所以应该不会感到惊讶:

  1. npx create-react-app restaurant-finder创建一个新的 React 应用,我们准备好了!
  2. 使用npm start测试您的设置并访问http://localhost:3000

使用 Firebase

我们要做的第一件事是设置我们的 Firebase 帐户。

请记住,Firebase 的用户界面(和大多数网站一样)会定期变化,所以我不会向你展示注册过程的截图。 如果在安装过程中遇到任何问题,可以参考文档。 下面是步骤:

  1. 请登录https://firebase.google.com
  2. 如果您还没有谷歌帐户,则需要创建一个谷歌帐户,然后访问控制台。
  3. 创建一个名为restaurant-database的新项目。
  4. 您可以选择为项目启用谷歌 Analytics; 由你决定。
  5. 在 Project Overview 页面,我们将使用>按钮来访问 web 应用的设置说明。
  6. 在接下来的屏幕上,创建一个应用昵称(你可以再次使用restaurant-database),你将不需要设置 Firebase Hosting。
  7. 下一个屏幕将向您展示包含 Firebase 配置的代码,但是我们不会严格按照的说明操作,因为我们可以使用 Node 模块来帮助我们! 尽管如此,还是要复制firebaseConfig变量中的信息:我们稍后将需要它。
  8. 当你的数据库创建完成后,在 UI 中选择 database 选项卡,选择 Realtime database,并在test mode中启动它。

然后你会看到类似这样的屏幕:

Figure 15.4 - Firebase's base test mode view

接下来,我们将返回到命令行,准备使用 Firebase。 安装 Firebase 工具包:npm install firebase

安装完毕! 简单! 接下来,在我们的项目的根目录下创建一个.env文件,并输入之前从firebaseConfig复制的凭据,类似如下:

REACT_APP_apiKey=<key>
REACT_APP_authDomain=restaurant-database-<id>.firebaseapp.com
REACT_APP_databaseURL=https://restaurant-database-<id>.firebaseio.com
REACT_APP_projectId=restaurant-database-<id>
REACT_APP_storageBucket=restaurant-database-<id>.appspot.com
REACT_APP_messagingSenderId=<id>
REACT_APP_appId=<id>

注意前缀REACT_APP_,等号,引号,以及缺少结尾逗号。 以类似的方式填写配置。

在继续之前,让我们先测试一下数据库。

测试数据库

现在我们要创建几个 React 组件。 在src目录下创建components目录,在components目录下创建databasefinder两个目录。 我们将从创建数据库引用开始:

  1. 在数据库目录下创建一个database.js文件。 请注意,它是js,而不是jsx,因为我们实际上并不打算渲染任何数据。 相反,我们将返回一个变量给一个jsx组件。 你的文件应该是这样的:
import * as firebase from 'firebase'

const app = firebase.initializeApp({
 apiKey: process.env.REACT_APP_apiKey,
 authDomain: process.env.REACT_APP_authDomain,
 databaseURL: process.env.REACT_APP_databaseURL,
 projectId: process.env.REACT_APP_projectId,
 storageBucket: process.env.REACT_APP_storageBucket,
 messagingSenderId: process.env.REACT_APP_messagingSenderId,
 appId: process.env.REACT_APP_appId
})

const Database = app.database()

export default Database

注意每个变量的前缀process.env以及末尾的逗号。 process.env指定应用应该查看dotenv提供的环境变量。

  1. 接下来,我们有Finder.jsx。 在finder目录下创建这个文件:
import React from 'react'
import Database from '../database/database'

export default class Finder extends React.Component {
 constructor() {
   super()

   Database.ref('/test').set({
     helloworld: 'Hello, World'
   })
 }

 render() {
   return <h1>Let's find some restaurants!</h1>
 }
}

我们的App.js文件看起来像这样:

import React from 'react'
import Finder from './components/finder/Finder'
import './App.css'

function App() {
 return (
   <div className="App">
     <Finder />     
   </div>
 );
}

export default App;
  1. 现在,由于我们刚刚创建了环境变量,我们需要再次停止并启动 React 应用。 这对于大多数 React 工作来说并不是必需的,但在这里却是必需的。
  2. 点击http://localhost:3000进入应用。 我们应该看到让我们在页面上找到一些餐厅,但如果我们去 Firebase,我们会看到这个:

Figure 15.5 - We have data in Firebase!

数据似乎被截断了,但是您可以单击它并查看整个语句。

万岁! 我们有 Firebase 工作。 现在来看看应用的其余部分。

创建应用

我们可以从Finder.jsx中删除测试插入。 这是我们要做的:

Figure 15.6 - Restaurant finder

为了做到这一点,我们将使用 Yelp API。 首先,你需要登录https://www.yelp.com/developers,注册 Yelp Fusion API 密钥。 一旦你有了它,我们将把它存储在一个新的api目录下的一个新的.env文件中。

The Yelp Fusion API is not available in all countries, so if you cannot access it, please look in Chapter-15 folder on GitHub for an alternative API usage example.

Yelp API 是一个 REST API,为了保护你的密钥,它不允许前端 JavaScript 连接。 所以,就像我们的菜谱一样,我们要创建一个小 API 层来处理我们的请求。 不像我们的食谱,这将是相当简单的,所以我们不会使用 Express。 让我们看看这些步骤:

  1. 在项目的根目录下,我们将安装一些工具:npm install yelp-fusion dotenv react-bootstrap
  2. 在项目的根目录下创建一个名为api的目录,并在其中创建一个api.js文件。
  3. 我们将在api目录中也有一个.env文件:
Yelp_Client_ID=<your client id>
YELP_API_Key=<your api key>
  1. 如果你使用的是 Git,别忘了把它添加到.gitignore条目中。

我们的api.js文件将相当简单:

const yelp = require('yelp-fusion');
const http = require('http');
const url = require('url');
require('dotenv').config();

const hostname = 'localhost';
const port = 3001;

const client = yelp.client(process.env.YELP_API_Key);

const server = http.createServer((req, res) => {
 const { lat, lng, value } = url.parse(req.url, true).query

 client.search({
   term: value,
   latitude: lat,
   longitude: lng,
   categories: 'Restaurants'
 }).then(response => {
   res.statusCode = 200;
   res.setHeader('Content-Type', 'application/json');

   res.write(response.body);
   res.end();
 })
   .catch(e => {
     console.error('error',e)
   })
 });

 server.listen(port, hostname, () => {
   console.log(`Server running at http://${hostname}:${port}/`);
 });

到目前为止,很多内容应该都很熟悉:我们将像以前那样包括一些包,比如 Yelp API,并且我们将定义一些变量来帮助我们。 接下来,我们将使用httpcreateServer方法来创建一个非常非常简单的服务器来响应我们的 API 请求。 在它里面,我们将使用urlparse方法来获取我们的查询字符串参数,我们将把它传递给我们的 API。

下一块,client.search,将是陌生的。 这是从 Yelp 文档中提取出来的,并且是专门按照他们的 API 要求制作的。 一旦有了异步响应,就将其发送回请求应用。 不要忘记处理错误! 然后我们在端口3001上启动服务器。 您可以继续使用node api.js启动该服务器,您将看到关于它运行的控制台错误消息。

现在让我们把注意力转向应用的 React 部分:

  1. 在我们的src目录中,当我们完成时,我们会有这样的文件结构:
.
├── App.css
├── App.js
├── App.test.js
├── components
   ├── database
    └── database.js
   ├── finder
    └── Finder.jsx
   ├── restaurant
    ├── Restaurant.css
    └── Restaurant.jsx
   └── search
       └── Search.jsx
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js

其中许多文件在我们之前搭建应用时已经创建了,但是components目录的一些部分是新的。

  1. 创建这些文件,我们将开始探索Restaurant.jsx:
import React from 'react'
import { Button, Card } from 'react-bootstrap'
import Database from '../database/database'

import './Restaurant.css'

export default class Restaurant extends React.Component {
 constructor() {
   super();

   this.saveRestaurant = this.saveRestaurant.bind(this)
 }

 saveRestaurant(e) {
   const { restaurant } = this.props

   Database.ref(`/restaurants/${restaurant.id}`).set({
     ...restaurant
   })
 }

 render() {
   const { restaurant } = this.props

   return (
     <Card>
       <Card.Img variant="top" src={restaurant.image_url} 
        alt={restaurant.name} />
       <Card.Body>
         <Card.Title>{restaurant.name}</Card.Title>
         {!this.props.saved && <Button variant="primary" 
         onClick={this.saveRestaurant}>Save Restaurant</Button>}
      </Card.Body>
     </Card>
   )
 }
}

其中大部分都不是新的,我们的食谱的结构可以帮助我们对此进行推理。 我们应该分解saveRestaurant方法,因为它使用了一些有趣的部分:

saveRestaurant(e) {
   const { restaurant } = this.props

   Database.ref(`/restaurants/${restaurant.id}`).set({
     ...restaurant
   })
 }

首先,我们可以推断,我们将从餐厅的props组件中获取数据。 这将直接从我们的搜索结果。 因此,我们需要稍微修改一下我们的数据。

下面是我们的props的搜索结果:

{id: "CO3lm5309asRY7XG5eXNgg", alias: "rahi-new-york", name: "Rahi", image_url: "https://s3-media1.fl.yelpcdn.com/bphoto/rPh_LboeIOiTVeXCuas5jA/o.jpg", is_closed: false, ...}
id: "CO3lm5309asRY7XG5eXNgg"
alias: "rahi-new-york"
name: "Rahi"
image_url: "https://s3-media1.fl.yelpcdn.com/bphoto/rPh_LboeIOiTVeXCuas5jA/o.jpg"
is_closed: false
url: "https://www.yelp.com/biz/rahi-new-york?adjust_creative=-YEyXjz9iO0W5ymAnPt6kA&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=-YEyXjz9iO0W5ymAnPt6kA"
review_count: 448
categories: (3) [{...}, {...}, {...}]
rating: 4.5
coordinates: {latitude: 40.7360271, longitude: -74.0005436}
transactions: (2) ["delivery", "pickup"]
price: "$$$"
location: {address1: "60 Greenwich Ave", address2: "", address3: null, city: "New York", zip_code: "10011", ...}
phone: "+12123738900"
display_phone: "(212) 373-8900"
distance: 1305.5181202902097
  1. 我们保存它到 Firebase 如下:
Database.ref(`/restaurants/${restaurant.id}`).set({
  ...restaurant
})

我们使用扩展操作符(三重点)将对象展开为其组成的键/值对,从而避免在数据库中嵌套对象。 我们也有一点 CSS 来格式化我们的卡片。

让我们把注意力转向Search组件:

import React from 'react'
import { Button } from 'react-bootstrap'
import Restaurant from '../restaurant/Restaurant'

export default class Search extends React.Component {
 constructor() {
   super()

   this.state = {
     businesses: []
   }

在我们的构造函数中,我们做了一些有趣的事情:浏览器地理位置

你见过某些网站上询问你位置时的小提醒窗口吗? 这些网站就是这样做的。 如果浏览器支持地理定位,我们会使用它,并在浏览器中设置纬度和经度。 否则,我们将简单地设置为null:

   if (navigator.geolocation) {
     navigator.geolocation.getCurrentPosition((position) => {
       this.setState({
         lng: position.coords.longitude,
         lat: position.coords.latitude
       })
     })

   } else {
     this.setState({
       lng: null,
       lat: null
     })
   }

   this.search = this.search.bind(this)
   this.handleChange = this.handleChange.bind(this)
 }

 handleChange(e) {
   this.setState({
     val: e.target.value
   })
 }

搜索端点的构造看起来应该很熟悉:

 search(event) {
   const { lng, lat, val } = this.state

   fetch(`http://localhost:3000/businesses/search?
   value=${val}&lat=${lat}&lng=${lng}`)
     .then(data => data.json())
     .then(data => this.handleSearchResults(data))
 }

 handleSearchResults(data) {
   this.setState({
     businesses: data.businesses
   })
 }

 render() {
   const { businesses } = this.state

   return (
     <>
       <h2>Enter a type of cuisine: <input type="text" onChange=
       {this.handleChange} /> <Button id="search" onClick={this.search}>
       Search!</Button></h2>
       <div className="card-columns">
         {
           businesses.length > 0 ? (
             businesses.map((restaurant, i) => (
               <Restaurant restaurant={restaurant} key={i} />
             ))
           ) : <p>No results</p>
         }
       </div>
     </>
   )
 }
}

As you progress through our code, if you get a null value for latitude or longitude, you may need to fully exit the React application and restart it.

类似于我们的菜谱通过代理调用 Express 应用的方式,不要忘记将这一行添加到您的package.json文件:"proxy": "http://localhost:3001"。 这样我们就可以用fetch了。 这些是我们传递给api.js的值,用于我们对 Yelp API 的请求。

我们几乎完成了我们的应用! 接下来是我们开始的Finder组件:

  1. 首先是我们的进口:
import React from 'react'
import Database from '../database/database'
import { Tabs, Tab } from 'react-bootstrap'
import Search from '../search/Search'
import Restaurant from '../restaurant/Restaurant'
  1. 接下来,我们有一些非常标准的作品:
export default class Finder extends React.Component {
 constructor() {
   super()

   this.state = {
     restaurants: []
   }

   this.getRestaurants = this.getRestaurants.bind(this)
 }

 componentDidMount() {
   this.getRestaurants()
 }
  1. 作为一篇新文章,让我们检查一下如何从 Firebase 检索信息:
 getRestaurants() {

   Database.ref('/restaurants').on('value', (snapshot) => {
     const restaurants = []

     const data = snapshot.val()

     for(let restaurant in data) {
       restaurants.push(data[restaurant])
     }
     this.setState({
       restaurants: restaurants
     })
   })
 }

Firebase 的一个有趣之处在于它是一个实时数据库; 您不必总是对它执行查询来检索最新的数据。 在这个构造中,我们告诉数据库,当/restaurants的值发生变化时,要不断更新组件的状态。 当我们拯救一家新餐馆,然后去我们的拯救! 选项卡,我们将看到我们的新条目。

  1. 我们通过使用其他组件来实现完整的循环:
 render() {

   const { restaurants } = this.state
   return (
     <>
       <h1>Let's find some restaurants!</h1>

       <Tabs defaultActiveKey="search" id="restaurantsearch">
         <Tab eventKey="search" title="Search!">
           <Search handleSearchResults={this.handleSearchResults} 
        />
         </Tab>
         <Tab eventKey="saved" title="Saved!">
           <div className="card-columns">
             {
               restaurants.length > 0 ? (
                 restaurants.map((restaurant, i) => (
                   <Restaurant restaurant={restaurant} saved={true} 
                    key={i} />
                 ))
               ) : <p>No saved restaurants</p>
             }
           </div>
         </Tab>
       </Tabs>
     </>
   )
 }
}

当所有完成时,我们将保持我们的api.js文件运行,并使用npm start启动我们的 React 应用,我们的应用就完成了!

该结束这一章了。

总结

在本章中,我们已经讨论了地段的土地。 JavaScript 在前端和后端的强大功能向我们表明,我们可以真正地取代 Python 来满足我们的许多应用需求。 我们已经使用了很多 React,但请记住,任何前端都可以在这里替换:Vue、Angular,甚至是无框架的 HTML、CSS 和 JavaScript 都可以用来创建强大的 web 应用。

在使用 JavaScript 和 api 时需要注意的一点是,在某些情况下,我们需要一个中间件层,例如,在保存文件或使用键访问 REST api 时。 将 Express 功能强大的路由与基本的 Node.js 脚本结合起来与 API 交互只是我们将 JavaScript 和 Node.js 结合在一起所能完成的工作的开始。

在下一章中,我们将探索 webpack,它允许我们逻辑地组合和打包 JavaScript 应用以进行部署。**