0%

总结2019年的收获、巩固知识点

  • http
  • es6/es7/es8
  • pwa
  • angular
  • rxjs
  • flutter
  • 微信
  • webpack
  • 其他知识点

Let’s Review !

一、HTTP

1. GET/POST

GET / POST主要区别:属于HTTP协议的两种请求方式,协议规定get用于获取信息,post用于修改数据;当携带参数时,get的参数在url里且长度大多受到服务器或浏览器限制(1024字符),post在body里,post传输的参数在地址栏里不可见所以安全性比get略好,但是在数据传输过程中,他们都是可以被捕获的,所以都是不安全的,所以需要进行加密处理,用到https。本质上最大的区别在于get请求是幂等性的,post不是,引用HTTP协议:

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用, GET http://www.xxx.com/article/1, 无论调用多少次,获取的只是这一篇文章, 如果POST http://www.xxx.com/article/1, 相当于创建一篇文章,如果没有唯一索引(唯一索引保证其幂等性),则每调用一次,都会创建一篇文章,post不具有幂等性。delete,put同样具有幂等性,所以在http协议下get更适合获取,post更适合创建,put更适合更新,现如今更多的只是语义上的区别

2. HTTP报文

http报文有请求报文和响应报文两种。请求报文是从客户端向服务器发送请求报文,响应报文是服务端的回答
一个请求报文由四部分组成:请求行、请求头、空行、请求数据。
请求行包括请求方法字段、url字段、http协议版本字段,并用空格分隔
GET /index.html HTTP/1.1
请求头有字值对组成,典型的请求头有user-agent:浏览器类型,accept:客户端可识别的内容类型列表,host:请求的主机名,accept-encoding:允许的压缩方式,connection:keep-alive保持客户端到服务器连结持续有效,当后续触发请求时,避免建立连接释放连接的开销
最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器之后不会再有请求头
请求数据中常见的有content-type的几个常用类型:application/x-www-form-urlencoded—form表单,multipart/form-data—文件,application/json—JSON字符串

4. URL加载全过程

DNS解析(递归查询)—TCP连接—发送http请求—服务器处理请求响应—浏览器解析渲染—结束连接
DNS查找缓存顺序:浏览器缓存——>hosts文件——>路由器缓存——>网络服务商缓存——>根域名服务器缓存

5. HTTPS

HTTP报文是包裹在TCP报文中发送的,服务器端收到TCP报文时会解包提取出HTTP报文。但是这个过程中存在一定的风险,HTTP报文是明文,如果中间被截取的话会存在一些信息泄露的风险。那么在进入TCP报文之前对HTTP做一次加密就可以解决这个问题了。HTTPS协议的本质就是HTTP + SSL(or TLS)。在HTTP报文进入TCP报文之前,先使用SSL对HTTP报文进行加密。HTTPS在传输数据之前需要客户端与服务器进行一个握手(TLS/SSL握手),在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL使用了非对称加密,对称加密以及hash等

HTTPS原理详解

7. HTTP缓存

HTTP缓存有多种规则,根据是否需要重新向服务器发起请求来分类,我将其分为两大类(强制缓存,对比缓存)。对于强制缓存来说,响应header中会有两个字段来标明失效规则(Expires/Cache-Control)Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。常见的http缓存只能缓存get请求响应的资源,对于其他类型的响应则无能为力。

1
2
3
4
5
private:             客户端可以缓存
public: 客户端和代理服务器都可缓存
max-age=xxx: 缓存的内容将在 xxx 秒后失效
no-cache: 需要使用对比缓存来验证缓存数据(后面介绍)
no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发


HTTP缓存机制详解

8. cors预请求

很多时候发送一个post请求之前会先发送一个options请求,为什么会这样?cors本身是一种机制(跨域资源共享),为跨域访问提供了安全的数据传输。options作为预请求主要用途有两个:获取服务器支持的http请求方法,检查服务器的性能;ajax进行跨域请求时,需要向另一个域名的资源发送一个options请求用于判断实际要发出的请求是否安全。本身从不同的域访问资源是受到同源策略禁止的,所以cors定义了这种浏览器和服务器交互的形式允许跨域请求。现阶段大部分浏览器都支持cors机制,服务端则需要配置Access-Control-Allow-Origin:*,允许任何域发起请求。
不会触发cors预检的请求称之为简单请求,日常开发满足下列条件的称之为简单请求:1.使用get,post,head其中一种;2.只使用了如下的安全首部字段,没有人为的设置其他首部字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type仅限:text/plain、multipart/form-data、application/x-www-form-urlencoded

    6. 跨域

    解决跨域的方法有很多种。cors是比较好的解决方案。JsonP只支持get请求,但是适配老式浏览器
    9. TCP三次握手
    三次握手的过程:
    主机向服务器发送一个建立连接的请求(您好,我想认识您);
    服务器接到请求后发送同意连接的信号(好的,很高兴认识您);
    主机接到同意连接的信号后,再次向服务器发送了确认信号(我也很高兴认识您),自此,主机与服务器两者建立了连接。
    四次挥手的过程:
    主机向服务器发送一个断开连接的请求(不早了,我该走了);
    服务器接到请求后发送确认收到请求的信号(知道了);
    服务器向主机发送断开通知(我也该走了);
    主机接到断开通知后断开连接并反馈一个确认信号(嗯,好的),服务器收到确认信号后断开连接;
    为什么建立连接是三次?为什么断开链接是四次呢?
    为了确保数据全部发送并且防止服务器端一直等待而浪费资源;

10. 性能优化

合理使用缓存,将资源放在浏览器端,这是最快的方式(service worker);合理进行http缓存;如果资源必须从网络中加载,则要考虑缩短连接时间,即DNS优化部分;减少响应内容大小,即对内容进行压缩,静态资源优化gzip、br;减少reflow的次数;无用代码移除;tree-shaking;差异化加载;减少http请求;预加载;懒加载;节流防抖;内存回收;
will-change优化动画、滚动等效果的性能,尤其在移动端(调动更多的GPU资源进行计算和重绘)
缩减首屏白屏时间:从资源和视觉两方面去优化:懒加载/按需加载(提高首屏加载速度),视觉上可以采取Medium渐变加载的策略,给用户一个柔和的感觉。

二、ES6/ES7/ES8

ES全称ECMAScript,ECMAScript是ECMA制定的标准化脚本语言。

es6的特性跨度和es5比较大,所以es6的特性比较多,es7,es8是在es6的基础上进行的补充。下面列举一些常用的es6特性:

  • 模块化
  • 箭头函数
  • 模版字符串
  • 解构赋值
  • Promise
  • let、const
  • 延展操作符
  • 函数参数默认值

es6引入了类的概念,js的面向对象编程更加容易理解。模块化主要有export和import组成,每一个模块都有自己单独的作用域,通过export暴露对外接口,通过import来引用其他模块提供的接口。箭头函数是es6中最重要的特性之一,可以很好的处理this指针的问题,箭头函数中this继承的是父执行上下文中的this,;箭头函数和bind方法,每次执行后都会返回一个新的函数引用。

es7新增Array.prototype.includes(),用来判断一个数组是否包含指定的值,包含返回true,不包含返回false;新增指数操作符,Math.pow(2,10)等价于210;

es8新增async/await;

三、PWA

Progressive Web App, 简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。他的本质上还是一个web app,但是借助了技术使其也具备了native app的部分特性,并兼顾了web app 和 native app各自的优点;
几大特性:

  • 性能提升,能够快速响应,有着相对很平滑的动画体验
  • 一键生成,可以添加到桌面,终端设备,避免应用商店下载,趋向于native app的体验形式
  • 弱网、离线情况下正常运行,通过service-worker代理请求,操作浏览器缓存;
  • HTTPS协议下确保了应用的安全性
  • 持续更新

区别于其他类型APP:

WebAPP:开发成本低,更新简单,体验差,不具备离线和推送功能;
NativeAPP:开发成本高,需审核,需下载;体验好;
HybridAPP:介于web app 和 native-app之间,通过ui-webview访问里面;一套代码多端运行,兼容差,体验差;

Web Worker:
一个网页只会有两个线程:GUI 渲染线程和 JS 引擎线程。JS 引擎线程和 GUI 渲染线程是互斥的,因此在 JS 执行的时候,UI 页面会被阻塞住。为了在进行高耗时 JS 运算时,UI 页面仍可用,那么就得另外开辟一个独立的 JS 线程来运行这些高耗时的 JS 代码,这就是 Web Worker。Web Worker只能服务于新建它的页面,不同页面之间不能共享同一个 Web Worker。
当页面关闭时,该页面新建的 Web Worker 也会随之关闭,不会常驻在浏览器中。
Service Worker 在 Web Worker 的基础上加上了持久离线缓存能力.
依赖Promise、html5 fetch API、缓存机制依赖cache API、https环境。它能够拦截和处理网络请求,并且配合 Cache Storage API,开发者可以自由的对页面发送的 HTTP 请求进行管理,这就是为什么 Service Worker 能让 Web 站点离线的原因。

ScrollView in Flutter

想象有一种应用场景,scrollable组件内部嵌套另一个scrollable组件。特别是当同时显示ListView和GridView时应该怎么处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
return Scaffold (
appBar: AppBar(
title: Text('listView')
),
body: Container(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
someWidget,
someWidget,
someWidget,
ListView(
children: <Widget>[
anotherWidget,
anotherWidget,
anotherWidget
]
)
]
)
)
)
)

运行以上代码,控制台会抛出异常,其中关键的一句翻译过来就是:

垂直视口被赋予无限高度。这种情况通常在可滚动小部件嵌套在另一个可滚动小部件内时发生。

这个时候,我们可以通过Slivers来实现这一需求。通过以下代码替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
return Scaffold (
appBar: AppBar(
title: Text('listView')
),
body: Container(
child: CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(
someWidget,
someWidget,
someWidget,
)
),
SliverList(
delegate: SliverChildListDelegate(
anotherWidget,
anotherWidget,
anotherWidget,
)
),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
delegate: SliverChildListDelegate(
thirdWidget,
thirdWidget,
thirdWidget,
)
),
]
)
)
)


下面介绍Slivers系列常见的控件及使用场景

SliverAppBar

经常用于AppBar展开收起的场景,通过配置flexibleSpace和expandedHeight属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
actions: <Widget>[
_buildAction(),
],
title: Text('SliverAppBar'),
backgroundColor: Theme.of(context).accentColor,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.asset('images/food01.jpeg', fit: BoxFit.cover),
),
),
SliverFixedExtentList(
itemExtent: 120.0,
delegate: SliverChildListDelegate(
products.map((product) {
return _buildItem(product);
}).toList(),
),
),
],
);


flexibleSpace是被展开和收起的组件,expandedHeight是其操控的高度;其他属性具体含义可以参考官方文档

SliverList

SliverList只需要设置delegate属性就可以,可以滑动的列表,常常用于滑动组件嵌套的场景

1
2
3
4
5
6
7
8
9
10
11
12
CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _buildItem(context, products[index]);
},
childCount: 3,
),
)
],
);

也可以通过SliverChildListDelegate来构建

1
2
3
4
5
6
7
8
9
10
11
CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate([
_buildItem(),
_buildItem(),
_buildItem(),
]),
)
],
);
SliverChildListDelegate和SliverChildBuilderDelegate的区别:

SliverChildListDelegate一般用来构item建数量明确的列表,会提前build好所有的子item,所以在效率上会有问题,适合item数量不多的情况。
SliverChildBuilderDelegate构建的列表理论上是可以无限长的。
两者的区别有些类似于ListView和ListView.builder()的区别。



SliverGrid

SliverGrid有三个构造函数:SliverGrid.count()、SliverGrid.extent和SliverGrid()。

  • SliverGrid.count()指定了一行展示多少个item,下面的例子表示一行展示4个:
1
SliverGrid.count(children: scrollItems, crossAxisCount: 4)
  • SliverGrid.extent可以指定item的最大宽度,然后让Flutter自己决定一行展示多少个item
1
SliverGrid.extent(children: scrollItems, maxCrossAxisExtent: 90.0)
  • SliverGrid()则是需要指定一个gridDelegate,它提供给了程序员一个自定义Delegate的入口,你可以自己决定每一个item怎么排列
1
2
3
4
5
6
7
8
9
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _buildItem(products[index]);;
}
);


SliverPersistentHeader

SliverPersistentHeader顾名思义,就是给一个可滑动的视图添加一个头(实际上,在CustomScrollView的slivers列表中,header可以出现在视图的任意位置,不一定要是在顶部)。这个Header会随着滑动而展开/收起

1
2
3
4
5
6
7
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 180.0,
child: Container(),
),
);

构建一个SliverPersistentHeader需要传入一个delegate,这个delegate是SliverPersistentHeaderDelegate类型的,而SliverPersistentHeaderDelegate是一个abstract类,我们不能直接new一个SliverPersistentHeaderDelegate出来,因此,我们需要自定义一个delegate来实现SliverPersistentHeaderDelegate类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
@required this.minHeight,
@required this.maxHeight,
@required this.child,
});

final double minHeight;
final double maxHeight;
final Widget child;

@override
double get minExtent => minHeight;

@override
double get maxExtent => math.max(maxHeight, minHeight);

@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new SizedBox.expand(child: child);
}

@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}

写一个自定义SliverPersistentHeaderDelegate很简单,只需重写build()、get maxExtent、get minExtent和shouldRebuild()这四个方法,上面就是一个最简单的SliverPersistentHeaderDelegate的实现。其中,maxExtent表示header完全展开时的高度,minExtent表示header在收起时的最小高度。因此,对于我们上面的那个自定义Delegate,如果将minHeight和maxHeight的值设置为相同时,header就不会收缩了,这样的Header跟我们平常理解的Header更像。



SliverToBoxAdapter

 SliverPersistentHeader一般来说都是会展开/收起的(除非minExtent和maxExtent值相同),那么如果想要在滚动视图中添加一个普通的控件,那么就可以使用SliverToBoxAdapter来将各种视图组合在一起,放在CustomListView中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CustomScrollView(
physics: ScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: _buildHeader(),
),
SliverGrid.count(
crossAxisCount: 3,
children: products.map((product) {
return _buildItemGrid(product);
}).toList(),
),
SliverToBoxAdapter(
child: _buildSearch(),
),
SliverFixedExtentList(
itemExtent: 100.0,
delegate: SliverChildListDelegate(
products.map((product) {
return _buildItemList(product);
}).toList(),
),
),
SliverToBoxAdapter(
child: _buildFooter(),
),
],
);


在Google IO 2019大会上提出了新的状态管理方案Provider用来替代之前的状态管理方案Provide,针对不同类型对象提供了多种不同的Provider;Provider借助了InheritWidget,将共享状态放到顶层Widget。

很细心的讲解文章

  • 借助了InheritWidget,允许将有效信息传递到组件树下的小组件
  • 提供DI
  • 创建和销毁实例
  • 结合Bloc等进行状态管理

Let’s Code

