Termux - 使用旧手机搭建服务

Termux - 使用旧手机搭建服务

Termux是一款 Android 终端模拟器和 Linux 环境应用程序,无需 root 或设置即可直接运行。自动安装最小的基本系统 - 使用 APT 包管理器可以使用其他包

安装 termux

f-droid 下载 termux apk 文件安装

环境准备

  • pkg up 更新本地包

  • pkg 是 termux 对 apt 封装

  • termux-setup-storage 设置手机存储权限

配置 ssh

  • pkg install openssh
  • passwd 配置密码
  • ifconfig 查看本地 ip
  • termux 默认使用 8022 端口访问 ssh
  • sshd 开启服务,pkill port 关闭服务,nmap localhost -p port 查看服务状态
  • 外部访问 ssh -p 8022 server_ip, 默认使用当前用户登录
  • 保持手机息屏 ssh 不断开,在手机下拉框里点击 ACQUIRE WAKELOCK,看到wake lock held即可

安装 node.js

pkg install nodejs

装好 node 后就可以开始愉快的进行服务部署了(#^.^#)

SaaS 应用的 12 因素

SaaS 应用的 12 因素

基准代码

codebase-deploys

  • 分布式系统中的每一个组件都是一个应用
  • 每一个应用只有一份基准代码
  • 共享代码拆分为独立类库
  • 一份基准代码可以有有多个部署

依赖

  • 通过依赖清单指明所有依赖项
  • 如果使用到系统工具,例如 curl, 同样把它放到应用中

配置

  • 代码和配置分离
  • 使用配置文件
  • 推荐将配置存储于环境变量(env vars),在部署启动时注入(环境变量的粒度要足够小,且相对独立)

后端服务

attached-resources

  • 把后端服务当作附加资源
  • 按需加载

构建、发布、运行

把基准代码变成实际可用的产品需要下面三个阶段:

  • 构建阶段 是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包 依赖项,编译成二进制文件和资源文件。
  • 发布阶段 会将构建的结果和当前部署所需 配置 相结合,并能够立刻在运行环境中投入使用。
  • 运行阶段 (或者说“运行时”)是指针对选定的发布版本,在执行环境中启动一系列应用程序 进程

release

每次发布都必须对应一个唯一的 ID,一旦发布就不修改,任何变动都应该产生一个新的版本

进程

以一个或多个无状态进程运行应用

  • 必须无状态切无共享
  • 持久化数据保存在后端服务中
  • 永远不考虑缓存在内存或硬盘上的内容在未来可用
  • 不使用 “粘性 session”,会话数据应该存在 Redis 这类带有过期时间的服务中

端口绑定

  • 互联网应用通过端口绑定来提供服务 ,并监听发送至该端口的请求
  • 应用独立,可以独立启动服务
  • 需有一个路由层转发请求
  • 一个应用程序可以成为另一个应用程序的支持服务

并发

process-types

  • processes 是一等公民
  • 遵行 UNIX 进程模型
  • 为不同类型工作分配到不同应用程序
  • 应用必须能够跨越多台物理机运行
  • 应用自身不管理进程或写入 PID 文件,使用系统或三方工具进行管理

通过快速启动和正常关闭来最大限度地提高鲁棒性

  • 进程最小化启动时间,最好在几秒钟内
  • 当进程收到来自进程管理器的 SIGTERM 信号时,进程会正常关闭
    • 停止监听端口
    • 完成当前请求
    • 退出
  • 对于 worker processes, 当前工作应该回到工作队列,当前工作的锁应该被释放
    • 所有作业都是可重做的(使用事务或 idempotent.)
  • 在底层硬件出现故障的情况下,进程还应该能够抵御突然死亡

保持开发、暂存和生产尽可能相似

  • 缩小时间间隔:开发者写了几个小时或几分钟代码就进行部署
  • 缩小人员差距:谁写代码谁部署
  • 缩小环境差距:开发环境和生产环境应尽可能相似
  • 不依赖适配器在开发和生产之间使用不同的支持服务

将日志视为事件流

  • 应用不关心日志的路由或存储

  • 应用程序只需将其未缓冲的事件流写入 stdout

  • 每个流程的流将由执行环境捕获,并整理在一起

  • 日志系统功能

    • 查找过去的某些事件
    • 大规模趋势图
    • 主动警报

将管理/管理任务作为一次性任务运行

  • 数据库迁移
  • 控制台运行代码
  • 一次性脚本

一次性管理任务应在与应用程序在相同的环境中运行,管理程序和应用程序一起提供避免同步问题

Hooks 的 Capture Value 特性

Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

函数组件容易阅读和测试,没有状态或生命周期。因此可以让我们快速的写一个渲染 UI 的组件,这也与 React 推崇函数式编程的思想契合。

在 React v16.8 推出之前函数组件只能简单的渲染组件,没有自身的状态,Hooks 的到来改变了这一现状,它赋予函数组件更多的能力。

通过 useState 让函数组件拥有了自身的状态。useEffect 让函数组件能够执行包含副作用的行为。

Capture Value

首先说明 class 组件是不具备 Capture 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
class ClassCount extends Component {
state = {
count: 0,
};

timer() {
setTimeout(() => {
console.log(`点击 3 秒后的 count: ${this.state.count}`);
}, 3000);
}

render() {
return (
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
this.timer();
}}
>
class 组件点击了 {this.state.count} 次
</button>
);
}
}

