BeanLee Blog 豆子先森的部落格 2021-01-12T07:51:56.389Z https://beanlee.github.io/ Bean Lee Hexo 【译】前端性能优化检查表 2020 https://beanlee.github.io/posts/fe-perf-checklist-2020/ 2021-01-11T07:17:46.000Z 2021-01-12T07:51:56.389Z 原文地址 https://www.smashingmagazine.com/2020/01/front-end-performance-checklist-2020-pdf-pages

简单对一篇完整的检查表目录做一个翻译,极力推荐阅读原文,文中也提到了大量的有关性能优化策略和方法的关键字和链接,方便查询。

准备计划和指标

  • 建立性能文化氛围(让更多人意识到性能的重要性)
  • 目标:比最快的竞争对手至少快 20%
  • 选择正确的指标(选择正确的指标,不是所有指标都重要)
  • 在能代表大多数用户群体的设备上收集数据(保证准确地数据收集)
  • 为测试环境准备“干净的”和“符合真实用户的”相关的配置
  • 与身边人分享性能指标(确保团队每个人都认可)

设定实际目标

  • 响应时间在 100ms,60fps
  • 3G 网络下的 FID<100ms,TTI<5​​s,速度索引<3s,关键文件大小预算<170KB(已压缩)

定义环境

  • 选择并配置好项目中的构建工具
  • 默认使用渐进增强
  • 选择良好的性能基准
  • 仔细评估框架和依赖(不是每个项目或者页面都需要笨重的框架和大量的依赖库)
  • 考虑使用 PRPL 模式和应用程序外壳体系的结构
  • 检查 API 是否存在优化空间(比如 GraghQL)
  • 框架选择,Google AMP 还是 Facebook 的 Instant Articles?
  • 合理运用 CDN

资源优化

  • 利用 Brotli 进行纯文本压缩(你可能总听到 gzip,brotli 是 2015 年 Google 提出的无损压缩的格式)
  • 使用响应式图片以及 webp
  • 图片是否可以再进一步优化?
  • 视频是否可以进行适当优化?
  • 网络字体是否经过优化?(适当删除和裁切可以减小加载体积)

构建优化

  • 设置优先事项
  • 在生产环境中使用原生 JavaScript 模块
  • 合理使用 tree-shaking, scope hoisting, code-splitting
  • 考虑将一些复杂繁重的 js 放在 Web Worker (预加载数据以及 PWA)
  • 考虑将一些复杂的计算逻辑放在 WebAssembly 中
  • 是否正在使用提前编译器?
  • 对于旧版浏览器仅提供旧版代码
  • 对于 JavaScript 使用模块还是非模块模式?
  • 通过增量解耦识别并重写旧代码
  • 识别并且删除没有用到的代码(代码覆盖率,Chrome 工具)
  • 缩减 JavaScript Bundle 大小
  • 对于 JavaScript Chunks 是否使用预测性的预读取(prefetch)
  • 针对 JavaScript 引擎有针对性的优化
  • CSR 还是 SSR?都要!
  • 使用依靠自建的 lib 资源库(安全、可控)
  • 限制第三方脚本影响
  • 设置正确的 HTTP 缓存头(检查 expires,max-age,cache-control)

Delivery Optimizations

  • 是否异步加载了所有的异步库?
  • 使用 IntersectionObserver 和优先级提示来延迟加载体积大的组件模块
  • 逐步加载图像(渐进式图像加载,由模糊到清晰)
  • 优先加载基础且重要的 CSS 资源
  • 尝试重新组合 CSS 规则
  • 是否对信息流有响应?
  • 考虑使你的组件具有连接意识(公用和复用数据)
  • 考虑使你的组件设备了解内存占用
  • 利用 dns-prefetch 加快交付速度
  • 善用 service workers 来缓存和网络容灾
  • 是否正在 CDN/Edge 上使用 service worker,比如 A/B Test?
  • 优化渲染性能
  • 是否优化了渲染体验
  • 是否有效阻止了重排和重绘?

网络和 HTTP/2

  • 是否开启 OCSP stapling
  • 是否已采用 IPV6
  • 确保所有资源请求都是经过 HTTP/2
  • 正确部署 HTTP/2
  • 确保服务器和 CDN 都已支持 HTTP/2
  • 支持 QUIC 的 HTTP(HTTP/3)
  • 是否采用 HPACK 压缩?
  • 确保服务器安全性(HTTPS 等)

测试与监控

  • 是否有话了审计工作流程(加强 CI 自动化)
  • 是否在代理浏览器和旧版浏览器上进行测试?
  • 是否测试了对可访问性的影响?
  • 是否设置了持续监控
]]>
<p>原文地址 <a href="https://www.smashingmagazine.com/2020/01/front-end-performance-checklist-2020-pdf-pages" target="_blank" rel="noopener">https://www.smashingmagazine.com/2020/01/front-end-performance-checklist-2020-pdf-pages</a></p> <p>简单对一篇完整的检查表目录做一个翻译,极力推荐阅读原文,文中也提到了大量的有关性能优化策略和方法的关键字和链接,方便查询。</p>
有关 SourceMap 的几点个人思考 https://beanlee.github.io/posts/think-about-sourcemap/ 2020-11-18T10:48:37.000Z 2021-01-12T07:50:07.032Z

这两天在组里协助排查一个 webpack dev 环境下重载时内存溢出的问题,现象是 webpack-dev-server 启动的过程中,在 node 10.xx 的环境下,一旦修改文件工程自动 reload 时会出现内存溢出导致服务直接中断。

排查原因,最终发现原来是脚手架在配置开发环境的 devtool 配置时,为了方便开发者 DEBUG,设置成了 cheap-module-source-map ,在代码修改时触发自动 load,在 loader 编译映射 sourcemap 的时候溢~出~了~,本文不会说明为什么溢出,排查的过程以及解决,其实花的时间也不长,重点说明一下 sourcemap 的作用以及使用过程的几点个人思考。

异常

1
2
3
4
5
6
7
8
9
10
11
12
13
[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

<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x1673f099e6c1 <JSObject>
0: builtin exit frame: stringify(this=0x1673f09919f9 <Object map = 0x1673955842a9>,0x1673fa7826f1 <undefined>,0x1673fa7826f1 <undefined>,0x1673500ffcc9 <Object map = 0x167345daabb1>,0x1673f09919f9 <Object map = 0x1673955842a9>)

1: arguments adaptor frame: 1->3
2: /* anonymous */(aka /* anonymous */) [0x1673e6f2d6a1] [/Users/xxx/node_modules/webpack/lib/SourceMap...

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

好了,上面的报错信息看过就可以忘记了。

SourceMap 在 WEB 开发过程中扮演着很重要的角色,因为被浏览器执行的代码往往与开发过程中原始代码相差很多,源码经过了很多次转译、合并、混淆、压缩等等步骤几乎不具有可读性,那么开发中调试、监控到异常定位快速与本地的源码映射匹配就成了它的唯一“使命”。

  • HTTP Headers 支持 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.附赠一篇昨天无意中看到的总结,写的实在挺好的,和业界大佬还是有很大差距,加油吧。

我在阿里云做前端 https://juejin.im/post/6844903808904986637

参考文章:

]]>
<!-- ## 有关 SourceMap 的几点个人思考 --> <p>这两天在组里协助排查一个 webpack dev 环境下重载时内存溢出的问题,现象是 webpack-dev-server 启动的过程中,在 <code>node 10.xx</code> 的环境下,一旦修改文件工程自动 reload 时会出现内存溢出导致服务直接中断。</p> <p>排查原因,最终发现原来是脚手架在配置开发环境的 <code>devtool</code> 配置时,为了方便开发者 DEBUG,设置成了 <code>cheap-module-source-map</code> ,在代码修改时触发自动 load,在 loader 编译映射 sourcemap 的时候<strong>溢~出~了~</strong>,本文不会说明为什么溢出,排查的过程以及解决,其实花的时间也不长,重点说明一下 sourcemap 的作用以及使用过程的几点个人思考。</p>
UNIX 编程艺术 读书笔记 1 https://beanlee.github.io/posts/note-about-unix-art-1/ 2020-11-17T11:02:20.000Z 2020-11-18T13:22:42.000Z

以下规则很受用,不分语言,甚至有一些规则可以作为日常做事的原则,日后逐条对应写一些个人的理解。

有关 UNIX 编程的规则(摘自原文):

  1. 模块原则:使用简洁的接口拼合简单的部件。
  2. 清晰原则:清晰胜于机巧。
  3. 组合原则:设计时考虑拼接组合。
  4. 分离原则:策略同机制分离,接口同引擎分离。
  5. 简洁原则:设计要简洁,复杂度能低则低。
  6. 吝啬原则:除非确无它法,不要编写庞大的程序。
  7. 透明性原则:设计要可见,以便审查和调试。
  8. 健壮原则:健壮源于透明与简洁。
  9. 表示原则:把知识叠入数据以求逻辑质朴而健壮。
  10. 通俗原则:接口设计避免标新立异。
  11. 缄默原则:如果一个程序没什么好说的,就沉默。
  12. 补救原则:出现异常时,马上退出并给出足够错误信息。
  13. 经济原则:宁花机器一分,不花程序员一秒。
  14. 生成原则:避免手工 hack,尽量编写程序去生成程序。
  15. 优化原则:雕琢前先要有原型,跑之前先学会走。
  16. 多样原则:决不相信所谓“不二法门”的断言。
  17. 扩展原则:设计着眼未来,未来总比预想来得快。
]]>
<!-- ## 《UNIX 编程艺术》读书笔记 1 --> <p>以下规则很受用,不分语言,甚至有一些规则可以作为日常做事的原则,日后逐条对应写一些个人的理解。</p> <p>有关 UNIX 编程的规则(摘自原文):</p>
中后台前端搭建经验总结-技术细节篇(一) https://beanlee.github.io/posts/think-about-fe-of-backsystem/ 2020-10-20T12:18:13.000Z 2020-10-20T14:00:07.000Z

本篇是总结一个旧项目,项目在 2019 年 Q3 开发并上线运营,经历两个月迭代后目前已交接给其他团队维护,整理个人草稿箱发现这边还没有完成的总结,补充一些内容更新至此,内容应该是顺着回想起细节,后面再补充。

关键词:中后台系统、从零到壹

项目背景

交易指挥中心是中台基础基础组件化向智能基础组件化升级的战略项目,同时驱动中台核心业务中心的平台化搭建。一期建成优惠监控、库存、订单、商品四大指挥中心系统以及门户,实现中台系统的从零到一的系统建设。

对于我们的前端团队意义在于,积累面向 B 端中后台系统开发经验,沉淀中后组件,配合中后台系统可视化构建平台完成。

技术选型

  • React + TypeScript + MobX
  • 构建工具 jdwcli(webpack + koa dev server)
  • UI(LEGAO React + 部分 Ant Design)
  • 图表框架 HightCharts

部分方案设计

菜单路由

由于中后台系统,页面功能及数据信息需要一定的访问权限,除了在数据返回之前控制以外,前端展示的权限菜单控制就很重要。

权限控制与菜单

动态菜单及路由

中后台系统其页面功能及数据信息比较敏感,在面向 C 端用户的基础上,只要控制当前基于数据安全方面的考虑,页面的访问控制非常重要。首先,在服务端接口数据保证权限校验的前提下,用户在前台访问页面所看到的菜单也需要进行权限控制。

出于上面的考虑,针对当前系统的菜单设计时可以前端架构采用动态菜单设计。用户有权限访问到的菜单通过权限接口返回,再由前端渲染。(这里没有将权限限制逻辑放在前台有两点考虑:1、HTTP 接口容易被抓去伪造,非权限菜单容易暴露;2、当前系统是基于 ERP 等方式单点登录,集成了 ERP 系统的角色控制,前台和后台没有必要做重复的权限判断逻辑;)

笔者曾在一家专注线上协同办公(OA)的软件公司,负责过一段组织模型与菜单角色模块,经历过基于 RBAC 模式的控制权限改造,从数据库设计、服务开发以及到前台实现。

前端这里要做的就是,需要同时支持两套方案

  • 第一类:前端固定路由信息(针对不敏感菜单)
  • 第二类:通过数据接口按照权限获取相关路由

上面提到的两种方案的实现,遵循统一的一套数据结构就可以实现。

实现上面的设计,依赖于 jdwcli 创建的项目模板中页面工厂 PageFactory这个类,即采用工厂模式将菜单数据缓存,通过 React.lazy 实现动态引用,这样即达到分包的特点减小初始包的大小,又可以实现菜单控制。

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
[
{
"key": "home",
"page": "home",
"path": "/home",
"show": true,
"title": "首页"
},
{
"key": "xxxyyy",
"page": "xxxyyy",
"path": "/xxxyyy",
"show": true,
"title": "XXXYYY一级菜单",
"children": [
{
"key": "yyy",
"page": "yyy",
"path": "/yyy",
"show": true,
"title": "yyy系统介绍"
},
{
"iframe": true,
"iframesrc": "//xxx.yyy.com",
"key": "xxxIndex",
"page": "xxxIndex",
"path": "/xxxIndex",
"show": true,
"title": "xxx首页"
}
]
}
]
迁移旧系统和页面

零售中台经过多年的沉淀,内部有很多的功能系统存在,在开发初期就基本确认无法通过段时间内一一重新开发实现,迁移旧页面这个功能也需要被考虑在内。

目前比较合适的方案有两种:

  1. 采用 IFRAME 内嵌;
  2. 通过菜单或链接跳转离开;

那么为了满足这两种方案,在前面设计菜单的同时,在数据结构中就要增加针对内嵌和跳转离开的标示,考虑前端路由当前页面是一个 IFRAME 内嵌系统,还是一个需要点击跳转离开的菜单。

完成这点以后,要做的就是开发一个统一接受 IFRAME 链接的 Component 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@withRouter
export default class IFrameCont extends React.Component<IFrameContProps> {
render() {
const { iframeSrc } = this.props;
if (!iframeSrc) return null;
return (
<>
<iframe
title={iframeSrc}
src={iframeSrc}
width="100%"
height="100%"
></iframe>
</>
);
}
}

BUT,上面的实现,我们似乎忽略了一个问题,我们只考虑到了用户点击菜单路由跳转过来的页面,由于我们使用的是 HASH 路由,并没有考虑如果用户在当前页面刷新或通过页面 URL 直接访问指定路由时,当前 Component 无法拿到 src 的 props 的。这里我采取的办法是:同时将 Store 中菜单信息监听到当前组件中,与当前页面 URL 中的 pathname 进行一次匹配。

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
+ @inject((stores: GlobalStores) => ({
+ menu: stores[PAGE_STORE].menu
+ }))
+ @observer
@withRouter
export default class IFrameCont extends React.Component<IFrameContProps> {
render() {
let iframeSrc = "";
+ if (!this.props.location.state) {
+ try {
+ iframeSrc = Array.from(this.getAllMenus(this.props.menu)).filter(menuItem => {
+ return menuItem.path === this.props.location.pathname;
+ })[0].iframesrc;
+ } catch (error) {
+ console.error("error iframe src");
+ return null;
+ }
+ } else {
+ const { menu } = this.props.location.state;
+ iframeSrc = menu.iframesrc;
+ }

if (!iframeSrc) {
return null;
}
return (
<>
<iframe title={`${Math.random()}`} src={iframeSrc} width="100%" height="100%"></iframe>
</>
);
}

+ private getAllMenus(pagedata: PageStore.Domain.PageType[], map?: Set<PageType>) {
+ return pagedata.reduce(
+ (prev, next) => {
+ prev.add(next);
+ if (next.children && next.children.length > 0) {
+ this.getAllMenus(next.children, prev);
+ }
+ return prev;
+ },
+ map ? map : new Set<PageType>()
+ );
+ }
}

数据可视化

中后台系统中,许多数据需要具有更丰富的展现形式,那么图与表的结合就是比较好的方案。

本项目中图采用 HighCharts 作为图库,考虑有两个原因:

  1. 有以往使用的经验,项目紧急时期可以快速实现,并且公司内拥有采购 HighCharts 的授权(Wiki 中翻到了历史采购清单文件);
  2. 对比同类型 BizCharts、ECharts、Chartjs、D3.js 等其它图表库,HighCharts 具备官方文档丰富且详细、DEMO 实例丰富、图表种类多,页面渲染依靠 SVG 效率较高、拥有官方支持 React 版本(highcharts-react-official)、可快速产品原型中的图例、行业老大位置等特点;

这是一篇迟来的总结文章,其实如果时间允许,可以选择的方案还有很多,今天还刚刚看到 Airbnb 团队公布开源了一个新的可视化组件库。https://airbnb.io/visx/

内部实现的有 双轴图、堆叠图、百分比堆叠图、饼图、坐标系图(散点图的延伸)实现细节以及部分问题总结详见另外一篇撰写中总结。

表单实现

对于中后台,健壮表单功能应该是必不可少的一环,由于当前项目都是比较简单的表单,目前的项目经验,大致分为两种:

  • 查询,带有复杂条件的关联查询,后面跟随查询结果
  • 传统 Form 提交,用户填充数据提交

这里提一个问题:当你看到 Form 表单会联想到什么?下面是目前我能想到的内容:

  • 输入框校验问题(正则、服务端校验、输入转义防止注入攻击)
  • 当前表单的状态保存,以及重置(关于 Store 的控制)
  • 交互体验(输入和可选框之间的联动,提交重置按钮出现的位置等等)
  • 表单的某一条或某些条目需要作为动态内容可以添加、删除和修改

那么这些表单能否有通过可视化拖拽,自动生成吗?答案是肯定的,同组其他的同事就在调研实现这个问题,找机会深入讨论一下。

性能优化点

级联组件性能

优惠指挥中心系统中价格力系数查询页面,品牌级联,级联菜单由于存在一个 3K 左右个的数据由后端一次性返回,需要前端在前台把数据组装成树接口分级,再传递到级联组件中,这对于浏览器中内存计算耗时,以及组件初始化大量数据的性能都造成很大影响,在开发初期没有发现,Mock 数据只有几十个。

解决方案:就要和后端商量将查询接口调整为分级查询,逐级进行查询,避免一次数量过大。这里还可以在继续深入优化的点,就是当前二级或三级级联数据被 load 过一次以后,前端缓存在当前页面内,鼠标划回父级数据时直接拿缓存数据。(当然,缓存永远是一把双刃剑,要考虑缓存什么时间失效,什么时间生效就要具体问题具体分析。)

缓存

又是一个经常被讨论到的问题,各种缓存策略,网络上可以找到很多优秀详细的文章,这里简单介绍一下本项目使用的一些缓存策略。

  1. 将工具、UI 库等比如 React、React-DOM、MobX,HighCharts 等,按需在其页面内静态引入,并且提前上线到 CDN ,将固定引用链接利用客户端缓存,不在更新;
  2. 页面 Nginx 响应头设置缓存时间;
  3. 配合后端检查,固定类变化特别小的数据请求,可以适当增大缓存时间,同时缓存到客户端本地;

开发环境与生产环境隔离

环境的隔离,应该是工程化中比较简单且常见的问题,在从零搭建系统过程中,也不免存在这个问题,我自己的解决方案是,利用头尾系统(内部系统)建立两套文件,分别对应预发测试和正式环境。预发测试环境需要经常更新,缓存时间设置也非常短,且请求的也是预发测试接口,这在上线前需要及时更换。

这点在项目构建打包时,可以根据设置环境来进行操作。

比如:利用 Babel 编译中移除项目内冗余的 console.log,线上仅保留 error 或者其他,那就可以在项目中的 .babelrc 进行如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"env": {
"dev": {
"plugins": [
[
"transform-remove-console",
{ "exclude": ["log", "debug", "error", "warn"] }
]
]
},
"prod": {
"plugins": [["transform-remove-console", { "exclude": ["error"] }]]
}
}
}

这样在 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 组件库来的更有意义。

]]>
<!-- ## 中后台前端搭建经验总结-技术细节篇(一)--> <blockquote> <p>本篇是总结一个旧项目,项目在 2019 年 Q3 开发并上线运营,经历两个月迭代后目前已交接给其他团队维护,整理个人草稿箱发现这边还没有完成的总结,补充一些内容更新至此,内容应该是顺着回想起细节,后面再补充。</p> </blockquote> <p>关键词:中后台系统、从零到壹</p>
单品页手势实现记录 https://beanlee.github.io/posts/page-with-gesture/ 2020-09-29T14:39:47.000Z 2020-10-20T12:20:05.000Z 撰写中,先占个坑以免自己忘了,主要记录仿抖音交互式 H5 页手势过程实现

]]>
<p>撰写中,先占个坑以免自己忘了,主要记录仿抖音交互式 H5 页手势过程实现</p>
Redis 入门指南(第 2 版)读书笔记 https://beanlee.github.io/posts/getting-started-about-redis/ 2020-09-22T05:55:28.000Z 2020-09-22T15:31:07.000Z

读书笔记《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
2
3
4
wget http://download.redis.io/redisstable.tar
tar xzf redis-stable.tar.gz
cd redis-stable
make

默认端口:6379

文件名说明
redis-server服务
redis-cli命令行客户端
redis-benchmark性能测试工具
redis-check-aofAOF 文件修复工具
redis-check-dumpRDB 文件检查工具
redis-sentinelSentinel 服务器(v2.8 后)

停止服务,Redis 会先断开客户端连接,然后根据配置持久化数据,最后退出。

启动时可以自定义配置 redis-server path/to/redis.conf

多数据库支持

Redis 默认支持 16 个数据库,不可以自定义数据库名,只能根据编号命名,默认从 0 开始。

不支持为每一个数据库设置密码,一个客户端要么能访问所有库,要么没权限访问。

数据类型

Redis 与 MySQL 等关系型数据库以二维表形式的存储有非常大的差异,Redis 是 NoSQL 中一员,采用 key - value 方式存储,它的键值支持存储的类型有下面集中类型:

  • 字符串类型
  • 散列类型
  • 列表类型
  • 集合类型
  • 有序集合类型

