思わず作りたくなる!初めてでもイケてるJSライブラリーを開発するコツ

2016/07/06

Tim Severien

21

Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

毎回使うあのJSのコード、ちゃんとまとめておくと便利じゃない? いつもは「ライブラリーは使うもの」という人もちょっと気にしておきたい、ライブラリー自作のコツをまとめました。

本記事はAdrian SanduVildan SofticDan Princeが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者の皆さんに感謝します。

ライブラリーとはなんでしょうか? ライブラリーは私たちがいつも利用している、開発者がプロジェクトで利用できるコードをまとめたパッケージのことです。ライブラリーを利用すれば、作業の手間を確実に省き、一から作業をする必要がなくなります。パッケージを再利用すれば、オープンソースかクローズソースかに関係なく、同じ機能を再構築したり、または過去のプロジェクトを人力でコピー&ペーストをするよりも効率的です。

では、ライブラリーはコードをパッケージする以外にどんな機能があるのでしょうか。一部の例外を除いて、ライブラリーは常に1つ、または数種類のファイルを単一のフォルダにまとめたものであるべきです。あなたのプロジェクトでライブラリーのコードを利用する場合、プロジェクトとは別に維持されるべきであり、手を加えるべきではありません。

また、ライブラリーは、プロジェクト固有の設定や動作の設定をできるようにする必要があります。USBポートを介してのみ通信ができるUSBデバイスのようなものだと考えれば分かりやすいでしょう。マウスやキーボードなどのデバイスの一部は、対応するインターフェイスを通して設定ができるようになっているものもあります。

この記事では、ライブラリーの作成方法を説明します。紹介するトピックのほとんどは別の言語にも対応していますが、JavaScriptライブラリーの作成に主眼を置いています。

なぜ独自のJavaScriptライブラリーを作成するのか?

ライブラリーは既存のコードを簡単に再利用できる仕組みです。過去のプロジェクトを探してファイルをコピーする必要がなくなり、ライブラリーからデータを引っ張ってくれば良いだけです。また、アプリケーションを分割して断片化し、小さなアプリケーションのコードを維持することで、よりメンテナンスしやすくなります。

特定の同じような目的を持つすべてのコードを抽象化してライブラリーにまとめることで、再利用がしやすくなるわけです。興味深い例に、jQueryがあります。現在ではjQueryのAPIよりも簡易化されたDOM APIはありますが、数年前までクロスブラウザーのDOM操作がまだかなり困難なことでした。

オープンソースブロジェクトが人気となり、より多くの開発者が利用するようになれば、問題点を申告したりコードベースの開発に貢献したりと、多くの人が参加し、協力してくれるようになるでしょう。いずれにしても、利用者の増加はライブラリーとすべてのプロジェクトの利益となります。

あなたのオープンソースプロジェクトが人気となれば、それは大きなチャンスにもなります。企業があなたの仕事に感動して新たな仕事を依頼してくるかもしれません。おそらく企業は、あなたのオープンソースプロジェクトを会社のアプリケーションに統合するために助けてほしいと頼むでしょう。ライブラリーを開発してまとめた人以上に、そのライブラリーを理解している人はいないのですから。

オープンソースのプロジェクトは、コードを書くのを楽しむ趣味の一環だったり、誰かを助けたりすることに過ぎませんが、その課程で学んだことはあなたの成長につながります。ぜひ限界を決めずに、新しいことに挑戦してください。

目的と目標

コードを書く前に、ライブラリーの作成目的をはっきりするべきです。つまり、目標設定が必要です。ライブラリーを利用してどんな問題を解決したいのかを集中して考えます。あなたのライブラリーは、問題を一から解決するよりも、扱いやすく、覚えやすいものにすべきです。ユーザーは、APIがシンプルになればなるほどライブラリーの使い方を学びやすくなります。UNIXの哲学にある、

1つのことを行い、またそれをうまくやるプログラムを書け。

と自分の胸に聞いてみましょう。ライブラリーはどのような問題を解決しますか? どうやって問題を解決するつもりですか? 自分でライブラリーの目的に関係することをすべてを書き出してみるか、またはほかの人の作成したライブラリーを使ってみてください。

