Taro 基礎教程

2021-09-30 17:35 更新

安裝好 Taro CLI 之后可以通過 ?taro init? 命令創(chuàng)建一個全新的項目,你可以根據你的項目需求填寫各個選項,一個最小版本的 Taro 項目會包括以下文件:

  1. ├── babel.config.js # Babel 配置
  2. ├── .eslintrc.js # ESLint 配置
  3. ├── config # 編譯配置目錄
  4. │ ├── dev.js # 開發(fā)模式配置
  5. │ ├── index.js # 默認配置
  6. │ └── prod.js # 生產模式配置
  7. ├── package.json # Node.js manifest
  8. ├── dist # 打包目錄
  9. ├── project.config.json # 小程序項目配置
  10. ├── src # 源碼目錄
  11. │ ├── app.config.js # 全局配置
  12. │ ├── app.css # 全局 CSS
  13. │ ├── app.js # 入口組件
  14. │ ├── index.html # H5 入口 HTML
  15. │ └── pages # 頁面組件
  16. │ └── index
  17. │ ├── index.config.js # 頁面配置
  18. │ ├── index.css # 頁面 CSS
  19. │ └── index.jsx # 頁面組件,如果是 Vue 項目,此文件為 index.vue

我們以后將會講解每一個文件的作用,但現在,我們先把注意力聚焦在 ?src? 文件夾,也就是源碼目錄:

入口組件

每一個 Taro 項目都有一個入口組件和一個入口配置,我們可以在入口組件中設置全局狀態(tài)/全局生命周期,一個最小化的入口組件會是這樣:

  1. import React, { Component } from 'react'
  2. import './app.css'
  3. class App extends Component {
  4. render () {
  5. // this.props.children 是將要會渲染的頁面
  6. return this.props.children
  7. }
  8. }
  9. // 每一個入口組件都必須導出一個 React 組件
  10. export default App
  1. import Vue from 'vue'
  2. import './app.css'
  3. const App = new Vue({
  4. render(h) {
  5. // this.$slots.default 是將要會渲染的頁面
  6. return h('block', this.$slots.default)
  7. }
  8. })
  9. export default App

每一個入口組件(例如 ?app.js?)總是伴隨一個全局配置文件(例如 ?app.config.js?),我們可以在全局配置文件中設置頁面組件的路徑、全局窗口、路由等信息,一個最簡單的全局配置如下:

  1. export default {
  2. pages: [
  3. 'pages/index/index'
  4. ]
  5. }
  1. export default {
  2. pages: [
  3. 'pages/index/index'
  4. ]
  5. }

你可能會注意到,不管是 還是 ,兩者的全局配置是一樣的。這是在配置文件中,Taro 并不關心框架的區(qū)別,Taro CLI 會直接在編譯時在 Node.js 環(huán)境直接執(zhí)行全局配置的代碼,并把 ?export default? 導出的對象序列化為一個 JSON 文件。接下來我們要講到 頁面配置 也是同樣的執(zhí)行邏輯。

因此,我們必須保證配置文件是在 Node.js 環(huán)境中是可以執(zhí)行的,不能使用一些在 H5 環(huán)境或小程序環(huán)境才能運行的包或者代碼,否則編譯將會失敗。

了解更多 Taro 的入口組件和全局配置規(guī)范是基于微信小程序而制定的,并對全平臺進行統(tǒng)一。 你可以通過訪問? React 入口組件? 和? Vue 入口組件?,以及?全局配置?了解入口組件和全局配置的詳情。 

頁面組件

