作为一个系列的开端,必定是要给自己挖坑的。
终端作为所有用户的真正使用设备,终端开发者也是离用户最近的开发人员,它肩负着将后方提供的一个又一个独立服务整合为体验良好的产品的使命。面对不同的场景,所挑选的后方服务不同,实现方法也不同。
而随着云上产品越来越多,SaaS,BaaS,FaaS的完善,终端开发人员的选择越来越多,这个系列之所以加一个「云」字,是希望在这里以一个终端开发人员的视角,对比在开发目前市面上常见功能的时候,使用传统方案和云上开发(cloudbase)方案的不同之处。
另还准备开另外一个坑,是想要对目前有要趋势的一些SaaS,做SDK接入整合与异常问题分析(目前暂定实时音视频与IM,以后可能有AI之类的,也许吧,又是一个大坑),姑且叫它「云终端系列吧」。
PS. 鄙人技术栈为 JavaScript 系,出身是一个前端,现在也许能称自己是个伪全栈...(萌新瑟瑟发抖)。
以下的介绍均为 Javascript 语言
当然这个坑的第一章要挖的简单一点...
实现一个短信验证码,我们最基本需要以下几个部分
(1)终端登录表单
(2)请求后端服务器
(3)后端服务器请求短信验证码发送短信,并将手机号与验证码的映射关系存于数据库中,并增加一条过期时间字段
(4)前端接受短信,提交完整表单
(5)后端判断是否符合映射关系,判断是否登录成功
听起来好像很简单,但是要从0开发,那就问题多多了...
首先你需要一台自己的购买自己的服务器,当然要是放在20年前,你大概得去买一台实体服务器,这就很「传统」,不过为了不为难大家,还是让大家直接从IaaS开始,买一台最简单的云服务器好了。
emmmm看起来还不够,要买台数据库来满足逻辑(3),或者自己在服务器上下载一个数据库
噢我的上帝,如果是公网服务器还访问不了数据库,咱们还需要购买一个vpc搞一个私有子网才能访问云上数据库
当然实际上这个业务场景搞个redis应该是最符合场景的
购买云数据库 Redis 实例,具体操作请参见 购买redis数据库。
记得买到同一个地域下面
参数 | 取值样例 |
---|---|
计费模式 | 按量计费 |
地域 | 与服务器同地域 |
数据库版本 | Redis 4.0 |
架构 | 标准架构 |
网络 | Demo VPC,Demo 子网 |
实例名 | 立即命名:Demo 数据库 |
购买数量 | 1 |
(以上来自腾讯云短信服务的需求,其他友商的服务所需都大同小异,因为短信这个东西比较严格)
短信签名、短信正文模板提交后,我们会在2个小时左右完成审核,您可以 配置告警联系人 并设置接收模板和签名审核通知,便于及时接收审核通知。
现在,搞前端的同学一定会有萌生如下想法:
不过我们有一个好消息,我们终于要开始..........写服务端业务代码啦!!!
以Node为例,以下是长长长长长的代码部分express实现,以下代码的部分仅供参考!,(而且可能写的时候写了啥bug不自知,阿巴阿巴阿巴)
const fs = require('fs');
const redis = require('ioredis');
const tencentcloud = require('tencentcloud-sdk-nodejs');
const queryParse = require('querystring')
const expireTime = 5 * 60;//验证码有效期5分钟
const express = require('express')
const app = express();
const http = require('http')
const https = require('https');
//跨域处理
app.all("*",function (req,res,next) {
res.header("Access-Control-Allow-Origin","*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1');
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
//路由
app.get('/sms', function(req,res,next){
let queryString = event.query // get形式
next(queryString);
})
app.post('/sms',function(req,res,next){
let queryString = queryParse.parse(req.body)
next(queryString);
}
app.use(function (queryString,req, res) {
if(!queryString || !queryString.method || !queryString.phone) {
return {
codeStr: 'InValidParam',
msg: "缺少参数"
}
}
const redisStore = new redis({
port: 6369, // Redis instance port, redis实例端口
host: process.env.REDIS_HOST, // Redis instance host, redis实例host
family: 4,
password: process.env.REDIS_PASSWORD, // Redis instance password, redis实例密码
db: 0
});
if(queryString.method === "getSms") {
return await getSms(queryString, redisStore)
} else if(queryString.method === "login") {
return await loginSms(queryString, redisStore)
}
})
//业务逻辑
/*
* 功能:登录,校验验证码
*/
async function loginSms(queryString, redisStore) {
if(!queryString.code) {
return {
codeStr: 'MissingCode',
errorMessage: "缺少验证码参数"
}
}
const redisResult = await redisPromise(redisStore, queryString)
if(!redisResult) {//没有找到记录
return {
codeStr: 'CodeHasExpired',
msg: "验证码已过期"
}
}
let result = JSON.parse(redisResult)
if(!result || result.used || result.num >= 3) {
return {
codeStr: 'CodeHasValid',
msg: "验证码已失效"
}
}
if(result.code == queryString.code) { //验证码校验正确
updateRedis(redisStore, queryString.phone, result, true) //将验证码更新为已使用
// 验证码校验通过,执行登录逻辑
console.log('校验验证码成功')
return {
codeStr: 'Success',
msg: '校验验证码成功'
}
} else { // 验证码校验失败
updateRedis(redisStore, queryString.phone, result, false)
return {
codeStr: 'CodeIsError',
msg: "请检查手机号和验证码是否正确"
}
}
}
// 更新redis状态
function updateRedis(redisStore, phone, result, used) {
const sessionCode = {
code: result.code,
sessionId: result.sessionId,
num: ++result.num, //验证次数,最多可验证3次
used: used //true-已使用,false-未使用
}
redisStore.set('sms_' + phone, JSON.stringify(sessionCode));
if(used) {
redisStore.expire('sms_' + phone, 0);
} else {
redisStore.expire('sms_' + phone, expireTime);
}
}
/*
* 功能:根据手机号获取短信验证码
*/
async function getSms(queryString, redisStore) {
const code = Math.random().toString().slice(-6);//生成6位数随机验证码
const sessionCode = {
code: code,
num: 0, //验证次数,最多可验证3次
used: false //false-未使用,true-已使用
}
redisStore.set('sms_' + queryString.phone, JSON.stringify(sessionCode));
redisStore.expire('sms_' + queryString.phone, expireTime);
let queryResult = await sendSms(queryString.phone, code)
return queryResult
}
/*
* 功能:通过sdk调用短信api发送短信
* 参数 手机号、短信验证码
*/
async function sendSms(phone, code) {
const SmsClient = tencentcloud.sms.v20190711.Client;
const Credential = tencentcloud.common.Credential;
const ClientProfile = tencentcloud.common.ClientProfile;
const HttpProfile = tencentcloud.common.HttpProfile;
const secretId = TENCENTCLOUD_SECRETID;
const secretKey = ENCENTCLOUD_SECRETKEY;
const token = ENCENTCLOUD_SESSIONTOKEN;
//改为自己的代码
let cred = new Credential(secretId, secretKey, token);
let httpProfile = new HttpProfile();
httpProfile.endpoint = "sms.tencentcloudapi.com";
let clientProfile = new ClientProfile();
clientProfile.httpProfile = httpProfile;
let client = new SmsClient(cred, "ap-guangzhou", clientProfile);
let req = {
PhoneNumberSet: ["+" + phone], //大陆手机号861856624****
TemplateID: process.env.SMS_TEMPLATE_ID, //腾讯云短信模板id
Sign: process.env.SMS_SIGN, //腾讯云短信签名
TemplateParamSet: [code],
SmsSdkAppid: process.env.SMS_SDKAPPID //短信应用id
}
let queryResult = await smsPromise(client, req)
return queryResult
}
async function smsPromise(client, req) {
return new Promise((resolve, reject) => {
client.SendSms(req, function(errMsg, response) {
if (errMsg) {
reject(errMsg)
} else {
if(response.SendStatusSet && response.SendStatusSet[0] && response.SendStatusSet[0].Code === "Ok") {
resolve({
codeStr: response.SendStatusSet[0].Code,
msg: response.SendStatusSet[0].Message
})
} else {
resolve({
codeStr: response.SendStatusSet[0].Code,
msg: response.SendStatusSet[0].Message
})
}
}
});
})
}
async function redisPromise(redisStore, queryString) {
return new Promise((res, rej) => {
redisStore.get('sms_' + queryString.phone, function (err, result) {
if (err) {
rej(err)
}
res(result)
});
})
}
//服务启动
let httpServer = http.createServer(app);
let httpsServer = https.createServer({
key: fs.readFileSync('./cert/privatekey.pem', 'utf8'),
cert: fs.readFileSync('./cert/certificate.crt', 'utf8')
}, app);
let PORT = 80;
let SSLPORT = 443;
httpServer.listen(PORT, function() {
console.log('HTTP Server is running on: http://localhost:%s', PORT);
});
httpsServer.listen(SSLPORT, function() {
console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
});
太快乐了,一个前端到现在还在看着后端写代码呢,wow~
事实上,在云端这么发达的今天,加上V8引擎和Node.js的快速发展,这些功能从组织架构上确实不一定由前端做,但是一个前端可以也应该去学会这些与服务器,数据库交互的写法,只会构建UI界面和交互的前端终究在时代里会被慢慢淘汰,而未来的前端应叫做「大前端」或者「终端」,请各位同学耗子尾汁~
好的那么终于到前端的代码了,这里就写个vue的组件吧,如果有需要大家自己改成自己需要的哈,样式就用ElementUI,请求用axios
//vue<script src="//unpkg.com/vue/dist/vue.js"></script>
<template>
<div id="app">
<el-form :model="form" class="demo-form-inline">
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="手机号"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getCode" :disable="this.buttonTime>0?'disabled':'false'’">
获取验证码{{this.buttonTime <= 0?"":this.buttonTime}}
</el-button>
</el-form-item>
<el-form-item label="验证码">
<el-input v-model="form.code" placeholder="验证码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlelogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import axios from 'axios';
import elementUI from 'element-ui'
export default {
data() {
return {
form: {
phone: '',
code: '',
buttonTime:0 //按钮是否可以按
}
}
},
methods: {
handleLogin() {
const host = ....;
//get
axios.get(`/${host}/sms?phone=${this.phone}&code=${this.code}`)
.then(function (response) {
console.log(`登陆成功,回调为:${response`);
})
.catch(function (error) {
console.log(`登陆失败,失败信息为:${error}`)
});
//post
axios.post(`/${host}/sms`, {
phone: this.phone,
code: this.code
})
.then(function (response) {
console.log(`登陆成功,回调为:${response`);
})
.catch(function (error) {
console.log(`登陆失败,失败信息为:${error}`)
});
},
getCode(){
const host = ....;
setButtonDisabled();
axios.get(`/${host}?phone=${this.phone}`)
.then(function (response) {
console.log(`登陆成功,回调为:${response`);
})
.catch(function (error) {
console.log(`登陆失败,失败信息为:${error}`)
});
//post
axios.post('/${host}', {
phone: this.phone,
})
.then(function (response) {
console.log(`发送成功,回调为:${response}`);
})
.catch(function (error) {
console.log(`发送失败,失败信息为:${error}`)
});
},
setButtonDisable(){
this.buttonTime = 60;
let timer = setInterval(()=>{
if(this.buttonTime == 0){
clearInterval(timer)
return;
}
this.buttonTime--;
},1000)
}
}
}
</script>
经过一番辛苦的折腾,咱们终于能把两端的代码写完了。
但是呢,写完 ≠ 跑通,虽然我们在本地启动node服务后可以在localhost层面上进行测试,但是要部署还有很多步骤
(1)首先我们使用Putty或者FileZilla这样的产品,将服务和编译后的前端静态文件部署到服务器上
(2)在云服务器内node启动服务,若想永久启动,可以npm下载pm2或forever
(3)之后访问静态文件的主页,就可以正常访问了
(4)如果你需要域名,或者需要ssl证书的话,又要购买其他产品并走相应的流程...
看到这里你是不是觉得很麻烦,就算我们简洁一点,把后端服务换成FaaS,去用云函数替代,这个部分也就是后端业务部署的部分简单了一些,这里对redis等配置,处理都还没有列出讲解(因为这毕竟是开发的文章,并不想花重大笔墨去阐述如何配置数据库,Nginx之类的,这已经有很多成熟的文章介绍了)。
所以对于一个开发人员而言,尤其是终端开发人员,编写与用户直接相关的代码(前端交互,接口逻辑)才是关键,但是事实上,如果我们真要用传统的方式来一遍流程,大量的时间开销会放在数据库、服务器、备案、证书等非业务逻辑上的东西,这并不是我们期待看到的。
所以为了解决这种难点,体现我们真正意义上的「云开发」,我们推荐cloudbase。也就是整体上云,采用云原生架构开发
这个的产品的具体内容可以看产品文档,这里只教怎么用
(0)配置腾讯云短信服务,这个都是要做的
(1)构建前端代码
const cloudbase = require("@cloudbase/js-sdk");
const extSms = require("@cloudbase/extension-sms");
const app = cloudbase.init({
env: "您的环境ID"
});
cloudbase.registerExtension(extSms);
demo();
async function demo() {
try {
let phone = ""; // 输入用户手机号
// 发送短信验证码
await cloudbase.invokeExtension(extSms.name, {
action: "Send",
app,
phone
});
let smsCode = ""; // 用户填写验证码
// 验证码校验
await cloudbase.invokeExtension(extSms.name, {
action: "Verify",
app,
phone,
smsCode
});
// 验证码登录
await cloudbase.invokeExtension(extSms.name, {
action: "Login",
app,
phone,
smsCode
});
console.log("登录成功,目前是正式用户");
} catch (err) {
console.log(JSON.stringify(err, null, 4));
}
}
这里我们重点关注 invokeExtension
这个API,这个API可以直接调用短信服务,你会惊讶的发现,好像我的前端就可以直接调用服务了一样,以前需要经过Node层转发。
是的这就是大前端时代下的一个体现,功能的接口调用直接被封装在前端SDK中,供开发者直接调用,那么这个调用的功能在哪呢?不会我们又要购买什么服务器数据库才能调吧?
No No No!我们只需要轻轻的在这里点一下安装就好了
然后?
然后就可以调用了宝贝儿!什么node,什么服务器启动都见鬼去吧!
你问我那部署咋办,我没买服务器我FileZilla传哪里呢?
。。。。。
同学你这个问题问的非常好
我们确实没有办法部署到服务器上
因为
我们只需要在这里点一下上传文件夹,把打包好的静态文件上传并在配置页面配置一下索引文档就好了呀~
cloudbase也提供了一个默认域名供给访问,如果你有自己的域名的话还可以配置上安全域名
云开发的核心是将所有的精力都放在开发者关心的功能与业务代码上
如果您看到了这里,麻烦点个赞吧,这对我真的很重要~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。