laibao101

laibao101 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

laibao101 关注了用户 · 11月12日

槟t1BGv @t1bgv

关注 2

laibao101 报名了系列讲座 · 11月11日

前端性能优化设计12问

你好,我是来自腾讯的前端开发工程师九思,擅长前端监控、工程化相关技术。 本课程主要面向初级/中级前端,前端性能优化几乎是每次面试的必问题目,也跟我们日常的开发息息相关,那么如何快速高效的掌握这些呢? 我将在此图文课程中与你分享这些知识&经验。

laibao101 报名了系列讲座 · 11月2日

前端面试攻略:肉老师的面试题详解

> 我的财宝吗?想要就给你们好了……去找吧!我把这世界上的一切都放在那里了…… > ——哥尔·D·罗杰 -------- > 啊啊啊啊,竟然把50张半价早鸟票卖光了,简直是人生巅峰啊! > 感谢大家捧场,为表达谢意,我会在本次讲座最后把上次买没送掉的几本书以抽奖的形式送给大家。 -------- 九月已至,金九银十的就业季再临,想必大家都已经心痒难耐,摩拳擦掌准备向更好的工作发起挑战。那么,面试,一定是最重要的挑战。 一方面,我相信面试要靠日常积累,同时运气成分也很大,很依赖面试官当时的主观感觉。这方面我帮不上什么忙。 另一方面,我也知道很多同学日常工作压力很大,重压之下动作难免变形,心生迷惘,不知道该向什么方向发展,只是被业务驱使着干这干那。这方面,我觉得应该可以有所作为。 所以我决定把自己积累的面试题详细地介绍给所有来听课的同学。从设置这道题的目的,考察的方向,希望听到的答案,答出多少大约是什么评价等等都来个彻底的公开。相信大家听后,可以更加明确日常学习的方向。 我的面试题由日常积累而来,包含HTML、CSS、布局、JS、框架、优化、开发习惯等等方面。可深可浅,根据招聘需求来实时调整。我用这套面试题面试了大约200人,有现场也有电话,对它的覆盖面基本满意。事后基本也验证得到验证。 为了照顾初段同学,这次还会分享我对面试的理解,简单的博弈论如零和博弈多和博弈等,方便大家在技术之外提升自己。 也欢迎其他高人名士前来指点。我也考虑从报名同学中抽1~2名现场模拟面试过程。 ## 面向观众 1. 前端开发者 2. 希望通过模拟面试提升面试技能 3. 希望通过面试题解析找到学习方向 ## 早鸟计划 1. 上线后,开始一周前5折 2. 开始前一周75折 3. 当天85折 4. 之后恢复全价

laibao101 报名了系列讲座 · 11月2日

前端进阶系列视频课程合集

前端入门到进阶系列教程,让您快速成长,找到学习的方向,从前端小白进阶为高级前端、前端专家。 备注:早期录制的课程,很多做的不好的地方,欢迎您提出宝贵建议,我们后期会优化。 课程内容 ---- 前端进阶组合课程包含: ####《Virtual DOM原理解析与编码实现》 - 学习基础Virtual DOM原理和实现,是现有前端框架比如React和Vue等基础原理,用到了许多很基础的JavaScript函数,作为前端基础课程。 ####《前端JavaScript函数式编程学以致用》 - 函数式编程是必学课,特别是了解下高阶函数,高阶组件。了解纯函数,副作用,科里化等等,对编程能力是很大的提高。也是很多高级JavaScript特性的实现原理基础知识。 ####《前端DSL与AST深入浅出》 - DSL和AST侧重于对编程语言有整体的理解和把握,了解前端如何实现代码编译,脚本转换的。特别是了解Babel和Webpack的原理和机制,可以用来做一些提效的事情。很多时候,提供另一种解决方案和视角。 ####《前端性能监控与用户体验优化实战》 - 用户体验和性能优化是前端开发必须要特别敏感的事情。关注用户体验,是业务的基础要求,也是做好的优秀的产品必备技能。性能优化和性能监控能及时帮助开发者发现问题,及时解决问题,减少不必要的损失。

laibao101 报名了系列讲座 · 11月2日

前端高级进阶知识点入门

