jQuery

【移行ガイド】jQueryからVanilla JS/モダンJSへ脱却する方法

カービー
【移行ガイド】jQueryからVanilla JS/モダンJSへ脱却する方法
#jQuery#JavaScript#移行#リファクタリング#モダンJS

jQueryからVanilla JavaScriptへの移行方法を解説。よく使うjQueryコードの書き換え例、段階的な移行手順、注意点を紹介します。

目次

  1. なぜjQueryから移行するのか
  2. 移行の準備
  3. 基本的な書き換えパターン
  4. ユーティリティ関数の作成
  5. 段階的な移行手順
  6. 移行時の注意点
  7. まとめ

なぜjQueryから移行するのか

移行のメリット

  1. パフォーマンス向上: ライブラリのオーバーヘッドを削減
  2. ファイルサイズ削減: 約30KB(gzip)の削減
  3. 依存関係の削減: セキュリティリスクを低減
  4. モダンJS習得: 将来的なスキルアップ
  5. フレームワーク移行の準備: React/Vue.jsへの足がかり

移行すべきでない場合

  • WordPressテーマ/プラグイン開発
  • jQueryプラグインに強く依存している
  • IE11対応が必須
  • チームにVanilla JSの知識がない
  • 保守のみで機能追加の予定がない

移行の準備

ブラウザ対応の確認

ES6+の機能を使うため、対応ブラウザを確認します。

// ES6+の主な機能と対応状況
// アロー関数: IE非対応
// let/const: IE11で部分対応
// Promise: IE非対応
// fetch: IE非対応
// classList: IE10+

// ポリフィルが必要な場合
// <script src="https://polyfill.io/v3/polyfill.min.js"></script>

現状の把握

プロジェクト内でjQueryがどう使われているか調査します。

# jQueryの使用箇所を検索
grep -r "\$(" --include="*.js" ./src
grep -r "jQuery" --include="*.js" ./src

移行計画の策定

  1. 新規コード: Vanilla JSで書く
  2. 既存コード: 優先度をつけて段階的に移行
  3. テスト: 移行後の動作確認を徹底

基本的な書き換えパターン

DOM取得

// jQuery
$('#element')           // → document.querySelector('#element')
$('.class')             // → document.querySelectorAll('.class')
$('div')                // → document.querySelectorAll('div')
$('#parent .child')     // → document.querySelectorAll('#parent .child')

// 単一要素の取得
const el = document.querySelector('#element');

// 複数要素の取得(NodeListを配列に変換)
const items = [...document.querySelectorAll('.item')];
// または
const items = Array.from(document.querySelectorAll('.item'));

クラス操作

// jQuery
$('#el').addClass('active');
$('#el').removeClass('active');
$('#el').toggleClass('active');
$('#el').hasClass('active');

// Vanilla JS
const el = document.querySelector('#el');
el.classList.add('active');
el.classList.remove('active');
el.classList.toggle('active');
el.classList.contains('active');

// 複数クラスを一度に
el.classList.add('class1', 'class2', 'class3');
el.classList.remove('class1', 'class2');

属性操作

// jQuery
$('#el').attr('href');
$('#el').attr('href', 'https://example.com');
$('#el').removeAttr('disabled');
$('#el').data('id');
$('#el').data('id', 123);

// Vanilla JS
const el = document.querySelector('#el');
el.getAttribute('href');
el.setAttribute('href', 'https://example.com');
el.removeAttribute('disabled');
el.dataset.id;          // data-id属性
el.dataset.id = 123;

CSS操作

// jQuery
$('#el').css('color');
$('#el').css('color', 'red');
$('#el').css({ color: 'red', fontSize: '16px' });
$('#el').show();
$('#el').hide();

// Vanilla JS
const el = document.querySelector('#el');
getComputedStyle(el).color;
el.style.color = 'red';
Object.assign(el.style, { color: 'red', fontSize: '16px' });
el.style.display = '';       // show
el.style.display = 'none';   // hide

コンテンツ操作

// jQuery
$('#el').text();
$('#el').text('新しいテキスト');
$('#el').html();
$('#el').html('<p>HTML</p>');
$('#el').val();
$('#el').val('新しい値');

