SharedWorker源码解析

最近工作需要了解WebWorker,根据RTFSC原则,空下来看一下Chrome的SharedWorker源码。

ShareWorker是共用Worker,Chrome的实现调用new SharedWorker会分配独立进程,不管调用多少次都只有这一个实例。除了进程管理,SharedWorker还需要通信,Chrome中SharedWorker通过MessagePort通信。

SharedWorker.cpp

// 构造函数,
inline SharedWorker::SharedWorker(ExecutionContext* context)
// 初始化父类AbstrctWorker,
// AbstractWorker继承自ActiveDOMObject,因为要在JS中使用,所以Worker需要是一个DOM Object。暂时不关注ActiveDOMObject的实现。
// AbstractWorker有一个方法,resolveURL,对传进来的url进行有效性和安全性检查。
: AbstractWorker(context)
    //初始化连接标志m_isBeingConnected。
      , m_isBeingConnected(false)
  {
  }

//接下来是SharedWorker真正创建的函数,create:
  PassRefPtrWillBeRawPtr<SharedWorker> SharedWorker::create(ExecutionContext* context, const String& url, const String& name, ExceptionState& exceptionState)
  {
    // SharedWorker是独立进程的
    ASSERT(isMainThread());
    // 因为目前还不支持worker与worker之间通信,context必须是JS中的document
    ASSERT_WITH_SECURITY_IMPLICATION(context->isDocument());

    // 引用计数,SharedWorker会被多次引用,所以需要引用计数,以便最后一个引用退出时析构。
    UseCounter::count(context, UseCounter::SharedWorkerStart);

    // 使用构造函数构造一个worker实例。
    RefPtrWillBeRawPtr<SharedWorker> worker = adoptRefWillBeNoop(new SharedWorker(context));// SharedWorker的构造函数并没有做太多的事情,初始化父类AbstractWorker,以及一个标志,是否连接

    // 通信管道,MessageChannel放到后面看。
    MessageChannel* channel = MessageChannel::create(context);
    // port是从channel中拿到的,管道中用来通信的
    worker->m_port = channel->port1();
    OwnPtr<WebMessagePortChannel> remotePort = channel->port2()->disentangle();
    ASSERT(remotePort);

    // 这个看名字就知道什么意思了
    worker->suspendIfNeeded();

    // 这里chrome已经注释了
    // We don't currently support nested workers, so workers can only be created from documents.
    Document* document = toDocument(context);
    // 判断下能不能连上
    if (!document->securityOrigin()->canAccessSharedWorkers()) {
      exceptionState.throwSecurityError("Access to shared workers is denied to origin '" + document->securityOrigin()->toString() + "'.");
      return nullptr;
    }

    // 判断url的安全性
    KURL scriptURL = worker->resolveURL(url, exceptionState);
    if (scriptURL.isEmpty())
      return nullptr;

    // 创建document与worker的连接
    if (document->frame()->loader().client()->sharedWorkerRepositoryClient())
      document->frame()->loader().client()->sharedWorkerRepositoryClient()->connect(worker.get(), remotePort.release(), scriptURL, name, exceptionState);

    // 返回worker的实例
    return worker.release();
  }

  SharedWorker::~SharedWorker()
  {
  }

  const AtomicString& SharedWorker::interfaceName() const
      {
          return EventTargetNames::SharedWorker;
}

bool SharedWorker::hasPendingActivity() const
    {
        return m_isBeingConnected;
}

其实SharedWorker挺简单的,进程管理,通信,下面看通信是如何实现的(急需恶补一番底层通信知识,预定下周把底层通信手段学习一遍)。

static void createChannel(MessagePort* port1, MessagePort* port2)
{
  // 创建连接
  WebMessagePortChannel* channel1;
  WebMessagePortChannel* channel2;
  Platform::current()->createMessageChannel(&channel1, &channel2);
  ASSERT(channel1 && channel2);

  // Now entangle the proxies with the appropriate local ports.
  port1->entangle(adoptPtr(channel2));
  port2->entangle(adoptPtr(channel1));
}

MessageChannel::MessageChannel(ExecutionContext* context)
// MessageChannel里面就是搞两个port
: m_port1(MessagePort::create(*context))
, m_port2(MessagePort::create(*context))
{
  createChannel(m_port1.get(), m_port2.get());
}

好吧,这个类也不怎么干活啊,活在WebMessagePortChannel和MessagePort里面做。

MessagePort

// 如果这里是管道通信的话,那可能需要两组channel worker端一组,web端一组
// 这是一个干活的类,create里面做的事情不多,构造一下,返回
PassRefPtrWillBeRawPtr<MessagePort> MessagePort::create(ExecutionContext& executionContext)
{
  RefPtrWillBeRawPtr<MessagePort> port = adoptRefWillBeNoop(new MessagePort(executionContext));
  port->suspendIfNeeded();
  return port.release();
}

//构造函数做的也不多,创建一个跟document关联的DOM Object,初始化几个标志参数
MessagePort::MessagePort(ExecutionContext& executionContext)
: ActiveDOMObject(&executionContext)
, m_started(false)
    , m_closed(false)
    , m_weakFactory(this)
{
}

MessagePort::~MessagePort()
{
  close();
  if (m_scriptStateForConversion)
    m_scriptStateForConversion->disposePerContextData();
}

