374 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			374 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # 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.
 |