Tech blog Produced by FOURIER

Web Componentライブラリ開発環境

Hirayama Hirayama 2022.11.09

はじめに

Web ComponentはReactやVue.jsのようにHTMLの要素をコンポーネント化し、再利用できるようにする技術ですが、快適に開発するには環境構築を整える必要があり、なかなか使い始めが大変です。

この記事では、まずはWeb Componentの開発しづらいポイントを解説し、それらを解消する開発環境の構築方法を紹介します。

Web Component開発しづらいポイント

1. HTML、CSSを扱いにくい

まず、Web Componentの基本的な定義は以下のようになります。

// エレメントのクラスを作成
class MyElement extends HTMLElement {
    constructor() {
        super();

        // CSS
        const style = `
            :host {
                  display: block;
                  border-radius: 1em;
                  border: 1em solid #4a6eff;
                  height: 100px;
                  width: 100px;
            }
        `;

        // HTML
        const template = document.createElement("template");
        template.innerHTML = `
            <style>${ style }</style>
            <p></p>
            <slot></slot>
        `;

        this.attachShadow({ mode: "open" });
        this.shadowRoot?.appendChild(template.content.cloneNode(true));
    }
}

// ブラウザが対応しているかチェックする
if ("customElements" in window) {
    // カスタムエレメントを定義
    // <fr-custom-input>が定義される
    customElements.define("my-element", MyElement);
}

コードを見ればお気づきになる方もいるかも知れませんが、CSSやHTMLをJavaScript内のテキストとして書いているためエディタの補正が効かなかったり、どうしてもコードが長くなりがちなので可読性が低くなるなど、この状態ではあまり開発しやすいとは言えません。

2. TypeScriptやSCSSなどで書けない

当たり前ですが、ブラウザはTypeScriptやSCSSを読み込めないため、Web Componentを書く際はJavaScriptやCSSで書く必要があります。

環境構築

上記で書いたいくつかの問題点を解決するため、webpackを使用して開発環境を整えます。

webpackインストール

まずは、webpackをインストールします。

npm init
npm i -D webpack webpack-cli

次に、webpackのconfigファイルを作成します。ライブラリなので output にはライブラリの設定を記述します。

export default {
    entry: [
        "./src/ts/index.js",
    ],
    output: {
        filename: "index.js",
        clean: true,
        library: {
            name: "my-lib",
            type: "umd",
            umdNamedDefine: true,
        },
    },
    resolve: {
        modules: [
            __dirname,
            path.resolve(__dirname, "node_modules"),
        ],
        extensions: [".wasm", ".ts", ".tsx", ".mjs", ".cjs", ".js", ".json"],
        plugins: [],
    },
    module: {
        rules: [],
    },
    optimization: {},
    plugins: [],
};

HTML、CSSファイルをインポートできるようにする

次に、以下のようにHTMLにHTMLファイル、CSSファイルを読み込ませるため、loader をインストールします。

今回の例ではSCSSを読み込めるようにしています。

npm i -D html-loader sass sass-loader
import path                    from "path";
import webpack                 from "webpack";

export default {
    entry: [
        "./src/ts/index.js",
    ],
    output: {
        filename: "index.js",
        clean: true,
        library: {
            name: "my-lib",
            type: "umd",
            umdNamedDefine: true,
        },
    },
    resolve: {
        modules: [
            __dirname,
            path.resolve(__dirname, "node_modules"),
        ],
        extensions: [".wasm", ".ts", ".tsx", ".mjs", ".cjs", ".js", ".json"],
        plugins: [],
    },
    module: {
        rules: [
            {
                test: /\.html$/i,
                use: "html-loader",
            },
            {
                test: /\.s[ac]ss$/i,
                use: [
                    {
                        loader: "sass-loader",
                        options: {
                            sassOptions: {
                                outputStyle: "compressed",
                            }
                        }
                    }
                ],
                type: 'asset/source',
            }
        ],
    },
    optimization: {},
    plugins: [],
};

これで、HTMLとCSS(SCSS)をimportをすることができるようになりました。

以下のようにしてファイルを読み込みます。

<p></p>
<slot></slot>
:host {
    display: block;
    border-radius: 1em;
    border: 1em solid #4a6eff;
    height: 100px;
    width: 100px;
}
import html  from "./template.html";
import style from "./style.scss";

export default class CustomInput extends HTMLElement {
    constructor() {
        super();

        const template     = document.createElement("template");
        template.innerHTML = `<style>${ style }</style>${ html }`;

        this.attachShadow({ mode: "open" });
        this.shadowRoot?.appendChild(template.content.cloneNode(true));
    }
}

これで、HTMLとCSSを外部で定義し、読み込めるようになったため、可読性を向上させられました。利用する際は一つのJavaScriptファイルを読み込むだけで済むので、手軽にコンポーネントを使用できます。