// 发消息
void MessagePort::postMessage(ExecutionContext* context, PassRefPtr<SerializedScriptValue> message, const MessagePortArray* ports, ExceptionState& exceptionState)
{
  // entangle,好吧,英语比较渣,google翻译下,是缠的意思,Orz
  if (!isEntangled())
    return;
  ASSERT(executionContext());
  ASSERT(m_entangledChannel);

  // 一个channel数组
  OwnPtr<MessagePortChannelArray> channels;
  // Make sure we aren't connected to any of the passed-in ports.
  // 防错代码
  if (ports) {
    for (unsigned i = 0; i < ports->size(); ++i) {
      MessagePort* dataPort = (*ports)[i].get();
      if (dataPort == this) {
        exceptionState.throwDOMException(DataCloneError, "Port at index " + String::number(i) + " contains the source port.");
        return;
      }
    }
    // 解绑
    channels = MessagePort::disentanglePorts(context, ports, exceptionState);
    if (exceptionState.hadException())
      return;
  }

  // 给channel发消息
  WebString messageString = message->toWireString();
  OwnPtr<WebMessagePortChannelArray> webChannels = toWebMessagePortChannelArray(channels.release());
  // 待看 WebMessagePortChannelArray
  m_entangledChannel->postMessage(messageString, webChannels.leakPtr());
}

// static 两组管道互相取
PassOwnPtr<WebMessagePortChannelArray> MessagePort::toWebMessagePortChannelArray(PassOwnPtr<MessagePortChannelArray> channels)
{
  OwnPtr<WebMessagePortChannelArray> webChannels;
  if (channels && channels->size()) {
    webChannels = adoptPtr(new WebMessagePortChannelArray(channels->size()));
    for (size_t i = 0; i < channels->size(); ++i)
    (*webChannels)[i] = (*channels)[i].leakPtr();
  }
  return webChannels.release();
}

// static
PassOwnPtrWillBeRawPtr<MessagePortArray> MessagePort::toMessagePortArray(ExecutionContext* context, const WebMessagePortChannelArray& webChannels)
{
  OwnPtrWillBeRawPtr<MessagePortArray> ports = nullptr;
  if (!webChannels.isEmpty()) {
    OwnPtr<MessagePortChannelArray> channels = adoptPtr(new MessagePortChannelArray(webChannels.size()));
    for (size_t i = 0; i < webChannels.size(); ++i)
    (*channels)[i] = adoptPtr(webChannels[i]);
    ports = MessagePort::entanglePorts(*context, channels.release());
  }
  return ports.release();
}

// 函数字面意思是,断开缠绕关系
PassOwnPtr<WebMessagePortChannel> MessagePort::disentangle()
{
  ASSERT(m_entangledChannel);
  // 将端口重置为0
  m_entangledChannel->setClient(0);
  return m_entangledChannel.release();
}

// Invoked to notify us that there are messages available for this port.
// This code may be called from another thread, and so should not call any non-threadsafe APIs (i.e. should not call into the entangled channel or access mutable variables).
// 通知port已经可用了,即start过了
void MessagePort::messageAvailable()
{
  ASSERT(executionContext());
  executionContext()->postTask(FROM_HERE, createCrossThreadTask(&MessagePort::dispatchMessages, m_weakFactory.createWeakPtr()));
}

// 类似于生命周期,start了通知一声
void MessagePort::start()
{
  // Do nothing if we've been cloned or closed.
  if (!isEntangled())
    return;

  ASSERT(executionContext());
  if (m_started)
    return;

  m_started = true;
  messageAvailable();
}

// 关掉缠绕关系
void MessagePort::close()
{
  if (isEntangled())
    m_entangledChannel->setClient(0);
  m_closed = true;
}

// 与远程channel缠绕
void MessagePort::entangle(PassOwnPtr<WebMessagePortChannel> remote)
{
  // Only invoked to set our initial entanglement.
  ASSERT(!m_entangledChannel);
  ASSERT(executionContext());

  m_entangledChannel = remote;
  m_entangledChannel->setClient(this);
}

const AtomicString& MessagePort::interfaceName() const
    {
        return EventTargetNames::MessagePort;
}

// 尝试从webChannel取message
static bool tryGetMessageFrom(WebMessagePortChannel& webChannel, RefPtr<SerializedScriptValue>& message, OwnPtr<MessagePortChannelArray>& channels)
{
  WebString messageString;
  WebMessagePortChannelArray webChannels;
  if (!webChannel.tryGetMessage(&messageString, webChannels))
  return false;

  if (webChannels.size()) {
    channels = adoptPtr(new MessagePortChannelArray(webChannels.size()));
    for (size_t i = 0; i < webChannels.size(); ++i)
    (*channels)[i] = adoptPtr(webChannels[i]);
  }
  message = SerializedScriptValueFactory::instance().createFromWire(messageString);
  return true;
}

bool MessagePort::tryGetMessage(RefPtr<SerializedScriptValue>& message, OwnPtr<MessagePortChannelArray>& channels)
{
  if (!m_entangledChannel)
    return false;
  return tryGetMessageFrom(*m_entangledChannel, message, channels);
}

// 分发消息
void MessagePort::dispatchMessages()
{
  // Because close() doesn't cancel any in flight calls to dispatchMessages() we need to check if the port is still open before dispatch.
  if (m_closed)
    return;

  // Messages for contexts that are not fully active get dispatched too, but JSAbstractEventListener::handleEvent() doesn't call handlers for these.
  // The HTML5 spec specifies that any messages sent to a document that is not fully active should be dropped, so this behavior is OK.
  if (!started())
    return;

  RefPtr<SerializedScriptValue> message;
  OwnPtr<MessagePortChannelArray> channels;
  // 从管道中拿到message
  while (tryGetMessage(message, channels)) {
    // close() in Worker onmessage handler should prevent next message from dispatching.
    if (executionContext()->isWorkerGlobalScope() && toWorkerGlobalScope(executionContext())->isClosing())
      return;

    // 这里有一次绑定端口
    OwnPtrWillBeRawPtr<MessagePortArray> ports = MessagePort::entanglePorts(*executionContext(), channels.release());
    RefPtrWillBeRawPtr<Event> evt = MessageEvent::create(ports.release(), message.release());

    dispatchEvent(evt.release(), ASSERT_NO_EXCEPTION);
  }
}

