ハンズオン：今時のフロントエンド開発の最初の一歩

# はじめに
この記事は、今時のフロントエンド開発の最初の一歩を踏み出すために理解すべきことを理解する、そして、実際にハンズオン形式で最初の一歩を踏み出すところまでを行いながら理解を深める、という目的のハンズオン資料です。

[社内勉強会](https://www.wantedly.com/companies/seedscompany-persol/post_articles/221846)のために作成した物を一般向けに改善したものとなります。

---
## この記事のターゲット

- 今時のフロントエンド開発が昔とどう違うのか知りたい人
- 今時のフロントエンド開発の前提を学びたい人
- 今時のフロントエンド開発を実際にハンズオンで学びたい人

---
# ES6+とは
ECMAScript6以降のこと
IEでは動かない新しい構文を含む
IEでは動かない新しい機能を含む

---
# ES6+の例
```javascript
// const, デフォルト引数, アロー関数
const maskKey = (key, padString = '*') => {
  // const
  const sliced = key.slice(-4);
  // String.prototype.padStart
  return sliced.padStart(key.length, padString);
};

console.log(maskKey('abcdefghij')); // "******ghij"
console.log(maskKey('abcdefghij', 'x')); // "xxxxxxghij"
```

---
## いずれもES5環境(IE11)では動かないES6+の構文・機能
[const - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/const)
[デフォルト引数 - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Default_parameters)
[アロー関数 - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions)
[String.prototype.padStart() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/padStart)

---
# 前時代のフロントエンド
---
## HTML+CSS+jQuery
- さらに +jQueryMobile+jQueryUI+bootstrap... だったり
- **書いたコードがそのままブラウザで動くコード**
  - ターゲットブラウザを意識して書く必要がある
  - ファイル数、モジュール数の分ファイルダウンロードさせる必要がある

---
## ターゲットブラウザを意識して書く必要がある
- IE11がターゲットに入っているからアロー関数は使わない
- IE8がターゲットに入っているからArray.prototype.forEachは使わない
- などなど

---
## ファイル数、モジュール数の分ファイルダウンロードさせる必要がある
- jQuery,bootstrap.css,bootstrap.js,...
    - 必要なモジュールの数だけユーザーのダウンロードファイル数が増える
- 共通のjsやページごとのjsなど
    - 管理のためにファイル分割するとユーザーのダウンロードファイル数が増える

---
## 前時代のフロントエンド：js,cssの読み込み
```html
<html>
  <head>
    <link rel="stylesheet" href="bootstrap.css" />
    <link rel="stylesheet" href="my-common.css" />
    <link rel="stylesheet" href="my-page.css" />
    <script src="jquery.js"></script>
    <script src="jquery-ui.js"></script>
    <script src="bootstrap.js"></script>
    <script src="moment.js"></script>
    <script src="alertify.js"></script>
    <script src="my-common.js"></script>
    <script src="my-page.js"></script>
  </head>
  <body>
    ...
  </body>
</html>
```

---
# 今時のフロントエンド
---
- 書いたコードをそのままブラウザが実行する**わけではない**


---
## ターゲットブラウザを意識しなくていい
- ES6+で書いて未対応ブラウザのためにtranspileする
- 未対応ブラウザでも使いたい機能が動くようpolyfillする

---
## ファイル数、モジュール数はユーザーのダウンロードファイル数と関係ない
- ソースコードを分割して管理し、bundleしてまとめた1つの物をユーザーがダウンロードする

---
## ソースコードの改行・インデントや変数名の長さはユーザーのダウンロードファイルの容量と関係ない
- ソースコードをminifyして圧縮された物をユーザーがダウンロードする

---
## 今時のフロントエンド：js,cssの読み込み

```html
<html>
  <head>
    <script src="bundle.js"></script>
  </head>
  <body>
    ...
  </body>
</html>
```
スタイル定義もjsに入っているのでcssの読み込み不要

---
# transpile(トランスパイル)

- あるプログラミング言語をインプットとして、同等のソースコードを別のプログラミング言語で出力すること
- ES6+で書いたコードをES6+が動かないブラウザでも動作するES5に変換する

---
# polyfill(ポリフィル)
- 古いブラウザであってもモダンブラウザと同等の機能を提供すること
- 例えばIEにString.padStartメソッドはない
- String.padStartがなくてもpadStart相当のことを行う実装を入れること

---
# transpile,polyfillの目的
- 開発で使う言語使用・及び機能についてターゲットブラウザを意識しなくてよくなる
  - IE11がターゲットに入っていてもスマートなコーディングができる

---

# bundle(バンドル)
- ソースコードを組み合わせてひとまとめにすること

---
# bundleの目的
- 開発時はユーザーのことを気にせず好きなだけファイル分割できるので、ソースコード管理が楽になる
- ユーザーがダウンロードするファイル数を減らせる

---
# minify(ミニファイ)
- ソースコードの実行時に不要となる改行やコメントの削除、変数の文字数を減らすなど同等の内容で短い記述にして、ソースコードの容量を減らすこと

---
# minifyの目的
- ユーザーがダウンロードするファイルの容量を減らせる

---
# ここまでのまとめ
---
## 今時のフロントエンドは
- transpileを前提として、好きな言語で書く
- polyfillを前提として、ブラウザごとの機能の有無を気にせずに書く
- bundleを前提として、適宜ファイル分割したりパッケージを使用して書く
- minifyを前提として、変数名やコンポーネント名は長くてもいいから分かりやすく書く

---

# 実際に今時のフロントエンド開発環境を作ろう
---
## 作る物
---
入力した文字列をマスクした内容をalertで表示するページ
<img width="637" alt="qiita_babel_webpack_sample.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71390/6f7d6d0f-305b-dc24-9a0b-af6450c7b957.png">

---

## 今回のゴール
- babel+webpackでフロントエンド開発環境を作成できる
- React,Vue,AngularのようなUIライブラリは<font color="red">**含まない**</font>

---
## 使用するツールと役割
- babel: transpile, polyfill
- webpack: bundle, minify

---
## 事前準備
- nodeのインストールが必要
- インストール方法はなんでもOK
  - 例： https://nodejs.org/ja/download/ からダウンロード&インストール
- ターミナルで `node -v` でバージョン情報が出ればOK

---
# 実装

---
## 自分で手を動かしたい方向け
- 空ディレクトリを作成する
- どこでもOK。以下は例
  - mkdir ~/frontend-handson
  - cd ~/frontend-handson
  
---
## または既に実装済みのものを動かせればよい方
- 以下リポジトリをcloneする
  - git clone git@github.com:yas-tyoukan/babel-webpack-handson.git
  - cd babel-webpack-handson

---
## 初期化する
- npm init # いろいろ聞かれるがenter連打でOK

---
## htmlを作成
- index.html

```html
<!DOCTYPE>
<html>
<head>
    <meta charset="utf8"/>
    <title>sample</title>
</head>
<body>
Input key and Click button.
<input name="key" type="text" value="abcdefgh" />
<button type="button" id="sample-button">mask</button>
<script src="bundle.js"></script>
</body>
</html>

```

---
## cssの作成
- sample.css

```css
button {
    border: 1px solid red;
}
```

---
## jsの作成1
- maskKey.js

```js
// const, アロー関数, デフォルト引数,
const maskKey = (key, padString = '*') => {
  // const
  const sliced = key.slice(-4);
  // String.prototype.padStart
  return sliced.padStart(key.length, padString);
};

export default maskKey;
```

---
## jsの作成2
- sampleFormHandler.js

```js
import maskKey from './maskKey';

const buttonEl = document.getElementById('sample-button');
const inputEl = document.querySelector('[name=key]');
buttonEl.addEventListener('click', () => {
  alert(maskKey(inputEl.value));
});
```

---
## jsの作成3
- entry.js

```js
// polyfillを実現するライブラリのimport
import 'core-js/stable';
import 'regenerator-runtime/runtime';

// 作成したファイルのimport
import './sampleFormHandler';
import './sample.css';

```

---
# 環境構築

---
## babel
---
### babelとは
- トランスパイル・ポリフィルを行うツール
- ES6+で書いたのでそれを古いブラウザでも動くようにするのが目的

---
### babelの環境構築
次のコマンドを実行してパッケージをインストールする

---
```sh
npm i core-js@^3.6.4 regenerator-runtime@^0.13.3
```

```sh
npm i -D @babel/core@^7.8.3 @babel/register@^7.8.3 @babel/preset-env@^7.8.3 @babel/cli@^7.8.3
```

---
### babelの設定ファイル作成
- .babelrc.js

```js
module.exports = (api) => {
  api.cache(true);
  const presets = [
    [
      '@babel/preset-env',
      {
        targets: {
          chrome: '79',
          ie: '11',
          firefox: '72',
          safari: '13',
        },
        useBuiltIns: 'entry',
        corejs: 3,
        debug: true,
      },
    ],
  ];

  const plugins = [];

  return {
    presets,
    plugins,
  };
};

```


---
## 動作確認

`maskKey.js`のトランスパイル

```sh
npx babel maskKey.js -o output.js
```

---

`output.js`が作成される。中身を見るとトランスパイルされていることが分かる

```js
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

// const, アロー関数, デフォルト引数,
var maskKey = function maskKey(key) {
  var padString = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '*';
  // const
  var sliced = key.slice(-4); // String.prototype.padStart

  return sliced.padStart(key.length, padString);
};

var _default = maskKey;
exports.default = _default;

```

---
`entry.js`のポリフィル

```sh
npx babel entry.js -o output.js
```

---
結果

```js
"use strict";

require("core-js/modules/es.symbol");

require("core-js/modules/es.symbol.description");

require("core-js/modules/es.symbol.async-iterator");

require("core-js/modules/es.symbol.has-instance");

require("core-js/modules/es.symbol.is-concat-spreadable");

require("core-js/modules/es.symbol.iterator");

require("core-js/modules/es.symbol.match");

require("core-js/modules/es.symbol.replace");

require("core-js/modules/es.symbol.search");

require("core-js/modules/es.symbol.species");

require("core-js/modules/es.symbol.split");

require("core-js/modules/es.symbol.to-primitive");

require("core-js/modules/es.symbol.to-string-tag");

require("core-js/modules/es.symbol.unscopables");

require("core-js/modules/es.array.concat");

require("core-js/modules/es.array.copy-within");

require("core-js/modules/es.array.every");

require("core-js/modules/es.array.fill");

require("core-js/modules/es.array.filter");

require("core-js/modules/es.array.find");

require("core-js/modules/es.array.find-index");

require("core-js/modules/es.array.flat");

require("core-js/modules/es.array.flat-map");

require("core-js/modules/es.array.for-each");

require("core-js/modules/es.array.from");

require("core-js/modules/es.array.includes");

require("core-js/modules/es.array.index-of");

require("core-js/modules/es.array.iterator");

require("core-js/modules/es.array.join");

require("core-js/modules/es.array.last-index-of");

require("core-js/modules/es.array.map");

require("core-js/modules/es.array.of");

require("core-js/modules/es.array.reduce");

require("core-js/modules/es.array.reduce-right");

require("core-js/modules/es.array.slice");

require("core-js/modules/es.array.some");

require("core-js/modules/es.array.species");

require("core-js/modules/es.array.splice");

require("core-js/modules/es.array.unscopables.flat");

require("core-js/modules/es.array.unscopables.flat-map");

require("core-js/modules/es.array-buffer.constructor");

require("core-js/modules/es.date.to-primitive");

require("core-js/modules/es.function.has-instance");

require("core-js/modules/es.function.name");

require("core-js/modules/es.json.to-string-tag");

require("core-js/modules/es.map");

require("core-js/modules/es.math.acosh");

require("core-js/modules/es.math.asinh");

require("core-js/modules/es.math.atanh");

require("core-js/modules/es.math.cbrt");

require("core-js/modules/es.math.clz32");

require("core-js/modules/es.math.cosh");

require("core-js/modules/es.math.expm1");

require("core-js/modules/es.math.fround");

require("core-js/modules/es.math.hypot");

require("core-js/modules/es.math.imul");

require("core-js/modules/es.math.log10");

require("core-js/modules/es.math.log1p");

require("core-js/modules/es.math.log2");

require("core-js/modules/es.math.sign");

require("core-js/modules/es.math.sinh");

require("core-js/modules/es.math.tanh");

require("core-js/modules/es.math.to-string-tag");

require("core-js/modules/es.math.trunc");

require("core-js/modules/es.number.constructor");

require("core-js/modules/es.number.epsilon");

require("core-js/modules/es.number.is-finite");

require("core-js/modules/es.number.is-integer");

require("core-js/modules/es.number.is-nan");

require("core-js/modules/es.number.is-safe-integer");

require("core-js/modules/es.number.max-safe-integer");

require("core-js/modules/es.number.min-safe-integer");

require("core-js/modules/es.number.parse-float");

require("core-js/modules/es.number.parse-int");

require("core-js/modules/es.number.to-fixed");

require("core-js/modules/es.object.assign");

require("core-js/modules/es.object.define-getter");

require("core-js/modules/es.object.define-setter");

require("core-js/modules/es.object.entries");

require("core-js/modules/es.object.freeze");

require("core-js/modules/es.object.from-entries");

require("core-js/modules/es.object.get-own-property-descriptor");

require("core-js/modules/es.object.get-own-property-descriptors");

require("core-js/modules/es.object.get-own-property-names");

require("core-js/modules/es.object.get-prototype-of");

require("core-js/modules/es.object.is");

require("core-js/modules/es.object.is-extensible");

require("core-js/modules/es.object.is-frozen");

require("core-js/modules/es.object.is-sealed");

require("core-js/modules/es.object.keys");

require("core-js/modules/es.object.lookup-getter");

require("core-js/modules/es.object.lookup-setter");

require("core-js/modules/es.object.prevent-extensions");

require("core-js/modules/es.object.seal");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.object.values");

require("core-js/modules/es.promise");

require("core-js/modules/es.promise.finally");

require("core-js/modules/es.reflect.apply");

require("core-js/modules/es.reflect.construct");

require("core-js/modules/es.reflect.define-property");

require("core-js/modules/es.reflect.delete-property");

require("core-js/modules/es.reflect.get");

require("core-js/modules/es.reflect.get-own-property-descriptor");

require("core-js/modules/es.reflect.get-prototype-of");

require("core-js/modules/es.reflect.has");

require("core-js/modules/es.reflect.is-extensible");

require("core-js/modules/es.reflect.own-keys");

require("core-js/modules/es.reflect.prevent-extensions");

require("core-js/modules/es.reflect.set");

require("core-js/modules/es.reflect.set-prototype-of");

require("core-js/modules/es.regexp.constructor");

require("core-js/modules/es.regexp.exec");

require("core-js/modules/es.regexp.flags");

require("core-js/modules/es.regexp.to-string");

require("core-js/modules/es.set");

require("core-js/modules/es.string.code-point-at");

require("core-js/modules/es.string.ends-with");

require("core-js/modules/es.string.from-code-point");

require("core-js/modules/es.string.includes");

require("core-js/modules/es.string.iterator");

require("core-js/modules/es.string.match");

require("core-js/modules/es.string.pad-end");

require("core-js/modules/es.string.pad-start");

require("core-js/modules/es.string.raw");

require("core-js/modules/es.string.repeat");

require("core-js/modules/es.string.replace");

require("core-js/modules/es.string.search");

require("core-js/modules/es.string.split");

require("core-js/modules/es.string.starts-with");

require("core-js/modules/es.string.trim");

require("core-js/modules/es.string.trim-end");

require("core-js/modules/es.string.trim-start");

require("core-js/modules/es.string.anchor");

require("core-js/modules/es.string.big");

require("core-js/modules/es.string.blink");

require("core-js/modules/es.string.bold");

require("core-js/modules/es.string.fixed");

require("core-js/modules/es.string.fontcolor");

require("core-js/modules/es.string.fontsize");

require("core-js/modules/es.string.italics");

require("core-js/modules/es.string.link");

require("core-js/modules/es.string.small");

require("core-js/modules/es.string.strike");

require("core-js/modules/es.string.sub");

require("core-js/modules/es.string.sup");

require("core-js/modules/es.typed-array.float32-array");

require("core-js/modules/es.typed-array.float64-array");

require("core-js/modules/es.typed-array.int8-array");

require("core-js/modules/es.typed-array.int16-array");

require("core-js/modules/es.typed-array.int32-array");

require("core-js/modules/es.typed-array.uint8-array");

require("core-js/modules/es.typed-array.uint8-clamped-array");

require("core-js/modules/es.typed-array.uint16-array");

require("core-js/modules/es.typed-array.uint32-array");

require("core-js/modules/es.typed-array.copy-within");

require("core-js/modules/es.typed-array.every");

require("core-js/modules/es.typed-array.fill");

require("core-js/modules/es.typed-array.filter");

require("core-js/modules/es.typed-array.find");

require("core-js/modules/es.typed-array.find-index");

require("core-js/modules/es.typed-array.for-each");

require("core-js/modules/es.typed-array.from");

require("core-js/modules/es.typed-array.includes");

require("core-js/modules/es.typed-array.index-of");

require("core-js/modules/es.typed-array.iterator");

require("core-js/modules/es.typed-array.join");

require("core-js/modules/es.typed-array.last-index-of");

require("core-js/modules/es.typed-array.map");

require("core-js/modules/es.typed-array.of");

require("core-js/modules/es.typed-array.reduce");

require("core-js/modules/es.typed-array.reduce-right");

require("core-js/modules/es.typed-array.reverse");

require("core-js/modules/es.typed-array.set");

require("core-js/modules/es.typed-array.slice");

require("core-js/modules/es.typed-array.some");

require("core-js/modules/es.typed-array.sort");

require("core-js/modules/es.typed-array.subarray");

require("core-js/modules/es.typed-array.to-locale-string");

require("core-js/modules/es.typed-array.to-string");

require("core-js/modules/es.weak-map");

require("core-js/modules/es.weak-set");

require("core-js/modules/web.dom-collections.for-each");

require("core-js/modules/web.dom-collections.iterator");

require("core-js/modules/web.immediate");

require("core-js/modules/web.queue-microtask");

require("core-js/modules/web.url");

require("core-js/modules/web.url.to-json");

require("core-js/modules/web.url-search-params");

require("regenerator-runtime/runtime");

require("./sampleFormHandler");

require("./sample.css");

```

---

## babelの設定を変えて確認
`.babelrc.js`の`targets`の部分を変えるとトランスパイル・ポリフィル結果が変わる。

例えば`ie: '11'`の部分はIE11でも動作するコードにトランスパイル・ポリフィルする設定であるが、`ie: 11`の部分をコメントアウトして試してみる。

---
## .babelrc.js (IE11指定をコメントアウト)

```js
module.exports = (api) => {
  api.cache(true);
  const presets = [
    [
      '@babel/preset-env',
      {
        targets: {
          chrome: '79',
          // ie: '11', // <- コメントアウト
          firefox: '72',
          safari: '13',
        },
        useBuiltIns: 'entry',
        corejs: 3,
        debug: true,
      },
    ],
  ];

  const plugins = [];

  return {
    presets,
    plugins,
  };
};
```
---

IE11をターゲットから外して `maskKey.js`のトランスパイル

```sh
npx babel maskKey.js -o output.js
```

---

結果

```js
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

// const, アロー関数, デフォルト引数,
const maskKey = (key, padString = '*') => {
  // const
  const sliced = key.slice(-4); // String.prototype.padStart

  return sliced.padStart(key.length, padString);
};

var _default = maskKey;
exports.default = _default;

```

---
IE11をターゲットから外して `entry.js`のポリフィル

```sh
npx babel entry.js -o output.js
```

---
結果

```js
"use strict";

require("core-js/modules/es.promise.finally");

require("core-js/modules/es.string.replace");

require("core-js/modules/es.typed-array.float32-array");

require("core-js/modules/es.typed-array.float64-array");

require("core-js/modules/es.typed-array.int8-array");

require("core-js/modules/es.typed-array.int16-array");

require("core-js/modules/es.typed-array.int32-array");

require("core-js/modules/es.typed-array.uint8-array");

require("core-js/modules/es.typed-array.uint8-clamped-array");

require("core-js/modules/es.typed-array.uint16-array");

require("core-js/modules/es.typed-array.uint32-array");

require("core-js/modules/es.typed-array.from");

require("core-js/modules/es.typed-array.of");

require("core-js/modules/web.dom-collections.iterator");

require("core-js/modules/web.immediate");

require("core-js/modules/web.url");

require("core-js/modules/web.url.to-json");

require("core-js/modules/web.url-search-params");

require("./sampleFormHandler");

require("./sample.css");
```
---
## 結果について
- アロー関数などが`function(){}`の書き方に変わっている
- polyfillのためのパッケージの`require`文が列挙されている
- IE11をターゲットから外すとアロー関数などはそのまま
- IE11をターゲットから外すとpolyfillのためのパッケージの`require`文が減る

---
# webpack

---
## webpackとは
- bundle, minifyを行うツール
- 他にもいろいろできる。linterを設定するとか（今回はやらない）
- 依存関係を解決して一つにまとめるのが目的

---
## webpackの環境構築
次のコマンドを実行してパッケージをインストールする

---
```sh
npm i webpack webpack-cli babel-loader css-loader style-loader
```

---
## webpackの環境構築
- webpack.config.babel.js

```js
import path from 'path';

export default (env, args) => {
  const isProduction = args.mode === 'production';
  const devtool = !isProduction && 'inline-source-map';
  const rules = [
    {
      test: /\.js?$/,
      use: ['babel-loader'],
    },
    {
      test: /\.css$/,
      use: ['style-loader', 'css-loader'],
    },
  ];

  const plugins = [];

  return {
    devtool,
    entry: './entry.js',
    output: {
      path: path.join(__dirname, './'),
      filename: 'bundle.js',
    },
    module: { rules },
    plugins,
  };
};
```

---
## 動作確認
minifyする

```sh
npx webpack --mode production
```

---
生成される`bundle.js`の中身(一部)

```js
!function(t){var e={};function n(r){if(e[r])return e[r]……(省略)
```

---


minifyしない

```sh
npx webpack --mode development
```

---
生成される`bundle.js`の中身(一部)

```js
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
……(省略)
```

---

# ブラウザで動作確認

- `index.html`をブラウザで開く

<img width="637" alt="qiita_babel_webpack_sample.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71390/6f7d6d0f-305b-dc24-9a0b-af6450c7b957.png">

---
# おまけ
---

## watch機能

ソースコードの変更を監視、変更があれば都度webpack実行(Ctrl+cで終了)

```sh
npx webpack --mode development --watch
```

---
## npm scriptsの活用
- `package.json`の`"scripts"`に以下のように記述追加
 
```json
  "scripts": {
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "watch": "webpack --mode development --watch"
  },
```
`npm run watch` だけでwatchできる 

---
## watchしながら実装変更してみよう
- 文言を変える
- cssを変える
- ロジック変える
- などなど

---
# まとめ

---
## 今回の学習のまとめ
- transpile,polyfill,bundle,minifyを前提として、ターゲットブラウザに依存せずにES6+で、ファイルを分割してプログラミングする今時の書き方が理解できた
- Babel で transpile, polyfill できるようになった
- Webpackでbundle,minify できるようになった

---
# 終わり