Sassyブログ

好きなことで暮らしを豊かにするブログ

Djangoのフロントエンド環境をWebpackを使ってより良くしてみた

最初に

私自身Webpack使いでもなく、仕事でWebpackを使い倒すということはあまりしてこなく修正程度を行うくらいでしか触ったことがありません。 なのでもっと良い書き方があるかもしれませんが、ネットの情報を見てもDjango+Webpackの情報だとWebpackのバージョンが古かったりしたので、それらの情報を元に試行錯誤して現状最新のWebpackでフロントエンド環境の分離を実施してみました。

そのため備忘録的な感じでやり方を残しておこうと思います。

【注意】 ただしまだ開発ビルドしか作成できてません。

経緯

・SCSS使いたかった ・SPA化まではいかないけど、Djangoでのフロントエンド環境との分離をどのようにすればよいかの知見を増やしたかった

環境

必要なnpmライブラリ * webpack: 5.44.0 * webpack-bundle-tracker : 1.0.0 * webpack-cli: 4.7.2 * webpack-dev-server : 3.11.2 * webpack-merge : 5.8.0 * copy-webpack-plugin : 9.0.1 * css-loader : 5.2.6 * file-loader : 6.2.0 * mini-css-extract-plugin : 2.1.0 * rimraf : 3.0.2 * sass : 1.35.2 * sass-loader : 12.1.0 * style-loader : 3.0.0

django-webpack-loaderの導入

以下のコマンドを叩いてdjango-webpack-loaderをインストールします。

※執筆時点では1.3.0がリリースされていたのでそちらを使用しても問題ないかと思います。

$ pip install django-webpack-loader==1.1.0

Django側の設定

settings.pyに以下を追加します

WEBPACK_LOADER = {
    'DEFAULT': {
        'CACHE': False,
        'BUNDLE_DIR_NAME': '',
        'STATS_FILE': os.path.join(BASE_DIR, "frontend", "webpack-stats.json"),
    }
}

Webpackの導入

まずはDjangoのプロジェクトルートにfrontend用のディレクトリを作成します。

私は、そのまま「frontend」と付けて作成しました。

その後frontend直下に移動して以下のコマンドを叩きpackage.jsonとnode_modulesを作成しました。

$ yarn init

その後Djangoプロジェクト直下に作成していた「templates」と「static」ディレクトリを作成したfrontendディレクトリ内に移動しました。

ここまで行うと以下のような感じになります。

frontend
├─static
│  ├─fonts
│  ├─images
│  ├─javascripts
│  │  └─app.js
│  └─stylesheets
│      └─app.css
├─templates
│   └─app.html
│
├─node_modules
├─package.json

ここから以下のコマンドを叩いて必要なモジュールを一気にインストールしていきます。

$ yarn webpack@5.44.0 webpack-bundle-tracker@1.0.0 webpack-cli@4.7.2 webpack-dev-server@3.11.2 webpack-merge@5.8.0 copy-webpack-plugin@9.0.1 css-loader@5.2.6 file-loader@6.2.0 mini-css-extract-plugin@2.1.0 rimraf@3.0.2 sass@1.35.2 sass-loader@12.1.0 style-loader@3.0.0

Webpackの設定ファイル

frontend直下のディレクトリにwebpack.config.jsを作成して以下の内容を追加しましょう。

const path = require('path');
const { merge } = require('webpack-merge');
const BundleTracker = require('webpack-bundle-tracker');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CopyPlugin = require("copy-webpack-plugin");
const StyleLintPlugin = require('stylelint-webpack-plugin')

const baseConfig = {
    entry: {
        'app': "./static/javascripts/app.js", // エントリポイントの分追加が必要
    },
    output: {
        filename: 'js/[name].[fullhash].bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'initial',
                    name: 'vendor',
                },
            },
        },
    },
    module: {
        rules: [
            {
                test: /\.(css|scss)$/,
                use: [MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'],
            },
            {
                test: /\.(eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'fonts'
                        }
                    }
                ]
            },
            {
                test: /\.(ico|jpg|jpeg|png|gif)(\?.*)?$/,
                generator: {
                    filename: 'images/[name][ext]'
                },
                type: 'asset/resource'
            }
        ],
    },
    plugins: [
        new BundleTracker({
            path: __dirname,
            filename: 'webpack-stats.json',
        }),
        new MiniCssExtractPlugin({
            filename: 'css/[name].[fullhash].bundle.css'
        }),
        new CopyPlugin({
            patterns: [
                { from: "static/images", to: "images" },
                { from: "static/favicon.ico", to: "favicon.ico" },
                { from: "static/sitemap.xml", to: "sitemap.xml" },
            ],
        }),
        new StyleLintPlugin({
            files: ['static/stylesheets/**/*.scss'],
            syntax: 'scss',
            fix: false
        }),
    ]
};

const devConfig = merge(baseConfig, {
    mode: 'development',
    output: {
        publicPath: 'http://localhost:3000/static/',
    },
    devServer: {
        port: 3000,
        hot: true,
        headers: {
            "Access-Control-Allow-Origin": "*"
        },
        watchOptions: {
            ignored: /node_modules/
        },
    },
});

const productConfig = merge(baseConfig, {
    mode: 'production',
    output: {
        publicPath: '/static/'
    }
})

module.exports = (env, options) => {
    return options.mode === 'production' ? productConfig : devConfig
}

package.jsonのscriptプロパティ内を修正する

以下のように修正します。 「rm:dist」というコマンドを作成して、ビルド前に前回のビルド時に作成される/distフォルダを削除してからビルドを行うようにしています。 /distがない場合はそのままビルドが実行されます。

{
    ・・・省略
    "scripts": {
      "build": "yarn rm:dist && webpack --mode=production",
      "dev": "webpack serve --mode=development",
      "rm:dist": "rimraf dist",
      ・・・省略
    },
    ・・・省略
  }

ここまで終えるとビルドできるようになるので以下のコマンドを叩いて/distが出力されることを確認します。

$ yarn build

Djangoテンプレートファイルwebpack用のタグを埋め込む

最後に開発サーバー起動後に画面を表示した時にビルドされる資材を読み込むようにテンプレートファイル内にWebpack用のタグを追加していきます。

これはdjango-webpack-loaderで提供されているタグで、webpack-bundle-trackerにより自動生成されるwebpack-stats.jsonというファイルを読み込んで、

そのファイルに記載されている静的ファイルを読み込むようになっています。

app.html

{% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %}

<!DOCTYPE html>
<html lang="ja">
<head>
  ・・・省略
  {% render_bundle 'app' 'css' %}
  ・・・省略
</head>
<body>
  <header>
  ・・・省略
  </header>
  <div class="content">
  ・・・省略
  </div>
  <footer>
  ・・・省略
  </footer>
  {% render_bundle 'app' 'js' %}
</body>
</html>

最後に

Webpack5を使ってのフロントエンド分離について、Webpack関連のプラグインDjango側のモジュールがWebpack5対応しているか不安でしたが、テンプレートに追加した「{% load webpack_static from webpack_loader %}」を追加しても画像ファイルが読み込んでくれないという問題が発生していました。

ただこれはこちらの実装ミスだったのでパスを修正し直して解決できたので比較的大きな問題は発生せず構築できました。

これでCSSも書きやすく可読性も上がったのでフロントエンドの開発もしやすくなったかなと感じます。

是非Djangoを使っていてモノリス構成から一歩踏み出したい場合の参考になれれば幸いです。