// Vanilla JS
const el = document.querySelector('#el');
el.textContent;
el.textContent = '新しいテキスト';
el.innerHTML;
el.innerHTML = '<p>HTML</p>';
el.value;
el.value = '新しい値';

要素の追加・削除

// jQuery
$('#parent').append('<p>末尾</p>');
$('#parent').prepend('<p>先頭</p>');
$('#el').after('<p>後ろ</p>');
$('#el').before('<p>前</p>');
$('#el').remove();
$('#el').empty();

// Vanilla JS
const parent = document.querySelector('#parent');
parent.insertAdjacentHTML('beforeend', '<p>末尾</p>');
parent.insertAdjacentHTML('afterbegin', '<p>先頭</p>');
el.insertAdjacentHTML('afterend', '<p>後ろ</p>');
el.insertAdjacentHTML('beforebegin', '<p>前</p>');
el.remove();
el.innerHTML = '';

// 要素として追加する場合
const newEl = document.createElement('p');
newEl.textContent = '新しい要素';
parent.appendChild(newEl);

イベント処理

// jQuery
$('#btn').on('click', function(e) { });
$('#btn').off('click');
$(document).on('click', '.dynamic', function() { });

// Vanilla JS
const btn = document.querySelector('#btn');
btn.addEventListener('click', function(e) { });
btn.removeEventListener('click', handler);

// イベント委譲
document.addEventListener('click', function(e) {
    if (e.target.matches('.dynamic')) {
        // 処理
    }
    // または closest を使用
    const target = e.target.closest('.dynamic');
    if (target) {
        // 処理
    }
});

ドキュメント準備完了

// jQuery
$(document).ready(function() { });
$(function() { });

// Vanilla JS
document.addEventListener('DOMContentLoaded', function() { });

// または、scriptをbody末尾に配置すればDOMは準備完了している

ループ処理

// jQuery
$('.items').each(function(index, el) {
    $(this).addClass('active');
});

// Vanilla JS
document.querySelectorAll('.items').forEach((el, index) => {
    el.classList.add('active');
});

// for...of も使える
for (const el of document.querySelectorAll('.items')) {
    el.classList.add('active');
}

Ajax

// jQuery
$.ajax({
    url: '/api/data',
    type: 'POST',
    data: JSON.stringify({ key: 'value' }),
    contentType: 'application/json',
    success: function(data) { },
    error: function(xhr, status, error) { }
});

// Vanilla JS (Fetch API)
fetch('/api/data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key: 'value' })
})
.then(response => {
    if (!response.ok) throw new Error('Network error');
    return response.json();
})
.then(data => { })
.catch(error => { });

// async/await
async function postData() {
    try {
        const response = await fetch('/api/data', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ key: 'value' })
        });
        if (!response.ok) throw new Error('Network error');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error(error);
    }
}

ユーティリティ関数の作成

jQueryライクな便利関数を作成して、移行をスムーズにします。

基本ユーティリティ

// シンプルなセレクタ関数
const $ = (selector, context = document) => context.querySelector(selector);
const $$ = (selector, context = document) => [...context.querySelectorAll(selector)];

// 使用例
const header = $('#header');
const items = $$('.item');
const childLinks = $$('a', header);

拡張ユーティリティ

const DOM = {
    // 要素取得
    get: (selector) => document.querySelector(selector),
    getAll: (selector) => [...document.querySelectorAll(selector)],

    // クラス操作
    addClass: (el, ...classes) => el.classList.add(...classes),
    removeClass: (el, ...classes) => el.classList.remove(...classes),
    toggleClass: (el, className) => el.classList.toggle(className),
    hasClass: (el, className) => el.classList.contains(className),

    // 表示/非表示
    show: (el) => el.style.display = '',
    hide: (el) => el.style.display = 'none',
    toggle: (el) => el.style.display = el.style.display === 'none' ? '' : 'none',

    // イベント
    on: (el, event, handler) => el.addEventListener(event, handler),
    off: (el, event, handler) => el.removeEventListener(event, handler),

    // DOMContentLoaded
    ready: (fn) => {
        if (document.readyState !== 'loading') {
            fn();
        } else {
            document.addEventListener('DOMContentLoaded', fn);
        }
    }
};

