Google Apps Script で Web アプリケーションとして作成しようと思い、フロントエンドを Angular 7にして、初期設定、デプロイまで実施してみました。
Angular(+2) の デプロイ方法は Web を調べても見つからなかったのですが、Vue.js の デプロイの手順を参考にして実施したところうまく画面表示するところまではできました。
手順、留意点をを記載します。


前提

以下の環境で実施しています。

  • OS

    sw_vers
    ProductName:    Mac OS X
    ProductVersion: 10.14.3
    BuildVersion:   18D109
    

  • Node.js のバージョン

    node -v
    v11.8.0
    

  • Angular のバージョン

    ng version     
    
         _                      _                 ____ _     ___
        / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
       / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
      / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
     /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                    |___/
    
    
    Angular CLI: 7.3.1
    Node: 11.8.0
    OS: darwin x64
    Angular: 
    ... 
    
    Package                      Version
    ------------------------------------------------------
    @angular-devkit/architect    0.13.1
    @angular-devkit/core         7.3.1
    @angular-devkit/schematics   7.3.1
    @schematics/angular          7.3.1
    @schematics/update           0.13.1
    rxjs                         6.3.3
    typescript                   3.2.4     
    


プロジェクトのディレクトリ構成について

Angular プロジェクトの frontend clasp プロジェクトの backend分けて作成しました。
ここは 1つにもできそうに思いますが、Angular の自動生成したファイル書き換えていいものか迷ったので 2つにしました。

  • Treeコマンドの結果

    tree -L 2              
    .
    ├── backend
    │   ├── LICENSE.txt
    │   ├── dist
    │   ├── node_modules
    │   ├── package-lock.json
    │   ├── package.json
    │   ├── src
    │   ├── tsconfig.json
    │   ├── tslint.json
    │   └── webpack.config.js
    ├── build.sh
    └── frontend
        ├── README.md
        ├── angular.json
        ├── dist
        ├── e2e
        ├── extra-webpack.config.js
        ├── node_modules
        ├── package-lock.json
        ├── package.json
        ├── src
        ├── tsconfig.json
        └── tslint.json
    

  • build.sh の内容
    プロジェクトルートのbuild.sh中身です。
    frontend ビルド後に、backend clasp プロジェクトをデプロイしています。
    もっとここはいい方法がありそうですが、これでも実現ができました。

    #!/bin/sh
    cd ./frontend
    ng build --prod
    cd ../backend
    npm run deploy
    


Angular CLI をインストール、Angular プロジェクトフォルダの作成

フロンドエンドのプロジェクトを作成します。

  • Angular CLIインストール
    Angular CLIインストールします。

    npm install -g @angular/cli 
    

  • プロジェクトフォルダの作成
    プロジェクトルートに移動して、ng コマンドで Angular プロジェクトを作成します。

    ng new frontend
    


Angular プロジェクトを Google Apps Script にデプロイするために調整する