上面的代码中,每次点击后 timer 输出的值和当前组件 state 的值是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function FunctionCount() {
const [count, setCount] = useState(0);

const timer = () => {
setTimeout(() => {
// timer 打印的 count 永远是上一次 render 中的值
console.log(`function 组件 3 秒后的 count: ${count}`);
}, 3000);
};

return (
<button
onClick={() => {
setCount(count + 1);
timer();
}}
>
function 组件点击了 {count} 次
</button>
);
}

在函数组件里,每次点击后 timer 输出的值和当前组件中的 state 不一致,这是因为函数组件他就是一个普通函数,react 每次渲染都会重新执行一遍这个函数,这样就导致,对于函数内部来说每次渲染都有自己的 Props 与 State。

每次渲染都有自己的 Props 与 State

可以认为每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Counter() {
const [count, setCount] = useState(0);

return (
<button
onClick={() => {
setCount(count + 1);
}}
>
function 组件点击了 {count} 次
</button>
);
}

当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
// react 第一次渲染时
function Counter() {
const count = 0;
return <div>{count}</div>;
}

// 点击触发 react 重新渲染,整个函数组件被重新调用了
function Counter() {
const count = 1;
return <div>{count}</div>;
}

// ...

React 仅仅只是在渲染输出中插入了 count 这个数字, 这个数字由 React 提供。当 setCount 的时候,React 会带着一个不同的 count 值再次调用组件。然后,React 会更新 DOM 以保持和渲染输出一致。

这里关键的点在于任意一次渲染中的 count 常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的 count 值独立于其他渲染。

每一次渲染都有它自己的事件处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}

当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量。对于 handleAlertClick 也是这样

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
//  react 第一次渲染
function Counter() {
const count = 0;

function handleAlertClick() {
setTimeout(() => {
// count === 0
alert('You clicked on: ' + count);
}, 3000);
}

return (...);
}

// 点击触发 react 重新渲染,整个函数组件被重新调用了
function Counter() {
const count = 01;

// 创建了新的 handleAlertClick
function handleAlertClick() {
setTimeout(() => {
// count === 1
alert('You clicked on: ' + count);
}, 3000);
}

return (...);
}

// ...

React 每次渲染都会创建新的 handleAlertClick, 而对于 handleAlertClick 来讲,每次他访问到的 count 都是一个全新的常量。

在任意一次渲染中,props 和 state 是始终保持不变的。如果 props 和 state 在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的 count 值。

每次渲染都有它自己的 Effects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(`useEffect 执行, count: ${count}`);
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

useEffect 和事件处理函数一样,每次渲染都被重新执行,传入 useEffect 的回调函数也被重新创建,所有内部引用的所有 state 都是独属于本次渲染的。

错误设置依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Counter4() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
// count 永远是 0
console.log('Counter4 setInterval', count);
setCount(count + 1);
}, 1000);

return () => clearInterval(id);
}, []);

return <h1>count: {count}</h1>;
}

上面的代码中的 setInterval 会持续执行,但是 count 的值保持不变,原因就是第一次执行时 setCount 触发重新渲染,但是由于 useEffect 的依赖是[],effect 不会再重新运行,它后面每一秒都会调用 setCount(0 + 1)

设置依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter5() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log('Counter5 useEffect');

const id = setInterval(() => {
// 每次都重新生成新的定时器
console.log('Counter5 setInterval');
setCount(count + 1);
}, 1000);

return () => clearInterval(id);
}, [count]);

return <h1>count: {count}</h1>;
}

上面的代码已经可以正确运行了,缺点是 count 每次改变都会重置 setInterval

仅指定行为,不依赖具体的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter6() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log('Counter6 useEffect');

const id = setInterval(() => {
// 仅指定行为,不依赖具体的值
console.log('Counter6 setInterval');
setCount(c => c + 1);
}, 1000);

return () => clearInterval(id);
}, []);

return <h1>count: {count}</h1>;
}

附 useState, useEffect 的简单实现

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
import { render } from '../index';

// hooks 存放在这个数组
let memoizedState = [];
// 当前 memoizedState 下标
let cursor = 0;

export function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;