// 使用例
DOM.ready(() => {
    const btn = DOM.get('#btn');
    DOM.on(btn, 'click', () => {
        DOM.toggleClass(btn, 'active');
    });
});

Fetchラッパー

const api = {
    async get(url) {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        return response.json();
    },

    async post(url, data) {
        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        return response.json();
    },

    async put(url, data) {
        const response = await fetch(url, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        return response.json();
    },

    async delete(url) {
        const response = await fetch(url, { method: 'DELETE' });
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        return response.json();
    }
};

// 使用例
const users = await api.get('/api/users');
await api.post('/api/users', { name: '山田' });

段階的な移行手順

ステップ1: 新規コードはVanilla JSで書く

// 新しい機能はVanilla JSで実装
document.addEventListener('DOMContentLoaded', () => {
    const newFeature = document.querySelector('#new-feature');
    newFeature.addEventListener('click', handleClick);
});

ステップ2: 独立した機能から移行

依存関係の少ない小さな機能から始めます。

// Before: jQuery
$('#toggle-btn').on('click', function() {
    $('#menu').slideToggle();
});

// After: Vanilla JS + CSS
document.querySelector('#toggle-btn').addEventListener('click', () => {
    document.querySelector('#menu').classList.toggle('open');
});

// CSS
// #menu { max-height: 0; overflow: hidden; transition: max-height 0.3s; }
// #menu.open { max-height: 500px; }

ステップ3: jQueryプラグインの置き換え

// 例: スライダープラグイン
// Before: jQuery + Slick
$('.slider').slick({ ... });

// After: Vanilla JS ライブラリ (Swiper等)
import Swiper from 'swiper';
new Swiper('.slider', { ... });

ステップ4: 残りの移行とjQuery削除

すべてのjQueryコードを移行後、jQueryの読み込みを削除します。

<!-- 削除 -->
<!-- <script src="jquery.min.js"></script> -->

移行時の注意点

1. this の違い

// jQuery: this は DOM要素
$('.item').on('click', function() {
    $(this).addClass('active');  // this = クリックされた要素
});

// Vanilla JS: アロー関数では this が異なる
$$('.item').forEach(item => {
    item.addEventListener('click', function() {
        this.classList.add('active');  // OK: 通常関数
    });
});

// アロー関数を使う場合は e.target か e.currentTarget を使用
$$('.item').forEach(item => {
    item.addEventListener('click', (e) => {
        e.currentTarget.classList.add('active');
    });
});

2. NodeList は配列ではない

// querySelectorAll は NodeList を返す
const items = document.querySelectorAll('.item');

// forEach は使える
items.forEach(item => { });

// map, filter は使えない(配列に変換が必要)
const filtered = [...items].filter(item => item.classList.contains('active'));

3. fetch のエラーハンドリング

// fetch は HTTPエラーでも reject しない
fetch('/api/data')
    .then(response => {
        // 404 や 500 でも then に来る
        if (!response.ok) {
            throw new Error('HTTP error: ' + response.status);
        }
        return response.json();
    })
    .catch(error => {
        // ネットワークエラーまたは上で throw したエラー
        console.error(error);
    });

4. アニメーションはCSSを使う

// jQuery のアニメーションより CSS の方が高パフォーマンス

// CSS
// .fade { opacity: 0; transition: opacity 0.3s; }
// .fade.show { opacity: 1; }

// JavaScript
element.classList.add('show');  // フェードイン
element.classList.remove('show');  // フェードアウト

まとめ

移行チェックリスト

  • 対応ブラウザの確認
  • jQueryの使用箇所を洗い出し
  • ユーティリティ関数の準備
  • 新規コードはVanilla JSで書く
  • 独立した機能から順次移行
  • jQueryプラグインの代替を検討
  • 十分なテストを実施
  • jQueryの削除

主な書き換え早見表

jQuery Vanilla JS
$() document.querySelector()
.addClass() .classList.add()
.on() .addEventListener()
$.ajax() fetch()
.show()/.hide() CSS + classList
.animate() CSS Transitions

焦らず段階的に移行を進めることで、安全にjQueryから脱却できます。