PWA系列 - Service Workers 异常处理

前言

前端在写PWA页面时, 经常会遇到ServiceWorker注册失败, 或ServiceWorker执行具体业务逻辑时失败, 但又连不上devtools, 或者连上了而从devtools能获取到的异常信息非常有限。本文详细分析一下ServiceWorker异常处理相关的问题。

线程退出时机

ServiceWorker 规范中提到, Service workers may be started by user agents without an attached document and may be killed by the user agent at nearly any time, 即ServiceWorker线程可能在任意时间被浏览器停止,即使关联的文档还未关闭ServiceWorker线程也有可能已被停止。这种设计主要是为了降低ServiceWorker对资源(比如, 浏览器内存,手机电量)的消耗,但会为前端带来很大的麻烦,俗称“坑”。

一些可能的坑:

(1)ServiceWorker JS里面不能使用全局变量,如果需要全局状态,必须自己进行持久化,比如使用IndexedDB API

(2)ServiceWorker注册过程中出现异常,无法连上devtools,无法从devtools获取异常信息。

那么,ServiceWorker线程一般在什么情况下会被停止呢?

(1)ServiceWorker JS有任何异常,都会导致ServiceWorker线程退出。包括但不限于, JS文件存在语法错误, ServiceWorker安装失败/ 激活失败,ServiceWorker JS执行时出现未捕获的异常。

(2)ServiceWorker 功能事件处理完成,处于空闲状态,ServiceWorker线程会自动退出。

(3)ServiceWorker JS执行时间过长,ServiceWorker线程会自动退出。比如, ServiceWorker JS执行时间超过30秒,或Fetch请求在5分钟内还未完成。

(4)浏览器会周期性检查各个ServiceWorker线程是否可以退出, 一般在启动ServiceWorker线程30秒会检查一次,杀掉空闲超过30秒的ServiceWorker线程。

(5)为了方便开发者调试, Chromium进行了特殊处理, 在连上devtools之后,ServiceWorker线程不会退出。
参考:Keep a serviceworker alive when devtools is attached

异常类型

ServiceWorker线程在启动或执行代码的过程中,一般会有下面几类异常:

(1)ServiceWorker JS 存在语法错误

blink::WorkerThread::initialize

--> blink::WorkerScriptController::evaluate

--> blink::ExecutionContext::reportException  // 此次会抛出异常, Uncaught SyntaxError: Unexpected token function

--> blink::ExecutionContext::dispatchErrorEvent

--> ... ...

--> content::ServiceWorkerDispatcher::OnRegistrationError  // 引起注册失败

--> blink::ScriptPromiseResolver::reject

这种情况,一般在启动WorkerThread的时候,initialize初始化时,会调用ScriptController::evaluate去执行ServiceWorker的JS代码,检查到语法错误时,会引起ServiceWorker注册失败。

(2)ServiceWorker 安装/激活的代码存在异常

 举个例子,下面serviceworker.js的安装/激活函数中,直接调用了self.skipWaiting() / self.clients.claim(), 如果浏览器还未支持这些接口,会出现什么问题呢?

self.addEventListener('install', function(e) {
return self.skipWaiting();
});

self.addEventListener('activate', function(e) {
return self.clients.claim(); 
});

这种情况,一般会在执行安装/激活事件的JS函数时,直接报告异常。

content::ServiceWorkerScriptContext::OnActivateEvent

--> blink::ServiceWorkerGlobalScope::dispatchExtendableEvent

--> blink::V8ScriptRunner::callFunction

--> blink::ExecutionContext::reportException  // 此次会抛出异常, Uncaught TypeError: undefined is not a function

--> blink::ExecutionContext::dispatchErrorEvent

 --> ... ....

--> blink::WaitUntilObserver::didDispatchEvent

--> blink::ServiceWorkerGlobalScopeClientImpl::didHandleActivateEvent

--> content::EmbeddedWorkerContextClient::didHandleActivateEvent

--> content::ServiceWorkerScriptContext::DidHandleActivateEvent   // send IPC

