Taro 項目進階與優(yōu)化

2021-09-30 17:44 更新

狀態(tài)管理

在我們實現(xiàn)帖子組件(?src/components/thread?)時,通過 Taro 內(nèi)置的 ?eventCenter? 發(fā)起了一個事件,把當(dāng)前帖子的數(shù)據(jù)注入到一個全局的 ?GlobalState? 中,然后在帖子詳情頁面再從 ?GlobalState? 取出當(dāng)前帖子的數(shù)據(jù)——這種簡單的發(fā)布/訂閱模式在處理簡單邏輯時非常有效且清晰。

一旦我們的業(yè)務(wù)邏輯變得復(fù)雜,一個簡單的發(fā)布訂閱機制綁定到一個全局的 ?state? 可能就會導(dǎo)致我們的數(shù)據(jù)流變得難以追蹤。好在這個問題不管是在 React 還是 Vue 社區(qū)中都有很好的解決方案。我們會使用這兩個社區(qū)最熱門的狀態(tài)管理工具:?Redux? 和 ?Vuex? 來解決這個問題。

首先安裝 ?redux? 和 ?react-redux?:

  1. npm i redux react-redux

在入口文件使用 ?react-redux? 的 ?Provider? 注入 ?context? 到我們的應(yīng)用:

  1. import React, { Component } from 'react'
  2. import { Provider } from 'react-redux'
  3. import { createStore, combineReducers } from 'redux';
  4. import './app.css'
  5. const reducers = combineReducers({
  6. thread: (state = {}, action) => {
  7. if (action.type === 'SET_CURRENT_THREAD') {
  8. return {
  9. ...state,
  10. ...action.thread
  11. }
  12. }
  13. return state
  14. }
  15. })
  16. const store = createStore(reducers)
  17. class App extends Component {
  18. render () {
  19. // this.props.children 是將要會渲染的頁面
  20. return (
  21. <Provider store={store}>
  22. {this.props.children}
  23. </Provider>
  24. )
  25. }
  26. }
  27. export default App

然后在帖子組件中我們就可以通過 ?connect? 一個 ?dispatch? 設(shè)置當(dāng)前的帖子:

  1. - eventCenter.trigger(Thread_DETAIL_NAVIGATE, this.props)
  2. + this.props.setThread(this.props)
  3. - export default Thread
  4. + const mapDispatchToProps = dispatch => {
  5. + return {
  6. + setThread: thread => dispatch({ type: 'SET_CURRENT_THREAD', thread })
  7. + }
  8. + }
  9. + export default connect(null, mapDispatchToProps)(Thread)

在帖子詳情組件中通過 ?connect? 一個 ?mapStateToProps? 獲取當(dāng)前帖子的數(shù)據(jù):

  1. - const id = GlobalState.thread.tid
  2. + const id = this.props.thread.tid
  3. - export default ThreadDetail
  4. + function mapStateToProps(state) {
  5. + return { thread: state.thread }
  6. + }
  7. + export default connect(mapStateToProps)(ThreadDetail)
請注意:此教程演示的是 ?Redux? 極簡用法,而非最佳實踐。詳情請訪問Redux 文檔 和react-redux 文檔。 

首先安裝 ?vuex?:

  1. npm i vuex

在入口文件中注入 Vuex 的 ?store?:

  1. import Vue from 'vue'
  2. import './app.css'
  3. const store = new Vuex.Store({
  4. state: {
  5. thread: {}
  6. },
  7. mutations: {
  8. setThread: (state, thread) => {
  9. state.thread = { ...thread }
  10. }
  11. }
  12. })
  13. const App = new Vue({
  14. store,
  15. render(h) {
  16. // this.$slots.default 是將要會渲染的頁面
  17. return h('block', this.$slots.default)
  18. }
  19. })
  20. export default App

然后在帖子組件中我們就可以通過 ?this.$store.setThread()? 設(shè)置當(dāng)前的帖子:

  1. - eventCenter.trigger(Thread_DETAIL_NAVIGATE, this.props)
  2. + this.$store.setThread(this.$props)

在帖子詳情組件中通過 ?computed? 獲取當(dāng)前帖子的數(shù)據(jù):

  1. {
  2. data () {
  3. return {
  4. - topic: GlobalState.thread,
  5. loading: true,
  6. replies: [],
  7. content: ''
  8. }
  9. },
  10. + computed: {
  11. + topic() {
  12. + return this.$store.state.thread
  13. + }
  14. + }
  15. }