const currentCursor = cursor;
function setState(newState) {
if (typeof newState === 'function') {
memoizedState[currentCursor] = newState(memoizedState[currentCursor]);
} else {
memoizedState[currentCursor] = newState;
}

render();
// 每更新一次都需要将_index归零,才不会不断重复增加 memoizedState
cursor = 0;
}

// cursor 加 1
cursor++;

// 返回当前 state
return [memoizedState[currentCursor], setState];
}

export function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true;
if (hasNoDeps || hasChangedDeps) {
setTimeout(callback);
memoizedState[cursor] = depArray;
}
cursor++;
}

从 generator 到 async/await

协程

协程是一种比线程更加轻量级的存在。可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程

协程有点像函数,又有点像线程。它的运行流程大致如下。

第一步,协程 A 开始执行。

第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B。

第三步,(一段时间后)协程 B 交还执行权。

第四步,协程 A 恢复执行。

上面流程的协程 A,就是异步任务,因为它分成两段(或多段)执行。

举例来说,读取文件的协程写法如下。

1
2
3
4
5
function asnycJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}

上面代码的函数 asyncJob 是一个协程,在执行到其中的 yield 命令处时,执行权将交给其他协程。也就是说,yield 命令是异步两个阶段的分界线。

协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除 yield 命令,简直一模一样。

Generator

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

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
// generator 函数
function* foo() {
let response1 = yield fetch('http://back.avatar.movie.test.sankuai.com/');
console.log('response1');
console.log(response1);
let response2 = yield fetch('http://back.avatar.movie.test.sankuai.com/');
console.log('response2');
console.log(response2);
}

// 执行 foo 函数的代码
let gen = foo();
function getGenPromise(gen) {
return gen.next().value;
}
getGenPromise(gen)
.then(response => {
console.log('response1');
console.log(response);
return getGenPromise(gen);
})
.then(response => {
console.log('response2');
console.log(response);
});
  • 首先执行的是let gen = foo(),创建了 gen 协程。
  • 然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。
  • gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象 response1,然后通过 yield 暂停 gen 协程的执行,并将 response1 返回给父协程。
  • 父协程恢复执行后,调用 response1.then 方法等待请求结果。
  • 等通过 fetch 发起的请求完成之后,会调用 then 中的回调函数,then 中的回调函数拿到结果之后,通过调用 gen.next 放弃主线程的控制权,将控制权交 gen 协程继续执行下个请求。

通过 Generator 和 Promise 相互配合执行,达到了将异步操作以同步方式书写的目的。不过通常,我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器

(可参考著名的 co 框架),如下面这种方式:

1
2
3
4
5
6
7
8
9
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
co(foo());

通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。

co 源码分析

Co 核心代码(删去了非核心代码)

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
function co(gen) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
onFulfilled();

function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, ' +
'but the following object was passed: "' +
String(ret.value) +
'"'
)
);
}
}

这儿,在给 co 传入一个generator函数后,co 会将其自动启动。然后调用onFulfilled函数。在onFulfilled函数内部,首先则是获取 next 的返回值。交由next函数处理。 而next函数则首先判断是否完成,如果这个 generator 函数完成了,返回最终的值。否则则将yield后的值,转换为Promise。最后,通过Promise的 then,并将onFulfilled函数作为参数传入。

1
2
3
if (value && isPromise(value)) {
return value.then(onFulfilled, onRejected);
}

而在generator中,yield句本身没有返回值,或者说总是返回undefined 而 next 方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。同时通过onFulfilled函数,则可以实现自动调用。这也就能解释为什么 co 基于Promise。且能自动执行了。

Async/await

async 到底是什么?根据 MDN 定义,async 是一个通过异步执行隐式返回 Promise 作为结果的函数。

对 async 函数的理解,需要重点关注两个词:异步执行隐式返回 Promise

1
2
3
4
5
async function foo() {
return 2;
}
// async 函数返回一个 promise
console.log(foo()); // Promise {<resolved>: 2}

await 到底是什么?

1
2
3
4
5
6
7
8
9
async function foo() {
console.log(1);
let a = await 100;
console.log(a);
console.log(2);
}
console.log(0);
foo();
console.log(3);

根据上面的代码来分析 async/await 的执行流程。

首先,执行console.log(0)这个语句,打印出来 0。

  1. 执行 console.log(0)
  2. 执行 foo 函数,foo 是 async 函数,js 引擎保留当前的调用栈等信息
  3. 执行 foo 函数中 console.log(1)
  4. 执行 await 100,当遇到 await 100语句,js 引擎默认创建一个 promise 对象,大致代码如下:
1
2
3
let promise_ = new Promise((resolve,reject){
resolve(100)
})
  1. js 引擎执行到 resolve(10)将任务提交给微任务队列
  2. 然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程
  3. 父协程拿到主线程控制权,做的一件事是调用 promise_.then 来监控 promise 状态的改变
  4. 继续执行console.log(3)
  5. 父协程将执行结束,在结束之前,检查微任务队列,然后执行微任务队列
  6. 执行resolve(100),触发 promise_.then 中的回调函数:
1
2
3
4
promise_.then(value => {
// 回调函数被激活后
// 将主线程控制权交给 foo 协程,并将 vaule 值传给协程
});
  1. 将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程
  2. foo 协程激活之后,会把刚才的 value 值赋给了变量 a
  3. 执行 foo 函数的后续语句,执行完成之后,将控制权归还给父协程。
  4. 完毕。

以上就是 await/async 的执行流程。正是因为 async 和 await 在背后为我们做了大量的工作,所以我们才能用同步的方式写出异步代码来。


Co 源码详细分析

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* 执行 generator function 或者 generator,
* 返回一个 Promise
* @param {Function} fn
* @return {Promise}
* @api public
*/

function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);

// we wrap everything in a promise to avoid promise chaining,
// 把传进来的所有东西都转为 promise
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function (resolve, reject) {
// 执行 gen 函数
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// gen函数返回的 gen 指针不存在或 gen.next 不是函数(意味着 gen 不是 Generator函数) 则返回空值
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();

/**
* @param {Mixed} res
* @return {Promise}
* @api private
* 把每次 yield 之后的异步函数的返回结果当做参数传回 generator 函数
* eg. let a = yeild b
* 此时经过 onFulfilled 函数的处理 a === b()
*/
function onFulfilled(res) {
var ret;
try {
/**
* gen.next(res),则是向generator函数传参数,作为yield的返回值
* ret 是 gen.next() 返回的对象
* ---
* yield 语句本身没有返回值,或者说总是返回undefined。
* next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
*/
ret = gen.next(res);
} catch (e) {
return reject(e);
}
// 每完成一次 yield,把返回的对象交给 next() 处理
next(ret);
}

/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* Get the next value in the generator,
* return a promise.
* 把 gen.next() 返回对象中的 value 变为 promise
*
* @param {Object} ret
* @return {Promise}
* @api private
*/

function next(ret) {
/**
* 如果这个generator函数完成了,返回最终的值
* 假设我们写的 generator函数没有 return someValue, 在所有yield完成后,调用next()会返回{valu: undefined, done: true}
* 所以需要手动return一个值。这样最后的value才不是undefined
*/
if (ret.done) return resolve(ret.value);
// 这个generator函数还没结束, 就统一交给 toPromise() 处理
var value = toPromise.call(ctx, ret.value);
// 这里value.then(onFulfilled, onRejected),实际上已经调用并传入了 onFulfilled, onRejected 两个参数。
// 把 onFulfilled 函数传入 value
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, ' +
'but the following object was passed: "' +
String(ret.value) +
'"'
)
);
}
});
}

剩下的(thunkToPromise, arrayToPromise)这些辅助函数就不写了

快速弄懂 BFC

参考

BFC 是什么

css 2.1 规范。BFC(Block formatting context)直译为”块级格式化上下文”。它是一个独立的渲染区域,只有 Block-level box 参与, 它规定了内部的 Block-level Box 如何布局,并且与这个区域外部毫不相干。

目的是: 形成一个完全独立的空间,让空间中的子元素不会影响到外面的布局。

如何触发 BFC

为元素设置一些 CSS 属性,就能触发 BFC ,即生成一个完全独立的空间。、

最常用触发规则:

  1. float不为 none;
  2. positiion 不为 staticrelative;
  3. overflowauto, scrollhidden;
  4. display 的值为 table-cellinline-block.

BFC 可以解决什么问题

1. 解决浮动元素令父元素高度坍塌的问题

正常情况:

1
2
3
4
5
6
<div class="father">
<div class="son left">元素A</div>
<div class="son right">元素B</div>
<div class="son left">元素C</div>
<div class="son right">元素D</div>
</div>
1
2
3
4
5
6
7
.father {
background: #333;
}
.son {
background: #ccc;
outline: 1px dashed red;
}

正常情况布局
为子元素添加浮动:

1
2
3
4
5
6
.left {
float: left;
}
.right {
float: right;
}

子元素浮动时

此时可以看到,father 元素已经不可见了(高度坍缩为 0 了)。

为 父元素 设置 BFC :

1
2
3
4
5
.father {
background: #333;
// 可根据具体情况设置适合的 css 属性来设置 BFC
overflow: hidden;
}

设置BFC后

可以看到,再设置了 BFC 后,father 元素再次可见,并且完全包裹住了内部浮动的子元素。

2. 解决两栏自适应布局问题(利用 float 实现时)

未设置 BFC 时:

