杨明山

杨明山 查看完整档案

成都编辑成都大学  |  网络工程 编辑思特沃克软件技术北京有限公司  |  UI Developer 编辑填写个人主网站
编辑

Love open free web

个人动态

杨明山 发布了文章 · 2018-11-05

我们为什么需要 lock 文件

前言

从 Yarn 横空出世推出 lock 文件以来,已经两年多时间了,npm 也在 5.0 版本加入了类似的功能,lock 文件越来越被开发者们接收和认可。本篇文章想从前端视角探讨一下我们为什么需要 lock 文件,以及它的一些成本与风险,当然其中一些观点对于后端也是适用的。

为什么需要 lock 文件

之所以需要 lock 文件,我觉得主要有 4 个原因:

确保各环境依赖版本的一致性

软件开发一般有着好几个环境,通常包括本地开发环境、集成测试环境、预发布环境以及线上环境。各环境依赖版本不一致通常是 bug 的一大来源,大家可能碰到过“在我的电脑上是好的”这种问题,也许就是依赖版本不一致导致的。这种问题通常很难定位,因为你很难确定到底是自己的问题还是依赖的问题。这也是 Yarn 推出 lock 文件的初衷之一,使用了 lock 文件,你在排查问题时至少可以排除依赖版本不一致这个因素。

语义化版本并不绝对可靠

一些开发者不愿意使用 lock 文件,一个主要原因是他们希望基于语义化版本号让依赖自动升级,认为只要选择的依赖可靠,大版本不变化就可以放心升级。在绝大多数情况下这么做不会有问题,但是也有意外情况,比如:

React 在 v16.4.0 对 getDerivedStateFromProps 的调用时机进行了调整。React 在 v16.3.0 引入了这个 API,最初只有在父组件引起的重渲染过程中会被调用,v16.4.0 调整为在所有渲染过程中都会被调用。如果你在不知情的情况下(自动)把 React 从 v16.3.0 升级到了 v16.4.0,那么极端情况下你的应用就会出问题。

虽然只有在很极端的情况下你才会碰到类似问题,但是这种问题本来就是不怕一万就怕万一。

可控的升级依赖

现在通过 webpack 把依赖单独提取为一个 vendor.js 是个很常见的做法,因为依赖变更相对来说没那么频繁,再配合上强缓存,可以做到即使发布了新版本,用户也可以使用缓存的 vendor.js 而不必重新下载。通常我们的应用不止一个依赖,这些依赖肯定也不是同一时间发布更新,如果不使用 lock 文件让其自由更新,可能会导致 vendor.js 缓存失效多次(每个依赖更新都会导致缓存失效)。如果使用 lock 文件就可以积累一段时间,让多个依赖集中更新,甚至跳过一些小版本更新,从而提高 vendor.js 的缓存命中率。

安全问题

几个月前 ESLint 发生了一个安全事故,一个攻击者窃取了 ESLint 维护者的 npm 账户,并发布了恶意版本的 eslint-scopeeslint-config-eslint(都是更新的小版本),其中前者是 babel-eslintwebpack 的依赖。如果没有使用 lock 文件,那么你就极有可能中招,ESLint 事后也建议开发者使用 lock 文件来避免自动安装新版本。

成本

使用 lock 文件自然会增加一点项目的维护成本,因为依赖不会再自动升级,所以需要项目维护者每隔一段时间手动进行升级。另外如果两个人同时修改了依赖,解决 lock 文件的冲突也是一件很麻烦的事。

但是手动升级依赖也有一些额外的好处,至少你升级每个依赖时都要去看一下它的 change log,这样可以对每一次升级做到心中有数,这也有助于你掌握依赖的发展趋势。比如前文提到的 React 的例子,只要你在升级时看一眼它的 change log,就很容易避开可能出现的问题。

风险

我唯一能想到的风险就是依赖版本固化问题,如果你使用了 lock 文件又没有花时间跟精力去维护它,那么你的项目就很容易陷入依赖版本固化的问题。如果太久没有升级依赖,你当前使用的版本跟最新版差别太大,升级就会很困难,考虑到现实成本问题,可能就永远不会升级了。

但是如果不使用 lock 文件就能完全避免这个问题吗,我想也不一定。不使用 lock 文件最多也只能在同一个大版本范围内自动升级,如果依赖升级了大版本,你没有花时间去升级,也会碰到同样的问题。只是相对于不使用 lock 文件,问题暴露的晚一些而已。

查看原文

赞 9 收藏 8 评论 0

杨明山 收藏了文章 · 2018-08-11

一篇文章理解Web缓存

最近把前端缓存重新整理了一下,从整体的层面上把前端所有能用的缓存方案梳理了一遍。同时,对于http缓存,使用了表格的方案,使得原先晦涩难记的特性变得清晰明了。特记录于此,若有什么欠缺,也望不吝指出。

1. 前端缓存概述

前端缓存主要是分为HTTP缓存和浏览器缓存。其中HTTP缓存是在HTTP请求传输时用到的缓存,主要在服务器代码上设置;而浏览器缓存则主要由前端开发在前端js上进行设置。下面会分别具体描述。

clipboard.png

2. 前端缓存分类

2.1 HTTP缓存

整体流程:HTTP缓存都是从第二次请求开始的。
第一次请求资源时,服务器返回资源,并在respone header头中回传资源的缓存参数;第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。

HTTP缓存分为强缓存和协议缓存,它们的区别如下:

clipboard.png

200 from disk or 200 from memory :
强缓存的200也有两种情况:200 from disk和200 from memory。现在我没有找到明确的文档来描述这种区别的发生条件。知乎这个问题中提到了一些情景,可以自行取用。

2.1.1 强缓存

clipboard.png

2.1.2 协商缓存

协商缓存都是成对出现的。
clipboard.png

2.1.3 最佳优化策略-消灭304

最佳优化策略:因为协商缓存本身也有http请求的损耗,所以最佳优化策略是要尽可能的将静态文件存储为较长的时间,多利用强缓存而不是协商缓存,即消灭304。

但是给文件设置一个很长的Cacha-Control也会带来其他的问题,最主要的问题是静态内容更新时,用户不能及时获得更新的内容。这时候就要使用hash的方法对文件进行命名,通过每次更新不同的静态文件名来消除强缓存的影响。

Hash命名:
http://xxx.com/main.5eas34fa.js
http://xxx.com/main.js?5eas34fa
http://xxx.com/5eas34fa/main.js

2.2 浏览器缓存

2.2.1 本地存储小容量

Cookie主要用于用户信息的存储,Cookie的内容可以自动在请求的时候被传递给服务器。
LocalStorage的数据将一直保存在浏览器内,直到用户清除浏览器缓存数据为止。
SessionStorage的其他属性同LocalStorage,只不过它的生命周期同标签页的生命周期,当标签页被关闭时,SessionStorage也会被清除。

clipboard.png

2.2.2 本地存储大容量

WebSql和IndexDB主要用在前端有大容量存储需求的页面上,例如,在线编辑浏览器或者网页邮箱。

clipboard.png

2.2.3 应用缓存与PWA

应用缓存全称为Offline Web Application,它的缓存内容被存在浏览器的Application Cache中。它也是一个被W3C标准废弃的功能,主要是通过manifest文件来标注要被缓存的静态文件清单。但是在缓存静态文件的同时,也会默认缓存html文件。这导致页面的更新只能通过manifest文件中的版本号来决定。而且,即使我们更新了version,用户的第一次访问还是会访问到老的页面,只有下一次再访问才能访问到新的页面。所以,应用缓存只适合那种常年不变化的静态网站。如此的不方便,也是被废弃的重要原因。

PWA全称是渐进式网络应用,主要目标是实现web网站的APP式功能和展示。尽管PWA也有manifest文件,但是与应用缓存却完全不同。不同于manifest简单的将文件通过是否缓存进行分类,PWA用manifest构建了自己的APP骨架。另外,PWA用Service Worker来控制缓存的使用。这一块的内容较多,在这里就不详细展开了。

