对基于React的应用程序平台(DHIS2)的渐进Web应用程序实现的深入研究
#javascript #react #pwa #platform

DHIS2,我们是一个偏远的开发人员团队,建立了世界上最大的健康信息管理系统。 DHIS2是奥斯陆大学开发的免费和开源全球公共商品。它是used in more than 90 countries around the world,是针对70多个国家 /地区的国家健康信息系统。它是一个通用数据收集和分析平台,用于管理常规健康服务提供以及针对Covid-19,疟疾,艾滋病毒/艾滋病,结核病,孕妇和儿童健康等的干预措施。我们的技术堆栈包括一个Postgres数据库,通常部署的Java服务器,本机Android应用程序以及30多个基于React的Web应用程序。为了支持我们团队维护的许多Web应用程序以及世界各地的a growing community of developers开发的应用程序,我们提供了一套构建工具和常见的应用程序基础架构,我们称为App Platform

我们对我们的应用程序平台中最新发布的Progressive Web应用程序(PWA)功能感到兴奋,您可以在this blog post introducing them中阅读,并且我们认为我们有一些有趣的故事可以分享有关其开发的信息。当我们试图使这些功能易于推广到任何应用程序时,我们面临着有趣的设计挑战,我们使用可用技术来解决这些挑战的方式非常独特。这篇文章的目的是分享我们以通用方式管理服务工作者生命周期和其他PWA功能的新方法。

内容

让我们从有关应用程序平台如何工作的一些必要上下文开始。

DHIS2应用程序平台

DHIS2在许多不同的国家和许多不同的情况下使用。每个DHIS2实例都有特定的要求,用例和用户体验工作流程。我们希望使其他组织中的开发人员尽可能容易地通过创建自己的Web应用程序(除其他类型的扩展名)以及share those apps with other implementers on our App Hub来扩展DHIS2的核心功能。我们还希望在创建和维护由我们的核心开发人员团队开发的30多个Web应用程序时,使自己的生活更轻松。

输入App Platform。该应用程序平台是统一的应用程序体系结构和构建管道,以简化和标准化DHIS2生态系统中的应用程序开发。该平台提供了许多DHIS2 Web应用程序所需的许多通用服务和功能,包括身份验证和授权,翻译基础架构,常见的UI组件和数据访问层 - 使得更容易,更快地开发自定义应用程序而无需重新开发自定义应用程序。车轮。

App Platform

此图像中的某些功能正在进行中。

构建时间的应用程序平台

应用程序平台由许多构建时间组件和开发工具组成,您可以在我们的koude0 repository中找到这些组件:

  1. 应用程序适配器:开发应用程序的包装器 - 它包裹了从应用程序的入口点导出的根组件(例如<App />)并执行其他作业。
  2. App Shell :为应用程序和其他资产提供HTML骨架,从该应用程序的输入点中导入root <App>组件,并使用应用程序适配器包装。它还为应用程序提供了一些环境变量。
  3. 应用程序脚本CLI :提供开发工具并执行构建时间作业,例如构建应用程序本身并运行开发服务器。 (也是d2 global CLI的一部分)

运行时的应用程序平台

在运行时,我们的平台提供了对正在开发的应用程序提供服务的React组件和挂钩。这些主要是两个库:

  1. App Runtime library 使用通用<Provider>组件提供上下文并支持几种有用的服务。应用程序适配器默认使用该平台将提供商添加到应用程序中。服务包括:
    1. Data Service :发布一个声明性的API,用于发送和接收往返DHIS2后端的数据
    2. 配置服务:公开多个应用程序配置参数
    3. 警报服务:提供声明性的API,用于显示和隐藏应用内警报。这也与应用程序适配器中的警报管理器组件进行协调,以显示UI
  2. A UI库提供可重复使用的接口组件来实现DHIS2设计系统。请参阅UI documentationkoude4 repository的更多信息。

应用程序平台乐团

