好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

使用 Node.js (TypeScript) 和 Azure Functions 创建无服务器 R

介绍

在本文中,我们将使用 azure 函数和 ComsmosDB 创建一个无服务器 REST API 应用程序。使用的语言是 Node.js/TypeScript。此外,由于我们使用 CosmosDB 作为数据库,因此我们将使用 Azure 函数触发器接收 Change Feed 事件作为奖励。

创建资源

创建资源组

 az group create   
         --name   ${  AZ_RESOURCE_GROUP  }    
         --location   ${  AZ_RESOURCE_GROUP_LOCATION  } 
 

创建 Azure Function App 资源

在创建 Azure Function App 资源之前,我们需要创建一个 Azure 存储帐户来支持 blob、队列和表存储。

 az storage account create   
     --name   ${  AZ_STORAGE_ACCOUNT  }    
     --location   ${  AZ_RESOURCE_GROUP_LOCATION  }    
     --resource-group   ${  AZ_RESOURCE_GROUP  }    
     --sku  Standard_LRS
 

将上面创建的 Azure 存储帐户的名称作为参数传递给 --storage-account ,以创建 Azure Function App 资源。

 az functionapp create   
     --name   ${  AZ_FUNCTION  }    
     --storage-account   ${  AZ_STORAGE_ACCOUNT  }    
     --consumption-plan-location   ${  AZ_RESOURCE_GROUP_LOCATION  }    
     --resource-group   ${  AZ_RESOURCE_GROUP  }    
     --functions-version  4   
     --runtime  node
 

创建 CosmosDB 资源

创建 CosmosDB 资源。始终将 --enable-free-tier 传递给 true 用于演示或测试目的。

 az cosmosdb create   
     --resource-group   ${  AZ_RESOURCE_GROUP  }    
     --name   ${  AZ_COSMOS_DB  }    
     --enable-free-tier   true    
     --enable-analytical-storage   false 
 

创建 Azure Function 本地项目

创建项目文件夹

  mkdir  az-function-rest-api
 cd  az-function-rest-api
npm init  -y 
 

安装库

 npm  install   --save-exact  @azure/cosmos
 

安装开发库

 npm  install   --save-exact   -D  @azure/functions @types/node azure-functions-core-tools typescript
 

生成 Azure 函数项目

 npx func init --worker-runtime node --language typescript
 

创建 Http 触发函数

这次我们将创建 4 个函数。

 npx func new  --name  get-todos  --template   'HTTP trigger'   --authlevel   'anonymous'   --language  typescript
 

 npx func new  --name  post-todos  --template   'HTTP trigger'   --authlevel   'anonymous'   --language  typescript
 

 npx func new  --name  delete-todos  --template   'HTTP trigger'   --authlevel   'anonymous'   --language  typescript
 

 npx func new  --name  patch-todos  --template   'HTTP trigger'   --authlevel   'anonymous'   --language  typescript
 

编辑 package.json

包.json

  {  
    "name"  :     "az-function-rest-api"  ,  
    "version"  :     "1.0.0"  ,  
    "description"  :     ""  ,  
    "main"  :     "index.js"  ,  
    "scripts"  :     {  
      "build"  :     "tsc"  ,     #     <=     追加  
      "watch"  :     "tsc -w"  ,     #     <=     追加  
      "prestart"  :     "npm run build"  ,     #     <=     追加  
      "start"  :     "func start"     #     <=     追加  
    },  
    "keywords"  :     [],  
    "author"  :     ""  ,  
    "license"  :     "ISC"  ,  
    "dependencies"  :     {  
      "@azure/cosmos"  :     "3.17.1"  
    },  
    "devDependencies"  :     {  
      "@azure/functions"  :     "3.2.0"  ,  
      "@types/node"  :     "18.11.5"  ,  
      "azure-functions-core-tools"  :     "4.0.4829"  ,  
      "typescript"  :     "4.8.4"  
    }  
  }  
  

当前项目结构

  . 
├── delete-todos
│   ├──  function .json
│   └── index.ts
├── get-todos
│   ├──  function .json
│   └── index.ts
├── host.json
├── local.settings.json
├── package-lock.json
├── package.json
├── patch-todos
│   ├──  function .json
│   └── index.ts
├── post-todos
│   ├──  function .json
│   └── index.ts
└── tsconfig.json
 

在本地运行 Azure 函数

 npm run start
 

默认情况下,每个函数的文件夹名称会自动设置为端点路径。

 ❯ npm run start

 >  az-function-rest-api@1.0.0 prestart
 >  npm run build


 >  az-function-rest-api@1.0.0 build
 >  tsc


 >  az-function-rest-api@1.0.0 start
 >  func start


Azure Functions Core Tools
Core Tools Version:       4.0.4829 Commit  hash : N/A   ( 64-bit ) 
Function Runtime Version: 4.11.2.19273

 [ 2022-10-25T05:54:42.841Z] Worker process started and initialized.

