雑記
 

ASP.NET Core で webpack を使う

2021/1/3

この記事が対象とする製品・バージョン

VS2019 Visual Studio 2019 対象です。
VS2017 Visual Studio 2017 × 対象外です。
VS2015 Visual Studio 2015 × 対象外です。
VS2013 Visual Studio 2013 × 対象外です。
VS2012 Visual Studio 2012 × 対象外です。
VS2010 Visual Studio 2010 × 対象外です。
VS2008 Visual Studio 2008 × 対象外です。
VS2005 Visual Studio 2005 × 対象外です。
VS.NET 2003 Visual Studio 2003 × 対象外です。
VS.NET 2002 Visual Studio (2002) × 対象外です。

目次

 

概要

この記事では Visual Studio を使って、ASP.NET Core + TypeScript を使い package.json で取得したモジュールを呼び出す方法=webpackを使う方法を説明します。

Visual Studioを使っていないプログラマーにはそのまま役には立ちませんが  webpack の使い方を知ることはできると思います。

Visual Studioは npmのプロジェクトをpackage.jsonやnode_modulesなど含めて素のまま扱うからです。

 

前提

前提1.Visual Studio 2019

この記事の内容を実際に試すには、Visual Studio 2019が必要です。 この記事の内容は Visual Studio 2019 (16.8.3)で確認しています。

 

前提2.Node.js がインストールされている

Node.js がインストールされているかは、PowerShellまたはコマンドプロンプトで Node --version と入力して、バージョンが表示されるかどうかでわかります。

Node.js をインストールするには、https://nodejs.org/ja/ から、インストーラーをダウンロードして次へ次へとするだけです。Visual Studioを起動している場合、Node.jsをインストールした後Visual Studioを再起動してください。

 

手順1.TypeScript のプログラムを作成

 

手順1-1.新しいプロジェクトの作成 > ASP.NET Core Webアプリケーション > 「次へ」ボタンクリック

ASP.NET Core Web アプリケーションを新規作成

 

手順1-2.プロジェクト名に「ASPNETCoreWabpack」を入力し、「作成」ボタンクリック

プロジェクト名は重要ではありません。プロジェクト名によって何かの設定が変わるということはありません。

 

手順1-3..NET Core ASP.NET Core 3.1 と Web アプリケーション を選択し、「作成」ボタンクリック

ASP.NET Core 5.0 を選択しても問題ありません。

他はデフォルトのままです。

 

作成が完了した初期状態ではソリューションエクスプローラーは次のようになっています。

初期状態のソリューションエクスプローラー

 

手順1-4.app.ts を追加

ソリューションエクスプローラーでプロジェクト(この記事では ASPNETCoreWebpack)を右クリックして、[追加] - [新しい項目]「TypeScript ファイル」を選択します。名前に app.ts と入力して「追加」します。

スクリプトのファイル名は何でもよいのですが、後の手順でこの名前を使うことになりますので、ためしにやってみる場合は同じ名前にしておくのが無難です。

TypeScriptファイルを追加すると、Visual Studioの上側に Microsoft.TypeScript.MSBuild をインストールするようにメッセージがでるので、「今すぐインストール」をクリックします。

 

後でうまくいっているか確認するために、ボタンをクリックしたら画面に固定の文字を表示するようにプログラムしてみます。

app.ts に次の通り記述して保存します。

function Test() {
    document.getElementById("message").innerHTML = "Hello, TypeScript!"
}

 

 

手順1-5.tsconfig.json を追加

同様にプロジェクトを右クリックして[追加] - [新しい項目] で 「TypeScript JSON 構成ファイル」を選択し、デフォルトの名前 tsconfig.json のまま「追加」ボタンをクリックします。

 

追加された tsconfig.json に次のように記述して保存します。

ほとんどの内容はデフォルトで入力されているので、追加するのは赤くした2行(とカンマ2つ)だけです。ファイルの内容を下記に丸ごと置き換えてもOKです。

