Compare commits

...
71 Commits
Author SHA1 Message Date
ascoders 069cf8f947 130 2019-11-25 08:58:49 +08:00
ascoders af86669e18 fix typo 2019-11-22 13:36:30 +08:00
ascoders 50c4409e29 fix typo 2019-11-22 13:35:05 +08:00
ascoders 5c44507443 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2019-11-18 08:47:57 +08:00
ascoders ffb736eb6d 129 2019-11-18 08:47:36 +08:00
黄子毅 abc677a5f0 Merge pull request #215 from vivaxy/patch-1
Update 114.精读《谁在世界中心》.md
2019-11-12 16:36:26 +08:00
ascoders 3c78a1a659 fix typo 2019-11-11 11:42:00 +08:00
ascoders 507df796b0 128 2019-11-11 11:28:14 +08:00
ascoders 0f58ce27dc fix typo error 2019-11-04 10:18:03 +08:00
ascoders 51c8f3bb69 127 2019-11-04 08:42:31 +08:00
vivaxy c4da7262cb Update 114.精读《谁在世界中心》.md 2019-11-03 09:35:36 +08:00
ascoders a513286318 126 2019-10-28 08:54:07 +08:00
ascoders 509dfe2c97 125 2019-10-21 08:55:05 +08:00
ascoders 806ee0177a fix: 修复歧义 2019-10-16 20:30:07 +08:00
ascoders fe4afdf89c 124 2019-10-14 09:09:13 +08:00
ascoders caec16066a 123 2019-10-08 09:33:26 +08:00
ascoders 7f53bde9a3 122 2019-09-29 09:11:00 +08:00
ascoders 63e2ca4d0d 121 2019-09-16 12:03:11 +08:00
黄子毅 9dbd1fb7b9 Merge pull request #205 from leiyaguang/patch-1
Update 104.精读《Function Component 入门》.md
2019-09-14 20:06:47 +08:00
黄子毅 7d8816c2c2 Merge pull request #206 from leiyaguang/patch-2
Update 079.精读《React Hooks》.md
2019-09-14 20:06:35 +08:00
黄子毅 44ae420f52 Merge pull request #207 from leiyaguang/patch-3
Update 080.精读《怎么用 React Hooks 造轮子》.md
2019-09-14 20:06:24 +08:00
mr_left 683b22d1ae Update 080.精读《怎么用 React Hooks 造轮子》.md 2019-09-11 17:22:13 +08:00
mr_left 4e58379585 Update 079.精读《React Hooks》.md 2019-09-11 16:38:29 +08:00
mr_left 1342144fef Update 104.精读《Function Component 入门》.md
修改了一处代码bug
2019-09-11 16:18:03 +08:00
ascoders a41f452df0 120 2019-09-09 09:42:48 +08:00
ascoders 0950c4cdaf 119 2019-09-04 14:54:04 +08:00
ascoders 46ad26c1a4 修复图片 2019-09-02 09:56:18 +08:00
ascoders 6dd26bee54 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2019-09-02 09:37:32 +08:00
ascoders 37d6b5f3f0 118 2019-09-02 09:37:21 +08:00
黄子毅 f23e88338f Merge pull request #200 from txs1992/patch-2
fix: typo
2019-08-29 23:20:43 +08:00
MT 3dda46d760 Update 001.精读 js 模块化发展.md
fix:修改错别字。
2019-08-29 15:08:53 +08:00
MT cd6a00bf3a Update 001.精读 js 模块化发展.md
fix: typo.
2019-08-29 15:04:50 +08:00
ascoders 6aad1da704 117 2019-08-26 08:58:18 +08:00
ascoders 03a5a0f484 add travis icon 2019-08-21 19:03:50 +08:00
ascoders 0066b890d4 add precommit hooks for lint-md 2019-08-21 19:00:29 +08:00
黄子毅 e0caf77d0a Merge pull request #198 from ForkRepo/v2
test(*): add lint-md-cli, and auto fix all issues
2019-08-21 17:30:23 +08:00
hustcc 1517e86999 test(*): add lint-md-cli, and auto fix all issues 2019-08-21 17:19:13 +08:00
黄子毅 c8f86df3f3 Merge pull request #197 from xuhongbo/patch-6
Update 116.精读《刷新》.md
2019-08-19 13:40:17 +08:00
leoxu 752d598d27 Update 116.精读《刷新》.md 2019-08-19 11:24:37 +08:00
ascoders e5fd022f00 116 2019-08-19 08:57:41 +08:00
ascoders f78c4fb511 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2019-08-15 15:07:15 +08:00
ascoders 41a90e5991 fix error 2019-08-15 15:07:10 +08:00
黄子毅 a4efe24cca Merge pull request #195 from joriewong/v2
Update 115.精读《Tableau 入门》.md
2019-08-13 23:15:23 +08:00
Veron 1a3b36074e Update 115.精读《Tableau 入门》.md 2019-08-13 11:43:38 +08:00
黄子毅 8dad4956b7 Merge pull request #194 from LiuL0703/patch-3
fix:typo
2019-08-12 14:08:02 +08:00
Linear-Enter 852d35501c fix:typo 2019-08-12 11:32:28 +08:00
ascoders b9d95182a1 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2019-08-12 08:17:19 +08:00
ascoders bfa3ab7b55 115 2019-08-12 08:17:13 +08:00
黄子毅 304e4d3aa3 Merge pull request #190 from vivaxy/patch-1
Update 092.精读《React PowerPlug 源码》.md
2019-08-05 14:30:23 +08:00
ascoders 62be743112 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2019-08-05 09:11:22 +08:00
ascoders 6d27e723cd update 114 2019-08-05 09:11:20 +08:00
vivaxy 704c9cda9e Update 092.精读《React PowerPlug 源码》.md
补全句子
2019-08-05 09:00:49 +08:00
黄子毅 44dd8057e2 Merge pull request #188 from xuhongbo/patch-5
Update 039.精读《全链路体验浏览器挖矿》.md
2019-07-29 22:54:20 +08:00
leoxu d956b15ad0 Update 039.精读《全链路体验浏览器挖矿》.md
fix type
2019-07-29 20:13:54 +08:00
黄子毅 961d77b4d1 Merge pull request #186 from xuhongbo/patch-3
Update 113.精读《Nodejs V12》.md
2019-07-29 13:23:28 +08:00
黄子毅 c763fc15bd Merge pull request #187 from xuhongbo/patch-4
Update 068.精读衡量用户体验.md
2019-07-29 13:23:16 +08:00
leoxu f9f04d711f Update 068.精读衡量用户体验.md
修改一些用词错误
2019-07-29 11:20:01 +08:00
leoxu 5d40208d45 Update 113.精读《Nodejs V12》.md
不是很通顺
2019-07-29 11:03:00 +08:00
ascoders 771bb9f914 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2019-07-29 08:56:57 +08:00
ascoders 96f850e29f 113 2019-07-29 08:53:44 +08:00
黄子毅 349e7df2b6 Merge pull request #185 from LiuL0703/patch-2
fix:typo
2019-07-27 22:16:19 +08:00
Linear-Enter b198d57cf6 fix:typo 2019-07-27 10:17:15 +08:00
黄子毅 03b8840291 Merge pull request #181 from cpprookie/cpprookie-patch-1
Update 112.精读《源码学习》.md
2019-07-23 09:45:31 +08:00
黄子毅 60899c0bf8 Merge pull request #183 from lengjing/patch-1
Update 082.精读《Htm - Hyperscript 源码》.md
2019-07-23 09:45:20 +08:00
lengjing 648e113e66 Update 082.精读《Htm - Hyperscript 源码》.md 2019-07-22 11:08:32 +08:00
chen tuo 61a5e74908 Update 112.精读《源码学习》.md
修改文字错误
2019-07-22 10:29:10 +08:00
ascoders d2273e72f6 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2019-07-22 09:58:20 +08:00
ascoders 386b605c32 finish 112 2019-07-22 09:19:57 +08:00
黄子毅 b60cca25d7 Merge pull request #180 from LiuL0703/patch-1
fix:typo
2019-07-21 23:43:19 +08:00
Linear-Enter 68e6c23f82 fix:typo 2019-07-21 19:12:01 +08:00
ascoders a9d2c1d47a 111 2019-07-16 08:54:37 +08:00
77 changed files with 6620 additions and 322 deletions
+2
View File
@@ -0,0 +1,2 @@
/node_modules
/yarn.lock
+7
View File
@@ -0,0 +1,7 @@
{
"excludeFiles": [],
"rules": {
"no-long-code": 0,
"no-trailing-punctuation": 0
}
}
+6
View File
@@ -0,0 +1,6 @@
language: node_js
node_js:
- "10"
before_install:
- npm i -g lint-md-cli
script: lint-md ./
+5 -5
View File
@@ -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年左右用的最多的还是 YUI2YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。
从 CommonJS 之前其实都只是封装,并没有一套模块化规范,这个就有些像类与包的概念。我在 10 年左右用的最多的还是 YUI2YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。
```javascript
YUI().use('node', 'event', function (Y) {
@@ -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),每周都有新的主题,每周五发布。**
+1 -1
View File
@@ -72,7 +72,7 @@
### 可访问性的反思
Accessibility 翻译过来是『无障碍访问』,是对不同终端用户的体验完善。每一个模态框,都要有通过键盘关闭的功能,通常使用ESC键。似乎我们程序员多少总会把我们自我的惯性思维带进实现的产品,尤其是当我们敲着外置的键盘,用着 PC 的时候。
Accessibility 翻译过来是『无障碍访问』,是对不同终端用户的体验完善。每一个模态框,都要有通过键盘关闭的功能,通常使用 ESC 键。似乎我们程序员多少总会把我们自我的惯性思维带进实现的产品,尤其是当我们敲着外置的键盘,用着 PC 的时候。
下面的这些问题都是对可访问性的反思:
+6 -6
View File
@@ -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 storeredux
这个 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 等框架真应该学一学。不过全家桶的方案比较适合全新项目使用,旧项目使用要评估好成本
+3 -3
View File
@@ -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 数据形态,是原始数据还是视图数据?
+3 -3
View File
@@ -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 对象,并附件尽可能多的错误信息,可以使用标准的属性名
+1 -1
View File
@@ -81,7 +81,7 @@ react-css-modules 引入了 styleName,将本地变量和全局变量很清晰
另外,使用 react-css-modules,可以方便的覆盖本地变量的样式:
```
```plain
import customStyles from './table-custom-styles.css';
<Table styles={customStyles} />;
+1 -1
View File
@@ -12,7 +12,7 @@
# 2 内容概要
使用 Object.assign 作用于大对象时,速度会成为瓶颈,比如拥有 `100,000` 个属性的对象,这个操作耗费了 134ms。性能损失主要原因是 “结构共享” 操作需要遍历近10万个属性,而这些引用操作耗费了100ms以上的时间。
使用 Object.assign 作用于大对象时,速度会成为瓶颈,比如拥有 `100,000` 个属性的对象,这个操作耗费了 134ms。性能损失主要原因是 “结构共享” 操作需要遍历近 10 万个属性,而这些引用操作耗费了 100ms 以上的时间。
解决办法就是减少引用指向的操作数量,而且由于引用指向到任何对象的损耗都几乎一致(无论目标对象极限小或者无穷大,引用消耗时间都几乎没有区别),我们需要一种精心设计的树状结构将打平的引用建立深度,以减少引用操作次数,`vector tries` 就是一种解决思路:
+16 -16
View File
@@ -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). Safari10版本中, 支持了 [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). Safari10 版本中, 支持了 [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 Components2011年到2017年这6年间毫无进展, 一共产出了6份标准, 其中两份已经被弃用. 几乎只有一个主流浏览器(chrome) 支持.
原文作者 dmitriid 主要是在喷 Web Components2011 年到 2017 年这 6 年间毫无进展, 一共产出了 6 份标准, 其中两份已经被弃用. 几乎只有一个主流浏览器(chrome) 支持.
![image](https://dmitriid.com/assets/img/blog/web-components-support.png)
- 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
View File
@@ -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 的一个插件,协助移动页面调试。
+2 -2
View File
@@ -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
![image](https://user-images.githubusercontent.com/9314735/27116337-3f1f16a8-5103-11e7-8dc6-c7197e1b1eab.png)
至于 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
View File
@@ -215,7 +215,7 @@ var bar = foo.bind(obj)
bar()
```
### 3.2.2 es6绑定
### 3.2.2 es6 绑定
这种情况类似使用箭头函数创建成员变量,以下方式等于创建了没有挂载到原型链的匿名函数,因此 this 不会丢失。
+21 -21
View File
@@ -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)PMMVVMMVC](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)PMMVVMMVC](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, G2Recharts, FusionChart; 等等;
在一个 BI 系统中, 在业务的发展中, 这个系统使用到了多套的 底层图表库,比如: Echarts, G2Recharts, FusionChart; 等等;
那么问题来了,
1. 如何去同时支持 这些底层库, 并且达到很容易切换的一个效果?
@@ -234,19 +234,19 @@ DCI 重点是关注 数据的不同场景的交互行为, 是面向对象系
3. 如何去考虑扩展业务 对图表的日益增强的业务功能(如: 行列转换、智能格式化 等等)
带着这些问题, 我们再来看下 DCI 给我们的启示, 我们来试试看相应的解法:
1. 图表的模型数据就是 数据Data , 我们可以把[日益增强的业务功能] 认为是各个场景交互Interactions;
1. 图表的模型数据就是 数据 Data , 我们可以把[日益增强的业务功能] 认为是各个场景交互 Interactions;
2. 接入更多类型的图表咋么搞?
不同类型的图表其实是图表数据模型的转换,我们也可以把这些转换的行为过程作为一个个的切片(Aspect),每个切片都是独立的, 松耦合的 ;
![image](https://user-images.githubusercontent.com/1456421/27744526-67fd0e3e-5d85-11e7-9b48-e1934d9b15f3.png)
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 也会使用一些面向切面和接口编程的设计思想去达到高内聚低耦合的目标。
+13 -13
View File
@@ -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 @@
![](https://pic1.zhimg.com/v2-cd4874c5dc98505c56b05dbd3193fa78_b.gif)
采用与用户交互的方式选择日期,如果今后应用上AI,单纯的日期选择器是不是会消失不见呢?..
采用与用户交互的方式选择日期,如果今后应用上 AI,单纯的日期选择器是不是会消失不见呢?..
## 3.5 特殊标识周末
![](https://pic1.zhimg.com/v2-d8410bede19d7bd4c212ad216ebd0770_b.png)
在机票、旅行场景中,周末是大家最有可能出行的时间点,采用竖线划分的方式着重标注提醒。
@@ -106,6 +106,6 @@
![](https://pic3.zhimg.com/v2-ec840145feb22eeac76e5a0503828436_b.png)
总得来说,日期选择器是一个业务组件,虽然现有很多组件库把它纳入UI基础组件。但在每个不通的业务场景和需求下的展现形式、交互都会有所有不同。首先一定一定要明确确定需要日期选择器的场景,尤其是与日期强关联的业务,比如机票定价、日程安排,结合到日期选择器中更直观,提高用户对信息的检索效率。满足用户需求场景的同时,尽量减少用户操作链路。
总得来说,日期选择器是一个业务组件,虽然现有很多组件库把它纳入 UI 基础组件。但在每个不通的业务场景和需求下的展现形式、交互都会有所有不同。首先一定一定要明确确定需要日期选择器的场景,尤其是与日期强关联的业务,比如机票定价、日程安排,结合到日期选择器中更直观,提高用户对信息的检索效率。满足用户需求场景的同时,尽量减少用户操作链路。
看到最后点个赞呗,给你比小心心 ❤ ~~
@@ -40,7 +40,7 @@
正如上面所说,我推荐以开放性问题开场,这样便于了解候选人的经历、熟悉哪些技术点,便于后面的技术提问。如果开场就以准备好的题目展开车轮战,容易引起候选人心里紧张,同时我们问的问题不一定是候选人所在行的,技术问题不是每一个都那么重要,很多时候我们只看到了候选人的冰山一角,但此时气氛已经尴尬,很多时候会遗漏优秀人才。
开放性问题最好基于行为面试法询问(Star法则):
开放性问题最好基于行为面试法询问(Star 法则):
- Situation: 场景 - 当时是怎样的场景
- Task: 任务 - 当时的任务是什么
@@ -83,7 +83,7 @@
面试主要是看候选人基础有多扎实,和思维能力。基础主要指的是,候选人提前了解了多少前端相关知识,比如对闭包的理解,对原生 api 的理解?如果候选人没接触过这两个知识点,会有两种情况:
- **这些知识点看完需要多久?如果是闭包和原生api的定义与用法,候选人这方面的缺陷可以通过5分钟来弥补,那么这种问题到底想考什么?我们真的在乎这5分钟看文档的时间吗?此时应该了解候选人对知识点的感悟,或者学习方式,因为这两点的差距可能几年都无法弥补**
- **这些知识点看完需要多久?如果是闭包和原生 api 的定义与用法,候选人这方面的缺陷可以通过 5 分钟来弥补,那么这种问题到底想考什么?我们真的在乎这 5 分钟看文档的时间吗?此时应该了解候选人对知识点的感悟,或者学习方式,因为这两点的差距可能几年都无法弥补**
- **如果候选人学习能力非常强,但几乎所有前端知识点都不了解,弥补完大概一共要花 1000*5 分钟,这时候量变引发质变了,是不是说明候选人本身对技术的热情存在问题?**
通过了基础问题还远远不够。甚至当问一个复杂的问题的时候,如果候选人瞬间把答案完美流畅表达出来,说明这个问题基本上白问了。
@@ -1,54 +1,54 @@
# 精读《Web fonts: when you need them, when you dont》
本期精读让我们来聊一聊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 的优劣具体使用场景。
## 主要观点
- 作者用一张流程图非常言简意赅地概括了文章的上半部分。
![流程图](https://cdn-images-1.medium.com/max/1000/1*MpuDht99XGlRIFlhjFb2yQ.png)
- 当然上半部分作者也讲了很多案例,其中一个很明显的案例就是维基百科利用字体来提升阅读体验,通过文章内的对比,能直观感受到这一点
- 文章后半部分着力介绍了怎么解决Web Font的带来的弊端:认识FOUT带来的问题,如何使用现有的前端解决方案来尽可能避免这个问题,以及样式上优雅降级的几个方案。
- 文章后半部分着力介绍了怎么解决 Web Font 的带来的弊端:认识 FOUT 带来的问题,如何使用现有的前端解决方案来尽可能避免这个问题,以及样式上优雅降级的几个方案。
## 把文章带入自己的开发环境
作为一个中文开发者,在我们的开发技术栈中,Web Fonts绝对是属于使用频率比较低的那一类的。本次精读选择这篇文章,也正是一探这一个不常见的领域。
作为一个中文开发者,在我们的开发技术栈中,Web Fonts 绝对是属于使用频率比较低的那一类的。本次精读选择这篇文章,也正是一探这一个不常见的领域。
对于英文字母,26个字母可以解决大部分的问题,算上大小写和基本符号,一张ASCII码标就可以包含住。让我再扩展一下,到大部分的西文书写系统,几百个字符就能解决多语言显示的问题了。但是对于汉语而言,Web Fonts真的是,想说爱你不容易,因为常用的汉字就有几千个(你想象中国还有《千字文》这种儿童读物……)。字体这东西跟字符数量直接挂钩,是很难通过压缩来获得性能提升的。
对于英文字母,26 个字母可以解决大部分的问题,算上大小写和基本符号,一张 ASCII 码标就可以包含住。让我再扩展一下,到大部分的西文书写系统,几百个字符就能解决多语言显示的问题了。但是对于汉语而言,Web Fonts 真的是,想说爱你不容易,因为常用的汉字就有几千个(你想象中国还有《千字文》这种儿童读物……)。字体这东西跟字符数量直接挂钩,是很难通过压缩来获得性能提升的。
通常的想法就是用多少,取多少,但是这个方法也就只能适用于标题美化等场景。对于一个系统性的前端工程,我们不可能去实现一个动态字符的字体文件(就是统计这个页面上会产生多少个字符,为这个字符集去生成一个字符子集)
虽然汉字书写系统和西文书写系统天生存在差异,但是把作者在文章中提出来的几个问题再站在中文的角度上再来看一下,也可以得到一个比较客观的答案。以下是我作为一个普通开发者的自问自答:
1. **字体对你的品牌很关键吗?**(需要特性字体的中文LOGO基本都用PNGSVG解决了,Web Font不实用。)
2. **字体让你的文字阅读起来更容易了吗?**(我平时开发产品没有成片聚集的文字,用无衬线字体就能满足需求。Web Font很好,我选择“微软雅黑”。)(注:泛指那些好用的支持全字集的系统自带字体;成片的文字适用衬线字体,个人认为中文的衬线字体,不同的字体带来的阅读体验还是有明显差别的。)
3. **你需要在不同设备上显示一样的字体吗?**(好像还没这么苛求吧……微软雅黑好看,安卓上的Roboto也很不错啊,Roboto这种字体还针对移动设备有优化,何乐而不为。)
4. **用了Web Font你会更开心吗?**(在icon中使用iconfont让我们告别了PNG Sprite图,嗯这很开心。至于文字上用Web Font,有好用的系统字体你不用,你这是何苦呢)(作者也说了,可能折腾半天还没系统字体看着舒服,那就是一行font-family的事情)
1. **字体对你的品牌很关键吗?**(需要特性字体的中文 LOGO 基本都用 PNGSVG 解决了,Web Font 不实用。)
2. **字体让你的文字阅读起来更容易了吗?**(我平时开发产品没有成片聚集的文字,用无衬线字体就能满足需求。Web Font 很好,我选择“微软雅黑”。)(注:泛指那些好用的支持全字集的系统自带字体;成片的文字适用衬线字体,个人认为中文的衬线字体,不同的字体带来的阅读体验还是有明显差别的。)
3. **你需要在不同设备上显示一样的字体吗?**(好像还没这么苛求吧……微软雅黑好看,安卓上的 Roboto 也很不错啊,Roboto 这种字体还针对移动设备有优化,何乐而不为。)
4. **用了 Web Font 你会更开心吗?**(在 icon 中使用 iconfont 让我们告别了 PNG Sprite 图,嗯这很开心。至于文字上用 Web Font,有好用的系统字体你不用,你这是何苦呢)(作者也说了,可能折腾半天还没系统字体看着舒服,那就是一行 font-family 的事情)
不同产品有着不同的场景,多像文章里问问自己会有最合适的答案。
## 关于FOUTFOIT
文章中大篇幅地在安利你使用Web Font,但是也很直白地指出了Web Font最大的问题,就是这个FOUT——Flash Of Unstyled Text。连作者毫不避讳地说了句:“噢我的老天,这太丑了!”
## 关于 FOUTFOIT
文章中大篇幅地在安利你使用 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,还有fallbackoptional可以控制先FOITFOUT来达到折中方案。
不过好在,有一个 font-display 的属性,可以在声明@font-face 的时候配合使用。对于未加载 Web Fonts 的时候,auto 属性可以选择隐藏也就是会产生 FOITswap 会产生替换也就是会产生 FOUT,还有 fallbackoptional 可以控制先 FOITFOUT 来达到折中方案。
还有一个思路,那就是预加载,对于字体,浏览器还是能够有效缓存的,如果能够做好预加载,还是不会太影响用户体验的。文章中就提到了一个方案,调用linkrel=preload来做预加载。因为通常加载字体是在CSS中的@font-face被读到的时候才去加载的,那么就会出现先加载CSS,后加载字体的情况。如果利用link预加载,那么在CSS中的@font-face被读到前就已经开始加载了,那么字体加载和CSS加载就可以同时加载,提升速度。
还有一个思路,那就是预加载,对于字体,浏览器还是能够有效缓存的,如果能够做好预加载,还是不会太影响用户体验的。文章中就提到了一个方案,调用 linkrel=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
View File
@@ -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
View File
@@ -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 把这一特性抛弃了. 现在很多流行的框架和库都使用了单向数据流(ReactAngularInfernoRedux等). 单向数据流倡导的是清晰的架构, 数据流动更加清晰和易管理. 对于单向数据流来说说了点View自动更新数据的便利, 但也得到了清晰的数据流.
早在 2009 年, 双向绑定是 Angualr 最受欢迎的特性之一, 但是 Angular 把这一特性抛弃了. 现在很多流行的框架和库都使用了单向数据流(ReactAngularInfernoRedux 等). 单向数据流倡导的是清晰的架构, 数据流动更加清晰和易管理. 对于单向数据流来说说了点 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-timeJIT)编译指的是代码的运行时, 被编译成机器代码的过程. 在JavaScript 运行时, JIT 能够找到代码的特定模式, 而这些模式可以让 JavaScript 更快的被执行.
Just-In-timeJIT)编译指的是代码的运行时, 被编译成机器代码的过程. 在 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 ExtensionsEME
本期精读的文章是: [W3C发布加密媒体扩展(Encrypted Media ExtensionsEME)正式推荐标准](http://www.chinaw3c.org/2017-09-pressrelease-eme-recommendation.html)
本期精读的文章是: [W3C 发布加密媒体扩展(Encrypted Media ExtensionsEME)正式推荐标准](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 的支持情况(点击图片可跳转到对应网址):
[![](./assets/26/eme-support.jpg) ](http://caniuse.com/#search=EME)
[![](./assets/26/eme-support.jpg)](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:模拟 LicenseKey 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 变化;发起证书请求;最后,通过 Licensekey 解密 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 ExtensionsEME)正式推荐标准》 · Issue #37 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/37)
> 讨论地址是:[精读《W3C 发布加密媒体扩展(Encrypted Media ExtensionsEME)正式推荐标准》 · Issue #37 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/37)
> 如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。
+1 -1
View File
@@ -93,7 +93,7 @@ OOCSS 成为 css 的面向对象加强版,每个 class 只处理一件事:
## 3.4 SMACSS
### 为css分类
### 为 css 分类
SMACSS 认为 css 有 5 个类别:
@@ -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 等。所选的框架要保证是被广泛使用并且经过考验的。不同框架对性能有着不同程度的影响,同时对应着不同的优化策略,所以要清楚的了解所选择框架的每个方面。
最好使用那些支持服务器端渲染的框架,如 AngularReactEmber 等。所选的框架要保证是被广泛使用并且经过考验的。不同框架对性能有着不同程度的影响,同时对应着不同的优化策略,所以要清楚的了解所选择框架的每个方面。
### 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,可以参考 [Herokus 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,可以参考 [Herokus 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没法提供的数据。SpeedTrackerLighthouseCalibre都是不错的选择。
在进行快速、无限制的测试时,最好使用一个个人的 WebPageTest 实例。建立一个能自动预警的性能预算监听。建立自己的用户时间标记从而测量并监测具体商用的数据。使用 SpeedCurve 对性能的变化进行监控,同时利用 New Relic 获取 WebPageTest 没法提供的数据。SpeedTrackerLighthouseCalibre 都是不错的选择。
部署私密的 [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),每周都有新的主题,每周五发布。
+11 -11
View File
@@ -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 垃圾回收算法的改进都是基于标记-清除算法的改进.
![](https://raw.githubusercontent.com/dt-fe/weekly/master/assets/29/3.gif)
### 自动 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 总结
@@ -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 @@
本期精读的文章是:[30js代码创建神经网络](https://medium.freecodecamp.org/how-to-create-a-neural-network-in-javascript-in-only-30-lines-of-code-343dafc50d49)。
本期精读的文章是:[30js 代码创建神经网络](https://medium.freecodecamp.org/how-to-create-a-neural-network-in-javascript-in-only-30-lines-of-code-343dafc50d49)。
懒得看文章?没关系,稍后会附上文章内容概述,同时,更希望能通过阅读这一期的精读,穿插着深入阅读原文。
## 1 引言
![Google Dream](https://cdn-images-1.medium.com/max/2000/1*Z6kowWUGajls6aYusTy4oA.jpeg)
自从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](https://img.alicdn.com/tfs/TB1kwPfcOqAXuNjy1XdXXaYcVXa-1398-698.png)
知道了 Sigmoid 函数了,我们可以看一个具体的 **Sigmoid神经元** 例子。
知道了 Sigmoid 函数了,我们可以看一个具体的 **Sigmoid 神经元** 例子。
![Sigmoid example](https://img.alicdn.com/tfs/TB186N_ffDH8KJjy1XcXXcpdXXa-1478-626.png)
@@ -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]));
### 更多讨论
> 讨论地址是:[精读《30js代码创建神经网络》 · Issue #45 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/45)
> 讨论地址是:[精读《30js 代码创建神经网络》 · Issue #45 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/45)
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。**
+1 -1
View File
@@ -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 的版本控制实际就是对文件进行管理和控制,其管理方法
![](https://user-images.githubusercontent.com/3983192/34076544-9e24bb62-e325-11e7-8a82-c71577d9589f.png)
其实是 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 对象可以理解为存储
![](https://user-images.githubusercontent.com/3983192/34078157-79aeaa84-e34f-11e7-93d9-49d807daa3f9.png)
目前每个目录里有一个对象,共5个对象,之前的总体 tree 图只包含了4个对象,执行 git log 查看 commit 记录。
目前每个目录里有一个对象,共 5 个对象,之前的总体 tree 图只包含了 4 个对象,执行 git log 查看 commit 记录。
![](https://user-images.githubusercontent.com/3983192/34078169-ed3a2db6-e34f-11e7-8a8d-f5d902584ed0.png)
@@ -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 的数据,以及最终阶段的数据
# 最后
从这篇文章的可视化案例优化样板,可以总结出以下步骤:
@@ -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"开始挖矿! 如下图
![mining](https://img.alicdn.com/tfs/TB13KIljiqAXuNjy1XdXXaYcVXa-880-686.png)
不错, 数字已经在跳动, 风扇开始工作, 永无尽头的挖矿已经开始了. 那么重要的是, 挖出来的加密货币在哪呢? 原来上面的代码里用的还是我的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.00002Monero(Symbol:XMR). 当前难度为66G一个block, 一个block reward 5.87 XMR, 得到一个XMR11.268G. 264K/11.268G = 0.0000234. 这就是你目前的收益.
- 替换上面代码中的 data-key 部分重新开始挖矿。好了,现在挖出的 Monero (这是啥详见下一节) 已经会到你自己的 coinhive 账户中用下图来说明你名下总计算过的 Hash 个数为 264K当前难度换算为 0.00002Monero(Symbol:XMR)当前难度为 66G 一个 block一个 block reward 5.87 XMR得到一个 XMR11.268G264K/11.268G = 0.0000234这就是你目前的收益
![coinhive dashbaord](https://img.alicdn.com/tfs/TB1LnMljiqAXuNjy1XdXXaYcVXa-2424-1118.png)
- 查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值, 那么民用级CPUGPU, 浏览器或任何沙盒, 虚拟机, 移动设备当然就都可以. 在我们的例子中, 计算过程被做成分布式, 每个用户可以各自计算, 结果按chunk发回master汇总. 这样就实现了终端用户 - 浏览器 - 共同贡献计算资源 - 换为XMR的过程.
既然是通用计算既然是算一个 hash 值,那么民用级 CPUGPU浏览器或任何沙盒虚拟机移动设备当然就都可以在我们的例子中计算过程被做成分布式每个用户可以各自计算结果按 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, 就失去了分布到终端用户的意义. 而CryptoNightGPU上只比同价值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就失去了分布到终端用户的意义。而 CryptoNightGPU 上只比同价值 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 循环体的结构:
![CryptoNight](https://img.alicdn.com/tfs/TB1KrQzjiqAXuNjy1XdXXaYcVXa-682-509.png)
### 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每个对应一个workerworker实际执行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(inputoutput) } while !(meetTarget(output));
websocket.postMessage({nonceoutput}) // hash done successfullysubmit
```
看一下这个过程, 结合[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_headerMerkle root hashand 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 keyrecover seeds. 交易所有多种多样的交易对, 有杠杆, 期货, 空和多, 多种挂单类型等等), 可以看[这里](https://www.blockchain-council.org/cryptocurrency/hot-wallet-vs-cold-wallet/)和[这里](https://github.com/txbits/txbits).
如果对钱包或者交易所感兴趣 (这两个也是很大的话题比如钱包分硬件钱包和软件钱包离线冷钱包和线上热钱包private keyrecover 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-008Specs集合
cns001-008Specs 集合
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
View File
@@ -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 场景,可能并不能为我们的分析带来太多进步。所以,二维的图形信息展示可能依然是增强现实可视化中很重要的一部分。
这篇文章更加引起我兴趣的地方在于它对于可视化基本元素进行了分析——颜色,亮度,纹理,尺寸,方向、形状、位置……这些从我们平时的显示器屏幕或者手机屏幕上所展示的可视化元素会带来完全不一样的表现。增强现实的增强在于我们可以对现实的展示进行改造,纹理是很有趣的一块。一般来说我们平时做可视化分析,常用到阴影,虚线这些纹理,但是现实世界里的纹理就太丰富了,我们可以充分利用现实花纹了,让展现更直观更融合。
+1 -1
View File
@@ -48,7 +48,7 @@ Rekit Studio 以逻辑视图重新组织了项目,文件目录不见了,取
同时支持在线管理本地文件、集成了 [https://microsoft.github.io/monaco-editor/](monaco-editor) 在线编辑文件,以及在线构建、测试等功能。
同时利用和弦图分析了路由与数据流之间绑定关系,路由与文件绑定关系,可以很轻松找到被遗弃的孤立节点。项目维护时,以看图代替看代码,效率至少提升2 3倍。
同时利用和弦图分析了路由与数据流之间绑定关系,路由与文件绑定关系,可以很轻松找到被遗弃的孤立节点。项目维护时,以看图代替看代码,效率至少提升 2 3 倍。
### Cli 与可视化等效
@@ -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的方式了,本周精读让我们来初窥ARKitReact Native ARKit这个库。
ARKit 是苹果推出的增强现实套装,而 react-native-arkit 是基于此的上层封装。对于前端开发而言,这可能是最快上手 ARKit 的方式了,本周精读让我们来初窥 ARKitReact 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 应用了。
![](https://img.alicdn.com/tfs/TB11dGFiFmWBuNjSspdXXbugXXa-540-960.jpg)
上面的图片来自原文,可以看到,在`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),每周都有新的主题,每周末发布。
+2 -2
View File
@@ -1,10 +1,10 @@
# 1 引言
本周精读, 来一起总结web开发的环节, 知识块和技能点. 是不是像xx速成班宣传的一样, 培训三个月, 经验顶三年, 入职BAT, 年薪三十万?
本周精读, 来一起总结 web 开发的环节, 知识块和技能点. 是不是像 xx 速成班宣传的一样, 培训三个月, 经验顶三年, 入职 BAT, 年薪三十万?
本文虽然是罗列知识点, 但我想很有意义. 对于学习的人来说, 提供一个路线图. 对从业者来说, 对全局有更好的把控, 利于看到自己的强项和不足. 对组建团队, 更能起到一个点将谱的作用.
在网上我没有搜到任何深入全面的总结, 提供的那几篇已经算稍微好一些的了. 其他的要么太过笼统(前端-后端-数据-运维, 完毕)要么太细太窄(并不是不好, 只是和本文性质不一样). GeneralistSpecialist之间永远是一对辩证矛盾, 持续思考.
在网上我没有搜到任何深入全面的总结, 提供的那几篇已经算稍微好一些的了. 其他的要么太过笼统(前端-后端-数据-运维, 完毕)要么太细太窄(并不是不好, 只是和本文性质不一样). GeneralistSpecialist 之间永远是一对辩证矛盾, 持续思考.
本文提供了有层级的列表形式, 如果有兴趣的读者可以把它做成概念图形式, 相互关联与距离相关, 可能会有意料之外的效果.
+2 -2
View File
@@ -94,9 +94,9 @@ CJS 的做法很不同,主要是由于相对于通过网络请求从文件系
![](https://hacks.mozilla.org/files/2018/03/25_module_map-500x239.png)
在浏览器中你只要将 `type="module" ` 放在 script 标签上。这会通知浏览器这个文件应该被转化为一个模块。同样,只有模块才能够被导入,浏览器也就知道了模块中有哪些引用。
在浏览器中你只要将 `type="module"` 放在 script 标签上。这会通知浏览器这个文件应该被转化为一个模块。同样,只有模块才能够被导入,浏览器也就知道了模块中有哪些引用。
不过在 Node 中,并没有 HTML 标签,所以也没有地方声明 type 属性。社区内的一种方式就是使用 `.mjs` 扩展。使用这个扩展告诉 Node这个文件是一个模块。
不过在 Node 中,并没有 HTML 标签,所以也没有地方声明 type 属性。社区内的一种方式就是使用 `.mjs` 扩展。使用这个扩展告诉 Node 这个文件是一个模块。
无论哪种方式,加载器将决定是否将文件转化为一个模块。如果是一个模块并且有导入的话,它就会开始处理直到所有的文件被获取和转化。
+1 -1
View File
@@ -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`
@@ -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
```
@@ -86,7 +86,7 @@ SELECT * from bees WHERE bee = 'red';
上面提到的推导符号 `=>` 在实际运行过程中,显然有两种方向左和右:
```
```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
View File
@@ -12,7 +12,7 @@
<img src="assets/68/2.jpg" />
产品用户体验不仅是指交互视觉,我的理解用户体验反用户与产品从认知,使用到传播整个情感的连接和反馈。能力上包括了产品设计与功能实现,用户交互界面,以及系统承载能力。
产品用户体验不仅是指交互视觉,我的理解用户体验反用户与产品从认知,使用到传播整个情感的连接和反馈。能力上包括了产品设计与功能实现,用户交互界面,以及系统承载能力。
上图是 CUBI MobelCUBI 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
+1 -1
View File
@@ -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
});
@@ -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
+2 -2
View File
@@ -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[];
}
```
+2 -2
View File
@@ -149,9 +149,9 @@ Fiber 利用分片的思想,把一个耗时长的任务分成很多小片,
因此,在组件更新时有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。所以 React Fiber 把一个更新过程分为两个阶段:
- 第一个阶段 Reconciliation PhaseFiber 会找出需要更新的 DOM,这个阶段是可以被打断的;
- 第二个阶段 Commit Phase,是无法打断,完成 DOM 的更新并展示;
- 第二个阶段 Commit Phase,是无法打断,完成 DOM 的更新并展示;
在使用 Fiber 后,需要检查与第一阶段相关的生命周期函数,避免逻辑的多次或重复调用:
在使用 Fiber 后,需要检查与第一阶段相关的生命周期函数,避免逻辑的多次或重复调用:
- componentWillMount
- componentWillReceiveProps
+1 -1
View File
@@ -504,7 +504,7 @@ export default {
## 2.13. Field
与 Value 组件唯一的区别,就是
与 Value 组件唯一的区别,就是支持了 `bind`
### 用法
+1 -1
View File
@@ -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 # 后端入口
+1 -1
View File
@@ -292,7 +292,7 @@ ReactDOM.render(
根据笔者的经验,**从上层业务到底层通用组件之间,本地状态数量是递增的:**
```
```plain
业务
-> 全局数据流
-> 页面(完全依赖全局数据流,几乎没有自己的状态)
+14 -9
View File
@@ -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
@@ -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;
+2 -2
View File
@@ -6,7 +6,7 @@
但读完这本书后,笔者发现不同人站在不同视角会有不同的理解:如果你是一名数据行业从业者,你可以理解数据在当今行业发展中如何起到作用;如果你是企业高管,你会领悟到商业平台发展的规则;如果你是一名创业者,你能体会到点线面体的存在,找到自己的定位;如果你是一名管理者,你能领域到管理模式正在发生的变化;如果你是一名传统行业从业者,你能体会到为什么互联网会对传统行业带来这么大的冲击;如果你是一名社会评论家,你会找到衡量智能时代对人类社会带来影响的标尺,等等。商业是推动人类社会发展的源动力,甚至也是文化与战争的源头,智能商业正因为将商业讲的通透,才摆脱了普通商业书籍枯燥的理论体系,从社会实践中总结理论,最终能上升到富有哲理的思考。
智能商业一书中有许多关键词,比如 “三浪叠加” “网络协同” “数据智能” “C2B” “S2B2C” “点线面体” “创造力革命” “网红” “互联网X” 等等,能将这些关键词串起来的,笔者认为是 “商业演化”,在近几十年范围内,商业模式存在一些不变底层逻辑(“三浪叠加” “网络协同” “数据智能”),而在大趋势下存在不断演变的商业模式(“C2B” “S2B2C” “点线面体” “创造力革命” “网红” “互联网X”)。
智能商业一书中有许多关键词,比如 “三浪叠加” “网络协同” “数据智能” “C2B” “S2B2C” “点线面体” “创造力革命” “网红” “互联网 X” 等等,能将这些关键词串起来的,笔者认为是 “商业演化”,在近几十年范围内,商业模式存在一些不变底层逻辑(“三浪叠加” “网络协同” “数据智能”),而在大趋势下存在不断演变的商业模式(“C2B” “S2B2C” “点线面体” “创造力革命” “网红” “互联网 X”)。
读完书后会发现,这么多的关键词,最终都为了实现 “C2B” 这个商业最终演化目标,即便是远在十八世纪的工业革命,也在为 C2B 模式打下让物质资源极大丰富的生产力基础,而网络协同和数据智能,都为了让商业规模更大,精准度更强,可以个性化识别每个用户的需求。新的组织模式也是为了更高效服务用户,整合社会 “点线面体” 的生态关系最终可以形成 “C2B” 的服务网络,而网红、互联网 X 都是 C2B 转型在不同阶段、不同行业的尝试。
@@ -48,7 +48,7 @@
**关于未来**
第六章是对未来的判断,重点在互联网与传统产业如何碰撞,提出的 互联网x 概念背后有着更深刻的含义。如果你今年听说了 “产业物联网” 这个名词,可以甄别一下相应的企业,是仅仅将互联网技术运用到了传统行业,还是将传统行业从底层的运作逻辑就互联网化了呢?互联网不仅是一种技术,更是一种思维,互联网思维可以将被传统行业束缚住的各个流程逐渐还原到最原始、高效的模样。
第六章是对未来的判断,重点在互联网与传统产业如何碰撞,提出的 互联网 x 概念背后有着更深刻的含义。如果你今年听说了 “产业物联网” 这个名词,可以甄别一下相应的企业,是仅仅将互联网技术运用到了传统行业,还是将传统行业从底层的运作逻辑就互联网化了呢?互联网不仅是一种技术,更是一种思维,互联网思维可以将被传统行业束缚住的各个流程逐渐还原到最原始、高效的模样。
比如说传统工程需要提前计算销量固化产能,但加入了互联网快速反馈的网络,就可以实时调整产能,当然这需要整个生产流程的互联网化,将整个环节都做到快速反馈。
+128
View File
@@ -0,0 +1,128 @@
# 1. 引言
前端展望的文章越来越不好写了,随着前端发展的深入,需要拥有非常宽广的视野与格局才能看清前端的未来。
笔者根据自身经验,结合下面几篇文章发表一些总结与感悟:
- [A Look at JavaScripts 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 和 KotlinFlutter 只支持 Dart,与其说这些语言更适合这些平台特性,不如说背后是谷歌、苹果、微软等巨头对平台生态掌控权的争夺。Web 与移动端要解决的问题是类似的:如何高效管理 UI 状态,现在大部分都采用数据驱动的思路,通过 JSX 或 Template 的方式描述出 UI DSL(更多可参考 [前端开发编程语言的过去、现在和未来](https://johnhax.net/2019/fe-lang/article1) UI DSL 一节)、以及性能提升:渲染和计算分离(这里又分为并发与调度两种实现思路,目的和效果是类似的)。
所以编程语言的未来也没什么悬念,前端领域如果有的选就用 JS,没得选只能依附所在平台绑定的语言,而前端语言最近正在完成一轮升级大迁徙:JS -> TSJAVA -> KotlinOC -> 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
View File
@@ -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)
+96
View File
@@ -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)
+193
View File
@@ -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)
+261
View File
@@ -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 拖拽到 ColumnsSales 拖拽到 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 DateYear)拖拽回 Rows。
4. 为了展示颜色与文字,将 Profit 拖拽到 ColorSales 拖拽到 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)
+93
View File
@@ -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)
+541
View File
@@ -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 设置为离散类型。
> TipsTables 对维度与度量分别分配了蓝色、绿色,当我们将绿色度量字段设置为离散类型时,这个度量字段会变成蓝色,也就是当作了维度字段进行处理。
最后,标记区域不仅能拖拽字段,还可以单击后修改详细配置,比如修改颜色详细配置:
<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)。
![](https://img.alicdn.com/tfs/TB1VQUveRv0gK0jSZKbXXbK2FXa-260-388.png)
网页颜色的对比度值在 1:1 到 21:1 之间,文本和图像文本的的对比度最小值为 4.5:1,也就是说低于这个值得对比度都不符合标准。 我们看一下列举的几种颜色对比度,对比度越高,也越有利于阅读。对比度越低,对于一些存在视力障碍或色觉缺陷的用户,可能就无法阅读。
![](https://img.alicdn.com/tfs/TB1G1MveUz1gK0jSZLeXXb9kVXa-1000-410.png)
### 演讲中的颜色解决方案
演讲在最开始首先讲了挪威的一个法律,不符合 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
```
![](https://img.alicdn.com/tfs/TB1zfcveUz1gK0jSZLeXXb9kVXa-1535-584.png)
可读性的问题解决了,但是紧接着又遇到了一个问题,如果用户选取的颜色很浅呢,与背景颜色的对比度小于 4.5,该怎么处理呢。
![](https://img.alicdn.com/tfs/TB14RsveQP2gK0jSZPxXXacQpXa-1254-402.png)
- 寻找对比度更强的颜色,增强可读性
演讲中给出的解决方法是不断的加深当前用户选择的颜色,循环获取到对比度最高的同色系颜色。代码如下:
![](https://img.alicdn.com/tfs/TB19J7veUH1gK0jSZSyXXXtlpXa-1457-663.png)
获取了一个更深的颜色后,通过给按钮加一个外边框的方式,优化整体的可读性。
![](https://img.alicdn.com/tfs/TB1aRQzeUY1gK0jSZFCXXcwqXXa-1802-571.png)
文章最后还介绍了,通过给定一个主题色,获取第二第三主题色的方式,通过将颜色放到 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)
+79
View File
@@ -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 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
- 深水区需要哪些技能
![image.png](https://img.alicdn.com/tfs/TB1oovQe8r0gK0jSZFnXXbRRXXa-1832-1032.png)
深水区需要是四个核心能力,分别是:技术、产品、业务和管理能力。
- 面对深水压力不需紧张
其实何止前端开发,整个技术行业都已步入深水区,只是前端工程师的感知来的晚一些而已。只要把眼光投向深水区,问题就会一个接一个的浮上来,当越来越多问题浮起来的时候,就是你慢慢沉向深水区的时候,这时候不需要太过紧张。
## 精读
深水区的理解首先需要达成一致,并不只是一个维度的加深,而是全方位多方面的困难同时加击,压强升高、光线减少、温度剧变等等。
对应到文中总结的解法就是需要『技术创新、流程优化、团队合作、影响大盘、驱动业务、商业决策和团队管理』。但你展开想一下,把这个角色换成后端、无线端、甚至是 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)
+290
View File
@@ -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)
+175
View File
@@ -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` 用来发送改变状态的指令。
至于为什么要用有限状态机管理工具,官方文档举了个例子 - 点击编辑后进入编辑态,点击保存后返回原始状态的例子:
![](https://img.alicdn.com/tfs/TB16AvLhAL0gK0jSZFAXXcA9pXa-998-96.png)
点击 Edit 按钮后,将进入下图的状态,点击 Save 后如果输入的内容校验通过保存后再回到初始状态:
![](https://img.alicdn.com/tfs/TB1LeYLhpP7gK0jSZFjXXc5aXXa-1013-97.png)
如果不用有限状态机,我们首先会创建两个变量存储是否处于编辑态,以及当前输入文本是什么:
```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 函数测试,因为这个函数结构与柯里化函数类似:
![](https://img.alicdn.com/tfs/TB1H4HvioT1gK0jSZFrXXcNCXXa-1180-442.png)
可以看到,babel 通过 `generator` `async` 属性来标识函数是否为 generator 或者 async 函数。同理,增加一个 `curry` 属性就可以实现第一步了:
![](https://img.alicdn.com/tfs/TB1c8jviXP7gK0jSZFjXXc5aXXa-1180-464.png)
要实现如上效果,只需在词法分析 `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,所以定制型不足。
![](https://img.alicdn.com/tfs/TB1X8Wvi4D1gK0jSZFyXXciOVXa-608-324.png)
举个例子,上图的结构用 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 嵌套结构,我们将图形纵向分成两大块,然后在每块内部继续嵌套划分布局,这是最经典的布局行为了。
![](https://img.alicdn.com/tfs/TB17_Oqi2b2gK0jSZK9XXaEgFXa-608-324.jpg)
样式文件里,我们需要对每层布局进行描述,同时支持多分辨率弹性布局,包括顶层 `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 方式进行调整:
![](https://img.alicdn.com/tfs/TB1cAmui2b2gK0jSZK9XXaEgFXa-640-400.jpg)
UI 是对文本的再抽象,同时可以规避一些不可能存在的语法,比如:
```scss
.card {
grid-template-areas:
"image name"
"image position"
"social image";
}
```
布局只能以凸多边形方式拓展,不可能分离,也不可能突然插入一个其他模块而变成凹多边形。因此 UI 可以将这个错误规避,并简化为横竖多条线的方式对 UI 进行划分,显然这种描述方式效率更高。
不得不说,Grid 以及图形化插件的探索,是布局领域的一大进步,是不断抽象的尝试,要解决的问题只有一个:如何提供一种更直观的描述 UI 的方式。
### 布局对模块化的影响
Grid 将布局方式提高了一个维度,会直接影响到 JS 模块化方式。
尤其是以 JSX 组织代码的情况下,一个模块等于 UI + JS,通过嵌套方式的布局会让我们更倾向于站在 UI 视角划分模块。
![](https://img.alicdn.com/tfs/TB1WQCvi.Y1gK0jSZFMXXaWcVXa-1052-750.png)
比如对于上图模块,如果用 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` 直观,可是配合一些可视化系统就非常直观了:
![](https://img.alicdn.com/tfs/TB1E.9AiYj1gK0jSZFuXXcrHpXa-2006-1470.png)
将 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)
+309
View File
@@ -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 选择了 VueNextjs 选择了 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)
+702
View File
@@ -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 概述 & 精读
本期大会思想、设计上的内容较多,具体实现层内容较少,因为行业领导者需要引领规范,而真正技术价值在于思维模型与算法,理解了解题思路,实现它其实并不难。
### 开发者体验与用户体验
- 开发者体验:DXdevelop experience
- 用户体验:UXuser 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。
### 提升加载速度
普通网页的加载流程是这样的:
![](https://img.alicdn.com/tfs/TB1gqmXlAY2gK0jSZFgXXc5OFXa-2102-1094.png)
先加载代码,然后会渲染页面,在渲染的同时发取数请求,等取数完成后才能渲染出真实数据。
那么如何改善这个情况呢?首先是预取数,提前解析出请求并在脚本加载的同时取数,可以节省大量时间:
![](https://img.alicdn.com/tfs/TB1r8Sblrj1gK0jSZFuXXcrHpXa-1704-890.png)
那么下载的代码可以再拆分吗?注意到并不是所有代码都作用于 UI 渲染,我们可以将模块分为 `ImportForDisplay``importForAfterDisplay`
![](https://img.alicdn.com/tfs/TB1sGx.lxz1gK0jSZSgXXavwpXa-2662-1352.png)
这样就可以优先加载与 UI 相关的代码,其余逻辑代码在页面展示出之后再加载:
![](https://img.alicdn.com/tfs/TB1_9N.lCf2gK0jSZFPXXXsopXa-2762-1206.png)
这样可以实现源码分段加载,并分段渲染:
![](https://img.alicdn.com/tfs/TB1YJN.lED1gK0jSZFGXXbd3FXa-2620-1308.png)
对取数来说也是如此,并不是所有取数都是初始化渲染阶段必须用上的。可以通过 `relay` 的特性 `@defer` 标记出可以延迟加载的数据:
```relay
fragment ProfileData on User {
classNameprofile_picture { ... }
...AdditionalData @defer
}
```
这下取数也可以分段了,首屏的数据会优先加载:
![](https://img.alicdn.com/tfs/TB1SIydluH2gK0jSZJnXXaT1FXa-2638-1330.png)
利用 `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
}
}
```
这样首屏数据中也只会按需加载用到的部分,请求时间可以再次缩短:
![](https://img.alicdn.com/tfs/TB1klKcly_1gK0jSZFqXXcpaXXa-2632-1426.png)
可以看到,与 relay 结合可以进一步优化加载性能。
### 加载体验
可以 `React.Suspense``React.lazy` 动态加载组件。通过 `fallback` 指定元素的占位图可以提升加载体验:
```tsx
<React.Suspense fallback={<MyPlaceholder />}>
<Post>
<Header />
<Body />
<Reactions />
<Comments />
</Post>
</React.Suspense>
```
`Suspense` 可以被嵌套,资源会按嵌套顺序加载,保证一个自然的视觉连贯性。
### 智能文档
通过解析 Markdown 自动生成文档大家已经很熟悉了,也有很多现成的工具可以用,但这次分享的文档系统有意思之处在于,可以动态修改源码并实时生效。
![](https://img.alicdn.com/tfs/TB1p1jKluT2gK0jSZFvXXXnFXXa-1692-1430.png)
不仅如此,还利用了 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,458 @@
## 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` 要一起更新,为了仅触发一次更新,使用了 `unstable_batchedUpdates` 将更新合并为一次:
```tsx
unstable_batchedUpdates(() => {
setIsValidating(false);
// ...
setData(newData);
});
```
其实还有别的解法,比如使用 `useReducer` 管理数据也能达到相同性能效果。
### 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` + `onErrorRetry` 机制实现依赖取数的。**
看下面这段代码:
```tsx
const { data: user } = useSWR("/api/user");
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
```
怎么做到智能按依赖顺序请求呢?我们看 `useSWR` 取数函数的主体逻辑:
```tsx
try {
// 设置 isValidation 为 true
// 取数、onSuccess 回调
// 设置 isValidation 为 false
// 设置缓存
// unstable_batchedUpdates
} catch (err) {
// 撤销取数、缓存等对象
// 调用 onErrorRetry
}
```
可见取数逻辑被 `try` 住了,那么 `user.id``useSWR("/api/user")` 没有 Ready 的情况一定会抛出异常,则自动进入 `onErrorRetry` 逻辑,看看下次取数时 `user.id` 有没有 Ready。
那么什么时候才轮到下次取数呢?这个时机是:
```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)
+630
View File
@@ -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://img.alicdn.com/tfs/TB1HsRRmubviK0jSZFNXXaApXXa-2042-592.png)
这就像路牌一样,可以更高效的看出代码结构,也包括了数据流结构,由于篇幅限制,感兴趣的同学可以看 [原视频](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 的位置区间在 0100
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-Alpha4 月)
<img width=300 src="https://img.alicdn.com/tfs/TB1TMJnnXY7gK0jSZKzXXaikpXa-1306-858.png">
Alpha5 月)
<img width=300 src="https://img.alicdn.com/tfs/TB1608pnoD1gK0jSZFGXXbd3FXa-1794-1186.png">
Beta1.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)
+28
View File
@@ -0,0 +1,28 @@
{
"name": "weekly",
"version": "1.0.0",
"description": "前端界的好文精读,每周更新!",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dt-fe/weekly.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/dt-fe/weekly/issues"
},
"homepage": "https://github.com/dt-fe/weekly#readme",
"dependencies": {
"husky": "^3.0.4",
"lint-md-cli": "^0.1.1"
},
"husky": {
"hooks": {
"pre-commit": "npx lint-md ./"
}
}
}
+4
View File
@@ -1,5 +1,9 @@
# 前端精读
<a href="https://travis-ci.org/dt-fe/weekly">
<img src="https://travis-ci.org/dt-fe/weekly.svg?branch=v2" alt="CircleCI Status">
</a>
前端界的好文精读,每周更新!
- [周刊参考池](https://github.com/dt-fe/weekly/issues/2)