Serverless 学习 04 云开发高阶应用(FaaS 运行机制、CloudBase Framework、重构 TodoList、文件上传及云存储、用户管理及登录授权、绑定自定义域名)

FaaS 的运行机制

参考:

在 FaaS 平台中,函数默认是不运行的,也不会分配任何资源。甚至 FaaS 中都不会保存函数代码。只有当 FaaS 接收到触发器的事件后,才会启动并运行函数。

前面我们就是使用 HTTP 的触发器来执行函数代码的,整个函数的运行过程实际上可以分为四个阶段:

  • 代码下载:FaaS 平台本身不会存储代码,而是将代码放在对象存储中,需要执行函数的时候,再从对象存储中将函数代码下载下来并解压,因此 FaaS 平台一般都会对代码包的大小进行限制,通常代码包不能超过 50 MB。
  • 启动容器:代码下载完成后,FaaS 会根据函数的配置,启动对应容器,FaaS 使用容器进行资源隔离。
  • 初始化运行环境:分析代码依赖、执行用户初始化逻辑、初始化入口函数之外的代码等。
  • 运行代码:调用入口函数执行代码。

当函数第一次执行时,会经过完整的四个步骤,前三个过程称为“冷启动”,最后一步称为“热启动”。

在这里插入图片描述

整个冷启动流程耗时可能达到百毫秒级别。

函数运行完毕后,运行环境会保留一段时间(几十分钟),这和具体云厂商有关。如果这段时间内函数需要再次执行,则 FaaS 平台就会使用上一次的运行环境,这就是**“执行上下文重用”,也叫做“实例复用”,函数的这个启动过程也叫“热启动”**。“热启动”的耗时就完全是启动函数的耗时了。

当一段时间内没有请求时,函数运行环境就会被释放,直到下次事件到来,再重新从冷启动开始初始化。

考虑下面这个云函数:

let i = 0
exports.main = async (event = {}) => {
  i++
  console.log(i)
  return i
}

在第一次调用该云函数的时候函数返回值为 1,这是符合预期的。

但如果连续调用这个云函数,其返回值有可能是从 2 递增,也有可能变成 1,这便是实例复用的结果:

  • 热启动时,执行函数的 Node.js 进程被复用,进程的上下文也得到了保留,所以变量 i 自增
  • 冷启动时,Node.js 进程是全新的,代码会从头完整的执行一遍,此时返回 1

可以在控制台在线编辑云函数并测试:

请添加图片描述

下面是一个函数的请求示意图,其中“请求1”“请求3”是冷启动,“请求2”是热启动。

在这里插入图片描述

函数执行完毕后销毁运行环境,虽然对首次函数执行的性能有损耗,但极大的提高了资源利用率,只有需要执行代码的时候才初始化环境、消耗硬件资源。并且如果你的应用请求量比较大,则大部分时候函数的执行可能都是热启动。

从函数运行的生命周期中你可以发现,如果函数每分钟都执行,则函数机会都是热启动的,也就是会重复使用之前的执行上下文。执行上下文就包括函数的容器环境、入口函数之外的代码。

云平台会根据当前负载情况,自动控制云函数实例的数量,并且均衡的分发请求。连续的多次请求有可能由同一实例处理,也可能不是。这就是我们在上面的代码中看到的,i 值非常放肆,根本就找不到规律。

所以,我们在编写云函数时,应注意保证云函数是无状态的、幂等的,即当次云函数的执行不依赖上一次云函数执行过程中在运行环境中残留的信息。

再次回到 TodoList 案例中,因为我们将全部的业务逻辑放到了一个云函数中,因此,处理的并发量会收到极大的限制,当并发量达到一定程度的时候,无法创建更多的函数实例,也就无法分配更多的服务器资源。

更好的方式,是对我们的业务逻辑进行拆分,一个云函数就对应一个独立的业务逻辑处理。

这在小程序的云开发中就有体现,默认给我们的小程序云开发模板中,就是一个小程序应用对应多个云函数的处理方式。

在这里插入图片描述

如图,cloudfunctions 就是存放云函数的目录,一个云函数对应一个文件夹(quickstartFunctions),其中 index.js 是云函数的入口文件。

我们可以借助小程序给我们的思路去改善 TodoList 应用。

CloudBase Framework

腾讯提供了一个 CloudBase Framework 工具,在手册-工具插件中可以找到。