Angular の内部で動いている webpack は 設定が隠蔽化されています。
Angular 6 で使用できた ng eject コマンドは Angular 7 では廃止されていて、ng eject コマンドを実行すると以下のようなメッセージが出力されます。

  • ng eject実行時のメッセージ

    ng eject
    The 'eject' command has been disabled and will be removed completely in 8.0.
    The new configuration format provides increased flexibility to modify the
    configuration of your workspace without ejecting.
    
    There are several projects that can be used in conjuction with the new
    configuration format that provide the benefits of ejecting without the maintenance
    overhead.  One such project is ngx-build-plus found here:
    https://github.com/manfredsteyer/ngx-build-plus
    
    メッセージに記載されている ngx-build-plusインストールだけではうまく動かず、Angular7でwebpack configを調整してバンドルサイズや挙動を自在に操る(ag-Grid入門付き!) - Qiita参考に設定したところうまくいきました。
    調整した内容について以下に記載します。

  • Angular プロジェクトルートに移動
    ここからの作業は、Angular プロジェクトルート で行います。

    cd frontend
    

  • webpack の調整に必要なパッケージのインストール
    以下、パッケージのインストールを行います。

    npm i -D @angular-builders/custom-webpack
    npm i -D ngx-build-plus
    npm i -D webpack
    

  • angular.json編集
    angular.json 内の記述を以下のように編集します。

    //builder を変更する
    //"builder": "@angular-devkit/build-angular:browser"
    "builder": "@angular-builders/custom-webpack:browser",
    "options": {
    "outputPath": "dist/frontend",
    "index": "src/index.html",
    "main": "src/main.ts",
    "polyfills": "src/polyfills.ts",
    "tsConfig": "src/tsconfig.app.json",
    //webpack の追加設定ファイルを指定する     
    "customWebpackConfig": {
        "path": "./extra-webpack.config.js"
    },
    

  • extra-webpack.config.js使用するパッケージのインストール
    extra-webpack.config.js使用するパッケージをインストールします。
    html-webpack-inline-source-plugin は 出力ファイルをHTMLファイル1つにまとめる ため、
    webpack-cdn-plugin は Angular 関連の JavaScript を CDN から取得するために使います。

    npm i -D html-webpack-plugin
    npm i -D html-webpack-inline-source-plugin
    npm i -D webpack-cdn-plugin
    

  • extra-webpack.config.js作成
    extra-webpack.config.js記述は以下になります。

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
    const WebpackCdnPlugin = require('webpack-cdn-plugin');
    
    module.exports = {
        // cdn から取得する対象のライブラリを、externals に指定する
        "externals": {
            "rxjs": "rxjs",
            "zone.js": "Zone",
            "@angular/core": "ng.core",
            "@angular/common": "ng.common",
            "@angular/platform-browser": "ng.platformBrowser",
            "@angular/router": "ng.router"
        },
        plugins: [
            new HtmlWebpackPlugin({
                      filename: 'index.html',
                      template: './src/index.html',
                      // webpack-cdn-plugin で cdn から読み込む対象にしているjs,cssはインライン化の対象外にする
                      inlineSource: '^(?!http).*.(js|css)$',
                      // Google Apps Script で読み込む際に都合が悪いので、minify時の動作を変更する
                      minify: {
                        removeAttributeQuotes: false,
                        removeScriptTypeAttributes: false
                      }
                }),
            new HtmlWebpackInlineSourcePlugin(),
            // Angular 関連のライブラリはCDNから取得する
            new WebpackCdnPlugin({
                modules: [
                  {
                        name: 'rxjs',
                        var: 'rxjs',
                        path: 'bundles/rxjs.umd.min.js'
                  },                
                  {
                    name: '@angular/core',
                    var: 'ng.core',
                    path: 'bundles/core.umd.min.js'
                  },
                  {
                    name: '@angular/common',
                    var: 'ng.common',
                    path: 'bundles/common.umd.min.js'
                  },
                  {
                    name: '@angular/platform-browser',
                    var: 'ng.platformBrowser',
                    path: 'bundles/platform-browser.umd.min.js'
                  },
                  {
                    name: '@angular/router',
                    var: 'ng.router',
                    path: 'bundles/router.umd.min.js'
                  },
                  {
                    name: 'zone.js',
                    var: 'Zone',
                    path: 'dist/zone.js'
                  }
                ],
                publicPath: '/node_modules'
              })
            ]
    }
    

  • app-routing.module.ts の調整
    Webアプリケーションとして公開時、HTML は以下のような URL に割り当てられます。
    https://xxxxxxxxxxxxxxxxxxxxxxxxxx-script.googleusercontent.com/userCodeAppPanel
    この URL に対して Angular の Router のマッピングがないと以下のようなエラーが発生します。

    core.umd.min.js:563 ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'userCodeAppPanel'
    Error: Cannot match any routes. URL Segment: 'userCodeAppPanel'
    
    このため自動生成されたapp-routing.module.ts Routes の定義を以下のように変更しました。
    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { AppComponent } from './app.component';
    
    const routes: Routes = [
      { path: '', component: AppComponent, pathMatch: 'full' },
      // root 以外の path を 空コンポーネントにマッピングしておく
      { path: '**', children: [] }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    

Angular プロジェクト に対して実施したことを以上です。


claspプロジェクトの作成

clasp プロジェクトは、howdy39/gas-clasp-starter: A starter template for Google Apps Script by claspベースに作成しました。

追加で実施したことについて記載します。

  • パッケージのインストール
    Angular プロジェクトで build した html を clasp の デプロイ時に含めたいので、copy-webpack-pluginインストールしました。

    npm i -D copy-webpack-plugin
    

  • backend/webpack.config.js修正
    copy-webpack-plugin対象のファイルをコピーする記述を追加しました。

    const path = require('path');
    const GasPlugin = require("gas-webpack-plugin");
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    
    module.exports = {
      mode: 'development',
      entry: './src/index.ts',
      devtool: false,
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
      },
      module: {
        rules: [
          {
            test: /\.ts$/,
            use: 'ts-loader'
          }
        ]
      },
      resolve: {
        extensions: [
          '.ts',
          '.js'
        ]    
      },
      plugins: [
        new GasPlugin(),
        // Angular プロジェクトでビルドした、index.html を、dist ディレクトリ配下にコピーする
        new CopyWebpackPlugin([
          {
            from: "../frontend/dist/frontend/index.html",
            to: "./index.html",
            toType: 'file'
          }
        ])
      ]
    };
    
    設定は以上です。
    これで、プロジェクトルートで、build.sh実行し、Google Apps Script で Web アプリケーションとして公開すると、ページが表示されるようになります。


その他試したこと、うまくいかないこと

上記作業中に試しはしたがうまくいかなかったので諦めたことになります。

  • dynamic-cdn-webpack-plugin利用
    webpack-cdn-plugin での パスの指定が面倒なので、dynamic-cdn-webpack-plugin - npm使おうとしましたが、以下、module-to-cdn で rxjs 6 以降の cdn のパスの解決ができない問題があり、webpack-cdn-plugin を使用しました。
    support rxjs 6 and only support version 5 and above by juanferreira · Pull Request #13 · mastilver/module-to-cdn

  • copy-webpack-plugin使わずに、html-webpack-plugin使っていた。
    copy-webpack-plugin存在を知らず、html-webpack-plugin使って Angular プロジェクトのファイルコピーを行なっていました。
    この方法でも、html-webpack-exclude-assets-plugin併用することで、ファイルコピーが実現可能ですが、あまりいいやり方ではないです。
    以下、パッケージをインストールして、

    npm i -D html-loader
    npm i -D html-webpack-exclude-assets-plugin
    npm i -D html-webpack-plugin
    
    以下、webpack.config.js でファイルコピーは実現できました。
    const path = require('path');
    const GasPlugin = require("gas-webpack-plugin");
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const HtmlWebpackExcludeAssetsPlugin = require('html-webpack-exclude-assets-plugin');
    
    module.exports = {
      mode: 'development',
      entry: './src/index.ts',
      devtool: false,
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
      },
      module: {
        rules: [
          {
            test: /\.ts$/,
            use: 'ts-loader'
          },
          {
            test: /\.html$/,
            loader: "html-loader"
          }      
        ]
      },
      resolve: {
        extensions: [
          '.ts',
          '.js'
        ]    
      },
      plugins: [
        new GasPlugin(),
        // Angularプロジェクトの
        new HtmlWebpackPlugin({
          excludeAssets: [/\.js$/] ,
          filename: "./index.html",
          template: "../frontend/dist/frontend/index.html",
        }),
        new HtmlWebpackExcludeAssetsPlugin()
      ]
    };
    

  • favicon.icoのbase64エンコード
    index.html 内に、favicon.ico の記載がありますが、webpack で favicon.ico を base64 エンコードする方法がわからず、一旦放置しました。
    Google Apps Script から直接 URL を指定するか、gulp ですが favicon を base64 エンコードする plugin があったので、後日実施しようかと思います。

  • GASで作成したWebページにファビコンを設定する方法
  • gulp-base64-favicon - npm

参考

以下、作業実施時に参考にした記事になります。

以上です。

コメント