Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于react与typescript开发pwa #24

Open
HolyZheng opened this issue Oct 24, 2018 · 1 comment
Open

基于react与typescript开发pwa #24

HolyZheng opened this issue Oct 24, 2018 · 1 comment

Comments

@HolyZheng
Copy link
Owner

HolyZheng commented Oct 24, 2018

titleimage

上一篇“记录一次基于vue、typescript、pwa的项目由开发到部署”,发布后,忙于秋招的我,终于有时间来写这篇文章。最近秋招也挺顺利,拿到了网易广州岗的offer,对目前想留在广州发展的我来说真是太合适不过了。在最近的反思中,楼主我也意识到了自己存在过于急功近利的毛病,前端是一个更新迭代很快的行业,最忌讳的就是浮躁的心态,所以楼主决定要好好调整自己,要有工匠精神和延时满足的意识。以后的文章也要好好调整,尽可能的写细写好。

下面进入正题,本篇文章介绍如何基于目前的create-react-app(目前的项目cra版本为2.0.3)利用workbox开发一款pwa,并将其发布到自己的服务器。

先来说一下项目,这次的项目和上一篇基于vue的项目是同一个项目,可以说是用react的重构,用途是用来浏览学校心理学院部分实验信息,因为楼主用vue较多,所以这个项目也是react的练手项目,为了多实践,项目中还用到了react-redux去进行状态管理(项目引入redux的目的只是为了实践),react-router进行路由控制。看一下效果:
bereactshow

可以看到,我们从手机桌面的一级入口,在断网的条件下,正常的打开了app,而且带有过渡动画,并正常的访问了实验数据,这就是pwa,拥有原生app般的体验,又拥有web的轻便。下面我们来看看,如何基于create-react-app2与workbox如何实现这个项目。

cra2 给我们带来了什么pwa特性?

note:部分链接需要梯子

我们的项目是基于 create-react-app 2 ,cra2支持了pwa,可以让我们可选的将我们的app 升级为 pwa。
关于cra2添加的新特性,可以在这里看what's new about cra2,首先我们利用cra2生成一个项目,:

npx create-react-app my-app

可以看到在src文件目录下有一个serviceWorker.js文件,这个文件看起来很复杂,其实就是检查一下环境变量是否为“production”,且当前浏览器是否兼容service worker。
再来看一下我们的 index.js 文件,可以看到最后面有一句:

serviceWorker.unregister();

如果我们想要使用cra2提供的pwa特性的话,我们需要将 serviceWorker.unregister() 改为 serviceWorker.register(),然后我们打包我们的项目,在打包得到的build文件夹下我们可以看到生成了一个server-worker.js文件 和 preche-manifest.xxxxxxxxx.js 和一个manifest.json。
其中的server-worker.js中规定了我们如何缓存我们的资源,preche-manifest 中列出了我们的静态资源
利用上一篇关于vue-cli 和pwa 中提到的web server工具 web-server-for-chrome

web server
为我们打包得到的build文件夹下的代码建一个服务并访问。
然后打开chrome控制台的Application查看,发现它帮助我们缓存了我们的静态文件。

react-default-pwa

我们把网断开,可以发现,我们的app依旧能正常打开。这就是目前的cra2(2.0.3)给我们提供的默认的pwa特性。我们可以选择是否去缓存我们的app shell,使得它可以在离线时正常的打开。

限制性

这足够了吗?很明显是不够的。在实际情况中,除了缓存我们的app shell,我们往往需要动态的对我们的路由和请求的数据进行缓存,这本来是一件很正常的事情,也就是说,我们需要去定制自己的servive worker去实现自己想要的功能。

但是在目前的cra2中,并没有提供这么一个方式让我们去定制自己的service-worker,除非你eject我们的项目,将所以的配置文件暴露出来,但是eject是一个不可逆的过程,将我们的配置文件暴露出来也是一个不优雅的做法。所以我们需要另开途径来实现我们的需求。

为什么 create-react-app 没提供定制service worker 的方法?

其实并不是cra2不提供相应的定制方法,目前cra2 也在准备实现相应的功能,我们可以看一下下面几个issue或request。

  1. Custom ServiceWorker config #2237 开发者huygn提出在create-react-app支持pwa后是否能够让开发者自定义相关配置

issueCustomPwa

但是react.js的工作人员gaearon回应短期内没有此打算,未来或许会实现该想法,并关闭了此issue。

responseForCustomPwa

  1. Import scripts in Service Worker #2714 开发者piotr-cz提出了一个方案,给create-react-app提了一个request,为当时create-react-app所使用的SWPrecacheWebpackPlugin插件提供一个importScripts选项,可以让我们在项目的public目录下定制自己的service worker

requestForCustomPwa

但是最终还是被create-react-app的合作作者Timer关闭了这个request,因为有开发者提出了更合理的实现方式
responseForRequest

  1. Workbox service worker #4169 开发者davejm 提出了更合理的方案,并且被采纳。davejm 提议将w-precache-webpack-plugin换成Workbox webpack plugin,并提供对应的配置文件。

