在上一节中我们学会了如何在页面中添加一个组件以及一些基本的Angular知识,而这一节将用Angular来创建一个单页应用(SPA)。这意味着,取代我们之前用Express在服务端运行整个网站逻辑的方式(jade、路由都需要在服务端编译),我们将用Angular在客户端浏览器上跑起来。PS:在正常的开发流程上,我们可能不会在服务器端创建了一个网站,然后又用SPA重建它。但从学习的角度来说这还不错,这样掌握了两种构建方式。

上一节所有Angular相关的代码都在一个js里面,这不便管理和维护,这一节在根目录下新建一个app_client,用来专门放单页相关的代码。不要忘记设置为静态:

  1. app.use(express.static(path.join(__dirname, 'app_client')))

Angular路由

在SPA应用中,页面间的切换并不会每次都向后台发送求请求。这一节将路由移到客户端,但保留母版页(layout.jade),其他视图用Angular实现。为此先在控制器中新建一个angularApp方法。

  1. module.exports.angularApp = function (req, res) {
  2. res.render('layout', { title: 'ReadingClub' });
  3. };

设置路由

  1. router.get('/', ctrlOthers.angularApp);

剩下的Express路由是多余的了,你可以删掉或者注释掉。为避免页面重新加载,Angular的默认做法就是在url中加一个#号。#号一般是用来作为锚,来定位页面上的点,Angular用来访问应用中的点。比如在Express中,访问about页面:

  1. /about

在Angular中,url会变成

  1. /#/about

不过这个#号也是可以拿掉的,毕竟看起来不是那么直观,这个在下一节讲。

老版本的Angular库是包含路由模块的,但是现在是作为一个外部依赖文件,可以自己维护。所以先需要下载并添加到项目中。https://code.angularjs.org/1.2.19/

下载angular-route.min.js和angular-route.min.js.map,并在app_client下创建一个app.js

在layout.jade 中添加

  1. script(src='/angular/angular.min.js')
  2. script(src='/lib/angular-route.min.js')
  3. script(src='/app.js')

使用路由前需要设置模块依赖,要注意的是路由的文件名是angular-route,但实际模块名称是ngRoute。在app_client/app.js 下:

  1. angular.module('readApp', ['ngRoute']);

ngRoute模块会生成一个$routeProvider对象 ,可以用来传递配置函数,也就是我们定义路由的地方:

  1. function config($routeProvider) {
  2. $routeProvider
  3. .when('/', {})
  4. .otherwise({ redirectTo: '/' });
  5. }
  6. angular
  7. .module('readApp')
  8. .config(['$routeProvider', config]);
回顾以前的$http,$scope,service 以及现在的$routeProvider 出现在函数参数的时候,Angular会自动为我们获取实例,这就是Angular的依赖注入机制;config方法定义了路由。而目前这个路由没有做多少活,但语法很直观,当URL是'/'时,也就是访问主页时什么也不做。而当是别的URL访问时就跳转到首页。接下来我们让这个路由干点活。

Angular 视图

先在app_client文件夹下创建一个home文件夹,用来放置主页的一些文件。但是目前首页都还是jade视图,我们需要将其转换为html,因此先创建一个home.view.html:

  1. <div class="row" >
  2. <div class="col-md-9 page" >
  3. <div class="row topictype"><a href="/" class="label label-info">全部</a><a href="/">读书</a><a href="/">书评</a><a href="/">求书</a><a href="/">求索</a></div>
  4. <div class="row topiclist" data-ng-repeat='topic in data'>
  5. <img data-ng-src='{{topic.img}}'><span class="count"><i class="coment">{{topic.commentCount}}</i><i>/</i><i>{{topic.visitedCount}}</i></span>
  6. <span class="label label-info">{{topic.type}}</span><a href="/">{{topic.title}}</a>
  7. <span class="pull-right">{{topic.createdOn}}</span><a href="/" class="pull-right author">{{topic.author}}</a>
  8. </div>
  9. </div>
  10. <div class="col-md-3">
  11. <div class="userinfo">
  12. <p>{{user.userName}}</p>
  13. </div>
  14. </div>
  15. </div>

