前言
之前看了雪碧大佬的將 React 渲染到嵌入式液晶屏覺得很有意思,React能被渲染到嵌入式液晶屏,那Vue是不是也可以呢?所以本文我們要做的就是:
如標題所示,就是將Vue渲染到嵌入式液晶屏。這裡使用的液晶屏是0.96 寸大128x64解析度的SSD1306。要將Vue渲染到液晶屏,我們還需要一個橋樑,它必須具備控制液晶屏及執行程式碼的能力。而樹莓派的硬體對接能力和可程式設計性天然就具備這個條件。最後一個問題來了,我們用什麼技術來實現呢?
這裡我選擇了 Node.js。原因:
- Atwood 定律:“任何可以使用 JavaScript 來編寫的應用,最終會由 JavaScript 編寫。” ?
- 驅動硬體我大 Node.js 一行
npm install
走天下。 ?
這個有趣的實踐可拆分為這幾個步驟:
- 在 Node.js 執行 Vue
- 樹莓派連線螢幕晶片
- Node.js 驅動硬體
Talk is cheap,Let's Go!!!
跨端渲染
無論是 基於 React 的 React Native 宣稱的「Learn Once, Write Anywhere」,還是基於 Vue 的 Weex 宣稱的「Write Once, Run Everywhere」口號,本質上強調的都是它們跨端渲染的能力。那什麼是跨端渲染呢?
React: ReactNative Taro ...
Vue: Weex UniApp ...
各種五花八門的前端框架紛紛襲來,前端工程師們紛紛抱怨學不動了~
老闆們看到紛紛笑嘻嘻, App 單,前端分,小程式單,前端吞,PC/H5,前端昏。skr~
這些跨平臺框架原理其實都大同小異,選定 Vue/React 作為 DSL,以這個 DSL 框架為標準在各端分別編譯,在執行時,各端使用各自的渲染引擎(Render Engines)進行渲染,底層渲染引擎中不必關心上層 DSL 的語法和更新策略,只需要處理 JS Framework 中統一定義的節點結構和渲染指令。也正是因為這一渲染層的抽象,使得跨平臺/框架成為了可能。
Vue 和 React 現在都實現了自定義渲染器,下面我們簡單介紹一下:
React Reconciler
React16 採用新的 Reconciler,內部採用了 Fiber 的架構。react-reconciler模組正是基於 v16 的新 Reconciler 實現,它提供了建立 React 自定義渲染器的能力.
const Reconciler = require("react-reconciler");
const HostConfig = {
// You'll need to implement some methods here.
// See below for more information and examples.
};
const MyRenderer = Reconciler(HostConfig);
const RendererPublicAPI = {
render(element, container, callback) {
// Call MyRenderer.updateContainer() to schedule changes on the roots.
// See ReactDOM, React Native, or React ART for practical examples.
},
};
module.exports = RendererPublicAPI;
Vue createRenderer
vue3 提供了createRender API,讓我們建立自定義渲染器。
createRenderer 函式接受兩個泛型引數: HostNode 和 HostElement,對應於宿主環境中的 節點 和 元素 型別。
自定義渲染器可以傳入特定於平臺的型別,如下所示:
import { createRenderer } from 'vue'
const { render, createApp } = createRenderer<Node, Element>({
patchProp,
...nodeOps
})
在 Node.js 上執行 Vue
SFC To JS
<template>
<text x="0" y="0">Hello Vue</text>
<text x="0" y="20">{{ time }}</text>
<text x="0" y="40">Hi SSD3306</text>
</template>
<script>
import { defineComponent, ref, toRefs, onMounted } from "vue";
import dayjs from "dayjs";
export default defineComponent({
setup() {
const time = ref(dayjs().format("hh:mm:ss"));
onMounted(() => {
setInterval(() => {
time.value = dayjs().format("hh:mm:ss");
}, 800);
});
return {
...toRefs({
time,
}),
};
},
});
</script>
要將 Vue 渲染到液晶屏,我們首先需要讓 Vue 能執行在 Node.js 上,但是上面這個 SFC 是沒辦法被 Node.js 識別的,它只是 vue 的程式設計規範,是一種方言。所以我們需要做的是先將 SFC 轉為 js。這裡我使用 Rollup 打包將 SFC 轉為 JS(相關配置這裡就不囉嗦了,貼個傳送門)。到了這一步,Node.js 就能成功執行打包後的 js 程式碼了,這還不夠,這時候 Vue 元件的狀態更新是沒辦法同步到 Node.js 的。
Create Custom Renderer
元件狀態更新我們需要通知 Node.js 更新並渲染液晶屏內容,我們需要建立自定義的"更新策略"。這裡就需要用到了我們前面提到的自定義渲染器:createRenderer API。下面我們簡單介紹下我們相關使用:
// index.js
// 自定義渲染器
import { createApp } from "./renderer.js";
// 元件
import App from "./App.vue";
// 容器
function getContainer() {
// ...
}
// 建立渲染器,將元件掛載到容器上
createApp(App).mount(getContainer());
// renderer.js
import { createRenderer } from "vue";
// 定義渲染器,傳入自定義nodeOps
const render = createRenderer({
// 建立元素
createElement(type) {},
// 插入元素
insert(el, parent) {},
// props更新
patchProp(el, key, preValue, nextValue) {},
// 設定元素文字
setElementText(node, text) {},
// 以下忽略,有興趣的童鞋可自行了解
remove(el) {},
createText(type) {},
parentNode(node) {},
nextSibling(nide) {},
});
export function createApp(root) {
return render.createApp(root);
}
vue 渲染器預設實現了 Web 平臺 DOM 程式設計介面,將 Virtual DOM 渲染為真實 DOM。但是這個渲染器只能執行在瀏覽器中,不具備跨平臺能力。所以我們必須重寫 nodeOps 相關鉤子函式,實現對應宿主環境元素的增刪改查操作。接下來我們定義一個介面卡,來實現相關邏輯。
Adapter
在實現前,我們先來理一下我們要實現的邏輯:
- 建立元素例項 (create)
- 將元素例項插入容器,由容器進行管理 (insert)
- 狀態改變時,通知容器進行更新 (update)
// adapter.js
// 文字元素
export class Text {
constructor(parent) {
// 提供一個父節點用於定址呼叫更新 (前面提到狀態更新由容器進行)
this.parent = parent;
}
// 元素繪製,這裡需要實現文字元素渲染邏輯
draw(text) {
console.log(text);
}
}
// 介面卡
export class Adapter {
constructor() {
// 裝載容器
this.children = [];
}
// 裝載子元素
append(child) {
this.children.push(child);
}
// 元素狀態更新
update(node, text) {
// 找到目標渲染進行繪製
const target = this.children.find((child) => child === node);
target.draw(text);
}
clear() {}
}
// 容器 === 介面卡例項
export function getContainer() {
return new Adapter();
}
好了,基本的介面卡已經完成了,接下來我們來實現渲染器。
Renderer Abstract
import { createRenderer } from "vue";
import { Text } from "./adapter";
let uninitialized = [];
const render = createRenderer({
// 建立元素,例項化Text
createElement(type) {
switch (type) {
case "text":
return new Text();
}
},
// 插入元素,呼叫介面卡方法進行裝載統一管理
insert(el, parent) {
if (el instanceof Text) {
el.parent = parent;
parent.append(el);
uninitialized.map(({ node, text }) => el.parent.update(node, text));
}
return el;
},
// props更新
patchProp(el, key, preValue, nextValue) {
el[key] = nextValue;
},
// 文字更新,重新繪製
setElementText(node, text) {
if (node.parent) {
console.log(text);
node.parent.clear(node);
node.parent.update(node, text);
} else {
uninitialized.push({ node, text });
}
},
remove(el) {},
createText(type) {},
parentNode(node) {},
nextSibling(nide) {},
});
export function createApp(root) {
return render.createApp(root);
}
樹莓派連線螢幕晶片
SSD1306 OLED
OLED,即有機發光二極體( Organic Light Emitting Diode)。是一種液晶螢幕。而 SSD1306 就是一種 OLED 驅動晶片。ssd1306 本身支援多種匯流排驅動方式:6800/8080 並口、SPI 及 IIC 介面方式。這裡我們選擇 IIC 介面方式進行通訊,理由很簡單: 1. 接線簡單方便(兩根線就可以驅動 OLED) 2.輪子好找...缺點就是 IIC 傳輸資料效率太慢了,重新整理率只有 10FPS 不到。而 SPI 重新整理率最大能達到 2200FPS。
硬體接線
IIC 僅需要 4 根線就可以,其中 2 根是電源,另外 2 根是 SDA 和 SCL。我們使用 IIC-1 介面。下面是樹莓派的 GPIO 引腳圖。
注意:請一定以螢幕的實際引腳編號為準。
- 螢幕 VCC 接樹莓派 1 號引腳。- 3.3v 電源
- 螢幕 GND 接樹莓派 9 號引腳。- 地線
- 螢幕 SDA 接樹莓派 3 號引腳。- IIC 通訊中為資料管腳
- 螢幕 SCL 接樹莓派 5 號引腳。- IIC 通訊中為時鐘管腳
樹莓派啟用 I2C
1.安裝工具包
sudo apt-get install -y i2c-tools
2.啟用 I2C
- sudo raspi-config
- 選擇 Interfacing Options
- Enable I2C
3.檢查裝置掛載狀態
i2c-tools 提供的 i2cdetect 命令可以檢視掛載裝置
sudo i2cdetect -y 1
Node.js 驅動硬體
Node.js Lib
我們先來看幾個 Node.js 庫,看完你會不得不感嘆~任何可以使用 JavaScript 來編寫的應用,最....
Johnnt-Five 是一個支援 JavaScript 語言程式設計的機器人和 IOT 開發平臺,基於 Firmata 協議。Firmata 是計算機軟體和微控制器之間的一種通訊協議。使用它,我們可以很簡單的架起樹莓派和螢幕晶片之間的橋樑。
Raspi IO 是一個為 Johnny-Five Node.js 機器人平臺提供的 I/O 外掛,該外掛使 Johnny-Five 能夠控制一個 Raspberry Pi 上的硬體。
5x7 oled 字型庫,將字元轉為 16 進位制編碼,讓 oled 程式能夠識別。用於繪製文字。
? 相容 johnny-five 的 oled 支援庫 (johnny-five 本身並不支援 oled),提供了操作 oled 的 API。
驅動程式實現
// oled.js
const five = require("johnny-five");
const Raspi = require("raspi-io").RaspiIO;
const font = require("oled-font-5x7");
const Oled = require("oled-js");
const OPTS = {
width: 128, // 解析度 0.96寸 ssd1306 128*64
height: 64, // 解析度
address: 0x3c, // 控制輸入地址,ssd1306 預設為0x3c
};
class OledService {
constructor() {
this.oled = null;
}
/**
* 初始化: 建立一個Oled例項
* 建立後,我們就可以通過操作Oled例項來控制螢幕了
*/
init() {
const board = new five.Board({
io: new Raspi(),
});
// 監聽程式退出,關閉螢幕
board.on("exit", () => {
this.oled && this.remove();
});
return new Promise((resolve, reject) => {
board.on("ready", () => {
// Raspberry Pi connect SSD 1306
this.oled = new Oled(board, five, OPTS);
// 開啟螢幕顯示
this.oled.turnOnDisplay();
resolve();
});
});
}
// 繪製文字
drawText({ text, x, y }) {
// 重置游標位置
this.oled.setCursor(+x, +y);
// 繪製文字
this.oled.writeString(font, 2, text, 1, true, 2);
}
clear({ x, y }) {
this.oled.setCursor(+x, +y);
}
// 重新整理螢幕
update() {
this.oled.update();
}
remove() {
// 關閉顯示
this.oled.turnOffDisplay();
this.oled = null;
}
}
export function oledService() {
return new OledService();
}
接下來,我們就可以在介面卡中呼叫 oled 程式渲染螢幕了~
// index.js
import { createApp } from "./renderer.js";
import { getContainer } from "./adapter";
import { oledService } from "./oled";
import App from "./App.vue";
const oledIns = oledService();
oledIns.init().then(() => {
createApp(App).mount(getContainer(oledIns));
});
// adapter.js
export class Text {
constructor(parent) {
this.parent = parent;
}
draw(ints, opts) {
ints.drawText(opts);
ints.update();
}
}
export class Adapter {
constructor(oledIns) {
this.children = [];
this.oled = oledIns;
}
append(child) {
this.children.push(child);
}
update(node, text) {
const target = this.children.find((child) => child === node);
target.draw(this.oled, {
text,
x: node.x,
y: node.y,
});
}
clear(opts) {
this.oled.clear(opts);
}
}
export function getContainer(oledIns) {
return new Adapter(oledIns);
}
到這一步,就可以成功點亮螢幕啦,來看看效果~
效果展示
參考
結語
完整程式碼已上傳到 Github,如果你覺得這個實踐對你有啟發/幫助,點個 star 吧~
Vue 已經成功渲染到嵌入式液晶屏了,那下一步是不是可以考慮接個搖桿寫個貪吃蛇遊戲了~哈哈哈,這很"Javascript"。
"閱讀式"的學習使我犯困,所以我更傾向通過一些有趣的實踐吸收知識。如果你和我一樣愛折騰,歡迎關注~