说明应用程序适配器,应用程序外壳和应用程序脚本如何一起工作,请考虑初始化和构建应用程序时发生的一系列事件:

  1. 使用d2 global CLI,一个新的平台应用程序是bootstrapped使用terminal中的d2 app scripts init new-app
  2. 在上述脚本刚刚创建的new-app/目录中,yarn build命令是运行的,该命令又运行koude8,该命令启动了以下步骤。下面描述的任何目录或文件路径相对于new-app/
  3. i18n作业被执行(本文的范围之外)。
  4. build脚本引导程序在.d2/shell/目录中的新应用外壳。
  5. 生成了Web应用清单。
  6. src/编写的应用代码被转移并复制到.d2/shell/src/D2App/目录中。
  7. 在此阶段,在外壳内部设置了文件,以便从开发的应用程序中从输入点导出的根组件(默认情况下,<App /> from <App /> frof src/App.js,现在已复制到.d2/shell/src/D2App/App.js)为由App Shell that wraps it with the App Adapter中的文件导入,然后将wrapped app gets rendered转入DOM中的锚节点。
  8. 现在在.d2/shell/目录中设置的Shell已封装的应用程序现在基本上是一个创建React App应用程序,并且可以使用react-scripts来编译缩小的生产构建。运行了react-scripts build脚本,并且在应用程序root中的build/app/目录输出构建。
  9. 也创建了该应用程序的拉链捆绑包并输出到build/bundle/,可以将其上传到DHIS2实例。

此示例在本文稍后阅读有关构建过程时会引用此示例。当我们改善构建工具时,此过程的一些细节可能会发生变化,但这是当前的设计。

要上下文化并预览即将到来的部分,这是我们对此过程进行的扩展,以将PWA功能添加到应用程序平台中:

  • 我们在步骤4 的启动的应用程序外壳中添加了一个服务工作者脚本
  • 我们在步骤5 中与Web应用清单一起生成PWA清单
  • 我们在步骤7中扩展了应用程序适配器,以支持几个客户端PWA功能
  • 步骤8 期间,App Shell中的服务工作者脚本被编译并添加到构建应用程序中

进入渐进式网络应用程序(PWA)

既然您在我们的应用程序架构和平台上有一些背景,那么让我们谈谈我们对渐进式Web应用程序(PWA)技术的实施,以及它如何提出一些设计挑战,因为我们开发了它可以推广到任何应用程序。我们希望我们的基于应用程序平台的Web应用程序支持PWAS核心的两个定义功能:

  • 安装性,这意味着该应用可以下载到设备并像本机应用一样运行,
  • 离线功能,这意味着该应用程序可以在设备离线时支持大部分或所有功能。当应用程序在浏览器中打开或以安装应用程序打开时,这既可以工作。

在DHIS2 App Platform中添加PWA功能,尤其是离线功能是一项艰巨的任务 - 实现PWA功能在单个应用中可能足够复杂,其中某些方面是famously tricky

最重要的是,我们还有其他一些独特的设计标准,使我们的项目增加了复杂性:

  • 这些功能应加入并易于添加到任何 平台应用程序。
  • 他们应提供任何应用程序都可以用于管理单个内容部分缓存的工具。我们将这些工具称为“可缓存”部分,并打算支持我们的仪表板应用程序的使用案例,以将单个仪表板保存用于离线用法。
  • 它们不应对不使用PWA功能的应用产生副作用。

目前,我们将在这篇文章中介绍安装性和简单的离线功能。可缓存的部分是在我们的PWA intro blog中引入的,但是由于它们更复杂并且面临许多特殊的设计挑战,因此将在另一个深度潜水帖子中进行描述。请继续关注DHIS2 developer's blog

添加安装

这是要添加的最简单的PWA功能;所需的只是一个PWA web manifest文件,该文件添加了有关Web应用程序的元数据,以便可以在设备上安装,然后从应用程序的index.html文件链接到它,如:

<link
    rel="manifest"
    crossorigin="use-credentials"
    href="%PUBLIC_URL%/manifest.json"
/>

在应用程序平台中,这是通过扩展App Scripts Cli build脚本的明显生成步骤来实现的(示例构建序列中的step 5)。该脚本从d2.config.js访问应用程序的配置,并使用适当的应用元数据生成manifest.json文件,包括名称,描述,图标和主题颜色;然后将manifest.json写入结果应用程序的public/目录,即.d2/shell/public/。您可以在应用程序脚本cli here中查看清单生成源代码。

然后,App Shell软件包包含应用程序将使用的index.html文件,因此这是指向manifest.json文件will be added的链接。

所有平台应用程序都会生成PWA Web清单,即使未启用PWA,但仅此功能就不会使应用程序可安装。带有'fetch'处理程序的服务人员也必须注册,这很复杂且下面描述。

添加简单的离线功能