clipboard.png

2.2.4 往返缓存

往返缓存又称为BFCache,是浏览器在前进后退按钮上为了提升历史页面的渲染速度的一种策略。BFCache会缓存所有的DOM结构,但是问题在于,一些页面开始时进行的上报或者请求可能会被影响。这个问题现在主要会出现在微信h5的开发中

去除BFCache有多种方法,但不是本文的重点,想了解的同学可以看《浏览器往返缓存(Back/Forward cache)问题的分析与解决

总结

本文梳理了前端所有可能涉及的缓存,希望能从整体层面建立起系统的缓存知识体系。限于篇幅,每一部分的描述都比较简略,仅起到抛砖引玉之用。如有错误,还望指出。

查看原文

杨明山 赞了文章 · 2018-08-11

一篇文章理解Web缓存

最近把前端缓存重新整理了一下,从整体的层面上把前端所有能用的缓存方案梳理了一遍。同时,对于http缓存,使用了表格的方案,使得原先晦涩难记的特性变得清晰明了。特记录于此,若有什么欠缺,也望不吝指出。

1. 前端缓存概述

前端缓存主要是分为HTTP缓存和浏览器缓存。其中HTTP缓存是在HTTP请求传输时用到的缓存,主要在服务器代码上设置;而浏览器缓存则主要由前端开发在前端js上进行设置。下面会分别具体描述。

clipboard.png

2. 前端缓存分类

2.1 HTTP缓存

整体流程:HTTP缓存都是从第二次请求开始的。
第一次请求资源时,服务器返回资源,并在respone header头中回传资源的缓存参数;第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。

HTTP缓存分为强缓存和协议缓存,它们的区别如下:

clipboard.png

200 from disk or 200 from memory :
强缓存的200也有两种情况:200 from disk和200 from memory。现在我没有找到明确的文档来描述这种区别的发生条件。知乎这个问题中提到了一些情景,可以自行取用。

2.1.1 强缓存

clipboard.png

2.1.2 协商缓存

协商缓存都是成对出现的。
clipboard.png

2.1.3 最佳优化策略-消灭304

最佳优化策略:因为协商缓存本身也有http请求的损耗,所以最佳优化策略是要尽可能的将静态文件存储为较长的时间,多利用强缓存而不是协商缓存,即消灭304。

但是给文件设置一个很长的Cacha-Control也会带来其他的问题,最主要的问题是静态内容更新时,用户不能及时获得更新的内容。这时候就要使用hash的方法对文件进行命名,通过每次更新不同的静态文件名来消除强缓存的影响。

Hash命名:
http://xxx.com/main.5eas34fa.js
http://xxx.com/main.js?5eas34fa
http://xxx.com/5eas34fa/main.js

2.2 浏览器缓存

2.2.1 本地存储小容量

Cookie主要用于用户信息的存储,Cookie的内容可以自动在请求的时候被传递给服务器。
LocalStorage的数据将一直保存在浏览器内,直到用户清除浏览器缓存数据为止。
SessionStorage的其他属性同LocalStorage,只不过它的生命周期同标签页的生命周期,当标签页被关闭时,SessionStorage也会被清除。

clipboard.png

2.2.2 本地存储大容量

WebSql和IndexDB主要用在前端有大容量存储需求的页面上,例如,在线编辑浏览器或者网页邮箱。

clipboard.png

2.2.3 应用缓存与PWA

应用缓存全称为Offline Web Application,它的缓存内容被存在浏览器的Application Cache中。它也是一个被W3C标准废弃的功能,主要是通过manifest文件来标注要被缓存的静态文件清单。但是在缓存静态文件的同时,也会默认缓存html文件。这导致页面的更新只能通过manifest文件中的版本号来决定。而且,即使我们更新了version,用户的第一次访问还是会访问到老的页面,只有下一次再访问才能访问到新的页面。所以,应用缓存只适合那种常年不变化的静态网站。如此的不方便,也是被废弃的重要原因。

PWA全称是渐进式网络应用,主要目标是实现web网站的APP式功能和展示。尽管PWA也有manifest文件,但是与应用缓存却完全不同。不同于manifest简单的将文件通过是否缓存进行分类,PWA用manifest构建了自己的APP骨架。另外,PWA用Service Worker来控制缓存的使用。这一块的内容较多,在这里就不详细展开了。

clipboard.png

2.2.4 往返缓存

往返缓存又称为BFCache,是浏览器在前进后退按钮上为了提升历史页面的渲染速度的一种策略。BFCache会缓存所有的DOM结构,但是问题在于,一些页面开始时进行的上报或者请求可能会被影响。这个问题现在主要会出现在微信h5的开发中

去除BFCache有多种方法,但不是本文的重点,想了解的同学可以看《浏览器往返缓存(Back/Forward cache)问题的分析与解决

总结

本文梳理了前端所有可能涉及的缓存,希望能从整体层面建立起系统的缓存知识体系。限于篇幅,每一部分的描述都比较简略,仅起到抛砖引玉之用。如有错误,还望指出。

查看原文

赞 113 收藏 93 评论 5

杨明山 收藏了文章 · 2018-08-11

浏览器往返缓存(Back/Forward cache)问题的分析与解决

博客源地址:https://github.com/LeuisKen/l...
相关讨论还请到源 issue 下。

什么是往返缓存(Back/Forward cache)

往返缓存(Back/Forward cache,下文中简称bfcache)是浏览器为了在用户页面间执行前进后退操作时拥有更加流畅体验的一种策略。该策略具体表现为,当用户前往新页面时,将当前页面的浏览器DOM状态保存到bfcache中;当用户点击后退按钮的时候,将页面直接从bfcache中加载,节省了网络请求的时间。

但是bfcache的引入,导致了很多问题。下面,举一个我们遇到的场景:

sample

页面A是一个任务列表,用户从A页面选择了“任务1:看新闻”,点击“去完成”跳转到B页面。当用户进入B页面后,任务完成。此时用户点击回退按钮,会回退到A页面。此时的A页面“任务1:看新闻”的按钮,应该需要标记为“已完成”,由于bfcache的存在,当存入bfcache时,“任务1”的按钮是“去完成”,所以此时回来,按钮也是“去完成”,而不会标记为“已完成”。

既然bug产生了,我们该如何去解决它?很多文章都会提到unload事件,但是我们实际进行了测试发现并不好用。于是,为了解决问题,我们的bfcache探秘之旅开始了。

bfcache 探秘

在检索page cache in chromium的时候,我们发现了这个issue:https://bugs.chromium.org/p/c... 。里面提到 chromium(chrome的开源版本)在很久以前就已经将PageCache(即bfcache)这部分代码移除了。也就是说现在的chrome应该是没有这个东西的。可以确定的是,chrome以前的版本中,bfcache的实现是从webkit中拿来的,加上我们项目目前面向的用户主体就是 iOS + Android,iOS下是基于Webkit,Android基于chrome(且这部分功能也是源于webkit)。因此追溯这个问题,我们只要专注于研究webkitbfcache的逻辑即可。

同样通过上文中描述的commit记录,我们也很快定位到了PageCache相关逻辑在Webkit中的位置:webkit/Source/WebCore/history/PageCache.cpp

该文件中包含的两个方法引起了我们的注意:canCachePagecanCacheFrame。这里的Page即是我们通常理解中的“网页”,而我们也知道网页中可以嵌套<frame><iframe>等标签来置入其他页面。所以,PageFrame的概念就很明确了。而在canCachePage方法中,是调用了canCacheFrame的,如下:

// 给定 page 的 mainFrame 被传入了 canCacheFrame
bool isCacheable = canCacheFrame(page.mainFrame(), diagnosticLoggingClient, indentLevel + 1);

源代码链接:webkit/Source/WebCore/history/PageCache.cpp

