‘Firebase’ タグのアーカイブ

suganuma

Firebase Genkitを試してみた

Firebase Genkit は、本番環境に対応した AI 搭載アプリの構築、デプロイ、モニタリングに役立つオープンソース フレームワークです。

今回、以下を試してみます。

  • ブラウザからFirebase Functionを呼び出し実行
  • Firebase Local Emulator Suiteを使ってローカルで開発
  • Cloud Firestore vector storeを使ってRAG(Retrieval Augmented Generation)のインデックス作成と検索を行う
  • モデルにGoogle AIのGemini ProとOpenAIのGPT-4oを使う

セットアップ

ここを参考にしています。
Node.jsのバージョン20以降が必要です。
Google AIのGeminiを使うため、Generative Language APIを有効にし、APIキーを作成しておきます。

空のディレクトリを作成し、そこで以下のコマンドを実行し、いくつかの質問に答えるとFirebaseプロジェクトが作成されます。

firebase init genkit
質問の中でサンプルフローを生成するか聞かれますので、「Y」を選択します。

Gemini APIキーを環境変数にセットします。

export GOOGLE_GENAI_API_KEY=*********
functionsディレクトリでnpm run buildでビルドした後、以下を実行しエミュレータを起動します。

GENKIT_ENV=dev firebase emulators:start --inspect-functions
さらに以下を実行しGenkit Dev UIを起動します。

genkit start --attach http://localhost:3100 --port 4001
ブラウザでhttp://localhost:4001/ にアクセスすると、サンプルフローのmenuSuggestionFlowがあります。試しに、Auth JSONに{"uid": 0,"email_verified": true}を入力し(サンプルコードでメール確認済ユーザのみとなっている)、input JSONに「牛丼」と入れてみました。
結果、以下のような 汁だく特盛牛丼を提案されました。

**"Gyu-don Grande"**

A super-sized portion of traditional gyudon, featuring:

* Double the amount of tender and juicy beef
* Extra fluffy rice with a hint of soy sauce and mirin glaze
* Ample servings of savory onions
* Topped with a generous drizzle of umami-rich sauce
* Served in a large bowl taller than a skyscraper

Pairs well with:

* Miso soup
* Cold soba noodles
* Pickled side dishes
インデックス作成Cloud Firestore vector storeを使ったインデックス作成します。
ここを参考にしています。

エミュレータでFirestoreを使うので、firebase init emulatorsを実行し、Firestore Emulatorを使えるように設定します。ついでに後で認証とホスティングも使うのでAuthentication emulatorとHosting Emulatorも使えるように設定しておきます。
あと、firebase init hostingでホスティング用の設定ファイルを作成しておきます。

以下の内容のファイルをfunctions/src/populate_collection.tsに作成します。
処理内容は以下です。

  • テキストファイルfunctions/company.txtを読んで、改行2つを区切りとしてチャンクに分ける

import { configureGenkit } from "@genkit-ai/core";
import { embed } from "@genkit-ai/ai/embedder";
import { defineFlow, run } from "@genkit-ai/flow";
import { textEmbeddingGecko001, googleAI } from "@genkit-ai/googleai";

import { FieldValue, getFirestore } from "firebase-admin/firestore";

import { chunk } from "llm-chunk";
import * as z from "zod";

import { readFile } from "fs/promises";
import path from "path";

const indexConfig = {
collection: "company",
contentField: "text",
vectorField: "embedding",
embedder: textEmbeddingGecko001,
};

configureGenkit({
plugins: [googleAI()],
enableTracingAndMetrics: false,
});

const firestore = getFirestore();

// Firebase functionにしないのでonFlowではなくdefineFlow関数を使う
export const indexFlow = defineFlow(
{
name: "indexFlow",
inputSchema: z.void(),
outputSchema: z.void(),
},
async () => {
const filePath = path.resolve('./company.txt');

const txt = await run("extract-text", () =>
extractText(filePath)
);

// 改行2つを区切りとしてチャンク作成
const chunks = await run("chunk-it", async () => chunk(txt, {delimiters: '\n\n' }));

// Add chunks to the index.
await run("index-chunks", async () => indexToFirestore(chunks));
}
);

