コトの発端
この記事を読んで「GASでもSFCが使えるんだー!」って知ったのですが、こちらはvue2.x系の記事です
Google Apps Scriptでちょっとしたアプリを作る場合はvue2.6系のCDN版を使ってましたが、nuxtもvuetifyもvue3.x系の正式リリースが迫ってきたので、そろそろ刷新時期かなぁと思い調べてみました
CDN版ですので例によってスクリプトエディタにコピペで動きます
まぁ、SFC使う規模のGAS webappならclasp使う方がラクだと思いますよ?
ってのは言わない約束で…
ソース
スクリプトエディタに配置するファイル的には3つです
- code.gs
- index.html
- sample.vue.html
GAS側 - code.gs
function doGet(request) {
return HtmlService.createTemplateFromFile('index')
.evaluate();
}
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename)
.getContent();
}
doGetとincludeはgoogleのページにある、いつものヤツです
ここは説明不要かと
フロント側 - 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を使う感じでしたが、この後継がvue3-sfc-loaderです
上記のソースはここのオススメに、GAS特有の改造を施した感じです
さて、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を以下の部分で読み込んでいます
// 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として使い、配列に格納しておきます
<?!=include('sample.vue')?> /* サーバー上に読み込んだjavascriptも実行されてしまう*/
<?=include('sample.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();
},
通常であればurlからfetchすればいい話ですが、GASにおいては前述の通りスクリプトエディタ上のファイルは取得できません
ですので、予め読んで登録済しておいた.vueがあればそれを返し
無ければ通常のurlとしてfetch処理をしてあげます
一応これで上手くいっているようですが、複雑な.vueを読み込む場合、include('sample.vue') の時に、読み込んだ文字列をパースする必要が出てくるかもしれません
SFC側 - 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で読み込むようお願いします
まとめ
なんか間違いあったらご指摘オナシャス!