这个工具提供了有一种方式,我们前面使用 CloudBase CLI 命令行工具,就是使用 CloudBase Framework 的对外接口工具。也就是说,我们使用的命令行,实际就是调用了 CloudBase Framework 提供的功能。

例如:tcb new 创建应用、tcb 应用部署、tcb service create 创建 HTTP 触发器,tcb fn code update <函数名> 增量更新代码,除了这些部署代码相关的命令,CloudBase Framework 还提供了一站式管理云平台资源的能力。

下面,我们按照 Serverless 的开发模式,对 TodoList 案例进行重构。

创建应用

在腾讯云开发 CloudBase 下,已经给我们创建好了各种各样的开发模板,使用 tcb new 这个命令就可以看到,在选择应用模板时,选择 Vue 应用,就可以创建一个 Vue 云开发的项目。

使用前提:npm i -g @cloudbase/cli

# 创建应用
tcb new
√ 请选择环境所在地域 · 广州
√ 请选择关联环境 · mycloudes - [mycloudes-7g8wlhkr324798db:按量计费]
√ 请选择应用模板 · Vue 应用
√ 请输入项目名称 · todos
√ 创建应用成功!
i 👉 开发完成后,执行命令 tcb 一键部署

查看应用 package.json 可以发现应用使用的是 Vue2,并增加了一个 @cloudbase/vue-provider 的依赖:

"dependencies": {
  "@cloudbase/vue-provider": "^0.5.1",
  "core-js": "^3.6.4",
  "vue": "^2.6.11"
},
# 进入应用目录
cd todos
# 安装依赖
npm i

查看存放云函数的目录 cloudfunctions,里面默认会生成一个名为 vue-echo 的云函数模板,里面默认引入了 @cloudbase/node-sdk

# 进入云函数目录
cd cloudfunctions/vue-echo
# 安装依赖
npm i

配置说明

参考:配置说明

应用中所有和云函数相关的配置都放在了 cloudbaserc.json 中,对于本地开发来说,这个文件就是对整个项目的配置文件:

{
  "version": "2.0",
  "envId": "mycloudes-7g8wlhkr324798db",
  "$schema": "https://framework-1258016615.tcloudbaseapp.com/schema/latest.json",
  "functionRoot": "cloudfunctions",
  "framework": {
    "name": "vue",
    "plugins": {
      "client": {
        "use": "@cloudbase/framework-plugin-website",
        "inputs": {
          "buildCommand": "npm run build",
          "outputPath": "dist",
          "cloudPath": "/vue",
          "envVariables": {
            "VUE_APP_ENV_ID": "{{env.ENV_ID}}"
          }
        }
      },
      "server": {
        "use": "@cloudbase/framework-plugin-function",
        "inputs": {
          "functionRootPath": "cloudfunctions",
          "functions": [
            {
              "name": "vue-echo",
              "timeout": 5,
              "envVariables": {},
              "runtime": "Nodejs10.15",
              "memory": 128,
              "aclRule": {
                "invoke": true
              }
            }
          ]
        }
      },
      "auth": {
        "use": "@cloudbase/framework-plugin-auth",
        "inputs": {
          "configs": [
            {
              "platform": "NONLOGIN",
              "status": "ENABLE"
            }
          ]
        }
      }
    }
  },
  "functions": [],
  "region": "ap-guangzhou"
}

  • version:值为 2.0,从 2.0 开始支持了一个叫做模板变量的特性,所谓“模板变量”即可以在应用目录下创建一个 .env 的配置文件,在里面配置一些变量(如 密钥),在 cloudbaserc.json 配置文件中通过 {{env.<变量名>}} 的方式引用,而 .gitignore 中配置了 .env 文件,所以在提交 git 的时候 .env 文件就会保留在本地,而不会提交,一面泄漏关键内容。
  • envId:环境 id
  • $schema:配置模板的描述信息
  • functionRoot:云函数存放路径
  • framework:CloudBase Framework 的配置信息
    • name:整个应用的名称(唯一标识),一个账号下不能有重名的应用名称
    • plugins:Framework 是支持插件机制的,即云官方提供了多种应用框架和云资源的插件,应用依赖哪些插件都在 plugins 中进行配置。Framework 会根据 plugins 配置来管理应用,处理应用中的进行构建、管理、开发、调试等流程。一个应用可以有多个插件(官方提供的插件),使用不同的自定义的名字对插件进行管理,例如模板默认配置了三个插件:
      • server:自定义插件配置的名称
        • use:引入使用的插件,@cloudbase/framework-plugin-function 是一键部署函数资源的插件
        • inputs:当前插件的配置信息
          • functionRootPath:云函数的本地存放路径
          • functions:具体的云函数的配置,如 vue-echo
            • name:云函数名称,如要修改,也要同步修改文件夹名称
            • timeout:运行超时时间
            • envVariables:环境变量的键值对对象
            • runtime:运行环境的语言和版本
            • memory:最大可分配内存
      • client:自定义插件配置的名称
        • use@cloudbase/framework-plugin-website 一键部署网站应用的静态网站插件,可以把 Vue、React 等框架的内容打包并上传到静态站点中。
        • inputs:插件配置,包括打包命令,打包路径,上传路径等
        • 过程就是在执行 tcb 命令后,会执行 npm run buildbuildCommand)命令,把打包生成的 distoutputPath) 目录中内内容,上传到静态网站托管管理中的 /vuecloudPath)路径中。
  • region:当前应用所在的地域