TypeScriptを読み込めるようにする

TypeScriptでコンポーネントを書く場合は、HTMLとCSSをインポートできるようにしたときと同じように、loader を追加します。

npm i -D typescript ts-loader
import path                    from "path";
import { TsconfigPathsPlugin } from "tsconfig-paths-webpack-plugin";
import webpack                 from "webpack";

export default {
    entry: [
        "./src/ts/index.ts",
    ],
    output: {
        filename: "index.js",
        clean: true,
        library: {
            name: "my-lib",
            type: "umd",
            umdNamedDefine: true,
        },
    },
    resolve: {
        modules: [
            __dirname,
            path.resolve(__dirname, "node_modules"),
        ],
        extensions: [".wasm", ".ts", ".tsx", ".mjs", ".cjs", ".js", ".json"],
        plugins: [
            new TsconfigPathsPlugin({
                configFile: "./tsconfig.json",
            }),
        ],
    },
    module: {
        rules: [
            {
                test: /\.html$/i,
                use: "html-loader",
            },
            {
                test: /\.tsx?$/,
                use: "ts-loader",
                exclude: /node_modules/,
            },
            {
                test: /\.s[ac]ss$/i,
                use: [
                    {
                        loader: "sass-loader",
                        options: {
                            sassOptions: {
                                outputStyle: "compressed",
                            }
                        }
                    }
                ],
                type: 'asset/source',
            }
        ],
    },
    optimization: {},
    plugins: [],
};

グローバルスタイルをコンパイルする

コンポーネントとは別にグローバルスタイルを定義し、CSSファイルとして出力したい場合、webpackの設定を少し工夫する必要があります。

理由としては、webpackでCSSをコンパイルして出力したいときは MiniCssExtractPlugin を使用しますが、単純に全てのSCSSファイルを対象とするとコンポーネント用のCSSまで出力されてしまい、コンポーネントにスタイルが当たらないからです。

解決策としては、コンポーネント用のSCSSファイル名の末尾を .module.scss に変え、ruleを定義するときに書く test パラメータを .module があるかないかで使用するローダーを切り替えます。

npm i -D mini-css-extract-plugin
import path                    from "path";
import { TsconfigPathsPlugin } from "tsconfig-paths-webpack-plugin";
import MiniCssExtractPlugin    from "mini-css-extract-plugin";
import webpack                 from "webpack";

export default {
    entry: [
        "./src/ts/index.ts",
        "./src/scss/index.scss", // グローバルスタイルエントリーファイル
    ],
    output: {
        filename: "index.js",
        clean: true,
        library: {
            name: "my-lib",
            type: "umd",
            umdNamedDefine: true,
        },
    },
    resolve: {
        modules: [
            __dirname,
            path.resolve(__dirname, "node_modules"),
        ],
        extensions: [".wasm", ".ts", ".tsx", ".mjs", ".cjs", ".js", ".json"],
        plugins: [],
    },
    module: {
        rules: [
            {
                test: /\.html$/i,
                use: "html-loader",
            },
            {
                test: /\.tsx?$/,
                use: "ts-loader",
                exclude: /node_modules/,
            },
            {
                test: /(?<!\.module)\.s[ac]ss$/i, //グローバルスタイル
                use: [
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                    {
                        loader: "sass-loader",
                        options: {
                            sassOptions: {
                                outputStyle: "compressed",
                            },
                        },
                    },
                ],
                sideEffects: true,
            },
            {
                test: /\.module\.s[ac]ss$/i, // コンポーネント用スタイル
                use: [
                    {
                        loader: "sass-loader",
                        options: {
                            sassOptions: {
                                outputStyle: "compressed",
                            }
                        }
                    }
                ],
                type: 'asset/source',
            }
        ],
    },
    optimization: {},
    plugins: [
        new MiniCssExtractPlugin({
            filename: "style.css",
        }),
    ],
};

これで、ファイル名の末尾が.module.scssの場合はコンポーネント用のスタイルとして読み込まれてJavaScriptファイルにインライン要素として出力され、.scssの場合はグローバルSCSSとしてCSSファイルとして出力されます。

まとめ

本記事では、Web Componentを開発する際のやりづらいポイントを紹介し、それらをwebpackを使用して解消しました。

webpackは機能が多く、自分も環境構築の際は知らなかったことが多くて苦労しましたが、webpackの機能をよく知れたいい機会だったなと思います。

本記事が読者の皆様の参考になれば幸いです。

Hirayama

Hirayama / Engineer

1997年生まれ、南伊豆出身。学生時代にC#で画像処理アプリケーションを作ったりしていました。業務では主にLaravelを使用してサーバーサイドのプログラミングをしています。趣味はドライブとシミュレーションゲーム。