Skip to content

Latest commit

 

History

History
798 lines (586 loc) · 32.9 KB

File metadata and controls

798 lines (586 loc) · 32.9 KB

十八、Node.js 和 MongoDB

你可能听说过MEAN栈:MongoDB, Express, Angular, and Node.js,或者MERN栈:MongoDB, Express, React, and Node.js。 我们还没有讨论的缺失部分是 MongoDB。 让我们探讨一下如何直接从 Express 中使用这个 NoSQL 数据库。 我们将构建我们的星际飞船游戏的下一个迭代,我们开始在第 13 章使用 Express,除了这次使用 MongoDB 和合并一些测试!

在本章中,我们将讨论以下主题:

  • 使用 MongoDB
  • 测试与笑话
  • 存储和检索数据
  • 将 API 连接在一起

技术要求

准备使用存储库的chapter-18目录中提供的代码:https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-18。 因为我们将使用命令行工具,所以还要确保您的 Terminal 或命令行 shell 可用。 我们需要一个现代化的浏览器和本地代码编辑器。

使用 MongoDB

MongoDB 的基本前提,使其不同于其他类型的结构化的键/值对数据库是无模式:你可以插入任意非结构化数据文档,而不需要关心另一个数据库中的条目是什么样子。 用 NoSQL 术语来说,文档就是我们已经熟悉的东西:JavaScript 对象!

**这是一个文档:

{
 "first_name": "Sonyl",
 "last_name": "Nagale",
 "role": "author",
 "mood": "accomplished"
}

我们可以看到它是一个基本的 JavaScript 对象; 更具体地说,它是 JSON,这意味着它也可以支持嵌套数据。 这里有一个例子:

{
 "first_name": "Sonyl",
 "last_name": "Nagale",
 "role": "author",
 "mood": "accomplished",
 "tasks": {
  "write": {
   "status": "incomplete"
  },
  "cook": {
   "meal": "carne asada"
  },
  "read": {
   "book": "Le Petit Prince"
  },
  "sleep": {
   "time": "8"
  }
 },
 "favorite_foods": {
  "mexican": ["enchiladas", "burritos", "quesadillas"],
  "indian": ["saag paneer", "murgh makhani", "kulfi"]
 }
}

这和 MySQL 有什么不同呢? 考虑以下 MySQL 模式:

Figure 18.1 – An example of a MySQL database table structure

如果熟悉 SQL 数据库,就会知道数据库表中的每个字段类型都必须是特定类型的。 当从 SQL 类型的数据库中检索时,我们使用S****结构化查询语言(SQL)。 正如我们的表是结构化的,我们的查询也是结构化的。

我们需要在使用数据库表之前创建它们,在 SQL 中,建议不要在没有做一些额外清理工作的情况下更改它们的结构。 下面是我们如何创建前面的表:

CREATE TABLE `admins` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `admin_role_id` int(11) DEFAULT NULL,
 `first_name` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `last_name` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `username` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `email` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
 `phone` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
 `password` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
 `avatar` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
 `admin_role` enum('admin','sub_admin') COLLATE utf8_unicode_ci DEFAULT
  NULL,
 `status` enum('active','inactive','deleted') COLLATE utf8_unicode_ci 
  DEFAULT NULL,
 `last_login` datetime DEFAULT NULL,
 `secret_key` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
 `last_login_ip` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `sidebar_status` enum('open','close') COLLATE utf8_unicode_ci DEFAULT
  'open',
 `created` datetime DEFAULT NULL,
 `modified` datetime DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `email` (`email`),
 KEY `password` (`password`),
 KEY `admin_role` (`admin_role`),
 KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

现在,对于 MongoDB,我们不会构造具有预定义数据类型和长度的表。 相反,我们将 JSON blob 作为文档插入数据库。 MongoDB 背后的想法非常类似于我们在第 17 章,*Security and Keys,*中使用 Firebase 插入 JSON 并查询它,即使是使用多个嵌套 JSON 对象而不是存储、交叉连接和查询多个表。

