Skip to content

Latest commit

 

History

History
594 lines (445 loc) · 32.3 KB

File metadata and controls

594 lines (445 loc) · 32.3 KB

六、控制器和视图模型

到目前为止,我们为应用编写的控制器都是非常基础的。 他们从向客户端发送文本响应的简单任务开始。 在前一章中,我们更新了控制器,使其呈现 HTML 视图并将 HTML 代码发送给客户端,而不是简单的文本响应。 控制器的主要工作是充当一个实体,它持有做出所有必要决策以正确地向客户机呈现响应的逻辑。 在本例中,这意味着检索和/或生成页面完整显示所需的数据。

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

  • 修改控制器,以便它们生成数据模型并将其传递给视图
  • 包括逻辑支持上传和保存图像文件
  • 更新控制器以实际呈现动态 HTML
  • 包括生成网站统计数据的部分助手
  • 通过 jQuery 在 UI 上迭代以包含改进的可用性

控制器

可以将控制器定义为一个实体,它负责操作模型并使用从相应模型接收到的数据启动视图渲染过程。 在我们目前开发的代码中,我们可以看到 express 路由实例用于将函数绑定到相应的路由。 这些函数不过是控制器。

对于我们在路由中创建的每一条路由,以下两个参数都是必要的:

  • 第一个参数是路由本身的字符串,即/images/:image_id
  • 第二个参数是一个控制器函数,当该路由被访问时,它将被执行

对于任何与图像有关的路由,我们都依赖于图像控制器。 同样地,任何与主页有关的路由都依赖于主控制器,以此类推。

我们将采取的步骤来定义我们的应用中的控制器纯粹是组织和基于个人偏好。 我们将控制器创建为模块,这样我们的路由就不会是一堆又长又复杂的意大利面代码。 我们可以简单地将控制器中包含的所有逻辑作为直接在路由中运行的函数,但这将是一种组织混乱,并为稍后需要维护的非常难以阅读的代码留出空间。

由于我们的示例应用相当小,我们目前只有两个控制器:home 和 image。 这些控制器负责为我们的 HTML 页面构建适当的视图模型,并渲染实际的页面。 执行每个页面和构建视图模型所需的任何逻辑都将通过我们的控制器来完成。

视图模型

假设我们的应用中只有一个 HTML 视图,我们将需要将数据附加到该页面,以便以一种方式包含正在呈现的模板,从而将页面的动态区域替换为真实内容。 为此,我们需要生成一个 View 模型。 在呈现过程中,模板引擎将解析模板本身,并寻找一种特殊的语法,该语法表明在运行时,特定的部分应该用 View 模型本身的值替换。 当我们在前一章探索 Handlebars 模板框架时,我们看到了这样的例子。 可以把这看作是一个奇特的 HTML 模板的运行时查找和替换——查找变量,并用发送给模板的 View 模型中存储的值替换它们。

This process happens at the server, and the result is only sent as a response to the HTTP request that our application receives.

View 模型通常只是一个可以传递给模板的单一 JavaScript 对象。 模板包含了我们正确呈现页面所需的所有必要逻辑。 模板引擎的任务是通过处理带有相关模型的模板来生成相应的 HTML。 页面的 View 模型通常包含呈现该页面特定于内容的部分所需的所有数据。 以我们的应用为例,特定图像页面的 View 模型可能包含图像的标题、描述、显示图像所需的信息和各种统计信息,例如点赞数量、视图和评论集合。 视图模型可以是简单的,也可以是复杂的。

术语视图模型在这里指的是模型的数据形式,当通过任何模板框架呈现 HTML 时,该数据形式将绑定到模板。

更新主控制器

如果你看看我们当前的家庭控制器(controllers/home.js),你可以看到 index 函数几乎没有任何代码:

res.render('index');

我们要做的第一件事是使用示例数据构建一个基本的 View 模型,这样我们就可以看到我们的 View 模型是如何工作的。 用以下更新的代码替换单个的res.render调用:

