From 33058f2b292b3a581333bdfb21b8f671898c5060 Mon Sep 17 00:00:00 2001 From: Peter Bengtsson Date: Tue, 8 Dec 2020 14:40:17 -0500 Subject: initial commit --- .../express_nodejs/deployment/index.html | 526 +++++++++++++ .../development_environment/index.html | 390 ++++++++++ .../displaying_data/author_detail_page/index.html | 89 +++ .../displaying_data/author_list_page/index.html | 89 +++ .../displaying_data/book_detail_page/index.html | 112 +++ .../displaying_data/book_list_page/index.html | 70 ++ .../index.html | 91 +++ .../bookinstance_list_page/index.html | 69 ++ .../date_formatting_using_moment/index.html | 58 ++ .../flow_control_using_async/index.html | 139 ++++ .../displaying_data/genre_detail_page/index.html | 120 +++ .../displaying_data/home_page/index.html | 134 ++++ .../express_nodejs/displaying_data/index.html | 87 +++ .../locallibrary_base_template/index.html | 69 ++ .../displaying_data/template_primer/index.html | 149 ++++ .../forms/create_author_form/index.html | 155 ++++ .../forms/create_book_form/index.html | 212 ++++++ .../forms/create_bookinstance_form/index.html | 150 ++++ .../forms/create_genre_form/index.html | 298 ++++++++ .../forms/delete_author_form/index.html | 165 ++++ .../server-side/express_nodejs/forms/index.html | 286 +++++++ .../forms/update_book_form/index.html | 189 +++++ .../learn/server-side/express_nodejs/index.html | 72 ++ .../installing_on_pws_cloud_foundry/index.html | 242 ++++++ .../express_nodejs/introduction/index.html | 525 +++++++++++++ .../server-side/express_nodejs/mongoose/index.html | 831 +++++++++++++++++++++ .../server-side/express_nodejs/routes/index.html | 425 +++++++++++ .../express_nodejs/skeleton_website/index.html | 476 ++++++++++++ .../tutorial_local_library_website/index.html | 93 +++ 29 files changed, 6311 insertions(+) create mode 100644 files/zh-cn/learn/server-side/express_nodejs/deployment/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/development_environment/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/author_detail_page/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/author_list_page/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/book_detail_page/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/book_list_page/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/bookinstance_detail_page_and_challenge/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/bookinstance_list_page/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/date_formatting_using_moment/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/flow_control_using_async/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/genre_detail_page/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/home_page/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/locallibrary_base_template/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/displaying_data/template_primer/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/forms/create_author_form/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/forms/create_book_form/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/forms/create_bookinstance_form/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/forms/create_genre_form/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/forms/delete_author_form/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/forms/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/forms/update_book_form/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/installing_on_pws_cloud_foundry/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/introduction/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/mongoose/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/routes/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/skeleton_website/index.html create mode 100644 files/zh-cn/learn/server-side/express_nodejs/tutorial_local_library_website/index.html (limited to 'files/zh-cn/learn/server-side/express_nodejs') diff --git a/files/zh-cn/learn/server-side/express_nodejs/deployment/index.html b/files/zh-cn/learn/server-side/express_nodejs/deployment/index.html new file mode 100644 index 0000000000..b4802ca3c5 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/deployment/index.html @@ -0,0 +1,526 @@ +--- +title: 'Express 教程 7: 部署到生产环境' +slug: learn/Server-side/Express_Nodejs/deployment +tags: + - Express + - Learn + - Node + - 初学者 + - 部署 +translation_of: Learn/Server-side/Express_Nodejs/deployment +--- +
{{LearnSidebar}}
+ +
{{PreviousMenu("Learn/Server-side/Express_Nodejs/forms", "Learn/Server-side/Express_Nodejs")}}
+ +

现在你已经创建(并测试)了一个不错的 本地图书馆 网站了,你打算把它发布到一个公共网络服务器,这样图书馆职工和网络上的其他成员就可以访问它了。这篇文章总结了你可以怎样找到一台主机部署你的网站,以及你需要为站点准备到生产环境做什么。

+ + + + + + + + + + + + +
预备知识:完成前面所有的指南主题,包括 Express Tutorial Part 6: Working with forms.
目标:学习你可以怎样以及在哪里部署一个Express 应用到生产环境。
+ +

概览

+ +

一旦您的站点完成(或完成“足够”以开始公共测试),您将需要将其托管在比您的个人开发计算机,更公开和可访问的地方。

+ +

到目前为止,您一直在开发环境中工作,使用 Express / Node 作为Web服务器,将您的站点共享到本地浏览器/网络,并使用(不安全的)开发设置运行您的网站,以显示调试和其他私人信息。在您可以在外部托管网站之前,您首先必须:

+ + + +

本教程提供了,有关选择托管站点的选项的一些指导,简要概述了为使您的Express 应用程序准备好生产,所需执行的操作,以及如何将LocalLibrary 网站安装到 Heroku云托管上的工作示例服务。

+ +

请记住,您不必使用Heroku  - 还有其他托管服务可用。我们还提供了一个单独的教程,以展示如何在 PWS/Cloud Foundry 上安装LocalLibrary。

+ +

什么是生产环境?

+ +

生产环境是服务器计算机提供的环境,您可以在其中运行网站,以供外部使用。环境包括:

+ + + +

服务器计算机,可以位于您的场所,并通过快速链接,连接到 Internet,但使用 “托管在云上” 的计算机更为常见。这实际上意味着,您的代码运行在托管公司的数据中心的某台远程计算机(或可能是“虚拟”计算机)。远程服务器,通常会以特定价格提供互联网连接,和一些保证级别的计算资源(例如CPU,RAM,存储器等)。

+ +

这种可远程访问的计算/网络硬件,称为基础架构即服务(IaaS)。许多IaaS 供应商,提供预安装特定操作系统的选项,您必须在其上,安装生产环境的其他组件。其他供应商,允许您选择功能更全面的环境,可能包括完整的 node 设置。

+ +
+

注意: 预构建环境,可以使您的网站设置变得非常简单,因为它们会减少配置,但可用选项可能会限制您使用不熟悉的服务器(或其他组件),并且可能基于较旧版本的操作系统。通常最好自己安装组件,以便获得所需的组件,并且当您需要升级系统的某些部分时,您可以知道从哪里开始!

+
+ +

其他托管服务提供商,支持 Express 作为平台即服务(PaaS)产品的一部分。使用此类托管时,您无需担心大多数生产环境(服务器,负载平衡器等),因为主机平台会为您处理这些问题。这使得部署非常简单,因为您只需要专注于 Web 应用程序,而不是任何其他服务器基础结构。

+ +

一些开发人员选择 IaaS ,相对于 PaaS ,IaaS 提供更高灵活性,而其他开发人员偏好 PaaS 的降低维护开销,和更轻松的扩展性。当您在一开始使用时,在 PaaS 系统上设置您的网站,要容易得多,因此我们将在本教程中使用 PaaS。

+ +
+

提示: 如果您选择Node/Express友好的托管服务提供商,他们应该提供,有关如何使用Web服务器,应用程序服务器,反向代理等不同配置,来设置 Express 网站的说明。例如,在数字海洋node社区文档中,有许多各种配置的手把手指南。

+
+ +

选择一个主机供应商

+ +

众所周知,众多托管服务提供商,都积极支持或与Node(和Express)合作。这些供应商提供不同类型的环境(IaaS,PaaS),以及不同价格的不同级别的计算和网络资源。

+ +
+

提示: 有很多托管解决方案,他们的服务和定价,可能会随着时间而改变。虽然我们在下面介绍几个选项,但在选择托管服务提供商之前,有必要自己进行互联网搜索。

+
+ +

选择主机时需要考虑的一些事项:

+ + + +

当你刚开始时,好消息是有很多网站提供“免费”的计算环境,尽管有一些条件。例如, Heroku “永远” 提供免费但资源有限的PaaS 环境,而 Amazon Web Services, Microsoft Azure 和开源选项 PWS/Cloud Foundry 在您第一次加入时,提供免费信用额度。

+ +

许多提供商还拥有“基本”层,可提供更多有用的计算能力,和更少的限制。举例来说, Digital Ocean 是一个流行的托管服务提供商,它提供了一个相对便宜的基本计算层(在本教程写作时,是每月5美元的较低范围)。

+ +
+

注意: 请记住,价格不是唯一的选择标准。如果您的网站成功,可能会发现可扩展性是最重要的考虑因素。

+
+ +

准备好发布你的网站

+ +

发布网站时,要考虑的主要问题是网络安全性和性能。至少,您需要删除开发期间,错误页面上包含的堆栈跟踪,整理日志记录,并设置适当的标头,以避免许多常见的安全威胁。

+ +

在以下小节中,我们概述了您应该对应用进行的、最重要的更改。

+ +
+

提示: Express文档中还有其他有用的提示 - 请参阅“生产最佳实践:性能和可靠性”,以及“生产最佳实践:安全性”。

+
+ +

设置 NODE_ENV 为 'production'

+ +

我们可以通过将 NODE_ENV 环境变量,设置为 production ,来删除错误页面中的堆栈跟踪(默认设置为 “development” )。除了生成较为不详细的错误消息之外,还要将变量设置为生产缓存视图模板,和从CSS扩展生成的CSS文件。测试表明,将NODE_ENV设置为生产,可以将应用程序性能提高三倍!

+ +

可以使用导出或环境文件,或使用OS初始化系统,以进行此更改。

+ +
+

注意: 这实际上是在环境设置,而不是应用中所做的更改,但重要的是,要注意这里!我们将在下面,展示如何为我们的托管示例设置。

+
+ +

Log appropriately

+ +

记录呼叫会对高流量网站产生影响。在生产环境中,您可能需要记录网站活动(例如,跟踪流量,或记录API调用),但您应尝试最小化为调试目的而添加的日志记录量。

+ +

在生产环境中,最小化“调试”日志记录的一种方法,是使用类似调试debug 的模块,允许您通过设置环境变量,来控制执行的日志记录。例如,下面的代码片段,显示了如何设置“author”日志记录。调试变量使用名称“author”声明,并且将自动显示,来自此对象的所有日志的前缀“author”。

+ +
var debug = require('debug')('author');
+
+// Display Author update form on GET
+exports.author_update_get = function(req, res, next) {
+
+    req.sanitize('id').escape().trim();
+    Author.findById(req.params.id, function(err, author) {
+        if (err) {
+            debug('update error:' + err);
+            return next(err);
+        }
+        //On success
+        res.render('author_form', { title: 'Update Author', author: author });
+    });
+
+};
+ +

然后,您可以通过在DEBUG环境变量中,将它们指定为逗号分隔列表,来启用特定日志集。您可以设置显示作者和书籍日志的变量,如图所示(也支持通配符)。

+ +
#Windows
+set DEBUG=author,book
+
+#Linux
+export DEBUG="author,book"
+
+ +
+

挑战: 调用debug可以替换您以前使用console.log()console.error()执行的日志记录。通过调试模块debug进行日志记录,替换代码中的所有console.log()调用。通过设置 DEBUG 变量,并在其中记录对日志记录的影响,在开发环境中,打开和关闭日志记录。

+
+ +

如果您需要记录网站活动,可以使用 Winston 或 Bunyan 等日志库。有关此主题的更多信息,请参阅:生产最佳实践:性能和可靠性

+ +

使用 gzip/deflate 压缩响应文件

+ +

Web服务器,通常可以压缩发送回客户端的 HTTP 响应,从而显着减少客户端获取和加载页面所需的时间。使用的压缩方法,取决于客户端在请求中支持的解压缩方法(如果不支持压缩方法,则响应将以未压缩的方式发送)。

+ +

您可以使用压缩中间件 compression,将其添加到您的站点。通过在项目的根目录下,运行以下命令,将其安装到项目中。

+ +
npm install compression
+ +

打开./app.js,并导入压缩库,如图所示。使用use()方法,将压缩库添加到中间件链(这应该出现在您想要压缩的任何路由之前 - 在本教程这种情况下,全部都是!)

+ +
var catalogRouter = require('./routes/catalog'); //Import routes for "catalog" area of site
+var compression = require('compression');
+
+// Create the Express application object
+var app = express();
+
+...
+
+app.use(compression()); //Compress all routes
+
+app.use(express.static(path.join(__dirname, 'public')));
+
+app.use('/', indexRouter);
+app.use('/users', usersRouter);
+app.use('/catalog', catalogRouter);  // Add catalog routes to middleware chain.
+
+...
+
+ +
+

注意: 对于生产中流量较大的网站,您不会使用此中间件。相反,你会使用像 Nginx 这样的反向代理。

+
+ +

使用 Helmet 避免被常见漏洞侵袭

+ +

Helmet 是一个中间件包,可以通过设置适当的 HTTP 标头,来帮助保护您的应用,免受一些众所周知的 Web 漏洞的影响(有关它设置的标头/防护漏洞的详细信息,请参阅文档)。

+ +

通过在项目的根目录下,运行以下命令,将其安装到项目中。

+ +
npm install helmet
+
+ +

打开./app.js,并导入如图所示的 helmet 库。然后使用use()方法将模块添加到中间件链。

+ +
var compression = require('compression');
+var helmet = require('helmet');
+
+// Create the Express application object
+var app = express();
+
+app.use(helmet());
+...
+ +
+

注意: 上面的命令,添加了对大多数站点有意义的可用标头子集。您可以按照npm上的说明,根据需要添加/禁用特定标头。

+
+ +

例子:在 Heroku 上安装一个本地图书馆

+ +

本节提供了如何在Heroku PaaS cloud云上安装LocalLibrary的实际演示。

+ +

为什么选择 Heroku?

+ +

Heroku 是运行时间最长,且最受欢迎的基于云的 PaaS 服务之一。它最初只支持 Ruby 应用程序,但现在可用于托管来自许多编程环境的应用程序,包括Node(以及Express)!

+ +

我们选择使用 Heroku 有以下几个原因:   

+ + + +

虽然 Heroku 非常适合举办此演示,但它可能并不适合您的真实网站。 Heroku可以轻松设置和扩展,但代价是灵活性较低,而且一旦退​​出免费套餐,可能会花费更多。

+ +

 Heroku 如何工作?

+ +

Heroku在一个或多个“Dynos”中运行网站,这些“Dynos”是独立的虚拟化Unix容器,提供运行应用程序所需的环境。 Dynos 是完全隔离的,并且有一个短暂的文件系统(一个短暂的文件系统,每次dyno重新启动时都会清理/清空)。 dynos 默认共享的唯一内容,是应用程序配置变量。 Heroku内部使用负载均衡器,将Web流量分配给所有“web”dynos。由于它们之间没有任何共享,Heroku可以通过添加更多dynos,来水平扩展应用程序(当然,您可能还需要扩展数据库,以接受其他连接)。

+ +

由于文件系统是短暂的,因此无法直接安装应用程序所需的服务(例如数据库,队列,缓存系统,存储,电子邮件服务等)。相反,Heroku Web应用程序使用 Heroku 或第三方作为独立“附加组件”提供的支持服务。连接到Web应用程序后,可以通过环境变量,在Web应用程序中访问附加服务。

+ +

为了执行您的应用程序,Heroku需要能够设置适当的环境和依赖关系,并了解它是如何启动的。对于Node应用程序,它所需的所有信息都是从package.json文件中获取的。

+ +

开发人员使用特殊的客户端应用程序/终端,与Heroku交互,这很像Unix bash脚本。这允许您上传存储在git存储库中的代码,检查正在运行的进程,查看日志,设置配置变量等等!

+ +

为了让我们的应用程序在Heroku上工作,我们需要将我们的Express Web应用程序放入git存储库,并对 package.json 进行一些小的更改。完成后,我们可以设置Heroku帐户,获取Heroku客户端,并使用它来安装我们的网站。

+ +

这是您开始教程所需的全部概述(有关更全面的指南,请参阅带有Node.js的Heroku入门)。

+ +

在 Github 上创建一个应用仓库

+ +

Heroku 与 git 源代码版本控制系统紧密集成,使用它来上传/同步您对实时运行系统所做的任何更改。它通过添加一个名为 heroku 的新 Heroku“远程”存储库,来指向您在Heroku云上的源存储库。在开发期间,您使用 git 在“主”存储库 master 中存储更改。如果要部署站点,请将更改同步到 Heroku 存储库。

+ +
+

注意: 如果您习惯于遵循良好的软件开发实践,那么您可能已经在使用git或其他一些SCM系统。如果您已有git存储库,则可以跳过此步骤。

+
+ +

有很多方法可以使用git,但最简单的方法之一,是首先在GitHub上建立一个帐户,在那里创建存储库,然后在本地同步它:

+ +
    +
  1. 访问 https://github.com/ 并创建一个帐户。
  2. +
  3. 登录后,单击顶部工具栏中的 + 号链接,然后选择新建存储库New repository
  4. +
  5. 填写此表单上的所有字段。虽然这些不是强制性的,但强烈建议使用它们。 +
      +
    • 输入新的存储库名称(例如,express-locallibrary-tutorial)和描述(例如 “以Express(node)编写的本地图书馆网站”)。
    • +
    • 在 Add .gitignore 选择列表中选择 Node
    • +
    • 在添加许可证 Add license 选择列表中,选择您偏好的许可证。
    • +
    • 点选 使用自述文件初始化此存储库 Initialize this repository with a README
    • +
    +
  6. +
  7. Create repository.
  8. +
  9. 单击新仓库页面上的绿色“克隆或下载”按钮 "Clone or download"。
  10. +
  11. 从显示的对话框的文本字段,复制URL值(它应该类似于:https://github.com/<your_git_user_id>/express-locallibrary-tutorial.git)。
  12. +
+ +

现在创建了存储库(“repo”),我们将要在本地计算机上克隆它:

+ +
    +
  1. 为您的本地计算机安装git(您可以在此处找到不同平台的版本)。
  2. +
  3. 打开命令提示符/终端,并使用您在上面复制的 URL ,克隆clone存储库: +
    git clone https://github.com/<your_git_user_id>/express-locallibrary-tutorial.git
    +
    + 这将在当前时间点之后,创建存储库。
  4. +
  5. 到新的仓库。 +
    cd express-locallibrary-tutorial
    +
  6. +
+ +

最后一步,是复制你的应用程序,然后使用 git ,将文件添加到你的仓库:

+ +
    +
  1. 将Express应用程序,复制到此文件夹中(不包括/node_modules,其中包含您应根据需要,从NPM获取的依赖项文件)。
  2. +
  3. 打开命令提示符/终端,并使用add命令,将所有文件添加到 git。
  4. +
  5. +
    git add -A
    +
    +
  6. +
  7. 使用 status 命令,检查要添加的所有文件是否正确(您希望包含源文件,而不是二进制文件,临时文件等)。它应该看起来有点像下面的列表。 +
    > git status
    +On branch master
    +Your branch is up-to-date with 'origin/master'.
    +Changes to be committed:
    +  (use "git reset HEAD <file>..." to unstage)
    +
    +        new file:   ...
    +
  8. +
  9. 如果您满意,请将文件提交到本地存储库: +
    git commit -m "First version of application moved into github"
    +
  10. +
  11. 然后使用以下内容,将本地存储库同步到Github网站: +
    git push origin master
    +
  12. +
+ +

完成此操作后,您应该可以返回创建存储库的Github上的页面,刷新页面,并查看您的整个应用程序现已上传。使用此添加/提交/推送循环,您可以在文件更改时,继续更新存储库。

+ +
+

提示: 这是备份你的“vanilla”项目的好时机 - 虽然我们将在以下部分中进行的一些更改,可能对任何平台(或开发)上的部署有用,而一些其他的更改可能没有用。

+ +

执行此操作的最佳方法,是使用git来管理您的修订。使用git,您不仅可以回到特定的旧版本,而且可以在生产变更的单独“分支”中进行维护,并选择在生产和开发分支之间移动的任何更改。学习Git非常值得,但超出了本主题的范围。

+ +

最简单的方法,是将文件复制到另一个位置。使用最符合您对 git 了解的方法!

+
+ +

更新Heroku的应用程序

+ +

本节介绍了您需要对 LocalLibrary 应用程序进行的更改,以使其在Heroku上运行。

+ +

设置 node 版本

+ +

package.json包含解决应用程序依赖项所需的所有内容,以及启动站点时,应启动的文件。 Heroku检测到此文件的存在,并将使用它来配置您的应用程序环境。

+ +

我们当前的package.json中,缺少的唯一有用信息,是 node 的版本。我们可以通过输入命令,找到我们用于开发的 node 版本:

+ +
>node --version
+v8.9.1
+ +

打开package.json,并将此信息添加为engines > node 部分,如图所示(使用系统的版本号)。

+ +
{
+  "name": "express-locallibrary-tutorial",
+  "version": "0.0.0",
+  "engines": {
+    "node": "8.9.1"
+  },
+  "private": true,
+  ...
+
+ +

数据库配置

+ +

到目前为止,在本教程中,我们使用了一个硬编码到app.js的单个数据库。通常我们希望,能够为生产和开发创建不同的数据库,接下来我们将修改 LocalLibrary 网站,以从OS环境获取数据库URI(如果已定义),否则使用我们的开发数据库。

+ +

打开app.js,并找到设置mongoDB连接变量的行。它看起来像这样:

+ +
var mongoDB = 'mongodb://your_user_id:your_password@ds119748.mlab.com:19748/local_library';
+ +

使用以下代码替换该行,该代码使用process.env.MONGODB_URI从名为MONGODB_URI的环境变量中,获取连接字符串(如果已设置)(使用您自己的数据库URL,而不是下面的占位符。)

+ +
var mongoDB = process.env.MONGODB_URI || 'mongodb://your_user_id:your_password@ds119748.mlab.com:19748/local_library';
+
+ +

安装依赖并重新测试

+ +

在我们继续之前,让我们再次测试该网站,并确保它不受我们的任何更改的影响。

+ +

首先,我们需要获取我们的依赖项(你会记得,我们没有将 node_modules文件夹,复制到我们的 git 树中)。您可以通过在项目根目录的终端中,运行以下命令来执行此操作:

+ +
npm install
+
+ +

现在运行该站点(请参阅测试路由的相关命令),并检查该站点,是否仍按预期运行。

+ +

将更改保存到 Github

+ +

接下来,让我们将所有更改保存到 Github。在终端中(在我们的存储库中),输入以下命令:

+ +
git add -A
+git commit -m "Added files and changes required for deployment to heroku"
+git push origin master
+ +

我们现在应该准备开始在 Heroku 上,部署 LocalLibrary。

+ +

获取一个 Heroku 账户

+ +

要开始使用Heroku,您首先需要创建一个帐户(如果您已经拥有一个帐户,并安装了Heroku客户端,请跳过创建并上传网站):

+ + + +

安装客户端

+ +

按照 Heroku上的说明,下载并安装Heroku客户端。

+ +

安装客户端后,您将能够运行命令。例如,要获得客户端的帮助说明:

+ +
heroku help
+
+ +

创建并上传网站

+ +

要创建应用程序,我们在存储库的根目录中,运行“create”命令。这将在我们的本地git环境中,创建一个名为 heroku 的 git remote(“指向远程存储库的指针”)。

+ +
heroku create
+ +
+

注意: 如果您愿意,可以在“创建”create 之后指定远程存储库的命名。如果你不这样做,你会得到一个随机的名字。该名称用于默认URL。

+
+ +

然后,我们可以将我们的应用程序,推送到Heroku存储库,如下所示。这将上传应用程序,获取所有依赖项,将其打包到dyno中,然后启动该站点。

+ +
git push heroku master
+ +

如果我们很幸运,该应用程序现在正在网站上“运行”。要打开浏览器并运行新网站,请使用以下命令:

+ +
heroku open
+ +
+

注意: 该站点将使用我们的开发数据库运行。创建一些书本和其他对象,并检查该网站是否按预期运行。在下一节中,我们将其设置为使用我们的新数据库。

+
+ +

设定配置变量

+ +

您将从前一节回忆起,我们需要将NODE_ENV设置为'production',以便提高性能,并生成更简洁的错误消息。我们通过输入以下命令,来完成此操作:

+ +
>heroku config:set NODE_ENV='production'
+Setting NODE_ENV and restarting limitless-tor-18923... done, v13
+NODE_ENV: production
+
+ +

我们还应该使用单独的数据库进行生产,在MONGODB_URI环境变量中,设置其URI。您可以完全按照我们原来的方式,设置新数据库和数据库用户,并获取其URI。您可以如下图所示设置URI(显然,要使用您自己的URI!)

+ +
>heroku config:set MONGODB_URI='mongodb://your_user:your_password@ds139278.mlab.com:39278/local_library_production'
+Setting MONGODB_URI and restarting limitless-tor-18923... done, v13
+MONGODB_URI: mongodb://your_user:your_password@ds139278.mlab.com:39278/local_library_production
+
+ +

您可以使用heroku config命令,随时检查配置变量 - 立即尝试:

+ +
>heroku config
+=== limitless-tor-18923 Config Vars
+MONGODB_URI: mongodb://your_user:your_password@ds139278.mlab.com:39278/local_library_production
+NODE_ENV:    production
+
+ +

Heroku会在更新变量时,重新启动应用程序。如果您现在检查主页,它应该显示对象计数的零值,因为上面的更改,意味着我们现在正在使用新的(空)数据库。

+ +

管理附加组件

+ +

Heroku 使用独立的附加组件,为应用程序提供支持服务 - 例如电子邮件或数据库服务。我们不在本网站中使用任何插件,但它们是使用Heroku的重要部分,因此您可能需要查看主题管理插件(Heroku docs)。

+ +

调试

+ +

Heroku客户端提供了一些调试工具:

+ +
heroku logs  # Show current logs
+heroku logs --tail # Show current logs and keep updating with any new results
+heroku ps   #Display dyno status
+
+ + + +

总结

+ +

本教程介绍在生产环境中,如何配置Express 应用。是Express系列教程的最后一个。我们希望你觉得这些教程有用。你可以在Github上取得完整的源码

+ +

相关链接

+ + + +

{{PreviousMenu("Learn/Server-side/Express_Nodejs/forms", "Learn/Server-side/Express_Nodejs")}}

+ +

 

+ +

本教程链接

+ + + +

 

diff --git a/files/zh-cn/learn/server-side/express_nodejs/development_environment/index.html b/files/zh-cn/learn/server-side/express_nodejs/development_environment/index.html new file mode 100644 index 0000000000..9a5e9ac7de --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/development_environment/index.html @@ -0,0 +1,390 @@ +--- +title: 设置 Node 开发环境 +slug: learn/Server-side/Express_Nodejs/development_environment +tags: + - Express + - Node + - node.js + - npm + - 初学者 + - 学习 + - 开发环境 + - 服务器端 +translation_of: Learn/Server-side/Express_Nodejs/development_environment +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/Introduction", "Learn/Server-side/Express_Nodejs/Tutorial_local_library_website", "Learn/Server-side/Express_Nodejs")}}
+ +

你已经了解了 Express 的用途,接下来将在 Windows、Linux(Ubuntu)和 Mac OS X 下搭建 Node/Express 开发环境。本节将介绍主流操作系统下开发 Express 程序的必备知识。

+ + + + + + + + + + + + +
预备知识:会打开终端 / 命令行。会为开发用操作系统安装软件包。
学习目标:在电脑上搭建 Express 开发环境。
+ +

Express 开发环境概述

+ +

使用 Node 和 Express 搭建 web 应用程序开发环境非常简便。这一章节将简述所需的工具,在主流操作系统(Ubuntu、macOS 和 Windows)上安装 Node 的步骤,以及测试安装是否成功的方法。

+ +

什么是 Express 开发环境?

+ +

完整的 Express 本地开发环境包括 Nodejs、NPM 包管理器和 Express 应用生成器(可选)。

+ +

Node 和 NPM 包管理器可以用二进制包、安装程序或系统包管理器一并安装(下文将介绍)。然后在开发每个 Express web 应用时,由 NPM 针对当前应用将 Express(以及模板引擎、数据库驱动程序、身份验证中间件、静态文件托管中间件等其它库)作为依赖项进行安装。

+ +

NPM 也可以安装(全局的)Express 应用生成器,可用于创建遵循 MVC模式 的 Express 应用框架。它不是必备的,因为无需这个工具就可以创建 Express 应用(或相同架构布局或依赖的 Express 应用)。但我们还是会使用它,因为它更容易上手,还有助于应用结构的模块化管理。

+ +
+

注: 与某些其他Web框架不同,开发环境不包含单独的开发Web服务器。在Node / Express中,Web应用程序将创建并运行自己的Web服务器!

+
+ +

典型的开发环境中还需要一些外围工具,包括用于编写代码的 文本编辑器 或 IDE ,用于代码控制管理的工具(比如代码版本控制工具 Git)。这里假定你已经安装了这些工具(尤其是文本编辑器)。

+ +

支持哪些操作系统?

+ +

Node 可以在 Windows、macOS、Linux 的诸多发行版本或 Docker 等环境运行(完整列表见 Node 下载页面)。几乎所有的个人电脑都具备 Node 开发所需性能。Express 运行在 Node 环境中,因此可运行 Node 的平台均可运行 Express。

+ +

本文将介绍 Windows、macOS 和 Ubuntu Linux 上的安装步骤。

+ +

应该选择 Node/Express 的哪个版本?

+ +

Node 有许多 发行版本,新版包含 bug 修复、对最新版本 ECMAScript 标准的支持,以及 API 的改进。

+ +

通常应该选择最新的 LTS(Long-term supported,长期支持版)发行版,因为它比当前发布版(current)更稳定。当前发布版包含最新的特性(维护中),如果需要 LTS 版本中没有提供的特征,那么可以选择它。

+ +

Express 应选用最新版本。

+ +

数据库和其它依赖该如何选择?

+ +

其它依赖(例如数据库驱动程序、模板引擎、身份认证引擎等)是应用的一部分,使用 NPM 将它们引入到应用环境中。稍后进行讨论。

+ +

安装 Node

+ +

先在操作系统上安装 Node.js 和 NPM 后才可使用 Express。接下来将介绍如何最简便地在 Ubuntu 18.04、macOS Mojave 以及 Windows 10 上安装 Node.js 最新的 LTS 版本。.

+ +
+

提示:以下内容将介绍在上述三种 OS 上安装 Node 和 NPM 的最简便方法。对于其它操作系统,以及更多的安装方法,可以参考 通过包管理器方式安装 Node.js (nodejs.org).

+
+ +

Windows 和 macOS

+ +

在 Windows 和 macOS 上安装 Node 和 NPM 非常简单明了,使用现成的安装包就行了:

+ +
    +
  1. 下载安装包: +
      +
    1. 访问 https://nodejs.org/zh-cn/
    2. +
    3. 左侧按钮上写着“推荐多数用户使用(LTS)”,点击下载。
    4. +
    +
  2. +
  3. 双击下载的安装包,按照提示即可安装。
  4. +
+ +

Ubuntu 18.04

+ +

安装 Node 最新的 LTS 版本的最简便方法就是使用 包管理器,可以直接从 Ubuntu 二进制发行仓库中下载。通过在终端运行以下两行简单的命令就可以做到:

+ +
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
+sudo apt-get install -y nodejs
+
+ +
+

警告:直接从 Ubuntu 默认仓库中下载的 Node 是 8.x 版本的。

+
+ +
    +
+ +

测试 Node.js 和 NPM 是否安装成功

+ +

检查 Node 是否成功安装的最简单方法就是在终端(或命令行)中运行 "version" 命令,看是否返回版本号字符串:

+ +
$ node -v
+v10.15.0
+ +

NPM 应该与 Node.js 一同成功安装,可以使用同样的方法来测试一下:

+ +
$ npm -v
+6.7.0
+ +

下面的测试也许会带来小小激动:创建一个非常基础的“纯 Node”服务器,在浏览器中访问正确的 URL 地址时将直接打印 "Hello world":

+ +
    +
  1. 以下代码使用了纯 Node 的特性(与 Express 无关)和一些 ES6 的语法,把它复制到 hellonode.js 文件中: + +
    // 加载 HTTP 模块
    +const http = require("http");
    +const hostname = '127.0.0.1';
    +const port = 3000;
    +
    +// 创建 HTTP 服务器
    +const server = http.createServer((req, res) => {
    +
    +  // 用 HTTP 状态码和内容类型(Content-Type)设置 HTTP 响应头
    +  res.statusCode = 200;
    +  res.setHeader('Content-Type', 'text/plain');
    +
    +  // 发送响应体
    +  res.end('Hello World\n');
    +});
    +
    +// 监听 3000 端口的请求,注册一个回调函数记录监听开始
    +server.listen(port, hostname, () => {
    +  console.log(`服务器运行于 http://${hostname}:${port}/`);
    +});
    +
    + +

    代码导入了 "http" 模块,并用它(createServer())创建了一个服务器来监听 3000 端口的 HTTP 请求。随后在控制台打印一条信息,提示测试服务器的正确 URL。createServer() 函数接受一个回调函数作为参数,并在接收 HTTP 请求后进行回调。直接返回了 HTTP 状态码 200 ("OK"),以及纯文本信息 "Hello World"。

    + +
    +

    注:现在看不懂这些代码请不要担心,开始使用 Express 后候会进行更加详细的解释。

    +
    +
  2. +
  3. 在命令行工具中进入 hellonode.js 文件所在的目录,输入“node + 文件名”并运行,服务器就启动了: +
    $ node hellonode.js
    +服务器运行于 http://127.0.0.1:3000/
    +
    +
  4. +
  5. 在浏览器中访问这个 URL(http://127.0.0.1:3000/),如果一切正常,浏览器会直接显示出 "Hello world" 字符串。
  6. +
+ +

使用 NPM

+ +

构建 Node 应用过程中,NPM 是除了 Node 本身之外最重要的工具。可用于获取应用开发、测试以及生产所需的所有包(JavaScript 库)。也可运行开发过程中使用的测试单元和工具。

+ +
+

注:以 Node 的角度来看,Express 只是一个用 NPM 安装、供人使用的包而已。

+
+ +

可以用 NPM 手动逐个安装所需包。但通常可用 package.json 文件来管理依赖。把每个依赖以一个 JavaScript “包”的形式(其中包括名称、版本、描述,初始执行文件、生产依赖,开发依赖、支持的 Node 版本,等等)罗列在这个文件中。package.json 文件包含 NPM 获取和运行应用程序所需的所有内容(在编写可重用的库时,可以用它把包上传到 NPM 仓库中供其他用户使用)。

+ +

添加依赖项

+ +

下面介绍用 NPM 下载包、将包保存进工程依赖树,以及在 Node 应用中调用包的方法和步骤。

+ +
+

注:现在来讲解获取和安装 Express 包的步骤。稍后解释为什么可以直接对 Express 包(乃至其它包)使用 Express 应用生成器。这段对理解 NPM 的工作原理和应用生成器的工作机制有一定的帮助。

+
+ +
    +
  1. 首先为新应用创建一个目录,并进入它: +
    $ mkdir myapp
    +$ cd myapp
    +
  2. +
  3. 然后,使用 NPM 的 init 命令为应用创建一个 package.json 文件。这个命令将请求一系列的信息,包括应用的名称和版本,程序初始进入点的文件名(默认为 index.js)。现在先接受默认信息即可: +
    $ npm init
    + +

    package.json 文件中保存了所接受的默认信息,最后一条是许可证信息:

    + +
    {
    +  "name": "myapp",
    +  "version": "1.0.0",
    +  "description": "",
    +  "main": "index.js",
    +  "scripts": {
    +    "test": "echo \"Error: no test specified\" && exit 1"
    +  },
    +  "author": "",
    +  "license": "ISC"
    +}
    +
    +
  4. +
  5. 接下来在 myapp 目录中安装 Express,用下面的命令将 Express 保存在 package.json 文件中的依赖表里: +
    $ npm install express
    + +

    此时 package.json 文件的底部会出现依赖列表("dependencies"),其中包含 Express:

    + +
    {
    +  "name": "myapp",
    +  "version": "1.0.0",
    +  "description": "",
    +  "main": "index.js",
    +  "scripts": {
    +    "test": "echo \"Error: no test specified\" && exit 1"
    +  },
    +  "author": "",
    +  "license": "ISC",
    +  "dependencies": {
    +    "express": "^4.16.4"
    +  }
    +}
    +
    +
  6. +
  7. 可以调用 require() 函数来使用库: +
    const express = require('express');
    +const app = express();
    +
    +app.get('/', (req, res) => {
    +  res.send('Hello World!')
    +});
    +
    +app.listen(8000, () => {
    +  console.log('示例程序正在监听 8000 端口!')
    +});
    +
    + +

    以上代码展示了一个最简单的 "HelloWorld" Express 应用。它导入了 "express" 模块并用它创建了一个服务器(app)来监听 8000 端口,并且在控制台打印了一条信息以提示测试服务器的正确 URL。app.get() 函数只响应对特定路径('/')的 HTTP GET 请求,此处的响应就是发送 "Hello World!"。
    +
    + 在 myapp 应用的根目录下新建一个 index.js 文件,将上述代码粘贴进来并保存。

    +
  8. +
  9. 在命令行输入 node + 文件名 即可启动服务器: +
    $ node index.js
    +示例程序正在监听 8000 端口!
    +
    +
  10. +
  11. 在浏览器中访问这个 URL(http://127.0.0.1:8000/),如果一切正常,浏览器会直接显示出 "Hello world!" 字符串。
  12. +
+ +

开发依赖(Development dependencies)

+ +

如果一个依赖只在开发过程中用到,应该将其保存为“开发依赖”(这样,包的用户便无需在生产环境中安装它们)。比如,如果要使用 eslint(一款流行的 JavaScript lint 工具)可以这样调用 NPM:

+ +
$ npm install eslint --save-dev
+ +

当前应用的 package.json 文件中将自动添加以下项目:

+ +
  "devDependencies": {
+    "eslint": "^5.12.0"
+  }
+
+ +
+

注:"lint" 是用于对软件进行静态分析的工具,可以发现并报告软件是否遵循某些最佳编程惯例。

+
+ +

运行任务

+ +

package.json 中,除了定义和获取依赖,还可以定义脚本,然后通过 NPM 的 run-script 命令来运行。这个用法普遍用于自动运行测试单元或部分应用,也可用于构建工具链(比如运行工具来压缩 JavaScript 文件或图片,lint 或分析代码,等等)。

+ +
+

注:GulpGrunt 等任务运行器可用于运行测试单元或其它外部工具。

+
+ +

比如,可以在 package.json 文件中添加以下内容来定义一个脚本,从而对上文的代码运行 eslint(假设应用代码在 /src/js 文件夹下):

+ +
"scripts": {
+  ...
+  "lint": "eslint src/js"
+  ...
+}
+
+ +

深入解释一下,eslint src/js 命令可以在终端/命令行对应用目录下的 src/js 目录中的 JavaScript 文件运行 eslint。把上面一段脚本添加进应用的 package.json 中还可以为此命令提供一个快捷方式—— lint。

+ +

然后就可以用 NPM 这样运行 eslint 了:

+ +
$ npm run-script lint
+ +

或使用别名:

+ +
$ npm run lint
+ +

这个示例看上去并没有让原始命令简洁多少,但在 NPM 脚本中可以加入更长的命令,甚至是多命令链。比如可以让单一的 NPM 脚本来一次运行所有的测试单元。

+ +

安装 Express 应用生成器

+ +

Express 应用生成器 工具可以生成一个 Express 应用的“框架”。可以用 NPM 这样安装它(-g 参数可以把该工具全局安装,那样就可以在任意应用中使用了):

+ +
$ npm install express-generator -g
+ +

进入应用目录,运行以下命令,即可创建一个名为 "helloworld" 的 Express 应用:

+ +
$ express helloworld
+ +
+

注:也可以指定模板库来使用其它丰富的设置。可通过 help 命令来查看所有选项:

+ +
$ express --help
+
+
+ +

NPM 将在当前位置的子目录中创建新的 Express 应用,可以在控制台看到构建的过程。在完成时,NPM 会提示你需要安装哪些 Node 依赖,以及如何开启应用。

+ +
+

新应用的根目录有一个 package.json 文件。可以打开它看看都安装了哪些依赖,其中可以看到 Express 和 Jade 模板库:

+ +
{
+  "name": "helloworld",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "start": "node ./bin/www"
+  },
+  "dependencies": {
+    "cookie-parser": "~1.4.3",
+    "debug": "~2.6.9",
+    "express": "~4.16.0",
+    "http-errors": "~1.6.2",
+    "jade": "~1.11.0",
+    "morgan": "~1.9.0"
+  }
+}
+
+ +

用下列命令可为 helloworld 应用安装所有依赖:

+ +
$ cd helloworld
+$ npm install
+
+ +

然后运行这个应用(Windows 环境):

+ +
> SET DEBUG=helloworld:* & npm start
+
+ +

(Linux/macOS 环境):

+ +
$ DEBUG=helloworld:* npm start
+ +

DEBUG 命令可以展示应用运行时返回的有用的日志信息,如下所示:

+ +

设置 DEBUG 命令显示的日志信息

+ +

打开浏览器并访问 http://127.0.0.1:3000/ 将看到 Express 的默认欢迎页面。

+ +

生成应用的默认主页

+ +

稍后在创建应用框架一节中将讨论生成应用的具体细节。

+ + + +

小结

+ +

现在 Node 开发环境已经配置好了,可以用于创建 Express 应用了。你还了解了用 NPM 导入 Express 的步骤,以及如何创建(使用 Express 应用生成器)和运行 web 应用。

+ +

下一节将开始用上述的环境和工具通过实战逐步搭建一个完整的 web 应用。

+ +

另请参阅

+ + + +

{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/Introduction", "Learn/Server-side/Express_Nodejs/Tutorial_local_library_website", "Learn/Server-side/Express_Nodejs")}}

+ +

本章目录

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/author_detail_page/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/author_detail_page/index.html new file mode 100644 index 0000000000..c62c5fbbec --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/author_detail_page/index.html @@ -0,0 +1,89 @@ +--- +title: 作者细节页面 +slug: learn/Server-side/Express_Nodejs/Displaying_data/Author_detail_page +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/Author_detail_page +--- +

作者细节页面需要呈现指定作者Author的信息,使用 _id 字段的值(自动产生)识别,接着是这个作者的所有书本物件Book的列表。

+ +

控制器

+ +

打开 /controllers/authorController.js

+ +

在档案最上方,加入底下几行,引入 async 和 Book 模组(作者细节页面需要它们)。

+ +
var async = require('async');
+var Book = require('../models/book');
+ +

找到 exported author_detail() 控制器方法,并用底下代码置换。

+ +
// Display detail page for a specific Author.
+exports.author_detail = function(req, res, next) {
+
+    async.parallel({
+        author: function(callback) {
+            Author.findById(req.params.id)
+              .exec(callback)
+        },
+        authors_books: function(callback) {
+          Book.find({ 'author': req.params.id },'title summary')
+          .exec(callback)
+        },
+    }, function(err, results) {
+        if (err) { return next(err); } // Error in API usage.
+        if (results.author==null) { // No results.
+            var err = new Error('Author not found');
+            err.status = 404;
+            return next(err);
+        }
+        // Successful, so render.
+        res.render('author_detail', { title: 'Author Detail', author: results.author, author_books: results.authors_books } );
+    });
+
+};
+
+ +

此处的控制器方法使用 async.parallel(),用平行的方式,查询作者 Author和相应的书本实例,并附加上绘制本页面的回调,如果 2 个要求都成功完成,就运行回调。这个方式,就跟前面的种类细节页面所说明的完全相同。

+ +

视图

+ +

创建 /views/author_detail.pug ,並複制貼上底下的文字。

+ +
extends layout
+
+block content
+
+  h1 Author: #{author.name}
+  p #{author.date_of_birth} - #{author.date_of_death}
+
+  div(style='margin-left:20px;margin-top:20px')
+
+    h4 Books
+
+    dl
+      each book in author_books
+        dt
+          a(href=book.url) #{book.title}
+        dd #{book.summary}
+
+      else
+        p This author has no books.
+
+ +

本模板里的所有事物,都在先前的章节演示过了。

+ +

它看起來像是?

+ +

运行本应用,并打开浏览器访问 http://localhost:3000/。选择All Authors 连结,然后选择一个作者。如果每个东西都设定正确了,你的网站看起来应该会像底下的截图。

+ +

Author Detail Page - Express Local Library site

+ +
+

注意: 作者的出生与死亡日期的外观很丑!我们将在本文最后的自我挑战处理它。

+
+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/author_list_page/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/author_list_page/index.html new file mode 100644 index 0000000000..055ae91e1c --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/author_list_page/index.html @@ -0,0 +1,89 @@ +--- +title: 作者清单面页、分类清单页面、与自我挑战 +slug: learn/Server-side/Express_Nodejs/Displaying_data/Author_list_page +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/Author_list_page +--- +

作者列表页面,需要呈现数据库中所有作者的列表,有每位作者的名字,并连结到作者详细内容页面。出生与死亡日期应该在名字后面,并且在同一列。

+ +

控制器

+ +

作者列表控制器函数,需要获取所有作者实例的列表,然后将这些实例传递给模板进行渲染。

+ +

打开/controllers/authorController.js。在文件顶部附近,找到导出的author_list() 控制器方法,并将其替换为以下代码(更改后的代码以粗体显示)。

+ +
// Display list of all Authors.
+exports.author_list = function(req, res, next) {
+
+  Author.find()
+    .sort([['family_name', 'ascending']])
+    .exec(function (err, list_authors) {
+      if (err) { return next(err); }
+      //Successful, so render
+      res.render('author_list', { title: 'Author List', author_list: list_authors });
+    });
+
+};
+ +

该方法使用模型的 find(), sort()exec() 函数,以返回所有Author对象,并按family_name的字母顺排列。传递给exec()方法的回调被调用,并将传入任何错误(或null)作为第一个参数,或者成功时,传入所有作者列表。如果出现错误,则调用带有错误值的下一个中间件函数,如果没有错误,则呈现 author_list(.pug)模板,传递页面标题title,和作者列表(author_list)。

+ +

视图

+ +

打开 /views/author_list.pug ,用底下文字取代它的内容。

+ +
extends layout
+
+block content
+  h1= title
+
+  ul
+  each author in author_list
+    li
+      a(href=author.url) #{author.name}
+      | (#{author.date_of_birth} - #{author.date_of_death})
+
+  else
+    li There are no authors.
+ +

如同我们其它的模板,上面视图也依照着同样的模式。

+ +

它看起來像是?

+ +

运行本应用,并打开浏览器访问 http://localhost:3000/ 。然后选择所有作者 All authors 连结。如果每个东西都设定正确了,页面看起来应该像底下的截图。

+ +

Author List Page - Express Local Library site

+ +
+

注意: 作者生命日期的外观是丑陋的!您可以使用我们用于BookInstance 列表的相同方法(将生命周期的虚拟属性,添加到 Author 模型),来改进此方法。

+ +

但是,这次缺少日期,除非严格模式生效,否则将忽略对不存在的属性的引用。moment()返回当前时间,并且您不希望将缺少的日期格式化为就像今天一样。

+ +

解决此问题的一种方法,是定义返回格式化日期的函数内容,以便返回空字符串,除非日期实际存在。例如:

+ +

return this.date_of_birth ? moment(this.date_of_birth).format('YYYY-MM-DD') : '';

+
+ +

种类列表页面—自我挑战!Edit

+ +

在这个部分,你应该实作你自己的种类列表页面。该页面应显示数据库中所有种类的列表,每个种类都链接到其关联的详细信息页面。预期结果的屏幕截图如下所示。

+ +

Genre List - Express Local Library site

+ +

种类列表控制器功能,需要获取所有种类实例的列表,然后将这些实例传递给模板进行渲染。

+ +
    +
  1. 您需要在 /controllers/genreController.js 中编辑genre_list()
  2. +
  3. 实现方式几乎与author_list()控制器完全相同。 +
      +
    • 按名称以上升顺序,对结果进行排序。
    • +
    +
  4. +
  5. 要呈现的模板,应命名为 genre_list.pug
  6. +
  7. 要呈现的模板应该传递变量title('Genre List')和种类列表genre_list(从Genre.find()回调返回)。
  8. +
  9. 该视图应与上面的屏幕截图/要求相匹配(这应该与作者列表视图具有非常相似的结构/格式,除了种类没有日期)。
  10. +
+ +

下一步

+ +

回到 Express 教程 5: 呈现图书馆数据

+ +

继续教程 5 下一個部分: 种类细节页面

diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/book_detail_page/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/book_detail_page/index.html new file mode 100644 index 0000000000..775bcd387f --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/book_detail_page/index.html @@ -0,0 +1,112 @@ +--- +title: 书本详细信息页面 +slug: learn/Server-side/Express_Nodejs/Displaying_data/Book_detail_page +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/Book_detail_page +--- +

书本细节页面需要呈现一本指定书本(Book)的信息, 使用它的 _id 字段值(自动产生)做为识别,接着是图书馆中书本实例(BookInstance)的信息。无论我们在哪里呈现一个作者、种类、或书本实例,都应该连结到它的细节页面。

+ +

控制器

+ +

打开 /controllers/bookController.js. ,找到 exported book_detail() 控制器方法,用底下的代码置换。

+ +
// Display detail page for a specific book.
+exports.book_detail = function(req, res, next) {
+
+    async.parallel({
+        book: function(callback) {
+
+            Book.findById(req.params.id)
+              .populate('author')
+              .populate('genre')
+              .exec(callback);
+        },
+        book_instance: function(callback) {
+
+          BookInstance.find({ 'book': req.params.id })
+          .exec(callback);
+        },
+    }, function(err, results) {
+        if (err) { return next(err); }
+        if (results.book==null) { // No results.
+            var err = new Error('Book not found');
+            err.status = 404;
+            return next(err);
+        }
+        // Successful, so render.
+        res.render('book_detail', { title: 'Title', book:  results.book, book_instances: results.book_instance } );
+    });
+
+};
+
+
+ +
+

注意: 我们不需要用 require 导入 async 和 BookInstance,当我们实作主页面控制器的时候,我们就已经引入这些模组。

+
+ +

此处的控制器方法使用 async.parallel(),用平行的方式找到 Book 以及它的相应复本 (BookInstances) 。这样的处理方式,就跟上面的 种类细节页面 所说明的完全相同。

+ +

视图

+ +

创建 /views/book_detail.pug 并加入底下文字。

+ +
extends layout
+
+block content
+  h1 #{title}: #{book.title}
+
+  p #[strong Author:]
+    a(href=book.author.url) #{book.author.name}
+  p #[strong Summary:] #{book.summary}
+  p #[strong ISBN:] #{book.isbn}
+  p #[strong Genre:]&nbsp;
+    each val, index in book.genre
+      a(href=val.url) #{val.name}
+      if index < book.genre.length - 1
+        |,
+
+  div(style='margin-left:20px;margin-top:20px')
+    h4 Copies
+
+    each val in book_instances
+      hr
+      if val.status=='Available'
+        p.text-success #{val.status}
+      else if val.status=='Maintenance'
+        p.text-danger #{val.status}
+      else
+        p.text-warning #{val.status}
+      p #[strong Imprint:] #{val.imprint}
+      if val.status!='Available'
+        p #[strong Due back:] #{val.due_back}
+      p #[strong Id:]&nbsp;
+        a(href=val.url) #{val._id}
+
+    else
+      p There are no copies of this book in the library.
+
+ +

在这个模板里,几乎每个东西都在先前的章节演示过了。

+ +
+

注意: 与该书相关的種類列表,在模板中的实作,如以下代碼。除了最后一本书之外,在与本书相关的每个种類之后,都会添加一个逗号。

+ +
  p #[strong Genre:]
+    each val, index in book.genre
+      a(href=val.url) #{val.name}
+      if index < book.genre.length - 1
+        |, 
+
+ +

它看起來像是?

+ +

运行本应用,并打开浏览器访问 http://localhost:3000/。选择 All books 连结,然后选择其中一本书。如果每个东西都设定正确了,你的页面看起来应该像是底下的截图。

+ +

Book Detail Page - Express Local Library site

+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/book_list_page/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/book_list_page/index.html new file mode 100644 index 0000000000..bb76f61d50 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/book_list_page/index.html @@ -0,0 +1,70 @@ +--- +title: 书本列表页面 +slug: learn/Server-side/Express_Nodejs/Displaying_data/Book_list_page +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/Book_list_page +--- +

接下做我们将实作书本列表页面。这个页面需要呈现数据库中所有书本的列表,包含每本书的作者、标题,标题将成为一个超连结,连到书本详细内容页面。

+ +

控制器

+ +

书本列表控制器函数,需要获取数据库中所有Book对象的列表,然后将这些对象传给模板进行呈现。

+ +

打开 /controllers/bookController.js. 找到导出的 book_list() 控制器方法,并替换為下面的代码。

+ +
// Display list of all Books.
+exports.book_list = function(req, res, next) {
+
+  Book.find({}, 'title author')
+    .populate('author')
+    .exec(function (err, list_books) {
+      if (err) { return next(err); }
+      //Successful, so render
+      res.render('book_list', { title: 'Book List', book_list: list_books });
+    });
+
+};
+ +

该方法使用模型的find()函数返回所有Book对象,选择仅返回标题title和作者author,因为我们不需要其他字段(它也会返回_id和虚拟字段)。这里我们还调用Book上的populate() ,指定作者author字段 — 这将用完整的作者信息,替换存储的书本作者 id。

+ +

成功时,传递给查询的回调,将呈现 book_list(.pug) 模板,将标题titlebook_list(包含作者的書本列表)作为变量传递。

+ +

视图

+ +

创建 /views/book_list.pug 并复制底下的文字。

+ +

 

+ +
extends layout
+
+block content
+  h1= title
+
+  ul
+  each book in book_list
+    li
+      a(href=book.url) #{book.title}
+      |  (#{book.author.name})
+
+  else
+    li There are no books.
+ +

這个视图扩展了 layout.pug 基本模板,并覆盖了名为 'content' 的區块 block 。它显示我们从控制器传入的标题title(通过render()方法),然后使用each-in-else语法,遍历book_list变量。为每本图书创建一个列表项,以显示书名,并作为书的详细信息页面的链接,后面跟着作者姓名。如果book_list中没有书,则执行else子句,并显示文字 “没有书” 'There are no books.'。

+ +
+

注意: 我们使用 book.url ,为每本书提供详细记录链接(我们已经实现了此路由,但尚未实现此页面)。这是 Book 模型的一个虚拟属性,它使用模型实例的 _id 字段,生成唯一的URL路径。

+
+ +

在这里,我們感兴趣的是,每本书被定义为两行,第二行使用管道(上面高亮显示)。这种方法是必要的,因为如果作者姓名位于上一行,那么它将成为超链接的一部分。

+ +

它看起來像是?

+ +

运行本应用 (参见 测试路由 有相关的命令) ,并打开你的浏览器,访问 http://localhost:3000/。然后选择 所有书本 连结。如果每样东西都设定正确了,你的网站看起来应该像底下的截图。

+ +

Book List Page - Express Local Library site

+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/bookinstance_detail_page_and_challenge/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/bookinstance_detail_page_and_challenge/index.html new file mode 100644 index 0000000000..cba25ab30d --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/bookinstance_detail_page_and_challenge/index.html @@ -0,0 +1,91 @@ +--- +title: 书本实例细节页面、与自我挑战 +slug: >- + learn/Server-side/Express_Nodejs/Displaying_data/BookInstance_detail_page_and_challenge +translation_of: >- + Learn/Server-side/Express_Nodejs/Displaying_data/BookInstance_detail_page_and_challenge +--- +

书本实例细节页面

+ +

BookInstance细节页面,需要呈现每一个BookInstance的信息,用 _id 字段值(自动产生)做识别。它包含了 Book 名称 (也是一个连结,连到 书本细节页面),接着是纪录中的其它的信息。

+ +

控制器

+ +

打开 /controllers/bookinstanceController.js. ,找到exported bookinstance_detail() 控制器方法,并替换以下代码。

+ +
// Display detail page for a specific BookInstance.
+exports.bookinstance_detail = function(req, res, next) {
+
+    BookInstance.findById(req.params.id)
+    .populate('book')
+    .exec(function (err, bookinstance) {
+      if (err) { return next(err); }
+      if (bookinstance==null) { // No results.
+          var err = new Error('Book copy not found');
+          err.status = 404;
+          return next(err);
+        }
+      // Successful, so render.
+      res.render('bookinstance_detail', { title: 'Book:', bookinstance:  bookinstance});
+    })
+
+};
+
+ +

该方法使用从URL(使用路由)中提取的特定书本实例的ID,调用BookInstance.findById(),并通过请求参数(req.params.id),在控制器中访问。然后调用populate()来获取相关Book的详细信息。

+ +

视图

+ +

创建 /views/bookinstance_detail.pug,并复制到下面的内容中。

+ +
extends layout
+
+block content
+
+  h1 ID: #{bookinstance._id}
+
+  p #[strong Title:]
+    a(href=bookinstance.book.url) #{bookinstance.book.title}
+  p #[strong Imprint:] #{bookinstance.imprint}
+
+  p #[strong Status:]
+    if bookinstance.status=='Available'
+      span.text-success #{bookinstance.status}
+    else if bookinstance.status=='Maintenance'
+      span.text-danger #{bookinstance.status}
+    else
+      span.text-warning #{bookinstance.status}
+
+  if bookinstance.status!='Available'
+    p #[strong Due back:] #{bookinstance.due_back}
+
+ +

本模组中的所有东西,都在先前的章节演示过了。

+ +

它看起來像是?

+ +

运行本应用,并打开浏览器访问 http://localhost:3000/。选择 All book-instances 连结,然后选择其中一本。如果每个东西都设定正确了,你的网站看起来应该像是底下的截图。

+ +

BookInstance Detail Page - Express Local Library site

+ +

自我挑战

+ +

目前,我们网站上显示的大多数日期,都使用默认的 JavaScript 格式(例如 Tue Dec 06 2016 15:49:58 GMT+1100(AUS东部夏令时间)。本文的挑战,是改善作者Author生命周期日期显示的外观信息(死亡/出生日期)和BookInstance详细信息页面,使用格式:December 6th, 2016。

+ +
+

注意: 您可以使用与我们用于 Book Instance List 的相同方法(将生命周期的虚拟属性,添加到Author模型,并使用moment来设置日期字符串的格式)。

+
+ +

这一挑战的要求:

+ +
    +
  1. 用 BookInstance 详细信息页面中的 due_back_formatted 替换 due_back
  2. +
  3. 更新作者模块以添加寿命虚拟属性。寿命应該有两个值: date_of_birth - date_of_death,這两个值的格式与 BookInstance.due_back_formatted的日期格式相同。
  4. +
  5. 在当前使用date_of_birthdate_of_death的所有视图中,使用 Author.lifespan
  6. +
+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/bookinstance_list_page/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/bookinstance_list_page/index.html new file mode 100644 index 0000000000..1e8403f0a5 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/bookinstance_list_page/index.html @@ -0,0 +1,69 @@ +--- +title: 书本实例列表页面 +slug: learn/Server-side/Express_Nodejs/Displaying_data/BookInstance_list_page +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/BookInstance_list_page +--- +

接下来,我们将实作图书馆中所有书本实例 (BookInstance) 的列表页面。这个页面需要包含与每个 BookInstance (链接到其详细信息页面) 关联的书本 Book 标题,以及BookInstance模型中的其他信息,包含每个副本的状态,印记和唯一ID。唯一ID的文字,应该链接到 BookInstance 详细信息页面。

+ +

控制器

+ +

BookInstance列表控制器函数,需要获取所有书本实例的列表,填充关联的书本信息,然后将列表传递给模板以进行呈现。

+ +

打开 /controllers/bookinstanceController.js。找到导出的 bookinstance_list() 控制器方法,并用以下代码替换它(更改后的代码以粗体显示)。

+ +
// Display list of all BookInstances.
+exports.bookinstance_list = function(req, res, next) {
+
+  BookInstance.find()
+    .populate('book')
+    .exec(function (err, list_bookinstances) {
+      if (err) { return next(err); }
+      // Successful, so render
+      res.render('bookinstance_list', { title: 'Book Instance List', bookinstance_list: list_bookinstances });
+    });
+
+};
+ +

此方法使用模型的find()函数,返回所有BookInstance对象。然后它将一个调用,以菊花链方式连接到populate(),附加书本book字段,这将使用完整的Book文档,替换每个BookInstance存储的书本ID。

+ +

成功时,传递给查询的回调,会呈现 bookinstance_list (.pug)模板,并将标题title和书籍实例列表bookinstance_list作为变量传递。

+ +

视图

+ +

创建 /views/bookinstance_list.pug ,並複制貼上底下的文字。

+ +
extends layout
+
+block content
+  h1= title
+
+  ul
+  each val in bookinstance_list
+    li
+      a(href=val.url) #{val.book.title} : #{val.imprint} -
+      if val.status=='Available'
+        span.text-success #{val.status}
+      else if val.status=='Maintenance'
+        span.text-danger #{val.status}
+      else
+        span.text-warning #{val.status}
+      if val.status!='Available'
+        span  (Due: #{val.due_back} )
+
+  else
+    li There are no book copies in this library.
+ +

这个視图与其他視图非常相似。它扩展了布局,替换内容區块,显示从控制器传入的标题title,并遍历bookinstance_list 中的所有书籍副本。对于每个副本,我们都会显示它的状态(用颜色编码),如果书本不可用,则显示其预期返回日期。這裡引入了一个新功能 — 我们可以在标签之后使用点符号表示法,來指定一個类別。因此,span.text-success 将被编译为 <span class="text-success"> (也可以用 Pug 编写为 span(class="text-success").

+ +

它看起來像是?

+ +

运行本应用,打开浏览器访问 http://localhost:3000/,然后选择 All book-instances 连结。假如每个东西都设定正确了,你的网站看起来应该像是底下的截图。

+ +

BookInstance List Page - Express Local Library site

+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/date_formatting_using_moment/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/date_formatting_using_moment/index.html new file mode 100644 index 0000000000..8abbf8d290 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/date_formatting_using_moment/index.html @@ -0,0 +1,58 @@ +--- +title: 使用 moment 做日期格式化 +slug: learn/Server-side/Express_Nodejs/Displaying_data/Date_formatting_using_moment +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/Date_formatting_using_moment +--- +

我们模型的日期预设呈现很难看: Tue Dec 06 2016 15:49:58 GMT+1100 (AUS Eastern Daylight Time)。在本节中,我们将展示如何更新上一节中的 書本實例 BookInstance 列表页面,以更友好的格式显示due_date字段:December 6th, 2016。

+ +

我们将使用的方法,是在我们的BookInstance模型中,创建一个返回格式化日期的虚拟屬性。我们将使用moment 来做实际的格式化,这是一个轻量级JavaScript日期库,用于解析,验证,操作和格式化日期。

+ +
+

注意: 我们可以直接在 Pug 模板中,使用 moment 格式化字符串,或者可以在许多其它地方格式化字符串。使用虚拟属性,可以使我们获得格式化的日期,這与我们当前获取 due_date 的方式完全相同。

+
+ +

安装 moment

+ +

在项目的根目录,输入下列命令

+ +
npm install moment
+ +

创建虚拟属性

+ +
    +
  1. 打开 ./models/bookinstance.js.
  2. +
  3. 在此页面最上方,引用 moment +
    var moment = require('moment');
    +
  4. +
+ +

在 url 属性后面,加入虚拟属性 due_back_formatted

+ +
BookInstanceSchema
+.virtual('due_back_formatted')
+.get(function () {
+  return moment(this.due_back).format('MMMM Do, YYYY');
+});
+ +
+

注意: 格式化方法可以使用几乎任何模式显示日期。moment文档中,可以找到表示不同日期组件的语法。

+
+ +

更新视图

+ +

打开 /views/bookinstance_list.pug ,然后用  due_back_formatted 取代 due_back

+ +
      if val.status!='Available'
+        //span  (Due: #{val.due_back} )
+        span  (Due: #{val.due_back_formatted} )       
+ +

这就是本章节的全部了。如果你访问侧边栏的 All book-instances ,你应该会看到所有的归还日期都更吸引人了!

+ +

下一步

+ + + +

 

diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/flow_control_using_async/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/flow_control_using_async/index.html new file mode 100644 index 0000000000..f720812c50 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/flow_control_using_async/index.html @@ -0,0 +1,139 @@ +--- +title: 使用 async 进行非同步流控制 +slug: learn/Server-side/Express_Nodejs/Displaying_data/flow_control_using_async +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/flow_control_using_async +--- +

有些本地图书馆网页的控制器代码,会依赖多重非同步要求的结果,可能会需要以某种特定次序运行,或者以平行方式运行。为了管理流控制,并在我们所有需要用到的信息,都已经可以取用的时候,再绘制网页,我们将使用许多人采用的 node async 模组。

+ +
+

注意: 在 JavaScript 中有许多其他方法,可以管理异步行为和流控制,包括相对较新的 JavaScript 语言功能,如 Promises

+
+ +

Async 有很多有用的方法(请查看文档)。一些最重要的功能是:

+ + + +

为什么需要这么做?

+ +

我们在 Express 中使用的大多数方法,都是异步的 - 您指定要执行的操作,传递回调。该方法立即返回,并在请求的操作完成时,调用回调。按照 Express 中的惯例,回调函数将错误值作为第一个参数传递(或成功时为 null),并将函数的结果(如果有的话)作为第二个参数传递。

+ +

如果控制器只需要执行一个异步操作,来获取呈现页面所需的信息,那么实现很简单 - 我们只需在回调中呈现模板。下面的代码片段,显示了一个函数,该函数呈现模型 SomeModel 的计数(使用Mongoose count()方法):

+ +
exports.some_model_count = function(req, res, next) {
+
+  SomeModel.count({ a_model_field: 'match_value' }, function (err, count) {
+    // ... do something if there is an err
+
+    // On success, render the result by passing count into the render function (here, as the variable 'data').
+    res.render('the_template', { data: count } );
+  });
+}
+
+ +

但是,如果您需要进行多个异步查询,并且在完成所有操作之前,无法呈现页面,该怎么办?一个单纯的实现可以用 “菊花链” 连接请求,在先前请求的回调中,启动后续请求,并在最终回调中呈现响应。这种方法的问题,是我们的请求必须串行运行,即使并行运行它们可能更有效。这也可能导致复杂的嵌套代码,通常称为回调地狱

+ +

一个更好的解决方案,是并行执行所有请求,然后在所有查询完成后执行单个回调。这是 Async 模块简化的流操作!

+ +

平行的非同步操作

+ +

方法async.parallel()用于并行运行多个异步操作。

+ +

async.parallel() 的第一个参数,是要运行的异步函数的集合(数组,对象或其他可迭代的)。每个函数都传递一个回调函数callback(err, result) ,它必须在完成时调用错误err(可以为null)和可选的结果值。

+ +

async.parallel()的可选第二个参数是一个回调,它将在第一个参数中的所有函数完成时运行。回调的调用,是使用错误参数和包含各个异步操作结果的结果集合。结果集合与第一个参数的类型相同(即,如果传递异步函数数组,则将使用结果数组,调用最终回调)。如果任何并行函数报告错误,则提前调用回调(具有错误值)。

+ +

下面的示例,显示了当我们将对象作为第一个参数传递时它是如何工作的。如您所见,结果将返回到一个对象中,该对象具有与传入的原始函数相同的属性名称。

+ +
async.parallel({
+  one: function(callback) { ... },
+  two: function(callback) { ... },
+  ...
+  something_else: function(callback) { ... }
+  },
+  // optional callback
+  function(err, results) {
+    // 'results' is now equal to: {one: 1, two: 2, ..., something_else: some_value}
+  }
+);
+ +

如果您将一组函数,作为第一个参数传递,则结果将是一个数组(数组顺序结果,将与声明函数的原始顺序匹配 - 而不是它们完成的顺序)。

+ +

序列的非同步操作

+ +

async.series()方法用于按顺序运行多个异步操作,后续函数不依赖于先前函数的输出。它本质上是声明的,并且行为与async.parallel().相同。

+ +
async.series({
+  one: function(callback) { ... },
+  two: function(callback) { ... },
+  ...
+  something_else: function(callback) { ... }
+  },
+  // optional callback after the last asynchronous function completes.
+  function(err, results) {
+    // 'results' is now equals to: {one: 1, two: 2, ..., something_else: some_value} 
+  }
+);
+ +
+

注意: ECMAScript(JavaScript)语言规范指出,对象的枚举顺序是未定义的,因此可能不会按照在所有平台上指定它们的顺序,调用这些函数。如果顺序真的很重要,那么你应该传递一个数组而不是一个对象,如下所示。

+
+ +
async.series([
+  function(callback) {
+    // do some stuff ...
+    callback(null, 'one');
+  },
+  function(callback) {
+    // do some more stuff ... 
+    callback(null, 'two');
+  }
+ ],
+  // optional callback
+  function(err, results) {
+  // results is now equal to ['one', 'two'] 
+  }
+);
+ +

依赖序列的非同步操作

+ +

方法async.waterfall()用于在每个操作依赖于前一个操作的结果时,依次运行多个异步操作。

+ +

每个异步函数调用的回调,包含第一个参数的null,与后续参数里的结果。该序列中的每个函数,都将前一个回调的结果参数,作为第一个参数,然后是回调函数。

+ +

完成所有操作后,将使用上一操作的结果,调用最终回调。当您参考下面的代码片段时,这种工作方式会更加明确(此示例来自 async 文档):

+ +
async.waterfall([
+  function(callback) {
+    callback(null, 'one', 'two');
+  },
+  function(arg1, arg2, callback) {
+    // arg1 now equals 'one' and arg2 now equals 'two' 
+    callback(null, 'three');
+  },
+  function(arg1, callback) {
+    // arg1 now equals 'three'
+    callback(null, 'done');
+  }
+], function (err, result) {
+  // result now equals 'done'
+}
+);
+ +

安装 async

+ +

使用 NPM 包管理器安装 async 模块,以便我们可以在代码中使用它。您可以常规方式执行此操作,在 LocalLibrary 项目的根目录中,打开命令提示并输入以下命令:

+ +

 

+ +
npm install async
+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/genre_detail_page/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/genre_detail_page/index.html new file mode 100644 index 0000000000..825474b9ab --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/genre_detail_page/index.html @@ -0,0 +1,120 @@ +--- +title: 种类细节页面 +slug: learn/Server-side/Express_Nodejs/Displaying_data/Genre_detail_page +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/Genre_detail_page +--- +

种类细节页面,需要利用_id 字段值 (自动生成) ,以呈现特定种类实例的信息。此页面应该呈现种类名称,各个种类的所有书本列表(每本书都连结到书本的细节页面)。

+ +

控制器

+ +

打开 /controllers/genreController.js ,并在档案最上方引用 async 和 Book 模组。

+ +
var Book = require('../models/book');
+var async = require('async');
+
+ +

找到导出的genre_detail()控制器方法,并将其替换为以下代码。

+ +
// Display detail page for a specific Genre.
+exports.genre_detail = function(req, res, next) {
+
+    async.parallel({
+        genre: function(callback) {
+            Genre.findById(req.params.id)
+              .exec(callback);
+        },
+
+        genre_books: function(callback) {
+          Book.find({ 'genre': req.params.id })
+          .exec(callback);
+        },
+
+    }, function(err, results) {
+        if (err) { return next(err); }
+        if (results.genre==null) { // No results.
+            var err = new Error('Genre not found');
+            err.status = 404;
+            return next(err);
+        }
+        // Successful, so render
+        res.render('genre_detail', { title: 'Genre Detail', genre: results.genre, genre_books: results.genre_books } );
+    });
+
+};
+
+ +

该方法使用async.parallel(),并行查询类型名称及其相关联的书本,并在(如果)两个请求成功完成时,回调呈现页面。

+ +

所需种类记录的 ID ,在 URL 的末尾编码,并根据路由定义(/genre/:id)自动提取。通过请求参数(req.params.id在控制器内访问 ID。它在Genre.findById()中用于获取当前种类。它还用于获取符合当前种类的所有Book对象,就是在种类字段中具有种类ID的那些 Book.find({ 'genre': req.params.id })

+ +
+

注意: 如果数据库中不存在该类型(即它可能已被删除),则findById()将成功返回,但没有结果。在这种情况下,我们想要显示一个“未找到”页面,因此我们创建一个Error对象,并将其传递给链中的下一个中间件函数next

+ +
if (results.genre==null) { // No results.
+    var err = new Error('Genre not found');
+    err.status = 404;
+    return next(err);
+}
+
+ +

然后,此消息将传播给我们的错误处理代码(这是在我们生成应用程序框架时设置的 - 有关更多信息,请参阅处理错误)。

+
+ +

渲染的视图是 genre_detail,它传递了该类型的标题title,种类genre和书本列表的变量(genre_books)。

+ +

视图

+ +

创建 /views/genre_detail.pug ,并填写底下文字:

+ +
extends layout
+
+block content
+
+  h1 Genre: #{genre.name}
+
+  div(style='margin-left:20px;margin-top:20px')
+
+    h4 Books
+
+    dl
+    each book in genre_books
+      dt
+        a(href=book.url) #{book.title}
+      dd #{book.summary}
+
+    else
+      p This genre has no books
+
+ +

这个视图跟我们其它的模板非常相似。主要的差别在于,我们不使用 title 传送第一个标题 (虽然它还是用在底层的 layout.pug 模板,设定页面的标题)。

+ +

它看起來像是?

+ +

运行本应用,并打开浏览器访问 http://localhost:3000/。选择 All genres 连结,然后选择其中一个种类 (例如,"Fantasy")。如果每样东西都设定正确了,你的页面看起来应该像底下的截图。

+ +

Genre Detail Page - Express Local Library site

+ +
+

您可能会收到与此类似的错误:

+ +
Cast to ObjectId failed for value " 59347139895ea23f9430ecbb" at path "_id" for model "Genre"
+
+ +

这是来自 req.params.id 的 mongoose 错误。要解决这个问题,首先需要在 genreController.js 页面上要求mongoose,如下所示:

+ +
 var mongoose = require('mongoose');
+
+然后使用 mongoose.Types.ObjectId()将 id 转换为可以使用的。例如: + +
exports.genre_detail = function(req, res, next) {
+    var id = mongoose.Types.ObjectId(req.params.id);
+    ...
+
+
+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/home_page/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/home_page/index.html new file mode 100644 index 0000000000..27dbdb5788 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/home_page/index.html @@ -0,0 +1,134 @@ +--- +title: 主页 +slug: learn/Server-side/Express_Nodejs/Displaying_data/Home_page +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/Home_page +--- +

我们创建的第一个页面,是网站的主页面,可以从网站的根目录 ('/') ,或者 catalog 的根目录 (catalog/) 访问。这将呈现一些网站的静态文字描述,以及动态计算数据库中不同记录类型的“计数”。

+ +

我们已经为主页创建了一个路由。为了完成页面,我们需要更新控制器函数,以从数据库中提取记录的“计数”,并创建一个可用于呈现页面的视图(模板)。

+ +

路由

+ +

前面的教程,我们创建 index 页面路由。此处要提醒的是,所有的路由函式,都定义在 /routes/catalog.js:

+ +
// GET catalog home page.
+router.get('/', book_controller.index);  //This actually maps to /catalog/ because we import the route with a /catalog prefix
+ +

/controllers/bookController.js 中,定义回调函数参数(book_controller.index) :

+ +
exports.index = function(req, res, next) {
+    res.send('NOT IMPLEMENTED: Site Home Page');
+}
+
+ +

我们扩展这个控制器函数,以从我们的模型获取信息,然后使用模板(视图)渲染它。

+ +

控制器

+ +

索引控制器函数需要获取以下有关信息,即数据库中有多少BookBookInstance,可用的BookInstanceAuthorGenre记录,将这些数据渲染到模板中,以创建HTML页面,然后将其返回到HTTP响应中。

+ +
+

Note: 我们使用count() 方法来获取每个模型的实例数量。这在具有一组可选条件的模型上进行调用,以匹配第一个参数,而回调放在第二个参数(如使用数据库(Mongoose))中讨论的那样,并且还可以返回 Query ,然后稍后以回调执行它。当数据库返回计数时,将返回该回调,并将错误值(或空值null)作为第一个参数,并将记录计数(如果存在错误,则返回null)作为第二个参数。

+ +
SomeModel.count({ a_model_field: 'match_value' }, function (err, count) {
+ // ... do something if there is an err
+ // ... do something with the count if there was no error
+ });
+
+ +

打开 /controllers/bookController.js. 在文件顶部附近,您应该看到导出的 index() 函数。

+ +
var Book = require('../models/book')
+
+exports.index = function(req, res, next) {
+ res.send('NOT IMPLEMENTED: Site Home Page');
+}
+ +

用以下代码片段替换上面的所有代码。这要做的第一件事,是导入(require())所有模型(以粗体突出高亮显示)。我们需要这样做,是因为我们将使用它们来获取记录的计数。然后它会导入异步模块 async

+ +
var Book = require('../models/book');
+var Author = require('../models/author');
+var Genre = require('../models/genre');
+var BookInstance = require('../models/bookinstance');
+
+var async = require('async');
+
+exports.index = function(req, res) {
+
+    async.parallel({
+        book_count: function(callback) {
+            Book.count({}, callback); // Pass an empty object as match condition to find all documents of this collection
+        },
+        book_instance_count: function(callback) {
+            BookInstance.count({}, callback);
+        },
+        book_instance_available_count: function(callback) {
+            BookInstance.count({status:'Available'}, callback);
+        },
+        author_count: function(callback) {
+            Author.count({}, callback);
+        },
+        genre_count: function(callback) {
+            Genre.count({}, callback);
+        },
+    }, function(err, results) {
+        res.render('index', { title: 'Local Library Home', error: err, data: results });
+    });
+};
+ +

async.parallel() 方法传递一个对象,其中包含用于获取每个模型计数的函数。这些函数都是在同一时间开始的。当这些函数全部完成时,最终回调将与结果参数中的计数(或错误)一起被调用。

+ +

成功时,回调函数调用 res.render(),指定名为 'index' 的视图(模板),以及一个对象包含了要插入其中的数据 (这包括我们模型计数的结果对象)。数据以键值对的形式提供,可以使用键在模板中访问。

+ +
+

注意: 上面的async.parallel()裡的回调函数有点不寻常,因为不管是否出现错误,我们都会渲染页面(通常您可能使用单独的执行路径来处理错误的显示)。

+
+ +

视图

+ +

打开 /views/index.pug ,并用底下文字取代它的内容。

+ +
extends layout
+
+block content
+  h1= title
+  p Welcome to #[em LocalLibrary], a very basic Express website developed as a tutorial example on the Mozilla Developer Network.
+
+  h1 Dynamic content
+
+  if error
+    p Error getting dynamic content.
+  else
+    p The library has the following record counts:
+
+    ul
+      li #[strong Books:] !{data.book_count}
+      li #[strong Copies:] !{data.book_instance_count}
+      li #[strong Copies available:] !{data.book_instance_available_count}
+      li #[strong Authors:] !{data.author_count}
+      li #[strong Genres:] !{data.genre_count}
+ +

这个视图很简单。我们扩展了 layout.pug 基本模板,覆盖了名为 'content' 的模块 block。第一个h1标题,将是传递给render()函数的title 变量的转义文本 — 请注意 'h1=' 的使用方式,将使得接下來的文本,被视为 JavaScript 表达式。然后我们放入一个介绍本地图书馆的段落。

+ +

在动态内容标题下,我们检查从render()函数传入的错误变量,是否已定义。如果是这样,我们列出这个错误。如果不是,我们从data变量中,获取并列出每个模型的副本数量。

+ +
+

注意: 我们没有转义计数值 (i.e. 我们使用 !{} 语法) ,因为计数值已经被计算过了。如果信息是由终端用户提供的,那么我们就会转义該变量,以用于显示。

+
+ +

它看起来像是?

+ +

此处,我们应该已经创建了呈现index页面,所需要的每样东西。运行本地图书馆应用,并打开浏览器访问 http://localhost:3000/。如果每样东西都设定正确了,你的网站看起来应该像底下的截图。

+ +

Home page - Express Local Library site

+ +
+

注意: 您将无法使用侧边栏链接,因为这些网页的网址,视图和模板尚未定义。例如,如果您尝试,取决于您点击的链接,您将获取“尚未实作:图书清单”等错误。在“控制器”文件中的不同控制器中,會指定这些字符串文字(将被合适的数据替换)。

+
+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/index.html new file mode 100644 index 0000000000..b59601e248 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/index.html @@ -0,0 +1,87 @@ +--- +title: 'Express 教程 5: 呈现图书馆数据' +slug: learn/Server-side/Express_Nodejs/Displaying_data +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/routes", "Learn/Server-side/Express_Nodejs/forms", "Learn/Server-side/Express_Nodejs")}}
+ +

我们现在准备好要新增网页,以显示本地图书馆网站的书本与其它资料。这些网页将包含一个主页 ,显示我们拥有的每个模型的记录数,以及所有模型的清单和详细信息页面。借此,我们将获得从数据库获取记录、以及使用模板的实战经验。

+ + + + + + + + + + + + +
前置条件:完成先前教程主题 (包含 Express 教程 Part 4: 路由与控制器)。
目标: +

了解如何使用异步模组与Pug 样版语言,以及如何从我们的控制器函数中的URL取得信息。

+
+ +

概览

+ +

在我们先前的教程中,定义了可以用来跟资料库互动的 Mongoose models ,并创建了一些初始的图书馆记录。我们接着创建本地图书馆网站需要的所有路由  ,但仅使用"空壳控制器" 函数(这些是骨架控制器函数,当一个网页被存取时,只回传一个"未实现" 信息)。

+ +

下一步,是为这些显示图书馆信息的网页,提供适当的实现(我们将在后面的文章,聚焦网页表单的实现,像是创建、更新、删除信息)。这包含了更新控制器函数,以利用我们的模型获取记录,并定义模板,为用户显示这些信息。

+ +

我们在一开始,提供概述/入门主题,解释在控制器函数中,如何管理异步操作,以及如何使用Pug编写模板。然后我们将为每一个主要的 "只读" 页面提供实现步骤,并且在使用到任何特别的、新的特性时附上简短的解释说明。

+ +

本教程的最后,你对路由、异步函数、视图、模型如何实际运作,应该有了更好的理解。

+ +

本教程的章节

+ +

本教程分为下列章节,讲解了为了显示图书馆网站需求的页面而新增各种特性的过程 。在进入下一个教程之前,你需要阅读并逐一实现下列章节。

+ +
    +
  1. 使用 async 进行异步流控制
  2. +
  3. 模版入门
  4. +
  5. 本地图书馆基础样版
  6. +
  7. 主页
  8. +
  9. 书本清单页面
  10. +
  11. 书本实例清单页面
  12. +
  13. 日期格式化-使用 moment
  14. +
  15. 作者清单页面、分类清单页面与自我挑战
  16. +
  17. 分类详情页面
  18. +
  19. 书本详情页面
  20. +
  21. 作者详情页面
  22. +
  23. 书本实例详情页面、与自我挑战
  24. +
+ +

总结

+ +

我们现在已经为我们的网站,创建了所有 "只读" 的页面: 一个主页,可以显示每一个模组的实例数量,书本的列表与详细信息页面,书本的实例、作者、分类。沿着目前的学习路径,我们学到了许多基本知识,有控制器、在异步操作时管理流控制、使用Pug创建视图模板、使用模型查询数据库、如何从视图传送信息到模板、如何创建并扩展模板。而完成挑战的人,还会学到如何用moment处理日期。

+ +

在下一篇文章,我们将依据目前为止学到的知识,创建HTML 表单以及表单管理代码,开始修改储存在网站中的资料。

+ +

参见

+ + + +

{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/routes", "Learn/Server-side/Express_Nodejs/forms", "Learn/Server-side/Express_Nodejs")}}

+ + + +

本教程文章列表

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/locallibrary_base_template/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/locallibrary_base_template/index.html new file mode 100644 index 0000000000..41d851e7d7 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/locallibrary_base_template/index.html @@ -0,0 +1,69 @@ +--- +title: 本地图书馆基础模板 +slug: learn/Server-side/Express_Nodejs/Displaying_data/LocalLibrary_base_template +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/LocalLibrary_base_template +--- +

现在我们了解如何使用Pug拓展模板,让我们开始项目,创建一个基础模板。这个模板会有一个侧边栏,连结到本教程中将要创建的各个页面(例如,呈现并创建书本、种类、作者等等),以及一个主要内容区域,我们将在每个页面中进行覆写。

+ +

开启 /views/layout.pug ,并以下列代码置换其内容。

+ +
doctype html
+html(lang='en')
+  head
+    title= title
+    meta(charset='utf-8')
+    meta(name='viewport', content='width=device-width, initial-scale=1')
+    link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css')
+    script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js')
+    script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js')
+    link(rel='stylesheet', href='/stylesheets/style.css')
+  body
+    div(class='container-fluid')
+      div(class='row')
+        div(class='col-sm-2')
+          block sidebar
+            ul(class='sidebar-nav')
+              li
+                a(href='/catalog') Home
+              li
+                a(href='/catalog/books') All books
+              li
+                a(href='/catalog/authors') All authors
+              li
+                a(href='/catalog/genres') All genres
+              li
+                a(href='/catalog/bookinstances') All book-instances
+              li
+                hr
+              li
+                a(href='/catalog/author/create') Create new author
+              li
+                a(href='/catalog/genre/create') Create new genre
+              li
+                a(href='/catalog/book/create') Create new book
+              li
+                a(href='/catalog/bookinstance/create') Create new book instance (copy)
+
+        div(class='col-sm-10')
+          block content
+ +

此模板使用(并包含)来自 Bootstrap 的 JavaScript 和 CSS ,以改进HTML页面的布局和呈现方式。使用Bootstrap 或其它客户端网页框架,是一种快速的方式,可以创建吸引人的网页,能够良好地适应不同的浏览器尺寸,并且允许我们处理页面的呈现,而不需要纠缠于任何不同尺寸的细节—此处我们只想专注于伺服端代码!

+ +

布局的安排应该相当明白,假如你已经阅读了之前的 模板入门。注意,使用 block content 当做定位符号,放到页面内容将要放置的地方。

+ +

基础模板也参考了一个本地 css 档 (style.css) ,此档提供了一些额外的样式。打开 /public/stylesheets/style.css ,并用底下的 CSS 代码,取代它的内容:

+ +
.sidebar-nav {
+    margin-top: 20px;
+    padding: 0;
+    list-style: none;
+}
+ +

当我们开始运行网站时,我们应该看到侧边栏出现!在本教程的下个部分,我们将使用以上的布局,来定义各个页面。

+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/displaying_data/template_primer/index.html b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/template_primer/index.html new file mode 100644 index 0000000000..374957bb1b --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/displaying_data/template_primer/index.html @@ -0,0 +1,149 @@ +--- +title: 模板入门 +slug: learn/Server-side/Express_Nodejs/Displaying_data/Template_primer +translation_of: Learn/Server-side/Express_Nodejs/Displaying_data/Template_primer +--- +

模板是一个文字档,定义了一个输出档的结构或者排版,使用定位符号表示,当模板被绘制时,资料将插入到何处(在Express,模板被称为视图)。

+ +

Express 模板选择

+ +

Express 可以与许多不同的模板渲染引擎一起使用。在本教程中,我们使用Pug(以前称为Jade)作为模板。这是最流行的 Node 模板语言,并且官方将自身描述为 “用于编写HTML,语法干净且空格敏感,受 Haml影响很大”。

+ +

不同的模板语言使用不同的方法,来定义布局和标记数据的占位符 —  一些使用 HTML 来定义布局,而另一些则使用可以编译为 HTML 的不同标记格式。Pug 是第二种类型;它使用 HTML 的表示形式,其中任何行中的第一个单词,通常表示HTML元素,后续行中的缩进,用于表示嵌套在这些元素中的任何内容。结果是一个页面定义直接转换为 HTML,但可以说更简洁,更容易阅读。

+ +
+

注意: 使用 Pug 的缺点,是它对缩进和空格敏感(如果在错误的位置添加额外的空格,可能会得到没什么帮助的错误代码)。但是,一旦您的模板到位,它们就很容易阅读和维护。

+
+ +

模板组态

+ +

在我们创建骨架网站时,LocalLibrary 配置为使用 Pug。您应该看到 Pug 模块作为依赖项,包含在网站的 package.json文件中,以及 app.js文件中的以下配置设置。设置告诉我们,使用 Pug 作为视图引擎,Express 应该在 /views子目录中搜索模板。

+ +
// View engine setup.
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'pug');
+ +

如果查看 views 目录,您将看到项目默认视图的 .pug 文件。这包括需要用自己的内容替换的主页(index.pug)和基本模板(layout.pug)的视图。

+ +
/express-locallibrary-tutorial  //the project root
+  /views
+    error.pug
+    index.pug
+    layout.pug
+
+ +

模板语法

+ +

下面的示例模板文件,展示了许多 Pug 最有用的功能。

+ +

首先要注意的是,该文件映射典型 HTML 文件的结构,其中(几乎)每一行中的第一个单词是 HTML 元素,并且缩进用于指示嵌套元素。因此,例如,body 本文元素位于 html 元素内,而段落元素(p)位于 body 元素内等。非嵌套元素(例如,各个段落)位于不同的行上。

+ +
doctype html
+html(lang="en")
+  head
+    title= title
+    script(type='text/javascript').
+  body
+    h1= title
+
+    p This is a line with #[em some emphasis] and #[strong strong text] markup.
+    p This line has un-escaped data: !{'<em> is emphasised</em>'} and escaped data: #{'<em> is not emphasised</em>'}.
+      | This line follows on.
+    p= 'Evaluated and <em>escaped expression</em>:' + title
+
+    <!-- You can add HTML comments directly -->
+    // You can add single line JavaScript comments and they are generated to HTML comments
+    //- Introducing a single line JavaScript comment with "//-" ensures the comment isn't rendered to HTML
+
+    p A line with a link
+      a(href='/catalog/authors') Some link text
+      |  and some extra text.
+
+    #container.col
+      if title
+        p A variable named "title" exists.
+      else
+        p A variable named "title" does not exist.
+      p.
+        Pug is a terse and simple template language with a
+        strong focus on performance and powerful features.
+
+    h2 Generate a list
+
+    ul
+      each val in [1, 2, 3, 4, 5]
+        li= val
+ +

元素属性被定义在其关联元素之后的括号中。在括号内,属性定义在以逗号或空格分隔的属性名称和属性值对的列表中,例如:

+ + + +

所有属性的值都被转义(例如 “>” 等字符转换为 HTML 代码等效项,如“&gt;”),以防止注入 JavaScript 或跨站点脚本攻击。

+ +

如果标记后跟着等号,则以下文本将被视为 JavaScript 表达式。因此,打个比方,在下面的第一行中,h1标记的内容将是标题变量title(在文件中定义,或从 Express 传递到模板中)。在第二行中,段落内容是与标题变量title连接的文本字符串。在这两种情况下,默认行为是转义该行。

+ +
h1= title
+p= 'Evaluated and <em>escaped expression</em>:' + title
+ +

如果标记后面没有等号,则将内容视为纯文本。在纯文本中,您可以使用#{}!{}语法,插入转义和非转义数据,如下所示。您还可以在纯文本中添加原始 HTML。

+ +
p This is a line with #[em some emphasis] and #[strong strong text] markup.
+p This line has an un-escaped string: !{'<em> is emphasised</em>'}, an escaped string: #{'<em> is not emphasised</em>'}, and escaped variables: #{title}.
+ +
+

提示: 您几乎总是希望转义来自用户的数据(通过#{}语法)。可信任的数据(例如,生成的记录计数等)可以不先转义就显示。

+
+ +

您可以在行的开头使用管道(“|”)字符来表示“纯文本”。例如,下面显示的附加文本,将显示在与前一个锚点相同的行上,但不会链接。

+ +
a(href='http://someurl/') Link text
+| Plain text
+ +

Pug 允许您使用if, else , else ifunless执行条件操作 - 例如:

+ +
if title
+  p A variable named "title" exists
+else
+  p A variable named "title" does not exist
+ +

以使用each-inwhile语法执行循环/迭代操作。在下面的代码片段中,我们循环遍历数组,以显示变量列表(注意,使用 'li =' 来评估 “val” ,以作为下面的变量。)迭代的值也可以传递给模板作为变量!

+ +
ul
+  each val in [1, 2, 3, 4, 5]
+    li= val
+ +

语法还支持注释(可以在输出中呈现 - 或者不是 - 可自行选择),支持mixins创建可重用的代码块,case语句和许多其他功能。有关更多详细信息,请参阅Pug文档。

+ +

扩展模板

+ +

在一个站点中,通常所有页面都有一个共同的结构,包括页首,页脚,导航等的标准HTML标记。比起强迫开发人员在每个页面中复制这个 “样板”的做法,Pug 允许你声明一个基本模板,然后扩展它,只替换每个特定页面不同的地方。

+ +

例如,在我们的骨架项目中,创建的基本模板 layout.pug,如下所示:

+ +
doctype html
+html
+  head
+    title= title
+    link(rel='stylesheet', href='/stylesheets/style.css')
+  body
+    block content
+ +

块标记block用于标记 “可在派生模板中替换的内容部分“(如果未重新定义块,则使用其在基类中的实现)。

+ +

默认的 index.pug(为我们的骨架项目所创建),显示了我们如何覆盖基本模板。extends标记,标识要使用的基本模板,然后我们使用 block section_name ,来指示我们将覆盖的部分的新内容。

+ +
extends layout
+
+block content
+  h1= title
+  p Welcome to #{title}
+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/create_author_form/index.html b/files/zh-cn/learn/server-side/express_nodejs/forms/create_author_form/index.html new file mode 100644 index 0000000000..84a1b71db3 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/create_author_form/index.html @@ -0,0 +1,155 @@ +--- +title: 创建作者表单 +slug: learn/Server-side/Express_Nodejs/forms/Create_author_form +translation_of: Learn/Server-side/Express_Nodejs/forms/Create_author_form +--- +

Ed本章节演示,如何为创建作者对象Author定义一个页面。

+ +

导入验证和清理方法

+ +

为了在种类表单使用express验证器,我们必须用 require 导入我们想用的函式。

+ +

打开 /controllers/authorController.js,并在档案最上方加入底下几行:

+ +
const { body,validationResult } = require('express-validator/check');
+const { sanitizeBody } = require('express-validator/filter');
+ +

控制器—get 路由

+ +

找到导出的 author_create_get()控制器方法,并替换为底下代码。这里单纯呈现 author_form.pug 视图,传送 title 变数。

+ +
// Display Author create form on GET.
+exports.author_create_get = function(req, res, next) {
+    res.render('author_form', { title: 'Create Author'});
+};
+ +

控制器—post 路由

+ +

找到导出的 author_create_post() 控制器方法,并替换为底下代码。

+ +
// Handle Author create on POST.
+exports.author_create_post = [
+
+    // Validate fields.
+    body('first_name').isLength({ min: 1 }).trim().withMessage('First name must be specified.')
+        .isAlphanumeric().withMessage('First name has non-alphanumeric characters.'),
+    body('family_name').isLength({ min: 1 }).trim().withMessage('Family name must be specified.')
+        .isAlphanumeric().withMessage('Family name has non-alphanumeric characters.'),
+    body('date_of_birth', 'Invalid date of birth').optional({ checkFalsy: true }).isISO8601(),
+    body('date_of_death', 'Invalid date of death').optional({ checkFalsy: true }).isISO8601(),
+
+    // Sanitize fields.
+    sanitizeBody('first_name').trim().escape(),
+    sanitizeBody('family_name').trim().escape(),
+    sanitizeBody('date_of_birth').toDate(),
+    sanitizeBody('date_of_death').toDate(),
+
+    // Process request after validation and sanitization.
+    (req, res, next) => {
+
+        // Extract the validation errors from a request.
+        const errors = validationResult(req);
+
+        if (!errors.isEmpty()) {
+            // There are errors. Render form again with sanitized values/errors messages.
+            res.render('author_form', { title: 'Create Author', author: req.body, errors: errors.array() });
+            return;
+        }
+        else {
+            // Data from form is valid.
+
+            // Create an Author object with escaped and trimmed data.
+            var author = new Author(
+                {
+                    first_name: req.body.first_name,
+                    family_name: req.body.family_name,
+                    date_of_birth: req.body.date_of_birth,
+                    date_of_death: req.body.date_of_death
+                });
+            author.save(function (err) {
+                if (err) { return next(err); }
+                // Successful - redirect to new author record.
+                res.redirect(author.url);
+            });
+        }
+    }
+];
+ +

此代码的结构和行为,几乎与创建Genre对象完全相同。首先,我们验证并清理数据。如果数据无效,那么我们将重新显示表单,以及用户最初输入的数据,和错误消息列表。如果数据有效,那么我们保存新的作者记录,并将用户重定向到作者详细信息页面。

+ +
+

注意:Genre post处理程序不同,我们不会在保存之前,检查Author对象是否已存在。可以说,我们应该这样做,尽管现在我们可以有多个具有相同名称的作者。

+
+ +

验证代码演示了几个新功能:

+ + + + + +

视图

+ +

创建 /views/author_form.pug 并复制贴上以下文字。

+ +
extends layout
+
+block content
+  h1=title
+
+  form(method='POST' action='')
+    div.form-group
+      label(for='first_name') First Name:
+      input#first_name.form-control(type='text' placeholder='First name (Christian) last' name='first_name' required='true' value=(undefined===author ? '' : author.first_name) )
+      label(for='family_name') Family Name:
+      input#family_name.form-control(type='text' placeholder='Family name (surname)' name='family_name' required='true' value=(undefined===author ? '' : author.family_name))
+    div.form-group
+      label(for='date_of_birth') Date of birth:
+      input#date_of_birth.form-control(type='date' name='date_of_birth' value=(undefined===author ? '' : author.date_of_birth) )
+    button.btn.btn-primary(type='submit') Submit
+  if errors
+    ul
+      for error in errors
+        li!= error.msg
+ +

此视图的结构和行为与genre_form.pug模板完全相同,因此我们不再对其进行描述。

+ +
+

注意: 某些浏览器不支持input type=“date”,因此您不会获得日期选取部件或默认的dd/mm/yyyy占位符,而是获取一个空的纯文本字段。一种解决方法,是明确添加属性placeholder='dd/mm/yyyy',以便在功能较少的浏览器上,仍然可以获得有关所需文本格式的信息。

+
+ +

自我挑战: 加入死亡日期

+ +

上面的模板少了一个输入字段 date_of_death 。依照跟生日表单同样的模式,创建此字段!

+ +

它看起來像是?

+ +

运行本应用,打开浏览器访问网址http://localhost:3000/,然后点击创建新作者 Create new author 连结。如果每个东西都设定正确了,你的网站看起应该像底下的截图。在你输入一个值之后,它应该会被储存,并且你将被带到作者详细信息页面。

+ +

Author Create Page - Express Local Library site

+ +
+

注意: 如果您尝试使用日期的各种输入格式,您可能会发现格式yyyy-mm-dd行为不正常。这是因为 JavaScript 将日期字符串,视为包含 0 小时的时间,但另外将该格式的日期字符串(ISO 8601标准)视为包括 0 小时 UTC 时间,而不是本地时间。如果您的时区在 UTC 以西,则日期显示(即本地)将在您输入的日期之前一天。这是我们在这里没有解决的几个复杂问题之一(例如多字姓和有多个作者的书本)。

+
+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/create_book_form/index.html b/files/zh-cn/learn/server-side/express_nodejs/forms/create_book_form/index.html new file mode 100644 index 0000000000..4b460b36ea --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/create_book_form/index.html @@ -0,0 +1,212 @@ +--- +title: 创建书本表单 +slug: learn/Server-side/Express_Nodejs/forms/Create_book_form +translation_of: Learn/Server-side/Express_Nodejs/forms/Create_book_form +--- +

Edit 此子文档显示如何定义页面/表单以创建Book对象。这比相同的作者Author或种类Genre页面稍微复杂一点,因为我们需要在我们的书本表单中,获取并显示可用的作者和种类记录。

+ +

导入验证和清理方法

+ +

打开 /controllers/bookController.js,并在文件顶部添加以下行:

+ +
const { body,validationResult } = require('express-validator/check');
+const { sanitizeBody } = require('express-validator/filter');
+ +

控制器—get 路由

+ +

找到导出的book_create_get() 控制器方法,并将其替换为以下代码。

+ +
// Display book create form on GET.
+exports.book_create_get = function(req, res, next) {
+
+    // Get all authors and genres, which we can use for adding to our book.
+    async.parallel({
+        authors: function(callback) {
+            Author.find(callback);
+        },
+        genres: function(callback) {
+            Genre.find(callback);
+        },
+    }, function(err, results) {
+        if (err) { return next(err); }
+        res.render('book_form', { title: 'Create Book', authors: results.authors, genres: results.genres });
+    });
+
+};
+ +

这使用异步模块 async(在教程5:显示数据库中的数据),来获取所有作者和种类对象。然后将它们作为名为authorsgenres的变量(以及页面标题title),传递给视图book_form.pug

+ +

控制器—post 路由

+ +

找到导出的book_create_post()控制器方法,并将其替换为以下代码。

+ +
// Handle book create on POST.
+exports.book_create_post = [
+    // Convert the genre to an array.
+    (req, res, next) => {
+        if(!(req.body.genre instanceof Array)){
+            if(typeof req.body.genre==='undefined')
+            req.body.genre=[];
+            else
+            req.body.genre=new Array(req.body.genre);
+        }
+        next();
+    },
+
+    // Validate fields.
+    body('title', 'Title must not be empty.').isLength({ min: 1 }).trim(),
+    body('author', 'Author must not be empty.').isLength({ min: 1 }).trim(),
+    body('summary', 'Summary must not be empty.').isLength({ min: 1 }).trim(),
+    body('isbn', 'ISBN must not be empty').isLength({ min: 1 }).trim(),
+
+    // Sanitize fields (using wildcard).
+    sanitizeBody('*').trim().escape(),
+    sanitizeBody('genre.*').escape(),
+    // Process request after validation and sanitization.
+    (req, res, next) => {
+
+        // Extract the validation errors from a request.
+        const errors = validationResult(req);
+
+        // Create a Book object with escaped and trimmed data.
+        var book = new Book(
+          { title: req.body.title,
+            author: req.body.author,
+            summary: req.body.summary,
+            isbn: req.body.isbn,
+            genre: req.body.genre
+           });
+
+        if (!errors.isEmpty()) {
+            // There are errors. Render form again with sanitized values/error messages.
+
+            // Get all authors and genres for form.
+            async.parallel({
+                authors: function(callback) {
+                    Author.find(callback);
+                },
+                genres: function(callback) {
+                    Genre.find(callback);
+                },
+            }, function(err, results) {
+                if (err) { return next(err); }
+
+                // Mark our selected genres as checked.
+                for (let i = 0; i < results.genres.length; i++) {
+                    if (book.genre.indexOf(results.genres[i]._id) > -1) {
+                        results.genres[i].checked='true';
+                    }
+                }
+                res.render('book_form', { title: 'Create Book',authors:results.authors, genres:results.genres, book: book, errors: errors.array() });
+            });
+            return;
+        }
+        else {
+            // Data from form is valid. Save book.
+            book.save(function (err) {
+                if (err) { return next(err); }
+                   //successful - redirect to new book record.
+                   res.redirect(book.url);
+                });
+        }
+    }
+];
+ +

此代码的结构和行为,几乎与创建种类Genre或作者Author对象完全相同。首先,我们验证并清理数据。如果数据无效,那么我们将重新显示表单,以及用户最初输入的数据,和错误消息列表。如果数据有效,我们将保存新的Book记录,并将用户重定向到Book详细信息页面。

+ +

与其他表单处理代码相关的第一个主要区别,是我们使用通配符,一次修剪和转义所有字段(而不是单独清理它们):

+ +
sanitizeBody('*').trim().escape(),
+ +

与其他表单处理代码相关的下一个主要区别,是我们如何清理种类Genre信息。表单返回一个Genre项的数组(而对于其他字段,它返回一个字符串)。为了验证信息,我们首先将请求转换为数组(下一步需要)。

+ +
// Convert the genre to an array.
+(req, res, next) => {
+    if(!(req.body.genre instanceof Array)){
+        if(typeof req.body.genre==='undefined')
+        req.body.genre=[];
+        else
+        req.body.genre=new Array(req.body.genre);
+    }
+    next();
+},
+ +

然后,我们在清理器中使用通配符(*)来单独验证每个种类数组条目。下面的代码显示了 - 这转换为 “清理关键种类genre下的每个项目”。

+ +
sanitizeBody('genre.*').trim().escape(),
+ +

与其他表单处理代码的最终区别,在于我们需要将所有现有的种类和作者传递给表单。为了标记用户已经检查过的种类,我们遍历所有种类,并将checked='true'参数,添加到我们的 POST 数据中(如下面的代码片段中所示)。

+ +
// Mark our selected genres as checked.
+for (let i = 0; i < results.genres.length; i++) {
+    if (book.genre.indexOf(results.genres[i]._id) > -1) {
+        // Current genre is selected. Set "checked" flag.
+        results.genres[i].checked='true';
+    }
+}
+ +

视图

+ +

创建 /views/book_form.pug,并复制下面的文本。

+ +
extends layout
+
+block content
+  h1= title
+
+  form(method='POST' action='')
+    div.form-group
+      label(for='title') Title:
+      input#title.form-control(type='text', placeholder='Name of book' name='title' required='true' value=(undefined===book ? '' : book.title) )
+    div.form-group
+      label(for='author') Author:
+      select#author.form-control(type='select', placeholder='Select author' name='author' required='true' )
+        for author in authors
+          if book
+            option(value=author._id selected=(author._id.toString()==book.author ? 'selected' : false) ) #{author.name}
+          else
+            option(value=author._id) #{author.name}
+    div.form-group
+      label(for='summary') Summary:
+      input#summary.form-control(type='textarea', placeholder='Summary' name='summary' value=(undefined===book ? '' : book.summary) required='true')
+    div.form-group
+      label(for='isbn') ISBN:
+      input#isbn.form-control(type='text', placeholder='ISBN13' name='isbn' value=(undefined===book ? '' : book.isbn) required='true')
+    div.form-group
+      label Genre:
+      div
+        for genre in genres
+          div(style='display: inline; padding-right:10px;')
+            input.checkbox-input(type='checkbox', name='genre', id=genre._id, value=genre._id, checked=genre.checked )
+            label(for=genre._id) #{genre.name}
+    button.btn.btn-primary(type='submit') Submit
+
+  if errors
+    ul
+      for error in errors
+        li!= error.msg
+ +

视图结构和行为与 genre_form.pug 模板几乎相同。

+ +

主要区别在于,我们如何实现选择类型字段:作者Author和种类Genre

+ + + +

它看起來像是?

+ +

运行应用程序,将浏览器打开到http://localhost:3000,然后选择Create new book链接。如果一切设置正确,您的网站应该类似于以下屏幕截图。提交有效的图书后,应将其保存,然后您将进入图书详细信息页面。

+ +

+ +

下一步

+ +

回到 Express 教程 6: 使用表单

+ +

继续教程 6 的下一个部分: 创建书本实例表单

diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/create_bookinstance_form/index.html b/files/zh-cn/learn/server-side/express_nodejs/forms/create_bookinstance_form/index.html new file mode 100644 index 0000000000..6809ec4e93 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/create_bookinstance_form/index.html @@ -0,0 +1,150 @@ +--- +title: 创建书本实例表单 +slug: learn/Server-side/Express_Nodejs/forms/Create_BookInstance_form +translation_of: Learn/Server-side/Express_Nodejs/forms/Create_BookInstance_form +--- +

Edi本章节演示如何定义一个页面/表单,以创建BookInstance 物件。这很像我们用来创建书本 Book 物件的表单。

+ +

导入验证和清理方法

+ +

打开 /controllers/bookinstanceController.js,并在档案最上方加入以下几行:

+ +
const { body,validationResult } = require('express-validator/check');
+const { sanitizeBody } = require('express-validator/filter');
+ +

控制器—get 路由

+ +

在档案最上方,用 require 导入书本模型 (因为每个BookInstance 都有关连的 Book)。

+ +
var Book = require('../models/book');
+ +

找到导出的 bookinstance_create_get() 控制器方法,并替换为底下代码。

+ +
// Display BookInstance create form on GET.
+exports.bookinstance_create_get = function(req, res, next) {
+
+    Book.find({},'title')
+    .exec(function (err, books) {
+      if (err) { return next(err); }
+      // Successful, so render.
+      res.render('bookinstance_form', {title: 'Create BookInstance', book_list:books});
+    });
+
+};
+ +

控制器取得所有书本的列表 (book_list) 并将它传送到视图 bookinstance_form.pug (里面附加上 title)。

+ +

控制器—post 路由

+ +

找到导出的  bookinstance_create_post() 控制器方法,并替换为底下代码。

+ +
// Handle BookInstance create on POST.
+exports.bookinstance_create_post = [
+
+    // Validate fields.
+    body('book', 'Book must be specified').isLength({ min: 1 }).trim(),
+    body('imprint', 'Imprint must be specified').isLength({ min: 1 }).trim(),
+    body('due_back', 'Invalid date').optional({ checkFalsy: true }).isISO8601(),
+
+    // Sanitize fields.
+    sanitizeBody('book').trim().escape(),
+    sanitizeBody('imprint').trim().escape(),
+    sanitizeBody('status').trim().escape(),
+    sanitizeBody('due_back').toDate(),
+
+    // Process request after validation and sanitization.
+    (req, res, next) => {
+
+        // Extract the validation errors from a request.
+        const errors = validationResult(req);
+
+        // Create a BookInstance object with escaped and trimmed data.
+        var bookinstance = new BookInstance(
+          { book: req.body.book,
+            imprint: req.body.imprint,
+            status: req.body.status,
+            due_back: req.body.due_back
+           });
+
+        if (!errors.isEmpty()) {
+            // There are errors. Render form again with sanitized values and error messages.
+            Book.find({},'title')
+                .exec(function (err, books) {
+                    if (err) { return next(err); }
+                    // Successful, so render.
+                    res.render('bookinstance_form', { title: 'Create BookInstance', book_list : books, selected_book : bookinstance.book._id , errors: errors.array(), bookinstance:bookinstance });
+            });
+            return;
+        }
+        else {
+            // Data from form is valid.
+            bookinstance.save(function (err) {
+                if (err) { return next(err); }
+                   // Successful - redirect to new record.
+                   res.redirect(bookinstance.url);
+                });
+        }
+    }
+];
+ +

此代码的结构和行为,与创建其他对象的结构和行为相同。首先,我们验证数据,并為数据做無害化處理。如果数据无效,我们会重新显示表單,以及用户最初输入的数据,還有错误消息列表。如果数据有效,我们保存新的BookInstance记录,并将用户重定向到详细信息页面。

+ +

视图

+ +

创建 /views/bookinstance_form.pug ,并复制贴上以下代码。

+ +
extends layout
+
+block content
+  h1=title
+
+  form(method='POST' action='')
+    div.form-group
+      label(for='book') Book:
+      select#book.form-control(type='select' placeholder='Select book' name='book' required='true')
+        for book in book_list
+          if bookinstance
+            option(value=book._id selected=(bookinstance.book.toString()==book._id.toString() ? 'selected' : false)) #{book.title}
+          else
+            option(value=book._id) #{book.title}
+
+    div.form-group
+      label(for='imprint') Imprint:
+      input#imprint.form-control(type='text' placeholder='Publisher and date information' name='imprint' required='true' value=(undefined===bookinstance ? '' : bookinstance.imprint))
+    div.form-group
+      label(for='due_back') Date when book available:
+      input#due_back.form-control(type='date' name='due_back' value=(undefined===bookinstance ? '' : bookinstance.due_back))
+
+    div.form-group
+      label(for='status') Status:
+      select#status.form-control(type='select' placeholder='Select status' name='status' required='true')
+        option(value='Maintenance') Maintenance
+        option(value='Available') Available
+        option(value='Loaned') Loaned
+        option(value='Reserved') Reserved
+
+    button.btn.btn-primary(type='submit') Submit
+
+  if errors
+    ul
+      for error in errors
+        li!= error.msg
+ +

这个视图的结构和行为,几乎等同于 book_form.pug 模板,因此我们就不再重覆说明一次了。

+ +
+

注意: 以上的模板将状态值 (Maintenance, Available, 等等) 写死在代码里,而且不能 "记忆" 使用者的输入值。如果你愿意的话,考虑重新实作此列表,当表单被重新呈现时,从控制器传入选项数据,并设定选中的值。

+
+ +

它看起來像是?

+ +

运行本应用,打开浏览器访问网址 http://localhost:3000/。然后点击创建新书本实例 Create new book instance (copy) 连结。如果每个东西都设定正确了,你的网站看起应该像底下的截图。在你提交一个有效的 BookInstance 之后,它应该会被储存,并且你将被带到详细信息页面。

+ +

+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/create_genre_form/index.html b/files/zh-cn/learn/server-side/express_nodejs/forms/create_genre_form/index.html new file mode 100644 index 0000000000..ffee2341cf --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/create_genre_form/index.html @@ -0,0 +1,298 @@ +--- +title: 创建种类表单 +slug: learn/Server-side/Express_Nodejs/forms/Create_genre_form +translation_of: Learn/Server-side/Express_Nodejs/forms/Create_genre_form +--- +

本章节演示如何定义我们的页面,创建Genre 物件(这是一个很好的起点,因为类型只有一个字段,它的名称name,没有依赖项)。像任何其他页面一样,我们需要设置路由,控制器和视图。

+ +

引入验证与无害化方法

+ +

在我们的控制器中使用 express-validator 验证器,我們必須导入我们想要从 'express-validator/check' 和 'express-validator/filter' 模块中使用的函数。

+ +

打开/controllers/genreController.js,并在文件顶部添加以下行:

+ +
const { body,validationResult } = require('express-validator/check');
+const { sanitizeBody } = require('express-validator/filter');
+ +

控制器—get路由

+ +

找到导出的genre_create_get() 控制器方法,并将其替换为以下代码。这只是渲染genre_form.pug视图,传递一个title变量。

+ +
// Display Genre create form on GET.
+exports.genre_create_get = function(req, res, next) {
+    res.render('genre_form', { title: 'Create Genre' });
+};
+ +

控制器—post 路由

+ +

找到导出的genre_create_post()控制器方法,并将其替换为以下代码。

+ +
// Handle Genre create on POST.
+exports.genre_create_post =  [
+
+    // Validate that the name field is not empty.
+    body('name', 'Genre name required').isLength({ min: 1 }).trim(),
+
+    // Sanitize (trim and escape) the name field.
+    sanitizeBody('name').trim().escape(),
+
+    // Process request after validation and sanitization.
+    (req, res, next) => {
+
+        // Extract the validation errors from a request.
+        const errors = validationResult(req);
+
+        // Create a genre object with escaped and trimmed data.
+        var genre = new Genre(
+          { name: req.body.name }
+        );
+
+
+        if (!errors.isEmpty()) {
+            // There are errors. Render the form again with sanitized values/error messages.
+            res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors.array()});
+        return;
+        }
+        else {
+            // Data from form is valid.
+            // Check if Genre with same name already exists.
+            Genre.findOne({ 'name': req.body.name })
+                .exec( function(err, found_genre) {
+                     if (err) { return next(err); }
+
+                     if (found_genre) {
+                         // Genre exists, redirect to its detail page.
+                         res.redirect(found_genre.url);
+                     }
+                     else {
+
+                         genre.save(function (err) {
+                           if (err) { return next(err); }
+                           // Genre saved. Redirect to genre detail page.
+                           res.redirect(genre.url);
+                         });
+
+                     }
+
+                 });
+        }
+    }
+];
+ +

首先要注意的是,控制器不是单个中间件函数(带参数(req, res, next)),而是指定一组中间件函数。数组传递给路由器函数,并按顺序调用每个方法。

+ + + +
+

注意: 这种方法是必需的,因为消毒/验证器是中间件功能。

+
+ +

数组中的第一个方法定义了一个验证器(body),来检查 name 字段是否为空(在执行验证之前调用trim(),以删除任何尾随/前导空格)。

+ +

数组中的第二个方法(sanitizeBody()),创建一个清理程序来调用trim()修剪名称字段和调用escape()转义任何危险的 HTML 字符。

+ +
// Validate that the name field is not empty.
+body('name', 'Genre name required').isLength({ min: 1 }).trim(),
+
+// Sanitize (trim and escape) the name field.
+sanitizeBody('name').trim().escape(),
+ + + +
+

注意: 验证期间运行的清洁器不会修改请求。这就是为什么我们必须在上面的两个步骤中调用trim()

+
+ +

在指定验证器和清理器之后,我们创建了一个中间件函数,来提取任何验证错误。我们使用isEmpty() 来检查验证结果中,是否有任何错误。如果有,那么我们再次渲染表单,传入我们的已清理种类对象和错误消息的数组(errors.array())。

+ +
// Process request after validation and sanitization.
+(req, res, next) => {
+
+    // Extract the validation errors from a request.
+    const errors = validationResult(req);
+
+    // Create a genre object with escaped and trimmed data.
+    var genre = new Genre(
+      { name: req.body.name }
+    );
+
+    if (!errors.isEmpty()) {
+        // There are errors. Render the form again with sanitized values/error messages.
+        res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors.array()});
+    return;
+    }
+    else {
+        // Data from form is valid.
+        ... <save the result> ...
+    }
+}
+ +

如果种类名称数据有效,那么我们检查,是否已存在具有相同名称的种类Genre(因为我们不想创建重复项)。

+ +

如果是,我们会重定向到现有种类的详细信息页面。如果没有,我们保存新种类,并重定向到其详细信息页面。

+ +
// Check if Genre with same name already exists.
+Genre.findOne({ 'name': req.body.name })
+    .exec( function(err, found_genre) {
+    if (err) { return next(err); }
+        if (found_genre) {
+            // Genre exists, redirect to its detail page.
+            res.redirect(found_genre.url);
+            }
+        else {
+            genre.save(function (err) {
+                if (err) { return next(err); }
+                    // Genre saved. Redirect to genre detail page.
+                    res.redirect(genre.url);
+                });
+        }
+});
+ +

在我们所有的 POST控制器中,都使用了相同的模式:我们运行验证器,然后运行消毒器,然后检查错误,并使用错误信息重新呈现表单,或保存数据。

+ +

视图

+ +

当我们创建一个新的种类Genre时,在GETPOST控制器/路由中,都会呈现相同的视图(稍后在我们更新种类Genre时也会使用它)。

+ +

GET情况下,表单为空,我们只传递一个title变量。在POST情况下,用户先前输入了无效数据 - 在种类变量genre中,我们传回了输入数据的已清理版本,并且在errors变量中,我们传回了一组错误消息。

+ +
res.render('genre_form', { title: 'Create Genre'});
+res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors.array()});
+ +

创建 /views/genre_form.pug,并复制下面的文本。

+ +
extends layout
+
+block content
+  h1 #{title}
+
+  form(method='POST' action='')
+    div.form-group
+      label(for='name') Genre:
+      input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name))
+    button.btn.btn-primary(type='submit') Submit
+
+  if errors
+    ul
+      for error in errors
+        li!= error.msg
+ +

从我们之前的教程中,可以很好地理解这个模板的大部分内容。首先,我们扩展 layout.pug基本模板,并覆盖名为 “content” 的块block。然后我们有一个标题,我们从控制器传入的标题title(通过render() 方法)。

+ +

接下来,我们有 HTML表单的 Pug 代码,它使用POST方法将数据发送到服务器,并且因为操作action是空字符串,所以将数据发送到与页面相同的URL。

+ +

表单定义了一个名为 “name” 的 “text” 类型的必填字段。字段的默认值,取决于是否定义了种类变量genre。如果从GET路由调用,它将为空,因为这是一个新表单。如果从POST路由调用,它将包含用户最初输入的(无效)值。

+ +

页面的最后一部分是错误代码。如果已定义错误变量,则只会打印错误列表(换句话说,当模板在GET路由上呈现时,此部分不会出现)。

+ +
+

注意: 这只是呈现错误的一种方法。您还可以从错误变量中,获取受影响字段的名称,并使用这些,来控制错误消息的呈现位置,以及是否应用自定义 CSS 等。

+
+ +

它看起來像是?

+ +

运行应用程序,打开浏览器到http://localhost:3000/,然后选择 Create new genre 链接。如果一切设置正确,您的网站应该类似于以下屏幕截图。输入值后,应保存该值,您将进入种类详细信息页面。

+ +

Genre Create Page - Express Local Library site

+ +

我们针对服务器端,验证的唯一错误是种类字段不能为空。下面的屏幕截图,显示了如果您没有提供种类(以红色突出显示),错误列表会是什么样子。

+ +

+ +
+

注意: 我们的验证使用trim(),来确保不接受空格作为种类名称。我们还可以在表单中​​的字段定义中,添加值required='true',来验证客户端字段不为空:

+ +
input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name), required='true' )
+
+ +

下一步

+ + + +
+ + + + + +
diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/delete_author_form/index.html b/files/zh-cn/learn/server-side/express_nodejs/forms/delete_author_form/index.html new file mode 100644 index 0000000000..3b38e85e8a --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/delete_author_form/index.html @@ -0,0 +1,165 @@ +--- +title: 删除作者表单 +slug: learn/Server-side/Express_Nodejs/forms/Delete_author_form +translation_of: Learn/Server-side/Express_Nodejs/forms/Delete_author_form +--- +

此子文档显示,如何定义页面以删除Author对象。

+ +

正如表单设计部分所讨论的那样,我们的策略是,只允许删除 “未被其他对象引用” 的对象(在这种情况下,这意味着如果作者Author被一本书Book引用,我们将不允许删除作者)。在实现方面,这意味着,表单需要在删除作者之前,先确认没有关联的书籍。如果存在关联的书籍,则应显示它们,并说明在删除Author对象之前,必须删除它们。

+ +

控制器—get 路由

+ +

打开/controllers/authorController.js。找到导出的author_delete_get() 控制器方法,并将其替换为以下代码。

+ +
// Display Author delete form on GET.
+exports.author_delete_get = function(req, res, next) {
+
+    async.parallel({
+        author: function(callback) {
+            Author.findById(req.params.id).exec(callback)
+        },
+        authors_books: function(callback) {
+          Book.find({ 'author': req.params.id }).exec(callback)
+        },
+    }, function(err, results) {
+        if (err) { return next(err); }
+        if (results.author==null) { // No results.
+            res.redirect('/catalog/authors');
+        }
+        // Successful, so render.
+        res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books } );
+    });
+
+};
+ +

控制器从URL参数(req.params.id)中,获取要删除的Author实例的 id。它使用async.parallel() 方法,并行获取作者记录和所有相关书本。当两个操作都完成后,它将呈现author_delete.pug视图,为titleauthor、和 author_books传递变量。

+ +
+

注意: 如果findById()返回“没有结果”,则作者不在数据库中。在这种情况下,没有什么可以删除,所以我们立即呈现所有作者的列表。

+ +
}, function(err, results) {
+    if (err) { return next(err); }
+    if (results.author==null) { // No results.
+        res.redirect('/catalog/authors')
+    }
+
+ +

控制器—post 路由

+ +

找到导出的author_delete_post()控制器方法,并将其替换为以下代码。

+ +
// Handle Author delete on POST.
+exports.author_delete_post = function(req, res, next) {
+
+    async.parallel({
+        author: function(callback) {
+          Author.findById(req.body.authorid).exec(callback)
+        },
+        authors_books: function(callback) {
+          Book.find({ 'author': req.body.authorid }).exec(callback)
+        },
+    }, function(err, results) {
+        if (err) { return next(err); }
+        // Success
+        if (results.authors_books.length > 0) {
+            // Author has books. Render in same way as for GET route.
+            res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books } );
+            return;
+        }
+        else {
+            // Author has no books. Delete object and redirect to the list of authors.
+            Author.findByIdAndRemove(req.body.authorid, function deleteAuthor(err) {
+                if (err) { return next(err); }
+                // Success - go to author list
+                res.redirect('/catalog/authors')
+            })
+        }
+    });
+};
+ +