字符串类型

key 和 value 都是字符串

SETGET

提示 Redis 对于键的命名并没有强制的要求,但比较好的实践是用“对象类型:对象 ID:对象属性”来命名一个键,如使用键 user:1:friends 来存储 ID 为 1 的用户的好友列表。对于多个单词则推荐使用“.”分隔,一方面是沿用以前的习惯(Redis 以前版本的键名不能包含空格等特殊字符),另一方面是在 rediscli 中容易输入,无需使用双引号包裹。另外为了日后维护方便,键的命名一定要有意义,如 u:1:f 的可读性显然不如 user:1:friends 好(虽然采用较短的名称可以节省存储空间,但由于键值的长度往往远远大于键名的长度,所以这部分的节省大部分情况下并不如可读性来得重要)。

散列类型

Hash,每一个键存储字段不同。

1
2
3
4
5
HSET key field value
HGET key
HMSET key field value [field value ...] # 存储某一个key,某一个或多个字段的 value
HMGET key field [field ...] # 获取某一key的某一个或多个value
HGETALL key # 获取某一 key 的所有value

自由地为任何键增减字段而不影响其他键。

列表类型

KEYS 命令需要遍历数据库中所有键,出于性能考虑,一般很少在生产环境使用。

List 可以存储一个有序的字符串列表,内部是 双向链表 实现的,所以在列表两端增加元素时间负责度都是 O(1),获取越接近两端的元素速度越快。链表的缺点是通过索引来访问某一个元素慢。

这种特性使列表类型能非常快速地完成关系数据库难以应付的场景:如社交网站的新鲜事,我们关心的只是最新的内容,使用列表类型存储,即使新鲜事的总数达到几千万个,获取其中最新的 100 条数据也是极快的。同样因为在两端插入记录的时间复杂度是 O(1),列表类型也适合用来记录日志,可以保证加入新日志的速度不会受到已有日志数量的影响。

1
2
3
4
5
6
7
8
9
LPUSH key value [value ...] # 向列表左边追加
RPUSH key value [value ...] # 向列表右边追加

LPOP key # 从列表左边弹出一个元素
RPOP key # 从列表右边弹出一个元素

LLEN key # 获取列表元素数量

LRANGE key start stop # 获取列表某一段,**!!常用!!**

集合类型

集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型在 Redis 内部是使用值为空的散列表(hashtable)实现的,所以这些操作的时间复杂度都是 O(1)。最方便的是多个集合类型键之间还可以进行并集、交集和差集运算…

1
2
3
4
5
6
7
8
9
SADD key member [member] # 添加
SREM key member [member] # 删除

SMEMBERS key # 获取key集合中所有元素
SISMEMBER key member # 判断某member元素是否在key集合中 **!!效率极高,O(1)!!**

SDIFF key [key ,,] # 集合差集
SINTER key [key ,,] # 集合交集
SUNION key [key ,,] # 集合并集

有序集合

区别于列表类型:

(1)列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会较慢,所以它更加适合实现如“新鲜事”或“日志”这样很少访问中间元素的应用。(2)有序集合类型是使用散列表和跳跃表(Skiplist)实现的,所以即使读取位于中间部分的数据速度也很快(时间复杂度是 O(log(N)))。(3)列表中不能简单地调整某个元素的位置,但是有序集合可以(通过更改这个元素的分数)。(4)有序集合要比列表类型更耗费内存。

1
2
3
4
5
6
7
8
ZADD key score member [score member ...] # 增加元素
ZSCORE key member # 获取

ZRANGE key start stop [WITHSCORES] # 获取有序的某一段(从小到大)
ZREVRANGE key start stop [WITHSCORES] # 获取有序的某一段(从大到小)


ZRANGEBYSCORE key min max [WITHSCORES] [LIMIToffsetcount] # 按照元素从小到大顺序返回 min 和 max 之间的元素

事务

关系型 SQL 数据库一个非常大的特点就是事务的特性,同样也是 Redis 命令的最小的执行单元,一个事务要么执行,要么不执行。(关系型事务,多个表操作,同一个事务内要么都成功,要么都失败。)

1
2
3
MULTI
# xxx 命令
EXEC

Redis 事务可以保证一个事务内的命令依次执行而不被其他命令插入。

Redis 事务的异常处理,首先需要先明确什么原因导致执行出错。1)语法错,一旦前面有错,后面不会执行;2)运行错,一旦有错,后续的命令会继续执行;

Redis 的事务没有关系数据库事务提供的回滚(rollback)[1]功能。为此开发者必须在事务执行出错后自己收拾剩下的摊子(将数据库复原回事务执行前的状态等)。

WATCH 命令,WATCH 命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。

过期时间

1
2
3
EXPIRE key seconds # 单位是秒,过期时间到了以后Redis自动删除key
PEXPIRE key ms # 单位毫秒
TTL key # 剩余到期时间,单位是秒

排序:SORT 命令,BY 参数,GET 参数,STORE 参数

消息通知,任务队列

生产/消费者模式,分发消息以及任务队列的实现,可以借助 Redis。

使用 Redis 实现简单的任务队列,一边 LPUSH,一边 RPOP 即可。(BRPOP 命令)

优先级队列,BRPOP key [key …] timeout

发布/订阅模式

1
2
PUBLISH channel message
SUBSCRIBE channel

存在规则订阅,PSUBSCRIBE 命令。

节省空间方法:1)精简键名和键值;2)内部编码优化

脚本

Lua 语言(Open Rest,Nginx 也可以使用 lua 语言,有机会学习了解一下)

这里不详细记录 Lua 语法,不过有一点思考,既然 Nginx 也可以使用 Lua,那么可以就有一种场景,Nginx 通过 lua 访问 Redis 读取数据,并且用 lua 渲染模板,达到页面直出,这样应该效率很高。

持久化

  • RDB (通过快照完成,当达到某种约定条件后自动生成一份备份并存储在硬盘上),快照原理
  • AOF (存储非临时数据,每执行一条都会追加存储在硬盘,有一些性能影响)默认关闭,通过 appendonly yes 开启

允许同时开启 RDB 和 AOF 两种模式。

集群

Redis 支持集群,可以通过主从数据库来来规避单点数据库故障导致的问题。主数据库负责读写(读写分离也可以),当写操作导致数据变化时自动将数据同步给从库,从库只读,并只接受主库同步数据。

配置文件,通过 slaveof 主库地址 主库端口 来完成主从复制的配置。

通过复制可以实现读写分离,以提高服务器负载能力。

关键字记录,Redis 支持哨兵,一主多从,需要自动监控 Redis 运行情况,作用:1)监控主从数据库运行正常;2)主数据库故障自动将从数据库转换成主数据库;细节待补充一篇琢磨透彻一点的分析文。

Redis 3.0 支持集群一大特点,还在学习中,这里不做细描述。(作者自己还一知半解,需要点时间和资料消化消化)

Redis 管理(偏 OPS 运维)

默认 Redis 允许所有连接,可以通过 bind 绑定某一地址,可以设置数据库密码,提升安全性。

将命令重命名,确保部分影响性能或整个数据库的命令,被自定义成一个新命令。

结语

个人拙见:

  1. 在速读的过程中,有几个想法,数据层面凭我个人经验来讲,可以分为热数据和冷数据,那么,其实无论是 Redis 这样 NoSQL 还是与 MySQL 这样各有专长的关系型数据库,可以组合来实现对前台数据的持续服务,冷数据来源于关系型,热数据来源于冷数据,定时按照 一定策略 进行更新与同步,这样既可以保证数据存储,也可以保证读写的高效
]]>
<!-- ## Redis 入门指南(第 2 版) 读书笔记 --> <blockquote> <p>读书笔记《Redis 入门指南》 李子骅. Redis 入门指南(第 2 版)(异步图书) (Chinese Edition) (Kindle Locations 627-632). 人民邮电出版社. Kindle Edition.</p> </blockquote> <p>前阵子部门团建要去外地,路上大巴车来回有将近 6、7 个小时的空闲时间,排除和身边人玩 Switch 的时间,大部分时间都用来速读一本纯技术类工具书,《Redis 入门指南(第 2 版)》,粗略自己摘录并且记录一些读书笔记,记录于此,以备自己方便查阅。内容可以较多但仅限个人记录一定有所疏漏,还是建议使用到 Redis 的开发者朋友,使用 Dash 或者其他查看 API 一类工具放手边随时查阅。</p>
基于 Storybook 5 打造 Style 组件库开发与文档站建设小结 https://beanlee.github.io/posts/about-storybook-5/ 2020-07-08T10:11:14.000Z 2020-07-09T09:15:34.000Z

写在前头

前阵子一直和组里的小伙伴共同“造轮子”,开发并维护了一套 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 几点优势,也是基于下面几个优势最终决定选型它的原因

  • 开发环境 预览环境整体基于 Webpack 构建,开发环境接近实际生产环境
  • 多面手 支持技术栈类型较多,可以支持 React、Vue 等等技术栈组件展示;
  • 代码即文档 无论是开发初期我们使用的 *.stories.js,还是后期因为要统一文档站说明和一些 UI,我们改用 MDX 重写了使用说明,不过大部分 API 列表还是基于 Props 的实现定义 Interface 中 JSDoc 自动渲染,非常方便;
  • 实时性 展示环境实时可交互,通过 knob 插件可以让使用者直接修改组件属性直接看到效果;

支持 TypeScript

官方文档,支持 TypeScript 编写组件,文档中罗列了几个选项,出于使用习惯以及 Babel 与 Microsoft 的合作关系,推荐使用 babel-loader 方式。(虽然它很慢,所以要尽量控制编译的范围。)

Tips

当使用 babel 编译 TypeScript 的时候,存在两个问题

  • 描述文件 d.ts 组件的描述文件如何生成,babel-loader 本身不具备这个能力。
  • BUG 文档站生成过程中,默认 storybook 配置了 devtool,导致即使当前环境是 production 依然会编译生成 sourcemap,也就需要手动将 devtool 改为 false,这里应该给 storybook 提一个 issue。文档见这里,有一句说明:注意:sourceMap 选项是被忽略的。当 webpack 配置了 sourceMap 时(通过 devtool 配置选项),将会自动生成 sourceMap。

关于声明文件的生成有两个方案:

  • 手动通过 tsc 本身具备的能力,结合 --emitDeclarationOnly参数和输出目录来只输出 *.d.ts
  • 后期我们重构组件库打包方式,组内的小伙伴使用 gulp 一样的道理通过单独编译声明文件并输出到与组件代码同级目录

上面的方案核心就是单独编译声明文件即可,即便是微软官方提供的例子也是单独输出声明文件,详见这里,如果小伙们有其他好办法欢迎留言讨论。

文档站构建优化

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 按照期望的方式进行排序。

这里简单描述一下个人实现思路:

  1. 数据准备阶段:将业务方期望的菜单分类好,我的做法是将期望菜单顺序拷贝至 Excel 中进行排序,之后复制回编辑器批量操作,制作一个简单 map 作为字典,作用单一就是根据 key 返回序号;
  2. 获取组件待排序属性:将每一个组件的 stories 中定义的 title 拿到用作排序时,作为 key 备用在字段中查询;
  3. 编写排序规则:这里就是一个简单的排序算法,两两比较即可;
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
// preview.js
...
storySort: (a, b) => {
/**
* 排序逻辑
* 将文档内容中所有内容去除中文、改小写,去空格,再去已经排序好的数据处理
* ex. 按钮 Button => button
*
* 排序规则 0: 相同位置 -1: 前 1: 后
*/
const aSinpleName = a[1].kind
.replace(/[\u4e00-\u9fa5\/]/g, "")
.toLowerCase()
.trim();
const bSinpleName = b[1].kind
.replace(/[\u4e00-\u9fa5\/]/g, "")
.toLowerCase()
.trim();
const aSortNum = getSortNumByMenu(aSinpleName); // 获取字典中排序编号
const bSortNum = getSortNumByMenu(bSinpleName);
// 自有组件,直接排在最后。PS. 非官方定义组件,二等公民无奈被放在最后
if (!aSortNum && bSortNum) return 1;
if (aSortNum && !bSortNum) return -1;
if (!aSortNum && !bSortNum) return 0;
return aSortNum > bSortNum ? 1 : -1;
},
...

addon-docs 插件

Storybook 中文档即代码功能的实现还是很有意思的,通过强大的 @storybook/addon-docs 插件(在 5.3.x 版本开始逐步废弃 @storybook/addon-info 插件,基本不再维护)可以实现文档中众多元素的直接渲染,我们经常用的组件有 import { Meta, Story, Preview, Props, Source, Title, Subtitle } from "@storybook/addon-docs/blocks"; 这些,其中着重介绍 PreviewProps 组件。

Props 组件

可以结合 JSDoc 自动生成文档,如下图:

感兴趣的同学可以抽时间研读了一下具体实现,源码地址

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

现存问题记录

  • 在暗黑模式的适配上还存在问题
  • 演示代码段,主题不可定制(查看源码知道虽然使用了 highlight 组件但是固定死了主题,并没有入口去修改)
  • 国际化支持不全,目前没有需求可以暂时不考虑

走过的弯路

插件选择失误,由于最一开始的选择错误,错误地选用 addon-info 已经被废弃的插件,导致后面升级后,在静态文档站需要定制化的需求出现时,原有 addon-info 出现的问题层出不穷,不得不切换至 addon-doc 插件,索性直接使用 mdx 对所有组件文档进行了一次重写,好在大部分代码和 DEMO 可以直接复用和自动生成。

写在最后

当然,我们目前并没有使用 Storybook 的全部功能,文章的最后也会列举调研期间收集到的非常牛团队开发的组件库和文档实例,那么我们可以学习到什么呢?

首先,我们所谓的重复造出来的轮子到底是有意义?答案虽然是肯定的,业界已经有很多成熟的案例,很多公司、部门、团队都可能有自己的库,早在我们在开始开发实现之初,但是我们为什么要做呢?这就是另外一个话题。我们不深入这个话题,仅从过程中讨论,我认为不仅仅只考虑组件的设计、易用性(API 等)、稳定性(质量)、组件的抽象、以及复用性等等这些问题,同时也应该作为组件使用者的角度来思考,从适用场景出发,当使用者决定是否使用某一组件时,组件的文档等等就是一个非常重要支持点,这里刚好 Storybook 帮我们解决了这其中的几个问题,看到这里感兴趣的小伙伴可以留言讨论,当你的团队或者新项目在选择 UI 库时基于哪些考虑?

再者,聚焦,Storybook 团队聚焦在组件的开发环境,单一功能强大通过拼接实现更多的功能,这种思想一定要多运用在实际情况中,但是也要注意区分场景,没必要成为“为了用而用”。

最后,UI 组件库应该是前端团队,最容易想到也是最难做好的一个 KPI 产物,这其中还有很多值得思考和讨论!

基于 Storybook 优秀实例

参考

]]>
<!-- ## 基于 Storybook 5 打造 Style 组件库开发与文档站建设小结 --> <h3 id="写在前头"><a href="#写在前头" class="headerlink" title="写在前头"></a>写在前头</h3><p>前阵子一直和组里的小伙伴共同“造轮子”,开发并维护了一套 PC 端 React UI 组件库,经过了一段时间的折腾,组件库从之前的 0.x、1.x 再到最近发布的 2.0.beta 的一个过程,这其中很多东西值得拿出来分享和讨论,有好的有失败的,今天就把组件库开发过程中的 DEMO 实时重现以及后期文档站的建设的技术选型以及实践简单做一个记录总结和大家一起做一个讨论。</p> <p>调研和选型具体细节,后面找时间再梳理输出另外一篇文章,这里介绍在使用 <code>Storybook 5</code> 的过程中的一些问题点以及经验分享。</p> <blockquote> <p>截止文章编写时 Storybook 6 正在进行 rc 版,作者也经历过将之前项目的 <code>5.2.x</code> 升级到 <code>5.3.x</code> 过程的阵痛,网络上关于使用 Storybook 的问题文章也比较少,除了 Storybook 官网文档以外一些问题点记录,因此成文,感兴趣可以继续阅读。有关 UI 组件库的建设,这里不做深入讨论。(就是我们暂时先不讨论 WHY 的问题,本文只讨论 HOW。)</p> </blockquote>
ESLint 修改点整理 https://beanlee.github.io/posts/sth-about-eslint/ 2020-06-03T13:39:00.000Z 2020-07-05T08:57:39.000Z

ESLint 为团队和项目带来的益处,这里不多说,本文主要是个人近期修改项目代码一些错误修改整理的内容。

备注:尽量使用默认 Prettier 规则,根据实际情况略有调整。项目技术栈 React, Redux, Redux-Sagas, TypeScript。

Plugin 列表

  • "plugin:react/recommended"
  • "plugin:@typescript-eslint/recommended"
  • "prettier/@typescript-eslint"
  • "plugin:prettier/recommended"

ERROR List

  • [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

    Desc Link

    补充提示:

1
2
3
4
Don't use `{}` as a type. `{}` actually means "any non-nullish value".

- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.
  • [Error] react/jsx-no-target-blank

    Desc Link

    出于安全考虑,React 中产生新打开页面的链接,需要增加 rel='noreferrer' 用来保护原站。具体说明详见:Link

WARN List

  • [Warn] Missing return type on function.eslint@typescript-eslint/explicit-module-boundary-types

    desc link
    说明:针对函数的定义,建议每一个函数都要显式的表明函数返回值。这在 *.jsx, *.tsx 文件中,React 生命周期函数都提示,可以使用 eslint overrides 规则,只针对 *.js, *.ts 生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"rules": {
// disable the rule for all files
"@typescript-eslint/explicit-module-boundary-types": "off"
},
"overrides": [
{
// enable the rule specifically for TypeScript files
"files": ["*.js", "*.ts"],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "warn"
}
}
]
}
  • [Warn] @typescript-eslint/no-unused-vars

应该是最经常遇到的一个警告,定义了变量,下文没有使用。

个人建议:非关键算法或逻辑代码,当你阅读时没用,就删掉吧,可以保留注释,因为即使你想保留这段代码,以备不时之需,但是真到需要用到代码你再次阅读的时候,实现思路以及上下文联系可能已经相差很远。

Prettier 细节

  • 关于默认逗号的变化 link

Prettier 自动 v2.0.0 开始,将 trailingComma 默认配置由 none 改为 es5,在我看来是一种很好的方式。

举个例子,当 import 多个内容、一个对象需要增加属性、一个数组追加元素,如果默认已经追加了逗号,那么就可以直接追加,而不必要移动光标去前一行手动增加一个逗号,这样就增加了一些便利性,与此同时便于快速定位甚至避免由于一个逗号的引起的错误。

TS2322 自定义属性

1
2
3
4
5
6
7
8
import { AriaAttributes, DOMAttributes } from "react";

declare module "react" {
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
// extends React's HTMLAttributes
custom?: string;
}
}

参考:https://dev.to/lukethacoder/use-custom-html-attribute-s-in-typescript-2co

TS2679

问题实例片段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function* loadSimilar(skuId: string) {
yield put({
type: ActionTypes.REQUEST_SIMILAR.PENDING,
});
try {
const { data } = yield API.querySimilar(skuId);
yield put({
type: ActionTypes.REQUEST_SIMILAR.SUCCESS,
data,
});
} catch (error) {
yield put({
type: ActionTypes.REQUEST_SIMILAR.FAILURE,
});
}
}

function* watchSimilar() {
yield takeEvery(ActionTypes.LOAD_SIMILAR, loadSimilar);
}

