Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Express
Code

使用 ExpressJS 构建完整的 MVC 网站

by
Difficulty:IntermediateLength:LongLanguages:

Chinese (Simplified) (中文(简体)) translation by Fuhuan (you can also view the original English article)

在本文中,我们将构建一个包含前端客户端的完整网站,以及用于管理网站内容的控制面板。 正如你可能猜到的,应用程序的最终工作版本包含许多不同的文件。 我按照开发过程一步一步地编写本教程,但是我没有包含每个文件,因为这会使这篇文章变得非常冗长乏味。 但是,源代码可以在 GitHub 上找到,我强烈建议你看一下。


介绍

Express 是 Node 的最佳框架之一。 它有强大的支持和一些有用的功能。 那里有很多很棒的文章,涵盖了所有的基础知识。 但是,这次我想深入挖掘并分享我创建完整网站的工作流程。 通常,本文不仅适用于 Express,还适用于与 Node 开发人员可用的其他一些优秀工具结合使用。

我假设你熟悉 Nodejs,将它安装在你的系统上,并且你可能已经使用它构建了一些应用程序。

核心是连接。 这是一个中间件框架,它带有很多有用的东西。 如果你想知道究竟什么是中间件,这里有一个简单的例子:

中间件基本上是一个接受请求和响应对象以及下一个功能的函数。 每个中间件都可以决定使用响应对象进行响应,或者通过调用下一个回调将流传递给下一个函数。 在上面的示例中,如果在第二个中间件中删除 next()方法调用,则 hello world 字符串将永远不会发送到浏览器。 一般来说,这就是 Express 的工作方式。 有一些预定义的中间件,当然,这可以为你节省大量时间。 例如,Body 解析器解析请求主体并支持 application / json,application / x-www-form-urlencoded 和 multipart / form-data。 或 Cookie 解析器,它解析 cookie 头并使用由 cookie 名称键入的对象填充 req.cookies。

Express 实际上包装了 Connect 并在其周围添加了一些新功能。 例如,路由逻辑,这使得流程更加顺畅。 以下是处理 GET 请求的示例:


建立

设置 Express 有两种方法。 第一个是将它放在你的 package.json 文件中并运行 npm install(有一个笑话,npm 意味着没问题的人 :))。

框架的代码将放在 node_modules 中,你将能够创建它的实例。 但是,我更喜欢使用命令行工具的替代选项。 只需使用 npm install -g express 全局安装 Express 。 通过这样做,你现在拥有了一个全新的 CLI 工具。 例如,如果你运行:

Express 将创建一个应用程序框架,其中包含一些已为你配置的内容。 以下是 express(1)命令的用法选项:

正如你所看到的,只有几个选项,但对我来说它们已经足够了。 通常我使用较少的 CSS 预处理器和 hogan 作为模板引擎。 在这个例子中,我们还需要会话支持,所以 --sessions 参数解决了这个问题。 上面的命令完成后,我们的项目如下所示:

如果你查看 package.json 文件,你将看到我们需要的所有依赖项都添加到此处。 虽然它们尚未安装。 为此,只需运行 npm install,然后会弹出 node_modules 文件夹。

我意识到上述方法并不总是合适的。 你可能希望将路由处理程序放在另一个目录或类似的东西中。 但是,正如你将在接下来的几章中看到的那样,我将对已经生成的结构进行更改,这很容易做到。 因此,你应该将 express(1)命令视为样板生成器。


敏捷开发

在本教程中,我设计了一个名为 FastDelivery 的虚假公司的简单网站。 这是完整设计的屏幕截图:

site

在本教程结束时,我们将拥有一个完整的 Web 应用程序,其中包含一个可用的控制面板。 我们的想法是在不同的限制区域内管理网站的每个部分。 布局是在 Photoshop 中创建的,并切成 CSS(less)和 HTML(hogan)文件。 现在,我不打算介绍切片过程,因为它不是本文的主题,但如果你对此有任何疑问,请不要犹豫。 切片后,我们有以下文件和 app 结构:

以下是我们要管理的网站元素列表:

  • 主页(中间的横幅 - 标题和文字)
  • 博客(添加,删除和编辑文章)
  • 服务页面
  • 职业页面
  • 联系页面

组态

