快速入门NativeScript,超详细的NativeScript学习笔记

6,761 阅读11分钟

1. 基础知识

1.1 为什么选择NativeScript

1.1.1 什么是NativeScript

NativeScript可以用javascript来写Android和iOS的应用,如下图所示NativeScript的代码与网页开发的代码很相似,都是用CSS写样式,javascript写业务逻辑,不同的是NativeScript使用XML来描述页面结构(NativeScript封装了自己的UI库)。

1.1.2 移动端开发方式的对比

如下图所示, 移动应用程序可分为四大类:native, hybrid, cross compiled, and just-in-time (JIT) compiled

让我们根据下图一起看一看不同类型的移动应用程序之间的差异以及它们如何在设备上运行
hybrid app本质上是在web浏览器中运行的网页。cross compiled app通过编译器被转换成native app。而JIT compiled app(例如NativeScript)运行在一个 JavaScript的虚拟机里。

1.1.3 NativeScript的优点

更少的shim代码(处理Android和iOS的不同)、一次写入(Android和iOS共用一套代码)、随处部署等等。

1.1.4 NativeScript能构建什么样的应用

因为NativeScript应用程序直接运行在设备上,并由运行在应用程序内部的JavaScript虚拟机解释,这意味着NativeScript应用程序不受访问本机设备api或硬件的限制,因此任何应用程序都可以编写为NativeScript应用。理论上是这样的,但是NativeScript应用程序是在JavaScript虚拟机中运行的,所以在应用程序和裸机之间有一个额外的(尽管很小)抽象层。要从设备中提取每一点性能,所以它不适合构建图形密集的游戏。

1.1.5 NativeScript是怎么工作的

  • NativeScript Runtime:NativeScript Runtime是连接JavaScript代码和Android和iOS原生API之间的接口代码。就像浏览器制造商教他们的JavaScript虚拟机如何使用DOM和windows对象一样,NativeScript Runtime也教JavaScript虚拟机如何使用本机设备底层的API。
  • NativeScript Core Modules: NativeScriptCore Modules是一组库,这些库是用来构建应用程序并指示NativeScript运行时在设备上做什么。核心模块由不同的库组成,如UI组件(按钮、列表视图、标签)、导航和应用程序。
  • JavaScript virtual machine:理解并执行JavaScript的代码,但是不知道怎么与设备交互,所以NativeScript开发团队编写了接口代码(称为NativeScript Runtime和NativeScript Core Modules)来教JavaScript虚拟机有关Android和iOS等移动设备API的知识。
  • NativeScript CLI:NativeScript CLI抽离了本地工具和SDK的复杂性,为我们提供了一组与平台无关的命令来构建和部署应用程序。

1.2 第一个NativeScript的应用

  1. 参考文档的Full Setup搭建环境
  2. 页面下载Android Studio然后按照教程配置Android的模拟器
  3. 打开命令行工具,运行tns create HelloWorld --template tns-template-hello-world
  4. 进入目标文件夹cd [filename]
  5. 运行命令tns run android --emulator,即可在Android模拟器上运行代码

2. 构建应用

2.1 剖析NativeScript应用程序

2.1.1 探索NativeScript应用程序的结构

|- app                 //开发目录
    |- App_Resources   //放置Android和iOS平台特殊的配置,例如App的图标等。
        |- Android
        |- iOS
    |- app.css             //全局的CSS样式,app运行时载入
    |- app.js              //启动文件,里面说明了从哪个页面启动应用
    |- bundle-config.js    //用于配置webpack(如果已安装了)
    |- main-page.js        //写业务逻辑
    |- main-page.xml       //写页面代码
    |- main-view-model.js  //相当于MVVM框架的vm层
    |- package.json        //描述应用程序的特征和依赖项
    |- references.d.ts     //为编辑器提供TypeScript的智能提示
|- node_modules        //依赖的库文件
|- platforms           //由NativeScript自动生成和维护
|- package.json        //描述应用程序的特征和依赖项
// package.json
{
    "description": "NativeScript Application",               //提供应用程序、功能和用途的简要说明
    "license": "SEE LICENSE IN <your-license-filename>",     //将协作开发人员指向您的许可文件,以描述其他人必须对您的应用程序代码作出贡献、修改、更改和重新分发哪些权限(可选)
    "readme": "NativeScript Application",                    //指向应用程序的README文件
    "repository": "<fill-your-repository-here>",             //应用程序公共或私有代码存储库的位置(可选)
    "nativescript": {                                        //一个特定于nativescript的部分,带有应用程序的标识符,Android和iOS平台使用它惟一地标识应用程序
        "id": "org.nativescript.myapp"
    },
    "dependencies": {                                        //npm使用的外部库和应用程序所依赖的库版本的列表
        "nativescript-theme-core": "~1.0.2",
        "tns-core-modules": "3.1.0"
    }
}

