URLの構造と相対URL
URL APIで各部分を取り出し、相対URLを基準URLへ結び付けます。日本語や区切り文字の符号化を確認し、最後にHTTP要求へ入るパス・クエリと、文書内の位置を指定するフラグメントを対応させます。
説明
URLには接続方式、宛先、資源のパス、検索条件、文書内の位置などが含まれます。それぞれの部分は用途が異なり、表示されたURL全体がそのままHTTPの要求行へ入るわけではありません。
URL APIで各部分を取り出し、相対URLを基準URLへ結び付けます。日本語や区切り文字の符号化を確認し、最後にHTTP要求へ入るパス・クエリと、文書内の位置を指定するフラグメントを対応させます。
1URLを解析して各部分を取得
ホスト名に大文字を含み、ポート8443、検索条件、文書内の位置を指定したURLを解析します。
var u = new URLURLを解析("https://Learn.Example.com:8443/guide/start?q=network#part2"); ({href:u.href, scheme:u.protocol方式, hostname:u.hostname, port:u.portホストとポート, path:u.pathnameパス, query:u.search, fragment:u.hashクエリとフラグメント})ホスト名は小文字へ正規化されます。protocolはhttps:、hostnameにはポートが含まれず、portは8443です。searchは?、hashは#から始まる形で取得できます。
2HTTP/1.1の要求へ入る部分
通常のorigin-formの要求行とHostヘッダーを、解析した値から組み立てます。
var target = u.pathname + u.searchパスとクエリを要求対象にする; "GET " + target + " HTTP/1.1\r\nHost: "要求行とHostを作る + u.hostホストと明示ポート + "\r\n\r\n"要求対象は/guide/start?q=network、Hostはlearn.example.com:8443です。httpsという接続方式や#part2は、この要求対象には入りません。表示の\r\nはCRLFの改行で、最後の空行はヘッダーの終端です。
3フラグメントだけを変更する
文書内の位置をpart3へ変え、要求対象が変わるか比較します。
u.hash = "part3"文書内の位置だけを変更; ({href:u.href, target:u.pathname + u.search, unchanged:target === u.pathname + u.searchHTTP要求対象の一致を確認})URLの末尾は変わりますが、要求対象は同じです。文書内のidへ移動するリンクでは、ブラウザ側がフラグメントを扱います。スクリプトが別途その値を送信する場合は、通常の要求対象とは別の処理です。
4基準URLから相対URLを解決する
末尾が/の基準URLに対して、ファイル名、親階層、ルート、クエリだけ、フラグメントだけを指定します。
var base = "https://learn.example.com/guide/"基準URL; ["chapter.html","../img/logo.svg","/help","?q=2","#summary"].map(ref => [ref,new URL(ref,base).href相対参照を絶対URLへ解決])chapter.htmlはguide内、../は一つ上、先頭/は同じ宛先のルートから解決されます。?だけの指定はパスを維持してクエリを変更し、#だけの指定はフラグメントを変更します。
5末尾スラッシュの有無
同じchapter.htmlを、末尾/あり・なしの基準へ解決します。
["https://learn.example.com/guide/","https://learn.example.com/guide"].map(base => new URL("chapter.html",base).href同じ相対参照を二つの基準へ解決)末尾/があれば/guide/chapter.html、なければ/chapter.htmlです。後者ではguideが基準の最後のパス部分として置き換わります。サーバー上で実際にフォルダが存在するかを調べて決めているわけではありません。
6日本語をUTF-8のバイトへ変換する
"あ"のUTF-8のバイト列と、URLの部品としてのpercent encodingを比較します。
({bytes:[...new TextEncoder().encode("あ")UTF-8の3バイトへ変換].map(b=>b.toString(16).padStart(2,"0")各バイトを16進2桁で表示), encoded:encodeURIComponent("あ")URL部品として符号化})"あ"はe3 81 82の3バイトです。各バイトを%と16進2桁で表すと%E3%81%82になります。文字一つを%一つで表す方式ではありません。
7値に含まれる区切り文字を符号化する
検索値「あ & +」をURLSearchParamsへ設定します。
var params = new URLSearchParams()クエリ項目を扱うAPI; params.set("q","あ & +")値を符号化前の文字列で設定; ({query:params.toString()フォーム形式へ直列化, value:params.get("q")解釈した値を取得})フォーム形式では空白は+、値に含まれる&は%26、文字としての+は%2Bになります。区切りとしての&と、値の一部の&を分けて扱えるようにします。
8同名の項目と空の値
同じtagを二つ追加し、emptyには空文字列を指定します。
params.append("tag","web"); params.append("tag","url")同じ名前で二件追加; params.set("empty","")存在する空の値; ({query:params.toString(), first:params.get("tag")最初の値, all:params.getAll("tag")同名の全値, empty:params.get("empty"), missing:params.get("missing")存在しない項目})getは最初のtag、getAllは二つのtagを返します。emptyは存在する空文字列で、存在しないmissingのnullとは異なります。同名項目を一つへ潰してよいかは、受け取る側の仕様にも依存します。
9クエリを反映した要求対象
URLのクエリへ直列化した値を設定し、送信対象とフラグメントをもう一度表示します。
u.search = params.toString()符号化済みのクエリを反映; ({url:u.href, requestTarget:u.pathname + u.search送信する要求対象, host:u.host, fragment:u.hash文書内の位置})日本語と区切り文字を符号化したクエリが要求対象に入ります。#part3は別に残り、HTTPの要求対象には入りません。URLの見た目ではなく、各部分を解析して組み立てることで境界を保てます。
まとめ
URLは接続先、パス、クエリ、フラグメントを分けて扱います。通常のHTTP要求対象はパスとクエリです。
相対参照は基準URLと末尾スラッシュによって解決結果が変わります。
percent encodingはバイトを表現します。項目名・値・区切りをAPIで扱い、同名項目や空値の意味も確認します。
操作履歴と解説を振り返れます。
JavaScript / URL API
var u = new URL("https://Learn.Example.com:8443/guide/start?q=network#part2"); ({href:u.href, scheme:u.protocol, hostname:u.hostname, port:u.port, path:u.pathname, query:u.search, fragment:u.hash}){
href: 'https://learn.example.com:8443/guide/start?q=network#part2',
scheme: 'https:',
hostname: 'learn.example.com',
port: '8443',
path: '/guide/start',
query: '?q=network',
fragment: '#part2'
}var target = u.pathname + u.search; "GET " + target + " HTTP/1.1\r\nHost: " + u.host + "\r\n\r\n"'GET /guide/start?q=network HTTP/1.1\r\nHost: learn.example.com:8443\r\n\r\n'
u.hash = "part3"; ({href:u.href, target:u.pathname + u.search, unchanged:target === u.pathname + u.search}){
href: 'https://learn.example.com:8443/guide/start?q=network#part3',
target: '/guide/start?q=network',
unchanged: true
}var base = "https://learn.example.com/guide/"; ["chapter.html","../img/logo.svg","/help","?q=2","#summary"].map(ref => [ref,new URL(ref,base).href])[ [ 'chapter.html', 'https://learn.example.com/guide/chapter.html' ], [ '../img/logo.svg', 'https://learn.example.com/img/logo.svg' ], [ '/help', 'https://learn.example.com/help' ], [ '?q=2', 'https://learn.example.com/guide/?q=2' ], [ '#summary', 'https://learn.example.com/guide/#summary' ] ]
["https://learn.example.com/guide/","https://learn.example.com/guide"].map(base => new URL("chapter.html",base).href)[ 'https://learn.example.com/guide/chapter.html', 'https://learn.example.com/chapter.html' ]
({bytes:[...new TextEncoder().encode("あ")].map(b=>b.toString(16).padStart(2,"0")), encoded:encodeURIComponent("あ")}){ bytes: [ 'e3', '81', '82' ], encoded: '%E3%81%82' }var params = new URLSearchParams(); params.set("q","あ & +"); ({query:params.toString(), value:params.get("q")}){ query: 'q=%E3%81%82+%26+%2B', value: 'あ & +' }params.append("tag","web"); params.append("tag","url"); params.set("empty",""); ({query:params.toString(), first:params.get("tag"), all:params.getAll("tag"), empty:params.get("empty"), missing:params.get("missing")}){
query: 'q=%E3%81%82+%26+%2B&tag=web&tag=url&empty=',
first: 'web',
all: [ 'web', 'url' ],
empty: '',
missing: null
}u.search = params.toString(); ({url:u.href, requestTarget:u.pathname + u.search, host:u.host, fragment:u.hash}){
url: 'https://learn.example.com:8443/guide/start?q=%E3%81%82+%26+%2B&tag=web&tag=url&empty=#part3',
requestTarget: '/guide/start?q=%E3%81%82+%26+%2B&tag=web&tag=url&empty=',
host: 'learn.example.com:8443',
fragment: '#part3'
}演習完了