简单对一篇完整的检查表目录做一个翻译,极力推荐阅读原文,文中也提到了大量的有关性能优化策略和方法的关键字和链接,方便查询。
这两天在组里协助排查一个 webpack dev 环境下重载时内存溢出的问题,现象是 webpack-dev-server 启动的过程中,在 node 10.xx 的环境下,一旦修改文件工程自动 reload 时会出现内存溢出导致服务直接中断。
排查原因,最终发现原来是脚手架在配置开发环境的 devtool 配置时,为了方便开发者 DEBUG,设置成了 cheap-module-source-map ,在代码修改时触发自动 load,在 loader 编译映射 sourcemap 的时候溢~出~了~,本文不会说明为什么溢出,排查的过程以及解决,其实花的时间也不长,重点说明一下 sourcemap 的作用以及使用过程的几点个人思考。
异常
1 | [27849:0x102880000] 133250 ms: Mark-sweep 1377.8 (1409.0) -> 1377.8 (1409.5) MB, 366.7 / 0.0 ms (average mu = 0.076, current mu = 0.015) allocation failure scavenge might not succeed |
好了,上面的报错信息看过就可以忘记了。
SourceMap 在 WEB 开发过程中扮演着很重要的角色,因为被浏览器执行的代码往往与开发过程中原始代码相差很多,源码经过了很多次转译、合并、混淆、压缩等等步骤几乎不具有可读性,那么开发中调试、监控到异常定位快速与本地的源码映射匹配就成了它的唯一“使命”。
SourceMap: <url> 属性,在请求时就能将映射的资源显示在浏览器的调试面板里。//# sourceMappingURL=path/to/xxx 这类注释,用来明确匹配到对应的指定文件方便跟踪。那么在使用 SourceMap 应该注意什么呢?
要发挥它的作用,就要生成的 SourceMap 文件尽量地映射成源码,方便 debug。
比如,Webpack 的 devtool 中支持很多种生成模式,其中常用的 cheap-module-source-map 配置,就可以映射方便定位 .tsx 的内容。
生成 SourceMap 映射文件的过程,是一个转码的过程,记录两边的映射关系,那么转换时性能就成为了一个需要平衡的问题。理想情况,开发时尽可能保证转换速率,而构建时可以降低构建的性能消耗,保证文件的映射还原度。
要避免源码泄漏,那么生成的 SourceMap 文件的存放位置就不能随意被外网访问到。
比如,异常监控系统中,为了方便开发者快速定位问题代码,在每一次构建时产生的 sourcemap 文件保存在内网指定一个位置,同时与当前上线部署版本做好匹配。
在做 Java 开发时候反编译这个词应该很常见,也有对应的工具用来混淆和反混淆,WEB 端的 JavaScript / CSS 通常来说对任何人都是透明地,所以一些关键逻辑(比如涉及抽奖,特殊逻辑)尽量不要写在 js 脚本中,可以放在服务端或者编译成诸如 WebAssembly 一类字节码来避免一些重要逻辑泄漏。
PS.附赠一篇昨天无意中看到的总结,写的实在挺好的,和业界大佬还是有很大差距,加油吧。
以下规则很受用,不分语言,甚至有一些规则可以作为日常做事的原则,日后逐条对应写一些个人的理解。
有关 UNIX 编程的规则(摘自原文):
]]>
- 模块原则:使用简洁的接口拼合简单的部件。
- 清晰原则:清晰胜于机巧。
- 组合原则:设计时考虑拼接组合。
- 分离原则:策略同机制分离,接口同引擎分离。
- 简洁原则:设计要简洁,复杂度能低则低。
- 吝啬原则:除非确无它法,不要编写庞大的程序。
- 透明性原则:设计要可见,以便审查和调试。
- 健壮原则:健壮源于透明与简洁。
- 表示原则:把知识叠入数据以求逻辑质朴而健壮。
- 通俗原则:接口设计避免标新立异。
- 缄默原则:如果一个程序没什么好说的,就沉默。
- 补救原则:出现异常时,马上退出并给出足够错误信息。
- 经济原则:宁花机器一分,不花程序员一秒。
- 生成原则:避免手工 hack,尽量编写程序去生成程序。
- 优化原则:雕琢前先要有原型,跑之前先学会走。
- 多样原则:决不相信所谓“不二法门”的断言。
- 扩展原则:设计着眼未来,未来总比预想来得快。
本篇是总结一个旧项目,项目在 2019 年 Q3 开发并上线运营,经历两个月迭代后目前已交接给其他团队维护,整理个人草稿箱发现这边还没有完成的总结,补充一些内容更新至此,内容应该是顺着回想起细节,后面再补充。
关键词:中后台系统、从零到壹
交易指挥中心是中台基础基础组件化向智能基础组件化升级的战略项目,同时驱动中台核心业务中心的平台化搭建。一期建成优惠监控、库存、订单、商品四大指挥中心系统以及门户,实现中台系统的从零到一的系统建设。
对于我们的前端团队意义在于,积累面向 B 端中后台系统开发经验,沉淀中后组件,配合中后台系统可视化构建平台完成。
由于中后台系统,页面功能及数据信息需要一定的访问权限,除了在数据返回之前控制以外,前端展示的权限菜单控制就很重要。
中后台系统其页面功能及数据信息比较敏感,在面向 C 端用户的基础上,只要控制当前基于数据安全方面的考虑,页面的访问控制非常重要。首先,在服务端接口数据保证权限校验的前提下,用户在前台访问页面所看到的菜单也需要进行权限控制。
出于上面的考虑,针对当前系统的菜单设计时可以前端架构采用动态菜单设计。用户有权限访问到的菜单通过权限接口返回,再由前端渲染。(这里没有将权限限制逻辑放在前台有两点考虑:1、HTTP 接口容易被抓去伪造,非权限菜单容易暴露;2、当前系统是基于 ERP 等方式单点登录,集成了 ERP 系统的角色控制,前台和后台没有必要做重复的权限判断逻辑;)
笔者曾在一家专注线上协同办公(OA)的软件公司,负责过一段组织模型与菜单角色模块,经历过基于 RBAC 模式的控制权限改造,从数据库设计、服务开发以及到前台实现。
前端这里要做的就是,需要同时支持两套方案
上面提到的两种方案的实现,遵循统一的一套数据结构就可以实现。
实现上面的设计,依赖于 jdwcli 创建的项目模板中页面工厂 PageFactory这个类,即采用工厂模式将菜单数据缓存,通过 React.lazy 实现动态引用,这样即达到分包的特点减小初始包的大小,又可以实现菜单控制。
1 | [ |
零售中台经过多年的沉淀,内部有很多的功能系统存在,在开发初期就基本确认无法通过段时间内一一重新开发实现,迁移旧页面这个功能也需要被考虑在内。
目前比较合适的方案有两种:
那么为了满足这两种方案,在前面设计菜单的同时,在数据结构中就要增加针对内嵌和跳转离开的标示,考虑前端路由当前页面是一个 IFRAME 内嵌系统,还是一个需要点击跳转离开的菜单。
完成这点以后,要做的就是开发一个统一接受 IFRAME 链接的 Component 即可。
1 | @withRouter |
BUT,上面的实现,我们似乎忽略了一个问题,我们只考虑到了用户点击菜单路由跳转过来的页面,由于我们使用的是 HASH 路由,并没有考虑如果用户在当前页面刷新或通过页面 URL 直接访问指定路由时,当前 Component 无法拿到 src 的 props 的。这里我采取的办法是:同时将 Store 中菜单信息监听到当前组件中,与当前页面 URL 中的 pathname 进行一次匹配。
1 | + @inject((stores: GlobalStores) => ({ |
中后台系统中,许多数据需要具有更丰富的展现形式,那么图与表的结合就是比较好的方案。
本项目中图采用 HighCharts 作为图库,考虑有两个原因:
这是一篇迟来的总结文章,其实如果时间允许,可以选择的方案还有很多,今天还刚刚看到 Airbnb 团队公布开源了一个新的可视化组件库。https://airbnb.io/visx/
内部实现的有 双轴图、堆叠图、百分比堆叠图、饼图、坐标系图(散点图的延伸)实现细节以及部分问题总结详见另外一篇撰写中总结。
对于中后台,健壮表单功能应该是必不可少的一环,由于当前项目都是比较简单的表单,目前的项目经验,大致分为两种:
这里提一个问题:当你看到 Form 表单会联想到什么?下面是目前我能想到的内容:
那么这些表单能否有通过可视化拖拽,自动生成吗?答案是肯定的,同组其他的同事就在调研实现这个问题,找机会深入讨论一下。
优惠指挥中心系统中价格力系数查询页面,品牌级联,级联菜单由于存在一个 3K 左右个的数据由后端一次性返回,需要前端在前台把数据组装成树接口分级,再传递到级联组件中,这对于浏览器中内存计算耗时,以及组件初始化大量数据的性能都造成很大影响,在开发初期没有发现,Mock 数据只有几十个。
解决方案:就要和后端商量将查询接口调整为分级查询,逐级进行查询,避免一次数量过大。这里还可以在继续深入优化的点,就是当前二级或三级级联数据被 load 过一次以后,前端缓存在当前页面内,鼠标划回父级数据时直接拿缓存数据。(当然,缓存永远是一把双刃剑,要考虑缓存什么时间失效,什么时间生效就要具体问题具体分析。)
又是一个经常被讨论到的问题,各种缓存策略,网络上可以找到很多优秀详细的文章,这里简单介绍一下本项目使用的一些缓存策略。
环境的隔离,应该是工程化中比较简单且常见的问题,在从零搭建系统过程中,也不免存在这个问题,我自己的解决方案是,利用头尾系统(内部系统)建立两套文件,分别对应预发测试和正式环境。预发测试环境需要经常更新,缓存时间设置也非常短,且请求的也是预发测试接口,这在上线前需要及时更换。
这点在项目构建打包时,可以根据设置环境来进行操作。
比如:利用 Babel 编译中移除项目内冗余的 console.log,线上仅保留 error 或者其他,那就可以在项目中的 .babelrc 进行如下配置:
1 | { |
这样在 npm script 中配置 BABEL_ENV=dev xxxx 依次类推可以根据需要配置,对打包进行区分。
注意: babelrc 中的 dev 是为了举例说明,配置成 development 和 production 当然最好,因为默认会读 NODE_ENV 环境变量的配置。
这里不提太多,总结了一个方便的项目模板,利用 VSCode 或者 WebStorm 自身来实现保存时自动按照 Prettier + ESLint -fix 来格式化和自动修复部分代码,同时还可以结合 husky 在提交之前再进行一次格式化,可以一定程度地约束项目代码。
Git 提交 message ,可以利用 commitizen + cz-conventional-changelog 来呈现更统一的提交信息。
中后台系统具有很强的业务属性,只要找到其中规律,可以抽象沉淀出一些通用的业务组件,这样一来可以为可视化搭建中后台系统积累物料,二来对于仅有的统一风格的 UI 组件库来的更有意义。
]]>读书笔记《Redis 入门指南》 李子骅. Redis 入门指南(第 2 版)(异步图书) (Chinese Edition) (Kindle Locations 627-632). 人民邮电出版社. Kindle Edition.
前阵子部门团建要去外地,路上大巴车来回有将近 6、7 个小时的空闲时间,排除和身边人玩 Switch 的时间,大部分时间都用来速读一本纯技术类工具书,《Redis 入门指南(第 2 版)》,粗略自己摘录并且记录一些读书笔记,记录于此,以备自己方便查阅。内容可以较多但仅限个人记录一定有所疏漏,还是建议使用到 Redis 的开发者朋友,使用 Dash 或者其他查看 API 一类工具放手边随时查阅。
转岗做前端之前,我是一名 Java 服务端工程师,最早接触 Redis 时,是在上个部门,将 Redis 作为缓存使用(在来京东之前,只使用过 memerorycache 以及 H2 内存数据库,Oracle、MySQL, MSSQL 水平一般,大部分使用 Hibernate 封装,加入京东以后基本使用 iBatis + MySQL ),最初使用 Redis 非常初级,直接调用被封装好的 API,基本使用到 GET 和 SET 操作,当时没有太深入,速读便于回顾。
Redis (Remote Dictionary Server)顾名思义他的存储形式是以字典形式存储结构,并且允许其他应用通过 TCP 协议进行读写操作。
Redis 数据库中的所有数据都存储在内存中。由于内存的读写速度远快于硬盘,因此在读写性能上对比其他基于硬盘存储的数据库系统有非常明显的优势,按照常理,存储在内存存在一个问题,就是一旦程序退出或者硬件断电,就会导致数据丢失,不过 Redis 提供了数据持久化到硬盘的方案支持,可以异步写入硬盘,同时不影响对客户端提供数据服务。
OS X
1 | brew install redis |
Linux 等
1 | wget http://download.redis.io/redisstable.tar |
默认端口:6379
| 文件名 | 说明 |
|---|---|
| redis-server | 服务 |
| redis-cli | 命令行客户端 |
| redis-benchmark | 性能测试工具 |
| redis-check-aof | AOF 文件修复工具 |
| redis-check-dump | RDB 文件检查工具 |
| redis-sentinel | Sentinel 服务器(v2.8 后) |
停止服务,Redis 会先断开客户端连接,然后根据配置持久化数据,最后退出。
启动时可以自定义配置 redis-server path/to/redis.conf
Redis 默认支持 16 个数据库,不可以自定义数据库名,只能根据编号命名,默认从 0 开始。
不支持为每一个数据库设置密码,一个客户端要么能访问所有库,要么没权限访问。
Redis 与 MySQL 等关系型数据库以二维表形式的存储有非常大的差异,Redis 是 NoSQL 中一员,采用 key - value 方式存储,它的键值支持存储的类型有下面集中类型:
key 和 value 都是字符串
SET 和 GET
提示 Redis 对于键的命名并没有强制的要求,但比较好的实践是用“对象类型:对象 ID:对象属性”来命名一个键,如使用键 user:1:friends 来存储 ID 为 1 的用户的好友列表。对于多个单词则推荐使用“.”分隔,一方面是沿用以前的习惯(Redis 以前版本的键名不能包含空格等特殊字符),另一方面是在 rediscli 中容易输入,无需使用双引号包裹。另外为了日后维护方便,键的命名一定要有意义,如 u:1:f 的可读性显然不如 user:1:friends 好(虽然采用较短的名称可以节省存储空间,但由于键值的长度往往远远大于键名的长度,所以这部分的节省大部分情况下并不如可读性来得重要)。
Hash,每一个键存储字段不同。
1 | HSET key field value |
自由地为任何键增减字段而不影响其他键。
KEYS 命令需要遍历数据库中所有键,出于性能考虑,一般很少在生产环境使用。
List 可以存储一个有序的字符串列表,内部是 双向链表 实现的,所以在列表两端增加元素时间负责度都是 O(1),获取越接近两端的元素速度越快。链表的缺点是通过索引来访问某一个元素慢。
这种特性使列表类型能非常快速地完成关系数据库难以应付的场景:如社交网站的新鲜事,我们关心的只是最新的内容,使用列表类型存储,即使新鲜事的总数达到几千万个,获取其中最新的 100 条数据也是极快的。同样因为在两端插入记录的时间复杂度是 O(1),列表类型也适合用来记录日志,可以保证加入新日志的速度不会受到已有日志数量的影响。
1 | LPUSH key value [value ...] # 向列表左边追加 |
集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型在 Redis 内部是使用值为空的散列表(hashtable)实现的,所以这些操作的时间复杂度都是 O(1)。最方便的是多个集合类型键之间还可以进行并集、交集和差集运算…
1 | SADD key member [member] # 添加 |
区别于列表类型:
(1)列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会较慢,所以它更加适合实现如“新鲜事”或“日志”这样很少访问中间元素的应用。(2)有序集合类型是使用散列表和跳跃表(Skiplist)实现的,所以即使读取位于中间部分的数据速度也很快(时间复杂度是 O(log(N)))。(3)列表中不能简单地调整某个元素的位置,但是有序集合可以(通过更改这个元素的分数)。(4)有序集合要比列表类型更耗费内存。
1 | ZADD key score member [score member ...] # 增加元素 |
关系型 SQL 数据库一个非常大的特点就是事务的特性,同样也是 Redis 命令的最小的执行单元,一个事务要么执行,要么不执行。(关系型事务,多个表操作,同一个事务内要么都成功,要么都失败。)
1 | MULTI |
Redis 事务可以保证一个事务内的命令依次执行而不被其他命令插入。
Redis 事务的异常处理,首先需要先明确什么原因导致执行出错。1)语法错,一旦前面有错,后面不会执行;2)运行错,一旦有错,后续的命令会继续执行;
Redis 的事务没有关系数据库事务提供的回滚(rollback)[1]功能。为此开发者必须在事务执行出错后自己收拾剩下的摊子(将数据库复原回事务执行前的状态等)。
WATCH 命令,WATCH 命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。
1 | EXPIRE key seconds # 单位是秒,过期时间到了以后Redis自动删除key |
排序:SORT 命令,BY 参数,GET 参数,STORE 参数
生产/消费者模式,分发消息以及任务队列的实现,可以借助 Redis。
使用 Redis 实现简单的任务队列,一边 LPUSH,一边 RPOP 即可。(BRPOP 命令)
优先级队列,BRPOP key [key …] timeout
发布/订阅模式
1 | PUBLISH channel message |
存在规则订阅,PSUBSCRIBE 命令。
节省空间方法:1)精简键名和键值;2)内部编码优化
Lua 语言(Open Rest,Nginx 也可以使用 lua 语言,有机会学习了解一下)
这里不详细记录 Lua 语法,不过有一点思考,既然 Nginx 也可以使用 Lua,那么可以就有一种场景,Nginx 通过 lua 访问 Redis 读取数据,并且用 lua 渲染模板,达到页面直出,这样应该效率很高。
appendonly yes 开启允许同时开启 RDB 和 AOF 两种模式。
Redis 支持集群,可以通过主从数据库来来规避单点数据库故障导致的问题。主数据库负责读写(读写分离也可以),当写操作导致数据变化时自动将数据同步给从库,从库只读,并只接受主库同步数据。
配置文件,通过 slaveof 主库地址 主库端口 来完成主从复制的配置。
通过复制可以实现读写分离,以提高服务器负载能力。
关键字记录,Redis 支持哨兵,一主多从,需要自动监控 Redis 运行情况,作用:1)监控主从数据库运行正常;2)主数据库故障自动将从数据库转换成主数据库;细节待补充一篇琢磨透彻一点的分析文。
Redis 3.0 支持集群一大特点,还在学习中,这里不做细描述。(作者自己还一知半解,需要点时间和资料消化消化)
默认 Redis 允许所有连接,可以通过 bind 绑定某一地址,可以设置数据库密码,提升安全性。
将命令重命名,确保部分影响性能或整个数据库的命令,被自定义成一个新命令。
个人拙见:
前阵子一直和组里的小伙伴共同“造轮子”,开发并维护了一套 PC 端 React UI 组件库,经过了一段时间的折腾,组件库从之前的 0.x、1.x 再到最近发布的 2.0.beta 的一个过程,这其中很多东西值得拿出来分享和讨论,有好的有失败的,今天就把组件库开发过程中的 DEMO 实时重现以及后期文档站的建设的技术选型以及实践简单做一个记录总结和大家一起做一个讨论。
调研和选型具体细节,后面找时间再梳理输出另外一篇文章,这里介绍在使用 Storybook 5 的过程中的一些问题点以及经验分享。
截止文章编写时 Storybook 6 正在进行 rc 版,作者也经历过将之前项目的
5.2.x升级到5.3.x过程的阵痛,网络上关于使用 Storybook 的问题文章也比较少,除了 Storybook 官网文档以外一些问题点记录,因此成文,感兴趣可以继续阅读。有关 UI 组件库的建设,这里不做深入讨论。(就是我们暂时先不讨论 WHY 的问题,本文只讨论 HOW。)

