読者です 読者をやめる 読者になる 読者になる

サーバサイドエンジニアのためのフロントエンド入門 〜ECMAScript編〜


f:id:Naotsugu:20160706221633p:plain

フロントエンドの移り変わりが激しすぎてついていけない。 というサーバサイドエンジニア向けのフロントエンドの概要第3回目です。 より深い話題については他を当たってください。。

前回は CSS について見ました。

etc9.hatenablog.com

今回は ECMAScript について見ていきましょう。

ECMAScript 小史

最初に、ECMAScript の歴史を簡単に振り返ってみましょう。

  • 1995年 Netscape Navigator 2.0 に JavaScript 1.0 (旧名LiveScript)が搭載される
  • 1996年 MS が IE3 に JScript 1.0 (JavaScript1.0互換) を 搭載
  • ブラウザ戦争の最中、ベンダー間で言語仕様の独自拡張が行われ互換性が極めて低い暗黒期
  • Netscape Navigator 社が ECMA に Javascript の標準化を依頼
    • ECMAとは、ECMAインターナショナルという情報通信システムの分野における国際的な標準化団体
  • 1997年7月、ECMAScript の標準規格 ECMA-262 リリース
  • 標準仕様がまとまらない時期を経つつ、2009年12月に ECMAScript 5 (ES5) リリース
    • ES5 は IE9以上、iOS7以上、その他のモダンブラウザであればほぼ対応している
  • 2015年6月、ECMAScript 2015 (ES6) リリース
  • 次期仕様 ECMAScript next (ES.next) は現在策定中
    • 今後は1年毎のサイクルでリリースされる仕様に、機能ごとに策定完了となったプロポーザルが含まれる形となる

ということで、現在はブラウザ側での ECMAScript 2015 の対応が進んできている状況となっています。

ECMAScript 2015

モダンブラウザであれば概ね対応が進んでいます。

結構大きな変更があるので、簡単に概要を見ていきましょう。

let と const よる変数宣言

letconst よる変数宣言が追加されました。これらはいずれもブロックスコープが有効で、悪名高き var とはおさらばできます。

let は通常の変数宣言、const は定数宣言となります。

const PI = 3.14;
let name = "Thome";

Arrow Functions

function の代わりに => で関数定義することができます。

const arr = [1, 2, 3, 4, 5];
const squares = arr.map(x => x * x);

let plus = (x, y) => x + y;
// let plus = (x, y) => { return x + y; };

Java のラムダ式と似てますね。

Promise

ようやく Promise オブジェクトが標準で提供されるようになりました。

コールバック地獄の処方箋として外部ライブラリを使うことなく、Promise できます(Java で言うと CompletableFuture)。

var promise = function() {  
  return new Promise((resolve, reject) => {
    $.getJSON('http://www.api.com/123')
      .done(json => resolve(json))
      .fail((xhr, status, err) => reject(status + err.message));
  });
}

promise.then(results => results.forEach(/* ・・・*/))
  .catch(err => console.log("error:", err));

Promise 以外にも Map Set WeakMap WeakSet や、Symbol などの新しいオブジェクトが追加されています。

Template strings

変数や式が ${} で扱えます。

let name = 'Tiger';
console.log(`My name is ${name}.`);

classによるクラス構文

class が使えるようになりました。 extendssuper も想像通りに使えます。

class Person {
    constructor(name, age) {
        this.name   = name;
        this.age    = age;
    }
 
    incrementAge() {
      this.age += 1;
    }
}

class Personal extends Person {
    constructor(name, age, gender) {
        super(name, age);
        this.gender = gender;
    }
}

let p = new Personal('Tiger', 16, 'Female');

Modules

lib/math.js で export します。

export function sum (x, y) { return x + y }
export const pi = 3.141593

別ファイルで以下のように import して使うことができます。

import { sum, pi } from "lib/math"
console.log("2π = " + sum(pi, pi))

別名を付けて import することもできます(モジュール内の全てが対象になります)。

import * as math from "lib/math"
console.log("2π = " + math.sum(math.pi, math.pi))

Generator function

function* によりジェネレーター関数を定義します。 処理を抜け出したり、後から再帰したりできる関数です。

function* idMaker(){
    var index = 0;
    while(true)
        yield index++;
}

var gen = idMaker();

console.log(gen.next().value); // 0
console.log(gen.next().value); // 1

next() の呼び出しにより、最初のyield演算子か、ほかのジェネレーター関数に委任する yield*に達するまで実行して値が返却されます。

for...of による イテレーション

let iterable = [10, 20, 30];
for (let value of iterable) {
  console.log(value);
}

レスト引数(Rest Parameter)

function f (x, y, ...a) {
    return (x + y) * a.length
}

デフォルト引数

function hello(name = 'world') {
    return hello + name;
}


この他にもいろいろありますが、ここでの説明は省略します。

Bable とは

ECMAScript の新しい仕様を先取りできるトランスパイラです。

ECMAScript2015 (ES6) や 次期仕様 ECMAScript next (ES.next)で書かれたソースコードを一般的なブラウザがサポートしている ECMAScript5 形式に変換するツールです。

元々は 6to5 という名前で、ES6 から ES5 への変換ツールでしたが、それにとどまらないトランスパイラとして Babel という名前に変わりました。

Bable プラグインをインストールする

早速 Bable の利用方法を見てみましょう。

前回まで同じようにインストールしていきます。

$ npm install gulp-babel --save-dev
$ npm install babel-preset-es2015 --save-dev

