面向边缘场景的 PWA 实践

在这里插入图片描述

背景

随着5G技术的发展,物联网边缘侧主要应用于数据传输量大、安全要求高以及数据实时处理等行业与应用场景中。其中,边缘计算是一种分布式计算模式,其将计算资源和数据处理能力推向接近数据源的边缘设备,以减少延迟并提高响应速度。

对前端领域而言,面对边缘场景下的应用开发也发生了相应的变化,其通常需要考虑边缘侧与终端侧的实现方式,并且还需考虑相较于传统 B/S 架构下的部署方案。本文旨在通过工业互联网场景下的一个实践案例,浅析面向边缘情形下的前端研发模式升级,以期能够给有边缘场景应用开发需求的读者提供一定的思路与借鉴。

架构设计

相较于传统前端研发场景,面对边缘情境下的前端研发模式,最重要的变化在于其环境的特殊性,包括:网络、存储等。在前期调研了部署环境后,为考虑用户体验,故而从架构设计上对整体系统进行了如下分层,分别是:应用层、服务层、平台层,如下图所示:

其中,应用层为了更好的体现离线与 Web 各自的优势,故而采用“Web+PWA”的形式进行呈现;案例中业务逻辑较为简单,服务层采用以Node.js为主的BFF形式的Serverless进行处理;对于平台层,本身案例应用部署环境为虚拟机环境,但考虑到多端的一致性,故而也支持容器化的部署。

技术选型

前期调研后,由于虚拟机Windows侧可能需要兼容IE 11,故而选择以Vue 2.x为主的全家桶构建,同时安装 PWA 的相关依赖。BFF侧,提供以mongoDB + Node.js的类 Serverless 服务,通过Docker容器、虚拟机及其他runtime进行调度,如下图所示:

源码分析

端侧

目录结构
- public- img- icons----------------------------------------------------- PWA所需icon物料- android-chrome-192x192.png- android-chrome-512x512.png- android-chrome-maskable-192x192.png- android-chrome-maskable-512x512.png- apple-touch-icon-60x60.png- apple-touch-icon-76x76.png- apple-touch-icon-120x120.png- apple-touch-icon-152x152.png- apple-touch-icon-180x180.png- apple-touch-icon.png- favicon-32x32.png- favicon.svg- msapplication-icon-144x144.png- mstile-150x150.png- safari-pinned-tab.svg- favicon.ico- index.html- robots.txt
- src- api- auth------------------------------------------------------- 登录接口- list------------------------------------------------------- 列表及查询接口- assets- logo.png- components- Footer.vue------------------------------------------------- 底部组件- Header.vue------------------------------------------------- 头部组件- Item.vue--------------------------------------------------- 列表组件- Layout.vue------------------------------------------------- 布局组件- router- index.js--------------------------------------------------- 路由拦截等相关逻辑- routes.js-------------------------------------------------- 路由表- store- index.js- styles- index.less- utils- http.js---------------------------------------------------- 封装http请求,axios拦截器- views- Home.vue--------------------------------------------------- 首页,用于路由表层级渲染- Login.vue-------------------------------------------------- 登录页- NotFound.vue----------------------------------------------- 路由未匹配页- App.vue-------------------------------------------------------- 根组件- main.js-------------------------------------------------------- Webpack打包的入口- registerServiceWorker.js--------------------------------------- PWA声明周期,service worker处理逻辑
- base.config.js----------------------------------------------------- 基础配置,用于脚手架读取
- default.conf------------------------------------------------------- nginx的conf配置
核心逻辑
router

构建路由表,用于处理页面的跳转,是一个树形结构,代码如下:

const routes = [{path: "/login",name: "Login",component: () => import("@/views/Login.vue"),},{path: "/",name: "/",redirect: "/home",component: () => import("@/components/Layout.vue"),children: [{path: "/home",name: "Home",component: () => import("@/views/Home.vue"),children: [{path: "/home/equipment",name: "Equipment",children: [{path: "/home/equipment/management",name: "Management",children: [{path: "/home/equipment/management/cpe",name: "CPE",},{path: "/home/equipment/management/hub",name: "Hub",},{path: "/home/equipment/management/switch",name: "Switch",},{path: "/home/equipment/management/robot",name: "Robot",},],},],},],},],},{path: "*",name: "NotFound",component: () => import("@/views/NotFound.vue"),},
];export default routes;

对于router的入口,需要处理一下登录的拦截,使用路由拦截进行处理,代码如下:

import Vue from "vue";
import VueRouter from "vue-router";Vue.use(VueRouter);import routes from "./routes";const router = new VueRouter({mode: "hash",base: process.env.BASE_URL,routes,
});router.beforeEach(async (to, from, next) => {if (to.path === "/login") {next();} else {const token = sessionStorage.getItem("token");if (!token) {next("/login");} else {next();}}
});export default router;
store

对于状态管理,需要对整体业务逻辑进行统一处理,由于比较简单,不需要用modules进行隔离,代码如下:

import Vue from "vue";
import Vuex from "vuex";
import createPersistedstate from "vuex-persistedstate";
Vue.use(Vuex);const store = new Vuex.Store({state: {mode: "",searchValue: "",count: 0,checkedList: [],},mutations: {changeMode(state, p) {state.mode = p;},changeValue(state, v) {state.searchValue = v;},changeCount(state, n) {state.count = n;},addItem(state, id) {console.log("addItem", id);if (state.checkedList.indexOf(id) == -1) {state.checkedList.push(id);}console.log("checkedList", state.checkedList);},deleteItem(state, id) {console.log("deleteItem", id);const idx = state.checkedList.indexOf(id);if (idx != -1) {state.checkedList.splice(idx, 1);}console.log("checkedList", state.checkedList);},},actions: {},modules: {},plugins: [createPersistedstate({key: "vwaver-iiot-end",}),],
});export default store;
views

