如何为Firebase构建Cloud Functions以从多个文件部署多个功能?


163

我想为Firebase创建多个Cloud Functions,并从一个项目同时部署所有这些功能。我还想将每个函数分成一个单独的文件。目前,如果我将两个函数都放在index.js中,则可以创建多个函数,例如:

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

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

但是我想把foo和bar放在单独的文件中。我尝试了这个:

/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的情况下完成此操作?


1
@JPVentura。真的不太了解你。请解释。
HuyLe

此版本是否已针对v1.0更新?我有问题:stackoverflow.com/questions/50089807/...
tccpg288

2
仅供参考,这个官方的Firebase函数示例包含.js通过以下文件导入的几个文件requiregithub.com/firebase/functions-samples/tree/master/…–
xanderiel

Answers:


126

嗯,Firebase的Cloud Functions负载节点模块正常运行,因此可以正常工作

结构体:

/functions
|--index.js
|--foo.js
|--bar.js
|--package.json

index.js:

const functions = require('firebase-functions');
const fooModule = require('./foo');
const barModule = require('./bar');

exports.foo = functions.database.ref('/foo').onWrite(fooModule.handler);
exports.bar = functions.database.ref('/bar').onWrite(barModule.handler);

foo.js:

exports.handler = (event) => {
    ...
};

bar.js:

exports.handler = (event) => {
    ...
};

1
例如,我可以在foo模块中具有几个功能吗?如果是这样,如何更好地实施呢?
亚历山大·基特夫

1
我想可以,并为foo的不同导出函数分配不同的处理程序:exports.bar = functions.database.ref('/ foo')。onWrite(fooModule.barHandler); Exports.baz = functions.database.ref('/ bar')。onWrite(fooModule.bazHandler);
jasonsirota'5

44
我不喜欢这种解决方案,因为它将信息(即数据库路径)从foo.js和bar.js移到index.js中,这使拥有这些单独文件的观点大失所望。
bvs

我同意@bvs,我认为Ced有很好的方法。我将通过显式导出每个模块以使index.ts超级清晰来对其进行稍微修改,例如,从“ ./authenticationFunctions”中导出{newUser}
Alan

2
我认为我最初的问题只是关于使用1个项目部署多个功能而不将这些功能放在index.js文件中,在何处以及如何传递数据库信息不在范围之内。如果是我,我可能会创建一个单独的模块来控制数据库访问,并分别在foo.js和bar.js中要求它,但这是一个风格决定。
jasonsirota

75

@jasonsirota的回答非常有帮助。但是,查看更详细的代码可能很有用,尤其是在使用HTTP触发功能的情况下。

使用与@jasonsirota答案相同的结构,可以说您希望在两个不同的文件中具有两个单独的HTTP触发函数:

