在 “精通 MEAN MEAN 堆栈简介” 中,咱们安装并配置了一个 MEAN开发环境。在本文中,我将带领您遍历所建立的样例 MEAN.JS 应用程序,进一步介绍 MEAN堆栈的四个关键部分:MongoDB、Express、AngularJS 和 Node.js。在遍历该应用程序时,您将从服务器端到客户端跟踪进入的HTTP 请求。css
输入 mongod
,启动您的本地 MongoDB 实例。(在 UNIX® 类操做系统上,能够输入mongod &
,在后台启动进程)。接下来,咱们将对上一篇文章中建立的测试目录执行 cd
并输入 grunt
,启动在 Yeoman 生成器中建立的应用程序。您将看到相似清单 1 所示的输出。html
应用程序node
$ 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 所示。jquery
图 1. 本地 MEAN.JS 主页git
接下来,咱们将查看目录结构,查看应用程序如何开始工做(查看 MEAN.JS 文档的 Folder Structure 页面,得到有关的更多信息)。github
咱们很快就会接触到源代码。首先,快速访问 package.json,这是您在 上一篇文章 中看到的 Node.js 配置文件。我还将介绍它在客户端的对应文件。这些文件都位于项目的根目录中。web
在全部 Node.js 应用程序中,能够将 package.json 看做是最重要的配置文件。在该文件中,您将会发现提供给 Yeoman生成器的应用程序的元数据,好比名称、描述和做者,如清单 2 中的部分 package.json 文件所示。mongodb
{ "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 所示。chrome
部分数据库
"scripts": { "start": "grunt", "test": "grunt test", "postinstall": "bower install --config.interactive=false" },
您已经输入了 grunt
来启动应用程序。稍后,您将输入 grunt test
来运行单元测试。postinstall
钩(hook)是区分服务器端依赖关系和客户端依赖关系的第一个提示。
在这个最重要的文件中,最重要的部分列出了应用程序的依赖关系,如清单 4 所示。这些 CommonJS 模块所有运行在应用程序的服务器端。
"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)。
如今,让咱们来关注一下客户端。浏览器中加载的 JavaScript 库在bower.json 中定义,如清单 5 所示。
{ "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 所示。
目录结构
$ 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 目录中查找应用程序主页的源代码。
清单 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 应用程序,那么您应该了解它的典型工做流:
如清单 8 所示,app/routes/core.server.routes.js 文件(Express 框架的一部分)包含应用程序的关键进入点。
'use strict'; module.exports = function(app) { // Root routing var core = require('../../app/controllers/core'); app.route('/').get(core.index); };
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 框架的一部分)。
'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。
{% extends 'layout.server.view.html' %} {% block content %} <section data-ui-view></section> {% endblock %}
这里没什么太多内容,只有清单 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 使用各类 模板引擎。一个名为 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 堆栈的过程当中您可能但愿将这个技巧添加到您的工具箱中)。
'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) 提供商一般会提供若干个特定于服务的变量。
设置单独的环境变量当然不错,可是您可能须要对一些相关的变量进行统一修改。例如,您但愿避免修改用户名但忘记修改对应密码之类的简单错误。幸运的是,这个MEAN 应用程序支持 命名环境 的概念。(这个概念并非 MEAN 应用程序所独有的。Rails、Grails 和许多其余流行的
Web 框架也提供了相似的功能)。
查看清单 13 的目录树中的 config/env,您将在其中看到一些命名环境文件。
目录结构
$ 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。
/** * 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。
'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 所示:
/** * 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 所示。
显示正确的合并结果
<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 应用程序之旅。
主页的关键内容(如清单 18 中的 app/views/layout.server.view.html 中定义)由 AngularJS 在客户端填充。
<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 目录下。
$ tree -L 1 public/ public/ |--- application.js |--- config.js |--- lib |--- modules
若是查看 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 模板。
<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 所示。
<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 所示。
'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 应用程序主页时在浏览器中看到的内容相匹配。
<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 模板并将其插入到主页面中。