阅读(402) (0)

Express Tutorial Part 5: Displaying library data

2017-05-15 17:26:34 更新
先决条件: 完成上一篇教程主题(包括 Express教程第4部分:路由和控制器)。
目的: 要了解如何使用异步模块和Pug模板语言,以及如何从我们的控制器函数中的URL获取数据。

概述

在之前的教程文章中,我们定义了 Mongoose模型,我们可以使用这些模型与数据库进行交互并创建一些初始库记录。 然后,我们创建了LocalLibrary网站所需的所有路线,但使用"虚拟控制器"函数(这些是骨架控制器函数,只返回"未实现" 消息)。

下一步是为我们的库信息页面提供适当的实现(我们将在后面的文章中讨论实现页面的具体表单以创建,更新或删除信息)。 这包括更新控制器函数以使用我们的模型提取记录,并定义模板以向用户显示此信息。

我们将从提供概述/主题主题开始,解释如何管理控制器函数中的异步操作以及如何使用Pug编写模板。 然后,我们将为每个主要的"只读"页面提供实现,并简要说明它们使用的任何特殊或新功能。

在本文结束时,您应该对路由,异步函数,视图和模型在实践中如何工作有一个良好的端到端理解。

使用异步的异步流控制

一些我们的LocalLibrary 页面的控制器代码将取决于多个异步请求的结果,这可能需要以某种特定的顺序或并行运行。 为了在我们获得所有必需的信息时管理流控制和渲染页面,我们将使用受欢迎的节点 > async 模块。

注意:还有其他一些方法可用于管理JavaScript中的异步行为和流量控制,包括最近的JavaScript语言功能,例如 / docs / Mozilla / Add-ons / Techniques / Promises"> Promises

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

  • async.parallel() to execute any operations that must be performed in parallel.
  • async.series() for when we need to ensure that asynchronous operations are performed in series.
  • async.waterfall() for operations that must be run in series, with each operation depending on the results of preceding operations.

为什么需要这个?

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

如果控制器只需要一个异步操作来获取呈现页面所需的信息,那么实现就很容易 - 我们只需在回调中呈现模板。 下面的代码片段显示了这样一个函数,它提供了一个模型的计数 SomeModel (使用Mongoose model_Model.count"class ="external"> 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 asyncrhonous 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'
}
);

安装异步

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

npm install async --save

模板引物

模板是定义输出文件的结构或布局的文本文件,其中占位符用于表示在模板呈现时插入数据的位置(在 Express 称为视图)。

Express可与许多不同的模板呈现引擎一起使用。 在本教程中,我们使用帕格(以前称为) 为我们的模板。 这是最受欢迎的Node模板语言,并且将其本身描述为用于编写HTML的"干净,空白敏感的语法,受到 Haml / a>"。

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

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

模板配置

已配置为使用帕格 ="/ webstart / Express_Nodejs / skeleton_website">创建了骨架网站。 您应该在网站的 package.json 文件中查看包含pug模块的依赖关系,以及 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

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

  • script(type='text/javascript'), link(rel='stylesheet', href='/stylesheets/style.css')
  • meta(name='viewport' content='width=device-width initial-scale=1')