1
2
3
4
5
6
const Provider.value({
Key key,
@required T value,
this.updateShouldNotify,
this.child
}) : dispose = null, super.value(key: key, value: value);

上面的源码value的类型为范型,并没有进行限制,所以可以绑定任意数据类型

如何绑定数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyApp extends StatelessWidget {
@override
Widget build (BuildContext context) {
return MaterialApp(
title: 'demo',
home: Provider<String>.value(
value: 'demo',
child: Demo()
)
)
}
}

Provider.of()在Provider窗口小部件对应的后代中BuildContext是必需的;获取BuildContext麻烦时Consumer()是很好的替代方式

class MyApp extends StatelessWidget {
@override
Widget build (BuildContext context) {
return MaterialApp(
title: 'demo',
home: Provider<String>.value(
value: 'demo',
child: Consumer<String>(
builder: (context, value, child) {
return Center(
child: Text(value)
)
}
)
)
)
}
}
如何获取数据

provider需要在绑定的子widget中获取数据,使用静态方法Provider.of(BuildContext context),此方法将从关联的widget树中查找最近的相同类型的数据

1
2
3
4
5
6
7
8
9
Class Demo extends StatelessWidget {
@override
Widget build (BuildContext context) {
final value = Provider.of<String>(context);
return Center(
child: Text(value)
)
}
}

#####以上简述了Provider的数据绑定几获取的方法,下面在应用场景里实现Provider的作用,以官方的计数器的例子来举例

首先创建一个Counter类,并封装他的增和减的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Counter with ChangeNotifier{
int _counter;
Counter(this._counter);
getCouonter => _counter;
setCounter(int counter) => _counter = counter;
void increment() {
_counter ++;
notifyListeners();
}
void decrement() {
_counter --;
notifyListeners();
}
}

现在Counter类拥有了监听的功能,我们需要调用notifyListeners()去通知监听器数据变化,并更新UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class MyApp extends StatelessWidget {
@override
Widget build (BuildContext context) {
return MaterialApp(
title: 'demo',
home: ChangeNotifierProvider<Counter>(
builder: (_) => Counter(0),
child: HomePage()
)
)
}
}

Class HomePage extends StatelessWidget {
@override
Widget build (BuildContext context) {
final counter = Provider.of<Counter>(context);
return Scaffold(
appBar: AppBar(
title: Text('ChangeNotifierProvider demo')
),
body: Center(
child: Text(counter.getCounter())
),
floatingActionButton: Column(
children: <Widget>[
FloatingActionButton(
onPressed: counter.increment,
child: Icon(Icons.add)
),
FloatingActionButton(
onPressed: counter.decrement,
child: Icon(Icons.remove)
),
]
)
)
}
}
Provider结合Bloc模式进行状态管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class CounterBloc {
final _valueController = StreamController<String>();
Stream<String> get stream => _valueController.stream;
int _number = 0;
void increment() {
_number++;
_valueController.sink.add(_number.toString());
}
void dispose() {
_valueController.close();
}
}

class ProviderPage extends StatelessWidget {
@override
Widget build (BuildContext context) {
return Provider<CounterBloc>(
builder: (_) => CounterBloc(),
dispose: (_, bloc) => bloc.dispose(),
child: Scaffold(
body: CounterText(),
floatingActionButton: _floatingButton(),
)
)
}


Widget _floatingButton() {
return Consumer<CounterBloc>(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: value.increment,
child: const Icon(Icons.add),
);
}
)
}
}


class CounterText extends StatelessWidget {
@override
Widget build (BuildContext context) {
final bloc = Provider.of<CounterBloc>(context);
return StreamBuilder<String>(
stream: bloc.stream,
builder: (context, snapshot) {
return Center(
child: Text(snapshot.data ?? '0')
)
}
)
}
}
以上可以用Provder.value()实现,引入简单值传播
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class ProviderValuePage extends StatefulWidget {
@override
ProviderValueState createState() => ProviderValueState();
}

class ProviderValueState extends State<ProviderValuePage> {
final _bloc = CounterBloc();

@override
Widget build(BuildContext context) {
return Scaffold(
body: Provider<CounterBloc>.value(
value: _bloc,
child: CounterText(),
),
floatingActionButton: FloatingActionButton(
onPressed: bloc.increment,
child: const Icon(Icons.add),
),
);
}

@override
void dispose() {
_bloc.dispose();
super.dispose();
}
}

class CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = Provider.of<CounterBloc>(context);

return StreamBuilder<String>(
stream: bloc.stream,
builder: (context, snapshot) {
return Center(
child: Text(snapshot.data ?? '0')
);
},
);
}
}

ChangeNotifierProvider与ChangeNotifierProvider.value()等提供商的唯一区别在于ChangeNotifierProvider.value()创建和销毁模型实例需要自行处理

多提供商的场景

1
2
3
4
5
6
7
8
9
10
Provider<Foo>.value(
value: foo,
child: Provider<Bar>.value(
value: bar,
child: Provider<Baz>.value(
value: baz,
child: someWidget
)
)
)

一层一层的嵌套导致代码可读性降低,所以产生了MultiProvider

1
2
3
4
5
6
7
8
MultiProvider(
providers: [
Provider<Foo>.value(value: foo),
Provider<Bar>.value(value: bar),
Provider<Baz>.value(value: baz),
],
child: someWidget
)
注意的是需要指定不同的类型,如果是相同的类型,将保留最后一个Provider的值

创建一个可以携带参数的flutter命名路由

MaterialApp提供了一个属性onGenerateRoute,需要一个返回Route<dynamic>,并接受RouteSettings参数的函数

首先创建一个Router类,并且创建一个静态函数,settings包括路由的名称和参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Router {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch(settings.name) {
case '/': return MaterialPageRoute(
builder: (_) => Home()
),
case '/feed': return MaterialPageRoute(
builder: (_) => Feed()
),
default: return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('没有路由')
)
)
)
}
}
}

为了避免我们的代码出现错误,我们将采用硬编码的方式定义路由名称,并放到全局可以访问的constant.dart文件中

1
2
const String homeRoute = '/';
const String feedRoute = '/feed';

switch case语句之后更改为
1
2
3
4
5
6
case homeRoute: return MaterialPageRoute(
builder: (_) => Home()
),
case feedRoute: return MaterialPageRoute(
builder: (_) => Feed()
),

之后,在定义MaterialApp应用程序时,generateRoute函数传递给onGenerateRoute,通过initialRoute配置起始视图
1
2
3
4
5
6
7
8
9
class MyApp extends StatelessWidget {
@override
Widget build (BuildContext context) {
return MaterialApp (
onGenerateRoute: Router.generateRoute,
initialRoute: homeRoute
)
}
}

这时,当你需要导航时,只需要使用
1
Navigator.pushNamed(context, feedRoute);

如果需要传递参数
1
2
3
4
5
6
Navigator.pushNamed(context, feedRoute, arguments: '传递的参数');
case feedRoute:
var data = settins.arguments as String;
return MaterialPageRoute(
builder: (_) => Feed(data)
),

什么是Streams?

streams就好比传送带,将物品放到一侧,他将自动运送到另一侧。我们可以将数据对象放在传送带上,他会被传送带传输。如果传送带不是无限长的(它不是一个无限的流, 例如rxjs里的interval,如果不取消订阅,他会随着时间一直流动),那么传送带的物品终会掉落。

avatar

为了避免传送带上的物品无辜掉落,我们可以做一些事情,使得物品在掉落之前实现某些价值。

avatar

Dart Streams 和 Rx

  • Rx里面的可观察对象命名未Observable,它与Dart Streams里的Stream是同等的意义,所以在可以使用Stream的任何地方赋予Observable的含义

  • listen / subscribe, 进行序列订阅,同等意义

  • listen()返回一个StreamSubscription对象,调用cancel()释放订阅

  • StreamController / Subject, 发布值,相当于在传送带的左侧添加物品,同等意义

使用StreamBuilder

不用使用initState()和setState(),flutter提供了一个方便的widget称之为StreamBuilder,它需要一个Stream和一个builder函数,只要Stream发出一个新值就会调用他,不再需要initState或dispose.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
body: Center(
child: Column(
mainAxisAligment: MainAxisAlignment.center,
children: <Widget>[
Text('demo'),
StreamBuilder<int>(
initialData: 0,
stream: _stream,
builder: (context, snappShot) {
String valueString = 'NoData';
if (snappShot != null && snappShot.hasData) {
valueString = snappShot.data.toString();
}
return Text(
valueString,
style: Theme.of(context).textTheme.display1
)
}
)
]
)
)

相对于直接订阅,使用StreamBuilder有几个明显的区别:

  • setState()在listen()时接受新值时会重建整页,而StreamBuilder只会重建他自己的widget
  • snappShot包含从Stream接收的最新数据
  • 含有一个initialData,用于第一次构建,即屏幕的第一帧,解决StreamBuilder不能在第一帧期间接收值的问题,如果snappShot无效,则返回默认Widget,适用于某些业务场景

flutter中使用BLOC模式

什么是bloc模式?

bloc[Business Logic Component]翻译过来就是业务逻辑组件,把业务逻辑抽出来,数据和ui解耦,一处改动,多处更新

avatar

上面描述的是组件的一些基本行为,【展示数据】,【发送事件】,flutter中实现Bloc的精髓就是stream,严格遵守了单一职责原则,代码解耦更好。

UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
...
}
class CounterPage extends StateLessWidget {
@override
Widget build(BuildContext context) {
final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
return Scaffod(
appBar: AppBar(
title: Text('block')
),
body: Center(
child: StreamBuilder<int>(
stream: bloc._counterStream,
initialData: 0,
builder: (BuildContext context, AsyncSnapShot<int> snapshot) {
return Text('clicked ${snapshot.data} times')
}
)
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
bloc._actionSink.add(null); // 发送事件
}
)
)
}
}

Bloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class IncrementBloc {
int _counter;
StreamController<int> _counterController = StreamController<int>();
StreamSink<int> _counterSink => _counterController.sink; // 发送数据
Stream<int> _counterStream => _counterController.stream; // stream

StreamController _actionController = StreamController();
StreamSink _actionSink => _actionController.sink;
Stream _actionStream => _actionController.stream;

void IncrementBloc() {
_counter = 0;
_actionStream.listen((data){
_counter = _counter + 1;
_counterSink.add(_counter);
});
}
}

Flutter框架结构

avatar

  • 底下两层(Foundation和Animation、Painting、Gestures被合并为一个dart UI层,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。

  • Rendering层,这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)。

  • Widgets层是Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道。

primarySwatch 和 primaryColor 的区别

1
2
3
4
5
6
7
8
Widget build (BuildContext context) {
return MaterialApp(
title: 'myApp',
theme: ThemeData(
primaryColor: Colors.white // primarySwatch
)
)
}

使用primaryColor可以Colors.white,使用primarySwatch不可以设置白色和黑色,primarySwatch中的颜色是调用MaterialColor这种颜色类

listView

1
2
3
4
5
6
7
8
9
10
11
12
Widget _buildSuggestions() {
return ListView.builder(
padding: EdgeInsets.all(16.0),
itemCount: 100,
itemBuilder: (BuildContext context, i) { //itemBuilder callback
if (i.isOdd) {
return Something();
}
return Another()
}
)
}

异步编程

与js一样,dart支持单线程执行,js中Promise对象表示异步操作的最终结果,dart中用Future对象来处理

1
2
3
4
5
6
_getIpAddress() {
final url = 'http://www.google.com';
HttpRequest.reqeust(url).then((value) => {
print(json.decode(value.responseText)['origin']);
}).catchError((error) => print(error))
}

async函数返回Future,await等待Future

1
2
3
4
5
6
_getIpAddress() {
final url = 'http://www.google.com';
var request = await HttpRequest.reqeust(url);
String ip = json.decode(request.responseText)['origin'];
print(ip);
}

布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Contianer(
child: Center(
child: Container(
child: Text('demo'),
decoration: BoxDecoration( // color, borderRadius, boxShadow, shape 通过BoxDecoration装饰
color: Colors.red,
borderRadius: BorderRadius.all(
Radius.circular(8.0)
),
boxshadow: <BoxShadow>[
BoxShadow(
color: Color(0xcc000000),
offset: Offset(0.0, 2.0)
),
BoxShadow(
color: Color(0000000000),
offset: Offset(0.0, 2.0)
)
],
shape: BoxShape.circle
),
padding: EdgesInsets.all(16.0),
width: 240.0
)
)
)
  • Positioned widget and Stack widget
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Container(
child: Stack(
children: [
Positioned(
child: Container(
child: Text('demo'),
decoration: BoxDecoration(
color: Colors.red
)
),
left: 24.0,
top: 24.0
)
]
),
width: 320.0,
height: 320.0
)
  • Transform widget and Scaling widget
1
2
3
4
5
6
7
8
9
10
11
12
Container(
child: Transform(
child: Container(
child: Text('demo')
),
alignment: Alignment.center,
transform: Matrix4.identity()..rotateZ(15 * 3.1415926 / 180) // rotate
transform: Matrix4.identity()..scale(1.5) // scale
),
width: 320.0,
height: 320.0
)
  • ellipsis
1
2
3
4
5
6
7
8
9
10
11
Container(
child: Container(
child: Text(
child: Text('demo'),
overflow: TextOverflow.ellipsis,
maxLines: 1
),
),
width: 320.0,
height: 320.0
)

GestureDetector 处理手势

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyButton extends StatelessWidget {
@override
Widget build (BuildContext context) {
return GestureDetector( // IconButton等使用GestureDetector提供onPressed回调
onTap: () { // onLongPress...
print('tapped')
},
child: Container(
child: Text('demo')
)
)
}
}

final const var static in Dart

static: 表示一个成员属于类而不是对象,修饰成员

final: 必须初始化,且值不可变,编译时不能确定值,修饰变量 // final list = [1,2,3]; list[0] = 4; => [4,2,3]

const: 编译时可确定,并且不能修改 // var list = const [1,2,3]; list[0] = 4; error

typedef 类型定义,通过用来检查函数类型

1
2
3
4
5
typedef int Compart(int a, int b);
int sort(int a, int b) => a - b;
main() {
assert(sort is Compare); // True
}

前言

RXJS全名Reactive Extensions for JavaScript,是JavaScript的响应式扩展。什么是响应式?响应式就是跟随时间不断变化的数据、状态、事件等转换成可被观察的序列,然后订阅那些变化,一旦变化则会执行业务逻辑。适用于异步场景。ReactiveX结合了观察者模式、迭代器模式和函数式编程构建一个管理事件序列的理想方式。

RxJS所能解决的问题:

时刻保持响应。这对于一个应用来说意味着当他处理用户的输入或者凭借AJAX从服务器接受一些数据时停止是一件不可能接受的事情。在JavaScript中解决问题的方案始终是大量运用回调函数来进行一些应用的处理。但回调的使用使内容丰富的大型应用变得凌乱,一旦你需要多块数据时你就陷入了回调地狱。Angular2中,组件间通讯@Output对应的EventEmitter实际上就是一个Subject;Http模块中Observable作为大部分API的交互对象使用。但是这只是官方推荐的外部扩展并不必须,也可以使用Promise,之后会介绍Observable和Promise的区别。