因为还没有数据,所以这个html片段什么也不会做。而接下来就是告诉Angular模块,访问主页的时候加载这个视图,这通过templateUrl 实现,修改路由:

  1. function config($routeProvider) {
  2. $routeProvider
  3. .when('/', {
  4. templateUrl: 'home/home.view.html'
  5. })
  6. .otherwise({ redirectTo: '/' });
  7. }

但这只是提供了一个模板地址,Angular从哪儿开始替换呢,像Asp.Net MVC中有一个@RenderBody的标记,在jade中是block content。这就需要用到ngRoute模块中的一个指令:ng-view。被标记的元素会被Angular当成一个容器来切换视图。我们不妨就加在block content的上方:

  1. #bodycontent.container
  2. div(ng-view)
  3. block content

控制器

有了路由和视图,还需要控制器.同样在home文件夹下创建一个home.controller.js文件,先还是使用静态数据。经过了上一节,这个部分是轻车熟路。

  1. angular
  2. .module('readApp')
  3. .controller('homeCtrl', homeCtrl);
  1. function homeCtrl($scope) {
  2. $scope.data = topics;
  3. $scope.user = {
  4. userName: "stoneniqiu",
  5. };
  6. }

再修改路由:

  1. function config($routeProvider) {
  2. $routeProvider
  3. .when('/', {
  4. templateUrl: 'home/home.view.html',
  5. controller: 'homeCtrl',
  6. })
  7. .otherwise({ redirectTo: '/' });
  8. }

这个时候访问页面,出来数据了。 所以不管是Asp.net MVC,Express还是Angular,MVC模式的思路是一致的,请求先到达路由,路由负责转发给控制器,控制器拿到数据然后渲染视图。

和上一节不同的是,没有在页面上使用ng-controller 指令了,而是在路由里面指定。

controllerAs 

Angular提供了一个创建视图模型的方法来绑定数据,这样就不用每次直接修改$scope 对象,保持$scope 干净。

  1. function config($routeProvider) {
  2. $routeProvider
  3. .when('/', {
  4. templateUrl: 'home/home.view.html',
  5. controller: 'homeCtrl',
  6. controllerAs: 'vm'
  7. })
  8. .otherwise({ redirectTo: '/' });
  9. }

红色代码表示启用controllerAs语法,对应的视图模型名称是vm。这个时候Angular会将控制器中的this绑定到$scope上,而this又是一个上下文敏感的对象,所以先定义一个变量指向this。controller方法修改如下

  1. function homeCtrl() {
  2. var vm = this;
  3. vm.data = topics;
  4. vm.user = {
  5. userName: "stoneniqiu",
  6. };
  7. }

注意我们已经拿掉了$scope参数。然后再修改下视图,加上前缀vm

  1. <div class="row" >
  2. <div class="col-md-9 page" >
  3. <div class="row topictype"><a href="/" class="label label-info">全部</a><a href="/">读书</a><a href="/">书评</a><a href="/">求书</a><a href="/">求索</a></div>
  4. <div class="error">{{ vm.message }}</div>
  5. <div class="row topiclist" data-ng-repeat='topic in vm.data'>
  6. <img data-ng-src='{{topic.img}}'><span class="count"><i class="coment">{{topic.commentCount}}</i><i>/</i><i>{{topic.visitedCount}}</i></span>
  7. <span class="label label-info">{{topic.type}}</span><a href="/">{{topic.title}}</a>
  8. <span class="pull-right">{{topic.createdOn}}</span><a href="/" class="pull-right author">{{topic.author}}</a>
  9. </div>
  10. </div>
  11. <div class="col-md-3">
  12. <div class="userinfo">
  13. <p>{{vm.user.userName}}</p>
  14. </div>
  15. </div>
  16. </div>

service:

因为服务是给全局调用的,而不是只服务于home,所以再在app_clinet下新建一个目录:common/services文件夹,并创建一个ReadData.service.js :

  1. angular
  2. .module('readApp')
  3. .service('topicData', topicData);
  4.  
  5. function topicData ($http) {
  6. return $http.get('/api/topics');
  7. };

直接拿来上一节的代码。注意function写法, 最好用function fool()的方式,而不要var fool=function() 前者和后者的区别是前者的声明会置顶。而后者必须写在调用语句的前面,不然就是undefined。修改layout

  1. script(src='/app.js')
  2. script(src='/home/home.controller.js')
  3. script(src='/common/services/ReadData.service.js')