bool MessagePort::hasPendingActivity() const
    {
      // The spec says that entangled message ports should always be treated as if they have a strong reference.
      // We'll also stipulate that the queue needs to be open (if the app drops its reference to the port before start()-ing it, then it's not really entangled as it's unreachable).
        return m_started && isEntangled();
}

// 解除port entangle关系?
PassOwnPtr<MessagePortChannelArray> MessagePort::disentanglePorts(ExecutionContext* context, const MessagePortArray* ports, ExceptionState& exceptionState)
{
  if (!ports || !ports->size())
    return nullptr;

  // HashSet used to efficiently check for duplicates in the passed-in array.
  HashSet<MessagePort*> portSet;

  // Walk the incoming array - if there are any duplicate ports, or null ports or cloned ports, throw an error (per section 8.3.3 of the HTML5 spec).
  for (unsigned i = 0; i < ports->size(); ++i) {
  MessagePort* port = (*ports)[i].get();
  if (!port || port->isNeutered() || portSet.contains(port)) {
    String type;
    if (!port)
      type = "null";
    else if (port->isNeutered())
      type = "already neutered";
    else
      type = "a duplicate";
    exceptionState.throwDOMException(DataCloneError, "Port at index "  + String::number(i) + " is " + type + ".");
    return nullptr;
  }
  portSet.add(port);
}

  UseCounter::count(context, UseCounter::MessagePortsTransferred);

  // Passed-in ports passed validity checks, so we can disentangle them.
  // 每个port disentangle
  OwnPtr<MessagePortChannelArray> portArray = adoptPtr(new MessagePortChannelArray(ports->size()));
  for (unsigned i = 0; i < ports->size(); ++i)
  (*portArray)[i] = (*ports)[i]->disentangle();
  return portArray.release();
}

// 绑定所有ports
PassOwnPtrWillBeRawPtr<MessagePortArray> MessagePort::entanglePorts(ExecutionContext& context, PassOwnPtr<MessagePortChannelArray> channels)
{
  // https://html.spec.whatwg.org/multipage/comms.html#message-ports
  // |ports| should be an empty array, not null even when there is no ports.
  if (!channels || !channels->size())
    return adoptPtrWillBeNoop(new MessagePortArray());

  OwnPtrWillBeRawPtr<MessagePortArray> portArray = adoptPtrWillBeNoop(new MessagePortArray(channels->size()));
  for (unsigned i = 0; i < channels->size(); ++i) {
  RefPtrWillBeRawPtr<MessagePort> port = MessagePort::create(context);
  port->entangle((*channels)[i].release());
  (*portArray)[i] = port.release();
}
  return portArray.release();
}

主要就是entangle函数里面,会做一次remote.setClient(this),然后dispatchEvent就很方便了。疑问就是 为什么postMessage会做一次disentangle

就是port看的晕晕的,还是去理解一下底层通信吧.

什么是Web-Worker?

WebWorker类似于浏览器中的多线程操作。之前的JS中,无论你是使用setTimeout setIntever 还是 使用了XMLHttpRequest,都是在一个线程里面,前两个使用消息队列,XMLHttpRequest则是浏览器会帮你进行闲时进行,归根结底,都是在一个线程里面跑。如果用户想进行一些阻塞操作,很可能会产生卡住页面的情况。甚至于我们想实现一个类似于Android的专用 公用Service,那该怎么办?

H5新标准提出了WebWorker的概念,各个浏览器都各自实现了,先来看一下WebWorker能做什么。

 

WebWorker特点,在后台线程执行JS的能力,与页面通过send message这种方式通信。

WebWorker有两种,专用worker(dedicatedworker)与公用worker(sharedworker)。

疑问:worker与主线程如何同步,worker与主线程同时操作了一个DOM元素,会不会产生脏数据?所以,worker的能力被加以限制,不能访问DOM元素。

worker在chrome中是如何实现的?

chrome浏览器是多进程架构,分为Browser进程以及Render进程,每打开一个页面,浏览器都会为其分配一个Render进程,webkit以及js都运行在这个进程内。如果要起一个DedicatedWorker,Chrome会在Render进程中起一个线程。如果要起一个SharedWorker就稍微复杂一点,必须起一个专门的进程,并且,相同的SharedWorker不管你创建多少次,都只存在一个。

Android中的Chrome有一个限制,限定9个进程。

1.如果用户创建太多SharedWorker,可能第二个标签页都打不开?

2.SharedWorker的优先级如何定义?如果使用SharedWorker的页面都在后台,其优先级如何?

目前Android上的SharedWorker还处于讨论阶段,未实现。

下面看一下worker的基本用法,DedicatedWorker:

worker.js

this.addEventListener('message', function(e) {
    var data = e.data;
    console.log("worker: " + data);
    this.postMessage(data + 1);
});

worker中需要一个this.onmessage接收消息

postMessage发送消息

参数在 event.data中

main.js

if (window.Worker) {
    var worker = new Worker("./worker.js");
    worker.onmessage = function(e) {
        document.getElementById("worker").innerHTML = e.data;
    };

    document.addEventListener('keydown', function(evt) {
        worker.postMessage(evt.keyCode);
    });
}

使用Worker这个API来创建DedicatedWorker。

同样通过worker实例的onmessage和postMessage通信。

结束Worker:

在worker中,可以通过close()来kill掉自己

main中则调用worker.terminate()

如果worker运行中出现错误,在main中使用worker.onerror可以接收到错误消息

SharedWorker:

https://github.com/mdn/simple-shared-worker

SharedWorker跟DedicatedWorker有两个不同的地方:

1.通信不再直接通过worker,而是worker的port类

2.worker中需要实现onconnect,且其参数中有一个port列表,但目前只使用到了第一个

sharedworker.js

onconnect = function(e) {
   var port = e.ports[0];

   port.onmessage = function(e) {
     var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
     port.postMessage(workerResult);
   }

}

