跨端开发面面谈之NativeScript从入门到放弃
在《跨端开发面面谈之基于WebView的Hybrid开发模式》一文总结了基于WebView的混合开发模式的发展路线和在各个方向的优化方式,简单介绍了基于这一模式的Ionic框架的应用和工具生态。本文继续跨端开发系列,谈一谈对NativeScript开发框架的理解。由于没有在生产环境、复杂应用上深入使用NativeScript,所以文中观点可能不够准确,更多的,是作为一个有经验的开发者,接触之后,结合一些文章的阅读,一点个人的理解。
从WebView到Native
基于WebView的混合开发模式在国内外得到广泛应用,开发者深入各个方向对其进行了各种优化,输出了很多最佳实践。大家想尽办法想要提升体验、抹平差异,是因为我们将Web App运行在WebView容器中,与Native App天然存在性能差距。此时便出现了新的思路,依然保持Web技术栈的开发体验,内置一个运行时环境,通过映射,使得最终用户操作的是原生视图,以此提供一个 接近 原生体验的高性能应用。
NativeScript是我了解的第一个实现这一思路的框架,采用这一框架,开发者在应用开发时采用Web技术栈,而构建出的App均为原生视图。自此,在 理论上 打破了WebView性能天花板,跨端开发技术从WebView迈向了Native侧。
NativeScript Hello World
NativeScript支持以多种前端技术栈来开发应用,最初其支持 model view view model 这一MVVM模式,以原生JavaScript业务逻辑/xml视图模板/css样式模式开发,后来引入对Angular 2的支持并持续跟进Angular的迭代,当前社区也提供了以Vue进行开发的支持。
不引入其他前端框架时,demo结构通常包括xml模板、JavaScript模型及视图模型、以及样式文件。例如,一个登录页面的xml模板可能如下:
<Page loaded="loaded">
<Page.actionBar>
<ActionBar title="Sign in"></ActionBar>
</Page.actionBar>
<StackLayout orientation="vertical">
<Image src="~/images/logo.png" stretch="none" horizontalAlignment="center" ></Image>
<TextField hint="Email Address" id="email" text="{{ email }}" keyboardType="email" autocorrect="false" autocapitalizationType="none"/>
<TextField hint="Password" text="{{password}}" secure="true"/>
<Button text="Sign in" tap="signIn"/>
<Button text="Sign up for Groceries" class="link" tap="register"/>
</StackLayout>
</Page>
对应的JavaScript模型及视图模型代码负责处理业务逻辑和数据绑定:
var frameModule = require('ui/frame');
var UserViewModel = require('../../shared/view-models/user-view-model')
var dialogsModule = require('ui/dialogs');
var user = new UserViewModel()
var page;
var email;
exports.loaded=function (args) {
page=args.object;
page.bindingContext = user;
}
exports.signIn = function () {
user.login()
.catch(function (error) {
dialogsModule.alert({
message:'Unfortunately we could not find your account',
okButtonText:'OK'
})
})
.then(function () {
frameModule.topmost().navigate("views/list/list")
})
}
exports.register=function () {
var topmost = frameModule.topmost();
topmost.navigate('views/register/register');
}
var config = require("../../shared/config");
var fetchModule = require("fetch");
var observableModule = require("data/observable");
function User(info) {
info = info || {};
var viewModel = new observableModule.fromObject({
email: info.email || "",
password: info.password || ""
});
viewModel.login = function() {
return fetchModule.fetch(config.apiUrl + "user/" + config.appKey + "/login", {
method: "POST",
body: JSON.stringify({
username: viewModel.get("email"),
password: viewModel.get("password")
}),
headers: getCommonHeaders()
})
.then(handleErrors)
.then(function(response) {
return response.json();
})
.then(function(data) {
config.token = data._kmd.authtoken;
});
};
viewModel.register = function() {
return fetchModule.fetch(config.apiUrl + "user/" + config.appKey, {
method: "POST",
body: JSON.stringify({
username: viewModel.get("email"),
email: viewModel.get("email"),
password: viewModel.get("password")
}),
headers: getCommonHeaders()
}).then(handleErrors)
};
return viewModel;
}
function getCommonHeaders() {
return {
"Content-Type": "application/json",
"Authorization": config.appUserHeader
}
}
function handleErrors(response) {
if (!response.ok) {
console.log('error')
console.log(JSON.stringify(response));
throw Error(response.statusText);
}
return response;
}
module.exports = User;
样式文件(如login.css)则定义组件的外观:
TextField {
border-width: 5;
border-style: solid;
border-color: #034793;
}
Image {
margin: 10;
}
Button {
margin: 10;
padding: 10;
}
从上述代码可以看到,视图模板中使用了NativeScript封装的组件系统,包括各式布局组件和具体视图组件。在view model与view之间,框架提供了对事件绑定和数据绑定的处理。在view model与model之间,框架也提供了一些工具方法,提供了异步请求和数据更新的支持。
NativeScript也提供对Angular的支持,一个完整的NativeScript Angular示例,推荐阅读《使用NativeScript和Angular2构建跨平台APP》,文中使用Angular和NativeScript实现了一个iOS计算器。
NativeScript Inside
NativeScript运行机制的核心包括NativeScript Runtimes、Core Modules、CLI和Plugins。结合其内部实现原理,NativeScript一大卖点是其号称可以 直接获取全部的系统及第三方模块的公共API的调用。以Android为例,这是通过反射机制实现的。在通过CLI命令行工具构建应用时,框架会通过反射获取所有原生公共API,将这些信息以二进制结构作为元数据信息打进应用包。当JavaScript端调用这些API时,Runtime会根据元数据信息反射调用相应API。
基于NativeScript Runtime获取Native API Access,NativeScript提供了Core Modules,这是框架提供的抽象层,包含了对视图、系统API、布局的统一抽象,其 write once, run anywhere 的核心正是基于这一跨平台抽象层的封装实现。
当已有Module不能满足应用的个性化需求时,NativeScript支持Plugins扩展。你可以选择使用社区第三方plugin或者实现自己的plugin。plugin可以是对已有组件的组合包装,也可以是对原生组件的自定义封装。
Bye,NativeScript
看上去很美,是我在初了解NativeScript后的感觉。对于前端开发者来说,无需切换技术栈,任意挑选原生JavaScript/Angular/Vue,利用框架组件库和API封装,快速开发出高性能跨平台应用,感觉特别美好。但继续深入了解其核心,在美好背后,在我看来,NativeScript主要有下面三个问题。
write everything in JavaScript是伪命题
NativeScript提出的重要卖点,write everything in JavaScript。在跨终端混合应用开发过程中,始终使用你精通的JavaScript,无需学习Java/Object-C,听起来确实有吸引力。但是,当你开始真正在实际项目中使用它,复杂度超过Hello World demo时,你一定会遇到某些个性化特性,框架没有帮你实现,社区中也没有已有实现(以NativeScript社区活跃度来看,这个概率很大),这个时候该怎么办呢?NativeScript支持plugin插件机制,并且要求 in JavaScript。
假设你需要扩展一个功能更复杂的ListView,参考官方ListView的实现,可以看到在Android和iOS平台分别用JavaScript编写了原生逻辑。例如,在Android平台,createNativeView 方法中直接使用Android API创建ListView;在iOS平台,构造函数中设置UITableView属性。这种用JavaScript实现原生代码逻辑的方式,对于前端开发者来说,并不友好,因为需要熟悉原生平台的API。而让各平台客户端开发者以JavaScript来实现这一逻辑,又要让他们了解JavaScript写法,并且代码可调试性不足,这样非但不能提升生产力,还会起到相反效果。
即使在普通业务开发中,也可能遇到这一问题。以前文提到的计算器应用为例,为实现结果展示区域文字自适应缩放,在展示组件中有大段用JavaScript调用原生API的代码,例如在Android平台基于Canvas计算文字宽度,在iOS平台使用特定属性。这种代码写在业务中,增加了复杂度。
在我看来,在一个混合应用开发过程中,必然有对原生开发部分的需求,这一部分如果让前端开发者来完成,就必然需要前端开发者学习原生技术栈;如果让原生开发者来完成,也没有问题。但是这一依赖应该放在原生端实现,再以其他方式扩展给JavaScript调用即可。像这样,以JavaScript来实现两套原生逻辑,再封装调用方式,以此为卖点,个人实在接受不了。
run JavaScript on the main UI thread可能是炸弹
《The Benefits of NativeScript’s Single Threading Model》一文解释了NativeScript的线程模型,框架选择将JavaScript放在UI线程运行,主要是为了高效暴露所有原生API给JavaScript,并且许多常见异步场景已通过插件处理。简单来说,这一模式去掉其他bridge,让JavaScript在UI线程同步执行,可以带来更高的性能。
但是,这种模式无法避免开发者在代码中埋下性能炸弹。例如,如果在JavaScript中写入一个长时间循环,它会直接阻塞UI线程,导致应用无响应。相比将JavaScript运行在独立线程,这一模式被这种问题影响的风险更大。
Integrating existing native apps支持不足
React Native面世时的火爆,部分因为其便捷的与已有App集成的能力。而NativeScript在其推出时,是一个非此即彼的排他性框架。从1.0迭代到3.0时,还不支持与原生应用集成,到现在4.0版本,可以集成进原生应用中。但是,查看文档,在原生应用集成方面,文档不太完善。集成的复杂程度高于React Native,从已有资料来看,其对这一块的支持刚刚开始,还不完善。
最后
NativeScript问世已久,一直没有大火。去年React专利风波时,知乎上有过安利,但一阵风过,好像没有下文。我本以为是大家没有发现这块良玉,一番探索后,决定从入门到放弃。当然我也没有在生产环境长期使用过,所以我的感受可能都是错的,仅从个人体验,不推荐使用这一框架。而如果你实在偏爱Angular,推荐Ionic了解一下;偏爱Vue,Weex了解一些。各路阵营都有其他方案,实在不必踩这里的坑。