218

clipboard.png

前置知识储备

PWA ( Progressive Web Apps )是网络上谈论最多的技术变革之一,在IT界从业者中获得了前所未有的势头。如果你是为web构建的,我相信PWA是添加到你的工作词汇中的最新“流行语”。这并不奇怪,因为PWA已经实现了在手机上安装web应用程序的遥不可及的梦想。

关于PWA的建设和它的优势,已经有了很多的焦点和“极客之谈”。大多数介绍PWA的尝试,特别是对新手来说,似乎都是行话,或者代码太多,可能会使他们不敢迈出第一步。在这篇文章中,我想要以一个简单的案例来教会各位如何起步。

关于PWA的概念以及前世今生我这边不会过多赘述,网络上有很多更加专业的文章供你学习,这篇文章只负责教会你如何使用它。

比起用一篇文章打消你的所有关于PWA的困惑来说我更希望你能简单了解概念之后将我的案例敲打一遍后再回过头去深入了解PWA。

什么是PWA
下一代 Web 应用模型 —— Progressive Web App
PWA官网

起步

// 创建一个简单的项目

mkdir pwa-project
cd pwa-project
touch index.html
touch app.js
touch style.css

相信各位都是使用chrome最新版的高端技术人才,这里为了省略webpack一些繁琐的配置我们直接编写es6代码运行在chrome即可

编写代码

假设我们有一个新闻站点,需要展示标题,图片,文章,并且根据不同的来源切换内容
根据这个要求我们可以编写以下代码
// index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>News</title>
    <link rel="stylesheet" href="./style.css">
</head>
<body>
    <header>
        <h1>News</h1>
        <select id="sourceSelector"></select>
    </header>

    <main></main>

    <script src="./app.js"></script>
</body>
</html>
// style.css

html {
    line-height: 1.15;
    /* 1 */
    -webkit-text-size-adjust: 100%;
    /* 2 */
}

body {
    margin: 0;
}

h1 {
    font-size: 2em;
    margin: 0.67em 0;
}

a {
    background-color: transparent;
    text-decoration: none;
}

button,
select {
    text-transform: none;
}

这里为了模拟真实用户数据我们可以去👇这里申请apikey获取一些真实数据
各位也可以直接copy代码来学习,代码逻辑很简单这里不做讲解。

// app.js

const apiKey = 'fa35a325ddfa4c4798102ebb76809bbb';
const main = document.querySelector('main');
const sourceSelector = document.querySelector('#sourceSelector');
const defaultSource = 'techcrunch'

// 页面加载后执行逻辑
window.addEventListener('load', async e => {
    updateNews();
    await updateSources();
    sourceSelector.value = defaultSource;

    sourceSelector.addEventListener('change', e => {
        updateNews(e.target.value);
    });
    
    // 判断浏览器是否支持serviceWorker
    if ('serviceWorker' in navigator) {
        try {
            // 尝试注册serviceWorker到sw.js文件中
            navigator.serviceWorker.register('sw.js');
            console.log('SW registered');
        } catch (error) {
            console.log('SW reg failed');
        }
    }
});

// 获取新闻来源
async function updateSources() {
    const res = await fetch(`https://newsapi.org/v2/sources?apiKey=${apiKey}`);
    const json = await res.json();

    sourceSelector.innerHTML = json.sources
        .map(src => `<option value="${src.id}">${src.name}</option>`)
        .join('\n');
}

// 根据来源获取新闻数据
async function updateNews(source = defaultSource) {
    const res = await fetch(`https://newsapi.org/v2/top-headlines?sources=${source}&apiKey=${apiKey}`);
    const json = await res.json();

    main.innerHTML = json.articles.map(createArticle).join('\n');
}

// 创建文章
function createArticle(article) {
    return `
        <div class="article">
            <a href="${article.url}">
                <h2>${article.title}</h2>
            </a>
            <img src="${article.urlToImage}" />
            <p>${article.description}</p>
        </div>
    `;
}

启动

现在启动我们的项目
执行npx http-server打开我们的localhost:8080端口(端口号根据具体情况而定)

我们点开控制台看看是不是报错了?