2.2 页面和导航

2.2.1 创建多页面应用

  1. 按照之前的操作创建HelloWorld项目
  2. 进入app文件夹,删除文件夹下的main-page.js, main-page.xml, main-view-model.js, 在app文件夹下创建view文件夹放页面,每个页面单独创建文件夹,文件夹应该与页面命名一致,文件目录如下:
|- app
    |- views
        |- home
            |- home.css
            |- home.xml
            |- home.js
        |- about
            |- about.css
            |- about.xml
            |- about.js
  1. 修改项目启动文件app.js,将app启动页面改成home页面
require("./bundle-config");
var application = require("application");
application.start({ moduleName: "views/home/home" });    //将moduleName的键值修改成home页面的路径
  1. 在home.xml文件中写入如下代码
<Page>    //页面中所有其他元素的容器,类似于网页开发里面的body标签       
    <StackLayout>    //布局元素,类似于网页开发里面的div标签
        <Label text="Welcome to the Tekmo App!" />     //在屏幕上显示文本
    </StackLayout>
</Page>
  1. 在about.xml文件中写入如下代码
<Page>
    <StackLayout>
        <Label textWrap="true" text="Small company that wants to bring you the best in retro gaming!" />    //textWrap为true时会自动换行
        <Label text="Come visit us in Louisville, KY" />
    </StackLayout>
</Page>

2.2.2 多页面之间的导航

  1. 在home.xml文件中写入如下代码
<Page>
    <StackLayout>
        <Label text="Welcome to the Tekmo App!" />
        <Button text="About" tap="onTap" />    //tap属性告诉NativeScript在单击按钮时调用onTap这个JavaScript函数
    </StackLayout>
</Page>
  1. 在home.js文件中写入如下代码
var frameModule = require("ui/frame");    //获取NativeScript框架中导航模块的引用
function onTap() {
    frameModule.topmost().navigate("views/about/about ");    //使用frame模块导航到about页面
}

exports.onTap = onTap;    //必须导出该函数,以便NativeScript运行时可以从UI访问它

2.2.3 在页面导航间应用转换动画

如下表所示是各个平台支持的过渡动画

接下来让我们看一下如何将这些过渡动画添加到导航跳转,在home.js中写入如下代码

var frames = require("ui/frame");
function onTap() {
    var navigationEntry = {
        moduleName: "views/about/about",
        transition: {
            name: "slideBottom"    //将想要应用的过渡动画写在这里
        }
    };
    frames.topmost().navigate(navigationEntry);
}

exports.onTap = onTap;

2.3 理解页面布局

参考文档进行学习

2.4 写app的样式

参考文档进行学习

3. 改善应用

3.1 数据绑定

3.1.1 普通数据的双向绑定

  1. 在home.xml文件中写入如下代码
<Page loaded="onLoaded">
    <StackLayout>
        <Label text="{{ Name }}" />    //在{{}}内写入需要绑定的变量
    </StackLayout>
</Page>
  1. 在home.js文件中写入如下代码
var observableModule = require("data/observable");
var viewModule = require ("ui/core/view");
    
exports.onLoaded = function(args){
    var page = args.object;
    var pet = new observableModule.Observable();    //pet对象是一个可观察的对象,它将绑定到页面上的所有元素
    page.bindingContext = pet;    //将pet设置为页面的绑定上下文,将其设置为用于绑定的页面级可观察对象
    pet.set("Name", "Riven");
    //也可以写成下面的形式
    //var pet = new observable.fromObject({
    //    Name: "Riven"
    //});
    //page.bindingContext = pet;
}

3.1.2 列表数据的双向绑定

  1. 在home.xml文件中写入如下代码
<Page loaded="onLoaded">
    <StackLayout>
        <ListView items="{{ pages }}" itemTap="onItemTap">    //pages是一个可观察数组
            <ListView.itemTemplate>    //pages中的每一项会被渲染一次
                <StackLayout>
                    <Label text="{{ title, title + ' Scrapbook Page' }}" />    //pages每一项上的title
                </StackLayout>
            </ListView.itemTemplate>
        </ListView>
    </StackLayout>