以前は Babel 本体のみで使えましたが、Bable 6 からは各種の変換が個別プラグインとして提供されるようになったため、利用したい機能を babel-preset-es2015 のようにインストールする必要があります。

package.jsongulp-babel の依存が追記されます。

{
  // ・・・
  "devDependencies": {
    "babel-preset-es2015": "^6.9.0",
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "^3.1.0",
    "gulp-babel": "^6.1.2",
    "gulp-sass": "^2.2.0"
  }
}

Babel のプラグインは transform-es2015-block-scoping のように個別機能毎に用意されますが、 babel-preset-es2015babel-preset-stage-3、React の場合は babel-preset-react のようにパッケージで指定するのが楽です。

Bable タスクを定義する

gulp-babel プラグインを使ったトランスパイラタスクを gulpfile.js に定義します。

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

gulp.task('babel', function() {
  gulp.src('./src/app/js/**/*.js')
    .pipe(babel({presets: ['es2015']}))
    .pipe(gulp.dest('dist'))
});

前回のSCSSと大体同じ流れです。

presets にて利用するパッケージを指定します。

Bable タスクの実行

試しにsrc/app/js/add.js として以下を作成します。

let plus = (x, y) => x + y;
console.log(`2 plus 3 is ${plus(2, 3)}.`);

では gulp で babel タスクを実行してみましょう。

./node_modules/.bin/gulp babel

ES6 の Arrow Functions が通常の関数定義に、let が通常の var に変換されました。

"use strict";

var plus = function plus(x, y) {
  return x + y;
};
console.log("2 plus 3 is " + plus(2, 3) + ".");

Modules のトランスパイラ

Babel によるトランスパイラで ES6 の import/export がどのように変換されるか見てみましょう。

src/app/js/math.js で以下のようにモジュールを export します。

export function sum (x, y) { return x + y }
export const PI = 3.141593

src/app/js/app.js で、export したモジュールを読み込んで使用します。

import * as math from "./math.js"
console.log("2π = " + math.sum(math.PI, math.PI));

これを Babel で トランスパイル してみましょう。

$ ./node_modules/.bin/gulp babel

export 側は以下のように変換されます。

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.sum = sum;
function sum(x, y) {
  return x + y;
}
var PI = exports.PI = 3.141593;

import 側は以下のように変換されます。

"use strict";

var _math = require("./math.js");

var math = _interopRequireWildcard(_math);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

console.log("2π = " + math.sum(math.PI, math.PI));

色々と追加されていますが、注意すべきは2行目の requireです。 ES6 の import は commonjs の require に変換されます。

node で利用する分には問題ないですが、通常のブラウザでは require によるモジュール読み込みはサポートされていません。ではどうするかというと、Browserify や Webpack の出番となります。

Browserify の導入

Browserify は、require によるモジュールの読み込みを、事前に依存関係を解決して1つの js ファイルとして出力することで対処します。

各種のモジュールが合わさった1つの js ファイルを HTML から <script src="bundle.js"></script> のように読み込むことでブラウザで動作させることができるようになります。 Java で例えると、jar に固めるイメージでしょうか。


Babel と browserify を組み合わせて gulp で処理するには、いくつかのパッケージを入れる必要があります。

まずはインストールしましょう。

$ sudo npm install browserify --save-dev
$ npm install babelify --save-dev
$ npm install vinyl-source-stream --save-dev

package.json に 依存が追記されます。

{
  // ・・・
  "devDependencies": {
    "babel-preset-es2015": "^6.9.0",
    "babelify": "^7.3.0",
    "browserify": "^13.0.1",
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "^3.1.0",
    "gulp-babel": "^6.1.2",
    "gulp-sass": "^2.2.0",
    "vinyl-source-stream": "^1.1.0"
  }
}

browserify の変換処理の中で、Babel のトランスパイルを行うために babelify というプラグインを使います。

gulp は ファイルオブジェクトを vinyl というオブジェクトとして扱ってストリーム形式で処理を連結していきます。しかし browserify の結果は vinyl ではないため、この変換のために vinyl-source-stream プラグインを使います。

Browserify による変換

browserify babelify を使ったタスクを gulpfile.js に定義します。

最小限の定義にしています。

var gulp = require('gulp');
var browserify = require('browserify');
var babelify = require("babelify");
var source = require('vinyl-source-stream');

gulp.task('babelify', function() {
  browserify('./src/app/js/app.js')
   .transform(babelify, {presets: ['es2015']})
   .bundle()
   .pipe(source('bundle.js'))
   .pipe(gulp.dest('dist'))
});

では先ほどの export の例と

export function sum (x, y) { return x + y }
export const PI = 3.141593

import の例をもとにして

import * as math from "./math.js"
console.log("2π = " + math.sum(math.PI, math.PI));

実行してみましょう。

$ ./node_modules/.bin/gulp babel

bundle.js が以下のように作成されます(長いですがそのまま全部のせます)。

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";

var _math = require("./math.js");

var math = _interopRequireWildcard(_math);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

console.log("2π = " + math.sum(math.PI, math.PI));

},{"./math.js":2}],2:[function(require,module,exports){
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.sum = sum;
function sum(x, y) {
  return x + y;
}
var PI = exports.PI = 3.141593;

},{}]},{},[1]);

モジュールが1つのファイルとして結合されて出力されました。

このファイルは、ブラウザ上で実行できる形式となっています。

まとめ

今回は、ECMAScript の Babel によるビルド処理を見てきました。

次回は gulp のその他便利なプラグインについて見ていきます。



Js.next: Ecmascript 6

Js.next: Ecmascript 6