頁面組件是每一項路由將會渲染的頁面,Taro 的頁面默認放在 ?src/pages? 中,每一個 Taro 項目至少有一個頁面組件。在我們生成的項目中有一個頁面組件:?src/pages/index/index?,細心的朋友可以發(fā)現,這個路徑恰巧對應的就是我們 全局配置 的 ?pages? 字段當中的值。一個簡單的頁面組件如下:

  1. import { View } from '@tarojs/components'
  2. class Index extends Component {
  3. state = {
  4. msg: 'Hello World!'
  5. }
  6. onReady () {
  7. console.log('onReady')
  8. }
  9. render () {
  10. return <View>{ this.state.msg }</View>
  11. }
  12. }
  13. export default Index
  1. <template>
  2. <view>
  3. {{ msg }}
  4. </view>
  5. </template>
  6. <script>
  7. export default {
  8. data() {
  9. return {
  10. msg: 'Hello World!'
  11. };
  12. },
  13. onReady () {
  14. console.log('onReady')
  15. }
  16. };
  17. </script>

這不正是我們熟悉的 ?onReady? 和 ?View? 組件嗎!但還是有兩點細微的差別:

  1. ?onReady? 生命周期函數。這是來源于微信小程序規(guī)范的生命周期,表示組件首次渲染完畢,準備好與視圖交互。Taro 在運行時將大部分小程序規(guī)范頁面生命周期注入到了頁面組件中,同時 React 或 Vue 自帶的生命周期也是完全可以正常使用的。
  2. ?View? 組件。這是來源于 ?@tarojs/components? 的跨平臺組件。相對于我們熟悉的 ?div?、?span? 元素而言,在 Taro 中我們要全部使用這樣的跨平臺組件進行開發(fā)。

和入口組件一樣,每一個頁面組件(例如 ?index.vue?)也會有一個頁面配置(例如 ?index.config.js?),我們可以在頁面配置文件中設置頁面的導航欄、背景顏色等參數,一個最簡單的頁面配置如下:

  1. export default {
  2. navigationBarTitleText: '首頁'
  3. }
了解更多 Taro 的頁面鉤子函數和頁面配置規(guī)范是基于微信小程序而制定的,并對全平臺進行統(tǒng)一。 你可以通過訪問 ?React 入口組件? 和? Vue 入口組件?,了解全部頁面鉤子函數和頁面配置規(guī)范。 

自定義組件

如果你看到這里,那不得不恭喜你,你已經理解了 Taro 中最復雜的概念:入口組件和頁面組件,并了解了它們是如何(通過配置文件)交互的。接下來的內容,如果你已經熟悉了 或 以及 Web 開發(fā)的話,那就太簡單了:

我們先把首頁寫好,首頁的邏輯很簡單:把論壇最新的帖子展示出來。

  1. import Taro from '@tarojs/taro'
  2. import React from 'react'
  3. import { View } from '@tarojs/components'
  4. import { ThreadList } from '../../components/thread_list'
  5. import api from '../../utils/api'
  6. import './index.css'
  7. class Index extends React.Component {
  8. config = {
  9. navigationBarTitleText: '首頁'
  10. }
  11. state = {
  12. loading: true,
  13. threads: []
  14. }
  15. async componentDidMount () {
  16. try {
  17. const res = await Taro.request({
  18. url: api.getLatestTopic()
  19. })
  20. this.setState({
  21. threads: res.data,
  22. loading: false
  23. })
  24. } catch (error) {
  25. Taro.showToast({
  26. title: '載入遠程數據錯誤'
  27. })
  28. }
  29. }
  30. render () {
  31. const { loading, threads } = this.state
  32. return (
  33. <View className='index'>
  34. <ThreadList
  35. threads={threads}
  36. loading={loading}
  37. />
  38. </View>
  39. )
  40. }
  41. }
  42. export default Index
  1. <template>
  2. <view class='index'>
  3. <thread-list
  4. :threads="threads"
  5. :loading="loading"
  6. />
  7. </view>
  8. </template>
  9. <script>
  10. import Vue from 'vue'
  11. import Taro from '@tarojs/taro'
  12. import api from '../../utils/api'
  13. import ThreadList from '../../components/thread_list.vue'
  14. export default {
  15. components: {
  16. 'thread-list': ThreadList
  17. },
  18. data () {
  19. return {
  20. loading: true,
  21. threads: []
  22. }
  23. },
  24. async created() {
  25. try {
  26. const res = await Taro.request({
  27. url: api.getLatestTopic()
  28. })
  29. this.loading = false
  30. this.threads = res.data
  31. } catch (error) {
  32. Taro.showToast({
  33. title: '載入遠程數據錯誤'
  34. })
  35. }
  36. }
  37. }
  38. </script>
