LoginSignup
33
24

More than 5 years have passed since last update.

【このご時世にjQuery!?】まさかのjQuery+webpack+babelでマンスリーカレンダーを作ってみた

Last updated at Posted at 2018-07-05

このReactのコード、jQueryで書いたらどうなるんだろう…

ふと、Reactを書いていてそんなことを思いました。

ここ最近Reactも主流になりつつあり、仕事でもReactを書くことが増えてきました。
Reactはstateやpropsを更新することによりviewを自動で更新してくれるのが超便利な点です。(あまり詳しくはないのですが、AngularやVueもそういうものだと思っています。)

それをもし、未だにjQueryでよろしくやっていたら、どのようにコードを書いていくのだろうと、なんか、ふと思いました。

jQueryにはどれだけお世話になったのだろう。。。
やっぱりjQueryで書かれたソースを見ると、どこか昔の恋人を思い出すような、そんな気持ちになる方も多いのではないでしょうか?

animate()でどれだけテンションが上がったことか…
$.ajax()でどれだけ感動したことか…

jQueryは、私たちのWeb制作を十二分に支えてくれて、可能性を最大限に広げてくれました。

ならば!es2015も主流になり始めた今!
その環境でjQueryを最大限に活用して、Reactで書いたほうが絶対に楽な「マンスリーカレンダー」を作っていきながら、過去をなぞっていこうではありませんか!!

webpackとbabelを使って、jQueryを書くぞ!!!

マンスリーカレンダーのイメージ

以下のような簡易的なものを作っていきます。jQueryとの甘酸っぱい過去をなぞりたいのでCSSなんて書いている余裕はありません。<table>にあたっているデフォルトのCSSを信用します。

年と月をキャプションとして表示し、その左右に前の月、次の月に行けるようにでボタンを作っています。

曜日が表示されているところは<thead>となります。<thead>の中身は見出しなので特に変わる必要はないですね。
それに対して<tbody>は、を押すたびにDOMが書き換わらないといけないですね。

この<tbody>の箇所が絶対にReactで書いたほうが楽なところです。本当にこれをjQueryで書くなんてどうかしてるぜ!

calendarパッケージを使う

カレンダーを作るにしても、「どの曜日から始まって、月に何日あるのか」というのをイチからプログラムを書いていたら大変で仕方ありません。
今回の目標はあくまでもjQueryとの甘い過去をなぞることなので、calendarというパッケージを使います。

import { Calendar } from 'calendar';

const monthlyCalendar = new Calendar().monthDays(2018, 7);

console.log(monthlyCalendar);

と書くと

[
  [1, 2, 3, 4, 5, 6, 7],
  [8, 9, 10, 11, 12, 13, 14],
  [15, 16, 17, 18, 19, 20, 21],
  [22, 23, 24, 25, 26, 27, 28],
  [29, 30, 31, 0, 0, 0, 0]
]

というArrayが返ってくるので、これを使ってカレンダーの見た目にしていきます。便利!

開発環境の構築

使ったnodeのバージョンはv8.7.0で、npmのバージョンは5.5.1です。(アップデートサボりました…)

フォルダ構成は以下のようにしています。

.
├── .babelrc
├── package-lock.json
├── package.json
├── public
│   ├── bundle.js
│   └── index.html
├── src
│   └── index.js
└── webpack.config.develop.babel.js

以下より各設定ファイルの紹介をしていきます。

package.json

package.json
{
  "name": "jquery-calendar",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --mode development --hot --inline --config webpack.config.develop.babel.js",
    "dev-build": "webpack --mode development --config webpack.config.develop.babel.js"
  },
  "keywords": [],
  "author": "rhirayamaaan",
  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.4",
    "babel-preset-es2015": "^6.24.1",
    "webpack": "^4.14.0",
    "webpack-cli": "^3.0.8",
    "webpack-dev-server": "^3.1.4"
  },
  "dependencies": {
    "calendar": "^0.1.0",
    "jquery": "^3.3.1",
    "moment": "^2.22.2",
    "moment-timezone": "^0.5.21"
  }
}

