前言
前端在写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日志。