├── 2016 ├── 重新设计 React 组件库 │ ├── select.jpg │ ├── select-label.jpg │ └── 重新设计 React 组件库.md └── 组件库设计实战 - 组件分类、文档管理与打包发布 │ └── 组件库设计实战 - 组件分类、文档管理与打包发布.md ├── 2017 ├── 组件库设计实战 - 复杂组件设计 │ ├── horse.jpg │ ├── carousel.gif │ ├── carousel.jpg │ ├── carousel-long.jpg │ └── 组件库设计实战 - 复杂组件设计.md ├── 服务端渲染与 Universal React App │ ├── architecture.jpg │ └── 服务端渲染与 Universal React App.md ├── 组件库设计实战 - 国际化方案 │ └── 组件库设计实战 - 国际化方案.md └── 前端数据层不完全指北 │ └── 前端数据层不完全指北.md ├── 2018 ├── 写给 Web 开发者的深度学习教程 - 向量化 & 矩阵 │ ├── matrix.jpg │ ├── equation1.png │ ├── equation1.svg │ └── 写给 Web 开发者的深度学习教程 - 向量化 & 矩阵.md ├── 写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化 │ ├── nn.jpg │ ├── cost.jpg │ ├── iris.jpg │ ├── cost2.jpg │ ├── cost3.jpg │ ├── equation1.png │ ├── equation2.png │ ├── equation3.png │ ├── standard.jpg │ ├── equation2.svg │ ├── 写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化.md │ ├── equation3.svg │ └── equation1.svg ├── React v16.3 版本新生命周期函数浅析及升级方案 │ ├── new-lifecycle.jpg │ ├── old-lifecycle.png │ └── React v16.3 版本新生命周期函数浅析及升级方案.md └── 从新的 Context API 看 React 应用设计模式 │ └── 从新的 Context API 看 React 应用设计模式.md ├── 2020 ├── Snowflake 招股书分析.md ├── 谈谈电商赛道的需求侧改革.md ├── 2020 个人投资笔记(上).md └── 如何打造一支业务前台的数据工程团队.md ├── 2021 └── 2020 个人投资笔记(中).md ├── 2022 └── 关于产品国际化的几点思考.md └── README.md /2022/关于产品国际化的几点思考.md: -------------------------------------------------------------------------------- 1 | # 关于产品国际化的几点思考 2 | 3 | -------------------------------------------------------------------------------- /2016/重新设计 React 组件库/select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2016/重新设计 React 组件库/select.jpg -------------------------------------------------------------------------------- /2017/组件库设计实战 - 复杂组件设计/horse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2017/组件库设计实战 - 复杂组件设计/horse.jpg -------------------------------------------------------------------------------- /2017/组件库设计实战 - 复杂组件设计/carousel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2017/组件库设计实战 - 复杂组件设计/carousel.gif -------------------------------------------------------------------------------- /2017/组件库设计实战 - 复杂组件设计/carousel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2017/组件库设计实战 - 复杂组件设计/carousel.jpg -------------------------------------------------------------------------------- /2016/重新设计 React 组件库/select-label.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2016/重新设计 React 组件库/select-label.jpg -------------------------------------------------------------------------------- /2017/组件库设计实战 - 复杂组件设计/carousel-long.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2017/组件库设计实战 - 复杂组件设计/carousel-long.jpg -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 向量化 & 矩阵/matrix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 向量化 & 矩阵/matrix.jpg -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/nn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/nn.jpg -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 向量化 & 矩阵/equation1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 向量化 & 矩阵/equation1.png -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/cost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/cost.jpg -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/iris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/iris.jpg -------------------------------------------------------------------------------- /2017/服务端渲染与 Universal React App/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2017/服务端渲染与 Universal React App/architecture.jpg -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/cost2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/cost2.jpg -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/cost3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/cost3.jpg -------------------------------------------------------------------------------- /2018/React v16.3 版本新生命周期函数浅析及升级方案/new-lifecycle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/React v16.3 版本新生命周期函数浅析及升级方案/new-lifecycle.jpg -------------------------------------------------------------------------------- /2018/React v16.3 版本新生命周期函数浅析及升级方案/old-lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/React v16.3 版本新生命周期函数浅析及升级方案/old-lifecycle.png -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation1.png -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation2.png -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation3.png -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/standard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanWei/blog/HEAD/2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/standard.jpg -------------------------------------------------------------------------------- /2021/2020 个人投资笔记(中).md: -------------------------------------------------------------------------------- 1 | # 2020 个人投资笔记(中) 2 | 3 | > 以下信息仅供参考,不对您构成任何投资建议。 4 | 5 | #### SaaS 6 | ##### 相关股票(7) 7 | * Zoom(ZM) 8 | * Salesforce(CRM) 9 | * Slack(WORK) 10 | * Dropbox(DBX) 11 | * DocuSign(DOCU) 12 | * Snowflake(SNOW) 13 | * Palantir(PLTR) 14 | 15 | #### 芯片及半导体 16 | ##### 相关股票(7) 17 | * 英特尔(INTC) 18 | * AMD(CRM) 19 | * 赛灵思(XLNX) 20 | * 英伟达(NVDA) 21 | * 高通(QCOM) 22 | * 博通(AVGO) 23 | * 台积电(TSM) 24 | 25 | #### 巨头 26 | ##### 相关股票(8) 27 | * Facebook(FB) 28 | * 苹果(AAPL) 29 | * 亚马逊(AMZN) 30 | * Netflix(NFLX) 31 | * 谷歌(GOOG) 32 | * 微软(MSFT) 33 | * 阿里巴巴(BABA|HK 09988) 34 | * 腾讯(HK 00700) 35 | 36 | #### 二线龙头 37 | ##### 相关股票(7) 38 | * 拼多多(PDD) 39 | * Sea(SE) 40 | * 京东(JD|HK 09618) 41 | * 美团(HK 03690) 42 | * 小米(HK 01810) 43 | * 优步(UBER) 44 | * 网易(NTES|HK 09999) 45 | 46 | #### REITs(Real estate investment trust - 房地产投资信托)+ 传统行业 47 | ##### 相关股票(9) 48 | * 西蒙地产(SPG) 49 | * Realty Income(O) 50 | * 卡特彼勒(CAT) 51 | * 宝洁(PG) 52 | * 卡夫亨氏(KHC) 53 | * 可口可乐(KO) 54 | * 麦当劳(MCD) 55 | * 加拿大鹅(GOOS) 56 | * Lululemon(LULU) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blog 2 | ## 目录 3 | 4 | ### 行业观察 5 | #### - [谈谈电商赛道的需求侧改革](https://github.com/AlanWei/blog/issues/13) 6 | #### - [Snowflake 与数据云(Data Cloud)](https://github.com/AlanWei/blog/issues/11) 7 | 8 | ### 投资 9 | #### - [2020 个人投资笔记(上)](https://github.com/AlanWei/blog/issues/14) 10 | ### 工程团队管理 11 | #### - [如何打造一支业务前台的数据工程团队](https://github.com/AlanWei/blog/issues/12) 12 | ### 组件库 13 | #### - [重新设计 React 组件库](https://github.com/AlanWei/blog/issues/1) 14 | #### - [组件库设计实战 - 组件分类、文档管理与打包发布](https://github.com/AlanWei/blog/issues/2) 15 | #### - [组件库设计实战 - 国际化方案](https://github.com/AlanWei/blog/issues/3) 16 | #### - [组件库设计实战 - 复杂组件设计](https://github.com/AlanWei/blog/issues/4) 17 | ### 数据层 18 | #### - [前端数据层不完全指北](https://github.com/AlanWei/blog/issues/5) 19 | #### - [从新的 Context API 看 React 应用设计模式](https://github.com/AlanWei/blog/issues/9) 20 | ### 服务端渲染 21 | #### - [服务端渲染与 Universal React App](https://github.com/AlanWei/blog/issues/6) 22 | ### 机器学习 23 | #### - [写给 Web 开发者的深度学习教程 - 向量化 & 矩阵](https://github.com/AlanWei/blog/issues/7) 24 | #### - [写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化](https://github.com/AlanWei/blog/issues/8) 25 | ### React 相关 26 | #### - [React v16.3 版本新生命周期函数浅析及升级方案](https://github.com/AlanWei/blog/issues/10) -------------------------------------------------------------------------------- /2020/Snowflake 招股书分析.md: -------------------------------------------------------------------------------- 1 | # Snowflake 与数据云(Data Cloud) 2 | 3 | 关注美股的朋友们最近应该都注意到了一家大热的 IPO 公司,那就是 Snowflake。作为云计算领域一只新进的独角兽,Snowflake 在资本市场受到了前所未有的追捧,其中最耀眼的标签就是『股神』巴菲特的加持。要知道老巴可是在 1956 年福特 IPO 后就再也没有参与过新股的发售,这次却通过伯克希尔公司购买了 700 万股的 Snowflake,按照发行价来算市值 8.4 亿美金,而按照截止十月十三日的收盘价计算,这 700 万股的 Snowflake 价值已经飙升到了 17 亿美金,翻了一倍不止。 4 | 5 | Snowflake 到底是一家什么样的公司能够让声称『不懂科技股』的巴菲特都为它破戒呢?让我们看看能不能从 Snowflake 的招股书中找到一些蛛丝马迹。 6 | 7 | 在招股书的第一页,Snowflake 的 slogon 是 MOBILIZING THE WORLD'S DATA,让世界上的数据流通起来。这个口号很好地诠释了云计算对于大数据行业的意义,那就是当所有的数据都存储在云端时,困扰传统企业的数据孤岛(data silos)问题也就不存在了,甚至还能够进一步打破企业之间甚至行业之间的数据壁垒,让各行各业间的数据都流通起来。为了实现这样一个美好的愿景,Snowflake 定义了在云原生(cloud native)时代数据系统的 3 个阶段,即 2014 年的云上数据仓库,亮点是超越本地数仓的强大性能;2019 年的云上数据平台,亮点是数据任务和用户的弹性可扩展;以及 2020 年的数据云,亮点是实现数据的网络效应。 8 | 9 | Snowflake 还从以下 8 个方面对比了自身与传统数据技术之间的优势。 10 | 11 | 1. 传统数据技术不能支持今天动态且多样化的数据需求。原因是传统的数据技术普遍对[半结构化数据](https://en.wikipedia.org/wiki/Semi-structured_data)(semi-structured data)的支持并不十分完善,也很难以一种统一的方式来存储多种多样的数据。而 Snowflake 则通过通用数据集(common data set)的方式解决了不同类型数据的存储问题,极大地提升了系统的稳定性和灵活性。这点可以简单总结为**对不同类型的数据做归一化处理**。 12 | 13 | 2. 传统数据技术不能支持海量的数据存储和查询。这点很好理解,也是云和本地之间最本质的区别,那就是从理论上来讲,本地的资源是有限的而云是无限的。Snowflake 可以根据客户在不同阶段的需要来提供相应的数据服务。 14 | 15 | 3. 传统数据技术不能同时支持众多不同的使用场景和用户。在不同场景下数仓的调优很多时候都是一个取舍的问题,而云端的 Snowflake 则可以动态地调度资源来平衡不同用户和不同场景下对资源的需求,为所有用户提供具有一致性的用户体验。 16 | 17 | 4. 传统数据技术在建设价格方面非常不友好。这点也是云服务的优势,那就是可以帮企业省去整个数据基础设施建设以及后续维护和调优的成本。在此基础上,Snowflake 还能够做到在查询时只访问需要用的数据,从而进一步替企业节省成本。 18 | 19 | 5. 传统数据技术的使用门槛很高。这一点相信做过大数据相关开发的朋友们都深有体会,不仅搭建一套完善的大数据系统的技术门槛很高,在使用时对分析师等非技术人员的门槛也很高。Snowflake 提供了一套简单易用的查询语言并在此基础上做到了按需收费,即用多少付多少的模型,帮助企业降低在使用过程中的成本。 20 | 21 | 6. 传统数据技术的维护成本很高。这点上面也提到过,不过另一个角度是 Snowflake 作为 DaaS(Data as a Service)的提供商,一切产品都是可以直接使用的服务,服务本身的升级迭代和企业的使用是完全隔离的。 22 | 23 | 7. 传统数据技术不能支持跨地区、跨云服务(AWS,Azure,Alibaba Cloud 等)的数据分发和共享。这点是 Snowflake 非常有意思的一个设计,那就是 Snowflake 本身也是架设在云计算平台之上的一个 DaaS,底层打通了 AWS,Azure 等这些基础平台的存储和算力资源,一方面降低了已经在使用如 Redshift 或 Azure SQL 等数仓服务用户的迁移成本,另一方面也让它所承诺的数据云变成了真正意义上的公有云(public cloud),在这个基础上实现跨地区的数据分发自然也是不在话下。 24 | 25 | 8. 传统数据技术不能支持数据分享。虽然现在已经是 2020 年,但确实很多企业内部包括企业之间的数据分享还停留在复制一份线下的数据,以 excel 或 csv 的形式进行分享。这其中当然存在着许多数据安全和管控的问题,但其实这么多年以来一直也没有公司试图去站在一个更高的纬度上去解决这个问题,直到 Snowflake。受益于数据云的概念,所有数据在 Snowflake 上都可以保证有且只有一份单一的来源并安全地分享给第三方。 26 | 27 | 看到这里,相信各位都对于 Snowflake 正在做以及想要做的事情有了一个更深的了解。在这里我想提 2 个从 Snowflake 身上看到的趋势。 28 | 29 | 一是 IaaS 和 SaaS 之间的分野正在越来越清晰,IaaS 服务的核心价值是用云的方式来实现对资源/算力的解放,而 SaaS 服务的核心价值是降低各种企业能力的进入门槛。虽然目前各大云平台都不满足于只做 IaaS,但 SaaS 的确是一个过于广阔的领域,很难出现一家或几家企业垄断了整个 SaaS 市场。尤其是在各个垂直 SaaS 领域蓬勃发展的今天,像 Snowflake 这样垂直领域的佼佼者层出不穷,这也促使各大云平台去更加积极地思考自己的定位以及未来几年的业务打法。另一方面,Snowflake 也给很多新兴的 SaaS 公司提供了一个新的思路,那就是如何借助现有的云平台去打造一家真正意义上**云原生的 SaaS 公司**。Snowflake 很好地实践了不要重复造轮子这样一个工程界的理念,不重头来做数仓的基础设施建设而是把重心放在如何让数仓更易用,这也是 SaaS 能够给客户(企业)提供的最大价值所在,即将许多原先只有巨头才能具备的能力以服务的形式和按需收费的模式,提供给中小型企业。 30 | 31 | 二是数据相关的 SaaS 正在成为新的蓝海。在 IM、文档、视频会议等传统的 SaaS 服务积极抢占市场的同时,更加专业及垂直的企业服务也在逐渐成长为下一个 SaaS 领域的增长点,数据当然是其中非常重要的一部分。不同于传统的类似于像 Oracle 等公司直接卖数据库的模式,新一代的数据 SaaS 公司更多地将目光投向了 DaaS,大家所售卖的不再是一个性能更好的数据库,而是一整套解决企业使用数据的解决方案。在这个解决方案中,有数据库,有数据查询服务,有数据分享服务,也有数据分析服务等等。相信在未来的市场竞争中,各家公司竞争的将不仅仅是谁的数据库更快或更便宜,而是谁能提供真正帮助企业解决经营问题的一揽子解决方案。 -------------------------------------------------------------------------------- /2020/谈谈电商赛道的需求侧改革.md: -------------------------------------------------------------------------------- 1 | # 谈谈电商赛道的需求侧改革 2 | 3 | 2020 年 11 月 12 日,拼多多(NASDAQ: PDD)在盘前发布了 2020 年第三季度的财报。该季度拼多多营收 142.098 亿元,同比增长 89%。归属于拼多多普通股股东的净亏损为 7.847 亿元,去年同期净亏损为 23.35 亿元。非美国通用会计准则(Non-GAAP)下,归属于拼多多普通股股东的净利润为 4.664 亿元,去年同期净亏损为 16.604 亿元,首次实现单季度盈利。随着这份财报的发布,拼多多的股价也在随后的两个交易日内大涨超过 30%,创下了 $155.61 的历史新高。 4 | 5 | 从 2018 年上市就让人『看不懂』的拼多多越来越让更多的人『看不懂』了,这其中也包括我自己。作为原先拼多多坚定的多头,却在 $86.75 就出光了自己所有的筹码,彻彻底底地踏空了这一波超过 70% 的涨幅。微信流量红利、二三线下沉市场、中小商家外溢、百亿补贴,这些贴在拼多多身上的标签真的足以支撑其 1800 亿美金的市值吗?这让许多信奉『价值投资』理念的投资者都只能对拼多多敬而远之。 6 | 7 | 在拼多多出现之前,许多人都断言电商这条赛道已经终结了,不论是现在还是未来,不可能再有第三个挑战者来撼动阿里和京东在这个市场中的地位。这样的断言像极了弗朗西斯·福山在《历史的终结及最后之人》中对人类社会演化的判断,但正如打破历史终结论的不会是另一个更好的西方自由民主制度,打破阿里和京东在电商市场垄断地位的也不会是另一家在供给侧做到极致的公司。 8 | 9 | 淘宝是一个 C 端的 App 吗?我觉得不完全是,起码跟抖音、头条等真正意义上的 C 端 App 比起来,淘宝的 C 端属性并不那么纯粹。对于传统的电商平台而言,他们做的生意其实就是利用平台来抹平买家和卖家之间的信息差,撮合更多的交易。而另一方面,在电商出现之前,人类就已经有了丰富的商业经验(消费是人类的刚需),早年间传统电商需要做的就是把原先只能在线下买到的货搬到线上来而已。基于这样的一个逻辑惯性,让许多传统的电商平台形成了某种对供给侧(商家侧)的路径依赖,认为只要在平台上有足够丰富的货源、足够有竞争力的价格、足够方便的支付渠道和足够快捷的物流体系,就可以吸引来相当的用户,生意也就可以做起来了。这样的逻辑链条对吗?当然对。这样在商业上形成的护城河深吗?当然深。不论是阿里的商家管理还是京东的物流体验,在这些方面想要去跟巨头们竞争那一定是会碰得头破血流的。 10 | 11 | 站在 2020 年末这个时间节点,我们再来对比下三家电商平台的首页设计。 12 | 13 | ### 天猫 14 | 15 | ![天猫](https://s3.ax1x.com/2020/12/13/re8a90.md.jpg) 16 | 17 | ### 京东 18 | 19 | ![京东](https://s3.ax1x.com/2020/12/13/re8Nhq.md.jpg) 20 | 21 | ### 拼多多 22 | 23 | ![拼多多](https://s3.ax1x.com/2020/12/13/re8tNn.md.jpg) 24 | 25 | 对比后我们可以清楚地看到以下两个差别: 26 | 27 | 一是可以使用的功能,相较于两大巨头,拼多多多出了『多多爱消除』、『天天领现金』、『多多果园』、『砍价免费拿』等一系列具备娱乐和社交属性的功能。 28 | 29 | 二是从浏览商品到下单的转化漏斗。在天猫首页,在商品这个维度,用户只能看到一张缩略图,没有价格也没有描述,用户无法点一下就进入商品购买页面,需要先选择一个专区。在京东首页,用户可以看到商品图及价格,没有描述,但点击商品图后进入的也不是该商品的购买页面,而是一个专区。在拼多多首页,有商品图、价格、描述甚至销量,所有影响用户消费决策的信息都在首页就有所体现,用户只需要点一次就可以进入商品购买页面。 30 | 31 | 在之前的市场上一直有种声音说,阿里是天然的流量黑洞,所以需要不断并购可以为其电商业务输送流量的公司(大文娱、本地生活等)。那拼多多的流量是哪来的呢?有人说是从微信来的,这个观点放在 5 年前是成立的,谁也不否认拼多多确实曾经从微信身上狠狠地薅了一把羊毛。但在 2019 年微信一视同仁地封杀了拼多多的外链之后,为什么拼多多却并没有因此一蹶不振,日活依然还在屡创新高呢?一方面如之前分析的 App 首页的区别,拼多多在试图通过游戏的方式来构建自己的私域流量池,给用户更多除了购物外每天打开拼多多的理由。另一方面回顾过去这几年,相信每个人都曾经有过被『拼多多 拼多多 拼得多 省得多』洗脑的经历。在拼多多之前,各大互联网公司都是普遍看不起各种电视广告和节目冠名赞助的,但拼多多身体力行地告诉了各位同行,用这样的方式来做用户增长,获客成本其实极低。此处不得不再 cue 一下黄铮的人生导师段永平,他创造的『小霸王 其乐无穷』也是许多人童年记忆的一部分。 32 | 33 | 转化漏斗这件事就更加不需要赘述了,互联网行业的朋友们应该都知道多点一步所带来的用户流失是惊人的,是做用户增长中的大忌。 34 | 35 | 这时候可能有人还会提另外一个问题,我也想像拼多多那种在首页就给用户展示商品的详细信息,但那样的话我就只能在首页展示 2 件商品,万一展示出来的商品不能激发用户的购买欲望怎么办?又会不会给用户留下一个我这边货品不够丰富的印象? 36 | 37 | 想要回答这个问题,就不得不提到用户增长中另一个关键词,即 A/B 测试。一方面,在大数据和推荐引擎如此发达的今天,如果公司的算法团队做不到在 5 - 10 个商品内给用户推荐出一个他会喜欢的商品,那么你应该考虑的是换一个算法团队,而不是去想如何在有限的空间内再塞 20 个商品进去。另一方面,没有人能预测市场接下来的流行趋势是什么,我们只能通过不断的 A/B 来判断更多的用户最近喜欢什么。首页每一个商品的点击率、转化率、甚至停留时长最终都应该成为这项 A/B 测试的一部分,在对这些数据进行了精细化分析之后,就可以从个人的纬度更精确地为用户推送他可能感兴趣的商品。而站在更高的纬度,在把所有这些数据聚合起来之后,平台就可以反推下游的供应链去生产时下大量消费者喜爱的商品。 38 | 39 | 这也就是我们今天要谈的主题,拼多多未来会不会颠覆电商这个传统赛道仍犹未可知,但由它所掀起的这一场电商需求侧改革是值得所有巨头们重视的。仅仅盯着『多快好省』就可以躺着赚钱的年代快要过去了,市场终究还是由需求侧驱动的。电商平台的客户当然包括商家和消费者,但最终的客户还是消费者。跨境商品丰富货源也好,柔性供应链也罢,最终需要为所有这些埋单的依然是消费者。 40 | 41 | 一家没有真正意义上能够吸引 C 端消费者产品,只有强悍商业化(变现)能力的公司可能会在未来的这个时代遭遇到极大的挑战,传统电商平台不能再以『流量黑洞』和『变现能力强』为借口来逃避自己在『用户增长』方面的短板了。如果仅仅看拼多多还觉得市场形势并没有那么糟糕的话,那么就再看看有着更高日活和巨量内容生产能力的抖音和快手吧,抖快才是真正在电商这片黑暗森林中未来可能对传统电商平台发动降维打击的对手。 42 | 43 | 正如克莱顿·克里斯坦森在《创新者的窘境》中提出的『破坏性创新』所预言的那样,未来能够颠覆传统电商企业的,一定不是另一家在供给侧(商家、供应链)做到登峰造极的公司,巨头们迫切需要对自己的产品进行一次彻彻底底的需求侧改革来应对这一未来可能会发生的困境。 44 | 45 | 当然,传统电商公司这么多年积累下来的服务工厂、商家、品牌的经验并不是没有价值的,但这些价值在未来会更多地投射在云和产业互联网领域,让我们拭目以待。 -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /2016/组件库设计实战 - 组件分类、文档管理与打包发布/组件库设计实战 - 组件分类、文档管理与打包发布.md: -------------------------------------------------------------------------------- 1 | # 组件库设计实战 - 组件分类、文档管理与打包发布 2 | 在上篇[《重新设计 React 组件库》](https://github.com/AlanWei/blog/issues/1)中我们从宏观层面一起探讨了结构自由且数据解耦的 React 组件库应当如何设计,在本文中让我们从具体实践的角度来看如何将这样的设计落地。 3 | 4 | ## 组件分类 5 | 在传统的组件库设计中,组件分类一直都不是一个必选项,大多数人都认为一个组件究竟是属于组件类还是控件类,不过是名字上的不同而已,并没有实际意义。但在将组件代码写法区分为纯函数与 ES6 class 两种之后,我们发现组件的写法同时也代表着组件的类型,这时就可以给予不同组件一个更清晰的定义,分别是: 6 | 7 | * 不含有内部状态的以纯函数写法表示的无交互的纯渲染组件 8 | * 含有内部状态以 ES6 class 写法表示的有交互的智能控件 9 | 10 | 在进行了这样清晰的分类之后,每当我们需要新增一个组件时,我们都可以从是否含有内部状态,是否有交互等几个方面来将其纳入组件或控件,并以此来确定其相应的代码规范。 11 | 12 | 延伸来说,除了基础的组件与控件的区别之外,我们还推荐大家从业务的角度出发再划分出一种新的组件类型,即容器。 13 | 14 | 举例来说,在 [Material Design](https://material.io/guidelines/) 大行其道的今天,应该不会有人对**卡片**这样一种基础的内容展示形式感到陌生。对应到前端组件库中,作为展示内容的骨架,卡片本身应当是一个纯渲染组件,但在将其带入具体的业务场景中后就会发现,卡片本身其实是有状态的,常见的如数据加载中、数据为空、数据错误等。这样一个无交互但含有自身状态的组件无论归于上述的哪个分类都会让人感到奇怪,所以我们又引入了容器这样一个新的分类,专门用来存放卡片这类组件。看到这里,相信聪明的你应该能体会到组件分类的真正意义了,那就是用组件分类这样一种形式来强迫工程师去思考每一个组件的本质,然后再利用 pure render 等方法去优化组件性能。作为离用户最近的一批工程师,前端工程师所应该关心的,除了代码本身之外,用户体验,人机交互等领域方面的经验与知识,也是判断前端工程师是否优秀的另一把标尺。 15 | 16 | 另一方面来讲,我们又可以从容器组件延伸出强依赖数据的组件应当如何设计这样一个更加抽象的问题。从组件库设计的角度来讲,正如上一篇文章中所提到的,不建议将数据获取等逻辑放在组件里去做的。但结合业务场景来说,统一数据获取等逻辑确实是提升业务开发效率的不二选择,这方面的具体实践大家可以参考[琼玖](https://www.zhihu.com/people/xile611)之前的文章[《React实践 - Component Generator》](https://zhuanlan.zhihu.com/p/21386862)。简而言之,使用高阶组件在这里是一个不错的选择。 17 | 18 | 回到代码本身,抛开纯函数组件不谈,我们这里再来讨论一个编写智能组件时经常会踩到的坑。 19 | 20 | 在 React 的生命周期函数中,有一个功能十分强大的函数,那就是 `componentWillReceiveProps`,在这个函数中,我们既可以拿到 `this.props` 又可以拿到 `nextProps`,所以理论上来讲,我们可以在这里利用这些数据对组件做任何逻辑上的变更。另一方面,智能组件一般需要支持木偶与智能两种调用方式,以方便使用者在使用时根据是否需要在业务代码中保存组件状态使用。木偶组件标配的 props 一般为 value 加一个回调函数 onChange,这时组件本身就只需要负责根据接收到的 props 进行渲染。而智能组件的标配 props 一般只需要设置一个 defaultValue,也就是外部只负责定义组件的初始状态,接下来组件自己会根据交互来改变内部状态。这里我们可以通过在 `componentWillReceiveProps` 中同步 props 到 state 的方式来支持两种不同的调用方式,即如果外部直接改变了 `value` 值,那么就将新的 `value` 值同步到组件内部的 state 上,如果外部没有改变 `value` 值,那么就交由组件内部的 state 全权负责组件状态的更新。 21 | 22 | ```javascript 23 | constructor(props) { 24 | super(props); 25 | 26 | this.state = { 27 | value: props.defaultValue, 28 | }; 29 | } 30 | 31 | componentWillReceiveProps(nextProps) { 32 | // sync state to props 33 | if (this.props.value !== nextProps.value) { 34 | this.setState({ 35 | value: nextProps.value, 36 | }); 37 | } 38 | } 39 | 40 | handleChange(value) { 41 | this.setState({ 42 | value, 43 | }); 44 | this.props.onChange(value); 45 | } 46 | 47 | render() { 48 | const { value } = this.state; 49 | return ; 50 | } 51 | ``` 52 | 53 | ## 文档管理 54 | 编写组件库本身并不是最终目的,让更多的人在业务开发中使用起来才是。组件库作为一个自身封装程度较高,内聚性较强的技术项目,开发文档是否足够清晰,完善,也是决定项目成败的另一个关键因素。 55 | 56 | 优秀的组件库文档起码要满足以下两个要求: 57 | 58 | * 属性全覆盖:属性名、具体描述、数据类型、是否有默认值、是否必须等 59 | * 示例丰富 60 | 61 | 属性全覆盖的重要性在这里不再赘述,使用者在不阅读源码的前提下想要了解组件的所有功能,阅读组件文档是唯一的途径。 62 | 63 | 另一方面,由于 React 组件本身是高度可定制的,所以如果开发者不能够提供具体的示例,使用者在使用组件进行一个复杂业务开发时就将因为缺少指导而变得异常痛苦。从代码质量管控的角度来讲,丰富的示例也是对组件单元测试的一次具象。在未来维护组件增加新功能时,示例丰富的好处就将体现得淋漓尽致:当组件新增了一些逻辑后,原先所有的示例都仍能完美运行时,我们也会对新加的这个功能更有信心并避免 regression 的发生。 64 | 65 | ## 打包发布 66 | 作为业务项目的基础依赖,组件库一般都需要打包发布至 npm 以方便业务项目使用。在对组件库进行打包时,为了方便业务项目在具体业务场景下的使用,组件库需要支持以下两种打包方式。 67 | 68 | 第一种打包方式是使用 `webpack` 将所有组件打包成一个文件至 `dist/` 文件夹中: 69 | 70 | ``` 71 | dist/xui.js 72 | dist/xui.css 73 | ``` 74 | 75 | 在业务项目中可以通过 76 | 77 | ``` 78 | import { XXX } from 'xui'; 79 | ``` 80 | 81 | 的方式直接调用相应组件。 82 | 83 | 另一种打包方式是使用 `babel` 将每个组件都分别编译至对应的 `lib/` 文件夹中,并分别编译每个组件的 `CSS` 文件: 84 | 85 | ``` 86 | lib/carousel/index.js 87 | lib/carousel/index.css 88 | lib/input/index.js 89 | lib/input/index.css 90 | ... 91 | ``` 92 | 93 | 在业务项目中可以通过 94 | 95 | ``` 96 | import Carousel from 'xui/lib/carousel'; 97 | import 'xui/lib/carousel/index.css'; 98 | ``` 99 | 100 | 的方式按需调用组件。 101 | 102 | ## 小结 103 | 在本文中,我们主要从组件分类、文档管理、打包发布三个方面阐述了如何将结构自由且数据解耦的 React 组件库落到实处。 104 | 105 | 在下一篇文章中,我们将与大家分享组件库国际化方案,敬请期待。 -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 向量化 & 矩阵/equation1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /2020/2020 个人投资笔记(上).md: -------------------------------------------------------------------------------- 1 | # 2020 个人投资笔记(上) 2 | 3 | > 以下信息仅供参考,不对您构成任何投资建议。 4 | 5 | 2020 年是我真正接触股票投资的第一年,从六月底开仓到现在刚好半年时间。昨天(12 月 18 日)是今年的最后一个四巫日(Quadruple witching day),上周我也对自己的投资组合进行了今年最终的调整,准备迎接接下来的圣诞行情。 6 | 7 | 因为本文涉及到的内容较多,我会分为上、中、下三篇。上、中两篇主要介绍我关注的具体板块及个股,下篇会更多地讲一些形而上的投资心得和对未来的展望。 8 | 9 | ### 总体回顾 10 | 11 | 按照惯例我们先来看一下这半年的总体投资回报率。 12 | 13 | ![roi.png](https://s3.ax1x.com/2020/12/19/rNjxQU.png) 14 | 15 | 借着年底的这波牛市,今年的投资回报率创下了 62.23% 的新高,以此推算的年化回报率大约为 132.06%,计算方式以老虎证券的算法为准。 16 | 17 | 这当然主要归功于今年四月份后美股的整体大牛市,除了九、十两个月出现了小幅度回调外,美股整体还是走出了一波非常强势的行情。虽然今年出现了像新冠肺炎这样大的黑天鹅事件,但在美联储 2 万亿救助法案和长期低利率的宽松货币政策下,美股在经历了三月份十天四次熔断后,又奇迹般地恢复了生机,三大股指(标普、纳斯达克、道琼斯)都在 12 月份创下了历史新高。 18 | 19 | 有人说现在的美股正在经历一段**虚假的繁荣**,这个观点我是赞同的。在全球范围内新冠疫情远未成功控制的情况下,根据国际货币基金组织(IMF)最新的预测,2020 年全球 GDP 的增长率为 -4.4%(2019 年为 +2.8%)。在这样的经济大背景下,屡创新高的美股自然存在着一定水分,但对于擅长造梦的资本市场来说,在某种程度上这样的表现又是合理的,原因主要有以下两点: 20 | 21 | 1. 上文中提到过的也是最核心的推动美股上涨的主因,那就是美联储的经济刺激计划(已经发放的 2 万亿救助法案和正在谈的万亿级别的经济刺激计划)和宽松的货币政策(美联储将维持低利率到 2025 年)。这直接导致市场上的钱变多了,在通胀的强大压力下,不论是债市还是楼市都无法吸纳数量如此庞大的资金,那么股市自然成为了最后的蓄水池。所以我们在判断未来美股走势的时候,一定不能只盯着需求侧即世界经济走势看,还需要分一部分精力在货币的供给侧,两相平衡下来才能做出较为正确的投资决策。 22 | 23 | 2. 疫情在重创世界经济的同时,也加速了人类**日常生活线上化**的进程。不论是在电商、外卖、远程办公、远程医疗等从非必需转向必需的领域,还是其他如线上娱乐、家庭健身等被疫情强烈加持的领域,都是疫情的受益者。所以我们要能够客观地看到疫情并不是一个摧毁一切的撒旦,它对资本市场的影响是一体两面的,很难直接导致市场的整体性崩盘。 24 | 25 | ### 板块分析 26 | 27 | 由于新冠疫情的一体两面性,在投资中对板块甚至个股进行深入的分析就显得尤为重要。下面我们来看下今年我个人比较关注的几个板块,也和各位分享一些对未来趋势的判断。 28 | 29 | #### 电动车 30 | ##### 相关股票(4) 31 | * 特斯拉(TSLA) 32 | * 蔚来(NIO) 33 | * 理想汽车(LI) 34 | * 小鹏汽车(XPEV) 35 | 36 | 电动车是 2020 年股市绕不过去的一个话题。 37 | 38 | 特斯拉年中涨了 730.68%,蔚来 1062.19%,理想 176.26%,小鹏 209.20%,可以说只要你在 2020 年重仓了这其中的任何一支股票,今年的总体回报率都不会太差。分析其原因,环保的大主题自然是推动电动车板块上涨的重要原因,但归根结底还是电将代替汽油成为一种成本更低廉的燃料,以及充电桩会代替加油站成为一种建设和维护成本都更低的出行基础设施。另一方面,国家政策也成为了电动车在推广过程中的一个重要助力,在这方面中国无疑是走在世界前列的,美国在拜登上台后也应该会在政策上有所倾斜。这两方面都为电动车提供了充分的基本面支撑,但却着实也撑不起以特斯拉为例 1330 倍的市盈率,也难怪很多股民朋友开玩笑说『现在的特斯拉不存在市盈率,只有市梦率』。玩笑归玩笑,资本市场既然愿意用真金白银为电动车股票买单,那么背后一定有其逻辑所在,而这个逻辑就是**汽车**会成为未来人类生活『第三空间』的存在。不论是车联网下新一代的操作系统,还是 L5 级别的自动驾驶,这些都是电动车巨头许给投资者们的美好未来。但至于这样的未来离我们还有多远以及如何解决自动驾驶所带来的『道德算法』困境,现在没有人能够给出一个明确的答案。 39 | 40 | 当然,很多投资者也戏称今年的『电动车泡沫』和 17 世纪的『郁金香泡沫』并没有什么不同,本质上不过是一场击鼓传花的游戏。但我还是更愿意给予今年成功押注了电动车领域的投资者更多的尊重,这其中当然有赌徒,但我确实很佩服在 100 块左右买入特斯拉或 5 块左右买入蔚来并坚定持有到今天的朋友。预测未来的趋势是一件很难的事情,2021 年爆火的板块会是哪一个?我们又能否猜中下一个呢? 41 | 42 | #### 泛娱乐 43 | ##### 相关股票(8) 44 | * 哔哩哔哩(BILI) 45 | * 爱奇艺(IQ) 46 | * 腾讯音乐(TME) 47 | * Spotify(SPOT) 48 | * 斗鱼(DOYU) 49 | * 虎牙(HUYA) 50 | * 欢聚集团(YY) 51 | * 迪士尼(DIS) 52 | 53 | 泛娱乐是我个人非常感兴趣的一个板块。在现在高强度的工作压力下,人们对于娱乐的需求也越来越旺盛。不论是视频,音频亦或是直播,游戏等泛娱乐领域,未来一定会诞生新的巨头。在这里我们先按下腾讯、Netflix、Sea、网易等巨头不表,先来看一下在泛娱乐细分领域的挑战者们。 54 | 55 | 从诞生第一天起,有人就把 B 站称为中国的 YouTube,但在我看来,B 站未来可能会成为一个超越 YouTube 的存在。相较于 YouTube,B 站最为关键的竞争优势在于其对社区氛围的打造与引导,让 B 站诞生了目前已经成功出圈的弹幕文化(弹幕延伸了视频创作的生命周期,视频 + 弹幕才是未来 UGC 视频的终极形态),并在很大程度上让 B 站具有了一定的在视频网站中非常稀缺的社交属性。当然,B 站未来也面临着许多挑战,其中最大的挑战来自于商业化。不论是游戏代理还是二次元电商,这些变现模式都有些受制于 B 站与生俱来的基因。这方面如果 B 站能够借鉴 YouTube 的成功经验,把自己定位为一个 UGC 内容的生产平台,成功转向广告 + 会员的模式,B 站的未来必定不可限量。另一方面,也非常建议 B 站认真考虑下国际化。B 站今天在中国的影响力可能不亚于 YouTube 在美国的影响力,但 YouTube 不止在北美提供服务。如果 B 站真的把 YouTube 看做是竞争对手的话,不做国际化是打不赢这场仗的。不要觉得我在讲一个天方夜谭的故事,看看抖音加持下的 TikTok 把 Facebook 逼到了哪般田地,B 站为什么打不赢 YouTube 呢? 56 | 57 | 我个人作为一个狂热的音乐爱好者,一直都坚信视频并不能代替音乐,所以长期仍然非常看好音乐这条赛道。但我预测音乐这条赛道在未来的 5 年内可能会掀起一轮新的变革,而这场变革的领导者很有可能是字节跳动。当传统音乐公司还在把 AI 算法推荐当做护城河的时候,抖音神曲已经悄悄占据了 QQ 音乐的热门榜单。抖音作为一个短视频应用,却成为了许多好歌被用户听到的第一来源,反过来给 QQ 音乐带量,帮助 QQ 音乐建立热门歌单,这本身就是一件很魔幻现实主义的事。不论是抖音,还是 TikTok,亦或者 Resso,它们所带来的全新的音乐发现机制、对原创音乐人的扶持以及以技术方式降低音乐制作门槛的尝试,都是对腾讯音乐和 Spotify 的一次次提醒。当音乐的生产模式和分发模式发生下一次根本性的变化时,所有类工具的应用都会被碾得粉碎。 58 | 59 | 直播作为去年非常火热的一个领域,今年出现了两个比较大的利空。一个是直播带货让电商和内容平台都发现了直播这个模式本身的价值,让这个本就不太宽敞的赛道在今年显得尤为拥挤。另一个则是更为严格的监管,这里无需多言。这些可能也是促成斗鱼和虎牙在今年选择合并的一个原因。虽然个人长期仍然非常看好斗鱼和虎牙本身的商业模式和盈利能力,但在合并事宜尚未尘埃落定之前,斗鱼和虎牙的股价应该是很难翻身了,再加上年底新的反垄断相关的消息,腾讯表示真是『屋漏偏逢连夜雨,船迟又遇打头风』。而 YY 作为直播行业的鼻祖,虽然最近遭遇了浑水的做空,但长期来讲还是期待海外的 BIGO 和 Likee 业务能够在地缘政治不确定的大环境下有所作为,给其他想要出海的中国企业打个样。 60 | 61 | 最后我们再来谈一下娱乐业的鼻祖,迪士尼。毫无疑问,新冠疫情对于迪士尼的冲击是巨大的,其旗下主题乐园、体验与产品(Parks, Experience and Products)部分的收入几乎腰斩。但令人敬佩的是,迪士尼凭借着新的流媒体服务 Disney+ 在第一年就揽下了 7000 万用户,着实让 Netflix 吓了一跳。拥有 Disney+、Hulu、ESPN+ 的迪士尼确实有了和 Netflix 一较高下的实力,也从另一个侧面证明了在内容行业,最重要三件事就是 IP、IP 还是 IP。内容、文化和娱乐,本质上都是影响力经济。在版权保护越来越健全的今天,好的有影响力的内容不愁赚不到钱,尤其是在 PGC(Professionally Generated Content)领域,谁能够源源不断地生产新的 IP,谁就是最终的赢家。从这个角度来讲,也就不难解释为什么国内的三大视频平台(腾讯视频、爱奇艺、优酷)都过得如此艰难了。毕竟不论是鬼吹灯还是创造 101,奇葩说亦或是这就是街舞,在米老鼠、公主、小熊维尼、皮克斯、漫威宇宙和星球大战面前,两者同时提起来大家都会觉得前者是在蹭热度而已。当然,这一切不是优爱腾的错,毕竟支撑美国在冷战结束后霸权的,不仅仅是航母舰队和金融体系,更是其强大的文化输出能力,吾辈当自强。 62 | 63 | ### 预告 64 | 65 | 在下一篇文章中,我们将重点分析 SaaS(Software as a service - 软件即服务)、芯片及半导体、巨头(FAANG + AT + 二线龙头)、REITs(Real estate investment trust - 房地产投资信托)+ 传统行业等四大板块,敬请期待。 -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化.md: -------------------------------------------------------------------------------- 1 | # 写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化 2 | 在[上一篇文章](https://github.com/AlanWei/blog/issues/7)中,我们介绍了如何将以数组形式存储的数据转换为适合进行深度学习模型训练的大型矩阵,以及矩阵的相关运算。向量化是一切矩阵运算的基础,从编码的角度来讲,可以帮助模型在每一次训练时省下一次与样本数量相等的 for 循环的计算量,即模型可以处理的数据结构将由一维数组升级为二维数组(矩阵)。 3 | 4 | ## 统计学与机器学习 5 | 在 [Tensorflow](https://www.tensorflow.org/),[PyTorch](http://pytorch.org/) 等机器学习框架逐渐成为主流的今天,再回过头去谈论统计学好像已经有些过时了。正如现在数据岗位的从业者更愿意把自己称为数据科学家而不是数据分析师那样,统计学好像已经在强大的机器学习面前败下阵来。但事实真的是这样吗?对于所有的数据问题,机器学习都是一种比统计学更为优秀的选择吗? 6 | 7 | 在回答这个问题之前,让我们先来回答一个更为基础的问题,对于一个分类问题来说,是数据集越小越好分类,还是数据集越大越好分类? 8 | 9 | 从常识的角度来讲,当然是数据集越小越好分类,因为假设极端情况下只有两个样本,那么一定能在空间中划出一条线将二者分开,但是如果有几万个,甚至几百万个样本,好像就很难找出这样一条分割线了。 10 | 11 | 但另一方面,许多机器学习结合大数据的优秀案例就摆在我们面前,它们都证明了只有数据样本足够大,才能够训练出一个较为准确的模型。 12 | 13 | 让我们以上一篇专栏中提到的 Iris 数据集为例。 14 | 15 | ![](./iris.jpg) 16 | 17 | 从上图中我们可以看出,setosa 品种和 versicolor,virginica 品种之间的差别非常大,而后两个品种之间相对来讲就显得非常难以区分。 18 | 19 | 假设我们现在想要训练一个深度学习模型来判断一朵花是不是 versicolor 品种,你会发现无论如何调整学习率,训练次数,隐藏层神经元个数,都很难得到一个收敛的结果,常见的损失函数的图像如下图所示: 20 | 21 | ![](./cost.jpg) 22 | 23 | > 学习率:0.01,训练次数:1000,隐藏层神经元:10 24 | 25 | 这证明了在 Iris 数据集的原始空间中,versicolor 与 virginica 几乎是不可分的(有重叠的部分),也就是说在原始空间中,无论你划一条多么复杂的曲线都无法做到一边是 versicolor,另一边是 virginica。 26 | 27 | 面对这样的结果,当初笔者在使用 [deeplearning-js](http://www.deeplearning-js.com/) 训练第一个模型时内心是崩溃的,无数次地怀疑是代码某个地方出错导致了模型不收敛。 28 | 29 | 直到后来与一位数据分析领域的朋友聊天时,才了解到原来神经网络在小样本量的数据集上表现并不好。因为神经网络算法跳过了传统统计方法中特征选择的过程,直接暴力地在样本空间进行搜索,试图找出一条曲线将样本分类。这在大样本量,超高纬度的数据集上,效果是非常好的,因为这些数据集本身信息量就非常丰富,简单来讲这样的数据集在超高维空间中样本是可分的。 30 | 31 | 再举一个可能不太恰当的生活中的例子。要将两百万个苹果分成两类,如果你能准确地捕捉到这两百万个苹果的所有信息,那么你一定能够利用所有的这些信息,找到一些特征将这两百万个苹果分类,换句话说想要找到两百万个非常相似的苹果几乎是不可能的,所以它们一定是可分的。但如果是将二十个苹果进行分类,那么最快速的方法是找一个苹果专家(领域知识),从某几个特定的维度上对这二十个苹果进行分类,但也有可能在极端情况下,这二十个苹果就是非常相似导致完全不可分。 32 | 33 | 上述例子中的苹果专家就代表着传统的统计学,统计学中有许多如主成分分析([PCA](https://en.wikipedia.org/wiki/Principal_component_analysis))等帮助数据降维的方法。对于传统统计学来说,高纬度的数据集是无法处理的,光是数据降纬的过程就十分漫长,而且在数据降纬的过程中,还会丢失掉许多原始数据集中包含的信息。 34 | 35 | 机器学习则恰好相反,借助于 GPU 超高的运算效率,它试图在一个超高维空间捕捉所有可利用的信息,并最终拟合出一个可用的模型。于是我们也就不难理解,为什么机器学习在处理非结构化数据(如图片,文字,语音等)方面表现比结构化数据更为优秀,其根本原因就是这些非结构化数据本身携带的信息量就非常丰富。如一张 256*256 像素的图片,其本身就是一个 65,536 维的数据,更别提现在一个普通手机就可以拍出动辄几千乘以几千像素的照片了。 36 | 37 | 到这里,我们应该可以对于开始提出的那个问题,即数据集越小越好分类,还是数据集越大越好分类,给出一个较为准确的答案,那就是在你的运算能力范围之内,一定是数据集越大越好分类,每个样本的维度越多越好分类。 38 | 39 | ## 数据标准化 40 | 在了解了统计学与机器学习的异同之后,我们能不能从统计学中借鉴一些优秀的数据处理方法来优化深度学习模型呢?当然可以。 41 | 42 | **数据标准化**就是其中一个非常有效的方法,目前 [deeplearning-js](http://www.deeplearning-js.com/) 支持两种数据标准化的方式: 43 | 44 | [Feature scaling](https://github.com/AlanWei/deeplearning-js/blob/master/src/preprocess/normalization/minMaxNormalization.ts): 45 | 46 | ![](./equation1.svg) 47 | 48 | [Standard score](https://github.com/AlanWei/deeplearning-js/blob/master/src/preprocess/normalization/zscore.ts): 49 | 50 | ![](./equation2.svg) 51 | 52 | 数据标准化的意义又是什么呢?我们来看下图: 53 | 54 | ![](./standard.jpg) 55 | 56 | 简单来说,数据标准化就是在不了解数据各个维度之间的关系时,强行将各个维度拉到相同权重的一种统计学方法。从空间变换的角度来讲,数据标准化可以将扁平空间变换为类圆形空间,不仅可以消除不同维度的权重对目标函数的影响,还可以加快后续梯度下降的速度,避免其在某一巨大的平面上以非常缓慢的速度下降。 57 | 58 | 让我们来对比一下未开启数据标准化和开启数据标准化的 Iris 数据集在 deeplearning-js 中的表现: 59 | 60 | 未开启数据标准化: 61 | 62 | ![](./cost2.jpg) 63 | 64 | > 未开启数据标准化,学习率:0.01,训练次数:500,隐藏层神经元:200 65 | 66 | 模型将所有样本都判定为不是 versicolor,即完全无法区分三种类别。 67 | 68 | 开启数据标准化: 69 | 70 | ![](./cost3.jpg) 71 | 72 | > 开启数据标准化,学习率:0.01,训练次数:500,隐藏层神经元:200 73 | 74 | 在 150 个样本中,模型可以正确判断 142 个,即只有小部分的 versicolor 与 virginica 仍无法被正确区分。我们可以通过增加训练次数的方式去继续逼近 100% 的准确率,但从第一张 Iris 数据集的分布图我们可以看出,一部分 versicolor 与 virginica 的样本几乎是重合的,所以即使在进行了空间变换后,想要区分他们的难度依然很大。 75 | 76 | 我们使用的隐藏层神经元个数也证明了这点,想要用数量很少的神经元个数去区分 versicolor 与 virginica 几乎是不可能的,这也说明即使在变换后的四维空间中也要划一条非常复杂的曲线才可以将这两个品种区分开来。 77 | 78 | ## 参数初始化 79 | 在对数据集进行了预处理之后,让我们正式开始搭建第一个深度学习模型。 80 | 81 | 如下图所示,深度学习模型就是由这样一层一层的神经网络构成的: 82 | 83 | ![](./nn.jpg) 84 | 85 | 用公式来表示: 86 | 87 | ![](./equation3.svg) 88 | 89 | 以 Iris 数据集为例,我们的输入层是一个 [4, 150] 的矩阵,那么第一个隐藏层的权重值矩阵 W 就需要是一个 [x1, 4] 的矩阵,使得两个矩阵可以进行点乘计算,这里的 x1 就是第一个隐藏层的神经元个数。经过第一个层之后,输出变为了一个 [x1, 150] 的矩阵,也就是说下一个隐藏层的权重矩阵形状应当是 [x2, x1],列数与前一层相同,行数取决于这一层的神经元个数。以此类推,因为每一层隐藏层的列数都等于前一层的行数,所以我们在初始化参数时就只需要输入当前层的神经元个数即可。 90 | 91 | 另一方面,因为偏置 b 与 W·A 之间是加法计算,那么就意味着 b 的形状需要与 W·A 相同。 92 | 93 | 从几何空间的角度来说,如果 W 是空间中一条复杂曲线, b 则可以帮助这条曲线进行平移,使得最终的曲线不必要必须经过原点,从而对数据集进行更准确的拟合。 94 | 95 | 以 deeplearning-js 中 `initializeParameters` API 为例: 96 | 97 | ```javascript 98 | const initialParameters = initializeParameters([{ 99 | size: trainSet.input.shape[0], // 输入层神经元个数等于原始输入数据集的行数,即输入数据维度数 100 | }, { 101 | size: 10, // 第一个隐藏层神经元个数 102 | activationFunc: 'relu', // 第一个隐藏层激活函数 103 | }, { 104 | ... // 第 N 个隐藏层 105 | }, { 106 | size: trainSet.output.shape[0], // 输出层神经元个数等于原始输出数据集的行数,即输出数据维度数 107 | activationFunc: 'sigmoid', // 输出层激活函数 108 | }], 109 | 0, // 初始化权重值时使用的平均数,默认为 0 110 | 1, // 初始化权重值时使用的方差,默认为 1 111 | 0.01, // 初始化权重值时使用的比例,默认为 1,建议使用 0.01 防止数值膨胀得过快 112 | ); 113 | 114 | /* 115 | initialParameters = { 116 | W1: Array2D, 117 | b1: Array2D, 118 | W2: Array2D, 119 | b2: Array2D, 120 | ... 121 | Wl: Array2D, // 第 l 层 122 | bl: Array2D, // 第 l 层 123 | } 124 | */ 125 | ``` 126 | 127 | ## 小结 128 | 在了解了如何对数据进行必要的预处理并根据需要设计深度学习模型结构后,是时候将数据输入到我们的模型中,让模型真正运转起来了。在下一篇文章中,我们将讲到正向传播,以及隐藏层常用的激活函数,敬请期待。 -------------------------------------------------------------------------------- /2020/如何打造一支业务前台的数据工程团队.md: -------------------------------------------------------------------------------- 1 | # 如何打造一支业务前台的数据工程团队 2 | 3 | 作为一个『二进宫』的阿里人,这个月刚好是入职 Lazada 的两周年。虽然两次与阿里结缘都是在数据团队(DT),但这次从数据中台到业务前台,从个人贡献者到 TL,团队和身份的转变让我对个人的发展及未来要做的事情都有了更深入的了解和认识,这里也和大家分享一下在业务前台做数据工程的经验与思考。 4 | 5 | 作为一名前端开发出身的工程师,16 年在 DT 时对于数据团队在整个企业中扮演的角色其实是没有很深的体感的,只知道自己做的是面向淘系商家的数据产品『生意参谋』。而在 18 年加入 Lazada 数据团队后,作为当时大团队里的第一个工程同学,才第一次有机会一窥数据团队的全貌。 6 | 7 | ## 数据团队的组成 8 | 9 | 从最粗的粒度上来讲,数据团队可以分为 4 大部分,即数据采集,数据仓库,数据服务和数据产品。4 个部分自下而上地融通出了一条条数据管道,让通过各个渠道采集过来的明细数据最终成为了驱动业务决策和运营的数据洞见(Insight)。所以从本质上来讲,数据团队并不生产数据,因为数据其实是来源于真实用户与业务系统的日常交互(投放、浏览、点击、购买、订单、物流等)。数据团队更像是数据的搬运工,并在搬运的过程中对数据进行适当加工,让海量、零散的数据最终可以成为业务决策的关键因子来影响下一轮真实用户与业务系统的交互方式,从而形成数据闭环。 10 | 11 | ## 工程团队的定位 12 | 13 | 在数据采集,数据仓库,数据服务和数据产品这 4 个部分中,工程团队很显然承担的是『数据服务』这一层。数据服务作为数据工程团队的交付物,如果我们再把『服务』这个词具象化,又可以拆解为数据接口,数据报表,数据产品和数据大屏这 4 类,分别对应工程师(上下游系统)、分析师、业务运营/商家、媒体,这 4 种不同的用户。 14 | 15 | 在搞清楚了我们是谁以及我们的客户是谁这 2 个关键问题之后,下一个要解决的问题就是如何服务好他们,并在此基础上沉淀出一定的技术积累。 16 | 17 | ## 怎么做数据接口 18 | 19 | 数据接口作为最基础的数据流通方式,一直以来都在各个业务系统之间扮演着非常重要的角色。 20 | 21 | 在许多人眼中,数据团队不就是提供数据的吗?只要能拿到想要的数据,一张数仓中的表和一个接口之间到底有什么区别呢?其实这个问题也从一个侧面代表了我在过去两年中的很多焦虑,那就是对于一个数据团队来说,工程研发到底重不重要?数据团队是不是只需要数据研发就够了,剩下的事情都交给下游的业务团队去做是不是也可以?关于这个问题,通过过去两年的实践,答案是越来越清晰的,那就是对于数量众多的下游系统而言,数据团队能够提供一个统一、安全、易用的接口层是一件非常重要的事情。 22 | 23 | 对于工程团队而言,统一性一直都是一个非常重要的话题。在工程界,各种各样的语言、数据库、框架等层出不穷,一方面带来了繁荣的生态,但另一方面也带来了许多系统之间交互的一致性问题。从这个方面来讲,其实数据仓库是解决得更好的,不论数据生产的过程中涉及到了多少技术栈,最终产出的结果就是一张张的宽表,这大大降低了比如像算法和数据研发之间的协作成本,解决了不同系统模块之间数据流转的问题。但数仓再往下,到了数据库这层,我们就又会发现许多的不一致性。MySQL,HBase,AnalyticDB 等等这些不同的数据库解决方案分别适用于不同的业务场景,也都有各自的成本考量,很难有一个 DB 可以解决所有问题。而这也就是集团内部在数据服务层的拳头产品 OneService 所要解决的问题,那就是让所有的后端应用都可以有一个统一的能够面向各种数据库的接口层。 24 | 25 | 而我们作为业务前台的工程团队,关注更多的还是业务逻辑,基于 OneService 我们为下游的兄弟团队提供了许多如指标查询服务,商家分层查询服务,商家私域用户标签查询服务等等。这些业务属性较强的数据服务同样需要一个统一的平台来管理,让调用方式,返回格式,版本迭代等信息持续保持对下游系统透明。这就是我们在过去一年中建设的 Lazada 数据统一网关层(Data Gateway),一方面解决对下游透明的问题,另一方面也在团队内部收敛如 BUC 鉴权、请求代理、消息推送等通用的后端能力。在解决了团队内部服务管理的问题之后,今年也会全面地将统一网关层升级为 Lazada 数据开放平台(Data Open Platform),让更多还未通过具体业务需求找到我们的团队更方便地了解 Lazada 数据团队已有的数据服务能力,避免同一份数据在不同团队之间的重复建设,也让和下游团队之间签订的 SLA 可以具体落地到平台上更细粒度的监控指标上去,更好地调配日常及大促期间的机器资源,提高系统稳定性。另外,通过这样一个开放平台统一的服务注册和申请机制,数据团队也可以方便地管控所有对外暴露的数据服务,做到所有操作和调用安全可追溯。 26 | 27 | ## 怎么做数据报表 28 | 29 | 数据报表作为数据接口和数据产品之间承上启下的一层,在日常的业务决策中扮演着非常重要的角色。 30 | 31 | 在报表搭建这方面,集团内部是有非常成熟的解决方案的,有了这样的珠玉在前,业务前台如何能够在报表搭建方面更进一步,是我们一直在思考的问题。时间回到 2 年前,当时集团内部的搭建平台还没有彻底得国际化,对于本地的同学来讲,使用起来门槛非常高。在明确了要解决『国际化』这样一个问题之后,团队利用业务开发之外的时间研发了一款新的搭建平台,以支撑国际业务的数据产品搭建。除了平台本身的国际化外,我们还创新地提出了『搭建产品国际化』这样一个目标,希望能够做到一次搭建,支持所有语言。 32 | 33 | 除了『国际化』之外,在数据报表方面另一个我们一直在思考的问题是:数据报表和业务系统之间的边界在哪里?关于这个问题,我们给出的答案是,业务系统是可以直接**使用**的数据报表。 34 | 35 | 长期以来,业务系统和数据报表之间一直存在着相当的割裂,一方面是因为生产环境中运行的业务系统和每天大量新增的数据报表从数量上来讲就不在一个量级。业务系统和数据报表之间也并不存在着严格意义上的对应关系,很难有人可以明确地指出说哪几张报表是用来指导运营在某一个业务场景下做决策的。久而久之,大家更习惯地是将这样的决策链路笼统地归于行业经验的范畴,即相信业务侧的同学在看到了特定的数据后就知道去哪个业务系统里进行相应的操作。但从逻辑上来讲,这并不是一个非常站得住脚的说法,因为报表数量多本身也是一件需要去治理的事情,企业更希望的是能够在纷繁复杂的业务中抽象出统一的几张或几十张报表来长期地指导业务运营。临时的取数和分析需求,从技术的角度来讲应该收敛在自助取数/分析的平台上,而不应该形成一批数量庞大却事实上在使用过一次后就鲜有人问津的报表。 36 | 37 | 回到技术的解决方案上来,在明确了要解决『能使用』和『方便与业务系统之间打通』这 2 个问题后,我们发现『微前端』的解决方案其实非常适合用来解决数据报表的问题。这里的『微前端』指的并不是现在许多团队在投入的基于微前端架构的应用框架,而是能够适应各种前端框架和渲染环境的报表组件。换句话说,我们要做的不是一个可以摆放任意东西的架子,而是一个个可以放在任何架子上的实物。另外,在现有的搭建平台基本都解决了搭建页面内部组件可交互的问题后,为了实现『能使用』这一目标,我们还需要为所有搭建平台支持的组件添加一个对外的出口,让其具备可以和外界 API 进行基于 POST 请求通讯的能力。最后,虽然『微前端』听起来像是一个全新的概念,但在落地时却又很容易简单地实现为所有组件都发一个包含所有依赖的独立 NPM 包。如果给这样简单粗暴的解决方案套上一个『微前端』的概念,确实有些名不副实。个人认为『微前端』其实是对整个前端开发流程的一次挑战,需要各个前端团队跳出原来以**应用**为最小粒度开发的模式,转向以**组件**为最小粒度。这要求配套的单组件研发平台,多组件调试平台,组件发布平台和公共依赖管理平台等多方面都需要达到一定的要求,才能够真正享受到『微前端』带来的红利。否则光是公共依赖重复打包导致代码膨胀和组件/嵌入系统平行升级困难这两个问题,都会让许多团队不得不得退回到应用粒度的开发模式。 38 | 39 | 回到数据报表的问题上来,相信在赋予了静态数据报表『易嵌入』和『可使用』这两大新特性后,它所能带来的业务价值也一定会有质的突破。 40 | 41 | ## 怎么做数据产品 42 | 43 | 数据产品作为数据域中与传统工程应用最为接近的一种交付物,与核心的业务系统一样,对系统的稳定性和扩展性都有着很高的要求。对比传统的 C 端应用,数据产品在技术上也有其一定的特点。不同于重并发轻计算的 C 端应用,数据产品更聚焦在大数据量下的快速复杂计算。又因为在电商的场景下,数据产品服务的对象更多是商家侧,所以从服务用户的上限来讲,对系统并发的要求不及 C 端应用那么严苛。 44 | 45 | 谈起数据产品,就不得不提到其中最原子的组成部分:数据指标。数据指标是一切数据产品的基础,如何快速准确地计算并管理数以千计的数据指标及其衍生指标,是每一个数据工程团队首先要解决的问题。好在集团内部也已经有了较为成熟的类 FaaS 的逻辑引擎解决方案,极大地降低了团队在最初落地时的成本。 46 | 47 | 虽然前面提到了数据产品在并发方面并不像 C 端那样动辄要服务上亿的用户,但如 Lazada 生意参谋这样的数据产品,每天也有大量的商家会登录查看自己的经营数据,应用的 QPS 也随着业务的发展,从 18 年到现在翻了 10 倍不止。既然要做对外的服务,大到应用的基础架构,上下游的依赖梳理,小到应用的发布管控,页面级别的渐进式更新迭代,都是真正考验一个工程团队内功的事情。如果说过去两年,在基础架构方面我们有哪些事情做对了的话,那一定是在业务需求交付不间断的前提下,保持了前后端核心框架和库的渐进式升级。 48 | 49 | 以前端为例,在过去的两年中,团队经历了从 Class Component 到 Hooks,从 redux-saga 到 laz-fetch,从 AntD v3 到 v4 等一系列的底层依赖升级并沉淀出了团队内部状态管理、代码审核、自动发布等一整套的工具链,没有陷入到大型应用在维护了几年之后底层架构慢慢腐烂,只能推倒重来的困境里去。 50 | 51 | 一个技术团队的好坏,团队 TL 作为一号位是责无旁贷的。这里有两点经验想和大家分享,一是如何在不影响业务交付的前提下对应用进行持续的技术升级。我认为这是一个『意愿大于能力』的事情,也是对技术 TL 追求卓越的真实考验。如果一个技术团队只有做业务的能力,而不具备持续升级技术的能力,那这个技术团队注定是走不远的。第二个经验是技术 TL 应该如何看待『重复造轮子』。诚然,如上文中提到的,团队在过去两年中的技术积累一点都不 fancy,在相关领域或许也都有更成熟的解决方案。但我认为,对于一个超过 10 人的技术团队,在开发规范和工具链上是需要给予团队成员一定自由度的。一方面是因为总有些特殊的业务需求,是开源或其他团队现有工具覆盖不到的,在团队底层工具链中保持一定的自主性和灵活性,是能够更好服务业务的基础。另一方面这也可以给团队成员带来更多成长的机会,让他们有机会去造一些重复却又具有一定创新的轮子。我们反对的,是毫无新意的重复造轮子,对于面向解决具体业务域问题的重复造轮子,技术 TL 还是要给与适当的鼓励与引导,这也是在保护未来团队有机会能够做出一些自主创新的土壤。 52 | 53 | ## 怎么做数据大屏 54 | 55 | 讲完了数据服务,数据报表和数据产品,我们再来谈谈数据大屏。很长时间以来,人们都对数据大屏的价值有着比较深的误解,认为数据大屏不过是大促或某些特定时间拿出来炫技的东西,在业务价值方面和上面提到的这 3 种交付物之间存在着明显的差距。从我个人的角度来讲,数据大屏存在的意义其实用一个词就可以概括,那就是『讲故事』。好的数据大屏可以为企业营造出一个特殊的『场』,让其更好地向外界传达公司的愿景,使命以及在达成这些目标过程中公司的进展。诚然,这种对于人心和市场情绪的影响很难量化,但『讲故事』的能力确实在整个人类的进化历程中都扮演了非常重要的角色,而大屏恰好是对『讲故事』最好的辅助。 56 | 57 | 过去 2 年中,团队在大屏方面的投入不多,但也落地了以大航海为背景的第一块讲述东南亚电商故事的数据大屏,之后有机会可以再跟大家详细介绍。 58 | 59 | ## 写在最后 60 | 61 | 以上只是个人对于数据工程团队的一点粗浅的认识,实际工作中的情况要比文章中描述得复杂许多,在这里也是抛砖引玉,欢迎对数据工程感兴趣的朋友们一起讨论。 -------------------------------------------------------------------------------- /2017/组件库设计实战 - 国际化方案/组件库设计实战 - 国际化方案.md: -------------------------------------------------------------------------------- 1 | # 组件库设计实战 - 国际化方案 2 | 放眼全球,中国整体的互联网技术实力毫无疑问仅次于美国并领先剩余所有的国家一大截。但如果我们非要找出一个中国互联网公司做得不够优秀的地方,那么产品国际化一定是其中之一。虽然我们也拥有诸如 AliExpress,天猫国际等成功案例,但不得不说大部分中国公司在选择出海后,都没有能够收获到与预期相匹配的回报。这其中原因自然很多,然而缺乏一套可以平台化,产品化的通用国际化方案一直都是其中一个非常重要的原因。 3 | 4 | 曾经笔者也天真地认为国际化不过是几个 json 文件的键值对匹配,但在深入了解了一些产品的国际化需求后,笔者才意识到要做一套好的国际化方案并没有那么简单。 5 | 6 | ## 服务端国际化 7 | 对于前端工程师而言,国际化所要面临的第一个挑战就是,并不是所有的数据都可以在前端做国际化。常见的例子如电商类产品的货品或商家信息,这些都是有强更新需求,需要存储在后端数据库中,通过产品后台进行更新的。如果一个商品要销往美国,德国,法国,西班牙,泰国,印度尼西亚,而运营人员又只想维护一套以中文为基准的商品信息,那么这类数据的国际化我们就需要将其做在服务端。 8 | 9 | 我们当然可以麻烦后端工程师帮助我们根据每个请求的域名或 HTTP header 中的 `content-language` 来返回不同表中的翻译,但如果你是一位致力于向全栈方向发展的前端工程师,不妨可以尝试将国际化这一需求服务化,使用 Node.js 来封装一个国际化中间件,在每个请求返回前对其返回值进行翻译处理。 10 | 11 | 因为每个公司的技术架构不同,我们暂且略过技术细节不表。但我们需要知道的是,相较于前端国际化,后端接口的国际化其实更为关键与重要。因为这涉及到我们是否能将我们的核心数据以用户可理解的语言展现出来,而国际化也绝不仅仅是将几个字符串翻译为对应语言那样简单。 12 | 13 | ## 哪些数据需要做国际化 14 | 在讨论具体的国际化方案之前,我们首先要明确一个问题,那就是产品中的哪些数据是需要做国际化的。 15 | 16 | 简而言之,除去后端返回的数据,所有在前端渲染的单词,语句,以及嵌套在其中的数据,都需要做相应的国际化。对应到代码层面,需要保证代码中没有任何一行硬编码的字符串与符号。不论是大到一个区块标题,还是小到一个确认按钮的文案,所有的展示信息都需要做国际化。 17 | 18 | ## 键值对匹配与多语言支持 19 | 回到前端,让我们从最简单的国际化场景说起。 20 | 21 | 例如下拉列表输入框中的“选择”占位符,假设我们需要同时将其翻译为英文与法文,首先我们需要引入两个语言文件: 22 | 23 | ```javascript 24 | // en-US.json 25 | { 26 | "web_select": "Select" 27 | } 28 | 29 | // fr-FR.json 30 | { 31 | "web_select": "Sélectionner" 32 | } 33 | ``` 34 | 35 | 并提供一个全局的 `localeUtil.js`,支持传入语言类型与 key 值,并返回相应的翻译。 36 | 37 | 这里提供两点最佳实践。 38 | 39 | 一是将不同语言的翻译存在独立的 json 文件中。虽然我们可以使用嵌套的数据结构将所有翻译都存储在一个 locale.json 里面,但考虑到生产环境中语言文件一般都是按需加载的,所以根据不同的语言存在对应的独立的的 json 文件中显然是一个更好的选择。 40 | 41 | 二是同一语言中 key 值的命名,同样不建议采取嵌套的结构。扁平化的语言文件可读性更强,取值时的效率也更高,同时也可以使用下划线来区别不同的层级,如 `web_homepage_banner_title`,即`平台_页面_模块_值`,当然具体情况也可以按需调整。 42 | 43 | ## 模板匹配与条件运算符 44 | 了解了最简单的场景,我们再来考虑一个复杂些的用例。 45 | 46 | 在显示商品价格时,为了可扩展性等多方面的考虑,后端在设计表结构时,是不会将商品价格直接存储为字符串的,而是拆分为货币符号(`string` 类型)及价格(`float` 类型)。而在前端显示时,我们经常会遇到要将其渲染为一句促销语的场景,如: 47 | 48 | ```text 49 | 2017年9月1日前购买,只需100元。 50 | ``` 51 | 52 | 对于时间类数据的国际化方案,我们这里先暂时按下不表,有兴趣的同学可以研究一下 [moment.js](https://momentjs.com/) 的实现,moment.js 也是目前前端届日期国际化的代表。 53 | 54 | 由于100元是一个动态的变量,所以我们的 `localeUtil.js` 还需要支持传入变量,这里一个常用的调用可以为: 55 | 56 | ```javascript 57 | localeGet( 58 | 'en-US', // locale 59 | 'web_merchantPage_item_promotion', // key 60 | { currency: item.currency, promoPrice: item.promoPrice }, // variable 61 | ); 62 | ``` 63 | 64 | 语言文件中的模板可以为: 65 | 66 | ```json 67 | "web_merchantPage_item_promotion": "Before YYYY/MM/DD, purchase at {currency} {price}." 68 | ``` 69 | 70 | 另一个常见的场景为英文名词的单复数问题,这里我们选择通过条件运算符的思路来解: 71 | 72 | ```text 73 | 优惠将于3天后结束。 74 | ``` 75 | 76 | ```json 77 | "web_merchantPage_item_promotion_condition": "Promotion will end in {count, =1{# day} other{# days}}", 78 | ``` 79 | 80 | ## 数据国际化 81 | 除去日期,货币外,数字也是字符串之外另一个国际化的难点,我们来看下面这个例子。 82 | 83 | ```text 84 | 阿里巴巴向印度尼西亚电商网站 Tokopedia 注资11亿美金。 85 | Alibaba leads $1.1b investment in Indonesia’s Tokopedia. 86 | ``` 87 | 88 | 这里我们需要将“11亿美金”翻译为“$1.1b”,为了达到这一目的,我们首先需要在各个语言文件中建立对应语言的基础单位 mapping,如: 89 | 90 | ```json 91 | // zh-CN 92 | "hundred": "百", 93 | "thousand": "千", 94 | "ten_thousand": "万", 95 | "million": "百万", 96 | "hundred_million": "亿", 97 | "billion": "十亿", 98 | 99 | // en-US 100 | "hundred": "hundred", 101 | "thousand": "thousand", 102 | "thousand_abbr": "k", 103 | "million": "million", 104 | "million_abbr": "m", 105 | "billion": "billion", 106 | "billion_abbr": "b", 107 | ``` 108 | 109 | 然后我们需要实现一个可以将浮点数进行纯数字与单位转换的函数,返回纯数字与所使用语言的单位 key 值: 110 | 111 | ```javascript 112 | function formatNum(num, isAbbr = false) { 113 | ... 114 | return { 115 | number: number, // 1.1 116 | unit: unit, // "billion_abbr" 117 | } 118 | } 119 | ``` 120 | 121 | 接着就可以调用 localeGet 来获得相应的翻译: 122 | 123 | ```javascript 124 | localeGet( 125 | 'en-US', 126 | 'news_tilte', 127 | { 128 | number: 1.1, 129 | unit: localeGet('billion_abbr'), 130 | currency: localeGet('currency_symbol'), 131 | }, 132 | ) 133 | ``` 134 | 135 | 语言文件中的模板如下: 136 | 137 | ```json 138 | // zh-CN 139 | "news_tilte": "阿里巴巴向印度尼西亚电商网站 Tokopedia 注资{number}{unit}{currency}。" 140 | 141 | // en-US 142 | "news_tilte: "Alibaba leads {currency}{number}{unit} investment in Indonesia's Tokopedia." 143 | ``` 144 | 145 | 在整个过程中,我们可以抽象出两种解决问题的思路。 146 | 147 | 一是拆分并抽象出基础数据,如单位等。 148 | 149 | 二是灵活运用模板与变量,将其调整为最符合当地用户阅读习惯的翻译。 150 | 151 | 类似的思想也可以推广到处理日期,小数,分数,百分数等。 152 | 153 | ## React 下的国际化方案 154 | 正如前文中所提到的,按需加载语言文件是国际化方案中必要的一环。简而言之,我们可以在项目的入口文件中加载所需的语言文件,但考虑到整体项目的统一性,我们最好可以将语言文件挂载在全局 redux store 下的一个分支,以使得每个页面都可以通过 props 方便地进行取值。而且,在 redux store 的层面加载语言文件,可以保证所有页面使用的都是同一份语言文件,后续也不需要在 `localeGet` 函数中传入具体的 `locale` 值。 155 | 156 | 示例代码如下: 157 | 158 | ```javascript 159 | import enUS from 'i18n/en-US.json'; 160 | 161 | function updateIntl(locale = 'en-US', file = enUS) { 162 | store.dispatch({ 163 | type: 'UPDATE_INTL', 164 | payload: { 165 | locale, 166 | file, 167 | }, 168 | }); 169 | } 170 | ``` 171 | 172 | 这样我们就可以方便地将语言文件挂载在 redux store 的一个分支下: 173 | 174 | ```javascript 175 | const mapStateToProps = (state) => ({ 176 | intl: state.intl, 177 | }); 178 | 179 | // usage 180 | localeGet(this.props.intl, 'web_select'); 181 | 182 | // with defaultValue to prevent undefined return 183 | localeGet(this.props.intl, 'web_select', 'Select'); 184 | ``` 185 | 186 | ## 其他 187 | 除了上述提到的这些问题之外,在生产环境中我们还需要注意以下两点: 188 | 189 | * HTML 转义字符 190 | * 特殊语言的 unicode 转码,如简体中文,繁体中文,泰语等 191 | 192 | 正如开篇时提到的,国际化是一个通用的系统性工程,以上提到的这些点也难免挂一漏万,更多的最佳实践还需要在实际开发工作中持续提炼,总结,沉淀。 193 | 194 | ## 小结 195 | 对于任何一家希望开拓国际市场的公司来说,产品国际化都是一个刚需。从技术人员的角度来讲,我们当然可以满足于一个 Node.js 中间件或一个前端的 npm 包来通用地解决这一问题。但事实上,我们还可以再向前一步,那就是将国际化这个服务做成一个完整的 SaaS 产品,这方面成功的案例如:[OneSky](https://www.oneskyapp.com/)。 196 | 197 | OneSky 所提供的额外功能,如云端存储,多文件类型支持,多人实时翻译协作等,每一个功能单拿出来都又是一个新的领域,而这也正是**服务产品化**的难点所在。 198 | 199 | 举例来说,前文中提到的国际化方案,都是默认所有翻译工作已经完成且 json 化完毕,可以直接 import 到项目中使用,而这也就是技术人员经常会陷入的一个思维盲区。翻译数量庞大的语言文件本身就是一件非常困难的事情,如何让身处世界各地的非技术背景的翻译人员进行协作并方便生产环境中的产品实时更新语言文件,这些问题都只有在把国际化服务做成一个成熟的商业产品之后才会被考虑到。 200 | 201 | 事实上,目前在各大互联网公司中,**技术服务产品化**已经成为了一股不可阻挡的趋势,许多技术出身的工程师都已经开始意识到一套只有技术人员才能理解并使用的解决方案是不够的,只有将这些“高深莫测”的技术服务产品化,傻瓜化,才能够打开一片更大的战场,使技术真正服务于商业产品并在现实世界中产生更大的价值。 -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 向量化 & 矩阵/写给 Web 开发者的深度学习教程 - 向量化 & 矩阵.md: -------------------------------------------------------------------------------- 1 | # 写给 Web 开发者的深度学习教程 - 向量化 & 矩阵 2 | 3 | ## 前言 4 | 在这个科技发展日新月异的时代,行业的宠儿与弃儿就如同手掌的两面,只需轻轻一翻,从业者的境遇便会有天翻地覆的改变。 5 | 6 | 人工智能作为近两年来业界公认的热门领域,不同于之前火热的移动端开发或前端开发,其距离传统软件开发行业之远,入门门槛之高,都是以往不曾出现过的,这也让许多希望能够终身学习,持续关注行业发展的软件工程师们望而却步。 7 | 8 | 在我们进一步讨论阻止传统软件工程师向人工智能领域转型的障碍之前,让我们先来明确几个名词的定义: 9 | 10 | * 人工智能:以机器为载体展现出的人类智能,如图像识别等传统计算机无法完成的工作 11 | * 机器学习:一种实现人工智能的方式 12 | * 深度学习:一种实现机器学习的技术 13 | 14 | 那么到底是哪些障碍阻止了传统软件工程师进入人工智能领域呢? 15 | 16 | * 数学:不同于后端,前端,移动端等不同领域之间的区别,人工智能,或者说我们接下来将要重点讨论的深度学习,是一门以数学为基础的科学。学习它的前置条件,不再是搭建某一个开发环境,了解某一门框架,而是需要去理解一些诸如矩阵,反向传播,梯度下降等数学概念。 17 | * 生态:很久以来,学术界都以 Python 作为其研究的默认语言,创造了如 [NumPy](http://www.numpy.org/),[Matplotlib](https://matplotlib.org/) 等一系列优秀的科学计算工具。而对于终日与用户界面打交道的 Web 开发者来说,一系列基础工具的缺乏直接导致了哪怕是建立起来一个最基础的深度学习模型都异常困难。 18 | 19 | 为了解决上面提到的这两个障碍,笔者使用 TypeScript 以零依赖的方式初步完成了一个基于 JavaScript 的深度学习框架:[deeplearning-js](http://www.deeplearning-js.com/),希望可以以 Web 开发者熟悉的语言与生态为各位提供一种门槛更低的深度学习的入门方式,并将以**写给 Web 开发者的深度学习教程**这一系列文章,帮助各位理解深度学习的基本思路以及其中涉及到的数学概念。 20 | 21 | ## 整体架构 22 | ```text 23 | src/ 24 | ├── activationFunction // 激活函数 25 | │ ├── index.ts 26 | │ ├── linear.spec.ts 27 | │ ├── linear.ts 28 | │ ├── linearBackward.spec.ts 29 | │ ├── linearBackward.ts 30 | │ ├── relu.spec.ts 31 | │ ├── relu.ts 32 | │ ├── reluBackward.spec.ts 33 | │ ├── reluBackward.ts 34 | │ ├── sigmoid.spec.ts 35 | │ ├── sigmoid.ts 36 | │ ├── sigmoidBackward.spec.ts 37 | │ ├── sigmoidBackward.ts 38 | │ ├── softmax.spec.ts 39 | │ ├── softmax.ts 40 | │ ├── softmaxBackward.spec.ts 41 | │ └── softmaxBackward.ts 42 | ├── costFunction // 损失函数 43 | │ ├── crossEntropyCost.spec.ts 44 | │ ├── crossEntropyCost.ts 45 | │ ├── crossEntropyCostBackward.spec.ts 46 | │ ├── crossEntropyCostBackward.ts 47 | │ ├── index.ts 48 | │ ├── quadraticCost.spec.ts 49 | │ ├── quadraticCost.ts 50 | │ ├── quadraticCostBackward.spec.ts 51 | │ └── quadraticCostBackward.ts 52 | ├── data // 数据结构:矩阵 & 标量 53 | │ ├── Array2D.spec.ts 54 | │ ├── Array2D.ts 55 | │ ├── Scalar.spec.ts 56 | │ ├── Scalar.ts 57 | │ └── index.ts 58 | ├── index.ts 59 | ├── math // 计算:矩阵计算函数 & 生成随机数矩阵 & 生成零矩阵 60 | │ ├── add.spec.ts 61 | │ ├── add.ts 62 | │ ├── divide.spec.ts 63 | │ ├── divide.ts 64 | │ ├── dot.spec.ts 65 | │ ├── dot.ts 66 | │ ├── index.ts 67 | │ ├── multiply.spec.ts 68 | │ ├── multiply.ts 69 | │ ├── randn.spec.ts 70 | │ ├── randn.ts 71 | │ ├── subtract.spec.ts 72 | │ ├── subtract.ts 73 | │ ├── transpose.spec.ts 74 | │ ├── transpose.ts 75 | │ ├── zeros.spec.ts 76 | │ └── zeros.ts 77 | ├── model // 模型:初始化参数 & 正向传播 & 反向传播 & 更新参数 78 | │ ├── Cache.ts 79 | │ ├── backPropagation.ts 80 | │ ├── forwardPropagation.ts 81 | │ ├── index.ts 82 | │ ├── initializeParameters.spec.ts 83 | │ ├── initializeParameters.ts 84 | │ ├── train.ts 85 | │ └── updateParameters.ts 86 | ├── preprocess // 数据预处理:数据标准化 87 | │ ├── index.ts 88 | │ └── normalization 89 | │ ├── index.ts 90 | │ ├── meanNormalization.spec.ts 91 | │ ├── meanNormalization.ts 92 | │ ├── rescaling.spec.ts 93 | │ └── rescaling.ts 94 | └── utils // 帮助函数:数据结构转换 & 矩阵广播 95 | ├── broadcasting.spec.ts 96 | ├── broadcasting.ts 97 | ├── convertArray1DToArray2D.ts 98 | ├── convertArray2DToArray1D.ts 99 | └── index.ts 100 | ``` 101 | 102 | 作为一个专注于深度学习本身的框架,deeplearning-js 只负责构建及训练深度学习模型,使用者可以使用提供的 API 在任意数据集的基础上搭建深度学习模型并获得训练后的结果,具体的例子各位可以参考 [Logistic regression](http://www.deeplearning-js.com/demos/logistic)。 103 | 104 | 我们将学习率,迭代次数,隐藏层神经元个数等这些[超参数](https://en.wikipedia.org/wiki/Hyperparameter)暴露给终端用户,deeplearning-js 会自动调整模型,给出不同的输出。基于这些输出,我们就可以自由地使用任意图表或可视化库来展现模型训练后的结果。 105 | 106 | 另外,大家在阅读本系列文章的同时,建议配合着 deeplearning-js 的[源码](https://github.com/AlanWei/deeplearning-js)一起阅读,相信这样的话,你将会对深度学习到底在做一件什么样的事情有一个更感性的认识。 107 | 108 | ## 向量化 109 | 不同于其他的机器学习教程,我们并不希望在一开始就将大量拗口的数学名词及概念灌输给大家,相反,我们将从训练深度学习模型的第一步数据处理讲起。 110 | 111 | 让我们以学术界非常著名的 [Iris](https://en.wikipedia.org/wiki/Iris_flower_data_set) 数据集为例。 112 | 113 | 现在我们拥有了 150 个分别属于 3 个品种的鸢尾属植物的花萼长度,宽度及花瓣长度,宽度的样本数据,目的是训练一个输入任意一个鸢尾属植物的花萼长度,宽度及花瓣长度,宽度,判断它是否是这 3 个品种中的某一个品种,即逻辑回归。 114 | 115 | 虽然我们的最终模型是输入任意一个样本数据得到结果,但我们在训练时,并不希望每次只能够输入一个样本数据,而是希望一次性地输入所有样本数据,得到训练结果与实际结果的差值,然后使用反向传播来修正这些差异。 116 | 117 | 于是我们就需要将多个样本数据组合成一个矩阵,如下图所示: 118 | 119 | ![](./matrix.jpg) 120 | 121 | 在将数据向量化后,我们才有了处理大数据集的能力,即在整个数据集上而不是在某个数据样本上训练模型。这也是为什么在深度学习领域,GPU 比 CPU 要快得多的原因。在训练深度学习模型时,所有的计算都是基于矩阵的,于是并行计算架构(处理多任务时计算时间等于最复杂任务的完成时间)的 GPU 就要比串行计算架构(处理多任务时计算时间等于所有任务运行时间的总和)的 CPU 快得多。 122 | 123 | 细心的读者可能会观察到上图中的一个数据样本中的不同维度的数据是竖排列的,这与传统数组中数据的横排列方式恰好相反,即我们需要将 124 | 125 | ```javascript 126 | [5.1, 3.5, 1.4, 0.2] 127 | ``` 128 | 129 | 转换为 130 | 131 | ```javascript 132 | [ 133 | [5.1], 134 | [3.5], 135 | [1.4], 136 | [0.2], 137 | ] 138 | ``` 139 | 140 | 细心的读者可能又会问了,如 Iris 数据集,为什么一定要将初始数据转换为 4 行 150 列的矩阵,用方便处理的 150 行 4 列的矩阵不可以吗? 141 | 142 | 对于这个问题有以下两方面的考虑。在接下来输入数据与隐藏层做矩阵点乘时 143 | 144 | ![](./equation1.svg) 145 | 146 | 隐藏层矩阵(W)的列数需要等于输入层(A)的行数,所以为了减少不必要的计算量,我们希望输入层的行数尽可能得小,于是我们将数据样本的维度数与样本数量进行对比,不难得出在绝大多数情况下,数据样本的维度数都远远小于样本数量这个结论。另一方面,在点乘之后,结果矩阵的列数将等于输入层的列数,也就是说如果我们希望我们的输出是一个 [X, 150] 的矩阵,输入层就需要是一个 [4, 150] 的矩阵。 147 | 148 | 那么如何快速地在原始数据集与使用数据集之间进行这样的转换呢?这就涉及到矩阵的一个常用运算,矩阵转置了。 149 | 150 | ## 矩阵 151 | 说起矩阵,它的许多奇怪的特性,如转置,点乘等,想必是许多朋友大学时代的噩梦。在这里我们不谈具体的数学概念,先尝试用几句话来描述一下矩阵及它的基础运算。 152 | 153 | 从最直观的角度来讲,确定一个矩阵需要哪些信息?一是矩阵的形状,即坐标系(空间),二是矩阵在这个坐标系下各个维度上的值(位置)。 154 | 155 | * 矩阵(Array2D):N 维空间中的一个物体,在每一维度上都有其确定的位置 156 | * 矩阵相加(add):在某一维度或多个维度上对原物体进行拉伸 157 | * 矩阵相减(subtract):在某一维度或多个维度上对原物体进行裁剪 158 | * 矩阵相乘(multiply):基于原物体的某一个原点对原物体进行等比放大 159 | * 矩阵相除(divide):基于原物体的某一个原点对原物体进行等比缩放 160 | * 矩阵转置(transpose):基于原物体的原点对原物体进行翻转 161 | * 矩阵点乘(dot):对原物体进行左边矩阵所描述的位置转换,即移动 162 | 163 | 在 deeplearning-js 中我们使用二维数组的数据结构来表示矩阵,对于上述运算的具体代码实现各位可以参考 [Array2D](https://github.com/AlanWei/deeplearning-js/blob/master/src/data/Array2D.ts)。 164 | 165 | 一个简单的数据转换的例子如下: 166 | 167 | ```javascript 168 | function formatDataSet(dataset: Array) { 169 | const datasetSize = dataset.length; 170 | let inputValues: Array = []; 171 | 172 | map(dataset, (example: { 173 | "sepalLength": number, 174 | "sepalWidth": number, 175 | "petalLength": number, 176 | "petalWidth": number, 177 | "species": string, 178 | }) => { 179 | const input: any = omit(example, 'species'); 180 | inputValues = inputValues.concat(values(input)); 181 | }); 182 | 183 | const input = new Array2D( 184 | [datasetSize, inputValues.length / datasetSize], 185 | inputValues, 186 | ).transpose(); 187 | 188 | return input; 189 | } 190 | ``` 191 | 192 | ## 小结 193 | 在理解了数据向量化及矩阵的概念后,相信大家已经可以将大样本量,以数组形式存储的数据转换为适合进行深度学习模型训练的大型矩阵了,接下来让我们从如何初始化参数开始,一步步搭建我们的第一个深度学习模型。 -------------------------------------------------------------------------------- /2018/写给 Web 开发者的深度学习教程 - 数据标准化 & 参数初始化/equation1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /2018/React v16.3 版本新生命周期函数浅析及升级方案/React v16.3 版本新生命周期函数浅析及升级方案.md: -------------------------------------------------------------------------------- 1 | # React v16.3 版本新生命周期函数浅析及升级方案 2 | 一个月前,React 官方正式发布了 v16.3 版本。在这次的更新中,除了前段时间被热烈讨论的[新 Context API](https://zhuanlan.zhihu.com/p/33925435) 之外,新引入的两个生命周期函数 `getDerivedStateFromProps`,`getSnapshotBeforeUpdate` 以及在未来 v17.0 版本中即将被移除的三个生命周期函数 `componentWillMount`,`componentWillReceiveProps`,`componentWillUpdate` 也非常值得我们花点时间去探究一下其背后的原因以及在具体项目中的升级方案。 3 | 4 | ## componentWillMount 5 | ### 首屏无数据导致白屏 6 | 在 React 应用中,许多开发者为了避免第一次渲染时页面因为没有获取到异步数据导致的白屏,而将数据请求部分的代码放在了 `componentWillMount` 中,希望可以避免白屏并提早异步请求的发送时间。但事实上在 `componentWillMount` 执行后,第一次渲染就已经开始了,所以如果在 `componentWillMount` 执行时还没有获取到异步数据的话,页面首次渲染时也仍然会处于没有异步数据的状态。换句话说,组件在首次渲染时总是会处于没有异步数据的状态,所以不论在哪里发送数据请求,都无法直接解决这一问题。而关于[提早发送数据请求](https://gist.github.com/bvaughn/89700e525ff423a75ffb63b1b1e30a8f),官方也鼓励将数据请求部分的代码放在组件的 `constructor` 中,而不是 `componentWillMount`。 7 | 8 | 另一个常见的 `componentWillMount` 的用例是在服务端渲染时获取数据,因为在服务端渲染时 `componentDidMount` 是不会被调用的。针对这个问题,笔者这里提供两种解法。第一个简单的解法是将所有的数据请求都放在 `componentDidMount` 中,即只在客户端请求异步数据。这样做可以避免在服务端和客户端分别请求两次相同的数据(`componentWillMount` 在客户端渲染时同样会被调用到),但很明显的缺点就是无法在服务端渲染时获取到页面渲染所需的所有数据,所以如果我们需要保证服务端返回的 HTML 就是用户最终看到的 HTML 的话,我们可以将每个页面的数据获取逻辑单独抽离出来,然后一一对应到相应的页面,在服务端根据当前页面的路由找到相应的数据请求,利用链式的 Promise 在渲染最终的页面前就将数据塞入 redux store 或其他数据管理工具中,这样服务端返回的 HTML 就是包含异步数据的结果了。 9 | 10 | ### 事件订阅 11 | 另一个常见的用例是在 `componentWillMount` 中订阅事件,并在 `componentWillUnmount` 中取消掉相应的事件订阅。但事实上 React 并不能够保证在 `componentWillMount` 被调用后,同一组件的 `componentWillUnmount` 也一定会被调用。一个当前版本的例子如服务端渲染时,`componentWillUnmount` 是不会在服务端被调用的,所以在 `componentWillMount` 中订阅事件就会直接导致服务端的内存泄漏。另一方面,在未来 React 开启异步渲染模式后,在 `componentWillMount` 被调用之后,组件的渲染也很有可能会被其他的事务所打断,导致 `componentWillUnmount` 不会被调用。而 `componentDidMount` 就不存在这个问题,在 `componentDidMount` 被调用后,`componentWillUnmount` 一定会随后被调用到,并根据具体代码清除掉组件中存在的事件订阅。 12 | 13 | ### 升级方案 14 | 将现有 `componentWillMount` 中的代码迁移至 `componentDidMount` 即可。 15 | 16 | ## componentWillReceiveProps 17 | ### 更新由 props 决定的 state 及处理特定情况下的回调 18 | 在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 `componentWillReceiveProps` 中判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但很多情况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。 19 | 20 | 在新版本中,React 官方提供了一个更为简洁的生命周期函数: 21 | 22 | ```javascript 23 | static getDerivedStateFromProps(nextProps, prevState) 24 | ``` 25 | 26 | 一个简单的例子如下: 27 | 28 | ```javascript 29 | // before 30 | componentWillReceiveProps(nextProps) { 31 | if (nextProps.translateX !== this.props.translateX) { 32 | this.setState({ 33 | translateX: nextProps.translateX, 34 | }); 35 | } 36 | } 37 | 38 | // after 39 | static getDerivedStateFromProps(nextProps, prevState) { 40 | if (nextProps.translateX !== prevState.translateX) { 41 | return { 42 | translateX: nextProps.translateX, 43 | }; 44 | } 45 | return null; 46 | } 47 | ``` 48 | 49 | 乍看下来这二者好像并没有什么本质上的区别,但这却是笔者认为非常能够体现 React 团队对于软件工程深刻理解的一个改动,即 **React 团队试图通过框架级别的 API 来约束或者说帮助开发者写出可维护性更佳的 JavaScript 代码**。为了解释这点,我们再来看一段代码: 50 | 51 | ```javascript 52 | // before 53 | componentWillReceiveProps(nextProps) { 54 | if (nextProps.isLogin !== this.props.isLogin) { 55 | this.setState({ 56 | isLogin: nextProps.isLogin, 57 | }); 58 | } 59 | if (nextProps.isLogin) { 60 | this.handleClose(); 61 | } 62 | } 63 | ``` 64 | 65 | ```javascript 66 | // after 67 | static getDerivedStateFromProps(nextProps, prevState) { 68 | if (nextProps.isLogin !== prevState.isLogin) { 69 | return { 70 | isLogin: nextProps.isLogin, 71 | }; 72 | } 73 | return null; 74 | } 75 | 76 | componentDidUpdate(prevProps, prevState) { 77 | if (!prevState.isLogin && this.props.isLogin) { 78 | this.handleClose(); 79 | } 80 | } 81 | ``` 82 | 83 | 通常来讲,在 `componentWillReceiveProps` 中,我们一般会做以下两件事,一是根据 props 来更新 state,二是触发一些回调,如动画或页面跳转等。在老版本的 React 中,这两件事我们都需要在 `componentWillReceiveProps` 中去做。而在新版本中,官方将更新 state 与触发回调重新分配到了 `getDerivedStateFromProps` 与 `componentDidUpdate` 中,使得组件整体的更新逻辑更为清晰。而且在 `getDerivedStateFromProps` 中还禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 `getDerivedStateFromProps` 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。 84 | 85 | ### 升级方案 86 | 将现有 `componentWillReceiveProps` 中的代码根据更新 state 或回调,分别在 `getDerivedStateFromProps` 及 `componentDidUpdate` 中进行相应的重写即可,注意新老生命周期函数中 `prevProps`,`this.props`,`nextProps`,`prevState`,`this.state` 的不同。 87 | 88 | ## componentWillUpdate 89 | ### 处理因为 props 改变而带来的副作用 90 | 与 `componentWillReceiveProps` 类似,许多开发者也会在 `componentWillUpdate` 中根据 props 的变化去触发一些回调。但不论是 `componentWillReceiveProps` 还是 `componentWillUpdate`,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 `componentDidMount` 类似,`componentDidUpdate` 也不存在这样的问题,一次更新中 `componentDidUpdate` 只会被调用一次,所以将原先写在 `componentWillUpdate` 中的回调迁移至 `componentDidUpdate` 就可以解决这个问题。 91 | 92 | ### 在组件更新前读取 DOM 元素状态 93 | 另一个常见的 `componentWillUpdate` 的用例是在组件更新前,读取当前某个 DOM 元素的状态,并在 `componentDidUpdate` 中进行相应的处理。但在 React 开启异步渲染模式后,render 阶段和 commit 阶段之间并不是无缝衔接的,也就是说在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在 94 | `componentDidUpdate` 中使用 `componentWillUpdate` 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。 95 | 96 | 为了解决上面提到的这个问题,React 提供了一个新的生命周期函数: 97 | 98 | ```javascript 99 | getSnapshotBeforeUpdate(prevProps, prevState) 100 | ``` 101 | 102 | 与 `componentWillUpdate` 不同,`getSnapshotBeforeUpdate` 会在最终的 render 之前被调用,也就是说在 `getSnapshotBeforeUpdate` 中读取到的 DOM 元素状态是可以保证与 `componentDidUpdate` 中一致的。虽然 `getSnapshotBeforeUpdate` 不是一个静态方法,但我们也应该尽量使用它去返回一个值。这个值会随后被传入到 `componentDidUpdate` 中,然后我们就可以在 `componentDidUpdate` 中去更新组件的状态,而不是在 `getSnapshotBeforeUpdate` 中直接更新组件状态。 103 | 104 | 官方提供的一个例子如下: 105 | 106 | ```javascript 107 | class ScrollingList extends React.Component { 108 | listRef = null; 109 | 110 | getSnapshotBeforeUpdate(prevProps, prevState) { 111 | // Are we adding new items to the list? 112 | // Capture the scroll position so we can adjust scroll later. 113 | if (prevProps.list.length < this.props.list.length) { 114 | return ( 115 | this.listRef.scrollHeight - this.listRef.scrollTop 116 | ); 117 | } 118 | return null; 119 | } 120 | 121 | componentDidUpdate(prevProps, prevState, snapshot) { 122 | // If we have a snapshot value, we've just added new items. 123 | // Adjust scroll so these new items don't push the old ones out of view. 124 | // (snapshot here is the value returned from getSnapshotBeforeUpdate) 125 | if (snapshot !== null) { 126 | this.listRef.scrollTop = 127 | this.listRef.scrollHeight - snapshot; 128 | } 129 | } 130 | 131 | render() { 132 | return ( 133 |
134 | {/* ...contents... */} 135 |
136 | ); 137 | } 138 | 139 | setListRef = ref => { 140 | this.listRef = ref; 141 | }; 142 | } 143 | ``` 144 | 145 | ### 升级方案 146 | 将现有的 `componentWillUpdate` 中的回调函数迁移至 `componentDidUpdate`。如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 `getSnapshotBeforeUpdate`,然后在 `componentDidUpdate` 中统一触发回调或更新状态。 147 | 148 | ## 小结 149 | 最后,让我们从整体的角度再来看一下 React 这次生命周期函数调整前后的异同: 150 | 151 | #### Before 152 | ![](./old-lifecycle.png) 153 | 154 | #### After 155 | ![](./new-lifecycle.jpg) 156 | 157 | 在第一张图中被红框圈起来的三个生命周期函数就是在新版本中即将被移除的。通过上述的两张图,我们可以清楚地看到将要被移除的三个生命周期函数都是在 render 之前会被调用到的。而根据原来的设计,在这三个生命周期函数中都可以去做一些诸如发送请求,setState 等包含副作用的事情。在老版本的 React 中,这样做也许只会带来一些性能上的损耗,但在 React 开启异步渲染模式之后,就无法再接受这样的副作用产生了。举一个 Git 的例子就是在开发者 commit 了 10 个文件更新后,又对当前或其他的文件做了另外的更新,但在 push 时却仍然只 push 了刚才 commit 的 10 个文件更新。这样就会导致提交记录与实际更新不符,如果想要避免这个问题,就需要保证每一次的文件更新都要经过 commit 阶段,再被提交到远端,而这也就是 React 在开启异步渲染模式之后要做到的。 158 | 159 | 另一方面,为了验证个人的理解及测试新版本的稳定性,笔者已经将个人负责的几个项目全部都升级到了 React 16.3 并根据上述提到的升级方案替换了所有即将被移除的生命周期函数。目前,所有项目在生产环境中都运行良好,没有收到任何不良的用户反馈。 160 | 161 | 当然,以上的这些生命周期函数的改动,一直要到 React 17.0 中才会实装,这给广大的 React 开发者们预留了充足的时间去适应这次改动。但如果你是 React 开源项目(尤其是组件库)的维护者的话,不妨花点时间去详细了解一下这次生命周期函数的改动。因为这不仅仅可以帮助你将开源项目更好地升级到 React 的最新版本,更重要的是可以帮助你提前理解即将到来的异步渲染模式。 162 | 163 | 同时,笔者也相信在 React 正式开启异步渲染模式之后,许多常用组件的性能将很有可能迎来一次整体的提升。进一步来说,配合异步渲染,许多现在的复杂组件都可以被处理得更加优雅,在代码层面得到更精细粒度上的控制,并最终为用户带来更加直观的使用体验。 -------------------------------------------------------------------------------- /2017/前端数据层不完全指北/前端数据层不完全指北.md: -------------------------------------------------------------------------------- 1 | # 前端数据层不完全指北 2 | 不知不觉间时间已经来到了 2017 年末尾。 3 | 4 | 在过去一年中,关于前端数据层的讨论依然在持续升温。不论是数据类型层面的 TypeScript,Flow,PropTypes,应用架构层面的 MVC,MVP,MVVM,还是应用状态层面的 Redux,MobX,RxJS,都各自拥有一批忠实的拥趸,却又谁都无法说服别人认同自己的观点。 5 | 6 | 关于技术选型上的讨论,笔者一直所持的态度都是求同存异。在讨论上述方案差异的文章已汗牛充栋的今天,不如让我们暂且放缓脚步,回头去看一下这些方案所要解决的共同的问题,并试图给出一些最简单的解法。 7 | 8 | 接下来让我们以通用的 MVVM 架构为例,逐层剖析前端数据层的共同痛点。 9 | 10 | ## Model 层 11 | 作为应用数据链路的最下游,前端的 Model 层与后端的 Model 层其实有着很大的区别。相较于后端 Model,前端 Model 并不能起到定义数据结构的作用,而更像是一个容器,用于存放后端接口返回的数据。 12 | 13 | 在这样的前提下,在 RESTful 风格的接口已然成为业界标准的今天,如果后端数据是按照数据资源的最小粒度返回给前端的话,我们是不是可以直接将每个接口的标准返回,当做我们最底层的数据 Model 呢?换句话说,我们好像也别无选择,因为接口返回的数据就是前端数据层的最上游,也是接下来一切数据流动的起点。 14 | 15 | 在明确了 Model 层的定义之后,让我们来看一下 Model 层存在的问题。 16 | 17 | ### 数据资源粒度过细 18 | 数据资源粒度过细通常会导致以下两个问题,一是单个页面需要访问多个接口以获取所有的显示数据,二是各个数据资源之间存在获取顺序的问题,需要按顺序依次异步获取。 19 | 20 | 对于第一个问题,常见的解法为搭建一个 Node.js 的数据中间层,来做接口整合,最终暴露给客户端以页面为粒度的接口,并与客户端路由保持一致。 21 | 22 | 这种解法的优点和缺点都非常明显,优点是每个页面都只需要访问一个接口,在生产环境下的页面加载速度可以得到有效的提升。另一方面,因为服务端已经准备好了所有的数据,做起服务端渲染来也很轻松。但从开发效率的角度来讲,不过是将业务复杂度后置的一种做法,并且只适用于页面与页面之间关联较少,应用复杂度较低的项目,毕竟页面级别的 ViewModel 粒度还是太粗了,而且因为是接口级别的解决方案,可复用性几乎为零。 23 | 24 | 对于第二个问题,笔者提供一个基于最简单的 redux-thunk 的工具函数来链接两个异步请求。 25 | 26 | ```javascript 27 | import isArray from 'lodash/isArray'; 28 | 29 | function createChainedAsyncAction(firstAction, handlers) { 30 | if (!isArray(handlers)) { 31 | throw new Error('[createChainedAsyncAction] handlers should be an array'); 32 | } 33 | 34 | return dispatch => ( 35 | firstAction(dispatch) 36 | .then((resultAction) => { 37 | for (let i = 0; i < handlers.length; i += 1) { 38 | const { status, callback } = handlers[i]; 39 | const expectedStatus = `_${status.toUpperCase()}`; 40 | 41 | if (resultAction.type.indexOf(expectedStatus) !== -1) { 42 | return callback(resultAction.payload)(dispatch); 43 | } 44 | } 45 | 46 | return resultAction; 47 | }) 48 | ); 49 | } 50 | ``` 51 | 52 | 基于此,我们再提供一个常见的业务场景来帮助大家理解。比如一个类似于知乎的网站,前端在先获取登录用户信息后,才可以根据用户 id 去获取该用户的回答。 53 | 54 | ```javascript 55 | // src/app/action.js 56 | function getUser() { 57 | return createAsyncAction('APP_GET_USER', () => ( 58 | api.get('/api/me') 59 | )); 60 | } 61 | 62 | function getAnswers(user) { 63 | return createAsyncAction('APP_GET_ANSWERS', () => ( 64 | api.get(`/api/answers/${user.id}`) 65 | )); 66 | } 67 | 68 | function getUserAnswers() { 69 | const handlers = [{ 70 | status: 'success', 71 | callback: getAnswers, 72 | }, { 73 | status: 'error', 74 | callback: payload => (() => { 75 | console.log(payload); 76 | }), 77 | }]; 78 | 79 | return createChainedAsyncAction(getUser(), handlers); 80 | } 81 | 82 | export default { 83 | getUser, 84 | getAnswers, 85 | getUserAnswers, 86 | }; 87 | ``` 88 | 89 | 在输出时,我们可以将三个 actions 全部输出,供不同的页面根据情况按需取用。 90 | 91 | ### 数据不可复用 92 | 每一次的接口调用都意味着一次网络请求,在没有全局数据中心的概念之前,许多前端在开发新需求时都不会在意所要用到的数据是否已经在其他地方被请求过了,而是粗暴地再次去完整地请求一遍所有需要用到的数据。 93 | 94 | 这也就是 Redux 中的 Store 所想要去解决的问题,有了全局的 store,不同页面之间就可以方便地共享同一份数据,从而达到了接口层面也就是 Model 层面的可复用。这里需要注意的一点是,因为 Redux Store 中的数据是存在内存中的,一旦用户刷新页面就会导致所有数据的丢失,所以在使用 Redux Store 的同时,我们也需要配合 `Cookie` 以及 `LocalStorage` 去做核心数据的持久化存储,以保证在未来再次初始化 Store 时能够正确地还原应用状态。特别是在做同构时,一定要保证服务端可以将 Store 中的数据注入到 HTML 的某个位置,以供客户端初始化 Store 时使用。 95 | 96 | ## ViewModel 层 97 | ViewModel 层作为客户端开发中特有的一层,从 MVC 的 Controller 一步步发展而来,虽然 ViewModel 解决了 MVC 中 Model 的改变将直接反应在 View 上这一问题,却仍然没有能够彻底摆脱 Controller 最为人所诟病的一大顽疾,即业务逻辑过于臃肿。另一方面,单单一个 ViewModel 的概念,也无法直接抹平客户端开发所特有的,业务逻辑与显示逻辑之间的巨大鸿沟。 98 | 99 | ### 业务逻辑与显示逻辑之间对应关系复杂 100 | 举例来说,常见的应用中都有使用社交网络账号登录这一功能,产品经理希望实现在用户连接了社交账户之后,首先尝试直接登录应用,如果未注册则为用户自动注册应用账户,特殊情况下如果社交网络返回的用户信息不满足直接注册的条件(如缺少邮箱或手机号),则跳转至补充信息页面。 101 | 102 | 在这个场景下,登录与注册是业务逻辑,根据接口返回在页面上给予用户适当的反馈,进行相应的页面跳转则是显示逻辑,如果从 Redux 的思想来看,这二者分别就是 action 与 reducer。使用上文中的链式异步请求函数,我们可以将登录与注册这两个 action 链接起来,定义二者之间的关系(登录失败后尝试验证用户信息是否足够直接注册,足够则继续请求注册接口,不足够则跳转至补充信息页面)。代码如下: 103 | 104 | ```javascript 105 | function redirectToPage(redirectUrl) { 106 | return { 107 | type: 'APP_REDIRECT_USER', 108 | payload: redirectUrl, 109 | } 110 | } 111 | 112 | function loginWithFacebook(facebookId, facebookToken) { 113 | return createAsyncAction('APP_LOGIN_WITH_FACEBOOK', () => ( 114 | api.post('/auth/facebook', { 115 | facebook_id: facebookId, 116 | facebook_token: facebookToken, 117 | }) 118 | )); 119 | } 120 | 121 | function signupWithFacebook(facebookId, facebookToken, facebookEmail) { 122 | if (!facebookEmail) { 123 | redirectToPage('/fill-in-details'); 124 | } 125 | 126 | return createAsyncAction('APP_SIGNUP_WITH_FACEBOOK', () => ( 127 | api.post('/accounts', { 128 | authentication_type: 'facebook', 129 | facebook_id: facebookId, 130 | facebook_token: facebookToken, 131 | email: facebookEmail, 132 | }) 133 | )); 134 | } 135 | 136 | function connectWithFacebook(facebookId, facebookToken, facebookEmail) { 137 | const firstAction = loginWithFacebook(facebookId, facebookToken); 138 | const callbackAction = signupWithFacebook(facebookId, facebookToken, facebookEmail); 139 | 140 | const handlers = [{ 141 | status: 'success', 142 | callback: () => (() => {}), // 用户登陆成功 143 | }, { 144 | status: 'error', 145 | callback: callbackAction, // 使用 facebook 账户登陆失败,尝试帮用户注册新账户 146 | }]; 147 | 148 | return createChainedAsyncAction(firstAction, handlers); 149 | } 150 | ``` 151 | 152 | 这里,只要我们将可复用的 action 拆分到了合适的粒度,并在链式 action 中将他们按照业务逻辑组合起来之后,Redux 就会在不同的情况下 dispatch 不同的 action。可能的几种情况如下: 153 | 154 | ```text 155 | // 直接登录成功 156 | APP_LOGIN_WITH_FACEBOOK_REQUEST 157 | APP_LOGIN_WITH_FACEBOOK_SUCCESS 158 | 159 | // 直接登录失败,注册信息充足 160 | APP_LOGIN_WITH_FACEBOOK_REQUEST 161 | APP_LOGIN_WITH_FACEBOOK_ERROR 162 | APP_SIGNUP_WITH_FACEBOOK_REQUEST 163 | APP_LOGIN_WITH_FACEBOOK_SUCCESS 164 | 165 | // 直接登录失败,注册信息不足 166 | APP_LOGIN_WITH_FACEBOOK_REQUEST 167 | APP_LOGIN_WITH_FACEBOOK_ERROR 168 | APP_REDIRECT_USER 169 | ``` 170 | 171 | 于是,在 reducer 中,我们只要在相应的 action 被 dispatch 时,对 ViewModel 中的数据做相应的更改即可,也就做到了业务逻辑与显示逻辑相分离。 172 | 173 | 这一解法与 MobX 及 RxJS 有相同又有不同。相同的是都定义好了数据的流动方式(action 的 dispatch 顺序),在合适的时候通知 ViewModel 去更新数据,不同的是 Redux 不会在某个数据变动时自动触发某条数据管道,而是需要使用者显式地去调用某一条数据管道,如上述例子中,在用户点击『连接社交网络』按钮时。综合起来和 redux-observable 的思路可能更为一致,即没有完全抛弃 redux,又引入了数据管道的概念,只是限于工具函数的不足,无法处理更复杂的场景。但从另一方面来说,如果业务中确实没有非常复杂的场景,在理解了 redux 之后,使用最简单的 redux-thunk 就可以完美地覆盖到绝大部分需求。 174 | 175 | ### 业务逻辑臃肿 176 | 拆分并组合可复用的 action 解决了一部分的业务逻辑,但另一方面,Model 层的数据需要通过组合及格式化后才能成为 ViewModel 的一部分,也是困扰前端开发的一大难题。 177 | 178 | 这里推荐使用抽象出通用的 **Selector** 和 **Formatter** 的概念来解决这一问题。 179 | 180 | 上面我们提到了,后端的 Model 会随着接口直接进入到各个页面的 reducer,这时我们就可以通过 Selector 来组合不同 reducer 中的数据,并通过 Formatter 将最终的数据格式化为可以直接显示在 View 上的数据。 181 | 182 | 举个例子,在用户的个人中心页面,我们需要显示用户在各个分类下喜欢过的回答,于是我们需要先获取所有的分类,并在所有分类前加上一个后端并不存在的『热门』分类。又因为分类是一个非常常用的数据,所以我们之前已经在首页获取过并存在了首页的 reducer 中。代码如下: 183 | 184 | ```javascript 185 | // src/views/account/formatter.js 186 | import orderBy from 'lodash/orderBy'; 187 | 188 | function categoriesFormatter(categories) { 189 | const customCategories = orderBy(categories, 'priority'); 190 | const popular = { 191 | id: 0, 192 | name: '热门', 193 | shortname: 'popular', 194 | }; 195 | customCategories.unshift(popular); 196 | 197 | return customCategories; 198 | } 199 | 200 | // src/views/account/selector.js 201 | import formatter from './formatter.js'; 202 | import homeSelector from '../home/selector.js'; 203 | 204 | const categoriesWithPopularSelector = state => 205 | formatter.categoriesFormatter(homeSelector.categoriesSelector(state)); 206 | 207 | export default { 208 | categoriesWithPopularSelector, 209 | }; 210 | ``` 211 | 212 | 在明确了 ViewModel 层需要解决的问题后,有针对性地去复用并组合 action,selector,formatter 就可以得到一个思路非常清晰的解决方案。在保证所有数据都只在相应的 reducer 中存储一份的前提下,各个页面数据不一致的问题也迎刃而解。反过来说,数据不一致问题的根源就是代码的可复用性太低,才导致了同一份数据以不同的方式流入了不同的数据管道并最终得到了不同的结果。 213 | 214 | ## View 层 215 | 在理清楚前面两层之后,作为前端最重要的 View 层反而简单了许多,通过 `mapStateToProps` 和 `mapDispatchToProps`,我们就可以将粒度极细的显示数据与组合完毕的业务逻辑直接映射到 View 层的相应位置,从而得到一个纯净,易调试的 View 层。 216 | 217 | ### 可复用 View 218 | 但问题好像又并没有那么简单,因为 View 层的可复用性也是困扰前端的一大难题,基于以上思路,我们又该怎样处理呢? 219 | 220 | 受益于 React 等框架,前端组件化不再是一个问题,我们也只需要遵守以下几个原则,就可以较好地实现 View 层的复用。 221 | 222 | * 所有的页面都隶属于一个文件夹,只有页面级别的组件才会被 connect 到 redux store。每个页面又都是一个独立的文件夹,存放自己的 action,reducer,selector 及 formatter。 223 | * components 文件夹中存放业务组件,业务组件不会被 connect 到 redux store,只能从 props 中获取数据,从而保证其可维护性与可复用性。 224 | * 另一个文件夹或 npm 包中存放 UI 组件,UI 组件与业务无关,只包含显示逻辑,不包含业务逻辑。 225 | 226 | ## 小结 227 | 虽然说开发灵活易用的组件库是一件非常难的事情,但在积累了足够多的可复用的业务组件及 UI 组件之后,新的页面在数据层面,又可以从其他页面的 action,selector,formatter 中寻找可复用的业务逻辑时,新需求的开发速度应当是越来越快的。而不是越来越多的业务逻辑与显示逻辑交织在一起,最终导致整个项目内部复杂度过高无法维护后只能推倒重来。 228 | 229 | ## 一点心得 230 | 在新技术层出不穷的今天,在我们执着于说服别人接受自己的技术观点时,我们还是需要回到当前业务场景下,去看一看要解决的到底是一个什么样的问题。 231 | 232 | 抛去少部分极端复杂的前端应用来看,目前大部分的前端应用都还是以展示数据为主,在这样的场景下,再前沿的技术与框架都无法直接解决上面提到的这些问题,反倒是一套清晰的数据处理思路及对核心概念的深入理解,再配合上严谨的团队开发规范才有可能将深陷复杂数据泥潭的前端开发者们拯救出来。 233 | 234 | 作为工程学的一个分支,软件工程的复杂度从来都不在于那些无法解决的难题,而是如何制定简单的规则让不同的模块各司其职。这也是为什么在各种框架,库,解决方案层出不穷的今天,我们还是在强调基础,强调经验,强调要看到问题的本质。 235 | 236 | 王阳明所说的知行合一,现代人往往是知道却做不到。但在软件工程方面,我们又常常会陷入照猫画虎地做到了,却并不理解其中原理的另一极端,而这二者显然都是不可取的。 -------------------------------------------------------------------------------- /2016/重新设计 React 组件库/重新设计 React 组件库.md: -------------------------------------------------------------------------------- 1 | # 重新设计 React 组件库 2 | 在 react + redux 已经成为大部分前端项目底层架构的今天,让我们再回到软件工程界一个永恒的问题上来,那就是如何提升一个开发团队的开发效率? 3 | 从宏观的角度来讲,只有对具体业务的良好抽象才能真正提高一个团队的开发效率,又囿于不同产品所面临的不同业务需求,当我们抽丝剥茧般地将一个个前端项目抽象到最后一层,那么剩下的就只有按钮、输入框、对话框、图标等这些毫无业务意义的纯 UI 组件了。 4 | 5 | 选择或开发一套适合自己团队使用的 UI 组件库应该是每一个前端团队在底层架构达成共识后下一件就要去做的事情,那么我们就以今天为始,分别从以下几个方面来探讨如何构建一套优秀的 UI 组件库。 6 | 7 | ## 第一个问题:选择开源 vs 自己造轮子 8 | 在 React 界,优秀且开源的 UI 组件库有很多,国外的如 [Material-UI](http://www.material-ui.com/),国内的如 [Ant Design](https://ant.design/),都是经过众多使用者检验,组件丰富且代码质量过硬的组件库。所以当我们决定再造一套 UI 组件库之前,不妨先尝试下这些在 UI 组件库界口碑良好的标品,再决定是否要进入这个看似简单实则困难重重的领域。 9 | 10 | 在这里,我们并不会去比较任何组件库之间的区别或优劣,但却可以从产品层面给出几个开发自有组 11 | 件库的判断依据,以供参考。 12 | 13 | * 产品有独立的设计规范,包括但不限于组件样式、交互模式。 14 | * 产品业务场景较为复杂,需要深度定制某些常用组件。 15 | * 前端团队需要同时支撑多条业务线。 16 | 17 | ## 设计思想:规范 vs. 自由 18 | 在选择了自己造轮子这样一条路之后,下一个摆在面前的艰难选择就是,要造一个规范的组件库还是一个自由的组件库? 19 | 20 | 规范的组件库可以从源码层面保证产品视觉、交互风格的一致性,也可以很大程度上降低业务开发的复杂度,从而提升团队整体的开发效率。但在遇到一些看似相似实则不同的业务需求时,规范的组件库往往会走入一个难以避免的死循环,那就是实现 A 需求需要使用 a 组件,但是现有的 a 组件又不能完全支持 A 需求。 21 | 22 | 这时摆在工程师面前的就只有两条路: 23 | 24 | * 重新开发一个完美支持 A 需求的 a+ 组件 25 | * 修改 a 组件源码使其支持 A 需求 26 | 27 | 方法一费时费力,会极大地增加本次项目的开发成本,而方法二又会导致 a 组件代码膨胀速度过快且逻辑复杂,极大地增加组件库后期的维护成本。 28 | 29 | 在多次陷入上面所描述的这个困境之后,在最近的一次内部组件库重构时,我们选择了拥抱自由,这其中既有业务方面的考虑,也有 React 在组件自由组合方面的天然优势,让我们来看一个例子。 30 | 31 | ### Select 32 | 33 | ```javascript 34 | // traditional select 35 |
36 |
41 | {value} 42 | 43 |
44 | {menu} 45 |
46 | ``` 47 | 48 | 这是一个非常传统的 Select 组件,触发下拉菜单的区域为一段文字加一个箭头。我们来看下面的一个业务场景: 49 | 50 | ![](./select.jpg) 51 | 52 | 这里触发下拉菜单的区域不再是传统的一段文字加一个箭头,而是一个自定义元素,点击后展开下拉列表。虽然它的交互模式和 Select 一模一样,但因为二者在 DOM 结构上的巨大差别,导致我们无法复用上面的这个 Select 来实现它。 53 | 54 | ```javascript 55 | // Customizeable Select 56 |
57 | { 58 | children 59 | || 60 | 61 | 62 | {label ? {label} : null} 63 | 64 | {currentValue !== '' ? currentValue : selectPlaceholder} 65 | 66 | 67 | 68 | 69 | } 70 | {this.renderPopup()} 71 |
72 | ``` 73 | 74 | 在支持传统的文字加箭头之外,更自由的 Select 添加了对 label 及 children 支持,分别可以对应有名称的 Select 75 | 76 | ![](./select-label.jpg) 77 | 78 | 及类似前面提到的自定义元素。 79 | 80 | ### Dropdown 81 | 82 | 类似的还有 Select 的孪生兄弟 Dropdown。 83 | 84 | ```javascript 85 | // Customizeable Dropdown 86 |
87 | {data.map((value, idx) => { 88 | return ( 89 | 95 | ); 96 | })} 97 |
98 | 99 | // Using Dropdown 100 | const demoData = [{ text: 'Robb Stark', age: 36 }] 101 | const DropdownItem = (props) => ( 102 |
103 |
{props.data.text}
104 |
is {props.data.age} years old.
105 |
106 | ); 107 | ``` 108 | 109 | 这是一个常见的下拉列表组件,是否允许用户传入 ItemComponent 其实就是一个规范与自由之间的取舍。在选择了拥抱自由之后,组件的使用者终于不会再被组件内部的 DOM 结构所束缚,转而可以自由地定制子元素的 DOM 结构。 110 | 111 | 相较于传统的规范的组件,自由的组件需要使用者在业务项目中多写一些代码,但如果我们往深处再看一层,这些特殊的下拉元素本就是属于某个业务所特有的,将其放在业务代码层恰恰是一种更合适的分层方法。 112 | 113 | 另一方面,我们在这里所定义的自由,绝不仅仅是多暴露几个渲染函数那么简单,这里的自由指的是组件内部 DOM 结构的自由。因为一旦某个组件定死了自己的 DOM 结构,外部使用时除了重写样式去强行覆盖外没有任何其他可行的方式去改变它。 114 | 115 | 虽然我们上面提到了许多自由的好处,但很多时候我们还是会被一个问题所挑战,那就是自由的组件在大部分时候不如规范的组件来得好用,因为调用起来很麻烦。 116 | 117 | 这个问题其实是有解的,那就是默认值。我们可以在组件库中内置许多常用的子元素,当用户不指定子元素时,使用默认的子元素来完成渲染,这样就可以在规范与自由之间达成一个良好的平衡,但这里需要注意的是,添加常用子元素的工作量也非常巨大,团队内部也需要对“常用”这个词有一个统一的认识。 118 | 119 | 或者你也可以选择针对不同的使用场景,做两套不同的解决方案。例如前端开源 UI 框架界的翘楚 antd,其底层依赖的 [react-component](https://github.com/react-component) 也是非常解耦的设计,几乎看不到任何固定的 DOM 结构,而是使用自定义组件或 children prop 将 DOM 结构的决定权交给使用者。 120 | 121 | ```javascript 122 | // react-component/dropdown 123 | return ( 124 | 144 | {children} 145 | 146 | ); 147 | ``` 148 | 149 | ## 数据处理:耦合 vs. 解耦 150 | 如果你问一个工程师在某个场景下,两个模块是耦合好还是解耦好?我想他甚至可能都不会问你是什么场景就脱口而出:“当然解耦好,耦合的代码根本没办法维护!” 151 | 152 | 但事实上,在传统的组件库设计中,我们一直都默认组件是可以和数据源(一般的组件都会有 data 这个 prop)相耦合的,这样就导致了我们在给某个组件赋值之前,要先写一个数据处理方法,将后端返回回来的数据处理成组件要求的数据结构,再传给组件进行渲染。 153 | 154 | 这时,如果后端返回的或组件要求的数据结构再变态一些(如数组嵌套),这个数据处理方法就很有可能会写得非常复杂,甚至还会导致许多的 edge case 使得组件在获取某个特定的 attribute 时直接报错。 155 | 156 | 如何将组件与数据源解耦呢?答案就是不要在组件代码(不论是视图层还是控制层)中出现 `data.xxx`,而是在回调时将整个对象都抛给调用者供其按需使用。这样组件就可以无缝适配于各种各样的后端接口,大大降低使用者在数据处理时犯错误的可能。 157 | 158 | 承接前文,其实这样的数据处理方式和前面提到的自由的设计思想是一脉相承的,正是因为我们赋予了使用者自由定制 DOM 结构的能力,所以我们同时也可以赋予他们在数据处理上的自由。 159 | 160 | 看到这里,支持规范组件的朋友可能已经有些崩溃了,因为听起来自由组件既不定义 DOM 结构,也不处理数据,那么我为什么还要用这个组件呢? 161 | 162 | 让我们以 Select 组件为例来回答这个问题。 163 | 164 | 是的,自由的 Select 组件需要使用者自定义下拉元素,还需要在回调中自己处理使用 data 的哪个 attribute 来完成下一步的业务逻辑,但 Select 组件真的什么都没有做吗?其实并不是,Select 组件规范了“选择”这个交互方式,处理了什么时候显示或隐藏下拉列表,响应了下拉列表元素的 `hover` 和 `click` 事件,并控制了绝对定位的下拉列表的弹出位置。这些通用的交互逻辑,才是 Select 组件的核心,至于多变的渲染和数据处理逻辑,打包开放出来反而更利于使用者在多变的业务场景下方便地使用 Select 组件。 165 | 166 | 讲完了组件与数据源之间的解耦,我们再来谈一下组件各个 props 之间解耦的必要性。 167 | 168 | 假设一个需求:按照中国、美国、英国、日本、加拿大的顺序显示当地时间,当地时间需从服务端获取且显示格式不同。 169 | 170 | 我们可以设计一个组件,接收不同国家的时间数据作为其 data prop,展示一个当地时间至少需要英文唯一标识符 `region`,中文显示名 `name`,当前时间 `time`,显示格式 `format` 等四个属性,由此我们可以设计组件的 data 属性为: 171 | 172 | ```javascript 173 | data: [{ 174 | region: 'china' 175 | name: '中国', 176 | time: 1481718888, 177 | format: 'MMMM Do YYYY, h:mm:ss a', 178 | }, { 179 | ... 180 | }] 181 | ``` 182 | 183 | 看起来不错,但事实真的是这样吗?我相信如果你把这份数据结构拿给后端同事看时,他一定会立刻指出一个问题,那就是后端数据库中是不会保存 `name` 及 `format` 字段的,因为这是由具体产品定义的展示逻辑,而接口只负责告诉你这个地区是哪里 `region` 以及这个地区的当前时间是多少 `time`。事情到这里也许还不算那么糟糕,因为我们可以在调用组件前,将异步获取到的数据再重新格式化一遍,补上缺失的字段。但这时一个更棘手的问题来了,那就是接口返回的数组数据一般是不保证顺序的,你还需要按照产品的要求,在补充完缺失的字段后,对整个数组进行一次重排以保证每一次渲染出来的地区都保持同样的顺序。 184 | 185 | 换一种方式,如果我们这样去设计组件的 props 呢? 186 | 187 | ```javascript 188 | { 189 | data: { 190 | china: { 191 | time: 1481718888, 192 | }, 193 | ... 194 | }, 195 | timeList: [{ 196 | region: 'china', 197 | name: '中国', 198 | format: 'MMMM Do YYYY, h:mm:ss a', 199 | }, { 200 | ... 201 | }], 202 | ... 203 | } 204 | ``` 205 | 206 | 当我们将需要异步获取的 props 抽离后,这个组件就变得非常 data & api friendly 了,仅通过配置 timeList prop 就可以完美地控制组件的渲染规则及渲染顺序并且再也不需要对接口返回的数据进行补全或定制了。甚至我们还可以通过设置默认值的方式,先将组件同步渲染出来,在异步数据请求完成后再重绘数值部分,给予用户更好的视觉体验。 207 | 208 | 除了分离非必须耦合的 props 之外,细心的朋友可能还会发现上面的 data prop 的数据结构从数组变为了对象,这又是为什么呢? 209 | 210 | ## 回调规范:数组 vs. 对象 211 | 设计思想可以是自由的,数据处理也可以是自由的,但一个成熟的 UI 组件库作为一个独立的前端项目,在代码层面必须要建立起自己的规范。抛开老生常谈的 JavaScript 及 Sass/Less 层面的代码规范不表,让我们从 CSS 类名、组件类别及回调规范三个方面分享一些最佳实践。 212 | 213 | 在组件库项目中,并不推荐使用 CSS Modules,一方面是因为其编译出来的复杂类名不便于使用者在业务项目里进行简单覆盖,更重要的是我们可以将每一个组件都看作是一个独立的模块,用添加 `xui-componentName` 类名前缀的方式来实现一套简化版的 CSS Modules。另外,在 jsx 中我们可以参考 antd 的做法,为每一个组件添加一个名为 `prefixCls` 的 prop,并将其默认值也设置为 `xui-componentName`,这样就在 jsx 层面也保证了代码的统一性,方便团队成员阅读及维护。 214 | 215 | 在这次内部组件库重构项目中,我们将所有的组件分为了纯渲染组件与智能组件两类,并规范其写法为纯函数与 ES6 class 两种,彻底抛弃了 `React.createClass` 的写法。这样一方面可以进一步规范代码,增强可读性,另一方面也可以让后续的维护者在一秒钟内判断出某个组件是纯渲染组件还是智能组件。 216 | 217 | 在回调函数方面,所有的组件内部函数都以 `handleXXX`(`handleClick`,`handleHover`,`handleMouseover` 等)为命名模板,所有对外暴露的回调函数都以 `onXXX`(`onChange`,`onSelect` 等)为命名模板。这样在维护一些依赖层级较深的底层组件时,就可以在 render 方法中一眼看出某个回调是在处理内部状态,还是将回调至更高一层。 218 | 219 | 在设计回调数据的数据结构时,我们只使用了单一值(如 Input 组件的回调)和对象两种数据结构,尽量避免了使用传统组件库中常用的数组。相较于对象,数组其实是一种含义更为丰富的数据结构,因为它是有向的(包含顺序的),比如在上面的例子中,timeList prop 就被设计为数组,这样它就可以在承载数据的同时包含数据展示的顺序,极大地方便了组件的使用。但在给使用者抛出回调数据时,并不是每一位使用者都能够像组件设计者那样清楚回调数据的顺序,使用数组实际上变相增加了使用者的记忆成本,而且笔者一直都不赞成在代码中出现类似于 `const value = data[0];` 这样的表达式。因为没有人能够保证数组的长度满足需要且当前位上的元素就是要取的值。另一方面,对象因为键值对的存在,在具体到某一个元素的表意上要比数组更为丰富。例如选择日历区间后的回调需要同时返回开始日期及结束日期: 220 | 221 | ```javascript 222 | // array 223 | ['2016-11-11', '2016-12-12'] 224 | 225 | // object 226 | { 227 | firstDay: '2016-11-11', 228 | lastDay: '2016-12-12', 229 | } 230 | ``` 231 | 232 | 严格来讲上述的两种方式并没有对错之分,只是对象的数据结构更能够清晰地表达每个元素的含义并消除顺序的影响,更利于不了解组件库内部代码的使用者快速上手。 233 | 234 | ## 小结 235 | 在本文中,我们从设计思想、数据处理、回调规范三个方面为各位剖析了在前端组件化已经成为既定事实的今天,我们还能在组件库设计方面做出怎样新的尝试与突破。也许这些新的尝试与突破并不会像一个新的框架那样给你带来全新的震撼,但我们相信这些实用的思考与经验可以让你少走许多弯路并打开一些新的思路,并且跳出前端这个“狭小”的圈子,站在软件工程的高度去看待这些看似简单实则复杂的工作。 236 | 237 | 在以后的文章中,我们还会从组件库整体代码架构、组件库国际化方案及复杂组件架构设计等方面为大家带来更多细节上的经验与体会,也会穿插更多的具体的代码片段来阐述我们的设计思想与理念,敬请期待。 238 | -------------------------------------------------------------------------------- /2017/组件库设计实战 - 复杂组件设计/组件库设计实战 - 复杂组件设计.md: -------------------------------------------------------------------------------- 1 | # 组件库设计实战 - 复杂组件设计 2 | 一个成熟的组件库通常都由数十个常用的 UI 组件构成,这其中既有按钮(Button),输入框(Input)等基础组件,也有表格(Table),日期选择器(DatePicker),轮播(Carousel)等自成一体的复杂组件。 3 | 4 | 这里我们提出一个**组件复杂度**的概念,一个组件复杂度的主要来源就是其自身的状态,即组件自身需要维护多少个不依赖于外部输入的状态。参考原先文章中提到过的木偶组件(dumb component)与智能组件(smart component),二者的区别就是是否需要在组件内部维护不依赖于外部输入的状态。 5 | 6 | ## 实战案例 - 轮播组件 7 | 在本篇文章中,我们将以轮播(Carousel)组件为例,一步一步还原如何实现一个交互流畅的轮播组件。 8 | 9 | ### 最简单的轮播组件 10 | 抛去所有复杂的功能,轮播组件的实质,实际上就是在一个固定区域实现不同元素之间的切换。在明确了这点后,我们就可以设计轮播组件的基础 DOM 结构为: 11 | 12 | ```jsx 13 | 14 | 15 | 16 | ... 17 | 18 | 19 | 20 | ``` 21 | 22 | 如下图所示: 23 | 24 | ![](./carousel.jpg) 25 | 26 | `Frame` 即轮播组件的真实显示区域,其宽高为内部由使用者输入的 `SlideItem` 决定。这里需要注意的一点是需要设置 `Frame` 的 `overflow` 属性为 `hidden`,即隐藏超出其本身宽高的部分,每次只显示一个 `SlideItem`。 27 | 28 | `SlideList` 为轮播组件的轨道容器,改变其 `translateX` 的值即可实现在轨道的滑动,以显示不同的轮播元素。 29 | 30 | `SlideItem` 是使用者输入的轮播元素的一层抽象,内部可以是 `img` 或 `div` 等 DOM 元素,并不影响轮播组件本身的逻辑。 31 | 32 | ### 实现轮播元素之前的切换 33 | 为了实现在不同 `SlideItem` 之间的切换,我们需要定义轮播组件的第一个内部状态,即 `currentIndex`,即当前显示轮播元素的 `index` 值。上文中我们提到了改变 `SlideList` 的 `translateX` 是实现轮播元素切换的关键,所以这里我们需要将 `currentIndex` 与 `SlideList` 的 `translateX` 对应起来,即: 34 | 35 | ```javascript 36 | translateX = -(width) * currentIndex 37 | ``` 38 | 39 | `width` 即为单个轮播元素的宽度,与 `Frame` 的宽度相同,所以我们可以在 `componentDidMount` 时拿到 `Frame` 的宽度并以此计算出轨道的总宽度。 40 | 41 | ```javascript 42 | componentDidMount() { 43 | const width = get(this.container.getBoundingClientRect(), 'width'); 44 | } 45 | 46 | render() { 47 | const rest = omit(this.props, Object.keys(defaultProps)); 48 | const classes = classnames('ui-carousel', this.props.className); 49 | return ( 50 |
{ this.container = node; }} 54 | > 55 | {this.renderSildeList()} 56 | {this.renderDots()} 57 |
58 | ); 59 | } 60 | ``` 61 | 62 | 至此,我们只需要改变轮播组件中的 `currentIndex`,即可间接改变 `SlideList` 的 `translateX`,以此实现轮播元素之间的切换。 63 | 64 | ### 响应用户操作 65 | 轮播作为一个常见的通用组件,在桌面和移动端都有着非常广泛的应用,这里我们先以移动端为例,来阐述如何响应用户操作。 66 | 67 | ```javascript 68 | {map(children, (child, i) => ( 69 |
78 | {child} 79 |
80 | ))} 81 | ``` 82 | 83 | 在移动端,我们需要监听三个事件,分别响应滑动开始,滑动中与滑动结束。其中滑动开始与滑动结束都是一次性事件,而滑动中则是持续性事件,以此我们可以确定在三个事件中我们分别需要确定哪些值。 84 | 85 | #### 滑动开始 86 | * startPositionX:此次滑动的起始位置 87 | 88 | ```javascript 89 | handleTouchStart = (e) => { 90 | const { x } = getPosition(e); 91 | this.setState({ 92 | startPositionX: x, 93 | }); 94 | } 95 | ``` 96 | 97 | #### 滑动中 98 | * moveDeltaX:此次滑动的实时距离 99 | * direction:此次滑动的实时方向 100 | * translateX:此次滑动中轨道的实时位置,用于渲染 101 | 102 | ```javascript 103 | handleTouchMove = (e) => { 104 | const { width, currentIndex, startPositionX } = this.state; 105 | const { x } = getPosition(e); 106 | 107 | const deltaX = x - startPositionX; 108 | const direction = deltaX > 0 ? 'right' : 'left'; 109 | this.setState({ 110 | moveDeltaX: deltaX, 111 | direction, 112 | translateX: -(width * currentIndex) + deltaX, 113 | }); 114 | } 115 | ``` 116 | 117 | #### 滑动结束 118 | * currentIndex:此次滑动结束后新的 currentIndex 119 | * endValue:此次滑动结束后轨道的 translateX 120 | 121 | ```javascript 122 | handleTouchEnd = () => { 123 | this.handleSwipe(); 124 | } 125 | 126 | handleSwipe = () => { 127 | const { children, speed } = this.props; 128 | const { width, currentIndex, direction, translateX } = this.state; 129 | const count = size(children); 130 | 131 | let newIndex; 132 | let endValue; 133 | if (direction === 'left') { 134 | newIndex = currentIndex !== count ? currentIndex + 1 : START_INDEX; 135 | endValue = -(width) * (currentIndex + 1); 136 | } else { 137 | newIndex = currentIndex !== START_INDEX ? currentIndex - 1 : count; 138 | endValue = -(width) * (currentIndex - 1); 139 | } 140 | 141 | const tweenQueue = this.getTweenQueue(translateX, endValue, speed); 142 | this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex)); 143 | } 144 | ``` 145 | 146 | 因为我们在滑动中会实时更新轨道的 translateX,我们的轮播组件便可以做到**跟手**的用户体验,即在单次滑动中,轮播元素会跟随用户的操作向左或向右滑动。 147 | 148 | ### 实现顺滑的切换动画 149 | 在实现了滑动中跟手的用户体验后,我们还需要在滑动结束后将显示的轮播元素定位到新的 `currentIndex`。根据用户的滑动方向,我们可以对当前的 `currentIndex` 进行 +1 或 -1 以得到新的 `currentIndex`。但在处理第一个元素向左滑动或最后一个元素向右滑动时,新的 `currentIndex` 需要更新为最后一个或第一个。 150 | 151 | 这里的逻辑并不复杂,但却带来了一个非常难以解决的用户体验问题,那就是假设我们有 3 个轮播元素,每个轮播元素的宽度都为 300px,即显示最后一个元素时,轨道的 translateX 为 -600px,在我们将最后一个元素向左滑动后,轨道的 translateX 将被重新定义为 0px,此时若我们使用原生的 CSS 动画: 152 | 153 | ```css 154 | transition: 1s ease-in-out; 155 | ``` 156 | 157 | 轨道将会在一秒内从左向右滑动至第一个轮播元素,而这是反直觉的,因为用户一个向左滑动的操作导致了一个向右的动画,反之亦然。 158 | 159 | 这个问题从上古时期就困扰着许多前端开发者,笔者也见过以下几种解决问题的方法: 160 | 161 | * 将轨道宽度定义为无限长(几百万 px),无限次重复有限的轮播元素。这种解决方案显然是一种 hack,并没有从实质上解决轮播组件的问题。 162 | * 只渲染三个轮播元素,即前一个,当前一个,下一个,每次滑动后同时更新三个元素。这种解决方案实现起来非常复杂,因为组件内部要维护的状态从一个 currentIndex 增加到了三个拥有各自状态的 DOM 元素,且因为要不停的删除和新增 DOm 节点导致性能不佳。 163 | 164 | 这里让我们再来思考一下滑动操作的本质。除去第一和最后两个元素,所有中间元素滑动后新的 translateX 的值都是固定的,即 `-(width * currentIndex)`,这种情况下的动画都可以轻松地完美实现。而在最后一个元素向左滑动时,因为轨道的 `translateX` 已经到达了极限,面对这种情况我们如何才能实现顺滑的切换动画呢? 165 | 166 | 这里我们选择将最后一个及第一个元素分别拼接至轨道的头尾,以保证在 DOM 结构不需要改变的前提下实现顺滑的切换动画: 167 | 168 | ![](./carousel-long.jpg) 169 | 170 | 这样我们就统一了每次滑动结束后 `endValue` 的计算方式,即 171 | 172 | ```javascript 173 | // left 174 | endValue = -(width) * (currentIndex + 1) 175 | 176 | // right 177 | endValue = -(width) * (currentIndex - 1) 178 | ``` 179 | 180 | ### 使用 requestAnimationFrame 实现高性能动画 181 | `requestAnimationFrame` 是浏览器提供的一个专注于实现动画的 API,感兴趣的朋友可以再重温一下[《React Motion 缓动函数剖析》](https://zhuanlan.zhihu.com/p/20458251)这篇专栏。 182 | 183 | 所有的动画本质上都是一连串的时间轴上的值,具体到轮播场景下即:以用户停止滑动时的值为起始值,以新 `currentIndex` 时 `translateX` 的值为结束值,在使用者设定的动画时间(如0.5秒)内,依据使用者设定的缓动函数,计算每一帧动画时的 `translateX` 值并最终得到一个数组,以每秒 60 帧的速度更新在轨道的 `style` 属性上。每更新一次,将消耗掉动画值数组中的一个中间值,直到数组中所有的中间值被消耗完毕,动画结束并触发回调。 184 | 185 | 具体代码如下: 186 | 187 | ```javascript 188 | const FPS = 60; 189 | const UPDATE_INTERVAL = 1000 / FPS; 190 | 191 | animation = (tweenQueue, newIndex) => { 192 | if (isEmpty(tweenQueue)) { 193 | this.handleOperationEnd(newIndex); 194 | return; 195 | } 196 | 197 | this.setState({ 198 | translateX: head(tweenQueue), 199 | }); 200 | tweenQueue.shift(); 201 | this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex)); 202 | } 203 | 204 | getTweenQueue = (beginValue, endValue, speed) => { 205 | const tweenQueue = []; 206 | const updateTimes = speed / UPDATE_INTERVAL; 207 | for (let i = 0; i < updateTimes; i += 1) { 208 | tweenQueue.push( 209 | tweenFunctions.easeInOutQuad(UPDATE_INTERVAL * i, beginValue, endValue, speed), 210 | ); 211 | } 212 | return tweenQueue; 213 | } 214 | ``` 215 | 216 | 在回调函数中,根据变动逻辑统一确定组件当前新的稳定态值: 217 | 218 | ```javascript 219 | handleOperationEnd = (newIndex) => { 220 | const { width } = this.state; 221 | 222 | this.setState({ 223 | currentIndex: newIndex, 224 | translateX: -(width) * newIndex, 225 | startPositionX: 0, 226 | moveDeltaX: 0, 227 | dragging: false, 228 | direction: null, 229 | }); 230 | } 231 | ``` 232 | 233 | 完成后的轮播组件效果如下图: 234 | 235 | ![](./carousel.gif) 236 | 237 | ### 优雅地处理特殊情况 238 | * 处理用户误触:在移动端,用户经常会误触到轮播组件,即有时手不小心滑过或点击时也会触发 `onTouch` 类事件。对此我们可以采取对滑动距离添加阈值的方式来避免用户误触,阈值可以是轮播元素宽度的 10% 或其他合理值,在每次滑动距离超过阈值时,才会触发轮播组件后续的滑动。 239 | * 桌面端适配:对于桌面端而言,轮播组件所需要响应的事件名称与移动端是完全不同的,但又可以相对应地匹配起来。这里还需要注意的是,我们需要为轮播组件添加一个 dragging 的状态来区分移动端与桌面端,从而安全地复用 handler 部分的代码。 240 | 241 | ```javascript 242 | // mobile 243 | onTouchStart={this.handleTouchStart} 244 | onTouchMove={this.handleTouchMove} 245 | onTouchEnd={this.handleTouchEnd} 246 | // desktop 247 | onMouseDown={this.handleMouseDown} 248 | onMouseMove={this.handleMouseMove} 249 | onMouseUp={this.handleMouseUp} 250 | onMouseLeave={this.handleMouseLeave} 251 | onMouseOver={this.handleMouseOver} 252 | onMouseOut={this.handleMouseOut} 253 | onFocus={this.handleMouseOver} 254 | onBlur={this.handleMouseOut} 255 | 256 | handleMouseDown = (evt) => { 257 | evt.preventDefault(); 258 | this.setState({ 259 | dragging: true, 260 | }); 261 | this.handleTouchStart(evt); 262 | } 263 | 264 | handleMouseMove = (evt) => { 265 | if (!this.state.dragging) { 266 | return; 267 | } 268 | this.handleTouchMove(evt); 269 | } 270 | 271 | handleMouseUp = () => { 272 | if (!this.state.dragging) { 273 | return; 274 | } 275 | this.handleTouchEnd(); 276 | } 277 | 278 | handleMouseLeave = () => { 279 | if (!this.state.dragging) { 280 | return; 281 | } 282 | this.handleTouchEnd(); 283 | } 284 | 285 | handleMouseOver = () => { 286 | if (this.props.autoPlay) { 287 | clearInterval(this.autoPlayTimer); 288 | } 289 | } 290 | 291 | handleMouseOut = () => { 292 | if (this.props.autoPlay) { 293 | this.autoPlay(); 294 | } 295 | } 296 | ``` 297 | 298 | ## 小结 299 | 至此我们就实现了一个只有 `tween-functions` 一个第三方依赖的轮播组件,打包后大小不过 2KB,完整的源码大家可以参考这里 [carousel/index.js](https://github.com/AlanWei/sea-ui/blob/master/components/carousel/index.js)。 300 | 301 | 除了节省的代码体积,更让我们欣喜的还是彻底弄清楚了轮播组件的实现模式以及如何使用 `requestAnimationFrame` 配合 `setState` 来在 react 中完成一组动画。 302 | 303 | ## 感想 304 | 305 | ![](./horse.jpg) 306 | 307 | 大家应该都看过上面这幅漫画,有趣之余也蕴含着一个朴素却深刻的道理,那就是在解决一个复杂问题时,最重要的是思路,但仅仅有思路也仍是远远不够的,还需要具体的执行方案。这个具体的执行方案,必须是连续的,其中不可以欠缺任何一环,不可以有任何思路或执行上的跳跃。所以解决任何复杂问题都没有银弹也没有捷径,我们必须把它弄清楚,搞明白,然后才能真正地解决它。 308 | 309 | 至此,组件库设计实战系列文章也将告一段落。在全部四篇文章中,我们分别讨论了组件库架构,组件分类,文档组织,国际化以及复杂组件设计这几个核心的话题,因笔者能力所限,其中自然有许多不足之处,烦请各位谅解。 310 | 311 | 组件库作为提升前端团队工作效率的重中之重,花再多时间去研究它都不为过。再加上与设计团队对接,形成设计语言,与后端团队对接,统一数据结构,组件库也可以说是前端工程师在拓展自身工作领域上的必经之路。 312 | 313 | **不要害怕重复造轮子,关键是每造一次轮子后,从中学到了什么。** 314 | 315 | 与各位共勉。 -------------------------------------------------------------------------------- /2018/从新的 Context API 看 React 应用设计模式/从新的 Context API 看 React 应用设计模式.md: -------------------------------------------------------------------------------- 1 | # 从新的 Context API 看 React 应用设计模式 2 | 在即将发布的 React v16.3.0 中,React 引入了新的声明式的,可透传 props 的 [Context API](https://github.com/facebook/react/pull/11818),对于新版 Context API 还不太了解朋友可以看一下笔者之前的一个[回答](https://www.zhihu.com/question/267168180/answer/319754359)。 3 | 4 | 受益于这次改动,React 开发者终于拥有了一个官方提供的安全稳定的 global store,子组件跨层级获取父组件数据及后续的更新都不再成为一个问题。这让我们不禁开始思考,相较于 Redux 等其他的第三方数据(状态)管理工具,使用 Context API 这种 vanilla React 支持的方式是不是一个更好的选择呢? 5 | 6 | ## Context vs. Redux 7 | 在 react + redux 已经成为了开始一个 React 项目标配的今天,我们似乎忘记了其实 react 本身是可以使用 state 和 props 来管理数据的,甚至对于目前市面上大部分的应用来说,对 redux 的不正确使用实际上增加了应用整体的复杂度及代码量。 8 | 9 | ### Vanilla React Global Store 10 | ```javascript 11 | import React from "react"; 12 | import { render } from "react-dom"; 13 | 14 | const initialState = { 15 | theme: "dark", 16 | color: "blue" 17 | }; 18 | 19 | const GlobalStoreContext = React.createContext({ 20 | ...initialState 21 | }); 22 | 23 | class GlobalStoreContextProvider extends React.Component { 24 | // initialState 25 | state = { 26 | ...initialState 27 | }; 28 | 29 | // reducer 30 | handleContextChange = action => { 31 | switch (action.type) { 32 | case "UPDATE_THEME": 33 | return this.setState({ 34 | theme: action.theme 35 | }); 36 | case "UPDATE_COLOR": 37 | return this.setState({ 38 | color: action.color 39 | }); 40 | case "UPDATE_THEME_THEN_COLOR": 41 | return new Promise(resolve => { 42 | resolve(action.theme); 43 | }) 44 | .then(theme => { 45 | this.setState({ 46 | theme 47 | }); 48 | return action.color; 49 | }) 50 | .then(color => { 51 | this.setState({ 52 | color 53 | }); 54 | }); 55 | default: 56 | return; 57 | } 58 | }; 59 | 60 | render() { 61 | return ( 62 | 69 | {this.props.children} 70 | 71 | ); 72 | } 73 | } 74 | 75 | const SubComponent = props => ( 76 |
77 | {/* action */} 78 | 88 |
{props.theme}
89 | {/* action */} 90 | 100 |
{props.color}
101 | {/* action */} 102 | 113 |
114 | ); 115 | 116 | class App extends React.Component { 117 | render() { 118 | return ( 119 | 120 | 121 | {context => ( 122 | 127 | )} 128 | 129 | 130 | ); 131 | } 132 | } 133 | 134 | render(, document.getElementById("root")); 135 | ``` 136 | 137 | 在上面的例子中,我们使用 Context API 实现了一个简单的 redux + react-redux,这证明了在新版 Context API 的支持下,原先 react-redux 帮我们做的一些工作现在我们可以自己来做了。另一方面,对于已经厌倦了整天都在写 action 和 reducer 的朋友们来说,在上面的例子中忽略掉 dispatch,action 等这些 Redux 中的概念,直接调用 React 中常见的 handleXXX 方法来 setState 也是完全没有问题的,可以有效地缓解 Redux 模板代码过多的问题。而对于 React 的初学者来说,更是省去了学习 Redux 及函数式编程相关概念与用法的过程。 138 | 139 | ### 正确地使用 Redux 140 | 从上面 Context 版本的 Redux 中可以看出,如果我们只需要 Redux 来做全局数据源并配合 props 透传使用的话,新版的 Context 可能是一个可以考虑的更简单的替代方案。另一方面,原生版本 Redux 的核心竞争力其实也并不在于此,而是其**中间件机制**以及社区中一系列非常成熟的中间件。 141 | 142 | 在 Context 版本中,用户行为(click)会直接调用 reducer 去更新数据。而在原生版本的 Redux 中,因为整个 action dispatch cycle 的存在,开发者可以在 dispatch action 前后,中心化地利用中间件机制去更好地跟踪/管理整个过程,如常用的 action logger,time travel 等中间件都受益于此。 143 | 144 | ### 渐进式地选择数据流工具 145 | #### Context 146 | * 我需要一个全局数据源且其他组件可以直接获取/改变全局数据源中的数据 147 | 148 | #### Redux 149 | * 我需要一个全局数据源且其他组件可以直接获取/改变全局数据源中的数据 150 | * 我需要全程跟踪/管理 action 的分发过程/顺序 151 | 152 | #### redux-thunk 153 | * 我需要一个全局数据源且其他组件可以直接获取/改变全局数据源中的数据 154 | * 我需要全程跟踪/管理 action 的分发过程/顺序 155 | * 我需要组件对同步或异步的 action 无感,调用异步 action 时不需要显式地传入 dispatch 156 | 157 | #### redux-saga 158 | * 我需要一个全局数据源且其他组件可以直接获取/改变全局数据源中的数据 159 | * 我需要全程跟踪/管理 action 的分发过程/顺序 160 | * 我需要组件对同步或异步的 action 无感,调用异步 action 时不需要显式地传入 dispatch 161 | * 我需要声明式地来表述复杂异步数据流(如长流程表单,请求失败后重试等),命令式的 thunk 对于复杂异步数据流的表现力有限 162 | 163 | ## Presentational vs. Container 164 | 时间回到 2015 年,那时 React 刚刚发布了 0.13 版本,Redux 也还没有成为 React 应用的标配,前端开发界讨论的[主题](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)是 **React 组件的最佳设计模式**,后来大家得出的结论是将所有组件分为 Presentational(展示型) 及 Container(容器型)两类可以极大地提升组件的可复用性。 165 | 166 | 但后来 Redux 的广泛流行逐渐掩盖了这个非常有价值的结论,开发者们开始习惯性地将所有组件都 connect 到 redux store 上,以方便地获取所需要的数据。 167 | 168 | 组件与组件之间的层级结构渐渐地只存在于 DOM 层面,大量展示型的组件被 connect 到了 redux store 上,以至于在其他页面想要复用这个组件时,开发者们更倾向于复制粘贴部分代码。最终导致了 redux store 越来越臃肿,应用的数据流并没有因为引入 Redux 而变得清晰,可复用的展示型组件越来越少,应用与应用之间越来越独立,没有人再愿意去思考应用层面的抽象与复用,项目越做越多,收获的却越来越少。 169 | 170 | 当所有的组件都与数据耦合在一起,视图层与数据层之间的界限也变得越来越模糊,这不仅彻底打破了 React 本身的分形结构,更是造成应用复杂度陡增的罪魁祸首。 171 | 172 | ## Context + Redux = 更好的 React 应用设计模式 173 | 除了更克制地使用 connect,区分展示型与容器型组件之外,受制于现在 Context API,开发者通常也会将主题,语言文件等数据挂在 redux store 的某个分支上。对于这类不常更新,却需要随时可以注入到任意组件的数据,使用新的 Context API 来实现依赖注入显然是一个更好的选择。 174 | 175 | ```javascript 176 | import React from "react"; 177 | import { render } from "react-dom"; 178 | import { createStore } from "redux"; 179 | import { Provider, connect } from "react-redux"; 180 | 181 | const ThemeContext = React.createContext("light"); 182 | class ThemeProvider extends React.Component { 183 | state = { 184 | theme: "light" 185 | }; 186 | 187 | render() { 188 | return ( 189 | 190 | {this.props.children} 191 | 192 | ); 193 | } 194 | } 195 | const LanguageContext = React.createContext("en"); 196 | class LanguageProvider extends React.Component { 197 | state = { 198 | laguage: "en" 199 | }; 200 | 201 | render() { 202 | return ( 203 | 204 | {this.props.children} 205 | 206 | ); 207 | } 208 | } 209 | const initialState = { 210 | todos: [] 211 | }; 212 | const todos = (state, action) => { 213 | switch (action.type) { 214 | case "ADD_TODO": 215 | return { 216 | todos: state.todos.concat([action.text]) 217 | }; 218 | default: 219 | return state; 220 | } 221 | }; 222 | function AppProviders({ children }) { 223 | const store = createStore(todos, initialState); 224 | return ( 225 | 226 | 227 | {children} 228 | 229 | 230 | ); 231 | } 232 | function ThemeAndLanguageConsumer({ children }) { 233 | return ( 234 | 235 | {language => ( 236 | 237 | {theme => children({ language, theme })} 238 | 239 | )} 240 | 241 | ); 242 | } 243 | 244 | const TodoList = props => ( 245 |
246 |
247 | {props.theme} and {props.language} 248 |
249 | {props.todos.map((todo, idx) =>
{todo}
)} 250 | 251 |
252 | ); 253 | 254 | const mapStateToProps = state => ({ 255 | todos: state.todos 256 | }); 257 | 258 | const mapDispatchToProps = { 259 | handleClick: () => ({ 260 | type: "ADD_TODO", 261 | text: "Awesome" 262 | }) 263 | }; 264 | 265 | const ToDoListContainer = connect(mapStateToProps, mapDispatchToProps)( 266 | TodoList 267 | ); 268 | 269 | class App extends React.Component { 270 | render() { 271 | return ( 272 | 273 | 274 | {({ theme, language }) => ( 275 | 276 | )} 277 | 278 | 279 | ); 280 | } 281 | } 282 | 283 | render(, document.getElementById("root")); 284 | ``` 285 | 286 | 在上面的这个完整的例子中,通过组合多个 Context Provider,我们最终得到了一个组合后的 Context Consumer: 287 | 288 | ```javascript 289 | 290 | {({ theme, language }) => ( 291 | 292 | )} 293 | 294 | ``` 295 | 296 | 另一方面,通过分离展示型组件和容器型组件,我们得到了一个纯净的 `TodoList` 组件: 297 | 298 | ```javascript 299 | const TodoList = props => ( 300 |
301 |
302 | {props.theme} and {props.language} 303 |
304 | {props.todos.map((todo, idx) =>
{todo}
)} 305 | 306 |
307 | ); 308 | ``` 309 | 310 | ## 小结 311 | 在 React v16.3.0 正式发布后,用 Context 来做依赖注入(theme,intl,buildConfig),用 Redux 来管理数据流,渐进式地根据业务场景选择 redux-thunk,redux-saga 或 redux-observable 来处理复杂异步情况,可能会是一种更好的 React 应用设计模式。 312 | 313 | 选择用什么样的工具从来都不是决定一个开发团队成败的关键,根据业务场景选择恰当的工具,并利用工具反过来约束开发者,最终达到控制整体项目复杂度的目的,才是促进一个开发团队不断提升的核心动力。 -------------------------------------------------------------------------------- /2017/服务端渲染与 Universal React App/服务端渲染与 Universal React App.md: -------------------------------------------------------------------------------- 1 | # 服务端渲染与 Universal React App 2 | 随着 Webpack 等前端构建工具的普及,客户端渲染因为其构建方便,部署简单等方面的优势,逐渐成为了现代网站的主流渲染模式。而在刚刚发布的 [React v16.0](https://reactjs.org/blog/2017/09/26/react-v16.0.html) 中,改进后更为优秀的服务端渲染性能作为六大更新点之一,被 React 官方重点提及。为此笔者还专门做了一个小调查,分别询问了二十位国内外(国内国外各十位)前端开发者,希望能够了解一下服务端渲染在使用 React 公司中所占的比例。 3 | 4 | 出人意料的是,十位国内的前端开发者中在生产环境使用服务端渲染的只有三位。而在国外的十位前端开发者中,使用服务端渲染的达到了惊人的八位。 5 | 6 | 这让人不禁开始思考,同是 React 的深度使用者,为什么国内外前端开发者在服务端渲染这个 React 核心功能的使用率上有着如此巨大的差别?在经过又一番刨根问底地询问后,真正的答案逐渐浮出水面,那就是可靠的 SEO(reliable SEO)。 7 | 8 | 相比较而言,国外公司对于 SEO 的重视程度要远高于国内公司,在这方面积累的经验也要远多于国内公司,前端页面上需要服务端塞入的内容也绝不仅仅是用户所看到的那些而已。所以对于国外的前端开发者来说,除去公司内部系统不谈,所有的客户端应用都需要做大量的 SEO 工作,服务端渲染也就顺理成章地成为了一个必选项。这也从一个侧面证明了国内外互联网环境的一个巨大差异,即虽然国际上也有诸如 Google,Facebook,Amazon 这样的巨头公司,但放眼整个互联网,这些巨头公司所产生的**黑洞效应**并没有国内 BAT 三家那样如此得明显,中小型公司依然有其生存的空间,搜索引擎所带来的自然流量就足够中小型公司可以活得很好。在这样的前提下,SEO 的重要性自然也就不言而喻了。 9 | 10 | 除去 SEO,服务端渲染对于前端应用的首屏加载速度也有着质的提升。特别是在 React v16.0 发布之后,新版 React 的服务端渲染性能相较于老版提升了三倍之多,这让已经在生产环境中使用服务端渲染的公司“免费”获得了一次网站加载速度提升的机会,同时也吸引了许多还未在生产环境中使用服务端渲染的开发者。 11 | 12 | ## 客户端渲染 vs. 服务端渲染 vs. 同构 13 | 在深入服务端渲染的细节之前,让我们先明确几个概念的具体含义。 14 | 15 | * 客户端渲染:页面在 JavaScript,CSS 等资源文件加载完毕后开始渲染,路由为客户端路由,也就是我们经常谈到的 SPA(Single Page Application)。 16 | * 服务端渲染:页面由服务端直接返回给浏览器,路由为服务端路由,URL 的变更会刷新页面,原理与 ASP,PHP 等传统后端框架类似。 17 | * 同构:英文表述为 Isomorphic 或 Universal,即编写的 JavaScript 代码可同时运行在浏览器及 Node.js 两套环境中,用服务端渲染来提升首屏的加载速度,首屏之后的路由由客户端控制,即在用户到达首屏后,整个应用仍是一个 SPA。 18 | 19 | 在明确了这三种渲染方案的具体含义后,我们可以发现,不论是客户端渲染还是服务端渲染,都有着其明显的缺陷,而同构显然是结合了二者优点之后的一种更好的解决方案。 20 | 21 | 但想在客户端写出一套完全符合同构要求的 React 代码并不是一件容易的事,与此同时还需要额外部署一套稳定的服务端渲染服务,这二者相加起来的开发或迁移成本都足以击溃许多想要尝试服务端渲染的 React 开发者的信心。 22 | 23 | 那么今天就让我们来一起总结下,符合同构要求的 React 代码都有哪些需要注意的地方,以及如何搭建起一个基础的服务端渲染服务。 24 | 25 | ## 总体架构 26 | 为了方便各位理解同构的具体实现过程,笔者基于 `react`,`react-router`,`redux` 以及 `webpack3` 实现了一个简单的[脚手架项目](https://github.com/AlanWei/react-boilerplate-ssr),支持客户端渲染和服务端渲染两种开发方式,供各位参考。 27 | 28 | ![](./architecture.jpg) 29 | 30 | 1. 服务端预先获取编译好的客户端代码及其他资源。 31 | 2. 服务端接收到用户的 HTTP 请求后,触发服务端的路由分发,将当前请求送至服务端渲染模块处理。 32 | 3. 服务端渲染模块根据当前请求的 URL 初始化 memory history 及 redux store。 33 | 4. 根据路由获取渲染当前页面所需要的异步请求(thunk)并获取数据。 34 | 5. 调用 renderToString 方法渲染 HTML 内容并将初始化完毕的 redux store 塞入 HTML 中,供客户端渲染时使用。 35 | 6. 客户端收到服务端返回的已渲染完毕的 HTML 内容并开始同步加载客户端 JavaScript,CSS,图片等其他资源。 36 | 7. 之后的流程与客户端渲染完全相同,客户端初始化 redux store,路由找到当前页面的组件,触发组件的生命周期函数,再次获取数据。唯一不同的是 redux store 的初始状态将由服务端在 HTML 中塞入的数据提供,以保证客户端渲染时可以得到与服务端渲染相同的结果。受益于 Virtual DOM 的 diff 算法,这里并不会触发一次冗余的客户端渲染。 37 | 38 | 在了解了同构的大致思路后,接下来再让我们对同构中需要注意的点逐一进行分析,与各位一起探讨同构的最佳实践。 39 | 40 | ## 客户端与服务端构建过程不同 41 | 因为运行环境与渲染目的的不同,共用一套代码的客户端与服务端在构建方面有着许多的不同之处。 42 | 43 | ### 入口(entry)不同 44 | 客户端的入口为 `ReactDOM.render` 所在的文件,即将根组件挂载在 DOM 节点上。而服务端因为没有 DOM 的存在,只需要拿到需要渲染的 react 组件即可。为此我们需要在客户端抽离出独立的 `createApp` 及 `createStore` 的方法。 45 | 46 | ```javascript 47 | // createApp.js 48 | 49 | import React from 'react'; 50 | import { Provider } from 'react-redux'; 51 | import Router from './router'; 52 | 53 | const createApp = (store, history) => ( 54 | 55 | 56 | 57 | ); 58 | 59 | export default createApp; 60 | ``` 61 | 62 | ```javascript 63 | // createStore.js 64 | 65 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 66 | import { routerReducer, routerMiddleware } from 'react-router-redux'; 67 | import reduxThunk from 'redux-thunk'; 68 | import reducers from './reducers'; 69 | import routes from './router/routes'; 70 | 71 | function createAppStore(history, preloadedState = {}) { 72 | // enhancers 73 | let composeEnhancers = compose; 74 | 75 | if (typeof window !== 'undefined') { 76 | // eslint-disable-next-line no-underscore-dangle 77 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 78 | } 79 | 80 | // middlewares 81 | const routeMiddleware = routerMiddleware(history); 82 | const middlewares = [ 83 | routeMiddleware, 84 | reduxThunk, 85 | ]; 86 | 87 | const store = createStore( 88 | combineReducers({ 89 | ...reducers, 90 | router: routerReducer, 91 | }), 92 | preloadedState, 93 | composeEnhancers(applyMiddleware(...middlewares)), 94 | ); 95 | 96 | return { 97 | store, 98 | history, 99 | routes, 100 | }; 101 | } 102 | 103 | export default createAppStore; 104 | ``` 105 | 106 | 并在 `app` 文件夹中将这两个方法一起输出出去: 107 | 108 | ```javascript 109 | import createApp from './createApp'; 110 | import createStore from './createStore'; 111 | 112 | export default { 113 | createApp, 114 | createStore, 115 | }; 116 | ``` 117 | 118 | ### 出口(output)不同 119 | 为了最大程度地提升用户体验,在客户端渲染时我们将根据路由对代码进行拆分,但在服务端渲染时,确定某段代码与当前路由之间的对应关系是一件非常繁琐的事情,所以我们选择将所有客户端代码打包成一个完整的 js 文件供服务端使用。 120 | 121 | 理想的打包结果如下: 122 | ```text 123 | ├── build 124 | │ └── v1.0.0 125 | │ ├── assets 126 | │ │ ├── 0.0.257727f5.js 127 | │ │ ├── 0.0.257727f5.js.map 128 | │ │ ├── 1.1.c3d038b9.js 129 | │ │ ├── 1.1.c3d038b9.js.map 130 | │ │ ├── 2.2.b11f6092.js 131 | │ │ ├── 2.2.b11f6092.js.map 132 | │ │ ├── 3.3.04ff628a.js 133 | │ │ ├── 3.3.04ff628a.js.map 134 | │ │ ├── client.fe149af4.js 135 | │ │ ├── client.fe149af4.js.map 136 | │ │ ├── css 137 | │ │ │ ├── style.db658e13004910514f8f.css 138 | │ │ │ └── style.db658e13004910514f8f.css.map 139 | │ │ ├── images 140 | │ │ │ └── 5d5d9eef.svg 141 | │ │ ├── vendor.db658e13.js 142 | │ │ └── vendor.db658e13.js.map 143 | │ ├── favicon.ico 144 | │ ├── index.html 145 | │ ├── manifest.json 146 | │ └── server (服务端需要的资源将被打包至这里) 147 | │ ├── assets 148 | │ │ ├── server.4b6bcd12.js 149 | │ │ └── server.4b6bcd12.js.map 150 | │ └── manifest.json 151 | ``` 152 | 153 | ### 使用的插件(plugin)不同 154 | 与客户端不同,除去 JavaScript 之外,服务端并不需要任何其他的资源,如 HTML 及 CSS 等,所以在构建服务端 JavaScript 时,诸如 `HtmlWebpackPlugin` 等客户端所特有的插件就可以省去了,具体细节各位可以参考项目中的 [webpack.config.js](https://github.com/AlanWei/react-boilerplate-ssr/blob/master/client/webpack.config.js)。 155 | 156 | ### 数据获取方式不同 157 | 异步数据获取一直都是服务端渲染做得不够优雅的一个地方,其主要问题在于无法直接复用客户端的数据获取方法。如在 redux 的前提下,服务端没有办法像客户端那样直接在组件的`componentDidMount` 中调用 action 去获取数据。 158 | 159 | 为了解决这一问题,我们针对每一个 view 为其抽象出了一个 thunk 文件,并将其绑定在客户端的路由文件中。这样我们就可以在服务端通过 `react-router-config` 提供的 `matchRoutes` 方法找到当前页面的 thunk,并在 `renderToString` 之前 dispatch 这些异步方法,将数据更新至 redux store 中,以保证 `renderToString` 的渲染结果是包含异步数据的。 160 | 161 | ```javascript 162 | // thunk.js 163 | import homeAction from '../home/action'; 164 | import action from './action'; 165 | 166 | const thunk = store => ([ 167 | store.dispatch(homeAction.getMessage()), 168 | store.dispatch(action.getUser()), 169 | ]); 170 | 171 | export default thunk; 172 | 173 | // createAsyncThunk.js 174 | import get from 'lodash/get'; 175 | import isArrayLikeObject from 'lodash/isArrayLikeObject'; 176 | 177 | function promisify(value) { 178 | if (typeof value.then === 'function') { 179 | return value; 180 | } 181 | 182 | if (isArrayLikeObject(value)) { 183 | return Promise.all(value); 184 | } 185 | 186 | return value; 187 | } 188 | 189 | function createAsyncThunk(thunk) { 190 | return store => ( 191 | thunk() 192 | .then(component => get(component, 'default', component)) 193 | .then(component => component(store)) 194 | .then(component => promisify(component)) 195 | ); 196 | } 197 | 198 | export default createAsyncThunk; 199 | 200 | // routes.js 201 | const routes = [{ 202 | path: '/', 203 | exact: true, 204 | component: AsyncHome, 205 | thunk: createAsyncThunk(() => import('../../views/home/thunk')), 206 | }, { 207 | path: '/user', 208 | component: AsyncUser, 209 | thunk: createAsyncThunk(() => import('../../views/user/thunk')), 210 | }]; 211 | ``` 212 | 213 | 服务端核心的页面渲染模块: 214 | 215 | ```javascript 216 | const ReactDOM = require('react-dom/server'); 217 | const { matchRoutes } = require('react-router-config'); 218 | const { Helmet } = require('react-helmet'); 219 | const serialize = require('serialize-javascript'); 220 | const createHistory = require('history/createMemoryHistory').default; 221 | const get = require('lodash/get'); 222 | const head = require('lodash/head'); 223 | const { getClientInstance } = require('../client'); 224 | 225 | // Initializes the store with the starting url = require( request. 226 | function configureStore(req, client) { 227 | console.info('server path', req.originalUrl); 228 | 229 | const history = createHistory({ initialEntries: [req.originalUrl] }); 230 | const preloadedState = {}; 231 | 232 | return client.app.createStore(history, preloadedState); 233 | } 234 | 235 | // This essentially starts passing down the "context" 236 | // object to the Promise "then" chain. 237 | function setContextForThenable(context) { 238 | return () => context; 239 | } 240 | 241 | // Prepares the HTML string and the appropriate headers 242 | // and subequently string replaces them into their placeholders 243 | function renderToHtml(context) { 244 | const { client, store, history } = context; 245 | const appObject = client.app.createApp(store, history); 246 | const appString = ReactDOM.renderToString(appObject); 247 | const helmet = Helmet.renderStatic(); 248 | const initialState = serialize(context.store.getState(), {isJSON: true}); 249 | 250 | context.renderedHtml = client 251 | .html() 252 | .replace(//g, appString) 253 | .replace(//g, ``) 254 | .replace(/<\/head>/g, [ 255 | helmet.title.toString(), 256 | helmet.meta.toString(), 257 | helmet.link.toString(), 258 | '', 259 | ].join('\n')) 260 | .replace(//g, ``) 261 | .replace(//g, ``); 262 | 263 | return context; 264 | } 265 | 266 | // SSR Main method 267 | // Note: Each function in the promise chain beyond the thenable context 268 | // should return the context or modified context. 269 | function serverRender(req, res) { 270 | const client = getClientInstance(res.locals.clientFolders); 271 | const { store, history, routes } = configureStore(req, client); 272 | 273 | const branch = matchRoutes(routes, req.originalUrl); 274 | const thunk = get(head(branch), 'route.thunk', () => {}); 275 | 276 | Promise.resolve(null) 277 | .then(thunk(store)) 278 | .then(setContextForThenable({ client, store, history })) 279 | .then(renderToHtml) 280 | .then((context) => { 281 | res.send(context.renderedHtml); 282 | return context; 283 | }) 284 | .catch((err) => { 285 | console.error(`SSR error: ${err}`); 286 | }); 287 | } 288 | 289 | module.exports = serverRender; 290 | ``` 291 | 292 | 在客户端,我们可以直接在 `componentDidMount` 中调用这些 action: 293 | 294 | ```javascript 295 | const mapDispatchToProps = { 296 | getUser: action.getUser, 297 | getMessage: homeAction.getMessage, 298 | }; 299 | 300 | componentDidMount() { 301 | this.props.getMessage(); 302 | this.props.getUser(); 303 | } 304 | ``` 305 | 306 | 在分离了服务端与客户端 dispatch 异步请求的方式后,我们还可以针对性地对服务端的 thunk 做进一步的优化,即只请求首屏渲染需要的数据,剩下的数据交给客户端在 js 加载完毕后再请求。 307 | 308 | 但这里又引出了另一个问题,比如在上面的例子中,getUser 和 getMessage 这两个异步请求分别在服务端与客户端各请求了一次,即我们在很短的时间内重复请求了同一个接口两次,这是可以避免的吗? 309 | 310 | 这样的数据获取方式在纯服务端渲染时自然是冗余的,但在同构的架构下,其实是无法避免的。因为我们并不知道用户在访问客户端的某个页面时,是从服务端路由来的(即首屏),还是从客户端路由(首屏之后的后续路由)来的。也就是说如果我们不在组件的 `componentDidMount` 中去获取异步数据的话,一旦用户到达了某个页面,再点击页面中的某个元素跳转至另一页面时,是不会触发服务端的数据获取的,因为这时走的实际上是客户端路由。 311 | 312 | ## 服务端渲染还能做些什么 313 | 除去 SEO 与首屏加速,在额外部署了一套服务端渲染服务后,我们当然希望它能为我们分担更多的事情,那么究竟有哪些事情放在服务端去做是更为合适的呢?笔者总结了以下几点。 314 | 315 | ### 初始化应用状态 316 | 除去获取当前页面的数据,在做了同构之后,客户端还可以将获取应用全局状态的一些请求也交由服务端去做,如获取当前时区,语言,设备信息,用户等通用的全局数据。这样客户端在初始化 redux store 时就可以直接获取到上述数据,从而加快其他页面的渲染速度。与此同时,在分离了这部分业务逻辑到服务端之后,客户端的业务逻辑也会变得更加清晰。当然,如果你想做一个纯粹的 Universal App,也可以把初始化应用状态封装成一个方法,让服务端与客户端都可以自由地去调用它。 317 | 318 | ### 更早的路由处理 319 | 相较于客户端,服务端可以更早地对当前 URL 进行一些业务逻辑上的判断。比如 `404` 时,服务端可以直接将另一个 `error.html` 的模板发送至客户端,用户也就可以在第一时间收到相应的反馈,而不需要等到所有 JavaScript 等客户端资源加载完毕之后,才看到由客户端渲染的 `404` 页面。 320 | 321 | ### Node.js 中间层 322 | 有了服务端渲染这一层后,服务端还可以帮助客户端向 Cookie 中注入一些后端 API 中没有的数据,甚至做一些接口聚合,数据格式化的工作。这时,我们所写的 Node.js 服务端就不再是一个单纯的渲染服务了,而是进化为了一个 Node.js 中间层,可以帮助客户端完成许多在客户端做不到或很难做到的事情。 323 | 324 | ## 要不要做同构 325 | 在分析了同构的具体实现细节并了解了同构的好处之后,我们也需要知道这一切的好处并不是没有代价的,同构或者说服务端渲染最大的瓶颈就是服务端的性能。 326 | 327 | 在用户规模大到一定程度之后,客户端渲染本身就是一个完美的分布式系统,我们可以充分地利用用户的电脑去运行 JavaScript 中那些复杂的运算,而服务端渲染却将这些工作全部揽了回来并加到了网站自己的服务器上。 328 | 329 | 所以,考虑到投入产出比,同构可能并不适用于前端需要大量计算(如包含大量图表的页面)且用户量非常巨大的应用,却非常适用于大部分的内容展示型网站,比如知乎就是一个很好的例子。以知乎为例,服务端渲染与客户端渲染的成本几乎是相同的,重点都在于获取用户时间线上的数据,这时多页面的服务端渲染可以很好地加快首屏渲染的速度,又因为运行 `renderToString` 时的计算量并不大,即使用户量很大,也仍然是一件值得去做的事情。 330 | 331 | ## 小结 332 | 结合之前文章中提到的[前端数据层](https://github.com/AlanWei/blog/issues/5)的概念,服务端渲染服务其实是一个很好的前端开发介入服务端开发的切入点,在完成了服务端渲染服务后,对数据接口做一些代理或整合也是非常值得去尝试的工作。 333 | 334 | 一个代码库之所以复杂,很多时候就是因为分层架构没有做好而导致其中某一个模块过于臃肿,集中了大部分的业务复杂度,但其他模块又根本帮不上忙。想要做好前端数据层的工作,只把眼光局限在客户端是远远不够的,将业务复杂度均分到客户端及服务端,并让两方分别承担各自适合的工作,可能会是一种更好的解法。 --------------------------------------------------------------------------------