假设我们有以下两个文档:

{
  "first_name": "Sonyl",
  "last_name": "Nagale",
  "admin_role": "admin",
  "status": "active"
},
{
  "first_name": "Jean-Luc",
  "last_name": "Picard",
  "admin_role": "admin",
  "status": "inactive"
}

如何将它们插入到数据库中? 这将是与 MySQL:

INSERT INTO
    admins(first_name, last_name, admin_role, status)
  VALUES
    ('Sonyl', 'Nagale', 'admin', 'active'),
    ('Jean-Luc', 'Picard', 'admin', 'inactive')

MongoDB 的答案实际上比 SQL 简单得多,因为我们可以轻松地放置数组,而不必担心数据类型或数据排序! 我们可以直接把文件塞进去,不担心其他事情,这更可能是我们从前线接收到的:

db.admins.insertMany([
{
  "first_name": "Sonyl",
  "last_name": "Nagale",
  "admin_role": "admin",
  "status": "active"
},
{
  "first_name": "Jean-Luc",
  "last_name": "Picard",
  "admin_role": "admin",
  "status": "inactive"
}]
)

现在,例如,要从前面的admins表中获取所有活跃的管理员,我们可以在 MySQL 中这样写:

SELECT
  first_name, last_name 
FROM 
  admins 
WHERE 
  admin_role = "admin" 
AND 
  status = "active"

first_namelast_name字段被预先定义为类型VARCHAR(变量字符),最大长度为 50 个字符。 admin_rolestatus是带有预定义可能值的ENUM(枚举类型)(就像站点上的下拉选择列表)。 然而,下面是我们如何在 MongoDB 中构造查询:

db.admins.find({ status: 'active', admin_role: 'admin'}, { first_name: 1, last_name: 1})

我们不会去深入到 MongoDB 语法这里,因为它有点超出了本书的范围,我们将只使用简单的查询。 话虽如此,在我们开始之前,我们应该多了解一点。

以下是我们在制作游戏时会用到的 mongo 命令列表:

  • find
  • findOne
  • insertOne
  • updateOne
  • updateMany

相当容易,对吧? 我们可以将许多 MongoDB 命令分解成以下一般的语法结构:

<dbHandle>.<collectionName>.<method>(query, projection)

在这里,queryprojection是指示 MongoDB 使用的对象。 例如,在前面的语句中,{ status: 'active', admin_role: 'admin' }是用来指定我们希望这些字段与这些值相等的查询。 这个例子中的投影指定了我们想要返回的内容。

让我们开始我们的项目吧。

开始

我们可以做的第一件事就是从https://MongoDBdb.com下载 MongoDB 社区服务器。 当你安装了它,从我们的 GitHub 仓库导航到chapter-18/starships目录,让我们试着启动它:

npm install
mkdir -p data/MongoDB
mongod --dbpath data/MongoDB

如果你已经正确安装了所有的东西,你应该会看到一连串的通知消息,以一个类似于[initandlisten] waiting for connections on port 27017的内容结尾。 如果一切按照计划进行而不是,请花一些时间确保您的安装工作正常。 一个有用的工具是 MongoDB Compass,这是一个用于连接 MongoDB 的 GUI 工具。 请确保检查权限,并确保适当的端口是开放的,因为我们将使用端口27017(MongoDB 的默认端口)进行连接。

这一章将是一个将我们的星际飞船游戏带到下一个级别的实验练习。 以下是我们将要构建的内容:

Figure 18.2 – Creating our fleet

然后,我们将把它连接到 MongoDB,并在这个接口中实际执行游戏玩法:

Figure 18.3 – Attack the enemy!

我们将使用 MERN 的简化版本,并使用香草 JavaScript 代替 React,依靠 Express 以一种比 React 更少控制的方式渲染我们的 HTML。 也许JEMN stack是个好名字?

