JavaScript | テンプレートリテラル(Template Literal)を使って文字列を表す

javascrpit JavaScript
スポンサーリンク

タグ付きテンプレート実践 — HTML整形・翻訳(i18n)・スタイル生成

タグ付きテンプレートは 文字列分解 → ロジックを挟む のに最適で、HTML生成の安全化、簡易翻訳、CSS-in-JS の軽い実装などにぴったり使えます。以下に実用的でコピーして使えるサンプルを 3つ 用意します。各サンプルは説明 → 実装 → 使い方(例) → 注意点の順で短く示します。

1. HTML整形(自動エスケープ & 危険な挿入の防止)

目的:ユーザー入力などを埋め込む際に XSS を防ぎつつ、テンプレートで分かりやすく HTML を組み立てる。

// --- helper: HTML エスケープ ---
function escapeHTML(str) {
  if (str == null) return '';
  return String(str).replace(/[&<>"']/g, c => ({
    '&': '&',
    '<': '<',
    '>': '>',
    '"': '"',
    "'": '''
  }[c]));
}

// --- tagged template: html ---
// strings: テンプレート文字列のリテラル部分の配列
// values: 埋め込み値の配列
function html(strings, ...values) {
  // 値をエスケープして結合
  let result = '';

  for (let i = 0; i < strings.length; i++) {
    result += strings[i];
    if (i < values.length) {
      // 値が安全に埋め込まれる(デフォルトエスケープ)
      result += escapeHTML(values[i]);
    }
  }
  return result;
}

// --- 利用例 ---
const user = { name: "<script>alert('X')</script>", bio: "JS好き\n改行あり" };
const node = html`
  <div class="user">
    <h2>${user.name}</h2>
    <pre>${user.bio}</pre>
  </div>
`;
console.log(node);
// 生成 HTML は安全にエスケープされるので DOM に直接入れても安全性が高くなる
// document.body.innerHTML = node;
JavaScript

注意:

  • デフォルトで全てエスケープする実装。安全第一。
  • 意図的に「この値は生HTML」としたい場合は unsafe() のようなホワイトリスト関数を用意して別扱いにする(でも慎重に)。

例(unsafe を許すパターン):

function raw(v) { return { __raw: String(v) }; }

function htmlAllowRaw(strings, ...values) {
  let r = '';
  for (let i = 0; i < strings.length; i++) {
    r += strings[i];
    if (i < values.length) {
      const v = values[i];
      r += (v && v.__raw) ? v.__raw : escapeHTML(v);
    }
  }
  return r;
}
JavaScript

2. 翻訳(i18n)タグ付きテンプレート — 簡易版

目的:テンプレートの定型文をキー化して置換、かつ ${} 中の値をローカライズ対応で差し込む。実務では複雑な plural/locale が必要だが、まずは基本形。

// --- 簡易翻訳辞書(実運用は外部ファイル/JSON) ---
const messages = {
  en: {
    "greet": "Hello, ${name}! You have ${count} new messages.",
    "items": "You have ${count} item(s)."
  },
  ja: {
    "greet": "こんにちは、${name}さん! ${count} 件の新着があります。",
    "items": "アイテムが ${count} 個あります。"
  }
};

// --- translator tagged template ---
// 使い方: t`greet`({ name: 'Halu', count: 3 })
function t(strings, ..._vals) {
  // このタグは `t\`message_key\`` のように使うことを想定(templates の中身は1要素)
  const key = strings.join('');
  return (params = {}, locale = 'en') => {
    let template = (messages[locale] && messages[locale][key]) || messages['en'][key] || key;
    // テンプレート中の ${...} を params によって置換(安全のためエスケープしても良い)
    return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
      // ここではシンプルに params[expr.trim()] を返す(式評価は避ける)
      const name = expr.trim();
      return params[name] == null ? '' : params[name];
    });
  };
}

// --- 利用例 ---
const greetEn = t`greet`({ name: 'Halu', count: 5 }, 'en');
const greetJa = t`greet`({ name: 'ハル', count: 2 }, 'ja');
console.log(greetEn); // Hello, Halu! You have 5 new messages.
console.log(greetJa); // こんにちは、ハルさん! 2 件の新着があります。
JavaScript

注意・拡張案:

  • 実務では plural(1個 vs 複数)の処理、日付・数値のローカライズが必要 → Intl API や i18next 等のライブラリと組み合わせる。
  • ここではテンプレート内の式を評価していない(安全)。式評価が必要なら、明示的に許可されたキーだけにする。

3. スタイル生成(簡易 CSS-in-JS)タグ付きテンプレート

目的:テンプレートで CSS を書き、ユニークなクラス名を返して <style> に注入する小さなユーティリティ。軽量なコンポーネントスタイルに使える。

// --- ユニークなハッシュ(簡易) ---
function simpleHash(s) {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
  }
  return ('c' + (h >>> 0).toString(36));
}

// --- style tag handler ---
// usage: const className = style`color: ${color}; padding: 8px;`
const styleSheet = (function() {
  const head = document.head || document.getElementsByTagName('head')[0];
  const styleEl = document.createElement('style');
  head.appendChild(styleEl);
  return styleEl.sheet;
})();

function style(strings, ...values) {
  // 結合して CSS テキストを作る
  let css = '';
  for (let i = 0; i < strings.length; i++) {
    css += strings[i];
    if (i < values.length) css += values[i];
  }
  // クラス名生成
  const className = simpleHash(css);
  // 既に追加済みか確認(重複防止)
  const rule = `.${className} { ${css} }`;
  // 簡易チェック:既存ルールに同一文字列があるか
  const exists = Array.from(styleSheet.cssRules).some(r => r.cssText === rule);
  if (!exists) {
    styleSheet.insertRule(rule, styleSheet.cssRules.length);
  }
  return className;
}

// --- 利用例 ---
const btnClass = style`background: #007bff; color: white; padding: 8px 12px; border-radius: 6px;`;
const el = document.createElement('button');
el.className = btnClass;
el.textContent = 'クリック';
document.body.appendChild(el);
JavaScript

注意:

  • この実装は非常に簡易的(scoping、メディアクエリ、擬似セレクタ、重ね順、同名の CSS 変化などは考慮していない)。実践では styled-components / emotion のような成熟ライブラリを推奨。
  • テンプレートの ${} にオブジェクトを入れるとそのまま toString() される。必要ならユーティリティで値のバリデーションを行う。

よくあるパターン & 実用上のアドバイス

  1. デフォルトは「エスケープして安全に出力」 を基本にする(特に HTML 関連)。
  2. タグ付きテンプレートは分離責務が強い:テンプレートはプレゼンテーション、タグ関数はロジック(エスケープ、翻訳、注入)にする。
  3. テンプレートの中で式評価(eval 的な処理)は危険)。可能な限り明示的なキー/パラメータ置換にとどめる。
  4. パフォーマンス:大量に生成する場合はキャッシュ(翻訳済みテンプレート、生成済み CSS 等)を用意する。
  5. テスト:出力のユニットテスト(期待される HTML / CSS / 翻訳文字列)を用意する。
タイトルとURLをコピーしました