本文背景
Vue和GQL都是我们的老熟人这里就不多说了,PWA是Web 应用程序的一种行为设定标准,当用户在 Chrome 中访问网页并且应用程序满足所有条件时,开发人员可以提示用户在其桌面上安装应用程序。这里我们不去深入介绍 PWA,想详细了解建议阅读progressive-web-apps,本文主要是通过项目实践来介绍相关的技术细节。
下面我们将展示:
- 使用 Vue CLI 构建PWA项目
- 使用 Vue Apollo 库将 Apollo Client 集成到 Vue 3 应用程序中
- 使用 Composition API 提高了 Vue 代码的可读性
- 使用 自定义的service-worker.js 管理请求
- 添加应用程序到桌面,可离线使用
构建项目
1、要创建一个新项目,请运行:
vue create my-app
然后根据系统提示选择一个预设,或者选择“手动选择功能”来选择你需要的功能。
按照系统提示,根据自己的需要添加项目feature,初始化完成后cd my-app、yarn serve
即可运行项目。
提示:service-worker只在生产环境起作用,要启用service-worker服务,我们需要运行yarn build构建生产项目,然后使用node创建本地端口来演示项目
2、添加 Apollo(使用vue-cli插件,或者手动安装)
vue add apollo
构建apollo client
import { ApolloClient, ApolloLink, InMemoryCache, from, HttpLink } from '@apollo/client/core';
const token = process.env.VUE_APP_GITHUB_ACCESS_TOKEN;
// HTTP connection to the API
const additiveLink = from([
new ApolloLink((operation, forward) => {
operation.setContext(({ headers }) => ({
headers: {
...headers,
authorization: token ? `Bearer ${token}` : null
}
}));
return forward(operation); // Go to the next link in the chain. Similar to `next` in Express.js middleware.
}),
new HttpLink({ uri: 'https://xxx.com/gql' })
]);
export const apolloClient = new ApolloClient({
link: additiveLink,
cache: new InMemoryCache()
});
// file: main.ts
// 在app中集成apollo
const app = createApp({
setup() {
provide(ApolloClients, {
default: apolloClient
});
},
render: () => h(App)
});
添加gql document
import { gql } from '@apollo/client/core';
export const MOVIE_FRAGMENT = gql`
fragment movieFragment on Movie {
id
name
introduction
cover
}
`;
export const MOVIES_QUERY = gql`
${MOVIE_FRAGMENT}
query moviesQuery($type: String!, $page: Int) {
movies(type: $type, page: $page) {
paginatorInfo {
currentPage
hasMorePages
}
data {
...movieFragment
series {
name
url
}
}
}
}
`;
在vue component中使用gql
<template>
<div class="category-movie">
<ul class="flex-grid-movies">
<li class="item-render" v-for="movie in movies" :key="movie.id">
<VideoItem :movie="movie" />
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, toRefs } from 'vue';
import { useQuery, useResult } from '@vue/apollo-composable';
import { MOVIES_QUERY } from '@/graphql';
import { MoviesQuery, MoviesQueryVariables } from '@/graphql/schema';
import VideoItem from './VideoItem.vue';
export default defineComponent({
components: {
VideoItem
},
props: {
options: {
type: Object as () => MoviesQueryVariables,
default: () => {
return {
type: '历史',
page: 1
};
}
}
},
setup(props: { options: Record<string, any> }) {
const { options } = toRefs(props);
const { result, loading, error } = useQuery<{
movies: MoviesQuery;
}>(MOVIES_QUERY, options);
const movies = useResult(result, [], (data: any) => data?.movies?.data);
return {
loading,
error,
movies
};
}
});
</script>
我们以后可能要在setup中做更多的事情,为了阅读性和维护性,可以添加自定义gql hook,简化setup中的代码
import { ref, onMounted, watch } from 'vue';
import { useQuery, useResult } from '@vue/apollo-composable';
import { MOVIES_QUERY } from '@/graphql';
import { MoviesQuery, MoviesQueryVariables } from '@/graphql/schema';
export default function useMoviesQuery(options: MoviesQueryVariables) {
const { result, loading, error } = useQuery<{
movies: MoviesQuery;
}>(MOVIES_QUERY, options);
const movies = useResult(result, [], (data: any) => data?.movies?.data);
return {
loading,
error,
movies
};
}
简化后在vue component中使用gql hook
<template>
<div class="category-movie">
<ul class="flex-grid-movies">
<li class="item-render" v-for="movie in movies" :key="movie.id">
<VideoItem :movie="movie" />
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, toRefs } from 'vue';
import { useQuery, useResult } from '@vue/apollo-composable';
import { useMoviesQuery } from '@/graphql';
import { MoviesQueryVariables } from '@/graphql/schema';
import VideoItem from './VideoItem.vue';
export default defineComponent({
components: {
VideoItem
},
props: {
options: {
type: Object as () => MoviesQueryVariables,
default: () => {
return {
type: '历史',
page: 1
};
}
}
},
setup(props: { options: Record<string, any> }) {
const { options } = toRefs(props);
const { loading, error, movies } = useMoviesQuery(options);
return {
loading,
error,
movies
};
}
});
</script>
默认的Service Worker
PWA必须遵循以下条件
- 必须通过 HTTPS 提供服务
- 必须安装一个 Service Worker 并且至少有一个 fetch 处理程序(确保应用程序能够离线使用)
- 必须提供有效的 manifest.json
- 页面必须是响应式的
我们使用vue-cli选择pwa feature来构建vue项目。这将在/src
目录中为你提供一个额外的文件:registerServiceWorker.js
。然后将该文件导入main.js
。此文件注册service-worker.js
通过运行yarn build
生成的默认文件。该文件由 Workbox 插件生成,集成在 Webpack 配置中。
这个自动生成的service-worker.js
文件如下所示:
该registerServiceWorker.js
文件如下所示:
默认设置使注册 Service Worker 和检测浏览器功能的过程更容易。它提供了许多我们稍后可以使用的事件注册。一些事件带有一个注册对象作为第一个参数。
此默认设置将提供:
- 所有构建文件的预缓存
- 离线加载应用程序的能力(一旦它被缓存)
- 可以在应用程序中做出反应的许多 Service Worker 注册事件
对于更复杂的设置,包括缓存gql的post请求、安装提醒、推送消息等,就需要一个自定义的service-worker
了。
自定义Service Worker
要创建我们自己的service-worker.js
文件,我们首先需要设置一些 Workbox 选项。这需要在vue.config.js
文件中完成,我们在项目的根目录中创建该文件,并使用以下配置选项填充它。
// vue.config.js
module.exports = {
pwa: {
// configure the workbox plugin
workboxPluginMode: 'InjectManifest',
workboxOptions: {
// swSrc is required in InjectManifest mode.
swSrc: 'src/service-worker.js',
importWorkboxFrom: 'disabled'
}
}
};
我们使用自定义的service-worker.js
实现了缓存gql请求,这里我们用idb-keyval(indexDB的library)来保存请求的键、值,要查看完整code请移步至评论区。
importScripts('./js/workbox-sw.js');
importScripts('./js/idb-keyval.js');
importScripts('./js/crypto-js.min.js');
const DATABASE_NAME = 'app-db-v1';
const STORE_NAME = 'app-post';
const dbStore = new idbKeyval.createStore(DATABASE_NAME, STORE_NAME);
if (workbox) {
console.log(`Yay! Workbox is loaded 🎉`);
} else {
console.log(`Boo! Workbox didn't load 😬`);
}
// Workbox with custom handler to use IndexedDB for cache.
workbox.routing.registerRoute(
new RegExp('/gql'),
// Uncomment below to see the error thrown from Cache Storage API.
//workbox.strategies.staleWhileRevalidate(),
async ({ event }) => {
return staleWhileRevalidate(event);
},
'POST'
);
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Return cached response when possible, and fetch new results from server in
// the background and update the cache.
self.addEventListener('fetch', async event => {
if (event.request.method === 'POST') {
console.log('fetch', event.request);
event.respondWith(staleWhileRevalidate(event));
}
// TODO: Handles other types of requests.
});
项目演示
1、构建生产项目
$ yarn build
2、创建本地端口
$ yarn add express --dev
添加server.js文件
var express = require('express');
var app = express();
const hostname = 'localhost';
const port = 3001;
app.use(express.static('./dist'));
app.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}`);
});
3、启动服务
$ node server
打开Chrome控制台查看Application(如下图),代表service-worker
正常运行
点击Storage/indexDB,可以看到已经缓存的gql请求
给Chrome断网后,我们可以看到从缓存中取出的数据,应用程序还是能正常显示内容
结尾
至此,我们的pwa程序已经基本构建完成。因为在安装提醒和推送通知这一块还没有深入探索,所以本文只是演示vue + pwa + gql项目的基本实践,但是已经足够让小伙伴了解并入门pwa项目开发了。
点击搜索栏右侧小图标可以安装我们的pwa程序