所有属性的值都被转义(例如,像"> "之类的字符被转换为类似"& 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}.

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

您可以使用管道(\' | \')在行的开头指示" "external"> plain text "。 例如,下面显示的附加文本将显示在与前一个锚相同的行上,但不会链接。

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

Pug允许您使用 if else else if 执行条件操作,除非

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

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

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

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

扩展模板

在整个网站中,通常所有页面都有一个通用的结构,包括头,页脚,导航等的标准HTML标记。而不是强制开发人员在每个页面复制这个"样板",

em>允许您声明一个基本模板,然后扩展它,只替换每个特定页面不同的位。

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

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

标签用于标记可在导出模板中替换的内容段(如果块未重新定义,则使用其在基本类中的实现)。

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

extends layout

block content
  h1= title
  p Welcome to #{title}

LocalLibrary基本模板

现在我们了解了如何使用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或另一个客户端web框架是一个快速的方式来创建一个有吸引力的网页,可以在不同的浏览器大小扩展好,它也允许我们处理页面演示,而无需进入任何细节 - 我们只是 想在这里聚焦于服务器端代码!

如果您已阅读我们上述的模板引用,布局应该非常明显。 请注意使用阻止内容作为占位符,用于放置我们各个页面的内容。

基本模板还引用了一个提供一些额外样式的本地css文件( styles.css )。 打开 /public/stylesheets/styles.css ,并将其内容替换为以下CSS代码:

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

当我们到运行我们的网站,我们应该看到侧边栏出现! 在接下来的部分中,我们将使用上述布局来定义单个页面。

主页

我们将创建的第一个页面是网站主页,可以从网站(\'/\')或目录(目录/ )访问。 这将显示一些描述站点的静态文本,以及数据库中不同记录类型的动态计算的"计数"。

我们已经为主页创建了一个路线。 为了完成页面,我们需要更新我们的控制器函数来从数据库中获取记录的"计数",并创建一个视图(模板),我们可以使用它来渲染页面。

路线

我们在上一个教程中创建了索引页路线。提醒您,所有路线功能都在 /routes/catalog.js strong>:

/* 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');
}

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

控制器

索引控制器功能需要获取有关 Book BookInstance ,可用的 BookInstance Author 代码> Genre 记录在数据库中,在模板中呈现这些数据以创建一个HTML页面,然后在HTTP响应中返回它。

请注意:我们使用 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'); 
}

将以上所有代码替换为以下代码片段。 第一件事是import( 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);
        },
        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()方法被传递一个对象,其函数用于获取每个模型的计数。 这些功能都是同时启动的。 当所有它们都完成时,最终回调被调用与results参数中的计数(或错误)。

成功时,回调函数调用 res.render() / code>,指定名为" index "的视图(模板)和包含要插入其中的数据的对象(这包括包含模型计数的results对象)。 数据作为键值对提供,可以使用键在模板中访问。

注意:上面 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 基本模板,覆盖名为"内容"的。 第一个 h1 标题将是传递到 render()函数中的 title 变量的转义文本, h1 = \',以便将以下文本视为JavaScript表达式。 然后我们包括一个介绍LocalLibrary的段落。

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

注意:我们没有转义计数值(即我们使用!{} 语法),因为计算的是计数值。 如果信息由最终用户提供,那么我们将转义变量来显示。

它是什么样子的?

在这一点上,我们应该创建显示索引页所需的一切。 运行应用程序并打开浏览器以 http:// localhost:3000 / 。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。

请注意:您将无法使用侧栏链接,因为尚未定义这些网页的网址,视图和模板。 如果您尝试,您将收到错误,例如"NOT IMPLEMENTED:Book list",例如,根据您点击的链接。 这些字符串文字(将被适当的数据替换)在居住在"controllers"文件中的不同控制器中指定。

图书列表页

接下来,我们将实现我们的图书列表页面。 此页面需要显示数据库中所有图书的列表及其作者,每个图书标题是其关联的图书详细信息页面的超链接。

控制器

书列表控制器函数需要获取数据库中所有 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 代码>,因为我们不需要其他字段(它也将返回 _id 和虚拟字段)。 这里我们还调用 Book 上的 populate(),指定作者字段 - 这将用完整的作者详细信息替换存储的图书作者ID。

成功时,传递到查询的回调会呈现 book_list (。pug)模板,传递 title book_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 基本模板,并覆盖了名为"内容"的。 它显示我们从控制器传递的 title (通过 render()方法),然后使用 >每个 - in - else 语法。 为显示书名的每本书创建一个列表项,作为书的详细页面的链接,后面跟着作者姓名。 如果 book_list 中没有图书,则会执行 else 子句,并显示文本"没有图书"。

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

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

它是什么样子的?

运行应用程序(请参阅相关命令的测试路线),然后打开浏览器 http:// localhost:3000 / 。 然后选择所有图书链接。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。

; width:918px;">

BookInstance列表页

接下来,我们将实现库中所有图书副本( BookInstance )的列表。 此页面需要包括与 BookInstance 模型中的其他信息相关联的 Book 的标题(链接到其详细信息页面) 包括每个副本的状态,印记和唯一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 字段 - 这将替换每个 BookInstance 代码> Book 文档。

成功时,传递到查询的回调会呈现 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 中的所有书副本。 对于每个副本,我们显示其状态(彩色编码),如果图书不可用,则其预期的返回日期。

它是什么样子的?

运行应用程序,打开浏览器 http:// localhost:3000 / ,然后选择所有图书 - 实例链接。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。

; width:1200px;">

使用时刻进行日期格式化

从我们的模型默认渲染日期是非常丑陋: Tue Dec 06 2016 15:49:58 GMT + 1100(AUS Eastern Daylight Time)。 在本节中,我们将介绍如何更新上一部分的 BookInstance列表页面,以更友好的格式显示 due_date 字段:2016年12月6日。

我们将使用的方法是在我们的 BookInstance 模型中创建一个虚函数,返回格式化的日期。 我们将使用时刻进行实际格式化,这是一个轻量级JavaScript日期库,用于解析,验证,操作 ,以及格式化日期。

注意:可以使用时刻在我们的Pug模板中直接设置字符串格式,也可以在其他多个位置格式化字符串。 使用虚拟属性允许我们以与我们得到 due_date 完全相同的方式获得格式化的日期。

安装时刻

在项目的根目录中输入以下命令:

npm install moment --save

创建虚拟属性

  1. Open ./models/bookinstance.js.
  2. At the top of the page, import moment.
    var moment = require('moment');
  3. Add the virtual property due_back_formatted just after the url property.
    BookInstanceSchema
    .virtual('due_back_formatted')
    .get(function () {
      return moment(this.due_back).format('MMMM Do, YYYY');
    });

注意:格式方法可以使用几乎模式显示日期。 用于表示不同日期组件的语法可以在此处找到

更新视图

打开 /views/bookinstance_list.pug 并将 due_back 的所有实例替换为 due_back_formatted

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

而已。 如果您转到侧边栏中的所有图书实例,您现在应该可以看到所有到期日更具吸引力!

作者列表页

作者列表页面需要显示数据库中所有作者的列表,每个作者名称链接到其相关联的作者详细信息页面。 出生日期和死亡日期应列在同一行的姓名之后。

控制器

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

打开 /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 / 。 然后选择所有作者链接。 如果一切都正确设置,页面应该看起来像下面的屏幕截图。

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

类型列表页面挑战!

在本节中,您应该实现自己的类型列表页面。 页面应显示数据库中所有类型的列表,每个类型链接到其关联的详细信息页面。 预期结果的屏幕截图如下所示。

; width:600px;">

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

  1. You will need to edit genre_list() in /controllers/genreController.js
  2. The implementation is almost exactly the same as the author_list() controller.
    • Sort the results by name, in ascending order.
  3. The template to be rendered should be named genre_list.pug.
  4. The template to be rendered should be passed the variables title ('Genre List') and list_genre (the list of genres returned from your Genre.find() callback.

为视图创建 /views/genre_list.pug 文件。 实现一个匹配上面的屏幕截图/要求的布局(这应该有一个非常类似于作者列表视图的结构/格式)。

类型详细信息页面

类别 网页需要显示特定类型实例的信息,使用它(自动生成) _id 字段值标识。 该页面应显示类型名称,以及类型中的所有图书的列表(每个图书都链接到图书的详细信息页面)。

控制器

打开 /controllers/genreController.js ,然后导入文件顶部的 async 图书模块。

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); }
        //Successful, so render
 
        res.render('genre_detail', { title: 'Genre Detail', genre: results.genre, genre_books: results.genre_books } );
    });

};

该方法使用 async.parallel()来并行查询流派名称及其相关联的图书,在(if)两个请求都成功完成时回调渲染页面。

所需流派记录的ID在网址末尾编码,并根据路线定义( / genre /:id )自动提取。 通过请求参数在控制器中访问ID: req.params.id 。 它用于 Genre.findById()以获取当前类型。 它还用于获取在其 genre 字段中具有类型ID的所有 Book 对象: Book.find({\'genre\':req.params.id })

呈现的视图是 genre_detail ,它传递 title genre 和此类别书籍列表的变量( genre_books / code>)。

视图

创建 /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 / 。 然后选择所有类型链接,然后选择一种类型(例如"幻想")。 如果一切设置正确,您的页面应该看起来像下面的屏幕截图。

; width:1000px;">

书详细信息页

书详细信息页需要显示特定 Book 的信息,该信息使用其(自动生成的) _id 字段值标识, 每个相关的副本在库( BookInstance )。 无论我们在何处显示作者,类型或书籍实例,都应将其链接到该项目的相关详细信息页面。

控制器

打开 /controllers/bookController.js 。 找到导出的 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 })
          //.populate('book')
          .exec(callback);
        },
    }, function(err, results) {
        if (err) { return next(err); }
        //Successful, so render
        res.render('book_detail', { title: 'Title', book:  results.book, book_instances: results.book_instance } );
    });
    
};

注意:我们不需要 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 in book.genre
      a(href=val.url) #{val.name}
      |, 
  
  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.text-success 将被编译为< p class ="text-success"> (也可以用Pug编写为 p class ="text-success))。

注意:与书籍相关联的类型列表在模板中实现,如下所示。 这会在与书相关联的每个类别之后添加逗号,这意味着在最后一个项目后面还将有一个逗号。

  p #[strong Genre:] 
    each val in book.genre
      a(href=val.url) #{val.name}
      |, 

没有默认方式获取有关Pug最后一次迭代的信息,因此要删除此逗号,您必须在JavaScript中构建此字符串并将其传递给您的模板(可能作为与当前书相关联的虚拟属性)。

它是什么样子的?

运行应用程序并打开浏览器以 http:// localhost:3000 / 。 然后选择所有图书链接,然后选择一本图书。 如果一切设置正确,您的页面应该看起来像下面的屏幕截图。

; width:1200px;">

作者详细页面

作者详细信息页面需要显示关于指定的 Author 的信息,使用其(自动生成的) _id 字段值以及所有作者相关联的对象。

控制器

打开 /controllers/authorController.js

将以下行添加到文件顶部以导入 async 图书模块(这些是我们的作者详细信息页所需的)。

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

找到导出的 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); }
        //Successful, so render

        res.render('author_detail', { title: 'Author Detail', author: results.author, author_books: results.authors_books } );
    });
    
};

该方法使用 async.parallel()并行查询 Author 及其关联的 Book 实例, )两个请求都成功完成。 该方法与上述流派详细信息页中描述的完全相同。

视图

创建 /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 / 。 然后选择所有图书链接,然后选择一本图书。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。

; width:1000px;">

请注意:作者的外观生命周期日期是丑陋的! 我们将在这个artice的最后挑战中解决这个问题。

BookInstance详细信息页面

BookInstance 详细信息页面需要显示使用其(自动生成的) _id 字段值标识的每个 BookInstance 的信息。 这将包括 Book 名称(作为图书详细信息页的链接)以及记录中的其他信息。

控制器

打开 /controllers/bookinstanceController.js 。 找到导出的 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); }
      //Successful, so render
      res.render('bookinstance_detail', { title: 'Book:', bookinstance:  bookinstance});
    });
    
};

请注意:我们不需要为此控制器添加 async Book

该方法调用 BookInstance.findById()与我们感兴趣的特定作者的ID从URL中提取(使用路由),并通过请求参数在控制器内访问: "font-style:normal; font-weight:normal;"> 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 / 然后选择所有图书实例链接,然后选择其中一个项目。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。

; width:1000px;">

挑战

目前网站上显示的大多数日期 使用默认的JavaScript格式(例如 2016年12月06日15:49:58 GMT + 1100(澳大利亚东部夏令时间) 本文旨在改进作者生命周期信息(死亡/出生日期)和 BookInstance详细信息页面的日期显示的外观,以使用以下格式:2016年12月6日

注意:您可以使用相同的方法来使用图书实例列表(添加虚拟属性 使用作者模型,并使用时刻格式化日期字符串)

满足这一挑战的要求:

  1. Replace the variable due_back with due_back_formatted in the BookInstance detail page.
  2. Update the Author module to add a lifespan virtual property. The lifespan should look like: date_of_birth - date_of_death, where both values have the same date format as BookInstance.due_back_formatted.
  3. Use Author.lifespan in all views where you currently explicitly use date_of_birth and date_of_death.

    概要

    我们现在为网站创建了所有的"只读"网页:一个主页,显示每个模型的实例,以及我们的图书,图书实例,作者和类型的列表和详细页面。 一路上,我们获得了许多关于控制器的基础知识,在使用异步操作时管理流控制,使用 pug 创建视图,使用我们的模型查询数据库,如何将信息传递到模板 您的视图,以及如何创建和扩展模板。 那些完成挑战的人也将学习一些关于使用时刻的日期处理。

    在下一篇文章中,我们将基于我们的知识,创建HTML表单和表单处理代码,以开始修改网站存储的数据。

    也可以看看