了解更多 可能你會注意到在一個 Taro 應用中發(fā)送請求是 ?Taro.request()? 完成的。 和頁面配置、全局配置一樣,Taro 的 API 規(guī)范也是基于微信小程序而制定的,并對全平臺進行統(tǒng)一。 你可以通過在 ?API 文檔? 找到所有 API。 

在我們的首頁組件里,還引用了一個 ?ThreadList? 組件,我們現在來實現它:

  1. import React from 'react'
  2. import { View, Text } from '@tarojs/components'
  3. import { Thread } from './thread'
  4. import { Loading } from './loading'
  5. import './thread.css'
  6. class ThreadList extends React.Component {
  7. static defaultProps = {
  8. threads: [],
  9. loading: true
  10. }
  11. render () {
  12. const { loading, threads } = this.props
  13. if (loading) {
  14. return <Loading />
  15. }
  16. const element = threads.map((thread, index) => {
  17. return (
  18. <Thread
  19. key={thread.id}
  20. node={thread.node}
  21. title={thread.title}
  22. last_modified={thread.last_modified}
  23. replies={thread.replies}
  24. tid={thread.id}
  25. member={thread.member}
  26. />
  27. )
  28. })
  29. return (
  30. <View className='thread-list'>
  31. {element}
  32. </View>
  33. )
  34. }
  35. }
  36. export { ThreadList }
  1. import Taro, { eventCenter } from '@tarojs/taro'
  2. import React from 'react'
  3. import { View, Text, Navigator, Image } from '@tarojs/components'
  4. import api from '../utils/api'
  5. import { timeagoInst, Thread_DETAIL_NAVIGATE } from '../utils'
  6. class Thread extends React.Component {
  7. handleNavigate = () => {
  8. const { tid, not_navi } = this.props
  9. if (not_navi) {
  10. return
  11. }
  12. eventCenter.trigger(Thread_DETAIL_NAVIGATE, this.props)
  13. // 跳轉到帖子詳情
  14. Taro.navigateTo({
  15. url: '/pages/thread_detail/thread_detail'
  16. })
  17. }
  18. render () {
  19. const { title, member, last_modified, replies, node, not_navi } = this.props
  20. const time = timeagoInst.format(last_modified * 1000, 'zh')
  21. const usernameCls = `author ${not_navi ? 'bold' : ''}`
  22. return (
  23. <View className='thread' onClick={this.handleNavigate}>
  24. <View className='info'>
  25. <View>
  26. <Image src={member.avatar_large} className='avatar' />
  27. </View>
  28. <View className='middle'>
  29. <View className={usernameCls}>
  30. {member.username}
  31. </View>
  32. <View className='replies'>
  33. <Text className='mr10'>
  34. {time}
  35. </Text>
  36. <Text>
  37. 評論 {replies}
  38. </Text>
  39. </View>
  40. </View>
  41. <View className='node'>
  42. <Text className='tag'>
  43. {node.title}
  44. </Text>
  45. </View>
  46. </View>
  47. <Text className='title'>
  48. {title}
  49. </Text>
  50. </View>
  51. )
  52. }
  53. }
  54. export { Thread }
  1. <template>
  2. <view className='thread-list'>
  3. <loading v-if="loading" />
  4. <thread
  5. v-else
  6. v-for="t in threads"
  7. :key="t.id"
  8. :node="t.node"
  9. :title="t.title"
  10. :last_modified="t.last_modified"
  11. :replies="t.replies"
  12. :tid="t.id"
  13. :member="t.member"
  14. />
  15. </view>
  16. </template>
  17. <script >
  18. import Vue from 'vue'
  19. import Loading from './loading.vue'
  20. import Thread from './thread.vue'
  21. export default {
  22. components: {
  23. 'loading': Loading,
  24. 'thread': Thread
  25. },
  26. props: {
  27. threads: {
  28. type: Array,
  29. default: []
  30. },
  31. loading: {
  32. type: Boolean,
  33. default: true
  34. }
  35. }
  36. }
  37. </script>
  1. <template>
  2. <view class='thread' @tap="handleNavigate">
  3. <view class='info'>
  4. <view>
  5. <image :src="member.avatar_large | url" class='avatar' />
  6. </view>
  7. <view class='middle'>
  8. <view :class="usernameCls">
  9. {{member.username}}
  10. </view>
  11. <view class='replies'>
  12. <text class='mr10'>{{time}}</text>
  13. <text>評論 {{replies}}</text>
  14. </view>
  15. </view>
  16. <view class='node'>
  17. <text class='tag'>{{node.title}}</Text>
  18. </view>
  19. </view>
  20. <text class='title'>{{title}}</text>
  21. </view>
  22. </template>
  23. <script>
  24. import Vue from 'vue'
  25. import { eventCenter } from '@tarojs/taro'
  26. import Taro from '@tarojs/taro'
  27. import { timeagoInst, Thread_DETAIL_NAVIGATE } from '../utils'
  28. import './thread.css'
  29. export default {
  30. props: ['title', 'member', 'last_modified', 'replies', 'node', 'not_navi', 'tid'],
  31. computed: {
  32. time () {
  33. return timeagoInst.format(this.last_modified * 1000, 'zh')
  34. },
  35. usernameCls () {
  36. return `author ${this.not_navi ? 'bold' : ''}`
  37. }
  38. },
  39. filters: {
  40. url (val) {
  41. return 'https:' + val
  42. }
  43. },
  44. methods: {
  45. handleNavigate () {
  46. const { tid, not_navi } = this.$props
  47. if (not_navi) {
  48. return
  49. }
  50. eventCenter.trigger(Thread_DETAIL_NAVIGATE, this.$props)
  51. // 跳轉到帖子詳情
  52. Taro.navigateTo({
  53. url: '/pages/thread_detail/thread_detail'
  54. })
  55. }
  56. }
  57. }
  58. </script>

