Sep 05, 2007

いざ要素名や属性名などを変えたくなった時にソースの中をあちこち探し回ったり巧みな正規表現を考えるために頭をひねったりしなくても済むようにするテク

拡張機能勉強会の時に焚き付けられたText Shadowのコード(textshadow.js)を教材にして拡張機能開発のノウハウを解説していくシリーズ。

Firefoxの拡張機能で、DOM要素ノードを動的に生成したり、編集したり、そうして生成したノードを後でまた収集したり、といった操作を行うような物を作る時は、必然的に、ソースの中に要素名や属性名が登場してくる。

var newNode = document.createElement('box');
newNode.setAttribute('class', 'my-custom-box');
parentBox.appendChild(newNode);

var nodes = document.evaluate('/descendant::*[@class="my-custom-box"]',
  document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var i = 0, maxi = nodes.snapshotLength; i < maxi; i++)
{
  this.processBox(nodes.snapshotItem(i));
}

こういう操作が一カ所だけにしか登場しないんなら別にいいけど、複数箇所で、似たような操作が何度もある場合は、要素名であるとか属性値・属性名であるとかノード検索の条件であるとかを、コードの冒頭で定数(定数プロパティ)として定義しておくことをお薦めしたい。

var myService = {
  CUSTOM_BOX_NODE_NAME  : 'box',
  CUSTOM_BOX_CLASS_NAME : 'my-custom-box',
  CUSTOM_BOX_EXPRESSION : '/descendant::*[@class="my-custom-box"]',
(略)

すると、さっきのような箇所はこうなる。

var newNode = document.createElement(this.CUSTOM_BOX_NODE_NAME);
newNode.setAttribute('class', this.CUSTOM_BOX_CLASS_NAME);
parentBox.appendChild(newNode);

var nodes = document.evaluate(this.CUSTOM_BOX_EXPRESSION, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var i = 0, maxi = nodes.snapshotLength; i < maxi; i++)
{
  this.processBox(nodes.snapshotItem(i));
}

textshadow.jsの冒頭箇所を見てみると、動的に生成するIDのプレフィクスとか、途中で生成する要素のクラス名であるとか、XPath式の中に埋め込む検索条件だとかを、片っ端から定数としてまとめて定義していることが分かるはずだ。

こうしておくと、いざクラス名が他の拡張機能とかぶっていたと判明した時なんかでも、ソースの頭の方をちょこっと書き換えるだけで済む。class属性の値として文字列リテラルが書かれている箇所を片っ端から探すようなことはしなくていい。

え? 一括置換を使えばすぐだろうって? まあ、確かにたいていの場合はそうなんだけど、でもそれじゃ解決できない時もある。

例えばこんな例。

(略)
myAttributeCheck : function(aNode, aNewValue)
{
  var value = aNode.getAttribute('myAttribute');
  if (value != aNewValue) {
    this.report('old value is '+value);
    aNode.setAttribute('myAttribute', aNewValue);
  }
},
(略)

ここで「myAttribute」という属性名が他の拡張機能とかぶってた!ということに気がついた時、このJavaScriptファイルの中を「myAttribute」→「myExtensionCustomAttribute」と一括置換したとする。すると、この箇所はこうなるだろう。

(略)
myExtensionCustomAttributeCheck : function(aNode, aNewValue)
{
  var value = aNode.getAttribute('myExtensionCustomAttribute');
  if (value != aNewValue) {
    this.report('old value is '+value);
    aNode.setAttribute('myExtensionCustomAttribute', aNewValue);
  }
},
(略)

属性名と同じ文字列があったメソッド名の定義の部分まで変わってしまったことに気がついただろうか。このメソッドがこのファイルの中でしか呼ばれていない物なんだったら別にいいけど、他のファイルで元の「myAttributeCheck」というメソッド名を参照してる箇所があったら、エラーになってしまうじゃないですか。

え? GREPしてから一括置換すればいいだろって? なるほどその通り。でも、その文字列がメソッド名にも使われてるということを忘れて、属性値の定義部分にしか登場しえないと思い込んで、メソッド名に使えない文字を置換文字列に使ってしまったら? 例えば、「myAttribute」を「my-extension-custom-attribute」に一括置換してしまったら?

(略)
my-extension-custom-attributeCheck : function(aNode, aNewValue)
{
  var value = aNode.getAttribute('my-extension-custom-attribute');
  if (value != aNewValue) {
    this.report('old value is '+value);
    aNode.setAttribute('my-extension-custom-attribute', aNewValue);
  }
},
(略)

これはもう問答無用でエラーになってしまう。

こういう例を見ても分かるとおり、一括置換というのは注意深くやらないと思わぬ所で問題を引き起こしてしまう可能性がある。無論、正規表現を駆使すれば「本当に変更したい箇所」だけをピンポイントで狙い撃ちすることもできるだろうけど、そんな事に頭を使うのって激しく無駄じゃないすか?

でも、こういう属性名の定義を定数プロパティにまとめておけば、そんな気苦労からはおさらばできる。

var myService = {
  CUSTOM_BOX_NODE_NAME  : 'box',
  CUSTOM_BOX_CLASS_NAME : 'my-custom-box',
  CUSTOM_BOX_EXPRESSION : '/descendant::*[@class="my-custom-box"]',
(略)

一括置換しなくても、さっきも書いたとおり、冒頭のここをちょこっと書き換えるだけで作業は終わりだ。

また、仮に一括置換をどうしても使いたくなってしまった時……「CUSTOM_BOX_CLASS_NAME」という定数プロパティ名自体が気に入らなくなってしまった時でも、安心して一括置換できる。なぜなら、ここでは「定数プロパティ名は全部大文字で書く」というコーディングルールを採用しているから、仮にこの定数プロパティ名を一括置換したところで、他の箇所の変数やメソッド名には全然影響しない。

ということで、まとめ。属性名や属性値など、後で変更する可能性があるものはソース内のあちこちに文字列リテラルで書いてしまわずに、一カ所にまとめて定数プロパティとして定義する。また、「定数プロパティ名は必ず大文字で書く」という風なコーディングルールを徹底しておく。安易に属性名を決めてしまったり、どんなメソッドを定義していたか忘れてしまったり、ということの多い僕のようなウッカリさんには、上記のようなミスを未然に防ぐためにも是非採用してもらいたいところです。

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能