Taro 小程式開發大型實戰(一):熟悉的 React,熟悉的 Hooks

tuture發表於2020-04-17

我們研發開源了一款基於 Git 進行技術實戰教程寫作的工具,我們圖雀社群的所有教程都是用這款工具寫作而成,歡迎 Star

如果你想快速瞭解如何使用,歡迎閱讀我們的 教程文件哦

正當移動網際網路進入白熱化階段時,以微信小程式為代表的一類“輕應用”異軍突起。它們無需下載,使用方便,“用完即走”,同時功能也較為完備,一經推出即得到了各大平臺和及使用者的熱烈追捧。但是問題也隨之而來——開發者們要同時維護 Web 端、移動端、微信小程式、支付寶小程式等等多套使用者介面,其維護成本可以想象。作為一個優秀的多端統一開發解決方案,Taro 的出現則改變了這一情況。正值 Taro 2.x 進入 beta 階段,讓我們沏上一杯茶,開始我們的 Taro 多端小程式開發之旅吧。

起步

對於國內 React 開發者來說,Taro 的出現無疑是福音——它能夠讓我們用熟悉的 React 程式碼去搭建各類小程式,並且一份程式碼可以編譯成多個平臺的應用(目前包括微信小程式、支付寶小程式、React Native、H5 等等)。隨著 Taro 的不斷進化,它對 React 程式碼的支援程度越來越好,所支援的目標平臺也越來越多,學習的價值自然不必多言。正值 Taro 進入 2.0.0 版本的 beta 階段,我們在這一篇教程將手把手帶你實現一個能夠部署到多端的小程式,讓你感受 Taro 的強大與魅力!

在這一系列教程中,我們將構建一個多端小程式應用——奧特曼俱樂部(Ultraman Club,簡稱 UltraClub),一個支援多端登入(微信和支付寶)的類似貼吧的小程式。我們還提供了專案倉庫的 GitHub 地址專案目前還在開發階段,您可以跳轉到任意一次 commit 檢視當前步驟的所有程式碼哦。

我們將構建什麼?

在完成這篇教程後,專案的 GIF 動圖展示如下:

前提條件

在閱讀這篇教程之前,我們希望你已經具備以下知識:

  • 瞭解 HTML、CSS、JavaScript 的基礎知識,如果瞭解 Sass 就更好了
  • 瞭解 React 框架的基礎知識,可以參考這篇教程進行學習;如果接觸過 React Native 以及 Hooks 則更好了
  • 瞭解並已經安裝好 Node 與 npm,可以參考這篇教程進行學習

除此之外,你還需要下載並安裝微信開發者工具,這裡是下載地址

本文所涉及的原始碼都放在了 Github 上,如果您覺得我們寫得還不錯,希望您能給❤️這篇文章點贊+Github倉庫加星❤️哦~

用 Taro 腳手架初始化專案

首先安裝 Taro CLI:

npm install -g @tarojs/cli

然後建立我們的專案:

taro init ultra-club

之後會出現一系列選項,按照下圖所示進行選擇即可(CSS 前處理器選擇 Sass,模板選擇“預設模板”,老司機可自行選擇使用 TS):

提示

本專案使用 Sass 主要是為了相容 taro-ui 的樣式,並沒有使用到 Sass 的高階特性,如果你不熟悉的話也不用擔心哦,就當成是常規的 CSS 程式碼。

進入到我們的專案目錄 ultra-club 之後,可以看到專案模板包括以下檔案:

.
├── config                    # 專案配置
│   ├── dev.js                # 開發環境配置檔案
│   ├── index.js              # 主配置檔案
│   └── prod.js               # 生產環境配置檔案
├── package.json
├── project.config.json       # 微信小程式專案配置
└── src                       # 專案原始碼目錄
    ├── app.scss              # 根元件樣式
    ├── app.jsx               # 根元件 app
    ├── index.html            # 等待被嵌入程式碼的 HTML 文件
    └── pages                 # 頁面目錄
        └── index             # index 頁面模組
            ├── index.scss    # index 頁面樣式
            └── index.jsx     # index 頁面元件

我們主要看一下兩個程式碼檔案:src/app.jsx 以及 src/pages/index/index.jsx

初探腳手架程式碼