這里可以發(fā)現我們把論壇帖子渲染邏輯拆成了兩個組件,并放在 ?src/components? 文件中,因為這些組件是會在其它頁面中多次用到。 拆分組件的力度是完全由開發(fā)者決定的,Taro 并沒有規(guī)定組件一定要放在 ?components? 文件夾,也沒有規(guī)定頁面一定要放在 ?pages? 文件夾。

另外一個值得注意的點是:我們并沒有使用 ?div?/?span? 這樣的 HTML 組件,而是使用了 ?View?/?Text? 這樣的跨平臺組件。

了解更多 Taro 文檔的?跨平臺組件庫? 包含了所有組件參數和用法。但目前組件庫文檔中的參數和組件名都是針對 React 的(除了 React 的點擊事件是 ?onClick? 之外)。 對于 Vue 而言,組件名和組件參數都采用短橫線風格(kebab-case)的命名方式,例如: <picker-view indicator-class="myclass" /> 

路由與 Tabbar

在 ?src/components/thread? 組件中,我們通過

  1. Taro.navigateTo({ url: '/pages/thread_detail/thread_detail' })

跳轉到帖子詳情,但這個頁面仍未實現,現在我們去入口文件配置一個新的頁面:

  1. export default {
  2. pages: [
  3. 'pages/index/index',
  4. 'pages/thread_detail/thread_detail'
  5. ]
  6. }

