まくまくJavaScriptノート
URL エンコード/デコードを行う (encodeURI, encodeURIComponent)
2018-12-03
ユーザ入力に基づいて URI を構成する場合は、URI として不正な文字が含まれないようにパーセントエンコーディング (percent-encoding) を行う必要があります。

URI の構成と予約文字

URI は下記のような構成をしています (RFC3986)。

encode-uri.png

各パートの区切り文字として、次のような予約文字 (reserved characters) が定義されており、例えば query 部分のデータとしてこれらの文字を含む場合は、適切にパーセントエンコーディングしてやる必要があります。

予約文字 (reserved characters)

reserved    = gen-delims / sub-delims
gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims  = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="

sub-delims に分類された文字をエンコードすべきかは、作成するシステムによりますが、少なくとも &= などは、query 文字列のキー&バリューの区切りに使用されるのでパーセントエンコーディングの必要があるでしょう。

一方で、URI の各パート内にそのまま含められる文字として、次のような非予約文字 (nonreserved characters) も定義されています。

非予約文字 (nonreserved characters)

unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"

これらは、URI のどのパートにおいてもそのまま使用できる文字です。 これらの文字もパーセントエンコーディングで表すことはできますが(例: A%41)、無意味なエンコーディングは避けた方がよいでしょう(RFC3986 においても、そのようなパーセントエンコーディングは行うべきではないとされています)。

JavaScript による URI エンコーディング

JavaScript には、URI のパーセントエンコード(およびデコード)を行うための関数として下記のものが用意されています。

  • encodeURI
    • URI 全体を表す文字列の中の、マルチバイト文字などをパーセントエンコードします。URI を構成するセパレータ文字などは正しく使用している前提で動作するため、/? などの予約語はエスケープされません。
  • decodeURI
    • encodeURI によってエンコードされた文字列を元に戻します。
  • encodeURIComponent
    • 下記の文字を除くすべての文字をパーセントエンコードします: アルファベット、10進数字、-_.!~*()
  • decodeURIComponent
    • encodeURIComponent によってエンコードされた文字列を元に戻します。

下記の表は、変換前の文字 (ch) が、encodeURI 関数と encodeURIComponent 関数によってどのようにエンコードされるかを示しています(表の生成に使用したスクリプト)。

ch encodeURI(ch) encodeURIComponent(ch)
%E3%81%82 %E3%81%82
` ` (スペース) %20 %20
A A A
; ; %3B
, , %2C
/ / %2F
? ? %3F
: : %3A
@ @ %40
& & %26
= = %3D
+ + %2B
$ $ %24
# # %23
- - -
_ _ _
. . .
! ! !
~ ~ ~
* * *
' ' '
( ( (
) ) )
[ %5B %5B
] %5D %5D

URI を構成する際に、パーセントエンコーディングが必要になる主なケースは、query 部分の値としてユーザ入力値を使用するケースでしょう。 例えば、ユーザが Web サイトのフォーム上で Nuts&Milk のような値を入力した場合、その値を query 部分の値として扱うには、&%26 にエスケープしなければいけません。

const userInput = 'Nuts&Milk';
console.log(encodeURIComponent(userInput));  //=> 'Nuts%26Milk'

query 部分の文字列として、key1=val1&key2=val2&key3=val3 といった 3 対のキー&バリューで構成したい場合は、val1val2val3 のそれぞれの値を encodeURIComponent でエスケープしてから結合しなければならないことに注意してください。 3 対のキー&バリューを &= で結合した後で、まとめて encodeURIComponent で変換してしまうと、セパレータ文字として扱うべき &= までエスケープされてしまいます。

JavaScript でユーザ入力を含む query 文字列を構成する場合、下記のようなユーティリティ関数を使用できます。

// キー&バリューのオブジェクトから query 文字列を作成する
function buildQuery(params) {
  const esc = encodeURIComponent;
  return Object.keys(params).map(k => {
    return esc(k) + '=' + esc(params[k]);
  }).join('&');
}

ECMAScript 2017 の Object.entries() を使用すると、下記のように記述できます。

function buildQuery(params) {
  return Object.entries(params)
    .map(pair => pair.map(encodeURIComponent).join('='))
    .join('&');
}

使用例

const params = {
  key1: 'a&b',  // ユーザー入力で構成されていると仮定
  key2: 'c&d'
};

const query = buildQuery(params);
console.log(query);  //=> 'key1=a%26b&key2=c%26d'

Node.js の場合

Node.js では、組み込みの querystring モジュール を使ってクエリ文字列を構築することができます。

const querystring = require('querystring');

const query = querystring.stringify({ key1: 'a&b', key2: 'c&d' });
console.log(query);  //=> 'key1=a%26b&key2=c%26d'
2018-12-03