requestForWorkbox

从这些资料中,可以看到,在将来,我们或许就可以像在vue-cli3.0中一样定制自己service worker了,配置的方式便是在根目录下建立 cra.config.js文件,然后写入类似下面的配置代码:

module.exports = {
  workbox: {
    method: 'inject',
    config: {
      swSrc: 'src/my-custom-service-worker.js'
    }
  }
}

这里不得不感叹一下react社区的力量。

现在怎么办?

既然目前还没有提供定制service worker的方法,那么我们应该怎么做呢?关于如何实现这个app:

  1. 如何在现阶段自定义自己的service worker。
  2. 如何换成我们的静态文件和请求的数据
  3. 如何部署我们的项目到自己的服务器上

楼主打算在下一篇文章中给大家介绍,因为楼主发现与其长篇大论把所有内容都放在同一篇文章,不如分开几篇细细的讲要有效果。

感兴趣可以扫描下面二维码体验项目:

note:

  1. 建议用uc浏览器打开,因为uc浏览器对pwa的支持较好。
  2. "添加到桌面的提示" 需要短时间多次进入web app 才会触发

qrcode

项目地址:browseExpByReact
如果感兴趣,可以对比着基于vue的实现来看: browseExpByVue

@HolyZheng
Copy link
Owner Author

HolyZheng commented Oct 30, 2018

上一篇文章中,我们了解到了create-react-app 给我们提供了哪些pwa支持,也了解到了有哪些不足。虽然create-react-app会帮我们自动生成一个service-worker.js 去缓存我们的app shell,但是并没有提供让开发者定制service worker的方法,除非我们eject项目,这篇文章继续往下讲,把在这个项目中学到的东西分享给大家。

项目回顾

这是一个移动端的pwa应用,使用react,typescript,react-redux,react-router,workbox 基于create-react-app 开发。可以添加到主屏幕,可以断网条件下正常打开和访问数据。项目地址:browseExpbyReact

addToScreen

bereactshow

使用typescript

typescript是JavaScript的超级,一方面在typescript中我们可以使用最新的特性,另一方面typescript给我们带来了类型系统,可以让我们写出健壮的代码,避免一些潜在的运行时错误。在create-react-app中使用typescript,官网推荐我们使用的是create-react-app的ts版本,他会帮你配置好typescript的相关配置,并使用react-script-ts代替react-script来驱动项目。但是这个版本的更新会稍稍滞后于原版,而且也不利于我们扩展脚手架的配置,所以这里不推荐使用。我们使用 react-app-rewired 来进行配置。

react-app-rewired

在create-react-app中修改默认配置有两种常用的方法,

  1. 一种是 eject 项目,eject会把我们的脚手架中的配置暴露出来,然后我们就可以去修改了,但是这是一个不可逆的过程,而且讲配置暴露出来也是一个不优雅的做法,所以不推荐。
  2. 第二种就是利用 react-app-rewired 去修改我们的配置,他可以让我们在不eject项目的前提下修改我们的配置。比如配置typescript,我们可以找到对应的插件 react-app-rewire-typescript 进行配置。具体可参考本项目

利用workbox 定制自己的service worker

这里到了本文的重点:如何在create-react-app中定制自己的service-worker.js。目前的cra引用了Workbox webpack plugin 代替了先前的 sw-precache-webpack-plugin。我们可以借助 react-app-rewired
去改写默认的Workbox webpack plugin 配置。主要步骤:

  1. 在 react-app-rewired 的配置文件 config.overrides.js 中修改 Workbox webpack plugin的配置
  2. 在public文件目录下建立自己的service-worker配置文件

首先在 config.overrides.js 中配置,替换默认的workbox-webpack-plugin配置:

/* config-overrides.js */
// typescript的配置插件
const rewireTypescript = require('react-app-rewire-typescript');
const workboxPlugin = require('workbox-webpack-plugin')
const path = require('path')

module.exports = {
  webpack: function (config, env) {
     // typescript的配置插件
    config = rewireTypescript(config, env);
    if (env === 'production') {
      // 在 ‘production’ 模式下加入自己的配置
      const workboxConfigProd = {
        swSrc: path.join(__dirname, 'public', 'cus-service-worker.js'),
        swDest: 'cus-service-worker.js',
        importWorkboxFrom: 'disabled'
      }
      // 删除默认的WorkboxWebpackPlugin配置
      config = removePreWorkboxWebpackPluginConfig(config)
     // 加入我们的配置
      config.plugins.push(new workboxPlugin.InjectManifest(workboxConfigProd))
    }
    return config
  }
}
// 此函数用来找出 默认配置中的 WorkboxWebpackPlugin, 并把它删除
function removePreWorkboxWebpackPluginConfig (config) {
  const preWorkboxPluginIndex = config.plugins.findIndex((element) => {
    return Object.getPrototypeOf(element).constructor.name === 'GenerateSW'
  })
  if (preWorkboxPluginIndex !== -1) {
    config.plugins.splice(preWorkboxPluginIndex, 1)
  }
  return config
}

