HOME > TECHNOLOGY > 開発環境

JavaScriptはBabel+Rollupで十分です!【WEBデザイナーのためのMac強化計画 #8】

Mac強化計画⑧

TypeScript流行ってますね。
でも、WEBデザイナーにとって必須技術か?と問われると、正直微妙です。

TypeScript自体はJavaScriptの発展系と言えるので、JavaScriptの知識を無駄にするような言語ではありませんし、JavaScriptの理解も深まることは事実です。
また、TypeScriptの学習で得られる知識は、大規模プロジェクトで使われるような言語に取り組む際、少なからず生かせるはずです。
そういった意味で、TypeScriptに取り組むのは全く無駄にはなりません。

しかし、必須かと言われると、全くそんなことはありません

これはGulpとWebpackについての記事にも書きましたが、WEBデザイナーに対してTypeScriptを必須とする制作案件は、ほとんどないといっていい状況です。
ハウスデザイナーやスタートアップエンジニアの強い情報力に振り回されているだけです。
いちいちTypeScriptのセッティングをして、厳密な設計なんてしている暇があったら、散歩でもしてた方が健康的です。

小規模プロジェクトにとってのTypeScriptは、生産性とメンテナンス性を奪うことはあっても、与えてくれることはありませんよ!

WEBデザイナーはブラウザで実行できるJavaScriptを一生懸命頑張れば良いのです。

ただし、JavaScriptにも大きな弱点があります。

一つはIE暗黒時代に端を発する言語仕様の統一性のなさです。
新しいJavaScriptの仕様が策定されても、WEBブラウザーがサポートするのを待つ必要があります。
せっかく書いたJavaScriptが、プロジェクトの推奨環境によっては、動いてくれないなんてことも発生します。

そのため、JavaScriptのみで開発するとなると、かなり古い言語仕様で書かないと、仕事としての制作は成り立たないのが現状です。
サポート状況は、こちらのサイトで確認できます。

もう一つは、ファイルを分割した開発がしにくいことです。
es6(ECMAScript2015)以降になると、JavaScriptに他のJavaScriptを読み込むことができるようになっています。
ファイルを分割して管理することで、再利用できるコードをコンポーネント化しておくことができ、生産性とメンテナンス性を向上させることができます。

ところが、一見良さそうなファイル分割ですが、実際のコンテンツで複数のファイルをロードする状況を考えると、ベストな方法論とはいえません。
ファイルを分割してしまうと、その分だけリクエストとレスポンスが発生します。
WEBページの表示は、ストリームが多ければ多いほど、通信状況の影響を受け表示速度が不安定になります。
キャッシュされたページの表示が速いのはそのためです。
つまり、同じ内容であればファイル数が少ないほど良好なレスポンスに繋がるということになります。
とはいえ、1ファイルでJavaScript開発するのは、いくら少人数プロジェクトとはいえ辛いものがあります。

と、前置きがかなり長くなってしまいましたが、今回はそういいった現状を踏まえ、WEBデザイナーのJavaScript開発環境の最適解を紹介していきたいと思います。

JavaScriptの問題への対応

古い仕様のブラウザーへの対応

IEは未だ絶滅していません。
IE11ですらES6のサーポート率が11%程度と、あまりにも残念な状況です。
どうせなら、新しいシンタックスで書きたいですよね!

そこでBabelです。

BABEL

Babelはトランスパイラと呼ばれるツールの一種で、最しい書き方で書かれたJavaScriptを古い書き方に変換してくれます。
ブラウザのサポートがなくて使えなかったECMAScriptの最新仕様を、自由に使える環境はありがたいです。
古いシンタックスを学ぶ必要がほとんどないということは、JavaScriptの学習コストを格段に下げる効果もありますね。

バンドルツールでリクエスト数を減らす

リクエストを減らすには、HTMLにロードするJavaScriptファイルの数を減らす努力をしなければなりません。
かつては、1ファイルの中に全てのロジックを詰め込むようなJavaScript開発手法を多く見かけましたが、現在ではバンドルツールの充実で、ファイルを分割しモジュール単位で開発するのが一般的です。
モジュール化すると再利用が容易になり、圧倒的にJavaScriptの開発期間を短縮することができます。
特に、WEBデザイナーの場合は、各サイトごとに使う機能が似通っているので効果は絶大です。

