serverless实战,基于uniCloud从零开始实现一个前端日志监控系统

896 阅读12分钟

serverless实战,基于uniCloud从零开始实现一个前端日志监控系统

写在前面

         serverless是近几年比较热门的一个概念,也是大前端发展的一个重要方向。serverless的兴起已经有一段时间了,早在几年前微信就推出了微信小程序云开发功能,其无需搭建服务器,只需使用平台提供的各项能力,即可快速开发业务。同时提供云数据库、云存储、云函数等功能,大大降低了开发者的开发成本,深受开发者喜爱。就在去年uni-app也推出了自家的serverless服务——uniCloud。

uniCloud是DCloud在阿里云和腾讯云的serverless服务上封装而成的。 它包含IaaS层(由阿里云和腾讯云提供硬件和网络)和PaaS层(由DCloud提供开发环境)。————————— uniClound官网

         uniClound相对于其它云开发产品我认为有以下几点优势:

  1. uniCloud 开发可搭配自家的HbuilderX编辑器发挥出1+1大于2的效果;
  2. 可无缝衔接uni-app和uni-ui实现产品、UI、服务三者的有机同一。
  3. 提供云函数URL化功能,非uni-app开发的系统也能轻松接入,产品的通用性和普适性更强大。

云函数默认是只有自己的app在前端通过uniCloud.callFunction来调用的,不会暴露到外网。一旦URL化后,开发者需要关注业务和资源安全。


云函数URL化 是 uniCloud 为开发者提供的 HTTP 访问服务,让开发者可以通过 HTTP URL 方式访问到云函数。

场景1:比如App端微信支付,需要配服务器回调地址,此时需要一个HTTP URL。 场景2:非uni-app开发的系统,想要连接uniCloud,读取数据,也需要通过HTTP URL方式访问。

下面本文就将基于uniClound从零开始搭建一个前端日志监控系统。

日志监控系统介绍

         本文的主要目的是介绍serverless和上手uniClound实战,重点在于日志的采集和展示。为了简化系统日志数据主要来源于两方面:一是Vue的全局错误捕获,二是来自请求响应拦截器拦截的后端API请求错误。这个系统的简单示意如下:

在这里插入图片描述

Vue全局错误捕获的简单实现

          根据vue官方文档介绍,Vue全局错误的捕获只需要配置Vue.config.errorHandler即可,这里为了使我们的日志监控系统更加完善和通用除了Vue的错误信息我们还需要采集错误发生的时间time(uniCloud有个时区差,时间建议使用时间戳表示),错误发生的项目名project。Vue全局错误捕获方法实现如下,其中addVueLog即是我们要通过云函数实现的API接口,关于接口的实现放在后面介绍。

// my-vatchvueerror.js

/****************************************************
 * @description 捕获Vue全局错误
 * @param {*} err 异常错误
 * @param {*} vm  页面示例
 * @param {*} info 错误说明
 * @return {*}
 * @author mingyong.g
 ****************************************************/
export default function(err, vm, info) {
	const route = (vm.$page && vm.$page.route) || (vm.$mp && vm.$mp.page.route); //  获取uni-app项目的页面路由
	let log = {                    // 日志对象
		err: err.toString(),
		info,
		route,
		time: new Date().getTime(),
		project:"test"
	};
	addVueLog(log);    // 新增日志的接口
}

main.js中配置错误捕获函数

// main.js
import catchVueError from "../my-vatchvueerror";
Vue.config.errorHandler = catchVueError;

响应拦截器错误日志采集

         这里以axios的响应拦截器为例进行说明。关于API错误日志,我们需要关心如下几个信息:

  1. 请求体即请求的参数
  2. 响应体即包含错误描述的响应数据
  3. 日志发生的时间,uniCloud有个时区差,时间建议使用时间戳表示
  4. 错误日志所在的项目

         下面的代码是axios响应拦截器的简单实现,其中addApiLog即是我们要通过云函数实现的API接口,关于接口的实现放在后面介绍。这里为了方面直接将包含请求参数的response.config和包含响应数据的response.data作为参数传入,其它公共信息放在接口内部实现。

