16. Browserify to Transpile JavaScript
Node uses something called CommonJS to export and require modules. This same pattern can be used in the browser if we run our code through a tool called http://browserify.org/. Browserify will end up taking our code entry point and all of the dependencies needed to give us a bundled JavaScript file that has everything we need.
npm install browserify --save-dev
With Browserify installed, the next step will be to rearrange our code so that we are consistently using module.exports
and require()
. In order to get our paths right in our require()
statements, we’ll need to move some files around.
Here are the module consumption scenarios we have:
/assets/index.js
needs:- React
- HelloWorld
- Timestamp
- HelloWorld needs: React
- Timestamp needs: React
That’s not too bad. But we do have another detail to worry about–we’re coding HelloWorld and Timestamp using JSX that is getting transformed to JavaScript. Browserify can handle transforms, and we’ll use that feature shortly, but let’s just get this hacked together for a moment (one step at a time).
Here’s the approach we’ll take in this (temporary) approach:
- Modify
/Components/Timestamp.jsx
to export its React class usingmodule.exports
/Components/HelloWorld.jsx
is already doing this
- Modify
/assets/index.js
to userequire
statements for React, HelloWorld, and Timestamp- Require HelloWorld and Timestamp using relative paths from
/assets
to `/lib/Components’ where the JS files have already been transformed from JSX
- Require HelloWorld and Timestamp using relative paths from
- Update our
gulpfile.js
to introduce a new “client-scripts” task for running browserify on/assets/index.js
Here’s what /Components/Timestamp.jsx
gets edited to.
var React = require('react') module.exports = React.createClass({ getInitialState: function() { return { date: "Initial State: " + new Date().toString() } }, render: function() { return{this.state.date}} })
Then /assets/index.js
will pick up proper require
statements. The relative paths for getting to our already-transformed components is pretty ugly here, but we’ll fix that later.
var React = require('react') var HelloWorld = require('../lib/Components/HelloWorld') var Timestamp = require('../lib/Components/Timestamp') var timestampInstance = React.createFactory(Timestamp)(); var timestampElement = React.render(timestampInstance, document.getElementById("reactContainer")); setInterval(function() { timestampElement.setState({ date: "Updated through setState: " + new Date().toString() }) }, 500) var helloInstance = React.createFactory(HelloWorld)( { from: "From the client" } ); var helloElement = React.render(helloInstance, document.getElementById("reactHelloContainer"));
To illustrate client rendering of the HelloWorld component here, we’ve added a couple of lines at the bottom of /assets/index.js
that will do that. We’re rendering the HelloWorld element into a “reactHelloContainer” element–we need to create that. Here’s what /index.jsx
looks like after adding that <div>
.
var React = require('react') , HelloWorld = require('./Components/HelloWorld') , express = require('express') , path = require('path') var app = express() app.use('/Components', express.static(path.join(__dirname, 'Components'))) app.use('/assets', express.static(path.join(path.join(__dirname, '..'), 'assets'))) app.get('/', function (req, res) { res.writeHead(200, {'Content-Type': 'text/html'}) var html = React.renderToString( <html> <head> <title>Hello World</title> </head> <body> <HelloWorld from="index.jsx on the server" /> <div id="reactContainer" /> <div id="reactHelloContainer"></div> </body> <script src="/assets/index.js"></script> </html>) res.end(html) }) app.listen(1337) console.log('Server running at http://localhost:1337/')
This should put all of the pieces in place for us:
/Components/Timestamp.jsx
usesrequire
to get React/Components/Timestamp.jsx
usesmodule.exports
to export itselfTimestamp.jsx
andHelloWorld.jsx
are already getting transformed into raw JavaScript- The result gets written to
/lib/Components/
- The result gets written to
/assets/index.js
usesrequire
to get React, HelloWorld, and Timestamp- It renders a Timestamp component into
<div id="reactContainer">
- It renders a HelloWorld component into
<div id="ReactHelloContainer">
- It renders a Timestamp component into
And as a reminder, there are a few other moving parts:
/index.jsx
gets transformed into raw JavaSCript at/lib/index.js
- We serve static assets from
/lib/Components
under the/Components
path - We serve static assets from
/assets
under the/assets
path
With this configuration, the page that gets served with a script tag pointing to /assets/index.js
and that results in our raw /assets/index.js
file getting to the browser. That file now has require()
statements in it and that’s where we need to utilize Browserify.
To pull this off, we’ll update our gulpfile.js
and introduce a “client-scripts” task to transform /assets/index.js
to /lib/assets/index.js
.
var gulp = require('gulp') , gulpReact = require('gulp-react') , gulpNodemon = require('gulp-nodemon') , gulpWatch = require('gulp-watch') , source = require('vinyl-source-stream') , browserify = require('browserify') gulp.task('watch-jsx', ['client-scripts'], function() { gulpWatch(['**/*.jsx', 'assets/*.js'], { ignored: 'lib/' }, function() { gulp.start('client-scripts') }) }) gulp.task('jsx', function() { return gulp.src('**/*.jsx') .pipe(gulpReact()) .pipe(gulp.dest('lib')) }) gulp.task('client-scripts', ['jsx'], function() { return browserify('./assets/index.js').bundle() .pipe(source('index.js')) .pipe(gulp.dest('lib/assets')) }) gulp.task('node', ['client-scripts', 'watch-jsx'], function() { gulpNodemon({ script: 'lib/index.js', ignore: ['gulpfile.js'], ext: 'js jsx' }) }) gulp.task('default', function() { gulp.start('node') })
Here are some notes on this step:
- We require ‘browserify’
- We created a new ‘client-scripts’ task that depends on the output of the ‘jsx’ task
- That task uses
browserify('./assets/index.js').bundle()
to create the bundle - We need to specify the source file name on the stream that gulp uses
- This is done using the
.pipe(source('index.js'))
statement - That step required running
npm install vinyl-source-stream --save-dev
- And then we also added the
require('vinyl-source-stream')
to use it
- This is done using the
- We then pipe the output of the gulp bundle’s stream using `.pipe(gulp.dest(‘lib/assets’))
- The ‘node’ task now depends on ‘client-scripts’
- The ‘watch-jsx’ task also depends on ‘client-scripts’
- The ‘watch-jsx’ task passes
/assets/*.js
intogulpWatch
now too
With all of this in place, we can edit /assets/index.js
while Node is running and Gulp will re-run browserify through our “client-scripts” task and restart node.
Now there’s one last detail we need to take care of: we need the browser to be able to get to the Browserify output. Right now, it can reach /lib/Components
and /assets
but we need it to get to /lib/assets
. The good news is that we no longer need direct access to /assets
. So let’s edit index.jsx
to serve static content from /lib/assets/
under the /assets
path.
BEFORE
app.use('/assets', express.static(path.join(path.join(__dirname, '..'), 'assets')))
AFTER
app.use('/assets', express.static(path.join(__dirname, 'assets')))
Because this file is running from the /lib
folder, and the Browserify output is now in the /lib/assets
folder, we were able to get rid of the ..
folder handling and now just serve /assets
out of the /lib/assets
folder more cleanly.
After restarting Gulp (to pick up on the new gulpfile.js
changes), we can load our page back up in the browser and see that this all came together. The page now shows the following (with the Timestamp updating):
This is from the HelloWorld.jsx component's render function. Rendered from: index.jsx on the server Updated through setState: Mon Apr 13 2015 16:53:13 GMT-0700 (PDT) This is from the HelloWorld.jsx component's render function. Rendered from: From the client
I’ll admit, this was a lot of work–more than I expected it to be. And I’m not very happy with the folder layout that we’ve arrived at:
/assets - meant for public js/css/image assets - now it's a source folder for JavaScript files to be run through Browserify - files have to know to use relative path require() statements to get to ../lib /Components - JSX-based React components - also a source folder where contents will be run through gulp-react /lib - build output from Browserify and gulp-react / - includes index.jsx, which is a source file that runs through gulp-react
Before we go any further, we’ll want to clean this structure up a bit.