首先,我们验证是否已提供id(这是通过表单主体参数发送的,而不是使用URL中的版本)。然后我们以与GET路由相同的方式,获得作者及其相关书本。如果没有书本,那么我们删除作者对象,并重定向到所有作者的列表。如果还有书本,那么我们只需重新呈现表格,传递作者和要删除的书本列表。

+ +
+

注意: 我们可以检查对findById()的调用,是否返回任何结果,如果没有,则立即呈现所有作者的列表。为简洁起见,我们将代码保留在上面(如果找不到id,它仍会返回作者列表,但这将在findByIdAndRemove()之后发生)。

+
+ +

视图

+ +

创建 /views/author_delete.pug 并复制贴上底下文字。

+ +
extends layout
+
+block content
+  h1 #{title}: #{author.name}
+  p= author.lifespan
+
+  if author_books.length
+
+    p #[strong Delete the following books before attempting to delete this author.]
+
+    div(style='margin-left:20px;margin-top:20px')
+
+      h4 Books
+
+      dl
+      each book in author_books
+        dt
+          a(href=book.url) #{book.title}
+        dd #{book.summary}
+
+  else
+    p Do you really want to delete this Author?
+
+    form(method='POST' action='')
+      div.form-group
+        input#authorid.form-control(type='hidden',name='authorid', required='true', value=author._id )
+
+      button.btn.btn-primary(type='submit') Delete
+ +