因此,重头戏就在canCacheFrame了。

canCacheFrame方法返回的是一个布尔值,也就是其中变量isCacheable的值。那么,isCacheable的判断策略是什么?更重要的,这里面的策略,有哪些是我们能够利用到的。

注意到这里的代码:

Vector<ActiveDOMObject*> unsuspendableObjects;
if (frame.document() && !frame.document()->canSuspendActiveDOMObjectsForDocumentSuspension(&unsuspendableObjects)) {
    // do something...
    isCacheable = false;
}

源代码链接:webkit/Source/WebCore/history/PageCache.cpp

很明显canSuspendActiveDOMObjectsForDocumentSuspension是一个非常重要的方法,该方法中的重要信息见如下代码:

bool ScriptExecutionContext::canSuspendActiveDOMObjectsForDocumentSuspension(Vector<ActiveDOMObject*>* unsuspendableObjects)
{

    // something here...

    bool canSuspend = true;

    // something here...

    // We assume that m_activeDOMObjects will not change during iteration: canSuspend
    // functions should not add new active DOM objects, nor execute arbitrary JavaScript.
    // An ASSERT_WITH_SECURITY_IMPLICATION or RELEASE_ASSERT will fire if this happens, but it's important to code
    // canSuspend functions so it will not happen!
    ScriptDisallowedScope::InMainThread scriptDisallowedScope;
    for (auto* activeDOMObject : m_activeDOMObjects) {
        if (!activeDOMObject->canSuspendForDocumentSuspension()) {
            canSuspend = false;
            // someting here
        }
    }

    // something here...

    return canSuspend;
}

源代码链接:webkit/Source/WebCore/dom/ScriptExecutionContext.cpp

在这一部分,可以看到他调用每一个 ActiveDOMObjectcanSuspendForDocumentSuspension 方法,只要有一个返回了falsecanSuspend就会是false(Suspend这个单词是挂起的意思,也就是说存入bfcache对于浏览器来说就是把页面上的frame挂起了)。

接下来,关键的ActiveDOMObject定义在:webkit/Source/WebCore/dom/ActiveDOMObject.h ,该文件这部分注释,已经告诉了我们最想要的信息。

The canSuspendForDocumentSuspension() function is used by the caller if there is a choice between suspending and stopping. For example, a page won't be suspended and placed in the back/forward cache if it contains any objects that cannot be suspended.

canSuspendForDocumentSuspension 用于帮助函数调用者在“挂起(suspending)”与“停止”间做出选择。例如,一个页面如果包含任何不能被挂起的对象的话,那么它就不会被挂起并放到PageCache中。

接下来,我们要找的就是,哪些对象是不能被挂起的?在WebCore目录下,搜索包含canSuspendForDocumentSuspension() const关键字的.cpp文件,能找到48个结果。大概看了一下,最好用的objects that cannot be suspended应该就是Worker对象了,见代码:

bool Worker::canSuspendForDocumentSuspension() const
{
    // 这里其实是有一个 FIXME 的,看来 webkit 团队也觉得直接 return false 有点简单粗暴。
    // 不过还是等哪天他们真的修了再说吧
    // FIXME: It is not currently possible to suspend a worker, so pages with workers can not go into page cache.
    return false;
}

源代码链接:webkit/Source/WebCore/workers/Worker.cpp

解决方案

业务上添加如下代码:

// disable bfcache
try {
    var bfWorker = new Worker(window.URL.createObjectURL(new Blob(['1'])));
    window.addEventListener('unload', function () {
        // 这里绑个事件,构造一个闭包,以免 worker 被垃圾回收导致逻辑失效
        bfWorker.terminate();
    });
}
catch (e) {
    // if you want to do something here.
}

Thanks to

相关链接

查看原文

杨明山 赞了文章 · 2018-08-11

浏览器往返缓存(Back/Forward cache)问题的分析与解决

博客源地址:https://github.com/LeuisKen/l...
相关讨论还请到源 issue 下。

什么是往返缓存(Back/Forward cache)

往返缓存(Back/Forward cache,下文中简称bfcache)是浏览器为了在用户页面间执行前进后退操作时拥有更加流畅体验的一种策略。该策略具体表现为,当用户前往新页面时,将当前页面的浏览器DOM状态保存到bfcache中;当用户点击后退按钮的时候,将页面直接从bfcache中加载,节省了网络请求的时间。

但是bfcache的引入,导致了很多问题。下面,举一个我们遇到的场景:

sample

页面A是一个任务列表,用户从A页面选择了“任务1:看新闻”,点击“去完成”跳转到B页面。当用户进入B页面后,任务完成。此时用户点击回退按钮,会回退到A页面。此时的A页面“任务1:看新闻”的按钮,应该需要标记为“已完成”,由于bfcache的存在,当存入bfcache时,“任务1”的按钮是“去完成”,所以此时回来,按钮也是“去完成”,而不会标记为“已完成”。

既然bug产生了,我们该如何去解决它?很多文章都会提到unload事件,但是我们实际进行了测试发现并不好用。于是,为了解决问题,我们的bfcache探秘之旅开始了。

bfcache 探秘

在检索page cache in chromium的时候,我们发现了这个issue:https://bugs.chromium.org/p/c... 。里面提到 chromium(chrome的开源版本)在很久以前就已经将PageCache(即bfcache)这部分代码移除了。也就是说现在的chrome应该是没有这个东西的。可以确定的是,chrome以前的版本中,bfcache的实现是从webkit中拿来的,加上我们项目目前面向的用户主体就是 iOS + Android,iOS下是基于Webkit,Android基于chrome(且这部分功能也是源于webkit)。因此追溯这个问题,我们只要专注于研究webkitbfcache的逻辑即可。

同样通过上文中描述的commit记录,我们也很快定位到了PageCache相关逻辑在Webkit中的位置:webkit/Source/WebCore/history/PageCache.cpp

该文件中包含的两个方法引起了我们的注意:canCachePagecanCacheFrame。这里的Page即是我们通常理解中的“网页”,而我们也知道网页中可以嵌套<frame><iframe>等标签来置入其他页面。所以,PageFrame的概念就很明确了。而在canCachePage方法中,是调用了canCacheFrame的,如下:

// 给定 page 的 mainFrame 被传入了 canCacheFrame
bool isCacheable = canCacheFrame(page.mainFrame(), diagnosticLoggingClient, indentLevel + 1);

源代码链接:webkit/Source/WebCore/history/PageCache.cpp

因此,重头戏就在canCacheFrame了。

canCacheFrame方法返回的是一个布尔值,也就是其中变量isCacheable的值。那么,isCacheable的判断策略是什么?更重要的,这里面的策略,有哪些是我们能够利用到的。

注意到这里的代码:

Vector<ActiveDOMObject*> unsuspendableObjects;
if (frame.document() && !frame.document()->canSuspendActiveDOMObjectsForDocumentSuspension(&unsuspendableObjects)) {
    // do something...
    isCacheable = false;
}

源代码链接:webkit/Source/WebCore/history/PageCache.cpp

很明显canSuspendActiveDOMObjectsForDocumentSuspension是一个非常重要的方法,该方法中的重要信息见如下代码:

bool ScriptExecutionContext::canSuspendActiveDOMObjectsForDocumentSuspension(Vector<ActiveDOMObject*>* unsuspendableObjects)
{

    // something here...

    bool canSuspend = true;

    // something here...

    // We assume that m_activeDOMObjects will not change during iteration: canSuspend
    // functions should not add new active DOM objects, nor execute arbitrary JavaScript.
    // An ASSERT_WITH_SECURITY_IMPLICATION or RELEASE_ASSERT will fire if this happens, but it's important to code
    // canSuspend functions so it will not happen!
    ScriptDisallowedScope::InMainThread scriptDisallowedScope;
    for (auto* activeDOMObject : m_activeDOMObjects) {
        if (!activeDOMObject->canSuspendForDocumentSuspension()) {
            canSuspend = false;
            // someting here
        }
    }

    // something here...

    return canSuspend;
}

