Dec 13

在普通Web中实现一个真正的sleep【Webview同步桥落地方案拓展】

Lrdcq , 2021/12/13 22:41 , 程序 , 閱讀(2736) , Via 本站原創
上次讨论Webview同步桥落地方案(https://lrdcq.com/me/read.php/151.htm)时,核心寻找了在js中表现为同步,但是实际为异步的方案,其中同步xhr这个特性(https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests)虽然在部分浏览器中会显示deprecated,但是并没有被ban掉,因此完全可以使用该特性在普通web中完成阻塞式API调用。这里尝试用该方案封装一个sleep函数,既不是for循环算时间这样消耗算力的sleep,也不是await这样的假同步sleep了。

该方案核心逻辑是依托于同步xhr发送api请求,通过service worker拦截请求,并且在worker中完成任务后返回,在此处worker需要完成的即sleep行为即setTimeout。worker的拦截逻辑如下:
//serviceworker.js
//service配置
const ServerConfig = {
  HOST_KEY: 'local_service_worker_command'
};

//api实现
const APIs = {
  sleep: function(params, callback) {
    let timeout = params.timeout;
    setTimeout(function(){
      callback(true, {});
    }, timeout);
  }
}

//注册拦截器
self.addEventListener('fetch', function(event) {
  const requestUrl = new URL(event.request.url);
  if (('' + event.request.url).match(ServerConfig.HOST_KEY)) {
    const querys = requestUrl.searchParams;
    const method = querys.get('method');
    const params = JSON.parse(querys.get('params'));

    if (APIs[method]) {
      event.respondWith(new Promise((resolve, reject) => {
        APIs[method](params, function(status, results) {
          var responseInit = {
            status: status ? 200 : 500,
            statusText: 'OK',
            headers: {
              'Content-Type': 'application/json'
            }
          };
          resolve(new Response(JSON.stringify(results), responseInit));
        });
      }));
    }
  }
});

- service worker拦截的所有request都会通过fetch事件的FetchEvent返回处理。但是拦截的请求的范围是固定的——既当前域下的所有请求。因此对于我们需要特殊url做拦截的情况,不能改变host,也最好不要改变path,那拦截标记只能在query上或者header中下文章了。

- event.respondWith必须在事件处理过程中同步调用,否则拦截器会认为并没有对request进行任何处理而进入正常网络请求逻辑。不过event.respondWith的入参可以是response对象或者promise,可以在promise中完成异步任务并且后续resolve。

- 返回的response需要保证基础的请求结构,如上面写的statuscode,statustext,content-type,否则浏览器会认为是无法解析的response而抛出异常。

- service worker注册很简单,代码如下:
//sleep.js  
//注册worker
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('serviceworker.js', {scope: './'}).then(function(registration) {
    }).catch(function(error) {
      console.warn(error);
    });
  } else {
    console.warn('不支持serviceWorker');
  }

完成Service Worker准备工作后便是主线程使用了,主线程的sleep方法如下:
//sleep.js  
  //公共方法:syncRequest
  const syncRequest = function(method, params) {
    let request = new XMLHttpRequest();
    request.open('GET', './?local_service_worker_command=1&method=sleep&params=' + JSON.stringify(params), false);
    request.send(null);
    
    if (request.status === 200) {
      try {
        return JSON.parse(request.responseText);
      } catch(e) {
        return null;
      }
    } else {
      return null;
    }
  }

  //暴露方法sleep
  window.sleep = function (timeout) {
    syncRequest('sleep', {'timeout': timeout});
  }

//使用即sleep(5000)这样调用即可

- 这里封装了公共的syncRequest方法来将函数调用转换为同步xhr并返回结果,参数与标记均是通过query传输。

- 这里sleep的return没意义因此没有return,实际上作为同步方法,整体设计应该从responseText回传数据并且反序列化为对象。

- 整体流程如下:
點擊在新視窗中瀏覽此圖片

【DEMO】整体完成demo:https://lrdcq.com/test/jssleep/。源码未混淆可以直接查看。这个demo实际运行起来目前也有几个问题:

- 实际上目前只有Firefox与Edge上有效运行,Chrome仍然不支持对同步xhr进行serviceworker拦截,这可能是一个未完成的feature(毕竟不合理,并且W3C Web Platform Tests也有这个测试用例https://github.com/web-platform-tests/wpt/blob/master/service-workers/service-worker/fetch-request-xhr-sync.https.html),但是自从2019年chrome上线service worker以来这个问题一直是open状态。

- Service Worker只能在https的网站上运行,务必要注意。

(Firefox中的运行表现)
點擊在新視窗中瀏覽此圖片
logo