Quantcast
Channel: 開発 –アカベコマイリ
Viewing all 74 articles
Browse latest View live

Browserify の require オプションでモジュールを外部公開する

$
0
0

Browserify で bundle している JavaScript 内のモジュールを外部公開したくなり、その方法を検討してみた。

前提条件

検討にあたり、前提条件や設計方針をまとめておく。

  • Browserify は CLI として利用
    • 最近の bundle は CLI で管理しているため
    • 設定をあわせれば Node インターフェイスでも同じことは可能なはず
  • CLI は package.json の npm-scripts から呼び出す
    • 最近の開発タスクは npm-scripts で管理しているため
    • 設定をあわせれば gulp とかでも同じことは可能なはず
  • JavaScript は ES2015 以降で実装、transplier には babelify を利用
    • TypeScript 派なら tsify でもよい
  • モジュールの外部公開とその参照を同時におこなうサンプルを作成する
    • 参照が有効であることを実証したい
  • 公開、参照は ES2015 の export/import で記述
    • CommonJS は使用しない
    • ES2015 以降を採用するだから、その範疇で実装する

require オプション

Browserify でモジュール公開機能は substack/node-browserify によると --require オプションを利用するようだ。短縮版は -r。以降はこちらで説明する。このオプションの詳細は README の multiple bundles に解説されている。

公式ドキュメントのサンプルには

$ browserify -r ./robot.js > static/common.js

とある。これを実行して生成された JavaScript は

