Skip to content

Latest commit

 

History

History
811 lines (567 loc) · 38.5 KB

File metadata and controls

811 lines (567 loc) · 38.5 KB

十三、使用 Express

正如我们已经讨论过的,后端 JavaScript 对于创建 web 应用和在前端和后端利用 JavaScript 的能力是非常有用的。 服务器端应用与前端交互的最基本工具之一是基本 web 服务器。 为了提供 api、数据库访问和其他不是设计来由浏览器处理的功能,我们首先需要设置一个软件来处理这些交互。

Express.js(或简称 Express)是一个 web 应用框架,被认为是 Node.js 的事实上的web 服务器。 它既受欢迎又易于使用。 让我们用它来构建一个完整的 web 应用。

本章将涵盖以下主题:

  • 支架:使用express-generator
  • 路线和视图
  • 控制器和数据:在 Express 中使用 api
  • 用 Express 创建 API

技术要求

准备在https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13使用 GitHub 存储库。 您所需要的只是一个代码编辑器和一个浏览器。 在路由和小节*、*中,我们将讨论一些使用代码编辑器的最佳实践。

命令行示例以 macOS/Linux 风格提供。 Windows 用户可能需要参考文档来理解 Windows 命令行中的一些细微差别。

脚手架:使用 express-generator

为了开始,我们需要再次进入命令行界面(CLI)。 第二章Can We Use JavaScript server ? 当然! ,我们在命令行上查看 Node 和npm。 让我们再检查一下我们的版本,以便对我们的应用做出一些决定。 在命令行中,运行node -v。 如果您的版本是 v8.2.0 或更高版本,您可以选择使用npx来安装某些设计为在项目生命周期中只运行一次的包,例如 express-generator。 但是,如果你有一个较低的版本,你可以使用npm来安装一次性使用的包以及在你的项目中使用的包。

在本章中,我们将继续学习npx,所以如果你需要快速浏览npmnpx,的文档,请务必给自己一些时间。 从本质上讲,要使用npm用于一次性的包,而这些包不应该存在于你的代码库中,例如,一个脚手架工具,如 Express 生成器或 React 应用创建者,你可以像这样在你的系统中全局安装这个包:npm install -g express-generator。 然后,您将使用 Express 运行该程序。 然而,这被认为是npm的遗留用法,因为npx在今天的景观中受到青睐。

让我们从头开始创建我们的 Express 应用,以建立肌肉记忆,而不是继续从第二章能否使用 JavaScript 服务器端? 当然!使用 Express web 服务器:

*1. 在一个方便的位置,用mkdir my-webapp创建一个新目录。 2. 用cd my-webapp在里面导航。 3. express 生成器将创建几个文件和目录:

Figure 13.1 - Creating our Express scaffold

我们将设置应用为模板使用 Handlebars,而不是 Jade,后者是默认选项。 表达支持多个模板语言的(可以扩展到使用),但为了便于使用,我们将坚持车把,这非常类似于我们如何工作与前端框架如反应和 Vue第八章,*处理框架和库时,*基本花括号标记。

  1. 使用npm install来安装依赖项。 (注意,即使你之前使用了npx,这里也将使用npm。) 这将花费几秒钟的时间,并将下载一些包和其他依赖项。 另一件需要注意的事情是,您将需要一个互联网连接,因为npm检索包从互联网。

  2. 现在,我们准备使用npm start启动应用:

Figure 13.2 - Our application starting

  1. 好的! 现在,让我们在浏览器中访问我们的 Express 站点:

Figure 13.3 - Express welcome page

太棒了! ,Can we Use JavaScript Server-Side?(我们可以使用 JavaScript 服务器端吗?) 当然!

RESTful 架构

许多 web 应用的核心是 REST(或 RESTful)应用。 RESTREpresentational State Transfer的缩写,它是一种设计模式,用于处理大多数 web 技术本身无状态这一事实。 设想一个不需要登录或大量数据的标准网站,就像我们在前几章中创建的那样,只需要静态 HTML 和 CSS,但更简单:没有 JavaScript。 如果我们从状态的角度来考虑一个网站,我们可以看到一堆 HTML 不知道我们的用户旅程,不知道我们是谁,坦白地说,它也不在乎。 像这样的网站就像印刷材料:你可以通过浏览、阅读和翻页来与之互动。 你不需要改变任何东西。 一般来说,真正修改书籍状态的唯一方法是使用书签来保存位置。 老实说,这比 HTML 和 CSS 的基本组合更具交互性!