通过向应用程序添加服务工作者,将基本的离线功能添加到平台中。服务工作者是一个脚本,可以与应用程序一起安装和运行,并通过收听应用程序中的fetch事件来访问该应用程序的网络流量,然后处理如何处理它收到的请求和响应。

服务工作者可以使用应用程序使用的数据维护离线缓存。然后,当用户的设备脱机并且应用程序制作fetch事件以请求数据时,服务工作者可以使用脱机缓存来响应请求,而需要通过网络获取该数据。这使该应用程序可以离线工作。您可以阅读有关服务工作人员here的基础知识的更多信息;以下各节对它们的工作方式进行了一些了解。

在应用程序平台中实施服务工作者采取了多个步骤:

  1. 创建一个服务工作者脚本以执行离线缓存
  2. 编译服务工作者并将其添加到应用程序
  3. 如果在应用程序的config
  4. 中启用了PWA,请从应用程序注册服务工作者
  5. 管理服务工作者的更新和生命周期

创建服务工作者脚本以执行离线缓存

我们将Workbox库及其公用事业用作我们的服务人员的基础。

有几种不同的策略可用于脱机数据,以平衡性能,网络使用率和数据新鲜度。我们解决了这些策略,以在平台应用中提供基本的离线功能:

  1. 是构建应用程序的一部分(JavaScript,CSS,图像等)的静态资产 conted
  2. 在运行时要求的数据始终使用网络,并结合了 spain-while-while-repbridative 获取图像资产的策略和其他数据的网络 - 优先>策略。

如果您想了解有关我们使用这些策略的决定的更多信息,则在我们的first PWA blog post中对它们进行了更深入的解释。

编译服务工作者并将其添加到应用程序中

服务工作者的实现约束是,当应用程序注册以安装在用户的浏览器中时,它们必须是一个单个,独立的文件,这意味着所有服务工作者代码及其依赖项必须汇总在构建时间中进入一个文件。我们的服务工作者依赖于几个外部包split up among several files,可以将其保留在imported in the App Shell之前,因此我们需要在平台中进行一些汇编工具。

工作箱提供了一个Webpack plugin,该Webpack plugin可以编译服务工作者,然后将生产构建输出到已构建的应用程序。一旦将正在开发的应用注入了我们的应用程序外壳,我们的build process就利用了主要编译步骤的创建React App(CRA)的build脚本,并且恰好将CRA配置在开箱即用以使用Wooksbox -webpack插件以编译服务工作者。它在CRA应用程序的src/目录中编译了service-worker.js文件,并将其输出到构建应用程序的public/目录中,因此我们使用CRA可以满足我们的大多数编译需求。

Wookbox-webpack插件还注入 porpache清单 ,这是服务工作者在安装后会取出和缓存的URL列表。该插件使用webpack从构建过程中输出的缩小静态文件列表来制作此清单,该清单涵盖了应用程序的JavaScript和CSS块以及index.html文件。

但是,这些并不涵盖应用程序build目录中静态资产的全部;需要单独处理来自jQuery等供应商的图标,Web清单和JavaScript文件。为了将这些剩余的文件添加到Portache清单中,我们将另一个步骤添加到我们的 CLI的构建过程中。执行CRA构建步骤后,我们使用koude41函数从koude42软件包中读取App的build目录中的所有其他 static文件,生成这些URL的清单,并注入 在准备好的占位持有人的汇编服务工作者中列表。您可以看到生成的injectManifest代码here

正确处理这些止痛药的表现对于保持应用程序的最新时间也很重要,这将在下面的"Managing the service worker's updates and lifecycle" section中进行描述。

为了实现PWA功能的选择加入性质,只有在应用程序的configuration中启用PWA时才能注册服务工作者。我们在koude24 app config file中添加了一个可以启用PWA的选项,看起来像这样:

// d2.config.js
module.exports = {
    type: 'app',
    title: 'My App',

    // Add this line:
    pwa: { enabled: true },

    entryPoints: {
        app: './src/App.js',
    },
}

d2-app-scripts startbuild流程中,读取配置文件,并将PWA_ENABLED值添加到应用程序的环境变量中。然后,在应用程序适配器的初始化逻辑中,它根据PWA_ENABLED环境变量进行注册或注册服务工作者。

在下面的"Registration of the service worker"部分中更详细地描述了注册逻辑。

管理服务人员的更新和生命周期

管理服务人员的lifecycle既复杂又至关重要。由于服务工作者负责从缓存文件提供该应用程序,因此现在它在用户看到的应用程序中具有哪些角色。请注意,服务工作者从编译时设置的文件列表中为该应用提供服务。因此,服务工作者本身需要在用户的浏览器中进行更新,以便为应用程序的更新版本提供。