{
},{}],"./robot.js":[function(require,module,exports){
}

のようになり、

var robot = require('./robot.js');
console.log(robot('boop'));

というふうに参照できる。-r で公開したモジュールの参照は指定されたパスで解決するらしい。深いパス、例えば

$ browserify -r ./src/lib/Library.js > src/assets/lib.js

のようにした場合、requireimport に指定するパスは ./src/lib/Library.js になる。HTML から読み込む場合、当然ながら公開モジュールを先、参照する側を後に定義しなければならない。

<script src="lib.js"></script>
<script src="app.js"></script>

公開するモジュール名を変更する

公式ドキュメントの external requires には

$ browserify -r through -r duplexer -r ./my-file.js:my-module > bundle.js

というサンプルがある。重要なのは -r ./my-file.js:my-module の部分。ファイルのパスに続けて :modulename を指定することで、その名前による参照が可能となる。

前述のパスよりも、こちらのほうが便利だろう。モジュール名を自由に変更できるため、開発環境ではファイル名を Library.js のような PascalCase にしておき、公開するときは npm 的な library という小文字 ( + ハイフンなど ) にするといった運用も可能。

実践編

掲載しているサンプルは必要最小にまとめているので、詳細を知りたい場合は記事の最後にリンクしたリポジトリを参照のこと。

プロジェクトを以下のように構成。

.
├── package.json
└── src/
    ├── assets/
    │   └── index.html
    └── js/
        ├── app/
        │   └── App.js
        └── lib/
            ├── Library.js
            └── Util.js

公開モジュールを含む Library.js の実装は

import Util from './Util.js';

/**
 * Outputs the "Library" to the console.
 */
function Echo() {
  console.log( 'Library' );
}

export { Util, Echo };

とする。Util.js に定義した class と自身のローカル関数を ES2015 Modules 形式で公開。これを参照する App.js は

import { Util, Echo } from 'library';

// Application entry point
window.onload = () => {
  const date = Util.formatDate();
  Echo();
  console.log( '[' + date + '] Application was launched.' );

  const elm = document.querySelector( '.date' );
  if( elm ) {
    elm.textContent = date;
  }
};

となる。こちらは公開されたものをひとつのモジュールから個別に import している。これらをコンパイルする npm-scripts を以下のように定義。

{
  "babel": {
    "presets": [ "es2015" ]
  },
  "browserify" : {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "build:js-lib": "browserify -r ./src/js/lib/Library.js:library -d | exorcist ./src/assets/lib.js.map > ./src/assets/lib.js",
    "build:js-app": "browserify ./src/js/app/App.js --im -d | exorcist ./src/assets/app.js.map > ./src/assets/app.js",
    "build": "npm-run-all -p build:js-lib build:js-app",
    "watch:js-lib": "watchify -r ./src/js/lib/Library.js:library -v -o \"exorcist ./src/assets/lib.js.map > ./src/assets/lib.js\" -d",
    "watch:js-app": "watchify ./src/js/app/App.js -v --im -o \"exorcist ./src/assets/app.js.map > ./src/assets/app.js\" -d",
    "watch": "npm-run-all -p watch:js-lib watch:js-app",
    "release:js-lib": "cross-env NODE_ENV=production browserify -r ./src/js/lib/Library.js:library | uglifyjs -c warnings=false -m > ./dist/lib.js",
    "release:js-app": "cross-env NODE_ENV=production browserify ./src/js/app/App.js --im | uglifyjs -c warnings=false -m > ./dist/app.js",
    "release:clean": "rimraf ./dist",
    "release:copy": "cpx \"./src/assets/**/{*.html,*.eot,*.svg,*.ttf,*.woff,package.json}\" ./dist",
    "release": "npm-run-all -s release:clean release:copy -p release:js-lib release:js-app"
  },
  "devDependencies": {
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-power-assert": "^1.0.0",
    "babel-register": "^6.9.0",
    "babelify": "^7.3.0",
    "browserify": "^13.0.1",
    "cross-env": "^1.0.8",
    "exorcist": "^0.4.0",
    "mocha": "^2.5.3",
    "npm-run-all": "^2.3.0",
    "rimraf": "^2.5.3",
    "uglify-js": "^2.7.0",
    "watchify": "^3.7.0"
  }
}

タスク名に :js-lib とあるものがライブラリとなる公開モジュール、:js-app はそれを参照する側になる。重要なポイントを解説する。

:js-lib では -r と前述の公開モジュール名変更により library という名前にした。

しかし参照する側も Browserify でコンパイルしているため、これをそのまま import すると npm install したわけでもないため参照エラーになる。この問題を回避するため :js-app では --im オプションを利用して解決できない参照は無視しておく。

これは Electron を試す – 開発環境の構築で試した方法になる。Electron の Main プロセスを Browserify でビルドする場合、fs や path など Node 由来のモジュールがあるため --im で参照の解決を実行時に遅延させている。

今回も原理は同じ。参照の解決は HTML 側で

<script src="lib.js"></script>
<script src="app.js"></script>

のように読み込まれた時となる。

まとめ

Browseify といえば単一ファイルを生成するもの、という印象があった。そのため複数ファイルを出力するなら webpack を利用したほうがよいという意見を聞いたこともある。

しかし

  • モジュールを公開できて
  • 複数ファイルを出力する

ぐらいの用途ならば Browserify & npm-scrpts でも十分に対応可能なことがわかった。

Browserify だけでは複数ファイルを出力できないが、個別にタスクを定義して npm-run-all で連結すれば同等の機能を実現できる。この記事の npm-scripts だと build、watch、release でそうしている。

最後にサンプル プロジェクト全体を公開する。


Redmine 運用について 1/3 –はじめに

$
0
0

以下の記事に触発され、私の実施している Redmine 運用についてまとめたくなった。

しかし書いているうちにものすごく長くなってしまったので、全 3 回へ分割することした。

シリーズまとめ
Redmine 運用について

初回は自身と Redmine の関わりについて。どのような立ち位置から書かれているのかを前置きすることで、読者は自身との距離感を意識しながら記事を読めるのではなかろうか。

例えば Redmine 導入を提案するのであれば、先行事例として参考に。提案されている側であれば、相手がどのような背景や思想をもっているかを理解するのに役立つ、というのが狙い。

私の Redmine 運用経験

私は前職も含めると足掛け 6 年ほど Redmine を運用してきた。バージョン更新などの環境構築や運用ルール策定なども含む、システム管理者として活動している。

導入のきっかけについて。

親戚に Ruby on Rails でアプリ開発する企業を経営している方がいて、プロジェクト管理に Redmine を採用している話を聞く機会を得た。ちょうど業務で複数プロジェクトを TDD で回したいと考えていたのだが、当時採用していた Trac では面倒そうだと感じていたところである。

語られた Redmine の機能では、特に複数プロジェクト管理に強く惹かれた。その企業では小さな実験的プロジェクトを数多く管理する必要がある。そのため気軽にプロジェクトを追加できる Redmine が重宝しているのだという。また、ある機能を共通モジュールに切り出すときも、そのプロジェクトに元となる機能のチケットを移動させるとか、モジュールのチケットを利用側のチケットに関連付けられるといった管理も便利とのこと。

この事例に強く興味を惹かれ、けれどもいきなり業務で採用するのは怖いので、まずは個人で試してみることにした。

さくらのVPS を使いはじめるはそのときの話。ちょうど Redmine v1.0 がリリースされるかも?というタイミングだったと思う。はじめは共用レンタルサーバーで動かそうとしたけど CGI モードは激重なので、Passenger のために VPS へ移行した。これは現在このブログを稼働させているサーバーでもある。

実際に個人開発で試してみた結果、気軽に複数プロジェクトを作成できること、ガントチャートやカレンダーなどのよく使う機能が標準搭載されている点が気に入った。複数プロジェクト管理を前提としているため、チケットや Wiki の参照を横断的に指定できるところもよい。

というわけで導入を提案。

前職で私が在籍していたのはソフトハウスの開発部署だったこと、既に Trac である程度 TDD に馴染んでいたことから、提案はすんなり通った。私がシステム管理者になることを明言し、本業に影響しなければいいよ、と許可を得た。

浸透もわりとスムーズだったと記憶している。基本ルール策定やインフラ面を管理していたぐらいで、割りとうまく分割統治できていた。

現職は移籍前から Redmine が運用されていたものの、あるレンタルサーバーの付属サービスを利用していた。残念ながら提供されるバージョンも数世代前であり更新予定もなさそうなので DB や添付ファイルを吸いだして VPS に立てなおした環境へ引き継いだ。

Redmine の運用期間は前職 4 年、現職 2 年ぐらい。前職はソフトウェア開発中心だったので Redmine によるチケット管理と相性がよく、基本的な運用の経験値を貯めるのに有用だった。

現職はソフトハウスではないのだが製造業の一種であるため、同様に Redmine 向きな業務が多いと認識している。現に一部の社員は積極的にチケットを利用しているのだが、熟年社員や IT リテラシーの高くない社員への浸透はイマイチである。

幸い管理職が積極的であるため、2 年を経て全社的にチケット管理で回そうという体制にはなりつつある。しかし社員個人が能動的に利用する状態になっていないと管理職の負担が増すだけなので、ここを改善するのが目下の課題であり、やりがいを感じる部分でもある。

業務におけるシステム管理の割合

Redmine のシステム管理を担当するとして、業務でどれぐらいの労力を掛けているのか?は気になるところだろう。私の場合は前職、現職ともに体感で 5 〜 10% ぐらいである。

Redmine 導入と更新、運用ルール改定にコストが掛かるものの、それ以外は分割統治がわりと上手く回っていて、メイン業務であるソフトウェア開発への影響は気にならないレベル。

Redmin 更新は基本的に Major、Minor レベルのバージョン単位で実施している。セキュリティに関する致命的な問題が解決されている場合は Revision レベルでも更新。おおむね数ヶ月に一度ぐらいで済んでおり、時間にして長くても 30 分ぐらいで済むから負担でもない。この辺、運用ルールあたりで詳しく書く予定。

v0.x 時代の Redmine は非常に不安定で、Ruby や Rails 絡みのトラブルに悩まされたものだけど、v1.0 あたりからは問題に遭遇することはほとんどなかった。また Ruby 環境を rbenv/rbenv で管理するようになったのも大きい。

Redmine の環境トラブルでよくあるのが、Gem にまつわるもの。あるプラグインを試そうとしたらグローバルな gem が必要になってインストールしたけど、依存関係の問題とかバージョンが古すぎて環境が壊れた、みたいな問題は何度か経験した。

rbenv の場合、Ruby 環境を複数同時に構築して切り替える方式となる。そのためある環境が壊れたら、安定していたものに戻すといった運用が可能。これで随分と管理が楽になった。また、Redmine 3.3.0 リリース – Enjoy*Study のように最新の Redmine 環境を Vagrant box として提供してくれる方がいるおかげで、新バージョンを検証しやすいのも助かる。

更に現職では私の他に 2 名のシステム管理者を任命、運用ルール Wiki に明記された管理業務であれば代行可能な体制にした。これも負担を下げるのに役立っている。

テーマ開発

私は akabekobeko/redmine-theme-minimalflat2 という Redmine テーマを開発している。元々、個人利用のために minimalflat を作り始めたのだが、minimalflat2 は職場で採用することを前提に開発している。

普段の業務で触れるものだと開発意欲が持続できてよい感じ。

たまに第三者から issue や Pull Request が来ることがあるのも嬉しい。励みになる。問い合わせは日本語でも受け付けているので、この記事を読まれている方でテーマへの要望があれば遠慮せずにどうぞ。

まとめ

はじめは単一記事に運用ルールも含めた内容を書くつもりだったが、今回の前書きだけでもかなりの量になり、もくじを付けても読みにくいので分割することにした。

6 年も関わっているシステムだけあり、いざ書き始めると触れたい内容が次々と湧いてくる。今回の内容も独立した記事としたこどで、当初の倍ぐらいになった。

個人的な体験談が多くて第三者からするとあまり興味のない回かもしれない。こうした回顧録は私的に楽しむものであり、当時の状況を懐かしみながら書けてよかった。私に良し、という感じ。

次回は Redmine の運用ルールと諸設定についてまとめる予定。

Redmine 運用について 2/3 –運用ルールと諸設定

$
0
0

Redmine 運用について書いてみるシリーズ 2/3。

今回は職場の Redmine を運用するためのルールと諸設定をまとめる。守秘義務に抵触しそうな内容を避けるため、ぼかして書いているところもあるが主旨には影響していないはず。

シリーズまとめ
Redmine 運用について

システム管理者

システム管理者とは Redmine のユーザー管理画面においてシステム管理者が有効になっている状態を指す。プロジェクトの管理者とは別なので注意すること。この権限を持つユーザーは Redmine のあらゆる設定を変更できる。

Redmine を運用するにあたり、少なくとも 1 名 ( 1 ユーザー ) は必要になる。

任命するのであれば Redmine のインフラと機能の両面に通じているユーザーであることが望ましい。とはいえ、Redmine の環境は Ruby だけでなく Passenger や HTTP サーバー、DBMS も絡み複雑である。もしこれらの知識に不安を感じるなら My Redmine のようなホスティング サービスを利用するのもあり。

インフラも担当するなら Redmine 本家のロードマップRedmine.JP Blog あたりを巡回して最新動向をおさえておく。最近のバージョンではあまり破壊的な変更はないものの、UI 変更はインパクト大きいので、更新前に利用者へ通知するぐらいの配慮はほしい。

システム管理者が 1 名だと運用に関する知見が属人化して代替をたてるのが難しくなる。負担も大きくなるため、複数のシステム管理者を確保しておきたい。現職の場合、

  • メイン 1 名
  • サブ 2 名

という体制にしている。メインが私でインフラも担当、サブはユーザーやプロジェクト追加など設定面の管理だけ実施という感じ。

インフラについては将来 Ansible で構築できるようにして DB や files だけ引き継ぐとか、そういう管理を考えている。現在はこのあたりの情報を Redmine の Wiki にまとめており、それを読みながらであればサブでも実行可能ではある。

認証

Redmine 管理画面の認証は以下のように設定している。

設定項目 解説
認証が必要 必要 Redmine コンテンツのアクセスはログイン必須とする。
自動ログイン 無効 手動ログインを必須化。いまどきの Web ブラウザならセッション維持機能が標準搭載されているだろうから、実用上の問題もないと判断。
ユーザーによるアカウント登録 無効 登録はシステム管理者が担当する。「手動で〜」にしてもよいが結局は内容を精査して許可することになるため、設定もシステム管理者に一任したほうがよい。
ユーザーによるアカウント削除を許可 無効 勝手に削除されては困るので無効。そもそもユーザー無効化は削除よりロックのほうが好ましい。
パスワードの最低必要文字数 4 旧 Redmine からのアカウント継承により標準のままとなっているが、近いうちに 32 文字へ変更する予定。複雑度を設定できないのが残念。職場では KeePassX によるアカウント管理を推進しているため、このツールでランダム生成された複雑度の高いパスワード設定を義務付けたい。
パスワードの有効期限 無効 期限による定期変更を強制するよりも、パスワードを長く複雑にするほうがよい。パスワードの定期的変更に関する徳丸の意見まとめ – 徳丸浩のtumblrの見解に賛同。
パスワードの再発行 有効 パスワードの管理はユーザー個人に委ねているため、紛失からの復旧手段も提供する必要がある。そのため有効にしておく。
追加メールアドレス数の上限 5 標準のまま。こんなに要るだろうか。自社でドメインを取っているなら、それに紐づくひとつで十分かもしれない。
OpenIDによるログインと登録 無効 システム管理者のみ登録、という運用なのでこの機能を提供する必要はない。

Redmine を業務管理に利用するのであれば、認証を可能な限り厳しくしてアカウント操作もシステム管理者のみに許可することが望ましい。管理と依頼窓口を一元化することで、不測のトラブルを防止できる。

OSS プロジェクトなら、不特定多数のユーザーを募ることになるだろうから、パスワードの文字数を長くするぐらいで他は標準設定のままでもよさそう。例えば Redmine 公式はユーザーによるアカウント登録を許可しており、テーマ作者自身が Theme List を更新できる。私も登録していて、このページを編集することがある。

メール通知

Redmine 上で自身の関係するチケットやウォッチ対象が更新されたとき、メールで通知する機能がある。詳しくはメール通知 – Redmine用語解説を参照こと。

地味ながら非常に重要な機能なので、必ず有効にしている。Redmine はチケットや Wiki をコンテンツとする一種の CMS と見なせる。CMS を浸透させるためには、そこに関心事があることを積極的に通知することが重要である。

コンテンツ更新を把握するため同期的に張り付くなんて、いまどきありえない。非同期に更新通知がきて、好みのタイミングで反応するのがあるべき姿ではないか。

なお、メール通知対象とする動作は大量にあるけれど、標準設定であるチケットの追加と更新だけ有効にしている。他の操作で通知するのは過剰に思われる。例外としてニュース機能を利用していなら、その性質上、メール通知したほうがよさそう。

プラグインとテーマ

プラグインは一切、使用していない。理由は以下。

  • インストールが面倒
    • 基本は手動で wget、unzip、rake コマンド実行
    • 公式リポジトリはなく、基本的に野良プラグイン
    • gem に絡む不具合でインストールできないケースも何度か遭遇
    • Ruby、Rails、DBMS の知識がないとエラーを理解できない
    • 時にはプラグインのコードに手を入れることになる
  • 著名なものでも更新が滞りがち
    • 最新の Redmine に追従できていない
    • 導入済みで migrate なら動作するが新規インストールには失敗する、とかあって怖い
  • プラグインに不具合が発生すると Redmine 全体がクラッシュすることがある
    • 業務で発生したら大損害
    • テーマ開発してるとき特定プラグインで表示がおかしくなるという issue が報告されたのでインストールしてみた時に遭遇
    • gem とか rake 絡みで問題が発生すると影響範囲が本体に及ぶことを知った
    • Redmine の設計的な問題だと思う
    • gem 未使用または静的に包含することを強制するとか、本体と隔絶した設計にしたほうがよいのではなかろうか

要するにエコシステムがうまく回っていない。インストールに一発で成功して動作するとガッツポーズを取ってしまうぐらい事故に見舞われたので、もうお腹いっぱいである。WordPress がいかによく出来ているかを実感する。

優れたプラグインもあるため、このあたりの問題に Redmine として対策されたなら改めて導入を検討したい。

一方、テーマは CSS と JavaScript だけで構成されており、それ自体で完結しているため問題には遭遇しにくい。少なくとも Redmine がクラッシュすることはないはず。そのため過去には A1、現在は自身で開発している minimalflat2 を採用している。

さきほど WordPress を引き合いに出したが、WordPress の場合はテーマが HTML の DOM 構造まで操作するので問題が起きやすい。逆にプラグインは基本的に WordPress 本体の提供する API のみを使用するようになっている。共通して、どちらも PECL は使用せず動作環境は配布されるイメージだけで完結している。

Redmine もこのようにすれば、公式リポジトリからの自動インストールなどをサポートしやすくなるし、本体 API だけ追従できれば互換の問題も起きにくくなるのになぁ、と思う。

プロジェクト分類と階層化

複数の案件を同時に管理する場合、その単位でプロジェクトを用意したくなる。

少ない案件を何年も回すなら単純にプロジェクトを追加してゆくだけでよい。数が増えてきたら見通しをよくするため、ルール化された階層化を推奨する。

現職の場合、扱う案件は受託と自社に大別されるので、これをベースに階層化している。

.
├── 受託
│   ├── 企業 A
│   │   ├── 案件 A
│   │   ├── 案件 B
│   │   └── 案件 C
│   ├── 企業 B
│   │   ├── 案件 A
│   │   └── 案件 B
│   └── 企業 C
│       ├── 案件 A
│       └── 案件 B
└── 自社
    ├── 部署 A
    ├── 部署 B
    └── 社員個人
        ├── 社員 A
        └── 社員 B

顧客から請け負う案件は受託に分類する。その下へ顧客となる企業名案件名というように階層化してゆく。顧客があまりにも多いとか類似案件が大量にある場合は、それをあらわす系にまとめてもよい。

例えばひとつのパッケージ製品があり、ほとんどの案件で作業が似通っているなら、

  • 受託直下にパッケージ製品のプロジェクトを用意
  • そのプロジェクトの子プロジェクト、またはロードマップで案件を管理

という感じにする。

社内向けの作業などは自社に分類する。配下は事業や部署単位で階層化する。特別なプロジェクトとして社員個人がある。この配下に社員単位で個別のプロジェクトを用意する。

ここはいわゆる砂場である。クライアントや部署とは隔絶された安全圏なので、自由に使ってもらう。Redmine を導入するにあたり、いきなり実案件でチケットや Wiki を書いてもらうより、個人プロジェクトで練習する機会を与えることで苦手意識を克服するのが狙い。

現職だと個人的なブックマークや Tips 集として利用されることが多い。チケットはあまり活用されていないが、Wiki でメモを取る需要はそれなりにあるようで、管理者として活動を眺めているとけっこう積極的に更新されている。

Redmine 管理用プロジェクト

Redmine のシステム管理用プロジェクトがあると便利だ。前述の階層構造であらわすと、自社の直下にRedmine 管理といった名前で用意する。

新規にプロジェクトやユーザーを追加したい場合、このプロジェクトのチケットで依頼してシステム管理者が対応する。確か Redmine 標準設定だとプロジェクト管理者のロール設定でプロジェクトやサブ プロジェクトの追加が有効になっているのだが、これは明示的に無効化する。

このようにすることで、プロジェクトを追加する窓口はシステム管理者に一元化される。

Redmine を運用していると、システム管理者の関与していないところで無尽蔵にプロジェクトが作成されてゆき見通しが悪くなりやすい。プロジェクトの階層構造にルールを設けても、たやすく破壊される可能性がある。こうした事態を防ぐため、システム管理者だけがプロジェクトを増減可能にしておく。

プロジェクトやユーザー追加をチケット化は、手続きを可視化するための施策。依頼方法を別途検討するより、Redmine 内で管理するほうがわかりやすい。例えばプロジェクト作成依頼の場合、チケット作成フォームの入力内容は

入力項目 内容
トラッカー サポート
題名 プロジェクト追加依頼
説明 テンプレートを埋めて書く。後述。
担当者 システム管理者

という感じになる。プロジェクト追加に必要な情報は Wiki にテンプレートを用意して、それを埋めてもらう。Textile のテーブル記法は慣れないと難しいので、単純なプレーンテキストを書き換えるようにした。

「ZZZZ」案件を管理するプロジェクト作成を依頼します。

顧客名 : YYYY 
案件名 : ZZZZ
管理者 : A、B
開発者 : C、D、E 

顧客名案件名は前述のプロジェクト階層に対応している。はじめての顧客なら、その階層から作成する。企業内で案件を管理する番号などがあれば、それを追加してもよい。このような管理には企業文化が反映されるものだ。

プロジェクトの初期メンバーは管理者開発者に列挙されたユーザーにしておく。以降、メンバーを増減する場合はプロジェクト管理者が実施する。時間トラッキングなど、プロジェクトに必要なモジュール設定なども同様。基本は分割統治である。

プロジェクトを作成したら、その URL をコメントしつつチケットのステータスを解決に変更、担当者を依頼者に設定して内容を確認してもうらう。問題なければ依頼者はステータスを終了にする。なにか設定に過不足があればフィードバックしてから、このやりとりを繰り返す。

プロジェクト管理者が変更できる設定なら、誤りがあってもそちらで対応してもらったほうがよい。そのため、プロジェクト追加の依頼者をメンバー設定でプロジェクト管理者にしておくと作業がスムーズに進む。

プロジェクト追加

プロジェクト追加にあたり、以下を考慮している。

  • プロジェクト名
    • 階層によって異なる
    • 企業名であればフルネーム
    • 案件系ならば実際の製品名をなるべく正確
    • コードネームを割り当てる文化があるなら、それを採用してもよい
  • プロジェクト識別子
    • プロジェクト階層を意識した命名にする
    • 例えば CLIENT-COMPANY_NAME-PRODUCT_NAME
    • 階層の区切り文字はハイフン、各ユニット内の区切りはアンダースコアにしている
    • CSS の命名ルールである BEM みたいな感じ
    • Gitolite などで自前リポジトリをプロジェクトに関連付ける場合、その名前にも流用する
    • 企業名については公式サイトのドメイン名を利用してもよい
    • 英語に訳しにくいとか、長過ぎる場合はドメイン名を参考にすることが多い
  • メンバー
    • プロジェクトは基本、メンバーにのみ公開する
    • 外部企業と Redmine 上でやりとりする場合、守秘義務も絡むので公開範囲はなるべく小さくする
    • 社員全員に公開するものでも、プロジェクトを公開せずメンバー追加で対応する
    • 非メンバー匿名ユーザーを追加しないように注意すること
  • ロール
    • Redmine 標準のロールである管理者開発者報告者をそのまま利用
    • プロジェクトに関する設定は管理者に任せる
    • 実務者は開発者にする
    • グラフィック デザインや資料作成も実務なので開発者ロールになる
    • 案件の成果物を検証する、進捗などを閲覧するなど補助的なユーザーは報告者にする

プロジェクトで使用するモジュール ( 各種機能 ) などは管理者に委ねる。基本は分割統治だが、システム管理者としてはプロジェクトが公開されていないか?だけ注意する。

これはプロジェクト設定の情報タブにある公開というチェックボックスで切り替えられるのだが、その権限はロール設定でも無効にできないため、たまに Redmine 管理画面のプロジェクト一覧で公開がチェックされていないことを確認したほうがよい。

なお、Redmine 管理画面の設定からプロジェクトを選び、デフォルトで新しいプロジェクトは公開にするのチェックを外すことで、プロジェクト追加時の初期値を非公開にできる。

ロードマップ ( バージョン )

ロードマップとは、期限を持つ工程に名前をつけて管理するための機能である。画面によってロードマップバージョンなど、呼称がバラバラなので注意する。というか、混乱するのでいい加減、どちらか一方に統一してほしいものだ。

ロードマップを利用する場合、ソフトウェア開発なら Semantic Versioning に従って v1.0.0 のようにバージョン番号で命名するとわかりやすい。v1.0.0 未満の新規プロジェクトであれば

1.Alpha
2.Beta
3.RC ( Release Candidate )
4.RTM ( Release To Manufacturing ), GM ( Gold Master )

のようにしてもよい。ソフトウェア開発以外でも何らかの工程管理を実施しているならば、それらに名前を付けていると思われるので、それをそのまま適用するとよい。

ロードマップを設定してチケットを関連付けると、それらを集計してロードマップ全体の進捗率を表示してくれる。例えば Redmine v3.3.1 をみると、2016/7/27 時点の進捗率は 64% になる。

時間トラッキングや期限を設定した場合、予想工数と実作業時間や残り日数も教えてくれるので、プロジェクト リーダーやマネージャーが現状をざっくり把握するのに有用である。そのため、更新や運用フェーズのないプロジェクトでもロードマップは設定しておいたほうがよい。

チケット管理

チケットを作成する場合、なるべく粒度を小さくすることを推奨している。プロジェクトやロードマップに比べて個人差が出やすいので、Wiki にガイドラインやサンプルを掲載しつつ、地道にアドバイスして慣れてもらう必要あり。

Redmine 標準のトラッカー、ステータス、優先順位は細か過ぎるので、必要なもの以外は削除して簡素化するのも有効。

チケット分類のためにトラッカーやステータスを増やしたがるユーザーもいる。例えばトラッカーに「設計」、「資料作成」、「顧客折衝」、ステータスに「検証」、「顧客待ち」など。

こうしたものはカテゴリで解決することを提案する。ある状態を定義できれば十分というケースが大半だろうし、それはプロジェクト固有の事情であったりするから、カテゴリでもよいはず。

トラッカーとステータスは全プロジェクトに影響するため、ひとたび設定すると廃止が難しい。そのため、よほど汎用なものでない限り避けたほうがよい。汎用の判断基準としては業界で標準的な概念とか業態で必須の工程だろうか。

入門Redmineに呉服屋の受発注を Redmine で管理する例が掲載されているけど、こうした業界であればその用語でトラッカーやステータスを定義してもよさそうだ。

時間管理

Redmine のチケットはそのまま利用しても便利だが、時間管理を組み合わせると更によい。プロジェクト設定のモジュール時間管理を有効することで利用を開始できる。

実際に運用する場合、以下のようになる。

  1. チケット作成時に予定工数を入力
  2. チケットで管理している作業が進行する
  3. チケット編集画面を開く
  4. 時間を記録欄に作業時間活動内容を入力
  5. 覚書などがあれば注記欄に記述

作業時間を入力する都度、チケット全体の作業時間に累積されてゆく。この情報は実際の作業時間と見なせるため、予想工数との乖離をみて見積もり精度の目安にするとか、管理職への日報の代りにするなどの用途に使える。

また、ロードマップ画面には全チケットの総計として予定工数作業時間が表示される。この情報は進捗率とあわせてプロジェクトの状況を大まかに把握するため役立つ。進捗率は問題ないが、予定工数を作業時間が超え始めたならば、危険の兆候と判断してよいだろう。

時間管理の単位について。

この設定は基本的に時間単位で入力するのだが、小数点も入れられる。そのため私は以下の
基準を設けている。

  • 30 分 = 0.5 時間
  • 半日 = 4 時間
  • 1 日 = 8 時間

最小を 30 分として、1 日を標準労働時間である 8 時間で換算。半日はその 1/2、数日かかる作業なら 8 の倍数にする。

予定工数を 1.5 日 = 12 時間とか細かく設定しだすとキリなく厳密さを求めたくなるため、1 日を超えるものは日単位でざっくり決めている。逆に作業時間はベンチマークとして重要なので可能な限り細かく設定。

記録の更新は作業でキリのよいタイミングにしている。進捗率と一緒に更新することが多い。なるべく注記も一緒に書いておくと後で振り返るのに便利だ。私は作業の考察などを結構マメに書いている。Twitter へつぶやく程度の気持ちでガンガン書くのがよい。

Redmine の検索は Wiki だけでなくチケットも対象になるので、ある機能や業務に関連づいたメモ書きというのは、後に有用な資料となることが多い。

リポジトリとの関連付け

職場の Redmine は Git リポジトリと関連付けている。設定についてはさくらのVPS を改めて使いはじめる 10 – Git、Gitolite、GitHub で書いたとおり。GitHub でプライベート リポジトリを運用するか迷ったが、

  • 既に Redmine で業務タスクを管理していた
  • Redmine と GitHub issue の使い分けで迷いそう
  • GitHub なら issue もこちらで管理したほうがよい
  • そうなると業務と開発のタスク管理が分散して運用しにくい
  • 業務タスクも GitHub に移行する場合、Redmine の過去資産と分断されて管理しにくい

という理由により Gitolite を採用した。

Redmine と共に、Git ( Gitolite ) リポジトリの管理も私が担当している。リポジトリの追加頻度は高くないため、なんとか回せているが、hook 周りの設定とか Redmine との関連付けが面倒なので、この辺を Shell Script か小さな Web サービスで自動化したいと考えている。

Redmine と連携される場合、ひとつ大きな問題がある。Redmine のチケット番号はシステム全体で連番となるため、プロジェクト単位でリポジトリを作成しても、コミット時のコメントに書くチケット番号を 1 から開始できない。

そのためGitHub にリポジトリと issue を 移行したい場合、コミット ログを書き換えてチケット番号を整理するなどの面倒な作業が必要。やろうとは思わないが、このチケット番号の仕様は Redmine における Vendor Lock-In のひとつと認識している。

削除について

ユーザー、プロジェクト、チケットの削除は原則禁止。削除を実行するとこれらを参照している部分に影響するため、削除を求められた場合は以下のように対応している。

  • ユーザー
    • Redmine 管理画面のユーザーロックを指定
    • これでログイン不能になる
    • Redmine の認証を必須としていれば、ユーザーが関わることはない
    • ユーザーが戻ってきた時はロックを解除するだけで済む
  • プロジェクト
  • チケット
    • 絶対に削除しない
    • Redmine のチケット番号はシステム全体で連番となるため、存在しない欠番が発生すると管理的にややこしい
    • 間違えて作成したものなら内容を編集して再利用する
    • または単純にステータスを却下終了にするだけでもよい
    • 私は誤りも記録されるべきと考えている
    • 誤りよりもそれを直さないことが問題であり、記録しないと顧みる機会も失われる

まとめ

記事を書くにあたり職場の Wiki に掲載したルールを見なおしたのだが、判断基準も添えて書いているうちによくない部分が可視化され、棚卸しにつながった。特に認証設定は緩かったので、本記事のとおり厳し目に変更している。これだけでも書いた価値があった。

次回は職場に Redmine を普及させるための施策と課題について書く予定。数日、間が開くかもしれない。

Redmine 運用について 3/3 –普及の施策と課題

$
0
0

Redmine 運用について書いてみるシリーズ 3/3。

最終回は Redmine に馴染みないユーザー、例えば工程管理やバグ表を Excel で運用していたとか、そういう管理職や社員に対して Redmine を普及させるための施策や課題などを書く。

これまでの内容と重複する部分もあるが、本記事ではルールよりも事例や留意したことに重きを置く。

シリーズまとめ
Redmine 運用について

現状分析

前職はソフトハウスであり、一部に Trac による TDD を経験していたプロジェクトもあったので Redmine 導入はスムーズに行われた。TDD に親しみのない社員も BTS として利用しはじめて、Excel ベースのバグ表を置き換えながら慣れていった。

一方、現職は私が移籍する前に Redmine を導入していたものの、数名が実験的に利用しているような状況で、業務は主に上長の手による Excel シートで管理されていた。

シートには部署に属する社員と担当業務がずらっと並んでいる。2 週間ごとにおこなわれる定例で聞いた話を紙にメモ、その後に自席に戻ってそれを Excel に反映させるのだという。

これを Redmine 管理へ置き換えることを提案することにした。まずは現状分析するために二ヶ月ほど定例に参加してみた。定例の規模は 7 人で 1 時間、参加した回数は 5 回ぐらい。その中で気になった点をまとめる

  • 同じ課題が何度も繰り返されている
    • あの問題どうなりました、確認中です、ではまた次回に、という感じ
    • 手元のメモで数をカウントしたら 3 回のものもある
  • 進捗報告は基本的に作業の開始と終了のみ
    • 〜に着手しました、終了しました
    • 中間報告は率ではなく問題の有無となる
    • 期限までに終了すれば経緯を問わない
    • 担当者が問題と感じたものだけが問題として扱われる
    • 良くいえば裁量主義、悪くいえば放置放任
  • 議事録がない
    • 定例の記録は各人のメモに委ねられている
    • 業務管理 Excel シートは一種の議事録ともいえる
  • 2 〜 3 人で細かな業務の議論が突発的にはじまる
    • それで大丈夫ですか?あれはどうなっています?という感じで開始
    • 細かくメモを取るタイプだと報告から問題に気づき、それを指摘することが多い
    • 議論の記録が保証されないためか、この場で結論せねば、という雰囲気
    • これが数回あると、定例が 2 時間に及ぶこともある
    • 議論に関係ない社員は終了を待ち続けるか、雑談をはじめる
    • 私は眠気が抑えきれず、船を漕いでいる

業務管理はもとより定例にも多くの問題がある。これをもとに Redmine で対策する方法を考える。

部署単位の業務管理

定例で私の番が回ってきたとき、現状分析で気になった点を発表した。そのうえで、これらの問題が既に導入されている Redmine で解決可能なことを説明。口頭のみでスライドなどは用意しなかったが、当時を思い出しながら問題と対策をまとめる。

  • 同じ課題が何度も繰り返されている
    • 課題を Redmine のチケットとして管理する
    • チケットには課題の内容、担当、期日、対応状況を設定できる
    • 個人のメモや記憶に頼らず、常に確認可能
    • チケットならば任意のタイミングで着手できる
    • 定例を待たずとも議論可能
  • 進捗報告は基本的に作業の開始と終了のみ
    • 途中経過も重要
    • 作業の見積もりと実際にかかった時間は計測されているべき
    • そうしないと効率化できない
    • 定例の合間となる 2 週間、質問するまで手が空いていることもわからない
    • チケットで期日、作業時間、進捗率を設定することでリアルタイムに現状報告される
    • 定例を待たずとも把握可能
    • 作業が発生したら定例の場でチケット化する
    • 理想はチケット一覧を眺めながら数値で現状確認すること
  • 議事録がない
    • 議事録ドリブンを提案
    • テキストで残して全員が読めることの重要性を説明
    • 定例の前に Redmine Wiki に議題を書き、読んできてもらう
    • 定例では Wiki の内容をテキスト エディタでプレビュー表示
    • 当時は Sublime Text + timonwong/OmniMarkupPreviewer
    • 現在は Atom + textile-preview を利用
    • 議題に対する発言や決定事項をエディタで編集
    • メモしてほしいことがあれば発言する、というルールを提示
    • 定例が終了したときには議事録が完成、Wiki に反映して共有できている
  • 2 〜 3 人で細かな業務の議論が突発的にはじまる
    • 議事録ドリブンにより大きな問題は議題として登録済み
    • 小〜中規模の問題はなるべく当事者間で議論、できればチケットで
    • テキストでやりとりしないと、理解の齟齬が起きやすいことを説明
    • 口頭では相手の発言を精査引用するのが難しい、博覧強記を求められる
    • すべてを把握するより必要な情報がある場所を管理するほうが簡単

こんな感じの話をした。私は既に自身の業務に関係する社員と Redmine でやりとりしていたので、そのチケットを見せたりもした。

結果、まずは部署内の業務管理で Redmine を活用してみようということになった。上長の Excel シート管理はその後もしばらくは続いていたようだが、ここ 1 年ぐらい見ていないので移行できたのだろう。

対策のうち議事録ドリブンは採用、業務のチケット化については少しずつ浸透している。オフィスを歩いているときに見える PC の画面で、チケット編集している様子をみることが増えた。

全社的な展開

現職の企業は社員数 20 名ほどの小さな会社である。そのため、全社的な展開といっても大企業の 1 部署程度なのだが、横展開の手法という意味では規模に関係なく汎用性がある話といえるかもしれない。

個人、部署レベルでは Redmine による業務単位を取り入れたので、これを全社へ展開することにした。管理職には前述の提案を繰り返すことで賛同を得られたのだが、具体的な運用がピンと来ないとのことで、勉強会を希望された。

勉強会を開催するにあたり、実運用しているプロジェクトをサンプルとした。私はソフトウェア開発を担当しているのだが、この業務は目的のはっきりしたチケットが多いため、サンプルにふさわしい。

ある機能の開発着手、進捗更新、解決報告して検証、終了という流れを見せた。またそれをソフトウェア開発とな異なる業務、たしか書類作成だったかに当てはめて、同じように進捗管理する手順を説明。

概ね納得していただけたようで、全社的に積極利用する運びとなった。社員に対する説明は Wiki
に掲載した運用ルールやチャット、必要ならば個別に対応する。既に Redmine が導入済みということもあり、基本操作については特に解説しない。

かわりに

入門Redmine―オープンソースの課題管理システム 第4版 入門Redmine―オープンソースの課題管理システム 第4版

を購入して会社の本棚へ公開した。

私はこれの第 2 版を持っているが、基本機能だけでなく呉服屋の管理事例など運用面の話も面白く、かなりオススメの書籍である。Redmine の更新ペースには追従できていないが、激変というほどでもないので入門には十分だと思う。

管理職の説明会には質問も多く挙げられた。それらの内、特筆すべきものをまとめる。

プロジェクトはどういう単位で作成すればよい?

この話は前回を参照のこと。

現在の〜という業務をチケット化するとしたら?

  • 実際にチケットを作成しながら説明
  • バージョンや親チケットなどは慣れないと分かりにくいので割愛
  • トラッカーで業務の大まなか分類を指定
    • ソフトウェア開発でなくても「実務 = 機能」、「問題への取り組み = バグ」、「顧客対応 = サポート」のように分類可能
  • 題名に業務内容を指定
    • 〜業務とするより、その業務で達成したいことや成果物で考えるとチケット化しやすい
    • 〜する、〜を作成する、など
  • 説明に業務の背景や補足情報を記述
  • ステータス新規進行中終了だけで管理する
    • 慣れたら解決フィードバックも利用して終了を判断するタイミングを設ける
  • 優先度は慣れるまで設定しなくてよい
  • 担当者は業務を依頼する社員を指定
    • チケット上で対話する場合は、その内容をコメントしたうえで担当者を相手に変更
    • 相手が返答したら相手が自分に担当を戻す
    • これで対話が成立
  • 開始日はチケットに着手した日を指定
    • 業務の見積もりに関わる、管理職として非常に重要な設定
    • デフォルトはチケット作成日
    • すぐに着手しないなら空欄にするとよい
  • 期日は業務の締め切りとなる日を指定
    • 業務の見積もりに関わる、管理職として非常に重要な設定
    • デフォルトは空欄
    • これを設定するなら開始日も厳密に指定すること
    • 開始日と期日を指定することで正確なガントチャートを表示できる
  • 予定工数は時間単位で指定
    • 業務の見積もりに関わる、管理職として非常に重要な設定
    • 1 日 = 8、半日 = 4、1 時間 = 1、30 分 = 0.5 ぐらいの単位で入力
    • ざっくりでよい
    • あわせて時間管理を有効にし、実作業の時間も記録すると効率化や見積もり精度の向上に役立つ
  • 進捗率
    • 業務の現状把握に関わる、管理職として非常に重要な設定
    • 0 からはじめて 10% 単位で更新
    • ステータスを解決終了にするときは 100% にする

業務の資料はなるべく Wiki に書くべきか?

  • なるべく Wiki がよい
  • Wiki は基本的な装飾とページ階層化、リンク機能があるので大抵の資料は書けるはず
  • チケットや Wiki のリンク略記も便利
  • 情報を Redmine に集約することで、検索の価値が高まる
  • 編集履歴も自動保存されるため、簡易なテキスト専用リポジトリとしても有用
  • 他社とのやりとりや印刷を考慮するなら Microsoft Office も可

Redmine に関するお知らせ

Redmine に関する情報を社員に告知したい時がある。例えば

  • Redmine 本体のメンテナンス
  • Tips
  • FAQ

など。これらを通知するため、以下の方法を採用している。

チャット

現職ではチャットワークを利用しており、Redmine という専用のグループを設置。メンバーは社員全員。例えば

  • Redmine の更新
  • サーバー更新に由来する再起動
  • 影響の大きな設定変更
  • 運用ルールの改訂

について通知している。突然利用できないとか動きが急に変わったといった問題に遭遇すると不安を感じるさせることになる。これを避けるため、事前通知は重要である。

また常時、質疑応答を受け付けている。その内容を FAQ にすれば社としての Redmine 知見を貯められる。ただ、あまり質問されることはなくて私が流した Tips に対する反応とか、そんな感じのやりとりが多い。

Wiki

Redmine の運用ルールや Tips、FAQ を貯める場所として利用。

チャットと同様に Redmine 専用プロジェクトを設置しており、Redmine に関することはその Wiki に記述。ここを読んでもらえれば Redmine はバッチリ!という状態を目指している。

Redmine の推進にあたり心がけていること

Redmine の推進にあたり心がけていることをまとめる。

古い管理手法を尊重する

業務やバグ管理に Excel シートを利用することについて、Redmine や GitHub issue を経験していると旧態依然に見えることだろう。

複数人の同時編集が難しい、履歴が取りにくい、セル管理の破綻しやすさなど、Web ベースの TDD/BTS に比べるとメリットなど皆無に感じられる。

それでも、紙でないだけマシである。また、紙であったとしても管理されているだけで価値がある。業務や開発において、最もよくないのは管理されていないことだ。

非効率、または無駄なルールが多くても、管理されているのであれば状況は可視化されている。つまりは改善の対象となりえる。管理されていない場合、業務で起きていることの分析・分類から始めなくてはいけない。

以上を踏まえ、Redmine や GitHub issue による管理を提案する場合は古い手法の管理者を尊重しよう。よくぞ管理してくれていました、と。

この記事でも挙げた Excel シートによる業務管理では社員単位の作業分担が記録されていた。ということはつまりチケットの題名と説明、担当者に流用できる。あなたの管理してきた情報は価値あるもので、それをより便利なツールに移行するだけという点を強調する。あまり過剰にすると褒め殺しっぽくなるので注意。

Redmine に限らず新しいツールや手法を提案するときは、いつもこのようにしている。対立を避けながら賛同を得るための方法としてなかなかよいと考えているのだけど、どうだろう?

とはいえ私は凡人なので、旧態をけなす誘惑に苛まれることがある。

Redmine 以外だと例えば Subversion から Git、jQuery から React へ移行したときには古巣がみすぼらしく感じられたものだ。それをけなすことで簡単に自身の成長を実感できる。いままで悩まされていたことを愚痴るのは、抗いがたい快楽をともなう。

この行為の問題は、新しい手法への敵対者を増やし賛同者を減らす効果にある。古巣に残らざるを得ない人にとって、そこをけなされることは耐え難い苦痛である。移行を検討できる人は対立を知ることで、それを見送るかもしれない。まだ議論は尽くされておらず新しもの好きが騒いでいるだけ、と。

この誘惑に負けてやらかした場合はなるべく深く悩んで、そのことを忘れないように心がけている。枕に顔を埋めてジタバタするわけだ。ああ、やらかしてしまった!という感じで。

活況を演出する

大昔に在籍していたプロジェクトで、知見を貯めるとかソフトウェアのリリース ページ作成などの目的で Wiki の採用が提案された。提案者は私ではないのだが、初めて触れる Wiki の機能性に感心してすぐさま賛同した。

そして運用がはじまった。しかし一部の賛同者が利用するだけで一向に広まらない。折にふれて Wiki ありますよ〜すばらしいですよ〜と宣伝してみるのだが、反応は冷ややかである。

そこで我々は考えた。

  • 使われないのは誰も使っていないからだ
  • この状態で書き込むのは新雪を踏むようなもので、気後れするもの仕方ない
  • 我々が書きまくろう
  • ジョークさえも許容しよう
  • 使ってよいという雰囲気を醸成するのが重要

というわけで、例えば海外チームのメンバーを紹介するのにヒッピー風とあれば、ヒッピーをリンクしてその説明を書いたりした。あと砂場というページを練習用に公開した。昔のことなので記憶が曖昧だが、Sandbox の和訳としてはじめから用意されていたページだったかもしれない。そこに冗談混じりのテキストのあることが重要だった。

しばらく、おそらく数ヶ月ぐらいかかった気がするけど次第に他のメンバーも書き込むようになった。ちょっとした技術的なテクニックとか考察なんかも充実してきて、ちゃんと分類したいということで PukiWiki へも移行した。他に内製の BTS もあったのだけど、そこに書かれないような情報は Wiki に蓄積されてゆき、当初の目標は達成された。

この経験から、あるシステムを普及させたい場合、活況であることの演出は非常に重要と考えるようになった。現職ではそのために以下の施策を実施している。

  • なんでもチケット化
    • ミーティングで議論が発生しそうな時は「それ、チケットで!」
    • なにか購入したいものがあるとき「稟議はチケットで!」
    • チケットが気軽に作成できて便利なものであることをアピール
    • 目的、担当者、期限のあるものなら大抵はチケット化できることをアピール
    • チケットに関わる機会を増やすことで自然と活況になる
  • チャットによる様々な通知
    • Redmine 自体の変化が活発であることを明示
    • 放置されていませんよ、ちゃんと生きていますよ、ということを演出
    • 空回りしている可能性も否めないが、続けることが大事
  • 様々なレポートの Wiki 記載
    • 現職ではセミナー参加を推奨している
    • 業務時間に出席することもあり、レポートを書くことが暗黙の義務となっている
    • レポートは Wiki に書かれ、その更新も通知している
    • これは筆者、読者の両方で関わっているが、かなり楽しい
    • アクセスログを見るに、読者数は多い
  • 議事録ドリブン
    • 私が司会者となるミーティングは基本的に議事録ドリブンで運用している
    • 議題と議事録は Wiki に書かれる
    • ミーティング中の発言が記録されて Wiki に反映される
    • 参加している感を強調
    • ジョークも記録しますよ、と言っているのだけどそれは遠慮されている
    • 司会と書記を兼ねるのは厳しいものがあり、タイピング速度の問題もあって、これをできる社員が 2 名しかいないのが難点

マメなサポート

これまでの内容と被る部分もあるのだけど、Redmine に関する質問や悩みは可能な限り迅速に対応している。基本はチャットだがテキストのやりとりが苦手な社員には口頭での相談も可。

ときには社員本人の席を訪れ、30 分ぐらいマンツーマンで Redmine に関する指導をすることもある。

普段、自分の利用している環境で直に説明すると高度な機能でも理解してもらいやすい。ついでにチケット化されていない業務を質問して、それをどのように Redmine 管理へ持ってゆくかを提案したりもする。

私のメイン業務はソフトウェア開発なので、こうしたサポートのコストは問題に感じられるかもしれない。しかし私は社全体で強くなることを目標としており、業務の効率化も立派なハックと考えている。そのために Redmine を提案したのだし、こうしたコストを捻出するためにメイン業務の見積もりはかなりの余裕を持たせている。

こうした考えに至ったのは、かつて在籍していたプロジェクトのリーダーの影響である。Wiki や議事録ドリブンもその一部で、その方はこうしたファシリテーションを非常に重視していた。実際、それを受ける側として効果を実感したこともあり、私もそれを踏襲している。

おわりに

つきあいの長い Redmine について以前からまとまった文章を書いてみたかったのだが、なかなかその気にはならなかった。書きはじめたら長くなるのは必然で、そのため腰が引けていた。そんな折、Redmineがいくら良くても会社の上司や経営者が見なければExcelがはびこってしまう事例: プログラマの思索とはてブの反応はよい刺激になった。

Excel 中心で管理したい上司をどう説得するか。説得を諦めて現実解として Excel ジェネレーターを実装するのか。

幸い私は理解ある環境に恵まれて Redmine 導入に成功したが、そうではない人や環境に対して知見の一部でも役立てば、という気持ちで記事にしてみた。

もし感想や Redmine 導入についての相談などがあれば、本記事のコメント欄や Twitter などでどうぞ。

Electron を試す 8 – electron-prebuilt のパッケージ名変更と Browserify

$
0
0

electron-packager の更新履歴をみていたら v7.5.0Add support for the new electron package name by zeke という PR に対応していた。内容を読むと electron-prebuilt のパッケージ名が electron に変更されたようだ。electron-prebuilt の README にも注記されている。

というわけで、このシリーズで作成したサンプルも名所変更に対応することにしたのだが Browserify 絡みで問題が起きたため、その内容と対策を記録しておく。

シリーズまとめ
Electron を試す

electron-prebuilt のパッケージ名変更

electron-prebuilt は Electron アプリの実行環境になる。従来、これをインストールするには

$ npm i -D electron-prebuilt

としていたのだが、v1.3.1 からパッケージ名が electron に変更されたので、以降は

$ npm i -D electron

とする。と、ここまでなら名前が短く分かりやすいので歓迎したいのだが…

Browserify のビルド問題と対策

Electron 本体に依存する機能は v1.0 から electron というパッケージ名で提供される。この辺の話は Electron を試す 7 – Electron v1.0 対応でも触れている。v1.0 より前は機能単位でパッケージ名を分けていたのだが、electron 配下へ統合された。

これを参照する場合、

const Electron = require( 'electron' );

とするか、ES2015 Modules であれば

import Electron from 'electron';

のようになるだろう。

通常はこれでよいのだが Browserify を利用して require/import を解決している場合は問題が起きる。electron-prebuilt のパッケージ名が electron に変更されたことで、electron に対する require/import が electron-prebuilt の実体を参照しようとしておかしくなるのだ。Browserify によるビルド結果は

module.exports = path.join(__dirname, fs.readFileSync(path.join(__dirname, 'path.txt'), 'utf-8'))

のようになる。そして electron-prebuilt からアプリを起動すると以下の実行時エラーが発生。

Error: ENOENT: no such file or directory, open '.../electron-starter/src/path.txt'
    at Error (native)
    at Object.fs.openSync (fs.js:640:18)
    at Object.module.(anonymous function) [as openSync] (ELECTRON_ASAR.js:167:20)
    at Object.fs.readFileSync (fs.js:508:33)
    at Object.fs.readFileSync (ELECTRON_ASAR.js:500:29)
    at Object.global.1.fs (.../electron-starter/src/main.js:5:42)
    at s (.../electron-starter/src/main.js:1:333)
    at .../electron-starter/src/main.js:1:384
    at Object.global.5.../common/Constants.js (.../electron-starter/src/main.js:231:17)
    at s (.../electron-starter/src/main.js:1:333)

この問題を回避する方法はふたつ。

  1. electron-prebuilt を旧名称で npm install する
  2. electron-prebuilt に対する require/import 参照を Browserify の対象外とする

方法 1 は electron-prebuilt 的に deprecated とされている。将来、旧名称が廃止される可能性もあって危険だ。よって正攻法の 2 を採用する。

substack/node-browserify の Usage を読むと --exclude オプションにパッケージ名を指定することで bundle ( 参照解決 ) の対象外となるようだ。

–exclude, -u Omit a file from the output bundle. Files can be globs.

というわけで npm-scripts で

{
  "scripts": {
    "build:js-main": "browserify -t [ babelify ] ./src/js/main/Main.js --im --no-detect-globals --node -d | exorcist ./src/main.js.map > ./src/main.js"
  }
}

となっていたものを

{
  "scripts": {
    "build:js-main": "browserify -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -d | exorcist ./src/main.js.map > ./src/main.js"
  }
}

に修正してビルドしたところ、アプリが正常に起動 & 動作することを確認できた。

Renderer プロセスについて

Renderer プロセスについて。私がこちらで electron を参照する場合、

const Electron = window.require( 'electron' );

のように window 経由で require を利用しているため Browserify の対象外なのだが、念のためこちらをビルドするときも --exclude electron している。なぜ Main/Renderer で参照方法を分けているのかは、

  • Main
    • electron の他にも fs など dependency にないパッケージを参照する可能性が高い
    • そのため --im オプションで dependency に見つからない参照を無視している
    • 今回の問題は electron が見つかるようになってしまったことで発生した
  • Renderer
    • dependency に存在するパッケージのみで構成
    • Web ブラウザ用の JavaScript ビルドと同じ思想で参照解決
    • --im オプションを利用しないので electron を直に require できない
    • よって window.require 経由で参照して Browserify の介在を回避

という理由から。この辺の話は Electron を試す – 開発環境の構築でも触れたが、今回の問題にも関わっているので改めて書き出してみた。

ちなみに Renderer が利用する electron 由来の機能も ipRenderer に限定している。プロセス間通信は Web アプリにおける Client–Server Model を踏襲し、

  • Clinet ( Renderer ) が必要に応じて Server ( Main )Request ( ipcRenderer )
  • ServerRequest の結果を ClientResponce ( sender.send )
  • Server は必要に応じて ClientPush Notification ( ipcMain )

という感じで設計している。

akabekobeko/examples-electron には対応を反映済み。各プロジェクトの npm-scripts で --exclude electron を削除してみれば、今回の問題とおかしくなったビルド結果を確認できる。

jsdoc-to-assert を試す

$
0
0

JavaScript の型チェックといえば FlowTypeScript だが、前者は型の定義ファイルが必要で、後者は言語自体を拡張しているため導入コストが少々、高い。もっと手軽に

  • 追加の外部ファイルは不要
  • JavaScript の組み込み型ぐらいをチェックできればよい

ぐらいのものがあれば、と探してみたら azu/jsdoc-to-assert がそんな感じなので試してみる。

2016/8/24
「省略可能な引数」と「null 許容型」の項を追記。

jsdoc-to-assert

jsdoc-to-assert は JSDoc 形式で書かれた関数のコメントを元に型チェックする。開発の経緯や詳細については JSDocをランタイムassertに変換するBabelプラグインを書いた | Web Scratch を参照のこと。おこなわれる処理は単純で、

  • JSDoc の @param から引数の型と名前を取得
  • 関数の冒頭に引数の型チェックを console.assert 形式で埋め込む

というもの。例えば

/**
 * Output log.
 *
 * @param {String} message Message text.
 */
function func( message ) {
  console.log( message );
}

というコードを jsdoc-to-assert に渡すと

/**
 * Output log.
 *
 * @param {String} message Message text.
 */
function func(message) {
  console.assert(typeof message === "string", 'Invalid JSDoc: typeof message === "string"');

  console.log(message);
}

に変換される。型チェックが偽ならば、その情報を assert として出力する。Firefox や Chrome の開発者ツールであれば関数の呼び出しと assert 箇所をコンソールから確認できるので、不正な値を指定した処理を修正するためのヒントになる。

環境構築と注意点

jsdoc-to-assert は本体と Babel plugin/preset 版が提供されている。plugin が Babel 用の機能拡張で、preset はそれをセットでパッケージ化したものになる。

いまのところ preset には babel-plugin-jsdoc-to-assert しか含まれていないので、どちらを選んでも機能差はないはず。とはいえ preset にしておけば依存が増えた時にも対応されるだろうから、今回は babel-preset-jsdoc-to-assert を採用。

以上を踏まえて package.json を定義。

{
  "babel": {
    "passPerPreset": true,
    "presets": [
      "es2015"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert",
          "jsdoc-to-assert"
        ]
      }
    }
  },
  "browserify": {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
    "build:": "browserify ./src/js/App.js -d | exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js",
    "watch": "watchify ./src/js/App.js -v -o \"exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js\" -d",
    "release": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs -c warnings=false -m > ./dist/bundle.js"
  },
  "devDependencies": {
    "babel-preset-es2015": "^6.13.2",
    "babel-preset-jsdoc-to-assert": "^3.0.2",
    "babel-preset-power-assert": "^1.0.0",
    "babel-register": "^6.11.6",
    "babelify": "^7.3.0",
    "browserify": "^13.1.0",
    "cross-env": "^2.0.0",
    "exorcist": "^0.4.0",
    "mocha": "^3.0.2",
    "npm-run-all": "^3.0.0",
    "power-assert": "^1.4.1",
    "uglify-js": "^2.7.3",
    "watchify": "^3.7.0"
  }
}