在开始真正的实施之前,我们必须做一些事情。 配置设置就是其中之一。 让我们假设我们的小站点应该部署到三个不同的位置 - 本地服务器,登台服务器和生产服务器。 当然,每个环境的设置都不同,我们应该实现一个足够灵活的机制。 如你所知,每个节点脚本都作为控制台程序运行。 因此,我们可以轻松发送将定义当前环境的命令行参数。 我将该部分包装在一个单独的模块中,以便稍后为其编写测试。 这是 /config/index.js 文件:

目前只有两种设置 - 模式和端口。 正如你可能猜到的,应用程序为不同的服务器使用不同的端口。 这就是为什么我们必须在 app.js 中更新站点的入口点。

要在配置之间切换,只需在最后添加环境。 例如:

会产生:

现在我们将所有设置放在一个地方,并且它们易于管理。


测试

我是 TDD 的忠实粉丝。 我将尝试涵盖本文中使用的所有基类。 当然,对绝对一切进行测试会使写作时间过长,但总的来说,这就是在创建自己的应用程序时应该如何进行。 我最喜欢的测试框架之一是茉莉花。 当然它在 npm 注册表中可用:

让我们创建一个测试目录来保存我们的测试。 我们要检查的第一件事是配置设置。 spec 文件必须以.spec.js 结尾,因此该文件应该被称为 config.spec.js。

运行 jasmine-node ./tests 你应该看到以下内容:

这一次,我首先编写了实现,然后编写了第二个测试。 这并不是 TDD 的做事方式,但在接下来的几章中我会做相反的事情。

我强烈建议花大量时间编写测试。 没有比完全测试的应用程序更好的了。

几年前,我意识到一些非常重要的东西,它可以帮助你制作更好的程序。 每次开始编写新类,新模块或新逻辑时,请问自己:

我该怎么测试呢?

这个问题的答案将帮助你更有效地编写代码,创建更好的 API,并将所有内容放入分离良好的块中。 你不能为意大利代码编写测试。 例如,在上面的配置文件中(/config/index.js) 我添加了在模块的构造函数中发送模式的可能性。 你可能想知道,当主要想法是从命令行参数获取模式时,为什么要这样做? 这很简单...... 因为我需要测试它。 让我们假设一个月后我需要检查生产配置中的某些内容,但是节点脚本是使用 staging 参数运行的。 没有那么小的改进,我将无法做出这种改变。 之前的一个小步骤现在实际上可以防止将来出现问题。


数据库

由于我们正在构建一个动态网站,我们需要一个数据库来存储我们的数据。 我选择使用 mongodb 作为本教程。 Mongo 是一个 NoSQL 文档数据库。 可以在此处找到安装说明,因为我是 Windows 用户,所以我遵循 Windows 安装。 完成安装后,运行 MongoDB 守护程序,默认情况下侦听端口 27017。 因此,理论上,我们应该能够连接到此端口并与 mongodb 服务器通信。 要从节点脚本执行此操作,我们需要一个 mongodb 模块 / 驱动程序。 如果你下载了本教程的源文件,则该模块已添加到 package.json 文件中。 如果没有,只需将 “mongodb”:“1.3.10” 添加到你的依赖项并运行 npm install。

接下来,我们将编写一个测试,检查是否有 mongodb 服务器在运行。 mongodb 客户端的.connect 方法中的回调接收 db 对象。

mongodb 客户端的.connect 方法中的回调接收 db 对象。 我们稍后将使用它来管理我们的数据,这意味着我们需要在模型中访问它。 每当我们必须向数据库发出请求时,创建一个新的 MongoClient 对象并不是一个好主意。 这就是为什么我在 connect 函数的回调中移动了快速服务器的运行:

更好的是,由于我们有配置设置,最好将 mongodb 主机和端口放在那里,然后将连接 URL 更改为:

密切关注中间件:attachDB,我在调用 http.createServer 函数之前添加了它。 由于这一点添加,我们将填充请求对象的.db 属性。 好消息是我们可以在路径定义期间附加多个功能。 例如:

因此,Express 会事先调用 attachDB 来到达我们的路由处理程序。 一旦发生这种情况,请求对象将具有.db 属性,我们可以使用它来访问数据库。


MVC

我们都知道 MVC 模式。 问题是这如何适用于 Express。 或多或少,这是一个解释问题。 在接下来的几章中,我将创建模块,它们充当模型,视图和控制器。

模型