バンドルツールには、BrowserifyWebpackRollupFuseBoxParcelなどたくさんの種類があり群雄割拠の状態です。
これらはそれぞれに特徴があり、使う職種やシチュエーションによって、どれを選択すべき変わってきます。

私の考えでは、WEBデザイナーにおすすめなのはRollupです。

Rollup

Rollupは出力されるJavaScriptが非常にコンパクトで見晴らしがバツグンです。
コンパイルの速度が遅いという情報がありますが、そもそもWEBデザイナーが扱うソースコードの量なんてたかが知れてますので、気になるほどではありません。
コンパクトであるということは、WEBページの表示速度を上げるためには、それだけでも有効性があります。

インストール

前回までのpugstylusをインストールした環境を利用します。

$ yarn add -D gulp-eslint babel-eslint gulp-rollup-stream rollup-plugin-babel rollup-plugin-includepaths @babel/core @babel/plugin-external-helpers @babel/preset-env gulp-strip-debug gulp-uglify gulp-rename

stylusの時にも増してパッケージが多いですね!
簡単に説明します。

gulp-eslint

JavaScriptの記述を統一するためのルールを設定します。
ルールに合ってない場合にエラーを出力してくれます。

babel-eslint

eslintの記述をパースする際の基準をbabelに合わせてくれます。

gulp-rollup-stream

gulpタスクでRollupを簡単に使えるようにするラッパー。

rollup-plugin-babel

RollupでJavaScriptをバンドルする際に、BabelでトランスパイルするためのRollupのプラグイン。

rollup-plugin-includepaths

RollupでJavaScriptをバンドルする際に、インクルードパスを簡易指定するためのRollupのプラグイン。

@babel/core

Babel本体

@babel/plugin-external-helpers

重複するコードを共通コードにまとめてくれるBabelのプラグイン。

@babel/preset-env

.babelrcという設定ファイルによって定義された内容に合わせたトランスパイルを行うプリセット

gulp-strip-debug

デバッグ用に記述したconsole.log()を削除してくれます。(リリース用)

gulp-uglify

トランスパイルされたJavaScriptを圧縮し、ファイルサイズを削減します。(リリース用)

gulp-rename

出力する際に.min付きなどのファイル名にリネームします。

ファイル構成

プロジェクトルートに移動し、rollupのソースファイルを置くディレクトリを作成します。
その中にindex.jsとcomponents/_helloworld.jsというファイルを設置します。

src/
  .babelrc
  .eslintrc
  rollup/
    index.js
    components/
      _helloworld.js

タスクの作成

gulpfile.jsを編集します。
前回のとstylus/scssのインストールで利用したファイルに追記します。


const gulp = require('gulp');
const server = require('browser-sync');
const plumber = require('gulp-plumber');
const filter = require('gulp-filter');
const sourcemaps = require('gulp-sourcemaps');
const gulpif = require('gulp-if');
const pug = require('gulp-pug');
const beautify = require('gulp-html-beautify');
const stylus = require('gulp-stylus');
const sass = require('gulp-sass');
const stylint = require('gulp-stylint');
const scsslint = require('gulp-scss-lint');
const postcss = require('gulp-postcss');
const csso = require('gulp-csso');
const rollup = require('gulp-rollup-stream');
const babel = require('rollup-plugin-babel');
const includepaths = require('rollup-plugin-includepaths');
const eslint = require('gulp-eslint');
const strip = require('gulp-strip-debug');
const uglify = require('gulp-uglify');
const rename = require('gulp-rename');

// ▽ ミニファイの設定
let minify = false;

// ▽ パスの設定
const src = 'src';
const dist = 'dist';
const path = {
  pug: {
    entry: [src+'/pug/**/*.pug']
  },
  stylus: {
    entry: [src+'/stylus/**/*.styl']
  },
  scss: {
    entry: [src+'/scss/**/*.scss']
  },
  rollup: {
    entry: [src+'/rollup/**/*.js'],
    import: [src+'/rollup/**']
  }
}

