GAミント至上主義

Web Monomaniacal Developer.

7行から始めるVue.jsプラグイン開発

Vue.jsでは、The Progressive JavaScript Frameworkを掲げる通り、コアは小さく、拡張しやすいように作られています。今期アニメではゴブリンスレイヤーとスライムでゴブリンの違いを楽しんでます。

そのため独自の機能が必要になった場合、自分でプラグインを追加していくということも簡単です(もちろんmixinVuexなどなど状況に応じて他の選択肢もあります)

プラグイン — Vue.js

実用的な簡単なプラグインの例としてyagish履歴書でも使ってるSiteというプラグインを紹介します。

サイト名などいろんな所で使う情報をまとめ、呼べるようにするプラグインです。

プラグイン本体は最小構成で7行ぐらい。サイト名nameを持った$siteを追加します。

実際はサイト名だけでなく、公開URLも入れて本番、ステージング、開発で変えられるようにしてます。

環境で変わる情報は環境変数に設定していますが、template内では直接process.envを呼べないため、computedなどでscript側でコンポーネントごとに作る必要があり、面倒なので作りました。

Vue CLI3での環境変数の詳細は下記のページにあります。
cli.vuejs.org

pluginsというフォルダは必須ではないですが、数が増えてきたら分けた方が分かりやすいと思います。

plugins/site.js

const Site = {
  install: function (Vue, options) {
    Vue.prototype.$site = {
      name: process.env.VUE_APP_SITE_NAME
    }
  }
}
export default Site

もちろん値を増やしたり、関数を追加することもできます。

main.jsでインストールします
main.js

import Site from './plugins/site'
Vue.use(Site)

main.jsでインストールしたのであとはどのコンポーネント内で使えます。
template内では{{ $site.name }}で表示や使用ができ、script内ではthis.$site.nameで使えるようになります。

yagiComponent.vue

<template>
<div v-if="$site.name">{{ $site.name }}</div>  
</template>

<script>
export default {
  data: function () {
    return {}
  },
  mounted: function () {
    console.log(this.$site.name)
  }
}
</script>

もっと高度なプラグイン

今回のnameプロパティはリアクティブではないので、もしnameが変更されたとしても、テンプレートは自動的に更新されません。
もし、どこでも呼び出せるリアクティブな値を持たせたい、すべてのコンポーネントでイベントをemit、onしたい場合などはプラグインにVueインスタンスを入れてしまうこともできます。
複雑になったらVuexの方が相応しい場合もあると思いますが、選択肢として知っていた方が開発は楽になると思います。

plugins/event-hub.js

const eventHub = {
  install: function (Vue, options) {
    Vue.prototype.$eventHub = new Vue()
  }
}
export default eventHub

medium.com
uyamazak.hatenablog.com

ちなみにyagish履歴書のヤギの名前、色などはVuexを使わず、yagiプラグインで行っています。
dataに色などの値を持ち、変更があったらローカルに保存したり、mountedで読み込み処理をしています。

f:id:uyamazak:20181026144715p:plain

Vue.js入門 基礎から実践アプリケーション開発まで

Vue.js入門 基礎から実践アプリケーション開発まで

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

ぼくが考えた最強のFirebase Functionsのファイル構成を考えてる途中

まだ全然最強感がないけど、やり始めたら1日過ぎたのでまとめる。

% tree -L 2 -I node_modules functions 
functions
├── index.js # メインのやつ
├── package-lock.json
├── package.json
├── src
│   ├── blog-rss-to-json.js # 呼び出すやつ
│   ├── globals.js # グローバル変数のやつ
│   ├── libs.js # 共有で使う関数とか
│   ├── not-found.js # 呼び出すやつ
│   ├── reply-postcard-to-yagish.js # 呼び出すやつ
│   ├── send-postcard-to-chat.js # 呼び出すやつ
│   └── update-omikuji-statistics.js # 呼び出すやつ
└── yarn.lock

1関数1ファイルに分割

この記事に詳しく書いたけど1関数1ファイルに分割する。
一覧のオブジェクトを見れば、どんな名前でどの関数を読んでるか分かるようになる。

index.js

const funcs = {
  blogRssToJson: './src/blog-rss-to-json',
  sendPostCard2Chat: './src/send-postcard-to-chat',
  replyPostCard2Yagish: './src/reply-postcard-to-yagish',
  notFound: './src/not-found',
  updateOmikujiStatistics: './src/update-omikuji-statistics'
}
const loadFunctions = (funcsObj) => {
  for(let name in funcsObj){
    if(! process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === name) {
      exports[name] = require(funcsObj[name])
    }
  }
}

loadFunctions(funcs)

とりあえずindex.js以外のjsファイルはsrc以下にぶちこんでる。
関数のファイルと後述のglobalsとかlibs的なファイルは増えてきたら別に分けた方がいいかもしれない。

これでindex.jsがめっちゃ長くなったり、無駄なモジュールを読み込むことが減る・・はず。

環境変数は.env的なファイルにする

firebase functionの環境変数の扱いはちょっと変わってる。
process.envではなく、funtions.config()を使えとなってる。

環境の構成  |  Firebase

呼び出しは

funtions.config().foo.bar

みたいな感じでいいとしても、セットがめんどい。

デプロイする前に、以下の slack.url 環境構成変数を設定します。

firebase functions:config:set slack.url=https://hooks.slack.com/services/XXX