前端知识点很多,很细碎。一般同学都是死记硬背一些知识点。机灵的同学会背一些案例,更聪明的同学会背一下原理,理解下大概。奈何时光催人老,再好的记忆也会有忘记的时候,况且人生的不同阶段所侧重的点也不一样。 所以本课程从面试考察的知识点入手,梳理前端知识点脉络,精讲各个点的长问问题和设计原理,让你从死记硬背转化为理解,实现前端能力增长。再也不需要死记硬背,该忘记就忘记吧,有事没事想一想,捋一捋就行。 - 课件脑图下载:[https://github.com/chalecao/fed-regain](https://github.com/chalecao/fed-regain) - 面试准备:[分析千份面试记录教你如何面试](https://segmentfault.com/ls/1650000020889956) 这里知识点很多,不太可能一一展开。我想在职场上的大家要学会自我学习,这是基本的能力。前端的知识点都是很容易get的,也可能是因为容易,所以很少有同学深入下去。课程中列的每一个点,希望大家可以自行学习总结,你一定会有所收获。 ### 第一章 HTML - 相识 1.1 前端增长,业界发展,盘他? 1.2 学习目标,人生就是起起落落落? 1.3 HTML 咋解析的呢?DOM 构建 1.4 CSSOM 如何构建?会阻塞吗 1.5 RenderTree 上来秀一波 1.6 Layout 布局引擎,新交规解析 1.7 牛逼的 render 进程合成层,拯救世界 1.8 HTML 加载阻塞?咋不上天呢 1.9 页面渲染会堵车吗?FM93 交通之声 ### 第二章 CSS - 相知 2.1 啥是 Containing Block?有鸟用 2.2 要 BFC?要啥自行车 2.3 到底是怎么定位?挖坑吗 2.4 咋布局?设套吗?flex 兄弟上车 2.5 CSS 优先级,优生优育 2.6 CSS 预处理原理 2.7 合成器和非合成器动画,爆 GPU 菊花 2.8 大哥,你的动画卡顿了,快逃 2.9 基线和行高的坑 ### 第三章 JavaScript - 相爱 3.1 浏览器引擎与 webkit 3.2 JavaScript 虚拟机运行原理流程剖析 3.3 JavaScript 类型推断 3.4 JavaScript 虚拟机对象访问优化 3.5 秒懂事件循环原理 3.6 事件循环之宏任务与微任务 3.7 JavaScript 虚拟机垃圾回收 3.7 JavaScript 数据类型与内存模型 3.9 数据类型检测与深浅克隆 3.10 数据监听方法有哪些? 3.11 模块数据通信的方法 3.12 原型和原型链理解不会忘 3.13 从执行上下文理解闭包 ### 第四章 浏览器 - 相生 4.1 所谓的单线程 4.2 为什么执行超过 50ms 是 longtask? 4.3 UI 线程与 worker 线程 4.4 浏览器强制缓存与协商缓存 4.5 serviceworker 与 pagecache 与 memorycache 4.6 http 请求幂等性和 2.0 的链接复用 4.7 跨域原因风险和通信方法有哪些? 4.8 hign resolution time api 4.9 performance.timing 加载性能指标计算 4.10 xss 漏洞原理和防御 4.11 CSRF 攻击原理和防御编辑删除 4.12 JSONP 劫持和越权漏洞 ### 第五章 框架 - 相克 5.1 字符串模板原理和比较 5.2 实现 virtualDOM 三要素 5.3 virtualNode 构建方法和原理 5.4 virtualDOM 构建过程和 render 原理 5.5 Vue 框架的特点和优势 5.6 Vue 中用到的设计模式 5.7 Vue 中 initState 初始化流程 5.8 Vue 数据响应系统实现原理 5.9 Vue 的缺点有哪些 5.10 React 的 diffDOM 算法 (tree diff 和 component) 5.11 React 的 element diff 算法 5.12 React 新特性 hooks?context?错误边界? 5.13 SPA 路由实现的几种方法和原理 ### 第 6 章 编码能力 - 相辅 6.1 常见排序算法简介 6.2 贪心、分治、动态规划、回溯等算法 6.3 编码能力设计模式等思考 ## 第 7 章 Node.js- 相成 7.1 Node 事件驱动高并发的原因 7.2 Node 事件循环机制 ### 前端知识图谱 ![前端增长大宝剑](https://haomou.oss-cn-beijing.aliyuncs.com/upload/2020/10/img_5f9056f43d119.png) ![](https://haomou.oss-cn-beijing.aliyuncs.com/upload/2020/10/img_5f9057d99fc36.png)

laibao101 报名了系列讲座 · 10月17日

资深面试官告诉你如何面试

世界上没有什么是不公平的,想要的多,做的少,是不公平的!赶快行动起来吧! 关于笔试面试一些搜集的面试题和技巧,经验分享,可以看我的专栏:https://www.fed123.com/frontend-interview-question ## 课程大纲 分析了千分面试记录,涵盖初级到高级、专家级别的同学面试情况,结合应聘者简历和面试官面试记录总结得出本课程的结论。 1 面试流程与简历筛选 2 技术面试 3 Leader面试与HRBP面试 4 面试总结 本课程给出的面试方法论仅供大家参考,各位同学多总结、多思考,用工在平时,莫欺少年穷。 课程推荐: [前端增长-高级进阶知识](https://segmentfault.com/ls/1650000019681091) ## 适宜人群 校招社招求职面试找工作的同学

laibao101 报名了系列讲座 · 10月17日

带你彻底掌握 CSS 浮动

## 什么是公开课? 公开课是 SegmentFault 讲堂面向所有开发者的知识普及产品。为了提升整个行业的水平,我们会邀请**业内一线**的**富有经验**的讲师,针对开发过程中**最基础**但是**问题最多**的知识点进行权威讲解。 公开课是**完全免费**的**公开资源**,欢迎大家扩散和分享它。 ## 这堂课讲了什么? CSS 浮动是前端开发中最基础但又是最容易犯错的问题,我在工作中经常遇到有多年经验的开发者都没有完全弄明白里面的概念。虽然近年 Flex 布局开始进入大家的视野,但是浮动定位仍然占据前端布局的主流地位,而理解它也可以让你更好地理解 CSS 盒状模型等很多相关概念。 因此借助 SegmentFault 公开课这一渠道,我为大家录制了这一期课程,希望能对你理解 CSS 浮动有所帮助。以下是课程主要覆盖的三个方面 1. 浮动的基本概念 2. 浮动的摆放 3. 浮动的清除与闭合 ## 面向人群 1. 前端初学者 2. 对浮动这一概念不太清晰的前端工程师 为了方便大家理解,我会在课程中结合实例讲解,希望你也能跟着我一起动手练习。

laibao101 报名了系列讲座 · 10月17日

自顶向下学 React 源码

曾有大牛说过:“前端领域十八个月难度翻一倍。”作为前端工程师,如果将自己的定位局限在“熟练使用技术栈完成业务”,那么随着技术革新,慢慢会陷入“学不动了”的境地。这个问题如何解决呢?答案是:探索前端的边界。 在业务之外,前端还有很多富有挑战与机遇的领域,包括但不限于:全栈、移动端、工程化、可视化、框架开发。熟练完成业务并且深入前端某一领域,也是“资深前端工程师”必须掌握的能力。在这些领域中,与我们日常开发关系最密切的,便是“框架开发”了。 ### 源码为什么难学 从`机遇`可以看到,小到具体`算法`,大到`编程思想`,要了解源码运行流程,需要学习很多前置知识。 通常我们调试库的源码,会从库的入口函数开始`debug`。对于`React`来说,这个入口就是`ReactDOM.render`。 ![8IBgRQ5kS7nVycq](https://vip2.loli.net/2020/10/15/8IBgRQ5kS7nVycq.png) 打印从`ReactDOM.render`执行到`视图渲染`过程的调用栈会发现:这中间的调用流程非常复杂,甚至还有异步流程。 初学源码的同学很容易陷入在源码的汪洋大海中,从入门到放弃。 ### 源码该怎么学 如果将调用栈按功能划分,可以分为三块: ![ODs6ESVemgtX7yr](https://vip2.loli.net/2020/10/15/ODs6ESVemgtX7yr.png) 分别对应了源码架构中的三大模块:调度 → 协调 → 渲染 ![lJbEg2tAmDVhGj1](https://vip2.loli.net/2020/10/15/lJbEg2tAmDVhGj1.png) 所以,在学习具体代码前,更好的方式是先了解`React`的架构体系。**那么`React`为什么要这样架构呢?**我们作为开发者,在开发一个功能前会关注开发这个功能的目的——是为了更好的用户体验,还是为了商业化? 同样,`React`团队作为框架的开发者,在开发`React`前也会关注开发目的。这个目的,就是`React`的设计理念。 可以从[官网React哲学](https://zh-hans.reactjs.org/docs/thinking-in-react.html)了解到设计理念: > React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。 可见,`React`的设计初衷就是为了践行`快速响应`的理念。 **Q:那么,是什么在制约页面的`快速响应`?** A:`CPU`的瓶颈与`IO`的瓶颈。 **Q:如何解决瓶颈?** A:以`并发`的模式更新视图。 Q:**至此,我们也解开了一大疑惑:为什么`React`要推出`Fiber`架构?** A:`React15`的架构无法实现`并发更新`。 可以看到,我们并没有从如 ReactDOM.render、this.setState 或 Hooks 等这些日常开发耳熟能详的 API 入手,而是从“理念”这样比较高的抽象层次开始学习,这是有意为之的。 ### 授课思路 为了帮你从本质上学懂`React`源码,本课程的讲解将从理念到架构,从架构到实现,从实现到具体代码。 这是一个自顶向下、抽象程度递减,符合大脑认知的过程。 基于此,本课程划分为三大篇章: * 理念篇:带你了解`React`的设计理念及演进史 * 架构篇:讲解“设计理念“如何具体落实到代码架构上 * 实现篇:讲解主要 API如何在“架构体系”中实现 ### 课程特色 * **基于最新源码,**本课程基于最新 React17.0.0-alpha 设计,全网稀缺。React17 发布的 Concurrent Mode 是React 前几年的努力方向,也是未来几年的发展方向。市面上目前还没有 React17 源码级别的课程。 * **辅助资料完备**,课程配备了配套电子书《React技术揭秘》、丰富的在线Demo、实战练习。 * **“自顶向下”的授课方式,**符合人类认知的过程。如果说别的源码分析课程是“高级”难度,那么本课程只有“中级”难度。原因在于课程使用“自顶向下”的授课方式,更符合人类认知的过程。 ### 课程大纲 #### **理念篇:React的设计理念及架构体系** * React的设计理念 * 为了满足设计理念,这些年React架构的演进历程 * 新的React架构是如何设计的 #### **架构篇:React架构的工作流程** * React架构render阶段的完整流程 * React架构commit阶段的完整流程 #### **实现篇:具体功能的源码实现** * Diff算法的实现 * 状态更新相关API的实现 * Hooks的实现 * Concurrent Mode的实现 ### 讲师介绍 卡颂,奇舞团前端工程师,React Contributor,《React技术揭秘》作者。在制作本课程前,已经通过电子书的形式帮助非常多小伙伴学习React源码,广受好评。并形成了上千人的源码学习社群,技术氛围浓郁。

laibao101 关注了专栏 · 10月2日

Jokcy的前端小站

看到,想到,做过的一些内容总结

关注 52

laibao101 赞了文章 · 8月13日

Typescript 设计模式之工厂方法

在现实生活中,工厂是负责生产产品的,比如牛奶、面包或礼物等,这些产品满足了我们日常的生理需求。此外,在日常生活中,我们也离不开大大小小的系统,这些系统是由不同的组件对象构成。

而作为一名 Web 软件开发工程师,在软件系统的设计与开发过程中,我们可以利用设计模式来提高代码的可重用性、可扩展性和可维护性。在众多设计模式当中,有一种被称为工厂模式的设计模式,它提供了创建对象的最佳方式。

工厂模式可以分为三类:

  • 简单工厂模式(Simple Factory Pattern)
  • 工厂方法模式(Factory Method Pattern)
  • 抽象工厂模式(Abstract Factory Pattern)

本文阿宝哥将介绍简单工厂模式与工厂方法模式,而抽象工厂模式将在后续的文章中介绍,下面我们先来介绍简单工厂模式。

一、简单工厂模式

1.1 简单工厂模式简介

简单工厂模式又叫 静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。简单工厂让使用者不用知道具体的参数就可以创建出所需的 ”产品“ 类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。

相信对于刚接触简单工厂模式的小伙伴来说,看到以上的描述可能会觉得有点抽象。这里为了让小伙伴更好地理解简单工厂模式,阿宝哥以用户买车为例,来介绍一下 BMW 工厂如何使用简单工厂模式来生产🚗。

在上图中,阿宝哥模拟了用户购车的流程,pingan 和 qhw 分别向 BMW 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。接下来,阿宝哥将介绍如何使用简单工厂来描述 BMW 工厂生产指定型号车子的过程。

1.2 简单工厂模式实战

  1. 定义 BMW 抽象类
abstract class BMW {
  abstract run(): void;
}
  1. 创建 BMW730 类(BMW 730 Model)
class BMW730 extends BMW {
  run(): void {
    console.log("BMW730 发动咯");
  }
}
  1. 创建 BMW840 类(BMW 840 Model)
class BMW840 extends BMW {
  run(): void {
    console.log("BMW840 发动咯");
  }
}
  1. 创建 BMWFactory 工厂类
class BMWFactory {
  public static produceBMW(model: "730" | "840"): BMW {
    if (model === "730") {
      return new BMW730();
    } else {
      return new BMW840();
    }
  }
}
  1. 生产并发动 BMW730 和 BMW840
const bmw730 = BMWFactory.produceBMW("730");
const bmw840 = BMWFactory.produceBMW("840");

bmw730.run();
bmw840.run();

以上代码运行后的输出结果为:

BMW730 发动咯 
BMW840 发动咯 

通过观察以上的输出结果,我们可以知道我们的 BMWFactory 已经可以正常工作了。在 BMWFactory 类中,阿宝哥定义了一个 produceBMW() 方法,该方法会根据传入的模型参数来创建不同型号的车子。

看完简单工厂模式实战的示例,你是不是觉得简单工厂模式还是挺好理解的。那么什么场景下使用简单工厂模式呢?要回答这个问题我们需要来了解一下简单工厂的优缺点。

1.3 简单工厂模式优缺点

1.3.1 优点
  • 将创建实例与使用实例的任务分开,使用者不必关心对象是如何创建的,实现了系统的解耦;
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。
1.3.2 缺点
  • 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,也有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

了解完简单工厂的优缺点,我们来看一下它的应用场景。

1.4 简单工厂模式应用场景

在满足以下条件下可以考虑使用简单工厂模式:

  • 工厂类负责创建的对象比较少:由于创建的对象比较少,不会造成工厂方法中业务逻辑过于复杂。
  • 客户端只需知道传入工厂类静态方法的参数,而不需要关心创建对象的细节。

介绍完简单工厂模式,接下来我们来介绍本文的主角 ”工厂方法模式“

二、工厂方法模式

2.1 工厂方法简介

工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫多态工厂(Polymorphic Factory)模式,它属于类创建型模式。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象, 这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

在上图中,阿宝哥模拟了用户购车的流程,pingan 和 qhw 分别向 BMW 730 和 BMW 840 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。接下来,阿宝哥来介绍如何使用工厂方法来描述 BMW 工厂生产指定型号车子的过程。

2.2 工厂方法实战

  1. 定义 BMW 抽象类
abstract class BMW {
  abstract run(): void;
}
  1. 创建 BMW730 类(BMW 730 Model)
class BMW730 extends BMW {
  run(): void {
    console.log("BMW730 发动咯");
  }
}
  1. 创建 BMW840 类(BMW 840 Model)
class BMW840 extends BMW {
  run(): void {
    console.log("BMW840 发动咯");
  }
}
  1. 定义 BMWFactory 接口
interface BMWFactory {
  produceBMW(): BMW;
}
  1. 创建 BMW730Factory 类
class BMW730Factory implements BMWFactory {
  produceBMW(): BMW {
    return new BMW730();
  }
}
  1. 创建 BMW840Factory 类
class BMW840Factory implements BMWFactory {
  produceBMW(): BMW {
    return new BMW840();
  }
}
  1. 生产并发动 BMW730 和 BMW840
const bmw730Factory = new BMW730Factory();
const bmw840Factory = new BMW840Factory();

const bmw730 = bmw730Factory.produceBMW();
const bmw840 = bmw840Factory.produceBMW();

bmw730.run();
bmw840.run();

通过观察以上的输出结果,我们可以知道我们的 BMW730Factory 和 BMW840Factory 工厂已经可以正常工作了。相比前面的简单工厂模式,工厂方法模式通过创建不同的工厂来生产不同的产品。下面我们来看一下工厂方法有哪些优缺点。

2.3 工厂方法优缺点

2.3.1 优点
  • 在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,更加符合 “开闭原则”。而简单工厂模式需要修改工厂类的判断逻辑。
  • 符合单一职责的原则,即每个具体工厂类只负责创建对应的产品。而简单工厂模式中的工厂类存在一定的逻辑判断。
  • 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
2.3.2 缺点
  • 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  • 一个具体工厂只能创建一种具体产品。

最后我们来简单介绍一下工厂方法的应用场景。

2.4 工厂方法应用场景

  • 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
  • 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

三、参考资源

四、推荐阅读

查看原文

赞 18 收藏 9 评论 0

laibao101 关注了用户 · 2018-09-26

幽_月 @you_yue

关注 58

laibao101 收藏了文章 · 2018-03-12

Koa原理学习路径与设计哲学

Koa原理学习路径与设计哲学

本文基于Koa@2.5.0

Koa简介(废话篇)

Koa是基于Node.jsHTTP框架,由Express原班人马打造。是下一代的HTTP框架,更简洁,更高效。

我们来看一下下载量(2018.3.4)

Koa:471,451 downloads in the last month
Express:18,471,701 downloads in the last month

说好的Koa是下一代框架呢,为什么下载量差别有这么大呢,Express一定会说:你大爷还是你大爷!

确实,好多知名项目还是依赖Express的,比如webpack的dev-server就是使用的Express,所以还是看场景啦,如果你喜欢DIY,喜欢绝对的控制一个框架,那么这个框架就应该什么功能都不提供,只提供一个基础的运行环境,所有的功能由开发者自己实现。

正是由于Koa的高性能和简洁,好多知名项目都在基于Koa,比如阿里的eggjs,360奇舞团的thinkjs

所以,虽然从使用范围上来讲,Express对于Koa你大爷还是你大爷!,但是如果Express很好,为什么还要再造一个Koa呢?接下来我们来了解下Koa到底带给我们了什么,Koa到底做了什么。

如何着手分析Koa

先来看两段demo。

下面是Node官方给的一个HTTP的示例。

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

下面是最简单的一个Koa的官方实例。

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Koa是一个基于Node的框架,那么底层一定也是用了一些Node的API。

jQuery很好用,但是jQuery也是基于DOM,逃不过也会用element.appendChild这样的基础API。Koa也是一样,也是用一些Node的基础API,封装成了更好用的HTTP框架。

那么我们是不是应该看看Koahttp.createServer的代码在哪里,然后顺藤摸瓜,了解整个流程。

Koa核心流程分析

Koa的源码有四个文件

  • application.js // 核心逻辑
  • context.js // 上下文,每次请求都会生成一个
  • request.js // 对原生HTTP的req对象进行包装
  • response.js // 对原生HTTP的res对象进行包装

我们主要关心application.js中的内容,直接搜索http.createServer,会搜到

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

刚好和Koa中的这行代码app.listen(3000);关联起来了。

找到源头,现在我们就可以梳理清楚主流程,大家对着源码看我写的这个流程

fn:listen
∨
fn:callback
∨
[fn:compose] // 组合中间件 会生成后面的 fnMiddleware
∨
fn:handleRequest // (@closure in callback)
∨
[fn(req, res):createContext] // 创建上下文 就是中间件中用的ctx
∨
fn(ctx, fnMiddleware):handleRequest // (@koa instance)
∨
code:fnMiddleware(ctx).then(handleResponse).catch(onerror);
∨
fn:handleResponse
∨
fn:respond
∨
code:res.end(body);

从上面可以看到最开始是listen方法,到最后HTTP的res.end方法。

listen可以理解为初始化的方法,每一个请求到来的时候,都会经过从callbackrespond的生命周期。

在每个请求的生命周期中,做了两件比较核心的事情:

  1. 将多个中间件组合
  2. 创建ctx对象

多个中间件组合后,会先后处理ctx对象,ctx对象中既包含的req,也包含了res,也就是每个中间件的对象都可以处理请求和响应。

这样,一次HTTP请求,接连经过各个中间件的处理,再到返回给客户端,就完成了一次完美的请求。

Koa中的ctx

app.use(async ctx => {
  ctx.body = 'Hello World';
});

上面的代码是一个最简单的中间件,每个中间件的第一个参数都是ctx,下面我们说一下这个ctx是什么。

创建ctx的代码:

  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }

直接上代码,Koa每次请求都会创建这样一个ctx对象,以提供给每个中间件使用。

参数的req, res是Node原生的对象。

下面解释下这三个的含义:

  • context:Koa封装的带有一些和请求与相应相关的方法和属性
  • request:Koa封装的req对象,比如提了供原生没有的host属性。
  • response:Koa封装的res对象,对返回的bodyhook了getter和setter。

其中有几行一堆 xx = xx = xx,这样的代码。

是为了让ctx、request、response,能够互相引用。

举个例子,在中间件里会有这样的等式

ctx.request.ctx === ctx
ctx.response.ctx === ctx

ctx.request.app === ctx.app
ctx.response.app === ctx.app

ctx.req === ctx.response.req
// ...

为什么会有这么奇怪的写法?其实只是为了互相调用方便而已,其实最常用的就是ctx。

打开context.js,会发现里面写了一堆的delegate

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

是为了把大多数的requestresponse中的属性也挂在ctx下,我们为了拿到请求的路径需要ctx.request.path,但是由于代理过path这个属性,ctx.path也是可以的,即ctx.path === ctx.request.path

ctx模块大概就是这样,没有讲的特别细,这块是重点不是难点,大家有兴趣自己看看源码很方便。

一个小tip: 有时候我也会把context.js中最下面的那些delegate当成文档使用,会比直接看文档快一点。