对于登录页,进行一个简单的验证,代码如下:

<template><div class="login-view"><section class="login-box"><div class="login-box-header"><imgclass="login-box-logo":src="require('@/assets/logo.png')"alt="logo"/><span class="login-box-title">{{ title }}</span></div><Form class="login-box-form" :form="form"><FormItem><Inputv-decorator="['uname',{ rules: [{ required: true, message: '请输入用户名!' }] },]"placeholder="请输入用户名"><Iconslot="prefix"type="user"style="color: rgba(0, 0, 0, 0.25);"/></Input></FormItem><FormItem><Inputv-decorator="['password',{rules: [{ required: true, message: 'Please input your Password!' },],},]"type="password"placeholder="请输入密码"><Iconslot="prefix"type="lock"style="color: rgba(0, 0, 0, 0.25);"/></Input></FormItem></Form><Button class="login-box-button" type="primary" @click="handleLogin">登录</Button></section></div>
</template><script>
import { Form, Input, Button, Icon } from "ant-design-vue";import { APILogin } from "@/api/auth";const { title } = require("../../base.config");export default {name: "Login",components: {Form,FormItem: Form.Item,Input,Button,Icon,},data() {return {form: this.$form.createForm(this, { name: "login" }),title,};},methods: {handleLogin() {this.form.validateFields(async (err, values) => {if (!err) {console.log("Received values of form: ", values);const res = await APILogin(values);console.log("res", res);if (res.success) {sessionStorage.setItem(`token`, res.data.token);this.$router.push("/");}}});},},
};
</script><style lang="less" scoped>
.login-view {width: 100%;height: 100%;background: linear-gradient(135deg, #513691, #61499b);display: flex;justify-content: center;align-items: center;.login-box {border: 1px solid #ececec;background: #fcfcfc;width: 80%;border-radius: 8px;box-shadow: 0 0 10px #ccc;display: flex;flex-direction: column;padding: 2rem 0;align-items: center;&-header {display: flex;align-items: center;justify-content: center;margin-bottom: 10px;}&-logo {height: 24px;}&-title {font-weight: bold;font-size: 24px;background: linear-gradient(135deg, #513691, #61499b);background-clip: text;color: transparent;margin-left: 6px;}&-form {width: 80%;}&-button {width: 80%;background: linear-gradient(135deg, #513691, #61499b);border-color:  #61499b;}}
}
</style>

对于Home页面,需要对页面的路由进行相应的渲染,代码如下:

<template><div class="home"><section v-if="$store.state.mode != 'search'" class="home-nav"><Breadcrumb separator=">"><BreadcrumbItem v-for="item in nav" :key="item.path"><a :href="'#' + item.path">{{ item.name }}</a></BreadcrumbItem></Breadcrumb></section><section class="home-list"><Item:mode="$store.state.mode"v-for="l in list":key="l.id":title="l.title":subTitle="l.subTitle":id="l.id"@jump="handleJump":count="l.children.filter((l) => $store.state.checkedList.indexOf(l) != -1).length":children="l.children":prev="l.prev"/></section></div>
</template><script>
import { Breadcrumb } from "ant-design-vue";
import Item from "@/components/Item";
import { APIList, APINav, APISearch } from "@/api/list";
import { mapMutations } from "vuex";export default {name: "Home",components: {Breadcrumb,BreadcrumbItem: Breadcrumb.Item,Item,},data() {return {nav: [],list: [],count: 0,};},mounted() {console.log("$route", this.$route);console.log("$router", this.$router);if (this.$mode !== "search") {this.onGetList();this.onGetNav();} else {this.onSearchList();}},watch: {"$route.path": {handler(val, oldVal) {console.log("val", val);if (oldVal != val) {this.onGetList();}},},"$store.state.mode": {handler(val) {if (val == "search") {this.list = this.onSearchList();}},},"$store.state.searchValue": {handler(value) {if (value) {this.onSearchList();}},},},beforeDestroy() {},methods: {...mapMutations(["changeCount"]),handleJump(id) {console.log("id", id);this.$router.push({path: `${this.$route.path}/${id}`,});this.$router.go(0);},async onGetList() {const res = await APIList({params: {name: this.$route.name,},});console.log("APIList", res);if (res.success) {this.list = res.data.list;}},async onGetNav() {const res = await APINav({params: {name: this.$route.name,},});console.log("APINav", res);if (res.success) {this.nav = res.data.nav;}},async onSearchList() {const res = await APISearch({value: this.$store.state.searchValue,});console.log("APISearch", res);if (res.success) {this.list = res.data.list;console.log("list.length", this.list.length);this.changeCount(this.list.length);}},},
};
</script><style lang="less" scoped>
// 鼠标hover时候的颜色
/deep/ .ant-checkbox-wrapper:hover .ant-checkbox-inner,
.ant-checkbox:hover .ant-checkbox-inner,
.ant-checkbox-input:focus + .ant-checkbox-inner {border: 1px solid #61499b !important;
}
// 设置默认的颜色
/deep/ .ant-checkbox {.ant-checkbox-inner {border: 1px solid #61499b;background-color: transparent;}
}
// 设置选中的颜色
/deep/ .ant-checkbox-checked .ant-checkbox-inner,
.ant-checkbox-indeterminate .ant-checkbox-inner {background-color: #61499b;border: 1px solid #61499b;
}.home {width: 100%;height: calc(100% - 3rem);&-nav {background: #fdfdfd;padding: 0.25rem 0.5rem;}&-list {}
}
</style>
components

对于顶部搜索,实现组件Header,代码如下:

<template><div class="header"><Search v-model="value" @search="handleSearch" /></div>
</template><script>
import { Input } from "ant-design-vue";
import { APISearch } from "@/api/list";
import { mapMutations } from "vuex";
export default {name: "Header",components: {Search: Input.Search,},data() {return {value: "",};},methods: {...mapMutations(["changeMode", "changeValue"]),async handleSearch(value) {console.log("value", value);const res = await APISearch({value,});console.log("search", res);if (value) {this.changeMode("search");this.changeValue(value);} else {this.changeMode("");this.changeValue(value);this.$router.go(0);}},},
};
</script><style lang="less" scoped>
.header {height: 1rem;width: 100%;background: #fff;display: flex;justify-content: center;align-items: center;padding: 0 0.5rem;
}
</style>

对于底部显示数量,实现组件Footer,代码如下:

<template><div class="footer"><template v-if="mode == 'search'"><span class="footer-text">已搜到{{ $store.state.count }}项</span></template><span class="footer-text" v-else>已选{{ $store.state.checkedList.length }}项</span></div>
</template><script>
export default {name: "Footer",props: {mode: {type: String,},},
};
</script><style lang="less" scoped>
.footer {width: 100%;height: 2rem;background: #fff;padding: 0.25rem 0.5rem;&-text {color: #1778fe;font-weight: bold;}
}
</style>

对于列表的每项的显示,则进行一个统一的抽离,这也是本案例中最为核心的一个组件,代码如下:

<template><div class="item"><section class="item-left"><Checkbox@change="handleChange":indeterminate="indeterminate":checked="checkAll"/><div class="item-left-text"><span class="item-left-title">{{ title }}</span><span v-if="mode == 'search'" class="item-left-subtitle">{{ subTitle }}</span></div></section><sectionv-if="children.length != 0"class="item-right"@click="handleClick"><span class="item-right-count">已选 {{ checkAll ? children.length : count }}</span><Icon type="right" /></section></div>
</template><script>
import { Checkbox, Icon } from "ant-design-vue";
import { mapMutations } from "vuex";
import routes from "@/router/routes";console.log("children", routes[1].children);const createTree = (children) => {const r = [];children.forEach((child) => {const key = child.path.split("/").pop();if (child.children) {r.push({key,children: createTree(child.children),});} else {r.push({key,});}});return r;
};const tree = createTree(routes[1].children);console.log("tree", tree);export default {name: "Item",props: {mode: {type: String,},title: {type: String,default: "",},subTitle: {type: String,default: "",},count: {type: Number,default: 0,},id: {type: String,},children: {type: Array,},prev: {type: Array,},},components: {Checkbox,Icon,},data() {return {checkAll: false,indeterminate: false,};},watch: {},methods: {handleClick() {this.$emit("jump", this.id);},handleChange(e) {console.log("e", e.target.checked, this.id);if (e.target.checked) {this.checkAll = true;this.indeterminate = false;if (this.children.length != 0) {this.children.forEach((child) => {this.addItem(child);});}this.addItem(this.id);} else {this.checkAll = false;this.indeterminate = false;if (this.children.length != 0) {this.children.forEach((child) => {this.deleteItem(child);});}this.deleteItem(this.id);if (this.prev.length != 0) {this.prev.forEach((pre) => {this.deleteItem(pre);});}}},...mapMutations(["addItem", "deleteItem"]),},mounted() {console.log("this.id", this.id);if (this.$store.state.checkedList.includes(this.id)) {this.checkAll = true;} else {this.checkAll = false;this.children.forEach((child) => {if (this.$store.state.checkedList.includes(child)) {this.indeterminate = true;}});}},
};
</script><style lang="less" scoped>
.item {padding: 0.25rem 0.5rem;margin: 1px 0;background: #fff;display: flex;justify-content: space-between;align-items: center;&-left {display: flex;align-items: center;&-text {margin-left: 0.125rem;display: flex;flex-direction: column;}&-subtitle {color: #ccc;margin-top: 0.125rem;}}&-right {flex: right;&-count {margin-right: 0.125rem;}}&-right:hover {cursor: pointer;color: #1778fe;}
}
</style>

边侧

目录结构
- db- __resource__- __temp__
- edge- model.js- operator.js- read.js- sync.js- utils.js- write.js
- public- index.html
- routes- api- auth.js-------------------------------------------------------- 登录接口- list.js-------------------------------------------------------- 列表及查询接口- object.js------------------------------------------------------ 对象存储接口
- app.js------------------------------------------------------------- express应用
- cluster.js--------------------------------------------------------- 用于监听app.js
- router.js---------------------------------------------------------- 统一的路由
- minio.js----------------------------------------------------------- minio设置
- mongodb.js--------------------------------------------------------- mongodb设置
- run.sh------------------------------------------------------------- wasmedge边缘运行时
核心逻辑
app.js

BFF采用简单的express服务,实例化入口app,代码如下:

const express = require("express");
const app = express();
const bodyParser = require("body-parser");app.use(express.static("public"));app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false,})
);app.use("/auth", require("./routes/auth"));app.use("/list", require("./routes/list"));app.use('/object', require('./routes/object'));app.listen(4000, () => {console.log("server running");
});
cluster.js

