Compare commits

..
34 Commits
Author SHA1 Message Date
ascoders ea56ac10ce Merge branch 'master' of https://github.com/ascoders/weekly 2021-06-07 09:33:53 +08:00
ascoders 90660c6d34 198 2021-06-07 09:33:47 +08:00
黄子毅 fcf04083a5 Merge pull request #322 from rmlzy/patch-3
Update 41.精读《Ant Design 3.0 背后的故事》.md
2021-05-31 16:05:52 +08:00
黄子毅 a1ea12a736 Merge pull request #323 from rmlzy/patch-4
Update 52.精读《图解 ES 模块》.md
2021-05-31 16:05:25 +08:00
黄子毅 02a795bb8a Merge pull request #324 from rmlzy/patch-5
Update 137.精读《当我在分享的时候,我在做什么?》.md
2021-05-31 16:05:12 +08:00
黄子毅 6bffed8eb5 Merge pull request #325 from rmlzy/patch-6
Update 136.精读《极客公园 IFX - 下》.md
2021-05-31 16:04:47 +08:00
黄子毅 ddd5ceb74a Merge pull request #326 from rmlzy/patch-7
Update 135.精读《极客公园 IFX - 上》.md
2021-05-31 16:04:34 +08:00
rmlzy 3c6cee2f49 Update 135.精读《极客公园 IFX - 上》.md
修正错别字
2021-05-31 16:00:59 +08:00
rmlzy fc2f0c6f68 Update 136.精读《极客公园 IFX - 下》.md
修正错别字
2021-05-31 15:50:42 +08:00
rmlzy 0dcf208315 Update 137.精读《当我在分享的时候,我在做什么?》.md
修正错别字
2021-05-31 15:47:03 +08:00
rmlzy 7730834e83 Update 52.精读《图解 ES 模块》.md
修正错别字
2021-05-31 15:40:39 +08:00
rmlzy 91875ab232 Update 41.精读《Ant Design 3.0 背后的故事》.md
修正格式错误
2021-05-31 15:32:52 +08:00
黄子毅 d81e6d7813 Merge pull request #320 from rmlzy/patch-1
Update 20.精读《Nestjs》文档.md
2021-05-31 10:02:25 +08:00
黄子毅 3e299d85e9 Merge pull request #321 from rmlzy/patch-2
Update 119.精读《前端深水区》.md
2021-05-31 10:02:04 +08:00
ascoders 455e1693f4 197 2021-05-31 10:01:23 +08:00
rmlzy 2a17e62107 Update 119.精读《前端深水区》.md
修正标点符号
2021-05-31 09:54:38 +08:00
rmlzy 0a090cf0a7 Update 20.精读《Nestjs》文档.md
Nextjs 目前是 Providers 配合 `@Injectable()` 装饰器实现 DI
2021-05-31 09:50:16 +08:00
ascoders c13f0af435 Merge branch 'master' of https://github.com/ascoders/weekly 2021-05-24 13:50:42 +08:00
ascoders 8ee7995ddc fix typo error 2021-05-24 13:50:31 +08:00
黄子毅 0e8c749e68 Merge pull request #318 from neighborhood999/fix/typo
fix(196): typo
2021-05-24 10:49:54 +08:00
Jie Peng ee0450ffb2 fix(196): typo
Signed-off-by: Jie Peng <im@jiepeng.me>
2021-05-24 10:32:12 +08:00
ascoders e0d8be6916 196 2021-05-24 10:12:55 +08:00
ascoders 7c4d37a428 fix typo 2021-05-17 13:21:06 +08:00
ascoders eeaebad903 195 2021-05-17 09:43:00 +08:00
黄子毅 c624e7e607 Merge pull request #315 from kamilic/patch-1
Update 194.精读《算法基础数据结构》.md
2021-05-12 13:53:55 +08:00
kamilic 0f724a20cf Update 194.精读《算法基础数据结构》.md
chores: fix typo
2021-05-11 21:08:13 +08:00
黄子毅 c7922c4a0e Merge pull request #314 from jihchi/patch-1
Update 194.精读《算法基础数据结构》.md -- 連結正確但文字錯誤
2021-05-10 14:23:45 +08:00
Jihchi Lee 9d98f72fd3 Update 194.精读《算法基础数据结构》.md 2021-05-10 11:25:53 +08:00
ascoders db9a256efa update readme 2021-05-10 08:59:01 +08:00
ascoders aedfdd4b91 Merge branch 'master' of https://github.com/dt-fe/weekly 2021-05-10 08:57:02 +08:00
ascoders 99d5c51d42 194 2021-05-10 08:56:52 +08:00
黄子毅 086b733288 Merge pull request #313 from jihchi/patch-1
96.精读《useEffect%20完全指南》-- 修正錯誤依賴
2021-05-08 10:46:03 +08:00
Jihchi Lee 6f9ccaef9e Update 96.精读《useEffect 完全指南》.md 2021-05-07 12:01:13 +08:00
ascoders dba92f52b5 update 2021-04-25 10:10:15 +08:00
18 changed files with 1264 additions and 25 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
const fs = require('fs')
const dirs = ['前沿技术', '设计模式', '编译原理', '源码解读', '商业思考']
const dirs = ['前沿技术', '设计模式', '编译原理', '源码解读', '商业思考', '算法']
dirs.forEach(dir => {
const readDir = fs.readdirSync(`./${dir}`);
+10 -2
View File
@@ -6,7 +6,7 @@
前端界的好文精读,每周更新!
最新精读:<a href="./前沿技术/193.精读《React Server Component》.md">193.精读《React Server Component》</a>
最新精读:<a href="./算法/198.精读《算法 - 动态规划》.md">198.精读《算法 - 动态规划》</a>
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
@@ -156,6 +156,10 @@
- <a href="./前沿技术/191.精读《高性能表格》.md">191.精读《高性能表格》</a>
- <a href="./前沿技术/192.精读《DOM diff 最长上升子序列》.md">192.精读《DOM diff 最长上升子序列》</a>
- <a href="./前沿技术/193.精读《React Server Component》.md">193.精读《React Server Component》</a>
- <a href="./前沿技术/194.精读《算法基础数据结构》.md">194.精读《算法基础数据结构》</a>
- <a href="./前沿技术/195.精读《新一代前端构建工具对比》.md">195.精读《新一代前端构建工具对比》</a>
- <a href="./前沿技术/196.精读《前端职业规划 - 2021 年》.md">196.精读《前端职业规划 - 2021 年》</a>
- <a href="./前沿技术/197.精读《低代码逻辑编排》.md">197.精读《低代码逻辑编排》</a>
### 设计模式
@@ -206,7 +210,7 @@
- <a href="./源码解读/110.精读《Inject Instance 源码》.md">110.精读《Inject Instance 源码》</a>
- <a href="./源码解读/122.精读《robot 源码 - 有限状态机》.md">122.精读《robot 源码 - 有限状态机》</a>
- <a href="./源码解读/128.精读《Hooks 取数 - swr 源码》.md">128.精读《Hooks 取数 - swr 源码》</a>
- <a href="./源码解读/130.精读《unstated 与 unstated-next 源码》 copy.md">130.精读《unstated 与 unstated-next 源码》 copy</a>
- <a href="./源码解读/130.精读《unstated 与 unstated-next 源码》.md">130.精读《unstated 与 unstated-next 源码》</a>
- <a href="./源码解读/151. 精读《@umijs use-request》源码.md">151. 精读《@umijs use-request》源码</a>
- <a href="./源码解读/155. 精读《use-what-changed 源码》.md">155. 精读《use-what-changed 源码》</a>
- <a href="./源码解读/156. 精读《react-intersection-observer 源码》.md">156. 精读《react-intersection-observer 源码》</a>
@@ -225,6 +229,10 @@
- <a href="./商业思考/136.精读《极客公园 IFX - 下》.md">136.精读《极客公园 IFX - 下》</a>
- <a href="./商业思考/137.精读《当我在分享的时候,我在做什么?》.md">137.精读《当我在分享的时候,我在做什么?》</a>
### 算法
- <a href="./算法/198.精读《算法 - 动态规划》.md">198.精读《算法 - 动态规划》</a>
## 关注前端精读微信公众号
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
@@ -10,7 +10,7 @@
## 概述
原文对于深水区的想法,讲的很清楚,还是建议读者去读一下原文。
对比 2010 年,整个前端生态已经翻新了好几遍,直到近几年的 Node BFF、IDE Cloud,抑或是客户端 AI,还是 Serverless 的建设,前端想要深度参与的话,单纯依靠原来的 HTML/CSS/JS 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
对比 2010 年,整个前端生态已经翻新了好几遍,直到近几年的 Node BFF、IDE Cloud,抑或是客户端 AI,还是 Serverless 的建设,前端想要深度参与的话,单纯依靠原来的 HTML/CSS/JS 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
- 深水区需要哪些技能
![image.png](https://img.alicdn.com/tfs/TB1oovQe8r0gK0jSZFnXXbRRXXa-1832-1032.png)
@@ -288,7 +288,7 @@ React Server Component 在折腾了这么久后,可以发现,最大的区别
所以,本质上还是 HTML 太简单了,无法适应如今前端的复杂度,而普通后端框架虽然后端能力强大,但在前端能力上还停留在 20 年前(直接返回 DOM),唯有 Node 中间层方案作为桥梁,才能较好的衔接现代后端代码与现代前端代码。
### PHP 前后端模块化
### PHP VS Server Component
其实在 PHP 时代,前后端都可以做模块化。后端模块化显而易见,因为可以将后端代码模块化的开发,最后打包至服务器运行。前端也可以在服务端模块化开发,只要我们将前后端代码剥离出来即可,下图青色是后端部分,红色是前端部分:
@@ -0,0 +1,150 @@
掌握了不同数据结构的特点,可以让你在面对不同问题时,采用合适的数据结构处理,达到事半功倍的效果。
所以这次我们详细介绍各类数据结构的特点,希望你可以融会贯通。
## 精读
### 数组
<img width=200 src="https://img.alicdn.com/imgextra/i2/O1CN01noho9m1Vltg5ISaq2_!!6000000002694-2-tps-418-110.png">
数组非常常用,它是一块连续的内存空间,因此可以根据下标直接访问,其查找效率为 O(1)。
但数组的插入、删除效率较低,只有 O(n),原因是为了保持数组的连续性,必须在插入或删除后对数组进行一些操作:比如插入第 K 个元素,需要将后面元素后移;而删除第 K 个元素,需要将后面元素前移。
### 链表
<img width=280 src="https://img.alicdn.com/imgextra/i3/O1CN010JfUOo1b0A5muE4sE_!!6000000003402-2-tps-584-112.png">
链表是为了解决数组问题而发明出来的,它提升了插入、删除效率,而牺牲了查找效率。
链表的插入、删除效率是 O(1),因为只要将对应位置元素断链、重连就可以完成插入、删除,而无需关心其他节点。
相应的查找效率就低了,因为存储空间不是连续的,所以无法像数组一样通过下标直接查找,而需要通过指针不断搜索,所以查找效率为 O(n)。
顺带一提,链表可以通过增加 `.prev` 属性改造为双向链表,也可以通过定义两个 `.next` 形成二叉树(`.left` `.right`)或者多叉树(N 个 `.next`)。
<img width=160 src="https://img.alicdn.com/imgextra/i2/O1CN01IqNVQI1m5ABrZXV4i_!!6000000004902-2-tps-318-316.png">
### 栈
<img width=240 src="https://img.alicdn.com/imgextra/i3/O1CN01xSS8e21xbe3LP1khU_!!6000000006462-2-tps-466-122.png">
栈是一种先入后出的结构,可以用数组模拟。
```typescript
const stack: number[] = []
// 入栈
stack.push(1)
// 出栈
stack.pop()
```
### 堆
<img width=500 src="https://img.alicdn.com/imgextra/i2/O1CN019O42yy1qE6NlA8w6V_!!6000000005463-2-tps-1154-952.png">
堆是一种特殊的完全二叉树,分为大顶堆与小顶堆。
大顶堆指二叉树根节点是最大的数,小顶堆指二叉树根节点是最小的数。为了方便说明,以下以大顶堆举例,小顶堆的逻辑与之相反即可。
大顶堆中,任意节点都比其叶子结点大,所以根节点是最大的节点。这种数据结构的优势是可以以 O(1) 效率找到最大值(小顶堆找最小值),因为直接取 `stack[0]` 就是根节点。
这里稍微提一下二叉树与数组结构的映射,因为采用数组方式操作二叉数,无论操作还是空间都有优势:第一项存储的是节点总数,对于下标为 K 的节点,其父节点下标是 `floor(K / 2)`,其子节点下标分别是 `K * 2``K * 2 + 1`,所以可以快速定位父子位置。
而利用这个特性,可以将插入、删除的效率达到 `O(logn)`,因为可以通过上下移动的方式调整其他节点顺序,而对于一个拥有 n 个节点的完全二叉树,树的深度为 `logn`
### 哈希表
<img width=300 src="https://img.alicdn.com/imgextra/i4/O1CN01u3J1JF1Sl25HB6Q0I_!!6000000002286-2-tps-740-598.png">
哈希表就是所谓的 Map,不同 Map 实现方式不同,常见的有 HashMap、TreeMap、HashSet、TreeSet。
其中 Map 和 Set 实现类似,所以以 Map 为例讲解。
首先将要存储的字符求出其 ASCII 码值,再根据比如余数等方法,定位到一个数组的下标,同一个下标可能对应多个值,因此这个下标可能对应一个链表,根据链表进一步查找,这种方法称为拉链法。
如果存储的值超过一定数量,链表的查询效率就会降低,可能会升级为红黑树存储,总之这样的增、删、查效率为 `O(1)`,但缺点是其内容是无序的。
为了保证内容有序,可以使用树状结构存储,这种数据结构称为 HashTree,这样时间复杂度退化为 `O(logn)`,但好处是内容可以是有序的。
### 树 & 二叉搜索树
<img width=380 src="https://img.alicdn.com/imgextra/i4/O1CN01vOCoG91w82pzSITaQ_!!6000000006262-2-tps-800-504.png">
二叉搜索树是一种特殊二叉树,更复杂的还有红黑树,但这里就不深入了,只介绍二叉搜索树。
二叉搜索树满足对于任意节点,`left 的所有节点 < 根节点 < right 的所有节点`,注意这里是所有节点,因此在判断时需要递归考虑所有情况。
二叉搜索树的好处在于,访问、查找、插入、删除的时间复杂度均为 O(logn),因为无论何种操作都可以通过二分方式进行。但在最坏的情况会降级为 O(n),原因是多次操作后,二叉搜索树可能不再平衡,最后退化为一个链表,就变成了链表的时间复杂度。
更好的方案有 AVL 树、红黑树等,像 JAVA、C++ 标准库实现的二叉搜索树都是红黑树。
### 字典树
<img width=400 src="https://img.alicdn.com/imgextra/i2/O1CN01TqDeaL1ll0lTX75y3_!!6000000004858-2-tps-872-510.png">
字典树多用于单词搜索场景,只要给定一个单独开头,就可以快速查找到后面有几种推荐词。
比如上面的例子,输入 "o",就可以快速查找到后面有 "ok" 与 "ol" 两个单词。要注意的是,每个节点都要有一个属性 `isEndOfWord` 表示到当前为止是否为一个完整的单词:比如 `go``good` 两个都是完整的单词,但 `goo` 不是,因此第二个 `o` 与第四个 `d` 都有 `isEndOfWord` 标记,表示读到这里就查到一个完整的单词了,叶子结点的标记也可以省略。
### 并查集
<img width=300 src="https://img.alicdn.com/imgextra/i4/O1CN01B5xA5r21rSBj442z3_!!6000000007038-2-tps-622-172.png">
并查集用来解决团伙问题,或者岛屿问题,即判断多个元素之间是属于某个集合。并查集的英文是 Union and Find,即归并与查找,因此并查集数据结构可以写成一个类,提供两个最基础的方法 `union``find`
其中 `union` 可以将任意两个元素放在一个集合,而 `find` 可以查找任意元素属于哪个根集合。
并查集使用数组的数据结构,只是有以下特殊含义,设下标为 k:
- `nums[k]` 表示其所属的集合,如果 `nums[k] === k` 表示它是这个集合的根节点。
如果要数一共有几个集合,只要数有多少满足 `nums[k] === k` 条件的数目即可,就像数有几个团伙,只要数有几个老大即可。
并查集的实现不同,数据也会有微妙的不同,高效的并查集在插入时,会递归将元素的值尽量指向根老大,这样查找判断时计算的快一些,但即便指向的不是根老大,也可以通过递归的方式找到根老大。
### 布隆过滤器
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01CWabYX26RPkR0T3zs_!!6000000007658-2-tps-650-334.png">
Bloom Filter 只是一个过滤器,可以用远远超过其他算法的速度把未命中的数据排除掉,但未排除的也可能实际不存在,所以需要进一步查询。
布隆过滤器是如何做到这一点的呢?就是通过二进制判断。
如上图所示,我们先存储了 a、b 两个数据,将其转化为二进制,将对应位置改为 1,那么当我们再查询 a 或 b 时,因为映射关系相同,所以查到的结果肯定存在。
但查询 c 时,发现有一项是 0,说明 c 一定不存在;但查询 d 时,恰好两个都查到是 1,但实际 d 是不存在的,这就是其产生误差的原因。
布隆过滤器在比特币与分布式系统中使用广泛,比如比特币查询交易是否在某个节点上,就先利用布隆过滤器挡一下,以快速跳过不必要的搜索,而分布式系统计算比如 Map Reduce,也通过布隆过滤器快速过滤掉不在某个节点的计算。
## 总结
最后给出各数据结构 “访问、查询、插入、删除” 的平均、最差时间复杂度图:
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01LV4sSl20vkHWdZ7nr_!!6000000006912-2-tps-2398-1272.png">
这个图来自 [bigocheatsheet](https://www.bigocheatsheet.com/#graphs),你也可以点开链接直接访问。
学习了这些基础数据结构之后,希望你可以融会贯通,善于组合这些数据结构解决实际的问题,同时还要意识到没有任何一个数据结构是万能的,否则就不会有这么多数据结构需要学习了,只用一个万能的数据结构就行了。
对于数据结构的组合,我举两个例子:
第一个例子是如何以 O(1) 平均时间复杂度查询一个栈的最大或最小值。此时一个栈是不够的,需要另一个栈 B 辅助,遇到更大或更小值的时候才入栈 B,这样栈 B 的第一个数就是当前栈内最大或最小的值,查询效率是 O(1),而且只有在出栈时才需要更新,所以平均时间复杂度整体是 O(1)。
第二个例子是如何提升链表查找效率,可以通过哈希表与链表结合的思路,通过空间换时间的方式,用哈希表快速定位任意值在链表中的位置,就可以通过空间翻倍的牺牲换来插入、删除、查询时间复杂度均为 O(1)。虽然哈希表就能达到这个时间复杂度,但哈希表是无序的;虽然 HashTree 是有序的,但时间复杂度是 O(logn),所以只有通过组合 HashMap 与链表才能达到有序且时间复杂度更优,但牺牲了空间复杂度。
包括最后说的布隆过滤器也不是单独使用的,它只是一个防火墙,用极高的效率阻挡一些非法数据,但没有阻挡住的不一定就是合法的,需要进一步查询。
所以希望你能了解到各个数据结构的特征、局限以及组合的用法,相信你可以在实际场景中灵活使用不同的数据结构,以实现当前业务场景的最优解。
> 讨论地址是:[精读《算法基础数据结构》· Issue #312 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/312)
**如果你想参与讨论,请 [点击这里](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,121 @@
本周精读的文章是 [Comparing the New Generation of Build Tools](https://css-tricks.com/comparing-the-new-generation-of-build-tools/)。
前端工程领域近期出了不少新工具,这些新工具都运用了一些新技术或者跨领域技术,实现了一些突破,因此有必要了解一下这些工具都有什么特性,以及是否可以投入生产环境。
由于原文比较啰嗦,所以具体用法和支持细节不在这里展开,如果想进一步了解细节,可以直接阅读 [原文]((https://css-tricks.com/comparing-the-new-generation-of-build-tools/))。
## 精读
按照从底层到上层的封装粒度,以 esbuild、snowpack、vite、wmr 的顺序介绍。
### esbuild
esbuild 使用 go 语言编写,由于相对 node 更为底层,且不提供 AST 操作能力,所以代码执行效率更高,根据其官方 benchmark 介绍提速有 10100 倍:
<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01hzHuDP1JXuBvRgX7x_!!6000000001039-2-tps-800-170.png">
esbuild 有两大功能,分别是 bundler 与 minifier,其中 bundler 用于代码编译,类似 babel-loader、ts-loaderminifier 用于代码压缩,类似 terser。
使用 esbuild 编译代码方法如下:
```typescript
esbuild.build({
entryPoints: ["src/app.jsx"],
outdir: "dist",
define: { "process.env.NODE_ENV": '"production"' },
watch: true,
});
```
但由于 esbuild 无法操作 AST,所以一些需要操作 AST 的 babel 插件无法与之兼容,导致生产环境很少直接使用 esbuild 的 bundler 模块。
幸运的是 minifier 模块可以直接替换 terser 使用,可以用于生产环境:
```typescript
esbuild.transform(code, {
minify: true,
});
```
由于 esbuild 牺牲了一些包大小换取了更高的执行效率,因此压缩后包体积会稍微大一些,不过也就是 177KB 与 165KB 的区别,几乎可以忽略。
esbuild 比较底层,所以可以与后续介绍的上层构建工具结合使用,当然根据工具设计理念,是否内置,内置到什么程度,以及是否允许通过插件替换就是另一回事了。
### snowpack
snowpack 是一个相对轻量的 bundless 方案,之前也写过一篇 [精读 snowpack](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/153.%20%E7%B2%BE%E8%AF%BB%E3%80%8Asnowpack%E3%80%8B.md),其实 bundless 就是利用浏览器支持的 [ESM import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) 特性,利用浏览器进行模块间依赖加载,而不需要在编译时进行。
跳过编译时依赖加载可以省很多事,比如不用考虑 tree shaking 问题,也不用为了最终产物加速而使用缓存,相当于这些工作交给最终执行的浏览器了,而浏览器作为最终运行时容器,比编译时工具更了解应该如何按需加载。
仅从编译时来看,修改单个文件的编译速度与项目整体大小有关,而若不考虑整体项目,仅编译单个文件(最多递归一下有限的依赖模块,解决比如 TS 类型变量判断问题)时间复杂度一定是 O(1) 的。
实际上我们很少单独使用 snowpack,因为其编译使用的 esbuild 还未达到 1.0 稳定版本,在生态兼容与产物稳定性上存在风险,所以编译打包时往往采用 rollup 或 webpack,但这种割裂也导致了开发与生产环境不一致,这往往代表着更大的风险,因此在 vite 框架可以看到这块的取舍。
snowpack 是开箱即用的:
```json
// package.json
"scripts": {
"start": "snowpack dev",
"build": "snowpack build"
},
```
我们还可以增加 `snowpack.config.js` 配置文件开启 `remote` 模式:
```js
// snowpack.config.js
module.exports = {
packageOptions: {
"source": "remote",
}
};
```
`remote` 模式是 [Streaming Imports](https://www.snowpack.dev/guides/streaming-imports#how-streaming-imports-work),即不用安装对应的 npm 包到本地,snowpack 自动从 [skypack](https://www.skypack.dev/) 读取文件并缓存起来。
snowpack 看起来更多是对 bundless 纯粹的尝试,而不是一个适合满足日常开发的工具,因为日常开发需要一个一站式工具,这就是后面说的 vite 与 wmr。
### vite
可以理解为结合了 snowpack 特色的一站式构建工具,从开发到发布全套流程都帮你搞定。
涉及的用法非常多,具体内容可以看 [官方文档](https://vitejs.dev/)。
与 snowpack 不同的是,snowpack 生产打包的产物是独立的文件,而 vite 没有采用 esbuild 而是 rollup 打包,目的是为了打包为一个整体,并规避 esbuild 不稳定的风险。
另外由于 vite 集成化更高,比 snowpack 多了许多功能,比如 css 拆分、多页、使用 esbuild 进行依赖预构建、monorepo 支持、对多框架支持、SSR 等等。具体可以看 [文档介绍](https://vitejs.dev/guide/comparisons.html#snowpack)。然而原文说这有利有弊,好处是开箱即用,弊端是缺乏定制的灵活性。
其实革命性突破主要是 bundless,在这基础上发展出一系列便捷的功能,这值得每一个工程化团队学习。其实就算决定再造一个轮子,也是维持 90% 功能不变的基础上,在默认的偏好设置做一些微调,而这些大多可以用 [插件](https://vitejs.dev/guide/api-plugin.html) 解决。
总结下来,Vite 是一个既积极拥抱新特性,又为生产环境考虑的工程化全家桶,相比之下,技术栈过于前沿的工具只能称为玩具,而 Vite 是真的可以用一用的。
### wmr
由 preact 作者开发,可以理解为 preact 版的 vite。所以对于 preact 技术栈的开发者更加友好,集成度更高。
原文提到的另一个特色是,wmr 使用了 [htm](https://github.com/developit/htm) 转换 JSX,使其获得了更加精确的报错体验,即可以精确到源码行的同时指定到具体列。
综合功能和 vite 差不多,单页 + ssr 都支持,如果你平时使用 preact,或者想开发一个体积极小的项目,可以考虑用 wmr 全家桶。
## 总结
新一代前端构建工具最大特色有两个:更底层的语言编写、bundless,如果用一个词描述就是高性能。积极拥抱浏览器新特性或者知识跨界都可以帮助前端领域取得新的突破。
另外构建工具已经变得越来越集成化,从仅用于编译的 esbuild,到支持开发的 snowpack,再到内置了最佳实践、甚至支持比如 ssr 等后端能力、最后到垂直场景的 [vitePress](https://github.com/vuejs/vitepress),每抽象一次,都更开箱即用,但带来的灵活性降低也成为各团队自己造轮子的理由,越上层越是有自己造轮子的冲动。
这和可视化领域很像,可视化从最底层的 svg、canvas、webgl 到基于其封装的命令式框架,再到数据驱动开发框架、完全 JSON 配置化的图表库、甚至到零配置,根据数据猜配置的智能化项目,也是配置越来越少,但灵活度越来越低,使用什么层次的完全看项目对细节的要求。
不过工程化相对还是标准化的,因为可视化面向的是用户,而工程化面向的是程序员,我们不能控制用户需求,但可以控制程序员的开发习惯 :P。
最后,除了升级你的构建工具外,换一台 M1 芯片电脑也可以极大提升开发效率,笔者亲测 webpack 构建速度提升 3 倍!
> 讨论地址是:[精读《新一代前端构建工具对比》· Issue #316 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/316)
**如果你想参与讨论,请 [点击这里](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,125 @@
不知道你上次思考前端职业规划是什么时候?
如果你是一位学生,你肯定对前端这个职业感到陌生,你虽然没有经验,但却对未来充满好奇,你有大把时间来思考,但可能摸不着方向,有种拳头打在棉花上的无力感。
如果你已经参加了工作,不论是刚开始实习,还是工作了 3 年、5 年甚至 10 年,一定觉得非常充实,但真正用于思考的时间足够吗?如果维持现状,再过 5 年自己的提升点在哪里?如果你对这些结论不清晰,很可能是缺乏了对职业规划的思考。
这种缺乏职业规划的焦虑已经发展成为了商机。当你没有清晰职业规划,正在迷茫的时候,培训机构站出来说,是不是对职业规划充满焦虑?如果是,可以订购我们的课程,名牌大厂 P10 带你跑赢职场。其实课程确实是干货,但一个具体课程并不能代替你自己的思考,你需要自己想明白自己想要的,而不是被别人灌输思想,因为职场没有标准路线,但培训机构的文案确实有标准写法。
所以这篇前端职业规划是站在我自己角度写的,你如果也在思考长线发展问题,可以作为参考。
我总结出三个主要思考方向,分别是 **知识分类**、**领域深耕**、**经济视角**。
**知识分类** 指的是你对知识的理解是否成体系。现在全球每天新增的知识,一个人穷尽一生也学不完,如果不建立一套你自己的知识筛选标准,长期发展就无从谈起。
**领域深耕** 是实践,天天学习也是没有用的,你必须要做出什么有价值的事情,才能为行业带来贡献,或者说将知识转化为财富。当然不同职业学习与实践的比例是不同的,比如理论物理可能模糊了学习与实践的边界,而在职场环境的工程师,更容易区分什么是学习,什么是实践。
**经济视角** 是说你要能够带着经济视角看问题。可以说没有经济活动,我们一切学习、生产、职业都没有任何意义,因为推动我们学习、推动社会生产的动力是交易,没有经济活动就没有需求,需求是推动一切活动的基础。稍微理解了经济和生产的关系,就能理解为什么技术要为商业服务,因为任何技术都要有转化为商业价值的潜力才值得被研究,大到社会价值,小到产品价值,都一样。
下面我分别讲讲自己对每个方向的理解。
## 知识分类
作为前端,为了保持技术敏锐度,我们会订阅许多专栏了解新知识。仅我知道的周更专栏就有 30 个,其实根据一些专门整理好的专栏检索网站,每周甚至可以看到超过 100 种不同的前端专栏。大部分专栏都在做文章聚合,每篇专栏聚合的文章一般有 5 篇到 30 篇不等,这样即便去除重复,一周至少有几百篇新的前端技术文章等你去读,所以有些同学会觉得焦虑,甚至喊出学不动了。
我每周写前端精读恰好也要找一些文章阅读,但几年下来,我恰恰觉得每周根本找不到有用的素材。就以本周的 [javascript weekly](https://javascriptweekly.com/issues/539) 为例,我摘了一些文章标题:
- [DOM Events: A Way to Visualize and Experiment with the DOM Event System](https://javascriptweekly.com/link/108484/web)。
- [Introducing WebContainers: Run Node.js Natively in the Browser](https://blog.stackblitz.com/posts/introducing-webcontainers/)。
- [New & Updated Course: Complete Intro to React v6 with Brian Holt](https://javascriptweekly.com/link/108483/web)
- [Parcel 2 Beta 3: A Wild Rust Appears!](https://v2.parceljs.org/blog/beta3/)
- [2D Optics Demos in JavaScript](https://javascriptweekly.com/link/108493/web)
- [A Complete Beginner's Guide to Next.js](https://www.youtube.com/watch?v=nBkRxwHMrto)
- [How to Create Reusable Web Components with Lit and Vue](https://javascriptweekly.com/link/108496/web)
第一篇是通过可视化帮你理解 DOM 事件的文章,UI 很有意思,但 DOM 事件作为前端基础,精读实在不适合拿过来炒冷饭,这个知识点讲一遍就行了,没必要做成 UI 后再讲一遍。
第二篇是讲一项技术可以让 Node 运行在浏览器的,这确实是一个新技术,但现阶段我们没必要为这项技术找场景,只要知道有这个东西就行了,没必要仔细阅读。第三篇是对 React 的完整教程,非常体系化,但没有新东西,适合前端新人读,所以也不需要看。
再后面几篇分别是框架升级带来的特性介绍、一个有趣的可视化效果、Next.js 新手入门、如何用 [Lit](https://lit.dev/) 框架开发组件。这些知识从直觉来看属于可读可不读的,读了吧觉得好像对自己没什么成长,不读又觉得错过了什么,真的像鸡肋。
如果你看到这些 Feed 流也有犹豫的感觉,我建议你建立一套前端知识分类体系。就像学习武功,如果你不了解什么是基本功,什么是花拳绣腿,那么每天面临几百本推送过来的 “武学新闻” 确实是无从学起,而且也学不过来。
在技术领域,知识分类体系是有规可循的,大致可以讲知识分为两种类型:通用、行业知识。
通用知识是指最为基础、适用面也最大的知识,比如数理化,这些知识我们上学时都学过,工作中用到的知识都是建立在这些通用知识基础之上的,比如没有一定数学基础就难以学习计算机可视化领域,因为其中会大量运用数学知识。
通用知识最有用,也最保值,所以学校时就安排给我们了,那么大学其实就在教通用行业知识,所以这个阶段如果没有打牢的基础,想要弥补也很简单,只要按照大学教材温习一遍就好了,对于计算机领域的通用知识一般有计算机原理、操作系统、设计模式、编译原理、数据结构、算法等。
领域通用知识看上去比较死板,而初入工作的同学一般都在做拧螺丝钉的事,往往会忽略行业通用知识的重要性,但当你不断深入接触公司核心技术时,会发现大量运用了大学里教的那些通用知识,等用到的时候再学就迟了。
如果说行业通用知识的保值时间是 30 年,那接下来提到的行业专用知识的保值时间只有 1 年。行业专用知识就是我们在 Weekly 上看到的大部分内容,也包括培训班帮我们速成的前端框架、API 等知识。这些知识非常有用,接地气,而且刚接触工作时第一时间就要用到,但这些知识最大的问题就是太过于上层,以至于同类产品过多,可替代性强,知识点可以随着新版本发布全变了样。
就像项目脚手架工具,现在每天都会出一个基于 webpack 或者 rollup 包装的新品牌,这种脚手架就不值得学习,你也不需要把新出的脚手架当作新知识,因为这些知识的生命周期大部分不到一年,大多没有人用,最重要的是除了名字以外,组成要素里没有任何新知识,所以读完源码也学不到新知识。更最重要的是,你无法根据这些知识生产同类产品,所以如果你真的想学脚手架相关知识,认真读好一个主流脚手架源码就行了,以后除了工作中用到,不需要看任何使用文档。
对于架构能力也一样,我们在工作中通过踩坑甚至把一个项目做失败得出的经验,可能只是设计模式这本书里提到的一个常见误区;我们在设计一个非常复杂的系统时,用到的模块通信设计,可能只是操作系统设计里的一种常见通信方法。一个能理解操作系统复杂度的人,基本上可以处理与其等价复杂度的软件工程问题,而软件工程的复杂度其实很难超越操作系统,所以与其在项目里试错,不如从这些基础知识里找答案。
所以如果你想在职业规划上更进一步,检查一下自己的基础是否牢固。如果你通用知识特别扎实,就可以快速学会行业基础知识,根据行业基础知识,你甚至可以独立创造任何一个新的框架,这些框架都会成为别人学习到的行业专用知识,如果另一位同学没有打基础,把时间都用在学习你做的框架上,那么他的职业发展一定程度会被你左右,而他如果只停留在用的阶段,而不了解实现原理,从长期来看,你的职业天花板一定会更高。
关于哪些是通用基础知识、行业基础知识、行业专用知识,这里不给出具体的建议,相信每个人都会有自己的判断。
## 领域深耕
> 这段思考 **不适用于** 刚参加工作的前端同学。
前端有一句有名的鸡汤 “前端不是因为做交互界面,而是因为站在业务的最前端”,其实这句话是有问题的,我觉得每一位工作经验超过三年的前端同学都有一种在业务领域的无力感。
其实最核心的业务模型天然在后端,这是因为前端只是一个用户与业务系统交互的窗口,没有前端,用户也可以和接口直接交互,只是这么做成本很大,所以为了降低用户上手难度,或者带来更好的用户体验,才需要不断升级 UI 界面,所以 UI 界面和后端往往是多对一的关系,移动端、小程序、网页对应的接口都是一套,目的就是为了方便任何场景用户都能轻松触达业务,所以作为前端,首先要对前端存在的原因有正确的认识。
注意这里说的是业务模型,没有提到体验深度,如果讲究体验深度,自然只有前端能做到。然而前端本质还是锦上添花的部分,因为在任何行业耕耘久了,如果仅仅只考虑前端,那么目标永远是体验度量、研发提效的事情,很少触及到业务层,以至于前端在业务价值的体现不直接,比较难解释体验度量、研发提效与最终业务增长之间的关系。
所以对于有一定工作经验的前端同学,想要更进一步,一定要在业务领域深耕。
那么如何在业务领域深耕呢?首先你要抛开前端视角,用业务眼光看问题,否则还是会陷入无尽的交互细节。首先要了解你所在的领域,比如笔者在的数据领域,要知道行业的历史、现状和未来,有哪些产品,每种产品的商业模式是什么,产品之间有什么关联,现在的产品距离头部产品还有哪些差距,今年产品目标主要解决什么问题,三年目标是什么等等。每个同学首先都应该理解产品,其次再产生研发、产品经理的分工。
然后审视一下自己的工作,在产品核心能力里扮演者什么角色?比如做 BI 工具,其核心是数据分析能力与报表可视化分析能力,如果你总在做类似报表列表页、个人中心这种通用中后台的工作,你就要想想,这些工作是不是可以外包出去,如果不行,那就想办法做一些领域搭建,往通用领域转吧。
当你审视了自己工作,发现核心产品能力与你工作内容不相符,而你又不想转到前端中后台通用领域一直做研发提效的事情,这时候你就要想办法和老板沟通改变一下工作内容了,你可以找一些前端也能接触强业务模型的领域,比如 BI 分析,数据可视化等等。其实通用领域也有不少深水区,比如语雀背后的富文本编辑器、流程图、研发工作台、业务组件库等等都是可以做深的通用领域,当你想再上一层楼时,就要像玉伯一样成为语雀整个产品的引领者,这样你其实又进入了知识协作、生产力工具这个专业领域。
如果你既不想往通用技术领域发展,又无法改变工作内容,就尝试承担更多职责吧,如果可能的话,尝试参与后端业务逻辑的开发,这样可以帮助你深入、全面理解业务逻辑。其实前端 + 产品的路线也可以很好在专业领域做深,前端 + 后端路线也可以,你需要根据自己团队实际情况做出调整。
任何产品的研发团队都要有产品全局观,这就是刚才说的在技术之外,你对你所在业务领域的理解程度,理解程度越高,技术方向就越明确,但如果你的职业规划是再继续攀爬,就要成为整个产品负责人了。现在的年轻人非常上进,许多公司都在尝试采取活水政策,让想更进一步的年轻人尝试新方向开疆拓土,而不是留在一个成熟的团队里内卷。
## 经济视角
做职业规划的另一个目的当然是升职加薪了,但是你的薪资并不能无限膨胀,其增长大致还是符合市场规律的。另外任何工作都是一笔经济账,我们要带着技术、产品和经济视角看业务,才能做出合理的判断。
因为去年疫情原因,全球远程办公得到了积极实践,并且在未来依然有增长潜力,因此作为用人单位方,必定会逐渐放眼全球去看人力成本问题,因为在哪都能办公。从全球软件开发数据来看,美国的工资水平最高,中国软件工程师的工资也紧随其后,所以在软件领域中国已经不存在劳动力成本低廉的优势了,尤其当你工作经验丰富后,要竞争中高级岗位,中国软件公司开的薪资放眼全球都不低。
然而国家之间技术发展阶段、教育水平仍然存在差距,如果同样的资深技术专家岗位,国内与国外开的薪资持平,但中国的软件工程师架构水平完全不及美国的软件工程师,那么长期来看,这种错配会造成企业用人成本浪费,企业会在一定程度想办法优化一下人员构成的。因此作为前端,或者软件工程师,你必须清楚长期而言,你要和全球的软件工程师竞争,所以你还要充分了解你的领域在全球范围的发展阶段,人才水平如何。
以上是个人的经济账,接下来谈谈业务的经济账。
首先你要了解自己的技术是怎样转化为收入,覆盖自己工资的。我们首先看市场竞争,市场竞争通过价格调节供需关系,我们做的产品成本、售价很清楚,是否值得做一目了然。然而对于复杂产品需要多人协作,如果人与人之间再通过市场化机制合作,往往容易产生低效的结果,比如我做的按钮按照 3 元一个的价格卖给后端,那为了提升我的价值,我会提价到 5 元一个,然而倾向于给产品加更多的按钮,这样都在看短期利益,谁也不会为产品长期发展负责。
所以公司是一个相对大锅饭的组织,谁也不要给自己工作定价,大家都尽可能的打磨产品,月底按照合同约定给固定薪酬。这样做确实解决了产品长期发展的问题,但这套机制成熟后,尤其在大公司,刚毕业就去拧螺丝钉的同学很可能永远没有机会了解何为成本,没有成本概念,就难以想清楚为什么做事要考虑投入产出比,或者觉得 ROI 这个词很高级,其实这个词一点不高级,只是公司将它屏蔽了,但如果这导致你做技术完全不考虑成本,只追求让你激动的技术细节,或者只做你感兴趣的技术方向,那其实是不成熟的表现,你做的事情可能也难以被业务认可。
如果你想往更高层次发展,成本意识是一定要培养的,可以了解一下人力成本、机器成本、以及接入二方、三方服务的外部成本,了解这些成本后,再算算产品年营收是否能覆盖这些成本,如果想继续加人,那明年产品营收相应要翻多少,现在市场空间允许产品翻这么多吗?如果想提供更好的服务,要加机器,那么你的业务方是否会因为服务变好变得更多?衡量业务方增多带来的价值一般从订单价格,MAU 来看,如果服务外部,直接看价格是否覆盖成本就行了,如果服务内部,就看 MAU 是否值得投入这些机器成本。
然而也不能只看钱,市场份额也很重要。如果 Chrome 对研发投入只看年营收,那现在 IE 估计还是主流浏览器。其实 Chrome 在确立霸主地位后,对谷歌产品生态的打通、W3C 的话语权、开发者吸引力有很大提升,这些看不见的影响面难以直接转化为金钱来统计,所以如果你认为产品市场份额的提升可以带来长线价值,那么也可以把市场份额作为目标之一。
最后经济视角也不仅仅让我们停留在算业务帐上,经济学的边际收益理论可以指导我们优先做边际收益更大的事。当前业务产品矩阵中,拓展哪些产品可以快速弥补不足,如果做技术优化,优化哪些模块带来产品收益、可维护性收益最大,如果时刻能想清楚这些问题,那每年的产品、技术方向就不会跑偏。
## 总结
总结一下文中提到的三个思考方向,其实是职业生涯发展中可能遇到的三种问题。
工作时间久了就会发现,哪怕依然有学习的激情,但保持刚毕业那会的学习方式已经难有突破了,你会发现:工作实践用到的知识不会很多,反复读或者写入门技术文章,只会让自己停留在校招生的技术水平;自己所处的职业也限制了进一步发展,你需要思考怎么打破职业天花板;甚至只钻研技术领域都是不够的,大家都在谈成本,你在谈技术,天然就不在一个频道上。
本文也给出了对应的三个解决方案,**知识分类** 帮助你解决反复学习无用的、入门知识的问题;**领域深耕** 帮助你解决职业天花板的问题;**经济视角** 帮助你解决技术单一视角的问题。
其实职业有天花板很正常,没有哪个职业上升通道是一路无阻的,但人是活的,你可以逐渐改变自己,在适当的时候多看看业务、经济问题,学习知识也不要仅停留在表面,虽然这些你工作中可能根本用不到,但这其实是悖论,因为你没掌握某些知识,所以也没机会接触那些工作,想打破悖论只能从痛苦的自我打破边界开始。
与一般前端职业规划不同,我并没有说很多前端领域专有名词,或者点名要学哪些框架,因为我觉得人之间智商差距并不大,必须掌握的知识工作几年都能学会,而真正能拉开人之间差距的,不是智商,而是学习方法,或者学习路线,如果你把时间用在错误的地方,或者错误的阶段,终将积累成巨大差距。
希望我的思考可以对你有帮助。
> 讨论地址是:[精读《前端职业规划 - 2021 年》· Issue #317 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/317)
**如果你想参与讨论,请 [点击这里](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,388 @@
逻辑编排是用可视化方式描述逻辑,在一般搭建场景中用于代替逻辑描述部分。
更进一步的逻辑编排是前后端逻辑混排,一般出现在一站式 paas 平台,今天就介绍一个全面实现了逻辑编排的 paas 工具 [node-red](https://github.com/node-red/node-red),本周精读的内容是其介绍视频:[How To Create Your First Flow In Node-RED](https://www.youtube.com/watch?v=cVWVr_T7kQ0),介绍了如果利用纯逻辑编排实现一个天气查询应用,以及部署与应用迁移。
## 概述
想要在本地运行 Node-RED 很简单,只要下面两条命令:
```bash
npm install -g --unsafe-perm node-red
node-red
```
之后你就可以看到这个逻辑编排界面了:
<img width=800 src="https://img.alicdn.com/imgextra/i3/O1CN01XBNkBE1km65n40X5S_!!6000000004725-2-tps-2870-1564.png">
我们可以利用这些逻辑节点构建前端网站、后端服务,以及大部分开发工作。光这么说还比较抽象,我们接下来会详细介绍每个逻辑节点的作用,让你了解这些逻辑节点是如何规划设计的,以及逻辑编排到底是怎么控制研发规范来提高研发效率的。
Node-RED 截止目前共有 42 个逻辑节点,按照通用、功能、网络、序列、解析、存储分为六大类。
所有节点都可能有左右连接点,左连接点是输入,右连接点是输出,特殊节点可能有多个输入或多个输出,其实对应代码也不难理解,就是入参和出参。
下面依次介绍每个节点的功能。
### 通用
通用节点处理通用逻辑,比如手动输入数据、调试、错误捕获、注释等。
### inject
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01Bnf5ob1gvTk4e8nXR_!!6000000004204-2-tps-262-66.png">
手动输入节点。可以定期产生一些输入,由下一个节点消费。
举个例子,比如可以定期产生一些固定值,如这样一个这个对象:
```javascript
return {
payload: new Date(),
topic: "abc",
};
```
当然这里是用 UI 表单配置的:
<img width=350 src="https://img.alicdn.com/imgextra/i3/O1CN015CP9Vu1cAWfuNVNiK_!!6000000003560-2-tps-988-226.png">
之后就是消费,几乎后面任何节点都可以消费,比如利用 `change` 节点来设置一些环境变量时,或者利用 `template` 节点设置 html 模版时,都可以拿到这里输入的变量。如果在模版里,变量通过 `{{msg.payload}}` 访问,如果是其它表单,甚至可以通过下拉框直接枚举选择。
然而这个节点往往用来设置静态变量,更多的输入情况是来自其它程序或者用户的,比如 `http in`,这个后面会讲到。其实通过这种组合关系,我们可以把任意节点的输入从生产节点替换为 `inject` 节点,从而实现一些 mock 效果,而 `inject` 节点也支持配置定时自动触发:
<img width=300 src="https://img.alicdn.com/imgextra/i3/O1CN011Pk9ld1TIvOiQNp26_!!6000000002360-2-tps-694-268.png">
### debug
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN018jKYhO1phaGduUHB8_!!6000000005392-2-tps-256-64.png">
用来调试的,当任何输出节点连接到 debug 的输入后,将会在控制台打印出输出信息,方便调试。
比如我们将 `inject` 的输入连上 `debug` 的输入,就可以在触发数据后在控制台看到打印结果:
<img width=500 src="https://img.alicdn.com/imgextra/i4/O1CN01C01riZ1OzuUZSLETu_!!6000000001777-2-tps-1428-374.png">
当然如果你把输入连接到 debug,那么原有逻辑就中断了,然而任何输出节点都可以无限制的输出给其它节点,你只要同时把输出连接到 debug 与功能节点就行了:
<img width=250 src="https://img.alicdn.com/imgextra/i3/O1CN01nzMC5T1m7SB2DgqE8_!!6000000004907-2-tps-670-184.png">
### complete
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01F2H5IN1h6vdyqezT0_!!6000000004229-2-tps-262-66.png">
监听某些节点触发完成动作。通过这个节点,我们可以捕获任意节点触发的动作,可以接入 `debug` 节点打印日志,或者 `function` 节点处理一下逻辑。
可以监听全部节点,也可以用可视化方式选择要监听哪些节点:
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01Sufo4B1bY3PIajzVI_!!6000000003476-2-tps-696-244.png">
#### catch
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01B5Q21C1yIhHuQLBfk_!!6000000006556-2-tps-258-66.png">
错误捕获节点,当任何或指定节点触发错误时输出,输出的格式为:
```text
error.message 字符串
错误消息。
error.source.id 字符串
引发错误的节点的ID。
error.source.type 字符串
引发错误的节点的类型。
error.source.name 字符串
引发错误的节点的名称。(如果已设置)
```
其实每个节点都有固定输出格式,这些固定格式限制了开发灵活度,但熟练掌握后可以大大提升开发效率,因为所有同类型节点格式都是一样的,这是逻辑编排带来规则约束的好处。
#### status
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01Wh5aZ01H4TVt0t1LJ_!!6000000000704-2-tps-260-66.png">
监听节点状态变化。
#### link in
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01FjAGpo1nJBwcdDDjV_!!6000000005068-2-tps-260-66.png">
只能连接 `link out``link in``link out` 就像一个传送门,用来整理逻辑编排节点,使之看上去易于维护。
比如下面的例子,在一个天气 `http in` 服务后,穿插了许多逻辑处理节点,有处理响应 html 内容的 `template` 节点,也有处理请求查询城市天气的 `http request` 服务,整体逻辑虽然聚合,但比较杂乱:
<img width=500 src="https://img.alicdn.com/imgextra/i4/O1CN01g81mDp1PtmoQID4dg_!!6000000001899-2-tps-1564-356.png">
较好的方式是分类,即类似代码开发中的模块化行为,将天气服务导出,其他任何用到的模块直接导入,这个导入动作就是通过 `link in` 实现的,`link out` -> `link in` 只是一个空间位置的变换,传输值是不会变的:
<img width=400 src="https://img.alicdn.com/imgextra/i2/O1CN017V2j9t1IGffzi0jNk_!!6000000000866-2-tps-1076-588.png">
这样模块看起来清晰了许多,如果要知道各个 “传送门” 见连接关系,只要鼠标点击其中一个就可以给出提示,看起来十分方便:
<img width=350 src="https://img.alicdn.com/imgextra/i4/O1CN01Dv0gju1MVZ0vYIR8K_!!6000000001440-2-tps-966-358.png">
#### link out
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01725F7V1YbJNMXzfea_!!6000000003077-2-tps-258-66.png">
`link in` 成对出现,用来导出输入值,后面对接 `link out` 可以像传送门一样将值传送过去,在视觉上不会形成连接线。
#### comment
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01hVy17l23PdMs9zEH7_!!6000000007248-2-tps-248-64.png">
注释,配合 `link` 系列使用,可以让逻辑编排 UI 更易于维护。
结合原视频的例子,对于天气服务,有创建环境变量逻辑,有查询逻辑,其中查询天气还分为查询当前天气、连续 5 天天气、查询国家信息,我们可以在 UI 上讲每块逻辑分组,并利用 `comment` 组件标记好注释,方便阅读:
<img width=500 src="https://img.alicdn.com/imgextra/i3/O1CN015NiEqe1FU0MLwZwdU_!!6000000000489-2-tps-1540-990.png">
### 功能
功能型节点,一般用于处理业务逻辑,所以包含了基础的 if else、js 代码、模版处理等等功能模块。
#### function
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01WLyQo81lBkGqbZgsO_!!6000000004781-2-tps-270-72.png">
最核心的 js 函数模块,你可以用它做任何事:
<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN010IMUYY1yUbaRguc8N_!!6000000006582-2-tps-910-386.png">
其输入会传导到 `msg` 对象,可以通过代码修改 `msg` 对象后再通过输出节点传导出去。
当然也可以访问和修改节点、流程、全局变量,这个在 `change` 节点里介绍。
#### switch
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01HqpK5Q1P0pHZbNkx3_!!6000000001779-2-tps-268-68.png">
对应代码的 switch,只是用起来更加方便,因为我们可以根据不同 case 导出不同的节点:
<img width=450 src="https://img.alicdn.com/imgextra/i4/O1CN013cbv1y1aArjtRZgyC_!!6000000003290-2-tps-1456-438.png">
注意看上图,因为有三条分支,所以节点的导出项也变成了三个,我们可以根据不同逻辑走不同的连接:
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01NPqb4W1lFrpn0CgR1_!!6000000004790-2-tps-730-326.png">
#### change
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01YQ7jN428WZRbbXnSP_!!6000000007940-2-tps-270-70.png">
用来改变环境变量。环境变量分为三种,分别是当前节点、流程(画布)、全局(跨应用)。也就是说,变量可以存储在某个节点上,也可以存储在整个画布上,也可以跨画布存储在全局。
访问参数分别为 `msg.``flow.``global.`,设置这些参数后,就像全局变量一样,任何节点都可以在任何地方使用,比较方便。
比如应用固定了一些 URL 地址,直接把一串字符串写死在某个 `http in` 节点里并不明智,因为后面的 html 或者其它节点里可能会访问它,一旦你进行修改,影响面会非常广,因此最好将其设置为全局变量,在节点中通过变量方式访问:
<img width=280 src="https://img.alicdn.com/imgextra/i2/O1CN01m4pnL520HRQzKfylb_!!6000000006824-2-tps-724-94.png">
其实在控制台,可以看到这三种变量的值:
<img width=200 src="https://img.alicdn.com/imgextra/i2/O1CN01zkx3bt1hEFyyORfVN_!!6000000004245-2-tps-610-682.png">
当我们利用 `change` 节点赋值后,可以通过调试面板查看不同作用域全局变量的值:
<img width=200 src="https://img.alicdn.com/imgextra/i1/O1CN01ghAKzs1ROIoiWcQ1A_!!6000000002101-2-tps-600-222.png">
#### range
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01RfMrp7237JWZPKFfJ_!!6000000007208-2-tps-270-68.png">
区间映射,将一个范围的值映射到另一个范围。其实通过 `function` 模块也能完成,只是因为比较常用所以封装了一个特殊节点。其实用户也可以自己封装节点,具体方式可以参考 [官方文档](https://nodered.org/docs/creating-nodes/)。
<img width=500 src="https://img.alicdn.com/imgextra/i1/O1CN0108N1Y81beSx6ofF89_!!6000000003490-2-tps-1212-324.png">
上图很容易理解,比如数据分析中归一化就可以用这个节点实现。
#### template
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01enr3Y61wnit7hcexe_!!6000000006353-2-tps-272-66.png">
以模版方式生成字符串或 json。
其实本质上也可以被 `function` 代替,只是用来写模版的话有高亮,维护起来比较方便。
内置了 [mustache](https://github.com/janl/mustache.js) 模版语法,通过 `{{}}` 方式使用变量。
比如我们通过 `inject` 注入一个变量给 `template`,并通过 `debug` 打印,流程是这样的:
<img width=600 src="https://img.alicdn.com/imgextra/i2/O1CN01Jj9eM21Mtq0Ym8sk6_!!6000000001493-2-tps-1660-322.png">
其中 `inject` 是这么配置的:
<img width=350 src="https://img.alicdn.com/imgextra/i2/O1CN01q23oD920maMfLkpah_!!6000000006892-2-tps-920-100.png">
可以看到,将 `msg.name` 设置为一个字符串,然后通过 `template` 访问 `name`:
<img width=450 src="https://img.alicdn.com/imgextra/i3/O1CN01IrgPL81IIxefRDgUz_!!6000000000871-2-tps-942-132.png">
#### delay
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01YannSw1xtxtvFQiGP_!!6000000006502-2-tps-270-68.png">
延迟发消息,一个快捷的工具,可以放在任何输入与输出中间,比如让上面的例子中,`inject` 触发后 5s 再打印结果,可以这么配置:
<img width=500 src="https://img.alicdn.com/imgextra/i2/O1CN012ZqRoP1W1vXy6fjyD_!!6000000002729-2-tps-1288-122.png">
#### trigger
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01WGfLxF24PvERo8K09_!!6000000007384-2-tps-268-66.png">
一个消息触发器,相比 `inject`,可以更灵活的设置何时重新触发。
<img width=300 src="https://img.alicdn.com/imgextra/i3/O1CN013x6HjE1pAbkewAwhI_!!6000000005320-2-tps-916-958.png">
从配置可以看出,首先和 `inject` 一样发送一条消息,然后可以等待,或者等待被重置,或者周期性触发(这样就和 `inject` 一样),其中 “发送第二条消息到单独的输出” 和 `switch` 一样会多一个输出口。
然后有重置条件,即 `payload` 为什么值时重置。
通过这个组件可以看出来,其实每个节点都可以用 `function` 节点实现,只不过通过定制一个节点,可以用 UI 而非代码的方式配置,使用起来更方便。
#### exec
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01lUJSvx1LiYdYvfRgu_!!6000000001333-2-tps-268-70.png">
执行系统命令,比如 `ls` 等,这个在系统后台执行而非前端,所以是一个相当危险的节点。
我们可以在配置中写入任何命令:
<img width=450 src="https://img.alicdn.com/imgextra/i4/O1CN017Z9jN91TYxGBdlTrE_!!6000000002395-2-tps-1058-164.png">
#### rbe
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01r1lhS821eAhsVdjQN_!!6000000007009-2-tps-268-66.png">
异常报告节点(Report by Exception),比如说当输入变化时进行阻塞。
### 网络
用于创建网络服务,比如 http、socket、tcp、udp 等等,因为其它都不常用,这次仅介绍 http 服务。
#### http in
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01zo2L6k1eFgMlLHK1v_!!6000000003842-2-tps-262-66.png">
创建一个 http 服务,可以是任何接口或者 web 服务。
当你把 Method 设置为 `post`,连接到 `http response` 就创建了后端接口;当设置为 `get` 请求,并连接 `template` 写上 html 模版,并连接到 `http response` 就创建了 web 服务。
虽然这种方式创建 web 服务难以使用 react 或 vue 框架,不过自定义节点还是为其创造了可能性,或许真的可以把前端模块化文件定义为节点相互串联。
#### http response
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01ElYlgm1jkR2WGCDx4_!!6000000004586-2-tps-266-70.png">
http 返回,只能对接 `http in` 的输出,总是与 `http in` 成对使用。
如果只用了 `http in` 但没有用 `http response`,就相当于后端代码里处理了请求,但没有调用类似:
```typescript
res.send("hello word");
```
来向客户端发送内容。
#### http request
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01TkO4p11wRHTuXqCHX_!!6000000006304-2-tps-268-68.png">
`http in` 创建一个 http 服务不同,`http request` 直接发送一个网络请求并将返回值导入到输出节点。
视频中获取天气的例子,就用了 `http request` 发起请求获取天气信息:
<img width=500 src="https://img.alicdn.com/imgextra/i2/O1CN01B6PIz61GTppGaeXgP_!!6000000000624-2-tps-1482-536.png">
不难看出,发送请求后,又使用了 `function` 节点处理返回结果。不过在逻辑编排中还是期望少使用 `function` 节点,因为除非有很好的命名,否则难以看出来节点含义,如果 `function` 处理内容过多或者 `function` 区块过多,就失去了逻辑编排的意义。
### 序列
序列是对数组进行处理的节点。
#### split
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01wQO6Hl23Dj4PliP2F_!!6000000007222-2-tps-268-68.png">
对应代码的 `split`,将字符串变为数组。
#### join
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01PTpi9Q22NVusDGfX8_!!6000000007108-2-tps-270-68.png">
对应代码的 `join`,一般与 `split` 配合使用,方便处理字符串。
#### sort
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01mHZRX3285Y4gq3w4y_!!6000000007881-2-tps-270-68.png">
对应代码 `sort`,只能根据 `key` 做简单的升序降序处理,对于简单场景比较方便,但对于复杂场景可能还会使用 `function` 节点代替。
#### batch
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01vvRGV71SXkbSkFCUt_!!6000000002257-2-tps-272-70.png">
批量接收输入流后,根据数量进行打包后统一输出,等于批量打包,可以按照数量或者时间间隔进行分组:
<img width=350 src="https://img.alicdn.com/imgextra/i3/O1CN014oSO1t1CAov3Bc5Jg_!!6000000000041-2-tps-826-292.png">
### 解析
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01VZcAqV29jE0GfTvU6_!!6000000008103-2-tps-270-390.png">
很容易理解,专门处理上述格式的数据,并按照数据特征输出,比如 csv 数据,可以每行一条消息的方式输出,或者打包为一个大数组以一条消息输出。
当然也可以被 `function` 节点代替,那么解析方式与输出方式都可以自定义。
### 存储
持久化存储,一般存储为文件。
#### file
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01ndGqqL1T716IwPxcd_!!6000000002334-2-tps-266-74.png">
输出为文件。
#### file in
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN0144b1Jt23aATTxnfDo_!!6000000007271-2-tps-270-68.png">
以文件作为输入,并将文件结果作为输出。
#### watch
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01UlIPAu1mNwR15k4cL_!!6000000004943-2-tps-260-68.png">
监听目录或文件的修改。
## 精读
看了上面 node-red 功能后,相信你对逻辑编排已经有较为体系化的认识了。
逻辑编排的目的是为了让非研发人群也可以快速上手研发工作,因此注定是为 paas 工具服务的,而逻辑编排到底好不好用,取决于节点功能是否完备,以及各节点之间通信是否顺畅,像 node-red 逻辑编排方案,在完备性上做的较为成熟,可以说只要熟练掌握了几个核心节点规则,使用起来还是非常提效的。
逻辑编排也有天然缺点,就是当所有节点都退化为 `function` 节点后,会存在两个问题:
- 所有节点都是 `function` 节点,即便有说明,但内部实现逻辑非常自由,导致逻辑编排无法起到约束输入输出的作用。
- 退化到代码函数式调用,本质上与写代码无异。逻辑编排之所以提效,很大程度上是我要的业务逻辑刚好与节点功能匹配,以低成本 UI 配置的方式实现效率才高。
然而这也是有解决方法的,如果你的业务无法被现有的逻辑编排节点满足,你可以尝试抽象一下,自己梳理出业务常用的节点,并用合理的配置封装,只要常用业务逻辑可以被封装为逻辑节点,逻辑编排就还有为业务提效的空间。
## 总结
逻辑编排是一种极端,即用 UI 方式描述通用业务逻辑,降低非专业开发人员的上手门槛。通过对 [node-red](https://github.com/node-red/node-red) 的分析可以发现,一个较为完备的逻辑编排系统还是能带来价值的。
然而针对非专业开发人员降本提效还有一种极端,就是完全代码化,但是把代码模块化、函数库、工具链甚至低代码平台建设的非常完备,以至于写代码的效率根本不低,这条路走到极致也不错,因为既然要深入开发系统,同样是投入时间学习,为什么学习写代码就一定比学习拖拽 UI 效率低呢?如果有高度封装的函数与工具辅助,效率不见得比 UI 拖拽来的低。
然而 node-red 在创建前端 UI 的模版上还可以再增强一下,把 `template` 从节点升级为 UI 搭建画布,逻辑编排仅用来处理逻辑,这样对大型全栈项目的前端开发体验会更好。
> 讨论地址是:[精读《低代码逻辑编排》· Issue #319 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/319)
**如果你想参与讨论,请 [点击这里](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)
+6 -6
View File
@@ -14,9 +14,9 @@ Nestjs 是我见过的,将 Typescript 与 Nodejs Framework 结合的最好的
Nestjs 不是一个新轮子,它是基于 Express、socket.io 封装的 nodejs 后端开发框架,对 Typescript 开发者提供类型支持,也能优雅降级供 Js 使用,拥有诸多特性,像中间件等就不展开了,本文重点列举其亮点特性。
## 2.1 Modules, Controllers, Components
## 2.1 Modules, Controllers, Providers
Nestjs 开发围绕着这三个单词,Modules 是最大粒度的拆分,表示应用或者模块。Controllers 是传统意义的控制器,一个 Module 拥有多个 Controller。Components 一般用于做 Services,比如将数据库 CRUD 封装在 Services 中,每个 Service 就是一个 Component
Nestjs 开发围绕着这三个单词,Modules 是最大粒度的拆分,表示应用或者模块。Controllers 是传统意义的控制器,一个 Module 拥有多个 Controller。Providers 一般用于做 Services,比如将数据库 CRUD 封装在 Services 中,每个 Service 就是一个 Provider
## 2.2 装饰器路由
@@ -50,7 +50,7 @@ export class UsersController {
## 2.3 模块间依赖注入
Modules, Controllers, Components 之间通过依赖注入相互关联,它们通过同名的 `@Module` `@Controller` `@Component` 装饰器申明,如:
Modules, Controllers, Providers 之间通过依赖注入相互关联,它们通过同名的 `@Module` `@Controller` `@Injectable` 装饰器申明,如:
```typescript
@Controller()
@@ -61,7 +61,7 @@ export class UsersController {
```
```typescript
@Component()
@Injectable()
export class UsersService {
getAllUsers() {
return []
@@ -72,12 +72,12 @@ export class UsersService {
```typescript
@Module({
controllers: [ UsersController ],
components: [ UsersService ],
providers: [ UsersService ],
})
export class ApplicationModule {}
```
`ApplicationModule` 申明其内部 Controllers 与 Components 后,就可以在 Controllers 中注入 Components 了:
`ApplicationModule` 申明其内部 Controllers 与 Providers 后,就可以在 Controllers 中注入 Providers 了:
```typescript
@Controller()
@@ -31,14 +31,15 @@ Atomic Design 书中提到模块化思路以及原子级的模块抽象的方法
体验策略的核心思路是以任务为导向的。主要通过四个方面去构建体验策略:流程与方法、度量体系、运营活动和最佳实践。
## 精读
整个分享感受颇深,但就『自然』这个关键词才生了我自己的疑问主字号、字阶和行高是否存在关系
整个分享感受颇深,但就『自然』这个关键词才生了我自己的疑问主字号、字阶和行高是否存在关系
在梳理的这层关系上,缺少了对字体的讨论,而字体又是非常关键的因素。我在之后,断断续续查阅了不少资料。写下关系背后还要考虑的问题。
1. 字体
我在查阅资料的时候,发现 x-height 在西文字体中的概念。在英文字体的设计中,字体的高度体包含三部份,以基线 (baseline) 为中央,以上称之上行区域 (ascender area),基准线内称之为 x-height,以下称为下行区域 (descender area)。小写西文字母中的核心部件都位于 x-height 位置中,这一位置也被称为排版的核心位置,是引导视线流动的关键。放一张在 wikipedie 上的图:
[image:CCC0E19B-0E90-4989-BD91-A2B33D38E8CD-6272-000031C10BD416C0/820px-Typography_Line_Terms.svg.png]
我在查阅资料的时候,发现 x-height 在西文字体中的概念。在英文字体的设计中,字体的高度体包含三部份,以基线 (baseline) 为中央,以上称之上行区域 (ascender area),基准线内称之为 x-height,以下称为下行区域 (descender area)。小写西文字母中的核心部件都位于 x-height 位置中,这一位置也被称为排版的核心位置,是引导视线流动的关键。放一张在 wikimedia 上的图:
![](https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/Typography_Line_Terms.svg/800px-Typography_Line_Terms.svg.png)
每一种西文字体的 x-height 是不一样的。非常幸运,Jukka Korpela 做了一网站专门可以测量 web 上字体的 x-height。其中,Arial 的 X-HEIGHT RATIO 是 0.519,而 Tahoma 是 0.545Times New Roman 是 0.448。Arial 和 Times New Roman 之间的比例差距大概 17%。
@@ -47,6 +48,7 @@ Atomic Design 书中提到模块化思路以及原子级的模块抽象的方法
这是第一个问题,第二个问题是中文字体没有 x-height,也就是说中文字体就等同于西文字体的全大写,错落的美感都没有。而且中文有一个问题是因为字形之前的差异,每个字之间的留白都不尽相同,看上去又会差一些。一般情况下,靠行间距来弥补视觉差,但总体上要排版达到西文字体的效果要花一些功夫。
2. 屏幕
我们的字体大小使用的是 points(pt),points 是一个物理衡量,它的标准是 72 points per inchPPI)。但我们不同设备的 PPI 都是不一样的,那么造成了同样的设定在不同屏幕下看到的字体也会有差异。
Macbook Pro 的 PPI 是 220Dell XPS 的 PPI 是 165iPhone 7 有 326,但 iPhone 7p 的 PPI 有 401,而一般 HDTV 的 PPI 是 30。其中,iPhoneMacbook 都是 retina 屏。
@@ -64,4 +66,4 @@ Macbook Pro 的 PPI 是 220Dell XPS 的 PPI 是 165iPhone 7 有 326,但
## 总结
曾经有国外的设计师有写文用黄金比例来构建字号与行高的关系,在一片喝彩中看到了资深设计师的反对,主要也是从以上和一些其它因素来说关系是比较难设定。
今天看到我们的设计与理性之间建立的关系,我还是比较坚信建立这种关系背后带来的是更大的价值。
今天看到我们的设计与理性之间建立的关系,我还是比较坚信建立这种关系背后带来的是更大的价值。
@@ -5,14 +5,14 @@ ES 模块为 JavaScript 开发者带来了官方并且标准化的模块系统
## 1. 引言
精读文章主要讨论了下面几点:
- 模块旨在解决些问题;
- 模块旨在解决些问题;
- 模块为开发者带来哪些;
- ES 模块化的工作机制;
- ES 模块化的现状;
## 2. 内容概要
### 模块旨在解决些问题
### 模块旨在解决些问题
JavaScript 开发可以简单地抽象成维护变量,赋值和计算操作。大量的代码在用于操作变量,开发者需要懂得如何去组织和维护这些变量。JavaScript 提供了一种方式,即函数作用域。在一个函数内只需要考虑这个函数的变量问题。不必去担心其他函数会操作这些变量。当然,随之带来的问题是,变量无法共享,无法在不同的函数之间相互共享变量。如果想要在作用域外共享变量,只能通过外层作用域,或者全局作用域。
@@ -422,7 +422,7 @@ function Article({ id }) {
return () => {
didCancel = true;
};
}, [API.fetchArticle]);
}, [id]);
// ...
}
@@ -14,7 +14,7 @@
大家都知道移动端即时通讯是一个唯一寡头市场,因此当米聊看到微信开始反超的时候,就已经知道这场战争已经结束。当时小米重点业务还在手机,米聊是团队试水的一款产品,但看到歪打误撞进入一个如此蓝海的市场,小米自己也很纠结要不要把资源都投入到米聊上。
反观微信,当时手机 QQ 也在做,本来怎么也轮不到微信出场,但张小龙、马化腾、张志东在微信简历了深夜小组,每天晚上都即时同步微信的进展,这让微信即时获取到了腾讯内部资源,在各种关键节点帮了很多忙,甚至让手机 QQ 技术大牛直接支持微信改善高并发问题,快速完成 QQ 好友导入功能。
反观微信,当时手机 QQ 也在做,本来怎么也轮不到微信出场,但张小龙、马化腾、张志东在微信建立了深夜小组,每天晚上都即时同步微信的进展,这让微信即时获取到了腾讯内部资源,在各种关键节点帮了很多忙,甚至让手机 QQ 技术大牛直接支持微信改善高并发问题,快速完成 QQ 好友导入功能。
雷军总结到 “如果腾讯一年后才有所反应,米聊胜率是 50%,如果是腾讯两三个月就有反应,米聊应该 100% 会死掉”。
@@ -32,7 +32,7 @@
前十年,手机设备制造厂商的格局发生了很大变化。国内经历了从小米,到 OPPO、VIVO,再到华为的演化。
印象深刻的是看了一个雷军创办小米前夕的访谈视频,雷军说 “大家看到苹果的成功,却没有看到这片蓝海的机会,现在手机制造领域竞争太不激烈了”。同时为了对抗苹果,谷歌开源了安卓源代码,小米利用这个机会打造一款符合中国人口味的手机操作系统,并借助用户社区与性价比优势一举占领了早市场。
印象深刻的是看了一个雷军创办小米前夕的访谈视频,雷军说 “大家看到苹果的成功,却没有看到这片蓝海的机会,现在手机制造领域竞争太不激烈了”。同时为了对抗苹果,谷歌开源了安卓源代码,小米利用这个机会打造一款符合中国人口味的手机操作系统,并借助用户社区与性价比优势一举占领了早市场。
2015-2018 年出现了 OV 领跑的情况,即 OPPO、VIVO 后来居上,有两点原因:小米还在强调各项参数指标,但 OV 宣传的概念很易懂 “充电五分钟,通话两小时”;同时 OV 还注意到了下沉市场,通过各种综艺节目冠名与 **平均 25 万家线下门店布局**,超越了小米。
@@ -96,7 +96,7 @@
切入点是 **融资**。BAT 上市融资额度分别是:百度:1.112 亿美元、**阿里巴巴 69.88 亿美元**、腾讯 0.2188 亿美元,总额 71.2 亿美元。**而滴滴到目前为止的融资已经达到 208 亿美元,** 滴滴融资超过 BAT 总和,这说明了什么?这说明滴滴走了一条不正常的商业路线,即先疯狂再冷静的烧钱路线。
当一个行业增长速度极速增加时,老玩家将失去优势和壁垒,所以谁能更快扩张谁就能成为最终赢家,此时如果有大量资本投入快速占领市场,让企业成为这个领域的绝对霸主,投资者就可以通过上市退出的方式把之前烧的赚回来。然而这种烧钱商业模式是有前提的,即 **极度充裕的资本 + 清晰的结构性机会**,滴滴的结构性机会非常清晰,先垄断再收割。
当一个行业增长速度极速增加时,老玩家将失去优势和壁垒,所以谁能更快扩张谁就能成为最终赢家,此时如果有大量资本投入快速占领市场,让企业成为这个领域的绝对霸主,投资者就可以通过上市退出的方式把之前烧的赚回来。然而这种烧钱商业模式是有前提的,即 **极度充裕的资本 + 清晰的结构性机会**,滴滴的结构性机会非常清晰,先垄断再收割。
> 传统商业模式:融资 -> 赚钱。
>
@@ -128,7 +128,7 @@ Uber 创始人 特拉维斯·卡兰尼克 说了一句很经典的话,翻译
如果资本不充裕了,对创业者来说也还有机会,比如相应的会带来低人力成本与低广告投放成本。
最后,周航宣传了一个创业孵化项目,即投资人与创业者深度交流几个月,在这几个月内让创业者得到成长,让投资人能看清创业者是否具备潜力,这种投资者与创业者培养感情的孵化方式是比较新颖的,相对面试来说,有更多机会呆在一起可以看人看得更清楚,投资者与创业者更容易简历信任关系。
最后,周航宣传了一个创业孵化项目,即投资人与创业者深度交流几个月,在这几个月内让创业者得到成长,让投资人能看清创业者是否具备潜力,这种投资者与创业者培养感情的孵化方式是比较新颖的,相对面试来说,有更多机会呆在一起可以看人看得更清楚,投资者与创业者更容易建立信任关系。
### 语言 AI 的未来构想
@@ -247,7 +247,7 @@ Uber 创始人 特拉维斯·卡兰尼克 说了一句很经典的话,翻译
1. 食材可见:比如大块杏仁碎、大块黄桃粒等。
2. 口味丰富:芝士、椰子、巧克力、曲奇。
以为朋友当场就订购了几箱,说实话还是蛮有诱惑力的,产品叫 ffit8,可以天猫自行搜索。
一位朋友当场就订购了几箱,说实话还是蛮有诱惑力的,产品叫 ffit8,可以天猫自行搜索。
极客大会每个人都送了几袋,尝了一下还是蛮好吃的,有甜味,但为什么说无糖呢,查了一下原因,原来用的是低聚异麦芽糖,这种麦芽糖难以被吸收,所以也就可以认为是无糖的啦。
@@ -93,7 +93,7 @@ VIPKID 起步是依靠朋友圈传播,但随着项目的起量,需要通过
一加手机做的是高端手机,操作系统主打的是简洁,不会有任何广告,盈利方式则是其较高的定价。而相比手机大厂,一加手机的突破点在于集中力量做旗舰手机,通过集中投入研发资源达到单点突破。
最近一加也在做电视了,目的是为了占领客厅市场,可能因为手机的比较火,资金链比较充裕所以做了更大的布局。
最近一加也在做电视了,目的是为了占领客厅市场,可能因为手机的比较火,资金链比较充裕所以做了更大的布局。
### 解题 - 社区零售新物种的进化之道
@@ -50,7 +50,7 @@
上面是最基本的写作技巧,我就不继续展开了,接下来要重点聊聊的是前端精读是怎么做分享的。我会从如何写作、如何坚持、如何形成正循环三个方面谈谈自己的感受。
首先是写作方式,前端精读的命题很明确,就是基于某个文章或者观点进行精读,因此每篇文章都有一个明确的主题。第二步是摘要,文章内容精简的表达出来,这可以锻炼你的总结能力,也让读者能了解到背景知识。第三步是精读,这一步需要你有一些私藏干货,毕竟把文章直接翻译一遍是没有任何价值的,我在精读自己不熟悉领域的文章时经常遇到这个问题,此时我一般会找几篇类似的文章结合阅读,并找到一些可以互补的观点,这样的精读可以让文章的观点更加饱满。最后是总结,总结时可以点题,将重要内容再梳理一遍,也可以进行延伸,指出更进一步的思考方向。
首先是写作方式,前端精读的命题很明确,就是基于某个文章或者观点进行精读,因此每篇文章都有一个明确的主题。第二步是摘要,文章内容精简的表达出来,这可以锻炼你的总结能力,也让读者能了解到背景知识。第三步是精读,这一步需要你有一些私藏干货,毕竟把文章直接翻译一遍是没有任何价值的,我在精读自己不熟悉领域的文章时经常遇到这个问题,此时我一般会找几篇类似的文章结合阅读,并找到一些可以互补的观点,这样的精读可以让文章的观点更加饱满。最后是总结,总结时可以点题,将重要内容再梳理一遍,也可以进行延伸,指出更进一步的思考方向。
为了让分享坚持下来,我在每周结束之前都会提前立好下周精读的 Flag,在 Github 开一个 issue,这样不仅可以提醒我周末的写作,还可以收获很多来自社区的讨论与反馈,让文章聚集了社区的智慧。这种提前立 Flag 的做法让我想到了自家小区物业费的收取方式,每年年初都会提前征收一整年的物业费,抛开商业手法不谈,这至少意味着物业对业务整整一年的承诺,这种承诺支撑了物业后续一整年的服务,也支撑了每周下一次的精读文章。
+108
View File
@@ -0,0 +1,108 @@
我站在前端角度梳理出数据领域技术专家应该学习的技术点,以下是能力要点:
- 同时具备产品、数据、技术能力。
- 以前端为切入点,深入各技术领域,包括前后端、数据技术。
- 打牢基础知识,熟悉领域知识。
对于知识,可以分为通用、领域的知识,其中越通用、越基础的知识越保值,比如数学知识的保质期可能持续到地球毁灭,而 Webpack5 的 API 可能保质期只有三个月。
所以在学习前,要对各类知识的保质期有个大概的了解:
- **通用基础知识**:100 年以上。
- **行业基础知识**,如产品、计算机基础知识:30 年以上。
- **行业通用领域知识**:10 年以内。
- **行业专用领域知识**:1 年左右。
### 能力模型
- **通用基础知识**
- 数学
- 坐标系
- 参数方程
- 向量
- 点乘
- 叉乘
- 矩阵
- 矩阵乘法
- 线性变换
- 仿射变换
- 计算机 **行业基础知识**
- 数据结构
- 数据
- 链表
-
-
- 哈希表
- 树 & 二叉树 & 二叉搜索树
- 字典树
- 并查集
- 布隆过滤器
- 算法
- 位运算
- 滑动窗口
- 双指针
- 贪心
- 回溯
- 递归
- 分治
- 动态规划
- 编译原理
- 设计模式
- 产品
- **行业基础知识**
- 经济学
- 商学
- 心理学
- 数据
- **行业基础知识**
- 数据明细与聚合
- SQL
- **行业通用领域知识**
- 计算字段
- **行业专用领域知识**
- 数据分析表达式
- 前端
- **行业基础知识**
- 编程范式
- 命令式
- 过程式
- 面向对象
- 声明式
- 逻辑式
- 函数式
- 数据修改
- 可变数据
- 不可变数据
- 图形学
- **行业通用领域知识**
- javascript、typescript
- css
- dom、svg、canvas
- webgl
- **行业专用领域知识**
- 前端框架
- React
- Vue
- 数据流框架
- Redux
- Mobx
- 构建工具
- Webpack
- Snowpack
- 脚手架
- Vite
- 全栈框架
- Next.js
- 后端
- **行业基础知识**
- 架构模式
- 一主多从
- **行业专用领域知识**
- 后端框架
- Spring
### 前端精读与能力模型
前端精读不可能覆盖上述所有知识,有的知识是课本学的,有的知识是步入社会后,看书或者通过专门渠道学习的,所以本篇只对能力模型做一个指导,而学海无涯,前端精读即便持续创作一百年,也仍然挂一漏万。
不过这个能力模型会对前端精读选材起到主导作用,我会尽量挑选重要的,保质期长的基础知识解读,而保质期较短的行业专用领域知识则尽量少讲,希望前端精读能形成一个正金字塔,底座是厚厚的基础知识,金字塔尖是重要的行业知识,风沙会经常侵蚀金字塔尖,所以金字塔尖需要不断的更新迭代,但我相信,只要有一个稳定的底座,上层的修缮总不是难事。
@@ -0,0 +1,337 @@
很多人觉得动态规划很难,甚至认为面试出动态规划题目是在为难候选人,这可能产生一个错误潜意识:认为动态规划不需要掌握。
其实动态规划非常有必要掌握:
1. 非常锻炼思维。动态规划是非常锻炼脑力的题目,虽然有套路,但每道题解法思路差异很大,作为思维练习非常合适。
2. 非常实用。动态规划听起来很高级,但实际上思路和解决的问题都很常见。
动态规划用来解决一定条件下的最优解,比如:
- 自动寻路哪种走法最优?
- 背包装哪些物品空间利用率最大?
- 怎么用最少的硬币凑零钱?
其实这些问题乍一看都挺难的,毕竟都不是一眼能看出答案的问题。但得到最优解又非常重要,谁能忍受游戏中寻路算法绕路呢?谁不希望背包放的东西更多呢?所以我们一定要学好动态规划。
## 精读
动态规划不是魔法,它也是通过暴力方法尝试答案,只是方式更加 “聪明”,使得实际上时间复杂度并不高。
### 动态规划与暴力、回溯算法的区别
上面这句话也说明了,所有动态规划问题都能通过暴力方法解决!是的,所有最优解问题都可以通过暴力方法尝试(以及回溯算法),最终找出最优的那个。
暴力算法几乎可以解决一切问题。回溯算法的特点是,通过暴力尝试不同分支,最终选择结果最优的线路。
而动态规划也有分支概念,但不用把每条分支尝试到终点,而是在走到分叉路口时,可以直接根据前面各分支的表现,直接推导出下一步的最优解!然而无论是直接推导,还是前面各分支判断,都是有条件的。动态规划可解问题需同时满足以下三个特点:
1. 存在最优子结构。
2. 存在重复子问题。
3. 无后效性。
### 存在最优子结构
即子问题的最优解可以推导出全局最优解。
什么是子问题?比如寻路算法中,走完前几步就是相对于走完全程的子问题,必须保证走完全程的最短路径可以通过走完前几步推导出来,才可以用动态规划。
不要小看这第一条,动态规划就难在这里,你到底如何将最优子结构与全局最优解建立上关系?
- 对于爬楼梯问题,由于每层台阶都是由前面台阶爬上来的,因此必然存在一个线性关系推导。
- 如果变成二维平面寻路呢?那么就升级为二维问题,存在两个变量 `i,j` 与上一步之间关系了。
- 如果是背包问题,同时存在物品数量 `i`、物品重量 `j` 和物品质量 `k` 三个变量呢?那就升级为三位问题,需要寻找三个之间的关系。
依此类推,复杂度可以上升到 N 维,维度越高思考的复杂度就越高,空间复杂度就越需要优化。
### 存在重复子问题
即同一个子问题在不同场景下存在重复计算。
比如寻路算法中,同样两条路线的计算中,有一段路线是公共的,是计算的必经之路,那么只算一次就好了,当计算下一条路时,遇到这个子路,直接拿第一次计算的缓存即可。典型例子是斐波那契数列,对于 `f(3)``f(4)`,都要计算 `f(1)``f(2)`,因为 `f(3) = f(2) + f(1)`,而 `f(4) = f(3) + f(2) = f(2) + f(1) + f(2)`
这个是动态规划与暴力解法的关键区别,动态规划之所以性能高,是因为 **不会对重复子问题进行重复计算**,算法上一般通过缓存计算结果或者自底向上迭代的方式解决,但核心是这个场景要存在重复子问题。
当你觉得暴力解法可能很傻,存在大量重复计算时,就要想想是哪里存在重复子问题,是否可以用动态规划解决了。
### 无后效性
即前面的选择不会影响后面的游戏规则。
寻路算法中,不会因为前面走了 B 路线而对后面路线产生影响。斐波那契数列因为第 N 项与前面的项是确定关联,没有选择一说,所以也不存在后效性问题。
什么场景存在后效性呢?比如你的人生是否能通过动态规划求最优解?其实是不行的,因为你今天的选择可能影响未来人生轨迹,比如你选择了计算机这个职业,会直接影响到工作的领域,接触到的人,后面的人生路线因此就完全变了,所以根本无法与选择了土木工程的你进行比较,因为人生赛道都变了。
有同学可能觉得这样局限是不是很大?其实不然,无后效性的问题仍然很多,比如背包放哪件物品、当前走哪条路线、用了哪些零钱,都不会影响整个背包大小、整张地图的地形、以及你最重要付款的金额。
### 解法套路 - 状态转移方程
解决动态规划问题的核心就是写出状态转移方程,所谓状态转移,即通过某些之前步骤推导出未来步骤。
状态转移方程一般写为 `dp(i) = 一系列 dp(j) 的计算`,其中 `j < i`
其中 `i``dp(i)` 的含义很重要,一般 `dp(i)` 直接代表题目的答案,`i` 就有技巧了。比如斐波那契数列,`dp(i)` 表示的答案就是最终结果,`i` 表示下标,由于斐波那契数列直接把状态转移方程告诉你了 `f(x) = f(x-1) + f(x-2)`,那么根本连推导都不必了。
**对于复杂问题,难在如何定义 `i` 的含义,以及下一步状态如何通过之前状态推导。** 这个做多了题目就有体会,如果没有,那即便再如何解释也难以说明,所以后面还是直接看例子吧。
先举一个最简单的动态规划例子 - 爬楼梯来说明问题。
### 爬楼梯问题
爬楼梯是一道简单题,题目如下:
> 假设你正在爬楼梯。需要 `n` 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?(给定 `n` 是一个正整数)
首先 `dp(i)` 就是问题的答案(解法套路,`dp(i)` 大部分情况就是答案,这样解题思路会最简化),即爬到第 `i` 阶台阶的方法数量,那么 `i` 自然就是要爬到第几阶台阶。
我们首先看是否存在 **最优子结构**?因为只能往上爬,所以第 `i` 阶台阶有几种爬方完全取决于前面有几种爬方,**而一次只能爬 1 或 2 个台阶,所以第 `i` 阶台阶只可能从第 `i-1``i-2` 个台阶爬上来的**,所以第 `i` 个台阶的爬法就是 `i-1``i-2` 总爬法之和。所以显然有最优子结构,连状态转移方程都呼之欲出了。
再看是否存在 **存在重复子问题**,其实爬楼梯和斐波那契数列类似,最终的状态转移方程是一样的,所以显然存在重复子问题。当然直观来看也容易分析出,10 阶台阶的爬法包含了 8、9 阶的爬法,而 9 阶台阶爬法包含了 8 阶的,所以存在重复子问题。
最后看是否 **无后效性**?由于前面选择一次爬 1 个或 2 个台阶并不会影响总台阶数,也不会影响你下一次能爬的台阶数,所以无后效性。如果你爬了 2 个台阶,因为太累,下次只能爬 1 个台阶,就属于有后效性了。或者只要你一共爬了 3 次 2 阶,就会因为太累而放弃爬楼梯,直接下楼休息,那么问题提前结束,也属于有后效性。
所以爬楼梯的状态转移方程为:
- `dp(i) = dp(i-1) + dp(i-2)`
- `dp(1) = 1`
- `dp(2) = 2`
注意,因为 1、2 阶台阶无法应用通用状态转移方程,所以要特殊枚举。这种枚举思路在代码里其实就是 **递归终结条件**,也就是作为函数 `dp(i)` 不能无限递归,当 `i` 取值为 1 或 2 时直接返回枚举结果(对这道题而言)。所以在写递归时,一定要优先写上递归终结条件。
然后我们考虑,对于第一阶台阶,只有一种爬法,这个没有争议吧。对于第二阶台阶,可以直接两步跨上来,也可以走两个一步,所以有两种爬法,也很容易理解,到这里此题得解。
关于代码部分,仅这道题写一下,后面的题目如无特殊原因就不写代码了:
```typescript
function dp(i: number) {
switch (i) {
case 1:
return 1;
case 2:
return 2;
default:
return dp(i - 1) + dp(i - 2);
}
}
return dp(n);
```
当然这样写重复计算了子结构,所以我们不要每次傻傻的执行 `dp(i - 1)`(因为这样计算了超多重复子问题),我们需要用缓存兜底:
```typescript
const cache: number[] = [];
function dp(i: number) {
switch (i) {
case 1:
cache[i] = 1;
break;
case 2:
cache[i] = 2;
break;
default:
cache[i] = cache[i - 1] + cache[i - 2];
}
return cache[i];
}
// 既然用了缓存,最好子底向上递归,这样前面的缓存才能优先算出来
for (let i = 1; i <= n; i++) {
dp(i);
}
return cache[n];
```
当然这只是简单的一维线性缓存,更高级的缓存模式还有 **滚动缓存**。我们观察发现,这道题缓存空间开销是 `O(n)`,但每次缓存只用了上两次的值,所以计算到 `dp(4)` 时,`cache[1]` 就可以扔掉了,或者说,我们可以滚动利用缓存,让 `cache[3]` 占用 `cache[1]` 的空间,那么整体空间复杂度可以降低到 `O(1)`,具体做法是:
```typescript
const cache: [number, number] = [];
function dp(i: number) {
switch (i) {
case 1:
cache[i % 2] = 1;
break;
case 2:
cache[i % 2] = 2;
break;
default:
cache[i % 2] = cache[(i - 1) % 2] + cache[(i - 2) % 2];
}
return cache[i % 2];
}
for (let i = 1; i <= n; i++) {
dp(i);
}
return cache[n % 2];
```
通过取余,巧妙的让缓存永远交替占用 `cache[0]``cache[1]`,达到空间利用最大化。当然,这道题因为状态转移方程是连续用了前两个,所以可以这么优化,如果遇到用到之前所有缓存的状态转移方程,就无法使用滚动缓存方案了。然而还有更高级的多维缓存,这个后面提到的时候再说。
接下来看一个进阶题目,最大子序和。
### 最大子序和
最大子序和是一道简单题,题目如下:
> 给定一个整数数组 `nums` ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
首先按照爬楼梯的套路,`dp(i)` 就表示最大和,由于整数数组可能存在负数,所以越多数相加,和不一定越大。
接着看 `i`,对于数组问题,大部分 `i` 都可以代表以第 `i` 位结尾的字符串,那么 `dp(i)` 就表示以第 `i` 位结尾的字符串的最大和。
可能你觉得以 `i` 结尾,就只能是 `[0-i]` 范围的值,那么 `[j-i]` 范围的字符串不就被忽略了?其实不然,`[j-i]` 如果是最大和,也会被包含在 `dp(i)` 里,因为我们状态转移方程可以选择不连上 `dp(i-1)`
现在开始解题:首先题目是最大和的连续子数组,一般连续的都比较简单,因为对于 `dp(i)`,要么和前面连上,要么和前面断掉,所以状态转移方程为:
- `dp(i) = dp(i-1) + nums[i]` 如果 `dp(i-1) > 0`
- `dp(i) = nums[i]` 如果 `dp(i-1) <= 0`
怎么理解呢?就是第 `i` 个状态可以直接由第 `i-1` 个状态推导出来,既然 `dp(i)` 是指以第 `i` 个字符串结尾的最大和,那么 `dp(i-1)` 就是以第 `i-1` 个字符串结尾的最大和,而且此时 `dp(i-1)` 已经算出来了,那么 `dp(i)` 怎么推导就清楚了:
因为字符串是连续的,所以 `dp(i)` 要么是 `dp(i-1)` + `nums[i]`,要么就直接是 `nums[i]`,所以选择哪种,取决于前面的 `dp(i-1)` 是否是正数,**因为以 `i` 结尾一定包含 `nums[i]`,所以 `nums[i]` 不管是正还是负,都一定要带上。** 所以容易得知,`dp(i-1)` 如果是正数就连起来,否则就不连。
好了,经过这么详细的解释,相信你已经完全了解动态规划的解题套路,后面的题目解释方式我就不会这么啰嗦了!
这道题如果再复杂一点,不连续怎么办呢?让我们看看最长递增子序列问题吧。
### 最长递增子序列
最长递增子序列是一道中等题,题目如下:
> 给你一个整数数组 `nums` ,找到其中最长严格递增子序列的长度。
>
> 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,`[3,6,2,7]` 是数组 `[0,3,1,6,2,2,7]` 的子序列。
其实之前的 [精读《DOM diff 最长上升子序列》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/192.%E7%B2%BE%E8%AF%BB%E3%80%8ADOM%20diff%20%E6%9C%80%E9%95%BF%E4%B8%8A%E5%8D%87%E5%AD%90%E5%BA%8F%E5%88%97%E3%80%8B.md) 有详细解析过这道题,包括还有更优的贪心解法,不过我们这次还是聚焦在动态规划方法上。
这道题与上一道的区别就是,首先递增,其次不连续。
按照套路,`dp(i)` 就表示以第 `i` 个字符串结尾的最长上升子序列长度,那么重点是,`dp(i)` 怎么通过之前的推导出来呢?
由于是不连续的,因此不能只看 `dp(i-1)` 了,因为 `nums[i]` 项与 `dp(j)`(其中 `0 <= j < i`)组合后都可能达到最大长度,因此需要遍历所有 `j`,尝试其中最大长度的组合。
所以状态转移方程为:
`dp[i] = max(dp[j]) + 1`,其中 `0<=j<i``num[j]<num[i]`
这道题的出现,预示着较为复杂的状态转移方程的出现,即第 `i` 项不是简单由 `i-1` 推导,而是由之前所有 `dp(j)` 推导,其中 `0<=j<i`
除此之外,还有推导变种,即根据 `dp(dp(i))` 推导,即函数里套函数,这类问题由于加深了一层思考脑回路,所以相对更难。我们看一道这样的题目:最长有效括号。
### 最长有效括号
最长有效括号是道困难题,题目如下:
> 给你一个只包含 `'('` 和 `')'` 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
这道题之所以是困难题,就因为状态转移方程存在嵌套思维。
我们首先按套路定义 `dp(i)` 为答案,即以第 `i` 下标结尾的字符串中最长有效括号长度。看出来了吗?一般字符串题目中,`i` 都是以字符串下标结尾来定义,很少有定义为开头或者别的定义行为。当然非字符串问题就不是这样了,这个在后面再说。
我们继续题目,如果 `s[i]``(`,那么不可能组成有效括号,因为最右边一定不闭合,所以考虑 `s[i]``)` 的场景。
如果 `s[i-1]``(`,那么构成了 `...()` 之势,最后两个自成合法闭合,所以只要看前面的即可,即 `dp(i-2)`,所以这种场景的状态转移方程为:
`dp(i) = dp(i-2) + 2`
如果 `s[i-1]``)` 呢?构成了 `...))` 的状态,那么只有 `i-1` 是合法闭合的,且这个合法闭合段之前必须是 `(` 与第 `i` 项形成闭合,才构成此时最长有效括号长度,所以这种场景的状态转移方程为:
`dp(i) = dp(i-1) + dp(i - dp(i-1) - 2) + 2`,你可以结合下面的图来理解:
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN016tRvXm1o4p8U1Plfk_!!6000000005172-2-tps-1088-378.png">
可以看到,`dp(i-1)` 就是第二条横线的长度,然后如果红色括号匹配的话,长度又 +2,最后别忘了最左边如果有满足匹配的也要带上,这就是 `dp(i - dp(i-1) - 2)`,所以加到一起就是这种场景的括号最大长度。
到这里,一维动态规划问题深度基本上探索完了,在进入多维动态规划问题前,还有一类一维动态规划问题,属于表达式不难,也没有这题这么复杂的嵌套 DP,但是思维复杂度极高,**你一定不要盯着全流程看,那样复杂度太高,你需要充分认可 dp(i-x) 已经算出来部分的含义,进行高度抽象的思考。**
### 栅栏涂色
栅栏涂色是一道困难题,题目如下:
> 有 `k` 种颜色的涂料和一个包含 `n` 个栅栏柱的栅栏,每个栅栏柱可以用其中一种颜色进行上色。
>
> 你需要给所有栅栏柱上色,并且保证其中相邻的栅栏柱 **最多连续两个** 颜色相同。然后,返回所有有效涂色的方案数。
这道题 `k``n` 都非常巨大,常规暴力解法甚至普通 DP 都会超时。选择 `i` 的含义也很重要,这里 `i` 到底代表用几种颜色还是几个栅栏呢?选择栅栏会好做一些,因为栅栏是上色的主体。这样 `dp(i)` 就表示上色前 `i` 个栅栏的所有涂色方案。
首先看下递归终止条件。由于最多连续两个颜色相同,因此 `dp(0)``dp(1)` 分别是 `k``k*k`,因为每个栅栏随便刷颜色,自由组合。那么 `dp(2)` 有三个栅栏,非法情况是三个栅栏全同色,所以用所有可能减掉非法即可,非法场景只有 `k` 中,所以结果是 `k*k*k - k`
那么考虑一般情况,对于 `dp(i)` 有几种涂色方案呢?直接思考情况太多,我们把情况一分为二,考虑 `i``i-1` 颜色相同与不同两种情况考虑。
如果 `i``i-1` 颜色相同,那么为了合法,`i-1` 肯定不能与 `i-2` 颜色相同了,否则就三个同色,这样的话,不管 `i-2` 是什么颜色,`i-1``i` 都只能少取一种颜色,少取的颜色就是 `i-2` 的颜色,因此 `[i-1,i]` 这个区间有 `k-1` 中取色方案,前面有 `dp(i-2)` 种取色方案,相乘就是最终方案数:`dp(i-2) * (k-1)`
**这背后其实存在动态思维,即每种场景的 `k-1` 都是不同的颜色组合,只是无论前面 `dp(i-2)` 是何种组合,后面两个栅栏一定有 `k-1` 种取法,虽然颜色组合的色值不同,但颜色组合数量是不变的,所以可以统一计算。理解这一点非常关键。**
如果 `i``i-1` 颜色不同,那么第 `i` 项只有 `k-1` 种取法,一样也是动态的,因为永远不能和 `i-1` 颜色相同。最后乘上 `dp(i-1)` 的取色方案,就是总方案数:`dp(i-1) * (k-1)`
所以最后总方案数就是两者之和,即 `dp(i) = dp(i-2) * (k-1) + dp(i-1) * (k-1)`
这道题的不同之处在于,变化太多,任何一个栅栏取的颜色都会影响后面栅栏要取的颜色,**乍一看觉得是个有后效性的题目,无法用动态规划解决**。但实际上,虽然有后效性,但如果进行合理的拆解,后面栅栏的总可能性 `k-1` 是不变的,**所以考虑总可能性数量,是无后效性的**,因此站在方案总数上进行抽象思考,才可能破解此题。
接下来介绍多维动态规划,从二维开始。二维动态规划就是用两个变量表示 DP,即 `dp(i,j)`,一般在二维数组场景出现较多,当然也有一些两个数组之间的关系,也属于二维动态规划,为了继续探讨字符串问题,我选择了字符串问题的二维动态规划范例,编辑距离这道题来说明。
### 编辑距离
编辑距离是一道困难题,题目如下:
> 给你两个单词 `word1` 和 `word2`,请你计算出将 `word1` 转换成 `word2` 所使用的最少操作数。
>
> 你可以对一个单词进行如下三种操作:
>
> - 插入一个字符
> - 删除一个字符
> - 替换一个字符
只要是字符串问题,基本上 `i` 都表示以第 `i` 项结尾的字符串,但这道题有两个单词字符串,**为了考虑任意匹配场景,必须用两个变量表示,即 `i` `j` 分别表示 `word1``word2` 结尾下标时,最少操作次数。**
那么对于 `dp(i,j)` 考虑 `word1[i]``word2[j]` 是否相同,最后通过双重递归,先递归 `i`,在递归内再递归 `j`,答案就出来了。
假设最后一个字符相同,即 `word1[i] === word2[j]` 时,**由于最后一个字符不用改就相同了,所以操作次数就等价于考虑到前一个字符**,即 `dp(i,j) = dp(i-1,j-1)`
假设最后一个字符不同,那么 **最后一步** 有三种模式可以得到:
1. 假设是替换,即 `dp(i,j) = dp(i-1,j-1) + 1`,因为替换最后一个字符只要一步,并且和前面字符没什么关系,所以前面的最小操作次数直接加过来。
2. 假设是插入,即 `word1` 插入一个字符变成 `word2`,那么只要变换到这一步再 +1 插入操作就行了,变换到这一步由于插入一个就行了,因此 `word1``word2` 少一个单词,其它都一样,要变换到这一步,就要进行 `dp(i,j-1)` 的变换,因此 `dp(i,j) = dp(i,j-1) + 1`。。
3. 假设是删除,即 `word1` 删除一个字符变成 `word2`,同理,要进行 `dp(i-1,j)` 的变化后多一步删除,因此 `dp(i,j) = dp(i-1,j) + 1`
由于题目取操作最少次数,所以这三种情况取最小即可,即 `dp(i,j) = min(dp(i-1,j-1), dp(i,j-1), dp(i-1,j)) + 1`
所以同时考虑了最后一个字符是否相同后,合并了的状态转移方程就是最终答案。
我们再考虑终止条件,即 `i``j` 为 -1 时的情况,因为状态转移方程 `i``j` 不断减小,肯定会减少到 0 或 -1,因为 0 是字符串还有一个字符,相对比如考虑 -1 字符串为空时方便,因此我们考虑 -1 时作为边界条件。
`i` 为 -1 时,即 `word1` 为空,此时要变换为 `word2` 很显然,只有插入 `j` 次是最小操作次数,因此此时 `dp(i,j) = j`;同理,当 `j` 为 -1 时,即 `word2` 为空,此时要删除 `i` 次,因此操作次数为 `i`,所以 `dp(i,j) = i`
### 非字符串问题
说到这,相信你在字符串动规问题上已经如鱼得水了,我们再看看非字符串场景的动规问题。非字符串场景的动规比较经典的有三个,第一是矩形路径最小距离,或者最大收益;第二是背包问题以及变种;第三是打家劫舍问题。
这些问题解决方式都一样,只是对于 `dp(i)` 的定义略有区别,比如对于矩形问题来说,`dp(i,j)` 表示走到 `i,j` 格子时的最小路径;对于背包问题,`dp(i,j)` 表示装了第 `i` 个物品时,背包还剩 `j` 空间时最大价格;对于打家劫舍问题,`dp(i)` 表示打劫到第 `i` 个房间时最大收益。
因为篇幅问题这里就不一详细介绍了,只简单说明一下矩形问题于打家劫舍问题。
对于矩形问题,状态转移方程重点看上个状态是如何转移过来的,一般矩形只能向右或者向下移动,路途可能有一些障碍物不能走,我们要做分支判断,然后选择一条符合题目最值要求的路线作为当前 `dp(i)` 的转移方程即可。
对于打家劫舍问题,由于不能同时打劫相邻的房屋,所以对于 `dp(i)`,要么为了打劫 `i-1` 而不打劫第 `i` 间,或者打劫 `i-2` 于第 `i` 间,取这两种终态的收益最大值即可,即 `dp(i) = max(dp(i-1), dp(i-2) + coins[i])`
## 总结
动态规划的核心分为三步,首先定义清楚状态,即 `dp(i)` 是什么;然后定义状态转移方程,这一步需要一些思考技巧;最后思考验证一下正确性,即尝试证明你写的状态转移方程是正确的,在这个过程要做到状态转移的不重不漏,所有情况都被涵盖了进来。
动态规划最经典的还是背包问题,由于篇幅原因,可能下次单独出一篇文章介绍。
> 讨论地址是:[精读《算法 - 动态规划》· Issue #327 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/327)
**如果你想参与讨论,请 [点击这里](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)