介绍
GIC从0.3.0版本开始正式支持JavaScript,也就意味你可以直接使用JavaScript来写业务逻辑,至此开始,结合XML、js文件、图片资源等静态文件,完全可以将整个的APP做成一个可以热更新的应用。另外,在开发的时候也可以通过HotReload的方式,无需编译整个APP就能实时刷新应用,进一步的加快应用的开发效率。
在最新的0.4.8版本中,GIC已经支持大部分的ES6特性,包括但不限于yield、generator、Promise等等,并且也支持了ES8中的async、await
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版本中,新增了对yield、generator、Promise、async、await的支持。
require
在GIC中,打开一个新页面相当于在浏览器中打开一个新页面,页面跟页面之间并不能共享JavaScript执行环境(JSContext),每个页面都有一个独立的sand box(JSContext)。因此,当一个页面需要某个功能的时候你需要通过require方法手动的将这个功能引入,每个页面都是如此,同一个功能如果在不同的页面中使用,那么意味着你需要在不同的页面单独引入。
当前require的功能更多的类似Nodejs的用法,如果你以前接触过nodejs的话,那么很容易理解。
其实,GIC提供的require函数相较于Nodejs中的require函数来说只是一个简化版的实现,功能比较简单。
使用module.exports来导出,使用require函数来导入,当前require函数支持js和json文件的导入。
比如: 在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举例
- 下载axios的JS文件。
- 将下载的JS文件拷贝到工程的js文件夹下面。
- 使用require函数以非module模式引入axios,
require('./axios.js', false); - 直接在代码中使用
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扩展。
上面已经提过,GIC对JavaScript的支持,是通过JavaScriptCore来实现的。具体一点是通过JSContext、JSValue来实现的。然而JavaScriptCore本身提供的API是有限的,比如console、XMLHttpRequest、setTimeout等API是没有的,只能通过扩展来实现。GIC已经提供了一些JavaScriptCore所没有的API,而其他的API就需要你自己来实现了。
然而对于目前很多从npm安装的库来说,很遗憾,GIC不支持。但是你可以使用直接加载js文件的方式来引入您的工程,但是注意这些库有可能用到了其他的一些api,那么这时候就需要你自己扩展实现这些API以便提供支持。
其实对于第三方库的支持已经跟GIC本身没有关系了,完全可以通过扩展+后期编译的方式来实现支持。不过这样的工程会比较大,你不可能做到对所有库的支持,只能针对特定库做有限的支持。
JSValue注意事项。
在实际的项目开发过程中,免不了要自定义JSAPI的扩展,而实现JSAPI的扩展那么你就回避不了对JSValue的使用,但是JSValue还是有些地方需要注意的,否则会为你的代码埋下内存管理的隐患。
各位在看这篇博客之前如果没有接触过JavaScriptCore相关的内容,我还是建议各位先去了解下JavaScriptCore,尤其是里面的JSContext和JSValue。
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>
</behaviors>
</page>
上面的JS代码$el.dataContext = new Home();就是为page元素设置数据源,并且这时候,RootDataContext就指向Home的实例。
在实际的开发过程中你不会直接接触RootDataContext,而是通过事件、绑定等形式间接的接触。你可以在数据绑定的表达式、事件表达式中通过this指针来访问。比如:
<lable text="按钮" event-tap="js:this.onClicked()"/>
这样,在tap事件中,绑定了一个JS回调,this.onClicked()指向classHome的onClicked方法。
而有的时候你可能需要直接在事件回调中修改元素的属性,那么可以通过$el来访问该元素本身,然后设置属性。比如:
<lable text="按钮" event-tap="js:$el.text = 'i clicked'"/>
这样,当该lable被点击的时候,文本内容就会被修改成i clicked。
事实上,上面在为
page设置数据源的时候,就是通过$el来设置的。
yield、generator
GIC在最新的版本中已经提供了对yield、generator的支持,而且generator是由native code开发的,并不是babel转码支持的,babel转码只能提供对yield的支持,但是generator转码过后就无法在iOS上运行了,因此我参考了generator的API,以JSAPI扩展的方式,用objective-c实现了generator整套API。实现过程我后面准备单写一篇博客来介绍如何实现。
GIC也已经提供了对Promise的支持,因此在Promise和yield的基础上,提供对async、await支持就变得顺理成章了。事实上,GIC已经支持ES8中的async、await功能了。
数据绑定
GIC本身是支持数据绑定的,在引入JavaScript后也提供了数据绑定的功能。但是JS的数据绑定跟naitve coed的数据绑定在实现方式上是不一样的,GIC在实现JS的数据绑定过程中,充分参考了VUE中的实现方式,并且研究了VUE的源码,最终将VUE的实现方式移植到了GIC中。可以参考这篇文章