1
2
3
4
<body>
<div class="left"></div>
<div class="main">...省略大量字符</div>
</body>
1
2
3
4
5
6
7
8
9
10
.left {
float: left;
width: 100px;
height: 100px;
background: lightgreen;
}
.main {
height: 140px;
background: lightblue;
}

未设置 BFC 时

可以看到 main 元素的内容溢出到了左侧,要解决这个问题我们就可以为 main 元素设置 BFC

1
2
3
4
5
6
.main {
height: 140px;
background: lightblue;
// 可根据具体情况设置适合的 css 属性来设置 BFC
overflow: hidden;
}

设置 BFC 后

此时main 元素就是一个独立的空间,不会和浮动的 left 重合了。

3. 解决外边距垂直方向重合的问题

一般情况下,兄弟元素的外边距在垂直方向会取两者的最大值而不是取和

1
2
3
4
<div class="father">
<div class="son1"></div>
<div class="son2"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
.father {
outline: 1px solid red;
}
.son1 {
height: 20px;
margin-bottom: 20px;
background: lightgreen;
}
.son2 {
height: 30px;
margin-top: 30px;
background: lightblue;
}

外边距重合

可以看到,虽然咱们分别给 son1son2 都设置了 margin 但是,他们之间的 margin 却只有 30px,要解决这个问题就可以通过设置 BFC 来实现。

1
2
3
4
5
6
<div class="father">
<div class="son1"></div>
<div class="bfc">
<div class="son2"></div>
</div>
</div>
1
2
3
4
.bfc {
// 可根据具体情况设置适合的 css 属性来设置 BFC
overflow: hidden;
}

外边距不重合

可以看到,此时 son1son2 元素的外边距都发挥了作用,不再重合在一起了。

注意

触发 BFC 的属性很多,但是选择要慎重,要考虑到实际的情况,避免影响到其他元素的布局,比如说,用 float 触发 BFC 的时候就要注意 float 可能对布局产生的影响所以一定要想清楚,用什么方式去处触发他。

这些问题的其他解决方案

要解决上述几个问题,其实不是只有设置 BFC 这一个解决方案。例如:

  1. 解决浮动元素令父元素高度坍塌的问题

    • 让父元素也浮动起来,父元素和子元素一起脱离文档流。(代码量少,可能影响后面元素的排列)
    • 给父元素添加一个固定高度。(只适用于已知子元素高度的情况,不灵活,难以维护)
    • 在浮动的子元素后添加空元素,设置(clear: both)来清除浮动。(会增加无意义便签,不利于维护)
    • 为浮动的最后一个子元素设置伪元素(:after: {clear: both})。(同上)
  2. 解决两栏自适应布局问题

    • 左边左浮动,右边设置左侧宽度的 margin-left
    • 左边绝对定位,右边设置左侧宽度的 margin-left
    • 左边绝对定位,右边同样绝对定位,left设为左侧宽度, width 设置 100%
    • 使用 flex 布局 等。
  3. 解决外边距垂直方向重合的问题

    • 使用 paddind 代替 margin 。(之后需要设置 padding 的话改起来麻烦)

通过样例来理解 MVC 模式

参考: 自制前端框架之 MVC
参考: MVC,MVP 和 MVVM 的图示

如何设计一个程序的结构,这是一门专门的学问,叫做”架构模式”(architectural pattern),属于编程的方法论。MVC 模式就是架构模式的一种,在 UI 编程领域大有大量使用 MVC 模式的开发框架 (Django 等后端框架),使得开发者能够借助该模式,构建出更易于扩展和维护的应用程序。

MVC 简介

大体上可将 MVC 模式的结构分为三层,即 Model(模型)、View(视图)和 Controller(控制)

  • Model: 专注数据的存取,对基础数据对象的封装。
  • Controller: 处理具体业务逻辑,即根据用户从”视图层”输入的指令,从 Model 中存取数据,并渲染到 View 中。
  • View : 负责界面视图,供用户查看和操作,可理解为【输入数据,输出界面】的模块,在其中通常不涉及的业务逻辑。

这三层是紧密联系在一起的,但又是互相独立的,每一层内部的变化不影响其他层。每一层都对外提供接口(Interface),供其他层调用。这样一来,软件就可以实现模块化,修改外观或者变更数据都不用修改其他层,大大方便了维护和升级。

需要注意的是,MVC 仅是一种模式理念,而非具体的规范。因此,根据 MVC 的理念所设计出的框架,在实现和使用上可能存在着较大的区别。

咱们的目标

常见的后端框架所封装的功能,不外乎对数据的增查改删与渲染。在前端,我们以一个非常简单的 Todo App 作为示例,来实际看看 MVC 模式到底是怎样工作的。

  • Model 模块实现 Todo 这一数据模型的存取。
  • View 模块实现将 Todo 数据模型渲染到页面。
  • Controller 模块实现对 Todo 数据的新增、编辑、删除等操作。

