Google Apps Script の Web アプリで CORS 制約を回避して Web Woker や ES Modules を使う

3 min読了の目安(約2700字TECH技術記事

外部スクリプトが使えない

GASで作ったWebアプリで下記のようなコードを書いてもエラーになります。

xxx.html
<!-- 前略 -->
<script type="module">
import { module } from "https://xxx/module.js"
cosnt worker = new Worker( "https://xxx/worker.js" )
</script>
<!-- 後略 -->

開発コンソールで確認してみると「CORS」や「SOP」といった文言のエラーメッセージが出ていると思います。
CORSはオリジン間リソース共有、SOPは同一生成元ポリシーのことで、ざっくり言うと「他のWebサイトから持ってきてはいけない」というエラーです。

つまり、他所から持って来ていないように見せれば解決します。

失敗例 ContentServiceを使ってみる

同じ失敗をしないため、まずは失敗例の紹介から。

GASにはHTMLを表示するHtmlServiceに対してプレーンテキストなどを表示するContentServiceというクラスがあります。

これを使ってdoGetメソッドをURLパラメーターを渡したときだけWorker用のJSファイルを返すようにカスタマイズします。

main.gs
function doGet( e ) {
  const js = e.parameter.js
  if ( js != null ) {
    const content = HtmlService.createHtmlOutputFromFile( js ).getContent()
    return ContentService
    .createTextOutput( content )
    .setMimeType( ContentService.MimeType.JAVASCRIPT )
  }
  return HtmlService.createTemplateFromFile( "index" ).evaluate()
}
index.html
<!-- 前略 -->
<script>
const url = "<?= ScriptApp.getService().getUrl() ?>?js=worker.js"
const worker = new Worker( url )
worker.onmessage = console.log
worker.onerror = console.error
worker.postMessage( "Test Message." )
</script>
<!-- 後略 -->
worker.js.html
self.addEventListener( "message", event => {
  self.postMessage( event.data )
} )

しかし、これでは上手くいきません。
何故ならHTMLがインラインフレーム(iframe)で入れ子にされるからです。

こちらで作ったHTMLは一番内側のiframe内に表示され、これが外側(ブラウザから見えるURL)と異なるドメインになっているため、結局同一生成元ポリシーに違反します。

成功例 文字列からBlobを起こしてObjectURLを生成する

所謂インラインワーカーを使います。

GAS側にWorkerのソースコード文字列を返すメソッドとしてgetCodeを定義します。
このメソッドをindex.html側でgoogle.script.runを使って呼びます。

main.gs
function doGet( e ) {
  return HtmlService.createTemplateFromFile( "index" ).evaluate()
}

function getCode() {
  return HtmlService.createHtmlOutputFromFile( "worker.js" ).getContent()
}
index.html
<!-- 前略 -->
<script>
google.script.run
.withSuccessHandler( code => {
  const url = URL.createObjectURL( new Blob( [ code ], { type: "text/javascript" } ) )
  const worker = new Worker( url )
  worker.onmessage = console.log
  worker.onerror = console.error
  worker.postMessage( "Test Message." )
})
.withFailureHandler( console.error )
.getCode()
</script>
<!-- 後略 -->
worker.js.html
self.addEventListener( "message", event => {
  self.postMessage( event.data )
} )

以上!

コードは Web Worker で説明しましたが、ES Modules の場合はObjectURLを生成するところまでは一緒で、Dynamic Importを使えば同様に実現できます。