正当移动互联网进入白热化阶段时,以微信小程序为代表的一类“轻应用”异军突起。它们无需下载,使用方便,“用完即走”,同时功能也较为完备,一经推出即得到了各大平台和及用户的热烈追捧。但是问题也随之而来——开发者们要同时维护 Web 端、移动端、微信小程序、支付宝小程序等等多套用户界面,其维护成本可以想象。作为一个优秀的多端统一开发解决方案,Taro 的出现则改变了这一情况。正值 Taro 2.x 进入 beta 阶段,让我们沏上一杯茶,开始我们的 Taro 多端小程序开发之旅吧。
起步
对于国内 React 开发者来说,Taro 的出现无疑是福音——它能够让我们用熟悉的 React 代码去搭建各类小程序,并且一份代码可以编译成多个平台的应用(目前包括微信小程序、支付宝小程序、React Native、H5 等等)。随着 Taro 的不断进化,它对 React 代码的支持程度越来越好,所支持的目标平台也越来越多,学习的价值自然不必多言。正值 Taro 进入 2.0.0 版本的 beta 阶段,我们在这一篇教程将手把手带你实现一个能够部署到多端的小程序,让你感受 Taro 的强大与魅力!
《一杯茶的时间,上手 Taro 多端小程序开发》分上下两部分:
- 上篇(也就是这篇)
- 用熟悉的 React 代码编写用户界面
- 使用 React Hooks 重构状态管理
- 用自带路由功能搭建多页面应用
- 下篇(✍️写作中)
- 利用 Taro UI 组件库,让界面专业美观
- 用 Redux 管理业务数据流
- 利用小程序云进行后端开发
- 编译为多端应用,并进行部署
我们还提供了项目仓库的 GitHub 地址。项目目前还在开发阶段,您可以跳转到任意一次 commit 查看当前步骤的所有代码哦。
我们将构建什么?
我们将构建一个具有多页面的多端小程序应用——奥特曼俱乐部(Ultraman Club,简称 UltraClub)。在完成上篇后,项目的 GIF 动图展示如下:
具体有三个页面:
- 主页:展示了所有帖子,以及添加新帖子的按钮。
- 帖子详情:展示单个帖子的全部内容。
- 个人主页:展示当前用户的个人信息,如果未登录则提示登录。
在部署方面,我们最终会将应用编译成微信小程序、支付宝小程序以及 H5(在下篇中实现哦)。
前提条件
在阅读这篇教程之前,我们希望你已经具备以下知识:
- 了解 HTML、CSS、JavaScript 的基础知识,如果了解 Sass 就更好了
- 了解 React 框架的基础知识,可以参考这篇教程进行学习;如果接触过 React Native 以及 Hooks 则更好了
- 了解并已经安装好 Node 与 npm,可以参考这篇教程进行学习
除此之外,你还需要下载并安装微信开发者工具,这里是下载地址。
用 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
函数中我们用的不再是 div
和 p
标签,而是 Taro 为我们准备好的 View
和 Text
组件。为什么 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.jsx
和 index.scss
。index.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.jsx
和 index.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;
}
为了方便在页面组件中使用 PostCard
和 PostForm
组件,我们把 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
:当前所有的帖子,每个帖子是一个包含title
和content
的对象 -
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
上面。
你尽可以打开模拟器试一下重构之后效果,看看功能是否与上一步完全一致哦!
来一打页面
在这一步中,我们将开始实现项目的其他页面,包括:
- 帖子详情
post
:进入单篇帖子的详情页面 - 我的
mine
:显示当前用户的个人信息(在后面的步骤中将实现登录注册哦)
其中,帖子详情页面中将复用前面编写的 PostCard
组件。为了方便管理,我们需要引入一个新的 prop(isList
),用于判断此组件是显示在首页列表中,还是在帖子详情页面中。
Taro 的路由功能
路由功能是实现多页面应用的核心,幸运的是 Taro 已经自带了。具体而言,在 Taro 中实现页面跳转只需两个步骤:
- 在入口文件(
src/app.jsx
)中在App
组件的config
中配置之前提到的pages
属性 - 在任意组件中通过
Taro.navigateTo
或Taro.redirectTo
即可实现页面的跳转或重定向
感觉不够直观?OK,我们直接撸起袖子写起来。
配置全部页面
首先在入口文件 src/app.jsx
中配置好所有页面:
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', 'pages/mine/mine', 'pages/post/post'],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black',
},
tabBar: {
list: [
{
pagePath: 'pages/index/index',
text: '首页',
iconPath: './images/home.png',
selectedIconPath: './images/homeSelected.png',
},
{
pagePath: 'pages/mine/mine',
text: '我的',
iconPath: './images/mine.png',
selectedIconPath: './images/mineSelected.png',
},
],
},
}
// 在 App 类中的 render() 函数没有实际作用
// 请勿修改此函数
render() {
return <Index />
}
}
Taro.render(<App />, document.getElementById('app'))
注意到我们还在 config
中注册了导航栏 tabBar
,用来在底部切换 index
页面和 mine
页面。
在 PostCard 中添加跳转逻辑
我们首先在 PostCard
组件中添加跳转逻辑,使得它被点击后将进入该帖子的详情页面。将 src/components/PostCard/index.jsx
按如下代码进行修改:
import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'
import './index.scss'
export default function PostCard(props) {
const handleClick = () => {
// 如果是列表,那么就响应点击事件,跳转到帖子详情
if (props.isList) {
const { title, content } = this.props
Taro.navigateTo({
url: `/pages/post/post?title=${title}&content=${content}`,
})
}
}
return (
<View className="postcard" onClick={handleClick}>
<View className="post-title">{props.title}</View>
<View className="post-content">{props.content}</View>
</View>
)
}
可以看到,我们在 PostCard
中注册了 handleClick
用于响应点击事件。在 handleClick
函数中,我们通过新引入的 isList
属性判断这个组件是否展示在首页列表中。如果是的话,就通过 Taro.navigateTo
进行跳转。
提示
眼尖的你一定发现了我们在调用
navigateTo
时还加上了查询字符串用于传递参数。在接下来实现帖子详情页面时,我们就可以接收到传递进来的title
和content
的值啦。
接着我们需要在首页模块中给 PostCard
组件加上 isList
。修改 src/pages/index/index.jsx
,代码如下:
// import 语句 ...
export default function Index() {
// 定义状态和 handleSubmit 函数 ...
return (
<View className="index">
{posts.map((post, index) => (
<PostCard
key={index}
title={post.title}
content={post.content}
isList
/>
))}
<PostForm
formTitle={formTitle}
formContent={formContent}
handleSubmit={e => handleSubmit(e)}
handleTitleInput={e => setFormTitle(e.target.value)}
handleContentInput={e => setFormContent(e.target.value)}
/>
</View>
)
}
// ...
实现“帖子详情”页面
在 src/pages
中创建 post
目录,然后在其中创建 post.jsx 和 post.scss,分别为页面模块和样式文件。post.jsx 代码如下:
import Taro, { useRouter } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { PostCard } from '../../components'
import './post.scss'
export default function Post() {
const router = useRouter()
const { params } = router
return (
<View className="post">
<PostCard title={params.title} content={params.content} />
</View>
)
}
Post.config = {
navigationBarTitleText: '帖子详情',
}
注意到我们用了 useRouter
这个 Hook(Taro 专有),它用来在函数组件中获取 router
,等同于之前类组件中的 this.$router
。有了 router
,我们就可以获取到在刚才 PostCard
组件跳转时传进来的 title
和 content
参数了。
post.scss 的代码如下:
.mine {
margin: 30px;
border: 1px solid #ddd;
text-align: center;
height: 90vh;
padding-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.mine-avatar {
width: 200px;
height: 200px;
border-radius: 50%;
}
.mine-nickName {
font-size: 40;
margin-top: 20px;
}
.mine-username {
font-size: 32px;
margin-top: 16px;
color: #777;
}
.mine-footer {
font-size: 28px;
color: #777;
margin-bottom: 20px;
}
实现“我的”页面
接着我们实现“我的”页面。创建 src/pages/mine
目录,在其中创建 mine.jsx 和 mine.scss。页面组件 mine.jsx 代码如下:
import Taro from '@tarojs/taro'
import { View, Image } from '@tarojs/components'
import './mine.scss'
import avatar from '../../images/avatar.png'
export default function Mine() {
return (
<View className="mine">
<View>
<Image src={avatar} className="mine-avatar" />
<View className="mine-nickName">图雀酱</View>
<View className="mine-username">tuture</View>
</View>
<View className="mine-footer">From 图雀社区 with Love ❤</View>
</View>
)
}
Mine.config = {
navigationBarTitleText: '我的',
}
样式文件 mine.scss 代码如下:
.mine {
margin: 30px;
border: 1px solid #ddd;
text-align: center;
height: 90vh;
padding-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.mine-avatar {
width: 200px;
height: 200px;
border-radius: 50%;
}
.mine-nickName {
font-size: 40;
margin-top: 20px;
}
.mine-username {
font-size: 32px;
margin-top: 16px;
color: #777;
}
.mine-footer {
font-size: 28px;
color: #777;
margin-bottom: 20px;
}
查看效果
又到了激动人心的验收环节。我们应该能看到下面所示的效果:
至此,《Taro 一杯茶》上篇也就结束啦。我们会马上推出下篇,手把手带大家用 Taro UI 升级界面,用 Redux 管理业务数据流,并接入小程序云,最后同时打包为微信小程序、支付宝小程序以及 H5 应用,敬请期待~
想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。
网友评论