0%

Vue.js 是一个以数据驱动为设计原理的框架。

所谓数据驱动,是指视图是由数据驱动生成的,比起视图如何生成,开发者更关注数据模型和数据流转。

接下来,我们会从源码角度来分析 Vue 是如何实现的,分析过程会以主线代码为主,重要的分支逻辑会放在之后单独分析。

先来看一个熟悉的 demo

熟悉的 demo

大家对这简单的案例应该都不会陌生,在我还是萌新的那会常看这个demo

demo的代码很简单,功能是点击数字然后累增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
<script src="/path/to/dist/vue.min.js"></script>
</head>
<body>
<div id="app">
<div @click="num++">
{{ num }}
</div>
</div>
<script>
var vm1 = new Vue({
el: '#app',
data: {
num: 1,
}
})
</script>
</body>
</html>

现在回想起来,这一切似乎就从这个 new Vue() 开始了:)

带着些许怀旧的心情,我们从 new vue() 开始一探究竟。

new Vue() 做了什么

new Vue() 本质上是创建了一个 Vue 的实例对象,那么 Vue 是怎么定义的呢

Vue 的定义

Vue本质是一个构造函数,只能通过 new 操作符去创建一个 Vue 的实例

1
const app = new Vue({ /* options */ });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/core/instance/index.js

// vue 构造函数
function Vue (options) {
// 初始化的入口
this._init(options)
}

// 下面的都是在 Vue.prototype 上挂载接来下要用到函数
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

Vue 本身很简单,就只做了一件事,就是调用初始化方法 this._init,并发参数传入

Vue 实例的函数并没直接写在 Vue 构造函数内,而是通过各种 mixin 挂载到了 Vue.prototype

其实这些函数都可以直接在当前文件中书写,但是拆开到不同模块分别挂载可以降低当前文件的耦合度,更利于代码的维护

initMixin 初始化

initMixin 用于挂载初始化相关的函数到 Vue.prototype

_init 函数则是通过 initMixin 注入到 Vue 的原型链上

阅读全文 »

什么是 Content-Type

HTTP协议(RFC2616)采用了请求/响应模型。
客户端向服务器发送一个请求,请求头包含请求的方法、URI、协议版本、以及包含请求修饰符、客户信息和内容的类似于MIME的消息结构。
服务器以一个状态行作为响应,相应的内容包括消息协议的版本,成功或者错误编码加上包含服务器信息、实体元信息以 及可能的实体内容。

通常HTTP消息由一个起始行,一个或者多个头域,一个只是头域结束的空行和可选的消息体组成。
HTTP的头域包括通用头,请求头,响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成。域名是大小写无关的,域值前可以添加任何数量的空格符,头域可以被扩展为多行,在每行开始处,使用至少一个空格或制表符。

请求消息和响应消息都可以包含实体信息,实体信息一般由实体头域和实体组成。实体头域包含关于实体的原信息,实体头包括Allow、Content- Base、Content-Encoding、Content-Language、 Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、 Etag、Expires、Last-Modified、extension-header。

Content-Type是返回消息中非常重要的内容,表示后面的文档属于什么MIME类型。

Content-Type: [type]/[subtype]; parameter。例如最常见的就是text/html,它的意思是说返回的内容是文本类型,这个文本又是HTML格式的。原则上浏览器会根据Content-Type来决定如何显示返回的消息体内容。

在请求中,Content-Type 是实体头部用于指示资源的MIME类型 media type,服务端根据这个类型来做不同的处理。

在响应中,Content-Type标头告诉客户端实际返回的内容的内容类型,浏览器根据Content-Type来对文件做不同的处理。
比如对请求 google.com 返回的content-type: text/html,charset=uft8,浏览器会把文本作为html进行解析,最终渲染到页面中。
response_content-type1

响应头中的 Content-Type 类型与请求头中的 Content-Type 并没有太多区别,但是有一些content-type只能在响应头中设置。

Content-Type 说明 案例 备注
请求头中的content-type 描述请求实体对应的MIME信息 Content-Type: application/json 简单理解就是:客户端告诉服务端,我传的数据是什么类型
响应头中的content-type 描述响应实体对应的MIME信息 text/html; charset=UTF8 简单理解就是:服务端告诉客户端,我返回的数据是什么类型

MIME

MIME类型

媒体类型(通常称为 Multipurpose Internet Mail Extensions 或 MIME 类型 )是一种标准,用来表示文档、文件或字节流的性质和格式。它在IETF RFC 6838中进行了定义和标准化。