Babel の設定は .babelrc に書いてもよい。というか、そのほうが一般的だと思う。私はなるべく package.json にプロジェクト設定を集約したい派なのでこちらへ定義している。Browserify 設定も同様。

Babel 関連の npm をまとめる。

npm 用途
babel-preset-es2015 ES2015 変換用プリセット。
babel-preset-jsdoc-to-assert jsdoc-to-assert を利用するためのプリセット。
babel-preset-power-assert 単体テストで assert を power-assert に置換するためのプリセット。
babel-register Babel 変換時、import/require を補足するためのモジュール。assert の power-assert 置換などに必要。
babelify Browserify 用 Babel プラグイン。

今回は開発用のコードと単体テストの両方で jsdoc-to-assert を試したいので power-assert 関連も利用する。JavaScript 全体の bundle 化用に Browserify を使いたいので、Babel 本体は babelify を採用。Browserify が不要なら babel-cli を選んでもよい。

Babel の設定について。

{
  "babel": {
    "passPerPreset": true,
    "presets": [
      "es2015"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert",
          "jsdoc-to-assert"
        ]
      }
    }
  }
}

冒頭の passPerPreset が重要で、これを true にするとプラグイン単位で変換をハンドリング可能となる。プラグインの実行順で変換結果に問題が起きたときに指定するオプションである。

jsdoc-to-assert を試したとき Object の property として定義された関数などが無視されたので報告したところ、修正報告にこの設定を有効にしてほしいと回答されていたので、そのようにしている。

もうひとつ、jsdoc-to-assert の処理が開発版でだけ動作するように envdevelopment 側に定義している。こうすると NODE_ENV=production のときは除外される。npm-scripts の

{
  "scripts": {
    "release": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs -c warnings=false -m > ./dist/bundle.js"
  },
}

がその指定。通常、npm-scripts から NODE_ENV は操作できないので cross-env を利用した。この方法は React をリリース用ビルドして余計な開発用コードを除去するなどでも必要なので、個人的に定石となっている。

実行してみる

package.json の定義と型チェック用の JavaScript を用意したら、実行してみよう。

$ npm run build

出力された bundle.js をテキスト エディタで開いてみると、console.assert が挿入されることを確認できるはず。わざと間違った型を指定子したスクリプトを実装して、それを読み込んだ HTML を Web ブラウザの開発者ツールで見てみると

jsdoc-to-assert による assert

こんな感じで assert が出力されている。なお、jsdoc-to-assert v2.4.2 時点では以下の関数が assert 対象となる。

  • キーワード function で定義される通常の関数
  • Object の property として定義された関数
  • ES2015 class の constructor
  • ES2015 class のメソッド
  • ES2015 class の static メソッド
  • ES2015 class の property setter

ES2015 の Arrow Function は対応していないようだが、これはコールバックなどで局所的に使用されることが多く、それに対して JSDoc を書く機会は少ないだろうし、なくてもさほど困らない。

また、型チェックの対象となるのは JavaScript の組み込み型になる。ES2015 class を定義し、それを型として JSDoc の param に指定しても無視される。

もうひとつ注意。jsdoc-to-assert は param が正しく書かれていることを前提としている。よって変数名が間違っていると常に assert がヒットすることになるので、気をつけること。引数の名前を修正するとき JSDoc に反映し忘れるとこの問題に遭遇する。というか、した。

単体テストの assert

単体テストの対象となるコードに jsdoc-to-assert が適用されるとどうなるのか。私の利用している mocha + power-assert の組み合わせで試してみた。

describe( 'Valid', () => {
  describe( 'Sample', () => {
    it( 'constructor', () => {
      const sample = new Sample( 'message' );
      assert( sample.message === 'message' );
    } );
  } );
} );

のような正常系と

describe( 'Invalid', () => {
  /** @test {Sample} */
  describe( 'Sample', () => {
    /** @test {Sample#constructor} */
    it( 'constructor', () => {
      const sample = new Sample( 7 );
      assert( sample.message === 'message' );
    } );
  } );
} );

という異常系を実装してテストを走らせてみる。

$ npm test

> using-jsdoc-to-assert@1.0.0 test .../jsdoc-to-assert
> mocha --compilers js:babel-register test/**/*.test.js



  Valid
    Sample
      ✓ constructor

  Invalid
    Sample
      1) constructor


  1 passing (2ms)
  1 failing

  1) Invalid Sample constructor:

      AssertionError: Invalid JSDoc: typeof message === "string"
      + expected - actual

      -false
      +true

      at Console.assert (console.js:95:23)
      at new Sample (Sample.js:52:26)
      at Context.<anonymous> (Sample.test.js:96:22)

jsdoc-to-assert の埋め込むものは console.assert なので mocha 上の扱いが気になっていたが、普通に failing となった。power-assert のように値の詳細は表示されないものの、位置はわかるので型を修正するヒントとしては十分である。

余談だが、単体テストで実行される関数がどのように変換されたかを知りたい場合は

describe( 'Invalid', () => {
  it( 'logStrStatic', () => {
    Sample.logStrStatic( 'message' );
    console.log( String( Sample.logStrStatic ) );
  } );
} );

のように関数自体を String で文字列化して console.log するとよい。出力結果は

function logStrStatic(message) {
      console.assert(typeof message === "string", 'Invalid JSDoc: typeof message === "string"');

      console.log(message);
    }

となる。この例だと jsdoc-to-assert により埋め込まれたコードを確認できる。

省略可能な引数

関数を設計する際、オプション扱いの設定などを省略可能にすることがある。例えば

/**
 * 指定されたファイルにデータを書き込みます。
 *
 * @param {String} path 書き込み対象となるファイルのパス情報。
 * @param {Buffer} data 書き込むデータ。
 * @param {String} mode 書き込みモード。省略時はファイルを新規作成します。
 *
 * @return {Boolean} 成功時は true。
 */
function writeFile( path, data, mode = 'w' ) {
}

という関数があったとする。引数のうち pathdata は必須だが mode は省略可能で、その場合はデフォルトの動作をする仕様。これをそのまま jsdoc-to-assert すると mode の型チェックが挿入されるため、省略したときに assert されてしまう。

Use JSDoc: @param の Optional parameters and default values を読むと省略可能な引数は {String=} のように記述するようだ。しかし jsdoc-to-assert では型として認識されず assert が生成されない。

よって型の列挙を代替案とする。

/**
 * 指定されたファイルにデータを書き込みます。
 *
 * @param {String} path 書き込み対象となるファイルのパス情報。
 * @param {Buffer} data 書き込むデータ。
 * @param {String|undefined} mode 書き込みモード。省略時はファイルを新規作成します。
 *
 * @return {Boolean} 成功時は true。
 */
function writeFile( path, data, mode = 'w' ) {
}

mode の型を String から String|undefined に変更することで、mode が undefined にもなり得ることを示す。これを jsdoc-to-assert すると

function writeFile( path, data, mode ) {
  // ...略
  console.assert(typeof mode === "string" || typeof undefined === "undefined" || mode instanceof undefined, 'Invalid JSDoc: typeof mode === "string" || (\ntypeof undefined === "undefined" || mode instanceof undefined\n)');
}

というコードが生成される。param の型に undefined が登場すると assert 内で常に true となる箇所が埋め込まれる。そのため評価がここに達すれば常に assert を通るわけだ。結果として省略可能な引数の指定に使える。

後続に到達できない instanceof が登場することから、もしかするとバグなのかもしれない。なお undefined のかわりに null を指定したときも同様の assert が生成される。

null の型は object なので、これを列挙される型として判定するなら value !== null のように値そのものが null であることを調べなくてはいけない。

null 許容型

省略可能な引数の扱いを調べていて、そういえば JSDoc 的に null 許容型はどういう扱いなんだろう?とググッてみたら Use JSDoc: @type に説明されていた。

Number 型があるとして、それを null 許容型にするなら {?Number}、許容しないなら {!Number} とする。試しにこれを jsdoc-to-assert に渡してみたら

/**
 * Output log.
 *
 * @param {String|?String} message Message text.
 * @param {String|!String} message2 Message text.
 */
function func(message, message2) {
  console.assert(typeof message === "string" || message == null || typeof message === "string", 'Invalid JSDoc: typeof message === "string" || (message == null || typeof message === "string")');
  console.assert(typeof message2 === "string" || message2 != null && typeof message2 === "string", 'Invalid JSDoc: typeof message2 === "string" || (message2 != null && typeof message2 === "string")');

  console.log(message);
}

というコードが生成された値をちゃんと null 判定しており、しかも許容と非許容も区別されている。判定の演算子を ===!== にしないのは null 判定においてを厳密に型チェックする意味がないからだろうか。

なお、null 許容だけだと型チェックが抜けるので、本来の型も列挙しておいたほうがよい。

あと ESDoc もこの記法に対応していた。null 許容と非許容を適切に判定し、元の型へのリンクを生成しつつ Attribute 欄に nullable: false/true を記述してくれる。

この情報を知れたのは収穫だった。

まとめ

JavaScritp 組み込み型に限定されるが、JSDoc さえキッチリ書いておけば、それだけで型チェックできるというのは便利ではなかろうか。独自の型を使用するような場所ではダック タイピングすればよく、その場合は assert ではなくユーザー向けコードでも明示的にインターフェースのチェックとエラー処理を実行したくなるだろう。

そういう意味でも今の私なら jsdoc-to-assert ぐらいで実用十分である。

今回の記事で作成したサンプル プロジェクトを以下に公開した。実際に動かして、assert がどのように出力されるか、コンパイルされたコードはどうなっているかを確認できる。

npm_package_config と npm_config

$
0
0

npm-run-allSupport npm config params という issue から npm_config の存在を知った。以前、npm-scripts 内で変数を展開したくなって cross-conf-env を開発した時に npm_package_config は調べたが、この npm_config も外部から npm-scripts にパラメータを渡す仕組みのようである。

これらの性質や用途について、簡単にまとめてみる。

npm_package_config

package.json の config 欄に解説されている。

A “config” object can be used to set configuration parameters used in package scripts that persist across upgrades. For instance, if a package had the following:

{ "name" : "foo"
, "config" : { "port" : "8080" } }

and then had a “start” command that then referenced the npm_package_config_port environment variable, then the user could override that by doing npm config set foo:port 8001.

package.json の config プロパティに key/value を定義することで npm-scripts 内へ npm_package_config_key と展開する機能。

公式資料では言及されていないのだが、npm-scripts は Shell になるため、macOS などの bash 系は $npm_package_config_key、Windows のコマンドプロンプトや PowerShell では %npm_package_config_key% のように定義する。

Node プログラム上からは process.env.npm_package_config_key として参照可能。

私の作成した cross-conf-env では、これらと修飾文字なしの npm_package_config をサポートしており、混在させても展開するところが特徴。つまり Shell を問わず好きな書式で記述できるようにしてある。

この機能を利用すると npm-scripts 内のパラメータを直値から変数にできる。npm-scripts 間で重複する値があるとか、頻繁に更新される値があるなら変数化して config プロパティ側の編集だけで済ませられるようにしておくと便利だ。

実例は akabekobeko/examples-electron を参照のこと。このリポジトリは複数の Electron プロジェクトを管理しているが、これらの npm-scripts は共通化され config プロパティでアプリ名などを分岐している。

npm_config

config に解説されている。この資料は npm_package_config に対しても言及あり。

npm_package_config と異なり、こちらは npm run される時の引数として渡されたパラメータを展開する。例えば

{
  "scripts": {
    "task": "echo npm_config_foo npm_config_bar"
  }
}

のように npm-scripts を定義して、以下のようにパラメータを渡す。

$ npm run task --foo=Foo --bar=Bar

Foo Bar

npm run されるスクリプトのオプションに --key=value を指定すると、スクリプト内の npm_config_key に展開される。Node プログラム上からは process.env.npm_config_key として参照可能。

package.json 外からパラメータを渡すとか、npm-scripts を多段実行する時に npm_package_config 代わりにするとかで役立つのかもしれない。本記事のきっかけとなった npm-run-all は npm-scripts を同期・非同期で多段実行するための npm なので、これに対応する必要があったのだろう。

npm_config が展開される側は Shell なので前述のとおり にプラットフォームごとの修飾文字が必要。cross-conf-env は v1.0.6 で対応した。

まとめ

私はプロジェクトに関する情報をなるべく package.json へ静的に定義したいので、npm_package_config のほうが好みだ。しかしこちらは config の定義が必要なうえ記述も長い。よって npm-scripts の多段実行は必要になるけれど、より短い npm_config も便利な場面があるかもしれない。

package.json で確定不能な設定については npm_config に頼らざるを得ない。npm_scripts を実行するのは主に開発者なので、このようなケースはあまり考えられないのだが、npm から取得できない環境情報なんかを渡すときによいのだろうか。

ほぼ共通の設定で一部だけ異なる npm-scripts があるときに便利かもしれない。そういえば Browserify の require オプションでモジュールを外部公開するを書いた後に業務で

  • 共通処理を定義した main.js
  • サブフォルダ単位で個別のデータを定義した data.js
  • HTML 上で main.js と組み合わせる data.js を変更することで、ページ内容が切り替えられる

という感じの Web サイトをビルドする機会があって、サブフォルダが増えるたびにその名前だけ変更した npm-scripts を追加する運用を考えていた。

しかしこれは面倒なので npm_config によりフォルダ名を部分展開するほうがスマートな気がする。複数同時に生成するとしても、npm-scripts の多段実行を前提とすれば package.json で完結できる。

記事のまとめに軽く考察でも、と雑に書いていたら業務の問題がひとつ解決してしまった。

npm-scripts で Web フロントエンド開発を管理する

$
0
0

gulp なしの Web フロントエンド開発から 1 年あまり。その間、特に問題もなく npm-scripts で Web フロントエンド開発を管理できているので、この間に得られた運用知見や所感などをまとめてみる。

もくじ

npm-scrips とは?

最近の Web フロントエンド開発では AltJS/AltCSSのビルドやリリース用イメージ作成などに Node.js + npm を利用することが一般化してきている。そのためプロジェクトは package.json で管理することになる。

package.json の提供する代表的な機能として

  • プロジェクト情報の定義
    • プロジェクトの成果物を npm として配布するための情報
    • プロジェクト名、バージョン、作者などのメタデータを定義する
  • 依存モジュール管理
    • プロジェクトが依存する npm とバージョンを管理する
    • この情報へ基づき npm install コマンドにより npm を一括で導入・更新できる
  • タスク管理
    • ビルドやユニットテストの実行などをタスクとして定義できる
    • タスクは Shell スクリプトとして記述
    • 定義されたタスクは Teminal から npm run TASKNAME コマンドで実行可能

などがある。これらのうちタスク管理機能を npm-scripts という。例えば

{
  "scripts": {
    "start": "npm run watch",
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
    "app": "electron --debug=5858 src/"
  }
}

のように定義する。このようにタスクを定義して実行する仕組みをタスクランナーと呼ぶこともある。npm-scripts 以外だと

あたりが普及している。いまは Grunt が廃れ gulp が全盛、webpack は単体もしくは gulp と組み合わせて使われることが多い感じ。

なぜ npm-scripts か?

タスクランナーとして gulp や webpack があるのに npm-scripts を選ぶ理由としては

などで指摘されている問題を避けるため。特にプラグインの寿命は実際に遭遇したこともあり、かなり問題視している。次項で解説するが、npm-scripts の扱いにくさを解決する npm によって私の用途では実用十分になったというのもある。

その他、

  • 基本的に package.json だけで設定と処理が完結する
  • タスクランナー独自のルールを学習しなくて済む

などが気に入って採用している。

npm-scripts の問題とその対応

