您好,登錄后才能下訂單哦!
這篇文章主要介紹“React怎么構建小程序”,在日常操作中,相信很多人在React怎么構建小程序問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”React怎么構建小程序”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
為了更清晰描述實現過程,我們把實現方案當作一個項目來對待。
項目需求:使如下計數器功能的 React 代碼運行到微信小程序平臺。
import React, { Component } from 'react' import { View, Text, Button } from '@leo/components' import './index.css' export default class Index extends Component { constructor() { super() this.state = { count: 0 } this.onAddClick = this.onAddClick.bind(this) this.onReduceClick = this.onReduceClick.bind(this) } componentDidMount () { console.log('執行componentDidMount') this.setState({ count: 1 }) } onAddClick() { this.setState({ count: this.state.count + 1 }) } onReduceClick() { this.setState({ count: this.state.count - 1 }) } render () { const text = this.state.count % 2 === 0 ? '偶數' : '奇數' return ( <View className="container"> <View className="conut"> <Text>count: {this.state.count}</Text> </View> <View> <Text className="text">{text}</Text> </View> <Button onClick={this.onAddClick} className="btn">+1</Button> <Button onClick={this.onReduceClick} className="btn">-1</Button> </View> ) } }
如果使用過 Taro 或者 Remax 等框架,對上述代碼應該有似曾相識的感覺,上述代碼正式模仿這類框架的 React DSL 寫法。如果想迫切看到實現這個需求的效果,可點擊此項目源碼進行獲取源碼,然后根據提示運行項目,即可觀察到如下效果:
到這里,就清楚了知道這個項目的需求以及最終實現結果是什么,接下來便是重點闡述從需求點到結果這個過程的具體實現。
開發過小程序的同學都知道,小程序框架包含主體和頁面,其中主體是由三個文件生組成的,且必須放在根目錄,這三個文件分別是: app.js
(必需,小程序邏輯),app.json
(必需,小程序公共配置),app.wxss
(非必須,小程序公共樣式表)。所以要將 React 代碼構建成小程序代碼,首先需要先生成app.js
和app.json
文件。因為本次轉換未涉及到app.js
文件,所以app.js
內容可以直接寫死 App({})
代替。app.json
是配置文件,可以直接在 React 工程新增一個app.config.js
用來填寫配置內容,即 React 代碼工程目錄如下:
├── src │ ├── app.config.js // 小程序配置文件,用來生成app.json內容 │ └── pages │ └── index │ ├── index.css │ └── index.jsx // React代碼,即上述計數器代碼 └── tsconfig.json
app.config.js
內容即是小程序全局配置內容,如下:
module.exports = { pages: ['pages/index/index'], window: { navigationBarTitleText: 'react-wxapp', navigationBarBackgroundColor: '#282c34' } };
有了這個配置文件,就可以通過如下方式生成app.js
和app.json
文件。
/*outputDir為小程序代碼生成目錄*/ fs.writeFileSync(path.join(outputDir, './app.js'), `App({})`) fs.writeFileSync(path.join(outputDir, './app.json'), JSON.stringify(config, undefined, 2)) // config即為app.config.js文件內容
小程序頁面則是由四種類型文件構成,分別是js
(必需,頁面邏輯)、wxml
(必需,頁面結是構)、json
(非必需、頁面配置)、wxss
(非必需、頁面樣式表)。而React代碼轉小程序,主要是考慮如何將React代碼轉換程序對應的js
和wxml
類型文件,后文會詳細闡述。
實現React代碼運行到小程序平臺上主要有兩種方式,一種是編譯時實現,一種是運行時實現,如果你已經查看的本項目項目源碼,就可以發現源碼里也體現出了這兩種方式(編譯時實現目錄:packages/compile-core
;運行時實現目錄:packages/runtime-core
)。
編譯時方式主要通過靜態編譯將 JSX 轉換成小程序對應的 template 來實現渲染,類似 Taro1.0 和 2.0,此方式性能接近原生小程序,但是語法卻有很大的限制。運行時實現是通過react-reconciler
重新在小程序平臺定義一個 React 渲染器,使得 React 代碼可以真正運行到小程序里,類似 Taro3.0、Remax 等,因此這種方式無語法限制,但是性能會比較差。本項目源碼正是參照 Taro、Remax 這類框架源碼并簡化很多細節進行實現的,因此這個項目源碼只是適合來學習的,并不能投入實際業務進行使用。
接下來將分別講述如何通過編譯時和運行時這兩種方式來實現 React 運行到小程序平臺。
在講述具體實現流程之前,首先需要了解下編譯時實現這個名詞的概念,首先這里的編譯并非傳統的高大上“編譯”,傳統意義上的編譯一般將高級語言往低級語言進行編譯,但這里只是將同等水平語言轉換,即將javascript
代碼字符串編譯成另一種javascript
代碼字符串,因此這里的編譯更類似于“轉譯”。其次,雖然這里稱編譯時實現,并非所有實現過程都是編譯的,還是需要少部分實現需要運行時配合,因此這種方式稱為重編譯輕運行方式更為合適。同樣的,運行時實現也含有少量編譯時實現,亦可稱為重運行輕編譯方式。
為了方便實現將javascript
代碼字符串編譯成另一種javascript
代碼字符串,這里直接采用Babel
工具,由于篇幅問題,這里就不詳細講述Babel
用法了,如果對Babel
不熟的話,可以看看這篇文章簡單了解下(沒錯,就是給自己打廣告)。接下來我們來分析編譯時實現步驟有哪些:
1. JSX轉換成對應小程序的模板
React是通過JSX
來渲染視圖的,而小程序則通過wxml
來渲染視圖,要將 React 運行到小程序上,其重點就是要如何實現JSX
轉換成對應的小程序的wxml
,其轉換規則就是將JSX
使用語法轉換成小程序相同功能的語法,例如:
標簽元素轉換:View
、Text
、Button
等標簽直接映射為小程序基礎組件本身(改為小寫)
樣式類名轉換:className
修改為class
<View className="xxx" /> ==> <View class="xxx" />
事件轉換:如onClick
修改為bindtap
<View onClick=xxx /> ==> <View bindtap =xxx />
循環轉換:map
語法修改為wx:for
list.map(i => <Text>{i}</Text>) => <Text wx:for="{{list}}">{{item}}</Text>
語法轉換遠不止上面這些類型,如果要保證開發者可以使用各種JSX
語法開發小程序,就需要盡可能窮舉出所有語法轉換規則,否則很可能開發者用了一個寫法就不支持轉換。而事實是,有些寫法(比如動態生成JSX片段等等)是根本無法支持轉換,這也是前文為什么說編譯時實現方案的缺點是語法有限制,開發者不能隨意編碼,需要受限于框架本身開發規則。
由于上述需要轉換JSX
代碼語法相對簡單,只需要涉及幾種簡單語法規則轉換,這里直接貼出轉換后的wxml
結果如下,對應的實現代碼位于:packages/compile-core/transform/parseTemplate.ts
。
<view class="container"> <view class="conut"><Text>count: {{count}}</Text></view> <view> <text class="text">{{text}}</text> </view> <button bindtap="onAddClick" class="btn">+1</button> <button bindtap="onReduceClick" class="btn">-1</button> </view>
2. 運行時適配
如前文所說,雖然這個方案稱為編譯時實現,但是要將React
代碼在小程序平臺驅動運行起來,還需要在運行時做下適配處理。適配處理主要在小程序js
邏輯實現,內容主要有三塊:數據渲染、事件處理、生命周期映射。
小程序js
邏輯是通過一個object
參數配置聲明周期、事件等來進行注冊,并通過setData
方法觸發視圖渲染:
Component({ data: {}, onReady () { this.setData(..) }, handleClick () {} })
而計數器React
代碼是通過class
聲明一個組件邏輯,類似:
class CustomComponent extends Component { state = { } componentDidMount() { this.setState(..) } handleClick () { } }
從上面兩段代碼可以看出,小程序是通過object
聲明邏輯,React 則是通過class
進行聲明。除此之外,小程序是通過setData
觸發視圖(wxml
)渲染,React 則是通過 setState
觸發視圖(render
方法)渲染。所以要使得 React 邏輯可以運行到小程序平臺,可以加入一個運行時墊片,將兩者邏輯寫法通過墊片對應起來。再介紹運行時墊片具體實現前,還需要對上述 React 計數器代碼進行簡單的轉換處理,處理完的代碼如下:
import React, { Component } from "../../npm/app.js"; // 1.app.js為墊片實現文件 export default class Index extends Component { static $$events = ["onAddClick", "onReduceClick"]; // 2.收集JSX事件名稱 constructor() { super(); this.state = { count: 0 }; this.onAddClick = this.onAddClick.bind(this); this.onReduceClick = this.onReduceClick.bind(this); } componentDidMount() { console.log('執行componentDidMount'); this.setState({ count: 1 }); } onAddClick() { this.setState({ count: this.state.count + 1 }); } onReduceClick() { this.setState({ count: this.state.count - 1 }); } createData() { // 3.render函數改為createData,刪除 this.__state = arguments[0]; // 原本的JSX代碼,返回更新后的state // 提供給小程序進行setData const text = this.state.count % 2 === 0 ? '偶數' : '奇數'; Object.assign(this.__state, { text: text }); return this.__state; } } Page(require('../../npm/app.js').createPage(Index))。 // 4.使用運行時墊片提供的createPage // 方法進行初始化 // 方法進行初始化
如上代碼,需要處理的地方有4處:
Component
進行重寫,重寫邏輯在運行時墊片文件內實現,即app.js
,實現具體邏輯后文會貼出。
將原本JSX
的點擊事件對應的回調方法名稱進行收集,以便在運行時墊片在小程序平臺進行事件注冊。
因為原本render
方法內JSX
片段轉換為wxml
了,所以這里render
方法可將JSX
片段進行刪除。另外因為React
每次執行setState
都會觸發render
方法,而render
方法內會接受到最新的state
數據來更新視圖,因此這里產生的最新state
正是需要提供給小程序的setData
方法,從而觸發小程序的數據渲染,為此將render
名稱重命名為createData
(生產小程序的data
數據),同時改寫內部邏輯,將產生的最新state
進行返回。
使用運行時墊片提供的createPage
方法進行初始化(createPage
方法實現具體邏輯后文會貼出),同時通過小程序平臺提供的Page
方法進行注冊,從這里可得知createPage
方法返回的數據肯定是一個object
類型。
運行時墊片(app.js)實現邏輯如下:
export class Component { // 重寫Component的實現邏輯 constructor() { this.state = {} } setState(state) { // setState最終觸發小程序的setData update(this.$scope.$component, state) } _init(scope) { this.$scope = scope } } function update($component, state = {}) { $component.state = Object.assign($component.state, state) let data = $component.createData(state) // 執行createData獲取最新的state data['$leoCompReady'] = true $component.state = data $component.$scope.setData(data) // 將state傳遞給setData進行更新 } export function createPage(ComponentClass) { // createPage實現邏輯 const componentInstance = new ComponentClass() // 實例化傳入進來React的Class組件 const initData = componentInstance.state const option = { // 聲明一個小程序邏輯的對象字面量 data: initData, onLoad() { this.$component = new ComponentClass() this.$component._init(this) update(this.$component, this.$component.state) }, onReady() { if (typeof this.$component.componentDidMount === 'function') { this.$component.componentDidMount() // 生命邏輯映射 } } } const events = ComponentClass['$$events'] // 獲取React組件內所有事件回調方法名稱 if (events) { events.forEach(eventHandlerName => { if (option[eventHandlerName]) return option[eventHandlerName] = function () { this.$component[eventHandlerName].call(this.$component) } }) } return option }
上文提到了重寫Component
類和createPage
方法具體實現邏輯如上代碼所示。
Component
內聲明的state
會執行一個update
方法,update
方法里主要是將 React 產生的新state
和舊state
進行合并,然后通過上文說的createData
方法獲取到合并后的最新state
,最新的state
再傳遞給小程序進行setData
,從而實現小程序數據渲染。
createPage
方法邏輯首先是將 React 組件實例化,然后構建出一個小程序邏輯的對應字面量,并將 React 組件實例相關方法和這個小程序邏輯對象字面量進行綁定:其次進行生命周期綁定:在小程序onReady
周期里出發 React 組件對應的componentDidMount
生命周期;最好進行事件綁定:通過上文提到的回調事件名,取出React 組件實例內的對應的事件,并將這些事件注冊到小程序邏輯的對應字面量內,這樣就完成小程序平臺事件綁定。最后將這個對象字面量返回,供前文所說的Page
方法進行注冊。
到此,就可以實現 React 代碼運行到小程序平臺了,可以在項目源碼里執行 npm run build:compile
看看效果。編譯時實現方案主要是通過靜態編譯JSX
代碼和運行時墊片結合,完成 React 代碼運行到小程序平臺,這種方案基本無性能上的損耗,且可以在運行時墊片做一些優化處理(比如去除不必要的渲染數據,減少setData數據量),因此其性能與使用小程序原生語法開發相近甚至某些場景會更優。然而這種方案的缺點就是語法限制問題(上文已經提過了),使得開發并不友好,因此也就有了運行時實現方案的誕生。
從上文可以看出,編譯時實現之所以有語法限制,主要因為其不是讓 React 真正運行到小程序平臺,而運行時實現方案則可以,其原理是在小程序平臺實現一個 React 自定義渲染器,用來渲染 React 代碼。這里我們以 remax 框架實現方式來進行講解,本項目源碼中的運行時實現也正是參照 remax 框架實現的。
如果使用過 React 開發過 Web,入口文件有一段類似這樣的代碼:
import React from 'react' import ReactDom from 'react-dom' import App from './App' ReactDom.render( App, document.getElementById('root') )
可以看出渲染 Web 頁面需要引用一個叫 react-dom
模塊,那這個模塊作用是什么?react-dom
是 Web 平臺的渲染器,主要負責將 React 執行后的Vitrual DOM
數據渲染到 Web 平臺。同樣的,React 要渲染到 Native,也有一個針對 Native 平臺的渲染器:React Native
。
React實現多平臺方式,是在每個平臺實現一個React渲染器,如下圖所示。
而如果要將 React 運行到小程序平臺,只需要開發一個小程序自定義渲染器即可。React 官方提供了一個react-reconciler 包專門來實現自定義渲染器,官方提供了一個簡單demo重寫了react-dom
。
使用react-reconciler
實現渲染器主要有兩步,第一步:實現渲染函數(render
方法),類似ReactDOM.render
方法:
import ReactReconciler from 'react-reconciler' import hostConfig from './hostConfig' // 宿主配置 // 創建Reconciler實例, 并將HostConfig傳遞給Reconciler const ReactReconcilerInst = ReactReconciler(hostConfig) /** * 提供一個render方法,類似ReactDom.render方法 * 與ReactDOM一樣,接收三個參數 * render(<MyComponent />, container, () => console.log('rendered')) */ export function render(element, container, callback) { // 創建根容器 if (!container._rootContainer) { container._rootContainer = ReactReconcilerInst.createContainer(container, false); } // 更新根容器 return ReactReconcilerInst.updateContainer(element, container._rootContainer, null, callback); }
第二步,如上圖引用的import hostConfig from './hostConfig'
,需要通過react-reconciler
實現宿主配置(HostConfig
),HostConfig
是宿主環境提供一系列適配器方案和配置項,定義了如何創建節點實例、構建節點樹、提交和更新等操作,完整列表可以點擊查看。值得注意的是在小程序平臺未提供DOM API
操作,只能通過setData
將數據傳遞給視圖層。因此Remax
重新定義了一個VNode
類型的節點,讓 React 在reconciliation
過程中不是直接去改變DOM
,而先更新VNode
,hostConfig
文件內容大致如下:
interface VNode { id: number; // 節點 id,這是一個自增的唯一 id,用于標識節點。 container: Container; // 類似 ReactDOM.render(<App />, document.getElementById('root') 中的第二個參數 children: VNode[]; // 子節點。 type: string | symbol; // 節點的類型,也就是小程序中的基礎組件,如:view、text等等。 props?: any; // 節點的屬性。 parent: VNode | null; // 父節點 text?: string; // 文本節點上的文字 appendChild(node: VNode): void; removeChild(node: VNode): void; insertBefore(newNode: VNode, referenceNode: VNode): void; ... } // 實現宿主配置 const hostConfig = { ... // reconciler提交后執行,觸發容器更新數據(實際會觸發小程序的setData) resetAfterCommit: (container) => { container.applyUpdate(); }, // 創建宿主組件實例,初始化VNode節點 createInstance(type, newProps, container) { const id = generate(); const node = new VNode({ ... }); return node; }, // 插入節點 appendChild(parent, child) { parent.appendChild(child); }, // insertBefore(parent, child, beforeChild) { parent.insertBefore(child, beforeChild); }, // 移除節點 removeChild(parent, child) { parent.removeChild(child); } ... };
除了上面的配置內容,還需要提供一個容器用來將VNode
數據格式化為JSON
數據,供小程序setData
傳遞給視圖層,這個容器類實現如下:
class Container { constructor(context) { this.root = new VNode({..}); // 根節點 } toJson(nodes ,data) { // 將VNode數據格式化JSON const json = data || [] nodes.forEach(node => { const nodeData = { type: node.type, props: node.props || {}, text: node.text, id: node.id, children: [] } if (node.children) { this.toJson(node.children, nodeData.children) } json.push(nodeData) }) return json } applyUpdate() { // 供HostConfig配置的resetAfterCommit方法執行 const root = this.toJson([this.root])[0] console.log(root) this.context.setData({ root}); } ... }
緊接著,我們封裝一個createPageConfig
方法,用來執行渲染,其中Page
參數為 React 組件,即上文計數器的組件。
import * as React from 'react'; import Container from './container'; // 上文定義的Container import render from './render'; // 上文定義的render方法 export default function createPageConfig(component) { // component為React組件 const config = { // 小程序邏輯對象字面量,供Page方法注冊 data: { root: { children: [], } }, onLoad() { this.container = new Container(this, 'root'); const pageElement = React.createElement(component, { page: this, }); this.element = render(pageElement, this.container); } }; return config; }
到這里,基本已經實現完小程序渲染器了,為了使代碼跑起來,還需要通過靜態編譯改造下 React 計數器組件,其實就是在末尾插入一句代碼:
import React, { Component } from 'react'; export default class Index extends Component { constructor() { super(); this.state = { count: 0 }; this.onAddClick = this.onAddClick.bind(this); this.onReduceClick = this.onReduceClick.bind(this); } ... } // app.js封裝了上述createPage方法 Page(require('../../npm/app.js').createPage(Index))
通過這樣,就可以使得React代碼在小程序真正運行起來了,但是這里我們還有個流程沒介紹,上述Container
類的applyUpdate
方法中生成的頁面JSON
數據要如何更新到視圖?首先我們先來看下這個JSON
數據長什么樣子:
// 篇幅問題,這里只貼部分數據 { "type": "root", "props": {}, "id": 0, "children": [{ "type": "view", "props": { "class": "container" }, "id": 12, "children": [{ "type": "view", "props": { "class": "conut" }, "id": 4, "children": [{ "type": "text", "props": {}, "id": 3, "children": [{ "type": "plain-text", "props": {}, "text": "count: ", "id": 1, "children": [] }, { "type": "plain-text", "props": {}, "text": "1", "id": 2, "children": [] }] }] } ... ... }] }
可以看出JSON
數據,其實是一棵類似Tree UI
的數據,要將這些數據渲染出頁面,可以使用小程序提供的Temlate
進行渲染,由于小程序模板遞歸嵌套會有問題(微信小程序平臺限制),因此需要提供多個同樣組件類型的模板進行遞歸渲染,代碼如下:
<template is="TPL" data="{{root: root}}" /> <!-- root為上述的JSON數據 --> <template name="TPL"> <block wx:for="{{root.children}}" wx:key="id"> <template is="TPL_1_CONTAINER" data="{{i: item, a: ''}}" /> </block> </template> <template name="TPL_1_view"> <view style="{{i.props.style}}" class="{{i.props.class}}" bindtap="{{i.props.bindtap}}" > <block wx:for="{{i.children}}" wx:key="id"> <template is="{{'TPL_' + (tid + 1) + '_CONTAINER'}}" data="{{i: item, a: a, tid: tid + 1 }}" /> </block> </view> </template> <template name="TPL_2_view"> <view style="{{i.props.style}}" class="{{i.props.class}}" bindtap="{{i.props.bindtap}}" > <block wx:for="{{i.children}}" wx:key="id"> <template is="{{'TPL_' + (tid + 1) + '_CONTAINER'}}" data="{{i: item, a: a, tid: tid + 1 }}" /> </block> </view> </template> <template name="TPL_3_view"> <view style="{{i.props.style}}" class="{{i.props.class}}" bindtap="{{i.props.bindtap}}" > <block wx:for="{{i.children}}" wx:key="id"> <template is="{{'TPL_' + (tid + 1) + '_CONTAINER'}}" data="{{i: item, a: a, tid: tid + 1 }}" /> </block> </view> </template> ... ...
至此,就可以真正實現 React 代碼運行到小程序了,可以在項目源碼里執行npm run build:runtime
看看效果。
到此,關于“React怎么構建小程序”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。