相应的home.controller.js 改动:

  1. function homeCtrl(topicData) {
  2. var vm = this;
  3. vm.message = "loading...";
  4. topicData.success(function (data) {
  5. console.log(data);
  6. vm.message = data.length > 0 ? "" : "暂无数据";
  7. vm.data = data;
  8. }).error(function (e) {
  9. console.log(e);
  10. vm.message = "Sorry, something's gone wrong ";
  11. });
  12. vm.user = {
  13. userName: "stoneniqiu",
  14. };
  15. }

这个时候页面已经出来了,但是日期格式不友好。接下来添加过滤器和指令

filter&directive

在common文件夹创建一个filters目录,并创建一个formatDate.filter.js文件,同上一节一样

  1. angular
  2. .module('readApp')
  3. .filter('formatDate', formatDate);
  4.  
  5. function formatDate() {
  6. return function (dateStr) {
  7. var date = new Date(dateStr);
  8. var d = date.getDate();
  9. var monthNames = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
  10. var m = monthNames[date.getMonth()];
  11. var y = date.getFullYear();
  12. var output = y + '/' + m + '/' + d;
  13. return output;
  14. };
  15. };

然后在common文件夹下新建一个directive文件夹,再在directive目录下新建一个ratingStars目录。ratingStars指令会在多个地方使用,它包含一个js文件和一个html文件,将上一节的模板文件复制过来,并命名为:ratingStars.template.html。然后新建一个ratingStars.directive.js文件,拷贝之前的指令代码,并改造两处。

  1. angular
  2. .module('readApp')
  3. .directive('ratingStars', ratingStars);
  4.  
  5. function ratingStars () {
  6. return {
  7. restrict: 'EA',
  8. scope: {
  9. thisRating : '=rating'
  10. },
  11. templateUrl: '/common/directive/ratingStars/ratingStars.template.html'
  12. };
  13. }

EA表示指令作用的范围,E表示元素(element),A表示属性(attribute),A是默认值。还C表示样式名(class),M表示注释(comment), 最佳实践还是EA。更多知识可以参考这篇博客 Angular指令详解

因为还没有创建booksController,先用topic.commentCount来测试ratingStars指令,并记得在layout下添加引用

  1. <div class="row topiclist" data-ng-repeat='topic in vm.data'>
  2. <img data-ng-src='{{topic.img}}'><span class="count"><i class="coment">{{topic.commentCount}}</i><i>/</i><i>{{topic.visitedCount}}</i></span>
  3. <small rating-stars rating="topic.commentCount"></small>
  4. <span class="label label-info">{{topic.type}}</span><a href="/">{{topic.title}}</a>
  5. <span class="pull-right">{{topic.createdOn | formatDate}}</span><a href="/" class="pull-right author">{{topic.author}}</a>
  6. </div>

 这个时候效果已经出来了。

有哪些优化?

这一节和上一节相比,展现的内容基本没有变化,但组织代码的结构变得更清晰好维护了,但还是不够好,比如layout里面我们增加了过多的js引用。这也是很烦的事情。所以我们可以做一些优化:

1.减少全局变量

第一点,在团队开发的时候要尽量减少全局变量,不然容易混淆和替换,最简单的办法就是用匿名函数包裹起来:
  1. (function() {
  2. //....
  3. })();

被包裹的内容会在全局作用域下隐藏起来。而且在这个Angular应用也不需要通过全局作用域关联,因为模块之间都是通过angular.module('readApp', ['ngRoute'])连接的。controller、service、directive这些js都可以处理一下。

2.减少JavaScript的尺寸

我们可以让js最小化,但有一个问题,在controller中的依赖注入会受影响。因为JavaScript在最小化的时候,会将一些变量替换成a,b,c

  1. function homeCtrl ($scope, topicData, otherData)

