我想为Firebase创建多个云功能,并从一个项目同时部署它们。我还想将每个函数分离到一个单独的文件中。目前,我可以创建多个函数,如果我把它们都放在index.js,如:

exports.foo = functions.database.ref('/foo').onWrite(event => {
    ...
});

exports.bar = functions.database.ref('/bar').onWrite(event => {
    ...
});

然而,我想把foo和酒吧在单独的文件。我试了一下:

/functions
|--index.js (blank)
|--foo.js
|--bar.js
|--package.json

foo.js在哪里

exports.foo = functions.database.ref('/foo').onWrite(event => {
    ...
});

bar.js是

exports.bar = functions.database.ref('/bar').onWrite(event => {
    ...
});

有没有一种方法可以在不把所有函数都放在index.js中的情况下实现这一点?


当前回答

我使用一个普通的JS引导加载器来自动包含我想使用的所有函数。

├── /functions
│   ├── /test/
│   │   ├── testA.js
│   │   └── testB.js
│   ├── index.js
│   └── package.json

index.js(引导)

/**
 * The bootloader reads all directories (single level, NOT recursively)
 * to include all known functions.
 */
const functions = require('firebase-functions');
const fs = require('fs')
const path = require('path')

fs.readdirSync(process.cwd()).forEach(location => {
  if (!location.startsWith('.')) {
    location = path.resolve(location)

    if (fs.statSync(location).isDirectory() && path.dirname(location).toLowerCase() !== 'node_modules') {
      fs.readdirSync(location).forEach(filepath => {
        filepath = path.join(location, filepath)

        if (fs.statSync(filepath).isFile() && path.extname(filepath).toLowerCase() === '.js') {
          Object.assign(exports, require(filepath))
        }
      })
    }
  }
})

这个例子index.js文件只在根目录中自动包含目录。它可以扩展到walk目录,honor .gitignore等。不过这对我来说已经足够了。

有了索引文件,添加新函数就很简单了。

/测试/ testA.js

const functions = require('firebase-functions');

exports.helloWorld = functions.https.onRequest((request, response) => {
 response.send("Hello from Firebase!");
});

/测试/ testB.js

const functions = require('firebase-functions');

exports.helloWorld2 = functions.https.onRequest((request, response) => {
 response.send("Hello again, from Firebase!");
});

NPM运行服务产生:

λ ~/Workspace/Ventures/Author.io/Firebase/functions/ npm run serve

> functions@ serve /Users/cbutler/Workspace/Ventures/Author.io/Firebase/functions
> firebase serve --only functions


=== Serving from '/Users/cbutler/Workspace/Ventures/Author.io/Firebase'...

i  functions: Preparing to emulate functions.
Warning: You're using Node.js v9.3.0 but Google Cloud Functions only supports v6.11.5.
✔  functions: helloWorld: http://localhost:5000/authorio-ecorventures/us-central1/helloWorld
✔  functions: helloWorld2: http://localhost:5000/authorio-ecorventures/us-central1/helloWorld2

这个工作流程基本上就是“编写并运行”,而不必在每次添加/修改/删除新函数/文件时修改index.js文件。

其他回答

我花了很多时间寻找同样的东西,我认为这是实现它的最佳方法(我使用firebase@7.3.0):

https://codeburst.io/organizing-your-firebase-cloud-functions-67dc17b3b0da

毫不费力;)

在Babel/Flow的情况下,它看起来像这样:

目录布局

.
├── /build/                     # Compiled output for Node.js 6.x
├── /src/                       # Application source files
│   ├── db.js                   # Cloud SQL client for Postgres
│   ├── index.js                # Main export(s)
│   ├── someFuncA.js            # Function A
│   ├── someFuncA.test.js       # Function A unit tests
│   ├── someFuncB.js            # Function B
│   ├── someFuncB.test.js       # Function B unit tests
│   └── store.js                # Firebase Firestore client
├── .babelrc                    # Babel configuration
├── firebase.json               # Firebase configuration
└── package.json                # List of project dependencies and NPM scripts

src/index.js -主要导出

export * from './someFuncA.js';
export * from './someFuncB.js';

src/db.js - Postgres云SQL客户端

import { Pool } from 'pg';
import { config } from 'firebase-functions';

export default new Pool({
  max: 1,
  user: '<username>',
  database: '<database>',
  password: config().db.password,
  host: `/cloudsql/${process.env.GCP_PROJECT}:<region>:<instance>`,
});

src/store.js - Firebase Firestore客户端

import firebase from 'firebase-admin';
import { config } from 'firebase-functions';

firebase.initializeApp(config().firebase);

export default firebase.firestore();

src/someFuncA.js -函数A

import { https } from 'firebase-functions';
import db from './db';

export const someFuncA = https.onRequest(async (req, res) => {
  const { rows: regions } = await db.query(`
    SELECT * FROM regions WHERE country_code = $1
  `, ['US']);
  res.send(regions);
});

src/someFuncB.js

import { https } from 'firebase-functions';
import store from './store';

export const someFuncB = https.onRequest(async (req, res) => {
  const { docs: regions } = await store
    .collection('regions')
    .where('countryCode', '==', 'US')
    .get();
  res.send(regions);
});

.babelrc

{
  "presets": [["env", { "targets": { "node": "6.11" } }]],
}

firebase.json

{
  "functions": {
    "source": ".",
    "ignore": [
      "**/node_modules/**"
    ]
  }
}

package.json