源代码链接:webkit/Source/WebCore/dom/ScriptExecutionContext.cpp

在这一部分,可以看到他调用每一个 ActiveDOMObjectcanSuspendForDocumentSuspension 方法,只要有一个返回了falsecanSuspend就会是false(Suspend这个单词是挂起的意思,也就是说存入bfcache对于浏览器来说就是把页面上的frame挂起了)。

接下来,关键的ActiveDOMObject定义在:webkit/Source/WebCore/dom/ActiveDOMObject.h ,该文件这部分注释,已经告诉了我们最想要的信息。

The canSuspendForDocumentSuspension() function is used by the caller if there is a choice between suspending and stopping. For example, a page won't be suspended and placed in the back/forward cache if it contains any objects that cannot be suspended.

canSuspendForDocumentSuspension 用于帮助函数调用者在“挂起(suspending)”与“停止”间做出选择。例如,一个页面如果包含任何不能被挂起的对象的话,那么它就不会被挂起并放到PageCache中。

接下来,我们要找的就是,哪些对象是不能被挂起的?在WebCore目录下,搜索包含canSuspendForDocumentSuspension() const关键字的.cpp文件,能找到48个结果。大概看了一下,最好用的objects that cannot be suspended应该就是Worker对象了,见代码:

bool Worker::canSuspendForDocumentSuspension() const
{
    // 这里其实是有一个 FIXME 的,看来 webkit 团队也觉得直接 return false 有点简单粗暴。
    // 不过还是等哪天他们真的修了再说吧
    // FIXME: It is not currently possible to suspend a worker, so pages with workers can not go into page cache.
    return false;
}

源代码链接:webkit/Source/WebCore/workers/Worker.cpp

解决方案

业务上添加如下代码:

// disable bfcache
try {
    var bfWorker = new Worker(window.URL.createObjectURL(new Blob(['1'])));
    window.addEventListener('unload', function () {
        // 这里绑个事件,构造一个闭包,以免 worker 被垃圾回收导致逻辑失效
        bfWorker.terminate();
    });
}
catch (e) {
    // if you want to do something here.
}

Thanks to

相关链接

查看原文

赞 13 收藏 11 评论 1

杨明山 评论了文章 · 2018-08-04

vue-bus: 一个 Vue.js 事件中心插件

vue-bus

一个 Vue.js 事件中心插件,同时支持 Vue 1.0 和 2.0

原因

Vue 2.0 重新梳理了事件系统,因为基于组件树结构的事件流方式实在是让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。虽然依然保留了父子组件间的事件流,但有诸多限制,比如不支持跨多层父子组件通信,也没有解决兄弟组件间的通信问题。

Vue 推荐使用一个全局事件中心来分发和管理应用内的所有事件,详见文档。这是一个最佳实践,同时适用于 Vue 1.0 和 2.0。你当然可以声明一个全局变量来使用事件中心,但你如果在使用 webpack 之类的模块系统,这显然不合适。每次使用都手动 import 进来也很不方便,所以就有了这个插件:vue-bus

vue-bus 提供了一个全局事件中心,并将其注入每一个组件,你可以像使用内置事件流一样方便的使用全局事件。

安装

$ npm install vue-bus

如果在一个模块化工程中使用它,必须要通过 Vue.use() 明确地安装 vue-bus:

import Vue from 'vue';
import VueBus from 'vue-bus';

Vue.use(VueBus);

如果使用全局的 script 标签,则无须如此(手动安装)。

使用

监听事件和清除监听

// ...
created() {
  this.$bus.on('add-todo', this.addTodo);
  this.$bus.once('once', () => console.log('这个监听器只会触发一次'));
},
beforeDestroy() {
  this.$bus.off('add-todo', this.addTodo);
},
methods: {
  addTodo(newTodo) {
    this.todos.push(newTodo);
  }
}

触发事件

// ...
methods: {
  addTodo() {
    this.$bus.emit('add-todo', { text: this.newTodoText });
    this.$bus.emit('once');
    this.newTodoText = '';
  }
}

注意:$bus.on$bus.once$bus.off$bus.emit 只是 $bus.$on$bus.$once$bus.$off$bus.$emit 的别名。 详见 API

项目地址

GitHub,喜欢的话给个 Star 吧 :P

查看原文

杨明山 评论了文章 · 2018-08-04

vue-bus: 一个 Vue.js 事件中心插件

vue-bus

一个 Vue.js 事件中心插件,同时支持 Vue 1.0 和 2.0

原因

Vue 2.0 重新梳理了事件系统,因为基于组件树结构的事件流方式实在是让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。虽然依然保留了父子组件间的事件流,但有诸多限制,比如不支持跨多层父子组件通信,也没有解决兄弟组件间的通信问题。

Vue 推荐使用一个全局事件中心来分发和管理应用内的所有事件,详见文档。这是一个最佳实践,同时适用于 Vue 1.0 和 2.0。你当然可以声明一个全局变量来使用事件中心,但你如果在使用 webpack 之类的模块系统,这显然不合适。每次使用都手动 import 进来也很不方便,所以就有了这个插件:vue-bus

vue-bus 提供了一个全局事件中心,并将其注入每一个组件,你可以像使用内置事件流一样方便的使用全局事件。

安装

$ npm install vue-bus

如果在一个模块化工程中使用它,必须要通过 Vue.use() 明确地安装 vue-bus:

import Vue from 'vue';
import VueBus from 'vue-bus';

Vue.use(VueBus);

如果使用全局的 script 标签,则无须如此(手动安装)。

使用

监听事件和清除监听

// ...
created() {
  this.$bus.on('add-todo', this.addTodo);
  this.$bus.once('once', () => console.log('这个监听器只会触发一次'));
},
beforeDestroy() {
  this.$bus.off('add-todo', this.addTodo);
},
methods: {
  addTodo(newTodo) {
    this.todos.push(newTodo);
  }
}

触发事件

// ...
methods: {
  addTodo() {
    this.$bus.emit('add-todo', { text: this.newTodoText });
    this.$bus.emit('once');
    this.newTodoText = '';
  }
}

注意:$bus.on$bus.once$bus.off$bus.emit 只是 $bus.$on$bus.$once$bus.$off$bus.$emit 的别名。 详见 API

项目地址

GitHub,喜欢的话给个 Star 吧 :P

查看原文

杨明山 赞了文章 · 2018-04-25

webpack4.0测试版指南

自8月初以来—当我们将 nex branch 合并到webpack/webpack#master—我们看到了巨大的贡献!

?今天,我们很自豪地发布webpack 4.0.0-beta.0来分享这项工作的成果!?

?A Promise Fulfilled — 可预测的发布周期

当我们完成webpack 3的发布后,我们向社区承诺,我们会在主要版本之间为您提供更长的开发周期。

我们已经实现了这个承诺[并继续实现它],为您带来一系列功能,改进和错误修复,我们已经等不及想要你试试这些新功能了!以下就讲讲如何开始!

?‍如何安装[v4.0.0-beta.0]

如果你使用的是yarn:

yarn add webpack@next webpack-cli --dev

或者 npm:

npm install webpack@next webpack-cli --save-dev

?如何迁移?

在测试webpack 4时,越来越多的人尝试将reporting plugin loader程序不兼容,我们就可以构建一个生动的移植指南。

因此,我们需要您查看官方更改日志以及我们的迁移草案并在我们有遗漏的地方提供反馈! 这将帮助我们的文档团队创建我们的官方稳定版本迁移指南!

webpack 4有什么新功能?

以下是一些您想要知道的以及一些更值得注意的功能. 有关更改功能和内部API修改的完整列表 请参阅我们的更改日志!!!

?性能

