Giter Club home page Giter Club logo

jsappview's Introduction

JSAppView

JSAppView is a swift class that extends iOS's WKWebView class to offer clean persistent storage APIs, easy inclusion of local files in the DOM (as in: <img src="bg.png">), and a smoother development experience in general.

Setup in Swift + Xcode

Add all JSAppView files to an Xcode project, along with core web app files. In the main storyboard, add a WKWebView with a subview, and attach it to ViewController.swift. It should look as shown below.

import WebKit

class ViewController: UIViewController {

    @IBOutlet var appview: JSAppView!
    override func loadView() {
        super.loadView()
        appview = JSAppView(viewController: self)
        appview.ready()
        self.view = appview
    }
}

In Xcode, the file tree should look like this:

- appName/
  - www/
    - bg.png
    - index.html
    - myapp.js
    - style.css
    - ...
  - JSAppView/
    - JSAppView.js
    - JSAppView.swift
    - JSAppViewFileSystem.swift
    - JSAppViewSQLite.swift
  - AppDelegate.swift
  - ViewController.swift
  - Main.storyboard
  - Assets.xcassets
  - LaunchScreen.storyboard
  - Info.plist

To use local networking for development purposes, add the following to info.plist:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsLocalNetworking</key>
    <true/>
</dict>

Javascript API

On the main WKWebView javascript thread (i.e., in your web app), you can access window.JSAppView_fs, window.JSAppView_path, and window.JSAppView_sqlite. However, tools like Webpack and Browserify can alias these variables to make things look and feel more like node.js. Here's an example webpack.config.js file:

module.exports = {
  entry: './src/index.js',
  output: {
    path: __dirname,
    filename: './build/bundle.js'
  },
  target: 'node',       // Tell webpack to assume the environment is node.
  node: {
    __dirname: false,   // Tell webpack not to define this.
    __filename: false,  // Tell webpack not to define this.
  },
  externals: {
    fs: 'JSAppView_fs',
    path: 'JSAppView_path',
    sqlite: 'JSAppView_sqlite'
  }
}

Using the above in combination with webpack, fs, path, and sqlite can be used with require calls as follows:

NOTICE: Failing to configure the target (and node) may result in the __dirname and __filename variables having incorrect values in certain contexts because webpack uses these variables to bundle the code (webpack assumes, incorrectly in our case, that / will work as a file system root).

const fs = require('fs')
const path = require('path')
const sqlite = require('sqlite')

// Do stuff.

File System

The file system module mimics the node.js fs API, but functions return promises instead of accepting callbacks. Performance is good - much better than the Cordova file plugin and internal web-server-based implementations, in my experience. A readdir call to a directory containing 1,500 files takes 15-18ms; readFile resolves data in 1-4ms.

At present, the fs functions assume that you're working in the Documents directory. Function like mkdir and secure handling of full file paths for reading and writing files are in todo status.

const fs = window.JSAppView_fs

__dirname                                        // 'file://.../Documents'
__filename                                       // 'file://.../Documents/index.html'

fs.exists(basename:String)                       // Promise<Boolean>
fs.stat(basename: String)                        // Promise<Object> - {birthtime, mtime, size}
fs.readFile(basename:String, encoding:String)    // Promise<String> - base64 or utf8
fs.writeFile(basename:String, data:String)       // Promise<Void>
fs.unlink(basename:String)                       // Promise<Void>
fs.readdir(dirpath:String)                       // Promise<Array<String>>
fs.downloadToFile(url:String, basename:String)   // Promise<Object> - {url, status}
fs.downloadFiles(urls:Array<String>)             // Promise<Array<Object>>

About downloadFiles

downloadFiles can fetch large numbers of files efficiently. It's asynchronous all the way down, and has been tested on LAN connections with mass downloads of more than 1,500 files (>1gb in all). The tasks finish in 10-15 seconds, with no visible degradation in the webview's DOM, animation, or general javascript performance.

Promise: The Array<Object> resolved by fs.downloadFiles is of the form:

[
  {url: 'http://...', result: 'success'},
  {url: 'http://...', result: new Error('...')},
  ...
]