Functions:

        delete-todos:  [ GET,POST] http://localhost:7071/api/delete-todos

        get-todos:  [ GET,POST] http://localhost:7071/api/get-todos

        patch-todos:  [ GET,POST] http://localhost:7071/api/patch-todos

        post-todos:  [ GET,POST] http://localhost:7071/api/post-todos
 

当我实际发送 HTTP 请求时,响应会正确返回。

 curl  "http://localhost:7071/api/delete-todos?name=azure" 
 

 ❯ curl  "http://localhost:7071/api/get-todos?name=azure" 
Hello, azure. This HTTP triggered  function  executed successfully.%
 

端点路径和 HTTP 方法可以从每个函数文件夹中自动生成的 function.json 中设置。

您可以在 bindings.methods 列表中指定要允许的HTTP 方法。 您可以使用 bidnings.route 指定端点路径。路径参数也可以指定为占位符,并且可以从函数中引用。您可以通过添加 ? 使路径参数成为可选参数。

获取待办事项/function.json

  {  
    "bindings"  :     [  
      {  
        "authLevel"  :     "Anonymous"  ,  
        "type"  :     "httpTrigger"  ,  
        "direction"  :     "in"  ,  
        "name"  :     "req"  ,  
        "methods"  :     [  "get"  ],     #     <=     postを消去  
        "route"  :     "todos/{id?}"     #     <=     追加  
      },  
      {  
        "type"  :     "http"  ,  
        "direction"  :     "out"  ,  
        "name"  :     "res"  
      }  
    ],  
    "scriptFile"  :     "dist/get-todos/index.js"  
  }  
  

编辑 get-todo/index.ts 以接收并查看路径参数。路径参数的数据存储在 context 对象的 bindingData 属性中,可以从函数的参数中接收。

获取待办事项/index.ts

  import   {   AzureFunction  ,   Context  ,   HttpRequest   }   from   "  @azure/functions  " 

 const   httpTrigger  :   AzureFunction   =   async   function   (  context  :   Context  ,   req  :   HttpRequest  ):   Promise  <  void  >   { 
     const   id   =   context  .  bindingData  .  id  ;   // <= context.bindingData のなかにpath paramのデータが保管されている。 

     context  .  res   =   { 
         body  :   { 
             id 
         } 
     }; 

 }; 

 export   default   httpTrigger  ; 
 

再次运行该函数,可以看到 function.json 中设置的内容已经体现出来了。

 npm run start
 

 ❯ npm run start

 >  az-function-rest-api@1.0.0 prestart
 >  npm run build


 >  az-function-rest-api@1.0.0 build
 >  tsc


 >  az-function-rest-api@1.0.0 start
 >  func start


Azure Functions Core Tools
Core Tools Version:       4.0.4829 Commit  hash : N/A   ( 64-bit ) 
Function Runtime Version: 4.11.2.19273

 [ 2022-10-25T06:16:51.255Z] Worker process started and initialized.

Functions:

        delete-todos:  [ GET,POST] http://localhost:7071/api/delete-todos

        get-todos:  [ GET] http://localhost:7071/api/todos/ {  id ? }   # <= 設定が反映されている。 

        patch-todos:  [ GET,POST] http://localhost:7071/api/patch-todos

        post-todos:  [ GET,POST] http://localhost:7071/api/post-todos
 

如果将路径参数传递给路由并发送 HTTP 请求,则可以确认返回带有路径参数的 id 。

 curl  -i   -X  GET http://localhost:7071/api/todos/123
 

 ❯ curl  -i   -X  GET http://localhost:7071/api/todos/123
 { 
   "id" : 123
 } %
 

创建 REST API

完成的项目结构

  . 
├── delete-todos
│   ├──  function .json
│   └── index.ts
├── get-todos
│   ├──  function .json
│   └── index.ts
├── host.json
├── lib
│   ├── db
│   │   └── db-config.ts
│   ├── dtos
│   │   ├── CreateTodoDto.ts
│   │   └── UpdateTodoDto.ts
│   ├── errors
│   │   └── HttpError.ts
│   ├── models
│   │   └── TodoItem.ts
│   ├── repositories
│   │   └── todo-repository.ts
│   ├── services
│   │   └── todo-service.ts
│   └── utils
│       └── generate-id.ts
├── local.settings.json
├── package-lock.json
├── package.json
├── patch-todos
│   ├──  function .json
│   └── index.ts
├── post-todos
│   ├──  function .json
│   └── index.ts
└── tsconfig.json
 

获取 todos 函数

