Ever since I wrote my first post on the tech I used to build the bedav.org, I’ve expanded to Pune, provided an overview for the entire city and created a page to view all locations along with their overviews (total, available and occupied beds in those cities/districts) and made it into an offline-first Progressive Web App.
自从我写了第一篇有关建造bedav.org的技术的文章以来,我已经扩展到了浦那,提供了整个城市的概览,并创建了一个页面来查看所有位置及其概览(总计,可用和可用)。在这些城市/地区占用床位),并使其成为脱机优先的渐进式Web应用程序。
Here, I’ll be talking about how I turned the website into a Progressive Web App (PWA) while focussing on an offline-first experience.
在这里,我将讨论如何将网站转变为渐进式Web应用程序(PWA),同时着重于离线优先体验。
但是首先,什么是脱机首次体验和渐进式Web应用程序? (But first, what is an offline first experience and a Progressive Web App?)
Below is a talk from Google I/O 2016 by Jake Archibald on how he turned an existing PWA into an offline-first experience and compares the improvement in performance by making it offline-first every step of the way.
以下是杰克·阿奇博尔德( Jake Archibald)在Google I / O 2016上的演讲,主题是他如何将现有的PWA变成脱机优先的体验,并通过将脱机优先置于每一个步骤的方式来比较性能的改进。
But if you do not want to watch the 45-minute video, here are the short definitions.
但是,如果您不想观看45分钟的视频,则可以使用以下简短定义。
渐进式Web应用程序(PWA) (Progressive Web Apps (PWA))
Progressive Web Apps in simple terms are Web applications that provide the speed, features and reliability similar to or equal to that of native apps. A web application that qualifies as a PWA can be installed and used as a standalone app with a native app like experience.
简单来说,渐进式Web应用程序是提供与本地应用程序相似或相等的速度,功能和可靠性的Web应用程序。 可以安装符合PWA资格的Web应用程序,并将其用作具有类似本机应用程序的独立应用程序。
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/fc29f3434a932019c9e878f7a32701a7.png)
PWA’s allow for easier access to your application, a much faster and responsive interface and in most cases allows users to access your application when they are offline.
PWA允许更轻松地访问您的应用程序,更快和响应速度更快的界面,并且在大多数情况下,允许用户在脱机时访问您的应用程序。
Check out this amazing article to learn more about PWA’s.
看看这篇很棒的文章,以了解有关PWA的更多信息。
先离线 (Offline first)
An offline-first website is a website that works offline (I think that was obvious lol). But it’s a lot more than just that. The website is made available offline by caching all assets and data. The result of caching all the assets and data is a uniform and fast user experience regardless of whether the user is online, offline or has a bad internet connection.
离线优先网站是可以离线工作的网站(我认为这很明显)。 但是,不仅限于此。 通过缓存所有资产和数据,可以离线使用该网站。 缓存所有资产和数据的结果是一致且快速的用户体验,无论用户是联机,脱机还是Internet连接不良。
To read more about the offline-first architecture checkout this article.
要了解有关脱机优先架构的更多信息,请参见本文。
To turn bedav.org into a PWA with an offline-first experience, the following things had to be done:
将bedav.org变成 具有离线优先体验的PWA,必须完成以下操作:
- Reduce the initial render times of the page 减少页面的初始渲染时间
- Caching assets that render the page缓存呈现页面的资产
- Cache the queried data缓存查询的数据
减少初始渲染时间(Reducing the initial render times)
The initial render times were around 5 seconds. The initial render times were incredibly high since all the hospitals were being rendered during the initial render.
初始渲染时间约为5秒。 由于所有医院都在初始渲染期间进行了渲染,因此初始渲染时间非常长。
The most common solution was implementing pagination with infinite scroll. But if that was done, it would mean sending a network request to the server every time the user reaches the bottom of the page. For many applications, this is not a problem. However, when the data for all the hospitals was only 25KB, sending numerous network requests to fetch data of only about a kilobyte is just too inefficient. Rather, a better solution was to fetch all the data at once and render it as the user scrolls down. This decreased initial render times to only 1–1.5 seconds!
最常见的解决方案是使用无限滚动实现分页。 但是,如果这样做,那就意味着每次用户到达页面底部时,都会向服务器发送网络请求。 对于许多应用程序来说,这不是问题。 但是,当所有医院的数据只有25KB时,发送大量网络请求以仅提取大约千字节的数据的效率太低。 相反,更好的解决方案是一次获取所有数据并在用户向下滚动时呈现它们。 这将初始渲染时间减少到只有1-1.5秒!
缓存资产 (Caching Assets)
To cache assets there were two options. Either using regular HTTP caching or Service Workers along with the caches API.
要缓存资产,有两种选择。 使用常规的HTTP缓存或服务工作者以及缓存API。
Service Workers along with the caches API was the obvious choice here as it’s reliable, predictable, persists until you delete it and can be accessed even when the user is offline. For a detailed comparison and explanation of the two check out this article on web.dev.
Service Workers与缓存API一起在这里是显而易见的选择,因为它可靠,可预测,并且一直持续到您删除它为止,即使用户处于脱机状态也可以访问。 有关这两者的详细比较和说明,请参阅web.dev上的本文。
What are service workers though?In short Service Workers are scripts that run in the background, independent of your main website. They enable a lot of things that were previously only possible with native apps such as sending push notifications, intercepting network requests, caching assets or background sync. Here’s a great article explaining service workers and walking through its basic usage.
什么是服务人员? 简而言之,服务人员是在后台运行的脚本,与您的主要网站无关。 它们启用了许多以前只有本机应用程序才能实现的功能,例如发送推送通知,拦截网络请求,缓存资产或后台同步。 这是一篇很棒的文章,解释了服务人员并逐步介绍了其基本用法。
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/df98240232f781c3a60775852c73f1f5.png)
The most common practice is to cache the assets required for the landing page and when the user navigates to another page, only then cache the assets required for that page (dynamic cache).
最常见的做法是缓存着陆页所需的资产,并且当用户导航到另一个页面时,才缓存该页面所需的资产(动态缓存)。
But in the case of bedav.org, there were only 4 different views/pages. The landing page showing all the locations, the locality view which shows the hospitals in a certain location, the hospital view which shows all the information regarding the hospital and the about page. In total all the assets required to render these pages were only about 2 MB, so I decided to just cache all of them when the user first visits the website itself or rather during the install event of the service worker. Even the fonts and the icons used on the page are cached when the service worker is installed.
但是对于bedav.org,只有4个不同的视图/页面。 显示所有位置的登录页面,显示特定位置的医院的位置视图,显示有关医院的所有信息的医院视图和About页面。 总计,渲染这些页面所需的全部资产只有大约2 MB,因此我决定仅在用户首次访问网站本身时或更确切地说在服务工作者的安装事件期间缓存所有页面。 安装Service Worker时,即使页面上使用的字体和图标也会被缓存。
All the javascript bundles are named using a content hash, which means even the slightest change to the code and the bundle name is changed. Since I decided to cache all assets required by the website during the install event, the service worker needed the names of the bundles generated by webpack. To get the names of these bundles and use them in the service worker, I used the serviceworker-webpack-plugin.
所有JavaScript包都使用内容哈希来命名,这意味着即使对代码进行了最细微的更改,包名称也都已更改。 由于我决定在安装事件期间缓存网站所需的所有资产,因此服务工作者需要Webpack生成的捆绑包的名称。 为了获取这些捆绑包的名称并在服务工作者中使用它们,我使用了serviceworker-webpack-plugin 。
Every time the user visits the website, the browser checks if there’s a change in the service worker and if there is a change it installs the new version. Since the bundle names are injected into the service worker by the serviceworker-webpack-plugin, the contents of the compiled service worker change every time I modify my code. Hence, the latest version of the website is always available to the user.
每次用户访问该网站时,浏览器都会检查Service Worker中是否有更改,以及是否有更改,它将安装新版本。 由于捆绑包名称是通过serviceworker-webpack-plugin注入到service worker中的,因此每次修改代码时,已编译service worker的内容都会更改。 因此,该网站的最新版本始终可供用户使用。
Versioning the cache is one more thing that had to be done because we don’t want older assets that are not going to be used anymore taking up space on the users’ device. There were many options for this. We could either version the cache based on the date or limit the size of the cache. But in either case, there are chances of overusing the cache. Since the main bundles’ name includes a content hash, I just decided to use that content hash as the name of the cache for that particular version.
对缓存进行版本控制是必须要做的另一件事,因为我们不希望不再使用不再使用的旧资产占用用户设备上的空间。 有很多选择。 我们可以根据日期对缓存进行版本控制,也可以限制缓存的大小。 但无论哪种情况,都有可能过度使用缓存。 由于主捆绑包的名称包含内容哈希,因此我决定使用该内容哈希作为该特定版本的缓存名称。
缓存数据 (Caching the Data)
For caching to work with GraphQL APIs we have to query for the same data. All the variable values and fields requested have to be the same. Even if the value of a variable/argument is slightly different we cannot retrieve it from the cache as the results would most likely be different.
为了使用GraphQL API进行缓存,我们必须查询相同的数据。 请求的所有变量值和字段必须相同。 即使变量/参数的值略有不同,我们也无法从缓存中检索它,因为结果很可能会有所不同。
If the user sorted by distance and searched for ‘s’, the query would look something like:
如果用户按距离排序并搜索“ s”,则查询将类似于:
query {
hospitals(lat: 1.12334, lon: 1.12345, orderBy: DISTANCE, descending: true, searchQuery: 's') {
name
distance
...otherFields
}
}
Now, the moment the user types another character in the search box another request is sent with a brand new query. If the user decides to sort by the available beds in the ICU, another request is sent. We do not want to cache every single query we send. Notice that we pass the latitude and longitude in the query arguments and the distance is calculated on the server side. This means if the user moves even slightly and coordinates change by a really small value, the query would change and we would not be able to retrieve it from the cache.
现在,在用户在搜索框中键入另一个字符的那一刻,另一个请求随即发出了全新查询。 如果用户决定按ICU中的可用床分类,则发送另一个请求。 我们不想缓存我们发送的每个查询。 注意,我们在查询参数中传递了纬度和经度,并且距离是在服务器端计算的。 这意味着,即使用户略微移动并且坐标更改的值非常小,查询也会更改,我们将无法从缓存中检索它。
本地化运营 (Localizing Operations)
To solve these problems, the number of arguments had to be reduced or they had to be eliminated. To do this we would have to get all hospital data at once and calculate the distance and perform all operation related to sorting, filtering and searching locally.
为了解决这些问题,必须减少争论的数量,或者必须消除争论的数量。 为此,我们必须立即获取所有医院数据并计算距离,并执行与本地分类,过滤和搜索有关的所有操作。
But, if we localized these calculations and operations wouldn’t that slow down the website?Based on testing, no it doesn’t. Since we have only a maximum of 300–400 hospitals per location, there isn’t much data to process. In fact, after localizing these operations, the results of searching, filtering and sorting were shown instantaneously (literally)! Now even if the user has a bad internet connection or even no internet connection, they can still search, filter and sort through the hospitals without giving up on speed.
但是,如果我们对这些计算和操作进行了本地化,这会不会减慢网站的速度? 根据测试,不,不是。 由于每个地点最多只有300-400家医院,因此没有太多数据需要处理。 实际上,在对这些操作进行本地化之后,搜索,过滤和排序的结果即刻显示(字面意义)! 现在,即使用户的互联网连接不良或什至没有互联网连接,他们仍然可以在医院中进行搜索,过滤和分类,而不会降低速度。
To do this I created a separate context whose value is the hospitals that need to be displayed, including the order and the HospitalList
component takes care of rendering them (including the pagination strategy mentioned in the reducing initial render times section).
为此,我创建了一个单独的上下文,其值是需要显示的HospitalList
,包括订单,而HospitalList
组件负责呈现它们(包括减少初始呈现时间部分中提到的分页策略)。
Now, querying for all the hospitals would look something like this
现在,查询所有医院看起来像这样
query {
hospitals {
name
latitude
longitude
# the latitude and longitude of the hospitals to calculate the
distance
...otherFields
}
}
Now the query is going to remain the same even when user moves around, searches for a hospital or even filter and sort through them.
现在,即使用户四处走动,搜索医院甚至过滤并对其进行排序,查询也将保持不变。
从中继切换到阿波罗 (Switching from Relay to Apollo)
The next step was to enable caching. At the time I was using Relay as my GraphQL Client and implementing a good and efficient caching strategy became a pain. Here are just some of the downsides of Relay with my limited experience with it:
下一步是启用缓存。 当时,我将Relay用作GraphQL Client,实现良好而有效的缓存策略变得很痛苦。 以下是中继的一些缺点,但我的经验有限:
- Community is pretty small and support is lacking 社区很小,缺乏支持
- Overly complicated concepts过于复杂的概念
- Documentation is subpar说明文件低于标准
You can cache queries via Relay but, it persists only in memory and only for a certain period. I was looking for something that could persist the cache in the local storage and be able to use it the next time the user visits the page no matter when it is. Persisting it in the local storage would also mean that the user would be able to use it online.
您可以通过Relay缓存查询,但是查询仅在内存中保留,并且仅保留一定时间。 我一直在寻找可以将缓存保留在本地存储中的东西,无论用户何时访问页面,都能在下次访问该页面时使用它。 将其保留在本地存储中还意味着用户将能够在线使用它。
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/c3a9695166a200d7ce447cf58b0126ad.png)
Due to the above mentioned reasons, I decided to make the switch to Apollo. First thing I did was go to the Apollo documentation and you could already see the difference. The documentation is so much better in every way!
由于上述原因,我决定切换到Apollo。 我所做的第一件事是转到Apollo文档,您已经可以看到其中的区别。 文档在各个方面都好得多!
One immediate advantage I saw was that navigating through multiple pages was a lot faster than before. For example, if you go the Bangalore page, then to the Pune page and then again you go back to the Bangalore page, there is no request sent to the server. Apollo gets the results from its cache. And this works with the default setup!
我看到的一个直接好处是浏览多个页面的速度比以前快了很多。 例如,如果您转到Bangalore页面,然后转到Pune页面,然后再次返回Bangalore页面,则没有请求发送到服务器。 Apollo从其缓存中获取结果。 这适用于默认设置!
Apollo’s caching techniques are much smarter than Relay’s.
Apollo的缓存技术比Relay的聪明得多。
The
InMemoryCache
normalizes query response objects before it saves them to its internal data store. Normalization involves the following steps:
InMemoryCache
将查询响应对象保存到其内部数据存储之前将其标准化。 规范化涉及以下步骤:The cache generates a unique ID for every identifiable object included in the response.
缓存为响应中包括的每个可识别对象生成唯一的ID 。
The cache stores the objects by ID in a flat lookup table.
高速缓存通过ID将对象存储在平面查找表中。
Whenever an incoming object is stored with the same ID as an existing object, the fields of those objects are merged.
每当输入的对象以与现有对象相同的ID存储时,这些对象的字段就会合并。
If the incoming object and the existing object share any fields, the incoming object overwrites the cached values for those fields.
如果传入对象和现有对象共享任何字段,则传入对象将覆盖这些字段的缓存值。
Fields that appear in only the existing object or only the incoming object are preserved.
仅出现在现有的对象或者只传入对象字段被保留。
To learn more about caching in Apollo, checkout their documentation here.
要了解有关在Apollo中进行缓存的更多信息,请在此处查看其文档。
The entire application was migrated from Relay to Apollo within a few hours! In the default setup, Apollo caches the query results in the memory. This is a problem because that cache only exists until the user is on the page. Once, the user exits the page, the cache is cleared. To persist the cache in the local storage I used apollo-cache-persists. All I had to do was add 3 lines of code and now my cache persisted in the local storage!
整个应用程序在几个小时内就从Relay迁移到Apollo! 在默认设置中,Apollo将查询结果缓存在内存中。 这是一个问题,因为该缓存仅存在,直到用户进入页面为止。 一旦用户退出页面,缓存将被清除。 为了将缓存保留在本地存储中,我使用了apollo-cache-persists 。 我要做的就是添加3行代码,现在我的缓存一直保存在本地存储中!
But now, the cache would never be updated because the data is always available in the cache. To fix this, all I had to do was set the fetch policy to cache-and-network
. By using cache-and-network
the data is always read from cache if it’s available and the data in the cache itself is updated in the background.
但是现在,缓存将永远不会更新,因为数据始终在缓存中可用。 为了解决这个问题,我所要做的就是将抓取策略设置为cache-and-network
。 通过使用cache-and-network
,总是从缓存中读取数据(如果有的话),并且缓存本身中的数据会在后台更新。
优化缓存 (Optimizing the Cache)
Lastly, there was one final problem. This wasn’t a problem, rather something that could be improved. When you go to a hospitals page, all the information is refetched. But, a lot of that information would already be added to the cache when the user visits a locations page. The only extra information needed for the hospital page was their phone number, website and address. So, I just added these fields to the query which fetches all the hospitals on the locations page and made use of client.readFragment
to fetch that data from the cache if it’s available.
最后,还有一个最后的问题。 这不是问题,而是可以改进的地方。 当您转到医院页面时,将重新获取所有信息。 但是,当用户访问位置页面时,很多信息已经被添加到缓存中。 医院页面所需的唯一额外信息是他们的电话号码,网站和地址。 因此,我只是将这些字段添加到查询中,以获取位置页面上的所有医院,并利用client.readFragment
从缓存中获取该数据(如果有)。
这些变化的结果 (Results of these changes)
- Initial render times decreased from around 5 seconds to only about a second.初始渲染时间从大约5秒减少到仅大约一秒。
- Searching, filtering and sorting are now literally instantaneous. 现在,搜索,过滤和排序实际上是瞬时的。
- The website can be used even when the user is offline. 即使用户处于脱机状态,也可以使用该网站。
- The speed and responsiveness of the website is pretty much independent of the users’ internet connection, i.e. it works equally well when the user is online, offline or has a bad internet connection (assuming they’ve visited the website before). 网站的速度和响应能力几乎与用户的互联网连接无关,即,当用户在线,离线或互联网连接不良(假设他们以前访问过该网站)时,它的效果同样好。
- It can now install the app and add it to their home screen to access it like a native app. 现在,它可以安装该应用程序并将其添加到其主屏幕,以像本机应用程序一样访问它。
- Lastly, it reduced the load on our server and database (this was never the goal but a welcome side effect!). 最后,它减轻了我们服务器和数据库的负载(这不是目标,而是令人欢迎的副作用!)。
All in all, these changes significantly improved the performance of the website, improved the user experience and made it more accessible, specially to those living in remote areas.
总而言之,这些更改大大改善了网站的性能,改善了用户体验,并使网站更易于访问,特别是对于偏远地区的人们而言。
Note: None of these queries are the actual queries used on the website and serves only as examples.
注意:这些查询都不是网站上使用的实际查询,仅作为示例。
翻译自: https://medium.com/swlh/improving-the-user-experience-on-bedav-org-104640e3f455