跨端开发面面谈之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模板如下

<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 model及view model为

//login.js
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');
}

//user-view-model.js
var config = require("../../shared/config");
var fetchModule = require("fetch");
var observableModule = require("data/observable");

function User(info) {
    info = info || {};

    // You can add properties to observables on creation
    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 calculator。

NativeScript Inside

NativeScript运行机制如下图所示,其核心包括NativeScript Runtimes, Core Modules, CLI, Plugins。


结合上图,深入看下NativeScript内部实现原理。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 run JavaScript on the main UI thread
  • Integrating existing native apps

write everything in JavaScript是伪命题

NativeScript提出的重要卖点,write everything in JavaScript。在跨终端混合应用开发过程中,始终使用你精通的JavaScript,无需学习Java/Object-C,挺起来确实有吸引力。但是,总是有但是,当你开始真正在实际项目中使用它,复杂度超过Hello World demo时,你一定会遇到某些个性化特性,框架没有帮你实现,社区中也没有已有实现(已NativeScript社区活跃度来看,这个概率很大),这个时候该怎么办呢?NativeScript支持plugin插件机制,并且,in JavaScript

假设你需要扩展一个功能更复杂的ListView,让我们先参考学习官方ListView的实现。


在node_modules/tns-core-modules/ui/路径下可以看到list-view代码实现,其结构如上图所示,.d.ts中描述结构定义,.common.js中定义通用逻辑,在.android.js、.ios.js中,分平台实现平台特有逻辑。

ListView.prototype.createNativeView = function () {
        initializeItemClickListener();
        var listView = new android.widget.ListView(this._context);
        listView.setDescendantFocusability(android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS);
        listView.setCacheColorHint(android.graphics.Color.TRANSPARENT);
        ensureListViewAdapterClass();
        var adapter = new ListViewAdapterClass(this);
        listView.setAdapter(adapter);
        listView.adapter = adapter;
        var itemClickListener = new ItemClickListener(this);
        listView.setOnItemClickListener(itemClickListener);
        listView.itemClickListener = itemClickListener;
        return listView;
    };
function ListView() {
        var _this = _super.call(this) || this;
        _this.widthMeasureSpec = 0;
        _this.nativeViewProtected = _this._ios = UITableView.new();
        _this._ios.registerClassForCellReuseIdentifier(ListViewCell.class(), _this._defaultTemplate.key);
        _this._ios.estimatedRowHeight = DEFAULT_HEIGHT;
        _this._ios.rowHeight = UITableViewAutomaticDimension;
        _this._ios.dataSource = _this._dataSource = DataSource.initWithOwner(new WeakRef(_this));
        _this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(_this));
        _this._heights = new Array();
        _this._map = new Map();
        _this._setNativeClipToBounds();
        return _this;
    }

当我分别在list-view.android.js、list-view.ios.js中看到上述代码时,这种美好的感觉不再有了。我开始问自己,我是谁、我在哪、我在干什么?
NativeScript可以获取native API,基于这一特性,其plugin扩展的逻辑,是让你用JavaScript实现原生代码逻辑。那么问题来了,这真的是为前端开发者准备的吗?因为,作为前端开发者的我,对原生平台的API,是真的做不到这么熟悉的。而让各平台客户端开发者以JavaScript来实现这一逻辑,又要让他们了解JavaScript写法,并且,这一代码可调试性不足,这样非但不能提升生产力,还会起到相反效果。