快速体验

将云函数的名称修改为 gettodo(一个业务逻辑对应一个云函数),注意要修改两个地方:

  • cloudbaserc.json 配置文件中的 framework.plugins.server.inputs.functions[0].name
  • vue-echo 文件夹名称

执行命令:

# 在 todos 目录下执行命令
tcb

查看静态网站托管:

在这里插入图片描述

可以访问 https://mycloudes-xxx-xxx.tcloudbaseapp.com/vue/index.html 查看效果

重构 TodoList 案例

封装初始化 SDK 实例

// todos\cloudfunctions\gettodo\db.js
const nodesdk = require('@cloudbase/node-sdk')

// 初始化 SDK
const app = nodesdk.init({
  env: 'mycloudes-xxx',
  secretId: 'xxx',
  secretKey: 'xxx',
  region: 'ap-guangzhou'
})

const db = app.database()

module.exports = db

修改 gettodo 业务逻辑

// todos\cloudfunctions\gettodo\index.js
const db = require("./db")

async function showTodo() {
  const backdb = await db.collection('todo').get()
  return backdb
}

exports.main = async (event, context) => {
  return showTodo()
}

部署

使用 tcb 命令会将整个应用进行打包部署,包括 todos\src 的客户端静态资源,所以只部署指定云函数的代码:

# 在 todos 目录下执行
tcb fn code update gettodo

在这里插入图片描述

创建触发器(HTTP 访问服务):

在这里插入图片描述

访问触发器测试结果。

可是又报错地域问题,region 也传了的。经检查,原来是因为 @cloudbase/cli 创建的 Vue 项目模板中默认引入的 node-sdk 的版本是 1.1.1,所以才会出现这个问题,将其改为最新的版本(本例是 2.9.0)即可。

// package.json 原内容
{
"name": "helloworld",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"author": "",
"license": "ISC",
"dependencies": {
 "@cloudbase/node-sdk": "1.1.1"
}
}

客户端调用云函数

业务逻辑非常多的情况下,就要创建很多触发器,这样的开发体验是非常不友好的。

其实云函数是可以在客户端直接调用的,因此就没有必要再去创建触发器了。

在 Vue 中调用云函数不需要通过 HTTP 请求,腾讯官方给我们封装了 Vue 的插件@cloudbase/vue-provider,提供全局入口、Vue 逻辑组件等功能。

Github 地址:https://github.com/TencentCloudBase/cloudbase-vue

Vue 模板的应用,默认已经安装了这个插件,修改环境 id 和 地域:

// todos\src\main.js
import Vue from "vue";
import App from "./App.vue";
import Cloudbase from "@cloudbase/vue-provider";

// 注意更新此处的TCB_ENV_ID为你自己的环境ID
window._tcbEnv = window._tcbEnv || {TCB_ENV_ID:"hello-cloudbase-test"};

export const envId = window._tcbEnv.TCB_ENV_ID;
export const region = window._tcbEnv.TCB_REGION;

Vue.config.productionTip = false;

Vue.use(Cloudbase, {
  env: 'xxx',
  region: 'ap-guangzhou'
});

