Keep moving just play & have fun

Angular1.x实践总结

2018-03-13
May

阅读:

Web

Angular1.x实践总结

Why Angular

进入公司的第一个前后端分离项目,在Angular1.x和React之间选型。主要从两个方面考虑:

框架本身的问题

  • 成熟度、背后支持

  • 具备的功能

  • 采用什么架构和模式

  • 生态系统是否丰富

需要自我反思的问题:

  • 团队是否能轻松学习并掌握

  • 是否适合项目

  • 开发体验是否足够好

从框架功能上来讲,两个都具备一些相同的核心功能:组件化、数据绑定以及平台无关的Render机制。

Angular

Angular除了提供一些需要最新浏览器支持的功能外,同时提供了

  • 依赖注入

  • 模板

  • 路由

  • ajax

  • 表单

  • 组件化CSS封装

  • XSS保护

  • 单元测试工具

React

相对Angular,React提供的功能就相对“简约”:

  • 无依赖注入

  • 使用JSX代替传统的HTML Templates

  • XSS保护

  • 单元测试工具

从语言与模式上来讲,Angular使用的对象都是传统的POJO对象,React使用JSX语法,这种在javascript中编写HTML标记的语言通常容易成为诟病。

项目结构


目录build、release主要放置编译、打包压缩后的前端代码,mocks是基于express编写的模拟服务restful接口,与前端页面服务分处于不同端口不同域下,因此express需要进行CORS跨域处理。partials目录用来存放全部工程代码,项目结构如下:

每个业务模块将javascript、css样式和html分离编写,css采用scss进行预编译。在gulp中配置命令合并全部scss文件后再处理成css文件,便于colors、resets全局变量共享。

Index索引页


index.html是一个标准的h5页面,但是内置了URL的配置模块,方便实施人员根据现场服务环境,对后端接口地址进行配置修改。但更好的实践是单独将其作为一个配置文件引入,代价是需要调整打包策略,避免gulp对其进行压缩混淆操作。

App程序启动点


app.js是整个程序的入口点,gulp自动化压缩后会作为bundle.js文件最顶部的一段代码,因此这里开启javascript严格模式后全局有效。每个js文件都使用立即调用函数表达式IIFEImmediately-Invoked Function Expression)进行封装,防止局部变量泄漏到全局。

Module


Angular module中的路由配置是整个前端的切割点,通过它完成了整个单页面应用在源码层的文件切分。路由是几乎所有的MVC(VM)框架都应该具有的特性,它是所有单页面应用(SPA)必不可少的组成部分。对于angular1也有内置的路由模块ngRoute,但是它难以解决多视图和嵌套视图的问题,所以项目中使用了ui-router作为路由解决方案。每一个路由都可以理解为一个state,$stateProvider.state(…)方法,它做了些什么工作?

  1. 首先,创建并存储一个state对象,里面包含该路由规则的所有配置信息。
  2. 然后,调用$urlRouteProvider.when(…)方法,进行路由的注册。
  3. 当hash值与state.url相匹配时,就跳转到该state。 为什么说跳转到该state,而不是url?ui.router是基于state(状态)的,而不是url。 $urlRouteProvider.when(…)方法,它做了哪些事情呢? 路由的查找匹配 这得从页面加载完毕说起:
  4. angular在刚开始$digest时,$rootScope会触发$locationChangeSuccess事件,(angular在每次浏览器hash change的时候也会触发$locationChangeSuccess事件)
  5. ui.router监听$locationChangeSuccess事件,于是开始通过遍历一系列rules,进行路由查找匹配
  6. 当匹配到路由后,就通过$state.transitionTo(state,…),跳转激活相应的state
  7. 最后,完成数据请求和模板渲染。

Controller控制器


控制器是Angular的核心之一,它的作用主要是对视图中的数据和事件处理函数进行挂在,同时进行一定的业务功能的底层封装和处理。

在Angular1.2之前控制器的定义是直接通过全局函数来处理,这样会造成全局数据污染,所以在Angular1.2版本之后做了彻底修改。

控制器的作用:

  1. 通过$scope进行数据状态的初始化操作

  2. 通过$scope进行事件处理函数的挂载操作

Directive指令


使用指令需要以下场景:

  1. 使你的html更具语义化,不需要深入研究代码和逻辑即可知道页面的大致逻辑

  2. 抽象一个自定义组件,在其他地方进行重用

指令包括以下几个内容