今回は年月日を扱うのでDateオブジェクトが必要になります。
dependenciesにインストールしているmomentというのはDateをとても扱いやすくしているものなので、使用していこうと思います。
momentの使い方はこちらを参考にしました。
また、タイムゾーンも設定しようと思ったので、moment-timezoneもインストールしています。

jQueryはせっかくwebpackで実装するので一緒にパックしちゃおうと思いdependenciesでインストールしてあります。

scriptにはdevdev-buildの二つを用意しています。
本来であればbuildを用意して、本番用のminify化したjsを出力するのが正しいですが、
今回はjQueryだとどういうコードになるのかを遊び半分で書くだけなのでdevだけでいいかなと思った次第です。^^

またwebpack dev serverを使って保存するたびにページをリロードする場合、ファイルを保存しても仮想的にしかbundle.jsを吐き出してくれない(実ファイルは変更されない)ため、ファイルをちゃんと吐き出してもらうためにdev-buildを用意しています。

.babelrc

.babelrc
{
  "presets": ["es2015"]
}

es2015で書いていくので、その指定をここに記します。
babel系のパッケージをdevDependenciesにインストールしているので、この設定ファイルを用意すればes2015でjsを書けるようになります。

webpack.config.develop.babel.js

webpack.config.develop.babel.js
import webpack from 'webpack';

export default {
  // ビルドの起点となるファイルの設定
  entry: ['./src/index.js'],
  // 出力されるファイルの設定
  output: {
    publicPath: '/',
    path: `${__dirname}/public/`, // 出力先のパス
    filename: 'bundle.js', // 出力先のファイル名
  },
  // ローカルサーバの設定
  devServer: {
    contentBase: './public',
    hot: true,
    inline: true,
    historyApiFallback: true,
  },
  resolve: {
    // importする拡張子の指定
    extensions: [
      '.js',
    ],
  },
  // ソースマップの設定
  devtool: 'inline-source-map',
  // loaderの設定
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
    ],
  },
  plugins: [
    // hotモードに必要なプラグイン
    new webpack.HotModuleReplacementPlugin(),
  ],
};

ファイル名にbabelとつけているのでes2015で書くことができます。

実際にjQueryを書く!

環境も整ったので、いい感じにjQueryをes2015で書いていこうと思います^^

下準備

まずはHTMLがないと始まらないので適当に書きます。

public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>jquery-calendar</title>
</head>
<body>
<div id="wrapper">
</div>
<script src="./bundle.js"></script>
</body>
</html>

webpack.config.develop.babel.jsでbundle.jsの吐き出し場所publicにしてあるので<script>を使って相対パスで読み込みます。
jQueryはbundle.jsに混ぜ込んでしまうので指定しません。

一応、<div id="wrapper">を作っています。この中にカレンダーのDOMをJSで生成していこうと思います。

index.jsの作成

webpack.config.develop.babel.jsに、entryの箇所を['./src/index.js']と記述したので、実際にsrc/index.jsを作成します。

src/index.js
import $ from 'jquery';
import { Calendar } from 'calendar';
import moment from 'moment-timezone';

class CreateCalendar {
  constructor() {
    return this;
  }
}

今回は、jQuery、calendar、momentを使用するので、それぞれを最初にimportしています。

その後にカレンダーを生成するためのプログラムを書いていくわけですが、せっかくes2015で書いているのでclassで書いてやろうじゃねーかと思った次第です^^

HTMLの定義

Reactであればrender()の中にHTML風にタグを記述していけばそれをレンダリングしてくれますが、jQueryとなるとそうはいきません。
$()でDOMをちまちま生成していかなければならないわけですが、せっかくclassで書こうとしているので、必要なHTMLの文字列を定義しておこうと思います。
インスタンスを生成する際にわざわざHTMLの文字列を定義する必要もないと感じたので、staticで定義しちゃいます。

src/index.js
// ...省略

class CreateCalendar {