よいことずくめに感じられる npm-scripts だが、問題もある。

  • 可読性
    • Node として処理を記述する gulp などに比べると、単一行 Shell スクリプトは読みにくい
    • CLI オプションを変更するとき、対象とする位置を見つけにくい
    • 前向きに評価するなら、必要な情報が一行に集約されているとも言えるのだが…
  • 環境依存
    • Shell スクリプトなので実行環境によっては使えない機能がある
    • macOS や Linux などの UNIX 系と Windows は異なる
    • 複数コマンドの連結方法などは Shell 依存である
  • 変数
    • 同じ意味と値を複数箇所へ指定する場合、変数がほしくなる
    • 一応、標準で npm_package_confignpm_config が提供されている
    • ただし、これらの参照記法は Shell に依存する
    • そのためクロスプラットフォームに記述できない

まず可読性だが、これは仕方ない。どうしても CLI に馴染めないなら、タスク処理を Node モジュールとして用意してから

{
  "scripts": {
    "build": "node ./scripts/build.js"
  }
}

のように Node として実行する方法もある。package.json で完結しなくなるけど、特定のタスクランナーに依存することを避けられるメリットは残る。また、Node モジュールなので残りの問題も同時に解決される。

npm-scripts だけでゆく場合は環境依存変数を緩和してくれる npm を利用するとよい。

npm-scripts で使える便利モジュールたち – Qiita が参考になる。この記事と被るものもあるが、私の利用している npm を以下に紹介する。

npm-run-all

npm-run-all は npm-scripts に定義された複数のタスクをまとめて実行してくれる。実行形式も同期・非同期から選べる。厳密な順番が必要なら同期、そうでないなら非同期にするとよい。

例えば Web アプリのリリース用イメージを作成する場合、

  1. イメージ作成用フォルダ dist を用意
  2. HTML や画像などの静的ファイルを dist にコピー
  3. AltJS をコンパイルして dist に出力
  4. AltCSS をコンパイルして dist に出力

のような感じで処理することになるだろう。これらの内、1 と 2 は依存していて 3 以降は 1 より後であればよい。これを踏まえて npm-run-all を利用すると

{
  "scripts": {
    "release:clean": "リリース用フォルダの準備",
    "release:copy": "静的ファイルをコピー",
    "release:js": "JavaScript をリリース用にコンパイル",
    "release:css": "CSS をリリース用にコンパイル",
    "release": "npm-run-all -s release:clean release:copy -p release:js release:css"
  }
}

のような感じになる。-s に続けて記述したものは同期、-p なら非同期に実行される。

Shell スクリプトでコマンドを連結する場合 UNIX 系と Windows で記法が異なるけれど、npm-run-all はそれ自体が連結を担当してくれるので、記法の問題も解決してくれる。

cpx、mkdirp、rimraf

これらはファイルとフォルダ操作系の npm である。

cpx はフォルダ構造を維持したままファイルをコピーするためのツール。gulp だと標準で提供される機能だが、npm-scripts の場合は Shell 依存の cp コマンドなどを利用しないと実現できないので、クロスプラットフォームを目指すなら cpx が役にたつ。

特定の拡張子を持つものをコピーするなら

$ cpx ./src/**/{*.js,*.css} ./dist

という感じで指定する。逆に特定の拡張子を除外したコピーの場合は

$ cpx ./src/**/!(*.js|*.css) ./dist

のようにする。工夫すればワンライナーでも実用十分なコピー処理を書ける。

mkdirp は指定された階層構造も込みでフォルダを作成してくれる。便利だが cpx はコピー先フォルダも作成してくれるので最近は使っていない。cpx の対象とならないフォルダが必要になったら利用するかも。

$ mkdirp ./dist/js ./dist/css

rimraf はフォルダを削除する。UNIX 系の rm -rf に相当し、中身の入っているフォルダも削除可能。

$ rimraf ./dist"

リリース用タスクを繰り返し実行する時、出力先フォルダに前回の結果が残っていると新旧ファイルが混ざって問題になる。これを防ぐため、先に rimraf でフォルダを消してから cpx や mkdirp を実行することでフォルダがクリーンなことを保証できる。

cross-conf-env

npm-scritps で変数を利用するためには npm_package_confignpm_config を利用するのだが、これらを参照するときの記法は実行環境に依存する。cross-conf-env はこれをクロスプラットフォームに記述可能とする。

詳しくは以下を参照のこと。

作者は私。Electron アプリ開発時、npm-scripts を複数プロジェクトで流用したくなって作成した。akabekobeko/examples-electron ではビルドに使用する Electron のバージョンやアプリ名などを変数化している。

package.json のルートに定義された version フィールドなんかも参照できるので、リリース用イメージを ZIP するときのファイル名に version を挿入なんてことも可能。akabekobeko/redmine-theme-minimalflat2 でそのようなタスクを組んでいる。

npm-scripts まめちしき

npm-scripts を運用するうえで知っておくと便利な知識をまとめる。

npm run と既定タスク

npm-scripts に定義されたタスクを CLI から呼び出す場合は

$ npm run task

とする。npm-scripts 内から別タスクを呼び出すときも同様に

{
  "scripts": {
    "task:A": "command -option",
    "task:B": "npm run task:A"
  }
}

とすればよい。これを利用するとタスクからタスクを再利用できる。starttest のように標準で意味付けされたものは npm runrun を省略して npm start のように実行可能。詳しくは scripts を参照のこと。

npm のパス

npm-scripts について解説している記事をみると、たまに

{
  "scripts": {
    "task": "$(npm bin)/command -option"
  }
}

としていたりする。これはプロジェクトのローカルにインストールされた npm を直に呼び出すとき node_modules/.bin を表す。npm-scripts であれば dependencies に登録された npm のパスが通っているので、この記法は不要。

{
  "scripts": {
    "task": "command -option"
  }
}

直に npm が CLI として公開している名前を指定するだけで呼び出せる。なお CLI 名は必ずしも npm の名称と一致するわけではない。例えば uglify-js の CLI は uglifyjs だし npm-run-all のように複数の CLI 名を持つこともある。

そのため必ず npm の README や docs から CLI リファレンスを調べる習慣をつけよう。

タスク内の引用符

npm の CLI オプションで引用符が必要になるとする。package.json は JSON で npm-scripts はフィールドの文字列値となるため、引用符がダブル クォーテーションならバックスラッシュでエスケープする。

{
  "scripts": {
    "task": "command --opt=\"options\""
  }
}

UNIX 系であればエスケープせずにシングル クォーテーションでも動作するが、Windows 環境だとエラーになるので注意する。どちらを採用しても動作は変わらないので、より汎用なエスケープをオススメする。

実践!Web フロントエンド環境構築 2016/10 版

実際に npm-scripts を利用した Web フロントエンド開発用のタスク例をまとめる。内容としては本記事の冒頭にあげた「gulp なしの Web フロントエンド開発」の 2016/10 版となる。

完全なプロジェクトの構成は examples-web-app/front-end-starter を参照のこと。

Babel などの仕組みが変更されたときや気が向いたときに npm を最新にするなどして、なるべく現代的であるように運用している。特定の View 系ライブラリや Flux などの依存も避けているため、本記事の知識で理解可能な作りになっているはず。

設計方針

はじめに環境の設計方針をまとめる。

  • 環境はプロジェクトのローカルで完結させる
    • 基本的に最新 Node.js、package.json、ローカル npm だけ使用する
    • 例えば Node 以外で「〜を入れて」という作業は不要
  • JavaScript コンパイルとファイル監視に対応する
    • AltJS には Babel を採用
    • latest プラグインにより常に最新の ECMAScript でコーディング可能
  • CSS コンパイルとファイル監視に対応する
    • AltCSS には Stylus を採用
  • ユニット テストに対応する
    • mocha と power-assert を採用
    • テスト自体も最新の ECMAScript で記述可能とする
  • コード ドキュメントに対応する
    • ESDoc を採用
  • クロスプラットフォーム対応
    • UNIX 系と Windows 環境で動作する
    • 前述の npm-scripts 向け npm 群により実現

AltJS だけ tsify にするとか webpack 管理にしたり、AltCSS を SCSS や PostCSS に変更してもよい。npm-scripts のタスクとして呼び出せるならば、これらは可換である。

プロジェクトのファイル構成と npm-scripts の全体像

プロジェクトのファイル構成は以下となる。ライセンス情報や README などは必須ではないため除外している。

.
├── esdoc.json
├── package.json
├── dist/
├── node_modules/
├── src/
│   ├── assets/
│   ├── js/
│   └── stylus/
└── test/

各ファイル、フォルダの役割をまとめる。

名前 内容
esdoc.json ESDoc 用の設定ファイル。コード ドキュメントを出力するために必要。
package.json プロジェクト情報や開発用タスクを管理するためのファイル。
dist/ リリース用イメージの出力先となるフォルダ。デプロイ対象となる。
node_modules/ インストールされた npm を格納するフォルダ。
src/ 開発用リソースを格納するフォルダ。
src/assets/ HTML、画像、Web Fonts などの静的リソースを格納するフォルダ。
src/js/ JavaScript 関連を格納するフォルダ。コンパイル結果は src/assets/ または dist/ に出力する。
src/stylus/ Stylus 関連を格納するフォルダ。コンパイル結果は src/assets/ または dist/ に出力する。
test/ ユニット テスト関連を格納するフォルダ。

静的リソースを src/ 直下ではなく src/assets/ としているのはリリース用イメージを生成するときのコピー指定を簡略化するため。このあたりは後ほど詳しく解説する。

以降では npm-scripts や設定を小分けに解説するが、それだと分かりにくいかもしれない。よって nameversion などの基本情報を除く、Web フロントエンド開発に関わるものの全体像も掲載しておく。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "browserify": {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
    "start": "npm run watch",
    "esdoc": "esdoc -c esdoc.json",
    "build:css": "stylus -c --include-css ./src/stylus/App.styl -o ./src/assets/bundle.css -m --sourcemap-base ../stylus",
    "build:js": "browserify ./src/js/App.js -d | exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js",
    "build": "npm-run-all -p build:css build:js",
    "watch:css": "stylus -c -w --include-css ./src/stylus/App.styl -o ./src/assets/bundle.css -m --sourcemap-base ../stylus",
    "watch:js": "watchify ./src/js/App.js -v -o \"exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js\" -d",
    "watch:server": "browser-sync start --server ./ --startPath src/assets/",
    "watch": "npm-run-all -p watch:css watch:js watch:server",
    "release:css": "stylus -c --include-css ./src/stylus/App.styl -o ./dist/bundle.css",
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs -c warnings=false -m > ./dist/bundle.js",
    "release:clean": "rimraf ./dist",
    "release:copy": "cpx \"./src/assets/**/!(*.js|*.css|*.map)\" ./dist",
    "release": "npm-run-all -s release:clean release:copy -p release:css release:js"
  },
  "dependencies": {
    "normalize.css": "^5.0.0"
  },
  "devDependencies": {
    "babel-preset-latest": "^6.16.0",
    "babel-preset-power-assert": "^1.0.0",
    "babel-register": "^6.16.3",
    "babelify": "^7.3.0",
    "browser-sync": "^2.17.0",
    "browserify": "^13.1.0",
    "cpx": "^1.5.0",
    "cross-env": "^3.1.1",
    "esdoc": "^0.4.8",
    "exorcist": "^0.4.0",
    "mocha": "^3.1.0",
    "npm-run-all": "^3.1.0",
    "power-assert": "^1.4.1",
    "rimraf": "^2.5.4",
    "stylus": "^0.54.5",
    "uglify-js": "^2.7.3",
    "watchify": "^3.7.0"
  }
}

dependenciesdevDependencies は 2016/10/7 時点で最新のもの。

JavaScript コンパイルとファイル監視

JavaScript は ES2015 以降の最新規格で記述可能して ES5 に変換する。

2016/10 時点でも ES2015 に 100% 対応した Web ブラウザは WebKit とそれを使用する Safari 10 ぐらいである。そのため ES2015 であってもしばらくは変換が必要であり、以降の規格も考慮すると今後も変換は前提となるだろう。

コンパイルには npm-scripts だけでなく、ES5 変換で使用する Babel や複数 JavaScript を bundle ( 結合 ) する Browserify の設定も必要。Babel の設定は .babelrc というファイルへ記述するのが一般的である。私は設定を package.json に集約するため、意図的に babel フィールドで指定している。

Browserify は変換に噛ませるプラグイン指定を transform にくくり出せる程度なので、npm-scripts 側へ -t [ babelify ] と CLI オプションで指定するほうがよいかもしれない。タスクが長くなってもよいなら CLI オプションにしよう。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "browserify": {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "build:js": "browserify ./src/js/App.js -d | exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js",
    "watch:js": "watchify ./src/js/App.js -v -o \"exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js\" -d",
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs -c warnings=false -m > ./dist/bundle.js",
  }
}

各タスクの処理内容を解説する。

タスク 処理
build:js 開発用に JavaScript をコンパイルして Source Maps と一緒に src/assets/ へ JavaScript ファイルを出力する。
watch:js 変更監視つきで JavaScript をコンパイルして Source Maps 一緒に src/assets/ へ JavaScript ファイルを出力する。
release:js リリース用に JavaScript をコンパイルして dist/ へ JavaScript ファイルを出力する。

必要な npm をまとめる。

npm 機能
browserify モジュールとして定義された複数のJavaScript 依存を解決して単一ファイルへ bundle する。プラグインを指定することで bundle 前に AltJS の変換も実行可能。
watchify Browserify のファイル監視 & 差分コンパイル版。
babelify ES2015 以降の機能を使用した JavaScript を ES5 に変換する Babel 公式の Browserify 用プラグイン。
babel-preset-latest ES2015 以降の規格に準じた変換用 Babel プリセットをまとめたもの。詳しくは Latest preset を参照のこと。
cross-env リリース用コンパイル時に Node の環境変数へ NODE_ENV=production を追加して、これを判定しているデバッグ用コードを削除するために使用。
exorcist 開発用に JavaScript を変換する際、Web ブラウザの開発者ツールから元コードでデバッグするための Source Maps ファイルを生成する。Browserify/babelify の変換結果をコマンドライン連結で受け取り Source Maps と JavaScript ファイルを出力する。
uglify-js リリース用コンパイル時、コード圧縮、使用されていない変数や到達不能コードの削除、引数や関数名の短縮などを実行する。これも exorcist と同様にコマンドライン連結で受け取った JavaScript を加工してファイルに出力する。

タスクとしては参照しないが IE11 環境で Promise などを利用するなら babel-polyfill も必要。これは言語機能ではなく API なので latest にも含まれない。そのため個別に追加してコード全体の冒頭へ import することになる。

Browserify で webpack のように複数の JavaScript を生成したい場合は Browserify の require オプションでモジュールを外部公開するを参照のこと。npm-scripts のタスクも生成したい数だけ定義して、まとめて呼び出せばよい。

CSS コンパイルとファイル監視

CSS は Stylus で書いて CSS3 に変換。

{
  "scripts": {
    "build:css": "stylus -c --include-css ./src/stylus/App.styl -o ./src/assets/bundle.css -m --sourcemap-base ../stylus",
    "watch:css": "stylus -c -w --include-css ./src/stylus/App.styl -o ./src/assets/bundle.css -m --sourcemap-base ../stylus",
    "release:css": "stylus -c --include-css ./src/stylus/App.styl -o ./dist/bundle.css"
  }
}

各タスクの処理内容を解説する。

タスク 処理
build:css 開発用に Stylus ファイルをコンパイルして Source Maps と一緒に src/assets/ へ CSS ファイルを出力する。 
watch:css 開発用に Stylus ファイル変更を監視しながら Source Maps と一緒に src/assets/ へ CSS ファイルを出力する。
release:css リリース用に Stylus ファイルをコンパイルして Source Maps と一緒に src/assets/ へ CSS ファイルを出力する。

必要な npm をまとめる。

npm 機能
stylus Stylus ファイルを CSS にコンパイル。Sourcde Maps 生成、圧縮、ファイル監視機能も搭載している。

最近の AltCSS では JavaScript における Babel のように将来標準を先取りする目的で postcss/postcss が台頭してきており、その CLI 版である postcss-cli も Stylus のように単体で開発に必要な機能を網羅している。そのため将来は PostCSS へ移行するかもしれない。

Web サーバー起動

Web フロントエンド部分を動作確認するとき、ローカル ファイルを直に Web ブラウザで表示するとセキュリティ上の問題を引き起こすことがある。これを防ぐため Chrome ではローカル ページ上では Ajax や Web Storage などの利用を抑止する対策が取られている。

しかし開発環境であることを理解して表示する分には、この制限は不便である。そのためローカルに簡易 Web サーバーを起動して Web ページをホストさせる。

{
  "scripts": {
    "watch:server": "browser-sync start --server ./ --startPath src/assets/"
  }
}

各タスクの処理内容を解説する。

タスク 処理
watch:server 指定されたフォルダをルートにして Web サーバー起動、OS 標準の Web ブラウザで表示する。この例ではプロジェクトのルートを指定、表示する初期ページは src/assets/ 内の index.html になる。

必要な npm をまとめる。

npm 機能
browser-sync Web サーバーと Web ブラウザ表示を担当。

browser-sync は非常に多機能。npm-scritps から利用するための CLI については Browsersync Command Line Usage を参照のこと。

特に指定されたファイルの変更を検出して Web ブラウザを自動更新する機能は便利だ。しかし私は変更前の状態を確認しつつ任意のタイミングで手動更新するほうが好みなので使用していない。

自動更新を有効にしたい場合は browser-sync の CLI オプションに reload --files=\"src/**/*\" を追加すれば src/ 配下のファイルが更新されるたびに Web ブラウザに読み込まれたページを自動更新してくれる。

browser-sync の CLI を実行すると Terminal に localhost と IP アドレスの 2 種類、URL が表示される。実行マシンの Web ブラウザには前者が表示される。同一ネットワーク上の別 PC やモバイル端末から動作確認したいなら後者へアクセスすればよい。

レスポンシブデザインを試すだけなら PC 上でもよいが、タッチ操作なども含めた実際の操作感はモバイル端末の実機でチェックしたほうがよい。またオフィス内で他社に Web ページを公開するときもにも、この機能は便利だ。

ユニット テスト

ユニット テストには mocha と power-assert を採用。テストも最新の ECMASCript で記述可能とする。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js"
  }
}

テスト コードは test/ 配下に置き、テスト対象となる機能を含む JavaScript ファイルの拡張子を .test.js に変えたものとする。このようにすることで

  • テスト対象とテストの対応が分かりやすい
  • Atom などのエディタでテスト対象とテストを並べても名前が衝突しない
  • test/ 配下にテストを含まない補助コードを置いても、それらとテストを区別できる

というメリットがある。

Babel を使用するので .babelrc か package.json の babel フィールドに設定が必要。これは JavaScript コンパイルと共有されるので latest による変換は共通、power-assert は env フィールドで開発時のみ有効にしておく。

こうするとリリース時のコンパイルで NODE_ENV=production が指定されていたら power-assert 処理を除外してくれる。Web フロントエンド開発なのでアプリ側コードに Node の assert が露出することはないけど安全のため指定しておいたほうがよい。

npm を自作するとき、この設定を流用できる利点もある。npm は Node 環境で実行されるため普通に assert を呼べるので。

各タスクの処理内容を解説する。

タスク 処理
test ユニット テストを実行する。

必要な npm をまとめる。

npm 機能
mocha テスト コードを実行するための環境。
babel-preset-latest ES2015 以降の規格に準じた変換用 Babel プリセットをまとめたもの。
babel-preset-power-assert 現在の Babel で power-assert を利用するための Babel prest。
babel-register Babel の JavaScript 変換処理を捕捉して間に処理をはさみこむためのツール。power-assert による Node 標準 assert の置換に使用。

Babel 本体はどこへ?と疑問に思われるかもしれないが、アプリ開発で使用する babelify の依存で確実にインストールされるため問題ない。

ユニット テストは基本的に DOM の絡まないものを対象とする。もし DOM 操作を含むコードをテストしたいなら jsdom などをオプションで追加してもよい。

mocha はひとつのテストにつき標準で 2 秒の制限時間を設けている。jsdom は初期化に時間がかかるため、この制限に引っかかって警告される可能性がある。その場合は mocha の CLI オプションへ --timeout 50000 のように大きめの制限時間を指定することで回避できる。時間の単位はミリ秒なので、この例だと 5 秒になる。

注意点がある。power-assert の置換対象は assert なので

import assert from 'assert';

ならよいが

import Assert from 'assert'

にすると ‘Assert’ は置換されず、Node 標準の assert になってしまう。そのため import または require 先は必ず assert にすること。

コードド キュメント

最新の ECMAScript で記述されたアプリ実装コードとコメントからコード ドキュメントを生成する。その際、コメント記述のカバレッジや機能とユニット テストの関連付けもおこないたいので ESDoc を利用する。

{
  "scripts": {
    "esdoc": "esdoc -c esdoc.json"
  }
}

コード ドキュメントがあると第三者に設計や実装を説明する時のよい参考資料になる。また、ドキュメント生成を前提とすることで読まれるコメントを書こうと意識するようにもなるだろう。

コードに最も近い場所へ記述される仕様ともいえる。あるクラスや関数が実現したい機能やインターフェースについて書くようにすると、それは自然と現状にそった自己言及的なドキュメントになるはず。

各タスクの処理内容を解説する。

タスク 処理
esdoc アプリの実装コードとテストを解析して HTML の資料を生成する。出力先は esdoc/

必要な npm をまとめる。

npm 機能
esdoc コード ドキュメント生成ツール。

ESDoc の設定は CLI オプションや package.json ではなく esdoc.json に記述する。このファイルはプロジェクト全般の設定に属するため、package.json と同じ階層に保存するとよい。

設定内容については ESDoc – A Documentation Generator For JavaScript(ES6) を参照のこと。

{
  "source": "./src/js",
  "destination": "./esdoc",
  "test": {
    "type": "mocha",
    "source": "./test"
  }
}

ESDoc が出力する HTML はこんな感じになる。ESDoc Hosting Service というサービス ( beta 版 ) も運用されている。ここに登録されたプロジェクトの GitHub 上のリポジトリから esdoc.json を検出すると HTML 生成して Web に公開してくれる。

README.md などにバッジを貼り付けてホストされた HTML へリンクできるので、GitHub でリポジトリ運用している OSS なプロジェクトなら採用をオススメする。

複数のタスクを組み合わせる

JavaScript や CSS 用のタスクを定義したら、これらを組み合わせたくなる。例えば開発時はファイルの自動監視とコンパイルを走らせつつ、動作確認用に Web サーバーを起動したい。このような連結は前述の npm-run-all を利用することでクロスプラットフォームにできる。

{
  "scripts": {
    "build": "npm-run-all -p build:css build:js",
    "watch": "npm-run-all -p watch:css watch:js watch:server",
    "release": "npm-run-all -s release:clean release:copy -p release:css release:js"
  }
}

npm-run-all を前提に完結した小さなタスクを定義してから組み合わせて実行すると管理しやすくなる。組み合わせられるものはタスク名で分類を明示するとよい。私は

タスク 処理
build:XXXX 開発ビルド。XXXX には js や css など処理対象を付与する。
build build:XXXX を組み合わせて実行するタスク。
watch:XXXX ファイル変更の監視つき開発ビルドや Web サーバー起動など。XXXX には js や css など処理対象を付与する。
watch watch:XXXX を組み合わせて実行するタスク。
release:XXXX リリース用ビルド。XXXX には js や css など処理対象を付与する。
release release:XXXX を組み合わせて実行するタスク。

という感じで命名している。

リリース用イメージ生成

Web フロントエンド開発の成果物をリリースするためのイメージを生成する。

{
  "scripts": {
    "release:clean": "rimraf ./dist",
    "release:copy": "cpx \"./src/assets/**/!(*.js|*.css|*.map)\" ./dist",
    "release": "npm-run-all -s release:clean release:copy -p release:css release:js"
  }
}

release:jsrelease:css はそれぞれの項に解説したので割愛。各タスクの処理内容を解説する。

タスク 処理
release:clean リリース用イメージの生成先となる dist/ フォルダを削除。前回の生成結果が残っていないことを保証する。
release:copy src/assets/ 内の静的リリースを対象にフォルダ階層を維持して dist/ にコピー。
release リリース用イメージ生成タスクを連結実行。生成先フォルダの削除と静的リソースのコピーを直列実行することで基本的な dist/ を準備、その後にここへリリース用の JavaScript と CSS を出力する。

必要な npm をまとめる。

npm 機能
rimraf 指定されたフォルダを中身ごと削除する。
cpx glob 形式で指定された条件にあったファイルやフォルダを、所定のフォルダへコピーする。
npm-run-all npm-scripts の複数タスクを連結して直列または並列実行する。

release:copy について補足する。「gulp なしの〜」を書いたときのコピー関連処理は

{
  "scripts": {
    "release:mkdir": "mkdirp ./dist && npm run release:clean && mkdirp ./dist",
    "release:copyfiles": "copyfiles -f ./src/*.html ./dist",
    "release:copydirs": "ncp ./src/fonts ./dist/fonts",
    "release:copy": "npm run release:copyfiles && npm run release:copydirs",
  }
}