// 响应拦截
service.interceptors.response.use(
	(response) => {
		let data = response.data;
		/* 
		 *  此处如果后台响应体中字段Msg = "ok" 则认为接口响应有效,否则视为错误响应
		 *  注意:这部分逻辑需根据业务和后端接口规范适当调整
		 */
		if (data.Msg == "ok" ) {
			return data;
		} else {
			addApiLog(response.config, data);   // 日志采集接口
			return Promise.reject(data);
		}
	},
	(err) => {
		let errMsg = "";
		if (err && err.response.status) {
			switch (err.response.status) {
				case 401:
					errMsg = "登录状态失效,请重新登录";
					router.push("/login");
					break;
				case 403:
					errMsg = "拒绝访问";
					break;

				case 408:
					errMsg = "请求超时";
					break;

				case 500:
					errMsg = "服务器内部错误";
					break;

				case 501:
					errMsg = "服务未实现";
					break;

				case 502:
					errMsg = "网关错误";
					break;

				case 503:
					errMsg = "服务不可用";
					break;

				case 504:
					errMsg = "网关超时";
					break;

				case 505:
					errMsg = "HTTP版本不受支持";
					break;

				default:
					errMsg = err;
					break;
			}
		} else {
			errMsg = err;
		}
		addApiLog(err.config, { statusCode: err.response.status, Msg: err.response.data });  // 日志采集接口
		return Promise.reject(errMsg);
	}
);

uniCloud admin

         为了简化开发工作uniCloud提供了基于uni-app 、 uni-ui 和uniCloud 的应用后台管理框架。

uniCloud admin 功能介绍

  • 它基于 uni-app 的宽屏适配,可自动适配 PC 宽屏和手机各端,
  • 它基于 uniCloud,是 serverless 的云开发。
  • 它基于 uni-id,使用 uni-id 的用户账户、角色、权限系统。 ——————————uniCloud官网

uniCloud界面

创建项目

         根据官方的使用教程,首先在HBuilderX 3.0+版本新建 uni-app 项目,选择 uniCloud admin 项目模板.在这里插入图片描述 创建完成后,可以跟随云服务空间初始化向导初始化项目,创建并绑定云服务空间 在这里插入图片描述 运行

  1. 进入 admin 项目
  2. 在uniCloud/cloudfunctions/common/uni-id/config.json 文件中填写自己的 passwordSecret 字段 (用于加密密码入库的密钥) 和 tokenSecret 字段 (为生成 token 需要的密钥,测试期间跳过本条也可以)
  3. 右键 uniCloud目录 运行云服务空间初始化向导,初始化数据库和上传部署云函数(如已创建并 绑定云服务空间,则跳过此步)
  4. 点击HBuilderX工具栏的运行【Ctrl+r】 -> 运行到浏览器。如果是连接本地云函数调试环境,上一步可以不上传云函数,但数据库仍需初始化。
  5. 从启动后的登录页面的底部,进入创建管理员页面(仅允许注册一次管理员账号)

创建云数据库,表

         登录uniCloud控制台:unicloud.dcloud.net.cn/找到上述步骤3创建的云服务空间,这里我创建的服务空间是gmyl 在这里插入图片描述 点击详情进入云服务空间,可以看到uniCloud admin 默认已经为我们创建了以下云数据表: 6. opendb-admin-menus : 左侧菜单树管理表 7. opendb-verify-codes :验证码记录表 8. uni-id-log :uniCloud 的登录日志 9. uni-id-log :权限表 10. uni-id-roles : 角色配置表 11. uni-id-users :账号表

         uniCloud admin提供了一套完整的后台管理方案,我们的目的在于搭建一个简单的日志监控系统,部分功能暂时还用不到这里现在uniCloud admin 注意是考虑到应用的扩展性。言归正题,除了上述框架自带的数据表以外我们还需要自行创建一张数据表用来存放日志数据,这里我为了区分分别建了两张表分别存放Vue日志和API日志。

  • vue日志表结构
{
  "bsonType": "object",
  "required": [],
  "permission": {
    "read": false,
    "create": false,
    "update": false,
    "delete": false
  },
  "properties": {
    "_id": {
      "description": "ID,系统自动生成"
    },
    "project": {
      "bsonType": "onject",
      "description": "项目名称",
      "trim": "both"
    },
    "url": {
      "bsonType": "onject",
      "description": "页面路由信息",
      "trim": "both"
    },
    "errmsg": {
      "bsonType": "onject",
      "description": "错误描述",
      "trim": "both"
    },
    "errtype": {
      "bsonType": "string",
      "description": "错误类型",
      "trim": "both"
    },
    "occurrence_timestamp": {
      "bsonType": "timestamp",
      "description": "问题发生时间"
    },
    "state": {
      "bsonType": "int",
      "description": "0 待处理 1:已处理 ",
      "trim": "both"
    },
    "handle_timestamp": {
      "bsonType": "timestamp",
      "description": "问题修复时间"
    },
    "reason": {
      "bsonType": "string",
      "description": "问题原因",
      "trim": "both"
    },
    "solution": {
      "bsonType": "string",
      "description": "解决办法",
      "trim": "both"
    }
  }
}