为了处理用户和数据,REST 被用作功能范例。 在使用 api 时,我们已经使用了两个主要的 HTTP 动词:GET 和 POST。 这是我们将要使用的两个主要动词,但是我们还会看另外两个:PUT 和 DELETE。

如果你熟悉创建、读取、更新和删除(CRUD)的概念,下面是标准 HTTP REST 动词的翻译:

| 概念 | HTTP 动词 | | 创建 | 创建 | | 读 | 得到 | | 更新 | 把或补丁 | | 删除 | 删除 |

For more information, you can take a look at the Packt REST Tutorial: https://hub.packtpub.com/what-are-rest-verbs-and-status-codes-tutorial/.

现在,可以只使用 GET 或 GET 和 POST 创建完整的应用,但出于安全性和架构方面的原因,您不希望这样做。 现在,让我们同意遵循最佳实践并在这个已建立的范例中工作。

现在,我们将创建一个 RESTful 应用。

路线和视图

路由和视图是 RESTful 应用的 url 如何作为其逻辑的路径以及如何将内容返回给用户的基础。 路由将确定代码的哪些部分与应用接口的 url 相对应。 视图决定向浏览器、另一个 API 或其他编程访问显示什么。

为了进一步理解 Express 应用的结构,我们可以检查它的路由和视图:

  1. 首先,让我们在您最喜欢的 IDE 中打开 Express 应用。 我将使用 VS Code。 如果你使用 VS Code、Atom、Sublime 或其他有命令行工具的 IDE,我强烈建议你安装它们。 例如,使用 Atom,您可以通过在命令提示符中输入atom .并在 Atom 中打开该目录来启动多面板 Atom 编辑界面。

  2. 类似地,VS Code 将使用code .做这个。 这看起来是这样的:

Figure 13.4 - VS Code

我已经在左边展开了目录,这样我们就可以看到层次结构的第一层。

  1. 打开app.js

你会注意到的第一件事是,这段代码的语法表达生成器为我们创建的是ES5,而不是 ES6。 目前,我们不需要考虑将它转换为 ES6; 我们待会再讲。 我们在第一个 node . js REST 应用工作,记住,有两种不同的方法实现我们的目标,我们将更详细的路径首先为了得到我们的功能工作,然后迭代它,使之更灵活更干燥。

  1. 现在,你不需要对app.js做任何改变,但一定要花一点时间来熟悉它的结构。 它较不熟悉的一个方面可能是文件开头的require()语句。 与在前端框架中使用import类似,require()是 Node 向其组件系统发送信号的方式,以便从其他文件中引入这些组件。 在这种情况下,前几行引入了通过npm,安装的模块,如下所示:
var express = require('express');

注意,在('express')前面没有路径。 简单的说。 这是一个指示,表明所引用的模块不是我们代码的原生模块。 然而,如果你看看indexRouterrequire语句,我们会看到路径:'./routes/index'。 它有而不是.js扩展,但它的路径适合我们的模块使用。

现在,让我们检查我们的routes/index.js文件。

路线

如果你打开routes/index.js,你会看到以下几行代码,这是为我们生成的:

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express' });
});

module.exports = router;

这里没有太多令人惊讶的地方:当我们开始收集时,Express 文件以require语句开头,尤其是express本身。 在下一个代码块中,我们开始看到 REST 服务的开始:GET home page。 看注释后面的router.get()方法。 显式地向路由器声明,当接收到 URL/的 GET 请求时,执行以下代码。

为了好玩,我们可以在这里添加更多的 GET 路径来验证这个事实。 让我们尝试像这样修改代码。 router.get()之后,module.exports之前,让我们在路由器上注册更多的路由:

/* GET sub page. */
 router.get('/hello', function(req, res, next) {
     res.render('index', { title: 'Hello! This is a route!' });
 });

现在,我们必须用Ctrl + C停止我们的 Express 服务器,用npm start重新启动它,并在http://localhost:3000/hello访问我们的新页面:

Figure 13.5 - A new route, with the Network tab open, showing we are making a GET request

到目前为止,这似乎是非常基本的。 现在,让我们做一些不同的事情。 让我们使用这个视图并为 Ajax POST 请求创建一个表单:

  1. 创建一个新文件public/javascripts/index.js
  2. 写一个基本的fetch请求到端点/hello,POST JSON 为{ message: "This is from Ajax" },如下所示:
fetch('/hello', {
 method: 'POST',
 body: JSON.stringify({ message: "This is from AJAX" }),
 headers: {
   'Content-Type': 'application/json'
 },
});
  1. views/index.hbs中包含该文件如下:
<h1>{{title}}</h1>

<p>Welcome to {{title}}</p>

<p id="data">{{ data }}</p>

<script src="/javascripts/index.js"></script>

注意,我们不需要在路径中包含public。 这是因为 Express 已经理解了public中的文件是静态提供的,不需要 Express 进行任何干预或解析,而节点文件则必须运行。

  1. 如果现在重新加载页面,实际上不会看到任何令人兴奋的事情发生,因为我们还没有编写处理 POST 请求的路由。 这样写:
/* POST to sub page. */
router.post('/hello', function(req, res, next) {
  res.send(req.body);
});
  1. 重新加载页面,你会看到……什么都没有。 Network 选项卡中没有 POST,当然也没有呈现任何内容。 发生了什么事?

Node 有几个工具用于在代码更改时重新启动 Express 服务器,这样引擎就会自动刷新,而不需要像之前那样杀死并重新启动它,但这次我们没有这样做。 这些工具会随着时间而变化,但我喜欢的是 Supervisor:https://www.npmjs.com/package/supervisor。 在项目目录下执行npm install supervisor即可将其安装到项目中。

  1. 现在,打开项目根目录下的package.json文件。 你应该看到一个类似的文件,但可能有一些版本差异:
{
 "name": "my-webapp",
 "version": "0.0.0",
 "private": true,
 "scripts": {
 "start": "node ./bin/www"
 },
 "dependencies": {
 "cookie-parser": "~1.4.4",
 "debug": "~2.6.9",
 "express": "~4.16.1",
 "hbs": "~4.0.4",
 "http-errors": "~1.6.3",
 "morgan": "~1.9.1",
 "supervisor": "^0.12.0"
 }
}

这是运行npm install时所安装的核心。 运行它时,您将看到创建了一个node_modules目录,并在其中写入了许多文件。

If you're using version control such as Git, you will not want to commit the node_modules directory. With Git, you would include node_modules in a .gitignore file.

  1. 接下来我们要做的是改变我们的开始脚本,现在使用 Supervisor:
"scripts": {
     "start": "supervisor ./bin/www"
 },

要使用它,我们仍然使用npm start,要退出它,只需按Ctrl + C。 值得注意的是,Supervisor 最适合本地开发工作,而不是制作工作; 还有其他工具,比如 Forever,可以达到这个目的。

  1. 现在,让我们运行npm start,看看会发生什么。 您应该看到一些控制台消息,以 Press rs 结束以重新启动进程。 在大多数情况下,发行rs是不需要的,但如果你需要它,它就在那里:

Figure 13.6 - Response from Ajax!

  1. 因为我们从我们的前端 JavaScript 发送This is from AJAX,我们看到它反映在我们的响应 HTML! 现在,如果我们想要它在我们的页面中,我们可以在我们的前端 JavaScript 中这样做:
fetch('/hello', {
 method: 'POST',
 body: JSON.stringify({ message: "This is from AJAX" }),
 headers: {
   'Content-Type': 'application/json'
 },
}).then((res) => {
 return res.json();
}).then((data) => {
 document.querySelector('#data').innerHTML = data.message
});

我们将看到以下内容:

Figure 13.7 - A message from Ajax!

接下来,让我们了解如何保存数据。

保存数据

对于下一步,我们将数据持久化到本地数据存储中,这将是一个简单的本地 JSON 文件:

  1. 继续,用Ctrl + C退出 Express。 让我们安装一个简单的模块,将数据保存在本地存储:npm install data-store

  2. 让我们修改我们的路由来使用它,像这样:

var express = require('express');
var router = express.Router();

const store = require('data-store')({ path: process.cwd() + '/data.json' });

/* GET home page. */
router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express', data: 
 JSON.stringify(store.get()) });
});

/* GET sub page. */
router.get('/hello', function(req, res, next) {
 res.render('index', { title: 'Hello! This is a route!' });
});

/* POST to sub page. */
router.post('/hello', function(req, res) {
 store.set('message', { message: `${req.body.message} at ${Date.now()}` })

 res.set('Content-Type', 'application/json');
 res.send(req.body);
});