のようになっていて非常に複雑だった。あの記事のはてブでもやり過ぎ感あると指摘されていた。これを解決するために

  • 静的リソースは src/ ではなく src/assets/ に置く
    • 静的リソースと動的リソース系の src/js/src/stylus/ を区別しやすくなる
    • src/assets/ に格納されているものは、そのままリリース用にコピー可能とする
    • src/assets/ は開発用ビルドで生成された JavaScript、CSS、Source Maps が動作確認のため出力される
  • cpx でブラック リスト式コピー
    • src/assets/ に出力された動的リリースをブラック リスト化して、それ以外をコピーする
    • 動的リソースはそれらのリリース用ビルドで dist/ へ出力するためコピー不要
    • 不要ファイルのブラック リスト化は glob の否定機能を利用する
    • cpx はコピー先フォルダを生成してくれるので mkdir も不要になる

上記で対応した。npm-scripts が大幅に簡素化され、運用としても静的リリースの置き場所が明確なので管理しやすい。

タスクを利用した開発スタイル

はじめにプロジェクトを構築する。Terminal は Windows の場合 cmd.exePowerShell などになる。

  1. プロジェクト開発フォルダを用意
  2. 開発フォルダのルートに package.json を作成、または既存のものを流用
  3. Terminal から npm i コマンドを実行して npm をインストール
  4. src/ 内に JavaScript、CSS、HTML を実装してゆく
  5. あわせて test/ にテストを書く

開発時に実行すること。

  1. Terminal から npm start コマンドを実行、JavaScript と CSS のファイル監視と Web サーバー起動
    1. この記事の設定なら http://localhost:3000/src/assets/ が Web ブラウザに表示される
    2. Terminal 上に IP アドレス版の URL も表示される
    3. 同一ネットワーク上の PC やスマートフォンで表示する場合は IP アドレス版の URL で OK
  2. JavaScript や Stylus ファイルを編集して保存
    1. Babel や Stylus の自動コンパイルが実行される
    2. Terminal にコンパイル終了が表示されるので Web ブラウザをリロードするときの目安になる
    3. 必要ならテストを書いて別 Terminal または別タブから npm test でチェックする
  3. Web ブラウザをリロードして変更をチェック
    1. http://localhost:3000/src/assets/ をリロード
    2. browser-syncreload オプションで実行しているなら自動化される
  4. 2 〜 3 を繰り返し、終了したくなったら Ctrl + C で中断する

リリース時に実行すること。

  1. Terminal から npm run release コマンドを実行
  2. Terminal をチェックして全タスクが終了するまで見守る
  3. 全タスクが終了したら dist/ の中身をリリース

JenkinsTravis CI などと組み合わせてもよい。例えば全テストを通過したときだけリリース ( デプロイ ) するとか。

まとめ

ここ 1 年ほどの npm-scripts 運用知見をまとめてみた。

クロスプラットフォーム対応も済んでおり、この記事にまとめた内容とほぼ同等の構成で業務プロジェクトも運用している。静的リソースを src/assets/ に集約する設計はデザイナーや HTML コーダーにも好評であった。

Browserify、Babel、Stylus が存続する限り今回の構成で完成形と考えている。もしこれらがなくなったとしても依存は npm だけなので十分に可換である。例えば JavaScript だけ webpack、CSS を PostCSS というのもありだ。

最後に本記事で紹介した npm-run-allcpx の作者であり、npm-scripts 運用についてのアドバイスもいただいた mysticatea (Toru Nagashima) さんに感謝したい。いつもありがとうございます。


Redmine theme minimalflat2 v1.2.2 release

$
0
0

長らく放置していた minimalflat2 を v1.2.2 としてリリースした。

今回の変更点は以下。

大きな変更、というか方針転換として Redmine プラグイン対応がある。

テーマ側で特定のプラグインに対応すると、それらの更新に影響を受けるため避けてきた。しかし何件か PR で対応されたことから考えを改めた。

現在の方針としては

  • 最新の Redmine に対応していること
  • Vagrant の最新 Redmine box で動作確認できること
  • テーマとして対応が容易であること
  • GitHub issues にて要望されること

を満たしたものについては対応することにした。なお、この条件は意外に厳しい。

かの有名な Redmine CMS の対応を要望されたのだが、依存 gem の関係か bundle に失敗、そのまま Redmine を再起動したら 500 error になってしまった。ログを見るに Active Record の参照問題っぽいが、それはテーマ側で修正するものではなく、どうにもならない。

前に Redmine 運用の記事でも書いたが、Redmine プラグインは gem 依存が自身で完結しておらず、うまく解決されないと Redmine 本体がクラッシュする。

要望があっても私の環境で動作確認しないことには対応できないわけで、今のところ gem 未使用なプラグインとか Redmine 最新版に追従できてるものだけサポートという状態である。

放置していたものをリリースすることになったのはこの issueで修正したはずの問題が再報告されたため。

開発者としては commit/push 時点で終わったつもりになっていたけど、それを反映したものをリリースしなければ意味がない。半端に修正したまま放置というのはいかにも無責任である。

ということに気づいたのでリリースとあいなった。

あと、対応するバージョンを決めかねる issue は v.NEXT という Milestone を設定することにした。Redmine でも同じような運用をしているのだが、将来に見送るものはそうであることを明示しておくと issue の状態がわかりやすくなる。

NSString 連結を利用して heredoc 風に定数を記述する

$
0
0

これまで Objective-C による iOS アプリで FMDB を使用するとき、SQL 文は define directive で定義していた。

#define SQL_READ @"SELECT user_id, name, age FROM users WHERE 20 <= age;"

この方法による定数は pre-process で単純なコードに対する置換として動作する。そのため内容が構文エラーであっても置換された結果が構文として正しいなら許容される。C 言語や C++ ではこれを利用して関数名の一部を pre-process で書き換えるなどのハックが横行していたものだ。

define directive のもう一つの特徴として、複数定義された場合は後勝ちになる。例えば

#define SQL_READ @"SELECT user_id, name, age FROM users WHERE 20 <= age;"
#define SQL_READ @""

と定義した場合、後に定義されたものが置換対象となる。この動作は思わぬ事故を招くので ifdefifndef directive により定義状態で条件分岐するものだが、いかにも冗長である。

なお、このような定義を見つけたら Xcode 8.1 は

'SQL_READ' macro redefined

と警告してくれるのだが、それでも後勝ちの定数はそのまま使用されてしまう。これを防ぐためにコンパイラーの警告レベルをあげてエラーにする手もあるが、そうした運用による対策なしに標準でエラーにしたいところ。

また Xcode 4 時代に Cocoa が提供する標準型の定数書式が改善されて NSArray や NSDictionary などを定数化しやすくなった。これらと一緒に定義するとき、文字列だけ define directive なのは違和感がある。

というわけで前述の定数を

static NSString * const kSQLRead = @"SELECT user_id, name, age FROM users WHERE 20 <= age;";

と書き換える。グローバル定数にするなら .h で以下のように宣言し、

extern NSString * const kSQLRead";

.m で値を定義する。

NSString * const kSQLRead = @"SELECT user_id, name, age FROM users WHERE 20 <= age;";

ただし NSObject 系を定数として宣言する際はポインターの示すものに注意すること。この辺の話は objective c – "sending ‘const NSString *’ to parameter of type ‘NSString *’ discards qualifiers" warning – Stack Overflow の議論が分かりやすい。Win32 API でプログラミングしていた頃はこうした宣言で事故らないように typedef で抽象化していたが、Objective-C だとこの方法を見かけないので素で書いている。

さて NSString 定数を宣言したところまではよかったが、SQL 文のように長くて可読性を求められるものを単一行に定義するのはつらい。構文に基づいてインデントしたくなる。こうしたとき heredoc を使えると便利。

ただ、はたして Objective-C は heredoc をサポートしているのだろうか。C 言語と見なして back slash による連結を利用する手もあるかも?だけど。

というわけで調べてみたら How to split a string literal across multiple lines in C / Objective-C? – Stack Overflow を見つけた。ここには C 言語と Objective-C 独自の方法が紹介されている。できれば一次情報にあたりたかったのだけど Stack Overflow にもリンクはなく、 Objective-C の言語仕様の日本語訳を見ると

「コンパイラのディレクティブ」に、定数文字列を連結するための 言語サポートについて文書化しました。

こうという更新履歴はあるものの当該項目は見当たらず。残念。この記事を読まれた方で情報をお持ちの方はコメ欄や Twitter などで指摘していただけると助かります。

とはいえ紹介されている Objective-C の方法はちゃんとコンパイルできて期待どおり動く。試しに

static NSString * const kSQLRead = @""
"SELECT "
  "user_id, name, age "
"FROM "
  "users "
"WHERE 20 <= age;";

と定義した SQL 文が FMDatabase – executeQuery で動作することを確認できた
。NSLog に渡すと結合された結果がちゃんと出力される。

この機能により文字列の定義における自由度は格段に向上した。好きなようにインデントできて嬉しい。ただし

  • 改行位置へ改行コードが自動挿入されない
  • 変数の展開がない

ことから heredoc と呼ぶのは語弊があるだろう。そのため heredoc 風としておく。それともうひとつ注意点がある。この機能は単に複数の文字列を連結しているだけなので、

static NSString * const kSQLRead = @""
"SELECT"
  "user_id, name, age"
"FROM"
  "users"
"WHERE 20 <= age;";

のように各行末から空白を取り除いてしまうとそのまま

static NSString * const kSQLRead = @"SELECTuser_id, name, ageFROMusersWHERE 20 <= age;";

こんな感じに結合され、SQL 文として成立しなくなる。そのため結果を意識しながら定義すること。SQL 文のインデントを目的とするなら、

  • Syntax とそれにぶら下がる単位で分割
  • 各行の末尾に空白を入れる

とするのがよいだろう。空白の重複は無視されるので「行末へ空白を入れる」ぐらいの単純化されたルールでもよい。

そんなわけで今後 SQL 文を定義するときは heredoc 風で書くことにした。あと Objective-C で本物の heredoc がサポートされることを願っている。

CocoaPods を試す

$
0
0

これまで akabekobeko/Examples-iOS というリポジトリで iOS に関するサンプルを公開していたのだが、以下のルールで新リポジトリ移行することにした。

  • サンプルごとにリポジトリを分割
    • Examples-iOS/VideoPlayer は examples-ios-video-player のように分割
    • 更新管理しやすくなる
  • 外部ライブラリは CocoaPods.org で管理する
    • 静的に組み込むのではなく、パッケージ管理に委ねる
    • ついでに FMDB サンプルを書き直して examples に追加したい
  • 可能なら Objective-C と Swift のサンプルを同時公開したい
    • iOS 以外でも Swift の利用が進みそうなので、触れておきたい
    • 慣れ親しんだ Objective-C で作成したものと同じサンプルを Swift 移植することで、学びやすくなるのではないか?
    • ひとつのサンプルに objc/swift というサブ ディレクトリがあり、そこが Xcode プロジェクトになる感じ

iOS アプリ開発から 2 年ぐらい遠ざかっておりサンプルも古びている。いまでは動かないものもあるだろう。これは、それらの棚卸しも兼ねた試みでもある。

もう Examples-iOS を更新することはないけれど、これはそのまま残しておく。かつてはこう書いていたという記録にはなるだろう。ただし更新されないサンプルは検索ノイズになってしまうから開発が中止されたことや代替サンプルについてだけ README で言及する予定。

CocoaPods

FMDB の解説記事は 2011 年に書かれたものだが現在も結構なアクセスがある。しかし ARC すら使われていない時代のもので、しかも組み込まれている FMDB が古すぎることから、いつか更新しなければと考えていた。

また、現在の iOS ライブラリ管理には CocoaPods を使用するのが一般的だろう。パッケージ管理にしておけば、ライブラリの依存やバージョン更新も楽になる。というわけで CocoaPods に入門する。

まずは CocoaPods をインストール。と、ここでいきなりつまづいた。

$ sudo gem install cocoapods
...
ERROR:  While executing gem ... (Errno::EPERM)
    Operation not permitted - /usr/bin/xcodeproj

私の環境は macOS Sierra なので、rootless により /usr への書き込みが制限されている。これは sudo でもはねられる。CocoaPods 公式の Getting Started を読むと --user-install でユーザー単位にインストールして shell の .profile にパスを通す方法を紹介していたので、試しにこれを採用してみた。

しかしこの方法でインストールした CocoaPods で生成した Podfile を Git リポジトリに入れて複数マシンで開発していたところ、新規に clone したプロジェクトに対して pod install した後にビルドしたら Podfile.lock No such file or directory というエラーが出るようになった。この問題は

などに解説されている方法で直るそうだが、私の環境ではダメだった。そもそも pod install の内容をみると Abort trap: 6 と出ている。これについて調べたら

にて CocoaPods の再インストールが勧められている。どうやら環境が壊れているらしい。そこでまずはアンインストール。

$ sudo gem uninstall cocoapods
Password:

Select gem to uninstall:
 1. cocoapods-0.34.2
 2. cocoapods-0.39.0
 3. All versions
> 3
Successfully uninstalled cocoapods-0.34.2
Remove executables:
    pod, sandbox-pod

in addition to the gem? [Yn]  Y
Removing pod
Removing sandbox-pod
Successfully uninstalled cocoapods-0.39.0

gem が複数バージョンあるようなので All versions を選び、すべて消した。その後に

$ sudo gem install -n /usr/local/bin cocoapods
...中略...
11 gems installed

でインストール。再び --user-install で入れるか迷ったが permission の通る場所ならばよいわけだし、いちいち .bash_profile を編集するのは面倒なのでこれでゆく。ちゃんとインストールできたか確認。

$ pod --version
1.1.1
$ gem list
...中略...
cocoapods (1.1.1)
cocoapods-core (1.1.1, 0.39.0, 0.34.2)
cocoapods-deintegrate (1.0.1)
cocoapods-downloader (1.1.3, 1.1.2, 0.9.3, 0.7.2)
cocoapods-plugins (1.0.0, 0.4.2, 0.3.1)
cocoapods-search (1.0.0, 0.1.0)
cocoapods-stats (1.0.0, 0.6.2)
cocoapods-trunk (1.1.2, 1.1.1, 0.6.4, 0.3.0)
cocoapods-try (1.1.0, 0.5.1, 0.4.1)

最新版が入ってる。CocoaPods 系 gem も単一バージョンだけのようだ。最後に CocoaPods を初期化する。

$ pod setup
Setting up CocoaPods master repo

CocoaPods 1.2.0.beta.1 is available.
To update use: `sudo gem install cocoapods --pre`
[!] This is a test version we'd love you to try.

For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.2.0.beta.1

Setup completed

これで準備完了。

CocoaPods でライブラリをインストールする

ある Xcode プロジェクトに CocoaPods 経由でライブラリをインストールするための手順。CocoaPods が導入済みで pod setup まで完了していることを前提とする。

はじめに Terminal で Xcode プロジェクトのルートへ移動。これは 名.xcodeproj ファイルの置かれた場所になる。

次に Terminal から pod init を実行。これで Podfile が生成される。このファイルは YAML 形式となっており、Xcode プロジェクト単位で設定が階層化されている。Xcode プロジェクトを作成する際、テストを有効にした場合は Tests や UITests といったプロジェクトも生成されるので、以下のようになるはず。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'UsingFMDB-Objective-C' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

  # Pods for UsingFMDB-Objective-C
  pod 'FMDB/FTS'

  target 'UsingFMDB-Objective-CTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'UsingFMDB-Objective-CUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

メインとなるプロジェクトでライブラリを使用したいならルートに追加する。この例では FMDB/FTS を追加している。

Swift プロジェクトの Podfile では use_frameworks! のコメント アウトを解除すること。こうしないと Swift 用のライブラリがインストールされないので注意する。

設定を済ませた後に pod instal を実行すると

$ pod install
Analyzing dependencies
Downloading dependencies
Installing FMDB (2.6.2)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.

こんな感じでライブラリがインストールされる。このとき .xcodeproj と同じ階層に以下のファイルとフォルダが追加される。

File/Folder 内容
.xcworkspace プロジェクト間の参照関係を管理するファイル。
Podfile.lock ライブラリのバージョンや依存情報を管理するためのファイル。
Pods/ インストールされたライブラリの実体を格納するフォルダ。

以降の開発で Xcode から開くファイルは .xcworkspace になる。間違って xcodeproj を対象にするとライブラリを参照できずビルドが通らないので気をつけること。

プロジェクトを Git リポジトリで管理しているなら gitignore/Objective-C.gitignore を使用していることだろう。CocoaPods を使用する場合、この .gitignorePods/ がコメントアウトされているので以下のように解除しておく。

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
Pods/

このフォルダは動的に取得したライブラリを格納するのでリポジトリに含める必要はない。それ以外はコミットしておく。他の環境でプロジェクトと共に CocoaPods 関連を取得して pod install すれば同じ環境が整う。

CocoaPods でインストールしたライブラリの参照方法について。Objective-C の場合、

#import <FMDatabase.h>

のようにする。.xcworkspace を開いていれば、ライブラリの参照が通っているので Xcode エディタから補完されるはず。Swift の場合は

import FMDB

となる。なお、補完に出てこない場合は Xcode を開き直すとか、Product – Clean してから Product – Build すると直ることがある。

というか Swift はバックグラウンドで走る構文チェックが不安定で、エラーの原因を修正してもなかなか警告が消えないとか地味にストレスたまる。Swift 3 で言語の破壊的な変更は落ち着くそうだから、Xcode 9 あたりで Objective-C なみにサクサク開発できるようになるのかな、と期待している。

あとこれは愚痴なのだけど、Xcode や CocoaPods 関連は環境的な問題にしばしば遭遇する。CocoaPods は Xcode に後付でパッケージ管理を加えているので仕方のないことではあるが、プログラミングと異なり環境系の問題は操作基盤そのものなので原因を特定するのが難しい。

Stack Overflow などで対策を見つけても、それが起きる原理まで掘り下げていることは滅多にない。そのため指定された手順をなぞっても直らないとか、仕方ないから環境をクリーンにしてから再試行で直ったとかで釈然としない。こういうのを繰り返してるとだんだん堪忍袋が温まってくる。

あのプロプライエタリで有名だった Visual Studio ですら方針転換して NuGet を導入したのだから、Xcode もそろそろ公式に CocoaPods なりそれに類するパッケージ管理をサポートしてほしいものだ。

今後の予定

現在、FMDB サンプルを作成中。

Objective-C 版は完了したのでそれをベースに Swift 版へ処理を移植中。Swift 学習も兼ねているため小さなサンプルなのに苦戦している。

他の examples-ios-XXXX への移行は断続的におこなう。ひとつプロジェクトを作成するたびにそこで起きたことなどをブログに記録してゆく予定。

ESDoc の設定を package.json に定義する

$
0
0

これまで ESDoc の設定は esdoc.json に定義していたが昨年末にリリースされた v0.5.0 から他の形式もサポートされるようになった。CHANGELOG.md には

  • .esdoc.json in current directory
  • .esdoc.js in current directory
  • esdoc property in package.json

とある。これらの内、とくに嬉しいのは 3 番目の package.json。私はプロジェクト設定をこのファイルへ集約する派であり Babel や Browserify もそうしている。

実際に ESDoc 設定を package.json へ定義して npm-scripts から呼ぶ場合は以下のように記述する。

{
  "esdoc": {
    "source": "./src/js",
    "destination": "./esdoc",
    "test": {
      "type": "mocha",
      "source": "./test"
    }
  },
  "scripts": {
    "esdoc": "esdoc"
  }
}

既に esdoc.json を利用しているならその内容を package.jsonesdoc プロパティにコピペして元ファイルを削除すればよい。esdoc コマンドの引数を省略して実行すると自動的にそれを読んで処理してくれる。

試しに以下のプロジェクトへ設定を反映し、動作することを確認済み。

なんでも package.json にまとめると肥大化して見通しが悪くなるという意見もあるだろう。しかしこのファイルを直に編集する機会は滅多にないし、設定が分散するよりも集約したほうが個人的には便利だと思う。

10 行ぐらいに収まる設定ならそうしたい。例えば ESDoc や Babel などの設定は大抵、短いからそうする。逆に ESLint は長くなりがちだから分離しておきたい。

iOS で SQLite – FMDB の使い方 2017

$
0
0

2011 年に書いた iOS で SQLite – FMDB の使い方という記事へ現在も結構なアクセスがある。

しかし当時は ARC すらなくサンプルとして古すぎる。なにしろ 5 年も前だし。また最近 iOS アプリ開発に戻ってきたこともあり、2017 年度の開発環境を学ぶ題材としてサンプルを再実装してみた。プロジェクトは GitHub に公開してある。

サンプル再実装における考察などは以下にまとめる。

開発方針

再実装にあたり開発方針をまとめる。

  • サンプル プロジェクトの機能と UI は元記事の内容を踏襲
  • Objective-C と Swift の両方を実装
  • FMDB は CocoaPods で管理
  • ユニット テストを実装する

なるべく最新の Objective-C と Storyboard を使用するのは当然として、Swift 版も実装する。Swift は 2014 年に発表されてから破壊的な変更を繰り返してきたが、Swift 3 で一段落ついたと認識している。

Swift 3の開発の振り返りとSwift 4の計画が記されたメールの紹介 – Qiita と冒頭で紹介されているメーリング リストを読むに、Swift 4 では互換性が重視され Swift 1 〜 3 のような構文レベルの大変更は抑止されるだろう。というわけで今こそ Swift 入門のチャンスと判断した。

サンプルは Objective-C、Swift 共に同等の内容とする。馴染みある Objective-C から先に実装してそれを Swift へ移植。なるべく Swift 的に好ましい機能や記法を採用するがオブジェクトやメソッド定義の変更をともなうレベルの差分は控える。

FMDB のインストールは CocoaPods を採用。元記事では FMDB のソースをプロジェクトにコピーしていたが、この方法だとバージョン管理に難がある。パッケージ管理が利用可能ならそちらへ任せるほうがよい。

あわせて Xcode の提供するユニット テスト機能も試す。

FMDB とは?

iOS アプリ開発において、SQLite を扱いやすくするためのライブラリ。Apple 的には Core Data 推しであり Xcode の GUI からテーブル編集可能などの優遇措置がある。

だが Core Data は SQLite を基本的に隠蔽している。そのため他のプラットフォームで培った SQL 知見を活かすには素で操作したくなる。特に iOS/Android 両対応のアプリを開発する場合、DB 設計と SQL 文を共用したい場面もあるだろう。

しかし SQLite は C 言語で実装されているため、Objective-C や Swift から利用しようとすると API や接続状態の管理が実に厄介だ。FMDB はこの辺をわかりやすく面倒みてくれる。

要は JDBC とか Android でいう SQLiteDatabase、.NET の System.Data.SQLite みたいなものだ。クライアント言語によりそった簡易なデータベース接続と操作 API を提供してくれる。

CocoaPods と FMDB

CocoaPods による FMDB インストールの詳細は CocoaPods を試すにまとめたので、そちらを参照のこと。サンプル プロジェクトのリポジトリには Podfile を定義してあるので、これを clone してから

$ pod install

すれば FMDB をインストールできる。その後に *.xcworkspace ファイルを Xcode で開けば FMDB への参照がプロジェクトに設定された状態となっている。

Xcode で iOS アプリを開発する場合、現在は以下のパッケージ管理を利用できる。これらから CocoaPods を選んだ理由について。

CocoaPods は Objective-C と Swift に両対応している。

Carthage は CocoaPods よりも先に Swift 対応したことで注目を集めたシステムで、機能も簡素である。CocoaPods の不満点を解消するために生まれたらしい。

Swift Package Manager は Swift 用である。Swift は OSS 化されているため iOS アプリ以外の開発でも採用される可能性がある。そのため iOS や macOS とは独立したパッケージ管理となっているようだ。

これらのうち今回は Objective-C から利用可能で知見も多い CocoaPods を選んだ。CocoaPods は 2016/5 に v1.0 がリリースされ、Podfile まわりで互換問題もあったようだが現在は落ち着いている。つまり当面は安定するだろう。これも採用理由である。

CocoaPods でインストール可能な FMDB には複数の仕向けがある。ccgus/fmdbe の CocoaPods 欄から引用。

pod 'FMDB'
# pod 'FMDB/FTS'   # FMDB with FTS
# pod 'FMDB/standalone'   # FMDB with latest SQLite amalgamation source
# pod 'FMDB/standalone/FTS'   # FMDB with latest SQLite amalgamation source and FTS
# pod 'FMDB/SQLCipher'   # FMDB with SQLCipher

今回は FMDB/FTS を採用する。FTS というのは全文検索モジュールのこと。SQLite FTS3 and FTS4 Extensions に詳しい。特別なテーブル内の TEXTMATCH で高速に検索可能となる。

この機能はオプションなので不要ならば使わなくてもよい。おなじみの SQLite へ便利機能が追加された程度であり、普通に使う分には FTS を意識することはないだろう。

DAO と RAII

FMDB はデータベース接続と SQL 文を実行する API を提供するのだが、その知識をカプセル化するため DAO ( Data Access Object ) パターンを採用。FMDB API を利用する DAO クラスと、DAO インスタンスの生成を担当する Factory クラスを用意する。