  static get ROOT_HTML() {
    return '<table />';
  }

  static get HEADER_HTML() {
    return `
      <thead>
        <tr>
          <th>日</th>
          <th>月</th>
          <th>火</th>
          <th>水</th>
          <th>木</th>
          <th>金</th>
          <th>土</th>
        </tr>
      </thead>
    `;
  }

  static get BODY_HTML() {
    return '<tbody />';
  }

  static get BODY_ROW_HTML() {
    return '<tr />';
  }

  static get BODY_CELL_HTML() {
    return '<td />';
  }

  static get CAPTION_HTML() {
    return '<caption />';
  }

  static get TITLE_HTML() {
    return '<span />';
  }

  static get PREV_BUTTON_HTML() {
    return '<span><</span>';
  }

  static get NEXT_BUTTON_HTML() {
    return '<span>></span>';
  }

  constructor() {
    return this;
  }
}

これでCreateCalendar.ROOT_HTMLという形でHTMLの文字列を取得できるようになりました。

初期値をセット

コンストラクタが実行された時点で、ある程度初期値を設定しておきたいのでそのプログラムを書きます。

src/index.js
// ...省略

class CreateCalendar {

  // ...省略

  constructor($target = $('body')) {
    this.nowDate = moment().tz('Asia/Tokyo');
    this.$target = $target;
    return this;
  }
}

new CreateCalendar()とするときに、どのDOMに対してカレンダーを出力するのかを定義したいよなと思い、new CreateCalendar($('#wrapper'))とできるように引数を設けました。
エラーとか書くのめんどくさいなとも思い、初期値に$('body')を指定できるようにしています。

また、new CreateCalendar()が実行されたときのDateオブジェクトと、$targetを参照できるようにクラス直下の変数に保存しています。

カレンダーデータをセット

カレンダーデータも立派な初期値ですが、カレンダーデータは月を移動することでデータが変わるので、何度もセットされることが予想されます。
なので、setCalendarDataという関数を用意した上で、それをconstructor()で叩いてあげます。

src/index.js
// ...省略

class CreateCalendar {

  // ...省略

  constructor($target = $('body')) {
    this.nowDate = moment().tz('Asia/Tokyo');
    this.$target = $target;

    // カレンダーデータをセット
    this.setCalendarData();

    return this;
  }

  setCalendarData() {
    this.calendarData = new Calendar().monthDays(this.nowDate.year(), this.nowDate.month())
  }
}

render用の関数を作成

Reactみたいに、実際にDOMをレンダリングする関数を作成します。
ただ、render()の中にダラダラと書くと見づらくなるので、今回は4つの関数を用意します。

  • createCaptionDOM():見出しとのボタン部分のDOMを生成する関数
  • createHeadDOM();曜日が記述された<thead>部分のDOMを生成する関数
  • createBodyDOM():日付が表示される<tbody>部分のDOMを生成する関数
  • render():すべてをwrapした<table>を生成し、$targetに追加する関数
src/index.js
// ...省略

class CreateCalendar {

  // ...省略

  constructor($target = $('body')) {
    // ...省略

    // render実行
    this.render();

    return this;
  }

  setCalendarData() {
    // ...省略
  }

  // captionのDOM生成
  createCaptionDOM() {
    const $caption = $(CreateCalendar.CAPTION_HTML);
    const $prev = $(CreateCalendar.PREV_BUTTON_HTML);
    const $next = $(CreateCalendar.NEXT_BUTTON_HTML);

    $caption
      .append($prev)
      .append($(CreateCalendar.TITLE_HTML).text(`${this.nowDate.year()}${this.nowDate.month() + 1}月`))
      .append($next);

    return $caption;
  }

  // theadのDOM生成
  createHeadDOM() {
    return $(CreateCalendar.HEADER_HTML);
  }

  // tbodyのDOM生成
  createBodyDOM() {
    const $body = $(CreateCalendar.BODY_HTML);

    this.calendarData.forEach(week => {
      const $row = $(CreateCalendar.BODY_ROW_HTML);

      $body.append($row);

      week.forEach(day => {
        const $cell = $(CreateCalendar.BODY_CELL_HTML);
        // `0`の場合はその月の日付ではないので`day`は表示しない
        if (day !== 0) $cell.text(day);
        $row.append($cell);
      });
    });

    return $body;
  }

  // DOM生成
  render() {
    // 中身削除
    this.$target.empty();

    const $calendar = $(CreateCalendar.ROOT_HTML);

    $calendar
      .append(this.createCaptionDOM())
      .append(this.createHeadDOM())
      .append(this.createBodyDOM());

    $calendar.appendTo(this.$target);
  }
}

// インスタンス生成
new CreateCalendar($('#wrapper'));

this.render()が最終的な出力を担うようにプログラムが書けました。
ちゃんとレンダリングされるようにthis.render()constructorで実行するように変更を入れています。

また、クラスは作ったらちゃんとインスタンスを生成してあげないといけないので、最後の行にnew CreateCalendar($('#wrapper'));を追加しています。

おそらく、これでカレンダー自体は表示されるようになったと思います。

ここから、前後の月に移動できるようにしていきましょう。

ボタンをクリックしたら月を移動できるようにする

createCaptionDOMで、$prev$nextを作成しているので、このDOMにclickイベントを付与していきます。

$prevがクリックされた場合は、前の月が表示されてほしいのでthis.calendarDataの中身が前の月のものにならなければなりません。
カレンダーデータを書き換えるということはsetCalendarDate()が実行されないといけません。

setCalendarData() {
  this.calendarData = new Calendar().monthDays(this.nowDate.year(), this.nowDate.month())
}

setCalendarData()を抜き出してみました。
setCalendarData()は、this.nowDateを基にデータを生成しthis.calendarDataに格納する関数なので、クリックされたらthis.nowDateを更新し、その後にsetCalendarDate()が実行されればいい感じになりそうですね!

src/index.js
// ...省略

class CreateCalendar {

  // ...省略

  // clickイベント名
  static get CLICK_EVENT_NAME() {
    return 'click.CreateCalendar';
  }

  constructor($target = $('body')) {
    // ...省略
  }

  setCalendarData() {
    this.calendarData = new Calendar().monthDays(this.nowDate.year(), this.nowDate.month())
    // データの更新はここで完了するのでここでレンダリング
    this.render();
  }

  // 前の月へ移動(クリックイベント時に実行)
  goToPrevMonth() {
    this.nowDate = this.nowDate.add(-1, 'month');
    this.setCalendarData();
  }

  // 次の月へ移動(クリックイベント時に実行)
  goToNextMonth() {
    this.nowDate = this.nowDate.add(1, 'month');
    this.setCalendarData();
  }

  createCaptionDOM() {
    const $caption = $(CreateCalendar.CAPTION_HTML);
    const $prev = $(CreateCalendar.PREV_BUTTON_HTML);
    const $next = $(CreateCalendar.NEXT_BUTTON_HTML);

    // イベントセット
    $prev.on(CreateCalendar.CLICK_EVENT_NAME, () => this.goToPrevMonth());
    $next.on(CreateCalendar.CLICK_EVENT_NAME, () => this.goToNextMonth());

    $caption
      .append($prev)
      .append($(CreateCalendar.TITLE_HTML).text(`${this.nowDate.year()}${this.nowDate.month() + 1}月`))
      .append($next);

    return $caption;
  }

  createHeadDOM() {
    // ...省略
  }

  createBodyDOM() {
    // ...省略
  }

  render() {
    // ...省略
  }
}

new CreateCalendar($('#wrapper'));

まず、クリックされたときに実行されるgoToPrevMonth()goToNextMonth()を用意しています。
momentを利用しているので、「1ヶ月前」と「1ヶ月後」を.add()を使って簡単にnowDateを生成することができます。
momentによって帰ってきた日付のオブジェクトをthis.nowDateに格納して、その直後にsetCalendarData()を実行すれば、勝手にthis.calendarDataも更新されるようになります。

Reactの場合はthis.statethis.setState()によって更新されると、勝手にthis.render()が実行されますが、今はただclassを作っているだけなのでthis.calendarDataを更新した後に自分でthis.render()を叩かなければなりません。
なのでsetCalendarDate()this.render()を追加しています。
(本来であればsetState()を作成すればよいのですが、ちょっとめんどくさいなーと思ってやめておきました…笑)

また、今回はjQueryで実装しているのでイベントの付与には.on()を使うことができます。
なので、ちゃんとjQueryの機能を最大限に使おうと思い、イベントに名前空間を付けるためにstaticでCLICK_EVENT_NAMEというのを用意し、click.CreateCalendarという名前空間付きのクリックイベント名を用意しました。

完成したプログラム

部分ごとに紹介してきたので、全体のコードをもう一度見てみましょう。

src/index.js
import $ from 'jquery';
import { Calendar } from 'calendar';
import moment from 'moment-timezone';

class CreateCalendar {

  static get ROOT_HTML() {
    return '<table />';
  }

  static get HEADER_HTML() {
    return `
      <thead>
        <tr>
          <th>日</th>
          <th>月</th>
          <th>火</th>
          <th>水</th>
          <th>木</th>
          <th>金</th>
          <th>土</th>
        </tr>
      </thead>
    `;
  }

  static get BODY_HTML() {
    return '<tbody />';
  }

  static get BODY_ROW_HTML() {
    return '<tr />';
  }

  static get BODY_CELL_HTML() {
    return '<td />';
  }

  static get CAPTION_HTML() {
    return '<caption />';
  }

  static get TITLE_HTML() {
    return '<span />';
  }

  static get PREV_BUTTON_HTML() {
    return '<span><</span>';
  }

  static get NEXT_BUTTON_HTML() {
    return '<span>></span>';
  }

  static get CLICK_EVENT_NAME() {
    return 'click.CreateCalendar';
  }

  constructor($target = $('body')) {
    this.nowDate = moment().tz('Asia/Tokyo');
    this.$target = $target;

    this.setCalendarData();

    this.render();

    return this;
  }

  setCalendarData() {
    this.calendarData = new Calendar().monthDays(this.nowDate.year(), this.nowDate.month())
    this.render();
  }

  goToPrevMonth() {
    this.nowDate = this.nowDate.add(-1, 'month');
    this.setCalendarData();
  }

  goToNextMonth() {
    this.nowDate = this.nowDate.add(1, 'month');
    this.setCalendarData();
  }

  createCaptionDOM() {
    const $caption = $(CreateCalendar.CAPTION_HTML);
    const $prev = $(CreateCalendar.PREV_BUTTON_HTML);
    const $next = $(CreateCalendar.NEXT_BUTTON_HTML);

    $prev.on(CreateCalendar.CLICK_EVENT_NAME, () => this.goToPrevMonth());
    $next.on(CreateCalendar.CLICK_EVENT_NAME, () => this.goToNextMonth());

    $caption
      .append($prev)
      .append($(CreateCalendar.TITLE_HTML).text(`${this.nowDate.year()}${this.nowDate.month() + 1}月`))
      .append($next);

    return $caption;
  }

  createHeadDOM() {
    return $(CreateCalendar.HEADER_HTML);
  }

  createBodyDOM() {
    const $body = $(CreateCalendar.BODY_HTML);

    this.calendarData.forEach(week => {
      const $row = $(CreateCalendar.BODY_ROW_HTML);

      $body.append($row);

      week.forEach(day => {
        const $cell = $(CreateCalendar.BODY_CELL_HTML);
        if (day !== 0) $cell.text(day);
        $row.append($cell);
      });
    });

    return $body;
  }

  render() {
    this.$target.empty();

    const $calendar = $(CreateCalendar.ROOT_HTML);

    $calendar
      .append(this.createCaptionDOM())
      .append(this.createHeadDOM())
      .append(this.createBodyDOM());

    $calendar.appendTo(this.$target);
  }
}

new CreateCalendar($('#wrapper'));

なっっっげ!!! 読みづらっ!!!

jQueryはDOM構造の構築には本当に向かない

もちろんやる前からわかってはいましたが、やっぱり、DOMの構造が全然わからないのは辛いですね><
バッククォートが出たことによって、JS内にHTML文字列をインデント付きでかけるようにはなりましたが、結局は.append()等で構造を作らなければならないので分かりづらくなってきます。

createBodyDOM() {
  const $body = $(CreateCalendar.BODY_HTML);

  this.calendarData.forEach(week => {
    const $row = $(CreateCalendar.BODY_ROW_HTML);

    $body.append($row);

    week.forEach(day => {
      const $cell = $(CreateCalendar.BODY_CELL_HTML);
      if (day !== 0) $cell.text(day);
      $row.append($cell);
    });
  });

  return $body;
}

createBodyDOM()を抜き出して見ましたが、これは本当に分かりづらいですね。。。
おそらくReactなら以下のようにかけるはずです。(仮で書いているのでミスってたら指摘してください><)

createBodyDOM() {
  return (
    <tbody>
      {this.calendarData.map((week, weekIndex) => (
        <tr key={weekIndex}>{
          week.map((day, dayIndex) => (
            <td key={dayIndex}>{day}</td>
          )) 
        }</tr>
      ))}
    </tbody>
  )
}

HTMLにロジックが入ってきているような感じなので見づらいなーと思う方もいるかも知れないですが、やっぱりなにより、一気にreturnできちゃうあたりがいいなーと個人的には思います。
jQueryだとどうしてもforEach(または$.each())を使わないといけないので、受け口となる変数を作ってあげなきゃいけなかったりと、考えることが地味に多いのが辛いところです。

やっぱり、Reactが最高で、jQueryがダメなのか?

Reactはデータ構造に合わせて、勝手にDOMを生成したり削除したりするのが売りのフレームワークです。
それに対してjQueryは、存在するDOMに対して様々な挙動を付与しやすいのが売りのフレームワークです。

変数に格納されているObjectを変えるだけで、勝手にviewが変わるなんてjQueryで書いている場合は想像もつかない世界です。
しかし、Reactを書いているときに「親のDOMを取得して、親の高さの半分のサイズにしたい」なんてことをやりたいときには、refでDOMを返してクラスの変数にぶち込んで、compoenntDidMountでクラスの変数から引っ張り出して.closest()で親を取得して.clientHeight()で高さをようやく取得して…みたいなことをするわけですが、jQueryだったら$('.self').closest('.parent').height()で終わっちゃうわけです。

また、例えば、ちょっとしたランディングページを作りましょうみたいなときに、わざわざnpmでいろいろパッケージをインストールして、webpackのconfig書いて、、、みたいなことをするのか?と言われると、答えはNoだと思います。
Sass使いたいからちょっとgulpでも使おうかな?という感じでnpmは使うかもですが、わざわざSPAにしてやろうみたいなことは滅多にやらないと思います。

<script src="./js/jQuery.min.js"></script>と書いて、どんどん$使って、ガンガンDOMアクセスして、さっとリリースしちゃえばいいわけです。

えーーーー今更jQueryなんて使うのーーーーみたいな感じもありますが、やっぱりJavaScriptをそのまま書くより、jQueryで書いたほうがそれだけで実装力があがりますし、やっぱりjQueryはすごいなぁと思いました!
初心者なんて絶対jQueryから入った方がいいと思いました!
animate()あるし、やっぱりビュンビュン動くもの作りたいし、できたらテンションあがりますもんね!

いろいろ作ってから考えてみて、やっぱりReactも最高だし、jQueryも最高だなと思いました!

そして、もしjQueryでカレンダーを作るなら、jQuery UIか、他の使いやすそうなjQueryプラグインを使おうと思いました!!!

以上です!

33
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
24