编写代码

Model 模块

按照 MVC 模式,Model 模块的主要工作是存取数据,并且在数据变化时将新数据传给 View 模块。Model 模块的核心是订阅-发布者模式

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
class Model {
// 在构造器中实例化数据与订阅者
// 本例中的数据就是 一个个的 todo
constructor() {
// 数据格式 [{id: 1, value: '123'}]
this.todo = [];
this.todo;
// 【初始化订阅者】
this.subscribers = [];
}

// 利用 ES6 class 语法定义模型实例的 getter
// 从而在调用 model.data 时返回正确的 Todo 数据
get data() {
return this.todo;
}

// 利用 ES6 class 语法定义模型实例的 setter
// 从而在执行形如 modle.data = newData 的赋值时
// 能够通知订阅了 Model 的模块进行相应更新 【数据更新时,触发订阅回调】
set data(data) {
this.todo = data;
this.publish(this.todo);
}

// 由 Model 实例调用的发布方法
// 在 Model 中的 setter 更新时,将新数据传入该方法
// 由该方法将新数据推送到每个订阅者提供的回调中
// 在 本项目中,订阅者为 Controller 的 render 方法 【触发所有订阅】
publish(data) {
// 此处的订阅者是 业务中的函数
this.subscribers.forEach(render => render(data));
}
}

在示例中可以发现,所谓的发布 - 订阅模式,其思路和实现均非常简单:

  1. 区分出【发布者】和【订阅者】的概念。本例中 Model 为发布者,Controller 为订阅者。
  2. 在发布者中维护【我有哪些订阅者】信息的数组,每个元素为一个订阅者提供的回调。
  3. 发布者数据更新时,依次触发所有订阅者的回调。

不过,Model 中的代码仅实现了【初始化发布者】与【触发所有订阅】,【数据更新时,触发订阅回调】的功能,并不是一个完整的发布 - 订阅模式。在完整的模式实现中,其余代码包括:

  1. 【订阅者订阅发布者】机制的实现,其代码位置为 Controller 中的最后一行 this.model.subscribers.push(this.render),在此将 render 方法作为订阅者回调,提供给了发布者。
  2. 【订阅者提供的订阅方法】的实现,在此即为 Controller 中提供的 this.render 方法。

Controller 模块

上文中已经明确,Controller 模块需要实现的功能为:

  • 与 Model / View 实例的绑定。
  • 对点击事件、DOM 选择等底层 API 的封装。
  • 用于渲染数据的 Render 方法。
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Controller {
constructor(conf) {
// 根据实例化参数,定义 Controller 基础配置
// 包括 DOM 容器、Model / View 实例及 onClick 事件等
this.el = document.querySelector(conf.el);
this.model = conf.model;
this.view = conf.view;
// 为 容器 dom 设置事件
this.bindEvent(this.el);
// 给 render 函数绑定 this
this.render = this.render.bind(this);
// 在 Model 更新时执行 controller 的 render 方法
this.model.subscribers.push(this.render);
}

// 根据点击 btn 的 class 属性绑定不同的事件回调
bindEvent() {
this.el.addEventListener('click', event => {
let el = event.target;
let id = el.dataset.id;
if (el.classList.contains('todo-delete')) {
deleteTodo(id);
} else if (el.classList.contains('todo-update')) {
updateTodo(el, id);
} else if (el.classList.contains('todo-add')) {
addTodo(el);
}
});

// 点击 add 时,把新的 todo 添加到 model 中
const addTodo = el => {
let input = el.parentElement.querySelector('.input-add');
let value = input.value;
if (value !== '') {
let data = this.model.data;
data.push({
id: data.length,
value: value,
});
this.model.data = data;
}
};

// 点击 delete 时,把对应 todo 从 model 中删除
const deleteTodo = id => {
this.model.data = this.model.data.filter(todo => {
return id !== String(todo.id);
});
};

// 点击 update 时,把对应 todo 在 model 中更新
const updateTodo = (el, id) => {
this.model.data = this.model.data.map(todo => {
return {
id: todo.id,
value: setValue(id, todo),
};
});
};

// 辅助 更新函数
const setValue = (id, todo) => {
if (id === String(todo.id)) {
let updateInput = document.querySelector(`input[data-id="${todo.id}"]`);
return updateInput.value;
} else {
return todo.value;
}
};
}

// 全量重置 DOM 的 naive render 实现
render() {
// 由于 view 是纯函数,故而直接对其传入 Model 数据
// 将输出的 HTML 模板作为 Controller DOM 内的新状态
this.el.innerHTML = this.view(this.model.todo);
}
}

可以看到 Controller 模块在点击事件触发时,没有直接修改 dom, 只是修改了 model 中的数据, dom 视图的改变是在 model 中数据变化时自动 render 的。

