在 “精通 MEAN MEAN 堆栈简介” 中,我们安装并配置了一个 MEAN开发环境。在本文中,我将带领您遍历所创建的样例 MEAN.JS 应用程序,进一步介绍 MEAN堆栈的四个关键部分:MongoDB、Express、AngularJS 和 Node.js。在遍历该应用程序时,您将从服务器端到客户端跟踪进入的HTTP 请求。
输入 mongod
,启动您的本地 MongoDB 实例。(在 UNIX® 类操作系统上,可以输入mongod &
,在后台启动进程)。接下来,我们将对上一篇文章中创建的测试目录执行 cd
并输入 grunt
,启动在 Yeoman 生成器中创建的应用程序。您将看到类似清单 1 所示的输出。
清单 1. 启动本地 MEAN.JS
应用程序
$ grunt
Running "jshint:all" (jshint) task
>> 46 files lint free.
Running "csslint:all" (csslint) task
>> 2 files lint free.
Running "concurrent:default" (concurrent) task
Running "nodemon:dev" (nodemon) task
Running "watch" task
Waiting...
[nodemon] v1.0.20
[nodemon] to restart at any time, enter 'rs'
[nodemon] watching: app/views/**/*.* gruntfile.js server.js config/**/*.js app/**/*.js
[nodemon] starting 'node --debug server.js'
debugger listening on port 5858
NODE_ENV is not defined! Using default development environment
MEAN.JS application started on port 3000
在浏览器中打开 http://localhost:3000,查看应用程序的主页,如图 1 所示。
图 1. 本地 MEAN.JS 主页
接下来,我们将查看目录结构,查看应用程序如何开始工作(查看 MEAN.JS 文档的 Folder Structure 页面,获得有关的更多信息)。
理解 Node.js 和 Bower 配置文件
我们很快就会接触到源代码。首先,快速访问 package.json,这是您在 上一篇文章 中看到的 Node.js 配置文件。我还将介绍它在客户端的对应文件。这些文件都位于项目的根目录中。
package.json
在所有 Node.js 应用程序中,可以将 package.json 看作是最重要的配置文件。在该文件中,您将会发现提供给 Yeoman生成器的应用程序的元数据,比如名称、描述和作者,如清单 2 中的部分 package.json 文件所示。
清单 2. package.json,第 1 部分
{
"name": "test",
"description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js",
"version": "0.0.1",
"author": "Scott Davis",
"engines": {
"node": "0.10.x",
"npm": "1.4.x"
},
接下来,您会看到一系列可以输入到命令提示符中的命令,如清单 3 所示。
清单 3. package.json,第 2
部分
"scripts": {
"start": "grunt",
"test": "grunt test",
"postinstall": "bower install --config.interactive=false"
},
您已经输入了 grunt
来启动应用程序。稍后,您将输入 grunt test
来运行单元测试。postinstall
钩(hook)是区分服务器端依赖关系和客户端依赖关系的第一个提示。
在这个最重要的文件中,最重要的部分列出了应用程序的依赖关系,如清单 4 所示。这些 CommonJS 模块全部运行在应用程序的服务器端。
清单 4. package.json,第 3 部分
"dependencies": {
"express": "~4.2.0",
"mongoose": "~3.8.8"
},
"devDependencies": {
"grunt-mocha-test": "~0.10.0",
"grunt-karma": "~0.8.2",
"karma": "~0.12.0",
"karma-jasmine": "~0.2.1",
"karma-coverage": "~0.2.0",
"karma-chrome-launcher": "~0.1.2",
"karma-firefox-launcher": "~0.1.3",
"karma-phantomjs-launcher": "~0.1.2"
}
dependencies
代码块中声明了运行时依赖关系(比如与路由有关的 Express,与 MongoDB 有关的Mongoose)。devDependencies
代码块中声明了开发者和编译时依赖关系(包括测试框架,比如Mocha、Jasmine 和 Karma)。
bower.json
现在,让我们来关注一下客户端。浏览器中加载的 JavaScript 库在bower.json 中定义,如清单 5 所示。
清单 5. bower.json
{
"name": "test",
"version": "0.0.1",
"description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js",
"dependencies": {
"bootstrap": "~3",
"angular": "~1.2",
"angular-resource": "~1.2",
"angular-mocks": "~1.2",
"angular-cookies": "~1.2",
"angular-animate": "~1.2",
"angular-touch": "~1.2",
"angular-sanitize": "~1.2",
"angular-bootstrap": "~0.11.0",
"angular-ui-utils": "~0.1.1",
"angular-ui-router": "~0.2.10"
}
}
您可以看到,bower.json 与 package.json 类似。它包含一些相同的元数据字段,并使用了一个dependencies
块来定义客户端依赖关系,比如 Bootstrap(用于感官以及响应式 Web 设计)和AngularJS(用于客户端单页面应用程序)。
同样,应用程序的源代码也被分到两个目录:一个用于服务器端,一个用于客户端。
理解目录结构
这个 MEAN 应用程序有四个主要目录,如清单 6 所示。
清单 6. MEAN
目录结构
$ ls -ld */
drwxr-xr-x+ 7 scott staff 238 Jun 6 14:06 app/
drwxr-xr-x+ 8 scott staff 272 Jun 6 14:06 config/
drwxr-xr-x+ 49 scott staff 1666 Jun 6 14:07 node_modules/
drwxr-xr-x+ 8 scott staff 272 Jun 6 14:06 public/
您主要应关注 app 和 public 目录。首先从 app 目录中查找应用程序主页的源代码。
研究 MEAN 堆栈的服务器端
清单 7 显示了 app 目录结构。
清单 7. app(服务器端)目录结构
$ tree app
app
|--- controllers
|�� |--- articles.server.controller.js
|�� |--- core.server.controller.js
|�� |--- users.server.controller.js
|--- models
|�� |--- article.server.model.js
|�� |--- user.server.model.js
|--- routes
|�� |--- articles.server.routes.js
|�� |--- core.server.routes.js
|�� |--- users.server.routes.js
|--- tests
|�� |--- article.server.model.test.js
|�� |--- user.server.model.test.js
|--- views
|--- 404.server.view.html
|--- 500.server.view.html
|--- index.server.view.html
|--- layout.server.view.html
如果您曾经写过服务器端 MVC 应用程序,那么您应该了解它的典型工作流:
- 传入的 HTTP 请求将到达某个路由器。
- 路由器找到合适的控制器来处理该请求。3. 控制器从数据库构建一个模型(或一个模型列表)并传递给一个视图。4. 视图将模型与一个模板组合在一起,从而构建 HTML 页面,然后,将完成的输出传递给正在等待的 HTTP 响应。
如清单 8 所示,app/routes/core.server.routes.js 文件(Express 框架的一部分)包含应用程序的关键进入点。
清单 8. app/routes/core.server.routes.js
'use strict';
module.exports = function(app) {
// Root routing
var core = require('../../app/controllers/core');
app.route('/').get(core.index);
};
Strict 模式
Strict 模式是 ECMAScript 5 规范的一部分,这也是最新的 JavaScript 主流版本。(有关的更多信息,请参见Mozilla Developer Network 上的文章“Strict 模式”)。Strict 模式可以向后兼容。不能理解 'use strict'
语句的早期浏览器版本会直接忽略它;所有新的浏览器将慎重处理它。因此,如果您的新版本浏览器中运行的代码启用了 strict模式,那么它也能在旧版本浏览器中运行。
该路由器定义了一个单一的路径(/
),由核心控制器的 index
函数处理。注意,核心控制器是一个
CommonJS 模块,类型为 require
。
清单 8 开头的 'use strict';
语句会将您的 JavaScript 运行时设置为 strict模式,这要比过去的 JavaScript 运行时的 “什么都可以” 的语法规则更严格。在 strict 模式下,JavaScript运行时会将诚实的错误(honest mistake)处理为语法错误 — 比如不小心将某个变量声明为global,或试图使用之前未经定义的变量。Strict 模式搭配使用JSHint 可确保在开发阶段而不是生产阶段捕捉到语法错误。(当然,实现无 bug完美版本的关键在于执行单元测试时实现足够大的代码覆盖范围)。
接下来,将查看清单 9 所示的 app/controllers/core.server.controller.js(Express 框架的一部分)。
清单 9. app/controllers/core.server.controller.js
'use strict';
/**
* Module dependencies.
*/
exports.index = function(req, res) {
res.render('index', {
user: req.user || null
});
};
index
函数接受传入的 HTTP 请求和传出的 HTTP响应。由于该请求不需要从数据库获取内容,因此没有对任何模型进行实例化。index
模板被呈现给响应,同时还有一个变量的JSON 块,它将取代模板中同名的占位符。
清单 10 显示了 app/views/index.server.view.html。
清单 10. app/views/index.server.view.html
{% extends 'layout.server.view.html' %}
{% block content %}
<section data-ui-view></section>
{% endblock %}
这里没什么太多内容,只有清单 11 所示的到app/views/layout.server.view.html 的链接。
清单 11. app/views/layout.server.view.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{title}}</title>
<!-- General META -->
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- Semantic META -->
<meta name="keywords" content="{{keywords}}">
<meta name="description" content="{{description}}">
Express 模板引擎
您可以对 Express 使用各种 模板引擎。一个名为 ConsolidateJS 的CommonJS 模块甚至可以适应非 Express 模块引擎,并使它们能够与
Express 兼容。本系列文章将继续使用 Swig;您自己可以选择使用其他模板库。
您现在可以看到一些看上去有些类似的 HTML。围绕 title
、keywords
和description
的 {{}}
分隔符将它们标识为 Swig占位符,这些占位符将被实际值所替代。Swig 是 MEAN.JS Yeoman 生成器安装的模板引擎。
但是,如果回头看 清单 9 中的 core
控制器,您会发现传递给这个模板的惟一一个值是user
。如果您怀疑其他占位符是配置文件中定义的默认值,那么您的怀疑是正确的。
了解配置和环境
看一下清单 12 所示的 config/env/all.js,其中包含title
、description
和 keywords
变量。(我对目录结构进行了搜索,查找这定义这些变量的位置 — 在了解 MEAN 堆栈的过程中您可能希望将这个技巧添加到您的工具箱中)。
清单 12. config/env/all.js
'use strict';
module.exports = {
app: {
title: 'Test',
description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js',
keywords: 'MongoDB, Express, AngularJS, Node.js'
},
port: process.env.PORT || 3000,
templateEngine: 'swig',
除了模板期望的关键字外,该文件还包含其他一些有趣的值,比如 port
和templateEngine
。
环境变量
您可以在应用程序以外的地方设置一些变量来修改应用程序的行为,比如 PORT
和NODE_ENV
。例如,注意 config/env/all.js 中的 port
设置:
port: process.env.PORT || 3000,
该设置告诉应用程序 “将内部的 port
变量设置为环境变量 PORT
的值,或在未找到PORT
的情况下设置为默认值 3000
”。
要对这一设置进行测试,请按下 Ctrl+C 停止应用程序。要重启应用程序,可以尝试使用PORT=4000 grunt
,而不是使用 grunt
命令。您的应用程序现在在端口 4000
上运行。
您可以对 Node.js 进行编码,从而根据不同的运行时环境(开发、生产、准备、测试等等)表现出不同的行为。与 PORT
一样,如果没有显式地指定运行时环境,那么 Express 将为 NODE_ENV
提供一个默认值 —development
。这解释了在重启应用程序时它发出的一个警告:
NODE_ENV is not defined! Using default development environment
为了增加一些灵活性,可以在环境变量中具体化运行时配置。在命令行中,可以临时设置 PORT
和NODE_ENV
等变量。这样,在进行开发和测试时就可以很容易地改变变量的值。(当然,您可以将它们添加到.bash_profile,或者在 Windows® 中的 Control Panel 中设置它们,使它们具有更长的寿命)。
您可能会因为安全性而使用环境变量。在环境变量中保存用户名、密码和连接URL,而不是将它们放到容易受破坏的配置文件中(或者扩展到源控制)。这种方法也便于跨多个开发人员或生产机器部署通用的配置文件,并允许每个机器通过本地环境变量插入惟一值或凭证。
并不只限制使用 PORT
和 NODE_ENV
环境变量。您的 Platform as a
Service (PaaS) 提供商通常会提供若干个特定于服务的变量。
命名环境(Named environment)
设置单独的环境变量固然不错,但是您可能需要对一些相关的变量进行统一修改。例如,您希望避免修改用户名但忘记修改对应密码之类的简单错误。幸运的是,这个MEAN 应用程序支持 命名环境 的概念。(这个概念并不是 MEAN 应用程序所独有的。Rails、Grails 和许多其他流行的
Web 框架也提供了类似的功能)。
查看清单 13 的目录树中的 config/env,您将在其中看到一些命名环境文件。
清单 13. config
目录结构
$ tree config/
config/
|--- config.js
|--- env
|�� |--- all.js
|�� |--- development.js
|�� |--- production.js
|�� |--- test.js
|--- express.js
|--- init.js
|--- passport.js
|--- strategies
|--- facebook.js
|--- google.js
|--- linkedin.js
|--- local.js
|--- twitter.js
2 directories, 13 files
在 config/env、development.js、production.js 和 test.js 中,都指定了命名环境。如果您认为 all.js包含对所有环境通用的值,那么您的理解就是正确的。
要查看这些文件的读取和合并位置,请查看清单 14 所示的 config/config.js。
清单 14. config/config.js
/**
* Module dependencies.
*/
var _ = require('lodash');
/**
* Load app configurations
*/
module.exports = _.extend(
require('./env/all'),
require('./env/' + process.env.NODE_ENV) || {}
);
Lo-dash 是一个 CommonJS 模块,为数组、对象和 JSON结构提供了方便的函数。在 清单 14 中,开发人员试图在 all.js中设置一些基本值,并允许它们被development.js(或 production.js 或 test.js)中的值覆盖。
您已经查看了 清单 12 中的 config/env/all.js。清单 15 显示了
config/env/development.js。
清单 15. config/env/development.js
'use strict';
module.exports = {
db: 'mongodb://localhost/meanjs-dev',
app: {
title: 'MeanJS - Development Environment'
},
理想情况下,lodash.extend
函数将合并两个 JSON 块来生成这个结果:
app: {
title: 'MeanJS - Development Environment',
description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js',
keywords: 'MongoDB, Express, AngularJS, Node.js'
}
不幸的是,这并不是您获得的输出。添加一行代码将合并后的结构输出到 config/config.js,如清单 16 所示:
清单 16. 将实际合并后的结果合并到控制台
/**
* Load app configurations
*/
module.exports = _.extend(
require('./env/all'),
require('./env/' + process.env.NODE_ENV) || {}
);
console.log(module.exports)
输入 PORT=4000 NODE_ENV=development grunt
,返回应用程序。控制台将显示:
app: { title: 'MeanJS - Development Environment' }
如控制台中所示,config/env/development.js 中的 JSON 结构覆盖了 config/env/all.js中的结构,而不是与之合并。幸运的是,您可以快速修改 config/config.js 以获得期望的结果。
将函数调用从 _.extend
修改为_.merge
。当再次返回应用程序时,应该可以看到期望的结果:
app:
{ title: 'MeanJS - Development Environment',
description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js',
keywords: 'MongoDB, Express, AngularJS, Node.js' },
如果在浏览器中单击主页面上的 View > Source,就可以看到 config 值已经与 HTML模板合并,如清单 17 所示。
清单 17. HTML
显示正确的合并结果
<head>
<title>MeanJS - Development Environment</title>
<!-- Semantic META -->
<meta name="keywords" content="MongoDB, Express, AngularJS, Node.js">
<meta name="description" content="Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js">
现在,我们将从服务器端移动到客户端,完成此次 MEAN 应用程序之旅。
研究 MEAN 堆栈的客户端
主页的关键内容(如清单 18 中的 app/views/layout.server.view.html 中定义)由 AngularJS 在客户端填充。
清单 18. app/views/layout.server.view.html
<body class="ng-cloak">
<header data-ng-include="'/modules/core/views/header.client.view.html'"
class="navbar navbar-fixed-top navbar-inverse"></header>
<section class="content">
<section class="container">
{% block content %}{% endblock %}
</section>
</section>
回忆一下,app 目录包含 MEAN 应用程序的 Express 服务器端部分。从两点可以看出 header
在客户端由AngularJS 管理。首先,无论何时看到 HTML 属性中有一个 ng
时,都表明它是由AngularJS 管理的。其次,更实用的一点是,包含所有服务器端代码的 app目录并不包含模块目录。排除使用服务器端作为一种可能的解决方案后,就剩下使用 public 目录中的客户端源代码。如清单 19 所示,modules目录明显位于 public 目录下。
清单 19. public(客户端)目录结构
$ tree -L 1 public/
public/
|--- application.js
|--- config.js
|--- lib
|--- modules
如果查看 lib 目录,就会看到一些第三方库:
清单 20. 第三方库的 public/lib
目录
$ tree -L 1 public/lib
public/lib
|--- angular
|--- angular-animate
|--- angular-bootstrap
|--- angular-cookies
|--- angular-mocks
|--- angular-resource
|--- angular-sanitize
|--- angular-touch
|--- angular-ui-router
|--- angular-ui-utils
|--- bootstrap
|--- jquery
回忆一下 bower.json 中指定的库。
但是,如果查看 modules 目录,就会发现 app/views/layout.server.view.html 中指定了modules/core/views/header.client.view.html 模板。
清单 21. modules/core/views/header.client.view.html
<div class="container" data-ng-controller="HeaderController">
<div class="navbar-header">
<button class="navbar-toggle" type="button" data-ng-click="toggleCollapsibleMenu()">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/#!/" class="navbar-brand">MeanJS</a>
</div>
如果将 class="navbar-brand"
anchor 的值从 MeanJS
修改为其他值,那么此更改在保存文件后将会立即反映到浏览器中。但是到主 payload 的路径(主页的主要内容)更加迂回。再次查看app/views/layout.server.view.html,如清单 22 所示。
清单 22. app/views/layout.server.view.html
<body class="ng-cloak">
<header data-ng-include="'/modules/core/views/header.client.view.html'"
class="navbar navbar-fixed-top navbar-inverse"></header>
<section class="content">
<section class="container">
{% block content %}{% endblock %}
</section>
</section>
container
内包含一个名为 content
的block
。请记住app/views/index.server.view.html:
{% extends 'layout.server.view.html' %}
{% block content %}
<section data-ui-view></section>
{% endblock %}
这个 block content
包含一个空的部分,其中有一个 `data-ui-view属性。该属性用于客户端 AngularJS 路由器。查看public/modules/core/config/core.client.routes.js,如清单 23 所示。
清单 23. app/views/index.server.view.html
'use strict';
// Setting up route
angular.module('core').config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
// Redirect to home view when route not found
$urlRouterProvider.otherwise('/');
// Home state routing
$stateProvider.
state('home', {
url: '/',
templateUrl: 'modules/core/views/home.client.view.html'
});
}
]);
当 URL 为 /
时,客户端路由器会将modules/core/views/home.client.view.html
模板(如清单 24 所示)插入到 app/views/index.server.view.html 部分,后者包含data-ui-view
属性。模板的内容应当与位于 MEAN 应用程序主页时在浏览器中看到的内容相匹配。
清单 24. modules/core/views/home.client.view.html
<section data-ng-controller="HomeController">
<h1 class="text-center">THANK YOU FOR DOWNLOADING MEAN.JS</h1>
<section>
<p>
Before you begin we recommend you read about the basic building
blocks that assemble a MEAN.JS application:
</p>
结束语
在本文中,详细了解了一个 MEAN 应用程序的所有关键部分。在服务器端,我们了解到,HTML 请求首先从 Express 路由开始,然后调用Express 控制器函数,而后者将 JSON 数据与 Swig 模板合并,并返回到客户机。但是整个流程并没有在此终结。在客户端,AngularJS路由获得 HTML 模板并将其插入到主页面中。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。