目录结构:

    /functions
       |--index.js
       |--foo.js
       |--bar.js
       |--package.json`

index.js:

'use strict';
const fooFunction = require('./foo');
const barFunction = require('./bar');

// Note do below initialization tasks in index.js and
// NOT in child functions:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase); 
const database = admin.database();

// Pass database to child functions so they have access to it
exports.fooFunction = functions.https.onRequest((req, res) => {
    fooFunction.handler(req, res, database);
});
exports.barFunction = functions.https.onRequest((req, res) => {
    barFunction.handler(req, res, database);
});

foo.js:

 exports.handler = function(req, res, database) {
      // Use database to declare databaseRefs:
      usersRef = database.ref('users');
          ...
      res.send('foo ran successfully'); 
   }

bar.js:

exports.handler = function(req, res, database) {
  // Use database to declare databaseRefs:
  usersRef = database.ref('users');
      ...
  res.send('bar ran successfully'); 
}

index.js中的当前结构对我而言效果不佳。我要做的是先导入firebase模块,然后初始化应用程序,然后再从其他文件夹导入功能。这样,我的应用程序将首先进行初始化,身份验证等操作,然后导入需要预先初始化应用程序的功能。
tonkatata

47

更新:该文档应该有所帮助,我的答案比该文档还旧。


这是我个人使用打字稿的方式:

/functions
   |--src
      |--index.ts
      |--http-functions.ts
      |--main.js
      |--db.ts
   |--package.json
   |--tsconfig.json

让我通过发出两个警告来开始这项工作:

  1. 索引中的导入/导出顺序很重要
  2. 数据库必须是一个单独的文件

对于第二点,我不确定为什么。Secundo你应该尊重我的指标,主要和DB配置完全相同(至少尝试一下)。

index.ts:处理出口。我发现让index.ts处理出口更干净。

// main must be before functions
export * from './main';
export * from "./http-functions";

main.ts:处理初始化。

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

initializeApp(config().firebase);
export * from "firebase-functions";

db.ts:仅重新导出数据库,因此其名称比database()

import { database } from "firebase-admin";

export const db = database();

http-functions.ts

// db must be imported like this
import { db } from './db';
// you can now import everything from index. 
import { https } from './index';  
// or (both work)
// import { https } from 'firebase-functions';

export let newComment = https.onRequest(createComment);

export async function createComment(req: any, res: any){
    db.ref('comments').push(req.body.comment);
    res.send(req.body.comment);
}

您的tsconfig是什么样的?如何编译到dist文件夹中,并让gcloud函数知道我的index.js在哪里?您在github上有代码吗?:)
bersling

@ choopage-JekBao对不起,已经很长时间了,我没有这个项目了。如果我没记错的话,您可以为firebase config提供一个目录(默认情况下为公共目录)。我可能是错的,因为已经过去一年多了
-Ced

嘿@ced-为什么内容不能db.ts进入内部main.ts(在管理员实例化之后?)。还是您只是为了清晰/简单而分开?
dsg38 '19

1
@ dsg38这种被张贴不久之前,我真的不明白为什么它应该是在一个单独的文件在回答现在正在寻找。我认为这是为了清晰
CED

21

现在,借助Cloud / Firebase功能提供的Node 8 LTS,您可以对传播算子进行以下操作:

/package.json

"engines": {
  "node": "8"
},

/index.js

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

module.exports = {
  ...require("./lib/foo.js"),
  // ...require("./lib/bar.js") // add as many as you like
};

/lib/foo.js

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

exports.fooHandler = functions.database
  .ref("/food/{id}")
  .onCreate((snap, context) => {
    let id = context.params["id"];

    return admin
      .database()
      .ref(`/bar/${id}`)
      .set(true);
  });

我想知道是否增加进口的数量会使每个功能的冷启动速度减慢,还是应该分开开发许多完全分开的模块?
西蒙·法基尔

2
unexpected token ...在index.js中收到eslint分离错误。
托马斯,

也许您没有使用Node 8
Luke Pighetti '19

@SimonFakir好问题。你找到了什么吗?
atereshkov

@atereshkov是的,我找到了一种使用“ process.env.FUNCTION_NAME”仅加载请求的函数(包括其依赖项)的方法,类似于下面的答案。如果您有兴趣,请与我联系,以供参考。
西蒙·法基尔

15

为了简单起见(但确实可行),我亲自设计了这样的代码。

布局

├── /src/                      
   ├── index.ts               
   ├── foo.ts           
   ├── bar.ts
|   ├── db.ts           
└── package.json  

foo.ts

import * as functions from 'firebase-functions';
export const fooFunction = functions.database()......... {
    //do your function.
}

export const someOtherFunction = functions.database().......... {
    // do the thing.
}

巴特

import * as functions from 'firebase-functions';
export const barFunction = functions.database()......... {
    //do your function.
}

export const anotherFunction = functions.database().......... {
    // do the thing.
}

数据库

import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

export const firestore = admin.firestore();
export const realtimeDb = admin.database();

索引

import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

admin.initializeApp(functions.config().firebase);
// above codes only needed if you use firebase admin

export * from './foo';
export * from './bar';

适用于任何嵌套级别的目录。只需遵循目录内的模式即可。

归功于@zaidfazil答案


1
这是打字稿最简单的答案之一,谢谢。例如,您如何处理firebase数据库的单个实例?admin.initializeApp(functions.config().firestore) const db = admin.firestore();您将其放在哪里以及如何在foo和bar中引用它?
elprl

嘿-为什么db.ts里面的内容不能进入index.ts(在管理员实例化之后?)。还是您只是为了清晰/简单而分开?
dsg38 '19

@ dsg38,您可以将它们混合在一起,这很清楚
Reza

10

如果使用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的Cloud 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 -功能B

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

9

bigcodenerd.org轮廓的,以便有方法更简单的架构模式分成不同的文件,并在出口一线的内index.js文件。

此样本中项目的体系结构如下:

projectDirectory

  • index.js
  • podcast.js
  • profile.js

index.js

const admin = require('firebase-admin');
const podcast = require('./podcast');
const profile = require('./profile');
admin.initializeApp();

exports.getPodcast = podcast.getPodcast();
exports.removeProfile = profile.removeProfile();

podcast.js

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

exports.getPodcast = () => functions.https.onCall(async (data, context) => {
      ...
      return { ... }
  });

概要文件中的removeProfile方法将使用相同的模式。


7

为了简单起见(但确实可行),我亲自设计了这样的代码。

布局

├── /src/                      
   ├── index.ts               
   ├── foo.ts           
   ├── bar.ts           
└── package.json  

foo.ts

export const fooFunction = functions.database()......... {
    //do your function.
}

export const someOtherFunction = functions.database().......... {
    // do the thing.
}

巴特

export const barFunction = functions.database()......... {
    //do your function.
}

export const anotherFunction = functions.database().......... {
    // do the thing.
}

索引

import * as fooFunctions from './foo';
import * as barFunctions from './bar';

module.exports = {
    ...fooFunctions,
    ...barFunctions,
};

适用于任何嵌套级别的目录。只需遵循目录内的模式即可。


我看不到这可能如何工作,因为Firebase当前支持不支持ES6导入指令的Node 6.11?
Aodh

如果您使用的是打字稿,则永远不会出现此问题。我最近将大部分代码移植到了打字稿中。
zaidfazil

2
zaidfazil,您可能应该记下答案中的所有先决条件。@Aodh,如果您按照Konstantin在答案中概述的方式使用Babel,它会起作用。stackoverflow.com/questions/43486278/...
PostureOfLearning

1
谢谢。这适用于打字稿和节点6 :)
Ahmad Moussa

4
而不是进口和转口与传播经营者,不能你只需要export * from './fooFunctions';export * from './barFunctions';在index.ts?
whatsthatitspat

5

这种格式使您的入口点可以查找其他功能文件,并自动导出每个文件中的每个功能。

主入口点脚本

在functions文件夹中查找所有.js文件,并导出从每个文件导出的每个函数。

const fs = require('fs');
const path = require('path');

// Folder where all your individual Cloud Functions files are located.
const FUNCTIONS_FOLDER = './scFunctions';

fs.readdirSync(path.resolve(__dirname, FUNCTIONS_FOLDER)).forEach(file => { // list files in the folder.
  if(file.endsWith('.js')) {
    const fileBaseName = file.slice(0, -3); // Remove the '.js' extension
    const thisFunction = require(`${FUNCTIONS_FOLDER}/${fileBaseName}`);
    for(var i in thisFunction) {
        exports[i] = thisFunction[i];
    }
  }
});

从一个文件导出多个功能的示例

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

const query = functions.https.onRequest((req, res) => {
    let query = req.query.q;

    res.send({
        "You Searched For": query
    });
});

const searchTest = functions.https.onRequest((req, res) => {
    res.send({
        "searchTest": "Hi There!"
    });
});

module.exports = {
    query,
    searchTest
}

http可访问的端点已适当命名

✔ functions: query: http://localhost:5001/PROJECT-NAME/us-central1/query
✔ functions: helloWorlds: http://localhost:5001/PROJECT-NAME/us-central1/helloWorlds
✔ functions: searchTest: http://localhost:5001/PROJECT-NAME/us-central1/searchTest

一个文件

如果只有几个其他文件(例如只有一个),则可以使用:

const your_functions = require('./path_to_your_functions');

for (var i in your_functions) {
  exports[i] = your_functions[i];
}


每个启动的函数实例在启动时是否都会过载?
Ayyappa,

4

因此,我有一个具有后台功能和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

每次将更改推送到存储库时,进行持续的集成和部署怎么样?您可以使用google google cloud build拥有它。它是免费的,直到特定时间为止:)检查此链接

./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

我已经按照您所说的导出了,但是firebase部署检测到了最后一个,例如:根据您的代码,它只使用module.exports.stripe = functions.https.onRequest(stripe);
OK200

@ OK200您在firebase命令行中使用的命令是什么?为了帮助您,我需要看一些代码
ajorquera

3

从长远来看,有一种很好的方法来组织所有云功能。我最近做了这个,它运行正常。

我所做的就是根据触发器的端点将每个云功能组织在单独的文件夹中。每个云函数文件名都以结尾*.f.js。例如,如果您有onCreateonUpdate触发器,user/{userId}/document/{documentId}则在目录中创建两个文件onCreate.f.js,然后函数将分别命名为和。(1)onUpdate.f.jsfunctions/user/document/userDocumentOnCreateuserDocumentOnUpdate

这是一个示例目录结构:

functions/
|----package.json
|----index.js
/----user/
|-------onCreate.f.js
|-------onWrite.f.js
/-------document/
|------------onCreate.f.js
|------------onUpdate.f.js
/----books/
|-------onCreate.f.js
|-------onUpdate.f.js
|-------onDelete.f.js

样例功能

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const db = admin.database();
const documentsOnCreate = functions.database
    .ref('user/{userId}/document/{documentId}')
    .onCreate((snap, context) => {
        // your code goes here
    });
exports = module.exports = documentsOnCreate;

Index.js

const glob = require("glob");
const camelCase = require('camelcase');
const admin = require('firebase-admin');
const serviceAccount = require('./path/to/ServiceAccountKey.json');
try {
    admin.initializeApp({ credential: admin.credential.cert(serviceAccount),
    databaseURL: "Your database URL" });
} catch (e) {
    console.log(e);
}

const files = glob.sync('./**/*.f.js', { cwd: __dirname });
for (let f = 0, fl = files.length; f < fl; f++) {
    const file = files[f];
    const functionName = camelCase(file.slice(0, -5).split('/')); 
    if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === functionName) {
        exports[functionName] = require(file);
      }
}

(1):您可以使用任何想要的名称。对我来说,onCreate.f.js,onUpdate.f.js等似乎与它们所触发的类型更相关。


1
这种方法真的很好。我想知道是否可以调整以允许在函数名称中使用斜杠,以便您可以分隔不同的api版本,例如(api v1,api v2等)
Alex Sorokoletov

您为什么要在同一项目下保留不同版本的云功能?尽管您可以通过稍微改变目录结构来做到这一点,但是默认情况下,除非您有选择地部署或在index.js中使用if条件,否则index.js会部署所有云功能,最终会导致代码混乱
krhitesh

1
我可以部署所有内容,我只想对我放置的功能进行版本控制(http触发的功能)
Alex Sorokoletov

我期望每个http触发器都在其自己的*.f.js文件中。您至少可以做的就是为每个版本重命名文件,方法是在文件名后面加上类似*.v1.f.js或诸如此类的后缀*.v2.f.js(假设所有http触发器的所有版本都处于活动状态)。如果您有更好的解决方案,请告诉我。
krhitesh

1

我使用香草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文件仅在根目录内自动包含目录。它可以扩展为遍历目录,荣誉.gitignore等。尽管这对我来说足够了。

有了索引文件后,添加新功能就很简单了。

/test/testA.js

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

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

/test/testB.js

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

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

npm run serve 产量:

λ ~/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文件。


这不会是冷启动吗?
Ayyappa,

1

如果您要使用打字稿创建云函数,这是一个简单的答案。

/functions
|--index.ts
|--foo.ts

在顶部,几乎所有常规导入都仅导出的所有功能foo.ts

export * from './foo';


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.