本笔记已完结;前面一部分自己总结,中间一部分来自 《AngularJS深度剖析与最佳实践》章节,后面“点滴”部分来自知乎平台。1/29/2019
背景
现在手头一个项目,采用的 AngularJS 技术栈,项目因为各种客观原因无法采用最新的技术栈来重构,但又需要维护和加入新的业务功能,因此需要较全面的了解 AngularJS;
通过大半年时间,深入业务的使用 AngularJS,已经完全扭转了我以前基于 DOM 的编程思维,基于数据模型的开发非常便捷高效!尤其是 AngularJS 这类大而全的框架方案,使人对“工程”的了解更加深入!
采用 AngularJS,开发者素质不高,程序运行性能将是最大的问题,运行项目 Shift + ESC
进行业务操作,内存占用(JavaScript 使用的内存)较高,以及 DevTools 中 Performance 测试 ‘Timer Fired / $digest’ 带来的性能瓶颈 … 另外,目前 AngularJS 已经处于 LTS 阶段;
这些技术环境换代带来的问题,并不阻碍开发者在学习和运用框架中去深入思考与理解其背后的架构思想,以了解软件工程的技术、思想、方法和概念。
问题
采用原生 JavaScript 或 jQuery 传统开发过程中,最大的问题是数据与DOM交织,如果再加上一个异步,要写出优雅的代码还是蛮有挑战的,更何况再加上一个团队协作;即使采用 MVC 这类设计模式对项目进行形式上的分层,仍然无法避免 DOM 交织,像 jQuery 中使用频率较高的 html/wrap/append/prepend/after/before/detach/remove/empty/
等方法,加上大量的字符串拼接,项目持久推进,代码量累积,参与开发者流动,解耦、结构优化、扩展、维护这类工程化问题会愈发凸显;
另外,大多数业务应用程序大多采用 Browser/Server 模型,交互体验上想达到 Client/Server 架构下的程序交互,因此基于 AJAX 和 HTML5 的 MV* 模式的 Single-Page Application (SPA) 技术栈兴起、流行与成熟;
MV* 这类设计模式框架,就是解决这些工程问题,最终目的就是各种解耦、各种效率提升、各种成本降低,实现软件工程在业务中的价值。
概念
“Web 应用由相互协作以达成特定目标的对象构成”,将“相互协作”和“特定目标的对象”拆解来实现,就有了 MV* 模式,
传统的 MV* 模式,需要项目本身采用创建型、结构型、行为型等设计模式去架构,以实现组件、模型、视图、控制器、模板,这是一个不小的挑战;而基于 MV* 模式的框架,具备“相互协作”的能力,并且对“特定目标的对象”进行了封装以供应,开发者开箱即用,只需明白模式理念对应的 API 即可;
AngularJS 大而全的项目解决方案,能让开发者从一个较高较深的角度去理解工程配置与调度,它并非只专注 MV* 模式的某一层;
AngularJS 需要明白的理念比较少,主要是围绕“特定目标的对象”能“相互协作”展开,它有引导程序(Bootstrap)、模块(Module)、供应者(Provider)、注入器(Injector)、依赖注入(Dependency Injection)、服务(Service)、视图(View)、组件(Component)、模板(Template)、表单(Form)、过滤器(Filter)、指令(Directive)、数据绑定(Data Binding)、表达式(Expression)、控制器(Controller)、模型(Model)、作用域(Scope)、表达式(Expression)、编译器(Compiler)、路由(Router)等概念,见下表:
概念 | 说明 |
---|---|
引导程序(Bootstrap) | 引导不同的模块(Module) |
模块(Module) | 模块化,功能集,为各组件提供编译上下文环境 |
依赖注入(Dependency Injection) | 负责创建和自动装载对象或函数 |
注入器(Injector) | 用来实现依赖注入(Injection)的容器 |
编译器(Compiler) | 用来编译模板(Template),并且对其中包含的指令(Directive)和表达式(Expression)进行实例化 |
供应者(Provider) | 提供“特定目标的对象” |
服务(Service) | 独立于视图(View)的、可复用的业务逻辑 |
控制器(Controller) | 视图(View)背后的业务逻辑 |
视图(View) | 用户看到的内容(DOM) |
组件(Component) | 可复用的模块 |
模板(Template) | 带有 Angular 扩展标记的 HTML |
模型(Model) | 用于显示给用户并且与用户互动的数据 |
作用域(Scope) | 用来存储模型(Model)的语境(context)。模型放在这个作用域中才能被控制器、指令和表达式等访问,$destroy ,$on ,$emit ,$broadcast , |
数据绑定(Data Binding) | 自动同步模型(Model)中的数据和视图(View)表现,$digest ,$apply ,$watch ,$observe |
表达式(Expression) | 模板中可以通过它来访问作用域(Scope)中的变量和函数,$parse ,$eval |
过滤器(Filter) | 负责格式化表达式(Expression)的值,以便呈现给用户 |
指令(Directive) | 用于通过自定义属性和元素扩展 HTML 的行为 |
表单(Form) | 提供验证服务、数据双向绑定,$parsers ,$formatters |
思考一下:哪些是用于组织功能?哪些是容器装载?哪些是功能对象?
结合工程化,Angular.JS 团队及社区还提供了如 animate、cookie、sanitize、localStorage、ui-router、translate等相关的服务;
MVVM
MVVM 模式,Model-View-ViewModel (模型-视图-视图模型),最早出现在微软的 WPF 和 SilverLight 框架中。MVVM 模式利用框架内置的双向绑定技术对 MVP (Model-View-Presenter) 模式变形,它引入了专门的 ViewModel (视图模型)来“粘合” View 和 Model,让 View 和 Model 进一步分离和解耦。
MVVM 模式的要点是:以领域对象 (Domain Model) 为中心,遵循“分离关注点”设计原则,这与 jQuery 的 DOM 驱动思维有显著差异,所以在做 MVVM 开发时应该谨记:
前端开发工程师不要先设计页面,然后用 DOM 操作去改变它。
在以往的 jQuery 开发中,我们会首先设计页面 DOM 结构,然后再利用 jQuery 来改变 DOM 结构或者实现动态交互效果。由于 jQuery 是为 DOM 驱动而设计的,所以对于拥有复杂交互逻辑的项目,JavaScript 代码会变得越来越臃肿,让交互逻辑分散到各处。
在 MVVM 模式框架中,我们要始终在脑子里挂着 Model 的弦。不能老想着“我有×××这个DOM,我要让它做×××变化”,而应该是先思考我们有或需要什么样的 Model 数据,然后设计我们的交互数据和交互逻辑,最后才去实现视图,并用 ViewModel 去粘合它们。
Angular.JS 启动过程
- 浏览器下载 HTML/CSS/JavaScript
- 浏览器构建 DOM 树
- jQuery 初始化
- AngularJS 初始化(2、3、4、5步任意顺序执行)
- 创建 module,作为其它对象的注册表
- 注册各种 AngularJS 对象,如 Controller、Service、Directive 等,如
myModule.controller()
是$controllerProvider.register
的快捷方式,myModule.service()
是$provide.service
的快捷方式,,myModule.directive()
是$compileProvider.directive
的快捷方式; - 注册后形成一个由名字和回调函数组成的对照表(并未执行),
- 注册
config
回调函数,在模块刚刚初始化时执行 - 注册
run
回调函数,在模块初始化完毕时执行
- jQuery 启动
- 触发 document 对象的 DOMContentLoaded 事件,执行
jQuery.ready
;执行 Angular 启动代码
- 触发 document 对象的 DOMContentLoaded 事件,执行
- Angular 启动,
- 查找
ng-app
指令节点,调用angular.bootstrap(element, moduleName)
,多个ng-app
启动需手动angular.bootstrap
方式来启动
- 查找
- 加载子模块
- 先前视图和数据模型未建立关联,无法反应数据模型的变动,于此,将之关联
- 此时,创建注入器(injector),将其关联到所在节点上
- 对当前节点所关联的模块及它所依赖的模块进行初始化;注册的
config
都会被顺序执行 - 第 4 步中注册的大多数对象尚未就绪,不能使用,在
config
回调函数中能够使用的只有注册的常量(Constant)对象和 Provider 类 - 这个阶段也是程序中唯一可以直接访问 Provider 类对服务进行配置的地方,如初始路由服务的 Provider,仅负责记录一个 URL 到“模板/控制器”组的映射表,供后面步骤使用
- 启动子模块
- 此阶段,各种 Angular 对象可以使用,包括 Service、Factory 等
- 路由模块获得控制权,使用
$location
服务解析当前页面的 URL - 根据 URL 查找对应的“模板/控制器”,来准备渲染一个页面
- 渲染页面
- 路由模块创建 Scope 对象
- 加载模板
$compile
对象解析模板成一个静态 DOM 树,逐个的扫描 DOM 树中的指令- 通过指令把 Scope 对象和 DOM 树关联,包括渲染内容的函数和进行事件处理的函数
- 替换一个特定指令所有的节点,在 ngRoute 中是带有
ng-view
的节点,在 angular-ui-router 中是带有ui-view
的节点
- 数据绑定与摘要循环
- 页面已经渲染,数据未渲染,使用 Scope 对象中的数据渲染视图
- 脏检查机制,每一个 Scope 成员变量求出一个摘要值保存在一个变量中,调用 Scope 对象的
$digest/$apply
方法时,重新计算一遍摘要值,数据变化则更新视图,如此为“摘要循环”,$apply
是对$digest
的包装 - 挂载第三方组件的事件,需要调用一次
$apply
- 于此,AngularJS 程序启动成功
Provider
Service 是对公共代码的抽象,遵循 DRY (不要重复你自己)原则;在工程实践中,引入服务的首要目的是为了优化代码结构,而不是复用;复用只是一项结果,不是目标;代码中混杂了表现层逻辑和业务层逻辑,需要认真考虑抽取服务,即使它看不到复用价值。
在 AngularJS 中,服务分成多种类型:
- 常量(Constant),声明不会被修改的值
- 变量(Value),声明会被修改的值
- 服务(Service),“把自己的孩子取名为‘孩子’”;与后端领域的“服务”实现方式相似,声明一个类,等待 AngularJS 把它
new
出来,然后保存这个实例,供它到处注入 - 工厂(Factory),不会被
new
,AngularJS 会调用这个函数获得返回值并保存,供它到处注入,取名为“工厂”,是因为:它本身不会被用于注入,使用的是它的产品 - 供应商(Provider),“工厂”只负责生产产品,规格不受控制,而“供应商”更加灵活,可对规格进行配置,以获得定制化的产品
除了 Constant,其它类型的服务,都是通过 Provider 实现;Constant 初始化时机非常早,可在 config
函数中使用;
纯数据,用 Value;需要添加行为时,用 Service;需要通过计算给出结果时,用 Factory;需要进行全局配置时,用 Provider;
类型 | Factory | Service | Value | Constant | Provider |
---|---|---|---|---|---|
可依赖其它服务 | 是 | 是 | 否 | 否 | 是 |
使用类型友好的注入 | 否 | 是 | 是 | 是 | 否 |
在 config 阶段可用 | 否 | 否 | 否 | 是 | 是 |
可用于创建函数/原生对象 | 是 | 否 | 是 | 是 | 是 |
-
可依赖其它服务
Value 和 Constant 的特殊声明形式,没有进行依赖注入的时机。 -
使用类型友好的注入
Factory 可以根据逻辑程序返回不同的数据类型,无法推断其结果是什么类型,对类型不够友好;Provider 灵活性比 Factory 更高。 -
在 config 阶段可用
只有在 Constant 和 Provider 类型在 config 阶段可用;其它都是 Provider 实例化之后的结果,只有在 config 阶段完成之后可用。 -
可用于创建函数/原生对象
Service 是 new 出来的,其结果必然是类实例,无法直接返回一个可供调用的函数或数字等原生对象。
AngularJS 提供 decorator
机制用来改变服务的行为,慎用。
指令生命周期
Inject → Compile → Controller 加载 → pre-link → post-link
点滴
UPDATE:2018/8/6
利用 AngularJS 的双向绑定机制、依赖注入、directive/controller/service 分层、AJAX数据获取,其它原生 JavaScript 处理。
UPDATE:2018/7/18
通过前几期的调研,对 AngularJS 有了一个整体上的认识,在 AngularJS Tutorial 上跑了一遍,算是在Runoob AngularJS 教程之后又对基础的做一次温习。
做了一个 ui router Demo,对其进行了较深入的理解,
做了一些 ui select Demo,对其进行了较深入的理解,
Demo 中采用了分层。
UPDATE:2018/7/11
Angularjs 指令与外层组件进行数据交互 (作者 MakingChoice):
- 和父级作用域共用一个
scope
(双向绑定/双向传递) - 继承父级作用域的
scope
(单向绑定/单向传递) - 通过事件监听来进行传递数据(/单向传递)
- 通过指令中
link
函数中attr进行获取父级数据(单向传递)
- 通过指令中
scope:{ }
中@
进行获取父级数据(单向传递) - 通过指令中
scope:{ }
中=
进行传递数据(双向绑定) - 通过
$scope.$watch()
进行数据监听进行数据传递(单向绑定) - 通过
$scope.$observe()
进行数据监听进行数据传递(单向绑定) - 指令通过
require:'ngModel'
进行数据传递(双向绑定)
angular.module('moduleName')
.directive('myDirective', function () {
return {
restrict: 'EA', //E = element(元素), A = attribute(属性), C = class, M = comment
scope: {
//@ reads the attribute value, = provides two-way binding, & works with functions
//@ 读取属性值, = 提供双向绑定, & 以函数一起工作
title: '@' },
template: '<div>{{ myVal }}</div>',
templateUrl: 'mytemplate.html',
controller: controllerFunction, //Embed a custom controller in the directive 在指令中嵌入一个自定义控制器
link: function ($scope, element, attrs) { } //DOM manipulation DOM 操作
}
});
// https://github.com/nixzhu/dev-blog/blob/master/2014-05-03-creating-custom-angularjs-directives-part-1-the-fundamentals.md
TODO:https://docs.angularjs.org/guide
UPDATE: 2018/7/10
逻辑拆分(作者 徐飞):
- Angular 应用最重要的事情是分层
- Angular 不良代码症状是 controller 又大又乱
- 远程请求,数据缓存等等一律放进 service
- 不得以而产生的 DOM 操作,一律放进 directive
- 数据的格式化,一律做成 filter
- controller
- 视图分块、分层
- 把 controller 和视图块按照一对一的关系维护,每块单独都能跑,然后拼起来
- 嵌套的视图,考虑作用域的关系
- 不应当在视图分块(姑且称为组件)的树状结构里,而是独立在外,跟这部分东西的交互,应当视情况使用 service 来通信,不要尝试在 $scope 体系上过多纠缠
UPDATE: 2018/7/9
分层设计与编码
service 层:
- service 主要负责数据交互和数据处理、处理业务逻辑;
- service 持久化应用数据在不同的 controller 之间使用;
- services 应该是由其他模块调用;
- service 注册 参考2018/6/7“小问”第 5 点;
controller 层:
- controller 初始化 $scope 变量传递给 view 层,
- controller 处理页面(view)交互逻辑,如显示、隐藏、Loading;
- controller 只在需要的时候才会初始化,一旦不需要就会被抛弃;
以 service 形式注入 controller 层;
如 controller 完全不依赖 $http 等服务,只依赖 service 传递的事件和数据;
UPDATE: 2018/6/7
在知乎和其它平台看了一些行业人员的观点,做了一些笔记,不是总结,都是别人的经验,只是为了全局观的认识:
设计模式框架:声明式渲染、指令、响应式 (Reactive) 、组件化(Composable)、模块化、流程控制、表单验证、路由、测试、i18n、安全、脚手架”等方案;
UI 框架:框架无关 UI 样式库,PureCSS、Bulma,要带 UI 交互,选择一个和设计模式框架相关的框架,但一定不要带 jQuery(很优秀,但不合适);
回到笔记部分:
大问
-
angular 的数据绑定采用什么机制?详述原理
- 脏检查机制;
- AngularJS 在 scope 模型上设置了一个监听队列,用来监听数据变化并更新 view;
- 每次绑定一个东西到 view 上时 AngularJS 就会往
$watch
队列里插入一条$watch
,用来检测它监视的 model 里是否有变化的东西; - 当浏览器接收到可以被 angular context 处理的事件时,
$digest
循环就会触发,遍历所有的$watch
,最后更新 dom;如click
产生一次更新的操作<button ng-click="val=val+1">increase 1</button>
,至少触发两次$digest
循环:- 按下按钮
- 浏览器接收到一个事件,进入到 angular context
$digest
循环开始执行,查询每个$watch
是否变化- 由于监视
$scope.val
的$watch
报告了变化,因此强制再执行一次$digest
循环 - 新的
$digest
循环未检测到变化 - 浏览器拿回控制器,更新
$scope.val
新值对应的 dom $digest
循环的上限是 10 次(超过 10次后抛出一个异常,防止无限循环)
- angular 通过监控能事件改变进行绑定;DOM事件(input改变,点击等),XHR的响应触发回调;浏览器地址变化,计时器触发回调等
-
两个平级界面块 a 和 b,如果 a 中触发一个事件,有哪些方式能让 b 知道,详述原理
- 两个 controller 之间进行交互可以通过父作用域数据共享,或者事件的冒泡广播机制
- 平级界面模块间通过共用服务或基于事件两种方式进行通信
- 共用服务: 通过 factory 可以生成一个单例对象,在需要通信的模块 a 和 b 中注入这个对象
- 基于事件: 在子 controller 中向父 controller 触发(
$emit
)一个事件,然后在父 controller 中监听($on
)事件,再广播($broadcast
)给子 controller ,这样通过事件携带的参数,实现了数据经过父 controller,在同级 controller 之间传播。
-
一个 angular 应用应当如何良好地分层?
- 目录结构划分: 小型项目,可以按文件类型组织,controllers/models/services/filters/templates
- 业务模块划分: 较大项目;
- 逻辑代码划分: 模型/视图模型(控制器)/视图
- 让 controller 这一层很薄
- 提取共用的逻辑到 service 中,如后台数据的请求、数据的共享和缓存、基于事件的模块间通信等
- 提取共用的界面操作到 directive 中,如将日期选择、分页等封装成组件等
- 提取共用的格式化操作到 filter 中
- 可以为实体建立对应的构造函数,在相关的 controller 中注入构造器生成一个实例,实现复用
-
angular 应用常用哪些路由库,各自的区别是什么?
- Angular1.x 中常用 ngRoute 和 ui.router,它们作为框架额外的附加功能,都必须以 模块依赖 的形式被引入;
- ngRoute 模块是 Angular 自带的路由模块;
- ui.router 模块是基于 ngRoute模块开发的第三方模块;
- ui.router 是基于 state (状态)的, ngRoute 是基于 url 的,ui.router 模块具有更强大的功能,主要体现在视图的嵌套方面;
- 使用 ui.router 能够定义有明确父子关系的路由;并通过 ui-view 指令将子路由模版插入到父路由模板的
<div ui-view></div>
中去,从而实现视图嵌套,在 ngRoute 中不能这样定义,如果同时在父子视图中 使用了<div ng-view></div>
会陷入死循环。
-
如果通过 angular 的 directive 规划一套全组件化体系,可能遇到哪些挑战?
- 组件如何与外界进行数据的交互
- 如何通过简单的配置就能使用
- 性能,文档
-
分属不同团队进行开发的 angular 应用,如果要做整合,可能会遇到哪些问题,如何解决?
- 服务冲突,angular 将所有模块的所有服务混入了应用级别的单一命名空间里。开发时规划好,尽量避免冲突;
-
angular的缺点有哪些?
- 强约束
- 不利于 SEO
- 移动端
- 数据的双向绑定,大数组、复杂对象会存在性能问题
- 减少监控项,对不会变化的数据采用单向绑定
- 主动设置索引(指定
track by
,简单类型默认用自身当索引,对象默认使用$$hashKey
,比如改为track by item.id
) - 降低渲染数据量
- 数据扁平化
-
如何看待angular 1.2中引入的 controller as 语法?
- 在 angular 1.2 以前,在 view 上的任何绑定都是直接绑定在
$scope
上的。使用 controllerAs,不需要再注入$scope
,controller 变成了一个很简单的 javascript 对象(POJO),一个更纯粹的 ViewModel。 - controllerAs 语法是把 controller 这个对象的实例用 as 别名在 $scope 上创建了一个属性;
- 使用 controllerAs,View 上所有字段都绑定在一个引用的属性,可以避免遇到 AngularJS 作用域带来的问题;
- 不引入
$scope
会出现的一个问题是,导致$emit
、$broadcast
、$on
、$watch
等$scope
下的方法无法使用。这些跟事件相关的操作可以封装起来统一处理,或者在单个 controller 中引入$scope
,特殊对待;
- 在 angular 1.2 以前,在 view 上的任何绑定都是直接绑定在
-
详述angular的“依赖注入”
- AngularJS 是通过构造函数的参数名字来推断依赖服务名称的,通过
toString()
来找到这个定义的function
对应的字符串,然后用正则解析出其中的参数(依赖项),再去依赖映射中取到对应的依赖,实例化之后传入; - AngularJS 的 injector 是假设函数的参数名就是依赖的名字,然后去查找依赖项;
function myCtrl = ($scope, $http){ }
,代码压缩后参数被重命名,无法查找到依赖项;- 数组注释法,行内标记:
myApp.controller('myCtrl', ['$scope', '$http', function($scope, $http){ }])
- 显式 scope, inject = [‘ http’]; `
- DI 容器具备三个要素: 依赖项的注册,依赖关系的声明,对象的获取;
module
和$provide
都可以提供依赖项的注册;- 内置的
injector
可以获取对象(自动完成依赖注入) - 依赖关系的声明:通过构造函数的参数名。
- AngularJS 是通过构造函数的参数名字来推断依赖服务名称的,通过
-
如何看待angular 2……
小问
-
ng-if
跟ng-show/hide
的区别有哪些?- 是否会生成此 DOM 元素
ng-if
会(隐式地)产生新作用域,ng-switch
、ng-include
等会动态创建一块界面的也是如此;ng-if
在后面表达式为true
的时候才创建这个dom
节点;ng-show
是初始时就创建了,用display:block
和display:none
来控制显示和不显示
-
ng-repeat
迭代数组的时候,如果数组中有相同值,会有什么问题,如何解决?- 相同值不能管理数据与DOM之间一对一的关系,会提示
Duplicates in a repeater are not allowed.
- 加
track by $index
可解决 - 也可以trace by 唯一性标识数组中的任何一项数据值即可
- 相同值不能管理数据与DOM之间一对一的关系,会提示
-
ng-click
中写的表达式,能使用 JS 原生对象上的方法,比如Math.max
之类的吗?为什么?- 在 View 中,不能直接调用原生 JS 方法,这些方法不存在于与 View 对应的 Controller 的
$scope
中 - 执行时是View对应的控制器中的
$scope.Math.max
方法,这些方法不存在于与
- 在 View 中,不能直接调用原生 JS 方法,这些方法不存在于与 View 对应的 Controller 的
-
{{now | 'yyyy-MM-dd'}}
这种表达式里面,竖线和后面的参数通过什么方式可以自定义?app.filter('过滤器名称',function(){ return function(需要过滤的对象, 过滤器参数1, 过滤器参数2, ...){ //做一些事情 return 处理后的对象; } });
- 直接在页面或:
<p>{{now | date : 'yyyy-MM-dd'}}</p>
- 在 js 里使用:
$filter('过滤器名称')(需要过滤的对象, 参数1, 参数2,...)
-
factory 和 service,provider 是什么关系?
- 都用于注册服务,provider 只能配置阶段注入,factory 和 service 只能运行阶段注入;
- provider 创建一个可通过 config 配置的 service;通过
$get
工厂函数创建新对象; - factory 把服务的方法和数据放在一个对象里,并返回这个对象;通过工厂函数创建新对象;
- service 通过构造函数方式创建 service,返回一个实例化对象;通过构造函数创建新对象,需要
new
和给属性加this
;
-
directive 如何调用外部函数,如何向函数里传递参数,如何 expose 函数给外部调用?
-
html:
{{currentDate()}}
js:$scope.currentDate = function(){return new Date();}
这种写法有没有问题?$digest
- 时间是实时变化的,然后会一直更新数据,效率低,脏数据检查到10次之后不再继续检查;
- 使用一个变量来接收函数调用
-
controller as
和controller
有什么区别,能解决什么问题 (同上第8条)$scope
-
简述
$compile
的用法