Vue3 + PWA + GQL 项目实践

2,668 阅读4分钟

本文背景

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断网后,我们可以看到从缓存中取出的数据,应用程序还是能正常显示内容

image.png

结尾

至此,我们的pwa程序已经基本构建完成。因为在安装提醒和推送通知这一块还没有深入探索,所以本文只是演示vue + pwa + gql项目的基本实践,但是已经足够让小伙伴了解并入门pwa项目开发了。

点击搜索栏右侧小图标可以安装我们的pwa程序

1704E334-44DF-4075-8D9F-D529E76F23C6.png

image.png