今天看啥  ›  专栏  ›  搬砖的码农

iOS混合开发库(GICXMLLayout)七、JavaScript篇

搬砖的码农  · 掘金  ·  · 2019-01-04 03:54
阅读 3

iOS混合开发库(GICXMLLayout)七、JavaScript篇

介绍

GIC从0.3.0版本开始正式支持JavaScript,也就意味你可以直接使用JavaScript来写业务逻辑,至此开始,结合XMLjs文件图片资源等静态文件,完全可以将整个的APP做成一个可以热更新的应用。另外,在开发的时候也可以通过HotReload的方式,无需编译整个APP就能实时刷新应用,进一步的加快应用的开发效率。

在最新的0.4.8版本中,GIC已经支持大部分的ES6特性,包括但不限于yieldgeneratorPromise等等,并且也支持了ES8中的asyncawait

GIC在最初的架构中根本就没有考虑对JavaScript提供支持,数据绑定事件绑定等等功能统统都是为native code设计的。而后来当我考虑想要对JavaScript进行支持的时候,也仅仅是通过behavior来实现。事实上,如果你看过GIC的源码,你会发现GIC可以实现对任意脚本的支持,而实现的方式也仅仅只是通过自定义behavior来实现。从这一点来说,侧面反映了behavior功能的强大之处。

GIC对于JavaScript的支持是基于JavaScriptCore这个苹果官方framework实现的,其实这个framework本来就是开源的,而且还是属于大名鼎鼎的Webkit其中的模块。再加上Webkit的跨平台能力,也就意味着只要是基于JavaScriptCore开发的功能,同样可以移植到安卓上面,也就是意味着GIC在对JavaScript的支持上具备了跨平台的潜力

对ES6的支持

首先各位要知道的是,iOS对于Es6规范的支持程度在不同的iOS版本中是不同的,iOS8对于ES6是完全不支持的,iOS9部分支持,最新的iOS12基本已经完全支持了ES6的规范。当然,这里就不列出不同的iOS版本具体支持哪些ES6特性,你只需要知道,不同的iOS版本对于不同的ES6规范支持程度不一就行了。

然而我们的app肯定是要运行在低版本上的,那么如何解决这个问题?

事实上,GIC本身并没有提供原生的解决方案,虽然也尝试过内置babel来实现实时转码,但是后来发现性能太差,对于复杂的JS文件进行转码会耗费大量的CPU资源。因此就断了内置babel的想法。

但是GIC通过为VSCode开发插件的方法,曲线实现了对ES6的完整支持,VSCode的插件(GICVSCodeExtension)可以一次性将整个工程的JS代码编译成ES5的代码。也就意味你可以放心的在你的工程中编写ES6的代码,然后通过VSCode进行编译,进而使得你的JS代码可以运行在不同的iOS版本上。

事实上,VSCode插件也是通过babel来转码的,并且GIC也自定义了babel插件。

你只需要通过GIC提供的脚手架来创建项目就能获得这样的能力,详细的脚手架以及IDE支持的介绍可以查看这篇文章

在最新的0.4.8版本中,新增了对yieldgeneratorPromiseasyncawait的支持。

require

GIC中,打开一个新页面相当于在浏览器中打开一个新页面,页面跟页面之间并不能共享JavaScript执行环境(JSContext),每个页面都有一个独立的sand box(JSContext)。因此,当一个页面需要某个功能的时候你需要通过require方法手动的将这个功能引入,每个页面都是如此,同一个功能如果在不同的页面中使用,那么意味着你需要在不同的页面单独引入。

当前require的功能更多的类似Nodejs的用法,如果你以前接触过nodejs的话,那么很容易理解。

其实,GIC提供的require函数相较于Nodejs中的require函数来说只是一个简化版的实现,功能比较简单。

使用module.exports来导出,使用require函数来导入,当前require函数支持jsjson文件的导入。

比如: 在a.js中定义

class ClassA {
}
module.exports = ClassA;
复制代码

b.js中引用

const ClassA = require('/js/a.js');
复制代码

或者一次性导出多个,比如: 在a.js中定义

class ClassA {
}
class ClassB extends ClassA {
}
class ClassC {
}
module.exports = { ClassA, ClassB, ClassC };
复制代码

b.js中引用

const { ClassA, ClassB, ClassC } = require('/js/a.js');
复制代码

以上用法,写过node.js的都很容易明白。唯一的区别就是,GIC中的路径全部是绝对路径。但是如果您是使用VSCode开发的话,那么可以借助插件自动将相对路径编译成绝对路劲

起初设计require函数实现的时候并没有参考nodejs,但是后来发现功能实在太弱,无法实现模块化,然后又重新设计了require的实现,但是神奇的是,等我写完以后才发现,这他喵的不就是nodejs中的require嘛!如果你用VSCode开发,那么你会发现,当你使用require导入JS文件后,VSCode竟然也能准确的识别,并且提供了完整的智能提示功能。

引入第三方库

GIC不支持npm包管理工具,如果你想在工程中使用第三方库的话只能直接将JS文件引入的方式来使用。拿axios举例

  1. 下载axios的JS文件。

    地址如下:unpkg.com/axios@0.18.…

  2. 将下载的JS文件拷贝到工程的js文件夹下面。
  3. 使用require函数以非module模式引入axios,
    require('./axios.js', false);
    复制代码
  4. 直接在代码中使用
    axios.get('https://www.sojson.com/open/api/lunar/json.shtml')
        .then((response) => {
            console.log(JSON.stringify(response));
        })
        .catch(function (err) {
            console.log(err);
        });
    复制代码