在webpack 4的多种场景中,性能将显着增强。以下是我们为实现此目标而做出的一些显着变化:

  • 默认情况下,在使用 production 模式时,我们将自动并行化并缓存由UglifyJS完成的缩小工作。
  • 我们发布了新版本的插件系统,以便事件挂钩和处理程序是单态的。
  • 此外,webpack现在已经放弃了对Node v4的支持,使我们能够添加大量较新的ES6语法和数据结构,并且也通过V8进行了优化。到目前为止,我们已经看到9小时12分钟的真实报道

PS: 我们甚至还没有实现全缓存和并行性 ? [webpack 5里程碑]

?更好的默认值 — #0CJS

直到今天,webpack一直要求您明确设置您的输入和输出属性。使用webpack 4,webpack会自动假设您的入口属性为./src/,并且默认情况下,bundle将输出为./dist

这意味着 您不再需要配置就可以开始使用webpack!!

现在webpack是#0CJS(Zero Configuration)开箱即用的打包程序,我们将在4.x5.0中奠定基础,以便在将来提供更多的默认功能。

?更好的默认值  — mode

您现在必须在两种模式之间选择(模式或 - 模式):“production” 或者“development”.”

  • 生产模式为您提供各种优化。这包括缩小,范围提升,抖动,无副作用的模块修剪,并且包括必须像NoEmitOnErrorsPlugin一样手动使用的插件。
  • 开发模式针对速度和开发人员的体验进以同样的方式,我们会自动在您的包输出中包含路径名称等功能,eval-source-maps,这些功能是为了易于阅读代码和快速构建!

?sideEffects —  bundle sizes的巨大胜利

我们在package.json中引入了对sideEffects:false。添加此字段时,它会向webpack发送信号,表明库中没有正在使用的sideEffects。这意味着webpack可以安全地消除代码中使用的任何重新导出。

例如,仅从lodash-es作为single_export_导入将花费约223 KiB [压缩后]。在webpack 4中,这个代价现在是〜3 KiB!

?JSON Support & Tree Shaking

当您使用ESModule语法导入JSON时,webpack将从“JSON模块”中消除未使用的导出。对于那些已经将大量未使用的片段导入到代码中的人来说,你会发现你的包的大小会显着减小。

?升级到UglifyJS2

这意味着您可以使用ES6语法,将其缩小,而无需第一个转译器。

我们要感谢UglifyJs2团队的贡献者为实现ES6支持所做的无私和努力工作。这不是一件容易的事情,我们很乐意让你去看看他们的 repository 并表达你的赞赏和支持

? Module Type的推出+ .mjs支持

历史上,JavaScript是webpack中唯一的一流模块类型。这给用户带来了很多尴尬的痛苦,他们无法有效地使用CSS / HTML Bundle等。现在我们从代码库中抽象出JavaScript特性,以允许这个新的API。我们现在有5个模块类型实现:

  • 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现在按此顺序查找.wasm.mjs.js.json扩展名以解析

这个功能最令人兴奋的是,现在我们可以继续使用CSS和HTML模块类型(4.x)。这将允许像HTML这样的功能成为您的入门点!

?WebAssembly支持

默认情况下,Webpack支持导入和导出任何本地WebAssembly模块。这意味着您还可以编写装载器,以便您直接导入Rust,C ++,C和其他WebAssembly主机lang文件:

?再见CommonsChunkPlugin

我们还删除了CommonsChunkPlugin并默认启用了其许多功能。另外,对于那些需要对其缓存策略进行细粒度控制的用户,我们添加了更丰富,更灵活的一组功能optimization.splitChunksoptimization.runtimeChunk

?还有更多!

还有更多的功能,我们强烈建议您在我们的 官方日志中中查看所有功能。

⌚倒计时

按照承诺,我们将从今天开始等待一个月,然后再释放webpack 4稳定版。这为我们的插件,加载程序和集成生态系统提供了测试,报告和升级到webpack 4.0.0的时间!

我们需要您帮助我们升级并测试此测试版。我们今天可以测试的越多,我们就可以更快地进行分类并找出任何可能出现的问题!

非常感谢所有帮助我们制作wepback 4的贡献者。正如我们总是说的那样,webpack的威力是我们的零件和生态系统的总和。

原文地址:https://medium.com/webpack/we...

查看原文

赞 1 收藏 0 评论 0

杨明山 赞了文章 · 2018-04-25

ReactV16.3,即将更改的生命周期

注释:本文是根据React的官方博客翻译而成(文章地址:https://reactjs.org/blog/2018...)。
主要讲述了React之后的更新方向,以及对之前生命周期所出现的问题的总结,之后的React将逐步弃用一些生命周期和增加一些更实用更符合实际情况的生命周期。其中也为从传统的生命周期迁移到新版本的React提出了一些解决方法。


一年多来,React团队一直致力于实现异步渲染。上个月,他在JSConf冰岛的演讲中,丹揭示了一些令人兴奋的新的异步渲染可能性。现在,我们希望与您分享我们在学习这些功能时学到的一些经验教训,以及一些帮助您准备组件以在启动时进行异步渲染的方法。

我们了解到的最大问题之一是,我们的一些传统组件生命周期会导致一些不安全的编码实践。他们是:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

这些生命周期方法经常被误解和滥用;此外,我们预计他们的潜在滥用可能在异步渲染方面有更大的问题。因此,我们将在即将发布的版本中为这些生命周期添加一个“UNSAFE_”前缀。 (这里,“不安全”不是指安全性,而是表示使用这些生命周期的代码将更有可能在未来的React版本中存在缺陷,特别是一旦启用了异步渲染)。

[](https://reactjs.org/#gradual-...

React遵循语义版本控制, 所以这种改变将是渐进的。我们目前的计划是:

  • 16.3:为不安全生命周期引入别名UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps和UNSAFE_componentWillUpdate。 (旧的生命周期名称和新的别名都可以在此版本中使用。)
  • 未来的16.x版本:为componentWillMount,componentWillReceiveProps和componentWillUpdate启用弃用警告。 (旧的生命周期名称和新的别名都可以在此版本中使用,但旧名称会记录DEV模式警告。)
  • 17.0:删除componentWillMount,componentWillReceiveProps和componentWillUpdate。 (从现在开始,只有新的“UNSAFE_”生命周期名称将起作用。)

请注意,如果您是React应用程序开发人员,那么您不必对遗留方法进行任何操作。即将发布的16.3版本的主要目的是让开源项目维护人员在任何弃用警告之前更新其库。这些警告将在未来的16.x版本发布之前不会启用。

我们在Facebook上维护了超过50,000个React组件,我们不打算立即重写它们。我们知道迁移需要时间。我们将采用逐步迁移路径以及React社区中的所有人。


从传统生命周期迁移

如果您想开始使用React 16.3中引入的新组件API(或者如果您是维护人员提前更新库),以下是一些示例,我们希望这些示例可以帮助您开始考虑组件的变化。随着时间的推移,我们计划在文档中添加额外的“配方”,以展示如何以避免有问题的生命周期的方式执行常见任务。

在开始之前,我们将简要概述为16.3版计划的生命周期更改:

  • We are adding the following lifecycle aliases: UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps, and UNSAFE_componentWillUpdate. (Both the old lifecycle names and the new aliases will be supported.)
  • We are introducing two new lifecycles, static getDerivedStateFromProps and getSnapshotBeforeUpdate.
  • 我们正在添加以下生命周期别名

(1) UNSAFE_componentWillMount,

(2) UNSAFE_componentWillReceiveProps

(3) UNSAFE_componentWillUpdate。 (旧的生命周期名称和新的别名都将受支持。)

  • 我们介绍了两个新的生命周期,分别是getDerivedStateFromProps和getSnapshotBeforeUpdate。

新的生命周期:getDerivedStateFromProps

class Example extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    // ...
  }
}

新的静态getDerivedStateFromProps生命周期在组件实例化以及接收新props后调用。它可以返回一个对象来更新state,或者返回null来表示新的props不需要任何state更新。

