Ben d'état

Ben Scott


~/Build pipeline for React

27 Aug 2015

I’m going to eshew Bower and just use NPM as the package manager. I’ve already got Node and NPM installed. The example site I’m creating is survey-thing - a simple thing for creating surveys. I also have a stub package which just includes a script for setting up the environment and a simple landing page.

I’m going to use all the shinies - Browserify for CommonJS modules and JSX instead of the seperate .js/.html structure of a typical AngularJS application, using Babel for ES6 features and SASS for stylesheet preprocessing.

Primer: Nancy as a static server

The site will be hosted in a simple Nancy app so the built React application will be output to src\SurveyThing\app. Host this in Nancy with a static convention - this example is for an ASP.NET site using OWIN.

Install some NuGet packages (Microsoft.Owin, Microsoft.Owin.Host.SystemWeb, Nancy and Nancy.Owin).

Add these values to the web.config file, within the configuration element:

<appSettings>
	<add key="owin:HandleAllRequests" value="true" />
</appSettings>
<system.webServer>
	<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>

Add Startup and Bootstrapper classes:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app
            .UseNancy(new NancyOptions
            {
                Bootstrapper = new Bootstrapper()
            })
            .UseStageMarker(PipelineStage.MapHandler);
    }
}

public class Bootstrapper : DefaultNancyBootstrapper
{
    protected override void ConfigureConventions(NancyConventions nancyConventions)
    {
        Conventions.StaticContentsConventions.Clear();
        Conventions.StaticContentsConventions.AddDirectory("/", "app");

        base.ConfigureConventions(nancyConventions);
    }
}

If you create \app\test.html inside the Nancy site’s root it should be served from http://localhost:PORT/test.html. To serve \app\index.html when requesting http://localhost:PORT a static route needs to added:

public class StaticModule : NancyModule
{
    public StaticModule()
    {
        Get["/"] = _ => Response.AsFile("app/index.html");
    }
}

Set up Gulp

I want to use:

There’s a fair bit happening in the gulpfile.js script but here are some highlights.

The 'code' step uses browserify to set up the CommonJS module system and uses app/layout/index.jsx as the entry point to the application. Babel is used to take advantage of ES6 and the script is minified.

 gulp.task('code', function(){
	browserify({
		entries: 'app/layout/layout.jsx',
		extensions: ['.jsx'],
		debug: true
	})
		.transform(babelify)
		.bundle()
		.pipe(source('site.js'))
		.pipe(gulp.dest(destinationPath));
});

CSS preprocessing is done using SASS. The entry point is app/site.scss.

gulp.task('css', function(){
	gulp
		.src([
			'app/site.scss',
			'app/**/*.css'
		])
		.pipe(sass().on('error', sass.logError))
		.pipe(pleeease())
		.pipe(concat('site.css'))
		.pipe(gulp.dest(destinationPath));
});

The template HTML file is copied verbatim to the destination path.

gulp.task('html', function(){
	gulp
		.src('app/index.html')
		.pipe(gulp.dest(destinationPath));
});

The default gulp task runs all the main tasks ('vendor' is not shown, it just bundles up vendor CSS and JS into vendor.css and vendor.js respectively).

gulp.task('default', ['code', 'css', 'html', 'vendor']);

The 'watch' task splits up the workload to keep live rebuilds snappy:

gulp.task('watch', ['default'], function() {
	gulp.watch(
		['app/**/*.jsx'], 
		['code']);
	gulp.watch(
		['app/**/*.css', 'app/**/*.scss'],
		['css']);
	gulp.watch(
		['app/index.html'],
		['html']);
});

React app structure

The structure is pretty trivial at this point:

app/
	index.html
	site.scss
	/layout
		layout.jsx

index.html references the stylesheets and scripts, and includes a div with an ID that will be used by the React app:

<!DOCTYPE html>
<html>
	<head>
		<title>Survey Thing</title>
		<link rel="stylesheet" type="text/css" href="site.css"/>
		<link rel="stylesheet" type="text/css" href="vendor.css"/>
	</head>
	<body>
		<div id="content"></div>

		<script src="site.js"></script>
		<script src="vendor.js"></script>
	</body>
</html>

In layout.jsx, ‘Hello world’ is rendered into the div using inline markup:

import React from 'react';

React.render(
	<h1>Hello, world!</h1>,
	document.getElementById("content")
);

Running gulp will run the 'default' task, which should build the app and write the artifacts to \src\SurveyThing\app. Running gulp watch will run the 'watch' task and rebuild whenever the monitored files change.

If everything works, we should have a happy ‘hello world’ page. I made mine pink because pink is cool to test SASS:

Minifying the site.js file

Without minification the site.js file is huge (1.5M). To conditionally minify the script I check for a --release argument:

var yargs = require('yargs');

var buildRelease = yargs.argv.release || false;

gulp.task('code', function(){
	var pipeline = browserify({
		entries: 'app/layout/layout.jsx',
		extensions: ['.jsx'],
		debug: true
	})
		.transform(babelify)
		.bundle()
		.pipe(source('site.js'));

	if (buildRelease) {
		pipeline.pipe(streamify(uglify()));
	}

	pipeline
		.pipe(gulp.dest(destinationPath));
});

Now running gulp --release results in a much more managable 185k file.

fin

Check out the survey-thing repo for updates and lols.

Resources & further reading:


comments powered by Disqus