基于child_process构建app的监听,代码如下:

var fork = require("child_process").fork;//保存被子进程实例数组
var workers = [];//这里的被子进程理论上可以无限多
var appsPath = ["./app.js"];var createWorker = function(appPath) {//保存fork返回的进程实例var worker = fork(appPath); //监听子进程exit事件worker.on("exit", function() {console.log("worker:" + worker.pid + "exited");delete workers[worker.pid];createWorker(appPath);});workers[worker.pid] = worker;console.log("Create worker:" + worker.pid);
};//启动所有子进程
for (var i = appsPath.length - 1; i >= 0; i--) {createWorker(appsPath[i]);
}//父进程退出时杀死所有子进程
process.on("exit", function() {for (var pid in workers) {workers[pid].kill();}
});
routes

对于鉴权部分,采用jwt进行验证,代码如下:

const router = require("../router");
const jwt = require("jsonwebtoken");const { mongoose } = require("../mongodb");const Schema = mongoose.Schema;const expireTime = 60 * 60;router.post("/login", async function (req, res) {const { uname, upwd } = req.body;const registerSchema = new Schema({uname: String,upwd: String,});const Register = mongoose.model("Register", registerSchema);const register = new Register({uname,upwd,});const token = jwt.sign({ uname, upwd }, "auth", { expiresIn: expireTime });register.save().then((result) => {console.log("成功的回调", result);res.json({code: "0",data: {token,},msg: "成功",success: true,});},(err) => {console.log("失败的回调", err);res.json({code: "-1",data: {err: err,},msg: "失败",success: false,});});
});module.exports = router;

