LoginSignup
9
5

More than 5 years have passed since last update.

Angular CLI 使わず手作業で Angular v5 SPA を webpack でビルドする環境を育てていったときに遭遇したトラブル x 9件の記録

Last updated at Posted at 2018-01-10

長いタイトルですみません。

では早速。

トラブル 1 - "Error: Can't resolve all parameters for {*Component}: (?)."

現象

まずは最小構成で、下記くらいの webpack.config.js を記述し、"Hello World" が動くところまで実装。

module.exports = {
    entry: ['./src/bootstrap.ts'],
    output: {
        filename: './js/bundle.js'
    },
    resolve: { extensions: ['.js', '.ts'] },
    module: {
        loaders: [
            { test: /\.ts$/, use: ['awesome-typescript-loader'] },
            { test: /\.html$/, loader: 'html-loader?minimize=false' },
            { test: /\.css$/, loaders: ['style-loader', 'css-loader'] },
        ]
    },
    devtool: 'source-map'
};

ここまではうまくいったので、続けて、とあるコンポーネントにてサーバー側 Web API を呼び出す処理を追加することに。
そこで HttpClinet サービスを利用すべく、コンストラクタで DI によるサービス注入を受け取るよう下記のように追記したところ、

import { Component } from '@angular/core'
import { HttpClient } from '@angular/common/http' // <- ここと、

@Component({
    selector: 'app-root',
    templateUrl: '/src/components/app.component/app.component.html'
})
export class AppComponent {

    constructor(
        private http: HttpClient // <- ここを追記
    ) {
    }

実行時に例外が発生して、Webブラウザの開発者コンソールに下記メッセージが表示されていました。

Error: Can't resolve all parameters for AppComponent: (?).

すなわち、DI 機構において、AppComponent クラスのインスタンス生成時に、コンストラクタ引数に何渡せばいいかわかんねー、ということのようです。

ちなみに、コンストラクタ引数に明示的に Inject デコレータを付けた場合は正常動作しました。

import { Component, Inject } from '@angular/core'
                 // ~~~~~~
                 // ↑ Inject も import して、
    ...

    constructor(
        @Inject(HttpClient) private http: HttpClient
     // ~~~~~~~~~~~~~~~~~~~
     // ↑ これ書き足したら正常動作した
    ) {
    }

あと、もちろん、tsconfig.json では、experimentalDecoratorsemitDecoratorMetadata をいずれも true に設定するのも忘れてはいませんでした。

{
  "compilerOptions": {
    ...
    /* Experimental Options */
    "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
  }
}

原因

せっかくデコレータのメタデータ生成を On にして TypeScript コンパイルしているのに、そのメタデータを読み込むモジュール = reflect-metadata を、(パッケージは npm install はしていましたが)、ロードしていなかったのが原因でした。

bootstrap.ts に reflect-metadata を import する1行を書き加えて解決しました。

import 'reflect-metadata'; // <- この行を追加
import 'zone.js';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app.module'

const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule);

トラブル 2 - "ERROR in [at-loader] {filename}.ts:{row}:{col} TS2304: Cannot find name 'require'"

現象

次は、angular-template-loader を導入し、component の template も bundle することにしてみました。

すると残念なことに、ビルド時に ERROR in [at-loader] ./src/components/app/app.component.ts:6:15 TS2304: Cannot find name 'require'. が発生してしまいました。

解決

angular-template-loader はモジュール読み込みの方式を require 文で生成するのでしょうか、しかしこの環境における TypeScript では require 構文はありません。

そこで、npm install @types/node して、require 構文に対する型情報を追加したところ、ビルドは成功するようになりました。

ちなみに、npm install @types/webpack-env でも解決します。

トラブル 3 - "Error: Expected 'styles' to be an array of strings."

現象

次に、コンポーネントクラスの @Component デコレータにて、styleUrls 指定を追加してスタイルシートの導入に着手しました。

先に angular2-template-loader は導入済みですから、CSS もバンドルされるはずです。

ところが、ビルドは難なく成功するものの、ブラウザで読み込んで実行したところ、実行時例外が発生して正常動作しません。

開発者コンソールには下記メッセージが表示されていました。

Error: Expected 'styles' to be an array of strings.

解決

webpack.confg.js にて、CSS の読み込みにあたり、to-string-loader をかませていなかったせいであるようです。