视图扩展了布局模板,覆盖了名为content的区块。在顶部显示作者详细信息。然后它包含一个,基于author_booksifelse子句)数量的条件语句。

+ + + +

加入一个删除控制器

+ +

接下来,我们将向 Author 详细视图添加 Delete 控件(详细信息页面是删除记录的好地方)。

+ +
+

注意: 在完整实现中,控件将仅对授权用户可见。但是在这个时间点上,我们还没有一个授权系统!

+
+ +

打开 author_detail.pug 视图,并在底部添加以下行。

+ +
hr
+p
+  a(href=author.url+'/delete') Delete author
+ +

控件现在应显示为链接,如下面的作者详细信息页面所示。

+ +

+ +

它看起來像是?

+ +

运行应用程序,并将浏览器打开,到http://localhost:3000/。然后选择所有作者链接 All authors,然后选择一个特定作者。最后选择删除作者链接 Delete author。

+ +

如果作者没有书本,您将看到这样的页面。按删除后,服务器将删除作者并重定向到作者列表。

+ +

+ +

如果作者确实有书本,那么您将看到如下视图。然后,您可以从其详细信息页面中,删除这些书本(一旦该代码被实现!)。

+ +

+ +
+

注意: 其他删除对象的页面,可以用相同的方式实现。我们把它留下,作为挑战。

+
+ +

下一步

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/index.html b/files/zh-cn/learn/server-side/express_nodejs/forms/index.html new file mode 100644 index 0000000000..f3cee84344 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/index.html @@ -0,0 +1,286 @@ +--- +title: 'Express 教程 6: 使用表单' +slug: learn/Server-side/Express_Nodejs/forms +translation_of: Learn/Server-side/Express_Nodejs/forms +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/Displaying_data", "Learn/Server-side/Express_Nodejs/deployment", "Learn/Server-side/Express_Nodejs")}}
+ +

在此教程中我们会教你如何使用Express并且结合Pug来实现HTML表单,并且如何从数据库中创建,更新和删除文档。

+ + + + + + + + + + + + +
前提条件: +

完成前面所有的教程,包括 Express 教程第5章: 展示图书馆数据

+
目标: +

了解如何编写表单获取用户信息,并且将这些数据更新到数据库中。

+
+ +

概览

+ +

HTML表单在网页中是一个或多个字段/小工具的集合,它被用来收集用户的信息,并将信息上传到服务器。表单作为一种用来收集用户的机制,非常的灵活,因为有各种合适的输入框,来接受各种类型的数据——文本框,复选框,单选按钮,时间选择器等。表单和服务器交互数据也相对安全,因为它使用POST请求发送数据,保护不受跨站点请求伪造攻击(cross-site request forgery)的威胁。

+ +

