Closed12

Babelとか振り返る

おーみーおーみー

このごく簡単な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を別ファイルに分けてもよい。

index.html
  <script src="counter.js"></script>
counter.js
"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。

index.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>
    <div id="react-root"></div>
  </body>
  <script src="counter.mjs" type="module"></script>
</html>

Node.jsではないのでES Modulesか否かは script[type] で判別されるから、拡張子をmjsにする必要はあまりない。気分。

counter.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

.babelrc
{
  "presets": [["@babel/preset-react", {}]]
}

counter.jsの拡張子をjsxに変更し、JSXに書き換える。

counter.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 が登場している。

counter.js
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形式でターゲットにするブラウザを設定するといい感じにトランスパイルしてくれる。

.babelrc
{
  "presets": [
    ["@babel/preset-react", {}],
    ["@babel/preset-env", { "targets": "defaults" }]
  ]
}

npx babel src/counter.jsx --out-file src/counter.js を打つと、今度は古めかしいソースファイルが生成される。"use strict"; の存在と constvar に変換されていることから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 して

webpack.config.js
/**
 * @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 を追加する。

src/counter.jsx
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 すると、

main.js
import { constructMessage } from "./submod.js";

const name = prompt("tell me your name");

const message = constructMessage(name);

console.log(message);
submod.js
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がバンドルしてくれるので。
tsconfig.json
{
  "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. */
  }
}
counter.tsx
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
);

型情報が落ちただけのファイルが出てくる。

counter.jsx
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 にするとさっき見たようなコードが出てくる。

counter.js
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を使う。

.babelrc
{
  "presets": [["@babel/preset-react", {}], ["@babel/preset-typescript"]]
}
おーみーおーみー

@babel/preset-typescript を入れて babel-loader を使うとwebpackからTypeScriptが処理できる。なお .resolve.extensions にTS系拡張子を追加しておかないとインポートで失敗する。

webpack.config.js
/**
 * @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", {}],
            ],
          },
        },
      },
    ],
  },
};
app.tsx
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
);
counter.tsx
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>
  );
};
このスクラップは2021/12/15にクローズされました