修改后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- function* loadSimilar(skuId: string) {
+ function* loadSimilar({ skuId }: { type: string; skuId: string }) {
yield put({
type: ActionTypes.REQUEST_SIMILAR.PENDING,
});
try {
const { data } = yield API.querySimilar(skuId);
yield put({
type: ActionTypes.REQUEST_SIMILAR.SUCCESS,
data,
});
} catch (error) {
yield put({
type: ActionTypes.REQUEST_SIMILAR.FAILURE,
});
}
}

function* watchSimilar() {
yield takeEvery(ActionTypes.LOAD_SIMILAR, loadSimilar);
}

解答:Redux-Saga 中 takeEvery 第二个参数是一个 action,所以定义 loadSimilar 时候需要遵循 TakeableChannel<unknown> ,定义 type。
参考:https://stackoverflow.com/a/60558041

切记,保证一路传递参数变量匹配,否则会出现无法赋值问题,例如下面代码:

1
2
3
4
5
6
7
8
- function* cartOptCheckOne({ param }: { type: string; param: any }) {
+ function* cartOptCheckOne({
+ // type = ActionTypes.OPT_CARTCHECKONE,
+ RequestParam,
+ }: {
+ type: string;
+ RequestParam: any;
+ }) {

外层触发 action 传参数代码如下:

1
2
3
this.props.optCartCheckOne({
RequestParam: requestParam,
});

如果定义 cartOptCheckOne 使用 param 就会导致传递过程中因为变量名称不同无法解构,导致传参中断。

ESLint Config

参考 https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project

]]>
<!-- ## ESLint 修改点整理 --> <p>ESLint 为团队和项目带来的益处,这里不多说,本文主要是个人近期修改项目代码一些错误修改整理的内容。</p> <blockquote> <p>备注:尽量使用默认 Prettier 规则,根据实际情况略有调整。项目技术栈 React, Redux, Redux-Sagas, TypeScript。</p> </blockquote>
【译】Google 出品 - Understand the JavaScript SEO basics https://beanlee.github.io/posts/understand-the-javascript-seo-basics/ 2019-07-30T14:39:20.000Z 2019-07-31T05:45:55.000Z 理解 JavaSript SEO 基础

原文 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

Googlebot 处理 JavaScript web 应用存在三个主要阶段:

  1. Crawling 爬取
  2. Rendering 渲染呈现
  3. Indexing 建立索引

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。

用唯一的 title 和 snippets 描述网页

唯一的,可描述的标题以及有用的 meta 描述信息可以帮助用户快速地有目的性的定位最佳的搜索结果,我们在指南中解释什么是好的 title 和 description

你也可以使用 JavaScript 设置或者修改 title 和 meta 描述信息。

Google Search 可以基于用户的查询展示不同的标题和描述。当标题或者说明与页面内容相关性较低,或者我们在页面中发现与搜索结果更匹配的替代方法时,就会发生这种情况。有关标题和说明的代码片段的详细信息,请参与此页面。

编写向下兼容的代码

浏览器提供了很多 API 并且 JavaScript 也是一个快速进化的编程语言。Googlebot 在支持哪些 API 和 JavaScript 功能方面有一些限制。为了确保你的代码可以兼容 Googlebot,请遵循我们的 JavaScript 故障指南

如果你发现你需要的 API 缺失,我们建议使用不同的服务和 polyfills。如果一些浏览器特性没有被 polyfill 补丁,我们建议你多看 polyfill 文档,了解潜在的限制。

使用有意义的 HTTP 状态码

在爬取页面时,Googlebot 使用 HTTP 状态码识别页面的一些问题。

你可以使用有意义的状态码告诉 Googlebot 当 前页面是否需要被爬取或者索引,比如 404 代表页面已经失联,401 代表登录前的页面。你可以使用 HTTP 状态码告诉 Googlebot 这个页面被转移到一个新的 URL ,因此索引可以被及时更新。

下面是 HTTP 的状态码列表

HTTP statusWhen to use
301/302当前页面已经跳转到新 URL
401/403当前页面由于权限问题不可访问
404/410当前页面不存在
5xx服务端异常

使用 meta robot tag

你可以通过 meta robots 标签标记来阻止 Googlebot 对页面标记索引页面。举个例子,在页面中增加下面一个标签:

1
2
<!-- Googlebot won't index this page or follow links on this page -->
<meta name="robots" content="noindex, nofollow" />

当 Googlebot 在 robots meta 标签中遇到 noindex,它就不会渲染或者索引这个页面。

使用 JavaScript 修改或删除 robots meta 标签可能不会如期望似的生效。如果 meta 标签包含 noindex ,Googlebot 会跳过渲染和 JavaScript 执行。如果你想要使用 JavaScript 修改 robots meta 标签,需要设置 noindex

处理图片和懒加载内容

图片加载在宽带和性能方面会带来很大的消耗。一个好的策略是使用延迟加载,仅在用户即将看到图像时加载图片。为了确保更好的实现延迟加载,请遵循我们的延迟加载指南

]]>
<h2 id="理解-JavaSript-SEO-基础"><a href="#理解-JavaSript-SEO-基础" class="headerlink" title="理解 JavaSript SEO 基础"></a>理解 JavaSript SEO 基础</h2><p>原文 <a href="https://developers.google.com/search/docs/guides/javascript-seo-basics" target="_blank" rel="noopener">https://developers.google.com/search/docs/guides/javascript-seo-basics</a></p> <blockquote> <p>你是否怀疑过由于 JavaScript 的问题可能会影响你的网页或者某一部分内容出现在 Google Search 的结果? 你通过我们的<a href="https://developers.google.com/search/docs/guides/fix-search-javascript" target="_blank" rel="noopener">故障排除指南</a>来了解如何解决 JavaScript 相关的这些问题。</p> </blockquote> <p>JavaScript 是 WEB 平台中重要的一部分,因为它能够提供由普通 web 网站向功能更强大的应用平台转变的众多特性。开发 JavaScript 驱动的 web 应用可以帮助你吸引新用户,同时可以通过 Google 搜索到你的 web 应用提供出来的有效内容来留存现有用户。当 Google Search 通过 Chromium 引擎来执行 JavaScript 时,你可以做一些下面这些事情去进行优化改进你的 Web 应用。</p>
全栈的另一个选择:Serverless 无服务框架浅谈 https://beanlee.github.io/posts/learn-serverless-1-perf/ 2019-05-27T01:49:55.000Z 2019-05-27T01:54:16.000Z

近期在准备部门内部的一个分享,思考了一些主题,最终还是决定结合自身 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,将近期火热的 CaaSFaaS 页加入到其中,如下图:

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 架构的云厂商,允许使用者可以开发服务端逻辑,并将其放在无状态的云计算容器中,由事件来进行触发,其他完全交给云来管理。

Serverless 分为两种:

  • BaaS Backend as a Service 后端即服务 如文件存储、数据存储、推送服务、身份统一验证等,我们平时写 NodeJS 或者 Java 都是接触不到的,不是这次讨论的主角。
  • FaaS Functions as a Service 函数即服务 服务商提供一个平台,提供给用户开发、运行和管理这些函数的功能,而无需搭建和维护基础框架。这是我们关注的重点

FaaS

FaaS 是一种事件驱动的由消息触发的函数服务。函数 Function顾名思义,计算机编程领域上的函数,有着最基础的定义,就是调用函数传入 INPUT 得到输出 OUTPUT,函数内部对于使用者可以是黑盒的,这云计算中也就可以理解成为是一种函数的服务。云厂商一般都会集成很多同步或者异步的事件源,通过订阅这些事件,去达到条件触发、定时触发来运行某一个云函数的效果。FaaS 允许我们上传一个完整的函数代码片段,这个函数理论上提供单一的无状态的服务,当事件触发执行这个函数的时候,它会创建实例、启动并开始执行,完成服务后等待被销毁,不存在上下文信息和状态。换言之,就是如果函数不运行,这个实例就不存在,云厂商的计费方式也发生了调整,既然没有实例存在运行,造成损耗,那么就没有产生计费。

可以直接理解为:只有当你的函数执行的时候才会按照运行次数收费,如下图,我们关心的只是应用层中的函数部分!

事件触发,给大家举一个类似例子,有一款老牌网络应用:

IFTTT:汇聚了世界各种有趣的 API ,通过触发一件、一件事来形成链式的调用完成有趣功能。比如,当你打一辆 Uber 回家的路上,当快到家附近的时候,它会自动触发家里的灯打开,空调开启;比如,你发布一个 Instagram Photo 会自动将图片存储到 Google Photo 或者同步到微博;诸如此类,都是当发生一个事件的时候才去触发下一个事件;FaaS 可以与 BaaS 通过事件订阅来做到联动。

应用场景

AWS Lambda 官网文档Google Cloud Functions 官网文档,举了几个的应用场景,其中包括物联网 IoT、独立应用 、游戏后台服务引擎、 数据报表(定时)等等。

貌似看上去离前端有些远,再举一个例子,2018年微信小程序提出的云开发,我个人理解也是对 Serverless 的一次尝试,让拥有开发小程序的工程师,不必依赖传统服务端,而利用云存储、云数据库、云函数来实现自给自足。

客户端开发,可以自行将数据、文件、信息同步到云端,而又不需要过于操心运维。

DEMO Express

接触新技术时,工程师们总是喜欢拿来直接上手实验,这里笔者使用 serverless.com 提供的 example,做了一个用于 TODO 应用的 server 端,使用 AWS Lambda, API Gateway, DynamoBD, S3 等服务,整体代码只有 200 行,其中 5 个 Function 服务,拥有 CRUD + Query 的功能。

如此以来,就拥有服务端的 API 服务,客户端就调用这几个服务,一个简单的 GTD 应用的雏形就已经完成了!

FaaS 优势

可以说 AWS Lambda 在市面上探索和发展的最久,用户量最大,Lambda 目前已经可以支持 Python、Java、Go、Ruby、.Net、NodeJS 和自建环境,这几个服务端语言都有自己擅长的场景和成熟的库,可以更方便地去助力完成数据计算、机器学习、图像处理等等工作。

  • 降低成本(开发、运营成本)
  • 计费方式按价值付费,不再为 7x24 埋单(即使遇到网络攻击,也可以设定阈值做到合理防护,不至于收到天价账单)
  • 灵活自动拓展,无需关心扩容缩容问题
  • 开发人员更专注核心业务

看上去万能?

No silver bullet. - The Mythical Man-Month

任何解决方案都不是万能的,一定是有它适合的场景,解决适合的问题而存在。

缺陷

  • 启动延迟、不适合长时间运行,支持语言版本更新不及时(AWS NodeJS 8.10)
  • 没有上下文环境,离线调试困难(serverless offline)
  • 完全依赖云,貌似是一条无法回头的路(是否一切都要部署到云端,这个一直处在争论中,本文也持中立态度,要分场景适用。)
  • 函数之间调用,目前还是比较保守的,AWS 已经提供这样的方案,但仍然不想传统服务与服务之间的调用的易用,其中一个非常严重的亟待解决问题,就是为了避免函数之间调用异常从而出现的死循环(函数既要做小,但是如果想提供复杂功能又不得不做的臃肿,这是一个矛盾)

总结

Serverless 架构带来的价值和挑战是并存的,国内的 Serverless 发展也在初期阶段,它改变了我们开发模式,也改变软件系统的一部分设计,未来发展的好坏,我们无法控制,但是我们可以保持关注,适当尝试。

参考资料

]]>
<blockquote> <p>近期在准备部门内部的一个分享,思考了一些主题,最终还是决定结合自身 Web 服务端开发经验,来分享自己调研 Serverless 无服务建构的相关内容。</p> </blockquote> <blockquote> <p>以下全文是前期准备阶段调研和整理的一部分资料,梳理总结后,在制作 Slide 过程中书写逐字稿缩减而来的。分享结束后还会继续在这部分挖掘和实践,云计算中的 Serverless 是一个非常大的主题范畴,单凭这一篇文字是没办法细说全部,笔者也在路上,感兴趣的同学可以留言讨论,持续关注。</p> </blockquote>
【译】React 中 State, Store, Static, This 的几个问题 https://beanlee.github.io/posts/react-state-this-static-store/ 2018-10-31T14:40:13.000Z 2019-04-23T15:49:06.000Z

笔者最近在整理前段时间接手的其他团队的 RN 项目代码,在梳理项目中旧代码过程中,对 React 中 State Store Static This 产生疑惑,借此翻译这篇文章解惑,也分享给各位。

原文 https://medium.freecodecamp.org/where-do-i-belong-a-guide-to-saving-react-component-data-in-state-store-static-and-this-c49b335e2a00

发表时间 2016-08

作者 Sam Corcos

Where to Hold React Component Data: state, store, static, and this

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。

让我们一起来详细了解下如何使用。

Local state 组件的本地状态

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
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
import React from 'react'

class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
this.addOne = this.addOne.bind(this)
}

addOne() {
this.setState({
counter: this.state.counter + 1
})
}

render() {
return (
<div>
<button
onClick={ this.addOne }>
Increment
</button>
{ this.state.counter }
</div>
)
}
}

Your data (the value of the counter) is stored within the App component, and can be passed down its children.

App 组件中数据被储存其中,并可以向子组件进行传递。

Use cases

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。

Redux store

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
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
import React from 'react'
import { connect } from 'react-redux'
import Actions from './Actions.js'

class App extends React.Component {
constructor(props) {
super(props)
this.addOne = this.addOne.bind(this)
}

addOne() {
this.props.dispatch(Actions.addOne())
}

render() {
return (
<div>
<button
onClick={ this.addOne }>
Increment
</button>
{ this.props.counter }
</div>
)
}
}

const mapStateToProps = store => {
return {
counter: store.counter
}
}

export default connect(mapStateToProps)(App)

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 时才会触发重新渲染。

Use cases

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

this

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
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
import React from 'react'

class App extends React.Component {
constructor(props) {
super(props)
this.counter = 0
this.addOne = this.addOne.bind(this)
}

addOne() {
this.counter += 1
this.forceUpdate()
}

render() {
return (
<div>
<button
onClick={ this.addOne }>
Increment
</button>
{ this.counter }
</div>
)
}
}

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() 去重新渲染。这是由于没有 stateprops 发生变化,是不会触发组件的重新渲染。

这也是一个实际的非常糟糕地不使用 this 的例子。如果你发现你自己正在使用 forceUpdate() 你就有可能犯了一个错误。期望做到值改变而触发重新 render,就应该使用 local state 或者 props 或者是 Redux store

Use cases

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
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
import React from 'react'
import { Socket } from 'phoenix'

class App extends React.Component {
componentDidMount() {
this.socket = new Socket('http://localhost:4000/socket')
this.socket.connect()
this.configureChannel("lobby")
}

componentWillUnmount() {
this.socket.leave()
}

configureChannel(room) {
this.channel = this.socket.channel(`rooms:${room}`)
this.channel.join()
.receive("ok", () => {
console.log(`Succesfully joined the ${room} chat room.`)
})
.receive("error", () => {
console.log(`Unable to join the ${room} chat room.`)
})
}

render() {
return (
<div>
My App
</div>
)
}
}

export default App

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

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
2
3
4
5
6
7
8
9
class App extends React.Component {
render() {
return (<div>{ this.props.title }</div>)
}
}

App.propTypes = {
title: React.PropTypes.string.isRequired
}
1
2
3
4
5
6
7
8
9
class App extends React.Component {
static propTypes {
title: React.PropTypes.string.isRequired
}

render() {
return (<div>{ this.props.title }</div>)
}
}

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 并不复杂。他仅仅是给类增加值的另外一种方式。在 staticthis 之间主要的差异主要是不需要实例化来访问这个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class App extends React.Component {
constructor() {
super()
this.prototypeProperty = {
baz: "qux"
}
}
static staticProperty = {
foo: "bar"
};

render() {
return (<div>My App</div>)
}
}

const proto = new App();
const proto2 = proto.prototypeProperty // => { baz: "qux" }

const stat = App.staticProperty // => { foo: "bar" }

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() 实例化以后才可以访问到。

Use cases

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
2
3
4
5
6
7
8
9
10
11
class App extends React.Component {
static requiredData = [
"username",
"email",
"thumbnail_url"
]

render() {
return(<div></div>)
}
}

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

That other option…

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
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
import React from 'react'

let counter = 0

class App extends React.Component {
constructor(props) {
super(props)
this.addOne = this.addOne.bind(this)
}

addOne() {
counter += 1
this.forceUpdate()
}

render() {
return (
<div>
<button
onClick={ this.addOne }>
Increment
</button>
{ counter }
</div>
)
}
}

export default App

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: 如果可以避免的话,不要使用模块作用域的变量。

]]>
<blockquote> <p>笔者最近在整理前段时间接手的其他团队的 RN 项目代码,在梳理项目中旧代码过程中,对 React 中 State Store Static This 产生疑惑,借此翻译这篇文章解惑,也分享给各位。</p> </blockquote> <p>原文 <a href="https://medium.freecodecamp.org/where-do-i-belong-a-guide-to-saving-react-component-data-in-state-store-static-and-this-c49b335e2a00" target="_blank" rel="noopener">https://medium.freecodecamp.org/where-do-i-belong-a-guide-to-saving-react-component-data-in-state-store-static-and-this-c49b335e2a00</a></p> <p>发表时间 2016-08</p> <p>作者 <a href="https://medium.freecodecamp.org/@SamCorcos" target="_blank" rel="noopener">Sam Corcos</a></p>
代码的艺术 - 章淼讲座笔记 https://beanlee.github.io/posts/Art-Of-Code/ 2018-10-08T02:41:48.000Z 2018-10-08T02:04:33.000Z 适合新手和正在努力进阶的高年级同学阅读 :)

工程师的内功修炼

章淼 简介

清华大学计算机博士;百度云前端技术负责人;百度 Golang & Python 技术委员会成员;

笔记

对比 Google 的工程师,国内的工程师写代码的占用时间显然过多了,而不太注重提前设计;Google 工程师们在开始实现某一模块或功能时,会事先在代码库中搜索是否已经有可重用的代码,并且代码库中的代码具有完整的注释和文档。

提前设计的重要性

尽可能地提前完成两个文档

  • 需求分析文档
  • 系统设计文档

原因:在未启动实现细节代码之前构思设计时发现问题的修改,对比后期真正已经开始 Coding 的时候,对比发现问题进行修改,成本要低很多。文档一般只写主要逻辑,而代码涉及更多细节。

笔者备注:但这不是绝对的,修改是正常的,不要惧怕修改,反复尝试积累经验。

  • 需求分析文档:主要是在定义黑盒状态,描述外在,描述 WHAT 要做什么?
  • 系统设计文档:主要实在定义白盒状态,描述内在,描述 HOW 怎么做?

两者要有区分,不要混淆,也不要混在一起写!

  • 需求分析文档的误区
    不要过早提前构想实现细节,我们的大脑会下意识地在我们构想如何实现时遇到的各种难题,而将原本的需求分析的思考挂起;举例:导弹 vs 炸弹,两者都有摧毁目标的能力,但是很明显导弹的价值更高,重要的是制导的功能,而不是爆炸本身。
  • 系统设计文档的误区
    主要要写定义系统的架构、模块、接口、数据、关键算法、设计思路等等得过程记录。
系统架构要写什么以及方法

概念、模型、视图等等。

  • 静:系统静态的样子,功能模块如何划分等
  • 动:系统运转起来,各模块联动起来的样子
  • 细:不同角度,不同层次去描述

  • 每一个组件(模块、函数)保证单一性,Single purpose. 只做一件事!

  • 轻耦合,低内聚(避免全局变量(多处操作难以控制))
  • 当前系统设计所受到的约束(当前设计的瓶颈在哪?比如网络、吞吐量、占用 CPU 或文件位置资源等)
  • 需求分析是系统设计的来源
  • 模型和抽象的思维能力很重要(涉及概念:模型、数据结构、算法等等)
设计接口(Interface)要注意什么?
  • 接口定义系统外在的功能
  • 接口定义当前系统与外部系统之间的关系

接口 Interface 定义了系统对外的接口,往往比系统实现内部细节代码更重要,不要过于草率,因为一旦定义了接口,提供出去给调用方使用,想修改就太难了。所以设计接口有重要原则,站在使用者的角度考虑问题!

两点细节:

  • 向前兼容(尽量不要接口已升级,老接口全不能用,那就不是好的接口设计)
  • 使用方便(让调用者可以一目了然知道接口的作用,简化传参,说明返回值等等)

如何写代码?

代码是一种表达的方式。是写给人看的,要有编程规范

拥有编程规范的理想状态:1. 看别人代码就像看自己代码一样易懂;2. 看代码主要看逻辑,不要过多注重细节;3. 代码尽可能地不要让人去多想。

Don’t make me think!

Moudle 模块

紧内聚,低耦合。单一功能。反例,定义一个 utils.py 内部包含诸多方法,不易懂。

模块一般可以分为两类:

  • 数据类的模块(1. 主要完成对数据的封装; 2. 对外提供的数据接口)
  • 过程类的模块(1. 不要包含数据,可以是调用数据类的模块或者调用其他过程类模块; 2. 具备操作性质的模块)

模块的重要性:1. 降低维护成本; 2. 更好地复用

Class 类 和 Function 函数

两者是不同的模型,各自有各自适用的范围。

推荐方法:和类的成员无关的函数,尽量独立出去单独一个函数,不建议作为类的成员函数。

面向对象思想的讨论:多态和继承,需要谨慎适用,作为 Python 的工程师,不太推崇 Java 中继承和多态,因为系统是逐渐长起来的,并不是从一开始就是一个成熟的样子,所以很难凭空去设计一个继承的关系。

模块的构成

文件头(注释)

  • 模块的说明,简要功能
  • 修改历史(时间、修改人,修改的内容)
  • 其他特殊细节的说明

函数(重要性仅次于模块)

  • 描述功能
  • 传入参数的描述(含义、类型、限制条件等等)
  • 返回值得描述(有足够明确的语义说明)
    • 逻辑判断型 check isXXX
    • 操作型(成功 or 失败)
    • 数据获取型(状态 + 数据)
  • 异常如何处理(是抛出?还是内部catch?要明确)
  • 明确单入口和单出口(多线程开发时尤为重要)

函数要尽可能的规模小,足够短(BUG 往往出现在非常长的一个函数里)

代码块的分段也很重要,分段背后是划分和逻辑表达。

代码是一种表达能力的体现,也算是文科的范畴!注释不是补出来的!

命名的重要性:要准确、易懂、可读性强,尽量做到 望名生义

互联网时代的系统是运营出来的。

可检测性也是尤其重要的。(埋点、监控等等)

没有数据的收集,等于系统没有上线。

监控不单单只有传统意义上的内存、CPU、网络、崩溃率的监控,还应有线上真实数据监控,需要有足够多的状态记录。

日志是很有限的一种监控手段,并且采集日志也是一种资源耗费。推荐的手段:可以使用埋点,对外提供接口,有单独的系统调用接口进行有针对性的采集。

修身

好的程序员,与工作年限无关,与学历无关

学习-思考-实践

学习:主观意愿地学习,途径也有很多,例如书籍、开源代码、社区。忌讳夜郎自大、井底之蛙。注重培养自己学习吸收的能力,多读多看但是数量不是最终目的。

Stay Hungry, Stay Foolish. – Steve Jobs

思考:学习需要经过思考,形成自己的思维。

实践:《卓有成效的时间管理者 - 德鲁克》推荐阅读

知识-方法-精神

知识过时会非常快!

方法:分析问题、解决问题的能力尤其重要(定义问题、识别问题、定义关键问题)

精神:决定型,要做就要坚持做

前进的道路上不能永远都是鲜花和掌声。

基础乃治学之根本。

数据结构、软件工程、逻辑思维能力、研究能力,需要5-8年时间磨炼。

推荐书籍

  • 人月神话
  • 代码大全
  • 201条准则
  • 快速开发
  • 系统结构
  • 操作系统
  • 网络基础
]]>
<p>适合新手和正在努力进阶的高年级同学阅读 :)</p>
VSCode 插件开发急速入门 https://beanlee.github.io/posts/vscode-extands-step-0/ 2018-06-13T07:41:48.000Z 2018-06-16T03:26:44.000Z VSCode (全称:Vistual Studio Code) 由 Microsoft 出品,其启动快速、稳定性好、占用内存较小、插件越来越丰富、社区活跃等特点,目前是个人和团队中很多小伙伴的主力代码编辑器。