但是表单同样也很复杂!开发者需要为表单编写 HTML,在服务器上验证,并且正确去除有害的数据(浏览器上也可能需要),对于任何不合法的字段,需要传给用户相应的错误信息,当数据成功提交后,处理数据,并设法通知用户提交成功。

+ +

此教程将展示上述的操作,如何在 Express 中实现。在此过程中,我们将扩展 LocalLibrary 网站,以允许用户创建,编辑和删除图书馆中的项目。

+ +
+

注意: 我们还没有考虑如何将特定路由,限制为经过身份验证或授权的用户,因此在这个时间点,任何用户都可以对数据库进行更改。

+
+ +

HTML表单

+ +

首先简要概述HTML表单。考虑一个简单的HTML表单,其中包含一个文本字段,用于输入某些 “团队” 的名称,及其相关标签:

+ +

Simple name field example in HTML form

+ +

表单在HTML中,定义为<form>...</form>标记内的元素集合,包含至少一个type="submit"input输入元素。

+ +

请注意,我非常建议您这里使用input的submit而不是button!这会使你感到愉悦。

+ +
<form action="/team_name_url/" method="post">
+    <label for="team_name">Enter name: </label>
+    <input id="team_name" type="text" name="name_field" value="Default name for team.">
+    <input type="submit" value="OK">
+</form>
+ +

虽然这里,我们只包含一个(文本)字段,用于输入团队名称,但表单可能包含任意数量的其他输入元素,及其相关标签。字段的 type 属性,定义将显示哪种窗口小部件。该字段的名称nameid ,用于标识JavaScript/CSS/HTML 中的字段,而 value定义字段首次显示时的初始值。匹配团队标签使用label标签,指定(请参阅上面的“输入名称” "Enter name"),其中 for 字段,包含input相关输入的id值。

+ +

另外,有必要说一下,HTML中form表单默认就是以post提交的。它比get方式存储量更大、传输更安全。

+ +

提交输入(submit)将显示为按钮(默认情况下) - 用户可以按此按钮,将其他输入元素包含的数据,上传到服务器(在本例中,只有team_name)。表单属性,定义用于发送数据的HTTP method方法,和服务器上数据的目标(action):

+ + + +

表单处理流程

+ +

表单处理使用的技术,与我们学习过、用来显示有关模型的信息的所有技术,是相同的:路由将我们的请求发送到控制器函数,该函数执行所需的任何数据库操作,包括从模型中读取数据,然后生成并返回HTML页面。使事情变得更复杂的是,服务器还需要能够处理用户提供的数据,并在出现任何问题时,重新显示带有错误信息的表单。

+ +

下面显示了处理表单请求的流程图,从包含表单的页面请求开始(以绿色显示):

+ +

+ +

如上图所示,构成处理代码所需要做的主要是:

+ +
    +
  1. 在用户第一次请求时显示默认表单。 +
      +
    • 表单可能包含空白字段(例如,如果您正在创建新记录),或者可能预先填充了初始值(例如,如果您要更改记录,或者具有有用的默认初始值)。
    • +
    +
  2. +
  3. 接收用户提交的数据,通常是在HTTP POST请求中。
  4. +
  5. 验证并清理数据。
  6. +
  7. 如果任何数据无效,请重新显示表单 - 这次使用用户填写的任何值,和问题字段的错误消息。
  8. +
  9. 如果所有数据都有效,请执行所需的操作(例如,将数据保存在数据库中,发送通知电子邮件,返回搜索结果,上传文件等)
  10. +
  11. 完成所有操作后,将用户重定向到另一个页面。
  12. +
+ +

表格处理代码,通常使用GET路由,以实现表单的初始显示,以及POST路由到同一路径,以处理表单数据的验证和处理。这是将在本教程中使用的方法!Express本身不提供表单处理操作的任何特定支持,但它可以使用中间件,以处理表单中的POSTGET参数,并验证/清理它们的值。

+ +

验证和清理

+ +

在储存表单中的数据之前,必须对其进行验证和清理:

+ + + +

在本教程中,我们将使用流行的 express-validator 模块,来执行表单数据的验证和清理。

+ +

安装

+ +

通过在项目的根目录中,运行以下命令来安装模块。

+ +
npm install express-validator --save
+
+ +

使用 express-validator

+ +
+

注意: Github上的express-validator指南,提供了API的良好概述。我们建议您阅读该内容,以了解其所有功能(包括创建自定义验证程序)。下面我们只介绍一个对LocalLibrary有用的子集。

+
+ +

要在我们的控制器中使用验证器,我们必须从'express-validator/check'和'express-validator/filter'模块中,导入我们想要使用的函数,如下所示:

+ +
const { body,validationResult } = require('express-validator/check');
+const { sanitizeBody } = require('express-validator/filter');
+
+ +

有许多可用的功能,允许您一次检查和清理请求参数,正文,标头,cookie 等数据,或所有数据。对于本教程,我们主要使用bodysanitizeBody,和 validationResult(如上面 “导入”的 )。

+ +

功能定义如下:

+ + + +

验证和清理链,是应该传递给Express路由处理程序的中间件(我们通过控制器,间接地执行此操作)。中间件运行时,每个验证器/清理程序都按指定的顺序运行。
+ 当我们实现下面的LocalLibrary表单时,我们将介绍一些真实的例子。

+ +

表单设计

+ +

图书馆中的许多模型都是相关/依赖的 - 例如,一本书需要一个作者,也可能有一个或多个种类。这提出了一个问题,即我们应该如何处理用户希望的情况:

+ + + +

在这个项目,我们将简单声明表单只能:

+ + + +

让我们看看更高级的内容吧:

+ +

我们通常会在“后台”接收form表单提交的数据。显而易见,这里应该是express!

+ +

首先我们可以知道(也许你会知道)应该先引入express:

+ +

const app=express(); 

+ +

这很好。

+ +

那么既然是post提交,给大家推荐一款中间件:body-parser。它能让你轻松地处理body数据。

+ +

哦,如果你涉及文件上传,那么你可能需要“multer”中间件,你大概听说过“formidable”,但multer比它更强大!

+ +
+

注意: 更“牢固”的实现,可能允许您在创建新对象时创建依赖对象,并随时删除任何对象(例如,通过删除依赖对象,或从数据库中,删除对已删除对象的引用) 。

+
+ +

路由

+ +

为了实现我们的表单处理代码,我们需要两个具有相同URL模式的路由。

+ +

第一个(GET)路由,用于显示用于创建对象的新空表单。第二个路由(POST),用于验证用户输入的数据,然后保存信息,并重定向到详细信息页面(如果数据有效),或重新显示有错误的表单(如果数据无效)。

+ +

我们已经在 /routes/catalog.js(在之前的教程中)为我们所有模型的创建页面,创建了路径。例如,种类路由如下所示:

+ +
// GET request for creating a Genre. NOTE This must come before route that displays Genre (uses id).
+router.get('/genre/create', genre_controller.genre_create_get);
+
+// POST request for creating Genre.
+router.post('/genre/create', genre_controller.genre_create_post);
+
+ +

Express 表单子文件

+ +

以下子文件,将带我们完成向示例应用程序添加所需表单的过程。在进入下一个文件之前,您需要依次阅读并解决每个问题。

+ +
    +
  1. 创建种类表单 — 定义我们的页面以创建Genre种类对象。
  2. +
  3. 创建作者表单 — 定义用于创建作者对象的页面。
  4. +
  5. 创建书本表单 — 定义页面/表单以创建书本对象。
  6. +
  7. 创建书本实例表单 — 定义页面/表单以创建书本实例对象。
  8. +
  9. 删除作者表单 — 定义要删除作者对象的页面。
  10. +
  11. 更新书本表单 — 定义页面以更新书本对象。
  12. +
+ +

挑战自我

+ +

实现Book, BookInstance, 和 Genre模型的删除页面,以与我们的作者删除页面相同的方式,将它们与关联的详细信息页面,链接起来。页面应遵循相同的设计方法:

+ + + +

一些提示:

+ + + +

实现BookInstance, Author, 和 Genre模型的更新页面,以与我们的书本更新页面相同的方式,将它们与关联的详细信息页面,链接起来。

+ +

一些提示:

+ + + +

总结

+ +

Express, node, 与NPM上面的第三方套件,提供你需要的每样东西 ,可用于新增表单到你的网站上。在本文中,你学到了如何使用Pug, how to create forms using Pug, validate and sanitize input using express-validator, and add, delete, and modify records in the database.

+ +

你现在应该了解如何新增基本表单,以及表单处理码到你的 node 网站!

+ +

请见

+ + + +

