原文連結:github.com/CodeFalling…
介紹一下 Remax
Remax 是一個跨多端小程式 React 開發方案,之所以稱其為“方案”而非框架是因為這並非一個新的框架,其主要能力就是讓 React 能夠直接執行在 微信小程式/支付寶小程式/位元組跳動小程式/H5(當然這個本來就支援) 等環境。
可能會有人要會問 “React 不是早就可以執行在小程式中了麼“?本文會介紹一下現如今的一些小程式框架的解決方案,以及為什麼我們認為把 React 直接搬進小程式是個更為合理的方案。
靜態編譯類框架
由於大多開發者都更熟悉 React 和 Vue 的 API 和語法,加上小程式本身的開發方式確實讓人痛苦,於是便有了一些框架來將這些熟悉的語法編譯到小程式的 WXML/WXSS/JS 上,其中比較具有代表性的例如 taro,其目標就是讓開發者能夠用 React 的開發方式編寫小程式。
而這類框架的實現原理其實並非真的是一個 React 或者類 React 框架,而是把看起來像是 JSX 的模板通過靜態編譯的方式翻譯成小程式自身的模板。
這樣做的限制非常明顯,那就是 JSX 是 JavaScript 的擴充語言(React Blog 寫的是 is a syntax extension to JavaScript
),而小程式所採用的 WXML 卻是一個表達能力非常受限的模板語言,我們不可能完成從一個通用程式語言到模板語言的編譯。
而靜態編譯類框架為了做到這一點,採取的方式就是限制開發者的寫法,這也是為什麼上面稱之為看起來像是 JSX 的模板,這也是為什麼 taro 對 JSX 的寫法做出了諸多限制。
這種方案大多聲稱這些限制並沒有限制生產力,或者符合最佳實踐等等。然而我們其實都知道這是由於小程式本身的坑造成的,靜態編譯方案編譯的永遠都只會是模板語言,而不是 JSX。
React Hooks
之所以我說這些限制並非基於最佳實踐,是因為 React 本身對於 JSX 的定位就 並非模板。
JSX is a syntax extension to JavaScript.
在最近 React 團隊已經向我們介紹了 Hooks,期望可以 functional component 不僅僅可以是無狀態元件,也可以是 useState
的。
import { useState } from `react`;
function Example() {
// Declare a new state variable, which we`ll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
React 官方部落格提到 Classes confuse both people and machines
,我們也明顯可以看到基於 function 的元件明顯更為簡潔,噪聲更小,未來 React 社群的方向更是會逐漸從 class component 過渡到 functional component。
在這種趨勢下,把 JSX 當做模板寫,且未來永遠也不可能支援 functional component 的方案絕非真的基於最佳實踐的選擇。
在 Remax 中,我們完全可以使用全新的 Hooks API 來開發元件
因為 Remax 中的 React 就是 React.js,而 JSX 就是 JavaScript 的超集。
上圖中使用小程式的原生語法,classname 和 inline style 就只能寫成
<view class="weui-navbar__item {{activeIndex == index ? `weui-bar__item_on` : ``}}">
</view>
<view style="left: {{sliderLeft}}px; transform: translateX({{sliderOffset}}px); -webkit-transform: translateX({{sliderOffset}}px);"></view>
複製程式碼
而使用 remax 後就可以寫成正常的 react:
const innerStyle = {
left: `${sliderLeft}px`,
transform: `translateX(${sliderOffset}px);`,
`-webkit-transform`: `translateX(${sliderOffset}px)`,
width: sliderWidth,
};
const itemClassName = classnames({
`weui-navbar__item`: true,
`weui-bar__item_on`: activeIndex === index,
});
return <View className={itemClassName}>
<View style={innerStyle} />
</View>
複製程式碼
實現原理
核心部分
Remax 的實現原理和基於靜態編譯的方案有所不同,其核心其實是重新實現了 ReactDOM 的部分。
眾所周知,React 本身的設計就是支援跨端渲染的,render 部分和 React 的核心邏輯是解耦的(甚至不在一個 npm 包裡)。主要的 render 有 ReactDOM(瀏覽器),ReactDOMServer(伺服器端)和 ReactNative。
Remax 要做的事情和 ReactNative 要做的事情非常類似,我們重新接管了 ReactDOM 的 render。
在原有的 React 頁面中,React 在完成 Diff 發現需要修改介面時,又 ReactDOM 把改變 Patch 到頁面上。
而在小程式中由於我們不能直接修改頁面,則由 React 完成 DIFF 後由 Remax 把修改 Patch 到記憶體中的虛擬 DOM 上,然後再通過小程式自己的虛擬 DOM 最後把改變同步到頁面上。
在這裡我把這個過程說得非常簡單,但實際上是有些坑要填的,主要也都是來自於小程式的限制,後續會有新的文章展開來講。但是這種實現方式使得我們完全可以把 React 的程式碼放在小程式的環境中執行。
工程化
工程化很理所當然的用 Webpack 來實現, 除了我們常用的打包等功能外,Webpack 外掛也使我們很容易構建一些我們需要的東西出來,例如我們需要在每個 js 入口除了放一個 js 外還需要新增一個 wxml
檔案,就可以通過一個很簡單的 Webpack 外掛來實現。
function GeneraeWxmlWebpackPlugin() {
const content = `<view>...</view>`;
const apply = (compiler) => {
const emit = (compilation, cb) => {
const {
chunks,
} = compilation;
chunks.forEach((item) => {
compilation.assets[`${item.name}.wxml`] = {
source: () => content,
size: () => content.length,
};
});
cb();
};
if (compiler.hooks) {
const plugin = { name: `GeneraeWxmlWebpackPlugin` };
compiler.hooks.emit.tapAsync(plugin, emit);
} else {
compiler.plugin(`emit`, emit);
}
};
return {
apply,
};
}
複製程式碼
跨端
這種方案想實現同一套程式碼跨到 H5 端顯然沒有什麼問題,至於支付寶小程式目前驗證了一下可行性也是可行的。
專案結構
這個專案主要由幾塊組成
- @remax/core
核心部分,負責 React 元件的 render - @remax/cli
顧名思義,CLI 工具,用於構建生成相應的小程式專案等工作 - @remax/components
底層 Component,包括諸如 View 等一些基礎元件,用於抹平不同環境的差異 - @remax/ui
自帶的基礎元件庫,這部分還待開發,目前只有一兩個示例元件
由於目前整個專案才剛剛起步,暫時還不能用於生產環境,目前的幾個主要開發者(和打算參與的)有 @CodeFalling @bramblex @ahonn @simplyy
目前的 DEMO 可以掃碼體驗:
或者在可以按照 github.com/CodeFalling… 體驗本地 DEMO。
如果有人想要參與進來一起開發可以聯絡我,開發相關的細節文件會陸續更新在 github.com/CodeFalling… 。