あつまるエンジニアブログ

集客プラットフォーム事業を手がける株式会社あつまるのエンジニアブログです

JavaScript のデータを CSV で保存する

どうも、株式会社あつまるでコンサルティングに必要なデータのとりまとめをしている三井です。


意外と需要のある JavaScript のデータを CSV として保存するスニペットを書き留めます。

var data = [
  ['name'  , 'age', 'gender'],
  ['Andrew', 26   , 'male'  ],
  ['Lisa'  , 21   , 'female'],
  ['Fred'  , 41   , 'male'  ],
]

このような多重配列を元にして、

f:id:todays_mitsui:20170423135440p:plain

このような CSV を保存します。

ちなみに、

var data = [
  {name: 'Andrew', age:26   , gender: 'male'  },
  {name: 'Lisa'  , age:21   , gender: 'female'},
  {name: 'Fred'  , age:41   , gender: 'male'  },
]

このような オブジェクトの配列 にも対応させました。


んで、
最初に書いておきますが、 Mac版 Excel には対応していない CSV を扱っています 。ご容赦ください。


コード

さっそくドン、

class CSV {
  constructor(data, keys = false) {
    this.ARRAY  = Symbol('ARRAY')
    this.OBJECT = Symbol('OBJECT')

    this.data = data

    if (CSV.isArray(data)) {
      if (0 == data.length) {
        this.dataType = this.ARRAY
      } else if (CSV.isObject(data[0])) {
        this.dataType = this.OBJECT
      } else if (CSV.isArray(data[0])) {
        this.dataType = this.ARRAY
      } else {
        throw Error('Error: 未対応のデータ型です')
      }
    } else {
      throw Error('Error: 未対応のデータ型です')
    }

    this.keys = keys
  }

  toString() {
    if (this.dataType === this.ARRAY) {
      return this.data.map((record) => (
        record.map((field) => (
          CSV.prepare(field)
        )).join(',')
      )).join('\n')
    } else if (this.dataType === this.OBJECT) {
      const keys = this.keys || Array.from(this.extractKeys(this.data))

      const arrayData = this.data.map((record) => (
        keys.map((key) => record[key])
      ))

      console.log([].concat([keys], arrayData))

      return [].concat([keys], arrayData).map((record) => (
        record.map((field) => (
          CSV.prepare(field)
        )).join(',')
      )).join('\n')
    }
  }

  save(filename = 'data.csv') {
    if (!filename.match(/\.csv$/i)) { filename = filename + '.csv' }

    console.info('filename:', filename)
    console.table(this.data)

    const csvStr = this.toString()

    const bom     = new Uint8Array([0xEF, 0xBB, 0xBF]);
    const blob    = new Blob([bom, csvStr], {'type': 'text/csv'});
    const url     = window.URL || window.webkitURL;
    const blobURL = url.createObjectURL(blob);

    let a      = document.createElement('a');
    a.download = decodeURI(filename);
    a.href     = blobURL;
    a.type     = 'text/csv';

    a.click();
  }

  extractKeys(data) {
    return new Set([].concat(...this.data.map((record) => Object.keys(record))))
  }

  static prepare(field) {
    return '"' + (''+field).replace(/"/g, '""') + '"'
  }

  static isObject(obj) {
    return '[object Object]' === Object.prototype.toString.call(obj)
  }

  static isArray(obj) {
    return '[object Array]' === Object.prototype.toString.call(obj)
  }
}

CSV というクラスを定義しています。
使い方はこのように、

(new CSV(data)).save('foobar.csv')

調子こいて スプレッド演算子Set などを多用しているので、比較的新しい Chrome とかでないと動かないかも知れませんね。


CSV の仕様

CSV はとてもシンプルな仕様です。
フィールド(Excel でいうところのセル)をカンマ , で区切ったものがレコードになります。
レコード同士は改行 \n で区切ります。

フィールドに ,\n値として 含まれる場合は、それがフィールドやレコードの区切り文字ではないことを示すためにフィールド全体をダブルクォート " で囲む必要があります。
さらに " で囲ったフィールドの中に " が値として含まれる場合は " 自体をエスケープしてあげる必要があります。エスケープは " を二つ重ねて "" に置換することで行います。


文字コード

CSV ファイルを保存する際の文字コードについては特に規定されていませんが、 日本語を含む CSV を Excel で開きたい 場合には少々のテクニックを要します。

採用する文字コードの選択肢はいくつかありますが、