然后在路徑 ?src/pages/thread_detail/thread_detail? 實現帖子詳情頁面,路由就可以跳轉,我們整個流程就跑起來了:

  1. import Taro from '@tarojs/taro'
  2. import React from 'react'
  3. import { View, RichText, Image } from '@tarojs/components'
  4. import { Thread } from '../../components/thread'
  5. import { Loading } from '../../components/loading'
  6. import api from '../../utils/api'
  7. import { timeagoInst, GlobalState } from '../../utils'
  8. import './index.css'
  9. function prettyHTML (str) {
  10. const lines = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']
  11. lines.forEach(line => {
  12. const regex = new RegExp(`<${line}`, 'gi')
  13. str = str.replace(regex, `<${line} class="line"`)
  14. })
  15. return str.replace(/<img/gi, '<img class="img"')
  16. }
  17. class ThreadDetail extends React.Component {
  18. state = {
  19. loading: true,
  20. replies: [],
  21. content: '',
  22. thread: {}
  23. } as IState
  24. config = {
  25. navigationBarTitleText: '話題'
  26. }
  27. componentWillMount () {
  28. this.setState({
  29. thread: GlobalState.thread
  30. })
  31. }
  32. async componentDidMount () {
  33. try {
  34. const id = GlobalState.thread.tid
  35. const [{ data }, { data: [ { content_rendered } ] } ] = await Promise.all([
  36. Taro.request({
  37. url: api.getReplies({
  38. 'topic_id': id
  39. })
  40. }),
  41. Taro.request({
  42. url: api.getTopics({
  43. id
  44. })
  45. })
  46. ])
  47. this.setState({
  48. loading: false,
  49. replies: data,
  50. content: prettyHTML(content_rendered)
  51. })
  52. } catch (error) {
  53. Taro.showToast({
  54. title: '載入遠程數據錯誤'
  55. })
  56. }
  57. }
  58. render () {
  59. const { loading, replies, thread, content } = this.state
  60. const replieEl = replies.map((reply, index) => {
  61. const time = timeagoInst.format(reply.last_modified * 1000, 'zh')
  62. return (
  63. <View className='reply' key={reply.id}>
  64. <Image src={reply.member.avatar_large} className='avatar' />
  65. <View className='main'>
  66. <View className='author'>
  67. {reply.member.username}
  68. </View>
  69. <View className='time'>
  70. {time}
  71. </View>
  72. <RichText nodes={reply.content} className='content' />
  73. <View className='floor'>
  74. {index + 1} 樓
  75. </View>
  76. </View>
  77. </View>
  78. )
  79. })
  80. const contentEl = loading
  81. ? <Loading />
  82. : (
  83. <View>
  84. <View className='main-content'>
  85. <RichText nodes={content} />
  86. </View>
  87. <View className='replies'>
  88. {replieEl}
  89. </View>
  90. </View>
  91. )
  92. return (
  93. <View className='detail'>
  94. <Thread
  95. node={thread.node}
  96. title={thread.title}
  97. last_modified={thread.last_modified}
  98. replies={thread.replies}
  99. tid={thread.id}
  100. member={thread.member}
  101. not_navi={true}
  102. />
  103. {contentEl}
  104. </View>
  105. )
  106. }
  107. }
  108. export default ThreadDetail
  1. <template>
  2. <view class='detail'>
  3. <thread
  4. :node="topic.node"
  5. :title="topic.title"
  6. :last_modified="topic.last_modified"
  7. :replies="topic.replies"
  8. :tid="topic.id"
  9. :member="topic.member"
  10. :not_navi="true"
  11. />
  12. <loading v-if="loading" />
  13. <view v-else>
  14. <view class='main-content'>
  15. <rich-text :nodes="content | html" />
  16. </view>
  17. <view class='replies'>
  18. <view v-for="(reply, index) in replies" class='reply' :key="reply.id">
  19. <image :src='reply.member.avatar_large' class='avatar' />
  20. <view class='main'>
  21. <view class='author'>
  22. {{reply.member.username}}
  23. </view>
  24. <view class='time'>
  25. {{reply.last_modified | time}}
  26. </view>
  27. <rich-text :nodes="reply.content_rendered | html" class='content' />
  28. <view class='floor'>
  29. {{index + 1}} 樓
  30. </view>
  31. </view>
  32. </view>
  33. </view>
  34. </view>
  35. </view>
  36. </template>
  37. <script>
  38. import Vue from 'vue'
  39. import Taro from '@tarojs/taro'
  40. import api from '../../utils/api'
  41. import { timeagoInst, GlobalState, IThreadProps, prettyHTML } from '../../utils'
  42. import Thread from '../../components/thread.vue'
  43. import Loading from '../../components/loading.vue'
  44. import './index.css'
  45. export default {
  46. components: {
  47. 'loading': Loading,
  48. 'thread': Thread
  49. },
  50. data () {
  51. return {
  52. topic: GlobalState.thread,
  53. loading: true,
  54. replies: [],
  55. content: ''
  56. }
  57. },
  58. async created () {
  59. try {
  60. const id = GlobalState.thread.tid
  61. const [{ data }, { data: [ { content_rendered } ] } ] = await Promise.all([
  62. Taro.request({
  63. url: api.getReplies({
  64. 'topic_id': id
  65. })
  66. }),
  67. Taro.request({
  68. url: api.getTopics({
  69. id
  70. })
  71. })
  72. ])
  73. this.loading = false
  74. this.replies = data
  75. this.content = content_rendered
  76. } catch (error) {
  77. Taro.showToast({
  78. title: '載入遠程數據錯誤'
  79. })
  80. }
  81. },
  82. filters: {
  83. time (val) {
  84. return timeagoInst.format(val * 1000)
  85. },
  86. html (val) {
  87. return prettyHTML(val)
  88. }
  89. }
  90. }
  91. </script>