View 模块

如前文所述, View 模块实质上就是一个在 Model 中数据变更时,由 Controller 在 render 方法中执行的一个纯函数。

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
function view(todos) {
const todosList = todos
.map(
todo => `
<div>
<span data-id="${todo.id}">
${todo.value}
</span>
<button data-id="${todo.id}" class="todo-delete">
删除
</button>
<span>
<input data-id="${todo.id}"/>
<button data-id="${todo.id}" class="todo-update">
Update
</button>
</span>
</div>
`
)
.join('');

return `
<main>
<input class="input-add"/>
<button class="todo-add">Add</button>
<div>${todosList}</div>
</main>
`;
}

index.html

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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./mvc.js"></script>
<script>
const model = new Model();
let conf = {
model,
view,
el: '#app',
};
const controller = new Controller(conf);
controller.render();
</script>
</body>
</html>

总结

在实现 MVC 模式的 todo app 的过程中,MVC 模式的特性得到了体现,【Model, View】模块直接只对外提供接口, 【Controller】模块则将两者连接了起来,而 ES6 所提供的 class 高级特性则大大简化这些特性的实现复杂度(setter getter 等)。

最后的最后,咱们再梳理下流程:

点击按钮 -> 触发 controller 里的事件 -> 更改 model 数据 -> 触发 render 函数 -> 更新视图

js 面向对象编程(一):封装

参考:阮一峰老师的博客

Javascript 是一种基于对象(object-based)的语言,我们遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)的概念。那么,如果我们要把”属性”(property)和”方法”(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?

1. 工厂模式(改进的原始模式)

这种模式抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const creatPerson = function (name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name);
},
};
};
// 生成实例对象的过程其实就是调用这个函数
const xcmy = creatPerson('小明', 23);
const xchw = creatPerson('小花', 22);
xcmy.sayName(); // 小明
console.log(xchw.age); // 22

弊端

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型),也不能反映实例之间的联系


构造函数模式(构造器模式)

为了解决从原型对象生成实例的问题,Javascript 提供了一个构造函数(Constructor)模式。所谓”构造函数”,其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。重写 Person

1
2
3
4
5
6
7
8
9
10
11
12
13
const Person = function (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
};
// 生成实例就是 new Person,
// 现在的 Person 函数叫做构造函数
const xcmy = new Person('小明', 23);
const xchw = new Person('小花', 22);
xcmy.sayName(); // 小明
console.log(xchw.age); // 22

这时xcmyxchw会自动含有一个constructor属性,指向它们的构造函数。

1
2
console.log(xcmy.constructor == Person); //true
console.log(xchw.constructor == Person); //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。
Javascript 还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。

1
2
console.log(xcmy instanceof Person); //true
console.log(xchw instanceof Person); //true

使用 new 创建实例对象发生了什么?

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。

弊端

每个用 new 创建出来的实例对象都拥有同样的属性和方法,但是方法(method) 对于每个实例都是一模一样的内容,每次生成一个实例,都必须重复同样的内容,多占用一些内存。这样既不环保,也缺乏效率

原型模式(Prototype 模式)

