公開揭密團隊成員開發鴻蒙 OpenHarmony 的完整過程(收穫官方7k獎金和開發板等,2w字用心總結)

wscats發表於2021-11-22

背景

隨著 OpenHarmony 元件開發大賽結果公佈,我們的團隊成員被告知獲得了二等獎,在開心之餘也想將我們這段時間寶貴的開發經驗寫下來與大家分享,當我們看到參賽通知的時候已經是 9 月中旬的時候,此時已經是作品可以提交的時間了,參考了一些其他作品發現,基於 Canvas 開發的元件目前還沒有,那我們就開始計劃寫一個基於 Canvas 和通用元件一起開發的元件,在這之前由於並沒有開發過 OpenHarmony 應用,我們團隊成員都沒有相關的經驗,大家從零開始在摸索,我們首先分工合作,有的成員負責去下載 IDE 和除錯裝置,有的成員負責研究和閱讀官方文件。先附上原始碼,分享和總結不易,求點贊關注一下⭐️:

https://github.com/Wscats/openharmony-sheet

配置

在閱讀完官方文件之後,我們成員分別在自己本地電腦和裝置上做了以下的環境配置:

  1. 下載並安裝好 DevEco Studio 2.1 Release 及以上版本
  2. 獲取 OpenHarmony SDK 包並解壓
  3. 配置 OpenHarmony SDK

DevEco 主介面,點選工具欄中的 File > Settings > Appearance & Behavior > System Settings > HarmonyOS SDK 介面,點選 HarmonyOS SDK Location 載入 SDK

然後一直點選 NextFinish 完成環境配置。

  1. 安裝額外包,進入 OpenHarmony-SDK-2.0-Canary/js/2.2.0.0/build-tools/ace-loader 目錄,然後在該目錄下執行命令列工具,分別執行如下命令,直至安裝完成
npm cache clean -f
npm install
  1. 下載 OpenHarmonyJSDemos 專案工程,將工程匯入 DevEco Studio
  2. 申請並配置證書,注意 OpenHarmonyHarmonyOS 的證書不通用,所以需要額外進行申請
  3. 進行編譯構建,生成一個 HAP 應用安裝包,生成 HAP 應用安裝包,安裝到 OpenHarmony 開發板
  4. 安裝執行後,在開發板螢幕上點選應用圖示即可開啟應用,即可在裝置上檢視應用示例執行效果,以及進行相關除錯
  5. 除了使用真機除錯,我們還可以使用遠端除錯和本地的 Previewer 除錯,雖然非常相當方便,但實際表現肯定和真機是有稍微差異的

前言

在實現 Canvas 應用之前,我們經過一些商量和討論,首先是希望能借助這一次開發提升對 OpenHarmony 的理解,方便後續業務的支援,其次我們團隊成員也是希望能拿到比較好的名次和獎勵,我們注意到比賽的評分由評委打分,滿分為 100 分,這裡會根據作品的創意性、實用性、使用者體驗、程式碼規範等四個維度點評打分,Canvas 的應用首先實現成本會比普通應用難度稍微大點,並且不好除錯,在創意性和實用性上我們優勢不大,因為大部分前端開發者接觸到的 Canvas 應用都是遊戲相關的,所以這條路註定是會相對艱難的,使用者體驗也是一個很大的難點,我們真機測試發現 Canvas 的表現也不是很好,比原生一些元件的體驗差很多,對於團隊成員的程式碼質量是有信心的,但是程式碼規範的評分比重卻是最少的,所以在立項的時候我們有比較大的分歧。

評選維度說明分值
創意性作品的創新程度30%
實用性作品在應用場景中的實際應用程度30%
使用者體驗使用者體驗價值,使用者能夠輕鬆使用元件,並獲得良好體驗感25%
程式碼規範程式碼的質量,美觀度,是否符合規範15%

計劃

正因為由上面總總的疑慮,我們先制定了三個計劃和一個目標:

渲染引擎是我們最終目標,雖然難度偏大,但我們團隊成員決定分開三步來實現該目標,首先至少先學會使用基礎元件和容器元件,然後再學會使用畫布元件,最後綜合這些經驗實現一個渲染引擎。

初體驗

我們首先實現了一個通用的畫廊元件來作為練手專案,它主要使用了四個基礎元件和容器元件:

我們放置一個按鈕來觸發 showGallery 方法,該方法控制 panel 彈出式元件的顯示和隱藏,這裡的 divbutton 標籤就是 hml 內建的元件,跟我們平常寫 html 很相似,它支援我們大部分的常規屬性如 idclasstype 等,方便我們用來設定元件基本標識和外觀特徵顯示。

<div class="btn-div">
  <button type="capsule" value="Click Here" onclick="showGallery"></button>
</div>

然後我們 panel 元件中放置可變更的畫廊內容展示視窗,並讓 modesrc 變成可設定的變數,這樣畫廊元件就能根據模式讓畫廊元件顯示不同的形態,根據傳入的圖片地址顯示不同的圖片內容,這裡的語法跟微信小程式很和 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 是畫廊元件的資料模型,型別可以是物件或者函式,如果型別是函式,返回值必須是物件,注意屬性名不能以 $_ 開頭,不要使用保留字,我們在這裡給 modeFlaggalleryUrl 設定預設值。

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 補充元件的樣式,可以讓我們的畫廊呈現更好的效果,這裡動畫樣式還支援動態的旋轉、平移、縮放和漸變效果,均可在 stylecss 中設定。

.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 方法是整個遊戲的核心邏輯,涉及小鳥的飛行動畫,運動軌跡,邊界處理和得分計算。

首先我們從畫布的左上角 XY 軸的起始位置開始繪製遊戲的背景。

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 渲染的實現方案。

由於大部分前端專案渲染層是使用框架根據排版模型樹結構逐層渲染的,整棵渲染樹也是與排版模型樹一一對應。因此,整個渲染的節點也非常多。專案較大時,效能會受到較大的影響。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ACtSdVX6-1637535201140)(https://p3-juejin.byteimg.com... "螢幕截圖.png")]

為了提升渲染效能,提供更優質的編輯體驗從 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.jsArea 初始化而來

  • 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();

<img width="250" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d88f654f3b04af1a1b7fdb54eb89472~tplv-k3u1fbpfcp-zoom-1.image" />

所以我們可以通過暴露的 $cell 方法控制每個單元格的文字資訊和單元格樣式,當然這裡支援你只傳入純文字,也支援你傳入複雜資料結構來裝飾單元格,這裡的 style 會有預設的值來自於 $cellStyle

bgcolor#ffffff
alignleft
valignmiddle
textwraptrue
underlinefalse
color#0a0a0a
boldfalse
italicfalse
rotate0
fontSize9
fontNameSource 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 類,我們在 XG9H11YB9D11 的範圍裡面的單元格是被合併的狀態,

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);
  }
});

資料可編輯

除了單元格合併,常用的還有畫布的事件處理,因為剛才所有的方法都只是表格只檢視狀態,表格還會被使用者所編輯,所以就得監聽使用者點選和輸入的事件,所以我們在表格渲染的時候繫結了 clickmousedownmousemovemouseup 事件等,我們可以通過監聽使用者點選行為,在對應的單元格的畫布的上方,即 DOM 元素 Z 軸顯示輸入框,給使用者提供輸入修改單元格功能。

bind($target, "click", (evt) => {});
bind($target, "mousedown", (evt) => {});

所以在 DOM 節點中我們除了放 <canvas> 元素,還可以安排上 <textarea> 元素,這裡可以留意到我們內聯樣式中有 areaTopareaLeft 來控制我們輸入框的具體位置,這個位置獲取也是非常容易,我們只需要拿到點選事件物件 evt.offsetXevt.offsetY,然後根據座標的位置算出是否在四個象限區域裡面並返回所在的行列資訊,結合行列的資訊就可以準確算出輸入框的偏移值 areaTopareaLeft,然後再讓輸入框切換為可顯示的狀態,使用者就可以在表格的對應單元格上看到輸入框。

<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 為自定義元件提供了一系列生命週期回撥方法,便於開發者管理自定義元件的內部邏輯。生命週期主要包括:onInitonAttachedonDetachedonLayoutReadyonDestroyonPageShowonPageHide。我們的表格元件可以利用各個生命週期回撥的時機暴露自身的生命週期。

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
](https://github.com/Wscats/sheet) 倉庫中,就完成了我們的表格引擎元件。

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

最後衷心希望 OpenHarmony 能發展的越來越強大,越來越順利,這條路雖然很難,但值得,長風破浪會有時,直掛雲帆濟滄海!

相關文章