main.js

if (!!window.SharedWorker) {
  var myWorker = new SharedWorker("worker.js");

    myWorker.port.postMessage([squareNumber.value,squareNumber.value]);

  myWorker.port.onmessage = function(e) {
    result2.textContent = e.data;
    console.log('Message received from worker');
  }
}

当然 还可以写另一个页面,同样可以使用跟main.js类似的方法跟sharedworker通信。

ServiceWorker:

ServiceWorker是WebWorker的一种,它更加复杂,所以也更加强大。

首先,ServiceWorker有独立的生命周期:

1.Register:主线程调用API注册ServiceWorker

2.Installing:浏览器启动安装过程,加载和缓存一些静态资源

有可能失败进入Error状态

3.Activated:激活阶段,此阶段可以升级ServiceWorker

4.激活后,ServiceWorker会接管页面,如果页面是刚刚注册,本次不会被接管,下次加载页面才会接管。

5.ServiceWorker接管页面后,如果有fetch和message事件,会处于onfetch和onmessage,其他情况可能被终止。

ServiceWorker的特性:

1.它是一个worker,同样不能操作dom元素,同样可以通过postMessage与调用线程通信

2.ServiceWorker增加了网络处理,onfetch

3.ServiceWorker不被使用的时候,它会自己终止,再次使用会被重新激活,不要依靠它的内存来保存信息,请用webStorage或者indexDB。

4.ServiceWorker大量使用Promise,就是封装的一个callback标准

5.ServiceWorker权限很大,即所有网络请求都经过它,可以劫持连接,伪造和过滤响应,所以只能在https网页上注册ServiceWorker(是只能么?)。

6.ServiceWorker只作用于同域的fetch。onfetch有一个缓存的例子,我们通过缓存类缓存一些request和response,下次request,直接去缓存找response,处理失败了再去网络实时请求。注:response的类型需要是basic,即同域请求。

8.ServiceWorker的自动更新。

当网页激活时,浏览器会检查ServiceWorker是否有更新(有一个字节不同就会认为有更新),浏览器后台下载。

下载完开始运行,进入install状态,之后进入waitting状态。因为此时旧的ServiceWorker仍然在运行。

当页面被杀掉,旧去新来。

当然,如果你之前缓存了request,更新后需要清理一下。

现有问题:

如果在Install时失败了,页面无法感知。

ServiceWorker主要作用是在onfetch里面缓存/处理request。

https://github.com/GoogleChrome/samples

中有大量的onfetch与cache结合使用做离线应用的例子,在此不多赘述。

来自

http://blog.csdn.net/yl02520/article/details/14446763

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

JS处理相对路径

遇到引擎不能处理相对路径的问题,写了一个简单的相对路径转绝对路径的函数,留下备用:

function rel2Abs(path) {
    var _path = path;
    var locationStr = location.href.toString();
    console.log(locationStr);
    var result = '';

    var relative;
    while (relative = _path.substring(0, _path.indexOf("\/"))) {
        console.log(relative);
        if (relative === '.') {
            // 遇到. path去掉最前面的./
            // 遇到.当前路径去掉/后面内容
            _path = _path.substring(_path.indexOf('\/') + 1, _path.length);
            locationStr = locationStr.substring(0, locationStr.lastIndexOf('\/') + 1);
        } else if (relative === '..') {
            // 遇到.. path去掉最前面的../
            // 遇到.. 当前路径去掉/xxx,然后再去掉/后面的内容
            _path = _path.substring(_path.indexOf('\/') + 1, _path.length);
            locationStr = locationStr.substring(0, locationStr.lastIndexOf('\/'));
            locationStr = locationStr.substring(0, locationStr.lastIndexOf('\/') + 1);
        } else {
            break;
        }
    }
    var result = locationStr + _path;
    path = result;
    return result;
}

原理很简单,只支持/,且遇到错误不能自动处理

JS常用方法封装

做项目是遇到一个封装的不错的Util类,写一下,长期备用:

CMD标准