CLIのコマンドでセットしないといけない。

JSONファイルも読ませられるけど、トップレベルのキーは指定しなきゃいけないので、1ファイルにすべてとまとめるのができない。
qiita.com

そのため環境ごとに下記のようなファイルを作り、デプロイのシェルスクリプトに追加することにした。
よくある.env.*にしなかったのは、コマンドがconfigだから。

.config.production

chat.webhook_url=https://chat.googleapis.com/v1/spaces/YAGIYAGIYAGIYAGISH
site.id=yagish_production
blog.rss_url=https://blog.yagish.jp/rss
blog.access_control_allow_origin= https://rirekisho.yagish.jp

そのままcatしただけだと改行が値に入ってしまうので、 tr "\n\r" " "をいれて改行をスペースに変換する
Windowsで書いたから改行がCRLFになってたのが原因、LFだったら大丈夫)

値を""で囲いたかったけど、どうしてもエスケープされて入ってしまうので諦めた
(printfとかcat、echoのオプションとか2時間ぐらい闘った)
もっとbashを理解したい

firebase functions:config:set `cat ./app/functions/.config.production| tr "\n\r" " "`

セットしたらちゃんとできてるか確認した方がいい。

firebase functions:config:get

実際のdeploy.shはこんな感じ(一部抜粋)
全部Dockerコンテナ内でやっているのでこんなになってしまう。一つのexecで&で繋げてもいいかな

sudo docker exec  \
    ${CONTAINER_ID} \
    yarn build-production

sudo docker exec  \
    ${CONTAINER_ID} \
    firebase -P ${PROJECT_ID} functions:config:set `cat ./app/functions/.config.production| tr "\n\r" " "`

sudo docker exec  \
    ${CONTAINER_ID} \
    firebase -P ${PROJECT_ID} functions:config:get

sudo docker exec  \
    ${CONTAINER_ID} \
    firebase deploy -P ${PROJECT_ID} -p dist-production

よく使う大きいオブジェクトはglobal変数にぶち込んで再利用&遅延評価

詳しくはここを参照
ヒントとアドバイス  |  Firebase

とりえあずよく使う大きいオブジェクトは、firebase-admin、firestoreのオブジェクトなのでこれをglobal化しておく。
ほぼ絶対使うfirebase-functionsもglobalにいれるけど、絶対使うので遅延評価はしない。

./src/globals.js

global.functions = require('firebase-functions')

global.admin = null
global.adminInit = function () {
  console.log('adminInit')
  global.admin = require('firebase-admin')
  global.admin.initializeApp()
  return global.admin
}

global.firestore = null
global.firestoreInit =  function () {
  console.log('firestoreInit')
  const admin = global.admin || global.adminInit()
  global.firestore = admin.firestore()
  global.firestore.settings({
    timestampsInSnapshots: true
  })
  return global.firestore
}

とりえあず変数名+Initという命名規則で読み込む関数を作る。
冗長で最強感ない。

これをindex.jsの冒頭で読ませておく
index.js

require('./src/globals')
// 省略

あとそれぞれファイルの冒頭に使うものを書いておく。

久しぶりにグローバル変数使った(WordPressぶり?)けど、改めて「こいつどこから来たんだ?」感が半端ないので、明示するという意味もある。
グローバル変数なのでglobalをつけずにも呼べるんだけど、あえてglobalを付けて使ってる。

ちなみにnodeのglobalオブジェクトは今回知った。
globalsにglobalオブジェクトがあるのでglobalも当然globalなオブジェクトなんだね(不明)。

global.firestore = global.firestore || global.firestoreInit()
global.admin = global.admin || global.adminInit()

関数の数も実行回数も少ないので、圧倒的に早すぎる最適化感がある。
今後問題を感じたら都度改善して最強を目指すか、もっといいのを見つけたらそっちにしようと思う。

Puppeteerでエラー"The previous printing job hasn't finished at Promise "

yagish履歴書で使っているhcep-pdf-serverでエラーが出ていたので調査。

Error: Protocol error (Page.printToPDF): The previous printing job hasn't finished at Promise

現在はGKEで動かしていて、このような制御できないエラーが出た場合はプロセスを終了させているので、すぐにkubernetesのdeployちゃんが再起動してくれて、解決する。

ユーザーは一度は失敗しちゃうけど、複数Pod動かしているので、もう一度実行すれば成功しているはず。

エラーは以前のjobが終わってないという内容だけど、連続で大きいリクエスト(吾輩は猫である全文とか)しまくっても再現せず。
頻度も2週間に1度とかだから、どこまでやるべきか悩みどころ。

検索すると下記記事が出てきて

Protocol error (Page.printToPDF): The previous printing job hasn't finished · Issue #3315 · GoogleChrome/puppeteer · GitHub

page.pdf()にawaitつけて待たせて、みたいな助言で終了しているけど、hcepではすでに付けているので違う問題っぽい。

Pageオブジェクトに現在のプリントジョブステータスを取得できるのがないかざっと見たけど見当たらず。

puppeteer/api.md at master · GoogleChrome/puppeteer · GitHub


そもそもブラウザは何日間も一つのページをサーバーとして安定して使うようにはできていないはずだから、
ユーザーがごくたまーに失敗するのはあきらめて、その時困らないようにUI側で親切に説明するのが最善だろうか。