对于列表及查询相关接口,代码如下:

const router = require("../router");
const url = require("url");const { mongoose } = require("../mongodb");const Schema = mongoose.Schema;const navMapSchema = new Schema({Home: [{ name: String, path: String }],Equipment: [{ name: String, path: String }],Management: [{ name: String, path: String }],CPE: [{ name: String, path: String }],Hub: [{ name: String, path: String }],Switch: [{ name: String, path: String }],Robot: [{ name: String, path: String }],}),columnMapSchema = new Schema({Home: [{id: String,title: String,subTitle: String,prev: [String],children: [String],},],Equipment: [{id: String,title: String,subTitle: String,prev: [String],children: [String],},],Management: [{id: String,title: String,subTitle: String,prev: [String],children: [String],},],CPE: [{id: String,title: String,subTitle: String,prev: [String],children: [String],},],Hub: [{id: String,title: String,subTitle: String,prev: [String],children: [String],},],Switch: [{id: String,title: String,subTitle: String,prev: [String],children: [String],},],Robot: [{id: String,title: String,subTitle: String,prev: [String],children: [String],},],});
const NavMap = mongoose.model("NavMap", navMapSchema),ColumnMap = mongoose.model("ColumnMap", columnMapSchema);// 简单化操作,设计时可对mongodb数据库进行更细粒度的集合处理
const navMap = new NavMap({Home: [{name: "全部",path: "/home",},],Equipment: [{name: "全部",path: "/home",},{name: "工业设备",path: "/home/equipment",},],Management: [{name: "全部",path: "/home",},{name: "工业设备",path: "/home/equipment",},{name: "设备管理",path: "/home/equipment/management",},],CPE: [{name: "全部",path: "/home",},{name: "工业设备",path: "/home/equipment",},{name: "设备管理",path: "/home/equipment/management",},{name: "CPE设备",path: "/home/equipment/management/cpe",},],Hub: [{name: "全部",path: "/home",},{name: "工业设备",path: "/home/equipment",},{name: "设备管理",path: "/home/equipment/management",},{name: "Hub设备",path: "/home/equipment/management/hub",},],Switch: [{name: "全部",path: "/home",},{name: "工业设备",path: "/home/equipment",},{name: "设备管理",path: "/home/equipment/management",},{name: "交换机设备",path: "/home/equipment/management/switch",},],Robot: [{name: "全部",path: "/home",},{name: "工业设备",path: "/home/equipment",},{name: "设备管理",path: "/home/equipment/management",},{name: "机器人设备",path: "/home/equipment/management/robot",},],
});router.get("/nav", async function (req, res) {const { name } = url.parse(req.url, true).query;console.log("/nav", name);console.log("nav", navMap[`${name}`]);navMap.save().then((result) => {console.log("成功的回调", result);res.json({code: "0",data: {nav: navMap[`${name}`],},msg: "成功",success: true,});},(err) => {console.log("失败的回调", err);res.json({code: "-1",data: {err: err,},msg: "失败",success: false,});});
});const columnMap = new ColumnMap({Home: [{id: "equipment",title: "工业设备",subTitle: "全部",prev: [],children: ["management","cpe","camera","wifi","hub","usb","ethernet","switch","two","three","four","robot","arm","leg",],},],Equipment: [{id: "management",title: "设备管理",subTitle: "全部 - 工业设备",prev: ["equipment"],children: ["cpe","camera","wifi","hub","usb","ethernet","switch","two","three","four","robot","arm","leg",],},],Management: [{id: "cpe",title: "CPE设备",subTitle: "全部 - 工业设备 - 设备管理",prev: ["equipment", "management"],children: ["camera", "wifi"],},{id: "hub",title: "Hub设备",subTitle: "全部 - 工业设备 - 设备管理",prev: ["equipment", "management"],children: ["usb", "ethernet"],},{id: "switch",title: "交换机设备",subTitle: "全部 - 工业设备 - 设备管理",prev: ["equipment", "management"],children: ["two", "three", "four"],},{id: "robot",title: "机器人设备",subTitle: "全部 - 工业设备 - 设备管理",prev: ["equipment", "management"],children: ["arm", "leg"],},],CPE: [{id: "camera",title: "摄像头",prev: ["equipment", "management", "cpe"],subTitle: "全部 - 工业设备 - 设备管理 - CPE设备",children: [],},{id: "wifi",title: "WiFi",prev: ["equipment", "management", "cpe"],subTitle: "全部 - 工业设备 - 设备管理 - CPE设备",children: [],},],Hub: [{id: "usb",title: "USB Hub",prev: ["equipment", "management", "hub"],subTitle: "全部 - 工业设备 - 设备管理 - Hub设备",children: [],},{id: "ethernet",title: "Ethernet Hub",prev: ["equipment", "management", "hub"],subTitle: "全部 - 工业设备 - 设备管理 - Hub设备",children: [],},],Switch: [{id: "two",title: "二层交换机",prev: ["equipment", "management", "switch"],subTitle: "全部 - 工业设备 - 设备管理 - 交换机设备",children: [],},{id: "three",title: "三层交换机",prev: ["equipment", "management", "switch"],subTitle: "全部 - 工业设备 - 设备管理 - 交换机设备",children: [],},{id: "four",title: "四层交换机",prev: ["equipment", "management", "switch"],subTitle: "全部 - 工业设备 - 设备管理 - 交换机设备",children: [],},],Robot: [{id: "arm",title: "机械臂",prev: ["equipment", "management", "robot"],subTitle: "全部 - 工业设备 - 设备管理 - 机器人设备",children: [],},{id: "leg",title: "腿式机器人",prev: ["equipment", "management", "robot"],subTitle: "全部 - 工业设备 - 设备管理 - 机器人设备",children: [],},],
});router.get("/columns", async function (req, res) {const { name } = url.parse(req.url, true).query;console.log("/columns", name);columnMap.save().then((result) => {console.log("成功的回调", result);res.json({code: "0",data: {list: columnMap[`${name}`],},msg: "成功",success: true,});},(err) => {console.log("失败的回调", err);res.json({code: "-1",data: {err: err,},msg: "失败",success: false,});});
});router.post("/search", async function (req, res) {const { value } = req.body;console.log("/columns", value);const names = Object.values(columnMap).flat();console.log("names", names);const list = names.filter((f) => f.title.indexOf(value) != -1);res.json({code: "0",data: {list,},msg: "成功",success: true,});
});module.exports = router;