1.restrict 指定指令在DOM中以什么形式被声明;取值有E(元素),A(属性),C(类),M(注释)。

2.priority 指定指令的优先级,当一个DOM上有多个指令时,则优先级高的先执行。

3.terminal 布尔型,若设置为true,则优先级低于其他指令的指令则无效

4.template 字符串或函数

5.scope 作用域

  1. 默认值为false,表示继承父作用域;儿子继承父亲的值,改变父亲的值,儿子的值也跟着改变,反之亦然,这就是继承且不隔离。
  2. true,表示继承父作用域,并创建自己的作用域(子作用域)。儿子继承父亲的值,父亲改变儿子跟着改变,但时改变儿子的值,父亲的值并没有改变,这就是继承但是隔离。
  3. {}表示创建一个全新的隔离作用域。没有继承父亲的值,所以儿子的值为空,改变任何一方的值都不影响另一方,这就是不继承隔离。

directive在使用隔离scope的时候,提供了三种方法同隔离之外的地方交互。分别是:

  1. @ 绑定一个局部的scope属性到当前dom
  2. & 提供一种方式执行一个表达式在父scope的上下文
  3. =同哟directive的attr属性的值在局部scope的属性和父scope的属性名之间建立双向绑定。

JWT权限控制


JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于json的开放标准。

JWT的声明一般被用在身份提供者和服务提供者间传递被认证的用户身份信息,以便从资源服务器获取资源。比如用在用户登录上。

基于session的登陆认证###

http协议本身是无状态的,而这就意味着,每次请求来时服务器不知道是哪个用户发出的。为了能识别用户,我们只能在服务器存储一份用户登录的信息,这份信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给服务器,这样应用就能识别是哪个用户发出的请求。这就是传统的基于session的认证。

基于session认证所暴露的问题###

服务器开销 每个用户经过我们的应用认证,我们的应用都会在服务器端做一次记录,以便下次用户请求的鉴别,通常session都是保存在内存中的,而随着认证用户的增多,服务端的开销会明显增大。 扩展性 用户认证之后,服务端做认证记录,如果认证记录被保存在内存中的话,这意味着下次请求还必须要请求在这台服务器上,才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡的能能力。 CSRF 因为是基于cookie来进行用户的识别,如果cookie被拦截,用户很容易受到跨站请求伪造攻击。

基于token的鉴权机制###

基于token的鉴权机制类似与http协议,也是无状态的,他不需要在服务端保留用户的认证信息或者会话信息。这就意味这基于token认证机制的应用不需要考虑用户在哪一台服务器登录了,这为应用的扩展提供了便利。 流程上是这样的:

  • 用户使用用户名密码请求服务器
  • 服务器验证用户信息
  • 服务器通过验证并发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据。

这个token必须在每次请求时传递给服务端,它应该保存在http请求头里,另外,服务器端要支持CORS策略。一般在服务端加上Access-Control-Allow-Origin: * 就可以了

如何应用###

一般在请求头里加入 Authorization

fetch('api/user/1', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
})

Module中的run和config


run和config分别是angular模块加载的两个生命周期:

1. **config** :首先执行的是config阶段,该阶段发生在provider注册和配置的时候,主要用于连接并注册好所有数据源,因此provider、constant都可以注入到config代码块,但是其他不确定是否初始化完成的服务不能注入进来。
2. **run**:其次开始进入run阶段,该阶段发生在injector创建完成之后,主要用于启动应用,为了避免在模块启动之后再进行配置操作,所以只允许注入service、value、constant.

$location的配置

$location服务是angular对浏览器原生window.location的封装,可以通过$locationProvider进行配置.

$locationProvider.html5Mode(true).hashPrefix("*");
  • Hash模式: http://localhost:5008/#!/index
  • Html5模式: http://localhost:5008/index

控制器之间的事件交互


当angular应用中一个页面存在多个控制器时,可以通过scope的事件机制进行通信。

  • $scope.on(name, listener);监听给定类型的事件;
  • $scope.emit(name, listener);向上派发事件,可以携带参数;
  • $scope.broadcast(name, listener);向下派发事件,可携带参数。

angular的html模板编译步骤


angular中的html模板编译经历3个步骤:

  1. $compile遍历DOM查找匹配的angular指令

  2. 当DOM上的所有指令被识别,$compile会按其priority属性的优先级进行排序,接下来指令的compile函数被执行,每一条指令的compile函数都将返回一个link函数,这些link函数最终会被合并到一个统一的链接函数当中。

  3. $compile通过这个被合并的链接函数,依次调用每个指令的link函数,注册监听器到html元素,以及在scope中设置$watch,最后完成scope和template的双向绑定。