Koa中间件机制

中间件函数的参数解释

  • ctx:上面讲过的在请求进来的时候会创建一个给中间件处理请求和响应的对象,比如读取请求头和设置响应头。
  • next:暂时可以理解为是下一个中间件,实际上是被包装过的下一个中间件。

一个小栗子

我们来看这样的代码:

// 第一个中间件
app.use(async(ctx, next) => {
  console.log('m1.1', ctx.path);
  ctx.body = 'Koa m1';
  ctx.set('m1', 'm1');
  next();
  console.log('m1.2', ctx.path);
});

// 第二个中间件
app.use(async(ctx, next) => {
  console.log('m2.1', ctx.path);
  ctx.body = 'Koa m2';
  ctx.set('m2', 'm2');
  next();
  debugger
  console.log('m2.2', ctx.path);
});

// 第三个中间件
app.use(async(ctx, next) => {
  console.log('m3.1', ctx.path);
  ctx.body = 'Koa m3';
  ctx.set('m3', 'm3');
  next();
  console.log('m3.2', ctx.path);
});

会输出什么呢?来看下面的输出:

m1.1 /
m2.1 /
m3.1 /
m3.2 /
m2.2 /
m1.2 /

来解释一下上面输出的现象,由于将next理解为是下一个中间件,在第一个中间件执行next的时候,第一个中间件就将执行权限给了第二个中间件,所以m1.1后输出的是m2.1,在之后是m3.1