在开始编写实际代码之前,让我们检查一下项目的设置,并开始进行测试!

测试与笑话

starships目录中,你可以找到完整的游戏。 让我们仔细分析它。

下面是目录列表:

.
├── README.md
├── app.js
├── bin
   └── www
├── controllers
   └── ships.js
├── jest-MongoDBdb-config.js
├── jest.config.js
├── models
   ├── MongoDB.js
   ├── setup.js
   └── ships.js
├── package-lock.json
├── package.json
├── public
   ├── images
      └── bg.jpg
   ├── javascripts
      ├── index.js
      └── play.js
   └── stylesheets
       ├── micromodal.css
       └── style.css
├── routes
   ├── enemy.js
   ├── index.js
   ├── play.js
   ├── ships.js
   └── users.js
├── tests
   ├── setup.model.test.js
   ├── ships.controller.test.js
   └── ships.model.test.js
└── views
    ├── enemy.hbs
    ├── error.hbs
    ├── index.hbs
    ├── layout.hbs
    └── play.hbs

我们将采取一点不同于我们其他项目的方法,实现一个非常轻的测试驱动开发(TDD)周期。 TDD 是在编写有效代码之前,编写失败的测试的实践。 虽然我们并没有实现真正的 TDD,但是使用测试来指导我们的思维过程的想法是我们将要做的。

我们将使用 Jest 作为我们的测试框架。 让我们看看这些步骤:

  1. tests目录下,创建一个新文件test.test.js。 第一个test是我们的测试套件的名称,在.test.js中结束文件的约定指示测试框架这是一个要执行的测试套件。 在文件中,创建这个测试脚本:
describe('test', () => {
 it('should return true', () => {
   expect(1).toEqual(1)
 });
});
  1. 使用node_modules/.bin/jest test.test.js运行测试(确保您已经运行了npm install!) 您将从测试套件中得到类似如下的输出:
$ node_modules/.bin/jest test.test.js
 PASS  tests/test.test.js
  test
     should return true (2ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.711s, estimated 1s
Ran all test suites matching /test.test.js/i.

我们刚刚编写了第一个测试套件! 它只是说,我希望 1 等于 1。 如果是,通过测试。 如果不是,那就通过测试。” 对于 5 行代码来说非常强大,对吧? 好吧,也许不是,但这将为我们提供所有其他测试的脚手架。

  1. models/mongo.js:
const MongoClient = require('mongodb').MongoClient;
const client = new MongoClient("mongodb://127.0.0.1:27017", { useNewUrlParser: true, useUnifiedTopology: true });

let db;
  1. 到目前为止,我们只是在建立 MongoDB 连接。 确保你的 MongoDB 连接还在运行:
const connectDB = async (test = '') => {
 if (db) {
   return db;
 }

 try {
   await client.connect();
   db = client.db(`starships${test}`);
 } catch (err) {
   console.error(err);
 }

 return db;
}
  1. 和所有好的数据库连接代码一样,我们在一个try/catch块中执行代码,以确保我们的连接是正确的:
const getDB = () => db

const disconnectDB = () => client.close()

module.exports = { connectDB, getDB, disconnectDB }

预览:我们将使用这个MongoDB.js文件从我们的测试和模型。 module.exports行指定从该文件导出哪些函数并暴露给程序的其他部分。 我们将在整个程序中一致地使用这个 export 指令:当我们想要公开一个方法时,我们将使用 export。

  1. 返回到test.test.js,并在文件的开头包含 MongoDB 模型:
const MongoDB = require('../models/mongo')
  1. 现在,让我们用我们的测试套件来做一些更有趣的事情。 在 ourdescribe方法中添加以下代码*:*
let db

beforeAll(async () => {
   db = await MongoDB.connectDB('test')
})

afterAll(async (done) => {
   await db.collection('names').deleteMany({})
   await MongoDB.disconnectDB()
   done()
})

并在简单的测试后添加以下情况:

it('should find names and return true', async () => {
   const names = await db.collection("names").find().toArray()
   expect(names.length).toBeGreaterThan(0)
})

然后使用与之前相同的命令node_modules/.bin/jest test.test.js运行。

这里发生了什么? 首先,在我们的测试套件中进行每一个单独的测试之前,我们都指定要按照我们在 MongoDB 模型中编写的方法连接到数据库。 在所有事情都完成之后,拆除数据库并断开连接。

当我们运行它时会发生什么? 一个史诗般的失败!

$ node_modules/.bin/jest test.test.js
 FAIL  tests/test.test.js
  test
     should return true (2ms)
     should find names and return true (9ms)

   test > should find names and return true

    expect(received).toBeGreaterThan(expected)

    Expected: > 0
    Received:   0

      20 |   it('should find names and return true', async () => {
      21 |     const names = await db.collection("names"
                ).find().toArray()
    > 22 |     expect(names.length).toBeGreaterThan(0)
         |                          ^
      23 |   })
      24 | });

      at Object.<anonymous> (tests/test.test.js:22:26)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.622s, estimated 2s