获取待办事项/index.ts

  import   {   AzureFunction  ,   Context  ,   HttpRequest   }   from   "  @azure/functions  "  ; 
 import   {   HttpError   }   from   "  lib/errors/HttpError  "  ; 
 import   {   todoService   }   from   "  lib/services/todo-service  "  ; 

 const   httpTrigger  :   AzureFunction   =   async   function   ( 
   context  :   Context  , 
   req  :   HttpRequest 
 ):   Promise  <  void  >   { 
   const   todoId   =   context  .  bindingData  .  id  ; 

   try   { 
     if   (  todoId  )   { 
       const   result   =   await   todoService  .  getOne  (  todoId  ); 
       context  .  res   =   { 
         status  :   200  , 
         body  :   result  , 
       }; 

       return  ; 
     } 
   }   catch   (  err  )   { 
     if   (  err   instanceof   HttpError  )   { 
       context  .  res   =   { 
         status  :   err  .  StatusCode  , 
         body  :   { 
           error  :   { 
             message  :   err  .  message  , 
           }, 
         }, 
       }; 
       return  ; 
     } 
   } 

   const   result   =   await   todoService  .  getOnes  (); 

   context  .  res   =   { 
     status  :   200  , 
     body  :   result  , 
   }; 
 }; 

 export   default   httpTrigger  ; 
 

获取待办事项/function.json

  {  
    "bindings"  :     [  
      {  
        "authLevel"  :     "Anonymous"  ,  
        "type"  :     "httpTrigger"  ,  
        "direction"  :     "in"  ,  
        "name"  :     "req"  ,  
        "methods"  :     [  "get"  ],  
        "route"  :     "todos/{id?}"  
      },  
      {  
        "type"  :     "http"  ,  
        "direction"  :     "out"  ,  
        "name"  :     "res"  
      }  
    ],  
    "scriptFile"  :     "dist/get-todos/index.js"  
  }  

  

发布待办事项功能

待办事项/index.ts

  import   {   AzureFunction  ,   Context  ,   HttpRequest   }   from   "  @azure/functions  "  ; 
 import   {   todoService   }   from   "  lib/services/todo-service  "  ; 

 const   httpTrigger  :   AzureFunction   =   async   function   ( 
   context  :   Context  , 
   req  :   HttpRequest 
 ):   Promise  <  void  >   { 
   const   todoCreateDto   =   req  .  body  ; 
   const   result   =   await   todoService  .  createOne  (  todoCreateDto  ); 

   context  .  res   =   { 
     status  :   201  , 
     body  :   result  , 
   }; 
 }; 

 export   default   httpTrigger  ; 
 

待办事项后/function.json

  {  
    "bindings"  :     [  
      {  
        "authLevel"  :     "Anonymous"  ,  
        "type"  :     "httpTrigger"  ,  
        "direction"  :     "in"  ,  
        "name"  :     "req"  ,  
        "methods"  :     [  "post"  ],  
        "route"  :     "todos"  
      },  
      {  
        "type"  :     "http"  ,  
        "direction"  :     "out"  ,  
        "name"  :     "res"  
      }  
    ],  
    "scriptFile"  :     "dist/post-todos/index.js"  
  }  
  

删除待办事项功能

删除待办事项/index.ts

  import   {   AzureFunction  ,   Context  ,   HttpRequest   }   from   "  @azure/functions  "  ; 
 import   {   HttpError   }   from   "  lib/errors/HttpError  "  ; 
 import   {   todoService   }   from   "  lib/services/todo-service  "  ; 

 const   httpTrigger  :   AzureFunction   =   async   function   ( 
   context  :   Context  , 
   req  :   HttpRequest 
 ):   Promise  <  void  >   { 
   const   todoId   =   context  .  bindingData  .  id  ; 

   try   { 
     await   todoService  .  deleteOne  (  todoId  ); 
   }   catch   (  err  )   { 
     if   (  err   instanceof   HttpError  )   { 
       context  .  res   =   { 
         status  :   err  .  StatusCode  , 
         body  :   { 
           error  :   { 
             message  :   err  .  message  , 
           }, 
         }, 
       }; 
       return  ; 
     } 
   } 

   context  .  res   =   { 
     status  :   204  , 
   }; 
 }; 

 export   default   httpTrigger  ; 
 

删除待办事项/function.json

  {  
    "bindings"  :     [  
      {  
        "authLevel"  :     "Anonymous"  ,  
        "type"  :     "httpTrigger"  ,  
        "direction"  :     "in"  ,  
        "name"  :     "req"  ,  
        "methods"  :     [  "delete"  ],  
        "route"  :     "todos/{id}"  
      },  
      {  
        "type"  :     "http"  ,  
        "direction"  :     "out"  ,  
        "name"  :     "res"  
      }  
    ],  
    "scriptFile"  :     "dist/delete-todos/index.js"  
  }  
  

补丁待办事项功能

