Sketch插件如何架构?

Sketch插件跟我们平时用得比较多的Chrome插件类似,都是可以调用容器提供的API来实现各种个样的能力。

Sketch的强大之处在于,它可以调用OSX系统级别的API。这个也就是我之前的文字提到过(《用CMake打包一个Sketch插件FrameWork》),为什么需要开发Sketch插件的时候需要研发一套动态库来支持能力拓展。

本文不是针对新手Sketch插件开发者的入门,需要了解架构原理,需要一些Sketch插件开发的基础。

技术选型

native or webview

native有优势主要体现在交互体验好,webview则是学习曲线平滑,开发迭代效率快。

对比 native webview
体验好
快速迭代
学习曲线低
热更新

作为一个产品的架构师,我们需要从3个方面去分析产品的架构生命周期:

  1. 体验: 良好的产品体验可以帮我们提升用户的粘性。
  2. 开发效率:快速迭代抢先占领市场才能抢得先机。
  3. 维护成本:产品上线,人员流动也是市场规律,需要保证新成员能快速上项目。

UiwebView or WkWebView

webview的好处前面我们已经分析过了。具体是选项 UiwebView 还是 WkWebView,我们也需要斟酌下。 UiwebView带给我们的好处主要是开发模式简单,同步的开发流程让架构设计更简单。

const webViewClass = {
  'webView:didCreateJavaScriptContext:forFrame:': (view) => {
      view.windowScriptObject().setValue_forKey(wrap({
        getName() {
          return 'zobor'
        },
      }), 'Demo');
  },
}

JSAPI的用户则是这样:

window.Demo.getName();

WkWebView才是未来,苹果推出它就是为了接替UiwebView的:

WkWebView推出Delegate模式:

const WebScriptHandlerClass = {
  'userContentController:didReceiveScriptMessage:': (_, msg) => {
    const jsonData = NSJSONSerialization.dataWithJSONObject_options_error(
      msg.body(), 0, null,
    );
    const nsstring = NSString.alloc().initWithData_encoding(jsonData, NSUTF8StringEncoding);
    const webParams = JSON.parse(`${nsstring}`);
    console.log(webParams);
  },
};

const wkwebviewConfig = WKWebViewConfiguration.alloc().init();
const messageDelegate = ObjCClass(WebScriptHandlerClass).new();

const webView = WKWebView
	.alloc()
	.initWithFrame_configuration(
		CGRectMake(0, 0, 300, 600),
		wkwebviewConfig,
	);

webView.configuration()
  .userContentController()
  .addScriptMessageHandler_name(messageDelegate, 'DemoMessage');

然后我们的JSAPI就是这样的:

window.webkit.messageHandlers.DemoMessage.postMessage({
	name: 'zobor',
});

postMessage就有个问题了,WkWebView内核该怎么回调webview呢? 我们是通过webView.evaluateJavaScript_completionHandler(script, nil);来实现的,有点像jQuery的Ajax方法回调函数的写法。

JSAPI的设计

内核和UI交互搭建好了,接下来需要提供接口能力。 例如如先实现一下获取Sketch的文档ID的接口: api-document.js

const Sketch = require('sketch');

const DocumentAPI = {
  getDocuments() {
    const docs = Sketch.getDocuments();
    const ret = [];
    docs.forEach((doc) => {
      if (doc.sketchObject) {
        ret.push(doc.sketchObject);
      }
    });
    return ret;
  },
  getDocument() {
    return this.getDocuments()[0];
  },
  getDocumentID() {
    return `${this.getDocument().documentData().objectID()}`;
  },
};

API定义好了,接下来需要跟WKWebView的ScriptMessage提供映射.

const DocumentAPI = require('./api-document');
const WebScriptHandlerClass = {
  'userContentController:didReceiveScriptMessage:': (_, msg) => {
    const jsonData = NSJSONSerialization.dataWithJSONObject_options_error(
      msg.body(), 0, null,
    );
    const nsstring = NSString.alloc().initWithData_encoding(jsonData, NSUTF8StringEncoding);
    const webParams = JSON.parse(`${nsstring}`);
    console.log(webParams);

    const { apiName, apiParams, callbackFn } = webParams;
    const rs = DocumentAPI[apiName](apiParams);
    const script = NSString.stringWithFormat('window.%@(%@)', callbackFn, rs);
    webView.evaluateJavaScript_completionHandler(script, nil);
  },
};

能力拓展

JS API的问题也是比较明显的,Sketch官方的JS API能力比较有限,比如我们经常会用到的curl,需要我们自己去实现。 从WKWebView到Sketch JSAPI,再到MacOs系统接口,我们的架构能力是完善了。 怎么写Sketch的插件拓展? 首先思路是通过Objective-C or Swift去编译一个FrameWork库,然后Sketch Plugin可以去Load这个库。

const loadFramework = _mocha.loadFrameworkWithName_inDirectory_('Demo', frameworkPath);
// 加载成功,会在Sketch的context上注册一个对象,对象名称是OC 定义的Class
// 例如:DMOHttp,有一个get 方法
const data = DMOHttp.get(url);
console.log(data);

总结

WKWebview+JSAPI的架构带来的好处是快速迭代 + 高效的开发效率,对此架构模式感兴趣的朋友可以联系我一起探讨.