define(function(require, exports, module) {
    "use strict";

    function Util() {}

    (function() {
        Util.isWindow = function(obj) {
            return obj !== null && obj === obj.window;
        };

        Util.isFunction = function(obj) {
            return typeof obj === 'function';
        };

        Util.isObject = function(obj) {
            return typeof obj === 'object';
        };

        Util.isArray = function(obj) {
            return obj instanceof Array;
        };

        Util.isPlainObject = function(obj) {
            return Util.isObject(obj) && !Util.isWindow(obj);
        };

        Util.isString = function(obj) {
            return typeof obj === 'string';
        };

        // 将其他的object统统搞到一个object中去
        // 这个函数不如直接写两个参数看着舒服 target objects,就不用各种slice shift了
        Util.extend = function(target) {
            var deep;
            var args = [].slice.call(arguments, 1);

            if (typeof target === 'boolean') {
                deep = target;
                target = args.shift();
            }

            args.forEach(function(arg) {
               extend(target, arg, deep);
            });

            return target;

            // 这个函数就是把source里面的一层一层往下展开给target,object 和 array会一直展开
            function extend(target, source, deep) {
                for (var key in source) {
                    if (deep && (Util.isPlainObject(source[key]) || Array.isArray(source[key]))) {
                        if (Util.isPlainObject(source[key]) && !Util.isPlainObject(source[key])) {
                            target[key] = {};
                        }

                        if (Array.isArray(source[key]) && !Array.isArray(target[key])) {
                            target[key] = [];
                        }
                        extend(target[key], source[key], deep);
                    } else {
                        target[key] = source[key];
                    }
                }
            }
        };

        Util.ajaxGet = function(url, data, callback) {
            // 简单学习一下XMLHttpRequest,一个JS对象,提供了封装的获得url上资源数据的方法,支持xml http ftp file
            // 步骤简单: open(method, url, sync), send(), 如果是异步的话 就调用onreadystatechange方法
            var xhr = new XMLHttpRequest();
            // 设置超时时间为30s
            xhr.timeout = 30000;

            xhr.onreadystatechange = function() {
                console.log("xhr.status is " + xhr.readyState);
                if (xhr.readyState === 4) {
                    var status = 0;
                    try {
                        status = xhr.status;
                    } catch(e) {
                        // eat and ignore the exception, since access the status of XHR might cause a exception after timeout occurs;
                        return;
                    }

                    if (status === 200) {
                        var result = null;
                        try {
                            result  = JSON.parse(xhr.responseText.replace(/\n|\r|\t|\b|\f/g, ''));
                        } catch(e) {
                            callback(null);
                            return;
                        }
                        callback(result);
                    } else {
                        callback(null);
                    }

                    xhr.onreadystatechange = null;
                    xhr = null;
                }
            };
            xhr.withCredentials = true;
            xhr.open("GET", url + "?" + data, true);
            xhr.send(null);
            return xhr;
        };

        Util.ajaxPost = function(url, data, callback) {
            var xhr = new XMLHttpRequest();
            // 原来navigator除了online,还有个connection
            // 这个connection,而且这个connection很刁,何以拿到type,还有change事件
            // 关键是一大堆浏览器都TM的不支持
            var connection = navigator.connection;
            switch(connection.type) {
                case connection.WIFI :
                case connection.ETHERNET:
                    xhr.timeout = 3000;
                    break;
                case connection.CELL_3G:
                case connection.CELL_4G:
                    xhr.timeout = 5000;
                    break;
                case connection.CELL_2G:
                    xhr.timeout = 30000;
                    break;
                default :
                    xhr.timeout = 30000;
            }

            // POST比GET复杂,首先不同网络timeout不一样,其次有很多事件需要监听
            xhr.addEventListener("error", function(e) {
                if (e && e.loaded === 60) {
                    // https error
                    if (e.total === 9) {
                        callback("CERTIFICATE TIME ERROR");
                    } else {
                        callback("CERTIFICATE ERROR");
                    }
                } else {
                    callback("NETWORK_ERROR");
                }
            });

            xhr.addEventListener("abort", function() {
                console.log("request abort!");
            });

            xhr.addEventListener("timeout", function() {
                callback("TIMEOUT");
            });

            xhr.addEventListener("readystatechange", function() {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        var result;
                        try {
                            result = JSON.parse(xhr.responseText.replace(/\n|\r|\t|\b|\f/g, ''));
                        } catch(e) {
                            callback("DATA_INVALID");
                            return;
                        }
                        callback(result);
                    } else if (xhr.status !== 0) {
                        callback("HTTP_ERROR");
                    }
                }
            });

            // withCredentials = true 将使得请求会带上cookie
            xhr.withCredentials = true;
            xhr.open("POST", url, true);
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhr.send(data);
            return xhr;
        };

        Util.abort = function(xhr) {
            xhr.abort;
        };

        Util.getGeoLocation = function(callback) {
            var timeout = 30000;
            if (navigator.connection && navigator.connection.type) {
                switch (navigator.connection.type) {
                    // wifi
                    case 2:
                        timeout = 8000;
                        break;
                    // 2G
                    case 3:
                        timeout = 30000;
                        break;
                    // 3g:
                    case 4:
                        timeout = 20000;
                        break;
                    default:
                        timeout = 30000;
                        break;
                }
            }

            // 这里就是浏览器获取地理位置的用法
            navigator.geolocation.getCurrentPosition(
                function(position) {
                callback.call(this, position);
                }.bind(this),
                function(error) {
                    callback.call(this, null);
                }.bind(this),
                {
                    enableHighAccuracy: true,
                    maximumAge: 180000,
                    timeout: timeout
                });
        };

        // 实时定位, 返回一个ID,用来取消监听
        Util.startWatchGeoLocation = function(callback) {
            return navigator.geolocation.watchPosition(
                function(position) {
                    callback.call(this, position);
                }.bind(this),
                function() {
                    callback.call(this, null);
                }.bind(this),
                {
                    maximumAge: 600000,
                    timeout: 20000
                }
            );
        };

        Util.stopWatchGeoLocation = function(geoId) {
            navigator.geolocation.clearWatch(geoId);
        };

        // 主要用来 ajax request中把object转为字符串
        Util.paramToStr = function(params) {
            var data = '';
            var isFirstChar = true;
            for (var key in params) {
                if (params.hasOwnProperty(key)) {
                    if (isFirstChar === true) {
                        isFirstChar = false;
                        data += key + "=" + params[key];
                    } else {
                        data += "&" + key + "=" + params[key];
                    }
                }
            }
            return data;
        };

        // 预加载一个Image
        Util.preloadImage = function(images) {
            if (images && images.length) {
                for(var i = 0; i < images.length; i++) {
                    var imgContainer = new Image();
                    imgContainer.src = images[i];
                    imgContainer = null;
                }
            }
        };

        // 就是把 rgb(,,)换成了rgba(,,,)
        Util.addOpacity = function(color, opacity) {
            var newColor = color.replace(/rgb/g, "rgba").replace(/\)/g, "," + opacity + ")");
            return newColor;
        };

        Util.isOffline = function() {
            return !navigator.onLine;
        };

        Util.getStorageValue = function(key) {
            var item = window.localStorage.getItem(key);
            if (!item) {
                return;
            }

            item = JSON.parse(item);
            var data = item.data;
            var type = item.type;
            var value = null;
            switch(type) {
                case "Boolean":
                    value = Boolean(data);
                    break;
                case "String":
                    value = String(data);
                    break;
                case "Number":
                    value = Number(data);
                    break;
                case "JSON":
                    value = JSON.parse(data);
                    break;
            }

            return value;
        };

        Util.setStorageValue = function(key, value) {
            var type = null;
            var data = value;
            if (typeof value === "boolean") {
                type = "Boolean";
            } else if (typeof value === "string") {
                type = "String";
            } else if (typeof value === "number") {
                type = "Number";
            } else if (typeof value === "object") {
                type = "JSON";
                data = JSON.stringify(value);
            }

            window.localStorage.setItem(key, JSON.stringify({data: data, type: type}));
        }
    }).();

    module.exports = Util;
});