补丁待办事项/index.ts

  import   {   AzureFunction  ,   Context  ,   HttpRequest   }   from   "  @azure/functions  "  ; 
 import   {   HttpError   }   from   "  lib/errors/HttpError  "  ; 
 import   {   todoService   }   from   "  lib/services/todo-service  "  ; 

 const   httpTrigger  :   AzureFunction   =   async   function   ( 
   context  :   Context  , 
   req  :   HttpRequest 
 ):   Promise  <  void  >   { 
   const   todoId   =   context  .  bindingData  .  id  ; 
   const   updateTodoDto   =   req  .  body  ; 

   try   { 
     await   todoService  .  updateOne  (  updateTodoDto  ,   todoId  ); 
   }   catch   (  err  )   { 
     if   (  err   instanceof   HttpError  )   { 
       context  .  res   =   { 
         status  :   err  .  StatusCode  , 
         body  :   { 
           error  :   { 
             message  :   err  .  message  , 
           }, 
         }, 
       }; 
       return  ; 
     } 
   } 

   context  .  res   =   { 
     status  :   204  , 
   }; 
 }; 

 export   default   httpTrigger  ; 
 

补丁待办事项/function.json

  {  
    "bindings"  :     [  
      {  
        "authLevel"  :     "Anonymous"  ,  
        "type"  :     "httpTrigger"  ,  
        "direction"  :     "in"  ,  
        "name"  :     "req"  ,  
        "methods"  :     [  "patch"  ],  
        "route"  :     "todos/{id}"  
      },  
      {  
        "type"  :     "http"  ,  
        "direction"  :     "out"  ,  
        "name"  :     "res"  
      }  
    ],  
    "scriptFile"  :     "dist/patch-todos/index.js"  
  }  
  

通用模块

模型

lib/models/TodoItem.ts

  export   interface   TodoItem   { 
   id  ?:   string  ; 
   title  :   string  ; 
   isCompleted  :   boolean  ; 
 } 
 

服务

lib/services/todo-service.ts

  import   {   CreateTodoDto   }   from   "  dtos/CreateTodoDto  "  ; 
 import   {   UpdateTodoDto   }   from   "  dtos/UpdateTodoDto  "  ; 
 import   {   HttpError   }   from   "  errors/HttpError  "  ; 
 import   {   TodoItem   }   from   "  models/TodoItem  "  ; 
 import   {   todoRepository   }   from   "  repositories/todo-repository  "  ; 
 import   {   generateId   }   from   "  utils/generate-id  "  ; 

 const   getOnes   =   async   ():   Promise  <  TodoItem  []   |   any  >   =>   { 
   return   await   todoRepository  .  getOnes  (); 
 }; 

 const   getOne   =   async   (  id  :   string  ):   Promise  <  any  >   =>   { 
   return   await   todoRepository  .  getOneById  (  id  ); 
 }; 

 const   createOne   =   async   (  dto  :   CreateTodoDto  ):   Promise  <  TodoItem  >   =>   { 
   const   newTodo  :   TodoItem   =   { 
     id  :   generateId  (), 
     ...  dto  , 
     isCompleted  :   false  , 
   }; 

   return   await   todoRepository  .  createOne  (  newTodo  ); 
 }; 

 const   deleteOne   =   async   (  id  :   string  )   =>   { 
   if   (  !  id  )   { 
     throw   new   HttpError  (  "  Todo item id is not provided  "  ,   400  ); 
   } 

   const   existingTodo   =   await   todoRepository  .  getOneById  (  id  ); 
   if   (  !  existingTodo  )   { 
     throw   new   HttpError  (  `TodoItem(id=  ${  id  }  ) is not found.`  ,   404  ); 
   } 

   todoRepository  .  removeOneById  (  existingTodo  .  id  ); 
 }; 

 const   updateOne   =   async   (  dto  :   UpdateTodoDto  ,   id  :   string  )   =>   { 
   if   (  !  id  )   { 
     throw   new   HttpError  (  "  Todo item id is not provided  "  ,   400  ); 
   } 

   const   existingTodo   =   await   todoRepository  .  getOneById  (  id  ); 

   if   (  !  existingTodo  )   { 
     throw   new   HttpError  (  `TodoItem(id=  ${  id  }  ) is not found.`  ,   404  ); 
   } 

   if   (  typeof   dto  .  title   !==   "  undefined  "  )   { 
     existingTodo  .  title   =   dto  .  title  ; 
   } 

   if   (  typeof   dto  .  isCompleted   !==   "  undefined  "  )   { 
     existingTodo  .  isCompleted   =   dto  .  isCompleted  ; 
   } 

   await   todoRepository  .  update  (  existingTodo  ); 
 }; 

 export   const   todoService   =   Object  .  freeze  ({ 
   getOnes  , 
   getOne  , 
   createOne  , 
   deleteOne  , 
   updateOne  , 
 }); 
 

存储库