RxJS初探:

首先尝试一个简单的小例子:
  • 存在一个数组,里面含有多种数据类型的元素
  • 找到其中的数字及字数组成的字符串
  • 每一个符合标准的元素乘以2
  • 累加

需要同时满足以上四个要求,可以通过循环列表来筛选满足要求的元素在进一步操作

1
2
3
4
5
6
7
8
const source = [1, 5, 9, 3, 'hi', 'tb', 456, '11', 'yoyoyo'];
let total = 0;
for (let i = 0; i < source.length; i++) {
const num = parseInt(source[i], 10);
if (!isNaN(num)) {
total += num * 2;
}
}

如果拥有函数式编程的经验,相信大家一定会通过es6的映射函数进行操作,接下来通过这个例子说明一下命令式编程和函数式编程的区别。

声明式编程发轫于人工智能的研究,主要包括函数式编程(functional programming,简称FP)和逻辑式编程(logic programming,简称LP);
如果想探究声明式编程与函数式编程的具体关系请访问:函数式与声明式的关系,在此不是本篇的重点

1
2
3
4
5
6
const source = [1, 5, 9, 3, 'hi', 'tb', 456, '11', 'yoyoyo'];
let total = source
.map(x => parseInt(x as any, 10))
.filter(x => !isNaN(x))
.map(x => x * 2)
.reduce((total, value) => total + value);

函数式编程中的函数这个术语不是指计算机中的函数,而是指数学中的函数,即自变量的映射。也就是说一个函数返回的值仅决定于函数参数的值,不依赖其他状态。命令式编程注重的是函数执行的细节,函数式编程注重的是函数执行的结果。
函数式编程对函数的使用有一些特殊要求:

  • 声明式函数
  • 纯函数
  • 数据不可变性

声明式编程是人脑思维方式的抽象,即利用数理逻辑或既定规范对已知条件进行推理或运算。声明式的函数,让开发者只需要表达”想要做什么”,而不需要表达“怎么去做”。
纯函数指的是执行结果由输入参数决定,参数相同时结果相同,不受其他数据影响,并且不会带来副作用的函数。副作用指的是函数做了和本身运算返回值没有关系的事情,如修改外部变量或传入的参数对象,甚至是执行console.log都算是副作用。前端中常见的副作用有发送http请求、操作DOM、调用alert或者confirm函数等。
数据不可变就是指这个数据一旦产生,它的值就永远不会变。JavaScript中字符串类型和数字类型就是不可改变的,而对象基本都是可变的,可能会带来各种副作用。

函数式编程带来的好处主要可以总结为以下两点:

  • 相比命令式编程,少了非常多的状态变量的声明与维护
  • 代码更为简洁,可读性更强

进入RxJS

流(Stream)无非是随时间流逝的一系列事件。流可以用来处理任何类型的事件,如:鼠标点击,键盘按下等等。你可以把流作为变量,它有能力从数据角度对发生的改变做出反应。Stream在其时间轴中发出三样东西,一个值,一个错误和完整的信号。我们必须捕获此异步事件并相应地执行函数。

想要抓取事件,一般可以用 callback 或是 Promise 来达成,promise和observable都是为解决异步问题而设计的(避免“回调地狱”), 然而 Promise 主要設设计一次性的事件与单一回傳=传值,而RxJS除了包含Promise外,提供了observable可观察对象,以惰性的方式推送多值的集合

Pull拉取 VS Push推送

拉和推是数据生产者和数据的消费者两种不同的交流协议;什么是”pull”?在”pull”体系中,数据的消费者决定何时从数据生产者那里获取数据,而生产者自身并不会意识到什么时候数据将会被发送给消费者。每一个JS函数都是一个“pull”体系.
什么是”push”?在push体系中,数据的生产者决定何时发送数据给消费者,消费者不会在接收数据之前意识到它将要接收这个数据。
Promise(承诺))是当今JS中最常见的Push体系,一个Promise(数据的生产者)发送一个resolved value(成功状态的值)来注册一个回调(数据消费者),但是不同于函数的地方的是:Promise决定着何时数据才被推送至这个回调函数。
RxJS引入了Observables(可观察对象),一个全新的”推体系”。一个可观察对象是一个产生多值的生产者,并”推送给”Observer(观察者)。

单值与多值

如果您通过Promise提出请求并等待回复。您可以确定对同一请求不会有多个响应。Observables允许您在调用observer.complete()函数之前解析多个值

总结RxJS VS Promise —— 三个最重要的区别

区别 Rxjs Promise
动作是否可以取消?
是否可以发射多个值?
各种工具函数?

开始了解RxJS中的几个重要成员

  • Observable(可观察对象):表示一个可调用的未来值或者时间序列上的事件集合
  • Observer(观察者):一个回调函数集合,它知道怎样去监听被Observable发送的值
  • Subscription(订阅): 表示一个可观察对象的执行,主要用于取消执行
  • Subject(主题):等同于一个事件驱动器,是将一个值或者事件广播到多个观察者的唯一途径
  • Operators(操作符): 纯函数,使得以函数编程的方式处理集合

Observable

Observable是一个具有一些特殊的特征的函数。它接收一个“观察者”(一个带有“next”,“error”和“complete”方法的对象)

  • Observable支持在应用程序中的发布者和订阅者之间传递消息。
  • Observable很懒惰。它不会开始生成数据,直到您订阅它为止。
  • subscribe()返回一个订阅,消费者可以在unsubscribe()取消订阅并销毁生产者。
  • RxJS提供了许多可用于创建Observable的函数。这些函数可以简化创建可观察对象的过程

Observer

什么是Observer?Observer是Observable传递过来的数据的消费者。Observers由一个带有“next”,“error”和“complete”方法的对象构成,next、error、和 complete用来传递数据。

1
2
3
4
5
6
var observer = {
next: x => console.log('Observable got a next value: ' + x),
error: err => console.log('Observable got and error: ' + err),
complete: () => console.log('Observable got a complete notification')
};
observable.subscribe(observer)

Subscription

一个Subscription代表了一个一次性的资源,通常表示的是一个Observable执行。一个Subscription有一个重要的方法,unsubscribe,它不需要参数,仅仅是取消订阅释放资源。

1
2
3
const observable = interval(1000);
const subscription = observable.subscribe(x => console.log(x));
subscription.unsubscribe();

Subscriptions也可以放在一起,这样会导致使用一个unsubscribe()将取消多个Observable执行,通过add、remove方法维护关联的Subscription

1
2
3
4
5
6
7
8
const observable1 = interval(400);
const observable2 = interval(300);
const subscription = observable1.subscribe(x => console.log('first: ' + x));
const childSubscription = observable2.subscribe(x => console.log('second: ' + x));
subscription.add(childSubscription);
setTimeout(() => {
subscription.unsubscribe();
}, 1000);

Subject

  • Subject是一种特殊类型的Observable,允许将值多播到许多观察者。虽然普通的Observable是单播的(每个订阅的Observer都拥有Observable的独立执行),但Subject是多播的

  • 每一个Subject都是一个observable可观察对象,给定一个Subject后,你可以订阅它,提供的观察者将会正常的开始接收值。从观察者的角度来看,它不能判断一个可观察对象的执行时来自于单播的Observable还是来自于一个Subject.
    在Subject的内部,subscribe并不调用一个新的发送值得执行。它仅仅在观察者注册表中注册给定的观察者,类似addEventListener的工作方式。

  • 每一个Subject都是一个Observer观察者对象。它是一个拥有next()/error()/complete()方法的对象。要想Subject提供一个新的值,只需调用next(),它将会被多播至用来监听Subject的观察者。

Subject就是一个可观察对象,只不过可以被多播至多个观察者。同时Subject也类似于EventEmitter:维护者着众多事件监听器的注册表。

BehaviorSubject

Subjects的一个变体是BehaviorSubject,其有”当前值”的概念。它储存着要发射给消费者的最新的值。无论何时一个新的观察者订阅它,都会立即接受到这个来自BehaviorSubject的”当前值”

BehaviorSubject对于表示”随时间的值”是很有用的。举个例子,人的生日的事件流是一个Subject,然而人的年龄的流是一个BehaviorSubject。

ReplaySubject

一个ReplaySubject类似于一个BehaviorSubject,因为它可以发送一个过去的值(old values)给一个新的订阅者,但是它也可以记录可观察对象的一部分执行。

一个ReplaySubject 从一个可观察对象的执行中记录多个值,并且可以重新发送给新的订阅者。

AsyncSubject

AsyncSubject是另一个变体,它只发送给观察者可观察对象执行的最新值,并且仅在执行结束时。AsyncSubject类似于last()操作符,因为它为了发送单一值而等待complete通知。

常用的操作符

每一个操作符都会产生一个新的Observable,不会对上游的Observable做任何修改,这完全符合函数式编程“数据不可变”的要求。pipe方法就是数据管道,会对数据流进行处理,可以添加操作符作为参数。

interval 创建一个无限长度的周期性序列

1
interval(1000)  // 输出: 0,1,2...

timer 指定一个额外的参数来调节第一值的静默时长,第二个参数可选,若无则仅仅在规定的静默时长后输出一个值,然后结束序列

1
timer(0,1000) // 输出:0,1,2...

from 可以将已有的数据转化为Observable,参数为iterable数据集对象
1
from([1,2,3,4])   // 输出:1,2,3,4

of 不在同一个数据集中的多个来源的数据
1
2
of([1,2,3])    //    [1,2,3]
from([1,2,3]) // 1,2,3

fromEvent 将事件流转化为Observable,
1
2
const el = document.getElementById("btn"); 
fromEvent(el,"click");

delay 推迟 参数为数字或Date对象

startWith 可以在源序列之前添加额外的元素

map 对源序列进行变换,并返回新的序列(改变了源)

1
2
const source = of(1,2,3); // 输出: 1 2 3
const target = source.map(x => x * 2); //输出: 2 4 6

concat有序拼接 , merge无序拼接

mergeMap 平坦化映射:首先将一个序列的各元素映射为序列,然后将各序列融合,参数是一个映射函数,返回值为序列

1
2
3
4
5
const source = fromEvent(document, 'click');
const target = source.pipe(
mapTo(1),
mergeMap(() => interval(1000).pipe(take(3)))
).subscribe(res => console.log(res)); //输出: 0 1 2 0 ...

switchMap 与mergeMap的区别在于将最新的序列中的元素输出

concatMap 将源序列各元素映射为序列,然后按顺序拼接 (与mergeMap的区别所在)

filter 筛选源序列中满足条件的元素,并返回新的序列

1
2
const source = of(1,2,3,4,5); //序列: 1 2 3 4 5
const target = source.filter(x => x < 4) //序列: 1 2 3

take 截取序列头部元素数量输出

distinct 去重,并返回一个新序列

1
2
const source = of(1,2,2,3,4,2,1); //序列: 1 2 2 3 4 2 1
const target = source.distinct(); //序列:1 2 3 4

distinctUntilChanged 相邻元素去重,并返回一个新序列
1
2
const source = of(1,2,2,3,4,2,1); //序列: 1 2 2 3 4 2 1
const target = source.distinctUntilChanged(); //序列:1 2 3 4 2 1

debounce 去抖动,一段时间内只取最新数据作为一次发射数据,其他数据取消发射

throttle (和debounce唯一区别是debounce取一段时间内最新的,而throttle忽略这段时间后,发现新值才发送, 通俗讲,都设定一个时间周期,持续触发事件,throttle为每到时间周期便会触发一次,bebounce为触发周期小于设定时间周期不予事件触发)

zip 支持可变数量的序列作为参数,最后一个参数应当是一个组合函数, 其返回值将作为目标序列的元素

1
2
3
4
5
const source1 = of(1, 2, 3);
const source2 = of(4, 5, 6);
const target = zip(source1, source2).subscribe(([val1, val2]) => { // 序列: 1-4 2-5 3-6
console.log(val1 + '-' + val2)
});

forkJoin 将多个序列的最后一个元素组合为一个数组后,作为目标序列的唯一元素,一个常见用例是在页面加载时你希望发起多个请求,并在所有请求都响应后再采取行动
1
2
3
4
5
const source1 = of(1, 2, 3);
const source2 = of(4, 5, 6);
const target = forkJoin(source1, source2).subscribe(([val1, val2]) => { // 序列: 3-6
console.log(val1 + '-' + val2)
});

combineLatest 将多个序列的最后一个元素,使用组合函数构成目标序列的一个新元素

const setHtml = id => val => document.getElementById(id).innerHTML = val;
const addOneClick$ = id => fromEvent(document.getElementById(id), 'click')
    .pipe(
        mapTo(1),
        startWith(0),
        scan((acc, curr) => acc + curr, 0),
        tap(setHtml(`${id}Total`))
    );
    const combineTotal$ = combineLatest(
        addOneClick$('red'),
        addOneClick$('black')
    ).pipe(
        map(([val1, val2]) => val1 + val2)
    )
    .subscribe(setHtml('total'));

map用于对自身对象数值进行映射,将发射对象转换成另一个发射对象发射, 返回一个包含映射结果的Observable对象 而mergeMap是把自身对象里的数值进行映射并转换成一个新的Observable对象.返回一个内部元素为映射的Observable对象的Observable对象

marble diagrams

为了解释operators是如何工作的,光是文本解释是不够的。许多operators和时间有关,它们可能会延迟执行,例如,throttle等。图标往往能够比文字更多表达清楚。Marble Diagrams能够可视化的表现出operators是如何工作的,包括输入的Observable(s),operator和它的参数,以及输出的Observable. Marble diagrams

使用Git管理项目总结

导语:

使用git已经一段时间了,总结一下我的理解关于使用git进行团队协作开发的流程。首先master分支作为主分支应该承担版本发布的责任。开发任务应该在develop分支上。不论当前分支是从哪个分支分离开来,都需要合并到那个分支上。根据需求的不同以及开发人员的指派,应该创建一系列feature-分支,一旦开发完毕后,进行代码的review,确认无误后合并分支到develop分支上,之后删除相应的feature-分支。在进行版本的发布前,需要从develop分支分离并创建对应的预发布分支,进行当前版本的测试,完成后,需要合并进develop分支和master分支,之后切换分支到master分支,生成版本节点标签并删除对应预发布分支,进行版本的发布。如果遇到bug问题,需要从master分支分离出修复bug分支,修复完成后合并进develop和master分支,之后切换分支到master分支,生成节点标签并删除对应bug分支。

一般的git命令已经用的很多了,今天记录几个很重要但是我自己在日常工作中很少使用的几个命令。

git stash

正在feature分支开发,master分支报了一个bug错误,此时需要即时修复bug,但是开发还没有完成,此时提交不太友好。Git提供的stash功能正好适用这个场景。可以把当前工作储存起来,处理完事情后继续工作。

