面向邊緣場(chǎng)景的PWA實(shí)踐

前端 | 面向邊緣場(chǎng)景的PWA實(shí)踐.png

背景

邊緣計(jì)算.png

隨著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)層累奈,如下圖所示:

pwa01.png

其中贬派,應(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)度,如下圖所示:

pwa02.png

源碼分析

端側(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)行prevchildren的派發(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而言汇竭,主要是以mongodbNoSQL數(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é):

  1. 端側(cè):提供高適配性能的應(yīng)用兼容,要注意某些特殊尺寸及渲染引擎剪切造成的功能問(wèn)題
  2. 邊側(cè):渲染場(chǎng)景中對(duì)于離線(xiàn)要求較高档冬,提供高性能的runtime是重中之重,例如:wasmedge(rust+wasm)
  3. 云側(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~

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

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市遍尺,隨后出現(xiàn)的幾起案子截酷,更是在濱河造成了極大的恐慌,老刑警劉巖乾戏,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迂苛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡歧蕉,警方通過(guò)查閱死者的電腦和手機(jī)灾部,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惯退,“玉大人赌髓,你說(shuō)我怎么就攤上這事〈吖颍” “怎么了锁蠕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)懊蒸。 經(jīng)常有香客問(wèn)我荣倾,道長(zhǎng),這世上最難降的妖魔是什么骑丸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任舌仍,我火速辦了婚禮,結(jié)果婚禮上通危,老公的妹妹穿的比我還像新娘铸豁。我一直安慰自己,他們只是感情好菊碟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布节芥。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪头镊。 梳的紋絲不亂的頭發(fā)上蚣驼,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音相艇,去河邊找鬼颖杏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛厂捞,可吹牛的內(nèi)容都是我干的输玷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼靡馁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼欲鹏!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起臭墨,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤赔嚎,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后胧弛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體尤误,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年结缚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了损晤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡红竭,死狀恐怖尤勋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情茵宪,我是刑警寧澤最冰,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站稀火,受9級(jí)特大地震影響暖哨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凰狞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一篇裁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧赡若,春花似錦茴恰、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)伐庭。三九已至粉渠,卻和暖如春分冈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霸株。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工雕沉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人去件。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓坡椒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親尤溜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子倔叼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容