FMDB におけるデータベースは FMDatabase - databaseWithPath などのメソッドから得られた FMDatabase インスタンスとなる。データベース接続する場合はこのインスタンスに対して open、接続を閉じるなら close メソッドを呼ぶ。

NSString   *path = @"データベース ファイルのパス";
FMDatabase *db = [FMDatabase databaseWithPath:path];
if ([db open]) {

  // ...データベース操作

  [db close];
}

データベース接続を安全に管理するため open/close を対応させる必要がある。簡単なのは対応をメソッドでカプセル化することだ。なにかデータベースを操作したくなったらメソッドを追加し、その中で open/close を完結させる。

しかしこの方法だと複数の操作を連続実行したい場合、その単位でカプセル化するか毎回 open/close することを覚悟してメソッドを複数実行することになる。元記事のサンプルではそうしていた。

今回はこれを避けるため、DAO クラスのインスタンス生成と破棄を open/close に対応させる。Objective-C は init/dealloc、Swift なら init/deinit で open/close を処理。いわゆる RAII ( Resource Acquisition Is Initialization ) 的な管理である。

Objective-C と Swift はインスタンス生成と破棄を明確にハンドリングできるため RAII と相性もよい。ARC ( Automatic Reference Counting ) を利用していても参照カウンター管理を自動化するだけである。インスタンス生成したスコープで参照が完結するなら、そこを抜けたときにカウンターがゼロとなり「インスタンス破棄 = データベース接続を閉じる」ことになる。

例えば以下のように DAO クラスを定義して

- (void)init:(FMDatabase *)db {
    if (!(db)) { return nil; }

    self = [super init];
    if (self) {
        self.db = db;
    }

    return self;
}

- (void)dealloc {
    [self.db close];
}

- (Book *)add:(NSString *)author title:(NSString *)title releaseDate:(NSDate *)releaseDate {
}

- (NSArray *)read {
}

DAO Factory クラスでは

- (BookDAO *)bookDAO {
    return [[BookDAO allo] init:[self connection]];
}

- (FMDatabase *)connection {
    FMDatabase* db = [FMDatabase databaseWithPath:self.dbFilePath];
    return ([db open] ? db : nil);
}

というようにデータベース接続を済ませた FMDatabase を渡してインスタンス生成する。このメソッドから返された DAO インスタンスはスコープを抜けるまで add や read を繰り返しても同じデータベース接続が使いまわされる。

- (void)sample {
    // データベースが open される
    BookDAO *dao = [self.daoFactory bookDAO];

    // DAO を使用したデータベース操作

    // DAO インスタンス破棄 & データベース接続を閉じる
}

イメージとしては上記のような感じ。

Objective-C から FMDB を利用する

FMDB 関連の API を参照する場合は

#import <FMDatabase.h>
#import <FMResultSet.h>

のように import する。CocoaPods でインストールした場合はフレームワーク扱いとなる。面倒なのでフレームワーク名を省略しているが、名前衝突を心配するなら <FMDB/FMDatabase.h> のように記述してもよい。

SQL 文の定義

SQL 文は NSString リテラルとして定義する。FMDB の API 呼び出しで直に書くよりも、定数にしておいたほうが管理しやすいと思われる。この辺の話は NSString 連結を利用して heredoc 風に定数を記述するにまとめた。

定数の位置は DAO クラスの *.m ファイル冒頭にしておく。SQL 文が長く大量にある場合は別ファイルに括りだすのものよいだろう。

CRUD

FMDB API の代表的なものとして FMDatabase はデータベース接続と SQL 文の実行を担当して FMResultSet が処理結果となる。基本、これらだけ覚えれば利用できる。

CREATE、INSERT ( UPDATE )、DELETE には FMDatabase - executeUpdate メソッドを使用。これは第一引数に SQL 文となる NSString、それ以降は可変長引数になっていて SQL 文の Placeholder へ対応する。INSERT だとこんな感じ。

static NSString * const kSQLInsert = @""
"INSERT INTO "
  "books (author, title, release_date) "
"VALUES "
  "(?, ?, ?);";

- (Book *)add:(NSString *)author title:(NSString *)title releaseDate:(NSDate *)releaseDate {
    Book *book = nil;
    if ([self.db executeUpdate:kSQLInsert, author, title, releaseDate]) {
        NSInteger bookId = [self.db lastInsertRowId];
        book = [Book bookWithId:bookId author:author title:title releaseDate:releaseDate];
    }

    return book;
}

SELECT では FMDatabase - executeQuery メソッドを利用する。引数の仕様は executeUpdate と一緒。WHERE 句で条件指定する場合は第二引数以降も使用することになるだろう。以下は単純な全行取得の例。

static NSString * const kSQLSelect = @""
"SELECT "
  "id, author, title, release_date "
"FROM "
  "books;"
"ORDER BY "
  "author, title;";

- (NSArray *)read {
    NSMutableArray *books = [NSMutableArray arrayWithCapacity:0];
    FMResultSet    *results = [self.db executeQuery:kSQLSelect];

    while ([results next]) {
        [books addObject:[Book bookWithId:[results intForColumnIndex:0]
                                   author:[results stringForColumnIndex:1]
                                    title:[results stringForColumnIndex:2]
                              releaseDate:[results dateForColumnIndex:3]]];
    }

    return books;
}

戻り値は FMResultSet になる。これはカーソル型のオブジェクトで next メソッドを呼ぶことで取得された行カーソルが進む。next は成否を BOOL で返すため行の列挙は while で処理するとよい。

行カーソルが示す先のデータを取得するのも FMResultSet のメソッドになる。XXXXForColumnIndex 系が SELECT 文に指定された列のインデックス、XXXXForColumn 系は列名を指定して値を取得する。XXXX には返される値の型を示す。

インデックスと名前のどちらで取得するかはお好みで。インデックスは単純だが順番変更に弱い。名前の場合は管理が面倒だが、名前さえ維持できれば順番を意識せずに取得できる。

Swift から FMDB を利用する

Swift でフレームワーク内のクラスを利用する場合は名前空間を import するだけでよい。

import FMDB

非常に楽ちんだ。

SQL 文の定義

Swift でも Objective-C のようにクラスの外周に定数を宣言できるのだが、SQL 文と DAO の関係は密なのでクラス単位の static 定数としておく。

class BookDAO: NSObject {
    private static let SQLCreate = "" +
    "CREATE TABLE IF NOT EXISTS books (" +
      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
      "author TEXT, " +
      "title TEXT, " +
      "release_date INTEGER" +
    ");"

    func create() {
        self.db.executeUpdate(BookDAO.SQLCreate, withArgumentsIn: nil)
    }
}

Swift の String は + で連結されるため、これを利用してインデントをつけている。

CRUD

FMDB API は Objective-C とほぼ共通。

CREATE、INSERT ( UPDATE )、DELETE には FMDatabase.executeUpdate(sql:, withArgumentsIn:) メソッドなどを使用する。第一引数に SQL 文となる String、第二引数の Array が SQL 文の Placeholder に対応づけられる。INSERT だとこんな感じ。

class BookDAO: NSObject {
    private static let SQLInsert = "" +
    "INSERT INTO " +
      "books (author, title, release_date) " +
    "VALUES " +
      "(?, ?, ?);"

    func add(author: String, title: String, releaseDate: Date) -> Book? {
        var book: Book? = nil
        if self.db.executeUpdate(BookDAO.SQLInsert, withArgumentsIn: [author, title, releaseDate]) {
            let bookId = db.lastInsertRowId()
            book = Book(bookId: Int(bookId), author: author, title: title, releaseDate: releaseDate)
        }

        return book
    }
}

SELECT は FMDatabase.executeQuery(sql:, withArgumentsIn:) メソッドとなる。引数の仕様は executeUpdate と一緒。以下は単純な全取得の例。

class BookDAO: NSObject {
    private static let SQLSelect = "" +
    "SELECT " +
      "id, author, title, release_date " +
    "FROM " +
      "books;" +
    "ORDER BY " +
      "author, title;"

    func read() -> Array<Book> {
        var books = Array<Book>()
        if let results = self.db.executeQuery(BookDAO.SQLSelect, withArgumentsIn: nil) {
            while results.next() {
                let book = Book(bookId: results.long(forColumnIndex: 0),
                                author: results.string(forColumnIndex: 1),
                                title: results.string(forColumnIndex: 2),
                                releaseDate: results.date(forColumnIndex: 3))
                books.append(book)
            }
        }

        return books
    }
}

Objective-C に対する特徴的な違いとして Optional 型の扱いがある。

Objective-C の場合、nil なオブジェクトに対し [obj message] 形式でメソッドやプロパティを呼び出すと処理が空振りする。これを利用して nil チェックを避けるテクニックがあるのだが、Swift なら if let で Optional な変数を受ければ配下のスコープで nil 済みの安全なオブジェクトを使用できる。

Objective-C の処理をそのまま Swift へ書き換えただけだと大量のエラーに見舞われて面食らうだろう。しかし慣れてくるとそれらが nil という曖昧な状態を避けるための設計ギプスとして有効であることを理解できる。unwrap まわりの面倒くささは確実に nil への抑止力となるはず。

こうした null/nil 安全については以下に詳しい。

Swift に触れ、私も null/nil 安全について以前より強く意識するようになった。

ユニット テスト

Xcode 標準のユニット テストを試す。

まず FMDB 関連は直に使用しない。DAO クラスのインスタンスは DAO Factory から得られるため、これらに関するものだけに依存する。

Xcode プロジェクトを作成する際に Include Unit Tests と UI Tests をチェックするとテスト用プロジェクトも追加される。前者がユニット テスト、後者は UI の動作をテストするものである。今回はユニット テストだけ使用した。

SQLite のデータベースはファイル単位として管理される。そのためテストを実行する都度、ファイルの存在をチェックして前回実行の結果に影響されぬよう注意が必要。もっと厳密にやるならテスト メソッド単位でファイルを消去するほうが安全だけど、そこまではしない。

Xcode のテストは Objective-C、Swift 共に XCTestCase を継承したテスト クラスを実装する。テスト全体の開始に setUp、終了時は tearDown が呼び出されるため、データベースの生成と破棄や DAO Factory 生成などはこれらに絡めておこなう。

以下は Swift の例。

import XCTest

class BookDAOTests: XCTestCase {
    private let filePath = BookDAOTests.databaseFilePath()
    private var daoFactory: DAOFactory!

    override func setUp() {
        super.setUp()

        self.clean()

        self.daoFactory = DAOFactory(filePath: self.filePath)
        if let dao = self.daoFactory.bookDAO() {
            dao.create()
        }
    }

    override func tearDown() {
        self.clean()
        super.tearDown()
    }

    func clean() {
        let manager = FileManager.default
        if manager.fileExists(atPath: self.filePath) {
            do {
                try manager.removeItem(atPath: self.filePath)
            } catch {
                print("Error: faild to remove database file.")
            }
        }
    }

    private static func databaseFilePath() -> String {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let dir   = paths[0] as NSString
        return dir.appendingPathComponent("test.db")
    }
}

テスト対象にしたいクラスは Xcode 右にあるファイルのプロパティから Target Membership で Tests 系プロジェクトもチェックに含める必要あり。Swift は *.swift、Objective-C なら *.m ファイルをチェックすればよい。

Objective-C の場合、更に XCTestCase 派生クラス側のコードでテスト対象クラスのヘッダーを import しなければならない。ヘッダー管理の必要な言語って面倒だ。

これらの設定をせずにテストを実行しようとすると Tests プロジェクトのビルドでリンク エラーになる。Target Membership はよく忘れるので注意すること。

XCTestCase 派生クラスに testXXXX というメソッドを実装するとそれがテスト対象となる。テストを実行した時の処理順は以下。

  1. setUp
  2. testXXXX
  3. tearDown

実際に DAO クラスのデータ更新処理をテストしてみる。

class BookDAOTests: XCTestCase {
    func testUpdate() {
        if let dao = self.daoFactory.bookDAO() {
            let book = dao.add(author: "author", title: "title", releaseDate: Date())
            XCTAssertNotNil(book)

            // Before
            var books = dao.read()
            XCTAssertEqual(books[0].title, "title")

            // After
            let book2 = Book(bookId: (book?.bookId)!, author: (book?.author)!, title: "title2", releaseDate: (book?.releaseDate)!)
            XCTAssert(dao.update(book: book2))
            books = dao.read()
            XCTAssertEqual(books[0].title, "title2")

            XCTAssert(dao.remove(bookId: (book?.bookId)!))

        } else {
            XCTAssert(false)
        }
    }
}

テストにおける値の妥当性チェックは XCTAssert 系の関数で実施する。値の性質ごとに関数が提供されているので、適切なものを選ぼう。よく使うものとしては以下がある。

関数 機能
XCTAssert 一つの値に対して真であることを判定。偽ならば失敗する。
XCTAssertEqual 二つの値を比較。不一致ならば失敗し、それぞれの値の内容を表示する。Objective-C の場合、NSObject 系の比較には利用できないので注意する。Swift は問題なし。
XCTAssertEqualObjects 二つの値を比較。不一致なら失敗し、それぞれの値の内容を表示する。Objective-C で NSObject 系を比較する場合はこの関数を使用する。Swift は不要なので提供されていないようだ。
XCTAssertNil 一つの値に対して nil であることを判定。nil でなければ失敗する。
Not 系 各種 XCTAssert 関数名に Not のついたもの。例えば XCTAssertNotEqual など。元と判定が逆転する。

NSString を判定する場合、厳密さを意識する必要があるかもしれない。詳しくは以下を参照のこと。

Xcode 8.1 でもこの挙動はそのままである。例にある文字列をテストすると XCTAssertEqualObjects でも不一致となった。ここまで判定したいなら NSString - compare を使用する。

これが問題になるとしたら macOS の HFS+ みたいに Unicode 正規化で「が」を「か」と濁点にわけて管理しているものと、そうでない環境で得られたファイル名やパスの同一性などが考えられる。

データを扱う環境がひとつの系で完結しているとか文字エンコーディングと正規化の系が統一されていればよく、iOS アプリの場合はあまり意識しなくてよさそう。そのため今回のサンプルにおけるテストでは XCTAssertEqualObjects を採用した。

この問題が気になるなら以下の記事も参照のこと。

まとめ

Objective-C と Swift について。

Objective-C については 2 年前まで使用していたのでそれほど変化を感じない。元記事のサンプルと比較したら差分は大きいのだけど、想定どおりである。

一方、Swift は印象的だった。Swift 1 〜 3 までの変遷と混乱をチラ見して大変そうな印象しかなかったのだけど、実際にプログラミングしてみると nil 安全まわりが実によい。

基本的に nil を抑止する方針であること、使わざるを得ないときは Optional 型として明示される点が気に入っている。これを経験したことにより、null/nil 安全のない言語で書くときの設計も影響を受けるだろう。

ユニット テストについて。

他のプラットフォームにおける Power Assert 系のように XCTAssert も普通の比較でも失敗時に値の詳細を表示してほしい。用途ごとに関数を使い分けるのは面倒だ。いちおう Swift には keygx/PAssert があるのだけど、Power Assert 的なものは標準にしてもよいのではないか。

あと Cocoa Touch を使用するから仕方ないことだけど、テストに iOS シミュレーターや実機を要するため実行が遅い。スクリプト言語の気軽なテストに慣れていると重く感じる。

FMDB について。

細かな改善はたくさん反映されているのだろうけど、使用感は 5 年前と変わらず。これはよいことだ。普通に設計したらこうなるよね、という期待に答える直感的な API である。

以前よりサンプルの古さが気になっていたので、書き直せてスッキリした。

以下、余談。

この記事とサンプルは 2016 年末に書き終えていたのだけど Swift コードを構文強調する手段に WP Code Highlight.js を使うためブログ全体の Markdown 移行をしているうちに 2017 年へずれ込んでしまった。

結局 WP Code Highlight.js は行間へ余計な br タグを自動挿入するのを防げなくて導入は見送った。仕方ないので SyntaxHighlighter Evolved: Swift Brush を導入して構文強調している。

npm 開発で脱 Babel してみる

$
0
0

自作 npm の開発で脱 Babel したときの対応と問題点まとめ。

  • 2017/1/31 訂正
    power-assert の作者、t_wada さんより power-assert は babel-register を通すなどしないと assert 置換が働かず素の assert になってしまうという指摘があったのでユニットテスト関連の記述を訂正

脱 Babel を決めた背景

私はいくつか自作 npm を公開している。これらは ES2015 以降の機能と構文を利用して npm publish の際に Babel で ES5 相当へ transpile している。この運用で特に問題も起きていない。またプリセットに babel-preset-latest を採用することで ES 関係の規格追従を Babel 任せにできる安心感もあり、ずっとこのままでいいと思っていた。

ある日、職場で Node アプリを開発している人から「Babel 依存は怖くないですか?」という質問があった。Babel にバグがあったら調査や修正は困難だし、使わなくてよいならばそうするに越したことはないのでは?と。

これまで C++、C#、Java などコンパイル前提の言語で開発した経験から、よほどのことがない限りコンパイル結果は信頼に足ると判断していた。また、コンパイルされたマシン語や中間言語を人間が直に記述するのは非常にキツイ。そのため脱コンパイラーという選択肢は現実的ではないと認識している。

一方、JavaScript における transpile は高級言語どうしの変換となる。その気になれば人間が書けるのだ。

transpiler への慣れから、この事実をすっかり忘れていた。Node であれば Web フロントエンドと異なり動作環境の分岐は少なく、package.json の engines にて対象環境を限定することも可能である。ならば新しい Node を前提として脱 Babel を検討してみるのもよさそうと考えはじめた。

そんな折、npm-run-all が v4.0.0 で Babel による transpile を廃止。Node は v4 時点で ES Modules を除く大半の ES2015 機能が実装されているため、この範囲で足りるなら transpile せずにそのままリリース可能だ。その前例として普段利用しているツールが脱 Babel したのはインパクトある。

これらを踏まえ、まずは自作 npm のうちダウンロード数の少なくニッチな wpxml2md から脱 Babel を試してみることにした。

脱 Babel への道のり

脱 Babel 対応で実施したことを書く。

Node 環境の明示的な指定

脱 Babel における前提条件として動作環境とする Node のバージョンを決める。npm-run-all は Node v4 を下限としているようだが、wpxml2md では v6 としておく。

開発と検証コストを考慮して自作 npm の動作環境は「最新 + 最新 LTS」としている。2017/1 時点の最新 Node は v7 系、LTS は v6 と v4 があるため対象は「v7 + v6」となる。これまでは Babel による transpile で v4 以下でも動作していたのだが、これを廃止することで明示的な下限の指定が必要となった。これは package.json の engines プロパティに記述する。

{
  "engines": {
    "node": ">= 6"
  }
}

ちなみに

node v6, v7

というバッヂを用意していて前から README へ掲載していた。今回の対応により、ようやくこれが本来の意味をあらわすようになった。

Babel の transpile を廃止

これまでは Babel の transpile を前提として以下のような Babel 設定と npm-scripts を利用していた。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register test/**/*.test.js",
    "watch": "babel src --out-dir ./ --watch",
    "start": "npm run watch",
    "build": "babel src --out-dir ./",
    "prepublish": "npm run build"
  }
}
script 内容
test 予約された npm run のタスク。 mocha と power-assert によるユニット テスト。
watch 開発用。ファイル監視による自動 transpile を実行。
start 予約された npm run のタスク。watch を呼び出すだけ。
build リリース用。現時点のソース コードで transpile を実行。
prepublish 予約された npm publish 時に呼び出されるタスク。build を呼び出しているため npm として公開されるイメージは transpile されたものになる。なお prepublish は現在 deprecated になっていて prepare へ修正すべきなのだが直し忘れていた。

これが脱 Babel によりこうなる。

{
  "babel": {
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register test/**/*.test.js"
  }
}

transpile 不要となるため関連するタスクが消え、ユニット テストだけが残った。ただし power-assert を利用しているなら標準 assert を置換するための transpile が必要なので、このための設定は維持する。

env.development.presetspower-assert だけ指定することで、Babel 依存がユニット テストに限定されていることが明示されるだろう。必要な Babel 関連の npm も babel-registerbabel-preset-power-assert だけになる。

ES Moduels を CommonJS 化する

Node の ES Modules 対応については以下が詳しい。

なお現時点の最新 Node である v7 においても ES Modules には対応していないため、export/import は CommonJS の exports/require へ修正する必要がある。例えば

export const Options = {
};

export default class CLI {
}

const Options = {
};

class CLI {
}

module.exports = {
  Options: Options,
  CLI: CLI
};

とする。読み込む側は

import CLI from `cli.js`;
import { Options } from `cli.js`;

const CLI = require( `cli.js` ).CLI;
const Options = require( `cli.js` ).Options;

とする。ひとつのモジュールから exportdefault export で複数のインターフェースを公開していると面倒である。しかし ES Modules で書いていたなら import はソース コード冒頭に集約されているから悩む余地なく機会的な作業になる。なんなら正規表現でまとめて変換できる。

ユニット テストについても同様に対応すること。

余談。

CommonJS にした後でも将来の ES Modules 移行を容易にするため require はソース コード冒頭へ書く習慣をつけたほうがよいかもしれない。その場合、読み込み先を camelCase で命名しているとローカル変数と競合する可能性が高くなるため PascalCase にしたくなり、私はそうしている。例えば fsFs と命名している。

Node と CommonJS だとスコープを意識して require を使い分け、なるべく関数ローカルで宣言する派が多数な感じなので ES Modules 対応されたときが気になる。私のようにするか、それともよりよい慣習となるのか?実に楽しみだ。

ESDoc 対応

npm のコード ドキュメント生成に ESDoc を採用している場合、そのままでは CommonJS を解釈できないので対応が必要になる。CommonJS は

上記 issue で紹介されている esdoc-node により対応できる。ESDoc にプラグイン機能があることを初めて知った。これを指定することでコードが ESDoc に解釈される前処理を実行できるらしい。例えば esdoc-node は CommonJS を ES Modules に変換して ESDoc に渡す。

さっそく使ってみよう。まず esdoc-node をプロジェクトに追加。

$ npm install -D esdoc-node

次にこれを ESDoc 設定へ追加する。私は ESDoc の設定を package.json に定義しているので

{
  "esdoc": {
    "source": "./src",
    "destination": "./esdoc",
    "test": {
      "type": "mocha",
      "source": "./test"
    },
    "plugins": [
      { "name": "esdoc-node" }
    ]
  }
}

このようにした。設定後、ESDoc を実行して CommonJS なコードも正しく解析されることを確認。

ただしローカル実行ではなく ESDoc Hosting Service は CommonJS やプラグインに対応していないようだ。試しに wpxml2md の API Document を生成 してみたのだが、2017/1/25 時点では esdoc-node 未指定の状態と同様に CommonJS の絡むものが解釈できていない。

この点について要望 issue を登録してみたのだけど、もしプラグイン対応する場合

  • Hosting Service 側でプラグインを網羅しておき選択実行する
    • プラグインを中央管理する仕組みがないと網羅できない
    • プラグインのバージョンはどうするのか?常に最新?
  • プロジェクトの ESDoc 設定と package.json から動的にプラグインを決定する
    • プラグインのインストールはどうする?
    • ドキュメント生成とごにプラグインをインストールすると Hosting Service の負荷が大きい
    • CI 系サービスのように VM や Docker コンテナで対応するとしても負荷は大きい

といった問題が予想されるため難しそうだ。しかし要望があることだけは記録しておきたかったので issue 登録することにした。

別の方法として ESDoc 本体が esdoc-node 処理を取り込むという選択肢もある。ただ、いずれ Node が ES Modules へ正式に対応するとしたら CommonJS は過渡期の存在である。

  • そのために対応コストを割くのか?
  • ESDoc と名乗るツールとして ECMAScript とは直に関係しない CommonJS へ対応することは設計思想として望ましくないのでは?

という考えもあるだろう。どのような対応、または非対応のままになったとしても ESDoc 作者の意向を尊重したい。

ESDoc Hosting Service を利用しない場合、対象プロジェクトが GitHub で管理されているならそのリポジトリに対して GitHub Pages を使う手もある。ローカルの ESDoc + esdoc-node で出力したものを自前でアップロードするか、そういうタスクを npm-scripts に定義して CI サービス経由で生成から公開まで自動化するなどの方法が考えられる。

本記事を書いた直後に ESDoc 作者の @h13i32maru さんから見解が。

ESDoc Hosting Service 的にはセキュリティや負荷を考慮し、ESDoc 公式プラグインのみサポートしているとのこと。ただ Node の ES Modules 対応は相当に先となりそうなので、この長い過渡期に Node かつ脱 Babel したい開発者としてどうか?という悩ましい課題がある。

まとめ

一部、問題もあったが transpile 不要となったことで Node のバージョンだけを意識して開発すればよい。記述したコードはそのまま実行されることが保証される。Babel 由来の潜在的なトラブルを考えなくてよいため精神的に楽。

あと npm をユニット テストではなく普通のプログラムとして走らせて検証してみたい場合、いちいち transpile しなくて済むのも助かる。