1
2
$ git stash    // order code
Saved working directory and index state WIP on feature-**: 4966d2d bingo

此时git status查看工作区是干净的。此时可以创建临时分支处理bug。处理完bug后切换回feature-分支继续开发。

1
2
$ git stash list // order code
stash@{0}: WIP on feature-**: 4966d2d bingo

工作现场存在,现在需要恢复现场。两个办法:

  • git stash apply && git stash drop // 前者恢复现场不删除stash内容,后者为补充删除stash内容

  • git stash pop // 恢复现场同时删除stash内容

如果多次stash的情况下,可以git stash list查看,然后恢复指定的stash

1
git stash apply stash@{0}  // 括号内为指定stash内容

–no-ff

这个参数的意思是保留原分支记录,默认情况下,执行fast-forward merge,直接将master分支指向develop分支

1
git merge --no-ff develop // 把develop合并进master分支

fast-forward

--no-ff

git diff

比较两次修改的差异

工作区 VS 暂存区

1
2
3
$ git diff <filename>  

$ git diff <branch> <filename> // 和另一分支的区别

暂存区 VS Git仓库

1
2
3
$ git diff --cached <filename>  

$ git diff --cached <commit> <filename> // 和指定commit的区别

工作目录 VS Git仓库

1
$ git diff <commit> <filename>

Git仓库 VS Git仓库

1
$ git diff <commit> <commit>  // git仓库任意两次commit的差别

扩展:

以上命令可以不指定,则对应全部文件操作
以上命令涉及git仓库对比的,均可指定commit版本

  • HEAD 最近一次commit

  • HEAD^ 上次提交

  • HEAD~100 上100次提交

  • 每次提交产生的哈希值

git rebase

将某个分支上的所有提交记录移植到另一个分支上,清除不必要的提交记录

永远不要rebase一个已经分享的分支

一图以意之

0

以下是一个例子讲解rebase的作用

1

切换到issue3分支后,对master执行rebase,解决冲突

1
2
$ git checkout issue3
$ git rebase master

冲突解决后不需要commit命令进行提交,而是执行rebase命令的continue选项或着abort选项

1
2
$ git add .
$ git rebase --continue / --abort

2

master分支的issue3分支可以fast-forward了。切换到master分支执行合并

1
2
$ git checkout master
$ git merge issue3

rebase的内容与merge的效果是一样的,但是历史记录会简洁

3

i18n国际化,演示angular控件ng2-translate

app.module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { TranslateModule, TranslateLoader, TranslateStaticLoader } from 'ng2-translate';
//..
export function createTranslateStaticLoader(http: Http) {
return new TranslateStaticLoader(http, './assets/i18n', '.json'); // 发送请求,拿到i18n文件夹下的json后缀文件
}
@NgModule({
imports: [
TranslateModule.forRoot({
provide: TranslateLoader,
useFactory: createTranslateStaticLoader,
deps: [Http]
})
]
})

app.component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { TranslateService } from 'ng2-translate';
@Component({
selector: 'app-root'
})
export class AppComponent {
constructor(
private translate: TranslateService
) {}
ngOnInit() {
this.translate.addLangs(['zh', 'en']);
this.translate.setDefaultLang('zh');
const browserLang = this.translate.getBrowserLang();
this.translate.use(browserLang.match(/zh|en/) ? browserLang : 'zh' );
}
}

模版使用

1
<p>{{ word | trnaslate }}<p>

利用Angular本身ChangeDetectionStrategyAPI改变检查策略脱离变化检测器,即减少不必要的检测来提高应用的性能

zone是什么

对浏览器的异步api做了封装,并对外发出通知何时开始何时结束,angular在得到异步事件结束的通知后,执行变化检查。

zone性能优化的重点在哪

精确的控制哪些异步事件是应该在angular的zone以内运行的,哪些是应该在angular的zone之外运行的。显然在angular的zone之外运行的事件是不会进行变化检测的,减少不必要的变化检测则实现了性能上的优化

1
2
3
4
5
6
7
import { NgZone } from '@angular/core';
constructor(private zone: NgZone) {}
mouseDown(event) {
this.zone.runOutsideAngular(() => {
window.document.addEventListener('mouseover', this.mouseMove.bind(this)); // 此处应该尽量避免直接操纵dom,应该利用底层封装api(renderer, renderer2)
})
}

NgZone可以让代码继续回到zone里运行,会再次触发anuglar的变化检测,调用NgZone.run();

1
2
3
4
5
6
mouseUp(event) {
this.zone.run(() => {
.....
})
window.docuemnt.removeEventListener('mousemove', this.mouseMove); // 移除mousemove绑定的回调
}

补充

zone.js为javascript提供执行上下文,可以在异步任务之间进行持久性传递。采用了猴子补丁将javascript中的异步任务包裹了一层。使得异步任务运行在zone的上下文中。每一个异步任务都被当作一个task,并在task基础上提供钩子函数。

  • onZoneCreated: 产生一个新的zone对象时的钩子函数,zone.fork也会产生一个继承基类zone的新zone,形成一个独立的zone上下文

  • beforeTask: zone Task 执行前的钩子函数

  • afterTask

  • onError: zone运行Task时候的异常钩子函数

并且对大多数异步事件进行了包裹封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const log = (phase) => {
return () => {
console.log('i am in zone.js' + phase + '!');
}
}
zone.fork({
onZoneCreated: log('onZoneCreated'),
beforeTask: log('beforeTask'),
afterTask: log('afterTask')
}).run(() => {
const methodLog = (func) => {
return () => {
console.log('i am from' + func + 'function')
}
},
foo = methodLog('foo'),
bar = methodLog('bar'),
baz = () => {
setTimeout( methodLog('baz in setTimeout'), 0)
};
foo();
baz();
bar();
})
1
2
3
4
5
6
7
8
9
10
输出结果:
i am in zone.js beforeTask;
i am from foo function;
i am from bar function;
i am in zone.js afterTask;

i am in zone.js onZoneCreated;
i am in zone.js beforeTask;
i am from baz in setTimeout function;
i am in zone.js afterTask;

上述例子中将run方法分为了两个task,分别为同步task和异步task。fork方法会产生一个继承根zone的子类,并在fork函数中配置特定的钩子函数,形成独立的zone上下文,而run方法则是启动执行业务代码的对外接口。

使用Observable优化脏检查

使用OnPush的检查策略,如果修改了对象内部的值,此时不会进行脏检查,不会进行视图更新。此时可以选择Observable对象,通过手动调用markForCheck()方法进行优化,当前组件到根组件的路径上的所有组件都会进行变化检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component({
...,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent implements OnInit{
@Input() todos: Observable<Todo[]>;
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.todos.subscribe(todos => {
业务代码;
this.cd.markForCheck();
})
}
}

记录每天学习中发现的知识点

总结TemplateRef与ViewContainerRef

TemplateRef: 用于表示内嵌的template模板元素,可以创建内嵌视图(createEmbeddedView),可以访问到封装后的nativeElement,模板经过渲染后会替换成comment元素

ViewContainerRef: 用于表示一个视图容器,可添加一或多个视图,通过ViewContainerRef实例,可以基于TemplateRef创建内嵌视图,并指定插入位置,主要创建管理内嵌视图

1
2
3
4
5
6
7
8
<template #tpl>
...
</template>
@ViewChild('tpl') tplRef: TemplateRef<any>;
@ViewChild('tpl', { read: ViewContainerRef }) tplVcRef: ViewContainerRef;
ngAfterViewInit(){
this.tplVcRef.createEmbeddedView(this.tplRef);
}

注意:@ViewChild属性装饰器若未设置read属性,默认返回ElementRef对象实例

指令的一般应用场景

分类:内置指令, 自定义指令

主要考虑自定义指令的属性指令与结构指令

自定义属性指令实现简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Directive({
selector: '[directivename]' // 这里定义的selector为什么使用中括号,是为了结合input装饰器获取属性值
})
export class SomeDirective {
private _default = '[someValue]' // 旨在指令类内部定义默认值
@Input(directivename) directiveAnotherName; // 输入属性
constructor(private el: ElementRef, private renderer: Renderer){ // 引用类实例化,其中renderer对象提供许多api供渲染元素

}
@HostListener(eventName) eventName(){ // 监听宿主元素事件

}

<h1 [changeColor]="'red'"></h1>
}

自定义结构指令是实现简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Directive({
selector: '[directivename]' // 这里定义的selector为什么使用中括号,是为了结合input装饰器获取属性值
})
export class SomeDirective {
@Input(directivename)
set condition(newCondition: boolean) { // 这里用了getter、setter存取器, 进行属性值的动态监听
if(newCondition){
this.tplVc.createEmbeddedView(this.tpl); // 创建内嵌视图,可以设置第二个参数{$implicit: somevalue}, 则angular提供了let模板语法,允许在生成的上下文是定义和传递
}else{
this.tplVc.clear(); // 清除内嵌视图
}
}
constructor(private tpf: TemplateRef, private tplVc: VireContainerRef){ // 引用类实例化,用于创建内嵌视图

}

<h1 *structureDirective = true></h1> // 这里用了angular结构性指令的语法糖,原理同*ngIf
}

总结以上属性指令与结构指令

1 ElementRef与Renderer等的作用:支持跨平台,从底层封住,统一了api接口; 2 TemplateRef与ViewContainerRef的作用: 前面有总结过他俩的作用;
*3 angular2中指令与组件的关系:组件继承与指令,并扩展了与视图的关系

自定义debounceClick指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Directive({
selector: '[directiveDebounceClick]'
})
export class SomeDirective {
@Input('debounceTime') debounceTime: string;
@Output('debounceClick') debounceClick = new EventEmitter();
private clicks = new Subject<any>(); // 定义subject处理点击事件
constructor(){ }
@HostListener('click',['$event']) // 监听宿主元素上的点击事件,第二个参数用于将事件传递给eventClick方法
eventClick(event){
event.preventDefault();
event.stopPropagation(); // 阻止事件默认行为与事件冒泡
this.clicks.next(event); // 发送新值
}

ngOnInit(){
this.clicks.debounceTime(this.debounceTime) // 去抖动,时间自定义
.subscribe(x => this.debounceClick.emit(x)); // 调用emit方法发出事件
}

<button directiveDebounceClick [debounceTime] ='300' (debounceClick)="log($event)"></button>
}
ng-content包装器

如果你尝试在 Angular 中编写可重复使用的组件,则可能会接触到内容投射的概念:

1
2
3
4
5
6
7
8
9
10
11
import { Component } from '@angular/core';

@Component({
selector: 'wrapper',
template: `
<div class="box">
<ng-content></ng-content>
</div>
`
})
class Wrapper {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core';

@Component({
selector: 'wrapper',
template: `
<div class="box red">
<ng-content></ng-content>
</div>
<div class="box blue">
<ng-content select="counter"></ng-content>
</div>
`,
styles: [`
.red {background: red;}
.blue {background: blue;}
`]
})
export class Wrapper { }

将包装器的不同子项投影到模板的不同部分。 支持一个 select 属性,可以让你在特定的地方投射具体的内容。该属性支持 CSS 选择器(my-element,.my-class,[my-attribute],…)来匹配你想要的内容。如果 ng-content 上没有设置 select 属性,它将接收全部内容,或接收不匹配任何其他 ng-content 元素的内容。

1
2
3
4
<wrapper>
<span>This is not a counter</span>
<counter></counter>
</wrapper>

counter组件被正确投影到第二个蓝色框中,而 span 元素最终会在全部红色框中。

ngProjectAs

内部组件会被隐藏在另一个更大的组件中。你只需要将其包装在额外的容器中即可应用 ngIf 或 ngSwitch。无论什么原因,通常情况下,你的内部组件不是包装器的直接子节点。所以需要使用ngProjectAs属性,将它用于指定的元素上。

表单自定义验证规则

举例:邮件匹配

定义用户类型接口

1
2
3
4
5
6
7
export interface User {
name: string;
account: {
email: string;
confirm: string;
}
}

邮件匹配规则函数

1
2
3
4
5
6
7
8
export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
const email = control.get('email');
const confirm = control.get('confirm');
if (!email || !confirm){
return;
}
return email.value === confirm.value ? null : { nomatch: true };
}

表单模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Component, OnInit} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { emailMatcher } from './email-matcher';
@Component({
selector: 'signup=form',
template: `
<form novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
<label>
name...
</label>
<div *ngIf="user.get('name').touched && user.get('name').hasError('required')">
name is required
</div>
<div formGroupName = "account">
<label>
<span>Email address</span>
<input type="email" placeholder="Your email address" formControlName="email">
</label>
<label>
<span>Confirm address</span>
<input type="email" placeholder="Confirm your email address" formControlName="confirm">
</label>
<div *ngIf="user.get('account').touched && user.get('name').hasError('nomatch')">
name is required
</div>
</div>
<button type="submit" [disabled]="user.invalid">Sign up</button>
</form>
`
})
export class SignUpFormCOmponent implements OnInit {
user: FormGroup;
constructor(private fb: FormBuilder){}
ngOnInit() {
this.user = this.fb.group({
name: ['', Validators.required],
account: this.fb.group({
email: ['', Validators.required],
confirm: ['', Validators.rquired],
}, {validator: emailMatcher})
})
}
}

对比创建表单自定义验证指令

模版

1
2
<input name="password" formControlName="password" validateEqual="password">
<input name="confirmpassword" formControlName="confirmpassword" validateEqual="password">

validateEqual指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Directive, forwardRef, Attribute } from '@angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
@DIrective({
selector: '[validateEqual][formControlName], [validateEqual][formControl],[validateEqual][ngModel]' ,
providers: [{
provide: NG_VALIDATORS,
useExisting: forward(() => EqualValidator),
multi: true
}]
扩展
})
export class EqualValidator implements Validator {
constructor(@Attribute('validateEqual') public validateEqual: string){}
validate(control: AbstractControl): { [key:string]: boolean } {
let v = control.value; 使用指令的控件自身值: confirmpassword
let e = control.root.get(this.validateEqual); 指令指定的控件: passwordControl;
if (e && v !== e.value) {
return { validateEqual: false}
}
return null;
}
}

验证器添加到指定控件,造成只有该控件数值变化才会进行验证,先输入confirmPassword造成验证器失效,需要给验证器添加reverse属性

Forward Reference

不论es6、es7、还是ts作为开发语言,现阶段最终都会编译为es5的代码。es5中只有function并没有class。造成js代码编译阶段只有变量声明和函数声明会自动提升,而函数表达式并不会自动提升。所以要解决此问题可以使用Angular2提供的forward reference特性进行解决(Buffer顺序在Socket之前声明也可以,但是angular项目中模块组件化开发确认注入依赖的顺序性会带来更大负担)。

es6+中的class不进行自动提升主要为了解决继承父类时,父类不可用的问题
1
2
3
4
5
6
7
8
import { forwardRef, Injectable } from '@angular2/core'
@Injectable()
class Socket {
constructor(@Inject(forwardRef(() => Buffer)) private buffer) {}
}
class Buffer {
constructor(@Inject(BUFFER_SIZE) private size: Number) {}
}

