代码区项目交易流程

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


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文件。

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

将下载的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 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() 指向class Home 的 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 中。可以参考这篇文章

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

点击收藏

LAST 微信 - 如何判断朋友圈分享成功 (译)函数式组件在Vue.js中的运用 NEXT