Closed3

Snowpack を使ってみた

AltechAltech

Snowpack とは

Webpack などでモジュール・バンドリングをせずに、ES Module を実行環境でもそのまま利用できるビルドツール。

create-react-app は Webpack が使われているが、minify を切っても実行コードが追いやすいとは言えない感じだった。その多くがモジュール周りの扱いだと感じたので、それを解決するツールを使ってみた。

参考:

AltechAltech

どういうことをしているか?

create-react-app (CRA) と同じものを実現するアプリについて、以下の三つのソースコードを掲載する。

  1. TypeScript で開発しているソースコード(TypeScript版)
  2. ビルドしたソースコード(ビルド版)
  3. Hot Module Replacement (HMR) が有効な開発版のソースコード(開発版)

理想的には、TypeScript 版に対してビルド版は型定義などが取り除かれ JSX が展開された JavaScript、ビルド版に対して開発版は HMR など開発専用の機能がシンプルに付与されていることを期待したい。

TypeScript版

import React, { useState, useEffect } from 'react';
import logo from './logo.svg';
import './App.css';

interface AppProps {}

function App({}: AppProps) {
  // Create the count state.
  const [count, setCount] = useState(0);
  // Create the counter (+1 every second).
  useEffect(() => {
    const timer = setTimeout(() => setCount(count + 1), 1000);
    return () => clearTimeout(timer);
  }, [count, setCount]);
  // Return the App component.
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <p>
          Page has been open for <code>{count}</code> seconds.
        </p>
        <p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </p>
      </header>
    </div>
  );
}

export default App;

ビルド版

import React, {useState, useEffect} from "../_snowpack/pkg/react.js";
import logo from "./logo.svg.proxy.js";
import "./App.css.proxy.js";
function App({}) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setTimeout(() => setCount(count + 1), 1e3);
    return () => clearTimeout(timer);
  }, [count, setCount]);
  return /* @__PURE__ */ React.createElement("div", {
    className: "App"
  }, /* @__PURE__ */ React.createElement("header", {
    className: "App-header"
  }, /* @__PURE__ */ React.createElement("img", {
    src: logo,
    className: "App-logo",
    alt: "logo"
  }), /* @__PURE__ */ React.createElement("p", null, "Edit ", /* @__PURE__ */ React.createElement("code", null, "src/App.tsx"), " and save to reload."), /* @__PURE__ */ React.createElement("p", null, "Page has been open for ", /* @__PURE__ */ React.createElement("code", null, count), " seconds."), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("a", {
    className: "App-link",
    href: "https://reactjs.org",
    target: "_blank",
    rel: "noopener noreferrer"
  }, "Learn React"))));
}
export default App;

変更点は以下。

import パス

npm package (react):

'react' -> "../_snowpack/pkg/react.js": react の本番用に minify されたコードをモジュールとして export していた(format して元ソースと比較しt確認)

アセット:

  • './logo.svg' -> "./logo.svg.proxy.js" ... svg のパスを文字列として export
  • './App.css' -> "./App.css.proxy.js" ... CSSに書かれたスタイルを DOM ツリーにアタッチ

これ以外はそのまま、TyepScript と JSX の変換のみ。

開発版

import * as  __SNOWPACK_HMR__ from '../_snowpack/hmr-client.js';
import.meta.hot = __SNOWPACK_HMR__.createHotContext(import.meta.url);
import * as __SNOWPACK_ENV__ from '../_snowpack/env.js';
import.meta.env = __SNOWPACK_ENV__;

/** React Refresh: Setup **/
if (import.meta.hot) {
  if (!window.$RefreshReg$ || !window.$RefreshSig$ || !window.$RefreshRuntime$) {
    console.warn('@snowpack/plugin-react-refresh: HTML setup script not run. React Fast Refresh only works when Snowpack serves your HTML routes. You may want to remove this plugin.');
  } else {
    var prevRefreshReg = window.$RefreshReg$;
    var prevRefreshSig = window.$RefreshSig$;
    window.$RefreshReg$ = (type, id) => {
      window.$RefreshRuntime$.register(type, "/Users/sohei/src/github.com/Altech/my-snowpack-react-tutorial/src/App.js" + " " + id);
    }
    window.$RefreshSig$ = window.$RefreshRuntime$.createSignatureFunctionForTransform;
  }
}

var _s = $RefreshSig$();

import React, { useState, useEffect } from "../_snowpack/pkg/react.v17.0.2.js";
import logo from "./logo.svg.proxy.js";
import "./App.css.proxy.js";

function App({}) {
  _s();

  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setTimeout(() => setCount(count + 1), 1e3);
    return () => clearTimeout(timer);
  }, [count, setCount]);
  return /* @__PURE__ */React.createElement("div", {
    className: "App"
  }, /* @__PURE__ */React.createElement("header", {
    className: "App-header"
  }, /* @__PURE__ */React.createElement("img", {
    src: logo,
    className: "App-logo",
    alt: "logo"
  }), /* @__PURE__ */React.createElement("p", null, "Edit ", /* @__PURE__ */React.createElement("code", null, "src/App.tsx"), " and save to reload."), /* @__PURE__ */React.createElement("p", null, "Page has been open for ", /* @__PURE__ */React.createElement("code", null, count), " seconds."), /* @__PURE__ */React.createElement("p", null, /* @__PURE__ */React.createElement("a", {
    className: "App-link",
    href: "https://reactjs.org",
    target: "_blank",
    rel: "noopener noreferrer"
  }, "Learn React"))));
}

_s(App, "/xL7qdScToREtqzbt5GZ1kHtYjQ=");

_c = App;
export default App;

var _c;

$RefreshReg$(_c, "App");

/** React Refresh: Connect **/
if (import.meta.hot) {
  window.$RefreshReg$ = prevRefreshReg
  window.$RefreshSig$ = prevRefreshSig
  import.meta.hot.accept(() => {
    window.$RefreshRuntime$.performReactRefresh()
  });
}
  • ホットモジュールリロードのコードが追加されている
AltechAltech

開発してみた所感

よかったこと

任意のレイヤーで動作を追っていける

  • ブラウザとエディタでレイヤーの差異がなく、自由に行き来できる
    • Typescript が素直に JavaScript にコンパイルされる
    • source map みたいなものも特にいらない(?)
  • ライブラリをどう読み込んでいるかも見たまま

Web フロントエンドへの入門であったり、小規模なアプリケーションの開発において複雑性を上げないメリットがあった。

逆に規模が大きくなると、How Snowpack Works とか HMR + Fast Refresh が効いてくるかもしれない。

わるかったこと

console からのモジュールテストがしづらい

  • モジュールなので当たり前なのだが、console から直接 React などのオブジェクトを参照できない
    • import しようとすると Uncaught SyntaxError: Cannot use import statement outside a module と言われる
  • これが console でモジュールの振る舞いを調べたい時にちょっとだけ不便
  • 代替としては dynamic import を使う(このとき pkg 以下のパスを指定する必要がある)
    • import("../_snowpack/pkg/react.v17.0.2.js").then(m => window.React = m)
    • importPkg(React: 'react') みたいな関数を仕込んでおくと良さそうではある(TODO: Snowpack のリフレクション調査)

(他にはあんまり自分では思いつかなかったけど「こう言うところ困りませんか?」みたいなのはありそう)

このスクラップは2021/08/15にクローズされました