componentDidUpdate一起,这个新的生命周期应该覆盖传统componentWillReceiveProps的所有用例。

新的生命周期:getSnapshotBeforeUpdate

class Example extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // ...
  }
}

新的getSnapshotBeforeUpdate生命周期在更新之前被调用(例如,在DOM被更新之前)。此生命周期的返回值将作为第三个参数传递给componentDidUpdate。 (这个生命周期不是经常需要的,但可以用于在恢复期间手动保存滚动位置的情况。)

componentDidUpdate一起,这个新的生命周期将覆盖旧版componentWillUpdate的所有用例。

You can find their type signatures in this gist.

我们看看如何在使用这两种生命周期的,例子如下:

例如:

注意

为简洁起见,下面的示例是使用实验类属性转换编写的,但如果没有它,则应用相同的迁移策略。

初始化状态:

这个例子展示了一个调用componentWillMount中带有setState的组件:

// Before
class ExampleComponent extends React.Component {
  state = {};

  componentWillMount() {
    this.setState({
      currentColor: this.props.defaultColor,
      palette: 'rgb',
    });
  }
}

这种类型的组件最简单的重构是将状态初始化移动到构造函数或属性初始值设定项,如下所示:

// After
class ExampleComponent extends React.Component {
  state = {
    currentColor: this.props.defaultColor,
    palette: 'rgb',
  };
}

获取外部数据

以下是使用componentWillMount获取外部数据的组件示例:

// Before
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentWillMount() {
    this._asyncRequest = asyncLoadData().then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
}

上述代码对于服务器呈现(其中不使用外部数据的地方)和即将到来的异步呈现模式(其中请求可能被多次启动)是有问题的。

对于大多数用例,建议的升级路径是将数据提取移入componentDidMount

// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._asyncRequest = asyncLoadData().then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
}

有一个常见的错误观念认为,在componentWillMount中提取可以避免第一个空的渲染。在实践中,这从来都不是真的,因为React总是在componentWillMount之后立即执行渲染。如果数据在componentWillMount触发的时间内不可用,则无论你在哪里提取数据,第一个渲染仍将显示加载状态。这就是为什么在绝大多数情况下将提取移到componentDidMount没有明显效果。

注意:
一些高级用例(例如,像Relay这样的库)可能想要尝试使用热切的预取异步数据。在这里可以找到一个这样做的例子

从长远来看,在React组件中获取数据的规范方式可能基于JSConf冰岛推出的“悬念”API。简单的数据提取解决方案以及像Apollo和Relay这样的库都可以在后台使用。它比上述任一解决方案的冗余性都要小得多,但不会在16.3版本中及时完成。

当支持服务器渲染时,目前需要同步提供数据 - componentWillMount通常用于此目的,但构造函数可以用作替换。即将到来的悬念API将使得异步数据在客户端和服务器呈现中都可以清晰地获取。

添加时间监听

下面是一个在安装时监听外部事件调度程序的组件示例:

// Before
class ExampleComponent extends React.Component {
  componentWillMount() {
    this.setState({
      subscribedValue: this.props.dataSource.value,
    });

    // This is not safe; it can leak!
    this.props.dataSource.subscribe(
      this.handleSubscriptionChange
    );
  }

  componentWillUnmount() {
    this.props.dataSource.unsubscribe(
      this.handleSubscriptionChange
    );
  }

  handleSubscriptionChange = dataSource => {
    this.setState({
      subscribedValue: dataSource.value,
    });
  };
}

不幸的是,这会导致服务器渲染(componentWillUnmount永远不会被调用)和异步渲染(在渲染完成之前渲染可能被中断,导致componentWillUnmount不被调用)的内存泄漏。

人们经常认为componentWillMountcomponentWillUnmount总是配对,但这并不能保证。只有调用componentDidMount后,React才能保证稍后调用componentWillUnmount进行清理。

出于这个原因,添加事件监听的推荐方式是使用componentDidMount生命周期:

// After
class ExampleComponent extends React.Component {
  state = {
    subscribedValue: this.props.dataSource.value,
  };

  componentDidMount() {
    // Event listeners are only safe to add after mount,
    // So they won't leak if mount is interrupted or errors.
    this.props.dataSource.subscribe(
      this.handleSubscriptionChange
    );

    // External values could change between render and mount,
    // In some cases it may be important to handle this case.
    if (
      this.state.subscribedValue !==
      this.props.dataSource.value
    ) {
      this.setState({
        subscribedValue: this.props.dataSource.value,
      });
    }
  }

  componentWillUnmount() {
    this.props.dataSource.unsubscribe(
      this.handleSubscriptionChange
    );
  }

  handleSubscriptionChange = dataSource => {
    this.setState({
      subscribedValue: dataSource.value,
    });
  };
}

有时候更新监听以响应属性变化很重要。如果您使用的是像Redux或MobX这样的库,库的容器组件会为您处理。对于应用程序作者,我们创建了一个小型库create-subscription来帮助解决这个问题。我们会将它与React 16.3一起发布。

Rather than passing a subscribable dataSource prop as we did in the example above, we could use create-subscription to pass in the subscribed value:

我们可以使用create-subscription来传递监听的值,而不是像上例那样传递监听 的dataSource prop。

import {createSubscription} from 'create-subscription';

const Subscription = createSubscription({
  getCurrentValue(sourceProp) {
    // Return the current value of the subscription (sourceProp).
    return sourceProp.value;
  },

  subscribe(sourceProp, callback) {
    function handleSubscriptionChange() {
      callback(sourceProp.value);
    }

    // Subscribe (e.g. add an event listener) to the subscription (sourceProp).
    // Call callback(newValue) whenever a subscription changes.
    sourceProp.subscribe(handleSubscriptionChange);

    // Return an unsubscribe method.
    return function unsubscribe() {
      sourceProp.unsubscribe(handleSubscriptionChange);
    };
  },
});

// Rather than passing the subscribable source to our ExampleComponent,
// We could just pass the subscribed value directly:
`<Subscription source={dataSource}>`
  {value => `<ExampleComponent subscribedValue={value} />`}
`</Subscription>`;
注意>>像Relay / Apollo这样的库应该使用与创建订阅相同的技术手动管理订阅(如此处所引用的),并采用最适合其库使用的优化方式。

基于props更新state

以下是使用旧版componentWillReceiveProps生命周期基于新的道具值更新状态的组件示例:

// Before
class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
  };

  componentWillReceiveProps(nextProps) {
    if (this.props.currentRow !== nextProps.currentRow) {
      this.setState({
        isScrollingDown:
          nextProps.currentRow > this.props.currentRow,
      });
    }
  }
}

尽管上面的代码本身并没有问题,但componentWillReceiveProps生命周期通常会被错误地用于解决问题。因此,该方法将被弃用。

从版本16.3开始,更新state以响应props更改的推荐方法是使用新的静态getDerivedStateFromProps生命周期。 (生命周期在组件创建时以及每次收到新道具时调用):