如果服务工作者生命周期和更新的管理不善,则该应用程序可能会粘在用户浏览器中的旧版本上,并且永远不会从服务器接收更新。这可能很难诊断,很难解决。下面的"Handling precached static assets between versions" section解释了为什么会发生这种情况。

管理PWA更新可能是一个著名的问题,我们认为我们已经遇到了一个可靠的系统来处理它,我们将在下面描述。

设计一个良好的用户体验以更新PWA应用程序

从UX角度来看,管理服务工作者更新很复杂:我们希望用户使用该应用程序的最新版本,但是更新服务工作者以激活生产中的新应用更新需要页面重新加载,由于下面描述的原因。重新加载可能会导致页面上未保存的数据丢失,因此未经用户同意,我们不想这样做。因此,它提出了UX设计挑战,以通知和说服用户重新加载应用程序尽快使用新更新,同时避免使用任何危险的,计划外的页面重新加载。

更重要的是,我们希望以最不可能的方式做到这一点,理想情况下,没有用户需要考虑任何技术。诸如“可用更新”之类的通知会太侵入性,甚至对某些用户看起来可疑。

要解决这些需求,我们解决的UX设计是:

  1. 首先,如果服务工作者已经安装并且已经准备就绪,我们将不会立即激活它。我们将拭目以待,并尝试在不需要做任何事情的情况下潜行更新。接下来发生的事情取决于一些条件。
  2. 如果这是服务工作者第一次为此应用程序安装,那么任何页面重新加载都将利用已安装的服务工作者,并且PWA功能将在该重新加载的页面中准备就绪。如果打开多个选项卡,则每个选项卡都需要重新加载才能使用服务工作者和PWA功能。
  3. 如果新安装的服务工作者是对现有的服务工作者的更新,但是重新装载将不会自动激活等待服务的工作者。
    1. 如果此应用程序只有一个选项卡,那么下次用户加载页面时,可以在更新中安全潜行。在加载应用程序的主要交互部分之前,App Shell检查了等待服务的工作人员,如果有一个,则将其激活,然后重新加载,因此可以安全地更新服务工作者而不会干扰用户的活动。
    2. 如果用户具有应用程序的多个选项卡,但是,我们将无法快速更新和重新加载。这是因为Active Service Worker同时控制所有活动选项卡,因此要激活新的服务工作者,需要同时重新加载所有选项卡。在未经用户权限的情况下重新加载所有选项卡可能会在其他打开选项卡中丢失未保存的数据,因此我们不想这样做。在这种情况下,我们依靠接下来的选项发生。
  4. 如果安装了新的服务工作者并等待接管,则在用户个人资料菜单的底部可见通知。如果他们单击它,将指示等待服务的工作人员接管,并且页面将重新加载。 Update available notification 如果有多个打开的选项卡,将显示所有选项卡都将重新加载,以便在继续之前保存这些选项卡中的数据。如果可能的话,在模式中显示了选项卡的数量,以帮助用户帐户遗忘的选项卡,如果用户打开了许多浏览器窗口或在移动设备上,则可能发生。 Reload confirmation modal
  5. 如果以上情况都没有发生,则该应用将依靠本机浏览器行为:在该浏览器中的应用程序的所有打开选项卡已关闭后,新的服务工作者将在下次打开应用程序时处于活动状态。

我们还在努力实施以改进此UX:

  1. 当新服务工作者在等待时,将在标题栏中的用户配置文件图标上显示徽章,以表明有新信息可以检查
  2. 在任何服务工作者控制该应用程序之前,标题栏中的某些UI元素将表明尚不可用的PWA功能

应用程序更新流的实现

在应用程序平台中实现此更新流,需要几个合作的功能和许多在服务工作者代码,客户端服务工作者注册功能和React用户界面中的幕后逻辑。

为了简化与navigator.serviceWorker API的反应环境中与服务工作者的沟通,我们制作了一个Offline Interface object,该Offline Interface object处理基于事件的服务与服务工作者的通信,并揭示了更易于使用的注册和更新操作的方法。它还提供了一些功能,可提供可缓存的部分和复杂的离线功能,这些功能将在后续PWA博客文章中进行详细描述。