首先,个人总结 Storybook 几点优势,也是基于下面几个优势最终决定选型它的原因
*.stories.js,还是后期因为要统一文档站说明和一些 UI,我们改用 MDX 重写了使用说明,不过大部分 API 列表还是基于 Props 的实现定义 Interface 中 JSDoc 自动渲染,非常方便;官方文档,支持 TypeScript 编写组件,文档中罗列了几个选项,出于使用习惯以及 Babel 与 Microsoft 的合作关系,推荐使用 babel-loader 方式。(虽然它很慢,所以要尽量控制编译的范围。)
当使用 babel 编译 TypeScript 的时候,存在两个问题
production 依然会编译生成 sourcemap,也就需要手动将 devtool 改为 false,这里应该给 storybook 提一个 issue。文档见这里,有一句说明:注意:sourceMap 选项是被忽略的。当 webpack 配置了 sourceMap 时(通过 devtool 配置选项),将会自动生成 sourceMap。关于声明文件的生成有两个方案:
tsc 本身具备的能力,结合 --emitDeclarationOnly参数和输出目录来只输出 *.d.tsgulp 一样的道理通过单独编译声明文件并输出到与组件代码同级目录上面的方案核心就是单独编译声明文件即可,即便是微软官方提供的例子也是单独输出声明文件,详见这里,如果小伙们有其他好办法欢迎留言讨论。
1 | build-storybook -c .storybook -o docs-static |
从打包命令中就可以看到,构建的配置支持 .storybook 文件夹下配置文件,默认读取 main.js 作为构建的补充,这里完全就是遵循 webpack 的配置,比如这里 webpackFinal: async (config) => { ...} 这段代码中拿到当前构建的全部 config 配置对象,那么既然拿到这个对象,那理论上就可以调整整个的构建过程。(当然,Storybook 构建确实使用了很多的 loader 和 plugin )。
静态站的打包和部署就和一个普通静态站部署没什么区别,外挂一个 HTTP 服务即可。由于 storybook 默认将每一个 stories (也就是组件)构建时进行了分包懒加载,访问每一个 stories 的时候内嵌的 iframe 展示时都只会请求当前组件的内容,首屏加载的内容也不是很多,经过简单的优化基本就可以实现很好的首屏加载。

当然静态站上的主题样式覆盖,Storybook 还做得不够开放,虽然它开放了定制 theme 主题的方式,但是仍有一些细节处无法完全定制,我们的实现方式是通过手写 css 覆盖解决。


默认情况下,左侧组件导航区的排序是按照字母排序规则,但当你的“业务方”想要一个所谓“有规划、基于设计原则”的排序进行展示就变的有一些难度,Storybook 预留了预览的 API 中就提供了一个可以自定义排序的方式,详见文档。
那么,需要我们做什么呢,那就是手动去实现一个简单的排序算法,让组件 stories 按照期望的方式进行排序。
这里简单描述一下个人实现思路:
map 作为字典,作用单一就是根据 key 返回序号;stories 中定义的 title 拿到用作排序时,作为 key 备用在字段中查询;1 | // preview.js |
Storybook 中文档即代码功能的实现还是很有意思的,通过强大的 @storybook/addon-docs 插件(在 5.3.x 版本开始逐步废弃 @storybook/addon-info 插件,基本不再维护)可以实现文档中众多元素的直接渲染,我们经常用的组件有 import { Meta, Story, Preview, Props, Source, Title, Subtitle } from "@storybook/addon-docs/blocks"; 这些,其中着重介绍 Preview 和 Props 组件。
可以结合 JSDoc 自动生成文档,如下图:

感兴趣的同学可以抽时间研读了一下具体实现,源码地址
这里描述一下自己看源码总结的实现思路,借助 JSDoc 的 parser 读取每一个参数 component 中 props 的属性的注释内容,并且与当前属性建立对应关系后 render 一个显示的 table 就自动生成了下面的文档。

