jQuery 非依存・JSONP 配信の Yubinbango から、ESM + TypeScript + fetch の jpzip-js に移すための実務ガイドです。HTML 側の
class="h-adr"構成は無修正のまま、自動入力ロジックだけを 30 行のシムに差し替えます。
#TL;DR
- Yubinbango のコードは 2017 年でほぼ凍結(
yubinbango-core0.6.3 / 2016-06-30、yubinbango本体の最後のコード変更は 2017-02-18)。データだけ毎月更新されている - JSONP で 3 桁プレフィックスの
.jsを<script>注入して読む方式は、CSPscript-srcを緩める ことを強要する - jpzip-js は fetch だけで完結。
connect-src https://jpzip.nadai.devを許可すればscript-src 'self'のまま動く - HTML 側の
h-adrフォームは無修正。30 行の互換シムで.p-postal-code→.p-region/.p-locality/.p-street-addressの自動入力を再現できる - 副次効果として TypeScript 型・ESM/CJS デュアル配布・Node / Cloudflare Workers でも動く・複数町域 (towns 配列) を扱える が手に入る
#なぜ移行するか
Yubinbango は 10 年以上前から日本語の住所自動入力フォームの de-facto で、現在も多くのサイトで動いています。一方で配布形態の制約から、現代のフロントエンド構成と相性が悪い場面が増えています。
| 比較項目 | Yubinbango | jpzip-js |
|---|---|---|
| パッケージ配布 | 本体は npm 未公開(yubinbango.github.io 直リンク)。yubinbango-core は npm にあるが最終更新 2016-06-30 | @jpzip/jpzip を npm から取得 |
| データ取得 | JSONP (<script> 注入 + window.$yubin(...) コールバック) | fetch で JSON を取得 |
| TypeScript 型 | なし | 同梱 (.d.ts) |
| モジュール形式 | グローバル window.YubinBango | ESM + CJS のデュアル |
| ランタイム対応 | ブラウザのみ | Node 18+ / Bun / Deno / ブラウザ / Cloudflare Workers / Vercel Edge |
| CSP 影響 | script-src に https://yubinbango.github.io を許可する必要 | connect-src https://jpzip.nadai.dev のみ |
| 複数町域への対応 | 1 件のみ返す | towns 配列で全件返す |
| ローマ字・JIS コード | なし | prefecture_roma / city_code などを同梱 |
| データ更新頻度 | yubinbango-data リポジトリで継続(2026-05-01 が直近) | 月次自動 (/blog/0002-cloudflare-pages-static-zipcode-delivery/ 参照) |
Yubinbango 自体は jQuery に依存していません。これは古い記事でしばしば誤解されている点で、yubinbango.js は document.querySelectorAll と addEventListener を直接使っており、jQuery がなくても動きます。それでも書き換えたい理由は、上の表で挙げた 配布形態・型・CSP・ランタイム互換性であって、jQuery 依存ではありません。
#移行手順
#1. 既存依存の棚卸し
git grep -n 'yubinbango' -- '*.html' '*.tsx' '*.ts' '*.js' '*.vue' '*.astro'
git grep -n 'YubinBango' -- '*.ts' '*.tsx' '*.js' '*.vue'
git grep -n 'p-postal-code\|h-adr' -- '*.html' '*.tsx' '*.vue' '*.astro'
3 つを見ます。
yubinbango.jsの<script>読み込み箇所new YubinBango.Core(...)を直接呼んでいる箇所class="h-adr"とclass="p-postal-code"を含む HTML
最後の HTML はそのまま使い続けるので、書き換え対象は最初の 2 つです。
#2. jpzip-js のインストール
npm install @jpzip/jpzip
zero runtime deps なので、package.json の dependencies に 1 行増えるだけです。tree-shaking が効くため、lookup だけ使う場合のバンドル増加は実測で 4 KiB 程度です(後述の計測セクション)。
#3. 互換シムを書く
yubinbango-shim.ts をプロジェクトに追加します。h-adr フォーム上の .p-postal-code の input イベントを購読し、jpzip.lookup の結果を都道府県・市区町村・町域に書き込む 30 行ほどのコードです。
import { lookup } from '@jpzip/jpzip';
const ZIP_RE = /\d{7}/;
const setField = (form: HTMLElement, sel: string, value: string) => {
const el = form.querySelector<HTMLInputElement>(sel);
if (el) el.value = value;
};
const fillAddress = async (input: HTMLInputElement) => {
const form = input.closest<HTMLElement>('.h-adr');
if (!form) return;
const raw = input.value.replace(/[^\d]/g, '');
if (!ZIP_RE.test(raw)) return;
const entry = await lookup(raw);
if (!entry) return;
// 複数町域が返る場合は先頭を採用 (Yubinbango 互換動作)
const town = entry.towns[0];
setField(form, '.p-region', entry.prefecture);
setField(form, '.p-locality', entry.city);
setField(form, '.p-street-address', town?.town ?? '');
};
export const initYubinbangoShim = () => {
document.querySelectorAll<HTMLInputElement>('.h-adr .p-postal-code').forEach((input) => {
input.addEventListener('input', () => {
void fillAddress(input);
});
});
};
ポイント:
- DOM に触れるのはこの 30 行だけ。
@jpzip/jpzip自体は DOM API に依存しないので、別の枠組み(React / Vue / Svelte) に乗せたくなったらlookupだけ呼ぶフックに書き換えれば済みます lookupはnullを返すケース(該当なし・入力不正)があるため必ず分岐するtowns[0]は Yubinbango 互換動作。複数町域を UI に出したい場合はtownsをそのまま渡す
#4. <script> タグから ESM import に切り替え
HTML から既存の <script> 読み込みを削除します。
- <script src="https://yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script>
エントリポイント側で初期化します。
import { initYubinbangoShim } from './yubinbango-shim';
initYubinbangoShim();
new YubinBango.Core(...) を直接呼んでいた箇所は、jpzip-js の lookup を直接呼ぶ形に書き換えます。
- new YubinBango.Core(zipcode, (addr) => {
- form.region.value = addr.region;
- form.locality.value = addr.locality;
- form.street.value = addr.street;
- });
+ const entry = await lookup(zipcode);
+ if (entry) {
+ form.region.value = entry.prefecture;
+ form.locality.value = entry.city;
+ form.street.value = entry.towns[0]?.town ?? '';
+ }
Yubinbango の region_id (JIS 都道府県番号) に相当するのは jpzip では entry.prefecture_code です。読み替えてください。
#5. CSP を引き締める
Yubinbango を使っていたサイトの CSP は、JSONP のため script-src に外部オリジンを許可していたはずです。
- Content-Security-Policy: script-src 'self' https://yubinbango.github.io;
+ Content-Security-Policy: script-src 'self'; connect-src 'self' https://jpzip.nadai.dev;
<script> 注入が消えるので script-src を 'self' まで絞れます。データ取得は fetch 経由なので connect-src を許可します。
#6. 動作確認
横浜市庁舎の郵便番号 231-0017 (神奈川県横浜市中区本町) と東京都庁の 163-8001 (東京都新宿区西新宿) で手動入力テストを行います。Vitest なら以下のように書けます。
import { describe, it, expect, beforeEach, vi } from 'vitest';
import userEvent from '@testing-library/user-event';
import { initYubinbangoShim } from './yubinbango-shim';
describe('yubinbango-shim', () => {
beforeEach(() => {
document.body.innerHTML = `
<form class="h-adr">
<input class="p-postal-code" />
<input class="p-region" />
<input class="p-locality" />
<input class="p-street-address" />
</form>
`;
initYubinbangoShim();
});
it('fills address fields when a valid zipcode is typed', async () => {
const user = userEvent.setup();
const zip = document.querySelector('.p-postal-code') as HTMLInputElement;
await user.type(zip, '2310017');
// jpzip.lookup は network call なので vi.mock しても、msw でモックしても OK
await vi.waitFor(() => {
const region = document.querySelector('.p-region') as HTMLInputElement;
expect(region.value).toBe('神奈川県');
});
});
});
#ハマりやすい所
- input イベントの発火頻度: 7 桁すべて入力された瞬間にだけ
lookupを呼びたい場合は、シム内でZIP_RE.test(raw)の前にデバウンスを挟むか、raw.length === 7で gate する - IME 確定タイミング: 日本語フォームでは
compositionstart/compositionendを考慮していないと、変換確定時に余分な発火が起きる。郵便番号フィールドに IME を使うケースはまれだが、コピペ補完で起きうる - 複数町域: 同一郵便番号で町名が分岐するケース(企業向け郵便番号や一部の地域)では
towns.length > 1になる。先頭採用で問題ないかは要件次第 - SSR でのフォーム初期描画: Next.js / Astro などで HTML を静的出力する場合、初期 DOM には
lookupをかけない。initYubinbangoShim()をuseEffect/client:loadで遅延させる - テストでの実 lookup:
lookupは CDN に fetch するので、テストでは MSW (Mock Service Worker) などでスタブする
#計測した結果
社内サンプルアプリ (Vite + TypeScript、フォーム 3 つ) で計測した実測値です。
| 指標 | Yubinbango | jpzip-js |
|---|---|---|
| 初回 lookup レイテンシ (p50, Tokyo → Cloudflare edge) | 約 180 ms | 約 70 ms |
| 2 回目以降 (キャッシュヒット) | 約 180 ms (毎回 JSONP) | 約 0.3 ms (L1) |
| バンドル増加 (gzip) | 0 (外部 <script>) | 約 4 KiB (lookup のみ) |
必要な CSP script-src 追加 | https://yubinbango.github.io | なし |
| TypeScript 型 | 自前で declaration file が必要 | 同梱 |
キャッシュ挙動の差が一番大きい: Yubinbango は <script> 注入のたびに新規 fetch になる(同一プレフィックスでもブラウザキャッシュに任せる以外の手段がない)のに対し、jpzip-js は L1 LRU を持っているので 2 回目以降のレイテンシが事実上ゼロになります。
#まとめ
Yubinbango のコード本体は実質凍結ですが、データ更新は続いていて壊れたわけではありません。とはいえ「JSONP で script-src を緩める」「TypeScript 型がない」「Node でも Workers でも動かない」という配布形態の限界は、現代の構成では地味に効いてきます。
jpzip-js への移行は、HTML 側の class="h-adr" 構成を残したまま、30 行のシムで完了します。CSP を一段引き締められる副次効果と、月次自動更新の安心感がついてきます。
関連:
- jpzip の全体像 — なぜ Cloudflare Pages の無料枠で配信しているのか
- 120,677 件の配信設計 — JSON のチャンク分割と L1/L2 キャッシュ戦略
- Claude / Cursor から MCP 経由で使う — MCP サーバーの作り方