  1. Shift_JIS
  2. BOM 付き UTF-8
  3. BOM 付き UTF-16LE

今回は 2. BOM 付き UTF-8 を採用しています。
ただし、そうやって保存した CSV は Mac 版の Excel で開くと文字化けします


日本語を含む CSV を Excel で正しく開かせるためのテクニックについては、「CSV Unicode Excel」などのフレーズで検索していただけると闇が垣間見られると思います。


元データを用意する

もうこの記事で伝えたいことの本題は終わっているんですが、データを用意する方法にも軽く触れておきます。

例えば、ここに食べログの東京都内のラーメン屋の検索結果画面がありまして、

f:id:todays_mitsui:20170423135518p:plain

インスペクタとにらめっこしまして、

f:id:todays_mitsui:20170423135538p:plain

jQuery などを駆使してこのようなコードを書きますと、

const data = $('.list-rst').map(function() {
  const $this = $(this)

  const name         = $this.find('.list-rst__rst-name a').text()
  const score        = parseFloat($this.find('.list-rst__rating-val').text())
  const reviewCount  = parseInt($this.find('.list-rst__rvw-count-num').text(), 10)
  const dinnerBudget = $this.find('.cpy-dinner-budget-val').text()
  const lunchBudget  = $this.find('.cpy-lunch-budget-val').text()
  const holiday      = $this.find('.list-rst__holiday-datatxt').text()
  const comment      = $this.find('.list-rst__pr-title').text().trim()
  const searchWord   = $this.find('.list-rst__search-word .list-rst__search-word-item').map(function() {
    return $(this).text().trim()
  }).get()

  return {
    name,
    score,
    reviewCount,
    dinnerBudget,
    lunchBudget,
    holiday,
    comment,
    searchWord,
  }
}).get()

すると、このようなデータが取れますので、

f:id:todays_mitsui:20170423135554p:plain

先ほどの CSV クラスとしてインスタンス化して保存すると、

(new CSV(data)).save('ramen.csv')

f:id:todays_mitsui:20170423135606p:plain

このようなダイアログが開いてデータを保存できるわけですね。

Excel で開くと、

f:id:todays_mitsui:20170423135620p:plain

はい、このように。


まとめ

数ヶ月後に『あー、このサイトの情報テキトーに CSV 保存してぇ』という場面に出くわすであろう自分に捧げます。


私からは以上です。

Python3 で言語処理100本ノック 2015 - 第1章

どうも、株式会社あつまるで Python 製の社内ツールなどを作っている三井です。


乾・岡崎研究室が公開している 言語処理100本ノック 2015 に取り組んで行きます。
使用する言語は Python3 です。

第1章から第10章で構成されているのでまずは第1章から。
ではスタート。


00. 文字列の逆順

文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

print("stressed"[::-1])
# => desserts

Python のスライスを使うだけですね。


01. 「パタトクカシーー」

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

print("パタトクカシーー"[::2])
# => パトカー

これもスライスを使うだけ。
Python のスライスは高機能ですね。

ちなみに "パタトクカシーー" から "タクシー" を取り出したいときは、

print("パタトクカシーー"[1::2])
# => タクシー


02. 「パトカー」+「タクシー」=「パタトクカシーー」

「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

print("".join(s1+s2 for s1, s2 in zip("パトカー", "タクシー")))
# => パタトクカシーー

zip して join する感じで。
この程度ならまだワンライナーで書いてしまいます。


03. 円周率

"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.

import re

sentence = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."

print([len(word) for word in re.split(r"[\s,.]+", sentence) if "" != word])
# => [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]

文章を単語毎に分割する部分を re.split(r"[\s,.]+", sentence) としています。
単語の区切りに 空白(\s), ,, . を選んでいますが、これは扱う言語の種類によってカスタマイズする必要がありますね。


04. 元素記号

"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

import re

specific_indexes = (1, 5, 6, 7, 8, 9, 15, 16, 19)

sentence = "Hi He Lied Because Boron Could Not Oxidize Fluorine." \
           " New Nations Might Also Sign Peace Security Clause. Arthur King Can."

print({word[:1] if index in specific_indexes else word[:2]: index for index, word in enumerate(re.split(r"[\s,.]+", sentence), start=1) if "" != word})
# =>
# {
#   'Si': 14, 'He': 2, 'Ar': 18, 'O': 8, 'K': 19, 'C': 6,
#   'N': 7, 'Li': 3, 'B': 5, 'Mi': 12, 'Cl': 17, 'S': 16,
#   'Be': 4, 'Al': 13, 'F': 9, 'Ca': 20, 'P': 15, 'H': 1,
#   'Ne': 10, 'Na': 11
# }

うわ、このコードはひどい。
本来は適切に関数を定義したり、適切に名付けた変数を使えばもっと読みやすくなるのですが、なんか出題自体が恣意的だったのでモジュール化する気力が湧きませんでした。

全体を 辞書内包表記 で処理しています。単語毎の分割は re.split(r"[\s,.]+", sentence) で。
単語の出現順を扱うために enumerate() を使って index という変数に受けています。
indexspecific_indexes = (1, 5, 6, 7, 8, 9, 15, 16, 19) に含まれていれば、対応する単語の先頭1文字だけを取り出し(word[:1])、含まれていなければ先頭2文字を取り出して(word[:2])辞書の Key にします。

辞書内包表記, 三項演算子, スライス, enumerate(), re.split() を1行に詰め込む。行儀の悪いコードのお手本みたいですね。


05. n-gram

与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

n-gram という概念自体、日本語だけでは説明しづらいですよね。
n-gram は「文章中に現れる N 個連続した連なり」でしょうか。

"Hello" の 3(tri)-gram は {"Hel", "ell", "llo"} という集合になります。

import re


def n_gram(seq, n=2):
    seq_set = (seq[i:] for i in range(n))

