Technology Blog Posts by SAP
cancel
Showing results for 
Search instead for 
Did you mean: 
ArthurYang
Product and Topic Expert
Product and Topic Expert
2,178

本文档目的是帮助您初步了解CAP+Nodejs的部署方式,阅读时间约为1小时。


 

使用前提是

1.完成开发入门练习

2.您的BTP账号本身拥有Cloud Foundry Runtime的权利

3.拥有HANA Cloud的权利,或者是拥有其他子账户内HANA Cloud的Mapping,如果希望部署后的应用公网可用,还需要为BTP环境配置上自己的域名

 

关于BTP账号如何创建子账户,分配资源,分配权限等,请参阅BTP基础练习 SAP Business Technology Platform (BTP) 中控台概览及基本操作练习 | SAP Blogs


 

如果您对BTP感兴趣,BTP个人精选内容目录 | SAP Blogs 可能有更多你需要的内容


 

本文档为CAP for Nodejs入门练习 | SAP Blogs 的后续,本练习会将前一练习的结果部署到SAP BTP的Cloud Foundry容器运行时中去



着重需要强调的一个概念就是MTA,是多目标应用的缩写称呼,是一个部署框架,CAP编程框架一般结合MTA框架下的MBT部署工具来完成对前后端数据库的整体打包,最后基于CF脚手架进行一站式部署

文档最后还涉及了approuter,是一个前端单一入口的服务器应用,这个应用没有UI界面,但是可以将其他各种语言开发的前端界面囊括在一起,作为一整个服务部署上云,从用户的角度看到的一个前端服务背后其实可能是由多个微服务组成;

同时还可以将前端服务单独部署到HTML5 Repo内,由服务托管的Approuter做转发来实现前端服务

 

本文档包含以下部分:

 

1.启用Cloud Foundry

2.为代码添加HANA支持

3.CF内启用HANA Cloud

4.为代码启用身份服务

5.安装部署工具mbt

6.为本地CF CLI配置MTA插件

7.配置nodejs版本

8.准备MTA工具

9.使用SAP approuter存储前端代码

10.本地访问应用

11.使用HTML5Repo存储前端代码

12.部署应用并访问


 
 

1.启用Cloud Foundry(CF)

Cloud Foundry是一款开源PaaS平台,提供了应用全生命周期管理的功能,BTP使用Cloud Foundry Runtime部署SAP的云应用,同时也提供给客户用来部署自己的应用,对该平台感兴趣的可以查阅Cloud Foundry开源PaaS平台概况介绍 | SAP Blogs

请参阅开头的 BTP基础练习 链接来为子账户启用Cloud Foundry

 

如果你使用的是本地的VSCode
最后,CF可以在BTP中控台进行操作,但是更多时候依赖命令行的CF CLI来完成操作,所以我们需要安装 CF的CLI脚手架:https://developers.sap.com/tutorials/btp-app-_install-the-cloud-foundry-command-line-interface.html


在命令行登录到CF环境:

打开BTP中控台-进入子账户-记录Org Name,API Endpoint和Org ID,接下来需要这几个属性来登录CF到子账户中

然后进入左菜单的 Cloud Foundry-空间,确保自己拥有空间,如果没有则创建一个名为dev的空间

cf login -a <子账户概览内的 api endpoint> -o "<子账户概览内的Org Name>" -s <子账户内的space名> -u <登陆用的邮箱>

 

 

2.为代码添加HANA支持


在cpapp目录下执行

cds add hana --for production

这一步会在根目录的package.json内cds-require下添加一个”[production]”结构,内容为”db”:”hana”,意思即为在生产部署时使用hana的数据库引擎。

 

 

3.启用HANA Cloud


我们需要一个HANA Cloud实例用以部署我们的数据库内容,

如果你已经有其他现存的HANA Cloud实例,请将该HANA 实例和此次新创建的space mapping相连:

进入已有HANA Cloud实例的子账户 – 左侧Cloud Foundry菜单 – space – 选择存在HANA 实例的space – 左侧SAP HANA Cloud 菜单 此处即会显示现有的实例,

再在右上角点击 管理HANA Cloud – 进入HANA Cloud Central后默认会将搜索条件设置在当前子账户/org和space,点击实例名 – 点击右上角的Create Mapping – 填入本次练习的目标space参数即可

 