new Vue({
  render: h => h(App)
}).$mount("#app");

启动客户端应用,访问 http://localhost:5000/vue

# 在 todos 下云星星
npm run dev

在访问页面的时候控制台会有一个 Uncaught SyntaxError: Unexpected token '<' (at _init_tcb-env.js:1:1) 的报错,这是因为在模板文件中引入了 /_init_tcb-env.js,这是部署到线上才会加载的资源,主要就是用来配置 _tcbEnv,本地开发可以先将其注释掉。

todos\src\components\HelloWorld.vue 组件中有一个调用云函数的实例,点击链接【调用 hello world 云函数】调用 callFunction 方法,相关文档并没有在【Vue 插件文档】中介绍,而是在【API 参考 - JavaScript(v1) - 云函数】 中。

注意:插件中使用的 cloudbase-js-skd 版本是 1.x.x,所以查看 JavaScript(v1) 版本的文档。

// todos\src\components\HelloWorld.vue
async callFunction() {
  try {
    const res = await this.$cloudbase.callFunction({
      name: "vue-echo", // 云函数名称
      data: {
        foo: "bar",
      }, // 云函数参数
    });
    this.callFunctionResult = res;
  } catch (e) {
    console.error(e)
    this.callFunctionResult = e.message;
  }
},

修改为 gettodo 云函数并打印:

// todos\src\components\HelloWorld.vue
async callFunction() {
  try {
    const res = await this.$cloudbase.callFunction({
      name: "gettodo", // 云函数名称
    });
    console.log(res)
    // this.callFunctionResult = res;
  } catch (e) {
    console.error(e)
    this.callFunctionResult = e.message;
  }
},

初始化多个云函数

修改配置文件 cloudbaserc.json

{
  ...
  "framework": {
    "name": "vue",
    "plugins": {
      "client": {...},
      "server": {
        "use": "@cloudbase/framework-plugin-function",
        "inputs": {
          "functionRootPath": "cloudfunctions",
          "functions": [
            {
              "name": "gettodo",
              "timeout": 5,
              "envVariables": {},
              "runtime": "Nodejs10.15",
              "memory": 128,
              "aclRule": {
                "invoke": true
              }
            },
            {
              "name": "addtodo",
              "timeout": 5,
              "envVariables": {},
              "runtime": "Nodejs10.15",
              "memory": 128,
              "aclRule": {
                "invoke": true
              }
            },
            {
              "name": "puttodo",
              "timeout": 5,
              "envVariables": {},
              "runtime": "Nodejs10.15",
              "memory": 128,
              "aclRule": {
                "invoke": true
              }
            },
            {
              "name": "deltodo",
              "timeout": 5,
              "envVariables": {},
              "runtime": "Nodejs10.15",
              "memory": 128,
              "aclRule": {
                "invoke": true
              }
            }
          ]
        }
      },
      "auth": {...}
    }
  },
  ...
}

todos/cloudfunctions 下分别创建 addtodoputtododeltodo 文件夹,将 gettodo 下的 index.jsdb.jspackage.json 复制到三个文件夹下,记得分别安装依赖。

完善添加任务功能

// todos\cloudfunctions\addtodo\index.js
const db = require("./db")

async function addTodo(event) {
  // 校验参数
  if (!event.title) {
    return '缺少 title'
  }
  const todo = {
    title: event.title,
    createTime: Date.now(),
    done: false
  }

  const backdata = await db.collection('todo').add(todo)
  return backdata
}

exports.main = async (event, context) => {
  return addTodo(event)
}

写完之后使用 scf-cli 和 postman 在本地测试云函数。

# 测试通过后部署到云平台
tcb fn code update addtodo

注意:虽然手册上没有说明,如果使用了 Vue、React 的插件请求云函数,传入的 event 对象是和小程序调用的方式一样,event 是调用云函数传入的参数,没有请求信息数据。

安装并配置 ElementUI:

npm i element-ui
// todos\src\main.js
import Vue from "vue";
import App from "./App.vue";
import Cloudbase from "@cloudbase/vue-provider";

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI)

...

修改客户端代码:

<!-- todos\src\App.vue -->
<template>
  <div id="app">
    <Index />
  </div>
</template>

<script>
import Index from "./components/Index.vue";

export default {
  name: "App",
  components: {
    Index,
  },
};
</script>

<style></style>