我们的服务工作者注册功能从创建React App PWA模板registration boilerplate中得出了很多,其中包括一些有用的逻辑,例如检查有效的服务工作者,处理LocalHost上的开发情况以及一些基本的更新检查程序。这些功能是一个有用的起点,但是我们的用例需要更加复杂,这导致了下面描述的详细说明。

服务工作人员的注册

如果启用了PWA,则当应用程序加载时,当离线接口对象为instantiated in the App Adapter时,koude52 functioncalled。在调用window对象上的load事件的register()函数之前,请致电navigator.serviceWorker.register()以提高页面加载性能:注册后的浏览器检查新服务工作者的检查,如果有一个,如果有一个,服务工作者将下载并安装任何需要的应用程序资产,则需要art。这些下载可能是资源密集的,因此它们会延迟以避免在第一次加载时干扰页面响应。

离线接口也是registers a listenernavigator.serviceWorker上的controllerchange事件,该事件将在新服务工作者控制时重新加载页面,即开始处理提取事件。这是为了确保应用程序使用最新的刻板资产加载。

与某些实施不同,我们的服务人员旨在耐心等待它安装。首次安装和激活后,它不会索取开放客户端,即控制这些页面并通过使用clients.claim() API开始处理提取事件;相反,它在控制之前等待页面重新加载。该设计可确保页面仅在其一生中由一个 服务工作者或 none 控制。服务工作者需要重新加载来控制以前未受控制或从上一个页面接管页面的页面。这确保该应用仅使用该应用程序的一个版本中的核心脚本和资产。服务工作者也不会自动 skip等待,并在安装新更新后控制页面;它将继续等待来自应用程序的信号或part 4 of the UX flow above中描述的默认条件。服务工作者的做法是listen for messages从客户端指示要求客户或跳过等待的客户,然后发送的用户单击“配置文件”菜单中的“单击重新加载”选项。听众看起来像这样:

self.addEventListener('message', (event) => {
    if (event.data.type === 'CLAIM_CLIENTS') {
        // Calls clients.claim() and reloads all tabs:
        claimClients()
    }
    if (event.data.type === 'SKIP_WAITING') {
        self.skipWaiting()
    }
})

'CLAIM_CLIENTS'首次为此应用程序安装了服务工作者时使用,并且在安装更新的服务工作者并准备接管时使用'SKIP_WAITING'。下面您可以看到有关这些消息的更多详细信息。

尽可能自动应用应用更新

在大多数情况下,koude62组件使应用程序可以在页面加载时在应用程序更新中潜行,而无需用户需要知道或做任何事情。它是implemented in the App Adapter,并由离线接口支持。它包装了应用程序的其余部分,在将其下方的组件树呈现之前,它检查是否有新的服务工作者正在等待接管。如果有一个,并且仅打开了一个选项卡,则可以指示新服务工作者在加载应用程序之前接管。这允许应用程序安全地更新和重新加载,而不会干扰用户的工作。

export const PWALoadingBoundary = ({ children }) => {
    const [pwaReady, setPWAReady] = useState(false)
    const offlineInterface = useOfflineInterface()

    useEffect(() => {
        const checkRegistration = async () => {
            const registrationState =
                await offlineInterface.getRegistrationState()
            const clientsInfo = await offlineInterface.getClientsInfo()
            if (
                (registrationState === REGISTRATION_STATE_WAITING ||
                    registrationState ===
                        REGISTRATION_STATE_FIRST_ACTIVATION) &&
                clientsInfo.clientsCount === 1
            ) {
                console.log(
                    'Reloading on startup to activate waiting service worker'
                )
                offlineInterface.useNewSW()
            } else {
                setPWAReady(true)
            }
        }
        checkRegistration().catch((err) => {
            console.error(err)
            setPWAReady(true)
        })
    }, [offlineInterface])

    return pwaReady ? children : null
}

渲染后,加载边界首先使用离线接口的getRegistrationState()方法(一种访问koude63 registration function的便利方法)首先检查任何新服务工作者。 getRegistrationState()是服务工作者的安装状态的简化检查,旨在确定现在是否准备好新服务工人。它返回几个值之一:'UNREGISTERED''WAITING'如果已经准备好更新的服务工作者,则'FIRST_ACTIVATION'如果这是第一次安装服务工作者,或者'ACTIVE'如果已经有控制的服务工作者,并且当前没有一个服务工人。