module.exports = router;
  1. 请注意store及其在hello/路线中的使用。 让我们也像这样修改我们的index.hbs文件:
<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>

<button id="add">Add Data</button>
<button id="delete">Delete Data</button>

<p id="data">{{ data }}</p>
<script src="/javascripts/index.js"></script>
  1. 稍后我们将使用Delete Data按钮,但现在,我们将使用Add Data按钮。 添加一些保存逻辑到public/javascripts/index.js如下:
const addData = () => {
 fetch('/hello', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify({ message: "This is from Ajax" })
 }).then((res) => {
   return res.json()
 }).then((data) => {
     document.querySelector('#data').innerHTML = data.message
 })
}
  1. 现在我们要添加点击处理程序:
document.querySelector('#add').addEventListener('click', () => {
 addData()
 window.location = "/"
})
  1. 如果你刷新/页面并点击 Add Data 按钮,你会看到如下内容:

Figure 13.8 - Adding data

  1. 现在,再次刷新该页面。 请注意,消息是持久的。 在文件系统中,您还应该注意到一个包含数据的data.json文件。

现在我们准备用 delete 方法来进一步处理这个问题。

删除

我们已经讨论了 GET 和 POST,现在是时候处理另一个基本的 REST 动词:DELETE

顾名思义,它的目标是从数据存储中删除数据。 我们已经有了这样的按钮,所以让我们把它连接起来:

  1. 在前端 JavaScript 中,我们将添加以下内容:
const deleteData = () => {
 fetch('/', {
   method: 'DELETE',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify({ id: 'message' })
 })
}
document.querySelector('#delete').addEventListener('click', () => {
 deleteData()
 window.location = "/"
})
  1. 现在,在路由中,添加这个路由:
/* DELETE from json and return to home page */
router.delete('/', function(req, res) {
 store.del(req.body.id);

 res.sendStatus(200);
});

这应该就是我们所需要的。 刷新您的索引页面,并尝试添加和删除按钮。 很简单,对吧? 在第 18 章Node.js 和 MongoDB中,我们将讨论在一个成熟的数据库中持久化和操作我们的数据,但现在,我们可以使用 GET, POST 和 DELETE 的知识。 我们将使用一个实际的数据库来处理 PUT。

的观点

我们在Routers一节中介绍了视图的操作,现在让我们深入讨论。 应用的视图层是表示层,这就是为什么它包含我们的前端 JavaScript。 虽然并不是所有后端 Node 应用都服务于前端,但是知道如何使用它是很方便的。 每当我建立一个简单的网络服务器,我达到 Express 和它的功能的前端和后端。

由于我们可以使用多种前端模板语言,让我们使用 Handlebars 作为逻辑和结构的示例。

如果需要,我们可以在前端代码中提供一些条件逻辑。 注意,这个逻辑是由后端呈现的,所以它是一个很好的例子,说明什么时候在后端呈现数据(这对前端来说更有性能),什么时候通过 JavaScript(表面上更灵活)。

让我们像这样改变我们的views/index.hbs文件:

{{#if data }}
 <p id="data">{{ data }}</p>
{{/if}}

让我们也修改一下routes/index.js:

/* GET home page. */
router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express', data: 
 JSON.stringify(Object.entries(store.get()).length > 0 ? store.get() :
  null) });
});

现在,我们使用三元运算符来简化显示逻辑。 由于我们来自商店的数据是 JSON,我们不能简单地测试它的长度:我们必须使用Object.entries方法。 如果你认为我们可以把store.get()保存为一个变量,而不是写两次,你是对的。 然而,在这种情况下,我们实际上不需要占用额外的内存空间,因为我们是立即返回它而不是操作它。 在这个场景中,性能影响可以忽略不计,但是要记住这一点。

现在,如果我们删除数据,我们会看到:

Figure 13.9 - After deleting data

看到空对象的花括号比看到空对象的花括号容易一点。 当然,我们可以通过编写一个更复杂的条件来完成前端的条件工作,但当后端可以向我们发送适当的数据时,为什么这样做呢? 当然,这两种情况都有,但在这种情况下,最好让每一块都发挥自己的作用。

你可以在这里找到我们完成的工作:https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13/my-webapp

现在让我们把注意力转向如何使用控制器将数据真正地导入 Express。

控制器和数据:在 Express 中使用 api