创建云函数

         回到HbuilderX找到刚才创建的项目,依次展开uniCloud >> cloudfunctions , 右键cloudfunctions 点击新建云函数命名 为addVueLog 在这里插入图片描述          一个初始云函数结构如下,其中前端传递参数通过 event.body获取。接下来的主要任务就是将前端传递的日志对象存储到云数据库中。云函数操作云数据库的教程可自行查阅官方文档:uniapp.dcloud.io/uniCloud/cf…,此处不再赘述。

// 初始云函数
'use strict';
exports.main = async (event, context) => {
	//event为客户端上传的参数
	console.log('event : ', event)
	
	//返回数据给客户端
	return event
};

// 将数据写入云数据库
'use strict';
const db = uniCloud.database();
exports.main = async (event, context) => {
	//event为客户端上传的参数
	let data = event.body ? JSON.parse(event.body) : event;
	if (event.project == "" && !event.body) {   // 判断数据是否有效
		return {
			Msg: "Invalid Data!",
			Data: "",
			Count: 0
		}
	} else {
		const dbCmd = db.command
		const $ = dbCmd.aggregate
		let res = await db.collection('vuelog_db').add(data)   // 向表vuelog_db插入一条数据

		//返回数据给客户端
		return {
			Data: "",
			Msg: "ok",
			Count: 0
		}
	}

};

云函数url化

         开启云函数url化之前首先上传并部署云函数,找到对应的云函数右键上传部署。 在这里插入图片描述          上传成功可在uniCloud控制台的云函数列表中找到刚才上传的云函数。 在这里插入图片描述

  1. 登录uniCloud后台,选择需要管理的服务空间。
  2. 单击左侧菜单栏【云函数】,进入云函数页面。
  3. 点击需要配置的云函数的【详情】按钮,配置访问路径。 在这里插入图片描述 云函数url化之后便可以向一般的API接口一样调用,此处的add_vuelog即是Vue全局错误捕获方法中的addVueLog接口实现。

运行测试

          在postman等API调试工具中,测试add_vuelogAPI ,测试过程就不再演示了,云函数调用成功,云数据库中将新增一条记录。 ![在这里插入图片描述](img-blog.csdnimg.cn/20210705133…](p3-juejin.byteimg.com/tos-cn-i-k3…)           addApiLog的实现方式同上。至此一个日志系统的数据采集、存储功能就已经实现了。🤙🤙🤙

数据展示

          数据展示部分虽然重要,但却不是本次开发的重点,这里直接使用uniCloud提供的schema2code (HBuilderX 3.1.0+ 支持)功能生成数据列表页面。

  • schema2code 使用教程:uniapp.dcloud.net.cn/uniCloud/sc… 在这里插入图片描述 在这里插入图片描述           对schema2code生成的数据稍加改造,将uni-list组件使用uni-table组件替代。
	<view class="uni-container">
			<unicloud-db
				ref="udb"
				@load="onqueryload"
				collection="vuelog_db"
				:options="options"
				:where="where"
				field="project,url,errmsg,errtype,occurrence_timestamp,state,handle_timestamp,reason,solution"
				page-data="replace"
				:orderby="orderby"
				:getcount="true"
				:page-size="options.pageSize"
				:page-current="options.pageCurrent"
				v-slot:default="{ data, pagination, loading, error }"
			>
				<uni-table
					:loading="loading"
					:emptyText="error.message || '没有更多数据'"
					border
					stripe
					type="selection"
					@selection-change="selectionChange"
				>
					<uni-tr>
						<uni-th align="center">项目</uni-th>
						<uni-th align="center">页面路由</uni-th>
						<uni-th align="center">错误描述</uni-th>
						<uni-th align="center">错误类型</uni-th>
						<uni-th align="center">原因</uni-th>
						<uni-th align="center">解决方案</uni-th>
						<uni-th  align="center">发生时间</uni-th>
						<uni-th  align="center">修复时间</uni-th>
						<uni-th  align="center">状态</uni-th>
						<uni-th  align="center">操作</uni-th>
						<uni-th  align="center">搜索</uni-th>
					</uni-tr>
					<uni-tr v-for="(item, index) in data" :key="index">
						<uni-td align="center">{{ item.project }}</uni-td>
						<uni-td align="center">{{ item.url }}</uni-td>
						<uni-td align="center">{{ item.errmsg }}</uni-td>
						<uni-td align="center">{{ item.errtype }}</uni-td>
						<uni-td align="center">{{ item.reason }}</uni-td>
						<uni-td align="center">{{ item.solution }}</uni-td>
						<uni-td align="center">
							<uni-dateformat :date="item.occurrence_timestamp" :threshold="[0, 0]" />
						</uni-td>
						<uni-td align="center">
							<uni-dateformat :date="item.handle_timestamp" :threshold="[0, 0]" />
						</uni-td>
						<uni-td align="center">
							<text v-if="item.state">已修复</text>
							<text v-else>待修复</text>
						</uni-td>
						<uni-td align="center">
							<view class="uni-group">
								<button
									@click="navigateTo('../edit?id=' + item._id+'&dbname=vuelog_db', false)"
									class="uni-button"
									size="mini"
									type="primary"
								>
									处理
								</button>
								<button @click="confirmDelete(item)" class="uni-button" size="mini" type="warn">
									删除
								</button>
							</view>
						</uni-td>
						<uni-td>
							<!-- #ifdef H5 -->
							<a
								v-for="engine in engines"
								:key="engine.url"
								:href="engine.url.replace('ERR_MSG', encodeURIComponent(item.errmsg))"
								target="_blank"
								class="err-search"
							>
								{{ engine.name }}
							</a>
							<!-- #endif -->
						</uni-td>
					</uni-tr>
				</uni-table>
				<view class="uni-pagination-box">
					<uni-pagination
						show-icon
						:page-size="pagination.size"
						v-model="pagination.current"
						:total="pagination.count"
						@change="onPageChanged"
					/>
				</view>
			</unicloud-db>
		</view>

          页面写好之后别忘了在uniCloud admin 自带的菜单管理中注册路由信息,如果没有注册路由信息则页面无法在左侧菜单栏中显示。 在这里插入图片描述

  • 最终呈现的页面效果 在这里插入图片描述

