-Поиск по дневнику

Поиск сообщений в rss_planet_mozilla

 -Подписка по e-mail

 

 -Постоянные читатели

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 19.06.2007
Записей:
Комментариев:
Написано: 7


Pomax: Taking React to the next level: Mixins, Gulp, and Browserify

Среда, 07 Января 2015 г. 04:03 + в цитатник

In a previous post I explored React because we're looking at using it for new apps in the Mozilla foundation engineering team, and I covered what you're going to run into when you pick it up for the first time.

The tl;dr: version is that React doesn't use HTML, anywhere. It only offers a convenient syntax shorthand that looks like HTML, but obeys XML rules, and is literally the same as writing React.createElement("tagname", {props object}). For a web UI framework, that might seem weird, but it frees you to think about your UI as functional blocks, where you don't have "divs in divs" but "functional components composed of other functional components", and model your UI logic and interaction that way. The fact that it renders into the browser is nice, because that's what we want, but in no way actually important to your design process.

So: I like React. But React can't do everything, and there are things that make no sense to do as a React component, like plain JS functionality. For instance, if you want an image manipulation component, then by all means write it, but you're not going to also include all the code for the under-the-hood image processing library. You want to keep that separate.

What's still missing?

That post didn't cover everything, and hopefully it left some questions on the table, so let's address those: what's still missing?

Looking at a concrete example of what you might find missing: this blog uses React, and components consist of some scaffolding and two meaningful sub-components: a MarkDown component, and an Editor component. The latter expresses a plain old which updates its internal state whenever the user changes data. If the user clicks outside the editable area, is informed of the changes made in the editor, hides the editor, reveals the MarkDown component, and then tells the MarkDown component to turn the new content into "whatever it turns it into", which the MarkDown component does by using the marked library.

It would be easy to hard-code that into the MarkDown element:

var MarkDown = React.createElement({
  ...
  render() {
    var markedDown = marked(this.props.data);
    return 
{markedDown}
}, ... });

But React knows we're technically injecting user-generated content here, and that's not guaranteed to be safe in the slightest. It won't let us do things this way. Instead, it requires us to do:

var MarkDown = React.createElement({
  ...
  render() {
    var markedDown = marked(this.props.data);
    return 
}, ... });

But now I disagree, because while I understand React's philosophy, I know that the markdown is simply "what it has to be". I'm not setting it "dangerously", I'm setting it intentionally. This is my blog, damnit!

Using mixins

Instead, we can use a "mixin", which is an object that you can give to React components, and they can be "mixed into" a component, so that it has this-level access to all the functions defined in the mixin. Looking at the markdown mixin I'm using for this blog, we see this code:

var MarkDownMixin = {
  markdown: function(string) {
    return {
      dangerouslySetInnerHTML: {
        __html : marked(string)
      }
    };
  }
};

This mixin returns an object with a function markdown(...) that, when called, formats the input (using marked(string)) and then forms the non-JSX object that React builds when you transform the JSX code dangerouslySetInnerHTML="...". Using this mixin in the MarkDown component, we see:

var MarkDown = React.createClass({

  mixins: [
    MarkDownMixin
  ],

  render: function() {
    this.html = this.markdown(this.props.text)
    return 
  },

  getHTML: function() {
    return this.html.dangerouslySetInnerHTML.__html;
  }

});

Two things to note: (1) the substitution pattern uses ..., because the markdown function generates the full object representation rather than a JSX string. We can add those in bits that are using JSX by using the {...thing} syntax. It's not the most beautiful syntax, but it's a lot nicer than seeing dangerouslySetInnerHTML, and that's number (2): this code is no longer offensive.

Writing mixins that kick in during life cycle functions

Let's look at another example: onClickOutside. React itself has no way to make components react to clicks that originate "outside" of them. It has an onClick={...} but no onClickOutside={...} concept. This can be inconvenient to downright annoying: does that mean we have to write document.click handlers in every component we use? Thankfully: no. We can implement this as a mixin, adding the document level click handler when a component is mounted, and making sure to do clean up when it the component gets unmounts:

var OnClickOutside = {
  componentDidMount: function() {
    if(!this.handleClickOutside)
      throw new Error("missing handleClickOutside(event) function in Component "+this.displayName);

    var fn = (function(localNode, eventHandler) {
      return function(evt) {
        var source = evt.target;
        var found = false;
        // make sure event is not from something "owned" by this component:
        while(source.parentNode) {
          found = (source === localNode);
          if(found) return;
          source = source.parentNode;
        }
        eventHandler(evt);
      }
    }(this.getDOMNode(), this.handleClickOutside));

    document.addEventListener("click", fn);
    associate(this, fn);
  },

  componentWillUnmount: function() {
    var fn = findAssociated(this);
    document.removeEventListener("click", fn);
  }
};

