如何为Firebase的Cloud Functions构建结构以从多个文件部署多个函数?

228
我想为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了吗?我遇到了问题:https://stackoverflow.com/questions/50089807/firebase-cloud-functions-functions-predeploy-error-when-structuring-multiple-f - tccpg288
4
这个官方的Firebase函数示例包含多个.js文件,通过require导入: https://github.com/firebase/functions-samples/tree/master/isomorphic-react-app/functions - AlexGrafe
这可能会有所帮助:https://dev59.com/91cQ5IYBdhLWcg3wA_BA - Ramesh-X
19个回答

146
啊,Firebase的云函数通常会加载Node模块,所以这个可以工作。
结构:
/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模块中有几个函数吗?如果可以,最好如何实现? - Alexander Khitev
1
我想你可以为foo的不同导出函数分配不同的处理程序:exports.bar = functions.database.ref('/foo').onWrite(fooModule.barHandler); exports.baz = functions.database.ref('/bar').onWrite(fooModule.bazHandler); - jasonsirota
69
我不喜欢这个解决方案,因为它将信息(即数据库路径)从foo.js和bar.js移动到index.js中,这有点违背了将这些文件分开的初衷。 - bvs
我同意@bvs的观点,我认为Ced的方法很好。我会稍微修改一下,通过显式地导出每个模块来使index.ts更加清晰,例如:export {newUser} from "./authenticationFunctions"。 - Alan
2
我认为我的最初问题只是关于在一个项目中部署多个函数而不将这些函数放在index.js文件中,数据库信息的传递方式和位置不在讨论范围内。如果是我,我可能会创建一个单独的模块来控制数据库访问,并分别在foo.js和bar.js中引用它,但这是一种风格上的决定。 - jasonsirota
这个有官方文档支持,其中的分组功能部分可能也会对你有所帮助,可以在这里查看:https://firebase.google.com/docs/functions/organize-functions - sarah

99

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'); 
}

1
目前在index.js中的结构对我来说不太好用。我所做的是首先导入firebase模块,然后初始化应用程序,最后从其他文件夹中导入函数。这样我的应用程序首先进行初始化、身份验证等操作,然后再导入需要事先初始化应用程序的函数。 - tonkatata
1
我觉得应该有更好的方法将函数文件连接到index.js中吧?目前手动连接的方式似乎很麻烦。 - Zorayr

67
注意:这是对一个旧问题的旧回答,而且是在更早的时候。到了2023年,很多事情已经发生了变化。
以下是我个人使用TypeScript的方法:
    /functions
       |--src
          |--index.ts
          |--http-functions.ts
          |--main.ts
          |--db.ts
       |--package.json
       |--tsconfig.json

让我先说明两个警告,以确保这个工作正常进行:
  1. index.ts 中,导入/导出的顺序很重要。
  2. 数据库必须是一个单独的文件。
关于第二点,我不确定为什么。其次,你应该完全遵守我的 index、main 和 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 配置文件放在一个目录下(默认情况下是公开的)。不过因为已经过去一年多了,我的记忆可能有误。 - Ced
嘿@ced - 为什么db.ts的内容不能放在main.ts中(在管理实例化之后)?还是您只是为了清晰/简单而将其分割出来? - dsg38
1
@dsg38 这个帖子发布得太久了,我现在看不出为什么它应该放在一个单独的文件中.. 我认为这是为了清晰明了。 - Ced
我们如何在同一个文件夹中使用 TypeScript 和 JavaScript 函数?我不得不创建两个不同的文件夹(一个用于 JavaScript,另一个用于 TypeScript),然后进行 Firebase 初始化等操作。有没有更好的方法来处理这个问题? - Lalit Rane

30

现在,随着云端/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);
  });

3
进口商品的不断增加是否会减缓每个函数的冷启动,或者是否应该开发许多完全独立的模块? - Simon Fakir
3
我在index.js里面遇到了一个eslint的解析错误unexpected token ... - dcts
@SimonFakir 很好的问题。你有找到相关信息吗? - atereshkov
@atereshkov 是的,我找到了一种方法,可以使用“process.env.FUNCTION_NAME”仅加载请求的函数及其依赖项。类似于下面的答案。如果您有兴趣,我还可以分享我的存储库作为参考。 - Simon Fakir
对于那些遇到“unexpected token”的人,可以将以下内容添加到生成的.eslintrc文档中:“parserOptions”:{“ecmaVersion”:6,“ecmaFeatures”:{“experimentalObjectRestSpread”:true}},并使用生成的版本更新ecmaVersion。 - Axes Grinds
显示剩余3条评论

23

为了保持简单(但有效),我个人将我的代码组织成这样。

布局

├── /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.
}

bar.ts

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

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

db.ts

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

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

index.ts

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的回答。


2
这是Typescript最简单的答案之一,谢谢。例如,如何处理firebase数据库的单个实例?admin.initializeApp(functions.config().firestore) const db = admin.firestore();你把它放在哪里,如何在foo和bar中引用它? - elprl
嘿 - 为什么不能将 db.ts 的内容放在 index.ts 中(在管理员实例化之后)?或者你只是为了清晰/简单而这样拆分的吗? - dsg38
1
@dsg38 你可以把它们混合在一起,这样会更清晰明了。 - Reza
我之前在TS中做过类似的事情,谢谢,这是一个简单而好的解决方案。 - Ruben

15

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();

播客.js

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

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

profile文件中,removeProfile方法将使用相同的模式。


14

对于使用 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

11

为使代码简洁易读,我个人将代码结构如下。

布局

├── /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.
}

bar.ts

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

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

index.ts

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

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

适用于任何嵌套级别的目录。只需按照目录内部的模式进行操作即可。


我看不出这怎么可能行得通,因为Firebase目前支持的是Node 6.11,而它并不支持ES6的导入指令。 - Aodh
如果您正在使用 TypeScript,这个问题不应该出现。最近我已经将我的大部分代码转换成了 TypeScript。 - zaidfazil
2
zaidfazil,你应该在回答中注明任何先决条件。 @Aodh,如果你按照Konstantin在一个答案中概述的方式使用Babel,它就可以工作。https://dev59.com/91cQ5IYBdhLWcg3wA_BA - PostureOfLearning
1
谢谢。这个在 TypeScript 和 Node 6 中可行 :) - Ahmad Moussa
4
在index.ts文件中,你可以使用export * from './fooFunctions';export * from './barFunctions';来代替使用spread操作符进行导入和重新导出的操作。这样做可以使代码更加简洁易懂。 - whatsthatitspat
当时没有考虑到这一点。那真是个好主意。 - zaidfazil

9

Firebase文档现已更新,提供了一个很好的多文件代码组织指南:

文档 > 云函数 > 编写函数 > 组织函数

总结如下:

foo.js

const functions = require('firebase-functions');
exports.foo = functions.https.onRequest((request, response) => {
  // ...
});

bar.js

const functions = require('firebase-functions');
exports.bar = functions.https.onRequest((request, response) => {
  // ...
});

index.js

const foo = require('./foo');
const bar = require('./bar');
exports.foo = foo.foo;
exports.bar = bar.bar;

1
你有没有找到一种更好的方法来导入 index.js 中的导出,而不是手动连线每个单独的文件? - Zorayr

6

这种格式可以让你的入口点找到其他函数文件,并自动导出每个文件中的每个函数。

主入口脚本

查找 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接