OpaqueToken

OpaqueToken允许创建基于字符串的Token类,在Provider中使用。只需导入Opaque类。

1
2
3
4
5
6
7
import { OpaqueToken } from '@angular/core';
const CONFIG_TOKEN = new OpaqueToken('config');
export const THIRDPARTYLIBPROVIDERS = [
{
provide: CONFIG_TOKEN, useClass: ThirdPartyConfig
}
]

OpaqueToken类的定义

1
2
3
4
5
6
export class OpaqueToken {
constructor(protected _desc: string) {}
toString() : string {
return `Token${this._desc}`
}
}

OpaqueToken类的使用

1
2
3
4
5
6
7
8
9
import { ReflectiveInjector, OpaquToken } from '@angular/core';
const t = new OpaquToken('value');
const injector = ReflectiveInjector.resolveAndCreate([
{
provide: t,
useValue: 'bindingValue'
}
]);
injector.get(t) // 'bindingValue'

InjectionToken (Angular4+)

使用ValueProvider

1
2
3
4
5
6
7
8
9
10
11
@NgModule({
...,
providers: [
{
provide: 'apiUrl',
userValue: 'http://localhost:4200/heroes'
}
],
bootstrap: [AppComponent]
})
export class AppModule {}

注入ProviderValue

1
2
3
4
5
6
7
8
9
@Injectable()
export class HeroService {
constructor(
private loggerService: LoggerService,
@Inject('apiUrl') private apiUrl: String
){
console.log(apiUrl) // http://localhost:4200/heroes
}
}

如果引入了第三方库且名称相同就产生了问题

1
2
3
4
5
6
export const THIRD_PARTY_PROVIDERS = [
{
provide: 'apiUrl', // 与localhost:4200相同
userValue: 'http://192.168.18.59:4200'
}
]

更新Provider配置信息则如下

1
2
3
4
5
6
7
8
9
10
11
12
13
import { THIRD_PARTY_PROVIDERS } from './third-party';
@NgModule({
...,
providers: [
{
provide: 'apiUrl',
useValue: 'http://localhost:4200/heros'
},
THIRD_PARTY_PROVIDERS
],
bootstrap: [AppComponent]
})
export class AppModule { }

localhost:4200被覆盖

使用字符串作为Token起冲突了。所以利用InjectionToken(Angular4+)统一管理Token信息。

1
2
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('apiUrl');

OpaqueToken 与 InjectionToken 异同点

相同点

  • 创建可在Provider中使用的Token

不同点

  • 前者ng2的类,后者ng4+引入的类,继承自OpaqueToken,且引入了泛型用于定义所关联的依赖对象的类型

Es6 Set Map

Es6提供了新的数据结构Set。类似于数组,但是成员都是唯一的,没有重复的值。接受一个数组或类数组对象作为参数

1
2
const set = new Set([1,2,3,4,2]);
[...set] // [1,2,3,4]
1
2
3
const set = new Set();
[1,2,2,2,3,4].map(x => set.add(x));
set // [1,2,3,4]

Set内部,两个对象不相等,两个NaN相等,用length检测,可以用for…of遍历

  • add(value)

  • delete(value)

  • has(value)

  • clear()

Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键, 初始化Map需要一个二维数组,或者直接初始化一个空Map

1
2
var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95
1
2
3
4
5
6
7
var m = new Map();
var o = {p: "Hello World"};
m.set(o, "content")
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
只有对同一个对象的引用,Map结构才视为同一个键
1
2
3
4
var map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面代码的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined。

ModuleWithProviders

创建一个共享模块,包含部分功能性模块、管道、指令、和服务。对于服务,通常作为单例的服务可能被多次提供,可以通过在共享模块内部返回ModuleWithProviders对象的静态方法forRoot解决这类问题, (相对于将service注入在NgModule,通过forRoot方法返回具有NgModule属性的ModuleWithProviders对象,可以解决service多次提供的情况)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NgModule, ModuleWithProviders } from '@angular/core';
import { someDirective, somePipe, someService } from './functions';
@NgModule({
declarations: [
somePipe,
someDirective
],
exports: [
somePipe,
someDirective
]
})
export class SharedModule {
static forRoot() : ModuleWithProviders {
return {
ngModule: SharedModule,
providers: [ someService ]
}
}
}

NgModule中并不提供服务,在模块类中定义forRoot静态方法,返回ModuleWithProviders接口对象,在应用模块中导入共享模块并调用静态方法forRoot来提供服务和其他指令管道等,这样根模块会把他得providers添加到根模块的服务提供商中,确切的说是angular会先累加所有的显式注入的提供商,然后进一步追加其他模块的提供商到@NgModule.providers中,可以确保显式添加的提供商优先级大于从其他模块导入的;

1
2
3
4
5
6
7
8
9
import { SharedModule } from './shared'
//:...
@NgModule({
imports: [
SharedModule.forRoot()
],
//:...
})
export class AppModule {}

不调用forRoot方法则会只访问共享的管道和指令,不在提供服务

1
2
3
4
5
6
7
8
9
import { SharedModule } from './shared'
//:...
@NgModule({
imports: [
SharedModule、
],
//:...
})
export class AppModule {}
Angular2中没有模块级别的service,所有在NgModule中声明的Provider都是注册在跟级别的DI中

shadow DOM 选择器

使用emulated进行样式隔离时,可以访问适用于shadow DOM的css选择器

宿主

1
2
3
4
5
6
:host {
color: red; // <song-track>
}
:host(.selected) {
color: red; // <song-track class="selected">
}

样式依赖于祖先元素

它会在组件的宿主元素的祖先元素中查找汽配的祖先元素直到文档的根

1
2
3
4
5
6
:host-content(.selected) {
color: red;
}
:host-content(#selected) {
color: red
}

宿主元素或后代元素(跨边界)

它会覆盖任何封装的宿主元素或者其子元素

1
2
3
4
5
6
:host /deep/ .selected{
color: red;
}
:host >>> .selected{
color: red;
}
angular-cli 启动的项目使用deep而不是>>>

ControlValueAccessor

它是一个接口用于连接表单模型和视图,自定义表单必须实现这个接口,来实现模型与视图的映射关系

Angular引入它的原因在于不同的输入控件更新数据方式不同,input或checkbox,但是可以通过ControlValueAccessor统一

  • DefaultValueAccessor - text/textarea类型

  • SelectControlValueAccessor - selec类型

  • CheckboxControlValueAccessor - checkbox类型

实现ControlValueAccessor接口

1
2
3
4
5
6
export interface ControlValueAccessor {
writeValue(obj: any): void; // 将模型中的新值写入视图
registerOnChange(fn: any): void; // 当控件接收到change事件后,调用的函数,通知外部组件发生变化
registerOnTouched(fn: any): void; // 接收到touched事件后调用的函数
setDisabledState?(isDisabled: boolean): void; // 当控件状态变成DISABLED或ENABLE时,调用该函数启用或禁用dom
}
1
2
3
4
5
6
7
8
9
@Component(...)
class CounterComponent implements ControlValueAccessor {
...
propagateChange = (_: any) => {};
registerOnChange(fn: any) {
this.propagateChange = fn; // view层的值发生改变,通知外部
}
registerOnTouched...
}

注册成为表单控件

  • NG_VALUE_ACCESSOR: token类型为ControlValueAccessor,将控件本身注册到DI框架,使其可以被表单访问

  • NG_VALIDTORS: 将控件注册成为一个可以让表单得到其验证状态的控件,token为function或Validator,配合useExisting可以让控件只暴露出对应的function或Validator的validate方法

  • forwardRef: 向前引用,允许我们引用一个尚未定义的对象

  • multi: 设置为true,该token对应多个依赖项,使用相同的token获取依赖项的时候,获取的是已注册的依赖对象列表。如果不是true,那么对于相同的token的提供商来说,后定义的提供商会覆盖前面定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component({
selector: 'exe-demo',
...
provides: [
// 创建Token为NG_VALUE_ACCESSOR的提供商
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SomeComponent),
multi: true
},
// 创建Token为NG_VALIDATORS的表单验证验证器
{
provide: NG_VALIDATORS,
useValue: validateCounterRange,
multi: true
}
]
})

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { Component, forwardRef , Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS,
FormControl, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms';
export const EXE_COUNTER_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CounterComponent),
multi: true
}
export const validateCounterRange: ValidatorFn = (control: AbstractControl) : ValidationErrors => {
return ( control.value > 10 || control.value < 0 ) ?
{
'rangeError': { current: control.value, max: 10, min: 0}
} : null
}
export const EXE_COUNTER_VALIDATOR = {
provide: NG_VALIDATORS,
useValue: validateCounterRange,
multi: true
}
@Component({
...
provides: [
EXE_COUNTER_VALUE_ACCESSOR,
EXE_COUNTER_VALIDATOR
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements ControlValueAccessor {
@Input() _count: number = 0;
get count() {
return this._count;
}
set count(value: number) {
this._count = value;
this.propagateChange(this._count);
}
propagateChange = (_: any) => {};
writeValue(value: any) {
if (value) {
this.count = value;
}
}
registerOnchange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(fn: any) {

}
increment() {
this.count++;
}
decrement() {
this.count--;
}
}

在Angular中通过Provider来描述与Token相关联的依赖对象的创建方式,分为以下四种

  • useClass

  • useValue

  • useExisting

  • useFactory

relation

useClass
1
2
3
providers: [ 
{ provide: ApiService, useClass: ApiService } // 简介写法直接 ApiService
]
useValue
1
2
3
providers: [ 
{ provide: API_URL, useValue: 'http://my.api.com/v1' }
]
useExisting
1
2
3
providers: [ 
{ provide: 'ApiServiceAlias', useValue: ApiService }
]
useFactory
1
2
3
4
5
6
7
export function configFactory(config: AppConfig) {
return () => config.load();
}
providers: [
{ provide: APP_INITIALIZER, useFactory: configFactory,
deps: [AppConfig, multi: true] }
]

日常遗忘知识点总结

利用伪类实现自定义title

1
<span mytitle="hello world">Today is Friday</span>
1
2
3
4
5
6
7
span[mytitle]:hover::after{
content: attr(mytitle);
position: absolute;
right:0
color: #ff0;
...
}

伪类清除浮动

1
2
3
4
<div class="parent">
<div class="child1" style="float:left;width:25%;"></div>
<div class="child2" style="float:right;width:25%;"></div>
</div>
1
2
3
4
5
6
.parent::after{
content: '\0020';
height: 0;
display: block;
clear: both;
}

Flex布局

引自 大漠老师的文章,进行flex布局的学习与记录总结 原文地址

显式声明flex容器后,启动了一个flexbox格式化上下文。

1
2
3
4
5
<ul>
<li></li>
<li></li>
<li></li>
</ul>
1
2
3
4
5
6
7
8
9
ul{
display: flex; //或者inline-flex;
li {
width: 100px;
height: 100px;
margin: 8px;
background-color: #8cacea;
}
}

flex

display显示设置了flex属性后,自身变为flex容器,子元素变为了flex项目。

容器属性: flex-direction || flex-wrap || flex-flow || justify-content || align-items || align-content

flex-direction

1
2
3
ul{
flex-direction: row || column || row-reverse || column-reverse; // default: row
}

row: 从左向右水平

flex-wrap

1
2
3
ul{
flex-wrap: wrap || norwrap || wrap-reverse; // default: nowrap(不换行)
}

如果拥有大量子元素,会自适应每个元素的大小,全部注入到一行内,即使宽度大于视窗宽度出现滚动条(nowrap)

nowrap

显示默认宽度都行排列,不会强迫一行有多少个flex项目(wrap)

wrap

flex-flow

是flex-direction和flex-wrap属性的速记属性

1
2
3
4
ul{
flex-flow: row wrap;
相当于 // flex-direction: row; flex-wrap: wrap;
}

justify-content

接受属性值: flex-start || flex-end || center || space-between || space-around

flex-start: 左对齐 (default)

flex-end: 右对齐

center: 居中对齐

space-between: 两端对齐(除了第一个和最后一个部分,间距相等)

space-between

space-around: 让每个flex元素都具有相同的空间

space-between

align-items (不同于justify-content,它处理的是容器的纵向排列)

接受属性值: flex-start || flex-end || center || stretch || baseline

stretch: 所有flex元素高度和容器高度一样 (default)

stretch

flex-start: 顶部对齐

flex-end: 底部对齐

center: 居中对齐

baseline: 沿着自身的基线对齐

baseline

align-content

添加大量子元素,让其多行排列,该属性用于控制多行排列的flex容器的排列方式,效果类似align-items,expect baseline

接受属性值: flex-start || flex-end || center || stretch

stretch: 纵向适应可用空间 (default)

stretch

flex-start: 沿着顶部到底部排列

flex-start

flex-end: 沿着低部到顶部排列

center: 居中

未完待续。。。

12.18 继续flex布局要点学习

order

允许flex项目在flex容器内重新排序,默认值为0,可以接受负值

order=0

1
2
3
li:nth-child(1){ 
order: 1; /*设置一个比0更大的值*/
}

从低到高拍下

order=1

flex-grow | flex-shrink

控制flex项目在容器的空间上进行扩展; 接受0或者大于0的任何正数,默认为0。

1
2
3
4
5
6
7
8
{
<ul>
<li>I am a simple list</li>
</ul>

ul {
display: flex;
}

当把flex-grow设置为1的时候,填充多余空间

flex-grow

当有多个flex项目需要进行扩展比设置

1
2
3
4
5
#main div:nth-of-type(1) {flex-grow: 1;}
#main div:nth-of-type(2) {flex-grow: 3;}
#main div:nth-of-type(3) {flex-grow: 1;}
#main div:nth-of-type(4) {flex-grow: 1;}
#main div:nth-of-type(5) {flex-grow: 1;}

flex-grow

当把flex-shrink设置为大于1的时候,缩小flex项目

1
2
3
4
5
6
7
8
9
<ul class="flex">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>

flex {display:flex;width:400px;margin:0;padding:0;list-style:none;}
flex li{width:200px;}
flex li:nth-child(3){flex-shrink:3;}

flex-shrink默认值为1,接受正整数,压缩空间比,c显式的定义了flex-shrink,a,b没有显式定义,但将根据默认值1来计算,可以看到总共将剩余空间分成了5份,其中a占1份,b占1份,c占3分,即1:1:3
我们可以看到父容器定义为400px,子项被定义为200px,相加之后即为600px,超出父容器200px。那么这么超出的200px需要被a,b,c消化
通过收缩因子,所以加权综合可得200 1+200 1+200 3=1000px;
于是我们可以计算a,b,c将被移除的溢出量是多少:
a被移除溢出量:(200
1/1000)200,即约等于40px
b被移除溢出量:(200
1/1000)200,即约等于40px
c被移除溢出量:(200
3/1000)*200,即约等于120px
最后a,b,c的实际宽度分别为:200-40=160px, 200-40=160px, 200-120=80px

flex-basis

初始化flex项目的大小。默认值auto.接受任何用于width的值。px | % | rem | em; 如果为0也需要加单位。

1
2
3
li {
flex-basis: 150px; // 初始固定大小
}

flex速记

1
2
3
li{
flex: 0 1 auto; // flex-grow、flex-shrink、flex-basis
}

align-self

改变一个弹性项目沿着侧轴的位置而不影响弹性项目

接受属性值: auto || flex-start || flex-end || center || stretch || baseline

stretch

auto为设置为父元素的align-items的值,若没有父元的话,设置为stretch

绝对和相对flex项目

二者区别在于间距的计算,相对flex项目的间距根据内容大小计算,绝对flex项目的间距根据flex属性计算,而不是内容

1
2
3
li{
flex: 1 1 auto;
}

relative

flex-basis: auto; 宽度自动计算; 此时为相对flex项目

1
2
3
li{
flex: 1;
}

flex项目变为绝对的了,宽度计算依据flex属性

relative

flex-basis: 0; 基于flex-grow共享可用空间

所有Flexbox属性都是基于合适的flex-direction起作用。 当在Flex项目上使用 margin: auto 时,值为 auto 的方向(左、右或者二者都是)会占据所有剩余空间

路由守卫

任何时候导航到任何地方往往不能满足业务需求,需要授权守护。返回值是boolean的三种形式

  • Observable
  • Promise
  • boolean

往往同步守卫不是一个好的选择,阻塞的情况时常发生。如果返回true,导航继续,否则,导航终止,停留原地

The router supports multiple guard interfaces:

在分层路由的每个级别上,我们都可以设置多个守卫。 路由器会先按照从最深的子路由由下往上检查的顺序来检查CanDeactivate()和CanActivateChild()守卫。 然后它会按照从上到下的顺序检查CanActivate()守卫。 如果特性模块是异步加载的,在加载它之前还会检查CanLoad()守卫。 如果任何一个守卫返回false,其它尚未完成的守卫会被取消,这样整个导航就被取消了。

特性模块的授权验证

匿名用户会重定向到登录页,因为区分用户是否授权,所以创建在根目录下(auth-guard.ts)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/rotuer';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
let url = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
if ('登录成功标识') {
reutrn true;
}
this.router.navigate(['/login']);
return false
}
}
ActivatedRouteSnapshot包含即将被激活的路由,RouterStateSnapshot包含即将到达的状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { AuthGuard } from '../aut-guard.service';
const routes : Routes = [
path: 'admin',
component: AdminComponent,
canActive: [AuthGuard],
...
]
@NgModule({
providers: [
AuthGuard
],
'''
})
...

CanActivateChild区别在于子路由被激活前的守卫

1
2
3
4
import { CanActivateChild } from '@angular/router';
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.canActivate(route, state);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes : Routes = [
path: 'admin',
component: AdminComponent,
canActive: [AuthGuard],
children: [
{
path: '',
canActivateChild: [AuthGuard],
children: [
...
]
}
]
]
...

CanDeactivate 处理未保存的更改;不保存并离开(true),保留更改并留下(false)

1
2
3
4
5
6
canDeactivate(): Observable<boolean> | promise<boolean> | boolean {
if (!this.changeStatus) {
return true; //如果没有改变直接导航,否则弹框
}
return this.someService.confirm('discard changes?');
}
1
2
3
4
5
6
7
8
9
10
11
{
path: ':id',
component: CrisisDetailComponent,
canDeactivate: [CanDeactivateGuard]
}
@NgModule({
providers: [
CanDeactivateGuard
],
'''
})

Resolve 预先获取数据

如果响应时间够长,就需要预先从服务器上获取数据,路由激活瞬间数据渲染完毕,此处需要Resolve守卫。

1
2
3
4
5
6
7
8
9
10
11
12
resolve(route: activatedRouteSnapshot, state: RouterStateSnapshot):Observable<any>{
const id = route.paramMap.get('id');
return this.someService.getDate(id).map(x => {
if (x) {
return x;
}else {
this.router.naviagte(['./someWhere']);
return null;
}
})
}
// 此处可以阻止路由被加载,直到数据获取完毕,如果没有数据获取则导航回指定路由,并且NgModule的providers中需要注册

异步路由

异步路由可以在获取请求时惰性加载特性模块,并且带来了一下好处

  • 对于体积庞大的特性模块可以在用户请求时进行加载
  • 持续扩充特性模块的功能,不用增加初始加载的体积及速度
  • 模块化开发提升开发效率,结构分明
1
2
3
4
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule' // 相对于app目录
}
惰性加载只会发生一次,在该路由首次被请求时,后续的请求是立即可用的

Canload守卫 保护对特性模块的未授权加载

1
2
3
4
canload(route: Route): boolean {
let url = `${route.path}`; // route为准备访问的目标地址
return this.checkLogin(url);
}
1
2
3
4
5
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',
canLoad: [AuthGuard]
}
路由加载形式:立即加载、惰性加载、预加载