This mixin is used in the blog's component, and allows for editing a post and then clicking anywhere outside of the editor, to effect a content saving and committing to github.

This mixing looks a little like a React component, in that it has functions that are named the same as the life cycle functions in a React component, and React will take any such function and make sure they gets called when a components associated life cycle function is called. Looking at the code for blog entries:

var Entry = React.createClass({
  mixins: [
    OnClickOutside
  ],
  componentDidMount: function() {
    // this code, *and the code from the mixin*, will run
    var state = this.props.metadata;
    state.postdata = this.props.postdata;
    this.setState(state);
  },
  ...
  handleClickOutside: function(evt) {
    // outside clicks simply tie into a function we already use:
    this.view();
  }, 
  ...
  view: function() {
    if(this.state.editing) {
      // go from edit mode to view mode:
      var self = this;
      self.setState({ editing: false }, function() {
        self.props.onSave(self);
      });
    }
  },
  ...
});

When 's componentDidMount function is triggered, the code from the mixin also gets run. We don't need to write any code inside the component to make sure we trigger things at exactly the right time: we can simply exploit the life cycle of a component and rely on mixins to tie into their associated functions to do what we need.

If you need it for one component, you probably need it more often

Mixins are a good way to capture aspects that are shared by multiple components, even if they share nothing else. For instance, if you need a universal timestampToUUID() function so that all your components will be using the same format, you can write a mixin for that, and be assured that all your components now speak the same timestamp language.

In traditional OOP this would be handled through inheritance, but React doesn't do inheritance: its OOP is based on compositing uncoupled objects into meaningful structures, so having mixins solves the problem of how to give multiple components "the same something" to work with.

Building and bundling

So far everything's been talking about loose files. Mixings and components alike have been written in code that doesn't really do much other than "be loaded into global context" and most developers will agree that's kind of bad. You might do that in combination with an index.dev.html or something, but seriously: bundle that stuff.

So let's look at how to bundle React code.

The most successful formula that I know of is the "write it in commonjs format, and use browserify" one, where we rewrite our code so that it follows the commonjs export/require methodology, so that we can run the code through a bundler that knows how to resolve all those requirements. My commonjs environment of choice is Node.js, and my bundler of choice is Browserify.

Let's-Node-that-code

Let's do a quick rewrite. As demonstrator, I'll rewrite the MarkDown component that we saw earlier:

var React = require("react");
module.exports = React.createClass({
  mixins: [
    require("../mixins/markdown")
  ],
  render: function() {
    this.html = this.markdown(this.props.text)
    return 
  },
  getHTML: function() {
    return this.html.dangerouslySetInnerHTML.__html;
  }
});

A pretty straight forward rewrite, but the important part there is the mixin require call. We no longer have the mixin live in global context, we need to load it from "somewhere". Node.js can load either from an installed package location, or from a relative file path, so in this case we load React as an installed package, the mixin from a local file, and we're done. Of course we need to make sure the mixins are of the right commonjs format, too:

var marked = require("../bower_components/marked/lib/marked");
module.exports = {
  markdown: function(string) {
    return {
      dangerouslySetInnerHTML: {
        __html : marked(string)
      }
    };
  }
};

And this is an equally simple rewrite. Instead of relying on marked living in global context, we explicitly require it in, and that's pretty much that.

If it doesn't error out in Node, it'll load in the browser

Browserify can take Node.js code, walk through all the dependencies and requirements and then spit out a single, bundled file that shims the require mechanism for the browser, so that code that works in Node, also works in the browser. Normally this doesn't require anything special, in that you just point browserify at your main app.js file and it does the rest:

$> browserify main.js -o bundle.js

Done, that would grow your 10 line main.js file into a 400kb bundle of all your required in code plus all the required dependencies. Your users cache that bundle once, and from then on the browser simply reloads it from cache. One HTTP request, once, and you know that if your URL is reachable, then none of the dependencies your app has will cause your app to fail loading: it's all just there. And it'll be small over the wire, because gzip transmissions can compress javascript really well. The bigger the file, the better the compression can be because there's more data to mark as "the same" (there's an upper limit to that of course, but a 400kb single file will typically compress far better than a hundred 4kb files).

However, React comes with JSX syntax, so it's not quite as easy as just running Browserify and being done, but that's where Gulp comes in...

Gulp - chain all parts of build for great success

Gulp is very nice. It lets you set up "streams" to pipe data through some filter, and the hand it off to the next "thing that looks at the pipe". For using Browserify, we want to do something like "take code, transform its JSX, then bundle it all up", and with Gulp, that's really simple! Here's the gulp build script for compiling the JS bundle that this blog relies on:

var gulp = require('gulp');
var concat = require('gulp-concat');

/**
 * Browserify bundling only.
 */