废话不多说,直奔主题,本文你将了解简单的 vscode 的插件开发入门,感兴趣的同学可以继续阅读。

以下文中提到的“插件”均为 Vistual Studio Code 的插件,Vistual Studio Code 也简称 VSCode

Start

环境准备

  • nodejs link 建议使用 LTS 版本,截止发文 v8.11.3
  • npm link 建议最新版本,截止发文 v6.1.0
  • yeoman link npm install -g yo
  • generator-code link npm install -g generator-code

做好上面的环境准备,正式进入开发,下面我会以自己刚做好的一个查询北京地区空气质量为例子,继续介绍(安心してください,代码很少,很好入门)。

数据准备

下面我们补充一下我们的例子的功能,首先明确我们要获取城市的 AQI(空气质量)信息,网络上可以搜索到直接调用 API 或者 SDK,笔者目前使用的京东云的一个产品京东万象中的一个天气免费 API,链接见此 注册名实名认证后可以获得一个调用应用的 KEY 既可以获取天气数据了。更多其他方面的 API 读者可以自行探索。

笔者使用的 API 是 https://wx.jdcloud.com/market/datas/26/10610 参数 city 支持 中英文名称、ID 和 IP 限制 5000次/天

锦囊

已有插件所在目录如下:

ospath
windows%USERPROFILE%\.vscode\extensions
macOS~/.vscode/extensions
Linux~/.vscode/extensions

在这个目录下我们可以看到已经安装的所有插件的代码,即使插件是使用 typescript 编写 out 文件夹也可以看到编译后的 javascript 代码,感兴趣的同学可以直接看一看自己平时最常用的插件是如何实现的。

开发

执行下面代码:

1
yo code

执行后会提示几个问题,第一个问题我们选择 New Extension (JavaScript),其他正常填写即可。(可以使用 yo code 来创建插件、主题、代码片段、语言支持、键盘映射、插件包,本文我们只讨论插件,其他暂且放在一边。)

填写完成后,会自动创建文件夹并帮助初始化完成文件,我们先看下目录结构。

1
2
3
4
5
6
7
8
9
10
.
├── CHANGELOG.md ## 修改记录
├── README.md ## 插件说明 README
├── extension.js ## 入口
├── jsconfig.json ## JavaScript 配置
├── node_modules ## 依赖
├── package-lock.json
├── package.json
├── test
└── vsc-extension-quickstart.md

熟悉的项目文件结构,直接查看 vsc-extension-quickstart.md 这个文档,文中提到 package.json 声明当前插件和命令的配置文件,用来注册命令等配置;extension.js 是主入口文件,插件的具体实现代码可以放在这里;

简单了解一下两个重要文件:

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "city-aqi", // 插件名称
"displayName": "city-aqi", // 插件显示名称
"description": "Air Quaility Index of City", // 插件描述
"version": "0.0.1",
"publisher": "beanleecode", // 插件发布者
...
"activationEvents": [ // 活动事件列表
"onCommand:extension.sayHello"
],
"main": "./extension", // 入口文件
"contributes": {
"commands": [ // 对应命令列表
{
"command": "extension.sayHello",
"title": "Hello World"
}
]
},
...
}
extension.js
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
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const vscode = require('vscode');

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
function activate(context) {

// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
// 这里的代码将只会在插件激活时执行一次
console.log('Congratulations, your extension "city-aqi" is now active!');

// The command has been defined in the package.json file
// 定义在 package.json 中的命令在这里定义
// Now provide the implementation of the command with registerCommand
// 提供 registerCommand 来注册实现代码
// The commandId parameter must match the command field in package.json
// commandId 参数必须与 package.json 匹配
let disposable = vscode.commands.registerCommand('extension.sayHello', function () {
// The code you place here will be executed every time your command is executed
// 这里的代码每次执行 这个命令 的时候都会被执行

// Display a message box to the user
// 显示信息框
vscode.window.showInformationMessage('Hello World!');
});

context.subscriptions.push(disposable);
}
exports.activate = activate;

// this method is called when your extension is deactivated
// 插件被停用的时候被调用
function deactivate() {
}
exports.deactivate = deactivate;

下面我们直接用 VSCode 打开这个项目的根目录,打开 extension.js 按下键盘 F5 或启动调试,接下来就可以看到,已经启动一个新的窗口,我们按下 command + shift + pcommand + p 再输入 > 即可调用插件,在 package.json 中已经定义过 commands 我们直接输入 Hello World 直接启动插件,此时可以看到 Hello World 的通知框,就可以调试代码了。

OK,下面我们来进一步实现我们的功能,调取天气 API 数据,展示。

extension.js
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
const vscode = require('vscode');
const axios = require('axios');

function activate(context) {
let cityAqi = vscode.commands.registerCommand('extension.sayCityAqi', function () {

const options = {
ignoreFocusOut: true,
password: false,
prompt: "Please type your city (eg.beijing or 北京)"
};

vscode.window.showInputBox(options).then((value) => {
if (value === undefined || value.trim() === '') {
vscode.window.showInformationMessage('Please type your city.');
}
else {
const cityName = value.trim();
// appkey=xxxxxxxx 替换成事先申请好的 key
axios.get(`https://way.jd.com/he/freeweather?city=${cityName}&appkey=xxxxxxxx`)
.then((rep) => {
if(rep.data.code !== '10000') {
vscode.window.showInformationMessage('Sorry, Please try again.');
return;
}
const weatherData = rep.data.result.HeWeather5[0]
if(weatherData.status !== 'ok') {
vscode.window.showInformationMessage(`Sorry, ${weatherData.status}`);
return;
}
vscode.window.showInformationMessage(`${weatherData.basic.city} 's AQI => PM25: ${weatherData.aqi.city.pm25}, PM10: ${weatherData.aqi.city.pm10} ${weatherData.aqi.city.qlty}`);
});
}
});
});

context.subscriptions.push(cityAqi);
}
exports.activate = activate;

// this method is called when your extension is deactivated
function deactivate() {
}
exports.deactivate = deactivate;

Tadaaa,简单的功能就完成了!:P

代码中用到了两个 vscode windows 的 api,showInputBoxshowInformationMessage 更多 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
2
3
$ vsce publish
Publishing city-aqi@0.0.1...
Successfully published city-aqi@0.0.1!

ALL Done 🎉

参考资料

写在最后

笔者几年前一直是 Sublime Text 的使用者,修改 jsp、vm、sql等等文本文件,后来出现了 Atom 社区也相当活跃,用户同样很多,笔者也曾试用过一段时间。

合适的才是最好的 近两年 VSCode 一直是我的主力开发编辑器,它也有缺点,但是它在不断精进和完善,大厂维护更新频率也很快。

很多大牛们喜爱的 VimEmcas 笔者没有用过,甚至连尝试都没有,可能是被过于灵活的配置吓到了

PS,最近知道一个词宜家理论,套在上面几个工具上面,应该都适用,没有最好的只有最合适自己的。现在机器上配置的 vscode 的开发环境,笔者已经不得不使用同步插件来进行备份,以免更换机器时重新配置。

VSCode 有很多方便的插件,可以帮助我们提高开发效率,以后找机会写一个推荐列表分享出来。

]]>
<p>VSCode (全称:Vistual Studio Code) 由 Microsoft 出品,其启动快速、稳定性好、占用内存较小、插件越来越丰富、社区活跃等特点,目前是个人和团队中很多小伙伴的主力代码编辑器。</p> <p>废话不多说,直奔主题,本文你将了解简单的 vscode 的插件开发入门,感兴趣的同学可以继续阅读。</p>
Webpack 4 实战 React 和 Vue 项目 https://beanlee.github.io/posts/something-about-webpack-4/ 2018-06-11T09:48:23.000Z 2018-06-20T01:31:47.000Z 最近一直在参与小组内“造轮子”(具体内容另寻机会再详说)在开发的过程中,了解并且学习到 Webpack v4 的一些内容,趁记忆还深,汇总成文。

鉴于 Webpack 作为关键在 Google 可以搜索到很多相关的文章,网上文章也是针对各自项目和某些情况的具体方案或者介绍说明,本文也不例外,只介绍分享过程中积累的 4.0 版本的个人实战经验。

导读:本文你将 Get 到使用 Webpack 4 从零开始分别搭建 React 16 和 Vue 2 项目,同时还有基于 Webpack 4 的一些开发和生产环境配置经验,感兴趣同学可以继续阅读。

PS. 前半部分较为基础,有一定经验的同学可以直接跳过阅读后半部分实战内容

以前也翻译过两篇关于 Webpack 的文章,感兴趣的同学可以点击下面链接查看:!!! 强烈推荐 !!!

正文开始

Webpack 4 从“零”开始

相信提到 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
2
3
4
mkdir webpack-4-demo
cd webpack-4-demo
yarn init -y
yarn add webpack --dev

修改 package.json 文件:

1
2
3
"scripts": {
"build": "webpack"
}

运行

1
yarn build

过程中会提示是否安装 webpack-cli 直接敲 yes 即可。

此时控制台执行结果会有如下报错:

1
2
3
4
5
ERROR in Entry module not found: Error: Can't resolve './src' in '/Users/xxxx/webpack-4-demo'
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! webpack-4-demo@1.0.0 build: `webpack`
npm ERR! Exit status 2

注意:我们目前没有写任何配置文件,但 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
2
3
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

回过头查看刚刚编译好的 main.js 文件,该文件已经直接被压缩好的,且可在生产模式下运行。

警告中提到的mode 就是 Webpack 4 新增的一个配置,具体内容可以见下面链接,以及笔者之前的翻译文章。

当然,也可以通过 --mode 选项来手动选择 bundle 的模式,比如 webpack --mode development

自此,如果你的项目 src 目录下的内容需要 Webpack 帮你编译,输出在 dist 目录,Webpack 几乎零配置就可以直接“胜任”了。

Webpack 4.x & React 16.x

上一节简单介绍了 V4.x “零”配置的基础应用。当然,实际工作中我们的项目都会比较复杂,上面的内容远不能满足我们的需求,下面我们就以一个 React 16 & Webpack 4 DEMO 项目为例,还原从零开始搭建基于 Webpack 打包编译项目的整个过程。

Facebook 官方推出的 create-react-app 工具已经非常好用,但仍然需要做一些修改才可以满足实际项目上线的需求,同时我们仍希望有更多所谓个性化设置来支持项目,且截至到今天 cra 使用 webpack 3.8 与我们本文介绍 webpack 4 有出入,所以下面内容不再提及,对 cra 感兴趣的同学也可以自行搜索查看了解。

React 项目初始化

首先,重复上面介绍的步骤:(创建目录、安装 react、安装 webpack、安装 babel)

1
2
3
4
5
6
7
8
9
10
11
12
mkdir react-webpack-demo
cd react-webpack-demo
## 初始化 package.json
yarn init -y
## 安装 react
yarn add react react-dom
## 安装 webpack
yarn add webpack webpack-cli --dev
## 安装 babel
yarn add @babel/core @babel/preset-env --dev
## 安装 babel-react
yarn add @babel/preset-react "babel-loader@^8.0.0-beta" --dev

注意:这里使用 babel 转义,此处既可以在项目根目录下创建 .babelrc 文件,也可以稍后在 webpack.config.js 中配置,这里我们选择在后者统一配置。

现在我们新建一个配置文件,webpack.config.js 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: { // babel 转义的配置选项
babelrc: false,
presets: [
require.resolve('@babel/preset-react'),
[require.resolve('@babel/preset-env'), { modules: false }],
],
cacheDirectory: true,
},
},
},
]
}
};

src 目录下创建 App.jsx

1
2
3
4
5
6
7
8
9
10
11
import React from "react";
import ReactDOM from "react-dom";
const App = () => {
return (
<div>
<p>Hello React and Webpack</p>
</div>
);
};
export default App;
ReactDOM.render(<App />, document.getElementById("app"));

src 下新建 index.jsx 内容如下:

1
import App from './App';

执行 yarn build 等待打包结果,此时目录 dist 下已经打包好 bundle。

我们接着创建 html 文件,在 src 下创建 index.html :

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Hello React Webpack</title>
</head>
<body>
<div id="app">
</div>
</body>
</html>

修改 build 的配置,拷贝 html

1
yarn add html-webpack-plugin

修改上面的 config:

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
const HtmlWebPackPlugin = require("html-webpack-plugin");

module.exports = {
mode: 'production',
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
babelrc: false,
presets: [
require.resolve('@babel/preset-react'),
[require.resolve('@babel/preset-env'), { modules: false }],
],
cacheDirectory: true,
},
},
},
]
},
plugins: [
+ new HtmlWebPackPlugin({
+ template: "src/index.html",
+ filename: "index.html"
})
]
};

执行 yarn build 就可以看到已经打包好的 index.html 和 bundle js 。

进入开发阶段

dev-server

通过 webpack-dev-server 搭建本地 server 服务,目前是通用的解决办法

安装依赖:

1
yarn add webpack-dev-server --dev

方便起见,我们在 package.json 的 scripts 中增加内容:

1
2
3
"scripts": {
"start": "webpack-dev-server --mode development --open",
}

运行 yarn start 此时会运行一个 dev-server 服务,这样我们就能方便地在本地进行开发了。

在开发中我们还有一些其他的需求,比如 sourceMap 、修改 dev-server 配置等,所以我们可以新建一个配置 webpack.config.dev.js

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
const path = require('path');

module.exports = {
mode: 'development',
+ devtool: 'cheap-module-source-map',
resolve: {
extensions: ['.js', '.jsx'],
},
+ devServer: {
+ contentBase: path.join(__dirname, "./src/"),
+ publicPath: '/',
+ host: '127.0.0.1',
+ port: 3000,
+ stats: {
+ colors: true,
+ },
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: { // babel 转义的配置选项
babelrc: false,
presets: [
require.resolve('@babel/preset-react'),
[require.resolve('@babel/preset-env'), { modules: false }],
],
cacheDirectory: true,
},
},
},
],
},
plugins: [
new HtmlWebPackPlugin({
template: 'src/index.html',
filename: 'index.html',
+ inject: true,
})
]
};

修改 package.json 中 scripts 执行所需要执行的配置文件:

1
2
3
"scripts": {
"start": "webpack-dev-server --config './webpack.config.dev.js'"
}

重新执行 yarn start 可以看到修改配置后的 dev-server

热更新 HMR

配置热更新就可以让我们在开发过程中,将修改后代码整页面无刷新且保持原有 state 的情况下直接反应到页面,下面我们继续修改 config.dev.js 并在 App.jsx 增加内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// webpack.config.dev.js config 部分
devServer: {
...
+ hot: true,
...
},

// webpack.config.dev.js plugins 部分
...
plugins: [
+ new webpack.HotModuleReplacementPlugin(),
...
]
...

// app.jsx 页面底部新增
...
+ if (module.hot) {
+ module.hot.accept((err) => {
+ if (err) {
+ console.error('Cannot apply HMR update.', err);
+ }
+ });
+ }

执行 yarn start 重新启动 server,每次修改代码后保存就可以看到控制台里重新编译的信息,浏览器中变化实际修改的内容了。

Webpack 4.x & Vue 2.x

Vue 官方也推出 vue-cli 来帮助使用者快速创建项目,同时也是使用 webpack 4 来进行构建项目,通过阅读使用文档和源码,需要满足现有复杂项目的需求,我们可能也是不仅需要使用 –options 的方式同时还需要做一些定制的开发才可以,因此下面不再提及。

Vue 项目初始化

与 React 项目初始化一致(创建目录、安装 vue、安装 webpack、安装 babel)

1
2
3
4
5
6
7
8
9
10
11
12
mkdir vue-webpack-demo
cd vue-webpack-demo
## 初始化 package.json
yarn init -y
## 安装 webpack
yarn add webpack webpack-cli --dev
## 安装 babel
yarn add @babel/core @babel/preset-env --dev
## 安装 vue
yarn add vue
## 安装 vue-loader
yarn add vue-loader vue-template-compiler --dev

在 vue-webpack-demo 文件夹下,创建 index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Webpack Vue Demo</title>
</head>
<body>
<div id="app">
</div>
</body>
</html>

新建 src 目录,并新建 app.js 和 app.vue 文件:

1
2
3
4
5
6
7
8
9
// app.js
import Vue from 'vue'
import App from './app.vue'

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app.vue
<template>
<div id="app">
Hello Vue & Webpack
</div>
</template>

<script>
export default {
}
</script>

<style>
</style>

下面来增加配置文件,webpack.config.js

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
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
},
extensions: ['*', '.js', '.vue', '.json']
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.vue$/,
include: '/src/',
loader: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
chunks: ['app', 'manifest', 'vendor'],
});
]
}

完成后执行 webpack 就可以看到 dist 下已经构建完成的项目。

注意:vue-loader 已经更新到 v15.x ,与之前 14.x 配置方式有差异。

用于开发 dev-server 配置与 React 基本一致,这里不重复。

实战内容经验积累

下面的内容是在笔者积累的项目优化实战经验汇总。

提取公共依赖

为了减少一次请求文件体积过大,同时修改业务代码时不必要重复重新下载公共依赖代码,我们通常将公共依赖模块如 reactreact-domvueaxio 等文件抽取出来独立打包。

注意:CommonsChunkPlugin 在 4.0 中已经被 optimization 选项取代,不需要安装该插件就可以实现相同效果,见下面配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mode: 'production',
output: {
...
+ chunkFilename: 'js/[name].[chunkhash:8].js',
...
},

optimization: {
nodeEnv: 'production',
...
runtimeChunk: {
name: 'manifest', // 运行时文件
},
+ splitChunks: {
+ cacheGroups: {
+ commons: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendor', // 依赖第三方库要提取成名字是的 vendor 的文件
+ chunks: 'all', // 提取所有 chunks
+ },
+ },
},
...
},

通过上面的配置,可以将公共依赖和业务代码隔离开来。但是,也会存在一些隐患

  • 随着项目复杂度增加,依赖库增多,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
+ const os = require('os');
...
// webpack config
optimization: {
nodeEnv: 'production',
+ minimizer: [
+ new UglifyJsPlugin({
+ cache: true, // node_modules/.cache/uglifyjs-webpack-plugin
+ parallel: os.cpus().length, // 并行 default:true os.cpus().length - 1
+ uglifyOptions: {
+ ecma: 5,
+ mangle: true,
+ },
+ sourceMap: false,
+ }),
],
},

注意:4.0 版本压缩的代码已经放在 optimizationminimizer 节点下。
备注:关于 parallel 选项,新版的 uglifyjs 已经支持多核 CPU 并行执行,所以已经不需要 webpack-parallel-uglify-plugin 插件。

proxy 接口代理

配合 dev-server 对代理本地启动的 server 某一域名进行代理,解决服务端接口暂时满足要求、本地请求跨域等问题。

1
2
3
4
5
6
7
8
devServer: {
...
+ proxy: {
+ "/api": "https://localhost:3000",
+ changeOrigin: true, // 支持跨域请求
+ secure: true, // 支持 https
+ }
}

文档见dev-server-proxy doc

支持多入口

虽然多数情况下,我们都在开发并且维护单页面的应用,但是当遇到需要多页面的时候,我们也希望在一个项目内进行构建,目前解决办法比较粗暴,当前有 n 个入口 html 就创建 n 个 HtmlWebpackPlugin 插件实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');
const path = require('path');
// 先找到项目指定目录下的所有 html 此处假设我们把入口 html 放在 src/html 下 app1.html、app2.html、app3.html
const appHtmlEntries = fs.readdirSync(resolveToAppRoot('./src/html/'))
.filter(f => f.match(/\.html?$/))
.reduce((acc, p) => Object.assign(acc, { [path.basename(p).replace(/\.html?$/, '')]: path.join(resolveToAppRoot('./src/html/'), p) }), {});
...
// 每一个 html 创建一个 HtmlWebpackPlugin 实例
Object.keys(appHtmlEntries).forEach((name) => {
const pluginHtml = new HtmlWebpackPlugin({
filename: `${name}.html`,
template: `src/html/${name}.html`,
chunks: [`${name}`, 'manifest', 'vendor'],
inject: true,
});
webpackConfig.plugins.push(pluginHtml);
});

预编译 sass、引入 postcss、处理 css 压缩 和 文件分离

这里要注意 rules 中 loader 数组的顺序,由于 webpack 执行 rules 是从最后一个开始倒叙执行,所以我们配置的顺序也是:

预编译 sass => 处理 postcss => 处理 css => 压缩并独立 css 文件

升级到 4.0,已经不再使用 extract-text-webpack-plugin 插件来进行文件抽取,改用 MiniCssExtractPlugin 插件,配合 OptimizeCSSAssetsPlugin 插件来压缩 css 文件。

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
53
54
55
56
57
58
59
60
61
62
63
64
const autoprefixer = require('autoprefixer');
const postcssFlexbugsFixes = require('postcss-flexbugs-fixes');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

...
// rules 配置
{
test: /\.(css|sass|scss)$/,
use: [
{
loader: MiniCssExtractPlugin.loader, // 这个 loader 放在最后一个执行,将编译好的 css 独立
},
require.resolve('css-loader'),
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
postcssFlexbugsFixes,
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9',
],
flexbox: 'no-2009',
}),
],
},
},
require.resolve('sass-loader'),
],
},
...

// optimization 配置
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const cssnano = require('cssnano');
...
optimization: {
nodeEnv: 'production',
minimizer: [
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: cssnano, // 默认使用 cssnano 处理 css
cssProcessorOptions: {
reduceIdents: false, // 禁止将 keyframes 自动更名
mergeIdents: false, // 禁止自动合并 keyframes
discardUnused: false, // 禁止移除掉未使用的 keyframes
autoprefixer: false, // 禁止默认删除掉一些前缀,以减少兼容性的问题
zindex: false, // 禁止自动转换 z-index
map: false,
},
}),
],
},
...
// 独立 css 文件
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}),
],