async function indexToFirestore(data: string[]) {
for (const text of data) {
const embedding = await embed({
embedder: indexConfig.embedder,
content: text,
});
await firestore.collection(indexConfig.collection).add({
[indexConfig.vectorField]: FieldValue.vector(embedding),
[indexConfig.contentField]: text,
});
}
}

async function extractText (filePath: string) {
const f = path.resolve(filePath);
return await readFile(f, 'utf-8');
}
上記を実行するため、functions/src/index.tsに以下を追加します。

export { indexFlow } from './populate_collection';
結果、Genkit Dev UIのフローに「indexFlow」が追加されました。
input JSONに何も入力せず、「RUN」ボタンを押します。
成功すると、Firestoreにcompanyコレクションが作成されます。
エミュレータで確認してみると、embeddingとtextフィールドを持つドキュメントが複数あり、embedding.valueにはベクター情報があります。

RAGのプロンプト生成

functions/src/index.tsを以下の内容に置き換えます。

import {retrieve} from "@genkit-ai/ai";
import {configureGenkit} from "@genkit-ai/core";
import {firebaseAuth} from "@genkit-ai/firebase/auth";
import {onFlow} from "@genkit-ai/firebase/functions";
import {geminiPro, textEmbeddingGecko001} from "@genkit-ai/googleai";
import * as z from "zod";
import {defineFirestoreRetriever, firebase} from "@genkit-ai/firebase";
import {googleAI} from "@genkit-ai/googleai";
import { getFirestore } from "firebase-admin/firestore";
import { defineDotprompt } from '@genkit-ai/dotprompt';
import { initializeApp } from "firebase-admin/app";
import { dotprompt } from '@genkit-ai/dotprompt';

initializeApp()

configureGenkit({
plugins: [
dotprompt(),
firebase(),
googleAI(),
],
flowStateStore: 'firebase',
logLevel: 'debug',
traceStore: 'firebase',
enableTracingAndMetrics: true,
});

const firestore = getFirestore();

const retrieverRef = defineFirestoreRetriever({
name: "companyRetriever",
firestore,
collection: "company",
contentField: "text",
vectorField: "embedding",
embedder: textEmbeddingGecko001,
distanceMeasure: "COSINE", // "EUCLIDEAN", "DOT_PRODUCT", or "COSINE" (default)
});


const CompanyQuestionInputSchema = z.object({
data: z.array(z.string()),
question: z.string(),
});

const companyPrompt = defineDotprompt(
{
name: 'companyPrompt',
model: geminiPro,
input: { schema: CompanyQuestionInputSchema },
output: { format: 'text' },
config: { temperature: 0.3 },
},
`
あなたは有限会社ランカードコムの広報担当者です。
以下は有限会社ランカードコムの会社概要です。
{{#each data~}}
- {{this}}
{{~/each}}

会社概要を元に以下のカスタマーの質問に回答してください:
{{question}}?
`
);

export const companyFlow = onFlow(
{
name: "companyFlow",
inputSchema: z.string(),
outputSchema: z.string(),
authPolicy: firebaseAuth((user) => {
if (!user.email_verified) {
throw new Error("Verified email required to run flow");
}
}),
},
async (question) => {
const docs = await retrieve({
retriever: retrieverRef,
query: question,
options: {limit: 5},
});
const llmResponse = await companyPrompt.generate({
input: {
data: docs.map(doc => doc.content[0].text || ''),
question,
},
});

return llmResponse.text();
}
);

export { indexFlow } from './populate_collection';
Genkit Dev UIのフローcompanyFlowでAuth JSONに{"uid": 0,"email_verified": true}を入力し、適当な質問をinput JSONに入力し実行すると、正しく回答が返ってきました。

モデルをOpenAIのGPT-4oに変更

genkitx-openaiを使います。

cd functions
npm i genkitx-openai
- import {geminiPro, textEmbeddingGecko001} from "@genkit-ai/googleai";
- import {googleAI} from "@genkit-ai/googleai";
+ import { gpt4o, openAI, textEmbedding3Small } from "genkitx-openai";
googleAI
をopenAIに、geminiProをgpt4o、textEmbeddingGecko001をtextEmbedding3Smallに置換します。

OpenAIの環境変数をセットし、エミュレータを起動します。

