用程序讀歷史,以數據講故事。
轉眼又是10月,去年立下更新APP 的 flag 好像又要爽約了,一方面是工作疊加疫情buff,整個上半年都已經不是996、幾乎就變成 007……
下半年又是各種意想不到,2022的最后一個季度,甭管能不能成,還是動動手、把問題一條條列出來逐個解決吧。
愁人的前端
寫 APP 的初心很簡單,就是曾經每天上下班路上倆小時、地鐵上手機信號又差、自此落下了看歷史故事書的毛病,但是有些歷史書作者吧、寫是很能寫,動不動就數十萬字的篇幅,可就是不舍得配上哪怕一幅地圖,每次涉及各種地名、方位的時候還得自己去找圖理解就很煩人,恰好那一陣遇上創業失敗的 gap month,想著干脆就自己寫個歷史地圖的搜索查看 APP吧:
初版服務端架構 ▼
服務端熟門熟路,很快就把架子干起來了,鑒于那會兒 Boot 還不流行,用的又是乞丐版云服務器,只有 的配置,反正業務也不復雜,就是定期爬取各網站歷史地圖資源的分享鏈接、然后用 建個索引,最后 Netty 簡單封裝一下 http 服務就完事。
不過到客戶端上就比較頭大了,雖說在 1.5 - 4.x 時代折騰過一陣 APP 開發,不過一直不擅長界面,用盡洪荒之力做出的效果天花板也就是像下面這樣了:
過于古樸的游戲界面 ▼
況且還有 iOS 那奇奇怪怪的 -C 語法要學,一想就頭大。所以最理想的還是能找一套前端跨端開發框架,本來嘛功能也不復雜,能不折騰就不折騰。
還好那時候跨端框架不多,選型倒也沒花太多時間。一個是以「卡」著稱,號稱前端 H5 開發人員零成本上手的 (現已更名為歸屬基金會,老實說 Adobe 超前概念不少,能做成的不多……);另一個就是 React ,號稱:「Learn once, write 」,翻譯過來是:「一次學習、/iOS 皆可寫」,如果 也弄個口號那就是「Write once, use 」,「寫一次代碼、/iOS 皆可用」。
如果當時是前端程序猿的話可能就上 的船了,不過作為后端出身,「Write one」的前提是,要把前端開發(html +css + js)技術棧從頭學一遍,那就……
最終,初版客戶端架構敲定如下:
初版客戶端架構 ▼
React 的界面布局采用的是JSX 語法,一種類 xml,跟 的 布局文件比較像,上手起來也沒有太多障礙,可以看一下當時第一版首頁的代碼:
import React, {Component} from 'react';
import {
StyleSheet,
Text,
View,
Navigator,
TouchableOpacity,
Image
}?from?'react-native';
import Search from 'react-native-search-box';
import SearchResult from './search.result.js'
export default class Home extends Component {
constructor(props) {
super(props);
}
beforeFocus = () => {
return new Promise((resolve, reject) => {
console.log('beforeFocus');
resolve();
});
}
onFocus = (text) => {
return new Promise((resolve, reject) => {
console.log('beforeFocus', text);
resolve();
});
}
afterFocus = () => {
return new Promise((resolve, reject) => {
console.log('afterFocus');
resolve();
});
}
onSearch = (text) => {
return new Promise((resolve, reject) => {
????????????console.log('onSearch',?text);
const { navigator } = this.props;
if(navigator) {
navigator.push({
name: '搜索結果',
component: SearchResult,
params: {
searchKey: text
}
})
}
});
}
onChangeText = (text) => {
return new Promise((resolve, reject) => {
console.log('onChangeText', text);
resolve();
});
}
render() {
return (
source={require('../../resources/img/head.jpg')}
/>
beforeFocus={this.beforeFocus}
onFocus={this.onFocus}
afterFocus={this.afterFocus}
onSearch={this.onSearch}
onChangeText={this.onChangeText}
placeholder='輸入搜索關鍵字' keyboardType='web-search'/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'stretch',
backgroundColor: '#F5FCFF'
},
headContainer: {
alignItems: 'center',
height: 100,
},
head: {
width: 200,
height: 100,
marginBottom: 50,
marginTop: 50,
},
searchContainer: {
marginTop: 100,
marginHorizontal: 10,
}
});
不似 Vue 那種 html + js + css 三段式結構,這里整一個文件都是 js,也就是 「Learn once」的核心概念,只需要會 js,那就可以開發 APP 了,而 js 最大的特點就是不用學,即拿即用……
特別是 15年 ES6 標準發布后,結合 async/await 關鍵字,整個 js 的開發習慣已經跟后端同步編程的習慣相當接近了。
最后一點是 React 提供了完整的工具鏈支持,對于起步階段的基本功能開發甚至可以完全不用懂原生開發,數行命令就可以把工程打包成 APP 往應用市場扔了。畢竟第一版僅有4個頁面,首次提交 審核還因為功能太簡單而被拒,幸好申訴后給過了,否則也就沒有后面的事了:
初版的4個頁面 ▼
就這樣,總算是一個人干成了四個人的活(某人說首次打開 APP 的時候就是覺著后面應該有四個人在開發),但就像「人生并不總能如初見」,隨著 APP 后續功能的添加:探索地圖、歷史上的今天、史籍資料…… 前端架構慢慢就演變成了下面這種奇奇怪怪的形狀:
V3版前端架構 ▼
因為 React 生態下沒有好用的地圖圖層渲染組件,引入了 ,對應的為了配套 的運行,就又在上面跑了一個 的容器來運行 Vue.js 的頁面……
為了保存史籍的閱讀進度記錄,又引入了 realm 數據庫來做本地存儲、同時還引入了各種 UI 組件、語音閱讀組件等,此外對于那些沒有 React 生態支持的功能,比如 QQ、微博登錄 SDK,還必須寫原生代碼封裝……
然后問題就來了,當初第一版 APP 發布的時候 React 的版本號是 0.43.3,5年過去了,現在的版本號是 0.70.2,反觀后出道的 都已經更新到 3.3 版本了。
React 一直不升 1.0.0 的正式版本,一方面是 都改名 Meta 做元宇宙去了,在這方面或許也沒留啥資源;另一方面就是,新發布的版本不做 API 兼容性的保證:after有兼容性問題嗎,隨時可能出現參數大調整,或者某個 API 突然就沒有了。
對于美團、京東這樣有資源的大公司可以通過維護自己的分支版本、構建自己的組件庫來屏蔽這種變化,可對于獨立開發者來說就只能被綁架上這輛奔馳的升級快車了。如果不跟著小步升級,未來某一天就有可能出現 、iOS 操作系統升級了,現有 API 不能支持的情況,但如果跟的太緊,因為第三方庫依賴太多,又會面臨各種 API 兼容性的問題,遇到這種情況如果是活躍的開源庫就等開發者修復、不活躍的就只能上手改源碼了……
所以到 V3 版本后,很多時間都不是花在功能完善上、就盡干這些 React 版本升級,然后修改各種兼容性的事情去了。就倆字:心累!
整點兒新活
如果要恢復APP 更新,第一件事就是得把 V3 版本的客戶端架構給推了重來,不過還好,APP停更的這兩年多也出現了不少新東西:、Rust、、小程序,有不少作業可以抄。
比如說面對著涌現出來的這么多跨端開發方案,美團給出的答卷是:「我全都要」—— 把 APP 做成業務無關的殼容器:
美團的容器化方案 ▼
涵蓋了 ()、React (MRN)、()、小程序等幾乎目前所有的跨端解決方案,并且還很良心的給出了不同容器所適用的業務場景總結:
容器選型標準 ▼
美團的這份作業可以說是解決了跨端開發下最大頭的界面代碼復用的問題,不過還有一個問題沒有解決,就是公共邏輯代碼的復用(比如飛書提到了一個場景,通過客戶端的 AI 模型來動態對聊天會話進行排序,優先為用戶顯示更關注的內容)。對這部分代碼有兩種選擇:一種是跟容器裝載的代碼放一塊,這樣不同容器就需要各自來實現對應的公共代碼;還有一種就是放在 上來由 、iOS 分別實現。當然也還有第三種方案,就是通過 C++ 代碼以 so 庫的形式供不同平臺來使用,不過嘛,C++ 的工具鏈支持以及開發效率實在是有些跟不上現代互聯網的迭代節奏了,所以在美團方案里也并沒有這方面的體現。
對此 給出了另一份答案:
跨端復用方案(手繪圖略亂) ▼
是一家專門為用戶提供各種亂七八糟密碼保存工具的公司,他們所面對的狀況就是既有桌面端版本(Linux、MacOS、),又有 web 端版本,對應的加密算法不僅要多個平臺實現,而且上層還需要根據不同平臺做適配。
為了屏蔽這種差異 基于 Rust 封裝了一個核心(Core)模塊,基于 FFI(:after有兼容性問題嗎,語言交互接口)與不同操作系統下的開發語言做交互,實現了不同桌面端的代碼共享,再基于 把 Rust 編譯成 wasm,最終實現了瀏覽器端也共享同一份代碼。
這里介紹一下 Rust:是由 主導開發的一款通用、編譯型語言(此外、華為、谷歌、微軟也是基金會的創始白金成員),其直接競爭對手是 C/C++、go,但是整個設計理念、工具鏈支持又很前端,比如說 cargo 可以對應到 npm,也引入了 Lint 做代碼規范檢查,近乎全平臺支持的開箱即用開發環境搭建等。當然,也因為語言本身陡峭的學習曲線一直不慍不火……
最后糅合了一下以上兩份作業,V4 版本的歷史地圖 APP 前端架構大概是長下面這樣:
V4版客戶端架構 ▼
把類似地圖計算,以及后續可能會引入的 AI 算法這些模塊邏輯下沉到 Rust 中來實現(Core實現統一的核心代碼、做不同操作系統的差異化適配、APIs對外暴露統一接口);容器層跟 層就專心負責各類業務邏輯的展示部分。
嗯,夢想是有了,下面就是實現了……
最后,公眾號后臺回復「代碼復用」可以獲取本文中提到各類參考資料地址。
讓我知道你“在看”