請注意:此教程演示的是 ?Vuex? 極簡用法,而非最佳實踐。詳情請訪問 Vuex 文檔。 

其它狀態(tài)管理工具

原理上來說,Taro 可以支持任何兼容 React 或 Vue 的狀態(tài)管理工具,使用這類工具通常都會要求在入口組件注入 ?context?,而在 Taro 中入口文件是不能渲染 UI 的。只要注意這點即可。

在 Vue 生態(tài)圈我們推薦使用 ?Vuex?。React 生態(tài)圈狀態(tài)管理工具百花齊放,考慮到使用 Taro 的開發(fā)者很多應(yīng)用會編譯到小程序,我們推薦幾個在性能或體積上有優(yōu)勢的狀態(tài)管理工具:

  • mobx-react: 和 Vuex 一樣響應(yīng)式的狀態(tài)管理工具
  • unstaged: 基于 React Hooks 的極簡狀態(tài)管理工具,壓縮體積只有 200 字節(jié)
  • Recoil: Facebook 推出的基于 React Hooks 的狀態(tài)管理工具

CSS 工具

在 Taro 中,我們可以自由地使用 CSS 預(yù)處理器和后處理器,使用的方法也非常簡單,只要在編譯配置添加相關(guān)的插件即可:

  1. const config = {
  2. projectName: 'v2ex',
  3. date: '2018-8-3',
  4. designWidth: 750,
  5. sourceRoot: 'src',
  6. outputRoot: 'dist',
  7. plugins: [
  8. '@tarojs/plugin-sass', // 使用 Sass
  9. // '@tarojs/plugin-less', // 使用 Less
  10. // '@tarojs/plugin-stylus', // 使用 Stylus
  11. ],
  12. defineConstants: {
  13. },
  14. mini: {
  15. },
  16. h5: {
  17. publicPath: '/',
  18. staticDirectory: 'static',
  19. module: {
  20. postcss: {
  21. autoprefixer: {
  22. enable: true
  23. }
  24. }
  25. }
  26. }
  27. }
  28. module.exports = function (merge) {
  29. if (process.env.NODE_ENV === 'development') {
  30. return merge({}, config, require('./dev'))
  31. }
  32. return merge({}, config, require('./prod'))
  33. }
了解更多: 除了 CSS 預(yù)處理器之外,Taro 還支持 CSS Modules 和 CSS-in-JS。 原理上還支持更多 CSS 工具,我們將在下面的自定義編譯繼續(xù)討論這個問題。 

渲染 HTML

在帖子詳情組件(?ThreadDetail?)中,我們使用了內(nèi)置組件 ?RichText? 來渲染 HTML,但這個組件的兼容性不好,無法在所有端都正常使用,某些特定的 HTML 元素也無法渲染。

幸運的是,Taro 內(nèi)置了 HTML 渲染,使用方法也和 React/Vue 在 Web 開發(fā)中沒什么區(qū)別:

  1. - <RichText nodes={reply.content} className='content' />
  2. + <View dangerouslySetInnerHTML={{ __html: reply.content }} className='content'></View>
  1. - <rich-text :nodes="reply.content_rendered | html" class='content' />
  2. + <view v-html="reply.content_rendered | html" class='content' />
info 了解更多 Taro 內(nèi)置的 HTML 渲染功能不僅可以按 Web 開發(fā)的方式去使用,也支持自定義樣式、自定義渲染、自定義事件這樣的高級功能。 你可以訪問 HTML 渲染文檔 了解更多。 

性能優(yōu)化

虛擬列表

在帖子列表組件(?ThreadList?)中,我們直接渲染從遠程得來的數(shù)據(jù)。這樣做沒有什么問題,但如果我們的數(shù)據(jù)非常龐大,或者列表渲染的 DOM 結(jié)構(gòu)異常復(fù)雜,這就可能會產(chǎn)生性能問題。