IScroll的使用-方向键绑定&自定义滚动条样式

之前在webkit上开发一个滚动控件,需要完成的是一段文字,上下键可以滚动,且自定义滚动条。第一想法就是浏览器原生overflow:scroll,且webkit支持自定义滚动条样式:

webkit自定义滚动条样式:

/*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
::-webkit-scrollbar
{
    width: 0.05rem;
    height:1rem;
    background-color: transparent;
}

/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track
{
    background-color: transparent;
}

/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb
{
    border-radius: 0.1rem;
    /*-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);*/
    background-color: rgba(255,255,255,0.6);
}

后来换了个引擎,发现其他引擎不支持自定义滚动条,那就满足不了UED的要求了。来同事推荐使用IScroll,用了一下,确实比较方便,与平台无关,可操作的属性还有性能都很理想,记录一下:

首先看官方文档 https://iiunknown.gitbooks.io/iscroll-5-api-cn/content/versions.html

github地址:https://github.com/cubiq/iscroll

demo:http://cubiq.org/dropbox/iscroll4/examples/simple/

直接拿demo中的iscroll.js套到自己的工程上,一个段落就是一个li,new 一下就完事了,滚动拖动也很happy。然后发现,我按上下方向键没有响应。

IScroll按键事件绑定:

查源码看iscroll的事件处理,都在handleEvent函数里面

handleEvent: function (e) {
    var that = this;
    switch(e.type) {
        case START_EV:
        if (!hasTouch && e.button !== 0) return;
        that._start(e);
        break;
        case MOVE_EV: that._move(e); break;
        case END_EV:
        case CANCEL_EV: that._end(e); break;
        case RESIZE_EV: that._resize(); break;
        case WHEEL_EV: that._wheel(e); break;
        case 'mouseout': that._mouseout(e); break;
        case TRNEND_EV: that._transitionEnd(e); break;
    }
}

根本就没有keyEvent,看来偷懒是不行的,官方文档看一下,原来iscroll有5个版本,各自平台都是不一样的,demo中这个估计是移动平台用的iscroll-lite版本,移动平台根本不鸟方向键的。

去github上down下来源码,找了找,build目录下,5个版本都有。用最原始的common版本,这个版本的handleEvent就丰富多了:

handleEvent: function (e) {
   switch ( e.type ) {
      case 'touchstart':
      case 'pointerdown':
      case 'MSPointerDown':
      case 'mousedown':
         this._start(e);
         break;
      case 'touchmove':
      case 'pointermove':
      case 'MSPointerMove':
      case 'mousemove':
         this._move(e);
         break;
      case 'touchend':
      case 'pointerup':
      case 'MSPointerUp':
      case 'mouseup':
      case 'touchcancel':
      case 'pointercancel':
      case 'MSPointerCancel':
      case 'mousecancel':
         this._end(e);
         break;
      case 'orientationchange':
      case 'resize':
         this._resize();
         break;
      case 'transitionend':
      case 'webkitTransitionEnd':
      case 'oTransitionEnd':
      case 'MSTransitionEnd':
         this._transitionEnd(e);
         break;
      case 'wheel':
      case 'DOMMouseScroll':
      case 'mousewheel':
         this._wheel(e);
         break;
      case 'keydown':
         this._key(e);
         break;
      case 'click':
         if ( !e._constructed ) {
            e.preventDefault();
            e.stopPropagation();
         }
         break;
   }
}

然后套用上去,可以支持方向键了。中间遇到两个小问题,第一个是按键一直没反应,检查下是z-index太小,keyEvent被上层元素拿走了;第二个是只有在#warpper拿到focus的时候才响应按键,但我用的引擎不支持focus,这个也不难,页面强行绑定handleEvent:

document.addEventListener("keydown", function(evt) {
    if (evt.keyCode === keyCodes.ENTER) {
    } else {
        myScroll && myScroll.handleEvent(evt);
    }
}, false);

然后整个页面随便按什么键,都可以响应了。接下来就是滚动条样式的问题了,这个也简单,跟着官方文档&样例走就行

http://lab.cubiq.org/iscroll5/demos/styled-scrollbars/

关键步骤有三个:

1.option

myScroll = new IScroll(document.getElementById('wrapper'), {
    keyBindings: true,          // 绑定按键事件
    scrollbars: 'custom',       // 自定义样式
    resizeScrollbars: false     // 是否自动缩放滚动条
});

设置了scrollbars: 'custom',在页面的Elements就可以找到

<div class="iScrollVerticalScrollbar iScrollLoneScrollbar" ></div>

里面还包含了

<div class="iScrollIndicator" ></div>

第一个是滚动区域,第二个是滚动条。拿到Element就好办了,css给定样式:

.iScrollVerticalScrollbar {
    position: absolute;
    z-index: 9999;
    width: 0.1rem;
    bottom: 2px;
    top: 2px;
    right: 0;
    overflow: hidden;
}

.iScrollIndicator {
    position: absolute;
    width: 0.08rem; height: 0.3rem;
    background: rgba(255,255,255,0.6);
    border-radius: 0.1rem;
}

大功告成!

当然,IScroll不止这点功能,官方文档后面还有无限滚动等高级用法,以后用到再添加。

使用grunt压缩代码&配置多个任务