那么为什么m3.1后面输出的是m3.2呢?第三个中间件之后已经没有中间件了,那么第三个中间件里的next又是什么?

我先偷偷告诉你,最后一个中间件的next是一个立刻resolve的Promise,即return Promise.resolve(),一会再告诉你这是为什么。

所以第三个中间件(即最后一个中间件)可以理解成是这样子的:

app.use(async (ctx, next) => {
    console.log('m3.1', ctx.path);
    ctx.body = 'Koa m3';
    ctx.set('m3', 'm3');
    new Promise.resolve(); // 原来是next
    console.log('m3.2', ctx.path);
});

从代码上看,m3.1后面就会输出m3.2

那为什么m3.2之后又会输出m2.2呢?,我们看下面的代码。

let f1 = () => {
  console.log(1.1);
  f2();
  console.log(1.2);
}

let f2 = () => {
  console.log(2.1);
  f3();
  console.log(2.2);
}

let f3 = () => {
  console.log(3.1);
  Promise.resolve();
  console.log(3.2);
}

f1();

/*
  outpout
  1.1
  2.1
  3.1
  3.2
  2.2
  1.2
*/

这段代码就是纯函数调用而已,从这段代码是不是发现,和上面一毛一样,对一毛一样,如果将next理解成是下一个中间件的意思,就是这样。