支持 CDN 路径替换

可以将 html 中 css 和 js 的相对引用路径自动替换成配置的前缀路径,用来支持静态资源上线到具体指定的 CDN 路径来增加 app 内静态资源的下载速度。

1
2
3
4
5
output: {
...
+ publicPath: "https://...cdnpath.../assets/" // CDN 资源 URL 前缀
...
},

生产环境的 source-map

有些时候我们会遇到在生产环境中代码出现问题的情况,而本地开发却不重现,这个时候 source-map 就成了辅助解决问题的一个有利的工具,具体见下面配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mode: 'production',
+ devtool: 'source-map',
optimization: {
nodeEnv: 'production',
minimizer: [
new UglifyJsPlugin({
...
+ sourceMap: true,
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: cssnano,
// 默认使用 cssnano 处理 css,另外 clean-css 也提供相应的方案,但需要 4.2 版本才可用且质量和效率都没有得到验证,暂且不提
cssProcessorOptions: {
...
+ map: { inline: false } ,
},
}),
],
},

处理图片资源

下面是关键 loaders 但不会列出所有配置,内容略有差异,可以根据配置 limit 大小来控制图片转 base64 或压缩等。

  • file-loader 文件无处理,直接拷贝
  • url-loader 可以增加 base64 处理
  • svg-url-loader 处理 svg 文件,也同样支持 base64
  • image-webpack-loader 图片文件降质压缩

其他插件

  • HashedModuleIdsPlugin 使用更稳定的 moudle id 生成方式
  • webpack.optimize.ModuleConcatenationPlugin 插件已经不需要单独配置,Webpack 4 已经默认在生产模式下打包时内置开启优化

小结

并不是每一个人都想成为 Webpack 配置工程师!

上面引用一句我们“造轮子”时使用的一句 slogan。

虽然 Webpack 4 还没有达到开箱即用的程度(当然,开箱即用也就意味着可配置的内容有所减少,这一定是一把双刃剑),况且开箱即用也并不是它的被创造出来的初衷,简单的配置无法满足项目中的实际需求,各种各样的配置和插件配合着形成解决各种问题的不同方案,只在很多次尝试后才能达到针对某一个项目项目最优的配置。

然而,它只是一个工具,也许再过一段会有新的工具来取代 Webpack,But 既然你现在用到它,还是有必要花时间了解一下如何更好地让它为你、你的团队和你的项目来服务。

关于 Webpack 4 的配置经验笔者也在摸着石头过这条小河,网上也有诸多优化和解决方案,我们造的轮子也需要更多的项目和时间来帮助其成长,文中并没有面面俱到地将所有配置详细说明,或者并不是所有配置都是最优选择,也欢迎私信留言讨论。

]]>
<p>最近一直在参与小组内“造轮子”(具体内容另寻机会再详说)在开发的过程中,了解并且学习到 Webpack v4 的一些内容,趁记忆还深,汇总成文。</p> <p>鉴于 Webpack 作为关键在 Google 可以搜索到很多相关的文章,网上文章也是针对各自项目和某些情况的具体方案或者介绍说明,本文也不例外,只介绍分享过程中积累的 <strong>4.0</strong> 版本的个人实战经验。</p> <blockquote> <p>导读:本文你将 Get 到使用 Webpack 4 从零开始分别搭建 React 16 和 Vue 2 项目,同时还有基于 Webpack 4 的一些开发和生产环境配置经验,感兴趣同学可以继续阅读。</p> </blockquote> <p>PS. 前半部分较为基础,有一定经验的同学可以直接跳过阅读<a href="#实战内容经验积累">后半部分实战内容</a>。</p>
【译】Google 出品 - 利用 webpack 做 web 性能优化 https://beanlee.github.io/posts/blog-translate-web-performance-optimization-with-webpack-from-google-webpack4/ 2018-05-02T06:35:33.000Z 2018-06-20T01:31:47.000Z 作者 Addy OsmaniIvan Akulov

原文 https://developers.google.com/web/fundamentals/performance/webpack/

PS. 在 20180211 笔者翻译过一次,当时也没有完全理解和使用文中提到的优化项,近期工作中因为用到 Webpack 4.x 对生产环境进行打包,加深了一些理解,本译文对原有译文补充的 Webpack 4 内容,同时对原译文进行了校对和一些细节措辞的修改。

Instroduction 介绍

作者 Addy Osmani

现代 Web 应用经常用到 bunding tool 来创建生产环境的打包文件(例如脚本、样式等等),打包文件是需要优化压缩最小化,同时能够被用户更快地加载。在这篇文章中,我们将会利用 webpack 来贯穿说明如何进行高效地优化网站资源。这能帮助用户更快地加载你的应用同时获得更好的体验。

webpack-logo

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

code-splitting

Note: 我们创建了一个练习的应用来演示下面这些优化的描述。尽力抽更多的时间来练习这些 tips webpack-training-project

让我们从现代 web 应用中最耗费资源之一的 Javascript 开始。


Decrease Front-end Size 减少前端体积

作者 Ivan Akulov

当你正在优化一个应用时,首要事情就是尽可能地将它体积的减小。下面我们就来看看通过 webpack 如何做到减小前端体积。

Use the production mode (webpack 4 only) 使用生产模式(仅用于 webpack 4)

Webpack 4 介绍了一种新的模式,你可以将其设置成 developmentproduction 用于告诉 Webpack 你正在为不同的环境打包:

1
2
3
4
// webpack.config.js
module.exports = {
mode: 'production',
};

当你正在为你的应用用于生产环境编译打包时要确定开启了 production 模式。这样就帮助 webpack 开启类似压缩最小化代码、去除依赖库中开发环境代码等其他的优化项。

Further reading 扩展阅读

Note: 笔者也翻译了另外一篇介绍新增 mode 的文章,感兴趣可以点击链接

Enable minification 开启最小化

Note: 大部分只针对 webpack 3 如果你正在使用 webpack 4 生产模式打包,bundle 级别的最小化功能已经开启 - 你只需要配置对应 loader 选项即可

最小化就是通过去除多余空格、缩短变量名等方式压缩代码。例如:

1
2
3
4
5
6
7
8
9
10
11
// Original code
function map(array, iteratee) {
let index = -1;
const length = array == null ? 0 : array.length;
const result = new Array(length);

while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}

1
2
// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}

Webpack 支持两种方式最小化代码:bundle-level 最小化 和 loader-specific options。他们可以同时使用。

Bundle-level minification bundle 级别的最小化

Bundle-level 最小化功能可以在编译完成后压缩整个 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)
"use strict";
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.最小化之后的代码大概是下面这个样子:

1
2
3
// minified bundle.js (part of)
"use strict";function t(e,n){console.log("Rendered!")}
Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)

Webpack 4 中,bundle 级别的的最小化是自动开启的 - 同时在生产模式下、没有启用 bundle-level 都会开启。它是利用 UglifyJS 引擎来进行最小化的。(如果你需要禁用最小化,仅仅设置开发模式或者设置 optimization.minimizefalse。)

Webpack 3 中,你需要直接使用 UglifyJS 插件。该插件是 webpack 提供的;开启并设置插件选项即可:

1
2
3
4
5
6
7
8
// webpack.config.js
const webpack = require('webpack');

module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};

Note: 在 webpack 3 中,UglifyJS 插件不能编译 ES2015+(ES6) 的代码,这就意味着你在代码中使用 classes, arrow function 或者其他新特性时,不能将他们编译成 ES5的代码,插件会抛错。
如果你需要编译这些新语法,就要用到 uglifyjs-webpack-plugin package,他也是在 webpack 中捆绑一起的,但是版本更新,并且可以编译 ES2015+ 的代码。

Loader-specific options 特定的 Loader 配置

最小化代码的第二步就是利用特定的 loader 配置。配置这些 loader,你可以压缩那些不能被最小化的部分。举个例子,当你使用 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}",""]);

这部分内容由于是字符串并没有被最小化。于是我们需要配置对应的 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 } },
],
},
],
},
};
Further reading

Specify NODE_ENV=production 明确生产环境信息

Note: 仅在 webpack 3 中生效,如果使用生产模式 webpack 4 打包,NODE_ENV=production 优化项已经开启,就可以直接跳过此小结

减小前端体积的另外一个方法就是在代码中将 NODE_ENV 环境变量设置为 production

Libraries 会读取 NODE_ENV 变量判断他们应该在那种模式下工作 - 开发模式 or 生成模式。很多库会基于这个变量有不同的表现。举个例子,当NODE_ENV没有设置成production,Vue.js 会做额外的检查并且输出一些警告:

1
2
3
4
5
6
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …

React 也是类似 - 开发模式下 build 带有一些警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}

// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …

这些检查和警告通常在生产环境下是不必要的,但是他们仍然保留在代码中并且会增加库的体积。

Webpack 4 中增加 optimization.nodeEnv: 'production' 选项即可剔除掉它们:

1
2
3
4
5
6
7
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};

Webpack 3 中则使用 DefinePlugin

1
2
3
4
5
6
7
8
9
10
11
 // webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
}),
new webpack.optimize.UglifyJsPlugin(),
],
};

optimization.nodeEnv: 'production' 选项和 DefinePlugin 插件采用相同的方式来解决这个问题 - 这个方式就是他们将 process.env.NODE_ENV 替换成特定的值,下面的配置可以说明:

1.Webpack 会将所有 process.env.NODE_ENV 替换成 "production"

1
2
3
4
5
6
7
// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}

1
2
3
4
5
6
7
// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if ("production" !== 'production') {
warn('props must be strings when using array syntax.');
}

2.与此同时最小化工具会移除掉所有 if 的条件分支 - 由于 "production" !== 'production' 永远会返回 false,这样分支内的代码就永远不会执行:

1
2
3
4
5
6
7
// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if ("production" !== 'production') {
warn('props must be strings when using array syntax.');
}

1
2
3
4
5
// vue/dist/vue.runtime.esm.js (without minification)
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
}
Further Reading 扩展阅读

Use ES Modules 使用 ES 模块

下面这个方式利用 ES modules 减小前端体积。

当你使用 ES module,webpack 有能力去做 tree-shaking。Tree-shaking 贯穿了整个依赖树,检查哪些依赖被使用,同时移除掉无用依赖。因此,如果你使用 ES module 方式的时候,webpack 帮你可以排除掉无用代码:

1.一个有多个 export 的文件,但是 app 只需要其中一个:

1
2
3
4
5
6
7
// comments.js
export const render = () => { return 'Rendered!'; };
export const commentRestEndpoint = '/rest/comments';

// index.js
import { render } from './comments.js';
render();

2.Webpack 分析 commentRestEndPoint 没有被用到,就不会在一个 bundle 中生成单独的 export:

1
2
3
4
5
6
7
8
9
// bundle.js (part that corresponds to comments.js)
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
const render = () => { return 'Rendered!'; };
/* harmony export (immutable) */ __webpack_exports__["a"] = render;

const commentRestEndpoint = '/rest/comments';
/* unused harmony export commentRestEndpoint */
})

3.最小化工具就会移除掉无用变量:

1
2
// bundle.js (part that corresponds to comments.js)
(function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})

如果他们都是有 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-env or babel-preset-es2015,检查一下当前的配置。默认情况下, ES import and export to CommonJS require and module.exports。通过设置 option 来禁止掉Pass the { modules: false } option

Futher reading 扩展阅读

Optimize images 优化图片

图片基本会占局页面一半以上体积。虽然它们不像 JavaScript 那么重要(比如它们不会阻止页面渲染),但图片仍然会占用掉一大部分带宽。可以利用 url-loadersvg-url-loaderimage-webpack-loader 来进行优化。

url-loader 允许将小的静态文件打包进 app。没有配置的话,他需要通过传递文件,将它放在编译后的打包 bundle 内并返回一个这个文件的 url。然而,如果我们注明 limit 选项,它将会编码成更小的文件 base64 url 并返回这个 url。这样将图片放在 Javascript 代码中,可以节省 HTTP 的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};
1
2
3
4
5
6
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`

Note: 内联图片减少了独立请求的数量,这是很好的方式(even with HTTP/2),但是会增加 bundle下载和转换的时间和内存的消耗。一定要确保不要嵌入超大图片或者较多的图片 - 否则增加的 bundle 的时间将会掩盖做成内联图片的收益。

svg-url-loaderurl-loader类似 - 都是将使用 URL encoding encode 文件。这对对于 SVG 图片很奏效 - 因为 SVG 文件是文本,encoding 在体积上更有效率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: 'svg-url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
// Remove the quotes from the url
// (they’re unnecessary in most cases)
noquotes: true,
},
},
],
},
};

Note: svg-url-loader 拥有改善 IE 浏览器支持的 options,但是在其他浏览器中更糟糕。如果你需要兼容 IE 浏览器,设置 iesafe: true 选项

image-webpack-loader压缩图片使之变小。它支持 JPG,PNG,GIF 和 SVG,因为我们将会使用它所有类型。

这个 loader 不会将图片嵌入在应用内,因此它必须与url-loadersvg-url-loader配合使用。避免复制粘贴到相同的 rules 中(一个用于 JPG/PNG/GIF 图片,另一个用于 SVG 图片),我们来使用enforce: pre作为单独的一个 rule 涵盖这个 loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre',
},
],
},
};

默认 loader 设置就已经可以满足需求了 - 但如果你想要深入配置,请查看 the plugin options。为了选择哪些 options 需要明确,可以查看 Addy Osmani 的 guide on image optimization

Further reading 扩展阅读

Optimize dependencies 优化依赖

平均一半以上的 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

Enable module concatenation for ES modules (aka scope hoisting) 为 ES modles 开启模块连接

Note: 如果你在使用生产模式下的 webpack 4,modules concatention 已经开启,可以直接跳过本小节。

当你构建 bundle 时,webpack 将每一个 module 封装进 function 中:

1
2
3
4
5
6
7
8
// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
console.log('Rendered!');
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bundle.js (part  of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();

}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_exports__["a"] = render;
function render(data, target) {
console.log('Rendered!');
}

})

在以前,这么做是使 CommonJS/AMD modules 互相分离所必须的。但是,这会增加体积并且性能表现堪忧。

Webpack 2 介绍了 ES modules 的支持,不像 CommonJS 和 AMD modules 一样,而是能够不用将每一个 module 用 function 封装起来。同时 Webpack 3 利用ModuleConcatenationPlugin完成这样一个 bundle,下面是例子:

1
2
3
4
5
6
7
8
// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
console.log('Rendered!');
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// 与前面的代码不同,这个 bundle 只有一个 module,同时包含两个文件

// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}

// CONCATENATED MODULE: ./index.js
render();

})

看到区别了吗?在这个 bundle 中, module 0 需要 module 1 的 render 方法。使用 ModuleConcatenationPluginrequire被直接简单的替换成 require 函数,同时 module 1 被删除删除掉了。这个 bundle 拥有更少的 modules,就有更少的 modules 损耗!

Webpack 4 中开启这个功能,启用 optimization.concatenateModules 选项即可:

1
2
3
4
5
6
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true,
},
};

webpack 3 中,使用 ModuleConcatenationPlugin 插件:

1
2
3
4
5
6
7
8
// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
],
};

Note:想要知道为什么这个功能不是默认启用?Concatenating modules 很棒, 但是他会增加编译的时间同时破坏 module 的热更新。这就是为什么只在生产环境中启用的原因了。

Further reading 扩展阅读

Use externals if you have both webpack and non-webpack code 如果代码中包含 webpack 和非 webpack 的代码要使用 externals

你可能拥有一个体积庞大的工程,其中一部分代码可以使用 webpack 编译,而有一些代码又不能。比如一个视频网站,播放器的 widget 可能通过 webpack 编译,但是其周围页面区域可能不是:

video-hosting

如果两部分代码有相同的依赖,你可以共享这些依赖以便减少重复下载耗时。the webpack’s externals option就干了这件事 - 它用变量或者外部引用来替代 modules。

如果依赖是挂载到 window 上的情况

如果你的非 webpack 代码依靠这些依赖,它们是挂载 window 上的变量,可以将依赖名称 alias 成变量名:

1
2
3
4
5
6
7
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
},
};

利用这个配置,webpack 将不会打包 reactreact-dom 包。取而代之,他们会被替换成下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})

如果依赖是当做 AMD 包被加载的情况

如果你的非 webpack 代码没有将依赖暴露挂载到 window 上,这就更复杂了。但是如果非 webpack 代码使用 AMD 包的形式消费了这些依赖,你仍然可以避免重复的代码加载两次。

具体如何做呢?将 webpack 代码编译成一个 AMD module 同时别名成一个库的 URLs:

1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
output: { libraryTarget: 'amd' },

externals: {
'react': { amd: '/libraries/react.min.js' },
'react-dom': { amd: '/libraries/react-dom.min.js' },
},
};

Webpack 将会把 bundle 包装进 define()同时让它依赖于这些URLs:

1
2
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });

如果非 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' 是同一个库,这样比较谨慎。

Further reading 扩展阅读

Summing up 总结

  • Enable the production mode if you use webpack 4 如果使用 webpack 4 开启生产模式
  • Minimize your code with the bundle-level minifier and loader options 使用 bundle 级别最小化 和 loader 选项来最小化你的代码
  • Remove the development-only code by replacing NODE_ENV with production 通过将 NODE_ENV 替换成 production 来移除开发期间代码
  • Use ES modules to enable tree shaking 启用 tree shaking
  • Compress images 压缩图片
  • Apply dependency-specific optimizations 开启依赖优化
  • Enable module concatenation 开启 module 连接
  • Use externals if this makes sense for you 如果有效果的话可以使用 externals

Make use of long-term caching 利用好长时缓存

作者 Ivan Akulov

在做完优化应用体积之后的下一步提升应用加载时间的就是缓存。在客户端中使用缓存作为应用的一部分,这样会在每一次请求中减少重新下载的次数。

Use bundle versioning and cache headers 使用 bundle 版本和缓存头信息

做缓存通用的解决办法:

1.告诉浏览器缓存一个文件很长时间(比如一年)

1
2
# Server header
Cache-Control: max-age=31536000

Note:如果你不熟悉 Cache-Control 做了什么,你可以看一下 Jake Archibald 的精彩博文 on caching best practices

2.当文件改变需要强制重新下载时去重命名这些文件

1
2
3
4
5
<!-- Before the change -->
<script src="./index-v15.js"></script>

<!-- After the change -->
<script src="./index-v16.js"></script>

这些方法可以告诉浏览器下载这些 JS 文件,将其缓存起来。浏览器将只会在文件名发生改变时才会请求网络(或者缓存失效的情况也会请求)。

使用 webpack,也可以做同样的事,但可以使用版本号来解决,需要明确这个文件的 hash 值。使用 [chunkhash] 可以将 hash 值包含进文件名中:

1
2
3
4
5
6
7
8
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.[chunkhash].js',
// → bundle.8e0d62a03.js
},
};

Note: webpack 可能会生成不同的 hash 即使 bundle 相同 - 比如你重名了了一个文件或者重新在不同的操作系统下编译了一个 bundle。 This is a bug.
如果你需要将文件名发送给客户端,也可以使用 HtmlWebpackPlugin 或者 WebpackManifestPlugin

HtmlWebpackPlugin 使用起来很简单,但灵活性有一些欠缺。编译时,插件会生成一个 HTML 文件,这其中包括所有的编译后的资源文件。如果你的业务逻辑不复杂,这就非常适合你:

1
2
3
4
<!-- index.html -->
<!doctype html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin 更灵活一些,它可以帮助你解决业务负责的部分。编译时它会生成一个 JSON 文件,这文件保存这没有 hash 值文件与有 hash 文件之间的映射。服务端利用这个 JSON 可以识别出那个文件有效:

1
2
3
4
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
Further reading 扩展阅读

Extract dependencies and runtime into a separate file 将依赖和运行环境代码提取到一个单独的文件

Dependencies 依赖

App 依赖通常情况下趋向于比实际 app 内代码中更少的变化。如果你将他们移到独立的文件中,浏览器将可以把他们独立缓存起来 - 同时不会每次 app 代码改变时重新下载。

Key Term: 在 webpack 的技术中,利用 app 代码拆分文件被称为 chunks。我们后面会用到这个名词。

为了将依赖包提取到单独的 chunk 中,下面分为三步:

1.使用 [name].[chunkname].js 替换output的文件名:

1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
output: {
// Before
filename: 'bundle.[chunkhash].js',
// After
filename: '[name].[chunkhash].js',
},
};

当 webpack 构建应用时,它会用一个带有 chunk 的名称来替换 [name]。如果没有添加 [name] 部分,我们不得不通过 chunks 之间的 hash 区别来比较他们的区别 - 那就太困难了!

2.将 entry 转成一个对象:

1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
// Before
entry: './index.js',
// After
entry: {
main: './index.js',
},
};

在这段代码中,”main” 对象是一个 chunk 的名字。这个名字将会被步骤 1 里面的 [name]代替。

目前为止,如果你构建一个 app,chunk 就会包括整个 app 的代码 - 就像我们没有做这些步骤一样。但是很快就会产生变化。

3.在 Webpack 4 中,在配置中增加 optimization.splitChunks.chunks: 'all' 即可:

1
2
3
4
5
6
7
8
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
}
},
};

这个选项会开启智能代码拆分。使用这个功能,webpack 将最小化和 Gzip 前大于 30KB 的代码提取出额外的 vendor 代码。它同时也会提取出 common 代码 - 这些代码在打包多个 bundles 会起到作用。(例如:通过路由拆分应用)。

Webpack 3 中,使用 CommonsChunkPlugin 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// A name of the chunk that will include the dependencies.
// This name is substituted in place of [name] from step 1
name: 'vendor',

// A function that determines which modules to include into this chunk
minChunks: module => module.context &&
module.context.includes('node_modules'),
}),
],
};

插件将包括全部 node_modules 路径下的 modules 同时将他们移到一个单独的文件中,这个文件被称为 vendor.[chunkhash].js

完成了上面的步骤,每一次 build 都会生成两个文件。浏览器会将他们单独缓存 - 以便代码发生改变时重新下载。

1
2
3
4
5
6
7
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor

Webpack runtime code 运行时代码

不幸的是,仅仅抽取 vendor 是不够的。如果你试图在应用代码中修改一些东西:

1
2
3
4
5
6
// index.js



// E.g. add this:
console.log('Wat');

你会注意到 vendor 的也会改变:

1
2
                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor

1
2
                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor

这因为 webpack 打包时,一部分 modules 的代码,拥有 a runtime - 管理模块执行一部分代码。当你将代码拆分成多个文件时,这小部分代码在 chunk ids 和 匹配的文件之间开始了一个映射:

1
2
3
4
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack 将最新生成的 chunk 包含在这个 runtime 内,这个 chunk 就是我们代码中的 vendor。与此同时每一次任何 chunk 的修改,即使这一小部分代码也改变,也会导致整个 vendor chunk 改变。

为了解决这个问题,我们将 runtime 转义到一个独立的文件中,在 Webpack 4 中,开启 optimization.runtimeChunk 选项:

1
2
3
4
5
6
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true,
},
}

Webpack 3中,通过 CommonsChunkPlugin 创建一个额外的空的 chunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',

minChunks: module => module.context &&
module.context.includes('node_modules'),
}),

// This plugin must come after the vendor one (because webpack
// includes runtime into the last chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',

// minChunks: Infinity means that no app modules
// will be included into this chunk
minChunks: Infinity,
}),
],
};

完成这一部分改变,每一次 build 都将生成三个文件:

1
2
3
4
5
6
7
8
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

将他们反过来顺序添加到 index.html 中,你就搞定了:

1
2
3
4
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>
Further reading 扩展阅读

Inline webpack runtime to save an extra HTTP request 内联 webpack runtime 节省额外的 HTTP 请求

为了做的更好,我们可以尽力把 webpack runtime 内联在 HTML 请求里。下面举例:

1
2
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

这样做:

1
2
3
4
<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

这个 runtime 很小,内联它可以帮助你节省 HTTP 请求(尤其对 HTTP/1 重要;但是在 HTTP/2 就没有那么重要了,但是仍能够提高效率)。

下面就来看看如何做。

如果使用 HtmlWebpackPlugin 来生成 HTML

如果使用 HtmlWebpackPlugin 来生成 HTML 文件,InlineChunkWebpackPlugin 就足够了。

如果使用自己的定制服务逻辑来生成 HTML

Webpack 4

1.增加 WebpackManifestPlugin 插件已知运行时 chunk:

1
2
3
4
5
6
7
8
// webpack.config.js (for webpack 4)
const ManifestPlugin = require('webpack-manifest-plugin');

module.exports = {
plugins: [
new ManifestPlugin(),
],
};

插件就会生成一个下面这样的文件:

1
2
3
4
// manifest.json
{
"runtime~main.js": "runtime~main.8e0d62a03.js"
}

2.将这些内容嵌入到 runtime chunk 中。例如:使用 Node.js 和 Express:

1
2
3
4
5
6
7
8
9
10
11
12
13
// server.js
const fs = require('fs');
const manifest = require('./manifest.json');

const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');

app.get('/', (req, res) => {
res.send(`

