17 Minuten Lesezeit This article is only available in English. JavaScript is very popular and is gaining more importance every day. The article shows you how to develop a cross-platform application with JavaScript. A demo application using Aurelia, Electron, npm, and NativeScript will show you how everything is wired together The advantages of cross-platform development are clear: you can write one application and run it on several platforms. Instead of having several applications in various languages, you are able to save a lot of code and expense. The fewer code and languages you use the fewer your bugs, distribution and maintenance effort. I wanted to see how easy it would be to support several platforms with a single language and environment. For this, I chose JavaScript which is gaining more and more impact every day and runs nearly everywhere. I will show you which frameworks I used and how cross-platform development with JavaScript could look like on the basis of some examples. The agony of choice There are different options in developing applications for multiple platforms. Xamarin, which uses C# compiled for multiple platforms using Mono, is one great approach. You can write apps and reuse a lot of C# code to support other platforms like desktop, but it lacks the ability to write applications for the web platform. Phonegap, however works well on the web and mobile but features including hardware access can get really time-consuming and you have no possibility to write desktop applications. Besides, it´s missing the native mobile look&feel to which users are used to. If you want to cover all platforms - Android, iOS, Windows Phone, MacOS, Linux, Windows and Browsers - the right JavaScript stack can be the solution. Everyone jokes that there are so many JavaScript libraries out there, but this enables us to find a stack to develop desktop, web and mobile applications with one language and share a lot of code. Future JavaScript Some of the reasons why JavaScript is on the rise might be because of more advanced frameworks, libraries and improving standards, so it becomes more and more interesting for serious software development. For using these new standards and use features like promises, arrow functions, classes and much more, you need a transpiler. With this, you can write ES6 and ES7 code and transpile it to current JavaScript afterward. For the examples here I used TypeScript from Microsoft. Unlike another famous transpiler called Babel, TypeScript gives you a static typing system which improves your code quality, makes it more robust and detects possible code failures before runtime. Cross-platform stack Node.js and Phonegap were some of the first attempts to use JavaScript across various platforms. Soon promising frameworks like Electron for desktop applications and NativeScript for developing cross compiled native apps followed. In the following picture, you can see how they fit into the chosen stack. The data is provided by a Node.js web service. Reusable code published as npm module is used by the Aurelia, Electron and NativeScript clients for consuming data from the web service. Let´s have a look at each framework. NativeScript NativeScript is a framework for cross-platform app development developed by Telerik, a company known for their UI components for different platforms. NativeScript uses native controls instead of web views and thus solves the problem we had with Cordova/PhoneGap regarding the look&feel and performance. It´s open source and highly extensible. If you want to you are able to use plugins from npm, cocoaPods or gradle directly. NativeScript even has a roadmap where they plan desktop support for Microsoft UWP and Apple OS X. Electron For bringing your JavaScript application to the desktop, Electron is awesome. It uses Chromium and Node.js and supports Windows, Mac and Linux. Electron is open source and was initially developed for the Atom editor from Github. Many known companies like Microsoft, Facebook or Slack use Electron for their Desktop apps already. Regarding desktop applications, users do not expect native looking applications, but with some effort it is possible to style Electron applications similar to Windows or MacOS applications with libraries like WinJS. Aurelia For the Electron part and the web application, I used Aurelia. Aurelia is a newer JavaScript framework that is built on web standards and allows you to write single-page applications with less framework, configuration, and boilerplate code. It is backed by a single company and invented by a person who now works at Microsoft and was in the former Angular 2 team for a while. Aurelia is still less popular but the community is growing every day. We chose it over other frameworks like Angular for several projects already and don´t regret it. Node.js The web service is written with Node.js which is really lightweight. This allows me to use JavaScript even on the server-side so you don´t need to translate between server and client side data formats. Node.js uses Chrome´s V8 JavaScript Engine and is developed by a company named Joyent,Inc. Each of the four frameworks - Aurelia, NativeScript, Node.js and Electron - supports TypeScript. This means we can use typings across the platforms and work with interfaces and classes when using shared code. Let the magic happen In the following examples, I will show you how you can develop a cross-platform application with the chosen stack. The example application will show a list of persons. You can select one by clicking on a list item. After selecting you will see that person´s profile. This list and detail view is conceived like a master-detail interface. On the list will be a kind of infinite scroll mechanism, so you can scroll down the list and it will dynamically load more data from the web service. Sometimes I will only show the interesting parts and no CSS at all. These are a lot of technologies but they are very alike and we will share a lot of code through modularization and reuse. Apart from this, most code will be for bootstrapping and wiring everything together. Node.js web service We will start with the Node.js web service so the applications are able to consume some data. We will use express because it provides a nice way of handling request handlers. For installing required npm packages, we need to execute on the console: 1 npm install --save express body-parser express-serve-static-core serve-static and for installing typings with typings (npm install typings): 1 typings install --global express body-parser express-serve-static-core serve-static For the web service, we are creating a Server class and a method for bootstrapping. In the constructor, we define some API methods which will handle incoming requests. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Server app: express.Application; static bootstrap(): Server { return new Server(); } constructor() { this.app = express(); this.app.all('*', (req: express.Request, res: express.Response, next: express.NextFunction) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.header('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Origin, Accept'); next(); }); this.app.get("/persons", (req: express.Request, res: express.Response) => { let start: number = Number(req.query.start); let limit: number = Number(req.query.limit); let response: Array<Person> = (start && limit) ? personlist.slice(start, start + limit) : personlist; res.send(response); }); this.app.get("/persons/:name", (req: express.Request, res: express.Response) => { let name = req.params.name; personlist.forEach((person: Person) => { //personlist is an array with some properties (not shown here) if (person.name === name) { res.send(person); return; } }); }); this.app.listen(3000, () => { console.log("Listening on port 3000..."); }); } } The only thing left now is to start the web service by calling Server.bootstrap(); This web service will now listen on port 3000 and return data when requested. Reusable code as npm module Next, I continue with the person-service which I will publish on npm so we can share it across the platforms. Yes, I could also just create some module and reuse it. Using npm is just because we can and because it´s pretty easy. This could make sense in a larger project when different teams are working on different pieces of code or platforms, so you could easy modularize and manage it through small packages (probably using private npm packages or self-hosted). The web service uses the new fetch standard which is not integrated into TypeScript, so you need to install a polyfill with 1 npm install whatwg-fetch --save First I create the class PersonService which will expose methods for fetching data from the published web service. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 export class PersonService { getPersons(start: number, limit: number): Promise<Array<Person>>{ return fetch('http://js-all-persons.azurewebsites.net/persons?start=' + start + '&limit=' + limit, { method: 'GET', headers: { 'Content-Type': 'application/json' } }).then((response: Response) => { return this.handleResponse(response); }).then((result: Array<Person>) => { return result; }, (e: Error) => { console.log("error", e); return null; }); } getPersonDetails(name: string): Promise<Person> { return fetch('http://js-all-persons.azurewebsites.net/persons/' + name, { method: 'GET', headers: { 'Content-Type': 'application/json' } }).then((response: Response) => { return this.handleResponse(response); }).then((result: Person) => { return result; }, (e: Error) => { console.log("error", e); return null; }); } private handleResponse(response: Response): Promise<any> { if (String(response.type) === 'opaque') { return null; } if (response.status !== 200) { console.log('Problem occured. Status Code: ' , response.status); return null; } return response.json(); } } We now have a method for a range of data and one for person details. You can easily imagine how we could also create methods or classes for business logic, data transitions or maybe even view models. I put the TypeScript definition file into the module itself in order to access the data typed. Aurelia web app Aurelia is a single-page application framework which comes with a lot of bootstrapping, tooling code and dependencies like all the other MV* Frameworks nowadays. Because I am using one of their skeletons, all we have to do to get started is to install all required packages with 1 npm install 1 jspm install 1 typings install and for installing the published person-service: 1 npm install person-service After everything is properly installed, we can create a people.html and people.ts. Because we have only one route I start straight with the only view that will contain a list of people on the left and the details for a person, on the right. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <template> <require from='../components/list-item/list-item.html'></require> <require from='../components/scroll-end/scroll-end'></require> <main class="content"> <div class="col-sm-5 col-xs-12"> <div class="person-list" scroll-end="load.call:loadPersons()"> <div repeat.for="person of persons" click.delegate="showDetails(person)" class.bind="person.name === selectedPerson.name ? 'person selected' : 'person'"> <list-item person.bind="person"></list-item> </div> </div> </div> <div class="col-sm-7 col-xs-12 details" if.bind="selectedPerson"> <div> <h2>${selectedPerson.Title}</h2> <img src.bind="selectedPerson.picture" alt="person image" if.bind="selectedPerson"/> <div class="descr-header">Profile</div> <div class="detail-list"> <div class="detail-item col-xs-6"> <div> <span class="title">Balance</span> <span class="value">${selectedPerson.balance}</span> </div> <div> <span class="title">Eye Color</span> <span class="value">${selectedPerson.eyeColor}</span> </div> ... </div> </div> </div> </div> </main> </template> This HTML contains a list-item custom element and one custom attribute the scroll-end element. The scroll-end element checks if you already reached the end of the list and then dynamically loads more data. The list-item is an HTML-only component and just encapsulated some recurring HTML code. To get some life into the view we require some data from the person-service in the related Persons class: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import { autoinject } from 'aurelia-framework'; import { HttpResponseMessage } from 'aurelia-http-client'; import { PersonService, Person } from 'person-service'; //importing the PersonService (our npm module) and the typings for Person @autoinject() export class Persons { persons: Array<Person> = new Array<Person>(); selectedPerson: Person; itemRange: number = 20; //start with 20 items itemStart: number = 0; constructor(private personService: PersonService) { } activate() { this.loadPersons(); } loadPersons() { this.personService.getPersons(this.itemStart, this.itemRange).then((data: Array<Person>) => { this.persons = this.persons.concat(data); this.itemStart += 10; }, error => { console.log("Error loading persons.", error) }); } showDetails(choosePerson: Person) { this.personService.getPersonDetails(choosePerson["name"]).then((data: Person) => { this.selectedPerson = data; }, error => { console.log("Error loading persons.", error) }); } } The used Aurelia skeleton comes with gulp so we only have to execute: 1 gulp serve and the application is running transpiled in a local web server and reachable over localhost:9000. Electron desktop app With Electron you can wrap web applications into desktop applications. For this, Electron uses the chromium web driver and Node.js and offers you a lot of packages to communicate with the underlying system. To getting started you need to download these packages 1 npm install electron -g for prebuilding the desktop app and 1 npm install electron-packager -g for packaging the app into a specific platform executable. Next, you have to create a commonly named main.js startup file that looks like this: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 'use strict'; const electron = require('electron'); const {app, BrowserWindow} = electron; let mainWindow; mainWindow.setMenu(null); mainWindow.maximize(); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('ready', () => { mainWindow = new BrowserWindow({ width: 800, height: 600 }); mainWindow.loadURL(`file://${__dirname}/index.html`); //define app entry point mainWindow.webContents.on('did-finish-load', () => { mainWindow.setTitle(app.getName()); }); mainWindow.on('closed', () => { mainWindow = null; }); You need to add a reference to this main.js file into your package.json so the electron command knows where to start. 1 2 3 4 5 { ... "main": "main.js" ... } That´s all configuration needed. The Aurelia TypeScript skeleton that I used, already includes an index.js and a README.md section about Electron. So you just need to add the provided index.js into your package.json and execute everything with 1 electron . or build it with : 1 electron-packager . 'person-electron-app' --platform win32 --version 1.3.8 for needed platform and project name/folder. Electron is not only good for wrapping a web app into an executable. Because it´s not a sand-boxed web browser you are able to access the filesystem, user shell and more, so you can write real desktop applications with it. NativeScript app NativeScript uses reflection to access the native device API´s and additionally offers a lot of wrappers around them. NativeScript comes with a mighty CLI integration similar to the Cordova one. To work with this CLI you firstly need to install it with the command: 1 npm install -g nativescript dependent on your platform you have to install the environments and some tools for building Android and iOS apps. Happily NativeScript helps you with this, you can read more about it in the official install guide. After everything is properly installed and your platforms are added, you can create your project with the command 1 tns create PersonApp --template tns-template-master-detail-ts There are a lot of templates for NativeScript, so you don´t need to waste time on the project setup and can just start coding. Here I used a template for a master-detail view with TypeScript. The app/app.ts inside the project is the first executed file, here we have to reference the starting view. I renamed the files and put them into folders so the app.ts looks like this now 1 2 3 4 import * as application from "application"; application.cssFile = "app.css"; application.mainModule = "person-list/person-list"; application.start(); I installed the person-service from npm and added methods for page load, item tapping and routing into the person.ts. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ... import { PersonService, Person } from "person-service"; let personService: PersonService = new PersonService(); let persons: ObservableArray = new ObservableArray([]); let itemRange: number = 20; let itemStart: number = 0; let twoPaneLayout = Math.min(platform.screen.mainScreen.widthDIPs, platform.screen.mainScreen.heightDIPs) > 600; export function loaded(args: EventData) { let pageData = new Observable({ persons: persons }); loadPersons(); (args.object).bindingContext = pageData; } export function loadPersons(args?: EventData) { personService.getPersons(itemStart, itemRange).then((data:Array) => { persons.push(data); itemStart += 10; }, error => { console.log("Error loading Persons." , error) } ); } export function showDetails(args: ItemEventData) { let selectedPerson: Observable = new Observable(); let navigationEntry = { moduleName: "details/details-page", context: selectedPerson }; personService.getPersonDetails(args.view.bindingContext.name).then((data: Person) => { selectedPerson.set('selectedPerson', data); if (!twoPaneLayout) { frames.topmost().navigate(navigationEntry); } }, error => { console.log("Error loading persons.", error) }); }; Everything you set on the bindingContext will be available in the corresponding view. In the code above I created the object pageData. We can push items to the persons Array at any time and because the pageData object is an Observable the view will update itself with the new data. The view is written in an XML syntax similar to how you would define the view in native iOS or Android. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <Page loaded="loaded" class="content"> <ActionBar title="Persons" class="page-title"> <StackLayout orientation="horizontal" ios:horizontalAlignment="center" android:horizontalAlignment="left"> <Label text="Persons"/> </StackLayout> </ActionBar> <StackLayout class="article-list"> <!-- here we register the tab handler --> <ListView items="{{ articles }}" itemTap="listViewItemTap"> <ListView.itemTemplate> <StackLayout orientation="horizontal" class="article"> <StackLayout orientation="vertical"> <Image src="{{ picture }}" class="img"/> </StackLayout> <StackLayout orientation="vertical" class="descr"> <Label text="{{ name }}" textWrap="true" class="title-name" /> <Label text="{{ about }}" textWrap="true"/> </StackLayout> </StackLayout> </ListView.itemTemplate> </ListView> </StackLayout> </Page> For the case that we are on a device with a width larger than 600px and want to show the master and the detail view next to each other, we can define XML views with a specific name schema. So I create a second view with the name person-list.minWH600.xml which looks like this: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <Page loaded="loaded" class="content"> <GridLayout rows="auto, *" columns="300, *"> <ActionBar title="Persons" class="page-title"> ... </ActionBar> <StackLayout class="article-list"> ... </StackLayout> //if an item got selected, include the details view here <GridLayout row="1" col="1" bindingContext="{{ selectedItem }}"> <app:details-view/> </GridLayout> </GridLayout> </Page> As you can see we used a lot of CSS classes. We are able to style the components with basic CSS (NativeScript only supports a subset) so we can reuse a lot of rules from the web app. For really writing the CSS just once, the best approach probably would be to start with the app CSS and think at first about how to slice the styles. For this example, I put all the style definitions into the app.css. The corresponding details-page looks pretty simple, because we are just listening to the navigate event and setting the page context. 1 2 3 4 5 6 7 8 import { EventData } from "data/observable"; import { Page } from "ui/page"; import * as scrollViewModule from "ui/scroll-view"; export function pageNavigatedTo(args: EventData) { var page = <Page>args.object; page.bindingContext = page.navigationContext; } For the view we can reuse some of the structure and just replace the div´s and span´s through StackLayout´s and Labels. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <Page navigatedTo="pageNavigatedTo"> <ActionBar title="Details" class="page-title"> <NavigationButton /> <StackLayout orientation="horizontal" ios:horizontalAlignment="center" android:horizontalAlignment="left"> <Label text="Details"/> </StackLayout> </ActionBar> <ScrollView> <StackLayout class="detail-list"> <Label text="{{ name }}" class="descr-name"/> <StackLayout orientation="vertical"> <Image src="{{ picture }}" class="detail-img"/> </StackLayout> <Label text="Profile" class="descr-header"/> <StackLayout class="detail-item"> <Label text="Balance" class="title"/> <Label text="{{ about }}" class="value"/> </StackLayout> <StackLayout class="detail-item"> <Label text="Eye Color" class="title"/> <Label text="{{ eyeColor }}" class="value"/> </StackLayout> ... </StackLayout> </ScrollView> </Page> we can run the app with several commands, I used tns livesync android which is pretty cool because it reloads your app as soon as you save your changes in the editor. The time will come Okay, we are not there yet, this is not the bright future you may have imagined. Granted, transforming a web application to a desktop one with only some additional code is really awesome. The code we had to write for all different platforms was not much, but the code we were able to reuse, especially with Aurelia and NativeScript, was not that much too. I had to nearly rewrite the whole view and reusing CSS also works not just out of the box. These little examples demonstrate how much you can achieve with a bit of JavaScript and how easy you can support several platforms. I am already convinced that these frameworks, JavaScript itself and the possibilities regarding cross-platform development will improve from day to day until we are finally there and that JavaScript is a real player for cross-platform development.
Medizintechnik & Gesundheitsindustrie – Medizingeräte agil entwickeln – darauf kommt es an Mehr erfahren
Digitalisierung und Disruption – AR-Brille zum Selbstbauen – Project North Star im Test Mehr erfahren