grunt是node环境下一个压缩、合并 js、css代码的工具,还可以做一下代码美化等。体验了一下压缩合并,还是不错的,大概流程如下:

在工程下建一个grunt目录,写两个文件 package.json-工程依赖Gruntfile.js-压缩任务

package.jsn:

{
  "name": "xxx",
  "version": "v0.1.0",
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-contrib-concat": "~0.5.1",
    "grunt-contrib-cssmin": "~0.12.3",
    "grunt-contrib-jshint": "~0.10.0",
    "grunt-contrib-nodeunit": "~0.4.1",
    "grunt-contrib-uglify": "~0.5.0",
    "grunt-htmlhint": "~0.9.2"
  }
}

Gruntfile.js:

module.exports = function(grunt) {
    // 配置参数
    grunt.initConfig({
        pkg:grunt.file.readJSON('package.json'),
        // 代码合并
        concat:{
            options:{
                separator: ";",
                stripBanners: true
            },
            dist:{
                src:[
                    "../focusManager/base.js",
                    "../focusManager/tween.js",
                    "../focusManager/widget.js",
                    "../focusManager/yunos.js"
                ],
                dest: "dest/focusManager.js"
            }
        },
        // 代码压缩
        uglify:{
            options:{

            },
            dist:{
                files:{
                    "dest/focusManager-min.js" : "dest/focusManager.js"
                }
            }
        },
        // css压缩
        cssmin: {
            options: {
                keepSpecialComments: 0
            },
            compress:{
                files:{
                    "dest/default.css": [
                        "../focusManager/base.css",
                        "../focusManager/reset.css"
                    ]
                }
            }
        }
    });

    // 载入concat、uglify、cssmin插件,进行压缩
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-cssmin');

    // 注册任务
    grunt.registerTask('default', ['concat', 'uglify', 'cssmin']);
};

这个Gruntfile很容易理解,想要压缩的文件以及目标文件写好就行,还有另一种自动化的写法,后面再说。

之后运行命令,grunt

1

出现了一个错误,网上解释

Note that installing grunt-cli does not install the grunt task runner! The job of the grunt CLI is simple: run the version of grunt which has been installed next to a Gruntfile. This allows multiple versions of grunt to be installed on the same machine simultaneously.

So in your project folder, you will need to install (preferably) the latest grunt version:

也就是说,之前装的grunt-cli只是grunt的运行环境,在工程目录下,还需要装一下grunt,运行一下命令:

$npm install grunt --save-dev

Option --save-dev will add grunt as a dev-dependency to your package.json. This makes it easy to reinstall dependencies.

2

之后再次运行grunt命令,又出现错误:

3

好吧,没有装这三个插件?原来是grunt一样,也要在工程下装,分别运行 npm install装一下,运行,成功:

4

工程复杂之后,肯定不会一个一个手动填文件,grunt也可以通过逻辑自动压缩js css文件,Gruntfile.js写法:

module.exports = function(grunt) {
    // 任务配置
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        // 压缩JS
        uglify: {
            // 文件头部输出信息
            options: {
                banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
            },
            my_target: {
                files: [
                    {
                        expand: true,
                        // 相对路径
                        cwd: '../focusManager',
                        src: ['*.js', '**/*.js'],
                        // src:['**/*.js', '!**/*.min.js'] 多个src的写法以及不包括哪些js
                        dest: 'dest/js/',
                        rename: function (dest, src) {
                            var folder = src.substring(0, src.lastIndexOf('/'));
                            var filename = src.substring(src.lastIndexOf('/'), src.length);
                            filename = filename.substring(0, filename.lastIndexOf('.'));
                            var resultname = dest + folder + filename + '-min.js';
                            grunt.log.writeln("正在处理文件:" + src + " 处理后文件:" + resultname);

                            return resultname;
                        }
                    }
                ]
            }
        },

        // 压缩css
        cssmin: {
            // 文件头部输出信息
            options: {
                banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
                // 美化代码
                beautify: {
                    // 防止乱码
                    ascii_only: true
                }
            },
            my_target: {
                files: [
                    {
                        expand: true,
                        // 相对路径
                        cwd: '../focusManager',
                        src: '*.css',
                        // src:['**/*.js', '!**/*.min.js'] 多个src的写法以及不包括哪些js
                        dest: 'dest/css/',
                        rename: function (dest, src) {
                            var folder = src.substring(0, src.lastIndexOf('/'));
                            var filename = src.substring(src.lastIndexOf('/'), src.length);
                            filename = filename.substring(0, filename.lastIndexOf('.'));
                            var resultname = dest + folder + filename + '-min.css';
                            grunt.log.writeln("正在处理文件:" + src + " 处理后文件:" + resultname);

                            return resultname;
                        }
                    }
                ]
            }
        }
    });

    // 加载模块
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-cssmin');

    grunt.registerTask('default', ['uglify', 'cssmin']);
};

逻辑也很简单,就是把src里面的文件,压缩,生成文件会经过rename函数处理。

这里面需要注意的地方有

src: ['*.js', '**/*.js'],

如果只写一个*.js,是不会递归cwd目录下的所有文件的,要递归几级写到数组中就可以了,如果不想添加什么文件,加!符号-‘!*.min.js’。

当然,这次也没有一帆风顺,运行的时候遇到错误:

5

报错在 最后一句,

grunt.registerTask('default', ['uglify', 'cssmin']);

查了半天也没找到这里有什么错误,网上搜了一下,大多数是 中括号里的uglify没有引号,跟我的情况也不一样。后来仔细检查了一下代码,发现了两处拼写错误,改了后就运行通过了,也就是说,其他错误都会报到最后一句。

做了一个小小的试验,写一个空模块

(function(){
    // for test
})();

压缩后是这样的:


对,什么都没了。

 

最近有新的需求,压缩不同版本的文件,就需要开多个contact uglify任务,琢磨了一会,搞出来了,同时对grunt有了进一步的认识。