ライブラリーの規模に関わらず、ロードマップを作成するようにしてください。欲しいと思う機能をすべて一覧にして書き出してください。そして、最小実行可能なライブラリーまで、できるだけ要素は削ぎ落としてください。それが最初のリリースとなります。ここから新しい機能を作成していくのです。すべての機能で大きな達成感を感じながら、楽しめるように、プロジェクトを小さい単位に分割しましょう。必ず効果がありますから、私を信じてやってみてください。

APIのデザイン

個人的には、エンドユーザーの視点から自分のライブラリーを見るようにしたいと考えています。ユーザー中心のデザインです。要するに、そのライブラリーを選んで使う誰もが便利になるように考え、ライブラリーの構想を練るわけです。同時に、どの部分をカスタマイズできるようにするか考える必要があります。これについては、後半で取り上げます。

究極のAPIの品質テストは、自分のプロジェクトで自分のライブラリーを使うことです。試しに自分のアプリのコードをライブラリーのコードと入れ替えて、使いたい機能がすべて利用できるか確認してください。カスタマイズして(後半で取り上げます)、特殊な事例にもぎりぎり対応できる柔軟性を残しつつ、できる限り簡潔にするように心がけてください。

次の例は、ユーザーエージェント文字列ライブラリーのアウトラインです。

// Start with empty UserAgent string
var userAgent = new UserAgent;

// Create and add first product: EvilCorpBrowser/1.2 (X11; Linux; en-us)
var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
application.setComment('X11', 'Linux', 'en-us');
userAgent.addProduct(application);

// Create and add second product: Blink/20420101
var engine = new UserAgent.Product('Blink', '20420101');
userAgent.addProduct(engine);

// EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101
userAgent.toString();

// Make some more changes to engine product
engine.setComment('Hello World');

// EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101 (Hello World)
userAgent.toString();

ライブラリーの複雑さによっては、構造を工夫したいと考えるかもしれません。ライブラリーの構築にあたって、さまざまなデザインパターンを利用するのは、技術的問題を解決する上でも良い方法です。新しい機能を追加するときに、大部分をリファクタリングすることになるリスクも軽減できます。

柔軟性とカスタマイズ

ライブラリーがすばらしいのは柔軟性ですが、カスタマイズできるものとできないものの線引きは困難です。その良い例が、chart.jsD3.jsです。どちらもデータを可視化する優れたライブラリーです。Chart.jsを使えば異なったタイプの組み込みチャートを作成でき、スタイルの変更もとても簡単です。しかし、グラフィックスをより詳細にコントロールする必要がある場合は、D3.jsをおすすめします。

ユーザーにコントロールを渡すにはさまざまな方法があります。コンフィグレーション、パブリックメソッドを提供する、コールバックやイベントの利用などです。

ライブラリーはたいていの場合、初期化中に設定されますが、ライブラリーによっては実行中にオプションの変更が可能です。オプションは通常、ちょっとしたことに限られており、これらのオプションを変更しても今後利用していくオプションの値が更新されるだけです。

// Configure at initialization
var userAgent = new UserAgent({
  commentSeparator: ';'
});

// Run-time configuration using a public method
userAgent.setOption('commentSeparator', '-');

// Run-time configuration using a public property
userAgent.commentSeparator = '-';

インスタンスとの情報のやりとりにメソッドが利用される可能性があります。たとえば、インスタンスからデータを取得する(getter)、データをインスタンスに送信する(setter)、アクションを実行するため、などです。

var userAgent = new UserAgent;

// A getter to retrieve comments from all products
userAgent.getComments();

// An action to shuffle the order of all products
userAgent.shuffleProducts();

コールバックはパブリックメソッドを利用して渡される場合があり、非同期タスクののちにユーザーコードを実行します。

var userAgent = new UserAgent;

userAgent.doAsyncThing(function asyncThingDone() {
  // Run code after async thing is done
});

イベントは大きなポテンシャルを秘めています。コールバックと似ています。ただし、イベントハンドラーの追加でアクションが起きない場合は除きます。おそらく推測通りだと思いますが、イベントは来事が起きることを指し示すために利用されます。コールバックと同様に、追加情報を提供し、ライブラリーの値を返します。

