AngularJS实践
什么是AngularJS
AngularJS的核心理念是什么? 在我看来,Angualr的核心思想是:Template + Scope => HTML, Template就是各种可以复用的模板,Scope就是数据.
WEB前端原本是DOM构成一个树形结构,然后数据杂乱分布. 开发者用JQuery之类的工具在DOM上添加各种各样的数据(data, trigger, listener)等.当网页的结构复杂起来之后,各种数据之间的关联错综复杂,到最后几乎没有人可以说清楚这个庞然大物里存在多少关联. 复杂的WEB应用里有大量同类型的节点,也导致JQuery里的$("#id")到处存在,维护性极差.
AngularJS从另外一个角度来解决,将数据组织成树状结构,每一个数据节点绑定对应的模板之后就渲染出对应的HTML. 所有的数据,包括函数,触发器都绑定在这棵树上, 每一个节点的渲染过程只跟该节点上的数据以及绑定的模板有关,需要协调的关系只能通过这棵树向上追溯. 比如两个模块需要共享一个用户登录状态时,可以将用户状态定义在两个元素上节点的共同祖先节点上,这样生成两个元素节点时都引用祖先的数据,就可以生成统一的HTML了.
这是从上往下生成HTML的过程,那么如果祖先节点上的状态发生了变化,怎么让子孙节点也同步变化呢. AngularJS提供了一个检查机制, 模板是不会变化的,而HTML的生成只与模板和数据相关. 因此AngularJS沿着数据树根节点往下检查,一旦某个数据节点发生了变化,就立刻重新渲染这个节点.
为了能及时进行检查, AngularJS绑定了WEB页面上的大部分操作, 比如按键等. 一旦有这些操作发生时, AngularJS都会开始脏检查, 这样可以确保页面的不断更新. 当然这一切是建立在现在浏览器处理能力越来越强的基础上的.
AngularJS与JQuery
AngularJS也需要操作DOM,因此AngularJS有一个JQuery的子集JQLite来做DOM操作. 但AngularJS的理念是模块化和封闭化, 每一个节点的渲染封闭在该节点的DATA和Template内部, 只能通过父节点了解外部变化. 这个与JQuery那种完全扁平的结构是冲突的, 使用JQuery,我们可以在随意的位置修改HTML上任意一个节点. 这种方便性就是导致最后页面无法维护的原因.
正因为这个理念的原因, AngularJS和JQuery其实是最好不要共存的. 在使用AngularJS的很多时候我们都会有忍不住拿出JQuery来的冲动, 明明看起来很容易完成的任务, 被AngularJS封装起来后仿佛变得复杂了很多. 这个时候更建议大家重新看一下自己的设计是否需要调整. AngularJS是一个全局的理念, 不是可以一半JQuery一半AngularJS (当然有些大神能够很好的将项目划分开, 也就尽信书不如无书了, 这个当然没有绝对的方案).
Directive的compile, link
Angular中controller是用来将view上的操作与实际业务逻辑挂钩,如果需要对dom进行操作时,directive才是合适的选择。directive中有两个非常重要的概念:compile和link
- compile
compile是一个预处理过程,directive在compile是与实际状态无关的,compile的目的就是为了得到最终的link函数。 - link
link函数负责在得到具体的scope之后渲染出最终的HTML页面。
常见问题
一些技巧
Server端初始化页面参数
我们经常会遇到的问题是需要从Server得到整个页面的一些初始化条件,然后再用AngularJS在页面端运行整个App。 有一个解决方案是在页面端通过Ajax去Server请求。不过既然整个页面都是从Server请求来的,这个Ajax请求未免有点多余。
这里提供一个解决方案,利用页面渲染过程在页面上写入启动参数。
我的实际做法如下:
在页面模板上写一个angular service:(我这里用的是Jade渲染引擎,读者只关注里面的js code就好)
script.
var serverInit = angular.module("serverInit", []);
serverInit.factory("serverParams", function(){
var params = !{JSON.stringify(serverParams)};
return params;
});
然后在server端渲染时给出参数:
res.render('page', {serverParams : {
Id : "51955"
}});
这样在页面端我就可以使用这个初始化参数了
var app = angular.module("testApp", ['serverInit']);
app.controller("testController", function(serverParams){
var id = serverParams.Id;
}
Angular Mocks测试时不拦截Http请求
在测试Angular程序时,我们经常会使用到AngularMocks库,这个库会用$httpBackend拦截被测试代码中的$http请求,这样可以测试代码是否如预期发送出了http请求,并且返回伪造的结果。
但如果我们不需要请求被拦截,而是需要请求发送到真实Server怎么办?StackOverflow上有一个网友给出了一个精彩的方案:
首先在测试代码文件中写一个新的angular module。
angular.module('httpReal', ['ng'])
.config(['$provide', function($provide) {
$provide.decorator('$httpBackend', function() {
return angular.injector(['ng']).get('$httpBackend');
});
}])
.service('httpReal', ['$rootScope', function($rootScope) {
this.submit = function() {
$rootScope.$digest();
};
}]);
然后在需要测试代码中注入httpReal Service, 这个Service会将真正的$httpBackend替换回来!这样angularMocks的拦截功能就无效了。
describe('my service', function() {
var myService, httpReal;
beforeEach(module('myModule', 'httpReal'));
beforeEach(inject(function( _myService_, _httpReal_ ) {
myService = _myService_;
httpReal = _httpReal_;
}));
it('should return valid data', function(done) {
myService.remoteCall().then(
function(data) {
expect(data).toBeDefined();
done();
}, function(error) {
expect(false).toBeTruthy();
done();
});
httpReal.submit();
});
});
注意这个submit(),因为在AngularJS中,使用了$q来返回promise,而promise的触发是需要在$scope做脏检查的时候做的,而在UnitTest需要手动调用一次$digest去触发。