const ViewModel = {
        images: [
            images: [{
                uniqueId: 1,
                title: 'Sample Image 1',
                description: '',
                filename: 'sample1.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now()
            }, {
                uniqueId: 2,
                title: 'Sample Image 2',
                description: '',
                filename: 'sample2.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now()
            }, {
                uniqueId: 3,
                title: 'Sample Image 3',
                description: '',
                filename: 'sample3.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now()
            }, {
                uniqueId: 4,
                title: 'Sample Image 4',
                description: '',
                filename: 'sample4.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now()
            }]
        };
res.render('index', ViewModel);

在前面的代码中,我们构建了一个基本的 JavaScript 对象集合。 我们声明的常数名为ViewModel,但是这个常数的名称实际上并不重要,可以是您想要的任何名称。 const ViewModel是一个对象,它包含一个名为images的属性,该属性本身是一个数组。

images数组包含四个样本图像,每个图像都有一些基本属性——最明显的属性是在决定每个图像需要什么样的信息时决定的。 集合中的每个图像具有uniqueIdtitledescriptionfilenameViewslikes counttimestamp属性。

设置了ViewModel后,只需将其作为第二个参数传递给res.render调用。 在呈现View的同时执行此操作可以使其中的数据对视图本身可用。 现在,如果您回想一下我们为 homeindex.HandlebarsView 编写的一些模板代码,我们有一个{{#each images}}循环,它遍历传递给模板的 View 模型的图像集合中的每个图像。 再看一下我们创建的 View 模型,我们看到它只有一个名为images的属性。 Handlebars 循环中的 HTML 代码将特别引用images数组中的每个图像的uniqueIdfilenametitle属性。

将更改保存到主控制器,再次启动应用,并导航到http://localhost:3300。 你应该在最新图片部分看到现在出现在主页上的四张图片(尽管,正如你在下面的截图中看到的,图像仍然是破碎的,因为我们实际上没有创建任何图像文件):

主页有一个相当简单的控制器和视图模型,您可能已经注意到侧边栏仍然是空的。 我们将在本章的稍后部分讨论侧边栏。

更新图像控制器

让我们为图像页面创建控制器和 View 模型。 图像的控制器稍微复杂一些,因为我们将编写逻辑来处理通过主页上的表单上传和保存图像文件。

显示一个图像

图像控制器中的index函数看起来与主控制器中的index函数几乎相同。 唯一的区别是,我们将为单个图像构建一个ViewModel数组,而不是生成images数组。 然而,这个图片的ViewModel将比主页上的图片包含更多的信息,因为我们正在构建一个页面,它将呈现一个更详细的图片视图(相对于主页上的缩略图集合)。 其中最值得注意的是图像的注释数组。

再看看我们的controllers/image.js文件中的原始index函数,我们可以看到简单的res.render代码行:

res.render('image');

我们想用ViewModel和更新后的res.render语句替换这一行,代码如下:

const ViewModel = {
    image: {
        uniqueId: 1,
        title: 'Sample Image 1',
        description: 'This is a sample.',
        filename: 'sample1.jpg',
        Views: 0,
        likes: 0,
        timestamp: Date.now()
    },
    comments: [{
        image_id: 1,
        email: 'test@testing.com',
        name: 'Test Tester',
        gravatar: 'http://lorempixel.com/75/75/animals/1',
        comment: 'This is a test comment...',
        timestamp: Date.now()
    }, {
        image_id: 1,
        email: 'test@testing.com',
        name: 'Test Tester',
        gravatar: 'http://lorempixel.com/75/75/animals/2',
        comment: 'Another followup comment!',
        timestamp: Date.now()
    }]
};
res.render('image', ViewModel);

这里,我们再次声明了一个新的ViewModel常量——这一次使用了一个包含单个图像属性的image属性。 除了image属性之外,还有一个comments属性,它是一个包含comment对象的数组。 您可以看到,每个注释都具有特定于每个图像的注释的各种属性。 这个 JavaScript 对象实际上是一个很好的预览,当我们将应用连接到 MongoDB 时,我们的真实数据最终会是什么样子。

在构建示例image对象及其注释集合之后,我们将其传递给res.render调用,从而将这个新的ViewModel直接发送给图像的 Handlebars 模板。 同样,如果查看image.Handlebars文件中的 HTML 代码,可以看到ViewModel的每个属性显示在哪里。

再次,让我们运行应用,并确保我们的图像页面显示正确:

$ node server.js

一旦应用运行,并在浏览器中启动它,点击主页的最新图片部分列出的任何图片。

这将带你到一个单独的图像页面,在那里你将看到如下截图所示的页面:

注意,标题、描述、喜欢、视图和时间戳(为便于用户阅读而转换为不同的格式)现在都出现在页面上。 此外,你还可以看到图片附近列出了一些评论!

上传一个图像

我们需要在图像控制器中实现的下一个特性是当用户在主页上提交一个图像 Upload image 时处理的逻辑。 尽管表单位于应用的主页上,但我们还是决定将处理上传的逻辑放置在图像控制器中,因为从逻辑上讲,这是最有意义的(因为该功能主要与图像有关,而不是主页本身)。 这纯粹是个人决定,你可以把逻辑放在任何你喜欢的地方。

您应该注意,主页上用于表单的 HTML 将其操作设置为/images,其方法设置为post。 这与我们之前设置的路由完全匹配,在那里我们监听post/images路由并调用图像控制器的create函数。

图像控制器中的create函数有以下几个关键职责:

  • 它应该为图像生成一个唯一的文件名,它也将作为标识符
  • 它应该将上传的文件保存到filesystem中,并确保它是一个image文件
  • 最后,一旦任务完成,它应该将控件重定向到img/image_id路由,以显示实际的图像

因为我们将在这个函数中使用filesystem,所以我们需要包含 Node.js 核心模块集合中的几个模块,特别是filesystem(fs)和path(path)模块。

在开始为 Upload Image 部分添加必要的代码之前,我们需要对应用的配置进行一个小修复。 此外,我们还需要在配置文件中添加一个额外的模块来支持文件上传,即multer。 使用以下命令将其作为依赖项添加到我们的应用中:

npm install multer --save

现在,通过server/configure.jsrequire转到配置文件:

multer = require('multer');

您可以将它放在文件中最初需要的模块下。 然后,在 Handlebars 引擎方法下插入以下代码片段:

app.use(multer({
    dest: path.join(__dirname, 'public/upload/temp')
}));

现在,我们的上传操作将正常工作,正如预期的那样。

让我们首先编辑controllers/image.js文件,并在文件的最顶端插入两个新的 require 语句:

const fs = require('fs'),
path = require('path');

接下来,取create函数的原始代码:

res.send('The image:create POST controller');
res.redirect('/images/1');

用以下代码替换此原始代码:

const saveImage = function() {
// to do...
};
saveImage();

这里,我们创建了一个名为saveImage的函数,并在声明它之后立即执行它。 这看起来可能有点奇怪,但当我们在下一章实现数据库调用时,这样做的原因就会很清楚了。 主要原因是我们将反复调用saveImage以确保我们生成的唯一标识符实际上是唯一的,并且不作为先前保存的图像标识符已经存在于数据库中。

让我们回顾一下将插入saveImage函数(替换// to do...注释)的代码细分。 我将覆盖这个函数的每一行代码,然后在最后给你整个代码块:

let possible = 'abcdefghijklmnopqrstuvwxyz0123456789',
imgUrl = '';

我们需要生成一个随机的六位字母数字字符串来表示图像的唯一标识符。 这个标识符将以类似的方式工作,以其他网站提供独特链接的短 url(例如,位)。 ly)。 要做到这一点,我们将首先提供一个可能的字符字符串,在生成随机字符串时可以使用:

for(let i=0; i < 6; i+=1) {
    imgUrl += possible.charAt(Math.floor(Math.random() *
possible.length));
}

然后,循环 6 次,并从可能的字符字符串中随机抽取一个字符,将其附加到每个循环中。 在这个for循环结束时,我们应该得到一个由六个随机字母和/或数字组成的字符串,例如a8bd73:

const tempPath = req.files.file.path,
ext = path.extname(req.files.file.name).toLowerCase(),
targetPath = path.resolve(`./public/upload/${imgUrl}${ ext}`);

在这里,我们声明了三个常量:已上载文件的临时存储位置、已上载文件的文件扩展名(例如,.png.jpg等)以及已上载图像最终驻留的目的地。

对于后两个变量,我们将使用 path node 模块,该模块在处理文件名和路径以及从文件(例如文件扩展名)获取信息时工作得很好。 接下来,我们将图片从它的临时上传路径移动到它的最终目的地:

if (ext === '.png' || ext === '.jpg' || ext === '.jpeg' || ext ===
    '.gif') {
    fs.rename(tempPath, targetPath, (err) => {
        if (err) throw err;
        res.redirect(`/images/ ${imgUrl}`);
    });
} else {
    fs.unlink(tempPath, () => {
        if (err) throw err;
        res.json(500, { error: 'Only image files are allowed.' });
    });
}

前面的代码执行一些验证。 具体来说,它进行检查以确保上传的文件扩展名与允许的扩展名列表(即已知的图像文件类型)匹配。 如果上传了有效的图像文件,则通过filesystemrename函数将其从temp文件夹中移出。 注意filesystem(fs)rename函数如何接受三个参数:原始文件、新文件和一个callback函数。

callback函数在rename完成后执行。 如果 Node 不工作这种方式(总是依靠callback功能),很有可能您的代码将执行后的执行rename功能,尝试对一个文件不存在(即rename函数甚至不完成工作)。 使用callback函数,我们有效地告诉 Node,一旦文件的rename完成,文件准备好并放置在它应该放置的地方,然后它可以执行callback函数中的代码。

else条件,遵循处理情况上传文件时是无效的(也就是说,不是一个图像),所以我们叫 fs 的unlink功能模块,它将删除原始文件(从temp目录上传到),然后发送一个简单的JSON 500一条错误消息。

下面是完整的saveImage函数(同样,下面的代码将替换前面的// to do...):

const possible = 'abcdefghijklmnopqrstuvwxyz0123456789',
    imgUrl = '';
for (let i = 0; i < 6; i += 1) {
    imgUrl += possible.charAt(Math.floor(Math.random() *
        possible.length));
}
const tempPath = req.files.file.path,
    ext = path.extname(req.files.file.name).toLowerCase(),
    targetPath = path.resolve(`./public/upload/${ imgUrl }${ ext }`);
if (ext === '.png' || ext === '.jpg' || ext === '.jpeg' || ext ===
    '.gif') {
    fs.rename(tempPath, targetPath, (err) => {
        if (err) throw err;
        res.redirect('/images/${ext}');
    });
} else {
    fs.unlink(tempPath, () => {
        if (err) throw err;
        res.json(500, { error: 'Only image files are allowed.' });
    });
}

有了这个新代码,我们现在就可以通过主页上的表单成功上传图像文件了。 尝试一下,启动应用并在浏览器中打开它。 一旦那里,点击浏览按钮的主要形式,并选择一个image文件从您的计算机。 如果成功,image文件应该以一个新的随机文件名存在于项目的public/upload文件夹中。

确保在项目中创建了public/upload文件夹,否则当您试图将文件写入不存在的位置时会出现运行时错误。 可能需要在文件夹上设置写权限,这取决于您的操作系统和安全访问。

当上传表单完成并且create控制器函数完成它的工作后,它会将您重定向到已上传图像的单个图像页面。

可重用代码的助手

到目前为止,我们呈现的每个页面都完美地显示了它们的ViewModel数据,但是那个烦人的边栏仍然是空白的。 我们将通过为侧边栏内容创建一些模块来解决这个问题,将它们实现为辅助模块。 应用的各个部分将重复使用这些 helper 模块,它们不一定属于controller文件夹或server文件夹。 因此,我们将创建一个名为helpers的新家,并将这些模块存储在那里。

由于我们只是将临时 fixture 数据加载到我们的ViewModels中,一旦我们实现 MongoDB,我们在helperscontrollers中设置的数据都将被实际的实时数据所取代; 我们将在下一章做这件事。

侧栏模块

首先,我们将为整个侧栏创建一个模块。 这个模块将负责调用多个其他模块来填充侧边栏的每个部分ViewModel。 由于我们将使用专门为侧边栏准备的数据填充每个页面自己的ViewModel,所以侧边栏模块的函数将接受原始的ViewModel作为参数。 这样我们就可以为每个页面添加数据到现有的ViewModel

这里,我们将附加一个边栏属性(这是一个 JavaScript 对象),它包含边栏每个部分的属性。

要开始,首先创建一个名为helpers/sidebar.js的文件,并插入以下代码:

const Stats = require('./stats'),
    Images = require('./images'),
    Comments = require('./comments');
module.exports = (ViewModel, callback) => {
    ViewModel.sidebar = {
        stats: Stats(),
        popular: Images.popular(),
        comments: Comments.newest()
    };
    callback(ViewModel);
};

在前面的代码中,您可以看到首先需要为侧栏的每个部分提供一个模块。 对于任何显示侧边栏的给定页面,现有的ViewModel是该函数的第一个参数。 我们向ViewModel添加了一个侧栏属性,并通过调用侧栏每个部分的模块来为每个属性设置值。 最后,我们执行了作为第二个参数传递给侧栏模块的callback。 这个callback是一个匿名函数,我们将使用它来执行 HTML 页面的呈现。

让我们更新主控制器和图像控制器,以包含对侧栏模块的调用,并将每个页面的 HTML 模板呈现延迟到侧栏模块的callback

编辑controllers/home.js并考虑以下代码行:

res.render('index', ViewModel);

用下面的代码块替换它:

sidebar(ViewModel, (ViewModel) => {
    res.render('index', ViewModel);
});

controllers/image.js文件做完全相同的更改,将index替换为image:

sidebar(ViewModel, (ViewModel) => {
    res.render('image', ViewModel);
});

同样,请注意我们如何执行侧栏模块,并将现有的ViewModel作为第一个参数传递,将一个基本的匿名函数作为callback作为第二个参数传递。 我们所做的就是等待sidebar填充ViewModel完成后才渲染视图的 HTML。 这是因为 Node.js 的异步特性。 假设我们以以下方式编写代码:

sidebar(ViewModel);
res.render('index', ViewModel);

这里,很可能在sidebar完成任何工作之前,res.render语句就会执行。 当我们在下一章介绍 MongoDB 时,这将变得非常重要。 此外,由于我们现在在每个控制器中使用sidebar模块,通过包含以下代码,确保在两个控制器的顶部都使用require模块:

const sidebar = require('../helpers/sidebar');

现在我们的sidebar模块已经完成,并且正在从两个控制器调用它,让我们通过创建所需的每个子模块来完成sidebar

统计模块

stats 模块将显示一些关于我们应用的随机统计数据。具体来说,它将显示整个网站的imagescommentsViewslikes的总数。

创建helpers/stats.js文件并插入以下代码:

module.exports = () => {
    const stats = {
        images: 0,
        comments: 0,
        Views: 0,
        likes: 0
    };
    return stats;
};

这个模块非常基本,它所做的就是创建一个标准的 JavaScript 对象,其中包含一些用于各种统计数据的属性,每个属性最初都设置为 0。

图像模块

images模块负责返回各种图像集合。 最初,我们将创建一个popular功能,用于返回网站上最受欢迎的图像集合。 最初,这个集合只是包含样本 fixture 数据的image对象数组。

创建helpers/images.js文件,并插入以下代码:

module.exports = {
    popular() {
        let images = [{
                uniqueId: 1,
                title: 'Sample Image 1',
                description: '',
                filename: 'sample1.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now()
            },
            {
                uniqueId: 2,
                title: 'Sample Image 2',
                description: '',
                filename: 'sample2.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now()
            },
            {
                uniqueId: 3,
                title: 'Sample Image 3',
                description: '',
                filename: 'sample3.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now()
            },
            {
                uniqueId: 4,
                title: 'Sample Image 4',
                description: '',
                filename: 'sample4.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now()
            }
        ];
        return images;
    }
};

评论模块

与图像的helper模块类似,comments模块将返回发布到站点的最新评论的集合。 一个想法特别感兴趣的是,每个评论也有一个图片,这样每个评论的实际图像时可以显示为缩略图显示的评论列表(否则,我们失去了上下文当我们看到一个随机的评论列表没有相关图片)。

创建helpers/comments.js文件,并插入以下代码:

module.exports = {
    newest() {
        let comments = [{
            image_id: 1,
            email: 'test@testing.com',
            name: 'Test Tester',
            gravatar: 'http://lorempixel.com/75/75/animals/1',
            comment: 'This is a test comment...',
            timestamp: Date.now(),
            image: {
                uniqueId: 1,
                title: 'Sample Image 1',
                description: '',
                filename: 'sample1.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now
            }
        }, {
            image_id: 1,
            email: 'test@testing.com',
            name: Test Tester ',
            gravatar: 'http://lorempixel.com/75/75/animals/2',
            comment: 'Another followup comment!',
            timestamp: Date.now(),
            image: {
                uniqueId: 1,
                title: 'Sample Image 1',
                description: '',
                filename: 'sample1.jpg',
                Views: 0,
                likes: 0,
                timestamp: Date.now
            }
        }];
        return comments;
    }
};

再一次,这只是一个基本的 JavaScript 数组对象的一些属性为每个评论,其中一个是一个实际的图像及其属性(image属性看起来应该很熟悉,因为这是一样的项目形象的helper模块)。

测试侧栏实现

既然已经完成了我们的sidebar模块以及它与各个statsimagescomments相关的子模块,现在是时候让我们的应用再次运行测试了。 启动 Node 服务器,并在浏览器中打开应用。

现在您应该在主页和图像着陆页面上看到包含完整内容的sidebar

在 UI 上迭代

既然我们的应用工作得相当好,并且可以与它实际交互,那么现在是时候回过头来看看我们可能需要改进的一些领域了。

一个区域是图像页面上的 Post Comment 表单。 我认为这个表单没有必要总是可见的,但是只有当有人真的想发表评论时,它才应该可用。

此外,我希望 Like 按钮不必向服务器提交完整的表单,从而导致整个页面重新加载(就像主页上的表单在上传图像时所做的那样)。 我们将使用 jQuery 向服务器提交一个 AJAX 调用来处理 likes,并实时发送和检索数据,而无需重新加载页面。

为了进行这些调整,我们需要在页面上引入少量的 JavaScript 来添加一点交互性。 为了让事情变得更简单,我们将使用流行的 jQuery JavaScript 库来轻松地创建交互式特性。

jQuery 已经存在了很多年,并且在前端开发中非常流行。 它允许您操作文档对象模型(DOM),即任何页面的 HTML 结构,这将在下一节中看到。 您可以在http://jquery.com了解更多关于 jQuery 的知识。

您可能没有注意到,在为main.Handlebars布局文件提供的 HTML 代码中,jQuery 已经作为一个外部的script标签(引用了托管在 CDN 上的 jQuery)包含在其中。 此外,还包括一个本地的scripts.js标签,我们将在其中放置自定义 jQuery JavaScript 代码,用于对 UI 进行更改。 当你看到main.Handlebars的底部,你可以看到以下代码:

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script type="text/javascript" src="/public/js/scripts.js"></script>

第一个脚本标记指向谷歌的代码内容交付网络(CDN),这意味着我们不必担心用我们的代码托管该文件。 然而,第二个文件是我们自己的文件,因此我们需要确保它存在。

CDN 是一种从全球分布的缓存服务器网络中传递文件的方法。 这意味着,一般来说,网络访问者经常下载的文件(比如 jQuery)可以通过一个更接近本地的下载源以及改进的缓存来更快地加载。 例如,如果多个网站使用相同的 CDN URL 来托管 jQuery,那么很有可能访问您网站的访问者在访问之前不相关的网站时已经下载了 jQuery。 因此,你的网站将加载得更快。

创建public/js/scripts.js文件,并插入以下代码:

$(function(){
// to do...
});

这是一个标准的代码块,您几乎每次使用 jQuery 时都会看到它。 这段代码所做的是在 jQuery 的$()包装器中执行一个匿名函数,这是编写以下代码的简写:

$(document).ready(function(){
// to do...
});

前面的代码基本上意味着callback函数将等待页面完全加载完毕并准备好后才执行。 这很重要,因为我们不想将 UI 事件处理程序和/或效果应用到 DOM 元素上,因为页面仍在加载中,这些元素实际上还不存在。 这也是为什么main.Handlebars布局文件中的脚本标记位于页面的最后几行; 这是为了确保它们是最后加载的,以确保文档已经完全下载并准备好进行操作。

首先,让我们解决post-comment功能。 我们希望在默认情况下隐藏评论表单,然后只有当用户单击图片下的 Post comment 按钮(Like 按钮的右侧)时才显示它。 在存在// to do...注释的callback函数中插入以下代码:

$('#post-comment').hide();
$('#btn-comment').on('click', function(event) {
    event.preventDefault();
    $('#post-comment').show();
});

第一行代码在具有post-commentID 的 HTML div 上执行hide函数。 然后,我们立即对 ID 为btn-comment的 HTML 按钮应用一个事件处理程序。 我们应用的事件处理程序是针对onClick的,因为我们希望它在用户单击该按钮时执行我们提供的匿名函数。 该函数只是阻止默认行为(该特定元素的默认行为; 在本例中是一个按钮),然后调用 show jQuery 函数,该函数将显示以前隐藏的post-commentdiv。 event.preventDefault()部分非常重要,因为如果我们不包含它,那么点击按钮的操作将执行浏览器所期望的操作,同时尝试执行我们的自定义 JavaScript 函数。 如果我们不包含这一点,我们的 UI 很可能会以不理想的方式运行。 一个很好的例子是,如果您想覆盖标准 HTML 链接的默认行为,您可以指定一个onClick事件处理程序并执行任何您想要的操作。 然而,如果你不执行event.preventDefault(),浏览器会将用户发送到 HREF 的链接,不管你的代码试图做什么。

现在,让我们添加一些代码来处理 Like 按钮功能。 我们将使用 jQuery 的.on函数为这个按钮添加一个事件处理程序,就像我们为 Post Comment 按钮所做的一样。 在前面添加的代码之后,将这个额外的代码块插入到ready语句中:

$('#btn-like').on('click', function(event) {
    event.preventDefault();
    let imgId = $(this).data('id');
    $.post('/images/' + imgId + '/like').done(function(data) {
        $('.likes-count').text(data.likes);
    });
});

前面的代码将onClick事件处理程序附加到btn-like按钮。 事件处理程序首先从 Like 按钮中检索data('id')属性(通过image.HandlebarsHTML 模板代码和ViewModel分配),然后执行一个 jQuery AJAX 发布到/images/:image_id/like路由。 从我们的 Nodeserver/routes.js文件中收回以下行:

app.post('/images/:image_id/like', image.like);

一旦完成 AJAX 调用,另一个匿名的callback函数将会执行更改 HTML 元素的文本与likes-count类,取而代之从 AJAX 调用返回的数据,在这种情况下,喜欢更新的总数(通常情况下,这将是不管以前+ 1)。

为了测试此功能,我们需要在image控制器中的like函数中实现一些 fixture 数据。 编辑controllers/image.js,在like函数中,用以下代码替换现有的res.send函数调用:

like(req, res) {
    res.json({ likes: 1 });
},

这段代码所做的就是用一个简单的对象返回 JSON 给客户端,该对象包含一个值为1的 likes 属性。 在下一章中,当我们将 MongoDB 引入应用时,我们将更新这段代码,以实际增加喜欢的计数并返回喜欢图像的真实值。

完成所有这些更改后,您应该能够重新启动 Node 服务器并在浏览器中打开网站。 点击主页上任意一张图片即可查看图片页面,点击喜欢按钮即可看到图片从0变为1。 别忘了看看别致的新发布评论按钮; 点击这个应该会显示评论表单。

总结

在本章的开始,我们通过应用在浏览器中显示了一些基本的 HTML 页面,但它们不包含任何内容和逻辑。 我们实现了每个控制器的逻辑,并讨论了 ViewModel 以及如何用内容填充页面。

除了通过 ViewModel 显示页面上的内容外,我们还实现了将图像文件上传和保存到本地filesystem的代码。

我们对 UI 做了一些微调,通过使用 jQuery 显示评论表单,并使用 AJAX 来跟踪 likes,而不是依赖于整个页面回发。

现在我们已经为 viewmodel 和控制器奠定了基础,让我们使用 MongoDB 将它们结合在一起,并开始处理真实的数据。 在下一章中,我们将再次更新控制器,这次实现从 MongoDB 服务器读取并保存数据的逻辑。