{
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": true,
    "target": "es5",
    "outDir": "wwwroot/js"
  },
  "exclude": [
    "node_modules",
    "wwwroot"
  ],
  "files": ["app.ts"]
}

これで、app.ts がコンパイルの対象として認識されます。コンパイルされて生成されるJavaScript は outDir で指定した wwwroot/js フォルダーに格納されます。

 

手順1-6.画面の準備

Pagesフォルダーにある Index.cshtml に次のように記述して保存します。

ほとんどの内容はデフォルトで入力されているので、追加するのは赤くした2行だけです。

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
    <div><span id="message" style="font-size:2em"></span></div>
    <input type="button" value="Test" onclick="Test()" />
</div>

 

さらに Pages\Sharedフォルダーにある _Layout.cshtml に次のように記述して保存します。

このファイルは少し長いので下記は抜粋です。@RenderSectionの上に1行追加するだけです。

・・・省略・・・
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>

<script src="~/js/app.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
・・・省略・・・

 

手順1-7.実行

準備が整ったのでVisual Studioの実行ボタンを押してデバッグを開始します。

うまくいくとこのような画面になります。Testボタンをクリックすると、Hello, TypeScript! と表示されます。

 

動作することを確認したら実行を終了します。

ソリューションエクスプローラーを確認すると wwwroot\jsフォルダーに app.js が生成されているのがわかります。

 

 

手順2.webpack の導入

 

手順2-1.app.tsの機能を外部に公開する

ここから webpack を使って、依存するパッケージも1つのJavaScriptファイルに含めて生成できるようにします。この処理を「バンドル」(bundle)と呼びます。英語のbundleの意味は「包む」、「束にする」などの意味です。

手順1で作成したプログラムはTypeScriptを1つ追加しただけなので、バンドルする意味はありません。手順2で実現される実行結果は手順1と同じです。手順2ではwebpackを追加して問題ないか確認したいだけです。ただし、手順1と区別するために生成されるJavaScriptのファイル名は「app-bundle.js」に変更します。そのやり方はすぐ後の手順で紹介します。

 

まず、プログラムを少し修正する必要があります。

app.tsに次のように1行追加します

function Test() {
    document.getElementById("message").innerHTML = "Hello, TypeScript!"
}

(window as any).Test = Test;

webpackでバンドルすると、結果として出力されるJavaScriptは無名の関数の中にラッピングされるので、外部から呼び出したければこのように積極的に公開する必要があります。

手順1で作成したアプリケーションはbuttonの onclick から Test を呼び出しているので、このようにTestを公開する必要があります。

※なお、HTMLのonclickを使用せずにTypeScript内でイベントハンドルを記述すれば、外から呼び出されるわけではなくなるので、このように機能を公開する必要はありません。そのほうが修正箇所が集約されてわかりやすいので本格的なアプリケーションを作成する際にはお勧めです。

 

手順2-2.package.json を追加

ソリューションエクスプローラーでプロジェクトを右クリックして[追加] - [新しい項目] で 「npm 構成ファイル」を選択し、デフォルトの名前 package.json のまま「追加」ボタンをクリックします。

 

追加された package.json に以下のように記述して保存します。

"dependencies"の前にカンマ , を追加するのを忘れないように気をつけてください。

{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
  },
  "dependencies": {
    "ts-loader": "~7.0.1",
    "typescript": "~3.8.3",
    "webpack": "~4.42.1",
    "webpack-cli": "~3.3.11"
  }
}

 

ソリューションエクスプローラーで package.json を右クリックして[パッケージの復元]をクリックします。

この操作でpackage.jsonに記述した4つのパッケージts-loader, typescript, webpack, webpack-cli がダウンロードされます。この機能は Visual Studio が npm を呼び出すことで実現されています。

この処理が完了するまで数分かかります。

ソリューションには node_modules というフォルダーが追加され、ここに追加したパッケージが保存されます。