src/app.jsx 定義了專案的根元件 App,它的程式碼如下:

import Taro, { Component } from '@tarojs/taro'
import Index from './pages/index'

import './app.scss'

// 如果需要在 h5 環境中開啟 React Devtools
// 取消以下注釋:
// if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5')  {
//   require('nerv-devtools')
// }

class App extends Component {
  config = {
    pages: ['pages/index/index'],
    window: {
      backgroundTextStyle: 'light',
      navigationBarBackgroundColor: '#fff',
      navigationBarTitleText: 'WeChat',
      navigationBarTextStyle: 'black',
    },
  }

  // 在 App 類中的 render() 函式沒有實際作用
  // 請勿修改此函式
  render() {
    return <Index />
  }
}

Taro.render(<App />, document.getElementById('app'))

如果你熟悉 React 的話,那麼上面這段程式碼一定不難理解,只不過是把相應的地方(導包、渲染)從之前的 React 以及 ReactDOM 改成 Taro

注意

可以看到這個元件還多了一個 config 屬性,這個屬性是小程式應用專屬的。其中要重點關注的是 pages 陣列,列出了所有的頁面模組,例如這裡的 pages/index/index 就對應 src/pages/index/index.jsx。後面在實現路由時還會用到 pages 屬性。

我們再看看 src/pages/index/index.jsx。按照最佳實踐,Taro 專案中一般把頁面元件放到 src/pages 目錄中,src/pages/index 就是 index 頁面元件模組,其中 index.jsx 的程式碼如下:

import Taro, { Component } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import './index.scss'

export default class Index extends Component {
  config = {
    navigationBarTitleText: '首頁',
  }

  render() {
    return (
      <View className="index">
        <Text>Hello world!</Text>
      </View>
    )
  }
}

依舊是熟悉的 React 元件風格,只不過與普通的 React 相比,在 render 函式中我們用的不再是 divp 標籤,而是 Taro 為我們準備好的 ViewText 元件。為什麼 Taro 要自己搞一套元件庫呢?因為 Taro 的目標是星辰大海……sorry,是能夠編譯到各個平臺。只有通過制訂 Taro 自己的元件庫,才能在各個平臺的原生元件庫上蓋了一層抽象層,進而實現跨平臺的目標

提示

如果你有過 React Native 的開發經驗,那麼一定對 Taro 元件庫不陌生。

執行小程式

Taro 提供的模板程式碼直接可以執行。開啟終端,執行以下命令:

npm run dev:weapp

會出現以下提示資訊:

當看到“監聽檔案修改中…”的提示後,我們就可以開啟微信開發者工具,用微信掃碼登入後介面如下:

點選那個碩大的➕號,開始匯入我們剛才建立的 ultra-club 專案:

如上圖所示,首先切換到”匯入專案“一欄,然後點選”目錄“輸入欄右側的按鈕選擇剛才建立的 ultra-club 資料夾,最後點選右下角的”匯入“按鈕即可。

匯入成功後,微信開發者工具的介面如下圖所示:

在模擬器頁面中,看到了我們 index 頁面渲染的 Hello world;編輯器能夠檢視所有程式碼,不過通常我們用自己習慣的程式碼編輯器來開發(VSCode 真香!);偵錯程式則是類似 Chrome 的開發者工具。

一切就緒,讓我們開始動工吧!

提示

從這一步開始,我們的主要開發目標將是微信小程式,但是不要擔心,我們會在下一篇教程中演示怎麼編譯到其他平臺哦。

React 程式碼,熟悉的味道

從這一步開始,我們就來實現”奧特曼俱樂部“小程式。按照 React 中”萬物皆元件“的思想,我們抽象出兩個元件:

  • PostCard:用於展示一篇帖子,包括標題 title 和內容 content
  • PostForm:用於釋出新帖子的表單

實現 PostCard 元件

首先建立 src/components 目錄,我們的通用元件都會放在這裡面。然後建立 src/components/PostCard 元件目錄,在其中分別建立 index.jsxindex.scssindex.jsx 程式碼如下:

import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'

import './index.scss'

export default function PostCard(props) {
  return (
    <View className="postcard">
      <View className="post-title">{props.title}</View>
      <View className="post-content">{props.content}</View>
    </View>
  )
}