到目前為止,我們已經實現了這個應用的所有邏輯,除去「節(jié)點列表」頁面(在進階指南我們會討論這個頁面組件)之外,剩下的頁面都可以通過我們已經講解過的組件或頁面快速抽象完成。按照我們的計劃,這個應用會有五個頁面,分別是:

  1. 首頁,展示最新帖子(已完成)
  2. 節(jié)點列表
  3. 熱門帖子(可通過組件復用)
  4. 節(jié)點帖子 (可通過組件復用)
  5. 帖子詳情 (已完成)

其中前三個頁面我們可以把它們規(guī)劃在 ?tabBar? 里,?tabBar? 是 ?Taro? 內置的導航欄,可以在 ?app.config.js? 配置,配置完成之后處于的 ?tabBar? 位置的頁面會顯示一個導航欄。最終我們的 ?app.config.js? 會是這樣:

  1. export default {
  2. pages: [
  3. 'pages/index/index',
  4. 'pages/nodes/nodes',
  5. 'pages/hot/hot',
  6. 'pages/node_detail/node_detail',
  7. 'pages/thread_detail/thread_detail'
  8. ],
  9. tabBar: {
  10. list: [{
  11. 'iconPath': 'resource/latest.png',
  12. 'selectedIconPath': 'resource/lastest_on.png',
  13. pagePath: 'pages/index/index',
  14. text: '最新'
  15. }, {
  16. 'iconPath': 'resource/hotest.png',
  17. 'selectedIconPath': 'resource/hotest_on.png',
  18. pagePath: 'pages/hot/hot',
  19. text: '熱門'
  20. }, {
  21. 'iconPath': 'resource/node.png',
  22. 'selectedIconPath': 'resource/node_on.png',
  23. pagePath: 'pages/nodes/nodes',
  24. text: '節(jié)點'
  25. }],
  26. 'color': '#000',
  27. 'selectedColor': '#56abe4',
  28. 'backgroundColor': '#fff',
  29. 'borderStyle': 'white'
  30. },
  31. window: {
  32. backgroundTextStyle: 'light',
  33. navigationBarBackgroundColor: '#fff',
  34. navigationBarTitleText: 'V2EX',
  35. navigationBarTextStyle: 'black'
  36. }
  37. }


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號