如果当前任一space都没有创建过HANA Cloud实例:首先要将HANA Cloud权利分配给当前子账户:请参阅SAP Business Technology Platform (BTP) 中控台概览及基本操作练习 | SAP Blogs 中的3,4,5,6

然后创建HANA Cloud实例:点击 实例与租用-HANA Cloud服务 ,在显示All Instances的页面点击 右上角的Create Instance,

Step1-选择SAP HANA Cloud, SAP HANA Database,

Step2-填写Instance Name和DB管理员密码(这个密码请自行保存),DB管理员账户默认为DBADMIN

Step3,4-自行配置

Step5-将Allowed connections选为Allow all IP addresses(此处为了方便开发),

Step6-暂时不需要Data Lake

 

即可完成创建

创建完后,进入All Instance管理,单击新创建的数据库Instance-Manage Configuration-Instance Mapping-Add Mapping:

Environment Type为Cloud Foundry, Environment Instance ID为子账户-概览-Cloud Foundry环境-Org ID

最后Review and Save

参阅Provision an Instance of SAP HANA Cloud, SAP HANA Database | SAP Tutorials

 

4.为代码启用身份服务


开发环境和生产环境比较大的区别除了数据库之外就是各种外部服务了,其中身份服务是最基础的一项,这里先为本地代码配置上身份服务(XSUAA即XS User Authentication and Authorization service):


cds add xsuaa --for production


此时检查cpapp/package.json即可看到cds – require – [production]

同时在cpapp目录下会生成一个xs-security.json文件,生成时会根据package.json定义过的用户自动生成身份配置:

{
  //scope即最细的权限颗粒,不能直接赋予用户,需要赋予role-template来间接赋予用户
  "scopes": [
    {
      "name": "$XSAPPNAME.RiskViewer",
      "description": "RiskViewer"
    },
    {
      "name": "$XSAPPNAME.RiskManager",
      "description": "RiskManager"
    }
  ],
  "attributes": [],
  //这里定义的两个role各自只包含一个scope
  "role-templates": [
    {
      "name": "RiskViewer",
      "description": "generated",
      "scope-references": [
        "$XSAPPNAME.RiskViewer"
      ],
      "attribute-references": []
    },
    {
      "name": "RiskManager",
      "description": "generated",
      "scope-references": [
        "$XSAPPNAME.RiskManager"
      ],
      "attribute-references": []
    }
  ]
}

Role template身份模板,scope, attributes, role身份,role collection身份集合, user 用户四种概念,

scope即每一个代表最细粒度的身份权限,不能再分

Role template会采用一个或几个scope来确定一个模板

Role就是role template的一个实例,一般默认会根据每一个role template产生一个role

Role collection就是最后直接分配给不同用户的权限模板,可以包含一个或几个role

 

Scope和Role template是1:n(n>=1)

Role template和Role是1:1(没有attribute时)

Role和Role Collection是n:1(n>=1;通常n>1)

 

Attributes代表对可见的数据的控制,如果Role template没有任何Attributes,则相应的角色与Role template相同,并且会自动创建;

如果Role template具有一个或多个Attributes,则必须基于Role template创建角色并提供Attributes

这也就是为什么role role template可以不是11的关系

 

 

5.安装部署工具mbt

如果你使用的是BAS,那么可以跳过这三步:

5.安装mbt, 6.为CF Cli安装MTA插件 7.配置Nodejs 

现在的代码已经包含了生产环境下对数据库和身份服务的引用,这里我们安装mbt工具来部署这份代码:

npm install --global mbt

如果你使用的是windows系统,还需要安装make工具,这个工具在linux系操作系统是预装好的

进入网站下载安装包Make for Windows (sourceforge.net) , 点击 Download下Complete package, except sources 右侧的setup按钮进行下载

下载完成后,打开exe安装包,安装在默认路径C:\Program Files (x86)\GnuWin32,安装完成后将该路径+bin(C:\Program Files (x86)\GnuWin32\bin)添加到环境变量下即可

 

 

6.为本地CF CLI配置MTA插件


执行Cf plugins,可以看到当前本地电脑安装过的插件列表

如果没有安装过multiapps这个插件,执行cf install-plugin multiapps,命令行安装失败时请参阅GitHub - cloudfoundry/multiapps-cli-plugin: A CLI plugin for Multi-Target Application (MTA) operatio...