预加载好比后台加载,在保证了尽可能小的初始加载体积和首屏加载速度的同时,同样满足了特性模块的按需加载;理想情况下,首屏加载完毕后会有一个短暂的空档期,如果此时完成了接下来将要访问的模块的加载成功,体验会有很大提高。此时的加载就是预加载。

原理

导航完成后,路由器会查找没有加载但是可以加载的模块,此时预加载策略决定了是否加载以及加载哪些模块。

Router内置了两种预加载策略

  • 完全不预加载,默认
  • 预加载所有特性模块: PreloadAllModules
1
2
3
4
import { PreloadAllModules, RouterModule } from '@angular/router'
RouterModule.forRoot(appRoutes, {
preloadingStrategy: PreloadAllModules
})
CanLoad守卫会阻塞预加载策略,优先级高于预加载策略

自定义预加载策略

结合路由定义时的data属性,只预加载preload为true的路由

1
2
3
4
5
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',
data: { preload: true }
}

添加selective-preloading-strategy.ts,实现自定义预加载策略

1
2
3
4
5
6
7
8
9
10
11
12
13
import { PreloadingStrategy , Route } from '@angular/router';
@Injectable()
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preloadModules: string[] = [];
preload(route: Route, load: () => Observable<any>) : Observable<any> {
if (route.data && route.data['preload']) {
this.preloadModules.push(route.path);
return load();
} else {
return Observable.of(null);
}
}
}

如果要进行预加载,返回一个靠用加载器的Observable,否则返回一个null值的Observable对象。

重定向迁移URL

1
{ path: 'heroe/:id', redirectTo: '/superHero/:id'}
1
RouterModule.forRoot(rotues, { useHash: true }) // 基于HashLocationStrategy

装饰器

方法装饰器

LogComponent组件只为负责打印日志,通过log装饰器修饰printLog方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class LogComponent {
@log
printLog(str) {
return str;
}
}
function log(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
// descriptor is a object contains writable, enumberable, configurable, setter, getter, value;
let origin = descriptor.value;
descriptor.value = function (...args) { // apply方法
console.log(this); // LogComponent
let result = origin.apply(this, args);
console.log('Log is - ' + result);
return result;
}
return descriptor;
}

应用场景: 可以在其他组件内继承LogComponent,也可以封装为一个service,服务于各个组件,职责单一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export class AppComponent extends LogComponent  implements OnInit {
constructor(
private viewContainerRef: ViewContainerRef,
private renderer: Renderer2,
private elementRef: ElementRef,
) {
super();
}
ngOnInit(): void {
this.setupMenus();
this.printLog('test');
}
printLog(str) {
super.printLog(str);
}
}

参数装饰器

1
2
3
4
5
6
7
8
9
@Injectable()
export class AppService {
login( @Inject name: string) { }
}

function Inject(target: Object, propertyKey: string, parameterIndex: number) {
console.log(propertyKey); // login 参数名称,注意是方法名
console.log(parameterIndex); // 0 参数索引
}

泛型(Generic)

集合类型如果设置为any可以实现同时支持多种类型,但是放弃了原本支持的类型检查,泛型则是为了解决这一点;帮助进行后面的类型检查

class Name<T>{
    sayName(name:T): void{
        console.log(' hello, i'm ' + name);
    }
}
let user1 = new Name<string>();
user1.sayName('lee');

let user2 = new Name<number>();
user2.sayName(3);

常规面试总结2017

导语:

在2017年对前端领域内的常见的问题进行系统性的总结,说法可能会有所不妥,会综合网上资源发表自己的见解

1.前端安全的理解与防范

开发过程中不可避免的会出现漏洞,黑客会抓住漏洞去攻击它获取利益,所以我们需要让我们的应用变的更加安全。

前端攻击有哪些形式,如何防范

XSS攻击:

一种安全漏洞,允许代码植入到其他页面中,通过插入script标签获取用户信息

如何防范: 将前端输入输出数据进行转义,避免使用eval执行个人重要信息,使用httpOnly提升cookie的安全性,限制web页面浏览器端script程序读取cookie。当使用append的时候,Jquery会将元素变为fragment,接着查找其中的script标签,使用eval去执行,会造成之前的问题,所以将输入输出部分进行转义,同样使用img标签时当加载失败时会调用onError方法,此时插入攻击代码同样需要转义进行防范

CSRF攻击:

跨站请求伪造,利用一些提交行为转换到操作其他网站(在网站支付时打款被转义到黑客账户);

如何防范: 遵循http协议,token即时验证,添加验证码阻止信息外泄

控制台注入代码:

不懂的人会被欺骗到某个网站在控制台通过执行某段代码暴漏个人信息从而被黑客截取。

2.this指向与箭头函数

this指向

this在函数执行时被绑定,指向为调用该函数的对象。函数调用模式的不同造就了this指向问题上的差异。
函数作为一个对象的方法时,this指向该对象

1
2
3
4
5
6
7
var name = 'window';
var obj = {
name: 'lee',
say: function(){
console.log(this.name); // lee
}
}

函数暴漏在全局作用域下,this指向为window
1
2
3
4
var name = 'window';
function say(){
console.log(this.name) // lee
}

构造函数内部this指向为构造函数实例对象
1
2
3
4
5
function Obj(){
this.name = 'lee';
}
var obj = new Obj();
obj.name // lee;

改变this指向的方法: apply、call、bind, call与apply的区别在于第二部分参数前者为多个参数,后者为一个参数数组,bind与它们的差异在于只是返回一个改变了this指向的新函数,需要调用,而apply与call在改变了this指向后立即执行了。

1
2
3
4
5
6
7
8
9
10
11
12
var name = "window";
var person = {
name: "lee"
};
function say() {
console.log(this.name);
}
say(); //window
say.apply(person); //lee
say.call(person); //lee
say.bind(person)(); //lee
say.apply(); //window

箭头函数

es6新增的特性之一,简化了函数定义.

1
2
3
x => ({
foo: x // 单纯的返回一个表达式对象,注意需要加()
})

this

箭头函数内部this是词法作用域,由上下文确定;而函数中的this指向则在函数执行时根据调用模式确定。

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = function () {
return new Date().getFullYear() - this.birth; // 匿名函数this指向window
};
var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象

return fn();
}
};

3.var let const 之间的区别

var定义的变量是该变量作用域的局部变量,可以定义全局变量,但是会污染全局环境不容易维护,不推荐。
const定义常量,且不可重新赋值,但是如果定义的变量是一个对象的话,对象内部变量是可以改变的。
let声明块级作用域,块作用域内的变量在包含它们的块或for循环之外是不能访问的,否定变量声明提升,对var的一种增强。
es6里面不建议使用var,因为其没有块级作用域,非严格模式下会有变量声明提升的情况,会产生意想不到的错误。

1
2
3
4
5
for(var i = 0; i <5; i++){
setTimeout(function(){
console.log(i)
},10)
}

结果会是 5 5 5 5 5; 为什么呢,怎么能打印出想要的 0,1,2,3,4,呢?

setTimeout事件在for循环结束后触发,此时i的值为5,解决方法;

1
2
3
4
5
for(var i = 0; i<5; i++){
(function(i){
setTimeout(function() { console.log(i); }, 10);
})(i)
}

利用立即执行函数迭代i的值;如果利用块级作用域呢?
1
2
3
4
5
for(let i = 0; i <5; i++){
setTimeout(function(){
console.log(i)
},10)
}

每次for循环都会产生对应的作用域

4.深拷贝与浅拷贝

浅拷贝只是复制了对象的指针,不会赋值对象本身,公用一块内存,所以改变一个对象的属性值都会变化,

1
2
3
4
5
6
7
8
var a = {
name: 'john'
}
var b = a;
b.name = 'jeff';

a // { name: 'jeff};
b // { name: 'jeff};

深拷贝则复制了对象,不会共享内存与指针,修改一个不会影响到另一个;
1
2
3
4
5
6
7
8
var a = {
name: 'john'
}
var b = Object.assign({}, a);
b.name = 'jeff';

a // { name: 'john};
b // { name: 'jeff};

但是由于javascript中存储对象都是存地址的,Object.assign的局限性存在于它只是相对浅拷贝深入了一层,换句话就是如果对象的属性值是一个指向对象的引用,它只拷贝那个引用值,可以利用对象字符串的转换(JSON.parse(JSON.stringify(obj))与递归实现真正的深拷贝。
1
2
3
4
5
6
var obj = { a:1, arr: [2,3] };
var shadowObj = Object.assign({}, obj);
obj.arr[0] = 22;
obj.a = 11;
// obj { a: 11, arr: [22,3]}
// shadObj { a: 1, arr: [22,3]}

Rxjs Subject 源码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* Subject继承于Observable
*/
export class Subject extends Observable {
constructor() {
super();
this.observers = []; // 观察者列表
this.closed = false;
this.isStopped = false;
this.hasError = false;
this.thrownError = null;
}
next(value) {
if (this.closed) {
throw new ObjectUnsubscribedError();
}
if (!this.isStopped) {
const { observers } = this;
const len = observers.length;
const copy = observers.slice();
for (let i = 0; i < len; i++) { // 循环调用观察者next方法,通知观察者
copy[i].next(value);
}
}
}
error(err) {
if (this.closed) {
throw new ObjectUnsubscribedError();
}
this.hasError = true;
this.thrownError = err;
this.isStopped = true;
const { observers } = this;
const len = observers.length;
const copy = observers.slice();
for (let i = 0; i < len; i++) { // 循环调用观察者error方法
copy[i].error(err);
}
this.observers.length = 0;
}
complete() {
if (this.closed) {
throw new ObjectUnsubscribedError();
}
this.isStopped = true;
const { observers } = this;
const len = observers.length;
const copy = observers.slice();
for (let i = 0; i < len; i++) { // 循环调用观察者complete方法
copy[i].complete();
}
this.observers.length = 0; // 清空内部观察者列表
}
}

因为 Subject 在订阅时,是把 observer 存放到观察者列表中,并在接收到新值的时候,遍历观察者列表并调用观察者上的 next 方法

Subject继承自Observable,将Observable的单路推送转换为多路推送。它就是讲单路Observable转变为多路Observable的桥梁。

Subject的几个衍生类:BehaviorSubject,ReplaySubject,AsyncSubject;

BehaviorSubject:保存最近向数据消费者发送的值,当一个Observer订阅后,他会立即收到最新的值;它非常适合表示随时间推移的值;BehaviorSubject 形容一个人的生日,随时间不断更新;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var subject = new Rx.BehaviorSubject(0) //初始值
subject.subscribe({
next:(v) => {
console.log('A' + v )
}
})
subject.next(1);
subject.next(2);
subject.subscribe({
next:(v) => {
console.log('B' + v )
}
})
subject.next(3);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var subject = new Rx.ReplaySubject(3); /* 回放数量 */
subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});