// After
class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.currentRow !== prevState.lastRow) {
      return {
        isScrollingDown:
          nextProps.currentRow > prevState.lastRow,
        lastRow: nextProps.currentRow,
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

You may notice in the example above that props.currentRow is mirrored in state (as state.lastRow). This enables getDerivedStateFromProps to access the previous props value in the same way as is done in componentWillReceiveProps.

你可能会注意到在上面的例子中,props.currentRow是一个镜像状态(如state.lastRow)。这使得getDerivedStateFromProps可以像在componentWillReceiveProps中一样访问以前的props值。

您可能想知道为什么我们不只是将先前的props作为参数传递给getDerivedStateFromProps。我们在设计API时考虑了这个选项,但最终决定反对它,原因有两个:

  • A prevProps parameter would be null the first time getDerivedStateFromProps was called (after instantiation), requiring an if-not-null check to be added any time prevProps was accessed.
  • Not passing the previous props to this function is a step toward freeing up memory in future versions of React. (If React does not need to pass previous props to lifecycles, then it does not need to keep the previous props object in memory.)
  1. 在第一次调用getDerivedStateFromProps(实例化后)时,prevProps参数将为null,需要在访问prevProps时添加if-not-null检查。
  2. 没有将以前的props传递给这个函数,在未来版本的React中释放内存的一个步骤。 (如果React不需要将先前的道具传递给生命周期,那么它不需要将先前的道具对象保留在内存中。)
注意:如果您正在编写共享组件,那么react-lifecycles-compat polyfill可以使新的getDerivedStateFromProps生命周期与旧版本的React一起使用。详细了解如何在下面使用它。

调用外部回调函数

下面是一个在内部状态发生变化时调用外部函数的组件示例:

// Before
class ExampleComponent extends React.Component {
  componentWillUpdate(nextProps, nextState) {
    if (
      this.state.someStatefulValue !==
      nextState.someStatefulValue
    ) {
      nextProps.onChange(nextState.someStatefulValue);
    }
  }
}

在异步模式下使用componentWillUpdate都是不安全的,因为外部回调可能会多次调用只更新一次。相反,应该使用componentDidUpdate生命周期,因为它保证每次更新只调用一次:

// After
class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (
      this.state.someStatefulValue !==
      prevState.someStatefulValue
    ) {
      this.props.onChange(this.state.someStatefulValue);
    }
  }
}

props改变的副作用

与上述 事例类似,有时组件在道具更改时会产生副作用。

// Before
class ExampleComponent extends React.Component {
  componentWillReceiveProps(nextProps) {
    if (this.props.isVisible !== nextProps.isVisible) {
      logVisibleChange(nextProps.isVisible);
    }
  }
}

componentWillUpdate一样,componentWillReceiveProps可能会多次调用但是只更新一次。出于这个原因,避免在此方法中导致的副作用非常重要。相反,应该使用componentDidUpdate,因为它保证每次更新只调用一次:

// After
class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (this.props.isVisible !== prevProps.isVisible) {
      logVisibleChange(this.props.isVisible);
    }
  }
}

props改变时获取外部数据

以下是根据propsvalues提取外部数据的组件示例:

// Before
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.id !== this.props.id) {
      this.setState({externalData: null});
      this._loadAsyncData(nextProps.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = asyncLoadData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}

此组件的推荐升级路径是将数据更新移动到componentDidUpdate中。在渲染新道具之前,您还可以使用新的getDerivedStateFromProps生命周期清除陈旧的数据:

// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    // Store prevId in state so we can compare when props change.
    // Clear out previously-loaded data (so we don't render stale stuff).
    if (nextProps.id !== prevState.prevId) {
      return {
        externalData: null,
        prevId: nextProps.id,
      };
    }

    // No state update necessary
    return null;
  }

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.externalData === null) {
      this._loadAsyncData(this.props.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = asyncLoadData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}
注意>如果您使用支持取消的HTTP库(如axios),那么卸载时取消正在进行的请求很简单。对于原生Promise,您可以使用如下所示的方法

在更新之前读取DOM属性

下面是一个组件的例子,它在更新之前从DOM中读取属性,以便在列表中保持滚动位置:

class ScrollingList extends React.Component {
  listRef = null;
  previousScrollOffset = null;

  componentWillUpdate(nextProps, nextState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (this.props.list.length < nextProps.list.length) {
      this.previousScrollOffset =
        this.listRef.scrollHeight - this.listRef.scrollTop;
    }
  }

  componentDidUpdate(prevProps, prevState) {
    // If previousScrollOffset is set, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    if (this.previousScrollOffset !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight -
        this.previousScrollOffset;
      this.previousScrollOffset = null;
    }
  }

  render() {
    return (
      `<div>`
        {/* ...contents... */}
      `</div>`
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}

在上面的例子中,componentWillUpdate被用来读取DOM属性。但是,对于异步渲染,“render”阶段生命周期(如componentWillUpdaterender)与“commit”阶段生命周期(如componentDidUpdate)之间可能存在延迟。如果用户在这段时间内做了类似调整窗口大小的操作,则从componentWillUpdate中读取的scrollHeight值将失效。

解决此问题的方法是使用新的“commit”阶段生命周期getSnapshotBeforeUpdate。在数据发生变化之前立即调用该方法(例如,在更新DOM之前)。它可以将React的值作为参数传递给componentDidUpdate,在数据发生变化后立即调用它。

这两个生命周期可以像这样一起使用:

class ScrollingList extends React.Component {
  listRef = null;

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      return (
        this.listRef.scrollHeight - this.listRef.scrollTop
      );
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      `<div>`
        {/* ...contents... */}
      `</div>`
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}
注意>>如果您正在编写共享组件,那么react-lifecycles-compat polyfill可以使新的getSnapshotBeforeUpdate生命周期与旧版本的React一起使用。详细了解如何使用它

其它情况

While we tried to cover the most common use cases in this post, we recognize that we might have missed some of them. If you are using componentWillMount, componentWillUpdate, or componentWillReceiveProps in ways that aren’t covered by this blog post, and aren’t sure how to migrate off these legacy lifecycles, please file a new issue against our documentation with your code examples and as much background information as you can provide. We will update this document with new alternative patterns as they come up.

除了以上的一些常见的例子,还可能会有别的情况本篇文章没有涵盖到,如果您以本博文未涉及的方式使用componentWillMountcomponentWillUpdatecomponentWillReceiveProps,并且不确定如何迁移这些传统生命周期,你可以提供您的代码示例和我们的文档,并且一起提交一个新问题。我们将在更新这份文件时提供新的替代模式。

开源项目维护者

开源维护人员可能想知道这些更改对于共享组件意味着什么。如果实现上述建议,那么依赖于新的静态getDerivedStateFromProps生命周期的组件会发生什么情况?你是否还必须发布一个新的主要版本,并降低React 16.2及更高版本的兼容性?

当React 16.3发布时,我们还将发布一个新的npm包, react-lifecycles-compat。该npm包会填充组件,以便新的getDerivedStateFromPropsgetSnapshotBeforeUpdate生命周期也可以与旧版本的React(0.14.9+)一起使用。

要使用这个polyfill,首先将它作为依赖项添加到您的库中:

# Yarn
yarn add react-lifecycles-compat

# NPM
npm install react-lifecycles-compat --save

接下来,更新您的组件以使用新的生命周期(如上所述)。

最后,使用polyfill将组件向后兼容旧版本的React:

import React from 'react';
import {polyfill} from 'react-lifecycles-compat';

class ExampleComponent extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    // Your state update logic here ...
  }
}

// Polyfill your component to work with older versions of React:
polyfill(ExampleComponent);

export default ExampleComponent;

文章来源

查看原文

赞 20 收藏 17 评论 1

杨明山 赞了文章 · 2018-02-02

async-helper,一个 PHP 的异步进程助手

async-helper

简介

PHP 的异步进程助手,借助于 AMQP 实现异步执行 PHP 的方法,将一些很耗时、追求高可用、需要重试机制的操作放到异步进程中去执行,将你的 HTTP 服务从繁重的业务逻辑中解脱出来。以一个较低的成本将传统 PHP 业务逻辑转换成非阻塞、高可用、可扩展的异步模式。

依赖

  • php 5.6+
  • ext-bcmath
  • ext-amqp 1.9.1+
  • ext-memcached 3.0.3+

安装

通过 composer 安装

composer require l669/async-helper

或直接下载项目源码

wget https://github.com/l669306630/async-helper/archive/master.zip

使用范例

业务逻辑:这里定义了很多等待被调用的类和方法,在你的项目中这可能是数据模型、或是一个发送邮件的类。

<?php
class SendMailHelper 
{
    /**
     * @param array $mail
     * @throws Exception
     */
    public static function request($mail)
    {
        // 在这里发送邮件,或是通过调用第三方提供的服务发送邮件
        // 发送失败的时候你抛出了异常,希望被进程捕获,并按设定的规则进行重试
    }    
}

