在使用开源软件构建专业网站方面,新兴的 MEANMongoDBExpressAngularJSNode.js)堆栈对一直以来广受欢迎的 LAMP 堆栈形成了巨大冲击。MEAN 代表架构和思维模型的一次重要转变 — 从关系数据库转变为 NoSQL,并从服务器端 Model-View-Controller 转变为客户端单页面应用程序。在此文章系列中,我们将了解 MEAN 堆栈技术如何对 LAMP 形成补充,以及如何使用它创建面向 21 世纪的现代化的全堆栈 JavaScript Web 应用程序。

在 “[_精通 MEAN_ MEAN 堆栈简介][0]” 中,我们安装并配置了一个 MEAN 开发环境。在本文中,我将带领您遍历所创建的样例 MEAN.JS 应用程序,进一步介绍 MEAN 堆栈的四个关键部分:MongoDBExpressAngularJSNode.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 所示。

##### 图 1. 本地 MEAN.JS 主页

![本地 MEAN.JS 主页的截图][2]

接下来,我们将查看目录结构,查看应用程序如何开始工作(查看 MEAN.JS 文档的 [Folder Structure][3] 页面,获得有关的更多信息)。

## 理解 Node.js 和 Bower 配置文件

我们很快就会接触到源代码。首先,快速访问 package.json,这是您在 [上一篇文章][0] 中看到的 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**
包含服务器端源代码。- **config**
包含配置文件。- **node_modules**
包含 package.json 中指定的服务器端模块。- **public**
包含客户端源代码 — 包括 lib 目录,其中含有在 bower.json 中指定的客户端库。

您主要应关注 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 应用程序,那么您应该了解它的典型工作流:

1. 传入的 HTTP 请求将到达某个路由器。
1. 路由器找到合适的控制器来处理该请求。
1. 控制器从数据库构建一个模型(或一个模型列表)并传递给一个视图。
1. 视图将模型与一个模板组合在一起,从而构建 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 模式][5]”)。Strict 模式可以向后兼容。不能理解 ‘use strict’ 语句的早期浏览器版本会直接忽略它;所有新的浏览器将慎重处理它。因此,如果您的新版本浏览器中运行的代码启用了 strict 模式,那么它也能在旧版本浏览器中运行。

该路由器定义了一个单一的路径(/),由核心控制器的 index 函数处理。注意,核心控制器是一个 CommonJS 模块,类型为 require。

清单 8 开头的 ‘use strict’; 语句会将您的 JavaScript 运行时设置为 strict 模式,这要比过去的 JavaScript 运行时的 “什么都可以” 的语法规则更严格。在 strict 模式下,JavaScript 运行时会将诚实的错误(honest mistake)处理为语法错误 — 比如不小心将某个变量声明为 global,或试图使用之前未经定义的变量。Strict 模式搭配使用 [JSHint][6] 可确保在开发阶段而不是生产阶段捕捉到语法错误。(当然,实现无 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 %}

{% endblock %}

这里没什么太多内容,只有清单 11 所示的到 app/views/layout.server.view.html 的链接。

##### 清单 11. app/views/layout.server.view.html

{{title}}

您现在可以看到一些看上去有些类似的 HTML。围绕 title、keywords 和 description 的 {{}} 分隔符将它们标识为 [Swig][7] 占位符,这些占位符将被实际值所替代。Swig 是 MEAN.JS Yeoman 生成器安装的模板引擎。

但是,如果回头看 [清单 9][8] 中的 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][9] 是一个 CommonJS 模块,为数组、对象和 JSON 结构提供了方便的函数。在 [清单 14][10] 中,开发人员试图在 all.js 中设置一些基本值,并允许它们被 development.js(或 production.js 或 test.js)中的值覆盖。

您已经查看了 [清单 12][11] 中的 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 显示正确的合并结果

MeanJS – Development Environment

现在,我们将从服务器端移动到客户端,完成此次 MEAN 应用程序之旅。

## 研究 MEAN 堆栈的客户端

主页的关键内容(如清单 18 中的 app/views/layout.server.view.html 中定义)由 AngularJS 在客户端填充。

##### 清单 18. app/views/layout.server.view.html

{% block content %}{% endblock %}

回忆一下,app 目录包含 MEAN 应用程序的 Express 服务器端部分。从两点可以看出 header 在客户端由 AngularJS 管理。首先,无论何时看到 HTML 属性中有一个 ng 时,都表明它是由 A**ng**ularJS 管理的。其次,更实用的一点是,包含所有服务器端代码的 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

如果将 class=”navbar-brand” anchor 的值从 MeanJS 修改为其他值,那么此更改在保存文件后将会立即反映到浏览器中。但是到主 payload 的路径(主页的主要内容)更加迂回。再次查看 app/views/layout.server.view.html,如清单 22 所示。

##### 清单 22. app/views/layout.server.view.html

{% block content %}{% endblock %}

container 内包含一个名为 content 的 block。请记住 app/views/index.server.view.html:

{% extends ‘layout.server.view.html’ %}

{% block content %}

{% 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

THANK YOU FOR DOWNLOADING MEAN.JS

Before you begin we recommend you read about the basic building
blocks that assemble a MEAN.JS application:

在本文中,详细了解了一个 MEAN 应用程序的所有关键部分。在服务器端,我们了解到,HTML 请求首先从 Express 路由开始,然后调用 Express 控制器函数,而后者将 JSON 数据与 Swig 模板合并,并返回到客户机。但是整个流程并没有在此终结。在客户端,AngularJS 路由获得 HTML 模板并将其插入到主页面中。

在下一篇文章中,将研究应用程序的文章部分,深入探讨 MongoDB 的角色以及 AngularJS。您还将了解如何在服务器端和客户端进行测试,确保最大程度减少生产环境中的异常行为。

那时,请尽情享受精通 MEAN 堆栈的快乐。

[0]: http://www.ibm.com/developerworks/cn/web/wa-mean1/index.html
[1]: http://localhost:3000
[2]: http://www.ibm.com/developerworks/cn/web/wa-mean2/localhost-home.jpg
[3]: http://meanjs.org/docs.html#folder-structure
[4]: http://www.ibm.com/developerworks/cn/web/wa-mean2/#ibm-pcon
[5]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode
[6]: http://www.jshint.com/
[7]: http://paularmstrong.github.io/swig/
[8]: http://www.ibm.com/developerworks/cn/web/wa-mean2/#listing9
[9]: http://lodash.com/
[10]: http://www.ibm.com/developerworks/cn/web/wa-mean2/#listing14
[11]: http://www.ibm.com/developerworks/cn/web/wa-mean2/#listing12