插件选择失误,由于最一开始的选择错误,错误地选用 addon-info 已经被废弃的插件,导致后面升级后,在静态文档站需要定制化的需求出现时,原有 addon-info 出现的问题层出不穷,不得不切换至 addon-doc 插件,索性直接使用 mdx 对所有组件文档进行了一次重写,好在大部分代码和 DEMO 可以直接复用和自动生成。
当然,我们目前并没有使用 Storybook 的全部功能,文章的最后也会列举调研期间收集到的非常牛团队开发的组件库和文档实例,那么我们可以学习到什么呢?
首先,我们所谓的重复造出来的轮子到底是有意义?答案虽然是肯定的,业界已经有很多成熟的案例,很多公司、部门、团队都可能有自己的库,早在我们在开始开发实现之初,但是我们为什么要做呢?这就是另外一个话题。我们不深入这个话题,仅从过程中讨论,我认为不仅仅只考虑组件的设计、易用性(API 等)、稳定性(质量)、组件的抽象、以及复用性等等这些问题,同时也应该作为组件使用者的角度来思考,从适用场景出发,当使用者决定是否使用某一组件时,组件的文档等等就是一个非常重要支持点,这里刚好 Storybook 帮我们解决了这其中的几个问题,看到这里感兴趣的小伙伴可以留言讨论,当你的团队或者新项目在选择 UI 库时基于哪些考虑?
再者,聚焦,Storybook 团队聚焦在组件的开发环境,单一功能强大通过拼接实现更多的功能,这种思想一定要多运用在实际情况中,但是也要注意区分场景,没必要成为“为了用而用”。
最后,UI 组件库应该是前端团队,最容易想到也是最难做好的一个 KPI 产物,这其中还有很多值得思考和讨论!
ESLint 为团队和项目带来的益处,这里不多说,本文主要是个人近期修改项目代码一些错误修改整理的内容。
备注:尽量使用默认 Prettier 规则,根据实际情况略有调整。项目技术栈 React, Redux, Redux-Sagas, TypeScript。
"plugin:react/recommended""plugin:@typescript-eslint/recommended""prettier/@typescript-eslint""plugin:prettier/recommended"[Error] eslint@typescript-eslint/ban-ts-comment
Do not use “// @ts-nocheck” because it alters compilation errors. Desc link
TypeScript 提供 @ts-expect-error @ts-ignore @ts-nocheck @ts-check 指令注释方式,用来改变 tsc 编译时处理文件的方式,如果大量使用此类注释影响 TypeScript 的特性,既然使用 ts 就要拥抱它的特性,lint 中默认对 no-check 会按照 error 进行提示;可以根据实际情况调整规则,改为 warn。
[Error] @typescript-eslint/ban-types
补充提示:
1 | Don't use `{}` as a type. `{}` actually means "any non-nullish value". |
[Error] react/jsx-no-target-blank
出于安全考虑,React 中产生新打开页面的链接,需要增加 rel='noreferrer' 用来保护原站。具体说明详见:Link
[Warn] Missing return type on function.eslint@typescript-eslint/explicit-module-boundary-types
desc link
说明:针对函数的定义,建议每一个函数都要显式的表明函数返回值。这在 *.jsx, *.tsx 文件中,React 生命周期函数都提示,可以使用 eslint overrides 规则,只针对 *.js, *.ts 生效。
1 | { |
应该是最经常遇到的一个警告,定义了变量,下文没有使用。
个人建议:非关键算法或逻辑代码,当你阅读时没用,就删掉吧,可以保留注释,因为即使你想保留这段代码,以备不时之需,但是真到需要用到代码你再次阅读的时候,实现思路以及上下文联系可能已经相差很远。
Prettier 自动 v2.0.0 开始,将 trailingComma 默认配置由 none 改为 es5,在我看来是一种很好的方式。
举个例子,当 import 多个内容、一个对象需要增加属性、一个数组追加元素,如果默认已经追加了逗号,那么就可以直接追加,而不必要移动光标去前一行手动增加一个逗号,这样就增加了一些便利性,与此同时便于快速定位甚至避免由于一个逗号的引起的错误。
1 | import { AriaAttributes, DOMAttributes } from "react"; |
参考:https://dev.to/lukethacoder/use-custom-html-attribute-s-in-typescript-2co
问题实例片段代码:
1 | function* loadSimilar(skuId: string) { |
修改后代码:
1 | - function* loadSimilar(skuId: string) { |
解答:Redux-Saga 中 takeEvery 第二个参数是一个 action,所以定义 loadSimilar 时候需要遵循 TakeableChannel<unknown> ,定义 type。
参考:https://stackoverflow.com/a/60558041
切记,保证一路传递参数变量匹配,否则会出现无法赋值问题,例如下面代码:
1 | - function* cartOptCheckOne({ param }: { type: string; param: any }) { |
外层触发 action 传参数代码如下:
1 | this.props.optCartCheckOne({ |
如果定义 cartOptCheckOne 使用 param 就会导致传递过程中因为变量名称不同无法解构,导致传参中断。
参考 https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
]]>原文 https://developers.google.com/search/docs/guides/javascript-seo-basics
你是否怀疑过由于 JavaScript 的问题可能会影响你的网页或者某一部分内容出现在 Google Search 的结果? 你通过我们的故障排除指南来了解如何解决 JavaScript 相关的这些问题。
JavaScript 是 WEB 平台中重要的一部分,因为它能够提供由普通 web 网站向功能更强大的应用平台转变的众多特性。开发 JavaScript 驱动的 web 应用可以帮助你吸引新用户,同时可以通过 Google 搜索到你的 web 应用提供出来的有效内容来留存现有用户。当 Google Search 通过 Chromium 引擎来执行 JavaScript 时,你可以做一些下面这些事情去进行优化改进你的 Web 应用。
下面这个指南描述了 Google Search 是如何处理 JavaScript 的,同时也在展示提升 Google Search SEO 结果的最佳实践。
一段油管视频地址
Googlebot 处理 JavaScript web 应用存在三个主要阶段:
Googlebot 爬取,渲染并索引页面的步骤。

Googlebot 会将网页排入队列,以便爬取和渲染。当页面正在等待被爬取时和正在等待等待渲染时,并不容易被用户立即察觉到。
当 Googlebot 从爬取队列中获取一个 URL 来创建一个 HTTP 请求时,它首先会检查你的网站是否允许被爬取。Googlebot 会阅读 robot.txt 文件。如果它标记了当前 URL 不允许爬取,Googlebot 会跳过对这个 URL 的 HTTP 请求。
Googlebot 会解析在 HTML 链接中的 href 属性转换出来的其他 URL 的响应,同时将这些 URL 放在待爬取的队列中。如果要阻止链接的查找,可以使用 nofollow mechanism。
爬取 URL 时处理 HTML 响应在传统型网站和服务端渲染页面的网站很奏效,服务端渲染网站其中的 HTTP 响应中包含了全部的内容。一些 JavaScript 网站可能会用到 app shell model,这些网站在初始 HTML 内容中并不包含实际的内容,Googlebot 需要在看到十几页面内容之前执行 JavaScript。
Googlebot 将需要渲染的页面排入队列,除非网站的 robots meta tag or header 信息告诉 Googlebot 不要对这个页面进行索引。这个页面可能在队列中短暂停留,但是它将花费更长的时间。一旦 Googlebot 的资源允许,headless Chromium 可以渲染这个页面并且执行 JavaScript。Googlebot 会再次解析渲染完成的 HTML 链接,并且按照队列查找的 URL 继续爬取。Googlebot 也会使用渲染完成的 HTML 对页面进行索引。
请记住,服务端渲染或者预加载仍然是很好的解决方案,因为这个方案可以使你的网站面向用户、爬虫时响应迅速,而不是所有 bots 可以运行 JavaScript。
唯一的,可描述的标题以及有用的 meta 描述信息可以帮助用户快速地有目的性的定位最佳的搜索结果,我们在指南中解释什么是好的 title 和 description。
你也可以使用 JavaScript 设置或者修改 title 和 meta 描述信息。
Google Search 可以基于用户的查询展示不同的标题和描述。当标题或者说明与页面内容相关性较低,或者我们在页面中发现与搜索结果更匹配的替代方法时,就会发生这种情况。有关标题和说明的代码片段的详细信息,请参与此页面。
浏览器提供了很多 API 并且 JavaScript 也是一个快速进化的编程语言。Googlebot 在支持哪些 API 和 JavaScript 功能方面有一些限制。为了确保你的代码可以兼容 Googlebot,请遵循我们的 JavaScript 故障指南。
如果你发现你需要的 API 缺失,我们建议使用不同的服务和 polyfills。如果一些浏览器特性没有被 polyfill 补丁,我们建议你多看 polyfill 文档,了解潜在的限制。
在爬取页面时,Googlebot 使用 HTTP 状态码识别页面的一些问题。
你可以使用有意义的状态码告诉 Googlebot 当 前页面是否需要被爬取或者索引,比如 404 代表页面已经失联,401 代表登录前的页面。你可以使用 HTTP 状态码告诉 Googlebot 这个页面被转移到一个新的 URL ,因此索引可以被及时更新。
下面是 HTTP 的状态码列表
| HTTP status | When to use |
|---|---|
| 301/302 | 当前页面已经跳转到新 URL |
| 401/403 | 当前页面由于权限问题不可访问 |
| 404/410 | 当前页面不存在 |
| 5xx | 服务端异常 |
你可以通过 meta robots 标签标记来阻止 Googlebot 对页面标记索引页面。举个例子,在页面中增加下面一个标签:
1 | <!-- Googlebot won't index this page or follow links on this page --> |
当 Googlebot 在 robots meta 标签中遇到 noindex,它就不会渲染或者索引这个页面。
使用 JavaScript 修改或删除
robots meta标签可能不会如期望似的生效。如果 meta 标签包含noindex,Googlebot 会跳过渲染和 JavaScript 执行。如果你想要使用 JavaScript 修改 robots meta 标签,需要设置noindex。
图片加载在宽带和性能方面会带来很大的消耗。一个好的策略是使用延迟加载,仅在用户即将看到图像时加载图片。为了确保更好的实现延迟加载,请遵循我们的延迟加载指南。
]]>近期在准备部门内部的一个分享,思考了一些主题,最终还是决定结合自身 Web 服务端开发经验,来分享自己调研 Serverless 无服务建构的相关内容。
以下全文是前期准备阶段调研和整理的一部分资料,梳理总结后,在制作 Slide 过程中书写逐字稿缩减而来的。分享结束后还会继续在这部分挖掘和实践,云计算中的 Serverless 是一个非常大的主题范畴,单凭这一篇文字是没办法细说全部,笔者也在路上,感兴趣的同学可以留言讨论,持续关注。
当今社会技术热点一直围绕区块链、AI、机器学习等领域,它们背后离不开云计算的支撑。云计算在经历多年的发展,从基础设施到应用场景拓展和落地方面,都已经取得了长足的进步,大到国家层面的云计算中心纷纷落地,小到商业上各大云服务商和互联网IT厂商纷纷推出各自的云产品。在众多云计算解决方案中,Serverless 逐渐崭露头角,受到了很多关注并且发展迅猛,如果你也像笔者一样感兴趣想了解一下的话,那么请继续阅读吧!
提到云计算的发展,我们可以先来简单了解云计算应用架构在没有出现 Serverless 概念之前的演进过程。
首先,先来通过一张网络流传很久的 Pizza as a Service 图来了解一下几个缩写,图源来自 Medium Blog

IDC:Internet Data Center 自建互联网数据中心,通俗讲就是硬件机房,自行维护硬件资源、物理机器、网络环境、机房温控等等等等,在开发阶段完成后,还需要进行繁重且耗时很长的部署过程。对应的就是左侧第一个传统部署的方案。IaaS:Infrastructure as a Service 基础设施服务,租用公共数据中心的物理机器资源,节省了一部分机器的成本。PaaS:Platform as a service 平台即服务,软件厂商提供一个平台,使用者只需要接入这个平台,就可以使用服务。SaaS:Software as a service 软件即服务,面向普通用户,用户只需要购买和使用软件就可以享受这种服务,像 Oracle 和用友财务软件、日常使用的 ERP 系统、办公自动化系统的软件等等,笔者之前所在的公司就是从事这类软件的研发,有固定的人群使用软件解决他们的问题。从左到右的演进的过程,我们可以看到的是软件应用架构在这个过程中,一步一步地将环境、硬件、部署、运维与开发进行了分离。
近期 Pizza as a Service 更新 2.0,将近期火热的 CaaS,FaaS 页加入到其中,如下图:

CaaS 容器即服务,它是基于以 Docker 为主的容器技术,还有由 Google 开源的 Kubernetes(K8S)进行容器自动编排等技术兴起而形成的一个服务架构。它将整个应用拆分成独立的微小的部分,实现快速部署,提供细粒度的微服务。
FaaS 就是 Serverless 中的其中一种方案,也是我们本文关注的重点。
好了,讲了一些枯燥的概念,那么我们回到我们的目前的工作岗位和内容。
作为一名前端工程师,大家日常业务开发的工作内容是什么?
切图还原业务功能视觉稿;用浏览器脚本去完成交互;调用数据接口、完成业务数据的展现或者操作;开发 UI 组件库;开发工程化共建工具;开发应用系统;参与服务端、客户端所谓大前端的融合;开发一款轻量的客户端游戏等等等等。
这些或许都是作为一名前端工程师的工作内容。
笔者日常工作和平时了解到前端工程师大概是以上这些内容,如果有遗漏欢迎补充讨论。
现在请大家换一个角度思考,如果我们没有所谓公司的既有平台、如果我们正处创业初期、如果我们是一名独立开发者、如果没有从开发到部署的完善的整套流程、没有 CDN、没有数据接口,我们要如何开发一个动态数据的网站或是一个独立的应用?
根据以往的经验,在开发业务功能的过程中对于依赖的服务、数据等,我们一般话语权不高,仅仅是服务的使用者,那么我们能成为服务的生产者吗?
有的朋友会说:“我可以用 NodeJS 写服务,自行维护 SQL、NoSQL 作为数据支撑,在容器构建运行,最终对外提供服务!” NodeJS 帮助我们这些善于写 JavaScript 代码的前端工程师,拓宽工作范围和内容,笔者所在的部门内部基于 NodeJS 开发系统和应用都是很好的例子,我们可以写越来越多的服务端逻辑,来直接提供服务。
这里简单提一下 GragphQL 这类方案不在本次分享的范围内,它的出现使得数据更具有弹性,但笔者认为 Serverless 的出现让服务的产生更彻底更直接。
那么,回到文中最开始说的披萨服务模型,如果不借助云服务,就会遇到这样的困惑。要选什么样的容器作为载体,什么配置的硬件机器,几核 CPU、内存、网络带宽;如果选择要选择云服务商云主机,可能有的同学会考虑,去亚马逊、微软、Google Cloud、阿里云、腾讯云、新浪云、华为云、京东云等等这样云厂商这里“选购”各种让人眼花缭乱的“配置组合”的云主机,让人非常苦恼。
既要为应用规模大小、扩容需求、安装哪一种操作系统、配合哪一种数据库、未来如何组成集群、如何做负载均衡、如何打通开发到部署流程等等等等问题而操心。即便是现在有 Docker、K8S 这样的容器技术和自动编排来辅助,他们虽然可以帮助你做到应用秒级部署,快速重复部署,也依然麻烦,仍然需要付出高昂地运维技术的学习成本,即便你是一个全能达人,你也需要花费大量的时间和精力去解决这些环境和资源的问题,这就对想快速实现一个点子开发出原型或者开发一个小型应用的你来说,像是拦在路上的不得不跨越的一座山,拖慢来你和你团队的行进速度。

绝大部分应用在云主机或虚拟机上线后,其 CPU 利用率、内存使用率、网络带宽的利用率都非常低,因为你在选购过程中就要为未来新增的流量做好缓冲,你的服务一旦 7X24 的上线运行,你就要为这些冗余的运行时产生的费用自掏腰包。我们现如今公司内部的基于私有云架构开发和部署也是如此的问题,都是需要开发者为这些运维相关的技术债务来埋单,有的同学也会经常收到自动监控系统发出的效率低的提示邮件。
那么既然有这么多困难,我们前端工程师还有机会吗?
希望总是会有的!
Serverless 架构的出现,为我们提供了一个窗口。目前还没有一个普遍公认的权威定义,最新的一个定义是这样描述的。
“无服务器架构是基于互联网的系统,其中应用开发不使用常规的服务进程。相反,它们仅依赖于第三方服务,客户端逻辑与托管远程服务之间的调用过程的组合。” – Wiki
这个概念在 2012 年由 iron.io 公司首先提出,而在 2014 年由 AWS 发布 Lambda 时正式商业化使用,目前 AWS 在云计算市场占有率高达 44%(2017 年数据)可以说是业界大佬,同时 Microsoft、Google、IBM 等国际大型 IT 公司都有对应的云产品;在国内,像京东云、阿里云、腾讯云、华为云也都提供了基础的功能,并且据说阿里内部正在力推适合的场景从其他框架向 Serverless 架构转变,从几个阿里前端的 BLOG 、讨论、以及业界大会分享主题来看,可以看到很多人在关注这种架构。

支持 Serverless 架构的云厂商,允许使用者可以开发服务端逻辑,并将其放在无状态的云计算容器中,由事件来进行触发,其他完全交给云来管理。
Serverless 分为两种:
BaaS Backend as a Service 后端即服务 如文件存储、数据存储、推送服务、身份统一验证等,我们平时写 NodeJS 或者 Java 都是接触不到的,不是这次讨论的主角。FaaS Functions as a Service 函数即服务 服务商提供一个平台,提供给用户开发、运行和管理这些函数的功能,而无需搭建和维护基础框架。这是我们关注的重点。FaaS 是一种事件驱动的由消息触发的函数服务。函数 Function顾名思义,计算机编程领域上的函数,有着最基础的定义,就是调用函数传入 INPUT 得到输出 OUTPUT,函数内部对于使用者可以是黑盒的,这云计算中也就可以理解成为是一种函数的服务。云厂商一般都会集成很多同步或者异步的事件源,通过订阅这些事件,去达到条件触发、定时触发来运行某一个云函数的效果。FaaS 允许我们上传一个完整的函数代码片段,这个函数理论上提供单一的无状态的服务,当事件触发执行这个函数的时候,它会创建实例、启动并开始执行,完成服务后等待被销毁,不存在上下文信息和状态。换言之,就是如果函数不运行,这个实例就不存在,云厂商的计费方式也发生了调整,既然没有实例存在运行,造成损耗,那么就没有产生计费。
可以直接理解为:只有当你的函数执行的时候才会按照运行次数收费,如下图,我们关心的只是应用层中的函数部分!

事件触发,给大家举一个类似例子,有一款老牌网络应用:
IFTTT:汇聚了世界各种有趣的 API ,通过触发一件、一件事来形成链式的调用完成有趣功能。比如,当你打一辆 Uber 回家的路上,当快到家附近的时候,它会自动触发家里的灯打开,空调开启;比如,你发布一个 Instagram Photo 会自动将图片存储到 Google Photo 或者同步到微博;诸如此类,都是当发生一个事件的时候才去触发下一个事件;FaaS 可以与 BaaS 通过事件订阅来做到联动。
AWS Lambda 官网文档 和 Google Cloud Functions 官网文档,举了几个的应用场景,其中包括物联网 IoT、独立应用 、游戏后台服务引擎、 数据报表(定时)等等。
貌似看上去离前端有些远,再举一个例子,2018年微信小程序提出的云开发,我个人理解也是对 Serverless 的一次尝试,让拥有开发小程序的工程师,不必依赖传统服务端,而利用云存储、云数据库、云函数来实现自给自足。
客户端开发,可以自行将数据、文件、信息同步到云端,而又不需要过于操心运维。
接触新技术时,工程师们总是喜欢拿来直接上手实验,这里笔者使用 serverless.com 提供的 example,做了一个用于 TODO 应用的 server 端,使用 AWS Lambda, API Gateway, DynamoBD, S3 等服务,整体代码只有 200 行,其中 5 个 Function 服务,拥有 CRUD + Query 的功能。




如此以来,就拥有服务端的 API 服务,客户端就调用这几个服务,一个简单的 GTD 应用的雏形就已经完成了!
可以说 AWS Lambda 在市面上探索和发展的最久,用户量最大,Lambda 目前已经可以支持 Python、Java、Go、Ruby、.Net、NodeJS 和自建环境,这几个服务端语言都有自己擅长的场景和成熟的库,可以更方便地去助力完成数据计算、机器学习、图像处理等等工作。
No silver bullet. - The Mythical Man-Month
任何解决方案都不是万能的,一定是有它适合的场景,解决适合的问题而存在。
死循环(函数既要做小,但是如果想提供复杂功能又不得不做的臃肿,这是一个矛盾)Serverless 架构带来的价值和挑战是并存的,国内的 Serverless 发展也在初期阶段,它改变了我们开发模式,也改变软件系统的一部分设计,未来发展的好坏,我们无法控制,但是我们可以保持关注,适当尝试。

笔者最近在整理前段时间接手的其他团队的 RN 项目代码,在梳理项目中旧代码过程中,对 React 中 State Store Static This 产生疑惑,借此翻译这篇文章解惑,也分享给各位。
发表时间 2016-08
作者 Sam Corcos
With the advent of React and Redux, a common question has emerged:
What should I hold in the Redux store, and what should I save in local state?
在开发 React 和 Redux 项目时,经常会被问到一个问题?
我应该把什么维护在 Redux Store 中?我该在 Local state 中保存什么?
But this question is actually too simplistic, because there are also two other ways you can store data for use in a component: static and this.
Let’s go over what each of these, and when you should use them.
然而问题非常简单,因为在 component 中你可以使用两种其他的方式储存数据:static 和 this。
让我们一起来详细了解下如何使用。
When React was first introduced, we were presented with local state. The important thing to know about local state is that when a state value changes, it triggers a re-render.
This state can be passed down to children as props, which allows you to separate your components between smart data-components and dumb presentational-components if you chose.
React 刚刚面世之初,我们就注意到 local state。每当 state 值发生变化,都会触发组件重新 render,因此了解 state 是非常重要的。
当前组件的 state 会被当做 props 传递到子组件中,这个 props 允许你在数据组件和描绘型组件之间做出区分。
下面一个简单的使用 local state 计数 App 例子:
1 | import React from 'react' |
Your data (the value of the counter) is stored within the App component, and can be passed down its children.
App 组件中数据被储存其中,并可以向子组件进行传递。
Assuming your counter is important to your app, and is storing data that would be useful to other components, you would not want to use local state to keep this value.
The current best practice is to use local state to handle the state of your user interface (UI) state rather than data. For example, using a controlled component to fill out a form is a perfectly valid use of local state.
Another example of UI data that you could store in local state would be the currently selected tab from a list of options.
假设计数器 Couter 对于 App 很重要,并且它正在存储其他组件的重要数据,你不会希望使用 local state 来保存数据的。
目前最佳实践是使用 local state 来处理 UI 的状态,不是使用数据。比如,使用 Controlled Components 去实现一个 form 表单时使用 local state 是非常合理的。
UI 数据的另外一个例子,可以在 local state 中存储备选 options 列表中已选中的选项。
A good way to think about when to use local state is to consider whether the value you’re storing will be used by another component. If a value is specific to only a single component (or perhaps a single child of that component), then it’s safe to keep that value in local state.
Takeaway: keep UI state and transitory data (such as form inputs) in local state.
思考何时使用 local state 一个好方法,是考虑兼顾你正在存储的值是否会被另外一个组件使用到。如果这个值非常明确地只在单一组件(或单一子组件)中出现,那么将它保存在 local state 中就是非常安全的做法。
Takeaway:可以将 UI 状态和临时数据(form 表单输入数据)保存在 local state。
Then after some time had elapsed and everyone started getting comfortable with the idea of unidirectional data flow, we got Redux.
With Redux, we get a global store. This store lives at the highest level of your app and passes data down to all children. You connect to the global store with the connect wrapper and a mapStateToProps function.
随着时间流逝,大家都在习惯这种单向数据流的思想,随着出现了 Redux。
在 Redux 中,我们有一个全局的 store,它在 App 中处于最高层级,可以将数据传递到所有子组件中。你可以使用 connect 和 mapStateToProps 方法将全局 store 和你的组件链接起来已获取数据。
At first, people put everything in the Redux store. Users, modals, forms, sockets… you name it.
Below is the same counter app, but using Redux. The important thing to note is that counter now comes from this.props.counter after being mapped from mapStateToProps in the connect function, which takes the counter value from the global store and maps it to the current component’s props.
期初,人们把所有的东西都塞进 Redux store 中。
下面是刚刚那个计数器,区别是使用了 Redux。要点是计数器在通过 connect 方法 mapStateToProps 映射之后获取 this.props.counter,这个值是从全局 store 中获取到并映射到当前组件的 props 中的。
1 | import React from 'react' |
Now when you click on the button, an action is dispatched and the global store is updated. The data is handled outside of our local component and is passed down.
It’s worth noting that when props are updated, it also triggers a re-render—just like when you update state.
当你点击按钮,与此链接的 action 就会被触发,同时全局的 store 就会更新。这样我们本地组件外层的数据就被操作并传递下去。
props 更新是没有副作用的,只有当你更新 state 时才会触发重新渲染。
The Redux store is great for keeping application state rather than UI state. A perfect example is a user’s login status. Many of your components will need access to this information, and as soon as the login status changes, all of those components (the ones that are rendered, at least) will need to be re-rendered with the updated information.
Redux is also useful for triggering events for which you need access on multiple components or across multiple routes. An example of this would be a login modal, which can be triggered by a multitude of buttons all across your app. Rather than conditionally rendering a modal in a dozen places, you can conditionally render it at the top-level of your app and use a Redux action to trigger it by changing a value in the store.
Takeaway: keep data that you intend to share across components in store.
Redux 中 store 应该维护应用的数据状态而不是 UI 的状态。用户登录数据状态就是另外一个例子。只要登录状态改变,项目中多数组件将需要访问这个登录信息,随着信息的更新,这些获取到信息更新的组件就都会重新 render。
Redux 通常也用于事件的触发,这些事件可能是横跨多个组件或者横跨多个路由。再以登录模块举例,可以在整个应用中来触发多个事件。你可以在应用的顶层,通过使用 Redux 对 store 进行修改,并使用 action 来触发条件渲染,而不是去不同地方去单独条件渲染。
Takeaway: 可以尝试在跨组件共享数据时,将数据保存进 store 。
One of the least utilized features when working with React is this. People often forget that React is just JavaScript with ES2015 syntax. Anything you can do in JavaScript, you can also do in React.
The example below is a functional counter app, similar to the two examples above.
在 React 众多特性中 this 就是其中之一。大家通常忘记一件事,就是 React 恰恰是使用 ES2015 语法的 Javascript 实现的。任何在 Javascript 可以完成的事情,同样可以放在 React 中完成。
下面就是一个函数型计数应用,与上面两个例子相似。
1 | import React from 'react' |
We’re storing the counter value in the component and using forceUpdate() to re-render when the value changes. This is because changes to anything other than state and props does not trigger a re-render.
This is actually an example of how you should not use this. If you find yourself using forceUpdate(), you’re probably doing something wrong. For values for which a change should trigger a re-render, you should use local state or props/Redux store.
我们在组件中存储 counter 的值,并且在这个值发生变化的时候使用 forceUpdate() 去重新渲染。这是由于没有 state 和 props 发生变化,是不会触发组件的重新渲染。
这也是一个实际的非常糟糕地不使用 this 的例子。如果你发现你自己正在使用 forceUpdate() 你就有可能犯了一个错误。期望做到值改变而触发重新 render,就应该使用 local state 或者 props 或者是 Redux store。
The use case for this is to store values for which a change should not trigger a re-render. For example, sockets are a perfect thing to store on this.
举个例子,this 所存储的变量发生改变,但并不希望触发重新 render。比如,sockets 就和适合存储在 this 上。
1 | import React from 'react' |
Also, many people don’t realize they’re already using this all the time in their function definitions. When you define render(), you’re really defining this.prototype.render = function(), but it’s hidden behind ES2015 class syntax.
Takeaway: use this to store things that shouldn’t trigger a re-render.
同时,很多人并没有意识到在定义 function 时已经一直在使用 this。当你定义 render() 时,实际上是在定义 this.prototype.render = function() ,但是它是 ES2015 类定义语法的隐藏式的写法。
Takeaway: 使用 this 存储变量不应该触发重新 render。
Static methods and properties are perhaps the least known aspect of ES2015 classes (calm down, yes, I know they aren’t really classes under the hood), mostly because they aren’t used all that frequently. But they actually aren’t especially complicated. If you’ve used PropTypes, you’ve already defined a static property.
The following two code blocks are identical. The first is how most people define PropTypes. The second is how you can define them with static.
Static methods 和 properties 可能是在 ES2015 类中最不为人知的一部分,主要是因为他们不太常用。然而他们并不难懂复杂。如果你已经用过 PropTypes, 那么你已经定义过 static 属性了。
下面这两段代码片段相同。第一段是大多数人如何定义 Proptypes。第二段是你可以使用 static 定义。
1 | class App extends React.Component { |
1 | class App extends React.Component { |
As you can see, static is not all that complicated. It’s just another way to assign a value to a class. The main difference between static and this is that you do not need to instantiate the class to access the value.
你可以看到,static 并不复杂。他仅仅是给类增加值的另外一种方式。在 static 和 this 之间主要的差异主要是不需要实例化来访问这个值。
1 | class App extends React.Component { |
In the example above, you can see that to get the staticProperty value, we could just call it straight from the class without instantiating it, but to get prototypeProperty, we had to instantiate it with new App().
在上面的例子中,你可以看到获取 staticProperty 静态属性的值,我们仅仅调用 class 中的静态方法即可,并不需要实例化,但是如果想要获取 prototypeProperty 属性,我们就不得不使用 new App() 实例化以后才可以访问到。
Static methods and properties are rarely used, and should be used for utility functions that all components of a particular type would need.
PropTypes are an example of a utility function where you would attach to something like a Button component, since every button you render will need those same values.
Another use case is if you’re concerned about over-fetching data. If you’re using GraphQL or Falcor, you can specify which data you want back from your server. This way you don’t end up receiving a lot more data than you actually need for your component.
静态方法和属性很少使用,应作为组件中的工具函数来使用。
PropTypes 就是工具函数的例子,当创建按钮组件等其他类似组件时,尽管渲染出来的每一个按钮仍然需要相同的值。
另一个应用例子就是,如果你考虑从远端 fetch 数据。如果你正使用 GraphQL 或者 Falcor,那么你可以从服务端区分想要的数据。这种方式你不需要在获取组件多余的数据。
1 | class App extends React.Component { |
So in the example component above, before requesting the data for a particular component, you could quickly get an array of required values for your query with App.requiredData. This allows you to make a request without over-fetching.
Takeaway: you’re probably never going to use static.
在上面实例组件中,在具体得组件获取数据之前,你可以快速地通过 App.requiredData 来获取这个数据的数组。这允许你不用 over-fetching 就可以完成请求。
Takeaway: 你可能永远不会用到 static。
There is actually another option, which I intentionally left out of the title because you should use it sparingly: you can store things in a module-scoped variable.
There are specific situations in which it makes sense, but for the most part you just shouldn’t do it.
还有另外一个选择,我故意省略了标题,因为你应该谨慎使用它:你可以储存在模块作用域变量中。
这是一种行之有效的特殊方法,但是最好不要使用。
1 | import React from 'react' |
You can see this is almost the same as using this, except that we’re storing the value outside of our component, which could cause problems if you have more than one component per file. You might want to use this for setting default values if the values are not tied to your store, otherwise using a static for default props would be better.
If you need to share data across components and want to keep data available to everything the module, it’s almost always better to use your Redux store.
Takeaway: don’t use module-scoped variables if you can avoid it.
从上面的代码你可以看到与使用 this 很相似,尤其是我们将值存储在了组件之外,如果每个文件有多个组件极有可能产生问题。如果你没有将这个值绑定在 store 上,那你可能很希望使用 this 去设定初始默认值,否则使用 static 来设置默认 props 会更好。
如果你需要跨组件之间共享数据,并且希望将这些数据维持在每一个模块都有效,那么使用 Redux store 会更好。
Takeaway: 如果可以避免的话,不要使用模块作用域的变量。
]]>清华大学计算机博士;百度云前端技术负责人;百度 Golang & Python 技术委员会成员;
对比 Google 的工程师,国内的工程师写代码的占用时间显然过多了,而不太注重提前设计;Google 工程师们在开始实现某一模块或功能时,会事先在代码库中搜索是否已经有可重用的代码,并且代码库中的代码具有完整的注释和文档。
尽可能地提前完成两个文档
原因:在未启动实现细节代码之前构思设计时发现问题的修改,对比后期真正已经开始 Coding 的时候,对比发现问题进行修改,成本要低很多。文档一般只写主要逻辑,而代码涉及更多细节。
笔者备注:但这不是绝对的,修改是正常的,不要惧怕修改,反复尝试积累经验。
两者要有区分,不要混淆,也不要混在一起写!
概念、模型、视图等等。
细:不同角度,不同层次去描述
每一个组件(模块、函数)保证单一性,Single purpose. 只做一件事!
接口 Interface 定义了系统对外的接口,往往比系统实现内部细节代码更重要,不要过于草率,因为一旦定义了接口,提供出去给调用方使用,想修改就太难了。所以设计接口有重要原则,站在使用者的角度考虑问题!
两点细节:
代码是一种表达的方式。是写给人看的,要有编程规范。
拥有编程规范的理想状态:1. 看别人代码就像看自己代码一样易懂;2. 看代码主要看逻辑,不要过多注重细节;3. 代码尽可能地不要让人去多想。
Don’t make me think!
紧内聚,低耦合。单一功能。反例,定义一个 utils.py 内部包含诸多方法,不易懂。
模块一般可以分为两类:
模块的重要性:1. 降低维护成本; 2. 更好地复用
两者是不同的模型,各自有各自适用的范围。
推荐方法:和类的成员无关的函数,尽量独立出去单独一个函数,不建议作为类的成员函数。
面向对象思想的讨论:多态和继承,需要谨慎适用,作为 Python 的工程师,不太推崇 Java 中继承和多态,因为系统是逐渐长起来的,并不是从一开始就是一个成熟的样子,所以很难凭空去设计一个继承的关系。
文件头(注释)
函数(重要性仅次于模块)
函数要尽可能的规模小,足够短(BUG 往往出现在非常长的一个函数里)
代码块的分段也很重要,分段背后是划分和逻辑表达。
代码是一种表达能力的体现,也算是文科的范畴!注释不是补出来的!
命名的重要性:要准确、易懂、可读性强,尽量做到 望名生义。
互联网时代的系统是运营出来的。
可检测性也是尤其重要的。(埋点、监控等等)
没有数据的收集,等于系统没有上线。
监控不单单只有传统意义上的内存、CPU、网络、崩溃率的监控,还应有线上真实数据监控,需要有足够多的状态记录。
日志是很有限的一种监控手段,并且采集日志也是一种资源耗费。推荐的手段:可以使用埋点,对外提供接口,有单独的系统调用接口进行有针对性的采集。
好的程序员,与工作年限无关,与学历无关
学习:主观意愿地学习,途径也有很多,例如书籍、开源代码、社区。忌讳夜郎自大、井底之蛙。注重培养自己学习吸收的能力,多读多看但是数量不是最终目的。
Stay Hungry, Stay Foolish. – Steve Jobs
思考:学习需要经过思考,形成自己的思维。
实践:《卓有成效的时间管理者 - 德鲁克》推荐阅读
知识过时会非常快!
方法:分析问题、解决问题的能力尤其重要(定义问题、识别问题、定义关键问题)
精神:决定型,要做就要坚持做
前进的道路上不能永远都是鲜花和掌声。
基础乃治学之根本。
数据结构、软件工程、逻辑思维能力、研究能力,需要5-8年时间磨炼。
废话不多说,直奔主题,本文你将了解简单的 vscode 的插件开发入门,感兴趣的同学可以继续阅读。
以下文中提到的“插件”均为 Vistual Studio Code 的插件,Vistual Studio Code 也简称 VSCode
npm install -g yonpm install -g generator-code做好上面的环境准备,正式进入开发,下面我会以自己刚做好的一个查询北京地区空气质量为例子,继续介绍(安心してください,代码很少,很好入门)。
下面我们补充一下我们的例子的功能,首先明确我们要获取城市的 AQI(空气质量)信息,网络上可以搜索到直接调用 API 或者 SDK,笔者目前使用的京东云的一个产品京东万象中的一个天气免费 API,链接见此 注册名实名认证后可以获得一个调用应用的 KEY 既可以获取天气数据了。更多其他方面的 API 读者可以自行探索。
笔者使用的 API 是 https://wx.jdcloud.com/market/datas/26/10610 参数 city 支持 中英文名称、ID 和 IP 限制 5000次/天
已有插件所在目录如下:
| os | path |
|---|---|
| windows | %USERPROFILE%\.vscode\extensions |
| macOS | ~/.vscode/extensions |
| Linux | ~/.vscode/extensions |
在这个目录下我们可以看到已经安装的所有插件的代码,即使插件是使用 typescript 编写 out 文件夹也可以看到编译后的 javascript 代码,感兴趣的同学可以直接看一看自己平时最常用的插件是如何实现的。
执行下面代码:
1 | yo code |
执行后会提示几个问题,第一个问题我们选择 New Extension (JavaScript),其他正常填写即可。(可以使用 yo code 来创建插件、主题、代码片段、语言支持、键盘映射、插件包,本文我们只讨论插件,其他暂且放在一边。)

填写完成后,会自动创建文件夹并帮助初始化完成文件,我们先看下目录结构。
1 | . |
熟悉的项目文件结构,直接查看 vsc-extension-quickstart.md 这个文档,文中提到 package.json 声明当前插件和命令的配置文件,用来注册命令等配置;extension.js 是主入口文件,插件的具体实现代码可以放在这里;
简单了解一下两个重要文件:
1 | { |
1 | // The module 'vscode' contains the VS Code extensibility API |
下面我们直接用 VSCode 打开这个项目的根目录,打开 extension.js 按下键盘 F5 或启动调试,接下来就可以看到,已经启动一个新的窗口,我们按下 command + shift + p 或 command + p 再输入 > 即可调用插件,在 package.json 中已经定义过 commands 我们直接输入 Hello World 直接启动插件,此时可以看到 Hello World 的通知框,就可以调试代码了。
OK,下面我们来进一步实现我们的功能,调取天气 API 数据,展示。
1 | const vscode = require('vscode'); |
Tadaaa,简单的功能就完成了!:P

代码中用到了两个 vscode windows 的 api,showInputBox 和 showInformationMessage 更多 API 详见链接

可以看到 API 包含了很多信息,所以可以做的事还可以很多 :)
1 | npm install -g vsce |
发布前我们可以把开发好的插件打包成 .vsix 文件,提供给身边的小伙伴试用一下。
1 | vsce package |
city-aqi-0.0.1.vsix 就打包完成了,在插件面板上选择 从 VSIX 安装 或命令行 具体详见文档 安装完成后重新加载 VSCode 就可以使用了。
过程中可能会提示你先修改 README.md 文件才能打包,简单描述功能即可。
详细完善过 README.md 和 CHANGELOG.md 并且删除了一些冗余的文件,你就可以将插件发布到微软的插件市场 Extension Marketplace 供他人下载使用了。
在发布插件之前,首先,获取 token,登录 Visual Studio Team Services 注册并登录账户,在 Security 中找到 Personal access tokens 新建一个 token。回到命令行:
1 | vsce create-publisher xxxx |

完成后不需要重新 login ,因为 vsce 已经帮你记录当前的 publisher。
1 | $ vsce publish |
ALL Done 🎉

笔者几年前一直是 Sublime Text 的使用者,修改 jsp、vm、sql等等文本文件,后来出现了 Atom 社区也相当活跃,用户同样很多,笔者也曾试用过一段时间。
合适的才是最好的 近两年 VSCode 一直是我的主力开发编辑器,它也有缺点,但是它在不断精进和完善,大厂维护更新频率也很快。
很多大牛们喜爱的 Vim 和 Emcas 笔者没有用过,甚至连尝试都没有,可能是被过于灵活的配置吓到了…
PS,最近知道一个词宜家理论,套在上面几个工具上面,应该都适用,没有最好的只有最合适自己的。现在机器上配置的 vscode 的开发环境,笔者已经不得不使用同步插件来进行备份,以免更换机器时重新配置。
VSCode 有很多方便的插件,可以帮助我们提高开发效率,以后找机会写一个推荐列表分享出来。
]]>鉴于 Webpack 作为关键在 Google 可以搜索到很多相关的文章,网上文章也是针对各自项目和某些情况的具体方案或者介绍说明,本文也不例外,只介绍分享过程中积累的 4.0 版本的个人实战经验。
导读:本文你将 Get 到使用 Webpack 4 从零开始分别搭建 React 16 和 Vue 2 项目,同时还有基于 Webpack 4 的一些开发和生产环境配置经验,感兴趣同学可以继续阅读。
PS. 前半部分较为基础,有一定经验的同学可以直接跳过阅读后半部分实战内容。
以前也翻译过两篇关于 Webpack 的文章,感兴趣的同学可以点击下面链接查看:!!! 强烈推荐 !!!
正文开始
相信提到 Webpack 无论是作为前端工程师,还是 Web 开发者都不会太陌生,它从诞生伊始就收到社区的追捧和大量的生产实践,大量的项目代码构建工具开始选择他作为主力构建工具,究竟它是什么样工具,它的官网是这样描述的:
At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.
其核心就是现代 Javascript 应用的静态模块构建器。当在应用中运行 Webpack 的时候,它就会在内部构建依赖图来映射项目中的每一个所需要的模块,并生成一个或多个 bundle 文件。
也就是说 Webpack 可以分析项目中模块之间的依赖,并将最终的结果打包成 bundle 文件,开发者只要在开发过程中做到正确的引用和正常的代码开发即可,打包的事情统统交给 Webpack 即可。
在 Webpack 逐渐进化的过程中,或多或少存在一些缺点被社区人们所诟病,比如配置繁琐、构建时间较长且占用 CPU 高、文档不完整等等问题,并且衍生出一些替代品,但 Webpack 团队和开源社区的持续不断的贡献,它在不断完善和修正,如今已经进入 v4.x.x 的时代。帮助开发者减少工作量是 4.0 的一个任务,从这个版本开始,Webpack 几乎 可以做到”零“配置或少配置地来构建项目生成 Bundle 文件,下面我们就先来看一下 Webpack 4 的“零”。
首先,我们来创建一个 demo 文件夹,做一些简单的初始化信息,并本地安装 webpack,此时项目中没有 webpack 配置文件。
1 | mkdir webpack-4-demo |
修改 package.json 文件:
1 | "scripts": { |
运行
1 | yarn build |
过程中会提示是否安装
webpack-cli直接敲yes即可。
此时控制台执行结果会有如下报错:
1 | ERROR in Entry module not found: Error: Can't resolve './src' in '/Users/xxxx/webpack-4-demo' |
注意:我们目前没有写任何配置文件,但 Webpack 仍会提示没有找到
./src目录下 module。自 v4.0 开始已经 Webpack 可以自动在不配置 entry 的情况下自动检索项目文件夹中 src 目录下的 js 文件作为入口文件进行编译了。
接下来我们按照错误提示,在目录下创建 src 文件夹,并且新建一个文件 index.js 并且输入内容 console.log('hello webpack 4'),再次运行 yarn build。
这时可以看到编译成功,项目目录下多出一个 dist 文件夹,我们事先也并没有配置 output 输出指向,Webpack 默认将 bundle 好的内容,放在了 dist 文件夹内。
在执行成功的过程中,有一处警告提示:
1 | WARNING in configuration |
回过头查看刚刚编译好的 main.js 文件,该文件已经直接被压缩好的,且可在生产模式下运行。
警告中提到的mode 就是 Webpack 4 新增的一个配置,具体内容可以见下面链接,以及笔者之前的翻译文章。
当然,也可以通过 --mode 选项来手动选择 bundle 的模式,比如 webpack --mode development。
自此,如果你的项目 src 目录下的内容需要 Webpack 帮你编译,输出在 dist 目录,Webpack 几乎零配置就可以直接“胜任”了。
上一节简单介绍了 V4.x “零”配置的基础应用。当然,实际工作中我们的项目都会比较复杂,上面的内容远不能满足我们的需求,下面我们就以一个 React 16 & Webpack 4 DEMO 项目为例,还原从零开始搭建基于 Webpack 打包编译项目的整个过程。
Facebook 官方推出的
create-react-app工具已经非常好用,但仍然需要做一些修改才可以满足实际项目上线的需求,同时我们仍希望有更多所谓个性化设置来支持项目,且截至到今天cra使用 webpack 3.8 与我们本文介绍 webpack 4 有出入,所以下面内容不再提及,对cra感兴趣的同学也可以自行搜索查看了解。
首先,重复上面介绍的步骤:(创建目录、安装 react、安装 webpack、安装 babel)
1 | mkdir react-webpack-demo |
注意:这里使用 babel 转义,此处既可以在项目根目录下创建
.babelrc文件,也可以稍后在 webpack.config.js 中配置,这里我们选择在后者统一配置。
现在我们新建一个配置文件,webpack.config.js 代码:
1 | module.exports = { |
在 src 目录下创建 App.jsx:
1 | import React from "react"; |
在 src 下新建 index.jsx 内容如下:
1 | import App from './App'; |
执行 yarn build 等待打包结果,此时目录 dist 下已经打包好 bundle。
我们接着创建 html 文件,在 src 下创建 index.html :
1 |
|
修改 build 的配置,拷贝 html
1 | yarn add html-webpack-plugin |
修改上面的 config:
1 | const HtmlWebPackPlugin = require("html-webpack-plugin"); |
执行 yarn build 就可以看到已经打包好的 index.html 和 bundle js 。
通过 webpack-dev-server 搭建本地 server 服务,目前是通用的解决办法
安装依赖:
1 | yarn add webpack-dev-server --dev |
方便起见,我们在 package.json 的 scripts 中增加内容:
1 | "scripts": { |
运行 yarn start 此时会运行一个 dev-server 服务,这样我们就能方便地在本地进行开发了。
在开发中我们还有一些其他的需求,比如 sourceMap 、修改 dev-server 配置等,所以我们可以新建一个配置 webpack.config.dev.js :
1 | const path = require('path'); |
修改 package.json 中 scripts 执行所需要执行的配置文件:
1 | "scripts": { |
重新执行 yarn start 可以看到修改配置后的 dev-server
配置热更新就可以让我们在开发过程中,将修改后代码整页面无刷新且保持原有 state 的情况下直接反应到页面,下面我们继续修改 config.dev.js 并在 App.jsx 增加内容:
1 | // webpack.config.dev.js config 部分 |
执行 yarn start 重新启动 server,每次修改代码后保存就可以看到控制台里重新编译的信息,浏览器中变化实际修改的内容了。
Vue 官方也推出 vue-cli 来帮助使用者快速创建项目,同时也是使用 webpack 4 来进行构建项目,通过阅读使用文档和源码,需要满足现有复杂项目的需求,我们可能也是不仅需要使用 –options 的方式同时还需要做一些定制的开发才可以,因此下面不再提及。
与 React 项目初始化一致(创建目录、安装 vue、安装 webpack、安装 babel)
1 | mkdir vue-webpack-demo |
在 vue-webpack-demo 文件夹下,创建 index.html :
1 |
|
新建 src 目录,并新建 app.js 和 app.vue 文件:
1 | // app.js |
1 | // app.vue |
下面来增加配置文件,webpack.config.js:
1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); |
完成后执行 webpack 就可以看到 dist 下已经构建完成的项目。
注意:vue-loader 已经更新到 v15.x ,与之前 14.x 配置方式有差异。
用于开发 dev-server 配置与 React 基本一致,这里不重复。
下面的内容是在笔者积累的项目优化实战经验汇总。
为了减少一次请求文件体积过大,同时修改业务代码时不必要重复重新下载公共依赖代码,我们通常将公共依赖模块如 react、react-dom、vue、axio 等文件抽取出来独立打包。
注意:
CommonsChunkPlugin在 4.0 中已经被optimization选项取代,不需要安装该插件就可以实现相同效果,见下面配置。
1 | mode: 'production', |
通过上面的配置,可以将公共依赖和业务代码隔离开来。但是,也会存在一些隐患:
vendor.js 的体积会越来越臃肿所以具体项目需要通过具体的需求来抽离出不同的 chunks 来分别引用,按需引用。
配置 mode: 'production' Webpack 会使用默认插件 [UglifyJs](https://github.com/webpack-contrib/uglifyjs-webpack-plugin) 来进行压缩代码。
官网提到在 Webpack v4 以前使用内置的 webpack.optimize.UglifyJsPlugin 插件,在 Webpack 4 以后,开始使用 ^1.0.0 独立的版本。
1 | + const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); |
注意:4.0 版本压缩的代码已经放在
optimization下minimizer节点下。
备注:关于parallel选项,新版的 uglifyjs 已经支持多核 CPU 并行执行,所以已经不需要webpack-parallel-uglify-plugin插件。
配合 dev-server 对代理本地启动的 server 某一域名进行代理,解决服务端接口暂时满足要求、本地请求跨域等问题。
1 | devServer: { |
虽然多数情况下,我们都在开发并且维护单页面的应用,但是当遇到需要多页面的时候,我们也希望在一个项目内进行构建,目前解决办法比较粗暴,当前有 n 个入口 html 就创建 n 个 HtmlWebpackPlugin 插件实例。
1 | const fs = require('fs'); |
这里要注意 rules 中 loader 数组的顺序,由于 webpack 执行 rules 是从最后一个开始倒叙执行,所以我们配置的顺序也是:
预编译 sass => 处理 postcss => 处理 css => 压缩并独立 css 文件
升级到 4.0,已经不再使用 extract-text-webpack-plugin 插件来进行文件抽取,改用 MiniCssExtractPlugin 插件,配合 OptimizeCSSAssetsPlugin 插件来压缩 css 文件。
1 | const autoprefixer = require('autoprefixer'); |
可以将 html 中 css 和 js 的相对引用路径自动替换成配置的前缀路径,用来支持静态资源上线到具体指定的 CDN 路径来增加 app 内静态资源的下载速度。
1 | output: { |
有些时候我们会遇到在生产环境中代码出现问题的情况,而本地开发却不重现,这个时候 source-map 就成了辅助解决问题的一个有利的工具,具体见下面配置:
1 | mode: 'production', |
下面是关键 loaders 但不会列出所有配置,内容略有差异,可以根据配置 limit 大小来控制图片转 base64 或压缩等。
file-loader 文件无处理,直接拷贝url-loader 可以增加 base64 处理svg-url-loader 处理 svg 文件,也同样支持 base64image-webpack-loader 图片文件降质压缩HashedModuleIdsPlugin 使用更稳定的 moudle id 生成方式webpack.optimize.ModuleConcatenationPlugin 插件已经不需要单独配置,Webpack 4 已经默认在生产模式下打包时内置开启优化
并不是每一个人都想成为 Webpack 配置工程师!
上面引用一句我们“造轮子”时使用的一句 slogan。
虽然 Webpack 4 还没有达到开箱即用的程度(当然,开箱即用也就意味着可配置的内容有所减少,这一定是一把双刃剑),况且开箱即用也并不是它的被创造出来的初衷,简单的配置无法满足项目中的实际需求,各种各样的配置和插件配合着形成解决各种问题的不同方案,只在很多次尝试后才能达到针对某一个项目项目最优的配置。
然而,它只是一个工具,也许再过一段会有新的工具来取代 Webpack,But 既然你现在用到它,还是有必要花时间了解一下如何更好地让它为你、你的团队和你的项目来服务。
关于 Webpack 4 的配置经验笔者也在摸着石头过这条小河,网上也有诸多优化和解决方案,我们造的轮子也需要更多的项目和时间来帮助其成长,文中并没有面面俱到地将所有配置详细说明,或者并不是所有配置都是最优选择,也欢迎私信留言讨论。
]]>原文 https://developers.google.com/web/fundamentals/performance/webpack/
PS. 在 20180211 笔者翻译过一次,当时也没有完全理解和使用文中提到的优化项,近期工作中因为用到 Webpack 4.x 对生产环境进行打包,加深了一些理解,本译文对原有译文补充的 Webpack 4 内容,同时对原译文进行了校对和一些细节措辞的修改。
NODE_ENV=production 明确生产环境信息作者 Addy Osmani
现代 Web 应用经常用到 bunding tool 来创建生产环境的打包文件(例如脚本、样式等等),打包文件是需要优化并压缩最小化,同时能够被用户更快地加载。在这篇文章中,我们将会利用 webpack 来贯穿说明如何进行高效地优化网站资源。这能帮助用户更快地加载你的应用同时获得更好的体验。

webpack 是当今最流行的打包工具之一,深入地利用它的特点去优化代码,将脚本拆分成不同的部分,同时剔除无用代码将能够保证你的应用维持最小的带宽和进程消耗。

Note: 我们创建了一个练习的应用来演示下面这些优化的描述。尽力抽更多的时间来练习这些 tips
webpack-training-project
让我们从现代 web 应用中最耗费资源之一的 Javascript 开始。
作者 Ivan Akulov
当你正在优化一个应用时,首要事情就是尽可能地将它体积的减小。下面我们就来看看通过 webpack 如何做到减小前端体积。
Webpack 4 介绍了一种新的模式,你可以将其设置成 development 和 production 用于告诉 Webpack 你正在为不同的环境打包:
1 | // webpack.config.js |
当你正在为你的应用用于生产环境编译打包时要确定开启了 production 模式。这样就帮助 webpack 开启类似压缩最小化代码、去除依赖库中开发环境代码等其他的优化项。
Note: 笔者也翻译了另外一篇介绍新增 mode 的文章,感兴趣可以点击链接
Note: 大部分只针对 webpack 3 如果你正在使用 webpack 4 生产模式打包,bundle 级别的最小化功能已经开启 - 你只需要配置对应 loader 选项即可
最小化就是通过去除多余空格、缩短变量名等方式压缩代码。例如:
1 | // Original code |
↓
1 | // Minified code |
Webpack 支持两种方式最小化代码:bundle-level 最小化 和 loader-specific options。他们可以同时使用。
Bundle-level 最小化功能可以在编译完成后压缩整个 bundle。下面来看下它是如何工作的:
1.原始代码如下:
1 | // comments.js |
2.Webpack 编译后的内容大概是下面这个样子:
1 | // bundle.js (part of) |
3.最小化之后的代码大概是下面这个样子:
1 | // minified bundle.js (part of) |
在 Webpack 4 中,bundle 级别的的最小化是自动开启的 - 同时在生产模式下、没有启用 bundle-level 都会开启。它是利用 UglifyJS 引擎来进行最小化的。(如果你需要禁用最小化,仅仅设置开发模式或者设置 optimization.minimize 为 false。)
在 Webpack 3 中,你需要直接使用 UglifyJS 插件。该插件是 webpack 提供的;开启并设置插件选项即可:
1 | // webpack.config.js |
Note: 在 webpack 3 中,UglifyJS 插件不能编译 ES2015+(ES6) 的代码,这就意味着你在代码中使用 classes, arrow function 或者其他新特性时,不能将他们编译成 ES5的代码,插件会抛错。
如果你需要编译这些新语法,就要用到 uglifyjs-webpack-plugin package,他也是在 webpack 中捆绑一起的,但是版本更新,并且可以编译 ES2015+ 的代码。
最小化代码的第二步就是利用特定的 loader 配置。配置这些 loader,你可以压缩那些不能被最小化的部分。举个例子,当你使用 css-loader 引入一个 css 文件时,文件会被编译成一个字符串:
1 | /* comments.css */ |
↓
1 | // minified bundle.js (part of) |
这部分内容由于是字符串并没有被最小化。于是我们需要配置对应的 loader 选项来达到最小化的目的:
1 | // webpack.config.js |
NODE_ENV=production 明确生产环境信息Note: 仅在 webpack 3 中生效,如果使用生产模式 webpack 4 打包,NODE_ENV=production 优化项已经开启,就可以直接跳过此小结
减小前端体积的另外一个方法就是在代码中将 NODE_ENV 环境变量设置为 production 。
Libraries 会读取 NODE_ENV 变量判断他们应该在那种模式下工作 - 开发模式 or 生成模式。很多库会基于这个变量有不同的表现。举个例子,当NODE_ENV没有设置成production,Vue.js 会做额外的检查并且输出一些警告:
1 | // vue/dist/vue.runtime.esm.js |
React 也是类似 - 开发模式下 build 带有一些警告:
1 | // react/index.js |
这些检查和警告通常在生产环境下是不必要的,但是他们仍然保留在代码中并且会增加库的体积。
在 Webpack 4 中增加 optimization.nodeEnv: 'production' 选项即可剔除掉它们:
1 | // webpack.config.js (for webpack 4) |
在 Webpack 3 中则使用 DefinePlugin :
1 | // webpack.config.js (for webpack 3) |
optimization.nodeEnv: 'production' 选项和 DefinePlugin 插件采用相同的方式来解决这个问题 - 这个方式就是他们将 process.env.NODE_ENV 替换成特定的值,下面的配置可以说明:
1.Webpack 会将所有 process.env.NODE_ENV 替换成 "production":
1 | // vue/dist/vue.runtime.esm.js |
↓
1 | // vue/dist/vue.runtime.esm.js |
2.与此同时最小化工具会移除掉所有 if 的条件分支 - 由于 "production" !== 'production' 永远会返回 false,这样分支内的代码就永远不会执行:
1 | // vue/dist/vue.runtime.esm.js |
↓
1 | // vue/dist/vue.runtime.esm.js (without minification) |
DefinePlugin, EnvironmentPlugin Define 插件和 Environment 插件文档下面这个方式利用 ES modules 减小前端体积。
当你使用 ES module,webpack 有能力去做 tree-shaking。Tree-shaking 贯穿了整个依赖树,检查哪些依赖被使用,同时移除掉无用依赖。因此,如果你使用 ES module 方式的时候,webpack 帮你可以排除掉无用代码:
1.一个有多个 export 的文件,但是 app 只需要其中一个:
1 | // comments.js |
2.Webpack 分析 commentRestEndPoint 没有被用到,就不会在一个 bundle 中生成单独的 export:
1 | // bundle.js (part that corresponds to comments.js) |
3.最小化工具就会移除掉无用变量:
1 | // bundle.js (part that corresponds to comments.js) |
如果他们都是有 ES module 编写,就是与一些库并存时也是生效的。
Note: 在 webpack 中,tree-shaking 没有 minifier 是无法生效的。 webpack 仅仅移除了没有被用到的 export 变量;
UglifyJSPlugin才会移除无用代码。所以如果你编译打包时没有使用 minifier,打包后体积并不会更小。你也可以不一定使用这个插件。其他最小化的插件也支持移除 dead code(例如:Babel Minify plugin or Google Closure Compiler plugin)
Warning: 不要将 ES module 编译到 CommonJS 中。 如果你使用 Babel
babel-preset-envorbabel-preset-es2015,检查一下当前的配置。默认情况下, ESimportandexportto CommonJSrequireandmodule.exports。通过设置 option 来禁止掉Pass the{ modules: false }option。
图片基本会占局页面一半以上体积。虽然它们不像 JavaScript 那么重要(比如它们不会阻止页面渲染),但图片仍然会占用掉一大部分带宽。可以利用 url-loader,svg-url-loader 和 image-webpack-loader 来进行优化。
url-loader 允许将小的静态文件打包进 app。没有配置的话,他需要通过传递文件,将它放在编译后的打包 bundle 内并返回一个这个文件的 url。然而,如果我们注明 limit 选项,它将会编码成更小的文件 base64 url 并返回这个 url。这样将图片放在 Javascript 代码中,可以节省 HTTP 的请求:
1 | // webpack.config.js |
1 | // index.js |
Note: 内联图片减少了独立请求的数量,这是很好的方式(even with HTTP/2),但是会增加 bundle下载和转换的时间和内存的消耗。一定要确保不要嵌入超大图片或者较多的图片 - 否则增加的 bundle 的时间将会掩盖做成内联图片的收益。
svg-url-loader与url-loader类似 - 都是将使用 URL encoding encode 文件。这对对于 SVG 图片很奏效 - 因为 SVG 文件是文本,encoding 在体积上更有效率:
1 | // webpack.config.js |
Note: svg-url-loader 拥有改善 IE 浏览器支持的 options,但是在其他浏览器中更糟糕。如果你需要兼容 IE 浏览器,设置 iesafe: true 选项
image-webpack-loader压缩图片使之变小。它支持 JPG,PNG,GIF 和 SVG,因为我们将会使用它所有类型。
这个 loader 不会将图片嵌入在应用内,因此它必须与url-loader和svg-url-loader配合使用。避免复制粘贴到相同的 rules 中(一个用于 JPG/PNG/GIF 图片,另一个用于 SVG 图片),我们来使用enforce: pre作为单独的一个 rule 涵盖这个 loader:
1 | // webpack.config.js |
默认 loader 设置就已经可以满足需求了 - 但如果你想要深入配置,请查看 the plugin options。为了选择哪些 options 需要明确,可以查看 Addy Osmani 的 guide on image optimization
平均一半以上的 Javascript 体积大小来源于依赖包,并且这些可能都不是必要的。
举一个例子来说,Lodash(v4.17.4)增加了最小化代码的 72KB 大小到 bundle 中。但是如果你仅仅用到它的20个方法,大约 65 KB 代码没有用处。
另外一个例子就是 Moment.js。 V2.19.1版本最小化后有 223KB,体积巨大 - 截至2017年10月一个页面内的 Javascript 平均体积是 452KB。但是,本地文件的体积占 170KB。如果你没有用到 多语言版 Moment.js,这些文件都会没有目的地使 bundle 更臃肿。
所有这些依赖都可以被轻易优化。我们在 Github repo 收集了优化的建议,check it out!
Note: 如果你在使用生产模式下的 webpack 4,modules concatention 已经开启,可以直接跳过本小节。
当你构建 bundle 时,webpack 将每一个 module 封装进 function 中:
1 | // index.js |
↓
1 | // bundle.js (part of) |
在以前,这么做是使 CommonJS/AMD modules 互相分离所必须的。但是,这会增加体积并且性能表现堪忧。
Webpack 2 介绍了 ES modules 的支持,不像 CommonJS 和 AMD modules 一样,而是能够不用将每一个 module 用 function 封装起来。同时 Webpack 3 利用ModuleConcatenationPlugin完成这样一个 bundle,下面是例子:
1 | // index.js |
↓
1 | // Unlike the previous snippet, this bundle has only one module |
看到区别了吗?在这个 bundle 中, module 0 需要 module 1 的 render 方法。使用 ModuleConcatenationPlugin,require被直接简单的替换成 require 函数,同时 module 1 被删除删除掉了。这个 bundle 拥有更少的 modules,就有更少的 modules 损耗!
在 Webpack 4 中开启这个功能,启用 optimization.concatenateModules 选项即可:
1 | // webpack.config.js (for webpack 4) |
在 webpack 3 中,使用 ModuleConcatenationPlugin 插件:
1 | // webpack.config.js (for webpack 3) |
Note:想要知道为什么这个功能不是默认启用?Concatenating modules 很棒, 但是他会增加编译的时间同时破坏 module 的热更新。这就是为什么只在生产环境中启用的原因了。
你可能拥有一个体积庞大的工程,其中一部分代码可以使用 webpack 编译,而有一些代码又不能。比如一个视频网站,播放器的 widget 可能通过 webpack 编译,但是其周围页面区域可能不是:

如果两部分代码有相同的依赖,你可以共享这些依赖以便减少重复下载耗时。the webpack’s externals option就干了这件事 - 它用变量或者外部引用来替代 modules。
如果你的非 webpack 代码依靠这些依赖,它们是挂载 window 上的变量,可以将依赖名称 alias 成变量名:
1 | // webpack.config.js |
利用这个配置,webpack 将不会打包 react 和 react-dom 包。取而代之,他们会被替换成下面这个样子:
1 | // bundle.js (part of) |
如果你的非 webpack 代码没有将依赖暴露挂载到 window 上,这就更复杂了。但是如果非 webpack 代码使用 AMD 包的形式消费了这些依赖,你仍然可以避免重复的代码加载两次。
具体如何做呢?将 webpack 代码编译成一个 AMD module 同时别名成一个库的 URLs:
1 | // webpack.config.js |
Webpack 将会把 bundle 包装进 define()同时让它依赖于这些URLs:
1 | // bundle.js (beginning) |
如果非 webpack 代码使用相同的 URLs 加载依赖,这些文件将会加载一次 - 多余的请求会使用缓存。
Note:webpack 只是替换那些
externals对象中的准确匹配的 keys 的引用。这意味着如果你的代码这样写import React from 'react/umd/react.production.min.js',这个库是不会被 bundle 排除掉的。这是因为 - webpack 并不知道import 'react'和import 'react/umd/react.production.min.js'是同一个库,这样比较谨慎。
externals externals 文档NODE_ENV with production 通过将 NODE_ENV 替换成 production 来移除开发期间代码externals if this makes sense for you 如果有效果的话可以使用 externals作者 Ivan Akulov
在做完优化应用体积之后的下一步提升应用加载时间的就是缓存。在客户端中使用缓存作为应用的一部分,这样会在每一次请求中减少重新下载的次数。
做缓存通用的解决办法:
1.告诉浏览器缓存一个文件很长时间(比如一年)
1 | # Server header |
Note:如果你不熟悉 Cache-Control 做了什么,你可以看一下 Jake Archibald 的精彩博文 on caching best practices
2.当文件改变需要强制重新下载时去重命名这些文件
1 | <!-- Before the change --> |
这些方法可以告诉浏览器下载这些 JS 文件,将其缓存起来。浏览器将只会在文件名发生改变时才会请求网络(或者缓存失效的情况也会请求)。
使用 webpack,也可以做同样的事,但可以使用版本号来解决,需要明确这个文件的 hash 值。使用 [chunkhash] 可以将 hash 值包含进文件名中:
1 | // webpack.config.js |
Note: webpack 可能会生成不同的 hash 即使 bundle 相同 - 比如你重名了了一个文件或者重新在不同的操作系统下编译了一个 bundle。 This is a bug.
如果你需要将文件名发送给客户端,也可以使用HtmlWebpackPlugin或者WebpackManifestPlugin。
HtmlWebpackPlugin 使用起来很简单,但灵活性有一些欠缺。编译时,插件会生成一个 HTML 文件,这其中包括所有的编译后的资源文件。如果你的业务逻辑不复杂,这就非常适合你:
1 | <!-- index.html --> |
WebpackManifestPlugin 更灵活一些,它可以帮助你解决业务负责的部分。编译时它会生成一个 JSON 文件,这文件保存这没有 hash 值文件与有 hash 文件之间的映射。服务端利用这个 JSON 可以识别出那个文件有效:
1 | // manifest.json |
App 依赖通常情况下趋向于比实际 app 内代码中更少的变化。如果你将他们移到独立的文件中,浏览器将可以把他们独立缓存起来 - 同时不会每次 app 代码改变时重新下载。
Key Term: 在 webpack 的技术中,利用 app 代码拆分文件被称为
chunks。我们后面会用到这个名词。
为了将依赖包提取到单独的 chunk 中,下面分为三步:
1.使用 [name].[chunkname].js 替换output的文件名:
1 | // webpack.config.js |
当 webpack 构建应用时,它会用一个带有 chunk 的名称来替换 [name]。如果没有添加 [name] 部分,我们不得不通过 chunks 之间的 hash 区别来比较他们的区别 - 那就太困难了!
2.将 entry 转成一个对象:
1 | // webpack.config.js |
在这段代码中,”main” 对象是一个 chunk 的名字。这个名字将会被步骤 1 里面的 [name]代替。
目前为止,如果你构建一个 app,chunk 就会包括整个 app 的代码 - 就像我们没有做这些步骤一样。但是很快就会产生变化。
3.在 Webpack 4 中,在配置中增加 optimization.splitChunks.chunks: 'all' 即可:
1 | // webpack.config.js (for webpack 4) |
这个选项会开启智能代码拆分。使用这个功能,webpack 将最小化和 Gzip 前大于 30KB 的代码提取出额外的 vendor 代码。它同时也会提取出 common 代码 - 这些代码在打包多个 bundles 会起到作用。(例如:通过路由拆分应用)。
在 Webpack 3 中,使用 CommonsChunkPlugin 插件:
1 | // webpack.config.js (for webpack 3) |
插件将包括全部 node_modules 路径下的 modules 同时将他们移到一个单独的文件中,这个文件被称为 vendor.[chunkhash].js。
完成了上面的步骤,每一次 build 都会生成两个文件。浏览器会将他们单独缓存 - 以便代码发生改变时重新下载。
1 | $ webpack |
不幸的是,仅仅抽取 vendor 是不够的。如果你试图在应用代码中修改一些东西:
1 | // index.js |
你会注意到 vendor 的也会改变:
1 | Asset Size Chunks Chunk Names |
↓
1 | Asset Size Chunks Chunk Names |
这因为 webpack 打包时,一部分 modules 的代码,拥有 a runtime - 管理模块执行一部分代码。当你将代码拆分成多个文件时,这小部分代码在 chunk ids 和 匹配的文件之间开始了一个映射:
1 | // vendor.e6ea4504d61a1cc1c60b.js |
Webpack 将最新生成的 chunk 包含在这个 runtime 内,这个 chunk 就是我们代码中的 vendor。与此同时每一次任何 chunk 的修改,即使这一小部分代码也改变,也会导致整个 vendor chunk 改变。
为了解决这个问题,我们将 runtime 转义到一个独立的文件中,在 Webpack 4 中,开启 optimization.runtimeChunk 选项:
1 | // webpack.config.js (for webpack 4) |
在 Webpack 3中,通过 CommonsChunkPlugin 创建一个额外的空的 chunk:
1 | // webpack.config.js (for webpack 3) |
完成这一部分改变,每一次 build 都将生成三个文件:
1 | $ webpack |
将他们反过来顺序添加到 index.html 中,你就搞定了:
1 | <!-- index.html --> |
为了做的更好,我们可以尽力把 webpack runtime 内联在 HTML 请求里。下面举例:
1 | <!-- index.html --> |
这样做:
1 | <!-- index.html --> |
这个 runtime 很小,内联它可以帮助你节省 HTTP 请求(尤其对 HTTP/1 重要;但是在 HTTP/2 就没有那么重要了,但是仍能够提高效率)。
下面就来看看如何做。
如果使用 HtmlWebpackPlugin 来生成 HTML 文件,InlineChunkWebpackPlugin 就足够了。
1.增加 WebpackManifestPlugin 插件已知运行时 chunk:
1 | // webpack.config.js (for webpack 4) |
插件就会生成一个下面这样的文件:
1 | // manifest.json |
2.将这些内容嵌入到 runtime chunk 中。例如:使用 Node.js 和 Express:
1 | // server.js |
1.将 runtime 名称改成静态的明确的文件名:
1 | // webpack.config.js (for webpack 3) |
2.嵌入到 runtime.js 内容。比如:Node.js 和 Express
1 | // server.js |
通常情况下,一个页面有或多或少的重要部分:
在这些案例中,通过仅下载最重要的部分,懒加载剩余区域能够提升最初的加载性能。使用 the import() function 和 code-splitting 解决这个问题:
1 | // videoPlayer.js |
import()明确表示你期望动态地加载独立的 module。当 webpack 看到 import('./module.js')时,他就会将这个 module 移到独立的 chunk 中:
1 | $ webpack |
并且只在代码执行到 import() 才会下载。
这将会让 main bundle 更小,提升初始加载的时间。更重要的是改进缓存 - 如果你修改 main chunk 的代码,其他部分的 chunk 也不会受影响。
Note: 如果使用 Babel 编译代码,你会因为 Babel 还不认识 import() 而遇到语法错误抛出来。可以使用
syntax-dynamic-import解决这个错误。
import() function webpack 中 import() 文档import() syntax如果你的应用拥有多个路由或者页面,但是代码中只有单独一个 JS 文件(一个单独的 main chunk),这看起来你正在每一个请求中节省额外的 bytes 带宽。举个例子,当用户正在访问你网站的首页:

他们并不需要加载另外不同的页面上渲染文章标题的的代码 - 但是他们还是会加载到这段代码。更严重的是如果用户经常只访问首页,同时你还经常改变渲染文章标题的代码,webpack 将会对整个 bundle 失效 - 用户每次都会重复下载全部 app 的代码。
如果我们将代码拆分到页面里(或者单页面应用的路由里),用户就会只下载对他有意义的代码。更好的是,浏览器也会更好地缓存代码:当你改变首页的代码时,webpack 只会让相匹配的 chunk 失效。
通过路由拆分带页面引用,使用 import()(看看 “Lazy-load code that you don’t need right now”这部分)。如果你在使用一个框架,现在已经有成熟的方案:
react-router‘s docs (for React)vue-router‘s docs (for Vue.js)通过页面拆分传统多页面应用,可以使用 webpack 的 entry points 。如果你的应用有三种页面:主页、文章页、用户账户页,那就分厂三个 entries:
1 | // webpack.config.js |
对于每一个 entry 文件,webpack 将构建出独立的依赖树,并且声称一个 bundle,它将通过 entry 来只包括用到的 modules:
1 | $ webpack |
因此,如果仅仅是文章页使用 Lodash ,home 和 profile 的 bundle 将不会包含 lodash - 同时用户也不会在访问首页的时候下载到这个库。
拆分依赖树也有缺点。如果两个 entry points 都用到了 loadash ,同时你没有在 vendor 移除掉依赖,两个 entry points 将包括两个重复的 lodash 。在 Webpack 4 中我们可以设置 optimization.splitChunks.chunks: 'all' 解决该问题:
1 | // webpack.config.js (for webpack 4) |
这个选项可以开启智能拆分代码,webpack 将自动寻找 common code 并将其提取到一个单独的文件中。
在 Webpack 3 可以使用CommonsChunkPlugin来解决这个问题 - 它会将通用的依赖转移到一个独立的文件中:
1 | // webpack.config.js (for webpack 3) |
随意使用minChunks的值来找到最优的选项。通常情况下,你想要它尽可能体积小,但它会增加 chunks 的数量。举个例子,3 个 chunk,minChunks 可能是 2 个,但是 30 个 chunk,它可能是 8 个 - 因为如果你把它设置成 2 ,过多的 modules 将会打包进一个通用文件中,文件更臃肿。
当编译代码时,webpack 会分配给每一个 module 一个 ID。之后,这些 ID 就会被 require() 引用到 bundle 内部。你可以在编译输出的右侧在 moudle 路径之前看到这些 ID:
1 | $ webpack |
↓
1 | [0] ./index.js 29 kB {1} [built] |
默认情况下,这些 ID 是使用计数器计算出来的(比如第一个 module 是 ID 0,第二个 moudle 就是 ID 1,以此类推)。这样的问题就在于当你新增一个 module 事,它会出现在原来 module 列表中的中间,改变后面所有 module 的 ID:
1 | $ webpack |
↓ 我们增加一个新 module
1 | [4] ./webPlayer.js 24 kB {1} [built] |
↓ 现在看这里做了什么! comments.js 现在的 ID 由 4 变成了 5
1 | [5] ./comments.js 58 kB {0} [built] |
↓ ads.js 的 ID 由 5 变成 6
1 | [6] ./ads.js 74 kB {1} [built] |
这将使包含或依赖于具有更改ID的模块的所有块无效 - 即使它们的实际代码没有更改。在我们的代码中,_0_ 这个 chunk 和 main chunk 都会失效 - 只有 main 才应该失效。
使用HashedModuleIdsPlugin插件改变module ID 如何计算来解决这个问题。它利用 module 路径的 hash 来替换掉计数器:
1 | $ webpack |
↓
1 | [3IRH] ./index.js 29 kB {1} [built] |
有了这个方法,只有你重命名或者删除这个 moudle 它的 ID 才会变化。新的 modules 不会因为 module ID 互相影响。
启用这个插件,在配置中增加 plugins:
1 | // webpack.config.js |
import 通过 import 懒加载非必要代码作者 Ivan Akulov
即使当你配置好你的 webpack 让你的应用尽可能体积较小的时候,跟踪这个应用就非常重要,同时了解里面包含了什么。除此之外,你安装一个依赖,它将让你的 app 增加两倍大小 - 但并没有注意到这个问题!
这一部分就来讲解一些能够帮助你理解你的 bundle 的工具。
在开发时可以使用webpack-dashboard和命令行bundlesize 来监控 app 的体积。
webpack-dashboard可以通过依赖体积大小、进程和其他细节来改进 webpack 的输出。

这个 dashborad 帮助我们跟踪大型依赖 - 如果你增加一个依赖,你就立刻能在 Modules section 始终看到它!
启用这个功能,需要安装 webpack-dashboard 包:
1 | npm install webpack-dashboard --save-dev |
同时在配置的 plugins 增加:
1 | // webpack.config.js |
或者如果正在使用基于 Express dev server 可以使用 compiler.apply():
1 | compiler.apply(new DashboardPlugin()); |
多尝试 dashboard 找出改进的地方!比如,在 modules section 滚动找到那个库体积过大,把它替换成小的可替代的库。
bundlesize 可以验证 webpack assets 不超过指定的大小。通过自动化 CI 就可以知晓 app 是否变的过于臃肿:

配置如下:
1.分析 app 尽可能减小体积,执行生产环境的 build。
2.在package.json中增加bundlesize部分:
1 | // package.json |
3.使用npx执行bundlesize:
1 | npx bundlesize |
它就会将每一个文件的 gzip 压缩后的体积打印出来:
1 | PASS ./dist/icon256.6168aaac8461862eab7a.png: 10.89KB PASS./dist/icon512.c3e073a4100bd0c28a86.png: 13.1KB PASS./dist/main.0c8b617dfc40c2827ae3.js: 16.28KB PASS./dist/vendor.ff9f7ea865884e6a84c8.js: 31.49KB |
4.每一个体积增加10-20%,你将得到最大体积。这个10-20%的幅度可以让你像往常一样开发应用程序,同时警告你,当它的大小增长太多。
bundlesize 启用 bundlesize5.安装bundlesize开发依赖
1 | npm install bundlesize --save-dev |
6.在package.json中的bundlesize部分,声明具体的最大值。对于某一些文件(比如图片),你可以单独根据文件类型来设置最大体积大小,而不需要根据每一个文件:
1 | // package.json |
7.增加一个 npm 脚本来执行检查:
1 | // package.json |
8.配置自动化 CI 来在每一次 push 时执行npm run check-size做检查。(如果你在 Github 上开发项目,直接可以使用integrate bundlesize with GitHub。)
这就全部了!现在如果你运行npm run check-size或者 push 代码,你就会看到输出的文件是否足够小:

或者下面失败的情况

你想要深挖 bundle 内,看看里面具体哪些 module 占用多大空间。webpack-bundle-analyzer
译者注:此处有视频,需要科学上网,请自行观看
(Screen recording from github.com/webpack-contrib/webpack -bundle-analyzer)
webpack-bundle-analyzer 可以扫描 bundle 同时构建一个查看内部的可视化窗口。使用这个可视化工具找到过大或者不必要的依赖。
使用这个分析器,需要安装webpack-bundle-analyzer包:
1 | npm install webpack-bundle-analyzer --save-dev |
在 config 中增加插件:
1 | // webpack.config.js |
运行生产环境的 build 这个插件就会在浏览器中打开一个显示状态的页面。
默认情况下,这个页面会显示语法分析后的文件体积(在 bundle 出现的文件)。您可能想比较 gzip 的大小,因为这更接近实际用户的体验;使用左边的边栏来切换尺寸。
Note: 如果你使用 ModuleConcatenationPlugin,它可能在webpack-bundle-analyzer输出时合并一部分 module,使得报告小一些细节。如果你使用这个插件,在执行分析的时候需要禁用掉。
下面是报告中需要看什么:
optimization.splitChunks.chunks,或者在 Webpack 3中 使用 CommonsChunkPlugin 将他们移到一个通用文件内)亦或是在同一个库中 bundle 拥有多个版本?同样的,也可以看看 Sean Larkin 的文章 great analysis of webpack bundles。
webpack-dashboard and bundlesize to stay tuned of how large your app iswebpack-bundle-analyzer总结:
Webpack 不仅仅是一个帮助你更快创建 app 的工具。它还帮助使你的 app 成为 a Progressive Web App ,你的应用拥有更好的体验以及自动化的填充工具就像Lighthouse根据环境给出建议。
不要忘记阅读 webpack docs - 里面提供了大量的优化相关的信息。
多多练习 with the training app!
]]>webpack 增加了一个模式配置(mode option)。下面来解释一下具体内容。

webpack 4 增加了一个模式选项(mode option)。并且这个配置是必须存在的。(实际上它不是必须的配置项,但如果你省略掉没有配就会有警告。)
webpack 4 目前有两个默认配置,development 和 production 。这两个值可以在 mode 中设置。
设置 development 将会带给你最好*的开发体验,它专注于以下几点:
(*实际上这里指最好的体验取决于你使用的方式,我们尽力在这里囊括了大多数通用部分)
设置 production 将会带给你在发布应用时的一系列有用的默认设置,它专注于以下几点:
development-only)的代码最后一点非常重要。它基本意味着 production 会提供给你优化后的资源包,但这不是完美的优化有的资源包。这里还存在着很多可优化点,但是它们会让结果更难使用。这些优化点是有意被忽略的,因为在这里,我们更看重入门的体验高于完美的优化。大部分优化点也只在大型应用上起作用。
随着这个 mode 配置的引入,我们尝试去减少 build 时必要配置。我们尝试着去用一些默认项去覆盖通用的配置。
然而从我们的经验来看我们也知道默认配置并不适用于所有人。不同的团队有不同的需求,有时候会因为遗留代码、有时候会因为个人喜好、有时候会因为特殊的应用或者有时候使用者会认为这并不是最好通用解决方案。许多人确实想要修改默认配置以适应他们自己的项目。We got you covered. 增加 mode 并不意味这我们移除掉了这些配置。这里面仍然是一切皆可配置。实际上我们使内部大多数优化都是可配置的(你可以禁用掉他们)。
mode 选项是通过在配置中设置默认项实现的。这个不会被其他配置执行的 mode 也不会做任何行为。
下一部分就会更深入地讨论因为mode或其他选项而影响的配置。
在 development 模式下默认是 eval。否则不使用 devtool。
eval devtool 没有提供最佳的质量,但是拥有很好的性能。这就是我们选择的折中方案。看一看这些配置的文档,这可以获取更高质量的 sourcemap。
只在 development 模式下启用,否则禁用缓存。
缓存模块可以避免在没有改变时重建。
在内存缓存只在 watch 模式下有用,并且我们假设你在开发时正在使用 watch 模式。不用缓存时,内存占用率更低。
只在 cache 启用时启用,否则禁用。
缓存机械依赖项可以避免重新解析它们。
只在 development 模式下启用,否则禁用。
这些额外的注释对于 debug 很有作用,尤其是使用 eval devtool。
只在 production 模式下启用,否则禁用。
体积限制只对最小化资源起作用,同时伴随着性能开销。因此它只在生产模式下启用。
总是开启。
当他们在父级 chunk groups 中都可用时,这些模块会被移除掉。它可以减少资源包的体积。因为有更少的代码生成,更小的资源包就意味着更快的 build 过程。
总是开启。
空 chunks 会被移除掉。这些在文件系统中减少 load 会导致更快的 build。
总是开启。
相等的 chunks 会被合并。结果就是更少的生成代码,更快的 build。
只在 production 模式下开启,否则禁用。
确定作为其他 chunks 子集的 chunks,并且这样方式对齐标记,即当加载较大 chunks 时,不必加载子集。
只在 production 模式下开启,否则禁用。
提供更常用的 ids 更小(更短)的值。
总是开启。
尽可能地确定每一个 module 的 exports。这个信息被用于其他优化或者生成代码。为了消除歧义:为 export * from 生成更有效率的代码。
只在 production 模式下开启,否则禁用。
尽可能地确定每一个 module 的 exports。它依赖于 optimization.providedExports。这个信息被用于其他优化或者生成代码。消除歧义:导出不会生成无用的 exports, 当所有的用途都被兼容的时候导出 names 就是零碎的单独字符定义。在最小化中 DCE 会有一处,同时移除掉无用的 exports。
只在 production 模式下开启,否则禁用。
认可在 package.json 或 rules 中的 sideEffects 标志去消除 modules。 它依赖 optimization.providedExports 和 optimization.usedExports。这些依赖都有所开销,但是消除依赖通过减少生成代码在性能上有积极的影响。它也依赖于你自己的代码。为了更好的性能,去尝试吧。
只在 production 模式下开启,否则禁用。
尝试查找模块图中可以安全连接到单个模块中的段。它依赖于optimization.providedExports 和 optimization.usedExports。
总是开启。
查找在 chunks 之间哪些 module 被共享,同时将他们拆分到独立的 chunks 中,目的是减少重复或者从 application modules 中分离 vendor modules。
总是开启。
为 webpack 运行时和 chunk manifest 创建一个独立的 chunk。这个 chunk 应该内联到 HTML 中。
只在 production 模式下开启,否则禁用。
当发生编译错误的时候不输出资源包。
只在 development 模式下开启,否则禁用。
取代数值型 ID,提供给 module 更有用的命名。
mode 值得默认项:development 或者 production。
定义 process.env.NODE_ENV 成为编译时常量值。这就允许移除掉 development only 的代码。
只在 production 模式下开启,否则禁用。
使用最小化工具来压缩输出的资源包,比如(optimization.minimizer默认使用的uglify-js)。
在文件内记录时启用,否则禁用。
记录中使用的标识与上下文目录有关。
2018-03-01 Beanlee
相信各位在移动端开发过程中一定遇到抓取数据包、拦截请求的场景,本文主要介绍移动端在针对 HTTPS 抓包时的几款软件的配置(包括Charles、Fiddler、Whistle),下面举例将已 iOS 11 为例,附带 Android 机型配置截图。
查看本机 IP,使用 ifconfig 命令查看本机 IP;或者打开系统设置查看 IP 如截图;
例如:当前本机 IP 192.168.191.4


设置 -> 无线局域网 -> HTTP 代理 -> 手动
服务器:192.168.191.4 端口:8888

此时可以在应用的界面中看到 HTTP 的请求数据了。
接下来继续 HTTPS 的配置。
打开 Charles 后如下图操作,电脑端信任证书,注意 此处选择system


打开 Safari 地址输入 192.168.191.4:8888 或 chls.pro/ssl 点击安装,此时会弹出要求需要输入手机密码,完成后证书安装成功。
此时查看 通用 -> 描述文件 中 charles proxy ca 已变成已验证。
注意 自 iOS 10.3 以上,还需要多一步操作,手动信任自定义根证书,才能确保证书安装并已启用;
关于本机 -> 证书信任设置 启用 charles proxy ca 截图详见文章底部锦囊
注意 Android 手机不需要安装 Charles 证书 ssl-certificates
下面截图中是 Android 仅安装了 Fiddler 和 Whistle 的证书截图。

Proxy->SSL Proxy setting Enabled SSL Proxying
同时添加希望拦截到的域名,例如:*.jd.com 端口 443


配置完成 : )
配置与 Charles 配置相同端口略有不同,此处不再重复。
总结几大步骤:


nodejs v0.10.0+npm i -g whistlew2 start完成上面几步操作后,此时 8899 端口已经常驻后台。

whistle 提供了一个基于 Chrome 的插件,可以方便切换代理,查看网络,编辑规则等,不过很遗憾作者已经将插件在 Web Store 下架,现在搜索不到,我把本地 0.10.0 版本打包成 crx 上传在 Google Driver 可以访问 下载
或者改用另外一个强大的插件 Proxy SwitchyOmega 该插件也拥有强大的代理功能,同时支持自定义规则,可以自动根据域名或端口等自动选择是否启用代理,以后有机会可以分享使用 Proxy SwitchOmega 的使用经验。
注意 自 iOS 10.3 以后,无论使用上面哪一种工具抓包,安装完 ca 证书以后,还需要多一步操作,才能确保证书安装并已启用;
通用 -> 关于本机 -> 证书信任设置 -> 启用相对应的证书,就可以看到工具中抓取到相应的 HTTPS 请求。

这一点跟苹果的 one more thing 貌似有那么一点不谋而合。
注意 Android 部分手机(如小米、华为)无法安装证书时候,不妨试试在电脑端将证书导出,打开手机 FTP 或者利用数据线传输到手机某个文件夹,点开 WLAN 配置 -> 高级设置 -> 安装证书 选择对应的证书安装即可。亦或是因为权限问题无法安装,打开安全隐私,选择信任 未知来源。
此办法对于安装测试版 APP 也管用 :)
have fun :D
]]>科学上网。已经熟悉以上三个工具基础配置,仅仅需要解决抓包 iOS 下 HTTPS 问题的同学可以直接打开锦囊
查看本机 IP,使用 ifconfig 命令查看本机 IP;或者打开系统设置查看 IP 如截图;
当前本机 IP 192.168.191.4