// Validate a product on addition
userAgent.on('product.add', function onProductAdd(e, product) {
  var shouldAddProduct = product.toString().length < 5;

  // Tell the library to add the product or not
  return shouldAddProduct;
});

場合によっては、ユーザーがライブラリーを拡張できるようにしたい場合もあります。その場合は、Angularモジュール(angular.module('myModule'))とjQueryのfnjQuery.fn.myPlugin)のように、ユーザーがデータを追加できるパブリックメソッドまたはプロパティを提供するか、もしくは何もせずに単にユーザーがライブラリーのネームスペースにアクセスできるようにします。

// AngryUserAgent module
// Has access to UserAgent namespace
(function AngryUserAgent(UserAgent) {

  // Create new method .toAngryString()
  UserAgent.prototype.toAngryString = function() {
    return this.toString().toUpperCase();
  };

})(UserAgent);

// Application code
var userAgent = new UserAgent;
// ...

// EVILCORPBROWSER/1.2 (X11; LINUX; EN-US) BLINK/20420101
userAgent.toAngryString();

同様に、メソッドのオーバーライトもできます。

// AngryUserAgent module
(function AngryUserAgent(UserAgent) {

  // Store old .toString() method for later use
  var _toString = UserAgent.prototype.toString;

  // Overwrite .toString()
  UserAgent.prototype.toString = function() {
    return _toString.call(this).toUpperCase();
  };

})(UserAgent);

var userAgent = new UserAgent;
// ...

// EVILCORPBROWSER/1.2 (X11; LINUX; EN-US) BLINK/20420101
userAgent.toString();

後者の場合、ユーザーにライブラリーのネームスペースを利用できるようにすると、拡張機能やプラグイン定義に関して管理権限が弱くなります。拡張機能が従来からの規則から逸脱しないようにするには、ドキュメント化すべきです。

テスト

テスト駆動開発を始める場合、アウトラインの作成から始めると良いです。つまり、テストの形で基準となるものを書き出してから実際にライブラリーを書き出します。また、テストで機能が本来あるべきように動作するか、テストを書き出してからライブラリーを作成しているかをチェックする場合は、ビヘイビア駆動開発(振舞駆動開発)と呼ばれます。どちらを利用するにせよ、テストでライブラリーのすべての機能を確認でき、コードがすべてのテストに合格した場合は、ライブラリーが確実に機能すると言えます。

Jani HartikainenがMochaを使って単体テストを書き出す方法を『Unit Test Your JavaScript Using Mocha and Chai』で紹介しています。『Testing JavaScript with Jasmine, Travis, and Karma』で、Tim EvkoがJasmineと呼ばれる別のフレームワークを利用して使い勝手の良いテストパイプラインの作り方を解説しています。これら2つのテストフレームワークは非常に人気がありますが、ほかにもさまざまな特徴あるフレームワークがあります。

記事の前半で作成したアウトラインの期待される結果にはすでにコメント済みです。すべてのテストは「期待」から始まります。私が作成したライブラリーをJasmineでテストすると以下のようになります。

describe('Basic usage', function () {
  it('should generate a single product', function () {
    // Create a single product
    var product = new UserAgent.Product('EvilCorpBrowser', '1.2');
    product.setComment('X11', 'Linux', 'en-us');

    expect(product.toString())
      .toBe('EvilCorpBrowser/1.2 (X11; Linux; en-us)');
  });

  it('should combine several products', function () {
    var userAgent = new UserAgent;

    // Create and add first product
    var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
    application.setComment('X11', 'Linux', 'en-us');
    userAgent.addProduct(application);

    // Create and add second product
    var engine = new UserAgent.Product('Blink', '20420101');
    userAgent.addProduct(engine);

    expect(userAgent.toString())
      .toBe('EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101');
  });

  it('should update products correctly', function () {
    var userAgent = new UserAgent;

    // Create and add first product
    var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
    application.setComment('X11', 'Linux', 'en-us');
    userAgent.addProduct(application);

    // Update first product
    application.setComment('X11', 'Linux', 'nl-nl');

    expect(userAgent.toString())
      .toBe('EvilCorpBrowser/1.2 (X11; Linux; nl-nl)');
  });
});