subject.next(5);

ReplaySubject 如同于 BehaviorSubject 是 Subject 的子类。通过 ReplaySubject 可以向新的订阅者推送旧数值,就像一个录像机 ReplaySubject 可以记录Observable的一部分状态(过去时间内推送的值);.一个 ReplaySubject 可以记录Observable执行过程中推送的多个值,并向新的订阅者回放它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var subject = new Rx.AsyncSubject();

subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});

subject.next(5);
subject.complete();

AsyncSubject是Subject的另外一个衍生类,Observable仅会在执行完成后(complete),推送执行环境中的最后一个值。业务上很少用

既然Subject是一个Observer,你可以把它作为subscribe(订阅)普通Observable时的参数

1
2
3
4
5
6
7
8
9
10
11
12
var subject = new Rx.Subject();

subject.subscribe({
next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
next: (v) => console.log('observerB: ' + v)
});

var observable = Rx.Observable.from([1, 2, 3]);

observable.subscribe(subject); // 你可以传递Subject来订阅observable

通过添加两个Observer到Observer列表中,之后Observable直接订阅Observer列表将普通的单路推送转换为多路推送

Cold & HOT

observable is default cold; cold: 表示只有 subscribe 出现 observer 才会被激活; 当有多个subscribe时,每一个都是一条独立的链;hot: 每个subscirbe共享一个链,不管什么时间插入subscribe,都不会重新开始。如何把一个cold 变成 hot?Subject则可以充当中介。multicast、refCount、publish、share则是通过Subject完成将cold转变为hot的方法。

1
2
3
4
5
6
7
8
9
10
11
let sub = new Subject();
let obs = sub.map(v => {
console.log("ajax call");
});
obs.subscribe(v => console.log("subscribe 1"));
obs.subscribe(v => console.log("subscribe 2"));
sub.next("value");
// ajax call
// subscribe 1
// ajax call
// subscribe 2

模拟异步请求数据的业务场景,如果有更多的subscribe的时候,则会对请求服务器多次,造成服务器负载严重,此时一般解决方法为以下两种

1
2
3
let obs = sub.map(v => {
console.log("ajax call");
}).share();
1
2
3
let obs = sub.map(v => {
console.log("ajax call");
}).publish().refCount();

引入multicast(组播)的概念,通过中介者订阅源序列在由它推送出去,下面是它的运作方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var source = Rx.Observable.interval(1000).take(3);

var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}

var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}

var subject = {
observers: [],
subscribe: function(observer) { //addObserver
this.observers.push(observer)
},
next: function(value) {
this.observers.forEach(o => o.next(value))
},
error: function(error){
this.observers.forEach(o => o.error(error))
},
complete: function() {
this.observers.forEach(o => o.complete())
}
}

subject.subscribe(observerA)

source.subscribe(subject);

setTimeout(() => {
subject.subscribe(observerB);
}, 1000);

换一种形式,用multicast方法来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var source = Rx.Observable.interval(1000)
.take(3)
.multicast(new Rx.Subject());

var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}

var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}

source.subscribe(observerA); // subject.subscribe(observerA)

source.connect(); // source.subscribe(subject) //开始推送

setTimeout(() => {
source.subscribe(observerB); // subject.subscribe(observerA)
}, 1000);
1
2
3
4
5
6
7
var result = Observable.interval(1000).take(6)  //执行两次
.map(x => Math.random())
// .share() //不会因为订阅者数量而执行多次
// .publish().refCount()

var subA = result.subscribe(x => console.log('A: ' + x));
var subB = result.subscribe(x => console.log('B: ' + x));

常用应用场景

1
2
3
4
5
6
7
let sub = new Subject();
let obs = sub.map(v => {
console.log("ajax call"); //请求接口
});
obs.subscribe(v => console.log("subscribe 1")); //分发
obs.subscribe(v => console.log("subscribe 2"));
sub.next("value");

ajax会打印两次,增加服务器端负载; 调用share()方法;其中angular2中的http也是not share的,在类似场景中同样的问题;

建立一個 subject 先拿去訂閱 observable(source),再把我們真正的 observer 加到 subject 中,這樣一來就能完成訂閱,而每個加到 subject 中的 observer 都能整組的接收到相同的元素。

Observable.multicast(new Rx.Subject()) == Observable.publish();对于Subject三种衍生形式,publishReplay(1)、publishBehavior(0)、publishLast()

另外 Observable.publish().refCount() == Observable.share()

总结Subject!

  • 既是Observable又是Observer
  • 对内部的observers进行组播
  • observer default is cold and not share.(cold 表示只有 subscribe 出现 observer 才会被激活. not share 表示每一个 subscribe 都会激活 observer 链)

业务场景:窗口a接收到A,b接收到B,c接受到C,本窗口d则需要异步的捕获a和b窗口的值并乘c窗口的值,d = (a+b)*c;

我们可以把每个数据的变更定义成流,然后定义出这些流的组合关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const A = new Rx.Subject()
const B = new Rx.Subject()
const C = new Rx.Subject()

const D = Rx.Observable
.combineLatest(A, B, C)
.map(data => {
let [a, b, c] = data
return (a + b) * c
})

D.subscribe(result => console.log(result))

setTimeout(() => A.next(2), 3000)
setTimeout(() => B.next(3), 5000)
setTimeout(() => C.next(5), 2000)

setTimeout(() => C.next(11), 10000)

为了简单,我们用定时器来模拟异步消息。实际业务中,对每个Subject的赋值是可以跟AJAX或者WebSocket结合起来,而且对D的那段实现毫无影响。我们可以看到,在整个这个过程中,最大的便利性在于,一旦定义完整个规则,变动整个表达式树上任意一个点,整个过程都会重跑一遍,以确保最终得到正确结果。无论中间环节上哪个东西变了,它只要更新自己就可以了,别人怎么用它的,不必关心。而且,我们从D的角度看,他只关心自己的数据来源是如何组织的,这些来源最终形成了一棵树,从各叶子汇聚到树根,也就是我们的订阅者这里,树上每个节点变更,都会自动触发从它往下到树根的所有数据变动,这个过程是最精确的,不会触发无效的数据更新。

首先贴上一个中文Rxjs文档链接,从官网被人直译下来的,也是比较全面的介绍rxjs了。最好的Rxjs中文文档

RXJS全名Reactive Extensions for JavaScript,JavaScript的响应式扩展。什么是响应式?响应式就是跟随时间不断变化的数据、状态、事件等转换成可被观察的序列,然后订阅那些变化,一旦变化则会执行业务逻辑。适用于异步场景。

RxJS所能解决的问题:

时刻保持响应。这对于一个应用来说意味着当他处理用户的输入或者凭借AJAX从服务器接受一些数据时停止是一件不可能接受的事情。在JavaScript中解决问题的方案始终是大量运用回调函数来进行一些运行的处理。但回调的使用使内容丰富的大型应用变得凌乱,一旦你需要多块数据时你就陷入了回调地狱。Angular2中,组件间通讯@Output对应的EventEmitter实际上就是一个Subject(主题:同时为Observable和Observer);Http模块中Observable作为大部分API的交互对象使用。但是这只是官方的外部扩展,并不必须,也可以使用.toPromise的方式转换为Promise来使用或第三方扩展库或Fetch API 传统Ajax已死,Fetch永生


RxJS初探:

RxJS是一个解决异步问题的JS开发库.它带来了观察者模式和函数式编程的相结合的最佳实践。 观察者模式是一个被实践证明的模式,基于生产者(事件的创建者)和消费者(事件的监听者)的逻辑分离关系.况且函数式编程方式的引入,如说明性编程,不可变数据结构,链式方法调用会使你极大的简化代码量。RxJS 引入了一个重要的数据类型——流(stream)。流(Streams)无非是随时间流逝的一系列事件。流(Streams)可以用来处理任何类型的事件,如:鼠标点击,键盘按下,等等。你可以把流作为变量,它有能力从数据角度对发生的改变做出反应。


Stream

流(Streams)无非是随时间流逝的一系列事件。流(Streams)可以用来处理任何类型的事件,如:鼠标点击,键盘按下等等。你可以把流作为变量,它有能力从数据角度对发生的改变做出反应。

1
2
3
4
5
6
var a = 2;
var b = 4;
var c = a + b;
console.log(c); //-> 6
a = 10; // reassign a
console.log(c); //-> still 6

事件引发的改变总是从事件源(生产者),向下传递到所有事件监听方(消费者)。如果变量看做流呢,又是什么结果呢?

1
2
3
4
5
6
var A$ = 2;
var B$ = 4;
var C$ = A$ + B$;
console.log(C$); //-> 6
A$ = 10;
console.log(C$); //-> 16

流的方式重新定义的变量值的动态行为.换句话说,C$是把两个流变量A$和B$进行合并操作。当一个新值被推进了A$,C$立刻响应式的变更为16。

开始了解RxJS中的几个重要成员

  • Observable可观察对象:表示一个可调用的未来值或者事件的集合。
  • Observer观察者:一个回调函数集合,它知道怎样去监听被Observable发送的值
  • Subscription订阅: 表示一个可观察对象的执行,主要用于取消执行。
  • Operators操作符: 纯粹的函数,使得以函数编程的方式处理集合比如:map,filter,contact,flatmap。
  • Subject(主题):等同于一个事件驱动器,是将一个值或者事件广播到多个观察者的唯一途径。
  • Schedulers(调度者): 用来控制并发,当计算发生的时候允许我们协调,比如setTimeout,requestAnimationFrame。

    第一个例子

    使用RxJS创建一个可观察对象:

    1
    2
    3
    4
    5
    var button = document.querySelector('button');
    Rx.Observable.fromEvent(button, 'click')
    .throttleTime(1000)
    .scan(count => count + 1, 0)
    .subscribe(count => console.log(`Clicked ${count} times`));

    observable可观察对象,以惰性的方式推送多值的集合

    下面的例子是一个推送1,2,3,4数值的可观察对象,一旦它被订阅1,2,3,就会被推送,4则会在订阅发生一秒之后被推送,紧接着完成推送

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var observable = Rx.Observable.create(observer=>{
    observer.next(1);
    observer.next(2);
    observer.next(3);
    setTimeout(() => {
    observer.next(4);
    observer.complete();
    }, 1000);
    });

    Pull拉取 VS Push推送

    拉和推是数据生产者和数据的消费者两种不同的交流协议(方式);


    什么是”Pull拉”?在”拉”体系中,数据的消费者决定何时从数据生产者那里获取数据,而生产者自身并不会意识到什么时候数据将会被发送给消费者。

    每一个JS函数都是一个“拉”体系,函数是数据的生产者,调用函数的代码通过“拉出”一个单一的返回值来消费该数据(return 语句)。

什么是”Push推”?在推体系中,数据的生产者决定何时发送数据给消费者,消费者不会在接收数据之前意识到它将要接收这个数据。

Promise(承诺))是当今JS中最常见的Push推体系,一个Promise(数据的生产者)发送一个resolved value(成功状态的值)来注册一个回调(数据消费者),但是不同于函数的地方的是:Promise决定着何时数据才被推送至这个回调函数。

RxJS引入了Observables(可观察对象),一个全新的”推体系”。一个可观察对象是一个产生多值的生产者,并”推送给”Observer(观察者)。

RxJS VS Promise—3个最重要的不同点

不同点 Rxjs Promise
动作是否可以取消?
是否可以发射多个值?
各种工具函数?

Operators操作符

create() 创建一个可观察序列,参数为一个封装数据生成逻辑的函数,该函数的参数为观察者

empty() 不需要传递参数,创建一个空序列并立即结束

1
2
3
Rx.Observable.empty()  =  Rx.Observable.create((o)=>{
o.onCompleted()
})

never() 不需要传递参数,创建一个空序列,并永远不结束

1
2
3
Rx.Observable.never()  =  Rx.Observable.create((o)=>{

})

throw() 创建一个空序列,参数来声明错误并立即抛出错误

1
2
3
Rx.Observable.throw('error')  =  Rx.Observable.create((o)=>{
0.onError('error')
})

range() 创建一个有线长度的整数序列,两个参数,第一个起始值,第二个元素数量

1
Rx.Observable.range(30,4)  //输出:30,31,32,33

interval() 创建一个无限长度的周期性序列

1
Rx.Observable.interval(1000) //输出:0,1,2...

timer() 指定一个额外的参数来调节第一值的静默时长,第二个参数可选,若无则仅仅在规定的静默时长后输出一个值,然后结束序列

1
Rx.Observable.timer(0,1000) //输出:0,1,2...

from() 可以将已有的数据转化为Observable,参数为iterable数据集对象,常见Array,String

1
Rx.Observable.from(iterable)

of() 不在同一个数据集中的多个来源的数据,使用of()方法直接构造:

1
2
Rx.Observable.of([1,2,3])    //    [1,2,3]
Rx.Observable.from([1,2,3]) // 1,2,3

just() 将任何数据转化为一个单值输出的Observable

1
Rx.Observable.just([1,2,3])    //    [1,2,3]

repeat() 创建一个重复值序列 par1:值,par2:次数

1
Rx.Observable.repeat('a',2)    //   a a

fromEvent() 将事件流转化为Observable,

1
2
var el = document.getElementById("btn"); //DOM对象作为事件源
Rx.Observable.fromEvent(el,"click");

toArray() 将序列还原为数组对象,只有在订阅后才还原为数组

1
2
3
4
5
var source = Rx.Observable.of(1,2,3,4);    //序列:1 2 3 4 
var target = source.toArray(); //序列:[1,2,3,4]
target.subscribe(function(d){
console.log(d); //d: [1,2,3,4]
})

delay() 推迟 参数为数字或Date对象

delaySubscription() 延迟订阅 参数同理

startWith() 可以在源序列之前添加额外的元素

1
2
var source = Rx.Observable.of(1,2,3); //序列:1 2 3
var target = source.startWith(7,8,9); //序列:7 8 9 1 2 3

map() 对源序列进行变换,并返回新的序列(改变了源)

1
2
3
4
5
var trandform = function(item){
return item*2;
}
var source = Rx.Observable.of(1,2,3); //输出: 1 2 3
var target = source.map(trandform); //输出: 2 4 6

concat() 有序拼接 merge()无序

