同样是在 2012 年, Web 性能工作组针对页面加载场景制定了一个加载过程模型,用来衡量页面加载各个阶段的耗时情况,相信这张图大家应该都见过:
但是随着时代的发展,图中的指标已经不太能准确的反映性能表现了,比如在一个 SPA 应用中,页面容器的 DOM 结构已经生成了,但实际上当前页面还是一个白屏的空壳、又比如页面中绝大部分内容已经渲染好了,但是有一张特别大的图片还在加载中…
针对这些问题,浏览器厂商、web 工作组等都提出了一些关键指标,比如谷歌提出了 4 个衡量用户体验的新指标(LCP、CLS、FID、INP),这 4 个指标将会影响谷歌搜索引擎的排名,Chrome 开发团队也提出了 4 个指标(FP、FCP、LCP、CLS),W3C 性能工作组则认为 LCP、CLS 和 TBT 是最值得关注的。
在现代前端性能监控系统中,我们也经常会见到它们,我们对页面性能的衡量,基本就是通过对这些指标的收集和计算得来的。有了这些指标,我们也可以有效的还原出页面加载的瀑布图、火焰图等,从而更有针对性的做出优化。
例如,这是一张火山引擎前端监控系统的截图:
在浏览器的 devtools 中,我们也可以通过 performance 来统计一次加载过程中的一些指标:
下面我们就来一起看一下这些指标的具体含义:
TTFB 是资源请求与响应的第一个字节开始到达之间的时间,如下图所示,TTFB 是 startTime 和 responseStart 之间经过的时间。
我们可以使用 Google 提供的 web-vitals 这个库来测量 TTFB 时间。同样,后文中的 CLS、FCP、FID、INP、LCP 时间也都可以通过这个库来获取。使用方式可以参考如下代码:
1 | import { onTTFB, onCLS, onFID, onLCP } from "web-vitals"; |
如果希望能自己通过底层 Web API 直接衡量(可能兼容性会弱于web-vitals
),可以这样实现:
1 | new PerformanceObserver((entryList) => { |
FP 代表浏览器第一次向屏幕传输像素的时间,也就是页面在屏幕上首次发生视觉变化的时间。在 FP 时间点之前,用户看到的都是没有任何内容的白色屏幕。在现代前端应用中,如前文所述, FP 已经不能准确反映页面性能了,目前 chrome、Lighthouse 等都已经不再统计 FP 了。
1 | performance |
FCP 用于测量从网页开始加载到网页任何一部分内容呈现在屏幕上的时间。“内容”是指文本、图片(包括背景图片)、svg 元素或非白色 canvas 元素。
如下图中,第二帧的时间,就是 FCP 时间。
1 | performance |
FMP 用来测量页面的主要内容何时对用户可见。由于对“主要内容”的判定很复杂,所以大部分情况下,FMP 是通过一些猜测算法得到的。具体实现方式可以参考 这篇论文。
FMP 和 FCP 最主要的区别在于 FMP 测量的是“有意义的内容”绘制的时间,但是这个指标的测量依赖于浏览器的实现细节,同时会受到多种因素影响引起剧烈波动,目前已经基本被弃用了,取而代之的是 LCP
。
SI 是 Lighthouse 提出的一个指标,用于衡量页面加载期间内容视觉显示的速度。
SI 是通过 speedline 计算得到的。
由于 FMP 和 SI 的实现非常复杂,同时难以解释,而且往往是错误的,这意味着它们仍然无法确定页面主要内容的加载时间。根据 W3C Web 性能工作组中的讨论和 Google 的研究,我们发现,要衡量网页主要内容的加载时间,更为准确的方法是查看最大元素的呈现时间,LCP 应运而生。
LCP 是指视口内可见的最大图片或文本块的呈现时间,包括以下元素:
img
元素svg
元素内的image
元素- 包含海报图片的
video
元素(系统会使用海报图片加载时间)- 一个元素,带有通过 url() 函数(而不是 CSS 渐变)加载的背景图片
- 包含文本节点或其他内嵌级别文本元素的子项的块级元素。
- 为自动播放
video
元素而绘制的第一帧(截至 2023 年 8 月)- 动画图片格式(例如 GIF 动画)的第一帧(截至 2023 年 8 月)
同时,浏览器还会通过自己的启发式算法来排除一些元素,比如在 Chromium 中,下列元素会被排除:
- 不透明度为 0 且对用户不可见的元素
- 覆盖整个视口的元素,很可能被视为背景而非内容
- 占位符图片或其他低熵的图片,可能无法反映网页真实内容
下图可以参考 LCP 时间:
可以如前文所述,使用 web-vitals 获取。
如果希望自己获取,可以这样使用 Largest Contentful Paint API
1 | new PerformanceObserver((entryList) => { |
但是实际应用中 LCP 的测量远比这里复杂,详细信息可以参考这篇文章。
TTI 是指从网页开始加载到主要子资源已加载且能够快速可靠地响应用户输入的时间。
TTI 的计算方式如下:
TBT 表示从 FCP 到 TTI 之间,所有 Long Task 的阻塞时间之和。
每当存在 Long Task 时,主线程就会被视为“阻塞”。我们称主线程“阻塞”,因为浏览器无法中断正在进行的任务。因此,如果用户在执行耗时较长的任务时与网页互动,浏览器必须等待任务完成才能响应。
如果任务的时间足够长(超过 50 毫秒的任何任务),用户很可能会注意到延迟,并认为页面运行缓慢或卡顿。
TBT 就是在 FCP 和 TTI 之间发生的每个耗时较长任务的阻塞时间之和。
FID 测量的是从用户第一次与网页互动(点击链接、点按按钮等操作)到浏览器实际能够开始处理事件处理脚本以响应该互动的时间。TTI 可以告诉我们网页什么时候可以开始流畅地响应用户的交互,但是如果用户在 TTI 的时间内,没有与网页产生交互,那么 TTI 其实是影响不到用户的,FID 则体现了真实的用户交互反馈。
同样,web-vitals 也可以直接提供 FID。
我们也可以这样记录一条 first-input
数据:
1 | new PerformanceObserver((entryList) => { |
CLS 衡量的是页面的整个生命周期内发生的每次意外布局偏移的最大突发性布局偏移量。
这句话可能有点难以理解,让我们来看一下下面这个场景:
我们正在一个购物网站中浏览,准备点击返回按钮,此时页面顶部忽然加载出了一条广告,导致页面布局发生了变化,造成误点了提交按钮,遇到这种情况,用户的心态应该是爆炸的。
CLS 可以帮我们统计出我们的页面上类似的布局偏移量,以便针对优化。
布局偏移由 Layout Instability API 定义,该 API 会在视口内可见的元素在两帧之间更改其起始位置(例如,在默认写入模式下的顶部和左侧位置)时,报告 layout-shift
条目。此类元素被视为不稳定的元素。
仅当现有元素更改其起始位置时,才会发生布局偏移。如果向 DOM 添加新元素或现有元素更改了尺寸,则不计为布局偏移,只要此类更改不会导致其他可见元素更改其起始位置即可。
和前面的几个指标一样,CLS 也可以使用 web-vitals 获取。
我们也可以这样记录一条布局偏移数据:
1 | // 最终的 CLS 数据应该由贯穿网页生命周期的多条数据计算得出 |
Long Task 是指在主线程上运行达 50 毫秒或以上的任务,我们通过对 Long Task 的分析,可以有效的找到具体哪些任务导致了页面卡顿。
Long Task 可以通过 PerformanceObserver 获取。
1 | const observer = new PerformanceObserver((list) => { |
DNS 解析耗时,可以使用 performance
中的 domainLookupEnd
- domainLookupStart
获取。
https://web.dev/articles/vitals
https://web.dev/explore/learn-core-web-vitals
https://developer.chrome.com/docs/lighthouse/performance/
https://mp.weixin.qq.com/s/TRY2mEMl4rZz3442SzC1EA?poc_token=HGPaSGWjW1CXHu3f8iyJPZiXY5QPKzU4e7Wowbf8
https://github.com/berwin/Blog/issues/46
SSL 库方面,最流行的 OpenSSL 决定自己实现 QUIC 支持,按照官网给出的 roadmap,看起来至少半年内是没什么希望的。
nginx 方面自己实现了一个 OpenSSL 的兼容层,1.25.0 以后版本的 nginx 中已经自带了。但是这个兼容层不支持 early data
。有些营销号直接把 early data
机翻成了“早期数据”,并解释为旧数据,实际上 early data
指的是 TLS1.3 中的 0-RTT data,详见这里。
所以目前 SSL 库最好从以下 3 个中 BoringSSL、 LibreSSL 或 QuicTLS 三选一。
以 BoringSSL 为例
1 | # 安装 go |
1 | # 安装 cmake 和 ninja |
1 | # 安装新版本 GCC 和 G++ |
1 | # 安装 nginx (1.25.0 版本开始支持HTTP3) |
1 | ; 基础配置,更多详细内容可以参考这里: |
1 | vim /etc/firewalld/zones/public.xml |
配置好之后,可以在这里监测一下,如图显示,我们已经成功启用了HTTP/3。
https://http3check.net/?host=xxx.com
同时,打开浏览器控制台,也能看到 protocol 这一项也已经升级为 h3。
如果前面的流程没有问题,还是显示不支持,可以在这里检测一下浏览器是否已经支持
https://quic.nginx.org/
记得关闭代理软件,有些代理协议不支持HTTP/3。
]]>维基百科给的概念是这样的:
色域是对一种颜色进行编码的方法,也指一个技术系统能够产生的颜色的总合。
顶级显示器制造商艺卓是这样解释的:
色域在人眼能够识别的色彩范围(即可见光谱)内限定了一个更具体的范围。彩色成像设备包括多种设备,例如数码相机、扫描仪、显示器和打印机,由于它们能够还原的色彩范围各不相同,所以采用色域这个概念来区分这些差别,并协调各个设备之间可以通用的颜色。
说人话就是:我们为了让相同的颜色在不同设备上看起来一样,需要一个标准。
我们知道在绘画时把红、黄、蓝这三种原色混合,可以生成不同的颜色。我们将红黄蓝三种原色的量分别定义为 XYZ 三个坐标轴,这样就得到了一个三维空间,每种可能的颜色在这个三维空间中都有唯一的一个位置,这就是一种色彩空间。
同样,在显示器领域,通常使用 RGB(红色、绿色、蓝色)三原色,以红色、绿色、蓝色为三维坐标轴,就得到了 RGB 色彩空间
。类似地,使用色相、饱和度(色度)和明度作为坐标轴,我们可以得到 HSV 色彩空间
,使用油墨的三原色(青、品、黄)搭配黑色油墨,我们可以得到印刷行业的通用标准 CMYK 色彩空间
。
由于人眼可见光的范围有限,我们制造显示器等成像设备时,没有必要也不可能做到支持完整的 RGB 色彩空间。
国际照明委员会(英文:International Commission on Illumination,法文:Commission internationale de l’éclairage,采用法文缩写:CIE)在 1931 年提出了一个基于人眼对于色彩的感知度量用数学方式定义色彩空间的方法:CIE 1931 XYZ
色彩空间,后续又在此基础上衍生出了CIE L*a*b*(CIELAB)
色彩空间,它们涵盖了正常人可见范围内的所有色彩。后续人们定义色彩空间时,通常使用这两个色彩空间作为参考标准。
依照在 RGB 色彩模型和 CIE 1931 XYZ 色彩空间之间的映射函数,在这个色彩空间中会出现一个有限的“覆盖区”,这个区域就被称为 CIE 色域,也就是人眼的色域,它在平面中的投影如图所示:
这就是 CIE xy 色度图,图中马蹄形的左右两边的轮廓线代表了波长由 380nm-700nm 连续变化的单色光,马蹄形的底边代表了紫红色光。值得一提的是,紫红色光并不是单色光,而是由红色(700nm)和紫色(380nm)混合而成。在马蹄形内部,越靠近马蹄形边缘的颜色饱和度越高。
我们在看显示器介绍的时候,经常会看到如 99% sRGB 色域,72% NTSC 色域等等,这些代表什么呢?
由于技术限制,目前的显示器还不能做到支持完整的 CIE 色域。因此,厂商们制定了一些新的色域标准,这些标准都是人眼全色域的子集,只能覆盖全色域中的一部分。对于显示器来说,色域代表了颜色的区域,也就是颜色的区域范围,色域越大,能显示的颜色就越多。
目前常见的显示器色域标准有:sRGB、AdobeRGB、NTSC 以及 DCI-P3 等。
那么,这些标准都代表什么,之间有什么区别呢?
sRGB 全名 standard Red Green Blue
,是由微软主导,联合惠普、爱普生等厂商在 1996 年联合制定的一套彩色语言协议,可以让显示器、打印机、扫描仪等设备色彩交互更加统一,就是让显示器上的颜色能够尽可能的完美打印出来的一套标准。因为通用性高,所以 sRGB 标准也是平面设计师、视频创作者等用户首选的色域标准。但是由于该色域标准比较老旧,当时的设备技术的色彩还原能力不高,所以 sRGB 的色彩容积也较低,只有 CIE 1931 标准的 30% 左右,同时该标准对绿色部分色域的覆盖非常少。上图的三角形划出的便是 sRGB 所能展现出的色彩容积,目前大部分较好的显示器都能展示 99% 以上的 sRGB 色域。。
我们前面提到过在印刷行业中,一般使用 CMYK 作为通用的色彩标准,而 CMYK 和 sRGB 的偏差是比较大的,从而使用 sRGB 模式展示的颜色在印刷时就会出现偏色。为了解决这个问题,Adobe 提出了 Adobe RGB, 从上图可以看出,Adobe RGB 的色域容积远比 sRGB 大。
NTSC 全名 National Television Standards Committee
,就是美国国家电视标准委员会的全名,因为该标准由美国国家电视标准委员会开发,故直接用该名作为简称。该标准主要用于美国标准电视广播传输和接受协议。很多笔记本电脑习惯使用 NTSC 来标注支持的色域,一般有 72% NTSC 和 45% NTSC 两种,其中 45% NTSC 的屏幕也常被人戏称”瞎眼屏”。72% NTSC 在面积上约等于 99% sRGB,注意这只是在面积上的大小比较,而这种对比意义不大。可以这么认为,故意标注自己的显示器支持 72% NTSC 色域的厂商,几乎所有的都是在暗示自己的产品基本覆盖了全部的 sRGB,而实际上一般还差很多。
前三种都是主要面向影视领域的色彩标准,覆盖范围 BT.2020 最大,DCI-P3 其次,Rec.709 最小。目前家用设备仅有少数高端电视和投影机支持 BT.2020,电影行业一般以 DCI-P3 作为标准使用。对比 sRGB 等标准,DCI-P3 在红绿等暖色系的显色范围更广,给人的视觉观感更好。
Display P3 是苹果主推的色域,覆盖区域和 DCI-P3 相同,而 Display P3 的 gamma 值近似为 2.2,DCI-P3 则是 2.6,所以 Display P3 的画面会更明亮一些。
webkit 提供了一个小工具可以方便的检测你的设备是否支持 DCI-P3 标准:
https://webkit.org/blog-files/color-gamut/comparison.html
如果你在第一张图中能明显的看到左侧部分比右侧偏暗,同时可以看到右侧图中的图标,说明你的显示器还不错。
也可以试试看这张图:
如果能看到第二个笑脸,说明你的显示器支持 100% 的 sRGB 标准,如果能看到第三个笑脸,说明支持 DCI-P3。
Windows 用户看不到的也不用折腾了,显示器模式、icc 配置、浏览器版本等都有可能有影响,Windows 的色彩管理实在一言难尽。
Chrome 92 支持在创建 2D canvas 时,使用 Display P3 色域。
1 | canvas.getContext("2d", { colorSpace: "display-p3" }); |
CSS Color Module Level 4 提供了一种新语法来设置 Display-P3 色域:
1 | color: color(display-p3 1 0.5 0); |
目前这个功能只有 Safari 15 以上版本支持,Chrome 正在规划中(https://bugs.chromium.org/p/chromium/issues/detail?id=1068610)
在不支持的浏览器上我们可以这样降级到 sRGB 模式:
1 | header { |
也可以通过 @supports 查询的方式来实现,如:
1 | /* sRGB color. */ |
除了 Safari 外,Chrome 实验特性中也提供了对 Display P3 的支持,可以通过
chrome://flags 中的 force color profile
选项手动选择 Display P3 D65
模式打开。注意在 Windows 系统上如果显示器不支持或没有配置好对应的驱动/ ICC 文件,系统将按照一定的规范把 P3 色域映射到 sRGB 色域上展示,会出现一定的过饱和现象。
如果一个显示器的色域足够广,是不是就可以说它的显示效果一定非常好呢?
并不是,还有其他几个非常重要的参数,比如色深和色准,下期再来分享~
https://zh.wikipedia.org/wiki/CIE_1931%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4
https://www.zhangxiaochun.com/color-space-1/
https://bbs.nga.cn/read.php?tid=20309949
https://yuque.antfin-inc.com/lili.qu/xioo87/fcxm2m
https://zhuanlan.zhihu.com/p/166413369
https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/
- 文中的『车程』不包含游览时间
南山文化旅游区是一座展示中国佛教传统文化的大型园区,福如东海寿比南山的南山就是这里了。主要景点有南山寺、南海观音佛像、不二法门、十方塔林与归根园、佛教文化交流中心等,世界上最大的一尊海上观音也坐落在此。有摇号需求的可以去参拜一下,此处 @安笺 @麦棋。
门票旺季 150 元,景区很大,建议多花 30 元购买游览车票。
天涯海角是人民币 2 元背面图案的取景地,有人说这是来三亚必须要去而且只会去一次的景点,第一次来的话还是去打个卡吧。有一说一 80 元的票价看几块石头还是有点心痛的,文昌免费的石头公园明显更适合我这种白嫖党。
同样建议坐观光车,可以大幅节约时间和体力。
椰梦长廊是一条 20 公里长的海滨风景大道,临海的一侧是热带植物园林,另一侧是休闲度假区。建议导航至Club Med 三亚度假村,然后沿三亚湾路游览。
鹿回头景区三面环海,一面毗邻三亚市区,是登高望海俯瞰全城的绝佳观景点。
太阳湾路号称“三亚最美公路”,是三亚自驾必打卡的地点之一,直接导航到太阳湾柏悦酒店即可,可以开到酒店内部观景台附近。顺便说一句,太阳湾柏悦是我心目中三亚最好的酒店,没有之一,值得体验。
南湾猴岛是世界上唯一的岛屿型猕猴自然保护区,现居住着 2500 多只猕猴。猴岛二期又名呆呆岛,是海南岛东海岸线最长的,保护最好的绝美沙滩。
又一个因《非诚勿扰 2》的取景地爆火的海湾,也是一个非常适合观赏日落的最佳地点。
建议打卡点凤凰九里书屋,面朝大海,是中国最美书店之一。
从书屋出发,建议导航至神州半岛泰悦居,这样可以保证一路沿着滨海旅游公路前进。
经过石梅湾艾美度假酒店后大约 800 米,路边会有南燕湾的观景台和停车场,可以休息游玩。
继续前进 8 公里,到达神州半岛泰悦居,路边有停车场,然后需要步行一公里前往海边。这里有一个网红灯塔,同样又双叒叕是一个看日落的地方(想看日出需要去文昌),如果想在这里看日落的话,可以从石梅湾先前往大花角,回程再来这里。
大花角由前鞍和后鞍两个山峦构成,双峰之间,有一处宽近百米的峡谷,与双峰一起伸向浩瀚的南海,于是形成一个天然的海湾,很适合坐在海边的岩石上思考人生。
从石梅湾/神州半岛出发可以直接导航到大花角,中途添加途经点蓝田观景台即可,一路上有很多观景点和停车场,随时可以停下来去海边游玩。
到达导航终点附近,会看到一个部队的营地,右转有一个停车场,会有当地村民来收费 10 元,没有任何票据,可以无视他。不想麻烦的话也可以直接把车停在部队门口的路边。
到了大花角,万宁滨海旅游公路就结束了,意犹未尽的,可以继续前往山钦湾(导航到上卿村)和玉带滩,需要注意还车时间。
如果觉得三亚的海已经看腻了,更推荐这条山水风光线路。
崖州湾出发,导航至玉龙山观景台,经环岛高速和海三高速后,沿昌化江一路前进,沿途一步一景,全程 131 公里,车程约 2 小时。
如果出发较早,从玉龙山观景台可以导航到王下乡看木棉花,3 月正是木棉花开的时节。途中会经过大广坝水库,值得一看。全程 51 公里,车程约 2 小时。
携程租车
优势:供应商多、车型全、交互方便、价格相对较低,每周三会员日可以领取 50 元无门槛代金券。
枫叶出行
优势:新车、新车、新车
劣势:价格稍贵,只有 A4L、320、C200、E260 和一些跑车等少数车型可选。
新用户的话可以用我的这个邀请链接 https://share.fyrentcar.com/staticPage/index.html?mobileShare=941035f6faa67ab8edbac3d8fdf761c74321b5454d61e033&rand=ae75fabc-6efc-4a8e-a36e-9e6f042c7297 注册,可以得到一张7折优惠券。
飞猪租车
优势:遇到问题可以内网发帖找飞猪运营帮忙解决。。。。
劣势:交互极其反人类
一嗨/神州/凹凸等
优势:平台保障、取还方便(三亚海口机场均支持自助)、价格优惠
劣势:车型较少、车龄较长
白嫖怪的福利
5.1 哪吒汽车48小时深度体验试驾
路径: 支付宝 - 芝麻信用 - 芝麻粒 - 信用试用 - 哪吒汽车48小时深度体验试驾
5.2 运通白金信用卡持卡人蔚来 ES8 36小时免费体验
可以叠加两张附属卡,一次刷满36*3小时,还可以顺便白嫖3张蔚来中心咖啡券。
活动地址:https://www.amexpressnetwork.com/rewards/product/204 (好像被公司屏蔽了,需要开代理)。
在我写下上一篇文章之后两周,2019.03.19,Babel@7.4.0
正式发布了。在 Babel@7.4.0
中,提供了对管道运算符、私有方法、TypeScript 3.4 等的支持,同时,polyfill 开始支持 core-js@3
。对于 core-js ,可能有些人还很陌生,根据 npmtrends 的统计, core-js 事实上已经是目前最流行的 polyfill 方案了,@babel/polyfill
就是靠它来转换代码的,只是很多人可能没有留意到其实自己已经在间接使用它了。回想一下执行 npm install
的时候,是不是对 As advertising: the author is looking for a good job -)
似曾相识? 这条广告还引发过技术人员能不能在 npm log 中为自己打广告的争论, npm fund
也是为此而生。
提到这个,顺便想起了前几天在这条 issue 中看到的一件事, core-js 的作者看起来因交通肇事被判处了 1.5 年的监禁,希望他好好改造重新做人 →_→
在 babel@6.x 版本中,我们通常使用 .babelrc
作为 babel 的配置文件,但是在实践中,遇到了很多问题。现在 babel 团队建议使用这两种方式来对 babel 做出配置:
Project-wide configuration
babel.config.json
files, with the different extensionsFile-relative configuration
.babelrc.json
files, with the different extensionspackage.json
files with a ‘babel’ key
更详细的信息可以参考这里: https://babeljs.io/docs/en/config-files
Babel 团队建议使用 .browserslistrc
文件来指定需要兼容到的浏览器,这样该配置可以被 autoprefixer、 eslint
等其他工具共用。
1 | // 示例: |
在之前的版本中,如果我们将 babel 的 useBuiltIns
属性设置为 entry
或 false
,我们需要在代码中手动引入 @babel/polyfill
,现在则只需要引入 regenerator 和 core-js 就可以了。在代码入口文件前引入它们,可以模拟完整的 ES2015+ 环境(不包含 < Stage 4 的提案)。
1 | // before |
而如果我们对构建产物的大小有限制,我们可以继续使用 useBuiltIns: usage
来按需导入所需的 polyfill 内容。
1 | // babel.config.json |
我们曾经提到过,transform-runtime 的方案不支持如 "foobar".includes("foo")
这样的实例方法。在 core-js@3 中,这个问题得到了解决。
1 | npm remove @babel/runtime-corejs2 |
1 | // babel.config.json |
如果我们需要支持 Stage < 4 阶段的提案,可以配置 proposals
属性来实现:
1 | // babel.config.json |
但是,@babel/plugin-transform-runtime
依然存在问题:它不能像 @babel/preset-env
一样指定 target。也就是说,哪怕针对现代浏览器,它依然会注入大量并不需要的内容,从而使得构建产物的体积大大增加。针对这个问题, babel 团队也还在思考中: https://github.com/babel/babel/issues/10008
在 Babel < 7.4.0 的时代,我们可能都听说过,由于 @babel/polyfill
有全局污染,如果我们在开发一个库/框架,我们只能使用 @babel/runtime
来进行部分 polyfill,其余部分需要库的使用者自行通过 @babel/polyfill
来完成。
现在,如果我们对文件大小不是特别敏感,我们可以不用顾虑这个问题了,直接使用 @babel/preset-env
来进行语法转换,剩余部分 @babel/runtime
即可完成。附上一份完整的 babel.config.json
配置供参考:
1 | // babel.config.json |
如果我们的环境对性能有极致要求,我们则需要使用 @babel/preset-env
并设置 useBuiltIns: "usage"
来完成 ES6+ 的转换。而想要避免全局污染,则需手动引入我们代码中使用到的新特性相关的插件,如 @babel/transform-es2015-classes
、 @babel/transform-es2015-arrow-functions
等。附上一份 @babel/preset-env
配置参考:
1 | // babel.config.json |
UltraFine 4K 看起来倒是很合适,但是用惯了27寸,总觉得24寸的屏幕实在太小。看来看去,3000-5000 的价位只剩下3款备选:DELL U2718Q
、BenQ PD2700U
、LG 27UL850
,对比了很久,还是觉得都不完美,正想着就明基算了,毕竟号称护眼。下单前论坛闲逛忽然发现 DELL 更新了 U2720Q,居然带有 90W Type-C 接口,虽然 MacBook Pro 16 的标配充电是 96W,90W 也勉强凑合吧,论坛内搜了一下,已经有了开箱体验,看起来还不错,果断决定入手。
官网联系客服,第一个就直接报价 3970 五年保修,听说有 3850 拿到的,但是换了几个售前,一个比一个高,懒得折腾了,3970 下单。2月17日付款,2.21日从厦门发货,嘉里大通物流2.23到杭州。
没有专业的校色软件,随便找了个网页的在线测试娱乐一下:
没有找到在哪里看面板型号,工程模式里也没看到,盲猜应该是 LM270WR3-SSA3
或者 LM270WR5-SSB1
?
和之前用的 AOC U2790VQ 对比一下:
两款都设置成 sRGB 模式,不知道我是不是瞎了,我竟然觉得看照片好像差不多?肉眼直接看上去还是有比较明显的差距的,DELL 的色彩明显更浓郁艳丽。
再来几张,左 DELL 右 AOC:
优点还是很明显的,出厂校准的 99% sRGB、95% DCI-P3,最重要的优点,如果你的设备支持 Type-C 输出,桌面上只需要一根 Type-C 线即可实现视频音频传输、USB-HUB、反向 90W 充电等功能,从此可以把各种绿联之类的辣鸡都扔掉了,目前 4000 的价位上应该没有其他能打的了。
各种宣传渠道都标出了的 DP1.4(支持MST菊花链) 功能根本没有,想要双显示器,必须再从电脑上再连一根 DP 或 HDMI 线,或者拿另一台支持 MST 的显示器作为主显示器。我查了一下,25 寸的版本 U2520DR 上反倒有这个功能,不懂为什么高端一点的版本反倒缩掉了这个接口。
虽然显示器附带了很多 USB 下行接口,但是要使用这些接口,必须使用 USB-C 转 USB-C 线或 USB-A 转 USB-C 线从电脑连接到显示器背后的 TYPE-C 接口,这样的话如果需要经常在 Mac 和 Windows 电脑之间切换,就必须频繁插拔这根线,在之前的众多型号(比如 U2718Q)上,是一个 USB-B 上行接口就解决了的问题,同样不知道为什么缩水掉了。
之前一直听说戴尔显示器售后天下无双,无论什么渠道购买都一样,售后有专门的工程师一对一处理。
结果刚开箱就体验了一波,附送的 USB-A 转 USB-C 线是坏的。联系售后,客服一脸懵逼,转接给所谓的售后工程师,指导我拍了N张照片,插拔N次之后,我发现他居然连USB-B和USB-C都不认识,而且对相应的显示器型号、接口等完全不了解。最终证明给他是这根线有问题之后提出帮我换一根,催了几次之后才告诉我要第二天才能提交到系统上去,号称发戴尔内部物流,看不到进度,查不到什么时候发货,也不知道大约多久可以到,『您只需要耐心等待就可以了呢』。。。要不是觉得退回去会被别人买到一个二手的,真的好想直接退了再买个新的。
以后再也不会为了 200 块在戴尔官网买东西了,还是要去和东哥做兄弟。
]]>本文所用环境:
操作系统: Linux September 5.3.11-1.el7.elrepo.x86_64 GNU/Linux
内核版本: centos-release-7-7.1908.0.el7.centos.x86_64
目前,最新版本的 nginx (1.17.5) 仍然不能原生支持 HTTP3,好在 CloudFlare 提供了基于 Quiche 和 Boringssl 实现的一个 patch,使得我们可以在 nginx 上尝鲜一下。
开启过程如下:
1 | wget https://nginx.org/download/nginx-1.17.5.tar.gz |
1 | # 前往 https://cmake.org/files/ 选择最新版本下载 |
1 | # 前往 https://www.cpan.org/src/ 选择最新版本下载 |
1 | # 为了支持 quiche,Rust 需要至少 1.39 版本 |
在 nginx 的编译参数中加入如下模块,编译安装
1 | --with-http_ssl_module --with-http_v2_module --with-http_v3_module --with-openssl=../quiche/deps/boringssl --with-quiche=../quiche |
安装完成之后,在 nginx 的配置文件中加入如下信息,重启 nginx 即可
1 | server { |
完成之后,我们可以在这里查看配置是否成功: https://http3check.net/
TLS1.3 需要 OpenSSL 1.1.1 以上版本,系统自带版本一般较旧,我们需要重新安装(前文所述的 quiche patch 中已经内置了 Boringssl,不需要再重新安装)
首先安装所有可能需要的依赖:
1 | yum install gc gcc gcc-c++ pcre-devel zlib-devel make wget openssl-devel libxml2-devel libxslt-devel gd-devel perl-ExtUtils-Embed GeoIP-devel gperftools gperftools-devel libatomic_ops-devel perl-ExtUtils-Embed dpkg-dev libpcrecpp0 libgd2-xpm-dev libgeoip-dev libperl-dev -y |
然后从官网 https://www.openssl.org/source/ 获取最新的 OpenSSL 下载链接,下载并解压编译安装:
1 | cd /usr/src |
完成后使用openssl version查看版本,如出现 /usr/bin/openssl: No such file or directory,可以使用如下方式解决:
1 | ln -s /usr/local/lib64/libssl.so.1.1 /usr/lib64/libssl.so.1.1 |
使用最新的 OpenSSL 重新编译 nginx:
1 | # 在 nginx 编译信息中加入如下参数 |
安装完成之后,修改 nginx 配置:
1 | server { |
重启 nginx,打开网站,在控制台 - Security - Overview - Connection 中可查看 TLS1.3 是否开启成功,如图所示:
下载 Brotli:1
2
3git clone https://github.com/google/ngx_brotli
cd ngx_brotli
git submodule update --init
在 nginx 的编译参数中加入 --add-dynamic-module=/path/to/ngx_brotli
,重新编译安装 nginx,即可使 nginx 支持 brotli 压缩。
新版本的 nginx 支持动态模块,也可以只编译 brotli 模块。
安装完成后,在 nginx 配置文件中加入如下信息,重启 nginx,访问网站,应该就可以看到压缩方式为 br 了。
1 |
|
https://www.mf8.biz/nginx-install-tls1-3/
https://zach.vip/server/%e4%bd%bf%e7%94%a8cloudflare%e7%9a%84quic%e5%ae%9e%e7%8e%b0quiche%e9%83%a8%e7%bd%b2nginx%e7%9a%84http3-quic%e5%8d%8f%e8%ae%ae/
max-age
,而在资源内容有更新时,则生成一个名字不同的新文件,以便用户能够及时收到最新的内容。我们都知道, HTTP 缓存可以分为强缓存和协商缓存,根据我们的配置,强缓存过期后,浏览器会向服务器发出一个校验请求,如果服务器判断资源没有更新过,则通知浏览器使用缓存,也就是我们常说的 304 响应。
但是在现代前端工程体系下,一个静态资源从发布之日起,可能永远也没有更新的需求了(更新后文件名/版本号/hash值等参数一定会变,也就是相当于一个全新的资源了)。但是即使我们配置了较长的 max-age ,用户在刷新页面的时候,浏览器依然会向服务器发出请求,在用户体量较大的时候,对网络资源也有着极大的浪费。按照 Facebook 的统计,大约有 20% 的请求是响应了 304 的。为此, Facebook 建议给 Cache-Control
增加一个新的属性: dont-revalidate
,在响应头里加入该字段表示该资源永不过期,不需要再发送条件请求。这个字段最终被 Firefox 49 根据 Cache Control Extensions 规范实现为一个 Behavioral extension
: immutable
。我们可以这样使用:
1 | Cache-Control: max-age=31536000, immutable |
对于支持 immutable 的浏览器,会一直使用本地的缓存,而对于尚未支持的浏览器,也可以根据 max-age 平滑的降级。
- Firefox: 49+ (仅支持HTTPS请求)
- Microsoft Edge: 15+
- Safari: 24+
- Chrome: 不支持,Chrome 自己实现了另一个解决这个问题的方案
在看 Chrome 的新方案之前,我们先来看一下浏览器在刷新时做了什么:
可以看到,在按下 F5 时,Chrome 为请求加上了 max-age=0
的 Cache-Control,从而保证至少要向服务器进行一次过期校验。而在 Ctrl + F5 时,则更为激进的直接设置了 no-cache
。
这里有一张较详尽的表格可以参考:
好像和 immutable 关系不大?我们再来看看 Chrome 中静态资源的刷新:
为了方便测试,我们给资源设置了一个 8 秒的 max-age,等待 3 秒后,我们按下 F5:
好像有哪里不对?说好的 max-age=0 呢?说好的 304 呢?
一番操作之后我们找到了这里:
https://bugs.chromium.org/p/chromium/issues/detail?id=654378
https://bugs.chromium.org/p/chromium/issues/detail?id=611416
可以看到,在 Chrome 54+ 版本中,Chrome 开始在刷新页面时只为 main resource
也就是我们的 html 页面加上 max-age=0
的设置,其他资源会遵循资源自身设定的缓存策略。通过修改 reload 的行为, Chrome 同样大量降低了刷新页面带来的带宽消耗。
还是上面的页面,我们等待 10 秒之后再刷新一次验证一下:
可以看到, max-age=8 的静态资源响应变成了 304 ,符合我们的预期。
由于我们的静态资源更新依赖于 html 中资源地址的更新,通常我们会对 html 文件设置为不缓存或仅缓存极短的时间。而绝大部分静态资源本身自发布起就不再需要更新了,所以我们可以大胆的将静态资源的缓存时间设置的足够长(比如一年),同时加入 immutable 属性,这样,几乎所有的现代浏览器都会帮我们取消缓存协商的过程,从而节约大量协商请求的带宽和流量。
]]>在我们执行 npm install 时,npm 会做出如下操作:
~/.npm
目录node_modules
目录而在 Node.js 中,我们提供给 require 方法的参数如果不是一个路径,也不是 node 的核心模块, node 将试图去当前目录的 node_modules 文件夹里搜索。如果当前目录的 node_modules 里没有找到, node 会继续试图在父目录的 node_modules 里搜索,这样递归下去直到根目录。
这些过程都需要进行大量的文件 I/O 操作,这无疑是非常低效的。为了解决这些问题,Facebook 提出了 Plug’n’Play(PnP) 方案。
在 Yarn 中,当我们开启 PnP 后,Yarn 会生成一个 .png.js
文件来描述项目的依赖信息和所需模块的查找路径。同时,项目目录下不再需要一个 node_modules 目录,取而代之的是一个全局的缓存目录,项目所需依赖都可以从这个目录中获取。
可以在项目目录下执行 yarn --pnp
,或直接在 package.json 中修改如下字段开启 PnP:1
2
3
4
5{
"installConfig": {
"pnp": true
}
}
如果你使用 CRA 来创建项目,也可以直接在命令中加入 --use-pnp
。
按照官方的说法, Generating it makes up for more than 70% of the time needed to run yarn install with a hot cache.
在我的一个实际项目中,使用 npm i、yarn 和 PnP 安装依赖完成时间分别为 26.48s、19.71s 和 11.25s,提升极为可观。
pnpm 使用了 symlink 来记录模块路径,如下所示:
1 | -> - a symlink (or junction on Windows) |
pnpm 在维护了模块层级的同时大幅度提升了安装速度,但是由于它的实现,无法保证和 npm 行为一致。
tink 是 npm 官方提出的一种类似 PnP 的解决方案,和 .pnp.js
文件类似, tink 会在项目中生成一个 .package-map.json
文件用来记录各安装包内文件的 hash 值。目前 tink 依然处于测试阶段,在 npm 8 中我们将能尝试这一特性。
在远古时代的代码中,我们可能看到过这样的处理:
1 | <script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/0.34.2/es5-shim.min.js"></script> |
shim
仅依靠旧版浏览器中现有的 API,实现了一些新版本 ES 的特性,而 sham
则是仅仅为了保证新版本特性能在浏览器中不抛出错误(因为有些新特性在低版本浏览器中根本无法实现)。
为了彻底解决上面的问题,Babel
诞生了。Babel 这个名字指的是巴比伦文明里面的通天塔,在那里,人们只说同一种语言。而在前端工程中,我们可以依靠 Babel 将 ES6+ 的语法转换为旧浏览器可以识别的版本。
在浏览器端,Babel 提供了一个运行时的转换器 babel-standlone
,它内置了大量的插件,所以可以直接在浏览器中运行并编译特定标签(type=”text/babel” 的 script 标签)内的代码。
而在 Webpack 中,我们则可以使用 babel-loader
来完成语法的转换。
但是 Babel 并不是万能的,它默认只转换 JavaScript 语法,而不转换新的 API,比如 Promise、Set、Map 等全局对象, Array.from、Object.assign 等全局静态函数和 Array.prototype.includes 等 实例方法也不会被转码。为了解决这个问题,我们需要使用 polyfill。
polyfill 可以认为是 shim 的一种,和常规的 shim 不同的是,它致力于抹平不同浏览器之间的差异。
1 | <script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script> |
我们可以在页面中引入这样一个 polyfill.io 提供的服务来实现最简单的 polyfill。服务器会根据用户浏览器的 UA 来判断对新特性的支持程度而返回不同的 polyfill 文件,如果用户的浏览器足够强大,那么这个服务将不返回任何内容,做到了一定意义上的按需加载。
但是它的缺点也很明显,这种方式并不能按照代码所用到的新特性按需进行 polyfill,而是会 polyfill 用户浏览器不支持的所有特性。
babel 为我们提供了几乎所有新特性的插件,比如 @babel/transform-es2015-classes
、 @babel/transform-es2015-arrow-functions
等,使用方式如下:
1 | // .babelrc |
这样,babel 就可以帮我们转换指定的特性了,可以看一下下面的栗子:
1 | const foo = (a, b) => { |
可以看到,@babel/plugin-transform-object-assign 使用 ES5 自己实现了一份 Object.assign 并插入到了我们的代码之前。
但是这样做有两个问题,一是在不同的模块中如果都使用了 Object.assign,它可能在每个使用到的模块中都被实现一次并将这些 helpers 代码添加到模块顶部,会大幅增加构建包的体积;另一个问题则是我们需要手动为我们用到的特性添加所有的插件。
在这个背景下, @babel/plugin-transform-runtime
应运而生。在 Babel 的插件配置中,我们只需要引入一个 @babel/plugin-transform-runtime ,它会自动检测我们代码中需要转换的部分并重写,例如 Object.assign 会被重写为 _extends 并引入,而重复的模块也会被抽离成一份。配置如下:
1 | // .babelrc |
和单独引入各个插件一样,transform-runtime 同样存在模块级别的限制,因此它也依然无法转换 Array.prototype.includes 这样的方法。
对于这种全局方法,Babel 提供了 babel-polyfill,我们可以这样引入:1
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.2.5/polyfill.js"></script>
显然,这样会全量引入所有的补丁,不论你的项目是否需要。
也可以这样使用 npm 包的形式加载:
1 | import '@babel/polyfill'; |
和使用 script 标签引入类似,全量引入后,这个仅有2行的原始文件,打包后的大小变成了 246K:1
2
3
4
5
6
7
8Build completed in 1.286s
Hash: 3103b6ec6e9cb29c304b
Version: webpack 4.29.6
Time: 1464ms
Built at: 2019-03-01 16:05:13
Asset Size Chunks Chunk Names
app.js 246 KiB app [emitted] [big] app
为了实现按需引入,我们需要依靠 @babel/env
,而在 Babel7 中,提供了 useBuiltIns: usage
这样一个新配置。结合配置如下:
1 | "presets": [ |
useBuiltIns 配置为 entry 时, 会根据指定的 targets 中的 browserlist 将polyfills拆分引入,仅引入有浏览器不支持的polyfill。而配置为 usage ,babel 则会检测代码中新特性等的使用情况,仅仅加载代码中用到的 polyfills,同时,不需要再手动 import '@babel/polyfill'
。browserlist 的配置可以在这里查看: https://browserl.ist/
重新打包,app.js 恢复了正常大小:1
2
3
4
5
6
7
8Build completed in 0.769s
Hash: 21afcbb413dad4242589
Version: webpack 4.29.6
Time: 946ms
Built at: 2019-03-01 16:13:23
Asset Size Chunks Chunk Names
app.js 22.8 KiB app [emitted] app
对于常规项目来说,我们可以手动指定要兼容到的 targets,进行如下配置来保证尽可能的安全并且按需加载:
1 | { |
而如果在开发的是一个库,为了避免全局的污染,则只能使用 transform-runtime 的方案,这种方案不能解决 Array.prototype.includes 等实例方法的转换,如果库中有应用,需要在文档中特意提醒开发者注意。
在HTML5中,有一个新 API module
,我们可以使用这个特性来检测浏览器对 ES6的支持程度,例如,支持 <script type="module">
的浏览器也支持 async、await、Class、Promise 等新特性。
因此,我们可以打包出2份 JavaScript 文件同时引入:
1 | <script type="module" src="app.js"></script> |
大部分现代浏览器都会自动识别 script 标签中的 type="module"
和 nomodule
属性,加载 app.js 忽略 app-legacy.js。
我们只需在 app-legacy.js 中添加 polyfill 即可,这样,在兼容低版本浏览器的同时,我们也将在现代浏览器中体验到巨大的性能提升。
https://www.babeljs.cn/docs/usage/polyfill/
https://github.com/sorrycc/blog/issues/80
https://segmentfault.com/a/1190000010106158
本文基于 Webpack@4.28.3 版本
在现代前端工程中,为了更有效的利用浏览器和 CDN 的缓存,我们通常会给静态资源设置一个比较长的缓存时间(cache-control等),在有内容更新的时候,通过在静态资源路径中加入 hash 的方式来实现。具体可以参考这里: 大公司里怎样开发和部署前端代码?。
但是在 Webpack 中,实现可靠的长期缓存并不容易。在 github 上,关于这个问题的 issue 已经讨论了三年之久。在 Webpack 4 时代,我们终于看到了一点希望。
我们从一个最基础的 Webpack 配置开始:
1 | // webpack.config.js |
构建一下,得到如下结果:
1 | Asset Size Chunks Chunk Names |
看起来好像很完美。
在实际项目中,我们通常会选择将公共模块提取成一个单独的文件,在 Webpack 3 时代,我们一般会选用 CommonsChunkPlugin
,在 Webpack 4 中,我们则可以使用 splitChunks
来实现这一需求。这里我们将来自 node_modules 的模块提取成 vendors.js
:
1 | // webpack.config.js |
打包结果如下:
1 | Asset Size Chunks Chunk Names |
你可能已经注意到了,我们的 app 和 vendors 的 hash 是相同的。这样打包,对 app 模块的任何更改同时也会导致我们的 vendors 模块的 hash 失效。
要解决这个问题,我们必须在文件名中把 hash 改成 chunkhash
。这是因为 hash 会为我们构建的所有内容生成一个全局哈希,而 chunkhash
只会使用它自己的模块中的内容来生成哈希值。
修改 webpack 配置如下,再次构建,我们得到了两个不同的哈希值:
1 | // webpack.config.js |
1 | Asset Size Chunks Chunk Names |
现在,更改 app 模块中的内容, vendor 模块不会再受影响了(最新版本的 Webpack 中,不再需要为了 hash 的稳定单独提取出 runtime 了)。
我们来添加一行代码测试一下:
1 | // app.js |
打包之后,很完美:
1 | Asset Size Chunks Chunk Names |
随着项目的增长,我们不可避免的出现了更多的依赖:
1 | // app.js |
我们再来构建一次。在这次构建中,显然我们只希望 app.js 的 hash 值更新,但是事情总是不尽如人意的:1
2
3
4Asset Size Chunks Chunk Names
app.adcbdfaa.js 2.44 KiB 0 [emitted] app
index.html 319 bytes [emitted]
vendors.a7e68620.js 114 KiB 1 [emitted] vendors
尽管我们的 vendors 模块没有任何变化,它的 hash 还是又一次改变了。原因又是一个 Webpack 中的细节: Webpack 会为每个 chunk 按顺序给出一个依次增加的 chunk id,随着新依赖的增加,chunk 顺序也可能会发生变化,于是 chunk id 也会随之更新。
为了解决这个问题,我们引入了一个新的插件: NamedChunksPlugin
。这是在 Webpack 2.4 版本中加入的一项特性,借助它我们可以让模块有自己的名字,而不是冷冰冰的数字(Webpack 4 中, development mode下已默认开启)。
1 | // weback.config.js |
这样配置,Webpack 将使用唯一的 chunk 名称而不是其 id 来标识一个 chunk。
在最新版的 Webpack 中,我们也可以使用 optimization.chunkIds 来达成同样的效果:
1 | // weback.config.js |
我们在添加和不添加 hello.js 的情况下分别再构建一次,应该就可以看到,vendor 块的哈希是保持不变的了。
1 | // 不添加 hello.js |
好吧,并没有。
这是因为和 chunk id 类似,Webpack 同样会对 module 使用自增数字命名。类似地,我们可以使用 NamedModulesPlugin
或 HashedModuleIdsPlugin
来命名 module,从而使 hash 固定。其中 NamedModulesPlugin 使用 module 的路径来命名,生成的名字更可读,但是和使用 module 路径生成的4位(可能出现重复的情况下位数会增加) hash 命名 module 的 HashedModuleIdsPlugin 相比,会明显增大文件体积,适合用于开发环境,生产环境适合使用 HashedModuleIdsPlugin。
和 chunkIds 类似,在最新版本的 Webpack 中,我们也可以使用 optimization.moduleIds
来配置这一功能(development 模式下 optimization.namedModules
已默认开启)。
继续更新我们的 Webpack 配置:
1 | // webpack.config.js |
再次更改 app 模块的内容打包,我们可以看到,更新前后其余文件的 hash 是不变的了。
1 | // 不添加 hello.js |
为了首屏性能等需求,我们不可避免的需要在项目中使用异步模块。我们添加一个异步模块再次打包:
1 | // router.js |
1 | Asset Size Chunks Chunk Names |
可以看到 vendors 的 hash 又更新了。打开对应的文件,发现这样一段内容:
1 | window["webpackJsonp"] || []).push([[0], |
显然,对新加入的这个异步模块的命名失效了,又变成了从 0 开始的自增序列。
查看源码可以得知,NamedChunksPlugin
仅对有 name 的 chunk 有效,但是可以通过自定义 nameResolver
的方式来实现我们需要的功能:
1 | new webpack.NamedChunksPlugin(chunk => { |
还有一个更简单的方案则是使用 Webpack 的魔法注释来给异步模块命名:
1 | // router.js |
打包结果如下:
1 | Asset Size Chunks Chunk Names |
可以看到,vendors 的 hash 恢复到了之前的状态。
是不是感觉好像还缺点什么?
在生产环境中,我们通常会将 css 模块打包为独立文件。我们增加一个 css 模块来看一下:
1 | // app.js |
1 | // webpack.config.js |
改变一下 hello.css 的内容,打包两次,对比如下:
1 | Asset Size Chunks Chunk Names |
只修改了 CSS 文件,app.js 的 hash 值又变了。很容易理解,毕竟它们属于同一 chunk。
为了解决这个问题,Webpack 4.3 中引入了一个新的概念: contenthash
。
修改 Webpack 配置如下:
1 | // webpack.config.js |
再次打包对比更改 css 文件前后的变化,只有 css 文件的 hash 改变了。
1 | Asset Size Chunks Chunk Names |
有些场景下,我们需要从 CDN 引入一个模块,以 jQuery 为例,一般会如下配置:
1 | <!-- index.html --> |
1 | //webpack.config.js |
1 | // app.js |
和异步模块类似,NamedChunksPlugin 也不会对 externals 模块起作用,我们可以参考这里,使用 NameAllModulesPlugin
来做一些相关的配置,从而为它命名。
目前,Webpack 已经发布了 v5.0.0-alpha.3 版本。
在 Webpack 5 中,采用了全新的算法来生成 chunkIds 和 moduleIds,在打包后的文件大小和控制缓存之间有了一个更好的平衡。
在 Webpack 4 的更新说明中,开发团队提到了 Webpack 5 中会有开箱即用的长缓存配置,在这个 alpha 版本中,我们也看到了 cache: { type: "filesystem" }
这样的配置,然而,这个策略依然是实验性质的。希望在正式版本中能一劳永逸的解决这个问题。
https://medium.com/webpack/predictable-long-term-caching-with-webpack-d3eee1d3fa31
https://webpack.js.org/guides/caching/
https://zhuanlan.zhihu.com/p/38456425
https://segmentfault.com/a/1190000016355127
https://segmentfault.com/a/1190000015919928
https://github.com/pigcan/blog/issues/9
先定一个小目标,争取把 Webpack 的打包时间优化到10秒以内吧。
先看一下现在打包一次需要的时间,73013ms,下面开始一步一步见证奇迹:
有很多工具提供了可视化的分析,如Webpack-bundle-analyzer、webpack-chart、 webpack-analyse。
以Webpack-bundle-analyzer为例,它提供了一个下图所示的图表,展示了引入的所有模块的大小、路径等信息,可以针对性的做出优化。
使用上也很简单:
1 | // 安装: |
1 | // webpack.config.js 配置 |
运行webpack
命令,会自动在浏览器中打开http://127.0.0.1:8888/
页面,展示可视化图表。
Webpack4 带来了极大的性能提升,按照开发者博客中的说法,构建速度最多甚至有高达98%的提升。
升级过程中遇到了一些网上的“Webpack4升级指南”等文章中没有列出的问题,在此分享一下:
1.1 升级 Vue-loader
Vue-loader 目前最新版本为 v15.2.6,使用方式有了很大不同。
现在,我们需要引入一个新的插件 VueLoaderPlugin
,具体使用方式如下:
1 | // webpack.config.js |
同时,在 v15版本的 Vue-loader 中,不再需要单独为 .vue 组件中的模板、CSS等内容单独配置 loader,可以共用普通文件的配置,如下所示:
1 | // webpack.config.js |
1.2 升级 Vue-router
在Vue-router v13.0.0版本中对模块导入做了更新,需要加入 default 配置,如下所示:1
2
3const Foo = () => import('./Foo.vue')
// 需要改为
const Foo = () => import('./Foo.vue').then(m => m.default)
同理:1
2
3const Foo = require('./Foo.vue')
// 需要改为
const Foo = require('./Foo.vue').default
详情可以参考https://github.com/vuejs/vue-loader/releases/tag/v13.0.0
1.3 Chunk 的命名
如果使用 webpackchunkname 魔法注释来命名,需要注意 .babelrc 中 comment 必须为true
1.4 提取 CSS 文件
在 Webpack4 环境下 extract-text-webpack-plugin
需要安装 @next
版本,我们这里直接使用了 mini-css-extract-plugin
来代替。
同时,在 mode 为 development
时,NamedChunksPlugin
和 NamedModulesPlugin
会默认开启,不需要再显式指定。
1.5 本地 mock 处理
2.x版本的 Vue-CLI 启动了一个 express 服务来处理本地数据的 mock,我们尝试做了一些简化,在 webpack-dev-server 的 before
方法中,使用 webpack-api-mocker
插件拦截了请求,读取本地的 mock 数据(JSON文件)返回。其中,mock/index.js
是通过服务启动过程中遍历本地数据文件生成的。
1 | // webpack.dev.conf.js |
1 | // mock/index.js |
升级完成之后,打包时间直接减少了半分钟,达到了44.534秒,离小目标还有很大距离,我们继续。
在前面的打包完成的图片中,我们可以看到生成了大量的文件,统计了一下,体积总计高达22.07M,文件近60个。
权衡了诸如加载时间等方面之后,我们决定采用按照一级路由来打包的方式。
具体实现如下: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
29Vue.use(Router);
import Vue from 'vue';
import Router from 'vue-router';
export default new Router({
routes: [{
path: '/a',
component: () =>
import ( /* webpackChunkName: 'a' */ '@/pages/a'),
name: 'a'
}, {
path: '/b',
component: () => import ( /* webpackChunkName: 'b' */ '@/pages/b'),
name: 'b',
children: [{
path: 'b/m',
component: require('@/pages/b/m').default,
name: 'm',
children: [{
path: 'b/m/p',
component: require('@/pages/b/m/p').default,
name: 'p'
}, {
path: 'b/m/q',
component: require('@/pages/b/m/q').default,
name: 'q'
},
]
}]
}]
经过上面的优化,我们将生成的js文件数量减少到了8个,大小减小到了5M.
来看一下打包时间:21.959秒!距离小目标越来越近了。
HappyPack 可以将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展为多进程的模式,从而加速代码构建。使用方式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module: {
loaders: [{
test: /\.less$/,
loader: ExtractTextPlugin.extract(
'style', path.resolve(__dirname, './node_modules', 'happypack/loader') + '?id=less'
)
}]
},
plugins: [
new HappyPack({
id: 'less',
loaders: ['css!less'],
threadPool: happyThreadPool,
cache: true,
verbose: true
})
]
经过测试,在我们的项目中,对 js 和 ts 文件使用 happypack 收益最大。
此处需要注意的是,vue-loader 不支持 happypack,可以使用 thread-loader
来进行加速,同样是新建一个进程来执行 loader 操作,使用方式也很简单:1
2
3
4
5
6
7
8
9 module: {
rules: [{
test: /\.vue$/,
use: [
'thread-loader',
'vue-loader'
]
}]
}
但是在我们的项目中,经过测试,thread-loader 对于打包速度几乎没有影响,是因为它本身的额外开销导致,建议只在极高性能消耗的场景下使用。
完成之后,测试一下打包时间:15.101秒。
我们可以对loader做如下配置来开启缓存:1
loader: 'babel-loader?cacheDirectory=true'
或者我们也可以使用 cache-loader :1
2
3
4
5
6
7rules: [{
test: /\.vue$/,
use: [
'cache-loader',
'vue-loader'
]
}]
加入缓存之后,再次测试打包时间:13.915秒。
在 Webpack4 中移除了我们此前常用的 CommonsChunkPlugin
插件,取而代之的是 splitChunks
。splitChunks
的默认配置已经足够我们日常使用,没有特殊需求可以不必特意处理。
我们此处的配置如下(生产环境):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
// styles: {
// name: 'index',
// test: /.stylus|css$/,
// chunks: 'all',
// enforce: true
// }
}
}
}
其中,commons 部分的作用是分离出 node_modules 中引入的模块,styles 部分则是合并 CSS 文件。
经过测试,在我们的项目中,styles 部分使构建时间增加了大约2秒,因此我们放弃了这部分操作。
开发过程中,我们经常需要引入大量第三方库,这些库并不需要随时修改或调试,我们可以使用DllPlugin和DllReferencePlugin单独构建它们。
具体使用如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
vendor: [
'axios',
'vue-i18n',
'vue-router',
'vuex'
]
},
output: {
path: path.resolve(__dirname, '../static/'),
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'build', '[name]-manifest.json'),
name: '[name]_library'
})
]
}
执行webpack命令,build目录下即可生成 dll.js 文件和对应的 manifest 文件,使用 DLLReferencePlugin 引入:1
2
3
4
5
6plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./build/vendor-manifest.json')
})
]
由于我们的项目中原本已经通过这种方式打包了大部分第三方库,所以这里对打包速度的提升不大,仅仅提升2秒,来到了11.509秒。
在我们的项目中,引入了一些如 moment、lodash 等重型库,然而他们提供的绝大部分功能都是我们不需要的,权衡之后,我们移除了他们,自己实现了部分功能或使用了更小体积的库代替。
移除了这些库之后,我们的打包时间来到了8.921秒!小目标达成了~
但是这距离10秒发布还不够,我们需要争取压缩出更多时间留给发布系统。还能不能继续提升呢?答案是肯定的。
Node.js的模块的载入及缓存机制如下:
载入内置模块
载入文件模块
载入文件目录模块
载入node_modules里的模块
自动缓存已载入模块
如果模块名不是路径,也不是内置模块,Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到,Node会从父目录的node_modules里搜索,这样递归下去直到根目录。
我们可以对搜索过程进行一些优化,比如可以像下面这样指定路径:1
2
3
4
5
6
7
8exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, 'src') // 精确指定要处理的目录
resolve: {
modules: [path.resolve(__dirname, 'node_modules')], // 指定node_modules的位置
alias: {
'api': resolve('src/api') // 创建别名
}
}
我们再来看一下时间:7.66秒!
到这里,我们的这次优化基本完成了,其实还有很多可以优化的空间,比如升级一颗 i9 处理器~
这里也只是列举出了一些常见的收益较大的优化方式,希望能对大家有一点帮助,也欢迎有兴趣的同学一起交流。
1 | $ npm init / install / uninstall / update |
1 | $ npm ls |
1 | npm config set proxy xxx.com |
1 | $ npm outdated |
1 | $ npm view @test/test-util |
1 | $ cd path/project |
1 | $ npm adduser |
1 | $ npm publish |
1 | $ npm access # 设置权限 |
1 | $ npm help install |
私有包托管在内部服务器或者单独的服务器上;
可以同步整个官方仓库,也可以只同步需要的;
对于下载,发布,有对应的权限管理。
缺点:贵
优点:开源闭源项目统一托管
https://github.com/rlidwka/sinopia
很久不维护了;
权限管理比较弱;
缓存优化不足;
不能做官方仓库的镜像。
https://github.com/verdaccio/verdaccio
优点:免费;本地速度快,带公有库缓存;支持 yarn
缺点:需要自己托管维护
https://github.com/cnpm/cnpmjs.org
配置:
1 | // ~/.cnpmjs.org/config.json |
1 | $ cnpmjs.org start |
开启 BBR 要求 4.10 以上版本 Linux 内核,可使用如下命令查看当前内核版本:
1 | uname -r |
可以得到类似如下的结果:
1 | 3.10.0-514.10.2.el7.x86_64 |
如果当前内核版本低于 4.10,可使用 ELRepo 源更新:1
2
3sudo rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
sudo rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
sudo yum --enablerepo=elrepo-kernel install kernel-ml -y
安装完成后,查看已安装的内核:
1 | rpm -qa | grep kernel |
得到结果如下:1
2
3
4
5
6kernel-3.10.0-123.el7.x86_64
kernel-headers-3.10.0-514.16.1.el7.x86_64
kernel-ml-4.11.0-1.el7.elrepo.x86_64
kernel-tools-3.10.0-514.16.1.el7.x86_64
kernel-3.10.0-514.16.1.el7.x86_64
kernel-tools-libs-3.10.0-514.16.1.el7.x86_64
在输出中看到类似kernel-ml-4.11.0-1.el7.elrepo.x86_64
的内容,表示安装成功。
执行:1
sudo egrep ^menuentry /etc/grub2.cfg | cut -f 2 -d \'
会得到如下结果:1
2
3
4
5CentOS Linux 7 Rescue a0cbf86a6ef1416a8812657bb4f2b860 (4.11.0-1.el7.elrepo.x86_64)
CentOS Linux (4.11.0-1.el7.elrepo.x86_64) 7 (Core)
CentOS Linux (3.10.0-514.16.1.el7.x86_64) 7 (Core)
CentOS Linux (3.10.0-123.el7.x86_64) 7 (Core)
CentOS Linux (0-rescue-2d3f9371c20d3e90a544ccc814d485e3) 7 (Core)
由于序号从0开始,设置默认启动项为1并重启系统:1
2sudo grub2-set-default 1
reboot
重启完成后,重新登录并重新运行uname命令来确认你是否使用了正确的内核:1
uname -r
得到如下结果则升级成功:1
4.11.0-1.el7.elrepo.x86_64
执行:1
2
3echo 'net.core.default_qdisc=fq' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv4.tcp_congestion_control=bbr' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
完成后,分别执行如下命令来检查 BBR 是否开启成功:1
2
3
4
5
6
7
8sudo sysctl net.ipv4.tcp_available_congestion_control
# 输出应为 net.ipv4.tcp_available_congestion_control = bbr cubic reno
sudo sysctl -n net.ipv4.tcp_congestion_control
# 输出应为 bbr
lsmod | grep bbr
# 输出应类似 tcp_bbr 16384 28
1 | # 需先在 firewalld 中开启 http 服务 |
访问 http://[your-server-IP]/500mb.zip
来测试一下下载速度吧~
1 | npm install webpack@next webpack-cli --save-dev |
Webpack 团队切出了 next
分支来开发 Webpack 4,原有的 master
分支将继续 3.x 版本的维护。
默认情况下,使用
production
模式时,会使用UglifyJS自动并行处理压缩工作并缓存。发布了一个新的插件系统(https://github.com/webpack/tapable) ,可以更有效的处理插件。
不再支持 Node 4,从而可以大量使用ES6,对V8引擎也进行了一些优化。
在 Webpack 4 中,不再强制要求指定 entry 和 output 路径。webpack 4 会默认 entry 为 ./src
,output 为 ./dist
。
也就是说,我们可以不再需要一个庞杂的 webpack.config.js
了。
现在在 Webpack 中,需要手动选择生产模式(production)和开发模式(development)。
生产模式中,提供了各种各样的优化,如代码压缩、作用域提升、移除未引用代码(tree-shaking)等,同时还引入了一些如 NoEmitOnErrorsPlugin
这样的原本需要手动使用的插件。
开发模式优化了开发速度和开发体验。同样地,在开发模式中,Webpack 4 提供了 path names
、 eval-source-maps
等功能来优化开发体验和构建速度。
Webpack 4 提供了 sideEffects
的配置,通过该配置,可以极大幅度地减小打包出的文件的体积,例如,从 lodash-es
中单独导入 export
,大小为223KB,而将该选项配置为 false
后,打包体积仅为 3 KB(压缩后)。
使用 ESModule 语法 导入 JSON 时,Webpack 会从 JSON Module 中移除未用到的部分,从而大幅度减小打包体积。
这意味着可以不再通过转换器,直接使用 ES6 语法。
- javascript/auto: (Webpack 3 默认类型) 支持所有的 Javascript 模块系统:CommonJS、AMD、ESM
- javascript/esm: EcmaScript 模块,不支持其他所有模块系统( .mjs 文件的默认类型)
- javascript/dynamic: 仅支持 CommonJS 和 AMD,不支持 EcmaScript 模块
- json: JSON 数据,可以使用 require 和 import 引入(.json 文件的默认类型)
- webassembly/experimental: WebAssembly 模块(目前是 .wasm 文件的默认类型)
Webpack 4 删除了 CommonsChunkPlugin,并默认启用了它的许多功能。此外,对于那些需要细粒度的缓存策略的用户,增加了 optimization.splitChunks
和 optimization.runtimeChunk
它们更灵活。
Webpack 现在默认支持 import 和 export 任意本地 WebAssembly 模块。这意味着你可以自己实现 loader 来 import Rust,C++,C 等语言。
更详细的内容可以参考官方更新日志:https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0
]]>在PC时代,为了保护服务器,浏览器对最大并发数进行了限制,例如 Firefox46 中每一域名下最大连接数为6,IE10 下为8。为了突破这一限制,可以将静态资源置于多个域名下,来提高并发数,使得资源加载速度更快。
同时,可以将不需要发送 Cookie 的请求与主域区分开,可以有效减小请求头大小,使得传输速度更快。
我们知道,在浏览器地址栏内输入一个域名并回车以后,第一步执行的就是DNS解析,将网址转换为IP地址,然后才能开始TCP握手。
DNS解析过程如下:
由于移动端存储空间成本、人为干扰等因素,缓存失效的可能性极高,而如果没有缓存,在移动网络等弱网环境下,DNS解析耗时将变得很长(通常要达到1秒以上)。对于静态资源域名,JS等资源的加载缓慢将直接阻塞页面渲染。所以,为了减少 DNS 请求的时间,收敛域名也成为了性能优化的重要方面,同时收敛域名也有利于更好的利用缓存。
在域名收敛之后,为了解决浏览器同域名下的并发数量限制,可以启用HTTP2(由于新协议中的 Header 压缩等原因,速度要快于SPDY)。
在HTTP2中,提出了多路复用(MultiPlexing)的概念,即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面,从而解决了浏览器的并发限制问题。
https://github.com/amfe/article/issues/1
http://www.alloyteam.com/2016/07/httphttp2-0spdyhttps-reading-this-is-enough/
http://taobaofed.org/blog/2015/12/16/h5-performance-optimization-and-domain-convergence/?utm_source=tuicool&utm_medium=referral
随着项目的发展,项目目录会越来越大,各种库也会越来越多,会直接导致Webpack的构建效率极低,比如下面的例子:1
2
3
4
5
6
7$ webpack
Hash: 6aa4a418e100b6563347
Version: webpack 3.5.5
Time: 20199ms
Asset Size Chunks Chunk Names
index.js 4.27 MB 0 [emitted] index
+ 395 hidden modules
可以看到,在这个项目中有大量的模块,构建一次的时间长达20秒,这显然是不可接受的。
有很多工具提供了可视化的分析,如Webpack-bundle-analyzer、webpack-chart、 webpack-analyse。
以Webpack-bundle-analyzer为例,它提供了一个下图所示的图表,展示了引入的所有模块的大小、路径等信息,可以针对性的做出优化。
使用上也很简单:
1 | // 全局安装: |
1 | // webpack.config.js 配置 |
运行webpack
命令,会自动在浏览器中打开http://127.0.0.1:8888/
页面,展示可视化图表。
DllPlugin
拆分模块开发过程中,我们经常需要引入大量第三方库,这些库并不需要随时修改或调试,我们可以使用DllPlugin
和DllReferencePlugin
单独构建它们。
具体使用如下:
1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); |
执行webpack
命令,build
目录下即可生成 dll.js
文件和对应的 manifest
文件,使用 DLLReferencePlugin
引入:
1 | plugins: [ |
externals
通过CDN引入第三方库Webpack提供了 externals
的方式来引入第三方库,我们可以在 HTML 文件中直接使用 script 标签的形式来引入:
1 | <script src="//cdn.bootcss.com/react.min.js"></script> |
在 Webpack 中如下配置即可:
1 | externals: { |
和 dll 的打包方式相比,主要有以下几点区别:
- 并非所有依赖库都提供了打包好的生产环境的文件,对于这种只能通过npm来引入的库, externals 无能为力。
- 部分依赖库中会存在循环依赖的现象,在一些 React 相关的库中尤为明显,使用 externals 处理会造成路径混乱无法识别。
- 使用dll的方式打包好的静态文件在生产环境中需要额外处理,同步到build目录中。(可以使用 CopyWebpackPlugin 等插件)。
happypack
开启多线程构建HappyPack可以将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展为多进程的模式,从而加速代码构建。使用方式如下:
1 | const HappyPack = require('happypack'); |
更多配置可以参考文档:https://github.com/amireh/happypack
Webpack默认提供的UglifyJS
插件速度很慢,可以使用webpack-parallel-uglify-plugin
替换。配置如下:
1 | const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); |
Node.js的模块的载入及缓存机制如下:
如果模块名不是路径,也不是内置模块,Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到,Node会从父目录的node_modules里搜索,这样递归下去直到根目录。
我们可以对搜索过程进行一些优化,比如可以直接指定node_modules的路径:
1 | module.exports = { |
在Webpack3.0 版本中,提供了一个新的功能:Scope Hoisting
,又译作“作用域提升”。在Webpack2中,打包后的文件里每个模块都会被包装在一个单独的闭包中,这些闭包会导致JS执行速度变慢,Scope Hoisting则可以将所有模块打包进一个大的闭包中。只需在配置文件中添加一个新的插件,就可以让 Webpack 打包出来的代码文件更小、运行的更快:
1 | module.exports = { |
在balel的官方文档中有一句话:babel-loader is slow!
。
我们在babel的配置中,也要尽量注意这一点。
比如,使用 /\.js$/
来匹配文件的话,可能会将node_modules中的文件一起处理,我们需要使用exclude、include等尽可能准确的来指定需要转换的内容。
我们还可以开启babel的缓存配置(cacheDirectory)来提升一倍以上的效率,配置如下:
1 | test: /\.js$/, |
1 | cat /var/log/secure | awk '/Failed/{print $(NF-3)}' | sort | uniq -c | awk '{print $2" = "$1;}' |
1 | 123.207.2.166 = 6 |
fail2ban是一个著名的入侵保护的开源框架,它可以监视系统日志,然后根据匹配到的错误信息(如有人多次尝试SSH、SMTP密码)自动触发不同的防御动作(一般为调用防火墙屏蔽)。
首先安装EPEL源:1
yum install -y epel-release
由于CentOS7默认防火墙是firewalld,我们安装firewalld版本的fail2ban:1
yum -y install fail2ban-firewalld fail2ban-systemd
新建并编辑配置文件:1
vim /etc/fail2ban/jail.local
此处以SSH Jail为例,如需其他规则如邮件、MySQL等可自行搜索。1
2
3
4
5
6
7
8
9
10
11
12
13[DEFAULT]
bantime = 86400
findtime = 300
maxretry = 3
[sshd]
enabled = true
port = 8888
banaction = firewallcmd-new
[sshd-ddos]
enabled = true
port = 8888
其中bantime是被封IP禁止访问的时间,单位是秒。
findtime是检测时间,在此时间内超过规定的次数会激活fail2ban,单位是秒。
maxretry是允许错误登录的最大次数。
启动fail2ban并设置开机启动:1
2systemctl enable fail2ban
systemctl start fail2ban
查看SSH服务监护状态,能看到当前被禁IP。1
fail2ban-client status sshd
输出如下:1
2
3
4
5
6
7
8
9Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 125
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 2
|- Total banned: 2
`- Banned IP list: 182.100.67.4 163.177.240.2
在SSH监护服务白名单中添加/删除IP:1
2fail2ban-client set sshd addignoreip 1.2.3.4
fail2ban-client set sshd delignoreip 1.2.3.4
查看fail2ban日志1
tail /var/log/fail2ban.log
输出如下:1
2
3
4
5
6
7
8
9
102017-05-05 12:54:46,757 fail2ban.actions [4073]: NOTICE [sshd] 182.100.67.4 already banned
2017-05-05 12:54:49,327 fail2ban.filter [4073]: INFO [sshd] Found 182.100.67.4
2017-05-05 12:54:52,043 fail2ban.filter [4073]: INFO [sshd] Found 182.100.67.4
2017-05-05 12:54:52,764 fail2ban.actions [4073]: NOTICE [sshd] 182.100.67.4 already banned
2017-05-05 12:54:54,508 fail2ban.filter [4073]: INFO [sshd] Found 182.100.67.4
2017-05-05 12:59:58,164 fail2ban.filter [4073]: INFO [sshd] Found 163.177.240.2
2017-05-05 13:00:00,069 fail2ban.filter [4073]: INFO [sshd] Found 163.177.240.2
2017-05-05 13:00:02,064 fail2ban.filter [4073]: INFO [sshd] Found 163.177.240.2
2017-05-05 13:00:02,116 fail2ban.actions [4073]: NOTICE [sshd] Ban 163.177.240.2
2017-05-05 13:00:03,853 fail2ban.filter [4073]: INFO [sshd] Found 163.177.240.2
和fail2ban不同,DenyHosts主要靠分析sshd的日志文件(/var/log/secure),来记录重复的IP到/etc/hosts.deny文件,从而达到自动屏IP的功能,而fail2ban则是通过监控各种系统服务来完成。
由于以下原因,不再推荐使用DenyHosts。
- DenyHosts 有尚未解决的安全问题Debian Bug report logs - #692229
- DenyHosts 至今已有9年未更新
- 已经有了能完全替代DenyHosts的产品:fail2ban
虽然有各种各样的防护工具,但是最好的做法依然是不依赖它们。
至少应该做到以下几点:
]]>
- 不允许root登录;
- 不允许SSH通过密码登录;
- 修改常用服务的默认端口(如22端口);
- 合理配置防火墙;
本文是运维从零单排系列的第二篇,文中配置环境要求OpenSSL 1.0.2k及以上,Nginx 1.12.0及以上(由1.0.2k及以上版本的OpenSSL编译),Nginx和OpenSSL的安装可参考:
运维从零单排(一)—— 服务器环境配置(安装Nginx、Node.js、OpenSSL)
HTTPS已经几乎成为了现代网站的标配,开启HTTPS需要申请SSL证书。SSL证书主要有以下几种:
Let’s Encrypt是一个免费的,自动化的,开放的证书颁发机构(CA),它是互联网安全研究小组提供(ISRG)的一个服务,有关它的介绍可以看这里:https://letsencrypt.org/about/。
下面我们以www.zhounan.win
为例,来申请Let’s Encrypt的免费SSL证书。
certbot是Let’s Encrypt官方推荐的获取证书所需的客户端
1 | sudo yum install epel-release -y |
申请过程中需要验证域名归属,验证方式有两种:
我们使用第二种方式来申请证书,修改Nginx的配置文件1
2
3
4
5
6
7
8
9
10
11
12
13server {
listen 80;
server_name www.example.com;
root /var/www/html;
# 如果没有该文件夹则新建一个
# certbot 会自动在该文件夹下创建一个隐藏文件`.well-known/acme-challenge`
# 并通过请求这个文件来验证域名归属。
location ~ /.well-known/acme-challenge {
allow all;
}
}
# 重启Nginx
systemctl restart nginx.service
运行如下命令来申请证书1
certbot certonly --webroot -w /var/www/html/ -d zhounan.win -d www.zhounan.win -d api.zhounan.win
具体参数用法可以参考这里:https://certbot.eff.org/#centosrhel7-nginx
注意Let’s Encrypt暂时不支持泛域名(*.zhounan.win)
执行上述命令后会依次要求输入邮箱、同意协议、选择是否共享邮箱,注意邮箱需要输入真实邮箱以便能接收到Let’s Encrypt的邮件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): test@163.com
Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
-------------------------------------------------------------------------------
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf. You must agree
in order to register with the ACME server at
https://acme-v01.api.letsencrypt.org/directory
-------------------------------------------------------------------------------
(A)gree/(C)ancel: A
-------------------------------------------------------------------------------
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about EFF and
our work to encrypt the web, protect its users and defend digital rights.
-------------------------------------------------------------------------------
(Y)es/(N)o: N
完成之后稍等片刻即可看到如下信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/zhounan.win/fullchain.pem. Your cert will
expire on 2017-07-25. To obtain a new or tweaked version of this
certificate in the future, simply run certbot again. To
non-interactively renew *all* of your certificates, run "certbot
renew"
- Your account credentials have been saved in your Certbot
configuration directory at /etc/letsencrypt. You should make a
secure backup of this folder now. This configuration directory will
also contain certificates and private keys obtained by Certbot so
making regular backups of this folder is ideal.
- If you like Certbot, please consider supporting our work by:
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le
证书已经生成完毕,保存在/etc/letsencrypt/live/zhounan.win/
文件夹内。
为了更好的加密,我们进一步配置PFS(Perfect Forward Secrecy)。
Forward Secrecy的概念很简单:客户端和服务器协商一个永不重用的密钥,并在会话结束时销毁它。服务器上的 RSA 私钥用于客户端和服务器之间的 Diffie-Hellman 密钥交换签名。从 Diffie-Hellman 握手中获取的预主密钥会用于之后的编码。因为预主密钥是特定于客户端和服务器之间建立的某个连接,并且只用在一个限定的时间内,所以称作短暂模式(Ephemeral)。如果使用Forward Secrecy,攻击者取得了一个服务器的私钥,他是不能解码之前的通讯信息的。这个私钥仅用于 Diffie Hellman 握手签名,并不会泄露预主密钥。Diffie Hellman 算法会确保预主密钥绝不会离开客户端和服务器,而且不能被中间人攻击所拦截。所有版本的 nginx 都依赖于 OpenSSL 给 Diffie-Hellman 的输入参数。如果不特别声明,将使用 OpenSSL 的默认设置,1024 位密钥,因为我们正在使用 2048 位证书,所以要有一个更强大的 DH。
我们可以使用下面的命令来生成它:1
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
Mozilla为我们提供了一个SSL配置生成器:https://mozilla.github.io/server-side-tls/ssl-config-generator/
在这里选择了服务器和相应的版本之后,它会为我们提供一份安全的SSL配置,修改一下文件路径并加入到nginx的配置中即可。
下面是一份较为完整的nginx.conf配置文件,供参考。
1 | #user nobody; |
配置完成后,重启nginx,访问你的域名,域名前方出现绿色的小锁图标,即说明配置成功。
Qualys SSL Labs 提供了全面的 SSL 安全性测试,在该链接中填入你的网站地址,即可测试安全性。如果不是A+,可以根据测试结果的提示对配置进行相应修改。
Let’s Encrypt的证书默认有效期只有90天,我们需要配置自动续期。
可以使用如下命令先模拟自动更新:
1 | certbot renew --dry-run |
如果出现如下提示则说明配置成功:
1 | Congratulations, all renewals succeeded. The following certs have been renewed: |
如果模拟更新正常,则可以使用crontab -e
来启用自动任务:
1 | crontab -e |
增加配置:
1 | 0 1 1 * * /mnt/apps/letsencrypt/certbot-auto renew -renew-hook "/etc/init.d/nginx reload" |
该配置含义为:每月1日凌晨1点执行任务
http://www.restran.net/2017/01/24/nginx-letsencrypt-https/
https://www.mzlion.com/CentOS7-Nginx-SSL-Install-Let's-Encrypt.html
https://certbot.eff.org/#centosrhel7-nginx
https://www.pupboss.com/nginx-add-ssl/
http://blog.lzuer.net/2016/10/25/https/
首先到官网https://nodejs.org/download/release/找到需要的版本,此处以v7.9.0
为例:
1 | wget https://nodejs.org/download/release/latest-v7.x/node-v7.9.0-linux-x64.tar.gz |
下载对应的包并解压安装至/usr/local
安装完成后可运行node -v
,返回版本号即安装成功。
如果服务器在国内,安装完成之后可以使用淘宝镜像来加速:
1 | npm config set registry https://registry.npm.taobao.org |
在某些版本的系统中使用这种方法安装可能需要先安装相关的依赖:1
yum install gcc gcc-c++ kernel-devel
首先到官网http://nodejs.org/dist/(注意:源码下载路径和已编译版本下载路径不同)找到需要的版本,此处以v7.9.0
为例:
1 | wget http://nodejs.org/dist/v7.9.0/node-v7.9.0-linux-x64.tar.gz |
nvm(Node version manager)是一个Node.js的版本管理工具,使用nvm可以轻松的切换Node.js版本。
在这里可以找到nvm的最新地址。
1 | wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash |
不推荐nvm的原因如下:
系统中自带的OpenSSL版本一般也较旧,为了HTTP2等新特性,我们需要重新安装1.0.2以上版本的OpenSSL。
首先安装所有可能需要的依赖:1
yum install gc gcc gcc-c++ pcre-devel zlib-devel make wget openssl-devel libxml2-devel libxslt-devel gd-devel perl-ExtUtils-Embed GeoIP-devel gperftools gperftools-devel libatomic_ops-devel perl-ExtUtils-Embed dpkg-dev libpcrecpp0 libgd2-xpm-dev libgeoip-dev libperl-dev -y
然后从官网https://www.openssl.org/source/获取OpenSSL下载链接,下载并解压编译:1
2
3
4
5
6
7
8
9
10
11cd /usr/src
wget https://www.openssl.org/source/openssl-1.1.0e.tar.gz
tar -zxf openssl-1.1.0e.tar.gz
cd openssl-1.1.0e.tar.gz
./config
make
make test
make install
备份原来的openssl
mv /usr/bin/openssl /root/
ln -s /usr/local/ssl/bin/openssl /usr/bin/openssl
完成后使用openssl version
查看版本,如出现 /usr/bin/openssl: No such file or directory
,使用如下方式解决1
2
3
4
5
6
7
8
9ln -s /usr/local/lib64/libssl.so.1.1 /usr/lib64/libssl.so.1.1
ln -s /usr/local/lib64/libcrypto.so.1.1 /usr/lib64/libcrypto.so.1.1
删除旧的符号链接
rm /bin/openssl
添加新版本的符号链接
ln -s /usr/local/bin/openssl /bin/openssl
重新查看版本
openssl version
OpenSSL 1.1.0e 16 Feb 2017
1 | sudo yum install nginx |
检查系统中firewalld防火墙服务是否开启,如果显示active (running),我们需要修改防火墙配置,开启Nginx外网端口访问。1
2
3
4sudo systemctl status firewalld
firewall-cmd --permanent --zone=internal --add-service=http
firewall-cmd --permanent --zone=internal --add-service=https
firewall-cmd --reload
也可以在/etc/firewalld/zones/public.xml
文件的zone一节中增加:1
2
3
4
5<zone>
··· ···
<service name="http"/>
<service name="https"/>
<zone>
保存后重新加载firewalld服务:1
sudo systemctl reload firewalld
完成后即可通过使用浏览器访问 http://<外网IP地址>
,如果出现Nginx欢迎页面,则Nginx成功启动。
使用这种方式安装的nginx版本通常较旧,如果需要安装更新的版本,可以自行在/etc/yum.repos.d/目录下创建一个源配置文件nginx.repo来修改yum源:1
vim /etc/yum.repos.d/nginx.repo
文件内容如下:1
2
3
4
5[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1
保存后使用yum info nginx
可查看源信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17[root@Forever ~]# yum info nginx
Loaded plugins: fastestmirror
··· ···
Determining fastest mirrors
Available Packages
Name : nginx
Arch : x86_64
Epoch : 1
Version : 1.12.0
Release : 1.el7.ngx
Size : 716 k
Repo : nginx/7/x86_64
Summary : High performance web server
URL : http://nginx.org/
License : 2-clause BSD-like license
Description : nginx [engine x] is an HTTP and reverse proxy server, as well as
: a mail proxy server.
检查无误后运行yum install nginx -y
即可完成安装。
有时我们对nginx有些特殊需求,比如开启SSL需要使用1.0.2以上版本的OpenSSL编译的nginx,这时我们需要自己编译安装nginx。
如果已经安装了nginx,首先使用nginx -V来查看相关信息:
1 | [root@Forever ~]# nginx -V |
其中configure arguments:
之后的就是编译参数,可以从此处复制一份备用。--prefix
为nginx安装路径,如果需要开启HTTP2,http_v2_module
和http_ssl_module
这两个模块必备,还需要加上--with-openssl=/usr/src/openssl-1.1.0e
来指定所用的OpenSSL版本,还需要启用哪些模块可以根据自己实际情况来决定。
1 | cd /root |
使用 cloudflare 的 TLS nginx__dynamic_tls_records 补丁来优化 TLS Record Size1
2wget https://raw.githubusercontent.com/cloudflare/sslconfig/master/patches/nginx__dynamic_tls_records.patch
patch -p1 < nginx__dynamic_tls_records.patch
如果提示 patch 命令找不到的话,则先安装 patch1
yum install patch
加上之前保存的参数开始编译1
2
3
4
5
6
7
8
9
10
11./configure --prefix=/etc/nginx --with-openssl=/usr/src/openssl-1.1.0e --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'
make
sudo make install
查看版本:
nginx -V
nginx version: nginx/1.12.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-11) (GCC)
built with OpenSSL 1.1.0e 16 Feb 2017
TLS SNI support enabled
出现built with OpenSSL 1.1.0e
,安装成功。
首先删除旧版本的PHP
通过yum list installed | grep php
可以查看所有已安装的php软件
使用yum remove php ……
删除,如yum remove php70w-common.x86_64 -y
通过yum list php*
查看是否有自己需要安装的版本,如果没有就需要添加第三方yum源, 推荐webtatic、rpmforge或网易的源
1 | # CentOs 7.X |
安装完成后可以使用yum repolist
查看已经安装的源,也可以通过ls /etc/yum.repos.d/
查看。
然后再yum install php71w……
就可以安装新版本PHP了,常用相关组件的安装如下:
1 | yum install -y php71w php71w-curl php71w-common php71w-cli php71w-mysql php71w-mbstring php71w-fpm php71w-xml php71w-pdo php71w-zip php71w-gd php71w-mcrypt php71w-soap php71w-xmlrpc |
1 | # 安装MySQL源 |