设置 -> 无线局域网 -> HTTP 代理 -> 手动
服务器:192.168.191.4 端口:8888
此时已经可以抓取 HTTP 数据了
打开 charles 后如下图操作,本机信任 charles 证书,注意 此处选择system


打开 safari 地址输入192.168.191.4:8888或chls.pro/ssl点击安装,此时会弹出要求需要输入手机密码,完成后证书安装成功。
此时查看通用->描述文件中 charles proxy ca 已变成已验证。
注意 自 iOS 10.3 以上,还需要多一步操作,手动信任自定义根证书,才能确保证书安装并已启用;如图所示;
关于本机->证书信任设置启用 charles proxy ca 截图详见锦囊
Proxy->SSL Proxy setting Enabled SSL Proxying
同时增加希望抓取的域名,例如:*.jd.com端口 443


配置完成 : )
配置与 Charles 配置相同端口略有不同,此处不再重复


nodejs v0.10.0+npm i -g whistlew2 start完成上面几步操作后,此时8899端口已经常驻后台。

whistle 提供了一个基于 Chrome 的插件,可以方便切换代理,查看网络,编辑规则等,不过很遗憾作者已经将插件在 Web Store 下架,现在搜索不到,我把本地 0.10.0 版本打包成 crx 上传在 Google Driver 可以访问 下载
或者改用另外一个强大的插件 Proxy SwitchyOmega
注意 自 iOS 10.3 以后,无论使用上面哪一种工具抓包,安装完 ca 证书以后,还需要多一步操作,才能确保证书安装并已启用;
通用 -> 关于本机 -> 证书信任设置 -> 启用相对应的证书,就可以看到工具中抓取到相应的 HTTPS 请求。