然后,要检查该应用程序的打开数量,PWALoadingBoundary使用了离线接口的koude71 method,它“询问” Ready Service Service Worker与此服务工作者范围相关联的客户有多少客户。为了在每种情况下准确获取此信息,服务工作者需要进行perform some special checks,如下所示。

/** Get all clients including uncontrolled, but only those within SW scope */
export function getAllClientsInScope() {
    // Include uncontrolled clients: necessary to know if there are multiple
    // tabs open upon first SW installation
    return self.clients
        .matchAll({
            includeUncontrolled: true,
        })
        .then((clientsList) =>
            // Filter to just clients within this SW scope, because other clients
            // on this domain but outside of SW scope are returned otherwise
            clientsList.filter((client) =>
                client.url.startsWith(self.registration.scope)
            )
        )
}

服务工作者将koude72 APIincludeUncontrolled选项一起使用,因为在服务工作者第一次安装时,某些选项卡可能无法控制。然后,由于该功能返回该域上的每个打开客户端,甚至在服务工作者控制范围之外的范围之外,因此所得的客户端需要将其过滤至范围中的客户端。服务工作者获取正确的客户端列表后,它将一条消息发给客户端以报告客户端信息。然后,getClientsInfo()方法返回一个承诺,该承诺要么解决客户信息,要么以失败原因拒绝。

如果有一个服务工作者正在等待接管(上面的'WAITING''FIRST_ACTIVATION'条件),并且应用程序打开了一个选项卡,PWALoadingBoundary将通过在离线接口上调用koude78 method来应用Ready更新。该方法指示新的服务工作者接管:â€检测到该新服务工作者是为此应用程序安装的第一个或对现有服务工作者进行更新的工作人员,然后将'CLAIM_CLIENTS'消息发送到第一个安装服务工人或'SKIP_WAITING'消息给更新的服务工作者。跳过服务工作者的等待或要求客户端的客户都会在Open客户端中导致a的controllerchange事件,这触发了活动的侦听器,该活动的侦听器在navigator.serviceWorker上设置了聆听该事件(从"Registration of the service worker"部分中召回)。然后,侦听器将致电window.location.reload()重新加载页面,以便该页面可以在新服务工作者的控制下加载。

如果没有新的服务工作者或打开多个选项卡,则应用程序的其余部分将按照普通状态加载。通过加载应用程序之前进行此检查,该应用程序可以应用PWA更新,而无需用户在大多数情况下需要做任何事情,这对于用户体验来说是一个不错的胜利。

为手动应用更新提供UI

koude84 hook提供了支持UI手动应用更新的逻辑,而koude85 component将钩子连接到相关的UI组件。像PWALoadingBoundary组件一样,挂钩和ConnectedHeaderBar组件在应用程序适配器中实现,并由离线接口支持。两者的代码如下所示 - 仔细查看usePWAUpdateState Hook的onConfirmUpdate()函数,confirmReload()函数和useEffect()钩。

export const usePWAUpdateState = () => {
    const offlineInterface = useOfflineInterface()
    const [updateAvailable, setUpdateAvailable] = useState(false)
    const [clientsCount, setClientsCount] = useState(null)

    const onConfirmUpdate = () => {
        offlineInterface.useNewSW()
    }
    const onCancelUpdate = () => {
        setClientsCount(null)
    }

    const confirmReload = () => {
        offlineInterface
            .getClientsInfo()
            .then(({ clientsCount }) => {
                if (clientsCount === 1) {
                    // Just one client; go ahead and reload
                    onConfirmUpdate()
                } else {
                    // Multiple clients; warn about data loss before reloading
                    setClientsCount(clientsCount)
                }
            })
            .catch((reason) => {
                // Didn't get clients info
                console.warn(reason)

                // Go ahead with confirmation modal with `0` as clientsCount
                setClientsCount(0)
            })
    }

    useEffect(() => {
        offlineInterface.checkForNewSW({
            onNewSW: () => {
                setUpdateAvailable(true)
            },
        })
    }, [offlineInterface])

    const confirmationRequired = clientsCount !== null
    return {
        updateAvailable,
        confirmReload,
        confirmationRequired,
        clientsCount,
        onConfirmUpdate,
        onCancelUpdate,
    }
}
export function ConnectedHeaderBar() {
    const { appName } = useConfig()
    const {
        updateAvailable,
        confirmReload,
        confirmationRequired,
        clientsCount,
        onConfirmUpdate,
        onCancelUpdate,
    } = usePWAUpdateState()

    return (
        <>
            <HeaderBar
                appName={appName}
                updateAvailable={updateAvailable}
                onApplyAvailableUpdate={confirmReload}
            />
            {confirmationRequired ? (
                <ConfirmUpdateModal
                    clientsCount={clientsCount}
                    onConfirm={onConfirmUpdate}
                    onCancel={onCancelUpdate}
                />
            ) : null}
        </>
    )
}

