GAS上で動かすCDN版 vue3.xでもSFC(.vueファイル)を使いたい！

# コトの発端
この記事を読んで「GASでもSFCが使えるんだー！」って知ったのですが、こちらはvue2.x系の記事です

https://qiita.com/txkodo/items/dc90f49fb7af03139439

Google Apps Scriptでちょっとしたアプリを作る場合はvue2.6系のCDN版を使ってましたが、nuxtもvuetifyもvue3.x系の正式リリースが迫ってきたので、そろそろ刷新時期かなぁと思い調べてみました

CDN版ですので例によってスクリプトエディタにコピペで動きます

まぁ、SFC使う規模のGAS webappならclasp使う方がラクだと思いますよ？
ってのは言わない約束で…

# ソース

スクリプトエディタに配置するファイル的には３つです
* code.gs
* index.html
* sample.vue.html

## GAS側 - code.gs
```javascript:code.gs
function doGet(request) {
  return HtmlService.createTemplateFromFile('index')
      .evaluate();
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename)
      .getContent();
}
```
doGetとincludeはgoogleのページにある、いつものヤツです
ここは説明不要かと

https://developers.google.com/apps-script/guides/html/best-practices#separate_html_css_and_javascript

## フロント側 - index.html
```index.html
<!DOCTYPE html>
<html>
  <body>
    <div id="app">
      <sample></sample>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@next"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader"></script>
    
    <script>
      // GAS上にある.vueファイルを文字列として事前に読み込んでしまう
      // nameに拡張子.vue 必須
      const vuefiles = [
        {
          name: 'sample.vue',
          tag: 'sample',
          template:<?=include('sample.vue')?>
        }
      ];
      const options = {
        moduleCache: {
          vue: Vue
        },
        async getFile(url) {
          // GASで読み込み済の.vueファイルがあったらそれを渡す
          const vue = vuefiles.find(v=>v.name===url);
          if ( vue ) return Promise.resolve(vue.template)

          // 外部のurlであればそれをfetchする
          const res = await fetch(url);
          if ( !res.ok )
            throw Object.assign(new Error(res.statusText + ' ' + url), { res });
          return await res.text();
        },
        addStyle(textContent) {
          const style = Object.assign(document.createElement('style'), { textContent });
          const ref = document.head.getElementsByTagName('style')[0] || null;
          document.head.insertBefore(style, ref);
        },
      };
      const app = Vue.createApp();

      const { loadModule } = window['vue3-sfc-loader'];
      vuefiles.forEach(v=>{
        // ルートで全てのコンポーネントをグローバル登録する
        app.component(v.tag, Vue.defineAsyncComponent( () => loadModule(v.name, options) ) );
      })

      app.mount('#app');
    </script>
  </body>
</html>
```
CDN版vue2.6でSFCを使うには、[http-vue-loader](https://github.com/FranckFreiburger/http-vue-loader)を使う感じでしたが、この後継がvue3-sfc-loaderです
上記のソースはここのオススメに、GAS特有の改造を施した感じです

https://github.com/FranckFreiburger/vue3-sfc-loader


さて、Vue.createAppされてコンポーネント登録をするのですが、入口である loadModule にはファイル名を渡す必要があります
しかし、GASにおいては './sample.vue' とかやっても、ファイルをアクセスできるわけではありません
また、vue3-sfc-loaderの内部的には　.vue / .mjs / .js を判別しているっぽいです
https://github.com/FranckFreiburger/vue3-sfc-loader/blob/80f10f9fd82d7dde6c8681d23d36e9ac3ab9a654/src/tools.ts#L370
ですので、拡張子の.vueは必須です

ここは一旦　'sample.vue'　として渡してしまいますが一工夫が必要となります

まず、Scriptエディタ上に配置された sample.vue.htmlを以下の部分で読み込んでいます

```javascript
      // GAS上にある.vueファイルを文字列として事前に読み込んでしまう
      // nameに拡張子.vue 必須
      const vuefiles = [
        {
          name: 'sample.vue',
          tag: 'sample',
          template: <?=include('sample.vue')?>
        }
      ];
```

この時使うのがGASのscriptlet構文です

ここではsample.vue.htmlを文字列として取得したいだけなので、この文脈で良く使われるForce-printing scriptlets構文ではなくprinting scriptletsとして使い、配列に格納しておきます
```javascript
<?!=include('sample.vue')?> /* サーバー上に読み込んだjavascriptも実行されてしまう*/
<?=include('sample.vue')?> /* サーバー上で読み込んで文字列をエスケープする*/
```

https://developers.google.com/apps-script/guides/html/templates


その後中で何やかんやした後でここに入ってきます
```javascript
        async getFile(url) {
          // GASで読み込み済の.vueファイルがあったらそれを渡す
          const vue = vuefiles.find(v=>v.name===url);
          if ( vue ) return Promise.resolve(vue.template)

          // 外部のurlであればそれをfetchする
          const res = await fetch(url);
          if ( !res.ok )
            throw Object.assign(new Error(res.statusText + ' ' + url), { res });
          return await res.text();
        },
```

通常であればurlからfetchすればいい話ですが、GASにおいては前述の通りスクリプトエディタ上のファイルは取得できません

ですので、予め読んで登録済しておいた.vueがあればそれを返し
無ければ通常のurlとしてfetch処理をしてあげます

一応これで上手くいっているようですが、複雑な.vueを読み込む場合、include('sample.vue')　の時に、読み込んだ文字列をパースする必要が出てくるかもしれません

## SFC側 - sample.vue.html
```vue:sample.vue.html
<template>
  <span class="btn">{{ msg }}</span>
</template>

<script setup>
  import { ref } from 'vue'
  const msg = ref("GAS Webappでも CDN版vue3.xのSFC(.vueファイル)が動いちゃった！!");
</script>

<style scoped>
.btn {
  color: white;
  background-color: teal;
  border-radius: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 3em;
  font-size: large;
  font-weight: 600;
}  
</style>
```

はい
特に普通です
script setupはラクで良いですね

ちなみに、.vue内でさらに別のCDNからライブラリとかを読み込むと上手く行きません
index.htmlで読み込むようお願いします

# まとめ

なんか間違いあったらご指摘オナシャス！