make webapp && make ios && make android - an experiment with JavaScript/NodeJS and Phonegap
This is the repo of the app AtOneGo
This file contains the documentation for developers.
Author: Michael Wager mail@mwager.de
Native App via Phonegap for iOS and Android. (WebApp served via Node.js)
- Zepto 1.0
- Require.js
- Lodash/Backbone.js 1.0.0
- SocketIO (0.9.x, not used anymore)
- mobiscroll (date & time scroller)
- Parts from Html5Boilerplate v4.X
- Grunt
- Inspired by the TodoMVC project and Yeoman
- Phonegap (v3.x)
API and Website via Node.js (at-one-go.com)
- Express framework (v3)
- Mongoose (v3)
- see
api/package.json
- Hosting at OpenShift (free plan)
- Mocha: bdd client- and serverside
- PhantomJS/CasperJS: headless webkit testing
- testem
Installiere alle Plugins:
NOTE: To update cordova plugins we need to remove and re-add !
cordova plugin add org.apache.cordova.console && \
cordova plugin add org.apache.cordova.device && \
cordova plugin add org.apache.cordova.dialogs && \
cordova plugin add org.apache.cordova.network-information && \
cordova plugin add org.apache.cordova.splashscreen && \
cordova plugin add org.apache.cordova.statusbar && \
cordova plugin add org.apache.cordova.vibration && \
cordova plugin add org.apache.cordova.globalization && \
cordova plugin add https://github.com/phonegap-build/PushPlugin.git
Remove all:
cordova plugin rm org.apache.cordova.console && \
cordova plugin rm org.apache.cordova.device && \
cordova plugin rm org.apache.cordova.dialogs && \
cordova plugin rm org.apache.cordova.network-information && \
cordova plugin rm org.apache.cordova.splashscreen && \
cordova plugin rm org.apache.cordova.statusbar && \
cordova plugin rm org.apache.cordova.vibration && \
cordova plugin rm org.apache.cordova.globalization && \
cordova plugin rm com.phonegap.plugins.PushPlugin && \
rm -rf plugins/ios.json && rm plugins/android.json
/app
- app sources (yeoman requirejs/backbone boilerplate)/api
- node.js sources (REST API, SocketIO, DB, Tests, etc)/api_deployment
- The openshift repo (we just copy the sources fromapi
to this directory and push it up to openshift)/api/server/website
- Static files of the website at-one-go.com/api/server/website/app
- The WebApp will be served from here (optimized sources from/dist
will be copied to this directory viamake webapp
)/api/server/test
- All backend tests/test
- All frontend tests/dist
- Created via Grunt. The optimized sources will be used in the phonegap app and the webapp/mobile/
- Phonegap project directory (v3.x)/docs
- All software documentation
Makefile
- The Makefile for everything/app/index.html
- The base html file for the phonegap app (goes to/mobile/ios/www/
or/mobile/android/assets/www/
via Makefile)/app/webapp.html
- The base html file for the web app (goes toapi/server/website/app/
via Makefile)
/app/scripts/config.js
- The RequireJS config file for development (see also/app/scripts/config.production.js
)/app/scripts/main.js
- The main bootstrapper, all initial event handling (domready/deviceready, global click/touch handlers, global ajax config...)/app/scripts/router.js
- The AppRouter, all client side navigation is done via history api (pushstate is on phonegap apps not needed). All routes of the app are defined here, and the router takes care of the rendering of root-views (screens)
$ cd path/to/your/projects
$ git clone repo-url.git atonego
$ cd atonego
# install local build system using grunt [optional]
$ npm install
# NOTE: The folder `atonego` should be served via a locally installed webserver like apache
$ open http://127.0.0.1/atonego # should serve index.html now
Via phonegap
$ make ios_build_dev && clear && t mobile/ios/cordova/console.log
Checkout the Makefile
for more information.
A RESTful API for the app is written in JavaScript using Node.js, the website at-one-go.com and the webapp will be served through Node.js too.
NOTE: The production config file api/server/config/environments/production.json
is not under version control.
$ cd api
$ npm install # install dependencies only once
$ mongod & # start mongodb if not already running
$ node server.js # start the node app in development mode
Now checkout something like:
# in the project root run:
$ jshint . # see .jshintrc and .jshintignore
Before committing, jshint MUST return zero:
$ jshint . # see .jshintrc and .jshintignore
$ echo $? # output 0 ?
# enable jshint git "pre-commit" hook
touch .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
echo "jshint ." > .git/hooks/pre-commit
Inspired from here
- Space indentation (4)
- Single-quotes
- Semicolons
- Strict mode
- No trailing whitespace
- Variables at the top of the scope (where possible)
- Multiple variable statements
- Space after keywords and between arguments and operators
- JSHint valid (see rules in the project root)
Example: (client side using requirejs)
define(function(require) {
'use strict';
var a = require('a'),
b = require('b');
function Foo() {}
Foo.prototype = {
bar: function() {
return 'baz';
}
};
return Foo;
});
We mark problems and todos in the code comments via the marker XXX
since using the well known marker TODO
in a todo app seems not to be a good idea.
All following commands should run without errors: (all from the project root)
$ cd api && npm test
$ testem ci
$ casperjs test test/functional
# there is a command for all of them:
$ make all_tests
All server side unit & functional tests (bdd style) in: /api/server/test
.
$ cd api
$ npm test # mongod running? (possibly in another shell)
Tests in /test
.
Clientside UNIT Tests via Mocha
and testem
.
CodeCoverage /app/scripts
via Blanket.js
, see Tests in the Browser via testem
.
$ cd project_root
$ testem # default mode - Browsers can run the testsuite at http://localhost:7357
$ testem ci # ci mode - run the suite in all available browsers
Execute the tests in a simulator or on a device:
$ make test_build # copies `/app` und `/test` in mobile's `www` directories
# after that, the file config.xml (ios/android) has to be edited:
<content src="test/index_browser.html" />
Then just run:
$ make ios # build the phonegap app with the current content of `mobile/ios/www`
$ make android # same for `mobile/android/assets/www`...
# shortcuts (config.xml!)
$ make test_build && make ios
$ make test_build && make android
Tests in /test/functional
via (casperjs.org)[http://casperjs.org].
# NOTE: Node and MongoDB must be running locally
$ casperjs test test/functional
See .travis.yml
and the travis page.
Checkout weinre
# Install & Run:
$ [sudo] npm install -g weinre
$ weinre --boundHost 192.168.1.233 --httpPort 8081 --verbose --debug --deathTimeout 60 --readTimeout 60
>>> 2013-03-28T11:27:10.401Z weinre: starting server at ...
Then include smt like this in app/index.html
right after <body>
:
<script type="text/javascript">
window.onerror = function(e) {
alert(e);
}
</script>
<script src="http://192.168.1.233:8081/target/target-script.js#anonymous"></script>
and open this page with a browser.
We use HTTP Basic auth over SSL everywhere. On login (or signup), a secret API TOKEN gets generated from the user's ID and a random string. This token will be encrypted via AES and sent over to the client. As RESTful APIs should be stateless, each following request must include this token in the password
field of the Authorization
-Header to authenticate against the service.
Example:
Authorization: "Basic base64encode('$username:$API_TOKEN')"
NOTE:
We cannot use custom certs and stuff on the openshift free plan so we cannot determine via node if the incoming request is secure (ssl) or not.
- Helpful Link: Best Practices for Designing a Pragmatic RESTful API
- stackoverflow: Best Practices for securing a REST API
The App and the API/website were developed with support for multilingualism. The following directories include all Texts:
- /app/scripts/libs/locales -> Texts of the App
- /api/server/locales -> Texts of the Website & API
Models are always returning the Error as the first parameter in the callback (Node.js-Style, null
on success), the second parameter can be used as return value, e.g. callback(null, user)
).
// in a model-method e.g. todolist.fetchList()
if (!list) {
utils.handleError('damnit some error message'); // optional logging
return callback({key: 'listNotFound'});
}
return callback(err);
// later: (e.g. in controllers)
if(err && err.key) {
// Error already logged (Log-files)
// Usage of key via `__(key)`
var text = __(err.key);
// `text` is now smt like "List not found" or
// "Liste nicht gefunden" (e.g. based on current request)
return displayErrToUserSomehow(text);
}
Be sure to check out the Makefile
for more infos.
Via PhoneGap for iOS [and Android]. There is a Makefile
for automating tasks
like optimizing the sources or compiling the native Apps via Phonegap.
We generate one optimized JavaScript file (aog.js
) via the requirejs optimizer, which will look something like this.
$ make clean # clean build directories
$ make ios_device # optimize sources, copy to ios `www` and build
$ make ios # build for ios via phonegap cli tools
$ make android # build for android via phonegap cli tools
# NOTE: Shortcuts for running and logging: (phonegap catches "console.log()" calls)
# We want a clean log file: (running in simulator)
# 1. iOS
$ echo "" > mobile/ios/cordova/console.log && make ios_build && clear && t mobile/ios/cordova/console.log
# 2- Android
# be sure to connect a real device before running the following one, else the android simulator could screw up your system (-;
$ make android_build && make android_run && clear && adb logcat | grep "Cordova"
Then just switch "Run" and "Archive" configs to "Distribution" under "edit scheme..." in xcode.
We use git tags for versioning. However, the file mobile/www/config.xml
[and api/package.json
and AndroidManifest.xml
] should be manually updated on releases.
The API has its own repository at openshift. (URL: atonego-mwager.rhcloud.com) We are using a "Node.js-Catridge", default Node-Version is 0.6.x (May 2013), but of course we want a newer version of Node.js, so we also set up this:
https://github.com/ramr/nodejs-custom-version-openshift
NOTE: this requires additional files (see /.gitignore
).
# 1. This command will optimize the sources using grunt and copy the generated stuff from `/dist/` to `/api/server/website/app/`:
$ make webapp
# 2. This command copies the sources from `api/*` over to `api_deployment/`
# and pushes the stuff from there up to the openshift server.
$ make api_deploy
# restart from cli:
$ make api_restart
# install: (needs ruby)
$ gem install rhc
> rhc app start|stop|restart -a {appName}
> rhc cartridge start|stop|restart -a {appName} -c mysql-5.1
When you do a git push, the app and cartridges do get restarted. It is best if you can find any indication of why it stopped via your log files. Feel free to post those if you need any further assistance.
You can access your logs via ssh:
> ssh $UUID@$appURL (use "rhc domain show" to find your app's UUID/appURL
> cd ~/mysql-5.1/log (log dir for mysql)
> cd ~/$OPENSHIFT_APP_NAME/logs (log dir for your app)
# RESTART DATABASE ONLY:
$ rhc cartridge start -a atonego -c rockmongo-1.1
# RESTART APP ONLY:
$ rhc app start -a atonego
Fix quota errors at openshift: (https://www.openshift.com/kb/kb-e1089-disk-quota-exceeded-now-what)
$ ssh ...
$ du -h * | sort -rh | head -50
$ rm -rf mongodb-2.2/log/mongodb.log*
$ rm -rf rockmongo-1.1/logs/*
$ echo "" > nodejs/logs/node.log
# and remove the mongodb journal files:
$ rm -rf mongodb/data/journal/*
Locally:
rhc app tidy atonego
openshift
On the Production-Server at openshift, there runs a minutely cronjob, checking all todos with notifications, so Email-, PUSH- and SocketIO-messages can be sent to notify users.
See api/.openshift/cron/minutely/atonego.sh
The cronjob has its own logfile:
$ ssh ...to openshift....
$ tail -f app-root/repo/server/logs/production_cronjob.log
The mobile projects were created like this:
-
downloaded: PhoneGap2.x (current 2.9.x
cat mobile/ios/CordovaLib/VERSION
) -
created ios and android projects
$ mkdir mobile && cd mobile $ alias create="/path/to/phonegap-2.7.0/lib/ios/bin/create" $ create ios de.mwager.atonego AtOneGo $ alias create="/path/to/phonegap-2.7.0/lib/android/bin/create" $ create android de.mwager.atonego AtOneGo
- download latest from phonegap.com
- checkout guides per platform
- copy all stuff manually
- iOS: check this
NOTE: see "Cronjobs" und also the demo script: api_deployment/server/ssl/push_demo.js
# run demo script via
### DEBUG=apn node ./api_deployment/server/ssl/push_demo.js
DEBUG=apn node ./api/server/push_demo.js
Some links:
In the Apple dev member center:
- create
development
provisioning profile for app idde.mwager.atonego
, download and install - create
development
push certificate within the app idde.mwager.atonego
- download this certificate, open with keychain access, export private key (
.p12
file) - checkout this PUSH Tutorial to create the certificates for the server
Testing the certificate:
Note: use gateway.sandbox.push.apple.com
in development (same port)
$ openssl s_client -connect gateway.push.apple.com:2195 -cert api/server/ssl/ck.pem -key api/server/ssl/ck.pem
# Output should be smt like:
Enter pass phrase for api/server/ssl/ck_dev.pem:
******
CONNECTED(00000003)
...
Server certificate
-----BEGIN CERTIFICATE-----
....
- create ssl files
- see
api/server/ssl/push_demo.js
-> sends a push message to hardcoded device token
See the GitHub Page.
Installed via plugman
:
$ cd mobile
$ plugman --platform android --project ./platforms/android --plugin https://github.com/phonegap-build/PushPlugin.git
$ plugman --platform ios --project ./platforms/ios --plugin https://github.com/phonegap-build/PushPlugin.git
Avoid underscores in files and folders because Phonegap may fail to load the contained files in Android. This is a known issue.
In a mobile environment like phonegap, memory management will be much more important than in the web. Variables in the global namespace are not cleaned up by the js engine's garbage collector, so keeping our variables as local as possible is a must.
To avoid polluting the global namespace as much as possible, Zepto, Backbone and some other files in /app/scripts/libs
were edited, see "atonego".
Libs which are still global:
window._
->/app/scripts/libs/lodash.js
window.io
->/app/scripts/libs/socket.io.js
[not used anymore]
Zepto's touch module was edited to prevent lots of strange errors like:
TypeError: 'undefined' is not an object file:///var/mobile/Applications/XXXXXXXXXX/atonego.app/www/scripts/libs/zepto.js on line 1651
Search /app/scripts/lib/zepto.js
for "atonego".
On iOS devices, there were problemes with the api endpoint at https://atonego-mwager.rhcloud.com/api
. The following workaround is necessary!
The file /mobile/ios/atonego/Classes/AppDelegate.m
was edited: (at the bottom)
@implementation NSURLRequest(DataController)
+ (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host
{
return YES;
}
@end
The files /app/styles/ratchet.css
and /app/scripts/libs/junior_fork.js
were edited to match our requirements.
Thanks to:
- ratchet project
- Some inspiration for rendering Backbone views with nice animations: junior.js project
The "disabled state" ("<button ... disabled .../>") will not be captured. So on every "tap" we must check if the element has an disabled attribute or class.
This is one of the most annoying problems I have ever had. Checkout main.js
and router.js
for some workarounds.
UPDATE: We do not use socket io anymore as its kind of senseless having an open connection in a todo-app. PUSH notifications should be enough. If you want to use websockets via phonegap on iOS, better do not use socketio version 0.9.
The app always crashed on resume after a (little) longer run. I was about to give up, then I found this
SocketIO's "auto reconnect" somehow crashed the app on the iOS test devices (seg fault!). As a workaround I disconnect the websocket connection on Phonegap's pause
-event, and manually re-connect (using same socket/connection again) on the resume
-event.
See also:
- Create the *.IPA file via XCode: check "iOS device", then: Product > Archive
- Create provisioning profile, download, copy profile via organizer to device , (dblclick installs in xcode first)
- XCode: update codesigning identity according to the downloaded provisioning profile (project AND target)
A minutely cronjob runs on the server, checking all todos which are due now, so we can notify users. However, I could not figure out a good solution to re-use my existing (running) node app for this. The current workaround is to listen for a POST to a specific URL, and POSTing to that URL via curl
from the cronjob with some pseudo credentials set to "make sure" that the request came from the shell script, not from outside )-:
Search /api/worker.js
-> "cron"
Open the app in chrome or safari (dev or live), then open the dev console and put in some of the following commands to play with the app:
# as there is (almost) nothing global, we must require stuff first, use this as template:
> var $ = require('zepto'), app = require('app'), common = require('common');
# then try some of these (-:
> app.VERSION
> app.isMobile
> app.changeLang('de') // or app.changeLang('en') if currently in german
> app.router.go('help')
> window.history.back()
> var list = app.todolists.get('object id of list from url or app.todolists');
> list.toJSON()
// URL: #todolists
list.set('title', 'hello world')
> app.fetchUser() // watch network tab
var list = app.todolists.get('get id hash from url');
list.set('title', '');
See docs/TODOs.md