JavaScript Advent Calendar 2020の11日目の記事です。
#本記事のターゲット
- どうやってJavaScriptのクラスが作られるのかを、試行の脇道も含めて見てみたい方
- URLを弄るクラスを求めて来た方
#TL;DR
- URL操作なら
URL
コンストラクタやURLSearchParams
とかを使ったりするね - でもどちらもIEでは使えなかったり、
URLSearchParams
についてはちょっと使い勝手が悪かったりするよね - というわけで、イチからURL操作用クラスを作ってみるよ
- IEでも使えるようにするよ(ただし、ページ最後に
class
式で書き直したものも載せておくよ) - URLのパラメータを整形するよ
- インスタンスのString化に対応させるよ
- 相対パス->絶対パス変換メソッドを作るよ
- パラメータのFormData化にも対応させるよ
- 作りながら薀蓄(補足説明)を垂れ流すよ
- 過程に興味の無い人は最後まで飛ばしてね
#クラスのガワを用意する
クラス名はUrl
にします。クラス外部に幾つか変数を用意したいので、全体を即時関数で囲い、クラスのみグローバルに放り出します。IEにも対応させると書いた通り、書き方はIE準拠です。
**Url.js(折りたたみ)**
(function(global){
function E(s) {//エラー用
var e = new Error(s);
e.name = 'UrlError';
throw e;
}
function Url(url) {
// クラス
}
// メソッドを追加する場所
Url.prototype['aaa'] = function() {};
// 作ったクラスをグローバル化
global.Url = Url;
})(this);
注: 要所要所に「現在のUrl.jsの形」を上記のように折りたたんで置きます。
#URLを解析する
解析するにはまず、URLの構造を把握すべし!
こんなURLがあるとします。
http://uname:pswrd@abc.com:1234/a/b/c/sample.html?p=1&t=text#title
##URLの各値の説明
###プロトコル(http:
)
URLスキームの区分の一つで、一般的にはwebページのURLなどに使われるものを指します。
URLスキームといえば、よく目にするものとして、webページのURLなどに使われるプロトコルであるところのhttp:
やhttps:
、ftp:
などや、CSSファイルへの画像埋め込みなどに使われるdata:
、URL.createObjectURL()
で生成されるblob:
、ローカルファイルにアクセスするためのfile:
、Google Chromeの各種設定用のページなどを表すchrome:
などがありますよね。しかしこれらは氷山の一角、どころか一欠片に過ぎず、恐らくは皆さんの想像するよりもはるかに大量の種類のURLスキームが存在します。それぞれの違いによって、通信の種類やアクセスする場所の違いがあったり、あるいは特に意味があったりなかったり。
###ユーザー名&パスワード(uname:pswrd@
)
ユーザー名(uname
)とパスワード(pswrd
)です。//
の後に、そして後述するホスト名
の前に置かれます。ベーシック認証において、一々ユーザー名とパスワードを入力しなくても自動でログインできるようにするためのものです。セキュリティ上の観点から、Firefox以外のほぼ全てのブラウザ(IEも!)の最新バージョンでは無効になっている(無視される)ことが殆どで、古代のプライベートなRSSフィードなどで発行されたURLに極稀に見ることのできる絶滅危惧種、近絶滅種です。言うまでもないことですが、あっても無意味なことが殆どなので、大抵は省略されています。本当は無視してやりたいのですが、仮にこれのあるURLを解析した場合、後述のホスト名
がしっちゃかめっちゃかになってしまうので、泣く泣く考慮します。
因みに、絶滅危惧種云々はあくまでwebページのURL上においてのもの。それに限らないのであれば、MySQL
などのデータベースに接続するURLなどで頻繁に見ることはできます。
###ホスト名(abc.com
)
webページのURLであれば、そのページが属しているサイトの名前を表す部分です。ドメイン
とも言います。そう、「クロスドメイン」のドメインです。
###ポート番号(:1234
)
言うなれば、送受信口の区分け、でしょうか。まあ、この記事では「そんなものがあるんだな」程度の理解で十分です。ごく一般的なwebページのURLでは不要なことが殆どなので、大抵は省略されています。
###オリジン(http://abc.com:1234
)
プロトコル
+//
+ホスト名
+ポート番号
で表される値です。ポート番号の部分は、特に明示されていない場合は、省略可能です。webページのURLであれば、そのページの属しているサイトそのもの、ページリソースの源泉を表す部分です。「オリジン間リソース共有(CORS; Cross-Origin Resource Sharing)」のオリジンのことでもあります。
複数の値を組み合わせて作られるものなので、今回のクラスではこれは考慮しません。個人で実装したいという方は、プロパティではなくゲッターなどで取得する、セッターで設定する方法を取ると良いです。ゲッターについてはまた後ほど説明しますので、そちらを参照してください。
###パス(/a/b/c/sample.html
)
webページのURLであれば、そのオリジン上におけるファイルの在り処を表す部分です。相対パスのURLの場合、プロトコル
やホスト
など、これより前に位置している値は省略されている可能性があります。
###パラメータ(?p=1&t=text
)
URLへの付加情報の一つで、サーバー側が受け取ることになる値です。IE未サポートのURL
コンストラクタで解析するときはsearch
というキーに格納されます。パス
の後に?
を付け、キー
+=
+値
の形式で付け加えていきます。複数のパラメータを付けたい場合は、例示にあるように&
で区切ります。その他細々としたルールがあるのですが、そのルールはサーバー側で使っているプログラミング言語やライブラリの仕様によってまちまちです。詳細は後述します。
###ハッシュ(#title
)
URLへの付加情報の一つで、ユーザー側のアクセシビリティの向上のために存在するものです。ページ上のアンカーへ飛ぶためのものですね。
##URLを正規表現で解析
###正規表現を用意
サンプル文字列(ここでは例示したURLを簡略化したもの)を用意しましょう。
http://uname:pswrd@ホスト名:1234/パス?p=1#title
これをRegExp
コンストラクタに放り込み、予約記号をエスケープしてみましょう。
var reg = new RegExp('http:\/\/uname:pswrd@ホスト名:1234\/パス\?p=1#title');
文字列を参考にメタ文字などに置き換え、上に挙げた、オリジン
以外のURLの各値ごとにグループ分けしましょう。省略可能とされる値(基本的にはパス
以外)はキャプチャ無効化グループ(?:)
を駆使しましょう。
var reg = new RegExp(
'^'
// (protocol)
+'(?:([^:\\s]+:)|)'
// (username), (password), (host), (port)
+'(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)'
// path
+'(\\/?[^\\?#]*)'
// (search)
+'(?:\\?([^\\?#]*)|)'
// (hash)
+'(?:#(.*)|)'
+'$'
,'i');
// 全部繋げて…
var reg = new RegExp('^(?:([^:\\s]+:)|)(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)(\\/?[^\\?#]*)(?:\\?([^\\?#]*)|)(?:#(.*)|)$','i');
出来ました!
では、この解析手法をクラスに実装してみましょう。
**Url.js(折りたたみ)**
(function(global){
var REG = {
URL: {
R: new RegExp('^(?:([^:\\s]+:)|)(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)(\\/?[^\\?#]*)(?:\\?([^\\?#]*)|)(?:#(.*)|)$','i'),
G: ['protocol','username','password','host','port','path','search','hash']
}
};
function E(s) {
var e = new Error(s);
e.name = 'UrlError';
throw e;
}
function Url(url) {
var s = this, u;
(u = (url ? String(url) : '').match(REG.URL.R))
? (REG.URL.G.forEach(function(g,i){
s[g] = u[i+1]
})
)
: E('Invalid URL: "' + url + '"');
}
Url.prototype['aaa'] = function() {};
global.Url = Url;
})(this);
実際に動かしてみると、
console.log(
new Url('http://uname:pswrd@abc.com:1234/a/b/c/sample.html?p=1&t=text#title')
);
/*
{
"protocol": "http:",
"username": "uname",
"password": "pswrd",
"host": "abc.com",
"port": "1234",
"path": "/a/b/c/sample.html",
"search": "p=1&t=text",
"hash": "title"
}
*/
大丈夫そうですね。ちなみに各値に該当する部分が無かった場合、path
は""
、それ以外はundefined
になるはずです。
しかし実際には""
なパスは存在しませんので、""
の時には"/"
にしておきましょう。
**Url.js(折りたたみ)**
(function(global){
var REG = {
URL: {
R: new RegExp('^(?:([^:\\s]+:)|)(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)(\\/?[^\\?#]*)(?:\\?([^\\?#]*)|)(?:#(.*)|)$','i'),
G: ['protocol','username','password','host','port','path','search','hash']
}
};
function E(s) {
var e = new Error(s);
e.name = 'UrlError';
throw e;
}
function Url(url) {
var s = this, u;
(u = (url ? String(url) : '').match(REG.URL.R))
? (REG.URL.G.forEach(function(g,i){
s[g] = u[i+1]
}),
s.path = s.path || '/',// 追加分
)
: E('Invalid URL: "' + url + '"');
}
Url.prototype['aaa'] = function() {};
global.Url = Url;
})(this);
では次です。
##URLパラメータ(search
)を辞書型に変換
p=1&t=text
のように登録されたsearch
を{p: "1", t: "text"}
のように変えましょう、という項です。そこまで難しくはなさそうですね。
しかし、ここで問題が発生します。
皆さんに質問です。
URLパラメータでサーバーに「配列」を、{ary: [1, 2, 3]}
を送信する場合、皆さんはどのように書きますか?
こうですか?
?ary=1,2,3
それとも?
?ary=1&ary=2&ary=3
はたまた…?
?ary[0]=1&ary[1]=2&ary[2]=3
どうでしょう、皆さんはどれが正解か分かりますか?
…
…
…
…
…
…
残念ながら、特定の正解はありません。環境によってはそのいずれもが正解になり得ますし、はたまた不正解にもなり得ます。HTML4のW3CのドキュメントにはFormData
に(つまりパラメータに)配列を渡す(同じキーで複数の値を渡す)方法についての言及が無く、しかし当時から既に一定の需要があったために、様々な開発者が好き勝手にバラバラな仕様を実装してしまったためです。HTML5にてフォーム部品のmultiple
属性が解禁されたことで、HTML5のW3Cのドキュメントにてようやく配列渡しの方法(?ary=1&ary=2&ary=3
)が言及されるようになりました。しかし時すでに遅しというやつなのでしょう。広まってしまった多種多様な仕様が変わることはありませんでした。
**環境別の配列登録方法の例(折りたたみ)**
###Wordpress
?ary=1,2,3
###Angular.js
?ary=1&ary=2&ary=3
###PHP, Python (urllib), Rails
?ary[]=1&ary[]=2&ary[]=3
###Node.js
?ary=1&ary=2&ary=3
あるいは
?ary[]=1&ary[]=2&ary[]=3
ただし中身が一つだけの配列を送りたい場合は後者を使わなければならないので、基本的にはブラケットを使用している後者がスタンダードです。
###ASP.NET MVC
?ary=1&ary=2&ary=3
あるいは
?ary[0]=1&ary[1]=2&ary[2]=3
後者の場合、PHPやNode.jsとは違ってブラケット内にインデックスをマストで書き入れなければなりません(PHPやNode.jsなどでは、インデックスの書き込みは任意です。HTMLのフォームに配慮し、書かないことが殆どですが。)。Node.jsと同様の理由で後者がスタンダードです。
参考: Stack Overflow - How to pass an array within a query string?
注: 上記の例はあくまで基本的・一般的な、あるいは筆者が普段使用している機能を用いた場合の例示であり、サーバー側のプログラミング言語上で使用されているライブラリなどによってはこの限りではありません。「私の知っているソレと違う!」なんてこともあるかもしれませんが、ここでは例示にあるような配列登録方法の種類があることだけ理解していただければ幸いです。
「へえ、環境によって違うんだね」と思うだけかもしれませんが、事はそう単純ではありません。例えば
?ary=1&ary=2&ary=3
こちらのパラメータ。上記に挙げた一部の環境では、これは{ary: ["1", "2", "3"]}
と同義ですが、この配列登録の形式をサポートしていない環境の場合、これは{ary: "3"}
と認識されてしまいます(同じキーのパラメータがあった場合、最後のもののみ適用される)。
この頭の痛くなる問題から逃れるため、例えばもしあなたのサーバーで動いているスクリプトがPHPなら、?ary[]=1&ary[]=2&ary[]=3
の形式のみを対応したものを作るのは理にかなった選択肢でしょう。しかし今回は、特定の環境だけに特化しない、全ての形式に対応したクラスを作りたいと思います。
ではまず、Url
クラスの第2引数にオプションを取りましょう。
**Url.js(折りたたみ)**
(function(global){
var REG = {
URL: {
R: new RegExp('^(?:([^:\\s]+:)|)(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)(\\/?[^\\?#]*)(?:\\?([^\\?#]*)|)(?:#(.*)|)$','i'),
G: ['protocol','username','password','host','port','path','search','hash']
}
};
var PARAM = {
BASE: function(s) {// [['p', '1'], ['t', 'text']]
return (s || '').split('&').filter(function(v){return v}).map(function(v){return (v = v.split('='), v[0] = decodeURI(v[0]).replace(/\+/g, ' '), v)})
},
ARRAY_TYPE: {
'comma': {
PARSE: function(s) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
v[1] = v[1].split(',').map(function(p){return decodeURI(p).replace(/\+/g, ' ')});
o[v[0]] = v[1].length - 1 ? v[1] : v[1][0];
});
return o;
}
},
'sepalate': {
PARSE: function(s) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
o[v[0]] = o[v[0]]
? (function(p, a){
return ((a = Array.isArray(p) ? p : [p]).push(v[1]), a);
})(o[v[0]])
: v[1]
});
return o;
}
},
'with_bracket': {
PARSE: function(s, i) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
var m;
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
(m=v[0].match(new RegExp(i ? '^(.+?)\\[(\\d+?)\\]$' : '^(.+?)\\[\\]$')))
? (o[m[1]] = (function(p, a){
return (a = (p==void(0)) ? [] : (Array.isArray(p) ? p : [p]), (m[2] ? (a[Number(m[2])] = v[1]) : a.push(v[1])), a);
})(o[m[1]]))
: (o[v[0]] = v[1])
});
return o;
}
},
}
}
function E(s) {
var e = new Error(s);
e.name = 'UrlError';
throw e;
}
// IEだと初期値設定ができない!! function Url(url, {arrayType='with_bracket', indexed=false}={}) {
function Url(url, option) {
var s = this, u, arrayType = (option && option.arrayType) || 'with_bracket', indexed = (option && option.indexed) || false;
(u = (url ? String(url) : '').match(REG.URL.R))
? (REG.URL.G.forEach(function(g,i){
s[g] = u[i+1]
}),
s.path = s.path || '/',
s.search = (function(t){
return t ? t.PARSE(s.search, indexed) : (E('Invalid arrayType: "' + arrayType + '"'), void(0))
})(PARAM.ARRAY_TYPE[arrayType]),
s._parseStyle = {array: {type: arrayType, indexed: indexed}}
)
: E('Invalid URL: "' + url + '"');
}
Url.prototype['aaa'] = function() {};
global.Url = Url;
})(this);
arrayType | indexed | 例 | |
---|---|---|---|
comma | false | ?ary=1,2 |
|
sepalate | false | ?ary=1&ary=2 |
|
with_bracket | false | ?ary[]=1&ary[]=2 |
★ |
with_bracket | true | ?ary[0]=1&ary[1]=2 |
|
(太字が初期値です) |
console.log(
new Url('http://uname:pswrd@abc.com:1234/a/b/c/sample.html?p=1&t=text&a[]=1&a[]=2#title').search
)
// {p: "1", t: "text", a: ["1", "2"]}
console.log(
new Url('http://uname:pswrd@abc.com:1234/a/b/c/sample.html?p=1&t=text&a[0]=1&a[1]=2#title', {
indexed: true
}).search
)
// {p: "1", t: "text", a: ["1", "2"]}
console.log(
new Url('http://uname:pswrd@abc.com:1234/a/b/c/sample.html?p=1&t=text&a=1&a=2#title', {
arrayType: 'sepalate'
}).search
)
// {p: "1", t: "text", a: ["1", "2"]}
console.log(
new Url('http://uname:pswrd@abc.com:1234/a/b/c/sample.html?p=1&t=text&a=1,2#title', {
arrayType: 'comma'
}).search
)
// {p: "1", t: "text", a: ["1", "2"]}
できましたね!
「『配列』のパラメータの登録は実装したのに、なぜ『辞書型』のパラメータの登録は実装しないの?」
PHPでは、
?t=text&obj[a][b]=1
のようなパラメータも許容されるそうです。この例の場合、{t: 'text', obj: {a: {b: 1}}}
というパラメータを送るという意味になります。しかし本クラスではこのような登録方法を実装しません。それはなぜか。URLパラメータとは元来、FormData
を送信するに際しての一形態であるためです。配列であれば、
<form>
<select multiple="multiple" name="favorite_fruit[]">
<option>apple</option>
<option>banana</option>
<option>orange</option>
</select>
</form>
HTML5にてこのような形のフォームで送信することになる可能性はあります。
しかしながら、辞書型オブジェクトの形式を、フォームが取ることはありません。ネストするようなキーの配置をする必要性はありませんし、不可能です。どうしても辞書型の形で送りたいのであれば、
obj=%7B%22a%22:%7B%22b%22:1%7D%7D
のように辞書型を文字列に書き換えてエスケープして送り、サーバー側でJSONやDictionalyなどの型などにパースするべきです。よって、今回は考慮しません。
インスタンスのString化を実装する
ここでいうString化の実装とは、Url
インスタンスのString型への変換メソッドを用意するということです。
String()
で包んでみたり、あるいはString型の別の値と結合してみたり…
console.log(
String(new Url('http://example.com/'))
)
// [object Object]
console.log(
'' + new Url('http://example.com/')
)
// [object Object]
console.log(
new Url('http://example.com/').toString()
)
// [object Object]
このようなObject型からString型への型変換が行われるとき、内部的には、そのオブジェクトのprototype
上にあるtoString
メソッドが間接的に呼び出されます(3つ目の例では直接呼び出していますがね…)。現状ではUrl
インスタンスをString化しても、"[object Object]"
という何の役にも立たない文字列に変わってしまいますが、前述のtoString
メソッドを上書きしてやれば、代わりにURLの文字列を出力させることができるようになります。
それはさておき、toString
メソッドで値を返却するため、解析して得られたURLの各値から再びURL文字列を復元してみましょう。
search
は辞書型オブジェクトですから、Object.keys
で配列に加工し、その後join
で直結すると良いですね。ここでもURLパラメータの配列とその登録方法ごとに処理を変える必要があることに留意してください。
// o: {p: "1", t: "text", a: ["1", "2"]}
// arrayType: commaの場合
Object.keys(o).map(function(k) {
return k + '=' + Array.isArray(o[k])
? o[k].join(',')
: o[k]
})
ただしこのString化、URL化をする際には、一部エスケープが必要な文字もあります。encodeURI
やencodeURIComponent
などを使用してエスケープも行いましょう。またパラメータ内では、例えエスケープをしようとも、半角スペースは使えないそうです。…ホントかな。まあ、そういうことにしておきましょう。ともかく、半角スペースは代わりに+
に置き換えられます。半角スペースはエスケープをすると%20
になりますから、最後にこれを+
に置き換えましょう。
何にせよ、書き足してみましょうか。
**Url.js(折りたたみ)**
(function(global){
var REG = {
URL: {
R: new RegExp('^(?:([^:\\s]+:)|)(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)(\\/?[^\\?#]*)(?:\\?([^\\?#]*)|)(?:#(.*)|)$','i'),
G: ['protocol','username','password','host','port','path','search','hash']
}
};
var PARAM = {
BASE: function(s) {// [['p', '1'], ['t', 'text']]
return (s || '').split('&').filter(function(v){return v}).map(function(v){return (v = v.split('='), v[0] = decodeURI(v[0]).replace(/\+/g, ' '), v)})
},
ARRAY_TYPE: {
'comma': {
PARSE: function(s) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
v[1] = v[1].split(',').map(function(p){return decodeURI(p).replace(/\+/g, ' ')});
o[v[0]] = v[1].length - 1 ? v[1] : v[1][0];
});
return o;
},
TO_STR: function(o) {
return Object.keys(o).map(function(k) {
return (k + '=' + Array.isArray(o[k])
? o[k].map(function(v){
return encodeURIComponent(v)
}).join(',')
: encodeURI(o[k]))
}).join('&')
}
},
'sepalate': {
PARSE: function(s) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
o[v[0]] = o[v[0]]
? (function(p, a){
return ((a = Array.isArray(p) ? p : [p]).push(v[1]), a);
})(o[v[0]])
: v[1]
});
return o;
},
TO_STR: function(o) {
return Object.keys(o).map(function(k) {
return (Array.isArray(o[k])
? o[k].map(function(v){
return k + '=' + encodeURI(v)
})
: k + '=' + encodeURI(o[k]))
}).reduce(function(a,v){
return a.concat(v)
},[]).join('&')
}
},
'with_bracket': {
PARSE: function(s, i) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
var m;
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
(m=v[0].match(new RegExp(i ? '^(.+?)\\[(\\d+?)\\]$' : '^(.+?)\\[\\]$')))
? (o[m[1]] = (function(p, a){
return (a = (p==void(0)) ? [] : (Array.isArray(p) ? p : [p]), (m[2] ? (a[Number(m[2])] = v[1]) : a.push(v[1])), a);
})(o[m[1]]))
: (o[v[0]] = v[1])
});
return o;
},
TO_STR: function(o, i) {
return Object.keys(o).map(function(k) {
return (Array.isArray(o[k])
? o[k].map(function(v,_){
return k + '[' + (i ? _ : '') + ']' + '=' + encodeURI(v)
})
: k + '=' + encodeURI(o[k]))
}).reduce(function(a,v){
return a.concat(v)
},[]).join('&')
}
},
}
}
function E(s) {
var e = new Error(s);
e.name = 'UrlError';
throw e;
}
function Url(url, option) {
var s = this, u, arrayType = (option && option.arrayType) || 'with_bracket', indexed = (option && option.indexed) || false;
(u = (url ? String(url) : '').match(REG.URL.R))
? (REG.URL.G.forEach(function(g,i){
s[g] = u[i+1]
}),
s.path = s.path || '/',
s.search = (function(t){
return t ? t.PARSE(s.search, indexed) : (E('Invalid arrayType: "' + arrayType + '"'), void(0))
})(PARAM.ARRAY_TYPE[arrayType]),
s._parseStyle = {array: {type: arrayType, indexed: indexed}}
)
: E('Invalid URL: "' + url + '"');
}
Url.prototype.toString = function() {
var s = this, a = s._parseStyle.array;
return (s.protocol && s.host
? s.protocol + '//' + (s.username && s.password
? s.username + ':' + s.password + '@'
: '')
+ s.host + (s.port ? ':' + s.port : '')
: ''
)
+ s.path
+ (Object.keys(s.search).length
? '?' + (function(t) {
return t ? t.TO_STR(s.search, a.indexed).replace(/%20/g,'+') : (E('Invalid arrayType: "' + a.arrayType + '"'), '')
})(PARAM.ARRAY_TYPE[a.arrayType])
: ''
)
+ (s.hash ? '#' + s.hash : '')
}
global.Url = Url;
})(this);
var u = new Url('http://example.com/#x');
u.search['a'] = [1, 2];
console.log('' + u);
// http://example.com/?a[]=1&a[]=2#x
できましたね!
…さて、こんなメソッドを作って何か意味があるのかと思われる方もいらっしゃるかもしれませんね。実は、大きなメリットがあるんです。
JavaScriptには「暗黙的な型変換」と呼ばれるものがあります。システム的に本来String型あるいはNumber型(他言語で言うところのInt型)の代入を望まれる場所に全く違う型のオブジェクトが代入された時、その値をString型の場合はtoString
メソッドを、Number型の場合はvalueOf
メソッドを自動で呼び出して変換してくれるんです。これはつまり、toString
メソッドが実装されることによって、URLの文字列(String型)を要求するような組み込み関数であるXMLHttpRequest
やfetch
の引数に対して、Url
インスタンスを直接代入できるようになるということなのです。
var xhr = new XMLHttpRequest;
xhr.addEventListener('load', e=>console.log(e.responseText));
xhr.open('GET', new Url('http://example.com/'));
xhr.send();
fetch(new Url('http://example.com/'))
.then(v=>v.text())
.then(v=>console.log(v))
豆知識: 論理演算子(
&&
・||
)の使い方論理演算子は条件式(
n >= 100 && n % 2 == 0
のような)に使われるだけのものという認識があるかもしれませんが、所謂if
式と同じような扱い方も可能です。次のようなスクリプトがあったとします。
var a = true, b = false;
a && alert('when "a" is true.');// 1
b || alert('when "b" is false.');// 2
(a && b) && alert('when both of "a" and "b" are true.');// 3
これは実質的に次のスクリプトと同義です。
var a = true, b = false;
if(a) {
alert('when "a" is true.');// 1
}
if(!b) {
alert('when "b" is false.');// 2
}
if(a && b) {
alert('when both of "a" and "b" are true.');// 3
}
AND (
x && f()
)の場合、左側がtrue
であれば、右側も同様にtrue
であることを期待して、右側の式の評価(解析)が開始されます。しかし左側がfalse
であった場合、x && f()
はその時点で真になり得ないことが確定するため、右側の式の評価は開始されないのです。そしてOR (
x || f()
)の場合、左側がfalse
であれば、右側はtrue
であることを期待して、右側の式の評価(解析)が開始されます。しかし左側がtrue
であった場合、x || f()
は既に真であることが確定するため、右側の式の評価は開始されないのです。
この手法は
if
単体のみを用いるような分岐の際には有効ですが、else
やelse if
などの分岐をさせたい場合は、三項演算子を用いましょう。
var a = true;
a ? alert('when "a" is true.') : console.log('when "a" is false.');
相対パス->絶対パス変換メソッドを作る
相対パスを絶対パスに変換するにはまず、相対パスについて把握すべし!
http://example.com
├── branch
│ ├── static
│ │ ├── main.html (http://example.com/branch/static/main.html)
│ │ ├── main.css
│ │ └── main.js
│ └── image
│ ├── logo16.png
│ └── logo32.png
│
└── config
└── config.json
相対パスとは、同じオリジン上に存在する別のファイルの在り処をあらわす際に用いられる、座標の短縮記法とそれを用いたパスのことです。main.html
からlogo16.png
という画像ファイルにアクセスする場合、短縮記法を使えば、このファイルの座標は../image/logo16.png
あるいは/branch/image/logo16.png
とあらわすことができます。main.html
からmain.css
というCSSファイルにアクセスする場合は、/branch/static/main.css
、./main.css
あるいはmain.css
となります。…複数通りがあるようですね。ええ、実はこの短縮記法、種類があります。
##相対パスの種類
###①オリジン(源泉)からの直接参照
/branch/image/logo16.png
実際のところ、これは相対パスではなく単なる短縮記法に過ぎませんが、面倒なので相対パスということにしてしまいましょう。先頭が/
となっていることと、現在位置がどこであろうとも変わることの無い固有性(この時点で全然「相対」じゃない!)が特徴です。http://example.com
というオリジン(源泉)から見て、どこにlogo16.png
が存在するのかをあらわします。
ここまでで作ったUrl
で解析した際にpath
に登録されるものも、これと同じ形式です。
###②現在位置からの遡及参照
../image/logo16.png
先程とは打って変わってややこしく、そして正しく相対パスであるのがこちら。
- 「共通するディレクトリまで遡り」
- 「そこから目的のファイルまで参照」
するのがこの方法です。さて、この../
は一体何でしょう。…これは「自分が所属しているディレクトリの一つ上のディレクトリを参照せよ」という意味です。1.の「遡り」のために定められたルールですね。
この例の場合、まず「../
でmain.html
の所属しているstatic
ディレクトリの一つ上、branch
ディレクトリを参照」します。このディレクトリの直下には、お目当てのファイルであるlogo16.png
の存在する祖先(親)ディレクトリimage
がありますね。であれば遡るのはここまで、次は2.、「image/logo16.png
と参照する」のです。
../
は複数重ねることもでき、例えばmain.html
からconfig.json
を参照するのであれば、../../config/config.json
のような形になります。
そしてもう一つ、この短縮記法には./
というものもあります。これは「自分が所属しているディレクトリと同じディレクトリを参照せよ」というものです。こういった相対パスを書く際にはあまり使う必要の無いもので、単に自分が所属しているディレクトリそのものをあらわすことぐらいにしか本来は用途がありません。が、だからといって相対パスにこれが使われないわけではありません。main.html
から見た時、main.css
は同じディレクトリにあるため、./main.css
と参照することができます。
しかしながらこれは儀礼的なもので、同じディレクトリ上にある場合は1.の「遡り」の必要がなく、そのまま2.の参照を行えます。ですので単にmain.css
と書けば、同じディレクトリにあるmain.css
にアクセスできます。…まあ、これだとファイル名なのかパスなのか分かりにくいですよね。なので「儀礼的」に、「これはパスである」と明示するために、これを使うのです。あっても無くても意味は変わらない…、幾つ./
があろうとも無意味…、そう、これについては無視できるのです。
では、ここまでに述べた「お約束」を元に、Url.jsに相対パス->絶対パス変換メソッドを書いてみましょう。
メソッド名はresolve
、引数に相対パスを入力することで、インスタンス内の各値をその相対パスに準じた情報に書き換えさせます。返り値はthis
とするので、そのままString化してfetch
などに放り込むもよし、あるいは他のメソッドとチェーンさせる起点にするもよし。
では下準備です。上記のルールを参考に、ざっくり処理を書いてみましょう。"/"
で相対パスをsplit
すれば、各ディレクトリ階層名と".."
、"."
、""
の配列を作り出せます(""
はパスの先頭の"/"
のこと;同じディレクトリを指す"."
は不要なので切り捨て)。これを相互比較し、pop
(配列末尾から一つ切り出し)とshift
(配列先頭以下略)を駆使し、共通ディレクトリを洗い出しましょう。
var base_path = '/a/b/c/sample.html';
var result_path;
var rel_path = '../x.php';
// var rel_path = '/a/b/x.php';
// var rel_path = '.././x.php';
var bs = base_path.split('/');
bs.pop();
var sp = rel_path.split('/').filter(function(v) {return v !== '.'});
// bs: ["", "a", "b", "c"]
// sp: ["..", "x.php"] or ["", "a", "b", "x.php"] or ["..", "x.php"]
sp[0] == ''
? (result_path = rel_path)
: (function() {
var v;
while((v = sp.shift())) {
v == '..'
? bs.pop() || console.log('Invalid relative path')// 限界以上に遡った時用;実装の際には処理を止めるため、エラーをthrowさせる
: bs.push(v)
}
result_path = bs.join('/')
})()
console.log(result_path);
// /a/b/x.php
いい具合ですね。
ただ、rel_path
に入力されるものは、文字通り相対パスでなければなりません。しかし入力されたパスが正規の相対パスかどうかを確認していないため、このままでは予期せぬエラーを引き起こすかもしれません。そこで、ここまでで作ったUrl
コンストラクタ自身の出番です。rel_path
をこれで解析し、host
がundefined
であることを確認すれば、相対パスのみを受け付けることができます。それにパスとそれ以外の分離もできますし、付加されているハッシュやパラメータも解析できます。
同様に、基になるURLは絶対パスのURL(プロトコル
やホスト名
のあるURL)のみに限定しておいた方が、胃のキリキリに悩まされずに済みます。基になるURLが、/a.html
みたいなURLならば問題はないのですが、何かの間違いで../a.html
みたいなものが使われていると、もうどうにもならなくなってしまいますからね。よって、インスタンスのthis.host
がundefined
の時にも弾いてしまうのが吉です。これに不満のある方は、是非ご自身でこの条件分岐地獄の苦しみと、「これ以外に想定していないケースは無いか?」という連関する自問自答による圧倒的強迫観念の二つを味わっていただきたく思います。
というわけで、これを少し書き換え、組み込みます。
**Url.js(折りたたみ)**
(function(global){
var REG = {
URL: {
R: new RegExp('^(?:([^:\\s]+:)|)(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)(\\/?[^\\?#]*)(?:\\?([^\\?#]*)|)(?:#(.*)|)$','i'),
G: ['protocol','username','password','host','port','path','search','hash']
}
};
var PARAM = {
BASE: function(s) {// [['p', '1'], ['t', 'text']]
return (s || '').split('&').filter(function(v){return v}).map(function(v){return (v = v.split('='), v[0] = decodeURI(v[0]).replace(/\+/g, ' '), v)})
},
ARRAY_TYPE: {
'comma': {
PARSE: function(s) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
v[1] = v[1].split(',').map(function(p){return decodeURI(p).replace(/\+/g, ' ')});
o[v[0]] = v[1].length - 1 ? v[1] : v[1][0];
});
return o;
},
TO_STR: function(o) {
return Object.keys(o).map(function(k) {
return (k + '=' + Array.isArray(o[k])
? o[k].map(function(v){
return encodeURIComponent(v)
}).join(',')
: encodeURI(o[k]))
}).join('&')
}
},
'sepalate': {
PARSE: function(s) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
o[v[0]] = o[v[0]]
? (function(p, a){
return ((a = Array.isArray(p) ? p : [p]).push(v[1]), a);
})(o[v[0]])
: v[1]
});
return o;
},
TO_STR: function(o) {
return Object.keys(o).map(function(k) {
return (Array.isArray(o[k])
? o[k].map(function(v){
return k + '=' + encodeURI(v)
})
: k + '=' + encodeURI(o[k]))
}).reduce(function(a,v){
return a.concat(v)
},[]).join('&')
}
},
'with_bracket': {
PARSE: function(s, i) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
var m;
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
(m=v[0].match(new RegExp(i ? '^(.+?)\\[(\\d+?)\\]$' : '^(.+?)\\[\\]$')))
? (o[m[1]] = (function(p, a){
return (a = (p==void(0)) ? [] : (Array.isArray(p) ? p : [p]), (m[2] ? (a[Number(m[2])] = v[1]) : a.push(v[1])), a);
})(o[m[1]]))
: (o[v[0]] = v[1])
});
return o;
},
TO_STR: function(o, i) {
return Object.keys(o).map(function(k) {
return (Array.isArray(o[k])
? o[k].map(function(v,_){
return k + '[' + (i ? _ : '') + ']' + '=' + encodeURI(v)
})
: k + '=' + encodeURI(o[k]))
}).reduce(function(a,v){
return a.concat(v)
},[]).join('&')
}
},
}
}
function E(s) {
var e = new Error(s);
e.name = 'UrlError';
throw e;
}
function Url(url, option) {
var s = this, u, arrayType = (option && option.arrayType) || 'with_bracket', indexed = (option && option.indexed) || false;
(u = (url ? String(url) : '').match(REG.URL.R))
? (REG.URL.G.forEach(function(g,i){
s[g] = u[i+1]
}),
s.path = s.path || '/',
s.search = (function(t){
return t ? t.PARSE(s.search, indexed) : (E('Invalid arrayType: "' + arrayType + '"'), void(0))
})(PARAM.ARRAY_TYPE[arrayType]),
s._parseStyle = {array: {arrayType: arrayType, indexed: indexed}}
)
: E('Invalid URL: "' + url + '"');
}
Url.prototype.toString = function() {
var s = this, a = s._parseStyle.array;
return (s.protocol && s.host
? s.protocol + '//' + (s.username && s.password
? s.username + ':' + s.password + '@'
: '')
+ s.host + (s.port ? ':' + s.port : '')
: ''
)
+ s.path
+ (Object.keys(s.search).length
? '?' + (function(t) {
return t ? t.TO_STR(s.search, a.indexed).replace(/%20/g,'+') : (E('Invalid arrayType: "' + a.arrayType + '"'), '')
})(PARAM.ARRAY_TYPE[a.arrayType])
: ''
)
+ (s.hash ? '#' + s.hash : '')
}
Url.prototype.resolve = function(rel_path) {
var rs, bs = this.path.split('/'), rl = new Url(rel_path, this._parseStyle.array), sp = rl.path.split('/').filter(function(v) {return v !== '.'});
return((this.host || E('Base URL is not a absolute path: "' + this + '"')), (rl.host && E('"' + rel_path + '" is not a relative path')), bs.pop(), sp[0] == ''
? (rs = rl.path)
: (function(v) {
while((v = sp.shift())) {
v == '..'
? bs.pop() || E('Invalid relative path: "' + rel_path + '"')
: bs.push(v)
}
rs = bs.join('/')
})(),
this.path = rs,
this.search = rl.search,
this.hash = rl.hash,
this)
}
global.Url = Url;
})(this);
console.log(
new Url('http://uname:pswrd@abc.com:1234/a/b/c/sample.html', {indexed: true})
.resolve('../x.php?a[0]=1&a[1]=2#title')
)
// {
// "protocol": "http:",
// "username": "uname",
// "password": "pswrd",
// "host": "abc.com",
// "port": "1234",
// "path": "/a/b/x.php",
// "search": {
// "a": [
// "1",
// "2"
// ]
// },
// "hash": "title",
// "_parseStyle": {
// "array": {
// "arrayType": "with_bracket",
// "indexed": true
// }
// }
// }
いい感じですね!
ここらで肝心のIEの動作確認をしてみましょう。
最高に使いづらいコンソールでイライラしますが、キチンと動いていることを確認できます。
豆知識: 丸括弧とカンマの使い方
次のようなスクリプトがあったとします。
var a, b;
console.log(
(a = [1, 2, 3], b = ['x', 'y', 'z'], a = a.concat(b), a.pop(), a)
)
この場合コンソールにログとして出力される値は
a
の中身、つまり[1, 2, 3, "x", "y"]
だけとなります。括弧内で処理がカンマに区切られている場合、その括弧があらわす値は最後の処理のリザルトのみです。もちろん途中のb = ['x', 'y', 'z']
などの処理は実行されていますが、出力はされません。この手法は通常、コードの圧縮などに使われます。この項で実装した
resolve
メソッドにもこの手法が用いられています。そんなコードの短縮に役立つ丸括弧ですが、実は幾つか制限があります。丸括弧内では
var
などの宣言子やif
、throw
、for
、while
などを直接使用できないのです。
(var a = 1)
// Uncaught SyntaxError: Unexpected token 'var'
(if(true) {console.log('a')})
// Uncaught SyntaxError: Unexpected token 'if'
(throw new Error('a'))
// Uncaught SyntaxError: Unexpected token 'throw'
(for(var a of [1,2]) {console.log(a)})
// Uncaught SyntaxError: Unexpected token 'for'
(while(true) {console.log('a')})
// Uncaught SyntaxError: Unexpected token 'while'
これらを使用する場合は、
- 事前に関数を用意する
- 直接即時関数を書き込む
- 論理演算子・三項演算子や
forEach
などの代替機能を使用する
などすると良いです。
#パラメータのFormData化を実装する
パラメータもFormData
も、どちらもサーバーに情報を送るフォーマットの一つです。普通どちらを使っても問題はありませんが、念のためパラメータをFormData
に変換させてみましょう。
…実のところ、このクラスにおいてはFormData
変換機構はあまり実用的ではなく、ほぼ不要です。これは単に私がゲッターについて書きたいが故のものです。
##ゲッターってなんだ
オブジェクトにおける値の取得の方法を、オブジェクトに直接定義づけたものがゲッターです。ちなみに、値の設定の場合はセッターです。「なんのこっちゃ」って感じですが、要は取得はできるのに自分で直接変更できない値を設定するものです(実際は値の設定では断じてないですが、今回はそういう認識で結構です)。
class Sample {
constructor() {
this._a = 1;
}
get a() {
return this._a;
}
}
// or
function Sample() {
this._a = 1;
Object.defineProperty(this, 'a', {
get: function() {
return this._a;
}
})
}
var s = new Sample;
console.log(s.a);
// 1
// (ゲッターは関数的だが関数ではないので()を付けない)
s.a = 2;
console.log(s.a);
// 1
// (値の取得のためのものなので、代入は無意味。_aの値が変われば変わる。)
ここまでで作ったUrl
コンストラクタは、パラメータを含む各値が外部から容易に書き換えられる仕組みです。ですので、このインスタンスに登録されたパラメータが登録時のままであるという保証は無いわけです。そんな中でFormData
化したパラメータを取得するならば、動的な取得方法を作るべきです。となれば、メソッドやゲッターが候補に挙がります。しかしインスタンスの値や何かしらのオブジェクトに対して変更を加えるわけでもないので、これをメソッドとして作るのはちょっとダサいです(繰り返しになりますが、単に私がゲッターを使いたいだけです。実際にはメソッドでも別にいいかなあ、だなんて思っています)。そこで、ゲッターの出番なのです。
##実際に実装する
ここまでくると、もはや書くべきことはほとんどありません。強いて挙げるならば、``FormData`コンストラクタに配列を登録するときの挙動についてでしょうか。
var f = new FormData;
f.append('a', [1, 2, 3]);
f.append('b[]', 1);
f.append('b[]', 2);
f.append('b[]', 3);
console.log(f.get('a'));
// "1,2,3"
console.log(f.get('b[]'));
// "3"
console.log(f.getAll('b[]'));
// ["1", "2", "3"]
こんな具合になります。これを念頭に置いて書き加えてみましょう。整形方法はString化と大して変わりません。
**Url.js(折りたたみ)**
(function(global){
var REG = {
URL: {
R: new RegExp('^(?:([^:\\s]+:)|)(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)(\\/?[^\\?#]*)(?:\\?([^\\?#]*)|)(?:#(.*)|)$','i'),
G: ['protocol','username','password','host','port','path','search','hash']
}
};
var PARAM = {
BASE: function(s) {// [['p', '1'], ['t', 'text']]
return (s || '').split('&').filter(function(v){return v}).map(function(v){return (v = v.split('='), v[0] = decodeURI(v[0]).replace(/\+/g, ' '), v)})
},
ARRAY_TYPE: {
'comma': {
PARSE: function(s) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
v[1] = v[1].split(',').map(function(p){return decodeURI(p).replace(/\+/g, ' ')});
o[v[0]] = v[1].length - 1 ? v[1] : v[1][0];
});
return o;
},
TO_STR: function(o) {
return Object.keys(o).map(function(k) {
return (k + '=' + Array.isArray(o[k])
? o[k].map(function(v){
return encodeURIComponent(v)
}).join(',')
: encodeURI(o[k]))
}).join('&')
},
TO_FORM: function(o) {
var f = new FormData;
return (Object.keys(o).forEach(function(k) {
f.append(k, o[k])
}), f)
}
},
'sepalate': {
PARSE: function(s) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
o[v[0]] = o[v[0]]
? (function(p, a){
return ((a = Array.isArray(p) ? p : [p]).push(v[1]), a);
})(o[v[0]])
: v[1]
});
return o;
},
TO_STR: function(o) {
return Object.keys(o).map(function(k) {
return (Array.isArray(o[k])
? o[k].map(function(v){
return k + '=' + encodeURI(v)
})
: k + '=' + encodeURI(o[k]))
}).reduce(function(a,v){
return a.concat(v)
},[]).join('&')
},
TO_FORM: function(o) {
var f = new FormData;
return (Object.keys(o).forEach(function(k) {
Array.isArray(o[k])
? o[k].forEach(function(v){
f.append(k, v)
})
: f.append(k, o[k])
}), f)
}
},
'with_bracket': {
PARSE: function(s, i) {
var b = PARAM.BASE(s), o = {};
b.forEach(function(v){
var m;
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
(m=v[0].match(new RegExp(i ? '^(.+?)\\[(\\d+?)\\]$' : '^(.+?)\\[\\]$')))
? (o[m[1]] = (function(p, a){
return (a = (p==void(0)) ? [] : (Array.isArray(p) ? p : [p]), (m[2] ? (a[Number(m[2])] = v[1]) : a.push(v[1])), a);
})(o[m[1]]))
: (o[v[0]] = v[1])
});
return o;
},
TO_STR: function(o, i) {
return Object.keys(o).map(function(k) {
return (Array.isArray(o[k])
? o[k].map(function(v,_){
return k + '[' + (i ? _ : '') + ']' + '=' + encodeURI(v)
})
: k + '=' + encodeURI(o[k]))
}).reduce(function(a,v){
return a.concat(v)
},[]).join('&')
},
TO_FORM: function(o, i) {
var f = new FormData;
return (Object.keys(o).forEach(function(k) {
Array.isArray(o[k])
? o[k].forEach(function(v,_){
f.append(k + '[' + (i ? _ : '') + ']', v)
})
: f.append(k, o[k])
}), f)
}
},
}
}
function E(s) {
var e = new Error(s);
e.name = 'UrlError';
throw e;
}
function Url(url, option) {
var s = this, u, arrayType = (option && option.arrayType) || 'with_bracket', indexed = (option && option.indexed) || false;
(u = (url ? String(url) : '').match(REG.URL.R))
? (REG.URL.G.forEach(function(g,i){
s[g] = u[i+1]
}),
s.path = s.path || '/',
s.search = (function(t){
return t ? t.PARSE(s.search, indexed) : (E('Invalid arrayType: "' + arrayType + '"'), void(0))
})(PARAM.ARRAY_TYPE[arrayType]),
s._parseStyle = {array: {arrayType: arrayType, indexed: indexed}},
Object.defineProperty(s, 'FormData', {
get: function() {
var s = this, a = s._parseStyle.array;
return(function(t){
return t ? t.TO_FORM(s.search, indexed) : (E('Invalid arrayType: "' + arrayType + '"'), void(0))
})(PARAM.ARRAY_TYPE[arrayType])
}
})
)
: E('Invalid URL: "' + url + '"');
}
Url.prototype.toString = function() {
var s = this, a = s._parseStyle.array;
return (s.protocol && s.host
? s.protocol + '//' + (s.username && s.password
? s.username + ':' + s.password + '@'
: '')
+ s.host + (s.port ? ':' + s.port : '')
: ''
)
+ s.path
+ (Object.keys(s.search).length
? '?' + (function(t) {
return t ? t.TO_STR(s.search, a.indexed).replace(/%20/g,'+') : (E('Invalid arrayType: "' + a.arrayType + '"'), '')
})(PARAM.ARRAY_TYPE[a.arrayType])
: ''
)
+ (s.hash ? '#' + s.hash : '')
}
Url.prototype.resolve = function(rel_path) {
var rs, bs = this.path.split('/'), rl = new Url(rel_path, this._parseStyle.array), sp = rl.path.split('/').filter(function(v) {return v !== '.'});
return((this.host || E('Base URL is not a absolute path: "' + this + '"')), (rl.host && E('"' + rel_path + '" is not a relative path')), bs.pop(), sp[0] == ''
? (rs = rl.path)
: (function(v) {
while((v = sp.shift())) {
v == '..'
? bs.pop() || E('Invalid relative path: "' + rel_path + '"')
: bs.push(v)
}
rs = bs.join('/')
})(),
this.path = rs,
this.search = rl.search,
this.hash = rl.hash,
this)
}
global.Url = Url;
})(this);
var url = new Url('http://example.com/x.php?a[]=1&a[]=2');
console.log(
url.FormData.getAll('a[]')
)
// ["1", "2"]
これを…
var xhr = new XMLHttpRequest;
xhr.addEventListener('load', e=>console.log(e.responseText));
xhr.open('POST', url);
xhr.send(url.FormData);
fetch(url, {method: 'POST', body: url.FormData})
.then(v=>v.text())
.then(v=>console.log(v))
こうじゃ!できた!!
長かった…。
#おわりに
さて、いかがだったでしょうか。
JavaScriptはクラスを作らなくてもある程度は動かせる言語ですし、クラス作成に慣れていないJSユーザーも案外多いのではないかと思います。しかしクラスを作れるようになれば、やれることの幅は間違いなく広がるはずです(少なくとも私はそうでした)。
この記事は、クラス作りの指南書とするには少々、スタティックメソッドやセッターなどのその他充実した機能についての説明が欠けている些か不完全な代物かとは思います。ですがこれを機に、皆さんがクラスについてちょっとでも興味を惹かれたなら、またこの記事が皆さんのJavaScriptアビリティ向上の一助となったなら、この上なく幸いです。
ご清覧ありがとうございました。
#おまけ
class
で書き直してみた。
**Url.js(折りたたみ)**
アロー関数はthis
を持たないという性質を用いることで、インスタンスのthis
をプロパティ内の内部関数で気兼ねなく使えたりしますね。あとはゲッターの設定のしやすさなど、やはり書きやすいです。まあ、書き直す前とコード量はそんなに変わらないです。
(global=>{
const REG = {
URL: {
R: new RegExp('^(?:([^:\\s]+:)|)(?:\\/\\/(?:([^:\\/\\?#]+?):([^:@]+?)@|)([^:\\/\\?#]+)(?::([0-9]+)|)|)(\\/?[^\\?#]*)(?:\\?([^\\?#]*)|)(?:#(.*)|)$','i'),
G: ['protocol','username','password','host','port','path','search','hash']
}
}, PARAM = {
BASE: s=>{// [['p', '1'], ['t', 'text']]
return (s || '').split('&').filter(v=>v).map(v=>(v = v.split('='), v[0] = decodeURI(v[0]).replace(/\+/g, ' '), v))
},
ARRAY_TYPE: {
'comma': {
PARSE: s=>{
const b = PARAM.BASE(s), o = {};
b.forEach(v=>{
v[1] = v[1].split(',').map(p=>decodeURI(p).replace(/\+/g, ' '));
o[v[0]] = v[1].length - 1 ? v[1] : v[1][0];
});
return o;
},
TO_STR: o=>{
return Object.keys(o).map(k=>{
return (k + '=' + Array.isArray(o[k])
? o[k].map(v=>{
return encodeURIComponent(v)
}).join(',')
: encodeURI(o[k]))
}).join('&')
},
TO_FORM: o=>{
const f = new FormData;
return (Object.keys(o).forEach(k=>{
f.append(k, o[k])
}), f)
}
},
'sepalate': {
PARSE: s=>{
const b = PARAM.BASE(s), o = {};
b.forEach(v=>{
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
o[v[0]] = o[v[0]]
? ((p, a)=>((a = Array.isArray(p) ? p : [p]).push(v[1]), a))(o[v[0]])
: v[1]
});
return o;
},
TO_STR: o=>{
return Object.keys(o).map(k=>{
return (Array.isArray(o[k])
? o[k].map(v=>k + '=' + encodeURI(v))
: k + '=' + encodeURI(o[k]))
}).reduce((a,v)=>{
return a.concat(v)
},[]).join('&')
},
TO_FORM: o=>{
const f = new FormData;
return (Object.keys(o).forEach(k=>{
Array.isArray(o[k])
? o[k].forEach(v=>f.append(k, v))
: f.append(k, o[k])
}), f)
}
},
'with_bracket': {
PARSE: (s, i)=>{
const b = PARAM.BASE(s), o = {};
b.forEach(v=>{
const m = v[0].match(new RegExp(i ? '^(.+?)\\[(\\d+?)\\]$' : '^(.+?)\\[\\]$'));
v[1] = decodeURI(v[1]).replace(/\+/g, ' '),
m ? (o[m[1]] = ((p, a)=>(a = (p==void(0)) ? [] : (Array.isArray(p) ? p : [p]), (m[2] ? (a[Number(m[2])] = v[1]) : a.push(v[1])), a))(o[m[1]]))
: (o[v[0]] = v[1])
});
return o;
},
TO_STR: (o, i)=>{
return Object.keys(o).map(k=>{
return (Array.isArray(o[k])
? o[k].map((v,_)=>k + '[' + (i ? _ : '') + ']' + '=' + encodeURI(v))
: k + '=' + encodeURI(o[k]))
}).reduce((a,v)=>a.concat(v),[]).join('&')
},
TO_FORM: (o, i)=>{
const f = new FormData;
return (Object.keys(o).forEach(k=>{
Array.isArray(o[k])
? o[k].forEach((v,_)=>f.append(k + '[' + (i ? _ : '') + ']', v))
: f.append(k, o[k])
}), f)
}
},
}
}, E = s=>{
const e = new Error(s);
e.name = 'UrlError';
throw e;
}, Url = class {
constructor(url, {arrayType='with_bracket', indexed=false}={}) {
const u = (url ? String(url) : '').match(REG.URL.R);
u ? (REG.URL.G.forEach((g,i)=>{
this[g] = u[i+1]
}),
this.path = this.path || '/',
this.search = (t=>t ? t.PARSE(this.search, indexed) : (E('Invalid arrayType: "' + arrayType + '"'), void(0)))(PARAM.ARRAY_TYPE[arrayType]),
this._parseStyle = {array: {arrayType: arrayType, indexed: indexed}}
)
: E('Invalid URL: "' + url + '"');
}
get FormData() {
const a = this._parseStyle.array;
return (t=>t ? t.TO_FORM(this.search, a.indexed) : (E('Invalid arrayType: "' + a.arrayType + '"'), void(0)))(PARAM.ARRAY_TYPE[a.arrayType])
}
toString() {
const a = this._parseStyle.array;
return (this.protocol && this.host
? this.protocol + '//' + (this.username && this.password
? this.username + ':' + this.password + '@'
: '')
+ this.host + (this.port ? ':' + this.port : '')
: ''
)
+ this.path
+ (Object.keys(this.search).length
? '?' + (t=>t ? t.TO_STR(this.search, a.indexed).replace(/%20/g,'+') : (E('Invalid arrayType: "' + a.arrayType + '"'), ''))(PARAM.ARRAY_TYPE[a.arrayType])
: ''
)
+ (this.hash ? '#' + this.hash : '')
}
resolve(rel_path) {
const bs = this.path.split('/'), rl = new Url(rel_path, this._parseStyle.array), sp = rl.path.split('/').filter(v=>v !== '.'), rs = ((this.host || E('Base URL is not a absolute path: "' + this + '"')), (rl.host && E('"' + rel_path + '" is not a relative path')), bs.pop(), sp[0] == ''
? rl.path
: (v=>{
while((v = sp.shift())) {
v == '..'
? bs.pop() || E('Invalid relative path: "' + rel_path + '"')
: bs.push(v)
}
return bs.join('/')
})());
return (this.path = rs,
this.search = rl.search,
this.hash = rl.hash,
this)
}
}
global.Url = Url;
})(this);