為了解決這一問題,Taro 內(nèi)置了虛擬列表(?VirtualList?)功能,比起全量渲染所有列表數(shù)據(jù),我們只需要渲染當(dāng)前可視區(qū)域(visable viewport)的視圖:

  1. import React from 'react'
  2. import { View, Text } from '@tarojs/components'
  3. import { Thread } from './thread'
  4. import { Loading } from './loading'
  5. import VirtualList from `@tarojs/components/virtual-list`
  6. import './thread.css'
  7. const Row = React.memo(({ thread }) => {
  8. return (
  9. <Thread
  10. key={thread.id}
  11. node={thread.node}
  12. title={thread.title}
  13. last_modified={thread.last_modified}
  14. replies={thread.replies}
  15. tid={thread.id}
  16. member={thread.member}
  17. />
  18. )
  19. })
  20. class ThreadList extends React.Component {
  21. static defaultProps = {
  22. threads: [],
  23. loading: true
  24. }
  25. render () {
  26. const { loading, threads } = this.props
  27. if (loading) {
  28. return <Loading />
  29. }
  30. const element = (
  31. <VirtualList
  32. height={800} /* 列表的高度 */
  33. width='100%' /* 列表的寬度 */
  34. itemData={threads} /* 渲染列表?的數(shù)據(jù) */
  35. itemCount={threads.length} /* 渲染列表的長度 */
  36. itemSize={100} /* 列表單項的高度 */
  37. >
  38. {Row} /* 列表單項組件,這里只能傳入一個組件 */
  39. </VirtualList>
  40. )
  41. return (
  42. <View className='thread-list'>
  43. {element}
  44. </View>
  45. )
  46. }
  47. }
  48. export { ThreadList }
  1. // 在入口文件新增使用插件
  2. import VirtualList from `@tarojs/components/virtual-list`
  3. Vue.use(VirtualList)
  1. <template>
  2. <thread
  3. :key="data.id"
  4. :node="data.node"
  5. :title="data.title"
  6. :last_modified="data.last_modified"
  7. :replies="data.replies"
  8. :tid="data.id"
  9. :member="data.member"
  10. />
  11. </template>
  12. <script>
  13. import Thread from './thread.vue'
  14. export default {
  15. components: {
  16. 'thread': Thread
  17. },
  18. props: ['index', 'data', 'css']
  19. }
  20. </script>
  1. <template>
  2. <view className='thread-list'>
  3. <loading v-if="loading" />
  4. <virtual-list
  5. v-else
  6. :height="500"
  7. :item-data="threads"
  8. :item-count="threads.length"
  9. :item-size="100"
  10. :item="Row"
  11. width="100%"
  12. />
  13. </view>
  14. </template>
  15. <script >
  16. import Vue from 'vue'
  17. import Loading from './loading.vue'
  18. import Thread from './thread.vue'
  19. import Row from './row.vue'
  20. export default {
  21. components: {
  22. 'loading': Loading,
  23. 'thread': Thread
  24. },
  25. props: {
  26. threads: {
  27. type: Array,
  28. default: []
  29. },
  30. loading: {
  31. type: Boolean,
  32. default: true
  33. }
  34. }
  35. }
  36. </script>
了解更多:在文檔 虛擬列表 你可以找到虛擬列表的一些高級用法,例如:無限滾動、滾動偏移、滾動事件等。 

預(yù)渲染

現(xiàn)在我們來實現(xiàn)最后一個頁面:節(jié)點列表頁面。這個頁面本質(zhì)說就是渲染一個存在本地的巨大列表:

  1. import React from 'react'
  2. import { View, Text, Navigator } from '@tarojs/components'
  3. import allNodes from './all_node'
  4. import api from '../../utils/api'
  5. import './nodes.css'
  6. function Nodes () {
  7. const element = allNodes.map(item => {
  8. return (
  9. <View key={item.title} className='container'>
  10. <View className='title'>
  11. <Text style='margin-left: 5px'>{item.title}</Text>
  12. </View>
  13. <View className='nodes'>
  14. {item.nodes.map(node => {
  15. return (
  16. <Navigator
  17. className='tag'
  18. url={`/pages/node_detail/node_detail${api.queryString(node)}`}
  19. key={node.full_name}
  20. >
  21. <Text>{node.full_name}</Text>
  22. </Navigator>
  23. )
  24. })}
  25. </View>
  26. </View>
  27. )
  28. })
  29. return <View className='node-container'>{element}</View>
  30. }
  31. export default Nodes
  1. <template>
  2. <view class='node-container'>
  3. <view v-for="item in allNodes" :key="item.title" class='container'>
  4. <view class='title'>
  5. <text style='margin-left: 5px'>{{item.title}}</text>
  6. </view>
  7. <view class='nodes'>
  8. <navigator
  9. v-for="node in item.nodes"
  10. :key="node.full_name"
  11. class='tag'
  12. :url="node | url"
  13. >
  14. <text>{{node.full_name}}</text>
  15. </navigator>
  16. </view>
  17. </view>
  18. </view>
  19. </template>
  20. <script>
  21. import Vue from 'vue'
  22. import allNodes from './all_node'
  23. import api from '../../utils/api'
  24. import './nodes.css'
  25. function getURL (node) {
  26. return `/pages/node_detail/node_detail${api.queryString(node)}`
  27. }
  28. export default {
  29. data () {
  30. return {
  31. allNodes
  32. }
  33. },
  34. filters: {
  35. url (node) {
  36. return getURL(node)
  37. }
  38. }
  39. }
  40. </script>