処理中のフォルダーを開くとエラーのように見えるときがあります。完了するとVisual Studioの左下に「パッケージのインストールが完了しました」と表示されます。

ソリューションエクスプローラーの node_modules フォルダーには大量のサブフォルダーが作成されます。

 

手順2-3.webpack-config.js を追加

ソリューションエクスプローラーでプロジェクトを右クリックして[追加] - [新しい項目] で 「JavaScript ファイル」を選択し、名前に webpack-config.js と入力して「追加」ボタンをクリックします。

 

追加された webpack-config.js に次の通り記述します。

module.exports = {
    devtool: 'source-map',
    entry: "./app.ts",
    mode: "development",
    output: {
        filename: "../wwwroot/js/app-bundle.js"
    },
    resolve: {
        extensions: ['.webpack.js', '.web.js', '.ts', '.js', '.jsx', '.tsx']
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'ts-loader'
                }
            }
        ]
    }
}

webpack-config.js は webpack がどのようにJavaScriptをまとめるかを定義します。この設定でwebpack を実行すると app.ts とその依存しているパッケージが1つのJavaScriptにまとめられ wwwroot/jsフォルダー以下にapp-bundle.js というファイルになります。

 

手順2-4.webpackの実行

ソリューションエクスプローラーでプロジェクト(この例ではASPNETCoreWebpack)を右クリックして、[Open in Terminal](ターミナルで開く)をクリックします。

ターミナルが開いたらタイトルが「開発者用 PowerShell」となっていることを確認し、次のコマンドを実行します。

node_modules/.bin/webpack-cli app.ts --config webpack-config.js

※PowerShellではなくコマンドプロンプトでやる場合は call node_modules/.bin/webpack-cli app.ts --config webpack-config.js とします。また、カレントディレクトリがプロジェクトフォルダーである必要があります。dir を実行して node_modules フォルダーが見えなければカレントディレクトリーが違います。

この画像のように表示されれば成功です。何かエラーが表示される場合はそれを解決する必要があります。package.json や webpack-config.js の設定内容が正しいかも確認してみてください。

メモ メモ  - node.exe が見つからないというエラー

前提に書いてあるように Node.js がインストールされていない次のようなエラーが表示される場合があります。

& : The term 'node.exe' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

このエラーが出る場合は Node.js をインストールしてください。https://nodejs.org/ja/ から、インストーラーをダウンロードして次へ次へとするだけです。Node.jsをインストールした後Visual Studioを再起動してください。

 

成功した場合、ソリューションエクスプローラーで、wwwroot\jsの下に app-bundle.js が生成されていることを確認できます。

 

 

手順2-5.app.js から app-bundle.js への差し替え

ここまでの手順で webpack を使って app.ts を JavaScript にコンパイル(トランスパイル)して、バンドルすることができるようになりました。

app.js はもういらないので消してしまいましょう。

ソリューションエクスプローラーで app.js を右クリックして[削除]してください。

 

それから、tsconfig.json でapp.tsをコンパイル(トランスパイル)する設定は不要なので消してしまいましょう。これがあると実行時に再び app.js が作成されてしまいます。

tsconfig.jsonの

"outDir" の前に // を追記してコメントにしてください。"outDir"の前(たいていは上の行の最後)にあるカンマも削除してください。

"files"の前にも // を追記してコメントにしてください。"file"の前(たいていは上の行の最後)にあるカンマも削除してください。

{
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": true,
    "target": "es5"
    //"outDir": "wwwroot/js"
  },
  "exclude": [
    "node_modules",
    "wwwroot"
  ],
  //"files": ["app.ts"]
}

※webpackはコンパイル(トランスパイル)時にtsconfig.jsonの設定を使用するので、tsconfig.jsonは引き続き必要です。しかし、コンパイル(トランスパイル)する対象がapp.ts で生成するJavaScriptのファイル名がapp-bundle.jsonであるという指定はwebpack側(Webpack-config.js)で行っているので、tsconfig.jsonでは不要になります。

 