Ran all test suites matching /test.test.js/i.

我们应该期待一个错误,因为我们还没有向名为names的集合(或任何其他数据!)插入任何信息。 欢迎来到 TDD:我们编写了一个失败的测试,然后才编写代码使其通过。

显然,该过程的下一步是实际插入一些数据! 做一下。

存储和检索数据

让我们使用我编写的测试套件来帮助确保我们的 MongoDB 连接更健壮,包括插入数据到数据库,然后测试以确保它存在:

  1. 检验test/setup.model.test.js:
const MongoDB = require('../models/mongo')
const insertRandomNames = require('../models/setup')

describe('insert', () => {
 let db

 beforeAll(async () => {
   db = await MongoDB.connectDB('test')
 })

 afterAll(async (done) => {
   await db.collection('names').deleteMany({})
   await MongoDB.disconnectDB()
   done()
 })

 it('should insert the random names', async () => {
   await insertRandomNames()

   const names = await db.collection("names").find().toArray()
   expect(names.length).toBeGreaterThan(0)
 })

})
  1. 如果我们运行node_modules/.bin/jest setup,我们将看到成功,因为insertRandomNames()方法存在于我们的设置模型中。 让我们来看看我们的设置模型(models/setups.js),看看它是如何填充数据库的:
const fs = require('fs')
const MongoDB = require('./mongo')

let db

const setup = async () => {
 db = await MongoDB.connectDB()
}

const insertRandomNames = async () => {
 await setup()

 const names = JSON.parse(fs.readFileSync(`${__dirname}/../
  data/starship-names.json`)).names

 const result = await db.collection("names").updateOne({ key: 
  "names" }, { $set: { names: names } }, { upsert: true })

 return result
}

module.exports = insertRandomNames
  1. 不是太坏! 我们有一个导出的方法,它根据我提供的“随机”星际飞船名称的 JSON 文件将名称插入到数据库中。 读取文件并将其放入数据库,如下所示:
db.collection("names").updateOne({ key: "names" }, { $set: { names: names } }, { upsert: true })

由于我们没有深入到 MongoDB 本身的内部,足以说这一行翻译为“在names集合(即使它还不存在),设置names键等于 JSON。 必要时更新或插入”。

现在,我们可以用从现在开始使用的船名填充数据库。 执行npm run install-data

到目前为止,一切顺利! 在这个项目中有很多文件,所以我们不会遍历所有的; 让我们检查一个代表性的样本。

模型、视图和控制器

模型-视图-控制器(MVC)范式是我们在 Express 中使用的。 虽然在 Express 中并不是必需的,但我发现逻辑上的关注点分离是有用的,而且比处理无差别文件的整体类型更容易。 在我们深入讨论之前,我要提到 MVC 可能被认为是一种过时的模式,因为它确实在层之间创建了一些额外的依赖关系。 尽管如此,在 MVC 中,将逻辑分离为离散参与者的架构范例背后的思想是合理的。 你可能会听到用MV代替,它基本上应该被理解为“模型,视图,以及任何将它们绑定在一起的东西。” 目前,MV在某些框架中使用更为普遍。