正如之前所說,PostCard 元件包含兩個 props:標題 title 和內容 content

PostCard 元件的樣式 index.scss 程式碼如下:

.postcard {
  margin: 30px;
  padding: 20px;
  border: 1px solid #ddd;
}

.post-title {
  font-weight: bolder;
  margin-bottom: 10px;
}

.post-content {
  font-size: medium;
  color: #666;
}

實現 PostForm 元件

接著我們實現用於建立新帖子的 PostForm 元件。在 src/components 中建立 PostForm 目錄,並在其中新增 index.jsxindex.scss 檔案。index.jsx 程式碼如下:

import Taro from '@tarojs/taro'
import { View, Form, Input, Textarea, Button } from '@tarojs/components'

import './index.scss'

export default function PostForm(props) {
  return (
    <View className="post-form">
      <View>新增新的帖子</View>
      <Form onSubmit={props.handleSubmit}>
        <View>
          <View className="form-hint">標題</View>
          <Input
            className="input-title"
            type="text"
            placeholder="點選輸入標題"
            value={props.formTitle}
            onInput={props.handleTitleInput}
          />
          <View className="form-hint">正文</View>
          <Textarea
            placeholder="點選輸入正文"
            className="input-content"
            value={props.formContent}
            onInput={props.handleContentInput}
          />
          <Button className="form-button" formType="submit" type="primary">
            提交
          </Button>
        </View>
      </Form>
    </View>
  )
}

PostForm 元件一共定義了五個 props,分別如下:

  • formTitle:當前編輯中帖子的標題
  • formContent:當前編輯中帖子的內容
  • handleSubmit:處理提交表單的回撥函式
  • handleTitleInput:處理標題接收到使用者輸入時的回撥函式
  • handleContentInput:處理內容接收到使用者輸入時的回撥函式

提示

如果你不熟悉 React,可能會對上面編寫表單的方式有點困惑。實際上,React 推薦用”受控元件“的方式編寫表單,可參考這篇文件

PostForm 的樣式檔案 index.scss 的程式碼如下:

.post-form {
  border: 1px solid #ddd;
  margin: 30px;
  padding: 30px;
}

.input-title {
  border: 1px solid #eee;
  padding: 10px;
  font-size: medium;
}

.input-content {
  border: 1px solid #eee;
  padding: 10px;
  height: 200px;
  font-size: medium;
}

.form-hint {
  font-size: small;
  color: gray;
  margin-top: 20px;
  margin-bottom: 10px;
}

.form-button {
  margin-top: 40px;
}

為了方便在頁面元件中使用 PostCardPostForm 元件,我們把 src/components 變成一個模組。具體地,建立 src/components/index.jsx,程式碼如下:

import PostCard from './PostCard'
import PostForm from './PostForm'

export { PostCard, PostForm }

在 index 頁面中接入 PostCard 和 PostForm

最後在 src/pages/index/index.jsx 檔案中加入之前寫好的 PostCard 和 PostForm 元件,程式碼如下:

import Taro, { Component } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { PostCard, PostForm } from '../../components'
import './index.scss'

export default class Index extends Component {
  state = {
    posts: [
      {
        title: '泰羅奧特曼',
        content: '泰羅是奧特之父和奧特之母唯一的親生兒子。',
      },
    ],
    formTitle: '',
    formContent: '',
  }

  config = {
    navigationBarTitleText: '首頁',
  }

  handleSubmit(e) {
    e.preventDefault()

    const { formTitle: title, formContent: content } = this.state
    const newPosts = this.state.posts.concat({ title, content })

    this.setState({
      posts: newPosts,
      formTitle: '',
      formContent: '',
    })
  }

  handleTitleInput(e) {
    this.setState({
      formTitle: e.target.value,
    })
  }

  handleContentInput(e) {
    this.setState({
      formContent: e.target.value,
    })
  }

  render() {
    return (
      <View className="index">
        {this.state.posts.map((post, index) => (
          <PostCard key={index} title={post.title} content={post.content} />
        ))}
        <PostForm
          formTitle={this.state.formTitle}
          formContent={this.state.formContent}
          handleSubmit={e => this.handleSubmit(e)}
          handleTitleInput={e => this.handleTitleInput(e)}
          handleContentInput={e => this.handleContentInput(e)}
        />
      </View>
    )
  }
}

