Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8d81fc3f5 | ||
|
|
43e264cc07 | ||
|
|
957737959f | ||
|
|
000e6511e6 | ||
|
|
a652284bbc | ||
|
|
1b88e5ac06 | ||
|
|
79ba9ec6a1 | ||
|
|
a4acf09d8b | ||
|
|
872ef2e346 | ||
|
|
8795ae0aaa | ||
|
|
23c4190d7e | ||
|
|
75d0109102 | ||
|
|
893bb2d97a | ||
|
|
0c2c8c6c03 | ||
|
|
97a313ca1e | ||
|
|
3fd57e26b8 | ||
|
|
aee6f6d6bb | ||
|
|
00fd6e541a | ||
|
|
6cc1af4ca9 | ||
|
|
ed27df0fe2 | ||
|
|
14ebdf19ca | ||
|
|
e89a1b0c7e | ||
|
|
a1bd0ec7a5 | ||
|
|
82d3665606 | ||
|
|
67a289e750 | ||
|
|
1b0a7ed8ea | ||
|
|
21ba3e0640 | ||
|
|
d5159b914e | ||
|
|
c45a3d09d5 | ||
|
|
c3ea6f2b17 | ||
|
|
fb98d1febc | ||
|
|
036a9779eb | ||
|
|
beb89cf31b | ||
|
|
5d65a777bd | ||
|
|
115d108404 | ||
|
|
f573e16473 | ||
|
|
ac53e3a325 | ||
|
|
d3dad3b151 | ||
|
|
7843682b1e | ||
|
|
11c34a9edc | ||
|
|
923c3990f7 | ||
|
|
b091d40925 | ||
|
|
5c76c9d567 | ||
|
|
0ad9a98d33 | ||
|
|
b9fb7c80f2 | ||
|
|
03519c41f6 | ||
|
|
8ff1e9e21a | ||
|
|
93882be318 | ||
|
|
d0dee4bbd7 | ||
|
|
3cc44000eb | ||
|
|
46cdabeb86 | ||
|
|
71258f1b83 | ||
|
|
1d95103bfb | ||
|
|
b9d9292040 | ||
|
|
7ce55795a2 | ||
|
|
2a420c455e | ||
|
|
069cf8f947 | ||
|
|
af86669e18 | ||
|
|
50c4409e29 | ||
|
|
d8f2e0977d | ||
|
|
f4e75f5e21 |
+1
-1
@@ -44,7 +44,7 @@
|
||||
|
||||
### 模态框定位
|
||||
|
||||
首先,Model 与 Toast、Notification、Message 以及 Popover 都会在某个时间点被触发弹出一个浮层,但与 Modal(模态框)还是有所不同的。定义上看,上述组件都不属于模态框,因为模态框有一个重要的特性,即阻塞原来主视窗下的操作,只能在框内作后续动作。也就是说模态框从界面上彻底打断了用户心流。
|
||||
首先,Modal 与 Toast、Notification、Message 以及 Popover 都会在某个时间点被触发弹出一个浮层,但与 Modal(模态框)还是有所不同的。定义上看,上述组件都不属于模态框,因为模态框有一个重要的特性,即阻塞原来主视窗下的操作,只能在框内作后续动作。也就是说模态框从界面上彻底打断了用户心流。
|
||||
|
||||
当然,这也是我们需要讨论的问题,如果只是一般的消息提醒,可以用信息条、小红点等交互形式,至少是不阻塞用户操作的。在原文末引用的 10 Guidelines to Consider when using Overlays 一文中,第 8 条强调了模态框不到万不得以不应该使用。这时我们应该思考什么情况下你非常希望他不要离开页面,来读框内的信息或作操作呢?
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ SELECT * from bees WHERE bee = 'red';
|
||||
|
||||
但是当我们将文法粒度变细,将 `CASE WHEN` 与 `WHERE` 区块分别交由两块文法解决,将等号这个通用的表达式抽离出来,就可以不关心上下文了,这种方式称为 **上下文无关文法**。
|
||||
|
||||
附上一个 [mysql 上下文无关文法集合](https://github.com/antlr/grammars-v4/blob/master/mysql/MySqlParser.g4)。
|
||||
附上一个 [mysql 上下文无关文法集合](https://github.com/antlr/grammars-v4/blob/master/sql/mysql/Positive-Technologies/MySqlParser.g4)。
|
||||
|
||||
### 左推导与右推导
|
||||
|
||||
|
||||
@@ -456,7 +456,7 @@ function Article({ id }) {
|
||||
|
||||
## useEffect 还有什么优势
|
||||
|
||||
`useEffect` 在渲染结束时执行,所以不会阻塞浏览器渲染进程,所以使用 Function Component 写的项目一般都有用更好的性能。
|
||||
`useEffect` 在渲染结束时执行,所以不会阻塞浏览器渲染进程,所以使用 Function Component 写的项目一般都拥有更好的性能。
|
||||
|
||||
自然符合 React Fiber 的理念,因为 Fiber 会根据情况暂停或插队执行不同组件的 Render,如果代码遵循了 Capture Value 的特性,在 Fiber 环境下会保证值的安全访问,同时弱化生命周期也能解决中断执行时带来的问题。
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ Table 主要配置分为行、列、标记与筛选。通过这四个配置区
|
||||
|
||||
我们会发现,原本存在于列的 Category 被自动挪到了行,原本存在于行的 Sales 被挪到了 “标记” 区域。在正式介绍 “标记” 区域前,先理解一下为何会发生这种转变:
|
||||
|
||||
**表格类组件是双维度组件,折线图是单维度组件。**也就是表格的行与列都是维度,而折线图横轴作为维度后,纵轴就要作为度量。上面的例子中,折线图维度有两个字段,虽然通过分面方式渲染出来了,但当切换为支持双维度的表格后, **可以将多余的一个维度挪到表格组件另一个维度区域中**。
|
||||
**表格类组件是双维度组件,折线图是单维度组件。** 也就是表格的行与列都是维度,而折线图横轴作为维度后,纵轴就要作为度量。上面的例子中,折线图维度有两个字段,虽然通过分面方式渲染出来了,但当切换为支持双维度的表格后, **可以将多余的一个维度挪到表格组件另一个维度区域中**。
|
||||
|
||||
而表格行与列都是维度的情况下,单元格的值就需要用 “标记” 中文本来表示,因此原折线图的度量字段自动转移到了 “标记” 区域。
|
||||
|
||||
@@ -313,7 +313,7 @@ Tableau 内置的图表分为 N 大类 - **表格、地图、柱折面饼、散
|
||||
|
||||
<img width=529 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566699040802-40a23a43-7a23-43d1-9d06-27a6413803e8.png#align=left&display=inline&height=314&name=image.png&originHeight=856&originWidth=1440&size=106220&status=done&width=529">
|
||||
|
||||
**上图也可以理解为展示出 Order Date 与 Order ID 的明细数据,按照 Order Date 分组且列合并。**下钻就是一步步接近明细数据的过程,但目的不是为了看明细表,而是看某些维度下按其他维度拆分的详细信息。
|
||||
**上图也可以理解为展示出 Order Date 与 Order ID 的明细数据,按照 Order Date 分组且列合并。** 下钻就是一步步接近明细数据的过程,但目的不是为了看明细表,而是看某些维度下按其他维度拆分的详细信息。
|
||||
|
||||
图表下钻和表格思路是一致的:
|
||||
|
||||
@@ -323,9 +323,9 @@ Tableau 内置的图表分为 N 大类 - **表格、地图、柱折面饼、散
|
||||
|
||||
<img width=667 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566700041429-03bcb68a-dd00-4cab-a256-f37234bb2300.png#align=left&display=inline&height=423&name=image.png&originHeight=1350&originWidth=2128&size=155697&status=done&width=667">
|
||||
|
||||
**可以认为,当行或列上最后一个字段为度量时,就会切换为图表展示,因为图表适合展示连续状态。**如果排除上图蓝色区域,剩下的区域就是个交叉表,交叉表只是行与列同时存在维度字段的场景,仅有行或列时就变成了普通表格;而图形的下钻和表格下钻机理相同,只是把 “单元格” 的文本换成了柱子或线。
|
||||
**可以认为,当行或列上最后一个字段为度量时,就会切换为图表展示,因为图表适合展示连续状态。** 如果排除上图蓝色区域,剩下的区域就是个交叉表,交叉表只是行与列同时存在维度字段的场景,仅有行或列时就变成了普通表格;而图形的下钻和表格下钻机理相同,只是把 “单元格” 的文本换成了柱子或线。
|
||||
|
||||
**所以对任何图表的下钻,都是对轴的下钻,**相同的是单元格属性永远不会改变,表格的单元格是文本,图形单元格是图形,一个简单折线图可以理解为对整体行与列单元格进行 “连续打通”:
|
||||
**所以对任何图表的下钻,都是对轴的下钻,** 相同的是单元格属性永远不会改变,表格的单元格是文本,图形单元格是图形,一个简单折线图可以理解为对整体行与列单元格进行 “连续打通”:
|
||||
|
||||
<img width=406 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566700448492-a7b96c99-b600-41b0-9159-e88412f4402b.png#align=left&display=inline&height=329&name=image.png&originHeight=1342&originWidth=1654&size=105794&status=done&width=406">
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ let [data, setData] = useState(
|
||||
let [isValidating, setIsValidating] = useState(false);
|
||||
```
|
||||
|
||||
而取数状态变化时往往 `data` 与 `isValidating` 要一起更新,为了仅触发一次更新,使用了 `unstable_batchedUpdates` 将更新合并为一次:
|
||||
而取数状态变化时往往 `data` 与 `isValidating` 要一起更新,为了仅触发一次更新,使用了 <del>`unstable_batchedUpdates` 将更新合并为一次:</del>
|
||||
|
||||
```tsx
|
||||
unstable_batchedUpdates(() => {
|
||||
@@ -344,7 +344,13 @@ unstable_batchedUpdates(() => {
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
其实还有别的解法,比如使用 `useReducer` 管理数据也能达到相同性能效果。
|
||||
目前源码已经从`unstable_batchedUpdates`切换为 `useReducer`管理
|
||||
```tsx
|
||||
dispatch(newState);
|
||||
```
|
||||
|
||||
|
||||
### 3.3 初始缓存
|
||||
|
||||
@@ -402,7 +408,7 @@ return {
|
||||
|
||||
### 3.5 依赖的请求
|
||||
|
||||
翻了一下代码,没有找到对循环依赖特别处理的逻辑,**后来看了官方文档才恍然大悟,原来是通过 `try/catch` + `onErrorRetry` 机制实现依赖取数的。**
|
||||
翻了一下代码,没有找到对循环依赖特别处理的逻辑,**后来看了官方文档才恍然大悟,原来是通过 `try/catch` 并巧妙结合 React 的 UI=f(data) 机制实现依赖取数的。**
|
||||
|
||||
看下面这段代码:
|
||||
|
||||
@@ -414,21 +420,34 @@ const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
|
||||
怎么做到智能按依赖顺序请求呢?我们看 `useSWR` 取数函数的主体逻辑:
|
||||
|
||||
```tsx
|
||||
try {
|
||||
// 设置 isValidation 为 true
|
||||
// 取数、onSuccess 回调
|
||||
// 设置 isValidation 为 false
|
||||
// 设置缓存
|
||||
// unstable_batchedUpdates
|
||||
} catch (err) {
|
||||
// 撤销取数、缓存等对象
|
||||
// 调用 onErrorRetry
|
||||
}
|
||||
const revalidate = useCallback(
|
||||
async() => {
|
||||
try {
|
||||
// 设置 isValidation 为 true
|
||||
// 取数、onSuccess 回调
|
||||
// 设置 isValidation 为 false
|
||||
// 设置缓存
|
||||
// unstable_batchedUpdates
|
||||
} catch (err) {
|
||||
// 撤销取数、缓存等对象
|
||||
// 调用 onError回调
|
||||
}
|
||||
},
|
||||
[key]
|
||||
)
|
||||
|
||||
useIsomorphicLayoutEffect(
|
||||
()=>{
|
||||
....
|
||||
},
|
||||
[key,revalidate,...]
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
可见取数逻辑被 `try` 住了,那么 `user.id` 在 `useSWR("/api/user")` 没有 Ready 的情况一定会抛出异常,则自动进入 `onErrorRetry` 逻辑,看看下次取数时 `user.id` 有没有 Ready。
|
||||
每次渲染的时候,SWR 会试着执行 `key` 函数(例如 () => "/api/projects?uid=" + user.id),如果这个函数抛出异常,那么就意味着它的依赖还没有就绪(user === undefined),SWR 将暂停这个数据的请求。在任一数据完成加载时,由于 `setState` 触发重渲染,上述 Hooks 会被重选执行一遍(再次检查数据依赖是否就绪)然后对就绪的数据发起新的一轮请求。
|
||||
|
||||
那么什么时候才轮到下次取数呢?这个时机是:
|
||||
另外对于一些正常请求碰到 error(shouldRetryOnError 默认为 true)的情况下,下次取数的时机是:
|
||||
|
||||
```tsx
|
||||
const count = Math.min(opts.retryCount || 0, 8);
|
||||
|
||||
@@ -0,0 +1,579 @@
|
||||
## 1 引言
|
||||
|
||||
[unstated](https://github.com/jamiebuilds/unstated) 是基于 Class Component 的数据流管理库,[unstated-next](https://github.com/jamiebuilds/unstated-next) 是针对 Function Component 的升级版,且特别优化了对 Hooks 的支持。
|
||||
|
||||
与类 redux 库相比,这个库设计的别出心裁,而且这两个库源码行数都特别少,与 180 行的 unstated 相比,unstated-next 只有不到 40 行,但想象空间却更大,且用法符合直觉,所以本周精读就会从用法与源码两个角度分析这两个库。
|
||||
|
||||
## 2 概述
|
||||
|
||||
**首先问,什么是数据流?React 本身就提供了数据流,那就是 `setState` 与 `useState`,数据流框架存在的意义是解决跨组件数据共享与业务模型封装。**
|
||||
|
||||
还有一种说法是,React 早期声称自己是 UI 框架,不关心数据,因此需要生态提供数据流插件弥补这个能力。但其实 React 提供的 `createContext` 与 `useContext` 已经能解决这个问题,只是使用起来稍显麻烦,而 unstated 系列就是为了解决这个问题。
|
||||
|
||||
### unstated
|
||||
|
||||
unstated 解决的是 Class Component 场景下组件数据共享的问题。
|
||||
|
||||
相比直接抛出用法,笔者还原一下作者的思考过程:利用原生 `createContext` 实现数据流需要两个 UI 组件,且实现方式冗长:
|
||||
|
||||
```jsx
|
||||
const Amount = React.createContext(1);
|
||||
|
||||
class Counter extends React.Component {
|
||||
state = { count: 0 };
|
||||
increment = amount => {
|
||||
this.setState({ count: this.state.count + amount });
|
||||
};
|
||||
decrement = amount => {
|
||||
this.setState({ count: this.state.count - amount });
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<Amount.Consumer>
|
||||
{amount => (
|
||||
<div>
|
||||
<span>{this.state.count}</span>
|
||||
<button onClick={() => this.decrement(amount)}>-</button>
|
||||
<button onClick={() => this.increment(amount)}>+</button>
|
||||
</div>
|
||||
)}
|
||||
</Amount.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AmountAdjuster extends React.Component {
|
||||
state = { amount: 0 };
|
||||
handleChange = event => {
|
||||
this.setState({
|
||||
amount: parseInt(event.currentTarget.value, 10)
|
||||
});
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<Amount.Provider value={this.state.amount}>
|
||||
<div>
|
||||
{this.props.children}
|
||||
<input
|
||||
type="number"
|
||||
value={this.state.amount}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</Amount.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render(
|
||||
<AmountAdjuster>
|
||||
<Counter />
|
||||
</AmountAdjuster>
|
||||
);
|
||||
```
|
||||
|
||||
而我们要做的,**是将 `setState` 从具体的某个 UI 组件上剥离,形成一个数据对象实体,可以被注入到任何组件。**
|
||||
|
||||
这就是 `unstated` 的使用方式:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { Provider, Subscribe, Container } from "unstated";
|
||||
|
||||
class CounterContainer extends Container {
|
||||
state = {
|
||||
count: 0
|
||||
};
|
||||
|
||||
increment() {
|
||||
this.setState({ count: this.state.count + 1 });
|
||||
}
|
||||
|
||||
decrement() {
|
||||
this.setState({ count: this.state.count - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
function Counter() {
|
||||
return (
|
||||
<Subscribe to={[CounterContainer]}>
|
||||
{counter => (
|
||||
<div>
|
||||
<button onClick={() => counter.decrement()}>-</button>
|
||||
<span>{counter.state.count}</span>
|
||||
<button onClick={() => counter.increment()}>+</button>
|
||||
</div>
|
||||
)}
|
||||
</Subscribe>
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
<Provider>
|
||||
<Counter />
|
||||
</Provider>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
```
|
||||
|
||||
首先要为 `Provider` 正名:`Provider` 是解决单例 Store 的最佳方案,当项目与组件都是用了数据流,需要分离作用域时,`Provider` 便派上了用场。如果项目仅需单 Store 数据流,那么与根节点放一个 `Provider` 等价。
|
||||
|
||||
其次 `CounterContainer` 成为一个真正数据处理类,只负责存储与操作数据,通过 `<Subscribe to={[CounterContainer]}>` RenderProps 方法将 `counter` 注入到 Render 函数中。
|
||||
|
||||
**unstated 方案本质上利用了 `setState`,但将 `setState` 与 UI 剥离,并可以很方便的注入到任何组件中。**
|
||||
|
||||
类似的是,其升级版 `unstated-next` 本质上利用了 `useState`,利用了自定义 Hooks 可以与 UI 分离的特性,加上 `useContext` 的便捷性,利用不到 40 行代码实现了比 `unstated` 更强大的功能。
|
||||
|
||||
### unstated-next
|
||||
|
||||
`unstated-next` 用 40 行代码号称 React 数据管理库的终结版,让我们看看它是怎么做到的!
|
||||
|
||||
还是从思考过程说起,笔者发现其 README 也提供了对应思考过程,就以其 README 里的代码作为案例。
|
||||
|
||||
首先,使用 Function Component 的你会这样使用数据流:
|
||||
|
||||
```jsx
|
||||
function CounterDisplay() {
|
||||
let [count, setCount] = useState(0);
|
||||
let decrement = () => setCount(count - 1);
|
||||
let increment = () => setCount(count + 1);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={decrement}>-</button>
|
||||
<p>You clicked {count} times</p>
|
||||
<button onClick={increment}>+</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
如果想将数据与 UI 分离,利用 Custom Hooks 就可以完成,这不需要借助任何框架:
|
||||
|
||||
```jsx
|
||||
function useCounter() {
|
||||
let [count, setCount] = useState(0);
|
||||
let decrement = () => setCount(count - 1);
|
||||
let increment = () => setCount(count + 1);
|
||||
return { count, decrement, increment };
|
||||
}
|
||||
|
||||
function CounterDisplay() {
|
||||
let counter = useCounter();
|
||||
return (
|
||||
<div>
|
||||
<button onClick={counter.decrement}>-</button>
|
||||
<p>You clicked {counter.count} times</p>
|
||||
<button onClick={counter.increment}>+</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
如果想将这个数据分享给其他组件,利用 `useContext` 就可以完成,这不需要借助任何框架:
|
||||
|
||||
```jsx
|
||||
function useCounter() {
|
||||
let [count, setCount] = useState(0);
|
||||
let decrement = () => setCount(count - 1);
|
||||
let increment = () => setCount(count + 1);
|
||||
return { count, decrement, increment };
|
||||
}
|
||||
|
||||
let Counter = createContext(null);
|
||||
|
||||
function CounterDisplay() {
|
||||
let counter = useContext(Counter);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={counter.decrement}>-</button>
|
||||
<p>You clicked {counter.count} times</p>
|
||||
<button onClick={counter.increment}>+</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
let counter = useCounter();
|
||||
return (
|
||||
<Counter.Provider value={counter}>
|
||||
<CounterDisplay />
|
||||
<CounterDisplay />
|
||||
</Counter.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
但这样还是显示使用了 `useContext` 的 API,并且对 `Provider` 的封装没有形成固定模式,这就是 `usestated-next` 要解决的问题。
|
||||
|
||||
所以这就是 `unstated-next` 的使用方式:
|
||||
|
||||
```jsx
|
||||
import { createContainer } from "unstated-next";
|
||||
|
||||
function useCounter() {
|
||||
let [count, setCount] = useState(0);
|
||||
let decrement = () => setCount(count - 1);
|
||||
let increment = () => setCount(count + 1);
|
||||
return { count, decrement, increment };
|
||||
}
|
||||
|
||||
let Counter = createContainer(useCounter);
|
||||
|
||||
function CounterDisplay() {
|
||||
let counter = Counter.useContainer();
|
||||
return (
|
||||
<div>
|
||||
<button onClick={counter.decrement}>-</button>
|
||||
<p>You clicked {counter.count} times</p>
|
||||
<button onClick={counter.increment}>+</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Counter.Provider>
|
||||
<CounterDisplay />
|
||||
<CounterDisplay />
|
||||
</Counter.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,`createContainer` 可以将任何 Hooks 包装成一个数据对象,这个对象有 `Provider` 与 `useContainer` 两个 API,其中 `Provider` 用于对某个作用域注入数据,而 `useContainer` 可以取到这个数据对象在当前作用域的实例。
|
||||
|
||||
对 Hooks 的参数也进行了规范化,我们可以通过 `initialState` 设定初始化数据,且不同作用域可以嵌套并赋予不同的初始化值:
|
||||
|
||||
```jsx
|
||||
function useCounter(initialState = 0) {
|
||||
let [count, setCount] = useState(initialState);
|
||||
let decrement = () => setCount(count - 1);
|
||||
let increment = () => setCount(count + 1);
|
||||
return { count, decrement, increment };
|
||||
}
|
||||
|
||||
const Counter = createContainer(useCounter);
|
||||
|
||||
function CounterDisplay() {
|
||||
let counter = Counter.useContainer();
|
||||
return (
|
||||
<div>
|
||||
<button onClick={counter.decrement}>-</button>
|
||||
<span>{counter.count}</span>
|
||||
<button onClick={counter.increment}>+</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Counter.Provider>
|
||||
<CounterDisplay />
|
||||
<Counter.Provider initialState={2}>
|
||||
<div>
|
||||
<div>
|
||||
<CounterDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</Counter.Provider>
|
||||
</Counter.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**可以看到,React Hooks 已经非常适合做状态管理,而生态应该做的事情是尽可能利用其能力进行模式化封装。**
|
||||
|
||||
> 有人可能会问,取数和副作用怎么办?`redux-saga` 和其他中间件都没有,这个数据流是不是阉割版?
|
||||
|
||||
首先我们看 Redux 为什么需要处理副作用的中间件。这是因为 `reducer` 是一个同步纯函数,其返回值就是操作结果中间不能有异步,且不能有副作用,所以我们需要一种异步调用 `dispatch` 的方法,或者一个副作用函数来存放这些 “脏” 逻辑。
|
||||
|
||||
而在 Hooks 中,我们可以随时调用 `useState` 提供的 `setter` 函数修改值,这早已天然解决了 `reducer` 无法异步的问题,同时也实现了 `redux-chunk` 的功能。
|
||||
|
||||
而异步功能也被 `useEffect` 这个 React 官方 Hook 替代。**我们看到这个方案可以利用 React 官方提供的能力完全覆盖 Redux 中间件的能力,对 Redux 库实现了降维打击,所以下一代数据流方案随着 Hooks 的实现是真的存在的**。
|
||||
|
||||
最后,相比 Redux 自身以及其生态库的理解成本(笔者不才,初学 Redux 以及其周边 middleware 时理解了好久),Hooks 的理解学习成本明显更小。
|
||||
|
||||
**很多时候,人们排斥一个新技术,并不是因为新技术不好,而是这可能让自己多年精通的老手艺带来的 “竞争优势” 完全消失。可能一个织布老专家手工织布效率是入门学员的 5 倍,但换上织布机器后,这个差异很快会被抹平,老织布专家面临被淘汰的危机,所以维护这份老手艺就是维护他自己的利益。希望每个团队中的老织布工人都能主动引入织布机。**
|
||||
|
||||
> 再看取数中间件,我们一般需要解决 **取数业务逻辑封装** 与 **取数状态封装**,通过 redux 中间件可以封装在内,通过一个 `dispatch` 解决。
|
||||
|
||||
其实 Hooks 思维下,利用 [swr](<[swr](https://github.com/dt-fe/weekly/blob/v2/128.%E7%B2%BE%E8%AF%BB%E3%80%8AHooks%20%E5%8F%96%E6%95%B0%20-%20swr%20%E6%BA%90%E7%A0%81%E3%80%8B.md)>) `useSWR` 一样能解决:
|
||||
|
||||
```jsx
|
||||
function Profile() {
|
||||
const { data, error } = useSWR("/api/user");
|
||||
}
|
||||
```
|
||||
|
||||
取数的业务逻辑封装在 `fetcher` 中,这个在 `SWRConfigContext.Provider` 时就已注入,还可以控制作用域!完全利用 React 提供的 Context 能力,可以感受到实现底层原理的一致性和简洁性,越简单越优美的数学公式越可能是真理。
|
||||
|
||||
而取数状态已经封装在 `useSWR` 中,配合 Suspense 能力,连 Loading 状态都不用关心了。
|
||||
|
||||
## 3 精读
|
||||
|
||||
### unstated
|
||||
|
||||
我们再梳理一下 `unstated` 这个库做了哪些事情。
|
||||
|
||||
1. 利用 `Provider` 申明作用范围。
|
||||
2. 提供 `Container` 作为可以被继承的类,继承它的 Class 作为 Store。
|
||||
3. 提供 `Subscribe` 作为 RenderProps 用法注入 Store,注入的 Store 实例由参数 `to` 接收到的 Class 实例决定。
|
||||
|
||||
对于第一点,`Provider` 在 Class Component 环境下要初始化 `StateContext`,这样才能在 `Subscribe` 中使用:
|
||||
|
||||
```jsx
|
||||
const StateContext = createReactContext(null);
|
||||
|
||||
export function Provider(props) {
|
||||
return (
|
||||
<StateContext.Consumer>
|
||||
{parentMap => {
|
||||
let childMap = new Map(parentMap);
|
||||
|
||||
if (props.inject) {
|
||||
props.inject.forEach(instance => {
|
||||
childMap.set(instance.constructor, instance);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<StateContext.Provider value={childMap}>
|
||||
{props.children}
|
||||
</StateContext.Provider>
|
||||
);
|
||||
}}
|
||||
</StateContext.Consumer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
对于第二点,对于 `Container`,需要提供给 Store `setState` API,按照 React 的 `setState` 结构实现了一遍。
|
||||
|
||||
值得注意的是,还存储了一个 `_listeners` 对象,并且可通过 `subscribe` 与 `unsubscribe` 增删。
|
||||
|
||||
`_listeners` 存储的其实是当前绑定的组件 `onUpdate` 生命周期,然后在 `setState` 时主动触发对应组件的渲染。`onUpdate` 生命周期由 `Subscribe` 函数提供,最终调用的是 `this.setState`,这个在 `Subscribe` 部分再说明。
|
||||
|
||||
以下是 `Container` 的代码实现:
|
||||
|
||||
```jsx
|
||||
export class Container<State: {}> {
|
||||
state: State;
|
||||
_listeners: Array<Listener> = [];
|
||||
|
||||
constructor() {
|
||||
CONTAINER_DEBUG_CALLBACKS.forEach(cb => cb(this));
|
||||
}
|
||||
|
||||
setState(
|
||||
updater: $Shape<State> | ((prevState: $Shape<State>) => $Shape<State>),
|
||||
callback?: () => void
|
||||
): Promise<void> {
|
||||
return Promise.resolve().then(() => {
|
||||
let nextState;
|
||||
|
||||
if (typeof updater === "function") {
|
||||
nextState = updater(this.state);
|
||||
} else {
|
||||
nextState = updater;
|
||||
}
|
||||
|
||||
if (nextState == null) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = Object.assign({}, this.state, nextState);
|
||||
|
||||
let promises = this._listeners.map(listener => listener());
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
if (callback) {
|
||||
return callback();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(fn: Listener) {
|
||||
this._listeners.push(fn);
|
||||
}
|
||||
|
||||
unsubscribe(fn: Listener) {
|
||||
this._listeners = this._listeners.filter(f => f !== fn);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对于第三点,`Subscribe` 的 `render` 函数将 `this.props.children` 作为一个函数执行,并把对应的 Store 实例作为参数传递,这通过 `_createInstances` 函数实现。
|
||||
|
||||
`_createInstances` 利用 `instanceof` 通过 Class 类找到对应的实例,并通过 `subscribe` 将自己组件的 `onUpdate` 函数传递给对应 Store 的 `_listeners`,在解除绑定时调用 `unsubscribe` 解绑,防止不必要的 renrender。
|
||||
|
||||
以下是 `Subscribe` 源码:
|
||||
|
||||
```jsx
|
||||
export class Subscribe<Containers: ContainersType> extends React.Component<
|
||||
SubscribeProps<Containers>,
|
||||
SubscribeState
|
||||
> {
|
||||
state = {};
|
||||
instances: Array<ContainerType> = [];
|
||||
unmounted = false;
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
_unsubscribe() {
|
||||
this.instances.forEach(container => {
|
||||
container.unsubscribe(this.onUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
onUpdate: Listener = () => {
|
||||
return new Promise(resolve => {
|
||||
if (!this.unmounted) {
|
||||
this.setState(DUMMY_STATE, resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_createInstances(
|
||||
map: ContainerMapType | null,
|
||||
containers: ContainersType
|
||||
): Array<ContainerType> {
|
||||
this._unsubscribe();
|
||||
|
||||
if (map === null) {
|
||||
throw new Error(
|
||||
"You must wrap your <Subscribe> components with a <Provider>"
|
||||
);
|
||||
}
|
||||
|
||||
let safeMap = map;
|
||||
let instances = containers.map(ContainerItem => {
|
||||
let instance;
|
||||
|
||||
if (
|
||||
typeof ContainerItem === "object" &&
|
||||
ContainerItem instanceof Container
|
||||
) {
|
||||
instance = ContainerItem;
|
||||
} else {
|
||||
instance = safeMap.get(ContainerItem);
|
||||
|
||||
if (!instance) {
|
||||
instance = new ContainerItem();
|
||||
safeMap.set(ContainerItem, instance);
|
||||
}
|
||||
}
|
||||
|
||||
instance.unsubscribe(this.onUpdate);
|
||||
instance.subscribe(this.onUpdate);
|
||||
|
||||
return instance;
|
||||
});
|
||||
|
||||
this.instances = instances;
|
||||
return instances;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StateContext.Consumer>
|
||||
{map =>
|
||||
this.props.children.apply(
|
||||
null,
|
||||
this._createInstances(map, this.props.to)
|
||||
)
|
||||
}
|
||||
</StateContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
总结下来,`unstated` 将 State 外置是通过自定义 Listener 实现的,在 Store `setState` 时触发收集好的 `Subscribe` 组件的 rerender。
|
||||
|
||||
### unstated-next
|
||||
|
||||
`unstated-next` 这个库只做了一件事情:
|
||||
|
||||
1. 提供 `createContainer` 将自定义 Hooks 封装为一个数据对象,提供 `Provider` 注入与 `useContainer` 获取 Store 这两个方法。
|
||||
|
||||
正如之前解析所说,`unstated-next` 可谓将 Hooks 用到了极致,认为 Hooks 已经完全具备数据流管理的全部能力,我们只要包装一层规范即可:
|
||||
|
||||
```jsx
|
||||
export function createContainer(useHook) {
|
||||
let Context = React.createContext(null);
|
||||
|
||||
function Provider(props) {
|
||||
let value = useHook(props.initialState);
|
||||
return <Context.Provider value={value}>{props.children}</Context.Provider>;
|
||||
}
|
||||
|
||||
function useContainer() {
|
||||
let value = React.useContext(Context);
|
||||
if (value === null) {
|
||||
throw new Error("Component must be wrapped with <Container.Provider>");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
return { Provider, useContainer };
|
||||
}
|
||||
```
|
||||
|
||||
可见,`Provider` 就是对 `value` 进行了约束,**固化了 Hooks 返回的 value 直接作为 `value` 传递给 `Context.Provider` 这个规范。**
|
||||
|
||||
而 `useContainer` 就是对 `React.useContext(Context)` 的封装。
|
||||
|
||||
真的没有其他逻辑了。
|
||||
|
||||
唯一需要思考的是,在自定义 Hooks 中,我们用 `useState` 管理数据还是 `useReducer` 管理数据的问题,这个是个仁者见仁的问题。不过我们可以对自定义 Hooks 进行嵌套封装,支持一些更复杂的数据场景,比如:
|
||||
|
||||
```jsx
|
||||
function useCounter(initialState = 0) {
|
||||
const [count, setCount] = useState(initialState);
|
||||
const decrement = () => setCount(count - 1);
|
||||
const increment = () => setCount(count + 1);
|
||||
return { count, decrement, increment };
|
||||
}
|
||||
|
||||
function useUser(initialState = {}) {
|
||||
const [name, setName] = useState(initialState.name);
|
||||
const [age, setAge] = useState(initialState.age);
|
||||
const registerUser = userInfo => {
|
||||
setName(userInfo.name);
|
||||
setAge(userInfo.age);
|
||||
};
|
||||
return { user: { name, age }, registerUser };
|
||||
}
|
||||
|
||||
function useApp(initialState) {
|
||||
const { count, decrement, increment } = useCounter(initialState.count);
|
||||
const { user, registerUser } = useUser(initialState.user);
|
||||
return { count, decrement, increment, user, registerUser };
|
||||
}
|
||||
|
||||
const App = createContainer(useApp);
|
||||
```
|
||||
|
||||
## 4 总结
|
||||
|
||||
借用 `unstated-next` 的标语:“never think about React state management libraries ever again” - 用了 `unstated-next` 再也不要考虑其他 React 状态管理库了。
|
||||
|
||||
而有意思的是,`unstated-next` 本身也只是对 Hooks 的一种模式化封装,Hooks 已经能很好解决状态管理的问题,我们真的不需要 “再造” React 数据流工具了。
|
||||
|
||||
> 讨论地址是:[精读《unstated 与 unstated-next 源码》 · Issue #218 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/218)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,218 @@
|
||||
## 1 引言
|
||||
|
||||
《从 0 到 1》是一本创业经典,创业非常有魅力,需要多种维度的商业知识,包括基础经济学、公司经济学、商业学、公司金融学、甚至历史学等等。
|
||||
|
||||
为什么要懂历史学?因为《从 0 到 1》这本书的作者是 彼得·蒂尔,他是 Paypal 的创始人和投资家,想读懂他的书就必须读懂他自己的创业经历,而 Paypal 的成长经历需要以考究历史的思维学习,了解什么是 Paypal 黑帮,他与其他公司的关系,为什么 Paypal 是继英特尔时隔 20 年之后的互联网黄埔军校。
|
||||
|
||||
为什么要懂商业学?本书第一句话就是 “在商业上机会只有一次”,这是商业基本准则之一。商业不是物理学,没有必然因果关系,没有商业必胜法。同时,商业也是训练多维度思考的战场,对一个商业结果的解读多种多样,我们需要避免对结果的简单归因、过度解读、甚至是本末倒置。《从 0 到 1》这本书抓住了创业成功的精髓。
|
||||
|
||||
《从 0 到 1》这本书,就是在商业这种复杂环境下,尝试总结一套通用的成功经验。然而前面我也说了,商业没有必胜法,那什么才是驱动成功与发展的根本引擎?**就是创新**。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
### 未来的挑战
|
||||
|
||||
什么人能胜任未来的挑战?彼得蒂尔认为,有创新能力的人可以,所以他面试时喜欢问:**“有什么你与其他人有不同看法,但你觉得却很重要的事”**。能真正回答好这个问题的人才算具备了基本创新能力。
|
||||
|
||||
人类技术演进分为 **水平进步与垂直进步**,水平进步是从 1 到 N 的规模化应用,而垂直进步是从 0 到 1 的创造,虽然水平进步可以给发展中国家带来巨大发展速度,但真正推动历史变革的还在于垂直进步。
|
||||
|
||||
对于创业团队,独立思考与速度很重要,因此团队规模要尽量小。彼得蒂尔对 Paypal 的管理理念重点有二:**招人越像越好、极端聚焦**,Paypal 在早期时隔工程师都是 UIUC 毕业的,5 个非技术人员都是彼得蒂尔在斯坦福校友网络认识的,背景非常趋同,因此沟通成本非常低,决策效率很高。彼得蒂尔要求员工的年终总结必须明确写出 “对公司最有价值的一个贡献”,只能写一个。
|
||||
|
||||
根据 Paypal 发展经历来看,难怪《从 0 到 1》这本书会强调小团队高灵活的重要程度,因为 Paypal 就是这么起家的。
|
||||
|
||||
### 像 1999 年那样狂欢
|
||||
|
||||
1993 年网景公司的成立拉开互联网时代的序幕,Paypal 就是这个时代成立的。
|
||||
|
||||
互联网狂欢兴起:
|
||||
|
||||
<img width=400 src="https://user-images.githubusercontent.com/7970947/69900559-e75c1e80-13af-11ea-8f60-c3c80ef2ab98.png">
|
||||
|
||||
互联网泡沫破裂:
|
||||
|
||||
<img width=350 src="https://user-images.githubusercontent.com/7970947/69900566-03f85680-13b0-11ea-86f5-8faf80ae546d.png">
|
||||
|
||||
自 1999 年之后,市场学会了保守,主要有四条:
|
||||
|
||||
1. 循序渐进的发展。
|
||||
2. 保持精简和灵活。
|
||||
3. 不要贸然开辟新市场。
|
||||
4. 专注产品而不是营销。
|
||||
|
||||
显然,1999 年互联网泡沫破裂后的美国企业家害怕了,逐渐走向了保守。**然而彼得蒂尔认为,1999 年互联网泡沫破裂的虽然惨烈,但正因如此才带来了美国未来几十年的增长。** 保守无法带来成功,相反,这四条的反面反而更正确:
|
||||
|
||||
1. 大胆尝试胜过平庸保守。
|
||||
2. 坏计划也好过没有计划。
|
||||
3. 竞争性市场对收益有负面影响。
|
||||
4. 营销和产品同样重要。
|
||||
|
||||
**狂妄自大的尝试必定导致大部分人悲惨的失败,但我们别无选择,创业必须创新,必须实现从 0 到 1。** 所以彼得蒂尔反直觉的观点就是,我们不能因为吸取 1999 年的教训就变得保守,反而美国需要 1999 年那股狂热驱动新的创新。
|
||||
|
||||
### 所有成功的企业都是不同的
|
||||
|
||||
彼得蒂尔完美解释了垄断的价值。
|
||||
|
||||
市场分为充分竞争与完全垄断,看上去充分竞争的市场更有活力,更健康,但实则不然。**充分竞争将利润完全吞噬,只有完全垄断才能获得持久价值,最终对市场有利。**
|
||||
|
||||
对创业者来说也一样,如果你相信充分竞争,你只会创建一家同质化的公司,扎到红海里拼命挣扎,这不会给你带来持久的利益,也不会给市场带来真正的发展。
|
||||
|
||||
垄断者为了逃避垄断保护法,会竭尽全力证明自己没有取得垄断地位(甚至随时会被市场吃掉),同理,**竞争者为了自我麻痹或争取到投资,也会竭尽全力证明自己还有机会,市场并未形成垄断。** 然而无论怎么说,真正为市场创造独一无二价值的还是垄断者,虽然他们看起来很可恶。
|
||||
|
||||
不仅在商业如此,互联网公司内部技术竞争也一样:**低水平的重复竞争挑战者会竭尽全力证明自己所在的领域不存在垄断,然后投入人力做一个注定会失败的项目,不仅无法为公司产生新的价值,还带来了资源内耗。相反,那个垄断者才是为公司源源不断带来价值的引擎,虽然竞争者们都厌恶它。这也是为什么阿里鼓励高水平竞争,禁止低水平重复轮子。**
|
||||
|
||||
### 竞争意识
|
||||
|
||||
大家觉得竞争理所应当,但其实竞争更多带来的是伤害。
|
||||
|
||||
在奇葩说里听到薛兆丰这么一句话:“求职者你们的竞争对手不是企业,而是其他求职者”。说的很有道理,真正的伤害是在竞争中产生的,而存在供需关系的公司与求职者之间哪存在什么竞争?直白一点说,如果整个市场只有一个应聘者,哪怕小学没毕业,阿里腾讯也会抢着要。
|
||||
|
||||
**竞争使我们过度看中过去的机会,而忽略创造新的可能性。** 就像 Paypal 与 X 合并一样,彼得蒂尔发现这两家公司的竞争关系是恶性的,只有合并后形成垄断才能创造新的价值。而 X 公司的创始人就是埃隆·马斯克,虽然最后因为极力推广 X 品牌被合并后的 Paypal 请出局后,依然在 Paypal 被 20 多亿美元收购后,获得了一亿多美元回报,才创建了特斯拉和太空探索公司,真正为社会创造新的价值。
|
||||
|
||||
### 后发优势
|
||||
|
||||
既然垄断如此重要,那么如何打造垄断?
|
||||
|
||||
**首先一个企业的价值是它未来创造利润的总和**。也许你会奇怪,为什么企业现在的资产不算做企业价值呢?企业价值一般指的是企业市值,企业市值描述的企业价值其实是它的 **当前投资价值**,一个不能在未来创造利润的企业,就算现在坐拥几千亿美元的资产,对你来说也是没有投资价值的。
|
||||
|
||||
建立企业垄断,可以建立企业的护城河,比如专利技术或者网络效应;或者先进入小市场,逐步扩大范围,就像亚马逊从图书在线交易切入,随后扩张到全品类。与你的对手产生放大收益,你不能仅仅取代你的对手,最好能为它赋能。这些都是企业的后发优势。
|
||||
|
||||
### 成功不是中彩票
|
||||
|
||||
虽然大部分成功创业者都会将一半功劳归功于运气,但你最好不要真的相信,否则为什么有那么多连续失败的创业者呢?如果创业需要运气,那为什么彼得蒂尔要写《从 0 到 1》这本书,为什么我还要精读它呢?
|
||||
|
||||
**成功者的运气是靠努力换来的**。
|
||||
|
||||
国家就是一个巨大的创业,彼得蒂尔对当下各国对未来看法划出了四象限图:
|
||||
|
||||
<img width=400 src="https://user-images.githubusercontent.com/7970947/69900946-35275580-13b5-11ea-880b-63403fa154f5.png">
|
||||
|
||||
- 明确乐观的未来:1950~1970 的美国,当时美国创新能力和工程应用都在上升期,未来是明确且乐观的。
|
||||
- 不明确乐观的未来:1982 至今的美国,由于技术发展遇到了瓶颈,比如生物制药和医疗都有巨大不确定性,人们只知道未来是美好的,但不知道何时可以到来。
|
||||
- 明确悲观的未来:**现在的中国,由于缺乏核心创新能力,现在中国迅猛发展其实在吃发达国家创新的红利,只是将这些技术规模化应用,所以发展方向是明确的,但一旦红利吃完,不确定自己是否能找到新的突破点,因此对未来是悲观的。**
|
||||
- 不明确悲观的未来:现在的欧洲,技术红利和规模化都吃完了,不知道未来该怎么走,也不知道走向哪里。
|
||||
|
||||
不论国家还是公司,在这个时代想要拥有最好的未来,就是不明确乐观的未来,虽然这个乐观是不明确的,也就是需要运气,但只要在正确的方向努力,总是可能会成功。如果你真的相信比尔盖兹成功来源于运气,那请理解这是一个明确的运气,而不是不明确的运气,并不是所有方向的创业都可能走向成功。
|
||||
|
||||
### 向钱看
|
||||
|
||||
当爱因斯坦宣称复利是“世界第八大奇迹”,因为钱可以生钱,本质原因是指数级增长。指数级增长之所以如此可怕,还因为并没有证据表明爱英斯坦说过这句话,但因为他的影响力有指数级影响力,所有有影响力的话可能都会 “归功给他”。
|
||||
|
||||
风险投资领域也是如此,一家风投最成功的项目带来的收益可能超过其他所有项目的总和,所以风投才会不断给有发展潜力的企业加注,这都是因为指数级效应。
|
||||
|
||||
所以如果你创业的公司不能成为幂次法则指数增长的类型,最好尽快换一个项目,因为做一个平庸的项目是没有意义的,世界的天枰都会为头部项目加码。
|
||||
|
||||
### 秘密
|
||||
|
||||
企业只有创新才能获得成功,那一定是发现了新的 “商业秘密”。
|
||||
|
||||
但现在社会发展遇到了瓶颈,大家都不愿意探索新的秘密,主要有四个原因:
|
||||
|
||||
1. 认为已经没有新的秘密。就像探索世界一样,当地球完全被开发,已经没有探索的必要。
|
||||
2. 规避风险。害怕没有找到秘密而耽误自己的人生。
|
||||
3. 自满。安于现状,认为不需要探寻新的秘密。
|
||||
4. 扁平化。由于互联网对社会的连接,我们更容易觉得竞争是全球化的,如果有新的秘密,一定会更优秀的人发现,而显然我不是最优秀的人,所以我没有必要去发觉秘密,那些最优秀的人会帮我做到。
|
||||
|
||||
想要扭转这个悲观思想,**你需要意识到现代分工是极度专业化的,不同领域间往往很难竞争**,一个物理学家可能难于解决情感问题,要相信还有许多未被关注的细分领域可能存在蓝海。
|
||||
|
||||
### 基础决定命运
|
||||
|
||||
就像宪法决定了国家基础一样,企业最初决定的重要思想对未来发展起到决定因素,比如行业方向与招聘要求。
|
||||
|
||||
因此初创公司一定要确保创始人团队之间是否有默契,所有权、经营权和控制权是否分配合理,不要有兼职员工,最好以股权激励员工。
|
||||
|
||||
在技术领域做架构设计也是如此,架构基础决定了未来发展命运,我们必须尽可能保证早期架构设计的合理性,并坚持这些原则,就像坚持宪法一样。
|
||||
|
||||
### 黑手党式的机制
|
||||
|
||||
为什么 Paypal 早期员工被称为 Paypal 黑帮?其实彼得蒂尔创建的 Paypal 由于触及到金融领域,相关利益方非常复杂,对于没有政府背景的他来说几乎是不可能做成的。
|
||||
|
||||
Paypal 招来的早期员工必须极度认同其企业文化,认同 “创造虚拟货币代替美元” 这个疯狂的想法。
|
||||
|
||||
**Paypal 黑帮对公司的使命有着近乎于 “邪教” 般的信仰,唯一区别是,他们做的事情本身并不坏。**
|
||||
|
||||
### 顾客不会自动上门
|
||||
|
||||
销售和技术同样重要。
|
||||
|
||||
在工程技术界,技术打造的产品功能界限清晰,不是生效就是失效,而销售界,需要通过精心设计活动来打动用户的芳心,但却不能改变产品的实质性内容。技术内容是务实的,销售内容是务虚的,但我们不能说务实一定比务虚重要。
|
||||
|
||||
销售的技巧也随着业务场景的不同而不同。
|
||||
|
||||
- 复杂营销。当面对大企业客户时,甚至要克服政治惰性说服政府太空飞船采用你们公司的技术,而一旦完成协议的签署,哪怕只有几单,也足够维持公司后续发展了。
|
||||
- 人员营销。和复杂营销相反,需要从具体场景逐渐深入,比如 Box 公司的云存储服务,首先卖给了斯坦福睡眠诊所,之后逐步扩展到整个斯坦福大学,但如果 Box 一开始就和斯坦福的校长洽谈整个学校的云服务方案,可能一开始就会失败。
|
||||
- 病毒式营销。Paypal 的增长过程就是病毒式营销的范例,通过邀请机制传播给好友,并给最多 20 美元的奖励,也就是获客成本 20 元支撑了 Paypal 病毒式营销的成立。
|
||||
|
||||
然而 Paypal 也不是漫无目的的砸钱,首先它砸钱有自己的原因,因为 Paypal 是一个拥有网络效应的项目,因此拥有越多的用户就能带来越多的未来价值,这是 Paypal 可以选择烧钱营销的最大原因。
|
||||
|
||||
其次 Paypal 也选择了两个聪明的营销方式,第一是通过邮箱营销,由于当时世界上拥有邮箱的用户很少,都是一些对新技术持有开放态度的用户,因此邮件营销的人群就比较正确。后来 Paypal 发现,eBay 有部分商家甚至主动在商户页面贴出注册 Paypal 的链接,不仅是为了赚取佣金,更因为 Paypal 网络支付的最大场景就是电商交易平台,因此后续 Paypal 重点转向 eBay 推广。
|
||||
|
||||
### 人类和机器
|
||||
|
||||
**机器未来并不是为了取代人类,而是辅助人类更高效工作。** 在 [精读《刷新》](https://github.com/dt-fe/weekly/blob/v2/116.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%88%B7%E6%96%B0%E3%80%8B.md#%E5%85%B6%E5%AE%83) 中,微软 CEO 萨提亚·纳德拉也提到了人与机器的关系 - “机器替代人类工作的过程,也是人类逐渐拾回作为人的尊严的过程。人本就应该将时间用于思考与创造,而不是重复性劳动。”
|
||||
|
||||
有意思的是,彼得蒂尔在创立 Paypal 过程中由于遇到不法分子盗刷信用卡的问题,因此专门研究网络安全并研发出验证码、数据分析等一直沿用至今的重要网络安全技术,甚至在 Paypal 被 eBay 收购后,彼得蒂尔还专门成立了 Clarium Capital 公司为政府提供安全服务,其核心技术就是在 Paypal 期间为了对抗支付安全问题时打下基础的。
|
||||
|
||||
所以彼得蒂尔在思考机器和人类关系时,会重点关注机器帮助人类提升价值的领域。其中有一句话触达了问题本质:**“机器不会有利己的诉求,因此价值最终会转移至人类”。** 只要机器永远不要求自我价值的实现,人类和机器就能和平共处下去。
|
||||
|
||||
### 绿色能源与特斯拉
|
||||
|
||||
由于彼得蒂尔与埃隆·马斯克曾经互为敌友关系,因此就关注到了特斯拉与绿色能源的问题。
|
||||
|
||||
彼得蒂尔认为,绿色能源技术要思考好如下 7 个问题:
|
||||
|
||||
1. 工程问题,如果一个新技术不能带来本质的突破,那么其未来增长价值就不明显,公司的未来也不够清晰,狂热的投资注定引发泡沫。新能源技术目前带来的提升不是数倍的,因此前景不明确,无法说服大家一定去用这个产品。
|
||||
2. 时机问题,目前新能源领域技术并没有质的突破,现在进入注定面临技术储备不足的问题。
|
||||
3. 垄断问题,新能源技术是否能够垄断?新能源公司可能在故意隐瞒自己在市场中的渺小程度,其实相对于全球能源市场,新能源只是很小的子版块,整个行业总市值可能都不大。
|
||||
4. 人员问题。新能源是个技术问题,但现在融资需要 CEO 们西装革履的到处募集资金,这是严重的人员问题。
|
||||
5. 销售问题。人们对新能源领域、新能源汽车的接受程度有多大?是否足够便捷?
|
||||
6. 持久问题。随着中国在新能源市场的加入,导致美国新能源企业增长疲软,所以指责中国的声音很多。这是个危险的信号,如果成为垄断者需要以指责的方式进行,注定会失败。另外化石燃料随着液压破碎法的成熟,导致 2008 年天然气价格下降了 70% 多,新能源已不再是解决能源问题的唯一破局方式。
|
||||
7. 秘密问题。节省能源是一个政治正确的问题,大家都在呼吁要环保,那么这就证明环保项目一定有市场?不一定。
|
||||
|
||||
特斯拉的成功是因为解决了这 7 个问题,并且从实际的小领域切入,并且和政府以及其他企业达成了技术合作。这说明,在能源 2.0 市场中,企业面临的主要挑战是如何找到一个正确的小型市场。
|
||||
|
||||
### 创始人的悖论
|
||||
|
||||
这个章节,彼得蒂尔分析了各种名人或创业者的特质,内容非常丰富,由于篇幅限制就不展开了,而且由于笔者在这方面缺乏相应的阅历,很难原汁原味的还原出他对每个名人的评价,因此细节还是推荐阅读原文。
|
||||
|
||||
以下只能做简单的总结,只能理解到其中部分思想:
|
||||
|
||||
1. 伟人都拥有矛盾的两面性,企业需要极端的创始人,平庸的人往往很难成为好的创始人。
|
||||
2. 伟人的两面性与其成功路径存在相互塑造的过程,很难说是因为存在矛盾才导致了其成功,还是在成功的过程中塑造了其矛盾的性格。
|
||||
3. 伟人往往都会亲手终结自己的良好形象,除非英年早逝。
|
||||
|
||||
当然,这并不是说为了成功,我们必须成为这样的人,这个章节只是对创始人悖论这个现象的一种解读,可能这是一种自然现象,我们不需要模仿,只需要理解。
|
||||
|
||||
### 对未来的预期
|
||||
|
||||
哲学家尼克·博斯特罗姆描述了四种预测未来的理论:
|
||||
|
||||
1. 兴衰交替。由于历史总是呈现繁荣与衰败的交替,因此未来也很可能逃不出这个循环。
|
||||
2. 未来稳定发展。按照当今世界发展节奏,最后所有国家都进入发达国家行列,人民生活水平整体提高。
|
||||
3. 毁灭性衰落。由于地缘政治原因,未来不可避免会发生毁灭性冲突,人类文明可能呈断崖式下跌。
|
||||
4. 奇点。非常难以预测的加速发展,以至于发展到现在人类难以理解的高度。因为这个概念本身突出的就是 “发展到难以理解的高度”,因此试图去理解它的思考都反而会偏题,因此把它当作一种无法预测的未来吧。
|
||||
|
||||
笔者发现,现代大师人物写的书,最后都有对未来的预测,而且大家对未来的预测不同与书籍观点间的差异,往往都是很趋同的,这到底是英雄所见略同还是人类顶级大脑能到达的高度已经达到天花板?这是一个开放问题。
|
||||
|
||||
最后,保持独立思考是我们能重构世界的最佳方式。
|
||||
|
||||
## 4 总结
|
||||
|
||||
那到底什么是创新?巴菲特说过,商业最重要的是护城河,护城河不是什么产品质量、高素质员工、巨大的市场份额。真正的护城河是:**企业无形资产比如品牌、高客户转换成本、成本优势、网络效应**。Paypal 创新的找到了符合网络效应的业务场景:“网络货币”。
|
||||
|
||||
为什么 “网络货币” 拥有网络效应呢?所谓网络效应是指,每新增一个用户,就会对产品价值带来指数级提升。支付网络每增加一个人,不但你可以参与交易,还让交易网络变得更大,让更多交易成为可能,甚至成为全球通用货币,获得比国家货币更强的流通性,而这个质变只需要更多的用户加入即可,这就是它的网络效应。
|
||||
|
||||
《从 0 到 1》是一本创新思维的启蒙书,但想要深入理解这本书提供的概念,基本的经济学、商业知识是必不可少的,至少要理解到创新指的是为企业构筑护城河,而网络效应是 Paypal 的一个重要护城河。
|
||||
|
||||
类似拥有网络效应的还有 Uber 和 Airbnb,但他们创新思维不同,导致网络效应的大小也不同。Airbnb 的网络效应是全球的,因为场景天然是 “旅游时自有房屋出租”,每成交一对商家与客户,都可能是跨地区的,而且客户也有自己的房子,可能下次自己就会成为商家。而 Uber 业务场景天然是同城的叫车服务,因此无法形成全球的网络效应壁垒,这也是为什么 Uber 无法竞争过中国的滴滴,但 Airbnb 的全球市场地位无人能撼动。
|
||||
|
||||
商业领域远远不止于此,研究商业就像研究历史,每个公司都能给我们带来巨大启发。而商业最迷人的地方就在它的非必然性,就算你反复研究历史,熟读《从 0 到 1》这本书,他也无法给你带来必胜的商业操作路径。但这本书真正能带来的是正确而成功的信念,只要确定你的方向是正确的 “创新”,至少你可以正视失败,坦然开启下一段创业旅程,而说不定哪一次就成功了呢。
|
||||
|
||||
> 讨论地址是:[精读《从 0 到 1》 · Issue #219 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/219)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,262 @@
|
||||
## 1 引言
|
||||
|
||||
搭配了合适的设计模式的代码,才可拥有良好的可维护性,[The Benefits of Orthogonal React Components](https://dmitripavlutin.com/orthogonal-react-components/) 这篇文章就重点介绍了正交性原理。
|
||||
|
||||
所谓正交,即模块之间不会相互影响。想象一个音响的音量与换台按钮间如果不是正交关系,控制音量同时可能影响换台,这样的设备很难维护:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1dczIpQL0gK0jSZFtXXXQCXXa-1000-993.png">
|
||||
|
||||
前端代码也一样,UI 与数据处理逻辑分离就是一种符合正交原则的设计,这样有利于长期代码质量维护。
|
||||
|
||||
## 2 概述
|
||||
|
||||
一个拥有良好正交性的 React App 会按照如下模块分离设计:
|
||||
|
||||
1. UI 元素(展示型组件)。
|
||||
2. 取数逻辑(fetch library, REST or GraphQL)。
|
||||
3. 全局状态管理(redux)。
|
||||
4. 持久化(local storage, cookies)。
|
||||
|
||||
文中通过两个例子说明。
|
||||
|
||||
### 让组件与取数逻辑正交
|
||||
|
||||
比如一个展示雇员列表组件 `<EmployeesPage>`:
|
||||
|
||||
```jsx
|
||||
import React, { useState } from "react";
|
||||
import axios from "axios";
|
||||
import EmployeesList from "./EmployeesList";
|
||||
|
||||
function EmployeesPage() {
|
||||
const [isFetching, setFetching] = useState(false);
|
||||
const [employees, setEmployees] = useState([]);
|
||||
|
||||
useEffect(function fetch() {
|
||||
(async function() {
|
||||
setFetching(true);
|
||||
const response = await axios.get("/employees");
|
||||
setEmployees(response.data);
|
||||
setFetching(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (isFetching) {
|
||||
return <div>Fetching employees....</div>;
|
||||
}
|
||||
return <EmployeesList employees={employees} />;
|
||||
}
|
||||
```
|
||||
|
||||
这样设计看上去没问题,但其实违背了正交原则,因为 `EmployeesPage` 既负责渲染 UI 又关心取数逻辑。正交的写法如下:
|
||||
|
||||
```jsx
|
||||
import React, { Suspense } from "react";
|
||||
import EmployeesList from "./EmployeesList";
|
||||
|
||||
function EmployeesPage({ resource }) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Fetching employees....</h1>}>
|
||||
<EmployeesFetch resource={resource} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function EmployeesFetch({ resource }) {
|
||||
const employees = resource.employees.read();
|
||||
return <EmployeesList employees={employees} />;
|
||||
}
|
||||
```
|
||||
|
||||
**`Suspense` 将 loading 状态剥离到父级组件,因此子组件只需要关心如何用数据,不需关心如何取数据(以及 loading 态)。**
|
||||
|
||||
### 让组件与滚动监听正交
|
||||
|
||||
比如一个滚动到一定距离就出现 "jump to top" 的组件 `<ScrollToTop>`,可能会这么实现:
|
||||
|
||||
```jsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
const DISTANCE = 500;
|
||||
|
||||
function ScrollToTop() {
|
||||
const [crossed, setCrossed] = useState(false);
|
||||
|
||||
useEffect(function() {
|
||||
const handler = () => setCrossed(window.scrollY > DISTANCE);
|
||||
handler();
|
||||
window.addEventListener("scroll", handler);
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
}, []);
|
||||
|
||||
function onClick() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
|
||||
if (!crossed) {
|
||||
return null;
|
||||
}
|
||||
return <button onClick={onClick}>Jump to top</button>;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,在这个组件中,按钮与滚动状态判断逻辑混合在了一起。如果我们将 “滚动到一定距离就渲染 UI” 抽象成通用组件 `IfScrollCrossed` 呢?
|
||||
|
||||
```jsx
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function useScrollDistance(distance) {
|
||||
const [crossed, setCrossed] = useState(false);
|
||||
|
||||
useEffect(
|
||||
function() {
|
||||
const handler = () => setCrossed(window.scrollY > distance);
|
||||
handler();
|
||||
window.addEventListener("scroll", handler);
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
},
|
||||
[distance]
|
||||
);
|
||||
|
||||
return crossed;
|
||||
}
|
||||
|
||||
function IfScrollCrossed({ children, distance }) {
|
||||
const isBottom = useScrollDistance(distance);
|
||||
return isBottom ? children : null;
|
||||
}
|
||||
```
|
||||
|
||||
有了 `IfScrollCrossed`,我们就能专注写 “点击按钮跳转到顶部” 这个 UI 组件了:
|
||||
|
||||
```jsx
|
||||
function onClick() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
|
||||
function JumpToTop() {
|
||||
return <button onClick={onClick}>Jump to top</button>;
|
||||
}
|
||||
```
|
||||
|
||||
最后将他们拼装在一起:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
|
||||
// ...
|
||||
|
||||
const DISTANCE = 500;
|
||||
|
||||
function MyComponent() {
|
||||
// ...
|
||||
return (
|
||||
<IfScrollCrossed distance={DISTANCE}>
|
||||
<JumpToTop />
|
||||
</IfScrollCrossed>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这么做,我们的 `<JumpToTop>` 与 `<IfScrollCrossed>` 组件就是正交关系,而且逻辑更清晰。不仅如此,这样的抽象使 `<IfScrollCrossed>` 可以被其他场景复用:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
|
||||
// ...
|
||||
|
||||
const DISTANCE_NEWSLETTER = 300;
|
||||
|
||||
function OtherComponent() {
|
||||
// ...
|
||||
return (
|
||||
<IfScrollCrossed distance={DISTANCE_NEWSLETTER}>
|
||||
<SubscribeToNewsletterForm />
|
||||
</IfScrollCrossed>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Main 组件
|
||||
|
||||
上面例子中,`<MyComponent>` 就是一个 Main 组件,Main 组件封装一些脏逻辑,即它要负责不同模块的组装,而这些模块之间不需要知道彼此的存在。
|
||||
|
||||
一个应用会存在多个 Main 组件,它们负责拼装各种作用域下的脏逻辑。
|
||||
|
||||
### 正交设计的好处
|
||||
|
||||
- **容易维护:** 正交组件逻辑相互隔离,不用担心连带影响,因此可以放心大胆的维护单个组件。
|
||||
- **易读:** 由于逻辑分离导致了抽象,因此每个模块做的事情都相对单一,很容易猜测一个组件做的事情。
|
||||
- **可测试:** 由于逻辑分离,可以采取逐个击破的思路进行单测。
|
||||
|
||||
### 权衡
|
||||
|
||||
如果不采用正交设计,因为模块之间的关联导致应用最终变得难以维护。但如果将正交设计应用到极致,可能会多处许多不必要的抽象,这些抽象的复用仅此一次,造成过度设计。
|
||||
|
||||
## 3 精读
|
||||
|
||||
正交设计一定程度可以理解为合理抽象,完全不抽象与过度抽象都是不可取的,因此列举了四块需要抽象的要点:UI 元素、取数逻辑、全局状态管理、持久化。
|
||||
|
||||
全局状态管理注入到组件,就是一种正交的抽象模式,即组件不用关心数据从哪来,而直接使用数据,而数据管理完全交由数据流层管理。
|
||||
|
||||
取数逻辑往往是可能被忽略的一环,无论是像原文中直接关心到 `fetch` 方法的 UI 组件,还是利用取数工具库关心了 `loading` 状态:
|
||||
|
||||
```jsx
|
||||
import useSWR from "swr";
|
||||
|
||||
function Profile() {
|
||||
const { data, error } = useSWR("/api/user", fetcher);
|
||||
|
||||
if (error) return <div>failed to load</div>;
|
||||
if (!data) return <div>loading...</div>;
|
||||
return <div>hello {data.name}!</div>;
|
||||
}
|
||||
```
|
||||
|
||||
虽然将取数生命周期封装到自定义 hook `useSWR` 中,但 `error` 信息对 UI 组件来说就是一个脏数据:**这让这个 UI 组件不仅要渲染数据,还要担心取数是否会失败,或者是否在 loading 中。**
|
||||
|
||||
好在 Suspense 模式解决了这个问题:
|
||||
|
||||
```jsx
|
||||
import { Suspense } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
function Profile() {
|
||||
const { data } = useSWR("/api/user", fetcher, { suspense: true });
|
||||
return <div>hello, {data.name}</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<div>loading...</div>}>
|
||||
<Profile />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这样 `<Profile>` 只要专注于做数据渲染,而不用担心 `useSWR('/api/user', fetcher, { suspense: true })` 这个取数过程发生了什么、是否取数失败、是否在 `loading` 中。因为取数状态由 `Suspense` 管理,而取数是否意外失败由 `ErrorBoundary` 管理。
|
||||
|
||||
合理的抽象使组件逻辑变得更简单,从而组件嵌套使用使不用担心额外影响。尤其在大型项目中,不要担心正交抽象会使本来就很多的模块数量再次膨胀,因为相比于维护 100 个相互影响,内部逻辑复杂的模块,维护 200 个职责清晰,相互隔离的模块也许会更轻松。
|
||||
|
||||
## 4 总结
|
||||
|
||||
从正交设计角度来看,`Hooks` 解决了状态管理与 UI 分离的问题,`Suspense` 解决了取数状态与 UI 分离的问题,`ErrorBoundary` 解决了异常与 UI 分离的问题。
|
||||
|
||||
在你看来,React 还有哪些逻辑需要与 UI 分离?分别使用哪些方法呢?欢迎留言。
|
||||
|
||||
> 讨论地址是:[精读《正交的 React 组件》 · Issue #221 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/221)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,207 @@
|
||||
## 1 引言
|
||||
|
||||
[尤雨溪](https://github.com/yyx990803) 在 2019 JSConf 的分享 [Seeking the Balance in Framework Design](https://www.youtube.com/watch?v=ANtSWq-zI0s) 十分精彩,道出了如何进行合理的前端框架设计与框架选型。
|
||||
|
||||
正如所说,框架对比不能只停留在 Star 数量、Npm 下载量、Stackoverflow 问题量这些简单的数据对比,而要深入到技术细节进行比较。比较框架有多种不同维度,这次分享就从服务范围、渲染机制、状态机制这三个维度进行对比。
|
||||
|
||||
## 2 概述
|
||||
|
||||
这次分享的精彩之处在于不偏不倚的站在客观立场分析了框架各维度好的一面与坏的一面,从中我们不仅能学习到一些框架知识,还能培养思辨能力。
|
||||
|
||||
### 服务范围
|
||||
|
||||
服务范围是个比较难翻译的单词,在原 PPT 中用了 “Scope” 这个单词表示,可以理解为 “作用域、框架的承诺功能范围、服务配套齐全程度”。比如提供的是一个工具库还是整体框架,插件管理是集中式还是依赖生态。
|
||||
|
||||
React 是典型的小服务范围框架,核心包只实现了基本功能,而其他生态基本靠社区拓展;Angular 是典型大服务范围框架,官方对所有业务场景都做了最佳实践能力覆盖;Vue 处在中间区域,通过功能分层,既拥有小服务范围的能力,又可以搭配官方插件实现更多场景化能力。
|
||||
|
||||
#### 小服务范围优势
|
||||
|
||||
**概念少,易上手**
|
||||
|
||||
小的服务范围代表了小的学习成本,因为暴露的基本能力较少,概念也会比较少,对新人上手比较友好。
|
||||
|
||||
**生态繁荣,百花齐放**
|
||||
|
||||
由于很多功能没有被官方实现,社区就有机会填补这些空白,因此会冒出许多第三方库,而且一旦做得好,就有机会成为 “事实标准”,因此开发者会更加积极参与到社区开发,自己做的框架 “上升空间” 也非常大。
|
||||
|
||||
同时,社区的力量会导致多元化,因此整体生态完整度与创新性都会非常亮眼,而且具有持续迭代的能力。
|
||||
|
||||
**核心维护成本低**
|
||||
|
||||
官方维护的核心代码较少,因此维护成本大大降低,而且官方可以将精力放在更多核心能力增强上,比如 Suspense 等,而不是将精力消耗在生态插件上。
|
||||
|
||||
#### 小服务范围的劣势
|
||||
|
||||
**复杂场景要引入新概念**
|
||||
|
||||
复杂场景无法支持时,就要引入新的概念解决,这导致后续技术选型可能产生分歧,并带来持续的新概念理解成本。
|
||||
|
||||
**非官方的开发模式逐渐产生**
|
||||
|
||||
随着时间的流逝,会逐渐涌出一些新的设计模式,成为当下几乎是必不可少的方案,但却不会出现在官方文档中,造成选型时的疑惑。Redux 就是一个例子。
|
||||
|
||||
**生态变化快,碎片化且持续流失**
|
||||
|
||||
非官方的生态也意味着不稳定,而且缺乏统一的管理,碎片化的模块之间可能经常出现不兼容的问题。
|
||||
|
||||
而且任何模块都可能被时代无情的淘汰,就像 Flux 到 Redux 再到 Hooks,带来额外的迁移成本和认知成本。谁也不希望自己的项目架构 “变得过时”,或者随时面临被新架构取代的风险,但第三方社区几乎一定代表未来会出现一种模式取代现有模式,只是时间早晚而已。
|
||||
|
||||
#### 大服务范围的优势
|
||||
|
||||
**大部分业务场景都被内置解决**
|
||||
|
||||
减少不必要的技术方案调研与纷争,大服务范围的框架内置的方案就能解决几乎 100% 业务问题,团队再也不会为通用架构问题烦恼了。
|
||||
|
||||
**生态稳定、连贯**
|
||||
|
||||
稳定是指,官方维护作为背书,几乎不会存在一些生态包突然不维护、与已有版本不兼容、被植入恶意程序等等意外情况。
|
||||
|
||||
连贯是指,官方会统一考虑一个改动在所有生态插件造成的影响,并以一个最合理的思路做整体改造,生态包无论是接口还是兼容性都不需要担心,设计思路也会一脉相承。
|
||||
|
||||
#### 大服务范围的劣势
|
||||
|
||||
**前期上手成本高**
|
||||
|
||||
全家桶的概念导致上手难度偏高,因为必须理解所有内置概念后才能开始项目。
|
||||
|
||||
**如果内置模块无法满足业务,会觉得有些死板**
|
||||
|
||||
一旦发生内置功能无法满足业务的场景,就很难拓展了,因为 all in one 的思路本质上就是排斥自定义拓展的,这点从 [angular-cli](https://github.com/angular/angular-cli) 就能看出来。
|
||||
|
||||
之所以觉得死板,是因为这种情况没办法用优雅的方式解决,只能在现有约束的框架内通过某些 “Hack” 方式解决,自然会有种死板的感觉。
|
||||
|
||||
#### 中等服务范围的优势
|
||||
|
||||
**分层设计,允许新特性渐进加入**
|
||||
|
||||
Vue 通过分层设计做到了折中,即官方还是会维护生态,只不过生态不是必须的,可以按需使用。这样做的好处是兼顾了一些优势。
|
||||
|
||||
**低学习门槛**
|
||||
|
||||
与小服务范围框架一样,对于核心包来说学习成本都比较低。
|
||||
|
||||
**依然有最佳实践解决所有业务问题**
|
||||
|
||||
和大服务范围框架一样,拥有全套官方最佳实践,但不内置,不强求一定要使用,因此你可以按需使用。
|
||||
|
||||
#### 中等服务范围的劣势
|
||||
|
||||
**维护成本高**
|
||||
|
||||
和大服务范围框架一样,虽然生态不强求,但毕竟官方还是要持续维护的,因此维护成本高的问题依然存在。
|
||||
|
||||
**生态多样性不高**
|
||||
|
||||
虽然生态是按需的,但毕竟中等服务范围的框架官方会实现一套标准生态插件,这会极大影响社区生态的发展空间,导致 “非官方插件没人愿意做”,因此生态多样性会差一些。
|
||||
|
||||
### 渲染机制
|
||||
|
||||
渲染机制区别主要在 JSX vs Template 之间,不同的表达方式之间还是存在一些很本质的区别,然而正如一开始所说,无法一言蔽之,必须从多个角度拆解的看。
|
||||
|
||||
#### JSX 的优势
|
||||
|
||||
**纯 JS 表达 UI**
|
||||
|
||||
单这一点就非常重要了,满足了 All In Js 的幻想。毕竟 Html、Css 相比 Js 来说,模块化能力和灵活性都很弱,将其都收敛到 Js 不仅表达方式更统一,更重要的是都获得了与 Js 一样的模块化、灵活性、Typescript 支持等能力。
|
||||
|
||||
**视图即数据**
|
||||
|
||||
将视图看作一种数据,让针对视图的逻辑测试成为可能。
|
||||
|
||||
同时也将视图概念泛化了,因为数据是平台无关的,一份描述视图的 DSL 可以运行在任何平台。
|
||||
|
||||
#### JSX 的劣势
|
||||
|
||||
**开销大**
|
||||
|
||||
页面节点越多,Diff 开销就越大。
|
||||
|
||||
**动态渲染很难性能优化**
|
||||
|
||||
由于所有 DOM 节点都是动态生成,因此无法根据初始状态结构进行安全的优化。相比之下,Template 模式可以确定哪部分属于变量,哪部分是固定的,对固定部分的 Diff 检测都可以跳过。
|
||||
|
||||
**动态调度虽然改善了性能,但依赖更重的运行时**
|
||||
|
||||
React ConcurrentMode 是一个调度优化器,但实现的逻辑也比较复杂,加重了运行时负担。
|
||||
|
||||
#### Template 的优势
|
||||
|
||||
**原生性能**
|
||||
|
||||
由于 Template 对节点进行直接渲染,因此与原生性能一致。
|
||||
|
||||
**Runtime 更小**
|
||||
|
||||
由于不需要额外优化,运行时代码会小很多。
|
||||
|
||||
#### Template 的劣势
|
||||
|
||||
**被 Template 语法约束,且无法拓展**
|
||||
|
||||
对于 Template 不支持的,只能选择接受,因为除了框架自己,没有人能拓展 Template 的特性。当遇到一些非常动态场景,但 Template 不支持的情况,只能选择接受,并用比较 Hack 的方式绕过解决,除此之外别无他法。
|
||||
|
||||
**模版冗长**
|
||||
|
||||
JSX 可以利用循环语句或者变量赋值进行模版区块的复用,但 Template 模式每次新模版都要一行一行的打出来,这种冗长的开发体验不太友好。
|
||||
|
||||
**运行时解析开销或者依赖编译期逻辑**
|
||||
|
||||
要么通过编译器预先生成 AST,要么运行时动态将 Template 解析成 AST,无论哪种方案都有额外的开销,一种是工程依赖的开销,一种是运行时动态解析的性能开销。
|
||||
|
||||
#### VDom + Template 的特色
|
||||
|
||||
Vue 在 Template 基础上支持了虚拟 DOM,因此兼具两者特色。
|
||||
|
||||
性能上,在编译时就进行 AST 解析,减少了运行时解析开销。
|
||||
|
||||
功能上,支持模版与 JSX 两种语法。
|
||||
|
||||
### 状态机制
|
||||
|
||||
状态机制 [尤雨溪](https://github.com/yyx990803) 在 JSConf 提到要单独拆出来讲,因为内容较多,时间可能不够,本次精读也限于篇幅原因略过:
|
||||
|
||||
- Mutable vs Immutable。
|
||||
- 依赖追踪 vs 脏检测。
|
||||
- 响应式 vs 模拟响应式。
|
||||
|
||||
显然,状态机制方案更是仁者见仁智者见智的事情,同样得从多个维度进行独立分析,并根据实际业务场景具体选择。
|
||||
|
||||
最后,意识到没有一个绝对均衡的框架设计方案,因为在工程领域,没有最好只有更好。
|
||||
|
||||
## 3 精读
|
||||
|
||||
我们再延伸谈一谈为什么框架设计要寻找平衡点。
|
||||
|
||||
**框架设计没有银弹**
|
||||
|
||||
与数学公式不同,框架设计甚至整个工程技术设计都没有所谓的真理,所谓条条大路通罗马,实现同一个技术目标的众多方案之间也许就是平行关系,可以根据不同维度列出一二三的对比,但无法得出一个总的结论,孰优孰劣。
|
||||
|
||||
**使用场景不同**
|
||||
|
||||
不同使用场景决定了对框架诉求的不同。
|
||||
|
||||
比如开发非常定制、炫酷的可视化大屏,那么前端开发框架基本也用不上,因为关注点不会聚焦在项目路由、UI 描述、甚至是数据流,而是聚焦在性能、图形渲染等问题。解决这些领域的框架可能是 虚幻 4、Unity 等游戏引擎,但普通的前端开发框架绝不会涉足这种领域,框架一定要确定自己功能范围。
|
||||
|
||||
即便仅局限在 Web 领域,也需要考虑是否要支持非 Web 场景,那么将 HTML 抽象成一个通用 DSL 就可能是一种选择,但非 Web 领域毕竟不是主打业务领域,在这种业务场景周边生态维护可能就比较少,这也是需要取舍的地方。
|
||||
|
||||
**使用的人不同**
|
||||
|
||||
不同团队对框架的要求也不同。
|
||||
|
||||
刚起步的小团队可能更需要保姆式的框架,因为这样最节省人力成本。对于规模较大的团队,希望对框架拥有较大定制能力时,小服务范围的框架可能更受青睐。当然框架作者可以像 Vue 一样做出渐进式官方能力增强方案,以此满足不同需求的用户,但毕竟也不能将生态完全交给社区,还是要做取舍。
|
||||
|
||||
所以当遇到更新更酷的框架时,需要冷静思考的不只是这个框架带来的收益与花费的迁移成本哪个更高,以及团队能否接受这套框架的开发习惯,更需要思考的是这个框架自身做了哪些权衡,如果这些权衡与 React、Vue、Angular 类似,那么仅仅变化了语法或者语言的改动其实意义不大,此时需要慎重考虑。
|
||||
|
||||
## 4 总结
|
||||
|
||||
这次没有提到的状态机制对比,你能分别列举出优缺点吗?欢迎留言。
|
||||
|
||||
> 讨论地址是:[精读《寻找框架设计的平衡点》 · Issue #223 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/223)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,111 @@
|
||||
## 1 引言
|
||||
|
||||
当下互联网行业里面最流行的就是 ABC:
|
||||
|
||||
> A: AI 人工智能 B: BIG DATA C: CLOUD
|
||||
|
||||
而阿里经济体中的 ABC,其中的 BIG DATA,即是我们 DT https://dt.alibaba.com/ ,我们用大数据赋能商业,创造价值。
|
||||
|
||||
而我们说数据中台,其实阿里提出的中台只有两个:业务中台与数据中台。业务中台的目的是让业务能够快速落地,数据中台的目的是完成数据的采集、建设、管理、使用这四个环节,让数据从生产到使用过程变得丝般顺滑,不仅不让数据资产成为累赘,还会最大限度发挥出数据潜藏的价值。
|
||||
|
||||
笔者所在的就是数据中台的大前端团队,既为阿里经济体提供数据服务,又着力为上云企业打造属于自己的数据中台,处在前端技术、商业模式、产品设计的最前沿,且听我慢慢道来。
|
||||
|
||||
## 2 精读
|
||||
|
||||
### 全链路数据能力
|
||||
|
||||
从能力上看,数据中台处理数据的方方面面,从数据产生开始就进行追踪,不仅打通了数据采集、存储、处理、查询、消费的全链路,还用以下几种方式赋能业务:研发数据管理平台并监控数据质量,研发生意参谋等数据分析产品直接服务大、中、小商家,提供统一数据服务标准化数据使用流程,将数据分析的算法能力服务化,将支撑内部的数据服务上云搭建客户自己的数据中台,研发 BI 平台完成数据决策的最后一环。
|
||||
|
||||
### 全链路数据技术
|
||||
|
||||
从技术架构上看,从底层的数据采集技术开始,逐步向上建设了数据计算与管理能力、数据服务、数据平台、数据应用与数据安全。
|
||||
|
||||
从使用者角度来看,现在的公司对数据的诉求可以概括为以下几点:
|
||||
|
||||
1. 数据从哪来,如何完全数字化:对应全链路数据采集服务。
|
||||
2. 如何得到想要的数据:数据计算、建模与管理服务。
|
||||
3. 如何使用数据:统一数据服务平台。
|
||||
4. 如何利用数据做商业决策:BI 平台。
|
||||
5. 如何保障数据安全:数据安全服务。
|
||||
|
||||
对阿里而言,还会额外考虑下面几点:
|
||||
|
||||
1. 如何让数据服务横向支撑所有业务线:数据服务平台化,数据智能化服务平台与 BI 平台。
|
||||
2. 如何让数据服务普惠到每一个企业:数据服务全面上云。
|
||||
3. 如何让数据服务更有价值:打通阿里经济体的数据体系,让数据相互产生化学反应。
|
||||
|
||||
当然,挑战性也非常大,首先是数据壁垒的挑战,要说服其他团队将数据交给你管理绝非易事。其次是价值挑战,如何证明数据中台存在的价值,并做到肉眼可见的业务增值。最后是技术挑战,对前端来说,几十款数据产品的搭建、几十万张数据报表的搭建,需要一个足够好用的数据产品搭建平台来支持;数据分析产品的下一代探索式分析也对 BI 引擎提出了新的要求;数据可视化远比普通可视化复杂,不仅要考虑大数据下的性能与可读性,还要理解商业,做出能体现数据分析价值的图表。
|
||||
|
||||
不论是数据搭建还是数据可视化,都是前端垂直领域的另一条好赛道,不仅有沉甸甸的业务价值,还有全新数据领域的的前端技术挑战,而且随着数据中台影响力的持续扩大,我们的前端技术也会带来业界越来越大的影响力。
|
||||
|
||||
### 如何建设和管理数据
|
||||
|
||||
想要数据用的好,首先要管的好,在大数据时代,企业必须建立一套自己的标准数仓系统对数据的采集、运维调度做全链路管理,让大数据变成好数据,让好数据可以发挥价值。
|
||||
|
||||

|
||||
|
||||
> Dataphin 数仓建设平台。
|
||||
|
||||
数仓的建设需要从物理空间与逻辑空间,也就是底层的表开始整理,通过对数据的采集、清洗、结构化,产出一套规范的数据定义。
|
||||
|
||||
所谓规范的数据定义即口径、算法、命名均一致的数据规范,降低数据二义性,提升数据查找效率与准确性。之后对数据建模,建模即是对数据的进一步抽象,可能是抽象为一个 Cube 模型,这样在顶层认知上,所有数据都是不同维度的 Cube,方便统一理解。
|
||||
|
||||
最后通过对数据进行在线的、离线的调度计算,产出数据资产。
|
||||
|
||||
### 如何看数据
|
||||
|
||||
或导出一个 Excel 文件仔细品味,或如双十一媒体大屏般夺目,或如股票操盘手般紧盯着屏幕,或随时随地的手机浏览。在哪看,怎么看,看什么,决定着同一份数据可带来不同的效果,产生不同的价值。
|
||||
|
||||
稳:双十一大屏,零点起得来,24 点收得住,每个彩蛋的出现,每个数字的跳动,如丝般顺滑,这不是播放 VCR,每一帧画面都是真实的数据展现。容:即是生意参谋用户的浏览器兼容,又是多端用户的兼容,也是 BI 分析结果的数据大容量。有容乃大,方显前端功底。
|
||||
|
||||
**“如何看数据” 这恰是做为数据前端人的使命和责任。** 不同的人,不同的端,不同的需求,这恰是给数据前端的挑战。而让用户透过数据创造价值,也正是数据前端人的价值。
|
||||
|
||||
### 如何分析数据
|
||||
|
||||
大数据浪潮之下,必然会诞生各式各样的数据产品,产品化的方式可以降低数据应用的门槛。我们希望人人都能成为数据分析师,于是 BI (商业智能)产品应运而生,作为大数据行业中的一个重要领域,BI 产品用大数据的方式解决了企业的业务分析需求,支撑企业进行数字化转型,从经验驱动决策转变为数据驱动决策,进而给企业带来超额收益。
|
||||
|
||||

|
||||
|
||||
> QuickBI 数据分析工具。
|
||||
|
||||
**人人都是数据分析师的情况在不断增强。**
|
||||
|
||||
根据 Gartner 对 2020 年 BI 产品发展趋势预测:
|
||||
|
||||
1. 到 2020 年,为用户提供对内部和外部数据策划目录的访问权限的组织将从分析投资中获得两倍的业务价值。
|
||||
2. 到 2020 年,业务部门的数据和分析专家数量的增速将是 IT 部门专家的 3 倍,这会迫使企业重新考虑其组织模式和技能。
|
||||
3. 到 2021 年,自然语言处理和会话分析这两个功能,会在新用户、特别是一线工作人员中,将分析和商业智能产品的使用率从 35% 提升到 50% 以上。
|
||||
|
||||
**快速增涨的市场规模。**
|
||||
|
||||
根据中国电子信息产业发展研究院发布的《中国大数据产业发展水平评估报告》,预计 2019 年我国大数据核心产业规模突破 5700 亿元,未来 2-3 年的市场规模的增长率仍将保持 35% 左右。未来切入这部分应用环节,BI 商业智能的潜在市场规模将在数百亿的市场空间。
|
||||
|
||||
**大数据与前端。**
|
||||
|
||||
前端的职业发展除了提升自己的技能技术储备之外,选择合适行业方向和研究领域也尤为重要。如果用路和车的关系来比喻的话,把前端技能比作车的话,各个行业都是路,有的路是乡间小路,有的路是城乡公路,而大数据行业当之无愧是行业中的上高速公路,路况更好,路面更宽,如果你拥有一辆好车,为什么不来高速公路上飞驰呢?
|
||||
|
||||
大数据下的前端面临哪些挑战?以 BI 为例,BI 领域的四大方向:数据集、渲染引擎、数据模型与可视化都有许多可以做深的技术点,每一块都需要深入沉淀几年技术经验才能做好,需要大量优秀人才通力协作才有可能做好。你也可以阅读 [精读《前端与 BI》](https://github.com/dt-fe/weekly/blob/v2/121.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E4%B8%8E%20BI%E3%80%8B.md) 了解更多 BI 相关知识。
|
||||
|
||||
### 我们是数据中台大前端
|
||||
|
||||
> “ 前端不是因为我们用 JavaScript,而是因为我们站在业务最前端,解决业务端的问题,所以我们是前端 ”。
|
||||
|
||||
BI 分析产品、做数据可视化、做产品搭建 .. 我们早已经跳出了“前端”的传统概念范畴。我们做大数据表格优化、 Web Excel、 SQL 编辑器、智能可视化。在数据中台,我们有着天然的复杂业务场景和海量数据优势,迫使你向自己提出更大的挑战来解决业务上的问题。如果你热爱挑战、热爱技术,请加入我们吧。
|
||||
|
||||
**在这里,你可以愉快的使用 React、TypesScript 写业务代码,尝试最新、最炫酷的 React Hooks 新特性,我们团队一直走在前端技术路线的最前沿,渴求技术创新。** 你也不需要担心伙伴的代码风格问题,因为我们有着严格的代码规;你不必担心每个人的代码都是一座孤岛,因为我们会对每一行代码做严格的 review;你不必担心你的成长空间,我们有定期的技术分享、团队内小竞赛,还有足够复杂的业务场景支撑;你也不必担心你会因工作日渐消瘦,下午茶和海量小零食等你来!
|
||||
|
||||
## 4 总结
|
||||
|
||||
**大数据前端人才缺口在 100 人以上,由于业务增长非常非常迅猛,春节前条件放宽、特批急召!**
|
||||
|
||||
如果你对我们感兴趣,请立刻把简历发送到邮箱 **ziyi.hzy@alibaba-inc.com** 吧!绝无仅有的好机会,响应速度绝对超乎你的想象!
|
||||
|
||||
> 讨论地址是:[精读《我在阿里数据中台大前端》 · Issue #224 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/224)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,268 @@
|
||||
## 1 引言
|
||||
|
||||
这次是极客大会十周年,也正好告别了 2019 年,因此主题是总结互联网前 10 年的发展,并预测下一个 10 年的变化。
|
||||
|
||||
这次是前半部分的大会感悟。
|
||||
|
||||
## 2 精读
|
||||
|
||||
### 微信的成功
|
||||
|
||||
腾讯和米聊分别在 2010.12、2011.01 上线,起初他们的用户基数相当,**每天都有恐怖的 10% 用户量增长**,然而这两家的差距在 2011.07 开始拉大,之后微信便占有绝对优势,米聊彻底失败。
|
||||
|
||||
所以有人说微信抄袭米聊,毕竟微信起步比米聊晚了一个月,然而微信的胜出有更深层的原因。
|
||||
|
||||
大家都知道移动端即时通讯是一个唯一寡头市场,因此当米聊看到微信开始反超的时候,就已经知道这场战争已经结束。当时小米重点业务还在手机,米聊是团队试水的一款产品,但看到歪打误撞进入一个如此蓝海的市场,小米自己也很纠结要不要把资源都投入到米聊上。
|
||||
|
||||
反观微信,当时手机 QQ 也在做,本来怎么也轮不到微信出场,但张小龙、马化腾、张志东在微信简历了深夜小组,每天晚上都即时同步微信的进展,这让微信即时获取到了腾讯内部资源,在各种关键节点帮了很多忙,甚至让手机 QQ 技术大牛直接支持微信改善高并发问题,快速完成 QQ 好友导入功能。
|
||||
|
||||
雷军总结到 “如果腾讯一年后才有所反应,米聊胜率是 50%,如果是腾讯两三个月就有反应,米聊应该 100% 会死掉”。
|
||||
|
||||
很巧的是,张志东事后也总结过一句话 “如果我们当初没有看清这个趋势,没有在微信起量的事后,看清这个本质,微信胜出概率也只有 50%”。
|
||||
|
||||
然而腾讯的反应实在太快了,米聊之后只好走差异化社区路线。
|
||||
|
||||
微信后面的发展也非常精彩,通过源于用户需求的少量功能,比如微信红包,不断引爆微信的增长。夸张的是,微信装机量的增长率始终与智能手机渗透率持平,这说明 **微信吃掉了所有新增的流量红利。**
|
||||
|
||||
微信的发展,是一个 **工具到平台,平台到生态的演进过程。** 真正让微信建立生态的是小程序。小程序是一个去中心化模式,当大家都想像公众号一样抢一波风口红利时,微信做的正是去中心化,微信不给任何小程序导量,每个小程序的流量入口都需要开发者自己经营,这种商业模式才可持续发展。
|
||||
|
||||
对公众号也是一样的态度:公众号要持续创造价值,没有初始红利。理解了这一点才理解了现在微信生态一系列做法,只有每位贡献者持续创造价值的生态才是可持续的,生态绝不是在创建之初让抢到先手的用户瓜分平台流量红利,这样是不可持续的。
|
||||
|
||||
### 移动终端的中场战事
|
||||
|
||||
前十年,手机设备制造厂商的格局发生了很大变化。国内经历了从小米,到 OPPO、VIVO,再到华为的演化。
|
||||
|
||||
印象深刻的是看了一个雷军创办小米前夕的访谈视频,雷军说 “大家看到苹果的成功,却没有看到这片蓝海的机会,现在手机制造领域竞争太不激烈了”。同时为了对抗苹果,谷歌开源了安卓源代码,小米利用这个机会打造一款符合中国人口味的手机操作系统,并借助用户社区与性价比优势一举占领了早起市场。
|
||||
|
||||
2015-2018 年出现了 OV 领跑的情况,即 OPPO、VIVO 后来居上,有两点原因:小米还在强调各项参数指标,但 OV 宣传的概念很易懂 “充电五分钟,通话两小时”;同时 OV 还注意到了下沉市场,通过各种综艺节目冠名与 **平均 25 万家线下门店布局**,超越了小米。
|
||||
|
||||
上面两点分别对应了创业的早期与扩张期,然而 2019 年产业进入成熟期,手机出货量开始下降,市场逐渐进入零和博弈阶段,此时大玩家华为入场,华为的入场姿势是投入数万名研发资源进行饱和式攻击,成熟的市场比拼的不是营销而是技术,从争夺用户变成留存用户,这个阶段华为胜出了。
|
||||
|
||||
值得关注的是,从苹果收入年报来看,其中软件服务收入占比正在逐年升高,这也代表了一种未来发展趋势,在垄断了硬件后将收入来源逐渐转化为软件和服务。第三天的 OnePlus 手机恰恰是反其道而行之,仅通过硬件赚钱,商业模式也运转的很好,这个到后面再细说。**这就是商业的有趣之处,第一商业历史的精彩程度不亚于国家战争史,第二商业模式没有万能法则,两种完全相反的模式都能活得很好,这是它最有魅力的地方。**
|
||||
|
||||
### 支付宝
|
||||
|
||||
支付宝是典型的工具场景,这次分享核心观点是:只要把工具分内的事情做好,自然会赢得用户,赢得市场。
|
||||
|
||||
第一个例子是早期 PC 支付时代,由于支付需要跳转到各大银行网银页面,整个链路长达 7 次跳转,用户整体付款成功率只有 60%,马云为此在年会上把支付宝团队狠批了一顿,这也促使支付宝在次年研发了快捷支付,将银行支付流程替换为支付宝自己的支付流程,支付成功率提高到了 95%。但这个改动是艰苦的,有一句话印象深刻:**“为了用户体验,能做的都做了,不能做的也都做了”**。
|
||||
|
||||
无论是二维码支付、芝麻信用还是小程序,都是由用户对工具的需求催生出来的。其中芝麻信用是因为支付宝解决了淘宝上买家与卖家的信任问题,但社会依然存在大量信任问题,芝麻信用的初心就是将淘宝信用解决方案推广到全社会。不积跬步,无以至千里,任何了不起的方案起步都是解决一个具体的问题。
|
||||
|
||||
### 拼多多
|
||||
|
||||
拼多多给人的刻板印象是“下沉市场”,然而这既不是拼多多的起点,也不是拼多多的终点。
|
||||
|
||||
在创立拼多多之前,黄峥创建了一个“拼好货”的应用,这个应用瞄准城市人群,本来可以在这个垂直领域深耕,但在拼好多过程中,黄峥发现微信用户已经达到 7 亿日活,有一大半人群还没有网购习惯,但具备了网购能力,因为正好赶上微信红包培养了用户付款习惯。
|
||||
|
||||
**为什么淘宝、京东不在微信里卖货?** 原因是担心成为微信的货架。为什么淘宝当初要切断百度搜索入口?因为一旦用户培养了在百度搜索淘宝的习惯,**淘宝就无法成为第一级用户触达者,一旦百度推荐自家电商产品或者切断淘宝流量,淘宝将遭受灭顶之灾。** 在微信也一样,淘宝和京东都不希望被微信扼住喉咙。但这毕竟是“巨头”担心的事情,就一个创业公司来说,成为微信的货架又如何?这是个很大的市场空白,迟早有人补位。
|
||||
|
||||
拼多多切入点是下沉市场,下沉市场的特点是“有用户,没商品”,因此拼团很好的解决了这个问题,既提高了购买量,提升了物流、供应商效率,大量的订单量也提升了拼多多对供应商谈判的筹码,导致拼多多可以以低价提供给买家,低价又促使买家下更多的单,形成一个小飞轮。
|
||||
|
||||
下沉市场只是拼多多的第一刀,举一个爆品的例子:拼多多与商家合作推出了爆品玻璃碗,又大、又厚、耐高温,一下子成为了爆品,让商家与拼多多双赢。**重点在于,打造爆品对促进飞轮运作太有用了,爆品意味着大量单一订单,拼多多对单一商品谈价能力提高到极限,商家制作成本压低到极限,爆品是效率最高的社会生产和消费方式。** 在这个过程中,拼多多主动帮助商家打造爆品,“平台”干预商家带来双赢可能是未来一个强有力的竞争武器。
|
||||
|
||||
### 美团的商业逻辑
|
||||
|
||||
“不设限”是对美团比较好的理解。大家都觉得美团什么都做,其实美团就是坚信“按照规律做事”,从模仿美国的 facebook - 校内网、twitter - 饭否、groupon - 美团,好的借鉴也是一种成功哲学。
|
||||
|
||||
**四纵三横的思想,更透彻理解不同平台做的事情:**
|
||||
|
||||
| | **咨询** | **通信** | **娱乐** | **电商** |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| **搜索** | 百度 | QQ | 热血传奇 | 淘宝网 |
|
||||
| **社交** | 新浪微博 | 人人网 | 开心网 | 蘑菇街 |
|
||||
| **移动** | 今日头条 | 微信 | | |
|
||||
|
||||
练好基本功,提升工作效率,管理层按规律做事,合适的事找合适的人,没做过的事就自己探索,这是美团总结的经验。
|
||||
|
||||
### 字节跳动
|
||||
|
||||
字节跳动的估值几乎是百度的两倍了,为什么看似体量更大、资源更多的百度会被字节跳动超越?大家都很感兴趣这个话题。
|
||||
|
||||
字节跳动核心能力是个 **性化推荐引擎**,旗下产品 “社交、自拍、咨询、教育、金融理财、短视频、问答、电商”都利用了技术中台输出的个性化推荐算法作为核心竞争力。
|
||||
|
||||
字节跳动推出的成功产品很多,像今日头条、抖音、火山、西瓜,背后的方法论就是“产品、技术、文化”。
|
||||
|
||||
产品上,地毯式孵化许多产品,并且根据上面总结的领域乘以个性化推荐进行了许多尝试,比如社交 X 个性化推荐,短视频 X 个性化推荐,咨询 X 个性化推荐。产品迭代也是个逐步的过程,比如抖音从直播,到小学生短视频工具,最终找到了城市潮人工具这个最合适的定位。
|
||||
|
||||
技术上,首先是大量从百度挖人,而且挖的都是核心技术架构骨干。其次,打造了技术中台:技术部分为“算法组、互娱组、产品技术组、垂直产品组”,最核心的技术人员在算法组,为所有产品横向赋能。总结一下就是豪华技术团队 + 技术能力中台化。
|
||||
|
||||
文化上,字节跳动保持很大的信息透明度,比如新员工可以查看所有历史工作资料与聊天记录,公司所有决定都是透明可查询的,公司管理扁平化。
|
||||
|
||||
### 共享出行与共享经济
|
||||
|
||||
#### 滴滴
|
||||
|
||||
2010 ~ 2019 年,共享出行的代表就是滴滴,这个话题从滴滴开始剖析了整个共享经济行业,非常有意思。
|
||||
|
||||
切入点是 **融资**。BAT 上市融资额度分别是:百度:1.112 亿美元、**阿里巴巴 69.88 亿美元**、腾讯 0.2188 亿美元,总额 71.2 亿美元。**而滴滴到目前为止的融资已经达到 208 亿美元,** 滴滴融资超过 BAT 总和,这说明了什么?这说明滴滴走了一条不正常的商业路线,即先疯狂再冷静的烧钱路线。
|
||||
|
||||
当一个行业增长速度极速增加时,老玩家将失去优势和壁垒,所以谁能更快扩张谁就能成为最终赢家,此时如果有大量资本投入快速占领市场,让企业成为这个领域的绝对霸主,投资者就可以通过上市退出的方式把之前烧的前赚回来。然而这种烧钱商业模式是有前提的,即 **极度充裕的资本 + 清晰的结构性机会**,滴滴的结构性机会非常清晰,先垄断再收割。
|
||||
|
||||
> 传统商业模式:融资 -> 赚钱。
|
||||
>
|
||||
> 非常态的商业模式:融资 -> 烧钱 -> 烧钱 -> 烧钱... -> 赚大钱。
|
||||
|
||||
Uber 创始人 特拉维斯·卡兰尼克 说了一句很经典的话,翻译过来就是:一个赛道上只要出现一个 “疯子”,所有人都必须变成 “疯子”。即一旦你所在的领域开始有公司利用融资 + 烧钱的方式运作时,你也必须这么做,否则你的市场会被对手抢走。
|
||||
|
||||
**然而也可以看到这几年大量烧钱的公司开始合并**,比如滴滴和快的打车、同城和赶集网、美团和大众点评、携程和去哪儿,这些公司合并的背后都是投资人运作的,那为什么要合并呢?道理很简单,双方投资人都在砸钱,谁也扳不倒谁,**此时投资人会计算现在烧的钱在垄断市场后能否收回来**,如果收不回来,双方投资人都不傻,大家为了不赔本,一定会促使两家公司合并,这样才能停止烧钱,即时上市止血。
|
||||
|
||||
有意思的是,滴滴从抢单模式变成派单模式,就体现了烧钱抢市场到精细化运营考虑盈利的一种转变。
|
||||
|
||||
#### 摩拜和 OFO
|
||||
|
||||
摩拜和 OFO 的发展本应该比较平静的,因为共享单车要解决的问题是 “看得见和愿意骑”,投放更多的车可以解决看得见问题,提升骑行体验可以解决愿意骑的问题,然而大量投资人从滴滴大战中大赚了一笔,想要把模式复制到共享单车领域,战斗就开始了。
|
||||
|
||||
由于资本的投入,摩拜和 OFO 重点都放在了“投更多的车”上,但这种抢占市场的方式并不像滴滴一样合理:
|
||||
|
||||
滴滴将大量私家车借给没车的人使用,本质是将“私人交通工具”变成“公共交通工具”,提升了“私人交通工具”的利用效率,对社会有益的事情自然能站得住脚。
|
||||
|
||||
共享单车的问题在于,**大家不会把自家自行车骑出来借给别人用,毕竟开着汽车可以带乘客,但骑着自行车带人变成服务也太奇怪了。** 所以各公司大量制造新的自行车投入市场,**要解决的是公共交通问题,但这些自行车并没总在路上跑着,而是在街头大量闲置,** 这样其实降低了自行车的工具利用效率,从根本来看没有创造剩余价值,因此盈利模式不太明朗。
|
||||
|
||||
#### 更多共享模式
|
||||
|
||||
后来出现的共享充电宝、共享车位、共享雨伞等等细分领域的创业,本来资本也想走烧钱模式,但发现走不通,还是回到了最初健康的模式。根本原因可能是这些行业无法产生寡头垄断,无法通过烧钱的方式快速占领市场并回收资本。
|
||||
|
||||
### 产业互联网与衰退期
|
||||
|
||||
看未来十年,互联网也许进入了一个“衰退周期”,互联网从纯线上变成与产业结合,比如软硬件都做,或者线上线下结合才能继续破局,反过来说,以前纯线上一本万利的高速扩张模式一去不复返了,互联网要深度与社会结合,发挥更多实际的价值才能得到自身成长,这是一个泡沫破裂的过程,也是互联网回归到真实价值的过程。
|
||||
|
||||
如果资本不充裕了,对创业者来说也还有机会,比如相应的会带来低人力成本与低广告投放成本。
|
||||
|
||||
最后,周航宣传了一个创业孵化项目,即投资人与创业者深度交流几个月,在这几个月内让创业者得到成长,让投资人能看清创业者是否具备潜力,这种投资者与创业者培养感情的孵化方式是比较新颖的,相对面试来说,有更多机会呆在一起可以看人看得更清楚,投资者与创业者更容易简历信任关系。
|
||||
|
||||
### 语言 AI 的未来构想
|
||||
|
||||
搜狗在 AI 语音布局很久了,我们熟悉的搜狗产品有“搜狗输入法”和“搜狗搜索”,这两个都是语言入口,所以搜狗基于语言来布局。
|
||||
|
||||
语言 AI 的发展方向是自然交互 + 知识计算。自然交互指人机自然的语言交互,利用语音技术、图像技术、视觉技术识别;知识计算指的是利用知识对语言进行处理,比如翻译、问答、对话。综合两者有可能产生未来的智能助理。
|
||||
|
||||
语音皮肤在知识付费领域就有应用场景,通过识别人的声音,将其特征提取后把另一个人的声音音色覆盖掉,这样就能让任何人代替讲师录制音频了。同样在导航语音也有类似适用场景,后面百度地图的分享会提到。
|
||||
|
||||
### 发生在边缘的 AI 计算革命
|
||||
|
||||
所谓边缘计算指的是去中心化的本地分散运算,比如自动驾驶,就是发生在每个车上的本地计算。为什么不是云计算?因为本地计算一般都需要即时响应,尤其是自动驾驶只有几百毫秒的生命线,万一网络出现延迟,后果是谁也承担不起的。
|
||||
|
||||
边缘计算产生的数据量非常庞大,一辆自动驾驶汽车平均每天产生 600-1000 TB 量的计算,而且自动驾驶 L1 - L5 需要的算力也是呈指数级增长的,要解决这个问题,自研芯片与算法的软硬配合是一种突破方式。
|
||||
|
||||
地平线公司要做的是智能互联的底层,做手机领域的思科,做智能化时代的底层基础设施。
|
||||
|
||||
### 通往人机交互“终极自由”的 AI 之路
|
||||
|
||||
报告显示全球有 26% 的手机用户每天使用手机超过 7 小时,35 岁以下人群平均每天解锁手机,人类都要成为手机的奴隶了,看似拓展了人类生活自由,但反而感觉人类被手机束缚住了。
|
||||
|
||||
原因有几块:
|
||||
|
||||
1. 交互方式不自然:按键和触屏都不方便。
|
||||
2. 智能手机不智能:appStore 就是智能手机了?就算有语音助手加持,也无法理解连续语义。
|
||||
|
||||
解决办法就是更自然的,让人类感受不到的电子设备交互方式,比如微型音频设备,AR 眼镜,体内芯片等外挂方式,交互上需要进化为语音交互、手势交互、脑波信号等。
|
||||
|
||||
目前这个阶段,智能手表和智能耳机都是较能符合这个进步趋势的尝试。
|
||||
|
||||
### 地图的破局
|
||||
|
||||
比较有意思的是利用 20 秒对话训练,可以产生一个你自己语音包,用你自己的声音导航。
|
||||
|
||||
另一个功能是预测第二天路况,并根据到达时间推荐一个合适的出发时间。
|
||||
|
||||
百度地图不止于导航,在如何挖掘地图额外价值方面也在做积极的尝试。
|
||||
|
||||
### 一起创造【所见及所能】的平行世界
|
||||
|
||||
外号科技介绍了一款产品:远距离二维码。
|
||||
|
||||
我们现在看到的二维码基本都是近距离的,近距离二维码可以:支付、加好友、账号登录、近距离信息获取等。
|
||||
|
||||
而远距离二维码是相对于近距离二维码的,在极端情况下甚至可以达到一公里的距离。
|
||||
|
||||
远距离二维码的适用场景有四种:
|
||||
|
||||
1. 远距离信息获取:服务机器人定位导航、无人机遂窗配送、电子围栏。
|
||||
2. 高精度定位:实时物流、室内定位报警。
|
||||
3. 增强现实:景区 AR 改造、AR 多人游戏、室内沉浸式导航、机场电子指示牌。
|
||||
4. 数据重建:室内测距和建模。
|
||||
|
||||
### 当科技拉近我们与世界的距离
|
||||
|
||||
这个演讲者是一名了不起的盲人曹军,他创立了保益科技帮助盲人像明眼人一样生活。
|
||||
|
||||
记忆最深刻的一句话是:**不要总以为帮助盲人就是出一款盲人专用手机、盲人专用 App,其实盲人最大诉求是像普通人一样享受科技的便利,普通人能用的手机、能用的 App、能开的车,盲人也都想用,** 普通人应该想办法把自己用的手机、软件改造成盲人可以使用的版本。这是最大的换位思考。
|
||||
|
||||
### 鹏友说 - 傅盛
|
||||
|
||||
傅盛带领的猎豹做智能机器人已经有几年了,今年有了最新进展,出货量达到 5000 台。
|
||||
|
||||
傅盛提到一点非常关键,就是机器人这个名字起的很不好,总让人觉得机器就应该拥有人一样的智慧,其实我们这个阶段还做不到,而且行业也不需要那样聪明的机器,要的而是一个服务工具。
|
||||
|
||||
举个例子,博物馆的导游可以被机器人替代,因为一方面机器人信息储备量大,工作效率高,而且还能听懂任何国家语言,这样一个机器人甚至能胜过好几位资深导游,而导游这种场景也相对局限,容易实现。
|
||||
|
||||
机器人也不一定要长得像人,在不同领域可以做出不同体型,适配不同的工作场景。**机器在某些垂直领域完全可以超越人类。**
|
||||
|
||||
### 探秘人工智能背后的【硬核英雄】
|
||||
|
||||
未来 10 年定制化数据服务领域可能分为 5 大块:
|
||||
|
||||
**设备的定制化**
|
||||
|
||||
比如无人车的场景,从多摄像头到摄像头 + 激光雷达的方案,随着业务场景不断多元化,对设备定制要求也会不断提高。
|
||||
|
||||
**场景的定制化**
|
||||
|
||||
还是无人驾驶场景,为了保证在多场景的安全性,需要模拟出许多情况下的交通场景,比如不同光线强度、角度、不同车道、不同车型、不同类似司机、人群和环境。
|
||||
|
||||
**样本的定制化**
|
||||
|
||||
今天很多 AI 是以人为中心,人群可以根据不同肤色、不同语言、不同年龄段、不同爱好等进行区分,所以根据基于样本的定制也是一大趋势。
|
||||
|
||||
**工作的协同化** 和 **工作的专业化**,即随着分工不断细化,协同度与专业化程度都会提高。
|
||||
|
||||
### 智能交通
|
||||
|
||||
九号机器人这家公司为了解决开车与步行之间存在的空白的问题,九号机器人提供了大量代步机器,比如智能滑板车,智能电动车,所有车辆都是“电动化、网络化”的,预测下一个 10 年会 加上“智能化”。
|
||||
|
||||
开车与步行之间的机器人除了代步,还有快递和配送这个巨大场景,而这个场景的优势在于,低速场景的机器人自动驾驶危险系数小,技术上较容易实现,因此可以快速投入到线下上进快速迭代。
|
||||
|
||||
未来十年可能是去智能化的十年,即所有的硬件都是智能硬件,所有车辆都是机器人,即智能化会极大的普及。
|
||||
|
||||
### 可折叠手机
|
||||
|
||||
介绍了联想集团出的一款可折叠手机,据说是全球首款无痕的可折叠手机 Moto Razr。
|
||||
|
||||
从视频来看,无痕可折叠的最大秘密在于,并没有将屏幕折叠到 180 度这个死角,折叠到 180 度目前没有任何一个屏幕材料不产生折痕,这款手机通过非常精巧的设计,让 **在外部折叠到 180 度时,内部屏幕仅折叠 100 度左右。**
|
||||
|
||||
### 下一个十年:科技链接健康
|
||||
|
||||
下一个十年,科技会更加关注健康领域,比如手环检测心跳是否异常,或者通过智能设备检测健康是否达标,以决定是否要去医院就诊,甚至以此决定医保的折扣率。
|
||||
|
||||
### 未来的年轻人吃什么?
|
||||
|
||||
这个标题有点标题党嫌疑,其实说的是一个减肥棒产品,吃了可以减肥。
|
||||
|
||||
一个原始年轻人的食谱,碳水化合物、脂肪、蛋白质含量分别占 22%-40%、28%-58%、19%-35%,总结起来就是低糖、优质蛋白质。
|
||||
|
||||
而进入农耕时代,一个年轻人的食谱,脂肪、蛋白质、碳水化合物分别占 10%-20%,10%-20%,50%-70%,即碳水化合物太多,糖分过多,而摄入蛋白质的量严重不足,这带来了大量肥胖问题。
|
||||
|
||||
解决办法就是做一个低糖、优脂、优蛋白的产品,所以这款产品最终效果就是“无糖、易吸收的小分子蛋白、好吃”,至于好吃是怎么做到的,因为做了这两个方面的优化:
|
||||
|
||||
1. 食材可见:比如大块杏仁碎、大块黄桃粒等。
|
||||
2. 口味丰富:芝士、椰子、巧克力、曲奇。
|
||||
|
||||
我以为朋友当场就订购了几箱,说实话还是蛮有诱惑力的,产品叫 ffit8,可以天猫自行搜索。
|
||||
|
||||
极客大会每个人都送了几袋,尝了一下还是蛮好吃的,有甜味,但为什么说无糖呢,查了一下原因,原来用的是低聚异麦芽糖,这种麦芽糖难以被吸收,所以也就可以认为是无糖的啦。
|
||||
|
||||
## 3 总结
|
||||
|
||||
前十年,无论巨头还是创业公司都经历了起起伏伏,商业路上哪有一帆风顺,唯有真正为社会创造价值,为用户解决问题的企业才可能成功。
|
||||
|
||||
最后留下一道思考题,你对互联网上个十年有什么感悟吗?
|
||||
|
||||
> 讨论地址是:[精读《极客公园 IFX - 上》 · Issue #225 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/225)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,164 @@
|
||||
## 1 引言
|
||||
|
||||
这次是极客大会十周年,也正好告别了 2019 年,因此主题是总结互联网前 10 年的发展,并预测下一个 10 年的变化。
|
||||
|
||||
这次是后半部分的大会感悟。
|
||||
|
||||
## 2 精读
|
||||
|
||||
### 一头在慢赛道下奔跑的大象
|
||||
|
||||
大象保险是一个互联网保险公司,可能在大家印象中保险公司是一个老而慢的行业,复杂的条款,繁琐的理赔流程,精心规划的商业套路等等。顺带一提,巴菲特就是利用收购的众多保险公司收集的保费进行杠杆投资,才取得了平均年化 20% 左右的神话。所以保险是一个比较难做,且在慢赛道的行业。
|
||||
|
||||
大象保险则利用科技的手段对保险流程进行优化,在初期尝试了几种有意思的互联网保险业务,比如上班下雨保险、堵车保险等,也取得过一些成果,现在尝试将自己平台化,将沉淀的互联网保险能力提供给其他互联网保险公司。
|
||||
|
||||
大象保险沉淀了一个业务中台,包括各种保险险种库、智能化投保决策、以及沉淀了大量数据,基于这个业务中台拓展出一个新品牌“象保保”,这是一个代理人数字化营销平台,集成险种库、出单管理、海报、计划书、课程、健康服务等一系列互联网保险基础能力,既服务于自己,又能服务于其他保险公司。
|
||||
|
||||
这种商业思路确实是很棒的,亚马逊的 AWS(亚马逊云)、FBA(第三方物流服务)、Amazon Go(即拿即走线下超市)都是前期研发投入高 + 固定成本很高的项目,这些项目诞生之初就服务于亚马逊自己这个大客户,等成熟后就拿到市场上检验,赋能其他行业。阿里内部服务成熟后上云也是一样的思维。
|
||||
|
||||
### bilibili CEO 专访
|
||||
|
||||
从专访中了解到,bilibili CEO 陈睿不是 b 站最早的创始人,而是后面加入的,一开始他兼职管理 b 站业务,并承诺自己公司上市后就加入,结果所在公司猎豹真的上市了,而他也正式加入 b 站,追求他的爱好。
|
||||
|
||||
b 站独特魅力在于 UGC 内容持续的创作,这样公司不用花功夫进行内容创作,而用户的智慧聚集起庞大的创作力量影响力又非常之大,同时用户自己创作的内容会更容易得到用户自己的认可,所以 b 站用户付费的意愿都很高。
|
||||
|
||||
所以陈睿一直强调的是一种社区文化生态,这种生态可以带来很强的归属感与认同感。
|
||||
|
||||
b 站的品类很多,按照热度排序可能是:动画、番剧、游戏、娱乐、国创、数码、科技、音乐、生活、舞蹈、放映厅、时尚等等,而 b 站的收入来源游戏业务占了一半,2019 Q3 游戏收入达 9.3 亿元,其余收入来源分别是直播和增值服务业务、广告业务、电商以及其他业务。这种收入模型对社区类创业者来说比较有借鉴意义。
|
||||
|
||||
### 如何把读书这件小事做到极致
|
||||
|
||||
樊登读书的 CEO 樊登过来了,樊登是知识付费四天王之一,知识付费的四天王分别是:吴晓波、罗振宇、樊登和李善友。
|
||||
|
||||
樊登真的很有演讲功力,笔者觉得樊登是极客大会 3 天所有演讲中讲的最好的没有之一,与他同台演讲的都是各大独角兽 CEO 级别人物,也包括百度等大公司事业部总经理,但就演讲能力而言,距离樊登还是差得太远。
|
||||
|
||||
这次樊登演讲主题是复杂体系 vs 简单体系,总结后其实就是一句话:复杂体系是自然生长出来的,简单体系是规划出来的,现在创业环境不适合简单体系,只有复杂体系才能应对这个世界的复杂性。
|
||||
|
||||
其中提到一个有意思的点:所有 KPI 都是错的,因为 KPI 是预测未来的工具,所有对未来的预测都是不准确的。在 KPI 压力下人的动作会产生变形,比如只追求结果不追求过程, 最终导致饮鸩止渴,不利于长期发展。樊登解决问题的方法挺有意思的,他对线下门店指定的 KPI 长达 100 多条,非常非常细致,但想要一一检验是不可能的,每到发奖金的时候,就随机抽取三条进行检验,由于不知道最终会检验哪一条,这样门店想要拿到奖金就需要本本分分做好每一点细节,做真正产生价值的事情。
|
||||
|
||||
樊登读书这款 App 笔者也听了一个星期,里面讲的内容很有针对性,都是职场、心灵、生活相关的,与完善自我紧密相关,特别是一款《逆商》的解读,非常有意思,推荐大家读一读。
|
||||
|
||||
### 大组织土壤中创新如何发芽结果
|
||||
|
||||
主讲人是阿里创新事业部总裁朱顺炎,大公司总是被诟病创新能力差,毕竟层级复杂体系庞大,看上去好像创新确实很困难。
|
||||
|
||||
阿里创新事业部有四个法宝:
|
||||
|
||||
1. 大家没有生存压力,不需要为了短期变现而产生动作的变形。
|
||||
2. CEO 深知创新是从小应用成长起来的,所以让创新项目从小开始独立孵化。之所以要独立孵化,也是认识到组合的产品创新能力是很脆弱的,真正成功的产品必须要独立撑起一片天。
|
||||
3. 给更有创造力的年轻人机会。
|
||||
4. 没有不变的业务,只有不变的文化,通过培养文化进行企业传承。
|
||||
|
||||
一起期待拥有长线计划的阿里创新事业部可以给市场带来更多有价值的产品吧!
|
||||
|
||||
### 面对不确定的未来,我们应该如何决策
|
||||
|
||||
大众汽车中国的 CEO 介绍到,中国已经成为世界最大的汽车市场之一。
|
||||
|
||||
PS1:其实中国不仅正在成为汽车最大的市场,中国其实在各个维度都在成为全球最大的消费市场。
|
||||
|
||||
PS2:大众汽车的历史很有意思,尤其是保时捷和大众的收购大战以保时捷发起,最终却被大众反收购,这段历史非常有趣。
|
||||
|
||||
这次分享讨论了三个问题:
|
||||
|
||||
1. 电动汽车出行肯定会实现吗?
|
||||
|
||||
关于这个问题,大众汽车的答案是肯定的。这句话很有意思,我记得去年参加这个大会时,许多初创新能源汽车制造公司就自己与老牌车场相比有什么优势时提到,老牌车场虽然实力雄厚,但航空母舰转身非常困难,这些大厂其实难以很快投入电动汽车的研发。从现在阶段来看,行业又发生了变化,老牌大厂纷纷加入实现了“掉头”,进入电动车行业,并且针对自动驾驶领域开始做技术合作与整合。
|
||||
|
||||
2. 软件公司和汽车公司谁将引领汽车行业的未来?
|
||||
|
||||
大众中国 CEO 通过四个力:责任里、靠谱力、盈利力、可持续力四个方面对大众汽车进行了全面夸赞,总之想表达的观点就是,汽车公司实力雄厚,可以通过再造一个规模一万人的软件公司,对互联网造车公司进行降维打击。
|
||||
|
||||
3. 出行服务会颠覆传统汽车制造商吗?
|
||||
|
||||
自行车厂商倒可以有这种担心,但汽车厂商不必有,因为每个人其实都梦想有一辆属于自己的车。在之前共享出行行业里也提到了,交通分为公共交通与私人交通,对于两点一线比如上班场景,就非常适合私人交通,因为大家对时间和稳定性要求非常强烈,毕竟谁都不想上班迟到。对于临时的交通需求,大家对公共交通需求更大,毕竟公共交通便捷性更强,特别是人在国外时,总不能在国外给自己也买辆车吧。
|
||||
|
||||
### 产业物联网中的机制成长从何而来
|
||||
|
||||
G7 去年成长了 5 倍,这是一家智能物流服务公司,提供货车智能服务。
|
||||
|
||||
去年的极客大会有介绍过 G7,几年就不再详细介绍了。G7 之所以有这么快速的成长,一方面是自己产品做的好,另一方面可能离不开整个中国产业互联网的腾飞,由于物流行业这几年快速发展,各个物流公司都在不断融资买货车提升自己的运力,对智能货车的服务需求才会不断增加,同时中国经济也进入了互联网广泛赋能各产业的阶段,这就是去年一直提的“产业互联网”,G7 作为一个平台,横向服务中国所有物流公司,享受到了中国发展的红利,得以快速发展。
|
||||
|
||||
也许未来 10 年还会迎来更加巨大的产业互联网机会,那些既做软件也做硬件的公司可以迟到这波趋势的红利。互联网将成为线下产业的钢铁侠外衣,对线下产业来说,得到互联网的加持可以大大提高运作效率,对互联网来说,线下产业发展的红利将带来极高的自然增速。
|
||||
|
||||
### VIPKID 鹏友说
|
||||
|
||||
VIPKID 创始人米雯娟谈了 VIPKID 最近的运营情况,比较有感触的是教育这块拉新的方式,一般教育领域花费都是比较高的,而且不仅仅是钱的问题,将孩子的成长托付给任何一家机构,家长都会特别谨慎,这是人之常情,所以大部分培训班很多新客都要通过老客推荐的方式获取。
|
||||
|
||||
VIPKID 起步是依靠朋友圈传播,但随着项目的起量,需要通过广告方式推广,最高的推广费用达到平均获客成本 8000 元,不过现在已经回归到正常水平,大概 4000 元左右,现在有 50% 的新客是通过老客推广的,无需费用。
|
||||
|
||||
### 一加手机
|
||||
|
||||
一加手机在国外销售非常火爆,最近一款 90 HZ 屏的产品使其又火了一把。之所以做 90 HZ 屏就是为了“更”流畅的使用体验,当被问及这么做性价比如何时,刘作虎回答的是:这就是高端品牌的极致追求,有的时候体验就提升那么一点,用户就会选择你。
|
||||
|
||||
一加手机做的是高端手机,操作系统主打的是简洁,不会有任何广告,盈利方式则是其较高的定价。而相比手机大厂,一加手机的突破点在于集中力量做旗舰手机,通过集中投入研发资源达到单点突破。
|
||||
|
||||
最近一加也在做电视了,目的是为了占领客厅市场,可能因为手机买的比较火,资金链比较充裕所以做了更大的布局。
|
||||
|
||||
### 解题 - 社区零售新物种的进化之道
|
||||
|
||||
每日优鲜的 CFO 王珺带来的一场分享,介绍每日优鲜是如何利用新技术实现新零售突破的。
|
||||
|
||||
每日优鲜业务的难度有三点:
|
||||
|
||||
1. 社区零售中最难的业态:大规模分布式连锁。
|
||||
2. 社区零售中最难的品类:生鲜非标品。
|
||||
3. 社区零售的三大挑战:体验、成本、复制。
|
||||
|
||||
生鲜零售难度确实很大:生鲜对保存时间短,运输过程中易磨损,品质管理层次不齐,我们来看每日优鲜是如何解决这些问题的。
|
||||
|
||||
每日优鲜通过部署 **“前置仓”** 解决物流问题。几乎所有物流业务想要提效,比如推出次日达甚至当日达业务,几乎都必须用前置仓解决。每日优鲜的前置仓甚至可以实现平均送达时间 36 分钟,而且价格比线下超市便宜 10%,这是怎么做到的呢?
|
||||
|
||||
每日优鲜分别从租金、人工、损耗三个方案解决问题。
|
||||
|
||||
首先是租金,每日优鲜专门租一些高性价比的地段,租金便宜但距离配送地点也不远的地方。
|
||||
|
||||
其次是人工,通过智能化的调度中心,减少了店员数量,但能保持服务效率。
|
||||
|
||||
最后的损耗,比如仓储管理,也通过合理的计算提升货物周转率,在配送服务方面,通过聚合订单,本来一个骑手一天只能送 20 单,但每日优鲜的骑手一天可以送 70 单,这是因为平均出车一次可以覆盖 10 位客户,这都取决于平台派单算法的优化。
|
||||
|
||||
对于规模化扩张方式,每日优鲜也有自己的做法。1.0 信息化阶段,利用系统辅助人,达到现在的高效率。未来 2.0 是智能化时代,用系统取代人,将成本压缩到极致。
|
||||
|
||||
### 智能汽车的白银时代
|
||||
|
||||
小鹏汽车的 CEO 何小鹏认为 2020-2025 年是电动车的白银时代,即拥有高度辅助功能(L3),2025 年之后是黄金时代,即受限场景的无人驾驶时代(准 L4)。
|
||||
|
||||
值得注意的是,小鹏汽车去年累计交付 1.3 万辆,虽然和传统汽车厂不在一个数量级,但其智能化数据还是比较亮眼的。
|
||||
|
||||
小鹏汽车明年要发行的新款有两大特色。基础能力包括:超长续航 + 超快充电 + 安全。特色能力是:主打高端的超级轿跑,配合顶级音响设备,再加上支持 L3 级别的智能化,看上去还是有一定竞争力的。
|
||||
|
||||
之前大众中国区 CEO 的分享也提到,传统汽车公司也开始进入电动车、自动驾驶领域了,纷纷开始组建硬件、软件子公司与团队,在软件上能否快速赶超走在前面的互联网造车公司是关键,如果传统汽车公司像华为入局手机制造业一样,以碾压性的资源投入快速实现 L4,并拉拢一批生态厂商制定标准规范,创业公司就比较难了,现在这个阶段正是互联网创业公司打时间差的最后时机。
|
||||
|
||||
### 智能新物种带来的智慧生态新体验
|
||||
|
||||
这次分享的嘉宾是美的集团 IOT 事业部总经理余尚锋,讲了关于未来家电的畅想。简单来说,未来的家电会万物互联,手机将不再是唯一入口,任何屏幕都可以是入口,任何家电都拥有智能,都可以拥有所有计算能力。
|
||||
|
||||
这具有很强的启发意义,未来家庭中可能会存在一个计算中心,所有设备都只是屏幕,是这个计算中心人机交互的输出界面,正因为如此,你的手机才屏幕才可以被卫生间镜子自动替代,就连煤气灶的显示屏也可以刷微信、玩游戏。
|
||||
|
||||
### AI 落地产业的这一年
|
||||
|
||||
分别由三角兽、杉树科技、文远知行三家公司的创始人谈一谈 AI 落地产业,这三家公司都是做的比较好的垂直领域公司,其中三角兽做的自然语言理解技术已经广泛运用于许多 Top 互联网公司,像百度语音助手也调用了其服务;杉树科技通过深度学习、机器学习、运筹学帮助滴滴、顺丰、京东等等公司做最优的决策;文远知行是一家做 L4 自动驾驶技术的公司,今年也在北京投放了十几辆限定区域的自动驾驶载客汽车试运行。
|
||||
|
||||
可以发现,这些公司都掌握核心 AI 技术,并成为互联网头部大公司坚实合作伙伴,通过对某个领域的极致钻研“坐在了大公司旁边”。
|
||||
|
||||
### 水滴公司 鹏友说
|
||||
|
||||
水滴公司的 CEO 沈鹏之前曾在美团就职,担任美团外卖全国业务负责人,可谓年少有为。在美团担任高管期间经历了许多磨练,也曾降职到地区负责人锤炼自己业务能力与管理能力,但即便如此,创立水滴公司后依然遇到许多挫折,沈鹏的感悟是,管理创业团队的难度比在公司当高管要难多了。
|
||||
|
||||
水滴公司的业务是帮助有困难的人,业务板块分为水滴商城与水滴互助,水滴商城提供一些高性价比的事前保障,水滴互助则是帮助遭遇重大疾病或变故的人筹集资金,通过参加水滴互助也让更多人了解到事前保证的重要性,促进了水滴商城的业务量。
|
||||
|
||||
## 3 总结
|
||||
|
||||
互联网真真切切渗透到社会每一个角落,从纯线上到与产业结合,从提升社会效率到关注人类健康,涉及到生活的方方面面,每一位公司的 CEO 都非常聪明,让互联网技术最大程度在各自领域发挥着价值。
|
||||
|
||||
商业领域如果有唯一不变的真理,那就是为人类带来价值的公司才能基业长青。你还了解哪些利用互联网给人类创造价值的公司吗?欢迎留言。
|
||||
|
||||
> 讨论地址是:[精读《极客公园 IFX - 下》 · Issue #226 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/226)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,77 @@
|
||||
## 1 引言
|
||||
|
||||
很荣幸被评为公司年度十佳作者,被要求写了这篇命题作文。
|
||||
|
||||
虽然我写了几年文章,稍稍学会了如何总结,但从来没想过要给自己 “做分享” 这件事做一个总结。这次我决定挑战一下自己,应邀写下这篇文章,谈谈我自己做分享这件事。
|
||||
|
||||
我将从 Why、What、How 三个角度去说明做分享这件事,分别阐述为什么做分享,做什么分享,以及如何做分享。
|
||||
|
||||
## 2 精读
|
||||
|
||||
### Why - 为什么要分享
|
||||
|
||||
在构思《前端精读》这个专栏的时候,那时网上的聚合专栏有很多,一般是每周收集一些优秀的技术、思考文章汇聚成一个列表,特别是一些知名度较高的头部专栏,用户阅读量很大,内容又多质量又好。当时我很羡慕这种模式,因为这种模式不用自己写文章,只要收集文章就可以了,而且在用户的监督下,也会促进你多阅读、多思考。
|
||||
|
||||
当时还萌生了另一个想法,就是现在《前端精读》的模式,每周找到一篇文章精读,并分享文章和自己对文章的观点。萌生这个想法的原因是,当时看了一些文章,觉得还不过瘾,想着如果把一系列关联知识串起来文章会更有价值,可是我并不能要求原文作者做这件事,因此就决定自己写关于这些文章的精读,将自己融会贯通后的理解展现给读者。但这样做有一些风险,首先就是自己写文章的要求比较高,我不能确定自己是否能坚持下来,其次是一周只写一篇,总感觉接收的信息量不如做聚合模式的大,毕竟别人一周就能看二、三十篇文章,而自己只能看一篇。
|
||||
|
||||
让我下决定的原因是看了一篇商业文章,是著名商业顾问刘润的一个观点:商业世界存在点、线、面、体,比如做一家杂志社就是一个点,做互联网信息收集入口就是线,而微博、微信都是面,整个社交行业就是体,每高一个维度都会对下一维度造成降维打击,所以科技行业才演变这么快,实际上是所处维度的不同。但高维也有自己脆弱的一面,就是竞争非常激烈,一个行业体中,通常是容不下太多面的。
|
||||
|
||||
同理,对写作来说,聚合专栏就是线,就像淘宝连接买家与卖家一样,聚合专栏收集优秀作者的文章,利用自己的流量分发给读者,但这个领域必然竞争激烈,当读者有了更好的线,为什么还需要差一些的呢?但做点就不一样了,你可以被无数线和想做线的人需要,你产出自己原创的价值,不会受到太大竞争影响。实际上我的经历也是这样,我可以将文章投放到各个平台(各个线上),这些线都成为了放大我影响力的工具,有越多的线,点的价值就越大,毕竟,想做线的人太多了。
|
||||
|
||||
在这里稍稍插一句,反过来,如果所有人都做点,只有极少数人做线,那线必定形成垄断,就像品牌商垄断农民货物一样,因为农民无法直达消费者,只能以很低的价格把农产品卖给品牌商,同样,消费者也只能通过品牌商买到货,所以品牌商就可以肆意加价。但互联网的发展改变了这些,无论是社交电商还是直播带货,都让生产者有了直接触达消费者的机会,就不用担心被中间商赚取差价;再者,如果大家觉得中间商有利可图,大量的品牌出现,生产者完全可以同时给多个品牌商供货,而在互联网分享的信息不会因为在一个平台的传播而消失,我们可以说文章与知识传播的边际成本完全为零,所以可以最大化利用多平台给自己带来优势。
|
||||
|
||||
所以我决定做一个点,将《前端精读》这个招牌培养起来。
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1pTGFtRr0gK0jSZFnXXbRRXXa-1052-741.png">
|
||||
|
||||
### What - 做什么分享
|
||||
|
||||
因为我的爱好与职业是前端,所以看上去要做什么分享这件事很简单,只要分享前端技术相关内容就可以了。但这几年持续下来发现,事情远远没有这么简单。
|
||||
|
||||
在分享刚一开始的时候,肚子里憋着一堆想说的话,恨不得一天写一篇精读,但奈何精力与表达能力有限,勉强以一周一篇的节奏坚持下来。写作的内容都是自己最熟悉、最想表达的前端技术内容,而且过程中为了活跃团队气氛,还拉上大家一起参与,坚持了蛮长一段时间。然而很快就遇到了第一个问题,坚持力问题。
|
||||
|
||||
持续做一件事情总会觉得枯燥,加上业务变得更有前途,大家都越来越忙碌,逐渐出现了下周找不到人写精读的情况,此时我选择顶上空缺。但毕竟那时候精读没有多少人关注,成就感不高,加上没有养成写作习惯,写一篇文章往往要花费一整周的精力,连续写两周就觉得非常痛苦,毕竟把自己的知识与想法写成文章有着不小的成本,对自己非常熟悉的知识感觉写下来有些浪费时间,逐渐觉得枯燥。
|
||||
|
||||
在枯燥的过程中,我逐渐培养出更快的写作速度,但一个严重的问题也渐渐浮现出来,我渐渐发现自己的存量知识已经见底,有时间写文章,但却不知道写什么。每周我都会从网上的聚合专栏寻找优秀的文章,但与其说寻找还不如说是过滤,因为很多知识我并没有深入了解,特别是技术领域大部分是英文文章,光看下来就费劲了,更别说写下自己的精读理解。但周更的频率不能停,我只能逼着着自己啃英文文章,从一眼看下去脑袋全懵的状态硬是培养到一眼扫下去就能评估出文章是否值得精读,这是个漫长的习惯过程,因为初期效率很低,唯一坚持下去的理由就是我知道未来阅读速度会越来越快,读英文文章的速度最终是可以追平读中文文章速度的。
|
||||
|
||||
渐渐的我可以通过快速阅读,每周掌握一些新知识,并通过与存量知识进行碰撞产生出新的理解,这解决了 “无话可写” 的尴尬情况,毕竟没有人能保证自己的存量知识够自己写 50 篇、100 篇的文章,现在精读更新到 100 多篇,绝大多数内容都是我新学到的,这也是写作带给我无比受益的地方。
|
||||
|
||||
前端内容写多了,不免觉得自己知识面还是太狭隘了,每周不是捣鼓新设计模式,就是研究新语法,关注技术新进展,这只能把自己培养成一颗 “黄金螺丝钉”,如果我未来能坚持十年,写了十年基础技术知识,可能也最多成为一颗 “钻石螺丝钉” 而已。我第一次非技术细节文章的尝试是第一百零三期的 [精读《为什么专家不再关心技术细节》](https://github.com/dt-fe/weekly/blob/v2/103.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%93%E5%AE%B6%E4%B8%8D%E5%86%8D%E5%85%B3%E5%BF%83%E6%8A%80%E6%9C%AF%E7%BB%86%E8%8A%82%E3%80%8B.md),这篇文章也道出了我对个人成长的看法:你想发挥更大的价值,就要能影响更多的人,研究 100 年前端技术成为不了马云;同理,让马云写前端,他也不可能一个人写出阿里巴巴。
|
||||
|
||||
实际上写作就是一件价值放大的事情,你将自己的优秀理念输出给其他人,让别人写出的代码和你一样优秀,这就可以提升整个团队的工作效率。但这还远远不够,代码只是软件研发流程的一部分,我逐渐发现,把握业务方向、做好团队管理这两大能力才能最大化输出自己的价值,所以后面又写了一些例如 [精读《前端未来展望》](https://github.com/dt-fe/weekly/blob/v2/111.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%9C%AA%E6%9D%A5%E5%B1%95%E6%9C%9B%E3%80%8B.md) 对前端进行综合展望,[精读《刷新》](https://github.com/dt-fe/weekly/blob/v2/116.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%88%B7%E6%96%B0%E3%80%8B.md) 对领导力进行领悟,以及一些极客公园系列文章增强对商业的理解,这些看似偏离前端技术的文章最终都是为前端服务,一个优秀的前端 Leader 具备的素质至少有:敏锐的商业嗅觉、清晰的理解业务方向、管理好团队,管理好人才、同时还是一个方向的技术大拿。
|
||||
|
||||
通过对商业、业务、管理的学习与写作,我并没有发现在专业知识上有多少延误,反而觉得自己以前认为是核心竞争力的 “技术思考” 变得越来越廉价,毕竟就前端技术甚至所有业务技术来说,理解任何一个技术点都没有绝对的壁垒,只要花费足够的时间就行了,难就难在需要花多久去理解,是否可以快速理解技术,理解业务。
|
||||
|
||||
<img width=220 src="https://img.alicdn.com/tfs/TB1x6uBtQL0gK0jSZFxXXXWHVXa-762-1004.png">
|
||||
|
||||
### How - 如何做分享
|
||||
|
||||
写作的时候,只要明确文章主旨,句句点题就不会写的太差,一定不要企图将你的想法在一篇文章中全部表达,毕竟你没写的不代表你不知道,而东拼西凑的文章对读者没什么益处,毕竟读者是为了某个明确目的来读文章的,如果内容和标题关系不大,读者大概率会选择离开。
|
||||
|
||||
上面是最基本的写作技巧,我就不继续展开了,接下来要重点聊聊的是前端精读是怎么做分享的。我会从如何写作、如何坚持、如何形成正循环三个方面谈谈自己的感受。
|
||||
|
||||
首先是写作方式,前端精读的命题很明确,就是基于某个文章或者观点进行精读,因此每篇文章都有一个明确的主题。第二步是摘要,讲文章内容精简的表达出来,这可以锻炼你的总结能力,也让读者能了解到背景知识。第三步是精读,这一步需要你有一些私藏干货,毕竟把文章直接翻译一遍是没有任何价值的,我在精读自己不熟悉领域的文章时经常遇到这个问题,此时我一般会找几篇类似的文章结合阅读,并找到一些可以互补的观点,这样的精读可以让文章的观点更加饱满。最后是总结,总结时可以点题,将重要内容再梳理一遍,也可以进行延伸,指出更进一步的思考方向。
|
||||
|
||||
为了让分享坚持下来,我在每周结束之前都会提前立好下周精读的 Flag,在 Github 开一个 issue,这样不仅可以提醒我周末的写作,还可以收获很多来自社区的讨论与反馈,让文章聚集了社区的智慧。这种提前立 Flag 的做法让我想到了自家小区物业费的收取方式,每年年初都会提前征收一整年的物业费,抛开商业手法不谈,这至少意味着物业对业务整整一年的承诺,这种承诺支撑了物业后续一整年的服务,也支撑了每周下一次的精读文章。
|
||||
|
||||
同时,我还找到了一种正循环模式促进写作,分别是让写作与工作、与分享、与生活结合。很自然的,我参与的数据中台工作本身就具有很大挑战性,工作中的内容与思考往往会成为精读内容的来源之一,比如之前写过的《手写 SQL 编译器》系列,因为数据工作中真的要用到这些知识。当我参加一些前端大会时,也可以顺便将分享稿整理成精读,将本来就要分享的内容分析得更彻底,有一种借力打力的感觉。在生活中,参加一些论坛,看过的书都可以成为精读的题材,无论是商业的,人文的,还是历史的,对多元化思维有帮助的内容都可以分享。
|
||||
|
||||
<img width=450 src="https://img.alicdn.com/tfs/TB1s5OAtHr1gK0jSZR0XXbP8XXa-1480-1136.png">
|
||||
|
||||
## 3 总结
|
||||
|
||||
回到主题,当我分享的时候,我在做什么?相信看完上面的内容,你已经得到了答案。
|
||||
|
||||
当我在分享时,我在传播知识,扩大自己的影响力,这是显而易见动作。但在这背后,我同时也在践行终身学习理念,每次分享都是一次新知识的学习,是一次知识边界的拓展。也许是一次对工作的思考,也许是一次对生活的感悟,然而每一次都是成长的记录。
|
||||
|
||||
思想不会因为传播给他人而减少,每一次分享都是在创造永不磨灭的价值,希望看到这篇文章的你也能认知到分享对自己、对他人的帮助,相信分享的力量,相信积累的力量。
|
||||
|
||||
> 讨论地址是:[精读《当我在分享的时候,我在做什么?》 · Issue #229 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/229)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,123 @@
|
||||
## 1 引言
|
||||
|
||||
本周精读的文章是 [Mastering JS console.log like a Pro](https://medium.com/javascript-in-plain-english/mastering-js-console-log-like-a-pro-1c634e6393f9),一起来更全面的认识 console 吧!
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
console 的功能主要在于控制台打印,它可以打印任何字符、对象、甚至 DOM 元素和系统信息,下面一一介绍。
|
||||
|
||||
### console.log( ) | info( ) | debug( ) | warn( ) | error( )
|
||||
|
||||
直接打印字符,区别在于展示形态的不同:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1xZ_WveH2gK0jSZFEXXcqMpXa-1492-566.png">
|
||||
|
||||
新版 chrome 控制台可以将打印信息分类:
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB1fZ2Vvhn1gK0jSZKPXXXvUXXa-420-446.png">
|
||||
|
||||
`log()` 与 `info()` 都对应 `info`,`warn()` 对应 `warnings`,`error()` 对应 `errors`,而 `debug()` 对应 `verbose`,因此建议在合适的场景使用合适的打印习惯,这样排查问题时也可以有针对性的筛选。
|
||||
|
||||
比如调试信息可以用 `console.debug` 仅在调试环境下输出,调试者即便开启了调试参数也不会影响正常 `info` 的查看,因为调试信息都输出在 `verbose` 中。
|
||||
|
||||
### 使用占位符
|
||||
|
||||
- %o — 对象
|
||||
- %s — 字符串
|
||||
- %d — 数字
|
||||
|
||||
如下所示,可通过占位符在一行中插入不同类型的值:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1GtL3vlr0gK0jSZFnXXbRRXXa-1840-504.png">
|
||||
|
||||
### 添加 CSS 样式
|
||||
|
||||
- %c - 样式
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1eK23vlr0gK0jSZFnXXbRRXXa-1832-978.png">
|
||||
|
||||
可以总结出,**console 支持输出复杂的内容,其输出能力堪比 HTML,但输入能力太弱,仅为字符串,因此采用了占位符 + 多入参修饰的设计模式解决这个问题。**
|
||||
|
||||
### console.dir( )
|
||||
|
||||
按 JSON 模式输出。笔者在这里也补充一句:`console.log()` 会自动判断类型,如果内容是 DOM 属性,则输出 DOM 树,但 `console.dir` 会强制以 JSON 模式输出,用在 DOM 对象时可强制转换为 JSON 输出。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1KQY1vbj1gK0jSZFuXXcrHpXa-922-302.png">
|
||||
|
||||
### 输出 HTML 元素
|
||||
|
||||
按照 HTML ELements 结构输出:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1mZ61va61gK0jSZFlXXXDKFXa-920-255.png">
|
||||
|
||||
这种输出结构和 Elements 打印形式是一致的,如果要看详细属性,可以使用 `console.dir()`。
|
||||
|
||||
### console.table
|
||||
|
||||
在控制台打印一个表格,属于功能增强。虽然仅文本也可以在控制台打印出漂亮的表格,但浏览器调试控制台的功能更强大,`console.table` 只是其富文本能力的一个体现。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1WldouKbviK0jSZFNXXaApXXa-928-742.png">
|
||||
|
||||
### console.group( ) & console.groupEnd( )
|
||||
|
||||
接下来是另一个富文本能力,按分组输出:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1UV6UvXY7gK0jSZKzXXaikpXa-919-377.png">
|
||||
|
||||
这种带有副作用的 API 显然是为方便阅读而设计的,然而在需要输出大量动态结构化数据的场景下,还需要进行结构转换,是比较麻烦的地方。
|
||||
|
||||
### console.count( )
|
||||
|
||||
`count()` 用来打印调用次数,一般用在循环或递归函数中。接收一个 `label` 参数以定制输出,默认直接输出 `1 2 3` 数字。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1ELLVveL2gK0jSZPhXXahvXXa-917-500.png">
|
||||
|
||||
### console.assert( )
|
||||
|
||||
`console` 版断言工具,当且仅当第一个参数值为 `false` 时才打印第二个参数作为输出。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1HEDUvfb2gK0jSZK9XXaEgFXa-1842-548.png">
|
||||
|
||||
这种输出结果为 error,所以也可被 `console.error` + 代码级别断言所取代。
|
||||
|
||||
### console.trace( )
|
||||
|
||||
打印此时的调用栈,在打印辅助调试信息时非常有用。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1Jh_YvkL0gK0jSZFAXXcA9pXa-1840-1096.png">
|
||||
|
||||
### console.time( )
|
||||
|
||||
打印代码执行时间,性能优化和监控场景比较常见。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1wAT2vbj1gK0jSZFuXXcrHpXa-1612-524.png">
|
||||
|
||||
### console.memory
|
||||
|
||||
打印内存使用情况。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1tPHYvkL0gK0jSZFAXXcA9pXa-1842-440.png">
|
||||
|
||||
### console.clear( )
|
||||
|
||||
清空控制台输出。
|
||||
|
||||
## 3 总结
|
||||
|
||||
`console` 提供了如此多的输出规范,其实也是在变相制定开发规范,毕竟离开发者最近的就是调试控制台,如果你的项目打印规范与标准规范有差异,那么调试时信息看起来就会很别扭。
|
||||
|
||||
可以看到,大部分开源库都良好的遵循了这套规范,比如三方库绝不会输出 `log()`,而且将错误、警告与调试信息正确分开,并尽量少的用 CSS 样式、分组、`table` 等功能,因为这些功能干扰性较强,不能保证所有用户都可接受。
|
||||
|
||||
相对的,项目源码就比较适合使用一些醒目的自定义规范,只要这套规则能被很好的执行起来。
|
||||
|
||||
最后留下一个讨论点:`console` 可以作为调试、招聘信息、隐藏菜单的投放点,你还看到过哪些有意思的 `console` 使用方式呢?欢迎留言。
|
||||
|
||||
> 讨论地址是:[精读《精通 console.log》 · Issue #228 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/228)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,279 @@
|
||||
## 1 引言
|
||||
|
||||
`JSON.parse` 是浏览器内置的 API,但如果面试官让你实现一个怎么办?好在有人已经帮忙做了这件事,本周我们一起精读这篇 [JSON Parser with Javascript](https://lihautan.com/json-parser-with-javascript/) 文章吧,再温习一遍大学时编译原理相关知识。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
要解析 JSON 首先要理解语法概念,之前的 [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md) 系列也有介绍过,不过本文介绍的更形象,看下面这个语法图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1EbjfvQL0gK0jSZFtXXXQCXXa-1837-857.png">
|
||||
|
||||
这是关于 Object 类型的语法描述图,从左向右看,根据箭头指向只要能走出这个迷宫就属于正确语法。
|
||||
|
||||
比如第一行 `{` → `whitespace` → `}` 表示 `{ }` 属于合法的 JSON 语法。
|
||||
|
||||
再比如观察向下的一条最长路线:`{` → `whitespace` → `string` → `whitespace` → `:` → `value` → `}` 表示 `{ string : value }` 属于合法的 JSON 语法。
|
||||
|
||||
你可能会问,双引号去哪儿了?这就是语法树最核心的概念了,这张图是关于 Object 类型的 **产生式**,同理还有 string、value 的产生式,产生式中可以嵌套其他产生式,甚至形成环路,以此拥有描述纷繁多变语法的能力。
|
||||
|
||||
最后我们再看一个环路,即 `{` → `whitespace` → `string` ... `,` → `whitespace` → `string` ... `,` ... `}`,我们发现,只要不走回头路,这条路是可以一直 “绕圈” 下去的,因此 Object 类型拥有了任意数量子字段的能力,只是每形成一个子字段,必须经过 `,` 号分割。
|
||||
|
||||
### 实现 Parser
|
||||
|
||||
首先实现一个基本结构:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
let i = 0;
|
||||
// TODO
|
||||
}
|
||||
```
|
||||
|
||||
`i` 表示访问字符的下标,当 `i` 走到字符串结尾表示遍历结束。
|
||||
|
||||
然后是下一步,用几个函数描述解析语法的过程:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
let i = 0;
|
||||
function parseObject() {
|
||||
if (str[i] === '{') {
|
||||
i++;
|
||||
skipWhitespace();
|
||||
|
||||
// if it is not '}',
|
||||
// we take the path of string -> whitespace -> ':' -> value -> ...
|
||||
while (str[i] !== '}') {
|
||||
const key = parseString();
|
||||
skipWhitespace();
|
||||
eatColon();
|
||||
const value = parseValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中 `skipWhitespace` 表示匹配并跳过空格,所谓匹配意味着匹配成功,此时 `i` 下标可以继续后移,否则匹配失败。下一步则判断如果 `i` 不是结束标志 `}`,则按照 `parseString` 匹配字符串 → `skipWhitespace` 跳过空格 → `eatColon` 吃掉冒号 → `parseValue` 匹配值,这个链路循环。其中吃掉冒号表示 “匹配冒号但不会产生任何结果,所以就像吃掉了一样”,吃这个动作还可以用在其他场景,比如吃掉尾分号。
|
||||
|
||||
> 对于看到这儿的小伙伴,笔者要友情提示一下,原文的思路是一种定制语法解析思路,无论是 `eatColon` 还是 `parseValue` 都仅具备解析 JSON 的通用性,但不具备解析任意语法的通用性。如果你想做一个具备解析任何通用语法的解析器,读入的内容应该是语法描述,处理方式必须更加通用,如果感兴趣可以阅读 [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md) 系列文章了解更多。
|
||||
|
||||
由于 Object 第一个元素前面不允许加逗号,因此可以利用 `initial` 做一个初始化判定,在初始时机不会吃掉逗号:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
let i = 0;
|
||||
function parseObject() {
|
||||
if (str[i] === '{') {
|
||||
i++;
|
||||
skipWhitespace();
|
||||
|
||||
let initial = true;
|
||||
// if it is not '}',
|
||||
// we take the path of string -> whitespace -> ':' -> value -> ...
|
||||
while (str[i] !== '}') {
|
||||
if (!initial) {
|
||||
eatComma();
|
||||
skipWhitespace();
|
||||
}
|
||||
const key = parseString();
|
||||
skipWhitespace();
|
||||
eatColon();
|
||||
const value = parseValue();
|
||||
initial = false;
|
||||
}
|
||||
// move to the next character of '}'
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
那么当第一个子元素前面存在逗号时,由于没有 “吃掉逗号” 这个功能,所以读到逗号会报错,语法解析提前结束。
|
||||
|
||||
吃逗号和吃冒号的代码都非常简单,即判断当前字符串必须是 “要吃的那个元素”,并且在吃掉后将 `i` 下标自增 1:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
// ...
|
||||
function eatComma() {
|
||||
if (str[i] !== ',') {
|
||||
throw new Error('Expected ",".');
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
function eatColon() {
|
||||
if (str[i] !== ':') {
|
||||
throw new Error('Expected ":".');
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在有了基本判定功能后,`fakeParseJSON` 需要返回 Object,因此我们只需在每个循环中对 Object 赋值,最后一并 return 即可:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
let i = 0;
|
||||
function parseObject() {
|
||||
if (str[i] === '{') {
|
||||
i++;
|
||||
skipWhitespace();
|
||||
|
||||
const result = {};
|
||||
|
||||
let initial = true;
|
||||
// if it is not '}',
|
||||
// we take the path of string -> whitespace -> ':' -> value -> ...
|
||||
while (str[i] !== '}') {
|
||||
if (!initial) {
|
||||
eatComma();
|
||||
skipWhitespace();
|
||||
}
|
||||
const key = parseString();
|
||||
skipWhitespace();
|
||||
eatColon();
|
||||
const value = parseValue();
|
||||
result[key] = value;
|
||||
initial = false;
|
||||
}
|
||||
// move to the next character of '}'
|
||||
i++;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
解析 Object 的代码就完成了。
|
||||
|
||||
接着试着解析 Array,下面是 Array 的语法图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1FvYjvKH2gK0jSZFEXXcqMpXa-1837-479.png">
|
||||
|
||||
我们只需要吃逗号和 `parseValue` 即可:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
// ...
|
||||
function parseArray() {
|
||||
if (str[i] === '[') {
|
||||
i++;
|
||||
skipWhitespace();
|
||||
|
||||
const result = [];
|
||||
let initial = true;
|
||||
while (str[i] !== ']') {
|
||||
if (!initial) {
|
||||
eatComma();
|
||||
}
|
||||
const value = parseValue();
|
||||
result.push(value);
|
||||
initial = false;
|
||||
}
|
||||
// move to the next character of ']'
|
||||
i++;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
接下来到了有趣的 `value` 语法图,可以看到 `value` 是许多种基础类型的 “或” 关系组成的:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1uGrmvND1gK0jSZFyXXciOVXa-1836-1293.png">
|
||||
|
||||
我们只需要继续拆解分析即可:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
// ...
|
||||
function parseValue() {
|
||||
skipWhitespace();
|
||||
const value =
|
||||
parseString() ??
|
||||
parseNumber() ??
|
||||
parseObject() ??
|
||||
parseArray() ??
|
||||
parseKeyword('true', true) ??
|
||||
parseKeyword('false', false) ??
|
||||
parseKeyword('null', null);
|
||||
skipWhitespace();
|
||||
return value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中 `parseKeyword` 函数用来解析一些保留关键字,比如将 `"true"` 解析成布尔类型 `true`:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
// ...
|
||||
function parseKeyword(name, value) {
|
||||
if (str.slice(i, i + name.length) === name) {
|
||||
i += name.length;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如上所示,只要在 name 与对应字符相等时,返回第二个传入参数即可。
|
||||
|
||||
### 处理异常输入
|
||||
|
||||
一个完整的语法解析功能需要包含错误处理,错误的情况主要分两种:
|
||||
|
||||
1. 非法字符。
|
||||
2. 非正常结尾。
|
||||
|
||||
原文提到的 JSON 错误提示优化非常棒,想想你在开发中突然看到下面的提示,是不是很蒙圈:
|
||||
|
||||
```text
|
||||
Unexpected token "a"
|
||||
```
|
||||
|
||||
既然我们是自己写的 JSON 解析器,就可以进行更友好的异常提示,比如:
|
||||
|
||||
```text
|
||||
// show
|
||||
{ "b"a
|
||||
^
|
||||
JSON_ERROR_001 Unexpected token "a".
|
||||
Expecting a ":" over here, eg:
|
||||
{ "b": "bar" }
|
||||
^
|
||||
You can learn more about valid JSON string in http://goo.gl/xxxxx
|
||||
```
|
||||
|
||||
更多 Demo 可以查看 [原文](https://lihautan.com/json-parser-with-javascript/)。
|
||||
|
||||
## 3 总结
|
||||
|
||||
这篇文章通过一个具体的例子解释如何做语法分析,对于词法解析入门非常直观,如果你想更深入理解语法解析,或者写一个通用语法解析器,可以阅读语法解析系列入门文章,笔者通过实际例子带你一步一步做一个完备的词法解析工具!
|
||||
|
||||
语法解析入门系列文章,建议阅读顺序:
|
||||
|
||||
- [精读《手写 SQL 编译器 - 词法分析》](https://github.com/dt-fe/weekly/blob/v2/064.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 文法介绍》](https://github.com/dt-fe/weekly/blob/v2/065.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%96%87%E6%B3%95%E4%BB%8B%E7%BB%8D%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 回溯》](https://github.com/dt-fe/weekly/blob/v2/067.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E5%9B%9E%E6%BA%AF%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 语法树》](https://github.com/dt-fe/weekly/blob/v2/070.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E6%A0%91%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 错误提示》](https://github.com/dt-fe/weekly/blob/v2/071.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E9%94%99%E8%AF%AF%E6%8F%90%E7%A4%BA%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 性能优化之缓存》](https://github.com/dt-fe/weekly/blob/v2/078.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E7%BC%93%E5%AD%98%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 智能提示》](https://github.com/dt-fe/weekly/blob/v2/085.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%99%BA%E8%83%BD%E6%8F%90%E7%A4%BA%E3%80%8B.md)
|
||||
|
||||
[syntax-parser](https://github.com/ascoders/syntax-parser) 这个零依赖的通用语法解析库就是根据上述文章一步一步完成的,看完了上面文章,就彻底理解了这个库的源码。
|
||||
|
||||
> 讨论地址是:[精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/233)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,233 @@
|
||||
## 1 引言
|
||||
|
||||
拖拽是前端非常常见的交互操作,但显然拖拽是强 DOM 交互的,而 React 绕过了 DOM 这一层,那么基于 React 的拖拽方案就必定值得聊一聊。
|
||||
|
||||
结合 [How To Use The HTML Drag-And-Drop API In React](https://www.smashingmagazine.com/2020/02/html-drag-drop-api-react/) 这篇文章,让我们谈谈 React 拖拽这些事。
|
||||
|
||||
## 2 概述
|
||||
|
||||
原文说的比较简单,笔者先快速介绍其中重点部分。
|
||||
|
||||
首先拖拽主要的 API 有 4 个:`dragEnter` `dragLeave` `dragOver` `drop`,分别对应拖入、拖出、正在当前元素范围内拖拽、完成拖入动作。
|
||||
|
||||
基于这些 API,我们可以利用 React 实现一个拖入区域:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
|
||||
const DragAndDrop = props => {
|
||||
const handleDragEnter = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDragLeave = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDragOver = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDrop = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={"drag-drop-zone"}
|
||||
onDrop={e => handleDrop(e)}
|
||||
onDragOver={e => handleDragOver(e)}
|
||||
onDragEnter={e => handleDragEnter(e)}
|
||||
onDragLeave={e => handleDragLeave(e)}
|
||||
>
|
||||
<p>Drag files here to upload</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default DragAndDrop;
|
||||
```
|
||||
|
||||
`preventDefault` 指的是阻止默认响应,这个响应可能是跳转页面之类的,`stopPropagation` 是阻止冒泡,这样同样监听了事件的父元素就不会收到响应,我们可以精准作用于嵌套的子元素。
|
||||
|
||||
接下来是拖拽状态管理,提到了 `useReducer`,顺便复习一下用法:
|
||||
|
||||
```jsx
|
||||
...
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_DROP_DEPTH':
|
||||
return { ...state, dropDepth: action.dropDepth }
|
||||
case 'SET_IN_DROP_ZONE':
|
||||
return { ...state, inDropZone: action.inDropZone };
|
||||
case 'ADD_FILE_TO_LIST':
|
||||
return { ...state, fileList: state.fileList.concat(action.files) };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
const [data, dispatch] = React.useReducer(
|
||||
reducer, { dropDepth: 0, inDropZone: false, fileList: [] }
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
最后一个关键点在于拖入后的处理,利用 `dispatch` 增加拖入文件、设置拖入状态即可:
|
||||
|
||||
```js
|
||||
const handleDrop = e => {
|
||||
...
|
||||
let files = [...e.dataTransfer.files];
|
||||
|
||||
if (files && files.length > 0) {
|
||||
const existingFiles = data.fileList.map(f => f.name)
|
||||
files = files.filter(f => !existingFiles.includes(f.name))
|
||||
|
||||
dispatch({ type: 'ADD_FILE_TO_LIST', files });
|
||||
e.dataTransfer.clearData();
|
||||
dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 });
|
||||
dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`e.dataTransfer.clearData` 函数用于清除拖拽过程中产生的临时变量,这些临时变量可以通过 `e.dataTransfer.xxx =` 的方式赋值,一般用于拖拽过程中值的传递。
|
||||
|
||||
总结一下,利用 HTML5 的 API 将拖拽转化为状态,最终通过状态映射到 UI。
|
||||
|
||||
原文内容还是比较简单的,笔者在精读部分再拓展一些更体系化的内容。
|
||||
|
||||
## 3 精读
|
||||
|
||||
现阶段拖拽主要分为两种,一种是 HTML5 原生规范的拖拽,这种方式在拖拽过程中不会影响 DOM 结构。另一种是完全所见即所得的拖拽方式,拖拽过程中 DOM 位置会随之变动,好处是可以立即反馈拖拽结果,当然缺点是华而不实,一旦用在生产环境,这种拖拽过程可能导致页面结构频繁跳动,反而看不清拖拽效果。
|
||||
|
||||
由于本文也采用了第一种拖拽方案,因为笔者再重新整理一遍自己的封装思路。
|
||||
|
||||
从使用角度反推,假设我们拥有一个拖拽库,那必定要拥有两个 API:
|
||||
|
||||
```jsx
|
||||
import { DragContainer, DropContainer } from 'dnd'
|
||||
|
||||
const DragItem = (
|
||||
<DragContainer>
|
||||
{({ dragProps }) => (
|
||||
<div {...dragProps} />
|
||||
)}
|
||||
</DragContainer>
|
||||
)
|
||||
|
||||
const DropItem = (
|
||||
<DropContainer>
|
||||
{({ dropProps }) => (
|
||||
<div {...dropProps} />
|
||||
)}
|
||||
</DropContainer>
|
||||
)
|
||||
```
|
||||
|
||||
`DragContainer` 包裹可以被拖拽的元素,`DropContainer` 包裹可以被拖入的元素,而至于 `dragProps` 与 `dropProps` 需要透传到子元素的 dom 节点,是为了利用 DOM API 控制拖拽效果,这也是拖拽唯一对 DOM 的要求,双方元素都需要有实体 DOM 承载。
|
||||
|
||||
而上面例子中给出 `dragProps` 与 `dropProps` 的方式属于 RenderProps,我们可以将 `children` 当作函数执行以达到效果:
|
||||
|
||||
```jsx
|
||||
const DragContainer = ({ children, componentId }) => {
|
||||
const { dragProps } = useDnd(componentId)
|
||||
|
||||
return children({
|
||||
dragProps
|
||||
})
|
||||
}
|
||||
|
||||
const DropContainer = ({ children, componentId }) => {
|
||||
const { dropProps } = useDnd(componentId)
|
||||
|
||||
return children({
|
||||
dropProps
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
那么这里创建了一个自定义 Hook `useDnd` 接收 `dragProps` 与 `dropProps`,这个自定义 Hook 可以这么写:
|
||||
|
||||
```jsx
|
||||
const useDnd = ({ componentId }) => {
|
||||
const dragProps = {}
|
||||
const dropProps = {}
|
||||
|
||||
return { dragProps, dropProps }
|
||||
}
|
||||
```
|
||||
|
||||
接下来,我们就要分别实现 `drag` 与 `drop` 了。
|
||||
|
||||
对 `drag` 来说,只要实现 `onDragStart` 与 `onDragEnd` 即可:
|
||||
|
||||
```jsx
|
||||
const dragProps = {
|
||||
onDragStart: ev => {
|
||||
ev.stopPropagation()
|
||||
ev.dataTransfer.setData('componentId', componentId)
|
||||
},
|
||||
onDragEnd: ev => {
|
||||
// 做一些拖拽结束的清理工作
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`stopPropagation` 的作用在原文简介中已经介绍过了,`setData` 则是通知拖拽方,当前拖拽的组件 id 是什么,**这是由于拖拽由 `drag` 发起而由 `drop` 响应,因此必须有个数据传输过程,而 `dataTransfer` 就最适合做这件事。**
|
||||
|
||||
对于 `drop` 来说,只要实现 `onDragOver` 与 `onDrop` 即可:
|
||||
|
||||
```jsx
|
||||
const dropProps = {
|
||||
onDropOver: ev => {
|
||||
// 做一些样式处理,提示用户此时松手会将元素防止在何处
|
||||
},
|
||||
onDrop: ev => {
|
||||
ev.stopPropagation()
|
||||
const componentId = ev.dataTransfer.getData('componentId')
|
||||
// 通过 componentId 修改数据,通过 React Rerender 刷新 UI
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
重点在 `onDrop`,它是实现拖拽效果的 “真正执行处”,最终通过修改 UI 的方式更新数据。
|
||||
|
||||
存在一种场景,一个容器既可以被拖动,也可以被拖入,这种情况一般这个组件是个容器,但这个容器可以被拖入到其他容器中,可以自由嵌套。
|
||||
|
||||
实现这种场景的方式就是将 `DragContainer` 与 `DropContainer` 作用到一个组件上:
|
||||
|
||||
```jsx
|
||||
const Box = (
|
||||
<DragContainer>
|
||||
{({ dragProps }) => (
|
||||
<DropContainer>
|
||||
{({ dropProps }) => {
|
||||
<div {...dragProps} {...dropProps} />
|
||||
}}
|
||||
</DropContainer>
|
||||
)}
|
||||
</DragContainer>
|
||||
)
|
||||
```
|
||||
|
||||
之所以能嵌套,在于 HTML5 的 API 允许一个元素同时拥有 `onDragStart`、`onDrop` 这两种属性,而上面的语法不过是同时将这两种属性传给组件 DOM。
|
||||
|
||||
所以,动手实现一个拖拽库就是这么简单,只要活用 HTML5 的拖拽 API,结合 React 一些特殊语法便够了。
|
||||
|
||||
## 4 总结
|
||||
|
||||
最后留下一个思考题,许多具有拖拽功能的系统都具备 “拖拽 placeholder” 的功能,即拖拽元素的过程中,在其 “落点” 位置展示一条横线或竖线,引导出松手后元素位置落点,如图所示:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB11H04wbY1gK0jSZTEXXXDQVXa-1434-384.png">
|
||||
|
||||
那么这条辅助线是通过什么方式实现的呢?欢迎在评论区留言!如果你有辅助线实现方案解析的文章,欢迎分享,也可以期待笔者未来专门写一篇 “拖拽 placeholder” 实现剖析的精读。
|
||||
|
||||
> 讨论地址是:[精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/233)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,108 @@
|
||||
## 1 引言
|
||||
|
||||
`useRef` 是常用的 API,但还有一个 `createRef` 的 API,你知道他们的区别吗?通过 [React.useRef and React.createRef: The Difference](https://blog.bitsrc.io/react-useref-and-react-createref-the-difference-afedb9877d0f) 这篇文章,你可以了解到何时该使用它们。
|
||||
|
||||
## 2 概述
|
||||
|
||||
其实原文就阐述了这样一个事实:`useRef` 仅能用在 FunctionComponent,`createRef` 仅能用在 ClassComponent。
|
||||
|
||||
第一句话是显然的,因为 Hooks 不能用在 ClassComponent。
|
||||
|
||||
第二句话的原因是,`createRef` 并没有 Hooks 的效果,其值会随着 FunctionComponent 重复执行而不断被初始化:
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
// 错误用法,永远也拿不到 ref
|
||||
const valueRef = React.createRef();
|
||||
return <div ref={valueRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
上述 `valueRef` 会随着 App 函数的 Render 而重复初始化,**这也是 Hooks 的独特之处,虽然用在普通函数中,但在 React 引擎中会得到超出普通函数的表现,比如初始化仅执行一次,或者引用不变**。
|
||||
|
||||
为什么 `createRef` 可以在 ClassComponent 正常运行呢?这是因为 ClassComponent 分离了生命周期,使例如 `componentDidMount` 等初始化时机仅执行一次。
|
||||
|
||||
原文完。
|
||||
|
||||
## 3 精读
|
||||
|
||||
那么知道如何正确创建 Ref 后,还知道如何正确更新 Ref 吗?
|
||||
|
||||
由于 Ref 是贯穿 FunctionComponent 所有渲染周期的实例,理论上在任何地方都可以做修改,比如:
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
const valueRef = React.useRef();
|
||||
|
||||
valueRef.current += 1;
|
||||
|
||||
return <div />;
|
||||
}
|
||||
```
|
||||
|
||||
但其实上面的修改方式是不规范的,React 官方文档里要求我们避免在 Render 函数中直接修改 Ref,请先看下面的 FunctionComponent 生命周期图:
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/tfs/TB12aHDwQL0gK0jSZFtXXXQCXXa-3300-2550.png">
|
||||
|
||||
从图中可以发现,在 `Render phase` 阶段是不允许做 “side effects” 的,也就是写副作用代码,这是因为这个阶段可能会被 React 引擎随时取消或重做。
|
||||
|
||||
修改 Ref 属于副作用操作,因此不适合在这个阶段进行。我们可以看到,在 `Commit phase` 阶段可以做这件事,或者在回调函数中做(脱离了 React 生命周期)。
|
||||
|
||||
当然有一种情况是可以的,即 [懒初始化](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily):
|
||||
|
||||
```ts
|
||||
function Image(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
// ✅ IntersectionObserver is created lazily once
|
||||
function getObserver() {
|
||||
if (ref.current === null) {
|
||||
ref.current = new IntersectionObserver(onIntersect);
|
||||
}
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
// When you need it, call getObserver()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
懒初始化的情况下,副作用最多执行一次,而且仅用于初始化赋值,所以这种行为是被允许的。
|
||||
|
||||
为什么对副作用限制的如此严格?因为 FunctionComponent 增加了内置调度系统,为了优先响应用户操作,可能会暂定某个 React 组件的渲染,具体可以看第 99 篇精读:[精读《Scheduling in React》](https://github.com/dt-fe/weekly/blob/v2/099.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md)
|
||||
|
||||
Ref 不仅可以拿到组件引用、创建一个 Mutable 副作用对象,还可以配合 `useEffect` 存储一个较老的值,最常用来拿到 `previousProps`,React 官方利用 Ref 封装了一个简单的 Hooks 拿到上一次的值:
|
||||
|
||||
```tsx
|
||||
function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
```
|
||||
|
||||
由于 `useEffect` 在 Render 完毕后才执行,因此 `ref` 的值在当前 Render 中永远是上一次 Render 时候的,我们可以利用它拿到上一次 Props:
|
||||
|
||||
```tsx
|
||||
function App(props) {
|
||||
const preProps = usePrevious(props);
|
||||
}
|
||||
```
|
||||
|
||||
要实现这个功能,还是要归功于 `ref` 可以将值 “在各个不同的 Render 闭包中传递的特性”。最后,不要滥用 Ref,Mutable 引用越多,对 React 来说可维护性一般会越差。
|
||||
|
||||
## 4 总结
|
||||
|
||||
你还挖掘了 `useRef` 哪些有意思的使用方式?欢迎在评论区留言。
|
||||
|
||||
> 讨论地址是:[精读《useRef 与 createRef 的区别》 · Issue #236 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/236)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,97 @@
|
||||
## 1 引言
|
||||
|
||||
任何软件都是协同开发的,所以 CodeReview 非常重要,它可以帮助你减少代码质量问题,提高开发效率,提升稳定性,同时还能保证软件架构的稳定性,防止代码结构被恶意破坏导致难以维护。
|
||||
|
||||
所以 CodeReview 机制是否健全是一个工程团队能否长期健康发展的决定因素之一,这次我们读一篇关于 CodeReview 如何做得更好的文章: [how-to-make-good-code-reviews-better](https://stackoverflow.blog/2019/09/30/how-to-make-good-code-reviews-better/)。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
作者结合自己在 Uber、微软的工作经历介绍了自己对如何做好 CodeReview 的看法。
|
||||
|
||||
### CodeReview 的覆盖范围
|
||||
|
||||
**Good CodeReview** 会检查代码的正确性、测试覆盖率、功能变化、是否遵循代码规范与最佳实践、可以指出一些较为明显的改进点,比如难以阅读的写法、未使用到变量、一些边界问题、commit 数量过大需要拆分等等。
|
||||
|
||||
**Better CodeReview** 会检查引入代码的必要性,与已有系统是否适配,是否具有可维护性,从抽象角度思考代码是否与已有系统逻辑能够自洽。
|
||||
|
||||
> Better CodeReview 会关注在可维护性层面,并具有全局性,往往几个局部正确的代码组合在一起会产生错误的结果,或者是没必要的代码,或者是相互冲突的逻辑。Better CodeReview 更多用在底层架构场景,因为架构底层模块关联比较紧密,需要有整体视角,而业务上层模块间最好采用解耦模式,这样不仅不需要更耗费精力的 Better CodeReview,也是一种更正确的架构设计。
|
||||
|
||||
### CodeReview 的语气
|
||||
|
||||
**Good CodeReview** 会给出建设性意见,而不是发表强硬措辞要求对方改正,或认为自己的意见是唯一正确的答案,因为这样的评论其实具有一定攻击性,激发对方的防御心理,产生敌对心态,这样会从内部瓦解一个团队。最好能给出建议,或者多个选择,给对方留有余地。
|
||||
|
||||
**Better CodeReview** 永远是考虑全面且正向积极的,会对写的好的地方进行鼓励,对写的不好的地方也体现出善解人意的关怀,考虑到对方可能花费了很多心血,以一种换位思考的鼓励心态进行评论。
|
||||
|
||||
> 其实读到语气这一章节,逐渐发现 CodeReview 不仅是一个技术专业行为,还是一个人与人相处的社交行为,有的人平时与人打交道非常谦逊,但在 CodeReview 中就变得尖酸刻薄,显然是只关注到了 CodeReview 的专业性这一面,忽略了社交性这一点。而要做到 Better CodeReview 还要学会换位思考,体现出包容、正向积极的态度,因为你技术经验更丰富,能指出别人的问题很正常,但能保持谦逊,让别人容易接受并受到鼓励,可以让你成为一个有气度的技术专家。
|
||||
|
||||
### 如何完成 CodeReview 的审阅
|
||||
|
||||
**Good CodeReview** 不会轻易通过那些开放式 PR,至少在其被得到充分讨论前,但每个 Review 者对自己关注的部分完成 Review 后需要进行反馈,无论是 “看起来不错” 或者用缩写单词 “LGTM”,之后需要有明确的跟进,比如通过协作软件通知作者进行进一步反馈。
|
||||
|
||||
**Better CodeReview** 实际执行中会更加灵活一些,对于一些比较紧急的改动会留下改进建议,但快速通过,让作者通过后续代码提交解决遗留的问题。
|
||||
|
||||
> 实际工作场景会遇到一些开放式或紧急的提交,良好的 CodeReview 习惯自然是要严谨一些,讨论清楚再通过,并且要及时反馈。但某些比较紧急的提交就要区别对待了,更好的态度是在实践中灵活对待,但及时紧急通过了,也要保证问题在后续得以修复,比如在代码中留一些 "TODO" 或 "FIXME" 的标记,写上对应的负责人与预期解决时间。
|
||||
|
||||
### 从 CodeReview 到直接交流
|
||||
|
||||
**Good CodeReview** 会给出完整的评论和修改建议,如果后续提交的代码不符合预期,Review 者可以直接与代码提交者面对面交流,这样可以避免后续花费更多沟通时间。
|
||||
|
||||
**Better CodeReview** 会在第一次给出完整的评论和修改建议,如果后续提交代码不符合预期,会立即与代码提交者当面沟通,避免异步沟通带来更多的理解偏差。
|
||||
|
||||
> 补充一下,在 PR 内容过多时也可以选择直接与提交者当面沟通,这样可以更多理解作者的想法,使 Review 准确性更高。另外并不要每次都直接交流,异步的 CodeReview 本身就是一种提效方案,这会使你工作节奏把握在自己手中,仅在这种方案出现沟通问题时再选择当面交流。
|
||||
|
||||
### 区分重点
|
||||
|
||||
**Good CodeReview** 可以区分提示的重要程度,并在不太重要的改动前面加上 “nit:” 标记,这样可以使提交者的注意力集中在重要的问题上。
|
||||
|
||||
**Better CodeReview** 会采取工具手段解决这些问题,比如一些代码 lint 工具,因为这些问题往往是可以被工具自动化解决的。
|
||||
|
||||
> 代码自动化工具的目的,很大一部分也是为了保证代码一致性,从而降低 CodeReview 成本,也减少不重要的评论信息出现,让 CodeReview 尽可能反馈逻辑问题而不是格式问题。
|
||||
|
||||
### 针对新人的 CodeReview
|
||||
|
||||
**Good CodeReview** 对任何人都是用相同评判标准,可以遵循上面几点注意事项。
|
||||
|
||||
**Better CodeReview** 会对新人区分对待,对新人给予对多的耐心、解释和评论,甚至给出解决方法,并更积极的给出鼓励。
|
||||
|
||||
> 任何人到一家新公司都有适应过程,一视同仁是 base 要求,但如果能给予新人更多关怀就更好啦。
|
||||
|
||||
### 跨办公区、时区的 CodeReview
|
||||
|
||||
**Good CodeReview** 仅在工作时间有重叠的时间范围内进行 CodeReview,这样能保证对方可以积极响应,在必要时进行语音、视频沟通。
|
||||
|
||||
**Better CodeReview** 会注意到更本质的问题,留意跨团队协作的必要性,如果某个模块经常被另一个时区同时修改,也许可以将这个模块交给对方维护,或者将 CodeReview 交给对方团队内部进行会更加高效。
|
||||
|
||||
> 笔者所在公司也有跨时区协作情况,但绝大部分场景会避免跨时区的 CodeReview,因为 CodeReview 一般会在同一时区团队内部进行,这样效率更高,应对跨时区协作时,往往也是电话、视频会议优先。
|
||||
|
||||
### 公司支持
|
||||
|
||||
**Good CodeReview** 会得到公司组织支持,公司能意识到这么做虽然看起来占用开发时间,但长远来看提升了开发效率,因此能任何 CodeReview 价值。
|
||||
|
||||
**Better CodeReview** 会得到公司进一步支持,公司甚至不断研发并完善 CodeReview 系统与流程,通过系统化方案保证上面几项 CodeReview 注意事项是否有在团队内落实,可以全员参与。
|
||||
|
||||
> CodeReview 也是一种团队文化和公司文化,公司文化带来的是规章制度与系统工具,团队文化带来的是良好 CodeReview 氛围与更高 CodeReview 的效率。
|
||||
|
||||
## 3 总结
|
||||
|
||||
总结一下,良好的 CodeReview 需要做到以下几点:
|
||||
|
||||
1. 更全面,从正确性到系统影响评估。
|
||||
2. 注意语气,从给出建设性一觉到换位思考。
|
||||
3. 及时完成审阅,从充分讨论到随机应变。
|
||||
4. 加强交流,从面对面交流到灵活选择最高效的沟通方式。
|
||||
5. 区分重点,从添加标记到利用工程化工具自动解决。
|
||||
6. 对新人要更友好。
|
||||
7. 尽量避免跨时区协作,必要时选择视频会议。
|
||||
|
||||
最后,希望 CodeReview 能够得到公司的支持,如果你们公司还没有认可 CodeReview 的价值,可以将这篇文章分享给你的领导。
|
||||
|
||||
> 讨论地址是:[精读《如何做好 CodeReview》 · Issue #237 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/237)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,302 @@
|
||||
## 1 引言
|
||||
|
||||
很多人都用过 React Suspense,但如果你认为它只是配合 React.lazy 实现异步加载的蒙层,就理解的太浅了。实际上,React Suspense 改变了开发规则,要理解这一点,需要作出思想上的改变。
|
||||
|
||||
我们结合 [Why React Suspense Will Be a Game Changer](https://medium.com/react-in-depth/why-react-suspense-will-be-a-game-changer-37b40fea71ec) 这篇文章,带你重新认识 React Suspense。
|
||||
|
||||
## 2 概述
|
||||
|
||||
异步加载是前端开发的重要环节,也是一直以来样板代码最严重的场景之一,原文通过三种取数方案的对比,逐渐找到一种最佳的异步取数方式。
|
||||
|
||||
在讲解这三种取数方案之前,首先通过下面这张图说明了 Suspense 的功能:
|
||||
|
||||

|
||||
|
||||
从上图可以看出,子元素在异步取数时会阻塞父组件渲染,并一直冒泡到最外层第一个 Suspense,此时 Suspense 不会渲染子组件,而是渲染 `fallback`,当所有子组件异步阻塞取消后才会正常渲染。
|
||||
|
||||
下面介绍文中给出的三种取数方式,首先是最原始的本地状态管理方案。
|
||||
|
||||
### 本地异步状态管理,直白但不利于维护
|
||||
|
||||
在 Suspense 方案出来之前,我们一般都在代码中利用本地状态管理异步数据。
|
||||
|
||||
即便代码做了一定抽象,那也只是把逻辑从一个文件移到了另一个问题,可维护性与可拓展性都没有本质的改变,因此基本可以用下面的结构说明:
|
||||
|
||||
```javascript
|
||||
class DynamicData extends Component {
|
||||
state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
data: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
fetchData(this.props.id)
|
||||
.then(data => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.id !== prevProps.id) {
|
||||
this.setState({ loading: true }, () => {
|
||||
fetchData(this.props.id)
|
||||
.then(data => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, error, data } = this.state;
|
||||
return loading ? (
|
||||
<p>Loading...</p>
|
||||
) : error ? (
|
||||
<p>Error: {error}</p>
|
||||
) : (
|
||||
<p>Data loaded ?</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如上所述,首先申明本地状态管理至少三种数据:异步状态、异步结果与异步错误,其次在不同的生命周期中处理初始化发请求与重新发请求的问题,最后在渲染函数中根据不同的状态渲染不同的结果,所以实际上我们写了三个渲染组件。
|
||||
|
||||
从下面几个角度对上述代码进行评价:
|
||||
|
||||
- **冗余的三种状态 - 糟糕的开发体验**
|
||||
- 很明显,存储了三套数据,渲染三种结果,不利于开发维护。
|
||||
- **冗余的样板代码 - 糟糕的开发体验**
|
||||
- 为了管理异步状态,上述代码非常冗长,显然这个问题是存在的。
|
||||
- **数据与状态封闭性 - 糟糕的用户体验 + 开发体验**
|
||||
- 所有数据与状态管理都存储在每一个这种组件中,将取数状态与组件绑定的结果就是,我们只能忍受组件独立运行的 Loading 逻辑,而无法对他们进行统一管理。
|
||||
- **重新取数 - 糟糕的开发体验**
|
||||
- 需要在另一个生命周期中申明重新取数,很明显是个麻烦的行为。
|
||||
- **一闪而过的短暂 Loading - 糟糕的用户体验**
|
||||
- 如果用户网速足够快,则 Loading 时间会非常短,此时一闪而过的 Loading 反而比没有 Loading 更烦人,我们应该在用户感知到卡的时候再出现 Loading 状态。
|
||||
|
||||
### Context 管理状态,有进步但问题依然很多
|
||||
|
||||
如果利用 Context 做状态共享,我们将取数的数据管理与逻辑代码写在父组件,子组件专心用于展示,效果会好一些,代码如下:
|
||||
|
||||
```javascript
|
||||
const DataContext = React.createContext();
|
||||
|
||||
class DataContextProvider extends Component {
|
||||
// We want to be able to store multiple sources in the provider,
|
||||
// so we store an object with unique keys for each data set +
|
||||
// loading state
|
||||
state = {
|
||||
data: {},
|
||||
fetch: this.fetch.bind(this)
|
||||
};
|
||||
|
||||
fetch(key) {
|
||||
if (this.state[key] && (this.state[key].data || this.state[key].loading)) {
|
||||
// Data is either already loaded or loading, so no need to fetch!
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
[key]: {
|
||||
loading: true,
|
||||
error: null,
|
||||
data: null
|
||||
}
|
||||
},
|
||||
() => {
|
||||
fetchData(key)
|
||||
.then(data => {
|
||||
this.setState({
|
||||
[key]: {
|
||||
loading: false,
|
||||
data
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.setState({
|
||||
[key]: {
|
||||
loading: false,
|
||||
error: e.message
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <DataContext.Provider value={this.state} {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicData extends Component {
|
||||
static contextType = DataContext;
|
||||
|
||||
componentDidMount() {
|
||||
this.context.fetch(this.props.id);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.id !== prevProps.id) {
|
||||
this.context.fetch(this.props.id);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { id } = this.props;
|
||||
const { data } = this.context;
|
||||
|
||||
const idData = data[id];
|
||||
|
||||
return idData.loading ? (
|
||||
<p>Loading...</p>
|
||||
) : idData.error ? (
|
||||
<p>Error: {idData.error}</p>
|
||||
) : (
|
||||
<p>Data loaded ?</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`DataContextProvider` 组件承担了状态管理与异步逻辑工作,而 `DynamicData` 组件只需要从 Context 获取异步状态渲染即可,这样来看至少解决了一部分问题,我们还是从之前的角度进行评价:
|
||||
|
||||
- **冗余的三种状态 - 糟糕的开发体验**
|
||||
- 问题依然存在,只不过代码的位置转移了一部分到父组件。
|
||||
- **冗余的样板代码 - 糟糕的开发体验**
|
||||
- 将展示与逻辑分离,成功降低了样板代码数量,至少当一个异步数据复用于多个组件时,不需要写多份样板代码了。
|
||||
- **数据与状态封闭性 - 糟糕的用户体验 + 开发体验**
|
||||
- 这个问题得到一定程度解决,但是引入了新问题,即这个子组件仅在特定环境下可以正常运行。但在一个良好的设计下,组件运行不应该依赖于它所处的位置。
|
||||
- **重新取数 - 糟糕的开发体验**
|
||||
- 问题依然存在。
|
||||
- **一闪而过的短暂 Loading - 糟糕的用户体验**
|
||||
- 问题依然存在。
|
||||
|
||||
### Suspense 管理状态,最棒的方案
|
||||
|
||||
利用 Suspense 进行异步处理,代码处理大概是这样的:
|
||||
|
||||
```javascript
|
||||
import createResource from "./magical-cache-provider";
|
||||
const dataResource = createResource(id => fetchData(id));
|
||||
|
||||
class DynamicData extends Component {
|
||||
render() {
|
||||
const data = dataResource.read(this.props.id);
|
||||
return <p>Data loaded ?</p>;
|
||||
}
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<DeepNesting>
|
||||
<DynamicData />
|
||||
</DeepNesting>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在原文写作的时候,Suspense 仅能对 React.lazy 生效,但现在已经可以对任何异步状态生效了,只要符合 Pending 中 throw promise 的规则。
|
||||
|
||||
我们再审视一下上面的代码,可以发现代码量减少了很多,其中和转换成 Function Component 的写法也有关系。
|
||||
|
||||
最后还是从如下几个角度进行评价:
|
||||
|
||||
- **冗余的三种状态 - 糟糕的开发体验** - ⭐️
|
||||
- 可以看到,组件只要处理成功得到数据的状态即可,三种状态合并成了一种状态。
|
||||
- **冗余的样板代码 - 糟糕的开发体验** - ⭐️
|
||||
- 展示与逻辑完全分离,展示只要拿到数据展示 UI 即可。
|
||||
- **数据与状态封闭性 - 糟糕的用户体验 + 开发体验** - ⭐️
|
||||
- 这个问题得到了完美的解决,具体看下面详细介绍。
|
||||
- **重新取数 - 糟糕的开发体验** - ⭐️
|
||||
- 不需要关心何时需要重新取数,当数据变化时会自动执行。
|
||||
- **一闪而过的短暂 Loading - 糟糕的用户体验**
|
||||
- 问题依然存在。
|
||||
|
||||
为了进一步说明 Suspense 的魔力,笔者特意把这段代码单独拿出来说明:
|
||||
|
||||
```javascript
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<DeepNesting>
|
||||
<MaybeSomeAsycComponent />
|
||||
<Suspense fallback={<p>Loading content...</p>}>
|
||||
<ThereMightBeSeveralAsyncComponentsHere />
|
||||
</Suspense>
|
||||
<Suspense fallback={<p>Loading footer...</p>}>
|
||||
<DeeplyNestedFooterTree />
|
||||
</Suspense>
|
||||
</DeepNesting>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上面代码表明了逻辑与展示的完美分离。
|
||||
|
||||
从代码结构上来看,我们可以在任何需要异步取数的组件父级添加 Suspense 达到 Loading 的效果,也就是说,如果只在最外层加一个 Suspense,那么整个应用所有 Loading 都结束后才会渲染,然而我们也能随心所欲的在任何层级继续添加 Suspense,那么对应作用域内的 Loading 就会首先执行完毕,并由当前的 Suspense 控制。
|
||||
|
||||
**这意味着我们可以自由决定 Loading 状态的范围组合。** 试想当 Loading 状态交由组件控制的方案一与方案二,是不可能做到合并 Loading 时机的,而 Suspense 方案做到了将 Loading 状态与 UI 分离,我们可以通过添加 Suspense 自由控制 Loading 的粒度。
|
||||
|
||||
## 3 精读
|
||||
|
||||
Suspense 对所有子组件异步都可以作用,因此无论是 React.lazy 还是异步取数,都可以通过 Suspense 进行 Pending。
|
||||
|
||||
异步时机被 Suspense pending 需要遵循一定规则,这个规则在之前的 [精读《Hooks 取数 - swr 源码》](https://github.com/dt-fe/weekly/blob/v2/128.%E7%B2%BE%E8%AF%BB%E3%80%8AHooks%20%E5%8F%96%E6%95%B0%20-%20swr%20%E6%BA%90%E7%A0%81%E3%80%8B.md) 有介绍过,即 Suspense 要求代码 suspended,即抛出一个可以被捕获的 Promise 异常,在这个 Promise 结束后再渲染组件,因此取数函数需要在 Pending 状态时抛出一个 Promise,使其可以被 Suspense 捕获到。
|
||||
|
||||
另外,关于文中提到的 fallback 最小出现时间的保护间隔,目前还是一个 [Open Issue](https://github.com/facebook/react/issues/17351),也许有一天 React 官方会提供支持。
|
||||
|
||||
不过即便官方不支持,我们也有方式实现,即让这个逻辑由 fallback 组件实现:
|
||||
|
||||
```jsx
|
||||
<Suspense fallback={MyFallback} />;
|
||||
|
||||
const MyFallback = () => {
|
||||
// 计时器,200 ms 以内 return null,200 ms 后 return <Spin />
|
||||
};
|
||||
```
|
||||
|
||||
## 4 总结
|
||||
|
||||
之所以说 Suspense 开发方式改变了开发规则,是因为它做到了将异步的状态管理与 UI 组件分离,所有 UI 组件都无需关心 Pending 状态,而是当作同步去执行,这本身就是一个巨大的改变。
|
||||
|
||||
另外由于状态的分离,我们可以利用纯 UI 组件拼装任意粒度的 Pending 行为,以整个 App 作为一个大的 Suspense 作为兜底,这样 UI 彻底与异步解耦,哪里 Loading,什么范围内 Loading,完全由 Suspense 组合方式决定,这样的代码显然具备了更强的可拓展性。
|
||||
|
||||
> 讨论地址是:[精读《Suspense 改变开发方式》 · Issue #238 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/238)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,132 @@
|
||||
## 1 引言
|
||||
|
||||
先说结论:Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!
|
||||
|
||||
我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。
|
||||
|
||||
模块联邦是 Webpack5 新内置的一个重要功能,可以让跨应用间真正做到模块共享,所以这周让我们通过 [webpack-5-module-federation-a-game-changer-in-javascript-architecture](https://indepth.dev/webpack-5-module-federation-a-game-changer-in-javascript-architecture/#its-important-to-note-these-are-special-entry-points-they-are-only-a-few-kb-in-size-containing-a-special-webpack-runtime-that-can-interface-with-the-host-it-is-not-a-standard-entry-point--7/) 这篇文章了解什么是 “模块联邦” 功能。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
### NPM 方式共享模块
|
||||
|
||||
想象一下正常的共享模块方式,对,就是 NPM。
|
||||
|
||||
如下图所示,正常的代码共享需要将依赖作为 Lib 安装到项目,进行 Webpack 打包构建再上线,如下图:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1MoLPy.z1gK0jSZLeXXb9kVXa-2494-1478.png">
|
||||
|
||||
对于项目 Home 与 Search,需要共享一个模块时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中。
|
||||
|
||||
虽然 Monorepo 可以一定程度解决重复安装和修改困难的问题,但依然需要走本地编译。
|
||||
|
||||
### UMD 方式共享模块
|
||||
|
||||
真正 Runtime 的方式可能是 UMD 方式共享代码模块,即将模块用 Webpack UMD 模式打包,并输出到其他项目中。这是非常普遍的模块共享方式:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1rQnSy4n1gK0jSZKPXXXvUXXa-2404-1484.png">
|
||||
|
||||
对于项目 Home 与 Search,直接利用 UMD 包复用一个模块。但这种技术方案问题也很明显,就是包体积无法达到本地编译时的优化效果,且库之间容易冲突。
|
||||
|
||||
### 微前端方式共享模块
|
||||
|
||||
微前端:micro-frontends (MFE) 也是最近比较火的模块共享管理方式,微前端就是要解决多项目并存问题,多项目并存的最大问题就是模块共享,不能有冲突。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1vqvTy1T2gK0jSZFvXXXnFXXa-2410-1520.png">
|
||||
|
||||
由于微前端还要考虑样式冲突、生命周期管理,所以本文只聚焦在资源加载方式上。微前端一般有两种打包方式:
|
||||
|
||||
1. 子应用独立打包,模块更解耦,但无法抽取公共依赖等。
|
||||
2. 整体应用一起打包,很好解决上面的问题,但打包速度实在是太慢了,不具备水平扩展能力。
|
||||
|
||||
### 模块联邦方式
|
||||
|
||||
终于提到本文的主角了,作为 Webpack5 内置核心特性之一的 Federated Module:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1qLz1yYj1gK0jSZFuXXcrHpXa-2414-1474.png">
|
||||
|
||||
从图中可以看到,这个方案是直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。
|
||||
|
||||
让应用具备模块化输出能力,其实开辟了一种新的应用形态,即 “中心应用”,这个中心应用用于在线动态分发 Runtime 子模块,并不直接提供给用户使用:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1ymbWy7Y2gK0jSZFgXXc5OFXa-1346-1442.png">
|
||||
|
||||
对微前端而言,这张图就是一个完美的主应用,因为所有子应用都可以利用 Runtime 方式复用主应用的 Npm 包和模块,更好的集成到主应用中。
|
||||
|
||||
模块联邦的使用方式如下:
|
||||
|
||||
```js
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
|
||||
|
||||
module.exports = {
|
||||
// other webpack configs...
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: "app_one_remote",
|
||||
remotes: {
|
||||
app_two: "app_two_remote",
|
||||
app_three: "app_three_remote"
|
||||
},
|
||||
exposes: {
|
||||
AppContainer: "./src/App"
|
||||
},
|
||||
shared: ["react", "react-dom", "react-router-dom"]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./public/index.html",
|
||||
chunks: ["main"]
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
模块联邦本身是一个普通的 Webpack 插件 `ModuleFederationPlugin`,插件有几个重要参数:
|
||||
|
||||
1. `name` 当前应用名称,需要全局唯一。
|
||||
2. `remotes` 可以将其他项目的 `name` 映射到当前项目中。
|
||||
3. `exposes` 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。
|
||||
4. `shared` 是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。
|
||||
|
||||
比如设置了 `remotes: { app_two: "app_two_remote" }`,在代码中就可以直接利用以下方式直接从对方应用调用模块:
|
||||
|
||||
```js
|
||||
import { Search } from "app_two/Search";
|
||||
```
|
||||
|
||||
这个 `app_two/Search` 来自于 `app_two` 的配置:
|
||||
|
||||
```js
|
||||
// app_two 的 webpack 配置
|
||||
export default {
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: "app_two",
|
||||
library: { type: "var", name: "app_two" },
|
||||
filename: "remoteEntry.js",
|
||||
exposes: {
|
||||
Search: "./src/Search"
|
||||
},
|
||||
shared: ["react", "react-dom"]
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
正是因为 `Search` 在 `exposes` 被导出,我们因此可以使用 `[name]/[exposes_name]` 这个模块,这个模块对于被引用应用来说是一个本地模块。
|
||||
|
||||
## 3 总结
|
||||
|
||||
模块联邦为更大型的前端应用提供了开箱解决方案,并已经作为 Webpack5 官方模块内置,可以说是继 Externals 后最终的运行时代码复用解决方案。
|
||||
|
||||
另外 Webpack5 还内置了大量编译时缓存功能,可以看到,无论是性能还是多项目组织,Webpack5 都在尝试给出自己的最佳思路,期待 Webpack5 正式发布,前端工程化会迈向一个新的阶段。
|
||||
|
||||
> 讨论地址是:[精读《Webpack5 新特性 - 模块联邦》 · Issue #239 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/239)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,384 @@
|
||||
## 1 引言
|
||||
|
||||
[React Router v6](https://github.com/ReactTraining/react-router) alpha 版本发布了,本周通过 [A Sneak Peek at React Router v6](https://alligator.io/react/react-router-v6/) 这篇文章分析一下带来的改变。
|
||||
|
||||
## 2 概述
|
||||
|
||||
### <Switch> 更名为 <Routes>
|
||||
|
||||
一个不痛不痒的改动,使 API 命名更加规范。
|
||||
|
||||
```jsx
|
||||
// v5
|
||||
import { BrowserRouter, Switch, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route path="/profile">
|
||||
<Profile />
|
||||
</Route>
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
在 React Router v6 版本里,直接使用 `Routes` 替代 `Switch`:
|
||||
|
||||
```jsx
|
||||
// v6
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="profile/*" element={<Profile />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### <Route> 升级
|
||||
|
||||
在 v5 版本里,想要给组件传参数是不太直观的,需要利用 RenderProps 的方式透传 `routeProps`:
|
||||
|
||||
```jsx
|
||||
import Profile from './Profile';
|
||||
|
||||
// v5
|
||||
<Route path=":userId" component={Profile} />
|
||||
<Route
|
||||
path=":userId"
|
||||
render={routeProps => (
|
||||
<Profile {...routeProps} animate={true} />
|
||||
)}
|
||||
/>
|
||||
|
||||
// v6
|
||||
<Route path=":userId" element={<Profile />} />
|
||||
<Route path=":userId" element={<Profile animate={true} />} />
|
||||
```
|
||||
|
||||
而在 v6 版本中,`render` 与 `component` 方案合并成了 `element` 方案,可以轻松传递 props 且不需要透传 `roteProps` 参数。
|
||||
|
||||
### 更方便的嵌套路由
|
||||
|
||||
在 v5 版本中,嵌套路由需要通过 `useRouteMatch` 拿到 `match`,并通过 `match.path` 的拼接实现子路由:
|
||||
|
||||
```jsx
|
||||
// v5
|
||||
import {
|
||||
BrowserRouter,
|
||||
Switch,
|
||||
Route,
|
||||
Link,
|
||||
useRouteMatch
|
||||
} from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="/profile" component={Profile} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
let match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav>
|
||||
<Link to={`${match.url}/me`}>My Profile</Link>
|
||||
</nav>
|
||||
|
||||
<Switch>
|
||||
<Route path={`${match.path}/me`}>
|
||||
<MyProfile />
|
||||
</Route>
|
||||
<Route path={`${match.path}/:id`}>
|
||||
<OthersProfile />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
在 v6 版本中省去了 `useRouteMatch` 这一步,支持直接用 `path` 表示相对路径:
|
||||
|
||||
```jsx
|
||||
// v6
|
||||
import { BrowserRouter, Routes, Route, Link, Outlet } from "react-router-dom";
|
||||
|
||||
// Approach #1
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="profile/*" element={<Profile />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
return (
|
||||
<div>
|
||||
<nav>
|
||||
<Link to="me">My Profile</Link>
|
||||
</nav>
|
||||
|
||||
<Routes>
|
||||
<Route path="me" element={<MyProfile />} />
|
||||
<Route path=":id" element={<OthersProfile />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Approach #2
|
||||
// You can also define all
|
||||
// <Route> in a single place
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="profile" element={<Profile />}>
|
||||
<Route path=":id" element={<MyProfile />} />
|
||||
<Route path="me" element={<OthersProfile />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
return (
|
||||
<div>
|
||||
<nav>
|
||||
<Link to="me">My Profile</Link>
|
||||
</nav>
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
注意 `Outlet` 是渲染子路由的 Element。
|
||||
|
||||
### useNavigate 替代 useHistory
|
||||
|
||||
在 v5 版本中,主动跳转路由可以通过 `useHistory` 进行 `history.push` 等操作:
|
||||
|
||||
```jsx
|
||||
// v5
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
function MyButton() {
|
||||
let history = useHistory();
|
||||
function handleClick() {
|
||||
history.push("/home");
|
||||
}
|
||||
return <button onClick={handleClick}>Submit</button>;
|
||||
}
|
||||
```
|
||||
|
||||
而在 v6 版本中,可以通过 `useNavigate` 直接实现这个常用操作:
|
||||
|
||||
```jsx
|
||||
// v6
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function MyButton() {
|
||||
let navigate = useNavigate();
|
||||
function handleClick() {
|
||||
navigate("/home");
|
||||
}
|
||||
return <button onClick={handleClick}>Submit</button>;
|
||||
}
|
||||
```
|
||||
|
||||
react-router 内部对 history 进行了封装,如果需要 `history.replace`,可以通过 `{ replace: true }` 参数指定:
|
||||
|
||||
```jsx
|
||||
// v5
|
||||
history.push("/home");
|
||||
history.replace("/home");
|
||||
|
||||
// v6
|
||||
navigate("/home");
|
||||
navigate("/home", { replace: true });
|
||||
```
|
||||
|
||||
### 更小的体积 8kb
|
||||
|
||||
由于代码几乎重构,v6 版本的代码压缩后体积从 20kb 缩小到 8kb。
|
||||
|
||||
## 3 精读
|
||||
|
||||
react-router v6 源码中有一段比较核心的理念,笔者拿出来与大家分享,对一些框架开发是大有裨益的。我们看 `useRoutes` 这段代码节选:
|
||||
|
||||
```jsx
|
||||
export function useRoutes(routes, basename = "", caseSensitive = false) {
|
||||
let {
|
||||
params: parentParams,
|
||||
pathname: parentPathname,
|
||||
route: parentRoute
|
||||
} = React.useContext(RouteContext);
|
||||
|
||||
if (warnAboutMissingTrailingSplatAt) {
|
||||
// ...
|
||||
}
|
||||
|
||||
basename = basename ? joinPaths([parentPathname, basename]) : parentPathname;
|
||||
|
||||
let navigate = useNavigate();
|
||||
let location = useLocation();
|
||||
let matches = React.useMemo(
|
||||
() => matchRoutes(routes, location, basename, caseSensitive),
|
||||
[routes, location, basename, caseSensitive]
|
||||
);
|
||||
|
||||
// ...
|
||||
|
||||
// Otherwise render an element.
|
||||
let element = matches.reduceRight((outlet, { params, pathname, route }) => {
|
||||
return (
|
||||
<RouteContext.Provider
|
||||
children={route.element}
|
||||
value={{
|
||||
outlet,
|
||||
params: readOnly({ ...parentParams, ...params }),
|
||||
pathname: joinPaths([basename, pathname]),
|
||||
route
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, null);
|
||||
|
||||
return element;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,利用 `React.Context`,v6 版本在每个路由元素渲染时都包裹了一层 `RouteContext`。
|
||||
|
||||
拿更方便的路由嵌套来说:
|
||||
|
||||
> 在 v6 版本中省去了 `useRouteMatch` 这一步,支持直接用 `path` 表示相对路径。
|
||||
|
||||
这就是利用这个方案做到的,因为给每一层路由文件包裹了 Context,所以在每一层都可以拿到上一层的 `path`,因此在拼接路由时可以完全由框架内部实现,而不需要用户在调用时预先拼接好。
|
||||
|
||||
再以 `useNavigate` 举例,有人觉得 `navigate` 这个封装仅停留在形式层,但其实在功能上也有封装,比如如果传入但是一个相对路径,会根据当前路由进行切换,下面是 `useNavigate` 代码节选:
|
||||
|
||||
```jsx
|
||||
export function useNavigate() {
|
||||
let { history, pending } = React.useContext(LocationContext);
|
||||
let { pathname } = React.useContext(RouteContext);
|
||||
|
||||
let navigate = React.useCallback(
|
||||
(to, { replace, state } = {}) => {
|
||||
if (typeof to === "number") {
|
||||
history.go(to);
|
||||
} else {
|
||||
let relativeTo = resolveLocation(to, pathname);
|
||||
|
||||
let method = !!replace || pending ? "replace" : "push";
|
||||
history[method](relativeTo, state);
|
||||
}
|
||||
},
|
||||
[history, pending, pathname]
|
||||
);
|
||||
|
||||
return navigate;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,利用 `RouteContext` 拿到当前的 `pathname`,并根据 `resolveLocation` 对 `to` 与 `pathname` 进行路径拼接,而 `pathname` 就是通过 `RouteContext.Provider` 提供的。
|
||||
|
||||
### 巧用多层 Context Provider
|
||||
|
||||
很多时候我们利用 Context 停留在一个 `Provider`,多个 `useContext` 的层面上,这是 Context 最基础的用法,但相信读完 React Router v6 这篇文章,我们可以挖掘出 Context 更多的用法:多层 Context Provider。
|
||||
|
||||
**虽然说 Context Provider 存在多层会采取最近覆盖的原则,但这不仅仅是一条规避错误的功能,我们可以利用这个功能实现 React Router v6 这样的改良。**
|
||||
|
||||
为了更仔细说明这个特性,这里再举一个具体的例子:比如实现搭建渲染引擎时,每个组件都有一个 id,但这个 id 并不透出在组件的 props 上:
|
||||
|
||||
```jsx
|
||||
const Input = () => {
|
||||
// Input 组件在画布中会自动生成一个 id,但这个 id 组件无法通过 props 拿到
|
||||
};
|
||||
```
|
||||
|
||||
此时如果我们允许 Input 组件内部再创建一个子元素,又希望这个子元素的 id 是由 Input 推导出来的,我们可能需要用户这么做:
|
||||
|
||||
```jsx
|
||||
const Input = ({ id }) => {
|
||||
return <ComponentLoader id={id + "1"} />;
|
||||
};
|
||||
```
|
||||
|
||||
这样做有两个问题:
|
||||
|
||||
1. 将 id 暴露给 Input 组件,违背了之前设计的简洁性。
|
||||
2. 组件需要对 id 进行拼装,很麻烦。
|
||||
|
||||
这里遇到的问题和 React Router 遇到的一样,我们可以将代码简化成下面这样,但功能不变吗?
|
||||
|
||||
```jsx
|
||||
const Input = () => {
|
||||
return <ComponentLoader id="1" />;
|
||||
};
|
||||
```
|
||||
|
||||
答案是可以做到,我们可以利用 Context 实现这种方案。关键点就在于,渲染 Input 但组件容器需要包裹一个 Provider:
|
||||
|
||||
```jsx
|
||||
const ComponentLoader = ({ id, element }) => {
|
||||
<Context.Provider value={{ id }}>{element}</Context.Provider>;
|
||||
};
|
||||
```
|
||||
|
||||
那么对于内部的组件来说,在不同层级下调用 `useContext` 拿到的 id 是不同的,这正是我们想要的效果:
|
||||
|
||||
```jsx
|
||||
const ComponentLoader = ({id,element}) => {
|
||||
const { id: parentId } = useContext(Context)
|
||||
|
||||
<Context.Provider value={{ id: parentId + id }}>
|
||||
{element}
|
||||
</Context.Provider>
|
||||
}
|
||||
```
|
||||
|
||||
这样我们在 `Input` 内部调用的 `<ComponentLoader id="1" />` 实际上拼接的实际 id 是 `01`,而这完全抛到了外部引擎层处理,用户无需手动拼接。
|
||||
|
||||
## 4 总结
|
||||
|
||||
React Router v6 完全利用 Hooks 重构后,不仅代码量精简了很多,还变得更好用了,等发正式版的时候可以快速升级一波。
|
||||
|
||||
另外从 React Router v6 做的这些优化中,我们从源码中挖掘到了关于 Context 更巧妙的用法,希望这个方法可以帮助你运用到其他更复杂的项目设计中。
|
||||
|
||||
> 讨论地址是:[精读《React Router v6》 · Issue #241 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/241)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,354 @@
|
||||
## 1 引言
|
||||
|
||||
React Hooks 渐渐被国内前端团队所接受,但基于 Hooks 的数据流方案却还未固定,我们有 “100 种” 类似的选择,却各有利弊,让人难以取舍。
|
||||
|
||||
本周笔者就深入谈一谈对 Hooks 数据流的理解,相信读完文章后,可以从百花齐放的 Hooks 数据流方案中看到本质。
|
||||
|
||||
## 2 精读
|
||||
|
||||
基于 React Hooks 谈数据流,我们先从最不容易产生分歧的基础方案说起。
|
||||
|
||||
### 单组件数据流
|
||||
|
||||
单组件最简单的数据流一定是 `useState`:
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const [count, setCount] = useState();
|
||||
}
|
||||
```
|
||||
|
||||
`useState` 在组件内用是毫无争议的,那么下个话题就一定是跨组件共享数据流了。
|
||||
|
||||
### 组件间共享数据流
|
||||
|
||||
跨组件最简单的方案就是 `useContext`:
|
||||
|
||||
```jsx
|
||||
const CountContext = createContext();
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState();
|
||||
return (
|
||||
<CountContext.Provider value={{ count, setCount }}>
|
||||
<Child />
|
||||
</CountContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
const { count } = useContext(CountContext);
|
||||
}
|
||||
```
|
||||
|
||||
用法都是官方 API,显然也是毫无争议的,但问题是数据与 UI 不解耦,这个问题 [unstated-next](https://github.com/jamiebuilds/unstated-next) 已经为你想好解决方案了。
|
||||
|
||||
### 数据流与组件解耦
|
||||
|
||||
[unstated-next](https://github.com/jamiebuilds/unstated-next) 可以帮你把上面例子中,定义在 `App` 中的数据单独出来,形成一个自定义数据管理 Hook:
|
||||
|
||||
```jsx
|
||||
import { createContainer } from "unstated-next";
|
||||
|
||||
function useCounter() {
|
||||
const [count, setCount] = useState();
|
||||
return { count, setCount };
|
||||
}
|
||||
|
||||
const Counter = createContainer(useCounter);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Counter.Provider>
|
||||
<Child />
|
||||
</Counter.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
const { count } = Counter.useContainer();
|
||||
}
|
||||
```
|
||||
|
||||
数据与 `App` 就解耦了,这下 `Counter` 再也不和 `App` 绑定了,`Counter` 可以和其他组件绑定作用了。
|
||||
|
||||
这个时候性能问题就慢慢浮出了水面,首当其冲的就是 `useState` 无法合并更新的问题,我们自然想到利用 `useReducer` 解决。
|
||||
|
||||
### 合并更新
|
||||
|
||||
`useReducer` 可以让数据合并更新,这也是 React 官方 API,毫无争议:
|
||||
|
||||
```jsx
|
||||
import { createContainer } from "unstated-next";
|
||||
|
||||
function useCounter() {
|
||||
const [state, dispath] = useReducer(
|
||||
(state, action) => {
|
||||
switch (action.type) {
|
||||
case "setCount":
|
||||
return {
|
||||
...state,
|
||||
count: action.setCount(state.count),
|
||||
};
|
||||
case "setFoo":
|
||||
return {
|
||||
...state,
|
||||
foo: action.setFoo(state.foo),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
},
|
||||
{ count: 0, foo: 0 }
|
||||
);
|
||||
|
||||
return { ...state, dispatch };
|
||||
}
|
||||
|
||||
const Counter = createContainer(useCounter);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Counter.Provider>
|
||||
<Child />
|
||||
</Counter.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
const { count } = Counter.useContainer();
|
||||
}
|
||||
```
|
||||
|
||||
这下即便要同时更新 `count` 和 `foo`,我们也能通过抽象成一个 `reducer` 的方式合并更新。
|
||||
|
||||
然而还有性能问题:
|
||||
|
||||
```jsx
|
||||
function ChildCount() {
|
||||
const { count } = Counter.useContainer();
|
||||
}
|
||||
|
||||
function ChildFoo() {
|
||||
const { foo } = Counter.useContainer();
|
||||
}
|
||||
```
|
||||
|
||||
更新 `foo` 时,`ChildCount` 和 `ChildFoo` 同时会执行,但 `ChildCount` 没用到 `foo` 呀?这个原因是 `Counter.useContainer` 提供的数据流是一个引用整体,其子节点 `foo` 引用变化后会导致整个 Hook 重新执行,继而所有引用它的组件也会重新渲染。
|
||||
|
||||
此时我们发现可以利用 Redux `useSelector` 实现按需更新。
|
||||
|
||||
### 按需更新
|
||||
|
||||
首先我们利用 Redux 对数据流做一次改造:
|
||||
|
||||
```jsx
|
||||
import { createStore } from "redux";
|
||||
import { Provider, useSelector } from "react-redux";
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "setCount":
|
||||
return {
|
||||
...state,
|
||||
count: action.setCount(state.count),
|
||||
};
|
||||
case "setFoo":
|
||||
return {
|
||||
...state,
|
||||
foo: action.setFoo(state.foo),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Child />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
const { count } = useSelector(
|
||||
(state) => ({ count: state.count }),
|
||||
shallowEqual
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`useSelector` 可以让 `Child` 在 `count` 变化时才更新,而 `foo` 变化时不更新,这已经接近较为理想的性能目标了。
|
||||
|
||||
但 `useSelector` 的作用仅仅是计算结果不变化时阻止组件刷新,但并不能保证返回结果的引用不变化。
|
||||
|
||||
### 防止数据引用频繁变化
|
||||
|
||||
对于上面的场景,拿到 `count` 的引用是不变的,**但对于其他场景就不一定了**。
|
||||
|
||||
举个例子:
|
||||
|
||||
```jsx
|
||||
function Child() {
|
||||
const user = useSelector((state) => ({ user: state.user }), shallowEqual);
|
||||
|
||||
return <UserPage user={user} />;
|
||||
}
|
||||
```
|
||||
|
||||
**假设 `user` 对象在每次数据流更新引用都会发生变化**,那么 `shallowEqual` 自然是不起作用,那我们换成 `deepEqual`深对比呢?结果是引用依然会变,只是重渲染不那么频繁了:
|
||||
|
||||
```jsx
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => ({ user: state.user }),
|
||||
// 当 user 值变化时才重渲染
|
||||
deepEqual
|
||||
);
|
||||
|
||||
// 但此处拿到的 user 引用还是会变化
|
||||
|
||||
return <UserPage user={user} />;
|
||||
}
|
||||
```
|
||||
|
||||
是不是觉得在 `deepEqual` 的作用下,没有触发重渲染,`user` 的引用就不会变呢?答案是会变,因为 `user` 对象在每次数据流更新都会变,`useSelector` 在 `deepEqual` 作用下没有触发重渲染,但因为全局 reducer 隐去组件自己的重渲染依然会重新执行此函数,此时拿到的 `user` 引用会不断变化。
|
||||
|
||||
因此 `useSelector` `deepEqual` 一定要和 `useDeepMemo` 结合使用,才能保证 `user` 引用不会频繁改变:
|
||||
|
||||
```jsx
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => ({ user: state.user }),
|
||||
// 当 user 值变化时才重渲染
|
||||
deepEqual
|
||||
);
|
||||
|
||||
const userDeep = useDeepMemo(() => user, [user]);
|
||||
|
||||
return <UserPage user={userDeep} />;
|
||||
}
|
||||
```
|
||||
|
||||
当然这是比较极端的情况,只要看到 `deepEqual` 与 `useSelector` 同时作用了,就要问问自己其返回的值的引用会不会发生意外变化。
|
||||
|
||||
### 缓存查询函数
|
||||
|
||||
对于极限场景,即便控制了重渲染次数与返回结果的引用最大程度不变,还是可能存在性能问题,这最后一块性能问题就处在查询函数上。
|
||||
|
||||
上面的例子中,查询函数比较简单,但如果查询函数非常复杂就不一样了:
|
||||
|
||||
```jsx
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => ({ user: verySlowFunction(state.user) }),
|
||||
// 当 user 值变化时才重渲染
|
||||
deepEqual
|
||||
);
|
||||
|
||||
const userDeep = useDeepMemo(() => user, [user]);
|
||||
|
||||
return <UserPage user={userDeep} />;
|
||||
}
|
||||
```
|
||||
|
||||
我们假设 `verySlowFunction` 要遍历画布中 1000 个组件的 n 3 次方次,那组件的重渲染时间消耗与查询时间相比完全不值一提,我们需要考虑缓存查询函数。
|
||||
|
||||
一种方式是利用 [reselect](https://github.com/reduxjs/reselect) 根据参数引用进行缓存。
|
||||
|
||||
想象一下,如果 `state.user` 的引用不频繁变化,但 `verySlowFunction` 非常慢,理想情况是 `state.user` 引用变化后才重新执行 `verySlowFunction`,但上面的例子中,`useSelector` 并不知道还能这么优化,只能傻傻的每次渲染重复执行 `verySlowFunction`,哪怕 `state.user` 没有变。
|
||||
|
||||
此时我们要告诉引用,`state.user` 是否变化才是重新执行的关键:
|
||||
|
||||
```jsx
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const userSelector = createSelector(
|
||||
(state) => state.user,
|
||||
(user) => verySlowFunction(user)
|
||||
);
|
||||
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => userSelector(state),
|
||||
// 当 user 值变化时才重渲染
|
||||
deepEqual
|
||||
);
|
||||
|
||||
const userDeep = useDeepMemo(() => user, [user]);
|
||||
|
||||
return <UserPage user={userDeep} />;
|
||||
}
|
||||
```
|
||||
|
||||
在上面的例子中,通过 `createSelector` 创建的 `userSelector` 会一层层进行缓存,当第一个参数返回的 `state.user` 引用不变时,会直接返回上一次执行结果,直到其应用变化了才会继续往下执行。
|
||||
|
||||
> 这也说明了函数式保持幂等的重要性,如果 `verySlowFunction` 不是严格幂等的,这种缓存也无法实施。
|
||||
|
||||
看上去很美好,然而实战中你可能发现没有那么美好,因为上面的例子都建立在 **Selector 完全不依赖外部变量**。
|
||||
|
||||
### 结合外部变量的缓存查询
|
||||
|
||||
如果我们要查询的用户来自于不同地区,需要传递 `areaId` 加以识别,那么可以拆分为两个 Selector 函数:
|
||||
|
||||
```jsx
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const areaSelector = (state, props) => state.areas[props.areaId].user;
|
||||
|
||||
const userSelector = createSelector(areaSelector, (user) =>
|
||||
verySlowFunction(user)
|
||||
);
|
||||
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => userSelector(state, { areaId: 1 }),
|
||||
deepEqual
|
||||
);
|
||||
|
||||
const userDeep = useDeepMemo(() => user, [user]);
|
||||
|
||||
return <UserPage user={userDeep} />;
|
||||
}
|
||||
```
|
||||
|
||||
所以为了不在组件函数内调用 `createSelector`,我们需要尽可能将用到外部变量的地方抽象成一个通用 Selector,并作为 `createSelector` 的一个先手环节。
|
||||
|
||||
但 `userSelector` 提供给多个组件使用时缓存会失效,原因是我们只创建了一个 Selector 实例,因此这个函数还需要再包装一层高阶形态:
|
||||
|
||||
```jsx
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const userSelector = () =>
|
||||
createSelector(areaSelector, (user) => verySlowFunction(user));
|
||||
|
||||
function Child() {
|
||||
const customSelector = useMemo(userSelector, []);
|
||||
|
||||
const user = useSelector(
|
||||
(state) => customSelector(state, { areaId: 1 }),
|
||||
deepEqual
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
所以对于外部变量结合的环节,还需要 `useMemo` 与 `useSelector` 结合使用,`useMemo` 处理外部变量依赖的引用缓存,`useSelector` 处理 Store 相关引用缓存。
|
||||
|
||||
## 3 总结
|
||||
|
||||
基于 Hooks 的数据流方案不能算完美,我在写作这篇文章时就感觉到这种方案属于 “浅入深出”,简单场景还容易理解,随着场景逐步复杂,方案也变得越来越复杂。
|
||||
|
||||
但这种 Immutable 的数据流管理思路给了开发者非常自由的缓存控制能力,只要透彻理解上述概念,就可以开发出非常 “符合预期” 的数据缓存管理模型,只要精心维护,一切就变得非常有秩序。
|
||||
|
||||
> 讨论地址是:[精读《React Hooks 数据流》 · Issue #242 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/242)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,141 @@
|
||||
## 1 引言
|
||||
|
||||
从 [@types/react](https://unpkg.com/browse/@types/react@16.9.34/index.d.ts) 源码中挖掘一些 Typescript 使用技巧吧。
|
||||
|
||||
## 2 精读
|
||||
|
||||
### 泛型 extends
|
||||
|
||||
泛型可以指代可能的参数类型,但指代任意类型范围太模糊,当我们需要对参数类型加以限制,或者确定只处理某种类型参数时,就可以对泛型进行 extends 修饰。
|
||||
|
||||
问题:`React.lazy` 需要限制返回值是一个 `Promise<T>` 类型,且 `T` 必须是 React 组件类型。
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
function lazy<T extends ComponentType<any>>(
|
||||
factory: () => Promise<{ default: T }>
|
||||
): LazyExoticComponent<T>;
|
||||
```
|
||||
|
||||
`T extends ComponentType` 确保了 T 这个类型一定符合 `ComponentType` 这个 React 组件类型定义,我们再将 T 用到 `Promise<{ default: T }>` 位置即可。
|
||||
|
||||
## 泛型 extends + infer
|
||||
|
||||
如果有一种场景,需要拿到一个类型,这个类型是当某个参数符合某种结构时,这个结构内的一种子类型,就需要结合 泛型 extends + infer 了。
|
||||
|
||||
问题:`React.useReducer` 第一个参数是 Reducer,第二个参数是初始化参数,其实第二个参数的类型是第一个参数中回调函数第一个参数的类型,那我们怎么将这两个参数的关系联系到一起呢?
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
function useReducer<R extends Reducer<any, any>, I>(
|
||||
reducer: R,
|
||||
initializerArg: I & ReducerState<R>,
|
||||
initializer: (arg: I & ReducerState<R>) => ReducerState<R>
|
||||
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
|
||||
|
||||
type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any>
|
||||
? S
|
||||
: never;
|
||||
```
|
||||
|
||||
`R extends Reducer<any, any>` 的意思在上面已经提过了,也就是 R 必须符合 `Reducer` 结构,也就是 `reducer` 必须符合这个结构,之后重点来了:`initializerArg` 利用 `ReducerState` 这个类型直接从 `reducer` 的类型 `R` 中将第一个回调参数挖了出来并返回。
|
||||
|
||||
`ReducerState` 定义中 `R extends Reducer<infer S, any> ? S : never` 的含义是:如果 R 符合 `Reducer<infer S, any>` 类型,则返回类型 `S`,这个 `S` 是 `Reducer<infer S>` 也就是 State 位置的类型,否则返回 `never` 类型。
|
||||
|
||||
所以 infer 表示待推断类型,是非常强大的功能,可以指定在任意位置代指其类型,并配合 extends 判断是否符合结构,可以使类型推断具备一定编程能力。
|
||||
|
||||
要用 extends 的另一个原因是,只有 extends 才能将结构描述出来,我们才能精确定义 infer 指代类型的位置。
|
||||
|
||||
### 类型重载
|
||||
|
||||
当一个类型拥有多种使用可能性时,可以采用类型重载定义复数类型,Typescript 作用时会逐个匹配并找到第一个满足条件的。
|
||||
|
||||
问题:`createElement` 第一个参数支持 FunctionComponent 与 ClassComponent,而且传入参数不同,返回值的类型也不同。
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
function createElement<P extends {}>(
|
||||
type: FunctionComponent<P>,
|
||||
props?: (Attributes & P) | null,
|
||||
...children: ReactNode[]
|
||||
): FunctionComponentElement<P>;
|
||||
function createElement<P extends {}>(
|
||||
type: ClassType<
|
||||
P,
|
||||
ClassicComponent<P, ComponentState>,
|
||||
ClassicComponentClass<P>
|
||||
>,
|
||||
props?: (ClassAttributes<ClassicComponent<P, ComponentState>> & P) | null,
|
||||
...children: ReactNode[]
|
||||
): CElement<P, ClassicComponent<P, ComponentState>>;
|
||||
```
|
||||
|
||||
将 `createElement` 写两遍及以上,并配合不同的参数类型与返回值类型即可。
|
||||
|
||||
### 自定义类型收窄
|
||||
|
||||
我们可以通过 `typeof` 或 `instanceof` 做一些类型收窄工作,但有些类型甚至自定义类型的收窄判断函数需要自定义,我们可以通过 `is` 关键字定义自定义类型收窄判断函数。
|
||||
|
||||
问题:`isValidElement` 判断对象是否是合法的 React 元素,我们希望这个函数具备类型收窄的功能。
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
function isValidElement<P>(
|
||||
object: {} | null | undefined
|
||||
): object is ReactElement<P>;
|
||||
|
||||
const element: string | ReactElement = "";
|
||||
|
||||
if (isValidElement(element)) {
|
||||
element; // 自动推导类型为 ReactElement
|
||||
} else {
|
||||
element; // 自动推导类型为 string
|
||||
}
|
||||
```
|
||||
|
||||
基于这个方案,我们可以创建一些很有用的函数,比如 `isArray`,`isMap`,`isSet` 等等,通过 `is` 关键字时其被调用时具备类型收窄的功能。
|
||||
|
||||
### 用 Interface 定义函数
|
||||
|
||||
一般定义函数类型我们用 `type`,但有些情况下定义的函数既可被调用,也有一些默认属性值需要定义,我们可以继续用 Interface 定义。
|
||||
|
||||
问题:`FunctionComponent` 既可以当作函数调用,同时又能定义 `defaultProps` `displayName` 等固定属性。
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
interface FunctionComponent<P = {}> {
|
||||
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
|
||||
propTypes?: WeakValidationMap<P>;
|
||||
contextTypes?: ValidationMap<any>;
|
||||
defaultProps?: Partial<P>;
|
||||
displayName?: string;
|
||||
}
|
||||
```
|
||||
|
||||
`(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null` 表示这种类型的变量可以作为函数执行:
|
||||
|
||||
```jsx
|
||||
const App: FunctionComponent = () => <div />;
|
||||
App.displayName = "App";
|
||||
```
|
||||
|
||||
## 3 总结
|
||||
|
||||
看完文章内容,相信你已经可以独立读懂 [@types/react](https://unpkg.com/browse/@types/react@16.9.34/index.d.ts) 这个包的所有类型定义!
|
||||
|
||||
更多基础内容可以阅读 [精读《Typescript2.0 - 2.9》](https://github.com/dt-fe/weekly/blob/7de3c77c3bdd7304c9e4b0c0f70c3ba6968ebd29/058.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md) 与 [精读《Typescript 3.2 新特性》](https://github.com/dt-fe/weekly/blob/v2/084.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%203.2%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md),由于 TS 更新频繁,后续 TS 技巧可能继续以阅读源码方式进行,希望这次选用的 React 类型源码可以让你印象深刻。
|
||||
|
||||
> 讨论地址是:[精读《@types/react 值得注意的 TS 技巧》 · Issue #245 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/245)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,133 @@
|
||||
## 1 引言
|
||||
|
||||
Error Boundaries 是 React16 提出来用来捕获渲染时错误的概念,今天我们一起读一读 [A Simple Guide to Error Boundaries in React](https://alligator.io/react/error-boundaries/) 这篇文章,了解一下这个重要机制。
|
||||
|
||||
## 2 概述
|
||||
|
||||
Error Boundaries 可以用来捕获渲染时错误,API 如下:
|
||||
|
||||
```jsx
|
||||
class MyErrorBoundary extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// 更新 state,下次渲染可以展示错误相关的 UI
|
||||
return { error: error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
// 错误上报
|
||||
logErrorToMyService(error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
// 渲染出错时的 UI
|
||||
return <p>Something broke</p>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `static getDerivedStateFromError`: 在出错后有机会修改 state 触发最后一次错误 fallback 的渲染。
|
||||
- `componentDidCatch`: 用于出错时副作用代码,比如错误上报等。
|
||||
|
||||
这两种方法中任意一个被定义时,这个组件就会成为 `Error Boundary` 组件,可以阻止子组件渲染时报错。
|
||||
|
||||
最后作者还提出一个建议,建议将 Error Boundary 单独作为一个组件,而不是将错误监听方法与业务组件耦合,一方面考虑到复用,另一方面则因为错误检测只对子组件生效。
|
||||
|
||||
好吧,其实 React 官方文档比这篇文章介绍的详细的多得多,原文介绍到此结束。
|
||||
|
||||
## 3 精读
|
||||
|
||||
[React Error Boundaries 官方文档](https://reactjs.org/docs/error-boundaries.html) 里提到了四种无法 Catch 的错误场景:
|
||||
|
||||
1. 回调事件。由于回调事件执行时机不在渲染周期内,因此无法被 Error Boundary Catch 住,如有必要得自行 try/catch。
|
||||
2. 异步。比如 `setTimeout` 或 `requestAnimationFrame`,和第一条同理。
|
||||
3. 服务端渲染。
|
||||
4. Error Boundary 组件自身触发的错误。因为只能捕获其子组件的错误。
|
||||
|
||||
这也是使用 Error Boundaries 最容易有疑问的地方。除了上面的情况,笔者结合自身经验再列举几种异常边界场景。
|
||||
|
||||
### 无法捕获编译时错误
|
||||
|
||||
很明显,即便是 React 官方 API `Error Boundary` 也只能捕获运行时错误,而对编译时错误无能为力。
|
||||
|
||||
编译时错误包括不限于编译环境错误、运行前的框架错误检查提示、TS/Flow 类型错误等,这些都是 `Error Boundary` 无法捕获的,而且没有更好的办法 Catch 住,遇到编译错误就在编译时解决吧,仅关注运行时错误就好了。
|
||||
|
||||
### 可以作用于 Function Component
|
||||
|
||||
虽然函数式组件无法定义 `Error Boundary`,但 `Error Boundary` 可以捕获函数式组件的错误,因此可以曲线救国:
|
||||
|
||||
```jsx
|
||||
// ErrorBoundary 组件
|
||||
class ErrorBoundary extends React.Component {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 可以捕获所有组件异常,包括 Function Component 的子组件
|
||||
const App = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Child />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 对 Hooks 也可生效
|
||||
|
||||
对于 Hooks 中异常也可以生效,比如下面的代码:
|
||||
|
||||
```jsx
|
||||
const Child = (props) => {
|
||||
React.useEffect(() => {
|
||||
console.log(1);
|
||||
props.a.b;
|
||||
console.log(2);
|
||||
}, [props.a.b]);
|
||||
|
||||
return <div />;
|
||||
};
|
||||
```
|
||||
|
||||
要注意的是,出现在 deps 中的错误会立即被 Catch,导致 `console.log(1)` 都无法打印。但如果是下面的代码,则可以打印出 `console.log(1)`,无法打印出 `console.log(2)`:
|
||||
|
||||
```jsx
|
||||
const Child = (props) => {
|
||||
React.useEffect(() => {
|
||||
console.log(1);
|
||||
props.a.b;
|
||||
console.log(2);
|
||||
}, []);
|
||||
|
||||
return <div />;
|
||||
};
|
||||
```
|
||||
|
||||
所以 React 官网的这句话并不是指 `Error Boundary` 对 Hooks 不生效,而是指 `Error Boundary` 无法以 Hooks 方式指定,对功能是没有影响的:
|
||||
|
||||
> componentDidCatch and getDerivedStateFromError: There are no Hook equivalents for these methods yet, but they will be added soon.
|
||||
|
||||
所以这里的理解要注意一下,另外 React 官方文档 [Hooks FQA](https://reactjs.org/docs/hooks-faq.html#how-do-lifecycle-methods-correspond-to-hooks) 有很多宝藏,建议抽时间逐条阅读。
|
||||
|
||||
## 4 总结
|
||||
|
||||
`Error Boundary` 可以捕获所有子元素渲染时异常,包括 render、各生命周期函数,但也有很多使用限制,希望你可以正确使用它。
|
||||
|
||||
错误捕获也不是万能的,更多时候我们要避免并及时修复错误,通过错误捕获降低出错时对用户体验的影响,并在第一时间内监控起来并快速修复。
|
||||
|
||||
最后,你有明明正确使用了 `Error Boundary` 却依然无法 Catch 住的错误 Case 吗?
|
||||
|
||||
> 讨论地址是:[精读《React Error Boundaries》 · Issue #246 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/246)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,199 @@
|
||||
## 1 引言
|
||||
|
||||
在数据中台做 BI 工具经常面对海量数据的渲染处理,除了组件本身性能优化之外,经常要排查整体页面性能瓶颈点,尤其是维护一些性能做得并不好的旧代码时。
|
||||
|
||||
React 性能调试是面对这种问题的必修课,借助 [Profiling React.js Performance](https://addyosmani.com/blog/profiling-react-js/) 这篇文章一起学习一下这个技能吧。
|
||||
|
||||
## 2 精读
|
||||
|
||||
本文介绍了众多性能检测工具与方法。
|
||||
|
||||
### React Profiler
|
||||
|
||||
`Profiler` 这个 API 是一种运行时 Debug 的补充,可以通过其 callback 拿到组件渲染信息,用法如下:
|
||||
|
||||
```jsx
|
||||
const Movies = ({ movies, addToQueue }) => (
|
||||
<React.Profiler id="Movies" onRender={callback}>
|
||||
<div />
|
||||
</React.Profiler>
|
||||
);
|
||||
|
||||
function callback(
|
||||
id,
|
||||
phase,
|
||||
actualTime,
|
||||
baseTime,
|
||||
startTime,
|
||||
commitTime,
|
||||
interactions
|
||||
) {}
|
||||
```
|
||||
|
||||
这个 callback 会在每次渲染时执行,渲染分为初始化和更新阶段,通过 `phase` 区分,下面是参数详细说明:
|
||||
|
||||
- id: 传入的 id。
|
||||
- phase: "mount" 或 "update",表示更新状态。
|
||||
- actualDuration: 实际渲染耗时。
|
||||
- baseDuration: 没有使用 memo 时的渲染预计耗时。
|
||||
- startTime: 开始渲染的时间。
|
||||
- commitTime: React 提交更新的时间
|
||||
- interactions: 何种原因导致的渲染,比如 `setState` 或 hooks changed 之类。
|
||||
|
||||
注意尽量不要轻易使用 `Profiler` 检测性能,因为 `Profiler` 本身也会消耗性能。
|
||||
|
||||
如果不想获得这么详细的渲染耗时,或者不想提前在代码中埋点,可以利用 DevTools 的 Profiler 查看更直观更简洁的渲染耗时:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1sPAuDuL2gK0jSZPhXXahvXXa-1846-1028.png">
|
||||
|
||||
其中 Ranked 可以展示按照渲染耗时排序后的结果,Interations 需要配合 Tracing API 使用,在后面会提到。
|
||||
|
||||
### Tracing API
|
||||
|
||||
利用 `scheduler/tracing` 提供的 `trace` API,我们可以记录某个动作的耗时,比如 “点击添加按钮收藏一个电影” 耗时多久:
|
||||
|
||||
```jsx
|
||||
import { render } from "react-dom";
|
||||
import { unstable_trace as trace } from "scheduler/tracing";
|
||||
|
||||
class MyComponent extends Component {
|
||||
addMovieButtonClick = (event) => {
|
||||
trace("Add To Movies Queue click", performance.now(), () => {
|
||||
this.setState({ itemAddedToQueue: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
在 Interations 中可以看到动作触发的耗时:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1XR.FDAY2gK0jSZFgXXc5OFXa-1846-1010.png">
|
||||
|
||||
这个动作还可以是渲染,比如可以记录 ReactDOM 渲染的耗时:
|
||||
|
||||
```jsx
|
||||
import { unstable_trace as trace } from "scheduler/tracing";
|
||||
|
||||
trace("initial render", performance.now(), () => {
|
||||
ReactDom.render(<App />, document.getElementById("app"));
|
||||
});
|
||||
```
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB18hyHfcKfxu4jSZPfXXb3dXXa-1846-740.png">
|
||||
|
||||
甚至还可以追踪异步的耗时:
|
||||
|
||||
```jsx
|
||||
import {
|
||||
unstable_trace as trace,
|
||||
unstable_wrap as wrap,
|
||||
} from "scheduler/tracing";
|
||||
|
||||
trace("Some event", performance.now(), () => {
|
||||
setTimeout(
|
||||
wrap(() => {
|
||||
// 异步操作
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
有了 `Profiler` 与 `trace` 这两件武器,我们可以监控任意元素的渲染耗时与交互耗时,几乎可以涵盖所有性能监控需要。
|
||||
|
||||
### Puppeteer
|
||||
|
||||
我们还可以利用 Puppeteer 实现自动化操作并打印报告:
|
||||
|
||||
```jsx
|
||||
const puppeteer = require("puppeteer");
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
const navigationPromise = page.waitForNavigation();
|
||||
await page.goto("https://react-movies-queue.glitch.me/");
|
||||
await page.setViewport({ width: 1276, height: 689 });
|
||||
await navigationPromise;
|
||||
|
||||
const addMovieToQueueBtn =
|
||||
"li:nth-child(3) > .card > .card__info > div > .button";
|
||||
await page.waitForSelector(addMovieToQueueBtn);
|
||||
|
||||
// Begin profiling...
|
||||
await page.tracing.start({ path: "profile.json" });
|
||||
// Click the button
|
||||
await page.click(addMovieToQueueBtn);
|
||||
// Stop profliling
|
||||
await page.tracing.stop();
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
首先利用 `puppeteer` 创建一个浏览器,新建一个页面并打开 `https://react-movies-queue.glitch.me/` 这个 URL,等待页面加载完毕后利用 DOM 选择器找到按钮,利用 `page.click` API 模拟点击这个按钮,并在前后利用 `page.tracing` 记录性能变化,并将这个文件上传到 DevTools Performance 面板,就会得到一份自动的性能检测报告:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1623EDxz1gK0jSZSgXXavwpXa-2769-2289.png">
|
||||
|
||||
这张图相当重要,是浏览器综合运行开销分析的利器,最上面分为 4 个部分:
|
||||
|
||||
- FPS:每秒帧数,绿色竖线越高表示 FPS 越高,出现红线则表示出现了卡顿。
|
||||
- CPU:CPU 资源,用面积图展示消耗 CPU 资源的事件。
|
||||
- NET:网络消耗,每条横杠表示一种资源的加载。
|
||||
- HEAP:内存水位,由于短时间内看不出来是否会内存溢出,一般只用来简单看看内存消耗是否符合预期,对于内存溢出的检测需要用持续监控上报的方式。
|
||||
|
||||
下面会有一张 Network 详细图解,比如这张图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1D.wKDxD1gK0jSZFyXXciOVXa-2868-750.png">
|
||||
|
||||
细线表示等待的时间,粗线表示实际加载的情况,其中浅色部分表示服务器等待时间,即从发送下载请求到服务器响应第一个字节的时间。这部分可以看出资源并行加载阻塞情况以及资源服务器响应时间是否存在问题。
|
||||
|
||||
Timings 展示了几个重要时间节点,这里列举一部分:
|
||||
|
||||
- FP:First Paint,第一次绘制。
|
||||
- FCP:First Contentful Paint,第一次内容绘制。
|
||||
- LCP:Largest Contentful Paint,最大内容绘制。
|
||||
- DCL:Document Content Loaded,DOM 内容加载完毕。
|
||||
|
||||
再下面是 JS 计算消耗,用了一张火焰图,火焰图是性能分析的常用可视化工具。以下面这张图为例:
|
||||
|
||||
<img width=350 src="https://img.alicdn.com/tfs/TB1JecIDrr1gK0jSZFDXXb9yVXa-1404-616.png">
|
||||
|
||||
看火焰图首先看跨度最长的函数,也就是最长的那条线,这是最耗时的部分,从左到右是浏览器脚本的调用顺序,从上到下是函数嵌套的顺序。
|
||||
|
||||
我们可以看到鼠标位置的 34 这个函数虽然长,但并不是性能瓶颈,因为下面执行的 n 函数长度和它一样,表示 34 函数的性能几乎无损耗,其性能由其调用的 n 函数决定。
|
||||
|
||||
我们可以利用这种方式一步步排查到叶子结点,找到对性能影响最大的元子函数。
|
||||
|
||||
### User Timing API
|
||||
|
||||
我们还可以利用 `performance.mark` 自定义性能检测节点:
|
||||
|
||||
```jsx
|
||||
// Record the time before running a task
|
||||
performance.mark("Movies:updateStart");
|
||||
// Do some work
|
||||
|
||||
// Record the time after running a task
|
||||
performance.mark("Movies:updateEnd");
|
||||
|
||||
// Measure the difference between the start and end of the task
|
||||
performance.measure("moviesRender", "Movies:updateStart", "Movies:updateEnd");
|
||||
```
|
||||
|
||||
这些节点可以在上面介绍的 Performance 面板中展示出来用于自定义分析。
|
||||
|
||||
## 3 总结
|
||||
|
||||
利用 Performance 进行通用性能分析,利用 React Profiler 进行 React 定制性能分析,这两个结合在一起几乎可以完成任何性能检测。
|
||||
|
||||
一般来说,首先应该用 React Profiler 进行 React 层面的问题筛查,这样更直观,更容易定位问题。如果某些问题跳出了 React 框架范围,或者不再能以组件粒度进行度量,我们可以回到 Performance 面板进行通用性能分析。
|
||||
|
||||
> 讨论地址是:[精读《React 性能调试》 · Issue #247 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/247)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,259 @@
|
||||
## 1 引言
|
||||
|
||||
Deno 是什么?Deno 和 Node 有什么关系?Deno 和我有什么关系?
|
||||
|
||||
Deno 将于 2020-05-13 发布 1.0,如果你还有上面的疑惑,可以和我一起通过 [Deno 1.0: What you need to know](https://blog.logrocket.com/deno-1-0-what-you-need-to-know/) 这篇文章一起了解 Deno 基础知识。
|
||||
|
||||
希望你带着疑问思考,未来 10 年看今天,会不会出现 Deno 官方生态壮大,完全替代 Node 进而影响到 Web 生态的局面呢?这个思考结果会影响到你未来职业发展,你需要学会自己思考,并对这个思考结果负责。
|
||||
|
||||
## 2 介绍 & 精读
|
||||
|
||||
Deno 的作者是 Ryan Dahl,他是 Nodejs 背后的策划者,曾经说过 [我对 Nodejs 感到遗憾的 10 件事](https://www.youtube.com/watch?v=M3BM9TB-8yA)。这也是为什么新开一个坑的原因,但 Deno 并不定位为 Nodejs 的替代品,从整体功能来看,Deno 有更大的野心,据我的推测是想要取代现在陈旧的前后端开发模式,让 Deno 一统前后端开发全流程。
|
||||
|
||||
Nodejs 是由 C++ 写的,而 Deno 则是由 Rust 写的,并选择了 [Tokio](https://tokio.rs/) 这个异步编程框架,并使用 V8 引擎解析 Javascript,并内置了对 Ts 的解析。
|
||||
|
||||
### 安装
|
||||
|
||||
Deno 支持如下安装方式:
|
||||
|
||||
**Shell:**
|
||||
|
||||
```shell
|
||||
curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||
```
|
||||
|
||||
**PowerShell:**
|
||||
|
||||
```shell
|
||||
iwr https://deno.land/x/install/install.ps1 -useb | iex
|
||||
```
|
||||
|
||||
**Homebrew:**
|
||||
|
||||
```shell
|
||||
brew install deno
|
||||
```
|
||||
|
||||
**Chocolatey:**
|
||||
|
||||
```shell
|
||||
choco install deno
|
||||
```
|
||||
|
||||
脚本执行方式为 `deno run`,可以类比为 `node`,但功能不同且支持远程文件,实际上远程依赖是 Deno 的一大特色,也是有争议的地方:
|
||||
|
||||
```shell
|
||||
deno run https://deno.land/std/examples/welcome.ts
|
||||
```
|
||||
|
||||
在 ts 文件中允许用远程脚本加载资源,这个后面还会提到:
|
||||
|
||||
```ts
|
||||
import { serve } from "https://deno.land/std@v0.42.0/http/server.ts";
|
||||
const s = serve({ port: 8000 });
|
||||
console.log("http://localhost:8000/");
|
||||
for await (const req of s) {
|
||||
req.respond({ body: "Hello World\n" });
|
||||
}
|
||||
```
|
||||
|
||||
### 安全性
|
||||
|
||||
Deno 是默认安全的,这体现在默认没有环境、网络访问权限、文件读写权限、运行子进程的能力。所以如果直接运行一个依赖权限的文件会报错:
|
||||
|
||||
```shell
|
||||
deno run file-needing-to-run-a-subprocess.ts
|
||||
|
||||
# error: Uncaught PermissionDenied: access to run a subprocess, run again with the --allow-run flag
|
||||
```
|
||||
|
||||
可以通过参数方式允许权限的执行,有 `--allow-read`、`--allow-write`、`--allow-net` 等:
|
||||
|
||||
```shell
|
||||
deno --allow-read=/etc
|
||||
```
|
||||
|
||||
上面表示 `/etc` 文件夹下的文件拥有文件读权限。
|
||||
|
||||
除了直接加参数调用、Bash 脚本调用外,还可以用 Make 运行,或者使用类似的 [drake](https://deno.land/x/drake/) 启动。
|
||||
|
||||
或者使用 `deno install` 命令,将脚本转化为一个快捷指令:
|
||||
|
||||
```shell
|
||||
deno install --allow-net --allow-read -n serve https://deno.land/std/http/file_server.ts
|
||||
```
|
||||
|
||||
`-n` 表示 `--name`,可以对这个脚本进行重命名,比如上面的例子中,`serve` 命令就等同于 `deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts`。
|
||||
|
||||
### 标准库
|
||||
|
||||
Deno 在标准库上很有特点,对常用功能提供了官方版本,保证可用性与稳定性。原文中列出了一些与 Npm 三方库的对比:
|
||||
|
||||
| Deno Module | Description | npm | Equivalents |
|
||||
| ----------- | --------------------------------------------------------------------------------- | --- | ------------------------ |
|
||||
| colors | Adds color to the terminal | | chalk, kleur, and colors |
|
||||
| datetime | Helps working with the JavaScript Date object | |
|
||||
| encoding | Adds support for external data scructures like base32, binary, csv, toml and yaml | |
|
||||
| flags | Helps working with command line arguments | | minimist |
|
||||
| fs | Helps with manipulation of the file system | |
|
||||
| http | Allows serving local files over HTTP | | http-server |
|
||||
| log | Used for creating logs | | winston |
|
||||
| testing | For unit testing assertion and benchmarking | | chai |
|
||||
| uuid | UUID generation | | uuid |
|
||||
| ws | Helps with creating WebSocket client/server | | ws |
|
||||
|
||||
从这个点上来看,Deno 既做运行环境又做基础生态,缓解了 Npm 生态下选择困难症,这件事需要辩证来看:集成了官方包对功能确定的模块来说是很有必要的,而且提高了底层库的稳定性;但 Deno 生态也有三方库,而且本质上三方库和官方库在功能上没有任何壁垒,因为实现代码都类似,唯一区别是谁能为其稳定性站台,假设微软和 Deno 同时出了基于 Npm 生态与 Deno 生态官方库,都保证会持续维护,你更相信谁呢?官方是否有优势要取决于官方自身的实力。
|
||||
|
||||
### 内置 Typescript
|
||||
|
||||
Deno 内置支持了 TS,因此不需要 `ts-node` 我们就可以用 `deno run test.ts` 运行 Typescript 文件。值得注意的是,Deno 内部也是利用 Typescript 引擎解析为 Js 后交由 V8 引擎解析,因此本质上没太大的变化,只是这样 Deno 的生态会更规范。
|
||||
|
||||
由于内置了 TS 支持,自然也不需要写 `tsconfig.json` 配置了,但你依然可以定制它:
|
||||
|
||||
```shell
|
||||
deno run -c tsconfig.json [file-to-run.ts]
|
||||
```
|
||||
|
||||
Deno 默认还开启了 TS 严格模式,所以看到这里,可以认为 Deno 是为了构建高质量理想库而诞生的运行环境,基于已有的生态来做,但做了更多内置技术选型,这和 Facebook 的 [rome](https://github.com/facebookexperimental/rome) 很像,但做的却更彻底。
|
||||
|
||||
其实从实现上来看,我们基于 Javascript 生态也能写出 `deno run test.ts` 这样类似的引擎,只不过是由 JS 驱动执行,可能编译还会选择 Webpack,但 Deno 本身基于 Rust 实现,并重新实现了一套模块加载标准,可以说从更底层的方式重新解读了 W3C 标准规范,以期望解决 Javascript 生态的各种痛点问题。
|
||||
|
||||
### 支持 Web 标准
|
||||
|
||||
Deno 还支持 W3C 标准规范,因此像 `fetch`、`setTimeout` 等 API 都可以被直接使用,如果你按照 Deno 支持的那几个函数写代码,可以保证在 Deno、Node、Web 三个平台实现跨平台运行。
|
||||
|
||||
虽然距离完全实现 W3C 所有标准规范还有一些路要走,但我们看到了 Deno 兼容规范的决心。
|
||||
|
||||
### ESModule
|
||||
|
||||
模块化是 Deno 的亮点,Deno 使用官方 ESModule 规范,但引用路径必须加上后缀:
|
||||
|
||||
```ts
|
||||
import * as log from "https://deno.land/std/log/mod.ts";
|
||||
import { outputToConsole } from "./view.ts";
|
||||
```
|
||||
|
||||
Deno 不需要申明依赖,代码的引用路径就是依赖申明,会包括完整的路径以及文件后缀,也支持网络资源,可以摆脱 NPM 中心化的包管理模式,因为这个路径可以是任何网络地址。
|
||||
|
||||
### 包管理
|
||||
|
||||
对于 `import * as log from "https://deno.land/std/log/mod.ts";` 这行代码,Deno 会下载到一个缓存文件夹,用户不会感知到这个文件夹与这个过程的存在,也就是说,Deno 环境中是没有 `node_modules` 的。
|
||||
|
||||
也可以通过 `deno --reload` 的方式强制刷新缓存。
|
||||
|
||||
但这里也要辩证的看待 “Deno 去中心化” 这件事,虽然引用了网络源,但会引发下面几个问题:
|
||||
|
||||
1. 实际上还存在一个 "node_modules",只是用户看不到。
|
||||
2. 网络下载速度放到运行时,第一次启动还是很慢。
|
||||
3. 普通模式下无 lock,必须配合 `deps.ts` 使用,这个后面会提到。
|
||||
|
||||
即使被打上 “中心化恶人” 的 npm 也有去中心化的一面,因为 npm 支持私有化部署,无论是速度还是稳定性都可以由公司自己掌控,从稳定性来说还是 npm 拥有压倒性优势。
|
||||
|
||||
### 三方库
|
||||
|
||||
Deno 还有第三方库生态,截止目前共有 [221 个三方库](<[](https://deno.land/x/)>)。
|
||||
|
||||
由于 Deno 走网络资源,我们可以借助 [Pika](https://www.pika.dev/cdn) 提供的 CDN 服务直接引用网络资源包:
|
||||
|
||||
```jsx
|
||||
import * as pkg from "https://cdn.pika.dev/preact@^10.3.0";
|
||||
```
|
||||
|
||||
虽然这样看上去很轻量,但对公司来说还是需要自建一个 “Pika” 保障稳定性,以及做全球 CDN 缓存等的工作。
|
||||
|
||||
### 告别 package.json
|
||||
|
||||
npm 生态下包信息存放在 `package.json`,包含但不限于下面的内容:
|
||||
|
||||
- 项目元信息。
|
||||
- 项目依赖和版本号。
|
||||
- 依赖还进行分类,比如 `dependencies`、`devDependencies` 甚至 `peerDependencies`。
|
||||
- 标记入口,`main` 和 `module`,还有 TS 用的 `types` 与 `typings`,脚手架的 `bin` 等等。
|
||||
- npm scripts。
|
||||
|
||||
随着标准的不断更新,`package.json` 信息已经非常臃肿了。
|
||||
|
||||
对于 Deno 来说,则使用 `deps.ts` 集中管理依赖:
|
||||
|
||||
```ts
|
||||
export { assert } from "https://deno.land/std@v0.39.0/testing/asserts.ts";
|
||||
export { green, bold } from "https://deno.land/std@v0.39.0/fmt/colors.ts";
|
||||
```
|
||||
|
||||
`deps.ts` 就是一个普通文件,只是将项目的依赖精确描述出来,这样其他地方引用 `assert` 时,就可以这么写了:
|
||||
|
||||
```ts
|
||||
// import { assert } from "https://deno.land/std@v0.39.0/testing/asserts.ts";
|
||||
import { assert } from "./deps.ts";
|
||||
```
|
||||
|
||||
如果需要锁定依赖,可以通过 `deno --lock=lock.json` 方式申明。
|
||||
|
||||
### deno doc
|
||||
|
||||
`deno doc <filename>` 命令可以根据文件按照 JS Doc 规则生成文档,同时也支持 TS 语法,比如下面这段代码:
|
||||
|
||||
```ts
|
||||
/** Asynchronously fulfill a response with a file from the local file
|
||||
* system. */
|
||||
export async function send(
|
||||
{ request, response }: Context,
|
||||
path: string,
|
||||
options: SendOptions = { root: "" }
|
||||
): Promise<string | undefined> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
生成文档如下:
|
||||
|
||||
```text
|
||||
function send(_: Context, path: string, options: SendOptions): Promise<string | undefined>
|
||||
Asynchronously fulfill a response with a file from the local file system.
|
||||
```
|
||||
|
||||
deno 本身文档就是用这个命令生成的,可以 [访问官方文档](https://doc.deno.land/) 查看使用效果。
|
||||
|
||||
### 内置工具链
|
||||
|
||||
前端 Javascript 工具链相当混乱,虽然业界已有 Umi 等框架做了开箱即用的封装,但回到 Javascript 设计的初衷就是可以在浏览器直接使用的,包括浏览器对不依赖构建工具的模块化支持,注定了未来 Webpack 一定会被消灭。
|
||||
|
||||
Deno 通过内置一套工具链的方式解决这个问题,包括:
|
||||
|
||||
- 测试:提供 `deno test` 命令与 `Deno.test()` 测试函数。
|
||||
- 格式化:提供 [vscode 插件](https://marketplace.visualstudio.com/items?itemName=axetroy.vscode-deno)。
|
||||
- 编译:提供 `deno bundle` 命令。
|
||||
|
||||
不过值得注意的是,在最重要的编译环节,`deno bundle` 目前提供的能力是相对欠缺的,比如还不支持 Tree Shaking。
|
||||
|
||||
用 Rust 等语言提升构建效率是业界一直在尝试的事,比如 @陈成 就基于 [esbuild](https://github.com/evanw/esbuild) 做了 [@umijs/plugin-esbuild](https://umijs.org/zh-CN/plugins/plugin-esbuild) 插件用于提升 Umi 构建速度,但为了防止生产构建产物与 Webpack 默认规则不一致,仅使用了其压缩(minifier)功能。
|
||||
|
||||
对 deno 来说也一样,目前其实没有任何证据表明 deno 的构建结果可以完美适配 webpack 环境,所以请勿认为 deno 发布了 1.0 版本就等于可以在生产环境使用。
|
||||
|
||||
## 3 总结
|
||||
|
||||
正如原文结尾所说的,Deno 虽然将要发布 1.0 版本,但仍不能完全替代 Nodejs,这背后的原因主要是历史兼容成本,也就是完整支持整个 Node 生态不只是设计的问题,更是一个体力活,需要一个个高地去攻克。
|
||||
|
||||
同样 Deno 对 Web 的支持也让人耳目一新,但仍不能放到生产环境使用,除了官方和三方生态还在逐渐完善外,`deno bundle` 对 Tree Shaking 能力的缺失以及构建产物无法保证与现在的 Webpack 完全相同,这样会导致对稳定性要求极高的大型应用迁移成本非常高。
|
||||
|
||||
最亮眼的改动是模块化部分,依赖完全去中心化从长远来看是一个非常好的设计,只是基础设施和生态要达到一个较为理想的水平。
|
||||
|
||||
最后,让我们站在一个预言者角度思考一下 Deno 到底会不会火吧:
|
||||
|
||||
Deno 做的初心是做一个更好的 Node,但很不幸,对于这种级别的生态底层工具来说,重新做一个并重新火起来的难度,不亚于重新做一个阿里巴巴并取代现在阿里的难度。也就是不同的时间点做同一件事,哪怕后者可以吸取教训,大概率也无法复制以前成功的路线。
|
||||
|
||||
从 Deno 的功能来看,解决了 Node 很多痛点,其中就包括去中心化管理,有点云开发的意思,但在 2020 年,基于 Nodejs 和 Webpack 的云开发都搞出来了,说实话是没有 Deno 什么空间的。从功能上来看,开篇就说了 Deno 基于 V8 解析 Javascript,对于性能和功能都没有革命性提升,从技术上作出突破也几乎不可能了。
|
||||
|
||||
Deno 的思想确实比 Node 先进,但不能说比 Node 好十倍,则无法撼动 Node 的生态,即便是 Node 作者自己可能也不行。
|
||||
|
||||
然而我上面说的可能都是错的。
|
||||
|
||||
> 讨论地址是:[精读《Deno 1.0 你需要了解的》 · Issue #248 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/248)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,453 @@
|
||||
## 1 引言
|
||||
|
||||
与组件生命周期绑定的 Utils 非常适合基于 React Hooks 来做,比如可以将 “发请求” 这个功能与组件生命周期绑定,实现一些便捷的功能。
|
||||
|
||||
这次以 [@umijs/use-request](https://hooks.umijs.org/zh-CN/hooks/async) 为例子,分析其功能思路与源码。
|
||||
|
||||
## 2 简介
|
||||
|
||||
[@umijs/use-request](https://hooks.umijs.org/zh-CN/hooks/async) 支持以下功能:
|
||||
|
||||
- 默认自动请求:在组件初次加载时自动触发请求函数,并自动管理 `loading`, `data` , `error` 状态。
|
||||
- 手动触发请求:设置 `options.manual = true` , 则手动调用 `run` 时才会取数。
|
||||
- 轮询请求:设置 `options.pollingInterval` 则进入轮询模式,可通过 `run` / `cancel` 开始与停止轮询。
|
||||
- 并行请求:设置 `options.fetchKey` 可以对请求状态隔离,通过 `fetches` 拿到所有请求状态。
|
||||
- 请求防抖:设置 `options.debounceInterval` 开启防抖。
|
||||
- 请求节流:设置 `options.throttleInterval` 开启节流。
|
||||
- 请求缓存 & SWR:设置 `options.cacheKey` 后开启对请求结果缓存机制,下次请求前会优先返回缓存并在后台重新取数。
|
||||
- 请求预加载:由于 `options.cacheKey` 全局共享,可以提前执行 `run` 实现预加载效果。
|
||||
- 屏幕聚焦重新请求:设置 `options.refreshOnWindowFocus = true` 在浏览器 `refocus` 与 `revisible` 时重新请求。
|
||||
- 请求结果突变:可以通过 `mutate` 直接修改取数结果。
|
||||
- 加载延迟:设置 `options.loadingDelay` 可以延迟 `loading` 变成 `true` 的时间,有效防止闪烁。
|
||||
- 自定义请求依赖:设置 `options.refreshDeps` 可以在依赖变动时重新触发请求。
|
||||
- 分页:设置 `options.paginated` 可支持翻页场景。
|
||||
- 加载更多:设置 `options.loadMore` 可支持加载更多场景。
|
||||
|
||||
一切 Hooks 的功能拓展都要基于 React Hooks 生命周期,我们可以利用 Hooks 做下面几件与组件相关的事:
|
||||
|
||||
1. 存储与当前组件实例绑定的 mutable、immutable 数据。
|
||||
2. 主动触发调用组件 rerender。
|
||||
3. 访问到组件初始化、销毁时机的钩子。
|
||||
|
||||
上面这些功能就可以基于这些基础能力拓展了:
|
||||
|
||||
**默认自动请求**
|
||||
|
||||
在组件初始时机取数。由于和组件生命周期绑定,可以很方便实现各组件相互隔离的取数顺序强保证:可以利用取数闭包存储 requestIndex,取数结果返回后与当前最新 requestIndex 进行比对,丢弃不一致的取数结果。
|
||||
|
||||
**手动触发请求**
|
||||
|
||||
将触发取数的函数抽象出来并在 CustomHook 中 return。
|
||||
|
||||
**轮询请求**
|
||||
|
||||
在取数结束后设定 `setTimeout` 重新触发下一轮取数。
|
||||
|
||||
**并行请求**
|
||||
|
||||
每次取数时先获取当前请求唯一标识 `fetchKey`,仅更新这个 key 下的状态。
|
||||
|
||||
**请求防抖、请求节流**
|
||||
|
||||
这个实现方式可以挺通用化,即取数调用函数处替换为对应 `debounce` 或 `throttle` 函数。
|
||||
|
||||
**请求预加载**
|
||||
|
||||
这个功能只要实现全局缓存就自然支持了。
|
||||
|
||||
**屏幕聚焦重新请求**
|
||||
|
||||
这个可以统一监听 window action 事件,并触发对应组件取数。可以全局统一监听,也可以每个组件分别监听。
|
||||
|
||||
**请求结果突变**
|
||||
|
||||
由于取数结果存储在 CustomHook 中,直接修改数据 data 值即可。
|
||||
|
||||
**加载延迟**
|
||||
|
||||
有加载延迟时,可以先将 `loading` 设置为 `false`,等延迟到了再设置为 `true`,如果此时取数提前完毕则销毁定时器,实现无 loading 取数。
|
||||
|
||||
**自定义请求依赖**
|
||||
|
||||
利用 `useEffect` 和自带的 deps 即可。
|
||||
|
||||
**分页**
|
||||
|
||||
基于通用取数 Hook 封装,本质上是多带了一些取数参数与返回值参数,并遵循 Antd Table 的 API。
|
||||
|
||||
**加载更多**
|
||||
|
||||
和分页类似,区别是加载更多不会清空已有数据,并且需要根据约定返回结构 `noMore` 判断是否能继续加载。
|
||||
|
||||
## 3 精读
|
||||
|
||||
接下来是源码分析。
|
||||
|
||||
首先定义了一个类 `Fetch`,这是因为一个 `useRequest` 的 `fetchKey` 特性可以通过多实例解决。
|
||||
|
||||
Class 的生命周期不依赖 React Hooks,所以将不依赖生命周期的操作收敛到 Class 中,不仅提升了代码抽象程度,也提升了可维护性。
|
||||
|
||||
```tsx
|
||||
class Fetch<R, P extends any[]> {
|
||||
// ...
|
||||
// 取数状态存储处
|
||||
state: FetchResult<R, P> = {
|
||||
loading: false,
|
||||
params: [] as any,
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
run: this.run.bind(this.that),
|
||||
mutate: this.mutate.bind(this.that),
|
||||
refresh: this.refresh.bind(this.that),
|
||||
cancel: this.cancel.bind(this.that),
|
||||
unmount: this.unmount.bind(this.that),
|
||||
};
|
||||
|
||||
constructor(
|
||||
service: Service<R, P>,
|
||||
config: FetchConfig<R, P>,
|
||||
// 外部通过这个回调订阅 state 变化
|
||||
subscribe: Subscribe<R, P>,
|
||||
initState?: { data?: any; error?: any; params?: any; loading?: any }
|
||||
) {}
|
||||
|
||||
// 此 setState 非彼 setState,作用是更新 state 并通知订阅
|
||||
setState(s = {}) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...s,
|
||||
};
|
||||
this.subscribe(this.state);
|
||||
}
|
||||
|
||||
// 实际取数函数,但下划线命名的带有一些历史气息啊
|
||||
_run(...args: P) {}
|
||||
|
||||
// 对外暴露的取数函数,对防抖和节流做了分发处理
|
||||
run(...args: P) {
|
||||
if (this.debounceRun) {
|
||||
// return ..
|
||||
}
|
||||
if (this.throttleRun) {
|
||||
// return ..
|
||||
}
|
||||
return this._run(...args);
|
||||
}
|
||||
|
||||
// 取消取数,考虑到了防抖、节流兼容性
|
||||
cancel() {}
|
||||
|
||||
// 以上次取数参数重新取数
|
||||
refresh() {}
|
||||
|
||||
// 轮询 starter
|
||||
rePolling() {}
|
||||
|
||||
// 对应 mutate 函数
|
||||
mutate(data: any) {}
|
||||
|
||||
// 销毁订阅
|
||||
unmount() {}
|
||||
}
|
||||
```
|
||||
|
||||
**默认自动请求**
|
||||
|
||||
通过 `useEffect` 零依赖实现,需要:
|
||||
|
||||
1. 有缓存则不需响应,当对应缓存结束后会通知,同时也支持了请求预加载功能。
|
||||
2. 为支持并行请求,所有请求都通过 `fetches` 独立管理。
|
||||
|
||||
```tsx
|
||||
// 第一次默认执行
|
||||
useEffect(() => {
|
||||
if (!manual) {
|
||||
// 如果有缓存
|
||||
if (Object.keys(fetches).length > 0) {
|
||||
/* 重新执行所有的 */
|
||||
Object.values(fetches).forEach((f) => {
|
||||
f.refresh();
|
||||
});
|
||||
} else {
|
||||
// 第一次默认执行,可以通过 defaultParams 设置参数
|
||||
run(...(defaultParams as any));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
默认执行第 11 行,并根据当前的 `fetchKey` 生成对应 `fetches`,如果初始化已经存在 `fetches`,则行为改为重新执行所有 **已存在的** 并行请求。
|
||||
|
||||
**手动触发请求**
|
||||
|
||||
上一节已经在初始请求时禁用了 `manual` 开启时的默认取数。下一步只要将封装的取数函数 `run` 定义出来并暴露给用户:
|
||||
|
||||
```tsx
|
||||
const run = useCallback(
|
||||
(...args: P) => {
|
||||
if (fetchKeyPersist) {
|
||||
const key = fetchKeyPersist(...args);
|
||||
newstFetchKey.current = key === undefined ? DEFAULT_KEY : key;
|
||||
}
|
||||
const currentFetchKey = newstFetchKey.current;
|
||||
// 这里必须用 fetchsRef,而不能用 fetches。
|
||||
// 否则在 reset 完,立即 run 的时候,这里拿到的 fetches 是旧的。
|
||||
let currentFetch = fetchesRef.current[currentFetchKey];
|
||||
if (!currentFetch) {
|
||||
const newFetch = new Fetch(
|
||||
servicePersist,
|
||||
config,
|
||||
subscribe.bind(null, currentFetchKey),
|
||||
{
|
||||
data: initialData,
|
||||
}
|
||||
);
|
||||
currentFetch = newFetch.state;
|
||||
setFeches((s) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
s[currentFetchKey] = currentFetch;
|
||||
return { ...s };
|
||||
});
|
||||
}
|
||||
return currentFetch.run(...args);
|
||||
},
|
||||
[fetchKey, subscribe]
|
||||
);
|
||||
```
|
||||
|
||||
主动取数函数与内部取数函数共享一个,所以 `run` 函数要考虑多种情况,其中之一就是并行取数的情况,因此需要拿到当前取数的 `fetchKey`,并创建一个 `Fetch` 的实例,最终调用 `Fetch` 实例的 `run` 函数取数。
|
||||
|
||||
**轮询请求**
|
||||
|
||||
轮询取数在 `Fetch` 实际取数函数 `_fetch` 中定义,当取数函数 `fetchService`(对多种形态的取数方法进行封装后)执行完后,无论正常还是报错,都要进行轮询逻辑,因此在 `.finally` 时机里判断:
|
||||
|
||||
```tsx
|
||||
fetchService.then().finally(() => {
|
||||
if (!this.unmountedFlag && currentCount === this.count) {
|
||||
if (this.config.pollingInterval) {
|
||||
// 如果屏幕隐藏,并且 !pollingWhenHidden, 则停止轮询,并记录 flag,等 visible 时,继续轮询
|
||||
if (!isDocumentVisible() && !this.config.pollingWhenHidden) {
|
||||
this.pollingWhenVisibleFlag = true;
|
||||
return;
|
||||
}
|
||||
this.pollingTimer = setTimeout(() => {
|
||||
this._run(...args);
|
||||
}, this.config.pollingInterval);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
轮询还要考虑到屏幕是否隐藏,如果可以触发轮询则触发定时器再次调用 `_run`,注意这个定时器需要正常销毁。
|
||||
|
||||
**并行请求**
|
||||
|
||||
每个 `fetchKey` 对应一个 `Fetch` 实例,这个逻辑在 **手动触发请求** 介绍的 `run` 函数中已经实现。
|
||||
|
||||
这块的封装思路可以品味一下,从外到内分别是 React Hooks 的 fetch -> Fetch 类的 run -> Fetch 类的 \_run,并行请求做在 React Hooks 这一层。
|
||||
|
||||
**请求防抖、请求节流**
|
||||
|
||||
这个实现就在 Fetch 类的 `run` 函数中:
|
||||
|
||||
```tsx
|
||||
function run(...args: P) {
|
||||
if (this.debounceRun) {
|
||||
this.debounceRun(...args);
|
||||
return Promise.resolve(null as any);
|
||||
}
|
||||
if (this.throttleRun) {
|
||||
this.throttleRun(...args);
|
||||
return Promise.resolve(null as any);
|
||||
}
|
||||
return this._run(...args);
|
||||
}
|
||||
```
|
||||
|
||||
由于防抖和节流是 React 无关的,也不是最终取数无关的,因此实现在 `run` 这个夹层函数进行分发。
|
||||
|
||||
这里实现的比较简化,防抖后 `run` 拿到的 Promise 不再是有效的取数结果了,其实这块还是可以进一步对 Promise 进行封装,无论在防抖还是正常取数的场景都返回 Promise,只需 resolve 的时机由 `Fetch` 这个类灵活把控即可。
|
||||
|
||||
**请求预加载**
|
||||
|
||||
预加载就是缓存机制,首先利用 `useEffect` 同步缓存:
|
||||
|
||||
```tsx
|
||||
// cache
|
||||
useEffect(() => {
|
||||
if (cacheKey) {
|
||||
setCache(cacheKey, {
|
||||
fetches,
|
||||
newstFetchKey: newstFetchKey.current,
|
||||
});
|
||||
}
|
||||
}, [cacheKey, fetches]);
|
||||
```
|
||||
|
||||
在初始化 `Fetch` 实例时优先采用缓存:
|
||||
|
||||
```tsx
|
||||
const [fetches, setFeches] = useState<Fetches<U, P>>(() => {
|
||||
// 如果有 缓存,则从缓存中读数据
|
||||
if (cacheKey) {
|
||||
const cache = getCache(cacheKey);
|
||||
if (cache) {
|
||||
newstFetchKey.current = cache.newstFetchKey;
|
||||
/* 使用 initState, 重新 new Fetch */
|
||||
const newFetches: any = {};
|
||||
Object.keys(cache.fetches).forEach((key) => {
|
||||
const cacheFetch = cache.fetches[key];
|
||||
const newFetch = new Fetch();
|
||||
// ...
|
||||
newFetches[key] = newFetch.state;
|
||||
});
|
||||
return newFetches;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
```
|
||||
|
||||
**屏幕聚焦重新请求**
|
||||
|
||||
在 `Fetch` 构造函数实现监听并调用 `refresh` 即可,源码里采取全局统一监听的方式:
|
||||
|
||||
```tsx
|
||||
function subscribe(listener: () => void) {
|
||||
listeners.push(listener);
|
||||
return function unsubscribe() {
|
||||
const index = listeners.indexOf(listener);
|
||||
listeners.splice(index, 1);
|
||||
};
|
||||
}
|
||||
|
||||
let eventsBinded = false;
|
||||
if (typeof window !== "undefined" && window.addEventListener && !eventsBinded) {
|
||||
const revalidate = () => {
|
||||
if (!isDocumentVisible()) return;
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
// dispatch 每个 listener
|
||||
const listener = listeners[i];
|
||||
listener();
|
||||
}
|
||||
};
|
||||
window.addEventListener("visibilitychange", revalidate, false);
|
||||
// only bind the events once
|
||||
eventsBinded = true;
|
||||
}
|
||||
```
|
||||
|
||||
在 `Fetch` 构造函数里注册:
|
||||
|
||||
```tsx
|
||||
this.limitRefresh = limit(this.refresh.bind(this), this.config.focusTimespan);
|
||||
|
||||
if (this.config.pollingInterval) {
|
||||
this.unsubscribe.push(subscribeVisible(this.rePolling.bind(this)));
|
||||
}
|
||||
```
|
||||
|
||||
并通过 `limit` 封装控制调用频率,并 push 到 `unsubscribe` 数组,一边监听可以随组件一起销毁。
|
||||
|
||||
**请求结果突变**
|
||||
|
||||
这个函数只要更新 `data` 数据结果即可:
|
||||
|
||||
```tsx
|
||||
function mutate(data: any) {
|
||||
if (typeof data === "function") {
|
||||
this.setState({
|
||||
data: data(this.state.data) || {},
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
值得注意的是,`cancel`、`refresh`、`mutate` 都必须在初次请求完成后才有意义,所以初次返回的函数是一个抛错:
|
||||
|
||||
```tsx
|
||||
const noReady = useCallback(
|
||||
(name: string) => () => {
|
||||
throw new Error(`Cannot call ${name} when service not executed once.`);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
loading: !manual || defaultLoading,
|
||||
data: initialData,
|
||||
error: undefined,
|
||||
params: [],
|
||||
cancel: noReady("cancel"),
|
||||
refresh: noReady("refresh"),
|
||||
mutate: noReady("mutate"),
|
||||
...(fetches[newstFetchKey.current] || {}),
|
||||
} as BaseResult<U, P>;
|
||||
```
|
||||
|
||||
等取数完成后会被 `...(fetches[newstFetchKey.current] || {})` 这一段覆盖为正常函数。
|
||||
|
||||
**加载延迟**
|
||||
|
||||
如果设置了加载延迟,请求发动时就不应该立即设置为 loading,这个逻辑写在 `_run` 函数中:
|
||||
|
||||
```tsx
|
||||
function _run(...args: P) {
|
||||
// 取消 loadingDelayTimer
|
||||
if (this.loadingDelayTimer) {
|
||||
clearTimeout(this.loadingDelayTimer);
|
||||
}
|
||||
this.setState({
|
||||
loading: !this.config.loadingDelay,
|
||||
params: args,
|
||||
});
|
||||
|
||||
if (this.config.loadingDelay) {
|
||||
this.loadingDelayTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
}, this.config.loadingDelay);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启动一个 `setTimeout` 将 loading 设为 `true` 即可,这个 timeout 在下次执行 `_run` 时被 `clearTimeout` 清空。
|
||||
|
||||
**自定义请求依赖**
|
||||
|
||||
最明智的做法是利用 `useEffect` 实现,实际代码做了组件 unmount 保护:
|
||||
|
||||
```tsx
|
||||
// refreshDeps 变化,重新执行所有请求
|
||||
useUpdateEffect(() => {
|
||||
if (!manual) {
|
||||
/* 全部重新执行 */
|
||||
Object.values(fetchesRef.current).forEach((f) => {
|
||||
f.refresh();
|
||||
});
|
||||
}
|
||||
}, [...refreshDeps]);
|
||||
```
|
||||
|
||||
非手动条件下,依赖变化所有已存在的 `fetche` 执行 `refresh` 即可。
|
||||
|
||||
分页和加载更多就不解析了,原理是在 `useAsync` 这个基础请求 Hook 基础上再包一层 Hook,拓展取数参数与返回结果。
|
||||
|
||||
## 4 总结
|
||||
|
||||
目前还有 错误重试、请求超时管理、Suspense 没有支持,看完这篇精读后,相信你已经可以提 PR 了。
|
||||
|
||||
> 讨论地址是:[精读《@umijs/use-request》源码 · Issue #249 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/249)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
Reference in New Issue
Block a user