我这里将手动下载的安装包放在了CAP-tutorial文件夹内,

执行cf install-plugin *path/CAP-tutorial/multiapps-plugin.win64.exe -f 就完成了安装

 

7.配置nodejs版本

本地启动项目时使用的是本地安装的nodejs,部署到CF后会使用容器运行时提供的nodejs,所以最好可以在配置文件中声明要使用的nodejs版本,本练习需要使用nodejs16及以上,

在package.json内添加一个和devDependencies同级的配置:”engines”,内容为”node”:”^16”

"engines": {
    "node": "^16"
  },

(我这里使用的node版本为18.19.0,故将^16替换为了^18, 要查看CF支持的Node版本请查阅Releases · cloudfoundry/nodejs-buildpack (github.com)

 

8.准备MTA配置

首先利用cds工具生成mbt工具需要的配置文件

cds add mta

这会在cpapp下生成一个mta.yaml文件,用于设定部署时的参数(基础文档:Link, 详细文档:Link,和一些mta.yaml文件的例子

在部署前,还需要注意的是,每次部署时如果项目的db/data文件夹内携带了用来初始化的csv数据文件,那么部署时会清除数据库中现有的数据并用初始化数据填充,所以需要修改部署文件以防止这个现象:

在mta.yaml第一级添加:(如果已经存在build-parameter这个第一层参数,则将其修改至与下方一致即可,其中- npx rimraf gen/db/src/gen/data如果没有添加,则会将db/data内的测试数据也部署到云端的数据库中去

build-parameters:
  before-all:
   - builder: custom
     commands:
      - npm install --production
      - npx -p @sap/cds-dk cds build --production
      - npx rimraf gen/db/src/gen/data

 



再修改mta.yaml内的内容:

在resources - name: cpapp-auth其下的parameters – config添加一个属性:

role-collections:
         - name: 'RiskManager-${space}'
           description: Manage Risks
           role-template-references:
             - $XSAPPNAME.RiskManager
         - name: 'RiskViewer-${space}'
           description: View Risks
           role-template-references:
             - $XSAPPNAME.RiskViewer

/////////////////////////////////////////

注解:

${space} 是一个占位符,表示空间名称(space),它会在部署时被替换为实际的空间名。这使得角色集合在不同的空间中可以有不同的名称。例如,如果应用分别被部署到开发环境(dev)和生产环境(prod),RiskManager-dev 和 RiskManager-prod 将分别为不同的空间创建角色集合

role-template-references:引用应用中定义的角色模板。$XSAPPNAME 是一个占位符,表示应用程序的名称。${XSAPPNAME}.RiskManager 和 ${XSAPPNAME}.RiskViewer 是指在应用中定义的角色模板(RiskManager 和 RiskViewer),这些模板描述了具体的角色权限

拥有RiskManager-${space} 角色集合的用户将获得 ${XSAPPNAME}.RiskManager 角色。

拥有RiskViewer-${space} 角色集合的用户将获得 ${XSAPPNAME}.RiskViewer 角色

更详细的MTA配置文档结构解释: Link

要手动定义部署完成后的应用URL: Routes | SAP Help Portal

/////////////////////////////////////////
 

 

9.使用SAP approuter存储前端代码


在app文件夹下新建文件夹approuter

新建文件package.json,内容如下:

{
    "name": "approuter",
    "scripts": {
        "start": "node node_modules/@sap/approuter/approuter.js"
    },
    "dependencies": {
      "@sap/approuter": "^13"
    }
}


 实质上approuter就是一个server,可以为其他前端app提供统一的身份验证,路由转发,反向代理等功能

 

再新建文件xs-app.json来配置approuter的路由信息,内容如下:

{
    "welcomeFile": "/launchpage/launchpage.html",
    "authenticationMethod": "none",
    "routes": [
        {
            "source": "^/odata/(.*)",
            "target": "/odata/$1",
            "destination": "srv-destination"
        },
        {
            "source": "^/app/(.*)",
            "target": "/$1",
            "localDir": "../mitigations"
        },
        {
            "source": "^/app2/(.*)",
            "target": "/$1",
            "localDir": "../risks"
        },
        {
            "source": "^/launchpage/(.*)",
            "target": "/$1",
            "localDir": "../"
        }
    ]
}

/////////////////////////////////////////

注解:

"welcomeFile": "/launchpage/launchpage.html"

当用户访问approuter的根路径 / 时,会自动定向到 welcomeFile,在用户角度会看到访问localhost:5000时,URL会自动变成localhost:5000/launchpage/launchpage.html

 

        {
            "source": "^/launchpage/(.*)",
            "target": "/$1",
            "localDir": "../"
        }

source: 正则表达式 ^/launchpage/(.*) 会匹配访问的路径,如果访问路径以 /launchpage 开头的话则会被这条路由规则捕获,并将去除/launchpage后剩余部分捕获为变量

target: 定义目标路径:"/$1"表示将捕获的变量直接作为目标路径

localDir:将变换路径后的请求发送到本地文件目录

 


destination: 定义路径请求发送的目标,而这个srv-destination 是在 BTP 中配置的 Destination 服务,指向一个后端服务 URL(即mta.yaml中由后端服务srv provide出来的服务)

所以访问approuter的URL时,请求会在路径上附加welcomeFile的值,

而这个请求被下方定义的路由规则"source": "^/launchpage/(.*)"所抓取,就会将请求转发至与approuter文件夹同级的launchpage.html

当然我们也可以手动把localhost:5000后的路径修改为/app/webapp/index.html,使其被其他路由规则匹配,并导航到mitigations应用内,此时,前端UI会基于app/mitigations/webapp/manifest.json中的datasources – mainService – uri 中的地址去发送请求,即/odata/v4/service/risk

然后这个请求也会被route捕捉到,并转发给destination srv_api,该destination在云端部署时会根据mta.yaml内的配置生成,所以本地启动项目时需要额外配置default-env.json,如下文)

/////////////////////////////////////////

在approuter文件夹下创建一个本地启动时模拟destination服务的文件default-env.json,内容如下

{
    "destinations" : [
          {
            "name": "srv-destination",
            "url": "http://localhost:4004",
            "forwardAuthToken": true
          }
    ]
}

这里主要是将访问srv_api的请求转发至4004,也就是我们的cds watch所启动的后端服务URL

 

mta.yaml内添加三个module,

(这样就会将mitigations和risks的代码在打包时放进approuter文件夹内)

  - name: mitigations
    type: html5
    path: app/mitigations
    build-parameters:
      builder: custom
      commands:
        - npm install
        - npm ci
        - npm run build
      supported-platforms:
        []
      build-result: .

  - name: risks
    type: html5
    path: app/risks
    build-parameters:
      builder: custom
      commands:
        - npm install
        - npm ci
        - npm run build
      supported-platforms:
        []
      build-result: .

  - name: approuter
    type: javascript.nodejs
    path: app/approuter
    parameters:
      memory: 128M
    requires:
      - name: cpapp-auth
      - name: cpapp-db
      - name: srv-api
    build-parameters:
      requires:
        - name: mitigations
          artifacts:
            - './*'
          target-path: mitigations
        - name: risks
          artifacts:
            - './*'
          target-path: risks

    properties:
      SEND_XFRAMEOPTIONS: false
      destinations:
        - name: local
          url: https://sapui5.hana.ondemand.com/resources/
          forwardAuthToken: false
        - name: srv-destination
          url: ~{srv-api/srv-url}
          forwardAuthToken: true

 

在项目根目录下执行

npm install


在根目录的xs-security.json中根部位置添加以下代码

(此处的oncloud.top是我的BTP账号绑定的域名,令本应用可接收来自该域名的认证转发,请更改为你的应用域名)

, "oauth2-configuration": { "redirect-uris": [ "https://*.oncloud.top/**" ] }

 

ArthurYang_0-1733662041962.png

 


10.本地访问应用

/////////////////////////////////////////

注解:

因为mta内为approuter这个module配置了require,所以会在打包时将mitigations和risks的代码打包进approuter文件夹中,然后再将approuter文件夹内的代码全部部署,

所以approuter文件夹内会在打包后出现一套risks和一套mitigations代码,

所以打包过的代码可以在xs-app.json中使用./risks这个路径来找到approuter文件夹内的risks文件夹

未打包过的代码只能在xs-app.json中使用../risks路径来找到和approuter同级的risks文件夹)

/////////////////////////////////////////

 

进入approuter文件夹,执行npm install, 再执行npm start,即可在本地启动前端服务

再确保根目录的cds watch处于打开状态

浏览器中打开http://localhost:5000/app/webapp/index.html 即可看到risks这个app

 

 

11.使用HTML5Repo存储前端代码

我们先前创建过两个前端app,这里为了展示如何部署前端代码到HTML5Repo

模仿入门练习的步骤创建一个fiori elements应用risks2,所有配置仅仅修改三个参数:

Module Name:risks2

Application title:Risk2

以及最后不添加mta deployment参数

ArthurYang_0-1733722646884.png

 

生成代码后,来到根目录执行

cds add workzone-standard

此时观察代码,会发现一些变化,有部分变化是不适合部署的,让我们我们手动修改前端和mta配置文件:

 

修改app/risks2/webapp/manifest.json 下 的uri,

从   "/odata/v4/service/risk/",    修改为   "odata/v4/service/risk/",
也就是去掉一个/

ArthurYang_0-1736433103256.png

 

在自动生成的app/risks2/xs-app.json中的第二行添加以下代码(如果已经存在则忽略本步)

  "welcomeFile": "/index.html",

ArthurYang_0-1733716766654.png

这样可以在访问到该应用时自动导航到index.html

 

最后分别进入approuter ,mitigations, risks 内的xs-app

将cds命令自动修改的routes  "source": "^/odata/(.*)",   destination属性内容从cpapp-srv-api 改回 srv-destination

 

/////////////////////////////////////////

注解:

srv-destination就是我们在mta.yaml module approuter properties destinations 内配置的名字,其实这里也可以把risks2的xs-app destination修改为srv-destination, 但我们这里先保留,让risks2去调用写在mta-resources里的destination cpapp-srv-api

/////////////////////////////////////////

 

修改完前端项目内的内容后,我们打开mta.yaml,这里也有新增内容,此时

在mta.yaml根部的build-parameters before-all builder:custom commands下新增一条

        - mkdir -p resources  

这样在开始构建包之前就会先创建一个文件夹resources

 

然后找到module cpapp-app-deployer,将其下path的内容从”gen”修改为 “.”

将其下build-parameters内的 build-result 和 cpapprisks2的target-path 的内容从”app/”修改为“resources/”

 

这样该module就可以将risks2项目打包后的结果risk2.zipresources文件夹中拿出,部署到html5Repo中去了

 

同时因为我们希望仅仅将新创建的risks2部署到html5repo中去,所以

删除module cpapp-app-deployer parameters requires中的以下部分

(cpapp-app-deployer会将前端代码部署到html5Repo内,这里删除完就只会将risks2部署进html5Repo)

        - name: cpappapprouter 
          artifacts:
            - cpappapprouter.zip
          target-path: app/
        - name: cpappmitigations 
          artifacts:
            - cpappmitigations.zip
          target-path: app/
        - name: risks
          artifacts:
            - risks.zip
          target-path: app/

 

/////////////////////////////////////////

注解:

最后查看resources cpapp-destination parameters config init_data instance destinations,其中一个destination的名字即为cpapp-srv-api,risks2即调用的该destination(但其实cpapp-srv-api和srv-destination除了名字的参数都一样,这里只是为了教学意义同时保留两个destination定义,一个用于approute,risks,mitigations,一个用于risks2)

/////////////////////////////////////////

 

12.部署应用并访问

最后打包前将app/approuter/xs-app.json修改为以下代码,以打开安全措施,并将重定向目标指向approuter下的risks和mitigations文件夹(mbt打包后才会看到,这里暂时看不到两个文件夹)

{
    "welcomeFile": "/app/webapp/index.html",
    "routes": [
      {
        "source": "^/odata/(.*)",
        "target": "/odata/$1",
        "destination": "srv-destination"
      },
      {
        "source": "^/app/(.*)",
        "target": "/$1",
        "localDir": "./mitigations"
      }, 
      { 
        "source": "^/app2/(.*)",
        "target": "/$1", 
        "localDir": "./risks" 
      }
    ]
}

然后分别进入app/risks2,approuter,risks,mitigations文件夹,执行npm install

 

然后回到项目根目录,执行

mbt build -t ./

cf login -a <子账户概览内的 api endpoint> -o "<子账户概览内的Org Name>" -s <子账户内的space名> -u <登陆用的邮箱>

cf deploy cpapp_1.0.0.mtar

(在频繁部署的时候,我一般会在根目录创建一个文件deploy.sh,把mbt build和cf deploy这两个命令写进该文件,然后在根目录执行chmod +x deploy.sh来赋予权限,之后就可以通过在根目录执行./deploy.sh来一次性执行两个命令了)

 

如果部署出错,请查看报错,可能的原因及解决办法:

1.缺少对应服务的license:

本教程部署时需要:

Cloud Foundry Runtime的Memory权限

hana服务或SAP HANA Schemas & HDI Containers的hdi-shared权限,

xsuaa服务的全部权限,

 

2.没有可用的数据库:可能因为没有提前创建数据库instance,或者是未将数据库instance与目标子账户进行mapping,导致部署时找不到可用数据库,请查阅前文内容

 

3.没有可用的domain

确保已经配置custom domain,或者已经从其他子账户分享domain到目标子账户

cf login到拥有custom domains的子账户后

首先执行cf domains查看当前登陆的子账户有没有app.internal之外的可用域名

如果有,则执行cf share-private-domain <目标子账户概览下的org name> <可用域名例如abc.com>
来将域名共享给目标子账户

如果需要修改域名,请参考 Link ,

应用的URL(即路由)由host和domain组成,中间以英文句号(即一个点 . )链接,如果mta.yaml中不配置parameter-routes参数,默认路由则会是:“${org}-${space}-<module_name>.default-domain”

可以试试在mta.yaml - approuter-parameters下添加一个参数routes, 其下再添加几个route参数(当然,要把hardcode的domain替换成你的域名)

  - name: approuter
    type: javascript.nodejs
    path: app/approuter
    parameters:
      memory: 128M
      routes:
      - route: "routeTest.innolab.oncloud.top"
      - route: "${default-host}-connectTwoDefault.${default-domain}"
      - route: "${org}-connectOrgSpace-${space}-<module_name>.innolab.oncloud.top"

 

4.部署过程报错,提示xsappname不支持空格等特殊字符(很有可能和你的子账户-概览-org name有关)

那么最简单快速的方式是子账户-概览-禁用cloud foundry(该操作等同于删除cloud foundry环境实例),再重新启用cloud foundry,启用时将org name配置为没有空格的形式,然后创建space
最后cf login到任一子账户后,执行cf orgs来查看新的org名是否已经出现,若已出现则可以cf target -o <新的org名>

而根本的解决办法就是让app不再使用{org}变量来组成服务名称,可以将mta.yaml内自动生成的{org}删掉或者替换为固定字符串

 

5.如果成功部署后,space内的approuter应用url无法访问到,请找你的域名提供商检查解析服务

 

6.如果成功部署后,打开应用提示cannot process the request because the redirect uri does not match the configuration,请检查第10步结尾对xs-security的操作


一切顺利的话,命令行执行cf services即可看到当前自动创建好的服务:

再检查app是否正常运行:cf apps

 

打开approuter托管的前端界面:

在浏览器打开子账户-Cloud Foundry-空间-approuter内,routes属性的URL

https://XX-dev-approuter.XX

即会根据之前定义的xs-app.jsonwelcomeFile跳转至https://XX-dev-approuter.XX/app/webapp/index.html

此时即可看到Mitigations页面,把url中的app替换为app2即可切换到Risks页面

 

但是现在当前用户还没有对应的身份,所以点开服务也无法访问到数据,

需要去子账户内为本用户添加身份后,然后刷新cookie(也可以换一个浏览器打开应用URL),再打开界面,在risks应用内点击Go以查看数据

(在这里需要新建一个角色集合,然后将应用产生的两个角色分配至角色集合,最后把这个角色集合赋予自己

 

打开HTML5Repo托管的前端界面:

子账户-HTML5应用程序,点击打开nsrisks2应用后,点击go,即可看到数据,

 

完成本练习后可以继续完成系列练习 CAP for nodejs安全的暴露接口练习 https://community.sap.com/t5/technology-blogs-by-sap/cap-for-nodejs%E5%AE%89%E5%85%A8%E7%9A%84%E6%9A... ,如果还想把HTML5Repo托管的前端应用集成到SAP Build Workzone门户中去,请参考独立练习https://developers.sap.com/tutorials/integrate-with-work-zone.html



关于本文内容有任何问题或见解,欢迎在评论区留下你的想法