初版のAPIデザインに十分満足できたら、次はアーキテクチャやライブラリーの利用法について考えます。

モジュールローダーの互換性

開発しているライブラリーにモジュールローダーを使うかどうか分かりませんが、あなたのライブラリーを選ぶ開発者がモジュールローダーを使う可能性はあります。従って、作成するライブラリーがモジュールローダーと互換性があるようにします。しかし、CommonJS、RequireJS、AMDなど、どのモジュールローダーでしょうか? もっとほかのモジュールにするかなど、どうやって選べば良いのでしょうか?

実際のところ、選択の必要性はありません! 複数のモジュールローダーをサポートする目的で作られた方法、Universal Module Definition(UMD)があるからです。ネット上にはさまざまな種類のスニペットがありますが、UMD GItHubリポジトリにもいろいろな種類のスニペットがあり、ライブラリーにUMDとの互換性を持たせられます。テンプレートを使ってライブラリー作成を開始するか、または好みの作成ツールを使ってUMDを追加してください。モジュールローダーの問題を回避できます。

ES2015のimport/exportシンタックスを利用する場合、BabelのUMDプラグインと併せてBabelを使ってES5にコンパイルすることを強くおすめします。ES2015をプロジェクトで利用でき、かつモジュールローダーに対応したライブラリーを作成できるからです。

ドキュメント化

個人的には、すべてのプロジェクトをドキュメント化することに賛成ですが、ドキュメント化には労力がかかるので後回しになり、最終的に忘れ去られがちです。

基本的な情報

ドキュメントは常にプロジェクトの名前や説明のような基本的な情報から始めるべきです。そうすれば、ほかの人がライブラリーでできることを理解しやすくなり、自分の目的に一致するかどうかを判断しやすくなります。

目的や目標のような追加情報を提供することで、ユーザーにより多くの情報とロードマップを提示でき、ユーザーは今後に期待できること、またはどのように貢献できるかが分かります。

API、チュートリアル、サンプル

もちろん、ユーザーにライブラリーの使い方を知ってもらう努力は必要です。そのためには、APIのドキュメント化から始めてください。チュートリアルやサンプルも加えるとなお良いですが、労力が必要になります。しかし、Inline documentationを使えば少ない労力で済みます。JSDocでコメントが構文解析され、ドキュメントページへ変換されます。

メタタスク

ライブラリーに変更を加えたいユーザーもいます。ほとんどの場合、ライブラリーの作成に貢献したいというのがその理由ですが、個人用にカスタマイズしたいと考えているユーザーもいます。そのようなユーザーのために、ライブラリーの作成、テストの実行、データの生成、変換またはダウンロードするためのコマンドを一覧にする形でドキュメントをメタタスクに入れることをお勧めします。

貢献者

ライブラリーをオープンソースにした場合、貢献者が現れるのがすごいところです。そのような貢献者への案内として、ライブラリー作成に対して貢献のステップや満たすべき基準を説明する文書を追加します。ライブラリーに貢献できる内容を検討、受諾できるようになるので、貢献者が適切に作業を進めるのがずっと簡単になります。

ライセンス

大事なことを言い忘れていましたが、ライセンスを入れるようにしてください。厳密には、ライセンスを入れなかった場合も著作権は保護されます。これはあまり知られていないことですので注意してください。

法律の専門家でなくてもライセンスを選択するのに適したリソースとして、ChooseALicense.comを見つけました。ライセンスを選択してからプロジェクトのルートにあるLICENSE.txtファイル内にテキストを保存すれば良いだけです。

終わりに添えたいこと

良いライブラリーにはバージョン付与が欠かせません。ライブラリーに新たな変更を加えたら、ユーザーはおそらく自分のデバイスに対応した最新バージョンのライブラリーを使いたいと考えるでしょう。

現在、バージョン名の事実上の基準は、Semantic VersioningまたはSemVerです。SemVer版は3つの数字からなり、それぞれが大・小・パッチと異なった変更を意味します。

Gitリポジトリへのバージョン・リリース情報の追加