1
2
3
4
5
var source = Rx.Observable.of(1,2,3); //序列:1 2 3
var target = source.concat(
Rx.Observable.of(4,5,6),
Rx.Observable.of(7,8,9)
); //序列:1 2 3 4 5 6 7 8 9

concatAll() 如果源序列的元素也是序列 —— 序列的序列,那么可以使用concatAll() 方法将各元素序列按顺序拼接起来

1
2
3
4
5
var source = Rx.Observable.of(10,20,30)
.map(function(item){
return Rx.Observable.range(item,3);
}); //序列: Observable{10,11,12} Observable{20,21,22} Observable{30,31,32}
var target = source.concatAll(); //序列:10 10 12 20 21 22 30 31 32

catch() 捕捉源序列错误,返回新序列

1
2
3
4
5
6
7
var source = Rx.Observable.create(function(o){
o.onNext(1);
o.onNext(2);
o.onError(new Error("fake error"));
o.onNext(4);
}); //序列: 1 2 <ERROR> 4
var target = source.catch(Rx.Observable.from("abc")); //序列: 1 2 a b c

pluck() 针对元素为json对象的源序列,返回指定属性的值的序列

1
2
var source = Rx.Observable.of({name:'john',age:33},{name:'lee',age:22});
var target = source.pluck('name'); //序列 john lee

flatMap() 平坦化映射:首先将一个序列的各元素映射为序列,然后将各序列融合 参数是一个映射函数,返回值为序列

1
2
3
4
5
var source = Rx.Observable.of(10,20);
var mf = function(item){
return Rx.Observable.range(item,3)
}
var target = source.flatMap(mf) // 序列:10,11,20,12,21,22

flatMapLatest() 与flatMap()的区别在于将最新的序列中的元素输出

concatMap() 将源序列各元素映射为序列,然后按顺序拼接 (与flatMap的区别所在)

1
2
3
4
5
var source = Rx.Observable.of(1,2,3);
var mf = function(item){
return Rx.Observable.range(item,3);
}
var target = source.concatMap(mf) //序列:1,2,3,2,3,4,3,4,5

flatMap与map异同点

Map用于对自身对象数值进行映射,将发射对象转换成另一个发射对象发射,返回一个包含映射结果的Observable对象。而flatMap是把自身对象里的数值进行映射并转换成一个新的Observable对象.当从其他类型对象中构建Observable对象时,需要使用flatMap方法

flatMap与concatMap异同点

merge和map结合与concat和map结合的区别,归根结底为merge与concat的区别,一个无序一个有序

filter() 筛选源序列中满足条件的元素,并返回新的序列

1
2
var source = Rx.Observable.of(1,2,3,4,5); //序列: 1 2 3 4 5
var target = source.filter(x => x<4) //序列: 1 2 3

skip(num) 抑制序列头部元素数量输出 skipLast(num)尾部 skipWhile(if)指定一个条件

take(num) 截取序列头部元素数量输出 takeLast(num)尾部 takeWhile(if)指定一个条件

distinct 去重,并返回一个新序列

1
2
var source = Rx.Observable.of(1,2,3,4,2,1); //序列: 1 2 3 4 2 1
var target = source.distinct(); //序列:1 2 3 4

distinctUntilChanged 去重,并返回一个新序列

1
2
var source = Rx.Observable.of(1,2,2,3,4,2,1); //序列: 1 2 3 4 2 1
var target = source.distinctUntilChanged(); //序列:1 2 3 4 2 1

debounce 去抖动,一段时间内只取最新数据作为一次发射数据,其他数据取消发射

throttle(和debounce唯一区别是debounce取一段时间内最新的,而throttle忽略这段时间后,发现新值才发送, 通俗讲,都设定一个时间周期,持续触发事件,throttle为每到时间周期便会触发一次,bebounce为触发周期小于设定时间周期不予事件触发)

buffer() 使用第二个序列触发源序列中多个元素的打包

1
2
3
var source = Rx.Observable.timer(0,1000);    //序列:0 1 2 3 ...
var boundaries = Rx.Observable.timer(2500); //延时2500ms触发
var target = source.buffer(boundaries); //序列: [0,1,2]

bufferWithTime() 按固定时间间隔对源序列进行打包

1
2
var source = Rx.Observable.timer(0,1000); //序列:0 1 2 3 ...
var target = source.bufferWithTime(2500); //序列:[0,1,2] [3,4] ...

zip() 支持可变数量的序列作为参数,最后一个参数应当是一个组合函数, 其返回值将作为目标序列的元素

1
2
3
4
var source1 = Rx.Observable.of(1,2,3); //序列: 1 2 3
var source2 = Rx.Observable.of(4,5,6); //序列:4 5 6
var cf = function(d1,d2){ return d1 + '-' + d2;};
var target = Rx.Observable.zip(source1,source2,cf); //序列: 1-4 2-5 3-6

forkJoin() 将多个序列的最后一个元素组合为一个数组后,作为目标序列的唯一元素

1
2
3
var source1 = Rx.Observable.of(1,2,3); //序列: 1 2 3
var source2 = Rx.Observable.of(4,5,6); //序列: 4 5 6
var target = Rz.Observable.forkJoin(source1,source2); //序列:[3,6]

combineLatest() 将多个序列的最后一个元素,使用组合函数构成目标序列的一个新元素

1
2
3
4
5
6
var source1 = Rx.Observable.interval(200).map(x => 'First:' + x)
var source2 = Rx.Observable.interval(100).map(x => 'Second:' + x)
var source = Rx.Observable.combineLatest(
source1,
source2
).take(4); // ["First:0", "Second:0"] ["First:0", "Second:1"] ["First:0", "Second:2"] ["First:1", "Second:2"]

依赖注入是一种软件设计模式,他允许你移除软件组件的硬编码方式,替代的是通过依赖注入制造低耦合的组件不论在编译阶段还是在运行阶段。硬编码就是在程序中将代码写死,低耦合就是尽量让每个模块独立,相关的处理尽量在单个模块中完成。


AngularJs有一个内在的注入机制,他可以把你的App分成许多个可重复使用的组件,当需要的时候通过依赖注入把这些自减注入进你的App中去。在需要的地方进行参数传递,这种方法不仅对测试很有用,而且还不会污染全局变量,是很好的设计模式。


AngularJS依赖注入的方法

  1. 通过函数的参数进行推断式注入声明

    如果没有明确的声明,AngularJS会假定名称就是依赖的名称。因此,它会在内部调用函数对象的toString()方法,分析并提取函数的参数列表,然后通过$injector将这些参数再注入进对象实例。下面是代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function myController($scope,$timeout){
    var updateTime = function(){
    $scope.clock = {
    time: new Date()
    };
    $timeout(function(){
    $scope.clock.time = new Date();
    updateTime();
    },1000)
    }
    }
    1
    2
    3
    <div ng-controller='myController'>
    <span>{{clock.time | data:'yyyy-MM-dd hh:mm:ss'}}</span>
    </div>

    创建了一个可以自动更新时间的应用,看看是如何进行依赖注入的。通过设置参数$scope和$timeout,angular会在内部调用函数的toString()方法,分析并提取函数的参数列表,然后通过$injector将这些参数注入到对象的实例。



    注意:

  • 此方法只适合未经压缩混淆的代码,因为angular需要解析未经压缩混淆的参数列表。

  1. 显式的注入声明

    显式的明确定义一个函数在被调用时需要用到的依赖关系,通过这种方法声明依赖,即使在源代码被压缩,参数名称发生改变的情况下依然可以工作。代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var mycontrollerFactory = function mycontroller($scope,$timeout){
    var updateTime = function () {
    $scope.clock = {
    time: new Date()
    };
    $timeout(function () {
    $scope.clock.time = new Date();
    updateTime();
    }, 1000);
    }
    updateTime();
    }
    1
    mycontrollerFactory.$inject = ['$scope','$timeout'];

    显式的将我们需要的依赖注入到函数中,所以在函数中参数也可以分别换成其他字段。

    1
    var mycontrollerFactory = function mycontroller(s,t){.....}
    注意:

  • 对于这种声明方式,参数的顺序是十分重要的,因为$inject数组元素的顺序必须和注入的参数顺序一一对应。

  1. 行内注入声明

    angular提供的行内注入方法实际上是一种语法糖,它与前面的提到的通过$inject属性进行声明的原理是一样的,但是允许我们在函数定义的时候从行内将参数传入,这种方法方便简洁,而且避免了在定义的过程中使用临时变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    angular.module('app',[])
    .controller('mycontroller',['$scope','$timeout',function($scope,$timeout){
    var updateTime = function () {
    $scope.clock = {
    time: new Date()
    };
    $timeout(function () {
    $scope.clock.time = new Date();
    updateTime();
    }, 1000);
    }
    updateTime();
    }])
    注意:

  • 行内声明的方式允许我们直接传入一个参数数组,而不是一个函数,数组的元素是字符串,他们代表的是可以被注入到对象中的依赖名字,最后一个参数就是依赖注入的目标函数对象本身。

下面来对比一下ng1与ng2的依赖注入的区别,以login组件为例

先来看一下angular2的架构图

providerToservice


Angular1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Login{
formValue : {login:string,password:string} = {login:'',password:''};
onSubmit(){
const service = new LoginService();
service.login(this.formValue);
}
}

module.component('login',{
controller: Login,
controllerAs ; 'ctrl',
template:`
<form ng-submit = "ctrl.onSubmit()">
Text <input type="text" ng-model="ctrl.formValue.login>
password <input type="password" ng-model="ctrl.formValue.password">
<button>submit</button>
</form>
`
})

Angular2
1
2
3
4
5
6
7
8
9
10
11
12
@Component({
selector:'login',
template:`
.....
`
})
class Login{
onSubmit(formValue:{login:string,password:string}){
const service = new LoginService();
service.login(formValue);
}
}

在这里直接把login组件绑定在login service上的,很难进行独立测试,也降低了它复用的可能性


with DI

在构造函数里面注射一个LoginService的实例,而不是直接创建

Angular1

1
2
3
4
5
6
7
class Login{
formValue : {login:string,password:string} = {login:'',password:''};
constructor(public service:LoginService){}
onSubmit(){
this.service.login(this.formValue);
}
}

我们还需要告诉框架应该创建这个service的实例

1
module.service('login',LoginService);



Angular2

同样,在构造函数里面注射一个LoginSerivce的实例

1
2
3
4
5
6
class Login{
constructor(public service: LoginService){}
onSubmit(formValue:{login:string,password:string}){
this.service.login(formValue);
}
}
  • 不同于ng1,我们需要将这个service添加到providers列表里面来实现
    1
    2
    3
    4
    5
    6
    @NgModule({
    bootstrap:[Login],
    providers:[LoginService],
    declarations:[Login]
    })
    class AppModule{}

    如果注入到它的根模块,则整个应用都可以调用,也可以注入到使用它的组件元数据里面,只需在装饰器加上一个providers配置项,这样注入的服务只对自己和后代可用

    1
    2
    3
    4
    5
    @Component({
    selector:'lr',
    providers:[LoginService]
    })
    class APPLr{}
    注意:
  • ng1依赖于字符串来配置DI,而ng2则默认使用注解的方式

    ng1里面有好几个Api可以用来给指令注入依赖,有些是根据名称注入的(LoginService),有些依赖会一直自动提供(link函数里面的),有些需要使用require进行配置


    ng2提供了统一的Api用来注入服务,指令等,所有这些内容都会被注入到组件的构造函数里面

    总结

  • DI是ng的核心机制之一
  • 他可以使你的代码更加松耦合
  • 提升了可测试性
  • ng2采用了统一的Api来给组件注入依赖

  • Directive(指令)
  • Controller(控制器)
  • Service(服务)
    以上为angular1的核心概念,我们究竟什么以什么样的方式去使用它们。

    Service

    Service是单例对象,会经常被传来传去,但是可以保证每次访问的都是同一个实例。所以很多Controler和Directive可以访问它内部的数值,所以它是一个存放数据,实现数据共享的好地方;


    首先创建一个module,

    1
    var module = angular.module('myModule,[]');


    下一步,创建一个服务,用来管理图书的BookService;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    module.factory('BookService',['rootScope',function($rootScope){
    var service = {
    books:[
    { title: "Magician", author: "Raymond E. Feist" },
    { title: "The Hobbit", author: "J.R.R Tolkien" }
    ],
    addbook : function(book){
    service.books.push(book);
    $rootScope.$broadcast('books.updata');
    }
    }
    return service;
    }])

    这里很好理解,一个对象里面有我要存放的书的集合,还有一个添加图书的方法,这个方法还会在应用上广播一个事件,告诉所有使用我们的服务的人,存放书的集合已经更新了,接下来就是要使用它的东西需要接收这个广播了。


    1
    2
    3
    4
    5
    6
    7
    module.controller('books.list',['$scope','BookService',function($scope,BookServie){
    $scope.$on('books.updata',function(event){
    $scope.books = BookService.books;
    $scope.$apply();
    });
    $scope.books = BookService.books;
    }])

    这里就是将前面创建的BookService中存放的books赋值给了controller内部的scope对象。如果我们在controller上创建一个数组,其他地方也要处理书籍的信息,通过scope来维护数据会很麻烦,scope很容易变得混乱不堪,通过一种集中的途径进行数据的管理,更容易理解也可以使代码模块化。所以当需要在不同的地方共享数据的时候,就要依靠服务了,谁要用就注入到谁那里,就这么容易。


    形容服务有一个例子特别的形象,A团A连和B团B连去执行任务,

    A团A连发现敌情,要报告给B团B连,军队里只有服从上级,所以他只能报告给A团A营,在一直向上报告直到A、B共同的长官C旅,C旅在下发给B团,直到B连,要是有个特殊情况,相信这时候B连已经死光了,所以才有了通信部门的存在,而这里的通信部门也就是angular中的服务。

    Controller

    Controller应该纯粹的把Service,依赖关系,以及其他对象串连在一起,通过scope关联到view上。Dom操作的部分最好把它放入指令里面;


    Directive

    应用中最复杂的部分应该在指令中,下面来扩展前面的例子,提供一个按钮,通过这个按钮向服务里面添加一本书。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.directive('addBookButton',['BookService',function(BookService){
    return {
    restrict:'EA',
    replace:true,
    link:function(scope,ele,attrs){
    ele.bind('click',function(){
    BookService.addbook({ title: "Star Wars", author: "George Lucas" });
    })
    }
    }
    }])

    创建了一个指令,目的是向books集合里面添加一本书,books已经注册在服务中,所以可以直接注入服务进行使用。下面将指令应用到视图中

    1
    <button add-book-button>Add Book</button>

    每当点击按钮的时候都会添加那本书,如果将控制器上面添加一个addBook方法呢,

    1
    2
    3
    $scope.addBook = function(){
    BookService.addBook( { title: "Star Wars", author: "George Lucas" } );
    }

    可以得到同样的结果,但是如果需要复用的话,只能copy这段代码,但是通过指令的方式,就会很方便了