MVC 结构将程序的逻辑分为三个部分:

  1. 模型处理数据交互。
  2. 视图处理表示层。
  3. 控制器处理数据操作,充当模型和视图之间的粘合剂。

以下是设计模式的可视化表示:

Figure 18.4 – The MVC paradigm's lifecycle

要理解这种关注点分离的一个更重要的部分是,视图层和控制器层不应该直接与数据存储交互; 这个荣誉是留给模特的。

现在让我们看一个视图:

视图/索引。 hbs

<h1>Starship Fleet</h1>

<hr />

<h2>Fleet Status</h2>
{{#if ships.length}}
 <table class="table">
   <tr>
     <th>Name</th>
     <th>Registry</th>
     <th>Top Speed</th>
     <th>Shield Strength</th>
     <th>Phaser Power</th>
     <th>Hull Damage</th>
     <th>Torpedo Complement</th>
     <th></th>
   </tr>
 {{#each ships}}
   <tr data-ship="{{this.registry}}">
     <td>{{this.name}}</td>
     <td>{{this.registry}}</td>
     <td>{{this.speed}}</td>
     <td>{{this.shields}}</td>
     <td>{{this.phasers}}</td>
     <td>{{this.hull}}</td>
     <td>{{this.torpedoes}}</td>
     <td><a class="btn btn-primary scuttle">Scuttle Ship</a></td>
   </tr>
 {{/each}}
 </table>
{{else}}
 <p>The fleet is empty. Create some ships below.</p>
{{/if}}

Express 控制我们的视图,我们使用 Handlebars 来处理我们的模板逻辑和循环。 虽然语法简单,但 Handlebars 功能强大,可以极大地简化我们的生活。 在本例中,我们正在测试和循环ships变量,以便创建我们拥有的ships的表,或者发送舰队为空的消息。 我们的观点如何得到ships? 它是通过我们的路由给我们的控制器视图。 这部分看起来是这样的:

routes/index.js

var express = require('express');
var router = express.Router();
const ShipsController = require('../controllers/ships');

/* GET home page. */
router.get('/', async (req, res, next) => {
 res.render('index', { ships: await ShipsController.getFleet() });
});

module.exports = router;

Why are we using var here instead of const or let? And why the semicolons? The answer is this: at the time of writing, the Express scaffolding tool still uses var and semicolons. It's always a best practice to standardize, but in this example I wanted to call attention to this fact. Feel free to standardize on the newer syntax as we work forward.

现在使用getFleet方法:

controller /ships.js

exports.getFleet = async (enemy = false) => {
 return await ShipsModel.getFleet(enemy)
}

因为这是一个简单的示例,所以我们的控制器除了从查询 MongoDB 的模型中获取信息之外没有做什么。 让我们来看看:

models/ships.js

exports.getFleet = async (enemy) => {
 await setup()

 const fleet = await db.collection((!enemy) ? "fleet" :
 "enemy").find().toArray();
 return fleet.sort((a, b) => (a.name > b.name) ? 1 : -1)
}

设置函数指示连接到 MongoDB(注意异步/等待设置!),我们的舰队要么来自敌人,要么来自我们的舰队集合。 该return线包含一个方便的排序舰队按字母顺序。

在本例中,我们将保持控制器相当简单,并依赖模型来完成繁重的工作。 这是一个风格上的决定,不过最好选择应用的一侧来完成大部分工作。

是时候从头到尾地看看这个程序了。

将 API 连接在一起

为了进一步理解游戏玩法,我们将通过从船上发射鱼雷的步骤:

  1. public/javascripts/play.js中找到前端 JavaScript:
document.querySelectorAll('.fire').forEach((el) => {
 el.addEventListener('click', (e) => {
   const weapon = (e.target.classList.value.indexOf('fire-torpedo') 
   > 0) ? "torpedo" : "phasers"
   const target = e.target.parentNode.getElementsByTagName
   ('select')[0].value
  1. 在这里,我们在我们的界面上做了一个点击处理fire按钮,并确定了我们的武器和目标船:
fetch(
`/play/fire?  attacker=${e.target.closest('td').dataset.attacker}&target=${target}&weapon=${weapon}`)
.then(response => response.json())
.then(data => {

这条线可能需要一点拆封。 我们从 JavaScript 中使用特定的查询字符串参数attackertargetweapon对我们的 Node 应用进行 AJAX 调用。 我们还希望从应用中返回 JSON。

  1. 记住,我们的反引号允许我们${ }中的变量组成字符串:
const { registry, name, shields, torpedoes, hull, scuttled } = data.target
  1. 我们使用对象解构来提取data.target中包含的每一条信息。 这比一个一个地定义它们或甚至用循环定义它们更有效率,对吧?
if (scuttled) {
       document.querySelector(`[data-ship=${registry}]`).remove()
       document.querySelectorAll(`option[value=${registry}]`).
        forEach(el => el.remove())

       const titleNode = document.querySelector("#modal-1-title")

       if (data.fleet.length === 0) {
         titleNode.innerHTML = "Your fleet has been destroyed!"
       } else if (data.enemyFleet.length === 0) {
         titleNode.innerHTML = "You've destroyed the Borg!"
       } else {
         titleNode.innerHTML = `${name} destroyed!`
       }

       MicroModal.show('modal-1')
       return
     }
  1. 如果scuttledtrue,我们的目标船已经被摧毁了,所以让我们告诉用户。 在这两种情况下,我们都要编辑船的值:
     const targetShip = document.querySelector(`[data-
      ship=${registry}]`)

     targetShip.querySelector('.shields').innerHTML = shields
     targetShip.querySelector('.torpedoes').innerHTML = torpedoes
     targetShip.querySelector('.hull').innerHTML = hull

   })
 })
})

这就是前端代码。 如果我们看看我们的app.js文件,我们可以看到我们的 AJAX 调用/play从一个app.use语句到playRouter。 因此,我们的下一站是路由器:

routes/play.js

const express = require('express');
const router = express.Router();
const ShipsController = require('../controllers/ships');

router.get('/', async (req, res, next) => {
 res.render('play', { fleet: await ShipsController.getFleet(), enemyFleet:
  await ShipsController.getFleet(true) });
});

router.get('/fire', async (req, res, next) => {
 res.json(await ShipsController.fire(req.query.attacker, req.query.target, 
  req.query.weapon));
});

module.exports = router;

因为我们的 URL 是由/play/fire构造的,所以我们知道第二个router.get语句是处理请求的语句。 转到控制器及其fire方法:

controller /ships.js

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

 return { target: target, fleet: await this.getFleet(false), enemyFleet: 
  await this.getFleet(true) }
}

在前面的代码中,我们看到了控制器和模型之间的粘合。 首先,我们要找到目标和源船。 你认为我为什么决定用let作为目标,const作为源? 如果你认为目标需要是可变的,那么你是对的:当我们在目标上使用registerDamage方法时,重写变量比创建一个新变量更有效。

在我们查看模型的registerDamage方法之前,请确认到目前为止的返回路径是控制器将返回到返回到前端脚本的路由。

向前!

models/ships.js

exports.registerDamage = async (ship, damage) => {
 const enemy = (!ship.registry.indexOf('NCC')) ? "fleet" : "enemy"
  const target = await db.collection(enemy).findOne({ registry:
   ship.registry })

 if (target.shields > damage) {
   target.shields -= damage
 } else {
   target.shields -= damage
   target.hull += Math.abs(target.shields)
   target.shields = 0
 }

 await db.collection(enemy).updateOne({ registry: ship.registry }, { $set: { shields: target.shields, hull: target.hull } })
  if (target.hull >= 100) {
   await this.scuttle(target.registry)
   target.scuttled = true
 }

 return target
}

现在在这里是我们实际与数据库通信的地方。 我们可以看到,我们正在检索目标,记录它的护盾和船体的伤害,在 MongoDB 中设置这些值,最终通过控制器返回目标船的信息,最终到达前端 JavaScript。

让我们看一下这一行:

await db.collection(enemy).updateOne({ registry: ship.registry }, { $set: { shields: target.shields, hull: target.hull } })

我们将更新收集中的一件物品,以说明它是敌人的船还是我们的舰队,并设置盾牌强度和船体伤害。

导出功能

到目前为止,您可能已经注意到一些模型方法,如registerDamage,以exports开头,而其他方法,如eliminateExistingShips,则没有。 在复杂的 JavaScript 应用中,良好的设计的一个方面是封装那些不是设计用来在特定上下文之外使用的函数。 当以exports开头时,函数可以从不同的上下文中调用,比如从控制器中调用。 如果它的设计不是面向应用的其余部分; 从本质上说,这是一个私人功能。 导出变量的概念与作用域的概念相似,因为我们要确保我们的应用是干净的,并且只公开程序中有用的部分。

如果我们看一下eliminateExistingShips,我们可以看到,它只是一个助手函数使用的createRandom,以确保我们没有分配相同的船舶注册号或名称给两艘不同的船舶。 我们可以在createRandom中看到这种用法:

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

const shipData = {
  name: (!enemy) ? names[randomSeed] : "Borg Cube",

更多代码…

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

为了确保我们的船舶注册号对我们的舰队来说是唯一的,我们将使用一个while循环来不断更新船舶注册号,直到它不是一个已经存在的。 使用eliminateExistingShipshelper 函数,我们返回并分解舰队中已经存在的名称和注册表,这样就不会创建重复的注册表。

我们不经常使用while循环,因为它们经常阻塞程序中的点,很容易被误用。 话虽如此,这是一个很好的while循环用例:它确保我们的程序不能继续,除非船舶的注册是唯一的。 对于 10,000 的随机乘数,不太可能在一行中生成两次或更多重复的随机注册表,所以使用while循环是合适的。

所以,出口还是不出口,这是个问题。 答案取决于我们是否需要在函数的直接作用域之外使用它。 如果该函数在程序的其他部分中没有用处,那么就不应该导出它。 在这种情况下,我们需要识别舰船的细节是否已经存在于舰队中,这只在我们的ships模型中有用,所以我们将避免导出它。

改善我们的程序

当你阅读ships模型和控制器时,我相信你能找到需要改进的地方。 例如,我所写的理解舰船是在我们的舰队还是敌人的舰队中的切换方式有点死板:它不可能在一场战斗中容纳三个独立的舰队。 每个程序员都会产生技术债务,或者是代码中的小错误或低效。 这就需要重构,即修改代码以使其更好。 不要误以为你曾经写过完美程序——这样的东西是不存在的。 改进和持续的迭代是编程过程的一部分。

然而,重构有一个重要的警告,那就是通常所说的契约。 当设计一个后端供前端使用时,当不同的方编写系统的不同部分时,重要的是彼此保持同步,以及程序作为一个整体的前提和需求保持同步。

让我们以前端 JavaScript 代码为例。 如果我们枚举它使用的端点,我们将看到四个端点正在被使用:

  • /ships
  • /ships/${e.currentTarget.closest('tr').dataset.ship}
  • /ships/random
  • `/play/fire?attacker=${e.target.closest('td').dataset.attacker}&target=${target}&weapon=${weapon}``

至少,当我们重构后端代码时,我们应该承担契约义务,既不改变这些端点的路径,也不改变接收到的数据类型的期望。

有一种方法可以帮助我们的代码更具前瞻性,那就是内联文档,使用一个叫做 JSDoc 的松散标准。 从代码注释创建文档是一种长期存在的实践,许多语言都存在注释结构,以促进标准的形成。 在 api 之类的情况下,通常会对源代码运行一个帮助程序来生成独立的文档,通常是一个小型 HTML/CSS 微站点。 您可能遇到过具有类似样式的在线文档的不相关程序。 这些不相关的文档站点很有可能是通过相同的机制从代码生成的。

为什么这在关于 MongoDB 的一章中很重要? 文档不仅仅是数据库使用的需要; 相反,在创建任何具有多个活动部分的程序时,它是很重要的。 考虑前面列表中的最后一个端点:`/play/fire?attacker=${e.target.closest('td').dataset.attacker}&target=${target}&weapon=${weapon}``。

fire 端点有三个参数:attackertargetweapon。 但这些参数是什么呢? 它们看起来像什么——它们是物体吗? 字符串? 布尔值吗? 数组? 此外,如果我们要接受用户生成的数据,我们需要比以前更加小心,因为GIGO:Garbage In, Garbage Out。 如果我们用坏数据填充数据库,那么我们所能期待的最好结果就是一个坏程序。 事实上,我们所能期待的最坏的情况是安全泄露:数据库或服务器凭证泄漏或恶意代码执行。 让我们谈谈安全问题。

安全

如果您熟悉 SQL,那么您可能熟悉一个名为SQL 注入的安全漏洞。 有关 web 应用安全最佳实践的良好信息可以在owasp.org上找到。 打开 Web 应用安全性项目(OWASP)是一个社区驱动的主动安全漏洞上的目录和教育用户在 Web 应用中,以便我们能更有效地打击恶意黑客。 如果你的邮箱、社交账户或网站被黑客入侵过,你就会知道随之而来的痛苦——数字身份盗窃。 OWASP 的 SQL 注入清单在这里:https://owasp.org/www-community/attacks/SQL_Injection

那么,如果我们使用的是 MongoDB 形式的 NoSQL 数据库,为什么还要讨论 SQL 呢? 因为 MongoDB 中不存在SQL 注入。 “太棒了!”你可能会说,“我的安全问题解决了!” 不幸的是,事实并非如此。 再加上重构以提高应用的效率,重构以减少安全入侵向量是 web 应用的重要组成部分。 我曾在一家被黑的公司工作过——原因是 URL 中输入的字符不到五个。 这使得黑客能够破坏 web 应用的运行,并执行任意的 SQL 命令。 对所有用户生成的内容进行安全消毒和重构是 web 安全的重要组成部分。 现在,我们还没有为这个应用这样做,因为我相信您不会黑自己的机器。

等待。 我刚才不是说 MongoDB 不存在 SQL 注入吗? 是的,NoSQL 数据库也有类似的攻击方法:代码和命令注入。 因为我们还没有对用户输入的完整性进行消毒或验证,所以应用可以存储和使用已提交并存储在数据库中的任意代码。 虽然关于 JavaScript 安全性的完整入门知识不在本书的范围之内,但请记住它。 长话短说就是对用户生成的输入进行消毒或验证。

到此为止,让我们结束这一章。 只要记住,在野外编写 MongoDB 应用时要确保安全!

总结

JavaScript 不是孤立存在的! MongoDB 是 JavaScript 的好伙伴,因为它的设计是面向对象的,并且依赖于 JavaScript 友好的查询语法。 我们学习了 TDD 背后的原则,使用了 MVC 范式,并进一步扩展了我们的游戏。

与所有的编码练习一样,在使用 MongoDB 这样的数据库时,一定要考虑用例:尽管 MongoDB 的语法不容易受到 SQL 注入的影响,但它仍然容易受到其他类型的注入的影响,这些注入会危及您的应用。

希望我们的星际飞船游戏足够有趣,让你继续开发它。 我们的下一章(也是最后一章)将总结我们的 JavaScript 开发原则并完善我们的游戏。**