Gitリポジトリを持っていれば、リポジトリにバージョン番号を追加できます。リポジトリのスナップショットのようなものだと考えれば良いです。これをタグと呼んでおり、タグを作成するにはターミナルを開き、以下のようにタイプしてください。

# git tag -a [version] -m [version message]
git tag -a v1.2.0 -m "Awesome Library v1.2.0"

GitHubなど多くのサービスは、すべてのバージョンとそれぞれのダウンロードリンクの概要を提供します。

共通リポジトリへの公開

npm

プログラミング言語には通常、対応するパッケージマネジャーまたはサードパーティ製のパッケージマネジャーがあります。これらのマネジャーを使うと、言語専用のライブラリーを利用できます。例えばRubyなら、PHPのComposerRubyGemsです。

Node.jsはスタンドアローン型のJavaScriptエンジンで、対応するnpmがあります。npmに馴染みがなくても、分かりやすい入門ガイドがありますので心配しなくても大丈夫です。

デフォルトでは、npmパッケージは公開されます。でも心配いりません。プライベートパッケージを利用してプライベートレジストリーを作成する、もしくは一切公開しないこともできます。

パッケージを公開するには、プロジェクトにpackage.jsonファイルが必要です。手作業かインタラクティブウィザードでも使えます。ウィザードを開始するのには、以下のようにタイプします。

npm init

versionプロパティはGitタグに適合しているはずです。また、必ずREADME.mdファイルを用意してください。GitHubと同様に、npmはパッケージを表示しているページに利用します。

以下のようにタイプし、パッケージを公表します。

npm publish

これで終わりです。npmパッケージの公開完了です。

Bower

数年前に、Bowerと呼ばれる新たなパッケージマネジャーが登場しました。このパッケージマネジャーは特定の言語向けというよりも特定のプラットホーム、Web向けに作られたものです。このマネジャーを使えば、主要な最先端のアセットをすべて見つけられます。ライブラリーがブラウザーに対応している場合にのみ、Bower上でパッケージを公開するのを楽しめるでしょう。

Bowerに馴染みがない人向けに入門ガイドを用意しています。

npmと同様に個人のリポジトリも作成可能です。ウィザードで完全非公開にもできます。

興味深いことに、ここ1、2年は、多くの人がフロントエンドのアセットを求めてnpmに乗り換えているようです。npmパッケージは基本的にはJavaScriptですが、フロントエンドのパッケージがたくさんnpm上で公開されています。どちらにしても、Bowerは依然として人気なので、パッケージをBower上でも公開することを強くおおすめします。

Bowerはnpmのモジュールであり、そもそもはnpmの影響を受けています。コマンドもほぼ同じものです。bower.jsonファイルを作成するには、以下のようにタイプしてください。

bower init

コマンドはnpm initと同様で、説明は不要でしょう。最後に、パッケージを公開するには次を参照してください。

bower register awesomelib https://github.com/you/awesomelib

以上のような手順でライブラリーを公開し、誰もが自分たちのNodeプロジェクトやWeb上で公開したライブラリーを利用できるようになります。

最後に

ライブラリーはコアとなるプロダクトです。問題を解決し、使いやすく安定したライブラリーの作成を目指してください。そうすれば、チームメンバーや多くの開発者が喜んでくれるはずです。

数多くのタスクを紹介しましたが、これらのタスクは簡単に自動化できます。たとえば、テストの実行、タグの作成、package.jsonのバージョンの更新、パッケージをnpmやBower上に再公開するタスクなどです。いま、どんどん新しいものを取り入れるスタートラインに立っています。Travis CIやJenkinsなどのツールも使ってみてください。記事で紹介したTim Evkoの記事でも取り上げられています。

(原文:Design and Build Your Own JavaScript Library: Tips & Tricks

[翻訳:中村文也]
[編集:Livit

Copyright © 2016, Tim Severien All Rights Reserved.

Tim Severien

Tim Severien

オランダ出身で最先端のものに情熱を燃やす開発者です。JavaScriptとSassが大好きで、コードを書いていないときはSitePointまたはTim’s blogの記事を書いています。

Loading...