export OPENAI_API_KEY=*************
GENKIT_ENV=dev firebase emulators:start --inspect-functions
indexFlowでインデックス作成し、companyFlowで正しく回答されるか確認します。

ブラウザからFirebase Functionを呼び出し実行

public/index.htmlを以下の内容に書き換えます。
ここの4.Replace public/index.html with the following:を、エミュレータで動作するように修正しました。

<!doctype html>
<html>
<head>
<title>Genkit demo</title>
</head>
<body>
<div id="signin" hidden>
<button id="signinBtn">Sign in with Google</button>
</div>
<div id="callGenkit" hidden>
<div>有限会社ランカードコムについてお聞きください。</div>
<textarea id="question" rows="5" cols="33"></textarea>
<div>
<button id="submit">送信</button>
</div>
<p id="answer"></p>
</div>
<script type="module">
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.10.0/firebase-app.js';
import {
getAuth,
connectAuthEmulator,
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup,
} from 'https://www.gstatic.com/firebasejs/10.10.0/firebase-auth.js';
import {
getFunctions,
httpsCallable,
connectFunctionsEmulator
} from 'https://www.gstatic.com/firebasejs/10.10.0/firebase-functions.js';
import {
getFirestore,
connectFirestoreEmulator
} from 'https://www.gstatic.com/firebasejs/10.10.0/firebase-firestore.js';
const firebaseConfig = await fetch('/__/firebase/init.json');
const firebaseApp = initializeApp(await firebaseConfig.json());

if (location.hostname === 'localhost') {
const auth = getAuth(firebaseApp)
const firestore = getFirestore(firebaseApp)
const functions = getFunctions(firebaseApp)

connectFirestoreEmulator(firestore, location.hostname, 8080, { ssl: false })
connectFunctionsEmulator(functions, location.hostname, 5001)
connectAuthEmulator(auth, `http://${location.hostname}:9099`)
}



async function generateAnswer() {
const companyFlow = httpsCallable(
getFunctions(),
'companyFlow'
);
const question = document.querySelector('#question').value;
const response = await companyFlow(question);
document.querySelector('#answer').innerText = response.data;
}

function signIn() {
signInWithPopup(getAuth(), new GoogleAuthProvider());
}

document
.querySelector('#signinBtn')
.addEventListener('click', signIn);
document
.querySelector('#submit')
.addEventListener('click', generateAnswer);

const signinEl = document.querySelector('#signin');
const genkitEl = document.querySelector('#callGenkit');

onAuthStateChanged(getAuth(), (user) => {
if (!user) {
signinEl.hidden = false;
genkitEl.hidden = true;
} else {
signinEl.hidden = true;
genkitEl.hidden = false;
}
});
</script>
</body>
</html>
http://localhost:5002/にアクセスし、「Sign in with Google」ボタンを押すとエミュレータのAuthenticationにアカウントを作成できます。テキストエリアに質問入力し送信し、回答が返ってくれば成功です。

回答例

課題

ストリームで回答出力

Genkit側ではgenerateStreamでストリーム出力できますが、onFlow(https.onCallのプロトコルに準拠)を使って、ブラウザにどうやって逐次データを返せばいいのか、分かりませんでした。Firestoreにチャンクデータを逐次保存すれば、onSnapshotでストリーミングでデータを返すことはできますが、課金および速度的にいい方法とは思えません。
Firebase Genkitはまだベータ版なので、今後Firebase functionsでストリーミングで返せるようになることを期待しています。
2024/08/01追記
onFlowを使わず、onRequestを使えば、ストリームで回答出力できました。onRequestはHTTPプロトコルなので、サーバー送信イベント (Server-Sent Events、SSE) を使うことができ、これを使いました。ただ作成したdefineFlowをonRequestに書き直すのは面倒です。
onFlowのような感じでonRequestのプロトコルを持つフローを作成するGenkitのプラグインがあれば良いのですが。

murave

Firebase Cloud Functions のNode.js 10更新、わたしの場合

2020年5月某日、

「Node.js 8のFunctionは来年3/15には止まるから10にしてね。あと、10は無料プランじゃ使えないからそこんとこヨロシク」(要約)

というメールがGoogleさんから来ました。

詳しくはその内容を整理してくださってるステキな記事をどうぞ。