Progress: The fs.downloadFiles function offers a course progress-tracking API, which reports the number of downloads done and the total number to be done. (N.b.: unless the files are of the same size presenting done ÷ total to the user as a float or percentage invites a false impression of precision; it may be better to report "13 of 61 files downloaded").

Here's an example using the .then()-style:

function updateDOM(done, total) {
  domElement.innerText = `${done} of ${total} downloaded`
}

fs.downloadFiles(urls)
  .progress(updateDOM)
  .then(doSomething)
  .catch(handleErr)

And here's the same example using async/await:

try {
  const results = await fs.downloadFiles(urls).progress(updateDOM)
  doSomething(results)
} catch(err) {
  handlerErr(err)
}

Path Module

The path module mimics a subset of the node.js module of the same name, but dispenses with non-POSIX functionality, and with functionality aimed at complex path parsing and generation, since JSAppView keeps everything in a single, flat directory.

const path = window.JSAppView_path

path.join(__dirname, 'log.txt')   // 'file://.../Documents/log.txt'
path.basename(fpath)              // 'log.txt'
path.basename(fpath, '.txt')      // 'log'
path.dirname('/foo/bar/baz')      // '/foo/bar/'
path.extname('log.txt')           // '.txt'
path.isAbsolute(__dirname)        // true

Safari

A simple API is provided enabling the JS app thread to background the app and open a link using Safari. This is particularly useful for itms-services links, which can be used trigger over-the-air updates for enterprise applications.

JSAppView.openUrlInSafari('itms-services://?action=download-manifest&url=...')

App Build Information

The window.__build object provides basic information about the iOS bundle, which is useful when, e.g., checking for updates, or determining whether the app should use a development or production server address at runtime.

window.__build.version  // '1.0'
window.__build.env      // 'debug', 'release', or

To get useful information from the env member, you must create two swift flags in XCode. Open Build Settings and type "swift flags" into the filter field. Add a debug flag with -DDEBUG as the value, and add a release flag with -DRELEASE as the value. XCode informs JSAppView whether the app is being built for testing/debugging, or for distribution. If the flags are not setup, env will default to not specified.

Console.log

Logging data with console.log or console.error will print output in both the browser console and in XCode. Circular and redundant objects and arrays are handled effectively, and types are stated explicitly for primitive values in the XCode console. For example:

console.log('Hello world!')

Produces the following in the XCode console:

<JSAppView>

  String: Hello world!

</JSAppView>

Care has been taken to ensure that the web view provides ample error data to window.onerror without requiring crossorigin script includes and CORS headers. This makes it possible to debug HTML5 apps without constant recourse to the Safari Web Inspector.

SQLite (@todo)

A single SQLite database is created and made available to your app, via an .sqlite member, to which one or more semicolon-separated which SQL statements can be passed. The function returns a promise which resolves results or rejects with errors. Under the hood, this is done via the SQLite C interface, without third party libraries, wrappers, helpers, etc.).

const sqlite = window.JSAppView_sqlite

try {
  const results = await sqlite('select * from ...')
  doSomething(results)
} catch (err) {
  console.log(err)
}

jsappview's People

Contributors

jeffmcmahan avatar

Watchers

 avatar

jsappview's Issues

fs.readFile(... 'utf8') fails for files containing grave accents (`)

The bridging code wraps utf8-encoded strings in back ticks to create a multiline string, passed to the fs.readFile promise resolution callback. There's no way to avoid this, since any string delimiter used to create the string may also be used within it. The solution is to escape the back ticks in JSAppViewFileSystem.readFileAsUtf8(...) before creating the multiline string, and then unescape them afterward.

Workaround (1) - Use base64 encoding and decode it with window.atob() within the JS context.

Workaround (2) - If reading JS files, don't read and eval. Just inject a script include with the src set the file's basename, and you get the same thing done faster.

downloadToFile API is weird

It'd be better if it followed writeFile and either rejected with a reason or resolved a string. Currently, it works like downloadFiles because they underlyingly use the same download method.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.