Taro 小程序开发大型实战(一):熟悉的 React,熟悉的 Hooks

正当移动互联网进入白热化阶段时,以微信小程序为表明的一类“轻应用”异军突起。它们无需下载,使用方便,“用完即走”,同时功能也较为完备,一经推出即获得了各大平台和及用户的热烈追捧。可是问题也随之而来——开发者们要同时维护 Web 端、移动端、微信小程序、支付宝小程序等等多套用户界面,其维护成本能够想象。做为一个优秀的多端统一开发解决方案,Taro 的出现则改变了这一状况。正值 Taro 2.x 进入 beta 阶段,让咱们沏上一杯茶,开始咱们的 Taro 多端小程序开发之旅吧。javascript

若是您以为咱们写得还不错,记得 点赞 + 关注 + 评论 三连,鼓励咱们写出更好的教程💪

起步

对于国内 React 开发者来讲,Taro 的出现无疑是福音——它可以让咱们用熟悉的 React 代码去搭建各种小程序,而且一份代码能够编译成多个平台的应用(目前包括微信小程序、支付宝小程序、React Native、H5 等等)。随着 Taro 的不断进化,它对 React 代码的支持程度愈来愈好,所支持的目标平台也愈来愈多,学习的价值天然没必要多言。正值 Taro 进入 2.0.0 版本的 beta 阶段,咱们在这一篇教程将手把手带你实现一个可以部署到多端的小程序,让你感觉 Taro 的强大与魅力!css

在这一系列教程中,咱们将构建一个多端小程序应用——奥特曼俱乐部(Ultraman Club,简称 UltraClub),一个支持多端登陆(微信和支付宝)的相似贴吧的小程序。咱们还提供了项目仓库的 GitHub 地址项目目前还在开发阶段,您能够跳转到任意一次 commit 查看当前步骤的全部代码哦。html

咱们将构建什么?

在完成这篇教程后,项目的 GIF 动图展现以下:前端

前提条件

在阅读这篇教程以前,咱们但愿你已经具有如下知识:java

  • 了解 HTML、CSS、JavaScript 的基础知识,若是了解 Sass 就更好了
  • 了解 React 框架的基础知识,能够参考这篇教程进行学习;若是接触过 React Native 以及 Hooks 则更好了
  • 了解并已经安装好 Node 与 npm,能够参考这篇教程进行学习

除此以外,你还须要下载并安装微信开发者工具,这里是下载地址react

本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦~

用 Taro 脚手架初始化项目

首先安装 Taro CLI:git

npm install -g @tarojs/cli

而后建立咱们的项目:github

taro init ultra-club

以后会出现一系列选项,按照下图所示进行选择便可(CSS 预处理器选择 Sass,模板选择“默认模板”,老司机可自行选择使用 TS):npm

提示

本项目使用 Sass 主要是为了兼容 taro-ui 的样式,并无使用到 Sass 的高级特性,若是你不熟悉的话也不用担忧哦,就当成是常规的 CSS 代码。json

进入到咱们的项目目录 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仓库加星❤️哦~

相关文章
相关标签/搜索