そして、app.js の代わりに app-bundle.js を使用するように、Pages\Sharedフォルダーにある _Layout.cshtml 内に記述したscriptタグの app.js を app-bundle.js に変更しましょう。

・・・省略・・・
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>

<script src="~/js/app-bundle.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
・・・省略・・・

 

手順2-6.実行

これで実行すると、さきほどと同じようにボタンをクリックすることで Hello, TypeScript! と表示されます。

機能はさきほどと同じですが、webpack で処理されたJavaScriptを使っているのが最初との違いです。

 

手順3.複数のパッケージを使う

手順3-1.fortune-tweetable パッケージの組み込み

それでは、webpack の本領を発揮するために別のパッケージを呼び出すようにプログラムを改修してみましょう。

今回はランダムに格言を表示する fortune-tweetable というパッケージを使って、画面に表示される文字をランダムにしてみます。

 package.json に以下のように記述して保存します。

追加するのは赤い字で書いた fortune-tweetableの行だけです。

{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
  },
  "dependencies": {
    "ts-loader": "~7.0.1",
    "typescript": "~3.8.3",
    "webpack": "~4.42.1",
    "webpack-cli": "~3.3.11"
    "fortune-tweetable": "1.0.0"

  }
}

 

ソリューションエクスプローラーでpackage.jsonを右クリックして[パッケージの復元]をします。

この操作で、node_modulesフォルダーにfortune-tweetableがダウンロードされ使用可能な状態になります。

 

手順3-2.格言を表示するようにプログラム改修

次に app.ts を次のように修正して、固定の「Hello, TypeScript!」という文字ではなく、fortune-tweetableが生成するランダムな格言を表示するようにします。

app.tsに次のように変更して保存します

import * as Fortune from 'fortune-tweetable'

function Test() {
    document.getElementById("message").innerHTML = Fortune.fortune()
}

(window as any).Test = Test;

fortune-tweetable は fortune という関数を公開しており、この関数はランダムに格言を返します。

このプログラムでは、import文を使って Fortune という名前で fortune-tweetable を参照できるようにして fortune関数を呼び出します。

 

手順3-3.webpack 再実行

ソースコードが変わったのでwebpackを再実行して app-bundle.jsを再作成します。

そのためにソリューションエクスプローラーでプロジェクト(この例ではASPNETCoreWebpack)を右クリックして、[Open in Terminal](ターミナルで開く)をクリックします。

ターミナルが開いたらタイトルが「開発者用 PowerShell」となっていることを確認し、次のコマンドを実行します。

node_modules/.bin/webpack-cli app.ts --config webpack-config.js

 

app.ts 内に import で fortune-tweetable を使用することが明示されるため、webpackコマンドを実行すると はfortune-tweetableも含めて1つの app-bundle.js にまとめてくれます。

 

手順3-4.実行

これで、Visual Studioでアプリケーションを実行すると、今度はボタンをクリックするたびに違った格言が表示されるようになります。

 

 

 

おまけ

ビルド時にwebpackコマンドを自動実行する方法

今回紹介したやり方だと、app.tsを修正するたびに自分で node_modules/.bin/webpack-cli app.ts --config webpack-config.js を実行して、app-bundle.js を生成する必要があります。

Visual Studioではビルド時にコマンドを自動実行させることができるので、この手間を省くことができます。

それには、ソリューションエクスプローラーでプロジェクトを右クリックして、ビルド前イベントコマンドラインに、このコマンドを登録しておきます。

ビルド前イベントでコマンドを自動実行させる

これ以降、Visual Studio でデバッグを開始したり、ビルドするたびにこのコマンドが自動実行されるので自分でコマンドを実行する必要がなくなります。

 

生成されるJavaScriptをミニファイする方法

webpack-config.js で mode を "development" から "production" に変更すると生成されるJavaScriptがミニファイされます。

