├── 2014 ├── think-like-a-rookie-while-practice-like-a-pro.html ├── think-like-a-rookie-while-practice-like-a-pro.md └── think-like-a-rookie-while-practice-like-a-pro │ └── cover.jpg ├── 2015 ├── an-illustrated-brief-introduction-to-algorithm.html ├── an-illustrated-brief-introduction-to-algorithm.md ├── an-illustrated-brief-introduction-to-algorithm │ └── cover.jpg ├── cors.html ├── cors.md ├── cors │ └── cors_server_flowchart.png ├── decision-tree.html ├── decision-tree.md ├── decision-tree │ ├── Makefile │ ├── id3.js │ ├── main.css │ ├── main.dist.js │ ├── main.js │ ├── ross-quinlan.jpg │ └── visualizeID3.js ├── fixing-latex-in-mac.html ├── fixing-latex-in-mac.md ├── frontend-visual-modeling.html ├── frontend-visual-modeling.md ├── frontend-visual-modeling │ ├── pai.png │ └── radar.png ├── git.html ├── git.md ├── lets-make-a-compiler.html ├── lets-make-a-compiler.md └── lets-make-a-compiler │ └── cover.jpg ├── 2016 ├── 70-math-quizs-for-programmers.html ├── 70-math-quizs-for-programmers.md ├── 70-math-quizs-for-programmers │ └── cover.jpg ├── from-icon-fonts-to-svg-icons.html ├── from-icon-fonts-to-svg-icons.md ├── from-socialcalc-to-ethercalc.html ├── from-socialcalc-to-ethercalc.md ├── from-socialcalc-to-ethercalc │ ├── flow-nodejs.png │ ├── flow-snapshot.png │ ├── multiplayer-socialcalc.png │ ├── profiler-jsdom.png │ ├── profiler-no-jsdom.png │ ├── scaling-cluster.png │ ├── scaling-evented.png │ ├── scaling-threads.png │ └── wikicalc-socialcalc.png ├── running-scripts-with-npm.html ├── running-scripts-with-npm.md ├── running-scripts-with-npm │ └── npm-script.png ├── socialcalc.html ├── socialcalc.md └── socialcalc │ ├── collab-borders.png │ ├── collab-conflict.png │ ├── collab-flow.png │ ├── collab-olpc.png │ ├── collab-resolution.png │ ├── richtext-example.png │ ├── richtext-flow.png │ ├── richtext-formats.png │ ├── richtext-screenshot.png │ ├── socialcalc-2046.png │ ├── socialcalc-cell-handles.png │ ├── socialcalc-class-diagram.png │ ├── socialcalc-command-runloop.png │ ├── socialcalc-input.png │ ├── socialcalc-parts.png │ ├── socialcalc-screenshot.png │ ├── wikicalc-components.png │ ├── wikicalc-flow.png │ ├── wikicalc-loading.png │ └── wikicalc-screenshot.png ├── 2017 ├── a-brief-intro-to-search-engine.html ├── a-brief-intro-to-search-engine │ ├── archie.png │ ├── baidu.svg │ ├── bing.svg │ ├── dash-docset.png │ ├── dash-index.png │ ├── dash.png │ ├── google-1998.png │ ├── google-adwords.png │ ├── market-share.js │ ├── other-subjects.js │ ├── other-subjects.png │ ├── overture.jpg │ ├── reveal.js │ ├── search-engine.svg │ ├── search-engine.xml │ ├── tim-bl.jpg │ ├── timeline.js │ ├── types-of-search-queries.js │ ├── webcrawler.gif │ └── yahoo-directory.png ├── a-technique-for-drawing-directed-graphs.html ├── a-technique-for-drawing-directed-graphs.md ├── a-technique-for-drawing-directed-graphs │ ├── figure-1-2a.png │ ├── figure-1-3a.png │ ├── figure-2-3.png │ ├── figure-2-4.png │ ├── figure-2-5.png │ ├── figure-4-2.png │ ├── figure-4-3.png │ ├── figure-4-4.png │ ├── figure-5-1.png │ ├── figure-5-4.png │ └── figure-5-5.png ├── mind-map-drawing-algorithms │ ├── downward-organizational.svg │ ├── downward-tree-organizational.svg │ ├── indented.png │ ├── right-fish-bone.svg │ ├── right-logical.svg │ └── standard.svg ├── mindmap-drawing-algorithms.html └── mindmap-drawing-algorithms.md ├── .editorconfig ├── .gitignore ├── .jscsrc ├── .zfinderrc.yaml ├── README.md ├── index.html └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [Makefile] 16 | indent_style = tab 17 | indent_size = 1 18 | 19 | [*.yml] 20 | indent_size = 2 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | db.json 4 | *.log 5 | node_modules/ 6 | public/ 7 | .deploy*/ 8 | 9 | .idea 10 | temp 11 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "requireCurlyBraces": null, 4 | "validateIndentation": 4, 5 | "safeContextKeyword": [ 6 | "me", 7 | "that" 8 | ], 9 | "requireCamelCaseOrUpperCaseIdentifiers": { 10 | "ignoreProperties": true, 11 | "strict": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.zfinderrc.yaml: -------------------------------------------------------------------------------- 1 | openOnStart: true 2 | port: 2333 3 | handler: 4 | serve-index: 5 | priority: 100 6 | suffixes: 7 | - /README.md 8 | -------------------------------------------------------------------------------- /2014/think-like-a-rookie-while-practice-like-a-pro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 像外行一样思考,像专家一样实践(修订版) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 |
36 | 44 |

像外行一样思考,像专家一样实践(修订版)

45 |

图书信息

46 |
像外行一样思考,像专家一样实践(修订版)
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
项目信息
作者(日)金出武雄
出版社电子工业出版社
副标题科研成功之道
译者马金城 / 王国强
审校绝云
出版年2015-4
ISBN9787121250958
85 |

简介

86 |

这是14年回国后第一次参与翻译/审校的书. 原书初版在03年, 修订版这本13年在日本面世. 作者金出武雄是科研大拿, 曾任卡耐基梅隆大学机器人研究所所长. 全书围绕作者对科研/工作的方法论 “像外行一样思考, 像专家一样实践” 进行叙述, 通过科研现场丰富的例证给大家推广了这个论点.

87 |

相信这本书对科研 / 工作第一线的研究者或者工程师是有所裨益的.

88 |

链接

89 | 93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /2014/think-like-a-rookie-while-practice-like-a-pro.md: -------------------------------------------------------------------------------- 1 | 像外行一样思考,像专家一样实践(修订版) 2 | ================================ 3 | 4 | ## 图书信息 5 | 6 | ![像外行一样思考,像专家一样实践(修订版)](./think-like-a-rookie-while-practice-like-a-pro/cover.jpg) 7 | 8 | 项目 | 信息 9 | ----|---- 10 | 作者 | (日)金出武雄 11 | 出版社 | 电子工业出版社 12 | 副标题 | 科研成功之道 13 | 译者 | 马金城 / 王国强 14 | 审校 | 绝云 15 | 出版年 | 2015-4 16 | ISBN | 9787121250958 17 | 18 | ## 简介 19 | 20 | 这是14年回国后第一次参与翻译/审校的书. 原书初版在03年, 修订版这本13年在日本面世. 作者金出武雄是科研大拿, 曾任卡耐基梅隆大学机器人研究所所长. 全书围绕作者对科研/工作的方法论 "像外行一样思考, 像专家一样实践" 进行叙述, 通过科研现场丰富的例证给大家推广了这个论点. 21 | 22 | 相信这本书对科研 / 工作第一线的研究者或者工程师是有所裨益的. 23 | 24 | ## 链接 25 | 26 | * [电子工业出版社](http://www.phei.com.cn/module/goods/wssd_content.jsp?bookid=42142) 27 | * [豆瓣](http://book.douban.com/subject/26340523/) 28 | -------------------------------------------------------------------------------- /2014/think-like-a-rookie-while-practice-like-a-pro/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2014/think-like-a-rookie-while-practice-like-a-pro/cover.jpg -------------------------------------------------------------------------------- /2015/an-illustrated-brief-introduction-to-algorithm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 图解简单算法 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 |
36 | 44 |

图解简单算法

45 |

图书信息

46 |
图解简单算法写给大家看的算法书
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
项目信息
作者(日)杉浦賢
出版社电子工业出版社
译者绝云
出版年2016
73 |

简介

74 |

“产品经理也能读懂的算法书”

75 |

相比起《算法导论》这样的鸿篇巨制,《图解简单算法》用轻松诙谐的行文,图文并茂的排版解除大家对算法的恐惧心,非常亲和地为大家普及算法的基本概念和几种常用的简单算法。

76 |

链接

77 | 83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /2015/an-illustrated-brief-introduction-to-algorithm.md: -------------------------------------------------------------------------------- 1 | 图解简单算法 2 | ========== 3 | 4 | ## 图书信息 5 | 6 | ![~~图解简单算法~~写给大家看的算法书](./an-illustrated-brief-introduction-to-algorithm/cover.jpg) 7 | 8 | 项目 | 信息 9 | ----|---- 10 | 作者 | (日)杉浦賢 11 | 出版社 | 电子工业出版社 12 | 译者 | 绝云 13 | 出版年 | 2016 14 | 15 | ## 简介 16 | 17 | "产品经理也能读懂的算法书" 18 | 19 | 相比起《算法导论》这样的鸿篇巨制,《图解简单算法》用轻松诙谐的行文,图文并茂的排版解除大家对算法的恐惧心,非常亲和地为大家普及算法的基本概念和几种常用的简单算法。 20 | 21 | ## 链接 22 | 23 | * [amazon](http://www.amazon.co.jp/dp/4797370939) 24 | * [china-pub](http://product.china-pub.com/4959916) 25 | * [电子工业出版社](http://www.phei.com.cn/module/goods/wssd_content.jsp?bookid=46572) 26 | * [京东](http://search.jd.com/Search?keyword=%E5%86%99%E7%BB%99%E5%A4%A7%E5%AE%B6%E7%9C%8B%E7%9A%84%E7%AE%97%E6%B3%95%E4%B9%A6&enc=utf-8) 27 | 28 | -------------------------------------------------------------------------------- /2015/an-illustrated-brief-introduction-to-algorithm/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2015/an-illustrated-brief-introduction-to-algorithm/cover.jpg -------------------------------------------------------------------------------- /2015/cors/cors_server_flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2015/cors/cors_server_flowchart.png -------------------------------------------------------------------------------- /2015/decision-tree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 决策树算法 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 |
36 | 44 |

决策树算法

45 |

概述

46 |

决策树是一种实现分治策略的层次数据结构。[1]

47 |

举个 🌰 要不要去见相亲对象呢?

48 |
    A["年龄"] -->|>=30| no1("不见")
 49 |     A -->|<30| B["长相"]
 50 |     B -->|"丑"| no2("不见")
 51 |     B -->|"中等偏帅"| C["收入"]
 52 |     C -->|"<100k"| no3("不见")
 53 |     C -->|">=500"| yes1("见!💖")
 54 |     C -->|"(100k, 500k)"| D["程序🐵"]
 55 |     D -->|"是"| no4("什么鬼?")
 56 |     D -->|"否"| yes2("见!")
 57 | 
58 |

这就是一棵已经构造好的决策树,其中,每个矩形代表一个特征,每个圆角矩形代表一个类标签。输入数据后,根据不同的特征值过滤最终可以得到输入数据所属的类别。决策树算法的目标就是从一堆原始数据里构造这么一棵决策树,以作为预测、判断和决策的参考。

59 |

决策树算法可以用作分类(分类决策树)也可以用作回归(回归决策树),可以用于提取特征值、相关性分析、建立专家系统、搜索排序、邮件过滤等。

60 |

在2006年,国际数据挖掘社区推出的《数据挖掘十大算法》中评选出来的十大算法里有两个是决策树相关的算法(C4.5和CART),可见决策树算法算法在数据挖掘中应用的广泛程度。

61 |

ID3(Iterative Dichotomiser 3)

62 |
Ross Quinlan
63 |

ID3诞生于70年代末,由Ross Quinlan提出。这个算法倾向更小的树,并且越能带来熵减的决策节点离根节点越近。

64 |

ID3每次确定划分数据集S的特征时,会计算每一个未使用的属性,计算其[熵(Entropy)](https://en.wikipedia.org/wiki/Entropy_(information_theory)H(S),然后选取导致最小熵值的属性作为特征值,分割数据集S得到子数据集。这是一种贪心算法,并不保证得到一棵最小树(找最小树的算法是NP完全算法),只是准确度和效率的一个权衡的结果。然后对每一个子数据集进行同样的处理,并且在下列情况下停止递归处理:

65 | 70 |

熵值的计算方式为:

71 |
    H(S) = - \sum_{x \in X}p(x)log_{2}p(x)
 72 | 
73 | 78 |

用ID3构建一棵决策树的过程可以用下述伪代码来表示:

79 |
tree = {};
 80 | tree.root = ID3(S);
 81 | function ID3(S){
 82 |     IF 数据集S纯度达到标准或者符合其它终止条件 THEN 返回类标签
 83 |     ELSE
 84 |         计算所有特征的熵值
 85 |         取最小熵值对应特征划分数据集S
 86 |         创建分支节点B
 87 |             FOR 每个划分的子数据集S'
 88 |                 B.splitS' = ID3(S')
 89 |         RETURN 分支节点B
 90 | }
 91 | 
92 |

ID3可视化

93 |
<div id="visualize-id3">
 94 |     <nav class="toolbar">
 95 |         <div class="menu menu-horizontal u-1-2">
 96 |             <ul class="menu-list">
 97 |                 <li class="menu-item">
 98 |                     <span class="color-select menu-link" data-color="#33CCFF" style="background-color: #33CCFF;">&nbsp;</span>
 99 |                 </li>
100 |                 <li class="menu-item">
101 |                 <li class="menu-item">
102 |                     <span class="color-select menu-link" data-color="#009933" style="background-color: #009933;">&nbsp;</span>
103 |                 </li>
104 |                 <li class="menu-item">
105 |                     <span class="color-select menu-link" data-color="#FF6600" style="background-color: #FF6600;">&nbsp;</span>
106 |                 </li>
107 |                 <li class="menu-item">
108 |                     <span class="color-select menu-link" data-color="#FFC508" style="background-color: #FFC508;">&nbsp;</span>
109 |                 </li>
110 |                 <li class="menu-item">
111 |                     <span class="btn-clear menu-link">清空</span>
112 |                 </li>
113 |                 <li class="menu-item">
114 |                     <span class="menu-link">选择颜色并画点</span>
115 |                 </li>
116 |             </ul>
117 |         </div>
118 |     </nav>
119 |     <canvas class="training-canvas" width="450" height="450"></canvas>
120 |     <svg class="decision-tree" width="450" height="450"><g/></svg>
121 | </div>
122 | 
123 |
./decision-tree/main.css
124 | 
125 |
../lib/d3/d3.js
126 | ../lib/dagre-d3/dist/dagre-d3.js
127 | ./decision-tree/main.dist.js
128 | 
129 |

forked from lagodiuk/decision-tree-js

130 |

数据示例(x和y为特征列,color为预测分类)

131 |
{
132 |     x: 200,
133 |     y: 100,
134 |     color: '#FFC508'
135 | }
136 | 
137 |

但这个例子不是非常纯粹的ID3实现,它对整数值进行了离散化。譬如这里是根据取值把坐标轴的划分为两个区间,每个区间对应同一个特征值的两个取值范围(譬如y>=200和y<200)。这里借鉴了一部分C4.5处理连续值的办法。

138 |

C4.5(successor of ID3)

139 |

这个算法也是Ross Quinlan提出的,作为对ID3的改进。

140 |

算法过程与ID3算法在宏观上一致。

141 |

改进点:

142 |

选择特征列是通过信息增益率(而不是熵值)

143 |

其中,**信息增益(IG)**指的是计算某一个属性导致的熵值下降的幅度。其计算方法为:

144 |
    IG(S,a) = H(S) - H(S|a)
145 |             = H(S) - (
146 |                 \sum_{v \in vals(a)} \frac{|\{x \in S | x_{a} = v\}|}{|S|} \cdot
147 |                 H({x \in S|x_{a} = v})
148 |             )
149 | 
150 |

而,**信息增益率(IGR)**则是

151 |
    IGR(S,a) = IG(S,a)/IV(S,a)
152 | 
153 |

其中,IV(S,a)的定义为

154 |
    IV(S,a) = - \sum_{v \in vals(a)} \frac{|\{x \in S | x_{a} = v\}|}{|S|} \cdot
155 |             log_{2}(\frac{|\{x \in S | x_{a} = v\}|}{|S|})
156 | 
157 |

在树构造的过程中剪枝

158 |

所谓的剪枝,就是用节点的子树或者子叶子节点来替换这个节点,以得到更低的错误率。为解决ID3过度拟合的问题,C4.5的软件包实现了基于悲观剪枝(Pessimistic Pruning)方法的剪枝。

159 |

这个方法通过递归地计算目标节点分支的错误率来获得这个目标节点的错误率。例如,对于一个有N个实例和E个错误(也就是和该叶子节点类别不一致的实例)的叶子节点,用$(E + 0.5)/N$表示这个叶子节点的错误率。假设一个节点有L个子节点,这些子节点共有$\sum E$个错误和$\sum N$个实例,那么该节点的错误率为$(\sum E + 0.5xL)/\sum N$。假设这个节点被它的最佳子节点替代后,在训练集上得到的错误分类数为J,那么如果$(J + 0.5)$$(\sum E + 0.5xL)$的1标准差范围内,悲观剪枝法就采用这个子节点来替代该节点。

160 |

处理连续数据

161 |

选取多决策准则来产生分支。譬如上述可视化的实现中,对连续值x采取了这样的策略:

162 | 166 |

不过Quinlan论文中讲到的两个处理连续值的方法比这个复杂。第一个方法是对每一个属性用信息增益率选择阀值,第二个方法是用Risannen的最小描述长度原理(MDL)。

167 |

处理数据缺失

168 |

在Quinlan的文献中有很多处理数据缺失的议题,其中比较重点的是三个。这里简单介绍一下问题并且提几个典型的解法。

169 |

生成树分支的时候需要比较多个属性值,但有些属性在某些实例中没有值

170 | 174 |

选定属性后,没有该属性值的实例没有办法放入这个训练的任何输出中

175 | 180 |

构建好的树测试实例时有可能被测试用例缺失某个测试节点对应的属性值,这时如何让测试进行下去

181 | 187 |

扩展阅读

188 |

C5.0/See5

189 |

CART(Classification and Regression Tree)

190 |

和C4.5的不同在于:

191 | 196 |

多变量树

197 |

上面讨论的算法都是基于一个输入维度来划分数据的,而构造多变量树时每个决策节点都可以使用所有的输入维度,因此更加一般化。当然这个已经超出本文的讨论范围了

198 |

随机森林

199 |

GBDT

200 |

附录

201 |

参考资料

202 | 207 |
208 |
209 |
    210 |
  1. 《机器学习导论》,Ethem Alpaydin著 ↩︎

    211 |
  2. 212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 | 223 | 224 | 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /2015/decision-tree.md: -------------------------------------------------------------------------------- 1 | 决策树算法 2 | ========= 3 | 4 | ## 概述 5 | 6 | 决策树是一种实现分治策略的层次数据结构。[^footnote-introduction-to-machine-learning] 7 | 8 | 举个 :chestnut: 要不要去见相亲对象呢? 9 | 10 | ```graph-TB 11 | A["年龄"] -->|>=30| no1("不见") 12 | A -->|<30| B["长相"] 13 | B -->|"丑"| no2("不见") 14 | B -->|"中等偏帅"| C["收入"] 15 | C -->|"<100k"| no3("不见") 16 | C -->|">=500"| yes1("见!💖") 17 | C -->|"(100k, 500k)"| D["程序🐵"] 18 | D -->|"是"| no4("什么鬼?") 19 | D -->|"否"| yes2("见!") 20 | ``` 21 | 22 | 这就是一棵已经构造好的决策树,其中,每个矩形代表一个特征,每个圆角矩形代表一个类标签。输入数据后,根据不同的特征值过滤最终可以得到输入数据所属的类别。决策树算法的目标就是从一堆原始数据里构造这么一棵决策树,以作为预测、判断和决策的参考。 23 | 24 | 决策树算法可以用作分类(分类决策树)也可以用作回归(回归决策树),可以用于提取特征值、相关性分析、建立专家系统、搜索排序、邮件过滤等。 25 | 26 | 在2006年,国际数据挖掘社区推出的《数据挖掘十大算法》中评选出来的十大算法里有两个是决策树相关的算法(C4.5和CART),可见决策树算法算法在数据挖掘中应用的广泛程度。 27 | 28 | ## ID3(Iterative Dichotomiser 3) 29 | 30 | ![Ross Quinlan](./decision-tree/ross-quinlan.jpg) 31 | 32 | ID3诞生于70年代末,由[Ross Quinlan](http://www.rulequest.com/Personal/)提出。这个算法倾向更小的树,并且越能带来熵减的决策节点离根节点越近。 33 | 34 | ID3每次确定划分数据集S的特征时,会计算每一个未使用的属性,计算其[熵(Entropy)](https://en.wikipedia.org/wiki/Entropy_(information_theory)`H(S)`,然后选取导致最小熵值的属性作为特征值,分割数据集S得到子数据集。这是一种贪心算法,并不保证得到一棵最小树(找最小树的算法是NP完全算法),只是准确度和效率的一个权衡的结果。然后对每一个子数据集进行同样的处理,并且在下列情况下停止递归处理: 35 | 36 | - 子数据集所有元素归属同一类别,这时创建叶子节点并标记为该类别 37 | - 没有任何未使用的属性,这时创建叶子节点并标记为子数据集中出现次数最多的类别 38 | - 子数据集中没有元素,这时创建叶子节点,并标记为父数据集中出现最多的类别 39 | 40 | **熵值**的计算方式为: 41 | 42 | ```math 43 | H(S) = - \sum_{x \in X}p(x)log_{2}p(x) 44 | ``` 45 | 46 | - S: 当前数据集 47 | - X: S中的所有类别 48 | - p(x): 类别x在S中所占的比例 49 | 50 | 用ID3构建一棵决策树的过程可以用下述伪代码来表示: 51 | 52 | ``` 53 | tree = {}; 54 | tree.root = ID3(S); 55 | function ID3(S){ 56 | IF 数据集S纯度达到标准或者符合其它终止条件 THEN 返回类标签 57 | ELSE 58 | 计算所有特征的熵值 59 | 取最小熵值对应特征划分数据集S 60 | 创建分支节点B 61 | FOR 每个划分的子数据集S' 62 | B.splitS' = ID3(S') 63 | RETURN 分支节点B 64 | } 65 | ``` 66 | 67 | ### ID3可视化 68 | 69 | ```html- 70 |
71 | 96 | 97 | 98 |
99 | ``` 100 | 101 | ```link- 102 | ./decision-tree/main.css 103 | ``` 104 | 105 | ```script- 106 | ../lib/d3/d3.js 107 | ../lib/dagre-d3/dist/dagre-d3.js 108 | ./decision-tree/main.dist.js 109 | ``` 110 | 111 | forked from [lagodiuk/decision-tree-js](https://github.com/lagodiuk/decision-tree-js) 112 | 113 | 数据示例(x和y为特征列,color为预测分类) 114 | 115 | ``` 116 | { 117 | x: 200, 118 | y: 100, 119 | color: '#FFC508' 120 | } 121 | ``` 122 | 123 | 但这个例子不是非常纯粹的ID3实现,它对整数值进行了离散化。譬如这里是根据取值把坐标轴的划分为两个区间,每个区间对应同一个特征值的两个取值范围(譬如y>=200和y<200)。这里借鉴了一部分C4.5处理连续值的办法。 124 | 125 | ## C4.5(successor of ID3) 126 | 127 | 这个算法也是[Ross Quinlan](http://www.rulequest.com/Personal/)提出的,作为对ID3的改进。 128 | 129 | 算法过程与ID3算法在宏观上一致。 130 | 131 | 改进点: 132 | 133 | ### 选择特征列是通过信息增益率(而不是熵值) 134 | 135 | 其中,**[信息增益(IG)](https://en.wikipedia.org/wiki/Information_gain_in_decision_trees)**指的是计算某一个属性导致的熵值下降的幅度。其计算方法为: 136 | 137 | ```math 138 | IG(S,a) = H(S) - H(S|a) 139 | = H(S) - ( 140 | \sum_{v \in vals(a)} \frac{|\{x \in S | x_{a} = v\}|}{|S|} \cdot 141 | H({x \in S|x_{a} = v}) 142 | ) 143 | ``` 144 | 145 | 而,**[信息增益率(IGR)](https://en.wikipedia.org/wiki/Information_gain_ratio)**则是 146 | 147 | ```math 148 | IGR(S,a) = IG(S,a)/IV(S,a) 149 | ``` 150 | 151 | 其中,IV(S,a)的定义为 152 | 153 | ```math 154 | IV(S,a) = - \sum_{v \in vals(a)} \frac{|\{x \in S | x_{a} = v\}|}{|S|} \cdot 155 | log_{2}(\frac{|\{x \in S | x_{a} = v\}|}{|S|}) 156 | ``` 157 | 158 | ### 在树构造的过程中剪枝 159 | 160 | 所谓的剪枝,就是用节点的子树或者子叶子节点来替换这个节点,以得到更低的错误率。为解决ID3过度拟合的问题,C4.5的软件包实现了基于悲观剪枝(Pessimistic Pruning)方法的剪枝。 161 | 162 | 这个方法通过递归地计算目标节点分支的错误率来获得这个目标节点的错误率。例如,对于一个有N个实例和E个错误(也就是和该叶子节点类别不一致的实例)的叶子节点,用`$(E + 0.5)/N$`表示这个叶子节点的错误率。假设一个节点有L个子节点,这些子节点共有`$\sum E$`个错误和`$\sum N$`个实例,那么该节点的错误率为`$(\sum E + 0.5xL)/\sum N$`。假设这个节点被它的最佳子节点替代后,在训练集上得到的错误分类数为J,那么如果`$(J + 0.5)$`在`$(\sum E + 0.5xL)$`的1标准差范围内,悲观剪枝法就采用这个子节点来替代该节点。 163 | 164 | ### 处理连续数据 165 | 166 | 选取多决策准则来产生分支。譬如上述可视化的实现中,对连续值x采取了这样的策略: 167 | 168 | * 对每一个x的取值,把测试集分为(>= x)和(< x)两个分支 169 | * 对每一个分支计算信息增益 170 | 171 | 不过Quinlan论文中讲到的两个处理连续值的方法比这个复杂。第一个方法是对每一个属性用信息增益率选择阀值,第二个方法是用Risannen的最小描述长度原理(MDL)。 172 | 173 | ### 处理数据缺失 174 | 175 | 在Quinlan的文献中有很多处理数据缺失的议题,其中比较重点的是三个。这里简单介绍一下问题并且提几个典型的解法。 176 | 177 | #### 生成树分支的时候需要比较多个属性值,但有些属性在某些实例中没有值 178 | 179 | * 直接忽略实例 180 | * 填充属性值(常用值、均值或者最能带来信息增益的值等) 181 | 182 | #### 选定属性后,没有该属性值的实例没有办法放入这个训练的任何输出中 183 | 184 | * 直接忽略实例 185 | * 选取常用值 186 | * 切分用例放到每一个输出下 187 | 188 | #### 构建好的树测试实例时有可能被测试用例缺失某个测试节点对应的属性值,这时如何让测试进行下去 189 | 190 | * 如果对于缺失值有单独分支,走这个分支 191 | * 选取最常用分支走 192 | * 确定最可能的属性值,并填充这个属性值 193 | * 不再走余下分支测试,直接设置成最常用类别 194 | 195 | ## 扩展阅读 196 | 197 | ### C5.0/See5 198 | 199 | ### CART(Classification and Regression Tree) 200 | 201 | 和C4.5的不同在于: 202 | 203 | * CART算法可以不转换的情况下直接处理连续型和标称型数据 204 | * CART算法没有停止准则,树会一直生长到最大尺寸 205 | * CART算法的剪枝步骤采用的是代价复杂度(Cost-Complexity Pruning) 206 | 207 | ### 多变量树 208 | 209 | 上面讨论的算法都是基于一个输入维度来划分数据的,而构造**多变量树**时每个决策节点都可以使用所有的输入维度,因此更加一般化。当然这个已经超出本文的讨论范围了 210 | 211 | ### [随机森林](https://en.wikipedia.org/wiki/Random_forest) 212 | 213 | ### [GBDT](https://en.wikipedia.org/wiki/Gradient_boosting) 214 | 215 | ## 附录 216 | 217 | ### 参考资料 218 | 219 | * [University of Regina cs831: Knowledge Discovery in Databases](http://www2.cs.uregina.ca/~dbd/cs831/index.html) 220 | * [wiki: ID3_algorithm](https://en.wikipedia.org/wiki/ID3_algorithm) 221 | * [Top10 data mining algrithms](http://www.cs.umd.edu/~samir/498/10Algorithms-08.pdf) 222 | 223 | [^footnote-introduction-to-machine-learning]: 《机器学习导论》,Ethem Alpaydin著 224 | -------------------------------------------------------------------------------- /2015/decision-tree/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | ../../node_modules/.bin/browserify main.js > main.dist.js 3 | -------------------------------------------------------------------------------- /2015/decision-tree/id3.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @author: 绝云(wensen.lws) 3 | * @description: description 4 | */ 5 | 6 | /** 7 | * Creates an instance of DecisionTree 8 | * 9 | * @constructor 10 | * @param builder - contains training set and 11 | * some configuration parameters 12 | */ 13 | 14 | var lang = require('zero-lang'); 15 | var declare = require('zero-oop/declare'); 16 | 17 | var DecisionTree = declare({ 18 | constructor: function (builder) { 19 | this.root = buildDecisionTree({ 20 | trainingSet: builder.trainingSet, 21 | ignoredAttributes: arrayToHashSet(builder.ignoredAttributes), 22 | categoryAttr: builder.categoryAttr || 'category', 23 | minItemsCount: builder.minItemsCount || 1, 24 | entropyThrehold: builder.entropyThrehold || 0.01, 25 | maxTreeDepth: builder.maxTreeDepth || 70 26 | }); 27 | }, 28 | predict: function (item) { 29 | return predict(this.root, item); 30 | } 31 | }); 32 | 33 | /** 34 | * Transforming array to object with such attributes 35 | * as elements of array (afterwards it can be used as HashSet) 36 | */ 37 | function arrayToHashSet(array) { 38 | var hashSet = {}; 39 | if (array) { 40 | lang.each(array, function (item) { 41 | hashSet[item] = true; 42 | }); 43 | } 44 | return hashSet; 45 | } 46 | 47 | /** 48 | * Calculating how many objects have the same 49 | * values of specific attribute. 50 | * 51 | * @param items - array of objects 52 | * 53 | * @param attr - variable with name of attribute, 54 | * which embedded in each object 55 | */ 56 | function countUniqueValues(items, attr) { 57 | var counter = {}; 58 | 59 | // detecting different values of attribute 60 | lang.each(items, function (item) { 61 | counter[item[attr]] = 0; 62 | }); 63 | 64 | // counting number of occurrences of each of values 65 | // of attribute 66 | lang.each(items, function (item) { 67 | counter[item[attr]] += 1; 68 | }); 69 | return counter; 70 | } 71 | 72 | /** 73 | * Calculating entropy of array of objects 74 | * by specific attribute. 75 | * 76 | * @param items - array of objects 77 | * 78 | * @param attr - variable with name of attribute, 79 | * which embedded in each object 80 | */ 81 | function entropy(items, attr) { 82 | // counting number of occurrences of each of values 83 | // of attribute 84 | var counter = countUniqueValues(items, attr); 85 | 86 | var e = 0; 87 | var p; 88 | lang.forIn(counter, function (c) { 89 | p = c / items.length; 90 | e += -p * Math.log(p); 91 | }); 92 | return e; 93 | } 94 | 95 | /** 96 | * Splitting array of objects by value of specific attribute, 97 | * using specific predicate and pivot. 98 | * 99 | * Items which matched by predicate will be copied to 100 | * the new array called 'match', and the rest of the items 101 | * will be copied to array with name 'notMatch' 102 | * 103 | * @param items - array of objects 104 | * 105 | * @param attr - variable with name of attribute, 106 | * which embedded in each object 107 | * 108 | * @param predicate - function(x, y) 109 | * which returns 'true' or 'false' 110 | * 111 | * @param pivot - used as the second argument when 112 | * calling predicate function: 113 | * e.g. predicate(item[attr], pivot) 114 | */ 115 | function split(items, attr, predicate, pivot) { 116 | var match = []; 117 | var notMatch = []; 118 | 119 | var attrValue; 120 | 121 | lang.each(items, function (item) { 122 | attrValue = item[attr]; 123 | if (predicate(attrValue, pivot)) { 124 | match.push(item); 125 | } else { 126 | notMatch.push(item); 127 | } 128 | }); 129 | return { 130 | match: match, 131 | notMatch: notMatch 132 | }; 133 | } 134 | 135 | /** 136 | * Finding value of specific attribute which is most frequent 137 | * in given array of objects. 138 | * 139 | * @param items - array of objects 140 | * 141 | * @param attr - variable with name of attribute, 142 | * which embedded in each object 143 | */ 144 | function mostFrequentValue(items, attr) { 145 | // counting number of occurrences of each of values 146 | // of attribute 147 | var counter = countUniqueValues(items, attr); 148 | 149 | var mostFrequentCount = 0; 150 | var mostFrequentV; 151 | 152 | lang.forIn(counter, function (value, key) { 153 | if (value > mostFrequentCount) { 154 | mostFrequentCount = value; 155 | mostFrequentV = key; 156 | } 157 | }); 158 | return mostFrequentV; 159 | } 160 | 161 | var predicates = { 162 | '==': function (a, b) { 163 | return a == b; 164 | }, 165 | '>=': function (a, b) { 166 | return a >= b; 167 | } 168 | }; 169 | 170 | /** 171 | * Function for building decision tree 172 | */ 173 | function buildDecisionTree(builder) { 174 | var trainingSet = builder.trainingSet; 175 | var minItemsCount = builder.minItemsCount; 176 | var categoryAttr = builder.categoryAttr; 177 | var entropyThrehold = builder.entropyThrehold; 178 | var maxTreeDepth = builder.maxTreeDepth; 179 | var ignoredAttributes = builder.ignoredAttributes; 180 | 181 | if ((maxTreeDepth === 0) || (trainingSet.length <= minItemsCount)) { 182 | // restriction by maximal depth of tree 183 | // or size of training set is to small 184 | // so we have to terminate process of building tree 185 | return { 186 | category: mostFrequentValue(trainingSet, categoryAttr) 187 | }; 188 | } 189 | 190 | var initialEntropy = entropy(trainingSet, categoryAttr); 191 | 192 | if (initialEntropy <= entropyThrehold) { 193 | // entropy of training set too small 194 | // (it means that training set is almost homogeneous), 195 | // so we have to terminate process of building tree 196 | return { 197 | category: mostFrequentValue(trainingSet, categoryAttr) 198 | }; 199 | } 200 | 201 | // used as hash-set for avoiding the checking of split by rules 202 | // with the same 'attribute-predicate-pivot' more than once 203 | var alreadyChecked = {}; 204 | 205 | // this variable expected to contain rule, which splits training set 206 | // into subsets with smaller values of entropy (produces informational gain) 207 | var bestSplit = {gain: 0}; 208 | 209 | for (var i = trainingSet.length - 1; i >= 0; i--) { 210 | var item = trainingSet[i]; 211 | 212 | // iterating over all attributes of item 213 | for (var attr in item) { 214 | if ((attr == categoryAttr) || ignoredAttributes[attr]) { 215 | continue; 216 | } 217 | 218 | // let the value of current attribute be the pivot 219 | var pivot = item[attr]; 220 | 221 | // pick the predicate 222 | // depending on the type of the attribute value 223 | var predicateName; 224 | if (typeof pivot == 'number') { 225 | predicateName = '>='; 226 | } else { 227 | // there is no sense to compare non-numeric attributes 228 | // so we will check only equality of such attributes 229 | predicateName = '=='; 230 | } 231 | 232 | var attrPredPivot = attr + predicateName + pivot; 233 | if (alreadyChecked[attrPredPivot]) { 234 | // skip such pairs of 'attribute-predicate-pivot', 235 | // which been already checked 236 | continue; 237 | } 238 | alreadyChecked[attrPredPivot] = true; 239 | 240 | var predicate = predicates[predicateName]; 241 | 242 | // splitting training set by given 'attribute-predicate-value' 243 | var currSplit = split(trainingSet, attr, predicate, pivot); 244 | 245 | // calculating entropy of subsets 246 | var matchEntropy = entropy(currSplit.match, categoryAttr); 247 | var notMatchEntropy = entropy(currSplit.notMatch, categoryAttr); 248 | 249 | // calculating informational gain 250 | var newEntropy = 0; 251 | newEntropy += matchEntropy * currSplit.match.length; 252 | newEntropy += notMatchEntropy * currSplit.notMatch.length; 253 | newEntropy /= trainingSet.length; 254 | var currGain = initialEntropy - newEntropy; 255 | 256 | if (currGain > bestSplit.gain) { 257 | // remember pairs 'attribute-predicate-value' 258 | // which provides informational gain 259 | bestSplit = currSplit; 260 | bestSplit.predicateName = predicateName; 261 | bestSplit.predicate = predicate; 262 | bestSplit.attribute = attr; 263 | bestSplit.pivot = pivot; 264 | bestSplit.gain = currGain; 265 | } 266 | } 267 | } 268 | 269 | if (!bestSplit.gain) { 270 | // can't find optimal split 271 | return {category: mostFrequentValue(trainingSet, categoryAttr)}; 272 | } 273 | 274 | // building subtrees 275 | 276 | builder.maxTreeDepth = maxTreeDepth - 1; 277 | 278 | builder.trainingSet = bestSplit.match; 279 | var matchSubTree = buildDecisionTree(builder); 280 | 281 | builder.trainingSet = bestSplit.notMatch; 282 | var notMatchSubTree = buildDecisionTree(builder); 283 | 284 | return { 285 | attribute: bestSplit.attribute, 286 | predicate: bestSplit.predicate, 287 | predicateName: bestSplit.predicateName, 288 | pivot: bestSplit.pivot, 289 | match: matchSubTree, 290 | notMatch: notMatchSubTree, 291 | matchedCount: bestSplit.match.length, 292 | notMatchedCount: bestSplit.notMatch.length 293 | }; 294 | } 295 | 296 | /** 297 | * Classifying item, using decision tree 298 | */ 299 | function predict(tree, item) { 300 | var attr, 301 | value, 302 | predicate, 303 | pivot; 304 | 305 | // Traversing tree from the root to leaf 306 | while (true) { 307 | if (tree.category) { 308 | // only leafs contains predicted category 309 | return tree.category; 310 | } 311 | 312 | attr = tree.attribute; 313 | value = item[attr]; 314 | 315 | predicate = tree.predicate; 316 | pivot = tree.pivot; 317 | 318 | // move to one of subtrees 319 | if (predicate(value, pivot)) { 320 | tree = tree.match; 321 | } else { 322 | tree = tree.notMatch; 323 | } 324 | } 325 | } 326 | 327 | module.exports = DecisionTree; 328 | -------------------------------------------------------------------------------- /2015/decision-tree/main.css: -------------------------------------------------------------------------------- 1 | /*visualizeID3*/ 2 | 3 | #visualize-id3 { 4 | width: 910px; 5 | margin: 0 auto; 6 | -webkit-user-select: none; /* Chrome all / Safari all */ 7 | -moz-user-select: none; /* Firefox all */ 8 | -ms-user-select: none; /* IE 10+ */ 9 | user-select: none; /* Likely future */ 10 | } 11 | 12 | #visualize-id3 .menu-list { 13 | list-style: none; 14 | padding-left: 0; 15 | cursor: pointer; 16 | } 17 | 18 | #visualize-id3 .menu-item { 19 | list-style: none; 20 | display: inline-block; 21 | } 22 | #visualize-id3 .menu-link { 23 | padding: .5em 1em; 24 | } 25 | #visualize-id3 canvas { 26 | border: 1px solid #f7f7f7; 27 | cursor: crosshair; 28 | } 29 | 30 | #visualize-id3 svg { 31 | background-color: #f7f7f7; 32 | cursor: move; 33 | } 34 | -------------------------------------------------------------------------------- /2015/decision-tree/main.js: -------------------------------------------------------------------------------- 1 | require('./visualizeID3'); 2 | -------------------------------------------------------------------------------- /2015/decision-tree/ross-quinlan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2015/decision-tree/ross-quinlan.jpg -------------------------------------------------------------------------------- /2015/decision-tree/visualizeID3.js: -------------------------------------------------------------------------------- 1 | var arrayUtils = require('zero-lang/array'); 2 | var Color = require('zero-colors/Color'); 3 | var domData = require('zero-dom/data'); 4 | var domEvent = require('zero-dom/event'); 5 | var domQuery = require('zero-dom/query'); 6 | var domStyle = require('zero-dom/style'); 7 | var sprintf = require('zero-fmt/sprintf'); 8 | var uuid = require('zero-crypto/uuid'); 9 | 10 | var DecisionTree = require('./id3'); 11 | 12 | var appContainer = domQuery.one('#visualize-id3'); 13 | var canvas = domQuery.one('.training-canvas', appContainer), 14 | context = canvas.getContext('2d'); 15 | 16 | var selectedColorElement; 17 | var selectedColor; 18 | function selectColorElement(element) { 19 | if (selectedColorElement) { 20 | domStyle.set(selectedColorElement, 'border', ''); 21 | } 22 | selectedColorElement = element; 23 | domStyle.set(element, 'border', '2px solid grey'); 24 | selectedColor = new Color(domData.get(element, 'color')); 25 | } 26 | selectColorElement(domQuery.one('.color-select', appContainer)); 27 | 28 | var points = []; 29 | 30 | function clearCanvas() { 31 | context.clearRect(0, 0, canvas.width, canvas.height); 32 | points = []; 33 | initGraph(); 34 | } 35 | 36 | function drawCircle(x, y, radius, color) { 37 | context.beginPath(); 38 | context.arc(x, y, radius, 0, 2 * Math.PI, false); 39 | 40 | context.fillStyle = color; 41 | context.fill(); 42 | context.closePath(); 43 | context.stroke(); 44 | } 45 | 46 | var POINT_RADIUS = 3; 47 | function addPoint(e) { 48 | var x = e.offsetX ? e.offsetX : (e.layerX - canvas.offsetLeft); 49 | var y = e.offsetY ? e.offsetY : (e.layerY - canvas.offsetTop); 50 | 51 | drawCircle(x, y, POINT_RADIUS, selectedColor.toCss()); 52 | points.push({ 53 | x: x, 54 | y: y, 55 | color: selectedColor.toCss() 56 | }); 57 | } 58 | 59 | var tree = null; 60 | function rebuildDecisionTree() { 61 | if (points.length === 0) { 62 | return; 63 | } 64 | var threshold = Math.floor(points.length / 100); 65 | threshold = (threshold > 1) ? threshold : 1; 66 | tree = new DecisionTree({ 67 | trainingSet: points, 68 | categoryAttr: 'color', 69 | minItemsCount: threshold 70 | }); 71 | 72 | showTreePredictions(); 73 | showPoints(); 74 | showDecisionTree(); 75 | } 76 | 77 | var MAX_ALPHA = 128; 78 | function putPixel(imageData, width, x, y, color, alpha) { 79 | var c = new Color(color); 80 | var indx = (y * width + x) * 4; 81 | 82 | var currAlpha = imageData.data[indx + 3]; 83 | 84 | imageData.data[indx + 0] = (c.r * alpha + imageData.data[indx + 0] * currAlpha) / 85 | (alpha + currAlpha); 86 | imageData.data[indx + 1] = (c.g * alpha + imageData.data[indx + 1] * currAlpha) / 87 | (alpha + currAlpha); 88 | imageData.data[indx + 2] = (c.b * alpha + imageData.data[indx + 2] * currAlpha) / 89 | (alpha + currAlpha); 90 | imageData.data[indx + 3] = alpha + currAlpha; 91 | } 92 | function showTreePredictions() { 93 | var width = canvas.width; 94 | var height = canvas.height; 95 | context.clearRect(0, 0, width, height); 96 | var imageData = context.getImageData(0, 0, width, height); 97 | 98 | for (var x = 0; x < width; x++) { 99 | for (var y = 0; y < height; y++) { 100 | var predictedColor = tree.predict({ 101 | x: x, 102 | y: y 103 | }); 104 | putPixel(imageData, width, x, y, predictedColor, MAX_ALPHA); 105 | } 106 | } 107 | 108 | context.putImageData(imageData, 0, 0); 109 | } 110 | function showPoints() { 111 | arrayUtils.each(points, function (p) { 112 | drawCircle(p.x, p.y, POINT_RADIUS, p.color); 113 | }); 114 | } 115 | 116 | var svg = d3.select('#visualize-id3 svg'); 117 | var inner = svg.select('g'); 118 | var zoom = d3.behavior.zoom().on('zoom', function () { 119 | inner.attr('transform', 'translate(' + d3.event.translate + ')' + 120 | 'scale(' + d3.event.scale + ')'); 121 | }); 122 | var render = new dagreD3.render(); 123 | var g; 124 | function initGraph() { 125 | g = new dagreD3.graphlib.Graph(); 126 | svg.call(zoom); 127 | g.setGraph({ 128 | nodesep: 30, 129 | ranksep: 150, 130 | rankdir: 'TB', 131 | marginx: 10, 132 | marginy: 10 133 | }); 134 | inner.call(render, g); 135 | } 136 | function addGraphElements(branch, parentId, edge) { 137 | if (!branch) { 138 | return; 139 | } 140 | var branchId = uuid(); 141 | var label = branch.category ? 142 | sprintf( 143 | '%s', 144 | branch.category, branch.category 145 | ) : 146 | sprintf('%s %s %s?', branch.attribute, branch.predicateName, branch.pivot); 147 | g.setNode(branchId, { 148 | id: branchId, 149 | labelType: 'html', 150 | label: label, 151 | rx: 5, 152 | ry: 5, 153 | padding: 5, 154 | }); 155 | if (parentId && edge) { 156 | g.setEdge(parentId, branchId, { 157 | label: edge, 158 | width: 40, 159 | lineInterpolate: 'basis', 160 | style: 'fill:none;', 161 | }); 162 | } 163 | if (branch.match) { 164 | addGraphElements( 165 | branch.match, 166 | branchId, 167 | sprintf('yes(%d)', branch.matchedCount) 168 | ); 169 | } 170 | if (branch.notMatch) { 171 | addGraphElements( 172 | branch.notMatch, 173 | branchId, 174 | sprintf('no(%d)', branch.notMatchedCount) 175 | ); 176 | } 177 | } 178 | function showDecisionTree() { 179 | if (!tree || !tree.root) { 180 | return; 181 | } 182 | initGraph(); 183 | 184 | addGraphElements(tree.root); 185 | inner.call(render, g); 186 | // Zoom and scale to fit 187 | var zoomScale = zoom.scale(); 188 | var graphWidth = g.graph().width; 189 | var graphHeight = g.graph().height; 190 | var width = parseInt(domStyle.get(svg, 'width')); 191 | var height = parseInt(domStyle.get(svg, 'height')); 192 | zoomScale = Math.min(width / graphWidth, height / graphHeight); 193 | var translate = [ 194 | (width / 2) - ((graphWidth * zoomScale) / 2), 195 | (height / 2) - ((graphHeight * zoomScale) / 2) 196 | ]; 197 | zoom.translate(translate); 198 | zoom.scale(zoomScale); 199 | zoom.event(svg); 200 | } 201 | 202 | var isLeftKeyHolding; 203 | domEvent.on(appContainer, 'click', '.color-select', function (e) { 204 | selectColorElement(e.delegateTarget); 205 | }); 206 | domEvent.on(appContainer, 'click', '.btn-clear', function () { 207 | clearCanvas(); 208 | }); 209 | domEvent.on(canvas, 'mousedown', function (e) { 210 | isLeftKeyHolding = true; 211 | addPoint(e); 212 | }); 213 | domEvent.on(canvas, 'mouseup', function () { 214 | if (isLeftKeyHolding) { 215 | isLeftKeyHolding = false; 216 | rebuildDecisionTree(); 217 | } 218 | }); 219 | domEvent.on(canvas, 'mouseout', function () { 220 | if (isLeftKeyHolding) { 221 | isLeftKeyHolding = false; 222 | rebuildDecisionTree(); 223 | } 224 | }); 225 | domEvent.on(canvas, 'mousemove', function (e) { 226 | if (isLeftKeyHolding) { 227 | addPoint(e); 228 | } 229 | }); 230 | -------------------------------------------------------------------------------- /2015/fixing-latex-in-mac.md: -------------------------------------------------------------------------------- 1 | Mac下使用latex遇到的问题以及解法 2 | ============================= 3 | 4 | ## 字体 5 | 6 | ```shell 7 | The font "[SIMKAI.TTF]" cannot be found. 8 | ``` 9 | 10 | * 安装MacTex以及相关包; 11 | * 文档里有以下几个设置: 12 | 13 | ```tex 14 | % !TEX program = XeLaTeX 15 | % !TEX encoding = UTF-8 16 | \documentclass[UTF8,nofonts]{ctexart} % 关键是nofonts参数,保证编译文章时不会去找奇怪的win字体 17 | 18 | % 把字体设置成mac预装好的字体 { 19 | \setCJKmainfont[BoldFont=STHeiti,ItalicFont=STKaiti]{STSong} 20 | \setCJKsansfont[BoldFont=STHeiti]{STXihei} 21 | \setCJKmonofont{STFangsong} 22 | % } 23 | 24 | \begin{document} 25 | 文章内容。 26 | \end{document} 27 | ``` 28 | 29 | ## 文档类型设置 30 | 31 | 直接按照以下最佳实践编写文档 32 | 33 | ### 书籍 34 | 35 | ```tex 36 | %!TEX program = XeLaTeX 37 | %!TEX encoding = UTF-8 Unicode 38 | \documentclass[cs4size,a4paper,fancyhdr,fntef,UTF8,nofonts,hyperref]{ctexbook} %文档类别 39 | %Packages { 40 | %latex 宏包 { 41 | \usepackage[top=1in,bottom=1in,left=1.25in,right=1.25in]{geometry} 42 | %\usepackage{algorithmic} 43 | \usepackage{amsmath} %math! 44 | \usepackage{float} 45 | \usepackage{fontspec} 46 | \usepackage{hyperref} 47 | \usepackage{indentfirst} 48 | \usepackage{ifthen} 49 | \usepackage{makeidx} 50 | \usepackage{paralist} %行内列表 51 | \usepackage{pgffor} 52 | %\usepackage{zhspacing} 53 | %} 54 | %自定义宏包 { 55 | \usepackage{package/translation} 56 | \usepackage{package/variable} 57 | %} 58 | %} 59 | %settrings { 60 | %字体 { 61 | \setCJKmainfont[BoldFont=STHeiti,ItalicFont=STKaiti]{STSong} 62 | \setCJKsansfont[BoldFont=STHeiti]{STXihei} 63 | \setCJKmonofont{STFangsong} 64 | %\zhspacing{} 65 | %} 66 | %样式 { 67 | %\pagestyle{plain} %在页脚正中显示页码 68 | \pagestyle{fancy} 69 | %} 70 | %信息 { 71 | \author{\varauthor} 72 | \title{\vartitle\\\varsubtitle} 73 | \date{} 74 | %} 75 | %} 76 | %制作index { 77 | \makeindex 78 | %} 79 | \begin{document} 80 | \frontmatter{} %封面相关 81 | %封面 { 82 | %\include{cover} 83 | %} 84 | %作者 profile { 85 | %\include{author} 86 | %} 87 | %序言 { 88 | %\include{preface} 89 | %} 90 | %目录 { 91 | \begin{small} % 使用小字体 92 | \tableofcontents % 不能加{},否则会输出空目录 93 | \end{small} 94 | %} 95 | \mainmatter{} %正文 96 | %章节 { 97 | \begin{normalsize} 98 | 章节内容 99 | \end{normalsize} 100 | %} 101 | \backmatter{} %背页 102 | %版权页 { 103 | %\include{copyright} 104 | %} 105 | %使用 index { 106 | \appendix 107 | \printindex 108 | %} 109 | \end{document} 110 | ``` 111 | 112 | ### 论文、其它文章 113 | 114 | ```tex 115 | %!TEX program = XeLaTeX 116 | %!TEX encoding = UTF-8 Unicode 117 | \documentclass[UTF8,nofonts]{ctexart} % 文档类别 118 | % Packages { 119 | % latex 宏包 { 120 | \usepackage[top=1in,bottom=1in,left=1.25in,right=1.25in]{geometry} 121 | \usepackage{float} 122 | \usepackage{fontspec} 123 | \usepackage{hyperref} 124 | \usepackage{indentfirst} 125 | \usepackage{makeidx} 126 | %\usepackage{zhspacing} 127 | \usepackage{multicol} 128 | \usepackage{multirow} 129 | %} 130 | %} 131 | % settings { 132 | % 字体 { 133 | \setCJKmainfont[BoldFont=STHeiti,ItalicFont=STKaiti]{STSong} 134 | \setCJKsansfont[BoldFont=STHeiti]{STXihei} 135 | \setCJKmonofont{STFangsong} 136 | %\zhspacing{} 137 | %} 138 | % 样式 { 139 | \pagestyle{plain} % 在页脚正中显示页码 140 | %} 141 | \title{出版许可协议书} 142 | \author{} 143 | \date{} 144 | %} 145 | % 制作index { 146 | \makeindex 147 | %} 148 | \begin{document} 149 | \maketitle 150 | \renewcommand{\contentsname}{目录} 151 | \tableofcontents 152 | 文章内容 153 | \end{document} 154 | ``` 155 | -------------------------------------------------------------------------------- /2015/frontend-visual-modeling.md: -------------------------------------------------------------------------------- 1 | 前端可视化建模技术概览 2 | =================== 3 | 4 | ## 前言 5 | 6 | 建模是计算机世界一个恒久的主题。根本的需求来源于“图形化展示数据、逻辑”。这个需求下我们有了ER图、流程图、UML图、BPMN图等标准,也诞生了很多经典的桌面图形建模应用,譬如[visio][visio]、[Rational Rose][rational-rose]、[yEd][yed]、[XMind][xmind]等等。 7 | 8 | 单页面应用已经不是新鲜词汇,而利用html5开发离线应用、native应用的技术方案也越来越流行。因而在前端做类似的可视化建模的需求和解决方案也越来越多。举个离我们比较近的例子:ACP里就用到流程图表示工作流程和状态。 9 | 10 | ```viz-dot 11 | digraph validatingFlow { 12 | rankdir="LR"; 13 | a[label="开始"]; 14 | b[label="审批中"]; 15 | c[label="已审批"]; 16 | d[label="已配置"]; 17 | e[label="结束"]; 18 | a->b 19 | b->c 20 | c->d 21 | d->e 22 | } 23 | ``` 24 | 25 | 当然,具体产品线里有更复杂的例子。譬如我们团队的[PAI][pai]使用DAG来描述数据挖掘的过程。 26 | 27 | ![PAI](./frontend-visual-modeling/pai.png) 28 | 29 | 下面和大家分享一下前端可视化建模方面的需求和技术方案。 30 | 31 | ## 需求 32 | 33 | 可视化建模最核心的需求就是画一个图形学上的[图][wiki-graph]。要么是根据用户给定的数据结构展示成可视化的图形(用svg、canvas、html+css或者混用);要么是经过用户和系统的一系列交互,画出可视化图形后,可以生成相应的数据结构。 34 | 35 | 即实现如下图的可视化建模系统。 36 | 37 | ```viz-dot 38 | digraph modeling { 39 | labelloc="t" 40 | label="前端可视化建模系统" 41 | 42 | a[label="数据结构"]; 43 | b[label="可视化图形"]; 44 | c[label="用户交互"]; 45 | 46 | a -> b [label="渲染"]; 47 | b -> c 48 | b -> a [label="转换"]; 49 | c -> b 50 | } 51 | ``` 52 | 53 | ## 分析 54 | 55 | ### 图素 56 | 57 | 上面的“前端可视化建模系统”就是一个典型的可视化图形。这个最最基础的图里包含以下基本图素: 58 | 59 | * 顶点(node/vertex) 60 | * 边(link/edge) 61 | 62 | 顶点表示描述的实体,边表示实体之间的关联。通过顶点和边的组合,就可以形成一个有意义的模型(图)。 63 | 64 | 从前端实现上可能还要抽象出来三个基本的要素: 65 | 66 | * 画布(canvas/graph) 67 | * 标签(label) 68 | * 连接桩(port) 69 | 70 | ### 渲染工具 71 | 72 | 前面提过,前端做可视化图形可以用svg、canvas、html+css或者混用。其中,svg(或者混用html)是在这个可视化细分领域最常用的技术。因为svg里的Shapes刚好对应图里的顶点,而Paths可以对应图里的边。从实现上,svg里也有g元素可以实现画布、分组;text元素可以实现标签。甚至可以通过foreignObject内嵌html来实现复杂的顶点样式定制。事实上,上文的图正是用svg画出来的。目前应用svg实现前端建模的产品、框架很多,譬如: 73 | 74 | * [IBM的开源项目Node-RED][node-red] 75 | * [图形布局库dagre-d3][dagre-d3] 76 | * [图形布局库cola.js][cola-js] 77 | * [开源画图库Joint][joint] 78 | * [开源画图库jsPlumb][jsPlumb] 79 | * [开源框架AlloyUI的画图工具][alloy-ui-diagramming] 80 | * [商业画图库mxGraph][mxGraph] 81 | * [商业画图库Draw2D][draw2d] 82 | * [微软的机器学习平台上的建模工具Azure-ML][azure-ml] 83 | 84 | 使用canvas的相对少一些,比较出名的有: 85 | 86 | * [GoJS][gojs] 87 | * [JS Graph][js-graph] 88 | * [Springy][springy] 89 | 90 | ## 技术方案 91 | 92 | 如果只使用最基础的svg、canvas,不借助画图库的情况下,实现可视化建模是一件相当复杂的事情。这就是为什么上述列举的前端建模产品或者工具库除了Node-RED和AlloyUI暂未商业化以外,要么是闭源的(Azure-ML/mxGraph/Draw2D/GoJS/JS Graph),要么只是某个商用协议产品的社区开源版(Joint/jsPlumb),要么已经不维护了(dagre-d3)。 93 | 94 | 下面介绍几个在实现可视化建模时可供使用或者借鉴的项目。 95 | 96 | ### mxGraph 97 | 98 | 这个商业产品是上述提到的可视化建模产品里最强大的一个。从05年立项至今,这个库开发时间已有十年。而它的前身JGraph立项时间更早,是2000年。虽然开发模式落后(还是绑定全局变量的方式)、体积庞大,但mxGraph的设计、功能、文档各个方面都难以挑剔。前端可视化建模的标杆作品[draw.io][draw-io]以及中文作图社区[ProcessOn][process-on]都是基于这个库的。基本上目前mxGraph能做到的,就是前端可视化建模能做到的。 99 | 100 | [demo: folding](https://jgraph.github.io/mxgraph/javascript/examples/folding.html)。 101 | 102 | ### Joint 103 | 104 | Joint用jQuery维护dom,用lodash辅助计算以及渲染模版,用Backbone的Model和Events定义实体和暴露接口,并且自己实现了一套SVG渲染引擎。算得上是组合型的库。对Backbone比较熟悉的同学使用Joint上手会比较快。Joint自定义节点(使用模版)非常方便,动画相关的功能也很强大。另外,Joint还可以和布局库dagre配合使用,实现自动布局。 105 | 106 | 相比起mxGraph而言,Joint有几个设计或者实现上的问题: 107 | 108 | 1. 兼容性做得不够,IE9-不支持,并且在firefox低版本上有些问题 109 | 2. 连接桩(port)是作为节点(node)的附属,要实现深层定制比较麻烦 110 | 3. 没有做图层管理,节点的上下关系只能通过渲染顺序来决定 111 | 4. 常用的缩放、画布拖拽、自动布局、交互式画图等功能都没有内置,需要自行编写(除非使用商业版) 112 | 113 | Joint作为rapid的社区版(开源版本)功能并不全面,很多时候要真正应用在项目里需要进行深入的定制。另外,其维护者对github上的issue响应速度很慢,有时候bug report也没有回应。 114 | 115 | 即便如此,Joint也算是可视化建模的开源库里最灵活、设计最优秀的库了。 116 | 117 | [demo: petri nets](http://www.jointjs.com/demos/pn)。 118 | 119 | ### jsPlumb 120 | 121 | jsPlumb采用的是svg和html混排的做法,把所有节点都是html,所有连线都是单独的svg节点包裹的path元素。这么做的好处是主要是可以兼容低版本浏览器,并且节点可以充分利用css进行定制。缺点也很明显,首先文档结构散乱,很难导出、转换,其次画出来的图总有莫名的违和感,感觉是像素图形和矢量图形生硬地放到了一起,再次,一旦css在js之后加载完成,jsPlumb的图就崩溃了,而jsPlumb的css也是有侵入性的。 122 | 123 | [demo: state machine](https://jsplumbtoolkit.com/community/demo/statemachine/index.html)。 124 | 125 | ### Alloy-UI diagrams-builder 126 | 127 | 这个建模工具只建议在技术栈为YUI、并且建模需求简单时选用。Alloy-UI的设计和jsPlumb差不多,都是svg和html混排的形式。 128 | 129 | ## 总结 130 | 131 | ### 常用前端可视化建模工具对比 132 | 133 | ![radar](./frontend-visual-modeling/radar.png) 134 | 135 | 以上雷达图对比的是比较成规模的,可以独立完成可视化建模的工具库。 136 | 137 | ### 选型建议 138 | 139 | 具体做技术选型时有这几个建议: 140 | 141 | * 个人项目可以尝试优秀的商业解决方案,体会这些强大的产品的设计 142 | * 如果只有简单的模型展示功能,建议选用dagre-d3、Springy这样的,带自动布局、带渲染器的简单方案 143 | * 如果有交互式建模的需求,但又不求深入定制,那可以根据开发者熟悉的技术栈选择Joint/jsPlumb/Alloy-UI等方案之一 144 | * 如果有深入定制建模工具的需求,而且预算充足,建议和[ProcessOn][process-on]一样,选择mxGraph。如果同等条件又偏好canvas,则可以考虑[GoJS][gojs] 145 | * 有深入定制建模工具的需求,又不允许使用商业产品,那么只剩下以下选择 146 | * 基于Joint做二次开发 147 | * 基于D3、raphael、svg.nap等实现一个可视化建模工具 148 | * 从0开始,实现一个可视化建模工具 149 | 150 | [alloy-ui-diagramming]: https://github.com/liferay/alloy-ui/tree/master/src/aui-diagram-builder 151 | [azure-ml]: https://studio.azureml.net 152 | [cola-js]: http://marvl.infotech.monash.edu/webcola/ 153 | [dagre-d3]: https://github.com/cpettitt/dagre-d3 154 | [draw-io]: http://draw.io 155 | [draw2d]: http://www.draw2d.org/draw2d/home/index.html 156 | [gojs]: http://www.nwoods.com/products/gojs/index.html 157 | [joint]: https://github.com/clientIO/joint 158 | [js-graph]: http://www.js-graph.com/ 159 | [jsPlumb]: https://github.com/sporritt/jsPlumb 160 | [mxGraph]: http://www.jgraph.com/javascript-graph-visualization-library.html 161 | [node-red]: http://nodered.org/ 162 | [pai]: http://pai.yushanfang.com/ 163 | [process-on]: https://www.processon.com/diagrams 164 | [rational-rose]: http://www.ibm.com/software/products/en/ratirosefami/ 165 | [springy]: http://getspringy.com/ 166 | [visio]: http://www.microsoftstore.com.cn/%E7%B1%BB%E5%88%AB/%E8%BD%AF%E4%BB%B6%E4%B8%8E%E6%9C%8D%E5%8A%A1/c/software 167 | [wiki-graph]: https://en.wikipedia.org/wiki/Graph_(abstract_data_type) 168 | [xmind]: http://www.xmind.net/ 169 | [yed]: http://www.yworks.com/products/yed 170 | -------------------------------------------------------------------------------- /2015/frontend-visual-modeling/pai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2015/frontend-visual-modeling/pai.png -------------------------------------------------------------------------------- /2015/frontend-visual-modeling/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2015/frontend-visual-modeling/radar.png -------------------------------------------------------------------------------- /2015/lets-make-a-compiler.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 自制编译器 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 |
36 | 44 |

自制编译器

45 |

图书信息

46 |
自制编译器
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
项目信息
作者(日)青木峰郎
出版社人民邮电出版社
译者阿逸 / 绝云
出版年2016
73 |

简介

74 |

我负责这本书后半部分的翻译. 作者在书中讲述如何基于javacc开发一个类C语言的编译器. 这是一本深入浅出并且操作性强的技术书. 作者青木峰郎是日本编程语言界的泰斗级人物.

75 |

链接

76 | 81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /2015/lets-make-a-compiler.md: -------------------------------------------------------------------------------- 1 | 自制编译器 2 | ========= 3 | 4 | ## 图书信息 5 | 6 | ![自制编译器](./lets-make-a-compiler/cover.jpg) 7 | 8 | 项目 | 信息 9 | ----|---- 10 | 作者 | (日)青木峰郎 11 | 出版社 | 人民邮电出版社 12 | 译者 | 阿逸 / 绝云 13 | 出版年 | 2016 14 | 15 | ## 简介 16 | 17 | 我负责这本书后半部分的翻译. 作者在书中讲述如何基于javacc开发一个类C语言的编译器. 这是一本深入浅出并且操作性强的技术书. 作者青木峰郎是日本编程语言界的泰斗级人物. 18 | 19 | ## 链接 20 | 21 | * [图灵社区](http://www.ituring.com.cn/book/1308) 22 | * [china-pub](http://product.china-pub.com/4960509) 23 | * [豆瓣](http://book.douban.com/subject/4117971) 24 | 25 | -------------------------------------------------------------------------------- /2015/lets-make-a-compiler/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2015/lets-make-a-compiler/cover.jpg -------------------------------------------------------------------------------- /2016/70-math-quizs-for-programmers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 程序员的算法趣题 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 |
36 | 44 |

程序员的算法趣题

45 |

图书信息

46 |
程序员的算法趣题
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
项目信息
作者(日)増井敏克
出版社人民邮电出版社
译者绝云
出版年2016
73 |

简介

74 |

源自日本著名程序员刷题网站CodeIQ,通过70道贴近生活的数学趣题,向读者普及了相当精辟的算法理论和编程技巧。

75 |

链接

76 | 80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /2016/70-math-quizs-for-programmers.md: -------------------------------------------------------------------------------- 1 | 程序员的算法趣题 2 | ============== 3 | 4 | ## 图书信息 5 | 6 | ![程序员的算法趣题](./70-math-quizs-for-programmers/cover.jpg) 7 | 8 | 项目 | 信息 9 | ----|---- 10 | 作者 | (日)増井敏克 11 | 出版社 | 人民邮电出版社 12 | 译者 | 绝云 13 | 出版年 | 2016 14 | 15 | ## 简介 16 | 17 | 源自日本著名程序员刷题网站CodeIQ,通过70道贴近生活的数学趣题,向读者普及了相当精辟的算法理论和编程技巧。 18 | 19 | ## 链接 20 | 21 | * [图灵社区](http://www.ituring.com.cn/book/1814) 22 | * [amazon(原书)](https://www.amazon.co.jp/70/dp/479814245X) 23 | -------------------------------------------------------------------------------- /2016/70-math-quizs-for-programmers/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/70-math-quizs-for-programmers/cover.jpg -------------------------------------------------------------------------------- /2016/from-icon-fonts-to-svg-icons.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 从icon fonts到SVG icons 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 |
36 | 44 |

从icon fonts到SVG icons

45 |

作为一个前端开发,在做项目,尤其是做个人项目的时候,使用icon fonts这件事常常让我感到很挫败。因为通常一个icon fonts库无法涵盖项目所有的图标需求,而混用不同的icon fonts库会带来进一步的问题:有时候你发现命名空间有冲突,有时候你发现两个库的图标padding不一致,从而有一堆修修补补的事情要做。如果项目需要用到彩色的图标,或者要基于图标做一些复杂的动画效果,又要引入SVG或者gif了。

46 |

要解决这些问题,统一用SVG icons是一个可行的办法。当然,用icon fonts还是SVG icons这个话题太大,不在本文讨论之列。有兴趣的同学可以看看这些文章。

47 | 52 |

我自己总结了一下,如果不考虑浏览器兼容性的话,SVG icons从易用性/可维护性/表现力等各方面都比传统的icon fonts更有优势。从根本上说,一个是矢量文字(font),一个是表现矢量图形的XML(SVG),有点降维打击的意思。

53 |

不过有个问题,现存的大部分开源图标库都是icon fonts的,包括影响力巨大的FontAwesome项目。相对而言,SVG icons方案可用的开源资源并不多。于是一个想法自然而言地诞生:能不能把现有的icon fonts直接转换成SVG icons?如果可以的话,从icon fonts升级到SVG icons的过程就非常平滑了。

54 |

最直接的办法是从icon fonts图标库里的SVG font文件(一般的icon fonts库都会带的一个SVG font文件,譬如FontAwesome的fontawesome-webfont.svg。如果没有,也可以很简单地从ttf文件转换得到:ttf2svg)入手转换。

55 |

得到这个SVG font文件之后,转换成可用的SVG文件就很简单了。仔细看看这个SVG font文件,会发现每个图标就是内部定义的一个glyph元素,这个元素内部就是一段SVG。取出这些glyph元素,我们就得到了一堆可独立使用的SVG片段。这里只需要注意一点:SVG font里的glyph的坐标系和SVG内嵌到HTML内时的坐标系是不一样的。glyph和普通的文字一样,左下角是坐标轴原点,而内嵌的SVG则和Canvas一样,左上角是坐标轴原点。所以第一步转换后还要在每个SVG片段最外层加一个用于坐标转换的<g>节点。

56 |
<!-- 原始的glyph元素 -->
 57 | <glyph unicode="xxx"><!-- Outline of xxx glyph --></glyph>
 58 | 
 59 | <!-- 转换后的SVG片段 -->
 60 | <svg xmlns="http://www.w3.org/2000/svg">
 61 |   <g transform="scale(1, -1)">
 62 |     <!-- Outline of xxx glyph -->
 63 |   </g>
 64 | </svg>
 65 | 
66 |

其中transform="scale(1, -1)"就是负责转换坐标轴的关键。至此,我们已经从一个传统的icon fonts图标库里提取出可用的SVG icon了,似乎就可以拿这些SVG icon合并成SVG sprite直接使用了?

67 |

还是不行。首先你会发现简单粗暴的坐标变换(y轴反转)会导致图标矢量在显示的时候是偏离中心线的,所以作为inline图标内嵌到HTML里会有问题。为解决这个问题,可以在得到的SVG片段上再加一个坐标偏移的transform。

68 |
<svg xmlns="http://www.w3.org/2000/svg">
 69 |   <g transform="scale(1, -1) translate(0 -${iconHeight})">
 70 |     <!-- Outline of xxx glyph -->
 71 |   </g>
 72 | </svg>
 73 | 
74 |

这里的-${iconHeight}就是矢量图形的高度,对应原本的SVG fonts文件中的<font-face>节点的units-per-em值。具体细节上的调整不少,关键点还是在坐标转换上。

75 |

解决了这个问题之后基本上和原来使用icon fonts的体验差不多了,还顺带解决了命名冲突/表现力等各方面的问题。不过慢慢地你会发现原来icon fonts方案的一个致命问题没有解决:如果两个图标分别来自两个不同的图标库,padding等还是有不统一的问题(怎么同一行同样样式的两个图标看着大小不一样?)!而且现在你手里的SVG片段全部都包了一层用来做坐标转换的<g>元素,怎么看怎么别扭。

76 |

先解决第二个问题,把这层碍眼的<g>元素干掉。幸好这件事不用太操心,已经有人做掉了。用svgo这个库就可以把这些杂七杂八的坐标转换干掉,还你一个清爽的SVG片段。

77 |

第一个问题有点棘手。最理想的结果是,我们把所有来自不同icon fonts库得到的SVG片段都清理一遍,去掉所有的padding,只留下表示矢量图形的片段,和一个viewBox属性标示这个矢量图形的实际宽高。这样只要给每个SVG片段设置同样的widthheight属性,就可以得到统一的视觉效果了。

78 |

根据前面的经验,我们只要设置恰当的transform把整个矢量图形移动到其边缘和两条坐标轴相切,剩下的事情就可以交给svgo了。关键就在于,我们怎么知道目前矢量图形偏离两个坐标轴多远(top和left)?

79 |

图标的SVG片段我们有了。通过遍历这个SVG片段内部的各种图形(Rect, Path, etc)和它们的各种属性,计算出与两个坐标轴的最短距离就能得到top和left的值。不过这件事相当难,举个例子,如果矢量图形里有个曲线,那计算起来真的是要了命了。另外,要得到正确的结果就一定要遍历所有的情况。而如果依靠穷举来做,最终代码维护一定是个深坑。

80 |

那么,最笨的办法是什么呢?创建一个SVG文件把这个SVG片段写进去,打开浏览器,打开调试控制台,看矢量图形部分的top和left属性。然后编辑这个文件,transform里加上translate=(-${left}, -${top})。返回浏览器刷新,我们得到了想要的结果。

81 |

好了,思路有了。既然浏览器能做,那直接拿一个无头浏览器也可以做,然后就可以脚本化、自动化了。经过试验,PhantomJSElectron都符合要求。一旦祭出这个终极方案,前面很多工作都可以省略了,譬如解释SVG font文件,计算坐标转换的各种参数等。

82 |

最终从icon fonts得到SVG icons的整个流程可以描述如下:由icon fonts库得到SVG fonts文件(可能要转换),然后抽取各个glyph片段,翻转坐标系得到SVG片段,用无头浏览器把矢量图形对齐到坐标轴,用svgo优化输出。

83 |

然后就可以享用SVG icon了。内嵌到页面/sprite/做动画/加彩色/更好的渲染效果…新世界的大门已经向你打开。

84 |

这个事情说起来逻辑还算简单清晰,似乎没有什么特别难的地方。不过说到底,icon fonts是一套标准,SVG fonts又是一套标准,SVG symbol/sprites又是不同的标准。在标准的转换之间需要特别严谨,兼容各种开源icon fonts库,兼容它们背后各种不同的设计风格等等又是另一堆问题。还有个比较尴尬的点:一旦转换的图标数量上来了,性能就成了不得不考虑的问题:每处理一个图标就要开一个无头浏览器进程,每次优化完还要重新所有图标处理一遍。不做任何优化的话,NodeJS进程是会挂掉的(弱爆了)。

85 |

好消息是,我已经把整个流程自动化了。绝大多数的坑也已经填好。还准备了10k+个从开源图标库里转换过来的SVG icons。如果你还在犹豫要不要从icon fonts转为SVG icons,那么你最后的借口已经没了。项目地址:svg-icon。目前项目在重构当中,欢迎各种issue和pull request。

86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /2016/from-icon-fonts-to-svg-icons.md: -------------------------------------------------------------------------------- 1 | # 从icon fonts到SVG icons 2 | 3 | 作为一个前端开发,在做项目,尤其是做个人项目的时候,使用[icon fonts](https://www.w3.org/WAI/GL/wiki/Icon_Font_with_an_On-Screen_Text_Alternative)这件事常常让我感到很挫败。因为通常一个icon fonts库无法涵盖项目所有的图标需求,而混用不同的icon fonts库会带来进一步的问题:有时候你发现命名空间有冲突,有时候你发现两个库的图标padding不一致,从而有一堆修修补补的事情要做。如果项目需要用到彩色的图标,或者要基于图标做一些复杂的动画效果,又要引入SVG或者gif了。 4 | 5 | 要解决这些问题,统一用SVG icons是一个可行的办法。当然,用icon fonts还是SVG icons这个话题太大,不在本文讨论之列。有兴趣的同学可以看看这些文章。 6 | 7 | * [icon fonts vs SVG](https://css-tricks.com/icon-fonts-vs-svg/) 8 | * [icon fonts vs SVG debate](https://www.sitepoint.com/icon-fonts-vs-svg-debate/) 9 | * [why and how I am using SVG over fonts for icons](https://medium.com/@webprolific/why-and-how-i-m-using-svg-over-fonts-for-icons-7241dab890f0#.oskcbcmfi) 10 | 11 | 我自己总结了一下,如果不考虑浏览器兼容性的话,SVG icons从易用性/可维护性/表现力等各方面都比传统的icon fonts更有优势。从根本上说,一个是矢量文字(font),一个是表现矢量图形的XML(SVG),有点降维打击的意思。 12 | 13 | 不过有个问题,现存的大部分开源图标库都是icon fonts的,包括影响力巨大的FontAwesome项目。相对而言,SVG icons方案可用的开源资源并不多。于是一个想法自然而言地诞生:能不能把现有的icon fonts直接转换成SVG icons?如果可以的话,从icon fonts升级到SVG icons的过程就非常平滑了。 14 | 15 | 最直接的办法是从icon fonts图标库里的[SVG font](https://www.w3.org/TR/SVG/fonts.html)文件(一般的icon fonts库都会带的一个SVG font文件,譬如FontAwesome的[fontawesome-webfont.svg](https://github.com/FortAwesome/Font-Awesome/blob/master/fonts/fontawesome-webfont.svg)。如果没有,也可以很简单地从ttf文件转换得到:[ttf2svg](https://github.com/qdsang/ttf2svg))入手转换。 16 | 17 | 得到这个SVG font文件之后,转换成可用的SVG文件就很简单了。仔细看看这个SVG font文件,会发现每个图标就是内部定义的一个glyph元素,这个元素内部就是一段SVG。取出这些glyph元素,我们就得到了一堆可独立使用的SVG片段。这里只需要注意一点:SVG font里的glyph的坐标系和SVG内嵌到HTML内时的坐标系是不一样的。glyph和普通的文字一样,左下角是坐标轴原点,而内嵌的SVG则和Canvas一样,左上角是坐标轴原点。所以第一步转换后还要在每个SVG片段最外层加一个用于坐标转换的``节点。 18 | 19 | ```xml 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ``` 30 | 31 | 其中`transform="scale(1, -1)"`就是负责转换坐标轴的关键。至此,我们已经从一个传统的icon fonts图标库里提取出可用的SVG icon了,似乎就可以拿这些SVG icon合并成[SVG sprite](https://css-tricks.com/svg-sprites-use-better-icon-fonts/)直接使用了? 32 | 33 | 还是不行。首先你会发现简单粗暴的坐标变换(y轴反转)会导致图标矢量在显示的时候是偏离中心线的,所以作为inline图标内嵌到HTML里会有问题。为解决这个问题,可以在得到的SVG片段上再加一个坐标偏移的transform。 34 | 35 | ```xml 36 | 37 | 38 | 39 | 40 | 41 | ``` 42 | 43 | 这里的`-${iconHeight}`就是矢量图形的高度,对应原本的SVG fonts文件中的``节点的`units-per-em`值。具体细节上的调整不少,关键点还是在坐标转换上。 44 | 45 | 解决了这个问题之后基本上和原来使用icon fonts的体验差不多了,还顺带解决了命名冲突/表现力等各方面的问题。不过慢慢地你会发现原来icon fonts方案的一个致命问题没有解决:如果两个图标分别来自两个不同的图标库,padding等还是有不统一的问题(怎么同一行同样样式的两个图标看着大小不一样?)!而且现在你手里的SVG片段全部都包了一层用来做坐标转换的``元素,怎么看怎么别扭。 46 | 47 | 先解决第二个问题,把这层碍眼的``元素干掉。幸好这件事不用太操心,已经有人做掉了。用[svgo](https://github.com/svg/svgo)这个库就可以把这些杂七杂八的坐标转换干掉,还你一个清爽的SVG片段。 48 | 49 | 第一个问题有点棘手。最理想的结果是,我们把所有来自不同icon fonts库得到的SVG片段都清理一遍,去掉所有的padding,只留下表示矢量图形的片段,和一个`viewBox`属性标示这个矢量图形的实际宽高。这样只要给每个SVG片段设置同样的`width`和`height`属性,就可以得到统一的视觉效果了。 50 | 51 | 根据前面的经验,我们只要设置恰当的`transform`把整个矢量图形移动到其边缘和两条坐标轴相切,剩下的事情就可以交给svgo了。关键就在于,我们怎么知道目前矢量图形偏离两个坐标轴多远(top和left)? 52 | 53 | 图标的SVG片段我们有了。通过遍历这个SVG片段内部的各种图形(Rect, Path, etc)和它们的各种属性,计算出与两个坐标轴的最短距离就能得到top和left的值。不过这件事相当难,举个例子,如果矢量图形里有个曲线,那计算起来真的是要了命了。另外,要得到正确的结果就一定要遍历所有的情况。而如果依靠穷举来做,最终代码维护一定是个深坑。 54 | 55 | 那么,最笨的办法是什么呢?创建一个SVG文件把这个SVG片段写进去,打开浏览器,打开调试控制台,看矢量图形部分的top和left属性。然后编辑这个文件,`transform`里加上`translate=(-${left}, -${top})`。返回浏览器刷新,我们得到了想要的结果。 56 | 57 | 好了,思路有了。既然浏览器能做,那直接拿一个无头浏览器也可以做,然后就可以脚本化、自动化了。经过试验,[PhantomJS](http://phantomjs.org/)和[Electron](https://github.com/electron/electron)都符合要求。一旦祭出这个终极方案,前面很多工作都可以省略了,譬如解释SVG font文件,计算坐标转换的各种参数等。 58 | 59 | 最终从icon fonts得到SVG icons的整个流程可以描述如下:由icon fonts库得到SVG fonts文件(可能要转换),然后抽取各个glyph片段,翻转坐标系得到SVG片段,用无头浏览器把矢量图形对齐到坐标轴,用svgo优化输出。 60 | 61 | 然后就可以享用SVG icon了。内嵌到页面/sprite/做动画/加彩色/更好的渲染效果......新世界的大门已经向你打开。 62 | 63 | 这个事情说起来逻辑还算简单清晰,似乎没有什么特别难的地方。不过说到底,icon fonts是一套标准,SVG fonts又是一套标准,SVG symbol/sprites又是不同的标准。在标准的转换之间需要特别严谨,兼容各种开源icon fonts库,兼容它们背后各种不同的设计风格等等又是另一堆问题。还有个比较尴尬的点:一旦转换的图标数量上来了,性能就成了不得不考虑的问题:每处理一个图标就要开一个无头浏览器进程,每次优化完还要重新所有图标处理一遍。不做任何优化的话,NodeJS进程是会挂掉的(弱爆了)。 64 | 65 | 好消息是,我已经把整个流程自动化了。绝大多数的坑也已经填好。还准备了10k+个从开源图标库里转换过来的SVG icons。如果你还在犹豫要不要从icon fonts转为SVG icons,那么你最后的借口已经没了。项目地址:[svg-icon](https://github.com/leungwensen/svg-icon)。目前项目在重构当中,欢迎各种issue和pull request。 66 | 67 | -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc.md: -------------------------------------------------------------------------------- 1 | 从SocialCalc到EtherCalc 2 | ======================= 3 | 4 | [EtherCalc](https://ethercalc.net/)是一个在线电子表格系统,它专注于在线协作编辑,使用SocialCalc作为浏览器端的电子表格引擎。SocialCalc由Dan Bricklin(电子表格的发明人)设计,是Socialtext平台的一部分。Socialtext是面向商业用户的协同工作平台。 5 | 6 | 对Socialtext团队而言,2006年开发SocialCalc的主要目标是性能。主要的考虑是,虽然客户端的JavaScript运算会比服务器端的Perl运算慢一个量级,但总体而言所消耗的时间会比AJAX往返所需的网络延迟要少得多。 7 | 8 | ![图2.1 WikiCalc和SocialCalc的性能模型。自2009年起,JavaScript运行时的改进已经把50毫秒降到了10毫秒以下](./from-socialcalc-to-ethercalc/wikicalc-socialcalc.png) 9 | 10 | SocialCalc把所有运算都放到浏览器上进行,服务器存在的意义仅仅是加载和保存电子表格。在[开源应用架构](http://aosabook.org/en/index.html)最后一章的[SocialCalc](http://blog.leungwensen.com/2016/socialcalc.md)部分,我们介绍了如何基于简单的、类聊天室的架构来实现电子表格的多人协作编辑。 11 | 12 | ![图2.2 多用户SocialCalc](./from-socialcalc-to-ethercalc/multiplayer-socialcalc.png) 13 | 14 | 不过,当我们开始在生产环境中测试时,我们发现这个系统有些性能和扩展性上的不足,这驱使我们对系统进行了一些重构以获得可接受的性能。在本文,我们会阐明如何实现新的架构,如何使用性能测试工具,以及如何实现新的工具以解决性能问题。 15 | 16 | 17 | ## 设计约束 18 | 19 | Socialtext平台同时有着带防火墙部署和云端部署的特性,这使得EtherCalc在资源和性能要求方面有着独特的约束。 20 | 21 | 在编写本文的时间节点,Socialtext基于VMWare的vSphere内部部署时,需要双核CPU和4GB内存。云端部署时,一个典型的Amazon EC2实例大概会提供上述配置的两倍,也就是4核CPU和7.5GB内存。 22 | 23 | 带防火墙部署意味着我们不能像多租户的托管系统(譬如DocVerse,后来是Google Docs的一部分)一样,把问题抛给硬件。我们只能承担一定量的服务器容量。 24 | 25 | 相比起内部部署,云端实例提供更好的容量和按需扩展特性,但通常网络连接会更慢,并且因为频繁的断开连接和重连接会困扰用户。 26 | 27 | 因此,下述资源约束影响了EtherCalc的架构设计: 28 | 29 | ### 内存 30 | 31 | 基于事件的服务器使得我们可以用很小的内存承载成千上万的并发连接。 32 | 33 | ### CPU 34 | 35 | 遵循SocialCalc的原始设计,我们把大部分的运算和内容渲染都移交客户端JavaScript。 36 | 37 | ### 网络 38 | 39 | 通过只传输操作信息,而不传输电子表格内容,我们大幅削减了带宽占用,并且提供在不稳定网络连接下的恢复功能。 40 | 41 | 42 | ## 最初原型 43 | 44 | 一开始我们通过[Feersum](https://metacpan.org/release/Feersum)用Perl 5实现了一个WebSocket服务器。Freesum是Socialtext开发的一个基于[libev](http://software.schmorp.de/pkg/libev.html)的非阻塞网络服务器。它非常快,能用单个CPU提供10,000连接每秒的并发能力。在Freesum上,我们使用[PocketIO](https://metacpan.org/release/PocketIO)中间件来响应JavaScript的Socket.io客户端。Socket.io能在不支持WebSocket的浏览器上提供向后兼容。 45 | 46 | 最初的原型酷似聊天服务器。每一个协作会话都是一个聊天室,客户端会把本地执行的命令和光标位置移动发送到服务器,服务器会把这些信息同步到同一个聊天室里的所有客户端。 47 | 48 | 下图描述了一个典型的操作流程。 49 | 50 | ![图2.3 有快照功能的原型服务器](./from-socialcalc-to-ethercalc/flow-snapshot.png) 51 | 52 | 这个方案解决了新客户端加入带来的CPU损耗问题,但带来了网络性能的问题,因为它依靠每个客户端上行的带宽。如果网络连接比较慢,会影响客户端后续上传的命令。 53 | 54 | 另外,这个方案下服务器也不能检查从客户端上传的快照的一致性。因此出错或者恶意的快照将会影响所有新加入的客户端,使得新客户端不能和已有的客户端状态同步。 55 | 56 | 聪明的读者这时应该能看出来,这两个问题都是因为服务器无法执行电子表格命令造成的。如果服务器能根据接收到的命令更新自己的状态,那它甚至不需要维护一个命令的日志备份。 57 | 58 | 浏览器中的SocialCalc引擎是用JavaScript编写的,我们曾尝试把这套逻辑转译到Perl中去,但这样做会带来维护两份代码的成本。我们也尝试过嵌入JS引擎([V8](https://metacpan.org/release/JavaScript-V8),[SpiderMonkey](https://metacpan.org/release/JavaScript-SpiderMonkey)),但在Feersum的事件循环之上运行时,会带来新的性能问题。 59 | 60 | 最终,在2011年8月,我们决定用Node.js重写服务器。 61 | 62 | ## 移植到Node.js 63 | 64 | 最初的重写比较顺利,因为Feersum和Node.js都基于同样的libev事件模型,并且PocketIO的API和Socket.io非常相近。我们只花了一个下午的时间,写了80行代码就得到了一个功能相当的服务器。这里感谢[ZappaJS](http://zappajs.com/)提供的简洁的API。 65 | 66 | 最初的[微型性能评估](http://c9s.github.com/BenchmarkTest/)显示,移植到Node.js后我们大约损失了最大吞吐量的一半。在2011年一个典型的因特尔i5内核CPU上,原来基于Feersum的方案每秒能处理5000个请求,而Node.js的Express每秒最多只能处理2800个。 67 | 68 | 这个性能损失在第一版JavaScript实现上我们是可以接受的,因为这个方案并不会很显著地提高用户操作延迟,并且我们确信它的性能会随着时间推移而上升。 69 | 70 | 接下来,我们继续减少客户端的CPU占用,并且通过用服务器版本的SocialCalc电子表格跟踪每个会话的状态来最小化带宽占用。 71 | 72 | ![图2.4 使用Node.js服务器管理电子表格状态](./from-socialcalc-to-ethercalc/flow-nodejs.png) 73 | 74 | 75 | ## 服务器版的SocialCalc 76 | 77 | 解决问题的关键技术是[jsdom](https://github.com/tmpvar/jsdom),一个W3C文档对象模型的完全实现,它令Node.js可以在模拟的浏览器环境中加载客户端JavaScript库。 78 | 79 | 使用了jsdom之后,我们可以随意创建任意数目的服务器端SocialCalc电子表格,每个表格隔离在单独的沙盒里,只占去大约30KB的内存: 80 | 81 | ```javascript 82 | require! <[ vm jsdom ]> 83 | create-spreadsheet = -> 84 | document = jsdom.jsdom \ 85 | sandbox = vm.createContext window: document.createWindow! <<< { 86 | setTimeout, clearTimeout, alert: console.log 87 | } 88 | vm.runInContext """ 89 | #packed-SocialCalc-js-code 90 | window.ss = new SocialCalc.SpreadsheetControl 91 | """ sandbox 92 | ``` 93 | 94 | 每个协作会话对应一个沙盒中的SocialCalc控制器,在上面执行所有获取的命令。然后服务器会把这个控制器的状态广播给所有新加入的客户端,顺带完全消除了记录操作日志的必要。 95 | 96 | 因为对性能评估结果很满意,我们开发了一个基于Redis的持久化层,然后对外发布了[EtherCalc.org](http://ethercalc.org/)公开测试版本。接下来的六个月内,它一直保持着良好的可扩展性,零故障完美地处理了百万级别的电子表格操作。 97 | 98 | 在2012年4月,在OSDC.tw会议上分享了EtherCalc后,我受Trend Micro邀请参加了他们的黑客马拉松,把EtherCalc改造成他们实时网络流量监控系统的可编程的可视化引擎。 99 | 100 | 针对他们的需求,我们创建了REST API,用于支持对单个单元格的GET/PUT操作,以及直接对电子表格实例本身POST操作命令。在黑客马拉松期间,新上线的REST处理模块每秒接受几百次调用,在感受不到延迟或者内存溢出的情况下在浏览器中更新着图表和公式单元格的内容。 101 | 102 | 不过,在最后一天的demo里,当我们把流量数据传入EtherCalc并在浏览器电子表格中输入公式后,服务器忽然锁死,冻结了所有活跃的连接。我们重启了Node.js进程,但很快服务器仍然用掉了100%CPU,并且锁死。 103 | 104 | 我们被吓坏了,赶紧换用更小的数据集,然后系统终于正确运行,最终我们顺利完成demo。不过我想知道的是,到底是什么导致了服务器死锁? 105 | 106 | 107 | ## Node.js性能探查 108 | 109 | 为了弄清楚CPU资源的去向,我们需要一个性能探查工具。 110 | 111 | 得益于卓绝的[NYTProf](https://metacpan.org/module/Devel::NYTProf)探查工具,对最初的Perl原型做性能探查非常直观,能看到每个函数、每一行、每个操作码和每个代码块的执行时间信息,并带有[调用栈的可视化图表](https://metacpan.org/module/nytprofcg)和HTML报告。配合NYTProf,我们还使用Perl内置的[DTrace支持](https://metacpan.org/module/perldtrace),跟踪长时间运行的进程,实时捕获开始和结束函数调用的统计信息。 112 | 113 | 相比而言,Node.js的性能探查工具可谓差强人意。直到编辑本文时,DTrace支持还局限于[基于illumos系统](https://nodejs.org/en/blog/uncategorized/profiling-node-js/)的32位模式。因此我们大多数情况下使用[Node Webkit Agent](https://github.com/c4milo/node-webkit-agent),尽管它只提供函数级别的统计信息,但有一个可以访问的探查界面。 114 | 115 | 一个典型的探查会话如下: 116 | 117 | ```shell 118 | # "lsc"是LiveScript编译器 119 | # 加载WebKit agent, 然后运行app.js: 120 | lsc -r webkit-devtools-agent -er ./app.js 121 | # 在另一个terminal标签页,启动探查工具: 122 | killall -USR2 node 123 | # 在Webkit内核的浏览器打开这个URL,开始探查: 124 | open http://tinyurl.com/node0-8-agent 125 | ``` 126 | 127 | 为重现重后台负荷,我们用Apache的性能评估工具[ab](http://httpd.apache.org/docs/trunk/programs/ab.html)模拟了高并发的REST API调用。为模拟浏览器端操作,譬如移动光标、更新公式等,我们使用[Zombie.js](http://zombie.labnotes.org/),并且用jsdom和Node.js构建了一个无头浏览器。 128 | 129 | 讽刺的是,我们最后发现性能的瓶颈在jsdom本身。 130 | 131 | ![图2.5 探查工具截图(使用jsdom)](./from-socialcalc-to-ethercalc/profiler-jsdom.png) 132 | 133 | 从图2.5可以看到,占用CPU资源最多的是`RenderSheet`。每次服务器接到一个命令,就要花几毫秒的时间去重回单元格中的`innerHTML`,从而展现每个命令的执行结果。 134 | 135 | 因为所有jsdom代码都在单独的线程中执行,后续的REST API调用就会阻塞到前一个命令渲染完成。在高并发的场景下,巨大的等待队列就触发了潜在的bug,从而造成服务器死锁。 136 | 137 | 我们检查堆占用后发现,所有渲染结果都没有被引用,因为我们本来就不需要在服务器端实时展示HTML。唯一会访问到这些渲染结果的是HTML导出API,而这个场景我们也可以通过内存中的电子表格结构随时重现每个单元格的`innerHTML`。 138 | 139 | 为此,我们把`RenderSheet`函数中的jsdom移除了,取而代之的是一个最小化的[只用liveScript20行代码实现](https://github.com/audreyt/ethercalc/commit/fc62c0eb#L1R97)的DOM,用于支持HTML导出。然后我们重新运行了性能探查工具(见图2.6)。 140 | 141 | ![图2.6 更新后的探查工具截图(不使用jsdom)](./from-socialcalc-to-ethercalc/profiler-no-jsdom.png) 142 | 143 | 改进非常明显!吞吐量提高了4倍,HTML导出快了20倍,并且服务器再也没有出现死锁。 144 | 145 | 146 | ## 多核扩展 147 | 148 | 经过这次改进,我们终于敢于把EtherCalc整合到Socialtext平台中去,支持wiki页面和电子表格风格的多人协作编辑。 149 | 150 | 为保证生产环境的响应时间,我们部署了反向代理的nginx服务器,利用它的`limit_req`指令提高API调用的并发数目。这个技术在带防火墙部署和专用实例部署场景下的表现都令人满意。 151 | 152 | Socialtext为中小商业客户准备了第三种部署方式,那就是多租户托管。一个独立的大的服务器上支持着35,000个公司,每个公司平均100个左右的用户。 153 | 154 | 在多租户的场景,所有客户在调用REST API时都有着同样的速率限制。这就使得每个客户端都存在每秒大约5个请求的并发约束。之前一节提到过,这个限制的存在是因为Node.js的所有运算只用到了单个CPU。 155 | 156 | ![图2.7 事件服务器(单核)](./from-socialcalc-to-ethercalc/scaling-evented.png) 157 | 158 | 有没有办法利用上多租户服务器上所有的备用CPU? 159 | 160 | 对于其它在多核服务器上的Node.js服务,我们利用预分叉的[cluster服务器](https://www.npmjs.com/package/cluster-server)在每个CPU上创建一个进程。 161 | 162 | ![图2.7 事件服务器(多核)](./from-socialcalc-to-ethercalc/scaling-cluster.png) 163 | 164 | 然而EtherCalc本身已经因为Redis而具备了多服务器扩展能力,如果在单台服务器上实现[Socket.io](https://github.com/socketio/socket.io)集群和[RedisStore](http://stackoverflow.com/questions/5739357/how-to-reuse-redis-connection-in-socket-io/5749667#5749667),这会大大增加逻辑复杂性,并使得调试更加困难。 165 | 166 | 另外,如果所有集群中的进程都绑定到CPU处理,后续的连接还是会被阻塞。 167 | 168 | 我们并没有采取预分叉固定数目的进程,而是寻求创建一个后台线程、然后把执行命令的工作分配到所有CPU核心上的办法。 169 | 170 | ![图2.8 事件线程服务器(多核)](./from-socialcalc-to-ethercalc/scaling-threads.png) 171 | 172 | 针对我们的目标,W3C的[Web Worker](http://www.w3.org/TR/workers/)API是一个完美的解决方案。它是为浏览器设计的,定义了在后台独立执行脚本的方法。这就使得费时的任务可以持续执行,而不影响主线程的响应。 173 | 174 | 因此我们创建了[webworker-threads](https://github.com/audreyt/node-webworker-threads)项目,在Node.js上实现了跨平台的Web Worker API。 175 | 176 | 使用webworker-threads,创建新的SocialCalc线程以及线程间通信变得非常直观: 177 | 178 | ```javascript 179 | { Worker } = require \webworker-threads 180 | w = new Worker \packed-SocialCalc.js 181 | w.onmessage = (event) -> ... 182 | w.postMessage command 183 | ``` 184 | 185 | 这个方案可谓两全其美:既可以按需为EtherCalc增加CPU,又把占用资源极少的后台进程保持在单CPU环境下。 186 | 187 | 188 | ## 收获 189 | 190 | ### 带着枷锁好跳舞 191 | 192 | Fred Brooks在他的著作*《设计原本》*中说到,约束可以压缩设计者的检索空间,从而帮助他专注下来、加速设计进程。这其中包括自我强加的约束: 193 | 194 | > 在设计任务中的人为约束有很好的特质,就是我们可以自由地解除。理想情况下,这些约束可以把人驱驰到设计领域的处女地,提高人的创造力。 195 | 196 | 在EtherCalc的开发中,这样的人为约束非常关键,可以帮助EtherCalc在几次迭代后保持*概念完整性*。 197 | 198 | 举个例子,一个看上去可行的方案是,我们可以为三种服务器类型(带防火墙,云端,多租户托管)实现三种不同的并发架构。然而,这样的过早优化会严重影响系统的概念完整性。 199 | 200 | 相反,我专注于让EtherCalc可以在各种资源需求下都能有不错的性能。因此优化CPU使用、降低内存占用和减少带宽占用是同时进行的。带来的结果就是,因为内存占用在100MB以下,我们甚至可以在类似Raspberry Pi这样的嵌入式平台下部署EtherCalc。 201 | 202 | 这个人为约束最终使得EtherCalc可以部署在所有三种资源都受限制的PaaS环境下(譬如DotCloud,Nodejitsu和Heroku)。人们甚至可以很容易搭建一个个人的电子表格服务,从而鼓励了更多独立集成商参与贡献。 203 | 204 | ### 最差的就是最好的 205 | 206 | 在2006年芝加哥的YAPC::NA会议上,我受邀对开源世界的前景作出预测,这是我的[分享](http://pugs.blogs.com/pugs/2006/06/my_yapcna_light.html): 207 | 208 | > 我不能证明这一点,但我觉得明年JavaScript 2.0就可以实现自举,完成自托管,编译成JavaScript 1,并且取代Ruby成为所有环境下的下一匹黑马。 209 | 210 | > 我觉得CPAN和JSAN将会合并,JavaScript会成为所有动态语言通用的后端,这样你可以写Perl代码,然后在浏览器执行,在服务器运行,在数据库里运行,而只需要准备一套开发工具。 211 | 212 | > 因为,我们都知道,*更差的就是更好的*,因此*最差的*脚本语言注定成为*最好*的。 213 | 214 | 这个观点在2009年附近随着接近机器指令速度的JavaScript引擎的到来而变为现实。在编写本文时,JavaScript已经成为一个“*一次编写,到处运行*”的虚拟机-所有其它的主流语言都可以编译成JavaScript,并且[几乎没有性能损耗](http://asmjs.org/)。 215 | 216 | 除了客户端有浏览器、服务器端有Node.js,JavaScript还在[进军](http://pgre.st/)Postgres数据库,享用着数目巨大的可以在所有这些运行环境中重用的[模块仓库](https://npmjs.org/)。 217 | 218 | 是什么使得社区成长如此迅猛?从EtherCalc的开发过程中,从初始阶段开始参与NPM社区的过程中的经验来看,我猜想恰恰是因为JavaScript约束很少,并且可以为不同的使用目的定制语言,从而让创新者可以专注于方言和工具本身(譬如jQuery和Node.js),每个团队都可以从一个通用的语言核心中抽象出他们自己的*语言精髓*。 219 | 220 | 新用户可以从一个非常精炼的子集入手,经验丰富的开发者则可以挑战已有系统的更好实现。JavaScript的草根开发方式并不依赖某个核心设计团队为大家设计好完整的语言层面的方案来满足所有可预期的需求,而用实际行动践行着Richard P. Gabriel著名的“[更差的就是更好的](http://www.dreamsongs.com/WorseIsBetter.html)”这句格言。 221 | 222 | ### LiveScript,趋于极致 223 | 224 | 相比起Perl风格的[Coro::AnyEvent](https://metacpan.org/module/Coro::AnyEvent),基于回调的Node.js API依赖深层回调,从而难以复用。 225 | 226 | 在尝试了几个流控制库之后,我最终通过选用[LiveScript](http://livescript.net/)解决了这个问题。它是一门新的、编译到JavaScript的语言,其语法深受Haskell和Perl的启发。 227 | 228 | 事实上EtherCalc的实现带有4门语言的血统:JavaScript, CoffeeScript, Coco和LiveScript。每次迭代都带来更强的表现力,非常感谢[js2coffee](http://js2coffee.org/)和[js2ls](http://js2ls.org/)项目为我们维护充分的向前和向后兼容性。 229 | 230 | 因为LiveScript并不会解释成自己的二进制代码,而是编译成JavaScript,它对支持函数作用域的性能探查工具很友好。它产生的代码和手写的JavaScript一样强大,可以充分利用现代的原生JavaScript运行时。 231 | 232 | 在语法上,LiveScript用小说的结构替代回调,譬如[backcalls](http://livescript.net/#backcalls)和[cascades](http://livescript.net/#cascades)。它还从语法上提供了书写函数式或者面向对象代码的强大工具。 233 | 234 | 我刚接触LiveScript时,我觉得它像是“Perl 6的一个小方言,挣扎着要脱颖而出”。LiveScript的目标实现得如此简单,因为它采用了和JavaScript相同的语义,并且严格专注于改善语法本身。 235 | 236 | ### 总结 237 | 238 | 和SocialCalc设计良好的标准和开发流程不同,EtherCalc从2011年中到2012年底基本上是一个独立的实验产品,并支持了从评估Node.js可用性到生产环境部署的各种使用场景。 239 | 240 | 这份没有约束的自由给我提供了尝试各种语言、库、算法和架构的令人振奋的机会。我非常感谢所有的贡献者、合作者和集成商,特别感谢Dan Bricklin和Socialtext的同事们对我试验这些技术的鼓励。多谢大家! 241 | 242 | 243 | 244 | ## 文档信息 245 | 246 | 项目 | 内容 247 | ---- | ---- 248 | 原文作者 | [Audrey Tang](https://github.com/audreyt) 249 | 原文链接 | http://aosabook.org/en/posa/from-socialcalc-to-ethercalc.html 250 | 本文链接 | http://leungwensen.github.io/blog/2016/from-socialcalc-to-ethercalc.html 251 | 相关文档 | http://leungwensen.github.io/blog/2016/socialcalc.html 252 | 253 | 如果发现翻译问题,欢迎反馈:[leungwensen@gmail.com](mailto:leungwensen@gmail.com) 254 | -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/flow-nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/flow-nodejs.png -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/flow-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/flow-snapshot.png -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/multiplayer-socialcalc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/multiplayer-socialcalc.png -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/profiler-jsdom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/profiler-jsdom.png -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/profiler-no-jsdom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/profiler-no-jsdom.png -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/scaling-cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/scaling-cluster.png -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/scaling-evented.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/scaling-evented.png -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/scaling-threads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/scaling-threads.png -------------------------------------------------------------------------------- /2016/from-socialcalc-to-ethercalc/wikicalc-socialcalc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/from-socialcalc-to-ethercalc/wikicalc-socialcalc.png -------------------------------------------------------------------------------- /2016/running-scripts-with-npm.md: -------------------------------------------------------------------------------- 1 | 用`npm`执行脚本 2 | ============== 3 | 4 | ![npm-script](./running-scripts-with-npm/npm-script.png) 5 | 6 | 大部分`npm`用户都知道可以在`package.json`文件中定义`npm start`或者`npm test`这样的脚本任务。其实npm的脚本功能远远不止于启动服务器或者执行测试。 7 | 8 | 这是一个典型的`package.json`文件。 9 | 10 | ```javascript 11 | // package.json 12 | // 定义start和test脚本任务 13 | { 14 | "name": "death-clock", 15 | "version": "1.0.0", 16 | "scripts": { 17 | "start": "node server.js", 18 | "test": "mocha --reporter spec test" 19 | }, 20 | "devDependencies": { 21 | "mocha": "^1.17.1" 22 | } 23 | } 24 | // 这里为讲解需要,我在JSON文件内容中加了注解 25 | // 事实上JSON文件中是不允许有注释的 26 | ``` 27 | 28 | `start`其实是默认脚本任务,内容也默认是`node server.js`,所以上述配置其实是冗余的。为了能用在`test`任务中调用`mocha`,还需要把它作为依赖加到`devDependencies`这一节下(当然,加到`dependencies`这一节下也可以,但因为在生产环境中不需要用到,所以放到`devDependencies`下更合适一些)。 29 | 30 | `mocha --reporter spec test`这句命令之所以能运行,是因为`npm`会在`node_modules/.bin`目录下检索相应的脚本文件,而`mocha`包被安装后,一个名为`mocha`的脚本也会安装到这个目录下。 31 | 32 | `mocha`项目的`package.json`配置中的这一段描述了被安装到`bin`目录下的脚本。[^typo-1] 33 | 34 | ```javascript 35 | // mocha package.json 36 | { 37 | "name": "mocha", 38 | ... 39 | "bin": { 40 | "mocha": "./bin/mocha", 41 | "_mocha": "./bin/_mocha" 42 | }, 43 | ... 44 | } 45 | ``` 46 | 47 | 可以看到,`mocha`包定义了两个脚本:`mocha`和`_mocha`。 48 | 49 | 很多`npm`包都定义了`bin`这一节的内容。这一节指定的脚本都可以像`mocha`一样被`npm`直接运行。执行`ls node_modules/.bin`命令就可以知道在当前项目下有哪些`npm`脚本。 50 | 51 | ```bash 52 | # 我一个项目下可用的npm脚本 53 | $ ls node_modules/.bin 54 | _mocha browserify envify jshint 55 | jsx lessc lesswatcher mocha 56 | nodemon uglifyjs watchify 57 | ``` 58 | 59 | ## 执行脚本任务 60 | 61 | `start`和`test`这样的特殊脚本任务都可以直接执行。 62 | 63 | ```bash 64 | # 执行"start"指定的脚本 65 | $ npm start 66 | $ npm run start 67 | 68 | # 执行"test"指定的脚本 69 | $ npm test 70 | $ npm run test 71 | ``` 72 | 73 | 所有其它的脚本任务都必须用`npm run`来执行。`npm run`是`npm run-script`的缩略。 74 | 75 | ```javascript 76 | { 77 | ... 78 | "scripts": { 79 | // watch-test starts a mocha watcher that listens for changes 80 | "watch-test": "mocha --watch --reporter spec test" 81 | }, 82 | } 83 | ``` 84 | 85 | 上述代码指定的脚本任务可以通过`npm run watch-test`来执行,执行`npm watch-test`则会报错。 86 | 87 | ## 直接执行脚本文件 88 | 89 | 上述例子中执行的脚本任务都定义在`package.json`中,但这并不是必要条件。`npm run`可以执行任意`node_modules/.bin`路径下的脚本。也就是说,除了`npm test`,我还可以直接通过`npm run mocha`来执行`mocha`脚本。[^error-1] 90 | 91 | ## 自动补全 92 | 93 | 我们很难记住各种模块提供的形形色色的脚本命令。如果有自动补全的话,输入命令就简单多了。事实上用`npm`执行脚本是可以做到自动补全的。`npm`提供了非常友好的命令补全功能。执行`npm completion`可以得到一个`npm`的命令自动补全脚本,只要应用这个脚本,就能很方便的自动补全`npm`的普通子命令或者`npm run`的脚本命令。太方便了! 94 | 95 | 我习惯于把各种自动补全脚本存成文件放到`.bashrc`文件可以调用的地方去。 96 | 97 | ```bash 98 | # npm_completion.sh 99 | . <(npm completion) 100 | 101 | # 我一个项目的自动补全输出 102 | $ npm run 103 | nodemon browserify build 104 | build-js build-less start 105 | jshint test deploy 106 | less uglify-js express 107 | mocha watch watch-js 108 | watch-less watch-server 109 | ``` 110 | 111 | 很帅吧! 112 | 113 | ## 组合脚本任务 114 | 115 | 上述的`npm`特性已经可以满足大部分场景了,不过有时候我们需要同时完成多项任务。`npm`也具备这样的能力。`npm run`其实最后会把脚本任务输出给`sh`执行,所以理论上我们可以像在命令行中一样组合各种脚本任务。 116 | 117 | ### 管道 118 | 119 | 假设我们要用`browserify`打包`javascript`文件,并且要用`uglifyjs`进行代码混淆。我只需要用管道(`|`)把`browserify`的输出转接给`uglifyjs`就可以了。非常简单。 120 | 121 | ```javascript 122 | // package.json 123 | // browserify的reactify选项用于处理React语法 124 | "scripts": { 125 | "build-js": "browserify -t reactify app/js/main.js | uglifyjs -mc > static/bundle.js" 126 | }, 127 | // 添加必要的依赖项 128 | "devDependencies": { 129 | "browserify": "^3.14.0", 130 | "reactify": "^0.5.1", 131 | "uglify-js": "^2.4.8" 132 | } 133 | ``` 134 | 135 | ### 串行 136 | 137 | 另一个场景是我们希望当且仅当上一个命令完成后,再执行下一个命令。可以通过串行符号(`&&`)来实现这个功能,当然,管道(`|`)也可以实现类似的效果。[^error-2] 138 | 139 | ```javascript 140 | "scripts": { 141 | // 如果build-js,则继续执行build-less 142 | "build": "npm run build-js && npm run build-less", 143 | ... 144 | "build-js": "browserify -t reactify app/js/main.js | uglifyjs -mc > static/bundle.js", 145 | "build-less": "lessc app/less/main.less static/main.css" 146 | } 147 | ``` 148 | 149 | 这里,我通过`build`脚本任务来执行另外两个在`package.json`中定义的脚本任务。和执行脚本不同之处在于:必须使用`npm run`来执行其它脚本任务。 150 | 151 | ### 并行 152 | 153 | 有时候并行地执行多个命令的功能也是必要的。使用并行符号(`&`)可以把子命令作为后台任务并行执行。 154 | 155 | ```javascript 156 | "scripts": { 157 | // 并行地执行watch-js,watch-less和watch-server 158 | "watch": "npm run watch-js & npm run watch-less & npm run watch-server", 159 | "watch-js": "watchify app/js/main.js -t reactify -o static/bundle.js -dv", 160 | "watch-less": "nodemon --watch app/less/*.less --ext less --exec 'npm run build-less'", 161 | "watch-server": "nodemon --ignore app --ignore static server.js" 162 | }, 163 | // 添加必要的依赖项 164 | "devDependencies": { 165 | "watchify": "^0.6.2", 166 | "nodemon": "^1.0.15" 167 | } 168 | ``` 169 | 170 | 上述配置有几个挺有意思的点。首先`watch`脚本任务使用`&`来并行执行三个`watch`任务。如果按下`Ctrl-C`杀掉进程,所有`watch`任务都会停止,因为它们由同一个父进程执行。 171 | 172 | `watchify`是`browserify`命令的`watch`模式。`watch-server`是`nodemon`的标准用法:每当有相应的文件变更时,重启服务器。 173 | 174 | `watch-less`则是`nodemon`的一种不常见的用法。每当有`less`文件变更的时候,它都会执行`npm run build-less`命令把`less`文件编译成`CSS`文件。`--ext less`是必需的选项。`--exec`选项指定的是`nodemon`执行的外部命令。 175 | 176 | ## 复杂脚本 177 | 178 | 如果是更复杂的脚本任务,我通常会写成`bash`文件,而在`package.json`中指定为脚本任务。下面是一个脚本实例,这个脚本做的事情是把编译好的资源加到一个发布分支,并且把这个分之推送到`Heroku`上。 179 | 180 | ```bash 181 | #!/bin/bash 182 | 183 | set -o errexit # Exit on error 184 | 185 | git stash save -u 'Before deploy' # Stash all changes, including untracked files, before deploy 186 | git checkout deploy 187 | git merge master --no-edit # Merge in the master branch without prompting 188 | npm run build # Generate the bundled Javascript and CSS 189 | if $(git commit -am Deploy); then # Commit the changes, if any 190 | echo 'Changes Committed' 191 | fi 192 | git push heroku deploy:master # Deploy to Heroku 193 | git checkout master # Checkout master again 194 | git stash pop # And restore the changes 195 | ``` 196 | 197 | 加上`package.json`的配置后,就可以通过`npm run deploy`命令执行这个脚本了。 198 | 199 | ```javascript 200 | "scripts": { 201 | "deploy": "./bin/deploy.sh" 202 | }, 203 | ``` 204 | 205 | ## 小结 206 | 207 | 对于`Node`而言,`npm`的存在意义并不止于包管理器。通过合适的配置,我们可以处理绝大部分的脚本需求。 208 | 209 | 另一个用`npm`执行脚本的原因是,只需要配置好`start`和`test`,我的项目就可以和`Heroku`和`TravisCI`这样的`SaaS`服务提供商整合起来了。 210 | 211 | ## 文档信息 212 | 213 | 项目 | 内容 214 | ---- | ---- 215 | 原文作者 | [Anders Janmyr](https://github.com/andersjanmyr) 216 | 原文链接 | http://anders.janmyr.com/2014/03/running-scripts-with-npm.html 217 | 本文链接 | http://leungwensen.github.io/blog/2016/from-socialcalc-to-ethercalc.html 218 | 219 | 如果发现翻译问题,欢迎反馈:[leungwensen@gmail.com](mailto:leungwensen@gmail.com) 220 | 221 | [^typo-1]: 原文笔误:`mocha`写成了`macha` 222 | [^error-1]: 原文错误:`npm test`写成了`mocha test` 223 | [^error-2]: 原文代码注释错误:`build-js`写成了`build-less` 224 | -------------------------------------------------------------------------------- /2016/running-scripts-with-npm/npm-script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/running-scripts-with-npm/npm-script.png -------------------------------------------------------------------------------- /2016/socialcalc/collab-borders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/collab-borders.png -------------------------------------------------------------------------------- /2016/socialcalc/collab-conflict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/collab-conflict.png -------------------------------------------------------------------------------- /2016/socialcalc/collab-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/collab-flow.png -------------------------------------------------------------------------------- /2016/socialcalc/collab-olpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/collab-olpc.png -------------------------------------------------------------------------------- /2016/socialcalc/collab-resolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/collab-resolution.png -------------------------------------------------------------------------------- /2016/socialcalc/richtext-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/richtext-example.png -------------------------------------------------------------------------------- /2016/socialcalc/richtext-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/richtext-flow.png -------------------------------------------------------------------------------- /2016/socialcalc/richtext-formats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/richtext-formats.png -------------------------------------------------------------------------------- /2016/socialcalc/richtext-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/richtext-screenshot.png -------------------------------------------------------------------------------- /2016/socialcalc/socialcalc-2046.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/socialcalc-2046.png -------------------------------------------------------------------------------- /2016/socialcalc/socialcalc-cell-handles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/socialcalc-cell-handles.png -------------------------------------------------------------------------------- /2016/socialcalc/socialcalc-class-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/socialcalc-class-diagram.png -------------------------------------------------------------------------------- /2016/socialcalc/socialcalc-command-runloop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/socialcalc-command-runloop.png -------------------------------------------------------------------------------- /2016/socialcalc/socialcalc-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/socialcalc-input.png -------------------------------------------------------------------------------- /2016/socialcalc/socialcalc-parts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/socialcalc-parts.png -------------------------------------------------------------------------------- /2016/socialcalc/socialcalc-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/socialcalc-screenshot.png -------------------------------------------------------------------------------- /2016/socialcalc/wikicalc-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/wikicalc-components.png -------------------------------------------------------------------------------- /2016/socialcalc/wikicalc-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/wikicalc-flow.png -------------------------------------------------------------------------------- /2016/socialcalc/wikicalc-loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/wikicalc-loading.png -------------------------------------------------------------------------------- /2016/socialcalc/wikicalc-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2016/socialcalc/wikicalc-screenshot.png -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 外行图说搜索引擎 6 | 7 | 8 | 9 | 25 | 26 | 27 |
28 |
29 |
30 |

外行人图说搜索引擎

31 |
32 |
33 |

History of Search Engines

34 |
35 |
36 |
37 |

Search Engine Market share in March 2017

38 |
39 |
40 |
41 |

Types of Search Queries

42 |
43 | Fall 2002, Andrei Broder 44 |
45 |
46 |

Parts of a Search Engine

47 | 48 |
49 |
50 |
51 |

Index?

52 |
53 |
54 |
    55 |
  • Doc1: I like search engines
  • 56 |
  • Doc2: I search keywords in Google
  • 57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
IlikesearchenginekeywordinGoogle
Doc11111000
Doc21010111
92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |
Doc1Doc2
engine10
Google01
I11
in01
keyword01
like10
search11
136 |
137 |
138 |
139 |
140 |

Actually, It's much more COMPLICATED

141 | 142 | 143 |
144 |
145 |

Word-sense Disambiguation

146 |
    147 |
  • 已/结婚/的/和/尚未/结婚/的/青年
  • 148 |
  • 已/结婚/的/和尚/未/结婚/的/青年
  • 149 |
  • 四是四十是十十四是十四四十是四十
  • 150 |
151 |
152 |
153 |

New Words Recognition

154 |
    155 |
  • 黄教主和Baby高呼闹太套
  • 156 |
157 |
158 |
159 |
160 |
161 | Real World Full-Text Search 162 | 163 |
164 |
165 | Documents 166 | 167 |
168 |
169 | Indices 170 | 171 |
172 |
173 |
174 |

References

175 | 207 |
208 |
209 |
210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/archie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/archie.png -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/baidu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 21 | 25 | 27 | 30 | 36 | 37 | 41 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/bing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 51 | 52 | 54 | 55 | 57 | image/svg+xml 58 | 60 | 61 | 62 | 63 | 64 | 69 | 75 | 79 | 84 | 90 | 96 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/dash-docset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/dash-docset.png -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/dash-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/dash-index.png -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/dash.png -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/google-1998.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/google-1998.png -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/google-adwords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/google-adwords.png -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/market-share.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let rendered = false 3 | const data = [ 4 | { 5 | name: 'Google', 6 | value: 80.52, 7 | }, 8 | { 9 | name: 'Bing', 10 | value: 6.92, 11 | }, 12 | { 13 | name: 'Baidu', 14 | value: 5.94, 15 | }, 16 | { 17 | name: 'Yahoo!', 18 | value: 5.35, 19 | }, 20 | ] 21 | 22 | Reveal.addEventListener('slidechanged', (event) => { 23 | // event.previousSlide, event.currentSlide, event.indexh, event.indexv 24 | if (event.indexh === 2 && event.indexv === 0 && !rendered) { 25 | const Stat = G2.Stat 26 | const chart = new G2.Chart({ 27 | id: 'market-share-2017', 28 | forceFit: true, 29 | height: 500, 30 | }) 31 | chart.source(data) 32 | chart.coord('theta', { 33 | radius: 0.8 // 设置饼图的大小 34 | }) 35 | chart.legend('name', { 36 | position: 'bottom', 37 | itemWrap: true, 38 | formatter: (val) => { 39 | for (let i = 0, len = data.length; i < len; i++) { 40 | const obj = data[i] 41 | if (obj.name === val) { 42 | return `${val}: ${obj.value}%` 43 | } 44 | } 45 | } 46 | }) 47 | chart.tooltip({ 48 | title: null, 49 | }) 50 | chart.intervalStack() 51 | .position(Stat.summary.percent('value')) 52 | .color('name') 53 | .label('name*..percent', (name, percent) => { 54 | percent = `${(percent * 100).toFixed(2)}%` 55 | return `${name} ${percent}` 56 | }) 57 | 58 | chart.render() 59 | // 设置默认选中 60 | const geom = chart.getGeoms()[0] // 获取所有的图形 61 | const items = geom.getData() // 获取图形对应的数据 62 | geom.setSelected(items[0]) // 设置选中 63 | rendered = true 64 | } 65 | }) 66 | })() 67 | -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/other-subjects.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let rendered = false 3 | const Util = G2.Util 4 | const Shape = G2.Shape 5 | 6 | function getTextAttrs(cfg) { 7 | const textAttrs = Util.mix(true, {}, { 8 | fillOpacity: cfg.opacity, 9 | fontSize: cfg.size, 10 | rotate: cfg.origin._origin.rotate, 11 | text: cfg.origin._origin.text, 12 | textAlign: 'center', 13 | fill: cfg.color, 14 | textBaseline: 'Alphabetic' 15 | }, cfg.style) 16 | return textAttrs 17 | } 18 | 19 | // 给point注册一个词云的shape 20 | Shape.registShape('point', 'cloud', { 21 | drawShape: function (cfg, container) { 22 | cfg.points = this.parsePoints(cfg.points); 23 | const attrs = getTextAttrs(cfg) 24 | // 给容器添加text类型的shape 25 | // 坐标仍然是原来的坐标 26 | // 文字样式为通过getTextAttrs方法获取的样式 27 | const shape = container.addShape('text', { 28 | attrs: Util.mix(attrs, { 29 | x: cfg.points[0].x, 30 | y: cfg.points[0].y 31 | }) 32 | }) 33 | return shape 34 | } 35 | }) 36 | 37 | const data = [ 38 | { 39 | name: 'Tokenization', 40 | value: 8, 41 | }, 42 | { 43 | name: 'Boolean Retrieval', 44 | value: 8, 45 | }, 46 | { 47 | name: 'Crawler', 48 | value: 7, 49 | }, 50 | { 51 | name: 'Page Ranking', 52 | value: 7, 53 | }, 54 | { 55 | name: 'Index Construction', 56 | value: 7, 57 | }, 58 | { 59 | name: 'Segmentation', 60 | value: 7, 61 | }, 62 | { 63 | name: 'Language Recognition', 64 | value: 2, 65 | }, 66 | { 67 | name: 'Optimization', 68 | value: 2, 69 | }, 70 | { 71 | name: 'Word-sense Disambiguation', 72 | value: 3, 73 | }, 74 | { 75 | name: 'New Words Recognition', 76 | value: 2, 77 | }, 78 | { 79 | name: 'Format Analysis', 80 | value: 5, 81 | }, 82 | { 83 | name: 'Dictionary', 84 | value: 2, 85 | }, 86 | { 87 | name: 'Faulty Storage', 88 | value: 3, 89 | }, 90 | { 91 | name: 'Large Scale Storage', 92 | value: 3, 93 | }, 94 | { 95 | name: 'Text Mining', 96 | value: 7, 97 | }, 98 | { 99 | name: 'Vertical Search', 100 | value: 3, 101 | }, 102 | { 103 | name: 'Faceted Search', 104 | value: 3, 105 | }, 106 | ] 107 | 108 | Reveal.addEventListener('slidechanged', (event) => { 109 | // event.previousSlide, event.currentSlide, event.indexh, event.indexv 110 | if (event.indexh === 6 && event.indexv === 0 && !rendered) { 111 | data.sort((a, b) => b.value - a.value) 112 | // 获取数据的最大值和最小值 113 | const max = data[0].value 114 | const min = data[data.length - 1].value 115 | // 构造一个词云布局对象 116 | const layout = new Cloud({ 117 | // 传入数据源 118 | words: data, 119 | // 设定宽高(默认为500*500) 120 | width: 500, 121 | height: 500, 122 | // 设定文字大小配置函数(默认为12-40px的随机大小) 123 | size: (words) => { 124 | // 将pv映射到canvas可绘制的size范围14-100(canvas默认最小文字为12px) 125 | return ((words.value + 1 - min) / (max - min)) * (100 - 14) + 14 126 | }, 127 | // 设定文字内容 128 | text: (words) => { 129 | let text = words.name 130 | return text 131 | } 132 | }) 133 | // 执行词云布局函数,并在回调函数中调用G2对结果进行绘制 134 | layout.exec((texts) => { 135 | const chart = new G2.Chart({ 136 | id: 'other-subjects', 137 | // canvas的宽高需要和布局宽高一致 138 | width: 500, 139 | height: 500, 140 | plotCfg: { 141 | margin: 0 142 | } 143 | }); 144 | chart.legend(false) 145 | chart.source(texts) 146 | chart.axis(false) 147 | chart.tooltip({ 148 | title: false 149 | }) 150 | // 将词云坐标系调整为G2的坐标系 151 | chart.coord().reflect() 152 | // 绘制点图,在x*y的坐标点绘制自定义的词云shape,颜色根据text字段进行映射,大小根据size字段的真实值进行映射,文字样式配置为词云布局返回的样式,tooltip显示site*pv两个字段的内容 153 | chart.point().position('x*y').color('text').size('size', size => size).shape('cloud').style({ 154 | fontStyle: texts[0].style, 155 | fontFamily: texts[0].font, 156 | fontWeight: texts[0].weight 157 | }); 158 | chart.render() 159 | }) 160 | rendered = true 161 | } 162 | }) 163 | })() 164 | 165 | -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/other-subjects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/other-subjects.png -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/overture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/overture.jpg -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/reveal.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // presentation 3 | Reveal.initialize() 4 | })() 5 | 6 | -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/search-engine.xml: -------------------------------------------------------------------------------- 1 | 7VrLkps6EP0aL5NCCGO8TJy596YqWSROVZKlAjJwR5ZcQviRr48A8ZAFzlQCsucxm0GtB+j08elWwwyutsd/OdolH1mEycx1ouMMvpu5LvCgI/8VllNlcQFcVpaYp5Ea1RrW6U+sjGpinKcRzrSBgjEi0p1uDBmlOBSaDXHODvqwDSP6XXcoxoZhHSJiWr+mkUgqa+D6rf0/nMZJfWfgq/39QOF9zFlO1f1mLtyUf1X3FtVrqY1mCYrYoWOCdzO44oyJ6mp7XGFSgFvDVs37Z6C3eW6OqXjIBE89hjjVW8eRREI1GRcJixlF5K61vg1zvsfFfCAb5V7LliNbidgS1YGPqfjWuf5eDHk9L1pU8NM3NaNstH3/YyFOig8oF0ya2of4wNhOrbhhVKhhwJftahvFsw8CoUwZy3moRrmKWojHWI2CjRMkuzHbYvl4cgjHBIl0r6+OFMviZlyLtLxQYPcDv3gBXgd+bgn44AV4HXjfEvC1tk+IvDOMPLhB5JeWkFe33iOSq0XXGPEwkbY7GqcUG45pkS4AOCSpwOsdKrdykCFfRz8TnN3jFSOMl7OhHwb4x6bpqSOoW2CZEtIZGSEcbMJBjPeYC3y8jLKJn5oA6xhbJyF1+9BGdOArW9KJ5rXtr8g+t0x2V9eZlvzf1dAIZUmzuF3qw+tRH/jT+6HCWnkCPCo32Aq60FCg9xLUI+ZPUnnmvq48gUXhmfcjLU0fEZXHlueBuF2tnz6X1zXGuaAx4Loa419PY8D0mb0echeXQu7t+cFWyIXT5/ngoh8WN+4IYMsT/mAsqPL/ZxIMoG8xGCwNzN+xMN8Wj/60QzAADwjBi6lC8PR1zEeT5oOePN9WVROYiX5N/8zwUJagXXFJmcB/QHun/Pst7SmjuAfbrLp2wUias9TZ/6ov53d62B+MwP5adCZlvztI/7PSWjnwmvxfmvy3VeP0LNQ4VX7Tfxa4qdynzxNDv6PxpciMxE3Vc435Pg3HLnv+nSCNIELN+8mLp2BvmhAMLRy/LohQtwJ6A0G4eWt9jby/XqND/cHgi0JRsPLxkX3peAa553CiAGtW1Rot+ZRjtVgXXrkvcQnBGhcTKkTSmMpmKHHAhWcKlNIQkTeqY5tGERlKmCYAerEMNFUBfapS19m7yLtjIG+eYT/jLCc3mk7Kn/g45D47wvZhHkyUTUJTPZ4R2b2z2kEv2XuQH4Ps0H2WZA/mOuaeY5Hs07+ntfM5iJbr/FnOAr2enMVW5aC+eYf7K44OZPRq2dUTFxiclQoWD5SYUbL0l3dVLd97avO2CgWwJ69B9D6lcXE8PWUCb58a78/L8p7N1+KeeQKa/uOzK3Lbu+b50zMzyC+SclTu6smpOXDO5Ly36OIsR6G1bLafhpd9nQ/w4d0v -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/tim-bl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/tim-bl.jpg -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/timeline.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let rendered = false 3 | const container = document.getElementById('timeline') 4 | const data = [ 5 | { 6 | id: 'archie', 7 | content: `The FIRST one: Archie 8 |
9 | 10 | 11 | 12 | `, 13 | start: '1990-09-10', 14 | }, 15 | { 16 | id: 'www', 17 | content: `Tim Berners-Lee & the WWW 18 |
19 | 20 | 21 | 22 | `, 23 | start: '1991-08-06', 24 | }, 25 | { 26 | id: 'yahoo', 27 | content: `Yahoo! Directory 28 |
29 | 30 | 31 | 32 | `, 33 | start: '1994-04-01', 34 | }, 35 | { 36 | id: 'webcrawler', 37 | content: `WebCrawler 38 |
39 | 40 | 41 | 42 | `, 43 | start: '1994-04-20', 44 | }, 45 | { 46 | id: 'Overture', 47 | content: `Pioneer of paid search 48 |
49 | 50 | 51 | 52 | `, 53 | start: '1998-02-01', 54 | }, 55 | { 56 | id: 'google-1998', 57 | content: `Google launched 58 |
59 | 60 | 61 | 62 | `, 63 | start: '1998-09-04', 64 | }, 65 | { 66 | id: 'baidu', 67 | content: `Baidu launched 68 |
69 | 70 | 71 | 72 | `, 73 | start: '2000-01-01', 74 | }, 75 | { 76 | id: 'google-AdWords', 77 | content: `Google AdWords 78 |
79 | 80 | 81 | 82 | `, 83 | start: '2000-10-23', 84 | }, 85 | { 86 | id: 'bing', 87 | content: `Bing launched 88 |
89 | 90 | 91 | 92 | `, 93 | start: '2009-06-01', 94 | } 95 | ] 96 | Reveal.addEventListener('slidechanged', (event) => { 97 | // event.previousSlide, event.currentSlide, event.indexh, event.indexv 98 | if (event.indexh === 1 && event.indexv === 0 && !rendered) { 99 | new vis.Timeline(container, data, {}) 100 | rendered = true 101 | } 102 | }) 103 | })() 104 | 105 | -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/types-of-search-queries.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let rendered = false 3 | const data = [ 4 | { 5 | category: 'User Survey', 6 | type: 'Navigational', 7 | desc: 'The immediate intent is to reach a particular site', 8 | value: 24.5, 9 | }, 10 | { 11 | category: 'User Survey', 12 | type: 'Informational', 13 | desc: 'The intent is to acquire some information assumed to be present on one or more web pages', 14 | value: 39, 15 | }, 16 | { 17 | category: 'User Survey', 18 | type: 'Transactional', 19 | desc: 'The intent is to perform some web-mediated activity', 20 | value: 36, 21 | }, 22 | { 23 | category: 'Query Log Analysis', 24 | type: 'Navigational', 25 | desc: 'The immediate intent is to reach a particular site', 26 | value: 20, 27 | }, 28 | { 29 | category: 'Query Log Analysis', 30 | type: 'Informational', 31 | desc: 'The intent is to acquire some information assumed to be present on one or more web pages', 32 | value: 48, 33 | }, 34 | { 35 | category: 'Query Log Analysis', 36 | type: 'Transactional', 37 | desc: 'The intent is to perform some web-mediated activity', 38 | value: 30, 39 | }, 40 | ] 41 | 42 | Reveal.addEventListener('slidechanged', (event) => { 43 | // event.previousSlide, event.currentSlide, event.indexh, event.indexv 44 | if (event.indexh === 3 && event.indexv === 0 && !rendered) { 45 | const Stat = G2.Stat 46 | const chart = new G2.Chart({ 47 | id: 'types-of-search-queries', 48 | forceFit: true, 49 | height: 500, 50 | plotCfg: { 51 | margin: 80 52 | } 53 | }) 54 | chart.source(data) 55 | // 以 year 为维度划分分面 56 | chart.facet(['category'], { 57 | margin: 50, 58 | facetTitle: { 59 | colDimTitle: { 60 | title: null 61 | }, 62 | colTitle: { 63 | title: { 64 | fontSize: 18, 65 | textAlign: 'center', 66 | fill: '#999' 67 | } 68 | } 69 | } 70 | }) 71 | chart.legend({ 72 | position: 'bottom' 73 | }) 74 | chart.coord('theta', { 75 | radius: 1, 76 | inner: 0.35 77 | }) 78 | chart.tooltip({ 79 | title: null 80 | }) 81 | chart.intervalStack().position(Stat.summary.percent('value')) 82 | .color('type') 83 | .label('type*..percent', (type, percent) => { 84 | percent = `${(percent * 100).toFixed(2)}%` 85 | return `${type} ${percent}` 86 | }) 87 | .style({ 88 | lineWidth: 1 89 | }) 90 | chart.render() 91 | rendered = true 92 | } 93 | }) 94 | })() 95 | -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/webcrawler.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/webcrawler.gif -------------------------------------------------------------------------------- /2017/a-brief-intro-to-search-engine/yahoo-directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-brief-intro-to-search-engine/yahoo-directory.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-1-2a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-1-2a.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-1-3a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-1-3a.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-2-3.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-2-4.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-2-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-2-5.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-4-2.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-4-3.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-4-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-4-4.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-5-1.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-5-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-5-4.png -------------------------------------------------------------------------------- /2017/a-technique-for-drawing-directed-graphs/figure-5-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/a-technique-for-drawing-directed-graphs/figure-5-5.png -------------------------------------------------------------------------------- /2017/mind-map-drawing-algorithms/downward-organizational.svg: -------------------------------------------------------------------------------- 1 | organizationalchild-1child-1-1child-1-2child-2child-3child-4child-4-1child-4-2child-1-2-1 -------------------------------------------------------------------------------- /2017/mind-map-drawing-algorithms/downward-tree-organizational.svg: -------------------------------------------------------------------------------- 1 | tree-organizationalchild-1child-1-1child-1-2child-2child-3child-4child-4-1child-4-2child-1-2-1 -------------------------------------------------------------------------------- /2017/mind-map-drawing-algorithms/indented.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leungwensen/blog/bd80c8bd2601d80a0c2cc04e8f1b447a5cb94b21/2017/mind-map-drawing-algorithms/indented.png -------------------------------------------------------------------------------- /2017/mind-map-drawing-algorithms/right-fish-bone.svg: -------------------------------------------------------------------------------- 1 | right-fish-bonechild-1child-1-1child-1-2child-2child-3child-4child-4-1child-4-2child-1-2-1 -------------------------------------------------------------------------------- /2017/mind-map-drawing-algorithms/right-logical.svg: -------------------------------------------------------------------------------- 1 | right-hierarchicalchild-1child-1-1child-1-2child-2child-3child-4child-4-1child-4-2child-1-2-1 -------------------------------------------------------------------------------- /2017/mind-map-drawing-algorithms/standard.svg: -------------------------------------------------------------------------------- 1 | standardchild-1child-1-1child-1-2child-2child-3child-4child-4-1child-4-2child-1-2-1 -------------------------------------------------------------------------------- /2017/mindmap-drawing-algorithms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 思维导图自动布局算法 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 |
36 | 44 |

思维导图自动布局算法

45 |

概述

46 |

为了让整理思路的过程更流畅,市面上的思维导图软件一般采用自动布局,使用户不必关心图形布局也能画出比较优美的思维导图。而为了用相对较小的代价(需要实时交互,实时布局)实现自动布局,常见的思维导图软件处理的图数据格式都是树形数据(严格地说,是有序根树,即ordered rooted tree)。本文以经典思维导图软件XMind接受的数据格式为例,汇总常见的思维导图自动布局算法。

47 |

输入

48 |
{
 49 |     "root": {
 50 |         "name": "root",
 51 |         "children": [
 52 |             {
 53 |                 "name": "child-1",
 54 |                 "children": [
 55 |                     {
 56 |                         "name": "child-1-1"
 57 |                     },
 58 |                     {
 59 |                         "name": "child-1-2",
 60 |                         "children": [
 61 |                             {
 62 |                                 "name": "child-1-2-1"
 63 |                             }
 64 |                         ]
 65 |                     }
 66 |                 ]
 67 |             },
 68 |             {
 69 |                 "name": "child-2"
 70 |             },
 71 |             {
 72 |                 "name": "child-3"
 73 |             },
 74 |             {
 75 |                 "name": "child-4",
 76 |                 "children": [
 77 |                     {
 78 |                         "name": "child-4-1"
 79 |                     },
 80 |                     {
 81 |                         "name": "child-4-2"
 82 |                     }
 83 |                 ]
 84 |             }
 85 |         ]
 86 |     },
 87 |     "links": [
 88 |         {
 89 |             "source": "child-1-1",
 90 |             "name": "special link",
 91 |             "target": "child-2"
 92 |         }
 93 |     ]
 94 | }
 95 | 
96 |
97 |

root及其子孙是思维导图里的节点,对应XMind里的Topic

98 |
99 |
100 |

links是思维导图节点间非继承关系的额外联系,对应XMind里的Relationship

101 |
102 |

更多关于.xmind文件的结构可参见xmind-sdk-javascript

103 |

输出

104 |
{
105 | 	"nodes": [], // 带坐标信息的节点
106 | 	"edges": [], // 带起点终点坐标信息的边
107 | }
108 | 
109 |

算法汇总

110 |

标准布局 standard

111 |
standard layout
112 |

特点

113 |
    114 |
  1. Root节点的子节点先左后右布局。左边子节点后续节点往左,右边子节点后续节点往右。
  2. 115 |
  3. Root节点的子节点围绕Root节点带向内的弧度紧凑布局。
  4. 116 |
  5. Root节点后两层以后的子节点和所在层子节点垂直对齐(右边节点左对齐,左边节点右对齐)。
  6. 117 |
  7. 布局时以Root节点为中心布局,布局完毕所有节点整体相对画布居中。
  8. 118 |
119 |

使用场景

120 |

这种布局是经典的脑图布局,能比较直观地描绘发散的大脑思维,帮助人合并不同来源的资料,整理复杂的问题。

121 |

算法描述

122 |

右向逻辑布局 right logical

123 |
right logical layout
124 |

特点

125 |
    126 |
  1. 从左往右布局各个层次的节点。
  2. 127 |
  3. 和经典的树图或者分层布局不同的地方在于,每个节点的位置只相对于父节点,和其他父节点不同的同层次节点位置不相关。
  4. 128 |
  5. 是所谓的"非分层紧凑树布局"。
  6. 129 |
130 |

使用场景

131 |

这种布局就是经典的树图层次结构布局,适合有明显分层的信息。譬如总结信息,族谱,目录结构和记录笔记等。

132 |

向下组织结构布局 downward organizational

133 |
downward organizational layout
134 |

向下目录组织布局 downward tree organizational

135 |
downward tree organizational layout
136 |

右向鱼骨布局 right fish bone

137 |
right fish bone layout
138 |

缩进布局 indented

139 |
indented layout
140 | 159 |

相关链接

160 | 163 |

附录

164 |

树布局的美学标准

165 |
    166 |
  1. 节点之间不重叠
  2. 167 |
  3. 子节点按照指定的顺序排列
  4. 168 |
  5. 父节点在子节点中心
  6. 169 |
  7. 某棵子树的绘制不取决于其在树中的位置,同样的子树绘制结果应该一致
  8. 170 |
  9. 树的反射的布局中,每个子节点顺序和原来布局相反
  10. 171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /2017/mindmap-drawing-algorithms.md: -------------------------------------------------------------------------------- 1 | 思维导图自动布局算法 2 | ================= 3 | 4 | ## 概述 5 | 6 | 为了让整理思路的过程更流畅,市面上的思维导图软件一般采用自动布局,使用户不必关心图形布局也能画出比较优美的思维导图。而为了用相对较小的代价(需要实时交互,实时布局)实现自动布局,常见的思维导图软件处理的图数据格式都是树形数据(严格地说,是有序根树,即ordered rooted tree)。本文以经典思维导图软件XMind接受的数据格式为例,汇总常见的思维导图自动布局算法。 7 | 8 | ### 输入 9 | 10 | ```javascript 11 | { 12 | "root": { 13 | "name": "root", 14 | "children": [ 15 | { 16 | "name": "child-1", 17 | "children": [ 18 | { 19 | "name": "child-1-1" 20 | }, 21 | { 22 | "name": "child-1-2", 23 | "children": [ 24 | { 25 | "name": "child-1-2-1" 26 | } 27 | ] 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "child-2" 33 | }, 34 | { 35 | "name": "child-3" 36 | }, 37 | { 38 | "name": "child-4", 39 | "children": [ 40 | { 41 | "name": "child-4-1" 42 | }, 43 | { 44 | "name": "child-4-2" 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | "links": [ 51 | { 52 | "source": "child-1-1", 53 | "name": "special link", 54 | "target": "child-2" 55 | } 56 | ] 57 | } 58 | ``` 59 | 60 | > root及其子孙是思维导图里的节点,对应XMind里的Topic 61 | 62 | > links是思维导图节点间非继承关系的额外联系,对应XMind里的Relationship 63 | 64 | 更多关于.xmind文件的结构可参见[xmind-sdk-javascript](https://github.com/leungwensen/xmind-sdk-javascript) 65 | 66 | ### 输出 67 | 68 | ```javascript 69 | { 70 | "nodes": [], // 带坐标信息的节点 71 | "edges": [], // 带起点终点坐标信息的边 72 | } 73 | ``` 74 | 75 | ## 算法汇总 76 | 77 | ### 标准布局 standard 78 | 79 | ![standard layout](mind-map-drawing-algorithms/standard.svg) 80 | 81 | #### 特点 82 | 83 | 1. Root节点的子节点先左后右布局。左边子节点后续节点往左,右边子节点后续节点往右。 84 | 2. Root节点的子节点围绕Root节点带向内的弧度紧凑布局。 85 | 3. Root节点后两层以后的子节点和所在层子节点垂直对齐(右边节点左对齐,左边节点右对齐)。 86 | 4. 布局时以Root节点为中心布局,布局完毕所有节点整体相对画布居中。 87 | 88 | #### 使用场景 89 | 90 | 这种布局是经典的脑图布局,能比较直观地描绘发散的大脑思维,帮助人合并不同来源的资料,整理复杂的问题。 91 | 92 | #### 算法描述 93 | 94 | ### 右向逻辑布局 right logical 95 | 96 | ![right logical layout](mind-map-drawing-algorithms/right-logical.svg) 97 | 98 | #### 特点 99 | 100 | 1. 从左往右布局各个层次的节点。 101 | 2. 和经典的树图或者分层布局不同的地方在于,每个节点的位置只相对于父节点,和其他父节点不同的同层次节点位置不相关。 102 | 3. 是所谓的"非分层紧凑树布局"。 103 | 104 | #### 使用场景 105 | 106 | 这种布局就是经典的树图层次结构布局,适合有明显分层的信息。譬如总结信息,族谱,目录结构和记录笔记等。 107 | 108 | ### 向下组织结构布局 downward organizational 109 | 110 | ![downward organizational layout](mind-map-drawing-algorithms/downward-organizational.svg) 111 | 112 | ### 向下目录组织布局 downward tree organizational 113 | 114 | ![downward tree organizational layout](mind-map-drawing-algorithms/downward-tree-organizational.svg) 115 | 116 | ### 右向鱼骨布局 right fish bone 117 | 118 | ![right fish bone layout](mind-map-drawing-algorithms/right-fish-bone.svg) 119 | 120 | ### 缩进布局 indented 121 | 122 | ![indented layout](mind-map-drawing-algorithms/indented.png) 123 | 124 | 143 | 144 | ## 相关链接 145 | 146 | - 算法实现:[mindmap-layouts](https://github.com/leungwensen/mindmap-layouts) 147 | 148 | ## 附录 149 | 150 | ### 树布局的美学标准 151 | 152 | 1. 节点之间不重叠 153 | 2. 子节点按照指定的顺序排列 154 | 3. 父节点在子节点中心 155 | 4. 某棵子树的绘制不取决于其在树中的位置,同样的子树绘制结果应该一致 156 | 5. 树的反射的布局中,每个子节点顺序和原来布局相反 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 文森叔叔的小屋 2 | ============ 3 | 4 | 链接: http://leungwensen.github.io/blog/ 仓库: https://github.com/leungwensen/blog 5 | 6 | ## 博文列表 7 | 8 | ### 2017 9 | 10 | * [外行人说搜索引擎](./2017/a-brief-intro-to-search-engine.html) 11 | * [[WIP]一种画有向图的技术(译文)](./2017/a-technique-for-drawing-directed-graphs.md) 12 | * [[WIP]思维导图自动布局算法](./2017/mindmap-drawing-algorithms.md) 13 | 14 | ### 2016 15 | 16 | * [SocialCalc(译文)](./2016/socialcalc.md) 17 | * [从SocialCalc到EtherCalc(译文)](./2016/from-socialcalc-to-ethercalc.md) 18 | * [从icon fonts到SVG icons](./2016/from-icon-fonts-to-svg-icons.md) 19 | * [用npm执行脚本(译文)](./2016/running-scripts-with-npm.md) 20 | * [程序员的算法趣题(译本)](./2016/70-math-quizs-for-programmers.md) 21 | * [[TRASH]~~决策树~~](./2015/decision-tree.md) 22 | 23 | 24 | ### 2015 25 | 26 | * [Mac下使用latex遇到的问题及解法](./2015/fixing-latex-in-mac.md) 27 | * [git二三事](./2015/git.md) 28 | * [~~图解简单算法~~写给大家看的算法书(译本)](./2015/an-illustrated-brief-introduction-to-algorithm.md) 29 | * [使用CORS(译文)](./2015/cors.md) 30 | * [前端可视化建模技术概览](./2015/frontend-visual-modeling.md) 31 | * [自制编译器(译本)](./2015/lets-make-a-compiler.md) 32 | 33 | ### 2014 34 | 35 | * [像外行一样思考,像专家一样实践(修订版)(译本)](./2014/think-like-a-rookie-while-practice-like-a-pro.md) 36 | 37 | 38 | ## 本地查看 39 | 40 | ```shell 41 | $ git clone https://github.com/leungwensen/blog.git $path/to/blog 42 | $ npm install 43 | $ npm start 44 | ``` 45 | 46 | *built by [zfinder](https://github.com/leungwensen/zfinder)* 47 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 文森叔叔的小屋 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 |
36 | 44 |

文森叔叔的小屋

45 |

链接: http://leungwensen.github.io/blog/ 仓库: https://github.com/leungwensen/blog

46 |

博文列表

47 |

2017

48 | 53 |

2016

54 | 62 |

2015

63 | 71 |

2014

72 | 75 |

本地查看

76 |
$ git clone https://github.com/leungwensen/blog.git $path/to/blog
77 | $ npm install
78 | $ npm start
79 | 
80 |

built by zfinder

81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leungwensen-blog", 3 | "title": "leungwensen's blog", 4 | "description": "leungwensen's blog", 5 | "homepage": "http://blog.leungwensen.com/", 6 | "version": "0.1.0", 7 | "author": { 8 | "description": "leungwensen@gmail.com", 9 | "url": "http://leungwensen.com" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/leungwensen/blog/issues" 13 | }, 14 | "engines": { 15 | "node": ">=0.8.0" 16 | }, 17 | "keywords": [ 18 | "leungwensen", 19 | "blog" 20 | ], 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "git@github.com:leungwensen/blog.git" 25 | }, 26 | "scripts": { 27 | "build": "zfinder build", 28 | "start": "zfinder serve" 29 | }, 30 | "dependencies": {}, 31 | "devDependencies": { 32 | "zfinder": "^0.2.10" 33 | } 34 | } 35 | --------------------------------------------------------------------------------