互联网号码分配机构(IANA)是负责跟踪所有官方MIME类型的官方机构,您可以在媒体类型页面中找到最新的完整列表。(在文章的最后)

阅读全文 »

静态资源加载的错误处理

图片资源加载的错误处理

当一项资源(如img或script)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。这些error事件不会向上冒泡到window,不过能被单一的window.addEventListener捕获。

图片也支持 error 事件,可以使用 addEventListener 或者 onerror 来添加。

1
2
3
4
5
6
var image = new Image();
image.addEventListener("load", (event) => { console.log("Image loaded!");
});
// 使用 onerror 函数监听错误事件
image.onerror = (event) => {console.log("Image not loaded!")};
image.src = "123"; // 不存在,资源会加载失败

图片资源加载错误1

1
2
3
4
5
6
7
var image = new Image();
image.addEventListener("load", (event) => { console.log("Image loaded!");
});
// 使用 addEventListener error 函数监听错误事件
image.addEventListener("error", (event) => {
console.log("Image not loaded!"); });
image.src = "doesnotexist.gif"; // 不存在,资源会加载失败

图片资源加载错误2

window.onerror 和 window.addListener(‘error’, () => {}) 都可以捕获到js的错误
区别是:
1.捕获到的错误参数不同
2.window.addEventListener(‘error’)可以捕获资源加载错误,但是window.onerror不能捕获到资源加载错误,window.addEventListener(‘error’)捕获到的错误,可以通过target?.src || target?.href区分是资源加载错误还是js运行时错误

script资源加载的错误处理

script资源加载与图片等其他资源加载没有太大区别

但是有需要注意的地方:
当加载自不同域的脚本中发生语法错误时,为避免信息泄露(参见bug 363897),语法错误的细节将不会报告,而代之简单的”Script error.”。在某些浏览器中,通过在script使用crossorigin属性并要求服务器发送适当的 CORS HTTP 响应头,该行为可被覆盖。一个变通方案是单独处理”Script error.”,告知错误详情仅能通过浏览器控制台查看,无法通过JavaScript访问。

阅读全文 »

前言

本文主要分为两部分,第一部分是分析redux的原理,第二部分是根据我们分析的结果来实现一个五脏六腑俱全的mini-redux

分析redux原理的过程中,会对必要但不是全部的概念都解释一遍,所以需要对 redux 有基础的认识

redux 官方教程传送门

在认识 redux 之前,我只知道 react-redux,并且认为 redux 是 react 的一部分,相信大多入门的新人也会有这样的误解

正文开始之前,先同步一下我们的认识点

  • redux 与 react 没有任何关系,redux 可以用在任何框架中,比如 React,Vue 等等;
  • redux 是一个数据状态管理器,用来实现数据的中心化管理
  • 正文不会对 react-router 做原理分析,react-router 会另开一篇做分析

Redux

redux 基础功能是数据管理和通知订阅,redux是如何实现的呢?

Store

先来看一段官方demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createStore } from 'redux'

function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + 1 }
case 'counter/decremented':
return { value: state.value - 1 }
default:
return state
}
}
let store = createStore(counterReducer, { value: 0 })

store.subscribe(() => console.log(store.getState()))

store.dispatch({ type: 'counter/incremented' })
// {value: 1}
store.dispatch({ type: 'counter/incremented' })
// {value: 2}
store.dispatch({ type: 'counter/decremented' })
// {value: 1}

redux 通过 createStore 创建一个数据中心管理器 store
createStore 可以说是数据流的入口

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
// src/createStore.ts
function createStore(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
enhancer?: StoreEnhancer<Ext, StateExt>
) {
// 对 reducer,preloader,enhancer做了很多空值校验
// ...

// 把 reducer,state,listener 都保存到闭包中,供下面的函数处理时使用
let currentReducer = reducer
let currentState = preloadedState as S
let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
let isDispatching = false

// 创建了若干函数用于操作数据流

// 获取当前的数据状态
function getState() {}

// 订阅
function subscribe() {}

// 派发事件
function dispatch() {}

// 替换 reducer
function replaceReducer() {}

// 监听 state 变化的观测器
// https://github.com/tc39/proposal-observable
function observable() {}

// 发起初始化的 action,这个是内部的action,需要在业务代码中有对应的 reducer 才会触发响应
dispatch({ type: ActionTypes.INIT } as A)

const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

// 最后返回了一个大对象,把上述创建的函数都丢了出去
return store;
}
阅读全文 »

Koa洋葱模型分析

在实现Koa的洋葱模型之前,先回顾一下洋葱模型的运作方式

洋葱模型

这张图大家应该都见过,但是只有一张图的情况下很难对内部的逻辑有深入了解