<!-- todos\src\components\Index.vue -->
<template>
  <div>
    <el-card shadow="always" class="box-card">
      <el-input v-model="addData.title" placeholder="请输入内容">
        <el-button slot="append" @click="addTodo">添加任务</el-button>
      </el-input>
    </el-card>
  </div>
</template>

<script>
export default {
  name: '',
  data() {
    return {
      addData: {
        title: ''
      }
    }
  },
  methods: {
    async addTodo() {
      const backdata = await this.$cloudbase.callFunction({
        name: 'addtodo',
        data: this.addData
      })
      console.log(backdata)
    }
  }

}
</script>

<style>
.dsb {
  color: #606266;
}
.text{
  font-size: 14px;
}
.item{
  margin-bottom: 18px;
}

.clearfix::before,
.clearfix::after {
  display: table;
  content: "";
}
.clearfix::after {
  clear: both;
}

.box-card{
  margin: 0 auto;
  width: 580px;
  margin-bottom: 20px;
}
.lists{
  display:flex;
  justify-content: space-between;
}
</style>

测试添加任务,成功后 tcb 部署到云平台。

注意:虽然 Vue 插件中调用云函数不需要通过触发器,但是调用的不是本地开发的代码,而是部署到云平台的代码,所以修改完云函数必须要部署上去才能使用。

渲染列表

<!-- todos\src\components\Index.vue -->
<template>
  <div>
    <el-card shadow="always" class="box-card">
      <el-input v-model="addData.title" placeholder="请输入内容">
        <el-button slot="append" @click="addTodo">添加任务</el-button>
      </el-input>
    </el-card>

    <el-card class="box-card">
      <div v-for="(todo, key) in todos" :key="key" class="text item">
        <template v-if="todo.done === false">
          <div class="lists">
            <el-checkbox v-model="todo.done" @change="todoDone(todo)">{{ todo.title }}</el-checkbox>
            <div>
              <i class="el-icon-notebook-2" @click="showTodo(todo)"></i>
              <i class="el-icon-delete" @click="deleteTodo(todo._id)"></i>
            </div>
          </div>
          <el-divider />
        </template>
      </div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: '',
  data() {
    return {
      addData: {
        title: ''
      },
      todos: []
    }
  },
  methods: {
    async addTodo() {
      const backdata = await this.$cloudbase.callFunction({
        name: 'addtodo',
        data: this.addData
      })
      console.log(backdata)
      this.getTodos()
    },
    async getTodos() {
      const backdata = await this.$cloudbase.callFunction({
        name: 'gettodo',
      })
      this.todos = backdata.result.data
    },
  },
  mounted() {
    this.getTodos()
  },
}
</script>

<style>...</style>

删除任务

// todos\cloudfunctions\deltodo\index.js
const db = require("./db")

async function delTodo(event) {
  if (!event.id) {
    return '缺少 id'
  }
  const backdb = await db.collection('todo').doc(event.id).remove()
  return backdb
}

exports.main = async (event, context) => {
  return delTodo(event)
}

<!-- todos\src\components\Index.vue -->
<template>...</template>

<script>
export default {
  ...
  methods: {
    ...
    async deleteTodo(id) {
      const backdata = await this.$cloudbase.callFunction({
        name: 'deltodo',
        data: {
          id
        }
      })
      console.log(backdata)
      this.getTodos()
    }
  },
  mounted() {
    this.getTodos()
  },
}
</script>

<style>...</style>

但是执行删除时候报错了:Error: {"code":"OPERATION_FAIL","msg":"[PERMISSION_DENIED] Permission denied"}

这是因为没有给插件授权,虽然可以查询和新增数据,但是有些服务还是需要登录鉴权才能使用。

登录鉴权

之前云函数中通过 @cloudbase/node-sdk 访问数据库,在初始化 SKD 的时候配置了密钥,可以使用管理员身份访问数据库,这也是权限管理的一种。

而客户端中要想调用云函数,也是需要鉴权的,详细参考:登录鉴权

登录的方式有很多种,因为本例还没有使用云环境提供的【用户管理】,也就是鉴权的服务,所以打开【登录授权】页面,如果允许任意用户都可以访问云函数,可以开启【匿名登录】,使用方式参考:登录方式-匿名登录

在这里插入图片描述

修改代码:

// todos\src\components\Index.vue
mounted() {
  // 登录授权
  const auth = this.$cloudbase.auth()
  auth.anonymousAuthProvider().signIn()

  // 获取任务列表
  this.getTodos()
},

完成任务

// todos\src\components\Index.vue
async todoDone(todo) {
  const backdata = await this.$cloudbase.callFunction({
    name: 'puttodo',
    data: {
      todo
    }
  })
  console.log(backdata)
}
// todos\cloudfunctions\puttodo\index.js
const db = require("./db")

async function putTodo(event) {
  if (event.todo._id === undefined) {
    return '缺少 _id'
  }
  if (event.todo.title === undefined) {
    return '缺少 title'
  }
  if (event.todo.done === undefined) {
    return '缺少 done'
  }

  const {_id, ...todo} = event.todo

  const backdata = await db.collection('todo').doc(_id).update(todo)

  return backdata
}

exports.main = async (event, context) => {
  return putTodo(event)
}

文件上传及云存储

现在要在给任务添加一个附件功能,允许上传图片到云平台。这里就需要用到云平台的云存储功能。

为了防止在使用过程中出现 CORS 报错,需要到安全配置 版块添加安全域名。

在这里插入图片描述

直接使用上传文件示例

<!-- todos\src\components\Index.vue -->
<template>
  <div>
    <el-card shadow="always" class="box-card">...</el-card>

    <el-card class="box-card">...</el-card>

    <el-drawer title="任务详情" :visible.sync="drawer">
      <div class="dra">
        <p>任务:{{ drawerTodo.title }}</p>
        <p>
          附件:
          <br/>
          <img :src="drawerTodo.tempUrl" alt="" />
        </p>
        <p>
          <input type="file" ref="File" />
          <el-button @click="upFile(drawerTodo)">点击上传</el-button>
        </p>
      </div>
    </el-drawer>
  </div>
</template>

<script>
export default {
  name: '',
  data() {
    return {
      addData: {
        title: ''
      },
      todos: [],
      drawer: false,
      drawerTodo: {}
    }
  },
  methods: {
    ...
    async showTodo(todo) {
      this.drawerTodo = todo

      if (todo.fileId) {
        // 获取图片临时链接
        const backdata = await this.$cloudbase.getTempFileURL({
          fileList: [todo.fileId]
        })
        console.log(backdata)
        this.drawerTodo.tempUrl = backdata.fileList[0].tempFileURL
      }

      this.drawer = true
    },
    async upFile() {
      const file = this.$refs.File.files[0]


      const backdata = await this.$cloudbase.uploadFile({
        // 云存储的路径
        cloudPath: 'todo/' + Date.now() + file.name,
        // 需要上传的文件,File 类型
        filePath: file
      })
      console.log(backdata)

      // 将图片的文件id与todo进行绑定
      this.drawerTodo.fileId = backdata.fileID
      const changedata = await this.$cloudbase.callFunction({
        name: 'puttodo',
        data: {
          todo: this.drawerTodo
        }
      })
      console.log(changedata)
    }
  },
  mounted() {
    // 登录授权
    const auth = this.$cloudbase.auth()
    auth.anonymousAuthProvider().signIn()

    // 获取任务列表
    this.getTodos()
  },
}
</script>

<style>...</style>

注意:因为临时链接每隔几分钟就会失效,所以要在查看的时候临时获取。

用户管理及登录授权服务

使用文件上传服务也需要登录授权,之前使用的是匿名登录的方式,下面通过示例了解腾讯提供的用户管理和登录授权服务。

基础架构代码

完成基础的代码和功能搭建:引入了路由、完成注册登录页面和对应的表单。

# 注意本例安装的是 Vue2 的路由模块
npm i vue-router@3

路由配置文件:

// todos\src\router\index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Index',
    component: () => import('../components/Index.vue')
  },
  {
    path: '/register',
    name: 'Register',
    component: () => import('../components/Register.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../components/Login.vue')
  },
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

导入注册路由配置:

// todos\src\main.js
import Vue from "vue";
import App from "./App.vue";
import Cloudbase from "@cloudbase/vue-provider";

import router from './router'

...

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");

配置导航:

<!-- todos\src\App.vue -->
<template>
  <div id="app">
    <div id="routers">
      <router-link to="/">首页</router-link>
      <router-link to="/login">登录</router-link>
      <router-link to="/register">注册</router-link>
    </div>
    <router-view />
  </div>
