知识准备

乾坤是什么?前端微应用有哪些优势?
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

微前端架构具备以下几个核心价值:

  • 技术栈无关
    主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 独立开发、独立部署
    微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 增量升级
    在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时
    每个微应用之间状态隔离,运行时状态不共享
    具体可以参考:乾坤官网

项目背景

最近在跟总包那边通过乾坤做前端项目集成:主应用采用的是Vue2,需要集成子应用Vue3 。目前由于Vue3 刚出不久,乾坤官网和各大网站上也没有详细介绍的教程,硬着头皮只能自己上了。踩坑的辛酸只有自己知道啊,白天在公司加班到很晚,晚上回家继续搜索,有时候晚上做梦满脑子里都是代码,不过还好经过接近两周的时间终于调通了,通了的那一刻真实无比激动,成就感满满,晚上吃饭给自己加个鸡腿O(∩_∩)O~,废话不多少了,言归正传。

主应用

主应用采用Vue2由总包做,要求子应用的配置:
入口 entry: http://ip:端口/touristflow-app/
触发规则activeRule: /touristflow-default/
后台接口:http://ip:端口/touristflow

子应用

子应用由我来做采用Vue3

  1. vue.config.js
'use strict'
const path = require('path');
function resolve(dir) {return path.join(__dirname, dir)
}
const packageName = 'touristflow-default';
// const packageName = require('./package.json').name;
const port = 9002;
const prod = process.env.NODE_ENV === 'production';const publicPath = prod ? '/touristflow-app/':'/';
module.exports = {publicPath:publicPath,outputDir: 'dist',assetsDir: 'static',productionSourceMap: false,filenameHashing: true,lintOnSave: false,runtimeCompiler: true,devServer: {port: port,hot: true,disableHostCheck: true,overlay: {warnings: false,errors: true},//以上的ip和端口是我们本机的;下面为需要跨域的proxy: {//配置跨域'/touristflow': {target: 'http://localhost:30080/touristflow',//这里后台的地址模拟的;应该填写你们真实的后台接口ws: true,changOrigin: true,//允许跨域pathRewrite: {'^/touristflow': ''//请求的时候使用这个api就可以}}},headers: {'Access-Control-Allow-Origin': '*' // 重要}},css:{loaderOptions:{sass:{prependData:`@import "public/common.scss";`,}}},// 自定义webpack配置configureWebpack: {resolve: {alias: {'@': resolve('src')}},output: {// 把子应用打包成 umd 库格式library: `${packageName}`,libraryTarget: 'umd',jsonpFunction: `webpackJsonp_${packageName}`},}
}

备注:const publicPath = prod ? ‘/touristflow-app/’:‘/’ 这句主要实现子应用的二级目录部署。子应用的访问路径为:http://ip:port/touristflow-app

nginx 的配置为:

server {#客流后台管理listen  30081;#默认端口是80,如果端口没被占用可以不用修改server_name  localhost;#charset koi8-r;#access_log  logs/host.access.log  main;#vue或者React项目的打包后的distlocation /touristflow-app {alias /opt/server/touristflow-app;#需要指向下面的@routertry_files $uri $uri/ /touristflow-app/index.html;index  index.html index.htm;}location /touristflow {proxy_pass http://ip:port/touristflow/;proxy_set_header X-Forwarded-Proto $scheme;proxy_set_header Host    $http_host;proxy_set_header X-Real-IP $remote_addr;}}
output: {// 把子应用打包成 umd 库格式library: `${packageName}`,libraryTarget: 'umd',jsonpFunction: `webpackJsonp_${packageName}`}

packageName 要和主应用定义的保持一致。

  1. main.js
import './public-path';// 加载对乾坤public-path 的配置
import {createApp} from 'vue';
import App from './App.vue';
import store from './store';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import '../public/common.scss';
import "@/vendor/Blob.js";
import "@/vendor/Export2Excel.js";
import router from "./router";
import VueCookies from 'vue-cookies';const isQiankun = window.__POWERED_BY_QIANKUN__;
//用于保存vue实例
let instance = null
console.log("是否isQiankun:" + isQiankun);
console.log(" CommonJS模块化:" + (typeof exports === 'object' && typeof module === 'object') || (typeof define === 'function' && define.amd) || typeof exports === 'object')function render(props = {}) {const {container} = propsinstance = createApp(App);instance.provide('$cookies',VueCookies);instance.use(store).use(router).use(ElementPlus).mount(container ? container.querySelector('#app') : '#app')
}export async function bootstrap() {console.log('[客流] vue app bootstraped');
}export async function mount(props) {console.log('[客流] props from main framework', props);storeTest(props);render(props);
}export async function unmount() {console.log('[客流] unmount')instance.unmount();instance._container.innerHTML = "";
}/*** 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效*/
export async function update(props) {console.log('update props', props);
}// 独立运行时直接挂载应用
if (!isQiankun) {render()
}function storeTest(props) {props.onGlobalStateChange &&props.onGlobalStateChange((value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),true,);props.setGlobalState && props.setGlobalState({ignore: props.name,user: {name: props.name,},});
}

备注:
1.导入public-path.js
2.配置render 函数。
3.导出乾坤会用到的几个生命周期函数。

  1. public-path.js 跟main.js 在同一目录
/* eslint-disable*/
if (window.__POWERED_BY_QIANKUN__) {/* eslint-disable*/console.log("public-path.js 开始加载");__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;console.log("__webpack_public_path__:"+__webpack_public_path__);console.log("public-path.js 加载完成");
}

备注:这里配置主要的作用是,当通过乾坤调用时动态的给webpack的public_path 赋予主应用的根路径。

  1. Router>>index.js 路由配置
import {createRouter, createWebHashHistory} from 'vue-router';
import store from '@/store/index.js';
import Home from "../views/Home.vue";let microPath = "";
const isQiankun = window.__POWERED_BY_QIANKUN__;
console.log("路由 index.js [isQiankun]:" + isQiankun);
if (isQiankun) {microPath = "/touristflow-default";
}const routes = [{path: '/',redirect: '/passengerFlowStatistics'},{path: "/",name: "Home",component: Home,children: [{path: "/passengerFlowStatistics",name: "PassengerFlowStatistics",meta: {title: '客流统计报表'},component: () => import( /* webpackChunkName: "PassengerFlowStatistics" */ "../views/PassengerFlowManagement/PassengerFlowStatistics.vue")}, {path: "/systemConfiguration",name: "SystemConfiguration",meta: {title: '系统配置'},component: () => import( /* webpackChunkName: "SystemConfiguration" */ "../views/SystemManagement/SystemConfiguration.vue")}, {path: "/exhibitionPassengerFlow",name: "ExhibitionPassengerFlow",meta: {title: '展项客流统计'},component: () => import( /* webpackChunkName: "ExhibitionPassengerFlow" */ "../views/PassengerFlowManagement/ExhibitionPassengerFlow.vue")}, {path: "/passengerFlowThreshold",name: "PassengerFlowThreshold",meta: {title: '客流阀值配置'},component: () => import( /* webpackChunkName: "PassengerFlowThreshold" */ "../views/SystemManagement/PassengerFlowThreshold.vue")}, {path: "/systemLog",name: "SystemLog",meta: {title: '系统日志'},component: () => import( /* webpackChunkName: "PassengerFlowThreshold" */ "../views/SystemManagement/SystemLog.vue")}, {path: "/deviceManagement",name: "DeviceManagement",meta: {title: '设备管理'},component: () => import( /* webpackChunkName: "PassengerFlowThreshold" */ "../views/SystemManagement/DeviceManagement.vue")}]},{path: "/login",name: "Login",meta: {title: '登录'},component: () => import( /* webpackChunkName: "login" */ "../views/Login.vue")}
];
const routes_qiankun = [{path: microPath + '/',redirect: microPath + '/passengerFlowStatistics'},{path: microPath + "/passengerFlowStatistics",name: "PassengerFlowStatistics",meta: {title: '客流统计报表'},component: () => import(  "../views/PassengerFlowManagement/PassengerFlowStatistics.vue")}, {path: microPath + "/systemConfiguration",name: "SystemConfiguration",meta: {title: '系统配置'},component: () => import( /* webpackChunkName: "SystemConfiguration" */ "../views/SystemManagement/SystemConfiguration.vue")}, {path: microPath + "/exhibitionPassengerFlow",name: "ExhibitionPassengerFlow",meta: {title: '展项客流统计'},component: () => import( /* webpackChunkName: "ExhibitionPassengerFlow" */ "../views/PassengerFlowManagement/ExhibitionPassengerFlow.vue")}, {path: microPath + "/passengerFlowThreshold",name: "PassengerFlowThreshold",meta: {title: '客流阀值配置'},component: () => import( /* webpackChunkName: "PassengerFlowThreshold" */ "../views/SystemManagement/PassengerFlowThreshold.vue")}, {path: microPath + "/systemLog",name: "SystemLog",meta: {title: '系统日志'},component: () => import( /* webpackChunkName: "PassengerFlowThreshold" */ "../views/SystemManagement/SystemLog.vue")}, {path: microPath + "/deviceManagement",name: "DeviceManagement",meta: {title: '设备管理'},component: () => import( /* webpackChunkName: "PassengerFlowThreshold" */ "../views/SystemManagement/DeviceManagement.vue")},{path: "/login",name: "Login",meta: {title: '登录'},component: () => import( /* webpackChunkName: "login" */ "../views/Login.vue")}
];const router = createRouter({history: createWebHashHistory(isQiankun ? '/touristflow-default' : '/'),routes: isQiankun ? routes_qiankun : routes
})
if (!isQiankun) {router.beforeEach((to, from, next) => {// 跳转到非登录页面的其他页面时需先判断是否登录let IS_LOGIN = store.getters.getToken;if (to.path !== microPath + '/login') {// 未登录则跳转到登录页面if (IS_LOGIN) {next()} else {next(microPath + '/login')} // path就是配置路由文件里的路由path(属性值一定要相同)} else {// 已登录则跳转到首页if (IS_LOGIN) next(microPath + '/passengerFlowStatistics')else next()}})
}export default router;

注意:(1)子应用的路由要跟主应用的路由方式保持一直,主应用用的hash,子应用也要用hash方式。

const router = createRouter({history: createWebHashHistory(isQiankun ? '/touristflow-default' : '/'),routes: isQiankun ? routes_qiankun : routes
})

创建路由时,base要取/touristflow-default,跟主应用里配置的触发规则保持一致。
(2)乾坤路由菜单前都要加上 /touristflow-default
5. request.js

import axios from 'axios';
import {ElMessageBox} from 'element-plus'
import store from '@/store/index.js';
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'const microPath = "/touristflow-default";
const isQiankun = window.__POWERED_BY_QIANKUN__;
console.log("requst isQiankun:" + isQiankun);
// 创建一个axios实例
const service = axios.create({headers: {'content-type': 'application/json;charset=UTF-8',},// baseURL: process.env.VUE_APP_BASE_API,baseURL: process.env.NODE_ENV === 'production' ? window.configObj.httpUrl : window.configObj.httpUrl,changeOrigin: true, //是否跨域timeout: 60000
})if (!isQiankun) {console.log("request 走 非乾坤")// 添加请求拦截器service.interceptors.request.use(config => {config.headers['X-Token'] = store.getters.getToken;// config.headers['X-Token'] = 'd30d134a-3627-4941-a06b-86bec6119da5';return config;}, error => {// 请求错误时做些事return Promise.reject(error);});
// 添加响应拦截器service.interceptors.response.use(response => {NProgress.start();if (response.data.code == 1000) {ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录','系统提示',{confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning',}).then(() => {store.commit("setToken", '');location.href = '/#' + microPath + '/login';}).catch(() => {})}if (response.status == 200) {const res = response.data;// 如果返回的状态不是200 就主动报错NProgress.done()return res;} else {NProgress.done()return Promise.reject("服务器请求错误")}}, error => {return Promise.reject(error); // 返回接口返回的错误信息})
} else {console.log("request 走 乾坤")// 添加请求拦截器service.interceptors.request.use(config => {if (store.getters.getToken){config.headers['X-CSRF-TOKEN'] = store.getters.getToken;}return config;}, error => {// 请求错误时做些事return Promise.reject(error);});// 添加响应拦截器service.interceptors.response.use(response => {if (response.status == 200) {const res = response.data;return res;} else {return Promise.reject("服务器请求错误")}}, error => {return Promise.reject(error); // 返回接口返回的错误信息})}export default service

后台配置

(1)本地引入三个jar 包:

pom.xml

<!--引入浪潮本地jar包--><dependency><groupId>com.inspur.msy</groupId><artifactId>green-channel-core</artifactId><version>1.3.6</version><scope>system</scope><systemPath>${pom.basedir}/lib/green-channel-core-1.3.6.jar</systemPath></dependency><dependency><groupId>com.inspur.msy</groupId><artifactId>green-channel-rcv</artifactId><version>1.3.6</version><scope>system</scope><systemPath>${pom.basedir}/lib/green-channel-rcv-1.3.6.jar</systemPath></dependency><dependency><groupId>bcprov</groupId><artifactId>bcprovjdk15to18</artifactId><version>1.69</version><scope>system</scope><systemPath>${pom.basedir}/lib/bcprov-jdk15to18-1.69.jar</systemPath></dependency>

(2) 添加 MyGreenChannelAuthorizeSpringFilter

package com.dechnic.psas.shiro;import com.inspur.msy.component.greenchannel.web.filter.GreenChannelAuthorizeSpringFilter;
import com.inspur.msy.component.greenchannel.web.utils.AuthTokenCheckUtils;
import lombok.Data;
import lombok.extern.java.Log;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @description: 继承 过滤器重写过滤doFilterInternal业务方法* @author: maty* @time: 2022/2/16 10:14*/
@Log
public class MyGreenChannelAuthorizeSpringFilter extends GreenChannelAuthorizeSpringFilter {public MyGreenChannelAuthorizeSpringFilter() {super();}/***  解析 浪潮Token  直接放开* @param request* @param response* @param filterChain* @throws ServletException* @throws IOException*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {log.info("MyGreenChannel doFilter 开始");AuthTokenCheckUtils.parseAndVerify(request);log.info("MyGreenChannel doFilter 结束");filterChain.doFilter(request, response);}
}

(3)登陆过滤器里去掉对“/touristflow” 的请求url的拦截

(4)白名单里添加对正式服务器域名的配置ip

踩坑碰到的问题

问题一:
Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry

参考官网地址1
踩坑主要点:通过参考官网和从各大网站搜索资料,不停的反复的尝试,就跟爱迪生发明电灯一样,失败了一次又一次,每一次满怀信心部署上去,想去见证奇迹时都失败了,可是我并没有放弃,当有一天老大问题调的怎么样时,我的回答是还没有调好,我不甘心这样放弃,甚至差一点换成auth2 的方式。直到有一天我在看前端知识时,无意间看到Vue3的新特性 treeshaking,我才彻底明白原来给乾坤准备的那几个函数被treeshaking掉了。然后我又从webpack 官网里了解到 将package.json 中 添加 “sideEffects”: true 就可以了
完整的package.json 文件:

{"name": "touristflow-default","version": "0.1.0","private": true,"sideEffects": true,"scripts": {"serve": "vue-cli-service serve","build:prod": "vue-cli-service build","build:stage": "vue-cli-service build --mode staging"},"dependencies": {"axios": "^0.24.0","core-js": "^3.6.5","echarts": "^5.2.2","element-plus": "^1.3.0-beta.1","file-saver": "^2.0.5","nprogress": "^0.2.0","qs": "^6.10.3","vue": "^3.0.0","vue-cookies": "^1.7.4","vue-router": "^4.0.0-0","vuex": "^4.0.0-0","vuex-persistedstate": "^4.1.0","xlsx": "^0.17.4"},"devDependencies": {"@vue/cli-plugin-babel": "~4.5.0","@vue/cli-plugin-router": "~4.5.0","@vue/cli-plugin-vuex": "~4.5.0","@vue/cli-service": "~4.5.0","@vue/compiler-sfc": "^3.0.0","node-sass": "^4.12.0","sass-loader": "^8.0.2","script-loader": "^0.7.2","webpack": "^4.7.0","webpack-cli": "^4.9.2"}
}

问题二:后台接口都能调通,但是前端不渲染
前台请求后台我用的是axios 的工具,
这个问题主要是request.js 里面只定义了请求拦截,没有定义response 导致的,完整代码如下:
request.js

import axios from 'axios';
import {ElMessageBox} from 'element-plus'
import store from '@/store/index.js';
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'const microPath = "/touristflow-default";
const isQiankun = window.__POWERED_BY_QIANKUN__;
console.log("requst isQiankun:" + isQiankun);
// 创建一个axios实例
const service = axios.create({headers: {'content-type': 'application/json;charset=UTF-8',},// baseURL: process.env.VUE_APP_BASE_API,baseURL: process.env.NODE_ENV === 'production' ? window.configObj.httpUrl : window.configObj.httpUrl,changeOrigin: true, //是否跨域timeout: 60000
})if (!isQiankun) {console.log("request 走 非乾坤")// 添加请求拦截器service.interceptors.request.use(config => {config.headers['X-Token'] = store.getters.getToken;// config.headers['X-Token'] = 'd30d134a-3627-4941-a06b-86bec6119da5';return config;}, error => {// 请求错误时做些事return Promise.reject(error);});
// 添加响应拦截器service.interceptors.response.use(response => {NProgress.start();if (response.data.code == 1000) {ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录','系统提示',{confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning',}).then(() => {store.commit("setToken", '');location.href = '/#' + microPath + '/login';}).catch(() => {})}if (response.status == 200) {const res = response.data;// 如果返回的状态不是200 就主动报错NProgress.done()return res;} else {NProgress.done()return Promise.reject("服务器请求错误")}}, error => {return Promise.reject(error); // 返回接口返回的错误信息})
} else {console.log("request 走 乾坤")// 添加请求拦截器service.interceptors.request.use(config => {if (store.getters.getToken){config.headers['X-CSRF-TOKEN'] = store.getters.getToken;}return config;}, error => {// 请求错误时做些事return Promise.reject(error);});// 添加响应拦截器service.interceptors.response.use(response => {if (response.status == 200) {const res = response.data;return res;} else {return Promise.reject("服务器请求错误")}}, error => {return Promise.reject(error); // 返回接口返回的错误信息})}export default service

问题三:日期选择框样式在子应用里怎么调都不起作用

主要原因是主应用虽然和子应用通过沙箱隔离,起了一部分作用,但是像有些组件 “日期选择框” 则之前挂到了主应用的body 下,不论你怎么修改都不起作用

参考乾坤主应用和子应用的样式隔离

最后通过在主应用下修改样式解决。

总结

配置步骤:

  1. 子应用的二级目录部署及访问要成功 http://ip:port/touristflow-app
  2. 主应用的的entry: http://ip:port/touristflow-app/ 最后的“/” 不用忘记写。
  3. 主应用的activeRule “/touristflow-default” 要跟子应用里的路由 base 、路由菜单前缀对应。
  4. Vue3 的treeshaking 会导致main.js 里的乾坤钩子函数被treeshaking 掉,需要禁用掉treeshaking 功能,在package.json 里添加 “sideEffects":true
  5. 后台接口配置成功
  6. 前后台联调成功。
  7. 后台接口能请求成功,但是不渲染,request.js里response 未配置。
  8. 主应用启用沙箱隔离后,子应用里日期选择框样式不加载,需要从主应用里单独配置。

乾坤主应用Vue2 集成子应用Vue3艰苦踩坑历程相关推荐

  1. qiankun + vue3使用踩坑记录

    qiankun + vue3使用踩坑记录 1.主应用 vue create qiankun-base npm install vue-router@4 npm i qiankun -S 在main.j ...

  2. VUE 集成富文本编辑器及踩坑记录

    一.查看 vue版本和vue cli版本 首先要知道自己所使用的VUE 版本和 脚手架(VUE CLI)版本 这样自己无论是在百度的时候还是选择富文本编辑器对应版本的时候都方便很多 1.查看vue 版 ...

  3. Android安卓集成融云推送踩坑

    此文档单单接入推送,暂时没有用IM或其他 如果您觉得可以帮助到您,麻烦帮我点个赞. -------------------------------- 写在前面,为什么要用这个,我并不想,实际接入过程中 ...

  4. wepy 父调用子组件方法_wepy踩坑小记(一)

    前言 最近在用wepy,相对于之前用的taro,由于自己没有怎么写过vue,虽然也是很容易就上手了,但是有擦坑啦. 复选框值绑定 在wepy官方文档是这样说的: 用法与 Vue 一致,参考 vue官方 ...

  5. Android集成阿里云旺即时通讯踩坑历程

    下载云旺的demo,将demo中的OneSDK直接拷贝,作为Moudle进行依赖,具体操作就不说了,OneSDK是最新的,一定不要进行修改, 进行依赖后,可能会遇到buildToolsVersion ...

  6. neo4j springboot 日志_Springboot2.3集成neo4j的过程和踩坑记

    最近有个需求是要使用neo4j这个数据库,看官方的介绍是个图形数据库,官方没有看到和springboot整合的文档(可能是我没找着),那就自己动手吧- 最近有个需求是要使用neo4j这个数据库,看官方 ...

  7. Vue2.0项目中使用sass(踩坑之路)

    今天用2.0创建项目的时候,使用scss一直不成功,一直报错------ 记录一下,防止下次踩坑 1.安装依赖包 vue的webpack项目中需要安装上node-sass.sass-loader和st ...

  8. Vue3+Naive踩坑

    NaiveUI-useMessage报错 NaiveUI很多组件虽然需要手动操作,但根据个人体验,认为该UI库性能方面的确比较时髦,无愧于新生代的UI组件库. 解决办法 单独新建Message.vue ...

  9. QT小例子GUI(主)线程与子线程之间的通信

    QT小例子GUI(主)线程与子线程之间的通信 在主线程上,可以控制子线程启动,停止,清零 如果子线程启动的话,每一秒钟会向主线程发送一个数字,让主线程更新界面上的数字. #ifndef TQT_H_ ...

最新文章

  1. Carrier frequency 和 EARFCN的关系
  2. 【Android应用开发】Android Studio 错误集锦 -- 将所有的 AS 错误集合到本文
  3. python之路_文件操作解析
  4. boost::math模块使用二项分布复制 NAG 库调用的测试程序
  5. 如何在linux环境下安装yaf
  6. redhat 6 配置 yum 源
  7. 如何在Github网页端处理不同分支之间的冲突
  8. Expo大作战(十二)--expo中的自定义样式Custom font,以及expo中的路由RouteNavigation
  9. oo面向对象第一单元总结
  10. 管理任务执行-如何排任务优先级
  11. 服务器虚拟化svc,SVC的虚拟化变革
  12. PAT 1044. 火星数字
  13. 4G内存为什么会少800M
  14. MFC 视频播放器实现局部放大功能
  15. VISA 通信command总结
  16. 有参构造方法的作用和无参构造方法的作用
  17. 元组与字典——python
  18. MAVEN(一)——配置以及plugins
  19. redis的sentinel mode
  20. 中路由怎么配置_【国土语录 第64期】茶队点金大鱼后发生了什么;否定Fy关于茶队中路看法...

热门文章

  1. 基于Blinker ESP8266 远程电压电流、功率计、温湿度计、ADC接口测温、温控风扇、低电压保护、低压报警功能。用来监测我的太阳能充电。SSD1306可轮番显示电压信息和温度信息。APP图表
  2. 电工配线端接实训装置-楼宇智能实训室
  3. 网吧服务器硬盘压力百分之百,网吧无盘服务器硬盘的测试方法
  4. scratch案例——九九乘法
  5. IE的F12开发人员工具不显示问题
  6. typedef define
  7. oracle 分区表好处,Oracle表分区的相关概念及其优点(转)
  8. “疫情”防控时期大势所趋,智慧社区尽显“智慧”迎来新的发展热潮
  9. Android4.3 Bluetooth基本介绍
  10. 5.4 Penalty-Based Local Search Algorithms基于惩罚的局部搜索算法