    return tuple("".join(chars) for chars in zip(*seq_set))


sentence = "I am an NLPer"

char_bi_gram = n_gram(sentence)
print("char_bi_gram:", char_bi_gram)
# => char_bi_gram:
# ('I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er')

words = re.split(r"[\s,.]", sentence)
word_bi_gram = n_gram(words)
print("word_bi_gram:", word_bi_gram)
# => word_bi_gram:
# ('Iam', 'aman', 'anNLPer')

n_gram() という関数を定義しました。

(seq[i:] for i in range(n)) は例えば "Hello" という文字列から ("Hello", "ello", "llo") と開始を1文字ずつずらした N 個組みのシーケンスを生成します。
それを zip すると (("H", "e", "l"), ("e", "l", "l"), ("l", "l", "o")) という組みが取れるので、あとは適切に join してあげる感じで。

これ、Haskell でリスト中の連続した N 個の要素を組みにして走査したいときのやり方をそのまま持ってきました。いやぁ、Haskell やってて良かった。


06. 集合

"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

def n_gram(seq, n=2):
    seq_set = (seq[i:] for i in range(n))

    return tuple("".join(chars) for chars in zip(*seq_set))


word_x = "paraparaparadise"
word_y = "paragraph"

x = set(n_gram(word_x))
y = set(n_gram(word_y))

union = x | y
print("union:", union)
# => union: {'pa', 'se', 'ad', 'is', 'ar', 'ap', 'gr', 'ag', 'ph', 'ra', 'di'}

intersection = x & y
print("intersection:", intersection)
# => intersection: {'pa', 'ra', 'ar', 'ap'}

difference_x_y = x - y
print("difference (x-y):", difference_x_y)
# => difference (x-y): {'di', 'is', 'se', 'ad'}

difference_y_x = y - x
print("difference (y-x):", difference_y_x)
# => difference (y-x): {'ph', 'ag', 'gr'}

print("'se' in X ?:", "se" in x)
# => 'se' in X ?: True

print("'se' in Y ?:", "se" in y)
# => 'se' in Y ?: False

さきほどの n_gram() を流用します。

Python には Set という集合を扱うためのデータ型があって、一通りの集合演算がメソッドと演算子で用意されているので便利です。


07. テンプレートによる文生成

引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.

def template(x, y, z):
    return u"{x}時の{y}は{z}".format(x=x, y=y, z=z)

print(template(x=12, y="気温", z=22.4))
# => 12時の気温は22.4

文字列の .format() メソッドを使っとけというだけの課題ですね。


08. 暗号文

与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.