</template>

<script>

export default {
  name: "App",
};
</script>

<style scoped>
#routers {
  margin: 20px auto;
  width: 580px;
  text-align: center;
}
</style>

登录组件

<!-- todos\src\components\Login.vue -->
<template>
  <div class="box-card">
    <el-row>
      <el-col>
        <el-card shadow="always">
          <el-form label-position="left" label-width="80px">
            <el-form-item label="手机号">
              <el-input />
            </el-form-item>
            <el-form-item label="密码">
              <el-input />
            </el-form-item>
            <el-form-item>
              <el-button type="primary">登录</el-button>
            </el-form-item>
          </el-form>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: 'Login',
  data() {
    return {

    }
  }

}
</script>

<style scoped>
.box-card {
  margin: 0 auto;
  width: 580px;
  margin-bottom: 20px;
}
</style>

注册组件

<!-- todos\src\components\Register.vue -->
<template>
  <div class="box-card">
    <el-row>
      <el-col>
        <el-card shadow="always">
          <el-form label-position="left" label-width="80px">
            <el-form-item label="手机号">
              <el-input>
                <template #append>
                  <el-button size="mini">发送验证码</el-button>
                </template>
              </el-input>
            </el-form-item>
            <el-form-item label="验证码">
              <el-input />
            </el-form-item>
            <el-form-item label="密码">
              <el-input />
            </el-form-item>
            <el-form-item>
              <el-button type="primary">注册</el-button>
            </el-form-item>
          </el-form>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: 'Register',
  data() {
    return {

    }
  }

}
</script>

<style scoped>
.box-card {
  margin: 0 auto;
  width: 580px;
  margin-bottom: 20px;
}
</style>

短信验证码注册

参考:短信验证码登录 - 开发指南

注意:腾讯云只会免费赠送首月100条短信验证码,超出部分就要单独购买。

要使用腾讯云提供的短信验证码的登录功能,需要在【登录授权】页面开启功能。

在这里插入图片描述

展开可以看到示例代码,从代码中可以看到它使用的是 cloudbase-js-sdk(模块名 @cloudbase/js-sdk),可以去对应文档中查看详细使用:API 参考 - JavaScript(v1) - 登录认证,主要用到两个 API:

实现功能:

由于当前使用的 Vue 插件中依赖安装了 @cloudbase/js-sdk,所以不需要单独安装,如果是未安装过的项目,则需要手动安装依赖:

npm i @cloudbase/js-sdk

封装 SDK:

// todos\src\assets\auth.js
import cloudbase from '@cloudbase/js-sdk'

const auths = {}

auths.install = function(vue) {
  const app = cloudbase.init({
    env: 'mycloudes-xxx',
    region: "ap-guangzhou"
  })

  const myauth = app.auth()

  vue.prototype.$auths = myauth
}

export default auths

注册:

// todos\src\main.js
import Vue from "vue";
import App from "./App.vue";
import Cloudbase from "@cloudbase/vue-provider";

import router from './router'

import auths from './assets/auth'
Vue.use(auths)

...

页面功能:

<!-- todos\src\components\Register.vue -->
<template>
  <div class="box-card">
    <el-row>
      <el-col>
        <el-card shadow="always">
          <el-form label-position="left" label-width="80px">
            <el-form-item label="手机号">
              <el-input v-model="user.phone">
                <template #append>
                  <el-button size="mini" @click="sendCode">{{ sendMsg }}</el-button>
                </template>
              </el-input>
            </el-form-item>
            <el-form-item label="验证码">
              <el-input v-model="user.code" />
            </el-form-item>
            <el-form-item label="密码">
              <el-input v-model="user.pwd" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="register">注册</el-button>
            </el-form-item>
          </el-form>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: 'Register',
  data() {
    return {
      sendMsg: '发送验证码',
      user: {
        phone: '',
        code: '',
        pwd: ''
      },
    }
  },
  methods: {
    async sendCode() {
      const res = await this.$auths.sendPhoneCode(this.user.phone)

      if (res) {
        this.sendMsg = '有效期5分钟'
      }
    },
    async register() {
      const res = await this.$auths.signUpWithPhoneCode(this.user.phone, this.user.code, this.user.pwd)
      if (res) {
        console.log('注册成功')
        this.$router.push({
          path: '/login'
        })
      }
    }
  }

}
</script>