其中,对于树形结构的构建,采用双向链表的形式进行prevchildren的派发,如下图所示:

router.js

构建统一的 express 路由,用于各routes模块的引用,代码如下:

const express = require('express');
const router = express.Router();module.exports = router;
minio.js

使用minio来对对象存储中的资源进行处理,边缘侧对网络要求较高,对于某些离线场景,需要将静态资源托管到本地,代码如下:

const Minio = require('minio');// 对于静态资源,在边缘侧可进行图片、视频等静态资源计算和缓存,与边缘侧部署存储方式有关
const minio = key => {return new Minio.Client({endPoint: 'ip',port: 9090,useSSL: false,accessKey: 'accessKey',secretKey: 'secretKey'});
}module.exports = minio;

对于同步操作,可以使用edge目录下的sync模块进行处理,代码如下:

const axios = require("axios");
const fs = require("fs");const url = "http://localhost:4000",bucketName = "bucketName",destDirName = "db/__resource__";const prefixFilter = (prefix) => prefix.substring(0, prefix.length - 1);const createImage = (bucketName, objectName) => {axios.post(`${url}/object/presignedGetObject`, {bucketName: bucketName,objectName: objectName,}).then((res) => {if (res.data.success) {axios({method: "get",url: res.data.data,responseType: "arraybuffer",}).then((r) => {fs.writeFile(`./${destDirName}/${objectName}`,r.data,"binary",function (err) {if (err) console.error(err);console.log(`创建图片${objectName}成功`);});});}});
};const recursive = (bucketName, prefix) => {axios.post(`${url}/object/listObjects`, {bucketName: bucketName,prefix: prefix,pageNum: -1,}).then((res) => {console.log("获取图片信息", res.data.data);if (res.data.success) {return res.data.data.lists;}}).then((data) => {data?.forEach((d) => {if (d.prefix) {if (fs.existsSync(`./${destDirName}/${prefixFilter(d.prefix)}`)) {recursive(bucketName, d.prefix);} else {fs.promises.mkdir(`./${destDirName}/${prefixFilter(d.prefix)}`).then(() => {recursive(bucketName, d.prefix);}).catch((err) => console.error(err));}} else {if (/\.(png|svg|jepg|jpg|gif|mp4|mp3|avi|flv)$/.test(d.name)) {console.log("d.name", d.name);createImage(bucketName, d.name);}}});});
};recursive(bucketName, "");
mongodb.js

对于数据的存储与隔离,则采用“边侧+云侧”的方式进行备份存储。其中,对于云侧,使用mongodb进行数据的存储与操作,代码如下:

const mongoose = require('mongoose');const uname = 'admin',upwd = 'abc123';const url = ['ip:port',// 127.0.0.1:27017 本地启动的mongodb
];// console.log(`mongodb://${uname}:${upwd}@${url.join(',')}`)async function db() {await mongoose.connect(`mongodb://${uname}:${upwd}@${url.join(',')}`);
}exports.db = db;exports.mongoose = mongoose;

对于边缘侧,则可以使用模拟的集合操作来进行磁盘的挂载与存储,代码如下:

// model.js
exports.DOCUMENTS_SCHEMA = {_name: String,_collections: Array,
};exports.COLLECTIONS_SCHEMA = {_id: String,
};// operator.js
const { read } = require('./read');const { write } = require('./write');exports.find = async (...args) => await read('FIND', ...args);exports.remove = async (...args) => await write('REMOVE', ...args);exports.add = async (...args) => await write('ADD', ...args);exports.update = async (...args) => await write('UPDATE', ...args);// read.js
const { isExit,genCollection,genDocument,findCollection,findLog,stringify,fs,compose,path
} = require('./utils');exports.read = async (method, ...args) => {let col = '', log = '';const isFileExit = isExit(args[0], `${args[1]}_${args[2]['phone']}.json`);console.log('isFileExit', isFileExit)const doc = genDocument(...args);switch (method) {case 'FIND':col = compose( stringify, findCollection )(doc, genCollection(...args));log = compose( stringify, findLog, genCollection )(...args);break;};if(isFileExit) {return fs.promises.readFile(path.resolve(__dirname, `../db/${args.slice(0,2).join('/')}`), {encoding: 'utf-8'}).then(res => {console.log('res', res);console.log(log)return {flag: true,data: res,};})} else {return {flag: false,data: {}};}
};// write.js
const {isExit,fs,path,stringify,compose,genCollection,addCollection,addLog,updateCollection,updateLog,removeCollection,removeLog,genDocument
} = require('./utils');exports.write = async (method, ...args) => {console.log('write args', args, typeof args[2]);const isDirExit = isExit(args.slice(0, 1));const doc = genDocument(...args);let col = '', log = '';switch (method) {case 'ADD':col = compose( stringify, addCollection )(doc, genCollection(...args));log = compose( stringify, addLog, genCollection )(...args);break;case 'REMOVE':col = compose( stringify, removeCollection )(doc, genCollection(...args));log = compose( stringify ,removeLog, genCollection )(...args);break;case 'UPDATE':col = compose( stringify, updateCollection )(doc, genCollection(...args));log = compose( stringify, updateLog, genCollection )(...args);break;}if (!isDirExit) {return fs.promises.mkdir(path.resolve(__dirname, `../db/${args[0]}`)).then(() => {console.log(`创建数据库${args[0]}成功`);return true;}).then(flag => {if (flag) {return fs.promises.writeFile(path.resolve(__dirname, `../db/${args.slice(0,2).join('/')}`), col).then(() => {console.log(log);return true;}).catch(err => console.error(err))}}).catch(err => console.error(err))} else {return fs.promises.writeFile(path.resolve(__dirname, `../db/${args.slice(0,2).join('/')}`), col).then(() => {console.log(log)return true;}).catch(err => console.error(err))}
};

对于工具函数utils,代码如下:

// utils
const { DOCUMENTS_SCHEMA, COLLECTIONS_SCHEMA } = require('./model');const { v4: uuidv4 } = require('uuid');const path = require('path');const fs = require('fs');exports.path = path;
exports.uuid = uuidv4;
exports.fs = fs;exports.compose = (...funcs) => {if(funcs.length===0){return arg=>arg;}if(funcs.length===1){return funcs[0];}return funcs.reduce((a,b)=>(...args)=>a(b(...args)));
};exports.stringify = arg => JSON.stringify(arg);exports.isExit = (...args) => fs.existsSync(path.resolve(__dirname, `../db/${args.join('/')}`));console.log('DOCUMENTS_SCHEMA', DOCUMENTS_SCHEMA);exports.genDocument = (...args) => {return {_name: args[1],_collections: []}
};console.log('COLLECTIONS_SCHEMA', COLLECTIONS_SCHEMA);exports.genCollection = (...args) => {return {_id: uuidv4(),...args[2]}
};exports.addCollection = ( doc, col ) => {doc._collections.push(col);return doc;
};exports.removeCollection = ( doc, col ) => {for(let i = 0; i < doc._collections.length; i++) {if(doc._collections[i][`_id`] == col._id) {doc._collections.splice(i,1)}}return doc;
};exports.findCollection = ( doc, col ) => {return doc._collections.filter(f => f._id == col._id)[0];
};exports.updateCollection = ( doc, col ) => {doc._collections = [col];return doc;
};exports.addLog = (arg) => {return `增加了集合 ${JSON.stringify(arg)}`
};exports.removeLog = () => {return `移除集合成功`
};exports.findLog = () => {return `查询集合成功`
};exports.updateLog = (arg) => {return `更新了集合 ${JSON.stringify(arg)}`
};
run.sh

对于边缘侧,由于其自身的环境限制,通常来说构建边缘侧运行时便成为了边缘计算性能好坏的关键因素。近年来,各大厂商及开发者都致力于对边缘侧运行时环境的探索。

其中,个人以为以“Rust+WebAssembly"的运行时构建技术方案相对来说具有一定的优势。首先,Rust自身是内存安全的,其对边缘场景有着天然的优势;其次,WebAssembly是各大语言转换方案中的一种重要桥梁,尤其对于以大前端为技术底座的体系而言,更可谓是恰如其分的弥补了前端体系的缺陷;最后,基于“rust+wasm”的方案相较于docker而言具有更小的初始体积。故而,这里采用了业界已有的WasmEdge的现成运行时方案,运行脚本代码如下:

# 下载wasmedge边缘运行时
wget https://github.com/second-state/wasmedge-quickjs/releases/download/v0.5.0-alpha/wasmedge_quickjs.wasm# 运行边缘侧node.js服务
$ wasmedge --dir .:. wasmedge_quickjs.wasm app.js

云侧

目录结构
- go- compute- machine.go- metal.go- service.go- network- balance.go- virtual.go- storage- block.go- container.go- file.go- object.go- build.sh- main.go
- src- database.js----------------------------------------------------- 云数据库封装- index.js-------------------------------------------------------- 云函数sdk打包入口- storage.js------------------------------------------------------ 云存储封装
- minio.yaml---------------------------------------------------------- 云端对象存储部署
- mongo.yaml---------------------------------------------------------- 云端数据库部署
核心逻辑
go