なお、今回例で使用した fortune-tweetable は格言集のサイズが1MB程度あるためソースコードの単純さのわりにはjsのサイズが大きくなります。文字列にはミニファイの効果がないため、ミニファイしても1MB以上になります。

 

 

Uncaught ReferenceError: exports is not defined

私は当初このエラーに苦しめられたので、検索でたどり着いた人のために少しわかったことを書いておきます。

私もあまり詳しくないのと、完全に調べきれないところがありますから間違っていたらごめんなさいレベルです。

 

どうすればよいのか、結論から言うと 「webpack を使う」が正解です。

といわれても、どうやって使うのかわからないと思うのでこの記事を読んでいただければと思います。

マイクロソフトの公式ドキュメントにも次の記述を発見しました。

「TypeScript では、ES2015 モジュール (つまり、import と export ステートメント) を、ブラウザーで読み込む最終的な 1 つの .js ファイルに変換できません。 ここでも WebPack が最善の選択肢となります。」

Node.js を使用して Vue.js アプリを作成する - Visual Studio | Microsoft Docs

 

メモ Note  -  In English

Uncaught ReferenceError: exports is not defined

In conclusion, all you have to do is "use webpack".

OK. I know. You may not familiar with the webpack.  So I have written this article. (This article is written with Japanese, But I hope its helful for you.)

I also found following sentence in Microsoft Docs.

"TypeScript doesn't know how to convert ES2015 modules (that is, import and export statements) into a single final .js file to load in the browser. Again, WebPack is the best choice here."

Create a Vue.js app using Node.js - Visual Studio | Microsoft Docs

 

背景

TypeScriptが記述されているファイル内に import または export があると、そのファイルは モジュールモード になります。(出典:オライリー)

モジュールモードのTypeScriptは、JavaScriptにトランスパイルすると次のように、exports を含んだコードが追加されます。(後述しますが設定によってはそうならない場合もあります。)

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var Fortune = require("fortune-tweetable");
function Test() {
    alert(Fortune.fortune());
}

 

この exports というのは commonJS(≒Node.js)で、モジュールを扱う機能です。(出典:Node.js の moduleのAPI)

commonJSというのは、JavaScriptをブラウザーの外でも動くようにした仕様であり、ブラウザーの中で動くJavaScriptの仕様とは異なります。(出典:Wikipedia)

一方、ブラウザーの中で動くJavaScriptの仕様は ECMAScript です。commonJS と ECMAScript はどちらもJavaScriptの仕様なので基本的な部分は一緒なのですが、モジュールを扱う機能には違いがあります。exportsはECMAScriptでは使えないのです。

ECMAScriptのモジュール機能はimportとexportです(exportの後ろにsがないことに注意。似ていますがexportsとは別物です。)。(出典:MDN export)

というわけで、commonJSの仕様で生成されたJavaScriptをブラウザー上で実行すると exports というものは定義されていないという意味で exports is not defined となるわけです。

自分で var exports = {} などと定義するというかわし方はあるようですが、これでかわしても次の行にある require でまたエラーになります。これも commonJS にしかない仕様です。そう考えると、commonJSのことをよく知らないで場当たり的にかわしていくと何が起こるか心配なので、場当たり的なかわし方は止めたほうがよいということになります。

webpack はうまいことcommonJSのモジュール機能をエミュレートして、ブラウザー上で ECMAScript として実行できるようにしてくれるので、webpack を使うのが最善というわけです。(出典なし。この部分は私の推測です)

 

ECMAScriptのモジュール機能(importとexport)は、commonJSに遅れをとって、第6版で定義されました。ECMAScriptの第6版は通称 ES6 と言います。(出典:Wikipedia)

TypeScriptをJavaScriptにトランスパイルするときにどのバージョンのJavaScriptにトランスパイルするのか、tsconfig.json の target で指定できます。(出典:tsconfig.jsonのリファレンス target)