這個時候我們整個應(yīng)用就完成了。但如果你把這個應(yīng)用放在真機小程序中,尤其是一些性能不高的真機中,切換到此頁面的時間可能會比較長,會有一段白屏?xí)r間。

這是由于 Taro 的渲染機制導(dǎo)致的:在頁面初始化時,原生小程序可以從本地直接取數(shù)據(jù)渲染,但 Taro 會把初始數(shù)據(jù)通過 React/Vue 渲染成一顆 DOM 樹,然后將這顆 DOM 樹序列化之后交給小程序渲染。也就是說,比起原生小程序 Taro 會在頁面初始化時多一次調(diào)用 ?setData? 函數(shù)的支出——而大部分小程序的性能問題是 ?setData? 數(shù)據(jù)過大導(dǎo)致的。

為了解決這個問題,Taro 引入了一種名為預(yù)渲染(Prerender)的技術(shù),和服務(wù)端渲染一樣,在 Taro CLI 直接將要渲染的頁面轉(zhuǎn)換為 ?wxml? 字符串,這樣就獲得了與原生小程序一致甚至更快的速度。

使用預(yù)渲染也非常簡單,我們只要進行簡單的配置即可:

  1. const config = {
  2. ...
  3. mini: {
  4. prerender: {
  5. include: ['pages/nodes/nodes'], // `pages/nodes/nodes` 也會參與 prerender
  6. }
  7. }
  8. };
  9. // 我們這里在編譯生產(chǎn)模式時才開啟預(yù)渲染
  10. // 如果需要開發(fā)時也開啟,那就把配置放在 `config/index` 或 `config/dev`
  11. module.exports = config
了解更多: 預(yù)渲染的配置支持條件渲染頁面、條件渲染邏輯、自定義渲染函數(shù)等功能,詳情可以訪問預(yù)渲染文檔。 

打包體積

默認而言使用生產(chǎn)模式打包,Taro 就會給你優(yōu)化打包體積。但值得注意,Taro 默認的打包配置是為了讓多數(shù)項目和需求都可以運行,而不是針對任何項目的最優(yōu)選擇。因此你可以在 Taro 配置的基礎(chǔ)之上再針對自己的項目進行優(yōu)化。

JavaScript

在 Taro 應(yīng)用中,所有 Java(Type)Script 都是通過 ?babel.config.js配置的,具體來說是使用 babel-prest-taro 這個 Babel 插件編譯的。

默認而言 Taro 會兼容所有 @babel/preset-env 支持的語法,并兼容到 ?iOS 9? 和 ?Android 5?,如果你不需要那么高的兼容性,或者不需要某些 ES2015+ 語法支持,可以自行配置 ?babel.config.js? 達到縮小打包體積效果。

例如我們可以把兼容性提升到 ?iOS 12?:

  1. // babel.config.js
  2. module.exports = {
  3. presets: [
  4. ['taro', {
  5. targets: {
  6. ios: '12'
  7. }
  8. }]
  9. ]
  10. }

你可以訪問 Babel 文檔 了解更多自定義配置的信息。

打包體積分析

Taro 使用 Webpack 作為內(nèi)部的打包系統(tǒng),有時候當(dāng)我們的業(yè)務(wù)代碼使用了 ?require? 語法或者 ?import default? 語法,Webpack 并不能給我們提供 ?tree-shaking? 的效果。在這樣的情況下我們通過 webpack-bundle-analyzer 來分析我們依賴打包體積,這個插件會在瀏覽器打開一個可視化的圖表頁面告訴我們引用各個包的體積。