--> content::ServiceWorkerVersion::OnActivateEventFinished  //Activate 失败,WebServiceWorkerEventResultRejected

--> content::ServiceWorkerRegistration::OnActivateEventFinished

--> content::ServiceWorkerVersion::Doom

--> content::ServiceWorkerVersion::StopWorker  // 引起ServiceWorker线程退出

注1:ScriptPromise本身会捕获异常,它仅仅返回Rejected/Fulfilled,并不会再将JS异常往上抛,很多时候前端仅仅能看到Promise Rejected了,但并不清楚是什么原因。

注2:WaitUntilObserver也一样,它也只返回Rejected/Fulfilled,并没有进一步将JS异常往上抛,很多时候前端仅仅能看到WaitUntil Rejected了,但并不清楚是什么原因。

(3)功能事件处理出错, 比如, Fetch ResponseWith出错

举个例子,下面serviceworker.js的fetch事件处理函数中,如果strategies.networkFallbackToCache执行出错了,会出现什么问题呢?

self.addEventListener("fetch", function(e) {
     return e.respondWith(strategies.networkFallbackToCache(e.request))
});

这种情况,respondWith会Rejected,但并不会抛异常, 表现为资源请求失败了,很可能造成页面白屏或者排版显示异常。

v8::internal::FunctionCallbackArguments::Call

--> blink::ScriptFunction::callCallback

--> blink::RespondWithObserver::ThenFunction::call

--> blink::RespondWithObserver::responseWasRejected  // respondWith会Rejected

 注: 这类问题也非常难跟进, 只能是一步一步的修改页面使用devtools等工具进行调试。

(4)普通的文档JS异常, 比如, Uncaught ReferenceError: require is not defined

blink::HTMLScriptRunner::execute

--> blink::ScriptLoader::executeScript

--> blink::ScriptController::executeScriptAndReturnValue

--> blink::V8ScriptRunner::runCompiledScript

--> v8::Script::Run

--> v8::internal::Execution::Call

--> v8::internal::Isolate::ReportPendingMessages

--> v8::internal::MessageHandler::ReportMessage

--> blink::ExecutionContext::reportException / blink::ExecutionContext::dispatchErrorEvent

(5)普通的JS函数调用异常, 比如, Uncaught ReferenceError: require is not defined

blink::ScriptController::callFunction

--> blink::V8ScriptRunner::callFunction

--> v8::Function::Call

--> v8::internal::Execution::Call

--> v8::internal::Isolate::ReportPendingMessages

--> v8::internal::MessageHandler::ReportMessage

--> blink::ExecutionContext::reportException / blink::ExecutionContext::dispatchErrorEvent

异常处理

从上面可以看到, ServiceWorker线程可能会出现各种各样的异常, 那么,我们有没有较统一的解决方案呢?坦诚的说,没有,需要具体问题具体分析。

一些可能的问题跟进思路:

(1)ServiceWorker注册/安装/激活失败

从浏览器开发的角度:

  • 思路一: 不让ServiceWorker线程退出,即使这些过程失败了也不退出。一种处理方式可以是,浏览器检测到文档与devtools连接,即不允许ServiceWorker线程退出。
  • 思路二:增加一些动态调试开关,在开关打开时,尽可能输出较完善的JS异常信息。比如,从上面的堆栈可以看到,很多异常都会走到ExecutionContext::reportException。
  • 思路三:完善ServiceWorker的错误处理流程,每个步骤出错都能输出清晰的日志。

从前端开发者的角度,一般的思路是尽可能连上devtools,如果没办法连上就逐步修改代码一步一步调试了,或者借助浏览器的一些调试日志进行分析。

(2)ServiceWorker功能函数执行异常

一般这种情况下,尽量想办法连上devtools的,在devtools上调试, 或者在业务代码增加一些调试日志。通常高版本的Chrome Devtools会有更加详细的调试信息,浏览器开发可以考虑增加更加完善的控制台或tracing日志。

 

参考文档

Service Workers: an Introduction

Limit Service Worker event execution time

猜你喜欢

转载自yq.aliyun.com/articles/608733