// ▽ pugのコンパイル
function pugCompile() {
  return gulp
    .src(path.pug.entry)
    .pipe(plumber())
    .pipe(pug({pretty: true}))
    .pipe(filter(function(file){return !/\/_/.test(file.path) && !/^_/.test(file.relative);}))
    .pipe(beautify({'indent_size': 2, 'indent_char': ' '}))  
    .pipe(gulp.dest(dist));
}

// ▽ stylusのコンパイル
function stylusCompile() {
  return gulp
    .src(path.stylus.entry)
    .pipe(plumber())
    .pipe(gulpif(!minify, sourcemaps.init()))
    .pipe(stylint())
    .pipe(stylint.reporter())
    .pipe(stylus())
    .pipe(postcss([
      require('autoprefixer')({cascade: false}),
      require('css-mqpacker')
    ]))
    .pipe(filter(function(file){return !/\/_/.test(file.path) && !/^_/.test(file.relative);}))
    .pipe(gulpif(minify, csso()))
    .pipe(gulpif(!minify, sourcemaps.write()))
    .pipe(gulp.dest(dist+'/css'));
}

// ▽ scssのコンパイル
function scssCompile() {
  return gulp
    .src(path.scss.entry)
    .pipe(plumber())
    .pipe(gulpif(!minify, sourcemaps.init()))
    .pipe(scsslint({config: ".scss-lint.yml"}))
    .pipe(sass({outputStyle: 'expanded'}))
    .pipe(postcss([
      require('autoprefixer')({cascade: false}),
      require('css-mqpacker')
    ]))
    .pipe(filter(function(file){return !/\/_/.test(file.path) && !/^_/.test(file.relative);}))
    .pipe(gulpif(minify, csso()))
    .pipe(gulpif(!minify, sourcemaps.write()))
    .pipe(gulp.dest(dist+'/css'));
}

// ▽ rollupでJavaScriptをバンドル(iife or es)
function jsRollup() {
  return gulp
    .src(path.rollup.entry)
    .pipe(gulpif(!minify, sourcemaps.init()))
    .pipe(eslint({useEslintrc: true}))
    .pipe(eslint.format())
    .pipe(rollup({plugins: [
      includepaths({paths: [path.rollup.import]}),
      babel()
    ]}))
    .pipe(filter(function(file){return !/\/_/.test(file.path) && !/^_/.test(file.relative);}))
    .pipe(gulpif(minify, strip()))
    .pipe(gulpif(minify, uglify()))
    .pipe(gulpif(!minify, sourcemaps.write()))
    .pipe(rename({suffix: '.min', extname: '.js'}))
    .pipe(gulp.dest(dist+'/js'));
}

// ▽ サーバーをリロード(browser-sync)
function browserReload(done){
  server.reload();
  done();
}

// ▽ ライブリロードサーバー(browser-sync)
function serverInit(done) {
  server.init({
    server: {baseDir: 'dist'}
  });
  done();
}

// ▽ ファイルを監視
function watchFile(done) {
  minify = false;
  gulp.watch(path.pug.entry).on('change', gulp.series(pugCompile, browserReload));
  gulp.watch(path.stylus.entry).on('change', gulp.series(stylusCompile, browserReload));
  gulp.watch(path.scss.entry).on('change', gulp.series(scssCompile, browserReload));
  gulp.watch(path.rollup.entry).on('change', gulp.series(jsRollup, browserReload));
  done();
}

// ▽ リリース時のタスクス開始処理
function destStart(done) {
  minify = true;
  done();
}

// ▽ リリース時のタスク終了処理
function destFinish(done) {
  minify = false;
  done();
}

// ▽ Gulpにタスクを登録
gulp.task('sync', serverInit);
gulp.task('watch', watchFile);
gulp.task('dest', gulp.series(destStart, pugCompile, stylusCompile, scssCompile, jsRollup, destFinish));

続いて.babelrcを編集してトランスパイルするプリセットを指定します。

