Help us understand the problem. What is going on with this article?

Vue.js製のSPAをGoogleAppsScriptにデプロイするためのWebpack設定

More than 1 year has passed since last update.

この記事のWebpack設定はVue CLI v2向けのものです。
Vue CLI v3のvue.config.jsに関しては下記の記事を参照してください。

GoogleAppsScriptのWebAppsでVue.jsするためのvue.config.js
https://qiita.com/clomie/items/ef10db03b9ecef29ca5d

はじめに

vue-cliのwebpackテンプレートで作成したVue.jsのアプリケーションを、GoogleAppsScript(以下GAS)のHTMLServiceで動作可能な形にビルドする手順をまとめます。
最終的にはnpm run buildでビルドしたファイルを、clasp pushでそのままアップロード可能にすることが目的です。

Vueのプロジェクトを作成するところからスタートします。
プロジェクト名は仮に"vue-deploy-gas-sample"として進めます。

$ vue init webpack vue-deploy-gas-sample
$ cd vue-deploy-gas-sample

対応手順

  • 出力ファイルをHTMLファイル1つにまとめる
  • GAS+claspのセットアップ
  • vue-routerとgoogle.script.historyの同期

セットアップ完了後のリポジトリです。
https://github.com/clomie/vue-deploy-gas-sample

手順通りにコミットを作ってあるので、追っていく感じで説明します。

出力ファイルをHTMLファイル1つにまとめる

GASでは、JSファイルやCSSファイルを分割して配信することはできません。
公式のガイドでは、styleタグやscriptタグとして分離したファイルを用意して、GAS内でインライン化してからレスポンスを返す方法が推奨されています。
https://developers.google.com/apps-script/guides/html/best-practices

今回はwebpackを使っているので、ビルドの段階ですべてHTMLファイルにインライン化します。

HtmlWebpackInlineSourcePluginの導入

https://github.com/clomie/vue-deploy-gas-sample/commit/47b3b5101f7fb1bcc17a476dfe3f1b4324089e89

vue-cliのwebpackテンプレートではHtmlWebpackPluginを使ってindex.htmlを生成していますが、サブプラグインであるHtmlWebpackInlineSourcePluginでインライン化できるようなので、これを導入します。

$ npm install -D html-webpack-inline-source-plugin
build/webpack.prod.conf.js
 const HtmlWebpackPlugin = require('html-webpack-plugin')
+const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin')
 const ExtractTextPlugin = require('extract-text-webpack-plugin')

プロダクションビルド用のwebpack設定ファイルに、HtmlWebpackInlineSourcePluginを適用します。デフォルトでは何もインライン化されない設定になっているので、inlineSourceプロパティの設定を忘れないように。
また、minifyの設定でremoveAttributeQuotesがtrueだとGAS上でエラーになってしまうのでOFFにします。

build/webpack.prod.conf.js
     // generate dist index.html with correct asset hash for caching.
     // you can customize output by editing /index.html
     // see https://github.com/ampedandwired/html-webpack-plugin
     new HtmlWebpackPlugin({
       filename: config.build.index,
       template: 'index.html',
       inject: true,
+      // embed inline all javascript and css
+      inlineSource: '.(js|css)$',
       minify: {
         removeComments: true,
         collapseWhitespace: true,
-        removeAttributeQuotes: true
         // more options:
         // https://github.com/kangax/html-minifier#options-quick-reference
       },
       // necessary to consistently work with multiple chunks via CommonsChunkPlugin
       chunksSortMode: 'dependency'
     }),
+    new HtmlWebpackInlineSourcePlugin(),

SourceMapのインライン化

https://github.com/clomie/vue-deploy-gas-sample/commit/2dedd779f7992c65f9b569572eb0948c714e8e01

GAS上でSourceMapを見たい場合は、SourceMapもインライン化する必要があります。

build/webpack.prod.conf.js
     // Compress extracted CSS. We are using this plugin so that possible
     // duplicated CSS from different components can be deduped.
     new OptimizeCSSPlugin({
       cssProcessorOptions: config.build.productionSourceMap
-        ? { safe: true, map: { inline: false } }
+        ? { safe: true, map: { inline: true } }
         : { safe: true }
     }),
config/index.js
     productionSourceMap: true,
     // https://webpack.js.org/configuration/devtool/#production
-    devtool: '#source-map',
+    devtool: '#inline-source-map',

静的ファイルのインライン化

https://github.com/clomie/vue-deploy-gas-sample/commit/0004c3db119a68cc9041d25a045ec536bf4855de

フォントファイル、画像ファイルなどもインライン化する必要があるため、url-loaderのlimit設定を取り除きます。
また、webpackテンプレートではstaticディレクトリが用意されていますが、これもなかったことにします。

build/webpack.base.conf.js
       {
         test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
         loader: 'url-loader',
         options: {
-          limit: 10000,
           name: utils.assetsPath('img/[name].[hash:7].[ext]')
         }
       },
       {
         test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
         loader: 'url-loader',
         options: {
-          limit: 10000,
           name: utils.assetsPath('media/[name].[hash:7].[ext]')
         }
       },
       {
         test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
         loader: 'url-loader',
         options: {
-          limit: 10000,
           name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
         }
       }
build/webpack.prod.conf.js
-    // copy custom static assets
-    new CopyWebpackPlugin([
-      {
-        from: path.resolve(__dirname, '../static'),
-        to: config.dev.assetsSubDirectory,
-        ignore: ['.*']
-      }
-    ])

GAS+claspのセットアップ

https://github.com/clomie/vue-deploy-gas-sample/commit/4506b4163ea6598530e592590941352641a86993

GASプロジェクト作成

作成したプロジェクトのディレクトリでclasp createコマンドを実行します。

$ clasp create vue-deploy-gas-sample

GASのプロジェクトが作成されて、.clasp.jsonappsscript.jsonが生成されます。
デフォルトの設定では、claspはプロジェクトフォルダ内のファイルをすべてGASプロジェクトにアップロードするため、ファイルの配置と内容を変更していきます。

.clasp.json

claspでGASプロジェクトへファイルをアップロードするための設定ファイルです。デフォルトではGASプロジェクトのIDだけが書かれています。
crasp pushコマンドを実行する際のカレントディレクトリに配置されている必要があるため、これはプロジェクトフォルダルートにこのまま置いておきます。
コマンド実行時にnpm run buildで生成したファイルがアップロードされるように、rootDirにdistを指定しておきます。

.clasp.json
-{"scriptId":"<Your GAS Project ID>"}
+{
+  "scriptId": "<Your GAS Project ID>",
+  "rootDir": "dist"
+}

gas/appsscript.json

.clasp.jsonのrootDirで指定したディレクトリに配置されている必要があります。
ただし、distディレクトリにビルド成果物以外を置いておきたくないので、プロジェクトフォルダ内にgasディレクトリを作ってそこに置くことにします。
webpack実行時にgasディレクトリからdistディレクトリにコピーする設定を追加します。

gas/Code.js

GASプロジェクトでのエントリポイントとなるファイルです。
実際にGAS上でページを開いたときに呼び出される処理です。これもgasディレクトリに配置します。

gas/Code.js
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}

.claspignore

claspでアップロードする際の無視ファイルリストです。
claspリポジトリのREADMEにも例がありますが、アップロードしたいファイルのほうが少ないので、ホワイトリスト方式で書いたほうが楽です。
これはプロジェクトフォルダルートに配置します。

.claspignore
**/**
!dist/appsscript.json
!dist/Code.js
!dist/index.html

webpack設定

gasディレクトリ内のファイルをビルド時にdistディレクトリにコピーする設定を、webpackの設定ファイルに追記します。

build/webpack.prod.conf.js
       children: true,
       minChunks: 3
     }),

+    // copy google apps script files
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../gas'),
+        ignore: ['.*']
+      }
+    ]),
+
     // copy custom static assets
     new CopyWebpackPlugin([
       {

ここまででとりあえず動く

ひとまず、ここまでで最低限の設定は完了です。
ビルドすればGASプロジェクトにアップロードするファイルが生成されます。

$ npm run build

> vue-deploy-gas-sample@1.0.0 build /path/to/vue-deploy-gas-sample
> node build/build.js

Hash: d0dd6dffd9a3d968a929
Version: webpack 3.11.0
Time: 11674ms
                                              Asset      Size  Chunks                    Chunk Names
           static/js/vendor.7fed9fa7b7ba482410b7.js    850 kB       0  [emitted]  [big]  vendor
              static/js/app.b22ce679862c47a75225.js   41.2 kB       1  [emitted]         app
         static/js/manifest.2ae2e69a05c33dfc65f8.js    7.5 kB       2  [emitted]         manifest
static/css/app.30790115300ab27614ce176899523b62.css   1.52 kB       1  [emitted]         app
                                         index.html    900 kB          [emitted]  [big]
                                    appsscript.json  98 bytes          [emitted]
                                            Code.js  77 bytes          [emitted]

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.

ビルド結果にはJSファイル、CSSファイルも含まれていますが、index.htmlを見るとすべてインライン化されています。
GASプロジェクトにアップロードしてみましょう。

$ clasp push
└─ dist/Code.js
└─ dist/appsscript.json
└─ dist/index.html
Pushed 3 files.

あとはGASプロジェクトからWebアプリケーションとしてデプロイすれば動くはずです。
https://developers.google.com/apps-script/guides/web#deploying_a_script_as_a_web_app

vue-routerとgoogle.script.historyの同期

https://github.com/clomie/vue-deploy-gas-sample/commit/0c5a01f3bb3a1262e2a6a6b38a94881c3752ef79

GASでデプロイしたアプリはiframe内で動作するため、vue-routerを使った場合、画面遷移とブラウザのURLが同期しません。
routerのナビゲーションを拾って、GASのClient-side APIからブラウザのURLを更新することで、これを解決できます。

src/router/gas-router-sync.js
export const sync = router => {
  if (!window.google) {
    return
  }

  window.google.script.url.getLocation(location => {
    const path = location.hash
    const query = location.parameter
    router.replace({ path, query })
  })

  router.afterEach(route => {
    window.google.script.history.replace(null, route.query, route.path)
  })
}

google.script.history, google.script.urlはGAS上のWebアプリでグローバルにアクセス可能なオブジェクトです。

その他

この記事ではvue-cliのwebpackテンプレートをベースに説明しましたが、
出力ファイルが1つで良いのであれば、webpackの設定はよりシンプルにできるはずで、主にExtractTextPluginや、CommonsChunkPluginなどは不要になるかと思います。
このあたりもう少し詰めればビルド時間が短くなったりするかもしれません。

参考リンク

google/clasp
Web Apps  |  Apps Script  |  Google Developers
HTML Service  |  Apps Script  |  Google Developers

Google Apps Scriptの新しい3つの機能 その③ CLI Tool Clasp
Google Apps Script をローカル環境で快適に開発するためのテンプレートを作りました
Google Apps Script でAngularJSを使った Single Page Application を構築・公開する方法(基礎編)

clomie
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away