正如你可能在网上听到的,Express 很棒,因为它对你如何使用它不是很有意见,同时,人们说 Express 很难使用,因为它不够有意见! 虽然 Express 通常不是按照传统的模型-视图-控制器设置来设置的,但将功能从路由中分离出来并分成独立的控制器可能是有益的,特别是当你可能最终在路由之间拥有类似的功能并希望保持代码 DRY 时。

如果你不是很熟悉模型-视图-控制器(MVC)范式,不要担心——我们不会详细讨论它,因为它是一个非常重要的主题,有它自己的辩论和约定。 现在,我们只定义几个术语:

  • 模型是应用处理数据操作的一部分,特别是与数据库的通信。
  • 控制器处理来自路由的逻辑(即来自用户的 HTTP 请求的路径)。
  • 视图是终端客户端的表示层,它向客户端提供标记,由控制器路由。

这是 MVC 范例的样子:

Figure 13.10 - The MVC paradigm

让我们看一个示例应用。 Athttps://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13/controllers是一个使用 Express 的应用。

这是一个使用控制器和模型的 API。 正如我们将看到的,这个结构将简化我们的工作流程。 这仍然是一个相当简单的例子,但这会让你明白为什么控制器和模型可以派上用场。 让我们调查:

  1. 继续运行npm install,然后运行npm start来运行应用。 它应该可以在您的浏览器中http://localhost:3000访问,但如果您有任何其他正在运行的东西,Node 将警告您并声明不同的端口。 以下是你将看到的:

Figure 13.11 - Our sample Express application

  1. 到目前为止很简单。 继续,点击 Add User 几次,尝试一下这个功能。 这在后端使用随机的用户 API 来创建用户并将他们持久化到文件系统数据存储中。
  2. public/javascripts目录中检查客户端 JavaScript。 这些看起来应该很熟悉。 如果我们记住了fetch()调用的结构,它会返回一个承诺,因此我们可以使用.then()范式来对我们的事件作出反应。
  3. public/javascripts/index.js中,我们可以看到当我们点击 Add User 时创建用户的机制:
document.querySelector('.add-user').addEventListener('click', (e) => {
  fetch('/user', {
    method: 'POST'
  }).then( (data) => {
    window.location.reload()
  })
})

这应该没什么好奇怪的:我们在一个事件处理程序中使用 JavaScript 的fetch通过 POST 调用/user路由。 路由基本上是 Express(或其他)应用中的一个端点:它包含一些对事件作出反应的逻辑。 那么,这个逻辑是什么?

  1. 开放routes/user.js:
var express = require('express');
var router = express.Router();

const UsersController = require('../controllers/users');

/* GET all users. */
router.get('/', async (req, res, next) => {
  res.send(await UsersController.getUsers());
});

/* GET user. */
router.get('/:user', async (req, res, next) => {
  const user = await UsersController.getUser(req.params.user);
  res.render('user', { user: user });
});

/* POST to create user. */
router.post('/', async (req, res, next) => {
  await UsersController.createUser();
  res.send(await UsersController.getUsers());
});

/* DELETE user. */
router.delete('/:user', async (req, res, next) => {
  await UsersController.deleteUser(req.params.user);
  res.sendStatus(200);
});

module.exports = router;

让我们首先比较这个和其他例子的结构。 首先,我们将看到用于用户的控制器的require()语句。 这里有一个router.post()方法语句,它使用async/await对控制器进行异步调用。 然后我们的控制器将调用我们的模型来做数据库工作。

到目前为止,有很多文件和路径可以执行。 在我们沉浸在代码中之前,让我们看一下图,看看一个前端方法,比如 Add User 单击处理程序,是如何与 Express 后端通信的:

Figure 13.14 - End-to-end communication

从左到右、从上到下阅读,我们可以看到流程中的每个步骤是如何发挥其作用的。 对于从 API 中检索信息这样基本的事情来说,它可能看起来有点复杂,但这种体系结构模式的部分功能在于,每一层都可以由不同的一方编写和控制。 例如,模型层通常由数据库专家负责,而不是其他类型的后端开发人员。

当您跟踪控制器和模型的代码时,请考虑在代码的每一层分离关注点如何使设计更加模块化。 例如,我们使用 LocalStorage 数据库来存储用户。 如果我们想把 LocalStorage 换成更健壮的系统,比如 MongoDB,我们实际上只有一个文件可以编辑:模型。 事实上,甚至模型也可以被抽象为具有统一的数据处理程序,然后为特定的数据库方法使用适配器。