<script>${runtimeContent}</script>

`);
});
Webpack 3

1.将 runtime 名称改成静态的明确的文件名:

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
minChunks: Infinity,
filename: 'runtime.js',
// → Now the runtime file will be called
// “runtime.js”, not “runtime.79f17c27b335abc7aaf4.js”
}),
],
};

2.嵌入到 runtime.js 内容。比如:Node.js 和 Express

1
2
3
4
5
6
7
8
9
10
11
// server.js
const fs = require('fs');
const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');

app.get('/', (req, res) => {
res.send(`

<script>${runtimeContent}</script>

`);
});

Lazy-load code that you don’t need right now 懒加载

通常情况下,一个页面有或多或少的重要部分:

  • 如果你在 YouTube 上加载一个视频页面,相比评论区域你更在乎视频区域。这就是视频要比评论区域重要。
  • 如果你在一个新闻网站打开一个报道,相比广告区域你更关心文章的内容。这就是文字比广告更重要。

在这些案例中,通过仅下载最重要的部分,懒加载剩余区域能够提升最初的加载性能。使用 the import() functioncode-splitting 解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});

import()明确表示你期望动态地加载独立的 module。当 webpack 看到 import('./module.js')时,他就会将这个 module 移到独立的 chunk 中:

1
2
3
4
5
6
7
8
9
$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

并且只在代码执行到 import() 才会下载。

这将会让 main bundle 更小,提升初始加载的时间。更重要的是改进缓存 - 如果你修改 main chunk 的代码,其他部分的 chunk 也不会受影响。

Note: 如果使用 Babel 编译代码,你会因为 Babel 还不认识 import() 而遇到语法错误抛出来。可以使用 syntax-dynamic-import 解决这个错误。

Further reading 扩展阅读

Split the code into routes and pages 拆分代码到路由和页面中

如果你的应用拥有多个路由或者页面,但是代码中只有单独一个 JS 文件(一个单独的 main chunk),这看起来你正在每一个请求中节省额外的 bytes 带宽。举个例子,当用户正在访问你网站的首页:

site-home-page

他们并不需要加载另外不同的页面上渲染文章标题的的代码 - 但是他们还是会加载到这段代码。更严重的是如果用户经常只访问首页,同时你还经常改变渲染文章标题的代码,webpack 将会对整个 bundle 失效 - 用户每次都会重复下载全部 app 的代码。

如果我们将代码拆分到页面里(或者单页面应用的路由里),用户就会只下载对他有意义的代码。更好的是,浏览器也会更好地缓存代码:当你改变首页的代码时,webpack 只会让相匹配的 chunk 失效。

For single-page apps 对于单页面应用

通过路由拆分带页面引用,使用 import()(看看 “Lazy-load code that you don’t need right now”这部分)。如果你在使用一个框架,现在已经有成熟的方案:

For traditional multi-page apps 对于传统的多页面应用

通过页面拆分传统多页面应用,可以使用 webpack 的 entry points 。如果你的应用有三种页面:主页、文章页、用户账户页,那就分厂三个 entries:

1
2
3
4
5
6
7
8
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
},
};

对于每一个 entry 文件,webpack 将构建出独立的依赖树,并且声称一个 bundle,它将通过 entry 来只包括用到的 modules:

1
2
3
4
5
6
7
8
9
10
11
$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime

因此,如果仅仅是文章页使用 Lodashhomeprofile 的 bundle 将不会包含 lodash - 同时用户也不会在访问首页的时候下载到这个库。

拆分依赖树也有缺点。如果两个 entry points 都用到了 loadash ,同时你没有在 vendor 移除掉依赖,两个 entry points 将包括两个重复的 lodash 。在 Webpack 4 中我们可以设置 optimization.splitChunks.chunks: 'all' 解决该问题:

1
2
3
4
5
6
7
8
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
}
},
};

这个选项可以开启智能拆分代码,webpack 将自动寻找 common code 并将其提取到一个单独的文件中。

Webpack 3 可以使用CommonsChunkPlugin来解决这个问题 - 它会将通用的依赖转移到一个独立的文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// A name of the chunk that will include the common dependencies
name: 'common',

// The plugin will move a module into a common file
// only if it’s included into `minChunks` chunks
// (Note that the plugin analyzes all chunks, not only entries)
minChunks: 2, // 2 is the default value
}),
],
};

随意使用minChunks的值来找到最优的选项。通常情况下,你想要它尽可能体积小,但它会增加 chunks 的数量。举个例子,3 个 chunk,minChunks 可能是 2 个,但是 30 个 chunk,它可能是 8 个 - 因为如果你把它设置成 2 ,过多的 modules 将会打包进一个通用文件中,文件更臃肿。

Further reading 扩展阅读

Make module ids more stable 使用稳定的 module ids

当编译代码时,webpack 会分配给每一个 module 一个 ID。之后,这些 ID 就会被 require() 引用到 bundle 内部。你可以在编译输出的右侧在 moudle 路径之前看到这些 ID:

1
2
3
4
5
6
7
8
9
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

1
2
3
4
5
6
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

默认情况下,这些 ID 是使用计数器计算出来的(比如第一个 module 是 ID 0,第二个 moudle 就是 ID 1,以此类推)。这样的问题就在于当你新增一个 module 事,它会出现在原来 module 列表中的中间,改变后面所有 module 的 ID:

1
2
3
4
5
6
7
8
9
10
11
12
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ 我们增加一个新 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
2
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module

这将使包含或依赖于具有更改ID的模块的所有块无效 - 即使它们的实际代码没有更改。在我们的代码中,_0_ 这个 chunk 和 main chunk 都会失效 - 只有 main 才应该失效。

使用HashedModuleIdsPlugin插件改变module ID 如何计算来解决这个问题。它利用 module 路径的 hash 来替换掉计数器:

1
2
3
4
5
6
7
8
9
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime

1
2
3
4
5
6
7
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module

有了这个方法,只有你重命名或者删除这个 moudle 它的 ID 才会变化。新的 modules 不会因为 module ID 互相影响。

启用这个插件,在配置中增加 plugins

1
2
3
4
5
6
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin(),
],
};
Further reading 扩展阅读

Summing up

  • Cache the bundle and differentiate between versions by changing the bundle name 缓存 bundle 包并通过修改 bundle 名称来做版本差异
  • Split the bundle into app code, vendor code and runtime 将 bundle 拆分成 app 业务代码、vendor 代码、runtime 代码
  • Inline the runtime to save an HTTP request 将 runtime 代码内联节省 HTTP 请求
  • Lazy-load non-critical code with import 通过 import 懒加载非必要代码
  • Split code by routes/pages to avoid loading unnecessary stuff 通过路由或页面拆分阻止加载不必要代码

Monitor and analyze the app 监控并分析

作者 Ivan Akulov

即使当你配置好你的 webpack 让你的应用尽可能体积较小的时候,跟踪这个应用就非常重要,同时了解里面包含了什么。除此之外,你安装一个依赖,它将让你的 app 增加两倍大小 - 但并没有注意到这个问题!

这一部分就来讲解一些能够帮助你理解你的 bundle 的工具。

Keep track of the bundle size 跟踪打包的体积

在开发时可以使用webpack-dashboard和命令行bundlesize 来监控 app 的体积。

webpack-dashboard

webpack-dashboard可以通过依赖体积大小、进程和其他细节来改进 webpack 的输出。

webpack-dashboard

这个 dashborad 帮助我们跟踪大型依赖 - 如果你增加一个依赖,你就立刻能在 Modules section 始终看到它!

启用这个功能,需要安装 webpack-dashboard 包:

1
npm install webpack-dashboard --save-dev

同时在配置的 plugins 增加:

1
2
3
4
5
6
7
8
// webpack.config.js
const DashboardPlugin = require('webpack-dashboard/plugin');

module.exports = {
plugins: [
new DashboardPlugin(),
],
};

或者如果正在使用基于 Express dev server 可以使用 compiler.apply()

1
compiler.apply(new DashboardPlugin());

多尝试 dashboard 找出改进的地方!比如,在 modules section 滚动找到那个库体积过大,把它替换成小的可替代的库。

bundlesize

bundlesize 可以验证 webpack assets 不超过指定的大小。通过自动化 CI 就可以知晓 app 是否变的过于臃肿:

bundlesize

配置如下:

Find out the maximum sizes 找出最大体积

1.分析 app 尽可能减小体积,执行生产环境的 build。
2.在package.json中增加bundlesize部分:

1
2
3
4
5
6
7
8
// package.json
{
"bundlesize": [
{
"path": "./dist/*"
}
]
}

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%的幅度可以让你像往常一样开发应用程序,同时警告你,当它的大小增长太多。

Enable bundlesize 启用 bundlesize

5.安装bundlesize开发依赖

1
npm install bundlesize --save-dev

6.在package.json中的bundlesize部分,声明具体的最大值。对于某一些文件(比如图片),你可以单独根据文件类型来设置最大体积大小,而不需要根据每一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package.json
{
"bundlesize": [
{
"path": "./dist/*.png",
"maxSize": "16 kB",
},
{
"path": "./dist/main.*.js",
"maxSize": "20 kB",
},
{
"path": "./dist/vendor.*.js",
"maxSize": "35 kB",
}
]
}

7.增加一个 npm 脚本来执行检查:

1
2
3
4
5
6
// package.json
{
"scripts": {
"check-size": "bundlesize"
}
}

8.配置自动化 CI 来在每一次 push 时执行npm run check-size做检查。(如果你在 Github 上开发项目,直接可以使用integrate bundlesize with GitHub。)

这就全部了!现在如果你运行npm run check-size或者 push 代码,你就会看到输出的文件是否足够小:

bundlesize-output-success

或者下面失败的情况

bundlesize-output-failure

Further reading 扩展阅读

Analyze why the bundle is so large 分析 bundle 为什么这么大

你想要深挖 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
2
3
4
5
6
7
8
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
};

运行生产环境的 build 这个插件就会在浏览器中打开一个显示状态的页面。

默认情况下,这个页面会显示语法分析后的文件体积(在 bundle 出现的文件)。您可能想比较 gzip 的大小,因为这更接近实际用户的体验;使用左边的边栏来切换尺寸。

Note: 如果你使用 ModuleConcatenationPlugin,它可能在webpack-bundle-analyzer输出时合并一部分 module,使得报告小一些细节。如果你使用这个插件,在执行分析的时候需要禁用掉。

下面是报告中需要看什么:

  • Large dependencies 大型依赖 为什么体积这么大?是否有更小的替代包(比如 Preact 替代 React)?用了全部代码(比如 Moment.js 包含大量的本地变量 that are often not used and could be dropped)?
  • Duplicated dependencies 重复依赖 是否在不同文件中看到过相同的库?(在 Webpack 4 中配置 optimization.splitChunks.chunks,或者在 Webpack 3中 使用 CommonsChunkPlugin 将他们移到一个通用文件内)亦或是在同一个库中 bundle 拥有多个版本?
  • Similar dependencies 相似依赖 是否存在有相似功能的相似库存在?(比如momentdate-fns 或者 lodashlodash-es)尽力汇总成一个。

同样的,也可以看看 Sean Larkin 的文章 great analysis of webpack bundles

Summing up 小结

  • Use webpack-dashboard and bundlesize to stay tuned of how large your app is
  • Dig into what builds up the size with webpack-bundle-analyzer

Conclusion

总结:

  • 剔除不必要的体积 把所有的代码都压缩最小化,剔除无用代码,增加依赖时保持谨慎小心。
  • 通过路由拆分代码 只在真正需要的时候才加载,其余部分做懒加载。
  • 缓存代码 应用程序某些部分代码更新频率低于其他部分代码,可以将这些部分拆分成文件,以便在必要时仅重新下载。
  • 跟踪体积大小 使用 webpack-dashboardwebpack-bundle-analyzer 监控你的 app。每隔几个月重新检查一下你的应用的性能。

Webpack 不仅仅是一个帮助你更快创建 app 的工具。它还帮助使你的 app 成为 a Progressive Web App ,你的应用拥有更好的体验以及自动化的填充工具就像Lighthouse根据环境给出建议。

不要忘记阅读 webpack docs - 里面提供了大量的优化相关的信息。

多多练习 with the training app

]]>
<p>作者 <a href="https://developers.google.com/web/resources/contributors/addyosmani" target="_blank" rel="noopener">Addy Osmani</a>,<a href="https://developers.google.com/web/resources/contributors/iamakulov" target="_blank" rel="noopener">Ivan Akulov</a></p> <p>原文 <a href="https://developers.google.com/web/fundamentals/performance/webpack/" target="_blank" rel="noopener">https://developers.google.com/web/fundamentals/performance/webpack/</a></p> <blockquote> <p>PS. 在 20180211 笔者翻译过一次,当时也没有完全理解和使用文中提到的优化项,近期工作中因为用到 Webpack 4.x 对生产环境进行打包,加深了一些理解,本译文对原有译文补充的 Webpack 4 内容,同时对原译文进行了校对和一些细节措辞的修改。</p> </blockquote>
【译】Webpack 4 mode and optimization https://beanlee.github.io/posts/blog-translate-webpack-4-mode-and-optimization/ 2018-04-18T06:26:11.000Z 2018-06-20T01:31:47.000Z 原文 https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a

作者 Tobias Koppers

webpack 增加了一个模式配置(mode option)。下面来解释一下具体内容。

webpack 4 增加了一个模式选项(mode option)。并且这个配置是必须存在的。(实际上它不是必须的配置项,但如果你省略掉没有配就会有警告。)

webpack 4 目前有两个默认配置developmentproduction 。这两个值可以在 mode 中设置。

设置 development 将会带给你最好*的开发体验,它专注于以下几点:

  • 浏览器 debug 工具
  • 开发周期内的快速增量编译
  • 运行时有用的错误信息

(*实际上这里指最好的体验取决于你使用的方式,我们尽力在这里囊括了大多数通用部分)

设置 production 将会带给你在发布应用时的一系列有用的默认设置,它专注于以下几点:

  • 更小的输出包尺寸
  • 运行时高效的代码
  • 忽略掉只在开发时启用(development-only)的代码
  • 不会暴露源码或者文件路径
  • 简化使用打包后资源过程

最后一点非常重要。它基本意味着 production 会提供给你优化后的资源包,但这不是完美的优化有的资源包。这里还存在着很多可优化点,但是它们会让结果更难使用。这些优化点是有意被忽略的,因为在这里,我们更看重入门的体验高于完美的优化。大部分优化点也只在大型应用上起作用。


随着这个 mode 配置的引入,我们尝试去减少 build 时必要配置。我们尝试着去用一些默认项去覆盖通用的配置。

然而从我们的经验来看我们也知道默认配置并不适用于所有人。不同的团队有不同的需求,有时候会因为遗留代码、有时候会因为个人喜好、有时候会因为特殊的应用或者有时候使用者会认为这并不是最好通用解决方案。许多人确实想要修改默认配置以适应他们自己的项目。We got you covered. 增加 mode 并不意味这我们移除掉了这些配置。这里面仍然是一切皆可配置。实际上我们使内部大多数优化都是可配置的(你可以禁用掉他们)。

mode 选项是通过在配置中设置默认项实现的。这个不会被其他配置执行的 mode 也不会做任何行为。

下一部分就会更深入地讨论因为mode或其他选项而影响的配置。


devtool

development 模式下默认是 eval。否则不使用 devtool。

eval devtool 没有提供最佳的质量,但是拥有很好的性能。这就是我们选择的折中方案。看一看这些配置的文档,这可以获取更高质量的 sourcemap。

  • 📉📉📉缺点:慢,bundle 体积大
  • 📈📈📈优点:优化 debug 的体验

cache

只在 development 模式下启用,否则禁用缓存。

缓存模块可以避免在没有改变时重建。

在内存缓存只在 watch 模式下有用,并且我们假设你在开发时正在使用 watch 模式。不用缓存时,内存占用率更低。

  • 📉缺点:内存占用
  • 📈📈📈优点:更快的增量编译

module.unsafeCache

只在 cache 启用时启用,否则禁用。

缓存机械依赖项可以避免重新解析它们。

  • 📉缺点:内存占用,缓存入口可能错误
  • 📈📈📈优点:更快的增量编译

output.pathinfo

只在 development 模式下启用,否则禁用。

这些额外的注释对于 debug 很有作用,尤其是使用 eval devtool。

  • 📉缺点:bundle 体积大,暴露路径信息
  • 📈优点:提升生成代码的可阅读性

performance

只在 production 模式下启用,否则禁用。

体积限制只对最小化资源起作用,同时伴随着性能开销。因此它只在生产模式下启用。

  • 📉缺点:算法消耗
  • 📈优点:对打包 bundle 体积大小产出警告

optimization.removeAvailableModules

总是开启。

当他们在父级 chunk groups 中都可用时,这些模块会被移除掉。它可以减少资源包的体积。因为有更少的代码生成,更小的资源包就意味着更快的 build 过程。

  • 📉📉缺点:算法消耗
  • 📈📈📈优点:bundle 体积优化

optimization.removeEmptyChunks

总是开启。

空 chunks 会被移除掉。这些在文件系统中减少 load 会导致更快的 build。

  • 📉缺点:算法消耗
  • 📈📈📈优点:更少的请求

optimization.mergeDuplicateChunks

总是开启。

相等的 chunks 会被合并。结果就是更少的生成代码,更快的 build。

  • 📉缺点:算法消耗
  • 📈📈📈优点:更少的请求和下载

optimization.flagIncludedChunks

只在 production 模式下开启,否则禁用。

确定作为其他 chunks 子集的 chunks,并且这样方式对齐标记,即当加载较大 chunks 时,不必加载子集。

  • 📉缺点:算法消耗
  • 📈📈📈优点:更少的请求和下载

optimization.occurrenceOrder

只在 production 模式下开启,否则禁用。

提供更常用的 ids 更小(更短)的值。

  • 📉缺点:算法消耗
  • 📈优点:bundle 体积优化

optimization.providedExports

总是开启。

尽可能地确定每一个 module 的 exports。这个信息被用于其他优化或者生成代码。为了消除歧义:为 export * from 生成更有效率的代码。

  • 📉缺点:算法消耗
  • 📈优点:bundle 体积优化,其他优化需求

optimization.usedExports

只在 production 模式下开启,否则禁用。

尽可能地确定每一个 module 的 exports。它依赖于 optimization.providedExports。这个信息被用于其他优化或者生成代码。消除歧义:导出不会生成无用的 exports, 当所有的用途都被兼容的时候导出 names 就是零碎的单独字符定义。在最小化中 DCE 会有一处,同时移除掉无用的 exports。

  • 📉📉缺点:算法消耗
  • 📈📈优点:bundle 体积优化

optimization.sideEffects

只在 production 模式下开启,否则禁用。

认可在 package.jsonrules 中的 sideEffects 标志去消除 modules。 它依赖 optimization.providedExportsoptimization.usedExports。这些依赖都有所开销,但是消除依赖通过减少生成代码在性能上有积极的影响。它也依赖于你自己的代码。为了更好的性能,去尝试吧。

  • 📉缺点:算法消耗
  • 📈📈📈优点:bundle 体积优化,更少生成代码

optimization.concatenateModules

只在 production 模式下开启,否则禁用。

尝试查找模块图中可以安全连接到单个模块中的段。它依赖于optimization.providedExportsoptimization.usedExports

  • 📉📉📉缺点:额外的语法分析,作用域分析和定义重命名
  • 📈📈📈优点:运行时性能、bundle 体积优化

optimization.splitChunks

总是开启。

查找在 chunks 之间哪些 module 被共享,同时将他们拆分到独立的 chunks 中,目的是减少重复或者从 application modules 中分离 vendor modules。

  • 📉缺点:算法消耗,额外的请求
  • 📈📈📈优点:更少生成代码,更好的缓存,更少的下载请求

optimization.runtimeChunk

总是开启。

为 webpack 运行时和 chunk manifest 创建一个独立的 chunk。这个 chunk 应该内联到 HTML 中。

  • 📉缺点:更大的 HTML 文件
  • 📈优点:更好的缓存

optimization.noEmitOnErrors

只在 production 模式下开启,否则禁用。

当发生编译错误的时候不输出资源包。

  • 📉缺点:无法使用应用程序的起作用的部分
  • 📈优点:没有损坏的 bundles

optimization.namedModules

只在 development 模式下开启,否则禁用。

取代数值型 ID,提供给 module 更有用的命名。

  • 📉缺点:bundle 体积增加
  • 📈优点:更好的错误报告和 debug

optimization.nodeEnv

mode 值得默认项:development 或者 production

定义 process.env.NODE_ENV 成为编译时常量值。这就允许移除掉 development only 的代码。

  • 📉缺点:开发环境的代码与生产环境的代码不同
  • 📈📈优点:减小 bundle 体积,提高运行效率

optimization.minimize

只在 production 模式下开启,否则禁用。

使用最小化工具来压缩输出的资源包,比如(optimization.minimizer默认使用的uglify-js)。

  • 📉📉📉缺点:编译速度降低
  • 📈📈📈优点:减小 bundle 体积

optimization.portableRecords

在文件内记录时启用,否则禁用。

记录中使用的标识与上下文目录有关。

  • 📉缺点:影响速度降低
  • 📈优点:记录与目录无关
]]>
<p>原文 <a href="https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a" target="_blank" rel="noopener">https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a</a></p> <p>作者 <a href="https://medium.com/@sokra" target="_blank" rel="noopener">Tobias Koppers</a></p> <p>webpack 增加了一个模式配置(mode option)。下面来解释一下具体内容。</p>
移动端 HTTPS 抓包工具配置说明 https://beanlee.github.io/posts/mobile-https-config/ 2018-03-29T10:53:55.000Z 2018-06-20T01:31:47.000Z 投稿部门公众号,做了一些微调,增加了 Android 机型的一些说明。

2018-03-01 Beanlee

相信各位在移动端开发过程中一定遇到抓取数据包、拦截请求的场景,本文主要介绍移动端在针对 HTTPS 抓包时的几款软件的配置(包括CharlesFiddlerWhistle),下面举例将已 iOS 11 为例,附带 Android 机型配置截图。

Charles

  • 官网
  • 按照 license 收费,收费后终身享受升级,仅限 OS X 平台

HTTP 抓包配置

1.确保手机与主机处于同一局域网内。

查看本机 IP,使用 ifconfig 命令查看本机 IP;或者打开系统设置查看 IP 如截图;

例如:当前本机 IP 192.168.191.4

ifconfig

wifi-config

2.设置手机 iOS 网络代理

设置 -> 无线局域网 -> HTTP 代理 -> 手动

服务器:192.168.191.4 端口:8888

ios&andoird-proxy-conf

3.配置完成后,在手机端访问任何一个 App 或网页,电脑端 Charles 会弹出提示,点击 Allow

此时可以在应用的界面中看到 HTTP 的请求数据了。

接下来继续 HTTPS 的配置。

HTTPS 抓包配置

1.信任证书 本机

打开 Charles 后如下图操作,电脑端信任证书,注意 此处选择system

charles-install-root-cer

charles-install-root-cer-system

2.信任证书 手机

打开 Safari 地址输入 192.168.191.4:8888chls.pro/ssl 点击安装,此时会弹出要求需要输入手机密码,完成后证书安装成功。

此时查看 通用 -> 描述文件 中 charles proxy ca 已变成已验证

注意 自 iOS 10.3 以上,还需要多一步操作,手动信任自定义根证书,才能确保证书安装并已启用;

关于本机 -> 证书信任设置 启用 charles proxy ca 截图详见文章底部锦囊

注意 Android 手机不需要安装 Charles 证书 ssl-certificates

下面截图中是 Android 仅安装了 Fiddler 和 Whistle 的证书截图。

android-cert

3. Charles 设置启用 SSL Proxy

Proxy->SSL Proxy setting Enabled SSL Proxying

同时添加希望拦截到的域名,例如:*.jd.com 端口 443

charles-ssl-config01

charles-ssl-config02

配置完成 : )

Fiddler

  • 官网
  • 微软出品,免费,由于需要 .Net 库仅 Windows 平台(也有一些 Fiddler For mac / linux 但仅限 beta 版且功能不完善,这里只推荐在 Win 平台使用)

HTTP 抓包

配置与 Charles 配置相同端口略有不同,此处不再重复。

总结几大步骤:

  1. 移动端和电脑同处同一局域网内
  2. 手机或移动设备设置网络代理对应电脑端 IP 和 端口

fiddler01

HTTPS 抓包配置

  • 勾选拦截 HTTPS 请求
  • 安装证书(电脑端 & 手机端)

fiddler02

Whistle

  • 官网
  • 国人开发,开源 MIT 协议,基于 NodeJS 跨平台
  • 推荐 功能强大,部门内也有小伙伴分享过一次使用经验

工具准备

  • nodejs v0.10.0+
  • npm i -g whistle
  • w2 start

完成上面几步操作后,此时 8899 端口已经常驻后台。

界面

whistle

Chrome 插件

whistle 提供了一个基于 Chrome 的插件,可以方便切换代理,查看网络,编辑规则等,不过很遗憾作者已经将插件在 Web Store 下架,现在搜索不到,我把本地 0.10.0 版本打包成 crx 上传在 Google Driver 可以访问 下载

或者改用另外一个强大的插件 Proxy SwitchyOmega 该插件也拥有强大的代理功能,同时支持自定义规则,可以自动根据域名或端口等自动选择是否启用代理,以后有机会可以分享使用 Proxy SwitchOmega 的使用经验。

HTTPS 拦截配置

whistle 文档 https 设置部分

HTTPS 抓包锦囊

注意 自 iOS 10.3 以后,无论使用上面哪一种工具抓包,安装完 ca 证书以后,还需要多一步操作,才能确保证书安装并已启用;

通用 -> 关于本机 -> 证书信任设置 -> 启用相对应的证书,就可以看到工具中抓取到相应的 HTTPS 请求。

ios-cer

这一点跟苹果的 one more thing 貌似有那么一点不谋而合。

注意 Android 部分手机(如小米、华为)无法安装证书时候,不妨试试在电脑端将证书导出,打开手机 FTP 或者利用数据线传输到手机某个文件夹,点开 WLAN 配置 -> 高级设置 -> 安装证书 选择对应的证书安装即可。亦或是因为权限问题无法安装,打开安全隐私,选择信任 未知来源

此办法对于安装测试版 APP 也管用 :)

have fun :D

]]>
<p>投稿部门公众号,做了一些微调,增加了 Android 机型的一些说明。</p> <p>2018-03-01 Beanlee</p> <p>相信各位在移动端开发过程中一定遇到抓取数据包、拦截请求的场景,本文主要介绍移动端在针对 HTTPS 抓包时的几款软件的配置(包括<a href="#Charles">Charles</a>、<a href="#Fiddler">Fiddler</a>、<a href="#Whistle">Whistle</a>),下面举例将已 iOS 11 为例,附带 Android 机型配置截图。</p>
iOS HTTPS 抓包工具配置说明 https://beanlee.github.io/posts/iOS-https/ 2018-03-01T10:53:55.000Z 2018-06-20T01:31:47.000Z 相信各位在移动端开发过程中一定遇到抓取请求的场景,本文主要显示 iOS 11 在 CharlesFiddlerWhistle 下HTTPS抓包配置,欢迎留言区补充其他工具。PS.留言使用 Disqus 插件需要科学上网

已经熟悉以上三个工具基础配置,仅仅需要解决抓包 iOS 下 HTTPS 问题的同学可以直接打开锦囊

Charles

  • 官网
  • 按照 license 收费,收费后终身享受升级,仅限 OS X 平台

HTTP 抓包

1.确保手机与主机处于同一局域网内。

查看本机 IP,使用 ifconfig 命令查看本机 IP;或者打开系统设置查看 IP 如截图;

当前本机 IP 192.168.191.4

ifconfig

wifi-config

2.设置手机 iOS 网络代理

设置 -> 无线局域网 -> HTTP 代理 -> 手动

服务器:192.168.191.4 端口:8888

3.配置完成后,在手机端访问任何一个 App 或网页,本机 charles 会弹出提示,点击 Allow

此时已经可以抓取 HTTP 数据了

HTTPS 抓包

1.信任证书 本机

打开 charles 后如下图操作,本机信任 charles 证书,注意 此处选择system

charles-install-root-cer

charles-install-root-cer-system

2.信任证书 手机

打开 safari 地址输入192.168.191.4:8888chls.pro/ssl点击安装,此时会弹出要求需要输入手机密码,完成后证书安装成功。

此时查看通用->描述文件中 charles proxy ca 已变成已验证

注意 自 iOS 10.3 以上,还需要多一步操作,手动信任自定义根证书,才能确保证书安装并已启用;如图所示;

关于本机->证书信任设置启用 charles proxy ca 截图详见锦囊

3. Charles 设置启用 SSL Proxy

Proxy->SSL Proxy setting Enabled SSL Proxying

同时增加希望抓取的域名,例如:*.jd.com端口 443

charles-ssl-config01

charles-ssl-config02

配置完成 : )

Fiddler

  • 官网
  • 微软出品,免费,由于需要 .Net 库仅 Windows 平台

HTTP 抓包

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

fiddler01

HTTPS 抓包配置

  • 勾选拦截 HTTPS 请求
  • 安装证书(电脑端 & 手机端)

fiddler02

Whistle

  • 官网
  • 国人开发,开源 MIT 协议,基于 NodeJS 跨平台
  • 推荐

工具准备

  • nodejs v0.10.0+
  • npm i -g whistle
  • w2 start

完成上面几步操作后,此时8899端口已经常驻后台。

界面

whistle

Chrome 插件

whistle 提供了一个基于 Chrome 的插件,可以方便切换代理,查看网络,编辑规则等,不过很遗憾作者已经将插件在 Web Store 下架,现在搜索不到,我把本地 0.10.0 版本打包成 crx 上传在 Google Driver 可以访问 下载

或者改用另外一个强大的插件 Proxy SwitchyOmega

HTTPS 拦截配置

whistle 文档 https 设置部分

iOS 11 HTTPS 抓包锦囊

注意 自 iOS 10.3 以后,无论使用上面哪一种工具抓包,安装完 ca 证书以后,还需要多一步操作,才能确保证书安装并已启用;

通用 -> 关于本机 -> 证书信任设置 -> 启用相对应的证书,就可以看到工具中抓取到相应的 HTTPS 请求。

ios-cer

这点跟苹果的 one more thing 貌似有那么一点不谋而合。

have fun :D

]]>
<p>相信各位在移动端开发过程中一定遇到抓取请求的场景,本文主要显示 iOS 11 在 <a href="#Charles">Charles</a>、<a href="#Fiddler">Fiddler</a>、<a href="#Whistle">Whistle</a> 下HTTPS抓包配置,欢迎留言区补充其他工具。PS.留言使用 Disqus 插件需要<code>科学上网</code>。</p> <p>已经熟悉以上三个工具基础配置,仅仅需要解决抓包 iOS 下 HTTPS 问题的同学可以直接打开<a href="/2018/03/01/2018-03-01-iOS-https/#tips">锦囊</a></p>
【译】利用 webpack 做 web 性能优化 https://beanlee.github.io/posts/blog-translate-web-performance-optimization-with-webpack-from-google/ 2018-02-11T06:35:33.000Z 2018-06-20T01:31:47.000Z 作者 Addy OsmaniIvan Akulov

原文 https://developers.google.com/web/fundamentals/performance/webpack/

Instroduction 介绍

作者 Addy Osmani

现代 Web 应用经常用到bunding tool来创建生产环境的打包文件(例如脚本、样式等),打包文件是需要优化并压缩最小化,同时能够被让用户更快地下载到。在这篇文章中,我们将会利用webpack来贯穿说明如何优化网站资源。这样可以帮助用户更快地加载你的应用同时获得更好的体验。

webpack-logo

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

code-splitting

Note: 我们创建了一个练习的应用来演示下面这些优化的描述。尽力抽更多的时间来练习这些 tips webpack-training-project

让我们从现代 web 应用中最耗费资源之一的 Javascript 开始。

  • 减小前端体积
  • 利用长时缓存
  • 监控并分析应用
  • 总结

Decrease Front-end Size 减少前端体积

作者 Ivan Akulov

当你正在优化一个应用时,首要事情就是尽可能地将它体积的减小。下面就是利用 webpack 如何做。

Enable minification 启用最小化

最小化是通过去除多余空格、缩短变量名等方式压缩代码。例如:

1
2
3
4
5
6
7
8
9
10
11
// Original code
function map(array, iteratee) {
let index = -1;
const length = array == null ? 0 : array.length;
const result = new Array(length);

while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}

1
2
// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}

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)
"use strict";
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)
"use strict";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+。

Further reading

Specify NODE_ENV=production 明确生产环境信息

减小前端体积的另外一个方法就是在代码中将NODE_ENV环境变量设置成production

Libraries 会读取NODE_ENV变量判断他们应该在那种模式下工作 - 开发模式 or 生成模式。很多库会基于这个变量有不同的表现。举个例子,当NODE_ENV没有设置成production,Vue.js 会做额外的检查并且输出一些警告:

1
2
3
4
5
6
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …

React 也是类似 - 开发模式下 build 带有一些警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}

// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …

这些检查和警告通常在生产环境下是不必要的,但是他们仍然保留在代码中并且会增加库的体积。通过配置 webpack 的 DefinePlugin 来剔除掉:

1
2
3
4
5
6
7
8
9
10
11
 // webpack.config.js
const webpack = require('webpack');

module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
}),
new webpack.optimize.UglifyJsPlugin(),
],
};

DefinePlugin用确定的变量替换所有存在的说明变量。利用下面配置:

1.DefinePlugin将用"production"替换到process.env.NODE_ENV

1
2
3
4
5
6
7
// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}

1
2
3
4
5
6
7
// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if ("production" !== 'production') {
warn('props must be strings when using array syntax.');
}

Note: 如果你偏向有通过 CLI 配置变量,可以查看一下 EnvironmentPlugin。它和DefinePlugin类似,但读环境并且自动替换process.env表达式。

2.UglifyJS会移除掉所有if分支 - 因为"production" !== 'production'永远返回 false ,插件理解代码内的判断分支将永远不会执行:

1
2
3
4
5
6
7
// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if ("production" !== 'production') {
warn('props must be strings when using array syntax.');
}

1
2
3
4
5
// vue/dist/vue.runtime.esm.js (without minification)
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
}

Note: 不一定强制要求使用 UglifyJSPlugin。你可以使用其他不同的最小化工具,这些页支持移除无用代码(例如,the Babel Minify plugin or the Google Closure Compiler plugin

Further Reading

Use ES Modules 使用 ES 模块

下面这个方式利用 ES modules 减小前端体积。

当你使用 ES module,webpack 有能力去做 tree-shaking。Tree-shaking 贯穿整个依赖树,检查那些依赖被使用,移除无用依赖。因此,如果你使用 ES module 语法,webpack 可以排除掉无用代码:

1.一个有多个 export 的文件,但是 app 只需要其中一个:

1
2
3
4
5
6
7
// comments.js
export const render = () => { return 'Rendered!'; };
export const commentRestEndpoint = '/rest/comments';

// index.js
import { render } from './comments.js';
render();

2.webpack 理解 commentRestEndPoint没有使用,同时不能在一个 bundle 中生成单独的 export:

1
2
3
4
5
6
7
8
9
// bundle.js (part that corresponds to comments.js)
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
const render = () => { return 'Rendered!'; };
/* harmony export (immutable) */ __webpack_exports__["a"] = render;

const commentRestEndpoint = '/rest/comments';
/* unused harmony export commentRestEndpoint */
})

3.UglifyJSPlugin移除无用变量:

1
2
// bundle.js (part that corresponds to comments.js)
(function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})

如果他们都是有 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-env or babel-preset-es2015,检查一下当前的配置。默认情况下, ES import and export to CommonJS require and module.exports。通过设置 option 来禁止掉Pass the { modules: false } option

Futher reading

Optimize images 优化图片

图片基本会占局页面一半以上体积。虽然它们不像 JavaScript 那么重要(比如它们不会阻止页面渲染),但图片仍然会占用掉一大部分带宽。利用url-loadersvg-url-loaderimage-webpack-loader来在 webpack 中进行优化。

url-loader 允许将小静态文件打包进 app。没有配置,他需要通过 file,将它放在编译后的打包 bundle 内并返回一个这个文件的 url。然而,如果我们注明limit选项,它将会 encode 成更小的文件 base64 文件 url。这是可以将图片放在Javascript 代码中,同时节省 HTTP 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};
1
2
3
4
5
6
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`

