Progressive Rails 应用
什么是 PWA(面向 Rails 开发者)
入门
推送通知
离线模式
热模块重载
Progressive Rails 应用
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
使用 Ruby on Rails 和 Webpacker 创建渐进式 Web 应用的分步教程
如果你在谷歌上搜索这个主题,你会找到很多关于如何使用 Rails 实现渐进式 Web 应用的教程。但我对目前找到的所有方案都不太满意:我想要的是一个完全基于 Webpack 的解决方案。
在本文中,我将向您介绍如何使用最新的 Webpacker 和最新的 Rails 版本来实现渐进式 Web 应用 (PWA)。
这项技术仅使用一个简单的 npm 包,并且支持任何现代 Rails 应用程序,因此我们将定义一个新概念:“渐进式 Rails 应用程序”(PRA)。
我提供的解决方案无需通过控制器来提供 Service Worker,也无需跳过 Webpack 管道,从而避免了任何变通方法。
如果您已经了解 PWA 是什么,可以直接跳到“入门指南”;否则,下一章将为您准备。
什么是 PWA(面向 Rails 开发者)
要使我们的应用被认定为 PWA,必须满足一些条件。一旦所有这些条件都得到满足,用户就可以像安装普通应用一样在移动设备上安装该应用。此外,不久之后,您还可以将您的 PWA 发布到 Play 商店或将其安装到桌面设备上。
让我们从 Rails 开发者的角度逐一来看这些要点:
即使离线也能立即加载
我们的应用程序必须注册一个 Service Worker,以便在没有网络连接的情况下也能加载。本教程重点介绍如何为 Rails 应用程序配置 Service Worker,我们将使用 Webpacker 来实现。
该网站通过 HTTPS 提供服务。
对于 Rails 开发者来说,这仅仅意味着需要config.force_ssl = true在production.rb配置中添加相应的参数。这样,你的网站将通过 https 提供服务,从而满足此要求。
响应式设计
有很多前端库可以简化响应式应用程序的设计。在之前的文章中,我解释了为什么你应该放弃 Sprockets,转而使用 Webpack 来处理 CSS和其他静态资源。如果你已经这样做了,那么你可以直接使用 npm 包来安装 Bootstrap 或 Bulma,它们可以帮助你设计前端。
manifest.json
您需要将此文件放置在公共文件夹中,该文件将包含在用户设备上安装应用程序所需的所有信息。
入门
创建一个全新的 Rails 应用(或者使用你现有的应用):
rails new progressive-rails-app --skip-sprockets
并在配置文件中启用生产环境的 HTTPS config/environments/production.rb。
这一点很重要,因为 PWA 的一个要求是它必须运行在 HTTPS 上。
如果您是从零开始,以下是一些创建内容的命令。我们添加一个简单的帖子 CRUD 功能,以便开始测试我们的应用程序:
bundle exec rails g scaffold Post title:string content:text
bundle exec rails db:migrate
并将根页面指向:
root 'posts#index'
你还可以添加一些种子:
5.times do |i|
Post.create(title: "Post #{i}", content: 'A lot of stuff')
end
现在我们有了一些数据和一个可运行的应用程序,我们可以开始添加使其成为 PWA 所需的功能了。
manifest.json
如果您通过 Google Chrome 控制台调试您的应用,您会发现您的应用不包含清单文件。
我们来创建一个清单文件。这是每个 PWA 的起点。
将以下内容添加到您的application.html.erb模板中:
<link rel="manifest" href="/manifest.json">
在项目文件夹中创建一个清单文件。清单文件应包含所有必要的配置项,以避免浏览器中出现警告。以下是一个manifest.json示例:public
# manifest.json example
{
"short_name": "PWA",
"name": "Progressive Web App",
"icons": [
{
"src": "/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6"
}
您可以从https://www.favicon-generator.org/生成您需要的所有图标,并将它们放在一个public/icons文件夹中。
调整 Webpacker 配置
在撰写本文时,Webpacker 提供的默认配置还不允许我们编写渐进式 Web 应用程序。
主要原因是 Service Workers 应该位于 public 文件夹中,而所有资源都编译在 public/packs 文件夹中。
我们需要自定义 Webpacker 配置,以便:
- 持续编译 public/packs 文件夹中的所有资源;
- 在 public 文件夹中编译 Service Worker,不要在名称末尾添加井号;
- 让 webpack-dev-server 监听整个公共文件夹,以便在 Service Worker 更改时也能热重载内容。
我修改了@rails/webpacker配置并将其发布为npm 包。该包允许您在开发渐进式 Rails 应用时充分利用 Webpack 的强大功能和热模块重载等高级特性。它完美地解决了上述问题。
安装webpacker-pwanpm 包并进行如下yarn add webpacker-pwa更改:config/webpack/environment.js
const { resolve } = require('path');
const { config, environment, Environment } = require('@rails/webpacker');
const WebpackerPwa = require('webpacker-pwa');
new WebpackerPwa(config, environment);
module.exports = environment;
并添加以下内容webpacker.json:
service_workers_entry_path: service_workers
service_workers这是我们将编写 Service Worker 的文件夹。
如果配置正确,运行后bin/webpack应该会看到:
webpacker-pwa is configured but no service workers are available.
第一位服务工作者
service-worker.js在文件夹中创建一个文件app/javascript/service_workers,并添加以下调试内容:
self.addEventListener('install', function(event) {
console.log('Service Worker installing.');
});
self.addEventListener('activate', function(event) {
console.log('Service Worker activated.');
});
self.addEventListener('fetch', function(event) {
console.log('Service Worker fetching.');
});
并将其注册到application.js:
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('ServiceWorker registered: ', registration);
var serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
console.log('Service worker installing.');
} else if (registration.waiting) {
serviceWorker = registration.waiting;
console.log('Service worker installed & waiting.');
} else if (registration.active) {
serviceWorker = registration.active;
console.log('Service worker active.');
}
}).catch(registrationError => {
console.log('Service worker registration failed: ', registrationError);
});
});
这将使我们能够查看服务工作线程是否正常工作。
注意:目前请勿使用 webpack-dev-server。它尚未正常工作,但我们会尽快解决!
注2:添加public/service-worker.js*到.gitignore。您不应该直接推送此文件。它由webpack编译。
如果一切操作正确,您将在 Chrome 开发者工具控制台中看到以下内容:
在 Chrome 控制台中,您应该看到:
ServiceWorker registered:
Service Worker installing.
Service worker installing.
Service Worker activating.
现在,当我们部署应用程序并且 SSL 可用时,您应该会在浏览器上看到安装图标。
如果你想在本地进行测试,可以使用像 ngrok.com 这样的服务,只需ngrok http 3000在项目的根文件夹上运行即可。
如果收到“阻止主机”错误,可以通过config.hosts = nil在内部设置来暂时禁用此检查config/environments/development.rb。
你的渐进式 Rails 应用
搞定!你的第一个 Service Worker 已经运行成功,你的应用现在正式成为一个渐进式 Rails 应用了!🎉
从现在开始,您可以按照任何渐进式 Web 应用 (PWA) 教程来着手实现您的服务。如果您已经熟悉 Service Worker,可以直接像往常一样开始编码。但在这里,我将更详细地介绍如何最大限度地发挥渐进式 Rails 应用的优势,并实现一些您可能经常需要的功能。
推送通知
这就是你来这里的原因,对吧?你也想向用户发送推送通知,对吧?你也想让你的应用显示“想要向您发送推送通知”这条超级烦人的消息,然后开始骚扰你的客户,对吧?那就开始吧!
征得许可
首先需要请求权限,否则无法发送任何通知。您应该在注册 Service Worker 之后执行此操作,因此请按如下方式更改 application.js 中的代码:
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('ServiceWorker registered: ', registration);
// all code from before
window.Notification.requestPermission().then(permission => {
if(permission !== 'granted'){
throw new Error('Permission not granted for Notification');
}
});
});
你应该妥善处理所有情况,但这超出了本教程的范围,请参考例如这篇教程,以了解更多关于如何/何时请求权限以及如何管理用户所有可能响应的详细信息。
点击“允许”以授予通知权限,如果您刷新页面,它将不会再询问您,因为它之前已经获得了权限。
订阅通知服务
您需要生成一对公钥/私钥才能发送通知。有关原因的详细信息,请参阅上面链接的文章。
yarn global add web-push
web-push generate-vapid-keys
现在,请将我们的service-worker.js设置更改为订阅推送管理器并对推送通知做出反应:
function urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (var i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
self.addEventListener('install', function(event) {
console.log('Service Worker installing.');
});
self.addEventListener('activate', async function(event) {
console.log('Service Worker activated.');
try {
const applicationServerKey = urlB64ToUint8Array('<YOUR_PUBLIC_KEY_HERE>')
const options = { applicationServerKey, userVisibleOnly: true }
const subscription = await self.registration.pushManager.subscribe(options)
console.log(JSON.stringify(subscription))
} catch (err) {
console.log('Error', err)
}
});
self.addEventListener('fetch', function(event) {
console.log('Service Worker fetching.');
});
self.addEventListener('push', function(event) {
console.log('[Service Worker] Push Received.');
console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);
const title = 'A nice title';
const options = {
body: event.data.text(),
icon: 'images/icon.png',
badge: 'images/badge.png'
};
event.waitUntil(self.registration.showNotification(title, options));
});
每次更改服务工作线程时,都需要在开发者工具的 Chrome 应用程序标签页中手动取消注册,然后刷新页面。
您应该在浏览器控制台中看到:
{ "endpoint":"https://fcm.googleapis.com/fcm/send/...",
"expirationTime":null,
"keys":{
"p256dh":"...",
"auth":"..." }
}
留着它们,以后会用到的。
在 DevTools 中,从您取消注册 Service Worker 的同一个窗口中,您现在可以发送测试推送通知。
从 Ruby 发送通知
简而言之,这很简单。添加webpushgem 并使用以下代码:
require 'webpush'
Webpush.payload_send(
message: 'Hello from ruby',
endpoint: <ENDPOINT-HERE>,
p256dh: <P256DH-HERE>,
auth: <AUTH-HERE>,
vapid: {
subject: 'Hello from ruby',
public_key: <PUBLIC-KEY>,
private_key: <PRIVATE-KEY>
}
)
您可以将其另存为notifications.rb并运行ruby notifications.rb以进行测试。
这意味着,当您在前端订阅时,您需要在后端保存端点和所有其他信息,并(可能)将它们与您的用户关联起来。
离线模式
即使没有网络连接,我们也希望向用户显示一些内容。让我们看看如何实现。我将介绍一个简单的离线页面,其中包含一张图片,之后的内容就由你来决定了。我的灵感来源于这篇文章。
首先创建离线页面:
get 'offline', to: 'home#offline', as: :offline
class HomeController < ApplicationController
def offline
render 'offline', layout: false
end
end
我们渲染一个非常简单的视图,不使用应用程序其余部分使用的布局。
html
head
css:
body {
background-color: #00a4cd;
color: #ffffff;
padding: 4rem 2rem;
}
.text-center {
text-align: center;
}
body
.text-center
= image_pack_tag 'logo_white.svg'
h1.text-center
' You need a working internet connection to use Agreeder
p.text-center
' Offline mode is not supported yet
并按如下方式编辑您的 Service Worker:
const OFFLINE_VERSION = 1;
const CACHE_NAME = 'offline';
const OFFLINE_URL = 'offline';
self.addEventListener('install', function (event) {
event.waitUntil((async () => {
const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the response
// isn't fulfilled from the HTTP cache; i.e., it will be from the network.
await cache.add(new Request(OFFLINE_URL, {cache: 'reload'}));
})());
});
self.addEventListener('activate', async function (event) {
event.waitUntil((async () => {
// Enable navigation preload if it's supported.
// See https://developers.google.com/web/updates/2017/02/navigation-preload
if ('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable();
}
})());
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
self.addEventListener('fetch', function (event) {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
event.respondWith((async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
return await caches.match(event.request) || await fetch(event.request);
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log('Fetch failed; returning offline page instead.', error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})());
});
重新加载 Service Worker 并刷新页面。现在,您可以在 Chrome 开发者工具中启用离线模式,您应该可以看到一个离线页面!
当然,要显示图片,我们还需要将其缓存。添加到缓存很简单,只需将其添加到添加离线页面的位置即可:
await cache.add(new Request('/packs/media/images/logo_white-a925d045a774f93a59598e709010d411.svg', {cache: 'reload'}));
重新加载 Service Worker,在“在线模式”下刷新页面,并在 Chrome 开发者工具中检查资源是否已正确缓存:
这种方法虽然可行,但并非最佳方案,因为你需要指定要缓存的资源的准确名称,而且如果资源或离线页面发生更改,你还需要记得更新 Service Worker。我会在另一篇文章中解释如何解决这个问题,但如果你时间紧迫,想提前了解,可以看看Google Workbox。
热模块重载
webpack-dev-server热模块重载仍然有效,但很遗憾,我们的服务工作线程无法使用。Webpacker 中间件默认只将请求重定向到/packswebpack-dev-server,而由于我们的服务工作线程位于公共文件夹目录中,因此无法被提供服务。
我开发了一个小型 gem,它添加了一个中间件,并允许你同时运行 Service Worker。它的名字是 [此处应填写webpacker-pwagem 名称]。你可以将它添加到你的 Gemfile 文件中,或者从 GitHub 项目复制粘贴中间件到你的项目中。
这个 Rack 中间件将拦截 Service Worker 的请求,并通过 webpack-dev-server 提供这些请求。
请注意,“刷新”后的 Service Worker 会被加载,但不会被激活。这是正常的!您可以通过添加以下代码来自动刷新它:
self.addEventListener('install', function(event) {
self.skipWaiting();
});
现在你可以开始编写你的服务工作线程代码,并充分利用热模块重载功能了!
Progressive Rails 应用
您已准备好开始编写您的渐进式 Rails 应用。
我想向您展示在 Rails 中使用 Service Worker 是多么简单,我希望 Webpacker 能尽快提供开箱即用的支持,或许可以借鉴其他方案webpacker-pwa。
现在您已经了解如何编写 Service Worker、发送推送通知以及实现离线页面,您可以开始阅读相关指南来实现您所需的功能。我希望本指南能帮助您开始将您的 Rails 应用升级为渐进式 Rails 应用,并且从现在开始,您会更有信心,因为这其实是一项非常简单的任务。
有关 Webpacker 的其他教程,请查看我之前在 dev.to 和 medium.com 上的博客文章。
感谢Renuo AG一如既往地支持我撰写本文,并感谢他们对 Rails 生态系统所做的所有改进。
不妨了解一下Agreeder,这是一款非常棒的应用程序,它可以通过整理你的偏好来帮助你做决定。
也请了解一下Renuo 的内部奖励系统Gifcoins。这是一个免费且易于使用的工具,方便公司内部员工互相表扬,也是一种表达“谢谢”的全新方式!
文章来源:https://dev.to/coorasse/the-progressive-rails-app-46ma