会变成:

  1. function homeCtrl(a,b,c){

这样依赖注入就会失效。这个时候怎么办呢,就要用到$inject ,$inject作用在方法名称后面,等于是声明当前方法有哪些依赖项。

  1. homeCtrl.$inject = ['$scope', 'topicData', 'otherData'];
  2. function homeCtrl ($scope, topicData, otherData) {

$inject数组中的名字是不会在最小化的时候被替换掉的。但记住顺序要和方法的调用顺序一致。

  1. topicData.$inject = ['$http'];
  2. function topicData ($http) {
  3. return $http.get('/api/topics');
  4. };

做好了这个准备,接下来就可以最小化了

3.减少文件下载

在layout中我们引用了好几个js,这样很烦,可以使用UglifyJS 去最小化JavaScript文件。 UglifyJS 能将Angular应用的源文件合并成一个文件然后压缩,而我们只需在layout中引用它的输出文件即可。

 安装:
 

然后在根目录/app.js中引用

  1. var uglifyJs = require("uglifyjs");
  2. var fs = require('fs');
接下来有三步
1.列出需要合并的文件
2.调用uglifyJs 来合并并压缩文件。
3.然后保存在Public目录下。
在/app.js下var一个appClientFiles数组,包含要压缩的对象。然后调用uglifyjs.minify方法压缩,然后写入public/angular/readApp.min.js
  1. var appClientFiles = [
  2. 'app_client/app.js',
  3. 'app_client/home/home.controller.js',
  4. 'app_client/common/services/ReadData.service.js',
  5. 'app_client/common/filters/formatDate.filter.js',
  6. 'app_client/common/directive/ratingStars/ratingStars.directive.js'
  7. ];
  8.  
  9. var uglified = uglifyJs.minify(appClientFiles, { compress : false });
  10.  
  11. fs.writeFile('public/angular/readApp.min.js', uglified.code, function (err) {
  12. if (err) {
  13. console.log(err);
  14. } else {
  15. console.log('脚本生产并保存成功: readApp.min.js');
  16. }
  17. });

最后修改layout:

  1. script(src='/angular/readApp.min.js')
  2. //script(src='/app.js')
  3. //script(src='/home/home.controller.js')
  4. //script(src='/common/services/ReadData.service.js')
  5. //script(src='/common/filters/formatDate.filter.js')
  6. //script(src='/common/directive/ratingStars/ratingStars.directive.js')

这里选择注释而不是删掉,为了便于后面的调试。但如果用nodemon启动,它会一直在重启。因为生产文件的时候触发了nodemon重启,如此循环。所以这里需要一个配置文件告诉nodemon忽略掉这个文件的改变。在根目录下新增一个文件nodemon.json

  1. {
  2. "verbose": true,
  3. "ignore": ["public//angular/readApp.min.js"]
  4. }

这样就得到了一个min.js 。原本5个文件是5kb,换成min之后是2kb。所以这个优化还是很明显的。

源码:https://github.com/stoneniqiu/ReadingClub (注意不同分支)

小结:这一节主要是构建SPA的基础环境,和以前不同的是我们将视图、路由、一部分的逻辑从服务端的Express移到了前端的Angular,学习了Angular路由、视图,结构上更加清楚,最后对整体的JavaScript进行了优化。下一节再更深入的讲解基于Angular的SPA。

Nodejs之MEAN栈开发(六)---- 用Angular创建单页应用(上)的更多相关文章

  1. Nodejs之MEAN栈开发(七)---- 用Angular创建单页应用(下)

    上一节我们走通了基本的SPA基础结构,这一节会更彻底的将后端的视图.路由.控制器全部移到前端.篇幅比较长,主要分页面改造.使用AngularUI两大部分以及一些优化路由.使用Angular的其他指令的 ...

  2. Nodejs之MEAN栈开发(四)---- form验证及图片上传

    这一节增加推荐图书的提交和删除功能,来学习node的form提交以及node的图片上传功能.开始之前需要源码同学可以先在git上fork:https://github.com/stoneniqiu/R ...

  3. Nodejs之MEAN栈开发(九)---- 用户评论的增加/删除/修改

    由于工作中做实时通信的项目,需要用到Nodejs做通讯转接功能,刚开始接触,很多都不懂,于是我和同事就准备去学习nodejs,结合nodejs之MEAN栈实战书籍<Getting.MEAN.wi ...

  4. Nodejs之MEAN栈开发(三)---- 使用Mongoose创建模型及API

    继续开扒我们的MEAN栈开发之路,前面两节我们学习了Express.Jade引擎并创建了几个静态页面,最后通过Heroku部署了应用. Nodejs之MEAN栈开发(一)---- 路由与控制器 Nod ...

  5. Nodejs之MEAN栈开发(五)---- Angular入门与页面改造

    这个系列一共会涉及两个JavaScript框架的讲解,一个是Express用做后端,一个是Angular用于前端.和Express一样,Angular分离内容,处理视图.数据和逻辑.和MVC模式很相似 ...

  6. Nodejs之MEAN栈开发(八)---- 用户认证与会话管理详解

    用户认证与会话管理基本上是每个网站必备的一个功能.在Asp.net下做的比较多,大体的思路都是先根据用户提供的用户名和密码到数据库找到用户信息,然后校验,校验成功之后记住用户的姓名和相关信息,这个信息 ...

  7. Nodejs之MEAN栈开发(二)----视图与模型

    上一节做了对Express做了简单的介绍,提出了controller,介绍了路由.这一节将重点放到视图和模型上,完成几个静态页面并部署到heroku上. 导航 前端布局使用bootstrap,从官网下 ...

  8. Nodejs之MEAN栈开发(一)---- 路由与控制器

    因为工作需要,最近再次学习了node,上一次学习node是2014年,纯粹是个人兴趣,学了入门之后没有运用,加上赶别的项目又不了了之.这次正好捡起来.废话不多说,这里的MEAN指的是Mongodb.E ...

  9. Python 全栈开发六 常用模块学习

    本节大纲: 模块介绍 time &datetime模块 random os sys shutil json & picle shelve configparser hashlib 一. ...

随机推荐

  1. SignalR系列续集[系列8:SignalR的性能监测与服务器的负载测试]

    目录 SignalR系列目录 前言 也是好久没写博客了,近期确实很忙,嗯..几个项目..头要炸..今天忙里偷闲.继续我们的小系列.. 先谢谢大家的支持.. 我们来聊聊SignalR的性能监测与服务器的 ...

  2. Android权限管理之RxPermission解决Android 6.0 适配问题

    前言: 上篇重点学习了Android 6.0的运行时权限,今天还是围绕着Android 6.0权限适配来总结学习,这里主要介绍一下我们公司解决Android 6.0权限适配的方案:RxJava+RxP ...

  3. SQL Server-聚焦NOT IN VS NOT EXISTS VS LEFT JOIN...IS NULL性能分析(十八)

    前言 本节我们来综合比较NOT IN VS NOT EXISTS VS LEFT JOIN...IS NULL的性能,简短的内容,深入的理解,Always to review the basics. ...

  4. NET Core-TagHelper实现分页标签

    这里将要和大家分享的是学习总结使用TagHelper实现分页标签,之前分享过一篇使用HtmlHelper扩展了一个分页写法地址可以点击这里http://www.cnblogs.com/wangrudo ...

  5. webapp应用--模拟电子书翻页效果

    前言: 现在移动互联网发展火热,手机上网的用户越来越多,甚至大有超过pc访问的趋势.所以,用web程序做出仿原生效果的移动应用,也变得越来越流行了.这种程序也就是我们常说的单页应用程序,它也有一个英文 ...

  6. 谈一谈NOSQL的应用,Redis/Mongo

    1.心路历程 上年11月份来公司了,和另外一个同事一起,做了公司一个移动项目的微信公众号,然后为了推广微信公众号,策划那边需要我们做一些活动,包括抽奖,投票.最开始是没有用过redis的,公司因为考虑 ...

  7. springMVC学习笔记--知识点总结1

    以下是学习springmvc框架时的笔记整理: 结果跳转方式 1.设置ModelAndView,根据view的名称,和视图渲染器跳转到指定的页面. 比如jsp的视图渲染器是如下配置的: <!-- ...

  8. DOM、BOM 操作超级集合

    本章内容: 定义 节点类型 节点关系 选择器 样式操作方法style 表格操作方法 表单操作方法 元素节点ELEMENT 属性节点attributes 文本节点TEXT 文档节点 Document 位 ...

  9. C++ 拷贝构造函数和赋值运算符

    本文主要介绍了拷贝构造函数和赋值运算符的区别,以及在什么时候调用拷贝构造函数.什么情况下调用赋值运算符.最后,简单的分析了下深拷贝和浅拷贝的问题. 拷贝构造函数和赋值运算符 在默认情况下(用户没有定义 ...

  10. unity3d导出到IOS程序下 集成unity3dAR功能

    转载自: 来自AR学院(www.arvrschool.com),原文地址为:http://www.arvrschool.com/index.php?c=post&a=modify&ti ...