可以看到,除了接入之前定義的兩個元件外,我們還加入了一些狀態:

  • posts:當前所有的帖子,每個帖子是一個包含 titlecontent 的物件
  • formTitle:當前正在編輯的帖子的標題
  • formContent:當前正在編輯的帖子的內容

以及定義了 PostForm 元件中所需要的三個回撥函式。

檢視效果

如果之前的開發伺服器還開啟著,那麼微信開發者工具應該就能直接看到效果了(如果剛才關了,可以執行 npm run dev:weapp 再次開啟哦):

注意

有時候 Taro 可能會出現樣式載入失敗的問題。如果你遇到了,可以關閉開發伺服器,重新執行 npm run dev:weapp

Hooks 輕裝上陣

自從 React 團隊在 2018 年的 React Conf 引入了 Hooks 之後,前端圈無疑是經歷了一場地震。僅僅只需幾個 API,就輕鬆地用純函式的方式搞定了元件的狀態管理和資料流,這是何等的神仙操作?

幸運的是,Taro 團隊也在 v1.3.0 版本中新增了對 Hooks 的支援。因此,我們也將在本專案中用 Hooks 解決狀態管理和資料流的問題。

Hooks 之 useState 快速複習

本文在這裡簡單地過一遍 useState Hook,如果你已經很熟悉了,請直接移步下面的動手環節。

比如我們之前有這麼一個類元件 ClickMe,它會抱怨你點了它多少次:

class ClickMe extends Component {
  state = { count: 0 }

  render() {
    return (
      <div>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          你點了我 {this.state.count} 次!
        </button>
      </div>
    )
  }
}

用 Hooks 改寫之後,就變成了一個函式式元件:

// 記得匯入 useState 函式
import Taro, { useState } from '@tarojs/taro'

function ClickMe() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>你點了我 {count} 次!</button>
    </div>
  )
}

可以看到,useState 函式返回了兩個值:

  • 狀態(也就是上面的 count):可以在渲染時直接使用
  • 修改狀態的函式(也就是上面的 setCount):用於在處理相應事件時,通過傳入新的狀態來更新狀態

還注意到 useState 接受一個引數,即狀態的初始值。這裡我們取了一個 Number 型別,事實上還可以是字串、陣列、物件等等。

動手環節

到了動手環節,我們用 useState 來重構我們的 index 頁面。具體地,我們將整個 Index 元件轉換成函式式元件,然後之前的三個狀態都用 useState 來建立,程式碼如下:

import Taro, { useState } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { PostCard, PostForm } from '../../components'
import './index.scss'

export default function Index() {
  const [posts, setPosts] = useState([
    {
      title: '泰羅奧特曼',
      content: '泰羅是奧特之父和奧特之母唯一的親生兒子。',
    },
  ])
  const [formTitle, setFormTitle] = useState('')
  const [formContent, setFormContent] = useState('')

  function handleSubmit(e) {
    e.preventDefault()

    const newPosts = posts.concat({ title: formTitle, content: formContent })
    setPosts(newPosts)
    setFormTitle('')
    setFormContent('')
  }

  return (
    <View className="index">
      {posts.map((post, index) => (
        <PostCard key={index} title={post.title} content={post.content} />
      ))}
      <PostForm
        formTitle={formTitle}
        formContent={formContent}
        handleSubmit={e => handleSubmit(e)}
        handleTitleInput={e => setFormTitle(e.target.value)}
        handleContentInput={e => setFormContent(e.target.value)}
      />
    </View>
  )
}

Index.config = {
  navigationBarTitleText: '首頁',
}

注意

由於我們把 Index 從類元件改造成了函式元件,所以掛載 config 要在 Index 元件定義之後直接掛載在 Index 上面。

你儘可以開啟模擬器試一下重構之後效果,看看功能是否與上一步完全一致哦!在接下來的第二篇中,我們將進一步實現多頁面跳轉,並用 Taro UI 元件庫升級我們的介面。

想要學習更多精彩的實戰技術教程?來圖雀社群逛逛吧。

本文所涉及的原始碼都放在了 Github 上,如果您覺得我們寫得還不錯,希望您能給❤️這篇文章點贊+Github倉庫加星❤️哦~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

圖雀社群

相關文章