parseQuery / stringifyQuery で URLSearchParams を使う
- 公開日:
- 更新日:
Nuxt.js(というか Vue Router)では、デフォルトのクエリ構文解析(parseQuery)・文字列化関数(stringifyQuery)を置き換えることができます。
ここでいうクエリとは、よく GET パラメータと呼ばれるもののことです。
デフォルトの stringifyQuery では、'foo bar'
という文字列を渡した時に、半角スペースがエンコードされて foo%20bar
となります。
個人的には Google 検索のように foo+bar
となってほしかったので、URLSearchParams で処理するようにします。
stringifyQuery
stringifyQuery はオブジェクトを受け取って、クエリ文字列(? 含む)に変換するのが仕事です。
最も簡単なのは、オブジェクトを URLSearchParams のコンストラクタに喰わせてしまうことです。
stringifyQuery(obj) {
if (!obj || !Object.keys(obj).length) return '' // 空オブジェクトなら処理しない
const query = (new URLSearchParams(obj)).toString()
return query ? '?' + query : ''
}
かなり簡潔に書けるのですが、クエリに配列が渡された場合の挙動がビミョーです。
const query = {
q: 'foo bar',
hoge: [2,3,5,7],
}
stringifyQuery(query) // => ?q=foo+bar&hoge=2%2C3%2C5%2C7
%2C
というのはカンマ(,)を指しているので、'2,3,5,7'
という文字列に変換されたということになります。これは、[2,3,5,7].toString()
の結果と同じです。
.split(',')
すれば配列の形には戻せますが、元々配列だったのか、'foo,bar'
という文字列だったのか、若しくは ['foo,', 'bar']
だったのか、クエリからは読み取れなくなってしまいます。
デフォルトの stringifyQuery ではどうしているのかというと、配列は同じキーを続けて表現します。PHP と似ていますが、[]
は付けません。
Google Fonts などで見られるクエリの書き方ですね。
defaultStringifyQuery(query) // => ?q=foo%20bar&hoge=2&hoge=3&hoge=5&hoge=7
また、デフォルトの stringifyQuery では、値が undefined であるキーを無視します。これはクエリを渡すときにかなり便利です。
const foo = 'foo'
const bar = someCond ? 'bar' : undefined
const baz = getSomeStr('baz') || undefined
defaultStringifyQuery({ foo, bar, baz })
// もし undefined を無視しない作りだと…
const query = { foo: 'foo' }
if (someCond) query['bar'] = 'bar'
const baz = getSomeStr('baz')
if (baz) query['baz'] = baz
stringifyQuery(query)
値が null の場合は、キーだけがクエリに追加されます。こちらは使い所がよく分かりません。
URLSearchParams では再現できないので、null も undefined と同じ扱いにしてしまって良さそうです。
defaultStringifyQuery({ foo: null, bar: [null, undefined, 0, NaN] }) // => ?foo&bar&bar=0&bar=NaN
URLSearchParams で再現する
先に URLSearchParams インスタンスを作っておいて、.append(key, value)
でクエリを追加していきます。
似たメソッドで .set(key, value)
がありますが、こちらは重複するキーを削除してしまいます。
// nuxt.config.js
export default {
router: {
stringifyQuery(obj) {
if (!obj) return ''
const keys = Object.keys(obj)
const keysMax = keys.length|0
if (!keysMax) return ''
const params = new URLSearchParams()
for (let i = 0; i < keysMax; i=(i+1)|0) {
const key = keys[i]
const val = obj[key]
const values = Array.isArray(val) ? val : [val]
const valuesMax = values.length|0
for (let j = 0; j < valuesMax; j=(j+1)|0) {
const value = values[j]
switch (value) {
case null:
case undefined:
continue
default:
params.append(key, value)
}
}
}
const query = params.toString()
return query ? '?' + query : ''
},
},
}
|0
は内部的に変数を整数型と認識させる裏技だそうです。インクリメント i++
はクッソ遅いらしいので避けたほうがいいですが、見やすさ優先で i+=1
とかでも良いんじゃないかと思います。
for of
を使っていないのは、generate 後のコードで何故か try catch 文(クッソ遅い)で囲われてしまったからです。
結局 Babel で for (var i = 0; i < length; i++)
の形に直されてしまうので、当分は自分で for 文書いたほうが良いんじゃないかと思います。
parseQuery
stringifyQuery が作ったクエリ文字列を受け取って、オブジェクトの形にするのが仕事です。
URLSearchParams のコンストラクタにクエリ文字列を与えると、クエリ内容を解析できます。
クエリ文字列の先頭に ? があっても無視されるので、一緒に渡してしまって構いません。
.getAll()
メソッドを使うと、同じキー名の値を配列で受け取ることができます。.get()
では最初の値しか取れません。
const params = new URLSearchParams('?hoge=2&hoge=3') // 先頭に ? があっても大丈夫
params.getAll('hoge') // => ['2', '3']
params.get('hoge') // => '2'
各クエリには .forEach()
でアクセスできますが、重複しているキーを飛ばしたりはしないので、Object.prototype.hasOwnProperty()
で条件分岐しながら処理します。
// nuxt.config.js
export default {
router: {
parseQuery(query) {
const queries = {}
if (query) {
const params = new URLSearchParams(query)
const hasProp = Object.prototype.hasOwnProperty
params.forEach((val, key) => {
if (!hasProp.call(queries, key)) {
const values = params.getAll(key)
queries[key] = values.length === 1 ? values[0] : values
}
})
}
return queries
},
},
}
注意点として、まず queries.hasOwnProperty()
を使うべきではありません。
クエリ文字列として ?hasOwnProperty=hoge&foo=0
が渡された場合、処理中に queries.hasOwnProperty
は文字列 'hoge'
となり、次の foo の処理でメソッドとして実行しようとしてエラーが起きるでしょう。
これを回避するには、Object のプロトタイプから hasOwnProperty を拾ってきて、.call()
で呼び出します。第1引数に this に設定するもの(ここでは queries)、第2引数以降に元々の関数の引数に設定するものを並べます。
また、params.forEach((val, key) => {})
の val
も使うべきではありません。
こちらも上記のような問題を抱えており、val は文字列とは限りません。クエリ文字列を取るときは、.get()
か .getAll()
を使いましょう。
プログラムから利用する
Vue Router の .resolve()
というメソッドを使うことで、ページの遷移なく URL を解析・生成できます。
オブジェクトを渡せば stringifyQuery が、URL 文字列を渡せば parseQuery が利用できます。とはいえ、現在のルートの情報は this.$route
から得られるので、後者を利用する場面はなさそうです。
export default {
asyncData({ app }) {
const { href } = app.router.resolve({
path: '/path/',
query: { foo: null, bar: undefined, baz: [null, undefined, 0, '', NaN] },
})
console.log(href) // => '/path/?baz=0&baz=&baz=NaN'
},
mounted() {
const { resolved } = this.$router.resolve('/path/?baz=0&baz=&baz=NaN')
console.log(resolved.path) // => '/path/'
console.log(resolved.query) // => { baz: ['0', '', 'NaN'] }
},
}
コンテキストを受け取れるプラグインでも使うことができますし、変わったところでは、モジュールのフック内から直接 stringifyQuery を触ったりできます。
// modules/hoge.js
export default function() {
this.nuxt.hook('generate:before', () => {
const url = new URL('path', 'https://example.com/api/v1/')
url.search = this.options.router.stringifyQuery({ q: 'foo bar' })
console.log(url.href) // => 'https://example.com/api/v1/path?q=foo+bar'
})
}