每周学习总结系列(一)
一、RESTful的来龙去脉
- level 0
- level 1
- level 2
- level 3
level 0 - 面向服务员
假设我们去麦当劳,想去买个汉堡包,首先和服务员说要一个汉堡包,然后等待喊订单号为123456的客户可以取餐了就行:
//input
{
"addOrder": {
"orderName": "hamburger"
}
}
//output
{
"orderId": "123456"
}
如果这时候,我们有一张会员卡,我们想先查询一下余额,这时我们应该询问一下:
//input
{
"queryBalance": {
"cardId": "886333"
}
}
//output
{
"balance": "0"
}
发现卡里没钱,汉堡没得吃了,于是我们取消订单
{
"deleteOrder": {
"orderId": "123456"
}
}
level 1 - 面向资源
还是上面那个例子,小麦当劳店扩张了,单单靠一个服务员肯定不够了,需要多个人来负责,有的人专门负责订单相关的事情:
/orders
{
"addOrder": {
"orderName": "hamburger"
}
}
{
"deleteOrder": {
"orderId": "123456"
}
}
有的人专门负责钱包相关的事情:
/cards
{
"queryBalance": {
"cardId": "886333"
}
}
Level 2 - 打上标签
服务的流程还可以继续优化,因为负责订单的人每次都需要去看是新增订单还是删除订单,很不方便,于是我们规定:
- 所有新增资源的请求,都在请求上面写上大大的
POST
,表示这是一笔新增资源的请求, - 其他种类的请求,比如查询类的,用
GET
表示, - 删除类的,用
DELETE
表示, - 修改类的,分为2种:
1.如果这个修改,无论发送多少次,最后一次修改后的资源,总是和第一次修改后的一样,我们使用PUT
,
2.如果这个修改,每次修改都会让这个资源和前一次的不一样,我们使用PATCH
或者POST
于是,上面变成了这样:
POST /orders
{
"orderName": "hamburger"
}
GET /cards/886333
DELETE /orders/123456
level 3 - 完美服务
忽然有一天,一个客户抱怨说,他下了单,不知道去哪里取消,一个服务员回复说,你不会看我们的宣传单吗?上面写着:DELETE /orders/{orderId}
,
顾客反问道,谁会去看那个啊,服务员不服,又说道,你瞎吗... ,然后,两人就打了起来。
有了上面的教训,我们可以继续优化,客户下了单之后,不仅给他们返回订单的编号,还给顾客返回所有可以对这个订单做的操作,于是变成了下面这样:
//input
POST /orders
{
"orderName": "hamburger"
}
//output
{
"orderId": "123456",
"link": {
"rel": "cancel",
"url": "/order/123456"
}
}
这次返回时多了一项link
信息,里面包含了一个rel
属性和url
属性,rel
是relationship
的意思,这里的关系是cancel
,url
则告诉你如何执行这个cancel
操作,接着你就可以这样子来取消订单啦,
DELETE /orders/123456
上面讲的Level0
~ Level3
,来自Leonard Richardson
提出的Richardson Maturity Model
:
二、typescript
原始数据类型
定义变量时,使用冒号在后面加上它的类型。
let isDone: boolean = false;
let decLiteral: number = 6;
let myName: string = 'Tom';
//声明一个void类型的变量没有什么用,因为你只能将它赋值为`undefined`和`null`
let unusable: void = undefined;
let u: undefined = undefined;
let n: null = null;
任意值
1.在任意值上访问任何属性都是允许的
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
2.变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型
let something;
something = 'seven';
something = 7;
something.setName('Tom');
类型推论
如果没有明确的指定类型,那么TypeScript
会依照类型推论(Type Inference
)的规则推断出一个类型。
let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
//等价于
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
联合类型
联合类型(Union Types
)表示取值可以为多种类型中的一种。
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
当TypeScript
不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
对象的类型——接口
在TypeScript
中,我们使用接口(Interfaces
)来定义对象的类型。
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25
};
接口一般首字母大写。有的编程语言中会建议接口的名称加上
I
前缀。
注意
- 赋值的时候,变量的形状必须和接口的形状保持一致,多一些或少一些都不行
- 可选属性
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom'
};
- 任意属性
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
- 只读属性
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
id: 89757,
name: 'Tom',
gender: 'male'
};
tom.id = 9527;
// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
数组的类型
1.「类型 + 方括号」表示法
let fibonacci: number[] = [1, '1', 2, 3, 5];
// Type 'string' is not assignable to type 'number'.
2.数组泛型
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
3.用接口表示数组
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
4.类数组
function sum() {
let args: number[] = arguments;
}
// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}
事实上常用的类数组都有自己的接口定义,如IArguments
, NodeList
, HTMLCollection
等
function sum() {
let args: IArguments = arguments;
}
函数的类型
参数定义自己的类型,冒号后面定义返回值
function sum(x: number, y: number): number {
return x + y;
}
三、观察者模式与订阅发布模式
很久以前,我去网上寻找vue响应式原理时,第一次知道了Object.defineProperty
和这两种设计模式,那会我不懂什么是设计模式,最近在看一些关于设计模式的知识,
重新看见这两个名词,感觉自己略微懂了,防止忘记,记录一下。
它们其实非常相似,区别在于观察者模式是发布者直接接触订阅者,而发布订阅模式,发布者和订阅者通过中间平台间接接触。
生活中的观察者模式
周一刚上班,前端开发李雷就被产品经理韩梅梅拉进了一个钉钉群——“员工管理系统需求第99次变更群”。这个群里不仅有李雷,还有后端开发 A,测试同学 B。三位技术同学看到这简单直白的群名便立刻做好了接受变更的准备、打算撸起袖子开始干了。此时韩梅梅却说:“别急,这个需求有问题,我需要和业务方再确认一下,大家先各忙各的吧”。这种情况下三位技术同学不必立刻投入工作,但他们都已经做好了本周需要做一个新需求的准备,时刻等待着产品经理的号召。
一天过去了,两天过去了。周三下午,韩梅梅终于和业务方确认了所有的需求细节,于是在“员工管理系统需求第99次变更群”里大吼一声:“需求文档来了!”,随后甩出了"需求文档.zip"文件,同时@所有人。三位技术同学听到熟悉的“有人@我”提示音,立刻点开群进行群消息和群文件查收,随后根据群消息和群文件提供的需求信息,投入到了各自的开发里。上述这个过程,就是一个典型的观察者模式。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
在我们上文这个钉钉群里,一个需求信息对象对应了多个观察者(技术同学),当需求信息对象的状态发生变化(从无到有)时,产品经理通知了群里的所有同学,以便这些同学接收信息进而开展工作:角色划分 --> 状态变化 --> 发布者通知到订阅者,这就是观察者模式的“套路”。
在实践中理解定义
结合我们上面的分析,现在大家知道,在观察者模式里,至少应该有两个关键角色是一定要出现的——发布者和订阅者。用面向对象的方式表达的话,那就是要有两个类。
首先我们来看这个代表发布者的类,我们给它起名叫Publisher
。这个类应该具备哪些“基本技能”呢?大家回忆一下上文中的韩梅梅,韩梅梅的基本操作是什么?首先是拉群(增加订阅者),然后是@所有人(通知订阅者),这俩是最明显的了。此外作为群主&产品经理,韩梅梅还具有踢走项目组成员(移除订阅者)的能力。OK,产品经理发布者类的三个基本能力齐了,下面我们开始写代码:
// 定义发布者类
class Publisher {
constructor() {
this.observers = []
console.log('Publisher created')
}
// 增加订阅者
add(observer) {
console.log('Publisher.add invoked')
this.observers.push(observer)
}
// 移除订阅者
remove(observer) {
console.log('Publisher.remove invoked')
this.observers.forEach((item, i) => {
if (item === observer) {
this.observers.splice(i, 1)
}
})
}
// 通知所有订阅者
notify() {
console.log('Publisher.notify invoked')
this.observers.forEach((observer) => {
observer.update(this)
})
}
}
ok,搞定了发布者,我们一起来想想订阅者能干啥——其实订阅者的能力非常简单,作为被动的一方,它的行为只有两个——被通知、去执行(本质上是接受发布者的调用,这步我们在Publisher中已经做掉了)。既然我们在Publisher中做的是方法调用,那么我们在订阅者类里要做的就是方法的定义:
// 定义订阅者类
class Observer {
constructor() {
console.log('Observer created')
}
update() {
console.log('Observer.update invoked')
}
}
最后,一段合并的代码:
观察者模式
class Publisher {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
this.observers.forEach((item, index) => {
if (item === observer) this.observers.splice(index, 1);
});
}
notifyObersers() {
this.observers.forEach((item) => {
item.update(this);
});
}
}
class Observer {
update(publisher) {
// 更新操作
}
}
// 在上面两个类中扩展出具体的类
class PMPublisher extends Publisher {
constructor() {
super();
this.state = null;
this.observers = [];
}
getState() {
return this.state;
}
setState(state) {
this.state = state;
this.notifyObersers();
}
}
class PMObserver extends Observer {
constructor(id) {
super();
this.id = id;
this.state = null;
}
update(publisher) {
this.state = publisher.getState();
this.work();
}
work() {
console.log(`员工 ${this.id} 接收状态 ${this.state},开始干活啦~`);
}
}
// 测试一下
const xiaoqiang = new PMPublisher(),
xiaoting = new PMObserver("#01"),
xiaoyong = new PMObserver("#02"),
xiaolong = new PMObserver("#03");
xiaoqiang.addObserver(xiaoting);
xiaoqiang.addObserver(xiaoyong);
xiaoqiang.addObserver(xiaolong);
xiaoqiang.setState("开会");
发布——订阅模式
class EventEmitter {
constructor() {
this.handlers = {};
}
// 注册事件监听器
on(event, callback) {
if (!this.handlers[event]) this.handlers[event] = [];
this.handlers[event].push(callback);
}
// 移除某个事件回调队列里的指定回调函数
off(event, callback) {
const callbacks = this.handlers[event],
index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
// 触发目标事件
emit(event, ...params) {
if (this.handlers[event]) {
this.handlers[event].forEach((callback) => {
callback(...params);
});
}
}
// 为事件注册单次监听器
once(event, callback) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...params) => {
callback(...params);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
四、简化拷贝写法:给全局JSON对象挂载一个deep
五、解构的属性的key是动态时