WordPress+Vue.JSで作るジョブボード(3) モダンJavascriptのお作法解説

SPONSORED LINK

前回までで投稿のCRUD(Create, Read, Update, Delete)の基本はお見せしたのだが、わざわざVue.JSというモダンJavascriptフレームワークを使うのだから、現在のトレンドを抑えつつ、WordPressとうまく両立する方法を紹介しよう。そう、最近のフロントエンド開発辛い問題と向き合う時期が来たのである。

昨今のJavascriptトレンド

さて、まずはおさらいといこう。最近のJavascript開発においては、次のような概念が「当たり前」の——そう、かっこつきの留保すべき当たり前の——ものとされている。

ポリフィル(埋め合わせ)

PHPのようなスクリプト言語と異なり、Javascriptは、制作者が実行環境を用意する(ex. サーバーにPHP7.1をインストールする)のではなく、ユーザーが自由に選択できるブラウザを実行環境とする。Chrome, Firefox, Safari, Edgetといったモダンブラウザから、IE10以下のようなレガシーブラウザまで、いくつかの選択肢がありえる。そして、各種ブラウザには当然差分があり、書き方が異なったりする。たとえば、Ajax通信でいえば、IE6以下とそれ以外のブラウザでは違いが存在するので、jQueryは次のような処理を行なっていた。

window.XMLHttpRequest = window.XMLHttpRequest || function () {
    /*global ActiveXObject*/
    try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) { }
    try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) { }
    try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) { }
    throw Error("This browser does not support XMLHttpRequest.");
};

これはなにをやっているかというと、AcriveXObjectを通してしかAjax通信を行えないIEのために、XmlHttpRequestオブジェクトを作っているのだ。他のモダンブラウザ(当時)はXmlHttpRequestオブジェクトが定義されているので、それをそのまま使う。

古くはPrototype.js、そしてjQuery、現在使われているBabelなどといったものは、多かれ少なかれこうしたブラウザごとの埋め合わせ(ポリフィル)を行なっている。もちろん、ポリフィルに特化したものもあれば、いろいろやってくれる上にポリフィルを含んでいるものもある。ファミレスもあれば、バーガーショップもあるというわけだ。

トランスパイル

トランスパイルとは、別の場所へを意味する接頭辞trans-とコンパイル(compile)の合成語である(だよね?)。C言語などはソースコードを実行形式にコンパイル(編纂)する。しかし、トランスパイルの場合、ソースコード→バイナリという変換ではなく、プログラミング言語A→プログラミング言語Bといったように、別の言語に翻訳することを指す。

なぜ「トランスパイル」という造語がとりわけモダンJavascriptの現場で流通するようになったのかというと、上記で挙げたポリフィルのように、「同じ実行環境で新しい言語機能を提要する」ということが当たり前になったからだといえよう。トランスパイルのかなりの部分が、上記のポリフィルを実現するための役割を担っている。

変換前 変換後 備考
CoffeeScript Javascript Ruby on Railsと一緒によく使われた。悪くいえばシンタックスシュガー。
TypeScript Javascript Microsoft開発の拡張Javascript。型付けができる。
ECMA2015(ES6) ECMA5 Javascriptの新言語仕様を現在普及している言語仕様に翻訳する。

PHPでいえばPHP7の構文で書いてもPHP5で動くようにすること、CSSでいえばSASSをCSSに変換することがトランスパイルだと思っていただければ良いだろう。ちなみに、ECMAとは、ECMA Scriptのことであり、Javascriptの言語仕様である。ECMA Scriptの実装としてはActionScriptなどもあるが、Flashはもう終わったので、事実上Javascriptの言語仕様だと思ってもらって間違いない。

モジュールシステム

現代的なJavascriptによる開発も他のプログラミング言語同様、別のライブラリやフレームワークに依存することが多い。また、ある程度の規模のソフトウェア(=Webサイト)を作る場合、それぞれの機能をモジュールとして分割したくなってくる。Javascriptにおいて、これらは長らくscriptタグの読み込み順で解決されてきた。

<!--jQuery -->
<script src="/path/to/jquery.js" type="text/javascript"></script>
<!--jQueryに依存する jQuery UI -->
<script src="/path/to/jquery-ui.js" type="text/javascript"></script>
<!--jQuery UIとjQueryに依存する自分のスクリプト -->
<script src="/path/to/my-script.js" type="text/javascript"></script>

そこでサーバーサイドJSであるNodeJSがより効率のよいパッケージマネージャーnpmを実装したのだが、このnpmはブラウザで実行されるJavascript(サーバーサイドJSに対し、フロントエンドJSと呼ぼう)でも利用されるようになった。モジュール化にはAMD, CommonJSなどのいくつかの手法があるが、CommonJSの構文 require と、新しいJavascriptの仕様ES6で提案されている モジュールローダー import がよく使われている。