lib/repositories/todo-repository.ts

  import   {   initDB   }   from   "  db/db-config  "  ; 

 const   db   =   initDB  (); 

 const   getOnes   =   async   ()   =>   { 
   const   {   container   }   =   await   db  ; 
   const   {   resources   }   =   await   container  .  items  .  readAll  ().  fetchAll  (); 
   return   resources  ; 
 }; 

 const   getOneById   =   async   (  id  :   string  )   =>   { 
   const   {   container   }   =   await   db  ; 
   const   {   resource   }   =   await   container  .  item  (  id  ,   id  ).  read  (); 
   return   resource  ; 
 }; 

 const   createOne   =   async   (  todo  :   any  )   =>   { 
   const   {   container   }   =   await   db  ; 
   const   {   resource   }   =   await   container  .  items  .  create  (  todo  ); 
   return   resource  ; 
 }; 

 const   removeOneById   =   async   (  id  :   string  )   =>   { 
   const   {   container   }   =   await   db  ; 
   await   container  .  item  (  id  ,   id  ).  delete  (); 
 }; 

 const   update   =   async   (  todo  :   any  )   =>   { 
   const   {   container   }   =   await   db  ; 
   await   container  .  item  (  todo  .  id  ,   todo  .  id  ).  replace  (  todo  ); 
 }; 

 export   const   todoRepository   =   Object  .  freeze  ({ 
   getOnes  , 
   getOneById  , 
   createOne  , 
   removeOneById  , 
   update  , 
 }); 
 

数据库设置

lib/db/db-config.ts

  import   {   CosmosClient   }   from   "  @azure/cosmos  "  ; 

 const   cosmosConfig   =   { 
   endpoint  :   process  .  env  .  COSMOSDB_URI  , 
   primaryKey  :   process  .  env  .  COSMOSDB_PRIMARY_KEY  , 
   database  :   process  .  env  .  COSMOSDB_DATABASE  , 
   container  :   process  .  env  .  COSMOSDB_CONTAINER  , 
   partitionKey  :   { 
     paths  :   [  "  /id  "  ], 
   }, 
 }; 

 export   const   initDB   =   async   ()   =>   { 
   const   cosmosClient   =   new   CosmosClient  ({ 
     endpoint  :   cosmosConfig  .  endpoint  , 
     key  :   cosmosConfig  .  primaryKey  , 
   }); 
   const   {   database   }   =   await   cosmosClient  .  databases  .  createIfNotExists  ({ 
     id  :   cosmosConfig  .  database  , 
   }); 

   const   {   container   }   =   await   database  .  containers  .  createIfNotExists  ({ 
     id  :   cosmosConfig  .  container  , 
     partitionKey  :   cosmosConfig  .  partitionKey  , 
   }); 

   return   { 
     cosmosClient  , 
     database  , 
     container  , 
   }; 
 }; 
 

自定义错误

lib/errors/HttpError.ts

  export   class   HttpError   extends   Error   { 
   statusCode  ; 
   constructor  (  message  ,   statusCode  )   { 
     super  (  message  ); 
     this  .  statusCode   =   statusCode  ; 
   } 

   get   StatusCode  ()   { 
     return   this  .  statusCode  ; 
   } 
 } 
 

效用

lib/utils/generate-id.ts

  import   *   as   crypto   from   "  crypto  "  ; 

 export   const   generateId   =   ()   =>   crypto  .  randomUUID  (); 
 

其他配置文件

