Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d76253ef75 | ||
|
|
46b68f1933 | ||
|
|
22f118e1b7 | ||
|
|
922eb6dc59 | ||
|
|
20526273d3 | ||
|
|
f3222c646b | ||
|
|
9ae28c5e85 | ||
|
|
e72318adb4 | ||
|
|
2682dc0504 | ||
|
|
8b4611b47a | ||
|
|
25938c60a6 | ||
|
|
9920f0dbb7 | ||
|
|
90f7d89db7 | ||
|
|
6ddbdbbbb2 | ||
|
|
3c8c9fd5ef | ||
|
|
91a9bf82fd | ||
|
|
4603ac77d8 | ||
|
|
998940af1b | ||
|
|
24ed724eab | ||
|
|
33b71628ab | ||
|
|
2dbf1429ba | ||
|
|
9867c1ee9d | ||
|
|
e8a0539984 | ||
|
|
685e8ba32a | ||
|
|
6c9493df7b | ||
|
|
1994438792 | ||
|
|
60be3e354b | ||
|
|
9d0bb5c996 | ||
|
|
304dc5fc36 | ||
|
|
819b6d7452 | ||
|
|
b066c8a5d7 | ||
|
|
80fb60c6fb | ||
|
|
6a2031899d | ||
|
|
aedcc2fd56 | ||
|
|
757eb403ac | ||
|
|
87c2745395 | ||
|
|
0d3d8ef54c | ||
|
|
53d5ee1960 | ||
|
|
92a8dfc55c | ||
|
|
c59f46cc1d | ||
|
|
ef3904a2f6 | ||
|
|
994c5844d9 | ||
|
|
3881e51b08 | ||
|
|
815ae1367a | ||
|
|
84ed47dbdf | ||
|
|
252980724a | ||
|
|
225aab476d | ||
|
|
f065539266 | ||
|
|
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 | ||
|
|
5c44507443 | ||
|
|
ffb736eb6d | ||
|
|
abc677a5f0 | ||
|
|
3c78a1a659 | ||
|
|
507df796b0 | ||
|
|
0f58ce27dc | ||
|
|
51c8f3bb69 | ||
|
|
c4da7262cb | ||
|
|
a513286318 | ||
|
|
509dfe2c97 | ||
|
|
806ee0177a | ||
|
|
fe4afdf89c | ||
|
|
caec16066a | ||
|
|
7f53bde9a3 | ||
|
|
63e2ca4d0d | ||
|
|
9dbd1fb7b9 | ||
|
|
7d8816c2c2 | ||
|
|
44ae420f52 | ||
|
|
683b22d1ae | ||
|
|
4e58379585 | ||
|
|
1342144fef | ||
|
|
a41f452df0 | ||
|
|
0950c4cdaf | ||
|
|
46ad26c1a4 | ||
|
|
6dd26bee54 | ||
|
|
37d6b5f3f0 | ||
|
|
f23e88338f | ||
|
|
3dda46d760 | ||
|
|
cd6a00bf3a | ||
|
|
6aad1da704 | ||
|
|
03a5a0f484 | ||
|
|
0066b890d4 | ||
|
|
e0caf77d0a | ||
|
|
1517e86999 | ||
|
|
c8f86df3f3 | ||
|
|
752d598d27 | ||
|
|
e5fd022f00 | ||
|
|
f78c4fb511 | ||
|
|
41a90e5991 | ||
|
|
a4efe24cca | ||
|
|
1a3b36074e | ||
|
|
8dad4956b7 | ||
|
|
852d35501c | ||
|
|
b9d95182a1 | ||
|
|
bfa3ab7b55 | ||
|
|
304e4d3aa3 | ||
|
|
62be743112 | ||
|
|
6d27e723cd | ||
|
|
704c9cda9e | ||
|
|
44dd8057e2 | ||
|
|
d956b15ad0 | ||
|
|
961d77b4d1 | ||
|
|
c763fc15bd | ||
|
|
f9f04d711f | ||
|
|
5d40208d45 | ||
|
|
771bb9f914 | ||
|
|
96f850e29f | ||
|
|
349e7df2b6 | ||
|
|
b198d57cf6 | ||
|
|
03b8840291 | ||
|
|
60899c0bf8 | ||
|
|
648e113e66 | ||
|
|
61a5e74908 | ||
|
|
d2273e72f6 | ||
|
|
386b605c32 | ||
|
|
b60cca25d7 | ||
|
|
68e6c23f82 | ||
|
|
a9d2c1d47a | ||
|
|
cc364c4b2f | ||
|
|
f65b11ea6b | ||
|
|
35458365c6 | ||
|
|
dde997ac27 | ||
|
|
53ae9e077e | ||
|
|
30bdcf13dd | ||
|
|
12b1b02539 | ||
|
|
2a92487a5e | ||
|
|
b6487f8754 | ||
|
|
7a8e1b25dd | ||
|
|
44683fa1dc | ||
|
|
3a4b82bc76 | ||
|
|
4f2ed7b3cb | ||
|
|
a72795b099 | ||
|
|
dea2acc384 | ||
|
|
74f539cc34 | ||
|
|
1947b9c08c | ||
|
|
e84c85cc1d | ||
|
|
170c0f4525 | ||
|
|
c836848228 | ||
|
|
7b87d5c3c4 | ||
|
|
add5bdd01e | ||
|
|
0633764a44 | ||
|
|
fcc183d523 | ||
|
|
627db8a249 | ||
|
|
b41530b37c | ||
|
|
2062b652dd | ||
|
|
d8f2e0977d | ||
|
|
f4e75f5e21 | ||
|
|
365997863e | ||
|
|
ab6978da4a | ||
|
|
f37d496f1b | ||
|
|
3bf34602b4 | ||
|
|
274ffc0a9f | ||
|
|
72e8b69dd7 | ||
|
|
62ac1d76b9 | ||
|
|
e537efe7bd | ||
|
|
f1871f59e5 |
@@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/yarn.lock
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"excludeFiles": [],
|
||||
"rules": {
|
||||
"no-long-code": 0,
|
||||
"no-trailing-punctuation": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "10"
|
||||
before_install:
|
||||
- npm i -g lint-md-cli
|
||||
script: lint-md ./
|
||||
+6
-6
@@ -10,7 +10,7 @@
|
||||
|
||||
<img src="assets/1/cube.jpeg" alt="logo" width="500" />
|
||||
|
||||
> 如今,Javascript 模块化规范非常方便、自然,但这个新规范仅执行了2年,就在 4 年前,js 的模块化还停留在运行时支持,10 年前,通过后端模版定义、注释定义模块依赖。对经历过来的人来说,历史的模块化方式还停留在脑海中,反而新上手的同学会更快接受现代的模块化规范。
|
||||
> 如今,Javascript 模块化规范非常方便、自然,但这个新规范仅执行了 2 年,就在 4 年前,js 的模块化还停留在运行时支持,10 年前,通过后端模版定义、注释定义模块依赖。对经历过来的人来说,历史的模块化方式还停留在脑海中,反而新上手的同学会更快接受现代的模块化规范。
|
||||
|
||||
但为什么要了解 Javascript 模块化发展的历史呢?因为凡事都有两面性,了解 Javascript 模块化规范,有利于我们思考出更好的模块化方案,纵观历史,从 1999 年开始,模块化方案最多维持两年,就出现了新的替代方案,比原有的模块化更清晰、强壮,我们不能被现代模块化方式限制住思维,因为现在的 ES2015 模块化方案距离发布也仅仅过了两年。
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
**外部依赖定义 (2007)**: 这种定义方式在 cocos2d-js 开发中普遍使用,其核心思想是将依赖抽出单独文件定义,这种方式不利于项目管理,毕竟依赖抽到代码之外,我是不是得两头找呢?所以才有通过 webpack 打包为一个文件的方式暴力替换为 commonjs 的方式出现。
|
||||
|
||||
**Sandbox模式 (2009)**: 这种模块化方式很简单,暴力,将所有模块塞到一个 `sanbox` 变量中,硬伤是无法解决明明冲突问题,毕竟都塞到一个 `sandbox` 对象里,而 `Sandbox` 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。
|
||||
**Sandbox 模式 (2009)**: 这种模块化方式很简单,暴力,将所有模块塞到一个 `sandbox` 变量中,硬伤是无法解决命名冲突问题,毕竟都塞到一个 `sandbox` 对象里,而 `Sandbox` 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。
|
||||
|
||||
**依赖注入 (2009)**: 就是大家熟知的 angular1.0,依赖注入的思想现在已广泛运用在 react、vue 等流行框架中。但依赖注入和解决模块化问题还差得远。
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
这篇文章所提供的模块化历史的方案都是逻辑模块化,**从 CommonJS 方案开始前端把服务端的解决方案搬过来之后,算是看到标准物理与逻辑统一的模块化**。但之后前端工程不得不引入模块化构建这一步。正是这一步给前端开发无疑带来了诸多的不便,尤其是现在我们开发过程中经常为了优化这个工具带了很多额外的成本。
|
||||
|
||||
从 CommonJS 之前其实都只是封装,并没有一套模块化规范,这个就有些像类与包的概念。我在10年左右用的最多的还是 YUI2,YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。
|
||||
从 CommonJS 之前其实都只是封装,并没有一套模块化规范,这个就有些像类与包的概念。我在 10 年左右用的最多的还是 YUI2,YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。
|
||||
|
||||
```javascript
|
||||
YUI().use('node', 'event', function (Y) {
|
||||
@@ -103,7 +103,7 @@ YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo
|
||||
|
||||
> 看到大家基本都提到了 HTTP/2,对这项技术解决前端模块化及资源打包等工程问题抱有非常大的期待。很多人也认为 HTTP/2 普及后,基本就没有 Webpack 什么事情了。
|
||||
|
||||
不过 Webpack 作者 @sokra 在他的文章 [webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6#.zdo4juvgo) 里提到了一个新的 Webpack 插件 `AggressiveSplittingPlugin`。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunck 不需要重新下载。
|
||||
不过 Webpack 作者 @sokra 在他的文章 [webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6#.zdo4juvgo) 里提到了一个新的 Webpack 插件 `AggressiveSplittingPlugin`。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunk 不需要重新下载。
|
||||
|
||||
可见,**即使不断的有新技术出现,也依然需要配套的工具来将前端工程问题解决方案推向极致。**
|
||||
|
||||
@@ -118,7 +118,7 @@ YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo
|
||||
### 补充阅读
|
||||
|
||||
- [JavaScript 模块化七日谈](https://huangxuan.me/2015/07/09/js-module-7day/)
|
||||
- [JavaScript模块化编程简史(2009-2016)](https://yuguo.us/weblog/javascript-module-development-history/)
|
||||
- [JavaScript 模块化编程简史(2009-2016)](https://yuguo.us/weblog/javascript-module-development-history/)
|
||||
|
||||
# 总结
|
||||
|
||||
@@ -136,4 +136,4 @@ YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo
|
||||
|
||||
至此,对于 javascript 模块化讨论已接近尾声,对其优缺点也基本达成了一致。前端复杂度不断提高,促使着模块化的改进,代理(浏览器、node) 的支持程度,与前端特殊性(流量、缓存)可能前端永远也离不开构建工具,新的标准会让这些工作做的更好,同时取代、增强部分特征,前端的未来是更加美好的,复杂度也更高。
|
||||
|
||||
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。**
|
||||
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。**
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@
|
||||
|
||||
### 模态框定位
|
||||
|
||||
首先,Model 与 Toast、Notification、Message 以及 Popover 都会在某个时间点被触发弹出一个浮层,但与 Modal(模态框)还是有所不同的。定义上看,上述组件都不属于模态框,因为模态框有一个重要的特性,即阻塞原来主视窗下的操作,只能在框内作后续动作。也就是说模态框从界面上彻底打断了用户心流。
|
||||
首先,Modal 与 Toast、Notification、Message 以及 Popover 都会在某个时间点被触发弹出一个浮层,但与 Modal(模态框)还是有所不同的。定义上看,上述组件都不属于模态框,因为模态框有一个重要的特性,即阻塞原来主视窗下的操作,只能在框内作后续动作。也就是说模态框从界面上彻底打断了用户心流。
|
||||
|
||||
当然,这也是我们需要讨论的问题,如果只是一般的消息提醒,可以用信息条、小红点等交互形式,至少是不阻塞用户操作的。在原文末引用的 10 Guidelines to Consider when using Overlays 一文中,第 8 条强调了模态框不到万不得以不应该使用。这时我们应该思考什么情况下你非常希望他不要离开页面,来读框内的信息或作操作呢?
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
### 可访问性的反思
|
||||
|
||||
Accessibility 翻译过来是『无障碍访问』,是对不同终端用户的体验完善。每一个模态框,都要有通过键盘关闭的功能,通常使用ESC键。似乎我们程序员多少总会把我们自我的惯性思维带进实现的产品,尤其是当我们敲着外置的键盘,用着 PC 的时候。
|
||||
Accessibility 翻译过来是『无障碍访问』,是对不同终端用户的体验完善。每一个模态框,都要有通过键盘关闭的功能,通常使用 ESC 键。似乎我们程序员多少总会把我们自我的惯性思维带进实现的产品,尤其是当我们敲着外置的键盘,用着 PC 的时候。
|
||||
|
||||
下面的这些问题都是对可访问性的反思:
|
||||
|
||||
|
||||
+6
-6
@@ -17,7 +17,7 @@
|
||||
**前端渲染的优势**
|
||||
|
||||
- 局部刷新。无需每次都进行完整页面请求
|
||||
- 懒加载。如在页面初始时只加载可视区域内的数据,滚动后rp加载其它数据,可以通过 react-lazyload 实现
|
||||
- 懒加载。如在页面初始时只加载可视区域内的数据,滚动后 rp 加载其它数据,可以通过 react-lazyload 实现
|
||||
- 富交互。使用 JS 实现各种酷炫效果
|
||||
- 节约服务器成本。省电省钱,JS 支持 CDN 部署,且部署极其简单,只需要服务器支持静态文件即可
|
||||
- 天生的关注分离设计。服务器来访问数据库提供接口,JS 只关注数据获取和展现
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
本次提出独到观点的同学有:[@javie007](http://link.zhihu.com/?target=https%3A//github.com/javie007) [@杨森](https://www.zhihu.com/people/c93b7957f6308990c7e3b16103c9356b) [@流形](https://www.zhihu.com/people/6c772f9726a914ed4a4b90c88010461c) [@camsong](https://www.zhihu.com/people/078cc0fb15845759ad8295b0f0e50099) [@Turbe Xue](https://www.zhihu.com/people/turbe-xue) [@淡苍](https://www.zhihu.com/people/5ac53c9c0484e83672e1c1716bdf0ff9) [@留影](https://www.zhihu.com/people/38c3c75795824de1bc5d99cff904a832) [@FrankFang](http://link.zhihu.com/?target=https%3A//github.com/FrankFang) [@alcat2008](http://link.zhihu.com/?target=https%3A//github.com/alcat2008) [@xile611](http://link.zhihu.com/?target=https%3A//github.com/xile611) [@twobin](http://link.zhihu.com/?target=https%3A//github.com/twobin) [@黄子毅](https://www.zhihu.com/people/3ec85a04bc9eaa35b1830874cc463a52) 精读由此归纳。
|
||||
|
||||
大家对前端和后端渲染的现状基本达成共识。即前端渲染是未来趋势,但前端渲染遇到了首屏性能和SEO的问题。对于同构争议最多,在此我归纳一下。
|
||||
大家对前端和后端渲染的现状基本达成共识。即前端渲染是未来趋势,但前端渲染遇到了首屏性能和 SEO 的问题。对于同构争议最多,在此我归纳一下。
|
||||
|
||||
### 前端渲染遇到的问题
|
||||
|
||||
@@ -46,7 +46,7 @@ SEO 很好理解。由于传统的搜索引擎只会从 HTML 中抓取数据,
|
||||
|
||||
### 同构的优点
|
||||
|
||||
同构恰恰就是为了解决前端渲染遇到的问题才产生的,至 2014 年底伴随着 React 的崛起而被认为是前端框架应具备的一大杀器,以至于当时很多人为了用此特性而[放弃 Angular 1 而转向 React](http://link.zhihu.com/?target=https%3A//blog.risingstack.com/from-angularjs-to-react-the-isomorphic-way/)。然而近3年过去了,很多产品逐渐从全栈同构的理想化逐渐转到首屏或部分同构。让我们再一次思考同构的优点真是优点吗?
|
||||
同构恰恰就是为了解决前端渲染遇到的问题才产生的,至 2014 年底伴随着 React 的崛起而被认为是前端框架应具备的一大杀器,以至于当时很多人为了用此特性而[放弃 Angular 1 而转向 React](http://link.zhihu.com/?target=https%3A//blog.risingstack.com/from-angularjs-to-react-the-isomorphic-way/)。然而近 3 年过去了,很多产品逐渐从全栈同构的理想化逐渐转到首屏或部分同构。让我们再一次思考同构的优点真是优点吗?
|
||||
|
||||
1. 有助于 SEO
|
||||
|
||||
@@ -65,7 +65,7 @@ SEO 很好理解。由于传统的搜索引擎只会从 HTML 中抓取数据,
|
||||
|
||||
1. 性能
|
||||
|
||||
把原来放在几百万浏览器端的工作拿过来给你几台服务器做,这还是花挺多计算力的。尤其是涉及到图表类需要大量计算的场景。这方面调优,可以参考 [walmart的调优策略](https://medium.com/walmartlabs/reactjs-ssr-profiling-and-caching-5d8e9e49240c)。
|
||||
把原来放在几百万浏览器端的工作拿过来给你几台服务器做,这还是花挺多计算力的。尤其是涉及到图表类需要大量计算的场景。这方面调优,可以参考 [walmart 的调优策略](https://medium.com/walmartlabs/reactjs-ssr-profiling-and-caching-5d8e9e49240c)。
|
||||
|
||||
个性化的缓存是遇到的另外一个问题。可以把每个用户个性化信息缓存到浏览器,这是一个天生的分布式缓存系统。我们有个数据类应用通过在浏览器合理设置缓存,双十一当天节省了 70% 的请求量。试想如果这些缓存全部放到服务器存储,需要的存储空间和计算都是很非常大。
|
||||
|
||||
@@ -97,7 +97,7 @@ export const isSsr = () => (
|
||||
|
||||
5. simple store(redux)
|
||||
|
||||
这个 store 是必须以字符串形式塞到前端,所以复杂类型是无法转义成字符串的,比如function。
|
||||
这个 store 是必须以字符串形式塞到前端,所以复杂类型是无法转义成字符串的,比如 function。
|
||||
|
||||
总的来说,同构渲染实施难度大,不够优雅,无论在前端还是服务端,都需要额外改造。
|
||||
|
||||
@@ -136,7 +136,7 @@ Next.js 是时下非常流行的基于 React 的同构开发框架。作者之
|
||||
|
||||
1. 巧妙地用标准化的解决了请求的问题。同构和页面开发类似,异步是个大难题,异步中难点又在接口请求。Next.js 给组件新增了 getInitialProps 方法来专门处理初始化请求,再也不用手动往页面上塞 DATA 和调用 ReactDOMServer.renderToString
|
||||
2. 使用 [styled-jsx](https://github.com/zeit/styled-jsx) 解决了 css-in-js 的问题。这种方案虽然不像 styled-component 那样强大,但足够简单,可以说是最小的成本解决了问题
|
||||
3. Fast by default。页面默认拆分文件方式打包,支持Prefetch页面预加载
|
||||
3. Fast by default。页面默认拆分文件方式打包,支持 Prefetch 页面预加载
|
||||
|
||||
全家桶式的的解决方案。简洁清晰的目录结构,这一点 Redux 等框架真应该学一学。不过全家桶的方案比较适合全新项目使用,旧项目使用要评估好成本
|
||||
|
||||
|
||||
@@ -164,11 +164,11 @@ async function mount() {
|
||||
|
||||
```javascript
|
||||
async function mount() {
|
||||
const result = await Promise.all(
|
||||
const result = await Promise.all([
|
||||
fetch('a.json'),
|
||||
fetch('b.json'),
|
||||
fetch('c.json')
|
||||
);
|
||||
]);
|
||||
|
||||
render(...result);
|
||||
}
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@
|
||||
3. 局部与全局状态的归一
|
||||
4. 分形思想
|
||||
5. action 分散执行
|
||||
5. app级别数据处理,推荐前端 `Orm`
|
||||
5. app 级别数据处理,推荐前端 `Orm`
|
||||
|
||||
整体来看,核心思路是推荐组件内部完成数据流的处理,不用关心使用了 `Redux` `Mobx` 或者 `Rxjs`,也不用关心这些库是否有全局管理的野心,如果全局管理那就挂载到全局,但组件内部还是局部管理。
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
以 Mobx 为代表,轻前端用的较多,因为复杂度集中在后端,前端做好数据展示即可,那么直接拥抱 js 这种基于对象的语言,结合原生 `Map` `Proxy` `Reflect` 将副作用进行到底,开发速度快得飞起。
|
||||
|
||||
数据存储方式按照视图形态来,因为视图之间几乎毫无关联,而且特别是数据产品,后端数据量巨大,把数据处理过程搬到前端是不可能的(为了推导出一个视图形态数据,需要动辄几GB的原始数据运算,存储和性能都不适合在前端做)。
|
||||
数据存储方式按照视图形态来,因为视图之间几乎毫无关联,而且特别是数据产品,后端数据量巨大,把数据处理过程搬到前端是不可能的(为了推导出一个视图形态数据,需要动辄几 GB 的原始数据运算,存储和性能都不适合在前端做)。
|
||||
|
||||
#### 函数式
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
### 分形的缺点
|
||||
|
||||
对于聊天室或者在线IDE等,全局数据居多,很多交叉绑定的情况,就不适合分形思想,反而纯 Redux 思想更合适。
|
||||
对于聊天室或者在线 IDE 等,全局数据居多,很多交叉绑定的情况,就不适合分形思想,反而纯 Redux 思想更合适。
|
||||
|
||||
## 3.3 数据形态,是原始数据还是视图数据?
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
Stack 部分主要在阐明 js 中函数调用栈的概念,它符合栈的基本特性『当调用时,压入栈顶。当它执行完毕时,被弹出栈』,简单看下面的代码:
|
||||
|
||||
```
|
||||
```plain
|
||||
function c() {
|
||||
try {
|
||||
var bar = baz;
|
||||
@@ -46,7 +46,7 @@ a();
|
||||
|
||||
## 如何使用堆栈追踪
|
||||
|
||||
该部分以 NodeJS 环境为例,讲解了 `Error.captureStackTrace `,将 stack 信息作为属性存储在一个对象当中,同时可以过滤掉一些无用的堆栈信息。这样可以隐藏掉用户不需要了解的内部细节。作者也以 Chai 为例,内部使用该方法对代码的调用者屏蔽了不相关的实现细节。通过以 Assertion 对象为例,讲述了具体的内部实现,简单来说通过一个 addChainableMethod 链式调用工具方法,在运行一个 Assertion 时,将它设为标记,其后面的堆栈会被移除;如果 assertion 失败移除起后面所有内部堆栈;如果有内嵌 assertion,将当前 assertion 的方法放到 ssfi 中作为标记,移除后面堆栈帧;
|
||||
该部分以 NodeJS 环境为例,讲解了 `Error.captureStackTrace`,将 stack 信息作为属性存储在一个对象当中,同时可以过滤掉一些无用的堆栈信息。这样可以隐藏掉用户不需要了解的内部细节。作者也以 Chai 为例,内部使用该方法对代码的调用者屏蔽了不相关的实现细节。通过以 Assertion 对象为例,讲述了具体的内部实现,简单来说通过一个 addChainableMethod 链式调用工具方法,在运行一个 Assertion 时,将它设为标记,其后面的堆栈会被移除;如果 assertion 失败移除起后面所有内部堆栈;如果有内嵌 assertion,将当前 assertion 的方法放到 ssfi 中作为标记,移除后面堆栈帧;
|
||||
|
||||
# 3. 精读
|
||||
参与本次精读的同学有:[范洪春](https://www.zhihu.com/people/fanhc/activities)、[黄子毅](https://www.zhihu.com/people/huang-zi-yi-83/answers)、[杨森](https://www.zhihu.com/people/yangsen/answers)、[camsong](https://www.zhihu.com/people/camsong/answers),该部分由他们的观点总结而出。
|
||||
@@ -83,7 +83,7 @@ describe('should.js和power-assert的区别', () => {
|
||||
|
||||
- 区分操作异常和程序员的失误。操作异常指可预测的不可避免的异常,如无法连接服务器
|
||||
- 操作异常应该被处理。程序员的失误不需要处理,如果处理了反而会影响错误排查
|
||||
- 操作异常有两种处理方式:同步 (try…catch) 和异步(callback, event - emitter)两种处理方式,但只能选择其中一种。
|
||||
- 操作异常有两种处理方式:同步 (try……catch) 和异步(callback, event - emitter)两种处理方式,但只能选择其中一种。
|
||||
- 函数定义时应该用文档写清楚参数类型,及可能会发生的合理的失败。以及错误是同步还是异步传给调用者的
|
||||
- 缺少参数或参数无效是程序员的错误,一旦发生就应该 throw。
|
||||
传递错误时,使用标准的 Error 对象,并附件尽可能多的错误信息,可以使用标准的属性名
|
||||
|
||||
@@ -81,7 +81,7 @@ react-css-modules 引入了 styleName,将本地变量和全局变量很清晰
|
||||
|
||||
另外,使用 react-css-modules,可以方便的覆盖本地变量的样式:
|
||||
|
||||
```
|
||||
```plain
|
||||
import customStyles from './table-custom-styles.css';
|
||||
|
||||
<Table styles={customStyles} />;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
# 2 内容概要
|
||||
|
||||
使用 Object.assign 作用于大对象时,速度会成为瓶颈,比如拥有 `100,000` 个属性的对象,这个操作耗费了 134ms。性能损失主要原因是 “结构共享” 操作需要遍历近10万个属性,而这些引用操作耗费了100ms以上的时间。
|
||||
使用 Object.assign 作用于大对象时,速度会成为瓶颈,比如拥有 `100,000` 个属性的对象,这个操作耗费了 134ms。性能损失主要原因是 “结构共享” 操作需要遍历近 10 万个属性,而这些引用操作耗费了 100ms 以上的时间。
|
||||
|
||||
解决办法就是减少引用指向的操作数量,而且由于引用指向到任何对象的损耗都几乎一致(无论目标对象极限小或者无穷大,引用消耗时间都几乎没有区别),我们需要一种精心设计的树状结构将打平的引用建立深度,以减少引用操作次数,`vector tries` 就是一种解决思路:
|
||||
|
||||
|
||||
+16
-16
@@ -7,29 +7,29 @@
|
||||
|
||||
我为什么要选这篇文章呢?
|
||||
|
||||
就在前几天的 Google I/O 2017上, Polymer 正式发布了 [Polymer 2.0](https://www.polymer-project.org/blog/2017-05-15-time-for-two) 版本.
|
||||
就在前几天的 Google I/O 2017 上, Polymer 正式发布了 [Polymer 2.0](https://www.polymer-project.org/blog/2017-05-15-time-for-two) 版本.
|
||||
|
||||
来看一下 Polymer 2.0 的一些变化:
|
||||
- 使用 Shadow DOM v1 代替 Polymer.dom. Shady DOM从 Polymer 中分离出来。
|
||||
- 使用 Shadow DOM v1 代替 Polymer.dom. Shady DOM 从 Polymer 中分离出来。
|
||||
- 使用 标准的 ES6 类和 Custom Elements v1 来自定义元素.
|
||||
- 还有数据系统的改进和生命周期的变更.
|
||||
|
||||
可以看到, Polymer 的这次升级主要是将 Shadow Dom 和 Custom Elements 升级到 v1 版本, 以获得更多浏览器的原生支持. 下一 代Web Components - v1规范,Chrome 已经支持了,Web Components 规范中的2个主要部分 - [Shadow Dom](https://www.chromestatus.com/feature/4667415417847808) 和 [Custom Elements](https://www.chromestatus.com/feature/4696261944934400). Safari在10版本中, 支持了 [Shadow DOM v1](https://webkit.org/status/#feature-shadow-dom) 规范并且完成了在Webkit内核中对 [Custom Elements v1](https://webkit.org/blog/7027/introducing-custom-elements/) 规范的实现;Firefox对 [Shadow DOM](https://platform-status.mozilla.org/#shadow-dom) 和 [Custom Elements v1规范](https://platform-status.mozilla.org/#custom-elements) 支持正在开发中;Edge也将对 [Shadow DOM](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/shadowdom/) 和 [Custom Elements](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/customelements/) 支持规划到他们的开发roadmap中。
|
||||
可以看到, Polymer 的这次升级主要是将 Shadow Dom 和 Custom Elements 升级到 v1 版本, 以获得更多浏览器的原生支持. 下一 代 Web Components - v1 规范,Chrome 已经支持了,Web Components 规范中的 2 个主要部分 - [Shadow Dom](https://www.chromestatus.com/feature/4667415417847808) 和 [Custom Elements](https://www.chromestatus.com/feature/4696261944934400). Safari 在 10 版本中, 支持了 [Shadow DOM v1](https://webkit.org/status/#feature-shadow-dom) 规范并且完成了在 Webkit 内核中对 [Custom Elements v1](https://webkit.org/blog/7027/introducing-custom-elements/) 规范的实现;Firefox 对 [Shadow DOM](https://platform-status.mozilla.org/#shadow-dom) 和 [Custom Elements v1 规范](https://platform-status.mozilla.org/#custom-elements) 支持正在开发中;Edge 也将对 [Shadow DOM](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/shadowdom/) 和 [Custom Elements](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/customelements/) 支持规划到他们的开发 roadmap 中。
|
||||
|
||||
这段时间, 大家都在讨论 react, vue, angular, 这些框架. 或者 该使用 redux 还 是 mobx 做数据管理. 在这个契机下, 我想我们可以不单单去思考这些框架, 也可以更多地去思考和了解 Web Components 标准. 对于 Web Components标准有一些思考. 所以我选了一篇关于 Web Components 的文章, 想让大家对于 Web Components 的发展, 和 Web Componets 与现在的主流框架如何协作有更多的思考和讨论.
|
||||
这段时间, 大家都在讨论 react, vue, angular, 这些框架. 或者 该使用 redux 还 是 mobx 做数据管理. 在这个契机下, 我想我们可以不单单去思考这些框架, 也可以更多地去思考和了解 Web Components 标准. 对于 Web Components 标准有一些思考. 所以我选了一篇关于 Web Components 的文章, 想让大家对于 Web Components 的发展, 和 Web Componets 与现在的主流框架如何协作有更多的思考和讨论.
|
||||
|
||||
|
||||
# 2 内容概要
|
||||
|
||||
**The broken promise of Web Components**
|
||||
原文作者dmitriid主要是在喷Web Components从2011年到2017年这6年间毫无进展, 一共产出了6份标准, 其中两份已经被弃用. 几乎只有一个主流浏览器(chrome) 支持.
|
||||
原文作者 dmitriid 主要是在喷 Web Components 从 2011 年到 2017 年这 6 年间毫无进展, 一共产出了 6 份标准, 其中两份已经被弃用. 几乎只有一个主流浏览器(chrome) 支持.
|
||||
|
||||

|
||||
|
||||
|
||||
- Web Components 这些规范强依赖 JS 的实现
|
||||
- Custom Elements 是 JS 脚本的一部分
|
||||
- HTML Templates 的出现就是为了被JS 脚本使用
|
||||
- HTML Templates 的出现就是为了被 JS 脚本使用
|
||||
- Shadow Dom 也需要配合 JS 脚本使用
|
||||
- 只有 HTML imports 可以脱离 JS 脚本使用
|
||||
- Web Components 操作 DOM
|
||||
@@ -38,16 +38,16 @@
|
||||
- 为了突破限制使用不同的方法来传递数据
|
||||
- CSS 作用域, 可以见上次精读[《请停止 css-in-js 的行为》](https://github.com/dt-fe/weekly/issues/12)
|
||||
|
||||
**来看一下Polymer 的 核心成员 Rob Dodson 对于本文的回应: Regarding the broken promise of Web Components**
|
||||
**来看一下 Polymer 的 核心成员 Rob Dodson 对于本文的回应: Regarding the broken promise of Web Components**
|
||||
|
||||
- Web Components 特性需要被浏览器支持,必须有平缓的过渡,良好的兼容,以及成熟的方案,因此推进速度会比较慢一些。
|
||||
- React 很棒, 但是也不要忽略其他基于 Web Components 的优秀库比如 [Amp](https://www.ampproject.org/)
|
||||
- 对于 DOM 更新的抽象比如 React/JSX很赞, 但是也带来了一些损耗. 在旧的移动设备上, 加载一个大的js 包性能依旧不理想, 最佳的做法是拆分你的 JS 包, 按需加载.
|
||||
- 使用 JSX 和 虚拟 DOM是很酷, 也可以直接把 JSX 用在 Web Components 内, 像[SkateJS](https://github.com/skatejs/skatejs)库, 已经在做这个事情了.
|
||||
- 没有标准的数据绑定, Polymer的数据绑定, 现在是基于[MDV](https://github.com/toolkitchen/mdv), 很多开发者更倾向于基于 Observables或者 ES6 Proxies的数据绑定方案.
|
||||
- 处理组件的字符串属性是很烦人, 但是由于每一个组件都是一个类的实例, 可以利用ES6 的 getters/setters来改变属性.
|
||||
- 对于 DOM 更新的抽象比如 React/JSX 很赞, 但是也带来了一些损耗. 在旧的移动设备上, 加载一个大的 js 包性能依旧不理想, 最佳的做法是拆分你的 JS 包, 按需加载.
|
||||
- 使用 JSX 和 虚拟 DOM 是很酷, 也可以直接把 JSX 用在 Web Components 内, 像[SkateJS](https://github.com/skatejs/skatejs)库, 已经在做这个事情了.
|
||||
- 没有标准的数据绑定, Polymer 的数据绑定, 现在是基于[MDV](https://github.com/toolkitchen/mdv), 很多开发者更倾向于基于 Observables 或者 ES6 Proxies 的数据绑定方案.
|
||||
- 处理组件的字符串属性是很烦人, 但是由于每一个组件都是一个类的实例, 可以利用 ES6 的 getters/setters 来改变属性.
|
||||
|
||||
Rob Dodson对于 Web Components 依然充满信心, 但是也承认推进标准总会有各种阻碍, 不可能像推荐框架一样快速把事情解决.
|
||||
Rob Dodson 对于 Web Components 依然充满信心, 但是也承认推进标准总会有各种阻碍, 不可能像推荐框架一样快速把事情解决.
|
||||
|
||||
# 3 精读
|
||||
|
||||
@@ -55,11 +55,11 @@ Rob Dodson对于 Web Components 依然充满信心, 但是也承认推进标准
|
||||
[@camsong](https://www.zhihu.com/people/078cc0fb15845759ad8295b0f0e50099) [@黄子毅](https://github.com/ascoders) [@杨森](https://www.zhihu.com/people/c93b7957f6308990c7e3b16103c9356b) [@rccoder](https://github.com/rccoder) [@alcat2008](https://github.com/alcat2008)精读由此归纳。
|
||||
|
||||
### 标准与框架
|
||||
Web Components 作为一个标准,骨子里的进度就会落后于当前可行的技术体系。正如文中所说,浏览器厂商 ship 一个新功能是很严肃的,很可能会影响到一票的线上业务,甚至会影响到一个产业(遥想当年 [Chrome Extension 禁用 NPAPI](https://blog.chromium.org/2013/09/saying-goodbye-to-our-old-friend-npapi.html)时的一片哀鸿遍野,许多返利插件都使用了这种技术)。那么 Web Components的缓慢推进也在情理之中了.
|
||||
即使真的有一天这个标准建立起来,Web Components作为浏览器底层特性不应该拿出来和React这类应用层框架相比较. 未来Web Components会做为浏览器非常重要的特性存在。API偏低层操作,会易用性不够. 在很长时间内开发者依旧会使用 React/Vue/Angular/Polymer 这样的框架,Web Components可能会做为这些框架的底层做一些 浏览器层面上的支持.
|
||||
Web Components 作为一个标准,骨子里的进度就会落后于当前可行的技术体系。正如文中所说,浏览器厂商 ship 一个新功能是很严肃的,很可能会影响到一票的线上业务,甚至会影响到一个产业(遥想当年 [Chrome Extension 禁用 NPAPI](https://blog.chromium.org/2013/09/saying-goodbye-to-our-old-friend-npapi.html)时的一片哀鸿遍野,许多返利插件都使用了这种技术)。那么 Web Components 的缓慢推进也在情理之中了.
|
||||
即使真的有一天这个标准建立起来,Web Components 作为浏览器底层特性不应该拿出来和 React 这类应用层框架相比较. 未来 Web Components 会做为浏览器非常重要的特性存在。API 偏低层操作,会易用性不够. 在很长时间内开发者依旧会使用 React/Vue/Angular/Polymer 这样的框架,Web Components 可能会做为这些框架的底层做一些 浏览器层面上的支持.
|
||||
|
||||
### 不需要 vendor 的自定义组件间调用
|
||||
在 Webpack 大行其道的时代,想在运行时做到组件即引即用变得很困难,因为这些组件大多是通过 React/Vue/Angular 开发的。不得不考虑引入一大堆 Vendor 包,这些 Vendor 里可能还必须包含 React 这类两个版本不能同时使用的库。目前我们团队在做组件化方案时就遇到这个问题,只能想办法避免两个版本的出现。你可以说这是 React 或 Webpack 引入的问题,但并没有看到 Web Compnents 标准化的解决方案。我想未来Web Components可能会作为浏览器的底层, 出现基于底层的标准方案来做组件间的相互应用的方法.
|
||||
在 Webpack 大行其道的时代,想在运行时做到组件即引即用变得很困难,因为这些组件大多是通过 React/Vue/Angular 开发的。不得不考虑引入一大堆 Vendor 包,这些 Vendor 里可能还必须包含 React 这类两个版本不能同时使用的库。目前我们团队在做组件化方案时就遇到这个问题,只能想办法避免两个版本的出现。你可以说这是 React 或 Webpack 引入的问题,但并没有看到 Web Compnents 标准化的解决方案。我想未来 Web Components 可能会作为浏览器的底层, 出现基于底层的标准方案来做组件间的相互应用的方法.
|
||||
|
||||
|
||||
### 为什么对 Web components 讨论不断
|
||||
@@ -69,7 +69,7 @@ Web Components 作为一个标准,骨子里的进度就会落后于当前可
|
||||
但使用前端框架的问题也日益暴露,随着前端框架种类的增多,同一个框架不同版本之间无法共存,导致组件无法跨框架复用,甚至只能固定在框架的某个版本,这与前端未来的模块化发展是相违背的,我们越是与之抗衡,就越希望 Web components 能站出来解决这个问题,因为浏览器原生支持模块化,相当于将 react angular vue 的能力内置在浏览器中,而且一定会向前兼容(这也是 Web components 推进缓慢的原因)。
|
||||
|
||||
# 4 总结
|
||||
我觉得 Web Components作为浏览器底层特性不应该拿出来和React, vue 这类应用层框架相比较. Web Components 的方向以及提供的价值都不会跟 应用框架一致. 而 Web Components 作为未来的 Web 组件标准 , 它在任何生态中都可以运行良好. 我倒是更加期待应用层去基于 Web Components 去做更多的实现, 让组件超越框架存在, 可以在不同技术栈中使用.
|
||||
我觉得 Web Components 作为浏览器底层特性不应该拿出来和 React, vue 这类应用层框架相比较. Web Components 的方向以及提供的价值都不会跟 应用框架一致. 而 Web Components 作为未来的 Web 组件标准 , 它在任何生态中都可以运行良好. 我倒是更加期待应用层去基于 Web Components 去做更多的实现, 让组件超越框架存在, 可以在不同技术栈中使用.
|
||||
|
||||
|
||||
> 讨论地址是:[精读《Web Components 的困境》 · Issue #15 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/15)
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ Chrome Dev Tools 非常强大,[dev-tips](https://umaar.com/dev-tips/) 列出
|
||||
|
||||
### 移动端控制台
|
||||
|
||||
- [Chrome远程调试](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews) app 支持后,连接 usb 或者局域网,即可通过 Dev Tools 调试 webview 页面。
|
||||
- [Chrome 远程调试](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews) app 支持后,连接 usb 或者局域网,即可通过 Dev Tools 调试 webview 页面。
|
||||
- [Weinre](http://people.apache.org/~pmuellr/weinre/docs/latest/Home.html) 通过页面加载脚本,与 pc 端调试器通信。
|
||||
- 通过内嵌控制台解决,比如 [eruda](http://eruda.liriliri.io/) [VConsole](https://github.com/WechatFE/vConsole)
|
||||
- [Rosin](http://alloyteam.github.io/Rosin/) fiddler 的一个插件,协助移动页面调试。
|
||||
|
||||
@@ -107,7 +107,7 @@ connect(props => ({
|
||||
|
||||
## HOC 的具体实践
|
||||
|
||||
HOC 在真实场景下的运行非常多,之前笔者在 [基于Decorator的组件扩展实践](https://zhuanlan.zhihu.com/p/22054582) 一文中也提过使用高阶组件将更细粒度的组件组合成 Selector 与 Search。结合精读文章,这次让我们通过 Form 组件的抽象来表现 HOC 具有的良好扩展机制。
|
||||
HOC 在真实场景下的运行非常多,之前笔者在 [基于 Decorator 的组件扩展实践](https://zhuanlan.zhihu.com/p/22054582) 一文中也提过使用高阶组件将更细粒度的组件组合成 Selector 与 Search。结合精读文章,这次让我们通过 Form 组件的抽象来表现 HOC 具有的良好扩展机制。
|
||||
|
||||
Form 中会包含各种不同的组件,常见的有 Input、Selector、Checkbox 等等,也会有根据业务需求加入的自定义组件。Form 灵活多变,从功能上看,表单校验可能为单组件值校验,也可能为全表单值校验,可能为常规检验,比如:非空、输入限制,也可能需要与服务端配合,甚至需要根据业务特点进行定制。从 UI 上看,检验结果显示的位置,可能在组件下方,也可能是在组件右侧。
|
||||
|
||||
@@ -115,7 +115,7 @@ Form 中会包含各种不同的组件,常见的有 Input、Selector、Checkbo
|
||||
|
||||

|
||||
|
||||
至于 HOC 在 Form 上的具体实现,首先将表单中的组件(Input、Selector...)与相应 validator 与组件值回调函数名(trigger)传入 Decorator,将 validator 与 trigger 相绑定。Decorator 完成了各种不同组件与 Form 内置 Store 间 value 的传递、校验功能的抽象,即精读文章中提到 Props Proxy 方式的其中两种作用:**提取state** 与 **操作props**
|
||||
至于 HOC 在 Form 上的具体实现,首先将表单中的组件(Input、Selector...)与相应 validator 与组件值回调函数名(trigger)传入 Decorator,将 validator 与 trigger 相绑定。Decorator 完成了各种不同组件与 Form 内置 Store 间 value 的传递、校验功能的抽象,即精读文章中提到 Props Proxy 方式的其中两种作用:**提取 state** 与 **操作 props**
|
||||
|
||||
```javascript
|
||||
function formFactoryFactory({
|
||||
|
||||
+1
-1
@@ -215,7 +215,7 @@ var bar = foo.bind(obj)
|
||||
bar()
|
||||
```
|
||||
|
||||
### 3.2.2 es6绑定
|
||||
### 3.2.2 es6 绑定
|
||||
|
||||
这种情况类似使用箭头函数创建成员变量,以下方式等于创建了没有挂载到原型链的匿名函数,因此 this 不会丢失。
|
||||
|
||||
|
||||
+21
-21
@@ -2,8 +2,8 @@
|
||||
|
||||
# 1 引言
|
||||
|
||||
随着前端ES6 ES7 的一路前行, 我们大前端借鉴和引进了各种其他编程语言中的概念、特性、模式;
|
||||
我们可以使用函数式Functional编程设计,可以使用面向对象OOP的设计,可以使用面向接口的思想,也可以使用AOP,
|
||||
随着前端 ES6 ES7 的一路前行, 我们大前端借鉴和引进了各种其他编程语言中的概念、特性、模式;
|
||||
我们可以使用函数式 Functional 编程设计,可以使用面向对象 OOP 的设计,可以使用面向接口的思想,也可以使用 AOP,
|
||||
可以使用注解,代理、反射,各种设计模式; 在大前端辉煌发展、在数据时代的当下 我们一起阅读了一篇设计相关的老文:
|
||||
《The DCI Architecture》
|
||||
一起来再探索和复习一下 相关的设计和思想
|
||||
@@ -11,15 +11,15 @@
|
||||
|
||||
# 2 内容摘要
|
||||
|
||||
DCI是数据Data 场景Context 交互Interactions 简称, 重点是关注 数据的不同场景的交互行为, 是面向对象系统 状态和行为的一种范式设计;
|
||||
DCI在许多方面是许多过去范式的统一,多年来这些模式已经成为面向对象编程的辅助工具。
|
||||
DCI 是数据 Data 场景 Context 交互 Interactions 简称, 重点是关注 数据的不同场景的交互行为, 是面向对象系统 状态和行为的一种范式设计;
|
||||
DCI 在许多方面是许多过去范式的统一,多年来这些模式已经成为面向对象编程的辅助工具。
|
||||
|
||||
尽管面向切面的编程(AOP)也有其他用途,但DCI满足了许多AOP的应用以及Aspects在解决问题方面的许多目标。根据AOP的基本原理,DCI基于深层次的反射或元编程。
|
||||
与Aspects不同,角色聚合并组合得很好。Context提供角色集之间的关联的范围关闭,而Aspect仅与应用它们的对象配对。
|
||||
在许多时候,虽然混合本身缺乏我们在Context语义中发现的动力 ,但DCI反映了混合风格策略。
|
||||
DCI实现了多范式设计的许多简单目标,能够将过程逻辑与对象逻辑分开。然而,DCI具有比多范式设计提供的更强大的技术更好的耦合和内聚效果
|
||||
尽管面向切面的编程(AOP)也有其他用途,但 DCI 满足了许多 AOP 的应用以及 Aspects 在解决问题方面的许多目标。根据 AOP 的基本原理,DCI 基于深层次的反射或元编程。
|
||||
与 Aspects 不同,角色聚合并组合得很好。Context 提供角色集之间的关联的范围关闭,而 Aspect 仅与应用它们的对象配对。
|
||||
在许多时候,虽然混合本身缺乏我们在 Context 语义中发现的动力 ,但 DCI 反映了混合风格策略。
|
||||
DCI 实现了多范式设计的许多简单目标,能够将过程逻辑与对象逻辑分开。然而,DCI 具有比多范式设计提供的更强大的技术更好的耦合和内聚效果
|
||||
|
||||
结合ATM 汇款场景案例,讲解了一下 DCI
|
||||
结合 ATM 汇款场景案例,讲解了一下 DCI
|
||||
角色提供了和用户相关 自然的边界,以转账为例,我们实际谈论的是钱的转移,以及源账户和目标账户的角色,算法(用例 角色行为集合)应该是这样:
|
||||
1.账户拥有人选择从一个账户到另外一个账户的钞票转移。
|
||||
2.系统显示有效账户
|
||||
@@ -35,12 +35,12 @@ DCI实现了多范式设计的许多简单目标,能够将过程逻辑与对
|
||||
2.源账户确认余额可用
|
||||
3.源账户减少其帐目
|
||||
4.源账户请求目标账户增加其帐目
|
||||
5.源账户请求目标账户更新其日志log
|
||||
5.源账户请求目标账户更新其日志 log
|
||||
6.源账户结束交易事务
|
||||
7.源账户显示给账户拥有人转账成功。
|
||||
|
||||
|
||||
```
|
||||
```plain
|
||||
template <class ConcreteAccountType>
|
||||
class TransferMoneySourceAccount: public MoneySource
|
||||
{
|
||||
@@ -88,7 +88,7 @@ DCI 尝试从人类思维角度出发,举一个例子:为什么在看电影
|
||||
2. 人或物发生了什么行为、交互?
|
||||
3. 现在在哪?厨房?太空舱?或者原始森林?
|
||||
|
||||
很快把这三件事弄清楚,我们就能快速理解当前场景的逻辑,并且**轻松理解该场景继续发生的状况**,即便是盗梦空间这种烧脑的电影,当我们搞清楚这三个问题后,就算街道发生了180度扭曲,也不会存在理解障碍,反而可以吃着爆米花享受,直到切换到下一个场景为止。
|
||||
很快把这三件事弄清楚,我们就能快速理解当前场景的逻辑,并且**轻松理解该场景继续发生的状况**,即便是盗梦空间这种烧脑的电影,当我们搞清楚这三个问题后,就算街道发生了 180 度扭曲,也不会存在理解障碍,反而可以吃着爆米花享受,直到切换到下一个场景为止。
|
||||
|
||||
当我们把街道扭曲 180 度的能力放在街道对象上时,理解就变的复杂了:这个函数什么时候被调用?为什么不好好承载车辆而自己发生扭曲?这就像电影开始时,把电影里播放的所有关于街道的状态都走马灯过一遍:我们看到街道通过了车辆、又卷曲、又发生了爆炸,实在觉得莫名其妙。
|
||||
|
||||
@@ -210,23 +210,23 @@ object MoneyTransferApp extends App {
|
||||
|
||||
- [Comparison of Architecture presentation patterns MVP(SC),MVP(PV),PM,MVVM and MVC](https://www.codeproject.com/Articles/66585/Comparison-of-Architecture-presentation-patterns-M)
|
||||
- [The DCI Architecture: A New Vision of Object-Oriented Programming](http://www.artima.com/articles/dci_vision.html)
|
||||
- [干净的架构The Clean Architecture](https://www.bbsmax.com/A/pRdBWY3ezn/)
|
||||
- [MVC的替代方案](https://gxnotes.com/article/71237.html)
|
||||
- [展示模式架构比较MVP(SC),MVP(PV),PM,MVVM和MVC](http://blog.csdn.net/lihenair/article/details/51791915)
|
||||
- [干净的架构 The Clean Architecture](https://www.bbsmax.com/A/pRdBWY3ezn/)
|
||||
- [MVC 的替代方案](https://gxnotes.com/article/71237.html)
|
||||
- [展示模式架构比较 MVP(SC),MVP(PV),PM,MVVM 和 MVC](http://blog.csdn.net/lihenair/article/details/51791915)
|
||||
- [Software Architecture Design](https://github.com/zenany/weekly/blob/master/resources/software_architecture.md)
|
||||
- [【译】什么是 Flux 架构?(兼谈 DDD 和 CQRS)](https://blog.jimmylv.info/2016-07-07-what-the-flux-on-flux-ddd-and-cqrs/)
|
||||
|
||||
|
||||
## 结合DCI 设想开发的过程中使用到一些设计方法和原则
|
||||
## 结合 DCI 设想开发的过程中使用到一些设计方法和原则
|
||||
|
||||
我们在开发的过程中多多少少都会使用到一些设计方法和原则
|
||||
DCI 重点是关注 数据的不同场景的交互行为, 是面向对象系统 状态和行为的一种范式设计;
|
||||
|
||||
它能够将过程逻辑与对象逻辑分开,是一种典型的行为模式设计;
|
||||
很好的点是 它根据AOP的基本原理,DCI 提出基于AOP 深层次的元编程(可以理解成面向接口编程), 去促使系统的内聚效果和降低耦合度;
|
||||
很好的点是 它根据 AOP 的基本原理,DCI 提出基于 AOP 深层次的元编程(可以理解成面向接口编程), 去促使系统的内聚效果和降低耦合度;
|
||||
|
||||
举个例子:
|
||||
在一个BI系统中, 在业务的发展中, 这个系统使用到了多套的 底层图表库,比如: Echarts, G2,Recharts, FusionChart; 等等;
|
||||
在一个 BI 系统中, 在业务的发展中, 这个系统使用到了多套的 底层图表库,比如: Echarts, G2,Recharts, FusionChart; 等等;
|
||||
|
||||
那么问题来了,
|
||||
1. 如何去同时支持 这些底层库, 并且达到很容易切换的一个效果?
|
||||
@@ -234,19 +234,19 @@ DCI 重点是关注 数据的不同场景的交互行为, 是面向对象系
|
||||
3. 如何去考虑扩展业务 对图表的日益增强的业务功能(如: 行列转换、智能格式化 等等)
|
||||
|
||||
带着这些问题, 我们再来看下 DCI 给我们的启示, 我们来试试看相应的解法:
|
||||
1. 图表的模型数据就是 数据Data , 我们可以把[日益增强的业务功能] 认为是各个场景交互Interactions;
|
||||
1. 图表的模型数据就是 数据 Data , 我们可以把[日益增强的业务功能] 认为是各个场景交互 Interactions;
|
||||
2. 接入更多类型的图表咋么搞?
|
||||
不同类型的图表其实是图表数据模型的转换,我们也可以把这些转换的行为过程作为一个个的切片(Aspect),每个切片都是独立的, 松耦合的 ;
|
||||

|
||||
|
||||
3. 接入多套底层库怎么搞? 每个图形库的 build方法,render 方法 , resize 方法,repaint 方法 都不一样 ,怎么搞 ? 我们可以使用 DCI 提到的元编程- 我们在这里理解为面向接口编程, 我们分装一层 统一的接口;
|
||||
3. 接入多套底层库怎么搞? 每个图形库的 build 方法,render 方法 , resize 方法,repaint 方法 都不一样 ,怎么搞 ? 我们可以使用 DCI 提到的元编程- 我们在这里理解为面向接口编程, 我们分装一层 统一的接口;
|
||||
利用面向接口的父类引用指向子类对象 我们就可以很方便的 接入更多的 implement 接入更多的图形库(当然,一个系统统一一套是最好的);
|
||||
|
||||
|
||||
|
||||
# 4 总结
|
||||
|
||||
DCI是数据Data 场景Context 交互Interactions的简称,DCI是一种特别关注行为的设计模式(行为模式),
|
||||
DCI 是数据 Data 场景 Context 交互 Interactions 的简称,DCI 是一种特别关注行为的设计模式(行为模式),
|
||||
DCI 关注数据不同场景的交互行为, 是面向对象 状态和行为的一种范式设计;DCI 尝试从人类思维,过程化设计一些行为;
|
||||
DCI 也会使用一些面向切面和接口编程的设计思想去达到高内聚低耦合的目标。
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
# 2 内容概要
|
||||
|
||||
### TC39是什么?包括哪些人?
|
||||
### TC39 是什么?包括哪些人?
|
||||
|
||||
一个推动 JavaScript 发展的委员会,由各个主流浏览器厂商的代表构成。
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
### TC39 这群人主要的工作是什么?
|
||||
|
||||
制定ECMAScript标准,标准生成的流程,并实现。
|
||||
制定 ECMAScript 标准,标准生成的流程,并实现。
|
||||
|
||||
### 标准的流程是什么样的?
|
||||
|
||||
@@ -26,34 +26,34 @@
|
||||
|
||||
- stage0 `strawman`
|
||||
|
||||
任何讨论、想法、改变或者还没加到提案的特性都在这个阶段。只有TC39成员可以提交。
|
||||
任何讨论、想法、改变或者还没加到提案的特性都在这个阶段。只有 TC39 成员可以提交。
|
||||
|
||||
- stage1 `proposal`
|
||||
(1)产出一个正式的提案。
|
||||
(2)发现潜在的问题,例如与其他特性的关系,实现难题。
|
||||
(3)提案包括详细的API描述,使用例子,以及关于相关的语义和算法。
|
||||
(3)提案包括详细的 API 描述,使用例子,以及关于相关的语义和算法。
|
||||
|
||||
- stage2 `draft`
|
||||
(1)提供一个初始的草案规范,与最终标准中包含的特性不会有太大差别。草案之后,原则上只接受增量修改。
|
||||
(2)开始实验如何实现,实现形式包括polyfill, 实现引擎(提供草案执行本地支持),或者编译转换(例如babel)
|
||||
(2)开始实验如何实现,实现形式包括 polyfill, 实现引擎(提供草案执行本地支持),或者编译转换(例如 babel)
|
||||
|
||||
- stage3 `candidate`
|
||||
(1)候选阶段,获得具体实现和用户的反馈。此后,只有在实现和使用过程中出现了重大问题才会修改。
|
||||
(1)规范文档必须是完整的,评审人和ECMAScript的编辑要在规范上签字。
|
||||
(2)至少要在一个浏览器中实现,提供polyfill或者babel插件。
|
||||
(1)规范文档必须是完整的,评审人和 ECMAScript 的编辑要在规范上签字。
|
||||
(2)至少要在一个浏览器中实现,提供 polyfill 或者 babel 插件。
|
||||
|
||||
- stage4 `finished`
|
||||
(1)已经准备就绪,该特性会出现在下个版本的ECMAScript规范之中。。
|
||||
(2)需要通过有2个独立的实现并通过验收测试,以获取使用过程中的重要实践经验。
|
||||
(1)已经准备就绪,该特性会出现在下个版本的 ECMAScript 规范之中。。
|
||||
(2)需要通过有 2 个独立的实现并通过验收测试,以获取使用过程中的重要实践经验。
|
||||
|
||||
### 一般可以去哪里查看TC39标准的进程呢?
|
||||
### 一般可以去哪里查看 TC39 标准的进程呢?
|
||||
|
||||
stage0 的提案 https://github.com/tc39/proposals/blob/master/stage-0-proposals.md
|
||||
stage1 - 4 的提案 https://github.com/tc39/proposals
|
||||
|
||||
### 我们怎么在程序中应用这些新特性呢?
|
||||
|
||||
babel的插件:`babel-presets-stage-0` `babel-presets-stage-1` `babel-presets-stage-2` `babel-presets-stage-3` `babel-presets-stage-4`
|
||||
babel 的插件:`babel-presets-stage-0` `babel-presets-stage-1` `babel-presets-stage-2` `babel-presets-stage-3` `babel-presets-stage-4`
|
||||
|
||||
# 3 精读
|
||||
|
||||
@@ -617,7 +617,7 @@ const firstName = message.body?.user?.firstName || 'default'
|
||||
|
||||
```javascript
|
||||
const firstName =
|
||||
()message &&
|
||||
(message &&
|
||||
message.body &&
|
||||
message.body.user &&
|
||||
message.body.user.firstName) || 'default'
|
||||
@@ -627,7 +627,7 @@ const firstName =
|
||||
|
||||
### [Math.signbit: IEEE-754 sign bit](http://jfbastien.github.io/papers/Math.signbit.html)
|
||||
|
||||
当值为 负数 或 -0 时返回 `true`。由于 `Math.sign` 不区分 +0 与 -0,因此提案建议增加此函数,而且此函数在 c、c++、go语言都有实现。
|
||||
当值为 负数 或 -0 时返回 `true`。由于 `Math.sign` 不区分 +0 与 -0,因此提案建议增加此函数,而且此函数在 c、c++、go 语言都有实现。
|
||||
|
||||
### [Error stacks](https://github.com/tc39/proposal-error-stacks)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 1. 摘要
|
||||
|
||||
日期选择器作为基础组件重要不可或缺的一员,大家已经快习惯它一成不变的样子,输入框+日期选择弹出层。但到业务中,这种墨守成规的样子真的能百分百契合业务需求吗。这篇文章从多个网站的日期选择场景出发,企图归纳出日期选择器的最佳实践。这篇文章对移动端的日期选择暂无涉猎,都是PC端,列举出通用场景,每个类型日期选择器需要考虑的设计。
|
||||
日期选择器作为基础组件重要不可或缺的一员,大家已经快习惯它一成不变的样子,输入框+日期选择弹出层。但到业务中,这种墨守成规的样子真的能百分百契合业务需求吗。这篇文章从多个网站的日期选择场景出发,企图归纳出日期选择器的最佳实践。这篇文章对移动端的日期选择暂无涉猎,都是 PC 端,列举出通用场景,每个类型日期选择器需要考虑的设计。
|
||||
文章链接:Designing The Perfect Date And Time Picker
|
||||
感谢本期评论官 @黄子毅 @流形 @王亮 @赵阳 @不知名的花瓣工程师
|
||||
# 2. 设计原则
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
2)用户自定义输入如何保证日期格式正确性?
|
||||
|
||||
3)是否需要提供预设场景输入? 比如昨天,三天前,七天前,30天前?像很多数据分析场景,分析师会关注数据周期,比如流量的周环比,月环比,年环比。
|
||||
3)是否需要提供预设场景输入? 比如昨天,三天前,七天前,30 天前?像很多数据分析场景,分析师会关注数据周期,比如流量的周环比,月环比,年环比。
|
||||
|
||||
4)是否需要包含默认值?如果有默认,应该是什么?像google flight 根据用户历史数据提供默认值,临近节假日默认填充节假日。同时像有些数据场景,数据存在延迟,需要默认提供T-1/T-2 ,避免用户选择当天。
|
||||
4)是否需要包含默认值?如果有默认,应该是什么?像 google flight 根据用户历史数据提供默认值,临近节假日默认填充节假日。同时像有些数据场景,数据存在延迟,需要默认提供 T-1/T-2 ,避免用户选择当天。
|
||||
|
||||
5)当用户激活输入框时,是否保留默认值?
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
2)用户选中后是否立刻做背景色提示?
|
||||
|
||||
3)当用户选择时,区间是否需要随着用户动作改变?比如用户hover时,动态改变选中区间。
|
||||
3)当用户选择时,区间是否需要随着用户动作改变?比如用户 hover 时,动态改变选中区间。
|
||||
|
||||
4)是否提供快捷键切换 日、月、年选择?
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||

|
||||
|
||||
采用与用户交互的方式选择日期,如果今后应用上AI,单纯的日期选择器是不是会消失不见呢?..
|
||||
采用与用户交互的方式选择日期,如果今后应用上 AI,单纯的日期选择器是不是会消失不见呢?..
|
||||
## 3.5 特殊标识周末
|
||||

|
||||
在机票、旅行场景中,周末是大家最有可能出行的时间点,采用竖线划分的方式着重标注提醒。
|
||||
@@ -106,6 +106,6 @@
|
||||
|
||||

|
||||
|
||||
总得来说,日期选择器是一个业务组件,虽然现有很多组件库把它纳入UI基础组件。但在每个不通的业务场景和需求下的展现形式、交互都会有所有不同。首先一定一定要明确确定需要日期选择器的场景,尤其是与日期强关联的业务,比如机票定价、日程安排,结合到日期选择器中更直观,提高用户对信息的检索效率。满足用户需求场景的同时,尽量减少用户操作链路。
|
||||
总得来说,日期选择器是一个业务组件,虽然现有很多组件库把它纳入 UI 基础组件。但在每个不通的业务场景和需求下的展现形式、交互都会有所有不同。首先一定一定要明确确定需要日期选择器的场景,尤其是与日期强关联的业务,比如机票定价、日程安排,结合到日期选择器中更直观,提高用户对信息的检索效率。满足用户需求场景的同时,尽量减少用户操作链路。
|
||||
|
||||
看到最后点个赞呗,给你比小心心 ❤ ~~
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
正如上面所说,我推荐以开放性问题开场,这样便于了解候选人的经历、熟悉哪些技术点,便于后面的技术提问。如果开场就以准备好的题目展开车轮战,容易引起候选人心里紧张,同时我们问的问题不一定是候选人所在行的,技术问题不是每一个都那么重要,很多时候我们只看到了候选人的冰山一角,但此时气氛已经尴尬,很多时候会遗漏优秀人才。
|
||||
|
||||
开放性问题最好基于行为面试法询问(Star法则):
|
||||
开放性问题最好基于行为面试法询问(Star 法则):
|
||||
|
||||
- Situation: 场景 - 当时是怎样的场景
|
||||
- Task: 任务 - 当时的任务是什么
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
面试主要是看候选人基础有多扎实,和思维能力。基础主要指的是,候选人提前了解了多少前端相关知识,比如对闭包的理解,对原生 api 的理解?如果候选人没接触过这两个知识点,会有两种情况:
|
||||
|
||||
- **这些知识点看完需要多久?如果是闭包和原生api的定义与用法,候选人这方面的缺陷可以通过5分钟来弥补,那么这种问题到底想考什么?我们真的在乎这5分钟看文档的时间吗?此时应该了解候选人对知识点的感悟,或者学习方式,因为这两点的差距可能几年都无法弥补**
|
||||
- **这些知识点看完需要多久?如果是闭包和原生 api 的定义与用法,候选人这方面的缺陷可以通过 5 分钟来弥补,那么这种问题到底想考什么?我们真的在乎这 5 分钟看文档的时间吗?此时应该了解候选人对知识点的感悟,或者学习方式,因为这两点的差距可能几年都无法弥补**
|
||||
- **如果候选人学习能力非常强,但几乎所有前端知识点都不了解,弥补完大概一共要花 1000*5 分钟,这时候量变引发质变了,是不是说明候选人本身对技术的热情存在问题?**
|
||||
|
||||
通过了基础问题还远远不够。甚至当问一个复杂的问题的时候,如果候选人瞬间把答案完美流畅表达出来,说明这个问题基本上白问了。
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
最后考察候选人的发展潜力与工作态度,我们一般通过询问简单的算法问题,进一步了解候选人是否对技术真正感兴趣,而不只是对前端工程感兴趣。同时,算法问题也考察候选人解决抽象问题的能力,或者让候选人设计一个组件,通过对组件需求的不断升级,考察候选人是否能及时给出解决方案。
|
||||
|
||||
最后时工作态度,首先会考察人品,对不懂的知识点装懂是违背诚信的行为,任何团队都不会要的。同时,**不正视自己技术存在的盲点,将是技术发展的最大阻碍**。不过这里也不怕被候选人套路,如果全部都回答不懂那也不用考虑了。
|
||||
最后是工作态度,首先会考察人品,对不懂的知识点装懂是违背诚信的行为,任何团队都不会要的。同时,**不正视自己技术存在的盲点,将是技术发展的最大阻碍**。不过这里也不怕被候选人套路,如果全部都回答不懂那也不用考虑了。
|
||||
|
||||
# 3 总结
|
||||
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
# 精读《Web fonts: when you need them, when you don’t》
|
||||
本期精读让我们来聊一聊Web Fonts,文章地址:[https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae](https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae)
|
||||
本期精读让我们来聊一聊 Web Fonts,文章地址:[https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae](https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae)
|
||||
|
||||
## 文章简介
|
||||
文章分析了Web Fonts的优劣具体使用场景。
|
||||
文章分析了 Web Fonts 的优劣具体使用场景。
|
||||
|
||||
## 主要观点
|
||||
- 作者用一张流程图非常言简意赅地概括了文章的上半部分。
|
||||

|
||||
- 当然上半部分作者也讲了很多案例,其中一个很明显的案例就是维基百科利用字体来提升阅读体验,通过文章内的对比,能直观感受到这一点
|
||||
- 文章后半部分着力介绍了怎么解决Web Font的带来的弊端:认识FOUT带来的问题,如何使用现有的前端解决方案来尽可能避免这个问题,以及样式上优雅降级的几个方案。
|
||||
- 文章后半部分着力介绍了怎么解决 Web Font 的带来的弊端:认识 FOUT 带来的问题,如何使用现有的前端解决方案来尽可能避免这个问题,以及样式上优雅降级的几个方案。
|
||||
|
||||
## 把文章带入自己的开发环境
|
||||
作为一个中文开发者,在我们的开发技术栈中,Web Fonts绝对是属于使用频率比较低的那一类的。本次精读选择这篇文章,也正是一探这一个不常见的领域。
|
||||
作为一个中文开发者,在我们的开发技术栈中,Web Fonts 绝对是属于使用频率比较低的那一类的。本次精读选择这篇文章,也正是一探这一个不常见的领域。
|
||||
|
||||
对于英文字母,26个字母可以解决大部分的问题,算上大小写和基本符号,一张ASCII码标就可以包含住。让我再扩展一下,到大部分的西文书写系统,几百个字符就能解决多语言显示的问题了。但是对于汉语而言,Web Fonts真的是,想说爱你不容易,因为常用的汉字就有几千个(你想象中国还有《千字文》这种儿童读物……)。字体这东西跟字符数量直接挂钩,是很难通过压缩来获得性能提升的。
|
||||
对于英文字母,26 个字母可以解决大部分的问题,算上大小写和基本符号,一张 ASCII 码标就可以包含住。让我再扩展一下,到大部分的西文书写系统,几百个字符就能解决多语言显示的问题了。但是对于汉语而言,Web Fonts 真的是,想说爱你不容易,因为常用的汉字就有几千个(你想象中国还有《千字文》这种儿童读物……)。字体这东西跟字符数量直接挂钩,是很难通过压缩来获得性能提升的。
|
||||
|
||||
通常的想法就是用多少,取多少,但是这个方法也就只能适用于标题美化等场景。对于一个系统性的前端工程,我们不可能去实现一个动态字符的字体文件(就是统计这个页面上会产生多少个字符,为这个字符集去生成一个字符子集)
|
||||
|
||||
虽然汉字书写系统和西文书写系统天生存在差异,但是把作者在文章中提出来的几个问题再站在中文的角度上再来看一下,也可以得到一个比较客观的答案。以下是我作为一个普通开发者的自问自答:
|
||||
|
||||
1. **字体对你的品牌很关键吗?**(需要特性字体的中文LOGO基本都用PNG和SVG解决了,Web Font不实用。)
|
||||
2. **字体让你的文字阅读起来更容易了吗?**(我平时开发产品没有成片聚集的文字,用无衬线字体就能满足需求。Web Font很好,我选择“微软雅黑”。)(注:泛指那些好用的支持全字集的系统自带字体;成片的文字适用衬线字体,个人认为中文的衬线字体,不同的字体带来的阅读体验还是有明显差别的。)
|
||||
3. **你需要在不同设备上显示一样的字体吗?**(好像还没这么苛求吧……微软雅黑好看,安卓上的Roboto也很不错啊,Roboto这种字体还针对移动设备有优化,何乐而不为。)
|
||||
4. **用了Web Font你会更开心吗?**(在icon中使用iconfont让我们告别了PNG Sprite图,嗯这很开心。至于文字上用Web Font,有好用的系统字体你不用,你这是何苦呢)(作者也说了,可能折腾半天还没系统字体看着舒服,那就是一行font-family的事情)
|
||||
1. **字体对你的品牌很关键吗?**(需要特性字体的中文 LOGO 基本都用 PNG 和 SVG 解决了,Web Font 不实用。)
|
||||
2. **字体让你的文字阅读起来更容易了吗?**(我平时开发产品没有成片聚集的文字,用无衬线字体就能满足需求。Web Font 很好,我选择“微软雅黑”。)(注:泛指那些好用的支持全字集的系统自带字体;成片的文字适用衬线字体,个人认为中文的衬线字体,不同的字体带来的阅读体验还是有明显差别的。)
|
||||
3. **你需要在不同设备上显示一样的字体吗?**(好像还没这么苛求吧……微软雅黑好看,安卓上的 Roboto 也很不错啊,Roboto 这种字体还针对移动设备有优化,何乐而不为。)
|
||||
4. **用了 Web Font 你会更开心吗?**(在 icon 中使用 iconfont 让我们告别了 PNG Sprite 图,嗯这很开心。至于文字上用 Web Font,有好用的系统字体你不用,你这是何苦呢)(作者也说了,可能折腾半天还没系统字体看着舒服,那就是一行 font-family 的事情)
|
||||
|
||||
不同产品有着不同的场景,多像文章里问问自己会有最合适的答案。
|
||||
|
||||
## 关于FOUT和FOIT
|
||||
文章中大篇幅地在安利你使用Web Font,但是也很直白地指出了Web Font最大的问题,就是这个FOUT——Flash Of Unstyled Text。连作者毫不避讳地说了句:“噢我的老天,这太丑了!”
|
||||
## 关于 FOUT 和 FOIT
|
||||
文章中大篇幅地在安利你使用 Web Font,但是也很直白地指出了 Web Font 最大的问题,就是这个 FOUT——Flash Of Unstyled Text。连作者毫不避讳地说了句:“噢我的老天,这太丑了!”
|
||||
|
||||
具体表现是采用了Web Font的文案会存在闪动,这个的根本原因在于相比于系统字体,Web Font最大的弊端在于它是异步加载的,你没有办法避免下载它所用的时间。文章中举了一个例子,在一个图文为主的页面中,一个542KB的字体文件,在第9秒才加载完成。在那之前只能以系统字体来展示,而在第9秒加载完成的时候,还会出现替换字体的情况,文字会突然跳动。
|
||||
具体表现是采用了 Web Font 的文案会存在闪动,这个的根本原因在于相比于系统字体,Web Font 最大的弊端在于它是异步加载的,你没有办法避免下载它所用的时间。文章中举了一个例子,在一个图文为主的页面中,一个 542KB 的字体文件,在第 9 秒才加载完成。在那之前只能以系统字体来展示,而在第 9 秒加载完成的时候,还会出现替换字体的情况,文字会突然跳动。
|
||||
|
||||
比FOUT更为极端的情况的是FOIT——Flash Of Invisible Text。很多浏览器的行为,并不是默认展示系统字体,而是直接隐藏。那么即使在极快的网速下也很难避免存在一个几百毫秒的时间滞后。
|
||||
比 FOUT 更为极端的情况的是 FOIT——Flash Of Invisible Text。很多浏览器的行为,并不是默认展示系统字体,而是直接隐藏。那么即使在极快的网速下也很难避免存在一个几百毫秒的时间滞后。
|
||||
|
||||
不过好在,有一个font-display的属性,可以在声明@font-face的时候配合使用。对于未加载Web Fonts的时候,auto属性可以选择隐藏也就是会产生FOIT,swap会产生替换也就是会产生FOUT,还有fallback和optional可以控制先FOIT后FOUT来达到折中方案。
|
||||
不过好在,有一个 font-display 的属性,可以在声明@font-face 的时候配合使用。对于未加载 Web Fonts 的时候,auto 属性可以选择隐藏也就是会产生 FOIT,swap 会产生替换也就是会产生 FOUT,还有 fallback 和 optional 可以控制先 FOIT 后 FOUT 来达到折中方案。
|
||||
|
||||
还有一个思路,那就是预加载,对于字体,浏览器还是能够有效缓存的,如果能够做好预加载,还是不会太影响用户体验的。文章中就提到了一个方案,调用link的rel=preload来做预加载。因为通常加载字体是在CSS中的@font-face被读到的时候才去加载的,那么就会出现先加载CSS,后加载字体的情况。如果利用link预加载,那么在CSS中的@font-face被读到前就已经开始加载了,那么字体加载和CSS加载就可以同时加载,提升速度。
|
||||
还有一个思路,那就是预加载,对于字体,浏览器还是能够有效缓存的,如果能够做好预加载,还是不会太影响用户体验的。文章中就提到了一个方案,调用 link 的 rel=preload 来做预加载。因为通常加载字体是在 CSS 中的@font-face 被读到的时候才去加载的,那么就会出现先加载 CSS,后加载字体的情况。如果利用 link 预加载,那么在 CSS 中的@font-face 被读到前就已经开始加载了,那么字体加载和 CSS 加载就可以同时加载,提升速度。
|
||||
|
||||
当然JS是万能的,也有一些库在支持这方面功能,例如bramstein/fontfaceobserver这样的。
|
||||
当然 JS 是万能的,也有一些库在支持这方面功能,例如 bramstein/fontfaceobserver 这样的。
|
||||
|
||||
愚以为,FO*T这种情况既然无法避免还是要具体情况具体分析的。如果你的用户网速够快,那么隐藏文字会更好,用户无感知;如果网速不确定,而且是文章为主的内容,那么内容至上就应该先用替代字体显示;如果你正在将Web Font应用在图标等东西上,那么我们自然不愿意看到满屏的方框方框,这种时候就选择隐藏吧。
|
||||
愚以为,FO*T 这种情况既然无法避免还是要具体情况具体分析的。如果你的用户网速够快,那么隐藏文字会更好,用户无感知;如果网速不确定,而且是文章为主的内容,那么内容至上就应该先用替代字体显示;如果你正在将 Web Font 应用在图标等东西上,那么我们自然不愿意看到满屏的方框方框,这种时候就选择隐藏吧。
|
||||
|
||||
文章也提了一点,如果你的字体授权很贵,但用户端深受FO*T折磨,那你还费这钱干嘛。
|
||||
文章也提了一点,如果你的字体授权很贵,但用户端深受 FO*T 折磨,那你还费这钱干嘛。
|
||||
|
||||
## 结论
|
||||
如果能解决FO*T的副作用,Web Font怎么舒服怎么用。但是中文字体大,常用西文字体诸如Google字体库又时常被墙,对于中国开发者,Web Font想说爱你不容易。(还是乖乖用微软雅黑吧,逃……
|
||||
如果能解决 FO*T 的副作用,Web Font 怎么舒服怎么用。但是中文字体大,常用西文字体诸如 Google 字体库又时常被墙,对于中国开发者,Web Font 想说爱你不容易。(还是乖乖用微软雅黑吧,逃……
|
||||
|
||||
## 彩蛋
|
||||
文章里有一段很精彩的话,摘抄出来翻译一下:
|
||||
|
||||
> 如果这个世界上有这样一个Sketch或者Photoshop的插件,可以在你每次打开一个文件的时候,延迟十秒才显示出字体;那么世界上就没有那么多多余的字体了。
|
||||
> 如果这个世界上有这样一个 Sketch 或者 Photoshop 的插件,可以在你每次打开一个文件的时候,延迟十秒才显示出字体;那么世界上就没有那么多多余的字体了。
|
||||
>
|
||||
> (译者注:删光设计师的电脑里的奇怪字体也能达到相同目的,哈哈哈~)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<img src="assets/22/v8.png" width="500" alt="logo" />
|
||||
|
||||
定时刷新一下对 js 的三观,防止经验变成坑。(对IE等浏览器的三观需保持不变)
|
||||
定时刷新一下对 js 的三观,防止经验变成坑。(对 IE 等浏览器的三观需保持不变)
|
||||
|
||||
# 2 内容概要
|
||||
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
本期精读的文章是:[API设计原则](https://coolshell.cn/articles/18024.html)
|
||||
本期精读的文章是:[API 设计原则](https://coolshell.cn/articles/18024.html)
|
||||
|
||||
# 1 引言
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
由于本文已经是翻译后的文章,概要只列出不涉及 c++ 概念的思路框架,细节请移步[译文](https://coolshell.cn/articles/18024.html)。
|
||||
|
||||
## 好 API 的6个特质
|
||||
## 好 API 的 6 个特质
|
||||
|
||||
极简且完备、语义清晰简单、符合直觉、易于记忆和引导API使用者写出可读代码。
|
||||
极简且完备、语义清晰简单、符合直觉、易于记忆和引导 API 使用者写出可读代码。
|
||||
|
||||
## 静态多态
|
||||
|
||||
@@ -76,7 +76,7 @@ function (const num) {
|
||||
|
||||
## 统一关键字库
|
||||
|
||||
所有api定义之前,先抽离业务和功能语义的关键字,统一关键字库; 可以更好的让多人协作看起来如出一辙, 而且关键字库 更能够让调用者感觉到 符合直觉、语义清晰; 关键字库也是项目组新同学 PREDO 的内容之一, 很有带入感;
|
||||
所有 api 定义之前,先抽离业务和功能语义的关键字,统一关键字库; 可以更好的让多人协作看起来如出一辙, 而且关键字库 更能够让调用者感觉到 符合直觉、语义清晰; 关键字库也是项目组新同学 PREDO 的内容之一, 很有带入感;
|
||||
|
||||
## 单一职责
|
||||
|
||||
@@ -119,6 +119,6 @@ const { setVisible } = this.props.store.article
|
||||
|
||||
最后,如果有精力,最好每半年重构一次(然后完整跑一遍测试)!
|
||||
|
||||
> 讨论地址是:[精读《API设计原则》 · Issue #34 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/34)
|
||||
> 讨论地址是:[精读《API 设计原则》 · Issue #34 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/34)
|
||||
|
||||
> 如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。
|
||||
|
||||
+22
-22
@@ -9,14 +9,14 @@
|
||||
|
||||
我为什么要选这篇文章呢?
|
||||
|
||||
之所以选这篇文章, 是因为非常认同作者写这两篇文章的原因. 作者在文中说, 现代JavaScript 的很多概念和思想在快速被传播和扩展, 很多新概念出现在前端相关的博客和文档中, 这些概念对于很多前端开发人员来说, 仍然很陌生. 因此我们有必要来学习一下现代的这些 JavaScript的概念, 看这些概念在现在 JavaScript 的库或应用中是怎么被使用的.
|
||||
之所以选这篇文章, 是因为非常认同作者写这两篇文章的原因. 作者在文中说, 现代 JavaScript 的很多概念和思想在快速被传播和扩展, 很多新概念出现在前端相关的博客和文档中, 这些概念对于很多前端开发人员来说, 仍然很陌生. 因此我们有必要来学习一下现代的这些 JavaScript 的概念, 看这些概念在现在 JavaScript 的库或应用中是怎么被使用的.
|
||||
|
||||
# 2 内容概要
|
||||
|
||||
文章讲了很多现代JavaScript中的概念, 罗列如下:
|
||||
文章讲了很多现代 JavaScript 中的概念, 罗列如下:
|
||||
|
||||
## 纯函数和副作用
|
||||
在了解纯函数之前, 首先要了解副作用. 副作用是指改变了其作用域外的状态. 副作用的举例有调用了一个 API, 操作了一个 DOM节点, 弹出了一个弹窗, 或者改变了一条数据等.
|
||||
在了解纯函数之前, 首先要了解副作用. 副作用是指改变了其作用域外的状态. 副作用的举例有调用了一个 API, 操作了一个 DOM 节点, 弹出了一个弹窗, 或者改变了一条数据等.
|
||||
|
||||
而纯函数则是指 函数的返回值仅仅由参数决定, 当给同样的参数时, 返回值是固定的.
|
||||
|
||||
@@ -27,7 +27,7 @@ Stateless 无状态, 有点像纯函数, 不管理自己的数据或状态, 结
|
||||
|
||||
可变对象与不可变对象概念很清楚, 可变对象指的是在创建后值仍可以被改变, 不可变对象指的是创建后值无法被改变.
|
||||
|
||||
相比于其他语言, 可变对象与不可变对象在 JavaScript 中更加模糊, 当你了解函数式编程时, 你会听到很多不可变对象的好处. 在 JavaScript 中, 你可以通过Object.freeze(obj), 让一个对象变得不可变, 但是注意这是浅层的冻结对象, 如果有一个属性的值是个对象, 那这个对象中的属性是可以被修改的. 现在 JavaScript 也出现了 npm deep-freeze , Immutable.js 这些库来帮助你在 JavaScript 中实现不可变对象.
|
||||
相比于其他语言, 可变对象与不可变对象在 JavaScript 中更加模糊, 当你了解函数式编程时, 你会听到很多不可变对象的好处. 在 JavaScript 中, 你可以通过 Object.freeze(obj), 让一个对象变得不可变, 但是注意这是浅层的冻结对象, 如果有一个属性的值是个对象, 那这个对象中的属性是可以被修改的. 现在 JavaScript 也出现了 npm deep-freeze , Immutable.js 这些库来帮助你在 JavaScript 中实现不可变对象.
|
||||
|
||||
## Imperative and Declarative Programming(命令式和声明式编程)
|
||||
命令式编程, 描述一段代码的逻辑怎么被显式调用去改变程序的状态. 声明式编程, 描述一段代码的逻辑, 而不需要描述如何完成这段逻辑.
|
||||
@@ -46,9 +46,9 @@ JavaScript 可以同时被写为命令式和声明式编程方式, 但是随着
|
||||
- 声明式代码去管理副作用和执行命令式编程
|
||||
|
||||
## Hot and Cold Observables
|
||||
Observables 和数组类似, 只不过数组是被保存在内存中, 而Observables的每一个元素则是异步加入进来. 我们可以订阅这些 observables.
|
||||
Observables 和数组类似, 只不过数组是被保存在内存中, 而 Observables 的每一个元素则是异步加入进来. 我们可以订阅这些 observables.
|
||||
|
||||
Hot Observables 容易会被执行, 即使我们没有订阅它们. 比如说 用户的操作界面的 按钮点击事件, 鼠标移动, 窗口大小改变, 这些都是 Hot Observables. 而cold observable则是需要我们去订阅, 并且会在我们订阅的时候开始执行.
|
||||
Hot Observables 容易会被执行, 即使我们没有订阅它们. 比如说 用户的操作界面的 按钮点击事件, 鼠标移动, 窗口大小改变, 这些都是 Hot Observables. 而 cold observable 则是需要我们去订阅, 并且会在我们订阅的时候开始执行.
|
||||
|
||||
## 响应式编程 RP
|
||||
响应式编程, 可以看作是面向异步事件流的编程, 声明式的, 表述去做什么, 而不是怎么做.
|
||||
@@ -69,18 +69,18 @@ Hot Observables 容易会被执行, 即使我们没有订阅它们. 比如说
|
||||
|
||||
随着现在各种 SPA 框架的兴起, 理解数据流概念, 对于现在 JS 开发者越来越重要, React 被认为是单向数据流的典范, 使用 Model 作为唯一的数据来源, 控制 View 的渲染. 在 View 层用事件的方式通知 Model 更新, 在反应到 View 层的变化上. 数据沿着一个方向流动, UI 永远不会更新 Model, 而是通过事件或者 setState 方法.
|
||||
|
||||
在双向数据绑定中, 数据是在两个方向上流动的, JS可以更新 Model 数据, View 层 也可以更新 Model 数据. AngularJs 的1.x 版本是双向数据流的典型实现.
|
||||
在双向数据绑定中, 数据是在两个方向上流动的, JS 可以更新 Model 数据, View 层 也可以更新 Model 数据. AngularJs 的 1.x 版本是双向数据流的典型实现.
|
||||
|
||||
早在2009年, 双向绑定是 Angualr 最受欢迎的特性之一, 但是 Angular 把这一特性抛弃了. 现在很多流行的框架和库都使用了单向数据流(React,Angular,Inferno,Redux等). 单向数据流倡导的是清晰的架构, 数据流动更加清晰和易管理. 对于单向数据流来说说了点View自动更新数据的便利, 但也得到了清晰的数据流.
|
||||
早在 2009 年, 双向绑定是 Angualr 最受欢迎的特性之一, 但是 Angular 把这一特性抛弃了. 现在很多流行的框架和库都使用了单向数据流(React,Angular,Inferno,Redux 等). 单向数据流倡导的是清晰的架构, 数据流动更加清晰和易管理. 对于单向数据流来说说了点 View 自动更新数据的便利, 但也得到了清晰的数据流.
|
||||
|
||||
## JS框架中的变化侦测: 脏检查, getter 和 setter, 虚拟 DOM
|
||||
变化侦测对于现代 SPA应用来说很重要. 当用户更新一些内容时, 应用必须以一种方法知道这种变化, 并做出反应更新.
|
||||
## JS 框架中的变化侦测: 脏检查, getter 和 setter, 虚拟 DOM
|
||||
变化侦测对于现代 SPA 应用来说很重要. 当用户更新一些内容时, 应用必须以一种方法知道这种变化, 并做出反应更新.
|
||||
|
||||
AngularJS 1.x 使用的是脏检查的方式, 具体做法是对View 中涉及到的 Model 进行深度比较. 脏检查的优点在于它的简单和可预测, 不涉及到 API 和对象的变更. 但是正因为涉及到大量比较, 也很低效.
|
||||
AngularJS 1.x 使用的是脏检查的方式, 具体做法是对 View 中涉及到的 Model 进行深度比较. 脏检查的优点在于它的简单和可预测, 不涉及到 API 和对象的变更. 但是正因为涉及到大量比较, 也很低效.
|
||||
|
||||
Ember 和 Backbone 是使用getters 和 setters 来做变化侦测, 这样涉及到数据修改时, 都会触发变更事件. 而React 是使用了虚拟 Dom 来做变化侦测, React 通过 setState方法来通知变更, 使用虚拟 Dom 来比较是否发生了数据变化.
|
||||
Ember 和 Backbone 是使用 getters 和 setters 来做变化侦测, 这样涉及到数据修改时, 都会触发变更事件. 而 React 是使用了虚拟 Dom 来做变化侦测, React 通过 setState 方法来通知变更, 使用虚拟 Dom 来比较是否发生了数据变化.
|
||||
|
||||
## Web Components组件
|
||||
## Web Components 组件
|
||||
Web 组件是 Web 平台上可复用的基础组件, 而 Web Components 则定义了一些规范来实现这些可复用组件.
|
||||
规范包括:
|
||||
- 自定义元素
|
||||
@@ -88,20 +88,20 @@ Web 组件是 Web 平台上可复用的基础组件, 而 Web Components 则定
|
||||
- Shadow Dom
|
||||
- HTML imports 引入
|
||||
|
||||
Web Components本身并不能代替 SPA框架的功能, 但是它的想法和核心概念, 在很多 SPA 框架中都有体现.
|
||||
Web Components 本身并不能代替 SPA 框架的功能, 但是它的想法和核心概念, 在很多 SPA 框架中都有体现.
|
||||
|
||||
## Smart 和 Dumb 组件
|
||||
现在Web 的开发严重依赖组件, 而很多时候我们把组件分成 Smart 组件和 Dumb 组件.
|
||||
现在 Web 的开发严重依赖组件, 而很多时候我们把组件分成 Smart 组件和 Dumb 组件.
|
||||
|
||||
Smart 组件, 又叫容器组件, 在组件内处理各种业务逻辑, 通常也管理 Dumb 组件,响应 Dumb 组件的事件.
|
||||
|
||||
Dumb 组件, 又叫展示组件, 通常被写成纯函数, 依赖于外部的数据和方法, 专注于展现数据.
|
||||
|
||||
## JIT 编译
|
||||
Just-In-time(JIT)编译指的是代码的运行时, 被编译成机器代码的过程. 在JavaScript 运行时, JIT 能够找到代码的特定模式, 而这些模式可以让 JavaScript 更快的被执行.
|
||||
Just-In-time(JIT)编译指的是代码的运行时, 被编译成机器代码的过程. 在 JavaScript 运行时, JIT 能够找到代码的特定模式, 而这些模式可以让 JavaScript 更快的被执行.
|
||||
|
||||
## AOT 编译
|
||||
Ahead-Of-Time(AOT), 指的是编写的代码在运行之前, 被翻译成机器代码的过程. AOT给 tree shaking 带来了可能, 使用AOT 预编译, 对于生产环境下的代码有以下好处:
|
||||
Ahead-Of-Time(AOT), 指的是编写的代码在运行之前, 被翻译成机器代码的过程. AOT 给 tree shaking 带来了可能, 使用 AOT 预编译, 对于生产环境下的代码有以下好处:
|
||||
|
||||
- 更少的异步请求, 模板和样式内联在 JS 内
|
||||
- 更小的体积
|
||||
@@ -111,24 +111,24 @@ Ahead-Of-Time(AOT), 指的是编写的代码在运行之前, 被翻译成机器
|
||||
## Tree Shaking
|
||||
Tree Shaking 是指打包 JS 模块时, 通过对代码的静态分析, 排除掉不用的代码的机制.
|
||||
|
||||
Tree Shaking 技术建立在 ES2015模块的, import和 export上, 支持我们导入特定的内容,而不是整个库.
|
||||
Tree Shaking 技术建立在 ES2015 模块的, import 和 export 上, 支持我们导入特定的内容,而不是整个库.
|
||||
|
||||
```
|
||||
```plain
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
```
|
||||
|
||||
这样我们只导入了 BehaviorSubject, 而没有导入整个 Rxjs 库.
|
||||
# 3 精读
|
||||
文中讲到的现代 JavaScript 已经很多了, 再对理解的现代JavaScript补充几条:
|
||||
文中讲到的现代 JavaScript 已经很多了, 再对理解的现代 JavaScript 补充几条:
|
||||
|
||||
## Dependent injection(依赖注入)
|
||||
|
||||
通过控制反转,父级不需要关心子实现细节,将子类可能用到的实例都初始化好,由子类决定引入哪些依赖。还有一个好处是维持了单实例,这一点在数据流中尤为重要,如果 store 不是单例的,那数据流必然乱了套,既希望传给子类使用,又要维持单例,依赖注入是很好的解决方案。
|
||||
|
||||
## Symbol Reflect Proxy
|
||||
Symbol 是 ES6中加入的一种新的数据类型, 每一个 Symbol 都是独一无二的, 不与其它 Symbol 重复.
|
||||
Symbol 是 ES6 中加入的一种新的数据类型, 每一个 Symbol 都是独一无二的, 不与其它 Symbol 重复.
|
||||
|
||||
ES6中的 Proxy , 则是通过 Proxy 方法, 实现对于对象的一层拦截. 提供一种机制, 代理对象的操作. 而 Reflect 是一个内置的对象,它提供可拦截 JavaScript 操作的方法。方法与代理处理程序的方法相同。
|
||||
ES6 中的 Proxy , 则是通过 Proxy 方法, 实现对于对象的一层拦截. 提供一种机制, 代理对象的操作. 而 Reflect 是一个内置的对象,它提供可拦截 JavaScript 操作的方法。方法与代理处理程序的方法相同。
|
||||
|
||||
这三篇文章非常详细介绍了这三位 API:[symbol](https://www.keithcirkel.co.uk/metaprogramming-in-es6-symbols/) [reflect](https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-2-reflect/) [proxy](https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-3-proxies/)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# 精读加密媒体扩展(Encrypted Media Extensions,EME)
|
||||
|
||||
本期精读的文章是: [W3C发布加密媒体扩展(Encrypted Media Extensions,EME)正式推荐标准](http://www.chinaw3c.org/2017-09-pressrelease-eme-recommendation.html)
|
||||
本期精读的文章是: [W3C 发布加密媒体扩展(Encrypted Media Extensions,EME)正式推荐标准](http://www.chinaw3c.org/2017-09-pressrelease-eme-recommendation.html)
|
||||
|
||||
感谢 [xekri](https://github.com/xekri) 提供 hacker news 上的热门讨论帖:[https://news.ycombinator.com/item?id=15278883](https://news.ycombinator.com/item?id=15278883)
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
>
|
||||
> —— 摘自《[HTML5 DRM 正式成为 Web 标准,EFF 辞职抗议](http://www.solidot.org/comments?sid=53884&op=reply&type=story)》
|
||||
|
||||
以上,是我17年9月19日晚收到的一条推送消息。我当时在写《[关于 React 系前端技术的思考](https://tingge.github.io/html/react-redux-reactrouter.html)》,可是它让我意识到,该关注下 <video /> 背后的故事了。
|
||||
17年下半年发生了两件有趣的撕 X 事件:Facebook 将部分开源项目的防专利流氓证书 “BSD + Pattern” 重新授权为 “MIT” 和 W3C 发布 HTML5 版权保护的 EME 推荐标准。一时,似乎著作权、版权和开源、分享,甚至普世、网络中立性,这些声音开始在不少人耳边盘绕。
|
||||
以上,是我 17 年 9 月 19 日晚收到的一条推送消息。我当时在写《[关于 React 系前端技术的思考](https://tingge.github.io/html/react-redux-reactrouter.html)》,可是它让我意识到,该关注下 <video /> 背后的故事了。
|
||||
17 年下半年发生了两件有趣的撕 X 事件:Facebook 将部分开源项目的防专利流氓证书 “BSD + Pattern” 重新授权为 “MIT” 和 W3C 发布 HTML5 版权保护的 EME 推荐标准。一时,似乎著作权、版权和开源、分享,甚至普世、网络中立性,这些声音开始在不少人耳边盘绕。
|
||||
|
||||
“无论如何,在当前的现实中,法律是保护著作权的。” 那么,我以 EME 为切入点,和大家聊聊 HTML 5 中如何保护知识产权吧。
|
||||
|
||||
@@ -34,24 +34,24 @@
|
||||
- EME:加密媒体扩展(Encrypted Media Extensions)是 W3C 提出的一种规范,用于在 Web 浏览器和 DRM 代理软件之间提供通信通道。
|
||||
|
||||
|
||||
- MSE:媒体源扩展(Media Source Extensions)是一项 [W3C](https://zh.wikipedia.org/wiki/%E4%B8%87%E7%BB%B4%E7%BD%91%E8%81%94%E7%9B%9F) 规范,它扩展了HTMLMediaElement,允许 JavaScript 生成媒体流以支持回放。这可以用于自适应流(adaptive streaming)及随时间变化的视频直播流(live streaming)等应用场景。
|
||||
- MSE:媒体源扩展(Media Source Extensions)是一项 [W3C](https://zh.wikipedia.org/wiki/%E4%B8%87%E7%BB%B4%E7%BD%91%E8%81%94%E7%9B%9F) 规范,它扩展了 HTMLMediaElement,允许 JavaScript 生成媒体流以支持回放。这可以用于自适应流(adaptive streaming)及随时间变化的视频直播流(live streaming)等应用场景。
|
||||
- CDM:内容解密模块(Content Decryption Module),客户端或者使用端软件或硬件提供的一个机制,可以播放加密内容。
|
||||
|
||||
#### 背景
|
||||
|
||||
长期以来,“多方利益”模式的 W3C ,以或标准化引领、或被各方优良实践推动再制定标准的方式,来影响着互联网的发展。
|
||||
|
||||
2011年时 Silverlight 、HTML5 及 Flash 还是最受热捧的 RIA (富互联网应用) 技术。当时,Silverlight 的PlayReady DRM、 Flash 的 Flash Media Rights Management(FMRM),在版权保护上已十分成熟。而 HTML5 还处于 <video> 未指明编码标准的萌芽状态、更谈不上版权保护。
|
||||
2011 年时 Silverlight 、HTML5 及 Flash 还是最受热捧的 RIA (富互联网应用) 技术。当时,Silverlight 的 PlayReady DRM、 Flash 的 Flash Media Rights Management(FMRM),在版权保护上已十分成熟。而 HTML5 还处于 <video> 未指明编码标准的萌芽状态、更谈不上版权保护。
|
||||
|
||||
随着移动互联网、视频直播、职能家电等等互联网快速发展,浏览器插件一度成为网络恶意攻击的重灾区,给网络用户安全性带来很大隐患。微软和许多企业都鼓励用户、开发者使用 HTML5 的通信协议,标准化通信可以极大增加网络安全性。其中包括 W3C 的 Media Source Extensions (MSE)、 Encrypted Media Extensions (EME),MPEG的 MPEG-DASH 和 Common Encryption (CENC)。
|
||||
随着移动互联网、视频直播、职能家电等等互联网快速发展,浏览器插件一度成为网络恶意攻击的重灾区,给网络用户安全性带来很大隐患。微软和许多企业都鼓励用户、开发者使用 HTML5 的通信协议,标准化通信可以极大增加网络安全性。其中包括 W3C 的 Media Source Extensions (MSE)、 Encrypted Media Extensions (EME),MPEG 的 MPEG-DASH 和 Common Encryption (CENC)。
|
||||
|
||||
终于,内容提供商(如 Netflix、Adobe、CableLabs 等)从 Flash、Silverlight 插件播放器过渡到统一的 HTML5 视频播放;各大浏览器公司(如 Google, Microsoft, Apple)也逐步抛弃了过时的媒体插件。
|
||||
|
||||
EME 作为 HTML 5 DRM 版权保护方案中的一员,虽然从2012年提案开始就颇多争议,但是事实上已被各浏览器以捆绑闭源的 CDM 的沙箱化方式“悄悄”分发。现在,W3C 只是给了它应有的名分罢了。
|
||||
EME 作为 HTML 5 DRM 版权保护方案中的一员,虽然从 2012 年提案开始就颇多争议,但是事实上已被各浏览器以捆绑闭源的 CDM 的沙箱化方式“悄悄”分发。现在,W3C 只是给了它应有的名分罢了。
|
||||
|
||||
#### EME 对 Web 产生的影响
|
||||
|
||||
W3C理事长 Tim Berners-Lee 在《[W3C Blog: 关于HTML5标准中的加密媒体扩展(EME)](http://www.chinaw3c.org/archives/1742/)》中阐述了 EME 对内容分发商、媒体、用户、开发者、安全技术研究人员的影响。
|
||||
W3C 理事长 Tim Berners-Lee 在《[W3C Blog: 关于 HTML5 标准中的加密媒体扩展(EME)](http://www.chinaw3c.org/archives/1742/)》中阐述了 EME 对内容分发商、媒体、用户、开发者、安全技术研究人员的影响。
|
||||
|
||||
对多数人的影响大概是,可以提供一个相对安全的在线环境使用户可以获取高品质商业级的 Web 音视频等内容,并便捷的就此进行在线互动。
|
||||
|
||||
@@ -69,7 +69,7 @@ W3C理事长 Tim Berners-Lee 在《[W3C Blog: 关于HTML5标准中的加密媒
|
||||
|
||||
以下是截取 caniuse 网站统计的 EME 和 ESM 的支持情况(点击图片可跳转到对应网址):
|
||||
|
||||
[ ](http://caniuse.com/#search=EME)
|
||||
[](http://caniuse.com/#search=EME)
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ W3C理事长 Tim Berners-Lee 在《[W3C Blog: 关于HTML5标准中的加密媒
|
||||
|
||||
今天,在传输工作室生产的付费内容的时候,DRM 是必要的。这些内容必须防止被盗,因此 DRM 的代码和工作过程都向终端用户和开发者屏蔽了。解密过的内容不会离开解码层,因此也不会被拦截。
|
||||
|
||||
为了标准化 DRM 以及为各平台的实现提供一定的互通性,几个 Web 巨头一起创建了通用加密标准[Common Encryption (CENC)]() 和通用的多媒体加密扩展[Encrypted Media Extensions](),以便为多个 DRM 提供商(例如,EME 可用于 Edge 平台上的 Playready 和 Chrome 平台上的 Widewine)构建一套通用的 API,这些 API 能够从 DRM 授权模块读取视频内容加密密钥用于解密。
|
||||
为了标准化 DRM 以及为各平台的实现提供一定的互通性,几个 Web 巨头一起创建了通用加密标准[Common Encryption (CENC)](.) 和通用的多媒体加密扩展[Encrypted Media Extensions](.),以便为多个 DRM 提供商(例如,EME 可用于 Edge 平台上的 Playready 和 Chrome 平台上的 Widewine)构建一套通用的 API,这些 API 能够从 DRM 授权模块读取视频内容加密密钥用于解密。
|
||||
|
||||
CENC 声明了一套标准的加密和密钥映射方法,它可用于在多个 DRM 系统上解密相同的内容,只需要提供相同的密钥即可。
|
||||
|
||||
@@ -111,7 +111,7 @@ CENC 没有规定授权的发放、授权的格式、授权的存储、以及使
|
||||
|
||||
- index.html:模拟内容服务商视频播放网页,获取 EME 设置(本例中 eme.js),通过调用 MSE 模块(本例中 mse.js) 逐块加载视频片段并控制播放。
|
||||
- resources.js:模拟 License(Key) server,与 CDM 模块交互并提供解密媒体资源所需的 `key`;
|
||||
- media:模拟Key System 和 Packaging service。主要功能是提供一种内容保护(DRM)机制,实际应用中常见的 Key System 有 Clear Key、Playready、Widevine 等;另外,作为 Packaging Service,提供编码并加密媒体资源以供发布和播放使用。
|
||||
- media:模拟 Key System 和 Packaging service。主要功能是提供一种内容保护(DRM)机制,实际应用中常见的 Key System 有 Clear Key、Playready、Widevine 等;另外,作为 Packaging Service,提供编码并加密媒体资源以供发布和播放使用。
|
||||
- eme.js: 模拟 EME 通信模块。主要包括监听 MediaKeys 的 message 和 keystatuseschange 变化;发起证书请求;最后,通过 License(key) 解密 video/audio 流;
|
||||
- mse.js:模拟媒体源扩展模块,通过调用浏览器提供的 MSE API,来控制视频流播放逻辑。
|
||||
|
||||
@@ -120,7 +120,7 @@ CENC 没有规定授权的发放、授权的格式、授权的存储、以及使
|
||||
| 开源的视频播放器 | 个人点评 |
|
||||
| ---------------------------------------- | ---------------------------------------- |
|
||||
| [video.js](https://github.com/videojs/video.js) 和其[插件](https://github.com/videojs/video.js/wiki/Plugins#community-plugins)。设备检测与配置逻辑的 [videojs-contrib-hls](https://github.com/videojs/videojs-contrib-hls) 、广告 [videojs-contrib-ads](https://github.com/videojs/videojs-contrib-ads) | 免费开源的 HTML5 和 Flash 播放器,通过强大的插件应用于 [400,000 网站](https://trends.builtwith.com/media/VideoJS)。采用 Apache License, Version 2.0 授权 |
|
||||
| [JW Player](https://github.com/jwplayer/jwplayer) | 号称世界上最流行的嵌入播放器,应用于200万网站、每月13亿播放次数。采用 [Creative Commons license](http://creativecommons.org/licenses/by-nc-sa/3.0/) 授权 |
|
||||
| [JW Player](https://github.com/jwplayer/jwplayer) | 号称世界上最流行的嵌入播放器,应用于 200 万网站、每月 13 亿播放次数。采用 [Creative Commons license](http://creativecommons.org/licenses/by-nc-sa/3.0/) 授权 |
|
||||
| [Shaka Player](https://github.com/google/shaka-player) | Google 开源的基于 MSE + EME 的 JavaScript 库,支持 DASH、HLS 等。采用 Apache License 2.0 授权 |
|
||||
| [dash.js](https://github.com/Dash-Industry-Forum/dash.js) | 一个支持 MPEG DASH 的参考实现,适合研究学习。采用 BSD 授权 |
|
||||
|
||||
@@ -130,5 +130,5 @@ CENC 没有规定授权的发放、授权的格式、授权的存储、以及使
|
||||
|
||||
期待随着标准的发布,注重著作权、版权的互联网能够很快地向有序方向发展。
|
||||
|
||||
> 讨论地址是:[精读《W3C发布加密媒体扩展(Encrypted Media Extensions,EME)正式推荐标准》 · Issue #37 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/37)
|
||||
> 讨论地址是:[精读《W3C 发布加密媒体扩展(Encrypted Media Extensions,EME)正式推荐标准》 · Issue #37 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/37)
|
||||
> 如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。
|
||||
@@ -93,7 +93,7 @@ OOCSS 成为 css 的面向对象加强版,每个 class 只处理一件事:
|
||||
|
||||
## 3.4 SMACSS
|
||||
|
||||
### 为css分类
|
||||
### 为 css 分类
|
||||
|
||||
SMACSS 认为 css 有 5 个类别:
|
||||
|
||||
|
||||
+21
-21
@@ -1,6 +1,6 @@
|
||||
本期精读的文章是:[Front End Performance Checklist 2017](https://www.smashingmagazine.com/2016/12/front-end-performance-checklist-2017-pdf-pages/)
|
||||
|
||||
现在随着 web 应用的复杂性日益增加,其性能优化就会显得尤为必要,同时会给性能指标分析带来新的挑战,因为性能指标之间的差异性非常大,这取决于使用的设备、浏览器、协议、网络类型以及其它能够对性能产生影响的潜在因素(如:CDN、ISP、cache、proxy、firewall、load balancer、server等)。
|
||||
现在随着 web 应用的复杂性日益增加,其性能优化就会显得尤为必要,同时会给性能指标分析带来新的挑战,因为性能指标之间的差异性非常大,这取决于使用的设备、浏览器、协议、网络类型以及其它能够对性能产生影响的潜在因素(如:CDN、ISP、cache、proxy、firewall、load balancer、server 等)。
|
||||
|
||||
# 1 引言
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
|
||||
根据 [psychological research](https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/#the-need-for-performance-optimization-the-20-rule) 指出,网站最少在速度上比别人快 20%,才能让用户感觉到比别人的更快。这个速度说的并不是整个页面的加载时间,而是[启动渲染时间](http://www.websiteoptimization.com/speed/tweak/start-render/),[首次有效渲染时间](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint),[交互时间](https://developers.google.com/web/tools/lighthouse/audits/time-to-interactive)。
|
||||
|
||||
### 控制响应时间在100ms,控制帧速在60帧/秒
|
||||
### 控制响应时间在 100ms,控制帧速在 60 帧/秒
|
||||
|
||||
[RAIL performance model](https://www.smashingmagazine.com/2015/10/rail-user-centric-model-performance/) 提出的性能优化指标:务必在用户初始操作后的 100ms 内提供反馈。考虑到存在响应时间不足 100ms 的情况,页面最迟要在 50ms 的时候,把控制权交给主线程。
|
||||
|
||||
针对动画,其每一帧都需要在 16ms 内完成,这样才能保证每秒 60帧(一秒/60=16.6ms),如果可以的话最好能在 10ms 内完成。
|
||||
针对动画,其每一帧都需要在 16ms 内完成,这样才能保证每秒 60 帧(一秒/60=16.6ms),如果可以的话最好能在 10ms 内完成。
|
||||
|
||||
### 控制首次有效渲染时间在 1.25s,控制 SpeedIndex 在 1000
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
### 做好构建工具的选型
|
||||
|
||||
不要过度使用那些酷炫的技术栈,坚持选择适合开发环境的工具,如Grunt、Gulp、Webpack、PostCSS,或者组合起来的工具。只要这个工具运行的速度够快,而且没有给项目维护带来太大问题,就够了。
|
||||
不要过度使用那些酷炫的技术栈,坚持选择适合开发环境的工具,如 Grunt、Gulp、Webpack、PostCSS,或者组合起来的工具。只要这个工具运行的速度够快,而且没有给项目维护带来太大问题,就够了。
|
||||
|
||||
### 渐进增强
|
||||
|
||||
@@ -40,11 +40,11 @@
|
||||
|
||||
### 前端框架
|
||||
|
||||
最好使用那些支持服务器端渲染的框架,如Angular,React,Ember 等。所选的框架要保证是被广泛使用并且经过考验的。不同框架对性能有着不同程度的影响,同时对应着不同的优化策略,所以要清楚的了解所选择框架的每个方面。
|
||||
最好使用那些支持服务器端渲染的框架,如 Angular,React,Ember 等。所选的框架要保证是被广泛使用并且经过考验的。不同框架对性能有着不同程度的影响,同时对应着不同的优化策略,所以要清楚的了解所选择框架的每个方面。
|
||||
|
||||
### AMP 或 Instant Articles
|
||||
|
||||
- Google 的 [AMP](https://www.ampproject.org/) 技术会提供一套可靠的性能优化框架(基于免费的CDN网络)
|
||||
- Google 的 [AMP](https://www.ampproject.org/) 技术会提供一套可靠的性能优化框架(基于免费的 CDN 网络)
|
||||
- Facebook 的 [Instant Articles](https://instantarticles.fb.com/) 技术可以在 Facebook 上提升网站的性能。
|
||||
|
||||
### 合理利用 CDN
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
### micro-optimization 和 progressive booting
|
||||
|
||||
- 使用 [skeleton screens](https://twitter.com/lukew/status/665288063195594752) 代替loading indicator 展示
|
||||
- 使用 [skeleton screens](https://twitter.com/lukew/status/665288063195594752) 代替 loading indicator 展示
|
||||
- 使用能够加速 App 初始化渲染的技术,如 [tree-shaking](https://medium.com/@richavyas/aha-moments-from-ngconf-2016-part-1-angular-2-0-compile-cycle-6f462f68632e#.8b9afnsub)、[code-splitting](https://webpack.github.io/docs/code-splitting.html)
|
||||
- 针对[服务端渲染](https://www.smashingmagazine.com/2016/03/server-side-rendering-react-node-express)增加[预编译](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/)环节
|
||||
- 使用 [Optimize.js](https://github.com/nolanlawson/optimize-js) 来加快初始加载速度,其原理是包装优先级高的调用函数
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
### 正确设置 HTTP cache header
|
||||
|
||||
需要正确设置 expires、cache-control、max-age以及其它 HTTP 缓存响应头。请使用 Cache-control: immutable,可以参考 [Heroku’s primer on HTTP caching headers](https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers)、[HTTP caching primer](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en)以及[缓存之最佳实践](https://jakearchibald.com/2016/caching-best-practices/)。
|
||||
需要正确设置 expires、cache-control、max-age 以及其它 HTTP 缓存响应头。请使用 Cache-control: immutable,可以参考 [Heroku’s primer on HTTP caching headers](https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers)、[HTTP caching primer](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en)以及[缓存之最佳实践](https://jakearchibald.com/2016/caching-best-practices/)。
|
||||
|
||||
### 减少使用第三方库,异步加载 JS
|
||||
|
||||
@@ -98,16 +98,16 @@
|
||||
- 可以使用 service worker 来达到字体缓存持久化
|
||||
- [关于如何快速入门字体优化的教程](https://pixelambacht.nl/2016/font-awesome-fixed/)
|
||||
|
||||
### 快速推送 critical CSS文件
|
||||
### 快速推送 critical CSS 文件
|
||||
|
||||
为了保证能够让浏览器快速渲染,会将所有用于首屏渲染的 CSS 文件整合成一个文件(即 [critical CSS](https://www.smashingmagazine.com/2015/08/understanding-critical-css/)),以 `<style>` 的行内形式内嵌到 `<head>`,这样可以减少 [critical 渲染路径](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/optimizing-critical-rendering-path?hl=en)。由于 HTTP 数据包大小的限制,因此 critical CSS 文件大小不能超过14KB。
|
||||
为了保证能够让浏览器快速渲染,会将所有用于首屏渲染的 CSS 文件整合成一个文件(即 [critical CSS](https://www.smashingmagazine.com/2015/08/understanding-critical-css/)),以 `<style>` 的行内形式内嵌到 `<head>`,这样可以减少 [critical 渲染路径](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/optimizing-critical-rendering-path?hl=en)。由于 HTTP 数据包大小的限制,因此 critical CSS 文件大小不能超过 14KB。
|
||||
|
||||
HTTP/2 协议可以让 critical CSS 用单个 CSS 文件存储,通过服务器推送 CSS 文件的传输方式来减少HTML 文件数据量,由于存在高速缓存问题,因此需要建立[带有缓存的 HTTP/2 服务器传输机制](https://css-tricks.com/cache-aware-server-push/)。
|
||||
HTTP/2 协议可以让 critical CSS 用单个 CSS 文件存储,通过服务器推送 CSS 文件的传输方式来减少 HTML 文件数据量,由于存在高速缓存问题,因此需要建立[带有缓存的 HTTP/2 服务器传输机制](https://css-tricks.com/cache-aware-server-push/)。
|
||||
|
||||
### tree-shaking 和 code-splitting 机制减轻负载
|
||||
|
||||
- [Tree-shaking](https://medium.com/@roman01la/dead-code-elimination-and-tree-shaking-in-javascript-build-systems-fb8512c86edf) 机制能够帮助清理生产环境中的冗余代码。可以通过 [Webpack2 Tree-Shaking](http://www.2ality.com/2015/12/webpack-tree-shaking.html) 机制来清理冗余的 exports 代码或者使用 [UnCSS](https://github.com/giakki/uncss)、[Helium](https://github.com/geuis/helium-css) 工具来清理冗余的CSS代码
|
||||
- [code splitting](https://webpack.github.io/docs/code-splitting.html) 机制是Webpack 的另一个特性,它能够将构建的代码分成多个 chunk,并且对 chunk 按需载入。只要在代码中定义了分离点(split point),Webpack 便会处理好相关的输出文件,不仅能够较少文件数据量,而且还能对代码做到按需载入。
|
||||
- [Tree-shaking](https://medium.com/@roman01la/dead-code-elimination-and-tree-shaking-in-javascript-build-systems-fb8512c86edf) 机制能够帮助清理生产环境中的冗余代码。可以通过 [Webpack2 Tree-Shaking](http://www.2ality.com/2015/12/webpack-tree-shaking.html) 机制来清理冗余的 exports 代码或者使用 [UnCSS](https://github.com/giakki/uncss)、[Helium](https://github.com/geuis/helium-css) 工具来清理冗余的 CSS 代码
|
||||
- [code splitting](https://webpack.github.io/docs/code-splitting.html) 机制是 Webpack 的另一个特性,它能够将构建的代码分成多个 chunk,并且对 chunk 按需载入。只要在代码中定义了分离点(split point),Webpack 便会处理好相关的输出文件,不仅能够较少文件数据量,而且还能对代码做到按需载入。
|
||||
- 用 [Rollup](http://rollupjs.org/) 来 export 代码也能够取得不错的效果
|
||||
|
||||
### 提升渲染性能
|
||||
@@ -131,7 +131,7 @@ HTTP/2 协议可以让 critical CSS 用单个 CSS 文件存储,通过服务器
|
||||
|
||||
从目前来看,浏览器对 HTTP/2 支持度还不错,使用 HTTP/2 后,就可以利用 service worker 以及 HTTP/2 的服务器推送功能来获取更显著的性能提升。
|
||||
|
||||
在项目进行 HTTPS 改造时,需要评估 HTTP/1.1 项目的用户基数,需要针对这类用户构建并发送符合HTTP2规范的报头。
|
||||
在项目进行 HTTPS 改造时,需要评估 HTTP/1.1 项目的用户基数,需要针对这类用户构建并发送符合 HTTP2 规范的报头。
|
||||
|
||||
### 正确部署 HTTP/2
|
||||
|
||||
@@ -142,7 +142,7 @@ HTTP/2 协议可以让 critical CSS 用单个 CSS 文件存储,通过服务器
|
||||
|
||||
### 确保服务器的安全性
|
||||
|
||||
需要检查是否[正确设置 HTTP 请求头部](https://securityheaders.io/),如 strict-transport-security,[使用 Snyk 工具](https://www.w3ctech.com/www.smashingmagazine.com/2016/01/eliminating-known-security-vulnerabilities-with-snyk/)排除已知的漏洞以及[使用 SSL Server Test](https://www.ssllabs.com/ssltest/) 网站来检查证书是否失效。 尽量保证从外部引入的插件以及 js 脚本的载入是通过 HTTPS 协议的,发起 HTTP 请求同时设置 strict-transport-security 以及 content-security-policy HTTP请求头。
|
||||
需要检查是否[正确设置 HTTP 请求头部](https://securityheaders.io/),如 strict-transport-security,[使用 Snyk 工具](https://www.w3ctech.com/www.smashingmagazine.com/2016/01/eliminating-known-security-vulnerabilities-with-snyk/)排除已知的漏洞以及[使用 SSL Server Test](https://www.ssllabs.com/ssltest/) 网站来检查证书是否失效。 尽量保证从外部引入的插件以及 js 脚本的载入是通过 HTTPS 协议的,发起 HTTP 请求同时设置 strict-transport-security 以及 content-security-policy HTTP 请求头。
|
||||
|
||||
### 服务器和 CDN 是否支持 HTTP/2
|
||||
|
||||
@@ -151,7 +151,7 @@ HTTP/2 协议可以让 critical CSS 用单个 CSS 文件存储,通过服务器
|
||||
### Brotli 或 Zopfli 压缩算法
|
||||
|
||||
- [Brotli](https://samsaffron.com/archive/2016/06/15/the-current-state-of-brotli-compression),是 Google 开源的无损数据格式,其压缩效率要远高于 Gzip 和 Deflate
|
||||
- [Zopfli压缩算法](https://blog.codinghorror.com/zopfli-optimization-literally-free-bandwidth/),能够将数据编码成 Deflate、Gzip、Zlib 数据格式。用 Zopfli 算法压缩过后的文件能够比同样用 Zlib 算法压缩的文件小 3%-8%
|
||||
- [Zopfli 压缩算法](https://blog.codinghorror.com/zopfli-optimization-literally-free-bandwidth/),能够将数据编码成 Deflate、Gzip、Zlib 数据格式。用 Zopfli 算法压缩过后的文件能够比同样用 Zlib 算法压缩的文件小 3%-8%
|
||||
|
||||
### 激活 OCSP stapling
|
||||
|
||||
@@ -186,7 +186,7 @@ HTTP/2 协议可以让 critical CSS 用单个 CSS 文件存储,通过服务器
|
||||
|
||||
### 持续监控
|
||||
|
||||
在进行快速、无限制的测试时,最好使用一个个人的WebPageTest实例。建立一个能自动预警的性能预算监听。建立自己的用户时间标记从而测量并监测具体商用的数据。使用SpeedCurve对性能的变化进行监控,同时利用New Relic获取WebPageTest没法提供的数据。SpeedTracker,Lighthouse和Calibre都是不错的选择。
|
||||
在进行快速、无限制的测试时,最好使用一个个人的 WebPageTest 实例。建立一个能自动预警的性能预算监听。建立自己的用户时间标记从而测量并监测具体商用的数据。使用 SpeedCurve 对性能的变化进行监控,同时利用 New Relic 获取 WebPageTest 没法提供的数据。SpeedTracker,Lighthouse 和 Calibre 都是不错的选择。
|
||||
|
||||
部署私密的 [WebPageTest](http://www.webpagetest.org/) 测试环境,有助于快速构建测试用例。针对性能开销大的环节建立自动报警机制,可以使用 [SpeedCurve](https://speedcurve.com/) 对性能的变化进行监控,利用 [New Relic](https://newrelic.com/browser-monitoring) 获取 WebPageTest 无法提供的数据。
|
||||
|
||||
@@ -251,9 +251,9 @@ render 部分包括 Recalculate Style 和 Layout,如果发现 render 部分耗
|
||||
|
||||
#### 降低样式计算和复杂度
|
||||
|
||||
添加或移除一个DOM元素、修改元素属性和样式类、应用动画效果等操作,都会引起DOM结构的改变,从而导致浏览器需要重新计算每个元素的样式、对页面或其一部分重新布局(多数情况下),这就是所谓的样式计算。
|
||||
添加或移除一个 DOM 元素、修改元素属性和样式类、应用动画效果等操作,都会引起 DOM 结构的改变,从而导致浏览器需要重新计算每个元素的样式、对页面或其一部分重新布局(多数情况下),这就是所谓的样式计算。
|
||||
|
||||
因此需要减少执行样式计算的元素的个数,降低样式选择器的复杂度,使用基于 class 的方式,如以BEM (Block, Element, Modifier)的方式编写 CSS 代码,能达到最好的样式计算的性能,因为这种方式建议对每个DOM元素都只使用一个样式class。
|
||||
因此需要减少执行样式计算的元素的个数,降低样式选择器的复杂度,使用基于 class 的方式,如以 BEM (Block, Element, Modifier)的方式编写 CSS 代码,能达到最好的样式计算的性能,因为这种方式建议对每个 DOM 元素都只使用一个样式 class。
|
||||
|
||||
#### 避免大规模、复杂的布局
|
||||
|
||||
@@ -280,7 +280,7 @@ Timeline 中绿色部分就是 Paint 部分,Summary 会展示绘制的总体
|
||||
#### 如何优化 Paint
|
||||
|
||||
- 提升元素渲染层为合成层,页面的绘制并非是在单层画面里完成的,浏览器的渲染原理,是浏览器将 DOM tree 映射成 GraphicsLayer tree,中间是经过了 RenderObject、RenderLayer 的一系列映射。元素所在的层提升为合成层后可以减少 Repaint
|
||||
- 使用 transform 或 opacity 实现动画,对于独立的合成层应用 transform 和 opacity 是不会触发 Repaint的,因此尽量对 transform 或 opactiy 应用动画来实现效果
|
||||
- 使用 transform 或 opacity 实现动画,对于独立的合成层应用 transform 和 opacity 是不会触发 Repaint 的,因此尽量对 transform 或 opactiy 应用动画来实现效果
|
||||
- 减少绘制区域,对于不需要重新绘制的区域应尽量避免绘制,已减少绘制区域,比如一个 fix 在页面顶部的固定不变的导航 header,在页面底部某个区域 Repaint 时,整个屏幕包括 fix 的 header 也会被重绘,而对于固定不变的区域,期望其并不会被重绘,因此可以通过之前的方法,将其提升为独立的合成层
|
||||
- 降低绘制复杂度,对于无法避免的 Paint,需要尽可能的减少 Paint 的消耗,有些效果的 Paint 代价十分昂贵,比如绘制一个阴影可能就比绘制一个边框更加耗时,因此开发过程中,需要研究能够实现相同的效果,同时却能达到更小的 Paint 消耗的方法
|
||||
|
||||
@@ -311,6 +311,6 @@ Timeline 中绿色部分就是 Paint 部分,Summary 会展示绘制的总体
|
||||
|
||||
现在随着 web 应用的复杂性日益增加,其性能优化的重要性越来越突出,且性能优化的方法、技巧、工具也越来越丰富和复杂,本文所展示的内容仅仅只是管中窥豹,希望读者们可以在此讨论一些在实际场景中的性能优化问题以及解决方案。
|
||||
|
||||
> 讨论地址是:[精读《2017前端性能优化备忘录》 · Issue #39 · dt-fe/weekly](http://github.com/dt-fe/weekly/issues/39)
|
||||
> 讨论地址是:[精读《2017 前端性能优化备忘录》 · Issue #39 · dt-fe/weekly](http://github.com/dt-fe/weekly/issues/39)
|
||||
|
||||
> 如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。
|
||||
|
||||
+12
-12
@@ -6,7 +6,7 @@
|
||||
|
||||
我为什么要选这篇文章呢?
|
||||
|
||||
sessionstack最近接连发了好几篇文章, 深入探讨JS, 以及 JS 中一些内部原理. 文中也讲到了, 伴随深入了解 JS 中的一些工作原理, 才有可能写出更好的代码和程序.
|
||||
sessionstack 最近接连发了好几篇文章, 深入探讨 JS, 以及 JS 中一些内部原理. 文中也讲到了, 伴随深入了解 JS 中的一些工作原理, 才有可能写出更好的代码和程序.
|
||||
|
||||
而 JS 中的内存管理, 我的感觉就像 JS 中的一门副科, 我们平时不会太重视, 但是一旦出问题又很棘手. 所以可以通过平时多了解一些 JS 中内存管理问题, 在写代码中通过一些习惯, 避免内存泄露的问题.
|
||||
|
||||
@@ -23,11 +23,11 @@ sessionstack最近接连发了好几篇文章, 深入探讨JS, 以及 JS 中一
|
||||
2. 使用分配到的内存(读, 写)
|
||||
3. 不需要时将其释放/归还
|
||||
|
||||
在 C语言中, 有专门的内存管理接口, 像`malloc()` 和 `free()`. 而在 JS 中, 没有专门的内存管理接口, 所有的内存管理都是"自动"的. JS 在创建变量时, 自动分配内存, 并在不使用的时候, 自动释放. 这种"自动"的内存回收, 造成了很多 JS 开发并不关心内存回收, 实际上, 这是错误的.
|
||||
在 C 语言中, 有专门的内存管理接口, 像`malloc()` 和 `free()`. 而在 JS 中, 没有专门的内存管理接口, 所有的内存管理都是"自动"的. JS 在创建变量时, 自动分配内存, 并在不使用的时候, 自动释放. 这种"自动"的内存回收, 造成了很多 JS 开发并不关心内存回收, 实际上, 这是错误的.
|
||||
|
||||
## JS 中的内存回收
|
||||
### 引用
|
||||
垃圾回收算法主要依赖于引用的概念. 在内存管理的环境中, 一个对象如果有访问另一个对象的权限(隐式或者显式), 叫做一个对象引用另一个对象. 例如: 一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用).
|
||||
垃圾回收算法主要依赖于引用的概念. 在内存管理的环境中, 一个对象如果有访问另一个对象的权限(隐式或者显式), 叫做一个对象引用另一个对象. 例如: 一个 Javascript 对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用).
|
||||
|
||||
### 引用计数垃圾收集
|
||||
这是最简单的垃圾收集算法.此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”. 如果没有引用指向该对象(零引用, 对象将被垃圾回收机制回收.
|
||||
@@ -64,13 +64,13 @@ window.onload = function(){
|
||||
### 标记-清除算法
|
||||
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”.
|
||||
|
||||
这个算法假定设置一个叫做根`root`的对象(在Javascript里,根是全局对象). 定期的, 垃圾回收器将从根开始, 找所有从根开始引用的对象, 然后找这些对象引用的对象, 从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象.
|
||||
这个算法假定设置一个叫做根`root`的对象(在 Javascript 里,根是全局对象). 定期的, 垃圾回收器将从根开始, 找所有从根开始引用的对象, 然后找这些对象引用的对象, 从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象.
|
||||
|
||||
从2012年起, 所有现代浏览器都使用了标记-清除内存回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进.
|
||||
从 2012 年起, 所有现代浏览器都使用了标记-清除内存回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记-清除算法的改进.
|
||||

|
||||
|
||||
### 自动 GC 的问题
|
||||
尽管自动 GC 很方便, 但是我们不知道GC 什么时候会进行. 这意味着如果我们在使用过程中使用了大量的内存, 而 GC 没有运行的情况下, 或者 GC 无法回收这些内存的情况下, 程序就有可能假死, 这个就需要我们在程序中手动做一些操作来触发内存回收.
|
||||
尽管自动 GC 很方便, 但是我们不知道 GC 什么时候会进行. 这意味着如果我们在使用过程中使用了大量的内存, 而 GC 没有运行的情况下, 或者 GC 无法回收这些内存的情况下, 程序就有可能假死, 这个就需要我们在程序中手动做一些操作来触发内存回收.
|
||||
|
||||
### 什么是内存泄露?
|
||||
本质上讲, 内存泄露就是不再被需要的内存, 由于某种原因, 无法被释放.
|
||||
@@ -94,11 +94,11 @@ function foo() {
|
||||
// Foo 被调用时, this 指向全局变量(window)
|
||||
foo();
|
||||
```
|
||||
在这种情况下调用`foo`, this被指向了全局变量`window`, 意外的创建了全局变量.
|
||||
在这种情况下调用`foo`, this 被指向了全局变量`window`, 意外的创建了全局变量.
|
||||
|
||||
我们谈到了一些意外情况下定义的全局变量, 代码中也有一些我们明确定义的全局变量. 如果使用这些全局变量用来暂存大量的数据, 记得在使用后, 对其重新赋值为 null.
|
||||
#### 2. 未销毁的定时器和回调函数
|
||||
在很多库中, 如果使用了观察着模式, 都会提供回调方法, 来调用一些回调函数. 要记得回收这些回调函数. 举一个 setInterval的例子.
|
||||
在很多库中, 如果使用了观察者模式, 都会提供回调方法, 来调用一些回调函数. 要记得回收这些回调函数. 举一个 setInterval 的例子.
|
||||
```javascript
|
||||
var serverData = loadData();
|
||||
setInterval(function() {
|
||||
@@ -133,7 +133,7 @@ setInterval(replaceThing, 1000);
|
||||
|
||||
这个范例的关键在于, 闭包之间是共享作用域的, 尽管`unused`可能一直没有被调用, 但是`someMethod` 可能会被调用, 就会导致内存无法对其进行回收. 当这段代码被反复执行时, 内存会持续增长.
|
||||
|
||||
该问题的更多描述可见[Meteor团队的这篇文章](https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156).
|
||||
该问题的更多描述可见[Meteor 团队的这篇文章](https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156).
|
||||
#### 4. DOM 引用
|
||||
很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中.
|
||||
```Javascript
|
||||
@@ -153,7 +153,7 @@ function removeImage() {
|
||||
另外需要注意的一个点是, 对于一个 Dom 树的叶子节点的引用. 举个例子: 如果我们引用了一个表格中的`td`元素, 一旦在 Dom 中删除了整个表格, 我们直观的觉得内存回收应该回收除了被引用的 `td`外的其他元素. 但是事实上, 这个`td` 元素是整个表格的一个子元素, 并保留对于其父元素的引用. 这就会导致对于整个表格, 都无法进行内存回收. 所以我们要小心处理对于 Dom 元素的引用.
|
||||
# 3 精读
|
||||
|
||||
ES6中引入`WeakSet` 和 `WeakMap`两个新的概念, 来解决引用造成的内存回收问题. `WeakSet` 和 `WeakMap`对于值的引用可以忽略不计, 他们对于值的引用是弱引用,内存回收机制, 不会考虑这种引用. 当其他引用被消除后, 引用就会从内存中被释放.
|
||||
ES6 中引入`WeakSet` 和 `WeakMap`两个新的概念, 来解决引用造成的内存回收问题. `WeakSet` 和 `WeakMap`对于值的引用可以忽略不计, 他们对于值的引用是弱引用,内存回收机制, 不会考虑这种引用. 当其他引用被消除后, 引用就会从内存中被释放.
|
||||
|
||||
JS 这类高级语言,隐藏了内存管理功能。但无论开发人员是否注意,内存管理都在那,所有编程语言最终要与操作系统打交道,在内存大小固定的硬件上工作。不幸的是,即使不考虑垃圾回收对性能的影响,2017 年最新的垃圾回收算法,也无法智能回收所有极端的情况。
|
||||
|
||||
@@ -161,7 +161,7 @@ JS 这类高级语言,隐藏了内存管理功能。但无论开发人员是
|
||||
|
||||
所以在 JS 这类高级语言中,有必要掌握基础内存分配原理,在对内存敏感的场景,比如 nodejs 代码做严格检查与优化。谨慎使用 dom 操作、主动删除没有业务意义的变量、避免提前优化、过度优化,在保证代码可读性的前提下,利用性能监控工具,通过调用栈定位问题代码。
|
||||
|
||||
同时对于如何利用 chrome调试工具, 分析内存泄露的方法和技巧. 可以参考上期精读[精读《2017前端性能优化备忘录》](https://zhuanlan.zhihu.com/p/30349982)
|
||||
同时对于如何利用 chrome 调试工具, 分析内存泄露的方法和技巧. 可以参考上期精读[精读《2017 前端性能优化备忘录》](https://zhuanlan.zhihu.com/p/30349982)
|
||||
|
||||
# 4 总结
|
||||
|
||||
@@ -174,4 +174,4 @@ JS 这类高级语言,隐藏了内存管理功能。但无论开发人员是
|
||||
|
||||
|
||||
|
||||
参考文章: [MDN 的内存管理介绍](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management)
|
||||
参考文章: [MDN 的内存管理介绍](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management)
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
|
||||
我为什么要选这篇文章呢?
|
||||
|
||||
sessionstack最近接连发了好几篇文章, 深入探讨JS, 以及 JS 中一些内部原理. 文中也讲到了, 伴随深入了解 JS 中的一些工作原理, 才有可能写出更好的代码和程序.
|
||||
sessionstack 最近接连发了好几篇文章, 深入探讨 JS, 以及 JS 中一些内部原理. 文中也讲到了, 伴随深入了解 JS 中的一些工作原理, 才有可能写出更好的代码和程序.
|
||||
|
||||
而 JS 中 Event Loop, 我的感觉就像 JS 中的一门内科, 我们平时只注意外科创伤,却忽视了内科问题往往容易莫名其妙的生病。了解 JS Event Loop 的原理,对 `setTimeout` `Promise` 这种基础概念不再浮在表层,可以写出更可靠的代码,如果你是前端新人,不要总是因为这个问题挂在一面 :p。
|
||||
|
||||
# 2 内容概要
|
||||
|
||||
从前我对 Event Loop 的理解也并不透彻,通过仔细阅读此文后, Event Loop 、宿主环境、js线程三者之间关系更加透明了,希望读者读完后也能有所体会。
|
||||
从前我对 Event Loop 的理解也并不透彻,通过仔细阅读此文后, Event Loop 、宿主环境、js 线程三者之间关系更加透明了,希望读者读完后也能有所体会。
|
||||
|
||||
文中 Promise、async/await 部分就忽略了,本篇重点介绍 Event Loop 。
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
本期精读的文章是:[30行js代码创建神经网络](https://medium.freecodecamp.org/how-to-create-a-neural-network-in-javascript-in-only-30-lines-of-code-343dafc50d49)。
|
||||
本期精读的文章是:[30 行 js 代码创建神经网络](https://medium.freecodecamp.org/how-to-create-a-neural-network-in-javascript-in-only-30-lines-of-code-343dafc50d49)。
|
||||
|
||||
懒得看文章?没关系,稍后会附上文章内容概述,同时,更希望能通过阅读这一期的精读,穿插着深入阅读原文。
|
||||
|
||||
## 1 引言
|
||||

|
||||
|
||||
自从Alpha Go 打败了李世石,大家对深度学习的体感更加的强烈,人工智能也越来越多的出现在大家的生活之中。很多人也会谈论,程序员什么时候会被人工智能给替代?与其慌张,在人工智能的潮流下,不断学习新的人工智能相关技术,武装自己,才是硬道理。 本文介绍了如何使用[Synaptic.js](https://synaptic.juancazala.com/#/) 创建简单的神经网络,解决异或运算的问题。
|
||||
自从 Alpha Go 打败了李世石,大家对深度学习的体感更加的强烈,人工智能也越来越多的出现在大家的生活之中。很多人也会谈论,程序员什么时候会被人工智能给替代?与其慌张,在人工智能的潮流下,不断学习新的人工智能相关技术,武装自己,才是硬道理。 本文介绍了如何使用[Synaptic.js](https://synaptic.juancazala.com/#/) 创建简单的神经网络,解决异或运算的问题。
|
||||
|
||||
## 2 内容概要
|
||||
|
||||
### 神经网络中的神经元和突触
|
||||
|
||||
对神经网络有所了解的人都知道,神经网络就是构建类似人脑的神经系统,在人脑的神经系统中,存在一种非常重要的细胞,叫神经元。在神经网络中,你可以把神经元理解为一个函数,它接受一些输入返回一些输出结果,其中[Sigmoid神经元](https://en.wikipedia.org/wiki/Sigmoid_function)是一种非常常用的神经元,这种神经元以 Sigmoid 函数 作为[激活函数](https://www.zhihu.com/question/22334626)。Sigmoid 函数接受任意的数值,输出 `0` 到 `1` 之间的值,大家可以看看常见的几种 Sigmoid 函数的函数曲线。
|
||||
对神经网络有所了解的人都知道,神经网络就是构建类似人脑的神经系统,在人脑的神经系统中,存在一种非常重要的细胞,叫神经元。在神经网络中,你可以把神经元理解为一个函数,它接受一些输入返回一些输出结果,其中[Sigmoid 神经元](https://en.wikipedia.org/wiki/Sigmoid_function)是一种非常常用的神经元,这种神经元以 Sigmoid 函数 作为[激活函数](https://www.zhihu.com/question/22334626)。Sigmoid 函数接受任意的数值,输出 `0` 到 `1` 之间的值,大家可以看看常见的几种 Sigmoid 函数的函数曲线。
|
||||
|
||||

|
||||
|
||||
知道了 Sigmoid 函数了,我们可以看一个具体的 **Sigmoid神经元** 例子。
|
||||
知道了 Sigmoid 函数了,我们可以看一个具体的 **Sigmoid 神经元** 例子。
|
||||
|
||||

|
||||
|
||||
@@ -71,11 +71,11 @@ for (var i = 0; i < 20000; i++) {
|
||||
}
|
||||
```
|
||||
|
||||
简单而言,运行 `myNetwork.activate([0,0])` 时,`[0, 0]`是输入值,它对应的异或运算的结果是false, 也就是 `0`。 这个是前向的传播,所以称为 **激活** 网络,每次前向传播之后,我们需要做一次**反向传播**来更新权重和偏差。
|
||||
简单而言,运行 `myNetwork.activate([0,0])` 时,`[0, 0]`是输入值,它对应的异或运算的结果是 false, 也就是 `0`。 这个是前向的传播,所以称为 **激活** 网络,每次前向传播之后,我们需要做一次**反向传播**来更新权重和偏差。
|
||||
|
||||
反向传播就是通过这行代码来做的: `myNetwork.propagate(learningRate, [0])`,其中 `learningRate `是告诉神经网络如何调整权重的常量,第二个参数 `[0]`是异或运算的结果。
|
||||
|
||||
经过20000次学习,我们得到如下的结果:
|
||||
经过 20000 次学习,我们得到如下的结果:
|
||||
|
||||
```js
|
||||
console.log(myNetwork.activate([0,0]));
|
||||
@@ -129,7 +129,7 @@ console.log(myNetwork.activate([1,1]));
|
||||
|
||||
## 4. 总结
|
||||
|
||||
本文介绍了使用[Synaptic.js](https://synaptic.juancazala.com/#/) 创建简单的神经网络,解决异或运算的问题过程,也对**反向传播**的过程进行了简单的解释。文中实现神经网络的代码非常简单,包行注释也不超过30行,但是只会代码会有点囫囵吞枣的感觉,大家可以参考文章中给的引文,了解更多的算法原理。
|
||||
本文介绍了使用[Synaptic.js](https://synaptic.juancazala.com/#/) 创建简单的神经网络,解决异或运算的问题过程,也对**反向传播**的过程进行了简单的解释。文中实现神经网络的代码非常简单,包行注释也不超过 30 行,但是只会代码会有点囫囵吞枣的感觉,大家可以参考文章中给的引文,了解更多的算法原理。
|
||||
|
||||
### 相关资料
|
||||
|
||||
@@ -144,6 +144,6 @@ console.log(myNetwork.activate([1,1]));
|
||||
|
||||
### 更多讨论
|
||||
|
||||
> 讨论地址是:[精读《30行js代码创建神经网络》 · Issue #45 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/45)
|
||||
> 讨论地址是:[精读《30 行 js 代码创建神经网络》 · Issue #45 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/45)
|
||||
|
||||
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
本篇是 《框架实现》。
|
||||
|
||||
本周精读的文章是 [dob文档](https://dobjs.github.io/dob-docs/v2/guide/introduction.html),如果不熟悉 API,可以简单读一读,文中有些地方会提到一些函数。
|
||||
本周精读的文章是 [dob 文档](https://dobjs.github.io/dob-docs/v2/guide/introduction.html),如果不熟悉 API,可以简单读一读,文中有些地方会提到一些函数。
|
||||
|
||||
## 1 引言
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ git 的版本控制实际就是对文件进行管理和控制,其管理方法
|
||||
|
||||

|
||||
|
||||
其实是 40位 hash-key 的前两位作为目录名,后 38位 作为文件名,这个对象里面的内容就是 file1.js 的内容,可以查看该对象的内容和类型。
|
||||
其实是 40 位 hash-key 的前两位作为目录名,后 38 位 作为文件名,这个对象里面的内容就是 file1.js 的内容,可以查看该对象的内容和类型。
|
||||
|
||||
执行 git cat-file -p [hash-key] 可以查看已经存在的 object 对象内容;
|
||||
|
||||
@@ -141,7 +141,7 @@ blob 对象用于存储对应文件的内容,tree 对象可以理解为存储
|
||||
|
||||

|
||||
|
||||
目前每个目录里有一个对象,共5个对象,之前的总体 tree 图只包含了4个对象,执行 git log 查看 commit 记录。
|
||||
目前每个目录里有一个对象,共 5 个对象,之前的总体 tree 图只包含了 4 个对象,执行 git log 查看 commit 记录。
|
||||
|
||||

|
||||
|
||||
@@ -173,7 +173,7 @@ git 版本控制住要就是围绕这三类内部对象展开,分别为 blob
|
||||
|
||||
# 4 总结
|
||||
|
||||
本文从一个实际问题即如何使用 git 维护丢失的 commit 入手,并给出相应的解决思路和方案,以及通过git 内部三种对象来分析其内部工作机制,希望能过解决读者们对 git 存在困惑的地方,同时也希望读者们积极参与每周精度的讨论,各抒己见,分享自身在实际工作中遇到的问题及其解决思路。
|
||||
本文从一个实际问题即如何使用 git 维护丢失的 commit 入手,并给出相应的解决思路和方案,以及通过 git 内部三种对象来分析其内部工作机制,希望能过解决读者们对 git 存在困惑的地方,同时也希望读者们积极参与每周精度的讨论,各抒己见,分享自身在实际工作中遇到的问题及其解决思路。
|
||||
|
||||
> 讨论地址是:[精读《When You “Git” in Trouble - a Version Control Story》 · Issue #49 · dt-fe/weekly](http://github.com/dt-fe/weekly/issues/49)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 摘要
|
||||
本期精读文章以一个简单的例子,抽丝剥茧细数讲述如何面向用户可视化设计,探索用户最终的目的,化繁为简,化多为少,揉和N张图至一张图,并传达更多的深意。本文原文:http://www.storytellingwithdata.com/blog/2017/12/14/how-we-position-and-what-we-compare
|
||||
本期精读文章以一个简单的例子,抽丝剥茧细数讲述如何面向用户可视化设计,探索用户最终的目的,化繁为简,化多为少,揉和 N 张图至一张图,并传达更多的深意。本文原文:http://www.storytellingwithdata.com/blog/2017/12/14/how-we-position-and-what-we-compare
|
||||
|
||||
下面是案例优化的具体步骤:
|
||||
# 庖丁解牛 - 可视化案例优化
|
||||
可视化的时候,一定要优先考虑用户能够比较什么,然后把这些数据放到一个基准上,并把要比较的东西放到最近的地方。这样用户就可以快速简便的处理比较数据。图中有一系列类似分面的柱状图,代表了 Q1 ~ Q3 三个季度的账户 targeted筛选 -> engaged订阅 -> pitched投放 -> adopted采用的四个状态的百分比,这四个状态是呈漏斗式的数据,分别是上一个数据的子集。这个案列中,用户希望能够在一张图中比较所有的数据。如下是基础原始图:
|
||||
可视化的时候,一定要优先考虑用户能够比较什么,然后把这些数据放到一个基准上,并把要比较的东西放到最近的地方。这样用户就可以快速简便的处理比较数据。图中有一系列类似分面的柱状图,代表了 Q1 ~ Q3 三个季度的账户 targeted 筛选 -> engaged 订阅 -> pitched 投放 -> adopted 采用的四个状态的百分比,这四个状态是呈漏斗式的数据,分别是上一个数据的子集。这个案列中,用户希望能够在一张图中比较所有的数据。如下是基础原始图:
|
||||
<div>
|
||||
<img src="http://images2017.cnblogs.com/blog/395937/201712/395937-20171224171406912-710179221.png" width = "500" height = "500" alt="图片名称" align=center />
|
||||
</div>
|
||||
@@ -11,9 +11,9 @@
|
||||
|
||||
1. 左上角,我们可以对比 Q1 北美 的四条柱状图,因为这四个数据相邻,并且是在一个坐标系中。
|
||||
|
||||
2. 最上面一排,我们可以对比 Q1 北美和EMEA engaged订阅(两条紫色柱子)的数据,这个对比需要我们横向用手指去比较,需要参照物,存在一定的门槛。
|
||||
2. 最上面一排,我们可以对比 Q1 北美和 EMEA engaged 订阅(两条紫色柱子)的数据,这个对比需要我们横向用手指去比较,需要参照物,存在一定的门槛。
|
||||
|
||||
基于第2点,我们可以考虑转换下思维,转换下数据呈现的类型可以更快速的比较。文中作者从自己在银行的职业生涯中发现线性图形的角度可以更快速的比较。所以就有了下面一张图
|
||||
基于第 2 点,我们可以考虑转换下思维,转换下数据呈现的类型可以更快速的比较。文中作者从自己在银行的职业生涯中发现线性图形的角度可以更快速的比较。所以就有了下面一张图
|
||||
<div>
|
||||
<img src="http://images2017.cnblogs.com/blog/395937/201712/395937-20171224215602928-500155458.png" width = "500" height = "500" alt="图片名称" align=center />
|
||||
</div>
|
||||
@@ -34,9 +34,9 @@
|
||||
|
||||
1. 每个阶段,北美的数据都比其他地区低
|
||||
|
||||
2. 相比其他区域,为什么APAC 的传播度有了突然的提升?
|
||||
2. 相比其他区域,为什么 APAC 的传播度有了突然的提升?
|
||||
|
||||
3. 最需要关注Q3的数据,以及最终阶段的数据
|
||||
3. 最需要关注 Q3 的数据,以及最终阶段的数据
|
||||
|
||||
# 最后
|
||||
从这篇文章的可视化案例优化样板,可以总结出以下步骤:
|
||||
|
||||
+49
-49
@@ -1,18 +1,18 @@
|
||||
本期精读的文章是: [coinhive官方文档](https://coinhive.com)及[Monero官方文档](https://getmonero.org/)
|
||||
本期精读的文章是: [coinhive 官方文档](https://coinhive.com)及[Monero 官方文档](https://getmonero.org/)
|
||||
|
||||
懒得看文章? 没关系.
|
||||
懒得看文章?没关系。
|
||||
|
||||
咦, 怎么是官方文档?
|
||||
咦,怎么是官方文档?
|
||||
|
||||
本期精读有所不同, 注重实操, 先操作获取感性认识, 然后再介绍相关的概念, 由浅入深, 力求不纠缠细节, 但不遮盖环节. 阅读本文需要对加密货币有一些基本常识 (如果你是一个开发者但是完全没了解过加密货币, 可以参考[这里](http://piotrpasich.com/introduction-bitcoin-for-developers/)). 希望同学们看完本文能对加密货币领域有一个更深更切实的体感. 如果要了解更多细节, 文末总结的延伸阅读链接列表是最好的开始.
|
||||
本期精读有所不同,注重实操,先操作获取感性认识,然后再介绍相关的概念,由浅入深,力求不纠缠细节,但不遮盖环节。阅读本文需要对加密货币有一些基本常识 (如果你是一个开发者但是完全没了解过加密货币,可以参考[这里](http://piotrpasich.com/introduction-bitcoin-for-developers/))。希望同学们看完本文能对加密货币领域有一个更深更切实的感受。如果要了解更多细节,文末总结的延伸阅读链接列表是最好的开始。
|
||||
|
||||
要注意一点, 文中很多说明是默认基于XMR和BTC的, 他们两个又同源, 机制非常相似. **所以很多命题判断并不适用于所有的成千上万的加密货币.** 正相反, 新的币种层出不穷, 几乎所有的惯例都被打破, 所有可能性都被尝试. 这一点以下不再做说明.
|
||||
要注意一点,文中很多说明是默认基于 XMR 和 BTC 的,他们两个又同源,机制非常相似。**所以很多命题判断并不适用于所有的成千上万的加密货币.** 正相反,新的币种层出不穷,几乎所有的惯例都被打破,所有可能性都被尝试。这一点以下不再做说明。
|
||||
|
||||
# 1 引言
|
||||
首先, tl;dr干货. 10行代码, 5分钟, 不需部署不需构建直接浏览器开挖.
|
||||
首先,干货。10 行代码,5 分钟,不需部署不需构建直接浏览器开挖。
|
||||
|
||||
- 本地创建文件test.html, 粘贴如下代码:
|
||||
```
|
||||
- 本地创建文件 test.html,粘贴如下代码:
|
||||
```plain
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -29,67 +29,67 @@
|
||||
</html>
|
||||
```
|
||||
|
||||
- 本地双击打开. 等待JS加载, 点击widget "Start Mining". 开始挖矿! 如下图
|
||||
- 本地双击打开。等待 JS 加载,点击 widget "Start Mining"。开始挖矿! 如下图
|
||||
|
||||

|
||||
|
||||
不错, 数字已经在跳动, 风扇开始工作, 永无尽头的挖矿已经开始了. 那么重要的是, 挖出来的加密货币在哪呢? 原来上面的代码里用的还是我的API key, 所以还没挖到你自己那里 :P 继续下面的步骤
|
||||
不错,数字已经在跳动,风扇开始工作,永无尽头的挖矿已经开始了。那么重要的是,挖出来的加密货币在哪呢?原来上面的代码里用的还是我的 API key,所以还没挖到你自己那里,所以请继续下面的步骤:
|
||||
|
||||
- 在coinhive注册账号并登陆. 它是做什么的? 别急, 后面会详细讲. 在coinhive/settings找到自己的API keypair, 把public key复制出来, 形如MUtCJzIDhrs01ERrf3qlqdawo35N0CYD
|
||||
- 在 coinhive 注册账号并登陆。它是做什么的?别急,后面会详细讲。在 `coinhive/settings` 找到自己的 `API keypair`,把 `public key` 复制出来,形如 `MUtCJzIDhrs01ERrf3qlqdawo35N0CYD`
|
||||
|
||||
- 替换上面代码中的data-key部分, 重新开始挖矿. 好了, 现在挖出的Monero (这是啥? 详见下一节) 已经会到你自己的coinhive账户中. 用下图来说明, 你名下总计算过的Hash个数为264K, 当前难度换算为0.00002个Monero(Symbol:XMR). 当前难度为66G一个block, 一个block reward 5.87 XMR, 得到一个XMR是11.268G. 264K/11.268G = 0.0000234. 这就是你目前的收益.
|
||||
- 替换上面代码中的 data-key 部分,重新开始挖矿。好了,现在挖出的 Monero (这是啥?详见下一节) 已经会到你自己的 coinhive 账户中。用下图来说明,你名下总计算过的 Hash 个数为 264K,当前难度换算为 0.00002 个 Monero(Symbol:XMR)。当前难度为 66G 一个 block,一个 block reward 5.87 XMR,得到一个 XMR 是 11.268G。264K/11.268G = 0.0000234。这就是你目前的收益。
|
||||
|
||||

|
||||
|
||||
- 查bitfinex可得现在XMR价格在375美元 (当你看到本文的时候, 价格可能早就又波动到不知哪里去了), 所以你 (以及你忠实勤劳的电脑) 获得的实际收益为0.000234 * 375 USD = 0.008775 USD, 快到一分钱了 :)
|
||||
- 查 bitfinex 可得现在 XMR 价格在 375 美元 (当你看到本文的时候,价格可能早就又波动到不知哪里去了),所以你 (以及你忠实勤劳的电脑) 获得的实际收益为 0.000234 * 375 USD = 0.008775 USD,快到一分钱了 :)
|
||||
|
||||
怎么样, 有没有一种浏览器点开即玩一刀999级的感觉. 以上操作的便捷直接, 是建立在无数前人大量的开发和基础设施建设之上的. 越是领域早期的工作, 越步履维艰, 收货也越丰厚. 如今加密货币已经走到了一个成年期, 逐渐稳定成熟起来.
|
||||
怎么样,有没有一种浏览器点开即玩一刀 999 级的感觉。以上操作的便捷直接,是建立在无数前人大量的开发和基础设施建设之上的。越是领域早期的工作,越步履维艰,收货也越丰厚。如今加密货币已经走到了一个成年期,逐渐稳定成熟起来。
|
||||
|
||||
接下来我们聚焦到上面过程的每个环节, 了解下拼图的每一块是怎样被构成全图的.
|
||||
接下来我们聚焦到上面过程的每个环节,了解下拼图的每一块是怎样被构成全图的。
|
||||
|
||||
# 2 聚焦
|
||||
让我们从最终端最接近用户的环节开始, 逐一聚焦, 最后走完整条链路.
|
||||
让我们从最终端最接近用户的环节开始,逐一聚焦,最后走完整条链路。
|
||||
|
||||
## 2.1 从浏览器说起
|
||||
本文标题叫浏览器挖矿, 也是和贴合前端的部分. 那么为什么可以在浏览器里挖矿? 为什么可以很多用户在多个终端浏览器同时为同一个人 (你) 挖矿?
|
||||
本文标题叫浏览器挖矿,也是和贴合前端的部分。那么为什么可以在浏览器里挖矿?为什么可以很多用户在多个终端浏览器同时为同一个人 (你) 挖矿?
|
||||
|
||||
我们知道, 挖矿是对加密货币产生机制的俗称. 主流大多采取Proof of Work (PoW) 机制. 最常见的PoW方式就是由网络中所有节点作为矿工, 每个节点都基于blockchain前面block已有信息计算一个新信息. 这个新信息的计算方式往往是某种hash function, 并且人为地被设置为需要巨大计算力和时间才能完成 (其具体难度一般也会实时调整). 当一个节点幸运地 (也依靠强大的算力) 第一个计算出结果后, 会把这个结果广播到网络中. 其他所有节点会验证这个结果 (我们知道非对称加密算法, 验证便宜而计算昂贵), 一旦证实就会停下手里的计算, 承认这个计算结果. 新的计算结果创造出新的块, 区块链的高度增加一层, 然后计算继续下去. 每一个块的生成一般在2-10分钟. 这个过程就被叫做挖矿.
|
||||
我们知道,挖矿是对加密货币产生机制的俗称。主流大多采取 Proof of Work (PoW) 机制。最常见的 PoW 方式就是由网络中所有节点作为矿工,每个节点都基于 blockchain 前面 block 已有信息计算一个新信息。这个新信息的计算方式往往是某种 hash function,并且人为地被设置为需要巨大计算力和时间才能完成 (其具体难度一般也会实时调整)。当一个节点幸运地 (也依靠强大的算力) 第一个计算出结果后,会把这个结果广播到网络中。其他所有节点会验证这个结果 (我们知道非对称加密算法,验证便宜而计算昂贵),一旦证实就会停下手里的计算,承认这个计算结果。新的计算结果创造出新的块,区块链的高度增加一层,然后计算继续下去。每一个块的生成一般在 2-10 分钟。这个过程就被叫做挖矿.
|
||||
|
||||
既然是通用计算, 既然是算一个hash值, 那么民用级CPU和GPU, 浏览器或任何沙盒, 虚拟机, 移动设备当然就都可以. 在我们的例子中, 计算过程被做成分布式, 每个用户可以各自计算, 结果按chunk发回master汇总. 这样就实现了终端用户 - 浏览器 - 共同贡献计算资源 - 换为XMR的过程.
|
||||
既然是通用计算,既然是算一个 hash 值,那么民用级 CPU 和 GPU,浏览器或任何沙盒,虚拟机,移动设备当然就都可以。在我们的例子中,计算过程被做成分布式,每个用户可以各自计算,结果按 chunk 发回 master 汇总。这样就实现了终端用户 - 浏览器 - 共同贡献计算资源 - 换为 XMR 的过程。
|
||||
|
||||
这就是对整个链路的一个描述. 从中我们会生出一些疑问, 比如:
|
||||
这就是对整个链路的一个描述。从中我们会生出一些疑问,比如:
|
||||
|
||||
> 给我看看具体算什么hash? 为什么要算XMR而不是比特币或者其他? 既然第一个算出的赢家通吃所有, 为什么我的收益却是线性的? 这种描述来看岂不是算力最大的一方**永远**都能算出结果而其他人颗粒无收吗?
|
||||
> 给我看看具体算什么 hash?为什么要算 XMR 而不是比特币或者其他?既然第一个算出的赢家通吃所有,为什么我的收益却是线性的?这种描述来看岂不是算力最大的一方**永远**都能算出结果而其他人颗粒无收吗?
|
||||
|
||||
要看具体算法, 没有问题. bitcoin在[这里](http://www.righto.com/2014/02/bitcoin-mining-hard-way-algorithms.html), XMR则看[CryptoNote Standard 008](https://cryptonote.org/cns/cns008.txt)
|
||||
要看具体算法,没有问题。bitcoin 在[这里](http://www.righto.com/2014/02/bitcoin-mining-hard-way-algorithms.html),XMR 则看[CryptoNote Standard 008](https://cryptonote.org/cns/cns008.txt)
|
||||
|
||||
读完两个算法我们就有了以上疑问的答案:
|
||||
|
||||
### 2.1.1 为什么要算XMR而不是比特币或者其他
|
||||
XMR不是唯一选择, 但是BTC是一个不可选的选择. 因为double SHA-265在专业级GPU上会比CPU上快10^4倍 ([更多信息](https://en.bitcoin.it/wiki/Why_a_GPU_mines_faster_than_a_CPU)). 这样一百万用户合力浏览器挖矿还不如一架子双路Titan, 就失去了分布到终端用户的意义. 而CryptoNight在GPU上只比同价值CPU快2倍. 另外CryptoNight算法也被设计为不适用[ASIC](https://en.wikipedia.org/wiki/Application-specific_integrated_circuit).
|
||||
### 2.1.1 为什么要算 XMR 而不是比特币或者其他
|
||||
XMR 不是唯一选择,但是 BTC 是一个不可选的选择。因为 double SHA-265 在专业级 GPU 上会比 CPU 上快 10^4 倍 ([更多信息](https://en.bitcoin.it/wiki/Why_a_GPU_mines_faster_than_a_CPU))。这样一百万用户合力浏览器挖矿还不如一架子双路 Titan,就失去了分布到终端用户的意义。而 CryptoNight 在 GPU 上只比同价值 CPU 快 2 倍。另外 CryptoNight 算法也被设计为不适用[ASIC](https://en.wikipedia.org/wiki/Application-specific_integrated_circuit)。
|
||||
|
||||
怎么实现的? CryptoNight算法开宗明义地写明, 运算主体是Memory-Hard Loop, 而不是Computation-Hard Loop. 每个循环都要在内存中检索. 实际运行CryptoNight时, CPU都会用最快, 最接近ALU也是容量最小的L3 Cache. 换到GPU, 显存虽然很大, 却没有L3 Cache一样的极致读写速度优化, 而且由于内存读写成了瓶颈, GPU中的大量ALU也没了用武之地. 下图简略地描述了CryptoNight循环体的结构:
|
||||
怎么实现的?CryptoNight 算法开宗明义地写明,运算主体是 Memory-Hard Loop,而不是 Computation-Hard Loop。每个循环都要在内存中检索。实际运行 CryptoNight 时,CPU 都会用最快,最接近 ALU 也是容量最小的 L3 Cache。换到 GPU,显存虽然很大,却没有 L3 Cache 一样的极致读写速度优化,而且由于内存读写成了瓶颈,GPU 中的大量 ALU 也没了用武之地。下图简略地描述了 CryptoNight 循环体的结构:
|
||||
|
||||

|
||||
|
||||
### 2.1.2 算力最大的一方永远都能算出结果
|
||||
看了比特币具体算法, 你应该明白了hash是靠不停改变nonce来生成的. 随机取一个值, 算了不对, 再随机取另一个nonce值... 既然是随机取, 就不会存在赢家恒赢.
|
||||
看了比特币具体算法,你应该明白了 hash 是靠不停改变 nonce 来生成的。随机取一个值,算了不对,再随机取另一个 nonce 值..。既然是随机取,就不会存在赢家恒赢.
|
||||
|
||||
### 2.1.3 为什么我的收益却是线性的
|
||||
这是一个隐蔽但是却非常重要的问题. 答案是, 本来确实是赢家通吃. 如果你的算力足够大, 挖矿时间足够长之后总会轮到你, 但是收益会有大幅波动.
|
||||
这是一个隐蔽但是却非常重要的问题。答案是,本来确实是赢家通吃。如果你的算力足够大,挖矿时间足够长之后总会轮到你,但是收益会有大幅波动.
|
||||
|
||||
就是因为如此, 矿工们逐渐建立了矿池组织. 大家把算力都投入到一起, 合力算, 然后不管这次实际是谁算出来, 都按照贡献的算力比例分配收益. 矿池是一个加密货币建立之初, 完全推崇去中心化时没有预料到的结构, 也产生了深远的影响. 现在来自中国矿池的算力早已超过网络50%, 他们会在挖出的块中打上矿池标记, 而这些矿池在加密货币的分叉, 路线图中也扮演举足轻重的角色.
|
||||
就是因为如此,矿工们逐渐建立了矿池组织。大家把算力都投入到一起,合力算,然后不管这次实际是谁算出来,都按照贡献的算力比例分配收益。矿池是一个加密货币建立之初,完全推崇去中心化时没有预料到的结构,也产生了深远的影响。现在来自中国矿池的算力早已超过网络 50%,他们会在挖出的块中打上矿池标记,而这些矿池在加密货币的分叉,路线图中也扮演举足轻重的角色.
|
||||
|
||||
所以你的算力并不是直接投入XMR网络中, 而是投入一个矿池. 在我们的例子里矿池就是coinhive, 只不过是一个比较特殊的矿池, 特殊在矿池成员都运作在浏览器中. 这就是为什么你会得到线性收益而不是all or none.
|
||||
所以你的算力并不是直接投入 XMR 网络中,而是投入一个矿池。在我们的例子里矿池就是 coinhive,只不过是一个比较特殊的矿池,特殊在矿池成员都运作在浏览器中。这就是为什么你会得到线性收益而不是 all or none.
|
||||
|
||||
自古以来各行业都会自发产生行业工会, 建立类似保险和行业守则 / 规范这些人人为我我为人人的机制. 在crypto行业也不例外. 这是意料之外而情理之中.
|
||||
自古以来各行业都会自发产生行业工会,建立类似保险和行业守则 / 规范这些人人为我我为人人的机制。在 crypto 行业也不例外。这是意料之外而情理之中.
|
||||
|
||||
## 2.2 在浏览器里发生了什么, 或, coinhive干了什么
|
||||
好, 我们搞清了一些基本的Monero挖矿机制, 下面来看看coinhive. 已经知道coinhive帮我们接入它的矿池, 让再小的算力也能按比例得到产出. 但是还有什么呢? 最关键的一点, coinhive是怎样把一个完整的mining过程拆分成小块, 让一个或许并不强大的设备上的浏览器, 也能快速接收task, 快速完成并且即时上传的呢?
|
||||
## 2.2 在浏览器里发生了什么,或 coinhive 干了什么
|
||||
好,我们搞清了一些基本的 Monero 挖矿机制,下面来看看 coinhive。已经知道 coinhive 帮我们接入它的矿池,让再小的算力也能按比例得到产出。但是还有什么呢?最关键的一点,coinhive 是怎样把一个完整的 mining 过程拆分成小块,让一个或许并不强大的设备上的浏览器,也能快速接收 task,快速完成并且即时上传的呢?
|
||||
|
||||
废话不多说, 打开源码. 本项目没有开源, 构建完成后的在https://coinhive.com/lib/coinhive.min.js. 先做初步format处理, 发现有些工作完成在后端, worker shard一侧. 以下用松散的伪码总结一下client side的[main success scenario](https://wiki.ihris.org/wiki/Understanding_Use_Cases)流程 (注意很多地方简化了):
|
||||
废话不多说,打开源码。本项目没有开源,构建完成后的在https://coinhive.com/lib/coinhive.min.js。先做初步 format 处理,发现有些工作完成在后端,worker shard 一侧。以下用松散的伪码总结一下 client side 的[main success scenario](https://wiki.ihris.org/wiki/Understanding_Use_Cases)流程 (注意很多地方简化了):
|
||||
|
||||
```
|
||||
```plain
|
||||
- start
|
||||
- loadWorkerResource
|
||||
- load worker-asmjs.min.js
|
||||
@@ -107,36 +107,36 @@ XMR不是唯一选择, 但是BTC是一个不可选的选择. 因为double SHA-26
|
||||
} else {
|
||||
this.worker.postMessage(job)
|
||||
}
|
||||
// 实例化若干个JobThread, 每个对应一个worker, worker实际执行asmjs.min.js
|
||||
- _connect // verify成功, 终于建立连接. 根据public key固定hash到一个shard池然后随机选一个shard, 建立websocket
|
||||
// 实例化若干个JobThread,每个对应一个worker,worker实际执行asmjs.min.js
|
||||
- _connect // verify成功,终于建立连接。根据public key固定hash到一个shard池然后随机选一个shard,建立websocket
|
||||
- websocket.onmessage: if (type==job) work()
|
||||
- work:
|
||||
do { hash(input, output) } while !(meetTarget(output));
|
||||
websocket.postMessage({nonce, output}) // hash done successfully. submit
|
||||
do { hash(input,output) } while !(meetTarget(output));
|
||||
websocket.postMessage({nonce,output}) // hash done successfully。submit
|
||||
```
|
||||
|
||||
看一下这个过程, 结合[cns003 XMR blockchain specs](https://cryptonote.org/cns/cns003.txt). XMR的整体hash input很小, 是:
|
||||
```
|
||||
- size of [block_header, Merkle root hash, and the number of
|
||||
看一下这个过程,结合[cns003 XMR blockchain specs](https://cryptonote.org/cns/cns003.txt)。XMR 的整体 hash input 很小,是:
|
||||
```plain
|
||||
- size of [block_header,Merkle root hash,and the number of
|
||||
transactions] in bytes (varint)
|
||||
- block_header,
|
||||
- Merkle root hash,
|
||||
- number of transactions (varint).
|
||||
```
|
||||
|
||||
这样用websocket发过来毫无问题. 之后就是完全独立的计算, 调整nonce来算不同的hash结果. target就是当前难度的一个指示.
|
||||
这样用 websocket 发过来毫无问题。之后就是完全独立的计算,调整 nonce 来算不同的 hash 结果。target 就是当前难度的一个指示.
|
||||
|
||||
这样整条链路就比较清晰了. 再思考一下以下问题:
|
||||
这样整条链路就比较清晰了。再思考一下以下问题:
|
||||
|
||||
### 2.2.1 为什么XMR适宜分布式客户端计算
|
||||
因为能够利用每个用户的CPU和其中的**高速L3 Cache**. 这是中心化执行难以具备的条件.
|
||||
### 2.2.1 为什么 XMR 适宜分布式客户端计算
|
||||
因为能够利用每个用户的 CPU 和其中的**高速 L3 Cache**。这是中心化执行难以具备的条件.
|
||||
|
||||
任何时候当考虑要不要把某项操作推到客户端进行时, 都要想明白可以利用客户端的哪个资源, 这个资源在客户端是否有明显优势, 是否比后端中心化执行更有利. **很多时候答案是, 优势并不明显. 那么引入的网络通信成本, 法规成本, 额外开销可能就并不值得.**
|
||||
任何时候当考虑要不要把某项操作推到客户端进行时,都要想明白可以利用客户端的哪个资源,这个资源在客户端是否有明显优势,是否比后端中心化执行更有利。**很多时候答案是,优势并不明显。那么引入的网络通信成本,法规成本,额外开销可能就并不值得.**
|
||||
|
||||
## 2.3 最后的步骤
|
||||
到了这里, 最后剩余步骤就很标准模式化了. coinhive作为矿场, 代管着用户生产的加密货币. 用户发请求提出XMR, 就需要提到一个自己的钱包地址保管, 比如[MyMonero](https://mymonero.com). 也可以直接提到Exchange交易所, 在其中交易成其他币种, 包括法币, 然后电汇等等形式提现.
|
||||
到了这里,最后剩余步骤就很标准模式化了。coinhive 作为矿场,代管着用户生产的加密货币。用户发请求提出 XMR,就需要提到一个自己的钱包地址保管,比如 [MyMonero](https://mymonero.com)。也可以直接提到 Exchange 交易所,在其中交易成其他币种,包括法币,然后电汇等等形式提现.
|
||||
|
||||
如果对钱包或者交易所感兴趣 (这两个也是很大的话题, 比如钱包分硬件钱包和软件钱包, 离线冷钱包和线上热钱包, private key和recover seeds. 交易所有多种多样的交易对, 有杠杆, 期货, 空和多, 多种挂单类型等等), 可以看[这里](https://www.blockchain-council.org/cryptocurrency/hot-wallet-vs-cold-wallet/)和[这里](https://github.com/txbits/txbits).
|
||||
如果对钱包或者交易所感兴趣 (这两个也是很大的话题,比如钱包分硬件钱包和软件钱包,离线冷钱包和线上热钱包,private key 和 recover seeds。交易所有多种多样的交易对,有杠杆,期货,空和多,多种挂单类型等等),可以看[这里](https://www.blockchain-council.org/cryptocurrency/hot-wallet-vs-cold-wallet/)和[这里](https://github.com/txbits/txbits).
|
||||
|
||||
# 3 更多讨论
|
||||
|
||||
@@ -152,7 +152,7 @@ https://en.wikipedia.org/wiki/Monero_(cryptocurrency)
|
||||
http://www.righto.com/2014/02/bitcoin-mining-hard-way-algorithms.html
|
||||
|
||||
https://cryptonote.org/cns/cns001.txt
|
||||
cns001-008是Specs集合
|
||||
cns001-008 是 Specs 集合
|
||||
|
||||
https://en.bitcoin.it/wiki/Why_a_GPU_mines_faster_than_a_CPU
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## 引言
|
||||
2018 年初,蚂蚁金服 See Conf 上第一个分享《Ant Design 3.0 背后的故事》给很多人带来了启发。主题精彩又深刻,值得反复咀嚼。
|
||||
|
||||
## 内容概要
|
||||
## 内容概要
|
||||
### 设计体系
|
||||
Atomic Design 书中提到模块化思路以及原子级的模块抽象的方法启发了很多设计师。而解读者中较为说法较作者认同。设计体系是一个具包容性且充满生命力的东西。包容性指的是从组件库到设计语言到设计方法等所有和产品设计相关的方面。而生命力指的是它并非静态的内容,而是可以应对不断变化的环境,是一个不断进化的过程。
|
||||
|
||||
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
# 增强现实与可视化
|
||||
## 引言
|
||||
增强现实,Augmented Reality,简称AR。在VR的热潮已经褪去,AI当下正红的技术圈里,AR似乎已经成为了过气网红。但似乎在现在,我们可以来冷静地看待一下增强现实这个概念。
|
||||
增强现实,Augmented Reality,简称 AR。在 VR 的热潮已经褪去,AI 当下正红的技术圈里,AR 似乎已经成为了过气网红。但似乎在现在,我们可以来冷静地看待一下增强现实这个概念。
|
||||
|
||||
做前端的最终还是要和用户打交道的,增强现实是不是为用户界面带来了新的可能?可视化作为前端的一个细分领域,这篇文章正是从这个角度出发来重新认识增强现实与可视化。
|
||||
|
||||
## 内容概要
|
||||
|
||||
移动设备为我们带来了巨大的便利,但是在各种好处下,有一点不容忽视:屏幕太小,根本没有更多的空间来放置更多的内容。对于数据可视化而言,更大的空间意味着更好的数据分析能力。而在空间这件事情上,AR正是很好地解决了这个问题。当下,AR的主要形态还是将虚拟信息作为图层叠加在现实图像之上的形态,这样的AR其实更接近于混合现实。例如使用面部追踪给你脸上加上小动物的Snapchat Lenses,可以在沙发茶几上玩MineCraft的微软HoloLens,和苹果最近新出的ARKit都是这种。
|
||||
移动设备为我们带来了巨大的便利,但是在各种好处下,有一点不容忽视:屏幕太小,根本没有更多的空间来放置更多的内容。对于数据可视化而言,更大的空间意味着更好的数据分析能力。而在空间这件事情上,AR 正是很好地解决了这个问题。当下,AR 的主要形态还是将虚拟信息作为图层叠加在现实图像之上的形态,这样的 AR 其实更接近于混合现实。例如使用面部追踪给你脸上加上小动物的 Snapchat Lenses,可以在沙发茶几上玩 MineCraft 的微软 HoloLens,和苹果最近新出的 ARKit 都是这种。
|
||||
|
||||
所以要怎么通过增强现实来解决当下移动端屏幕空间不足的问题呢?在AR的概念下,一切皆屏幕,现实之上的任何地方都可以展示信息。对于增强现实的未来,文章给出了3个可能的方向。首先,需要提供更好的针对个人的视觉体验。其次,要更好地利用3D展示。最后,让屏幕悬浮在任何地方。
|
||||
所以要怎么通过增强现实来解决当下移动端屏幕空间不足的问题呢?在 AR 的概念下,一切皆屏幕,现实之上的任何地方都可以展示信息。对于增强现实的未来,文章给出了 3 个可能的方向。首先,需要提供更好的针对个人的视觉体验。其次,要更好地利用 3D 展示。最后,让屏幕悬浮在任何地方。
|
||||
|
||||
但是我们也不妨从反面重新理性地看到了增强现实。是不是有了足够空间展示东西我们就要把东西一股脑都展示出来呢?并不是的,增强现实是和真实世界的混合。所以增强现实下的可视化,也不是也不是简单地照搬网页。在增强现实下,我们所展示的每一个虚拟物体,是需要有现实依据的。但是也不必拘泥于现实,增强现实也可以改造所展示的世界。增强现实并不是简单的做加法,只能构建出各类虚拟物品叠加在世界上。同样也能改造世界,使事物变形和消失。
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
|
||||
前端是什么?对于这样一个本源性的话题。每个人都有自己不同的理解。我也不会说书式得非得确定这个问题的答案。但是可以确定的是,如果一切的描述要带上设备端的前提,是短视的。
|
||||
|
||||
从PC Web到移动App,前端并没有消失,各大网站也都纷纷开始将自己的网站搬到了手机上以移动APP的形式存在。同样的,如果有一天增强现实来到我们的生活。前端作为和用户界面直接接触的工作,毫无疑问已经会是很重要的一部分。
|
||||
从 PC Web 到移动 App,前端并没有消失,各大网站也都纷纷开始将自己的网站搬到了手机上以移动 APP 的形式存在。同样的,如果有一天增强现实来到我们的生活。前端作为和用户界面直接接触的工作,毫无疑问已经会是很重要的一部分。
|
||||
|
||||
再看可视化。移动化的大潮让我们开始习惯移动设备,触屏之类的交互的确让我们的信息获取变得更加方便。但是这并不代表全部,特殊行业的人依旧离不开传统的PC,尤其是数据分析人员,对于他们而言,更大的屏幕等于一切。尤其是金融分析师,你给他们四块屏幕都不够用。增强现实下的可视化的确是对这些人的一个重要利好。原因无非就是增强现实的可展现区域相当于整个人眼的视角,远远大于任何我们可以感知到的屏幕。
|
||||
再看可视化。移动化的大潮让我们开始习惯移动设备,触屏之类的交互的确让我们的信息获取变得更加方便。但是这并不代表全部,特殊行业的人依旧离不开传统的 PC,尤其是数据分析人员,对于他们而言,更大的屏幕等于一切。尤其是金融分析师,你给他们四块屏幕都不够用。增强现实下的可视化的确是对这些人的一个重要利好。原因无非就是增强现实的可展现区域相当于整个人眼的视角,远远大于任何我们可以感知到的屏幕。
|
||||
|
||||
如果只是在我们的眼睛上盖上一层信息,那是远远不够的,这样的增强现实仅仅是对屏幕的扩展。增强现实最关键的一点是和现实世界的充分结合:例如配合地理信息和我们的地理定位所带来的信息可视化展示;通过图像识别对物体的感知后,在物体上进行信息可视化展示;等等。文章的几个建议中,最后一个特别有趣——让屏幕悬浮在任何地方。这有两层含义,一是要充分利用好上面所提到的增强现实下的空间,二是我们需要清楚地认识到,人类对于二维世界的感知能力还是远远超出三维世界的。过度利用现实世界所构建出的3D场景,可能并不能为我们的分析带来太多进步。所以,二维的图形信息展示可能依然是增强现实可视化中很重要的一部分。
|
||||
如果只是在我们的眼睛上盖上一层信息,那是远远不够的,这样的增强现实仅仅是对屏幕的扩展。增强现实最关键的一点是和现实世界的充分结合:例如配合地理信息和我们的地理定位所带来的信息可视化展示;通过图像识别对物体的感知后,在物体上进行信息可视化展示;等等。文章的几个建议中,最后一个特别有趣——让屏幕悬浮在任何地方。这有两层含义,一是要充分利用好上面所提到的增强现实下的空间,二是我们需要清楚地认识到,人类对于二维世界的感知能力还是远远超出三维世界的。过度利用现实世界所构建出的 3D 场景,可能并不能为我们的分析带来太多进步。所以,二维的图形信息展示可能依然是增强现实可视化中很重要的一部分。
|
||||
|
||||
这篇文章更加引起我兴趣的地方在于它对于可视化基本元素进行了分析——颜色,亮度,纹理,尺寸,方向、形状、位置……这些从我们平时的显示器屏幕或者手机屏幕上所展示的可视化元素会带来完全不一样的表现。增强现实的增强在于我们可以对现实的展示进行改造,纹理是很有趣的一块。一般来说我们平时做可视化分析,常用到阴影,虚线这些纹理,但是现实世界里的纹理就太丰富了,我们可以充分利用现实花纹了,让展现更直观更融合。
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Rekit Studio 以逻辑视图重新组织了项目,文件目录不见了,取
|
||||
|
||||
同时支持在线管理本地文件、集成了 [https://microsoft.github.io/monaco-editor/](monaco-editor) 在线编辑文件,以及在线构建、测试等功能。
|
||||
|
||||
同时利用和弦图分析了路由与数据流之间绑定关系,路由与文件绑定关系,可以很轻松找到被遗弃的孤立节点。项目维护时,以看图代替看代码,效率至少提升2 3倍。
|
||||
同时利用和弦图分析了路由与数据流之间绑定关系,路由与文件绑定关系,可以很轻松找到被遗弃的孤立节点。项目维护时,以看图代替看代码,效率至少提升 2 3 倍。
|
||||
|
||||
### Cli 与可视化等效
|
||||
|
||||
|
||||
+13
-13
@@ -1,33 +1,33 @@
|
||||
# 精读《快速上手构建ARKit应用》
|
||||
# 精读《快速上手构建 ARKit 应用》
|
||||
|
||||
原文地址: [how-to-make-your-own-arkit-app-in-5-minutes-using-react-native](https://medium.com/@HippoAR/how-to-make-your-own-arkit-app-in-5-minutes-using-react-native-9d7ce109a4c2)
|
||||
|
||||
## 引言
|
||||
ARKit是苹果推出的增强现实套装,而react-native-arkit是基于此的上层封装。对于前端开发而言,这可能是最快上手ARKit的方式了,本周精读让我们来初窥ARKit和React Native ARKit这个库。
|
||||
ARKit 是苹果推出的增强现实套装,而 react-native-arkit 是基于此的上层封装。对于前端开发而言,这可能是最快上手 ARKit 的方式了,本周精读让我们来初窥 ARKit 和 React Native ARKit 这个库。
|
||||
|
||||
## 概要
|
||||
本次精读我们带来的是一篇《快速上手构建ARKit应用》,原文链接如上。原文标题更加直接,直译的话是“如何在5分钟里利用react native搭建出你自己的ARKit应用”。确实,这篇文章整体也非常明确,以跑起整个ARKit Demo为最直接最主要的目的。
|
||||
本次精读我们带来的是一篇《快速上手构建 ARKit 应用》,原文链接如上。原文标题更加直接,直译的话是“如何在 5 分钟里利用 react native 搭建出你自己的 ARKit 应用”。确实,这篇文章整体也非常明确,以跑起整个 ARKit Demo 为最直接最主要的目的。
|
||||
|
||||
跑起ARKit,也很简单。硬件上,只要有一台iPhone 6S以上的手机;软件上,只要准备好最新版本的XCode和日常开发要用的Node环境了就好。按照`react-native-arkit`的里面的README就可以跑起来了。这个库不
|
||||
跑起 ARKit,也很简单。硬件上,只要有一台 iPhone 6S 以上的手机;软件上,只要准备好最新版本的 XCode 和日常开发要用的 Node 环境了就好。按照`react-native-arkit`的里面的 README 就可以跑起来了。这个库不
|
||||
|
||||
## 3 精读
|
||||
在开始精读前,我先抛出我的问题三连:Why AR? Why ARKit? Why React Native ARKit?
|
||||
|
||||
### 3.1 Why AR?
|
||||
在之前的第43期精读评论中,我们探讨了AR对于和前端结合的可能性。总的来说,AR把前端开发不再局限在有限的屏幕空间上,对于可视化等对前端展示空间有强烈需求的细分领域,AR是一个很值得研究的内容。如果对于这一块内容有兴趣,欢迎回看第43期精读评论 [《精读〈增强现实与可视化〉》](https://github.com/dt-fe/weekly/blob/master/43.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A2%9E%E5%BC%BA%E7%8E%B0%E5%AE%9E%E4%B8%8E%E5%8F%AF%E8%A7%86%E5%8C%96%E3%80%8B.md)。
|
||||
在之前的第 43 期精读评论中,我们探讨了 AR 对于和前端结合的可能性。总的来说,AR 把前端开发不再局限在有限的屏幕空间上,对于可视化等对前端展示空间有强烈需求的细分领域,AR 是一个很值得研究的内容。如果对于这一块内容有兴趣,欢迎回看第 43 期精读评论 [《精读〈增强现实与可视化〉》](https://github.com/dt-fe/weekly/blob/master/43.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A2%9E%E5%BC%BA%E7%8E%B0%E5%AE%9E%E4%B8%8E%E5%8F%AF%E8%A7%86%E5%8C%96%E3%80%8B.md)。
|
||||
|
||||
### 3.2 Why ARKit?
|
||||
为什么选择 ARKit 入手进行实验?其因有二。第一,相比于 Microsoft HoloLens 的价格,售价只有它三分之一的iPhone X无论是体积重量,还是性价比,抑或是保有量都是大大占优的。噢对,说到保有量,iPhone 6S及以上都支持ARKit。所以说iPhone是我们身边最容易接触到的AR设备是不为过的。第二,ARKit对于硬件的利用能力非一般的前端库可以做到的。大部分的AR前端库可以做到利用陀螺仪来构建一个三维立体空间。但是ARKit更进一步,他利用高频调用摄像头,通过对图像进行识别分析,可以进行空间感知,例如可以识别出一个平面。而这些都是ARKit所提供的,我们只需要调用它的能力就好了。对于开发者而言,ARKit会比一般的AR库更近一步。
|
||||
为什么选择 ARKit 入手进行实验?其因有二。第一,相比于 Microsoft HoloLens 的价格,售价只有它三分之一的 iPhone X 无论是体积重量,还是性价比,抑或是保有量都是大大占优的。噢对,说到保有量,iPhone 6S 及以上都支持 ARKit。所以说 iPhone 是我们身边最容易接触到的 AR 设备是不为过的。第二,ARKit 对于硬件的利用能力非一般的前端库可以做到的。大部分的 AR 前端库可以做到利用陀螺仪来构建一个三维立体空间。但是 ARKit 更进一步,他利用高频调用摄像头,通过对图像进行识别分析,可以进行空间感知,例如可以识别出一个平面。而这些都是 ARKit 所提供的,我们只需要调用它的能力就好了。对于开发者而言,ARKit 会比一般的 AR 库更近一步。
|
||||
|
||||
### 3.3 Why React Native ARKit?
|
||||
对于当下的前端开发,所有事情可以分为两种——0. 可以用 JavaScript 写的 1. 其他。至于为什么选择`react-native-arkit`这个库,原因自然也可以理解。相比于用原生的Swift来开发,React Native 的开发方式对于前端而言明显是更加容易上手了。对于尝试新东西,这也未尝不可。
|
||||
对于当下的前端开发,所有事情可以分为两种——0. 可以用 JavaScript 写的 1. 其他。至于为什么选择`react-native-arkit`这个库,原因自然也可以理解。相比于用原生的 Swift 来开发,React Native 的开发方式对于前端而言明显是更加容易上手了。对于尝试新东西,这也未尝不可。
|
||||
|
||||
### 3.4 About Demo
|
||||
相比于原文中从初始化开始的步骤,官方还提供了一个已经配置好的[官方Demo](https://github.com/HippoAR/ReactNativeARKit)。使用这个,如果环境没有问题,的确只需要5分钟就可以跑起来一个ARKit应用了。
|
||||
相比于原文中从初始化开始的步骤,官方还提供了一个已经配置好的[官方 Demo](https://github.com/HippoAR/ReactNativeARKit)。使用这个,如果环境没有问题,的确只需要 5 分钟就可以跑起来一个 ARKit 应用了。
|
||||
|
||||

|
||||
|
||||
上面的图片来自原文,可以看到,在`react-native-arkit`这个库里面的所支持的9种基本图形和文字。使用如下已经封装好的React Native组件就可以直接使用了。
|
||||
上面的图片来自原文,可以看到,在`react-native-arkit`这个库里面的所支持的 9 种基本图形和文字。使用如下已经封装好的 React Native 组件就可以直接使用了。
|
||||
|
||||
```javasctipt
|
||||
<ARKit.Box
|
||||
@@ -37,16 +37,16 @@ ARKit是苹果推出的增强现实套装,而react-native-arkit是基于此的
|
||||
```
|
||||
|
||||
[几何构造](http://v.youku.com/v_show/id_XMzUxMjk3NjUxMg==.html)
|
||||
上面的一个视频片段是我们在跑起来Demo后的立体效果。可以很清楚地看到,ARKit感知到了房间这个立方体空间后所构建出来的AR的效果。
|
||||
上面的一个视频片段是我们在跑起来 Demo 后的立体效果。可以很清楚地看到,ARKit 感知到了房间这个立方体空间后所构建出来的 AR 的效果。
|
||||
|
||||
[平面识别](http://v.youku.com/v_show/id_XMzUxMjk3OTc0NA==.html)
|
||||
而最后的这段视频会更加有趣一些,中央的红圈的出现逻辑是停留在最近识别出的一个平面上。我们可以看到首先识别出了地面,红圈随地面而动;再移向桌面时,很快又识别出了桌面,重新生成了一个停留在桌面上的红圈。通过这一段可以看出无论是明暗划分明显的地面,还是堆满杂物的桌面,ARKit都可以很轻松的识别出来。
|
||||
而最后的这段视频会更加有趣一些,中央的红圈的出现逻辑是停留在最近识别出的一个平面上。我们可以看到首先识别出了地面,红圈随地面而动;再移向桌面时,很快又识别出了桌面,重新生成了一个停留在桌面上的红圈。通过这一段可以看出无论是明暗划分明显的地面,还是堆满杂物的桌面,ARKit 都可以很轻松的识别出来。
|
||||
|
||||
## 4. 总结
|
||||
苹果的ARKit对空间平面的感知能力胜过了一般的AR渲染库。而iPhone 6S就能跑的特性又让我们觉得AR其实并没有那么遥远。在此基础之上的React Native封装`react-native-arkit`,让我们通过JS就拥有操作ARKit的能力。这的确是一个快速上手ARKit的方式。
|
||||
苹果的 ARKit 对空间平面的感知能力胜过了一般的 AR 渲染库。而 iPhone 6S 就能跑的特性又让我们觉得 AR 其实并没有那么遥远。在此基础之上的 React Native 封装`react-native-arkit`,让我们通过 JS 就拥有操作 ARKit 的能力。这的确是一个快速上手 ARKit 的方式。
|
||||
|
||||
|
||||
## 5 更多讨论
|
||||
讨论地址是:[精读《快速上手构建ARKit应用》 · Issue #70 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/70)
|
||||
讨论地址是:[精读《快速上手构建 ARKit 应用》 · Issue #70 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/70)
|
||||
|
||||
如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周末发布。
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 1 引言
|
||||
|
||||
本周精读, 来一起总结web开发的环节, 知识块和技能点. 是不是像xx速成班宣传的一样, 培训三个月, 经验顶三年, 入职BAT, 年薪三十万?
|
||||
本周精读, 来一起总结 web 开发的环节, 知识块和技能点. 是不是像 xx 速成班宣传的一样, 培训三个月, 经验顶三年, 入职 BAT, 年薪三十万?
|
||||
|
||||
本文虽然是罗列知识点, 但我想很有意义. 对于学习的人来说, 提供一个路线图. 对从业者来说, 对全局有更好的把控, 利于看到自己的强项和不足. 对组建团队, 更能起到一个点将谱的作用.
|
||||
|
||||
在网上我没有搜到任何深入全面的总结, 提供的那几篇已经算稍微好一些的了. 其他的要么太过笼统(前端-后端-数据-运维, 完毕)要么太细太窄(并不是不好, 只是和本文性质不一样). Generalist和Specialist之间永远是一对辩证矛盾, 持续思考.
|
||||
在网上我没有搜到任何深入全面的总结, 提供的那几篇已经算稍微好一些的了. 其他的要么太过笼统(前端-后端-数据-运维, 完毕)要么太细太窄(并不是不好, 只是和本文性质不一样). Generalist 和 Specialist 之间永远是一对辩证矛盾, 持续思考.
|
||||
|
||||
本文提供了有层级的列表形式, 如果有兴趣的读者可以把它做成概念图形式, 相互关联与距离相关, 可能会有意料之外的效果.
|
||||
|
||||
|
||||
+2
-2
@@ -94,9 +94,9 @@ CJS 的做法很不同,主要是由于相对于通过网络请求从文件系
|
||||
|
||||

|
||||
|
||||
在浏览器中你只要将 `type="module" ` 放在 script 标签上。这会通知浏览器这个文件应该被转化为一个模块。同样,只有模块才能够被导入,浏览器也就知道了模块中有哪些引用。
|
||||
在浏览器中你只要将 `type="module"` 放在 script 标签上。这会通知浏览器这个文件应该被转化为一个模块。同样,只有模块才能够被导入,浏览器也就知道了模块中有哪些引用。
|
||||
|
||||
不过在 Node 中,并没有 HTML 标签,所以也没有地方声明 type 属性。社区内的一种方式就是使用 `.mjs` 扩展。使用这个扩展告诉 Node这个文件是一个模块。
|
||||
不过在 Node 中,并没有 HTML 标签,所以也没有地方声明 type 属性。社区内的一种方式就是使用 `.mjs` 扩展。使用这个扩展告诉 Node 这个文件是一个模块。
|
||||
|
||||
无论哪种方式,加载器将决定是否将文件转化为一个模块。如果是一个模块并且有导入的话,它就会开始处理直到所有的文件被获取和转化。
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ const incrementAsync = async count => {
|
||||
|
||||
### 将 action + reducer 改为两种 action
|
||||
|
||||
redux 抽象的 action 与 reducer 的指责很清晰,action 负责改 store 以外所有事,而 reducer 负责改 store,偶尔用来做数据处理。这种概念其实比较模糊,因为往往不清楚数据处理放在 action 还是 reducer 里,同时过于简单的 reducer 又要写 action 与之匹配,感觉过于形式化,而且繁琐。
|
||||
redux 抽象的 action 与 reducer 的职责很清晰,action 负责改 store 以外所有事,而 reducer 负责改 store,偶尔用来做数据处理。这种概念其实比较模糊,因为往往不清楚数据处理放在 action 还是 reducer 里,同时过于简单的 reducer 又要写 action 与之匹配,感觉过于形式化,而且繁琐。
|
||||
|
||||
重新考虑这个问题,我们只有两类 action:`reducer action` 与 `effect action`。
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ function getTokenBlockComment(restStr: string) {
|
||||
```typescript
|
||||
while (sqlStr) {
|
||||
token =
|
||||
getTokenWhitespace(sqlStr, token) | getTokenBlockComment(sqlStr, token);
|
||||
getTokenWhitespace(sqlStr, token) || getTokenBlockComment(sqlStr, token);
|
||||
|
||||
sqlStr = sqlStr.substring(token.value.length);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
我们将一块语法规则称为 **产生式**,使用 “Left → Right” 表示任意产生式,用 “Left => Right” 表示产生式的推导过程,比如对于产生式:
|
||||
|
||||
```
|
||||
```plain
|
||||
E → i
|
||||
E → E + E
|
||||
```
|
||||
@@ -17,7 +17,7 @@ E → E + E
|
||||
|
||||
举个例子,比如 `SELECT * FROM table` 可以被表达为:
|
||||
|
||||
```
|
||||
```plain
|
||||
S → SELECT * FROM table
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ S → SELECT * FROM table
|
||||
|
||||
终结符就是语句的终结,读到它表示产生式分析结束,相反,非终结符就是一个新产生式的开始,比如:
|
||||
|
||||
```
|
||||
```plain
|
||||
<selectStatement> ::= SELECT <selectList> FROM <tableName>
|
||||
|
||||
<selectList> ::= <selectField> [ , <selectList> ]
|
||||
@@ -43,7 +43,7 @@ S → SELECT * FROM table
|
||||
|
||||
对于有二义性的文法,可以通过 **上下文相关文法** 方式描述,也就是在产生式左侧补全条件,解决二义性:
|
||||
|
||||
```
|
||||
```plain
|
||||
aBc -> a1c | a2c
|
||||
dBe -> d3e
|
||||
```
|
||||
@@ -52,7 +52,7 @@ dBe -> d3e
|
||||
|
||||
上面表示,非终结符 `B` 在 `ac` 之间时,可以解析为 `1` 或 `2`,而在 `de` 之间时,解析为 `3`。但我们可以增加一个非终结符让产生式可读性更好:
|
||||
|
||||
```
|
||||
```plain
|
||||
B -> 1 | 2
|
||||
C -> 3
|
||||
```
|
||||
@@ -80,13 +80,13 @@ 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)。
|
||||
|
||||
### 左推导与右推导
|
||||
|
||||
上面提到的推导符号 `=>` 在实际运行过程中,显然有两种方向左和右:
|
||||
|
||||
```
|
||||
```plain
|
||||
E + E => ?
|
||||
```
|
||||
|
||||
@@ -100,7 +100,7 @@ E + E => ?
|
||||
|
||||
比如 `select <selectList>` 的 `selectList` 产生式,它可以表示为:
|
||||
|
||||
```
|
||||
```plain
|
||||
<SelectList> ::= <SelectList> , <SelectField>
|
||||
| <SelectField>
|
||||
```
|
||||
@@ -115,7 +115,7 @@ E + E => ?
|
||||
|
||||
> Token 见上一期精读 [精读《手写 SQL 编译器 - 词法分析》](https://github.com/dt-fe/weekly/blob/master/64.%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)
|
||||
|
||||
```
|
||||
```plain
|
||||
<SelectList> ::= <SelectField> <G>
|
||||
|
||||
<G> ::= , <SelectList>
|
||||
@@ -124,12 +124,12 @@ E + E => ?
|
||||
|
||||
这其实是一个通用处理,可以抽象出来:
|
||||
|
||||
```
|
||||
```plain
|
||||
E → E + F
|
||||
E → F
|
||||
```
|
||||
|
||||
```
|
||||
```plain
|
||||
E → FG
|
||||
G → + FG
|
||||
G → null
|
||||
@@ -139,7 +139,7 @@ G → null
|
||||
|
||||
笔者建议此处不要生硬的套公式,在套了公式后,再对产生式做一些修饰,让其更具有语义:
|
||||
|
||||
```
|
||||
```plain
|
||||
<SelectList> ::= <SelectField>
|
||||
| , <SelectList>
|
||||
```
|
||||
@@ -152,7 +152,7 @@ G → null
|
||||
|
||||
设想如下的 sql 文法:
|
||||
|
||||
```
|
||||
```plain
|
||||
<Field> ::= <Text> as <Text>
|
||||
| <Text> as<String>
|
||||
| <Text> <Text>
|
||||
@@ -161,7 +161,7 @@ G → null
|
||||
|
||||
其实 Text 本身也是比较复杂的产生式,最坏的情况需要对 Text 连续匹配六遍。我们将 Text 公因式提取出来就可以仅匹配一遍,因为无论是何种 Field 产生式,都必定先遇到 Text:
|
||||
|
||||
```
|
||||
```plain
|
||||
<Field> ::= <Text> <F>
|
||||
|
||||
<F> ::= <G>
|
||||
|
||||
@@ -131,7 +131,7 @@ const selectList =
|
||||
|
||||
显然这样做不具备通用性,因为我们将参数名与数量固定了。考虑到上期精读学到的[文法](https://github.com/dt-fe/weekly/blob/master/65.%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),我们可以这样描述 `selectList`:
|
||||
|
||||
```
|
||||
```plain
|
||||
selectList ::= word (',' selectList)?
|
||||
word ::= [a-zA-Z]
|
||||
```
|
||||
@@ -183,7 +183,7 @@ const field = () => word()
|
||||
|
||||
这时注意 `field` 作为一个字段,也可能是文本或函数,我们假设拥有函数处理函数 `functional`,那么用文法描述 `field` 就是:
|
||||
|
||||
```
|
||||
```plain
|
||||
field ::= text | functional
|
||||
```
|
||||
|
||||
@@ -228,7 +228,7 @@ function tree(...args: any[]) {
|
||||
|
||||
可选函数就是分支函数的一个特例,可以描述为:
|
||||
|
||||
```
|
||||
```plain
|
||||
func? => func | ε
|
||||
```
|
||||
|
||||
@@ -244,25 +244,25 @@ const optional = fn => tree(fn, () => true)
|
||||
|
||||
上面通过对 SQL 语句的实践,发现了 `match` 匹配单个单词、 `&&` 连接、`tree` 分支、`ε` 空字符串的产生式这四种基本用法,这是符合下面四个基本文法组合思想的:
|
||||
|
||||
```
|
||||
```plain
|
||||
G ::= ε
|
||||
```
|
||||
|
||||
空字符串产生式,对应 `() => true`,不消耗 Token,总是返回 `true`。
|
||||
|
||||
```
|
||||
```plain
|
||||
G ::= t
|
||||
```
|
||||
|
||||
单词匹配,对应 `match(t)`。
|
||||
|
||||
```
|
||||
```plain
|
||||
G ::= x y
|
||||
```
|
||||
|
||||
连接运算,对应 `match(x) && match(y)`。
|
||||
|
||||
```
|
||||
```plain
|
||||
G ::= x
|
||||
G ::= y
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
为了更加详细的描述这个问题,举一个例子,存在以下岔路:
|
||||
|
||||
```
|
||||
```plain
|
||||
a -> tree() -> c
|
||||
-> b1 -> b1'
|
||||
-> b2 -> b2'
|
||||
@@ -174,7 +174,7 @@ ChainNode 是对链表节点的定义,这里给出了和当前文章内容相
|
||||
|
||||
整个链表结构可能是这样的:
|
||||
|
||||
```
|
||||
```plain
|
||||
node1 <-> node2 <-> node3 <-> node4
|
||||
|- function2-1
|
||||
|- matchToken2-1
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
|
||||
<img src="assets/68/2.jpg" />
|
||||
|
||||
产品用户体验不仅是指交互视觉,我的理解用户体验反应用户与产品从认知,使用到传播整个情感的连接和反馈。能力上包括了产品设计与功能实现,用户交互界面,以及系统承载能力。
|
||||
产品用户体验不仅是指交互视觉,我的理解用户体验反映用户与产品从认知,使用到传播整个情感的连接和反馈。能力上包括了产品设计与功能实现,用户交互界面,以及系统承载能力。
|
||||
|
||||
上图是 CUBI Mobel:CUBI UX - User Experience Model。完整地说明了用户体验从内容、商业目标、交互、用户目标四个方面组合。
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
我试着列了几类
|
||||
|
||||
1. 产品商业阶段性目标和最终目标。营收提高,优化结构,工程效率提高,质量提高,能力覆盖。
|
||||
2. 工程研发过程及能力。投入产出比,系统成本,系统稳定性,创新性?。
|
||||
2. 工程研发过程及能力。投入产出比,系统成本,系统稳定性,创新性。
|
||||
3. 用户可用性。满意度,过程效率,过程质量。
|
||||
|
||||
## 总结
|
||||
|
||||
@@ -249,7 +249,7 @@ const App = epitath(function*() {
|
||||
|
||||
通过 immutagen,依次调用 `next`,生成新组件,且下一个组件是上一个组件的子组件,因此会产生下面的效果:
|
||||
|
||||
```
|
||||
```plain
|
||||
yield <A>
|
||||
yield <B>
|
||||
yield <C>
|
||||
|
||||
@@ -49,7 +49,7 @@ runPromiseByQueue([
|
||||
|
||||
得到的输出是:
|
||||
|
||||
```
|
||||
```plain
|
||||
promise 1
|
||||
promise 2
|
||||
promise 3
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
重回 “手写 SQL 编辑器” 系列。这次介绍如何利用缓存优化编译器执行性能。
|
||||
|
||||
可以利用 **Frist 集** 与 **Match 节点缓存** 这两种方式优化。
|
||||
可以利用 **First 集** 与 **Match 节点缓存** 这两种方式优化。
|
||||
|
||||
本文会用到一些图做解释,下面介绍图形规则:
|
||||
|
||||
@@ -44,7 +44,7 @@ Match 节点缓存,指在运行时,缓存节点到其第一个终结符的
|
||||
|
||||
拿 `select a, b, c, d from e` 这个语句做测试:
|
||||
|
||||
| node 节点访问次数 | Frist 集优化 | First 集 + Match 节点缓存优化 |
|
||||
| node 节点访问次数 | First 集优化 | First 集 + Match 节点缓存优化 |
|
||||
| ----------------- | ------------ | ----------------------------- |
|
||||
| 784 | 669 | 652 |
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ class Component extends React.PureComponent<Props, State> {
|
||||
this.rootDom = ReactDOM.findDOMNode(this.rootDomRef) as HTMLDivElement;
|
||||
|
||||
this.chart = new G2.Chart({
|
||||
container: document.getElementById("chart"),
|
||||
container: this.rootDom,
|
||||
forceFit: true,
|
||||
height: 300
|
||||
});
|
||||
@@ -276,7 +276,7 @@ Hook 函数必须以 "use" 命名开头,因为这样才方便 eslint 做检查
|
||||
|
||||
为什么不能用 condition 包裹 useHook 语句,详情可以见 [官方文档](https://reactjs.org/docs/hooks-rules.html#explanation),这里简单介绍一下。
|
||||
|
||||
React Hooks 并不是通过 Proxy 或者 getters 实现的(具体可以看这篇文章 [React hooks: not magic, just arrays](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e)),而是通过数组实现的,每次 `useState` 都会改变下标,如果 `useState` 被包裹在 condition 中,那每次执行的下标就可能对不上,导致 `useState` 导出的 `setter` 更新错数据。
|
||||
React Hooks 并不是通过 Proxy 或者 getters 实现的(具体可以看这篇文章 [React hooks: not magic, just arrays](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e)),而是通过链表实现的,每次 `useState` 都会改变下标,如果 `useState` 被包裹在 condition 中,那每次执行的下标就可能对不上,导致 `useState` 导出的 `setter` 更新错数据。
|
||||
|
||||
虽然有 [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) 插件保驾护航,但这第一次将 “约定优先” 理念引入了 React 框架中,带来了前所未有的**代码命名和顺序限制**(函数命名遭到官方限制,JS 自由主义者也许会暴跳如雷),但带来的便利也是前所未有的(没有比 React Hooks 更好的状态共享方案了,约定带来提效,自由的代价就是回到 renderProps or HOC,各团队可以自行评估)。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 1 引言
|
||||
|
||||
上周的 [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/master/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md) 已经实现了对 React Hooks 的基本认知,也许你也看了 React Hooks 基本实现剖析(就是数组),但理解实现原理就可以用好了吗?学的是知识,而用的是技能,看别人的用法就像刷抖音一样(哇,饭还可以这样吃?),你总会有新的收获。
|
||||
上周的 [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/master/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md) 已经实现了对 React Hooks 的基本认知,也许你也看了 React Hooks 基本实现剖析(单向链表),但理解实现原理就可以用好了吗?学的是知识,而用的是技能,看别人的用法就像刷抖音一样(哇,饭还可以这样吃?),你总会有新的收获。
|
||||
|
||||
这篇文章将这些知识实践起来,看看广大程序劳动人民是如何发掘 React Hooks 的潜力的(造什么轮子)。
|
||||
|
||||
@@ -259,7 +259,7 @@ const { loading, error, result } = useAsync(fetchUser, [id]);
|
||||
实现:在 Promise 的初期设置 loading,结束后设置 result,如果出错则设置 error,这里可以将请求对象包装成 `useAsyncState` 来处理,这里就不放出来了。
|
||||
|
||||
```tsx
|
||||
export function useAsync(asyncFunction) {
|
||||
export function useAsync(asyncFunction: any, params: any[]) {
|
||||
const asyncState = useAsyncState(options);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -326,8 +326,8 @@ const fetchUser = id =>
|
||||
});
|
||||
|
||||
function useFetchUser(id) {
|
||||
const asyncFetchUser = useAsync(fetchUser, id);
|
||||
return asyncUser;
|
||||
const asyncFetchUser = useAsync(fetchUser, [id]);
|
||||
return asyncFetchUser;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -499,12 +499,23 @@ useEffect(() => {
|
||||
const update = useUpdate();
|
||||
```
|
||||
|
||||
实现:我们知道 `useState` 下标为 1 的项是用来更新数据的,而且就算数据没有变化,调用了也会刷新组件,所以我们可以把返回一个没有修改数值的 `setValue`,这样它的功能就仅剩下刷新组件了。
|
||||
实现:我们知道 `useState` 下标为 1 的项是用来更新数据的,但数据必须有变化才会触发 render,因此我们可以这样设计:
|
||||
|
||||
```tsx
|
||||
const useUpdate = () => useState(0)[1];
|
||||
const useUpdate = () => {
|
||||
const [, setState] = useState(0);
|
||||
return () => setState(cnt => cnt + 1);
|
||||
};
|
||||
```
|
||||
|
||||
或者利用 `useReducer` 做一个简单的 Action 来支持:
|
||||
|
||||
```tsx
|
||||
const [, forceRender] = useReducer(s => s + 1, 0);
|
||||
```
|
||||
|
||||
> 感谢:感谢用户 [cike8899](https://github.com/cike8899) 对此处的勘误,并提供示例代码。
|
||||
|
||||
> 对于 `getSnapshotBeforeUpdate`, `getDerivedStateFromError`, `componentDidCatch` 目前 Hooks 是无法模拟的。
|
||||
|
||||
#### isMounted
|
||||
|
||||
@@ -31,7 +31,7 @@ html`
|
||||
`;
|
||||
```
|
||||
|
||||
很显然,由于跳过了 JSX 编译,换成了原生的 [Template Strings ](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/template_strings) ,所以所有组件、属性部分都需要改成 `${}` 语法,比如:
|
||||
很显然,由于跳过了 JSX 编译,换成了原生的 [Template Strings](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/template_strings) ,所以所有组件、属性部分都需要改成 `${}` 语法,比如:
|
||||
|
||||
`<${Header}>` 这种写法略显别扭,但整体上还是蛮直观的。
|
||||
|
||||
@@ -57,7 +57,7 @@ interface VDom {
|
||||
props: {
|
||||
[attrKey: string]: string;
|
||||
};
|
||||
chindren: VDom[];
|
||||
children: VDom[];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -149,9 +149,9 @@ Fiber 利用分片的思想,把一个耗时长的任务分成很多小片,
|
||||
因此,在组件更新时有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。所以 React Fiber 把一个更新过程分为两个阶段:
|
||||
|
||||
- 第一个阶段 Reconciliation Phase,Fiber 会找出需要更新的 DOM,这个阶段是可以被打断的;
|
||||
- 第二个阶段 Commit Phase,是无法别打断,完成 DOM 的更新并展示;
|
||||
- 第二个阶段 Commit Phase,是无法被打断的,完成 DOM 的更新并展示;
|
||||
|
||||
在使用 Fiber 后,需要要检查与第一阶段相关的生命周期函数,避免逻辑的多次或重复调用:
|
||||
在使用 Fiber 后,需要检查与第一阶段相关的生命周期函数,避免逻辑的多次或重复调用:
|
||||
|
||||
- componentWillMount
|
||||
- componentWillReceiveProps
|
||||
|
||||
@@ -504,7 +504,7 @@ export default {
|
||||
|
||||
## 2.13. Field
|
||||
|
||||
与 Value 组件唯一的区别,就是
|
||||
与 Value 组件唯一的区别,就是支持了 `bind`。
|
||||
|
||||
### 用法
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ const functionC = () => chain("y", "c")();
|
||||
|
||||
我们就得到了如下的链表:
|
||||
|
||||
```
|
||||
```plain
|
||||
ChainNode(main)
|
||||
└── FunctionNode(functionA) ─ TreeNode ─ FunctionNode(functionC)
|
||||
│── FunctionNode(functionB1)
|
||||
|
||||
@@ -163,7 +163,7 @@ export const backendMain = () => {
|
||||
|
||||
在文件夹视图下,可以做如下结构规划:
|
||||
|
||||
```
|
||||
```plain
|
||||
.
|
||||
├── client # 前端入口
|
||||
├── server # 后端入口
|
||||
|
||||
@@ -456,7 +456,7 @@ function Article({ id }) {
|
||||
|
||||
## useEffect 还有什么优势
|
||||
|
||||
`useEffect` 在渲染结束时执行,所以不会阻塞浏览器渲染进程,所以使用 Function Component 写的项目一般都有用更好的性能。
|
||||
`useEffect` 在渲染结束时执行,所以不会阻塞浏览器渲染进程,所以使用 Function Component 写的项目一般都拥有更好的性能。
|
||||
|
||||
自然符合 React Fiber 的理念,因为 Fiber 会根据情况暂停或插队执行不同组件的 Render,如果代码遵循了 Capture Value 的特性,在 Fiber 环境下会保证值的安全访问,同时弱化生命周期也能解决中断执行时带来的问题。
|
||||
|
||||
|
||||
+1
-1
@@ -292,7 +292,7 @@ ReactDOM.render(
|
||||
|
||||
根据笔者的经验,**从上层业务到底层通用组件之间,本地状态数量是递增的:**
|
||||
|
||||
```
|
||||
```plain
|
||||
业务
|
||||
-> 全局数据流
|
||||
-> 页面(完全依赖全局数据流,几乎没有自己的状态)
|
||||
|
||||
@@ -51,7 +51,9 @@ CI/CD 具体是个什么样的流程呢,如下图所示,差异仅在于是
|
||||
- 需要有持续集成的基础,测试用例需要覆盖足够的代码
|
||||
- 部署需要自动化,用户只需要手动触发,剩余的部署应该自动化
|
||||
- 团队需要增加新特性标志,避免未完成的新特性进入待发布的产品
|
||||
产出:
|
||||
|
||||
产出:
|
||||
|
||||
- 部署软件变得非常简单。团队不需要花费 n 天准备发布。
|
||||
- 可以提高发布频率,加速新特性触达用户进程。
|
||||
- 小的更改,对决策的压力要小得多,可以更快地迭代。
|
||||
@@ -63,7 +65,9 @@ CI/CD 具体是个什么样的流程呢,如下图所示,差异仅在于是
|
||||
- 测试必须要做到足够。测试的质量将决定发布的质量。
|
||||
- 文档建设需要和产品部署保持同步。
|
||||
- 新特性的发布需要协调其他部门,包括售后支持&市场&推广等。
|
||||
产出:
|
||||
|
||||
产出:
|
||||
|
||||
- 快速的发布节奏,因为每个新特性一旦完成都会自动的发布给用户。
|
||||
- 发布风险降低,修复问题更容易,因为每次变更都是小步迭代发布。
|
||||
- 用户可以看到持续性的优化和质量提升,而不是非要等到按月,按季度,甚至按年
|
||||
|
||||
@@ -117,7 +117,7 @@ CEO 通过顶层设计调动了全公司资源,而业务线总裁通过任务
|
||||
|
||||
1. 技术细节学习难度不大,在需要深入的时候再深入了解最佳。
|
||||
2. 想要做成事,需要更宏观的技术思维,所以专家渐渐变得眼光宽阔,格局很大。
|
||||
3. 专家拥有快速学习技术细节的能力,只是这已不是其核心竞争力,所以与其写技术细节的文章,比如写方法论的思考带来的价值更大。
|
||||
3. 专家拥有快速学习技术细节的能力,只是这已不是其核心竞争力,所以与其写技术细节的文章,不如写方法论的思考带来的价值更大。
|
||||
4. 指引方向比走路更重要,专家都要逐渐成为引路人。
|
||||
5. 技术最终为业务服务,懂技术细节和让业务先赢没有必然的关系,所以在深入技术细节之前,要先理解业务,把握方向,防止技术细节出现路线问题。
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ function Counter() {
|
||||
|
||||
如果我们 **在三秒内连续点击三次**,那么 `count` 的值最终会变成 `3`,而随之而来的输出结果是。。?
|
||||
|
||||
```
|
||||
```plain
|
||||
0
|
||||
1
|
||||
2
|
||||
@@ -109,7 +109,7 @@ class Counter extends Component {
|
||||
|
||||
嗯,结果应该等价吧?3 秒内快速点击三次按钮,这次的结果是:
|
||||
|
||||
```
|
||||
```plain
|
||||
3
|
||||
3
|
||||
3
|
||||
@@ -271,7 +271,7 @@ function Counter() {
|
||||
const log = () => {
|
||||
setCount(1 + 1);
|
||||
setTimeout(() => {
|
||||
console.log(currentCount.current); // 此时 currentCount.current: 3
|
||||
console.log(currentCount.current);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
@@ -293,7 +293,7 @@ function Counter() {
|
||||
const log = () => {
|
||||
setCount(2 + 1);
|
||||
setTimeout(() => {
|
||||
console.log(currentCount.current); // 此时 currentCount.current: 3
|
||||
console.log(currentCount.current);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
@@ -315,7 +315,7 @@ function Counter() {
|
||||
const log = () => {
|
||||
setCount(3 + 1);
|
||||
setTimeout(() => {
|
||||
console.log(currentCount.current); // 此时 currentCount.current: 3
|
||||
console.log(currentCount.current);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
@@ -505,7 +505,7 @@ function Counter() {
|
||||
|
||||
我们将 `count` 作为了 `useEffect` 的依赖项,就得到了正确的结果:
|
||||
|
||||
```
|
||||
```plain
|
||||
1
|
||||
2
|
||||
3
|
||||
@@ -813,25 +813,30 @@ function Parent() {
|
||||
换一个例子就可以看得更清楚:
|
||||
|
||||
```js
|
||||
function Parent() {
|
||||
function Parent(props) {
|
||||
const [count, setCount] = useState(0);
|
||||
const [step, setStep] = useState(0);
|
||||
const [other, setOther] = useState(0);
|
||||
const drag = useDraggable(count, step); // 封装了拖拽函数
|
||||
const drag = useDraggable(props.dom, count, step); // 封装了拖拽函数
|
||||
|
||||
useEffect(() => {
|
||||
// dom 变化时重新实例化
|
||||
drag()
|
||||
}, [drag])
|
||||
}
|
||||
```
|
||||
|
||||
假设我们使用 [Sortablejs](https://github.com/SortableJS/Sortable) 对某个区域进行拖拽监听,这个函数每次都重复执行的性能损耗非常大,**然而这个函数内部可能因为仅仅要上报一些日志,所以依赖了没有实际被使用的 `count` `step` 变量:**
|
||||
|
||||
```js
|
||||
function useDraggable(count, step) {
|
||||
function useDraggable(dom, count, step) {
|
||||
return useCallback(() => {
|
||||
// 上报日志
|
||||
report(count, step);
|
||||
|
||||
// 对区域进行初始化,非常耗时
|
||||
// ... 省略耗时代码
|
||||
}, [count, step]);
|
||||
}, [dom, count, step]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -964,7 +969,7 @@ function Parent() {
|
||||
}
|
||||
```
|
||||
|
||||
虽然 `Child` 可以通过 `memo` 或 `useMemo` 进行优化,**但当程序复杂时,可能存在多个函数在所有 Function Component 间共享的情况 **,此时就需要新 Hook: `useContext` 来拯救了。
|
||||
虽然 `Child` 可以通过 `memo` 或 `useMemo` 进行优化,**但当程序复杂时,可能存在多个函数在所有 Function Component 间共享的情况**,此时就需要新 Hook: `useContext` 来拯救了。
|
||||
|
||||
### 使用 Context 做批量透传
|
||||
|
||||
@@ -1130,7 +1135,7 @@ const Step = () => {
|
||||
一个普通的 Redux 组件:
|
||||
|
||||
```js
|
||||
const mapStateToProps = state => (count: state.count);
|
||||
const mapStateToProps = state => ({count: state.count});
|
||||
|
||||
const mapDispatchToProps = dispatch => dispatch;
|
||||
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
# 1. 引言
|
||||
|
||||
本周精读的内容是:[Google I/O 19](https://www.youtube.com/watch?v=c0oy0vQKEZE)。
|
||||
|
||||
2019 年 Google I/O 介绍了一些激动人心的 JS 新特性,这些特性有些已经被主流浏览器实现,并支持 polyfill,有些还在草案阶段。
|
||||
|
||||
我们可以看到 JS 语言正变得越来越严谨,不同规范间也逐渐完成了闭环,而且在不断吸纳其他语言的优秀特性,比如 WeakRef,让 JS 在成为使用范围最广编程语言的同时,也越成为编程语言的集大成者,让我们有信心继续跟随 JS 生态,不用被新生的小语种分散精力。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
本视频共介绍了 16 个新特性。
|
||||
|
||||
## private class fields
|
||||
|
||||
私有成员修饰符,用于 Class:
|
||||
|
||||
```js
|
||||
class IncreasingCounter {
|
||||
#count = 0;
|
||||
|
||||
get value() {
|
||||
return this.#count;
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.#count++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过 `#` 修饰的成员变量或成员函数就成为了私有变量,如果试图在 Class 外部访问,则会抛出异常:
|
||||
|
||||
```js
|
||||
const counter = new IncreasingCounter()
|
||||
counter.#count
|
||||
// -> SyntaxError
|
||||
counter.#count = 42
|
||||
// -> SyntaxError
|
||||
```
|
||||
|
||||
虽然 `#` 这个关键字被吐槽了很多次,但结论已经尘埃落定了,只是个语法形式而已,不用太纠结。
|
||||
|
||||
目前仅 Chrome、Nodejs 支持。
|
||||
|
||||
## Regex matchAll
|
||||
|
||||
正则匹配支持了 `matchAll` API,可以更方便进行正则递归了:
|
||||
|
||||
```js
|
||||
const string = 'Magic hex number: DEADBEEF CAFE'
|
||||
const regex = /\b\p{ASCII_Hex_Digit}+\b/gu/
|
||||
for (const match of string.matchAll(regex)) {
|
||||
console.log(match)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// ['DEADBEEF', index: 19, input: 'Magic hex number: DEADBEEF CAFE']
|
||||
// ['CAFE', index: 28, input: 'Magic hex number: DEADBEEF CAFE']
|
||||
```
|
||||
|
||||
相比以前在 `while` 语句里循环正则匹配,这个 API 真的是相当的便利。And more,还顺带提到了 `Named Capture Groups`,这个在之前的 [精读《正则 ES2018》](https://github.com/dt-fe/weekly/blob/v2/091.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%AD%A3%E5%88%99%20ES2018%E3%80%8B.md#22-named-capture-groups) 中也有提到,具体可以点过去阅读,也可以配合 `matchAll` 一起使用。
|
||||
|
||||
## Numeric literals
|
||||
|
||||
大数字面量的支持,比如:
|
||||
|
||||
```js
|
||||
1234567890123456789 * 123;
|
||||
// -> 151851850485185200000
|
||||
```
|
||||
|
||||
这样计算结果是丢失精度的,但只要在数字末尾加上 `n`,就可以正确计算大数了:
|
||||
|
||||
```js
|
||||
1234567890123456789n * 123n;
|
||||
// -> 151851850485185185047n
|
||||
```
|
||||
|
||||
目前 BigInt 已经被 Chrome、Firefox、Nodejs 支持。
|
||||
|
||||
## BigInt formatting
|
||||
|
||||
为了方便阅读,大数还支持了国际化,可以适配成不同国家的语言表达形式:
|
||||
|
||||
```js
|
||||
const nf = new Intl.NumberFormat("fr");
|
||||
nf.format(12345678901234567890n);
|
||||
// -> '12 345 678 901 234 567 890'
|
||||
```
|
||||
|
||||
记住 `Intl` 这个内置变量,后面还有不少国际化用途。
|
||||
|
||||
同时,为了方便程序员阅读代码,大数还支持带分隔符的书写方式,可以使用 `useGrouping` 属性配置,默认为 `true`:
|
||||
|
||||
```js
|
||||
const nf = new Intl.NumberFormat("fr", { useGrouping: true });
|
||||
nf.format(12345678901234567890n);
|
||||
// -> '12 345 678 901 234 567 890'
|
||||
```
|
||||
|
||||
目前已经被 Chrome、Firefox、Nodejs 支持。
|
||||
|
||||
## flat & flatmap
|
||||
|
||||
等价于 lodash [flatten](https://lodash.com/docs/4.17.11#flatten) 功能:
|
||||
|
||||
```js
|
||||
const array = [1, [2, [3]]];
|
||||
array.flat();
|
||||
// -> [1, 2, [3]]
|
||||
```
|
||||
|
||||
还支持自定义深度,如果支持 `Infinity` 无限层级:
|
||||
|
||||
```js
|
||||
const array = [1, [2, [3]]];
|
||||
array.flat(Infinity);
|
||||
// -> [1, 2, 3]
|
||||
```
|
||||
|
||||
这样我们就可以配合 `.map` 使用:
|
||||
|
||||
```js
|
||||
[2, 3, 4].map(duplicate).flat();
|
||||
```
|
||||
|
||||
因为这个用法太常见,js 内置了 `flatMap` 函数代替 `map`,与上面的效果是等价的:
|
||||
|
||||
```js
|
||||
[2, 3, 4].flatMap(duplicate);
|
||||
```
|
||||
|
||||
目前已经被 Chrome、Firefox、Safari、Nodejs 支持。
|
||||
|
||||
## fromEntries
|
||||
|
||||
`fromEntries` 是 `Object.fromEntries` 的语法,用来将对象转化为数组的描述:
|
||||
|
||||
```js
|
||||
const object = { x: 42, y: 50, abc: 9001 };
|
||||
const entries = Object.entries(object);
|
||||
// -> [['x', 42], ['y', 50]]
|
||||
```
|
||||
|
||||
这样就可以对对象的 key 与 value 进行加工处理,并通过 `fromEntries` API 重新转回对象:
|
||||
|
||||
```js
|
||||
const object = { x: 42, y: 50, abc: 9001 }
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(object)
|
||||
.filter(([ key, value]) => key.length === 1)
|
||||
.map(([ key, value ]) => [ key, value * 2])
|
||||
)
|
||||
// -> { x: 84, y: 100 }
|
||||
```
|
||||
|
||||
不仅如此,还可以将 object 快速转化为 Map:
|
||||
|
||||
```js
|
||||
const map = new Map(Object.entries(object));
|
||||
```
|
||||
|
||||
目前已经被 Chrome、Firefox、Safari、Nodejs 支持。
|
||||
|
||||
## Map to Object conversion
|
||||
|
||||
`fromEntries` 建立了 object 与 map 之间的桥梁,我们还可以将 Map 快速转化为 object:
|
||||
|
||||
```js
|
||||
const objectCopy = Object.fromEntries(map);
|
||||
```
|
||||
|
||||
目前已经被 Chrome、Firefox、Safari、Nodejs 支持。
|
||||
|
||||
## globalThis
|
||||
|
||||
> 业务代码一般不需要访问全局的 window 变量,但是框架与库一般需要,比如 polyfill。
|
||||
|
||||
访问全局的 this 一般会做四个兼容,因为 js 在不同运行环境下,全局 this 的变量名都不一样:
|
||||
|
||||
```js
|
||||
const getGlobalThis = () => {
|
||||
if (typeof self !== "undefined") return self; // web worker 环境
|
||||
if (typeof window !== "undefined") return window; // web 环境
|
||||
if (typeof global !== "undefined") return global; // node 环境
|
||||
if (typeof this !== "undefined") return this; // 独立 js shells 脚本环境
|
||||
throw new Error("Unable to locate global object");
|
||||
};
|
||||
```
|
||||
|
||||
因此整治一下规范也合情合理:
|
||||
|
||||
```js
|
||||
globalThis; // 在任何环境,它就是全局的 this
|
||||
```
|
||||
|
||||
目前已经被 Chrome、Firefox、Safari、Nodejs 支持。
|
||||
|
||||
## Stable sort
|
||||
|
||||
就是稳定排序结果的功能,比如下面的数组:
|
||||
|
||||
```js
|
||||
const doggos = [
|
||||
{ name: "Abby", rating: 12 },
|
||||
{ name: "Bandit", rating: 13 },
|
||||
{ name: "Choco", rating: 14 },
|
||||
{ name: "Daisy", rating: 12 },
|
||||
{ name: "Elmo", rating: 12 },
|
||||
{ name: "Falco", rating: 13 },
|
||||
{ name: "Ghost", rating: 14 }
|
||||
];
|
||||
|
||||
doggos.sort((a, b) => b.rating - a.rating);
|
||||
```
|
||||
|
||||
最终排序结果可能如下:
|
||||
|
||||
```js
|
||||
[
|
||||
{ name: "Choco", rating: 14 },
|
||||
{ name: "Ghost", rating: 14 },
|
||||
{ name: "Bandit", rating: 13 },
|
||||
{ name: "Falco", rating: 13 },
|
||||
{ name: "Abby", rating: 12 },
|
||||
{ name: "Daisy", rating: 12 },
|
||||
{ name: "Elmo", rating: 12 }
|
||||
];
|
||||
```
|
||||
|
||||
也可能如下:
|
||||
|
||||
```js
|
||||
[
|
||||
{ name: "Ghost", rating: 14 },
|
||||
{ name: "Choco", rating: 14 },
|
||||
{ name: "Bandit", rating: 13 },
|
||||
{ name: "Falco", rating: 13 },
|
||||
{ name: "Abby", rating: 12 },
|
||||
{ name: "Daisy", rating: 12 },
|
||||
{ name: "Elmo", rating: 12 }
|
||||
];
|
||||
```
|
||||
|
||||
注意 `choco` 与 `Ghost` 的位置可能会颠倒,这是因为 JS 引擎可能只关注 `sort` 函数的排序,而在顺序相同时,不会保持原有的排序规则。现在通过 **Stable sort** 规范,可以确保这个排序结果是稳定的。
|
||||
|
||||
目前已经被 Chrome、Firefox、Safari、Nodejs 支持。
|
||||
|
||||
## Intl.RelativeTimeFormat
|
||||
|
||||
`Intl.RelativeTimeFormat` 可以对时间进行语义化翻译:
|
||||
|
||||
```js
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
|
||||
rtf.format(-1, "day");
|
||||
// -> 'yesterday'
|
||||
rtf.format(0, "day");
|
||||
// -> 'today'
|
||||
rtf.format(1, "day");
|
||||
// -> 'tomorrow'
|
||||
rtf.format(-1, "week");
|
||||
// -> 'last week'
|
||||
rtf.format(0, "week");
|
||||
// -> 'this week'
|
||||
rtf.format(1, "week");
|
||||
// -> 'next week'
|
||||
```
|
||||
|
||||
不同语言体系下,`format` 会返回不同的结果,通过控制 `RelativeTimeFormat` 的第一个参数 `en` 决定,比如可以切换为 `ta-in`。
|
||||
|
||||
## Intl.ListFormat
|
||||
|
||||
`ListFormat` 以列表的形式格式化数组:
|
||||
|
||||
```js
|
||||
const lfEnglish = new Intl.ListFormat("en");
|
||||
lfEnglish.format(["Ada", "Grace"]);
|
||||
// -> 'Ada and Grace'
|
||||
```
|
||||
|
||||
可以通过第二个参数指定连接类型:
|
||||
|
||||
```js
|
||||
const lfEnglish = new Intl.ListFormat("en", { type: "disjunction" });
|
||||
lfEnglish.format(["Ada", "Grace"]);
|
||||
// -> 'Ada or Grace'
|
||||
```
|
||||
|
||||
目前已经被 Chrome、Nodejs 支持。
|
||||
|
||||
## Intl.DateTimeFormat -> formatRange
|
||||
|
||||
`DateTimeFormat` 可以定制日期格式化输出:
|
||||
|
||||
```js
|
||||
const start = new Date(startTimestamp);
|
||||
// -> 'May 7, 2019'
|
||||
const end = new Date(endTimestamp);
|
||||
// -> 'May 9, 2019'
|
||||
const fmt = new Intl.DateTimeFormat("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
});
|
||||
const output = `${fmt.format(start)} - ${fmt.format(end)}`;
|
||||
// -> 'May 7, 2019 - May 9, 2019'
|
||||
```
|
||||
|
||||
最后一句,也可以通过 `formatRange` 函数代替:
|
||||
|
||||
```js
|
||||
const output = fmt.formatRange(start, end);
|
||||
// -> 'May 7 - 9, 2019'
|
||||
```
|
||||
|
||||
目前已经被 Chrome 支持。
|
||||
|
||||
## Intl.Locale
|
||||
|
||||
定义国际化本地化的相关信息:
|
||||
|
||||
```js
|
||||
const locale = new Intl.Locale("es-419-u-hc-h12", {
|
||||
calendar: "gregory"
|
||||
});
|
||||
locale.language;
|
||||
// -> 'es'
|
||||
locale.calendar;
|
||||
// -> 'gregory'
|
||||
locale.hourCycle;
|
||||
// -> 'h12'
|
||||
locale.region;
|
||||
// -> '419'
|
||||
locale.toString();
|
||||
// -> 'es-419-u-ca-gregory-hc-h12'
|
||||
```
|
||||
|
||||
目前已经被 Chrome、Nodejs 支持。
|
||||
|
||||
## Top-Level await
|
||||
|
||||
支持在根节点生效 `await`,比如:
|
||||
|
||||
```js
|
||||
const result = await doSomethingAsync();
|
||||
doSomethingElse();
|
||||
```
|
||||
|
||||
目前还没有支持。
|
||||
|
||||
## Promise.allSettled/Promise.any
|
||||
|
||||
`Promise.allSettled` 类似 `Promise.all`、`Promise.any` 类似 `Promise.race`,区别是,在 Promise reject 时,`allSettled` 不会 reject,而是也当作 fulfilled 的信号。
|
||||
|
||||
举例来说:
|
||||
|
||||
```js
|
||||
const promises = [
|
||||
fetch("/api-call-1"),
|
||||
fetch("/api-call-2"),
|
||||
fetch("/api-call-3")
|
||||
];
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
```
|
||||
|
||||
即便某个 `fetch` 失败了,也不会导致 `reject` 的发生,这样在不在乎是否有项目失败,只要拿到都结束的信号的场景很有用。
|
||||
|
||||
对于 `Promise.any` 则稍有不同:
|
||||
|
||||
```js
|
||||
const promises = [
|
||||
fetch("/api-call-1"),
|
||||
fetch("/api-call-2"),
|
||||
fetch("/api-call-3")
|
||||
];
|
||||
|
||||
try {
|
||||
const first = await Promise.any(promises);
|
||||
// Any of ths promises was fulfilled.
|
||||
console.log(first);
|
||||
} catch (error) {
|
||||
// All of the promises were rejected.
|
||||
}
|
||||
```
|
||||
|
||||
只要有子项 fulfilled,就会完成 `Promise.any`,哪怕第一个 Promise reject 了,而第二个 Promise fulfilled 了,`Promise.any` 也会 fulfilled,而对于 `Promise.race`,这种场景会直接 rejected。
|
||||
|
||||
如果所有子项都 rejected,那 `Promise.any` 也只好 rejected 啦。
|
||||
|
||||
目前已经被 Chrome、Firefox 支持。
|
||||
|
||||
## WeakRef
|
||||
|
||||
WeakRef 是从 OC 抄过来的弱引用概念。
|
||||
|
||||
为了解决这个问题:当对象被引用后,由于引用的存在,导致对象无法被 GC。
|
||||
|
||||
所以如果建立了弱引用,那么对象就不会因为存在的这段引用关系而影响 GC 了!
|
||||
|
||||
具体用法是:
|
||||
|
||||
```js
|
||||
const obj = {};
|
||||
const weakObj = new WeakRef(obj);
|
||||
```
|
||||
|
||||
使用 `weakObj` 与 `obj` 没有任何区别,唯一不同时,`obj` 可能随时被 GC,而一旦被 GC,弱引用拿到的对象可能就变成 `undefined`,所以要做好错误保护。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
JS 这几个特性提升了 JS 语言的成熟性、完整性,而且看到其访问控制能力、规范性、国际化等能力有着重加强,解决的都是 JS 最普遍遇到的痛点问题。
|
||||
|
||||
那么,这些 JS 特性中,你最喜欢哪一条呢?想吐槽哪一条呢?欢迎留言。
|
||||
|
||||
> 讨论地址是:[精读《What's new in javascript》 · Issue #159 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/159)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
**special Sponsors**
|
||||
|
||||
- [DevOps 全流程平台](https://e.coding.net/?utm_source=weekly)
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,156 @@
|
||||
# 1. 引言
|
||||
|
||||
本周精读内容是:[《数据之上 智慧之光》](http://www.fanruan.com/2018/databook2),由帆软软件公司出品。
|
||||
|
||||
帆软公司是国内一家做大数据 BI 和分析平台的提供商,主打产品是 [FineBI](http://www.fanruan.com/finebi)。笔者所在阿里数据中台也处于数据分析应用的前沿,本次精读的文章就是帆软公司的 《数据之上 智慧之光 2018》,感谢提供这份国内数据市场研究报告,让我们更深入全面的了解国内数据市场的发展方向。
|
||||
|
||||
随着 5G 的逐渐推行,网速比 4G 提高了 100 倍,将会为物联网打下通信基础,未来的世界将人与物、物与物进行互联。随着越来越多的设备接入网络,产生数据,而未来还有 6G、7G 将网速继续提高至 1 万倍、1 百万倍,利用卫星实现全球网络覆盖,将现实与虚拟融合等等,无不需要强大的数据处理分析技术才能掌握。
|
||||
|
||||
数据的总量将呈几何倍数上升,如果不能提前对数据的存储、处理、挖掘和分析提出一套解决方案,那么 5G 时代的海量数据就是人类社会的累赘,如果有一套数据处理与分析的方案,我们就有可能掌握海量的数据为自己所用,利用数据进一步推动人类社会向前发展。
|
||||
|
||||
上面是对未来的畅想,那么我国现阶段国内的数据市场的容量、需求是什么样呢?《数据之上 智慧之光》这本书给了我们答案。
|
||||
|
||||
PS:本文使用 2018 年的数据。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
## 大数据行业发展趋势
|
||||
|
||||
2018 年中国大数据产业规模预计 329 亿元人民币,同比增长 39.4%。可以看到增长速度逐年增加,预计在 2020 年数据市场规模可达 586 亿元人民币。
|
||||
|
||||
笔者查了一下,2018 年全国网上零售额为 90065 亿元,比数据市场规模多了一个数量级,所以我国的数据产业其实还在萌芽期,可能还需要 5 到 10 年才能完全成熟,这也意味着目前数据市场是一片蓝海,从后面的数据和国内数据应用使用情况也可以看出来。
|
||||
|
||||
另外,各企业在大数据领域的投入资金与部门组织都同比 2017 年有所增加,其中接近四成的受访企业已经在应用大数据,较 2016 年提升了 4.5%,暂不考虑大数据的企业从 2016 年 7.8% 下降到 6.8%。
|
||||
|
||||
从微观角度观察社会也能发现这样的趋势,近些年研究大数据的公司明显增多,许多公司都逐渐设立了 “数据分析” 岗位和部门,可视化大屏在 toB 与 toG 领域都越来越得到重视。
|
||||
|
||||
## 企业数据应用情况
|
||||
|
||||
数据应用分为数据采集、数据治理、数据处理、数据分析这四大阶段,其中数据采集是获取数据的最重要方式,而数据治理是将分散在各种不同形态数据库的文件用统一方式管理起来,比如形成数据联邦,这是数据使用前最重要的一步治理。数据处理就是将数据按照业务需求进行计算,而不同量级的数据计算方式会不同,特别是大数据场景要分为离线计算与实时计算,只有极为重要、实时性要求强的指标才进行实时计算,现在正处于离线与实时计算混合的混合计算转型期。数据分析一般通过 BI 平台完成,也是分析数据最重要的一步,BI 也经历了漫长的版本迭代,第一阶段是数据报表阶段,第二阶段是具备分析能力与数据挖掘能力的分析阶段,第三阶段是机器自动识别用户意图的智能化分析阶段。
|
||||
|
||||
从智慧之光的调查结果来看,只有 22.47% 的企业实用了 BI 系统,而使用 BI 系统的企业中,超过七成认为 BI 项目能较好的满足现在的需求。说明未来还会有更多企业使用 BI,BI 的市场还有 4 倍的增长空间。
|
||||
|
||||
在数据应用成熟度方面,仅有 3.5% 的企业处于数据盈利阶段,也就是大部分企业对数据的治理还在投入阶段,但无需质疑,持续对数据进行投入一定能得到回报,但短期来看会拖累财务报表。
|
||||
|
||||
再看目前企业的数据价值需求,看看业务方对 BI 工具的期望有哪些。
|
||||
|
||||
期望从高到低分别是:
|
||||
|
||||
- (72.8%) 整合多系统数据,打通数据壁垒
|
||||
- (69.1%) 提高报表数据效率,更快更准更省事
|
||||
- (53.7%) 辅助管理预测,提高决策成功率
|
||||
- (51.4%) 提高生产效率,降低人力成本
|
||||
- (50.0%) 数据结合管理,优化管理方式
|
||||
- (47.8%) 业务监管分析,促进业务增至
|
||||
|
||||
这个排列顺序基本上也是 BI 平台迭代的顺序。
|
||||
|
||||
BI 刚起步时都要先做数据整合,对于大部分公司,数据孤岛的情况还是很普遍的,甚至有大量数据分散在各自工作人员电脑的 Excel 文件中,已存在的各业务平台见数据无法打通也很普遍,如果不能将多套系统间数据打通,你就没有对数据的掌控力。像阿里云的 [Dataphin](https://www.aliyun.com/product/dataphin) 就可以帮助企业建立数仓,建立一套数据资产管理体系,其中第一步就是帮助你打通数据壁垒。
|
||||
|
||||
解决取数问题后,就可以建设 BI 平台了,BI 平台初期基本以构建报表为主,而构建报表的方式根据发展阶段也各有不同,下面是智慧之光中一张很经典的 BI 发展阶段:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1szfQb.CF3KVjSZJnXXbnHFXa-2432-1104.png">
|
||||
|
||||
在 IT-完全主导型阶段,主要任务就是制作报表,而业务人员能配置的部分只有 BI 模版的 5%,剩余 95% 都需要 IT 人员参与开发,不仅浪费人力资源,而且对业务线的时间成本也很高。
|
||||
|
||||
IT-强主导型阶段,BI 平台具有一定的配置能力,业务有 20% 的自主配置权,而 IT 仍需完成 80% 的工作。
|
||||
|
||||
在业务强主导型阶段,BI 层 80% 的工作都可以由业务方完成,IT 人员只参与 20%,这 20% 可能包括复杂场景的定制,比如电子表格或者复杂的分析功能。这个阶段真正实现了更快更准更省事。
|
||||
|
||||
业务完全主导型阶段,基本上 BI 层不需要 IT 人员参与,业务同学可以完全主导对 BI 平台的拓展,或者 BI 平台已经能满足业务线几乎所有的诉求,同时业务还能参与数据模型的控制,让业务能力下沉到数据层。到这个阶段的企业已经非常少了,也许只有少数互联网巨头可以达到这个阶段。
|
||||
|
||||
智能自助型,这个阶段不需要 IT 人员参与,业务仅需参与 1%,原因是 99% 的需求都有人工智能自动分析出来,也就是将业务数据拿到后,计算机已经知道该怎么看这份数据了。智能自主型在国内还处于概念阶段,在国外 BI 工具比如 PowerBI 与 Tableau 已经在这个领域深耕多年了,然而门槛比较高,目前效果应该还不太理想,因为这个阶段一旦成熟,国内的 BI 企业将面临巨大冲击,之所以国内处于业务强主导阶段的 BI 平台依然存在,除了数据安全的理由之外,只能认为国外智能自助型 BI 平台依然 “不够智能”。
|
||||
|
||||
通过上面的分析可以总结出,BI 平台不仅业务发展阶段迥异,对技术人才的要求在不同阶段也不一样,技术层面需要以 后端 -> 前端 -> ETL -> AI 人才 的递进态势演变,对技术人员来说,如何在 BI 技术演变的过程中不断自我学习,满足下个阶段的技术要求,是非常严峻的挑战。
|
||||
|
||||
另一个值得关注的是企业数据来源,根据 2016 与 2017 年的对比,来自企业内部的数据正在逐渐增多,从外部购买的数据从 16.7% 降低到 15.1%,而从政府免费开放的数据比例从 13.5% 提升到了 14.6%。这表示企业正在逐渐摆脱对外部购买数据的依赖,转而产生更多自己业务的数据,而政府也在逐渐加强开放数据建设,努力减少各企业间数据资源的壁垒。
|
||||
|
||||
## 企业数据使用方式
|
||||
|
||||
根据调查显示:
|
||||
|
||||
- (70.0%)使用传统的 SQL + Excel 分析数据
|
||||
- (64.8%)使用业务系统自带的报表或分析功能
|
||||
- (35.6%)使用 BI 工具
|
||||
- (10.8%)手工写代码
|
||||
|
||||
首先频繁的手工写代码只有 10% 不到的比例,这是因为稍稍有点长远打算的企业,都会打造一支技术团队,而业务也会给技术团队打造一些生产效能提升的工具,只有 10% 左右的企业无法割舍短期利益,导致所有数据分析需求都要手工写代码。
|
||||
|
||||
大部分企业依然采用 SQL + Excel 分析数据,这个结果在情理之中,因为 SQL + Excel 都是现成的工具,不需要研发成本,而 Excel 的强大分析能力也基本满足了业务需求。但这种模式无法共享分析结果,存在数据安全隐患,且无法进入分析与智能阶段。
|
||||
|
||||
使用业务系统自带的报表或分析功能也占了 64.8% 的比例,笔者所了解到的中小型公司也的确属于这个阶段,公司内不同业务线都有自己的业务平台,每个业务平台内都有或多或少的数据分析和报表能力,这对大部分企业来说够用了,但对于要建立 **数据中台** 的企业来说,分散在各业务系统的数据与报表能力,反而是一种阻碍。PS:阿里数据中台已进入 2.0 阶段,但对大部分企业来说,是不可能越过数据中台 1.0,直接进入 2.0 的,就像不可能跳过 5G 做 6G 一样。
|
||||
|
||||
只有 35.6% 的企业在使用 BI 工具,因为使用 BI 工具需要一定门槛,比如做数据治理等,当然也可以直接订购阿里云的 [Dataphin](https://www.aliyun.com/product/dataphin) 快速接入 QuickBI。
|
||||
|
||||
在企业使用 BI 时,选型的考虑因素也很有意思:
|
||||
|
||||
- (69.1%)产品是否高效易用
|
||||
- (59.2%)产品是否稳定性高,性能好
|
||||
- (58.5%)产品是否拥有丰富强大的功能
|
||||
- (51.4%)产品是否具备大数据分析能力
|
||||
- (33.6%)采购成本
|
||||
- (31.2%)生态与学习资源
|
||||
- (24.4%)厂商本身的实力
|
||||
|
||||
可以看到,BI 工具靠自身实力吃饭的,而不依赖公司光环,因为业务方对实用性要求更大。
|
||||
|
||||
69.1% 的企业看中是否高效易用,说明目前国内企业对 BI 培训能力较弱,希望有高投入产出比,同时也说明了 BI 自身的特性,它是面向非技术人员的产品,如果易用性不强,只是功能强大是没有用的。
|
||||
|
||||
59.2% 的企业看中稳定性和性能,这是因为对数据分析来说,看报表是高频操作,业务方会使用 BI 查看 KPI 报表,发日报或月报,用户是无法忍受频繁使用的产品稳定性出现问题的。
|
||||
|
||||
第三点就是功能是否强大,对一款面向用户的工具来说,如果功能有欠缺,就意味着无法满足业务需求。比如对折线图做归一化,如果 BI 平台的折线图自身不支持这个功能,使用者也没办法立马拉上一名前端同学拓展出这个功能,因为 BI 平台表面看上去易用,但底层设计复杂,一旦遇到功能不支持,除了等待更新外,没有更好的办法。
|
||||
|
||||
最后一个超过 50% 的用户期待就是具备大数据分析能力,这是因为企业数据量级普遍都很大,而 BI 平台底层的多维建模一般采用 OLAP 查询,遇到海量数据可能要等上几十分钟,需要 BI 平台内置一些数据加速的功能。ROLAP 给予关系型数据库,特点是兼容性强、灵活性强,但查询速度慢,而 MOLAP 是实现将各维度数据计算好,查询时直接映射到多为数据库访问,性能好,但是对存储空间的依赖极高,需要付出大量的金钱才能支撑这种模式的查询。
|
||||
|
||||
下面是企业对 BI 功能要求:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1itL4bW1s3KVjSZFAXXX_ZXXa-1676-1310.png">
|
||||
|
||||
可以看到,对报表能力需求量最大,说明报表是 BI 工具基础的要求,也说明我国对数据的使用方式还停留在最初级的阶段。
|
||||
|
||||
另一个就是移动 BI 需求,在移动端看报表,PC 端做报表已经非常普遍了。
|
||||
|
||||
之所以数据填报排到了第三名,是因为不同公司并不是所有数据都统一管理,BI 支持数据填报,就可以将遗漏的数据录入进去。
|
||||
|
||||
相信在未来,这个条形图最长边会逐渐移动到腰部。
|
||||
|
||||
最后是企业面临的综合挑战:
|
||||
|
||||
- (64.8%)数据的整合与治理
|
||||
- (58.1%)与管理层及业务部门的配合
|
||||
- (51.8%)数据人才的培养
|
||||
- (49.8%)数据分析工具的选择
|
||||
- (42.4%)IT 部门自身的能力提升
|
||||
- (38.1%)衡量数据分析的价值产出
|
||||
- (27.6%)公司重视程度或预算投入
|
||||
- (14.1%)项目风险的控制
|
||||
|
||||
数据整合与治理是最大问题再次反映了我国数据可视化处于较为初级阶段,第二名的 “与管理层及业务部门的配合”,也印证了这一点,如何将数据价值传达给管理层,让管理层认可前期投入在未来是可以得到回报的,是在企业里做数据分析比较头疼的问题,而其他业务部门如果不予配合,不将数据交给数据中台部门,又难以解决数据整合的问题,而这个往往又依赖管理层的决定,因此管理层与业务部门的配合问题是相辅相成的。
|
||||
|
||||
第三名是数据人才培养的问题,这个问题笔者认为还好,前几年流行大数据人才,近几年流行 AI 人才,我国数据人才应该有不少的储备。
|
||||
|
||||
后面几项最重要的就是 衡量数据分析的价值产出,任何做数据的部门,如果不能让数据为公司带来价值,这件事件就没有可持续性。笔者建议从数据整合后的管理提效,节省机器成本的角度计算出收益,从数据分析平台为其他业务部门提供的决策依据,计算出为业绩提高作出的贡献,再从对公司内部做报表、邮件的研发人力节省,管理层快速查看公司整体实时数据分析的角度计算出软贡献价值。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
尽管 BI 平台与数据分析可以为公司带来巨大的价值,但制作 BI 平台的成本是相当大的,而且 BI 平台具有马太效应,目前国际第一梯队的 Tableau、PowerBI 无论是吸引的人才,投入的资源,市场份额都远超追赶者的总和。
|
||||
|
||||
<img width=800 src="https://img.alicdn.com/tfs/TB1xd_1b2WG3KVjSZFPXXXaiXXa-1860-1027.png">
|
||||
|
||||
从 17-18,18-19 的 BI 四维度对比可以看出,低端 BI 的角逐正在越来越激烈,行业龙头 PowerBI 与 Tableau 位置越来越稳,国内 BI 龙头 FineBI,以及正在逐渐发力的 QuickBI 希望能挤进国际梯队,在 BI 技术领域拉平与发达国家的差距。
|
||||
|
||||
> PS:目前国内市场的情况,反而不适应 PowerBI 与 Tableau 阶段的 BI 工具,给国产 BI 工具创造了发展机遇,我们要抓住这次机遇带领中国数据市场走向第三代增强分析型,并使国内 BI 工具在国际市场占有一席之地。
|
||||
|
||||
> 讨论地址是:[精读《数据之上·智慧之光 - 2018》 · Issue #162 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/162)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
**special Sponsors**
|
||||
|
||||
- [DevOps 全流程平台](https://e.coding.net/?utm_source=weekly)
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,383 @@
|
||||
# 1. 引言
|
||||
|
||||
备受开发者喜爱的特性 [Optional chaining](https://github.com/tc39/proposal-optional-chaining) 在 2019.6.5 进入了 stage2,让我们详细读一下草案,了解一下这个特性的用法以及讨论要点。
|
||||
|
||||
借着这次精读草案,让我们了解一下一个完整草案的标准文档结构是怎样的。
|
||||
|
||||
一个新特性的文档,首先要描述 **起因** 是什么,也就是为什么要增加这个特性,大家不会没有理由的就增加一个特性。其次是**其他语言是否有现成的实现版本**,参考他们并进行归纳总结,可以增加思考角度的全面性。
|
||||
|
||||
第三点就是 **语法介绍**,也就进入了新特性的正题,这里要详细介绍所有可能的使用情况。第四点是 **语义**,也就是诠释语法的含义。
|
||||
|
||||
然后是可选的 **是否有不支持的情况**,对于不支持的点是否有意而为之,为什么?此处一般会留下讨论的 ISSUE。然后是 **暂不考虑的点**,是由于性价比低、使用场景少,或者实现成本高的原因,为什么某些已经想到的点暂不考虑,这里也会留下讨论的 ISSUE。
|
||||
|
||||
后面一般还有 “正在讨论的点”、“FAQ”、“草案进度”、“参考文献”、“相关问题”、“预先讨论资料” 等内容。
|
||||
|
||||
# 2. 概述&精读
|
||||
|
||||
首先让我们回顾一下什么是 **“Optional chaining”**。
|
||||
|
||||
## 起因介绍
|
||||
|
||||
当访问一个深层树形结构的对象时,我们总需要判断中间节点属性是否存在:
|
||||
|
||||
```js
|
||||
var street = user.address && user.address.street;
|
||||
```
|
||||
|
||||
而且很多 API 返回的属性都可能为 Null,而我们往往只想获取非 Null 时的结果:
|
||||
|
||||
```js
|
||||
var fooInput = myForm.querySelector('input[name=foo]')
|
||||
var fooValue = fooInput ? fooInput.value : undefined
|
||||
```
|
||||
|
||||
> 笔者这里补充,在人机交互的领域,可能为 Null 的情况很多。首先是交互行为模块很多,行为复杂,很容易导致数据分散且难以预测(可能为空),仅是 DOM 元素就需要太多兼容,因为 DOM 被修改的实际太多了,大家都在共享一个可变的结构;其次是交互过程中间状态很多,出现状态残缺的可能性也很大,就拿 SQL 解析为例:后端只要检测 Query 是否正确就可以了,但前端的 SQL 编辑器需要在输入不完整的情况下给出提示,也就是在语法树错误的情况下给出提示,因此需要进行容错。
|
||||
|
||||
而 Optional chaining 可以解决为了容错而写过多重复代码的问题:
|
||||
|
||||
```js
|
||||
var street = user.address?.street
|
||||
var fooValue = myForm.querySelector('input[name=foo]')?.value
|
||||
```
|
||||
|
||||
正如上面的例子:如果 `user.address` 为 `undefined`,那 `street` 拿到的就是 `undefined`,而不是报错。
|
||||
|
||||
配合另一个在 stage2 的新特性 [Nullish Coalescing](https://github.com/tc39/proposal-nullish-coalescing) 做默认值处理非常方便:
|
||||
|
||||
```js
|
||||
// falls back to a default value when response.setting is missing or nullish
|
||||
// (response.settings == null) or when respsonse.setting.animationDuration is missing
|
||||
// or nullish (response.settings.animationDuration == null)
|
||||
const animationDuration = response.settings?.animationDuration ?? 300;
|
||||
```
|
||||
|
||||
`??` 号可以理解为 “默认值场景下的 `||`”:
|
||||
|
||||
```js
|
||||
const response = {
|
||||
settings: {
|
||||
nullValue: null,
|
||||
height: 400,
|
||||
animationDuration: 0,
|
||||
headerText: '',
|
||||
showSplashScreen: false
|
||||
}
|
||||
};
|
||||
|
||||
const undefinedValue = response.settings?.undefinedValue ?? 'some other default'; // result: 'some other default'
|
||||
const nullValue = response.settings?.nullValue ?? 'some other default'; // result: 'some other default'
|
||||
const headerText = response.settings?.headerText ?? 'Hello, world!'; // result: ''
|
||||
const animationDuration = response.settings?.animationDuration ?? 300; // result: 0
|
||||
const showSplashScreen = response.settings?.showSplashScreen ?? true; // result: false
|
||||
```
|
||||
|
||||
`0 || 1` 的结果是 `1`,因为 `0` 判定为 `false`,而 `||` 在前面的变量为 `false` 型才继续执行,而我们想要的是 “前面的对象不存在时才使用后面的值”。`??` 则代表了 “前面的对象不存在” 这个含义,即便值为 `0` 也会认为这个值是存在的。
|
||||
|
||||
Optional chaining 也可以用在方法上:
|
||||
|
||||
```js
|
||||
iterator.return?.()
|
||||
```
|
||||
|
||||
或者试图调用某些未被实现的方法:
|
||||
|
||||
```js
|
||||
if (myForm.checkValidity?.() === false) { // skip the test in older web browsers
|
||||
// form validation fails
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
比如某个旧版本浏览器不支持 `myForm.checkValidity` 方法,则不会报错,而是返回 `false`。
|
||||
|
||||
## 已有实现调研
|
||||
|
||||
Optional chaining 在 C#、Swift、CoffeeScript、Kotlin、Dart、Ruby、Groovy 已经实现了,且实现方式均有差异,可以看到每个语言在实现语法时都是有取舍的,但是大方向基本是相同的。
|
||||
|
||||
想了解其他语言是如何实现 Optional chaining 的读者可以 [点击阅读原文](https://github.com/tc39/proposal-optional-chaining#prior-art)。
|
||||
|
||||
这些语言实现 Optional chaining 的差异基本在 **语法、支持范围、边界情况处理** 等不同,所以如果你每天要在不同语言之间切换工作,看似相同的语法,但不同的细节可能把你绕晕(所以会的语言多,只会让你变成一个速记字典,满脑子都是哪些语言在哪些语法讨论倾向哪一边,选择了哪些特性这些毫无意义的结论,如果不想记这些,基础语法都没有掌握怎么好意思说会这门语言呢?所以学 JS 就够了)。
|
||||
|
||||
## 语法
|
||||
|
||||
Optional Chaining 的语法有三种使用场景:
|
||||
|
||||
```js
|
||||
obj?.prop // optional static property access
|
||||
obj?.[expr] // optional dynamic property access
|
||||
func?.(...args) // optional function or method call
|
||||
```
|
||||
|
||||
也就是将 `.` 替换为 `?.`,但要注意第二行与第三行稍稍有点反直觉,比如在函数调用时,需要将 `func(...args)` 写为 `func?.(...args)`。至于为什么语法不是 `func?(...args)` 这种简洁一点的表达方式,在 FAQ 中有提到这个例子:
|
||||
|
||||
`obj?[expr].filter(fun):0` 引擎难以判断 `obj?[expr]` 是 Optional Chaning,亦或这是一个普通的三元运算语句。
|
||||
|
||||
可见,要支持 `?.` 这个看似简单的语法,在整个 JS 语法体系中要考虑的边界情况很多。
|
||||
|
||||
即便是 `?.` 这样完整的用法,也需要注意 `foo?.3:0` 这种情况,不能将 `foo?.` 解析为 Optional chanining,而要将其解析为 `foo? .3 : 0`,这需要解析引擎支持 lookahead 特性。
|
||||
|
||||
## 语义
|
||||
|
||||
**当 `?.` 前面的变量值为 `null` 或 `undefined` 时,`?.` 返回的结果为 `undefined`**。
|
||||
|
||||
```js
|
||||
a?.b // undefined if `a` is null/undefined, `a.b` otherwise.
|
||||
a == null ? undefined : a.b
|
||||
|
||||
a?.[x] // undefined if `a` is null/undefined, `a[x]` otherwise.
|
||||
a == null ? undefined : a[x]
|
||||
|
||||
a?.b() // undefined if `a` is null/undefined
|
||||
a == null ? undefined : a.b() // throws a TypeError if `a.b` is not a function
|
||||
// otherwise, evaluates to `a.b()`
|
||||
|
||||
a?.() // undefined if `a` is null/undefined
|
||||
a == null ? undefined : a() // throws a TypeError if `a` is neither null/undefined, nor a function
|
||||
// invokes the function `a` otherwise
|
||||
```
|
||||
|
||||
### 短路
|
||||
|
||||
所谓短路,就是指引入了 Optional chaining 后,某些看似一定会执行的语句在特定情况下会短路(终止执行),比如:
|
||||
|
||||
```js
|
||||
a?.[++x] // `x` is incremented if and only if `a` is not null/undefined
|
||||
a == null ? undefined : a[++x]
|
||||
```
|
||||
|
||||
第一个例子,如果 `a` 时 `null/undefined`,就不会执行 `++x`。
|
||||
|
||||
原因是这段代码部分等价于 `a == null ? undefined : a[++x]`,如果 `a == null` 为真,自然不会执行 `a[++x]` 这个语句。但由于 Optional chaining 使这个语句变得 “简洁了”,虽然带来了便利,但也可能导致看不清完整的执行逻辑,引发误判。
|
||||
|
||||
所以看到 `?.` 语句时,一定要反射性的思考一下,这个语句会触发 “短路”。
|
||||
|
||||
### 长“短路”
|
||||
|
||||
Optional chaining 在 JS 的规范中,作用域仅限于调用处。看下面的例子:
|
||||
|
||||
```js
|
||||
a?.b.c(++x).d // if `a` is null/undefined, evaluates to undefined. Variable `x` is not incremented.
|
||||
// otherwise, evaluates to `a.b.c(++x).d`.
|
||||
a == null ? undefined : a.b.c(++x).d
|
||||
```
|
||||
|
||||
可以看到 `?.` 仅在 `a?.` 这一层生效,而不是对后续的 `b.c`、`c(++x).d` 继续生效。而对于 C+ 与 CoffeeScript,这个语法是对后续所有 `get` 生效的(**这里再次提醒,不要用 `CoffeeScript` 了,因为对于相同语法,语义都发生了变化,对你与你的同事都是巨大的理解负担,或者说没有人愿意注意,为什么代码在 `CoffeeScript` 里不报错,而转移到 JS 就报错了,是因为 Optional chaining 语义不一致造成的。**)。
|
||||
|
||||
正因为 Optional chaining 在 JS 语法中仅对当前位置起保护作用,因此一个调用语句中允许出现多个 `?.` 调用:
|
||||
|
||||
```js
|
||||
a?.b[3].c?.(x).d
|
||||
a == null ? undefined : a.b[3].c == null ? undefined : a.b[3].c(x).d
|
||||
// (as always, except that `a` and `a.b[3].c` are evaluated only once)
|
||||
```
|
||||
|
||||
上面这段代码,对 `a?.b`、`c?.(x)` 的访问与调用是安全的,而对于 `b[3]`、 `b[3].c`、`c?.(x).d` 的调用是不安全的。
|
||||
|
||||
在 FAQ 环节也提到了,为什么不学习 C# 与 CoffeeScript 的语义,将安全保护从 `a?.` 之后就一路 “贯穿” 下去?
|
||||
|
||||
原因是 JS 对 Optional chaining 的理解不同导致的。Optional chaining 仅仅是安全访问保护,不代表 `try catch`,也就是它不会捕获异常,举一个例子:
|
||||
|
||||
```js
|
||||
a?.b()
|
||||
```
|
||||
|
||||
这个调用,在 `a.b` 不是一个函数时依然会报错,原因就是 Optional chaining 仅提供了对属性访问的安全保护,不代表对整个执行过程进行安全保护,该抛出异常还是会抛出异常,因此 Optional chaining 没有必要对后面的属性访问安全性负责。
|
||||
|
||||
笔者认为 TC39 对这个属性的理解是合理的,否则用 `try catch` 就能代替 Optional chaining 了。**让一个特性仅实现分内的功能,是每个前端从业者都要具备的思维能力。**
|
||||
|
||||
> PS:笔者再多提一句,在任何技术设计领域,这个概念都适用。想想你设计的功能,写过的函数,如果为了图方便,扩大了其功能,终究会带来整体设计的混乱,适得其反。
|
||||
|
||||
### 边界情况 - 分组
|
||||
|
||||
我们知道,JS 代码可以通过括号的方式进行分组,分组内的代码拥有更高的执行优先级。那么在 Optional chaining 场景下考虑这个情况:
|
||||
|
||||
```js
|
||||
(a?.b).c
|
||||
(a == null ? undefined : a.b).c
|
||||
```
|
||||
|
||||
与不带括号的进行对比:
|
||||
|
||||
```js
|
||||
a?.b.c
|
||||
a == null ? undefined : a.b.c
|
||||
```
|
||||
|
||||
我们会发现,由于括号提高了优先级,导致在 `a` 为 `null/undefined` 时,解析出了 `undefined.c` 这个必定报错的荒谬语法。因此我们不要试图为 Optional chaining 进行括号分组,这样会打破逻辑顺序,使安全保护不但不生效,反而导致报错。
|
||||
|
||||
### Optional delete
|
||||
|
||||
中文大概可以翻译为 “安全删除” 吧,也就是 JS 的 Optional chaining 支持下面的使用方式:
|
||||
|
||||
```js
|
||||
delete a?.b
|
||||
a == null ? true : delete a.b
|
||||
```
|
||||
|
||||
这样不论 `b` 是否存在,得到的都是 `b` 删除成功的信号(返回值 `true`)。
|
||||
|
||||
至于为什么要支持 Optional delete,草案里也有提到,笔者认为非常有意思:
|
||||
|
||||
讨论重点应该是 “我们为什么不支持 Optional delete”,而不是 “我们为什么要支持 Optional delete”,有点像反证法的思路。由于 Optional delete 具备一定的使用场景,而且支持方式零成本(改写为 `a == null ? true : delete a.b` 即可),所以就支持它吧!
|
||||
|
||||
## 不支持的特性
|
||||
|
||||
下面三个特性不支持,原因是没什么使用场景:
|
||||
|
||||
- 安全的 construction:`new a?.()`
|
||||
- 安全的 template literal:a?.\`string\`
|
||||
- 上面两者的结合:`new a?.b()`, a?.b\`string\`
|
||||
|
||||
首先看 new 一个对象,如果 new 出来的结果是 `undefined`,那这个返回值使用起来也没有意义。
|
||||
|
||||
对于第二个安全的 template literal 来说,比如下面的语法:
|
||||
|
||||
```js
|
||||
a?.b
|
||||
`c`
|
||||
```
|
||||
|
||||
会被解析为
|
||||
|
||||
```js
|
||||
a == null ? undefined : a.b`c`
|
||||
```
|
||||
|
||||
那么对于下面这种翻译结果:
|
||||
|
||||
```js
|
||||
a == null ? undefined : a.b `c`
|
||||
```
|
||||
|
||||
目前不会有人这么写代码,因为这种语法的使用场景一般都是 “前面的属性必定存在时的简化语法”,比如 `styled-components` 的:
|
||||
|
||||
```js
|
||||
div`
|
||||
width: 300px;
|
||||
`
|
||||
```
|
||||
|
||||
而如果解析为:
|
||||
|
||||
```js
|
||||
(a == null ? undefined : a?.b) `c`
|
||||
```
|
||||
|
||||
则更不会有人愿意尝试这种写法,所以安全的 template literal 这种需求是不存在的,自然第三种需求也是不存在的。
|
||||
|
||||
下面一个不支持的特性,虽然有一定使用场景,但依然被否定的:
|
||||
|
||||
- 安全的赋值:`a?.b = c`
|
||||
|
||||
[讨论 ISSUE](https://github.com/tc39/proposal-optional-chaining/issues/18)
|
||||
|
||||
笔者总结一下,一共有这几种令人烦恼的地方,导致大家不想支持 **安全赋值** 特性:
|
||||
|
||||
**短路特性导致的理解成本:**
|
||||
|
||||
比如 `a?.b = c()`,如果 `a` 为 `null/undefined`,那么函数 `c()` 就不会被执行,这种语法太违背开发者的常识,如果支持这个特性带来的理解负担会很大。
|
||||
|
||||
**连带考虑场景很多:**
|
||||
|
||||
如果支持了这种看似简单的赋值场景,那么至少还有下面五种赋值场景需要考虑到:
|
||||
|
||||
- 简单赋值: `a?.b = c`
|
||||
- 聚合赋值: `a?.b += c, a?.b >>= c`
|
||||
- 自增,自减: `a?.b++, --a?.b`
|
||||
- 解构赋值: `{ x: a?.b } = c, [ a?.b ] = c`
|
||||
- for 循环中的临时赋值: `for (a?.b in c), for (a?.b of c)`
|
||||
|
||||
总和这几种考虑,支持安全赋值会带来更多灵活的用法,导致代码复杂度陡增(想想你的同事大量使用上面的后四种例子,你绝对想要找他决斗,因为这种写法和乱用 window 变量一样,在 JS 允许的框架内写出难以维护的逻辑,像是钻了法律的孔子),因此 TC39 决定不支持这种用法,从源头上杜绝被滥用。
|
||||
|
||||
以上不支持的功能点会在静态编译时被禁止,但以后也许会重新讨论。
|
||||
|
||||
另外对于 Class 的私有变量是否支持 `a?.#b` `a?.#b()` 还在讨论中,这取决于私有成员变量草案是否能最终落地。
|
||||
|
||||
## 暂不讨论的点
|
||||
|
||||
目前有两个 Optional chaining 功能点暂不讨论,分别是 [Optional spread](https://github.com/tc39/proposal-optional-chaining/issues/55) 与 [Optional destructuring](https://github.com/tc39/proposal-optional-chaining/issues/74)
|
||||
|
||||
对于 Optional spread,建议是:
|
||||
|
||||
```js
|
||||
const arr = [...?listOne, ...?listTwo];
|
||||
foo(...?args);
|
||||
```
|
||||
|
||||
但由于可以结合 [Nullish Coalescing](https://github.com/tc39/proposal-nullish-coalescing) 达到同样的效果:
|
||||
|
||||
```js
|
||||
foo(...args ?? [])
|
||||
```
|
||||
|
||||
所以暂时不深入讨论,因为存在意义不大。
|
||||
|
||||
对于 Optional destructuring,建议是:
|
||||
|
||||
|
||||
```js
|
||||
// const baz = obj?.foo?.bar?.baz;
|
||||
const { baz } = obj?.foo?.bar?;
|
||||
```
|
||||
|
||||
也就是对于解构用法,在最后一个位置添加 `?`,使其能安全的解构。
|
||||
|
||||
但由于基于这个特性会演变出太多的使用变体:
|
||||
|
||||
```js
|
||||
const {foo ?: {bar ?: {baz}}} = obj?
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```js
|
||||
const {
|
||||
foo?: {
|
||||
bar?: { baz }
|
||||
}
|
||||
} = obj;
|
||||
```
|
||||
|
||||
对开发者的理解成本压力较大,毕竟 Optional chaining 的出发点只是 `?.` 这么简单。而且对于默认值,我们又有 `??` 语法可以快速满足,因此这个特性的讨论也被搁置了。
|
||||
|
||||
## 余下的 Q&A
|
||||
|
||||
大部分 Q&A 在上面的解读都有提及,下面列出剩余的两个 Q&A:
|
||||
|
||||
### 为什么语法是 `?.` 而不是 `.?` ?
|
||||
|
||||
原因是与三元运算符冲突了,思考下面的用法:
|
||||
|
||||
```js
|
||||
1.?foo : bar
|
||||
```
|
||||
|
||||
在 js 中,`1.` 等价于 `1`,那么这就是一个标准的三元运算表达式,因此 `.?` 语法会产生歧义,只能选择 `?.`。
|
||||
|
||||
### 为什么 `null?.b` 的结果不是 `null` 呢?
|
||||
|
||||
由于 `.` 表达式不关心 `.` 前面对象的类型,因为它的目的是访问 `.` 后面的属性,因此不会因为 `null?.b` 就返回 `null`,而是统一返回 `undefined`。
|
||||
|
||||
最后,需要 TC39 最终审核后,Optional chaining 才能进入 Stage3,我们拭目以待吧!
|
||||
|
||||
# 3. 总结
|
||||
|
||||
写一篇 JS 特性草案的完整解读真的很累,以后也许很少有机会这么完整的解读草案了,但希望借着这次解读 Optional chaining 的机会,让大家理解 TC39 是如何制定草案的,草案都在讨论什么,怎么讨论的,流程有哪些。
|
||||
|
||||
同时,还希望让大家意识到,为一个语言添加一个看似简单的新特性有多么的不容易,一个简单的 `?.` 语法就牵涉到与三元运算符、分组、解构等等已存在语法的交织与冲突,所以想要安全又妥当的添加一个新特性,参与讨论的人必须对 JS 语言有完整全面的理解,同时也要对边界情况考虑的很周全,懂得对语法融会贯通。
|
||||
|
||||
最后,希望大家可以意识到,JS 这么重量级的语言,一个新的语法特性其实也是这么三言两语讨论下来的,其中不乏有一些拍脑袋的地方、对于“即可也可”的情况,稍稍结合一些具体案例就定下来其中一种的现象也是存在的,甚至对于某些规范点根本不存在一个完美的 “真理”,比如为什么语法是 `?.` 而不是 `a&.b`(Ruby 使用的就是 `&.`),认清了这种情况存在,就不会执着于 “语法的学习”,而转向更底层,更有用的 “语义的学习”,并能通过阅读 TC39 的草案了解其他语言的实现差异,从而快速掌握其他语言的语法。
|
||||
|
||||
> 讨论地址是:[精读《Optional chaining》 · Issue #165 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/165)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
**special Sponsors**
|
||||
|
||||
- [DevOps 全流程平台](https://e.coding.net/?utm_source=weekly)
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
# 1. 引言
|
||||
|
||||
[智能商业](https://book.douban.com/subject/30357931/) 是阿里巴巴前总参谋长曾鸣于 2018-11 出版的商业图书,对最近 20 年中国商业以及互联网发展有着深刻的总结,并描述了未来智能商业的蓝图。
|
||||
|
||||
笔者之所以读这本书,是因为笔者所在阿里巴巴数据中台,需要更深刻的理解数据,而《智能商业》就提到了数据时代的变革,对笔者工作有所帮助。
|
||||
|
||||
但读完这本书后,笔者发现不同人站在不同视角会有不同的理解:如果你是一名数据行业从业者,你可以理解数据在当今行业发展中如何起到作用;如果你是企业高管,你会领悟到商业平台发展的规则;如果你是一名创业者,你能体会到点线面体的存在,找到自己的定位;如果你是一名管理者,你能领域到管理模式正在发生的变化;如果你是一名传统行业从业者,你能体会到为什么互联网会对传统行业带来这么大的冲击;如果你是一名社会评论家,你会找到衡量智能时代对人类社会带来影响的标尺,等等。商业是推动人类社会发展的源动力,甚至也是文化与战争的源头,智能商业正因为将商业讲的通透,才摆脱了普通商业书籍枯燥的理论体系,从社会实践中总结理论,最终能上升到富有哲理的思考。
|
||||
|
||||
智能商业一书中有许多关键词,比如 “三浪叠加” “网络协同” “数据智能” “C2B” “S2B2C” “点线面体” “创造力革命” “网红” “互联网 X” 等等,能将这些关键词串起来的,笔者认为是 “商业演化”,在近几十年范围内,商业模式存在一些不变底层逻辑(“三浪叠加” “网络协同” “数据智能”),而在大趋势下存在不断演变的商业模式(“C2B” “S2B2C” “点线面体” “创造力革命” “网红” “互联网 X”)。
|
||||
|
||||
读完书后会发现,这么多的关键词,最终都为了实现 “C2B” 这个商业最终演化目标,即便是远在十八世纪的工业革命,也在为 C2B 模式打下让物质资源极大丰富的生产力基础,而网络协同和数据智能,都为了让商业规模更大,精准度更强,可以个性化识别每个用户的需求。新的组织模式也是为了更高效服务用户,整合社会 “点线面体” 的生态关系最终可以形成 “C2B” 的服务网络,而网红、互联网 X 都是 C2B 转型在不同阶段、不同行业的尝试。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
智能商业全书分为六个章节,分别是 “智能商业”、“商业模式变革”、“战略变革”、“组织变革”、“案例分析”、“关于未来”。
|
||||
|
||||
笔者看过一些类似的书评,将书中的观点一一枚举出来,这样的解读笔者认为是难以抓到重点的。看似把重点一一提取了出来,但没有一条 “逻辑线” 将其贯穿,分散的理解任何一个知识点都不会有太大的帮助。而这条 “逻辑线” 其实就是作者的目录组织结构。
|
||||
|
||||
任何一本书,写作的目的是作者为了全面阐述一个观点,书中的重点都是一个个割裂的小观点,作者会通过目录方式组织一条最合理的逻辑路线,将这些重点串联起来,最终引出作者想阐述的大观点(智能商业),因此请跟着笔者从这本书的章节结构开始,有一个连贯的理解。
|
||||
|
||||
**前言**
|
||||
|
||||
前言笔者认为是最精彩的部分,因为提到了一个核心概念 “三浪叠加”,中国人口众多,土地广袤,互联网发展程度不均衡,因此任何互联网模式都可能存在,再加上互联网自身演化很快,当第二浪盖过第一浪时,第三浪已经悄然形成了,只从规模上可能难以分辨处于尾声的第一浪与处于巅峰的第二浪,更难分辨出还没有起色的第三浪在哪。读完这本书如果能看清楚中国商业发展的前三浪,并预测出未来三浪,目的就达到了。
|
||||
|
||||
**智能商业**
|
||||
|
||||
第一章的名字和书名一样,表示我们现在正处于智能商业时代。通过对中国社会的分析,解释了为什么商业时代发展的这么快,而且为什么创业方向那么多,有些行业快速崛起,有些行业快速衰退,而想要抓住未来,就要把握住互联网机遇,利用**网络协同**与**数据智能**实现智能商业,然后为什么这样的智能商业模式可以胜出。
|
||||
|
||||
**商业模式变革**
|
||||
|
||||
第二章讲的是商业模式由传统的 B2C 逐渐演变到 C2B,而在 C2B 演变的过程中,一种过渡阶段 S2B2C 正在快速崛起,而这些名词并非人为创造,而是商业发展自然演化而来的,能理解到 S2B2C 是通向 C2B 的自然演化路径,自然就能理解现在一些企业模式(比如网红、大搜车)等,也能自然理解 S2B2C 的不足(毕竟是过渡阶段),未来的战略方向自然就清晰了。
|
||||
|
||||
**战略变革**
|
||||
|
||||
前两章分别介绍了什么是智能商业,为什么要做智能商业,以及商业模式的演变,那第三章就自然要介绍企业战略变革了。第三章介绍了企业战略如何转型才能应对智能商业的节奏,比如何制定战略计划,以及通过 点-线-面-体 理解企业在市场中的定位,理解了这一点,不仅能理解各企业在市场中定位,还能理解之间相互关系,以及 点-线-面-体 的定位是可以改变的,抓住机遇的企业会逐渐向上发展,失去机遇的企业会逐渐向下退化。
|
||||
|
||||
理解了 点-线-面-体 的特性,可以更好的找准自己的定位,越往上资源越多,但排他性就越强,大部分时候,做一个深耕垂直行业的点,虽然同质化可能很多,但竞争不是排他性的,而且有线与面的平台支撑,特别是合理利用多个 “面” 后,可能爆发出强劲的商业价值,比如网红就是同时利用多个 “面” 的典型例子。
|
||||
|
||||
> 从战略变革这一章可以看到,这本书虽然前两章站在 BAT 高级战略的视角俯瞰商业演化,看似与普通企业,普通个人没什么关系,但读到战略变革这一章时,可以明显体会到理解 “面” 与 “体” 角度下商业思维后,可以给 “点” 与 “线” 带来巨大的战略价值。
|
||||
|
||||
**组织变革**
|
||||
|
||||
第四章是组织变革,因为当战略变革后,必须轮到组织变革了。工业革命带来了生产力的极大提高,那互联网则带来了创造力革命的浪潮,没有统一机器的约束,每个人都能充分发挥自己的创造力 - 前提是组织管理模式要支持。一个新的组织管理模式不是自上而下的分配任务,而是自下而上,充分发挥每个人创造力的 “赋能” 管理模式。都是互联网的管理模式是打平的,其实这是终极的理想情况,通过形成自组织协同网络,充分调动每一个人的创造力。
|
||||
|
||||
**案例分析**
|
||||
|
||||
读到第五章就没有多少新概念了,但第五章是真正把前四章理论映射到现实案例的实战环节,这一章我们能看懂许多企业战略背后的战略模式,都可以归纳到网络协同、数据智能的布局,商业模式都在向 C2B 转型,旧的面被新的面取代而下降为线,线抓住了机遇逐渐发展成面,多个面相互协同逐渐形成了 “体” 等多个维度的变化。
|
||||
|
||||
**关于未来**
|
||||
|
||||
第六章是对未来的判断,重点在互联网与传统产业如何碰撞,提出的 互联网 x 概念背后有着更深刻的含义。如果你今年听说了 “产业物联网” 这个名词,可以甄别一下相应的企业,是仅仅将互联网技术运用到了传统行业,还是将传统行业从底层的运作逻辑就互联网化了呢?互联网不仅是一种技术,更是一种思维,互联网思维可以将被传统行业束缚住的各个流程逐渐还原到最原始、高效的模样。
|
||||
|
||||
比如说传统工程需要提前计算销量固化产能,但加入了互联网快速反馈的网络,就可以实时调整产能,当然这需要整个生产流程的互联网化,将整个环节都做到快速反馈。
|
||||
|
||||
**结语 - 新文明:感受未来已来**
|
||||
|
||||
印象最深的是引用了经济学家周其仁的一句话:“文明的一次次传承和复兴,就是一步步找回对人的尊重”。害怕机器取代人类的思想还是被局限在现有的世界观、价值观之中的,将工人固定在工厂流水线,或者程序员每天写着相似的业务逻辑,本身就是一种践踏人类尊严的行为,而计算机可以逐步取代这些低创造性的工作,可以理解为抢了那些人的饭碗,但站在历史长河的角度,何不是还给人类以尊严?
|
||||
|
||||
## 智能商业
|
||||
|
||||
首先是分析互联网巨头都至少做对了这三个方向中的两个:**在线化、智能化、网络化**。
|
||||
|
||||
在线化是指将业务都搬到互联网上,这基本是必备的一条。智能化是利用算法打造竞争优势,比如谷歌搜索算法。网络化就是形成多方共赢的协作网络,比如广告主与网站主通过谷歌搜索形成网络化协作。
|
||||
|
||||
简介提到的 **网络协同**与**数据智能** 就是指后两者,它们之间要形成一种反馈闭环就形成了智能商业的双螺旋:
|
||||
|
||||
**网络协同** 产生数据,通过 **数据智能** 进行学习,进一步优化 **网络协同**。
|
||||
|
||||
**网络协同** 需要建立起一张多角色之间的协同网,比如优步组织的司机与乘客的协同网。协同网络越复杂,经济效益越大、门槛越高,比如淘宝的协同网络非常复杂,体现在协同者多(买家,卖家,物流,客服,淘女郎)等等,他们之间也有相互关联,各角色对网络需求粘性强,网络的不可替代性就高。
|
||||
|
||||
**数据智能** 现在所有企业都没有充分利用数据,数据的潜在价值是无穷的,理论上可以利用数据做任何战略决策、管理决策。
|
||||
|
||||
而网络化与智能化叠加,会产生黑洞效应,也就是数据越多越吸附数据,网络协同越多就越容易扩张出新的协同。
|
||||
|
||||
作者对 **互** **联** **网** 这三个字的拆字解读也更容易让我们理解互联网的本质:
|
||||
|
||||
**联:** 联接,从 PC 互联网开始,到移动互联网,再到万物互联,联接内容越来越多。
|
||||
|
||||
**互:** 交互,从一对多的门户时代,到通过关注方式的微博时代,再到社交朋友圈时代,交互越来越简单,越来越频繁,也越来越精准。
|
||||
|
||||
**网:** 网络协同。
|
||||
|
||||
看了这么多概念,不知道你是否能理解智能商业的概念呢?也许每个人都有自己的体会,也许智能商业概念难以被定义,但 **网络协同**、**数据智能** 一定是核心,谁能充分利用这两股力量,将其充分发挥黑洞效应,形成一套更广泛的“互”,更多的“联”,更复杂的“网”络协同,谁就能更好利用互联网实现智能商业。
|
||||
|
||||
## 商业模式变革
|
||||
|
||||
商业领域较为常见的模式有 B2B、B2C、C2C。
|
||||
|
||||
B2B 代表企业是阿里巴巴、中化网,阿里巴巴是水平 B2B,是指企业与客户之间是平行关系;而中化网属于垂直 B2B,帮助企业寻找上下游合作伙伴。
|
||||
|
||||
B2C 代表企业是亚马逊、天猫、京东,也就是直接把商品卖给消费者。
|
||||
|
||||
C2C 代表企业是易贝、淘宝,即个人用户服务与个人,淘宝主要是个人用户开网店卖给个人。
|
||||
|
||||
而商业模式的变革,是指这些模式最终都要演化为 C2B 模式,即个人提出需求,企业快速满足。按照笔者理解,C2B 是由客户驱动的模式,虽然只是简单的单词调整位置,但背后需要企业做巨大的转型,不仅组织结构需要调整,还需要企业具有第一章说的 “智能商业” 属性,因为只有将服务在线化,通过数据智能与网络协同,才能精准触达每一位消费者,了解每个人的需求,快速服务与消费者。
|
||||
|
||||
然而快速服务消费者的需求还需要背后的供应链平台支持,所以 C2B 将以客户驱动的模式一直改造到背后的供应链逻辑。
|
||||
|
||||
然而 C2B 模式跨度太大,最近还诞生了一种过渡模式,就是 S2B2C 的模式,S 指的是供应平台,通过对小 B 的赋能,让小 B 直接服务于 C。这种模式是看场景的,因为只有 S2B 的价值大于单纯的 B,这个模式才行得通,所以在比如汽车、医药行业,小 B 急需 S 赋的业务场景可以做起来,而在本身就有大 B 存在的行业,就算有 S 赋能,小 B 依然竞争不过大 B,就不适合 S2B2C 这种模式。
|
||||
|
||||
另外 S2B2C 的模式也在升级,未来的产品可能会同时透出 S 于 B 的品牌,因为只透出 B 的品牌,可能导致 S 不能很好的掌握消费者需求,只透出 S 的品牌,就变成了传统加盟模式,而加盟模式最大的问题是无法发挥每个小 B 的积极性触达客户,加盟本质上还是 B2C,比如肯德基,一个大品牌对应每个消费者,就算加盟再多店铺也不会改变这一点,但是 S2B2C 比如网红模式,淘宝平台给网红赋能,网红通过自己的品牌吸引能力圈住一批客户,带来非常高的转化率,这就结合了两者优势。
|
||||
|
||||
另外也提到了云集,笔者以前认为云集是一种传销模式,和微商差不多,但其实云集要做的事情就是 S2B2C,将供应链完全打通后,包括网络系统一并提供给小 B,云集的小 B 就是任何有微信的用户,用户的资源就是他的朋友(朋友圈),所以云集号称没有商品就能做卖家,因为它的 S 服务做得好,集成性高,给小 B 带来的便利性就高。但问题是 小 B 到 C 环节是云集的弱势环节,拥有朋友圈的普通人与网红有本质的区别,普通人随意转发消息也许会带来朋友的反感与屏蔽,而普通人也不能为客户带来更大的价值,反观网红,他们可以得到粉丝的认可,成为粉丝的榜样,但是你愿意认可朋友圈里随便一个人成为你的榜样吗?
|
||||
|
||||
第二章总的来说解读了目前出现的网红现象,以及一些做的较好的独角兽(比如土巴兔、大搜车),其实他们都属于 S2B2C 的模式,而他们最终的目的地是 C2B。
|
||||
|
||||
## 战略变革
|
||||
|
||||
既然商业模式变革了,战略也要变革。之前也说过互联网处于三浪叠加状态,从 B2B 开始产生了很多新模式,从 C2B 到 S2B2C,比如 S2B2C 的模式也是在发展过程中逐渐发现的新模式,因此企业对战略的制定要采取一种高效反馈闭环,**核心在于做战略实验**。
|
||||
|
||||
首先确定几个未来可能的战略方向,各投入一些人力尝试,尝试一年后自然会发现正确的方向,此时再将其他方向合并到正确方向。比如 2011 年阿里巴巴独立了三个子公司 - 淘宝、天猫、一淘,是为了赌未来的局势到底是 B2C,还是 C2C,还是一个搜索引擎指向无数小 B2C。最终发现由于中国网络基础设施还不成熟,导致独立 B2C 成本太高,因此 一淘 就回到了阿里巴巴。
|
||||
|
||||
因此当你发现公司在同时做几个相似的业务时,先不要急着觉得公司傻,这样做是在浪费资源,但你是否能看清楚这几个业务间微妙的差别?也许你不能猜到哪一个才是未来方向(能猜到你就当 CEO 吧),但至少能理解公司这样做的战略意图,而不是做什么都是淘宝。
|
||||
|
||||
对于企业战略选择,作者给出的建议是 **点-线-面-体**。也就是企业一定要在这其中找到自己的定位。
|
||||
|
||||
根据笔者读后的理解,点就是各种各样服务的角色,比如卖家、模特、独立开发者都属于点。线就是连接点与面沟通桥梁,比如微商或微博大 V 都属于线,原因是他们联接了平台与点。面就是指平台,比如淘宝属于面,因为它撬动了整个行业的资源,对上面无数个点赋能,联接了无数个点,面也是竞争最激烈的一环,也就是所谓的生态竞争,如果面对点的赋能力度不够,点也许就被其他的面吸引过去了。体是最大的概念,由多个 **相互协同的面** 组成,比如物流平台、网购平台、支付平台这三个面之间相互协作,才能逐渐形成体。
|
||||
|
||||
顺带一提,体不是一开始就形成,面也不是谁设计出来的,而是先有一个简单构想,根据市场需求逐步演化过来的,比如淘宝就是由 BBS 演化过来的,那 BBS 就是淘宝的基因,因此淘宝可以协同那么多点,可以快速反馈用户需求,可以演化出支付、物流、云业务并各自独立发展成新的面。
|
||||
|
||||
点-线-面-体 定位越上升,拥有的资源就越多,但面对的变化挑战就越多,其中“面”的竞争最为激烈,比如传统媒体本来是面,但在门户网站出现有,就降维到了点,微博的出现又使门户网站降为成线,而微信的出现使微博降为成线。
|
||||
|
||||
所以看似风光的 BAT 都选择了最为艰难的 “体” 的打造,而笔者认为,到了体这个级别,将撬动巨量的社会资源,带来巨大的回报,但排他性也是最强的。一个最完整的 “体” 本质上就是一个全面的协同网络 - 国家,国家与国家之间的排斥性大家可以想象,因此留给体的位置并不多,而新体的出现必然会与旧体展开生死决战。因此如果创业,将自己定位为“点”是比较靠谱的,因为有大量的“面”资源可用,只要能找到自己的亮点,就算有竞争,也不会收到太大的影响。
|
||||
|
||||
## 组织变革
|
||||
|
||||
战略变革后,就轮到组织变革了。组织变革的目的是最大程度激发员工的创造力,因此自上而下的结构是不适合了,需要一种新的组织形态与管理思路。
|
||||
|
||||
这种新的管理思路就是 “赋能” 的思路,一方面,赋能的思路可以提升员工的自主程度,充分发挥其创造力,一方面,赋能可以转变管理者的管理方式,使一个经理能管理十几、二十几个下属。互联网行业的工资都很高,尤其是顶尖人才,对于金钱的渴望已经不是找工作的最大决定因素,“成就感” “使命感” 更容易受这些顶尖人才的青睐,因此 “赋能” 的管理思路也是招募到顶尖人才的方法。
|
||||
|
||||
最后作者提到了 “自组织协同网”,这是一个非常超前的概念,也源于企业最大的痛点 - 如何衡量 KPI。
|
||||
|
||||
随着商业环境复杂性提高,几个核心指标远不能反应一个企业真实情况。有句话说,如果你只看一个指标,那最后达成的方式一定是你最不愿意看到的,比如淘宝为了冲刺销量 KPI,出了全年免网购费用的年卡,也许一天就能完成全年 KPI,但未来一年内可能会亏空整个公司老本。因此利用数据,从多个维度衡量指标是唯一的解法,换个说法,就是用复杂性对抗复杂性。
|
||||
|
||||
通过将公司所有业务数据化,训练出一个逐步优化的模型,是可能从所有维度逐渐趋向最真实反馈公司表现的多维度指标的,衡量员工工作绩效方式也同理。
|
||||
|
||||
读完这一段,笔者感受到数据最终也会被用在员工身上这句话,简单来说就是晋升答辩不用写 PPT 了,年底通过上千、上万种维度对你进行综合测评,直接出结果。现在已经能感受到公司在这个方向发力了,第一步是将所有开发过程数据化,也许离这一天已经不远。
|
||||
|
||||
## 案例分析
|
||||
|
||||
案例分析十分精彩,由于篇幅限制,笔者就不洋洋洒洒的转述了,如果感兴趣强烈推荐读[原文](https://detail.tmall.com/item.htm?spm=a220m.1000858.1000725.1.5518639bdKutaT&id=587836001802&standard=1&user_id=832978172&cat_id=2&is_b=1&rn=ab05936e7d8ab2699ab0bbc07bb2cb3f),笔者至少还会再读一遍。
|
||||
|
||||
从案例分析中,有两个核心观点笔者在此处提一下。
|
||||
|
||||
第一个是平台演化的自然性,作者以淘宝的发展历程作为案例,说明了淘宝并不是顶层设计的产物,而是根据市场反馈的产物,唯有如此才能在高速变化的时代搭建一个平台。
|
||||
|
||||
第二个是网红案例,网红不仅完成了点到线的演化,而且是综合利用了多个“面”的案例,通过综合利用社交平台(微博),电商平台(淘宝),快速反应供应链平台(由网红推动产生的新型供应链),结合这三个平台,网红这个线被赋予前所未有的能量,带来了巨大收益。
|
||||
|
||||
## 关于未来
|
||||
|
||||
读完本书的目的,不仅是了解当下的智能商业,更是为了思考未来。
|
||||
|
||||
在这个大变革时代,未来战略是难以预测的,所以凭空去勾勒未来蓝图没有什么意义,我们要在通过战略实验快速试探出未来几年的方向,在第二浪即将到达巅峰时,找到第三浪并积极布局。
|
||||
|
||||
其实本书只能给出寻找战略方向的方法论,而不能给出具体的未来发展方向是什么,因为这套方法论本身就是通过战略实验快速寻找方向的过程,唯有投入资源去做尝试,仔细观察身边发生的变化,才能逐渐找到未来的新商业模式。未来的商业模式也是在逐步演变的,受到的影响因素太多,因此大概处于一种 “不可观测” 的状态,但至少未来十年内 C2B 的模式,笔者认为是一个固定的大方向,而传统行业与互联网结合的产业互联网也是新的发展机遇,利用互联网优化传统行业的各个环节,是一个确定的方向标。
|
||||
|
||||
无论未来商业怎么发展,都会为消费者带来越来越好的体验,这是一个消费为王的时代,根据消费者的需求,掀起从平台到供应链的全方位改造,目的是带来更好的消费体验。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
读完了智能商业,笔者留下一个思考题:尝试站在智能商业的角度,分析你熟悉的公司各处于什么发展阶段,走的是什么商业模式?
|
||||
|
||||
> 讨论地址是:[精读《智能商业》 · Issue #169 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/169)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,340 @@
|
||||
# 1. 引言
|
||||
|
||||
Vue 3.0 的发布引起了轩然大波,让我们解读下它的 [function api RFC](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#comparison-with-react-hooks) 详细了解一下 Vue 团队是怎么想的吧!
|
||||
|
||||
首先官方回答了几个最受关注的问题:
|
||||
|
||||
**Vue 3.0 是否有 break change,就像 Python 3 / Angular 2 一样?**
|
||||
|
||||
不,100% 兼容 Vue 2.0,且暂未打算废弃任何 API(未来也不)。之前有草案试图这么做,但由于用户反馈太猛,被撤回了。
|
||||
|
||||
**Vue 3.0 的设计盖棺定论了吗?**
|
||||
|
||||
没有呀,这次精读的稿子就是 RFC(Request For Comments),翻译成中文就是 “意见征求稿”,还在征求大家意见中哦。
|
||||
|
||||
**这 RFC 咋这么复杂?**
|
||||
|
||||
RFC 是写给贡献者/维护者的,要考虑许多边界情况与细节,所以当然会复杂很多喽!当然 Vue 本身使用起来还是很简单的。
|
||||
|
||||
> Vue 本身 Mutable + Template 就注定了是个用起来简单(约定 + 自然),实现起来复杂(解析 + 双绑)的框架。
|
||||
|
||||
**这次改动很像在模仿 React,为啥不直接用 React?**
|
||||
|
||||
首先 Template 机制还是没变,其次模仿的是 Hooks 而不是 React 全部,如果你不喜欢这个改动,那你更不会喜欢用 React。
|
||||
|
||||
PS: 问这个问题的人,一定没有同时理解 React 与 Vue,其实这两个框架到现在差别蛮大的,后面精读会详细说明。
|
||||
|
||||
下面正式进入 Vue 3.0 Function API 的介绍。
|
||||
|
||||
# 2. 概述
|
||||
|
||||
Vue 函数式基本 Demo:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<span>count is {{ count }}</span>
|
||||
<span>plusOne is {{ plusOne }}</span>
|
||||
<button @click="increment">count++</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { value, computed, watch, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
// reactive state
|
||||
const count = value(0)
|
||||
// computed state
|
||||
const plusOne = computed(() => count.value + 1)
|
||||
// method
|
||||
const increment = () => { count.value++ }
|
||||
// watch
|
||||
watch(() => count.value * 2, val => {
|
||||
console.log(`count * 2 is ${val}`)
|
||||
})
|
||||
// lifecycle
|
||||
onMounted(() => {
|
||||
console.log(`mounted`)
|
||||
})
|
||||
// expose bindings on render context
|
||||
return {
|
||||
count,
|
||||
plusOne,
|
||||
increment
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
函数式风格的入口是 `setup` 函数,采用了函数式风格后可以享受如下好处:类型自动推导、减少打包体积。
|
||||
|
||||
`setup` 函数返回值就是注入到页面模版的变量。我们也可以返回一个函数,通过使用 `value` 这个 API 产生属性并修改:
|
||||
|
||||
```jsx
|
||||
import { value } from 'vue'
|
||||
|
||||
const MyComponent = {
|
||||
setup(props) {
|
||||
const msg = value('hello')
|
||||
const appendName = () => {
|
||||
msg.value = `hello ${props.name}`
|
||||
}
|
||||
return {
|
||||
msg,
|
||||
appendName
|
||||
}
|
||||
},
|
||||
template: `<div @click="appendName">{{ msg }}</div>`
|
||||
}
|
||||
```
|
||||
|
||||
要注意的是,`value()` 返回的是一个对象,通过 `.value` 才能访问到其真实值。
|
||||
|
||||
为何 `value()` 返回的是 Wrappers 而非具体值呢?原因是 Vue 采用双向绑定,只有对象形式访问值才能保证访问到的是最终值,这一点类似 React 的 `useRef()` API 的 `.current` 规则。
|
||||
|
||||
那既然所有 `value()` 返回的值都是 Wrapper,那直接给模版使用时要不要调用 `.value` 呢?**答案是否定的,直接使用即可,模版会自动 `Unwrapping`:**
|
||||
|
||||
```jsx
|
||||
const MyComponent = {
|
||||
setup() {
|
||||
return {
|
||||
count: value(0)
|
||||
}
|
||||
},
|
||||
template: `<button @click="count++">{{ count }}</button>`
|
||||
}
|
||||
```
|
||||
|
||||
接下来是 **Hooks**,下面是一个使用 Hooks 实现获得鼠标实时位置的例子:
|
||||
|
||||
```jsx
|
||||
function useMouse() {
|
||||
const x = value(0)
|
||||
const y = value(0)
|
||||
const update = e => {
|
||||
x.value = e.pageX
|
||||
y.value = e.pageY
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', update)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', update)
|
||||
})
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
// in consuming component
|
||||
const Component = {
|
||||
setup() {
|
||||
const { x, y } = useMouse()
|
||||
const { z } = useOtherLogic()
|
||||
return { x, y, z }
|
||||
},
|
||||
template: `<div>{{ x }} {{ y }} {{ z }}</div>`
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,`useMouse` 将所有与 “处理鼠标位置” 相关的逻辑都封装了进去,乍一看与 React Hooks 很像,但是有两个区别:
|
||||
|
||||
1. `useMouse` 函数内改变 `x`、`y` 后,不会重新触发 `setup` 执行。
|
||||
2. `x` `y` 拿到的都是 Wrapper 而不是原始值,且这个值会动态变化。
|
||||
|
||||
另一个重要 API 就是 **`watch`**,它的作用类似 React Hooks 的 **useEffect**,但实现原理和调用时机其实完全不一样。
|
||||
|
||||
`watch` 的目的是监听某些变量变化后执行逻辑,比如当 `id` 变化后重新取数:
|
||||
|
||||
```jsx
|
||||
const MyComponent = {
|
||||
props: {
|
||||
id: Number
|
||||
},
|
||||
setup(props) {
|
||||
const data = value(null)
|
||||
watch(() => props.id, async (id) => {
|
||||
data.value = await fetchData(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
之所以要 `watch`,因为在 Vue 中,`setup` 函数仅执行一次,所以不像 React Function Component,每次组件 `props` 变化都会重新执行,因此无论是在变量、`props` 变化时如果想做一些事情,都需要包裹在 `watch` 中。
|
||||
|
||||
后面还有 `unwatching`、生命周期函数、依赖注入,都是一些语法定义,感兴趣可以继续[阅读原文](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#dependency-injection),笔者就不赘述了。
|
||||
|
||||
# 3. 精读
|
||||
|
||||
对于 Vue 3.0 的 Function API + Hooks 与 React Function Component + Hooks,笔者做一些对比。
|
||||
|
||||
## Vue 与 React 逻辑结构
|
||||
|
||||
React Function Component 与 Hooks,虽然在实现原理上,与 Vue3.0 存在 Immutable 与 Mutable、JSX 与 Template 的区别,但逻辑理解上有着相通之处。
|
||||
|
||||
```ts
|
||||
const MyComponent = {
|
||||
setup(props) {
|
||||
const x = value(0)
|
||||
|
||||
const setXRandom = () => {
|
||||
x.value = Math.random()
|
||||
}
|
||||
|
||||
return { x, setXRandom }
|
||||
},
|
||||
template: `
|
||||
<button @onClick="setXRandom"/>{{x}}</button>
|
||||
`
|
||||
}
|
||||
```
|
||||
|
||||
虽然在 Vue 中,`setup` 函数仅执行一次,看上去与 React 函数完全不一样(React 函数每次都执行),但其实 Vue 将渲染层(Template)与数据层(setup)分开了,而 React 合在了一起。
|
||||
|
||||
我们可以利用 React Hooks 将数据层与渲染层完全隔离:
|
||||
|
||||
```jsx
|
||||
// 类似 vue 的 setup 函数
|
||||
function useMyComponentSetup(props) {
|
||||
const [x, setX] = useState(0)
|
||||
|
||||
const setXRandom = useCallback(() => {
|
||||
setX(Math.random())
|
||||
}, [setX])
|
||||
|
||||
return { x, setXRandom }
|
||||
}
|
||||
|
||||
// 类似 vue 的 template 函数
|
||||
function MyComponent(props: { name: String }) {
|
||||
const { x, setXRandom } = useMyComponentSetup(props)
|
||||
|
||||
return (
|
||||
<button onClick={setXRandom}>{x}</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
这源于 JSX 与 Template 的根本区别。JSX 使模版与 JS 可以写在一起,因此数据层与渲染层可以耦合在一起写(也可以拆分),但 Vue 采取的 Template 思路使数据层强制分离了,这也使代码分层更清晰了。
|
||||
|
||||
而实际上 Vue3.0 的 `setup` 函数也是可选的,再配合其支持的 TSX 功能,与 React 真的只有 Mutable 的区别了:
|
||||
|
||||
```jsx
|
||||
// 这是个 Vue 组件
|
||||
const MyComponent = createComponent((props: { msg: string }) => {
|
||||
return () => h('div', props.msg)
|
||||
})
|
||||
```
|
||||
|
||||
我们很难评价 Template 与 JSX 的好坏,但为了更透彻的理解 Vue 与 React,需要抛开 JSX&Template,Mutable&Immutable 去看,其实去掉这两个框架无关的技术选型,React@16 与 Vue@3 已经非常像了。
|
||||
|
||||
> Vue3.0 的精髓是学习了 React Hooks 概念,因此正好可以用 Hooks 在 React 中模拟 Vue 的 setup 函数。
|
||||
|
||||
关于这两套技术选型,已经是相对完美的组合,不建议在 JSX 中再实现类似 Mutable + JSX 的花样来(因为喜欢 Mutable 可以用 Vue 呀):
|
||||
|
||||
- Vue:Mutable + Template
|
||||
- React:Immutable + JSX
|
||||
|
||||
真正影响编码习惯的就是 Mutable 与 Immutable,使用 Vue 就坚定使用 Mutable,使用 React 就坚定使用 Immutable,这样能最大程度发挥两套框架的价值。
|
||||
|
||||
## Vue Hooks 与 React Hooks 的差异
|
||||
|
||||
先看 React Hooks 的简单语法:
|
||||
|
||||
```jsx
|
||||
const [ count, setCount ] = useState(0)
|
||||
|
||||
const setToOne = () => setCount(1)
|
||||
```
|
||||
|
||||
Vue Hooks 的简单语法:
|
||||
|
||||
```jsx
|
||||
const count = value(0)
|
||||
|
||||
const setToOne = () => count.value = 1
|
||||
```
|
||||
|
||||
之所以 React 返回的 `count` 是一个数字,是因为 Immutable 规则,而 Vue 返回的 `count` 是个对象,拥有 `count.value` 属性,也是因为 Vue Mutable 规则导致,这使得 Vue 定义的所有变量都类似 React 中 `useRef` 定义变量,因此不存 React `capture value` 的特性。
|
||||
|
||||
> 关于 capture value 更多信息,可以阅读 [精读《Function VS Class 组件》 Capute Value 介绍](https://github.com/dt-fe/weekly/blob/v2/095.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20VS%20Class%20%E7%BB%84%E4%BB%B6%E3%80%8B.md#capture-props)
|
||||
|
||||
另外,对于 Hooks 的值变更机制也不同,我们看 Vue 的代码:
|
||||
|
||||
```jsx
|
||||
const Component = {
|
||||
setup() {
|
||||
const { x, y } = useMouse()
|
||||
const { z } = useOtherLogic()
|
||||
return { x, y, z }
|
||||
},
|
||||
template: `<div>{{ x }} {{ y }} {{ z }}</div>`
|
||||
}
|
||||
```
|
||||
|
||||
由于 `setup` 函数仅执行一次,怎么做到当 `useMouse` 导致 `x`、`y` 值变化时,可以在 `setup` 中拿到最新的值?
|
||||
|
||||
在 React 中,`useMouse` 如果修改了 `x` 的值,那么使用 `useMouse` 的函数就会被重新执行,以此拿到最新的 `x`,而在 Vue 中,将 Hooks 与 Mutable 深度结合,通过包装 `x.value`,使得当 `x` 变更时,引用保持不变,仅值发生了变化。所以 Vue 利用 Proxy 监听机制,可以做到 `setup` 函数不重新执行,但 Template 重新渲染的效果。
|
||||
|
||||
这就是 Mutable 的好处,Vue Hooks 中,不需要 `useMemo` `useCallback` `useRef` 等机制,仅需一个 `value` 函数,直观的 Mutable 修改,就可以实现 React 中一套 Immutable 性能优化后的效果,这个是 Mutable 的魅力所在。
|
||||
|
||||
## Vue Hooks 的优势
|
||||
|
||||
笔者对 RFC 中对 Vue、React Hooks 的对比做一个延展解释:
|
||||
|
||||
首先最大的不同:`setup` 仅执行一遍,而 React Function Component 每次渲染都会执行。
|
||||
|
||||
**Vue 的代码使用更符合 JS 直觉。**
|
||||
|
||||
这句话直截了当戳中了 JS 软肋,JS 并非是针对 Immutable 设计的语言,所以 Mutable 写法非常自然,而 Immutable 的写法就比较别扭。
|
||||
|
||||
当 Hooks 要更新值时,Vue 只要用等于号赋值即可,而 React Hooks 需要调用赋值函数,**当对象类型复杂时,还需借助第三方库才能保证进行了正确的 Immutable 更新。**
|
||||
|
||||
**对 Hooks 使用顺序无要求,而且可以放在条件语句里。**
|
||||
|
||||
对 React Hooks 而言,调用必须放在最前面,而且不能被包含在条件语句里,这是因为 React Hooks 采用下标方式寻找状态,一旦位置不对或者 Hooks 放在了条件中,就无法正确找到对应位置的值。
|
||||
|
||||
而 Vue Function API 中的 Hooks 可以放在任意位置、任意命名、被条件语句任意包裹的,因为其并不会触发 `setup` 的更新,只在需要的时候更新自己的引用值即可,而 Template 的重渲染则完全继承 Vue 2.0 的依赖收集机制,它不管值来自哪里,只要用到的值变了,就可以重新渲染了。
|
||||
|
||||
**不会再每次渲染重复调用,减少 GC 压力。**
|
||||
|
||||
这确实是 React Hooks 的一个问题,所有 Hooks 都在渲染闭包中执行,每次重渲染都有一定性能压力,而且频繁的渲染会带来许多闭包,虽然可以依赖 GC 机制回收,但会给 GC 带来不小的压力。
|
||||
|
||||
而 Vue Hooks 只有一个引用,所以存储的内容就非常精简,也就是占用内存小,而且当值变化时,也不会重新触发 `setup` 的执行,所以确实不会造成 GC 压力。
|
||||
|
||||
**必须要总包裹 `useCallback` 函数确保不让子元素频繁重渲染。**
|
||||
|
||||
React Hooks 有一个问题,就是完全依赖 Immutable 属性。**而在 Function Component 内部创建函数时,每次都会创建一个全新的对象,这个对象如果传给子组件,必然导致子组件无法做性能优化。** 因此 React 采取了 `useCallback` 作为优化方案:
|
||||
|
||||
```jsx
|
||||
const fn = useCallback(() => /* .. */, [])
|
||||
```
|
||||
|
||||
只有当第二个依赖参数变化时才返回新引用。但第二个依赖参数需要 lint 工具确保依赖总是正确的(关于为何要对依赖诚实,感兴趣可以移步 [精读《Function Component 入门》 - 永远对依赖诚实](https://github.com/dt-fe/weekly/blob/v2/104.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20Component%20%E5%85%A5%E9%97%A8%E3%80%8B.md#%E6%B0%B8%E8%BF%9C%E5%AF%B9%E4%BE%9D%E8%B5%96%E9%A1%B9%E8%AF%9A%E5%AE%9E))。
|
||||
|
||||
回到 Vue 3.0,由于 `setup` 仅执行一次,因此函数本身只会创建一次,不存在多实例问题,不需要 `useCallback` 的概念,更不需要使用 [lint 插件](https://www.npmjs.com/package/eslint-plugin-react-hooks) 保证依赖书写正确,这对开发者是实实在在的友好。
|
||||
|
||||
**不需要使用 `useEffect` `useMemo` 等进行性能优化,所有性能优化都是自动的。**
|
||||
|
||||
这也是实在话,毕竟 Mutable + 依赖自动收集就可以做到最小粒度的精确更新,根本不会触发不必要的 Rerender,因此 `useMemo` 这个概念也不需要了。
|
||||
|
||||
而 `useEffect` 也需要传递第二个参数 “依赖项”,在 Vue 中根本不需要传递 “依赖项”,所以也不会存在用户不小心传错的问题,更不需要像 React 写一个 lint 插件保证依赖的正确性。(这也是笔者想对 React Hooks 吐槽的点,React 团队如何保障每个人都安装了 lint?就算装了 lint,如果 IDE 有 BUG,导致没有生效,随时可能写出依赖不正确的 “危险代码”,造成比如死循环等严重后果)
|
||||
|
||||
# 4. 总结
|
||||
|
||||
通过对比 Vue Hooks 与 React Hooks 可以发现,Vue 3.0 将 Mutable 特性完美与 Hooks 结合,规避了一些 React Hooks 的硬伤。所以我们可以说 Vue 借鉴了 React Hooks 的思想,但创造出来的确实一个更精美的艺术品。
|
||||
|
||||
但 React Hooks 遵循的 Immutable 也有好的一面,就是每次渲染中状态被稳定的固化下来了,不用担心状态突然变更带来的影响(其实反而要注意状态用不变更带来的影响),对于数据记录、程序运行的稳定性都有较高的可预期性。
|
||||
|
||||
最后,对于喜欢 Mutable 的开发者,Vue 3.0 是你的最佳选择,基于 React + Mutable 搞的一些小轮子做到顶级可能还不如 Vue 3.0。对于 React 开发者来说,坚持你们的 Immutable 信仰吧,Vue 3.0 已经将 Mutable 发挥到极致,只有将 React Immutable 特性发挥到极致才能发挥 React 的最大价值。
|
||||
|
||||
> 讨论地址是:[精读《Vue3.0 Function API》 · Issue #173 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/173)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,196 @@
|
||||
# 1. 引言
|
||||
|
||||
本周精读的源码是 [inject-instance](https://github.com/ascoders/inject-instance) 这个库。
|
||||
|
||||
这个库的目的是为了实现 Class 的依赖注入。
|
||||
|
||||
比如我们通过 `inject` 描述一个成员变量,那么在运行时,这个成员变量的值就会被替换成对应 Class 的实例。这等于让 Class 具备了申明依赖注入的能力:
|
||||
|
||||
```js
|
||||
import {inject} from 'inject-instance'
|
||||
import B from './B'
|
||||
|
||||
class A {
|
||||
@inject('B') private b: B
|
||||
public name = 'aaa'
|
||||
|
||||
say() {
|
||||
console.log('A inject B instance', this.b.name)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
试想一下,如果成员函数 `b` 是通过 New 出来的:
|
||||
|
||||
```js
|
||||
class A {
|
||||
private b = new B()
|
||||
|
||||
say() {
|
||||
console.log('A inject B instance', this.b.name)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个 `b` 就不具备依赖注入的特点,因为被注入的 `b` 是外部已经初始化好的,而不是实例化 A 时动态生成的。
|
||||
|
||||
需要依赖注入的一般都是框架级代码,比如定义数据流,存在三个 Store 类,他们之间需要相互调用对方实例:
|
||||
|
||||
```js
|
||||
class A {
|
||||
@inject('B') private b: B
|
||||
}
|
||||
|
||||
class B {
|
||||
@inject('C') private c: C
|
||||
}
|
||||
|
||||
class C {
|
||||
@inject('A') private a: A
|
||||
}
|
||||
```
|
||||
|
||||
那么对于引用了数据流 A、B、C 的三个组件,**要保证它们访问到的是同一组实例 `A` `B` `C` 该怎么办呢?**
|
||||
|
||||
这时候我们需要通过 `injectInstance` 函数统一实例化这些类,保证拿到的实例中,成员变量都是属于同一份实例:
|
||||
|
||||
```js
|
||||
import injectInstance from 'inject-instance'
|
||||
|
||||
const instances = injectInstance(A, B, C)
|
||||
instances.get('A')
|
||||
instances.get('B')
|
||||
instances.get('C')
|
||||
```
|
||||
|
||||
那么框架底层可以通过调用 `injectInstance` 方式初始化一组 “正确注入依赖关系的实例”,拿 React 举例,这个动作可以发生在自定义数据流的 `Provider` 函数里:
|
||||
|
||||
```js
|
||||
<Provider stores={{ A, B, C }}>
|
||||
<Root />
|
||||
</Provider>
|
||||
```
|
||||
|
||||
那么在 `Provider` 函数内部通过 `injectInstance` 实例化的数据流,**可以保证 `A` `B` `C` 操作的注入实例都是当前 `Provider` 实例中的那一份**。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
那么开始源码的解析,首先是整体思路的分析。
|
||||
|
||||
我们需要准备两个 API: `inject` 与 `injectInstance`。
|
||||
|
||||
`inject` 用来描述要注入的类名,值是与 Class 名相同的字符串,`injectInstance` 是生成一系列实例的入口函数,需要生成最终生效的实例,并放在一个 Map 中。
|
||||
|
||||
## inject
|
||||
|
||||
`inject` 是个装饰器,它的目的有两个:
|
||||
|
||||
1. 修改 Class 基类信息,使其实例化的实例能拿到对应字段注入的 Class 名称。
|
||||
2. 增加一个字段描述注入了那些 Key。
|
||||
|
||||
```ts
|
||||
const inject = (injectName: string): any => (target: any, propertyKey: string, descriptor: PropertyDescriptor): any => {
|
||||
target[propertyKey] = injectName
|
||||
|
||||
// 加入一个标注变量
|
||||
if (!target['_injectDecorator__injectVariables']) {
|
||||
target['_injectDecorator__injectVariables'] = [propertyKey]
|
||||
} else {
|
||||
target['_injectDecorator__injectVariables'].push(propertyKey)
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
```
|
||||
|
||||
`target[propertyKey] = injectName` 这行代码中,`propertyKey` 是申明了注入的成员变量名称,比如 Class `A` 中,`propertyKey` 等于 `b`,而 `injectName` 表示这个值需要的对应实例的 Class 名,比如 Class `A` 中,`injectName` 等于 `B`。
|
||||
|
||||
而 `_injectDecorator__injectVariables` 是个数组,为 Class 描述了这个类参与注入的 key 共有哪些,这样可以在后面 `injectInstance` 函数中拿到并依次赋值。
|
||||
|
||||
## injectInstance
|
||||
|
||||
这个函数有两个目的:
|
||||
|
||||
1. 生成对应的实例。
|
||||
2. 将实例中注入部分的成员变量替换成对应实例。
|
||||
|
||||
代码不长,直接贴出来:
|
||||
|
||||
```ts
|
||||
const injectInstance = (...classes: Array<any>) => {
|
||||
const classMap = new Map<string, any>()
|
||||
const instanceMap = new Map<string, any>()
|
||||
|
||||
classes.forEach(eachClass => {
|
||||
if (classMap.has(eachClass.name)) {
|
||||
throw `duplicate className: ${eachClass.name}`
|
||||
}
|
||||
classMap.set(eachClass.name, eachClass)
|
||||
})
|
||||
|
||||
// 遍历所有用到的类
|
||||
classMap.forEach((eachClass: any) => {
|
||||
// 实例化
|
||||
instanceMap.set(eachClass.name, new eachClass())
|
||||
})
|
||||
|
||||
// 遍历所有实例
|
||||
instanceMap.forEach((eachInstance: any, key: string) => {
|
||||
// 遍历这个类的注入实例类名
|
||||
if (eachInstance['_injectDecorator__injectVariables']) {
|
||||
eachInstance['_injectDecorator__injectVariables'].forEach((injectVariableKey: string) => {
|
||||
const className = eachInstance.__proto__[injectVariableKey];
|
||||
if (!instanceMap.get(className)) {
|
||||
throw Error(`injectName: ${className} not found!`);
|
||||
}
|
||||
|
||||
// 把注入名改成实际注入对象
|
||||
eachInstance[injectVariableKey] = instanceMap.get(className);
|
||||
});
|
||||
}
|
||||
|
||||
// 删除这个临时变量
|
||||
delete eachInstance['_injectDecorator__injectVariables'];
|
||||
});
|
||||
|
||||
return instanceMap
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,首先我们将传入的 Class 依次初始化:
|
||||
|
||||
```ts
|
||||
// 遍历所有用到的类
|
||||
classMap.forEach((eachClass: any) => {
|
||||
// 实例化
|
||||
instanceMap.set(eachClass.name, new eachClass())
|
||||
})
|
||||
```
|
||||
|
||||
这是必须提前完成的,因为注入可能存在循环依赖,我们必须在解析注入之前就生成 Class 实例,此时需要注入的字段都是 `undefined`。
|
||||
|
||||
第二步就是将这些注入字段的 `undefined` 替换为刚才实例化 Map `instanceMap` 中对应的实例了。
|
||||
|
||||
我们通过 `__proto__` 拿到 Class 基类在 `inject` 函数中埋下的 `injectName`,配合 `_injectDecorator__injectVariables` 拿到 key 后,直接遍历所有要替换的 key, 通过类名从 `instanceMap` 中提取即可。
|
||||
|
||||
> `__proto__` 仅限框架代码中使用,业务代码不要这么用,造成额外理解成本。
|
||||
|
||||
所以总结一下,就是提前实例化 + 根据 `inject` 埋好的信息依次替换注入的成员变量为刚才实例化好的实例。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
希望读完这篇文章,你能理解依赖注入的使用场景,使用方式,以及一种实现思路。
|
||||
|
||||
框架实现依赖注入都是提前收集所有类,统一初始化,通过注入函数打标后全局替换,这是一种思维套路。
|
||||
|
||||
如果有其他更有意思的依赖注入实现方案,欢迎讨论。
|
||||
|
||||
> 讨论地址是:[精读《Inject Instance 源码》 · Issue #176 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/176)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,128 @@
|
||||
# 1. 引言
|
||||
|
||||
前端展望的文章越来越不好写了,随着前端发展的深入,需要拥有非常宽广的视野与格局才能看清前端的未来。
|
||||
|
||||
笔者根据自身经验,结合下面几篇文章发表一些总结与感悟:
|
||||
|
||||
- [A Look at JavaScript’s Future](https://www.toptal.com/javascript/predicting-javascript-future)
|
||||
- [前端开发 20 年变迁史](https://mp.weixin.qq.com/s/yNg7Q0XNLJMnqffTIJhNUg)
|
||||
- [前端开发编程语言的过去、现在和未来](https://johnhax.net/2019/fe-lang/article1)
|
||||
- [绕过技术纷争,哪些技术决定前端开发者的未来?](https://mp.weixin.qq.com/s?__biz=MzUxMzcxMzE5Ng==&mid=2247491704&idx=1&sn=95ad66f7fe606801cdac74e296a41783)
|
||||
- [未来前端的机会在哪里?](https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&mid=2247490769&idx=1&sn=7ee6e01045a6fe7e15f16aa33afcc2ad&chksm=e92921dede5ea8c8e93489271e8877d2e8688bd511b32e22c287b6c468904c5466b40f6a2bec&xtrack=1&scene=90&subscene=93&sessionid=1562200039&clicktime=1562)
|
||||
|
||||
读完这几篇文章可以发现,即便是最资深的前端从业者,每个人看前端未来也有不同的侧重点。这倒不是因为视野的局限,而是现在前端领域太多了,专精其中某几个领域就足够了,适量比全面更好。
|
||||
|
||||
同时前端底层也在逐渐封闭,虽然目睹了前端几十年变迁的开发者仍会对一些底层知识津津乐道,但通往底层的大门已经一扇扇逐渐关闭了,将更多的开发者挤到上层区域建设,所以仅学会近几年的前端知识依然能找到不错的工作。
|
||||
|
||||
然而上层建设是不封顶的,有人看到了山,有人看到了星球,不同业务环境,不同视野的人看到的东西都不同。
|
||||
|
||||
有意思是的国内和国外看到前端未来的视角也不同:国内看到的是追求更多的参与感、影响力,国外看到的是对新特性的持续跟进。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
前端可以从多个角度理解,比如规范、框架、语言、社区、场景以及整条研发链路。
|
||||
|
||||
看待前端未来的角度随着视野不同也会有变化,比如 Serverless 是未来,务实的思考是:前端在 Serverless 研发链路中仅处于使用方,并不会因为用了 Serverless 而提升了技术含量。更高格局的思考是:怎么推动 Serverless 的建设,不把自己局限在前端。
|
||||
|
||||
所以当我们读到不同的人对前端理解的时候,有人站在一线前端研发的角度,有人站在全栈的角度,也有人站在业务负责人的角度。其实国内前端发展也到了这个阶段,老一辈的前端开拓者们已经进入不同的业务领域,承担着更多不同的职能分工,甚至是整个大业务线的领导者,这说明两点:
|
||||
|
||||
1. 前辈已经用行动指出了前端突破天花板的各种方向。
|
||||
2. 同是前端未来展望,不同的文章侧重的格局不同,两个标题相同的文章内容可能大相径庭。
|
||||
|
||||
笔者顺着这些文章分析角度,发表一些自己的看法。
|
||||
|
||||
## 框架
|
||||
|
||||
在前端早期,也就是 1990 年浏览器诞生的时候,JS 没有良好的设计,浏览器也没有全面的实现,框架还没出来,浏览器之间就打起来了。
|
||||
|
||||
这也给前端发展定了一个基调:凭实力说话。
|
||||
|
||||
后面诞生的 Prototype、jquery 都是为了解决时代问题而诞生的,所以有种时代造就前端框架的感觉。
|
||||
|
||||
但到了最近几年,React、Angular、Vue 大有前端框架引领新时代的势头,前端要做的不再是填坑,而是模式创新。国内出现的小程序浪潮是个意料之外的现象,虽然群雄割据为开发者适配带来了一定成本,但本质上是中国在前端底层领域争取话语权的行为,而之所以各大公司不约而同的推出自己的小程序,则是商业、经济发展到了这个阶段的自然产物。
|
||||
|
||||
在原生开发领域,像 RN、Flutter 也是比较靠谱的移动端开发框架,RN 就长在 React 上,而 Flutter 的声明式 UI 也借鉴了前端框架的思路。每个框架都想往其他框架的领域渗透,所以标准总是很相近,各自的特色并没有宣传的那么明显,这个阶段只选用一种框架是明智的选择,未来这些框架之间会有更多使用场景争夺,但更多的是融合,推动新的开发方式提高生产力。
|
||||
|
||||
在数据驱动 UI 的方式上,具有代表性的是 React 的 Immutable 模式与 Vue 的 MVVM 观察者模式,前者模式虽然新颖,但是符合 JS 语言自然运行机制,Vue 的 MVVM 模式也相当好,特别是 Vue3.0 的 API 巧妙的解决了 React Hooks 无法解决的难题。如果 Vue 继续保持蓬勃的发展势头,未来前端 MVVM 模式甚至可能标准化,那么 Vue 是作为标准化的事实规范,还是和 JQuery 一样的命运,还需观察。
|
||||
|
||||
## 语言
|
||||
|
||||
JS 语言本身有满多缺陷的,但通过 babel 前端工程师可以提前享受到大部分新特性,这在很大程度上抵消了早期语言设计带来的问题。
|
||||
|
||||
横向对比来看,我们还可以把编程语言分为:前端语言、后端语言、能编译到 JS 的语言。
|
||||
|
||||
之所以有 “能编译到 JS 的语言” 这一类,是因为 JS Runtime 几乎是前端跨平台的通用标准,能编译到 JS 就代表了可跨平台,然而现在 “能编译到 JS 的语言” 除了紧贴 JS 做类型增强的 TS 外,其他并没有火起来,有工具链生态不匹配的原因,也有各大公司之间利益争夺的原因。
|
||||
|
||||
后端语言越来越贴场景化,比如 Go 主打轻量级高并发方案,Python 以其易用性占领了大部分大数据、人工智能的运算场景。
|
||||
|
||||
与此对应的是前端语言的同质化,前端语言绑定在前端框架的趋势越来越明显,比如 IOS 平台只能用 OC 和 Swift,安卓只能用 JAVA 和 Kotlin,Flutter 只支持 Dart,与其说这些语言更适合这些平台特性,不如说背后是谷歌、苹果、微软等巨头对平台生态掌控权的争夺。Web 与移动端要解决的问题是类似的:如何高效管理 UI 状态,现在大部分都采用数据驱动的思路,通过 JSX 或 Template 的方式描述出 UI DSL(更多可参考 [前端开发编程语言的过去、现在和未来](https://johnhax.net/2019/fe-lang/article1) UI DSL 一节)、以及性能提升:渲染和计算分离(这里又分为并发与调度两种实现思路,目的和效果是类似的)。
|
||||
|
||||
所以编程语言的未来也没什么悬念,前端领域如果有的选就用 JS,没得选只能依附所在平台绑定的语言,而前端语言最近正在完成一轮升级大迁徙:JS -> TS,JAVA -> Kotlin,OC -> Swift,前端语言的特性、易用性正在逐步趋同。需要说明的是,如果仅了解这些语言的语法,对编程能力是毫无帮助的,了解平台特性,解决业务问题,提供更好的交互体验才是前端应该不断追求的目标,随着前端、Native 开发者之间的流动,前端领域语言层面差异会会来越小,大家越关注上层,越倾向抹平语言差异,甚至可能 All in JS,这不是因为 JS 有多大野心,而是因为在解决的问题趋同、业务优先的大背景下,大家都需要减少语言不通带来的障碍,最好的办法就是统一语言,从人类语言的演变就可以发现,要解决的问题趋同(人类交流)、与国家绑定的小众语言一直都有生存空间、语法大同小异,但不同语言都有一定自己的特色(比如法语表意更精确)、跨语言学习成本高,所以当国际化协作频繁时,一定会催生一套官方语言(英语),而使用基数大的语言可能会发展为通用国际语言(中文)。
|
||||
|
||||
将编程语言的割裂、统一比作人类语言来看,就能理解现状,和未来发展趋势了。
|
||||
|
||||
## 可视化
|
||||
|
||||
前面也说过,前端的底层在逐渐封闭,而可视化就是前端的上层。
|
||||
|
||||
所以笔者很少提到工程化,原因就是未来前端开发者接触工程化的机会越来越少,工程化机制也越来越完善,前端会逐渐回归到自己的本质 - 人机交互,而交互的重要媒介就是图形,无论组件库还是智能化设计稿 To Code 都为了解放简单、模式化的交互工作,专业前端将更多聚集到图形化领域。
|
||||
|
||||
图形和数据是分不开的,所以图形化还要考虑性能问题与数据转换。
|
||||
|
||||
可视化是对性能要求最高的,因此像 web worker、GPU 加速都是常见处理手段,WASM 技术也会用到可视化中。具体到某个图表或大屏的性能优化,还会涉及数据抽样算法,分层渲染等,仅仅性能优化领域就有不少探索的空间。性能问题一般还伴随着数据量大,所以数据序列化方案也要一并考虑。
|
||||
|
||||
可视化图形学是非常学术的领域,从图形语法到交互语法,从一图一做的简单场景,到可视化分析场景的灵活拓展能力,再到探索式分析的图形语法完备性要求,可视化库想要一层层支持不同业务场景的需求,要有一个清晰的分层设计。
|
||||
|
||||
仅可视化的图形学领域,就足够将所有时间投入了,未来做可视化的前端会越来越专业,提供的工具库接口也越来越有一套最佳实践沉淀,对普通前端越来越友好。
|
||||
|
||||
BI 可视化分析就是前端深造的一个方向,跟随 BI 发展阶段,对前端的要求也在不断变化:工程化、组件化、搭建技术、渲染引擎、可视化、探索式、智能化,跟上产品对技术能力的要求,其实是相当有挑战性的。
|
||||
|
||||
## 编辑器
|
||||
|
||||
编辑器方向主要有 IDE(Web IDE)、富文本编辑器。
|
||||
|
||||
**IDE 方向** 国产做的比较好的是 HBuilder,国际上做的比较好的是 VSCode,由于微软还同时推出了 Web 版 MonacoEditor,让 Web IDE 开发的门槛大大降低。
|
||||
|
||||
作为使用者,现在和未来的主流可能都是微软系,毕竟微软在操作系统、IDE 方面人才储备和经验积累很多。但随着云服务的变迁,引导着开发方式升级,IDE 游戏规则可能迎来重大改变 - 云化。云化使得作为开发者拥有更多竞争的机会,因为云上 IDE 市场现在还是蓝海,现在很多创业公司和大公司内部都在走这个方向,这标志着中国计算机技术往更底层的技术发展,未来会有更多的话语权。
|
||||
|
||||
从发展阶段来说,前端也发展到了 Web IDE 这个时代。对大公司来说,内部有许许多多割裂的工程化孤岛,不仅消耗大量优秀的前端同学去维护,也造成内部物料体系、工程体系难以打通,阻碍了内部技术流通,而云 IDE 天生的中心化环境管理可以解决这个问题,同时还能带来抹平计算机环境差异、统一编译环境、源码不落盘、甚至实现自动的多人协作也成为了可能,而云 IDE 因为在云上,也不止于 IDE,还可以很方便的集成流程,将研发全链路打通,因此在阿里内部也成为了今年四大方向之一。
|
||||
|
||||
所以今年可以明显看到的是,前端又在逐步替代低水平重复的 UI 设计,从设计稿生成代码,到研发链路上云,这种顶层设计正在进一步收窄前端底层建设,所以未来会有更多专业前端涌入可视化领域。
|
||||
|
||||
**富文本编辑器方向** 是一个重要且小众的领域,老牌做的较好的是 UEditor 系列,现在论体验和周边功能完善度,做得最好的是语雀编辑器。开源也有很多优秀的实现,比如 Quill、DraftJS、Slate 等等,但现在富文本编辑器核心能力是功能完备性(是否支持视频、脑图、嵌入)、性能、服务化功能打通了多少(是否支持在线解析 pdf、ppt 等文件)、交互自然程度(拷贝内容的智能识别)等等。如果将眼光放到全球,那国外有大量优秀富文本编辑器案例,比如 Google Docs、Word Online、iCloud Pages 等等。
|
||||
|
||||
最好用的富文本编辑器往往不开源,因为投入的技术研发成本是巨大的,本身这项技术就是一个产品,卖点就是源码。
|
||||
|
||||
富文本编辑器功能强度可以分为三个级别:L0~L2:
|
||||
|
||||
- L0:利用浏览器自带的输入框,主要指 `contenteditable` 实现。
|
||||
- L1:在 L0 的基础上通过 DOM API 自主实现增删改的功能,自定义能力非常强。
|
||||
- L2:从输入框、光标开始自主研发,完全不依赖浏览器特性,如果研发团队能力强,可以实现任何功能,典型产品比如 Google Docs。
|
||||
|
||||
无论国内外都鲜有进入 L2 强度的产品,除了超级大公司或者主打编辑器的创业公司。
|
||||
|
||||
所以编辑器方向中,无论 IDE 方向,还是富文本编辑器方向,都值得深入探索,其中 IDE 方向更偏工程化一些,考验体系化思维,编辑器方向更偏经验与技术,考验基本功和架构设计能力。
|
||||
|
||||
## 智能化
|
||||
|
||||
笔者认为智能化离前端这个工种是比较远的,智能化最终服务前后端,给前后端开发效率带来一个质的提升,而在此之前,作为前端从业者无非有两种选择:加入智能化开拓者队伍,或者准备好放弃可能被智能化替代的工作内容,积极投身于智能化解放开发者双手后,更具有挑战性的工作。这种挑战性的工作恰好包括了上面分析过的四个点:语言、框架、可视化、编辑器。
|
||||
|
||||
类比商业智能化,商业智能化包括网络协同和数据智能,也就是大量的网络协同产生海量数据,通过数据智能算法促进更好的算法模型、更高效的网络协同,形成一个反馈闭环。前端智能化也是类似,不管是自动切图、生成图片、页面,或者自动生成代码,都需要算法和前端工程师之间形成协同关系,并完成一个高效的反馈闭环,算法将是前端工程师手中的开发利器,且越规模化的使用功效越大。
|
||||
|
||||
另一种智能化方向是探索 BI 与可视化结合的智能化,通过功能完备的底层图表库,与后端通用 Cube 计算模型,形成一种探索式分析型 BI 产品,Tableau 就是典型的案例,在这个智能化场景中,需要对数据、产品、可视化全面理解的综合性人才,是前端职业生涯另一个突破点。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
本文列举的五点显然不能代表前端的全貌,还遗漏了太多方面,比如工程化、组件化、Serverless 等,但 **语言、框架、可视化、编辑器、智能化** 这五个点是笔者认为前端,特别是国内前端值得持续发力,可以做深的点,成为任何一个领域的专家都足以突破前端工程师成长的天花板。
|
||||
|
||||
最后,前端是最贴近业务的技术之一,业务的未来决定了前端的未来,创造的业务价值决定了前端的价值,从现在开始锻炼自己的商业化思考能力与产品意识,看得懂业务,才能看到未来。
|
||||
|
||||
> 讨论地址是:[精读《前端未来展望》 · Issue #178 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/178)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
# 1. 引言
|
||||
|
||||
[javascript-knowledge-reading-source-code](https://www.smashingmagazine.com/2019/07/javascript-knowledge-reading-source-code/) 这篇文章介绍了阅读源码的重要性,精读系列也已有八期源码系列文章,分别是:
|
||||
|
||||
- [精读《Immer.js》源码](https://github.com/dt-fe/weekly/blob/v2/048.%E7%B2%BE%E8%AF%BB%E3%80%8AImmer.js%E3%80%8B%E6%BA%90%E7%A0%81.md)
|
||||
- [精读《sqorn 源码》](https://github.com/dt-fe/weekly/blob/v2/073.%E7%B2%BE%E8%AF%BB%E3%80%8Asqorn%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
|
||||
- [精读《Epitath 源码 - renderProps 新用法》](https://github.com/dt-fe/weekly/blob/v2/075.%E7%B2%BE%E8%AF%BB%E3%80%8AEpitath%20%E6%BA%90%E7%A0%81%20-%20renderProps%20%E6%96%B0%E7%94%A8%E6%B3%95%E3%80%8B.md)
|
||||
- [精读《Htm - Hyperscript 源码》](https://github.com/dt-fe/weekly/blob/v2/082.%E7%B2%BE%E8%AF%BB%E3%80%8AHtm%20-%20Hyperscript%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
|
||||
- [精读《React PowerPlug 源码》](https://github.com/dt-fe/weekly/blob/v2/092.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20PowerPlug%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
|
||||
- [精读《syntax-parser 源码》](https://github.com/dt-fe/weekly/blob/v2/093.%E7%B2%BE%E8%AF%BB%E3%80%8Asyntax-parser%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
|
||||
- [精读《react-easy-state 源码》](https://github.com/dt-fe/weekly/blob/v2/098.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-easy-state%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
|
||||
- [精读《Inject Instance 源码》](https://github.com/dt-fe/weekly/blob/v2/110.%E7%B2%BE%E8%AF%BB%E3%80%8AInject%20Instance%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
|
||||
|
||||
笔者自己的感悟是,读过大量源码的程序员有以下几个特质:
|
||||
|
||||
1. 思考具有系统性,主要体现在改一处代码模块时,会将项目所有文件串联起来整体考虑,提前评估影响面。
|
||||
2. 思考具有前瞻性,对已实现的方案可以快速评价所处阶段(临时 or 标准 or 可拓展),将边界情况提前解决,将框架 BUG 降低到最小程度。
|
||||
3. 代码实现更优雅,有大量源码经验做支撑,解决同样问题时,这些程序员可以用更短的行数、更合适的三方库解决问题,代码可读性更好,模块拆分更合理,更利于维护。
|
||||
|
||||
既然阅读源码这么重要,那么怎么才能读好源码呢?本周精读的文章就是一篇方法论文章,告诉你如何更好的阅读源码。
|
||||
|
||||
# 2. 概述
|
||||
|
||||
原文分三个部分:阅读源码的好处、阅读源码的技巧、以及 Redux Connect 的案例研究。
|
||||
|
||||
## 阅读源码的好处
|
||||
|
||||
阅读源码有助于理解抽象的概念,比如虚拟 DOM;有助于做方案调研,而不仅仅只看 Github star 数量;了解优秀框架目录结构的设计;看到一些陌生的工具函数,还可能激发你对 JS 规范的查阅,这种问题驱动的方式也是笔者推荐的 JS 规范学习方式。
|
||||
|
||||
## 阅读源码的技巧
|
||||
|
||||
最好的阅读源码方式是看文章,如果源码的作者有写源码解读文章,这就是最省力的方式。虽然直接看代码可以了解到所有细节,但当你不清楚设计思路时,仅看源码可能会找不到方向,而读源码的最终目的是找到核心的设计理念,如果一个框架没有自己核心设计理念,这个框架也不值得诞生,更不值得被阅读。如果框架的作者已经将框架核心理念写成了文章,那读文章就是最佳方案。
|
||||
|
||||
还有一种方式是断点,写一个最小程序,在框架执行入口出打下断点,然后按照执行路径一步步理解。虽然执行路径中会存在大量无关的函数干扰精力,但如果你足够有耐心,当断点走完时一定会有所收获。
|
||||
|
||||
原文还提到了一种看源码方式,即没有目的的寻宝。在寻找框架主要思路的过程中,遇到一些有意思的函数,可以停下来仔细阅读,可能会发现一些对你有启发的代码片段。
|
||||
|
||||
## Redux Connect 案例研究
|
||||
|
||||
原文以 Redux Connect 作为案例介绍研究思路。
|
||||
|
||||
首先看到 Connect 的功能 “包装组件” 后,就要问自己两个问题:
|
||||
|
||||
1. Connect 是如何实现包装组件后原样返回组件,但却增强组件功能的?(高阶组件知识)
|
||||
2. 了解这个设计模式后,如何利用已有的文档实现它?
|
||||
|
||||
通过创建一个使用 Connect 的基本程序:
|
||||
|
||||
```js
|
||||
class MarketContainer extends Component {
|
||||
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(MarketContainer);
|
||||
```
|
||||
|
||||
比如从生成 connect 函数的 [createConnect](https://github.com/reduxjs/react-redux/blob/master/src/connect/connect.js#L46) 我们就可以学习到 [Facade Pattern](http://jargon.js.org/_glossary/FACADE_PATTERN.md) - 门面模式。
|
||||
|
||||
从 `createConnect` 函数调用处:
|
||||
|
||||
```js
|
||||
export function createConnect({
|
||||
connectHOC = connectAdvanced,
|
||||
mapStateToPropsFactories = defaultMapStateToPropsFactories,
|
||||
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
|
||||
mergePropsFactories = defaultMergePropsFactories,
|
||||
selectorFactory = defaultSelectorFactory
|
||||
} = {})
|
||||
```
|
||||
|
||||
我们可以学习到解构默认函数参数的知识点。
|
||||
|
||||
总之,在学习源码的过程中,可以了解到一些新的 JS 特性,一些设计模式,这些都是额外的宝藏,不断理解并学会运用到自己写的框架里,就实现了源码学习的目的。
|
||||
|
||||
# 3. 精读
|
||||
|
||||
原文介绍了学习源码的两个技巧,并利用 Redux Connect 实例说明了源码学习过程中可以学到许多周边知识,都让我们受益匪浅。
|
||||
|
||||
笔者结合之前写过的八篇源码分析文章,把最重要的设计思路提取出来,以实际的例子展示阅读源码能给我们思维带来哪些帮助。
|
||||
|
||||
## Immerjs 源码的精华
|
||||
|
||||
Immer 可以让我们以 Mutable 的方式更新对象,最终得到一个 Immutable 对象:
|
||||
|
||||
```js
|
||||
this.setState(produce(state => (state.isShow = true)))
|
||||
```
|
||||
|
||||
> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/048.%E7%B2%BE%E8%AF%BB%E3%80%8AImmer.js%E3%80%8B%E6%BA%90%E7%A0%81.md#3-%E7%B2%BE%E8%AF%BB)。
|
||||
|
||||
核心思路是利用 Proxy 把脏活累活做掉。上面的例子中,`state` 已经是一个代理(Proxy)对象,通过自定义 `setting` 不断递归进行浅拷贝,最后返回一个新引用的顶层对象作为 `produce` 的返回值。
|
||||
|
||||
从 Immerjs 中,我们学到了 Proxy 可以化腐朽为神奇的用法,比看任何 Proxy 介绍文章都直观。
|
||||
|
||||
## sqorn 源码的精华
|
||||
|
||||
sqorn 是一个 sql orm,举例来看:
|
||||
|
||||
```js
|
||||
const sq = require("sqorn-pg")();
|
||||
|
||||
const Person = sq`person`,
|
||||
Book = sq`book`;
|
||||
|
||||
// SELECT
|
||||
const children = await Person`age < ${13}`;
|
||||
// "select * from person where age < 13"
|
||||
```
|
||||
|
||||
> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/073.%E7%B2%BE%E8%AF%BB%E3%80%8Asqorn%20%E6%BA%90%E7%A0%81%E3%80%8B.md#3-%E7%B2%BE%E8%AF%BB)
|
||||
|
||||
核心思路是在链式调用过程中创建 context 存储结构,并在链式调用的时候不断填充 context 信息,最终拿到的是一个结构化 context 对象,生成 sql 语句也就简单了。
|
||||
|
||||
从 sqorn 中,我们学到了如何实现链式调用 `init().a().b().c().print()` 最后拿到一个综合的结果,原理是内部维护了一个不断修改的对象。不论前端 React Vue 还是后端框架 Koa 等,一般都有内置的 context,一般实现这种优雅语法的框架内部都会维护 context。
|
||||
|
||||
## Epitath 源码的精华
|
||||
|
||||
Epitath 在 React Hooks 之前出来,解决了高阶函数地狱的问题:
|
||||
|
||||
```js
|
||||
const App = epitath(function*() {
|
||||
const { count } = yield <Counter />
|
||||
const { on } = yield <Toggle />
|
||||
|
||||
return (
|
||||
<MyComponent counter={count} toggle={on} />
|
||||
)
|
||||
})
|
||||
|
||||
<App />
|
||||
```
|
||||
|
||||
> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/075.%E7%B2%BE%E8%AF%BB%E3%80%8AEpitath%20%E6%BA%90%E7%A0%81%20-%20renderProps%20%E6%96%B0%E7%94%A8%E6%B3%95%E3%80%8B.md#3-%E7%B2%BE%E8%AF%BB)
|
||||
|
||||
其核心是利用 `generator` 的迭代,将 React 组件的平级结构还原成嵌套结构,将嵌套写法打平了:
|
||||
|
||||
```plain
|
||||
yield <A>
|
||||
yield <B>
|
||||
yield <C>
|
||||
// 等价于
|
||||
<A>
|
||||
<B>
|
||||
<C />
|
||||
</B>
|
||||
</A>
|
||||
```
|
||||
|
||||
从 epitath 中,我们了解到 `generator` 原来可以这么用,正因为其执行是多次迭代的,因此我们可以利用这个特性,改变代码运行结构。
|
||||
|
||||
## Htm - Hyperscript 源码的精华
|
||||
|
||||
Htm 将模版语法很自然的融入到了 html 中:
|
||||
|
||||
```js
|
||||
html`
|
||||
<div class="app">
|
||||
<${Header} name="ToDo's (${page})" />
|
||||
<ul>
|
||||
${todos.map(
|
||||
todo => html`
|
||||
<li>${todo}</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
<button onClick=${() => this.addTodo()}>Add Todo</button>
|
||||
<${Footer}>footer content here<//>
|
||||
</div>
|
||||
`;
|
||||
```
|
||||
|
||||
> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/082.%E7%B2%BE%E8%AF%BB%E3%80%8AHtm%20-%20Hyperscript%20%E6%BA%90%E7%A0%81%E3%80%8B.md#3-%E7%B2%BE%E8%AF%BB)
|
||||
|
||||
其核心是怎么根据模版拿到 dom 元素的 AST?拿到 AST 后就方便生成后续内容了。
|
||||
|
||||
作者的办法是:
|
||||
|
||||
```js
|
||||
const TEMPLATE = document.createElement("template");
|
||||
TEMPLATE.innerHTML = str;
|
||||
```
|
||||
|
||||
这样 TEMPLATE 就自带了 AST 解析,这是利用浏览器自带的 AST 解析拿到了 AST。从 Htm 中,我们学到了 `innerHTML` 可以生成标准 AST,所以只要有浏览器运行环境,需要拿 AST 的时候,不需要其他库,`innerHTML` 就是最好的方案。
|
||||
|
||||
## React PowerPlug 源码的精华
|
||||
|
||||
React PowerPlug 是一个利用 render props 进行状态管理的工具库。
|
||||
|
||||
它可以在 JSX 中对任意粒度插入状态管理:
|
||||
|
||||
```js
|
||||
<Value initial="React">
|
||||
{({ value, set, reset }) => (
|
||||
<>
|
||||
<Select
|
||||
label="Choose one"
|
||||
options={["React", "Preact", "Vue"]}
|
||||
value={value}
|
||||
onChange={set}
|
||||
/>
|
||||
<Button onClick={reset}>Reset to initial</Button>
|
||||
</>
|
||||
)}
|
||||
</Value>
|
||||
```
|
||||
|
||||
> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/092.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20PowerPlug%20%E6%BA%90%E7%A0%81%E3%80%8B.md#2-%E7%B2%BE%E8%AF%BB)
|
||||
|
||||
这个库的核心就是利用 render props 解决 JSX 局部状态管理的痛点,通过读源码了解 render props 的使用方式是这个源码带给你的最大价值。
|
||||
|
||||
## syntax-parser 源码的精华
|
||||
|
||||
syntax-parser 是一个 JS 版语法解器生成器,笔者也是作者,使用方式:
|
||||
|
||||
```js
|
||||
import { createParser, chain, matchTokenType, many } from "syntax-parser";
|
||||
|
||||
const root = () => chain(addExpr)(ast => ast[0]);
|
||||
|
||||
const addExpr = () =>
|
||||
chain(matchTokenType("word"), many(addPlus))(ast => ({
|
||||
left: ast[0].value,
|
||||
operator: ast[1] && ast[1][0].operator,
|
||||
right: ast[1] && ast[1][0].term
|
||||
}));
|
||||
|
||||
const addPlus = () =>
|
||||
chain("+"), root)(ast => ({
|
||||
operator: ast[0].value,
|
||||
term: ast[1]
|
||||
}));
|
||||
|
||||
const myParser = createParser(
|
||||
root, // Root grammar.
|
||||
myLexer // Created in lexer example.
|
||||
);
|
||||
```
|
||||
|
||||
> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/093.%E7%B2%BE%E8%AF%BB%E3%80%8Asyntax-parser%20%E6%BA%90%E7%A0%81%E3%80%8B.md#2-%E7%B2%BE%E8%AF%BB)
|
||||
|
||||
syntax-parser 的核心是利用双向链表实现了可回溯的语法解析器,了解了这个库,你可以自己实现 JS 调用堆栈,并在任意时候返回某个之前的执行状态重新执行。同时这个库的源码也会加强你对链表的理解,以及拓展你对链表使用场景的想象。
|
||||
|
||||
## react-easy-state 源码的精华
|
||||
|
||||
react-easy-state 利用 Proxy 创建了一个简易的全局数据流管理方式:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
import { store, view } from "react-easy-state";
|
||||
|
||||
const counter = store({ num: 0 });
|
||||
const increment = () => counter.num++;
|
||||
|
||||
export default view(() => <button onClick={increment}>{counter.num}</button>);
|
||||
```
|
||||
|
||||
> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/098.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-easy-state%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
|
||||
|
||||
react-easy-state 利用了 [observer-util](https://github.com/nx-js/observer-util) 实现主要功能,从中我们能学到最有价值的就是 Proxy 与 React 结合的设计理念,即利用 `getter` `setter` 实现数据与视图的双向绑定,或者叫依赖追踪,更多细节就不在这里展开,感兴趣可以阅读笔者之前写的 [抽丝剥茧,实现依赖追踪](https://github.com/dt-fe/weekly/blob/master/35.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0%E3%80%8B.md#%E6%8A%BD%E4%B8%9D%E5%89%A5%E8%8C%A7%E5%AE%9E%E7%8E%B0%E4%BE%9D%E8%B5%96%E8%BF%BD%E8%B8%AA) 一节。
|
||||
|
||||
## Inject Instance 源码的精华
|
||||
|
||||
inject-instance 是一个 Class 实现依赖注入的库:
|
||||
|
||||
```js
|
||||
import {inject} from 'inject-instance'
|
||||
import B from './B'
|
||||
|
||||
class A {
|
||||
@inject('B') private b: B
|
||||
public name = 'aaa'
|
||||
|
||||
say() {
|
||||
console.log('A inject B instance', this.b.name)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/110.%E7%B2%BE%E8%AF%BB%E3%80%8AInject%20Instance%20%E6%BA%90%E7%A0%81%E3%80%8B.md#2-%E7%B2%BE%E8%AF%BB)
|
||||
|
||||
主要对我们有两个启发,第一可以利用装饰器为对象存储一些额外信息,这些信息在必要的时候我们可以用到;第二是依赖注入并不复杂,通过提前实例化后,可以解决循环依赖的问题,即所有循环依赖问题都可以通过加一个父级解决。
|
||||
|
||||
# 4. 总结
|
||||
|
||||
阅读代码不是目的,读懂源码背后要表达的核心设计思路才是目的。比如写脚手架,阅读了大量脚手架源码的人写出的代码,与一个没有经验的人写出的代码会有天壤之别,这之间的差距就是对一些设计模式、三方库、结构设计的经验差距。
|
||||
|
||||
只学习理论太空洞,只看代码又太局限,学会从代码中看出理论才是最佳学习方式。
|
||||
|
||||
> 讨论地址是:[精读《源码学习》 · Issue #179 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/179)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,96 @@
|
||||
# 1. 引言
|
||||
|
||||
Node12 发布有几个月了,让我们跟随 [Nodejs 12](https://blog.logrocket.com/node-js-12/) 一起看看 Node12 带来了哪些改变。
|
||||
|
||||
# 2. 概述
|
||||
|
||||
Node12 与以往的版本不同,带来了许多重大升级,包括更多 V8 特性,Http 解析速度的提升,启动速度的提升,更好的诊断报告、内置堆分析工具,ESM 模块的更新等。
|
||||
|
||||
## V8 引擎升级
|
||||
|
||||
V8 升级带来了如下几个特性:
|
||||
|
||||
- [zero-cost async 堆栈信息](https://v8.dev/blog/v8-release-72#async-stack-traces) 原生支持了 async 堆栈信息,不会添加额外运行时内容。
|
||||
- [参数数量不匹配时性能优化](https://v8.dev/blog/v8-release-74#faster-calls-with-arguments-mismatch) 即便参数传递多了或少了,现在都几乎不会影响 Node 的执行速度。
|
||||
- [更快的 async](https://v8.dev/blog/v8-release-73#faster-await) async /await 已经比 promises 快了两个 microticks。
|
||||
- [更快的 Js 解析速度](https://v8.dev/blog/v8-release-72#javascript-parsing) 网页中的 V8 引擎一般花费 9.5% 时间在 JS 解析上,经过解析加速后,现在花费在 JS 解析上的时间降低到平均 7.5%。
|
||||
|
||||
可见 V8 引擎的升级不仅给 Node12 带来了福音,也会一定程度上提升网页的运行效率。
|
||||
|
||||
## TLS 1.3 更好的安全性
|
||||
|
||||
随着 Node12 的发布,TLS 从 1.2 升级到了 1.3,更安全且更易配置。通过使用 TLS 1.3,Node 程序可以减少 Https 握手所需时间来提升请求性能。
|
||||
|
||||
## 默认堆被正确配置了
|
||||
|
||||
以前默认堆大小需要通过 `-max-old-space-size` 设置,而且默认值是一个固定值,现在这个默认值可以根据可用内存动态分配,这样当内存较小时,Node 不会让内存移除而报错,而是主动终止自己的进程。
|
||||
|
||||
## 默认的 http 解析器变为 llhttp
|
||||
|
||||
nodejs 的 [http-parser](https://github.com/nodejs/http-parser) 已经非常难以维护和优化了,因此 [llhttp](https://github.com/nodejs/llhttp#readme) 这个库,比 http-parser 快 156%,更重要的是,在 Node12 中,将默认解析器切换到了 llhttp。
|
||||
|
||||
## 提供诊断报告
|
||||
|
||||
Node12 有一项实验功能,根据用户需求提供诊断报告,包括崩溃、性能下降、内存泄露、CPU 使用高等等。
|
||||
|
||||
## 堆内存 dump
|
||||
|
||||
在以前,如果要将堆内存生成 dump 文件,需要在生产环境安装额外的模块,而 Node12 集成了这个功能。
|
||||
|
||||
## 更好的原生模块支持
|
||||
|
||||
C++ 拓展 [N-API](https://nodejs.org/api/n-api.html#n_api_n_api) 升级到版本 4,同时一个原生模块可以被 C++ 编写并发布到 npm,就像一个普通 JS 模块一样被引用。不过要注意一些区别:
|
||||
|
||||
| | | JS 模块 | 原生拓展 |
|
||||
| --- | -------------------------------------- | ------- | ------------------ |
|
||||
| 1. | ... 需要编译 | 否 | 如果预编译了则不用 |
|
||||
| 2. | ... 是否可以运行在所有平台 | 是 | 如果预编译了则可以 |
|
||||
| 3. | ... 是否兼容所有 Node 版本 | 是 | 否 |
|
||||
| 4. | ... 会被加载多次 | 是 | 否 |
|
||||
| 5. | ... 如果没有明确使用多线程,则线程安全 | 是 | 否 |
|
||||
| 6. | ... 可以被销毁 | 是 | 否 |
|
||||
|
||||
## Worker 被正式启用了
|
||||
|
||||
`--experimental-worker` 实验开关已取消,默认支持 `worker_threads`。
|
||||
|
||||
要注意的是,执行 CPU 密集型任务时适合用 worker(大量计算),而执行 I/O 密集型任务时,Worker 反而没有 Node 内置的 I/O 操作性能好(读写文件)。
|
||||
|
||||
## 启动速度优化
|
||||
|
||||
通过在构建时提前为内置库生成代码缓存,最终使启动时间加快 30%。
|
||||
|
||||
## 支持 ES6 module
|
||||
|
||||
Node12 对 ES6 module 的支持依然处于实验阶段,需要通过 `--experimental-modules` 开启。
|
||||
|
||||
简单来说,就是支持了 Import Export 语法,不需要再转成 `require` 了!如果在 `package.json` 增加 `"type": "module"` 的配置,Node 将按照 ES6 module 方式处理。
|
||||
|
||||
## 新的编译器和平台要求
|
||||
|
||||
由于升级到新的 V8 引擎以及内部改造,因此 Node12 在 Mac 与 Windows 之外的平台上,需要至少 GCC6 和 glibc 2.17。
|
||||
|
||||
# 3. 精读
|
||||
|
||||
对于 V8 引擎升级、TLS 升级、堆配置自动化、http-parser 升级到 llhttp、启动速度优化都属于被动优化,代码无需改动,只要升级 Node 版本就可以享受。
|
||||
|
||||
支持 ES6 module 这个特性其实比较鸡肋,毕竟源码用 Ts 写的话,这些升级并不会对源码产生影响。
|
||||
|
||||
`worker_threads` 可以被默认启用,就像以前支持 `async/await` 一样,会带来 Nodejs 多线程更广泛的使用。
|
||||
|
||||
Node12 更新了 V8 引擎,随着 V8 的更新,很多 ES 新规范也落地了,比如 Class 成员函数、私有成员变量等等。
|
||||
|
||||
# 4. 总结
|
||||
|
||||
Nodejs 仅有 10 年历史,但现在越来越被开发者欢迎,因为它可以让 JS 运行在服务端,是扩大 JS 生态的重要一环。从 Node 更新历史中可以看到,性能和语法能力稳步提升,一些服务端环境需要的诊断报告、堆栈分析能力都在逐渐完善,社区上也有 Alinode 与 egg、express、koa 等好用的服务框架,相对于前端翻天覆地的变化,对 Node 的评价只有一个字:稳。
|
||||
|
||||
|
||||
> 讨论地址是:[精读《Nodejs V12》 · Issue #184 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/184)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,193 @@
|
||||
# 1. 引言
|
||||
|
||||
[谁在世界中心](https://book.douban.com/subject/27045287/) 是一本介绍地缘政治的书,这本书以海洋为连接世界的主要桥梁,介绍了当今全球视野下海洋争霸的政治格局。
|
||||
|
||||
谁征服了海洋,谁就征服了世界。陆地霸权注定无法拥有全球视野,只有海洋霸权才能征服世界,如今中国已成为海洋贸易霸主,但海洋的武力霸主仍然是美国,如果中国想成为新的全球霸主,就要突破旧的海洋霸权封锁,成为新的海洋霸主。
|
||||
|
||||
当然想成为海洋霸主是非常困难的,这涉及到多方政治力量的博弈,但我们可以通过《谁在世界中心》这本书了解地缘政治关系,让我们看清当下,布局未来。
|
||||
|
||||
《谁在世界中心》共五章,分别介绍了当下谁在主宰世界、东亚与西太平洋、东南亚与南海、南亚与印度洋、俄罗斯与北冰洋。
|
||||
|
||||
之所以标题都是地区与海的关系,是因为陆地与海洋的博弈就是海洋霸权的逻辑。本书需要结合地图理解,因此笔者会贴一些书中地图,围绕着地图讲解本书。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
## 谁在主宰世界
|
||||
|
||||
现在 **美、俄、欧、中** 是这个舞台的主角,但谁也不能仅凭一个地区征服世界,因此与一些重要地区结盟,并成为地区的领导者才可能成为世界的霸主。**边缘地带理论** 就是指,控制了大陆板块的边缘地区,就可以对大陆进行封锁,进而控制大陆。在将眼光放到边缘地区之前,先看看现在世界舞台上的主要政治力量:
|
||||
|
||||
1. 俄罗斯 - 大陆的征服者。俄罗斯一直有扩张的野心,但是在苏联解体后,值保有大部分欧亚大陆中心地带,目前已经失去领衔主演的资格。
|
||||
2. 欧盟 - 世界的发现者。作为大航海时代的开启者,欧洲史就是浓缩的世界史,并且随着疯狂的资本掠夺积累了大量原始资本。但由于英国在欧洲板块处于海洋势力,无法完全控制大陆,因此极力避免欧洲土地上出现一家独大的情况,这导致了美国的崛起。当然现在欧盟的成立也标志着欧洲进入了漫长的整合时期,德国由于其较差的地缘位置(二战后海外利益尽失),更愿意以裹挟欧盟的方式让自己成为主角。
|
||||
3. 印度 - 低纬度地区的代言人。由于低纬度炎热的气候,印度人并不热衷于国际事务,但和美国一样,印度也发展了自己的地缘优势以及人口优势,希望代表低纬度地区参与大国游戏。但是要承受另一个边缘地区国家 - 中国的压力。
|
||||
4. 中国 - 世界中心最有力的挑战者。中国拥有极大的战略纵深,集体主义文化,拥有挑战世界霸主的潜力,但在这个道路上还需解决许多问题,尤其是如何突破由美国主导的 “新世界岛俱乐部” 的封锁。
|
||||
5. 美国 - **“新世界岛俱乐部”** 的缔造者。北美是新世界岛的中心地区,英国和日本是新世界岛的外围地区,分别用来控制 “欧亚大陆西边缘地区(西欧)” 与 “欧亚大陆东边缘地区(中国)”。
|
||||
|
||||
为什么拥有海洋就拥有了世界?“海权论” 有三个主要观点:
|
||||
|
||||
1. **谁掌握了世界核心的咽喉航道、运河和航线,谁就掌握了世界经济和能源运输之门。**
|
||||
2. **谁掌握了世界经济和能源运输之门,谁就掌握了世界各国的经济和安全命脉。**
|
||||
3. **谁掌握了世界各国的经济和安全命脉,谁就控制了全世界。**
|
||||
|
||||
但独霸海洋非常困难,因此美国奉行的是 “边缘地带理论”。也就是**通过控制欧亚大陆东西两端的边缘地带,进而控制了欧亚大陆核心地区,封锁住欧亚大陆的强国,以此保证美国世界霸主的地位。**
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1R76hca61gK0jSZFlXXXDKFXa-1816-2648.jpg">
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1gaHecXP7gK0jSZFjXXc5aXXa-1816-2648.jpg">
|
||||
|
||||
从上图可以看出,以北美为 “新世界岛” 的中心地区,通过控制日本与英国,牵制住西欧与中国。美国实际上也做到了这一点。而随着印度的崛起,美国也找到了澳大利亚作为遏制印度的桥头堡。
|
||||
|
||||
那么中国怎么崛起呢?很显然,中国需要组建属于自己的 “世界岛俱乐部”,取代由美国主导的 “旧世界岛俱乐部”:
|
||||
|
||||
1. **与欧亚大陆中心地带的大部分国家(主要是俄罗斯)结盟。**
|
||||
2. **将 “欧亚大陆南边缘地区”(印度)拉入同盟。**
|
||||
3. **寻找可能的 “世界岛外围地区”,并使之倒向同盟(日本、韩国、朝鲜等)。**
|
||||
|
||||
但就目前状况来看,中印关系竞争与合作同时上升,俄国由于前苏联的老大地位暂时不愿意放下身段,日本更处于美国为中心的俱乐部中,因此这条路困难重重。之所以将日本拉进来,一方面是因为与印、俄结盟不足以取得与 “旧世界岛俱乐部” 竞争的优势,一方面是中日地缘距离近,且日本国民性格敬仰强大的对手,另一方面日本是美国牵制中国的力量,拉拢过来不仅可以打消美国的算盘,还能增强东亚整体实力。
|
||||
|
||||
第一章总览了世界地缘政治关系的全貌,并为中国崛起指出了道路。后面几章则具体介绍各个存在联动的政治板块间的具体博弈情况,做到知己知彼。
|
||||
|
||||
## 东亚与西太平洋
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1DbnfcoY1gK0jSZFMXXaWcVXa-1746-1480.png">
|
||||
|
||||
参与东亚与西太平洋博弈的主要国家有:**中国、朝鲜、韩国、日本、俄罗斯**。中国是参与板块博弈的核心,比如俄罗斯会在朝鲜半岛问题方面发表意见,但不会干涉钓鱼岛问题,而中国都参与其中。
|
||||
|
||||
东亚平原如此广袤,以至于东亚地区的民族都认为控制了这片核心区就控制了世界中心。但随着西方殖民者从海路上到来,**中国人才明白自己并不是世界的中心**,但长期 “中央之国的心态” 影响着我们每一个人。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1fqvHceL2gK0jSZPhXXahvXXa-1594-1368.png">
|
||||
|
||||
**中国农耕区域总是受到来自北方三个势力的威胁:**“东北森林渔猎民族”、“蒙古高原草原游牧民族”、“青藏高原高原游牧民族”,这是由于农耕的生产方式稳定,创造的财富大,因此源源不断吸引这些外来者的入侵,有趣的是,每一次农耕区域都能同化外来的入侵者,而 “中国” 的传统观念也是同化他们的重要因素。所以到后面会讲到为何印度人进取心不如中国强,原因就在中国需要长期与北方威胁斗争,而印度不需要,印度由于地缘位置,导致不会受到太多来自边远民族的入侵,这个在分析印度时会讲到。
|
||||
|
||||
**日本、朝鲜半岛由于地理阻隔,在东亚大陆统一时得保持独立**。而朝鲜半岛与大陆相连却一直没有被征服的原因是,从地图上看,想要入侵朝鲜半岛必须沿着海岸线,但通过辽西走廊进入辽河平原时,辽河平原地理气候的不稳定性容易切断朝鲜半岛与东亚核心区脆弱的地缘联系,导致渗入半岛的人口要么退回,要么融于当地族群。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1JdHKcoz1gK0jSZLeXXb9kVXa-1582-1496.png">
|
||||
|
||||
东亚面临西太平洋区域被外包包夹形成四个 “内海”,可以形容为 **“第一岛链” 与 “第二岛链”**,美国正是通过控制这些岛链来控制 “欧亚大陆东边缘地区” 的。
|
||||
|
||||
**第一岛链包括:日本群岛、琉球群岛、冲绳岛、台湾岛、南至菲律宾群岛、大巽他群岛**。其中日本是势力最大的岛链,在日本 “大东亚共荣圈” 计划中,极盛时期控制的范围如下图所示:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1GBzJceT2gK0jSZFvXXXnFXXa-1610-1452.png">
|
||||
|
||||
而中国想要成为世界霸主,**就要构建以中国为主导核心的 “东亚核心圈 + 东盟十国”**,见下图。
|
||||
|
||||
对日本来说,如今已没有实力做这个核心圈的老大,但最起码希望和中国共同主导,但核心圈只有一家独大才能发挥称霸世界的力量,中国与日本还有很多问题需要解决。相比欧盟,虽然也在融合(3 + 10 模式,即三个核心 - 法、德、英 + 10 个其他国家 不包含俄罗斯),但由于地缘特点不可能出现一家独大的情况。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1USbGcaL7gK0jSZFBXXXZZpXa-1864-1606.png">
|
||||
|
||||
**第二岛链包括:从日本岛作为起点,南经小笠原诸国、火山列岛、马里亚纳群岛、关岛、雅浦岛、帕劳群岛,直至哈马黑拉岛等岛群。** 不过第二岛链的威胁远没有第一岛链大。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1uRPKchD1gK0jSZFsXXbldVXa-1604-1134.png">
|
||||
|
||||
从西太平洋向东看看美国。**对美国来说,太平洋所有岛屿都是他进攻的跳板**。如上图所示,美国通过诸多岛屿作为跳板进攻,在二战中,甚至跳过了对某些战略要地的争夺,通过前沿岛屿作为基地,直接攻击日本本岛。
|
||||
|
||||
## 东南亚与南海
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1H7HOckH0gK0jSZPiXXavapXa-1870-1514.png">
|
||||
|
||||
**东南亚区域分位:中南半岛(缅甸、越南与印度支那、泰国)、南洋群岛、文莱、巴厘岛以及东帝汶、马六甲海峡等重要区域。**
|
||||
|
||||
首先看中南半岛:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1exPKcaL7gK0jSZFBXXXZZpXa-1742-2534.png">
|
||||
|
||||
**中南半岛由 5 个国家组成,从西到东分别是:缅甸、泰国、柬埔寨、老挝、越南**,其中缅、老、 越与中国接壤,除了老挝外都有足够的海岸线。这些国家大部分是殖民时代的遗产,英法分别在缅甸、越南发力,将泰国定位缓冲国。法国人曾将柬埔寨、老挝、越南合并成 “印支联邦” 与英国对抗,虽然现在又分裂成三个国家,因此却为越南埋下了大国梦。
|
||||
|
||||
缅甸在位置上,可以在陆地及海洋延伸中国的地缘影响力,而且也曾成为支持中国抗战的重要援助物资运输线。在缅甸东边是 “金三角地区”:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1iwnRchD1gK0jSZFKXXcJrVXa-1396-1266.png">
|
||||
|
||||
**金三角地区** 盛产鸦片,首先是金三角环境适合种植鸦片,其次由于所处缅甸、老挝、泰国交界处特别适合逃避法律打击。解决问题的办法就是联合执法,在 “湄公河惨案” 后,由中国主导的联合执法开发形成常态,**金三角成为中国拓展自己地缘影响力的重要抓手**。
|
||||
|
||||
中国与中南半岛虽然地缘上存在天然阻隔,但在云贵高原与克钦邦之间存在的南方丝绸之路、中印缅之间存在因抗日战争运输物资而修建了史迪威公路。这些重要的交通枢纽对维系中、缅两国的共同利益有着推动作用。
|
||||
|
||||
**越南** 一直想成为中南半岛的强国,但先后被清朝打压、后与法美中几大国相继开战,始终没有得到什么实际利益。越南狭长的地形使其一直存在南北分裂的风险。
|
||||
|
||||
**泰国** 之所以能在西方殖民者将土地瓜分完毕时仍保持独立,是凭借其高超的平衡技巧,成为了英法殖民地之间的缓冲国。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB17bYZcXY7gK0jSZKzXXaikpXa-1420-980.png">
|
||||
|
||||
克拉地峡是继马六甲海峡后另一个有价值的航线,是否能开挖取决于各方利益平衡,尤其是这样会切断泰国南北,导致加大泰国南部的分裂倾向。
|
||||
|
||||
<img width=700 src="https://img.alicdn.com/tfs/TB12hvTckP2gK0jSZPxXXacQpXa-2534-1734.png">
|
||||
|
||||
**南洋群岛由 6 个国家组成,分别是:印尼、马拉西亚、菲律宾、文莱、新加坡、东帝汶。**
|
||||
|
||||
“下南洋” 期间,在西方殖民者的推动下,大量华人下南洋开发,因此南洋群岛留着部分华夏民族血液。在新加坡,甚至因为华人占据了 75% 的人口,马来西亚为了在脱离英国殖民统治后保证马来人获得多数票,因此将新加坡排除在马来西亚联邦之外,才使得新加坡独立建国。
|
||||
|
||||
接着看文莱、巴厘岛和东帝汶:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1ZB24cfb2gK0jSZK9XXaEgFXa-1434-1274.png">
|
||||
|
||||
**文莱** 在马来西亚中是个弹丸小国,但因为在西方殖民者接入之前,文莱的前身 “渤泥国” 的势力范围很大,因此在被殖民者打碎野心的情况下,文莱有着强烈独立的愿望,从争取 “保护国” 的地位到最终独立,文莱一路走来很不容易。但是文莱被 “林梦地区” 一分为二,马来西亚也不会容忍文莱有更多的领土要求,两者僵持不下。但我们相信,身处这种状况的文莱更希望获得外部力量的支持,作为与南海隔海相望的中国将会是其理想的盟友。
|
||||
|
||||
**巴厘岛** 不仅是度假胜地,在 14 世纪末至 15 世纪初,在伊斯兰教强大压力下,坚守印度教的少数马来人从爪哇岛移民至巴厘岛,因为宗教信仰的不同,这里引起恐怖分子的关注。
|
||||
|
||||
**东帝汶** 是欧洲殖民者划分殖民地的产物,南部的澳大利亚觊觎其丰富油矿资源而积极干涉东帝汶的事物。反过来想,如果中国控制了东帝汶区域,就可以对澳大利亚施加政治影响力。
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/tfs/TB1TVY6cXT7gK0jSZFpXXaTkpXa-1736-2532.png">
|
||||
|
||||
相比南洋群岛,**南海** 离中国更近。如果要控制南海,就要分别控制位于南海五个方向的:**东沙群岛、西沙群岛、黄岩岛、中沙大环礁、南沙群岛**。
|
||||
|
||||
中国想要经略南海,首先要提升自己的综合实力。最近能够在南海问题上有所突破,本质上还是中国综合实力得到了提高。但经略南海不代表占领南海,而是要与南海周边的国家进行博弈,合纵连横。搁置争议,共同开发是最好的策略,如果中国能够掌握深海石油勘采技术,至少能在投资、技术层面让多方面获益。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1gpr9coz1gK0jSZLeXXb9kVXa-1640-1146.png">
|
||||
|
||||
从中国海上突围角度来看,有三条航线可选:**南海-马六甲海峡-印度洋航线、印尼通道-印度洋航线、西太平洋-南太平洋-印度洋航线**
|
||||
|
||||
**马六甲海峡** 是南海的咽喉,被新加坡控制,且战时容易被封锁。备选方案印尼通道是个不错的选择,而且相比马六甲海峡三国(新加坡、马来西亚、印度尼西亚),**印尼通道** 只要和印尼搞好关系即可。然而印尼也可能被日本拉拢,但由于印尼与中国没有直接利益冲突,站队日本对印尼来说得不到什么好处。
|
||||
|
||||
## 南亚与印度洋
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1dC2_coH1gK0jSZSyXXXtlpXa-1734-2532.png">
|
||||
|
||||
**南亚包括 7 个国家**,分别是南亚次大陆的:**尼泊尔、不丹、巴基斯坦、印度、孟加拉国** 和印度洋上的岛国:**斯里兰卡、马尔代夫。**
|
||||
|
||||
由于 “印巴分治”,巴基斯坦于 1947 年独立,但由于东西距离太远,中间隔着印度,因此东边独立成了孟加拉国。不丹处于印度保护国状态,而斯里兰卡除了地理阻隔外,有意识的选择了不同的宗教,也是一直保持独立的重要原因。
|
||||
|
||||
再往南的马尔代夫给人留下的印象就是度假胜地,但这个海拔只有 1.2 米的岛国,随着气候变暖可能是最先消失的国家。
|
||||
|
||||
**印度** 之所以走上与中国不同的道路,主要因为外部压力相对较小。之前也介绍了中国长期受到北方民族的入侵,是因为中国北方有足够大的阶梯地形让北方民族适应 “低原反应”,而印度北方的山脉没有足够的缓冲区,为印度形成了天然的防护屏障。
|
||||
|
||||
虽然热带气候可以让文明较早发展,但没有边缘民族入侵压力,会让文明变得非常脆弱,也缺乏扩张的动力。从融合的角度来说,印度虽然也融合了其他民族,但相比 **中国的 “家天下”,印度属于 “种姓” 文明框架。** “家天下” 的模式每个人都有平等的机会,而 “种姓” 制度确保了阶级固化,加上热带地区物产丰富,不至于出现被饿死的情况,因此这种制度得以稳定下来。
|
||||
|
||||
**克什米尔** 是印度河的上游,在工业化时代,掌握了上游就可以控制下游的水资源,现在印巴两国共享上印度河平原,任何一方都不会轻易放弃这块战略要地。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1R93bcoT1gK0jSZFhXXaAtVXa-1414-1102.png">
|
||||
|
||||
中国想要扩大自己在印度洋的影响力,就需要找到 **缅甸、巴基斯坦、斯里兰卡、东帝汶、肯尼亚** 这五个点做支持。如今中国一带一路计划,为东亚各国修筑高铁等基础设施,就是拓展中国外交空间的良好手段,加深经济的合作才有可能迎来政治合作。
|
||||
|
||||
## 俄罗斯与北冰洋
|
||||
|
||||
**俄国** 虽然北临北冰洋,但是没有不冻港是无法通航的。俄国人通过不平等条约使中国东部边界从 **库页岛** 移到了 **乌苏里江**,因此俄国成为了第二个同时可以对三个洋(太平洋、大西洋、北冰洋)施加地缘影响力的国家。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1tf7ccmf2gK0jSZFPXXXsopXa-1410-1108.png">
|
||||
|
||||
不过好在中俄存在 “背靠背” 的战略伙伴关系,因此有合作的空间(俄国要应对西欧,中国要应对东南亚)。但俄罗斯的海岸线很短,这导致俄国在海权争霸的舞台只能当配角,但这个地缘结构不是一成不变的,如果全球变暖导致北冰洋融化,看到的将是另一个格局:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1x6RGbKbviK0jSZFNXXaApXXa-1440-1230.png">
|
||||
|
||||
如果北冰洋融化后可以通航,俄罗斯将成为北冰洋地缘势力最强大的国家,其次是加拿大与阿拉斯加。如果俄国没有短视将阿拉斯加卖给美国,俄国将为成为北冰洋唯一的霸主。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
《谁在世界中心》这本书一定要看着地图读,这样会发现板块运动随机产生的变化竟然会对世界政治格局产生这么重大的影响,一个国家能否独立最重要的还是看地缘位置。
|
||||
|
||||
这本书更是一本中国崛起的地缘解决方案指南,其中一些解决方案在商业逻辑中可以拿来借鉴:
|
||||
|
||||
1. 竞争是一个过程,唯有不断参与其中,才有可能掌握话语权,主导权,最终达到政治目的。任何领土都是通过与周边地区漫长博弈后逐渐确立下来的,想得到利益首先得参与到游戏中。
|
||||
2. 各玩家实力是动态变化的,即便是无法通航的北冰洋,都可能因为温室效应变成不冻港,因此提前看到趋势并提前准备是必须的。
|
||||
3. 想从对方获得利益,首先要了解对方想获得什么利益,自己有什么筹码,这样才容易促成合作。在寻找盟友前,先站在对方角度掂量一下自己是否合适。
|
||||
4. 不可能一家独大,想成为霸主,必须建立一个生态。以前是小弟听大哥的话,现在大哥得给小弟好处,才能得到小弟的忠诚。
|
||||
5. 已有霸主的地位不是一朝一夕就能摧毁的,就像中国想突破马六甲海峡的封锁,在不突破整体海洋封锁的前提下是不可能的,因为海洋霸权是一个全球化整体,美国封锁亚洲有完整的第一岛链、第二岛链逻辑,解决问题的视角要全面。
|
||||
|
||||
如今处于大变革的和平时代,国家之间看似和平,实则在进行经济扩张,大国之间要学会不撕破脸的竞争方式。而这个多方博弈的复杂性,使得阴谋几乎不可能得逞,大国的政策都是阳谋,比的是谁更能顺势而为,拉拢更多合作者。
|
||||
|
||||
> 讨论地址是:[精读《谁在世界中心》 · Issue #189 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/189)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,261 @@
|
||||
# 1. 引言
|
||||
|
||||
引用著名瑞典统计学家 Hans Rosling 的一句话:想法来源于数字、信息,再到理解。
|
||||
|
||||
分析数据的最好方式是可视化,因为可视化承载的信息密度更高,甚至可以从不同维度对数据进行交互式分析。今天要精读的文章就分析了经典可视化分析工具 Tableau:[data-visualisation-made-easy](https://www.analyticsvidhya.com/blog/2017/07/data-visualisation-made-easy/)。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
[Tableau](https://www.tableau.com/) 是一款广泛用于智能商业的强大数据分析工具,通过不同可交互的图表和仪表盘帮助你获得业务洞见。
|
||||
|
||||
## 安装
|
||||
|
||||
Tableau 提供了三种使用方式:
|
||||
|
||||
**Tableau Desktop**
|
||||
|
||||
[拥有 14 天免费试用的桌面版](https://www.tableau.com/products/trial),可以将工作数据存储在计算机本地,如果你是学生或老师可以获得一年的免费使用权。
|
||||
|
||||
**Tableau Public**
|
||||
|
||||
[公开版完全免费](https://public.tableau.com/s/download),和桌面版的唯一区别是,所有数据都无法保存在本地,只能保存在 Tableau 服务器的云端,而且是公开的。
|
||||
|
||||
**Tableau Online**
|
||||
|
||||
[网页版也完全免费](https://sso.online.tableau.com/public/idp/SSO),是 Tableau Public 的网页版。
|
||||
|
||||
## 连接数据源
|
||||
|
||||
安装好 Tableau 后,第一步就是连接数据源。它支持连接本地或云端的数据源,本地最常用的数据源可以从 Excel 转换。这里是一份 [样例数据](https://github.com/pavleenkaur/TableauTutorial-On-AnalyticsVidhya/blob/master/Sample-Superstore.xls),包含了一个超市几年内的销售情况,我们可以用这份数据练手。
|
||||
|
||||
下载好这份数据后,选择从 Excel 导入,确认后将 **Orders** 表拖拽到右侧区域,如下图所示:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1Cvh5cV67gK0jSZPfXXahhFXa-1440-900.png">
|
||||
|
||||
可以看到,导入的数据格式有些问题,这是因为这份 Excel 文件表头有一些描述信息干扰。勾选 **Use Data Interpreter** 后,可以开启数据解析功能,自动分析出你想要的表结构:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1XLJ_c7T2gK0jSZPcXXcKkpXa-1440-900.png">
|
||||
|
||||
可以看到表结构已经正常了,在数据清洗的过程中,Tableau 强大的数据分析功能已经初见端倪。你甚至可以点击 **Review ths results** 看看它是如何清洗数据的:点击后会下载一份分析 Excel,其中过滤掉的数据会被标记,自动分析出的表结构会被高亮。
|
||||
|
||||
## 数据可视化
|
||||
|
||||
在页面最底部有几个切换项,依次是 **Data Source**:数据源、**Sheet**:工作簿,后面跟随的三个按钮可以继续创建多个 Sheet、Dashboard、Story,这些后面都会讲到。首先点击 Sheet 进入可视化分析的工作簿:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1S2LzcebviK0jSZFNXXaApXXa-1440-900.png">
|
||||
|
||||
可以看到,Orders 表的字段已经被自动分析成 **维度** **度量** 了。维度和度量是数据分析中重要的概念:
|
||||
|
||||
- **维度:** 维度是不能被计数的字段,一般为字符串或离散的值,用来描述数据的维度。
|
||||
- **度量:** 度量是可以被计数的字段,一般为数字、日期等连续的值,用来描述数据的量。
|
||||
|
||||
右侧空白区域是图表展示区域,**可以响应拖拽交互**,顶部的 Columns、Rows 表示列与行,Filters 是过滤器,拖拽字段上去可以对此字段进行过滤,Marks 是标记,Tableau 将图表所有辅助标记功能都抽象为:颜色、大小、文本、具体值、工具提示。举个例子,如果将销量 Sales 字段拖拽到大小区域,那么任何能描述大小的图表,都会以销量的多少来决定大小,比如散点图。
|
||||
|
||||
右上角的 **Show Me** 是图表自动推荐区域,当你拖拽不同字段的时候,Tableau 会自动展示合适的图表,但你也可以点击 Show Me 进行图表切换。
|
||||
|
||||
那么开始动手吧!**首先我们要看看大盘数据如何,也就是这家超市的总利润、质量、销量:**
|
||||
|
||||
> 在左侧维度栏目下,最后一个字段 **Measure Names** 表示所有度量的集合。
|
||||
|
||||
1. 将 **Measure Names** 拖拽到画布的空白区域。
|
||||
2. 移除我们不关心的 Row ID, Discount 等字段。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1caeXcYH1gK0jSZFwXXc7aXXa-1440-900.png">
|
||||
|
||||
可以看到,总利润大概是总销量的 10%。如果想展示横向表格,将 Measure Names 从 Rows 拖拽到 Columns 即可。
|
||||
|
||||
> Tips: 为了方便区分,Tableau 贴心的将维度标记为蓝色,度量标记为绿色。
|
||||
> 同时可以看到,Tableau 对于单指标拖拽,默认采取表格方式渲染。
|
||||
|
||||
**接下来我们要看每一年的详细销量与利润:**
|
||||
|
||||
1. 将 Order Date 与 Sales 拖拽到 Rows。
|
||||
2. 右键 Sales,将类型从连续改成非连续,这样就会自动变成表格展示。
|
||||
3. 为了展示利润,将 Profit 字段拖拽到 Marks 的 Text 字段上。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1fxV_c4v1gK0jSZFFXXb0sXXa-1440-900.png">
|
||||
|
||||
我们可以看到,无论是销量还是利润都在逐年上升。**接下来我们想具体看看每个月份的数据**:
|
||||
|
||||
1. 右键 Order Date,将日期维度从年切换到月。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1SCN9c.T1gK0jSZFrXXcNCXXa-1440-900.png">
|
||||
|
||||
我们可以看到,销量较高的月份分布在:3、9、11、12 月。注意由于没有对年份做筛选,这里的每月统计数据是整合了 2013~2016 四年份的。也就是 1 月的数据其实代表了 2013.1 + 2014.1 + 2015.1 + 2016.1 共四个 1 月份数据的总和。
|
||||
|
||||
**接下来我们想了解销量与利润增长的趋势:**
|
||||
|
||||
1. 将 Order Date 拖拽到 Columns。
|
||||
2. 将 Sales 拖拽到 Rows,此时会出现一条线。接下来将 Profit 拖拽到 **左 Y 轴**。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1zVF_c7P2gK0jSZPxXXacQpXa-1440-900.png">
|
||||
|
||||
这里就涉及到线图拖拽交互设计了,线图一共有三种拖拽方式。如果将一个新字段拖拽到左 Y 轴,就会在左 Y 轴多出一条线;如果拖拽到中间图表区域,则这个字段会当作已有字段的工具提示;如果拖拽到右 Y 轴,则会自动变成双轴图。
|
||||
|
||||
从上图中能看到,销量增长明显,但利润增长缓慢,看来经营是存在一定问题的,还要继续分析问题在哪。
|
||||
|
||||
**我们再看看数据按月分布情况**,同样右击 Order Date,选择 月 粒度:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1RmJ9c.H1gK0jSZSyXXXtlpXa-1440-900.png">
|
||||
|
||||
上图可以明显看到三个峰值出现在 3、9、11 月份,然而这段期间利润增长幅度却不大,可以看出这段期间采取了薄利多销的手段。
|
||||
|
||||
**再从地区维度分析数据:**
|
||||
|
||||
1. 将 Regions 和 Sales 拖拽到 Columns。
|
||||
2. 切换到饼图。
|
||||
3. 将 Sales 拖拽到 Marks Pane 的 Label 上。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1KEJ_c7Y2gK0jSZFgXXc5OFXa-1440-900.png">
|
||||
|
||||
可以看到东西部地区是销量最高的区域。**接下来我们想看具体城市的销量:**
|
||||
|
||||
1. 将 States 拖拽到画布空白区域,此时会自动出现地图并定位到美国。将 Profits 拖拽到 Color。
|
||||
2. 将地区切换到 Filled Map,将 Profits 拖拽到 Label。
|
||||
|
||||
这样就绘制了一张地区,颜色越深利润越高,数字表示销量。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1CFWbc.Y1gK0jSZFCXXcwqXXa-1440-900.png">
|
||||
|
||||
可以看到数值越大的区域一般颜色也越深,但这不是分析利润/销量性价比的最佳方式,我们先只看到加州和纽约是销售业绩最好的区域,而科罗拉多州虽然销量不错,但利润却是负的。
|
||||
|
||||
上面的地图对地形比较直观,但要分析销售健康度,还是用散点图更合适。**我们想看看城市销量/利润的健康度分布:**
|
||||
|
||||
1. Profit 拖拽到 Columns,Sales 拖拽到 Rows,此时散点图出现,但只有一个点(之所以出现散点图,是因为横纵轴拖拽的都是度量)。
|
||||
2. 我们想按城市下钻,只要把 State 拖拽到 Detail 即可。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1EMl9cVY7gK0jSZKzXXaikpXa-1440-900.png">
|
||||
|
||||
可以看到,遥遥领先的城市有三个,加州是销售之王。
|
||||
|
||||
由于还没有介绍到筛选条件,这里简略介绍一下,其实还可以将年份拖拽到筛选条件,只看 2013 年的分布图,也可以点击或圈选其中某些点选择排除某些城市。
|
||||
|
||||
**现在需要进一步分析明细数据,将不同商品种类按年份细分,看按月的销量,并看看这些月份的利润如何:**
|
||||
|
||||
1. 此时需要用到高亮表格。首先将 Category 和 Order Date 拖拽到 Rows,简单的表格出现了。
|
||||
2. 将 Order Date 再拖拽到 Columns,并右键将其粒度改为月。
|
||||
3. 在 Show Me 中切换为 Highlight Table,重新将 Order Date(Year)拖拽回 Rows。
|
||||
4. 为了展示颜色与文字,将 Profit 拖拽到 Color,Sales 拖拽到 Label。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1Ud07cWL7gK0jSZFBXXXZZpXa-1440-900.png">
|
||||
|
||||
可以看到,办公套件和科技产品业绩最好,其中办公套件在 2015 年 12 月销量利润双丰收,科技产品在 2015 年 10 月与 2016 年 3 月销量利润双丰收。整体来看前半年是淡季。
|
||||
|
||||
但这张图无法看到销量与利润性价比关系,**我们要找出利润率最高的商品和利润率最低的商品:**
|
||||
|
||||
1. 将 Proft 拖拽到 Columns。
|
||||
2. 将 Sub-Category 拖拽到 Rows。
|
||||
3. 切换到 Horizontal Bars。
|
||||
4. 将销量 Sales 拖拽到 Color。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB10T4.c9f2gK0jSZFPXXXsopXa-1440-900.png">
|
||||
|
||||
可以明显看到 Copiers 就是性价比之王,拥有最高的利润,但销量却不是很高(颜色深度中等),而桌子是性价比最低的,利润为负,而且销量不低。
|
||||
|
||||
## 其他功能
|
||||
|
||||
除了上面基本可视化分析能力之外,Tableau 还有许多辅助功能。
|
||||
|
||||
### 筛选器
|
||||
|
||||
在按月分布的折线图中,如果我们只想看某一年的,可以将 Order Date 拖拽到 Filters 区域,只勾选想要保留的年份:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1jgWcc1H2gK0jSZJnXXaT1FXa-1440-900.png">
|
||||
|
||||
Tablueau 这种交互等价于 Sql 中 `in` 语句,当然 Tablueau 还支持更复杂的条件或代码表达式,这里只是将更友好的筛选方式优先展示区来。
|
||||
|
||||
### 上卷下钻
|
||||
|
||||
Tableau 支持任意维度之间的上卷下钻,只要你将他们分好组。
|
||||
|
||||
比如将 Order Date、Order ID、Ship Date、Ship Mode 拖拽到一起,成为 Orders 组;将 Category、Sub-Category、Product ID Product Name 形成 Product 组:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB11lmcc.Y1gK0jSZFCXXcwqXXa-1440-900.png">
|
||||
|
||||
我们就可以将 Product 直接拖拽到画布区域,并选择矩形树图,通过点击指标上的 “+” “-” 号进行上卷或下钻:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1yud_cVT7gK0jSZFpXXaTkpXa-1440-900.png">
|
||||
|
||||
上卷下钻是顺序相关的,比如 Product - Order Date 表示在产品类目基础上,对每个类目按日期下钻。而 Order Date - Product 这个顺序,表示在日期分布的基础上,对日期按产品类目下钻,了解不同日期下每个产品的分布情况。
|
||||
|
||||
### 趋势线
|
||||
|
||||
为使用趋势线,先制作一个双轴图:
|
||||
|
||||
1. 将 Sales 与 Profit 拖拽到 Rows。
|
||||
2. 将 Order Date 拖拽到 Columns 并切换到月维度。
|
||||
3. 选择 Show Me 的 Dual Combination 即混合图。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1YOeacV67gK0jSZPfXXahhFXa-1440-900.png">
|
||||
|
||||
点击 Analytics Tab,将 Trend Line 拖入 chart 中:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1HL5gc.Y1gK0jSZFCXXcwqXXa-1440-900.png">
|
||||
|
||||
趋势图有几种算法,比如线性,Log 或指数,因此在做趋势分析前,首先要判断自己的业务属于哪种增长阶段,如果是爆发期可以选择指数,平稳期可以选择线性等等。
|
||||
|
||||
### 预测
|
||||
|
||||
回到按月分布的图表,如果我们想预测未来销量和利润的走势,可以使用预测功能:
|
||||
|
||||
1. 切换到 Analytics Tab,并将 Forecast 拖拽到图表中。
|
||||
2. 可以点击右键配置预测参数。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1Cumcc4D1gK0jSZFsXXbldVXa-1440-900.png">
|
||||
|
||||
预测趋势有一个浅色区域,表示预测范围。
|
||||
|
||||
### 聚类
|
||||
|
||||
象限图的四象限是多维度综合判断的法则,然而 Tableau 支持的聚类分析可以自动做到这些:
|
||||
|
||||
1. 切换到 Analytics Tab,选择 Clusters。
|
||||
2. 可以选择自动聚类个数,也可以手动指定个数。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1BsWgc1P2gK0jSZFoXXauIVXa-1440-900.png">
|
||||
|
||||
从上图可以看到,指定了 4 个分类,最右上角加州就是最突出的一组,整个聚类只有它一个元素,而画面偏左下角的也是一类,这些是业绩较差的一组数据。使用了 K 均值聚类算法,并且当你点击右键查看详细星系时,还能把组间、组内方差展示出来:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1O7Oec1H2gK0jSZFEXXcqMpXa-1440-900.png">
|
||||
|
||||
## 仪表板
|
||||
|
||||
仪表板可以将多个 Sheets 内容聚合在一起并自由布局,但仪表板最精髓的功能是图表联动功能:
|
||||
|
||||
1. 点击任意图表,选择 “作为筛选条件”。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1cVjIcebviK0jSZFNXXaApXXa-1440-900.png">
|
||||
|
||||
Tableau 的所有图表都支持点选,排除等操作,那么点选这类操作本质上其实是个筛选的过程,比如柱状图点击了某根柱子,可以认为是选择了这根柱子当前的维度值作为筛选条件。
|
||||
|
||||
当一个 Sheet 作为筛选条件后,类似点选这种操作产生的筛选就会作用于其他同数据集的图表,因此如上图所示,当点击了条形图的某一根柱子时,上面的销量地图也自动做了筛选,仅展示当前选中的产品的销量分布。
|
||||
|
||||
## 故事
|
||||
|
||||
Story 更像是 PPT,将分析后有价值或有意义的图表组合在一起,再配合上说明,得出一些结论:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1wa1gc1L2gK0jSZFmXXc7iXXa-1018-870.png">
|
||||
|
||||
如上图所示,比如得到这家超市的大盘数据,这一般也是数据分析的最后一步,最后生成报表。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
Tableau 的交互式分析思路印证了这句话:
|
||||
|
||||
数字、信息,再到理解最终才能产生 Idea。我们从拿到 Excel 导入数据集开始,数据就已经变成了维度和度量的信息,再经过主动思考,将同一份数据进行不同维度的展示,最终得出加州销量最好、家具销售业绩最差、而桌子是负利润的主要来源等等洞见。
|
||||
|
||||
通过原文对 Tablueau 功能的分析能看到,Tableau 的核心资产是具备交互式分析能力的图表,这些图表通过智能推荐的方式展示出来,可以在不知道如何分析数据时找到一些灵感,真正做到以数据角度思考,图表展示只是辅助的视觉效果。
|
||||
|
||||
目前国内还处于报表制作的时代,即先选择报表再配数据集,这种使用思路是展示数据优先,而不是分析数据优先,笔者认为原因在于国内大部分做报表的业务场景都处于最末端,也就是数据洞见已经有了,再使用 BI 将这个洞见还原出来。而 BI 工具真正想做的还是在前面 “分析洞见” 这一步,希望数据分析师能可以通过 BI 平台挖掘出商业洞见。
|
||||
|
||||
要走到这一步,需要国内 BI 平台与使用 BI 的人都发展到下一阶段,而这种探索式数据分析功能早在 2012 年就在国外由 Tableau 团队实现,相信未来三年内国内一定能迎来一波探索式数据分析浪潮!
|
||||
|
||||
> 讨论地址是:[精读《Tableau 入门》 · Issue #192 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/192)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,93 @@
|
||||
# 1. 引言
|
||||
|
||||
微软的市值已经突破一万亿美元了,我们很难想象当年僵化而封闭的微软是怎么涅槃重生的。从仅支持自家 Windows 到收购 Github、从失去移动操作系统市场到与 AWS 平分云服务市场、从 Windows 收费升级到 Win10 限时免费升级、从咒骂 Linux 是癌症到大部分云服务都跑在 Linux 操作系统上、从反垄断、数据隐私被诉讼大户,到 Facebook Google 被监管部门调查时却可以置身事外,微软一定从内部发生了彻底的变革。
|
||||
|
||||
新微软的变革经验值得我们学习,[《刷新》](https://www.baidu.com/link?url=tVqSngscNfJokBYi3Jwg66SDAJy8Sn5Y4nChDn1gJRAdsrbNYqXSQl43UyyryLtsNhlf4e0UjUfseUAUpPTuMK&wd=&eqid=c6d6c373000d8ee4000000045d58efd4) 就是一本介绍这场变革的书,它的作者是领导这场变革的现任微软 CEO 萨提亚·纳德拉。
|
||||
|
||||
这本书的关键词是:**同理心、文化变革、成长型思维**。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
本书围绕家庭与事业两个层面展开,从家庭中得到的领悟帮助作者更好的工作。
|
||||
|
||||
## 萨提亚的家庭
|
||||
|
||||
作者萨提亚·纳德拉的母亲是一名教师,父亲是一个勤奋的印度高级官员,然而他的成长环境相当宽松,**使作者从小就懂得独立思考并按照自己的意愿做事**。母亲难以兼顾事业与家庭而选择放弃工作,**让作者体会到女性工作的不公平**,父亲**追求上进心态与行为帮助作者得到更多职业发展机会**。而孩子扎因天生的重度大脑性瘫痪**使作者学着站在孩子的角度思考问题,学会真正理解同理心。**
|
||||
|
||||
作者爱好的运动是板球,这是印度最受欢迎的运动。这项运动带给作者的除了热血沸腾之外,还有对团队合作的理解:**好的领导不仅自己能力要出色,还要能帮助队员提升信心,发挥队员的潜力**,而专业技能优秀的球员,如果不能进行良好的团队合作,最坏的情况甚至会损害团队整体利益。
|
||||
|
||||
## 微软面对怎样的危机
|
||||
|
||||
危机往往是多个维度体现的,且相辅相成。微软面临的两大主要危机分别是 **员工失去信心** 与 **业绩下滑**,员工失去信心是内因,引发了业绩下滑的外因。
|
||||
|
||||
在最糟糕的时候,微软内部帮派林立,各部门负责人只想巩固自己的地盘,这让微软失去了创新领域竞争的机会。科技行业的业务趋势总是处于 **三浪叠加状态**:
|
||||
|
||||
- **旧的领域业绩已经在下滑**,但基数大,往往也是公司发家的根基,对部门负责人自己来说,再吃几年老本对自己的利益最大,但这终将导致公司走向失败。
|
||||
- **当前领域增长已经逐渐放慢**,但未来仍有很大增长空间,这些业务被寄予了厚望。
|
||||
- **新的领域尚不清晰**,但一旦探索到正确的方向,增长速度甚至会年年翻番,这些业务会在未来几年内成为公司的收入支柱。
|
||||
|
||||
微软的个人计算机 Windows 操作系统太过成功,使微软在移动端浪潮下没能将足够的资源投入到移动端业务中,真正的创新部门被边缘化,旧领域部门掌握着绝对话语权,如果 CEO 不能作出改变,公司将走向不可逆的衰亡。
|
||||
|
||||
业务上,微软也在这三个主要方向全面落后:
|
||||
|
||||
- **操作系统领域**:微软个人计算机出货量和财务增长已陷入停滞,而苹果、谷歌的智能手机和平板电脑销量正在上升。
|
||||
- **搜索领域**:谷歌的搜索和在线广告收入也在持续增长,而微软的搜索技术才刚起步,市场份额只有竞争对手的零头。
|
||||
- **云技术领域**:亚马逊推出的 AWS 已经在市场建立起领导地位,微软由于 Windows 原因,不愿意接受云计费模式,还在固守一次性买卖思维,甚至连云产品都没有。
|
||||
|
||||
## 微软是如何转型的
|
||||
|
||||
站在首席执行官视角,转型一定是从文化转型开始的,只有转变了企业文化,才能充分激发每一个人的潜力,使公司朝着正确方向发展。作者在成为微软 CEO 后,在文化上作出的改变主要分为三点:
|
||||
|
||||
- **找到微软公司的新使命**。显然,让每个人都拥有一台电脑这个目标已经达成了,为了推动微软继续前进,作者将新的目标设定为:赋能大众,通过做平台、工具,来提升全社会各组织、团体的工作效率、医疗效率、组织效率等等。
|
||||
- **建立耳目一新、出人意料的伙伴关系**。不论是 Linux 、苹果公司还是亚马逊,一方面是强劲竞争对手,但另一些领域也有合作的价值,比如将微软办公套件通过 IOS 平台普惠到大众这种部分领域合作的心态是不可或缺的。微软封闭的文化也在这一点上真正转向了开放,独占的思维模式如果走不通,合作能带来更多的机会。
|
||||
- **同理心**。微软高级副总裁沈向洋在 2019 年极客大会的分享也提到了这一点,微软通过制造辅助设备帮助帕金森患者正常完成写字、绘画。从广义上说,微软正式通过同理心,站在用户角度思考,才领悟到如何才能真正的帮助用户,比如一位安卓用户需要在手机查看 Word 文档,那么让 Word 支持安卓平台,推出基于云平台的 Office 365 就是一个自然的行为。
|
||||
|
||||
在文化转型的推动下,微软在业务上也进行了一系列积极的调整:
|
||||
|
||||
- **将云业务放到核心位置**。这一点和阿里的云战略转型很像。云业务一开始都不怎么赚钱,需要大量资金和人才投入,在数年后才能看到回报,微软最大的问题是如何打破公司内资源分配不均匀的问题。通过一系列人事调整与战略制定,微软的云业务走上了正规,现在已经与 AWS 平分市场份额。
|
||||
- **在可能的领域与竞争对手达成合作**。除了推出 IOS 平台的 Office 套件外,必应还成为了雅虎搜索的搜索引擎,微软甚至放弃了排他性条款,允许雅虎同时使用其他公司的搜索引擎服务,即便如此,必应引擎现在仍驱动着大部分雅虎搜索功能,而良好的开放心态也加速必应搜索引擎能力的迭代。
|
||||
- **推动部门之间员工的协作**。随着文化变革,微软内部部门孤岛的情况有了好转,从不接收其他部门意见的 Windows 研发部门开始采纳其他部门员工提出的建议。笔者了解到 Facebook 的大部分源码每个员工都有充分权限参与修改,维护一个系统不只是相应业务线员工的特权,来自其他部门的创意往往更优秀。
|
||||
|
||||
## 三条领导原则
|
||||
|
||||
无论是推动文化变革,还是推动业务增长,都需要高级、中层管理人员的实施,作者给出了三点领导原则:
|
||||
|
||||
- **向共事的人传递明确信息**。传达信息是领导者每天都在做的事情,领导者应该把信息交流重点放在事情上,而不是人上,也就是关注如何把事情做好,而不是讨论谁更聪明。
|
||||
- **领导者要产生能量,不仅在自己团队中,还要在整个公司中**。领导者身处在多个圈子中,有自己管理的团队的圈子,也有来自上级组织的圈子,有来自公司级横向委员会的圈子,也有核心管理层的圈子,作者站在 CEO 的角度,要求领导者要将最高一层圈子放在首要地位,也就是整体利益大于局部利益。
|
||||
- **找到取得成功和让事情发生的方式**。也就是正确的做事,懂得平衡长期利益与短期利益,不走极端;让团队成员找到自己热爱的工作方式;能跨越边界,全球化思维。
|
||||
|
||||
## 其它
|
||||
|
||||
本文要突出的介绍的内容已经结束,本书还有最后几个部分笔者简要带过:
|
||||
|
||||
**三大变革:**
|
||||
|
||||
作者提出未来可能由技术引领行业变革的三个方向:混合现实、人工智能和量子计算。这就是跨越边界的思维方式,微软积极布局的这三个前沿领域,对准的是未来的 “第三浪”。
|
||||
|
||||
**隐私、安全和言论自由:**
|
||||
|
||||
捍卫隐私、安全与言论自由也是微软转型的重要内容,微软通过积极与监管部门合作,通过实际行动捍卫言论自由,使得微软从政府监管对象逐渐转变为监管原则的捍卫者,这也是近年来科技巨头纷纷作出一个改变。
|
||||
|
||||
**人与机器的关系:**
|
||||
|
||||
不要把机器与人想成竞争关系,要理解为机器辅助人类的关系。同时机器也是释放人类创造力的最重要方式,虽然在变革前期会导致大量失业,但消失的旧行业都是重复性高的,创造出来的新行业更能激发人类的创造力。有一句话笔者印象最深刻:机器替代人类工作的过程,也是人类逐渐拾回作为人的尊严的过程。人本就应该将时间用于思考与创造,而不是重复性劳动。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
引导微软一系列变革的源泉可以认为是 “同理心”,因为同理心可以练就开放的性格,指引正确的方向。微软 CEO 萨提亚从家庭与生活中养成了同理心,并将其运用在公司的变革上,最终让微软每一位员工都能换位思考,利用同理心做正确的事,这种思想的传导是最难的一步,作者做到了。
|
||||
|
||||
对于我们的思考是,无论是公司的管理者,还是基层员工,都应该培养自己的同理心,因为有同理心的人不仅能更好的工作,在生活中也能更融洽的与人相处。
|
||||
|
||||
在工作中,同理心也是突破职业天花板的能力之一,想要提升为客户带来的价值,首先要接触并理解客户,站在客户视角思考问题,在面临内部矛盾或外部竞争时,仍能坚守为客户创造价值的目标,下一步改革的方向就会变得清晰,矛盾会逐渐化解,竞争也不会是一个问题,用户想要的不是竞争,而是被赋能,持有这种心态做事,与竞争对手合作就是利益最大化的选择了。
|
||||
|
||||
微软的首席执行官萨提亚正因为抱有同理心,才能作出超越竞争、封闭的决策,这对还没能掌握这一心智的公司来说,是种降维打击。一个用一切手段赋能用户、在核心能力不惧竞争(云计算)、在可合作领域充分合作的公司是极其强大的。
|
||||
|
||||
> 讨论地址是:[精读《刷新》 · Issue #196 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/196)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,541 @@
|
||||
# 1. 引言
|
||||
|
||||
Tableau 探索式分析功能非常强大,各种功能组合似乎有着无限的可能性。
|
||||
|
||||
今天笔者会分析这种探索式模型解题思路,一起看看这种探索式分析功能是如何做到的。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
要掌握探索式分析,先要掌握探索式分析背后的思维模型。
|
||||
|
||||
## 理解数据
|
||||
|
||||
有分析意义的数据一般是表结构,即分为行与列,列定义了数据含义,行则构成了数据明细。
|
||||
|
||||
当我们将数据作为 “原材料” 使用时,需要将这些明细数据封装为 “数据集” 的概念来理解,数据集概念中,数据就是一个个字段,对于字段,要理解 “维度” 与 “度量” 这两个概念。
|
||||
|
||||
### 维度
|
||||
|
||||
维度是不能被计数的字段,一般为字符串或离散的值,用来描述数据的维度。
|
||||
|
||||
### 度量
|
||||
|
||||
度量是可以被计数的字段,一般为数字、日期等连续的值,用来描述数据的量。
|
||||
|
||||
<img width=172 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566632483137-9e0268d9-f890-45e6-a3e5-805355b35af9.png#align=left&display=inline&height=464&name=image.png&originHeight=1096&originWidth=406&size=83329&status=done&width=172">
|
||||
|
||||
我们首先要将数据集字段归类到维度与度量,才能提高数据分析的效率。**数据分析就是从不同维度下看度量值**,先想清楚要看的是什么数据,比如销量还是利润?这些字段都属于度量,然后想一想要怎么看这些度量,是看总数、拆解到年看、还是按地区看呢?这些字段都属于维度。
|
||||
|
||||
**维度和度量是可以单独看的,如果单看维度,那只能看这个维度的明细,比如看 订单日期 这个字段**:
|
||||
|
||||
<img width=190 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633158647-cba541bd-673c-498c-95e6-53c89346c458.png#align=left&display=inline&height=149&name=image.png&originHeight=298&originWidth=380&size=24178&status=done&width=190">
|
||||
|
||||
需要注意的时,维度与度量字段还可以分为 **连续** 与 **离散** 。
|
||||
|
||||
### 连续
|
||||
|
||||
值是连续关系,即任意两个值之间可以计算差值。
|
||||
|
||||
### 离散
|
||||
|
||||
值是离散关系,即任意两个值之间无法计算差值,无法以连续的方式去理解。
|
||||
|
||||
**一般来说,维度字段都是离散的,度量字段都是连续的。**从字段类型意义上也能得出相同的结论:维度字段一般为字符串或日期类型,字符串类型都是离散的,度量字段一般为数字类型,数字天生就可以连续。
|
||||
|
||||
值得注意的是,连续与离散其实与字段类型、维度度量并无关系,比如维度的日期字段就是可连续的,而就算是字符串类型,也可以以字符串长度等方式 “定义” 一种连续的计算方式。对数字类型的度量字段来说,我们也可以忽略数字之间的联系,将数字看待为字符串,这样数字之间就是离散的。
|
||||
|
||||
**上图的 “离散方式看日期” 就是看维度的直观方式,但仍可以用 “连续方式看日期”:**
|
||||
|
||||
<img width=309 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633194083-b17c1c2c-7023-47cd-a94c-48d57fd37217.png#align=left&display=inline&height=308&name=image.png&originHeight=616&originWidth=618&size=37644&status=done&width=309">
|
||||
|
||||
离散方式下单看维度只有一条条数据,数据间并无排序规则,而以连续方式看维度,维度就会以某种方式排序:比如上图以时间类型进行排序。此时展示方式也从表格切换为了柱状图,因为表格适合展示离散数据,柱状图的一根柱子就可以展示连续数据。
|
||||
|
||||
单看度量时,由于 **度量要依附于维度展示**,因此仅有度量时,只能看这个度量的 **聚合** 概念:
|
||||
|
||||
<img width=200 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633468748-6ffb22b0-8c8d-4f6c-bdb6-a16616e29e70.png#align=left&display=inline&height=107&name=image.png&originHeight=214&originWidth=400&size=15238&status=done&width=200">
|
||||
|
||||
如上图所示,单看销量这个度量字段时,我们只能将数据集中所有销量字段聚合在一起来看,**但这种聚合方式也可以分成若干种计算类型 - 求和、平均值、中位数、计数、计数去重、最小值、最大值、方差等等:**
|
||||
|
||||
<img width=414 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633611811-f0bef366-b9ca-47dd-adbc-4e2d311f52de.png#align=left&display=inline&height=502&name=image.png&originHeight=1004&originWidth=828&size=228956&status=done&width=414">
|
||||
|
||||
这些能力之间都是 “正交” 的,即单看度量这一个字段,可以以这么多种类型进行计算,那么按维度拆分后,度量依然可以享受如上不同的计算方式。
|
||||
|
||||
**也可以用连续方式看度量:**
|
||||
|
||||
<img width=184 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633797820-b7980691-1be7-4644-a321-cac98cc36e5f.png#align=left&display=inline&height=304&name=image.png&originHeight=608&originWidth=368&size=23542&status=done&width=184">
|
||||
|
||||
与连续-维度不同,连续-度量图形中除了最后一个值,其他过渡数值都是无效的,因为连续-度量只有一个值。连续-维度也要注意,由于以连续的方式画出图形,中间不存在的点也被 “无缝连接” 了。
|
||||
|
||||
数据之间也可以存在父子级关系,有父子级关系就可以进行上卷下钻了,这种父子级关系被称为 “层系字段”:
|
||||
|
||||
<img width=209 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566634402929-9e23bff4-4810-4827-bf3b-c6af9ef617ea.png#align=left&display=inline&height=217&name=image.png&originHeight=434&originWidth=418&size=33356&status=done&width=209">
|
||||
|
||||
上图的 Orders 就是一个层系字段。层系字段是几个字段的排序组合,**由上到下依次构成下钻关系,从下到上则是上卷的关系。**
|
||||
|
||||
### 层系
|
||||
|
||||
**只有维度字段才能有层系,**因为度量是不能被拆分的,只有维度才可以被拆分。
|
||||
|
||||
维度的拆分可以是有逻辑含义的,也可以是任意的。
|
||||
|
||||
**有逻辑含义的层系**
|
||||
|
||||
最典型有逻辑含义的层系字段就是时间了。一个好的 BI 系统识别到日期字段后,应该将拿到的日期字段进行归类,比如判断日期字段粒度到天,则自动生成一个日期层系字段,自动聚合到年,并允许用户随意切换:
|
||||
|
||||
<img width=277 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566634723539-8f80cdd3-f8af-41a2-9e89-db94ecfa3200.png#align=left&display=inline&height=161&name=image.png&originHeight=322&originWidth=554&size=51469&status=done&width=277">
|
||||
|
||||
如果数据集字段值精确到月,则层系只能最多展开到月。
|
||||
|
||||
日期层系的逻辑含义在于,年、季度、月、天这种下钻关系是天然从大到小的关系,符合自然理解。
|
||||
|
||||
**任意层系**
|
||||
|
||||
如果层系字段不代表日期,就只能以业务含义组合层系字段了。**比如可以将层系按照 订单日期 -> 商品 ID -> 运货日期的方式组合:**
|
||||
|
||||
<img width=624 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566634964577-6dad1f7c-b01b-419a-8b7c-6e0832d6c8b6.png#align=left&display=inline&height=132&name=image.png&originHeight=308&originWidth=1454&size=41917&status=done&width=624">
|
||||
|
||||
这种下钻方式,可以看到每个订单日期下有哪些商品,每个商品分别运货日期是什么。
|
||||
|
||||
**也可以按照商品 ID 拆分出不同的订单日期与运货日期,这种层系组合方式就是以商品 ID 为主要视角:**
|
||||
|
||||
<img width=622 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566635114693-57a2b260-d7cc-4945-9050-361ce4608e09.png#align=left&display=inline&height=129&name=image.png&originHeight=302&originWidth=1456&size=44825&status=done&width=622">
|
||||
|
||||
可以看到,不同思维角度会按照不同的方式组合层系。比如一家大公司要查看财务问题,维度有:BU、日期,度量有:销量。
|
||||
|
||||
那么有两种下钻方式:BU -> 日期、日期 -> BU。无论哪种下钻方式,都能看到每个 BU 按日期销量的明细,但 BU -> 日期 能看到每个 BU 按日期聚合的总销量,而 日期 -> BU 能看到不同日期按 BU 聚合的总销量,前者更易对比出 BU 之间差异,后者更易对比出日期之间的差异。
|
||||
|
||||
## 理解配置
|
||||
|
||||
配置是探索式分析的入口,要理解分析模型首先得理解配置模型。
|
||||
|
||||
Table 主要配置分为行、列、标记与筛选。通过这四个配置区域可以组合成千变万化的数据洞察模型。既然如此,让我们看看这种配置思路是什么,以及为何这四种配置相互组合就能覆盖整个探索式分析场景?
|
||||
|
||||
我们不需要考虑三维数据分析场景,因为三维透视的关系,图形丢失了精确大小关系,没有精度的数据是没有分析价值的。由于在二位平面中分析数据,**大部分图表都可以用 “行、列” 方式进行配置**。
|
||||
|
||||
**也许有人会问,为什么不用维度与度量替代行列呢**?这是一个很好的问题,有数据分析经验的人会站在维度与度量角度思考问题,因此对于任意图表,只要配置维度、度量即可呀?笔者从三个方面说说自己的理解:
|
||||
|
||||
1. 探索式分析思路中,不关心图表是什么,也不关心图表如何展示,因此图表是千变万化的,比如折线图可以横过来,条形图也可以变成柱状图,因此 **你将维度放到列,就是一个柱状图,你将维度放到行,就是一个条形图** 。
|
||||
2. 将精力真正放到你要拖拽的字段上。由于字段已经有维度、度量的区别,配置区域就不要再限定维度与度量了,减少理解成本。
|
||||
3. 维度与度量可以同时放在行或列上,这是探索式分析的另一个精髓能力,看下图:
|
||||
|
||||
<img width=306 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566636365926-c5d37423-0e32-4382-ae7d-2b9760caeb97.png#align=left&display=inline&height=435&name=image.png&originHeight=1240&originWidth=872&size=94620&status=done&width=306">
|
||||
|
||||
做探索式分析功能时,要跳出思维定式:**为什么条形图的纵轴不能放维度呢?**如上图所示,如果行拖拽了两个不同的度量,那么可以出现两条线或者双轴图,但当拖拽一个维度一个度量时,可以对图表进行 **分面** ,比如观察 2013 ~ 2016 年不同顾客对销量的贡献。
|
||||
|
||||
### 行
|
||||
|
||||
表格类的行、图表类的纵轴。一般建议放置度量字段。
|
||||
|
||||
### 列
|
||||
|
||||
表格类的列、图表类的横轴。一般建议放置维度字段。
|
||||
|
||||
如上所示,无论行还是列,都可以进行任意维度度量组合,且字段数量不限,而且可以在任何层级进行下钻。**对图表来说,多个维度时需要进行分面处理:**
|
||||
|
||||
<img width=476 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566637370597-d52b7677-9ec4-40f3-aa54-a0382ac56de1.png#align=left&display=inline&height=428&name=image.png&originHeight=1448&originWidth=1612&size=106528&status=done&width=476">
|
||||
|
||||
如上图所示,将列放置两个维度字段成为柱状图,那么横轴就要同时表示两个维度,如上图所示。如果横轴还有更多的维度,可以再不断对横轴进行拆分。
|
||||
|
||||
横轴(列)多维度字段的顺序也会影响图表的展现。**上图最后一个字段是 Category 默认是离散的,所以这个离值就决定了图表使用柱状图,图表类型由维度周最后一个字段连续或离散决定。**
|
||||
|
||||
比如我们对调 Order Date 与 Category 会怎样?
|
||||
|
||||
<img width=476 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566637657990-0387a37c-f4dc-45ca-a53a-aa10b4573318.png#align=left&display=inline&height=420&name=image.png&originHeight=1456&originWidth=1620&size=137950&status=done&width=467">
|
||||
|
||||
我们得到了三个不同类目近 12 个月的趋势,之所以是折线图,因为图表的维度轴(列)是连续的。**如果我们对 Order Date 进行天级别的下钻:**
|
||||
|
||||
<img width=462 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566637772126-a9825860-4851-41ae-a56d-df984ea49b4c.png#align=left&display=inline&height=328&name=image.png&originHeight=1462&originWidth=2062&size=128020&status=done&width=462">
|
||||
|
||||
可以看到,**下钻功能本质上就是维度轴支持对多个维度字段拆分处理。只要图表支持了维度轴任意维度字段的分面展示,那么配置端就可以将下钻按照拖了多个字段的方式去理解了。**
|
||||
|
||||
**如果我们将折线图切换为表格,会发生什么?**
|
||||
|
||||
<img width=578 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566637986444-1a78b31f-6bb6-4c3e-b69b-0bd5a32c399f.png#align=left&display=inline&height=211&name=image.png&originHeight=738&originWidth=2018&size=111516&status=done&width=578">
|
||||
|
||||
我们会发现,原本存在于列的 Category 被自动挪到了行,原本存在于行的 Sales 被挪到了 “标记” 区域。在正式介绍 “标记” 区域前,先理解一下为何会发生这种转变:
|
||||
|
||||
**表格类组件是双维度组件,折线图是单维度组件。** 也就是表格的行与列都是维度,而折线图横轴作为维度后,纵轴就要作为度量。上面的例子中,折线图维度有两个字段,虽然通过分面方式渲染出来了,但当切换为支持双维度的表格后, **可以将多余的一个维度挪到表格组件另一个维度区域中**。
|
||||
|
||||
而表格行与列都是维度的情况下,单元格的值就需要用 “标记” 中文本来表示,因此原折线图的度量字段自动转移到了 “标记” 区域。
|
||||
|
||||
### 标记
|
||||
|
||||
标记区域也采取字段拖拽的方式,即对字段进行标记。
|
||||
|
||||
标记区域分为 **颜色、大小、标签、详细信息、工具提示、路径。**标记正如其名,是作用于图表上的标记,**即不会对图表框架有实质性影响的辅助标记信息。**
|
||||
|
||||
对不同图表来说,影响最大的是行与列,它能决定用什么图表,如何拆分数据。而标记往往是改变图表中辅助性元素,比如文字或者颜色等等。
|
||||
|
||||
#### 工具提示
|
||||
|
||||
不影响任何图像显示,仅仅在提示信息中新增字段信息。
|
||||
|
||||
**对图表来说,指的是 Tooltip 提示信息增加对应的字段:**
|
||||
|
||||
<img width=424 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566655533184-4ea4560f-8fef-40aa-9910-c301842c53b4.png#align=left&display=inline&height=363&name=image.png&originHeight=1000&originWidth=1168&size=91870&status=done&width=424">
|
||||
|
||||
从上图可以看到,利润字段放在工具提示区域,则图表的 Tooltip 会新增利润这个字段的信息。**值得关注的是,Tableau 所有图表都支持 Tooltip 包括表格:**
|
||||
|
||||
<img width=623 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566655642537-e7a18aa6-f619-45c4-b2b3-f95274d40107.png#align=left&display=inline&height=157&name=image.png&originHeight=374&originWidth=1480&size=42938&status=done&width=623">
|
||||
|
||||
这保证了配置统一,行为统一。
|
||||
|
||||
#### 大小
|
||||
|
||||
控制图表大小。
|
||||
|
||||
对于线图,控制线的粗细;对于气泡图控制气泡大小;对于柱状图控制柱子粗细;但是对面积图与表格没有明显作用。这得益于 Tableau 将每个图表大小属性尽可能抽象出来。
|
||||
|
||||
<img width=360 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566655966495-9241a848-b8c0-48b7-b0a4-8fbcc44ff58c.png#align=left&display=inline&height=273&name=image.png&originHeight=748&originWidth=988&size=58386&status=done&width=360">
|
||||
|
||||
<img width=360 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566656245320-7f29da9d-363f-4fd3-95f3-15b0ec2a3522.png#align=left&display=inline&height=223&name=image.png&originHeight=982&originWidth=1602&size=99303&status=done&width=364">
|
||||
|
||||
#### 文本
|
||||
|
||||
即直接展示在图表上的文本。
|
||||
|
||||
对普通图表来说,文本体现为 Label,即直接展示在图表上的文字。比如柱状图默认是没有 Label 文字的,要将对应字段拖拽到文本标记上才会出现。
|
||||
|
||||
<img width=404 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566656520351-588602a5-db01-4e46-bfdd-6f771353d7a8.png#align=left&display=inline&height=379&name=image.png&originHeight=934&originWidth=996&size=77362&status=done&width=404">
|
||||
|
||||
这体现出与普通报表构思的不同。对普通报表来说,Label 是通过一个勾选项开启的,Label 对应的值就是图表度量这个字段的值。而 Tableau 将标签值以字段方式开放拖拽,就有了展示与值分开的可能性,可适用范围更广。
|
||||
|
||||
> 有人觉得长度和数字一定要对应上,这也是对数据理解不同导致的。Tableau 将文本(标签)列在标记里,说明文本和颜色、大小一样,都是一种附加的信息展示维度,很多时候不需要两种方式展示同一种信息,反而需要图形以更多方式以不同维度展示信息。
|
||||
|
||||
#### 颜色
|
||||
|
||||
控制图表的颜色。
|
||||
|
||||
比如在度量为销量时,可以将利润作为颜色,甚至再将折扣作为文本,通过一个折线图同时看多种度量信息:
|
||||
|
||||
<img width=386 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566656981186-8f2441d6-78d0-4d34-af7f-139b0f21cb30.png#align=left&display=inline&height=344&name=image.png&originHeight=888&originWidth=996&size=82343&status=done&width=386">
|
||||
|
||||
与之对比,我们可以将利润放在右 Y 轴作为双轴图达到相同的效果:
|
||||
|
||||
<img width=439 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566657023624-c01b0008-e7d7-4991-99c1-beddc8c72762.png#align=left&display=inline&height=319&name=image.png&originHeight=886&originWidth=1218&size=112190&status=done&width=439">
|
||||
|
||||
**标记就是为了在不增加行、列字段数量基础上,通过颜色、大小、标签、工具提示等维度展示出额外信息。**
|
||||
|
||||
#### 详细信息
|
||||
|
||||
如果将度量拖拽到详细信息,会发现完全没有作用。因为 “详细信息” 只有拖拽维度字段才生效。“详细信息” 其实是用作下钻的,拖拽一个维度字段后,可以按照这个维度进行下钻。
|
||||
|
||||
<img width=533 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566657906186-3348cdf7-823f-4111-9685-dbf5fb05c0f8.png#align=left&display=inline&height=376&name=image.png&originHeight=1054&originWidth=1496&size=102786&status=done&width=533">
|
||||
|
||||
如上图所示,将销售按照产品线拆解成三条线。但这三条线无法分辨,因此可以使用颜色来拆分维度:
|
||||
|
||||
<img width=533 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566657988073-e8e50316-feb2-4c7f-98dc-45f4a2ffd624.png#align=left&display=inline&height=372&name=image.png&originHeight=1052&originWidth=1510&size=112469&status=done&width=534">
|
||||
|
||||
这样就能将拆解的内容按不同颜色展示。因此, **对标记作用的字段如果是维度字段,且作用于颜色、大小、标签、详细信息时,会额外进行维度进行拆解,并对拆解后的内容进行颜色或大小区分。**
|
||||
|
||||
相信读到这里会有个疑问:按照维度进行拆解与维度拖拽多个字段进行字段有什么区别?我们试一下看看效果,将产品类目维度拖拽到销量所在的行,对销量进行销量维度的拆分:
|
||||
|
||||
<img width=570 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566658461645-fbcc1b14-0111-47b1-b643-d23f36f96ef2.png#align=left&display=inline&height=512&name=image.png&originHeight=1058&originWidth=1178&size=92443&status=done&width=570">
|
||||
|
||||
**可以看到,在行、列进行的多维度拆分使用的是分面策略,而在标记中对维度进行拆分使用的是单图表多轴方式来实现。**
|
||||
|
||||
除此之外的区别在于,在标记进行的维度拆分默认作用于度量,而行列上的多维度拆分可以任意作用于维度或度量。
|
||||
|
||||
> 同时配置端要限制 **能拆分的只有维度或离散状态的度量** ,也就是只有离散状态的字段可以被拆分。如上图所示,我们不能将 Category 拖拽到 Sales 右侧,除非将 Sales 设置为离散类型。
|
||||
> Tips:Tables 对维度与度量分别分配了蓝色、绿色,当我们将绿色度量字段设置为离散类型时,这个度量字段会变成蓝色,也就是当作了维度字段进行处理。
|
||||
|
||||
最后,标记区域不仅能拖拽字段,还可以单击后修改详细配置,比如修改颜色详细配置:
|
||||
|
||||
<img width=220 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566658916573-5c989763-bd8f-4f2d-8f57-9f52b9c39b20.png#align=left&display=inline&height=448&name=image.png&originHeight=896&originWidth=442&size=39310&status=done&width=221">
|
||||
|
||||
或者对工具提示的 Tooltip 内容进行定制:
|
||||
|
||||
<img width=557 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566658941101-e4eac6aa-d4f1-4397-9be2-ccc424211f00.png#align=left&display=inline&height=319&name=image.png&originHeight=910&originWidth=1590&size=207889&status=done&width=557">
|
||||
|
||||
### 筛选器
|
||||
|
||||
Tableau 将所有筛选条件都收敛到筛选器中,我们可以通过拖拽字段的方式对某个字段进行筛选:
|
||||
|
||||
<img width=635 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659069046-3e4f195e-1c9d-492d-bfe7-6f6d89997692.png#align=left&display=inline&height=210&name=image.png&originHeight=494&originWidth=1494&size=138806&status=done&width=635">
|
||||
|
||||
如上图所示,比如只看办公用品与科技产品。但其实除了这个通用功能之外,Tableau 还支持更强大的图表交互功能,即点击或圈选图表后,可以对选中的点(字段值)进行保留或排除:
|
||||
|
||||
<img width=606 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659178209-134b5fc5-b067-4481-bf99-ed36c8c458c7.png#align=left&display=inline&height=198&name=image.png&originHeight=442&originWidth=1350&size=55242&status=done&width=606">
|
||||
|
||||
**当我们选择排除这几个点时,会自动生成一份对维度字段的筛选条件排除掉选中日期,所以图表是完全数据驱动的:** 一般来说
|
||||
|
||||
<img wdith=576 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659259441-22ef9da8-e3bc-4cef-b32e-c538c1dfe3e0.png#align=left&display=inline&height=268&name=image.png&originHeight=696&originWidth=1494&size=189252&status=done&width=576">
|
||||
|
||||
如果属性存在下钻关系会如何呢?无论是行列中对维度的下钻,还是通过标记对维度进行了拆解,筛选都是对 **字段层系** 生效的:
|
||||
|
||||
<img width=575 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659420072-3e6e41f4-bc99-4d7d-a93f-553abe429a94.png#align=left&display=inline&height=169&name=image.png&originHeight=442&originWidth=1504&size=155974&status=done&width=575">
|
||||
|
||||
如上图所示,对下钻后的字段进行筛选,**那么筛选条件也会自动构造出临时的字段层系,并对这个临时层系进行筛选。** 可以看到,我们不仅能在字段配置区动态组成层系字段,在筛选器中也可以生成临时层系进行筛选,我们需要支持任意层系组合的字段,并作用于筛选器、行列,甚至是标记上。
|
||||
|
||||
顺带一提,我们还可以对设置了筛选的字段层系组合拖拽到任意地方使用:
|
||||
|
||||
<img width=393 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659649713-3428813a-d156-4c80-935f-e40de333a8fe.png#align=left&display=inline&height=434&name=image.png&originHeight=1050&originWidth=950&size=88795&status=done&width=393">
|
||||
|
||||
要处理这种场景,**我们需要让所有字段都拥有筛选能力**,普通字段等于没有筛选条件,我们也可以对一个包含了筛选条件的字段拖拽到任何位置作用。
|
||||
|
||||
刚才是对维度进行的筛选,有没有对度量进行筛选的场景呢?有,但我们只能手动将度量字段拖拽到筛选器位置进行手动筛选:
|
||||
|
||||
<img width=613 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659919516-77548c29-7c02-4857-9eec-fb1465826f9a.png#align=left&display=inline&height=312&name=image.png&originHeight=832&originWidth=1636&size=86512&status=done&width=613">
|
||||
|
||||
如果我们进行图表内的圈选操作,增加的筛选条件一定是按维度来的:
|
||||
|
||||
<img width=613 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659955376-66830503-bce1-443d-ab98-9fbc90e54fdb.png#align=left&display=inline&height=273&name=image.png&originHeight=756&originWidth=1694&size=80004&status=done&width=612">
|
||||
|
||||
这么理解这一行为:维度是离散的,勾选操作能表达的含义有限,比如勾选折线图的某些点,如何知道我们要勾选的是维度的那几个月,还是度量的利润范围呢?
|
||||
|
||||
<img width=613 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566660078571-ecf7c466-44c1-46a5-9ef0-d3692bf22fb8.png#align=left&display=inline&height=555&name=image.png&originHeight=1468&originWidth=1620&size=113222&status=done&width=613">
|
||||
|
||||
**由于最终勾选操作落地在点上,而不是区间上(连续值也不适合进行圈选),所以默认按对维度进行筛选是最准确的理解。**如果上图的操作意图中,你想勾选的不是 6~12 月的区间,而是销量在 13k ~ 45.5k,则需要手动拖拽利润字段,并精确输入筛选范围:
|
||||
|
||||
<img width=482 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566660226577-102ca29f-b6ef-43a7-860a-041b04274da8.png#align=left&display=inline&height=353&name=image.png&originHeight=774&originWidth=1056&size=81734&status=done&width=482">
|
||||
|
||||
值得注意的是,对连续型度量进行筛选前,还可选择聚合方式:比如对求和的值进行范围筛选,或者对最大值进行范围筛选,功能十分强大。
|
||||
|
||||
## 理解图表
|
||||
|
||||
图表是数据可视化的载体,只有数据与配置,没有各式各样的图表,很难产生直观的数据洞察。
|
||||
|
||||
可以说, **按照探索式分析的思路,当配置好数据与配置后,可以有多种可视化载体去展示这种配置信息。** 比如行、列分别拖拽了日期与销量,那么折线图、表格、散点图、柱状图都可以满足需求,但如果行所在的字段是离散的,那么折线图、散点图就不适合了,这就需要图表推荐功能根据配置推荐合适的图形展示。
|
||||
|
||||
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 分组且列合并。** 下钻就是一步步接近明细数据的过程,但目的不是为了看明细表,而是看某些维度下按其他维度拆分的详细信息。
|
||||
|
||||
图表下钻和表格思路是一致的:
|
||||
|
||||
<img width=529 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566699822756-b087948b-3104-4bd4-a76b-1cb2067edd65.png#align=left&display=inline&height=335&name=image.png&originHeight=1338&originWidth=2114&size=109757&status=done&width=529">
|
||||
|
||||
对于维度轴多维度下钻,将每个维度轴下钻到更细粒度。图表在行与列同时下钻时,与表格的表现稍有不同。仅从轴来看拆解方式是相同的,内部展示了多套轴:
|
||||
|
||||
<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">
|
||||
|
||||
如果继续对行列添加维度进行下钻,其实是对轴进行下钻。**排除度量字段不看,就是一个交叉表的下钻过程,如下图所示蓝色框圈住的部分就是一组大的单元格**:
|
||||
|
||||
<img width=629 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566700520147-c644eae8-8c04-4c7d-82aa-b5e4e1c2e4d8.png#align=left&display=inline&height=467&name=image.png&originHeight=1340&originWidth=1806&size=163645&status=done&width=629">
|
||||
|
||||
由于最后一个字段是度量,因此在叶子结点的展开就不是表格模式的单元格,而是连续的线条了。
|
||||
|
||||
经过上面的总结,我们要意识到,在探索式分析场景对行列的下钻,表格与图表的逻辑是通用的,实现时也要整体考虑。**将轴功能抽离成通用部分来做,表格与图表的区别只是对最后一个字段单元格是离散处理还是连续处理。**
|
||||
|
||||
#### 层系的下钻
|
||||
|
||||
<img width=528 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566699587535-dd5e11f6-6fd2-42ce-be2b-e01d7dee650a.png#align=left&display=inline&height=300&name=image.png&originHeight=818&originWidth=1438&size=102881&status=done&width=528">
|
||||
|
||||
层系字段下钻与拖多个字段表现一致,但由于存在父子关系,因此在图表上可以展现出 “展开” “收起” 按钮,点击后并不是对图表本身进行操作,而是发送一个事件对 “行” 进行操作,最后通过数据驱动完成展开或收起动作。
|
||||
|
||||
#### 不适合行列的图表
|
||||
|
||||
饼图就不适合行列,因为饼图是根据离散维度进行拆分,扇叶大小可以由一个度量字段决定,因此对饼图来说,行就对应到 “颜色”、列就对应到新增的 “角度” 这个标记:
|
||||
|
||||
<img width=336 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566704242607-bab5e722-d27c-4ad5-8518-b470fddbf9da.png#align=left&display=inline&height=279&name=image.png&originHeight=558&originWidth=672&size=50344&status=done&width=336">
|
||||
|
||||
#### 没有维度轴的图表
|
||||
|
||||
只有行配置的图形推荐用表格,但柱状图、折线图也可以支持这种情况,只要把横轴忽略即可:
|
||||
|
||||
<img width=300 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706245345-61b365db-793a-4d15-9677-b4e332381bda.png#align=left&display=inline&height=312&name=image.png&originHeight=912&originWidth=878&size=53419&status=done&width=300">
|
||||
|
||||
从样式上来看没有横轴,其实这种情况是把所有维度的横轴都聚合后的表现。
|
||||
|
||||
### 连续与离散值
|
||||
|
||||
我们分别看看连续与离散作用于维度和度量时的区别。
|
||||
|
||||
#### 作用于度量
|
||||
|
||||
图表要能适配对连续或离散值的处理。比如对销量来说,如果切换为离散值,则当成字符串展示:
|
||||
|
||||
<img width=632 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566662092893-32545363-82da-498a-868f-18a59bb41c24.png#align=left&display=inline&height=141&name=image.png&originHeight=472&originWidth=2122&size=63869&status=done&width=632">
|
||||
|
||||
如果将销量切换为连续值,则单元格就要使用线条长度代表值的大小,**即连续性的值要能够产生 “对比感”:**
|
||||
|
||||
<img width=632 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566662072534-23177d9d-f32c-4aa2-bea4-8fef0ab6e3c2.png#align=left&display=inline&height=161&name=image.png&originHeight=534&originWidth=2106&size=58097&status=done&width=635">
|
||||
|
||||
上图组件是表格,本身适合展示离散值,但可以看到对连续值展示做了适配。对于适合展示连续值的图形,则无法做离散适配:
|
||||
|
||||
<img width=200 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566703397222-9da012d8-9d13-4378-9d5a-50903f3ba777.png#align=left&display=inline&height=253&name=image.png&originHeight=976&originWidth=772&size=48952&status=done&width=200">
|
||||
|
||||
比如这个柱状图,如果将销量切换为离散,则会自动切换到表格,因为对于双离散值用柱折面饼展示是无意义的。
|
||||
|
||||
#### 作用于维度
|
||||
|
||||
如上图所示,就是维度使用了离散字段的例子,由于维度是离散的,因此使用柱状图展示,因为柱子间也是隔离的。
|
||||
|
||||
**对于连续型字段作用于维度,默认适合散点图,因为散点图的行与列都是度量,适合作为默认推荐:**
|
||||
|
||||
<img width=283 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566703779092-ce78066e-75ec-4b89-9fc1-f44c135e5b5d.png#align=left&display=inline&height=312&name=image.png&originHeight=1154&originWidth=1048&size=66333&status=done&width=283">
|
||||
|
||||
但能用散点图的就也能用线图, **当维度是连续日期字段时,适合用折线图而不是散点图。**因为日期虽然连续,但 **本身不适合做比较** ,因此作为一种连续型维度展示比较合适;而散点图两个轴都适合连续型度量,因此不适合方日期这种连续型维度字段。
|
||||
|
||||
<img width=406 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566703754082-6ec2f472-ea42-4003-aab3-c07f47b8a340.png#align=left&display=inline&height=289&name=image.png&originHeight=1166&originWidth=1636&size=96183&status=done&width=406">
|
||||
|
||||
当然也具备将折线图随时切换为散点图的能力,但这种图形没有什么业务价值:
|
||||
|
||||
<img width=406 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566704003468-7ba3e545-9abf-470f-841d-44dd42e1f754.png#align=left&display=inline&height=292&name=image.png&originHeight=1178&originWidth=1654&size=97442&status=done&width=410">
|
||||
|
||||
因此我们对折线图进行标记:行适合连续型维度字段,对散点图进行标记:行列都适合连续型度量字段,就可以根据配置 **实现推荐图表的功能**。
|
||||
|
||||
### 标记
|
||||
|
||||
除了饼图支持 “角度”、线图支持 “路径” 这些特殊标记外,所有图表都支持下面五种通用标记:“工具提示”、“大小”、“文本”、“颜色”、“详细信息”。
|
||||
|
||||
**工具提示** 比较简单,所有图表都支持鼠标 Hover 后弹出 Tooltip 即可,并且这个 Tooltip 允许自定义和拓展工具提示字段。
|
||||
|
||||
**大小** 则只有折、柱、散三种图支持,因为这三种图分别有可以描述的大小的线条粗细、柱子宽度、圆圈半径。
|
||||
|
||||
**文本** 对应柱折面饼的 Label、对应表格,矩形树状图,地图的 **单元格内容。**
|
||||
|
||||
**颜色、详细信息** 则比较特殊,下面详细说明:
|
||||
|
||||
**拖拽已有字段到详细信息 - 没有任何效果:**
|
||||
|
||||
<img width=476 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566705683330-21e04dfb-41f7-45dc-92b8-69dab98bf009.png#align=left&display=inline&height=249&name=image.png&originHeight=740&originWidth=1416&size=73008&status=done&width=476">
|
||||
|
||||
因为本身就在看这个字段的详细信息,因此没有效果。
|
||||
|
||||
**但如果拖拽已有字段到颜色,则可以根据数值大小或分类进行按颜色区分:**
|
||||
|
||||
<img width=479 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566705734424-b2bc735d-35fa-4cf9-8b67-e24e3dccd0f5.png#align=left&display=inline&height=250&name=image.png&originHeight=736&originWidth=1412&size=76772&status=done&width=479">
|
||||
|
||||
等于开启了图表筛选功能,当颜色筛选条件字段是连续型时,出现筛选滑块,**是离散型时,出现图例:**
|
||||
|
||||
<img wdith=479 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566705795546-6a5e79b7-b32a-4c37-90bc-43926a3a6283.png#align=left&display=inline&height=245&name=image.png&originHeight=720&originWidth=1408&size=80186&status=done&width=480">
|
||||
|
||||
**如果拖拽字段不存在于行和列上,对于度量字段,会根据值进行颜色排序(度量拖拽到详细信息依然没有效果):**
|
||||
|
||||
<img width=479 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566705874311-ebc4ec5d-021f-4601-9f8d-d5becfa9b1ce.png#align=left&display=inline&height=244&name=image.png&originHeight=720&originWidth=1422&size=79431&status=done&width=482">
|
||||
|
||||
如上图所示,我们可以从长度看利润,从颜色深度看销量。
|
||||
|
||||
**如果拖拽字段不存在于行和列上,且是维度字段,则会先进行维度拆分,之后如果选择的是 “颜色” 标记区域,还会对同一组的拆分标记颜色区分。**
|
||||
|
||||
**由于标记区域对维度的拆分是不分行于列的,因此每个图表会根据自身情况进行合适的拆分。**
|
||||
|
||||
比如条形图如果按某个新维度拆分,则会采取 “堆积柱状图” 的策略:
|
||||
|
||||
<img width=471 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706116218-919d2ce7-3fb2-4a32-8624-a3f6cb3122fb.png#align=left&display=inline&height=327&name=image.png&originHeight=986&originWidth=1422&size=117354&status=done&width=471">
|
||||
|
||||
如果是折线图,则会采取 “多条线” 的策略:
|
||||
|
||||
<img width=471 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706404581-01866c81-0553-4245-9b56-176bc5708bfe.png#align=left&display=inline&height=319&name=image.png&originHeight=960&originWidth=1416&size=127313&status=done&width=471">
|
||||
|
||||
如果是散点图,只要将拆分后多出来的点打散出来即可。由于散点图的维度拆分不像折线图和柱状图可以分段,因此如果不采用按颜色打散,是无法分辨分组的:
|
||||
|
||||
<img width=471 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706443331-c817102a-5134-493f-bddf-04fc73f9548f.png#align=left&display=inline&height=319&name=image.png&originHeight=958&originWidth=1414&size=107140&status=done&width=471">
|
||||
|
||||
之所以说探索式分析的复杂度很高,是因为其可能性公式为:
|
||||
|
||||
**字段 x 离散连续 x 行列 x 行列下钻 x 标记种类 x 筛选 x 图表**
|
||||
|
||||
这种组合的笛卡尔积几乎是无穷无尽的。
|
||||
|
||||
### 轴交互
|
||||
|
||||
图表一些特定功能是隐藏在轴交互里的。拿折线图来说,一共有 5 个拖拽交互位置,如下图所示:
|
||||
|
||||
<img width=439 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706982072-996f5f6e-b922-40ec-8066-761857fb2e30.png#align=left&display=inline&height=354&name=image.png&originHeight=1324&originWidth=1642&size=100488&status=done&width=439">
|
||||
|
||||
一般这些区域是用来拖拽度量字段的,所以如果拖拽了维度字段过来,最终会被归类到行列或标记上。
|
||||
|
||||
#### 拖拽维度
|
||||
|
||||
**维度拖拽到底部 1 区域等于替换列字段** :
|
||||
|
||||
<img width=195 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707169109-6e38483a-123a-4064-81ff-fd0ed82010ed.png#align=left&display=inline&height=344&name=image.png&originHeight=1010&originWidth=572&size=44840&status=done&width=195">
|
||||
|
||||
**维度拖拽到图表中 4 区域等于拖到了颜色标记** :
|
||||
|
||||
<img width=387 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707229005-d6ed84f8-8639-4dc6-96b0-ae2bebe70dc0.png#align=left&display=inline&height=331&name=image.png&originHeight=974&originWidth=1138&size=111933&status=done&width=387">
|
||||
|
||||
**维度拖拽到左侧 3 区域等于对行进行下钻:**
|
||||
|
||||
<img width=406 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707279972-a22eb90b-2ebc-4727-911c-10c1d329a06c.png#align=left&display=inline&height=285&name=image.png&originHeight=1012&originWidth=1444&size=106415&status=done&width=406">
|
||||
|
||||
同理拖拽到最上面区域等于对列进行下钻。
|
||||
|
||||
#### 拖拽度量
|
||||
|
||||
让我们看看拖拽度量时的情况。度量能拖拽的范围更多。**比如拖拽到右轴 5 区域,则形成了双轴图:**
|
||||
|
||||
<img width=447 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707391460-e528c513-bc22-4b02-9717-e9339b83d419.png#align=left&display=inline&height=360&name=image.png&originHeight=994&originWidth=1234&size=120248&status=done&width=447">
|
||||
|
||||
**拖拽到左侧 2 区域则表示在图中额外增加一个轴:**
|
||||
|
||||
<img width=571 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707459746-8af4aa6e-acf4-4600-8fce-32c5fbe3400c.png#align=left&display=inline&height=333&name=image.png&originHeight=1020&originWidth=1748&size=126450&status=done&width=571">
|
||||
|
||||
要注意的是,上图的行显示 “度量值”,这是个特殊的字段,并通过筛选器筛选出拖拽的两个字段 Profit 和 Sales。除了拖拽以外,还可以通过将左侧 “度量值” 字段直接拖入行实现:
|
||||
|
||||
<img width=579 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707593546-3670ea98-6e42-4fd2-8438-402c42c318d6.png#align=left&display=inline&height=275&name=image.png&originHeight=1042&originWidth=2190&size=243455&status=done&width=579">
|
||||
|
||||
如上图所示,将度量值放到行,并按度量名称进行颜色标记,就得到了拖拽度量到左侧 2 区域的效果。 **这也说明了所有图表交互最终都是通过映射到配置完成,所有能拖拽的操作都可以通过配置配出来** 。
|
||||
|
||||
对表格来说,能拖拽的区域是行、列、单元格:
|
||||
|
||||
<img width=521 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707733747-55ea9f8c-d2c2-4766-b7b1-57d7abb351d2.png#align=left&display=inline&height=132&name=image.png&originHeight=358&originWidth=1416&size=29996&status=done&width=521">
|
||||
|
||||
拖拽到行或列于拖拽到字段配置区域的行或列没有区别,拖拽到单元格等于拖拽到文本标记区域。通过图表于配置区域结合的方式,即便不完全理解配置的人也可以通过将字段拖拽到图表上得到直观的操作感。
|
||||
|
||||
### 点击、圈选交互
|
||||
|
||||
所有图表都支持点击、圈选的方式选中 “点”。对表格来说,点就是单元格:
|
||||
|
||||
<img width=505 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707894327-f5b88abb-d84e-4839-9c48-5fbbc9ccd929.png#align=left&display=inline&height=128&name=image.png&originHeight=356&originWidth=1408&size=42932&status=done&width=505">
|
||||
|
||||
对柱状图来说,点就是柱子:
|
||||
|
||||
<img width=307 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707923552-680d890f-bc57-4692-b29e-ae787522bfb6.png#align=left&display=inline&height=268&name=image.png&originHeight=972&originWidth=1114&size=85024&status=done&width=307">
|
||||
|
||||
|
||||
对折线图来说,点就是节点:
|
||||
|
||||
<img width=427 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707968058-fd8a598a-48fe-45c8-99f9-94c09f686435.png#align=left&display=inline&height=189&name=image.png&originHeight=640&originWidth=1428&size=59096&status=done&width=421">
|
||||
|
||||
对饼图来说,点就是扇叶:
|
||||
|
||||
<img width=374 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707997384-1f1e4755-0863-48de-8abc-f56ae9feaad1.png#align=left&display=inline&height=169&name=image.png&originHeight=338&originWidth=748&size=26955&status=done&width=374">
|
||||
|
||||
所有的点被选中后都有基本高亮功能,最重要的是能对选中的点进行保留、排除、局部排序等等。
|
||||
|
||||
**比如我们可以对上图饼图选中的几个扇形区域进行从小到大排序:**
|
||||
|
||||
<img width=316 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566708115948-0dc7a73e-97bd-4099-b8bc-12dc16f56501.png#align=left&display=inline&height=243&name=image.png&originHeight=568&originWidth=740&size=48022&status=done&width=316">
|
||||
|
||||
我们也可以排除某些点,这个在配置章节有提到过,这个操作最终将转化为新增筛选条件:
|
||||
|
||||
<img width=283 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566708205782-b74a2d1f-8141-4a05-bea4-8824240a5e67.png#align=left&display=inline&height=280&name=image.png&originHeight=658&originWidth=666&size=52520&status=done&width=283">
|
||||
|
||||
最后,选中状态在单图表中看似只有高亮效果,但是在多图表联动时,高亮的选中区域会组成一个临时的筛选条件,作用于所有相同数据集的图表,并对这些图表的筛选结果做高亮处理。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
理解了探索模型对数据、配置、图表的理解,就能学会探索式思维分析数据,对制作探索式 BI 也有借鉴意义。
|
||||
|
||||
> 讨论地址是:[精读《Tableau 探索式模型》 · Issue #199 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/199)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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 @@
|
||||
> 作者:五灵
|
||||
|
||||
本周工作中遇到类似颜色主题的问题,在查资料的时候,看到这个视频,觉得讲得很清楚,而且趣味性丰富,所以想拿出来讲讲这个很有意思的主题。
|
||||
|
||||
视频链接: [CSSconf EU 2018 | Dag-Inge Aas & Ida Aalen: Generating Colors with JS and CSS Custom Properties](https://www.youtube.com/watch?v=zi6L0ZqrKfA)
|
||||
|
||||
## 1. 精读
|
||||
|
||||
### CSS 变量
|
||||
|
||||
CSS 变量及 CSS Variables(Custom Properties),目前几乎都已经被主流浏览器所支持,但是估计还有一部分读者不熟悉这个功能,简单列举一下使用方法:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg-color: brown; // 定义颜色变量
|
||||
}
|
||||
.btn {
|
||||
// 直接使用颜色预定义的颜色变量
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
```
|
||||
|
||||
### Web 内容无障碍指南的对比度
|
||||
|
||||
Web 内容无障碍指南的对比度指的是 W3C 组织发布的 [《Web Content Accessibility Guidelines (WCAG)》](https://www.w3.org/TR/WCAG/#glossary),这个指南中涵盖了让 Web 内容更易于访问的各种建议,其中针对网页的颜色对比度发布了规范。
|
||||
|
||||
在 Chrome 中对于颜色编辑的时候,打开颜色选择器也会看到当前颜色的对比度值(Contrast ratio)。
|
||||
|
||||

|
||||
|
||||
网页颜色的对比度值在 1:1 到 21:1 之间,文本和图像文本的的对比度最小值为 4.5:1,也就是说低于这个值得对比度都不符合标准。 我们看一下列举的几种颜色对比度,对比度越高,也越有利于阅读。对比度越低,对于一些存在视力障碍或色觉缺陷的用户,可能就无法阅读。
|
||||
|
||||

|
||||
|
||||
### 演讲中的颜色解决方案
|
||||
|
||||
演讲在最开始首先讲了挪威的一个法律,不符合 Web 内容无障碍指南的站点在挪威是非法的,所以挪威的 Web 开发者非常注重站点的内容无障碍。
|
||||
|
||||
首先讲了使用 css 变量的方式,支持各种颜色主题的切换。 利用 js 去设置颜色变量,支持主题的颜色切换。
|
||||
|
||||
但是紧接着就提出了问题,如果用户可以随意切换颜色主题背景色,那一些按钮的文字可读性如何去保障呢?如果用户选择了与按钮颜色想接近的背景色,我们又该怎么处理了,紧接着这个演讲给出了根据明度决定按钮文字颜色是黑色还是白色的方案。
|
||||
|
||||
- 根据明度决定是黑色还是白色
|
||||
|
||||
具体代码如下,大致原理是把彩色转为灰度的颜色,有一个著名的心理学公式:`Gray = R*0.299 + G*0.587 + B*0.114`,然后在根据颜色灰度决定使用黑色的主题还是白色的主题。
|
||||
|
||||
```javascript
|
||||
if (red*0.299 + green*0.587 + blue*0.114) > 186 use #000000 else use #ffffff
|
||||
```
|
||||
|
||||

|
||||
|
||||
可读性的问题解决了,但是紧接着又遇到了一个问题,如果用户选取的颜色很浅呢,与背景颜色的对比度小于 4.5,该怎么处理呢。
|
||||
|
||||

|
||||
|
||||
- 寻找对比度更强的颜色,增强可读性
|
||||
|
||||
演讲中给出的解决方法是不断的加深当前用户选择的颜色,循环获取到对比度最高的同色系颜色。代码如下:
|
||||
|
||||

|
||||
|
||||
获取了一个更深的颜色后,通过给按钮加一个外边框的方式,优化整体的可读性。
|
||||
|
||||

|
||||
|
||||
文章最后还介绍了,通过给定一个主题色,获取第二第三主题色的方式,通过将颜色放到 HSL 的颜色轮上,转动 hue 的值 60 度,得到一个新的第二主题色。不过演讲者也没有说清楚为什么要这么做,只是说了这么做是出于经验,觉得这样能够得到一个恰当的主题色盘。
|
||||
|
||||
### 衍生的纯 css 解决方案
|
||||
|
||||
演讲中提供颜色变更的解决方案基本都是基于 JS 计算的,后来有人在 [css-tricks](https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/) 抛出一篇文章说,这个功能基于 css 就可以完全实现,其实关于颜色的原理都是一致的,只是觉得这个实现更加 magic,但是功能都能够完全满足。比如这篇文章中,关于根据明度决定按钮文字是黑色还是白色的代码如下:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--light: 80;
|
||||
/* 文字颜色变化的临界值 */
|
||||
--threshold: 60;
|
||||
}
|
||||
.btn {
|
||||
/* 会被解析成黑色或者白色 */
|
||||
--switch: calc((var(--light) - var(--threshold)) * -100%);
|
||||
color: hsl(0, 0%, var(--switch));
|
||||
}
|
||||
```
|
||||
|
||||
### 可视化图表对于颜色的应用
|
||||
|
||||
在可视化图表当中,对于颜色的应用要比 Web 要谨慎的多。我们在做 Web 开发的时候,也不妨来看一下可视化图表当中对于颜色应用的一些规范。在可视化图表中,选择的颜色不可以过于随意,每次颜色的变更都是图表信息的改变,都为图表增加了新的数据,图表的每一种颜色也是要表达的信息。列举一些图表中的颜色使用规范,比如:
|
||||
|
||||
1. 不建议使用多种颜色表达同种数据
|
||||
2. 在多条行图表中,不要使用不同的颜色或颜色轮中对立面的颜色。颜色对比过强会使读者无法专心于数据。
|
||||
3. 一般而言,应避免颜色的主体性表现,避免使用具有特殊意义的颜色。比如使用红色和绿色表示销售额的变化。
|
||||
|
||||
当然对于可视化图表来说,并不是遵循了一些色彩使用的准则,就可以得到一个优雅呈现的可视化图表。注重图表呈现的最重要的视觉元素,在视觉信息角度减少用户,减少用户视觉疲劳也很重要。
|
||||
|
||||
## 3. 相关链接
|
||||
|
||||
CSS 前景背景自动配色技术简介: [https://www.zhangxinxu.com/wordpress/2018/11/css-background-color-font-auto-match/](https://www.zhangxinxu.com/wordpress/2018/11/css-background-color-font-auto-match/)<br />
|
||||
纯 css 解决方案:[https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/](https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/)<br />
|
||||
获取颜色的 Demo: [https://confrere.com/a11y/test/](https://confrere.com/a11y/test/)<br />
|
||||
颜色色盘推荐的文章:[https://blog.graphiq.com/finding-the-right-color-palettes-for-data-visualizations-fcd4e707a283](https://blog.graphiq.com/finding-the-right-color-palettes-for-data-visualizations-fcd4e707a283)
|
||||
|
||||
> 讨论地址是:[精读《使用 css 变量生成颜色主题》 · Issue #203 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/203)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,79 @@
|
||||
> 作者:五灵
|
||||
|
||||
## 简介
|
||||
|
||||
其实关于前端深水区的讨论,已经有了很多,也有了很多相关的文章。我也想借这篇关于深水区的讨论文章,讲一下自己对于深水区的理解。
|
||||
原文链接:[技术路线:前端开发已进入深水区](https://www.yuque.com/sxc/front/kvokg4)
|
||||
|
||||
本期精读,[@camsong](https://github.com/camsong)、[@arcthur](https://github.com/arcthur)、[@ascoders](https://github.com/ascoders) 都有贡献观点。
|
||||
|
||||
## 概述
|
||||
|
||||
原文对于深水区的想法,讲的很清楚,还是建议读者去读一下原文。
|
||||
对比 2010 年,整个前端生态已经翻新了好几遍,直到近几年的 Node BFF、IDE Cloud,抑或是客户端 AI,还是 Serverless 的建设,,前端想要深度参与的话,单纯依靠原来的 HTML/CSS/JS 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
|
||||
|
||||
- 深水区需要哪些技能
|
||||

|
||||
深水区需要是四个核心能力,分别是:技术、产品、业务和管理能力。
|
||||
- 面对深水压力不需紧张
|
||||
其实何止前端开发,整个技术行业都已步入深水区,只是前端工程师的感知来的晚一些而已。只要把眼光投向深水区,问题就会一个接一个的浮上来,当越来越多问题浮起来的时候,就是你慢慢沉向深水区的时候,这时候不需要太过紧张。
|
||||
|
||||
## 精读
|
||||
|
||||
深水区的理解首先需要达成一致,并不只是一个维度的加深,而是全方位多方面的困难同时加击,压强升高、光线减少、温度剧变等等。
|
||||
|
||||
对应到文中总结的解法就是需要『技术创新、流程优化、团队合作、影响大盘、驱动业务、商业决策和团队管理』。但你展开想一下,把这个角色换成后端、无线端、甚至是 UED,是不是也能完美匹配。所以这些能力应该是技术人员发展到一定程度面临的普遍问题而不仅仅是前端。
|
||||
|
||||
但这些能力是否有个更好的概括?当然有,就是明确一个方向并带领一群人完成目标并实线商业价值。这其实就是商业或者说业务的整个运作过程。
|
||||
|
||||
这其实也在抛一个命题,前端发展到一定程度就一定要转业务吗?
|
||||
是也不是。当然要转,但并不是全转。全转业务你过去的积累有什么用?不转业务单纯前端能发挥的影响力就会受限。所以答案是利用前端技术优势同时补充业务能力推动商业流程。
|
||||
|
||||
所以此文并不是严格上讲前端技术的深水区,或者作者肯定认为他能接触的前端技术已经到瓶颈,且没有想到突破口。
|
||||
|
||||
怎么去定义深水区,@流形 认为是需要建立技术壁垒或学术壁垒。当我们看待一向技术,如果在投入一到两年就可以对齐,那么显然技术本身的深度是可观的,如果是十年才能对齐,这时候除了会影响经济或政治外,不会有人会去重做,只能使用。用另一个类似的概念反摩尔定律来对应深水区说,每隔两年,技术不能显著带来效能的成倍提升。
|
||||
|
||||
### 深水区值得关注的方向
|
||||
|
||||
#### 业务领导力
|
||||
|
||||
也就是原文提到的 “技术创新、流程优化、团队合作、影响大盘、驱动业务、商业决策、团队管理” 等能力,一个拥有领导力的人发挥的价值远超自身孤立的价值。
|
||||
|
||||
#### 业务价值
|
||||
|
||||
发挥业务价值是技术人的最终目标,比如数据库技术想发挥业务价值,就要做到高效、稳定,价值越大往往技术难度就越大。
|
||||
|
||||
值得庆幸的是,前端的业务价值与技术难度往往不成正比,有时候将客户的业务场景固化成一套模版,整合起来赋能给更多客户,这等于将商业模型作为能力赋予了其他客户,但本身并没有用到一些高级技术。前端能做的不仅是内部提效和外部体验,因为前端是人机交互的入口,才有机会将业务思考打包到代码中,直接透出给客户。
|
||||
|
||||
#### 端技术的发展
|
||||
|
||||
1. 数字孪生。那么在端上的仿真能力需要大幅提高,那么结合模型自动生成,不同物体的建模能力等都是很大挑战
|
||||
2. 虚拟实现。这点上就不赘述,从 FB 重点发展 Oculus,微软发展 HoloLens 可以看到这个趋势,从互动的未来来看,这不是终局,但是最适合今天要突破的技术。
|
||||
3. 可视分析。数据在人类面前还是过分难懂,结合数据的分析系统在各行各业正在渗透,端上结合可视化的能力就显得非常重要。
|
||||
4. 更多的,像边缘计算,前端安全等领域都是非常深入的领域。
|
||||
这些问题,已经不是一年就能完全突破的,需要 3-5 年,甚至 10 年时间。
|
||||
|
||||
#### 前端深入体系
|
||||
|
||||
1. 但对于我所处的大数据环境来说,确实接触了前端技术深水区。来源于端计算能力 + 网络基建 + 大数据的爆炸式增长。
|
||||
编辑器:复杂的开发离不开代码,前端们一直孜孜不倦的把 IDE 引入 web,VS Code 做了很成功的尝试但还是需要一层壳套着。且对于大数据处理这样的领域,需要定制的能力远超过通用的 Manaco editor 等能提供。
|
||||
2. 表格类数据处理能力:比尔盖茨最引以为豪的微软软件是 Excel。你永远不知道 Excel 有多少种酷的用法来解决用户问题。能否把 Excel 引入到 web?同时对数百万条数据做交叉分析,这对性能和架构都有很大的挑战。
|
||||
3. 可视化数据展现:大数据的一个典型特征就是价值稀疏性,如何把蕴含的价值展现出来,需要了解图形学、统计学、交互色彩等各种能力。大学老师教的内容终于能派生用场了。
|
||||
|
||||
## 总结
|
||||
|
||||
在局部领域前端已经有可能深入,当然前端技能上说这些也不能用 HTML, CSS, JS 来解决,需要开发者有深入学科的背景。但今天前端面向还是产品功能的需要,在端上更强调的还是产品功能为主。我们做一款复杂产品,更多还会在工程上纠结。如果没在功能的深入性上思考更多,以对应真正技术发展,那么深水区还远。
|
||||
|
||||
正如前面所说,深水区会压强升高、光线减少、温度剧变,需要自己发光发热和更多的坚持。
|
||||
|
||||
跨过深水区,让其他人处在浅水区就能做事,这或许就是你走出深水区的标志。就像 Alan Perlis 说的一句话『简单不先于复杂,而是在复杂之后』,也许未来看来你今天挣扎的深水区只是个小泥坑。
|
||||
|
||||
> 讨论地址是:[精读《前端深水区》 · Issue #193 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/193)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,290 @@
|
||||
## 简介
|
||||
|
||||
React 16.8 于 2019.2 正式发布,这是一个能提升代码质量和开发效率的特性,笔者就抛砖引玉先列出一些实践点,希望得到大家进一步讨论。
|
||||
|
||||
然而需要理解的是,没有一个完美的最佳实践规范,对一个高效团队来说,稳定的规范比合理的规范更重要,因此这套方案只是最佳实践之一。
|
||||
|
||||
## 精读
|
||||
|
||||
### 环境要求
|
||||
|
||||
- 拥有较为稳定且理解函数式编程的前端团队。
|
||||
- 开启 ESLint 插件:[eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)。
|
||||
|
||||
### 组件定义
|
||||
|
||||
Function Component 采用 `const` + 箭头函数方式定义:
|
||||
|
||||
```tsx
|
||||
const App: React.FC<{ title: string }> = ({ title }) => {
|
||||
return React.useMemo(() => <div>{title}</div>, [title]);
|
||||
};
|
||||
|
||||
App.defaultProps = {
|
||||
title: 'Function Component'
|
||||
}
|
||||
```
|
||||
|
||||
上面的例子包含了:
|
||||
|
||||
1. 用 `React.FC` 申明 Function Component 组件类型与定义 Props 参数类型。
|
||||
2. 用 `React.useMemo` 优化渲染性能。
|
||||
3. 用 `App.defaultProps` 定义 Props 的默认值。
|
||||
|
||||
#### FAQ
|
||||
|
||||
> 为什么不用 React.memo?
|
||||
|
||||
推荐使用 `React.useMemo` 而不是 `React.memo`,因为在组件通信时存在 `React.useContext` 的用法,这种用法会使所有用到的组件重渲染,只有 `React.useMemo` 能处理这种场景的按需渲染。
|
||||
|
||||
> 没有性能问题的组件也要使用 useMemo 吗?
|
||||
|
||||
要,考虑未来维护这个组件的时候,随时可能会通过 `useContext` 等注入一些数据,这时候谁会想起来添加 `useMemo` 呢?
|
||||
|
||||
> 为什么不用解构方式代替 defaultProps?
|
||||
|
||||
虽然解构方式书写 `defaultProps` 更优雅,但存在一个硬伤:对于对象类型每次 Rerender 时引用都会变化,这会带来性能问题,因此不要这么做。
|
||||
|
||||
### 局部状态
|
||||
|
||||
局部状态有三种,根据常用程度依次排列: `useState` `useRef` `useReducer` 。
|
||||
|
||||
#### useState
|
||||
|
||||
```tsx
|
||||
const [hide, setHide] = React.useState(false);
|
||||
const [name, setName] = React.useState('BI');
|
||||
```
|
||||
|
||||
状态函数名要表意,尽量聚集在一起申明,方便查阅。
|
||||
|
||||
#### useRef
|
||||
|
||||
```tsx
|
||||
const dom = React.useRef(null);
|
||||
```
|
||||
|
||||
`useRef` 尽量少用,大量 Mutable 的数据会影响代码的可维护性。
|
||||
|
||||
但对于不需重复初始化的对象推荐使用 `useRef` 存储,比如 `new G2()` 。
|
||||
|
||||
#### useReducer
|
||||
|
||||
局部状态不推荐使用 `useReducer` ,会导致函数内部状态过于复杂,难以阅读。 `useReducer` 建议在多组件间通信时,结合 `useContext` 一起使用。
|
||||
|
||||
#### FAQ
|
||||
|
||||
> 可以在函数内直接申明普通常量或普通函数吗?
|
||||
|
||||
不可以,Function Component 每次渲染都会重新执行,常量推荐放到函数外层避免性能问题,函数推荐使用 `useCallback` 申明。
|
||||
|
||||
### 函数
|
||||
|
||||
所有 Function Component 内函数必须用 `React.useCallback` 包裹,以保证准确性与性能。
|
||||
|
||||
```tsx
|
||||
const [hide, setHide] = React.useState(false);
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
setHide(isHide => !isHide)
|
||||
}, [])
|
||||
```
|
||||
|
||||
`useCallback` 第二个参数必须写,[eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) 插件会自动填写依赖项。
|
||||
|
||||
### 发请求
|
||||
|
||||
发请求分为操作型发请求与渲染型发请求。
|
||||
|
||||
#### 操作型发请求
|
||||
|
||||
操作型发请求,作为回调函数:
|
||||
|
||||
```tsx
|
||||
return React.useMemo(() => {
|
||||
return (
|
||||
<div onClick={requestService.addList} />
|
||||
)
|
||||
}, [requestService.addList])
|
||||
```
|
||||
|
||||
#### 渲染型发请求
|
||||
|
||||
渲染型发请求在 `useAsync` 中进行,比如刷新列表页,获取基础信息,或者进行搜索, **都可以抽象为依赖了某些变量,当这些变量变化时要重新取数** :
|
||||
|
||||
```tsx
|
||||
const { loading, error, value } = useAsync(async () => {
|
||||
return requestService.freshList(id);
|
||||
}, [requestService.freshList, id]);
|
||||
```
|
||||
|
||||
### 组件间通信
|
||||
|
||||
简单的组件间通信使用透传 Props 变量的方式,而频繁组件间通信使用 `React.useContext` 。
|
||||
|
||||
以一个复杂大组件为例,如果组件内部拆分了很多模块, **但需要共享很多内部状态** ,最佳实践如下:
|
||||
|
||||
#### 定义组件内共享状态 - store.ts
|
||||
|
||||
```tsx
|
||||
export const StoreContext = React.createContext<{
|
||||
state: State;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
}>(null)
|
||||
|
||||
export interface State {};
|
||||
|
||||
export interface Action { type: 'xxx' } | { type: 'yyy' };
|
||||
|
||||
export const initState: State = {};
|
||||
|
||||
export const reducer: React.Reducer<State, Action> = (state, action) => {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### 根组件注入共享状态 - main.ts
|
||||
|
||||
```tsx
|
||||
import { StoreContext, reducer, initState } from './store'
|
||||
|
||||
const AppProvider: React.FC = props => {
|
||||
const [state, dispatch] = React.useReducer(reducer, initState);
|
||||
|
||||
return React.useMemo(() => (
|
||||
<StoreContext.Provider value={{ state, dispatch }}>
|
||||
<App />
|
||||
</StoreContext.Provider>
|
||||
), [state, dispatch])
|
||||
};
|
||||
```
|
||||
|
||||
#### 任意子组件访问/修改共享状态 - child.ts
|
||||
|
||||
```tsx
|
||||
import { StoreContext } from './store'
|
||||
|
||||
const app: React.FC = () => {
|
||||
const { state, dispatch } = React.useContext(StoreContext);
|
||||
|
||||
return React.useMemo(() => (
|
||||
<div>{state.name}</div>
|
||||
), [state.name])
|
||||
};
|
||||
```
|
||||
|
||||
如上解决了 **多个联系紧密组件模块间便捷共享状态的问题** ,但有时也会遇到需要共享根组件 Props 的问题,**这种不可修改的状态不适合一并塞到 `StoreContext` 里**,我们新建一个 `PropsContext` 注入根组件的 Props:
|
||||
|
||||
```tsx
|
||||
const PropsContext = React.createContext<Props>(null)
|
||||
|
||||
const AppProvider: React.FC<Props> = props => {
|
||||
return React.useMemo(() => (
|
||||
<PropsContext.Provider value={props}>
|
||||
<App />
|
||||
</PropsContext.Provider>
|
||||
), [props])
|
||||
};
|
||||
```
|
||||
|
||||
#### 结合项目数据流
|
||||
|
||||
参考 [react-redux hooks](https://github.com/reduxjs/react-redux/blob/master/docs/api/hooks.md)。
|
||||
|
||||
### debounce 优化
|
||||
|
||||
比如当输入框频繁输入时,为了保证页面流畅,我们会选择在 `onChange` 时进行 `debounce` 。然而在 Function Component 领域中,我们有更优雅的方式实现。
|
||||
|
||||
> 其实在 Input 组件 `onChange` 使用 `debounce` 有一个问题,就是当 Input 组件 **受控** 时, `debounce` 的值不能及时回填,导致甚至无法输入的问题。
|
||||
|
||||
我们站在 Function Component 思维模式下思考这个问题:
|
||||
|
||||
1. React [scheduling](https://github.com/dt-fe/weekly/blob/v2/099.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md) 通过智能调度系统优化渲染优先级,我们其实不用担心频繁变更状态会导致性能问题。
|
||||
2. 如果联动一个文本还觉得慢吗? `onChange` 本不慢,大部分使用值的组件也不慢,没有必要从 `onChange` 源头开始就 `debounce` 。
|
||||
3. 找到渲染性能最慢的组件(比如 iframe 组件),**对一些频繁导致其渲染的入参进行 `useDebounce`** 。
|
||||
|
||||
下面是一个性能很差的组件,引用了变化频繁的 `text` (这个 `text` 可能是 `onChange` 触发改变的),我们利用 `useDebounce` 将其变更的频率慢下来即可:
|
||||
|
||||
```typescript
|
||||
const App: React.FC = ({ text }) => {
|
||||
// 无论 text 变化多快,textDebounce 最多 1 秒修改一次
|
||||
const textDebounce = useDebounce(text, 1000)
|
||||
|
||||
return useMemo(() => {
|
||||
// 使用 textDebounce,但渲染速度很慢的一堆代码
|
||||
}, [textDebounce])
|
||||
};
|
||||
```
|
||||
|
||||
使用 `textDebounce` 替代 `text` 可以将渲染频率控制在我们指定的范围内。
|
||||
|
||||
### useEffect 注意事项
|
||||
|
||||
事实上,`useEffect` 是最为怪异的 Hook,也是最难使用的 Hook。比如下面这段代码:
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
props.onChange(props.id)
|
||||
}, [props.onChange, props.id])
|
||||
```
|
||||
|
||||
如果 `id` 变化,则调用 `onChange`。但如果上层代码并没有对 `onChange` 进行合理的封装,导致每次刷新引用都会变动,则会产生严重后果。我们假设父级代码是这么写的:
|
||||
|
||||
```tsx
|
||||
class App {
|
||||
render() {
|
||||
return <Child id={this.state.id} onChange={id => this.setState({ id })} />
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这样会导致死循环。虽然看上去 `<App>` 只是将更新 id 的时机交给了子元素 `<Child>`,但由于 `onChange` 函数在每次渲染时都会重新生成,因此引用总是在变化,就会出现一个无限死循环:
|
||||
|
||||
新 `onChange` -> `useEffect` 依赖更新 -> `props.onChange` -> 父级重渲染 -> 新 `onChange`...
|
||||
|
||||
想要阻止这个循环的发生,只要改为 `onChange={this.handleChange}` 即可,**`useEffect` 对外部依赖苛刻的要求,只有在整体项目都注意保持正确的引用时才能优雅生效。**
|
||||
|
||||
然而被调用处代码怎么写并不受我们控制,这就导致了不规范的父元素可能导致 React Hooks 产生死循环。
|
||||
|
||||
因此在使用 `useEffect` 时要注意调试上下文,注意父级传递的参数引用是否正确,如果引用传递不正确,有两种做法:
|
||||
|
||||
1. 使用 [useDeepCompareEffect](https://github.com/streamich/react-use/blob/master/docs/useDeepCompareEffect.md) 对依赖进行深比较。
|
||||
2. 使用 `useCurrentValue` 对引用总是变化的 props 进行包装:
|
||||
|
||||
```tsx
|
||||
function useCurrentValue<T>(value: T): React.RefObject<T> {
|
||||
const ref = React.useRef(null);
|
||||
ref.current = value;
|
||||
return ref;
|
||||
}
|
||||
|
||||
const App: React.FC = ({ onChange }) => {
|
||||
const onChangeCurrent = useCurrentValue(onChange)
|
||||
};
|
||||
```
|
||||
|
||||
`onChangeCurrent` 的引用保持不变,但每次都会指向最新的 `props.onChange`,从而可以规避这个问题。
|
||||
|
||||
## 总结
|
||||
|
||||
如果还有补充,欢迎在文末讨论。
|
||||
|
||||
如需了解 Function Component 或 Hooks 基础用法,可以参考往期精读:
|
||||
|
||||
- [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/v2/079.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md)
|
||||
- [精读《怎么用 React Hooks 造轮子》](https://github.com/dt-fe/weekly/blob/v2/080.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md)
|
||||
- [精读《useEffect 完全指南》](https://github.com/dt-fe/weekly/blob/v2/096.%E7%B2%BE%E8%AF%BB%E3%80%8AuseEffect%20%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E3%80%8B.md)
|
||||
- [精读《Function Component 入门》](https://github.com/dt-fe/weekly/blob/v2/104.精读《Function%20Component%20入门》.md)
|
||||
|
||||
> 讨论地址是:[精读《React Hooks 最佳实践》 · Issue #202 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/202)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,175 @@
|
||||
## 简介
|
||||
|
||||
商业智能(Business Intelligence)简称 BI,即通过数据挖掘与分析找到商业洞察,助力商业成功。
|
||||
|
||||
一个完整的 BI 链路包含数据采集、数据清洗、数据挖掘、数据展现,其本质是对数据进行多维分析。前端的主要工作在数据展现环节,由于展示方式繁多、分析模型复杂且数据量大,前端环节的复杂度很高。
|
||||
|
||||
在 BI 做前端非常有挑战,开发者需要充分理解数据概念,而本身复杂度较高的可视化建站也只是 BI 的基础能力,想要建设 BI 的上层能力,比如探索式分析和数据洞察,都需要在前后端引入更复杂的计算模型。
|
||||
|
||||
本文作为一个引子,简单介绍笔者做 BI 的经验,后面如果有机会再写一个系列文章对细节进行阐述。
|
||||
|
||||
## 精读
|
||||
|
||||
国内目前处于 BI 1.0 阶段,也就是报表阶段,因此笔者将阐述这个阶段 BI 的核心开发概念。
|
||||
|
||||
> BI 2.0 探索式分析阶段是国内数据分析最前沿领域,这部分等开发完成后再分享。
|
||||
|
||||
BI 1.0 阶段的核心概念包括 **数据集、渲染引擎、数据模型、可视化** 这四个技术模块。
|
||||
|
||||
### 数据集
|
||||
|
||||
数据集即数据的集合,在 BI 领域更多指一种标准化的数据结构。
|
||||
|
||||
任何数据都可以封装成数据集,比如 txt 文本、excel、mysql 数据库等等。
|
||||
|
||||
数据集的基本形态是二维表格,列头表示字段,每一行就是一份数据,数据展示就是通过对这些数据字段进行多维度分析。
|
||||
|
||||
#### 数据集导入
|
||||
|
||||
一般来说数据集导入有两种方式,分别是本地文件上传与数据库链接。本地文件上传又分为多种文件类型处理,比如对 excel 的解析,可能还包括数据清洗;数据库链接分析可视化导入与 SQL 输入。
|
||||
|
||||
可视化导入需要提前对数据库进行结构分析,绘制出表结构与字段结构,不用理解 SQL 也可以进行可视化操作。
|
||||
|
||||
SQL 输入可以利用 [monaco-editor](https://github.com/microsoft/monaco-editor) 等 web 代码编辑器作为输入框,最好能结合智能提示提高 sql 编写效率。sql 智能提示可以参考往期精读 [精读《手写 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)。
|
||||
|
||||
#### 数据集建模
|
||||
|
||||
数据集建模一般包含 **维度度量建模、字段配置、层系建模**。
|
||||
|
||||
维度度量建模需要智能分析出字段属于维度还是度量,一般会结合字段实际的值或者字段名来智能判断字段类型,如果数据库信息中已存储了字段类型,就可以 100% 准确归类。
|
||||
|
||||
字段配置即对字段进行增删或修改,还可以新增聚合字段或对比字段。
|
||||
|
||||
聚合字段是指将一个字段表达式封装为一个新字段,这里也会用到一个简单的 sql 编辑器,只需要支持四则运算、字段提示、以及一些基本函数的组合即可。
|
||||
|
||||
对比字段是指新增的字段是基于已有字段在某个时间周期内的对比,比如对 UV 字段的年同比就可以封装为一个对比字段。对比字段在前端技术上没有什么难度,仅需理解概念即可。
|
||||
|
||||
### 渲染引擎
|
||||
|
||||
渲染引擎包括了对报表进行编辑与渲染的引擎,理论上可以合二为一。
|
||||
|
||||
渲染引擎的重要模块包括:画布拖拽、组件编辑、事件中心。
|
||||
|
||||
画布拖拽其实包含了组件自定义开发流程,到 CDN 发布、CDN 加载、组件拖拽、画布排版等一系列技术点,每个点展开都有写不完的细节,但好在这套功能属于通用建站基础功能点,本文就不再赘述。
|
||||
|
||||
组件编辑中,基本属性的编辑与属于通用建站领域的表单模型范畴,一般通过 UISchema 来描述通用表单,这块也不再赘述。组件编辑的另一部分就是数据编辑,这部分在后面数据模型章节里详细讲。
|
||||
|
||||
事件中心是渲染引擎部分,此功能在编辑状态需要禁用。这个功能可以实现图表联动、上卷下钻等数据能力。一个通用事件中心一般包括 **事件触发** 与 **事件响应** 两部分,基本结构如下:
|
||||
|
||||
```typescript
|
||||
interface Event {
|
||||
trigger:
|
||||
| {
|
||||
type: 'callback';
|
||||
callbackName: string;
|
||||
}
|
||||
| {
|
||||
type: 'listener';
|
||||
eventName: string;
|
||||
}
|
||||
| {
|
||||
type: 'system';
|
||||
name: string;
|
||||
};
|
||||
|
||||
action:
|
||||
| {
|
||||
type: 'dispatch';
|
||||
eventName: string;
|
||||
}
|
||||
| {
|
||||
type: 'jumpUrl';
|
||||
url: string;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`trigger` 即事件触发,包括基本的系统事件 `system`,比如定时器或者初始化自动触发;组件的回调 `callback` 比如当按钮被点击时;事件监听 `listener` 比如另一个事件被触发时,这个事件可能来自于 `action`。
|
||||
|
||||
`action` 即事件响应,包括基本的事件触发 `dispatch`,可以触发其他事件,可以构成一个事件链路;其他的 `action` 就是数据相关,可以用来做条件联动、字段联动、数据集联动等等,因为实现各异这里不做介绍。
|
||||
|
||||
事件机制还需要支持值传递,即事件触发源的值可以传递到事件响应方。值传递可以在触发源内部进行,比如当触发源是回调函数时,函数参数就自然作为值传递过去,触发源通过 `...args` 方式接收。
|
||||
|
||||
#### 数据钻取
|
||||
|
||||
配置了层系的字段都可以进行数据钻取。层系可以在数据集配置,也可以在报表编辑页配置,可以理解为一个顺序有关的文件夹,将文件夹作为字段使用时,默认生效的是第一个子元素,之后可以按照顺序分别进行下钻。
|
||||
|
||||
比如 “地区” 层系包含了国家、省、市、区,那么就可以按照这个层级进行数据上卷下钻。
|
||||
|
||||
如果一个字段是层系字段,图表需要有对应的操作区域进行上卷下钻,数据编辑区域也可以进行同样操作。数据钻取的计算过程不在图表内部处理,而是触发一个状态后,由渲染引擎将这个层系字段实例状态改为下钻到第 N 层,并且每下钻一次就多拿到一列的数据,由图表组件进行下钻展示。
|
||||
|
||||
一般来说下钻后数据仍是全量的,有时候为了避免数据量过大,比如在柱状图点击某个柱子进行下钻,只想看这个柱子下钻后的数据:比如 2017、2018、2019 年三年的数据,下钻到月后数据量是 3 x 12 = 36 条,但如果仅在 2019 年进行下钻,只想看 2019 年的 12 条数据,可以转化为下钻 + 筛选条件的模式:全局下钻展开后 36 条,在 2019 年上点击下钻后,增加一个筛选条件(年 = 2019),这样就达到了效果,整个流程对图表组件是无感知的。
|
||||
|
||||
### 数据模型
|
||||
|
||||
与通用表单模型 UISchema 相对应,数据模型笔者称之为 CubeSchema,因为 BI 领域对数据的多维处理模型成为 Cube 立方体,数据配置即表示如何对这个立方体进行查询,因此其配置表单成为 CubeSchema。
|
||||
|
||||
不管是探索式分析还是 BI 1.0 的报表阶段,数据模型的基本概念是通用的(探索式分析固定了行列,且增加了标记):将字段放置到不同的区域,这些区域的划分方式可以按照功能:横轴、纵轴;按照概念:维度、度量;按照探索分析思路:固化为行、列等等。
|
||||
|
||||
这块可能涉及到的技术点有:拖拽、批量选择+拖拽、双击后按照维度度量自动添加、图表切换后区域字段自动迁移、对字段拖拽的系列配置:限制数量、限制类型、限制数据集、是否重复等等。
|
||||
|
||||
拖拽可以用 [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd) 等库,与渲染引擎拖拽方案基本类似,遇到有层系的数据集还需支持嵌套层级的拖拽。
|
||||
|
||||
图表切换后字段迁移,可以将每个拖拽区域设置若干类型:
|
||||
|
||||
```json
|
||||
{
|
||||
"dataType": ["dimension"]
|
||||
}
|
||||
```
|
||||
|
||||
这样在切换后,维度类型的字段可以自动迁移到维度类型区域,如果对应区域字段数量达到了 `limit` 限制,就继续填充到下一个区域,直到字段用尽或区域填充完为止。
|
||||
|
||||
如果在探索式分析场景里,需要提前对字段进行维度度量建模,在切换时按照图表情况进行相应的处理。比如折线图切换到表格的情况:折线图是天然一个维度(主轴) + N 个度量的场景,表格是天然两个维度(行、列)+ 1 个度量的场景(也可以支持多个,对单元格进行再切分即可),那么从折线图切换到表格时,度量就会落到标记的文本区域;如果从拥有行和列的表格切换到柱状图(之所以无法切换到折线图,是因为表格的度量值一般是离散的,而折线图度量值一般是连续的),表格的行与列的字段会落到柱状图的维度轴,表现效果是对维度轴进行下钻。
|
||||
|
||||
> [精读《Tableau 探索式模型》](https://github.com/dt-fe/weekly/blob/v2/117.%E7%B2%BE%E8%AF%BB%E3%80%8ATableau%20%E6%8E%A2%E7%B4%A2%E5%BC%8F%E6%A8%A1%E5%9E%8B%E3%80%8B.md) 了解更多探索式分析。
|
||||
|
||||
数据模型还包括数据分析相关配置,比如设置对比字段,或者均值线等分析功能。这些数据计算工作放在后端,前端需要将配置项整理到取数接口中,并按照数据驱动的方式展现。
|
||||
|
||||
对于对比字段等 “拓展字段” 的分析功能,可以拓展通用取数接口,图表组件无感知,相当于多添加了几个隐藏字段;去特殊值等对标准数据进行操作的情况图表组件也无需感知。
|
||||
|
||||
聚类、均值线等需要图表组件额外展示的部分抽象为一套固定的数据格式透传给图表组件,由图表组件自行处理。
|
||||
|
||||
可以看出来,都是取数 + 展示,普通的前端业务与 BI 业务开发的区别:
|
||||
|
||||
普通前端业务是以业务逻辑为核心的,根据业务需要确定接口格式;BI 业务是以数据为核心的,围绕数据计算模型确定一套固定的接口格式,取数不依赖组件,所有组件对标准数据都有对应的展现。
|
||||
|
||||
### 可视化
|
||||
|
||||
与普通可视化组件不同,BI 可视化组件需要对接 CubeSchema 模型,同时还要支持 **大数据性能优化、边界数据展示优化、交互响应**。
|
||||
|
||||
对接 CubeSchema 即统一对接二维表格的数据,大部分组件都是二维以上结构展示,因此对接起来并不困难,有一些一维数据结构的组件比如单指标块就要舍弃其中的某一维,需要确定一套规则。
|
||||
|
||||
二维以上部分是较为通用的,虽然计算模型是基于 Cube N 维的,但组件可以通过标准轴进行多维度展开,或者说下钻来实现类似效果。对于折线图来说,轴的含义有限,可以用分面的方式展示多维数据。当然也有一些组件只适合展示特定维度数量的数据。
|
||||
|
||||
#### 大数据性能优化
|
||||
|
||||
可视化组件特别需要关注性能优化,因为 BI 查询出的数据量可能非常大,特别是多层下钻或基于地理的数据。
|
||||
|
||||
技术手段包括 GPU 渲染、缓存 canvas、多线程运算等,业务手段包括数据抽样、按需渲染可视区域、限制数据条数等等。
|
||||
|
||||
#### 边界数据展示优化
|
||||
|
||||
永远不知道数据集会给出怎样的数据,因此 BI 边界情况特别多,可能点非常密集,也可能丢失一些数据导致渲染异常。图表组件需要利用避让算法将密集的数据打散或着色,目的是为了容易阅读,对于丢失的异常数据也要有保护性的补全机制。
|
||||
|
||||
#### 交互响应
|
||||
|
||||
包括上卷下钻、点选、圈选、高亮等交互操作,这些操作反馈到渲染引擎导致数据变化并将新的数据灌入图表组件。
|
||||
|
||||
业务逻辑上这些交互操作并不复杂,难点在使用的可视化库是否有这个能力,以及如何统一交互行为。
|
||||
|
||||
## 总结
|
||||
|
||||
BI 领域的四大方向:数据集、渲染引擎、数据模型与可视化都有许多可以做深的技术点,每一块都需要深入沉淀几年技术经验才能做好,需要大量优秀人才通力协作才有可能做好。
|
||||
|
||||
目前我们在阿里数据中台正在打造一款面向未来的优秀 BI 工具,如果 BI 领域让你觉得有挑战,随时欢迎你的加入,联系邮箱:ziyi.hzy@alibaba-inc.com
|
||||
|
||||
> 讨论地址是:[精读《前端与 BI》 · Issue #208 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/208)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,293 @@
|
||||
## 1 概述
|
||||
|
||||
本期精读的是有限状态机管理工具 [robot](https://github.com/matthewp/robot) 源码。
|
||||
|
||||
有限状态机是指有限个数的状态之间相互切换的数学模型,在业务与游戏开发中有限状态都很常见,包括发请求也是一种有限状态机的模型。
|
||||
|
||||
笔者将在简介中介绍这个库的使用方式,在精读中介绍实现原理,最后总结在业务中使用的价值。
|
||||
|
||||
## 2 简介
|
||||
|
||||
这个库的核心就是利用 `createMachine` 创建一个有限状态机:
|
||||
|
||||
```typescript
|
||||
import { createMachine, state, transition } from 'robot3';
|
||||
|
||||
const machine = createMachine({
|
||||
inactive: state(
|
||||
transition('toggle', 'active')
|
||||
),
|
||||
active: state(
|
||||
transition('toggle', 'inactive')
|
||||
)
|
||||
});
|
||||
|
||||
export default machine;
|
||||
```
|
||||
|
||||
如上图所示,我们创建了一个有限状态机 `machine`,包含了两种状态:`inactive` 与 `active`,并且可以通过 `toggle` 动作在两种状态间做切换。
|
||||
|
||||
与 React 结合则有 [react-robot](https://github.com/matthewp/react-robot):
|
||||
|
||||
```tsx
|
||||
import { useMachine } from 'react-robot';
|
||||
import React from 'react';
|
||||
import machine from './machine'
|
||||
|
||||
function App() {
|
||||
const [current, send] = useMachine(machine);
|
||||
|
||||
return (
|
||||
<button type="button" onClick={() => send('toggle')}>
|
||||
State: {current.name}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
通过 `useMachine` 拿到的 `current.name` 表示当前状态值,`send` 用来发送改变状态的指令。
|
||||
|
||||
至于为什么要用有限状态机管理工具,官方文档举了个例子 - 点击编辑后进入编辑态,点击保存后返回原始状态的例子:
|
||||
|
||||

|
||||
|
||||
点击 Edit 按钮后,将进入下图的状态,点击 Save 后如果输入的内容校验通过保存后再回到初始状态:
|
||||
|
||||

|
||||
|
||||
如果不用有限状态机,我们首先会创建两个变量存储是否处于编辑态,以及当前输入文本是什么:
|
||||
|
||||
```js
|
||||
let editMode = false;
|
||||
let title = '';
|
||||
```
|
||||
|
||||
如果再考虑和后端的交互,就会增加三个状态 - 保存中、校验、保存是否成功:
|
||||
|
||||
```js
|
||||
let editMode = false;
|
||||
let title = '';
|
||||
let saving = false;
|
||||
let validating = false;
|
||||
let saveHadError = false;
|
||||
```
|
||||
|
||||
就算使用 React、Vue 等框架数据驱动 UI,我们还是免不了对复杂状态进行管理。如果使用有限状态机实现,将是这样的:
|
||||
|
||||
```js
|
||||
import { createMachine, guard, immediate, invoke, state, transition, reduce } from 'robot3';
|
||||
|
||||
const machine = createMachine({
|
||||
preview: state(
|
||||
transition('edit', 'editMode',
|
||||
// Save the current title as oldTitle so we can reset later.
|
||||
reduce(ctx => ({ ...ctx, oldTitle: ctx.title }))
|
||||
)
|
||||
),
|
||||
editMode: state(
|
||||
transition('input', 'editMode',
|
||||
reduce((ctx, ev) => ({ ...ctx, title: ev.target.value }))
|
||||
),
|
||||
transition('cancel', 'cancel'),
|
||||
transition('save', 'validate')
|
||||
),
|
||||
cancel: state(
|
||||
immediate('preview',
|
||||
// Reset the title back to oldTitle
|
||||
reduce(ctx => ({ ...ctx, title: ctx.oldTitle })
|
||||
)
|
||||
),
|
||||
validate: state(
|
||||
// Check if the title is valid. If so go
|
||||
// to the save state, otherwise go back to editMode
|
||||
immediate('save', guard(titleIsValid)),
|
||||
immediate('editMode')
|
||||
)
|
||||
save: invoke(saveTitle,
|
||||
transition('done', 'preview'),
|
||||
transition('error', 'error')
|
||||
),
|
||||
error: state(
|
||||
// Should we provide a retry or...?
|
||||
)
|
||||
});
|
||||
```
|
||||
|
||||
其中 `immediate` 表示直接跳到下一个状态,`reduce` 则可以对状态机内部数据进行拓展。比如 `preview` 返回了 `oldTitle`,那么 `cancle` 时就可以通过 `ctx.oldTitle` 拿到;`invoke` 表示调用第一个函数后,再执行 `state`。
|
||||
|
||||
通过上面的代码我们可以看到使用状态机的好处:
|
||||
|
||||
1. 状态清晰,先罗列出某个业务逻辑的全部状态,避免遗漏。
|
||||
2. 状态转换安全。比如 `preview` 只能切换到 `edit` 状态,这样就算在错误的状态发错指令也不会产生异常情况。
|
||||
|
||||
## 3 精读
|
||||
|
||||
[robot](https://github.com/matthewp/robot) 重要的函数有 `createMachine, state, transition, immediate`,下面一一拆解说明。
|
||||
|
||||
### createMachine
|
||||
|
||||
[createMachine](https://github.com/matthewp/robot/blob/master/machine.js#L122) 表示创建状态机:
|
||||
|
||||
```js
|
||||
export function createMachine(current, states, contextFn = empty) {
|
||||
if(typeof current !== 'string') {
|
||||
contextFn = states || empty;
|
||||
states = current;
|
||||
current = Object.keys(states)[0];
|
||||
}
|
||||
if(d._create) d._create(current, states);
|
||||
return create(machine, {
|
||||
context: valueEnumerable(contextFn),
|
||||
current: valueEnumerable(current),
|
||||
states: valueEnumerable(states)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,如果传递了一个对象,通过 `Object.keys(states)[0]` 拿到第一个状态作为当前状态(标记在 `current`),最终将保存三个属性:
|
||||
|
||||
- `context` 当前状态机内部属性,初始化是空的。
|
||||
- `current` 当前状态。
|
||||
- `states` 所有状态,也就是 `createMachine` 传递的第一个参数。
|
||||
|
||||
再看 `create` 函数:
|
||||
|
||||
```js
|
||||
let create = (a, b) => Object.freeze(Object.create(a, b));
|
||||
```
|
||||
|
||||
也就是创建了一个不修改的对象作为状态机。
|
||||
|
||||
这个是 `machine` 对象:
|
||||
|
||||
```js
|
||||
let machine = {
|
||||
get state() {
|
||||
return {
|
||||
name: this.current,
|
||||
value: this.states[this.current]
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
也就是说,状态机内部的状态管理是通过对象完成的,并提供了 `state()` 函数拿到当前的状态名和状态值。
|
||||
|
||||
### state
|
||||
|
||||
[state](https://github.com/matthewp/robot/blob/master/machine.js#L70) 用来描述状态支持哪些转换:
|
||||
|
||||
```js
|
||||
export function state(...args) {
|
||||
let transitions = filter(transitionType, args);
|
||||
let immediates = filter(immediateType, args);
|
||||
let desc = {
|
||||
final: valueEnumerable(args.length === 0),
|
||||
transitions: valueEnumerable(transitionsToMap(transitions))
|
||||
};
|
||||
if(immediates.length) {
|
||||
desc.immediates = valueEnumerable(immediates);
|
||||
desc.enter = valueEnumerable(enterImmediate);
|
||||
}
|
||||
return create(stateType, desc);
|
||||
}
|
||||
```
|
||||
|
||||
`transitions` 与 `immediates` 表示从 `args` 里拿到 `transition` 或 `immediate` 的结果。
|
||||
|
||||
方法是通过如下方式定义 `transition` 与 `immediate`:
|
||||
|
||||
```js
|
||||
export let transition = makeTransition.bind(transitionType);
|
||||
export let immediate = makeTransition.bind(immediateType, null);
|
||||
|
||||
function filter(Type, arr) {
|
||||
return arr.filter(value => Type.isPrototypeOf(value));
|
||||
}
|
||||
```
|
||||
|
||||
**那么如果一个函数是通过 `immediate` 创建的,就可以通过 `immediateType.isPrototypeOf()` 的校验,此方法适用范围很广,在任何库里都可以用来校验拿到对应函数创建的对象。**
|
||||
|
||||
如果参数数量为 0,表示这个状态是最终态,无法进行转换。**最后通过 `create` 创建一个对象,这个对象就是状态的值**。
|
||||
|
||||
### transition
|
||||
|
||||
[transition](https://github.com/matthewp/robot/blob/master/machine.js#L53) 是写在 `state` 中描述当前状态可以如何变换的函数,其实际函数是 `makeTransistion`:
|
||||
|
||||
```js
|
||||
function makeTransition(from, to, ...args) {
|
||||
let guards = stack(filter(guardType, args).map(t => t.fn), truthy, callBoth);
|
||||
let reducers = stack(filter(reduceType, args).map(t => t.fn), identity, callForward);
|
||||
return create(this, {
|
||||
from: valueEnumerable(from),
|
||||
to: valueEnumerable(to),
|
||||
guards: valueEnumerable(guards),
|
||||
reducers: valueEnumerable(reducers)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
由于:
|
||||
|
||||
```js
|
||||
export let transition = makeTransition.bind(transitionType);
|
||||
export let immediate = makeTransition.bind(immediateType, null);
|
||||
```
|
||||
|
||||
可见 `from` 为 `null` 即表示立即转换到状态 `to`。`transition` 最终返回一个对象,其中 `guards` 是从 `transition` 或 `immediate` 参数中找到的,由 `guards` 函数创建的对象,当这个对象回调函数执行成功时此状态才生效。
|
||||
|
||||
`...args` 对应 `transition('toggle', 'active')` 或 `immediate('save', guard(titleIsValid))`,而 `stack(filter(guardType, args).map(t => t.fn), truthy, callBoth)` 这句话就是从 `...args` 中寻找是否有 `guards`,`reducers` 同理。
|
||||
|
||||
最后看看状态是如何改变的,设置状态改变的函数是 [transitionTo](https://github.com/matthewp/robot/blob/master/machine.js#L136):
|
||||
|
||||
```js
|
||||
function transitionTo(service, fromEvent, candidates) {
|
||||
let { machine, context } = service;
|
||||
for(let { to, guards, reducers } of candidates) {
|
||||
if(guards(context)) {
|
||||
service.context = reducers.call(service, context, fromEvent);
|
||||
|
||||
let original = machine.original || machine;
|
||||
let newMachine = create(original, {
|
||||
current: valueEnumerable(to),
|
||||
original: { value: original }
|
||||
});
|
||||
|
||||
let state = newMachine.state.value;
|
||||
return state.enter(newMachine, service, fromEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,如果存在 `guards`,则需要在 `guards` 执行返回成功时才可以正确改变状态。同时 `reducers` 可以修改 `context` 也在 `service.context = reducers.call(service, context, fromEvent);` 这一行体现了出来。最后通过生成一个新的状态机,并将 `current` 标记为 `to`。
|
||||
|
||||
最后我们看 `state.enter` 这个函数,这个函数在 [state](https://github.com/matthewp/robot/blob/master/machine.js#L79) 函数中有定义,其本质是继承了 `stateType`:
|
||||
|
||||
```js
|
||||
let stateType = { enter: identity };
|
||||
```
|
||||
|
||||
而 `identity` 这个函数就是立即执行函数:
|
||||
|
||||
```js
|
||||
let identity = a => a;
|
||||
```
|
||||
|
||||
因此相当于返回了新的状态机。
|
||||
|
||||
## 4 总结
|
||||
|
||||
有限状态机相比普通业务描述,其实是增加了一些状态间转化的约束来达到优化状态管理的目的,并且状态描述也会更规范一些,在业务中具有一定的实用性。
|
||||
|
||||
当然并不是所有业务都适用有限状态机,因为新框架还是有一些学习成本要考虑。最后通过源码的学习,我们又了解到一些新的框架级小技巧,可以灵活应用到自己的框架中。
|
||||
|
||||
> 讨论地址是:[精读《robot 源码 - 有限状态机》 · Issue #209 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/209)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,486 @@
|
||||
## 1 引言
|
||||
|
||||
在写这次精读之前,我想谈谈前端精读可以为读者带来哪些价值,以及如何评判这些价值。
|
||||
|
||||
前端精读已经写到第 123 篇了,大家已经不必担心它突然停止更新,因为我已养成每周写一篇文章的习惯,而读者也养成了每周看一篇的习惯。所以我想说的其实是一种更有生命力的自媒体运作方式,定期更新。一个定期更新的专栏比一个不不定期更新的专栏更有活力,也更受读者喜爱,因为读者能看到文章之间的联系,跟随作者一起成长。个人学习也是如此,养成定期学习的习惯,比在培训班突击几个月更有用,学会在生活中规律的学习,甚至好过读几年名牌大学。
|
||||
|
||||
前端精读想带给读者的不仅是一篇篇具体的内容和知识,知识是无穷无尽的,几万篇文章也说不完,但前端精读一直沿用了“引言-概述-精读-总结”这套学习模式,无论是前端任何领域的问题,还是对人生和世界的思考都可以套用,希望能为读者提供一套学习思维框架,让你能学习到如何找到好的文章,以及如何解读它。
|
||||
|
||||
至今已经选择了许多源码解读的题材,与培训思维的源码解读不同,我希望你不要带着面试的目的学习源码,因为这样会让你只局限在 react、vue 这种热门的框架上。前端精读选取的框架类型之所以广泛,是希望你能静下心来,吸取不同框架风格与作者的优势,培养一种优雅编码的气质。
|
||||
|
||||
进入正题,这次选择的文章 [《用 Babel 创造自定义 JS 语法》](https://lihautan.com/creating-custom-javascript-syntax-with-babel/) 也是培养编码气质的一类文章,虽然对你实际工作用处不大,但这篇文章可以培养几个程序员梦寐以求的能力:深入理解 Babel、深入理解框架拓展机制。理解一个复杂系统或培养框架思维不是一朝一夕的,但持续阅读这种文章可以让你越来越接近掌握它。
|
||||
|
||||
之所以选择 Babel,是因为 Babel 处理的一直是语法树相关的底层逻辑,编译原理是程序世界的基座之一,拥有很大的学习价值。所以我们的目的并不是像文章标题说的 - 创造一个自定义 JS 语法,因为你创造的语法只会让 JS 复杂体系更加混乱,但可以让你理解 Babel 解析标准 JS 语法的原理,以及看待新语法提案时,拥有从实现层面思考的能力。
|
||||
|
||||
最后,不必多说,能重温 Babel 经典的插件机制,你可以发现 Babel 的插件拓展机制和 Antrl4 很像,在设计业务模块拓展方案时也可以作为参考。
|
||||
|
||||
## 2 概述
|
||||
|
||||
我们要利用 Babel 实现 `function @@` 的新语法,用 `@@` 装饰的函数会自动柯里化:
|
||||
|
||||
```js
|
||||
// '@@' makes the function `foo` curried
|
||||
function @@ foo(a, b, c) {
|
||||
return a + b + c;
|
||||
}
|
||||
console.log(foo(1, 2)(3)); // 6
|
||||
```
|
||||
|
||||
可以看到,`function @@ foo` 描述的函数 `foo` 支持 `foo(1, 2)(3)` 这种柯里化调用。
|
||||
|
||||
实现方式分为两步:
|
||||
|
||||
1. Fork babel 源码。
|
||||
2. 创建一个 babel 转换器插件。
|
||||
|
||||
不要畏惧这些步骤,“如果你读完了这篇文章,你将成为同事眼中的 Babel 大神” - 原文。
|
||||
|
||||
首先 Fork babel 源码到本地,执行下面的命令可以初始化并编译 babel:
|
||||
|
||||
```bash
|
||||
$ make bootstrap
|
||||
$ make build
|
||||
```
|
||||
|
||||
babel 使用 [Makefile](https://opensource.com/article/18/8/what-how-makefile) 执行编译命令,并且采用 monorepo 管理,我们这次要关心的是 `package/babel-parser` 这个模块。
|
||||
|
||||
### 词法
|
||||
|
||||
首先要了解词法知识,更详细的可以阅读原文或精读之前的一篇系列文章:[精读《词法分析》](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)。
|
||||
|
||||
要解析语法,首先要进行词法分析。任何语法输入都是一个字符串,比如 `function @@ foo(a, b, c)`,词法分析就是要将这个长度为 24 的字符拆分为一个个有语义的单词片段:`function` `@@` `foo` `(` `a` ..
|
||||
|
||||
由于 `@@` 是我们创造的语法,所以我们第一个任务就是让 babel 词法分析可以识别它。
|
||||
|
||||
下面是 `package/babel-parser` 的文件结构:
|
||||
|
||||
```text
|
||||
- src/
|
||||
- tokenizer/
|
||||
- parser/
|
||||
- plugins/
|
||||
- jsx/
|
||||
- typescript/
|
||||
- flow/
|
||||
- ...
|
||||
- test/
|
||||
```
|
||||
|
||||
可以看到,分为词法分析 `tokenizer`,语法分析 `parser`,以及支持一些特殊语法的插件,以及测试用例 `test`。
|
||||
|
||||
推荐使用 **Test-driven development (TDD) - 测试驱动开发的方式**,就是先写测试用例,再根据测试用例开发。这种开发方式在后端或者 babel 这种底层框架很常见,因为 TDD 方式开发的逻辑能保证测试用例 100% 覆盖,同时先看测试用例也是个很好的切面编程思维。
|
||||
|
||||
```js
|
||||
// packages/babel-parser/test/curry-function.js
|
||||
|
||||
import { parse } from '../lib';
|
||||
|
||||
function getParser(code) {
|
||||
return () => parse(code, { sourceType: 'module' });
|
||||
}
|
||||
|
||||
describe('curry function syntax', function() {
|
||||
it('should parse', function() {
|
||||
expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
可以利用 jest 直接测试这段代码:
|
||||
|
||||
```bash
|
||||
BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/c
|
||||
```
|
||||
|
||||
结果会出现如下报错:
|
||||
|
||||
```text
|
||||
SyntaxError: Unexpected token (1:9)
|
||||
|
||||
at Parser.raise (packages/babel-parser/src/parser/location.js:39:63)
|
||||
at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16)
|
||||
at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18)
|
||||
at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23)
|
||||
at Parser.parseIdentifier (packages/babel-pars
|
||||
```
|
||||
|
||||
第 9 个字符就是 `@`,说明程序现在还不支持函数前面的 `@` 解析。我们还可以在错误堆栈中找到报错位置,并把当前 Token 与下一个 Token 打印出来:
|
||||
|
||||
```js
|
||||
// packages/babel-parser/src/parser/expression.js
|
||||
|
||||
parseIdentifierName(pos: number, liberal?: boolean): string {
|
||||
if (this.match(tt.name)) {
|
||||
// ...
|
||||
} else {
|
||||
console.log(this.state.type); // current token
|
||||
console.log(this.lookahead().type); // next token
|
||||
throw this.unexpected();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`this.state.type` 代表当前 Token,`this.lookahead().type` 表示下一个 Token。`lookahead` 是词法分析的专有词,表示向后查看。打印之后,我们会发现输出了两个 `@` Token:
|
||||
|
||||
```js
|
||||
TokenType {
|
||||
label: '@',
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
下一步,我们需要让 babel 词法分析识别 `@@` 这个 Token。首先需要注册这个 Token:
|
||||
|
||||
```js
|
||||
// packages/babel-parser/src/tokenizer/types.js
|
||||
|
||||
export const types: { [name: string]: TokenType } = {
|
||||
// ...
|
||||
at: new TokenType('@'),
|
||||
atat: new TokenType('@@'),
|
||||
};
|
||||
```
|
||||
|
||||
注册了之后,我们要在遍历 Token 时增加判断 “如果当前字符是 `@` 且下一个字符也是 `@`,则整体构成了 `@@` Token 并且光标向后移动两格”:
|
||||
|
||||
```js
|
||||
// packages/babel-parser/src/tokenizer/index.js
|
||||
|
||||
getTokenFromCode(code: number): void {
|
||||
switch (code) {
|
||||
// ...
|
||||
case charCodes.atSign:
|
||||
// if the next character is a `@`
|
||||
if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) {
|
||||
// create `tt.atat` instead
|
||||
this.finishOp(tt.atat, 2);
|
||||
} else {
|
||||
this.finishOp(tt.at, 1);
|
||||
}
|
||||
return;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
再次运行测试文件,输出变成了:
|
||||
|
||||
```js
|
||||
// current token
|
||||
TokenType {
|
||||
label: '@@',
|
||||
// ...
|
||||
}
|
||||
|
||||
// next token
|
||||
TokenType {
|
||||
label: 'name',
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
到这一步,已经能正确解析 `@@` Token 了。
|
||||
|
||||
## 语法
|
||||
|
||||
词法已经可以将 `@@` 解析为 `atat` Token,下一步我们就要利用这个 Token,让生成的 AST 结构中包含柯里化函数的信息,并利用 babel 插件在解析时实现柯里化功能。
|
||||
|
||||
首先我们可以在 [Babel AST explorer](https://lihautan.com/babel-ast-explorer/#?eyJiYWJlbFNldHRpbmdzIjp7InZlcnNpb24iOiI3LjYuMCJ9LCJ0cmVlU2V0dGluZ3MiOnsiaGlkZUVtcHR5Ijp0cnVlLCJoaWRlTG9jYXRpb24iOnRydWUsImhpZGVUeXBlIjp0cnVlfSwiY29kZSI6ImZ1bmN0aW9uICogZm9vKCkge30ifQ==) 看到 AST 解析的结构,我们拿 generator 函数测试,因为这个函数结构与柯里化函数类似:
|
||||
|
||||

|
||||
|
||||
可以看到,babel 通过 `generator` `async` 属性来标识函数是否为 generator 或者 async 函数。同理,增加一个 `curry` 属性就可以实现第一步了:
|
||||
|
||||

|
||||
|
||||
要实现如上效果,只需在词法分析 `parser/statement` 文件的 `parseFunction` 处新增 `atat` 解析即可:
|
||||
|
||||
```js
|
||||
// packages/babel-parser/src/parser/statement.js
|
||||
|
||||
export default class StatementParser extends ExpressionParser {
|
||||
// ...
|
||||
parseFunction<T: N.NormalFunction>(
|
||||
node: T,
|
||||
statement?: number = FUNC_NO_FLAGS,
|
||||
isAsync?: boolean = false
|
||||
): T {
|
||||
// ...
|
||||
node.generator = this.eat(tt.star);
|
||||
node.curry = this.eat(tt.atat);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`eat` 是吃掉的意思,实际上可以理解为吞掉这个 Token,这样做有两个效果:1. 为函数添加了 `curry` 属性 2. 吞掉了 `@@` 标识,保证所有 Token 都被识别是 AST 解析正确的必要条件。
|
||||
|
||||
关于递归下降语法分析的更多知识,可以参考 [精读《手写 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),或者阅读原文。
|
||||
|
||||
我们再次执行测试函数,发现测试通过了,一切都在预料中。
|
||||
|
||||
## babel 插件
|
||||
|
||||
现在我们得到了标记了 `curry` 的 AST,那么最后需要一个 babel 解析插件,实现柯里化。
|
||||
|
||||
首先我们通过修改 babel 源码的方式实现的效果,是可以转化为自定义 babel parser 插件的:
|
||||
|
||||
```js
|
||||
// babel-plugin-transformation-curry-function.js
|
||||
|
||||
import customParser from './custom-parser';
|
||||
|
||||
export default function ourBabelPlugin() {
|
||||
return {
|
||||
parserOverride(code, opts) {
|
||||
return customParser.parse(code, opts);
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
这样就可以实现修改 babel 源码一样的效果,这也是做框架常用的插件机制。
|
||||
|
||||
其次我们要理解如何实现柯里化。柯里化可以通过柯里函数包装后实现:
|
||||
|
||||
```js
|
||||
function currying(fn) {
|
||||
const numParamsRequired = fn.length;
|
||||
function curryFactory(params) {
|
||||
return function (...args) {
|
||||
const newParams = params.concat(args);
|
||||
if (newParams.length >= numParamsRequired) {
|
||||
return fn(...newParams);
|
||||
}
|
||||
return curryFactory(newParams);
|
||||
}
|
||||
}
|
||||
return curryFactory([]);
|
||||
}
|
||||
|
||||
// from
|
||||
function @@ foo(a, b, c) {
|
||||
return a + b + c;
|
||||
}
|
||||
|
||||
// to
|
||||
const foo = currying(function foo(a, b, c) {
|
||||
return a + b + c;
|
||||
})
|
||||
```
|
||||
|
||||
柯里化函数通过构造参数数量相关的递归,当参数传入不足时返回一个新函数,并持久化之前传入的参数,最后当参数齐全后一次性调用函数。
|
||||
|
||||
我们需要做的是,将 `@@ foo` 解析为 `currying()` 函数包裹后的新函数。
|
||||
|
||||
下面就是我们熟悉的 babel 插件部分了:
|
||||
|
||||
```js
|
||||
// babel-plugin-transformation-curry-function.js
|
||||
|
||||
export default function ourBabelPlugin() {
|
||||
return {
|
||||
// ...
|
||||
visitor: {
|
||||
FunctionDeclaration(path) {
|
||||
if (path.get('curry').node) {
|
||||
// const foo = curry(function () { ... });
|
||||
path.node.curry = false;
|
||||
path.replaceWith(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier(path.get('id.name').node),
|
||||
t.callExpression(t.identifier('currying'), [
|
||||
t.toExpression(path.node),
|
||||
])
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`FunctionDeclaration` 就是 AST 的 visit 钩子,这个钩子在执行到函数时被触发,我们通过 `path.get('curry')` 拿到 **柯里化函数**,并利用 `replaceWith` 将这个函数构造为一个被 `currying` 函数包裹的新函数。
|
||||
|
||||
剩下最后一个问题:`currying` 函数源码放在哪里。
|
||||
|
||||
第一种方式,创建类似 `babel-plugin-transformation-curry-function` 这样的插件,在 babel 解析时将 `currying` 函数注册到全局,这是全局思维的方案。
|
||||
|
||||
第二种是模块化解决方案,创建一个自定义的 `@babel/helpers`,注册一个 `currying` 标识:
|
||||
|
||||
```js
|
||||
// packages/babel-helpers/src/helpers.js
|
||||
helpers.currying = helper("7.6.0")`
|
||||
export default function currying(fn) {
|
||||
const numParamsRequired = fn.length;
|
||||
function curryFactory(params) {
|
||||
return function (...args) {
|
||||
const newParams = params.concat(args);
|
||||
if (newParams.length >= numParamsRequired) {
|
||||
return fn(...newParams);
|
||||
}
|
||||
return curryFactory(newParams);
|
||||
}
|
||||
}
|
||||
return curryFactory([]);
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
在 visit 函数使用 `addHelper` 方式拿到 `currying`:
|
||||
|
||||
```js
|
||||
path.replaceWith(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier(path.get('id.name').node),
|
||||
t.callExpression(this.addHelper("currying"), [
|
||||
t.toExpression(path.node),
|
||||
])
|
||||
),
|
||||
])
|
||||
);
|
||||
```
|
||||
|
||||
这样在 babel 转换后,就会自动 import helper,并引用 helper 中导出的 `currying`。
|
||||
|
||||
最后原文末尾留下了一些延伸阅读内容,感兴趣的同学可以 [点击到原文](https://lihautan.com/creating-custom-javascript-syntax-with-babel/)。
|
||||
|
||||
## 3 精读
|
||||
|
||||
读完这篇文章,相信你不仅对 babel 插件有了更深刻的认识,而且还掌握了如何为 js 添加新语法这种黑魔法。
|
||||
|
||||
我来帮你从 babel 这篇文章总结一些编程模型和知识点,借助 babel 创造自定义语法的实例,加深对它们的理解。
|
||||
|
||||
### TDD
|
||||
|
||||
Test-driven development 即测试驱动的开发模式。
|
||||
|
||||
从文章的例子可以看出,创造一个新语法,可以先在测试用例先写上这个语法,通过执行测试命令通过报错堆栈一步步解决问题。这种方式开发可以让测试覆盖率更高,目的更专注,更容易保障代码质量。
|
||||
|
||||
### 联想编程
|
||||
|
||||
联想编程不属于任何编程模型,但从简介的思路来看,作者把 “为 babel 创建一个新 js 语法” 看作一种探案式探索过程,通过错误堆栈和代码阅读,一步一步通过合理联想实现最终目的。
|
||||
|
||||
在 AST 那一节,还借助了 [Babel AST explorer](https://lihautan.com/babel-ast-explorer/#?eyJiYWJlbFNldHRpbmdzIjp7InZlcnNpb24iOiI3LjYuMCJ9LCJ0cmVlU2V0dGluZ3MiOnsiaGlkZUVtcHR5Ijp0cnVlLCJoaWRlTG9jYXRpb24iOnRydWUsImhpZGVUeXBlIjp0cnVlfSwiY29kZSI6ImZ1bmN0aW9uICogZm9vKCkge30ifQ==) 工具查看 AST 结构,通过联想到 generator 函数找到类似的 AST 结构,并找到拓展 AST 的突破口。
|
||||
|
||||
随着解决问题的不同,联想方式也不同,如果能够举一反三,对不同场景都能合理的联想,才算是具备了技术专家的软素质。
|
||||
|
||||
### 词法、语法分析
|
||||
|
||||
词法、语法分析属于编译原理的知识,理解词法拆分、递归下降,可以帮助你技术走的更深。
|
||||
|
||||
不论是 Babel 插件的使用、还是 Babel 增加自定义 JS 语法,都要具备基本编译原理知识。编译原理知识还能帮助你开发在线编辑器,做智能语法提示等等。
|
||||
|
||||
### 插件机制
|
||||
|
||||
如下是 babel 自定义 parser 的插件拓展方式:
|
||||
|
||||
```js
|
||||
export default function ourBabelPlugin() {
|
||||
return {
|
||||
parserOverride(code, opts) {
|
||||
return customParser.parse(code, opts);
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
这只是插件拓展的一种,有申明式,也有命令式;有用 JS 书写的,也有用 JSON 书写的。babel 选择了通过对象方式拓展,是比较适合对 AST 结构统一处理的。
|
||||
|
||||
做框架首先要确定接口规范,比如 parser,先按照接口规范实现一套官方解析,对接时按照接口进行对接,就可以自然而然被用户自定义插件替代了。
|
||||
|
||||
可以参考的文章: [精读《插件化思维》](https://github.com/dt-fe/weekly/blob/v2/053.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8F%92%E4%BB%B6%E5%8C%96%E6%80%9D%E7%BB%B4%E3%80%8B.md)
|
||||
|
||||
### 柯里化
|
||||
|
||||
柯里化是面试经常考察的一个知识点,我们能学到的有两点:理解递归、理解如何将函数变成柯里化。
|
||||
|
||||
这里再拓展一下,我们还可以想到 JS 尾递归优化。如何快速写一个支持尾递归的函数?
|
||||
|
||||
```js
|
||||
const fn = tailCallOptimize(() => {
|
||||
if ( /* xxx */ ) {
|
||||
fn()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
通过封装 `tailCallOptimize` 函数,可以很方便的构造一个支持尾递归的函数,这个函数可以这么写:
|
||||
|
||||
```js
|
||||
export function tailCallOptimize<T>(f: T): T {
|
||||
let value: any;
|
||||
let active = false;
|
||||
const accumulated: any[] = [];
|
||||
return function accumulator(this: any) {
|
||||
accumulated.push(arguments);
|
||||
if (!active) {
|
||||
active = true;
|
||||
while (accumulated.length) {
|
||||
value = (f as any).apply(this, accumulated.shift());
|
||||
}
|
||||
active = false;
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
感兴趣的读者可以在评论里解释一下这个函数的原理。
|
||||
|
||||
### AST visit
|
||||
|
||||
遍历 AST 树常采用的方案是做一个遍历器 visitor,所以在遍历过程中进行拓展常采用 babel 这种方式:
|
||||
|
||||
```js
|
||||
return {
|
||||
// ...
|
||||
visitor: {
|
||||
FunctionDeclaration(path) {
|
||||
if (path.get('curry').node) {
|
||||
// const foo = curry(function () { ... });
|
||||
path.node.curry = false;
|
||||
path.replaceWith(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier(path.get('id.name').node),
|
||||
t.callExpression(t.identifier('currying'), [
|
||||
t.toExpression(path.node),
|
||||
])
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
`visitor` 下每一个 key 名都是遍历过程中的拓展点,比如上面的例子,我们可以对函数定义位置进行拓展和改写。
|
||||
|
||||
### 内置函数注册
|
||||
|
||||
babel 提供了两种内置函数注册方式,一种类似 polyfill,在全局注册 window 级的变量,另一种是模块化的方式。
|
||||
|
||||
除此之外,可以学习的是 babel 通过 `this.addHelper("currying")` 这种插件拓展方式,在编译后会自动从 helper 引入对应的模块,前提是 `@babel/helper` 需要注册 `currying` 这个 helper。
|
||||
|
||||
babel 将编译过程隐藏了起来,通过一些高度封装的函数调用,以较为语义化方式书写插件,这样写出来的代码也容易理解。
|
||||
|
||||
## 4 总结
|
||||
|
||||
《用 Babel 创造自定义 JS 语法》这篇文章虽然说的是 babel 相关知识,但可以从中提取到许多通用知识,这就是现在还去理解 babel 的原因。
|
||||
|
||||
从某个功能点为切面,走一遍框架的完整流程是一种高效的进阶学习方式,如果你也有看到类似这样的文章,欢迎推荐出来。
|
||||
|
||||
> 讨论地址是:[精读《用 Babel 创造自定义 JS 语法》 · Issue #210 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/210)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,372 @@
|
||||
## 1 引言
|
||||
|
||||
Flex 与 Grid 相比就像功能键盘和触摸屏。触摸屏的控制力相比功能键盘来说就像是降维打击,因为功能键盘只能上下左右控制(x、y 轴),而触摸屏打破了布局障碍,直接从(z 轴)触达,这样 **无论 UI 内部布局再复杂,都可以通过 touch 直接定位。**
|
||||
|
||||
Flex 是一维布局方式,我们需要不断嵌套 Div 才能形成复杂结构,而一旦布局产生了变化,原有嵌套结构如果不能 “兼容变化” 到新结构,代码就需要重构。而 Grid 就像触摸屏一样,可以二维布局,即便布局方式做了翻天覆地的调整,也仅需少量修改就能适配。
|
||||
|
||||
这就是这次精读 [用 css grid 重新思考布局](https://www.freecodecamp.org/news/css-grid-changes-how-we-can-think-about-structuring-our-content/) 的原因,理解这个革命性布局技术给布局,甚至代码逻辑组织带来的变化。
|
||||
|
||||
## 2 概述
|
||||
|
||||
作者首先抛出了 Flex 的问题,其实是 `block` `float` `flex` 这三种布局模式的通病:
|
||||
|
||||
- 布局结构由 Div 层级结构描述,导致 Div 层级复杂且遇到结构变更时难以维护。
|
||||
- 定制能力弱。Flex 布局有一些不受控制的智能设定,比如宽度 50% 的子元素会被同级元素挤到 50% 以下,这种智能化在某些场景是需要的,但由于没有提供像 Grid 的 `minmax` 之类的 API,所以定制型不足。
|
||||
|
||||

|
||||
|
||||
举个例子,上图的结构用 Flex 描述可能是这样的:
|
||||
|
||||
```html
|
||||
<div class="card">
|
||||
<div class="profile-sidebar">
|
||||
<img src="https://i.pravatar.cc/125?image=3" alt="" class="profile-img" />
|
||||
<ul class="social-list">
|
||||
<li>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-dribbble-square"></i
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-facebook-square"></i
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-twitter-square"></i
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="profile-body">
|
||||
<h2 class="profile-name">Ramsey Harper</h2>
|
||||
<p class="profile-position">Graphic Designer</p>
|
||||
<p class="profile-info">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere a tempore,
|
||||
dignissimos odit accusantium repellat quidem, sit molestias dolorum
|
||||
placeat quas debitis ipsum esse rerum?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
利用 HTML 嵌套结构,我们将图形纵向分成两大块,然后在每块内部继续嵌套划分布局,这是最经典的布局行为了。
|
||||
|
||||

|
||||
|
||||
样式文件里,我们需要对每层布局进行描述,同时支持多分辨率弹性布局,包括顶层 `card` 容器在内的一些样式需要做一定调整:
|
||||
|
||||
```scss
|
||||
.card {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 600px;
|
||||
background: #005e9b;
|
||||
flex-basis: 250px;
|
||||
color: white;
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
font-weight: 300;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.profile-sidebar {
|
||||
margin-right: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
letter-spacing: 1px;
|
||||
font-size: 2rem;
|
||||
margin: 0.75em 0 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.profile-name::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 2em;
|
||||
height: 1px;
|
||||
background: #5bcbf0;
|
||||
margin: 0.5em auto 0.65em;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.profile-position {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 3px;
|
||||
margin: 0 0 2em;
|
||||
line-height: 1;
|
||||
color: #5bcbf0;
|
||||
}
|
||||
|
||||
.profile-img {
|
||||
max-width: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.social-list {
|
||||
list-style: none;
|
||||
justify-content: space-evenly;
|
||||
display: flex;
|
||||
min-width: 125px;
|
||||
max-width: 175px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
color: #5bcbf0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.social-link:hover,
|
||||
.social-link:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bio {
|
||||
padding: 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) {
|
||||
.bio {
|
||||
text-align: left;
|
||||
max-width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.bio-title {
|
||||
color: #0090d1;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bio-body {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.profile {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) {
|
||||
.card {
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.profile-name::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
让我们看看 Grid 是怎么做的吧!Grid 有许多 API,我们重点看 `grid-template-areas` 这个属性,利用它,我们可以不关心模块的 HTML 结构,直接平铺方式描述:
|
||||
|
||||
```html
|
||||
<div class="card">
|
||||
<img src="https://i.pravatar.cc/125?image=3" alt="" class="profile-img" />
|
||||
<ul class="social-list">
|
||||
<li>
|
||||
<a href="#" class="social-link"><i class="fab fa-dribbble-square"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="social-link"><i class="fab fa-facebook-square"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="social-link"><i class="fab fa-twitter-square"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="profile-name">Ramsey Harper</h2>
|
||||
<p class="profile-position">Graphic Designer</p>
|
||||
<p class="profile-info">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere a tempore,
|
||||
dignissimos odit accusantium repellat quidem, sit molestias dolorum placeat
|
||||
quas debitis ipsum esse rerum?
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
可以看到,使用 Grid 可以将 UI 结构与 HTML 结构分离,HTML 结构仅描述包含关系,我们只需在样式文件中描述具体 UI 结构。
|
||||
|
||||
样式文件只截取 Grid 相关部分:
|
||||
|
||||
```scss
|
||||
.card {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 600px;
|
||||
background: #005e9b;
|
||||
flex-basis: 250px;
|
||||
color: white;
|
||||
padding: 2em;
|
||||
text-align: left;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-column-gap: 2em;
|
||||
grid-template-areas:
|
||||
"image name"
|
||||
"image position"
|
||||
"social description";
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
grid-area: name;
|
||||
}
|
||||
.profile-position {
|
||||
grid-area: position;
|
||||
}
|
||||
.profile-info {
|
||||
grid-area: description;
|
||||
}
|
||||
.profile-img {
|
||||
grid-area: image;
|
||||
}
|
||||
.social-list {
|
||||
grid-area: social;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,`grid-template-areas` 是进一步抽象的语法,将页面结构通过直观的文本描述,无论是理解还是修改都更为轻松。
|
||||
|
||||
这种描述方式适配不同分辨率下也具有优势,只要重组 `grid-template-areas` 即可:
|
||||
|
||||
```scss
|
||||
@media (min-width: 600px) {
|
||||
.card {
|
||||
text-align: left;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-template-areas:
|
||||
"image name"
|
||||
"image position"
|
||||
"social description";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
归根结底,Grid 通过二维结构描述,将子元素布局控制收到了父级,使布局描述更加直观。
|
||||
|
||||
最后作者也提到,Flex 依然有使用场景,即简单的一维结构,或者 `space-between` 等 Flex 独有语法的情况。因此推荐整体、复杂的二维布局采用 Grid,一维的简单布局采用 Flex。
|
||||
|
||||
## 3 精读
|
||||
|
||||
Grid 的布局思路给了我很多启发,HTML 结构与 UI 结构的分离有助于减少 DIV 的层级结构,使代码看上去更清晰。
|
||||
|
||||
也许有人会疑惑,Grid 无非将 HTML 布局部分功能挪到了 CSS,整体复杂度应该不变。其实,从 `grid-template-areas` 这个 API 可以看到,Grid 不仅仅将布局功能抽到 CSS 中,更是将布局描述进行了一层抽象,使代码更易维护。
|
||||
|
||||
### 抽象,再抽象
|
||||
|
||||
为什么 Grid 可以对布局进行抽象?因为 Grid 将二维结构都掌握在手中,得到了更大的布局能力,才能进一步将结构化语法抽象为字符串的描述。
|
||||
|
||||
抽象的好处是不言而喻的,你觉得一堆嵌套的 DIV 与下面的代码,哪个更易读呢?
|
||||
|
||||
```scss
|
||||
.card {
|
||||
grid-template-areas:
|
||||
"image name"
|
||||
"image position"
|
||||
"social description";
|
||||
}
|
||||
```
|
||||
|
||||
这就是抽象的好处,一般来说,代码抽象程度越高就越易读,越易维护。
|
||||
|
||||
再看一个 Chrome Grid 插件,将 Grid 可视化显示出来,并可以以 UI 方式进行调整:
|
||||
|
||||

|
||||
|
||||
UI 是对文本的再抽象,同时可以规避一些不可能存在的语法,比如:
|
||||
|
||||
```scss
|
||||
.card {
|
||||
grid-template-areas:
|
||||
"image name"
|
||||
"image position"
|
||||
"social image";
|
||||
}
|
||||
```
|
||||
|
||||
布局只能以凸多边形方式拓展,不可能分离,也不可能突然插入一个其他模块而变成凹多边形。因此 UI 可以将这个错误规避,并简化为横竖多条线的方式对 UI 进行划分,显然这种描述方式效率更高。
|
||||
|
||||
不得不说,Grid 以及图形化插件的探索,是布局领域的一大进步,是不断抽象的尝试,要解决的问题只有一个:如何提供一种更直观的描述 UI 的方式。
|
||||
|
||||
### 布局对模块化的影响
|
||||
|
||||
Grid 将布局方式提高了一个维度,会直接影响到 JS 模块化方式。
|
||||
|
||||
尤其是以 JSX 组织代码的情况下,一个模块等于 UI + JS,通过嵌套方式的布局会让我们更倾向于站在 UI 视角划分模块。
|
||||
|
||||

|
||||
|
||||
比如对于上图模块,如果用 Flex 方式布局,我们可能会首先创建模块 X 作为左侧容器,子元素是 A 和 B,创建模块 Y 作为右侧容器,子元素是 C 以及新容器 Z,Z 容器的子元素是 D 和 E。
|
||||
|
||||
如果你的第一印象是这么组织代码,不得不承认模块化会受到布局方式的影响。虽然许多时候这样划分是正确的,但当这 5 个模块各自没有关联时,我们创建的容器 X、Y、Z 就失去了复用性,在新的组合场景我们又要重新组合一遍。
|
||||
|
||||
但是在 Grid 语法中,我们不需要 X、Y、Z,只需要用 [css grid generator](https://cssgrid-generator.netlify.com/) 按照上图的方式拖拖拽拽即可自动生成如下布局代码:
|
||||
|
||||
```scss
|
||||
.parent {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr repeat(2, 1fr);
|
||||
grid-template-rows: repeat(5, 1fr);
|
||||
grid-column-gap: 0px;
|
||||
grid-row-gap: 0px;
|
||||
}
|
||||
|
||||
.div1 {
|
||||
grid-area: 1 / 1 / 3 / 2;
|
||||
}
|
||||
.div2 {
|
||||
grid-area: 3 / 1 / 6 / 2;
|
||||
}
|
||||
.div3 {
|
||||
grid-area: 1 / 2 / 2 / 4;
|
||||
}
|
||||
.div4 {
|
||||
grid-area: 2 / 2 / 6 / 3;
|
||||
}
|
||||
.div5 {
|
||||
grid-area: 2 / 3 / 6 / 4;
|
||||
}
|
||||
```
|
||||
|
||||
其实 `grid-template-columns` `grid-template-rows` 组合起来使用比 `grid-template-areas` 更强大,但是纯代码方式描述没有 `grid-template-areas` 直观,可是配合一些可视化系统就非常直观了:
|
||||
|
||||

|
||||
|
||||
将 A ~ E 这 5 个模块布局抽出来后,它们之间的关系就打平了,我们可以完全从逻辑视角审视如何做模块化了。
|
||||
|
||||
## 4 总结
|
||||
|
||||
CSS Grid 本质上是一种二维布局的语法,相比 [Block](https://www.w3schools.com/Css/css_inline-block.asp)、[Flex](https://www.w3schools.com/Css/css3_flexbox.asp) 等一维布局方案,多了一个维度可以同时从行与列角度定义布局,因此派生出 `grid-template-areas` 等语法,整体上更内聚更直观,抽象度也更高了。
|
||||
|
||||
理解了这些也就理解了布局未来的发展方向,**让布局与 Dom 分离** 一直是前端的一个梦想,开发 UI 部分时,只需关心页面由哪些模块组成,去实现这些模块就行了,而不需要关心模块之间应该如何组合。在描述组合时,可以通过可视化或比较抽象的字符串描述布局的结构,并对应到写好的模块上,这样的代码维护性远高于用 DIV 描述结构的方案。
|
||||
|
||||
> 讨论地址是:[精读《用 css grid 重新思考布局》 · Issue #211 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/211)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,136 @@
|
||||
## 1 引言
|
||||
|
||||
函数式语言在深度学习领域应用很广泛,因为函数式与深度学习模型的契合度很高,[The Beauty of Functional Languages in Deep Learning — Clojure and Haskell](https://www.welcometothejungle.co/fr/articles/btc-deep-learning-clojure-haskell) 就很好的诠释了这个道理。
|
||||
|
||||
通过这篇文章可以加深我们对深度学习与函数式编程的理解。
|
||||
|
||||
## 2 概述与精读
|
||||
|
||||
深度学习是机器学习中基于人工神经网络模型的一个分支,通过模拟多层神经元的自编码神经网络,将特征逐步抽象化,这需要多维度、大数据量的输入。[TensorFlow](https://www.tensorflow.org/) 和 [PyTorch](https://pytorch.org/) 是比较著名的 Python 深度学习框架,同样 [Keras](https://blog.rstudio.com/2017/09/05/keras-for-r/) 在 R 语言中也很著名。然而在生产环境中,基于 **性能和安全性** 的考虑,一般会使用函数式语言 [Clojure](https://www.clojure.org/) 或 [Haskell](https://www.haskell.org/)。
|
||||
|
||||
在生产环境中,可能要并发出里几百万个参数,因此面临的挑战是:如何高效、安全的执行这些运算。
|
||||
|
||||
**所以为什么函数式编程语言可以胜任深度学习的计算要求呢?** 深度学习的计算模型本质上是数学模型,而数学模型本质上和函数式编程思路是一致的:数据不可变且函数间可以任意组合。这意味着使用函数式编程语言可以更好的表达深度学习的计算过程,因此更容易理解与维护,同时函数式语言内置的 Immutable 数据结构也保障了并发的安全性。
|
||||
|
||||
另外函数式语言的函数之间都是相互隔离的,即便在多线程环境下也不会发生竞争和死锁的情况,函数式编程语言会自动处理这些情况。
|
||||
|
||||
比如说 [Clojure](https://www.clojure.org/),**它甚至可在两个同时修改同一引用的程序并发运行时,自动重试其中之一,而不需要手动加锁**:
|
||||
|
||||
```clojure
|
||||
(import ‘(java.util.concurrent Executors))
|
||||
(defn test-stm [nitems nthreads niters]
|
||||
(let [refs (map ref (repeat nitems 0))
|
||||
pool (Executors/newFixedThreadPool nthreads)
|
||||
tasks (map (fn [t]
|
||||
(fn []
|
||||
(dotimes [n niters]
|
||||
(dosync
|
||||
(doseq [r refs]
|
||||
(alter r + 1 t))))))
|
||||
(range nthreads))]
|
||||
(doseq [future (.invokeAll pool tasks)]
|
||||
(.get future))
|
||||
(.shutdown pool)
|
||||
(map deref refs)))
|
||||
(test-stm 10 10 10000) -> (550000 550000 550000 550000 550000 550000 550000 550000 550000 550000)
|
||||
```
|
||||
|
||||
上面的代码创建了引用(refs),同时创建了多个线程自增这个引用对象,按理说每个线程都修改这个引用会导致竞争状态出现,但从结果来看是正常的,说明 Clojure 引擎在执行时会自动解决这个问题。实际上当两个线程出现竞争而失败时,Clojure 会自动重试其中之一。
|
||||
|
||||
> [原文介绍](https://clojure.org/about/concurrent_programming)
|
||||
|
||||
**Clojure 的另一个优势是并行效率高:**
|
||||
|
||||
```clojure
|
||||
(defn calculate-pixels-2 []
|
||||
(let [n (* *width* *height*)
|
||||
work (partition (/ n 16) (range 0 n))
|
||||
result (pmap (fn [x]
|
||||
(doall (map
|
||||
(fn [p]
|
||||
(let [row (rem p *width*) col (int (/ p *height*))]
|
||||
(get-color (process-pixel (/ row (double *width*)) (/ col (double *height*))))))
|
||||
x)))
|
||||
work)]
|
||||
(doall (apply concat result))))
|
||||
```
|
||||
|
||||
使用 `partition` 结合 `pmap` 可以使并发效率达到最大化,也就是 CPU 几乎都消耗在实际计算上,而不是并行的任务管理与上下文切换。Clojure 凭借 `partition` 对计算进行分区,采取分而治之并对分区计算结果进行合并的思路优化了并发性能。
|
||||
|
||||
> [原文介绍](http://www.fatvat.co.uk/2009/05/jvisualvm-and-clojure.html)
|
||||
|
||||
Clojure 另一个特性是函数链式调用:
|
||||
|
||||
```clojure
|
||||
;; pipe arg to function
|
||||
(-> "x" f1) ; "x1"
|
||||
|
||||
;; pipe. function chaining
|
||||
(-> "x" f1 f2) ; "x12"
|
||||
```
|
||||
|
||||
其中 `(-> "x" f1 f2)` 等价于 `f2(f1("x"))`,这种描述不仅更简洁清晰,也更接近于实际数学模型。
|
||||
|
||||
> [原文介绍](http://xahlee.info/clojure/clojure_function_chaining.html)
|
||||
|
||||
最后,Clojure 还具备计算安全性,计算过程不会修改已有的数据,因此在神经网络的任何一层的原始值都会保留,每层计算都可以独立运行且函数永远幂等。
|
||||
|
||||
[Haskell](https://www.haskell.org/) 也有独特的优势,**它具有类型推断、惰性求值等特性**,被认为更适合用于机器学习。
|
||||
|
||||
类型推断即 Haskell 类型都是静态的,如果试图赋予错误的类型会报错。
|
||||
|
||||
Haskell 的另一个优势是可以非常清晰的描述数学模型。
|
||||
|
||||
想想一般数学模型是怎么描述函数的:
|
||||
|
||||
```text
|
||||
fn =>
|
||||
f1 = 1
|
||||
f2 = 9
|
||||
f3 = 16
|
||||
n > 2, fn = 3fn-3 + 2fn-2 + fn-1
|
||||
```
|
||||
|
||||
一般语言用 `if-else` 描述等价关系,但 Haskell 可以几乎原汁原味的还原函数定义过程:
|
||||
|
||||
```haskell
|
||||
solve :: Int -> Interger
|
||||
solve 1 = 1
|
||||
solve 2 = 9
|
||||
solve 3 = 16
|
||||
solve n = 3 * solve (n - 3) + 2 * solve (n - 2) + solve (n - 1)
|
||||
```
|
||||
|
||||
这使得阅读 Haskell 代码和阅读数学公式一样轻松。
|
||||
|
||||
> [原文](https://blog.jle.im/entry/purely-functional-typed-models-1.html)
|
||||
|
||||
Haskell 另一个优势是惰性求值,即计算会在真正用到时才进行,而不会在计算前提前消费掉,比如:
|
||||
|
||||
```haskell
|
||||
let x = [1..]
|
||||
let y = [2,4 ..]
|
||||
head (tail tail( (zip x y)))
|
||||
```
|
||||
|
||||
可以看到,`x` 与 `y` 分别是 `1,2,3,4,5,6...` 与 `2,4,6,8...` 的无限数组,而 `zip` 函数将其整合为一个新数组 `(1,2),(2,4),(3,6),(4,8)...` 这也是无限数组,如果将 `zip` 函数执行完那么程序就会永远执行下去。但 Haskell 却不会陷入死循环,而是直接输出第一位数字 `1`。这就是惰性计算的特性,无论数组有多长,只有真正用到某项时才对其进行计算,所以哪怕初始数据量或计算量很大,实际消耗的运算资源只取决于这次计算实际用到的部分。
|
||||
|
||||
由于深度学习数据量巨大,惰性求值可以忽略海量数据输入,大大提升计算性能。
|
||||
|
||||
## 3 总结
|
||||
|
||||
本文介绍了为什么深度学习更适合使用函数式语言,以及介绍了 Clojure 与 Haskell 语言的共性:安全性、高性能,以及各自独有的特性,证明了为何这两种语言更适合用在深度学习中。
|
||||
|
||||
在前端领域说到函数式或函数之美,大部分时候想到的是 Class Component 与 Function Component 的关系,这个理解是较为片面的。通过本文我们可以了解到,函数式的思想与数学表达式思想如出一辙,以写数学公式的思维方式写代码,就是一种较好的函数式编程思路。
|
||||
|
||||
函数式应该只有表达式,没有语句,这是因为函数式是为了处理运算而诞生的,因此很适合用在深度学习领域。
|
||||
|
||||
> 讨论地址是:[精读《深度学习 - 函数式之美》 · Issue #212 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/212)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,309 @@
|
||||
## 1 引言
|
||||
|
||||
[Nuxt](https://github.com/nuxt/nuxt.js) 是基于 Vue 的前端开发框架,这次我们通过 [Introduction toNuxtJS](https://www.youtube.com/watch?v=NS0io3Z75GI) 视频了解框架特色以及前端开发框架的基本要素。
|
||||
|
||||
> nuxt 与 [next](https://github.com/zeit/next.js) 结构很像,可以结合在一起看
|
||||
|
||||
视频介绍了 NuxtJs 的安装、目录结构、页面路由、导航模版、asyncData、meta、vueX。
|
||||
|
||||
这是一个入门级视频,所以上面所列举的特征都是一个前端开发框架的最核心的基本要素。一个前端开发框架,安装、目录结构、页面路由、导航模版一定是最要下功夫认真设计的。
|
||||
|
||||
asyncData 和 Vuex 都在解决数据问题,meta 则是通过约定语法控制网页 meta 属性,这部分值得与 React 体系做对比,在精读部分再展开。
|
||||
|
||||
Nuxtjs 前端开发框架不仅提供了脚手架的基本功能,还对项目结构、代码做了约定,以减少代码量。从这点可以看出,脚手架永远围绕两个核心目标:**让每一行源码都在描述业务逻辑;让每个项目结构都相同且易读**。
|
||||
|
||||
20 年前,几百行 HTML、Css、Js 代码就能完成一个完整的项目,只需要遵守 W3C 的基本规范就足够了,每一个项目代码都简单清晰,而且由于没有复杂的业务逻辑,导致代码结构也非常简单。但现在前端项目复杂度逐渐升高,一个大型项目源码数量可能达到几十万行、几百万行,这是 W3C 规范没有设想到的,因此出现了各种工程化与模块化方案解决这个复杂度问题,也引发了各个框架间约定的割裂,且设计合理程度各不相同。
|
||||
|
||||
Nuxtjs 等框架要做的就是定义支持现代大型项目的前端研发标准,这个规范具有网络效应,即用的人越多,价值越大。
|
||||
|
||||
接下来我们进入正题,看看 Nuxt 脚手架定义了怎样的开发规范。
|
||||
|
||||
## 2 概述
|
||||
|
||||
### 安装
|
||||
|
||||
使用 `npx create-nuxt-app app-name` 创建新项目。这个命令与 `create-react-app` 一样,区别主要是模版以及配置不同。
|
||||
|
||||
这个命令本质上是拉取一个模版到本地,并安装 `nuxt` 系列脚本作为项目依赖,并自动生成一系列 npmScripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
之后即可通过 `npm start` 等命令开发项目,对大部分项目来说,npmScripts 启动是最能达成共识的。
|
||||
|
||||
这种安装方式另一个好处是,依赖都被安装在了本地,即开发环境 100% 内置在项目中。Nuxt 没有采用全局 cli 命令方式执行,第一是 npmScripts 更符合大家通用习惯,不需要记住不同脚手架繁琐的名称与不同约定的启动命令,第二是全局脚手架一旦进行不兼容升级,老项目就面临维护难题。
|
||||
|
||||
### 目录结构
|
||||
|
||||
```text
|
||||
├── .nuxt
|
||||
├── layouts
|
||||
├── pages
|
||||
├── store
|
||||
├── assets
|
||||
├── static
|
||||
├── middleware
|
||||
├── plugins
|
||||
├── nuxt.config.js
|
||||
```
|
||||
|
||||
**pages**
|
||||
|
||||
页面文件存放的目录,路径 + 文件名即路由名,关于更多约定路由的信息,在下一节页面路由详细说明。
|
||||
|
||||
**layouts**
|
||||
|
||||
模版文件存放的目录,文件名即模版名,页面可以通过定义模版在选择使用的模版。
|
||||
|
||||
**store**
|
||||
|
||||
全局数据流目录,在 vueX 章节介绍。
|
||||
|
||||
**assets**、**static**
|
||||
|
||||
分别存放不需被编译的资源文件与非 `.vue` 的静态文件,比如 scss 文件。
|
||||
|
||||
由于 `.vue` 文件集成了 html、js、css,因此一般不会再额外定义样式文件在 static 文件夹中。
|
||||
|
||||
当然,这是 Vue 生态的特别之处,在 React 生态中会存在大量 `.scss` 文件混杂在各个目录中,比较影响阅读。
|
||||
|
||||
**middleware**、**plugins**
|
||||
|
||||
中间件与插件,这两个目录是可选的,作为一种定制化拓展能力。
|
||||
|
||||
**.nuxt**
|
||||
|
||||
为实现约定路由等便捷功能,启动项目时需要自动生成一些文件作为真正项目入口,这些文件就存储在 `.nuxt` 目录下,gitingore 且无需手动修改。
|
||||
|
||||
**nuxt.config.js**
|
||||
|
||||
nuxt 使用 js 文件作为配置文件,比 json 配置文件拓展性更好一些,这个文件也是整个项目唯一的配置文件。
|
||||
|
||||
基本上 **pages**、**layouts**、**store**、**assets**、以及唯一的配置文件基本成为现代前端开发框架的标配。
|
||||
|
||||
### 页面路由
|
||||
|
||||
nuxt 支持约定路由:
|
||||
|
||||
```text
|
||||
├── pages
|
||||
│ ├── home.vue
|
||||
│ └── index.vue
|
||||
```
|
||||
|
||||
上述目录结构描述了两个路由:`/` 与 `/home`。
|
||||
|
||||
也支持参数路由,只要以下划线作为前缀命名文件,就定义了一个动态参数路由:
|
||||
|
||||
```text
|
||||
├── pages
|
||||
│ ├── videos
|
||||
│ │ └── _id.vue
|
||||
```
|
||||
|
||||
`/videos/*` 都会指向这个文件,且可以通过 `$route.params.id` 拿到这个 url 参数。
|
||||
|
||||
另一个特性是嵌套路由:
|
||||
|
||||
```text
|
||||
├── pages
|
||||
│ ├── videos
|
||||
│ │ └── index.vue
|
||||
│ └── videos.vue
|
||||
```
|
||||
|
||||
`videos.vue` 与 `videos/index.vue` 都指向 `/videos` 这个路由,如果这两个文件同时存在,那么外层的 videos 就会作为外层拦截所有 `/videos` 文件夹下的路由,可以通过 `nuxt-child` 透出子元素:
|
||||
|
||||
```html
|
||||
# pages/videos.vue
|
||||
<template>
|
||||
<div>
|
||||
videos
|
||||
<nuxt-child />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 导航模版
|
||||
|
||||
页面公共逻辑,比如导航条可以放在模版里,模版的目录在 `layouts` 文件夹下。
|
||||
|
||||
默认 `layouts/default.vue` 对所有页面生效,但也可以创建例如 `layouts/videos.vue` 特殊导航文件,在 `pages/` 页面文件通过如下申明指定使用这个模版:
|
||||
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
layout: "videos"
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### asyncData
|
||||
|
||||
`asyncData` 是 nuxt 支持的异步取数函数,可以替代 `data`。
|
||||
|
||||
`data` 函数:
|
||||
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
对于异步场景,可以用 `asyncData` 替代:
|
||||
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
async asyncData() {
|
||||
return await fetch("/");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### meta
|
||||
|
||||
nuxt 允许在 `.vue` 页面文件自定义 head 标签信息:
|
||||
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
headr() {
|
||||
return {
|
||||
title: "",
|
||||
meta: {
|
||||
charset: "utf-8"
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
这是开发框架提供的特性,不过在 React 体系下可以通过 `useTitle` 等自定义 Hooks 解决此问题,将框架功能降维到代码功能,会更容易理解些。
|
||||
|
||||
### vueX
|
||||
|
||||
nuxt 集成了 [vuex](https://github.com/vuejs/vuex),在 `store/` 文件夹下创建数据模型:
|
||||
|
||||
```js
|
||||
export const state = () => ({
|
||||
videos: [],
|
||||
currentVideo: {}
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
SET_VIDEOS (state, videos) {
|
||||
state.videos = videos
|
||||
}
|
||||
SET_CURRENT_VIDEO (state, video) {
|
||||
state.currentVideo = video
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
接下来就能在 `pages` 文件夹下的页面组件使用了:
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
async fetch({ $axios, params, store }) {
|
||||
const reponse = await $axios.get(`/videos/${params.id}`);
|
||||
const video = response.data.data.arrtibutes;
|
||||
store.commit("SET_CURRENT_VIDEO", video);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
将 `return` 替换为 `store.commit` 即可,更多语法可以参考 [vuex 文档](https://github.com/vuejs/vuex)。
|
||||
|
||||
## 3 精读
|
||||
|
||||
Nuxtjs 框架做了几件事情:
|
||||
|
||||
1. 统一执行命令。
|
||||
2. 统一开发框架。
|
||||
3. 统一目录与代码规范。
|
||||
4. 内置公共 utils 函数。
|
||||
|
||||
### 统一执行命令
|
||||
|
||||
命令行是所有开发者每天都要用上十几次甚至几十次的场景,试想一下团队中项目分别有如下这么多不同的启动命令会怎么样?
|
||||
|
||||
1. npm start.
|
||||
2. monkey dev.
|
||||
3. npm run ng.
|
||||
4. npm run bootstrap & banana start.
|
||||
5. ...
|
||||
|
||||
我永远不知道下一个项目该如何启动,这大大降低了开发效率。更严重的是,有的项目可以通过 `npm run docs` 查看文档,有的项目不能;有的项目 `npm run build` 可以触发编译,有的项目却无需编译,等等,所谓的环境不一致或者说迁移成本,学习成本,都是由最开始负责搭建项目脚手架的同学对架构设计不一致导致的,**然而没有必须用 `monkey dev` 才能运行起来的项目,但项目却可能因为被设计为 `monkey dev` 启动而显得与其他项目格格不入,甚至难以统一维护。**
|
||||
|
||||
Nuxtjs 等前端开发框架统一执行命令就是为了解决这个问题,统一开发者习惯需要很长的时间周期,但这个趋势不可挡。
|
||||
|
||||
### 统一开发框架
|
||||
|
||||
**虽然现在 React、Vue、Angular 框架各有利弊,但如果一个团队的项目同时使用了两个以上的框架,没有人会觉得这是一件好事。**
|
||||
|
||||
诚然每个框架都有自己的特点,在不同维度都一些优势,但三大框架能并存,说明各自都没有绝对的杀手锏来消灭对方。
|
||||
|
||||
对开源来说,多元化是活力的源动力,但对一家公司来说,多元化就是一场灾难,至今没有一个框架敢说自己的优势是 “与其他框架混合使用可以提升整体开发效率”。
|
||||
|
||||
前端开发框架要解决的最重要问题也是这一点,无论如何只能选择一种开发框架,Nuxtjs 选择了 Vue,Nextjs 选择了 React。
|
||||
|
||||
### 统一目录与代码规范
|
||||
|
||||
目录和代码规范不会从根本上影响项目的通用性,因为不同的目录结构可以通过映射来兼容,不同的代码规范不会影响代码执行。所以目录与代码规范真正影响的是一个程序员对项目的 “解码成本”。
|
||||
|
||||
所谓解码成本,就是程序员理解项目逻辑所需要的成本。如果你是一个销售主管,让团队周报统一用一种格式汇总绝对比 “用自己喜欢的方式汇总” 效率高,而对编程也一样,一个完全不同的目录结构和代码规范对程序员来说是巨大的阅读阻碍,甚至可能引发恶心反应。
|
||||
|
||||
所以不同的目录结构和代码规范是没有必要的壁垒,除非你的团队已经对某种规范产生达成了牢固的共识,否则最好和其他团队共享相同的目录结构与代码规范。改变代码规范是一件很难得事情,但只要不同规范的团队间产生了长期合作关系,规范统一就势必会被提上议程,那么为何不能在公司层面早一点达成共识,提前消除这种痛苦呢?
|
||||
|
||||
所以统一目录与代码规范是前端开发框架需要优先确定的,很多时候不要去质疑为什么目录叫 `layouts` 而不叫 `layout`,因为这个规范背后形成的协同网络规模越大,叫什么名字就越不重要。
|
||||
|
||||
### 内置公共 utils 函数
|
||||
|
||||
让业务开发更聚焦,还可以通过抽取通用的逻辑的方式解决,但需要解决两个问题:
|
||||
|
||||
1. 虽然将公共函数抽成 npm 包可以解决代码复用问题,但关键是怎么保证你的代码能被别人复用?
|
||||
2. 如何让业务通用的 utils 代码有效沉淀并从项目中移除?
|
||||
|
||||
脚手架内置公共 utils 函数就为了解决这个问题。上面几个小节解决了通用命令、框架、规范,但实际代码中,`router` `history` `fetch` `store` 等等概念也都是可以统一的,**没有一个项目必须用定制的 `fetch` 函数才能取数,但一开始就定制了 `fetch` 会导致耦合了不可预期的、没有必要的业务逻辑,成为理解与提效的阻碍。**
|
||||
|
||||
所以统一这些能统一的包,是进一步提效的关键。也许有人会觉得断了自己造轮子的路,但就像我们如今都不会重写浏览器内核逻辑一样,稳定的逻辑不仅带来了全行业的提效,还催生了前端岗位带来大量的就业,同样的,统一底层通用函数,其实是断了无意义产出这条路,每个人都有追求更高价值事情的权利,不要把自己困在反复造 `fetch` 函数这个低水平的活里。
|
||||
|
||||
## 4 总结
|
||||
|
||||
如果一个项目没有使用类似 Nuxtjs 开发框架,它面临的不仅仅是技术选型不统一的问题,久而久之这种项目势必成为 **代码孤岛**,当尘封在代码仓库几年后,一系列文档工具链接都失效后,就成为谁也不想碰,不敢碰的高危代码。
|
||||
|
||||
所以我们今天不仅要看到 Nuxtjs 提供的能力对项目开发有多么便捷,更要看到这类框架带来的协同效应有多么巨大,如果它不能成为整个前端的标准,至少要成为你们公司,或者你们团队的标准。
|
||||
|
||||
> 讨论地址是:[精读《Nuxtjs》 · Issue #213 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/213)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,702 @@
|
||||
## 1 引言
|
||||
|
||||
[React Conf 2019](https://www.youtube.com/watch?v=RCiccdQObpo) 在今年 10 月份举办,内容质量还是一如既往的高,如果想进一步学习前端或者 React,这个大会一定不能错过。
|
||||
|
||||
希望前端精读成为你学习成长路上的布道者,所以本期精读就介绍 React Conf 2019 - Day1 的相关内容。
|
||||
|
||||
总的来看,React Conf 今年的内容视野更广了,不仅仅有技术内容,还有宣扬公益、拓展到移动端、后端,最后还有对 web 发展的总结与展望。
|
||||
|
||||
前端世界正变得越来越复杂,可以看到大家对未来都充满了希望,永不停歇的探索精神是这场大会的主旋律。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
本期大会思想、设计上的内容较多,具体实现层内容较少,因为行业领导者需要引领规范,而真正技术价值在于思维模型与算法,理解了解题思路,实现它其实并不难。
|
||||
|
||||
### 开发者体验与用户体验
|
||||
|
||||
- 开发者体验:DX(develop experience)
|
||||
- 用户体验:UX(user experience)
|
||||
|
||||
技术人解决的问题总是围绕 DX 与 UX,而一般来说,优化了 DX 往往会带来 UX 的提升,这是因为一个解决开发者体验的技术创新往往也会带来用户体验的升级,至少也能让开发者有更好的心情、更充足的时间做出好产品。
|
||||
|
||||
如何优化开发者体验呢?
|
||||
|
||||
**易上手**
|
||||
|
||||
React 确实致力于解决这个问题,因为 React 实际上是一个开发者桥梁,无论你开发 web、ios 还是单片机,都可以通过一套统一的语法去实现。React 是一个协议标准(读到 reactReconciler 章节会更有体感),React 像 HTML,但 React 不止能构建 HTML 应用,React 希望构建一切。
|
||||
|
||||
**高效开发**
|
||||
|
||||
React 解决调试、工具问题,让开发者更高效的完成工作,这也是开发者体验重要组成部分。
|
||||
|
||||
**弹性**
|
||||
|
||||
React 编写的程序拥有良好可维护性,包括数据驱动、模块化等等特征都是为了更好服务于不同规模的团队。
|
||||
|
||||
对于 UX 问题,React 也有 Concurrent mode、Suspense 等方案。
|
||||
|
||||
虽然 React 还不完美,但 React 致力于解决 DX 与 UX 的目标和效果都是我们有目共睹的,更好的 DX、UX 一定是前端技术未来发展的大趋势。
|
||||
|
||||
### 样式方案
|
||||
|
||||
Facebook 使用 css-in-js,而今年的 React conf 给出了一种技术方案,将 413 kb 的样式文件体积降低到 74kb!
|
||||
|
||||
一步步了解这个方案,从用法开始:
|
||||
|
||||
```tsx
|
||||
const styles = stylex.create({
|
||||
blue: { color: "blue" },
|
||||
red: { color: "red" }
|
||||
});
|
||||
|
||||
function MyComponent(props) {
|
||||
return <span className={styles("blue", "red")}>I'm red now!</span>;
|
||||
}
|
||||
```
|
||||
|
||||
如上是这个方案的写法,通过 `stylex.create` 创建样式,通过 `styles()` 使用样式。
|
||||
|
||||
**主题方案**
|
||||
|
||||
如果使用 CSS 变量定义主题,那么换肤就可以由最外层 `class` 轻松决定了:
|
||||
|
||||
```scss
|
||||
.old-school-theme {
|
||||
--link-text: blue;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: var(--link-text);
|
||||
}
|
||||
```
|
||||
|
||||
字体颜色具体的值由外层 `class` 决定,因此外层的 `class` 就可以控制所有子元素的样式:
|
||||
|
||||
```html
|
||||
<div class="old-school-theme">
|
||||
<a class="text-link" href="...">
|
||||
I'm blue!
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
将其封装成 React 组件,也不需要用 `context` 等 JS 能力,而是包裹一层 `class` 即可。
|
||||
|
||||
```tsx
|
||||
function ThemeProvider({ children, theme }) {
|
||||
return <div className={themes[theme]}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**图标方案**
|
||||
|
||||
下面是设计师给出的 svg 代码:
|
||||
|
||||
```tsx
|
||||
<svg viewBox="0 0 100 100">
|
||||
<path d="M9 25C8 25 8..." />
|
||||
</svg>
|
||||
```
|
||||
|
||||
将其包装为 React 组件:
|
||||
|
||||
```tsx
|
||||
function SettingsIcon(props) {
|
||||
return (
|
||||
<SVGIcon viewBox="0 0 100 100" {...props}>
|
||||
<path d="M9 25C8 25 8..." />
|
||||
</SVGIcon>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
结合上面提到的主题方案,就可以控制 svg 的主题颜色。
|
||||
|
||||
```tsx
|
||||
const styles = stylex.create({
|
||||
primary: { fill: "var(--primary-icon)" },
|
||||
gighlight: { fill: "var(--highlight-icon)" }
|
||||
});
|
||||
|
||||
function SVGIcon(color, ...props) {
|
||||
return (
|
||||
<svg>
|
||||
{...props}
|
||||
className={styles({
|
||||
primary: color === "primary",
|
||||
highlight: color === "highlight"
|
||||
})}
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**减少样式大小的秘密**
|
||||
|
||||
```tsx
|
||||
const styles = stylex.create({
|
||||
blue: { color: "blue" },
|
||||
default: { color: "red", fontSize: 16 }
|
||||
});
|
||||
|
||||
function MyComponent(props) {
|
||||
return <span className={styles("default", props.isBlue && "blue")} />;
|
||||
}
|
||||
```
|
||||
|
||||
对于上述样式文件代码,最终会编译成 `c1`、`c2`、`c3` 三个 `class`:
|
||||
|
||||
```scss
|
||||
.c1 {
|
||||
color: blue;
|
||||
}
|
||||
.c2 {
|
||||
color: red;
|
||||
}
|
||||
.c3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
出乎意料的是,并没有根据 `blue` 和 `default` 生成对应的 `class`,而是根据实际样式值生成 `class`,这样做有什么好处呢?
|
||||
|
||||
首先是加载顺序,`class` 生效的顺序与加载顺序有关,而按照样式值生成的 `class` 可以精确控制样式加载顺序,使其与书写顺序对应:
|
||||
|
||||
```tsx
|
||||
// 效果可能是 blue 而不是 red
|
||||
<div className="blue red" />
|
||||
|
||||
// 效果一定是 red,因为 css-in-js 在最终编排 class 时,虽然两种样式都存在,但书写顺序导致最后一个优先级最高,
|
||||
// 合并的时候就会舍弃失效的那个 class
|
||||
<div className={styles('blue', 'red')} />
|
||||
```
|
||||
|
||||
这么做永远不会出现头疼的样式覆盖问题。
|
||||
|
||||
更重要的是,随着样式文件的增多,`class` 总量会减少。这是因为新增的 `class` 涵盖的属性可能已经被其他 `class` 写到并生成了,此时会直接复用对应属性生成的 `class` 而不会生成新的:
|
||||
|
||||
```tsx
|
||||
<Component1 className=".class1"/>
|
||||
<Component2 className=".class2"/>
|
||||
```
|
||||
|
||||
```scss
|
||||
.class1 {
|
||||
background-color: mediumseagreen;
|
||||
cursor: default;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.class2 {
|
||||
background-color: thistle;
|
||||
cursor: default;
|
||||
justify-self: flex-start;
|
||||
margin-left: 0px;
|
||||
}
|
||||
```
|
||||
|
||||
正如这个 Demo 所示,正常情况的 `class1` 与 `class2` 存在许多重复定义的属性,但换成 css-in-js 的方案,编译后的效果等价于将 `class` 复用并拆解了:
|
||||
|
||||
```tsx
|
||||
<Component1 classNames=".classA .classB .classD">
|
||||
|
||||
<Component2 classNames=".classA .classC .classD .classE">
|
||||
```
|
||||
|
||||
```scss
|
||||
.classA {
|
||||
cursor: default;
|
||||
}
|
||||
.classB {
|
||||
background-color: mediumseagreen;
|
||||
}
|
||||
.classC {
|
||||
background-color: thistle;
|
||||
}
|
||||
.classD {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.classE {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
```
|
||||
|
||||
这种方式不仅节省空间、还能自动计算样式优先级避免冲突,并将 413 kb 的样式文件体积降低到 74kb。
|
||||
|
||||
### 字体大小方案
|
||||
|
||||
`rem` 的好处是相对的字体大小,使用 `rem` 作为单位可以很方便实现网页字体大小的切换。
|
||||
|
||||
但问题是现在工业设计都习惯了以 px 作为单位,所以一种全新的编译方案产生了:在编译阶段将 `px` 自动转换成 `rem`。
|
||||
|
||||
这等于让以 `px` 为单位的字体大小可以跟随根节点字体大小随意缩放。
|
||||
|
||||
### 代码检测
|
||||
|
||||
静态检测类型错误、拼写错误、浏览器兼容问题。
|
||||
|
||||
在线检测 dom 节点元素问题,比如是否有可访问性,比如替代文案 aria-label。
|
||||
|
||||
### 提升加载速度
|
||||
|
||||
普通网页的加载流程是这样的:
|
||||
|
||||

|
||||
|
||||
先加载代码,然后会渲染页面,在渲染的同时发取数请求,等取数完成后才能渲染出真实数据。
|
||||
|
||||
那么如何改善这个情况呢?首先是预取数,提前解析出请求并在脚本加载的同时取数,可以节省大量时间:
|
||||
|
||||

|
||||
|
||||
那么下载的代码可以再拆分吗?注意到并不是所有代码都作用于 UI 渲染,我们可以将模块分为 `ImportForDisplay` 与 `importForAfterDisplay` :
|
||||
|
||||

|
||||
|
||||
这样就可以优先加载与 UI 相关的代码,其余逻辑代码在页面展示出之后再加载:
|
||||
|
||||

|
||||
|
||||
这样可以实现源码分段加载,并分段渲染:
|
||||
|
||||

|
||||
|
||||
对取数来说也是如此,并不是所有取数都是初始化渲染阶段必须用上的。可以通过 `relay` 的特性 `@defer` 标记出可以延迟加载的数据:
|
||||
|
||||
```relay
|
||||
fragment ProfileData on User {
|
||||
classNameprofile_picture { ... }
|
||||
|
||||
...AdditionalData @defer
|
||||
}
|
||||
```
|
||||
|
||||
这下取数也可以分段了,首屏的数据会优先加载:
|
||||
|
||||

|
||||
|
||||
利用 `relay` 还可以以数据驱动方式结合代码拆分:
|
||||
|
||||
```relay
|
||||
... on Post {
|
||||
... on PhotoPost {
|
||||
@module('PhotoComponent.js')
|
||||
photo_data
|
||||
}
|
||||
|
||||
... on VideoPost {
|
||||
@module('VideoComponent.js')
|
||||
video_data
|
||||
}
|
||||
|
||||
... on SongPost {
|
||||
@module('SongComponent.js')
|
||||
song_data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这样首屏数据中也只会按需加载用到的部分,请求时间可以再次缩短:
|
||||
|
||||

|
||||
|
||||
可以看到,与 relay 结合可以进一步优化加载性能。
|
||||
|
||||
### 加载体验
|
||||
|
||||
可以 `React.Suspense` 与 `React.lazy` 动态加载组件。通过 `fallback` 指定元素的占位图可以提升加载体验:
|
||||
|
||||
```tsx
|
||||
<React.Suspense fallback={<MyPlaceholder />}>
|
||||
<Post>
|
||||
<Header />
|
||||
<Body />
|
||||
<Reactions />
|
||||
<Comments />
|
||||
</Post>
|
||||
</React.Suspense>
|
||||
```
|
||||
|
||||
`Suspense` 可以被嵌套,资源会按嵌套顺序加载,保证一个自然的视觉连贯性。
|
||||
|
||||
### 智能文档
|
||||
|
||||
通过解析 Markdown 自动生成文档大家已经很熟悉了,也有很多现成的工具可以用,但这次分享的文档系统有意思之处在于,可以动态修改源码并实时生效。
|
||||
|
||||

|
||||
|
||||
不仅如此,还利用了 Typescript + MonacoEditor 在网页上做语法检测与 API 自动提示,这种文档体验上升了一个档次。
|
||||
|
||||
虽然没有透露技术实现细节,但从热更新的操作来看像是把编译工作放在了浏览器 web worker 中,如果是这种实现方式,原理与 [CodeSandbox 实现原理](https://segmentfault.com/a/1190000019679430) 类似。
|
||||
|
||||
### GraphQL and Stuff
|
||||
|
||||
这一段在安利利用接口自动生成 Typescript 代码提升前后端联调效率的工具,比如 go2dts。
|
||||
|
||||
我们团队也开源了基于 swagger 的 Typescript 接口自动生成工具 [pont](https://github.com/alibaba/pont),欢迎使用。
|
||||
|
||||
### React Reconciler
|
||||
|
||||
这是知识密度最大的一节,介绍了如何使用 React Reconclier。
|
||||
|
||||
React Reconclier 可以创建基于任何平台的 React 渲染器,也可以理解为通过 React Reconclier 可以创建自定义的 ReactDOM。
|
||||
|
||||
比如下面的例子,我们尝试用自定义函数 `ReactDOMMini` 渲染 React 组件:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
import logo from "./logo.svg";
|
||||
import ReactDOMMini from "./react-dom-mini";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [showLogo, setShowLogo] = React.useState(true);
|
||||
|
||||
let [color, setColor] = React.useState("red");
|
||||
React.useEffect(() => {
|
||||
let colors = ["red", "green", "blue"];
|
||||
let i = 0;
|
||||
let interval = setInterval(() => {
|
||||
i++;
|
||||
setColor(colors[i % 3]);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="App"
|
||||
onClick={() => {
|
||||
setShowLogo(show => !show);
|
||||
}}
|
||||
>
|
||||
<header className="App-header">
|
||||
{showLogo && <img src={logo} className="App-logo" alt="logo /" />}
|
||||
// 自创语法
|
||||
<p bgColor={color}>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React{" "}
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOMMini.render(<App />, codument.getElementById("root"));
|
||||
```
|
||||
|
||||
`ReactDOMMini` 是利用 `ReactReconciler` 生成的自定义组件渲染函数,下面是完整的代码:
|
||||
|
||||
```typescript
|
||||
import ReactReconciler from "react-reconciler";
|
||||
|
||||
const reconciler = ReactReconciler({
|
||||
createInstance(
|
||||
type,
|
||||
props,
|
||||
rootContainerInstance,
|
||||
hostContext,
|
||||
internalInstanceHandle
|
||||
) {
|
||||
const el = document.createElement(type);
|
||||
|
||||
["alt", "className", "href", "rel", "src", "target"].forEach(key => {
|
||||
if (props[key]) {
|
||||
el[key] = props[key];
|
||||
}
|
||||
});
|
||||
|
||||
// React 事件代理
|
||||
if (props.onClick) {
|
||||
el.addEventListener("click", props.onClick);
|
||||
}
|
||||
|
||||
// 自创 api bgColor
|
||||
if (props.bgColor) {
|
||||
el.style.backgroundColor = props.bgColor;
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
createTextInstance(
|
||||
text,
|
||||
rootContainerInstance,
|
||||
hostContext,
|
||||
internalInstanceHandle
|
||||
) {
|
||||
return document.createTextNode(text);
|
||||
},
|
||||
|
||||
appendChildToContainer(container, child) {
|
||||
container.appendChild(child);
|
||||
},
|
||||
appendChild(parent, child) {
|
||||
parent.appendChild(child);
|
||||
},
|
||||
appendInitialChild(parent, child) {
|
||||
parent.appendChild(child);
|
||||
},
|
||||
|
||||
removeChildFromContainer(container, child) {
|
||||
container.removeChild(child);
|
||||
},
|
||||
removeChild(parent, child) {
|
||||
parent.removeChild(child);
|
||||
},
|
||||
insertInContainerBefore(container, child, before) {
|
||||
container.insertBefore(child, before);
|
||||
},
|
||||
insertBefore(parent, child, before) {
|
||||
parent.insertBefore(child, before);
|
||||
},
|
||||
|
||||
prepareUpdate(
|
||||
instance,
|
||||
type,
|
||||
oldProps,
|
||||
newProps,
|
||||
rootContainerInstance,
|
||||
currentHostContext
|
||||
) {
|
||||
let payload;
|
||||
if (oldProps.bgColor !== newProps.bgColor) {
|
||||
payload = { newBgCOlor: newProps.bgColor };
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
commitUpdate(
|
||||
instance,
|
||||
updatePayload,
|
||||
type,
|
||||
oldProps,
|
||||
newProps,
|
||||
finishedWork
|
||||
) {
|
||||
if (updatePayload.newBgColor) {
|
||||
instance.style.backgroundColor = updatePayload.newBgColor;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const ReactDOMMini = {
|
||||
render(wahtToRender, div) {
|
||||
const container = reconciler.createContainer(div, false, false);
|
||||
reconciler.updateContainer(whatToRender, container, null, null);
|
||||
}
|
||||
};
|
||||
|
||||
export default ReactDOMMini;
|
||||
```
|
||||
|
||||
笔者拆解一下说明:
|
||||
|
||||
React 之所以具备跨平台特性,是因为其渲染函数 `ReactReconciler` **只关心如何组织组件与组件间关系,而不关心具体实现**,所以会暴露出一系列回调函数。
|
||||
|
||||
**创建实例**
|
||||
|
||||
由于 React 组件本质是一个描述,即 `tag` + 属性,所以 `Reconciler` 不关心元素是如何创建的,需要通过 `createInstance` 拿到组件基本属性,在 Web 平台利用 DOM API 实现:
|
||||
|
||||
```typescript
|
||||
createInstance(
|
||||
type,
|
||||
props,
|
||||
rootContainerInstance,
|
||||
hostContext,
|
||||
internalInstanceHandle
|
||||
) {
|
||||
const el = document.createElement(type);
|
||||
|
||||
["alt", "className", "href", "rel", "src", "target"].forEach(key => {
|
||||
if (props[key]) {
|
||||
el[key] = props[key];
|
||||
}
|
||||
});
|
||||
|
||||
// React 事件代理
|
||||
if (props.onClick) {
|
||||
el.addEventListener("click", props.onClick);
|
||||
}
|
||||
|
||||
// 自创 api bgColor
|
||||
if (props.bgColor) {
|
||||
el.style.backgroundColor = props.bgColor;
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
```
|
||||
|
||||
之所以说 React 对 DOM 事件都做了一层代理,是因为 JSX 的所有函数都没有真正透传给 DOM,而是通过类似 `el.addEventListener("click", props.onClick)` 的方式代理实现的。
|
||||
|
||||
而自定义这个函数,我们甚至能创建例如 `bgColor` 这种特殊语法,只要解析引擎实现了这个语法的 Handler。
|
||||
|
||||
除此之外,还有 **创建、删除实例** 的回调函数,我们都要利用 DOM 平台的 API 重新实现一遍,这样不仅可以实现对浏览器 API 的兼容,还可以对接到比如 react-native 等非 WEB 平台。
|
||||
|
||||
**更新组件**
|
||||
|
||||
实现了 `prepareUpdate` 与 `commitUpdate` 才能完成组件更新。
|
||||
|
||||
`prepareUpdate` 返回的 `payload` 被 `commitUpdate` 函数接收到,并根据接收到的信息决定如何更新实例节点。这个实例节点就是 `createInstance` 回调函数返回的对象,所以如果在 WEB 环境返回的 instance 就是 DOMInstance,后续所有操作都使用 DOMAPI。
|
||||
|
||||
总结一下:`react` 主要用平台无关的语法生成具有业务含义的 AST,而利用 `react-reconciler` 生成的渲染函数可以解析这个 AST,并提供了一系列回调函数实现完整的 UI 渲染功能,`react-dom` 现在也是基于 `react-reconciler` 写的。
|
||||
|
||||
### 图标体积优化
|
||||
|
||||
Facebook 团队通过优化,将图标大小从 4046.05KB 降低到了 132.95kb,体积减少了惊人的 96.7%,减少体积占总包体积的 19.6%!
|
||||
|
||||
实现方式很简单,下面是原始图标使用的代码:
|
||||
|
||||
```jsx
|
||||
<FontAwesomeIcon icon="coffee" />
|
||||
<Icon icon={["fab", "twitter"]} />
|
||||
<Button leftIcon="user" />
|
||||
<FeatureGroup.Item icon="info" />
|
||||
<FeatureGroup.Item icon={["fail", "info"]} />
|
||||
```
|
||||
|
||||
在编译期间通过 AST 分析,将所有字符串引用换成了图标实例的引用,利用 webpack 的 tree-shaking 功能实现按需加载,从而删除了没有使用到的图标。
|
||||
|
||||
```jsx
|
||||
import {faCoffee,faInfo,faUser} from "@fontawesome/free-solid-svg-icons"
|
||||
import {faTwitter} from '@fontawesome/free-brands-svg-icons'
|
||||
import {faInfo as faInfoFal} from '@fontawesome/pro-light-svg-icons'
|
||||
|
||||
<FontAwesomeIcon icon={faCoffee} />
|
||||
<Icon icon={faTwitter} />
|
||||
<Button leftIcon={faUser} />
|
||||
<FeatureGroup.Item icon={faInfo} />
|
||||
<FeatureGroup.Item icon={faInfoFal} />
|
||||
```
|
||||
|
||||
[替换工具](https://github.com/skovy/font-awesome-codemod) 的链接放出来了,感兴趣的同学可以点进去了解更多。
|
||||
|
||||
这也从某种意义上说明了 iconFont 注定被淘汰,因为字体文件目前无法按需加载,只有全部使用 SVG 图标的项目才能使用这种优化。
|
||||
|
||||
### Git & Github
|
||||
|
||||
这一节介绍了基本 Git 知识以及 Github 用法,笔者略过比较水的部分,直接列出两个可能你不知道的点:
|
||||
|
||||
**干预 Github 项目主要语言检测**
|
||||
|
||||
如果你提交的代码包含许多自动生成的文件,可能你实际使用的语言不会被 Github 解析为主要语言,这时候可以通过 `.gitattributes` 文件忽略指定文件夹的检测:
|
||||
|
||||
```text
|
||||
static/* linguist-vendored
|
||||
```
|
||||
|
||||
这样语言文件占比统计就会忽略 `static/` 文件夹。
|
||||
|
||||
**Git hooks 的技巧**
|
||||
|
||||
以下是几个比较具有启发的点,我们可以利用 Git hooks 做点什么:
|
||||
|
||||
- 阻止提交到 master。
|
||||
- 在 commit 之前执行 prettier/eslint/jest 检测。
|
||||
- 检测代码规范、合并冲突、检测是否有大文件。
|
||||
- commit 成功后给出提示或记录到日志。
|
||||
|
||||
但 Git hooks 仍然有局限性:
|
||||
|
||||
- 容易被绕过:--no-verifuy --no-merge --no-checkout ---force。
|
||||
- 本地 hooks 无法提交,导致项目开发规则可能不尽相同。
|
||||
- 无法替代 CI、服务端分支保护、Code Review。
|
||||
|
||||
可以畅想一下,在 WebIDE 环境可以通过自定义 git 命令禁止检测绕过,自然解决第二条环境不一致的问题。
|
||||
|
||||
### GraphQL + Typescript
|
||||
|
||||
GraphQL 是没有类型支持的,如果要手动创建一遍类型文件是非常痛苦的:
|
||||
|
||||
```typescript
|
||||
interface GetArticleData {
|
||||
getArticle: {
|
||||
id: number;
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
||||
const query = graphql(gql`
|
||||
query getArticle {
|
||||
article {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
apolloClient.query<GetArticleData>(query);
|
||||
```
|
||||
|
||||
同样的代码分散在两处维护一定会带来问题,我们可以利用比如 `typed-graphqlify` 这种库解决类型问题:
|
||||
|
||||
```typescript
|
||||
import { params, types, query } from "typed-graphqlify";
|
||||
|
||||
const getArticleQuery = {
|
||||
article: params({
|
||||
id: types.number,
|
||||
title: types.string
|
||||
})
|
||||
};
|
||||
|
||||
const gqlString = query("getUser", getUserQuery);
|
||||
```
|
||||
|
||||
只要一遍定义就可以自动生成 GQLString,并且拿到 Typescript 类型。
|
||||
|
||||
### React 文档国际化
|
||||
|
||||
即便是谷歌翻译也不是很靠谱,国际化文档还是要靠人肉,[Nat Alison](https://github.com/tesseralis) 利用 Github 充分发动各国人民的力量,共同打造了一个个 reactjs group 下的国际化仓库。
|
||||
|
||||
国际化仓库命名规则是 `reactjs/xx.reactjs.org`,比如简体中文的国际化仓库是:https://github.com/reactjs/zh-hans.reactjs.org
|
||||
|
||||
从仓库的 readme 可以看到维护规则是这样的:
|
||||
|
||||
- 请 fork 这个仓库。
|
||||
- 基于 fork 后的仓库中 master 分支拉取一个新的分支(名字自取)。
|
||||
- 翻译(校对)你所选择的文章,提交到新的分支。
|
||||
- 此时提交 Pull Request 到该仓库。
|
||||
- 会有专人 Review 该 Pull Request,当两人以上通过该 Pull Request 时,你的翻译将被合并到仓库中。
|
||||
- 删除你所创建的分支(如继续参与,参考同步流程)。
|
||||
|
||||
之后定期从 React 官方文档项目拉取最新代码即可保持文档的同步更新。
|
||||
|
||||
### 你需要 redux 吗?
|
||||
|
||||
关于数据流的话题目前没有什么新意,但这次 React Conf 关于数据流总结的算是比较真诚的,总结了以下几个点:
|
||||
|
||||
1. 全局数据流现在不是必须的,比如 Redux,但也不能说完全不能用,至少在全局状态较为复杂时有必要使用。
|
||||
2. 不要只使用一种数据流方案,根据状态的作用域确定方案比较好。
|
||||
3. 工程技术与科学不同,工程世界没有最好的方案,只有更好的方案。
|
||||
4. 就算有了完美方案也不要停止学习的步伐,总会有新知识产生。
|
||||
|
||||
### web 历史
|
||||
|
||||
很精彩的演讲,不过新鲜内容并不多,比较有感触一点是:以前的网页地址对应到的是服务器磁盘的某个具体文件,比如早期 php 应用,现在后端不再是文件化而是服务化了,这层抽象让服务端摆脱了对文件结构的依赖,可以构建更多复杂动态逻辑,也支持了前后端分离的技术方案。
|
||||
|
||||
## 3 总结
|
||||
|
||||
这届 React Conf 让我们看到前端更多的可能性,我们不仅要关注技术实现细节,更要关注行业标准以及团队愿景。
|
||||
|
||||
React 团队的愿景是让 React 包罗万象,提升全球开发者的开发体验、提升全球产品的用户体验,基于这个目标,React Conf 自然不能只包含 DOM Diff、Reconciler 等等技术细节,更需要展示 React 如何帮助全球开发者,如何让这些开发者帮助到用户,如何推动行业标准的演进,如何让 React 打破国界、语言的壁垒。
|
||||
|
||||
相比其他前端大会非常多的干货来说,React Conf 虽然显得主题比较杂,但这正是人文情怀的体现,我相信只有带着更高的使命愿景,真诚帮助他人的技术团队才可以走得更远。
|
||||
|
||||
> 讨论地址是:[精读《React Conf 2019 - Day1》 · Issue #214 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/214)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,477 @@
|
||||
## 1 引言
|
||||
|
||||
取数是前端业务的重要部分,也经历过几次演化:
|
||||
|
||||
- [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) 的兼容性已经足够好,足以替换包括 `$.post` 在内的各种取数封装。
|
||||
- 原生用得久了,发现拓展性更好、支持 ssr 的同构取数方案也挺好,比如 [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch)、[axios](https://github.com/axios/axios)。
|
||||
- 对于数据驱动场景还是不够,数据流逐渐将取数封装起来,同时针对数据驱动状态变化管理进行了 `data` `isLoading` `error` 封装。
|
||||
- Hooks 的出现让组件更 Reactive,我们发现取数还是优雅回到了组件里,[swr](https://github.com/zeit/swr) 就是一个教科书般的例子。
|
||||
|
||||
[swr](https://github.com/zeit/swr) 在 2019.10.29 号提交,仅仅 12 天就攒了 4000+ star,平均一天收获 300+ star!本周精读就来剖析这个库的功能与源码,了解这个 React Hooks 的取数库的 Why How 与 What。
|
||||
|
||||
## 2 概述
|
||||
|
||||
首先介绍 swr 的功能。
|
||||
|
||||
为了和官方文档有所区别,笔者以探索式思路介绍这个它,但例子都取自官方文档。
|
||||
|
||||
### 2.1 为什么用 Hooks 取数
|
||||
|
||||
首先回答一个根本问题:为什么用 Hooks 替代 fetch 或数据流取数?
|
||||
|
||||
因为 **Hooks 可以触达 UI 生命周期,取数本质上是 UI 展示或交互的一个环节。** 用 Hooks 取数的形式如下:
|
||||
|
||||
```typescript
|
||||
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>;
|
||||
}
|
||||
```
|
||||
|
||||
首先看到的是,以同步写法描述了异步逻辑,这是因为渲染被执行了两次。
|
||||
|
||||
`useSWR` 接收三个参数,第一个参数是取数 `key`,这个 `key` 会作为第二个参数 `fetcher` 的第一个参数传入,普通场景下为 URL,第三个参数是配置项。
|
||||
|
||||
Hooks 的威力还不仅如此,上面短短几行代码还自带如下特性:
|
||||
|
||||
1. 可自动刷新。
|
||||
2. 组件被销毁再渲染时优先启用本地缓存。
|
||||
3. 在列表页中浏览器回退可以自动记忆滚动条位置。
|
||||
4. tabs 切换时,被 focus 的 tab 会重新取数。
|
||||
|
||||
当然,自动刷新或重新取数也不一定是我们想要的,[swr](https://github.com/zeit/swr) 允许自定义配置。
|
||||
|
||||
### 2.2 配置
|
||||
|
||||
上面提到,`useSWR` 还有第三个参数作为配置项。
|
||||
|
||||
**独立配置**
|
||||
|
||||
通过第三个参数为每个 `useSWR` 独立配置:
|
||||
|
||||
```tsx
|
||||
useSWR("/api/user", fetcher, { revalidateOnFocus: false });
|
||||
```
|
||||
|
||||
配置项可以参考 [文档](https://github.com/zeit/swr#options)。
|
||||
|
||||
> 可以配置的有:suspense 模式、focus 重新取数、重新取数间隔/是否开启、失败是否重新取数、timeout、取数成功/失败/重试时的回调函数等等。
|
||||
|
||||
> 第二个参数如果是 object 类型,则效果为配置项,第二个 fetcher 只是为了方便才提供的,在 object 配置项里也可以配置 fetcher。
|
||||
|
||||
**全局配置**
|
||||
|
||||
`SWRConfig` 可以批量修改配置:
|
||||
|
||||
```tsx
|
||||
import useSWR, { SWRConfig } from "swr";
|
||||
|
||||
function Dashboard() {
|
||||
const { data: events } = useSWR("/api/events");
|
||||
// ...
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<SWRConfig value={{ refreshInterval: 3000 }}>
|
||||
<Dashboard />
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
独立配置优先级高于全局配置,在精读部分会介绍实现方式。
|
||||
|
||||
最重量级的配置项是 `fetcher`,它决定了取数方式。
|
||||
|
||||
### 2.3 自定义取数方式
|
||||
|
||||
自定义取数逻辑其实分几种抽象粒度,比如自定义取数 url,或自定义整个取数函数,而 [swr](https://github.com/zeit/swr) 采取了相对中间粒度的自定义 `fetcher`:
|
||||
|
||||
```tsx
|
||||
import fetch from "unfetch";
|
||||
|
||||
const fetcher = url => fetch(url).then(r => r.json());
|
||||
|
||||
function App() {
|
||||
const { data } = useSWR("/api/data", fetcher);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
所以 `fetcher` 本身就是一个拓展点,我们不仅能自定义取数函数,自定义业务处理逻辑,甚至可以自定义取数协议:
|
||||
|
||||
```tsx
|
||||
import { request } from "graphql-request";
|
||||
|
||||
const API = "https://api.graph.cool/simple/v1/movies";
|
||||
const fetcher = query => request(API, query);
|
||||
|
||||
function App() {
|
||||
const { data, error } = useSWR(
|
||||
`{
|
||||
Movie(title: "Inception") {
|
||||
releaseDate
|
||||
actors {
|
||||
name
|
||||
}
|
||||
}
|
||||
}`,
|
||||
fetcher
|
||||
);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
这里回应了第一个参数称为取数 Key 的原因,在 graphql 下它则是一段语法描述。
|
||||
|
||||
到这里,我们可以自定义取数函数,但却无法控制何时取数,因为 Hooks 写法使取数时机与渲染时机结合在一起。[swr](https://github.com/zeit/swr) 的条件取数机制可以解决这个问题。
|
||||
|
||||
### 2.4 条件取数
|
||||
|
||||
所谓条件取数,即 `useSWR` 第一个参数为 null 时则会终止取数,我们可以用三元运算符或函数作为第一个参数,使这个条件动态化:
|
||||
|
||||
```tsx
|
||||
// conditionally fetch
|
||||
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);
|
||||
|
||||
// ...or return a falsy value
|
||||
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);
|
||||
```
|
||||
|
||||
上例中,当 `shouldFetch` 为 false 时则不会取数。
|
||||
|
||||
第一个取数参数推荐为回调函数,这样 [swr](https://github.com/zeit/swr) 会 catch 住内部异常,比如:
|
||||
|
||||
```tsx
|
||||
// ... or throw an error when user.id is not defined
|
||||
const { data, error } = useSWR(() => "/api/data?uid=" + user.id, fetcher);
|
||||
```
|
||||
|
||||
如果 `user` 对象不存在,`user.id` 的调用会失败,此时错误会被 catch 住并抛到 `error` 对象。
|
||||
|
||||
实际上,`user.id` 还是一种依赖取数场景,当 `user.id` 发生变化时需要重新取数。
|
||||
|
||||
### 2.5 依赖取数
|
||||
|
||||
如果一个取数依赖另一个取数的结果,那么当第一个数据结束时才会触发新的取数,这在 [swr](https://github.com/zeit/swr) 中不需要特别关心,只需按照依赖顺序书写 `useSWR` 即可:
|
||||
|
||||
```tsx
|
||||
function MyProjects() {
|
||||
const { data: user } = useSWR("/api/user");
|
||||
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
|
||||
|
||||
if (!projects) return "loading...";
|
||||
return "You have " + projects.length + " projects";
|
||||
}
|
||||
```
|
||||
|
||||
[swr](https://github.com/zeit/swr) 会尽可能并行没有依赖的请求,并按依赖顺序一次发送有依赖关系的取数。
|
||||
|
||||
可以想象,如果手动管理取数,当依赖关系复杂时,为了确保取数的最大可并行,往往需要精心调整取数递归嵌套结构,而在 [swr](https://github.com/zeit/swr) 的环境下只需顺序书写即可,这是很大的效率提升。优化方式在下面源码解读章节详细说明。
|
||||
|
||||
依赖取数是自动重新触发取数的一种场景,其实 [swr](https://github.com/zeit/swr) 还支持手动触发重新取数。
|
||||
|
||||
### 2.6 手动触发取数
|
||||
|
||||
`trigger` 可以通过 Key 手动触发取数:
|
||||
|
||||
```tsx
|
||||
import useSWR, { trigger } from "swr";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<Profile />
|
||||
<button
|
||||
onClick={() => {
|
||||
// set the cookie as expired
|
||||
document.cookie =
|
||||
"token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
|
||||
// tell all SWRs with this key to revalidate
|
||||
trigger("/api/user");
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
大部分场景不必如此,**因为请求的重新触发由数据和依赖决定,但遇到取数的必要性不由取数参数决定,而是时机时,就需要用手动取数能力了。**
|
||||
|
||||
### 2.7 乐观取数
|
||||
|
||||
特别在表单场景时,数据的改动是可预期的,此时数据驱动方案只能等待后端返回结果,其实可以优化为本地先修改数据,等后端结果返回后再刷新一次:
|
||||
|
||||
```tsx
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
function Profile() {
|
||||
const { data } = useSWR("/api/user", fetcher);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>My name is {data.name}.</h1>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const newName = data.name.toUpperCase();
|
||||
// send a request to the API to update the data
|
||||
await requestUpdateUsername(newName);
|
||||
// update the local data immediately and revalidate (refetch)
|
||||
mutate("/api/user", { ...data, name: newName });
|
||||
}}
|
||||
>
|
||||
Uppercase my name!
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
通过 `mutate` 可以在本地临时修改某个 Key 下返回结果,特别在网络环境差的情况下加快响应速度。乐观取数,表示对取数结果是乐观的、可预期的,所以才能在结果返回之前就预测并修改了结果。
|
||||
|
||||
### 2.8 Suspense 模式
|
||||
|
||||
在 React Suspense 模式下,所有子模块都可以被懒加载,包括代码和请求都可以被等待,只要开启 `suspense` 属性即可:
|
||||
|
||||
```tsx
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.9 错误处理
|
||||
|
||||
`onErrorRetry` 可以统一处理错误,包括在错误发生后重新取数等:
|
||||
|
||||
```tsx
|
||||
useSWR(key, fetcher, {
|
||||
onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
|
||||
if (retryCount >= 10) return;
|
||||
if (error.status === 404) return;
|
||||
|
||||
// retry after 5 seconds
|
||||
setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 3 精读
|
||||
|
||||
### 3.1 全局配置
|
||||
|
||||
在 Hooks 场景下,包装一层自定义 `Context` 即可实现全局配置。
|
||||
|
||||
首先 `SWRConfig` 本质是一个定制 `Context Provider`:
|
||||
|
||||
```tsx
|
||||
const SWRConfig = SWRConfigContext.Provider;
|
||||
```
|
||||
|
||||
在 `useSWR` 中将当前配置与全局配置 Merge 即可,通过 `useContext` 拿到全局配置:
|
||||
|
||||
```tsx
|
||||
config = Object.assign({}, defaultConfig, useContext(SWRConfigContext), config);
|
||||
```
|
||||
|
||||
### 3.2 useSWR 的一些细节
|
||||
|
||||
从源码可以看到更多细节用心,`useSWR` 真的比手动调用 `fetch` 好很多。
|
||||
|
||||
**兼容性**
|
||||
|
||||
`useSWR` 主体代码在 `useEffect` 中,但是为了将请求时机提前,放在了 UI 渲染前(`useLayoutEffect`),并兼容了服务端场景:
|
||||
|
||||
```tsx
|
||||
const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect;
|
||||
```
|
||||
|
||||
**非阻塞**
|
||||
|
||||
请求时机在浏览器空闲时,因此请求函数被 `requestIdleCallback` 包裹:
|
||||
|
||||
```tsx
|
||||
window["requestIdleCallback"](softRevalidate);
|
||||
```
|
||||
|
||||
`softRevalidate` 是开启了去重的 `revalidate`:
|
||||
|
||||
```tsx
|
||||
const softRevalidate = () => revalidate({ dedupe: true });
|
||||
```
|
||||
|
||||
即默认 2s 内参数相同的重复取数会被取消。
|
||||
|
||||
**性能优化**
|
||||
|
||||
由于 [swr](https://github.com/zeit/swr) 的 `data`、`isValidating` 等数据状态是利用 `useState` 分开管理的:
|
||||
|
||||
```tsx
|
||||
let [data, setData] = useState(
|
||||
(shouldReadCache ? cacheGet(key) : undefined) || config.initialData
|
||||
);
|
||||
// ...
|
||||
let [isValidating, setIsValidating] = useState(false);
|
||||
```
|
||||
|
||||
而取数状态变化时往往 `data` 与 `isValidating` 要一起更新,为了仅触发一次更新,使用了 <del>`unstable_batchedUpdates` 将更新合并为一次:</del>
|
||||
|
||||
```tsx
|
||||
unstable_batchedUpdates(() => {
|
||||
setIsValidating(false);
|
||||
// ...
|
||||
setData(newData);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
其实还有别的解法,比如使用 `useReducer` 管理数据也能达到相同性能效果。
|
||||
目前源码已经从`unstable_batchedUpdates`切换为 `useReducer`管理
|
||||
```tsx
|
||||
dispatch(newState);
|
||||
```
|
||||
|
||||
|
||||
### 3.3 初始缓存
|
||||
|
||||
当页面切换时,可以暂时以上一次数据替换取数结果,即初始化数据从缓存中拿:
|
||||
|
||||
```tsx
|
||||
const shouldReadCache = config.suspense || !useHydration();
|
||||
|
||||
// stale: get from cache
|
||||
let [data, setData] = useState(
|
||||
(shouldReadCache ? cacheGet(key) : undefined) || config.initialData
|
||||
);
|
||||
```
|
||||
|
||||
上面一段代码在 `useSWR` 的初始化期间,`useHydration` 表示是否为初次加载:
|
||||
|
||||
```tsx
|
||||
let isHydration = true;
|
||||
|
||||
export default function useHydration(): boolean {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
isHydration = false;
|
||||
}, 1);
|
||||
}, []);
|
||||
|
||||
return isHydration;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 支持 suspense
|
||||
|
||||
Suspense 分为两块功能:异步加载代码与异步加载数据,现在提到的是异步加载数据相关的能力。
|
||||
|
||||
Suspense 要求代码 suspended,即抛出一个可以被捕获的 Promise 异常,在这个 Promise 结束后再渲染组件。
|
||||
|
||||
核心代码就这一段,抛出取数的 Promise:
|
||||
|
||||
```tsx
|
||||
throw CONCURRENT_PROMISES[key];
|
||||
```
|
||||
|
||||
等取数完毕后再返回 `useSWR` API 定义的结构:
|
||||
|
||||
```tsx
|
||||
return {
|
||||
error: latestError,
|
||||
data: latestData,
|
||||
revalidate,
|
||||
isValidating
|
||||
};
|
||||
```
|
||||
|
||||
如果没有上面 `throw` 的一步,在取数完毕前组件就会被渲染出来,所以 `throw` 了请求的 Promise 使得这个请求函数支持了 Suspense。
|
||||
|
||||
### 3.5 依赖的请求
|
||||
|
||||
翻了一下代码,没有找到对循环依赖特别处理的逻辑,**后来看了官方文档才恍然大悟,原来是通过 `try/catch` 并巧妙结合 React 的 UI=f(data) 机制实现依赖取数的。**
|
||||
|
||||
看下面这段代码:
|
||||
|
||||
```tsx
|
||||
const { data: user } = useSWR("/api/user");
|
||||
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
|
||||
```
|
||||
|
||||
怎么做到智能按依赖顺序请求呢?我们看 `useSWR` 取数函数的主体逻辑:
|
||||
|
||||
```tsx
|
||||
const revalidate = useCallback(
|
||||
async() => {
|
||||
try {
|
||||
// 设置 isValidation 为 true
|
||||
// 取数、onSuccess 回调
|
||||
// 设置 isValidation 为 false
|
||||
// 设置缓存
|
||||
// unstable_batchedUpdates
|
||||
} catch (err) {
|
||||
// 撤销取数、缓存等对象
|
||||
// 调用 onError回调
|
||||
}
|
||||
},
|
||||
[key]
|
||||
)
|
||||
|
||||
useIsomorphicLayoutEffect(
|
||||
()=>{
|
||||
....
|
||||
},
|
||||
[key,revalidate,...]
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
每次渲染的时候,SWR 会试着执行 `key` 函数(例如 () => "/api/projects?uid=" + user.id),如果这个函数抛出异常,那么就意味着它的依赖还没有就绪(user === undefined),SWR 将暂停这个数据的请求。在任一数据完成加载时,由于 `setState` 触发重渲染,上述 Hooks 会被重选执行一遍(再次检查数据依赖是否就绪)然后对就绪的数据发起新的一轮请求。
|
||||
|
||||
另外对于一些正常请求碰到 error(shouldRetryOnError 默认为 true)的情况下,下次取数的时机是:
|
||||
|
||||
```tsx
|
||||
const count = Math.min(opts.retryCount || 0, 8);
|
||||
const timeout =
|
||||
~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval;
|
||||
```
|
||||
|
||||
重试时间基本按 2 的指数速度增长。
|
||||
|
||||
所以 [swr](https://github.com/zeit/swr) 会优先按照并行方式取数,存在依赖的取数会重试,直到上游 Ready。这种简单的模式稍稍损失了一些性能(没有在上游 Ready 后及时重试下游),但不失为一种巧妙的解法,而且最大化并行也使得大部分场景性能反而比手写的好。
|
||||
|
||||
## 4 总结
|
||||
|
||||
笔者给仔细阅读本文的同学留下两道思考题:
|
||||
|
||||
- 关于 Hooks 取数还是在数据流中取数,你怎么看呢?
|
||||
- swr 解决依赖取数的方法还有更好的改进办法吗?
|
||||
|
||||
> 讨论地址是:[精读《Hooks 取数 - swr 源码》 · Issue #216 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/216)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,630 @@
|
||||
## 1 引言
|
||||
|
||||
这是继 [精读《React Conf 2019 - Day1》](https://github.com/dt-fe/weekly/blob/v2/127.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day1%E3%80%8B.md) 之后的第二篇,补充了 React Conf 2019 第二天的内容。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
第二天的内容更为精彩,笔者会重点介绍比较干货的部分。
|
||||
|
||||
### Fast refresh
|
||||
|
||||
[Fast refresh](https://facebook.github.io/react-native/docs/fast-refresh) 是更好的 react-hot-loader 替代方案,目前仅支持 react-native 平台,很快就会支持 react-dom 平台。
|
||||
|
||||
相比不支持 Function component、无法错误恢复、更新经常失灵的 hot reloading 来说,fast refresh 还拥有以下几个优点:
|
||||
|
||||
- 状态保持。
|
||||
- 支持 Function Component Hooks。
|
||||
- 更快的更新速度。
|
||||
|
||||
Fast refresh 更新速度更快,是基于 Function Component 生成了 “签名”,从而最大成都避免销毁重渲染,尽可能保持对组件的 rerender 刷新。下面介绍签名机制的工作原理。
|
||||
|
||||
Fast refresh 对每个 Function component 都生成了一份专属签名,用以描述这个组件核心状态,当这个核心状态改变时,就只能销毁重渲染了,但对于不触及核心的修改就能进行代价非常小的 rerender。
|
||||
|
||||
这个签名包含了 hooks 和参数名:
|
||||
|
||||
```js
|
||||
// signature: "useState{isLoggedIn}"
|
||||
|
||||
function ExampleComponent() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(true);
|
||||
}
|
||||
```
|
||||
|
||||
比如当参数名变更时,这个组件的逻辑已发生改动,此时只能销毁并重渲染了。因此实际上通过对签名的对比来判断是否要销毁并重刷新组件:
|
||||
|
||||
```js
|
||||
// signature: "useState{isLoggedOut}"
|
||||
|
||||
function ExampleComponent() {
|
||||
const [isLoggedOut, setIsLoggedOut] = useState(true);
|
||||
}
|
||||
```
|
||||
|
||||
同理,当 hooks 从 `useState` 改成了 `useReducer`,签名也会发生变化从而导致彻底的重渲染。
|
||||
|
||||
但除此之外,**比如对样式的修改、Dom 结构的修改都不会触发签名的变化**,从而保证了 “对不触及逻辑的改动进行高效的轻量 renreder”。
|
||||
|
||||
然而 Fast refresh 也有如下局限性:
|
||||
|
||||
- 还不能友好支持 Class component。
|
||||
- 混合导出 React 和非 React 组件时无法精确的 hot reload。
|
||||
- 更高的内存要求。
|
||||
|
||||
可以看到,Fast Refresh 随着功能推广与内置,现在已经覆盖了 Facebook 95% 以上 hot reload 场景了:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1bjm1mYr1gK0jSZR0XXbP8XXa-1598-1018.png">
|
||||
|
||||
这部分内容不仅揭开了 hot reload 技术内幕,还对其功能进行了进一步优化,2019 年的 React 开发体系已经进入精细化阶段。
|
||||
|
||||
### 重写 React devtools
|
||||
|
||||
React devtools 的更新终于被正式介绍了,本来笔者以为新的 devtools 只是支持了 hooks,但听完分享后发现还有更多有用的改进,包括:
|
||||
|
||||
- 更高的性能。
|
||||
- 更多特性支持。
|
||||
- 更好用户体验。
|
||||
|
||||
**找到节点渲染链路**
|
||||
|
||||
并不是每个 React 节点都参与渲染,新版 React devtools 可以展示出 rendered by:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1OSWomV67gK0jSZPfXXahhFXa-2354-668.png">
|
||||
|
||||
**调试 Suspense**
|
||||
|
||||
在 Day1 中讲到的 Suspense 特性可以在 React devtools 调试了:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1W790m4D1gK0jSZFsXXbldVXa-1816-660.png">
|
||||
|
||||
通过点击时钟 icon,可以模拟 Suspense 处于 pendding 或 ready 状态。
|
||||
|
||||
**增强调试能力**
|
||||
|
||||
可以通过点击直接跳转到组件源码:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1IBK1m4n1gK0jSZKPXXXvUXXa-1806-772.png">
|
||||
|
||||
最新版已增强至点击按钮后直接通过 Source 打开源码位置,**这样可以快速通过 UI 寻找到代码**。同时还可以看到,通过点击 debugger 按钮将当前组件信息打到控制台调试。
|
||||
|
||||
除此之外还可以动态修改组件的 props 与 hook state,大大增强了调试能力。
|
||||
|
||||
**profiler**
|
||||
|
||||
分析工具也得到了增强,现在可以看到每个组件被渲染了几次以及重新渲染的原因:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1cla1m7P2gK0jSZPxXXacQpXa-1824-690.png">
|
||||
|
||||
比如上图组件被渲染了 4 次,主要有两个原因:Hooks 改变与 Props 改变。
|
||||
|
||||
除此之外,还优化了更多细节体验,比如高亮搜索、HOC 的展示优化、嵌套层级过多时不会占用过多的横向宽度等等。
|
||||
|
||||
### react codemod
|
||||
|
||||
codemod 是一个代码重构的方式,通过 AST 方式精准触达代码,我们可以认为 codemod 是一个更聪明的“查找/替换”。
|
||||
|
||||
codemod 主要有以下三种使用方式:
|
||||
|
||||
- 重命名。
|
||||
- 代码排序。
|
||||
- 一定程度的代码替换。
|
||||
|
||||
接下来就讲到 [react codemod](https://github.com/reactjs/react-codemod) 了,它是 react 场景的 codemod 解决方案,facebook 是这么使用 react codemod 的:
|
||||
|
||||
- 迁移 facebook 代码。
|
||||
- 涉及几万个组件。
|
||||
- 修复了 3500 个文件的 React.PropTypes。
|
||||
- 修复了 8500 个文件的生命周期 unsafe。
|
||||
- 修复了 20000 个文件的 createClass 转 JSX。
|
||||
|
||||
使用方式:
|
||||
|
||||
```bash
|
||||
npx react-codemod React-PropTypes-to-prop-types
|
||||
```
|
||||
|
||||
可以看到,通过 cli 对文件进行一次性重构处理。除此之外,再列举几种使用场景:
|
||||
|
||||
- create-element-to-jsx 将 `React.createElement` 转换为 JSX。
|
||||
- error-boundaries 将 `unstable_handleError` 改为 `componentDidCatch`。
|
||||
- findDOMNode 将 `React.createClass` 中 `this.getDOMNode()` 改为 `React.findDOMNode`。
|
||||
- sort-comp 将 Class Component 生命周期按照规范排序,[eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md) 插件也有相同能力。
|
||||
|
||||
理论上来讲,所有 codemode 做的事情都可以替换为 eslint 的 autofix 来完成,比如 sort-comp 就同时被 codemode 和 eslint 支持。
|
||||
|
||||
### Suspense
|
||||
|
||||
要理解 Suspense,就要理解 Suspense 与普通 loading 有什么区别。
|
||||
|
||||
从代码角度来说,Suspense 可以类比为 `try/catch` 的体验。为了简化代码复杂度,我们可以用 `try/catch` 包裹代码,从而简化 try 区块代码复杂度,并将兜底代码放在 catch 区块:
|
||||
|
||||
```js
|
||||
try {
|
||||
// 只要考虑正确情况
|
||||
} catch {
|
||||
// 错误时 fallback
|
||||
}
|
||||
```
|
||||
|
||||
Suspense 也一样,它在渲染 React 组件时如果遇到了 Promise 抛出的 Error,就会进入 `fallback`,所以 `fallback` 含义是 Loading 中状态:
|
||||
|
||||
```jsx
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<ProfilePage />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
与此同时,实际业务组件中的取数也不需要担心取数是否正在进行中,只要直接处理拿到数据的情况就好了:
|
||||
|
||||
```jsx
|
||||
function ProfileDetails() {
|
||||
// 直接使用 user,不用担心失败。
|
||||
const user = resource.user.read();
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
进一步的,如果要处理组件渲染的异常,再使用 `ErrorBoundary` 包裹即可,此时的 `fallback` 含义是组件加载异常的错误状态:
|
||||
|
||||
```jsx
|
||||
function Home(props) {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorMessage />}>
|
||||
<Suspense fallback={<Placeholder />}>
|
||||
<Composer />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Suspense 模式的取数好处是 “fetch on render”,即渲染与取数同时进行,而普通模式的取数是 “fetch after render”,即渲染完成后再通过 `useEffect` 取数,此时取数时机已晚。
|
||||
|
||||
**队列加载**
|
||||
|
||||
假设 `Composer` 与 `NewsFeed` 组件内部都通过 `useQuery` 取数,那么并行取数时加载机制如下:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1AonZm7L0gK0jSZFtXXXQCXXa-1770-778.png">
|
||||
|
||||
这可能有两个问题:组件内部加载顺序不统一与组件间加载顺序不统一。
|
||||
|
||||
如果组件内部有图片,可能图片与组件渲染实际不一致,此时可以利用 Suspense 统一 hold 所有子组件的特性,将图片加载改为 Suspense 模式:
|
||||
|
||||
```jsx
|
||||
<div>
|
||||
<YourImage src={uri} alt={...} />
|
||||
<MoreComposer />
|
||||
</div>
|
||||
```
|
||||
|
||||
同一个 Suspense 可以等待所有子元素都 Ready 后才会一把渲染出 UI,因此可以看到网页被一次性刷新而不是分部刷新。
|
||||
|
||||
第二个问题是组件间加载顺序不统一,可能导致先渲染了文章内容,再渲染出文章头部,此时如果区块高度不固定,文章头部可能会撑开,导致文章内容下移,用户的阅读体验会遭到打断。可以通过 `suspense ordering` 解决这个问题:
|
||||
|
||||
```jsx
|
||||
function Home(props) {
|
||||
return (
|
||||
<SuspenseList revealOrder="forwards">
|
||||
<Suspense fallback={<ComposerFallback />}>
|
||||
<Composer />
|
||||
</Suspense>
|
||||
<Suspense fallback={<FeedFallback />}>
|
||||
<NewsFeed />
|
||||
</Suspense>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
比如 `forwards` 表示从上到下,那么一定会先渲染头部再渲染文章内容,这样文章内容就不会都抖动了。
|
||||
|
||||
### Render as you fetch
|
||||
|
||||
相比 “fetch on render”,更高级别的优化是 “Render as you fetch”,即取数在渲染时机之前。
|
||||
|
||||
比如页面路由的跳转、Hover 到一个区块,此时如果取数由这个动作触发,就可以再次将取数时机提前,Facebook 为此创造了一个新的 Hook:`usePreloadedQuery`。
|
||||
|
||||
用法是,在某个事件中取数,比如点击页面跳转按钮时,通过 `preloadQuery` 预取数,得到的结果并不是取数结果,而是一个标识,在渲染组件中,把这个标识传给 `usePreloadedQuery` 可以拿到真实取数结果:
|
||||
|
||||
```js
|
||||
// 组件 A 的 onClick
|
||||
const reference = preloadQuery(query, variables);
|
||||
// 组件 B 的 render
|
||||
const data = usePreloadedQuery(query, reference);
|
||||
```
|
||||
|
||||
可以看到,取数真正触发的时机在渲染函数执行之前,所以在 `usePreloadedQuery` 调用时取数肯定已经在路上,甚至已经完成。相比之下,普通的 `useQuery` 函数存在下面几个问题:
|
||||
|
||||
- 由于取数过程存在状态变化,可能导致组件在 “取数无意义” 状态下重新渲染多次。
|
||||
- 可能取数还未完成就触发重渲染。
|
||||
- 没有取消的机制,没有清除结果的机制。
|
||||
- 没有办法唯一标识组件。
|
||||
|
||||
preloadQuery 的好处就是将取数时机与 UI 分离,这样可以更细粒度的控制逻辑:
|
||||
|
||||
- 调用 preloadQuery 时:
|
||||
- 在组件销毁时取消取数。
|
||||
- 有新取数触发时取消取数。
|
||||
- 销毁一些轮询机制。
|
||||
- 渲染组件调用 usePreloadedQuery 时:
|
||||
- 不会再触发取数,不会触发意外的 re-render。
|
||||
- 不需要清空,因为取数不在这里发起。
|
||||
- 不需要清理轮询。
|
||||
|
||||
可见 preloadQuery 相比 useQuery 的确有了一些体验提升,然而这个优化比较追求极致,对大部分国内项目来说可能还走不到 facebook 这么极致的性能优化,所以投入产出比显得不是那么高,而且这个开发方式对开发者不是太友好,因为它让请求的时机割裂到两个模块中。
|
||||
|
||||
但毕竟用户体验是大于开发者体验的,React 尽量通过提高开发者体验来间接提高用户体验,使双方都满意,但像 preloadQuery 就无法两者兼顾了,为了用户体验可以适当的降低一些开发者体验。
|
||||
|
||||
### 如何维护代码
|
||||
|
||||
这个分享讲述了如何提升代码维护效率,毕竟一个月后可能连自己写的代码都看不懂了。[hydrosquall](http://github.com/hydrosquall) 通过类比地图的方式解释了程序员是如何维护代码的。
|
||||
|
||||
首先看我们是如何认路的。认路分为三个层次:
|
||||
|
||||
- 随意走走。
|
||||
- 通过一些地标判断方向。
|
||||
- 有方向的寻路。
|
||||
- 通过跟随同伴或者了解更多本地信息找到目的地。
|
||||
- 地图。
|
||||
- 通过 GPS 定位。
|
||||
- 通过模拟地图方式指出路线。
|
||||
|
||||
可以看到这三种方式是逐层递进的,那么类比到代码就有意思了:
|
||||
|
||||
- 随意走走(滚动查看源代码 + ctrl/f 查找代码 + grep 搜索)。
|
||||
- 入口(找到入口节点,查看数据结构)。
|
||||
- 标记(查看代码注释、查看 README)。
|
||||
- 发信号弹(断点、console.log 等调试行为)
|
||||
- 找到方向。
|
||||
- git blame 查看 owner,或直接根据文档找到 codeowners。
|
||||
- 地图。
|
||||
- 幸运的话你可以找到一份架构流程图。
|
||||
|
||||
可以看到,地图有几种抽象层次,比如忽略了细节的纽约地铁线路图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB14k7pmYr1gK0jSZR0XXbP8XXa-1014-702.png">
|
||||
|
||||
或者是包含丰富地面信息的地铁线路图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1Sbwpm.T1gK0jSZFrXXcNCXXa-692-750.png">
|
||||
|
||||
抽象到什么层次取决于用户使用的场景,那么代码抽象也是如此。[hydrosquall](http://github.com/hydrosquall) 做了一个工具自动分析出代码调用关系:[js-callgraph](https://github.com/persper/js-callgraph)
|
||||
|
||||

|
||||
|
||||
这就像路牌一样,可以更高效的看出代码结构,也包括了数据流结构,由于篇幅限制,感兴趣的同学可以看 [原视频](https://youtu.be/JDDxR1a15Yo?t=6579) 了解更多。
|
||||
|
||||
### 写作与写代码
|
||||
|
||||
本章讲了写作(小说)与写代码的关联,总结出如下几个重点:
|
||||
|
||||
- 写小说和写代码都是创造行为。
|
||||
- 写代码需要抽象思维,写小说也要有抽象思维构造人物和情节。
|
||||
- Show, don't tell,写作天然就是申明式的,和数据驱动很相似。
|
||||
|
||||
更多可以去看 [原视频](https://youtu.be/JDDxR1a15Yo?t=9135)。
|
||||
|
||||
### 移动端动画最佳实践
|
||||
|
||||
首先要使用一个真实的手机设备调试,否则可能出现 PC Chrome 一切正常,而手机上实际效果性能很差的情况!
|
||||
|
||||
**手势下拉退出**
|
||||
|
||||
利用 [react-spring](https://github.com/react-spring/react-spring) 和 [react-use-gesture](react-use-gesture) 做一个下滑消失的 Demo:
|
||||
|
||||
```jsx
|
||||
import { animated, useSpring } from "react-spring";
|
||||
import { useDrag } from "react-use-gesture";
|
||||
|
||||
const [{ y }, set] = useSpring(() => {
|
||||
y: 0;
|
||||
});
|
||||
```
|
||||
|
||||
首先定义一个 `y` 纵向位置,通过 `useDrag` 将拖拽操作与 UI 绑定,通过回调将其与 `y` 数据绑定:
|
||||
|
||||
```js
|
||||
const bind = useDrag(({ last, movement: [, movementY], memo = y.value }) => {
|
||||
if (last) {
|
||||
// 拖拽结束时,如果偏移量超过 50 则效果和结束一样,直接将 y 设置为 100
|
||||
const notificationClosed = movementY > 50;
|
||||
|
||||
return set({
|
||||
y: notificationClosed ? 100 : 0,
|
||||
onReset: notificationClosed && removeNotification
|
||||
});
|
||||
}
|
||||
|
||||
// y 的位置区间在 0~100
|
||||
set([{ y: clamp(0, 100, memo + movementY) }]);
|
||||
|
||||
return memo;
|
||||
});
|
||||
```
|
||||
|
||||
将 `useDrag` 与 `y` 绑定后,就可以用在 UI 组件上了:
|
||||
|
||||
```jsx
|
||||
<StyledNotification
|
||||
as={animated.div}
|
||||
onTouchStart={bind().onTouchStart}
|
||||
style={{
|
||||
opacity: y.interpolate([0, 100], [1, 0]),
|
||||
transform: y.interpolate(y => `translateY(${y}px)`)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
将 `opacity` 与 `transform` 与位置 `y` 绑定就可以做出下拉消失的效果。
|
||||
|
||||
**滑动的洞见**
|
||||
|
||||
接着讲到了滑动的三个洞见:
|
||||
|
||||
1. 要立刻响应,任何延迟都会造成用户额外精神负担。
|
||||
2. 滚动速度衰减可以提升用户体验:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1HocDm1H2gK0jSZJnXXaT1FXa-1348-878.gif">
|
||||
|
||||
接着我们需要预测用户的意图,比如在一个类似微信消息列表页左右滑动时:
|
||||
|
||||
- 是否想取消手势交互?
|
||||
- 是否想展示出更多交互按钮?
|
||||
- 是否想删除所有内容?
|
||||
|
||||
这需要更多设计思考。
|
||||
|
||||
1. 橡皮筋滚动,即列表页可以一直向下拉,上面部分像橡皮筋一样可以被拉出空白页的效果。
|
||||
|
||||
在设计手势动画时要考虑三个要点:
|
||||
|
||||
- 使用移动增量作为手势动画的基准点。
|
||||
- 动画和手势应该随时可以被中断,通过 springs 即可实现。
|
||||
- 完成手势后的动画速度应该与手势速度相当,这样视觉体验更自然。
|
||||
|
||||
最后提到了动画兼容性与性能,比如尽量只使用 `transform` 与 `opacity` 可以保证移动端的流畅度,不同移动设备的默认手势效果不同,最好通过 `touch-action` 禁用默认行为以达到更好的兼容性与效果。
|
||||
|
||||
### 唱片与 React
|
||||
|
||||
J.Dash 拥有十年软件开发经验,同时也卖过很多唱片,他介绍了唱片行业与软件开发的共同点。
|
||||
|
||||
唱片行业需要音乐编排能力,这与编码能力类似,都存在良好的设计模式,并且需要团队合作,开发过程中会遇到一些痛苦的经历,但最终完成音乐和项目时都会获得满足的喜悦。
|
||||
|
||||
### 函数式编程
|
||||
|
||||
> Declaratives UIs are the future, and the future is Comonadic. - Phil Freeman
|
||||
|
||||
申明式 UI 是未来,未来则是 Comonadic。
|
||||
|
||||
所谓申明式 UI 可以用下面的公式表达:
|
||||
|
||||
```js
|
||||
type render = (state: State) => View;
|
||||
```
|
||||
|
||||
然后用一段公式介绍了 Comonadic:
|
||||
|
||||
```js
|
||||
class Functor w => Comonad w where
|
||||
extract :: w a -> a
|
||||
duplicate :: w a -> w (w a)
|
||||
extend :: (w a -> a) -> w a -> w b
|
||||
```
|
||||
|
||||
用 JS 版本做一个解释:
|
||||
|
||||
```js
|
||||
const Store = ({ state, render }) => ({
|
||||
extend: f => Store({ state, render: state => f(Store({ state, render })) }),
|
||||
extract: () => render(state)
|
||||
});
|
||||
```
|
||||
|
||||
`extract` 调用后会进行申明式渲染 UI,即 `render(state)`。
|
||||
|
||||
`extend` 表示拓展,接收一个拓展函数作为参数,返回一个新的 Store 对象。这个拓展函数可以拿到 `state`、`render` 并返回新的 `state` 作为 `extract` 时 `render` 的输入。使用例子是这样的:
|
||||
|
||||
```jsx
|
||||
const App = Store({
|
||||
state: { msg: "World" },
|
||||
render: ({ msg }) => <p>Hello {msg}</p>
|
||||
});
|
||||
|
||||
App.extend(({ state }) =>
|
||||
state.msg === "World" ? { msg: "ReactConf" } : state
|
||||
).extract(); // <p> Hello ReactConf </p>
|
||||
```
|
||||
|
||||
然而尴尬的是,笔者看了很久也没看懂 `Store` 函数,最后运行了一下发现这个 Demo 抛出了异常 😂。
|
||||
|
||||
下面是笔者稍微修改后的例子,至少能跑起来:
|
||||
|
||||
```js
|
||||
const Store = ({ state, render }) => ({
|
||||
extend: f => Store({ state, render: state => render(f({ state, render })) }),
|
||||
extract: () => render(state)
|
||||
});
|
||||
|
||||
const app = Store({
|
||||
state: { msg: "Hello World" },
|
||||
render: ({ msg }) => console.log("render " + msg)
|
||||
});
|
||||
|
||||
app
|
||||
.extend(({ state }) => {
|
||||
return { msg: state.msg + " extend1" };
|
||||
})
|
||||
.extend(({ state }) => {
|
||||
return { msg: state.msg + " extend2" };
|
||||
})
|
||||
.extract(); // render Hello World extend2 extend1
|
||||
```
|
||||
|
||||
然而作者的意思仍是未解之谜,希望对函数式了解的同学可以在评论区指点一下。
|
||||
|
||||
### wick editor
|
||||
|
||||
[wick editor](https://www.wickeditor.com/) 是一个开源的动画、游戏制作软件。
|
||||
|
||||
wick editor 是一个动画制作工具,但拓展了一些 js 编程能力,因此可以很好的将动画与游戏结合在一起:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1hLJpnbr1gK0jSZR0XXbP8XXa-1766-1002.png">
|
||||
|
||||
演讲介绍了 wick editor 的演化过程:
|
||||
|
||||
从很简陋的 MVP 版本开始(1 周)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB11sdsneL2gK0jSZFmXXc7iXXa-1192-764.png">
|
||||
|
||||
到 Pre-Alpha(4 月)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1TMJnnXY7gK0jSZKzXXaikpXa-1306-858.png">
|
||||
|
||||
Alpha(5 月)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1608pnoD1gK0jSZFGXXbd3FXa-1794-1186.png">
|
||||
|
||||
Beta(1.5 年)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1CKJpnoD1gK0jSZFGXXbd3FXa-1274-854.png">
|
||||
|
||||
重点是 1.0 版本采用 React 重写了!继 Beta 之后又经历了 1 年:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1ZZhrneH2gK0jSZFEXXcqMpXa-906-596.png">
|
||||
|
||||
这个团队最棒的地方是,将游戏与教育结合,针对不同场景做了很多用户调研并根据反馈持续改进。
|
||||
|
||||
### React Select
|
||||
|
||||
[react-select](https://github.com/JedWatson/react-select) 的作者 [Jed Watson](https://github.com/JedWatson) 被请来啦。作为一个看上去很简单组件(select)的开发者,却拥有如此大的关注量(1.8w star),那作者有着怎样的心路历程呢?
|
||||
|
||||
react-select 看似简单的名字背后其实有挺多的功能,比如作者列举了一些功能层面的内容:
|
||||
|
||||
- autocomplete - 输入时搜索。
|
||||
- 单、多选。
|
||||
- focus 管理。
|
||||
- 下拉框层级与位置,比如可以放在根 DOM 节点,也可以作为当前节点的子元素。
|
||||
- 异步下拉框内容。
|
||||
- 键盘、触控。
|
||||
- Createble,即在搜索时如果没有内容可以动态创建。
|
||||
- 等等。
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1RYS1mubviK0jSZFNXXaApXXa-1166-226.png">
|
||||
|
||||
在设计层面:
|
||||
|
||||
- 申明式。
|
||||
- 可以被定制。
|
||||
- 性能要求。
|
||||
- 等等。
|
||||
|
||||
随着 Star 逐渐上涨,越来越多的需求被提出,核心库代码量越来越大,甚至许多需求之间都是相互冲突的,而且作者每天都会被上百个 Issue 与 PR 吵醒。做一个业务 Select 可能只要 5 分钟,但做一个开源 Select 却要 5 年,原因是一个简单的 Select 如何满足所有不同业务场景?这绝对是个巨大的挑战。
|
||||
|
||||
比如用户即需要受控也要非受控的组件,如何满足好这个需求同时又让代码更可维护呢?
|
||||
|
||||
假设我们拥有一个受控的组件 `SelectComponent`,那么它的主要 props 是 `value` 与 `onChange`,如果要拓展成一个既支持 `defaultValue`(非受控)又支持 `value`(受控)的组件,我们可以创建一个 `manageState` 组件对 `SelectComponent` 进行封装:
|
||||
|
||||
```jsx
|
||||
const manageState = SelectComponent => ({
|
||||
value: valueProps,
|
||||
onChange: onChangeProp,
|
||||
defaultValue,
|
||||
...props
|
||||
}) => {
|
||||
const [valueState, setValue] = useState(defaultValue);
|
||||
|
||||
const value = valueProps !== undefined ? valueProps : valueState;
|
||||
|
||||
const onChange = (newValue, actionMeta) => {
|
||||
if (typeof onChangeProp === "function") {
|
||||
onChangeProp(newValue, actionMeta);
|
||||
}
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return <SelectComponent {...props} value={value} onChange={onChange}>
|
||||
};
|
||||
```
|
||||
|
||||
这样就可以组合为一个受控/非受控的综合 Select 组件:
|
||||
|
||||
```js
|
||||
import BaseSelect from "./Select";
|
||||
import manageState from "./manageState";
|
||||
|
||||
export default manageState(Select);
|
||||
```
|
||||
|
||||
同理对异步的封装也可以放在 `makeAsync` 函数中:
|
||||
|
||||
```jsx
|
||||
const makeAsync = SelectComponent => ({
|
||||
getOptions,
|
||||
defaultOptions,
|
||||
...props
|
||||
}) => {
|
||||
const [options, setOptions] = useState(defaultOptions);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onInputChange = async newValue => {
|
||||
setIsLoading(true);
|
||||
const newOptions = await getOptions(newValue);
|
||||
setIsLoading(false);
|
||||
setOptions(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectComponent
|
||||
{...props}
|
||||
options={options}
|
||||
isLoading={isLoading}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
可以看到,`SelectComponent` 是一个完全受控的数据驱动的 UI,无论是 `manageState` 还是 `makeAsync` 都是对数据处理的拓展,所以这三者之间才可以融洽的组合:
|
||||
|
||||
```js
|
||||
import BaseSelect from "./Select";
|
||||
import manageState from "./manageState";
|
||||
import makeAsync from "./async";
|
||||
|
||||
export default manageState(Select);
|
||||
|
||||
export const AsyncSelect = manageState(makeAsync(Select));
|
||||
```
|
||||
|
||||
后面还有一些风格化、开源协作的思考,这里就不展开了,对这部分感兴趣的同学可以查看原视频了解更多。
|
||||
|
||||
### React + 政府财政透明项目
|
||||
|
||||
usaspending.gov 这个网站使用 React 建设,可以查看美国政府支持财政的明细,通过流畅的体验让更多用户可以了解国家财政支出,进一步推动财政支出的透明化。由于并不涉及前端技术的介绍,主要是产品介绍,因此精读就不详细展开了。
|
||||
|
||||
顺便说一句,智能分析数据就用 [QuickBI](https://www.alibabacloud.com/zh/product/quickbi),QuickBI 是我们团队研发的一款智能 BI 服务平台,如果你将美国政府的财政支持作为数据集输入,你会分析得更透彻。
|
||||
|
||||
### React + 星舰模拟器
|
||||
|
||||
最后介绍的是使用 React 制作的星舰模拟器,看上去像一个游戏:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1HrxAneL2gK0jSZPhXXahvXXa-1946-1104.png">
|
||||
|
||||
有星系图、船体、驾驶员信息、武器装备、燃料、通信等等内容。甚至可以模拟太空驾驶,进行任务,可以实时多人协同。对太空迷们的吸引力很大,感兴趣的同学建议直接观看 [视频](https://youtu.be/JDDxR1a15Yo?t=28638)。
|
||||
|
||||
## 3 总结
|
||||
|
||||
第二天的内容非常全面,涉及了 React API、开发者周边、codemod 工具、代码维护、写作/音乐与代码、动画、函数式编程、看似简单的 React 组件、使用 React 制作的各种脑洞大开的项目,等等。
|
||||
|
||||
React Conf 要展示的是一个完整的 React 世界,第一天提到了 React 是一个桥梁,正因为这个桥梁,连接了各行各业不同的人群以及不同的项目,大家都有一个共同的语言:React。
|
||||
|
||||
"We not only react code, but react the world"。
|
||||
|
||||
> 讨论地址是:[精读《React Conf 2019 - Day2》 · Issue #217 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/217)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,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))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user