其他的第三方库都可以采用这样的方式引入。但是需要注意的是,第三方库可能用到了GIC本身不支持的API,这时候就需要你自己以JSAPI扩展的方式来提供支持了。

调试

很遗憾,当前由于JavaScriptCore的限制,GIC并不具备JS调试的能力。目前调试手段只能是通过console.log来追踪,更进一步的是通过直接打印调用堆栈的方式来实现方法调用追踪,但是调用堆栈的信息在JSContext中并不详细。

另外,你也可以通过safari来查看某个JSContext对象,来查看JSContext的内容。

目前的调试手段有限,而作者也在研究如何配合VSCode实现调试功能,不过目前进展不大。如果有哪位大能之士有方案的话还请告知下。

JSAPI扩展。

上面已经提过,GICJavaScript的支持,是通过JavaScriptCore来实现的。具体一点是通过JSContextJSValue来实现的。然而JavaScriptCore本身提供的API是有限的,比如consoleXMLHttpRequestsetTimeout等API是没有的,只能通过扩展来实现。GIC已经提供了一些JavaScriptCore所没有的API,而其他的API就需要你自己来实现了。

然而对于目前很多从npm安装的库来说,很遗憾,GIC不支持。但是你可以使用直接加载js文件的方式来引入您的工程,但是注意这些库有可能用到了其他的一些api,那么这时候就需要你自己扩展实现这些API以便提供支持。

其实对于第三方库的支持已经跟GIC本身没有关系了,完全可以通过扩展+后期编译的方式来实现支持。不过这样的工程会比较大,你不可能做到对所有库的支持,只能针对特定库做有限的支持。

JSValue注意事项。

在实际的项目开发过程中,免不了要自定义JSAPI的扩展,而实现JSAPI的扩展那么你就回避不了对JSValue的使用,但是JSValue还是有些地方需要注意的,否则会为你的代码埋下内存管理的隐患。

各位在看这篇博客之前如果没有接触过JavaScriptCore相关的内容,我还是建议各位先去了解下JavaScriptCore,尤其是里面的JSContextJSValue

JSValue的最大问题就在于内存管理的问题。

JS和OC在内存管理方面是不一样的,JS的内存管理机制被叫做垃圾回收,而OC的内存管理是基于引用计数的,因此,这两种语言如果想要实现互调,那么必须得解决内存管理的问题。而JSValue就是干这个事的,但是千万不要以为只要使用了JSValue就万事无忧了。

JSValue是OC的对象,需要遵循引用计数的内存管理机制,而JSValue指向的JS对象却是垃圾回收的,如果你在代码中直接将JSValue作为变量保存下来那么等待你的有可能就是循环引用。这时候为了解决循环引用的问题,就需要JSManagedValue出场了,JSManagedValue专门为了解决这个问题的,即能让你拥有JS对象,也不会引起循环引用的问题,可以理解为OC对JSValue的弱引用。

如果出现了循环引用,系统有可能报Attempting to release access but the mutator does not have access这样的错误,这时候App会直接崩溃。那么这时候就需要检查你的代码中是否循环引用了JSValue

RootDataContext

如果要在XML中直接写JS代码,这里几个概念需要注意的。具体参考文档

RootDataContext--根数据源,也即是你在一个页面中第一个设置的数据源。比如:

<page title="Home">
    <behaviors>
        <script path="./Home.js" />
        <script private="true">
            $el.dataContext = new Home();
        </script>
    </behaviors>
</page>
复制代码

上面的JS代码$el.dataContext = new Home();就是为page元素设置数据源,并且这时候,RootDataContext就指向Home的实例

在实际的开发过程中你不会直接接触RootDataContext,而是通过事件绑定等形式间接的接触。你可以在数据绑定的表达式事件表达式中通过this指针来访问。比如:

<lable text="按钮" event-tap="js:this.onClicked()"/>
复制代码

这样,在tap事件中,绑定了一个JS回调,this.onClicked()指向classHomeonClicked方法。

而有的时候你可能需要直接在事件回调中修改元素的属性,那么可以通过$el来访问该元素本身,然后设置属性。比如:

<lable text="按钮" event-tap="js:$el.text = 'i clicked'"/>
复制代码

这样,当该lable被点击的时候,文本内容就会被修改成i clicked

事实上,上面在为page设置数据源的时候,就是通过$el来设置的。

yield、generator

GIC在最新的版本中已经提供了对yieldgenerator的支持,而且generator是由native code开发的,并不是babel转码支持的,babel转码只能提供对yield的支持,但是generator转码过后就无法在iOS上运行了,因此我参考了generator的API,以JSAPI扩展的方式,用objective-c实现了generator整套API。实现过程我后面准备单写一篇博客来介绍如何实现。

GIC也已经提供了对Promise的支持,因此在Promiseyield的基础上,提供对asyncawait支持就变得顺理成章了。事实上,GIC已经支持ES8中的asyncawait功能了。

数据绑定

GIC本身是支持数据绑定的,在引入JavaScript后也提供了数据绑定的功能。但是JS的数据绑定跟naitve coed的数据绑定在实现方式上是不一样的,GIC在实现JS的数据绑定过程中,充分参考了VUE中的实现方式,并且研究了VUE的源码,最终将VUE的实现方式移植到了GIC中。可以参考这篇文章




原文地址:访问原文地址
快照地址: 访问文章快照