{
  "name": "functions",
  "verson": "0.0.0",
  "private": true,
  "main": "build/index.js",
  "dependencies": {
    "firebase-admin": "^5.9.0",
    "firebase-functions": "^0.8.1",
    "pg": "^7.4.1"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-jest": "^22.2.2",
    "babel-preset-env": "^1.6.1",
    "jest": "^22.2.2"
  },
  "scripts": {
    "test": "jest --env=node",
    "predeploy": "rm -rf ./build && babel --out-dir ./build src",
    "deploy": "firebase deploy --only functions"
  }
}

$ yarn install                  # Install project dependencies
$ yarn test                     # Run unit tests
$ yarn deploy                   # Deploy to Firebase

我也在为云函数寻找最佳的文件夹结构,所以我决定分享我的想法:

+  /src
|    - index.ts
|    + /events
|    |    - moduleA_events.ts
|    |    - moduleB_events.ts
|    + /service
|    |    - moduleA_services.ts
|    |    - moduleB_services.ts
|    + /model
|    |    - objectA.ts
|    |    - objectB.ts
|    |    - objectC.ts

/ src /索引。Ts此文件作为应用程序中所有可用事件(函数)的入口点,如数据库事件,HTTPS请求,计划函数。然而,函数不是直接在index.js中声明的,而是在事件文件夹indead中声明的。代码示例: 出口。user = require("./events/userEvents") 出口。order = require("./events/orderEvents") 出口。product = require("./events/productEvents")

注意:根据GCF官方文档,这种方法会自动将所有函数重命名为“模块-函数”模式。示例:如果在userEvents中有"userCreated"函数。ts, firebase将重命名此函数为"user-userCreated"

/src/events this folder should only contain cloud functions declarations and should not handle business logic directly. For the actual business, you should call custom functions from your /service folder (which maps the same modules as in the events folder). Code sample for userEvents.ts: exports.userCreated = functions.firestore.document("/users/{documentId}").onCreate(async (snapshot) => { userServices.sendWelcomeEmail() } /src/service the actual busienss logic that will connect with other firebase services such as firestore, storage, auth. You can also import your /model layer here (typescript only). /src/model the interfaces used in typescript to ensure strong typed functions and objects.

正如您所注意到的,这种方法主要基于MVC和OOP原则。有很多关于我们是否应该在无服务器环境中使用函数式编程的争论。由于我的后台背景是Java和c#,我在这里介绍的文件夹结构对我来说似乎更自然,然而,我很想知道当转向函数式编程方法时,这种文件夹结构会有什么不同。

以上的答案为我指明了正确的方向,只是没有一个真正适合我。下面是一个工作原型,一个onCall, onRequest和数据库触发器的例子

foo.js - 随叫随到,随叫随到。

exports.handler = async function(data, context, admin) {
    // const database = admin.database();
    // const firestore = admin.firestore();
    //...
};

bar.js - onRequest

exports.handler = async function(req, res, admin) {
    // const database = admin.database();
    // const firestore = admin.firestore();
    //...
};

jar.js - trigger/document/onCreate .js

exports.handler = async function(snapshot, context, admin) {
    // const database = admin.database();
    // const firestore = admin.firestore();
    //...
};

index.js

//导入firebase管理SDK依赖项

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase); 

// import functions
const foo = require("./foo");
const bar = require("./bar");
const jar = require("./jar");

// onCall for foo.js
exports.foo = functions.https.onCall((data, context) => {
    return foo.handler(data, context, admin);
});

// onRequest for bar.js
exports.bar = functions.https.onRequest((req, res) => {
    return bar.handler(req, res, admin);
});

// document trigger for jar.js
exports.jar = functions.firestore
  .document("parentCollection/{parentCollectionId}")
  .onCreate((snapshot, context) => {
    return jar.handler(snapshot, context, admin);
});

注意:你也可以创建一个子文件夹来存放你的各个函数

我有这个项目,它有后台函数和http函数。我还有用于单元测试的测试。CI/CD将使您在部署云功能时更加轻松

文件夹结构

|-- package.json
|-- cloudbuild.yaml
|-- functions
    |-- index.js
    |-- background
    |   |-- onCreate
    |       |-- index.js
            |-- create.js
    |
    |-- http
    |   |-- stripe
    |       |-- index.js
    |       |-- payment.js
    |-- utils
        |-- firebaseHelpers.js
    |-- test
        |-- ...
    |-- package.json

注意:utils/文件夹用于在函数之间共享代码

函数/ index.js

在这里,您可以导入所需的所有函数并声明它们。这里不需要逻辑。在我看来,这样更干净。

require('module-alias/register');
const functions = require('firebase-functions');

const onCreate = require('@background/onCreate');
const onDelete = require('@background/onDelete');
const onUpdate = require('@background/onUpdate');

const tours  = require('@http/tours');
const stripe = require('@http/stripe');

const docPath = 'tours/{tourId}';

module.exports.onCreate = functions.firestore.document(docPath).onCreate(onCreate);
module.exports.onDelete = functions.firestore.document(docPath).onDelete(onDelete);
module.exports.onUpdate = functions.firestore.document(docPath).onUpdate(onUpdate);

module.exports.tours  = functions.https.onRequest(tours);
module.exports.stripe = functions.https.onRequest(stripe);

CI / CD

每次将更改推送到回购时都进行持续集成和部署如何?你可以使用谷歌谷歌云构建。它是免费的,直到某个点:)检查这个链接。

。/ cloudbuild.yaml

steps:
  - name: "gcr.io/cloud-builders/npm"
    args: ["run", "install:functions"]
  - name: "gcr.io/cloud-builders/npm"
    args: ["test"]
  - name: "gcr.io/${PROJECT_ID}/firebase"
    args:
      [
        "deploy",
        "--only",
        "functions",
        "-P",
        "${PROJECT_ID}",
        "--token",
        "${_FIREBASE_TOKEN}"
      ]

substitutions:
    _FIREBASE_TOKEN: nothing