这对我们来说可能有点过头了,但接下来让我们将目光转向使用我们刚刚学到的原则创造一款星际飞船游戏。 我们将使用这个 Node.js 后台为我们的最终项目在 JavaScript 为游戏做一个前端。

在下一节中,我们将开始创建游戏的 API。

用 Express 创建 API

谁不喜欢像《星球大战》或《星际迷航》中那样的星际飞船大战呢? 我非常喜欢科幻小说,所以让我们使用存储、路由、控制器和模型来构建一个 RESTful API 来跟踪我们的游戏玩法。 虽然我们将重点放在这个应用的后端,但我们将为数据的填充和测试提供一个简单的前端。

你可以在https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13/starship-app找到一个正在工作的示例应用。 让我们从这里开始,您可以使用以下步骤完成它:

  1. 克隆存储库(如果还没有克隆的话)。
  2. 进入带有cd starship-app的目录,运行npm install
  3. npm start开始项目。
  4. 在浏览器中打开http://localhost:3000。 如果您已经在 3000 端口上运行了任何项目,start命令可能会提示您使用不同的端口。 这是我们的基本前端:

Figure 13.15 - Starship Fleet

  1. 继续尝试随机和手动添加和凿船。 这将是我们的游戏设置。
  2. 现在,让我们解包代码正在做什么。 下面是我们的文件结构:
.
├── README.md
├── app.js
├── bin
 └── www
├── controllers
 └── ships.js
├── data
 └── starship-names.json
├── models
 └── ships.js
├── package-lock.json
├── package.json
├── public
 ├── images
  └── bg.jpg
 ├── javascripts
  └── index.js
 └── stylesheets
 └── style.css
├── routes
 ├── index.js
 ├── ships.js
 └── users.js
└── views
 ├── error.hbs
 ├── index.hbs
 └── layout.hbs
  1. 打开public/javascripts/index.js。 让我们首先检查随机船只创建的事件处理程序:
document.querySelector('.random').addEventListener('click', () => {
 fetch('/ships/random', {
   method: 'POST'
 }).then( () => {
   window.location.reload();
 })
})

到目前为止还好。 这些看起来应该很熟悉。

  1. 让我们来看看这条路线:/ships/random。 打开routes/ships.js(我们可以猜测/ships/的路由将在ships.js文件中,但我们可以通过读取app.js文件中的路由来确认这一点,正如我们所了解的)。 阅读/random路线:
router.post('/random', async (req, res, next) => {
 await ShipsController.createRandom();
 res.sendStatus(200);
});

我们注意到的第一件事是,这是一个async/await构建,因为我们将在前端使用fetch,并在后端使用数据库。

  1. 接下来让我们看看控制器方法:
exports.createRandom = async () => {
 return await ShipsModel.createRandom();
}
  1. 很容易。 下面是模型方法:
exports.createRandom = async () => {
 const shipNames = require('../data/starship-names');
 const randomSeed = Math.ceil(Math.random() * 
  shipNames.names.length);

 const shipData = {
   name: shipNames.names[randomSeed],
   registry: `NCC-${Math.round(Math.random()*10000)}`,
   shields: 100,
   torpedoes: Math.round(Math.random()*255+1),
   hull: 0,
   speed: (Math.random()*9+1).toPrecision(2),
   phasers: Math.round(Math.random()*100+1),
   x: 0,
   y: 0,
   z: 0
 };

 if (storage.getItem(shipData.registry) || storage.values('name') 
 == shipData.name) {
   shipData.registry = `NCC-${Math.round(Math.random()*10000)}`;
   shipData.name = shipNames.names[Math.round(Math.random()*
    shipNames.names.length)];
 }
  await storage.setItem(shipData.registry, shipData);
 return;
}

好吧,这有点复杂,我们来解一下这个。 前几行只是从提供给您的种子文件中选择一个随机名称。 我们的shipData对象由几个键/值对构成,每个键/值对对应于我们新创建的船的特定属性。 之后,我们检查数据库,看看是否已经有该名称或注册号的船只。 如果是这样,我们将再次随机化。

然而,与每个应用一样,也有需要改进的地方。 给你一个挑战。

挑战