下面从简单的例子开始一起探讨洋葱模型中间件

中间件的执行机制

先来看个简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 声明一个异步函数作为中间件
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});

// 输出顺序是:
// 1
// 3
// 4
// 2

所有的请求经过一个中间件的时候都会执行两次,执行next前和执行next后的代码分为两部分执行。

中间件是怎么保存的

通过上面的案例,可以把中间件的注册关键方法锁定在 app.use

我们先看 use 方法

koa/lib/application.js
koa-application-use

简化一下可以得到

1
2
3
4
5
use(fn) {
// 保存在middleware数组中
this.middleware.push(fn);
return this;
}

app.use 中,中间件会被推入到一个 middleware 数组中保存起来。

中间件被保存起来后,在什么时候,用什么方式调用的呢?

阅读全文 »

本文主要讲 service worker 的主要基本特性以及使用方法,从实际开发的 PWA 中学习 service worker。

Service Worker 是什么

Service Worker 是一个运行在在浏览器后台,独立于网页运行的由事件驱动的脚本。
本质上是 Web Worker 的一种具体实现,通常与 Cache API 相结合使用,因为 Worker 线程执行的脚本与网页脚本相独立的缘故,
Worker 线程内不能访问到 window 以及 dom 相关的数据。同时,在 Service Worker 中处理大量数据时也不会阻塞的主线程。所以通常可以在 Service Worker 线程中拦截浏览器的请求,读取和存储请求内容,进而实现离线应用。

Service Worker 能做什么

  • 网络优先
    当客户端发起请求时,Service worker会先请求对应的资源,并把结果返回,同时在cache storage中存储一份。

    在出现网络不可访问或服务出错时可以从缓存中获取相同的资源做容灾处理。

  • 缓存优先.png
    当客户端发起请求时,Service worker会先从cache storage中读取资源,并把结果返回,同时请求一份资源,成功的话更新到缓存中。

    这样可以保持极快的响应速度,适用于更新频率不高的资源。

Service Worker 的基本架构

  • 独立的声明周期
  • 基于 Web Worker 的事件机制
阅读全文 »

预备动作

阅读本篇之前你可能需要一定的TypeScript基础,如果还没开始上手建议从这里开始

以下是传送门

  1. TypeScript Document 官方文档
  2. TypeScript HandBook 小手册
  3. TypeScript Playground 旧版练习场
  4. TypeScript Playground 新版练习场 (比上面那个高级,写作期间是beta版)

正片开始

主要介绍 TypeScript 的常用和不常用的各种技巧,包括用这些技巧创建一些高级类型函数,会附上 Playground 的链接。

关键词

用于更加细粒度的处理接口和类型别名**

  • @scope runtime 运行时
  • @scope lexical 静态分析/词法分析

类型断言 / 类型守卫 / 类型约束

可以获取,确定,约束变量类型

typeof @scope runtime

获取变量的类型,常用于一些需要确认变量的类型然后进行不同操作的场景

1
2
3
4
5
6
7
type AOrB = { s: string; } | string;
const val: AOrB = { s: 'a' };
if (typeof val === 'object') {
console.log(val.s);
} else {
console.log(val);
}

Playground Link

extends @scope lexical

类型约束,通常用在泛型传参时约束参数范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Gen {
s: string;
}

interface Others {
d: number;
}

function BaseFunc<P extends Gen>(p: P) {
console.log(p);
}

const s1: Gen = {
s: 'something',
};

const s2: Others = {
d: 1,
};

BaseFunc(s1); // good

BaseFunc(s2); // bad, error

Playground Link

instanceof @scope runtime

确认目标是是否在指定对象的原型链上

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
class Animal {
name: string;

constructor(name: string) {
this.name = name;
}
}

class Dog extends Animal {
wang: number;

constructor(name: (typeof Animal)['name'], wang: number) {
super(name);
this.wang = wang;
}

wangwang() {
console.log('wangwang');
}
}

class Cat extends Animal {
miao: number;

constructor(name: (typeof Animal)['name'], miao: number) {
super(name);
this.miao = miao;
}

miaomiao() {
console.log('miaomiao');
}
}

const dog: Dog = new Dog('dog', 1);

const cat: Cat = new Cat('cat', 1);

function doS<C extends Animal>(cls: C) {
/** bad */
cls.wangwang(); // error
cls.miaomiao(); // error
/** good */
/** 函数已经限制了 cls 的约束范围 */
if (cls instanceof Dog) {
/** 通过 instanceof 类型守卫 进一步确认类型 */
cls.wangwang();
} else if (cls instanceof Cat) {
/** 同上 */
cls.miaomiao();
}
}