生产者:通常是 HTTP 服务,传统的 PHP 项目或是一个命令行程序,接收到某个请求或指令后进行一系列的操作。

<?php 
use l669\AsyncHelper;
class UserController
{
    public function register()
    {
        // 假设这是一个用户注册的请求,用户提交了姓名、邮箱、验证码
        // 第一步、校验用户信息
        // 第二步、实例化异步助手,这时候会连接 AMQP
        $async_helper = new AsyncHelper([
            'host' => '127.0.0.1',
            'port' => '5672',
            'user' => 'root',
            'pass' => '123456',
            'vhost' => '/'
        ]);
        // 第三步、保存用户信息到数据库
        $mail = [
            'from' => 'service@yourdomain.com', 
            'to' => 'username@163.com', 
            'subject' => '恭喜你注册成功',
            'body' => '请点击邮件中的链接完成验证....'
        ];
        // 第四步、通过异步助手发送邮件
        $async_helper->run('\\SendMailHelper', 'request', [$mail]);
        
        // 这是同步的模式去发送邮件,如果邮件服务响应迟缓或异常,就会直接影响该请求的响应时间,甚至丢失这封重要邮件
        // SendMailHelper::request($mail);
    }
}

消费者:PHP 的异步进程,监听消息队列,执行你指定的方法。并且该消费者进程是可扩展的高可用的服务,这一切都得益于 AMQP,这是系统解耦、布局微服务的最佳方案。

consume.php

<?php
require_once('vendor/autoload.php');
require_once('SendMailHelper.php');

use l669\AsyncHelper;
use l669\CacheHelper;

$cache_helper = new CacheHelper('127.0.0.1', 11211);
while(true){
    try{
        $async_helper = new AsyncHelper([
            'host' => '127.0.0.1',
            'port' => '5672',
            'user' => 'root',
            'pass' => '123456',
            'vhost' => '/',
            'cacheHelper' => $cache_helper
        ]);
        $async_helper->consume();
    }catch(Exception $e){
        // 可以在这里记录一些日志
        sleep(2);
    }
}
# 在命令行下启动消费者进程,推荐使用 supervisor 来管理进程
php consume.php

支持事务:需要一次提交执行多个异步方法,事务可以确保完成性。

// 接着上面的示例来说,这里省略了一些重复的代码,下同
$async_helper->beginTransaction();
try{
    $async_helper->run('\\SendMailHelper', 'request', [$mail1]);
    $async_helper->run('\\SendMailHelper', 'request', [$mail2]);
    $async_helper->run('\\SendMailHelper', 'request', [$mail3]);
    $async_helper->commit();
}catch(\Exception $e){
    $async_helper->rollback();
}

阻塞式重试:当异步进程执行一个方法,方法内部抛出异常时进行重试,一些必须遵循执行顺序的业务就要采用阻塞式的重试,通过指定重试最大阻塞时长来控制。

use l669\CacheHelper;
use l669\AsyncHelper;
$async_helper = new AsyncHelper([
    'host' => '127.0.0.1',
    'port' => '5672',
    'user' => 'root',
    'pass' => '123456',
    'vhost' => '/',
    'cacheHelper' => new CacheHelper('127.0.0.1', 11211),
    'retryMode' => AsyncHelper::RETRY_MODE_REJECT,  // 阻塞式重试
    'maxDuration' => 600                            // 最长重试 10 分钟
]);
$send_mail_helper = new \SendMailHelper();
$mail = new \stdClass();
$mail->from = 'service@yourdomain.com';
$mail->to = 'username@163.com';
$mail->subject = '恭喜你注册成功';
$mail->body = '请点击邮件中的链接完成验证....';
$async_helper->run($send_mail_helper, 'request', [$mail]);

// 如果方法中需要抛出异常来结束程序,又不希望被异步进程重试,可以抛出以下几种错误码,进程捕获到这些异常后会放弃重试:
// l669\AsyncException::PARAMS_ERROR
// l669\AsyncException::METHOD_DOES_NOT_EXIST
// l669\AsyncException::KNOWN_ERROR

非阻塞式重试:当异步执行的方法内部抛出异常,async-helper 会将该方法重新放进队列的尾部,先执行新进入队列的方法,回头再重试刚才执行失败的方法,通过指定最大重试次数来控制。

use l669\CacheHelper;
use l669\AsyncHelper;
$async_helper = new AsyncHelper([
    'host' => '127.0.0.1',
    'port' => '5672',
    'user' => 'root',
    'pass' => '123456',
    'vhost' => 'new',
    'cacheHelper' => new CacheHelper('127.0.0.1', 11211),
    'queueName' => 'emails.vip',                    // 给付费的大爷走 VIP 队列
    'retryMode' => AsyncHelper::RETRY_MODE_TTL,     // 非阻塞式重试
    'maxRetries' => 10                              // 最多重试 10 次
]);
$mail = new \stdClass();
$mail->from = 'service@yourdomain.com';
$mail->to = 'username@163.com';
$mail->subject = '恭喜你注册成功';
$mail->body = '请点击邮件中的链接完成验证....';
$async_helper->run('\\SendMailHelper', 'request', [$mail]);

应用和解惑

  • 我们采用的是开源的 RabbitMQ 来为我们提供的 AMQP 服务。
  • 你的项目部署在拥有很多服务器节点的集群上,每个节点的程序都需要写日志文件,现在的问题就是要收集所有节点上面的日志到一个地方,方便我们及时发现问题或是做一些统计。所有节点都可以使用 async-helper 异步调用一个写日志的方法,而执行这个写日志的方法的进程只需要在一台机器上启动就可以了,这样所有节点的日志就都实时掌握在手里了。
  • 做过微信公众号开发的都知道,腾讯微信可以将用户的消息推送到我们的服务器,如果我们在 5s 内未及时响应,腾讯微信会重试 3 次,其实这就是消息队列的应用,使用 async-helper 可以轻松的做和这一样的事情。
  • 得益于 RabbitMQ,你可以轻松的横向扩展你的消费者进程的能力,因为 RabbitMQ 天生就支持集群部署,你可以轻松的启动多个消费者进程,或是将消费者进程分布到多台机器上。
  • 如果 RabbitMQ 服务不可用怎么办呢?部署 RabbitMQ 高可用服务是容易的,对外提供单一 IP,这个 IP 是个负载均衡,背后是 RabbitMQ 集群,负载均衡承担对后端集群节点的健康检查。
  • async-helper 能否承受高并发请求?async-helper 生产者使用的是短连接,也就说在你的 HTTP 还没有响应浏览器的时候 async-helper 就已经结束了工作,你连接 RabbitMQ 的时间是百分之百小于 HTTP 请求的时间的,换言之,只要 RabbitMQ 承受并发的能力超过你的 HTTP 服务的承受并发的能力,RabbitMQ 就永远不会崩,通过横向扩展 RabbitMQ 很容易做到的。

和传统 PHP 相比

  • 对任何 PHP 方法通过反射进行异步执行;
  • 高可用,执行方法进入消息队列,可持久化,即使服务器宕机,执行任务也不丢失;
  • 高可用,对异常可以进行不限次数和时间的重试,重试次数和时间可配置;
  • 支持对多个异步方法包含在事务中执行,支持回滚事务;
  • 方法的参数类型支持除资源类型(resource)和回调函数(callable)外的任意类型的参数;
  • 得益于 AMQP,异步方法可以承受高并发、高负载,支持集群部署、横向扩展;
  • 低延时,实测延时时间 0.016 ~ 0.021s;
  • 适用于:日常数据库操作、日志收集、金融交易、消息推送、发送邮件和短信、数据导入导出、计算大量数据生成报表;

附录

查看原文

赞 9 收藏 25 评论 3

认证与成就

  • 获得 67 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-11-10
个人主页被 654 人浏览