这是您的第一个任务:您能想到如何改进代码,以便在重新随机化中,它优雅地检查,看看是否新的随机化也存在于我们的数据库中? 提示:您可能想要创建一两个单独的 helper 函数。

https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13/starship-app-solution1):

const eliminateExistingShips = async () => {
 const shipNames = require('../data/starship-names');
 const ships = await storage.values();

 const names = Object.values(ships).map((value, index, arr) => {
   return value.name;
 });

 const availableNames = shipNames.names.filter((val) => {
   return !names.includes(val);
 });

 const unavailableRegistryNumbers = Object.values(ships).map((value, index, 
 arr) => {
   return value.registry;
 });

 return { names: availableNames, unavailableRegistries: 
 unavailableRegistryNumbers };
}

要使用它,执行以下命令:

exports.createRandom = async () => {
 const { names, unavailableRegistries } = await eliminateExistingShips();

 const randomSeed = Math.ceil(Math.random() * names.length);

 const shipData = {
   name: names[randomSeed],
   registry: `NCC-${Math.round(Math.random() * 10000)}`,
   shields: 100,
   torpedoes: Math.round(Math.random() * 255 + 1),
   hull: 0,
   speed: (Math.random() * 9 + 1).toPrecision(2),
   phasers: Math.round(Math.random() * 100 + 1),
   x: 0,
   y: 0,
   z: 0
 };

 while (unavailableRegistries.includes(shipData.registry)) {
   shipData.registry = `NCC-${Math.round(Math.random() * 10000)}`;
 }
 await storage.setItem(shipData.registry, shipData);
 return;
}

那么,我们在这里做什么? 首先,让我们看看Objects.map的用法:

const names = Object.values(ships).map((value, index, arr) => {
   return value.name;
});

在这里,我们使用ships对象的.map()方法来创建一个新的数组,其中只有是我们现有船只的名称。 本质上,我们所做的只是将对象的每个名称返回到数组中,所以现在我们有了一个可枚举的数据类型。

接下来,我们想要从我们的可能性中消除使用的名称,所以我们将使用数组的.filter()函数,只在它没有包含在我们之前创建的数组中的情况下返回值:

const availableNames = shipNames.names.filter((val) => {
   return !names.includes(val);
});

我们对注册表号执行与名称相同的操作,并返回一个对象。

现在,这里有一个新的技巧:解构一个对象。 看看这个:

 const { names, unavailableRegistries } = await eliminateExistingShips();

我们在这里做的是一次性赋值两个变量! 因为我们的eliminateExistingShips()方法返回一个对象,所以我们可以使用析构将其分解为单独的变量。 这不是完全必要的,但它通过减少使用点符号的次数而简化了我们的代码。

起。

船的属性

以下是我们为游戏定义的船只属性及其描述。 这张属性表对我们建造的所有船都是一样的,不管是随机的还是手动的:

| name | 一个字符串值。 | | 注册表 | 一个字符串值。 | | 护盾 | 盾牌强度的数量,初始化为 100。 当船受到伤害时,这个值会减少。 | | 鱼雷 | 表示该船拥有的鱼雷数量的数字。 每次我们在游戏中发射一枚鱼雷,这个值就会减少 1。 | | 船体 | 从 0 开始,这个数字表示在盾被耗尽后船体承受了多少伤害。 当这个数达到 100 时,飞船就毁了。 希望大家都到逃生舱了! | | speed | 从曲速 1 到 9.99,我们的飞船速度是可变的。 | | phasers | 没有相位枪的船是不完整的! 定义一个从 1 到 100 的随机数来指定激光炮造成的伤害。 | | x, y, z | 我们的船在三维空间中的位置坐标,从[0,0,0]开始。 在我们的游戏中,我们将坐标上限设为[100,100,100]。 我们可不想让我们的飞船在太空中迷路! |

对于我们的数据库,我们没有做任何复杂的事情; 我们正在使用一个名为node-persist的 Node 包。 它使用文件系统上的一个目录来存储值。 这是基本的,但它能完成任务。 我们将在第 18 章Node.js 和 MongoDB中进入真实的数据库。 注意,这些方法也是async/await函数,因为在代码与数据库(在本例中是我们的文件系统)交互时,我们预计会有一点延迟。

好的! 因为我们只是从函数中不返回任何东西,它将触发控制器方法的完成,然后返回到我们的路由并向前端返回一个200 OK消息。 根据我们的前端代码,页面然后重新加载,显示我们的新船。