npm install -D to-string-loader してパッケージをインストールし、続けて webpack.config.js にて、CSS のバンドル時に to-string-loader を挟むようにしました。

module.exports = {
  ...
  module: {
    loaders: [
      ...
      { test: /\.css$/, loaders: ['to-string-loader', 'style-loader', 'css-loader'] }
                                // ~~~~~~~~~~~~~~~~
                                // ↑ これを追加
    ]
  },
  ...
};

これでビルドしなおしたところ、指定したスタイルシートも適用されて正常動作するようになりました。

トラブル 4 - "Error: Template parse errors: Can't bind to 'ngModel' since it isn't a known property of 'mat-{selector}'."

現象

次に、Angular Material を導入しました。

mat-toolbarmat-card などを使ってざっくりレイアウトも作ることができました。

続けてコンポーネントのプロパティを、Angular Material によるラジオボタンにバインドしてみました。

<mat-radio-group [(ngModel)]="foo">
   ...
</mat-radio-group>

ところがここで、実行時例外が発生するようになりました。

開発者コンソールには下記メッセージが表示されていました。

Error: Template parse errors: Can't bind to 'ngModel' since it isn't a known property of 'mat-radio-group'.

解決

ググってみたところ、Angular の FormsModule をインポートしていなかったのが原因のようでした。

参考: angular/material2: Issue #1335

上記 Issue に書かれていた情報を参考に、下記要領で FormsModule を組み込んだところ、正常動作するようになりました。

// app.module.ts
...
import { FormsModule } from '@angular/forms'; // <- この行と、
...
@NgModule({
    ...
    imports: [
        ...
        FormsModule, // <- この行を追加
        ...
    ],
    ...
})
export class AppModule {
}

トラブル 5 - webpack-dev-server を導入するも、変更が反映されない

そろそろ、コードを変更してはブラウザ上で Ctrl + R して読み込みなおすのがだるくなってきましたので、まずは webpack-dev-server を導入するところまで進めてみました。

まずは webpack-dev-server を npm install し、続けて webpack.config.js 中に devServer セクションを追記しました。

module.exports = {
  ...
  output: {
    filename: './js/bundle.js'
  },
  ...
  devServer: {        // <- ここから3行を追記
    publicPath: '/js' // <- 特にここはバンドルファイルの配置パスとなるよう注意
  }
};

この状態で webpack-dev-server を実行すると無事起動しました。

ところが、続けてコードを変更しても、変更内容が反映されません。
ブラウザ上で Ctrl + Shift + R しても、相変わらず、最後にビルドしてファイルシステム上に保存された bundle.js が読み込まれてしまいます。

解決

どうやら、webpack.config.js の構成中、output.filename に "./js/bundle.js" と指定しているだけではだめだったようです。

  • output.path に絶対パスでバンドル出力先フォルダを指定、
  • output.filename にはフォルダ名を除いたバンドルファイル名本体部分 + 拡張子のみ記載

というように、フォルダ名とファイル名とを分けて記述することで、期待どおりコード変更が反映されるようになりました。

const path = require('path'); // <- これを追加して、
module.exports = {
  ...
  output: {
    path: path.join(__dirname, 'js'), // <- バンドル出力先フォルダの指定を追加し、
    filename: 'bundle.js' // <- ここにはフォルダ名は含めないように変更
  },
  ...
};

なお他にも、devServer.publicPath の指定も、ルートを示すスラッシュから始まるフォルダ指定 ('/js') を指定しないと、コード変更が反映されない現象になりました。

トラブル 6 - "TS2339: Property 'hot' does not exist on type 'NodeModule'"

現象

ここまでで webpack-dev-server が導入できたので、次は Hot Module Replacement (HMR) を有効化して、さらにコード変更からのブラウザ上の更新を快適にすることにしました。

まず webpack.config.js に加筆して webpack 側での Hot Module Replacement 有効化を実施。

続けてコード側では、bootstrap.ts にて Hot Module Replacement 対応のコードを書き加えました。

import 'reflect-metadata';
...

// ↓ このブロックを追加
if (module.hot) {
  module.hot.accept();
}

const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule);

これで webpack-dev-server を起動したところ、残念なことに TypeScript コンパイルで下記エラーが発生してしまいました。

TS2339: Property 'hot' does not exist on type 'NodeModule'