主机.json

  {  
    "version"  :     "2.0"  ,  
    "logging"  :     {  
      "applicationInsights"  :     {  
        "samplingSettings"  :     {  
          "isEnabled"  :     true  ,  
          "excludedTypes"  :     "Request"  
        }  
      }  
    },  
    "extensionBundle"  :     {  
      "id"  :     "Microsoft.Azure.Functions.ExtensionBundle"  ,  
      "version"  :     "[3.*, 4.0.0)"  
    }  
  

您可以使用 local.settings.json 设置环境变量。添加 COSMOSDB_URI 和 COSMOSDB_PRIMARY_KEY 作为环境变量以连接到CosmosDB。为 COSMOSDB_DATABASE 和 COSMOSDB_CONTAINER 指定任意值。

local.settings.json

  {  
    "IsEncrypted"  :     false  ,  
    "Values"  :     {  
      "AzureWebJobsStorage"  :     ""  ,  
      "FUNCTIONS_WORKER_RUNTIME"  :     "node"  ,  
      "COSMOSDB_URI"  :     "https://<COSMOSDB_RESOURCE_NAME>.documents.azure.com:443/"  ,  
      "COSMOSDB_PRIMARY_KEY"  :     "<COSMOSDB_PRIMARY_KEY>"  ,  
      "COSMOSDB_DATABASE"  :     "node_azure_functions_db"  ,  
      "COSMOSDB_CONTAINER"  :     "todos"  
    }  
  }  
  

COSMOSDB_URI 和 COSMOSDB_PRIMARY_KEY 的值可以在 azure 门户中的“CosmosDB 资源 => Settgins => Keys”中查看。

或者您可以使用以下命令获取它。

CosmosDB URI

 az cosmosdb show   
     --resource-group   ${  AZ_RESOURCE_GROUP  }    
     --name   ${  AZ_COSMOS_DB  }    
     -o  tsv   
     --query   "documentEndpoint" 
 

CosmosDB 主键

 az cosmosdb keys list   
     --resource-group   ${  AZ_RESOURCE_GROUP  }    
     --name   ${  AZ_COSMOS_DB  }    
     --type   "keys"    
     -o  tsv   
     --query   "primaryMasterKey" 
 

调用 REST API

 npm run start
 

 ❯ npm run start

 >  az-function-rest-api@1.0.0 prestart
 >  npm run build


 >  az-function-rest-api@1.0.0 build
 >  tsc


 >  az-function-rest-api@1.0.0 start
 >  func start


Azure Functions Core Tools
Core Tools Version:       4.0.4829 Commit  hash : N/A   ( 64-bit ) 
Function Runtime Version: 4.11.2.19273

 [ 2022-10-26T02:25:55.113Z] Worker process started and initialized.

Functions:

        delete-todos:  [ DELETE] http://localhost:7071/api/todos/ {  id  } 

        get-todos:  [ GET] http://localhost:7071/api/todos/ {  id ? } 

        patch-todos:  [ PATCH] http://localhost:7071/api/todos/ {  id  } 

        post-todos:  [ POST] http://localhost:7071/api/todos
 

实际上,点击以下命令来测试 API。

 curl  -i   -X  POST  -d   '{"title":"todo 1"}'  http://localhost:7071/api/todos
 

 curl  -i   -X  GET http://localhost:7071/api/todos
 

 curl  -i   -X  GET http://localhost:7071/api/todos/ {  id  } 
 

 curl  -i   -X  PATCH  -d   '{"title":"todo 1 updated"}'  http://localhost:7071/api/todos/ {  id  } 
 

 curl  -i   -X  DELETE http://localhost:7071/api/todos/ {  id  } 
 

添加待办事项

 ❯ curl  -i   -X  POST  -d   '{"title":"todo 1"}'  http://localhost:7071/api/todos
HTTP/1.1 201 Created
Content-Type: text/plain ;   charset  = utf-8
Date: Wed, 26 Oct 2022 02:53:40 GMT
Server: Kestrel
Transfer-Encoding: chunked

 { 
   "id" :  "10cb9c6f-6161-4798-8de5-e213a5e2be1d" ,
   "title" :  "todo 1" ,
   "isCompleted" :  false ,
   "_rid" :  "8T0VAJe9jpsDAAAAAAAAAA==" ,
   "_self" :  "dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsDAAAAAAAAAA==/" ,
   "_etag" :  "  "  c001744e-0000-2300-0000-6358a1350000  "  " ,
   "_attachments" :  "attachments/" ,
   "_ts" : 1666752821
 } %  
 

获取待办事项

 ❯ curl  -i   -X  GET http://localhost:7071/api/todos
HTTP/1.1 200 OK
Content-Type: text/plain ;   charset  = utf-8
Date: Wed, 26 Oct 2022 02:55:41 GMT
Server: Kestrel
Transfer-Encoding: chunked

 [ 
   { 
     "id" :  "10cb9c6f-6161-4798-8de5-e213a5e2be1d" ,
     "title" :  "todo 1" ,
     "isCompleted" :  false ,
     "_rid" :  "8T0VAJe9jpsDAAAAAAAAAA==" ,
     "_self" :  "dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsDAAAAAAAAAA==/" ,
     "_etag" :  "  "  c001744e-0000-2300-0000-6358a1350000  "  " ,
     "_attachments" :  "attachments/" ,
     "_ts" : 1666752821
   } 
 ] % 
 

编辑待办事项

 ❯ curl  -i   -X  PATCH  -d   '{"title":"todo 1 updated"}'  http://localhost:7071/api/todos/10cb9c6f-6161-4798-8de5-e213a5e2be1d
HTTP/1.1 204 No Content
Content-Type: text/plain ;   charset  = utf-8
Date: Wed, 26 Oct 2022 02:56:31 GMT
Server: Kestrel
 

通过 ID 获取待办事项

 ❯ curl  -i   -X  GET http://localhost:7071/api/todos/10cb9c6f-6161-4798-8de5-e213a5e2be1d
HTTP/1.1 200 OK
Content-Type: text/plain ;   charset  = utf-8
Date: Wed, 26 Oct 2022 02:57:34 GMT
Server: Kestrel
Transfer-Encoding: chunked

 { 
   "id" :  "10cb9c6f-6161-4798-8de5-e213a5e2be1d" ,
   "title" :  "todo 1 updated" ,
   "isCompleted" :  false ,
   "_rid" :  "8T0VAJe9jpsDAAAAAAAAAA==" ,
   "_self" :  "dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsDAAAAAAAAAA==/" ,
   "_etag" :  "  "  c0013f9b-0000-2300-0000-6358a1e00000  "  " ,
   "_attachments" :  "attachments/" ,
   "_ts" : 1666752992
 } %
 

确认数据已添加到 Azure 门户端的 CosmosDB。

创建更改提要侦听器函数

 npx func new  --name  change-feed-listener  --template   'Azure Cosmos DB trigger'   --language  typescript
 

  . 
├── change-feed-listener  # <= 追加 
│   ├──  function .json
│   └── index.ts
├── delete-todos
│   ├──  function .json
│   └── index.ts
├── get-todos
│   ├──  function .json
│   └── index.ts
├── host.json
├── lib
│   ├── db
│   │   └── db-config.ts
│   ├── dtos
│   │   ├── CreateTodoDto.ts
│   │   └── UpdateTodoDto.ts
│   ├── errors
│   │   └── HttpError.ts
│   ├── models
│   │   └── TodoItem.ts
│   ├── repositories
│   │   └── todo-repository.ts
│   ├── services
│   │   └── todo-service.ts
│   └── utils
│       └── generate-id.ts
├── local.settings.json
├── package-lock.json
├── package.json
├── patch-todos
│   ├──  function .json
│   └── index.ts
├── post-todos
│   ├──  function .json
│   └── index.ts
└── tsconfig.json
 

cosmosDBTrigger 在连接的 CosmosDB 中添加或编辑数据时将执行该函数,并传递该记录的数据。擦除数据时不执行 cosmosDBTrigger 。

更改提要监听器/index.ts

  import   {   AzureFunction  ,   Context   }   from   "  @azure/functions  " 

 const   cosmosDBTrigger  :   AzureFunction   =   async   function   (  context  :   Context  ,   documents  :   any  []):   Promise  <  void  >   { 
     if   (  !!  documents   &&   documents  .  length   >   0  )   { 
         context  .  log  (  '  Document:  '  ,   documents  ); 
     } 
 } 

 export   default   cosmosDBTrigger  ; 
 

如果将 connectionStringSetting 的值直接替换为连接字符串的值,则会发生错误。将实际值写入 local.settings.json ,并将 connectionStringSetting 的值保留为 COSMOSDB_CONNECTION_STRING_DOCUMENTDB 。

更改馈送侦听器/function.json

  {  
    "bindings"  :     [  
      {  
        "type"  :     "cosmosDBTrigger"  ,  
        "name"  :     "documents"  ,  
        "direction"  :     "in"  ,  
        "leaseCollectionName"  :     "leases"  ,  
        "connectionStringSetting"  :     "COSMOSDB_CONNECTION_STRING_DOCUMENTDB"  ,     #     <=     local.settings.jsonの値を参照。  
        "databaseName"  :     "node_azure_functions_db"  ,  
        "collectionName"  :     "todos"  ,  
        "createLeaseCollectionIfNotExists"  :     true  
      }  
    ],  
    "scriptFile"  :     "dist/src/change-feed-listener/index.js"  
  }  
  

编辑 local.settings.json 并添加 COSMOSDB_CONNECTION_STRING_DOCUMENTDB 。 COSMOSDB_CONNECTION_STRING_DOCUMENTDB 的值是 COSMOSDB_URI 和 COSMOSDB_PRIMARY_KEY 的组合。请务必在键名末尾添加 _DOCUMENTDB (需要引用来自 function.json 的值)。

local.settings.json

  {  
    "IsEncrypted"  :     false  ,  
    "Values"  :     {  
      "AzureWebJobsStorage"  :     ""  ,  
      "FUNCTIONS_WORKER_RUNTIME"  :     "node"  ,  
      "COSMOSDB_URI"  :     "https://<COSMOSDB_NAME>.documents.azure.com:443/"  ,  
      "COSMOSDB_PRIMARY_KEY"  :     "<COSMOSDB_PRIMARY_KEY>"  ,  
      "COSMOSDB_DATABASE"  :     "node_azure_functions_db"  ,  
      "COSMOSDB_CONTAINER"  :     "todos"  
      #     追加  
      "COSMOSDB_CONNECTION_STRING_DOCUMENTDB"  :     "AccountEndpoint=https://<COSMOSDB_NAME>.documents.azure.com:443/;AccountKey=<COSMOSDB_PRIMARY_KEY>;"  
    }  
  }  
  

或者您可以从 azure 门户中的“CosmosDB 资源 => Settgins => Keys”进行检查。

 npm run start
 

添加了一个新的 change-feed-listener: cosmosDBTrigger 作为函数。

 ❯ npm run start

 >  az-function-rest-api@1.0.0 prestart
 >  npm run build


 >  az-function-rest-api@1.0.0 build
 >  tsc


 >  az-function-rest-api@1.0.0 start
 >  func start


Azure Functions Core Tools
Core Tools Version:       4.0.4829 Commit  hash : N/A   ( 64-bit ) 
Function Runtime Version: 4.11.2.19273

 [ 2022-10-26T05:15:06.386Z] Worker process started and initialized.

Functions:

        delete-todos:  [ DELETE] http://localhost:7071/api/todos/ {  id  } 

        get-todos:  [ GET] http://localhost:7071/api/todos/ {  id ? } 

        patch-todos:  [ PATCH] http://localhost:7071/api/todos/ {  id  } 

        post-todos:  [ POST] http://localhost:7071/api/todos

        change-feed-listener: cosmosDBTrigger  # <= 追加 
 

当我实际添加数据时。

 ❯ curl  -i   -X  POST  -d   '{"title":"todo 1"}'  http://localhost:7071/api/todos
HTTP/1.1 201 Created
Content-Type: text/plain ;   charset  = utf-8
Date: Wed, 26 Oct 2022 05:40:11 GMT
Server: Kestrel
Transfer-Encoding: chunked

 { 
   "id" :  "29c84398-6b37-4dfd-ad8b-45403a62f4d7" ,
   "title" :  "todo 1" ,
   "isCompleted" :  false ,
   "_rid" :  "8T0VAJe9jpsEAAAAAAAAAA==" ,
   "_self" :  "dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsEAAAAAAAAAA==/" ,
   "_etag" :  "  "  cf01af89-0000-2300-0000-6358c83b0000  "  " ,
   "_attachments" :  "attachments/" ,
   "_ts" : 1666762811
 } %
 

change-feed-listener 日志显示从更改源接收到的数据,确认 change-feed-listener 函数已成功执行。

  [ 2022-10-26T05:40:16.500Z] Executing  'Functions.change-feed-listener'   (  Reason  =  'New changes on collection todos at 2022-10-26T05:40:16.4919090Z' ,  Id  = 50591fbf-9ef8-49cc-b34a-abbdca3631d1 ) 
 [ 2022-10-26T05:40:16.519Z] Document:  [ 
 [ 2022-10-26T05:40:16.519Z]    { 
 [ 2022-10-26T05:40:16.519Z]      id :  '29c84398-6b37-4dfd-ad8b-45403a62f4d7' ,
 [ 2022-10-26T05:40:16.519Z]     _rid:  '8T0VAJe9jpsEAAAAAAAAAA==' ,
 [ 2022-10-26T05:40:16.519Z]     _self:  'dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsEAAAAAAAAAA==/' ,
 [ 2022-10-26T05:40:16.519Z]     _ts: 1666762811,
 [ 2022-10-26T05:40:16.519Z]     _etag:  '"cf01af89-0000-2300-0000-6358c83b0000"' ,
 [ 2022-10-26T05:40:16.519Z]     title:  'todo 1' ,
 [ 2022-10-26T05:40:16.519Z]     isCompleted:  false ,
 [ 2022-10-26T05:40:16.519Z]     _lsn: 11
 [ 2022-10-26T05:40:16.519Z]    } 
 [ 2022-10-26T05:40:16.519Z]  ] 
 

功能部署

只需在项目根目录执行以下命令即可轻松完成部署。

 npx func azure functionapp publish  ${  AZ_FUNCTION  } 
 

 ❯ npx func azure functionapp publish myTestFunction231917910
Setting Functions site property  'netFrameworkVersion'  to  'v6.0' 
Getting site publishing info...
Creating archive  for  current directory...
Uploading 1.61 MB  [  #########################################################] 
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Functions  in  myTestFunction231917910:
    change-feed-listener -  [ cosmosDBTrigger]

    delete-todos -  [ httpTrigger]
        Invoke url: https://mytestfunction231917910.azurewebsites.net/api/todos/ {  id  } 

    get-todos -  [ httpTrigger]
        Invoke url: https://mytestfunction231917910.azurewebsites.net/api/todos/ {  id ? } 

    patch-todos -  [ httpTrigger]
        Invoke url: https://mytestfunction231917910.azurewebsites.net/api/todos/ {  id  } 

    post-todos -  [ httpTrigger]
        Invoke url: https://mytestfunction231917910.azurewebsites.net/api/todos
 

您可以使用以下命令查看已部署函数的日志。

 npx func azure functionapp logstream  ${  AZ_FUNCTION  } 
 

结尾

我能够使用 Node.js (TypeScript) 和 Azure Functions 创建一个无服务器 REST API 应用程序。如果使用 azure 函数,则可以高速开发应用程序。部署也很容易。您可以轻松地接收来自 CosmosDB 的更改源,因此您可以轻松地添加以 azure 函数为中心的微服务应用程序。


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308631507.html

查看更多关于使用 Node.js (TypeScript) 和 Azure Functions 创建无服务器 R的详细内容...

  阅读:63次