背景
隨着 OpenHarmony
組件開發大賽結果公布,我們的團隊成員被告知獲得了二等獎,在開心之余也想將我們這段時間寶貴的開發經驗寫下來與大家分享,當我們看到參賽通知的時候已經是 9 月中旬的時候,此時已經是作品可以提交的時間了,參考了一些其他作品發現,基於 Canvas
開發的組件目前還沒有,那我們就開始計划寫一個基於 Canvas
和通用組件一起開發的組件,在這之前由於並沒有開發過 OpenHarmony
應用,我們團隊成員都沒有相關的經驗,大家從零開始在摸索,我們首先分工合作,有的成員負責去下載 IDE
和調試設備,有的成員負責研究和閱讀官方文檔。先附上源代碼:
https://github.com/Wscats/openharmony-sheet
配置
在閱讀完官方文檔之后,我們成員分別在自己本地電腦和設備上做了以下的環境配置:
- 下載並安裝好 DevEco Studio 2.1 Release 及以上版本
- 獲取 OpenHarmony SDK 包並解壓
- 配置
OpenHarmony SDK
在 DevEco
主界面,點擊工具欄中的 File
> Settings
> Appearance & Behavior
> System Settings
> HarmonyOS SDK
界面,點擊 HarmonyOS SDK Location
加載 SDK
:

然后一直點擊 Next
和 Finish
完成環境配置。
- 安裝額外包,進入
OpenHarmony-SDK-2.0-Canary/js/2.2.0.0/build-tools/ace-loader
目錄,然后在該目錄下運行命令行工具,分別執行如下命令,直至安裝完成
npm cache clean -f
npm install
- 下載 OpenHarmonyJSDemos 項目工程,將工程導入
DevEco Studio
- 申請並配置證書,注意
OpenHarmony
與HarmonyOS
的證書不通用,所以需要額外進行申請 - 進行編譯構建,生成一個
HAP
應用安裝包,生成HAP
應用安裝包,安裝到OpenHarmony
開發板 - 安裝運行后,在開發板屏幕上點擊應用圖標即可打開應用,即可在設備上查看應用示例運行效果,以及進行相關調試
- 除了使用真機調試,我們還可以使用遠程調試和本地的
Previewer
調試,雖然非常相當方便,但實際表現肯定和真機是有稍微差異的

前言
在實現 Canvas
應用之前,我們經過一些商量和討論,首先是希望能借助這一次開發提升對 OpenHarmony
的理解,方便后續業務的支持,其次我們團隊成員也是希望能拿到比較好的名次和獎勵,我們注意到比賽的評分由評委打分,滿分為 100 分,這里會根據作品的創意性、實用性、用戶體驗、代碼規范等四個維度點評打分,Canvas
的應用首先實現成本會比普通應用難度稍微大點,並且不好調試,在創意性和實用性上我們優勢不大,因為大部分前端開發者接觸到的 Canvas
應用都是游戲相關的,所以這條路注定是會相對艱難的,用戶體驗也是一個很大的難點,我們真機測試發現 Canvas
的表現也不是很好,比原生一些組件的體驗差很多,對於團隊成員的代碼質量是有信心的,但是代碼規范的評分比重卻是最少的,所以在立項的時候我們有比較大的分歧。
評選維度 | 說明 | 分值 |
---|---|---|
創意性 | 作品的創新程度 | 30% |
實用性 | 作品在應用場景中的實際應用程度 | 30% |
用戶體驗 | 用戶體驗價值,用戶能夠輕松使用組件,並獲得良好體驗感 | 25% |
代碼規范 | 代碼的質量,美觀度,是否符合規范 | 15% |
計划
正因為由上面總總的疑慮,我們先制定了三個計划和一個目標:
- 使用基礎組件和容器組件等實現通用組件 - OpenHarmonyGallery
- 使用畫布組件實現
Canvas
游戲 - OpenHarmonyFlappyBird - 使用基礎組件,容器組件和畫布組件實現
Canvas
渲染引擎 - OpenHarmonySheet
渲染引擎是我們最終目標,雖然難度偏大,但我們團隊成員決定分開三步來實現該目標,首先至少先學會使用基礎組件和容器組件,然后再學會使用畫布組件,最后綜合這些經驗實現一個渲染引擎。
初體驗
我們首先實現了一個通用的畫廊組件來作為練手項目,它主要使用了四個基礎組件和容器組件:
我們放置一個按鈕來觸發 showGallery
方法,該方法控制 panel
彈出式組件的顯示和隱藏,這里的 div
和 button
標簽就是 hml
內置的組件,跟我們平常寫 html
很相似,它支持我們大部分的常規屬性如 id
,class
和 type
等,方便我們用來設置組件基本標識和外觀特征顯示。
<div class="btn-div">
<button type="capsule" value="Click Here" onclick="showGallery"></button>
</div>
然后我們 panel
組件中放置可變更的畫廊內容展示窗口,並讓 mode
和 src
變成可設置的變量,這樣畫廊組件就能根據模式讓畫廊組件顯示不同的形態,根據傳入的圖片地址顯示不同的圖片內容,這里的語法跟微信小程序很和 Vue
框架相似,都可以使用 Mustache
語法來控制屬性值。
<panel
id="gallery"
class="gallery"
type="foldable"
mode="{{modeFlag}}}"
onsizechange="changeMode"
>
<div class="panel-div" onclick="closeGallery">
<image class="panel-image" onclick="closeGallery" src="{{galleryUrl}}}"></image>
<button
class="panel-circle"
onclick="closeGallery"
type="circle"
icon="/common/images/close.svg"
></button>
</div>
</panel>
實現完視圖和布局之后,我們就可以在同級目錄下 index.js
中補充畫廊組件的邏輯,由於支持 ES6
語法,我們寫的也很舒服很高效,這里的 data 是畫廊組件的數據模型,類型可以是對象或者函數,如果類型是函數,返回值必須是對象,注意屬性名不能以 $
或 _
開頭,不要使用保留字,我們在這里給 modeFlag
和 galleryUrl
設置默認值。
export default {
data: {
modeFlag: "full",
galleryUrl:
"https://pic1.zhimg.com/v2-3be05963f5f3753a8cb75b6692154d4a_1440w.jpg?source=172ae18b",
},
};
而顯示和隱藏邏輯比較簡單,只需要獲取 panel
的節點,然后觸發 show
或者 hide
方法即可,當然除了該方法,我們還可以使用 渲染屬性
來實現:
for
根據設置的數據列表,展開當前元素if
根據設置的boolean
值,添加或移除當前元素show
根據設置的boolean
值,顯示或隱藏當前元素
showGallery(e) {
this.$element('gallery').show()
},
closeGallery(e) {
if(e.target.type==='image') return
this.$element('gallery').close()
},
我們還可以在同級目錄下在 index.css
補充組件的樣式,可以讓我們的畫廊呈現更好的效果,這里動畫樣式還支持動態的旋轉、平移、縮放和漸變效果,均可在 style
或 css
中設置。
.panel-div {
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
}
整體實現的效果如下圖所示,效果簡單粗暴,寫完了這個 DEMO
之后,我們團隊成員對 OpenHarmony
的基礎組件運用有了最基本的了解:



進階
雖然上面我們掌握了最基礎的組件使用,但我們還是沒使用到 Canvas
畫布組件,所以我們繼續翻閱官方文檔,發現 OpenHarmony
是提供了齊全的畫布接口:

我們使用經典 FlappyBird
游戲作為我們畫布組件的第一次嘗試。
收集素材
首先我們先准備好游戲的圖片和動畫素材:
素材 | |
---|---|
背景 | ![]() |
小鳥 | ![]() |
地面 | ![]() |
水管 | ![]() ![]() |
然后我們准備好畫布,設置好高度和寬度,並監聽畫布按下的方法 ontouchend
。
<div class="container">
<canvas ref="canvas" style="width: 280px; height: 512px;" ontouchend="moveUp"></canvas>
</div>
數據初始化
准備好畫布之后,我們就需要初始化游戲的初始數據,核心的主要涉及幾個:
el | 畫布元素 |
---|---|
gap | 管道間距 |
score | 得分 |
bX | 小鳥 X 軸坐標 |
bY | 小鳥 Y 軸坐標 |
gravity | 重力指數 |
pipe | 管道數據 |
birdHeight | 小鳥高度 |
birdWidth | 小鳥寬度 |
pipeNorthHeight | 上側管道高度 |
pipeNorthWidth | 下側管道高度 |
cvsHeight | 畫布高度 |
cvsWidth | 畫布寬度 |
fgHeight | 地面高度 |
fgWidth | 地面寬度 |
實現這個游戲之前,我們不但需要掌握基礎的組件,還需要了解一部分生命周期,OpenHarmony
有兩種生命周期,分別是應用生命周期和頁面生命周期,我們這里第一次運用到生命周期 onShow
,它是在頁面打開的時候觸發,並且應用處於前台時觸發,我們需要它在開始的時候幫我們初始化一些關鍵數據,獲取畫布的節點,保存畫布的上下文作用域 ctx
,清空管道數據和觸發游戲幀繪制。
onShow() {
this.el = this.$refs.canvas;
this.ctx = this.el.getContext('2d');
this.pipe[0] = {
x: this.cvsWidth,
y: 0,
};
requestAnimationFrame(this.draw);
},
這里的 this.draw
方法是整個游戲的核心邏輯,涉及小鳥的飛行動畫,運動軌跡,邊界處理和得分計算。
首先我們從畫布的左上角 X
和 Y
軸的起始位置開始繪制游戲的背景。
const ctx = this.ctx;
ctx.drawImage(this.bg, 0, 0);
然后我們繪制小鳥飛行過程中出現在天空和地面的管道,這里需要計算天空的管道位置,上管道的位置需要用兩個管道預設的間距加上下管道的高度的出來的,當位置計算出來后,只需要配合定時器或者 requestAnimationFrame
來實時更新管道和鳥的位置就能讓用戶感知游戲動態畫面的效果,這里我使用了 requestAnimationFrame
請求動畫幀體驗會更好,但是它從 API Version 6
才開始支持,並且不需要你導入,所以讀者需要留意你的 SDK
是否是比較新的版本。
for (let i = 0; i < this.pipe.length; i++) {
this.constant = this.pipeNorthHeight + this.gap;
ctx.drawImage(this.pipeNorth, this.pipe[i].x, this.pipe[i].y);
ctx.drawImage(this.pipeSouth, this.pipe[i].x, this.pipe[i].y + this.constant);
this.pipe[i].x--;
}
碰撞檢測
這里我們使用一個條件判斷來做邊界處理即碰撞檢測,也就是小鳥如果碰到地面,碰到天空的管道或者地面的管道就會使所有動畫停止,即游戲結束,如果游戲結束則結算成績,並且使用 OpenHarmony
內置的彈窗提醒玩家是否需要重新開始新的游戲。
if (
(this.bX + this.birdWidth >= this.pipe[i].x &&
this.bX <= this.pipe[i].x + this.pipeNorthWidth &&
(this.bY <= this.pipe[i].y + this.pipeNorthHeight ||
this.bY + this.birdHeight >= this.pipe[i].y + this.constant)) ||
this.bY + this.birdHeight >= this.cvsHeight - this.fgHeight
) {
prompt.showDialog({
buttons: [{ text: "重來一次" }],
success: (data) => this.restart(),
});
clearInterval(this.interval);
}
當處理完邊界,我們還需要處理當小鳥一直飛下去的時候,要不斷創建新的管道,回收舊管道算得分,這個邏輯也相當之簡單,本質上也算是一種碰撞檢測,當管道位置變更到畫面左側 X
軸為 5
的位置,即小鳥已經安全通過,則成功得分,當最新的管道變更到畫面右側 X
軸為 125
的位置,即小鳥將要飛躍的下一個管道,則提前創建好下一個新的管道,如果小鳥飛躍的距離比較長,我們還需要考慮優化管道數組,不能讓數組無限制的增長下去,我們還可以優化,所以當舊管道已經完全消失在畫面中的時候,我們可以考慮把舊管道的數據從數組中刪除。
if (this.pipe[i].x == 125) {
this.pipe.push({
x: this.cvsWidth,
y: Math.floor(Math.random() * this.pipeNorthHeight) - this.pipeNorthHeight,
});
}
if (this.pipe[i].x == 5) {
this.score++;
}
上面所有的這些邏輯本質都是繪制管道的動畫,我們配合偏移量和重力因素,很輕易的就能繪制出小鳥的飛行軌跡,我們這里還順便把得分繪制到屏幕的左下角,以便實時展示玩家得分。
ctx.drawImage(this.fg, 0, this.cvsHeight - this.fgHeight);
ctx.drawImage(this.bird, this.bX, this.bY);
this.bY += this.gravity;
ctx.fillStyle = "#000";
ctx.font = "20px Verdana";
ctx.fillText("Score : " + this.score, 10, this.cvsHeight - 20);
操作和計分
而我們玩家參與整個游戲只需要一個操作,就是用手指點擊屏幕,盡量讓小鳥安全飛過管道之間,所以我們需要監聽屏幕的點擊事件,本質也就是畫布的點擊事件,當用戶點擊一下的時候,我們就讓小鳥往上方移動一點距離。
moveUp() {
this.bY -= 25;
},
而重置游戲跟初始化的邏輯很相似,只要把玩家得分,鳥的位置和管道的數據全部恢復到默認狀態即可:
restart() {
this.pipe = [];
this.pipe[0] = {
x: this.cvsWidth,
y: 0,
};
this.constant = 0;
this.score = 0;
this.bY = 150;
},
封裝組件


由於比賽要求我們是實現一個通用組件,所以在案例 2 中我們希望更進一步,嘗試把這個把這個游戲封裝成一個通用的組件,查閱官方文檔發現實現起來很簡單,詳情在自定義組件,所謂自定義組件就是是用戶根據業務需求,將已有的組件組合,封裝成的新組件,可以在工程中多次調用,從而提高代碼的可讀性。綜上所述,我們只需要使用 <element>
組件把我們剛才實現的組件引入到宿主頁面即可。
<element name="Flappy" src="./flappy//pages//index/index.hml"></element>
<div class="container">
<Flappy></Flappy>
</div>
終極挑戰
有了前面兩個案例的積累,我們團隊對 OpenHarmony
開發有了更清晰的認識,就要進入最后激動人心的終極挑戰了,我們要完整的移植一個 Canvas
引擎,我們一開始考慮的是實現一個游戲引擎,但考慮到比賽剩余時間並不足夠,並且游戲引擎的實用性和創意性不利於展現,所以經過我們團隊綜合考量,我們最終決定實現一個文檔表格渲染引擎。
思考
可能有人疑問為什么會選擇移植一個文檔渲染引擎,這里想起外網知乎有過類似的討論,中國要用多久才能研發出類似 Excel,且功能涵蓋 Excel 95% 功能的替代軟件?,這條路很崎嶇很艱難,引用最高贊一些大 V 的回答吧:
微軟輪子哥
:做不出來的,那么多東西,要把需求文檔寫好都得好幾年。
微軟的 Belleve:各位程序員可以試試先實現下 recalc(根據公式更新單元格數值),就知道難度了,文檔項目作為國內最復雜的 C++ 項目絕非浪得虛名。微軟的妖怪弟弟
:作為 Excel 的工程師,哥認真的答一個,不能,因為我們隔壁組已經嘗試過了,兩年大概覆蓋了 40%上下吧。IBM 的 Caspar Cui
:如果是開發常用的 Excel 功能的話, WPS 已經是很好的替代品了。而且微軟和金山也有交叉授權。但是說要提到 95%功能的 Excel 已經做到了這種事兒。。。還是有點小瞧 Excel 了。就一個幫助文檔量,WPS 也得多努力。中科大的 Sixue
:假如微軟腦抽,把 Excel 源碼弄丟了,不可恢復了。那就是世界末日,大家一起完蛋。哪怕微軟把 Excel 團隊原班人馬找回來,離職的反聘,英年早逝的復活,然后重新開發一個 Excel。他也沒辦法保證把 Excel 的功能恢復到 95%,沒法保證 95%的 Excel 文件正常打開。Bbcallen
:不可能的,微軟自己都做不到。
不管任何人怎么說,這條路我們也必須走,就如鴻蒙誕生背后的意義,我們選擇去迎接這個挑戰,這里面的每一個坎每一個坑都值得留下一個中國人的腳印。
從技術和目標角度理性去看,我們更應該實現的不是已經固化了市場和用戶習慣的本地個人文檔而是在線協同文檔,本地文檔只需考慮個人,不需要考慮多人協同場景,只需要考慮離線,不需要考慮在線場景,只需要考慮客戶端場景,不需要考慮服務器場景等...
在線文檔的宿主環境是瀏覽器,本地文檔背后是系統,國內任何在線文檔背后都沒有像谷歌文檔基於谷歌瀏覽器的支持,沒有微軟 Office
基於微軟 Windows
系統的支持,事實上基於這一切我們也該清醒認識到,做到 95%
是很難的。要知道谷歌為了開發瀏覽器前后投入了十幾年上千人上百億,微軟 Windows
系統就更不用說了,在國內我們可能擁有不了這樣的技術背景,但我們仍在努力縮小差距頑強追趕。
實現方案
在談談實現方案之前,我們先講講表格渲染有多復雜,表格的渲染一般來說有兩種實現方案:
DOM
渲染。Canvas
渲染。
業界比較出名的 handsontable
開源庫就是基於 DOM
實現渲染,同等渲染結果,需要對 DOM
節點進行精心的設計與構造,但顯而易見十萬、百萬單元格的 DOM
渲染會產生較大的性能問題。因此,如今很多在線表格實現都是基於 Canvas
和疊加 DOM
來實現的,但使用 Canvas
實現需要考慮可視區域、滾動操作、畫布層級關系,也有 Canvas
自身面臨的一些性能問題,包括 Canvas
如何進行直出等,對開發的要求較高,但為了更好的用戶體驗,更傾向於 Canvas
渲染的實現方案。
由於大部分前端項目渲染層是使用框架根據排版模型樹結構逐層渲染的,整棵渲染樹也是與排版模型樹一一對應。因此,整個渲染的節點也非常多。項目較大時,性能會受到較大的影響。
為了提升渲染性能,提供更優質的編輯體驗從 DOM
更換成 Canvas
渲染,方便開發者構建重前端大型在線文檔項目,在國內外實現類似引擎的公司僅僅只有幾家,如:騰訊文檔,金山文檔和谷歌文檔等。
頂層 | ||
---|---|---|
↑ | DOM | 容器插件輸入框等 |
↑ | Canvas | 高亮選區等 |
↑ | Canvas | 內容字體背景色等 |
底層 |
我們通過分類收集視圖元素,再進行逐類別渲染的方式,減少 Canvas
繪圖引擎切換狀態機的次數,降低性能損耗,優化渲染耗時,整個核心引擎代碼控制在 1500
行左右,另補充演示代碼 500
行,方便大家理解閱讀和進行二次開發。
這里移植的引擎主要參考了一個商業項目和一個開源項目:
畫布初始化
我們構造一個 table
類,在初始化的時候創建畫布,並設置好高度和寬度,並放入 DOM
中,並把常用的屬掛載到原型鏈上並暴露到全局 window
變量上。
class Table {
constructor(container, width, height) {
const target = document.createElement("canvas");
document.querySelector(container).appendChild(target);
this.$target = target;
this.$draw = Canvas2d.create(target);
this.$width = width;
this.$height = height;
}
}
我們就可以,頁面中在 <div id="table"></div>
放置好表格元素,全局環境中直接實例化該畫布,這里組件通過 id
屬性標識后,可以使用該 id
獲取組件對象並調用相關組件方法。
const table = new Table("#table", 800, 500);
左上區域 | col 列 | col 列 |
row 行 | cell 單元格 | cell 單元格 |
row 行 | cell 單元格 | cell 單元格 |
坐標系建立
有了畫布,我們就要開始籌備渲染,我們 table
類里面封裝 render
方法,render
方法主要繪制四個區域,也就是類似數學上的笛卡爾直角坐標系的四個想象,涉及格子線段,格子的信息,列頭部(A-Z 列),行頭部和凍結區域。
2 - 左上區域 | 1 - 右上區域 |
---|---|
3 -左下區域 | 4 - 右下區域 |
這些 area
都是通過 area.js
的 Area
初始化而來
- 2 區域至 1 區域就是 A-Z 列頭部
- 2 區域至 3 區域就是行頭部
- 4 區域最常用,是可編輯的單元格
renderBody.call(this, draw, area4);
renderRowHeader.call(this, draw, iarea3);
renderColHeader.call(this, draw, iarea1);
renderFreezeLines.call(this, draw, area4.x, area4.y);
而這個四個區域大部分的核心思路本質都是繪制格子,所有都共同用到 renderLinesAndCells
方法,里面分別有用於繪制區域的線條和格子信息的方法,里面的 renderCells
會遍歷區域然后觸發 renderCell
繪制每一個單獨的單元格,這里還會處理一些特殊的單元格,比如合並的單元格和選中的單元格,而 renderLines
則會遍歷每行每列去繪制所有行列的間隔線。
function renderLinesAndCells() {
renderLines(draw, area, lineStyle);
renderCells(draw, type, area, cell, cellStyle, selection, selectionStyle, merges);
}
單元格渲染
繪制了表格的單元格之后,就需要往每個單元格渲染數據和格式了,這里在 Table
原型鏈上掛載了一個 cell
方法,它接受一個回調函數並把它存到靜態屬性 $cell
上,當 renderCell
函數觸發的時候就會調用這個方法並把行列號傳入 $cell
方法中獲取單元格的信息。
Table.prototype["cell"] = function (callback) {
this[`$$cell`] = callback;
return this;
};
const sheet = [
["1", "1", "1"],
["1", "0", "1"],
["1", "1", "1"],
];
table.cell((ri, ci) => sheet?.[ri]?.[ci] || "").render();

所以我們可以通過暴露的 $cell
方法控制每個單元格的文字信息和單元格樣式,當然這里支持你只傳入純文本,也支持你傳入復雜數據結構來裝飾單元格,這里的 style
會有默認的值來自於 $cellStyle
。
bgcolor | #ffffff |
---|---|
align | left |
valign | middle |
textwrap | true |
underline | false |
color | #0a0a0a |
bold | false |
italic | false |
rotate | 0 |
fontSize | 9 |
fontName | Source Sans Pro |
這些樣式本質是使用 Canvas
提供的接口 draw.attr()
來展示的。
const c = cell(ri, ci);
let text = c.text || "";
let style = c.style;
cellRender(draw, text, cellRect, style);
有上面最基礎的方法,我們已經擁有表格數據展示的功能,此時我們可以暴露更豐富的接口給第三方使用,比如常用的合並單元格,我們調用 merges
方法告知 table
類,我們在 X
軸 G9
至 H11
到 Y
軸 B9
至 D11
的范圍里面的單元格是被合並的狀態,
Table.create("#table", 800, 500).merges(["G9:H11", "B9:D11"]);
那此時 renderCells
就會將該區域的格子做特殊的處理,把它渲染成合並單元格的狀態。
eachRanges(merges, (it) => {
if (it.intersects(area)) {
renderCell(draw, it.startRow, it.startCol, cell, area.rect(it), cellStyle);
}
});
數據可編輯
除了單元格合並,常用的還有畫布的事件處理,因為剛才所有的方法都只是表格只查看狀態,表格還會被用戶所編輯,所以就得監聽用戶點擊和輸入的事件,所以我們在表格渲染的時候綁定了 click
,mousedown
,mousemove
和 mouseup
事件等,我們可以通過監聽用戶點擊行為,在對應的單元格的畫布的上方,即 DOM
元素 Z
軸顯示輸入框,給用戶提供輸入修改單元格功能。
bind($target, "click", (evt) => {});
bind($target, "mousedown", (evt) => {});
所以在 DOM
節點中我們除了放 <canvas>
元素,還可以安排上 <textarea>
元素,這里可以留意到我們內聯樣式中有 areaTop
和 areaLeft
來控制我們輸入框的具體位置,這個位置獲取也是非常容易,我們只需要拿到點擊事件對象 evt.offsetX
和 evt.offsetY
,然后根據坐標的位置算出是否在四個象限區域里面並返回所在的行列信息,結合行列的信息就可以准確算出輸入框的偏移值 areaTop
和 areaLeft
,然后再讓輸入框切換為可顯示的狀態,用戶就可以在表格的對應單元格上看到輸入框。
<div
if="{{isShowArea}}"
style="width: 100px; height: 25px; top: {{areaTop}}px; left: {{areaLeft}}px"
>
<textarea
if="{{isShowArea}}"
focus="{{isFocus}}"
value="{{content}}"
selectchange="change"
onchange="change"
></textarea>
</div>
有了輸入框我們就可以監聽用戶的輸入操作,我們把輸入事件綁定在 textarea
組件上,當組件達到事件觸發條件時,會執行 JS
中對應的事件回調函數,實現頁面 UI
視圖和頁面 JS
邏輯層的交互,事件回調函數中通過參數可以攜帶額外的信息,如組件上的數據對象 dataset
事件特有的回調參數,當組件觸發事件后,事件回調函數默認會收到一個事件對象,通過該事件對象可以獲取相應的信息,我們通過事件對象得到用戶輸入的值,並調用 cell
方法重新更新表格里面對應單元格的值,當然實際情況有時候比較復雜,比如用戶是修改單元格文字的顏色,所以這里會判斷數據格式。
textarea.addEventListener("input", (e) => {
let input = e.target.value;
table.cell((ri, ci) => {
if (ri === row && ci === col) {
return (window.data[ri][ci] =
typeof value === "string" || typeof value === "number"
? input
: { ...value, text: input });
}
return window.data[ri][ci];
});
});
下圖是我們真機的實測效果,可以看到我們引入了內置庫 @system.prompt
,點擊對應的單元格彈窗顯示對應的行列信息,方便我們開發調試,我們使用手機的內置輸入法輸入內容測試,輸入框會准確獲取到信息並更新到表格上,而使用 IDE
內置的 Previewer
預覽則無效,猜測是 PC
端鍵盤的輸入事件沒有被觸發。

工具欄實現
有了最基本的查看和編輯表格的功能,下一步我們就可以考慮實現工具欄了,工具欄的實現,一般會提供設置行列高度,文本加粗,居中,斜體,下划線和背景色等設置,其實就是上面單元格 style
方法配合行列位置或者范圍信息的再封裝各種接口實現。

我們這里介紹幾個常用的,colHeader
可以設置你的列表行頭和其高度,如果你不對它進行設值,他也會有默認的高度。
table.colHeader({ height: 50, rows: 2 }).render();
某些情況,我們在查閱表格的時候,我們可能需要固定某些行和某些列的單元格來提高表格閱讀性,此時 .freeze
就可以派上用場,以下設置它會幫你凍結 C6
以內的列。
table.freeze("C6").render();
scrollRows
一般配合凍結區域使用,讓凍結區域以外的選區可以做滾動操作。
table.scrollRows(2).scrollCols(1).render();
我們可以使用以下方法更新單元格第二行第二列的數據為 8848
,顏色為紅色:
table
.cell((ri, ci) => {
if (ri === 2 && ci === 2) {
return {
text: "8848",
style: {
color: "red",
},
};
}
return this.sheet?.[ri]?.[ci] || "";
})
.render();
由於可設置單元格的形式太多了,這里不一一展開,具體可以參考以下接口,支持各種豐富的多樣的改動,可以看出來其實跟我們設置 CSS
樣式是很相似的:
{
cell: {
text,
style: {
border, fontSize, fontName,
bold, italic, color, bgcolor,
align, valign, underline, strike,
rotate, textwrap, padding,
},
type: text | button | link | checkbox | radio | list | progress | image | imageButton | date,
}
}
我們將上面常見的接口做了一些演示,運行 OpenHarmonySheet,長按
任一單元格彈出對話框
並點擊對應選項即可查看常用接口的運行結果,此演示僅供參考,更多實際使用場景請參考文檔實現:




生命周期和事件
在完成上面上述的功能之后,我們就需要考慮暴露生命周期和事件並封裝成一個通用組件給接入方使用。
@sheet-show
表格顯示@sheet-hide
表格隱藏@click-cell-start
單元格點擊前@click-cell-end
單元格點擊后@click-cell-longpress
長按表格@change
修改單元格數據
由於 OpenHarmony
為自定義組件提供了一系列生命周期回調方法,便於開發者管理自定義組件的內部邏輯。生命周期主要包括:onInit
,onAttached
,onDetached
,onLayoutReady
,onDestroy
,onPageShow
和 onPageHide
。我們的表格組件可以利用各個生命周期回調的時機暴露自身的生命周期。
this.$emit("eventName", data);
這里 name
屬性指自定義組件名稱,組件名稱對大小寫不敏感,默認使用小寫。src
屬性指自定義組件 hml
文件路徑,如果沒有設置 name
屬性,則默認使用 hml
文件名作為組件名。而自定義組件中綁定子組件事件使用 onXXX
或 @XXX
語法,子組件中通過 this.$emit
觸發事件並進行傳值,通過綁定的自定義事件向上傳遞參數,父組件執行 bindParentVmMethod
方法並接收子組件傳遞的參數。
<element name="Sheet" src="../../components/index.hml"></element>
<Sheet
sheet="{{sheet}}"
@sheet-show="sheetShow"
@sheet-hide="sheetHide"
@click-cell-start="clickCellStart"
@click-cell-end="clickCellEnd"
@click-cell-longpress="clickCellLongpress"
@change="change"
></Sheet>
我們把上面這些通通打包,並完善了介紹文檔和接入文檔上傳到 Gitee
- OpenHarmonySheet
倉庫中,就完成了我們的表格引擎組件。

回顧整個過程雖然有難度有挑戰,但我們團隊還是的群策群力解決了,在整個比賽過程中我們團隊也學習了很多關於鴻蒙的東西,在以前一直沒有這個機會去了解,借着這次比賽的機會能重新認識鴻蒙,也認識了一些志同道合的開發者,暫且做個總結吧,作品在參賽前要找准一個方向,最好是這個領域自己熟悉的,並且與其他參賽作品是不重合的,這樣對自己的作品是負責,對別人的作品是友好,對比賽也是尊重的,在把握好方向之后就要制定每一個小計划和最終的目標,做好前中后期的具體規划,步子不能跨地太大,不能好高騖遠,要腳踏實地的去完成每一個小計划,這個過程對於團隊和自己都是雙贏的,雖然我們不一定能到達終點,但回頭看我們每一個腳印都是自己努力的回報,團隊成員之間要團結,有針對性地完成好每一個任務,認真負責,互相幫忙,共同進步,正如鴻蒙所經歷的一樣,一個完善的系統需要千萬開發者齊心協力一起去構建和打磨,希望能有越來越多好的 OpenHarmony
開源項目,不積跬步,無以至千里,不積小流,無以成江海,一起去打造屬於我們的生態。
最后衷心希望 OpenHarmony
能發展的越來越強大,越來越順利,這條路雖然很難,但值得,長風破浪會有時,直掛雲帆濟滄海!