  • 英小文字ならば(219 - 文字コード)の文字に置換
  • その他の文字はそのまま出力

この関数を用い,英語のメッセージを暗号化・復号化せよ.

import re


def cipher(plaintext):
    return re.sub(r"[a-z]", lambda m: chr(219 - ord(m.group(0))), plaintext)


plaintext = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor" \
            " incididunt ut labore et dolore magna aliqua."
print("Plaintext:", plaintext)
# => Plaintext:
# Lorem ipsum dolor sit amet, consectetur adipiscing elit,
# sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

encrypt = cipher(plaintext)
print("Encryption:", encrypt)
# => Encryption:
# Llivn rkhfn wloli hrg znvg, xlmhvxgvgfi zwrkrhxrmt vorg,
# hvw wl vrfhnlw gvnkli rmxrwrwfmg fg ozyliv vg wloliv nztmz zorjfz.

decrypt = cipher(encrypt)
print("Decryption:", decrypt)
# => Decryption:
# Lorem ipsum dolor sit amet, consectetur adipiscing elit,
# sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

最初、『まず文章を文字毎に分解して...』とか考えていたんですが、 re.sub を使って条件に合う文字だけ置換してあげれば一撃でした。


09. Typoglycemia

スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ. ただし,長さが4以下の単語は並び替えないこととする. 適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え,その実行結果を確認せよ.

Typoglycemia って何? という方はコチラを参照、

ただの都市伝説かと思いきや奥深いんですよ。

import random


def stir(word):
    if 5 > len(word):
        return word

    head = word[0]
    last = word[-1]
    body = word[1:-1]

    return head + "".join(random.sample(body ,len(body))) + last

def genTypoglycemia(sentence):
    return " ".join(map(stir, sentence.split(" ")))


plaintext = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
print("Plaintext:", plaintext)
# => Plaintext:
# I couldn't believe that I could actually understand what I was reading :
# the phenomenal power of the human mind .

typoglycemia = genTypoglycemia(plaintext)
print("Typoglycemia:", typoglycemia)
# => Typoglycemia:
# I clon'udt bleivee that I cloud aatlulcy unredantsd what I was raiedng :
# the penonehmal pewor of the hmaun mind .

意外と 文字の順序をランダムに並び替える てところで突っかかったんでコチラを参考にしました!


所感

第1章はここまで。
正直、第1章は仕事終わりにビール飲みながら暇つぶしで解いていたんですが、後半はそういうわけにもいかなくなるでしょうね。

第2章以降は解き終わり次第随時上げていきます。


私からは以上です。


その他の章の回答はこちらから

【jQueryの基本のき】パート4 - jQueryのいろんなバージョン

どうも、株式会社あつまるでフロントエンドエンジニアをやっている三井です。


これまでjQueryの使い方についてねちっこく解説してきましたが...。
今回は「使い方」ではなく「選び方」です。

ビギナーズからよく飛び出す質問「どのバージョンのjQueryを使えばいいの?」について。


jQueryは最新版だけでも全部で8つ

2016年8月末時点でjQuery本体の最新バージョンは バージョン3.1.0 です。
しかし バージョン1系列バージョン2系列 についてもサポートが打ち切られているわけではありません。
さらにファイル名に「.slim」や「.min」という文字がつくものも配布されています。

現在 公式サイトで配布されているjQuery を並べてみるとこんな感じ。

うむ、困りましたね。
少しずつ噛みくだきながら解説していきます。


.min の有無はファイルサイズの違い(機能は同じ)

「.min」が付くものと付かないものは、コードが圧縮されているか圧縮されていないかという違い があります。

JavaScript はコードの途中とちゅうに改行やスペースを入れても動作自体は変わりません。
これは HTML や CSS と同じ性質ですよね。

そのために「.min」無しバージョンでは適宜改行を入れ込んで読みやすく整形されています。
逆に「.min」付きバージョンではできる限り改行もスペースも入れずに書かれているため、ファイルサイズが小さくなっています。


というわけで「jquery-3.1.0.js」と「jquery-3.1.0.min.js」などは基本的に同じ機能を持っています。
どちらを使っても同じです。

ファイルの読みやすさを重視するならば「.min」無しを、サイズの小ささを重視するならば「.min」付きを選べばいいでしょう。
ただ、jQuery本体のソースコードを読みたくなることってほとんど無いので、基本的には「.min」付きで充分 だと思います。

参考に、

  • jquery-3.1.0.js - 263,767 バイト
  • jquery-3.1.0.min.js - 86,351 バイト

1/3 ほどのファイルサイズになってますね。軽い!


バージョン1系列とバージョン2系列の違いは対応しているブラウザの違い

バージョン1系列とバージョン2系列は 対応しているブラウザの違い です。
バージョン2系列は比較的新しいブラウザでしか動きませんが、バージョン1系列を使えば古いブラウザでも動きます。

公式サイト によれば、バージョン2系列の対応ブラウザは、

  • Google Chrome の最新版とひとつ前のバージョン
  • Microsoft Edge の最新版とひとつ前のバージョン
  • Firefox の最新版とひとつ前のバージョン
  • Internet Explorer 9~11
  • Safari の最新版とひとつ前のバージョン
  • Opera の最新バージョン
  • (モバイル)Android 4.0以上の標準ブラウザ
  • (モバイル)iOS 7以上のSafari

だそうです。

バージョン1系列を使えば、ここにさらに古いブラウザが加わります。

  • Internet Explorer 6~8
  • Opera 12.1x (Prestoエンジンを積んだバージョン)
  • Safari 5.1以上

特に重要なのは Internet Explorer に対応するかどうかでしょう。
IE8 などでも問題なく表示できるサイトにしようと思えば、jQueryのバージョン1.12を使うべきです。


ただし、対応しているブラウザが多いことはメリットだけではありません。
バージョン1系列は古いブラウザに対応させるためにファイルサイズが大きく、処理速度も遅いのです。


ちなみに、対応ブラウザって時代によって変わります

さて、ここで要注意なんですが、
さきほど紹介した対応ブラウザはあくまでも2016年8月末時点のものです。

時が流れて「こんな古いブラウザ誰も使ってないよね」というようなブラウザが出てくれば、そのブラウザのサポートは打ち切られるはずです。
公式サイト では常に対応ブラウザについての情報が公開されています。
定期的に情報を確認して、最新のブラウザ事情にキャッチアップしてみてください。


バージョン2系列とバージョン3系列の違いは設計の違い

バージョン2系列とバージョン3系列では対応しているブラウザは同じです。
が、バージョン3系列は2016年現在のブラウザ事情や JavaScript 事情に合わせて 設計のレベルから見直されて作り直されています

そのために機能の面でも大幅な違いがあり、jQuery バージョン2.2.4 を想定して作られたプラグインとjQuery バージョン3.1.0を組み合わせて使うと正常に動作しないかもしれません。


バージョン2系列からバージョン3系列に上がるあたって、もっとも大きく変わったのは .hide().show() が廃止されたことです。

.hide() はページ上の要素を見えなくする機能、反対に .show() は見えなくなっていた要素を表示する機能です。
これらがバージョン3系列では廃止されています。

jQueryの開発チーム曰く、要素を隠したり再表示したりしたいとき今後は、

.hide {
  display: none;
}

というような CSS を用意したうえで、隠したい要素に class="hide" を付与するようにして対応してほしいとのことです。

.hide().show() はこれまでにも多用されてきた機能だけに、その影響範囲は大きそう です。
バージョン3系列を採用する前に、自分が使いたいjQueryプラグインがバージョン3系列に対応しているかどうか調べる必要があります。

その代わり、バージョン3系列ではこれまで以上の性能向上が謳われていますよ。


.slim 付きは必要最低限の機能だけが含まれている

バージョン3系列にはさらにファイル名に「.slim」が付くものと付かないものがあります。
「.slim」付きバージョンは、「.slim」無しバージョンから Ajax やアニメーション関連機能など 一部の機能を削除することで究極の軽量化を目指したバージョン です。

「機能を削除する」なんてそんなことして大丈夫なんでしょうか?
大丈夫です。jQueryは本当に多機能で、中には「この機能最近あんまり使う機会無いよなぁ」というようなものもあるのです。

「.slim」付きバージョンはそういった機能を思い切って削除しています。


ちょっと余談ですが、Ajax やアニメーション関連機能がなぜ最近あんまり使う機会が無いのかお話ししましょう。


当たり前に使われるようになったがために標準機能に組み込まれた Ajax

Ajax とは、ユーザーがページを見ている最中にブラウザとサーバーがやり取りするための仕組みです。

実は Ajax 自体は2005年よりも前からブラウザに標準で存在した伝統的な機能でした。
しかし使い方が少々煩雑で初心者にはなかなか扱いづらいものだったのです。

そんな扱いづらい Ajax を断然扱いやすくする機能を提供したのもjQueryの功績でした。


それから時は流れ...
現在では Ajax はWebサイトを作るうえで欠かせない機能になっています。多くのサイトで今日も Ajax は多用されています。

そして需要の増加に伴って最新のブラウザでは、扱いが煩雑な Ajax の代わりに、より高機能でより扱いやすい Fetch API という機能が標準搭載されるようになってきています。

実際「もう Ajax のためにjQuery持ち出さなくていいよね、Fetch API使おうぜ」という考え方が現在の主流になりつつあります。


アニメーションの舞台は JS から CSS へ

もう一つアニメーション関連機能についてです。

こちらはご存知の方も多いでしょう。
CSS3 でアニメーション関連のプロパティ transition, animate などが追加されたおかげで、ページ上で要素をアニメーションさせることはとても手軽になりました。

CSSアニメーションはブラウザ自身が動作を制御してくれるため負担が少なく、レスポンシブデザインにも対応しやすいという利点があります。
「いまどきjQueryでアニメーションなんて流行らねぇんだよ、時代はCSSアニメーションだ!」そんな声が聞こえてきそうです。



このような時代背景から、わざわざjQueryでやる必要もなさそうな機能が「.slim」付きバージョンでは削除されています。
最新のWeb制作事情にしっかりと追いつけている人であれば、「.slim」付きバージョンも魅力的に見えるでしょう。

参考に再びサイズ比べを、

  • jquery-3.1.0.js - 263,767 バイト
  • jquery-3.1.0.min.js - 86,351 バイト
  • jquery-3.1.0.slim.min.js - 68,952 バイト

うーんコンパクト!


まとめ

ざっくりまとめます。

  • 基本的に「.min」付きのほうがファイルサイズ小さくて素敵
  • 古いブラウザにも対応したいならバージョン1系列を使わざるを得ない
  • プラグインなどがバージョン3系列に対応してそうなら、バージョン3系列を使ってみよう
  • 慎重になるなら様子見でバージョン2系列かなぁ...
  • 最新のトレンドに乗っかって究極の最小化を目指すなら、バージョン3系列の「.slim」付きで

こんな感じになりますかね。


私からは以上です。


jQueryの基本の"き" シリーズ

tech.atsu-maru.co.jp

tech.atsu-maru.co.jp

tech.atsu-maru.co.jp

【jQueryの基本のき】パート3 - 起動スクリプトを囲っているアレをひもとく

どうも、株式会社あつまるでフロントエンドエンジニアをやっている三井です。


さて、前回までjQueryプラグインの基本的な使い方をおさらいして、さらに起動スクリプトについて詳しく解説してみました。

今回は、これまで意図的に触れずにスルーしてきた部分をピックアップします。
起動スクリプトを囲っているよく見るアレ についてです。

さっそく「囲っているよく見るアレ」に登場していただきましょう、はい、

<script>
// ↓よく見る書き方。これが今回の主役です
jQuery(function($) {

  /* ここに起動スクリプトの本体が書かれることが多い */

})
</script>

この書き方、ものすごく重要 なんですがjQueryに馴染まない人はいまいち意味が掴みづらいようです。
「サンプルをそのまま試しているのに何故か動かない」と言うときは、この 囲っているよく見るアレ を使いこなせていない事が多いようです。

さて、


囲っているアレ ファミリー

この 起動スクリプトを囲っているアレ にはいくつかのバリエーションがあります。
微妙に書き方が違うバリエーションがいくつもあるのも初心者が混乱する要因の1つのようです。

起動スクリプトを囲っているアレ の意味を解説する前に、起動スクリプトを囲っているアレ ファミリーを全て並べてみましょう。

<script>
// ========= パターン① ========

// その1
$(document).ready(function() {
  /* ... 起動スクリプト ... */
});

// その2
jQuery(document).ready(function() {
  /* ... 起動スクリプト ... */
});

// その3
$(function() {
  /* ... 起動スクリプト ... */
});

// その4
jQuery(function($) {
  /* ... 起動スクリプト ... */
});

// ========= パターン② ========

// その5
$(window).load(function() {
  /* ... 起動スクリプト ... */
});

// その6
jQuery(window).load(function() {
  /* ... 起動スクリプト ... */
});
</script>

目が回ってきましたか?
大丈夫、なんと1~4は全部同じ意味です。5と6も同じ意味。

なので実際は2種類の書き方だけ覚えればいいんですね。
ここでは仮に、「囲ってるアレ パターン①」「囲ってるアレ パターン②」と呼びましょう。

まずは「書き方のバリエーションはいろいろあるけれど効果は同じなんだ」と理解する事が大切です。


囲っているアレ の効果

お待ちかね、起動スクリプトを囲っているアレ の効果を解説しましょう。

起動スクリプトを囲っているアレ には 起動スクリプトが実行されるのを遅らせる 効果があります。
「ちょっと待って!まだ起動しないで。 (数秒後...)はい、もういいよ。起動スクリプト実行!」とそんな感じに。

「囲ってるアレ パターン①」と「囲ってるアレ パターン②」の違いもそこにあります。

<script>
// ========= パターン① ========

jQuery(function($) {
  /*
   * ここに書かれた起動スクリプトは
   * HTML全体が読み込み終わるまで実行されない。
   **/
});

// ========= パターン② ========

jQuery(window).load(function() {
  /*
   * ここに書かれた起動スクリプトは
   * Windowの読み込みが完了するまで実行されない。
   * HTML全体, 全てのCSS, 全ての画像 の読み込みが終わるまでじっと待つ。
   **/
});
</script>


パターン① と パターン② の違いをお分かりいただけるでしょうか?
それぞれ、HTML の読み込みが終わるまでの間Window の読み込みが終わるまでの間 起動スクリプトが実行されるのを遅らせます。

『なぜ遅らせる?そんな必要ある?』
もっともな疑問ですね。


なぜ HTML の読み込みを待つのか

なぜ 囲ってるアレ を持ち出してまで HTML の読み込みを待たなければいけないんでしょうか。
それは JavaScript は読み込まれた瞬間に実行される からなのですが、ちょっとイメージしづらいと思うので、こんな感じの HTML を書いて読み込ませる例を見てみましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>起動スクリプト サンプルページ</title>

<script src="js/jquery.js"></script>
<script>
$('.target').css({
  color: 'red',
});
</script>

</head>
<body>

<main>
  <h1>起動スクリプト サンプルページ</h1>
  <p class="target">新しい朝が来た、希望の朝だ</p>
</main>

</body>
</html>

jQueryの .css() という機能を使ってターゲットの CSS を書き替えています。
さて、この HTML をブラウザで読み込ませて JavaScript を実行させてみると何が起こるでしょうか?

実は、何も起こりません。


不思議ですね。
実は <head> タグの中に JavaScript を書いているがために、<body> タグ内のターゲット要素を見つけられないのです。

では、HTML が読み込まれる様子をスーパースローカメラでご覧いただきましょう。

f:id:todays_mitsui:20160903221102p:plain

f:id:todays_mitsui:20160903221117p:plain

f:id:todays_mitsui:20160903221141p:plain

f:id:todays_mitsui:20160903221150p:plain

いかがでしょうか?
このスクリプトが期待通りに動かない理由が分かりましたね。


きょう我々はとても重要なことを学びました。
スクリプトが実行されるとき、ターゲットが既にロードされているとは限らないのです。

「JavaScript は読み込まれた瞬間に実行される」とはそう言う意味です。
ブラウザはとてもせっかちなので、ターゲットのロードが完了いなくてもお構いなしです。スクリプトを見つけた瞬間に実行してしまいます。


そこで 起動スクリプトを囲っているアレ ですよ。


HTML の読み込みを待つ意義

反省会が終わったところで先ほどのコードを改良しましょう。

ターゲットのロードが終わってからjQueryのスクリプトが実行されるように、HTML の読み込み完了までスクリプトの実行を待ってもらえばいいのです。
こんなふうに、

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>起動スクリプト サンプルページ</title>

<script src="js/jquery.js"></script>
<script>
jQuery(function($) {

  // これで HTML の全体が読み込まれるまで
  // このスクリプトは実行されない。
  $('.target').css({
    color: 'red',
  });

});
</script>

</head>
<body>

<main>
  <h1>起動スクリプト サンプルページ</h1>
  <p class="target">新しい朝が来た、希望の朝だ</p>
</main>

</body>
</html>

再び HTML が読み込まれる様子を見てみましょう。


f:id:todays_mitsui:20160903221226p:plain

f:id:todays_mitsui:20160903221233p:plain

先ほどの反省を活かして HTML が全て読み込まれたあとでスクリプトを実行しています。
対象である <p class="target"> タグも今回はちゃんと見つかります。

いい感じですね。


「HTML の読み込み」「Window の読み込み」どう違う?

スクリプトの実行をあえて待つことの意義については分かってもらえたと思います。
では 囲っているアレ のパターン①とパターン②の違いについては大丈夫でしょうか?

  • パターン① - HTML が読み込まれるまで待つ
  • パターン② - Window が読み込まれるまで待つ

という違いでしたね。

「HTML の読み込み完了」と「Window の読み込み完了」はどう違うのでしょうか?


Window の読み込み完了は HTML の読み込み完了よりも時間がかかります

「Window の読み込み完了」は CSS や 画像 など 全てのファイルの読み込みが完了した瞬間を意味しているからです。

例えば、HTML を読み込んでいる最中に <link rel="stylesheet" href="style.css"> というような記述があったら、ブラウザはこの「style.css」を追加で読み込みます。
同じように <img src="cat.jpg" alt="My Kitten"> という記述を見つけたら、「cat.jpg」というファイルを追加でサーバーからもらってきます。

f:id:todays_mitsui:20160903221242p:plain

HTML を最終行まで読み終わっても、その中で見つけた CSS や 画像 のダウンロードが完了していなければ Window の読み込み完了とは見なされません。
全ての CSS と全ての画像が読み込まれて、ブラウザ上でページが完全に出来上がった瞬間が「Window の読み込み完了」です。


先ほどは HTML の読み込み完了まで待てば対象の要素が読み込まれているであろうと考えました。

では仮に 対象の画像 を操作するようなスクリプトを実行したいときは?
HTML 読み込み完了時点で 対象の画像 がロードされているとは限りませんよね。その場合は Window の読み込みが完了するまで待ってスクリプトを実行するべきかもしれません。


まとめ

さて 起動スクリプトを囲っているアレ について長々と解説してきました。
今回のダイジェストです。

  • 囲っているアレ は HTML や Window の読み込み完了を待つ
  • スクリプト実行時にターゲットの読み込みが終わっているとは限らない
  • Window の読み込みは HTML の読み込みよりも時間がかかる

これさえ分かればもう怖くないですね。

<script>
// ========= パターン① ========

// その1
$(document).ready(function() {
  /*
   * ここに書かれたスクリプトは
   * HTML の読み込み完了を待って実行される
   * */
});

// その2
jQuery(document).ready(function() {
  /*
   * ここに書かれたスクリプトは
   * HTML の読み込み完了を待って実行される
   * */
});

// その3
$(function() {
  /*
   * ここに書かれたスクリプトは
   * HTML の読み込み完了を待って実行される
   * */
});

// その4
jQuery(function($) {
  /*
   * ここに書かれたスクリプトは
   * HTML の読み込み完了を待って実行される
   * */
});

// ========= パターン② ========

// その5
$(window).load(function() {
  /*
   * ここに書かれたスクリプトは
   * Window の読み込み完了を待って実行される
   * */
});

// その6
jQuery(window).load(function() {
  /*
   * ここに書かれたスクリプトは
   * Window の読み込み完了を待って実行される
   * */
});
</script>

たったのこれだけ!

どれを使えばいいか迷います?
ひとまずは、その4 と その6 を使い分ければ大丈夫ですよ。


私からは以上です。



前回と前々回のリンクを貼っておきます。

tech.atsu-maru.co.jp

tech.atsu-maru.co.jp