亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

React怎么構建小程序

發布時間:2021-12-23 10:47:19 來源:億速云 閱讀:122 作者:iii 欄目:移動開發

這篇文章主要介紹“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 寫法。如果想迫切看到實現這個需求的效果,可點擊此項目源碼進行獲取源碼,然后根據提示運行項目,即可觀察到如下效果:

React怎么構建小程序

到這里,就清楚了知道這個項目的需求以及最終實現結果是什么,接下來便是重點闡述從需求點到結果這個過程的具體實現。

實現方案

構建小程序框架產物

開發過小程序的同學都知道,小程序框架包含主體和頁面,其中主體是由三個文件生組成的,且必須放在根目錄,這三個文件分別是: app.js (必需,小程序邏輯),app.json(必需,小程序公共配置),app.wxss(非必須,小程序公共樣式表)。所以要將 React 代碼構建成小程序代碼,首先需要先生成app.jsapp.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.jsapp.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代碼轉換程序對應的jswxml類型文件,后文會詳細闡述。

React運行到小程序平臺方案分析

實現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使用語法轉換成小程序相同功能的語法,例如:

  • 標簽元素轉換:ViewTextButton等標簽直接映射為小程序基礎組件本身(改為小寫)

  • 樣式類名轉換: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 官方提供了一個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,而先更新VNodehostConfig文件內容大致如下:

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怎么構建小程序”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

双江| 崇信县| 石屏县| 公主岭市| 泰顺县| 环江| 桂林市| 临海市| 邮箱| 苍梧县| 如东县| 贵州省| 仁化县| 霍州市| 文山县| 大城县| 郁南县| 府谷县| 盘锦市| 延津县| 中山市| 龙州县| 岑溪市| 舟曲县| 乌鲁木齐市| 同仁县| 治多县| 蓬安县| 湖北省| 石城县| 宁安市| 长海县| 五家渠市| 拜城县| 昌宁县| 普兰县| 郯城县| 阿鲁科尔沁旗| 云和县| 延边| 盖州市|