以前看了雪碧大佬的將 React 渲染到嵌入式液晶屏以爲頗有意思,React能被渲染到嵌入式液晶屏,那Vue是否是也能夠呢?因此本文咱們要作的就是: html
如標題所示,就是將Vue渲染到嵌入式液晶屏。這裏使用的液晶屏是0.96 寸大128x64分辨率的SSD1306。要將Vue渲染到液晶屏,咱們還須要一個橋樑,它必須具有控制液晶屏及運行代碼的能力。而樹莓派的硬件對接能力和可編程性自然就具有這個條件。最後一個問題來了,咱們用什麼技術來實現呢?前端
這裏我選擇了 Node.js。緣由:vue
npm install
走天下。 🐶
這個有趣的實踐可拆分爲這幾個步驟:node
Talk is cheap,Let's Go!!!react
不管是 基於 React 的 React Native 宣稱的「Learn Once, Write Anywhere」,仍是基於 Vue 的 Weex 宣稱的「Write Once, Run Everywhere」口號,本質上強調的都是它們跨端渲染的能力。那什麼是跨端渲染呢?git
React: ReactNative Taro ...github
Vue: Weex UniApp ...web
各類五花八門的前端框架紛紛襲來,前端工程師們紛紛抱怨學不動了~npm
老闆們看到紛紛笑嘻嘻, App 單,前端分,小程序單,前端吞,PC/H5,前端昏。skr~編程
這些跨平臺框架原理其實都大同小異,選定 Vue/React 做爲 DSL,以這個 DSL 框架爲標準在各端分別編譯,在運行時,各端使用各自的渲染引擎(Render Engines)進行渲染,底層渲染引擎中沒必要關心上層 DSL 的語法和更新策略,只須要處理 JS Framework 中統必定義的節點結構和渲染指令。也正是由於這一渲染層的抽象,使得跨平臺/框架成爲了可能。
Vue 和 React 如今都實現了自定義渲染器,下面咱們簡單介紹一下:
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;
vue3 提供了createRender API,讓咱們建立自定義渲染器。
createRenderer 函數接受兩個泛型參數: HostNode 和 HostElement,對應於宿主環境中的 節點 和 元素 類型。
自定義渲染器能夠傳入特定於平臺的類型,以下所示:
import { createRenderer } from 'vue' const { render, createApp } = createRenderer<Node, Element>({ patchProp, ...nodeOps })
<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 的。
組件狀態更新咱們須要通知 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.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(); }
好了,基本的適配器已經完成了,接下來咱們來實現渲染器。
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); }
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 引腳圖。
注意:請必定以屏幕的實際引腳編號爲準。
sudo apt-get install -y i2c-tools
i2c-tools 提供的 i2cdetect 命令能夠查看掛載設備
sudo i2cdetect -y 1
咱們先來看幾個 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"。
"閱讀式"的學習使我犯困,因此我更傾向經過一些有趣的實踐吸取知識。若是你和我同樣愛折騰,歡迎關注~