中间件组合的过程分析

用户使用中间件就是用app.use这个API,我们看看做了什么:

  // 精简后去掉非核心逻辑的代码
  use(fn) {
    this.middleware.push(fn);
    return this;
  }

可以看到,当我们应用中间件的时候,只是把中间件放到一个数组中,然后返回this,返回this是为了能够实现链式调用。

那么Koa对这个数组做了什么呢?看一下核心代码

const fn = compose(this.middleware); // @callback line1
// fn 即 fnMiddleware 
return fnMiddleware(ctx).then(handleResponse).catch(onerror); // @handleRequest line_last

可以看到用compose处理了middleware数组,得到函数fnMiddleware,然后在handleRequest返回的时候运行fnMiddleware,可以看到fnMiddleware是一个Promiseresolve的时候就会处理完请求,能猜到compose将多个中间件组合成了一个返回Promise的函数,这就是奇妙之处,接下来我们看看吧。

精简后的compose源码

// 精简后去掉非核心逻辑的代码
00    function compose (middleware) {
01      return function (context, next) { // fnMiddleware
02        return dispatch(0)
03        function dispatch (i) {
04          let fn = middleware[i] // app.use的middleware
05          if (!fn) return Promise.resolve()
06          return fn(context, function next () {
07            return dispatch(i + 1)
08          })
09        }
10      }
11    }