</Page>
  1. 在home.js文件中写入如下代码
var observable = require("data/observable");
var observableArray = require("data/observable-array");

exports.onLoaded = function(args) {
    var page = args.object;
    var filledPage = new observable.Observable({
        title: "Riven's Page"
    });
    var home = new observable.Observable({
        pages: new observableArray.ObservableArray(filledPage)    //生成可观察对象数组
    }); 
    page.bindingContext = home;
};

可以将数据绑定的内容封装成单独的pageName-view-model.js文件,方便多个页面共用一个view model.

3.2 NativeScript与设备硬件的交互

3.2.1 文件系统模块

var fileSystemModule = require("file-system");    //要使用文件系统模块,需要导入它
            
exports.onLoaded = function(){
    var fileName = "myFile.json";
    var file = fileSystemModule.knownFolders.documents().getFile(fileName);    //使用documents文件夹存储应用程序需要的离线文件
    var data = {name: "Brosteins", type: "filesystemexample"};
    var jsonDataToWrite = JSON.stringify(data);
    file.writeText(jsonDataToWrite);    //将数据写入文件系统
    console.log("Wrote to the file: " + jsonDataToWrite);
    var jsonDataRead = file.readTextSync();    //使用对文件的引用来读取数据。数据可以同步读取,也可以异步读取
    console.log("Read from the file: " + jsonDataRead);
    file.remove();    //删除该文件
};

3.2.2 相机

安装相机的插件:npm install nativescript-camera --save

var camera = require("nativescript-camera");
var image = require("image-source");

exports.onAddImageTap = function (args) {
    var page = args.object;
    var scrapbookPage = page.bindingContext;
    camera.requestPermissions();    //要使用照相机需要获得许可
    camera
        .takePicture()    //返回一个promise
        .then(function (picture) {    //当promise解析后,调用then()函数,传递图片
            image.fromAsset(picture).then(function (imageSource) {    
                scrapbookPage.set("image", imageSource);    //创建要绑定到视图的图像源对象
            });
        });
}

如果保存图片,需要先用 image.toBase64String("png") 将图片的二进制数据转换成base64字符串然后再保存起来。

3.2.3 GPS定位

安装定位的插件:tns plugin add nativescript-geolocation

var camera = require("nativescript-camera");
var image = require("image-source");
var geolocation = require("nativescript-geolocation"); 

exports.onAddImageTap = function (args) {
    var page = args.object;
    var scrapbookPage = page.bindingContext;
    if (!geolocation.isEnabled()) {    //在使用位置服务之前,应该检查是否启用了它,并请求启用它
        geolocation.enableLocationRequest();
    }
    camera
        .takePicture({ width: 100, height: 100, keepAspectRatio: true })
        .then(function (picture) {
            image.fromAsset(picture).then(function (imageSource) {
                scrapbookPage.set("image", imageSource);
            });
            geolocation.getCurrentLocation().then(function (location) {    //获取位置数据会自动提示用户请求权限
                scrapbookPage.set("lat", location.latitude);    //返回的位置的纬度值
                scrapbookPage.set("long", location.longitude);    //返回的位置的经度值    
            });
        });
};

3.3 创建具有主题的专业UI

参考文档进行学习

3.4 改善用户的体验

3.4.1 用modal构建更专业的UI

  1. 主页面的xml文件如下
<Page backgroundColor="green" loaded="onLoaded">
    <StackLayout backgroundColor="lightGreen">
        <Button text="BirthDate" tap="onBirthDateTap"/>
    </StackLayout>
</Page>
  1. 主页面的js文件如下
var page;

exports.onLoaded = function(args) {
    page = args.object;
    var scrapbookPage = page.navigationContext.model;
    page.bindingContext = scrapbookPage;
};

exports.onBirthDateTap = function(args) {
    var modalPageModule = "views/selectDate-page";
    var context = { birthDate: page.bindingContext.birthDate };
    var fullscreen = true;
    page.showModal(
        modalPageModule,
        context,
        function closeCallback(birthDate) {    //关闭modal时的回调函数,可以将modal页面的数据传递回来
            page.bindingContext.set("birthDate", birthDate);
        },
        fullscreen
    );
};
  1. modal页面的xml