【Firebase】Cloud Functionsが従量課金プランでしか使えなくなるらしい
(https://qiita.com/azukiazusa/items/edd6ca9cba2d48c4c3e2)

1年くらい前(2019年3月末)にリリースしたシステムが該当しており放置すると来年の3月には死。
あの時はまだNode.js 8すらベータ版だったやんか。1年でこんなことになるとは…


まずお客様へご説明し、更新作業のコストや時期について調整しなければなりません。

本番プロジェクトは元からBlaze(従量課金)プランですのでその点は問題ありません。
基盤更新への対応作業の発生は想定していましたが発生頻度が初期の想定を超えそうです。

更新コストについては、今回はサービス、以降は作業料をいただく方向でまとまりました。
実施時期については6月中に行うことに(COVID-19の影響で7月になるまで当該システム稼働なし確定のため)

ということでリリース完了して余裕ができたのでこれを書いてるってわけです。

更新作業どうだった?

Functions関係の更新は、最初に驚きのつまづきがありましたが、順調だったと言えます。

開発用プロジェクトをBlazeプランにする

開発用プロジェクトはSpark(無料)プランでしたので変更が必要でした。
今回のシステムの場合、開発プロジェクトでBlazeの無料枠を超えることはまず無いのでとくに気にせず変えちゃいます。

が、この変更を行う場所が(Google Cloud Platformのコンソールで)見つからなくて「あれ〜?」となりました。

FIrebaseコンソールの存在を忘れてた。変だと思った。— murave (@murave) June 11, 2020

最初のつまづきとはコレです。驚きましたね、Firebaseコンソールの存在を忘れていた自分に。

Google Cloud Platformのコンソールでもプロジェクトは見えてますしCloud FunctionsやFirestoreも見えるのですぐには気づかなかったのでした。専用のコンソールがあるFirebaseがイレギュラーですし〜

念の為書いておくと、Firebaseコンソールには以下のURLからアクセスできます。
https://console.firebase.google.com/

Node.js 10 を指定してデプロイする

Node.js 8の関数がデプロイされていると以下のような表示が出るようになってました。

Node.js 10への変更はFunctionsのpackage.json内で “engines”: {“node”: “10”} を指定します。

変更前のpackage.json

{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase serve --only functions -p 5000 -o 0.0.0.0",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"dependencies": {
"firebase-admin": "~7.0.0",
"firebase-functions": "^2.2.0"
},
"private": true,
"engines": {"node": "8"}
}
変更後のpackage.json

{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase serve --only functions -p 5000 -o 0.0.0.0",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"dependencies": {
"firebase-admin": "~7.0.0",
"firebase-functions": "^2.2.0"
},
"private": true,
"engines": {"node": "10"}
}
とりいそぎコレでデプロイしてみる。


ベータ版じゃねーか!(2020/06/24現在、ベータ版表示は消えたようです)

動作確認したところエラーとなりました。残念。

依存パッケージを更新する

前記package.jsonにありますようにfirebase-adminとfirebase-functionsに依存していますのでこれらを更新してみます。

$ npm install firebase-admin@latest
(出力略)
$ npm install firebase-functions@latest
(出力略)
更新後のpackage.json

{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase serve --only functions -p 5000 -o 0.0.0.0",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"dependencies": {
"firebase-admin": "^8.12.1",
"firebase-functions": "^3.7.0"
},
"private": true,
"engines": {"node": "10"}
}
デプロイすると正常に動作しました!

最後に

「メンテナンスコストをさげたくてサーバレスにしたのになぁ」
まぁ、忘れるくらいFirebaseコンソールを使ってなかったんだから成功といえるかな。

しかし

これから更新作業を行うプラットホームのEnd of Lifeが来年4月末だと知ったときの顔をしている— murave (@murave) June 16, 2020

あと、実はついでにクライアントの(一部)依存パッケージ更新やったらハマりまくりでそちらで時間がくわれまくったんですよね(カオスすぎて記事にする気もおきない)

murave

FirestoreのServerTimestampさん

有能!

サーバー側で採時するので集計対象の抽出などに安心して使用できます。
これのおかげで必要なデータ取得数を大幅に削減できています。
Firestoreだとデータ取得数は速度や効率だけでなく課金に直結しますしね。