{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/Displaying_data", "Learn/Server-side/Express_Nodejs/deployment", "Learn/Server-side/Express_Nodejs")}}

+ +

本教程

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/update_book_form/index.html b/files/zh-cn/learn/server-side/express_nodejs/forms/update_book_form/index.html new file mode 100644 index 0000000000..de1faa0909 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/update_book_form/index.html @@ -0,0 +1,189 @@ +--- +title: 更新书本表单 +slug: learn/Server-side/Express_Nodejs/forms/Update_Book_form +translation_of: Learn/Server-side/Express_Nodejs/forms/Update_Book_form +--- +

Edit本文最后一部分演示如何定义一个页面,以更新书本(Book)对象。当更新一本书的时候,表单处理更像是创建一本书,除了你必须将表单填进 GET 路由,并附加上来自数据库的值。

+ +

控制器—get 路由

+ +

打开 /controllers/bookController.js. 找到 exported book_update_get() 控制方法,并用底下的代码替换。

+ +
// Display book update form on GET.
+exports.book_update_get = function(req, res, next) {
+
+    // Get book, authors and genres for form.
+    async.parallel({
+        book: function(callback) {
+            Book.findById(req.params.id).populate('author').populate('genre').exec(callback);
+        },
+        authors: function(callback) {
+            Author.find(callback);
+        },
+        genres: function(callback) {
+            Genre.find(callback);
+        },
+        }, function(err, results) {
+            if (err) { return next(err); }
+            if (results.book==null) { // No results.
+                var err = new Error('Book not found');
+                err.status = 404;
+                return next(err);
+            }
+            // Success.
+            // Mark our selected genres as checked.
+            for (var all_g_iter = 0; all_g_iter < results.genres.length; all_g_iter++) {
+                for (var book_g_iter = 0; book_g_iter < results.book.genre.length; book_g_iter++) {
+                    if (results.genres[all_g_iter]._id.toString()==results.book.genre[book_g_iter]._id.toString()) {
+                        results.genres[all_g_iter].checked='true';
+                    }
+                }
+            }
+            res.render('book_form', { title: 'Update Book', authors:results.authors, genres:results.genres, book: results.book });
+        });
+
+};
+ +

这个控制器从URL参数(req.params.id)中,取得要更新的书本 Book 的 id。它使用 async.parallel()方法,取得指定的书本 Book 纪录 (填入它的种类和作者字段) ,并列出所有作者 Author 和种类 Genre对象。当所有操作都完成,它用勾选的方式,标记当前选择的种类,并呈现 book_form.pug 视图,传送变数 titlebook、所有 authors、所有 genres

+ +

控制器—post 路由

+ +

找到 exported book_update_post() 控制器方法,并替换为底下的代码。

+ +
// Handle book update on POST.
+exports.book_update_post = [
+
+    // Convert the genre to an array
+    (req, res, next) => {
+        if(!(req.body.genre instanceof Array)){
+            if(typeof req.body.genre==='undefined')
+            req.body.genre=[];
+            else
+            req.body.genre=new Array(req.body.genre);
+        }
+        next();
+    },
+
+    // Validate fields.
+    body('title', 'Title must not be empty.').isLength({ min: 1 }).trim(),
+    body('author', 'Author must not be empty.').isLength({ min: 1 }).trim(),
+    body('summary', 'Summary must not be empty.').isLength({ min: 1 }).trim(),
+    body('isbn', 'ISBN must not be empty').isLength({ min: 1 }).trim(),
+
+    // Sanitize fields.
+    sanitizeBody('title').trim().escape(),
+    sanitizeBody('author').trim().escape(),
+    sanitizeBody('summary').trim().escape(),
+    sanitizeBody('isbn').trim().escape(),
+    sanitizeBody('genre.*').trim().escape(),
+
+    // Process request after validation and sanitization.
+    (req, res, next) => {
+
+        // Extract the validation errors from a request.
+        const errors = validationResult(req);
+
+        // Create a Book object with escaped/trimmed data and old id.
+        var book = new Book(
+          { title: req.body.title,
+            author: req.body.author,
+            summary: req.body.summary,
+            isbn: req.body.isbn,
+            genre: (typeof req.body.genre==='undefined') ? [] : req.body.genre,
+            _id:req.params.id //This is required, or a new ID will be assigned!
+           });
+
+        if (!errors.isEmpty()) {
+            // There are errors. Render form again with sanitized values/error messages.
+
+            // Get all authors and genres for form.
+            async.parallel({
+                authors: function(callback) {
+                    Author.find(callback);
+                },
+                genres: function(callback) {
+                    Genre.find(callback);
+                },
+            }, function(err, results) {
+                if (err) { return next(err); }
+
+                // Mark our selected genres as checked.
+                for (let i = 0; i < results.genres.length; i++) {
+                    if (book.genre.indexOf(results.genres[i]._id) > -1) {
+                        results.genres[i].checked='true';
+                    }
+                }
+                res.render('book_form', { title: 'Update Book',authors:results.authors, genres:results.genres, book: book, errors: errors.array() });
+            });
+            return;
+        }
+        else {
+            // Data from form is valid. Update the record.
+            Book.findByIdAndUpdate(req.params.id, book, {}, function (err,thebook) {
+                if (err) { return next(err); }
+                   // Successful - redirect to book detail page.
+                   res.redirect(thebook.url);
+                });
+        }
+    }
+];
+ +

这很像是创建一本书的时候,所使用的 post 路由。首先,我们验证来自表单的书本数据,并进行无害化处理,并使用它创建一个新的书本 Book 对象 (将它的 _id 值,设置给将要更新的对象的 id)。当我们验证资料,然后重新呈现表单的时候,如果存在错误,再附加显示使用者输入的资料、错误信息、以及种类和作者列表。当我们调用Book.findByIdAndUpdate() 去更新 Book ,如果没有错误,就重新导向到它的细节页面。

+ +

视图

+ +

打开 /views/book_form.pug ,并更新作者表单控制器的区段,以加入底下条件控制代码。

+ +
    div.form-group
+      label(for='author') Author:
+      select#author.form-control(type='select' placeholder='Select author' name='author' required='true' )
+        for author in authors
+          if book
+            //- Handle GET form, where book.author is an object, and POST form, where it is a string.
+            option(
+              value=author._id
+              selected=(
+                author._id.toString()==book.author._id
+                || author._id.toString()==book.author
+              ) ? 'selected' : false
+            ) #{author.name}
+          else
+            option(value=author._id) #{author.name}
+ +
+

注意: 此处代码的更动,是为了让书本表单 book_form ,能被创建和更新书本的对象共同使用 (如果不这么做,当创建表单时,在 GET 路由会发生一个错误)。

+
+ +

加入一个更新按钮

+ +

打开 book_detail.pug 视图,并确认在页面下方,有删除和更新书本的连结,如下所示。

+ +
  hr
+  p
+    a(href=book.url+'/delete') Delete Book
+  p
+    a(href=book.url+'/update') Update Book
+ +

你现在应该能够更新来自书本细节页面的书了。

+ +

它看起來像是?

+ +

运行本应用,打开浏览器,访问网址 http://localhost:3000/,点击所有书本 All books 连结,然后点击一本书。最后点击更新书本 Update Book 连结。

+ +

表单看起来应该就像是创建书本页面,只是标题变为 'Update book',并且事先填入纪录值。

+ +

+ +
+

注意: 其它更新对象的页面,也可以用同样的方式处理。我们把这些更新页面的实作留下,做为自我挑战。

+
+ +

 

+ +

下一步

+ + + +

 

diff --git a/files/zh-cn/learn/server-side/express_nodejs/index.html b/files/zh-cn/learn/server-side/express_nodejs/index.html new file mode 100644 index 0000000000..294474589a --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/index.html @@ -0,0 +1,72 @@ +--- +title: Express Web Framework (Node.js/JavaScript) +slug: learn/Server-side/Express_Nodejs +tags: + - Express + - Node +translation_of: Learn/Server-side/Express_Nodejs +--- +
{{LearnSidebar}}
+ +

Express是一款受欢迎的开源web框架,构建语言是JavaScript,可以在node.js环境运行。本系列文章介绍了该框架的优点,如何搭建开发环境以及部署web开发环境并进行开发任务。

+ +

前提

+ +

在开始这个模块之前你需要知道什么是服务端网页编程和 web 框架,建议你先阅读 服务端网页编程 模块。强烈推荐了解编程概念和  JavaScript ,但这对理解核心概念不是必需的。

+ +
+

注意:这个网站有很多有用的资源用来学习JavaScript做客户端开发: JavaScriptJavaScript Guide, JavaScript BasicsJavaScript (learning). 核心的JavaScript语言和概念用Nodejs服务端开发是相同的,也是相关的。Node.js 提供 额外的API 用于支持在无浏览器环境中有用的功能,例如,创建HTTP服务器并访问文件系统,但不支持JavaScript API以使用浏览器和DOM。

+ +

这篇指南将会提供一些Node.js 和 Express的信息, 并且有很多优秀的网络资源和书籍。一些链接 比如How do I get started with Node.js (StackOverflow) 和 What are the best resources for learning Node.js? (Quora).

+
+ +

指南

+ +
+
Express/Node 介绍
+
在这篇文章中,我们回答了“什么是 Node?”和“什么是 Express?”并为您概述了Express web框架的特殊之处。我们将介绍主要功能,并向您展示Express应用程序的一些主要构建模块(尽管此时您还没有可用于测试它的开发环境)。
+
搭建 Node(Express) 开发环境
+
介绍了 Express 的所用之处后,我们将向您展示如何在不同操作系统下建立并验证 Node/Express 开发环境。无论您使用任何操作系统,这篇文章都可以完全指导如何开始构建 Express 应用。
+
Express 教程——第一部分:本地图书馆
+
该实用教程系列中的第一篇文章,介绍了即将学习的内容,并概述了在后续文章中不断迭代的 “本地图书馆”例子 。
+
Express 教程——第二部分:建立网站的骨架
+
这篇文章将介绍如何建立一个网站项目的 “骨架”,然后您可以继续添加自己的路由、模板/视图和数据库。
+
Express 教程——第三部分:使用数据库(Mongoose)
+
这篇文章简单介绍了在 Node/Express 中如何使用数据库。本文中我们将会使用 Mongoose 为该项目(本地图书馆)提供数据访问,同时解释了如何定义对象模式、模型和基础和验证。本文也简单介绍了访问模型数据的一些主流方式。
+
Express 教程——第四部分:路由和控制器
+
我们在本教程中会设置路由来编写一些本地图书馆所需的伪造端点(endpoints)。在接下来的文章中,路由会有一个模块结构,可用来拓展real handler functions。最终,我们会对用Express创建模块化路由有很好的理解。
+
Express 教程——第五部分:在 HTML 上展示图书数据
+
+ +

我们现在已经准备好为展示本地图书馆图书和其他数据添加页面,包括展示每个model有多少记录的主页,以及list和detail页面。我们会积累从database获取记录以及使用模版的实战经验。

+ +
+
Express教程——第六部分: 如何使用表单
+
本教程我们会教你如何在Express使用HTML表单,Pug,以及从数据库中创建,更新,删除文件。
+
Express教程——第七部分:如何部署到生产环境
+
现在你已经创建了一个很棒的本地图书馆网站,你可以把本地环境迁移到公共网络服务器上,别人也可以使用网站。本章提供了如何找服务器,部署网站到生产环境的概览。
+
+ +

另见

+ +
+
在PWS/Cloud Foundry安装LocalLibrary
+
本文提供了如何在Pivotal Web Services PaaS 云上安装LocalLibrary的实际演示 - 这是Heroku的全功能,开源替代品,Heroku是本教程第7部分中使用的PaaS云服务,如上所列。 如果您正在寻找Heroku(或其他PaaS云服务)的替代方案,或者只是想尝试不同的东西,PWS / Cloud Foundry绝对值得一试。
+
+
+ +

其它教程

+ +
+

本教程到此结束,如果你想要更进一步,以下包含更多有趣的主题:

+ + + +

当然,如果做一个评估任务会更好!

+
diff --git a/files/zh-cn/learn/server-side/express_nodejs/installing_on_pws_cloud_foundry/index.html b/files/zh-cn/learn/server-side/express_nodejs/installing_on_pws_cloud_foundry/index.html new file mode 100644 index 0000000000..e3f321e83a --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/installing_on_pws_cloud_foundry/index.html @@ -0,0 +1,242 @@ +--- +title: 在 PWS/Cloud Foundry 上,安装 LocalLibrary +slug: learn/Server-side/Express_Nodejs/Installing_on_PWS_Cloud_Foundry +translation_of: Learn/Server-side/Express_Nodejs/Installing_on_PWS_Cloud_Foundry +--- +
{{LearnSidebar}}
+ +

本文提供了如何在 Pivotal Web Services PaaS cloud 云上安装 LocalLibrary的实际演示 - 这是 Heroku 的全功能,开源替代品,Heroku 是我们教程第 7 部分中使用的 PaaS 云服务。如果您正在寻找 Heroku(或其他 PaaS 云服务)的替代方案,或者只是想尝试不同的东西,PWS / Cloud Foundry 绝对值得一试。

+ +

为什么是 PWS?

+ +

Pivotal Web Services 是开源云代工平台的公共实例。它是一个多语言平台,支持许多不同的语言,包括Node.js,Java,PHP,Python,Staticfiles 和 Ruby。它有一个免费试用版,对于运行 Node 应用程序非常有效!由于 Node 和 Express 是开源项目,因此与使用 Cloud Foundry 等开放式部署平台保持一致。您可以深入了解应用程序的托管方式。

+ +

使用PWS有多种原因!

+ + + +

PWS 是如何工作的?

+ +

PWS通过容器来运行网站和应用已经有些年头了。Cloud Foundry一开始采用的容器技术名为Warden,现在使用的是名为Garden的容器系统。这些技术与流行的Docker容器很相似,而且事实上Cloud Foundry云平台的很多设施都支持部署Docker容器。

+ +

使用Cloud Foundry云平台的好处之一是您不需要创建容器规范,因为Cloud Foundry的构建包会基于最新的组件来生成它们。由于容器是临时部署的,而且可能随时被清理并部署到云中的其他位置,所以Cloud Foundry云平台上的应用需要遵循十二要素准则。如此可以确保您的应用和平台使用最新的软件。一个应用程序可以包含多个实例,在这些实例中,应用程序被放置到冗余容器中,从而实现应用程序的高可用性。Cloud Foundry会自动处理相同实例之间的所有负载平衡。这允许您扩展应用程序的性能和可用性。

+ +

由于文件系统是临时的,所以任何临时存储或服务都应该使用备份服务放置到其他地方。这可以通过使用不同提供商提供的市场服务或通过用户自己提供的服务来实现。

+ +

What do we cover below?

+ +

This post covers how to modify the LocalLibrary application from the tutorial for deployment on PWS and Cloud Foundry. In doing so, it covers the basics of deploying any node.js application to PWS with the following steps.

+ + + +

So let's get started. You have two options, you can go through the tutorial from the beginning or you can just download the completed project and modify it from there for use on PWS. To do the latter, you can do the following from a terminal:

+ +
git clone https://github.com/mdn/express-locallibrary-tutorial
+ +

You'll then need to follow the preparation steps listed in the Getting your website ready to publish section of Express Tutorial Part 7: Deploying to production, before then following the steps listed below.

+ +
+

Note: This work flow is based on the Mozilla Heroku work flow in the main Express/Node tutorial series for consistency, to help readers compare and contrast. 

+
+ +

Modifying the LocalLibrary for PWS

+ +

Deployment of a Node application on Cloud Foundry takes the following steps. Using the downloaded 'cf' CLI tool on your environment, your source code and supporting metadata files are uploaded to Cloud Foundry which will assemble and package the components of your application. Note that your files need to be located on your system to deploy or as a zip archive somewhere accessible on the internet. We'll assume the former.

+ +

This means, no assumptions about which source control system is used. As long as you have a complete source tree in your local file system you can deploy the app. There are some things you have to make available to ensure the correctly assembly of your Node application. First Cloud Foundry cf CLI will look for the presence of the 'package.json' file to determine the dependencies and download the necessary components. The rules of this assembly are defined in Cloud Foundry's nodejs buildpack. An optional cloud foundry manifest file can specify information about your application such as name, size and start command if non-standard. In addition to deploying the application, the cf CLI tool can also configure services, set environment variables and view logs. That's all the overview you need in order to get started (see Getting Started on Pivotal Web Services for a more comprehensive guide). Let's start making the changes so you'll need to deploy the LocalLibrary application to  PWS.

+ +

Set node version

+ +

The package.json contains everything needed to work out your application dependencies and what file should be launched to start your site. Cloud Foundry and PWS detects the presence of this file, and will use it to provision your app environment. The only useful information missing in our current package.json is the version of node. We can find the version of node we're using for development by entering the command:

+ +
node --version
+# will return version e.g. v6.10.3
+ +

Open package.json with a text editor, and add this information as an engines > node section as shown (using the version number retrieved above).

+ +
{
+  "name": "express-locallibrary-tutorial",
+  "version": "0.0.0",
+  "engines": {
+    "node": "6.10.3"
+  },
+  "private": true,
+  ...
+
+ +

Database configuration

+ +

So far in this tutorial we've used a single database that is hard coded into the app.js file. Normally we'd like to be able to have a different database for production and development, so next we'll modify the LocalLibrary website to get the database URI from the OS environment, and otherwise use our development database that we added manually earlier. Cloud Foundry has a very flexible services model that enables multiple services of the same type to exist in the environment. It stores all services related configurations in a single parseable JSON object called VCAP_SERVICES. A typical VCAP_SERVICES variable looks like this:

+ +
{
+ "VCAP_SERVICES": {
+  "mlab": [
+   {
+    "credentials": {
+     "uri": "mongodb://CloudFoundry_test_dev:somecr8zypassw0rd@dbhost.mlab.com:57971/CloudFoundry_dbname"
+    },
+    "label": "mlab",
+    "name": "node-express-tutorial-mongodb",
+    "plan": "sandbox",
+    "provider": null,
+    "syslog_drain_url": null,
+    "tags": [
+     "Cloud Databases",
+     "Developer Tools",
+     "Web-based",
+     "Data Store",
+    ],
+    "volume_mounts": []
+   }
+  ]
+ }
+}
+
+
+ +

Writing the code to extract and parse this environment variable is not hard, but it doesn't add a lot of value when others have written libraries to do this. In this case, there is a node.js package we can use called cfenv.

+ +

This will download the cfenv module and its dependencies, and modify the package.json file as required. Open app.js and find the block with all the 'requires' that load the modules into your application. In this example look for the line that looks something like this:

+ +
var expressValidator = require('express-validator');
+ +

If you cannot find that exact line, look for the blocks of 'requires' and look for the last one. Add the following text after it:

+ +
var cfenv = require('cfenv');
+ +
    +
  1. +

    To install the package, go to your terminal and make sure you are in the directory where the package.json file for LocalLibrary is. From the command line, type:

    + +
    npm install cfenv
    +
  2. +
  3. +

    Now that you have loaded the module, this next line will instantiate an object that will contain the app and services information required for deployment. Add the following after the line that contains app.use(helmet());

    + +
    // Set up CF environment variables
    +var appEnv = cfenv.getAppEnv();
    +
    + +

    When this line executes, all the Cloud Foundry application environment information will become available to the application in the appEnv object.

    +
  4. +
  5. +

    Now it is time to update the application to use the database configured by the platform. Find the line that sets the mongoDB connection variable. It will look something like this:

    + +
    var mongoDB = process.env.MONGODB_URI || dev_db_url;
    +
  6. +
  7. +

    You will now modify the line with the following code  appEnv.getServiceURL('node-express-tutorial-mongodb') to get the connection string from an environment variable that is being managed by the cfenv  module. If no service has been created and bound it will use your own database URL you created as part of the tutorial instead of the one from the environment. So replace the line above with the following:

    + +
    var mongoDB = appEnv.getServiceURL('node-express-tutorial-mongodb') || dev_db_url;
    +
    +
  8. +
  9. +

    Now run the site locally (see Testing the routes for the relevant commands) and check that the site still behaves as you expect. At this point your app is ready to use with Cloud Foundry and Pivotal Web Services.

    +
  10. +
+ +

Get a Pivotal Web Services account

+ +

To start using Pivotal Web Services you will first need to create an account (skip ahead to Create and upload the website if you've already got an account and have already installed the PWS cf CLI client).

+ + + +

Install the cf CLI client

+ +

The cf CLI client is a software tool for managing and deploying your application. Download and install the PWS cf CLI client by following the instructions on Pivotal Web Services or downloading directly from GIthub. Be sure to download the correct version for your computer. After the client is installed you will be able run commands, for example to get help on the client:

+ +
cf help
+
+ +

We'll now go through the steps to login to PWS using the CLI and deploy — or in Cloud Foundry parlance "push" your app.

+ +

Create and upload the website

+ +

To create the app we navigate to the directory where our modified files are. This is the same directory where the LocalLibrary package.json file is located. First, let's tell the cf CLI which Cloud Foundry instance you want to use. We need to do this, since the cf CLI tool can be used with any standard Cloud Foundry system, so this command indicates which specific Cloud Foundry you are using. Enter the following terminal command now:

+ +
cf api api.run.pivotal.io
+ +

Next login using the following command (enter your email and password when prompted):

+ +
cf login
+Email: enter your email
+Password: enter your password
+ +

We can now push our app to PWS. In the below example. replace 'some-unique-name' with something you can remember that is likely to be unique. If it isn't unique, the system will let you know. The reason this name has to be unique to the PWS system is it is the address we will use to to access your LocalLibrary application. I used mozilla-express-tutorial-xyzzy. You should use something else.

+ +
cf push some-unique-name -m 256MB
+ +

Note the -m flag we added is not required. We just included it so that we only use 256MB of memory to run the app. Node apps typically can run in 128 MB, but we are being safe. If we didn't specify the memory, the CLI would use the default 1 GB of memory, but we want to make sure your trial lasts longer. You should now see a bunch of text on the screen. It will indicate that the CLI is uploading all your files, that it's using the node buildpack, and it will start the app. If we're lucky, the app is now "running" on the site at the URL https://some-unique-name.cfapps.io. Open your browser and run the new website by going to that URL.

+ +
Note: The site will be running using our hardcoded development database at this time. Create some books and other objects, and check out whether the site is behaving as you expect. In the next section we'll set it to use our new database.
+ +

Setting configuration variables

+ +

You will recall from a preceding section that we need to set NODE_ENV to 'production' in order to improve our performance and generate less-verbose error messages.

+ +
    +
  1. +

    Do this by entering the following command:

    + +
    cf set-env some-unique-name NODE_ENV production
    +
    +
  2. +
  3. +

    We should also use a separate database for production. Cloud Foundry can take advantage of a marketplace to create a new service and automatically bind it to your app. Bind means place the service database credentials in the environment variable space of the container running your application for you to access. Enter the following commands:

    + +
    cf create-service mlab sandbox node-express-tutorial-mongodb
    +cf bind-service some-unique-name node-express-tutorial-mongodb
    +
    +
  4. +
  5. +

    You can inspect your configuration variables at any time using the cf env some-unique-name command — try this now:

    + +
    cf env some-unique-name
    +
    +
  6. +
  7. +

    In order for your applications to use the new credentials you will have to restage your application, meaning that it will restart and apply the new environment variables. This can be done using the following — enter this command now:

    + +
    cf restage some-unique-name
    +
    +
  8. +
  9. +

    If you check the home page now it should show zero values for your object counts, as the changes above mean that we're now using a new (empty) database.

    +
  10. +
+ +

Debugging

+ +

The PWS cf client provides a few tools for debugging:

+ +
>cf logs some-unique-name --recent  # Show current logs
+>cf logs some-unique-name # Show current logs and keep updating with any new results
+ +

Summary

+ +

If you followed the above steps, you should have now deployed the LocalLibrary app to PWS. Well done! If the deployment wasn't successful, double check all the steps.

diff --git a/files/zh-cn/learn/server-side/express_nodejs/introduction/index.html b/files/zh-cn/learn/server-side/express_nodejs/introduction/index.html new file mode 100644 index 0000000000..3f0372024e --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/introduction/index.html @@ -0,0 +1,525 @@ +--- +title: Express/Node 入门 +slug: learn/Server-side/Express_Nodejs/Introduction +tags: + - Express + - Node + - node.js + - 初学者 + - 学习 + - 服务器端 + - 脚本 +translation_of: Learn/Server-side/Express_Nodejs/Introduction +--- +
{{LearnSidebar}}
+ +
{{NextMenu("Learn/Server-side/Express_Nodejs/development_environment", "Learn/Server-side/Express_Nodejs")}}
+ +

本节将回答“什么是 Node”以及“什么是 Express”这两个问题,并通过主要特征和构成要件来简要介绍 Express 框架的与众不同之处。(只是目前尚不能用一个开发环境来测试它)

+ + + + + + + + + + + + +
预备知识:掌握计算机基础知识。了解 服务器端编程,特别是 客户-服务器交互机制
学习目标:熟悉 Express,以及它适配 Node 的方式、具体功能和构成要件。
+ +

什么是 Node?

+ +

Node(正式名称 Node.js)是一个开源的、跨平台的运行时环境,有了它,开发人员可以使用 JavaScript 创建各种服务器端工具和应用程序。此运行时主要用于浏览器上下文之外(即可以直接运行于计算机或服务器操作系统上)。据此,该环境省略了一些浏览器专用的 JavaScript API,同时添加了对更传统的 OS API(比如 HTTP 库和文件系统库)的支持。

+ +

从 web 服务器开发的角度来看,Node 有很多好处:

+ + + +

可以用 Node.js 的 HTTP 包来创建一个简单的 web 服务器。

+ +

Hello Node.js

+ +

以下示例将创建一个 web 服务器,它将监听对 URL http://127.0.0.1:8000/ 所有种类的 HTTP 请求,当接收到一个请求时,脚本将做出响应:返回一个字符串“Hello World”。如果已经安装了 Node,可以按照下面的步骤尝试一下:

+ +
    +
  1. 打开终端(Windows 中打开命令行工具)
  2. +
  3. 创建一个空文件夹用来存放项目,比如 "test-node",然后在终端输入以下命令进入这个文件夹:
  4. +
+ +
cd test-node
+ +
    +
  1. 用你最喜欢的文本编辑器创建一个名为 "hello.js" 的文件,把以下代码粘贴进来。
  2. +
+ +
// 调用 HTTP 模块
+const http = require("http");
+
+// 创建 HTTP 服务器并监听 8000 端口的所有请求
+http.createServer((request, response) => {
+
+   // 用 HTTP 状态码和内容类型来设定 HTTP 响应头
+   response.writeHead(200, {'Content-Type': 'text/plain'});
+
+   // 发送响应体 "Hello World"
+   response.end('Hello World\n');
+}).listen(8000);
+
+// 在控制台打印访问服务器的 URL
+console.log('服务器运行于 http://127.0.0.1:8000/');
+ +
    +
  1. 将其保存在刚才创建的文件夹。
  2. +
  3. 返回终端并输入以下命令:
  4. +
+ +
$ node "hello.js"
+ +

最后,在浏览器地址栏中输入 "http://localhost:8000" 并按回车,可以看到一个大面积空白的网页,左上角有 “Hello World" 字样。

+ +

Web 框架

+ +

Node 本身并不支持其它常见的 web 开发任务。如果需要进行一些具体的处理,比如运行其它 HTTP 动词(比如 GETPOSTDELETE 等)、分别处理不同 URL 路径的请求(“路由”)、托管静态文件,或用模板来动态创建响应,那么可能就要自己编写代码了,亦或使用 web 框架,以避免重新发明轮子。

+ +

什么是 Express?

+ +

Express 是最流行的 Node 框架,是许多其它流行 Node 框架 的底层库。它提供了以下机制:

+ + + +

虽然 Express 本身是极简风格的,但是开发人员通过创建各类兼容的中间件包解决了几乎所有的 web 开发问题。这些库可以实现 cookie、会话、用户登录、URL 参数、POST 数据、安全头等功能。可在 Express 中间件 网页中找到由 Express 团队维护的中间件软件包列表(还有一张流行的第三方软件包列表)。

+ +
+

注: 这种灵活性是一把双刃剑。虽然有一些中间件包可以解决几乎所有问题或需求,但是挑选合适的包有时也会成为一个挑战。这里构建应用没有“不二法门”,Internet 上许多示例也不是最优的,或者只展示了开发 web 应用所需工作的冰山一角。

+
+ +

Node 和 Express 从哪儿来?

+ +

Node 发布于 2009 年,最初版本仅支持 Linux。NPM 包管理器发布于 2010年,并于 2012 年支持 Windows。目前(2019 年 1 月)的 LTS 版本是 Node 10.15.0,最新版本是 Node 11.8.0。这只是沧海一粟,更多历史信息请到 维基百科 探究。

+ +

Express 发布于 2010 年 11 月,目前 API 的版本为 4.16.4。可以查看 修改记录 来查看当前版本的更新信息,或者访问 GitHub 页面来查看更详细的历史发布记录。

+ +

Node 和 Express 有多流行?

+ +

一个 web 框架是否流行是至关重要的,因为这预示着它是否会得到持续维护,是否会有更丰富的文档、插件库和技术支持。

+ +

服务器端框架的流行程度不容易量化(尽管有 Hot Frameworks 这样的网站试图通过计算 GitHub 项目和 StackOverflow 问题的数量等机制来评估框架的流行程度)。可以换个角度思考:Node 和 Express 是否“足够流行”、能够避免冷门平台带来的问题?它们还在持续更新吗?遇到问题时能得到帮助吗?学 Express 能挣钱吗?

+ +

基于使用 Express 的 知名企业 的数量、维护代码库的人数、以及提供免费或付费支持的人数来说,Express是一个流行的框架!

+ +

Express 是固执的吗?

+ +

Web 框架通常自称“固执的(opinionated)”或“包容的(unopinionated)”。

+ +

固执的框架认为应该有一套“标准答案”来解决各类具体任务。通常支持特定领域的快速开发(解决特定类型的问题)。因为标准答案通常易于理解且文档丰富。然而在解决主领域之外的问题时,就会显得不那么灵活,可用的组件和方法也更少。

+ +

相比之下,那些包容的框架,对于用于实现目标的组件组合的最佳方式限制要少得多,甚至不怎么限定组件的选择。这使开发人员更容易使用最合适的工具来完成特定的任务,但是要付出亲自寻找组件的成本。

+ +

Express 是高度包容的。几乎可以将任何兼容的中间件以任意顺序插入到请求处理链中,只要你喜欢。可以用单一文件或多个文件构造应用,怎样的目录结构都可以。有时候你自己都会觉得眼花缭乱!

+ +

Express 代码是什么样子的?

+ +

传统的数据驱动型网站中,web 应用是用于等待来自浏览器(或其它客户端)的 HTTP 请求的。当 web 应用收到一个请求时,会根据 URL 的模式,以及 POST 数据和 GET 数据可能包含的信息,来解析请求所需的功能。根据请求的内容,web 应用可能会从数据库读或写一些信息,等等操作来满足请求。随后,web 应用会返回给浏览器一个响应,通常是动态生成一页 HTML,在页面中用所取得的信息填充占位符。

+ +

使用 Express 可以调用特定 HTTP 动词(GET, POST, SET等)函数和 URL 模式(“路由”)函数,还可以指定模板(“视图”)引擎的种类、模板文件的位置以及渲染响应所使用的模板。可以使用 Express 中间件来添加对 cookie、会话、用户、获取 POST/GET 参数,等。可以使用Node 支持的任何类型数据库(Express 本身没有定义任何数据库行为)。

+ +

下文将介绍 Express 和 Node 的一些常见知识点。

+ +

Helloworld Express

+ +

首先来看 Express 的 Hello World 的示例(下文将逐行讨论)。

+ +
+

提示:如果你已经安装了 Node 和 Express(或者你已经按照 下一节 中的说明安装好了),可以将此代码保存为 app.js,并通过在 bash 中这样运行它:

+ +

node ./app.js

+
+ +
const express = require('express');
+const app = express();
+
+app.get('/', (req, res) => {
+  res.send('Hello World!');
+});
+
+app.listen(3000, () => {
+  console.log('示例应用正在监听 3000 端口!');
+});
+
+ +

前两行通过 require() 导入 Express 模块,并创建了一个 Express 应用。传统上把这个对象命名为 app,它可以进行路由 HTTP 请求、配置中间件、渲染 HTML 视图、注册模板引擎以及修改 应用程序设置 等操作,从而控制应用的行为(例如,环境模式,路由定义是否为区分大小写等)。

+ +

代码的中间部分(从 app.get() 开始共三行)是路由定义app.get() 方法指定了一个回调(callback)函数,该函数在每监听到一个关于站点根目录路径('/')的 HTTP GET 请求时调用。此回调函数以一个请求和一个响应对象作为参数,并直接调用响应的 send() 来返回字符串“Hello World!”

+ +

最后一个代码块在 “3000” 端口上启动服务器,并在控制台打印日志。服务器运行时,可用浏览器访问 localhost:3000,看看响应返回了什么。

+ +

导入和创建模块

+ +

模块是 JavaScript 库或文件,可以用 Node 的 require() 函数将它们导入其它代码。Express 本身就是一个模块,Express 应用中使用的中间件和数据库也是。

+ +

下面的代码以 Express 框架为例展示了如何通过名字来导入模块。首先,调用 require() 函数,用字符串('express')指定模块的名字,然后调用返回的对象来创建Express 应用 。然后就可以访问应用对象的属性和函数了。

+ +
const express = require('express');
+const app = express();
+
+ +

还可以创建自定义模块,并用相同的方法导入。

+ +
+

提示:你一定会有自建模块的需求,因为这可以让代码管理更有序。单文件应用是很难理解和维护的。使用模块还有助于管理名字空间,因为在使用模块时只会导入模块中显式导出的变量。

+
+ +

为了让对象暴露于模块之外,只需把它们设置为 exports 对象的附加属性即可。例如,下面的 square.js 模块就是一个导出了 area()perimeter() 方法的文件:

+ +
exports.area = width => { return width * width; };
+exports.perimeter = width => { return 4 * width; };
+
+ +

可以用 require() 导入这个模块,然后调用导出的方法,用法如下:

+ +
const square = require('./square');
+// 这里 require() 了文件名,省略了 .js 扩展名(可选)
+console.log('边长为 4 的正方形面积为 ' + square.area(4));
+ +
+

注:为模块指定绝对路径(或模块的名字,见最初的示例)也是可行的。

+
+ +

一次赋值不仅能构建一个单一的属性,还能构建一个完整的对象,可以像下面这样把对象赋值给 module.exports(也可以让 exports 对象直接作为一个构造器或另一个函数):

+ +
module.exports = {
+  area: width => { return width * width; },
+  perimeter: width => { return 4 * width; }
+};
+ +
+

注:在一个既定的模块内,可以把 exports 想象成 module.exports快捷方式exports 本质上就是在模块初始化前为 module.exports 的值进行初始化的一个变量。这个值是对一个对象(这里是空对象)的引用。这意味着 exportsmodule.exports 引用了同一个对象,也意味着如果为 exports 赋其它值不会影响到 module.exports

+
+ +

更多信息请参阅 模块(Node API 文档)。

+ +

使用异步 APIs

+ +

JavaScript 代码在完成那些需要一段时间才能完成的操作时,经常会用异步 API 来取代同步 API 。同步 API 下,每个操作完成后才可以进行下一个操作。例如,下列日志函数是同步的,将按顺序将文本打印到控制台(第一、第二)。

+ +
console.log('第一');
+console.log('第二');
+
+ +

而异步 API 下,一个操作开始后(在其完成之前)会立即返回。一旦操作完成,API 将使用某种机制来执行附加操作。例如,下面的代码将打印“第二、第一”。这是因为虽然先调用了 setTimeout() 方法并立即返回,但它的操作到 3 秒后才完成。

+ +
setTimeout(() => {
+  console.log('第一');
+}, 3000);
+console.log('第二');
+ +

在 Node 中使用无阻塞异步 API 甚至比在浏览器中更为重要,这是因为 Node 是一个单线程事件驱动的执行环境。“单线程”意味着对服务器的所有请求运行在同一个线程上,而不是分布在不同的进程上。这个模式在速度和管理服务器资源方面效率很高,但也意味着如果以同步方式调用的函数占用了很长时间,不仅会阻塞当前请求,还会阻塞当前 web 应用其它所有请求。

+ +

有多种方法可以让一个异步 API 通知当前应用它已执行完毕。最常用的是在调用异步 API 时注册一个回调函数,在 API 操作结束后将“回调”之。这也是上面的代码所使用的方法。

+ +
+

提示:如果有一系列独立的异步操作必须按顺序执行,那么使用回调可能会非常“混乱”,因为这会导致多级嵌套回调。人们通常把这个问题叫做“回调地狱”。缓解这个问题有以下办法:良好的编码实践(参考 http://callbackhell.com/)、使用 async 等模块、迁移至 ES6 并使用 Promise 等特性。

+
+ +
+

注:Node 和 Express 有一个一般性约定,即:使用“错误优先”回调。这个约定要求回调函数的第一个参数是错误值,而后续的参数包含成功数据。以下博文很好的解释了这个方法的有效性:以 Node.js 之名:理解错误优先回调(fredkschott.com 英文文章)

+
+ +

创建路由处理器(Route handler)

+ +

上文的 Hello World 示例中定义了一个(回调)路由处理函数来处理对站点根目录('/')的 HTTP GET 请求。

+ +
app.get('/', (req, res) => {
+  res.send('Hello World!');
+});
+
+ +

回调函数将请求和响应对象作为参数。该函数直接调用响应的 send() 以返回字符串“Hello World!”。有 许多其它响应方法 可以结束请求/响应周期,例如,通过调用 res.json() 来发送JSON 响应、调用 res.sendFile() 来发送文件。

+ +
+

JavaScript 提示:虽然回调函数的参数命名没有限制,但是当调用回调时,第一个参数将始终是请求,第二个参数将始终是响应。合理的命名它们,在回调体中使用的对象将更容易识别。

+
+ +

Express 应用 对象还提供了为其它所有 HTTP 动词定义路由处理器的方法,大多数处理器的使用方式完全一致:

+ +

checkout(), copy(), delete(), get(), head(), lock(), merge(), mkactivity(), mkcol(), move(), m-search(), notify(), options(), patch(), post(), purge(), put(), report(), search(), subscribe(), trace(), unlock(), unsubscribe().

+ +

有一个特殊的路由方法 app.all(),它可以在响应任意 HTTP 方法时调用。用于在特定路径上为所有请求方法加载中间件函数。以下示例(来自 Express 文档)中的处理程序将在监听到针对 /secret 的任意 HTTP 动词(只要 HTTP 模块 支持)的请求后执行。

+ +
app.all('/secret', (req, res, next) => {
+  console.log('访问私有文件 ...');
+  next(); // 控制权传递给下一个处理器
+});
+
+ +

路由器可以匹配 URL 中特定的字符串模式,并从 URL 中提取一些值作为参数传递给路由处理程序(作为请求对象的属性)。

+ +

可以为站点的特定部分提供一组路由处理器(使用公共路由前缀进行组合)。(比如对于一个有 维基(Wiki)内容的站点,可以把所有 Wiki 相关的路由放在同一个文件里,使用路由前缀 '/wiki/' 访问它们)。在 Express 中可以使用 express.Router 对象实现。例如,可以把所有维基相关的路由都放在一个 wiki.js 模块中,然后导出 Router 对象,如下:

+ +
// wiki.js - 维基路由模块
+
+const express = require('express');
+const router = express.Router();
+
+// 首页路由
+router.get('/', (req, res) => {
+  res.send('维基首页');
+});
+
+// “关于”页面路由
+router.get('/about', (req, res) => {
+  res.send('关于此维基');
+});
+
+module.exports = router;
+
+ +
+

注意:Router 对象添加路由就像向之前为 app 对象添加路由一样。

+
+ +

首先 require() 路由模块(wiki.js),然后在 Express 应用中调用 use()Router 添加到中间件处理路径中,就可以在主应用中使用这个模块中的路由处理器了。路由路径有两条:/wiki/wiki/about/

+ +
const wiki = require('./wiki.js');
+// ...
+app.use('/wiki', wiki);
+
+ +

今后将介绍更多关于路由的信息,特别是关于 Router 的用法,请参见 路由和控制器 一节。

+ +

使用中间件(Middleware)

+ +

中间件在 Express 应用中得到了广泛使用,从提供错误处理静态文件、到压缩 HTTP 响应等等。路由函数可以通过向 HTTP 客户端返回一些响应来结束 HTTP “请求 - 响应”周期,而中间件函数通常是对请求或响应执行某些操作,然后调用“栈”里的下一个函数,可能是其它中间件或路由处理器。中间件的调用顺序由应用开发者决定。

+ +
+

注:中间件可以执行任何操作,运行任何代码,更改请求和响应对象,也可以结束“请求 - 响应”周期。如果它没有结束循环,则必须调用 next() 将控制传递给下一个中间件函数(否则请求将成为悬挂请求)。

+
+ +

大多数应用会使用第三方中间件来简化常见的 web 开发任务,比如 cookie、会话、用户身份验证、访问请求 POST 和 JSON 数据,日志记录等。参见 Express 团队维护的中间件包列表(包含受欢迎的第三方包)。NPM 有提供其它 Express 包。

+ +

要使用第三方中间件,首先需要使用 NPM 将其安装到当前应用中。比如,要安装 morgan HTTP 请求记录器中间件,可以这样做:

+ +
$ npm install morgan
+
+ +

然后,您可以对 Express 应用对象调用 use() 将该中间件添加到栈:

+ +
const express = require('express');
+const logger = require('morgan');
+const app = express();
+app.use(logger('dev'));
+...
+ +
+

注意:中间件和路由函数是按声明顺序调用的。一些中间件的引入顺序很重要(例如,如果会话中间件依赖于 cookie 中间件,则必须先添加 cookie 处理器)。绝大多数情况下要先调用中间件后设置路由,否则路由处理器将无法访问中间件的功能。

+
+ +

可以自己编写中间件函数,这是基本技能(仅仅为了创建错误处理代码也需要)。中间件函数和路由处理回调之间的唯一区别是:中间件函数有第三个参数 next,在中间件不会结束请求周期时应调用这个 next(它包含中间件函数调用后应调用的下一个函数)。

+ +

可以使用 app.use()app.add() 将一个中间件函数添加至处理链中,这取决于中间件是应用于所有响应的,还是应用于特定 HTTP 动词(GETPOST等)响应的。可以为两种情况指定相同的路由,但在调用 app.use() 时路由可以省略。

+ +

下面的示例显示了如何使用这两种方法添加中间件功能,以及是否使用路由。

+ +
const express = require('express');
+const app = express();
+
+// 示例中间件函数
+const a_middleware_function = (req, res, next) => {
+  // ... 进行一些操作
+  next(); // 调用 next() ,Express 将调用处理链中下一个中间件函数。
+};
+
+// 用 use() 为所有的路由和动词添加该函数
+app.use(a_middleware_function);
+
+// 用 use() 为一个特定的路由添加该函数
+app.use('/someroute', a_middleware_function);
+
+// 为一个特定的 HTTP 动词和路由添加该函数
+app.get('/', a_middleware_function);
+
+app.listen(3000);
+ +
+

JavaScript提示:上面代码中单独声明了中间件函数,并把它设置为回调。之前是把路由处理函数在使用时声明为回调。在 JavaScript 中,两种方法都可行。

+
+ +

请参阅 Express 文档中关于 使用开发 Express 中间件的内容。

+ +

托管静态文件

+ +

可以使用 express.static 中间件来托管静态文件,包括图片、CSS 以及 JavaScript 文件(其实 static() 是 Express 提供的原生中间件函数之一)。例如,可以通过下面一行来托管 'public' 文件夹(应位于 Node 调用的同一级)中的文件:

+ +
app.use(express.static('public'));
+
+ +

现在 'public' 文件夹下的所有文件均可通过在根 URL 后直接添加文件名来访问了,比如:

+ +
http://localhost:3000/images/dog.jpg
+http://localhost:3000/css/style.css
+http://localhost:3000/js/app.js
+http://localhost:3000/about.html
+
+ +

可以通过多次调用 static() 来托管多个文件夹。如果一个中间件函数找不到某个文件,将直接传递给下一个中间件(中间件的调用顺序取决于声明顺序)。

+ +
app.use(express.static('public'));
+app.use(express.static('media'));
+
+ +

还可以为静态 URL 创建一个虚拟的前缀,而不是直接把文件添加到根 URL 里。比如,这里 指定了一个装载路径,于是这些文件将通过 '/media' 前缀调用:

+ +
app.use('/media', express.static('public'));
+
+ +

现在可以通过 '/media' 路径前缀来访问 'public' 文件夹中的文件。

+ +
http://localhost:3000/media/images/dog.jpg
+http://localhost:3000/media/video/cat.mp4
+http://localhost:3000/media/cry.mp3
+
+ +

更多信息请参阅 Express 文档 托管静态文件

+ +

错误处理

+ +

用来处理错误的特殊中间件函数有四个参数(err, req, res, next),而不是之前的三个。例如:

+ +
app.use((err, req, res, next) => {
+  console.error(err.stack);
+  res.status(500).send('出错了!');
+});
+
+ +

错误处理中间件可以任何所需内容,但是必须在所有其它 app.use() 和路由调用后才能调用,因此它们是需求处理过程中最后的中间件。

+ +

Express 内建了错误处理机制,可以协助处理 app 中没有被处理的错误。默认的错误处理中间件函数在中间件函数栈的末尾。如果一个错误传递给 next() 而没有用错误处理器来处理它,内建处理机制将启动,栈跟踪的错误将回写给客户端。

+ +
+

注: 生产环境中不保留栈跟踪轨迹。可将环境变量 NODE_ENV 设置为 'production' 来运行所需的生产环境。

+
+ +
+

注:HTTP404 和其它“错误”状态码不作为错误处理。可使用中间件来自行处理这些状态。更多信息请参阅 Express 文档 FAQ

+
+ +

更多信息请参阅 Express 文档 错误处理

+ +

使用数据库

+ +

Express 应用可以使用 Node 支持的所有数据库(Express 本身并没有定义任何数据库管理的附加行为或需求)。其中包括:PostgreSQL、MySQL、Redis、SQLite、MongoDB,等等。

+ +

使用数据库前先要用 NPM 来安装驱动程序。比如,要安装流行的 NoSQL 数据库 MongoDB 的驱动程序,可运行以下命令:

+ +
$ npm install mongodb
+
+ +

数据库可以安装在本地或云端。在 Express 代码中 require() 驱动程序,连接,然后就可以执行增加、读取、更新、删除四种操作(CRUD)。以下示例展示了如何查找 MongoDB 表中 '哺乳动物' 的记录:

+ +
// MongoDB 3.0 以上版本适用,老版本不适用。
+const MongoClient = require('mongodb').MongoClient;
+
+MongoClient.connect('mongodb://localhost:27017/animals', (err, client) => {
+  if(err) {
+    throw err;
+  }
+
+  let db = client.db('动物');
+  db.collection('哺乳动物').find().toArray((err, result) => {
+    if(err) throw err;
+    console.log(result);
+    client.close();
+  });
+});
+ +

还有一种通过“对象关系映射(Object Relational Mapper,简称 ORM)”间接访问数据库的方法。可以把数据定义为“对象”或“模型”,然后由 ORM 根据给定的数据库格式搞定所有映射关系。这种方法对于开发者有一个好处:可以用 JavaScript 对象的思维而无需直接使用数据库语法,同时传进的数据也有现成的检查工具。稍后详细讨论数据库问题。.

+ +

更多信息请参阅 Express 文档 数据库集成

+ +

渲染数据(视图,view)

+ +

模板引擎可为输出文档的结构指定一个模板,在数据处先放置占位符,并于页面生成时填充。模板通常用于生成 HTML,也可以生成其它类型的文档。Express 支持 多个版本的模板引擎,可以参阅:JavaScript 模板引擎对比评测:Jade、Mustache、Dust与其它

+ +

在应用设置代码中声明了模板引擎的名称和位置后,Express 可以使用 'views''view engines' 设置来寻找模板,如下所示(必须事先安装包含模板库的包!):

+ +
const express = require('express');
+const app = express();
+
+// 设置包含模板的文件夹('views')
+app.set('views', path.join(__dirname, 'views'));
+
+// 设置视图引擎,比如'some_template_engine_name'
+app.set('view engine', 'some_template_engine_name');
+
+ +

模板的外观取决于所使用的引擎。假设一个模板文件名为 "index.<template_extension>",其中包括数据变量 'title''message' 的两个占位符,可以在路由处理器函数中调用 Response.render() 来创建并发送 HTML 响应:

+ +
app.get('/', (req, res) => {
+  res.render('index', { title: '关于狗狗', message: '狗狗很牛!' });
+});
+ +

更多信息请参见 Express 文档 使用模板引擎

+ +

文件结构

+ +

Express 不对文件结构和组件的选用做任何约定。路由、视图、静态文件,以及其它应用具体逻辑均可按任意文件结构保存在任意数量的文件中。当然可以让整个 Express 应用保存在单一文件中,但是一般情况下,把应用按功能(比如账户管理、博客、论坛)和架构问题域(比如 MVC 架构 中的模型、视图、控制器)进行拆分是有意义的。

+ +

后文将使用 Express 应用生成器 来创建一个模块化的应用框架,从而可以更方便的扩展出新的 web 应用。

+ + + +

小结

+ +

恭喜,你迈出了 Express/Node 旅程的第一步 !你现在已经了解了 Express 与 Node 的主要优势,并大致了解了 Express 应用的结构 (路由处理器、中间件、错误处理和模板代码)。你还了解到 Express 作为一个高度包容的框架,让你在组织应用结构和库时更自由,更开放!

+ +

诚然,Express 是一个非常轻量的 web 应用框架,这是有意为之的,它巨大的裨益和无尽的潜能都来自第三方的库和功能。今后的章节会详细讨论。下一节会讲如何设置 Node 开发环境,之后就能开始 Express 的实战了。

+ +

另请参阅

+ + + +
{{NextMenu("Learn/Server-side/Express_Nodejs/development_environment", "Learn/Server-side/Express_Nodejs")}}
+ +

本章目录

+ +
+ +
diff --git a/files/zh-cn/learn/server-side/express_nodejs/mongoose/index.html b/files/zh-cn/learn/server-side/express_nodejs/mongoose/index.html new file mode 100644 index 0000000000..6233ef5edb --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/mongoose/index.html @@ -0,0 +1,831 @@ +--- +title: Express 教程 3:使用数据库 (Mongoose) +slug: learn/Server-side/Express_Nodejs/mongoose +tags: + - Express + - MongoDB + - Node + - ODM + - mongoose + - nodejs + - 初学者 + - 学习 + - 数据库 + - 服务器端 +translation_of: Learn/Server-side/Express_Nodejs/mongoose +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/skeleton_website", "Learn/Server-side/Express_Nodejs/routes", "Learn/Server-side/Express_Nodejs")}}
+ +

本文简要介绍了数据库以及 Node/Express 应用的数据库集成。然后演示了 MongooseLocalLibrary 提供数据库访问的方式。还讲解了对象模式(Schema)和模型(Model)的声明方式、主要域的类型、基础验证机制。同时还简短演示了访问模型数据的一些方法。

+ + + + + + + + + + + + +
预备知识:Express 教程 2: 创建站点骨架,了解数据库基础知识。
目标:使用 Mongoose 设计建造模型。
+ +

概览

+ +

图书馆的员工会使用 LocalLibrary 网站来保存藏书和借阅者的信息。会员会浏览和查找所需藏书,找到后预约或借阅。为了更高效地存取信息,网站将使用数据库。

+ +

Express 应用支持多款数据库,执行新建(Create)、读取(Read)、更新(Update)和删除(Delete)操作 (CRUD) 操作也有诸多途径。本教程先做一个不完全简介,然后对教程选用的机制进行详细介绍。

+ +

我可以使用什么数据库?

+ +

Express 应用可以使用 Node 支持的所有数据库(Express 本身不支持数据库管理的任何具体行为/需求)。有许多 流行的选择,包括 PostgreSQL、MySQL、Redis、SQLite 和 MongoDB。

+ +

选用数据库应考虑以下因素:进入生产状态用时/学习曲线、性能、复制/备份的易用度、成本、社区支持,等等。这些数据库各有千秋,但绝大多数都足以胜任 LocalLibrary 这样中小规模的网站了。

+ +

更多信息请参阅:数据库集成(Express 文档)。

+ +

与数据库交互的最佳方式是什么?

+ +

与数据库交互有两种方法:

+ + + +

使用 SQL 或其它受到支持的查询语言才能达到最佳性能。ODM 通常慢一些,因为在对象和数据库格式之间存在一层用于映射的翻译代码,使它不一定会选用最高性能的数据库查询(尤其是普遍使用级别的 ODM,它必须在各类数据库功能方面做出更大的折衷)。

+ +

使用 ORM 的好处是:程序员可以继续用 JavaScript 对象的思维而不用转向数据库语义的思维。 在(同一个或不同网站)使用不同数据库时尤为明显。使用 ORM 还可以更方便地对数据进行验证和检查。

+ +
+

提示:使用 ODM / ORM 通常可以降低开发和维护成本!除非你非常熟悉本地查询语言,或者项目对性能要求很高,否则强烈推荐使用 ODM。

+
+ +

我应该使用哪种 ORM/ODM ?

+ +

NPM 站点上有许多 ODM / ORM 解决方案(另请参阅 NPM 站点上的 odmorm 标签列表)。

+ +

以下是迄今(2018 年 12 月)几种流行的解决方案:

+ + + +

一般来说,选择解决方案应该考虑功能和“社区活跃度”(下载量、贡献数、bug 报告、文档质量,等)。在撰写本文时,Mongoose 是最受欢迎的 ODM,选用 MongoDB 数据库时,它是一个合理的选择。

+ +

在 LocalLibrary 中使用 Mongoose 和 MongoDb

+ +

我们将在本地图书馆示例(以及本主题的其余部分)中使用 Mongoose ODM 来访问图书馆数据。Mongoose 作为 MongoDB(面向文档数据模型的开源 NoSQL 数据库)的前端。MongoDB 数据库里,“集合”中的“文档” 类似于 关系数据库里“表”中的“行”。

+ +

这种 ODM 和数据库的结合方式在 Node 社区中非常流行,一定程度上是因为文档存储和查询系统与 JSON 十分相似,因此 JavaScript 开发人员会非常熟悉。

+ +
+

提示:使用 Mongoose 无需事先了解 MongoDB,但是部分 Mongoose文档 对于熟悉 MongoDB 的朋友会更易于使用和理解。

+
+ +

下面将介绍如何为 LocalLibrary 网站 定义和访问 Mongoose 模式和模型。

+ +

设计 LocalLibrary 模型

+ +

在开始投入模型编写之前,有必要先思考一下:本网站需要存储什么数据?不同对象之间的关系是怎样的?

+ +

图书馆需要存储藏书信息(书名、摘要、作者、种类、ISBN),藏书副本信息(全站唯一 ID,借出状态,等)。还可能需要存储作者姓名之外的更多信息,以及多个作者的信息。还希望数据库内容能够根据书名、作者姓名、种类和编目进行排序。

+ +

有必要为每个“对象”(一组相关信息)设计独立的模型。本示例的关键对象包括书籍、书籍副本和作者。

+ +

也许还希望使用模型而不是站点代码来表示选项表(比如下拉列表),在选项无法预知或可能更改时更推荐模型方式。很明显,藏书类型(比如科幻小说、法语诗歌,等)就是这种情况。

+ +

确定模型和字段后还要考虑它们之间的关系,以下 UML 图显示了本示例即将定义的模型(框图)。如上所述,我们为藏书(一般细节)、藏书副本(系统)和作者创建了模型。还有一个可以动态选择的书籍种类模型。对于 BookInstance:status,我们不会为它建立模型,而是将可能的值直接编入站点代码中,因为我们不希望这些值发生变化。下图每个框都包括模型名、字段名和类型,还有方法及其返回类型。

+ +

下图还展示了模型之间的关系以及重复度(Multiplicity)。重复度就是图中两框间连线两端的数字,表示两个模型之间存在的关系的数量(最大值和最小值)。例如,Book 框和 Genre 框之间有连线说明二者之间存在关系,Book 模型端的数字(0..*)表示一个种类必包括零种或多种藏书(多少都可以),而 Genre 端的数字表示一种藏书可以有零个或多个种类。

+ +
+

注:正如下文 Mongoose 入门 中所讲,通常应该把定义文档/模型关系的字段置于同一模型中(仍可通过在搜索相关 _id 来回寻模型间的关系)。以下的 Book 模式中定义了 Book/Genre 和 Book/Author 关系,BookInstance 模式中定义了 Book/BookInstance 关系。这样做是简便起见,但稍存歧义,让这些字段存在于其他模式中也是可以的。

+
+ +

Mongoose Library Model  with correct cardinality

+ +
+

注:下面是一段入门知识,讲解如何定义和使用模型。请在阅读时想想将如何构建上图中的模型。

+
+ +

Mongoose 入门

+ +

这一段将简要介绍如何将 Mongoose 连接到 MongoDB 数据库,如何定义模式和模型,以及如何进行基本查询。

+ +
+

注:本入门受到 npm 上的 Mongoose 快速入门Mongoose 官方文档 的“深度影响”。

+
+ +

安装 Mongoose 和 MongoDB

+ +

Mongoose 像任何其他依赖项一样,使用 NPM 将其安装在您的项目(package.json)中 。请在项目文件夹中运行下面的命令以完成安装:

+ +
$ npm install mongoose
+
+ +

安装 Mongoose 会添加所有依赖项,包括 MongoDB 数据库驱动程序,但不会安装 MongoDB 本身。要安装 MongoDB 服务器,可以 点击下载 各操作系统的安装程序在本地安装。也可以使用云端 MongoDB 实例。

+ +
+

注:本教程选用 mLab 提供的 沙箱级 云端“数据库即服务”(Database as a Service,DBaaS)。它适用于开发环境,且部署过程与操作系统无关(DBaaS 也适用于生产环境)。

+
+ +

连接到 MongoDB

+ +

Mongoose 需要连接到 MongoDB 数据库。可以 require() 之,并通过 mongoose.connect() 连接到本地数据库,如下。

+ +
// 导入 mongoose 模块
+const mongoose = require('mongoose');
+
+// 设置默认 mongoose 连接
+const mongoDB = 'mongodb://127.0.0.1/my_database';
+mongoose.connect(mongoDB);
+// 让 mongoose 使用全局 Promise 库
+mongoose.Promise = global.Promise;
+// 取得默认连接
+const db = mongoose.connection;
+
+// 将连接与错误事件绑定(以获得连接错误的提示)
+db.on('error', console.error.bind(console, 'MongoDB 连接错误:'));
+ +

可以用 mongoose.connection 取得默认的 Connection 对象。一旦连接,Connection 实例将触发打开事件。

+ +
+

提示:可以使用 mongoose.createConnection() 创建其它连接。该函数与 connect() 的参数(数据库 URI,包括主机地址、数据库名、端口、选项等)一致,并返回一个 Connection 对象。

+
+ +

定义和添加模型

+ +

模型使用 Schema 接口进行定义。 Schema 可以定义每个文档中存储的字段,及字段的验证要求和默认值。还可以通过定义静态和实例辅助方法来更轻松地处理各种类型的数据,还可以像使用普通字段一样使用数据库中并不存在的虚拟属性(稍后讨论)。

+ +

mongoose.model() 方法将模式“编译”为模型。模型就可以用来查找、创建、更新和删除特定类型的对象。

+ +
+

注:MongoDB 数据库中,每个模型都映射至一组文档。这些文档包含 Schema 模型定义的字段名/模式类型。

+
+ +

定义模式

+ +

下面的代码片段中定义了一个简单的模式。首先 require() mongoose,然后使用 Schema 构造器创建一个新的模式实例,使用构造器的对象参数定义各个字段。

+ +
// 获取 Mongoose
+const mongoose = require('mongoose');
+
+// 定义一个模式
+var Schema = mongoose.Schema;
+
+var SomeModelSchema = new Schema({
+    a_string: String,
+    a_date: Date
+});
+
+ +

上面示例只有两个字段(一个字符串和一个日期),接下来将展示其它字段类型、验证和其它方法。

+ +

创建模型

+ +

使用 mongoose.model() 方法从模式创建模型:

+ +
// 定义模式
+const Schema = mongoose.Schema;
+
+const SomeModelSchema = new Schema({
+    a_string: String,
+    a_date: Date
+});
+
+// 使用模式“编译”模型
+const SomeModel = mongoose.model('SomeModel', SomeModelSchema);
+ +

第一个参数是为模型所创建集合的别名(Mongoose 将为 SomeModel 模型创建数据库集合),第二个参数是创建模型时使用的模式。

+ +
+

注:定义模型类后,可以使用它们来创建、更新或删除记录,以及通过查询来获取所有记录或特定子集。我们将在以下“使用模型”部分展示,包括创建视图的情况。

+
+ +

模式类型(字段)

+ +

模式可以包含任意数量的字段,每个字段代表 MongoDB 文档中的一段存储区域。下面是一个模式的示例,其中有许多常见字段类型和声明方式:

+ +
const schema = new Schema(
+{
+  name: String,
+  binary: Buffer,
+  living: Boolean,
+  updated: { type: Date, default: Date.now },
+  age: { type: Number, min: 18, max: 65, required: true },
+  mixed: Schema.Types.Mixed,
+  _someId: Schema.Types.ObjectId,
+  array: [],
+  ofString: [String], // 其他类型也可使用数组
+  nested: { stuff: { type: String, lowercase: true, trim: true } }
+})
+ +

大多数 模式类型SchemaType,字段名之后的描述符)都是自解释的。除了:

+ + + +

代码还展示了声明字段的两种方法:

+ + + +

关于选项的更多信息请参阅 模式类型(Mongoose 英文文档)。

+ +

验证

+ +

Mongoose 提供内置的和自定义的验证器,以及同步的和异步的验证器。你可以在所有情况下,指定可接受的范围或值,以及验证失败的错误消息。

+ +

内置的验证器包括:

+ + + +

以下是类型验证器和错误消息的设定方法(从 Mongoose 文档稍作修改而来):

+ +
const breakfastSchema = new Schema({
+  eggs: {
+    type: Number,
+    min: [6, '鸡蛋太少'],
+    max: 12
+  },
+  drink: {
+    type: String,
+    enum: ['咖啡', '茶']
+  }
+});
+ +

字段验证的完整信息请参阅 验证(Mongoose 英文文档)。

+ +

虚拟属性

+ +

虚拟属性是可以获取和设置、但不会保存到 MongoDB 的文档属性。getter 可用于格式化或组合字段,而 setter 可用于将单个值分解为多个值从而便于存储。文档中的示例,从名字和姓氏字段构造(并解构)一个全名虚拟属性,这比每次在模板中使用全名更简单,更清晰。

+ +
+

注:我们将使用库中的一个虚拟属性,用路径和记录的 _id 来为每个模型记录定义唯一的 URL。

+
+ +

更多信息请参阅 虚拟属性(Mongoose 英文文档)。

+ +

方法和查询助手

+ +

模式支持 实例方法静态方法查询助手。实例方法和静态方法外表很相似,但有本质区别,实例方法针对特定记录,且可以访问当前对象。查询助手可用于扩展 Mongoose 的 链式查询 API(例如,在 find()findOne()findById() 方法外还可以添加一个 “byName” 查询)。

+ +

使用模型

+ +

就可以使用创建好的模式来创建模型。模型即数据库中可以搜索的一类文档,模型的实例即可以存取的单个文档。

+ +

以下是简介。更多信息请参阅:模型(Mongoose 英文文档)。

+ +

创建和修改文档

+ +

可以通过定义模型的实例并调用 save() 来创建记录。以下示例假定 SomeModel 是用现有模式创建的模型(只有一个字段 "name" ):

+ +
// 创建一个 SomeModel 模型的实例
+const awesome_instance = new SomeModel({ name: '牛人' });
+
+// 传递回调以保存这个新建的模型实例
+awesome_instance.save( function (err) {
+  if (err) {
+    return handleError(err);
+  } // 已保存
+});
+
+ +

记录的创建(以及更新、删除和查询)操作是异步的,可以提供一个回调函数在操作完成时调用。由于 API 遵循错误参数优先的惯例,因此回调的第一个参数必为错误值(或 null)。如果 API 需要返回一些结果,则将结果作为第二个参数。

+ +

还可以使用 create(),在定义模型实例的同时将其保存。回调的第一个参数返回错误,第二个参数返回新建的模型实例。

+ +
SomeModel.create(
+  { name: '也是牛人' },
+  function(err, awesome_instance) {
+    if (err) {
+      return handleError(err);
+    } // 已保存
+  }
+);
+ +

每个模型都有一个相关的连接(使用 mongoose.model() 时将做为默认连接)。可以通过创建新连接并对其调用 .model(),从而在另一个数据库上创建文档。

+ +

可以使用“圆点”加字段名来访问、修改新记录中的字段。操作后必须调用 save()update() 以将改动保存回数据库。

+ +
// 使用圆点来访问模型的字段值
+console.log(awesome_instance.name); // 控制台将显示 '也是牛人'
+
+// 修改字段内容并调用 save() 以修改记录
+awesome_instance.name = "酷毙了的牛人";
+awesome_instance.save( function(err) {
+   if (err) {
+     return handleError(err);
+   } // 已保存
+});
+
+ +

搜索纪录

+ +

可以使用查询方法搜索记录,查询条件可列在 JSON 文档中。以下代码展示了如何在数据库中找到所有网球运动员,并返回运动员姓名和年龄字段。这里只指定了一个匹配字段(运动项目,sport),也可以添加更多条件,指定正则表达式,或去除所有条件以返回所有运动员。

+ +
const Athlete = mongoose.model('Athlete', yourSchema);
+
+// SELECT name, age FROM Athlete WHERE sport='Tennis'
+Athlete.find(
+  { 'sport': 'Tennis' },
+  'name age',
+  function (err, athletes) {
+    if (err) {
+      return handleError(err);
+    } // 'athletes' 中保存一个符合条件的运动员的列表
+  }
+);
+ +

若像上述代码那样指定回调,则查询将立即执行。搜索完成后将调用回调。

+ +
+

注:Mongoose 中所有回调都使用 callback(error, result) 模式。如果查询时发生错误,则参数 error 将包含错误文档,resultnull。如果查询成功,则 errornull,查询结果将填充至 result

+
+ +

若未指定回调,则 API 将返回 Query 类型的变量。可以使用该查询对象来构建查询,随后使用 exec() 方法执行(使用回调)。

+ +
// 寻找所有网球运动员
+const query = Athlete.find({ 'sport': 'Tennis' });
+
+// 查找 name, age 两个字段
+query.select('name age');
+
+// 只查找前 5 条记录
+query.limit(5);
+
+// 按年龄排序
+query.sort({ age: -1 });
+
+// 以后某个时间运行该查询
+query.exec(function (err, athletes) {
+  if (err) {
+    return handleError(err);
+  } // athletes 中保存网球运动员列表,按年龄排序,共 5 条记录
+})
+ +

上面的查询条件定义在 find() 方法中。也可以使用 where() 函数来执行此操作,可以使用点运算符(.)将所有查询链接在一起。以下代码与上述的查询基本相同,还添加了年龄范围的附加条件。

+ +
Athlete.
+  find().
+  where('sport').equals('Tennis').
+  where('age').gt(17).lt(50).  // 附加 WHERE 查询
+  limit(5).
+  sort({ age: -1 }).
+  select('name age').
+  exec(callback); // 回调函数的名字是 callback
+ +

find() 方法会取得所有匹配记录,但通常你只想取得一个。以下方法可以查询单个记录:

+ + + +
+

注:还有一个 count() 方法,可获取匹配条件的项目的个数。在只期望获得记录的个数而不想获取实际的记录时可以使用。

+
+ +

查询还能做更多。请参阅 查询(Mongoose 英文文档)。

+ +

文档间协同 —— population

+ +

可以使用 ObjectId 模式字段来创建两个文档/模型实例间一对一的引用,(一组 ObjectIds 可创建一对多的引用)。该字段存储相关模型的 id。如果需要相关文档的实际内容,可以在查询中使用 populate() 方法,将 id 替换为实际数据。

+ +

例如,以下模式定义了作者和简介。每个作者可以有多条简介,我们将其表示为一个 ObjectId 数组。每条简介只对应一个作者。“ref”(黑体字)告知模式分配哪个模型给该字段。

+ +
const mongoose = require('mongoose');
+const Schema   = mongoose.Schema;
+
+const authorSchema = Schema({
+  name    : String,
+  stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
+});
+
+const storySchema = Schema({
+  author : { type: Schema.Types.ObjectId, ref: 'Author' },
+  title  : String
+});
+
+const Story  = mongoose.model('Story', storySchema);
+const Author = mongoose.model('Author', authorSchema);
+ +

可以通过分配 _id 值来保存对相关文档的引用。下面我们创建一个作者、一条简介,并将新简介的 author 字段设置为新建作者的 id。

+ +
const wxm = new Author({ name: '司马迁' });
+
+wxm.save(function (err) {
+  if (err) {
+    return handleError(err);
+  }
+
+  // 现在库中有了作者司马迁,我们来新建一条简介
+  const story = new Story({
+    title: "司马迁是历史学家",
+    author: wxm._id    // author 设置为作者 司马迁 的 _id。ID 是自动创建的。
+  });
+
+  story.save(function (err) {
+    if (err) {
+      return handleError(err);
+    }  // 司马迁有了一条简介
+  });
+});
+ +

现在简介文档通过作者文档的 ID 引用了作者。可使用 populate() 在简介中获取作者信息,如下所示。

+ +
Story
+  .findOne({ title: '司马迁是历史学家' })
+  .populate('author') // 使用作者 id 填充实际作者信息
+  .exec(function (err, story) {
+    if (err) {
+      return handleError(err);
+    }
+    console.log('作者是 %s', story.author.name);
+    // 控制台将打印 "作者是 司马迁"
+  });
+ +
+

注:目光敏锐的读者可能会注意到,新的简介添加了作者,但并没有添加到 stories 数组中。那么怎样才能得到指定作者的所有简介?考虑把作者添加到 stories 数组中,但会导致作者和简介相关信息的要在两处进行维护。

+ +

更好的方法是获取作者的 _id,然后使用 find() 在所有简介的作者字段中搜索。

+ +
Story
+  .find({ author : wxm._id })
+  .exec(function (err, stories) {
+    if (err) {
+      return handleError(err);
+    } // 返回所有 author 字段的值为 司马迁id 的简介
+  });
+
+
+ +

以上是本教程中“项目间协同”需要了解的所有内容。更多详细信息请参阅 Population(Mongoose 英文文档)。

+ +

一模式(模型)一文件

+ +

虽然创建模式和模型没有文件结构的限制,但强烈建议将单一模式定义在单一模块(文件)中,并通过导出方法来创建模型。如下所示:

+ +
// 文件:./models/somemodel.js
+
+// Require Mongoose
+const mongoose = require('mongoose');
+
+// 定义一个模式
+const Schema = mongoose.Schema;
+
+const SomeModelSchema = new Schema({
+    a_string : String,
+    a_date   : Date
+});
+
+// 导出函数来创建 "SomeModel" 模型类
+module.exports = mongoose.model('SomeModel', SomeModelSchema );
+ +

然后就可以在其它文件中,require 并使用该模型。下面是通过 SomeModel 模块来获取所有实例的方法。

+ +
// 通过 require 模块来创建 SomeModel 模型
+const SomeModel = require('../models/somemodel')
+
+// 使用 SomeModel 对象(模型)来查找所有的 SomeModel 记录
+SomeModel.find(callback_function);
+ +

架设 MongoDB 数据库

+ +

我们已经初步了解了 Mongoose 以及设计模型的方法,现在该开始搭建 LocalLibrary 网站了。第一件事就是设置 MongoDB 数据库,来存储图书馆的数据。

+ +

本教程将使用 mLab 免费版“沙盒”云数据库。这一版不适用于生产环境,因为它没有冗余设计,但非常适合进行开发和原型设计。选用它是因为它免费且易于设置,并且 mLab 是一家流行的数据库服务供应商,也是生产环境数据库的理想选择(撰写本文时(2019年1月),国内流行的云数据库解决方案有 阿里云腾讯云百度云 等)。

+ +
+

注:也可以下载并安装 对应系统的安装包,设置本地版 MongoDB 数据库。多数指令和使用云数据库是一样的,除了连接时数据库的 URL。

+
+ +
+

译注:目前 mLab 网站在国内速度很慢,若遇到无法正常注册或登录的情况可以考虑本地版 MongoDB。

+
+ +

首先 用 mLab 创建一个账户(这是免费的,只需要输入基本联系信息,并同意服务条款)。

+ +

登录后将进入 mLab 主屏幕

+ +
    +
  1. 单击 MongoDB Deployments(MongoDB 部署)部分中的 Create New(新建)
  2. +
  3. 将打开 Cloud Provider(云服务提供商)选择屏幕。
    + MLab - screen for new deployment +
      +
    • 在 Plan Type(方案类型)部分中,选择 SANDBOX(Free)免费沙箱方案。
    • +
    • Cloud Provider(云服务提供商)部分选择任意提供商。不同地区适用不同提供商(显示在选定的计划类型下面)。
    • +
    • 点击 Continue(继续)按钮。
    • +
    +
  4. +
  5. 此时将打开 Select Region(选择区域)屏幕。 +

    Select new region screen

    + +
      +
    • +

      选择离你最近的地区,然后 Continue

      +
    • +
    +
  6. +
  7. +

    将打开 Final Details(最后的细节)屏幕。
    + New deployment database name

    + +
      +
    • +

      输入新数据库的名称 local_library,然后 Continue

      +
    • +
    +
  8. +
  9. +

    将打开 Order Confirmation(订单确认)屏幕。
    + Order confirmation screen

    + +
      +
    • +

      点击 Submit Order(提交订单)以创建数据库。

      +
    • +
    +
  10. +
  11. +

    将返回到主屏幕。点击刚创建的新数据库可以打开详细信息屏幕。当前数据库还没有任何数据。

    + +

    mLab - Database details screen
    +
    + 表单显示了访问数据库的 URL(上图的红框)。此时可以创建一个用户,并在 URL 中指定用户名,就可以访问这个 URL 了。

    +
  12. +
  13. 点击 Users 选项卡,点击 Add database user 按钮。
  14. +
  15. 输入用户名和密码(两次),然后按 Create。不要选择 Make read-only
    +
  16. +
+ +

现在数据库已经创建好了,并且有一个可访问的 URL(带有用户名和密码):mongodb://<dbuser>:<dbpassword>@ds019038.mlab.com:19038/local_library

+ +

安装 Mongoose

+ +

打开终端,并转到 LocalLibrary 站点骨架 的目录。通过以下命令安装 Mongoose(及其依赖项),并将其添加至 package.json 文件,若你在阅读 Mongoose 入门 时已经完成这一操作,请忽略本段。

+ +
$ npm install mongoose
+
+ +

连接到 MongoDB

+ +

打开 /app.js(位于项目根目录),并将以下代码复制到声明 Express 应用对象的位置(var app = express(); 之后)。将数据库 URL 字符串('在此插入数据库_URL')替换为真实的 URL(设置自 mLab)。

+ +
// 设置 Mongoose 连接
+const mongoose = require('mongoose');
+const mongoDB = '在此插入数据库_URL';
+mongoose.connect(mongoDB, { useNewUrlParser: true , useUnifiedTopology: true});
+mongoose.Promise = global.Promise;
+const db = mongoose.connection;
+db.on('error', console.error.bind(console, 'MongoDB 连接错误:'));
+ +

如上文 Mongoose 入门 所讲,以上代码创建了与数据库的默认连接,并绑定了错误事件(错误信息将及时打印到控制台)。

+ +

定义 LocalLibrary 模式

+ +

如上文所述,我们将为每个模型定义单独的模块。首先在项目根目录中创建一个文件夹用来保存模型(/models),然后为每个模型创建单独的文件:

+ +
/express-locallibrary-tutorial  // 项目根目录
+  /models
+    author.js
+    book.js
+    bookinstance.js
+    genre.js
+
+ +

作者模型(Author)

+ +

将下方的 Author 模式代码复制粘贴至 ./models/author.js 文件中。模式中定义了两个 String 模式类型来表示作者的姓氏和名字(这两个字段是必需的,且长度不能超过 100 字符),定义了两个 Date 字段做为作者的生卒日期。

+ +
const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+
+const AuthorSchema = new Schema(
+  {
+    first_name: {type: String, required: true, max: 100},
+    family_name: {type: String, required: true, max: 100},
+    date_of_birth: {type: Date},
+    date_of_death: {type: Date},
+  }
+);
+
+// 虚拟属性'name':表示作者全名
+AuthorSchema
+  .virtual('name')
+  .get(function () {
+    return this.family_name + ', ' + this.first_name;
+  });
+
+// 虚拟属性'lifespan':作者寿命
+AuthorSchema
+  .virtual('lifespan')
+  .get(function () {
+    return (this.date_of_death.getYear() - this.date_of_birth.getYear()).toString();
+  });
+
+// 虚拟属性'url':作者 URL
+AuthorSchema
+  .virtual('url')
+  .get(function () {
+    return '/catalog/author/' + this._id;
+  });
+
+// 导出 Author 模型
+module.exports = mongoose.model('Author', AuthorSchema);
+
+ +

我们还为 AuthorSchema 声明了一个 "url" 虚拟属性,以返回模型特定实例的绝对 URL。在模板中需要获取特定作者的链接时可使用该属性。

+ +
+

注:有必要将 URL 声明为虚拟属性,因为这样,项目的 URL 就只需在一处进行更改。
+ 此时,使用此 URL 的链接还不能工作,因为目前还没有设置任何路由,无法处理特定模型实例的代码。这个问题下节再讲。

+
+ +

模块的最后对模型进行导出。

+ +

藏书模型(Book)

+ +

将下方的 Book 模式代码复制粘贴至 ./models/book.js 文件中。大体结构与作者模型相似,有三个字符串字段, 一个用于获取特定藏书记录 URL 的虚拟属性,代码最后对模型进行导出。

+ +
const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+
+const BookSchema = new Schema({
+  title: {type: String, required: true},
+  author: {type: Schema.Types.ObjectId, ref: 'Author', required: true},
+  summary: {type: String, required: true},
+  isbn: {type: String, required: true},
+  genre: [{type: Schema.Types.ObjectId, ref: 'Genre'}]
+});
+
+// 虚拟属性'url':藏书 URL
+BookSchema
+  .virtual('url')
+  .get(function () {
+    return '/catalog/book/' + this._id;
+  });
+
+// 导出 Book 模块
+module.exports = mongoose.model('Book', BookSchema);
+
+ +

主要区别在于:此处有两个字段是对其他模型的引用(黑体字所示):

+ + + +

藏书副本模型(BookInstance)

+ +

最后将 BookInstance 模式代码复制粘贴至 ./models/bookinstance.js 文件中。 BookInstance 表示可供借阅的藏书的特定副本,其中包含该副本是否可用、还书期限,“出版批次”或版本详细信息。

+ +
const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+
+const BookInstanceSchema = new Schema({
+    // 指向相关藏书的引用
+    book: { type: Schema.Types.ObjectId, ref: 'Book', required: true },
+    // 出版项
+    imprint: {type: String, required: true},
+    status: {
+      type: String,
+      required: true,
+      enum: ['Available', 'Maintenance', 'Loaned', 'Reserved'],
+      default: 'Maintenance'
+    },
+    due_back: {type: Date, default: Date.now}
+  }
+);
+
+// 虚拟属性'url':藏书副本 URL
+BookInstanceSchema
+  .virtual('url')
+  .get(function () {
+    return '/catalog/bookinstance/' + this._id;
+  });
+
+// 导出 BookInstancec 模型
+module.exports = mongoose.model('BookInstance', BookInstanceSchema);
+ +

以上代码有点儿新东西,即字段选项(黑体字):

+ + + +

其他内容和之前的模式大同小异。

+ +

图书种类模型(Genre)——挑战自我!

+ +

打开 ./models/genre.js 文件,并创建一个模式来存储 Genre(书本的类别,例如它是小说类还是纪实类,是爱情题材还是军事史题材,等)。

+ +

与之前模型的定义方式相似:

+ + + +

测试——添加项目

+ +

好了,现在所有模型已准备完毕。

+ +

为了测试这些模型(并添加一些示例藏书和项目,以便后续使用),我们来运行一个单独的脚本来为每种类型创建一些项目:

+ +
    +
  1. 下载(或新建)文件 populatedb.js,保存在 express-locallibrary-tutorial 目录(package.json 所在位置) 。 + +
    +

    注:无需深究 populatedb.js,它只是为数据库添加一些示例数据。

    + +

    译注:针对node.js3.0及以后版本,mlab使用“mongodb+srv://”链接而非“mongodb://”, 请对populatedb.js源码酌情修改,否则会报错而添加数据失败。

    +
    +
  2. +
  3. 在项目根目录运行以下命令,以安装脚本所需的异步模块(后续教程再展开讲) +
    $ npm install async
    +
  4. +
  5. 在命令提示符下用 node 运行此脚本,并以 MongoDB 数据库的 URL 作为参数(同 app.js 中替换 insert_your_database_url_here 占位符的 URL): +
    $ node populatedb <mongodb url>​​​​
    +
  6. +
  7. 该脚本应一路运行至完成,并在终端中记录所创建的项目。
  8. +
+ +
+

提示:打开 mLab 数据库主页面,现在藏书、作者、种类和藏书副本的集合应该都可以打开了,也可以查看单个文档。

+
+ +

小结

+ +

本节介绍了数据库和 ORM(Node/Express 环境)的一些知识,以及定义 Mongoose 模式与模型的方法。随后为 LocalLibrary 网站设计并实现了 BookBookInstanceAuthorGenre 模型。

+ +

本文最后(使用独立运行的脚本)创建了一些测试实例 。下一节将关注如何创建页面以显示这些对象。

+ +

另请参阅

+ + + +

{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/skeleton_website", "Learn/Server-side/Express_Nodejs/routes", "Learn/Server-side/Express_Nodejs")}}

+ +

本章目录

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/routes/index.html b/files/zh-cn/learn/server-side/express_nodejs/routes/index.html new file mode 100644 index 0000000000..eca1531fde --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/routes/index.html @@ -0,0 +1,425 @@ +--- +title: Express 教程 4: 路由和控制器 +slug: learn/Server-side/Express_Nodejs/routes +tags: + - Express + - Node + - nodejs + - 初学者 + - 服务器端编程 + - 路由 +translation_of: Learn/Server-side/Express_Nodejs/routes +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/mongoose", "Learn/Server-side/Express_Nodejs/Displaying_data", "Learn/Server-side/Express_Nodejs")}}
+ +

本节将为 LocalLibrary 站点中所需的资源端点(Endpoint)配置路由。先用空的处理函数搭建起路由处理的模块结构(下节会将它们扩充为真实的处理函数)。并详细介绍了 Express 路由模块的创建方法。

+ + + + + + + + + + + + +
预备知识:回顾 Express/Node 入门。 完成本教程之前小节(Express 教程 3:使用数据库 (Mongoose) 等)。了解服务器端编程,了解正则表达式。
学习目标:理解简单路由的创建方法。设置示例中所有 URL 端点。
+ +

概览

+ +

上节 定义了与数据库交互的 Mongoose 模型,使用一个(独立)脚本创建了一些初始记录。现在可以编写代码来向用户展示这些信息。首先要确定页面中应显示哪些信息,然后定义适当的 URL 来返回这些资源。随后应创建路由(URL 处理器)和视图(模板)来显示这些页面。

+ +

下图展示了 HTTP 请求/响应处理的主数据流和需要实现的行为。图中除视图(View)和路由(Route)外,还展示了控制器(Controller),它们是实际的请求处理函数,与路由请求代码是分开的。

+ +

模型已经创建,现在要创建的主要是:

+ + + +

Express HTTP 请求/响应 路径

+ +

因此我们需要页面来显示藏书、藏书种类、作者、藏书副本的列表和详细信息,还需要页面来创建、更新和删除记录。这些内容对于本节来说不算少,因此本节将主要集中在路由和控制器设置。本节编写的这些函数都只有框架,后续章节再扩展控制器方法,以使用模型数据。

+ +

第一段提供了 Express 的 Router 中间件的“入门”知识。后续设置 LocalLibrary 路由时将用到这些知识。

+ +

路由入门

+ +

路由是一段 Express 代码,它将 HTTP 动词(GETPOSTPUTDELETE 等)、URL 路径/模式和处理函数三者关联起来。

+ +

创建路由有几种方法。本教程将使 express.Router 中间件,因为使用它可以将站点特定部分的路由处理程序打包,并使用通用路由前缀访问它们。我们会将所有与图书馆有关的路由保存在 catalog 模块中,在添加处理帐户或其他功能的路由时,可以分开保存。

+ +
+

注:Express 简介 > 创建路由处理程序 简要讨论了 Express 应用的路由机制。使用 Router 可以保证更好的模块化(下文所述),且用法与直接在 Express 应用对象定义路由非常类似。

+
+ +

本段以下内容介绍使用 Router 定义路由的方法。

+ +

定义和使用单独的路由模块

+ +

以下代码举例说明了如何创建路由模块,以及如何在 Express 应用中使用它。

+ +

首先,在 wiki.js 模块中创建一个维基路由。代码一开始导入 Express 应用对象,用它取得一个 Router 对象,然后用 get() 方法向其添加两个具体的路由。模块的最后导出该 Router 对象。

+ +
// wiki.js - 维基路由模块
+
+const express = require('express');
+const router = express.Router();
+
+// 主页路由
+router.get('/', (req, res) => {
+  res.send('维基主页');
+});
+
+// “关于页面”路由
+router.get('/about', (req, res) => {
+  res.send('关于此维基');
+});
+
+module.exports = router;
+ +
+

注:上面的路由处理回调直接定义在了路由函数中。LocalLibrary 的回调将定义在单独的控制器模块中。

+
+ +

要在主应用中使用该路由模块,首先应 require 它(wiki.js),然后对 Express 应用对象调用 use()(指定路径‘/wiki’),即可将其添加到中间件处理路径。

+ +
const wiki = require('./wiki.js');
+// ...
+app.use('/wiki', wiki);
+ +

这时 wiki 模块中定义的两个路由就可以从 /wiki//wiki/about/ 访问了。

+ +

路由函数

+ +

上述模块定义了两个典型的路由函数。Router.get() 方法定义的 “about” 路由(下方重现的)仅响应 HTTP GET 请求。第一个参数是 URL 路径,第二个参数是一个回调,在收到带有路径的HTTP GET 请求将调用之。

+ +
router.get('/about', (req, res) => {
+  res.send('关于此维基');
+});
+
+ +

该回调有三个参数(通常命名为:reqresnext),分别是:HTTP 请求对象、HTTP 响应、中间件链中的下一个函数。

+ +
+

注:路由函数就是 Express 中间件,这意味着它们必须(通过响应)结束请求,否则必须调用链中的 next 函数。上述示例使用send() 完成了请求,所以没有使用 next 参数(参数表中将其省略)。

+ +

上述路由函数只需要一个回调,可以根据需要指定任意数量的回调参数,或一个回调函数数组。每个函数都将加入中间件链,并且将按添加顺序调用(若有回调完成请求则中止当前周期)。

+
+ +

此处的回调对响应对象调用 send(),当收到带有路径('/about')的 GET 请求时将返回字符串“关于此维基”。还有许多其它可以结束请求/响应周期 响应方法,例如,可调用 res.json() 来发送 JSON 响应,或调用 res.sendFile() 来发送文件。构建 LocalLibrary 最常使用的响应方法是 render(),它使用模板和数据创建并返回 HTML 文件。后续章节进一步讨论。

+ +

HTTP 动词

+ +

上面的示例使用 Router.get() 方法来响应特定路径的 HTTP GET 请求。

+ +

Router 还为所有其他 HTTP 动词提供路由方法,大都用法相同:post(), put(), delete(), options(), trace(), copy(), lock(), mkcol(), move(), purge(), propfind(), proppatch(), unlock(), report(), ​​​​​​ mkactivity(), checkout(), merge(), m-search(), notify(), subscribe(), unsubscribe(), patch(), search(), 和 connect()

+ +

比如下方代码与上方 /about 路由行为一致,但只响应 HTTP POST 请求。

+ +
router.post('/about', (req, res) => {
+  res.send('关于此维基');
+});
+ +

路由路径

+ +

路由路径用于定义可请求的端点。之前示例中路径都是字符串,并且必须精确写为:'/'、'/ about'、'/ book',等等。

+ +

路由路径也可以是字符串模式(String Pattern)。可用部分正则表达式语法来定义端点的模式。以下是所涉及的正则表达式(注意,连字符( -)和点(.)在字符串路径中解释为字面量,不能做为正则表达式):

+ + + +

路由路径也可以是 JavaScript 正则表达式。例如,下面的路由路径将匹配 catfishdogfish,但不会匹配 catflapcatfishhead 等。注意,正则表达式路径不再用引号 "..." 括起来,而是正则表达式语法 /.../

+ +
app.get(/.*fish$/, (req, res) => {
+  ...
+});
+ +
+

注:LocalLibrary 的大部分路由都只使用字符串,很少用字符串模式和正则表达式。接下来将讨论“路由参数”。

+
+ +

路由参数

+ +

路径参数是命名的 URL 段,用于捕获在 URL 中的位置指定的值。命名段以冒号为前缀,然后是名称(例如 /:your_parameter_name/。捕获的值保存在 req.params 对象中,键即参数名(例如 req.params.your_parameter_name)。

+ +

举例说,一个包含用户和藏书信息的 URL:http://localhost:3000/users/34/books/8989,可以这样提取信息(使用 userIdbookId 路径参数):

+ +
app.get('/users/:userId/books/:bookId', (req, res) => {
+  // 通过 req.params.userId 访问 userId
+  // 通过 req.params.bookId 访问 bookId
+  res.send(req.params);
+});
+
+ +

路由参数名必须由“单词字符”(/[A-Za-z0-9_]/)组成。

+ +
+

注:URL /book/create 会匹配 /book/:bookId 这样的路由(将提取值为'create' 的 'bookId')。第一个与传入 URL 相匹配的路由会被使用,因此 /book/create 的路由处理器必须定义在 /book/:bookId 路由之前,才能妥善处理。

+
+ +

以上就是使用路由所有的预备知识。Express 文档中还能找到更多信息:基础路由路由指南。以下是 LocalLibrary 路由和控制器的设置过程。

+ +

LocalLibrary 所需路由

+ +

以下是站点页面的完整 URL 列表。<object> 是模型名称(bookbookinstancegenreauthor),<objects> 是一组模型,<id> 是每个 Mongoose 模型实例默认的标识字段(_id)。

+ + + +

首页和列表页面没有包含任何额外信息。因此它们返回的结果只取决于模型类型和数据库内容,获取信息的查询操作是恒定不变的(类似地,创建对象的代码也没有较大改动)。

+ +

与之相反,其他 URL 是用于处理特定文档/模型实例的,它们会将项目的标识嵌入 URL(上文的 <id>)。可以用路径参数来提取嵌入的信息,并传递给路由处理器(后续章节中用于动态获取数据库中的信息)。通过在 URL 中嵌入信息,使得每种类型的所有资源只需要一个路由(例如,所有藏书副本的显示操作只需要一个路由)。

+ +
+

注:Express 可以通过任何方式构造 URL,可以在 URL 正文中嵌入信息(如上文),或使用 URL GET 参数(例如 /book/?id=6)。无论哪种方法,URL 都应保持整洁、合理且易读(另请参阅 W3C 相关建议)。

+
+ +

下面我们为上述所有 URL 创建路由处理器回调函数和路由代码。

+ +

创建路由处理器回调函数

+ +

定义路由之前应先使用单独的“控制器”模块创建回调的结构骨架。(文件/模块结构没有限制,但以下结构很适合当前项目的规模)。

+ +

首先在项目根目录新建一个存放控制器的文件夹(/controllers),然后为每个模型创建单独的控制器文件(模块):

+ +
/express-locallibrary-tutorial  // 项目根目录
+  /controllers
+    authorController.js
+    bookController.js
+    bookinstanceController.js
+    genreController.js
+ +
+

译注:上述四个文件可到 GitHub 下载 dummyControllers.zip。(链接已失效,请移步英文版查看具体代码)

+
+ +

Author 控制器

+ +

以下是 /controllers/authorController.js 文件的内容:

+ +
const Author = require('../models/author');
+
+// 显示完整的作者列表
+exports.author_list = (req, res) => { res.send('未实现:作者列表'); };
+
+// 为每位作者显示详细信息的页面
+exports.author_detail = (req, res) => { res.send('未实现:作者详细信息:' + req.params.id); };
+
+// 由 GET 显示创建作者的表单
+exports.author_create_get = (req, res) => { res.send('未实现:作者创建表单的 GET'); };
+
+// 由 POST 处理作者创建操作
+exports.author_create_post = (req, res) => { res.send('未实现:创建作者的 POST'); };
+
+// 由 GET 显示删除作者的表单
+exports.author_delete_get = (req, res) => { res.send('未实现:作者删除表单的 GET'); };
+
+// 由 POST 处理作者删除操作
+exports.author_delete_post = (req, res) => { res.send('未实现:删除作者的 POST'); };
+
+// 由 GET 显示更新作者的表单
+exports.author_update_get = (req, res) => { res.send('未实现:作者更新表单的 GET'); };
+
+// 由 POST 处理作者更新操作
+exports.author_update_post = (req, res) => { res.send('未实现:更新作者的 POST'); };
+
+ +

该模块首先导入了用于访问和更新数据的模型,然后为每个需要处理(添加、更新和删除表单,以及相应的 POST 请求,稍后在 使用表单 一节中讲解)的 URL 导出一个函数。

+ +

所有函数都遵循 Express 中间件函数的标准形式,三个参数依次为:请求 req、响应 res、当前方法无法完成请求循环时(这里不存在这种情况)调用的 next 函数。上述方法只返回一个字符串,显示相关页面尚未创建。接收路径参数的控制器函数可将参数输出到消息字符串中(代码中出现的 req.params.id )。

+ +

BookInstanceGenreBook 控制器

+ +

这三个控制器与 Author 的模式完全相同,只是 Book 有一个用于显示站点欢迎页面的 index() 函数:

+ +
// /controllers/bookController.js
+
+const Book = require('../models/book');
+
+exports.index = (req, res) => { res.send('未实现:站点首页'); };
+
+...
+
+ +

创建藏书编目 catalog 路由模组

+ +

定义好控制器后,我们来为 LocalLibrary 网站创建完整的 URL 路由。

+ +

站点骨架中有一个 ./routes 文件夹,其中包含两个路由文件:index 和 user,在这里新建一个 catalog.js 路由文件,如下所示:

+ +
/express-locallibrary-tutorial   // 项目根目录
+  /routes
+    index.js
+    users.js
+    catalog.js
+ +
+

译注:可到 GitHub 下载完整的 catalog.js

+
+ +

/routes/catalog.js 中有以下代码:

+ +
const express = require('express');
+const router = express.Router();
+
+// 导入控制器模块
+const book_controller = require('../controllers/bookController');
+const author_controller = require('../controllers/authorController');
+const genre_controller = require('../controllers/genreController');
+const book_instance_controller = require('../controllers/bookinstanceController');
+
+/// 藏书路由 ///
+
+// GET 获取藏书编目主页
+router.get('/', book_controller.index);
+
+// GET 请求添加新的藏书。注意此项必须位于显示藏书的路由(使用了 id)之前。
+router.get('/book/create', book_controller.book_create_get);
+
+// POST 请求添加新的藏书
+router.post('/book/create', book_controller.book_create_post);
+
+// GET 请求删除藏书
+router.get('/book/:id/delete', book_controller.book_delete_get);
+
+// POST 请求删除藏书
+router.post('/book/:id/delete', book_controller.book_delete_post);
+
+// GET 请求更新藏书
+router.get('/book/:id/update', book_controller.book_update_get);
+
+// POST 请求更新藏书
+router.post('/book/:id/update', book_controller.book_update_post);
+
+// GET 请求藏书
+router.get('/book/:id', book_controller.book_detail);
+
+// GET 请求完整藏书列表
+router.get('/books', book_controller.book_list);
+
+/// 藏书副本、藏书种类、作者的路由与藏书路由结构基本一致,只是无需获取主页 ///
+
+module.exports = router;
+
+ +

该模块导入了 express 并创建了一个 Router 对象 router。所有路由都设置在 router 上,最后将其导出。

+ +

router 对象调用 .get().post() 函数即可定义路由。这里所有路径都使用字符串定义(而不用字符串模式或正则表达式)。某些特定资源(如藏书)的路由使用路径参数从 URL 中获取对象标识。

+ +

处理函数均导入自上文的控制器模块。

+ +

更新 index 路由模块

+ +

新路由已经设置完毕,还需要设置一下 index 模块。我们将网站的首页重定向(redirect)至刚创建的地址 '/catalog'。

+ +

/routes/index.js 中的中间件修改一下:

+ +
// GET 请求主页
+router.get('/', (req, res) => {
+  res.redirect('/catalog');
+});
+ +
+

注:这是我们第一次使用 redirect() 响应方法。它会使路由重定向到指定的页面,默认发送 HTTP 状态代码“302 Found”。可以根据需要更改返回的状态代码、设置绝对或相对路径。

+
+ +

更新 app.js

+ +

最后一步是在 app.js 中将路由添加到中间件链。

+ +

打开 app.js,在 indexusers 路由下方添加 catalog 路由:

+ +
const indexRouter = require('./routes/index');
+const usersRouter = require('./routes/users');
+const catalogRouter = require('./routes/catalog');  // 导入 catalog 路由
+ +

然后在已定义的路由下方将目录路由添加进中间件栈:

+ +
app.use('/', indexRouter);
+app.use('/users', usersRouter);
+app.use('/catalog', catalogRouter);  // 将 catalog 路由添加进中间件链
+ +
+

注:我们将图书编目模块添加到了 '/catalog' 路径,该路径是 catalog 模块中所有路径的前缀。例如,访问藏书列表 的URL 为:/catalog/books/

+
+ +

大功告成。LocalLibrary 网站所需的所有 URL 的路由和框架函数都已准备完毕。

+ +

测试路由

+ +

要测试路由,先要启动网站

+ + + +
+

译注:以上命令只对 bash 有效,要在 Windows 上使用 bash,最简单的方法就是安装 Git。(详情参见 教程 2 相关译注

+
+ +

接下来浏览上面一些 URL,确保不会收到错误页面(HTTP 404)。可以尝试以下示例:

+ + + +

小结

+ +

网站的路由已创建完毕,接下来的教程将把完整的实现填入控制器框架中。藉此来学习 Express 路由的基础知识,以及组织路由和控制器的一些方式。

+ +

下一节将使用视图 (模板) 和模型里的信息创建一个欢迎页面。

+ +

另请参阅

+ + + +

{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/mongoose", "Learn/Server-side/Express_Nodejs/Displaying_data", "Learn/Server-side/Express_Nodejs")}}

+ +

本章目录

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/skeleton_website/index.html b/files/zh-cn/learn/server-side/express_nodejs/skeleton_website/index.html new file mode 100644 index 0000000000..077b7a3862 --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/skeleton_website/index.html @@ -0,0 +1,476 @@ +--- +title: Express 教程 2:创建站点框架 +slug: learn/Server-side/Express_Nodejs/skeleton_website +tags: + - Express + - Node + - node.js + - npm + - 入门 + - 初学者 + - 学习 + - 开发环境 + - 服务器端 + - 服务器端编程 +translation_of: Learn/Server-side/Express_Nodejs/skeleton_website +--- +
{{LearnSidebar}}
+ +

{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/Tutorial_local_library_website", "Learn/Server-side/Express_Nodejs/mongoose", "Learn/Server-side/Express_Nodejs")}}

+ +

本节将演示如何创建一个可添加路由、模板/视图、和数据库调用的“骨架”站点。

+ + + + + + + + + + + + +
预备知识:配置 Node 开发环境。复习 Express 教程。
目标:掌握用 Express 应用生成器 创建站点的方法。
+ +

概览

+ +

本节演示了如何使用 Express 应用生成器 创建一个可添加路由、模板/视图和数据库调用的“骨架”网站。这里我们将使用该生成器为 本地图书馆网站 创建框架,以便在以后章节添加其它代码。过程非常简单,只需要在命令行运行 “生成器 + 项目名称” 即可,此外还可以指定站点的模板引擎和 CSS 生成器。

+ +

以下内容介绍了应用生成器的用法,以及视图 / CSS 的一些不同选项。还介绍了骨架站点的组织结构。最后,我们将介绍站点的运行方法,从而对其进行验证。

+ +
+

注:Express 应用生成器并非唯一的 Express 应用生成工具,而且生成项目的结构也不是组织文件和目录的唯一可行方式。但生成项目具有易于扩展和理解的模块化结构。最简单的 Express 应用请参阅 Hello world 示例(Express 镜像站)。

+
+ +

使用应用生成器

+ +
+

译注:本教程中命令操作基于 Linux/macOS 的 bash 终端,Windows 的命令提示符 cmd/PowerShell 与 bash 的概念和用法略有不同, 为在 Windows 上获得一致的体验,可以:

+ + +
+ +
+

译注:你可能已经发现国内用 NPM 太慢了!这是由众所周知的不可抗力造成的。可用淘宝提供的 CNPM 代替之,功能和用法基本一致(只是不能上传自己的包)。

+
+ +

你应该已经安装好了生成器,它是 设置 Node 开发环境 的一部分。可以使用 NPM 来安装全局的生成器,如下所示:

+ +
$ sudo npm install express-generator -g
+ +

生成器有许多选项,可以使用 --help(或 -h)命令进行查看:

+ +

express 生成器的帮助信息

+ +

大意如下:

+ +
$ express --help
+
+  用法:express [选项] [目录]
+
+  选项:
+
+        --version        打印版本号
+    -e, --ejs            添加 ejs 引擎支持
+        --pug            添加 pug 引擎支持
+        --hbs            添加 handlebars 引擎支持
+    -H, --hogan          添加 hogan.js 引擎支持
+    -v, --view <engine>  添加 <engine> 试图引擎支持 (ejs|hbs|hjs|jade|pug|twig|vash) (默认为 jade)
+    -c, --css <engine>   添加 <engine> 样式表引擎支持 (less|stylus|compass|sass) (默认为纯 css)
+        --git            添加 .gitignore
+    -f, --force          对非空文件夹强制执行
+    -h, --help           打印帮助信息
+
+ +

可以直接运行 express 命令,将使用 Jade 视图引擎和纯 CSS 在当前目录中创建项目。(如果指定目录名,则在子目录中创建项目)。

+ +
$ express
+ +

还可以使用 --view 选择视图(模板)引擎,并且/或者使用 --css 选择 CSS 生成引擎。

+ +
+

注:不推荐用 --hogan--ejs--hbs 等参数选用模板引擎。请使用 --view(或 -v)。

+
+ +

我应该用哪个视图引擎?

+ +

Express 应用生成器支持多款流行的视图/模板引擎,包括 EJSHbsPug (Jade)、TwigVash,缺省选项是 Jade。Express 本身也支持大量其他模板语言,开箱即用

+ +
+

注:如果要使用生成器不支持的模板引擎,请参阅 在 Express 中使用模板引擎(Express 文档)和所选视图引擎的文档。

+
+ +

一般来说,你应该选择一个大而全的模板引擎,可以尽快进入生产状态。就像你选择其他组件一样!选用模板引擎需要考虑以下因素:

+ + + +
+

提示:互联网上有许多资源,可帮助你选择合适的视图/模板引擎。

+
+ +

本项目选用 Pug 模板引擎(Jade 是它不久前的曾用名),它是最流行的 Express / JavaScript 模板语言之一,且对 Express 生成器 开箱即用

+ +

我应该用哪个 CSS 引擎?

+ +

Express 应用生成器支持最常见的 CSS 引擎:LESS, SASS, Compass, Stylus

+ +
+

注:CSS 的一些限制导致某些任务完成起来非常困难。CSS 引擎提供了更强大的语法来定义 CSS,然后将定义编译为纯 CSS 供浏览器使用。

+
+ +

与模板引擎一样,你也应该使用样式表引擎,这可以最大化团队生产力。本项目将使用原始 CSS(默认的),因为我们对 CSS 要求不复杂,没有必要使用引擎。

+ +

我应该用哪个数据库?

+ +

生成器生成的代码不使用、也不包含任何数据库。 Express 应用可以使用 Node 支持的所有 数据库(Express 本身不提供数据库管理机制)。

+ +

我们后续讨论数据库集成问题。

+ +

创建项目

+ +

我们为本地图书馆应用创建一个名为 express-locallibrary-tutorial 的项目,使用 Pug 模板库,不使用 CSS 引擎。

+ +

首先,进入准备放置项目的目录,然后在命令提示符运行 Express 应用生成器,生成器将创建(并列出)项目的文件:

+ +

用 express 生成器生成一个应用

+ +

生成器在最后还告诉你如何安装(package.json 中所列的)依赖,以及如何运行该应用。

+ +

运行骨架网站

+ +

现在我们已经拥有一个完整的项目骨架。虽然这个网站现在还做不了什么,但运行一下,展示一下工作原理也是值得的。

+ +
    +
  1. 首先,安装依赖项(install 命令将获取项目的 package.json 文件中列出的所有依赖项包)。 + +
    $ npm install
    +
  2. +
  3. 然后运行该应用。 +
    $ DEBUG=express-locallibrary-tutorial:* npm start
    +
  4. +
  5. 最后在浏览器中导航至 http://localhost:3000/ ,就可以访问该应用。你应该可以看到: Express 应用生成器生成的应用启动成功
  6. +
+ +

一个 Express 应用就配置成功了,它托管于 localhost:3000。

+ +
注:指定 DEBUG 变量可启用控制台日志记录/调试。例如,当你访问上面的页面时,你会看到像这样的调试输出: 用 npm start 启动这个应用 直接通过 npm start 命令启动应用也可以,但不会看到调试信息。
+ +

文件改动时重启服务器

+ +

只有重启服务器才能看到 Express 网站所做的改动。每次改动后手动启停服务器实在太烦人了,有必要花点时间让这项工作自动化。

+ +

nodemon 是最简便的自动化工具之一。通常将其全局安装(因为它是一个“工具”):

+ +
$ sudo npm install -g nodemon
+ +

这里还可以把它作为开发依赖将安装在本地,于是使用这个项目的开发人员只要安装这个应用就能自动获得。通过以下命令将其安装在骨架项目的根目录:

+ +
$ npm install --save-dev nodemon
+ +

项目的 package.json 文件将自动添加一个新的属性:

+ +
  "devDependencies": {
+    "nodemon": "^1.18.9"
+  }
+
+ +

如果没有全局安装该工具,就无法从命令行启动它(除非我们将其添加到路径中),但是可以在 NPM 脚本中调用它,因为 NPM 掌握所有已安装包的信息。找到 package.json 的 scripts 部分。在 "start" 一行的末尾添加逗号,并在新的一行中添加 "devstart",如下所示:

+ +
  "scripts": {
+    "start": "node ./bin/www",
+    "devstart": "nodemon ./bin/www"
+  },
+
+ +

现在可以用新建的 devstart 命令启动服务器:

+ +
$ DEBUG=express-locallibrary-tutorial:* npm run devstart
+ +

现在,如果编辑项目中的任何文件,服务器将自动重启(或者可以随时使用 rs 命令来重启)。查看更新后的页面需要点击浏览器的“刷新”按钮。

+ +
+

注:这里必须使用“npm run <scriptname>”命令,而不是 npm start,因为 “start” 本质上是映射到脚本的一条 NPM 命令。我们可以在 start 脚本中替换它,但我们只想在开发期间使用 nodemon,因此有必要创建一条新的脚本命令。

+
+ +

生成的项目

+ +

我们来看看刚刚创建的项目。

+ +

目录结构

+ +

安装好依赖项的生成项目具有如下文件结构(带 “/” 前缀的是文件):

+ +
/express-locallibrary-tutorial
+    app.js
+    /bin
+        www
+    package.json
+    /node_modules
+        [约 4,500 个子文件夹和文件]
+    /public
+        /images
+        /javascripts
+        /stylesheets
+            style.css
+    /routes
+        index.js
+        users.js
+    /views
+        error.pug
+        index.pug
+        layout.pug
+
+
+ +

package.json 文件定义依赖项和其他信息,以及一个调用应用入口(/bin/www,一个 JavaScript 文件)的启动脚本,脚本中还设置了一些应用的错误处理,加载 app.js 来完成其余工作。/routes 目录中用不同模块保存应用路由。/views 目录保存模板。

+ +

下面来详细介绍这些文件。

+ +

package.json

+ +

package.json 文件中定义了应用依赖和其他信息:

+ +
{
+  "name": "express-locallibrary-tutorial",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "start": "node ./bin/www"
+    "devstart": "nodemon ./bin/www"
+  },
+  "dependencies": {
+    "cookie-parser": "~1.4.3",
+    "debug": "~2.6.9",
+    "express": "~4.16.0",
+    "http-errors": "~1.6.2",
+    "morgan": "~1.9.0",
+    "pug": "2.0.0-beta11"
+  },
+  "devDependencies": {
+    "nodemon": "^1.18.9"
+  }
+}
+
+ +

依赖包括 express 包,和选用的视图引擎包(pug)。还有以下一些实用的包:

+ + + +

"scripts" 部分,定义了一个 "start" 脚本,当运行 npm start 时会调用它来启动服务器。在脚本定义中可以看到 start 实际上运行了 "node ./bin/www"。还有一个 "devstart" 脚本,可以通过运行 npm run devstart 来运行 "nodemon ./bin/www"。

+ +
  "scripts": {
+    "start": "node ./bin/www",
+    "devstart": "nodemon ./bin/www"
+  },
+
+ +

www 文件

+ +

文件 /bin/www 是应用入口!它做的第一件事是 require() “真实” 的应用入口(即项目根目录中的 app.js ),app.js 会设置并返回 express() 应用对象。

+ +
#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ * 模块依赖项。
+ */
+
+var app = require('../app');
+
+ +
+

注:require() 是一个全局的 node 函数,可将模块导入当前文件。这里使用相对路径指定 app.js 模块,并省略了 .js 扩展名(可选)。

+
+ +

文件的其余部分先为 app 设置端口(环境变量中的预定义值或默认值 3000),再创建一个 HTTP 服务器,然后开始监听请求,报告服务器错误和连接信息。其它内容可暂时忽略(这里所有内容都是机器生成的模板),但感兴趣的话可以随时回来看看。

+ +

app.js

+ +

此文件创建一个 express 应用对象(依照惯例命名为 app),通过各种设置选项和中间件来设置这个应用,然后从该模块中导出。以下代码只展示了文件中创建和导出应用对象的部分:

+ +
var express = require('express');
+var app = express();
+...
+module.exports = app;
+
+ +

上文的 www 入口文件中 require()app 就是这里导出的 。

+ +

我们来详细了解一下 app.js 文件。首先,它使用 require() 导入了一些实用 node 库,其中包括之前用 NPM 下载的 expresshttp-errorsmorgancookie-parser,还有一个 path 库,它是用于解析文件和目录的核心 node 库。

+ +
var express = require('express');
+var createError = require('http-errors');
+var logger = require('morgan');
+var cookieParser = require('cookie-parser');
+var path = require('path');
+
+ +

然后 require() 的是用户路由目录中的模块。这些模块/文件用于处理特定的“路由”(URL 路径)。可以通过添加新文件来扩展骨架应用,比如添加图书相关的路由来列出所有馆藏书目。

+ +
var indexRouter = require('./routes/index');
+var usersRouter = require('./routes/users');
+
+ +
+

注意: 此时我们刚刚导入了模块;还没有真正使用过其中的路由(稍后会使用)。

+
+ +

下面我们用导入的 express 模块来创建 app 对象,然后使用它来设置视图(模板)引擎。设置引擎分两步:首先设置 'views' 以指定模板的存储文件夹(此处设为子文件夹 /views)。然后设置 'view engine' 以指定模板库(本例中设为 “pug” )。

+ +
var app = express();
+
+// view engine setup
+// 视图引擎设定
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'pug');
+
+ +

下一组 app.use() 调用将中间件库添加进请求处理链。除了之前导入的第三方库之外,我们还使用 express.static 中间件将项目 /public 目录下所有静态文件托管至根目录。

+ +
app.use(logger('dev'));
+app.use(express.json());
+app.use(express.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(express.static(path.join(__dirname, 'public')));
+
+ +

所有中间件都已设置完毕,现在把(之前导入的)路由处理器添加到请求处理链中。从而为网站的不同部分定义具体的路由:

+ +
app.use('/', indexRouter);
+app.use('/users', usersRouter);
+
+ +
+

注:这些路径('/' 和 '/users')将作为导入路由的前缀。如果导入的模块 users/profile 定义了路由,则可以在 /users/profile 访问该路由。我们将在后面的文章中,详细讨论路由。

+
+ +

最后一个中间件为错误和 HTTP 404 响应添加处理方法。

+ +
// catch 404 and forward to error handler
+// 捕获 404 并抛给错误处理器
+app.use(function(req, res, next) {
+   next(createError(404));
+});
+
+// error handler
+// 错误处理器
+app.use(function(err, req, res, next) {
+   // set locals, only providing error in development
+   // 设置 locals,只在开发环境提供错误信息
+   res.locals.message = err.message;
+   res.locals.error = req.app.get('env') === 'development' ? err : {};
+
+   // render the error page
+   // 渲染出错页面
+   res.status(err.status || 500);
+   res.render('error');
+});
+
+ +

Express 应用对象(app)现已完成配置。最后一步是将其添加到 exports 模块(使它可以通过 /bin/www 导入)。

+ +
module.exports = app;
+ +

路由

+ +

路由文档 /routes/users.js 如下所示(由于路由文件均使用类似结构,所以 index.js 略过不讲)。首先加载 express 模块​​并获取 express.Router 对象(命名为 router)。然后为 router 指定路由,最后导出 router(就可以导入 app.js 了)。

+ +
var express = require('express');
+var router = express.Router();
+
+/* GET users listing. */
+router.get('/', function(req, res, next) {
+  res.send('respond with a resource');
+});
+
+module.exports = router;
+
+ +

该路由定义了一个回调,在检测到正确模式的 HTTP GET 请求时将调用该回调。正确模式即导入模块时指定的路由('/users')加该模块('/')中定义的任何内容。换句话说,在收到 /users/ URL 时使用此路由。

+ +
+

提示:用 node 启动该应用并访问 http://localhost:3000/users/,浏览器会返回一条消息:'respond with a resource'。

+
+ +

值得注意的是,上述回调函数有第三个参数 'next',因此它是一个中间件函数,而不是简单的路由回调。next 参数暂时还用不到,在 '/' 路径中添加多个路由处理器时才会涉及。

+ +

视图(模板)

+ +

视图(模板)存保存在 /views 目录中( app.js 中指定),使用 .pug 扩展名。 Response.render() 方法用某对象的某个变量值一同来渲染一个特定的模板,然后将结果作为响应发送。在 /routes/index.js 中可以看到,该路由使用 'index' 模板和一个模板变量 title 来渲染响应。

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

以下是上文代码中涉及到的模板(index.pug)。pug 语法稍后再详细讨论。现在只需要知道:title 变量将以 'Express' 作为值插入模板的指定位置。

+ +
extends layout
+
+block content
+  h1= title
+  p Welcome to #{title}
+
+ +

挑战自我

+ +

/routes/users.js 中添加一个新路由,在URL /users/cool/ 处显示文本 "你好酷"。运行服务器,并在浏览器中访问 http://localhost:3000/users/cool/ ,测试一下是否成功。

+ + + +

小结

+ +

现在你已经为 本地图书馆 创建好了骨架,并且成功在 node 上运行起来。同时最重要的是,你了解了项目的结构,从而了解了如何为本地图书馆添加路由和视图。

+ +

下一节我们开始修改骨架,让它具备一些图书馆网站的功能。

+ +

另请参阅

+ + + +

{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/Tutorial_local_library_website", "Learn/Server-side/Express_Nodejs/mongoose", "Learn/Server-side/Express_Nodejs")}}

+ +

本章目录

+ + diff --git a/files/zh-cn/learn/server-side/express_nodejs/tutorial_local_library_website/index.html b/files/zh-cn/learn/server-side/express_nodejs/tutorial_local_library_website/index.html new file mode 100644 index 0000000000..86714e0b8a --- /dev/null +++ b/files/zh-cn/learn/server-side/express_nodejs/tutorial_local_library_website/index.html @@ -0,0 +1,93 @@ +--- +title: 'Express 教程: 本地图书馆网站' +slug: learn/Server-side/Express_Nodejs/Tutorial_local_library_website +tags: + - Express + - Node + - nodejs + - web框架 + - 初学者 + - 后端框架 + - 学习教程 +translation_of: Learn/Server-side/Express_Nodejs/Tutorial_local_library_website +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/development_environment", "Learn/Server-side/Express_Nodejs/skeleton_website", "Learn/Server-side/Express_Nodejs")}}
+ +

在实战教程第一节中,你将了解要学习哪些内容,对「本地图书馆」示例网站有一个初步的印象 。本章接下来的内容就是逐步完成这个网站。

+ + + + + + + + + + + + +
预备知识:阅读 Expres 入门。进行以后的小节还需要阅读 配置 Node 开发环境
学习目标:引入本教程的示例应用,了解所涉及的所有主题。
+ +

概览

+ +

欢迎来到 MDN "本地图书馆" Express (Node) 教程,我们将开发一个管理本地图书馆编目的网站。

+ +

本系列教程中,你将:

+ + + +

你可能已经学过(或之前接触过)其中的部分主题。学完列系教程后,你就拥有足够技能独立开发简单的 Express 应用了。

+ +

本地图书馆网站(LocalLibrary)

+ +

我们给本地图书馆网站起一个名字——LocalLibrary,这个名字将始终伴随本教程。顾名思义,此网站是为一家小型本地图书馆提供线上图书编目而建,用户可以能够浏览馆藏书目,还能管理自己的帐号。

+ +

本示例是精心挑选的,它规模灵活,可以根据我们的需求进行自由调整。还能演示绝大多数 Express 特性。更重要的是,这里提供的指引对所有网站都适用:

+ + + +

尽管这个示例具备相当可观的扩展度,但依然有理由把它叫做本地图书馆(LocalLibrary)。 我们希望呈现给你最少的信息,从而帮助你尽快上手并运行 Express。因此,我们只保留书名、本数、作者以及其它关键信息。我们会省略掉其它可能用到的信息,也不会提供多图书馆架构或“大型图书馆"等特性的支持。

+ +

我被难住了,哪里有源代码?

+ +

本教程进行过程中,我们将在每个知识点为你提供适当的代码片段,其中一些内容我们希望你能(在一定指引下)自己填充。

+ +

别总是复制粘贴这些片段,试着独立完成,长期来看这样做是有好处的,你下次编写类似代码时将更熟练。

+ +

如果实在进行不下去,可以参考 Github 上的完整版本。

+ +
+

注:本教程中的代码,已在特定版本(项目的 package.json 所列版本)的 node、Express 及其它模组的环境下通过测试。

+
+ +

总结

+ +

现在,你对 LocalLibrary 网站和即将学习的东西又多了解了一点,下面,我们开始创建一个用于存放它的 框架 吧!

+ +

{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/development_environment", "Learn/Server-side/Express_Nodejs/skeleton_website", "Learn/Server-side/Express_Nodejs")}}

+ +

本章目录

+ + -- cgit v1.2.3-54-g00ecf