gulp.task('browserify', function() {
  var browserify = require('browserify');
  var transform = require('vinyl-transform');
  var reactify = require('reactify');
  var source = require('vinyl-source-stream');

  // Don't process react/octokit, because we can "bundle"
  // those in far more efficiently at the cost of a global
  // variable for React and Octokit. "oh no"
  var donottouch = require('browserify-global-shim').configure({
    'react': 'React',
    'octokit': 'Octokit'
  });

  return browserify('./components/App.jsx')
    .transform(reactify)
    .transform(donottouch)
    .bundle()
    .pipe(source('bundle.js'))
    .pipe(gulp.dest('./build/'));
});

/**
 * Pack in the React and Octokit libraries, because this
 * saves about 200kb on the final minified version compared
 * to running both through browserify the usual way.
 */
gulp.task('enrich', ['browserify'], function() {
  return gulp.src([
     './bower_components/react/react.min.js',
     './bower_components/octokit/octokit.js',
     './build/bundle.js'
   ])
   .pipe(concat('enriched.js'))
   .pipe(gulp.dest('./build/'));
});

/**
 * Minify everything, using uglify.
 */
gulp.task('minify', ['enrich'], function() {
  var uglify = require('gulp-uglify');
  return gulp.src('./build/enriched.js')
   .pipe(concat('gh-weblog.js'))
   .pipe(uglify())
   .pipe(gulp.dest('./dist/'));
});

/**
 * our "default" task runs everything, but -crucially- it
 * runs the subtasks in order. That means we'll wait for
 * files to be written before we move on to the next task,
 * because in this case we can't run parallel tasks.
 */
gulp.task('default', ['minify'], function() {
  console.log("Finishing packing up.");
});

This runs three tasks: one to browserify everything, one to enrich the resulting bundle with additional dependencies, and one to minify the whole thing. Those familiar with Gulp might ask "why is this three tasks instead of just one massive pipe" to which the answer is that Browserify with Vinyl and Uglify does really weird things. Some of those parts generate streams, some of them generate buffers, and some of them really, really don't want to work together without an additional three modules designed specifically to do things like "Forcing things to buffers" or "injecting files in the middle of a pipe". It gets weird. Also, and this is the proper reason rather than just "it's more of a hassle": Gulp tasks should be written such that each tasks only does one job. If you need to do multiple things, you write one task per thing, and chain them. And that's exactly what we're doing here.

Wait, why do we run Browserify and bundle dependencies manually?

Fun fact: this is a limitation due to how Browserify works. By resolving require() statements to its dependency, Browserify will bundle up any code that is required, even if the code never actually makes use of that requirement when running in the browser. For instance: Octokit can run in the browser with a <60kb footprint, because the browser has virtually everything it needs. In Node.js it needs to shim a fair number of things so with all require()ments loaded its footprint is 320 kb. Browserify has no concept of "pruning after checking what gets loaded", so it just builds the full 320kb. Even React suffers from this: using the for-browsers version of React, as opposed to letting Browserify bundle it in, saves us about 25kb. It's not as much a difference as for Octokit, but still adds up for justifiable reason.

Back to our gulp runner

So we run the three tasks: use Browserify for everything we can bundle without bloating (so, without React and Octokit), then a task that adds React and Octokit on top, and then a task that minifies everything using the Uglify minifier. This recipe turns 17 files amounting to 250kb into a single file that's only 183kb. Fewer script includes AND less data. Very nice. Now we can load this up with a single script requirement on-page:


And thus we get production levels of happiness. Adding server-side gzipping will make this package only 50kb over the wire, which is perfectly acceptable for "everything the app needs to run".

The take-home message

Writing React code so that it can be efficiently bundled for production deployment is actually pretty simple, once you know which tools to use. The difference between a plain React component definition and a Node.js style definition is almost nothing (and there's even a Sublime Text plugin that'll just generate you a full Node.js style React component skeleton by typing "new R" and selecting "rcc"), but using the Node.js style code means we can run everything through Browserify and pipe filters, and end up with a highly optimised package, ready for delivery to the user.

The corrollary: remember you're working with computers!

Technology's not perfect: sweet as Gulp and Browserify are, in this case we had to mess around with what Browserify gets to require into the bundle and what we want to keep out, because we care about deploy size. For any project, that's a thing to take into consideration. Maybe you don't even want to bundle certain things in at all. For instance, we could have left React out of the bundle entirely and loaded it from CDN. I decided not to, because facebook's CDN for React is actually a redirect to an http URL, even when you're on HTTPS, so anyone loading the site over HTTPS will see the site header, and nothing else. So: remember you're working with computers!

http://pomax.github.io/#gh-weblog-1420592591221


 

Добавить комментарий:
Текст комментария: смайлики

Проверка орфографии: (найти ошибки)

Прикрепить картинку:

 Переводить URL в ссылку
 Подписаться на комментарии
 Подписать картинку