<Page shownModally="onShownModally" loaded="onLoaded">
    <StackLayout>
        <DatePicker date="{{ date }}" />
        <Button class="btn btn-primary btn-rounded-sm btn-active" text="Done" tap="onDoneTap" />                          
    </StackLayout>
</Page>
  1. modal页面的js
var observableModule = require("data/observable");
                
var model;   
var closeCallback;
      
exports.onLoaded = function(args) {
    var page = args.object;
    model = new observableModule.fromObject({
        date: new Date(Date.now())
    });
    page.bindingContext = model;
};

exports.onShownModally = function(args) {
    closeCallback = args.closeCallback;   
};                                        

exports.onDoneTap = function(args) { closeCallback(model.date); };

3.4.2 适配平板电脑

可以创建 page-name.land.minWH600.xmlpage-name.land.minWH600.jspage-name.land.minWH600.css文件单独写平板端的页面。

3.5 部署前的配置

3.5.1 修改app图标

1. Android

Android的app图标放在 App_Resources目录下的drawable-*的各个文件夹中,也就是不同分辨率的设备用相应的图标

不通过文件夹对应的设备的分辨率如下表所示:
所以修改Android的app图标时,可以先在这个网站生成需要的图标再放入对应文件夹。 在 App_Resources/ AndroidManifest.xml 文件中有关于app的各项设置

<application
    android:name="com.tns.NativeScriptApplication"
    android:allowBackup="true"
    android:icon="@drawable/icon"    //在这里修改app的图标
    android:label="@string/app_name"    //在string.xml里配置app_name
    android:theme="@style/AppTheme" >    
    <activity
        android:name="com.tns.NativeScriptActivity"
        android:label="@string/title_activity_kimera"    //在string.xml里配置使用时app的名字
        android:configChanges="keyboardHidden|orientation|screenSize">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <activity android:name="com.tns.ErrorReportActivity"/>
</application>

2. iOS

App_Resources\iOS\Assets.xcassets\AppIcon.appiconset 文件下放入用入在这个网站下生成的iOS需要的适配各种设备的图标

3.5.2 修改app名字

1. Android

App_Resources/Android/values/strings.xml 里面修改app的名字

<resources>
    <string name="app_name">Pet Scrapbook</string>    //在这里改应用程序的名称
    <string name="title_activity_kimera">Pet Scrapbook</string>    //使用时app的名字
</resources>

2. iOS

App_Resources/iOS/Info.plist里面修改app的名字

<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>    //在这里修改名字,可以把名字直接写在<string>标签里

3.5.3 修改app版本号

1. Android

App_Resources/ AndroidManifest.xml里配置

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="__PACKAGE__"
    android:versionCode="1"    //用户看不到的内部版本号
    android:versionName="1.0">    //用户在谷歌商店可以看到的版本号

2. iOS

App_Resources/iOS/Info.plist里面修改app的版本号

    <key>CFBundleVersion</key>
    <string>1.0.0</string>    //版本号
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>    //构建号

3.5.4 修改app适配机器

1. Android

App_Resources/ AndroidManifest.xml里配置

<supports-screens
    android:smallScreens="true"       //支持约2-3英寸的屏幕
    android:normalScreens="true"      //支持约2-5英寸的屏幕
    android:largeScreens="true"       //支持约4-7英寸的屏幕
    android:xlargeScreens="true"/>    //支持约7+英寸的屏幕

Android屏幕大小和相应的屏幕分辨率DPIs如下表所示

2. iOS

App_Resources/iOS/Info.plist里适配各种设备

<key>UISupportedInterfaceOrientations</key>    //适配iPhones
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
<key>UISupportedInterfaceOrientations~ipad</key>    //适配iPads
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>

3.5.5 修改app启动页面

1. Android

drawable-nodpi目录下的splash_screen.xml配置了app的启动页面

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:gravity="fill">
    <item>
        <bitmap android:gravity="fill" android:src="@drawable/background" />    //启动屏幕的背景图
    </item>
    <item>
        <bitmap android:gravity="center" android:src="@drawable/logo" />    //启动屏幕中心的logo图片
    </item>
</layer-list>

启动屏幕的背景图和中心的logo图配置的方法和app的图标是相同的

2. iOS

  • App_Resources\iOS\Assets.xcassets\LaunchScreen.AspectFill.imageset里放启动屏幕的背景图
  • App_Resources\iOS\Assets.xcassets\LaunchScreen.Center.imageset里放启动屏幕中心的logo图片 可以在这个网站生成适配各种设备的图片

4. 结语