clipboard.png

在点开这里我们可以发现找不到sw.js这个文件,因为我们根本没有嘛!😂

clipboard.png

既然没有那我们写一个不就好了-_-!!

编写sw.js

首先我们什么都不写,直接创建sw.js文件到项目中就不会报错了,但是什么都没写就意味着你什么都没有做。
那我们到底可以用这个文件做什么事呢?

缓存我们的静态资源文件

// sw.js

const staticAssets = [
    './',
    './style.css',
    './app.js'
];

// sw.js首次被注册时候触发
self.addEventListener('install', async event => {
    const cache = await caches.open('news-static');
    cache.addAll(staticAssets);
})

现在我们再次刷新页面(记得清理缓存哈)就可以看到我们的静态资源都被缓存起来了。

clipboard.png

什么?没有缓存起来?
那你肯定是没有告诉浏览器刷新时候要更新你的sw.js文件。可以勾选这里,然后再次刷新。

clipboard.png

但是缓存是缓存起来了,我们要是不拿来用那也没啥卵用。
所以我们要拦截请求并告诉浏览器我们要使用这些缓存。

// sw.js (添加以下代码)

// sw监听到fetch事件时候触发
self.addEventListener('fetch', event => {
    const req = event.request;

    event.respondWith(cacheFirst(req));
});

// 使用浏览器缓存
async function cacheFirst(req) {
    const cachedResponse = await caches.match(req);
    return cachedResponse || fetch(req);
}

编写完这些之后我们可以先勾选offline按钮,之后刷新页面,发现我们的站点依然有数据。

我们点开network看看请求。

clipboard.png

我们发现我们拦截了http请求并且将浏览器的缓存数据返回回去了!

是不是很神奇😊!

到这一步,相信你已经见识到了PWA的一些能力了。

正确使用缓存

在上面的代码中其实是有一些问题的,我们在离线状态下使用缓存是ok的,可是如果我们处于联网状态情况下,我们还需要返回缓存吗?当然不需要,不仅不需要,我们还应该用服务器的最新数据更新我们的缓存。

所以我们要根据网络优先的原则修改下sw逻辑。

self.addEventListener('fetch', event => {
    const req = event.request;
    const url = new URL(req.url);
    
    // 当本地开发时候可以这么配置
    if (url.origin === location.origin) {
        event.respondWith(cacheFirst(req));
    } else if ((req.url.indexOf('http') !== -1)) {
        // chrome的https协议限制,接口必须满足https
        event.respondWith(networkFirst(req));
    }
});

// 缓存优先
async function cacheFirst(req) {
    const cachedResponse = await caches.match(req);
    return cachedResponse || fetch(req);
}

// 网络优先
async function networkFirst(req) {
    // 将请求到的数据缓存在id为news-dynamic中
    const cache = await caches.open('news-dynamic');

    try {
        const res = await fetch(req); // 获取数据
        cache.put(req, res.clone()); // 更新缓存
        return res;
    } catch (error) {
        return await cache.match(req); // 报错则使用缓存
    }
}

至此我们的案例基本上完成了,但是还是可以再次优化一下。

假设用户还没有查看过我们的站点页面其他内容,也就是说我们的缓存不完整,这个时候可以提供一个mock数据提示用户等待可以联网的时候再来查看当前页面。(具体文件可以从我的github仓库中获取)

PWA就这么点能力吗?

其实PWA还提供给我们将web站点以app图标形式放置在桌面以及移动手机中。

这里配置与编写浏览器插件过程有点类似。

我们需要一个manifest.json文件,并在index.html中引入。

manifest.json可以在这个站点中生成。

// html文件需要引入一行代码

<link rel="manifest" href="manifest.json">

配置成功后我们可以在控制台中查看

clipboard.png

有了这个文件和相应图标后我们就可以添加至桌面端了,是不是很酷炫~

未完待续

截止项目代码可在我的仓库中获取。(能否给个小星星鼓励下呢😂)

后续会补充在具体项目中应该怎么集成pwa,这篇文章只分析了pwa的核心代码~


Kyrielin
1.1k 声望330 粉丝

Coding is amazing!