//
// CommonJS
// ============

// モジュール定義する側
// something.js
module.exports = function(a) {
  return doSomething(a);
}

// モジュールを使う側
// app.js
var somethind = require('./something");
var thing = something('Do something');


//
// ES6のモジュールローダー
// ============

// モジュール定義する側
// something.js
export function something(a) {
  return doSomething(a);
}

// モジュールを使う側
// app.js
import something from "./something";
var thing = something('Do something');

とはいえ、フロントエンドJSにはファイルシステムを読み込む能力がないので、インポートすることはできない。また、モダンなプログラミング言語ではオートローダーの仕組みが存在し、利用するライブラリをいちいち読み込むことはせず(※WordPressはしている)、利用を宣言すれば自動で読み込まれる。

<?php
// 昔のPHPは必要なファイルを指定して読み込み。
require_once 'path/to/class/file.php';
$foo = new MyClass();
require_once 'path/to/class/another.php';
$var = new AnotherClass();


// 最近のPHPではComposerのautoloaderを使えば動的に読み込まれる。
require_once __DIR__ . '/vendor/autoload.php';
$foo = new MyClass();
$var = new AnotherClass();

こうした「依存するファイルの読み込み」を実現する形で主流となったのが各種バンドルツールである。依存するファイルをnpmでかき集めて、一つのファイルにパッケージングするのだ。Browserify, Webpackなどが存在する。

// Browserifyでapp.jsに記載されている読み込み順を全部集めてパッケージング
browserify js/app.js -o app.bundle.js

※なぜ一つにまとめるのか?

こうしたフロントエンドJSの開発環境を整えるチュートリアルでは、最終的に一つのファイルにまとめることが多い。なぜファイルが分かれたままではいけないのだろうか? 筆者の私見だが、下記の理由があるように思う。

  • カプセル化、つまり外部に影響を与えないため。グローバル汚染を嫌うエンジニアは多い。
  • この手のフロントエンド開発ではSPA(Single Page Application) を想定しているので、1つでよい。ルーティングなどもあるので、なんでもかんでも含めて1つのファイルにすれば済んでしまう。
  • PHPのオートローダーのような、必要なときに読み込む仕組みmodule loader APIはまだ策定段階であるため。Javascriptの場合、インポートがネットワーク経由になることにより、非同期処理にならざるを得ず、技術的に難しい部分が多い。

といったところである。したがって、「一つのファイルにまとめなければならない」わけではないことを理解しておこう。とりわけ、HTTP2.0の普及により、module loader APIの方が効率がよくなる可能性もある。

これまで書いたソースコードの改善

それでは、前段が凄まじく長くなってしまったが、上記のようなモダン開発の手法でこれまで書いたJavascriptを改善する余地はあるだろうか?

テンプレート構文

まず、ECMA2015(ES6)を採用することで、Vue.JSのテンプレート構文の可読性をあげることができる。変数埋め込みができるし、改行も含めることができる。

let hoge = 'ほげ';
let myString = `ES6からは変数${hoge}が埋め込めます。`;

筆者は現在Gulpを利用してテーマ内のJSをミニファイしているので、Babelを利用し、ES6対応のコードが未対応のブラウザでも実行できるようにしよう。現在のGulpは次の通りである。

// src/js ディレクトリのJSを圧縮する
gulp.task('js', function () {
  return gulp.src(['./src/js/**/*.js'])
    .pipe($.sourcemaps.init({
      loadMaps: true
    }))
    .pipe($.uglify({
      preserveComments: 'some'
    }))
    .on('error', $.util.log)
    .pipe($.sourcemaps.write('./map'))
    .pipe(gulp.dest('./assets/js/'));
});

ここにgulp-babelなどを追加し……

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

Gulpに処理を追加する。

gulp.task('js', function () {
  return gulp.src(['./src/js/**/*.js'])
    .pipe($.sourcemaps.init({
      loadMaps: true
    }))
    // ここでES6にする
    .pipe($.babel({
      "presets": ["es2015"]
    }))
    .pipe($.uglify({
      preserveComments: 'some'
    }))
    .on('error', $.util.log)
    .pipe($.sourcemaps.write('./map'))
    .pipe(gulp.dest('./assets/js/'));
});

これでコンポーネントのテンプレートを書き直すと……

{
// 以前
  template: '<div class="job-board-editor">' +
    '<strong>{{post.status}}</strong>' +
    '<p>{{post.post_title}}</p>' +
    '<p v-if="post.editable"><label>' +
      '<input type="text" v-model="newTitle" /><button type="button" v-on:click="saveTitle">保存</buttont>' +
    '</label></p>' +
    '<p>{{post.post_content}}</p>' +
    '<p v-if="post.editable"><label>' +
    '<textarea v-model="newContent"></textarea>' +
    '</label></p>' +
    '<button type="button" v-if="post.editable" v-on:click="publish">申請する</button>' +
    '</div>',


// ES6採用後
template: `
    <div class="job-board-editor">
        <strong>{{post.status}}</strong>
        <p>{{post.post_title}}</p>
        <p v-if="post.editable">
            <label>
                <input type="text" v-model="newTitle" />
                <button type="button" v-on:click="saveTitle">保存</buttont>
            </label>
        </p>
        <p>{{post.post_content}}</p>
        <p v-if="post.editable">
            <label>
                <textarea v-model="newContent"></textarea>
            </label>
        </p>
        <button type="button" v-if="post.editable" v-on:click="publish">申請する</button>
    </div>
  `
}

となる。どうだろう、プラス記号の数が減ってだいぶスッキリしたのではないだろうか。

コンポーネントの分離

今回のファイルリストではコンポーネントを利用しているが、これを別ファイルに分割しよう。

job-board-admin.js
↓
job-board/
├job-board-admin.js
├job-board-container.js
├job-board-editor.js
└rest-api.js

それでは、このファイルを読み込むのだが、ここでWordPressならではの方法を紹介したい。そもそもWordPressには wp_enqueue_script という依存関係を解決しながら重複なくJavascriptを読み込む仕組みが用意されている。しかし、筆者の経験として面倒に思うのが、Javascriptの登録をPHPで指定しなければならない点だ。JSの開発に集中したいのに、新しいモジュールを追加するたびにいちいちPHPに戻るのはとても面倒である。しかも、その依存関係をPHPで書かなければいけないとなると、さらに頭が痛くなる。

そこで、筆者が作ったライブラリ wp-enqueue-manager を利用すると、JSの依存関係を解消しながら、wp_enqueue_script で読み込むことができる。

composer require hametuha/wp-enqueue-manager
<?php
// composerのオートローダーを読み込み
require_once __DIR__ . '/vendor/autoload.php';
// ディレクトリのJSを全部登録
add_action( 'init', function() {
  wp_register_script( 'vue-js', 'https://cdn.jsdelivr.net/npm/vue', [], 'latest', true );
  Hametuha\WpEnqueueManager::register_js( get_stylesheet_directory() . '/assets/js/job-board', 'capitalp-', wp_get_theme()->get( 'Version' ), true );
  wp_localize_script( 'capitalp-rest-api', 'CapitapRest', [
    'endpoint'  => rest_url(),
    'nonce' => wp_create_nonce( 'wp_rest' ),
  ] );
} );
// 以降、すべての依存関係が登録されているので、いままで通り読み込みできる。
add_action( 'admin_enqueue_scripts', function( $page ) {
  if ( 'toplevel_page_job-board' !== $page ) {
    return;
  }
  wp_enqueue_script( 'capitalp-job-board-admin' );
} );

wp_enqueue_scriptwp_localize_script と組み合わせることで、JavascriptにPHPの変数を渡すことができるし、国際化をWordPressに一本化できるので便利だ。依存関係はファイル先頭のコメントwpdeps=jquery  で反映されるようになっているので、詳しくはドキュメントをご覧いただきたい。

ポイントとしては、コメントをライセンスコメント(/*!で始まるもの)にし、それがgulp-uglifyミニファイする際に削除されないようにすること。gulp-uglifyでコメントを残す方法が変わったの?などを参考にしてほしい。

/*!
 * Job board related stuff.
 *
 * wpdeps=capitalp-job-board-container,vue-js
 */

/*global Vue: false*/

(function ($) {
  'use strict';
  let app = new Vue({
    el: '#job-board-container',
  });
})(jQuery);

おまけ:Vueファイルという方法

Vue.JSには「単一ファイルコンポーネント」という仕組みがあり、HTML, JS, CSSからなるコンポーネントを一つのファイル(拡張子.vueファイル)にまとめる仕組みがある。ある程度webpackやBrowserifyなどに詳しくなれば、webpackテンプレートVueifyなどを利用することで、この単一ファイルコンポーネントも利用可能だ。

もちろん、.vueファイルが必要になる環境はかなりの大規模開発だと思われるので、自分がそうした例に該当すると思う方は採用してみるといいかもしれない。

まとめ

以上でES6記法とモジュールローディングができるようになった。次回以降はアプリケーション完成に向けて完成度を高めていきたい。今回もCapitalistの皆さんのためにコーディング動画とソースコードをつけておくので、ご覧いただきたい。

続きの 3% を読み、添付されたファイルにアクセスするには、Gumroadでライセンスキーを取得してください!

Club Capital P

Club Capital PはCapital Pのファンクラブです。有料会員制となっており、Gumroad経由でサブスクリプションをご購入いただき、ライセンスキーを登録いただくことで、会員特典を受け取ることができます。