首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于云开发数据库的使用经验和建议

关于云开发数据库的使用经验和建议

原创
作者头像
腾讯云开发TCB
发布2021-04-22 20:28:06
7630
发布2021-04-22 20:28:06
举报

一、前言

小程序·云开发是微信团队联合腾讯云推出的专业的小程序开发服务。

开发者可以使用云开发快速开发小程序、小游戏、公众号网页等,并且原生打通微信开放能力。

开发者无需搭建服务器,可免鉴权直接使用平台提供的 API 进行业务开发。

数据库的上手、初始化等可参看官方链接:小程序·云开发

二、使用经验

直接使用云开发API

场景:页面或方法的逻辑简单,关联一个数据库,无联表查询

例子:

db.collection('todos').doc('todo-identifiant-aleatoire').get({
  success: function(res) {
    // res.data 包含该记录的数据
    console.log(res.data)
  }
})

使用数据聚合能力

场景:页面或方法的逻辑中等,关联多个数据库,可能存在联表查询或数据处理

例子:

const db = wx.cloud.database()
const $ = db.command.aggregate
db.collection('books').aggregate()
  .group({
    // 按 category 字段分组
    _id: '$category',
    // 让输出的每组记录有一个 avgSales 字段,其值是组内所有记录的 sales 字段的平均值
    avgSales: $.avg('$sales')
  })
  .end()

借助promise,async等

场景:页面或方法的逻辑较为复杂,关联多个数据库,可能存在多次查询以及云函数或https请求

以下是对云开发CMS导出数据的扩展案例

其中整合了上述的几种方式

例子:

const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
var xlsx = require('node-xlsx');
​
const db = cloud.database();
const MAX_LIMIT = 100;
const _ = db.command;
exports.main = async (event, context) => {
  console.log(event)
  event.queryStringParameters = event.queryStringParameters||{};
  const collection = event.collection || event.queryStringParameters.collection;
​
  const params = event.params || event.queryStringParameters.params || {};
  // const acceptType = ["String", "Tel", "Array", "Number", "Connect", "Boolean", "Enum", "Date", "DateTime"]; //"File","Image"
  const unacceptType = ["File", "Image"];
  const schemasRes = await db.collection("tcb-ext-cms-schemas").where({
    collectionName: collection
  }).get();
  const schemas = schemasRes.data[0];
​
  let connectList = [];
​
  const title = event.title || event.queryStringParameters.title || schemas.displayName || "数据";
​
  // 先取出集合记录总数
​
  const countRes = await db.collection(collection).where(params).count();
  const fields = schemas.fields.filter(function (schemas) {
    return unacceptType.indexOf(schemas.type) == -1 && (!schemas.isHidden);
  });
  const connectResourcenList = [];
  fields.forEach(field => {
    if (field.type == "Connect") {
      connectList.push(field);
      connectResourcenList.push(field.connectResource)
    }
  });
​
  const schemasListRes = await db.collection("tcb-ext-cms-schemas").where({
    _id: _.in(connectResourcenList)
  }).limit(MAX_LIMIT).get();
  const schemasList = schemasListRes.data || [];
​
  // console.log("fields==============================")
  console.log(schemasList)
  const total = countRes.total
  // 计算需分几次取
  const batchTimes = Math.ceil(total / MAX_LIMIT)
  // 承载所有读操作的 promise 的数组
  const tasks = []
  for (let i = 0; i < batchTimes; i++) {
    //console.log(connectList.length)
    if (connectList.length > 0) {
      let lookupList = [];
      connectList.forEach(connect => {
        const connectschemas = schemasList.filter(function (schemas) {
          return schemas._id == connect.connectResource;
        })[0];
        lookupList.push({
          from: connectschemas.collectionName,
          localField: connect.name,
          foreignField: '_id',
          as: "connect" + connect.name
        })
​
      });
​
      let aggregate = db.collection(collection).aggregate().match(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT);
​
      for (let index = 0; index < connectList.length; index++) {
        aggregate = aggregate.lookup(lookupList[index]);
​
      }
​
      aggregate = aggregate.end();
​
      tasks.push(aggregate)
    } else {
​
      const promise = db.collection(collection).where(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get();
      tasks.push(promise)
    }
​
  }
  console.log(tasks)
  // 等待所有
  let recordRes = (await Promise.all(tasks)).reduce((acc, cur) => {
​
    return {
      list: (acc.list || []).concat(cur.list || []),
      data: (acc.data || []).concat(cur.data || []),
    }
  })
​
  let records = (recordRes.list || []).concat(recordRes.data || []) || [];
  //1.定义表格名
  let dataCVS = title + '.xlsx';
  let excelData = [];
  let row = [];
  fields.forEach(field => {
    row.push(field.displayName)
  });
​
  excelData.push(row);
​
  records.forEach(record => {
    let arr = [];
    fields.forEach(field => {
      if (!record.hasOwnProperty(field.name)) {
        arr.push("")
      } else {
        switch (field.type) {
          case "Connect":
            arr.push(join2Str(record["connect" + field.name], field.connectField))
            break;
          case "DateTime":
            arr.push(formatDateTime(record[field.name]))
            break;
          case "Date":
            arr.push(formatDate(record[field.name]))
            break;
          case "Boolean":
            arr.push(record[field.name] ? "是" : "否")
            break;
            case "Enum":
              let enumElements = field.enumElements;
              let enumElement= enumElements.find(function(item){
                return item.value = record[field.name];
              })
              
              arr.push(enumElement.label)
              break;
          default:
            arr.push(record[field.name])
            break;
        }
      }
​
    });
    excelData.push(arr);
  });
​
  //3,把数据保存到excel里
  var buffer = await xlsx.build([{
    name: title,
    data: excelData
  }]);
  //4,把excel文件保存到云存储里
  const excelFileIdRes = await cloud.uploadFile({
    cloudPath: dataCVS,
    fileContent: buffer, //excel二进制文件
  });
​
  return await cloud.getTempFileURL({
    fileList: [excelFileIdRes.fileID]
  }).then(function (res) {
    return res.fileList[0].tempFileURL
  })
​
}
​
function join2Str(obj, fieldName) {
  if (Object.prototype.toString.call(obj) == "[object Array]") {
    let resultArr = [];
    obj.forEach(item => {
      if (item.hasOwnProperty(fieldName))
        resultArr.push(item[fieldName])
    });
    return resultArr.join(",")
  } else {
    if (obj.hasOwnProperty(fieldName))
      return obj[fieldName]
  }
}
​
function formatDateTime(inputTime) {
  var date = new Date(inputTime);
  var y = date.getFullYear();
  var m = date.getMonth() + 1;
  m = m < 10 ? ('0' + m) : m;
  var d = date.getDate();
  d = d < 10 ? ('0' + d) : d;
  var h = date.getHours();
  h = h < 10 ? ('0' + h) : h;
  var minute = date.getMinutes();
  var second = date.getSeconds();
  minute = minute < 10 ? ('0' + minute) : minute;
  second = second < 10 ? ('0' + second) : second;
  return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second;
};
​
function formatDate(inputTime) {
  var date = new Date(inputTime);
  var y = date.getFullYear();
  var m = date.getMonth() + 1;
  m = m < 10 ? ('0' + m) : m;
  var d = date.getDate();
  d = d < 10 ? ('0' + d) : d;
  return y + '-' + m + '-' + d;
};

整合数据库框架

场景:小程序或APP的业务逻辑复杂,模板页面的开发,组件的开发和统一异常处理

例子:

以下例子引用了wxboot的小程序框架

//app.js
// const {WXBoot} = require('wxbootstart');
​
​
require('./lib-webpack/wxboot');
import login from "./login/login"
​
import utils from "./utils/utils"
import constants from "./constants/constants"
App.A({
  config: {
    initCloud:{ 
      // env: '',
    traceUser: true,},
    route: '/pages/$page/$page',
    pageApi: utils,
    consts: constants,
    updata:{
      arrObjPath:false,
      arrCover:false
    },
    mixins:[login,App.A.Options] ,
  },
  getOpenidFunc: function(){
    return this.cloud.callFunction({
      name:"getWXContext"
    }).then(res=>{
      return  res.result.openid;
    }).catch(err=>{ 
      console.error(err)
      return ""
    })
  },
  onLaunch: function (opts) {
​
    App.A.on('some_message', function (msg) {
      console.log('Receive message:', msg)
    })
    console.log('APP is Running', opts)  
​
  },
  store: {
    id: 0
  },
​
  auth:{
    canUseXXX:false
  },
​
  globalData: {
    version: "v1.0.0",
    id: 0,
    userInfo: null,
    addressInfo: null,
    sessionKey: null,
    loginTime: 0,
    openid: "",
    theme: {
      color: "#FFFFFF"
    },
    share: {
      title: "开启一天好运",
      imageUrl: "https://XXX.jpg",
      path: "/pages/index/index"
    },
    settings: null
  },
​
  onAwake: function (time) {
    console.log('onAwake, after', time, 'ms')
  },
  onShow: function () {
    console.log('App onShow')
  },
  /*小程序主动更新
   */
  updateManager() {
    if (!wx.canIUse('getUpdateManager')) {
      return false;
    }
    const updateManager = wx.getUpdateManager();
    updateManager.onCheckForUpdate(function (res) {});
    updateManager.onUpdateReady(function () {
      wx.showModal({
        title: '有新版本',
        content: '新版本已经准备好,即将重启',
        showCancel: false,
        success(res) {
          if (res.confirm) {
            updateManager.applyUpdate()
          }
        }
      });
    });
    updateManager.onUpdateFailed(function () {
      wx.showModal({
        title: '更新提示',
        content: '新版本下载失败',
        showCancel: false
      })
    });
  },
  "navigateToMiniProgramAppIdList": [
    "wx8abaf00ee8c3202e"
​
  ]
})
​

全局封装增删改 ,我们更专注的关注于业务逻辑,统一异常处理

module.exports = {
​
  $callFun: callFunction,
  $add: add,
  $get: get,
  $update: update,
  $remove: remove,
  $count:count
}
​
//取数据库实例。一个数据库对应一个实例
​
/**
 * 封装查询操作
 * 增 查 改 删
 * 
 */
​
//增
async function add(collectionName, data, openParse = false) {
  if (openParse) {
    data = await parseQuery(data, this)
  }
  return this.$collection(collectionName).add({
    data
  }).then(res => {
    return res._id
  }).catch(res => {
    return ""
  })
}
​
//查询
//对应id取不到的时候,返回{}
async function get(collectionName, query, openParse = false) {
  switch (type(query)) {
    case "string":
      return this.$collection(collectionName).doc(query).get().then(res => {
        return res.data
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return {}
      })
    case "object":
      const defaultOptions = {
        where: null,
        order: null,
        skip: 0,
        limit: 20,
        field: null,
        pageIndex: 1
      }
      const parsequery = setDefaultOptions(query, defaultOptions);
      let {
        where, order, skip, limit, field, pageIndex
      } = parsequery;
      let collectionGet = this.$collection(collectionName);
      if (where != null) {
        if (openParse) {
          where = await parseQuery(where, this)
        }
        collectionGet = collectionGet.where(where)
      }
      if (order != null) {
        if (type(order) == "object") {
          collectionGet = collectionGet.orderBy(order.name, order.value);
        }
        if (type(order) == "array") {
          order.forEach(orderItem => {
            collectionGet = collectionGet.orderBy(orderItem.name, orderItem.value);
          });
        }
      }
      if (field) {
        collectionGet = collectionGet.field(field);
      }
      if (pageIndex > 1) {
        collectionGet = collectionGet.skip((pageIndex - 1) * limit).limit(limit);
      } else {
        collectionGet = collectionGet.skip(skip).limit(limit);
      }
      return collectionGet.get().then(res => {
        return res.data
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return []
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return null;
  }
}
​
async function count(collectionName, query, openParse = false) {
  switch (type(query)) {
     
    case "object":
      let collectionUpdate = this.$collection(collectionName);
      if (openParse) {
        query = await parseQuery(query, this)
      }
      collectionUpdate = collectionUpdate.where(query)
      return collectionUpdate.count().then(res => {
        return res.total
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
    default:
      return this.$collection(collectionName).count().then(res => {
        return res.total
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
     
  }
}
​
//修改
async function update(collectionName, query, updata, openParse = false) {
  switch (type(query)) {
    case "string":
      return this.$collection(collectionName).doc(query).update({
        data: updata
      }).then(res => {
        return res.stats.updated
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return 0
      })
    case "object":
      let collectionUpdate = this.$collection(collectionName);
      if (openParse) {
        query = await parseQuery(query, this)
      }
      collectionUpdate = collectionUpdate.where(query)
      return collectionUpdate.update({
        data: updata
      }).then(res => {
        return res.stats.updated
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return 0
  }
}
​
​
//删除
async function remove(collectionName, query, openParse=false) {
  switch (type(query)) {
    case "string":
      return this.$collection(collectionName).doc(query).remove().then(res => {
        return res
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return {}
      })
    case "object":
      let collectionRemove = this.$collection(collectionName);
      if (openParse) {
        query = await parseQuery(query, this)
      }
      collectionRemove = collectionRemove.where(query)
      return collectionRemove.remove().then(res => {
        return res
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return []
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return 0
  }
}
​
​
function setDefaultOptions(options = {}, defaultOptions = {}) {
  return Object.assign(defaultOptions, options);
}
​
function promisify(api) {
  return (options, ...query) => {
    return new Promise((resolve, reject) => {
      api(Object.assign({}, options, {
        success: resolve,
        fail: reject
      }), ...query);
    })
  }
}
​
async function callFunction(options) {
  return await this.cloud.callFunction(options)
}
​
var undef = void(0)
function type(obj) {
  if (obj === null) return 'null'
  else if (obj === undef) return 'undefined'
  var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj))
  return m ? m[1].toLowerCase() : ''
}
async function parseQuery(query, self) {
  let queryStr = JSON.stringify(query);
  if (queryStr.indexOf("{openid}") > -1) {
    let openid = await self.$getOpenid();
    return JSON.parse(queryStr.replace(/{openid}/g, openid));
  } else {
    return query
  }
}

高级用法,结合云函数和https 以及封装api ,实现统一对外接口,对接其他语言

场景:多项目,多后台,多端打通,数据迁移等

// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
});
const db = cloud.database();
// 云函数入口函数
exports.main = async (event, context) => {
  let body = event.body;
​
  let cloudParams = urlToObj(decodeURIComponent(body));
  let {
    cloudType,
    collectionName
  } = cloudParams;
​
  let data = JSON.parse(cloudParams.data || "{}");
​
  let query = JSON.parse(cloudParams.query || "{}");
  if(type(query)=="object"){
    
  query.where =  JSON.parse(query.where ||"{}" );
    if(query.field)
  query.field =  JSON.parse(query.field ||"{}" );
  }   
  console.log(query)
  let promise = null;
  switch (cloudType) {
    case "ADD":
      promise = add(collectionName, data);
      break;
​
    case "GET":
      promise = get(collectionName, query)
      break;
    case "UPDATE":
      promise = update(collectionName, query, data)
      break;
    case "REMOVE":
      promise = remove(collectionName, query)
      break;
    case "COUNT":
​
      let countquery = null;
      if (type(query) == "string") {
        countquery = query
      } else {
        countquery = query.where || null
      }
      promise = count(collectionName, countquery)
      break;
​
    default:
      break;
  }
  return promise;
}
​
function urlToObj(str) {
  var obj = {};
​
  var arr2 = str.split("&");
  for (var i = 0; i < arr2.length; i++) {
    var res = arr2[i].split("=");
    obj[res[0]] = res[1] || "";
  }
  return obj;
}
​
//增
async function add(collectionName, data, openParse = false) {
  if (openParse) {
    data = await parseQuery(data)
  }
  return db.collection(collectionName).add({
    data
  }).then(res => {
​
    return res._ids || res._id;
  }).catch(res => {
    return ""
  })
}
​
//查询
//对应id取不到的时候,返回{}
async function get(collectionName, query, openParse = false) {
  if (query.limit && query.limit == "all") {
    let countquery = null;
    if (type(query) == "string") {
      countquery = query
    } else {
      countquery = query.where || null
    }
    // 先取出集合记录总数
    const total = await count(collectionName, countquery);
​
    // 计算需分几次取
    const batchTimes = Math.ceil(total / 20)
    // 承载所有读操作的 promise 的数组
    const tasks = []
    for (let i = 0; i < batchTimes; i++) {
      query.limit = 20;
      query.pageIndex = i + 1;
      const promise = get(collectionName, query);
      tasks.push(promise)
    }
    // 等待所有
    return (await Promise.all(tasks)).reduce((acc, cur) => {
      acc = acc || [];
      cur = cur || [];
      return acc.concat(cur);
    })
  }
​
  switch (type(query)) {
    case "string":
      return db.collection(collectionName).doc(query).get().then(res => {
        return res.data
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return {}
      })
    case "object":
      const defaultOptions = {
        where: null,
        order: null,
        skip: 0,
        limit: 20,
        field: null,
        pageIndex: 1
      }
      const parsequery = setDefaultOptions(query, defaultOptions);
      let {
        where, order, skip, limit, field, pageIndex
      } = parsequery;
      let collectionGet = db.collection(collectionName);
      if (where != null) {
        if (openParse) {
          where = await parseQuery(where)
        }
        collectionGet = collectionGet.where(where)
      }
      if (order != null) {
        if (type(order) == "object") {
          collectionGet = collectionGet.orderBy(order.name, order.value);
        }
        if (type(order) == "array") {
          order.forEach(orderItem => {
            collectionGet = collectionGet.orderBy(orderItem.name, orderItem.value);
          });
        }
      }
      if (field) {
        collectionGet = collectionGet.field(field);
      }
      if (pageIndex > 1) {
        collectionGet = collectionGet.skip((pageIndex - 1) * limit).limit(limit);
      } else {
        collectionGet = collectionGet.skip(skip).limit(limit);
      }
      return collectionGet.get().then(res => {
        return res.data
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return []
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return null;
  }
}
​
async function count(collectionName, query, openParse = false) {
  switch (type(query)) {
​
    case "object":
      let collectionUpdate = db.collection(collectionName);
      if (openParse) {
        query = await parseQuery(query)
      }
      collectionUpdate = collectionUpdate.where(query)
      return collectionUpdate.count().then(res => {
        return res.total
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
    default:
      return db.collection(collectionName).count().then(res => {
        return res.total
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
​
  }
}
​
//修改
async function update(collectionName, query, updata, openParse = false) {
  switch (type(query)) {
    case "string":
      return db.collection(collectionName).doc(query).update({
        data: updata
      }).then(res => {
        return res.stats.updated
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return 0
      })
    case "object":
      let collectionUpdate = db.collection(collectionName);
      if (openParse) {
        query = await parseQuery(query)
      }
      collectionUpdate = collectionUpdate.where(query)
      return collectionUpdate.update({
        data: updata
      }).then(res => {
        return res.stats.updated
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return 0
  }
}
​
//删除
async function remove(collectionName, query, openParse = false) {
  switch (type(query)) {
    case "string":
      return db.collection(collectionName).doc(query).remove().then(res => {
        return res
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return {}
      })
    case "object":
      let collectionRemove = db.collection(collectionName);
      if (openParse) {
        query = await parseQuery(query)
      }
      collectionRemove = collectionRemove.where(query)
      return collectionRemove.remove().then(res => {
        return res
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return []
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return 0
  }
}
​
function setDefaultOptions(options = {}, defaultOptions = {}) {
  return Object.assign(defaultOptions, options);
}
​
function promisify(api) {
  return (options, ...query) => {
    return new Promise((resolve, reject) => {
      api(Object.assign({}, options, {
        success: resolve,
        fail: reject
      }), ...query);
    })
  }
}
​
var undef = void(0)
​
function type(obj) {
  if (obj === null) return 'null'
  else if (obj === undef) return 'undefined'
  var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj))
  return m ? m[1].toLowerCase() : ''
}
async function parseQuery(query) {
  let queryStr = JSON.stringify(query);
  if (queryStr.indexOf("{openid}") > -1) {
    let openid = cloud.getWXContext().OPENID;
    return JSON.parse(queryStr.replace(/{openid}/g, openid));
  } else {
    return query
  }
}

三、建议

  • 云开发是主要是类似mongdb的非关系数据库,可以保存json的数据,我们可以多直接保存复杂的值
  • 尝试使用自己封装的业务逻辑来全局控制异常等
  • 数据库的权限、索引等可以对数据库检索性能进一步优化

产品介绍

云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流群、最新资讯关注微信公众号【腾讯云云开发】

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、使用经验
    • 直接使用云开发API
      • 使用数据聚合能力
        • 借助promise,async等
          • 整合数据库框架
            • 高级用法,结合云函数和https 以及封装api ,实现统一对外接口,对接其他语言
            • 三、建议
            • 产品介绍
            相关产品与服务
            云开发 CloudBase
            云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档