if (module.hot) {... としている箇所で、変数 module の型 NodeModule には、hot なんていう属性は存在しないよ、ということだそうです。

たしかに、この hot 属性は、webpack の Hot Module Replacement 機構によって生えるもので、通常の Node.js 実行環境には存在しないことと思いました。

解決

参照する型定義を @types/node ではなく、@types/webpack-env に変更することで解決しました。

型定義 @types/webpack-env であれば、この webpack 環境独自の hot 属性についても型情報を提供してくれているので、TypeScript コンパイルが通るようになるようです。

トラブル 7 - HMR が効かず、常にページ全体のリロードになってしまう

現象

先の手順で TypeScript コンパイルも通るようになり、無事 webpack-dev-server も起動したので、これでいよいよ Hot Module Replacement (HMR) が有効になったと思いきや、なんと、コード変更すると (変更モジュールのみの差し替えではなく) ページ全体のリロードになってしまいます。

解決

webpack.config.js の設定がよくなかったようです。

最初に webpack-dev-server を導入した際、webpack.config.js にて、バンドルファイルのマッピング先フォルダ指定 (publicPath) を、devServer ノードに記述していたのですが、

...
module.exports = {
  ...
  output: {
    path: path.join(__dirname, 'js'),
    filename: 'bundle.js'
  },
  ...
  devServer: {
    publicPath: '/js' // <- ここのこと
  }
};

Hot Module Replacement を有効にするには上記ではなく、output ノードのほうに publicPath を指定しないとならないようでした。

...
module.exports = {
  ...
  output: {
    publicPath: '/js' // <- こっちに追加
    path: path.join(__dirname, 'js'),
    filename: 'bundle.js'
  },
  ...
  devServer: {
    // publicPath: '/js' <- ここは削除
  }
};

このように webpack.config.js を変更したところ、ちゃんと Hot Module Replacement が機能するようになりました。

ちなみに、Hot Module Replacement を有効化する前の構成に戻して、ただし publicPath の指定は devServer ではなく output の側に指定して、改めて webpack-dev-server を起動したところ、ちゃんと webpack-dev-server によって HMR なしではありますがコード変更にともなう自動更新が行われました。

HMR の有効有無に関係なく、webpack-dev-server でホストするときは、publicPath の指定は devServer ではなく output の側に指定するのがよさそうです。

トラブル 8 - webpack コマンドの --env オプションの使い方がよくわからない

現象

ここで、そろそろプロダクションモードでのビルドをできるようにしておこうと思い至りました。

Visual Studio 2017 のプロジェクトテンプレートから Angular SPA プロジェクトを作成した際に生成される webpack.config.js を参考にして、webpack.config.js を変更しました。

すなわち、webpack.config.js において、構成オブジェクトを直にエクスポートするのではなく、「構成オブジェクトを返す関数」をエクスポートするようにします。

ここでその「構成オブジェクトを返す関数」は、1つのオブジェクトを引数にとるよう実装すると、その引数にプロダクションモードでビルドすべきか否かを判定する情報を引き渡せるようです。

...
// 構成オブジェクトを直にではなく、「構成オブジェクトを返す関数」をエクスポートするよう変更。
// 「構成オブジェクトを返す関数」はオブジェクトをひとつ引数にとる。
module.exports = (env) => {
  const isDevBuild = !(env && env.prod);
  ... // ここで isDevBuild の真偽に応じてプロダクションモード時のビルド構成を仕分け、
      // 最終的に構築した webpack 構成オブジェクトを返すようにする。
}

と、ここまではよかったのですが、では、この、「構成オブジェクトを返す関数」の引数に渡されるオブジェクトにおいて、どのように webpack コマンドを実行すれば .prod 属性を true とすることができるのか、行き詰りました。

webpack コマンドの実行時、--env オプションをコマンドライン引数に追加することで何かできるらしいことはわかったのですが、webpack -h で簡易ヘルプを見る程度では、

--env Environment passed to the config, when it is a function

「環境を config に渡します (関数をエクスポートしてた場合)」としか読み取れず。

憶測で webpack --env prod とかやってみましたが、プロダクションモードでのビルドになりません。

解決

手本とした、Visual Studio 2017 で作成した Angular SPA プロジェクトの、プロジェクトファイル (.csproj) の中身をエディタで閲覧し、この Visual Studio プロジェクトの MSBuild によるビルド実行時、どのように webpack を実行しているのか調べてみました。

すると、今回の webpack.config.js における正しい --env オプションの指定方法は、webpack --env prod ではなく、webpack --env.prod (--envprod の間が、空白ではなくピリオド) であることがわかりました。

トラブル 9 - サーバー側を ASP.NET Core で実装時、開発中、HMR が効かない

現象

自分はサーバー側を C# による ASP.NET Core で実装していたので、クライアント側とサーバー側との実装の統合を始めることにしました。

webpack-dev-server を使う代わりに、ASP.NET Core 側で WebpackDevMiddleware を使うことで、Hot Module Replacement も有効となった開発時 Web サーバーが立ち上がります。

ところが、ブラウザ上で下記メッセージの例外が発生してしまい、Hot Module Replacement が効きません。

can’t establish a connection to the server at http://localhost:52707/js__webpack_hmr.

Hot Module Replacement をトリガーするためのサーバープッシュ通信が開通しないようです。

解決

上記例外メッセージをよく読むと、サーバープッシュ通信先として /js__webpack_hmr というエンドポイントが表示されています。

しかし、「webpack の HMR」という趣旨のエンドポイントであれば、/__webpack_hmr というエンドポイントになりそうなものです。
なぜ、/js~ などという変なプレフィクス (?) が付いてしまったのでしょうか。

ふと気づいたのは、今回の webpack 構成ではバンドル出力先フォルダとして /js を指定しています。
もしかして、このバンドル出力先フォルダの指定が、この変なプレフィクスに化けてしまったのではないでしょうか。

そこで、webpack.config 中、output.publicPath 指定において、末尾のスラッシュが無かった ("/js" と指定してた) ところを、末尾にスラッシュをつけてみました。

...
module.exports = (env) => {
  ...
  output: {
    publicPath: '/js/' // <- ここが `/js` と末尾スラッシュなかったので末尾スラッシュ追加
    path: path.join(__dirname, 'js'),
    filename: 'bundle.js'
  },
  ...
};

予想は的中し、変更後は正しく Hot Module Replacement が機能するようになりました。

ちなみに、webpack-dev-server 用の構成でも、publicPath に末尾スラッシュを付けた設定でも正しく動作しました。
ASP.NET Core 開発との統合を踏まえた場合は、publicPath の指定には、末尾スラッシュは必ずつけたほうが無難なようです。

まとめ・感想

ng new や各種開発環境のプロジェクトテンプレートからの新規作成だと、すべて問題解決済みではあるが、内容てんこ盛りで、どの構成がどのような意味や影響があるのか、理解が難しいです。

今回、そのような形でのプロジェクト新規作成ではなく、素の npm install から始めて Angular SAP を組み立てていったところ、いろいろなトラブルに遭遇する羽目となりました。

もちろん、その原因の多くは、「○○を記載忘れてた」みたいな感じで、ng new していれば何の問題もなかったようなトラブルがほとんどです。

でもそのお陰で、webpack に対する理解が深まり、また、何かビルド時例外・実行時例外が発生しても、エラーメッセージを手掛かりにトラブルシューティングすることのできる、"トラブル耐性" が向上したのではないかと思うところです。

今後、脱 webpack の動きもあろうかと思います。
しかしながらサーバー側実装に ASP.NET Core を採用していると、WebpackDevMiddleware によるサーバー側実装との統合ができることもあり、まだ当面は webpack から離れられなさそうなので、今回得た経験はまだ活かせそうです。

2017-1-11 追記 - ASP.NET Core の Angular CLI 統合

しかしながらサーバー側実装に ASP.NET Core を採用していると、WebpackDevMiddleware によるサーバー側実装との統合ができることもあり、

などと書いておりましたが、ASP.NET Core 側も Angular CLI との統合が進んでいました。

ASP.NET の GitHub リポジトリにある、プロジェクトテンプレート中の Startup.cs など見ますと、今や下記のように UseSpaUseAngularCliServer などの新顔が登場しています。

app.UseSpa(spa =>
{
  ...
  if (env.IsDevelopment())
  {
    spa.UseAngularCliServer(npmScript: "start");
  }
});

package.json を見ても、依存パッケージから webpack はなくなって代わりに @angular/cli が追加され、scripts セクションには ng コマンドがずらっと並んでいます。

Microsoft Docs にも、この新しいプロジェクトテンプレートに基づくドキュメントが "リリース候補版" として既に掲載済み (下記リンク先)。

"Use the Angular project template (release candidate)"

思ってたより早くに脱 webpack が進みそう...

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5