If you work in an organisation that runs Windows, an HTML Application (HTA) is a handy way to distribute browser-based tools as a single file, which needs nothing additional installed to run and doesn't need to have a multi-MB runtime distributed with it.
HTAs run Internet Explorer with the permissions of a "fully trusted" application, bypassing cross-domain restrictions for HTTP requests and granting access to the filesystem and, well... pretty much everything else on Windows (e.g. you don't need the data: URI hack to export an HTML table to Excel when you can create an Excel.Application
object!)
I've recently been using React (for painless UI components) and superagent (for HTTP requests) to develop apps which directly hit the REST API of our intranet installation of Crucible to slice and dice Project and Code Review data in various ways.
Actually developing an app as an HTA is a hellish experience, so the goal of this template is to allow you to develop the majority of the app in a proper browser (keeping the limitations of IE in mind and learning new ones as you go!) and only have to drop down to HTA for testing and HTA-only code.
This template is the skeleton of the second app I've built this way - its build automates a bunch of stuff I was doing manually first time around, like creating a final HTA build which bundles all the project's minified JavaScript and CSS into a single file.
<app>
├── build build directory
├── dist
│ ├── browser final browser build
│ └── hta final HTA build
├── public
│ └── css
│ └── style.css app-specific CSS
├── src
│ └── app.js entrypoint for the application
├── templates
│ ├── index.html template for browser version: handles .min.(css|js) extensions
│ └── <app>.hta template for HTA version, inlines all CSS & JS
├── vendor
│ └── css vendored CSS (e.g. Bootstrap)
└── server.js dev server
npm run dist
- build all dependencies, build browser version & build HTA versionnpm start
- run the dev server to serve up the browser version and proxy HTTP requests to external APIs
Gulp Build
Running gulp
or gulp watch
will lint and rebuild (the browser version, by default) every time anything in /src
, /public/css
or /templates
is modified.
If you break browserification of the source code (usually bad syntax or a bad require()
) the build will beep twice. It will beep once when the problem is subsequently fixed.
Tasks which will need tweaks from project to project, based on dependencies:
deps
- bundle all CSS and JavaScript dependencies into single files and create minified versionsjs-deps
- use browserifyrequire()
calls to bundle (and alias, if necessary) JavaScript dependencies, installed via npmcss-deps
- concatenate and minify/vendor/css
build
- bundle app code, starting from/src/app.js
. Dependencies from thejs-deps
tasks must be mirrored by calls to browserify'sexternal()
here
Flags which can be passed to the Gulp build:
--production
- when passed, minified versions of CSS & JS will be generated and used. Otherwise, original versions will be used and the browser version's JavaScript will include a sourcemap.--runtime=(browser|hta)
- controls which version gets built. Defaults to"browser"
. You should only need this if you want to build the HTA version on every change while testing it or working on HTA-only code:gulp watch --runtime=hta
Environment variables, envify
and uglify
The Gulp build sets up the following environment variables for use in your code. The build uses envify
, so references to these in your code will be replaced with a string containing the value of the environment variable:
process.env.RUNTIME
-"browser"
or"hta"
, depending on which version is being builtprocess.env.NODE_ENV
-"development"
or"production"
, depending on which version is being builtprocess.env.VERSION
- the contents of the version field frompackage.json
This allows you to fence off code for each version - e.g. to hit an API URL directly from the HTA version, but go through the dev server's /proxy
URL for the browser version, you might do something like:
var req
if ('browser' === process.env.RUNTIME) {
req = superagent.post('/proxy').send({
url: url
, username: credentials.username
, password: credentials.password
})
}
if ('hta' === process.env.RUNTIME) {
req = superagent.get(url).auth(credentials.username, credentials.password)
}
req.accept('json').end(function(err, res) {
// ...
When uglify
is run during a --production
build, its dead code elimination will identify code which will never get executed for the runtime that's currently being built (which will now look like, e.g if ('hta' === "browser")
) and remove the code from the minified version entirely.
Holy wow! Thank G-d I don't do anything enterprisey anymore but this is classy. Thanks for sharing.