Javascript 规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Person = function (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.sayName = function () {
console.log(this.name);
};
// 生成实例就是 new Person,
// 现在的 Person 函数叫做构造函数
const xcmy = new Person('小明', 23);
const xchw = new Person('小花', 22);
xcmy.sayName(); // 小明
console.log(xchw.age); // 22
console.log(xcmy.sayName === xchw.sayName); // true

这时所有实例的sayName()方法,其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率。

Prototype 模式的验证方法

为了配合 prototype 属性,Javascript 定义了一些辅助方法,帮助我们使用它。

isPrototypeOf()

这个方法用来判断,某个proptotype对象和某个实例之间的关系。

1
2
console.log(Person.prototype.isPrototypeOf(xcmy)); //true
console.log(Person.prototype.isPrototypeOf(xchw)); //true

hasOwnProperty()

每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。

1
2
console.log(xcmy.hasOwnProperty('name')); // true
console.log(xcmy.hasOwnProperty('sayName')); // false

in 运算符

in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。

1
2
console.log('name' in xcmy); // true
console.log('sayName' in xcmy); // true

in运算符还可以用来遍历某个对象的所有属性。

1
2
3
for (let prop in xcmy) {
console.log('xcmy[' + prop + ']=' + xcmy[prop]);
}

js 面向对象编程(二):构造函数的继承

组合继承

原理:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 父类
const Person = function (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.sayName = function () {
console.log(this.name);
};
// 子类
const Male = function (name, age) {
// 继承属性
Person.call(this, name, age);
this.gender = 'male';
};
// 继承方法
Male.prototype = new Person();
// 把新的原型中的 constructor 指回自身
Male.prototype.constructor = Male;

// test
const p1 = new Male('whh', 23);
console.log(p1);
// Male {name: "whh", age: 23, gender: "male"}

图解

弊端:调用了两次父类的构造函数,导致原型中产生了无效的属性。

寄生组合式继承

原理:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。主要就是用一个空的构造函数,来当做桥梁,并且把其原型对象指向父构造函数的原型对象,并且实例化一个 temp,temp 会沿着这个原型链,去找到父构造函数的原型对象。举例:

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
// 父类
const Person = function (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.sayName = function () {
console.log(this.name);
};
// 子类
const Male = function (name, age) {
// 继承属性
Person.call(this, name, age);
this.gender = 'male';
};
// 寄生式组合继承
const extend = function (subType, superType) {
const Temp = function () {};
// 把Temp构造函数的原型对象指向superType的原型对象
Temp.prototype = superType.prototype;
// 用构造函数Temp实例化一个实例temp
let temp = new Temp();
// 把子构造函数的原型对象指向temp
subType.prototype = temp;
// 把temp的constructor指向subType
temp.constructor = subType;
};
// 使用
extend(Male, Person);

// test
const p1 = new Male('whh', 23);
console.log(p1);
// Male {name: "whh", age: 23, gender: "male"}

图解

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof isPrototypeOf()

目前程序猿认为解决继承问题最好的方案

其他继承方式(不完美的)

参考: 掘金地址

原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原型式继承
function createObjWithObj(obj){ // * 传入一个原型对象
function Temp(){}
Temp.prototype = obj
let o = new Temp()
return o
}

// * 把Person的原型对象当做temp的原型对象
let temp = createObjWithObj(Person.prototype)

// * 也可以使用Object.create实现
// * 把Person的原型对象当做temp2的原型对象
let temp2 = Object.create(Person.prototype)

寄生式继承

1
2
3
4
5
6
7
8
9
// 寄生式继承
// 我们在原型式的基础上,希望给这个对象新增一些属性方法
// 那么我们在原型式的基础上扩展
function createNewObjWithObj(obj) {
let o = createObjWithObj(obj)
o.name = "whh"
o.age = 23
return o
}

拷贝继承

1
2
3
4
5
6
7
const extend2(Child, Parent) {
let p = Parent.prototype
let c = Child.prototype
for (var i in p) {
       c[i] = p[i]
}
  }

这个函数的作用,就是将父对象的 prototype 对象中的属性,一一拷贝给 Child 对象的 prototype 对象。使用的时候,这样写:

1
2
3
extend2(Male, Person);
let p1 = new Male('whh', 23);
console.log(p1.age); // 23

js 中的原型和原型链

什么是原型?

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向prototype 属性所在函数的指针。举例:

1
2
3
4
const Person = function (name) {
this.name = name;
};
console.log(Person.prototype.constructor === Person); // true

在上例中,Person.prototype.constructor 指向 Person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

实例对象与构造函数的关系

1
2
const person1 = new Person('小明');
console.log(person1); // Person {name: "小明"}

上面的 person1 就是实例对象,Person 就被我们叫做 构造函数

构造函数的 prototype 属性与它创建的实例对象的[[prototype]]属性指向的是同一个对象,即 实例对象.__proto__ === 构造函数.prototype,所以:

1
console.log(person1.__proto__ === Person.prototype); // true

原型链

  • 每个对象都拥有一个隐藏的属性[[prototype]],指向它的原型对象,这个属性可以通过Object.getPrototypeOf(obj)obj.__proto__ 来访问。

  • 原型对象同样也有一个隐藏的属性[[prototype]],指向它的原型对象。

  • 通过__proto__ 属性将对象和原型连接起来,就组成了原型链。

原型链的作用

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
下面的示例来自 MDN

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
// 让我们从一个自身拥有属性a和b的函数里创建一个对象o:
let f = function () {
this.a = 1;
this.b = 2;
};

let o = new f(); // o: {a: 1, b: 2}
// 在f函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;

/*
不要在f函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
o.[[Prototype]] 有属性 b 和 c (其实就是o.__proto__或者o.constructor.prototype)
o.[[Prototype]].[[Prototype]] 是 Object.prototype.
最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
这就是原型链的末尾,即 null,
根据定义,null 没有[[Prototype]].
综上,整个原型链如下:
{a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null
*/
console.log(o.a); // 1
//a是o的自身属性吗?是的,该属性的值为1

console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为2
// 原型上也有一个'b'属性,但是它不会被访问到.这种情况称为"属性遮蔽 (property shadowing)"

console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为4

console.log(o.d); // undefined
// d是o的自身属性吗?不是,那看看原型上有没有
// d是o.[[Prototype]]的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 没有d属性,返回undefined

总结

  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • 函数的 prototype 是一个对象
  • 对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链
  • 对象里找不到的属性会到原型里去找