<style scoped>
.box-card {
  margin: 0 auto;
  width: 580px;
  margin-bottom: 20px;
}
</style>

注意:注册用户的密码长度不小于 8 位,不大于 32 位,需要包含字母和数字。

注册成功后可以在控制台查看:

在这里插入图片描述

登录和验证

登录和验证需要用到的 API:

由于短信验证码免费数量有限,所以登录采用手机号+密码方式。

<!-- todos\src\components\Login.vue -->
<template>
  <div class="box-card">
    <el-row>
      <el-col>
        <el-card shadow="always">
          <el-form label-position="left" label-width="80px">
            <el-form-item label="手机号">
              <el-input v-model="user.phoneNumber" />
            </el-form-item>
            <el-form-item label="密码">
              <el-input v-model="user.password" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="signin">登录</el-button>
            </el-form-item>
          </el-form>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: 'Login',
  data() {
    return {
      user: {
        phoneNumber: '',
        password: ''
      }
    }
  },
  methods: {
    async signin() {
      const res = await this.$auths.signInWithPhoneCodeOrPassword(this.user)
      if (res) {
        console.log('登录成功')
        this.$router.push({
          path: '/'
        })
      }
    }
  }

}
</script>


<style scoped>
.box-card {
  margin: 0 auto;
  width: 580px;
  margin-bottom: 20px;
}
</style>

获取登录状态:


保存登录状态

初始化文档中介绍登录状态的持久化保留方式(persistence)有三种:

  • local:在显式退出登录之前的 30 天内保留身份验证状态
  • session:在窗口关闭时清除身份验证状态
  • none:在页面重新加载时清除身份验证状态

首先看看 local 方式:

// todos\src\assets\auth.js
import cloudbase from '@cloudbase/js-sdk'

const auths = {}

auths.install = function(vue) {
  const app = cloudbase.init({
    env: 'mycloudes-xxx',
    region: "ap-guangzhou"
  })

  const myauth = app.auth({
    persistence: 'local'
  })

  vue.prototype.$auths = myauth
}

export default auths

local 方式是将登录状态存储在 localStorage 中:

在这里插入图片描述

注意:文档介绍的默认为 session 方式,而本例实际上默认使用的 local 方式,所以使用时建议指定具体的值。

在看 session 方式:

const myauth = app.auth({
  persistence: 'session'
})

session 方式是将登录状态存储在 session 中:

在这里插入图片描述

none 方式并没有在本地存储登录状态。

修改云存储权限

当使用了用户管理后,上传云存储的文件就被受限了,默认【所有用户可读,仅创建者及管理员可写】,上传文件时会报错:Have no access right to the storage

可以通过【自定义安全规则】配置读写的权限,例如本例开放了无条件的读写权限:

在这里插入图片描述

部署上线绑定自定义域名

# 全量部署
tcb

现在部署后使用的是默认域名访问的,实际场景应该部署自己的域名。

域名请自行购买和备案

在【访问服务】页面添加自定义域名:

在这里插入图片描述
在这里插入图片描述

腾讯云目前仅支持 HTTPS 的域名,可以直接在腾讯云申请 SSL 证书,点击【点此前往】,申请免费证书,有效期1年:

在这里插入图片描述
在这里插入图片描述

注意:免费的个人证书只能申请二级域名,例如 abc.com 默认申请的 www.abc.com,或者手动指定 ***.abc.com

在这里插入图片描述

选择推荐的 DNS 验证:

在这里插入图片描述

到域名服务商平台添加解析记录,本例使用的域名是在阿里云购买的:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

回到跟腾讯云【查看域名验证状态】,一般需要等待10分钟左右,如果之前申请过,则会很快。

验证成功后,不需要【下载证书】和【一键部署】,只需要回到添加自定义域名那里输入域名地址,即可自动获取 HTTPS 证书:

在这里插入图片描述

点击【确定】后开始绑定,需要等待一段时间。绑定成功后,仍不能直接使用这个域名去访问,还需要将这个域名的 CNAME 指向【默认域名】才行。

在这里插入图片描述

回到域名购买平台的解析设置添加记录:

在这里插入图片描述

等待自定义域名绑定成功就可以通过 <自定义域名>/vue 访问托管的静态站点了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值