go部分是进行云中间件相关产物的构建,这里不是前端Serverless构建的核心,需要配合云产商或者云相关的部门进行协作,这里以go语言为基础蓝本,简写下相关产品的一些伪码逻辑

database.js

基于云端数据库产品的封装,对于Serverless而言,主要是以mongodbNoSQL数据库为主

storage.js

基于云端存储产品的封装,包括:对象存储、块存储、文件存储等

index.js

Serverless云函数相关的sdk封装,代码如下:

import database from './database';
import storage from './storage';function cloud() {console.log('vwaver-cloud-sdk');
}cloud.prototype.database = database;cloud.prototype.storage = storage;export default cloud;
minio.yaml

对于云平台的对象存储,采用minio的k8s相关部署,代码如下:

apiVersion: v1
kind: Pod
metadata:labels:app: minioname: minio
spec:containers:- name: minioimage: quay.io/minio/minio:latestcommand:- /bin/bash- -cargs: - minio server /minio --console-address :9090volumeMounts:- mountPath: /minioname: minio-volumevolumes:- name: minio-volumehostPath:path: /mnt/miniotype: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:name: minio
spec:type: ClusterIPselector:app: minioports:- port: 9090targetPort: 9090
mongo.yaml

对于云平台的mongodb数据库,部署代码如下:

apiVersion: apps/v1 
kind: Deployment
metadata:name: mongodblabels:app: mongodb
spec:replicas: 3selector:matchLabels:app: mongodbtemplate:metadata:labels:app: mongodbspec:containers:- name: mongodbimage: hub.docker.com/mongo:latestimagePullPolicy: Alwaysresources:limits:cpu: 5memory: 10Grequests:cpu: 1memory: 1Genv:- name: MONGO_INITDB_ROOT_USERNAME  # 设置用户名value: admin- name: MONGO_INITDB_ROOT_PASSWORD  # 设置密码value: abc123volumeMounts:- mountPath: /mongodb                    name: mongodb-volumevolumes:- name: mongodb-volumehostPath:path: /mnt/mongodbtype: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:name: mongodb
spec:type: ClusterIPselector:app: mongodbports:- port: 27017targetPort: 27017

总结

对于本次应用构建,对于业务的逻辑而言,其实还是相对简单的,但是对于环境的部署与调试带来的不确定性还是需要各位开发者去思考和延展的,尤其是对于复杂边缘场景的生产化过程,其本身的复杂性也要远远超过业务逻辑本身,可进行如下总结:

  1. 端侧:提供高适配性能的应用兼容,要注意某些特殊尺寸及渲染引擎剪切造成的功能问题
  2. 边侧:渲染场景中对于离线要求较高,提供高性能的runtime是重中之重,例如:wasmedge(rust+wasm)
  3. 云侧:提供基于k8s或者k3s的服务编排集群,支持Serverless化,提供云、边、端一致性的环境部署及开发

业务开发本身并不仅仅是考察如何对业务逻辑进行拆解,更重要的是能够透过业务本身来思考今后开发过程中的研发模式以及一些痛点问题的解决与规避,前端工程师并不仅仅是一个业务逻辑的实现者,更要是问题的发现者,发现问题、解决问题并形成一套统一的模板方案,这才是工程师的标准与要求,共勉!!!

最后,本次业务实践的代码也进行了开源,有需要的同学可以进行查看,如果觉得还可以还可以的话,欢迎点个 star~

  1. vwaver-iiot-end
  2. vwaver-iiot-edge
  3. vwaver-iiot-cloud

参考

  • 【华为云 IoTEdge 学习笔记】四大常见边缘场景如何深度使用
  • 史上最全的边缘计算应用场景
  • UCS(优势)—边缘计算五大典型应用场景
  • 一文读懂边缘计算及其应用场景
  • 带你走进 PWA 在业务中的实践方案
  • 现代化 Web 开发实践之 PWA
  • PWA 技术在游戏落地中的探索
  • 使用 workbox 开发 PWA
  • PWA实践/应用(Google Workbox)
  • k8s部署MongoDB
  • Minio官网
  • WasmEdge官网
  • Mongoose官网
  • 边缘云上的微服务:使用 WasmEdge 和 Rust 构建高性能且安全的应用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/118820.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

信息系统架构的设计理论与实践

信息系统架构的设计理论与实践 信息系统架构概述 信息系统架构的定义和发展 信息系统架构的定义 骚戴理解&#xff1a;这里只要背定义即可 信息系统架构的发展 信息系统架构的分类&#xff08;集中式和分布式&#xff09; 集中式结构 分布式结构 信息系统常用的四种架构模型…

番外8.2---配置/管理硬盘