通过使用一个空依赖性阵列的useEffect钩,首先渲染时,usePWAUpdateState钩子通过调用离线接口的checkForNewSW()方法来检查新服务工作者,这基本上只是暴露了koude95 registration function。与PWALoadingBoundary使用的getRegistrationState()函数相比,checkForUpdates()更为复杂,因为它检查了安装和准备就绪的服务工作人员,请听取新的服务,并检查这些州之间的服务工作人员。我们需要检查许多变量以处理所有可能的安装条件:

  • 服务工作者可以是其生命周期的四个步骤之一:安装,安装,激活或激活
  • 可以同时在service worker registration object中以installingwaitingactive同时存在多个服务工作者
  • 有时,活动服务工作者不在 Control 中,因为它是此应用程序的第一个服务工作者安装

对于完整的控制流,请看一下checkForUpdates() source code

如果有准备了新服务,则称为onNewSW()回调函数,作为向checkForNewSW()的参数提供的回调函数,该函数将hook返回的updateAvailable布尔人设置为trueConnectedHeaderBar组件将此值作为道具传递给标头键,该键显示“可用的新应用版本”在用户配置文件菜单中的通知。

Update available notification

如果用户打开配置文件菜单并单击“可用新版本”通知,则调用confirmReload()函数。它通过检查该应用程序的多少个选项卡,以处理更新流的下一部分,以便如果打开多个选项卡,可以向警告显示它们将全部重新加载。像PWALoadingBoundary一样,它使用离线接口的getClientsInfo()方法来获取与此服务工作者关联的客户次数。

收到客户端信息后,如果为此服务工作者范围打开一个客户端,confirmReload()将使用离线接口的koude78 method来指示新服务工作者像PWALoadingBoundary一样控制控制。如果有多个客户端打开,或者getClientsInfo()请求失败,则confirmationRequired布尔人由usePWAUpdateState Hook返回的Boolean将解析为true。在ConnectedHeaderBar组件中,这将导致渲染ConfirmReloadModal警告当所有打开选项卡都重新加载时数据丢失。

Reload confirmation modal

如果用户单击模式中的“重新加载”,则称为onConfirmUpdate()函数,该功能称为offlineInterface.useNewSW()函数并触发更新。如果用户单击“取消”,则称为onCancelUpdate()函数,将confirmationRequired布尔值重置为false,通过将clientsCount设置为null,它将关闭模式。

引擎盖下的所有这些步骤均已协调以创建强大的user experienc described above,并确保服务工作者和应用程序正确更新。

处理版本之间的静态资产

如上所述的"Compiling the service worker" section中所述,在使用App Assets的孔道上时,应在App和Service Worker更新方面正确处理多个注意事项。方便地,这些最佳实践由Wookbox工具(WebPack插件和workbox-build软件包)处理。

使用曲折策略时,即使服务器上有新版本的应用程序,应用程序可能会陷入用户客户端的旧版本。由于不访问网络的不访问网络的情况将直接从高速缓存提供,因此在服务工作者本身更新,下载新的资产并为其服务之前,新应用更新将永远不会访问。

要使服务工作者更新,服务器上的脚本文件需要从客户端上运行的同名文件的文件(在我们的情况下)差异(在我们的情况下) 。浏览器检查服务工作者范围中导航事件或调用navigator.serviceWorker.register功能时的导航事件时,将检查服务工作者的更新。为了确保服务器上的应用程序中的更新最终在客户端的浏览器中,修订Info 在服务工作者的potache清单中的文件名中添加到文件名中,如果该文件名还没有。当更改应用程序文件时,内容哈希将在portache清单中更改,因此service-worker.js文件的内容将不同。

现在,当用户的浏览器检查服务器上的service-worker.js文件时,它将是字节不同的,并且客户端将下载并安装新的应用资产以使用。

您可以在Workbox documentation上阅读有关使用Workbox的肠道的更多信息。

为流氓服务工作者添加杀戮开关