配置多任务是这么写的

module.exports = function(grunt) {
    // 配置参数
    grunt.initConfig({
        pkg:grunt.file.readJSON('package.json'),
        // 代码合并
        concat:{
            options:{
                separator: ";",
                stripBanners: true
            },
            123456: {
                src:[
                    "../../system/123.js",
                    "../../system/456.js",
                    "../../system/network/789.js"
                ],
                dest: "out/123456789.js"
            },
            all: {
                src:[
                    "../../system/blitz.js",
                    "../../system/base.js",
                    "../../system/**/*.js",
                ],
                dest: "out/test_all.js"
            }
        },
        uglify:{
            123456: {
                files: [
                    {
                        "out/123456789.min.js" : "out/123456789.js"
                    }
                ]
            },
            all: {
                files: [
                    {
                        "out/test_all.min.js" : "out/test_all.js"
                    }
                ]
            },
            every: {
                // 文件头部输出信息
                //options: {
                //    banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
                //},
                files: [
                    {
                        expand: true,
                        // 相对路径
                        cwd: '../../system',
                        // 这里写三层,如果还有更多的再说
                        src: ['*.js', '**/*.js', '**/**/*.js'],
                        // src:['**/*.js', '!**/*.min.js'] 多个src的写法以及不包括哪些js
                        dest: './out/all/',
                        rename: function (dest, src) {
                            var folder = src.substring(0, src.lastIndexOf('/'));
                            var filename = src.substring(src.lastIndexOf('/'), src.length);
                            filename = filename.substring(0, filename.lastIndexOf('.'));
                            var resultname = dest + folder + filename + '.min.js';
                            grunt.log.writeln("正在处理文件:" + src + " 处理后文件:" + resultname);

                            return resultname;
                        }
                    }
                ]
            }
        }
    });

    // 载入concat、uglify、cssmin插件,进行压缩
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    //grunt.loadNpmTasks('grunt-contrib-cssmin');

    // 注册任务
    grunt.registerTask('default', ['concat', 'uglify']);
};

r简单看一下配置文件

contact

首先配置了option,option中配置了不同文件中间增加;分隔符。

这个是contact所有任务的全局配置

后面又写了三个合并任务,分别配置下src和dest就可以了。

合并顺序就是 src中配置的顺序,*.js则按照字母排序。

uglify

uglify中配置了三个压缩任务,

压缩contact的文件

压缩所有文件并生成.min.js

最后运行

grunt.registerTask('default', ['concat', 'uglify']);

会自动加载contact和uglify中的所有任务。

来源于http://www.haorooms.com/post/qd_grunt_cssjs。

ES6新特性

ECMAScript出现了许多有趣的新特性,梳理一下:

1.支持constant声明

ES5中,想实现constant变量必须通过修改var的property实现

Object.defineProperty(this, "PI", {
    value: 3.1415926,
    configurable: false,
    writable: false
});

2.支持块作用域声明let,

JS只有函数作用域,没有块作用域,var声明有变量提升的效果,即如果var声明了一个未声明过的变量,会自动将此声明提升到顶端,所以想实现块作用域在ES5中是通过闭包的形式。

(function(param) {})(i);

同时ES6也支持了块级作用域的函数

3.一种function的新写法

function中支持this的使用

nums.forEach(function(v) {
    return foo(v);
});

nums.forEach(v => {
    foo(v)
});

4.undefined参数默认配置

ES6:

function(x, y = 7, z = 10) {
    return x + y + z;
}

等效于ES5:

function(x, y, z) {
    if (y === undefined) {
        y = 7;
    }
    if (z === undefined) {
        z = 10;
    }
    return x + y + z;
}

5.支持剩余参数写法

即...p写法会被解释为

p.prototype.slice

ES6:

function f (x, y, ...a) {
    return (x + y) * a.length
}
f(1, 2, "hello", true, 7) === 9

ES5:

function f (x, y) {
    var a = Array.prototype.slice.call(arguments, 2);
    return (x + y) * a.length;
};
f(1, 2, "hello", true, 7) === 9;

6.可以直接在字符串中加入${表达式}

即console.log("Error: ${e.toString()}");

7.引用

//lib/math.js
export function sum(x, y) {return x + y};
export var pi = 3.1415926;

//app.js
import * as math from "lib/math"
console.log(math.sum(math.pi, math.pi););

8.支持class 继承,setter、getter

class Shape {
    constructor (id, x, y) {
        this.id = id;
        this.move(x, y);
    }
    move (x, y) {
        this.x = x;
        this.y = y;
    }
}
class Rectangle extends Shape {
    constructor (id, x, y, width, height) {
        super(id, x, y);
        this.width  = width;
        this.height = height;
    }
    set width(width) {this.width = width}
    get width()      {return this.width}
}
class Circle extends Shape {
    constructor (id, x, y, radius) {
        super(id, x, y);
        this.radius = radius;
    }
}

9.支持Promise Generators(待学习)

10.bufferArray

class Example {
    constructor (buffer = new ArrayBuffer(24)) {
        this.buffer = buffer
    }
    set buffer (buffer) {
        this._buffer    = buffer
        this._id        = new Uint32Array (this._buffer,  0,  1)
        this._username  = new Uint8Array  (this._buffer,  4, 16)
        this._amountDue = new Float32Array(this._buffer, 20,  1)
    }
    get buffer ()     { return this._buffer       }
    set id (v)        { this._id[0] = v           }
    get id ()         { return this._id[0]        }
    set username (v)  { this._username[0] = v     }
    get username ()   { return this._username[0]  }
    set amountDue (v) { this._amountDue[0] = v    }
    get amountDue ()  { return this._amountDue[0] }
}

let example = new Example()
example.id = 7
example.username = "John Doe"
example.amountDue = 42.0