""" Step1&#xff1a;清楚磁盘、硬盘&#xff08;HDD&#xff09;、光驱的概念及是否具有包含关系。 Step2&#xff1a;硬件设备&#xff08;IDE、SCSI、SATA、NVMe、软驱等&#xff09;命名方式及在linux系统里对应的文件名称。 Step3&#xff1a;&#xff1…

2023-10学习笔记

1.sql注入 不管是上一篇博客&#xff0c;通过java代码执行sql 还是我们常用的Mybatis的#{}和${} 都会提到sql注入的问题 1.1啥是sql注入 应该知道是说传入无关的参数&#xff0c;比如本来是想要一个where条件查询参数 但是你拼了一个drop 比如 原来的sql select * from…

正点原子嵌入式linux驱动开发——RGB转HDMI

目前大多数的显示器都提供了HDMI接口&#xff0c;HDMI的应用范围也越来越广&#xff0c;但是STM32MP157这颗芯片原生并不支持HDMI显示。可以通过RGB转HDMI芯片将RGB信号转为HDMI信号&#xff0c;这样就可以连接HDMI显示器了。本章就来学习一下如何在正点原子的STM32MP1开发板上…

《从零开始大模型开发与微调 :基于PyTorch与ChatGLM》简介

内 容 简 介 大模型是深度学习自然语言处理皇冠上的一颗明珠&#xff0c;也是当前AI和NLP研究与产业中最重要的方向之一。本书使用PyTorch 2.0作为学习大模型的基本框架&#xff0c;以ChatGLM为例详细讲解大模型的基本理论、算法、程序实现、应用实战以及微调技术&#xff0c;…

Qt中的枚举变量,Q_ENUM,Q_FLAG以及Qt中自定义结构体、枚举型做信号参数传递

Qt中的枚举变量,Q_ENUM,Q_FLAG,Q_NAMESPACE,Q_ENUM_NS,Q_FLAG_NS以及其他 理论基础&#xff1a;一、Q_ENUM二、QMetaEnum三、Q_FLAG四、示例 Chapter1 Qt中的枚举变量,Q_ENUM,Q_FLAG,Q_NAMESPACE,Q_ENUM_NS,Q_FLAG_NS以及其他前言Q_ENUM的使用Q_FLAG的引入解决什么问题&#xf…

Pytorch指定数据加载器使用子进程

torch.utils.data.DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue,num_workers4, pin_memoryTrue) num_workers 参数是 DataLoader 类的一个参数&#xff0c;它指定了数据加载器使用的子进程数量。通过增加 num_workers 的数量&#xff0c;可以并行地读取和预处…

如何将音频与视频分离

您一定经历过这样的情况&#xff1a;当你非常喜欢视频中的背景音乐时&#xff0c;希望将音频从视频中分离出来&#xff0c;以便你可以在音乐播放器中收听音乐。有没有一种有效的方法可以帮助您快速从视频中提取音频呢&#xff1f;当然是有的啦&#xff0c;在下面的文章中&#…

根据输入类型来选择函数不同的实现方法functools.singledispatch

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 根据输入类型来选择函数不同的实现方法 functools.singledispatch 输入6后&#xff0c;下列输出正确的是&#xff1f; from functools import singledispatch singledispatch def calcu…

树莓派系统文件解析

title: “树莓派系统文件分析” date: 2023-10-25 permalink: /posts/2023/10/blog-post-5/ tags: 树莓派 本篇blog来分析和总结下树莓派系统文件以及他们的作用。使用的系统是Raspberry Pi OS with desktop System: 64-bitKernel version: 6.1Debian version: 12 (bookworm)…

经典链表试题(二)

文章目录 一、移除链表元素1、题目介绍2、思路讲解3、代码实现 二、反转链表1、题目介绍2、思路讲解3、代码实现 三、相交链表1、题目介绍2、思路讲解3、代码实现 四、链表的中间结点1、题目介绍2、思路讲解3、代码实现 五、设计循环队列1、题目介绍2、思路讲解3、代码实现 六、…

2023高频前端面试题-http

1. HTTP有哪些⽅法&#xff1f; HTTP 1.0 标准中&#xff0c;定义了3种请求⽅法&#xff1a;GET、POST、HEAD HTTP 1.1 标准中&#xff0c;新增了请求⽅法&#xff1a;PUT、PATCH、DELETE、OPTIONS、TRACE、CONNECT 2. 各个HTTP方法的具体作用是什么&#xff1f; 方法功能G…

『C语言进阶』动态内存管理

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f516;系列专栏&#xff1a; C语言、Linux、Cpolar ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 前言一、动态内存函数的介绍1.1 malloc和free函数1.2 calloc函数1.3 realloc函数 二、常见的动态内存错误2.1 …

行业模型应该如何去拆解?

行业模型应该如何去拆解&#xff1f; 拆解行业模型是一个复杂的过程&#xff0c;涉及对整个行业的深入分析和理解。下面是一些步骤和方法&#xff0c;可以帮助你系统地拆解行业模型&#xff1a; 1. 确定行业范围 定义行业&#xff1a;明确你要分析的行业是什么&#xff0c;包括…

React中的Virtual DOM(看这一篇就够了)

文章目录 前言了解Virtual DOMreact创建虚拟dom的方式React Element虚拟dom的流程虚拟dom和真实dom的对比后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;react合集 &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌…

在pycharm中创建python模板文件

File——>Setting——>File and Code Templates——>Python Scripts 在文本框中输入模板内容

vue首页多模块布局(标题布局)

<template><div class"box"><div class"content"><div class"box1" style"background-color: rgb(245,23,156)">第一个</div><div class"box2" style"background-color: rgb(12,233,…

windows下使用FFmpeg开源库进行视频编解码完整步聚

最终解码效果: 1.UI设计 2.在控件属性窗口中输入默认值 3.复制已编译FFmpeg库到工程同级目录下 4.在工程引用FFmpeg库及头文件 5.链接指定FFmpeg库 6.使用FFmpeg库 引用头文件 extern "C" { #include "libswscale/swscale.h" #include "libavdevic…

composer安装thinkphp6报错

composer安装thinkphp6报错&#xff0c; 查看是否安装了对应的PHP扩展&#xff0c;我这边使用的是宝塔的环境&#xff0c;全程可以可视化操作 这样就可以安装完成了

【AIGC】百度文库文档助手之 - 一键生成PPT

百度文库文档助手之 - 一键生成PPT 引言一、文档助手&#xff1a;体验一键生成PPT二、文档助手&#xff1a;进阶用法三、其它生成PPT的方法3.1 ChatGPT3.2 文心一言 引言 就在上个月百度文库升级为一站式智能文档平台&#xff0c;开放四大AI能力&#xff1a;智能PPT、智能总结、…