背景
隨著5G技術(shù)的發(fā)展扎运,物聯(lián)網(wǎng)邊緣側(cè)主要應(yīng)用于數(shù)據(jù)傳輸量大、安全要求高以及數(shù)據(jù)實(shí)時(shí)處理等行業(yè)與應(yīng)用場(chǎng)景中饮戳。其中豪治,邊緣計(jì)算是一種分布式計(jì)算模式,其將計(jì)算資源和數(shù)據(jù)處理能力推向接近數(shù)據(jù)源的邊緣設(shè)備扯罐,以減少延遲并提高響應(yīng)速度负拟。
對(duì)前端領(lǐng)域而言,面對(duì)邊緣場(chǎng)景下的應(yīng)用開(kāi)發(fā)也發(fā)生了相應(yīng)的變化歹河,其通常需要考慮邊緣側(cè)與終端側(cè)的實(shí)現(xiàn)方式掩浙,并且還需考慮相較于傳統(tǒng) B/S 架構(gòu)下的部署方案。本文旨在通過(guò)工業(yè)互聯(lián)網(wǎng)場(chǎng)景下的一個(gè)實(shí)踐案例秸歧,淺析面向邊緣情形下的前端研發(fā)模式升級(jí)厨姚,以期能夠給有邊緣場(chǎng)景應(yīng)用開(kāi)發(fā)需求的讀者提供一定的思路與借鑒。
架構(gòu)設(shè)計(jì)
相較于傳統(tǒng)前端研發(fā)場(chǎng)景键菱,面對(duì)邊緣情境下的前端研發(fā)模式谬墙,最重要的變化在于其環(huán)境的特殊性,包括:網(wǎng)絡(luò)纱耻、存儲(chǔ)等芭梯。在前期調(diào)研了部署環(huán)境后,為考慮用戶(hù)體驗(yàn)弄喘,故而從架構(gòu)設(shè)計(jì)上對(duì)整體系統(tǒng)進(jìn)行了如下分層玖喘,分別是:應(yīng)用層、服務(wù)層蘑志、平臺(tái)層累奈,如下圖所示:
其中贬派,應(yīng)用層為了更好的體現(xiàn)離線(xiàn)與 Web 各自的優(yōu)勢(shì),故而采用“Web+PWA”的形式進(jìn)行呈現(xiàn)澎媒;案例中業(yè)務(wù)邏輯較為簡(jiǎn)單搞乏,服務(wù)層采用以Node.js
為主的BFF
形式的Serverless
進(jìn)行處理;對(duì)于平臺(tái)層戒努,本身案例應(yīng)用部署環(huán)境為虛擬機(jī)環(huán)境请敦,但考慮到多端的一致性,故而也支持容器化的部署储玫。
技術(shù)選型
前期調(diào)研后侍筛,由于虛擬機(jī)Windows側(cè)可能需要兼容IE 11,故而選擇以Vue 2.x
為主的全家桶構(gòu)建撒穷,同時(shí)安裝 PWA 的相關(guān)依賴(lài)匣椰。BFF側(cè),提供以mongoDB
+ Node.js
的類(lèi) Serverless 服務(wù)端礼,通過(guò)Docker
容器禽笑、虛擬機(jī)及其他runtime
進(jìn)行調(diào)度,如下圖所示:
源碼分析
端側(cè)
目錄結(jié)構(gòu)
- 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------------------------------------------------------- 列表及查詢(xún)接口
- assets
- logo.png
- components
- Footer.vue------------------------------------------------- 底部組件
- Header.vue------------------------------------------------- 頭部組件
- Item.vue--------------------------------------------------- 列表組件
- Layout.vue------------------------------------------------- 布局組件
- router
- index.js--------------------------------------------------- 路由攔截等相關(guān)邏輯
- routes.js-------------------------------------------------- 路由表
- store
- index.js
- styles
- index.less
- utils
- http.js---------------------------------------------------- 封裝http請(qǐng)求蛤奥,axios攔截器
- views
- Home.vue--------------------------------------------------- 首頁(yè)佳镜,用于路由表層級(jí)渲染
- Login.vue-------------------------------------------------- 登錄頁(yè)
- NotFound.vue----------------------------------------------- 路由未匹配頁(yè)
- App.vue-------------------------------------------------------- 根組件
- main.js-------------------------------------------------------- Webpack打包的入口
- registerServiceWorker.js--------------------------------------- PWA聲明周期,service worker處理邏輯
- base.config.js----------------------------------------------------- 基礎(chǔ)配置凡桥,用于腳手架讀取
- default.conf------------------------------------------------------- nginx的conf配置
核心邏輯
router
構(gòu)建路由表邀杏,用于處理頁(yè)面的跳轉(zhuǎn),是一個(gè)樹(shù)形結(jié)構(gòu)唬血,代碼如下:
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;
對(duì)于router
的入口,需要處理一下登錄的攔截唤崭,使用路由攔截進(jìn)行處理拷恨,代碼如下:
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
對(duì)于狀態(tài)管理,需要對(duì)整體業(yè)務(wù)邏輯進(jìn)行統(tǒng)一處理谢肾,由于比較簡(jiǎn)單腕侄,不需要用modules
進(jìn)行隔離,代碼如下:
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
對(duì)于登錄頁(yè)芦疏,進(jìn)行一個(gè)簡(jiǎn)單的驗(yàn)證冕杠,代碼如下:
<template>
<div class="login-view">
<section class="login-box">
<div class="login-box-header">
<img
class="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>
<Input
v-decorator="[
'uname',
{ rules: [{ required: true, message: '請(qǐng)輸入用戶(hù)名!' }] },
]"
placeholder="請(qǐng)輸入用戶(hù)名"
>
<Icon
slot="prefix"
type="user"
style="color: rgba(0, 0, 0, 0.25);"
/>
</Input>
</FormItem>
<FormItem>
<Input
v-decorator="[
'password',
{
rules: [
{ required: true, message: 'Please input your Password!' },
],
},
]"
type="password"
placeholder="請(qǐng)輸入密碼"
>
<Icon
slot="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>
對(duì)于Home
頁(yè)面,需要對(duì)頁(yè)面的路由進(jìn)行相應(yīng)的渲染酸茴,代碼如下:
<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>
// 鼠標(biāo)hover時(shí)候的顏色
/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;
}
// 設(shè)置默認(rèn)的顏色
/deep/ .ant-checkbox {
.ant-checkbox-inner {
border: 1px solid #61499b;
background-color: transparent;
}
}
// 設(shè)置選中的顏色
/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
對(duì)于頂部搜索分预,實(shí)現(xiàn)組件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>
對(duì)于底部顯示數(shù)量薪捍,實(shí)現(xiàn)組件Footer
笼痹,代碼如下:
<template>
<div class="footer">
<template v-if="mode == 'search'">
<span class="footer-text">已搜到{{ $store.state.count }}項(xiàng)</span>
</template>
<span class="footer-text" v-else>
已選{{ $store.state.checkedList.length }}項(xiàng)
</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>
對(duì)于列表的每項(xiàng)的顯示配喳,則進(jìn)行一個(gè)統(tǒng)一的抽離,這也是本案例中最為核心的一個(gè)組件凳干,代碼如下:
<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>
<section
v-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>
邊側(cè)
目錄結(jié)構(gòu)
- 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-------------------------------------------------------- 列表及查詢(xún)接口
- object.js------------------------------------------------------ 對(duì)象存儲(chǔ)接口
- app.js------------------------------------------------------------- express應(yīng)用
- cluster.js--------------------------------------------------------- 用于監(jiān)聽(tīng)app.js
- router.js---------------------------------------------------------- 統(tǒng)一的路由
- minio.js----------------------------------------------------------- minio設(shè)置
- mongodb.js--------------------------------------------------------- mongodb設(shè)置
- run.sh------------------------------------------------------------- wasmedge邊緣運(yùn)行時(shí)
核心邏輯
app.js
BFF采用簡(jiǎn)單的express
服務(wù)晴裹,實(shí)例化入口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
構(gòu)建app
的監(jiān)聽(tīng)救赐,代碼如下:
var fork = require("child_process").fork;
//保存被子進(jìn)程實(shí)例數(shù)組
var workers = [];
//這里的被子進(jìn)程理論上可以無(wú)限多
var appsPath = ["./app.js"];
var createWorker = function(appPath) {
//保存fork返回的進(jìn)程實(shí)例
var worker = fork(appPath); //監(jiān)聽(tīng)子進(jìn)程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);
};
//啟動(dòng)所有子進(jìn)程
for (var i = appsPath.length - 1; i >= 0; i--) {
createWorker(appsPath[I]);
}
//父進(jìn)程退出時(shí)殺死所有子進(jìn)程
process.on("exit", function() {
for (var pid in workers) {
workers[pid].kill();
}
});
routes
對(duì)于鑒權(quán)部分涧团,采用jwt
進(jìn)行驗(yàn)證,代碼如下:
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("成功的回調(diào)", result);
res.json({
code: "0",
data: {
token,
},
msg: "成功",
success: true,
});
},
(err) => {
console.log("失敗的回調(diào)", err);
res.json({
code: "-1",
data: {
err: err,
},
msg: "失敗",
success: false,
});
}
);
});
module.exports = router;
對(duì)于列表及查詢(xún)相關(guān)接口经磅,代碼如下:
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);
// 簡(jiǎn)單化操作泌绣,設(shè)計(jì)時(shí)可對(duì)mongodb數(shù)據(jù)庫(kù)進(jìn)行更細(xì)粒度的集合處理
const navMap = new NavMap({
Home: [
{
name: "全部",
path: "/home",
},
],
Equipment: [
{
name: "全部",
path: "/home",
},
{
name: "工業(yè)設(shè)備",
path: "/home/equipment",
},
],
Management: [
{
name: "全部",
path: "/home",
},
{
name: "工業(yè)設(shè)備",
path: "/home/equipment",
},
{
name: "設(shè)備管理",
path: "/home/equipment/management",
},
],
CPE: [
{
name: "全部",
path: "/home",
},
{
name: "工業(yè)設(shè)備",
path: "/home/equipment",
},
{
name: "設(shè)備管理",
path: "/home/equipment/management",
},
{
name: "CPE設(shè)備",
path: "/home/equipment/management/cpe",
},
],
Hub: [
{
name: "全部",
path: "/home",
},
{
name: "工業(yè)設(shè)備",
path: "/home/equipment",
},
{
name: "設(shè)備管理",
path: "/home/equipment/management",
},
{
name: "Hub設(shè)備",
path: "/home/equipment/management/hub",
},
],
Switch: [
{
name: "全部",
path: "/home",
},
{
name: "工業(yè)設(shè)備",
path: "/home/equipment",
},
{
name: "設(shè)備管理",
path: "/home/equipment/management",
},
{
name: "交換機(jī)設(shè)備",
path: "/home/equipment/management/switch",
},
],
Robot: [
{
name: "全部",
path: "/home",
},
{
name: "工業(yè)設(shè)備",
path: "/home/equipment",
},
{
name: "設(shè)備管理",
path: "/home/equipment/management",
},
{
name: "機(jī)器人設(shè)備",
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("成功的回調(diào)", result);
res.json({
code: "0",
data: {
nav: navMap[`${name}`],
},
msg: "成功",
success: true,
});
},
(err) => {
console.log("失敗的回調(diào)", err);
res.json({
code: "-1",
data: {
err: err,
},
msg: "失敗",
success: false,
});
}
);
});
const columnMap = new ColumnMap({
Home: [
{
id: "equipment",
title: "工業(yè)設(shè)備",
subTitle: "全部",
prev: [],
children: [
"management",
"cpe",
"camera",
"wifi",
"hub",
"usb",
"ethernet",
"switch",
"two",
"three",
"four",
"robot",
"arm",
"leg",
],
},
],
Equipment: [
{
id: "management",
title: "設(shè)備管理",
subTitle: "全部 - 工業(yè)設(shè)備",
prev: ["equipment"],
children: [
"cpe",
"camera",
"wifi",
"hub",
"usb",
"ethernet",
"switch",
"two",
"three",
"four",
"robot",
"arm",
"leg",
],
},
],
Management: [
{
id: "cpe",
title: "CPE設(shè)備",
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理",
prev: ["equipment", "management"],
children: ["camera", "wifi"],
},
{
id: "hub",
title: "Hub設(shè)備",
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理",
prev: ["equipment", "management"],
children: ["usb", "ethernet"],
},
{
id: "switch",
title: "交換機(jī)設(shè)備",
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理",
prev: ["equipment", "management"],
children: ["two", "three", "four"],
},
{
id: "robot",
title: "機(jī)器人設(shè)備",
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理",
prev: ["equipment", "management"],
children: ["arm", "leg"],
},
],
CPE: [
{
id: "camera",
title: "攝像頭",
prev: ["equipment", "management", "cpe"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - CPE設(shè)備",
children: [],
},
{
id: "wifi",
title: "WiFi",
prev: ["equipment", "management", "cpe"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - CPE設(shè)備",
children: [],
},
],
Hub: [
{
id: "usb",
title: "USB Hub",
prev: ["equipment", "management", "hub"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - Hub設(shè)備",
children: [],
},
{
id: "ethernet",
title: "Ethernet Hub",
prev: ["equipment", "management", "hub"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - Hub設(shè)備",
children: [],
},
],
Switch: [
{
id: "two",
title: "二層交換機(jī)",
prev: ["equipment", "management", "switch"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - 交換機(jī)設(shè)備",
children: [],
},
{
id: "three",
title: "三層交換機(jī)",
prev: ["equipment", "management", "switch"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - 交換機(jī)設(shè)備",
children: [],
},
{
id: "four",
title: "四層交換機(jī)",
prev: ["equipment", "management", "switch"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - 交換機(jī)設(shè)備",
children: [],
},
],
Robot: [
{
id: "arm",
title: "機(jī)械臂",
prev: ["equipment", "management", "robot"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - 機(jī)器人設(shè)備",
children: [],
},
{
id: "leg",
title: "腿式機(jī)器人",
prev: ["equipment", "management", "robot"],
subTitle: "全部 - 工業(yè)設(shè)備 - 設(shè)備管理 - 機(jī)器人設(shè)備",
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("成功的回調(diào)", result);
res.json({
code: "0",
data: {
list: columnMap[`${name}`],
},
msg: "成功",
success: true,
});
},
(err) => {
console.log("失敗的回調(diào)", 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;
其中,對(duì)于樹(shù)形結(jié)構(gòu)的構(gòu)建馋贤,采用雙向鏈表的形式進(jìn)行prev
及children
的派發(fā)赞别,如下圖所示:
[圖片上傳失敗...(image-76d7bd-1698252174837)]
router.js
構(gòu)建統(tǒng)一的 express 路由,用于各routes
模塊的引用配乓,代碼如下:
const express = require('express');
const router = express.Router();
module.exports = router;
minio.js
使用minio
來(lái)對(duì)對(duì)象存儲(chǔ)中的資源進(jìn)行處理仿滔,邊緣側(cè)對(duì)網(wǎng)絡(luò)要求較高,對(duì)于某些離線(xiàn)場(chǎng)景犹芹,需要將靜態(tài)資源托管到本地崎页,代碼如下:
const Minio = require('minio');
// 對(duì)于靜態(tài)資源,在邊緣側(cè)可進(jìn)行圖片腰埂、視頻等靜態(tài)資源計(jì)算和緩存飒焦,與邊緣側(cè)部署存儲(chǔ)方式有關(guān)
const minio = key => {
return new Minio.Client({
endPoint: 'ip',
port: 9090,
useSSL: false,
accessKey: 'accessKey',
secretKey: 'secretKey'
});
}
module.exports = minio;
對(duì)于同步操作,可以使用edge
目錄下的sync
模塊進(jìn)行處理屿笼,代碼如下:
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(`創(chuàng)建圖片${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
對(duì)于數(shù)據(jù)的存儲(chǔ)與隔離牺荠,則采用“邊側(cè)+云側(cè)”的方式進(jìn)行備份存儲(chǔ)。其中驴一,對(duì)于云側(cè)休雌,使用mongodb
進(jìn)行數(shù)據(jù)的存儲(chǔ)與操作,代碼如下:
const mongoose = require('mongoose');
const uname = 'admin',
upwd = 'abc123';
const url = [
'ip:port',
// 127.0.0.1:27017 本地啟動(dòng)的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;
對(duì)于邊緣側(cè)肝断,則可以使用模擬的集合操作來(lái)進(jìn)行磁盤(pán)的掛載與存儲(chǔ)杈曲,代碼如下:
// 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(`創(chuàng)建數(shù)據(jù)庫(kù)${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))
}
};
對(duì)于工具函數(shù)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 `查詢(xún)集合成功`
};
exports.updateLog = (arg) => {
return `更新了集合 ${JSON.stringify(arg)}`
};
run.sh
對(duì)于邊緣側(cè)胸懈,由于其自身的環(huán)境限制担扑,通常來(lái)說(shuō)構(gòu)建邊緣側(cè)運(yùn)行時(shí)便成為了邊緣計(jì)算性能好壞的關(guān)鍵因素。近年來(lái)趣钱,各大廠商及開(kāi)發(fā)者都致力于對(duì)邊緣側(cè)運(yùn)行時(shí)環(huán)境的探索涌献。
其中,個(gè)人以為以“Rust+WebAssembly"的運(yùn)行時(shí)構(gòu)建技術(shù)方案相對(duì)來(lái)說(shuō)具有一定的優(yōu)勢(shì)羔挡。首先洁奈,Rust
自身是內(nèi)存安全的间唉,其對(duì)邊緣場(chǎng)景有著天然的優(yōu)勢(shì);其次利术,WebAssembly
是各大語(yǔ)言轉(zhuǎn)換方案中的一種重要橋梁呈野,尤其對(duì)于以大前端為技術(shù)底座的體系而言,更可謂是恰如其分的彌補(bǔ)了前端體系的缺陷;最后,基于“rust+wasm”的方案相較于docker
而言具有更小的初始體積已添。故而娇钱,這里采用了業(yè)界已有的WasmEdge
的現(xiàn)成運(yùn)行時(shí)方案朽合,運(yùn)行腳本代碼如下:
# 下載wasmedge邊緣運(yùn)行時(shí)
wget https://github.com/second-state/wasmedge-quickjs/releases/download/v0.5.0-alpha/wasmedge_quickjs.wasm
# 運(yùn)行邊緣側(cè)node.js服務(wù)
$ wasmedge --dir .:. wasmedge_quickjs.wasm app.js
云側(cè)
目錄結(jié)構(gòu)
- 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----------------------------------------------------- 云數(shù)據(jù)庫(kù)封裝
- index.js-------------------------------------------------------- 云函數(shù)sdk打包入口
- storage.js------------------------------------------------------ 云存儲(chǔ)封裝
- minio.yaml---------------------------------------------------------- 云端對(duì)象存儲(chǔ)部署
- mongo.yaml---------------------------------------------------------- 云端數(shù)據(jù)庫(kù)部署
核心邏輯
go
go
部分是進(jìn)行云中間件相關(guān)產(chǎn)物的構(gòu)建,這里不是前端Serverless
構(gòu)建的核心,需要配合云產(chǎn)商或者云相關(guān)的部門(mén)進(jìn)行協(xié)作,這里以go
語(yǔ)言為基礎(chǔ)藍(lán)本率触,簡(jiǎn)寫(xiě)下相關(guān)產(chǎn)品的一些偽碼邏輯
database.js
基于云端數(shù)據(jù)庫(kù)產(chǎn)品的封裝,對(duì)于Serverless
而言汇竭,主要是以mongodb
的NoSQL
數(shù)據(jù)庫(kù)為主
storage.js
基于云端存儲(chǔ)產(chǎn)品的封裝葱蝗,包括:對(duì)象存儲(chǔ)、塊存儲(chǔ)细燎、文件存儲(chǔ)等
index.js
Serverless
云函數(shù)相關(guān)的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
對(duì)于云平臺(tái)的對(duì)象存儲(chǔ),采用minio
的k8s相關(guān)部署玻驻,代碼如下:
apiVersion: v1
kind: Pod
metadata:
labels:
app: minio
name: minio
spec:
containers:
- name: minio
image: quay.io/minio/minio:latest
command:
- /bin/bash
- -c
args:
- minio server /minio --console-address :9090
volumeMounts:
- mountPath: /minio
name: minio-volume
volumes:
- name: minio-volume
hostPath:
path: /mnt/minio
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: minio
spec:
type: ClusterIP
selector:
app: minio
ports:
- port: 9090
targetPort: 9090
mongo.yaml
對(duì)于云平臺(tái)的mongodb
數(shù)據(jù)庫(kù)悼凑,部署代碼如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongodb
labels:
app: mongodb
spec:
replicas: 3
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: hub.docker.com/mongo:latest
imagePullPolicy: Always
resources:
limits:
cpu: 5
memory: 10G
requests:
cpu: 1
memory: 1G
env:
- name: MONGO_INITDB_ROOT_USERNAME # 設(shè)置用戶(hù)名
value: admin
- name: MONGO_INITDB_ROOT_PASSWORD # 設(shè)置密碼
value: abc123
volumeMounts:
- mountPath: /mongodb
name: mongodb-volume
volumes:
- name: mongodb-volume
hostPath:
path: /mnt/mongodb
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: mongodb
spec:
type: ClusterIP
selector:
app: mongodb
ports:
- port: 27017
targetPort: 27017
總結(jié)
對(duì)于本次應(yīng)用構(gòu)建,對(duì)于業(yè)務(wù)的邏輯而言璧瞬,其實(shí)還是相對(duì)簡(jiǎn)單的户辫,但是對(duì)于環(huán)境的部署與調(diào)試帶來(lái)的不確定性還是需要各位開(kāi)發(fā)者去思考和延展的,尤其是對(duì)于復(fù)雜邊緣場(chǎng)景的生產(chǎn)化過(guò)程嗤锉,其本身的復(fù)雜性也要遠(yuǎn)遠(yuǎn)超過(guò)業(yè)務(wù)邏輯本身寸莫,可進(jìn)行如下總結(jié):
- 端側(cè):提供高適配性能的應(yīng)用兼容,要注意某些特殊尺寸及渲染引擎剪切造成的功能問(wèn)題
- 邊側(cè):渲染場(chǎng)景中對(duì)于離線(xiàn)要求較高档冬,提供高性能的
runtime
是重中之重,例如:wasmedge(rust+wasm) - 云側(cè):提供基于
k8s
或者k3s
的服務(wù)編排集群桃纯,支持Serverless
化酷誓,提供云、邊态坦、端一致性的環(huán)境部署及開(kāi)發(fā)
業(yè)務(wù)開(kāi)發(fā)本身并不僅僅是考察如何對(duì)業(yè)務(wù)邏輯進(jìn)行拆解盐数,更重要的是能夠透過(guò)業(yè)務(wù)本身來(lái)思考今后開(kāi)發(fā)過(guò)程中的研發(fā)模式以及一些痛點(diǎn)問(wèn)題的解決與規(guī)避,前端工程師并不僅僅是一個(gè)業(yè)務(wù)邏輯的實(shí)現(xiàn)者伞梯,更要是問(wèn)題的發(fā)現(xiàn)者玫氢,發(fā)現(xiàn)問(wèn)題帚屉、解決問(wèn)題并形成一套統(tǒng)一的模板方案,這才是工程師的標(biāo)準(zhǔn)與要求漾峡,共勉9サ!生逸!
最后牢屋,本次業(yè)務(wù)實(shí)踐的代碼也進(jìn)行了開(kāi)源,有需要的同學(xué)可以進(jìn)行查看槽袄,如果覺(jué)得還可以還可以的話(huà)烙无,歡迎點(diǎn)個(gè) star~
參考
- 【華為云 IoTEdge 學(xué)習(xí)筆記】四大常見(jiàn)邊緣場(chǎng)景如何深度使用
- 史上最全的邊緣計(jì)算應(yīng)用場(chǎng)景
- UCS(優(yōu)勢(shì))—邊緣計(jì)算五大典型應(yīng)用場(chǎng)景
- 一文讀懂邊緣計(jì)算及其應(yīng)用場(chǎng)景
- 帶你走進(jìn) PWA 在業(yè)務(wù)中的實(shí)踐方案
- 現(xiàn)代化 Web 開(kāi)發(fā)實(shí)踐之 PWA
- PWA 技術(shù)在游戲落地中的探索
- 使用 workbox 開(kāi)發(fā) PWA
- PWA實(shí)踐/應(yīng)用(Google Workbox)
- k8s部署MongoDB
- Minio官網(wǎng)
- WasmEdge官網(wǎng)
- Mongoose官網(wǎng)
- 邊緣云上的微服務(wù):使用 WasmEdge 和 Rust 構(gòu)建高性能且安全的應(yīng)用