Note: 内联图片减少了独立请求的数量,这是很好的方式(even with HTTP/2),但是会增加 bundle下载和转换的时间和内存的消耗。一定要确保不要嵌入超大图片或者较多的图片 - 否则增加的 bundle 的时间将会掩盖做成内联图片的收益。

svg-url-loaderurl-loader类似 - 都是将使用 URL encoding encode 文件。这对对于 SVG 图片很奏效 - 因为 SVG 文件是文本,encoding 在体积上更有效率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: 'svg-url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
// Remove the quotes from the url
// (they’re unnecessary in most cases)
noquotes: true,
},
},
],
},
};

Note: svg-url-loader 拥有改善 IE 浏览器支持的 options,但是在其他浏览器中更糟糕。如果你需要兼容 IE 浏览器,设置 iesafe: true 选项

image-webpack-loader压缩图片使之变小。它支持 JPG,PNG,GIF 和 SVG,因为我们将会使用它所有类型。

这个 loader 不会将图片嵌入在应用内,因此它必须与url-loadersvg-url-loader配合使用。避免复制粘贴到相同的 rules 中(一个用于 JPG/PNG/GIF 图片,另一个用于 SVG 图片),我们来使用enforce: pre作为单独的一个 rule 涵盖这个 loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre',
},
],
},
};

默认 loader 设置就已经可以满足需求了 - 但如果你想要深入配置,请查看 the plugin options。为了选择哪些 options 需要明确,可以查看 Addy Osmani 的 guide on image optimization

Further reading

Optimize dependencies 优化依赖

平均一半以上的 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

Enable module concatenation for ES modules (aka scope hoisting)

当你构建 bundle 时,webpack 将每一个 module 封装进 function 中:

1
2
3
4
5
6
7
8
// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
console.log('Rendered!');
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bundle.js (part  of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();

}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_exports__["a"] = render;
function render(data, target) {
console.log('Rendered!');
}

})

在以前,这么做是使 CommonJS/AMD modules 互相分离所必须的。但是,这会增加体积并且性能表现堪忧。

Webpack 2 介绍了 ES modules 的支持,不像 CommonJS 和 AMD modules 一样,而是能够不用将每一个 module 用 function 封装起来。同时 Webpack 3 利用ModuleConcatenationPlugin完成这样一个 bundle,下面是例子:

1
2
3
4
5
6
7
8
// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
console.log('Rendered!');
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// 与前面的代码不同,这个 bundle 只有一个 module,同时包含两个文件

// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}

// CONCATENATED MODULE: ./index.js
render();

})

看到区别了吗?在这个 bundle 中, module 0 需要 module 1 的 render 方法。使用 ModuleConcatenationPluginrequire被直接简单的替换成 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 的热更新。这就是为什么只在生产环境中启用的原因了。

Further reading

Use externals if you have both webpack and non-webpack code 如果代码中包含 webpack 和非 webpack 的代码要使用 externals

你可能拥有一个体积庞大的工程,其中一部分代码可以使用 webpack 编译,而有一些代码又不能。比如一个视频网站,播放器的 widget 可能通过 webpack 编译,但是其周围页面区域可能不是:

video-hosting

如果两部分代码有相同的依赖,你可以共享这些依赖以便减少重复下载耗时。the webpack’s externals option就干了这件事 - 它用变量或者外部引用来替代 modules。

如果依赖是挂载到 window 上的情况

如果你的非 webpack 代码依靠这些依赖,它们是挂载 window 上的变量,可以将依赖名称 alias 成变量名:

1
2
3
4
5
6
7
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
},
};

利用这个配置,webpack 将不会打包 reactreact-dom包。取而代之,他们会被替换成下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})

如果依赖是当做 AMD 包被加载的情况

如果你的非 webpack 代码没有将依赖暴露挂载到 window 上,这就更复杂了。但是如果非 webpack 代码使用 AMD 包的形式消费了这些依赖,你仍然可以避免重复的代码加载两次。