这部分的配置大概意思就是,当环境为生成环境时,找出webpack中关于workbox-webpack-plugin的配置,把它删掉,然后用自己的配置替代它。

这里解释一下 removePreWorkboxWebpackPluginConfig 这个函数。我们可以自己用create-react-app新建一个无用的项目,然后eject它,那么我们可以在暴露出来的config文件夹下的 webpack.config.prod.js 中看到关于 workbox-webpack-plugin 的配置

new WorkboxWebpackPlugin.GenerateSW({
      clientsClaim: true,
      exclude: [/\.map$/, /asset-manifest\.json$/],
      importWorkboxFrom: 'cdn',
      navigateFallback: publicUrl + '/index.html',
      navigateFallbackBlacklist: [
        // Exclude URLs starting with /_, as they're likely an API call
        new RegExp('^/_'),
        // Exclude URLs containing a dot, as they're likely a resource in
        // public/ and not a SPA route
        new RegExp('/[^/]+\\.[^/]+$'),
      ],
    }),

所以我们可以通过下面这段代码找到这段配置的位置:

// 对plugins数组调用findIndex方法,找到构造函数的name属性为‘GenerateSW’的成员
const preWorkboxPluginIndex = config.plugins.findIndex((element) => {
    return Object.getPrototypeOf(element).constructor.name === 'GenerateSW'
  })
// 删除这个成员
  if (preWorkboxPluginIndex !== -1) {
    config.plugins.splice(preWorkboxPluginIndex, 1)
  }

替换掉workbox-webpack-plugin的配置后,根据自己的配置在public目录下新建cus-service-worker.js文件,这个文件会代替默认生成的service-worker.js文件,我们就可以通过配置cus-service-worker.js来定制自己的pwa配置了,而且cus-service-worker.js 里的内容也是有讲究的,以本项目为例:

// 引入workbox全局变量
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js');

if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);
} else {
  console.log(`Boo! Workbox didn't load 😬`);
}
// set the prefix and suffix of our sw's name
workbox.core.setCacheNameDetails({
  prefix: 'browse-exp',
  suffix: 'v1.0.0',
});
// have our sw update and control a web page as soon as possible.
workbox.skipWaiting();
workbox.clientsClaim();

// 将静态资源进行预缓存
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

// 定制自己的需求
// cache our data, and use networkFirst strategy.
workbox.routing.registerRoute(
  new RegExp('.*experiments\?.*'), 
  workbox.strategies.networkFirst()
);
workbox.routing.registerRoute(
  new RegExp('.*experiments/\\d'),
  workbox.strategies.networkFirst()  
)
workbox.routing.registerRoute(
  new RegExp('.*experiment_types.*'),
  workbox.strategies.networkFirst()
)

首先通过importScripts 引入workbox全局变量。在打包的时候,脚手架会为我们生成一个 precache-manifest列表,里面会列举一系列的静态文件,我们可以通过 self.__precacheManifest 拿到这个列表,
所以我们需要通过一下语句预缓存这些静态资源:

self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

然后就是为了尽快的让我们的service worker控制页面,我们可以在开头加入一下语句:

// 跳过等待
workbox.skipWaiting();
// 控制客户端
workbox.clientsClaim();

剩下的部分自己就可以按自己的需求进行发挥了,像要什么功能就配置什么功能,这里的话我为自己获取数据的路由进行了缓存,采用的是 networkFirst 策略,什么是networkFirst策略呢?就是首先会进行网络请求,如果失败的话再使用缓存中的数据。

networkFirst

当我们打包项目的时候,就会发现再build文件下,会生成一个cus-service-worker.js文件,并且再开头多了一句:

importScripts("/precache-manifest.cd8115bc0ff644d6d74bec08ffcbdeb4.js");

这就是我们可以通过 self.__precacheManifest 拿到预缓存列表的原因。

到目前为止:我们已经可以定制自己的service-worker.js了。

manifest.json

manifest.json可以让我们的web app添加到桌面,再create-react-app中配置manifest非常简单,直接再public目录下的manifest.json配置就可以了,关于什么么配置项,可以到这里谷歌官网教程查看,另外manifest.json的配置不会马上生效,需要在https协议下,多次进入该网页的时候才会弹出添加到桌面的提示。

总结:

  1. 利用 react-app-rewired 改写我们的配置
  2. 在 config.overrides.js 中替换默认的 WorkboxWebpackPlugin 的配置
  3. 在 public 目录下编写自己的pwa配置

到这里我们可以在create-react-app生成的脚手架中定制自己的pwa配置了,在下一篇文章中,我会继续讲解:

  1. 如何部署将该项目部署到nginx服务器上。
  2. 为它配置证书,让它运行在https协议上。
  3. 体验该pwa项目。

感兴趣的同学可以扫描下面二维码体验项目:

note:

  1. 建议用uc浏览器打开,因为uc浏览器对pwa的支持较好。
  2. "添加到桌面的提示" 需要短时间多次进入web app 才会触发

qrcode

项目地址:browseExpByReact
如果感兴趣,可以对比着基于vue的实现来看: browseExpByVue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant