はじめに
この記事は、今時のフロントエンド開発の最初の一歩を踏み出すために理解すべきことを理解する、そして、実際にハンズオン形式で最初の一歩を踏み出すところまでを行いながら理解を深める、という目的のハンズオン資料です。
社内勉強会のために作成した物を一般向けに改善したものとなります。
この記事のターゲット
- 今時のフロントエンド開発が昔とどう違うのか知りたい人
- 今時のフロントエンド開発の前提を学びたい人
- 今時のフロントエンド開発を実際にハンズオンで学びたい人
ES6+とは
ECMAScript6以降のこと
IEでは動かない新しい構文を含む
IEでは動かない新しい機能を含む
ES6+の例
// 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
デフォルト引数 - JavaScript | MDN
アロー関数 - JavaScript | MDN
String.prototype.padStart() - JavaScript | MDN
前時代のフロントエンド
HTML+CSS+jQuery
- さらに +jQueryMobile+jQueryUI+bootstrap... だったり
-
書いたコードがそのままブラウザで動くコード
- ターゲットブラウザを意識して書く必要がある
- ファイル数、モジュール数の分ファイルダウンロードさせる必要がある
ターゲットブラウザを意識して書く必要がある
- IE11がターゲットに入っているからアロー関数は使わない
- IE8がターゲットに入っているからArray.prototype.forEachは使わない
- などなど
ファイル数、モジュール数の分ファイルダウンロードさせる必要がある
- jQuery,bootstrap.css,bootstrap.js,...
- 必要なモジュールの数だけユーザーのダウンロードファイル数が増える
- 共通のjsやページごとのjsなど
- 管理のためにファイル分割するとユーザーのダウンロードファイル数が増える
前時代のフロントエンド:js,cssの読み込み
<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>
<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を前提として、変数名やコンポーネント名は長くてもいいから分かりやすく書く
実際に今時のフロントエンド開発環境を作ろう
作る物
今回のゴール
- babel+webpackでフロントエンド開発環境を作成できる
- React,Vue,AngularのようなUIライブラリは含まない
使用するツールと役割
- 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
<!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
button {
border: 1px solid red;
}
jsの作成1
- maskKey.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
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
// polyfillを実現するライブラリのimport
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// 作成したファイルのimport
import './sampleFormHandler';
import './sample.css';
環境構築
babel
babelとは
- トランスパイル・ポリフィルを行うツール
- ES6+で書いたのでそれを古いブラウザでも動くようにするのが目的
babelの環境構築
次のコマンドを実行してパッケージをインストールする
npm i core-js@^3.6.4 regenerator-runtime@^0.13.3
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
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
のトランスパイル
npx babel maskKey.js -o output.js
output.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
のポリフィル
npx babel entry.js -o output.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指定をコメントアウト)
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
のトランスパイル
npx babel maskKey.js -o output.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
のポリフィル
npx babel entry.js -o output.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の環境構築
次のコマンドを実行してパッケージをインストールする
npm i webpack webpack-cli babel-loader css-loader style-loader
webpackの環境構築
- webpack.config.babel.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する
npx webpack --mode production
生成されるbundle.js
の中身(一部)
!function(t){var e={};function n(r){if(e[r])return e[r]……(省略)
minifyしない
npx webpack --mode development
生成されるbundle.js
の中身(一部)
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
……(省略)
ブラウザで動作確認
-
index.html
をブラウザで開く
おまけ
watch機能
ソースコードの変更を監視、変更があれば都度webpack実行(Ctrl+cで終了)
npx webpack --mode development --watch
npm scriptsの活用
-
package.json
の"scripts"
に以下のように記述追加
"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 できるようになった