管理画面でデータを時系列に並べたりするのにも便利なのでドキュメント登録時には必ず入れておいたほうが良いくらいの有能さです。

Webでは

import firebase from '~/plugins/firebase'

payload.createdAt = firebase.firestore.FieldValue.serverTimestamp()
await db.collection('コレクション名').add(payload)

(Functionsなど)node.jsでfirebase-adminを使用する場合は

const admin = require("firebase-admin");

const serviceAccount = require("サービスアカウントの鍵.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});

admin.firestore().collection("コレクション名").doc("ドキュメントのキー").set({
updatedAt: admin.firestore.FieldValue.serverTimestamp()
});
といった感じでサーバー側でTimestampを投入するように指示できます。

でも注意点が。

登録直後にドキュメントを取得するとTimestampが入ってない状態で返ってきます(2019年3月)。await等で完了をまっても、です。
ただしこれはフロントエンドからの取得の話でonCreateで起動されたFunctionsではさすがにちゃんと入っているようです。

フロントエンドで登録直後にデータを取得し(Server)Timestampを利用するには採時されるのを待つ必要があります。タイミングの問題ってことです(;´Д`)

私の場合は操作ログ記録のための変更後のドキュメント取得でしたので、ログ登録という処理の性質上ユーザーから見える必要はないので非同期に500msディレイして取得・登録処理を行うようにしました(lodashのdelayを利用)

なんか記事書いててもホントかよって感じですが。

murave

Firestoreのセキュリティルールのこと

バックエンドをFirebaseでSPAのシステムを作る場合、データを守る砦はデータベース(今回はFirestore)のセキュリティルールです。
Authenticationのログイン情報とFirestore上のユーザー情報を紐付けし権限によりドキュメントへのアクセスを制限するなど、適切な設定しなければなりません。

service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
これは「ログインしているユーザーはフルアクセス」という最低限設定しておくべきルールです。開発中はお世話になりました。
しかし最終的にはもっとこまかく制限をかける必要がありますから、それを前提にデータ構造を設計しておいたほうが良いです。

例として今回作ったシステムのデータ構造とセキュリティルールの一部について解説します。

今回のシステムでは権限による制限が必要だったのでFirestoreにemailをキーとするコレクション、usersを作成し権限情報roleをもたせました。セキュリティルール中でAuthenticationのログイン情報からemailを取得できますので紐づけが可能です。

usersのドキュメント例(権限に関するroleのみ)

murave@hoge.var: {
role: "admin"
}
これを利用してアクセス制限を行います。一部ルールを例示します。

firestore.rules

service cloud.firestore {
match /databases/{database}/documents {
function getUser() {
return get(/databases/$(database)/documents/users/$(request.auth.token.email));
}

match /users/{email} {
allow get: if request.auth != null;
allow read, write: if getUser().data.role == 'admin';
}

match /journals/{journal} {
allow read: if getUser().data.role in ['viewer', 'admin'];
allow create: if request.auth != null;
}

match /summaries/{summary} {
allow read: if request.auth != null;
allow write: if getUser().data.role == 'admin';
}
}
}
この設定は以下の制限・意図となっています。

  • usersはログインしていればドキュメントの(個別)取得は可能。一覧取得、書き込みは管理者(admin)のみ可能。
    ログインしたユーザーが自身の権限情報を取得できる必要がある。
  • ユーザー情報は管理者が管理する。

jornalsへの新規登録はログインしていれば可能。読み出しは閲覧者(viewer)と管理者(admin)が可能。

  • これは集計前の生データなので、ログインしたユーザーすべてが入力する可能性がある。
  • 閲覧者と管理者は登録された生データの分析を行うことができる。

summariesはログインしていれば読み出しは可能。書き込みは管理者(admin)のみが可能。

  • jornalsを集計したデータでユーザーは基本的にはこの集計されたデータを見る。
  • 集計処理はjornalsへのドキュメント新規登録をトリガーとして実行されるFirebase側のFunctionで行われる。
  • 管理者は集計を修正できる。

集計処理はFirebaseのFunctionsを利用しサーバー側に委譲しており、一般・閲覧ユーザーの権限を絞ることが可能となっているのもミソです。

getUser()はAuthenticationのログイン情報のemail(request.auth.token.email)からusersのドキュメントを取得する関数です。内部で使用しているget()等、組み込みの関数には使用回数制限があることに注意してください。同じドキュメントの取得では使用回数は増えないなど独特のルールがあるので意識して、制限を超えないような構造にしなければなりません。

適切なセキュリティルールで安全なFirestoreライフを!

murave

Nuxt.js 悩み解決の小ネタ集

ここ1ヶ月程Nuxt.js + FirebaseでSPAのシステムを作っていました。

短期構築がご要望でリアルタイム性が求められる内容。Firebaseに向いていると判断、提案したところ技術構成は自由にやらせていただけることになりました。
別件で一昨年末あたりからずっとバックエンドはLaravel、フロントエンドは素のVue.js + Vuex + Vue Routerで開発やってるんですが、PWA化したほうがよさそうだったこともあってNuxt.js(以降Nuxt)使ってみようかなってことで採用。

Nuxt、便利ですね。特にプラグインとミドルウェアが素敵!

Firebase(今回使用したのはAuthentication、Firestore、Hosting、Functions)との相性もバッチリでした。

ですが初めてということもありいろいろと悩んだりもするわけで…
それらをどう解決したか?の小ネタ集です(というか今回の落とし所、かな)

cross-envで環境を切り替える

設定ファイルの扱いをどうするか?という悩みです。

バックエンドにFirebaseを使用していることもあり、クライアント側にapiKey等の設定をもたせる必要がありました。当然ながら開発用の設定と本番用の設定は異なります。
開発者の勘を働かせて(大げさ)envというキーワードでモジュールを調査。結果、cross-envを採用しました。

Qiitaにきっちりまとめてくださっている方がいらっしゃったので助かりました

Nuxtでcross-envを使い環境ごとに環境変数を分ける

今回のpackege.jsonのscriptsは以下のように開発・動作確認用Firebaseプロジェクトへのデプロイ用のbuild:staging、顧客練習用のbuild:trainingを追加しています。

"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"build:staging": "cross-env NODE_ENV=\"staging\" nuxt build",
"build:training": "cross-env NODE_ENV=\"training\" nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"precommit": "npm run lint"
},

Nuxtでは通常のnuxt buildを行うとNODE_ENVはproductionになりますのでbuildをそのまま本番用としています。

インスタンスをプラグインで初期化する

ライブラリインスタンスの初期化をどこで行うか?という悩みです。

例えばFirebaseを利用するにはFirebaseプロジェクト用設定での初期化が必要ですが、プラグインの仕組みを利用して初期化するのが常道のようです。

確かに実行タイミング的に適していますね。

plugins/firebase.js

import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'

if (!firebase.apps.length) {
firebase.initializeApp(process.env.firebase)
}
export default firebase
利用側では初期化したインスタンスをimportして利用します。

store/index.js

import firebase from '~/plugins/firebase'

export const strict = false

const db = firebase.firestore()
// 以下略
Vuexの厳格モードをオフにするせっかく用意されている厳格モード、ご指導いただくべき?という悩みです。

Vuexの厳格モードは人類には厳しすぎる!というのが私の感想です。

前掲の利用側コードにありますがVuexの厳格モードは export const strict = false でオフにしました(開発中も)

ドキュメントに「本番環境で厳格モードを有効にしてデプロイしてはいけません! 」とあることですし、気にしない。

問題なく開発できる方は厳格モードを活用してください。

共通処理をプラグイン化する

同じ処理をバラバラ書くのは避けたいもの。どこにまとめるべき?という悩みです。

プラグインに落ち着きました。さまざまなインジェクションも可能ですし、いろいろな場所で使う処理をあつかうのに便利です。他で再利用することは無いようなプロジェクト独自の処理もプラグインに書いています。

plugins/forApp.js

import Vue from 'vue'
import { cloneDeep } from 'lodash'
import firebase from '~/plugins/firebase'

const forApp = {
testA: function(a) {
return a
},
testB: function() {
return this.testA('b')
}
}
Vue.prototype.$forApp = forApp
export default forApp
のような感じでインジェクションとエクスポートを行えばvueファイル内ではthis.$forApp.testA(‘ええじゃないか’)みたいに使えますし、前掲のfirebase.jsのようにimportして使用することもできます。

こんなもんかな。
もっと良い方法があったら教えてくださいね。