首先安裝 ?webpack-bundle-analyzer? 依賴:

  1. npm install webpack-bundle-analyzer -D

然后在 mini.webpackChain 中添加如下配置:

  1. const config = {
  2. ...
  3. mini: {
  4. webpackChain (chain, webpack) {
  5. chain.plugin('analyzer')
  6. .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
  7. }
  8. }
  9. }

運行編譯命令完成之后就可以看到各文件依賴關(guān)系及體積。

你可以訪問 webpack-bundle-analyzer 文檔了解詳細的用法。

分包

在一些情況,我們希望我們的頁面只有當(dāng)用到時才按需進行加載。這種情況在 Taro 應(yīng)用被稱為分包,分包的使用也非常簡單,只需要通過配置入口文件 ?app.config.js? 即可。

假設(shè)我們需要把剛剛實現(xiàn)預(yù)渲染的所有節(jié)點頁面進行分包:

  1. export default {
  2. pages: [
  3. 'pages/index/index',
  4. // 'pages/nodes/nodes', 把要分包的頁面從 `pages` 字段中刪除
  5. 'pages/hot/hot',
  6. 'pages/node_detail/node_detail',
  7. 'pages/thread_detail/thread_detail'
  8. ],
  9. // 在 `subpackages` 字段添加分包
  10. "subpackages": [
  11. {
  12. "root": "pages",
  13. "pages": [
  14. "nodes/nodes"
  15. ]
  16. }
  17. ]
  18. tabBar: {
  19. list: [{
  20. 'iconPath': 'resource/latest.png',
  21. 'selectedIconPath': 'resource/lastest_on.png',
  22. pagePath: 'pages/index/index',
  23. text: '最新'
  24. }, {
  25. 'iconPath': 'resource/hotest.png',
  26. 'selectedIconPath': 'resource/hotest_on.png',
  27. pagePath: 'pages/hot/hot',
  28. text: '熱門'
  29. }, {
  30. 'iconPath': 'resource/node.png',
  31. 'selectedIconPath': 'resource/node_on.png',
  32. pagePath: 'pages/nodes/nodes',
  33. text: '節(jié)點'
  34. }],
  35. 'color': '#000',
  36. 'selectedColor': '#56abe4',
  37. 'backgroundColor': '#fff',
  38. 'borderStyle': 'white'
  39. },
  40. window: {
  41. backgroundTextStyle: 'light',
  42. navigationBarBackgroundColor: '#fff',
  43. navigationBarTitleText: 'V2EX',
  44. navigationBarTextStyle: 'black'
  45. }
  46. }

自定義編譯

在特定的情況下,Taro 自帶的編譯系統(tǒng)沒有辦法滿足我們的編譯需求,這時 Taro 提供了兩種拓展編譯的方案:

使用 Webpack 進行拓展

打包體積分析中我們在 mini.webpackChain 添加了一個 Webpack 插件,達到了打包體積/依賴分析的效果。

事實上通過 ?mini.webpackChain? 這個配置我們可以幾乎使用任何 Webpack 生態(tài)的插件和 ?loader?,例如我們想使用 ?CoffeeScript ?來進行開發(fā):

  1. const config = {
  2. ...
  3. mini: {
  4. webpackChain (chain, webpack) {
  5. chain.merge({
  6. module: {
  7. rule: {
  8. test: /\.coffee$/,
  9. use: [ 'coffee-loader' ]
  10. }
  11. }
  12. })
  13. }
  14. }
  15. }

同樣,之前我們提到過的 ?CSS Modules? 也可以通過 Webpack 的形式進行拓展支持。詳情可以訪問 webpack-chain 文檔了解詳細的用法。

使用插件化系統(tǒng)進行拓展

在 [CSS 工具](./guide#CSS 工具) 我們已經(jīng)使用了名為 ?@tarojs/plugin-sass? 的插件來實現(xiàn)對 ?Sass? 的支持。比起使用 Webpack 拓展編譯,Taro 的插件功能不用在每個端都對 Webpack 進行配置,只用使用插件即可。

除此之外,Taro 的插件化功能還可以拓展 Taro CLI 編譯命令,拓展編譯流程,拓展編譯平臺,你可以訪問 插件功能文檔 了解更多自定義配置的信息。

了解更多:除了以上兩種方式外,Taro 還提供大量的編譯相關(guān)選項,你可以訪問編譯配置詳情文檔了解更多。 


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號