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 slash forward icon Engineer

業務では主にPHPやTypeScriptを使用したバックエンドアプリケーションやデスクトップアプリケーションの開発をしています。趣味は登山。

関連記事