一旦遍历和编译完毕就回返回一个叫模板函数的函数。在这个函数被返回(return)之前我们可以对编译后的DOM进行修改。通常情况下,如果设置了compile函数,说明我们希望在指令和实时数据被放到DOM中之前进行DOM操作,在这个函数中进行DOM的操作诸如添加删除节点等是安全的。

当我们设置了link选项,实际上是创建了一个postLink链接函数,以便compile函数可以定义链接函数。compile函数负责对模板DOM进行转换。link函数负责将作用域和DOM进行链接。

compile和link的区别

  • compile函数的作用是对指令的模板进行转换
  • link函数的作用是建立模型和视图间的关联,包括在元素上注册监听事件。
  • scope在链接阶段才会被绑定到元素上,因此compile阶段操作scope会报错。
  • 对于同一个指令的多个实例,compile只会执行一次,而link对于指令的每个实例都会执行一次。
  • 一般情况下只需要写link函数就可以了。
  • 如果编写的自定义的compile函数,则自定义的link函数无效,因为compile函数会返回一个link函数供后续处理。

$digest循环和$apply


angular为我们提供一个广为人知的令人惊叹的功能就是双向绑定。那angular是怎么做到的呢?

angular为数据模型设置了一个监听器watcher,正是由于这个监听器,无论合适数据模型发生变化,视图也会更新。而angular是怎么知道模型发生变化而去调用相应的监听器的呢?这就要从$digest循环开始说起。

监听器(watcher)是在$scope.$digest()中被启用的。当运行$digest时,$digest循环就开始了,$digest循环开始的时候,他会启动每一个watcher监听器。这些监听器会去检查当前的数据模型的值是否与最后一次计算的相同,如果不相同,对应的监听函数就会被执行。

但是,angular并不是直接调用$digest,而是通过$scope.$apply(),然后调用相应的$rootScope.$digest()。

什么时候需要人为的调用$apply()呢

angular指挥关心在angualr的执行上下文中发生的数据模型变化。angular内建的指令也会自动触发$digest()循环。

但是,如果我们更改一个不在angular执行上下文的数据模型,就需要人为的调用$apply()来提醒angular数据模型发生了变化。例如,当使用javascript的setTimeout()函数来更新数据模型时,或者在指令中设置了DOM事件监听器,更改数据模型的代码在事件处理函数里,页需要调用$apply()来保证数据的更新。

$digest循环要执行多少次

加入一个监听函数自己改变了数据模型,angualr怎么知道呢?

答案是,$digest循环并不是只运行一次。在当前循环结束之后,他会再次启动来检查是否有数据模型发生改变,这叫做脏检查。所以,$digest循环会一直保持循环知道没有数据模型发生改变,或者达到最大循环次数(10次)。($digest至少会循环两次即使监听函数没有更改任何数据模型。)

综上所述,在$digest脏检查机制带来的双向绑定遍历的同时,也引来了angular的“性能杀手”。

慎用$watch,及时销毁

要想提高angular页面的性能,那么在开发的时候,就应该尽量减少显式使用$scope.watch函数,尽量复用ng内置指令和事件指令。

one-time绑定

angular1.3引入了新语法,,以“::”作为前缀,一次性绑定。

滚屏加载

又称“Endless Scrolling”,“unpagination”,这是用于大量数据集显示的时候,又不想表格分页,所以一般放在页面最底部,当滚动屏幕到达页面底部的时候,就会尝试加载一个序列的数据集。

Provider Provider用于创建可以由injector依赖注入的服务,Provider需要通过auto模块中的$provide服务进行创建。 所有的供应商都纸杯实例化一次,也就是说他们都是单例的。 Provider拥有以下6中创建方式。

  1. constant
  2. value 就是一个简单的可注入的值
  3. service 是一个可注入构造器
  4. factory 是一个可注入的方法。它与service的区别就是factory就是一个普通的function,而service是一个构造器(constructor)。angular在调用service的时候会用new,而调用factory就是调用普通的function。
  5. decorator 可以修改或封装其他的供应商,除了constant
  6. provider 是一个可配置的factory

$sce服务 ***

$sce用于在angular中提供严格的上下文转义(SCE, Strict Content Escaping)服务,从而避免XSS(跨站脚本攻击)等安全问题。


下一篇 HTTP相关

Comments

Content