First commit
This commit is contained in:
+373
@@ -0,0 +1,373 @@
|
||||
# Modular Haxe-JS
|
||||
|
||||
Code splitting and hot-reload for Haxe-JS applications.
|
||||
|
||||
*Haxe modular is a set of tools and support classes allowing Webpack-like code-splitting,
|
||||
lazy loading and hot code reloading. Without Webpack and the JS fatigue.*
|
||||
|
||||
For complete architecture examples using this technique you can consult:
|
||||
|
||||
- [Haxe React+Redux sample](https://github.com/elsassph/haxe-react-redux)
|
||||
- [Haxe React+MMVC sample](https://github.com/elsassph/haxe-react-mmvc)
|
||||
|
||||
Notes:
|
||||
- There is in fact also a [webpack-haxe-loader](https://github.com/jasononeil/webpack-haxe-loader),
|
||||
based on this library.
|
||||
- Do not confuse with [modular-js](https://github.com/explorigin/modular-js/network),
|
||||
which has a similar general goal but a different approach based on emitting one JS file
|
||||
per Haxe class (check the forks).
|
||||
|
||||
> This project is compatible with Haxe 3.2.1+
|
||||
|
||||
## Context
|
||||
|
||||
JavaScript is one of the target platforms of
|
||||
[Haxe](http://haxe.org/documentation/introduction/language-introduction.html),
|
||||
a mature, strictly typed, high level, programming language offering a powerful type system
|
||||
and FP language features. The compiler stays very fast, even with massive codebases.
|
||||
|
||||
If anything, *optimised bundling* is exactly was Haxe does best: Haxe offers out of the
|
||||
box incomparable dead-code elimination and generates very efficient JavaScript.
|
||||
|
||||
What Haxe lacks natively is *code splitting and HMR*. Haxe doesn't suggest any best
|
||||
practice to implement it.
|
||||
|
||||
The goal of this project is to propose one robust and scalable solution.
|
||||
|
||||
## Overview of solution
|
||||
|
||||
1. NPM dependencies bundling in a single libs/vendor JavaScript file
|
||||
|
||||
Best practice (for speed and better caching) is to regroup all the NPM dependencies
|
||||
into a single JavaScript file, traditionally called `vendor.js` or `libs.js`.
|
||||
|
||||
2. Haxe-JS code and source-maps splitting, and lazy-loading
|
||||
|
||||
Code splitting works by identifying features which can be asynchronously loaded at
|
||||
run time. JS bundles can be created automatically by using the `Bundle.load` helper.
|
||||
|
||||
4. Hot-reload
|
||||
|
||||
A helper class can be used listen to a LiveReload server and reload lazy-loaded
|
||||
modules automatically.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
You need to install both a Haxe library and a NPM module:
|
||||
|
||||
# code splitting and hot-reload
|
||||
npm install haxe-modular --save
|
||||
|
||||
# Haxe support classes
|
||||
haxelib install modular
|
||||
|
||||
Add to your HXML:
|
||||
|
||||
# Haxe support classes and output rewriting
|
||||
-lib modular
|
||||
|
||||
|
||||
## NPM dependencies bundling
|
||||
|
||||
Best practice (for compilation speed and better caching) is to regroup all the NPM
|
||||
dependencies into a single JavaScript file, traditionally called `vendor.js` or `libs.js`.
|
||||
|
||||
It is absolutely required when doing a modular Haxe application sharing NPM modules,
|
||||
and in particular if you want to use the React hot-reload functionality.
|
||||
|
||||
### Template
|
||||
|
||||
Create a `src/libs.js` file using the following template:
|
||||
|
||||
```javascript
|
||||
//
|
||||
// npm dependencies library
|
||||
//
|
||||
(function(scope) {
|
||||
'use-strict';
|
||||
scope.__registry__ = Object.assign({}, scope.__registry__, {
|
||||
//
|
||||
// list npm modules required in Haxe
|
||||
//
|
||||
'react': require('react'),
|
||||
'react-dom': require('react-dom'),
|
||||
'redux': require('redux')
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// enable React hot-reload
|
||||
require('haxe-modular');
|
||||
}
|
||||
|
||||
})(typeof $hx_scope != "undefined" ? $hx_scope : $hx_scope = {});
|
||||
```
|
||||
|
||||
It hopefully is understandable that we are defining a "registry" of NPM modules.
|
||||
In the browser we will have a global object `window.$hx_scope.__registry__` used by
|
||||
Modular to resolve NPM modules.
|
||||
|
||||
It is important to correctly name the keys of the object (eg. `'react'`) to match the
|
||||
Haxe require calls (eg. `@:jsRequire('react')`).
|
||||
|
||||
For React hot-module replacement, you just have to `require('haxe-modular')`. Notice
|
||||
that this enablement is only for development mode and will be removed when doing a
|
||||
release build.
|
||||
|
||||
Tip: there is nothing forcing your to register NPM modules, you can register any
|
||||
valid JavaScript object here.
|
||||
|
||||
### Building
|
||||
|
||||
The library must be "compiled", that is required modules should be injected,
|
||||
typically using [Browserify](http://browserify.org/) (small, simple, and fast).
|
||||
|
||||
For development (code with sourcemaps):
|
||||
|
||||
browserify src/libs.js -o bin/libs.js -d
|
||||
|
||||
For release, optimise and minify:
|
||||
|
||||
cross-env NODE_ENV=production browserify src/libs.js | uglifyjs -c -m > bin/libs.js
|
||||
|
||||
The difference is significant: React+Redux goes from 1.8Mb for dev to 280Kb for release
|
||||
(and 65Kb with `gzip -6`).
|
||||
|
||||
Note:
|
||||
- `NODE_ENV=production` will tell UglifyJS to remove "development" code from modules,
|
||||
- `-d` to make source-maps, `-c` to compress, and `-m` to "mangle" (rename variables),
|
||||
- [cross-env](https://www.npmjs.com/package/cross-env) is needed to be able to set the
|
||||
`NODE_ENV` variable on Windows. Alternatively you can use `envify`.
|
||||
|
||||
### Usage
|
||||
|
||||
If you use NPM libraries (like React and its multiple addons), you will want to create
|
||||
at least one library. The library MUST be loaded before your Haxe code referencing
|
||||
them is loaded.
|
||||
|
||||
Simply reference the library file in your `index.html` in the right order:
|
||||
|
||||
```html
|
||||
<script src="libs.js"></script>
|
||||
<script src="index.js"></script>
|
||||
```
|
||||
|
||||
You can create other libraries, and even use the same [lazy loading](#lazy-loading) method
|
||||
to load them on demand, just like you will load Haxe modules. If you have a Haxe module
|
||||
with its own NPM dependencies, you will load the dependencies first, then the Haxe module.
|
||||
|
||||
**Important:**
|
||||
- all the NPM dependencies have to be moved into these NPM bundles,
|
||||
- do not run Browserify on the Haxe-JS files!
|
||||
|
||||
|
||||
## Haxe-JS code splitting
|
||||
|
||||
Code splitting requires a bit more planning than in JavaScript, so **read carefully**!
|
||||
|
||||
Features need to have one entry point class that can be loaded asynchronously.
|
||||
|
||||
A good way to split is to break down your application into "routes" (cf.
|
||||
[react-router](https://github.com/ReactTraining/react-router/tree/master/docs)) or
|
||||
reusable complex components.
|
||||
|
||||
### How it works
|
||||
|
||||
- A graph of the classes "direct references" is created,
|
||||
- The references graph is split at the entry point of bundles,
|
||||
- Each bundle will include the direct (non-split) graph of classes,
|
||||
- unless the class is present in the main bundle (it will be shared).
|
||||
|
||||
What is a direct reference?
|
||||
- `new A()`
|
||||
- `A.b` / `A.c()`
|
||||
- `Std.is(o, A)`
|
||||
- `cast(o, A)`
|
||||
- ...
|
||||
|
||||
#### Difference between Debug and Release builds
|
||||
|
||||
Debug builds are optimised for "hot-reload":
|
||||
|
||||
- Enums are compiled in the main bundle, otherwise you may load several incompatible
|
||||
instances of the enum definitions.
|
||||
- Transitive dependencies will be duplicated (eg. sub-components of views may be
|
||||
included in several routes) so you can hot-reload these sub-components.
|
||||
|
||||
Release builds are optimised for size:
|
||||
|
||||
- All classes (and their dependencies) used in more than one bundle will be included
|
||||
in the main bundle.
|
||||
|
||||
## Bundling
|
||||
|
||||
The `Bundle` class provides the module extraction functionality which then translates into
|
||||
the regular "Lazy loading" API.
|
||||
|
||||
```haxe
|
||||
import myapp.view.MyAppView;
|
||||
...
|
||||
Bundle.load(MyAppView).then(function(_) {
|
||||
// Class myapp.view.MyAppView can be safely used from now on.
|
||||
// It's time to render the view.
|
||||
new MyAppView();
|
||||
});
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
`Bundle.load(module:Class, loadCss:Bool = false):Promise<String>`
|
||||
|
||||
- `module`: the entry point class reference,
|
||||
- `loadCss`: optionally load a CSS file of the same name.
|
||||
- returns a Promise providing the name of the loaded module
|
||||
|
||||
(API is identical generally to the "Lazy loading" feature below)
|
||||
|
||||
### React-router usage
|
||||
|
||||
`Bundle.loadRoute(MyAppView)` generates a wrapper function to satisfy React-router's
|
||||
async routes API using [getComponent](https://github.com/ReactTraining/react-router/blob/master/docs/API.md#getcomponentnextstate-callback):
|
||||
|
||||
```js
|
||||
<Route getComponent=${Bundle.loadRoute(MyAppView)} />
|
||||
```
|
||||
|
||||
Magic! `MyAppView` will be extracted in its own bundle and loaded lazily when the
|
||||
route is activated.
|
||||
|
||||
|
||||
## Lazy loading
|
||||
|
||||
The `Require` class provides Promise-based lazy-loading functionality for JS files:
|
||||
|
||||
```haxe
|
||||
Require.module('view').then(function(_) {
|
||||
// 'view.js' was loaded and evaluated.
|
||||
});
|
||||
```
|
||||
|
||||
`Require.module` returns the same Promise for a same module unless it failed,
|
||||
otherwise, calling the function again will attempt to reload the failed script.
|
||||
|
||||
### API
|
||||
|
||||
`Require.module(module:String, loadCss:Bool = false):Promise<String>`
|
||||
|
||||
- `module`: the name of the JS file to load (Haxe-JS module or library),
|
||||
- `loadCss`: optionally load a CSS file of the same name.
|
||||
- returns a Promise providing the name of the loaded module
|
||||
|
||||
`Require.jsPath`: relative path to JS files (defaults to `./`)
|
||||
|
||||
`Require.cssPath`: relative path to CSS files (defaults to `./`)
|
||||
|
||||
|
||||
## Hot-reload
|
||||
|
||||
Hot-reload functionality is based on the lazy-loading feature.
|
||||
|
||||
Calling `Require.hot` will set up a LiveReload hook. When a JS file loaded using
|
||||
`Require.module` will change, it will be automatically reloaded and the callbacks will
|
||||
be triggered to allow the application to handle the change.
|
||||
|
||||
```haxe
|
||||
#if debug
|
||||
Require.hot(function(_) {
|
||||
// Some lazy-loaded module has been reloaded (eg. 'view.js').
|
||||
// Class myapp.view.MyAppView reference has now been updated,
|
||||
// and new instances will use the newly loaded code!
|
||||
// It's time to re-render the view.
|
||||
new MyAppView();
|
||||
});
|
||||
#end
|
||||
```
|
||||
|
||||
**Important**: hot-reload does NOT update code in existing instances - you must create new
|
||||
instances of reloaded classes to use the new code.
|
||||
|
||||
### API
|
||||
|
||||
`Require.hot(?handler:String -> Void, ?forModule:String):Void`
|
||||
|
||||
- `handler`: a callback to be notified of modules having reloaded
|
||||
- `forModule`: if provided, only be notified of a specific module changes
|
||||
|
||||
### React hot-reload wrapper
|
||||
|
||||
When using hot-reload for React views you will want to use the handy `autoRefresh` wrapper:
|
||||
|
||||
```haxe
|
||||
var app = ReactDOM.render(...);
|
||||
|
||||
#if (debug && react_hot)
|
||||
ReactHMR.autoRefresh(app);
|
||||
#end
|
||||
```
|
||||
|
||||
The feature leverages [react-proxy](https://github.com/gaearon/react-proxy/tree/master) and
|
||||
needs to be enabled by calling `require('haxe-modular')`, preferably in your NPM modules bundle.
|
||||
|
||||
Note: you must compile with `-D react_hot`. The feature is only enabled in `debug` mode.
|
||||
|
||||
### LiveReload server
|
||||
|
||||
The feature is based on the [LiveReload](https://livereload.com) API. `Require` will
|
||||
set a listener for `LiveReloadConnect` and register a "reloader plugin" to handle changes.
|
||||
|
||||
It is recommended to simply use [livereloadx](http://nitoyon.github.io/livereloadx/). The
|
||||
static mode dynamically injects the livereload client API script in HTML pages served:
|
||||
|
||||
npm install livereloadx -g
|
||||
livereloadx -s bin
|
||||
open http://localhost:35729
|
||||
|
||||
The reloader plugin will prevent page reloading when JS files change, and if the JS file
|
||||
corresponds to a lazy-loaded module, it is reloaded and re-evaluated.
|
||||
|
||||
The feature is simply based on filesystem changes, so you just have to rebuild the
|
||||
Haxe-JS application and let LiveReload inform our running application to reload some of
|
||||
the JavaScript files.
|
||||
|
||||
PS: stylesheets and static images will be normally live reloaded.
|
||||
|
||||
## Known issues
|
||||
|
||||
### Problem with init
|
||||
|
||||
If you don't know what `__init__` is, don't worry :)
|
||||
[If your're curious](http://old.haxe.org/doc/advanced/magic#initialization-magic)
|
||||
|
||||
When using `__init__` you may generate code that will not be moved to the right bundle:
|
||||
|
||||
- assume that `__init__` code will be duplicated in all the bundles,
|
||||
- unless you generate calls to static methods.
|
||||
|
||||
```haxe
|
||||
class MyComponent
|
||||
{
|
||||
static function __init__()
|
||||
{
|
||||
// these lines will go in all the bundles
|
||||
var foo = 42;
|
||||
untyped window.something = function() {...}
|
||||
|
||||
// these lines will go in the bundle containing MyComponent
|
||||
MyComponent.doSomething();
|
||||
if (...) MyComponent.anotherThing();
|
||||
|
||||
// this line will go in the bundle containing OtherComponent
|
||||
OtherComponent.someProp = 42;
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Further troubleshooting
|
||||
|
||||
Modular recognises a few additional debugging flags:
|
||||
|
||||
- `-D modular_dump`: generate an additional `.graph` file showing the relationship
|
||||
between classes,
|
||||
- `-D modular_debugmap`: generate, for each module, an additiona; `.map.html` file
|
||||
showing a visual representation of the sourcemap.
|
||||
Reference in New Issue
Block a user