您如今已经了解了 MEAN 应用程序的机制,接下来咱们将对第一期文章中建立的 MEAN.JS应用程序进行定制。咱们在第二期文章中对该应用程序有了一个大体的了解。在第三期文章中,我将演示该应用程序的基本 CRUD功能。您还会了解一些有关响应式 Web 设计和 Bootstrap 的内容。css
本系列其他部分将要构建的应用程序被命名为 UGLI:User Group List and Information 应用程序。我从 2010年开始运营 HTML5 Denver User Group(前身是 Boulder Java User Group,更早之前是 Denver Java UserGroup),所以我是本地用户组的狂热粉丝,可是让我不解的是一直没有专门的软件来运行用户组。如今咱们就要解决这个问题了。html
许多用户组都在 Meetup.com 创建了一个在线主页。我使用 MEAN 和UGLI 应用程序的目标并非要取代 Meetup.com;相反,我想与它创建更深刻的集成。Meetup.com集中了运行成功的用户组所需的大部分核心功能;注册新用户,发布会议细节、处理 RSVP等等。可是对于用户组领导者来讲仍然缺失一些关键功能,包括管理一组会议主持人(presenter)并连接到幻灯片(slide deck)。UGLI能够填补这方面的空缺。(参见 下载 得到完整的样例代码)。web
建立应用程序 UGLI 的第一个任务就是调整应用程序的标记(branding)。须要在应用程序的服务器端对 config 和 app 目录作一些修改;另外要对客户端的 public 目录作一些修改。mongodb
首先从 config/env/all.js 中的元数据开始。将标题修改成 HTML5 Denver(或您选择的用户组),并将描述修改成 HTML5 Denver User Group,如清单 1 所示。bootstrap
'use strict'; module.exports = { app: { title: 'HTML5 Denver', description: 'HTML5 Denver User Group', keywords: 'MongoDB, Express, AngularJS, Node.js' },
config/env/development.js 中的标题也须要修改,如清单 2 所示。上篇文章中咱们已经了解到 development.js 和 all.js 会在运行时合并。后端
'use strict'; module.exports = { db: 'mongodb://localhost/test-dev', app: { title: 'HTML5 Denver' },
接下来,修改导航栏左上角显示的品牌。为此,须要编辑
public/modules/core/views/header.client.view.html。在大概第 9 列的地方找到 anchor 标记和 navbar-brand
类,将 body 修改成 HTML5 Denver
,如清单 3 所示。浏览器
<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">HTML5 Denver</a> </div> <!-- ...snip... --> </div>
要验证所作的修改,请在命令行输入 mongod
启动 MongoDB,而后输入 grunt
启动应用程序。在浏览器中查看 Web 应用程序,看看标记是否显示在菜单和标题栏中。服务器
要完成标记修改,须要替换 public/modules/core/views/home.client.view.html中的标准文本(boilerplate),该文本显示在主页的正文中。建立一个名为 home.client.view.html.original的副本,这样就能够在稍后往回引用(若是须要的话)。架构
该文件利用 Bootstrap框架的功能,确保您的网站从一开始就面向移动应用。在继续以前,须要了解 Bootstrap 提供的 12 列的网格布局。app
查看任意硬拷贝新闻或杂志,您都会看到其中使用了列。有时,一副图片或标题由于某种设计风格而需跨越多个列,可是一个基本的柱状布局构成了几乎全部打印页面的基础。
Web 页面也是如此。例如,访问 TIME网站。您看到它的布局也是基于列的。可是,当您将浏览器窗口的宽度从全屏缩小到很是窄的时候,注意会发生什么。可见列的数量将随着窗口变小而减小,并随着窗口增大而增多。
这种效应被称为 响应式Web 设计,由于 Web 页面会 响应 并调整设计来适应设备所要求的屏幕尺寸。现代 Web开发人员构建的网站能够无缝地支持从最小的手持设备到拥有最大屏幕的台式机或壁挂屏幕等各类设备。分别使用 http://m.* 和http://www.* URL 为智能手机、平板电脑、笔记本等建立专门的、离散的网站,这种作法早已过期。
响应式 Web 设计 并非 一个全能的解决方案;相反,它是“一个外观要求能够适应全部设备的网站”。您不须要选择用户访问网站所使用的设备类型,所以您的设计具有内置的灵活性,能够相应地进行自我调整。
许多流行网站(包括 Facebook 和 Instagram)更多地是经过移动设备而不是传统计算机来进行访问的。Twitter的用户群绝大多数是移动用户。Twitter 规范了其响应式 Web 设计策略并实现了与 Bootstrap 相同的开源化。Bootstrap 有12 列的布局,能够根据您用来定义列的 CSS 类进行缩小或放大。
请注意,MEAN.JS 应用程序中对 MongoDB、Express、AngularJS 和 Node.js 使用了四个列的布局,如图 1 所示。
图 1. Bootstrap 的列布局示例
如今查看 public/modules/core/views/home.client.view.html 中的源代码,如清单 4 所示,看看Bootstrap 的 12 列布局是什么样子的。
<div class="row"> <div class="col-md-3"> <h2><strong>M</strong>ongoDB</h2> </div> <div class="col-md-3"> <h2><strong>E</strong>xpress</h2> </div> <div class="col-md-3"> <h2><strong>A</strong>ngularJS</h2> </div> <div class="col-md-3"> <h2><strong>N</strong>ode.js</h2> </div> </div>
若是您向一个父 div
添加 class="row"
,那么您能够向子div
添加 class="col-_xx_-_N_"
属性来将它们分红几个列。_N_
值必须介于 1 和 12之间,_xx_
值取决于您但愿优化布局的设备的尺寸:
xs
适用于极小设备(低于 768 像素宽)sm
用于小型设备(768 和 991 像素之间)md
适合中型设备(992 和 1,199 像素之间)lg
适合大型设备(1,200 像素或更高)查看 Bootstrap CSS 文档的 网格系统小节,了解有关的更多信息。
因为清单 4 中的每一个列针对中型(md
)设备进行了优化,所以若是在屏幕宽度低于 992 像素的设备上访问该页面,列将垂直堆叠而不是水平堆叠。将您的浏览器窗口变得足够窄来触发这一更改,如图 2 所示。
图 2. 移动设备上的响应式 Web 设计示例
如今,可使用咱们已经需到的知识,使用特定于 UGLI 的文本替换 home.client.view.html 中的标准文本。
首先,从 W3C HTML5 徽标页面 下载256 像素的 HTML5 徽标,并将其复制到public/modules/core/img/brand/HTML5_Logo_256.png。而后使用清单 5 中的源代码替换public/modules/core/views/home.client.view.html 中现有的 HTML。
<section data-ng-controller="HomeController"> <div class="jumbotron text-center"> <div class="row"> <div class="col-md-4"> <img alt="HTML5" class="img-responsive center-block" src="modules/core/img/brand/HTML5_Logo_256.png" /> </div> <div class="col-md-8"> <h1>The HTML story is still being written.</h1> <h2><em>Come hear the latest chapter at the HTML5 Denver User Group.</em></h2> </div> </div> </div> </section>
在较宽的浏览器窗口中查看网站时,HTML5 徽标会出如今文本旁边,如图 3 所示。
图 3. 新的 UGLI 主页
当您将浏览器窗口变得足够窄时,徽标会出如今文本的上方,如图 4 所示。
图 4. 新的 UGLI 主页,它会出如今移动设备上
使用 Bootstrap 能够轻松地让您的网站对移动应用程序变得更友好,我在为客户构建每一个新网站时都使用 Bootstrap 做为基础技术。
如今咱们将要在 MEAN 堆栈中处理 CRUD。
Meetup.com 能够帮助我很好地管理用户组活动。可是,在某个活动结束后,就时间方面而言,该活动的重要性不如当天晚上的谈话。
换句话说,这个网站的一个用户用例就是:“下次会议要讨论什么?”Meetup.com 能够很好地知足这种用户用例。
第二个用户用例(“向我显示与 MEAN 堆栈有关的全部谈话,不论是何时发生的” )正是我准备经过 UGLI应用程序解决的用例。要实现这个用例,必须围绕一个新的名为 Talk
的模型对象建立一个 CRUD
基础架构。幸运的是,可使用一个 Yeoman 生成器来实现这个基础架构。
在应用程序的根目录,输入 yo meanjs:crud-module talks
。响应提示:
Yes
,将 CRUD 模块连接添加到菜单。3. 当生成器询问要使用哪一个菜单时,接受默认设置(topbar
)。清单 6 显示了交互式命令行序列。
模块
$ yo meanjs:crud-module talks [?] Which supplemental folders would you like to include in your angular module? css, img, directives, filters [?] Would you like to add the CRUD module links to a menu? Yes [?] What is your menu identifier? topbar create app/controllers/talks.server.controller.js create app/models/talk.server.model.js create app/routes/talks.server.routes.js create app/tests/talk.server.model.test.js create public/modules/talks/config/talks.client.routes.js create public/modules/talks/controllers/talks.client.controller.js create public/modules/talks/services/talks.client.service.js create public/modules/talks/tests/talks.client.controller.test.js create public/modules/talks/config/talks.client.config.js create public/modules/talks/views/create-talk.client.view.html create public/modules/talks/views/edit-talk.client.view.html create public/modules/talks/views/list-talks.client.view.html create public/modules/talks/views/view-talk.client.view.html create public/modules/talks/talks.client.module.js
在清单 6 中,请注意,生成器建立了服务器端基础架构(保存在 app 目录中):路由、一个控制器、一个模型和一个单元测试。它还在public/modules/talks 目录下构建了全部客户端工件。
您稍后将向 Talk
对象添加一些自定义字段。在此以前,在浏览器中访问网站,查看默认状况下会获得哪些内容。
单击右上角的 Signin 连接,输入本系列早些时候建立的用户名和密码,或者单击Signup 并建立一组新的凭证。
完成登陆后,能够在左上角看到一个 Talks 菜单。从菜单中选择 New Talk打开一个 HTML 表单,其中提供了一个独立的 Name 字段,如图 5 所示。
图 5. 自定义以前的 New Talk 表单
这是一个良好的开端,可是要捕捉 Talk
的全部属性,您须要的不只仅是一个简单文本。
要向 Talk
添加新字段,必须编辑 6 个文件 — 四个用于显示,两个用于持久性:
首先要处理持久性。解决方案一半用在服务器端,另外一半用在客户端。
服务器端模型(在 app/models/talk.server.model.js中定义)是应用程序的原型。您将在其中命名字段,提供数据类型,验证规则等等。
客户端控制器(在 public/modules/controllers/talks.client.controller.js中定义)收集来自用户的数据输入,并经过 HTTP 请求将数据推到服务器。控制器还经过链接得到 JSON 数据,并提供给视图以用于演示。
此架构的一个有趣之处是对象模型永远不会离开服务器。对象是来自客户机的数据的具体化实现,并在 HTTP 响应中序列化到 JSON。
该应用程序有两个控制器(一个位于服务器端,另外一个位于客户端),可是咱们只关心客户端控制器。服务器端控制器只是将进入的 JSON推入到模型对象。所以在向模型添加额外字段时不须要对服务器端控制器作任何调整。客户端控制器要进行一些调整来容纳新的字段。
打开 app/models/talk.server.model.js,向服务器端模型添加新的字段,如清单 7 所示。您能够看到展开的name
字段(如 图 5
所示),同时还定义了两个元数据字段:created
和 user
。
/** * Talk Schema */ var TalkSchema = new Schema({ name: { type: String, default: '', required: 'Please fill Talk name', trim: true }, created: { type: Date, default: Date.now }, user: { type: Schema.ObjectId, ref: 'User' } });
这个基于 JSON 的模式无需多加解释。在定义新字段时,您能够指定数据类型、默认值和错误消息,以显示给必要的字段。您还能够作出许多其余优化。查看 Mongoosedocumentation,得到有关的更多信息。
对 description
、presenter
和 slidesUrl
添加新字段,如清单 8 所示。在本例中,description
和 presenter
都是必要字段。slidesUrl
字段是可选字段。
/** * Talk Schema */ var TalkSchema = new Schema({ name: { type: String, default: '', required: 'Please fill Talk name', trim: true }, description: { type: String, default: '', required: 'Please fill Talk description', trim: true }, presenter: { type: String, default: '', required: 'Please fill Talk presenter', trim: true }, slidesUrl: { type: String, default: '', trim: true }, created: { type: Date, default: Date.now }, user: { type: Schema.ObjectId, ref: 'User' } });
此时,您的服务器端后端已经准备好接收新字段。如今您须要处理客户端控制器。打开public/modules/controllers/talks.client.controller.js,添加新的字段,如清单 9 所示。
// Create new Talk $scope.create = function() { // Create new Talk object var talk = new Talks ({ name: this.name, description: this.description, presenter: this.presenter, slidesUrl: this.slidesUrl }); // Redirect after save talk.$save(function(response) { $location.path('talks/' + response._id); }, function(errorResponse) { $scope.error = errorResponse.data.message; }); // Clear form fields this.name = ''; this.description = ''; this.presenter = ''; this.slidesUrl = ''; };
在 $scope.create
函数中,表格字段将被汇集到一个 JSON对象,并被发送给服务器,以便实现持久存储。从模型向控制器添加相应的字段后,您就实现了持久存储。
如今咱们要将注意力转移到演示层,这样用户就能够查看新字段并进行交互。
查看 public/modules/talks/views/。有四个字段与 CRUD 生命周期有关:
打开 create-talk.client.view.html,如清单 10 所示。
<section data-ng-controller="TalksController"> <div class="page-header"> <h1>New Talk</h1> </div> <div class="col-md-12"> <form class="form-horizontal" data-ng-submit="create()" novalidate> <fieldset> <div class="form-group"> <label class="control-label" for="name">Name</label> <div class="controls"> <input type="text" data-ng-model="name" id="name" class="form-control" placeholder="Name" required> </div> </div> <div class="form-group"> <input type="submit" class="btn btn-default"> </div> <div data-ng-show="error" class="text-danger"> <strong data-ng-bind="error"></strong> </div> </fieldset> </form> </div> </section>
将与 Name
有关的代码块复制三次,以便支持Description
、Presenter
和slidesUrl
,如清单 11 所示。我将 Description
字段设置为textarea
,而不是一个简单的文本字段。一样,我从 slidesUrl
字段移除了required
属性,并将 input type
从 text
修改成 url
。
create-talk.client.view.html
<section data-ng-controller="TalksController"> <div class="page-header"> <h1>New Talk</h1> </div> <div class="col-md-12"> <form class="form-horizontal" data-ng-submit="create()" novalidate> <fieldset> <div class="form-group"> <label class="control-label" for="name">Name</label> <div class="controls"> <input type="text" data-ng-model="name" id="name" class="form-control" placeholder="Name" required> </div> </div> <div class="form-group"> <label class="control-label" for="description">Description</label> <div class="controls"> <textarea data-ng-model="description" id="description" class="form-control" placeholder="Description" required></textarea> </div> </div> <div class="form-group"> <label class="control-label" for="presenter">Presenter</label> <div class="controls"> <input type="text" data-ng-model="presenter" id="presenter" class="form-control" placeholder="Presenter" required> </div> </div> <div class="form-group"> <label class="control-label" for="slidesUrl">Slides</label> <div class="controls"> <input type="url" data-ng-model="slidesUrl" id="slidesUrl" class="form-control" placeholder="Slides Url"> </div> </div> <div class="form-group"> <input type="submit" class="btn btn-default"> </div> <div data-ng-show="error" class="text-danger"> <strong data-ng-bind="error"></strong> </div> </fieldset> </form> </div> </section>
在 Web 浏览器中,您新修改的 New Talk 页面应当相似图 6 所示。
图 6. 自定义后的 New Talk 表单
若是对所作的更改感到满意,请打开 edit-talk.client.view.html 并执行相应的更改,如清单 12 所示。
<div class="col-md-12"> <form class="form-horizontal" data-ng-submit="update()" novalidate> <fieldset> <div class="form-group"> <label class="control-label" for="name">Name</label> <div class="controls"> <input type="text" data-ng-model="talk.name" id="name" class="form-control" placeholder="Name" required> </div> </div> <div class="form-group"> <label class="control-label" for="description">Description</label> <div class="controls"> <textarea data-ng-model="talk.description" id="description" class="form-control" placeholder="Description" required></textarea> </div> </div> <div class="form-group"> <label class="control-label" for="presenter">Presenter</label> <div class="controls"> <input type="text" data-ng-model="talk.presenter" id="name" class="form-control" placeholder="Presenter" required> </div> </div> <div class="form-group"> <label class="control-label" for="slidesUrl">Slides</label> <div class="controls"> <input type="url" data-ng-model="talk.slidesUrl" id="name" class="form-control" placeholder="Slides Url"> </div> </div> <div class="form-group"> <input type="submit" value="Update" class="btn btn-default"> </div> <div data-ng-show="error" class="text-danger"> <strong data-ng-bind="error"></strong> </div> </fieldset> </form> </div>
请注意,用于编辑的 HTML 与以前修改的建立表单稍微有些不一样。在编辑时,您已经有了一个 Talk
对象,所以data-ng-model
属性将以彻底限定的方式引用字段,好比用 talk.name
而不是name
。在 Web 浏览器中查看修改,如图 7 所示。
图 7. 自定义后的 Edit Talk 表单
view-talk.client.view.html 页面是对象的只读视图。用户在保存新的 Talk
,更新现有的Talk
或从列表页面中选择 Talk
后未来到该视图。如清单 13 所示作出修改。
<div class="page-header"> <h1 data-ng-bind="talk.name"></h1> <h2><em>by {{talk.presenter}} <span ng-if="talk.slidesUrl !== '' ">[<a href="{{talk.slidesUrl}}">slides</a>]</span></em></h2> <p>{{talk.description}}</p> </div>
前面提到 slidesUrl
是可选字段。在视图页面中,您将使用 ng-if
指令有条件地显示字段(若是已填充)。在浏览器中查看页面,检查这一行为,如图 8 所示。
图 8. 自定义后的 View Talk 表单
List 视图是最后一个须要作出调整的视图。打开 list-talks.client.view.html 并如清单 14 所示进行修改。
<div class="list-group"> <a data-ng-repeat="talk in talks" data-ng-href="#!/talks/{{talk._id}}" class="list-group-item"> <h4 class="list-group-item-heading" data-ng-bind="talk.name"></h4> <p><em>by {{talk.presenter}}</em></p> </a> </div>
请注意,这里使用 data-ng-repeat
指令显示了服务器返回的 talk 列表中的每一个talk。在浏览器中查看结果,如图 9 所示。
图 9. 自定义后的 List Talks 表单
此时,您已经了解了 MEAN 堆栈交互的各个方面。您使用 Bootstrap 的响应式 Web 设计功能确保您的网站可以适应全部设备,而不只限于传统的有 101 个键和鼠标的传统台式机。您已经领略了使用 Yeoman 生成器向应用程序添加新 CRUD模块的强大之处及其便利性。该生成器将原始工件放到正确的目录中,您只须要对它们进行自定义便可。
范例代码:wa-mean3src.zip