再优化一下

          为了让界面更美观,结合uni-app插件市场的ReportPro 数据报表(云函数版)秋云 ucharts echarts 高性能跨全端图表组件 升级一下页面的首页,做一个数据看板。这里放一下效果图。实现逻辑参考云函数操作云数据库:uniapp.dcloud.net.cn/uniCloud/cf…

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

  • 移动端适配 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述           OK!到这里整个简单的日志监控系统就实现了。赶紧动手尝试一下吧!

一些个人感受

          之所以要动手做这样一个项目,一方面是处于对技术的研究和探索,早在19年的时候就接触过微信小程序的云开发模式,不过一直都是做一些技术层面的探索和了解并没有真正动手实践;另一方面是随着自己开发的一些项目落地难免会有错误和bug发生,以往发生错误都是靠用户反馈然后滞后处理,导致用户体验非常糟糕,久而久之容易使用户对产品失去信心甚至产生质疑。由此便谋生了此项目的想法,也正好自己熟悉的uni-app 也推出了云开发模式,因此便有了本项目。

          再说说对severless的感受,serverless即无服务器,这里的无服务器是对开发而言,服务器直接由云服务器供应商提供并代为管理了。如此一来开发人员就只需要关注业务,同时前后端的差异也越来越小,以本项目为例,全程没有后端参与也没有写过一条SQL语句,这一切得益于云服务商提供的一系列开发的API接口,当然方便的同时也带来一定的局限。使用 Serverless,我们不需要再过多关注服务端的运维,不需要关心我们不熟悉的领域,我们只需要专注于业务的开发、专注于产品的实现。我们需要关心的事情变少了,但我们能做的事情更多了。Serverless模式将进一步拓展前端的边界,现在的前端开发早已不是以往的前端开发,前端不仅可以做网页,还可以做小程序,做 APP,做桌面程序,现在前端也可以做服务端了!

Atwood's Law: Any application that can be written in JavaScript, will eventually be written in >JavaScript.


任何可以用 JavaScript 来写的应用,最终都将用 JavaScript 来写。

写在最后

          一个完整的日志监控系统还应该包括消息通知模块,在我的初期架构中也是这样设想和规划的,由于消息通知借助的是第三方服务实现,是一个相对独立的功能模块,因此我将它独立出来,后续将整理为一篇单独的文章,给大家介绍一下uniCloud云函数如何调用第三方API,如何使用npm安装第三方服务。

在这里插入图片描述           最后,本文同步发布于个人G众号"前端知识营地",点击关注,获取更多优质有趣的内容。后续会将项目源码整理后放在公众号供大家参考,感兴趣的朋友可以点击下方链接点个关注!


(完)