この記事で紹介しているサンプルでは es5 が指定されています。つまり ECMAScript 第5版 です。このバージョンにはモジュール機能がないため、モジュールモードのTypeScriptをトランスパイルするとcommonJS仕様になってしまうようです。(出典なし。この部分は私の推測です)

ES6以上を指定すると、ECMAScriptの仕様でモジュール機能をトランスパイルしてくれます。(tsconfig.jsonでmoduleResolution を Node にする必要もあります。)

たとえば、このようになります。

import * as Fortune from 'fortune-tweetable';
function Test() {
    alert(Fortune.fortune());
}

最近(2020年現在)のブラウザーはこのプログラムも実行できます。(出典:MDN export ブラウザーの互換性 )

このまま実行すると Uncaught SyntaxError: Cannot use import statement outside a module というエラーになりますが、scriptタグに type="module" を付ければこのエラーは解決できます。

<script src="~/js/app.js" type="module" ></script>

 

ならば webpack など使わなくてもいいじゃないかと思われるかもしれませんが、この先に壁があります。

次のエラーになります。

Uncaught TypeError: Failed to resolve module specifier "fortune-tweetable". Relative references must start with either "/", "./", or "../".

このエラー自体は相対パスについての指摘ですが、要点は、import しようとしている fortune-tweetable がないじゃないかということです。

※この場合のimportの指定の仕方がまた悩ましいのですが、ここで悩んでも何も解決しないので割愛します。

たしかにたいていの場合、使用しているパッケージはクライアント側には存在しません。それなら、使用しているパッケージもクライアント側に公開すれば良いと思うかもしれません。でも、このパッケージはpackage.jsonを使って取得しているものですよね。package.jsonは npm が使う設定ファイルですよね。 npm は Node.js のパッケージ管理ツールですね。そう、多分、このソースコードはブラウザーでは動作しないんです(ECMAScriptではないということです)。ためしにforutune-tweetableのソースコードを見てみましたが、ソースコードの中で exports や require を使っています。

それにもう1つ問題があります。大きなパッケージは、さらに別のパッケージに依存していることがよくあります。だから、1つのパッケージをクライアントにダウンロードするためには依存するすべてのパッケージを適切にダウンロードさせる必要があります。

ここでマイクロソフトの公式ドキュメントの記述をもう一度引用します。ES2015とは、ES6の別名です。

「TypeScript では、ES2015 モジュール (つまり、import と export ステートメント) を、ブラウザーで読み込む最終的な 1 つの .js ファイルに変換できません。 ここでも WebPack が最善の選択肢となります。」

webpackを使わないで自力で頑張るのは茨の道ということです。

 

 

参考

 

TypeScript を使用した ASP.NET Core アプリの作成 - Visual Studio | Microsoft Docs

マイクロソフト公式。わかりやすいです。

プロジェクトの作成から、TypeScriptの使用、npmパッケージの追加まで扱っていますが、追加するパッケージが既にクライアント側に含まれているjQueryなのでwebpackでのバンドルが不要で、webpackのような仕組みは説明されていません。

 

Node.js と React のアプリを作成する - Visual Studio | Microsoft Docs

こちらもマイクロソフト公式。わかりやすいです。

jsx や webpack も含めてプロジェクトの作成から解説してくれています。ただし、ASP.NET Core ではなく、Node.js Webアプリケーションを扱っています。

 

【TypeScript】モジュール分割で起きたエラーを調べる(CommonJSが原因) - クモのようにコツコツと (i-ryo.com)

私はこのサイトがとても参考になりました。正解を解説してくれているわけではなく試行錯誤の過程が、どのように考えたのかを交えて説明してあり、腹落ちしやすかったです。

私は当初 webpack を使わないでもっとお手軽に ASP.NET Core で npmパッケージを扱う方法がないか調べており、このサイトで解説されていることと同じようなことを考え、同じような失敗をしていました。