这里有第二个需要改进的地方:是否可以使用 DOM 操作在不刷新页面的情况下将船只添加到页面中? 你必须修改堆栈的所有层次,通过返回随机值到前端来实现你的目标。

在你开始之前,让我们问自己一个重要的问题:用我们现在的结构这样做有意义吗? 如果你的思考过程导致了一个过于复杂的解决方案,就像我的一样,答案是否定的。 理所当然,处理 DOM 更新的最佳方法是利用我们拥有的另一个工具:框架。 我们将把它留在现在,但我们将在我们的最终项目第 19 章把它全部放在一起,,当我们创建一个完整的应用。

接下来,让我们看看星际飞船战斗将如何发生。 如果我们回到我们的船只路由器,我们会看到这条路线:

router.get('/:ship1/attack/:ship2', async (req, res, next) => {
 const damage = await ShipsController.fire(req.params.ship1, 
 req.params.ship2);
 res.sendStatus(200);
});

如果你能从航线的构造中猜到,该航线将以第一艘船的名字作为参数(ship1),然后是attack字符串,然后是第二艘船的名字。 这是一个 RESTful 路由的示例,以及 Express 如何处理路径参数。 在我们的控制器调用中,我们使用这些参数和控制器的.fire()方法。 在控制器中,我们看到:

exports.fire = async (ship1, ship2, weapon) => {
 const target = await ShipsModel.getShip(ship2);
 const source = await ShipsModel.getShip(ship1);
 let damage = calculateDamage(source, target, weapon);

 if (weapon == 'torpedo' && source.torpedoes > 0) {
   ShipsModel.fireTorpedo(ship1);
 } else {
   damage = 0
 }

 return damage;
}

现在我们玩得很开心。 您可以跟踪不同的模型片段,但我想指出calculateDamage辅助函数的使用。 你会在文件的顶部找到它。

对于伤害计算,我们将使用以下方法:

或者,用英语来说,“目标被源击中的几率由两艘飞船在三维空间中的距离 100 减去,概率在 0%到 100%之间。” 要计算这个值,取四舍五入 100 减去xyz坐标的平方和的平方根。” (是的,我得查一下三维空间中距离的计算。 如果这对你来说是陌生的,也不要担心。)

R1为 0 ~ 100 之间的伪随机值,取四舍五入。 在 JavaScript 中,就像在所有编程语言中一样,一个随机数在技术上只是一个伪随机数:

或者,“源的移相器可能造成的损害是由源的移相器功率的乘积通过一个Math.random()数字来计算的。”

然而,如果源发射鱼雷(并且有鱼雷剩余),那么可能造成**= 125的伤害。

设*R2*为 0 ~ 100 之间的伪随机数,四舍五入:

如果概率减去随机数大于 0,则损伤将作为可能损伤发生。 否则,不会造成损坏。

好了,现在我们有了计算。 你能找出 JavaScript 代码来做这件事吗?

这里是:

const calculateDamage = (ship1, ship2, weapon) => {
 const distanceBetweenShips = Math.sqrt(Math.pow(ship2.x - ship1.x, 2) + 
 Math.pow(ship2.y - ship1.y, 2) + Math.pow(ship2.z - ship1.z, 2));
 const chanceToStrike = Math.floor(100-distanceBetweenShips);
 const didStrike = (Math.ceil(Math.random()*100) - chanceToStrike) ? true : 
 false;
 const damage = (didStrike) ? ((weapon == 'phasers') ? 
 Math.ceil(Math.random()*ship1.phasers) : TORPEDO_DAMAGE) : 0;
 return damage;
}

为了完成我们的游戏,我们需要创造一种机制去射击并在前端记录伤害。

总结

我们在这一章中涵盖了很多内容,从路由到控制器再到模型。 请记住,不是每个应用都遵循这个范例,但它是一个很好的基线,可以用来开始与前端相关的后端服务方法。

我们应该记住,使用express-generator可以帮助支架脱离应用,使用npmnpx。 路由和视图是应用的前线,它指示代码在哪里路由,以及终端客户端查看什么(无论是 JSON 还是 HTML)。 我们与 API 合作,探索 API 固有的异步行为,并创建了我们自己的API!

*在下一章中,我们将讨论是什么使 Express 成为一种不同于 Django 或 Flask 的框架类型。 我们还将研究如何加入我们的前端和后端框架。

进一步的阅读