该模型将处理我们的应用程序中的数据。 它应该可以访问 MongoClient 返回的 db 对象。 我们的模型还应该有一个扩展它的方法,因为我们可能想要创建不同类型的模型。 例如,我们可能需要 BlogModel 或 ContactsModel。 所以我们需要编写一个新的规范:/tests/base.model.spec.js,以便测试这两个模型的功能。 请记住,通过在开始编写实现之前定义这些功能,我们可以保证我们的模块将只执行我们希望它执行的操作。

我决定传递一个模型对象,而不是一个真正的 db 对象。 那是因为稍后,我可能想要测试一些具体的东西,这取决于来自数据库的信息。 extend 方法的实现有点棘手,因为我们必须更改 module.exports 的原型,但仍保留原始构造函数。

extend 方法的实现有点棘手,因为我们必须更改 module.exports 的原型,但仍保留原始构造函数。 值得庆幸的是,我们已经编写了一个很好的测试,证明我们的代码可以工作。 通过上述内容的版本如下所示:

这里有两种辅助方法。 db 对象的 setter 和数据库集合的 getter 。

视图

视图将向屏幕呈现信息。 本质上,视图是一个向浏览器发送响应的类。 Express 提供了一种简短的方法:

响应对象是一个包装器,它有一个很好的 API,使我们的生活更轻松。 但是,我更愿意创建一个封装此功能的模块 。默认视图目录将更改为模板,并将创建一个新模板,它将承载基本视图类。 这一小改变现在需要另一个改变。 我们应该通知 Express 我们的模板文件现在放在另一个目录中:

首先,我将定义我需要的内容,编写测试,然后编写实现。 我们需要一个符合以下规则的模块:

  • 其构造函数应该接收响应对象和模板名称。
  • 它应该有一个接受数据对象的 render 方法。
  • 它应该是可扩展的。

你可能想知道我为什么要扩展 View 类。 是不是只是调用 response.render 方法? 实际上,在某些情况下,你需要发送不同的标头或以某种方式操纵响应对象。 例如,提供 JSON 数据:

不要每次都这样做,最好有一个 HTMLView 类和一个 JSONView 类。 甚至是用于将 XML 数据发送到浏览器的 XMLView 类。 如果你建立一个大型网站,那就更好了,包装这些功能,而不是一遍又一遍地复制粘贴相同的代码。

这是 /views/Base.js 的规范:

为了测试渲染,我不得不创建一个模型。 在这种情况下,我创建了一个模仿 Express 的响应对象的对象。 在测试的第二部分中,我创建了另一个 View 类,它继承了基类并应用了自定义 render 方法。 这是 /views/Base.js 类。

现在我们在 tests 目录中有三个规范,如果你运行 jasmine-node ./tests,结果应该是:

调节器

还记得路线以及它们的定义方式吗?

在 “/” 后途径,其在上面的例子中,实际上是控制器。 它只是一个接受请求,响应和下一个的中间件功能。

以上是你的控制器应该在 Express 的上下文中的外观。 该快递(1)命令行工具创建一个新的目录路径,但在我们的情况下,更好地为它被命名为控制器,所以我改变,以反映这一命名方案。

因为我们不只是构建一个非常小的应用程序,所以如果我们创建一个基类,我们可以扩展它将是明智的。 如果我们需要将某种功能传递给所有控制器,那么这个基类将是最佳选择。 再次,我将首先编写测试,所以让我们定义我们需要的东西:

  • 它应该有一个 extend 方法,它接受一个对象并返回一个新的子实例
  • 子实例应该有一个 run 方法,这是旧的中间件函数
  • 应该有一个 name 属性,用于标识控制器
  • 我们应该能够基于类创建独立的对象

所以现在只是一些事情,但我们可能会在以后添加更多功能。 测试看起来像这样:

这是 /controllers/Base.js 的实现:

当然,每个子类都应该定义自己的 run 方法以及它自己的逻辑。


FastDelivery 网站

好的,我们为 MVC 架构提供了一套很好的类,我们已经用测试覆盖了新创建的模块。 现在我们准备继续使用我们的假公司 FastDelivery 的网站。 让我们假设该网站有两个部分 - 前端和管理面板。 前端将用于向最终用户显示数据库中写入的信息。 管理面板将用于管理该数据。 让我们从我们的管理(控制)面板开始。

控制面板

让我们首先创建一个简单的控制器,它将作为管理页面。 /controllers/Admin.js 文件:

通过为控制器和视图使用预先编写的基类,我们可以轻松地为控制面板创建入口点。 该视图类接受一个模板文件的名称。 根据上面的代码,该文件应该被称为 admin.hjs,并且应该放在 / templates 中。 (为了使本教程保持相当简短且易于阅读的格式,我不打算展示每个视图模板。

(为了使本教程保持相当简短且易于阅读的格式,我不打算展示每个视图模板。 我强烈建议你从 GitHub 下载源代码。)

现在要使控制器可见,我们必须在 app.js 中添加一个路径:

请注意,我们不是直接将 Admin.run 方法作为中间件发送。 那是因为我们想要保持上下文。 如果我们这样做:

单词本中管理员将指向别的东西。

保护管理面板

每个以 / admin 开头的页面都应受到保护。 为实现这一目标,我们将使用 Express 的中间件:Sessions。 它只是将一个对象附加到名为 session 的请求上。 我们现在应该更改管理控制器以执行另外两项操作:

  • 它应检查是否有可用的会话。 如果没有,则显示登录表单。
  • 它应接受登录表单发送的数据,并在用户名和密码匹配时授权用户。

这是一个我们可以用来完成这个的小辅助函数:

首先,我们有一个声明尝试通过会话对象识别用户。 其次,我们检查表格是否已提交。 如果是这样,表单中的数据在 request.body 对象中可用,该对象由 bodyParser 中间件填充。 然后我们只检查用户名和密码是否匹配。

现在这里是控制器的 run 方法,它使用我们的新助手。 我们检查用户是否被授权,显示控制面板本身,否则我们显示登录页面:

管理内容

正如我在本文开头所指出的那样,我们有很多东西需要管理。 为简化过程,我们将所有数据保存在一个集合中。 每条记录都有标题,文字,图片和类型属性 该类型属性将决定记录的所有者。 例如,“联系人” 页面只需要一个类型为 “联系人” 的记录,而 “博客” 页面则需要更多记录。 因此,我们需要三个新页面来添加,编辑和显示记录。 在我们开始创建新模板,样式和将新内容放入控制器之前,我们应该编写我们的模型类,它位于 MongoDB 服务器和我们的应用程序之间,当然还提供了一个有意义的 API。

该模型负责为每条记录生成唯一的 ID。 我们将需要它以便稍后更新信息。

如果我们想为 “联系人” 页面添加新记录,我们可以简单地使用:

因此,我们有一个很好的 API 来管理我们的 mongodb 集合中的数据。 现在我们准备编写用于使用此功能的 UI。 对于此部分,Admin 控制器需要进行相当多的更改。 为了简化任务,我决定将添加的记录列表与添加 / 编辑它们的表单结合起来。 正如你在下面的屏幕截图中看到的那样,页面左侧部分是为列表保留的,右侧部分是为表单保留的。

control-panel

将所有内容放在一个页面上意味着我们必须关注呈现页面的部分,或者更具体地说明我们发送到模板的数据。 这就是为什么我创建了几个组合的辅助函数,如下所示:

它看起来有点难看,但它可以按我的意愿工作。 第一个帮助器是 del 方法,它检查当前的 GET 参数,如果找到 action = delete&id = [记录的 id],它将从集合中删除数据。 第二个函数称为表单,它主要负责显示页面右侧的表单。 它检查表单是否已提交并正确更新或在数据库中创建记录。 最后,list 方法获取信息并准备一个 HTML 表,稍后将其发送到模板。 这三个助手的实现可以在本教程的源代码中找到。

在这里,我决定向你展示处理文件上传的功能:

如果提交了文件,请求对象的节点脚本.files 属性将填充数据。 在我们的例子中,我们有以下 HTML 元素:

这意味着我们可以通过 req.files.picture 访问提交的数据。 在上面的代码片段中,req.files.picture.path 用于获取文件的原始内容。 稍后,相同的数据将写入新创建的目录中,最后会返回正确的 URL。 所有这些操作都是同步的,但使用 readFileSync,mkdirSync 和 writeFileSync 的异步版本是一个好习惯。

前端

努力工作现已完成。 管理面板正在运行,我们有一个 ContentModel 类,它允许我们访问存储在数据库中的信息。 我们现在要做的是编写前端控制器并将它们绑定到保存的内容。

这是主页的控制器 - /controllers/Home.js

主页需要一个具有主页类型的记录和四个具有博客类型的记录。 控制器完成后,我们只需要在 app.js 中添加一个路径:

同样,我们将 db 对象附加到请求。 几乎与管理面板中使用的工作流程相同。

我们的前端(客户端)的其他页面几乎完全相同,因为它们都有一个控制器,它通过使用模型类获取数据,当然还有定义的路径。 有两个有趣的情况我想更详细地解释一下。 第一个与博客页面相关。 它应该能够显示所有文章,但也只能呈现一个。 所以,我们必须注册两条路线:

它们都使用相同的控制器:Blog,但调用不同的运行方法。 注意 / blog /:id 字符串。 这条路线将匹配像 URL / 博客 / 4e3455635b4a6f6dccfaa1e50ee71f1cde75222b 和长哈希将可 req.params.id。 换句话说,我们能够定义动态参数。 在我们的例子中,这是记录的 ID。 获得此信息后,我们就可以为每篇文章创建一个唯一的页面。

第二个有趣的部分是我如何构建服务,职业和联系人页面。 很明显,他们只使用数据库中的一条记录。 如果我们必须为每个页面创建一个不同的控制器,那么我们必须复制 / 粘贴相同的代码,只需更改类型字段。 有一种更好的方法来实现这一点,只有一个控制器,它接受其 run 方法中的类型。 以下是路线:

控制器看起来像这样:


部署

部署基于 Express 的网站实际上与部署任何其他 Node.js 应用程序相同:

  • 这些文件放在服务器上。
  • 应该停止节点进程(如果它正在运行)。
  • 一个 NPM 安装命令应以安装新的依赖(如果有的话)来运行。
  • 然后应该再次运行主脚本。

请记住,Node 仍然相当年轻,所以并非所有内容都可以按预期工作,但总是会有改进。 例如,永远保证你的 Nodejs 程序将连续运行。 你可以通过发出以下命令来执行此操作:

这也是我在服务器上使用的内容。 这是一个不错的小工具,但它解决了一个大问题。 如果你只使用节点 yourapp.js 运行你的应用程序,一旦你的脚本意外退出,服务器就会关闭。 永远,只需重新启动应用程序。

现在我不是系统管理员,但我想分享我将节点应用程序与 Apache 或 Nginx 集成的经验,因为我认为这在某种程度上是开发工作流程的一部分。

如你所知,Apache 通常在端口 80 上运行,这意味着如果你打开 http:// localhost 或 http:// localhost:80,你将看到 Apache 服务器提供的页面,并且很可能你的节点脚本正在侦听不同的港口。 因此,你需要添加一个接受请求的虚拟主机并将它们发送到正确的端口。 例如,假设我想在 expresscompletewebsite.dev 地址下的本地 Apache 服务器上托管我们刚刚构建的站点 我们要做的第一件事是将我们的域添加到 hosts 文件中。

之后,我们必须编辑 Apache 配置目录下的 httpd-vhosts.conf 文件并添加

服务器仍然接受端口 80 上的请求,但是将它们转发到节点正在侦听的端口 3000。

Nginx 设置要容易得多,说实话,它是托管基于 Nodejs 的应用程序的更好选择 你仍然需要在 hosts 文件中添加域名。 之后,只需在 Nginx 安装下的 / sites-enabled 目录中创建一个新文件。 该文件的内容如下所示:

请记住,你无法使用上述主机设置同时运行 Apache 和 Nginx。 这是因为它们都需要端口 80. 此外,如果你计划在生产环境中使用上述代码段,你可能需要对更好的服务器配置进行一些额外的研究。 正如我所说,我不是这方面的专家。


结论

Express 是一个很棒的框架,它为你提供了一个开始构建应用程序的良好起点。 正如你所看到的,关于如何扩展它以及你将使用它构建的内容,这是一个选择问题。 它通过使用一些很棒的中间件简化了枯燥的任务,并为开发人员留下了有趣的部分。

源代码

我们构建的此示例站点的源代码可在 GitHub 上获得 - https://github.com/tutsplus/build-complete-website-expressjs。 可以随意保存并修改它。 以下是运行网站的步骤。

  • 下载源代码
  • 转到 app 目录
  • 运行 npm install
  • 运行 mongodb 守护程序
  • 运行节点 app.js
关注我们的公众号
Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.