具体如何做呢?将 webpack 代码编译成一个 AMD module 同时别名成一个库的 URLs:

1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
output: { libraryTarget: 'amd' },

externals: {
'react': { amd: '/libraries/react.min.js' },
'react-dom': { amd: '/libraries/react-dom.min.js' },
},
};

Webpack 将会把 bundle 包装进 define()同时让它依赖于这些URLs:

1
2
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });

如果非 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' 是同一个库,这样比较谨慎。

Further reading

Summing up 总结

  • Minimize your code with the UglifyJsPlugin and loader options
  • Remove the development-only code with the DefinePlugin
  • Use ES modules to enable tree shaking
  • Compress images
  • Apply dependency-specific optimizations
  • Enable module concatenation
  • Use externals if this makes sense for you

Make use of long-term caching 利用好长时缓存

作者 Ivan Akulov

在做完优化应用体积之后的下一步提升应用加载时间的就是缓存。在客户端中使用缓存作为应用的一部分,这样会在每一次请求中减少重新下载的次数。

Use bundle versioning and cache headers 使用 bundle 版本和缓存头信息

做缓存通用的解决办法:

1.告诉浏览器缓存一个文件很长时间(比如一年)

1
2
# Server header
Cache-Control: max-age=31536000

Note:如果你不熟悉 Cache-Control 做了什么,你可以看一下 Jake Archibald 的精彩博文 on caching best practices

2.当文件改变需要强制重新下载时去重命名这些文件

1
2
3
4
5
<!-- Before the change -->
<script src="./index-v15.js"></script>

<!-- After the change -->
<script src="./index-v16.js"></script>

这些方法可以告诉浏览器下载这些 JS 文件,将其缓存起来。浏览器将只会在文件名发生改变时才会请求网络(或者缓存失效的情况也会请求)。

使用 webpack,也可以做同样的事,但可以使用版本号来解决,需要明确这个文件的 hash 值。使用 [chunkhash] 可以将 hash 值包含进文件名中:

1
2
3
4
5
6
7
8
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.[chunkhash].js',
// → bundle.8e0d62a03.js
},
};

Note: webpack 可能会生成不同的 hash 即使 bundle 相同 - 比如你重名了了一个文件或者重新在不同的操作系统下编译了一个 bundle。 This is a bug.
如果你需要将文件名发送给客户端,也可以使用 HtmlWebpackPlugin 或者 WebpackManifestPlugin

HtmlWebpackPlugin 很简单,但灵活性欠缺一些。编译时,插件会生成一个 HTML 文件,这其中包括所有的编译后的资源文件。如果你的业务逻辑不复杂,这就非常适合你:

1
2
3
4
<!-- index.html -->
<!doctype html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin更灵活一些,它可以帮助你解决业务负责的部分。编译时它会生成一个 JSON 文件,这文件保存这没有 hash 值文件与有 hash 文件之间的映射。服务端利用这个 JSON 可以识别出那个文件有效:

1
2
3
4
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}

Further reading

Extract dependencies and runtime into a separate file 将依赖和运行环境代码提取到一个单独的文件

Dependencies 依赖包

App 依赖通常情况下趋向于比实际 app 内代码中更少的变化。如果你将他们移到独立的文件中,浏览器将可以把他们独立缓存起来 - 同时不会每次 app 代码改变时重新下载。

Key Term: 在 webpack 的技术中,利用 app 代码拆分文件被称为 chunks。我们后面会用到这个名词。

为了将依赖包提取到单独的 chunk 中,下面分为三步:

1.使用[name].[chunkname].js替换output的文件名:

1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
output: {
// Before
filename: 'bundle.[chunkhash].js',
// After
filename: '[name].[chunkhash].js',
},
};

当 webpack 构建应用时,它会用一个带有 chunk 的名称来替换[name]。如果没有添加[name]部分,我们不得不通过 chunks 之间的 hash 区别来比较他们的区别 - 那就太难了!

2.将entry转成一个对象:

1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
// Before
entry: './index.js',
// After
entry: {
main: './index.js',
},
};

在这段代码中,”main” 对象是一个 chunk 的名字。这个名字将会被步骤 1 里面的 [name]代替。目前为止,如果你构建一个 app,chunk 就会包括整个 app 的代码 - 就像我们没有做这些步骤一样。但是很快就会产生变化。

3.添加 CommonsChunkPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// A name of the chunk that will include the dependencies.
// This name is substituted in place of [name] from step 1
name: 'vendor',

// A function that determines which modules to include into this chunk
minChunks: module => module.context &&
module.context.includes('node_modules'),
}),
],
};

插件将包括全部node_modules路径下的 modules 同时将他们移到一个单独的文件中,这个文件被称为 vendor.[chunkhash].js

完成了上面的步骤,每一次 build 都会生成两个文件。浏览器会将他们单独缓存 - 以便代码发生改变时重新下载。

1
2
3
4
5
6
7
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor

Webpack runtime code

不幸的是,仅仅抽取vendor是不够的。如果你试图在应用代码中修改一些东西:

1
2
3
4
5
6
// index.js



// E.g. add this:
console.log('Wat');

你会注意到vendor的 hash 值也会改变:

1
2
                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor

1
2
                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor

发生这样的事是因为 webpack 打包时,一部分 modules 的代码,拥有 a runtime - 管理模块执行一部分代码。当你将代码拆分成多个文件时,这小部分代码在 chunk ids 和 匹配的文件之间开始了一个映射:

1
2
3
4
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack 将最新生成的 chunk 包含在这个 runtime 内,这个 chunk 就是我们代码中的vendor。与此同时每一次任何 chunk 的改变,这一小部分代码也改变,导致整个 vendor chunk 也会改变。

为了解决这个问题,我们将 runtime 转义到一个独立的文件中,通过CommonsChunkPlugin创建一个额外的空的 chunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',

minChunks: module => module.context &&
module.context.includes('node_modules'),
}),

// This plugin must come after the vendor one (because webpack
// includes runtime into the last chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',

// minChunks: Infinity means that no app modules
// will be included into this chunk
minChunks: Infinity,
}),
],
};

完成这一部分改变,每一次 build 都将生成三个文件:

1
2
3
4
5
6
7
8
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

将他们反过来顺序添加到 index.html 中,你就搞定了:

1
2
3
4
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Further reading

Inline webpack runtime to save an extra HTTP request 内联 webpack runtime 节省额外的 HTTP 请求

为了做的更好,尽力把 webpack runtime 内联在 HTML 请求里。下面举例:

1
2
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

这样做:

1
2
3
4
<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

这个 runtime 很小,内联它可以帮助你节省 HTTP 请求(尤其对 HTTP/1 重要;但是在 HTTP/2 就没有那么重要了,但是仍能够提高效率)。

下面就来看看如何做。

如果使用 HtmlWebpackPlugin 来生成 HTML

如果使用 HtmlWebpackPlugin 来生成 HTML 文件,InlineChunkWebpackPlugin 就足够了。

如果使用自己的定制服务逻辑来生成 HTML

1.将 runtime 名称改成静态的明确的文件名:

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
minChunks: Infinity,
filename: 'runtime.js',
// → Now the runtime file will be called
// “runtime.js”, not “runtime.79f17c27b335abc7aaf4.js”
}),
],
};

2.以方便的方式将 runtime.js 嵌入进去。比如:Node.js 和 Express

1
2
3
4
5
6
7
8
9
10
11
// server.js
const fs = require('fs');
const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');

app.get('/', (req, res) => {
res.send(`

<script>${runtimeContent}</script>

`);
});

懒加载

有时候,页面拥有或多或少的部分:

  • 如果你在 YouTube 上加载一个视频页面,相比评论区域你更在乎视频区域。这就是视频要比评论区域重要。
  • 如果你在一个新闻网站打开一个报道,相比广告区域你更关心文章的内容。这就是文字比广告更重要。

在这些案例中,通过仅下载最重要的部分,懒加载剩余区域能够提升最初的加载性能。使用 the import() functioncode-splitting 解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});

import()明确表示你期望动态地加载独立的 module。当 webpack 看到 import('./module.js')时,他就会将这个 module 移到独立的 chunk 中:

1
2
3
4
5
6
7
8
9
$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

并且只在代码执行到 import() 才会下载。

这将会让 main bundle 更小,提升初始加载的时间。更重要的是改进缓存 - 如果你修改 main chunk 的代码,其他部分的 chunk 也不会受影响。

Note: 如果使用 Babel 编译代码,你会因为 Babel 还不认识 import() 而遇到语法错误抛出来。可以使用 syntax-dynamic-import 解决这个错误。

Further reading

Split the code into routes and pages 拆分代码到路由和页面中

如果你的应用拥有多个路由或者页面,但是代码中只有单独一个 JS 文件(一个单独的 main chunk),这看起来你正在每一个请求中节省额外的 bytes 带宽。举个例子,当用户正在访问你网站的首页:

site-home-page

他们并不需要加载另外不同的页面上渲染文章标题的的代码 - 但是他们还是会加载到这段代码。更严重的是如果用户经常只访问首页,同时你还经常改变渲染文章标题的代码,webpack 将会对整个 bundle 失效 - 用户每次都会重复下载全部 app 的代码。

如果我们将代码拆分到页面里(或者单页面应用的路由里),用户就会只下载对他有意义的代码。更好的是,浏览器也会更好地缓存代码:当你改变首页的代码时,webpack 只会让相匹配的 chunk 失效。

For single-page apps 对于单页面应用

通过路由拆分带页面引用,使用 import()(看看 “Lazy-load code that you don’t need right now”这部分)。如果你在使用一个框架,现在已经有成熟的方案:

For traditional multi-page apps 对于传统的多页面应用

通过页面拆分传统多页面应用,可以使用 webpack 的 entry points 。如果你的应用有三种页面:主页、文章页、用户账户页,那就分厂三个 entries:

1
2
3
4
5
6
7
8
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
},
};

对于每一个 entry 文件,webpack 将构建出独立的依赖树,并且声称一个 bundle,它将通过 entry 来只包括用到的 modules:

1
2
3
4
5
6
7
8
9
10
11
$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime

因此,如果仅仅是文章页使用 Lodashhomeprofile 的 bundle 将不会包含 lodash - 同时用户也不会在访问首页的时候下载到这个库。

拆分依赖树也有缺点。如果两个 entry points 都用到了 loadash ,同时你没有在 vendor 移除掉依赖,两个 entry points 将包括两个重复的 lodash 。我们可以使用CommonsChunkPlugin来解决这个问题 - 它会将通用的依赖转移到一个独立的文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// A name of the chunk that will include the common dependencies
name: 'common',

// The plugin will move a module into a common file
// only if it’s included into `minChunks` chunks
// (Note that the plugin analyzes all chunks, not only entries)
minChunks: 2, // 2 is the default value
}),
],
};

随意使用minChunks的值来找到最优的选项。通常情况下,你想要它尽可能体积小,但它会增加 chunks 的数量。举个例子,3 个 chunk,minChunks 可能是 2 个,但是 30 个 chunk,它可能是 8 个 - 因为如果你把它设置成 2 ,过多的 modules 将会打包进一个通用文件中,文件更臃肿。

Further reading

Make module ids more stable 让 module ide 更稳定

当编译代码时,webpack 会分配给每一个 module 一个 ID。之后,这些 ID 就会被require()引用到 bundle 内部。你可以在编译输出的右侧在 moudle 路径之前看到这些 ID:

1
2
3
4
5
6
7
8
9
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime

1
2
3
4
5
6
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

默认情况下,这些 ID 是使用计数器计算出来的(比如第一个 module 是 ID 0,第二个 moudle 就是 ID 1,以此类推)。这样的问题就在于当你新增一个 module 事,它会出现在原来 module 列表中的中间,改变后面所有 module 的 ID:

1
2
3
4
5
6
7
8
9
10
11
12
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ 我们增加一个新 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
2
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module

这将使包含或依赖于具有更改ID的模块的所有块无效 - 即使它们的实际代码没有更改。在我们的代码中,_0_这个 chunk 和 main chunk 都会失效 - 只有main才应该失效。

使用HashedModuleIdsPlugin插件改变module ID 如何计算来解决这个问题。它利用 module 路径的 hash 来替换掉计数器:

1
2
3
4
5
6
7
8
9
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime

↓ Here

1
2
3
4
5
6
7
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module

有了这个方法,只有你重命名护着删除这个 moudle 它的 ID 才会变化。新的 modules 不会因为 module ID 互相影响。

启用这个插件,在配置中增加 plugins

1
2
3
4
5
6
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin(),
],
};

Further reading

Summing up

  • Cache the bundle and differentiate between them by changing their names
  • Split the bundle into app code, vendor code and runtime
  • Inline the runtime to save an HTTP request
  • Lazy-load non-critical code with import
  • Split code by routes/pages to avoid loading unnecessary stuff

Monitor and analyze the app 监控并分析

作者 Ivan Akulov

即使当你配置好你的 webpack 让你的应用尽可能体积较小的时候,跟踪这个应用就非常重要,同时了解里面包含了什么。除此之外,你安装一个依赖,它将让你的 app 增加两倍大小 - 但并没有注意到这个问题!

这一部分就来讲解一些能够帮助你理解你的 bundle 的工具。

Keep track of the bundle size 跟踪打包的体积

在开发时可以使用webpack-dashboard和命令行bundlesize 来监控 app 的体积。

webpack-dashboard

webpack-dashboard可以通过依赖体积大小、进程和其他细节来改进 webpack 的输出。

webpack-dashboard

这个 dashborad 帮助我们跟踪大型依赖 - 如果你增加一个依赖,你就立刻能在 Modules section 始终看到它!

启用这个功能,需要安装 webpack-dashboard 包:

1
npm install webpack-dashboard --save-dev

同时在配置的 plugins 增加:

1
2
3
4
5
6
7
8
// webpack.config.js
const DashboardPlugin = require('webpack-dashboard/plugin');

module.exports = {
plugins: [
new DashboardPlugin(),
],
};

或者如果正在使用基于 Express dev server 可以使用 compiler.apply()

1
compiler.apply(new DashboardPlugin());

多尝试 dashboard 找出改进的地方!比如,在 modules section 滚动找到那个库体积过大,把它替换成小的可替代的库。

bundlesize

bundlesize 可以验证 webpack assets 不超过指定的大小。通过自动化 CI 就可以知晓 app 是否变的过于臃肿:

bundlesize

配置如下:

Find out the maximum sizes 找出最大体积

1.分析 app 尽可能减小体积,执行生产环境的 build。
2.在package.json中增加bundlesize部分:

1
2
3
4
5
6
7
8
// package.json
{
"bundlesize": [
{
"path": "./dist/*"
}
]
}

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%的幅度可以让你像往常一样开发应用程序,同时警告你,当它的大小增长太多。

Enable bundlesize 启用 bundlesize

5.安装bundlesize开发依赖

1
npm install bundlesize --save-dev

6.在package.json中的bundlesize部分,声明具体的最大值。对于某一些文件(比如图片),你可以单独根据文件类型来设置最大体积大小,而不需要根据每一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package.json
{
"bundlesize": [
{
"path": "./dist/*.png",
"maxSize": "16 kB",
},
{
"path": "./dist/main.*.js",
"maxSize": "20 kB",
},
{
"path": "./dist/vendor.*.js",
"maxSize": "35 kB",
}
]
}

7.增加一个 npm 脚本来执行检查:

1
2
3
4
5
6
// package.json
{
"scripts": {
"check-size": "bundlesize"
}
}

8.配置自动化 CI 来在每一次 push 时执行npm run check-size做检查。(如果你在 Github 上开发项目,直接可以使用integrate bundlesize with GitHub。)

这就全部了!现在如果你运行npm run check-size或者 push 代码,你就会看到输出的文件是否足够小:

bundlesize-output-success

或者下面失败的情况

bundlesize-output-failure

Further reading

Analyze why the bundle is so large 分析 bundle 为什么这么大

你想要深挖 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
2
3
4
5
6
7
8
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
};

运行生产环境的 build 这个插件就会在浏览器中打开一个显示状态的页面。

默认情况下,这个页面会显示语法分析后的文件体积(在 bundle 出现的文件)。您可能想比较 gzip 的大小,因为这更接近实际用户的体验;使用左边的边栏来切换尺寸。

Note: 如果你使用 ModuleConcatenationPlugin,它可能在webpack-bundle-analyzer输出时合并一部分 module,使得报告小一些细节。如果你使用这个插件,在执行分析的时候需要禁用掉。

下面是报告中需要看什么:

  • 大型依赖 为什么体积这么大?是否有更小的替代包(比如 Preact 替代 React)?用了全部代码(比如 Moment.js 包含大量的本地变量 that are often not used and could be dropped)?
  • 重复依赖 是否在不同文件中看到相同的库?(使用CommonsChunkPlugin将他们移到一个通用文件内)亦或是在同一个库中 bundle 拥有多个版本?
  • 相似依赖 是否存在有相似功能的相似库存在?(比如momentdate-fns 或者 lodashlodash-es)尽力汇总成一个。

同样的,也可以看看 Sean Larkin 的文章 great analysis of webpack bundles

Summing up

  • Use webpack-dashboard and bundlesize to stay tuned of how large your app is
  • Dig into what builds up the size with webpack-bundle-analyzer

Conclusion

总结一下:

  • 剔除不必要的体积 把所有的都压缩,剔除无用代码,增加依赖是保持谨慎小心。
  • 通过路由拆分代码 只在真正需要的时候才加载,其他的部分做来加载。
  • 缓存代码 应用程序的某些部分更新频率低于其他部分,将这些部分拆分成文件,以便在必要时仅重新下载。
  • 跟踪体积大小 使用 webpack-dashboardwebpack-bundle-analyzer 监控你的 app。每隔几个月重新检查一下你的应用的性能。

Webpack 不仅仅是一个帮助你更快创建 app 的工具。它还帮助使你的 app 成为 a Progressive Web App ,你的应用拥有更好的体验以及自动化的填充工具就像Lighthouse根据环境给出建议。

不要忘记阅读 webpack docs - 里面提供了大量的优化的信息。

多多练习 with the training app

]]>
<p>作者 <a href="https://developers.google.com/web/resources/contributors/addyosmani" target="_blank" rel="noopener">Addy Osmani</a>,<a href="https://developers.google.com/web/resources/contributors/iamakulov" target="_blank" rel="noopener">Ivan Akulov</a></p> <p>原文 <a href="https://developers.google.com/web/fundamentals/performance/webpack/" target="_blank" rel="noopener">https://developers.google.com/web/fundamentals/performance/webpack/</a></p>
【译】NPM vs Bower vs Browserify vs Gulp vs Grunt vs Webpack https://beanlee.github.io/posts/NPM-vs-Bower-vs-Browserify-vs-Gulp-vs-Grunt-vs-Webpack/ 2016-06-17T06:35:33.000Z 2018-06-20T01:31:47.000Z 这是一篇在 Stack overflow 看到的一篇问题和回答,对于自己有一些解惑

Source Link

作者问题

描述:我正在试着总结关于最流行的 Javascript 包管理器,打包器和任务执行器的知识。如果有错误请纠正我。

  • npm & bower 是包管理工具。他们只是将依赖文件下载,并不知道在下载这些文件的基础上如何编译项目。他们知道的是在获取所有依赖之后去调用 webpack/gulp/grunt
  • bower 很像 npm ,但是编译依赖树不在行(不像 npm 那样递归着进行)。意味着 npm 获取每一依赖(可能获取几次相同的文件),然而 bower 期望你手动去包含子依赖。有时候 bowernpm 可以一起被使用,分别作用前端和后端(在前端每一mb都很重要)。
  • glupgulp 是任务运行器,目的是将能够被自动化的所有事任务自动化执行。(比如编译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 CommonJS

Questions

  • 什么是 webpack 和 webpack-dev-server ?官方文档说它是模块打包器,但对于我而言,它知识任务运行工具。有什么不同?
  • 你在哪里用到 browserify ?我们能不能和 node/ES6 一样做?
  • 你什么时候在 npm + plugins 基础上使用 gulp/grunt

Beat Answer

WebpackBrowerify 做了很多相同的工作,比如用于在一个浏览器环境钟打包你的模块。这个模块就是一个 Node 特征,它不在浏览器中存在,并且 ES6 中的 import 还没有在任何浏览器中实现,这就是为什么需要被打包。但是,他们在很多方式上是有区别的,Webpack 默认提供很多工具(比如代码拆分),Browerify 只能在下载插件之后才能做这些,但是使用这两种都能实现相似的效果。它取决于个人习惯(我常使用 Webpack )。Webpack 不是一个任务运行器,它只是你通过CLI或任务运行器直接运行文件的一个中间过程器。

Webpack-dev-server 提供类似于 Browser-sync - 它是一个你可以将你的 app 部署的服务,并且验证你的前端开发进程直接通过 dev-server 自动刷新浏览器或者在没有热部署的情况下传播变化(比如 React components)。

我为了项目的完整和简单的任务编写已经使用Gulp,但是我后来发现我既不需要Gulp也不需要Grunt。所有我需要的都可以使用npm组合脚本去运行第三方工具利用它们的 API 完成。在GulpGruntnpm script之间选择取决于你的需要、JS 经验和你工作时的开发经验。

当然Gulp中的任务是易读的,甚至与JS不是很相似,它还是引用和学习另一个工具,并且我个人倾向于缩小我的依赖并且保持简单。另外一面,使用npm组合脚本和运行文件(配置和执行Webpack文件中函数)替代这些任务是更具有挑战性的。但是重要的是他们三个的结果是相同的。

举例说我建议你看一看 react starter project ,它可以向你很好的展示npm组合脚本,Webpackbrowser-sync。即使你可以处理你的源文件,如果你愿意,你可以使用Gulp或者Grunt运行你的开发服务,我更喜欢第一个选项。

]]>
<p>这是一篇在 Stack overflow 看到的一篇问题和回答,对于自己有一些解惑</p>