这点跟苹果的 one more thing 貌似有那么一点不谋而合。
have fun :D
]]>原文 https://developers.google.com/web/fundamentals/performance/webpack/
作者 Addy Osmani
现代 Web 应用经常用到bunding tool来创建生产环境的打包文件(例如脚本、样式等),打包文件是需要优化并压缩最小化,同时能够被让用户更快地下载到。在这篇文章中,我们将会利用webpack来贯穿说明如何优化网站资源。这样可以帮助用户更快地加载你的应用同时获得更好的体验。

webpack 目前是最流行的打包工具之一,深入地利用它的特点去优化代码,将脚本拆分成重要和非重要部分,还有剔除无用的代码能够保证你的应用维持最小的带宽和进程消耗。

Note: 我们创建了一个练习的应用来演示下面这些优化的描述。尽力抽更多的时间来练习这些 tips
webpack-training-project
让我们从现代 web 应用中最耗费资源之一的 Javascript 开始。
作者 Ivan Akulov
当你正在优化一个应用时,首要事情就是尽可能地将它体积的减小。下面就是利用 webpack 如何做。
最小化是通过去除多余空格、缩短变量名等方式压缩代码。例如:
1 | // Original code |
↓
1 | // Minified code |
Webpack 支持两种方式最小化代码:UglifyJS 插件和loader-specific options。他们可以同时使用。
The UglifyJS plugin在 bundle 层级中起作用,在编译之后压缩 bundle。下面来展示如何工作:
1.代码:1
2
3
4
5// comments.js
import './comments.css';
export function render(data, target) {
console.log('Rendered!');
}
2.Webpack 打包后大概是下面这样:1
2
3
4
5
6
7
8
9
10
11// bundle.js (part of)
;
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony export (immutable) */ __webpack_exports__["render"] = render;
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default =
__webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__);
function render(data, target) {
console.log('Rendered!');
}
3.使用 UglifyJS 插件压缩最小化后大概是下面这样:1
2
3// minified bundle.js (part of)
;function t(e,n){console.log("Rendered!")}
Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
插件集成在 webpack 中,把它的配置在plugins中就可以启用:1
2
3
4
5
6
7
8// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
第二种方式loader-specific options 利用 loader options,可以压缩 Uglify 插件无法最小化的部分。举例,当你利用css-loader引入一个 CSS 文件时,文件会编译成一个字符串:1
2
3
4/* comments.css */
.comment {
color: black;
}
↓1
2
3// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n color: black;\r\n}",""]);
UglifyJS 不能压缩字符串。要压缩这段 css 内容,需要配置 loader :1
2
3
4
5
6
7
8
9
10
11
12
13
14// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
Note: UglifyJS 插件不能编译 ES2015+(ES2016),这意味着如果你的 diamante 中使用类、箭头函数和一些新特性语法,不能编译成 ES5,插件会抛异常。
如果需要编译新语法,要使用 uglifyjs-webpack-plugin 包。也是集成在 webpack 中相同的插件,但是更新一些,能够有能力编译 ES2015+。
NODE_ENV=production 明确生产环境信息减小前端体积的另外一个方法就是在代码中将NODE_ENV环境变量设置成production。
Libraries 会读取NODE_ENV变量判断他们应该在那种模式下工作 - 开发模式 or 生成模式。很多库会基于这个变量有不同的表现。举个例子,当NODE_ENV没有设置成production,Vue.js 会做额外的检查并且输出一些警告:
1 | // vue/dist/vue.runtime.esm.js |
React 也是类似 - 开发模式下 build 带有一些警告:
1 | // react/index.js |
这些检查和警告通常在生产环境下是不必要的,但是他们仍然保留在代码中并且会增加库的体积。通过配置 webpack 的 DefinePlugin 来剔除掉:
1 | // webpack.config.js |
DefinePlugin用确定的变量替换所有存在的说明变量。利用下面配置:
1.DefinePlugin将用"production"替换到process.env.NODE_ENV:
1 | // vue/dist/vue.runtime.esm.js |
↓
1 | // vue/dist/vue.runtime.esm.js |
Note: 如果你偏向有通过 CLI 配置变量,可以查看一下 EnvironmentPlugin。它和
DefinePlugin类似,但读环境并且自动替换process.env表达式。
2.UglifyJS会移除掉所有if分支 - 因为"production" !== 'production'永远返回 false ,插件理解代码内的判断分支将永远不会执行:
1 | // vue/dist/vue.runtime.esm.js |
↓
1 | // vue/dist/vue.runtime.esm.js (without minification) |
Note: 不一定强制要求使用
UglifyJSPlugin。你可以使用其他不同的最小化工具,这些页支持移除无用代码(例如,the Babel Minify plugin or the Google Closure Compiler plugin)
DefinePlugin, EnvironmentPlugin下面这个方式利用 ES modules 减小前端体积。
当你使用 ES module,webpack 有能力去做 tree-shaking。Tree-shaking 贯穿整个依赖树,检查那些依赖被使用,移除无用依赖。因此,如果你使用 ES module 语法,webpack 可以排除掉无用代码:
1.一个有多个 export 的文件,但是 app 只需要其中一个:
1 | // comments.js |
2.webpack 理解 commentRestEndPoint没有使用,同时不能在一个 bundle 中生成单独的 export:
1 | // bundle.js (part that corresponds to comments.js) |
3.UglifyJSPlugin移除无用变量:
1 | // bundle.js (part that corresponds to comments.js) |
如果他们都是有 ES module 编写,就是与一些库并存时也是生效的。
Note: 在 webpack 中,tree-shaking 没有 minifier 是无法生效的。 webpack 仅仅移除了没有被用到的 export 变量;
UglifyJSPlugin才会移除无用代码。所以如果你编译打包时没有使用 minifier,打包后体积并不会更小。你也可以不一定使用这个插件。其他最小化的插件也支持移除 dead code(例如:Babel Minify plugin or Google Closure Compiler plugin)
Warning: 不要将 ES module 编译到 CommonJS 中。 如果你使用 Babel
babel-preset-envorbabel-preset-es2015,检查一下当前的配置。默认情况下, ESimportandexportto CommonJSrequireandmodule.exports。通过设置 option 来禁止掉Pass the{ modules: false }option。
图片基本会占局页面一半以上体积。虽然它们不像 JavaScript 那么重要(比如它们不会阻止页面渲染),但图片仍然会占用掉一大部分带宽。利用url-loader,svg-url-loader和image-webpack-loader来在 webpack 中进行优化。
url-loader 允许将小静态文件打包进 app。没有配置,他需要通过 file,将它放在编译后的打包 bundle 内并返回一个这个文件的 url。然而,如果我们注明limit选项,它将会 encode 成更小的文件 base64 文件 url。这是可以将图片放在Javascript 代码中,同时节省 HTTP 请求:
1 | // webpack.config.js |
1 | // index.js |
Note: 内联图片减少了独立请求的数量,这是很好的方式(even with HTTP/2),但是会增加 bundle下载和转换的时间和内存的消耗。一定要确保不要嵌入超大图片或者较多的图片 - 否则增加的 bundle 的时间将会掩盖做成内联图片的收益。
svg-url-loader与url-loader类似 - 都是将使用 URL encoding encode 文件。这对对于 SVG 图片很奏效 - 因为 SVG 文件是文本,encoding 在体积上更有效率:
1 | // webpack.config.js |
Note: svg-url-loader 拥有改善 IE 浏览器支持的 options,但是在其他浏览器中更糟糕。如果你需要兼容 IE 浏览器,设置 iesafe: true 选项
image-webpack-loader压缩图片使之变小。它支持 JPG,PNG,GIF 和 SVG,因为我们将会使用它所有类型。
这个 loader 不会将图片嵌入在应用内,因此它必须与url-loader和svg-url-loader配合使用。避免复制粘贴到相同的 rules 中(一个用于 JPG/PNG/GIF 图片,另一个用于 SVG 图片),我们来使用enforce: pre作为单独的一个 rule 涵盖这个 loader:
1 | // webpack.config.js |
默认 loader 设置就已经可以满足需求了 - 但如果你想要深入配置,请查看 the plugin options。为了选择哪些 options 需要明确,可以查看 Addy Osmani 的 guide on image optimization
平均一半以上的 Javascript 体积大小来源于依赖包,并且这些可能都不是必要的。
举一个例子来说,Lodash(v4.17.4)增加了最小化代码的 72KB 大小到 bundle 中。但是如果你仅仅用到它的20个方法,大约 65 KB 代码没有用处。
另外一个例子就是 Moment.js。 V2.19.1版本最小化后有 223KB,体积巨大 - 截至2017年10月一个页面内的 Javascript 平均体积是 452KB。但是,本地文件的体积占 170KB。如果你没有用到 多语言版 Moment.js,这些文件都会没有目的地使 bundle 更臃肿。
所有这些依赖都可以被轻易优化。我们在 Github repo 收集了优化的建议,check it out!
当你构建 bundle 时,webpack 将每一个 module 封装进 function 中:
1 | // index.js |
↓
1 | // bundle.js (part of) |
在以前,这么做是使 CommonJS/AMD modules 互相分离所必须的。但是,这会增加体积并且性能表现堪忧。
Webpack 2 介绍了 ES modules 的支持,不像 CommonJS 和 AMD modules 一样,而是能够不用将每一个 module 用 function 封装起来。同时 Webpack 3 利用ModuleConcatenationPlugin完成这样一个 bundle,下面是例子:
1 | // index.js |
↓
1 | // Unlike the previous snippet, this bundle has only one module |
看到区别了吗?在这个 bundle 中, module 0 需要 module 1 的 render 方法。使用 ModuleConcatenationPlugin,require被直接简单的替换成 require 函数,同时 module 1 被删除删除掉了。这个 bundle 拥有更少的 modules,就有更少的 modules 损耗!
启用这个功能,可以在插件列表中增加ModuleConcatenationPlugin:1
2
3
4
5
6
7
8// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
],
};
Note:想要知道为什么这个功能不是默认启用?Concatenating modules 很棒, 但是他会增加编译的时间同时破坏 module 的热更新。这就是为什么只在生产环境中启用的原因了。
externals if you have both webpack and non-webpack code 如果代码中包含 webpack 和非 webpack 的代码要使用 externals你可能拥有一个体积庞大的工程,其中一部分代码可以使用 webpack 编译,而有一些代码又不能。比如一个视频网站,播放器的 widget 可能通过 webpack 编译,但是其周围页面区域可能不是:

如果两部分代码有相同的依赖,你可以共享这些依赖以便减少重复下载耗时。the webpack’s externals option就干了这件事 - 它用变量或者外部引用来替代 modules。
如果你的非 webpack 代码依靠这些依赖,它们是挂载 window 上的变量,可以将依赖名称 alias 成变量名:
1 | // webpack.config.js |
利用这个配置,webpack 将不会打包 react 和 react-dom包。取而代之,他们会被替换成下面这个样子:
1 | // bundle.js (part of) |
如果你的非 webpack 代码没有将依赖暴露挂载到 window 上,这就更复杂了。但是如果非 webpack 代码使用 AMD 包的形式消费了这些依赖,你仍然可以避免重复的代码加载两次。
具体如何做呢?将 webpack 代码编译成一个 AMD module 同时别名成一个库的 URLs:
1 | // webpack.config.js |
Webpack 将会把 bundle 包装进 define()同时让它依赖于这些URLs:
1 | // bundle.js (beginning) |
如果非 webpack 代码使用相同的 URLs 加载依赖,这些文件将会加载一次 - 多余的请求会使用缓存。
Note:webpack 只是替换那些
externals对象中的准确匹配的 keys 的引用。这意味着如果你的代码这样写import React from 'react/umd/react.production.min.js',这个库是不会被 bundle 排除掉的。这是因为 - webpack 并不知道import 'react'和import 'react/umd/react.production.min.js'是同一个库,这样比较谨慎。
externalsUglifyJsPlugin and loader optionsDefinePluginexternals if this makes sense for you作者 Ivan Akulov
在做完优化应用体积之后的下一步提升应用加载时间的就是缓存。在客户端中使用缓存作为应用的一部分,这样会在每一次请求中减少重新下载的次数。
做缓存通用的解决办法:
1.告诉浏览器缓存一个文件很长时间(比如一年)
1 | # Server header |
Note:如果你不熟悉 Cache-Control 做了什么,你可以看一下 Jake Archibald 的精彩博文 on caching best practices
2.当文件改变需要强制重新下载时去重命名这些文件
1 | <!-- Before the change --> |
这些方法可以告诉浏览器下载这些 JS 文件,将其缓存起来。浏览器将只会在文件名发生改变时才会请求网络(或者缓存失效的情况也会请求)。
使用 webpack,也可以做同样的事,但可以使用版本号来解决,需要明确这个文件的 hash 值。使用 [chunkhash] 可以将 hash 值包含进文件名中:
1 | // webpack.config.js |
Note: webpack 可能会生成不同的 hash 即使 bundle 相同 - 比如你重名了了一个文件或者重新在不同的操作系统下编译了一个 bundle。 This is a bug.
如果你需要将文件名发送给客户端,也可以使用HtmlWebpackPlugin或者WebpackManifestPlugin。
HtmlWebpackPlugin 很简单,但灵活性欠缺一些。编译时,插件会生成一个 HTML 文件,这其中包括所有的编译后的资源文件。如果你的业务逻辑不复杂,这就非常适合你:
1 | <!-- index.html --> |
WebpackManifestPlugin更灵活一些,它可以帮助你解决业务负责的部分。编译时它会生成一个 JSON 文件,这文件保存这没有 hash 值文件与有 hash 文件之间的映射。服务端利用这个 JSON 可以识别出那个文件有效:
1 | // manifest.json |
App 依赖通常情况下趋向于比实际 app 内代码中更少的变化。如果你将他们移到独立的文件中,浏览器将可以把他们独立缓存起来 - 同时不会每次 app 代码改变时重新下载。
Key Term: 在 webpack 的技术中,利用 app 代码拆分文件被称为
chunks。我们后面会用到这个名词。
为了将依赖包提取到单独的 chunk 中,下面分为三步:
1.使用[name].[chunkname].js替换output的文件名:
1 | // webpack.config.js |
当 webpack 构建应用时,它会用一个带有 chunk 的名称来替换[name]。如果没有添加[name]部分,我们不得不通过 chunks 之间的 hash 区别来比较他们的区别 - 那就太难了!
2.将entry转成一个对象:
1 | // webpack.config.js |
在这段代码中,”main” 对象是一个 chunk 的名字。这个名字将会被步骤 1 里面的 [name]代替。目前为止,如果你构建一个 app,chunk 就会包括整个 app 的代码 - 就像我们没有做这些步骤一样。但是很快就会产生变化。
3.添加 CommonsChunkPlugin:
1 | // webpack.config.js |
插件将包括全部node_modules路径下的 modules 同时将他们移到一个单独的文件中,这个文件被称为 vendor.[chunkhash].js。
完成了上面的步骤,每一次 build 都会生成两个文件。浏览器会将他们单独缓存 - 以便代码发生改变时重新下载。
1 | $ webpack |
不幸的是,仅仅抽取vendor是不够的。如果你试图在应用代码中修改一些东西:
1 | // index.js |
你会注意到vendor的 hash 值也会改变:
1 | Asset Size Chunks Chunk Names |
↓
1 | Asset Size Chunks Chunk Names |
发生这样的事是因为 webpack 打包时,一部分 modules 的代码,拥有 a runtime - 管理模块执行一部分代码。当你将代码拆分成多个文件时,这小部分代码在 chunk ids 和 匹配的文件之间开始了一个映射:
1 | // vendor.e6ea4504d61a1cc1c60b.js |
Webpack 将最新生成的 chunk 包含在这个 runtime 内,这个 chunk 就是我们代码中的vendor。与此同时每一次任何 chunk 的改变,这一小部分代码也改变,导致整个 vendor chunk 也会改变。
为了解决这个问题,我们将 runtime 转义到一个独立的文件中,通过CommonsChunkPlugin创建一个额外的空的 chunk:
1 | // webpack.config.js |
完成这一部分改变,每一次 build 都将生成三个文件:
1 | $ webpack |
将他们反过来顺序添加到 index.html 中,你就搞定了:
1 | <!-- index.html --> |
为了做的更好,尽力把 webpack runtime 内联在 HTML 请求里。下面举例:
1 | <!-- index.html --> |
这样做:
1 | <!-- index.html --> |
这个 runtime 很小,内联它可以帮助你节省 HTTP 请求(尤其对 HTTP/1 重要;但是在 HTTP/2 就没有那么重要了,但是仍能够提高效率)。
下面就来看看如何做。
如果使用 HtmlWebpackPlugin 来生成 HTML 文件,InlineChunkWebpackPlugin 就足够了。
1.将 runtime 名称改成静态的明确的文件名:
1 | // webpack.config.js |
2.以方便的方式将 runtime.js 嵌入进去。比如:Node.js 和 Express
1 | // server.js |
有时候,页面拥有或多或少的部分:
在这些案例中,通过仅下载最重要的部分,懒加载剩余区域能够提升最初的加载性能。使用 the import() function 和 code-splitting 解决这个问题:
1 | // videoPlayer.js |
import()明确表示你期望动态地加载独立的 module。当 webpack 看到 import('./module.js')时,他就会将这个 module 移到独立的 chunk 中:
1 | $ webpack |
并且只在代码执行到 import() 才会下载。
这将会让 main bundle 更小,提升初始加载的时间。更重要的是改进缓存 - 如果你修改 main chunk 的代码,其他部分的 chunk 也不会受影响。
Note: 如果使用 Babel 编译代码,你会因为 Babel 还不认识 import() 而遇到语法错误抛出来。可以使用
syntax-dynamic-import解决这个错误。
import() functionimport() syntax如果你的应用拥有多个路由或者页面,但是代码中只有单独一个 JS 文件(一个单独的 main chunk),这看起来你正在每一个请求中节省额外的 bytes 带宽。举个例子,当用户正在访问你网站的首页:

他们并不需要加载另外不同的页面上渲染文章标题的的代码 - 但是他们还是会加载到这段代码。更严重的是如果用户经常只访问首页,同时你还经常改变渲染文章标题的代码,webpack 将会对整个 bundle 失效 - 用户每次都会重复下载全部 app 的代码。
如果我们将代码拆分到页面里(或者单页面应用的路由里),用户就会只下载对他有意义的代码。更好的是,浏览器也会更好地缓存代码:当你改变首页的代码时,webpack 只会让相匹配的 chunk 失效。
通过路由拆分带页面引用,使用 import()(看看 “Lazy-load code that you don’t need right now”这部分)。如果你在使用一个框架,现在已经有成熟的方案:
react-router‘s docs (for React)vue-router‘s docs (for Vue.js)通过页面拆分传统多页面应用,可以使用 webpack 的 entry points 。如果你的应用有三种页面:主页、文章页、用户账户页,那就分厂三个 entries:
1 | // webpack.config.js |
对于每一个 entry 文件,webpack 将构建出独立的依赖树,并且声称一个 bundle,它将通过 entry 来只包括用到的 modules:
1 | $ webpack |
因此,如果仅仅是文章页使用 Lodash ,home 和 profile 的 bundle 将不会包含 lodash - 同时用户也不会在访问首页的时候下载到这个库。
拆分依赖树也有缺点。如果两个 entry points 都用到了 loadash ,同时你没有在 vendor 移除掉依赖,两个 entry points 将包括两个重复的 lodash 。我们可以使用CommonsChunkPlugin来解决这个问题 - 它会将通用的依赖转移到一个独立的文件中:
1 | // webpack.config.js |
随意使用minChunks的值来找到最优的选项。通常情况下,你想要它尽可能体积小,但它会增加 chunks 的数量。举个例子,3 个 chunk,minChunks 可能是 2 个,但是 30 个 chunk,它可能是 8 个 - 因为如果你把它设置成 2 ,过多的 modules 将会打包进一个通用文件中,文件更臃肿。
当编译代码时,webpack 会分配给每一个 module 一个 ID。之后,这些 ID 就会被require()引用到 bundle 内部。你可以在编译输出的右侧在 moudle 路径之前看到这些 ID:
1 | $ webpack |
↓
1 | [0] ./index.js 29 kB {1} [built] |
默认情况下,这些 ID 是使用计数器计算出来的(比如第一个 module 是 ID 0,第二个 moudle 就是 ID 1,以此类推)。这样的问题就在于当你新增一个 module 事,它会出现在原来 module 列表中的中间,改变后面所有 module 的 ID:
1 | $ webpack |
↓ 我们增加一个新 module
1 | [4] ./webPlayer.js 24 kB {1} [built] |
↓ 现在看这里做了什么!comments.js现在的 ID 由 4 变成了 5
1 | [5] ./comments.js 58 kB {0} [built] |
↓ ads.js 的 ID 由 5 变成 6
1 | [6] ./ads.js 74 kB {1} [built] |
这将使包含或依赖于具有更改ID的模块的所有块无效 - 即使它们的实际代码没有更改。在我们的代码中,_0_这个 chunk 和 main chunk 都会失效 - 只有main才应该失效。
使用HashedModuleIdsPlugin插件改变module ID 如何计算来解决这个问题。它利用 module 路径的 hash 来替换掉计数器:
1 | $ webpack |
↓ Here
1 | [3IRH] ./index.js 29 kB {1} [built] |
有了这个方法,只有你重命名护着删除这个 moudle 它的 ID 才会变化。新的 modules 不会因为 module ID 互相影响。
启用这个插件,在配置中增加 plugins:
1 | // webpack.config.js |
import作者 Ivan Akulov
即使当你配置好你的 webpack 让你的应用尽可能体积较小的时候,跟踪这个应用就非常重要,同时了解里面包含了什么。除此之外,你安装一个依赖,它将让你的 app 增加两倍大小 - 但并没有注意到这个问题!
这一部分就来讲解一些能够帮助你理解你的 bundle 的工具。
在开发时可以使用webpack-dashboard和命令行bundlesize 来监控 app 的体积。
webpack-dashboard可以通过依赖体积大小、进程和其他细节来改进 webpack 的输出。

这个 dashborad 帮助我们跟踪大型依赖 - 如果你增加一个依赖,你就立刻能在 Modules section 始终看到它!
启用这个功能,需要安装 webpack-dashboard 包:
1 | npm install webpack-dashboard --save-dev |
同时在配置的 plugins 增加:
1 | // webpack.config.js |
或者如果正在使用基于 Express dev server 可以使用 compiler.apply():
1 | compiler.apply(new DashboardPlugin()); |
多尝试 dashboard 找出改进的地方!比如,在 modules section 滚动找到那个库体积过大,把它替换成小的可替代的库。
bundlesize 可以验证 webpack assets 不超过指定的大小。通过自动化 CI 就可以知晓 app 是否变的过于臃肿:

配置如下:
1.分析 app 尽可能减小体积,执行生产环境的 build。
2.在package.json中增加bundlesize部分:
1 | // package.json |
3.使用npx执行bundlesize:
1 | npx bundlesize |
它就会将每一个文件的 gzip 压缩后的体积打印出来:
1 | PASS ./dist/icon256.6168aaac8461862eab7a.png: 10.89KB PASS./dist/icon512.c3e073a4100bd0c28a86.png: 13.1KB PASS./dist/main.0c8b617dfc40c2827ae3.js: 16.28KB PASS./dist/vendor.ff9f7ea865884e6a84c8.js: 31.49KB |
4.每一个体积增加10-20%,你将得到最大体积。这个10-20%的幅度可以让你像往常一样开发应用程序,同时警告你,当它的大小增长太多。
bundlesize 启用 bundlesize5.安装bundlesize开发依赖
1 | npm install bundlesize --save-dev |
6.在package.json中的bundlesize部分,声明具体的最大值。对于某一些文件(比如图片),你可以单独根据文件类型来设置最大体积大小,而不需要根据每一个文件:
1 | // package.json |
7.增加一个 npm 脚本来执行检查:
1 | // package.json |
8.配置自动化 CI 来在每一次 push 时执行npm run check-size做检查。(如果你在 Github 上开发项目,直接可以使用integrate bundlesize with GitHub。)
这就全部了!现在如果你运行npm run check-size或者 push 代码,你就会看到输出的文件是否足够小:

或者下面失败的情况

你想要深挖 bundle 内,看看里面具体哪些 module 占用多大空间。webpack-bundle-analyzer
(Screen recording from github.com/webpack-contrib/webpack -bundle-analyzer)
webpack-bundle-analyzer 可以扫描 bundle 同时构建一个查看内部的可视化窗口。使用这个可视化工具找到过大或者不必要的依赖。
使用这个分析器,需要安装webpack-bundle-analyzer包:
1 | npm install webpack-bundle-analyzer --save-dev |
在 config 中增加插件:
1 | // webpack.config.js |
运行生产环境的 build 这个插件就会在浏览器中打开一个显示状态的页面。
默认情况下,这个页面会显示语法分析后的文件体积(在 bundle 出现的文件)。您可能想比较 gzip 的大小,因为这更接近实际用户的体验;使用左边的边栏来切换尺寸。
Note: 如果你使用 ModuleConcatenationPlugin,它可能在webpack-bundle-analyzer输出时合并一部分 module,使得报告小一些细节。如果你使用这个插件,在执行分析的时候需要禁用掉。
下面是报告中需要看什么:
同样的,也可以看看 Sean Larkin 的文章 great analysis of webpack bundles。
webpack-dashboard and bundlesize to stay tuned of how large your app iswebpack-bundle-analyzer总结一下:
Webpack 不仅仅是一个帮助你更快创建 app 的工具。它还帮助使你的 app 成为 a Progressive Web App ,你的应用拥有更好的体验以及自动化的填充工具就像Lighthouse根据环境给出建议。
不要忘记阅读 webpack docs - 里面提供了大量的优化的信息。
多多练习 with the training app!
]]>描述:我正在试着总结关于最流行的 Javascript 包管理器,打包器和任务执行器的知识。如果有错误请纠正我。
npm & bower 是包管理工具。他们只是将依赖文件下载,并不知道在下载这些文件的基础上如何编译项目。他们知道的是在获取所有依赖之后去调用 webpack/gulp/grunt 。bower 很像 npm ,但是编译依赖树不在行(不像 npm 那样递归着进行)。意味着 npm 获取每一依赖(可能获取几次相同的文件),然而 bower 期望你手动去包含子依赖。有时候 bower 和 npm 可以一起被使用,分别作用前端和后端(在前端每一mb都很重要)。glup 和 gulp 是任务运行器,目的是将能够被自动化的所有事任务自动化执行。(比如编译css/sass,处理图片,打包还有最小化混淆代码)。grunt vs gulp (就像maven vs gradle 或是 配置 vs 编码)。Grunt 是基于分离独立的任务配置的,每一个任务开始/处理/关闭文件。Gulp 需要少量代码,并且基于 node streams,那允许它编译链(w/o重复打开相同文件)而且执行很快。webpack (webpack-dev-server) 对于我来说,它是任务执行器随着变化热加载,那些允许你忘记关于所有js/css的监视器。npm/bower+plugin 可以替代任务运行器。他们的能力经常交叉因此如果你需要使用 gulp/grunt 在 npm+plugin 之上时存在着不同的潜在影响。但是任务运行器处理复杂任务定义的更好(比如 “在每一个编译打包,从ES6转义成ES5,在所有浏览器仿真器上运行,制作镜像还有通过ftp部署到dropbox”)。browserify 允许为浏览器打包node模块。browserify vs node's require 就像 AMD vs CommonJSgulp/grunt ?Webpack 和 Browerify 做了很多相同的工作,比如用于在一个浏览器环境钟打包你的模块。这个模块就是一个 Node 特征,它不在浏览器中存在,并且 ES6 中的 import 还没有在任何浏览器中实现,这就是为什么需要被打包。但是,他们在很多方式上是有区别的,Webpack 默认提供很多工具(比如代码拆分),Browerify 只能在下载插件之后才能做这些,但是使用这两种都能实现相似的效果。它取决于个人习惯(我常使用 Webpack )。Webpack 不是一个任务运行器,它只是你通过CLI或任务运行器直接运行文件的一个中间过程器。
Webpack-dev-server 提供类似于 Browser-sync - 它是一个你可以将你的 app 部署的服务,并且验证你的前端开发进程直接通过 dev-server 自动刷新浏览器或者在没有热部署的情况下传播变化(比如 React components)。
我为了项目的完整和简单的任务编写已经使用Gulp,但是我后来发现我既不需要Gulp也不需要Grunt。所有我需要的都可以使用npm组合脚本去运行第三方工具利用它们的 API 完成。在Gulp,Grunt和npm script之间选择取决于你的需要、JS 经验和你工作时的开发经验。
当然Gulp中的任务是易读的,甚至与JS不是很相似,它还是引用和学习另一个工具,并且我个人倾向于缩小我的依赖并且保持简单。另外一面,使用npm组合脚本和运行文件(配置和执行Webpack文件中函数)替代这些任务是更具有挑战性的。但是重要的是他们三个的结果是相同的。
举例说我建议你看一看 react starter project ,它可以向你很好的展示npm组合脚本,Webpack和browser-sync。即使你可以处理你的源文件,如果你愿意,你可以使用Gulp或者Grunt运行你的开发服务,我更喜欢第一个选项。