即使不是封装自定义组件,在普通业务开发中,也有可能遇到这一问题。以前文提到的使用NativeScript和Angular2构建跨平台APP为例,在这个计算器应用中,为实现结果展示区域文字自适应缩放,在展示组件displayer中有如下代码

ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
    //Android Only
    if(device.os !== platformNames.android) return;
    for (let propName in changes) {
      let chng = changes[propName];
      let cur  = JSON.stringify(chng.currentValue);
      let prev = JSON.stringify(chng.previousValue);
      console.log('current ' + cur + ' previous ' + prev);
      if(this.displayer && this.wrapper) {
       let paint:any = this.displayer.android.getPaint();
       let textWidth:number = paint.measureText(cur);
       let wrapperWidth:number = this.wrapper.android.getWidth();
       if(Math.abs(wrapperWidth - textWidth) < 50 && this.currentFont > Displayer.MIN_FONT) {
          this.currentFont -= 10;
       }
       if(Math.abs(wrapperWidth - textWidth) > wrapperWidth / 2) {
         this.currentFont = Displayer.MAX_FONT;
       }
      this.displayer.android.setTextSize(this.currentFont);
      }
    }
  }

  setIOSLabelAutoFont(elem: Label) {
    if(device.os === platformNames.ios){
      elem.ios.numberOfLines = 1;
      elem.ios.minimumFontSize = Displayer.MIN_FONT;
      elem.ios.adjustsFontSizeToFitWidth = true;
    }
  }

上述代码中,在iOS平台,使用了adjustsFontSizeToFitWidth属性,而安卓平台没有这一属性,为了实现这一效果,在安卓平台基于canvas计算文字实际渲染宽度,再计算缩放字号。这种大段以JavaScript来完成原生逻辑的代码写在业务代码中,看的实在是难受。
另外,文中提到如下内容,这是不对的。直接实现四个字该如何定义,上述NativeScript的实现算不算直接实现?在React Native中,一样可以引入Canvas组件,先计算出文字实际渲染宽度,再根据缩放比例完成文字缩放。

这个交互,在 ReactNative 中,是无法直接实现的! 这也是 NativeScript 相比于 ReactNative 的强大之处。

在我看来,在一个混合应用开发过程中,必然有对原生开发部分的需求,这一部分如果让前端开发者来完成,就必然需要前端开发者学习原生技术栈,这没有问题。这一部分如果让原生开发者来完成,也没有问题。但是这一部分对原生端的依赖,应该放在原生端实现,再以其他方式直接扩展给JavaScript调用即可,像这样,以JavaScript来实现两套原生逻辑,再封装调用方式,以此为卖点,个人实在是接受不了。

run JavaScript on the main UI thread可能是炸弹

The Benefits of NativeScript’s Single Threading Model一文,解释了NativeScript的线程模型,说明了框架选择将JavaScript放在UI线程运行的考虑,主要有下面两点

  • Running app code on the UI thread by default allows NativeScript to expose all native APIs to app JavaScript in an efficient and high performing way.
  • Many common scenarios that require background thread processing are already covered by NativeScript plugins and modules.

简单来说,这一模式去掉其他bridge,让JavaScript在UI线程同步执行,可以带来更高的性能,一些常见的需要异步处理场景,框架本身通过扩展机制已经帮你在其他线程完成了处理。

那么,如果一个开发者在JavaScript代码中写下如下代码呢?

while(i<100000000){}

run JavaScript on the main UI thread去掉了异步通信过程,的确能带来更高的性能,但是这种模式,无法避免开发者在代码中埋下的炸弹,相比将JavaScript运行在独立线程,这一模式被这种炸弹毁掉的概率大得多。

Integrating existing native apps支持不足

在我的印象中,React Native面世时的火爆,主要因为两个方面,一是其热更新能力,二是其便捷的与已有App集成的能力。而NativeScript在其推出时,是一个非此即彼的排他性框架。没有具体去查演进历史,大概印象是其从1.0迭代到3.0时,还不支持与原生应用集成,到现在4.0版本,可以集成进原生应用中。但是,查看文档,在原生应用集成方面,文档不太完善。查看指向的GitHub项目,集成的复杂程度,高于React Native,我没有在生产项目中集成使用它的经验,只是从已有资料来看,其对这一块的支持刚刚开始,还不完善。

最后

NativeScript问世已久,一直没有大火。去年React专利风波时,知乎上有安利过NativeScript 简介,一阵风过,好像没有下文。我本以为是大家没有发现这块良玉,一番探索后,决定从入门到放弃。
当然我也没有在生产环境长期使用过,所以我的感受可能都是错的,仅从个人体验,不推荐使用这一框架,而如果你实在偏爱Angular,推荐Ionic了解一下,偏爱Vue,Weex了解一些。各路阵营都有其他方案,实在不必踩这里的坑。

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容