{
  "presets": [
    ["@babel/preset-env", {
      // "loose": true,
      "targets": {"browsers": ["> 1%, not dead"]},
      "modules": false
    }]
  ]
}

続いて.eslintrcに記述に関するルールを定義します。

{
  "parser": "babel-eslint",
  "env": {"browser": true},
  "rules": {
    "indent": ["error", 2],
    "linebreak-style": ["error", "unix"],
    "semi": ["error", "always"],
    "quotes": ["warn", "single"],
    "eqeqeq": ["warn", "always"],
    "no-var": "warn"
  }
}

これでpugとStylusに加え、es6で書かれたスクリプトファイルをリリース環境に合わせたJavaScriptにトランスパイルできるようになりました!

JavaScriptをバンドルしてみよう!

複数ファイルで構成されたJavaScriptを1ファイルにバンドルしてみましょう。
先ほど生成したindex.jsを編集します。

import hello from 'components/_helloworld';
// _helloworld.jsをimportして実行
hello();

チェック用なので、単純にインポートのみ記述してあります。
続いて、components/_helloworld.jsを編集します。

export default function hello() {
  const message = 'Hello world!';
  alert(message);
  console.log(message);
}

HTMLにCSSを適用する必要がありますので、/src/pug/index.pugも修正しておきます。

- var TITLE = "agrius 佐藤農園 pugのコンパイルテスト"

doctype html
html(lang="ja")
  head
    include assets/_head
    link(rel="stylesheet", type="text/css", href="css/style.css")
  body
    .flex
      p パグが正常にできるかのテストです。
      p 1
      p 上の数字を変更してリロードされればOKです!

    script(src="js/index.min.js", defer)

ターミナルからタスクを起動します。

$ yarn dev

ブラウザが起動して以下アラートが表示されると思います。

Hello world!

出力されたdist/js/index.min.jsを確認してみましょう。

'use strict';
function hello() {
  var message = 'Hello world!';
  alert(message);
  console.log(message);
}
// _helloworld.jsをimportして実行
hello();

キレイにファイルが統合されていますね!
constがvarに変更されるなど、IE10でも実行可能なJavaScriptに変換されています。

開発とリリースの切り替え

前回の記事では開発とリリースの切り替えをフラグで処理していましたが、今回はもう少し発展させてみましょう。

今回のgulpfile.jsには以下のような追記をしています。

// ▽ リリース時のタスクス開始処理
function destStart(done) {
  minify = true;
  done();
}

// ▽ リリース時のタスク終了処理
function destFinish(done) {
  minify = false;
  done();
}
// ▽ Gulpにタスクを登録
gulp.task('dest', gulp.series(destStart, pugCompile, stylusCompile, scssCompile, jsRollup, destFinish));

destというタスク実行の際、minifyフラグをtrueにして、HTML/CSS/JavaScriptの出力タスクを全て実行するように書いてあります。

package.jsonにdestinationという名前で、destタスクを実行するスクリプトを追加します。

"scripts": {
  "dev": "gulp sync watch",
  "destination": "gulp dest"
}

ターミナルからdestタスクを起動します。

$ yarn destination

こうすることにより、タスクを切り替えるだけで、ファイルの圧縮/コメントやデバック出力の削除を一括で処理できるようになります。

もう一度、出力されたdist/js/index.min.jsを確認してみましょう。

"use strict";function hello(){}hello();

キレイに圧縮されていますね!
余分なスペースやコメント、アラートやコンソールの出力などのデバックリソースも跡形もなく消してくれます。

destタスクはリリース時にしか使いませんが、登録しておくと便利です。


すぐにでもコーディングしたくなってきますよね!

基本、WEBデザイナーにはこれで十分です。

WEBの情報にはユニットテストやら、ヘッドレスブラウザなんかも必須に扱っている記事も多くみられますが、WEBデザイナーには全く必要ありません。

シンプルで無駄のない構成かと思いますので、ぜひ皆さんも利用してみてください。


田畑を耕しながら田舎でのんびりWEB開発?
若い頃から準備すれば誰にでもできますよ!
今に疲れているクリエイターの方々の少しでも参考になれば嬉しいです。