検証はテストに定義すべきでは?という意見もあるだろうけどリポジトリの examples フォルダに npm として参照したときのサンプルを配置しているとき、その動作検証で npm publish する前の現行コードを試したくなったりする。その場合、transpile なしだとビルド系タスクを実行せず動かせてよい。

以上を踏まえ、他の npm についても気が向いたら脱 Babel してゆく予定。

JavaScript Standard Style を試す

$
0
0

話題の JavaScript Standard Style を試してみた。

背景

以前、以下の記事とはてブで JavaScript Standard Style を知った。

はてブではセミコロンの省略に抵抗感のある人が多く、私もそうだった。しかし同はてブで id:mysticatea さんが指摘されているように ESLint の no-unexpected-multiline でセミコロン省略時に問題のおきるコードを検出できる。また、

  • 昨年末に Swift 入門してセミコロンのないコードに慣れた
  • Electron の JavaScript コードがセミコロンなしルールで読みやすかった

という理由もあり、セミコロンなしも案外よいものじゃないかと考えるようになった。

私のコーディング スタイルは世間の標準からみると独特で、これは過去に在籍していたプロジェクトのルールを踏襲している。主な特徴としては

  • 括弧の内側にスペースを入れる
  • ifwhile などのキーワードと関数名の後にはスペースを入れない

というもの。C 言語や JavaScript でよく見られる K&R 系だとこんな感じのコードも

function isArray (arg) {
  if (Array.isArray) {
    return Array.isArray(arg);
  }

  return Object.prototype.toString.call(arg) === '[object Array]';
}

私のスタイルだとこうなる。

function isArray( arg ) {
  if( Array.isArray ) {
    return Array.isArray( arg );
  }

  return Object.prototype.toString.call( arg ) === '[object Array]';
}

これはこれで気に入っていたのだが GitHub で OSS を運用にするようになり、第三者からの PR は一般的な方のスタイルでくるため扱いに困っていた。スタイルの違いを理由に断るとか直すのも面倒なので今はそのまま merge しているけれど、そもそも自分の好みより世に迎合するほうがよいんじゃないか?と思い始めた。

あと Xcode の Editor におけるコーディング スタイル設定が貧弱というのもある。他の IDE だと私のスタイルを再現するのに十分な設定があるためそうしてきた。しかし Xcode の Text Editing は驚くほど設定がない。まともにいじれるのは Indentation ぐらいである。

これまでは仕方なく根性で手動整形してきたが、Swift 入門を機にあきらめた。iOS で SQLite – FMDB の使い方 2017のサンプルでは Xcode の提示するスニペットそのままに書いている。

この経験を経て、自分のスタイルへ固執することをやめることにした。プラットフォーム標準があればそれに従い、IDE や Editor、Linter の補助を最大限に享受する方針へ転換する。

というわけで、まずは公私ともに書く機会の多い JavaScript のコーディング スタイルから変更してみる。

JavaScript Standard Style

JavaScript のコーディング スタイルとしては

あたりが有名どころらしい。どれを選ぶか迷ったが Electron のようなセミコロンなしスタイルを採用している JavaScript Standard Style にしてみた。Standard と銘打つ度胸と GitHub の star 数も判断材料である。セミコロン以外はよく見るスタイルなので、ここを受け入れられるかが重要。

JavaScript Standard Style への準拠にあたり、それを保証する仕組みがほしいので ESLint を利用。今回は akabekobeko/npm-wpxml2md プロジェクトで試す。

feross/eslint-config-standard を参考にプロジェクトのローカルに必要な npm をインストール。

$ npm i -D eslint-config-standard eslint-plugin-standard eslint-plugin-promise

次にプロジェクトのルートで .eslintrc を定義。

{
  "extends": "standard",
  "env": {
    "mocha": true
  },
  "rules": {
    "no-multi-spaces": 0,
    "yoda": 0
  }
}

JavaScript Standard Style を使用するだけなら "extends": "standard" だけでよい。しかし mocha で書いたユニット テストも対象にしたいのと、

  • 連続した複数行の変数宣言などで縦位置をスペースで揃えたい
  • if 文で不等号による範囲チェックを if (0 <= value && value < max) のように書きたい

のでそれらの設定を追加した。JavaScript Standard Style はスタイルに準拠していることを示す証として

Standard - JavaScript Style Guide

というバッヂを提供している。ルール緩和した場合でもこれをつけてよいものか迷ったけれど緩和は極小なので README へ掲示することにした。第三者が README をながめたとき、基本となるコーディング スタイルを視認できるのはよいことだ。

私は JavaScript のコーディングに Atom を使用しており、ESLint によるリアル タイムなチェックのため

を採用している。これまで linter-eslint はグローバルにインストールした ESLint とプラグインを使用して設定も ~/.eslintrc を参照するようにしていたが、このプラグインはプロジェクトのローカルに ESLint と .eslintrc を検出するとそちらを優先してくれる。

そのため既存プロジェクトは現行のスタイルを維持しつつ、個別に JavaScript Standard Style を採用する運用が可能である。いきなりグローバルを書き換えてもよいけど、少しずつ移行するほうが安全だろう。

スタイルのチェックは基本的に Atom 上で確認 & 修正するのだがファイル単位で個別に作業していると抜けも出やすいため、一括チェック可能な仕組みも用意する。私は npm-scripts に

{
  "scripts": {
    "eslint": "eslint ./src"
  }
}

を定義して

$ npm run eslint

を実行している。これは AltJS/AltCSS の transpile のようにバックグラウンドでファイル変更の検出と自動チェックさせるほうがよいのかもしれない。

セミコロンなき世界

旧スタイルから JavaScript Standard Style へこのように書き換えてみた。これらの中で比較的、短めのコードを引用する。

#!/usr/bin/env node

'use strict'

const CLI = require('./cli.js').CLI
const WpXml2Md = require('../lib/index.js')

/**
 * Entry point of the CLI.
 *
 * @param {Array.<String>} argv   Arguments of the command line.
 * @param {WritableStream} stdout Standard output.
 *
 * @return {Promise} Promise object.
 */
function main (argv, stdout) {
  return new Promise((resolve, reject) => {
    const options = CLI.parseArgv(argv)
    if (options.help) {
      CLI.printHelp(stdout)
      return resolve()
    }

    if (options.version) {
      CLI.printVersion(stdout)
      return resolve()
    }

    if (!(options.input)) {
      return reject(new Error('"-i" or "--input" has not been specified. This parameter is required.'))
    }

    if (!(options.output)) {
      return reject(new Error('"-o" or "--output" has not been specified. This parameter is required.'))
    }

    return WpXml2Md(options.input, options.output, {
      noGFM: options.noGFM,
      noMELink: options.noMELink,
      report: options.report
    })
  })
}

main(process.argv.slice(2), process.stdout)
.then()
.catch((err) => {
  console.error(err)
})

実にスッキリ。見慣れるまでは JavaScript に見えないかもしれない。

これまで C 言語系の構文をもつプログラミング言語に慣れ親しんできたためセミコロン入力は手癖になっていたけど、いざ不要になるとこれがどれだけ負担だったかを認識させられる。

はじめは、ほんの 1 文字だしプログラミングでは書くより考える時間のほうが長いのだから気にするほどのことか?と考えていた。しかし ; + EnterEnter に置き換わることは、実際に体験してみると実に大きい。正確に構文の末尾へセミコロンを置くことと、単に改行するだけというのはかなり違う。セミコロンなし派が一定数いる意味を身をもって知った。

なおセミコロン省略により起き得る問題は前述のように no-unexpected-multiline が検出してくれる。実際の eslint-config-standard/eslintrc.json でも "no-unexpected-multiline": "error" と設定されているため安心だ。

所感

JavaScript Standard Style 導入の所感をまとめる。

  • 一般的な JavaScript と自身のコードを交互にながめても違和感をおぼえにくくなった
  • Atom のスニペットをそのまま利用できるようになった
  • 括弧のスペースを詰めてもそれなりに読める
  • まともな Editor なら構文強調のおかげで括弧とそれ以外を区別しやすいので困らない
  • セミコロンなしはスッキリしてかなり読みやすい
  • セミコロンを入力するのがどれだけ手間だったか実感できる

結論。JavaScript Standard Style は素晴らしかった。今後、他のプロジェクトでも採用する予定。


babel-preset-env を試す

$
0
0

npm として配布するものは純粋な Node 機能のみで構成したいため脱 Babelしたが、Web フロントエンドや Electron では最新の ECMAScript 機能を利用したい。

というわけで、これまでは Babel + babel-preset-latest で JavaScript を変換してきた。しかし latest だと Web ブラウザーや Electron が最新規格に対応しても個別に変換を無効化するのが難しい。

例えば ES2015 Classes は大半の Web ブラウザーが対応済みにも関わらず

var Sample = function () {
  function Sample() {
    _classCallCheck(this, Sample);
  }
}

のように変換されてしまう。一方、機能単位で変換を無効にできるとしても Web ブラウザー毎の対応状況を調べるのは実に面倒。ECMAScript 6 compatibility table をマメにチェックしながら下限となる環境を決める必要がある。

こうした悩みを解決するツールとして babel-preset-env が提供されている。用途を latest と比較すると以下のようになる。

preset 用途
latest 最新 ECMAScript を常時 ES5 相当に変換。
env 最新 ECMAScript を指定された環境に基づき最小限 ES5 に変換。

対象環境を考えるのが面倒で変換コストや品質を気にしないなら latest、なるべく無駄な変換をなくしたいなら env を採用することになるだろう。

試してみる

babel-preset-env を試してみる。

Web フロントエンドや Electron だと Babel 単体よりも Browserify + babelifywebpack + babel-loader のように bundler と組み合わせて利用する機会が多い。しかし bundler 部分も含めると Babel が変換した結果に限定してチェックするのが難しいため実行には babel-cli を採用することにした。

変換対象として ES2015 と ES2016 の代表的な機能を使用した 2 種類の JavaScript を定義。まずは sample.js

// ES2015
export default class Sample {
  message (text) {
    console.log(text)
  }

  async asyncFunc () {
    const wait = (n) => {
      return new Promise((resolve) => setTimeout(() => resolve(n), n))
    }

    await wait(1000)
    console.log('finish!!')
  }

  static func (text = 'sample') {
    console.log(text)
  }
}

// ES2016: Exponentiation Operator
export function pow (a = 0, b = 0) {
  return a ** b
}

// ES2016: Array.prototype.includes
export function includes (arr = [], value) {
  return Array.isArray(arr) ? arr.includes(value) : false
}

sample.js を参照する index.js

import Sample, { pow, includes } from './sample.js'

{
  const sample = new Sample()
  sample.message('Message1')
}

Sample.func('Message2')

console.log(pow(2, 3))
console.log(includes([1, 2, 3], 2))

これらを babel-preset-env の README に掲載されている Examples と現時点で最新の Electron v1.6 向け設定で変換する npm-scripts を定義した。Babel の設定も含めて package.json で完結しているため長いけれど全掲載する。

{
  "name": "using-babel-preset-env",
  "version": "1.0.0",
  "description": "",
  "author": "akabeko",
  "license": "MIT",
  "main": "index.js",
  "keywords": [
    "babel-preset-env"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/akabekobeko/examples-web-app.git"
  },
  "babel": {
    "env": {
      "default": {
        "presets": [["env"]]
      },
      "chrome52": {
        "presets": [
          ["env", {
            "targets": {
              "chrome": 52
            }
          }]
        ]
      },
      "chrome52webpack_loose": {
        "presets": [
          ["env", {
            "targets": {
              "chrome": 52
            },
            "modules": false,
            "loose": true
          }]
        ]
      },
      "browserslist": {
        "presets": [
          ["env", {
            "targets": {
              "chrome": 52,
              "browsers": ["last 2 versions", "safari 7"]
            }
          }]
        ]
      },
      "node_current": {
        "presets": [
          ["env", {
            "targets": {
              "node": "current"
            }
          }]
        ]
      },
      "debug_output": {
        "presets": [
          ["env", {
            "targets": {
              "safari": 10
            },
            "modules": false,
            "useBuiltIns": true,
            "debug": true
          }]
        ]
      },
      "include_exclude_plugins_buildin": {
        "presets": [
          ["env", {
            "targets": {
              "browsers": ["last 2 versions", "safari >= 7"]
            },
            "include": ["transform-es2015-arrow-functions", "es6.map"],
            "exclude": ["transform-regenerator", "es6.set"]
          }]
        ]
      },
      "electron": {
        "presets": [
          ["env", {
            "targets": {
              "electron": 1.6
            }
          }]
        ]
      }
    }
  },
  "scripts": {
    "start": "run-s build",
    "build:default": "cross-env NODE_ENV=default babel ./src --out-dir ./dist/default",
    "build:chrome52": "cross-env NODE_ENV=chrome52 babel ./src --out-dir ./dist/chrome52",
    "build:chrome52webpack_loose": "cross-env NODE_ENV=chrome52webpack_loose babel ./src --out-dir ./dist/chrome52webpack_loose",
    "build:browserslist": "cross-env NODE_ENV=browserslist babel ./src --out-dir ./dist/browserslist",
    "build:node_current": "cross-env NODE_ENV=node_current babel ./src --out-dir ./dist/node_current",
    "build:debug_output": "cross-env NODE_ENV=debug_output babel ./src --out-dir ./dist/debug_output",
    "build:include_exclude_plugins_buildin": "cross-env NODE_ENV=include_exclude_plugins_buildin babel ./src --out-dir ./dist/include_exclude_plugins_buildin",
    "build:electron": "cross-env NODE_ENV=electron babel ./src --out-dir ./dist/electron",
    "build": "run-s build:*"
  },
  "devDependencies": {
    "babel-cli": "^6.24.0",
    "babel-preset-env": "^1.2.2",
    "cross-env": "^3.2.4",
    "npm-run-all": "^4.0.2"
  }
}

Babel の設定は NODE_ENV 単位に定義できることを利用して cross-env により分岐している。npm start を実行すると dist/ 配下に設定ごとの変換結果が出力される。普通に指定するなら presets 配下の内容を Babel 設定のルートに記述すればよい。例えば .babelrc に Electron 用の設定を定義するなら以下のようにする。

{
  "presets": [
    ["env", {
      "targets": {
        "electron": 1.6
      }
    }]
  ]
}

サンプルの出力結果を比較すると、例えば default なら全変換され chrome52webpack_loose だと async/await 以外はそのままであることが分かる。また chrome52 では async/await 変換があり electron だと v1.6.0 から Chromium 56.0.2924.87 を採用しているため async/await はそのままに Modules は共通して変換されていた。

babel-preset-env の README によれば、変換の基準は前述の ECMAScript 6 compatibility table を基準として判断しているのだという。つまり機能と対象環境の組み合わせ管理を babel-preset-env に丸投げできるわけだ。本当に ECMAScript 6 compatibility table や Electron のバージョンをチェックしているのか?と targets にデタラメな値を設定してみたら実行時にエラーとなった。Electron に関しては 1.0 以降が対象で最新の 1.6 系まで指定可能。

ただし targets のバージョンは JSON の number になるため小数点第一位までしか対応しておらず 1.6.2 のように semver 形式は受け付けていない。また "node": "current" は可能だが "electron": "current" を指定したらエラーになった。Electron も browsers のように相対値で指定したいものだ。

とはいえ babel-preset-latest を使用しつつ「将来 ES2015 変換をやめたくなったらどうしよう?」などと考えていたので、そのような処理を動作環境の指定へ抽象化して変換してくれる babel-preset-env は実にありがたい。

babel-preset-env 設定により想定している動作環境が明示される点もメリット。これはアプリの仕様を外部へ説明するのに役立つ。また設定を Git リポジトリなどで管理しているなら動作環境の変遷も記録される。例えば IE サポートを打ち切るとして、その履歴をきちんと残せるのだ。

最後に動作確認で使用したサンプル プロジェクトを公開しておく。

babel-preset-env と minify

$
0
0

babel-preset-env を試す で試した Electron 用の設定を実際に akabekobeko/examples-electron へ適用してみたところ、いろいろと問題があったので記録しておく。

JavaScript のビルド設定

この記事を読むための前提条件として必要な情報を先にまとめておく。

私は Electron 用 JavaScript ビルドを package.json へ定義している。必要最小の npm は以下。

npm 用途
browserify Bundler。JavaScript 間のファイル参照を解決して単一ファイルを出力する。
babelify Transpiler である Babel の Browserify 用プラグイン。ES.next で書かれた JavaScript を ES5 などに変換してくれる。
babel-preset-env Babel が ES.next を解析するためのプリセット。対象環境を設定することで、それにあわせた変換を実行してくれる。従来、環境を指定せず最新 ES.next を常に ES5 化する場合は babel-preset-latest を使用していたが、現在これをインストールすると deprecated と共に env を使用せよと警告される。
babel-preset-react Babel が React の JSX を解析するためのプリセット。
cross-env npm-scripts 上で環境変数 NODE_ENV の値を設定するツール。npm の中にはこの値によって処理を分岐するものがあり、例えば React は production だとリリース用となりログ出力処理などが無効化される。
exorcist Web ブラウザーの開発者ツールで変換前後の JavaScript を紐付けて解析するための Source Maps を生成するツール。これがあると実行は変換後、デバッガー表示は変換前のファイルという運用ができて便利。
uglify-js JavaScript を minify するツール。非常に高機能でインデント削除だけでなく参照を解析したうえで変数名などを短縮するなどの最適化や難読化もおこなえる。

これらを使用した package.json 定義は以下。私は Babel の設定も独立したファイルではなく npm-scripts などと共に package.json へ記述する派。

{
  "babel": {
    "presets": [
      [
        "env",
        {
          "targets": {
            "electron": 1.6
          }
        }
      ],
      "react"
    ]
  },
  "scripts": {
    "build:js-main": "browserify -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -d | exorcist ./src/assets/main.js.map > ./src/assets/main.js",
    "build:js-renderer": "browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron -d | exorcist ./src/assets/renderer.js.map > ./src/assets/renderer.js",
    "release:js-main": "cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node | uglifyjs -c warnings=false -m -d DEBUG=false > ./dist/src/assets/main.js",
    "release:js-renderer": "cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron | uglifyjs -c warnings=false -m -d DEBUG=false > ./dist/src/assets/renderer.js",
  },
  "devDependencies": {
    "babel-preset-env": "^1.3.2",
    "babel-preset-react": "^6.23.0",
    "babelify": "^7.3.0",
    "browserify": "^14.1.0",
    "cross-env": "^4.0.0",
    "exorcist": "^0.4.0",
    "uglify-js": "^2.8.19"
  }
}

Electron は Main/Renderer プロセスのエントリー ポイントが別れているため、個別にビルドする。build:js-* は開発用、release:js-* がリリース用の定義。babel-preset-env の設定は現時点で最新の Electron v1.6 を対象としている。

この状態で開発用ビルドを実行すると Electron v1.6 は Chromium 56.0.2924.87 を採用しているため、ES2015 Classes など対応されているものはそのままに、Modules など未対応のものだけ変換される。もちろん、変換されたコードはちゃんと Electron アプリとして実行可能。

しかしリリース用ビルドは問題がある。開発用と同様に Browserify + babelify 部分まではよいのだが、uglify-js は ES2015 以降に対応していないためエラーになる。というわけで、この問題へ対応してみる。

uglify-js の harmony 版

ES2015 が標準化されてひさしい。そのため当然 uglify-js にも対応が要望されている。ただし ES2015 を含む ES.next へ対応するのはかなり難しい。そのため uglify-js は実験的な ES.next 対応として harmony 版を提供、README の Harmony 欄にはこう書かれている。

If you wish to use the experimental harmony branch to minify ES2015+ (ES6+) code please use the following in your package.json file:

"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony"

or to directly install the experimental harmony version of uglify:

npm install --save-dev uglify-js@github:mishoo/UglifyJS2#harmony

See #448 for additional details.

これを踏まえて uglify-js を harmony 版へ差し替える。npm un -D uglify-js してから npm i -D uglify-js@github:mishoo/UglifyJS2#harmony して devDependencies 上のバージョン表記が npm 管轄から特別なホストへ切り替わることを確認。

{
  "devDependencies": {
    "uglify-js": "github:mishoo/UglifyJS2#harmony"
  }
}

この状態で再びリリース用ビルドを実行すると正常に終了した。出力されたファイルを見ると class などはそのままに uglify-js へ指定された設定どおりの minify が実行されている。ただし問題もある。

harmony は実験的なものでありバグも多い。uglify-js の issue を ES6ES2015 で検索すると対応に苦慮しているようだ。これまでの流れと現状を把握するには README にも掲載されている issue Harmony support を読むとよい。2014 年から始まり今も活況である。

Releases についても harmony は Pre-release となり npmjs としてのバージョン管理下にはない。よって現時点ではプロダクトに採用するのを避けたほうがよいだろう。

babel-preset-babili

uglify-js がダメなら ES.next へ正式対応している minify ツールを使えばよい。というわけで Babel ファミリーの babel-preset-babili を試す。npm i -D babel-preset-babili してから Babel と npm-scripts を書き換える。

{
  "babel": {
    "presets": [
      [
        "env",
        {
          "targets": {
            "electron": 1.6
          }
        }
      ],
      "react"
    ],
    "env": {
      "production": {
        "presets": [
          "babili"
        ]
      }
    }
  },
  "scripts": {
    "release:js-main": "cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -o ./dist/src/assets/main.js",
    "release:js-renderer": "cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron -o ./dist/src/assets/renderer.js",
  }
}

リリース版だけ minify したいので env.production.presets に babili を指定する。この状態でリリース用ビルドを実行すると確かに env も考慮しつつ ES.next 部分が minify された。しかし標準では dependencies の npm は minify されず、細かなカスタマイズはプリセットを構成する各種プラグインについて学習する必要がある。

uglify-js と同等の minify を再現できるかは分からないが babel-preset-env と組み合わせるなら、同じ Babel ファミリーであり ES.next 対応の知見も共有されそうな babili がよさそう。

というわけで babili を掘り下げようと思ったのだが README をざっと読んだだけでも重量級っぽい雰囲気がプンプンただよってるため、後で独立した記事としてまとめる予定。

babel-preset-babili を試す

$
0
0

babel-preset-env と minify の続き。前回は ES.next なコードを minify する方法として uglify-js を中心に babel-preset-babili を少しだけ試したところで終わった。今回は後者の使い方を掘り下げる。

Babel における plugin と preset

babel-preset-babili は ES.next な JavaScript を ES5 以降の書式に変換する Babel 関連のツールで minify を担当。

現在の Babel 本体はランタイムに徹し、実際のコード解析や変換はランタイム上で動作する plugin により処理される。開発者は機能ごとに plugin を組み合わせることになるが、これらは膨大である。そのため直に plugin を利用するのではなく、plugin 集となる preset を選ぶほうがよいだろう。

例えば以下のような preset がある。

preset 用途
babel-preset-env 対象とする動作環境 (Web ブラウザーなど) の組み合わせやバージョンを指定することで ECMAScript 6 compatibility table に基き ES.next を ES5 以降の形式へ必要最小に変換する。標準では babel-preset-latest 相当の全変換を実施。latest は deprecated になったので全変換の場合でも env を使用することが望ましい。
babel-preset-babili ES.next を minify する。標準では minify のみ。細かな設定は preset を構成する plugin 単位で指定。
babel-preset-react ReactJSXFlow などを変換する。Flow は babel-preset-flow という単体版もあるが、この preset はそれを組み込んでいる。このように preset は他の preset も含められる。

Babel の設定は .babelrc ファイルや package.jsonbabel プロパティに定義する。env と babili の場合、標準設定であれば

{
  "presets": ["babili"]
}

のように presets Array へ preset 名だけを String として記述。preset に含まれる pluguin を個別に設定するなら

{
  "presets": [["babili", {
    "mangle": {
      "blacklist": {
        "ParserError": true,
        "NetworkError": false
      }
    },
    "unsafe": {
      "typeConstructors": false
    },
    "keepFnName": true
  }]]
}

のように preset を String から Array へ書き換えて、第二要素の Object へ plugin 単位のプロパティを記述してゆく。

ここからは babili を構成する plugin とその設定についてまとめる。オプションについては README に型や既定値が掲載されていないため GitHub に公開されているコードも参考にした。

babel-plugin-minify-constant-folding

リテラル同士の演算を定数に変換する plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "evaluate": true
  }]]
}

例えば "a" + "b""ab" となり、"b" + a + "c" + "d" のように変数が含まれるなら "b" + a + "cd" という感じでそこをを避けてくれる。

babel-plugin-minify-dead-code-elimination

dead code (到達不能コード) を除去する plugin。babili 標準で有効、オプションは以下。

設定 既定値 内容
optimizeRawSize Boolean false README のサンプルに記載されているものの、設定値については解説されておらず用途不明。実装をみるとスコープと変数 bind に関する最適化の実行フラグになっている。
keepFnName Boolean false 元の関数名を維持する。
keepFnArgs Boolean false 関数の引数を維持する。
keepClassName Boolean false 元のクラス名を維持する。
{
  "presets": [["babili", {
    "deadcode": {
      "optimizeRawSize": false,
      "keepFnName": false,
      "keepFnArgs": false,
      "keepClassName": false
    }
  }]]
}