在某些情况下,服务工作者的生命周期可能会失控,并且应用程序工作人员可以与服务旧应用资产的服务工作者陷入困境。如果该应用程序未检测到新的服务工作者,并且不会为用户提供重新加载的选项,则将不会更新用户浏览器中的应用程序。这可能是一个很难调试的问题,并且需要用户的手动步骤来解决。如本文所述,我们一直在努力构建我们的应用程序平台,以至于应用程序不需要做任何特别的事情来处理服务工作者更新 - 这一切都在平台层和离线接口中进行了处理。当一个旧版本的应用程序曾经注册服务工作者并通过限制策略为应用程序资产服务时,我们有时会遇到这个问题。然后,当在没有服务工作者的情况下部署新版本的应用程序时,新部署的应用程序无法从上一个版本中接管。即使已将新版本部署到服务器上,该应用似乎被卡在旧版本上,并且缺少新的修复程序。

为了处理这个流氓服务工作者案件,我们向平台中的服务工作者添加了杀戮 - 开关模式,这将帮助使用该应用程序的旧版本的服务工作者来解开应用程序。这利用了浏览器的服务工作者更新设计:为了响应注册事件或活动范围的导航,浏览器也会检查服务器是否具有相同的文件名的新版本的服务工作者,即使那样服务人员被缓存。如果服务器上有一个服务工作者,并且与活动工具的字节差异为字节,则浏览器将启动从服务器下载的新服务工作者的安装过程(这也与上述更新过程有关)。

为了利用该过程,每个平台应用程序实际上都会在构建的应用程序中添加了一个名为service-worker.js的编译服务工作者,无论是否启用了PWA。这有助于一个非PWA应用程序接管并卸载了安装在用户浏览器中的PWA应用程序。对于非PWA应用程序,如果确实安装了从PWA应用程序接管该代码:

/** Called if the `pwaEnabled` env var is not `true` */
export function setUpKillSwitchServiceWorker() {
    // A simple, no-op service worker that takes immediate control and tears
    // everything down. Has no fetch handler.
    self.addEventListener('install', () => {
        self.skipWaiting()
    })

    self.addEventListener('activate', async () => {
        console.log('Removing previous service worker')
        // Unregister, in case app doesn't
        self.registration.unregister()
        // Delete all caches
        const keys = await self.caches.keys()
        await Promise.all(keys.map((key) => self.caches.delete(key)))
        // Delete DB
        await deleteSectionsDB()
        // Force refresh all windows
        const clients = await self.clients.matchAll({ type: 'window' })
        clients.forEach((client) => client.navigate(client.url))
    })
}

它将在安装完成以要求所有开放客户端的安装后立即等待,并且在控制自己后,将自身注册,删除所有CacheStorage缓存和一个“部分”索引索引索引,该索引将在有关可缓存的后续帖子中介绍部分,然后重新加载页面。重新加载后,服务工作者将不活跃,新的应用资产将从服务器中获取而不是离线缓存服务,从而使应用程序正常运行。

最终,通过包括这种杀戮开关模式,我们阻止应用程序陷入未来我们过去一直陷入困境的应用程序。

但是,请注意,如果您的应用程序还使用CacheStorage或可缓存的部分工具,这可能会导致数据丢失。但是,一个杀戮切换工人激活非常不寻常,因此遇到这样的问题的可能性很大,但是我们想指出,对于可能使用这些工具的少数开发人员。

结论

我们希望您喜欢DHIS2应用程序平台及其PWA功能的介绍。我们介绍了安装性,构建工具以阅读应用程序的配置并编译服务工作者,缓存策略以及服务工作者更新和生命周期管理。我们描述的许多挑战和解决方案都适用于任何PWA应用程序开发人员。我们希望您也对这些功能如何共同起作用以使DHIS2应用程序的离线功能有更深入的了解。如果您发现这篇文章有趣或有用,请在下面发表评论!

在后续文章中,我们将描述设计挑战和解决方案,以创建可缓存的部分以及PWA introduction blog post中描述的其他一些应用程序运行时功能(请继续关注DHIS2 developer's blog,并在此处关注)。

)。

)。

您是否想了解有关此主题的更多信息,或者还有其他问题或评论?可以随意通过e-mailSlackTwitter或我们的Community of Practice与我们联系!我们总是很高兴听到感兴趣的开发人员和社区成员的来信。如果您想加入我们的团队以应对PWA实施等挑战,请在我们的网站中查看我们的careers section。我们所有的软件团队角色都是遥不可及的,我们鼓励所有身份和背景的人申请。