Babelとか振り返る
Next.jsが隠蔽しているものが知りたい。
このごく簡単なHTMLから始める。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Web Frontend Learn</h1>
</body>
</html>
Reactを使ってカウンターを作ってみる。ReactはまだCDN (unpkg) から導入する。
CDNから突っ込まれたスクリプトがグローバルに React
ReactDOM
を定義する。ブラウザコンソールから React
にアクセスできるはずだ。
<!DOCTYPE html>
<html lang="en">
<head>
<script
src="https://unpkg.com/react@17/umd/react.development.js"
crossorigin
></script>
<script
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
crossorigin
></script>
</head>
<body>
<h1>Web Frontend Learn</h1>
<div id="react-root"></div>
</body>
<script>
"use strict";
const Counter = () => {
const [count, setCount] = React.useState(0);
console.log(typeof count);
return React.createElement(
"div",
{},
React.createElement("div", {}, count),
React.createElement(
"button",
{
onClick: () => setCount((count) => count + 1),
},
"+1"
)
);
};
const reactRoot = document.getElementById("react-root");
ReactDOM.render(React.createElement(Counter), reactRoot);
</script>
</html>
JSを別ファイルに分けてもよい。
<script src="counter.js"></script>
"use strict";
const Counter = () => {
const [count, setCount] = React.useState(0);
console.log(typeof count);
return React.createElement(
"div",
{},
React.createElement("div", {}, count),
React.createElement(
"button",
{
onClick: () => setCount((count) => count + 1),
},
"+1"
)
);
};
const reactRoot = document.getElementById("react-root");
ReactDOM.render(React.createElement(Counter), reactRoot);
グローバルに展開されたくない場合はES Modulesから導入するという手がある。今回はPreact。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Web Frontend Learn</h1>
<div id="react-root"></div>
</body>
<script src="counter.mjs" type="module"></script>
</html>
Node.jsではないのでES Modulesか否かは script[type]
で判別されるから、拡張子をmjsにする必要はあまりない。気分。
import { h, render } from "https://cdn.skypack.dev/preact";
import { useState } from "https://cdn.skypack.dev/preact/hooks";
const Counter = () => {
const [count, setCount] = useState(0);
return h(
"div",
{},
h("div", {}, count),
h(
"button",
{
onClick: () => setCount((count) => count + 1),
},
"+1"
)
);
};
const reactRoot = document.getElementById("react-root");
render(h(Counter), reactRoot);
Reactに戻る。要素をJSXで書きたいのでBabelを導入する。BabelはJS->JSのコンパイラ。npm i -D @babel/core @babel/cli @babel/preset-react
。
{
"presets": [["@babel/preset-react", {}]]
}
counter.jsの拡張子をjsxに変更し、JSXに書き換える。
const Counter = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount((count) => count + 1)}>+1</button>
</div>
);
};
const reactRoot = document.getElementById("react-root");
ReactDOM.render(<Counter />, reactRoot);
この状態で npx babel src/counter.jsx
を実行するとトランスパイル結果が標準出力される。npx babel src/counter.jsx --out-file src/counter.js
のようにするとファイルに出力できる。
ちょうどさっき利用した React.createElement
が登場している。
const Counter = () => {
const [count, setCount] = React.useState(0);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", null, count), /*#__PURE__*/React.createElement("button", {
onClick: () => setCount(count => count + 1)
}, "+1"));
};
const reactRoot = document.getElementById("react-root");
ReactDOM.render( /*#__PURE__*/React.createElement(Counter, null), reactRoot);
Babelのもともとの意義はES6(ES2015)で書かれたJSソースをES5相当に「落とす」ことであった。それもやってみる。npm i -D @babel/preset-env
。オプションからbrowserslist形式でターゲットにするブラウザを設定するといい感じにトランスパイルしてくれる。
{
"presets": [
["@babel/preset-react", {}],
["@babel/preset-env", { "targets": "defaults" }]
]
}
npx babel src/counter.jsx --out-file src/counter.js
を打つと、今度は古めかしいソースファイルが生成される。"use strict";
の存在と const
が var
に変換されていることからES5相当に変換されていることがわかる。
"use strict";
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
var Counter = function Counter() {
var _React$useState = React.useState(0),
_React$useState2 = _slicedToArray(_React$useState, 2),
count = _React$useState2[0],
setCount = _React$useState2[1];
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", null, count), /*#__PURE__*/React.createElement("button", {
onClick: function onClick() {
return setCount(function (count) {
return count + 1;
});
}
}, "+1"));
};
var reactRoot = document.getElementById("react-root");
ReactDOM.render( /*#__PURE__*/React.createElement(Counter, null), reactRoot);
ReactをCDNからではなくnpmから導入する。npmパッケージから持ってきたCommonJSモジュールをブラウザで実行するためにwebpackを使用する。npm i -D webpack webpack-cli babel-loader
して
/**
* @type {import("webpack").Configuration}
*/
module.exports = {
mode: "development",
module: {
rules: [
{
test: /\.jsx$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [["@babel/preset-react", {}]],
},
},
},
],
},
};
を設定する。ソースファイルに import
を追加する。
import React from "react";
import ReactDOM from "react-dom";
const Counter = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount((count) => count + 1)}>+1</button>
</div>
);
};
const reactRoot = document.getElementById("react-root");
ReactDOM.render(<Counter />, reactRoot);
npx webpack ./src/counter.jsx
すると、ReactとReactDOMのコード (とカウンターのちっちゃなコード) が含まれた巨大なファイルが dist/main.js
に出力される。
上記にもあったように、webpackはES Modulesで書かれたソースファイルを解釈しバンドルする。
このようなソースファイル群を用意して npx webpack ./main.js
すると、
import { constructMessage } from "./submod.js";
const name = prompt("tell me your name");
const message = constructMessage(name);
console.log(message);
export const constructMessage = (name) => `Hello, ${name}!`;
このようなファイルが出力される (読みやすいようPrettierで整形してある)。
(() => {
"use strict";
const o = `Hello, ${prompt("tell me your name")}!`;
console.log(o);
})();
TypeScriptを導入する。npm i -D typescript @types/react @types/react-dom
。
TypeScriptコンパイラ(tsc)は役割的にBabelとかぶっているところが多い。Babelの直後にやるべきだったな。
-
jsx: preserve
を選択する。tscにJSXの変換をさせることもできるが、後のBabelの工程に任せてしまってもよい。 -
target: esnext
を選択する。ES5などでの出力も可能だが、Babelに任せたいときは可能な限りそのま出力する。 -
module: esnext
を選択する。ES Modulesのままでもどうせwebpackがバンドルしてくれるので。
{
"files": ["src/counter.tsx"],
"compilerOptions": {
/* Language and Environment */
"target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"jsx": "react" /* Specify what JSX code is generated. */,
/* Modules */
"module": "esnext" /* Specify what module code is generated. */,
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
/* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
/* Interop Constraints */
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
import React from "react";
import ReactDOM from "react-dom";
const Counter: React.VFC = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount((count) => count + 1)}>+1</button>
</div>
);
};
const reactRoot = document.getElementById("react-root");
ReactDOM.render(
<React.StrictMode>
<Counter />
</React.StrictMode>,
reactRoot
);
型情報が落ちただけのファイルが出てくる。
import React from "react";
import ReactDOM from "react-dom";
const Counter = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount((count) => count + 1)}>+1</button>
</div>
);
};
const reactRoot = document.getElementById("react-root");
ReactDOM.render(
<React.StrictMode>
<Counter />
</React.StrictMode>,
reactRoot
);
jsx: react
にするとさっき見たようなコードが出てくる。
import React from "react";
import ReactDOM from "react-dom";
const Counter = () => {
const [count, setCount] = React.useState(0);
return React.createElement(
"div",
null,
React.createElement("div", null, count),
React.createElement(
"button",
{ onClick: () => setCount((count) => count + 1) },
"+1"
)
);
};
const reactRoot = document.getElementById("react-root");
ReactDOM.render(
React.createElement(
React.StrictMode,
null,
React.createElement(Counter, null)
),
reactRoot
);
@babel/preset-typescript
を使うとBabelでTypeScriptをJavaScriptに変換できる。内部的にtscは使われておらず、型チェックの工程はない。型チェックや型定義の出力には別にtscを使う。
{
"presets": [["@babel/preset-react", {}], ["@babel/preset-typescript"]]
}
@babel/preset-typescript
を入れて babel-loader を使うとwebpackからTypeScriptが処理できる。なお .resolve.extensions
にTS系拡張子を追加しておかないとインポートで失敗する。
/**
* @type {import("webpack").Configuration}
*/
module.exports = {
mode: "production",
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
["@babel/preset-typescript"],
["@babel/preset-react", {}],
],
},
},
},
],
},
};
import { Counter } from "./counter";
import React from "react";
import ReactDOM from "react-dom";
const reactRoot = document.getElementById("react-root");
const App = () => {
return (
<div>
<Counter />
</div>
);
};
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
reactRoot
);
import React from "react";
export const Counter: React.VFC = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount((count) => count + 1)}>+1</button>
</div>
);
};