Playground Link

阅读全文 »

我在之前有提到过pixiv用React重写了部分页面,导致以前的方法不能爬取那部分的页面了。
熟悉 React/Vue/Angular 开发的同学应该很清楚,这类框架开发的Web应用有个很明显的特征,web应用的原始页面的DOM只有简单body和一个id为root的节点(当然,其他人为加入的也会存在)。如果用以前的superagent配合request只能把这部分下载下来,如果pixiv使用了service-side rendering的还好,但是pixiv并没有使用(我觉得以后pixiv可能大概会用上)。

说回正题,因为pixiv的部分页面改为“动态加载”后,直接获取文档不能获取到DOM,那么有没有什么办法能够获取代码然后执行js生成DOM,再获取页面的内容呢?
仔细想想这种行为不就像是一个浏览器了吗?
其实这种情况我之前就遇到过,当时是因为有要求爬取京东和淘宝的部分页面,但是京东和淘宝的首页出了靠近顶部部分以外的都是懒加载。顺着上面所说的思路,我很快想到了以前看到过的 headless browser,然后找到了 puppeteer,中文名是木偶戏

Puppeteer 是一个提供了高集成API,通过开发者工具协议来控制无页面Chrome或者Chromium的 node 库. 它同样能用来控制正常的 Chrome 和 Chromium。
我们可以用 Puppeteer 做什么?

  • 生成一个快照或者PDF。
  • 爬取SPA(单页应用)并生成预渲染的内容 (比如: “SSR”)。
  • 自动化提交,输入,UI测试等。
  • 创建一个最新的,全自动测试的环境。在最新Chrome上使用最新的javascript和浏览器特性运行测试代码。
  • 按照时间线来跟踪捕获网站的运行,用来帮助诊断性能问题。

Bingo,这正是我在这次爬虫中要用到的,接下来将使用它爬取pixiv的React App。

源代码,源代码已经是最新的,如果想看以前的方法的可以 git checkout 到以前的仓库节点。

阅读全文 »

早在 React v16.3 发布前,就时不时能看见各路大佬对新API的分析,每次看完之后都受益匪浅 :) 。如今 React v16.3 也更新了一小段时间了,看着旧项目更新到 React v16.3后满满的 warning,我决定重新阅读新的API后对代码进行更新。(当然,可以使用官方的迁移工具,但是不亲自体验一下总觉得少了什么 :- )

React v15.x ~ React v16 -> React v16.3

这次的升级主要是几个生命周期的API改动以及加入新的 context api,生命周期的改动是为了适应 React fiber

生命周期

组件生命周期相关-官方文档

新API:static getDerivedStateFromProps()

用于替代以前的 componentWillReceiveProps()componentWillUpdate()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Comp extends React.Component {
// ...

state = {
someText: 'old text',
otherAttr: 'value',
}

static getDerivedStateFromProps(props, state) {
// 返回一个对象,对象会用于更新state
return {
someText: 'a new text value'
};
// 返回null表示不需要更新
/*
* return null;
*/
}

// ...
}

getDerivedStateFromProps被设计为了 static 方法,以后可以直接通过 componentName.getDerivedStateFromProps 调用。这个方法在组件初始化时和组件的props更新时都会被调用,方法接收了两个参数,分别是新的 props 和 旧的state

阅读全文 »

移动端开发时难免会有列表,列表最典型的的分页模式就是上滑加载更多,此外还需要刷新列表——下拉刷新数据。

最初,为了列表的上滑加载更多和下拉刷新数据的功能,我把目光放到了 mint-uiloadmore 组件,虽说这个库已经有相当长的一段时间没有优化了。

移动端开发大部分时候还会遇到顶部一排tab,页面滑动可以切换tab这种需求,是的,每天中午点开的饿了么那一种,熟悉Android的同学大概知道那个叫做 TabLayoutViewPager。但是这次的重点不是这个,而是我在使用 loadmore 的下拉加载时与我个人封装的 swipe-tab-layout 进行组合使用时体验不太理想,尤其是下拉太过容易触发,本想看看文档,查看组件是否有一个角度属性,用来设置触发下拉。不过很遗憾,并没有这种属性。

这种情况当然是只能查看源码,看看有没有能够修改的。果不其然,在查看 loadmore 组件的源码时发现了两个重要的方法:

  1. handleTouchStart
  2. handleTouchMove

故名思议,第一个方法是处理手指刚触摸到屏幕时的处理,第二个方法是处理手指在屏幕上滑动时的情况。

阅读全文 »