Feuille de chaleur NativeScript¶
NativeScript - Applications mobiles autochtones avec JavaScript
NativeScript est un cadre open-source pour construire des applications mobiles vraiment natives en utilisant JavaScript, TypeScript, Angular, Vue.js ou React. Il fournit un accès direct aux API natives et aux composants d'interface utilisateur sans WebViews.
Sommaire¶
- [Installation] (#installation)
- [Pour commencer] (#getting-started)
- [Structure du projet] (#project-structure)
- [Commandes CLI] (#cli-commands)
- [Concepts de base] (#core-concepts)
- [Composants de l'UI] (#ui-components)
- [Layouts] (#layouts)
- [Navigation] (#navigation)
- [Reliure des données] (#data-binding)
- [Styling] (#styling)
- APIs plate-forme
- [Plugins] (#plugins)
- [Intégration angulaire] (#angular-integration)
- [Intégration Vue.js] (#vuejs-integration)
- [Réagir à l'intégration] (#react-integration)
- [Essais] (#testing)
- [Performance] (#performance)
- [Déploiement] (#deployment)
- [Meilleures pratiques] (#best-practices)
Installation¶
Préalables¶
# Install Node.js (version 14 or later)
# Download from nodejs.org
# Verify Node.js installation
node --version
npm --version
# Install Android Studio (for Android development)
# Download from developer.android.com
# Install Xcode (for iOS development, macOS only)
# Download from Mac App Store
# Set environment variables
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools
NativeScript CLI Installation¶
# Install NativeScript CLI globally
npm install -g @nativescript/cli
# Verify installation
ns --version
tns --version # Alternative command
# Check environment setup
ns doctor
ns doctor android
ns doctor ios
# Install platform tools
ns install
```_
### Développement Environnement
```bash
# For Android development
# 1. Install Android Studio
# 2. Install Android SDK
# 3. Set ANDROID_HOME environment variable
# 4. Add platform-tools to PATH
# For iOS development (macOS only)
# 1. Install Xcode
# 2. Install Xcode Command Line Tools
xcode-select --install
# 3. Install CocoaPods
sudo gem install cocoapods
# Verify setup
ns doctor
```_
## Commencer
### Créer un nouveau projet
```bash
# Create new NativeScript project
ns create MyApp
# Create with specific template
ns create MyApp --template @nativescript/template-hello-world
ns create MyApp --template @nativescript/template-tab-navigation
ns create MyApp --template @nativescript/template-drawer-navigation
# Create with framework
ns create MyApp --ng # Angular
ns create MyApp --vue # Vue.js
ns create MyApp --react # React
# Create with TypeScript
ns create MyApp --ts
# Navigate to project directory
cd MyApp
# Run the app
ns run android
ns run ios
Modèles de projet¶
# JavaScript templates
ns create MyApp --template @nativescript/template-hello-world
ns create MyApp --template @nativescript/template-tab-navigation
ns create MyApp --template @nativescript/template-drawer-navigation
ns create MyApp --template @nativescript/template-master-detail
# TypeScript templates
ns create MyApp --template @nativescript/template-hello-world-ts
ns create MyApp --template @nativescript/template-tab-navigation-ts
# Angular templates
ns create MyApp --template @nativescript/template-hello-world-ng
ns create MyApp --template @nativescript/template-tab-navigation-ng
ns create MyApp --template @nativescript/template-drawer-navigation-ng
# Vue.js templates
ns create MyApp --template @nativescript/template-hello-world-vue
ns create MyApp --template @nativescript/template-tab-navigation-vue
# React templates
ns create MyApp --template @nativescript/template-hello-world-react
Structure du projet¶
Structure de base¶
MyApp/
├── app/ # Application source code
│ ├── app.js # Application entry point
│ ├── app.css # Global styles
│ ├── main-page.js # Main page logic
│ ├── main-page.xml # Main page markup
│ └── main-view-model.js # Main page view model
├── platforms/ # Platform-specific code (generated)
│ ├── android/
│ └── ios/
├── node_modules/ # Dependencies
├── package.json # Project configuration
├── nsconfig.json # NativeScript configuration
├── webpack.config.js # Webpack configuration
└── App_Resources/ # Platform resources
├── Android/
│ ├── src/main/res/
│ └── app.gradle
└── iOS/
├── Info.plist
└── build.xcconfig
app.js (point d'entrée)¶
import { Application } from '@nativescript/core';
// Start the application
Application.run({ moduleName: 'app-root' });
// Alternative with navigation
Application.run({ moduleName: 'main-page' });
page principale.xml (marque UI)¶
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="onNavigatingTo" class="page">
<Page.actionBar>
<ActionBar title="My App" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<GridLayout class="page">
<Label text="Hello, NativeScript!" class="h1 text-center" />
<Button text="Tap me!" tap="{{ onTap }}" class="btn btn-primary" />
</GridLayout>
</Page>
page principale.js (Page Logic)¶
import { fromObject } from '@nativescript/core';
export function onNavigatingTo(args) {
const page = args.object;
const viewModel = fromObject({
message: 'Hello, NativeScript!',
onTap: function() {
this.set('message', 'Button tapped!');
}
});
page.bindingContext = viewModel;
}
CLI Commandes¶
Gestion de projet¶
# Create new project
ns create <app-name> [options]
# Add platform
ns platform add android
ns platform add ios
# Remove platform
ns platform remove android
ns platform remove ios
# List platforms
ns platform list
# Clean project
ns platform clean android
ns platform clean ios
# Update platform
ns platform update android
ns platform update ios
Commandes de développement¶
# Run on device/emulator
ns run android
ns run ios
# Run with specific options
ns run android --device
ns run ios --device
# Run on specific device
ns run android --device <device-id>
ns run ios --device <device-id>
# Debug mode
ns debug android
ns debug ios
# Preview in browser (limited functionality)
ns preview
# Serve for hot reload
ns run android --hmr
ns run ios --hmr
Construire des commandes¶
# Build for platform
ns build android
ns build ios
# Build for release
ns build android --release
ns build ios --release --for-device
# Build with specific configuration
ns build android --env.production
ns build ios --env.production
# Clean build
ns clean
# Prepare platform
ns prepare android
ns prepare ios
Gestion des greffons¶
# Install plugin
ns plugin add <plugin-name>
# Remove plugin
ns plugin remove <plugin-name>
# List installed plugins
ns plugin list
# Update plugin
ns plugin update <plugin-name>
# Search for plugins
npm search nativescript-plugin
Concepts fondamentaux¶
Observable et liaison des données¶
import { Observable, fromObject } from '@nativescript/core';
// Create observable object
const viewModel = new Observable();
viewModel.set('name', 'John Doe');
viewModel.set('age', 30);
// Listen to property changes
viewModel.on(Observable.propertyChangeEvent, (args) => {
console.log(`Property ${args.propertyName} changed to ${args.value}`);
});
// Create from object
const user = fromObject({
firstName: 'John',
lastName: 'Doe',
fullName: function() {
return `${this.firstName} ${this.lastName}`;
},
updateName: function(first, last) {
this.set('firstName', first);
this.set('lastName', last);
}
});
// Observable Array
import { ObservableArray } from '@nativescript/core';
const items = new ObservableArray([
{ name: 'Item 1', value: 1 },
{ name: 'Item 2', value: 2 },
{ name: 'Item 3', value: 3 }
]);
// Listen to array changes
items.on(ObservableArray.changeEvent, (args) => {
console.log('Array changed:', args);
});
// Add items
items.push({ name: 'Item 4', value: 4 });
// Remove items
items.splice(0, 1);
Navigation des pages¶
import { Frame, topmost } from '@nativescript/core';
// Navigate to page
export function navigateToDetails() {
const frame = Frame.topmost();
frame.navigate({
moduleName: 'details-page',
context: { id: 123, name: 'Sample Item' }
});
}
// Navigate with animation
export function navigateWithAnimation() {
topmost().navigate({
moduleName: 'details-page',
animated: true,
transition: {
name: 'slide',
duration: 300,
curve: 'ease'
}
});
}
// Navigate back
export function goBack() {
topmost().goBack();
}
// Clear history and navigate
export function navigateAndClearHistory() {
topmost().navigate({
moduleName: 'home-page',
clearHistory: true
});
}
// Modal navigation
export function showModal() {
const frame = topmost();
frame.showModal('modal-page', {
context: { data: 'modal data' },
fullscreen: false,
animated: true
});
}
// Close modal
export function closeModal() {
const frame = topmost();
frame.closeModal();
}
Cycle de vie de l'application¶
import { Application, on as applicationOn, launchEvent, suspendEvent, resumeEvent, exitEvent } from '@nativescript/core';
// Application launch
applicationOn(launchEvent, (args) => {
console.log('Application launched');
if (args.android) {
// Android-specific code
console.log('Launched Android application with the following intent: ' + args.android.getIntent());
} else if (args.ios) {
// iOS-specific code
console.log('Launched iOS application with options: ' + args.ios);
}
});
// Application suspend
applicationOn(suspendEvent, (args) => {
console.log('Application suspended');
// Save application state
});
// Application resume
applicationOn(resumeEvent, (args) => {
console.log('Application resumed');
// Restore application state
});
// Application exit
applicationOn(exitEvent, (args) => {
console.log('Application exit');
// Cleanup resources
});
// Low memory warning
applicationOn('lowMemory', (args) => {
console.log('Low memory warning');
// Free up memory
});
// Orientation change
applicationOn('orientationChanged', (args) => {
console.log('Orientation changed to:', args.newValue);
});
Composantes de l'assurance-chômage¶
Composantes de base¶
<!-- Label -->
<Label text="Hello World" class="label" />
<Label text="{{ message }}" textWrap="true" />
<!-- Button -->
<Button text="Click Me" tap="{{ onTap }}" class="btn btn-primary" />
<Button text="Disabled" isEnabled="false" />
<!-- TextField -->
<TextField text="{{ username }}" hint="Enter username" />
<TextField secure="true" hint="Password" />
<!-- TextView -->
<TextView text="{{ description }}" editable="false" />
<TextView hint="Enter description" />
<!-- Image -->
<Image src="~/images/logo.png" stretch="aspectFit" />
<Image src="{{ imageUrl }}" loadMode="async" />
<!-- ActivityIndicator -->
<ActivityIndicator busy="{{ isLoading }}" />
<!-- Progress -->
<Progress value="{{ progressValue }}" maxValue="100" />
<!-- Slider -->
<Slider value="{{ sliderValue }}" minValue="0" maxValue="100" />
<!-- Switch -->
<Switch checked="{{ isEnabled }}" />
<!-- DatePicker -->
<DatePicker date="{{ selectedDate }}" />
<!-- TimePicker -->
<TimePicker time="{{ selectedTime }}" />
<!-- SearchBar -->
<SearchBar text="{{ searchQuery }}" hint="Search..." submit="{{ onSearch }}" />
Liste des composants¶
<!-- ListView -->
<ListView items="{{ items }}" itemTap="{{ onItemTap }}">
<ListView.itemTemplate>
<Label text="{{ name }}" class="list-item" />
</ListView.itemTemplate>
</ListView>
<!-- ListView with complex template -->
<ListView items="{{ users }}" separatorColor="transparent">
<ListView.itemTemplate>
<GridLayout columns="auto, *" rows="auto, auto" class="list-item">
<Image src="{{ avatar }}" row="0" col="0" rowSpan="2" class="avatar" />
<Label text="{{ name }}" row="0" col="1" class="name" />
<Label text="{{ email }}" row="1" col="1" class="email" />
</GridLayout>
</ListView.itemTemplate>
</ListView>
<!-- Repeater -->
<Repeater items="{{ items }}">
<Repeater.itemTemplate>
<StackLayout class="item">
<Label text="{{ title }}" class="title" />
<Label text="{{ description }}" class="description" />
</StackLayout>
</Repeater.itemTemplate>
</Repeater>
Composants avancés¶
<!-- WebView -->
<WebView src="{{ url }}" />
<WebView src="~/html/index.html" />
<!-- HtmlView -->
<HtmlView html="{{ htmlContent }}" />
<!-- Placeholder -->
<Placeholder creatingView="{{ onCreatingView }}" />
<!-- ContentView -->
<ContentView content="{{ dynamicContent }}" />
<!-- Frame -->
<Frame defaultPage="main-page" />
<!-- TabView -->
<TabView selectedIndex="{{ selectedTab }}">
<TabViewItem title="Tab 1">
<Label text="Content of Tab 1" />
</TabViewItem>
<TabViewItem title="Tab 2">
<Label text="Content of Tab 2" />
</TabViewItem>
</TabView>
<!-- SegmentedBar -->
<SegmentedBar items="{{ segments }}" selectedIndex="{{ selectedSegment }}" />
<!-- ActionBar -->
<ActionBar title="My App" class="action-bar">
<ActionItem text="Settings" tap="{{ openSettings }}" ios.position="right" android.position="actionBar" />
<ActionItem icon="res://ic_menu" tap="{{ openMenu }}" ios.position="left" android.position="actionBar" />
</ActionBar>
Mise en page¶
PiocheLayout¶
<!-- Vertical stack (default) -->
<StackLayout>
<Label text="First" />
<Label text="Second" />
<Label text="Third" />
</StackLayout>
<!-- Horizontal stack -->
<StackLayout orientation="horizontal">
<Button text="Button 1" />
<Button text="Button 2" />
<Button text="Button 3" />
</StackLayout>
<!-- With spacing and padding -->
<StackLayout spacing="10" padding="20">
<Label text="Item 1" />
<Label text="Item 2" />
</StackLayout>
GridLayout¶
<!-- Basic grid -->
<GridLayout columns="*, *, *" rows="auto, auto">
<Label text="(0,0)" row="0" col="0" />
<Label text="(0,1)" row="0" col="1" />
<Label text="(0,2)" row="0" col="2" />
<Label text="(1,0)" row="1" col="0" />
<Label text="(1,1)" row="1" col="1" />
<Label text="(1,2)" row="1" col="2" />
</GridLayout>
<!-- Spanning cells -->
<GridLayout columns="*, *" rows="auto, auto, auto">
<Label text="Header" row="0" col="0" colSpan="2" class="header" />
<Label text="Left" row="1" col="0" />
<Label text="Right" row="1" col="1" />
<Label text="Footer" row="2" col="0" colSpan="2" class="footer" />
</GridLayout>
<!-- Complex layout -->
<GridLayout columns="100, *, 50" rows="auto, *, auto">
<Label text="Fixed 100" row="0" col="0" />
<Label text="Flexible" row="0" col="1" />
<Label text="Fixed 50" row="0" col="2" />
<ScrollView row="1" col="0" colSpan="3">
<StackLayout>
<!-- Content -->
</StackLayout>
</ScrollView>
<Button text="Footer" row="2" col="0" colSpan="3" />
</GridLayout>
FlexboxLayout¶
<!-- Basic flexbox -->
<FlexboxLayout flexDirection="row" justifyContent="space-between">
<Label text="Start" />
<Label text="Center" />
<Label text="End" />
</FlexboxLayout>
<!-- Vertical flexbox -->
<FlexboxLayout flexDirection="column" alignItems="center">
<Label text="Centered" />
<Button text="Button" />
</FlexboxLayout>
<!-- Flex wrap -->
<FlexboxLayout flexDirection="row" flexWrap="wrap">
<Label text="Item 1" flexGrow="1" />
<Label text="Item 2" flexGrow="1" />
<Label text="Item 3" flexGrow="1" />
<Label text="Item 4" flexGrow="1" />
</FlexboxLayout>
<!-- Complex flexbox -->
<FlexboxLayout flexDirection="column" height="100%">
<Label text="Header" class="header" />
<FlexboxLayout flexDirection="row" flexGrow="1">
<Label text="Sidebar" class="sidebar" />
<ScrollView flexGrow="1">
<StackLayout>
<!-- Main content -->
</StackLayout>
</ScrollView>
</FlexboxLayout>
<Label text="Footer" class="footer" />
</FlexboxLayout>
Layout absolu¶
<!-- Absolute positioning -->
<AbsoluteLayout>
<Label text="Top Left" left="0" top="0" />
<Label text="Top Right" right="0" top="0" />
<Label text="Bottom Left" left="0" bottom="0" />
<Label text="Bottom Right" right="0" bottom="0" />
<Label text="Center" left="50%" top="50%" />
</AbsoluteLayout>
<!-- Overlapping elements -->
<AbsoluteLayout>
<Image src="~/images/background.jpg" width="100%" height="100%" />
<Label text="Overlay Text" left="20" top="50" class="overlay-text" />
<Button text="Action" right="20" bottom="50" />
</AbsoluteLayout>
DockLayout¶
<!-- Basic dock layout -->
<DockLayout>
<Label text="Top" dock="top" class="dock-top" />
<Label text="Bottom" dock="bottom" class="dock-bottom" />
<Label text="Left" dock="left" class="dock-left" />
<Label text="Right" dock="right" class="dock-right" />
<Label text="Fill" class="dock-fill" />
</DockLayout>
<!-- App layout with dock -->
<DockLayout>
<ActionBar dock="top" title="My App" />
<GridLayout dock="bottom" columns="*, *, *" class="tab-bar">
<Button text="Home" col="0" />
<Button text="Search" col="1" />
<Button text="Profile" col="2" />
</GridLayout>
<ScrollView>
<StackLayout>
<!-- Main content -->
</StackLayout>
</ScrollView>
</DockLayout>
Enveloppe¶
<!-- Horizontal wrap -->
<WrapLayout orientation="horizontal" itemWidth="100" itemHeight="100">
<Label text="1" class="item" />
<Label text="2" class="item" />
<Label text="3" class="item" />
<Label text="4" class="item" />
<Label text="5" class="item" />
</WrapLayout>
<!-- Vertical wrap -->
<WrapLayout orientation="vertical" itemWidth="150" itemHeight="50">
<Button text="Button 1" />
<Button text="Button 2" />
<Button text="Button 3" />
<Button text="Button 4" />
</WrapLayout>
Navigation¶
Navigation du cadre¶
import { Frame, topmost } from '@nativescript/core';
// Basic navigation
export function navigateToPage() {
topmost().navigate('details-page');
}
// Navigation with context
export function navigateWithData() {
topmost().navigate({
moduleName: 'details-page',
context: {
id: 123,
title: 'Sample Item',
data: { key: 'value' }
}
});
}
// Navigation with transition
export function navigateWithTransition() {
topmost().navigate({
moduleName: 'details-page',
animated: true,
transition: {
name: 'slideLeft',
duration: 300,
curve: 'easeIn'
}
});
}
// Back navigation
export function goBack() {
if (topmost().canGoBack()) {
topmost().goBack();
}
}
// Clear history
export function navigateAndClearHistory() {
topmost().navigate({
moduleName: 'home-page',
clearHistory: true
});
}
Navigation modale¶
// Show modal
export function showModal() {
const frame = topmost();
frame.showModal({
moduleName: 'modal-page',
context: { message: 'Hello from modal' },
fullscreen: false,
animated: true,
stretched: false
}).then((result) => {
console.log('Modal closed with result:', result);
});
}
// Close modal with result
export function closeModal() {
const frame = topmost();
frame.closeModal('Modal result data');
}
// Modal page setup
// modal-page.js
export function onShownModally(args) {
const page = args.object;
const context = args.context;
page.bindingContext = fromObject({
message: context.message,
closeModal: function() {
page.closeModal('Data from modal');
}
});
}
Navigation des onglets¶
<!-- TabView navigation -->
<TabView selectedIndex="{{ selectedTab }}" selectedIndexChanged="{{ onTabChanged }}">
<TabViewItem title="Home" iconSource="res://ic_home">
<Frame defaultPage="home-page" />
</TabViewItem>
<TabViewItem title="Search" iconSource="res://ic_search">
<Frame defaultPage="search-page" />
</TabViewItem>
<TabViewItem title="Profile" iconSource="res://ic_profile">
<Frame defaultPage="profile-page" />
</TabViewItem>
</TabView>
Navigation des tiroirs¶
<!-- RadSideDrawer (requires plugin) -->
<nsDrawer:RadSideDrawer xmlns:nsDrawer="@nativescript/ui-sidedrawer">
<nsDrawer:RadSideDrawer.drawerContent>
<StackLayout class="drawer-content">
<Label text="Menu" class="drawer-header" />
<Button text="Home" tap="{{ navigateToHome }}" />
<Button text="Settings" tap="{{ navigateToSettings }}" />
<Button text="About" tap="{{ navigateToAbout }}" />
</StackLayout>
</nsDrawer:RadSideDrawer.drawerContent>
<nsDrawer:RadSideDrawer.mainContent>
<Frame defaultPage="main-page" />
</nsDrawer:RadSideDrawer.mainContent>
</nsDrawer:RadSideDrawer>
Reliure des données¶
Reliure à une voie¶
<!-- Text binding -->
<Label text="{{ message }}" />
<Label text="{{ 'Hello ' + name }}" />
<!-- Property binding -->
<Button isEnabled="{{ canSubmit }}" />
<Image src="{{ imageUrl }}" />
<ActivityIndicator busy="{{ isLoading }}" />
<!-- Conditional binding -->
<Label text="{{ isLoggedIn ? 'Welcome' : 'Please login' }}" />
<Button visibility="{{ hasData ? 'visible' : 'collapsed' }}" />
Reliure à deux voies¶
<!-- TextField two-way binding -->
<TextField text="{{ username, mode=twoWay }}" />
<Slider value="{{ volume, mode=twoWay }}" />
<Switch checked="{{ isEnabled, mode=twoWay }}" />
<DatePicker date="{{ selectedDate, mode=twoWay }}" />
Reliure des événements¶
<!-- Event handlers -->
<Button text="Click Me" tap="{{ onButtonTap }}" />
<TextField text="{{ username }}" textChange="{{ onTextChange }}" />
<ListView items="{{ items }}" itemTap="{{ onItemTap }}" />
<SearchBar text="{{ query }}" submit="{{ onSearch }}" clear="{{ onClear }}" />
Liste de liaison¶
// View model with observable array
import { Observable, ObservableArray } from '@nativescript/core';
export function createViewModel() {
const viewModel = new Observable();
const items = new ObservableArray([
{ id: 1, name: 'Item 1', description: 'First item' },
{ id: 2, name: 'Item 2', description: 'Second item' },
{ id: 3, name: 'Item 3', description: 'Third item' }
]);
viewModel.set('items', items);
viewModel.set('addItem', function() {
const newItem = {
id: items.length + 1,
name: `Item ${items.length + 1}`,
description: `Item number ${items.length + 1}`
};
items.push(newItem);
});
viewModel.set('removeItem', function(index) {
items.splice(index, 1);
});
viewModel.set('onItemTap', function(args) {
const item = items.getItem(args.index);
console.log('Tapped item:', item.name);
});
return viewModel;
}
<!-- List with binding -->
<StackLayout>
<Button text="Add Item" tap="{{ addItem }}" />
<ListView items="{{ items }}" itemTap="{{ onItemTap }}">
<ListView.itemTemplate>
<GridLayout columns="*, auto" class="list-item">
<StackLayout col="0">
<Label text="{{ name }}" class="item-name" />
<Label text="{{ description }}" class="item-description" />
</StackLayout>
<Button text="Delete" col="1" tap="{{ $parents['Page'].removeItem, $index }}" />
</GridLayout>
</ListView.itemTemplate>
</ListView>
</StackLayout>
Style¶
CSS Style¶
/* app.css - Global styles */
/* Import NativeScript theme */
@import '@nativescript/theme/css/core.css';
@import '@nativescript/theme/css/default.css';
/* Global styles */
.page {
background-color: #f0f0f0;
}
.action-bar {
background-color: #3f51b5;
color: white;
}
/* Typography */
.h1 {
font-size: 24;
font-weight: bold;
color: #333;
}
.h2 {
font-size: 20;
font-weight: bold;
color: #666;
}
.body {
font-size: 16;
color: #333;
}
/* Buttons */
.btn {
font-size: 16;
padding: 12;
margin: 8;
border-radius: 4;
}
.btn-primary {
background-color: #3f51b5;
color: white;
}
.btn-secondary {
background-color: #f0f0f0;
color: #333;
border-width: 1;
border-color: #ccc;
}
/* Layout helpers */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.m-10 {
margin: 10;
}
.p-10 {
padding: 10;
}
/* List styles */
.list-item {
padding: 16;
border-bottom-width: 1;
border-bottom-color: #e0e0e0;
}
.list-item:active {
background-color: #f5f5f5;
}
/* Form styles */
.form-field {
margin: 8;
padding: 12;
border-width: 1;
border-color: #ccc;
border-radius: 4;
}
.form-field:focus {
border-color: #3f51b5;
}
/* Platform-specific styles */
.android .action-bar {
height: 56;
}
.ios .action-bar {
height: 44;
}
/* Responsive styles */
@media (orientation: landscape) {
.container {
flex-direction: row;
}
}
SCSS Appui¶
// app.scss
@import '@nativescript/theme/scss/variables/blue';
@import '@nativescript/theme/scss/core';
@import '@nativescript/theme/scss/default';
// Variables
$primary-color: #3f51b5;
$secondary-color: #f0f0f0;
$text-color: #333;
$border-color: #e0e0e0;
// Mixins
@mixin button-style($bg-color, $text-color) {
background-color: $bg-color;
color: $text-color;
padding: 12;
border-radius: 4;
font-size: 16;
}
@mixin card-style {
background-color: white;
border-radius: 8;
elevation: 2;
margin: 8;
padding: 16;
}
// Styles
.page {
background-color: $secondary-color;
}
.btn-primary {
@include button-style($primary-color, white);
}
.btn-secondary {
@include button-style($secondary-color, $text-color);
border-width: 1;
border-color: $border-color;
}
.card {
@include card-style;
}
// Nested styles
.list-container {
.list-item {
padding: 16;
border-bottom-width: 1;
border-bottom-color: $border-color;
.item-title {
font-size: 18;
font-weight: bold;
color: $text-color;
}
.item-subtitle {
font-size: 14;
color: lighten($text-color, 20%);
}
}
}
Styling spécifique à la plateforme¶
/* Platform-specific CSS */
.android .action-bar {
background-color: #3f51b5;
color: white;
height: 56;
}
.ios .action-bar {
background-color: #007aff;
color: white;
height: 44;
}
/* Device-specific styles */
.phone .container {
padding: 16;
}
.tablet .container {
padding: 32;
max-width: 600;
horizontal-align: center;
}
/* Orientation-specific styles */
.portrait .layout {
flex-direction: column;
}
.landscape .layout {
flex-direction: row;
}
Style dynamique¶
// Dynamic styling in code
import { Color } from '@nativescript/core';
export function applyDynamicStyles(view, theme) {
if (theme === 'dark') {
view.backgroundColor = new Color('#333');
view.color = new Color('#fff');
} else {
view.backgroundColor = new Color('#fff');
view.color = new Color('#333');
}
}
// CSS class manipulation
export function toggleTheme(view) {
if (view.className.includes('dark-theme')) {
view.className = view.className.replace('dark-theme', 'light-theme');
} else {
view.className = view.className.replace('light-theme', 'dark-theme');
}
}
// Style animation
import { Animation } from '@nativescript/core';
export function animateButton(button) {
const animation = new Animation([{
target: button,
scale: { x: 1.2, y: 1.2 },
duration: 200
}, {
target: button,
scale: { x: 1, y: 1 },
duration: 200
}]);
animation.play();
}
API des plateformes¶
Informations sur le périphérique¶
import { Device, Screen, isAndroid, isIOS } from '@nativescript/core';
// Device information
console.log('Device Type:', Device.deviceType);
console.log('OS:', Device.os);
console.log('OS Version:', Device.osVersion);
console.log('Model:', Device.model);
console.log('Manufacturer:', Device.manufacturer);
console.log('UUID:', Device.uuid);
console.log('Language:', Device.language);
console.log('Region:', Device.region);
// Screen information
console.log('Screen Width:', Screen.mainScreen.widthDIPs);
console.log('Screen Height:', Screen.mainScreen.heightDIPs);
console.log('Screen Scale:', Screen.mainScreen.scale);
// Platform detection
if (isAndroid) {
console.log('Running on Android');
// Android-specific code
} else if (isIOS) {
console.log('Running on iOS');
// iOS-specific code
}
// Platform-specific API access
if (isAndroid) {
const context = Application.android.context;
const packageManager = context.getPackageManager();
// Use Android APIs
} else if (isIOS) {
const currentDevice = UIDevice.currentDevice;
// Use iOS APIs
}
Système de fichiers¶
import { File, Folder, knownFolders, path } from '@nativescript/core';
// Known folders
const documentsFolder = knownFolders.documents();
const tempFolder = knownFolders.temp();
const appFolder = knownFolders.currentApp();
// Create file
const file = documentsFolder.getFile('data.txt');
// Write to file
file.writeText('Hello, NativeScript!')
.then(() => {
console.log('File written successfully');
})
.catch((error) => {
console.error('Error writing file:', error);
});
// Read from file
file.readText()
.then((content) => {
console.log('File content:', content);
})
.catch((error) => {
console.error('Error reading file:', error);
});
// Create folder
const folder = documentsFolder.getFolder('myFolder');
// List folder contents
folder.getEntities()
.then((entities) => {
entities.forEach((entity) => {
console.log('Entity:', entity.name, entity.path);
});
});
// File operations
const sourceFile = documentsFolder.getFile('source.txt');
const targetFile = documentsFolder.getFile('target.txt');
// Copy file
sourceFile.copy(targetFile.path)
.then(() => {
console.log('File copied');
});
// Move file
sourceFile.move(targetFile.path)
.then(() => {
console.log('File moved');
});
// Delete file
file.remove()
.then(() => {
console.log('File deleted');
});
// Check if file exists
if (file.exists) {
console.log('File exists');
}
// Get file info
console.log('File size:', file.size);
console.log('Last modified:', file.lastModified);
Demandes HTTP¶
import { Http } from '@nativescript/core';
// GET request
Http.getJSON('https://api.example.com/users')
.then((result) => {
console.log('Users:', result);
})
.catch((error) => {
console.error('Error:', error);
});
// POST request
Http.request({
url: 'https://api.example.com/users',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
content: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
})
.then((response) => {
console.log('Response:', response.content.toJSON());
})
.catch((error) => {
console.error('Error:', error);
});
// File upload
Http.request({
url: 'https://api.example.com/upload',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data'
},
content: {
file: file,
description: 'Uploaded file'
}
})
.then((response) => {
console.log('Upload successful');
})
.catch((error) => {
console.error('Upload failed:', error);
});
// Download file
Http.getFile('https://example.com/file.pdf', documentsFolder.path + '/downloaded.pdf')
.then((file) => {
console.log('File downloaded:', file.path);
})
.catch((error) => {
console.error('Download failed:', error);
});
Stockage local¶
import { ApplicationSettings } from '@nativescript/core';
// Store data
ApplicationSettings.setString('username', 'john_doe');
ApplicationSettings.setNumber('userId', 123);
ApplicationSettings.setBoolean('isLoggedIn', true);
// Retrieve data
const username = ApplicationSettings.getString('username', 'default_user');
const userId = ApplicationSettings.getNumber('userId', 0);
const isLoggedIn = ApplicationSettings.getBoolean('isLoggedIn', false);
// Remove data
ApplicationSettings.remove('username');
// Check if key exists
if (ApplicationSettings.hasKey('username')) {
console.log('Username exists');
}
// Clear all data
ApplicationSettings.clear();
// Store complex objects
const user = {
id: 123,
name: 'John Doe',
email: 'john@example.com'
};
ApplicationSettings.setString('user', JSON.stringify(user));
// Retrieve complex objects
const storedUser = JSON.parse(ApplicationSettings.getString('user', '{}'));
Greffons¶
Greffons de base¶
# Camera plugin
ns plugin add @nativescript/camera
# Geolocation plugin
ns plugin add @nativescript/geolocation
# Local notifications
ns plugin add @nativescript/local-notifications
# Social share
ns plugin add @nativescript/social-share
# Email plugin
ns plugin add @nativescript/email
# Phone plugin
ns plugin add @nativescript/phone
# Fingerprint authentication
ns plugin add @nativescript/fingerprint-auth
# Secure storage
ns plugin add @nativescript/secure-storage
# Background HTTP
ns plugin add @nativescript/background-http
Utilisation du plugin caméra¶
import { Camera, requestPermissions } from '@nativescript/camera';
export function takePicture() {
requestPermissions()
.then(() => {
const options = {
width: 300,
height: 300,
keepAspectRatio: true,
saveToGallery: false
};
return Camera.takePicture(options);
})
.then((imageAsset) => {
console.log('Picture taken:', imageAsset);
// Use the image
const image = new Image();
image.src = imageAsset;
})
.catch((error) => {
console.error('Camera error:', error);
});
}
export function selectFromGallery() {
const options = {
width: 300,
height: 300,
keepAspectRatio: true
};
Camera.requestPermissions()
.then(() => {
return Camera.selectFromGallery(options);
})
.then((imageAsset) => {
console.log('Image selected:', imageAsset);
})
.catch((error) => {
console.error('Gallery error:', error);
});
}
Utilisation du plugin de géolocalisation¶
import { Geolocation, CoreTypes } from '@nativescript/geolocation';
export function getCurrentLocation() {
const options = {
desiredAccuracy: CoreTypes.Accuracy.high,
maximumAge: 5000,
timeout: 10000
};
Geolocation.getCurrentLocation(options)
.then((location) => {
console.log('Location:', location);
console.log('Latitude:', location.latitude);
console.log('Longitude:', location.longitude);
console.log('Altitude:', location.altitude);
console.log('Speed:', location.speed);
})
.catch((error) => {
console.error('Location error:', error);
});
}
export function watchLocation() {
const options = {
desiredAccuracy: CoreTypes.Accuracy.high,
updateDistance: 10,
minimumUpdateTime: 1000
};
const watchId = Geolocation.watchLocation(
(location) => {
console.log('Location update:', location);
},
(error) => {
console.error('Location error:', error);
},
options
);
// Stop watching
setTimeout(() => {
Geolocation.clearWatch(watchId);
}, 30000);
}
Développement de plugins personnalisés¶
// Create custom plugin
// my-plugin/index.js
export class MyPlugin {
static getMessage() {
return 'Hello from custom plugin!';
}
static performAction(data) {
return new Promise((resolve, reject) => {
// Plugin logic here
setTimeout(() => {
resolve(`Action completed with: ${data}`);
}, 1000);
});
}
}
// my-plugin/index.android.js
export class MyPlugin {
static getMessage() {
// Android-specific implementation
return 'Hello from Android plugin!';
}
static performAction(data) {
return new Promise((resolve, reject) => {
// Android-specific logic
const context = Application.android.context;
// Use Android APIs
resolve(`Android action completed with: ${data}`);
});
}
}
// my-plugin/index.ios.js
export class MyPlugin {
static getMessage() {
// iOS-specific implementation
return 'Hello from iOS plugin!';
}
static performAction(data) {
return new Promise((resolve, reject) => {
// iOS-specific logic
// Use iOS APIs
resolve(`iOS action completed with: ${data}`);
});
}
}
// Usage
import { MyPlugin } from './my-plugin';
console.log(MyPlugin.getMessage());
MyPlugin.performAction('test data')
.then((result) => {
console.log('Plugin result:', result);
})
.catch((error) => {
console.error('Plugin error:', error);
});
Intégration angulaire¶
Configuration du projet angulaire¶
# Create Angular project
ns create MyAngularApp --ng
# Add Angular dependencies
cd MyAngularApp
npm install @angular/core @angular/common @angular/forms @angular/router
# Generate components
ng generate component home
ng generate component details
ng generate service data
Module App¶
// app.module.ts
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { NativeScriptModule } from '@nativescript/angular';
import { NativeScriptRouterModule } from '@nativescript/angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { DetailsComponent } from './details/details.component';
@NgModule({
bootstrap: [AppComponent],
imports: [
NativeScriptModule,
AppRoutingModule
],
declarations: [
AppComponent,
HomeComponent,
DetailsComponent
],
providers: [],
schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }
Routage¶
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { NativeScriptRouterModule } from '@nativescript/angular';
import { HomeComponent } from './home/home.component';
import { DetailsComponent } from './details/details.component';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'details/:id', component: DetailsComponent }
];
@NgModule({
imports: [NativeScriptRouterModule.forRoot(routes)],
exports: [NativeScriptRouterModule]
})
export class AppRoutingModule { }
Composante¶
// home.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { DataService } from '../data.service';
@Component({
selector: 'ns-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
items: any[] = [];
isLoading = false;
constructor(
private router: Router,
private dataService: DataService
) { }
ngOnInit(): void {
this.loadData();
}
loadData(): void {
this.isLoading = true;
this.dataService.getItems()
.subscribe(
(data) => {
this.items = data;
this.isLoading = false;
},
(error) => {
console.error('Error loading data:', error);
this.isLoading = false;
}
);
}
onItemTap(item: any): void {
this.router.navigate(['/details', item.id]);
}
onRefresh(): void {
this.loadData();
}
}
<!-- home.component.html -->
<ActionBar title="Home" class="action-bar">
<ActionItem text="Refresh" (tap)="onRefresh()" ios.position="right"></ActionItem>
</ActionBar>
<GridLayout rows="*">
<ActivityIndicator [busy]="isLoading" *ngIf="isLoading"></ActivityIndicator>
<ListView [items]="items" (itemTap)="onItemTap($event.object.bindingContext)" *ngIf="!isLoading">
<ng-template let-item="item">
<GridLayout columns="auto, *" rows="auto, auto" class="list-item">
<Label [text]="item.title" row="0" col="1" class="item-title"></Label>
<Label [text]="item.description" row="1" col="1" class="item-description"></Label>
</GridLayout>
</ng-template>
</ListView>
</GridLayout>
Services¶
// data.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Http } from '@nativescript/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com';
constructor() { }
getItems(): Observable<any[]> {
return new Observable((observer) => {
Http.getJSON(`${this.apiUrl}/items`)
.then((data: any[]) => {
observer.next(data);
observer.complete();
})
.catch((error) => {
observer.error(error);
});
});
}
getItem(id: number): Observable<any> {
return new Observable((observer) => {
Http.getJSON(`${this.apiUrl}/items/${id}`)
.then((data: any) => {
observer.next(data);
observer.complete();
})
.catch((error) => {
observer.error(error);
});
});
}
createItem(item: any): Observable<any> {
return new Observable((observer) => {
Http.request({
url: `${this.apiUrl}/items`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
content: JSON.stringify(item)
})
.then((response) => {
observer.next(response.content.toJSON());
observer.complete();
})
.catch((error) => {
observer.error(error);
});
});
}
}
Vue.js Intégration¶
Installation du projet Vue¶
# Create Vue project
ns create MyVueApp --vue
# Add Vue dependencies
cd MyVueApp
npm install vue@next @vue/runtime-core
Principale application¶
// app.js
import { createApp } from 'nativescript-vue';
import Home from './components/Home.vue';
const app = createApp(Home);
app.start();
Composante Vue¶
<!-- components/Home.vue -->
<template>
<Frame>
<Page>
<ActionBar title="Home" />
<GridLayout rows="auto, *">
<StackLayout row="0" class="form">
<TextField v-model="newItem" hint="Enter new item" />
<Button text="Add Item" @tap="addItem" class="btn btn-primary" />
</StackLayout>
<ListView row="1" :items="items" @itemTap="onItemTap">
<template #default="{ item, index }">
<GridLayout columns="*, auto" class="list-item">
<Label col="0" :text="item.name" class="item-name" />
<Button col="1" text="Delete" @tap="deleteItem(index)" class="btn btn-secondary" />
</GridLayout>
</template>
</ListView>
</GridLayout>
</Page>
</Frame>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
const newItem = ref('');
const items = reactive([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]);
const addItem = () => {
if (newItem.value.trim()) {
items.push({
id: Date.now(),
name: newItem.value
});
newItem.value = '';
}
};
const deleteItem = (index) => {
items.splice(index, 1);
};
const onItemTap = (event) => {
const item = event.item;
console.log('Tapped item:', item.name);
};
return {
newItem,
items,
addItem,
deleteItem,
onItemTap
};
}
};
</script>
<style scoped>
.form {
padding: 20;
}
.list-item {
padding: 16;
border-bottom-width: 1;
border-bottom-color: #e0e0e0;
}
.item-name {
font-size: 16;
vertical-align: center;
}
.btn {
margin: 8;
}
</style>
Vue Routeur¶
// router.js
import { createRouter } from 'nativescript-vue';
import Home from './components/Home.vue';
import Details from './components/Details.vue';
const router = createRouter({
routes: [
{ path: '/', component: Home },
{ path: '/details/:id', component: Details }
]
});
export default router;
Réagir à l'intégration¶
Configuration du projet Réagir¶
# Create React project
ns create MyReactApp --react
# Add React dependencies
cd MyReactApp
npm install react react-nativescript
Composante App¶
// app.jsx
import React from 'react';
import { StackNavigator } from 'react-nativescript-navigation';
import { Home } from './components/Home';
import { Details } from './components/Details';
const AppContainer = StackNavigator(
{
Home: {
screen: Home
},
Details: {
screen: Details
}
},
{
initialRouteName: 'Home'
}
);
export default AppContainer;
Composante réagir¶
// components/Home.jsx
import React, { useState, useEffect } from 'react';
export function Home({ navigation }) {
const [items, setItems] = useState([]);
const [newItem, setNewItem] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setIsLoading(true);
try {
// Simulate API call
const data = [
{ id: 1, name: 'Item 1', description: 'First item' },
{ id: 2, name: 'Item 2', description: 'Second item' },
{ id: 3, name: 'Item 3', description: 'Third item' }
];
setItems(data);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setIsLoading(false);
}
};
const addItem = () => {
if (newItem.trim()) {
const item = {
id: Date.now(),
name: newItem,
description: `Description for ${newItem}`
};
setItems([...items, item]);
setNewItem('');
}
};
const deleteItem = (id) => {
setItems(items.filter(item => item.id !== id));
};
const navigateToDetails = (item) => {
navigation.navigate('Details', { item });
};
return (
<page>
<actionBar title="Home" />
<gridLayout rows="auto, *">
<stackLayout row={0} className="form">
<textField
text={newItem}
onTextChange={(args) => setNewItem(args.value)}
hint="Enter new item"
/>
<button
text="Add Item"
onTap={addItem}
className="btn btn-primary"
/>
</stackLayout>
{isLoading ? (
<activityIndicator row={1} busy={true} />
) : (
<listView
row={1}
items={items}
onItemTap={(args) => navigateToDetails(args.item)}
cellFactory={(item) => (
<gridLayout columns="*, auto" className="list-item">
<stackLayout col={0}>
<label text={item.name} className="item-name" />
<label text={item.description} className="item-description" />
</stackLayout>
<button
col={1}
text="Delete"
onTap={() => deleteItem(item.id)}
className="btn btn-secondary"
/>
</gridLayout>
)}
/>
)}
</gridLayout>
</page>
);
}
Crochets de réaction¶
// hooks/useApi.js
import { useState, useEffect } from 'react';
import { Http } from '@nativescript/core';
export function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const result = await Http.getJSON(url);
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage in component
export function DataComponent() {
const { data, loading, error } = useApi('https://api.example.com/data');
if (loading) return <activityIndicator busy={true} />;
if (error) return <label text={`Error: ${error.message}`} />;
return (
<listView
items={data}
cellFactory={(item) => (
<label text={item.name} />
)}
/>
);
}
Essais¶
Essai en unité¶
# Install testing dependencies
npm install --save-dev @nativescript/unit-test-runner
npm install --save-dev jasmine
npm install --save-dev karma
npm install --save-dev karma-jasmine
npm install --save-dev karma-nativescript-launcher
# Run tests
ns test android
ns test ios
Configuration d'essai¶
// karma.conf.js
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
'app/**/*.spec.js'
],
exclude: [],
preprocessors: {},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: false,
browsers: ['NativeScript'],
singleRun: true,
concurrency: Infinity
});
};
Exemple de test unitaire¶
// tests/example.spec.js
describe('Calculator', function() {
let calculator;
beforeEach(function() {
calculator = new Calculator();
});
it('should add two numbers correctly', function() {
const result = calculator.add(2, 3);
expect(result).toBe(5);
});
it('should subtract two numbers correctly', function() {
const result = calculator.subtract(5, 3);
expect(result).toBe(2);
});
it('should multiply two numbers correctly', function() {
const result = calculator.multiply(4, 3);
expect(result).toBe(12);
});
it('should divide two numbers correctly', function() {
const result = calculator.divide(10, 2);
expect(result).toBe(5);
});
it('should throw error when dividing by zero', function() {
expect(function() {
calculator.divide(10, 0);
}).toThrow();
});
});
// Calculator class
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
}
E2E Essais¶
# Install Appium
npm install --save-dev appium
npm install --save-dev wd
# Install test framework
npm install --save-dev mocha
npm install --save-dev chai
// e2e/test.spec.js
const wd = require('wd');
const { expect } = require('chai');
describe('App E2E Tests', function() {
let driver;
before(async function() {
this.timeout(60000);
driver = wd.promiseChainRemote('localhost', 4723);
const desiredCaps = {
platformName: 'Android',
platformVersion: '10',
deviceName: 'emulator-5554',
app: './platforms/android/app/build/outputs/apk/debug/app-debug.apk',
automationName: 'UiAutomator2'
};
await driver.init(desiredCaps);
});
after(async function() {
if (driver) {
await driver.quit();
}
});
it('should display home screen', async function() {
const title = await driver.elementByAccessibilityId('home-title');
const titleText = await title.text();
expect(titleText).to.equal('Home');
});
it('should navigate to details screen', async function() {
const button = await driver.elementByAccessibilityId('details-button');
await button.tap();
const detailsTitle = await driver.elementByAccessibilityId('details-title');
const titleText = await detailsTitle.text();
expect(titleText).to.equal('Details');
});
it('should add new item', async function() {
const textField = await driver.elementByAccessibilityId('new-item-field');
await textField.sendKeys('Test Item');
const addButton = await driver.elementByAccessibilityId('add-button');
await addButton.tap();
const listItem = await driver.elementByAccessibilityId('list-item-0');
const itemText = await listItem.text();
expect(itemText).to.contain('Test Item');
});
});
Rendement¶
Gestion de la mémoire¶
// Proper cleanup
export function onNavigatingFrom(args) {
const page = args.object;
// Remove event listeners
if (page.bindingContext && page.bindingContext.cleanup) {
page.bindingContext.cleanup();
}
// Clear timers
if (page._timer) {
clearInterval(page._timer);
page._timer = null;
}
// Clear references
page.bindingContext = null;
}
// Observable cleanup
class ViewModel extends Observable {
constructor() {
super();
this._subscriptions = [];
}
addSubscription(subscription) {
this._subscriptions.push(subscription);
}
cleanup() {
this._subscriptions.forEach(sub => {
if (sub && sub.unsubscribe) {
sub.unsubscribe();
}
});
this._subscriptions = [];
}
}
Optimisation de l'image¶
// Lazy loading images
export function onImageLoading(args) {
const image = args.object;
const listView = image.parent;
// Only load images for visible items
if (listView && listView.isItemAtIndexVisible) {
const index = listView.getItemAtIndex(args.index);
if (listView.isItemAtIndexVisible(index)) {
image.src = args.item.imageUrl;
}
}
}
// Image caching
class ImageCache {
constructor() {
this.cache = new Map();
this.maxSize = 50; // Maximum number of cached images
}
get(url) {
return this.cache.get(url);
}
set(url, imageSource) {
if (this.cache.size >= this.maxSize) {
// Remove oldest entry
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(url, imageSource);
}
clear() {
this.cache.clear();
}
}
const imageCache = new ImageCache();
// Optimized image loading
export function loadOptimizedImage(imageView, url) {
// Check cache first
const cachedImage = imageCache.get(url);
if (cachedImage) {
imageView.imageSource = cachedImage;
return;
}
// Load and cache image
ImageSource.fromUrl(url)
.then((imageSource) => {
imageCache.set(url, imageSource);
imageView.imageSource = imageSource;
})
.catch((error) => {
console.error('Error loading image:', error);
});
}
Liste des performances¶
// Virtual scrolling for large lists
export function createVirtualList(container, items, itemHeight, visibleItems) {
let startIndex = 0;
let endIndex = Math.min(visibleItems, items.length);
const updateList = () => {
container.removeChildren();
for (let i = startIndex; i < endIndex; i++) {
const item = items[i];
const itemView = createItemView(item, i);
container.addChild(itemView);
}
};
const onScroll = (scrollY) => {
const newStartIndex = Math.floor(scrollY / itemHeight);
const newEndIndex = Math.min(newStartIndex + visibleItems, items.length);
if (newStartIndex !== startIndex || newEndIndex !== endIndex) {
startIndex = newStartIndex;
endIndex = newEndIndex;
updateList();
}
};
updateList();
return { updateList, onScroll };
}
// Efficient list item recycling
export function createRecyclingListView(items, itemTemplate) {
const recycledViews = [];
const activeViews = new Map();
const getRecycledView = () => {
return recycledViews.pop() || null;
};
const recycleView = (view) => {
recycledViews.push(view);
};
const createItemView = (item, index) => {
let view = getRecycledView();
if (!view) {
view = itemTemplate();
}
// Bind data to view
view.bindingContext = item;
activeViews.set(index, view);
return view;
};
const removeItemView = (index) => {
const view = activeViews.get(index);
if (view) {
activeViews.delete(index);
recycleView(view);
}
};
return { createItemView, removeItemView };
}
Déploiement¶
Déploiement Android¶
# Build debug APK
ns build android
# Build release APK
ns build android --release --key-store-path myapp.keystore --key-store-password password --key-store-alias myapp --key-store-alias-password password
# Generate keystore
keytool -genkey -v -keystore myapp.keystore -alias myapp -keyalg RSA -keysize 2048 -validity 10000
# Build AAB (Android App Bundle)
ns build android --release --aab --key-store-path myapp.keystore --key-store-password password --key-store-alias myapp --key-store-alias-password password
# Upload to Google Play Console
# Use the generated AAB file in platforms/android/app/build/outputs/bundle/release/
Déploiement iOS¶
# Build for device
ns build ios --for-device --release
# Build with provisioning profile
ns build ios --for-device --release --provision "MyApp Distribution Profile"
# Create IPA
ns build ios --for-device --release --provision "MyApp Distribution Profile" --team-id TEAM_ID
# Upload to App Store
# Use Xcode or Application Loader to upload the IPA
Intégration continue¶
# .github/workflows/build.yml
name: Build and Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Install NativeScript CLI
run: npm install -g @nativescript/cli
- name: Add platforms
run: |
ns platform add android
ns platform add ios
- name: Build Android
run: ns build android --release
- name: Build iOS
run: ns build ios --for-device --release
- name: Run tests
run: ns test android --justlaunch
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: builds
path: |
platforms/android/app/build/outputs/apk/release/
platforms/ios/build/Release-iphoneos/
Meilleures pratiques¶
Structure du projet¶
app/
├── core/ # Core functionality
│ ├── services/ # Business logic services
│ ├── models/ # Data models
│ ├── utils/ # Utility functions
│ └── constants/ # App constants
├── shared/ # Shared components
│ ├── components/ # Reusable UI components
│ ├── directives/ # Custom directives
│ └── pipes/ # Custom pipes
├── features/ # Feature modules
│ ├── home/ # Home feature
│ ├── profile/ # Profile feature
│ └── settings/ # Settings feature
├── assets/ # Static assets
│ ├── images/
│ ├── fonts/
│ └── sounds/
└── styles/ # Global styles
├── _variables.scss
├── _mixins.scss
└── app.scss
Code Organisation¶
// Use consistent naming conventions
// PascalCase for classes and constructors
class UserService {
constructor() {
this.users = [];
}
}
// camelCase for variables and functions
const userName = 'john_doe';
const getUserById = (id) => {
return users.find(user => user.id === id);
};
// UPPER_CASE for constants
const API_BASE_URL = 'https://api.example.com';
const MAX_RETRY_ATTEMPTS = 3;
// Use meaningful names
// Bad
const d = new Date();
const u = users.filter(x => x.a);
// Good
const currentDate = new Date();
const activeUsers = users.filter(user => user.isActive);
// Use consistent error handling
class ApiService {
async fetchData(url) {
try {
const response = await Http.getJSON(url);
return { success: true, data: response };
} catch (error) {
console.error('API Error:', error);
return { success: false, error: error.message };
}
}
}
// Use proper async/await patterns
// Bad
function loadUserData(userId) {
getUserById(userId)
.then(user => {
return getProfileData(user.id);
})
.then(profile => {
return getPreferences(profile.id);
})
.then(preferences => {
console.log('User data loaded');
})
.catch(error => {
console.error('Error:', error);
});
}
// Good
async function loadUserData(userId) {
try {
const user = await getUserById(userId);
const profile = await getProfileData(user.id);
const preferences = await getPreferences(profile.id);
console.log('User data loaded');
return { user, profile, preferences };
} catch (error) {
console.error('Error loading user data:', error);
throw error;
}
}
Meilleures pratiques en matière de rendement¶
// Optimize data binding
// Use one-time binding for static data
// XML: text="{{ message, mode=oneTime }}"
// Minimize property changes
class OptimizedViewModel extends Observable {
constructor() {
super();
this._batchUpdates = false;
this._pendingUpdates = {};
}
startBatch() {
this._batchUpdates = true;
}
endBatch() {
this._batchUpdates = false;
Object.keys(this._pendingUpdates).forEach(key => {
super.set(key, this._pendingUpdates[key]);
});
this._pendingUpdates = {};
}
set(name, value) {
if (this._batchUpdates) {
this._pendingUpdates[name] = value;
} else {
super.set(name, value);
}
}
}
// Use efficient list operations
// Bad
items.forEach((item, index) => {
if (item.shouldRemove) {
items.splice(index, 1);
}
});
// Good
const itemsToKeep = items.filter(item => !item.shouldRemove);
items.splice(0, items.length, ...itemsToKeep);
// Optimize image loading
const optimizeImageLoading = (imageView, url, placeholder) => {
// Show placeholder immediately
imageView.src = placeholder;
// Load actual image asynchronously
ImageSource.fromUrl(url)
.then(imageSource => {
imageView.imageSource = imageSource;
})
.catch(error => {
console.error('Image loading failed:', error);
// Keep placeholder or show error image
});
};
// Use proper cleanup
class ComponentManager {
constructor() {
this.components = new Map();
this.timers = new Set();
this.subscriptions = new Set();
}
addTimer(timer) {
this.timers.add(timer);
}
addSubscription(subscription) {
this.subscriptions.add(subscription);
}
cleanup() {
// Clear timers
this.timers.forEach(timer => clearTimeout(timer));
this.timers.clear();
// Unsubscribe from observables
this.subscriptions.forEach(sub => {
if (sub && sub.unsubscribe) {
sub.unsubscribe();
}
});
this.subscriptions.clear();
// Clear component references
this.components.clear();
}
}
Résumé¶
NativeScript fournit une plate-forme puissante pour construire des applications mobiles vraiment natives en utilisant les technologies web:
Avantages clés: - True Native Performance: Accès direct aux API natives sans WebViews - ** Partage de code** : Partager la logique d'affaires entre les plateformes tout en maintenant l'interface utilisateur native - ** Flexibilité du cadre**: Support pour Angulaire, Vue.js, Réaction et vanille JavaScript - Native UI: spécifique à la plateforme Composantes de l'assurance-chômage qui semblent natives - Écosystà ̈me Plugin: Riche écosystème de plugins pour la fonctionnalité native
Cas de la meilleure utilisation: - Applications nécessitant des performances natives et de l'assurance-chômage - Applications multiplateformes avec une logique commerciale complexe - Équipes ayant une expertise en développement web - Applications nécessitant un large accès à l'API native - Applications d'entreprise avec exigences personnalisées
Considérations: - Courbe d'apprentissage Steeper par rapport aux cadres hybrides - Plus grande taille d'application par rapport aux solutions web - Essais spécifiques à la plate-forme requis - Nécessité de comprendre les concepts de plate-forme native
NativeScript est idéal pour les développeurs qui veulent tirer parti des technologies web tout en construisant des applications mobiles vraiment natives avec d'excellentes performances et intégration de plate-forme.