既定値のまま変換すると、例えば function foo() {var x = 1;}function foo() {} になる。関数のようにインターフェースとして露出している部分は維持しつつ、ローカルの範疇で解析可能な dead code を除去するようだ。

babel-plugin-minify-infinity

JavaScript のグローバルな Object である Infinity1 / 0; に変換する plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "infinity": true
  }]]
}

他のプラットフォームではゼロ除算を致命的なエラーとすることが多い。しかし JavaScript だと結果は Infinity になる。

という仕様を踏まえて Object から演算に変換していると思われるが、なぜこの処理が必要なのかは不明。Object を演算にすることで Infinity への代入を構文エラーとして検出したいとか、そんな目的があるのだろうか?

babel-plugin-minify-mangle-names

コンテキストやスコープを考慮して Name mangling (名前修飾) 処理を実行得する plugin。babili 標準で有効、オプションは以下。

設定 既定値 内容
blacklist Object {} 変換対象から除外する識別子の設定。例えば foo という名前を除外したい場合は {"foo":true} のように指定する。
eval Boolean false eval がアクセス可能な範囲で mangle を有効にする。
keepFnName Boolean false 元の関数名を維持する。
topLevel Boolean false 最上位スコープに対する mangle を有効にする。
keepClassName Boolean false 元のクラス名を維持する。
{
  "presets": [["babili", {
    "mangle": {
      "blacklist": {
        "foo": true
      },
      "eval": false,
      "keepFnName": false,
      "topLevel": false,
      "keepClassName": false
    }
  }]]
}

mangle 処理により変数、関数、クラス名などが aaa といった短い名前に変更される。結果、ファイル サイズの削減や単純な難読化といった効果を得られる。

babel-plugin-minify-numeric-literals

数値リテラルを可能な範囲で短縮する plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "numericLiterals": true
  }]]
}

例えば 10001e3 のように指数表記へ置き換える。実装をみると Number.prototype.toExponential() の結果から /\+/g/e0/ を空文字に置換したものとなり、変換結果が元の表記よりも長い場合はキャンセルされる。

babel-plugin-minify-replace

ユーザーが明示した設定に従って置換を実行する plugin。babili 標準で有効、オプションは以下。

設定 既定値 内容
replacements Array 置換対象コレクション。
[].identifierName String 置換対象とする識別子の名前。
[].replacement Object 置換方法。
[].replacement.type String 置換対象となる識別子の型。サンプルを見るに Babel types*Literal 系を指定するようだ。
[].replacement.value Any 置換する値。型は replacement.type に指定されたものと対応。
{
  "presets": [["babili", {
    "replace": {
      "replacements": [
        {
          "identifierName": "DEBUG",
          "replacement": {
            "type": "numericLiteral",
            "value": 0
          }
        }
      ]
    }
  }]]
}

識別子を直に置換することからマクロのようなメタ プログラミング、要するに他のプラットフォームでいうところの pre-processing を実現可能。uglify-js の --define オプションによる ifdef DEBUG 処理の代替になる。

babel-plugin-minify-simplify

ステートメントを式に変換してコードを短くする plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "simplify": true
  }]]
}

例えば if (x) a(); は短絡評価を利用して x && a(); になる。if (x) { a(); } else { b(); } は三項演算に展開され x ? a() : b(); となる。ただし圧縮率を高めるため undefinedvoid 0foo['bar']foo.bar に変換する処理はハック的なコードに対して副作用を及ぼすかもしれない。

babel-plugin-transform-merge-sibling-variables

ステートメントの別れた変数の宣言をひとつにまとめる plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "mergeVars": true
  }]]
}

例えば var a = 0; var b = 2;var a = 0, b = 0; に変換する。for 文の外で宣言され for 文内でしか使用されていない変数は for の宣言に移動される。

babel-plugin-transform-minify-booleans

Boolean リテラルをより短い表記に変換する plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "booleans": true
  }]]
}

true!0false!1 になる。短縮される文字数こそ少ないが、リテラルのみを対象とするため副作用の心配もない。

babel-plugin-transform-regexp-constructors

RegExp コンストラクターを正規表現リテラルに変換する plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "regexpConstructors": true
  }]]
}

例えば

const foo = 'ab+';
var a = new RegExp(foo+'c', 'i');

const foo = 'ab+';
var a = /ab+c/i;

になる。演算子 new も含めると RegExp インスタンス生成の表記はかなり長いため、これを使用しているなら相当の短縮が見込める。

babel-plugin-transform-remove-console

console.* の呼び出しを削除する plugin。babili 標準では無効、オプションなし。

{
  "presets": [["babili", {
    "removeConsole": false
  }]]
}

ライブラリーやフレームワークの場合、ユーザーが開発者なので残したほうがよい。アプリ層については plugin を有効にして余計な出力を除去、といった使い分けをする。

babel-plugin-transform-remove-debugger

debugger ステートメントを削除する plugin。babili 標準では無効、オプションなし。

{
  "presets": [["babili", {
    "removeDebugger": false
  }]]
}

私はこれまで使用したことはなかったのだが、この debugger ステートメントは Web ブラウザーの開発者ツールにおいてコード上に記述するブレーク ポイントとして機能するそうだ。コード上なので到達すると常にブレークする。JavaScriptのデバッグ方法 – JSを嫌いにならないためのTips によるとコールバック関数まわりのデバッグに役立つらしい。

同記事で「当然、本番用のコードに残しておきたいものではありません。」とあるとおり開発用の機能なので、アプリ層ならば babili で除去したほうがよい。

babel-plugin-transform-remove-undefined

変数への代入や戻り値の指定された undefined を削除する plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "removeUndefined": true
  }]]
}

例えば var b = undefined;var b;return undefined;return; になる。変数の場合、対象は関数内に限定されるため副作用の心配はない。

babel-plugin-transform-undefined-to-void

undefined 参照を void 0 に変換する plugin。babili 標準で有効、オプションなし。

{
  "presets": [["babili", {
    "undefinedToVoid": true
  }]]
}

前述の simplify でも同様の処理がおこなわれるのでは?と思ったが、あちらはステートメントを対象としている。こちらは foo === undefined;foo === void 0 のように参照を変換する。plugin の説明を読みと simplify で予想したとおり undefined の上書き事故を防ぐことが目的とのこと。

Electron 向け minify を uglify-js から babili に変更する

本記事の冒頭に紹介した前回記事では

  • babili の細かなカスタマイズには plugin 学習が必要なこと
  • package.json における dependencies の npm が minify されないこと

を課題として残した。これらのうち前者は今回の記事で対応してみた。基本、babili 標準で問題ないだろう。一方、後者については babili というより browserify + babelify という組み合わせの問題である。状況を把握しやすくするため処理遷移をまとめる。

  1. Browserify 実行
  2. Browserify が transpile のために babelify を実行
  3. babelify が presets の順番に従い JavaScript を transpile
  4. presets の babel-preset-env により ES.next が ES5 + α な JavaScript へ transpile
  5. presets の babel-preset-babili により ES5 + α な JavaScript を minify
  6. Browserify が transpile された JavaScript 内の require を解決して単一ファイルに bundle (結合)

uglify-js と異なり babili は babelify から実行されるため、この時点では dependencies の npm は対象外である。よってアプリ側のファイルだけ transpile + minify され、その後に dependencies 側がそのまま bundle されてしまう。React のように巨大な npm だとファイル サイズ的に問題となるし NODE_ENV=production を経ていないためデバッグ用のコードも残る。

そのため

  1. Browserify 以降に minify を実行する
  2. babelify 時点でなんとか dependencies も対象とする

として 1 はビルド プロセスが複雑になる。これをおこなうぐらいなら babel-preset-env を全変換で実行して uglify-js するほうがよい。または前回記事で触れた uglify-js harmony 版を使用するとか。2 については babili の対象を node_modules まで広げる必要がある。しかし Babel は bundler にあらず、参照まで辿って処理はしない。

詰んだかも?しかし React に限定すれば npm に minify 版も付属するため babili を利用して参照を書き換えてしまえばよさそう。というわけで

{
  "presets": [["babili", {
    "replace": {
      "replacements": [
        {
          "identifierName": "react",
          "replacement": {
            "type": "stringLiteral",
            "value": "react/dist/react.min"
          }
        },
        {
          "identifierName": "DEBUG",
          "replacement": {
            "type": "numericLiteral",
            "value": 0
          }
        }
      ]
    }
  }]]
}

この設定により reactreact/dist/react.min に書き換えようとしたのだが失敗した。identifierNamevalue の内容を ' で囲って typeidentifier を指定してもダメだった。replace のテストは文字列リテラルの書き換えがないため、そもそもサポートしていないのだろう。

Browserify の exclude/require

では env や babili とは別に import 対象を変更する plugin はないか?と探してみたら babel-plugin-transform-rename-import を見つけた。

"plugins": [
  [
    "transform-rename-import",
    {
      "original": "react",
      "replacement": "react/dist/react.min"
    }
  ]
]

そして上記のように設定して実行してみたが、置換元と同じ名前が置換先に含まれていると重複変換されるようで

Error: Cannot find module 'react/dist/react/dist/react' from '.../examples-electron/audio-player/src/js/renderer/main'

というエラーになってしまった。仕方ないので Browserify として exclude + require することに。以下はそのコマンド。

cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron --exclude react --exclude react-dom --require ./node_modules/react/dist/react.min.js:react --require ./node_modules/react-dom/dist/react-dom.min.js:react-dom -o ./dist/src/assets/renderer.js

めちゃくちゃ長くなってしまった。この処理で生成した renderer.js について

  • class など ES2015 以降の予約語が維持されること
  • Electron アプリとして動作すること

を確認したのだが、当然ながら Browserify の bundle 用コードは minify されない。そのためファイル サイズが肥大化した。

env minify size
標準 (latest 相当) uglify-js 約 320KB
Electron babili 約 400KB

dependencies をまったく使わないか React のように重量級の npm を使用しないなら babili でもよさそう。Electron は Node として require できるから bundler なしにする手もある。ただし私が bundler を使用するにはアプリの配布イメージから node_modules を除外する目的もあるため、この案は却下せざるをえない。

babel-cli + babili

そもそも transpile と bundle が分離しているのだから、uglify-js のようにビルド処理の最後に minify を実装するのはどうか?ということで処理を組み替えてみた。package.json から必要最小の抜粋。babili の設定は長くなるのでとりあえず標準。

{
  "babel": {
    "presets": [
      ["env", {"targets": {"electron": 1.6}}],
      "react"
    ],
    "env": {
      "production": {
        "presets": ["babili"]
      }
    }
  },
  "scripts": {
    "bundle": "browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron --exclude react --exclude react-dom --require ./node_modules/react/dist/react.min.js:react --require ./node_modules/react-dom/dist/react-dom.min.js:react-dom -o ./dist/src/assets/renderer.js",
    "minify": "cross-env NODE_ENV=production babel ./dist/src/assets/renderer.js -d ./",
    "build": "run-s bundle minify"
  },
  "dependencies": {
    "react": "^15.4.2",
    "react-dom": "^15.4.2"
  }
  "devDependencies": {
    "babel-cli": "^6.24.0",
    "babel-preset-babili": "0.0.12",
    "babel-preset-env": "^1.3.2",
    "babel-preset-react": "^6.23.0",
    "babel-register": "^6.24.0",
    "babelify": "^7.3.0",
    "browserify": "^14.1.0",
    "cross-env": "^3.2.4",
    "npm-run-all": "^4.0.2"
  }
}

bundle は env による ES.next の transpile と Browserify の bundle だけを実行。

minify で bundle 結果となるファイルを minify して上書き保存。中間ファイルは不要。bundle 対象に dependencies が含まれていた場合を考慮して NODE_ENV=production を指定、npm によってはこの判定によりデバッグ処理を dead code化して除去されることに期待した設計となっている。

これらを buildnpm-run-all により直列実行する。結果、400KB まで肥大化したファイル サイズは約 318KB になって uglify-js より縮小された。Browserify で React 系を exclude/require しない、つまり babili で minify するようにしてみたのだが、めちゃくちゃ時間がかかったのとサイズが 400KB 超えしたので却下。

この方法で minify したファイルでアプリが正常に動作することも確認済み。

まとめ

ES.next に対応した minify ツールとして babili を試した。現時点のバージョンは v0.0.12 なので本番プロダクトに採用するのは不安があるものの、Babel ファミリーということもあって uglify-js 代替となることを期待している。

ES.next の transpiler としては babel-preset-env が本命となるだろう。しかし代表的な Web ブラウザー全種と Node が Modules 対応するのは相当に先の話となり、特に Web ブラウザー向けには Browserify や webpack などの bundler を採用することになるだろう。よって完全な minify を実行するためには transpile/bundle を経たコードを対象としなければならない。そのため uglify-js、babili のどちらも bundler と組み合わせるならビルドの最終工程で実行したほうがよい。

最後に用途別 minify 戦略をまとめる。

  • 常に最新 ES.next を利用してあらゆる環境に対応したい
    • env は標準設定、つまり latest にする
    • ES.next はすべて ES5 相当に変換
    • 環境によっては無駄な変換も含まれるが許容する
    • 変換結果に ES.next が含まれないため minify は実績のある uglify-js 通常版を採用
  • 対象環境を明示してなるべく無駄な変換をなくしたい
    • env に対象環境を設定
    • ES.next は設定に応じて最小の ES5 変換
    • 変換結果に ES.next が含まれるため minify は babili か uglify-js harmony 版の二択

私としては現時点なら env 標準 + uglify-js を選ぶ。まだ babili は実績と品質面で不安がある。それと uglify-js harmony 版のなりゆきを見て判断したい。

npm 開発で再び Babel を導入することにした

$
0
0

以前、npm 開発で脱 Babel してみるという記事を書いた。そして二ヶ月ほど特に問題なく運用できていたのだが、babel-preset-env を試してみたら考えが変わった。

脱 Babel を決めた時点では latest で常時 ES5 変換か plugin を細かく組み合わせることを想定していた。しかし babel-preset-env なら明示的に Node のバージョンを指定することで必要最小の変換をおこなえる。Node としては ES Modules 以外の ES.next 仕様へ積極対応しているため、Active な LTS を下限としておけば変換は ES Modules + α ぐらいで済む。

というわけで Babel を再導入することにした。

プロジェクト構成

Babel を再導入した akabekobeko/npm-xlsx-extractor の構成例。

.
├── package.json
├── examples/
├── dist/
└── src/
    ├── bin
    └── lib
名前 内容
package.json プロジェクト設定ファイル。
dist/ リリース用ディレクトリ。ビルドによって動的生成される。.gitignore 対象。
src/ 開発用ディレクトリ。
src/bin npm を CLI として実行した時のコードを格納するディレクトリ。
src/ npm を Node として実行した時のコードを格納するディレクトリ。

よくある構成だとこれに mocha などのユニット テストを格納するため test/ があるものだけど、Babel 再導入にあたりテストは対象となるコードのあるディレクトリに併置した。

プロジェクト設定

プロジェクト設定はすべて package.json に定義。必要最小の内容を抜粋する。

{
  "engines": {
    "node": ">= 6"
  },
  "main": "dist/lib/index.js",
  "bin": "dist/bin/index.js",
  "files": ["dist"],
  "esdoc": {
    "source": "./src",
    "destination": "./esdoc",
    "test": {"type": "mocha", "source": "./src"}
  },
  "babel": {
    "presets": [
      ["env", {"targets": {"node": 6}}]
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register src/**/*.test.js",
    "start": "npm run watch",
    "esdoc": "esdoc",
    "eslint": "eslint ./src",
    "build": "babel src --out-dir dist --ignore *.test.js,typedef.js",
    "watch": "babel src --out-dir dist --ignore *.test.js,typedef.js --watch",
    "prepare": "npm run build"
  },
  "devDependencies": {
    "babel-cli": "^6.24.1",
    "babel-preset-env": "^1.3.3",
    "babel-preset-power-assert": "^1.0.0",
    "babel-register": "^6.23.0",
    "esdoc": "^0.5.2",
    "eslint": "^3.15.0",
    "eslint-config-standard": "^6.2.1",
    "eslint-plugin-promise": "^3.4.2",
    "eslint-plugin-standard": "^2.0.1",
    "mocha": "^3.2.0",
    "power-assert": "^1.4.2"
  }
}

順に解説する。

ビルド設定

babel-preset-env を "node": 6 にする。2017/4/1 をもって Node v4 LST は Maintenance になったので、Active な v6 LTS を下限としている。すべての dependencies が v4 対応しているなら Babel がよしなに変換してくれるので "node": 4 にしてもよい。

Babel 再導入を機にコードを ES2015 以降の仕様で書き直した。Node は v6 で大半の ES2015 仕様に対応したので大きな変換は Modules と CommonJS 変換ぐらいとなる。ビルド関係の npm-scripts は以下。

{
  "scripts": {
    "build": "babel src --out-dir dist --ignore *.test.js,typedef.js",
    "watch": "babel src --out-dir dist --ignore *.test.js,typedef.js --watch"
  }  
}

ユニット テストは src/ 内で対象となるコードと併置している。また ESDoc 用に class や module として定義されないものを記述した typedef.js がある。これらは Babel 変換する意味がないので bbel-cli の --ignore オプションで除外。コード検証は基本的にユニット テストを利用するが、npm link で CLI の実操作を試しやすくするために watch タスクを定義している。

ビルド結果の出力先は dist/ にしている。以前はプロジェクト直下に bin/lib/ を生成していたが、現時点の npm react 構造を参考に変えた。node_modules の中身を気にするユーザーはそれほどいないし package.jsonmainbin を明示できるのだから階層が深くなっても問題ないはず。

注意点がひとつ。過去に Babel で変換していた時もそうしていたのだが、export default class XlsxExtractor が CommonJS 化されると exports.default = XlsxExtractor; になり、これをそのまま require した場合、constructor を利用できなくなる。この問題を回避するためには

import XlsxExtractor from './xlsx-extractor.js'
module.exports = XlsxExtractor

という仲介処理を定義し、それを npm の main にエントリー ポイント指定する。このファイルは以下のように変換され、

'use strict';

var _xlsxExtractor = require('./xlsx-extractor.js');

var _xlsxExtractor2 = _interopRequireDefault(_xlsxExtractor);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

module.exports = _xlsxExtractor2.default;

通常の require で読んだ状態と等しくなる。Babel 変換対象の範囲内であればうまく解決されるのだが、npm としてエントリー ポイントを提供する場合は忘れずに対応しておく。

私はこれを忘れてリリースして examples が動かず冷や汗をかいた。

ユニット テスト、ESLint、ESDoc

wip-testable-js.md の影響でテスト用コードと対象を併置。具体的には以下となる。

.
├── bin
│   ├── cli.js
│   ├── cli.test.js
│   └── index.js
└── lib
    ├── index.js
    ├── xlsx-extractor.js
    ├── xlsx-extractor.test.js
    ├── xlsx-util.js
    └── xlsx-util.test.js

もともと Atom などテキスト エディター上でテストと対象コードを同時にタブ表示しても区別しやすくするため a.js のテストは a.test.js と命名していた。そのため併置によるファイル名の競合は発生しない。またきっかけとなった gist にも解説されているとおり

  • テストから対象への相対参照パスが短縮される
    • 併置されているため '../../src/lib/a.js'./a.js と書ける
    • ディレクトリ構造を変更してもテストと対象の併置を維持すればパス変更しなくて済む
  • テストと対象の距離が近い
    • これ以上ない近さ
    • 往復が容易なのでテストを書いたり内容を確認する負担が軽減される
  • テストの有無を視認しやすい
    • *.test.js の併置を確認すればよい
    • なければテストなしと判断できる

といったメリットがある。あらゆるコードが src 内に集約される。よってユニット テスト、ESLint、ESDoc もこのディレクトリだけを対象にすればよい。

{
  "esdoc": {
    "source": "./src",
    "destination": "./esdoc",
    "test": {
      "type": "mocha",
      "source": "./src"
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register src/**/*.test.js",
    "esdoc": "esdoc",
    "eslint": "eslint ./src"
  }
}

あと ESDoc で CommonJS をサポートするためには esdoc-node が必要だが ESDoc 公式 plugin ではないため ESDoc Hosting Service 上で動かせない。この点については作者の @h13i32maru さんと Twitter 上で相談して plugin の公式化を検討していただいている。しかし今回の対応で Babel 再導入を前提にコードを ES.next で書き直した。よって esdoc-node は不要となり、そのまま ESDoc Hosting Service を利用できる。

publish

Babel 再導入により npm を publish する際にビルドが必要となる。これは以前、prepublish で定義していたが npm v4 から deprecated になったので prepare を利用するように修正。

{
  "main": "dist/lib/xlsx-extractor.js",
  "bin": "dist/bin/index.js",
  "files": ["dist"],
  "scripts": {
    "build": "babel src --out-dir dist --ignore *.test.js,typedef.js",
    "prepare": "npm run build"
  }  
}

ビルド結果を dist にしているため、files の内容がスッキリした。

まとめ

対象 Node に応じたコード修正コストを babel-preset-env に丸投げできるのは便利だ。もっと早く知っていれば脱 Babel しなかったかもしれない。依然として変換品質の懸念はあるものの、自前で Node の Release note や ECMAScript 6 compatibility table を調べて対応するよりもはマシなはず。

他の npm も Babel 再導入とユニット テスト併置などを反映させてゆく予定。

併置といえば Web フロント エンド開発だと CSS Modules により View コンポーネントと CSS の関係も見直されている。これも面白い潮流だ。React の JSX において関心と技術の分離は異なるという話があった。ユニット テストや CSS も対象と密なら近接するのが自然である。

Transpiler や Bundler を前提とすることで JavaScript における動作環境や技術の分離に関する問題は解決されつつあり、より本質的な関心事にもとづいて設計できる時代になったと感じる。

Electron を試す 9 – Babel 変換を最小におさえつつ minify

$
0
0

小ネタ。

Electron が採用している Chromium は ECMAScript 対応がかなり進んでいる。よって Babel を使用しつつも変換を最小におさえたくなる。

この点について以前 babel-preset-env と minify という記事を書いたのだが、uglify-js の ES2015 以降への対応が暫定版なため、よりよい選択肢として babel-preset-babili を試してみた。その記事のコメントで mysticatea さんが提案されているように Browserify へ -g オプションをつければ node_modules 部分も含めて minify 可能だが、それでも Browserify の Bundle 処理は minify されない。

よって uglify-js harmony 版が正式リリースされるのを待っていたところ、uglify-es が提供されたので試してみる。

uglify-es

従来 uglify-js で harmony と呼ばれていた ES2015 以降へ対応する仕向け。README によれば uglify-js@3.x 系に対して API と CLI 互換がある。CLI 名も uglifyjs のまま。よって最新の uglify-js を使用しているなら、特に処理を変えず移行可能。

移行は package.jsondevDependencies で uglify-js を uglify-es へ置き換えればよい。uglify-js@3.x から提供となるので、バージョン系は 3.x 以降となる。安全のため npm un -D uglify-js してから npm i -D uglify-es するのがよいだろう。

余談だが Support UglifyJS 3 と関連 issue にて webpack の uglify-es 対応が検討されている。難航しているようだ。

babel-preset-env

uglify-es により ES2015 以降を解析可能となるため、Babel 変換も最小にする。babel-preset-env へ開発で使用している Electron のバージョンを指定。以下は package.json の例。.babelrc なら "babel" プロパティの値をルートにする。

"babel": {
  "presets": [
    [
      "env",
      {
        "targets": {
          "electron": "1.6"
        }
      }
    ]
  ]
}

この設定で akabekobeko/examples-electron のプロジェクトをビルドして classconstlet といった ES2015 以降の機能はそのままに uglify-es で minify されることを確認できた。

問題点

uglify-es は ES2015 なら対応しているけれど async/await を含むコードはエラーになる。試しに async function – JavaScript | MDN のサンプルを含めてみたところ、

Parse error at 0:2932,6
async function add1(x) {
      ^
ERROR: Unexpected token: keyword (function)
    at JS_Parse_Error.get (eval at <anonymous> (.../examples-electron/audio-player/node_modules/uglify-es/tools/node.js:21:1), <anonymous>:86:23)
    at fatal (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:286:52)
    at run (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:241:9)
    at Socket.<anonymous> (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:179:9)
    at emitNone (events.js:110:20)
    at Socket.emit (events.js:207:7)
    at endReadableNT (_stream_readable.js:1045:12)
    at _combinedTickCallback (internal/process/next_tick.js:102:11)
    at process._tickCallback (internal/process/next_tick.js:161:9)

となった。ちなみに Browserify も以前は es6 async class function fails to parse? という問題があったが修正済み。uglify-es の issue [ES8] async/await not supported also in harmony を読むに、近く修正されそうな気がする。

まとめ

async/await 対応の問題はあるものの、babel-preset-env + uglify-es の組み合わせで Electron 向けの Babel 変換を最小におさえられた。

Chrome Canary 60 はフラグ付きで ES Modules が有効になるそうで、これが Electron に採用されたら Bundle と minify 事情も変わりそう。しかし node_modules も含めたサイズ圧縮の観点から Bundle と minify はしばらく必要な処理なので、今後も動向は継続的にチェックしたい。

Viewing all 74 articles
Browse latest View live