精简后代码只有十几行,但是我认为这是Koa最难理解、最核心、最优雅、最奇妙的地方。

看着各种function,各种return有点晕是吧,不慌,不慌啊,一行一行来。

compose返回了一个匿名函数,这个匿名函数就是fnMiddleware

刚才我们是有三个中间件,你们准备好啦,请求已经过来啦!

当请求过来的时候,fnMiddleware就运行了,即运行了componse返回的匿名函数,同时就会运行返回的dispatch(0),那我们看看dispatch(0)做了什么,仔细一看其实就是

// dispatch(0)的时候,fn即middleware[0]
return middleware[0](context, function next (){
  return dispatch(1);
})

// 上面的context和next即中间件的两个参数
// 第一个中间件
app.use(async(ctx, next) => {
  console.log('m1.1', ctx.path);
  ctx.body = 'Koa m1';
  ctx.set('m1', 'm1');
  next(); // 这个next就是dispatch(1)
  console.log('m1.2', ctx.path);
});

同理,在第二个中间件里面的next,就是dispatch(2),也就是用上面的方法被包裹一层的第三个中间件。

  • 现在来看第三个中间件里面的next是什么?

可以看到精简过的compose05行有个判断,如果fn不存在,会返回Promise.resolve(),第三个中间件的nextdispatch(3),而一共就有三个中间件,所以middleware[3]是undefined,触发了分支判断条件,就返回了Promise.resolve()

再来复盘一下:

  1. 请求到来的事情,运行fnMiddleware(),即会运行dispatch(0)调起第一个中间件。
  2. 第一个中间件的nextdispatch(1),运行next的时候就调起第二个中间件
  3. 第二个中间件的nextdispatch(2),运行next的时候就调起第三个中间件
  4. 第三个中间件的nextdispatch(3),运行next的时候就调起Promise.resolve()。可以把Promise.resolve()理解成一个空的什么都没有干的中间件。

到此,大概知道了多个中间件是如何被compose成一个大中间件的了吧。

中间件的类型

koa2中,支持三种类型的中间件:

  • common function:普通的函数,需要返回一个promise
  • generator function:需要被co包裹一下,就会返回一个promise
  • async function:直接使用,会直接返回promise

可以看到,无论哪种类型的中间件,只要返回一个promise就好了,因为这行关键代码return fnMiddleware(ctx).then(handleResponse).catch(onerror);,可以看到KoafnMiddleware的返回值认为是promise。如果传入的中间件运行后没有返回promise,那么会导致报错。

结语

Koa的原理就解析到这里啦,欢迎交流讨论。
为了更好地让大家学习Koa,我写了一个mini版本的Koa,大家可以看一下 https://github.com/geeknull/t...

查看原文