我们将在这个教程开发一个简单的 V2EX 论坛客户端,你将在本节教程学到 Taro 的基础知识、概念和部分优化技巧。学习这些知识并不需要事先了解 Taro、小程序开发或多端开发。当你掌握这些知识之后,你应该可以快速高效地开发多端应用。
这篇教程适合对喜欢边学边做或小程序开发完全没有了解的开发者,如果你倾向于按部就班地学习,请把
文档
的内容全部过一遍。当你看完
文档
就可以发现:本篇教程就是文档的渐近式索引。
这篇教程将分为四个章节:
环境准备
:当我们使用 Taro 需要安装什么东西;
基础教程
:Taro 的基础概念和开发指南;
项目进阶与优化
:当项目变大变慢时,应该如何保持或提高应用的可维护性和性能;
多端开发
:已经使用 Taro 开发了一端的应用,如何快速拓展到多端应用。
(如果你在大屏幕浏览时)在本篇教程右边的标题目录包含了所有章节的子目录,你可以查看或导航到你感兴趣的章节。
前置知识
在本篇教程中我们假定你已经对 Web 前端开发和 JavaScript 有一定的了解。我们将使用
React
和
Vue
两个框架分别实现我们的应用,在代码实例中点击
React
或
Vue
按钮就可以切换不同的实现。如果你对两个框架都不太熟悉,可以通过
React 文档
或
Vue 文档
对照代码实现。
在我们的代码实现中还会使用部分 ES6 语法,你可以通过
ES6 入门教程
查看或学习 ES6 语法。
环境准备
目前 Taro 仅提供一种开发方式:安装 Taro 命令行工具(Taro CLI)进行开发。
Taro CLI 依赖于 Node.js 环境,所以在你的机器上必须安装 Node.js 环境。安装 Node.js 环境有很多种方法,如果你完全不了解 Node.js 可以访问
Node.js 官网
下载一个可执行程序进行安装。请确保已具备较新的 node 环境(>=16.20.0)。
当你的机器已经存在了 Node.js 环境,可以通过在终端输入命令
npm i -g @tarojs/cli
安装 Taro CLI。安装完毕之后,在终端输入命令
taro
,如果出现类似内容就说明安装成功了:
👽 Taro v3.0.0-beta.6 Usage: taro < command > [ options ] Options: -V, --version output the version number -h, --help output usage information Commands: init [ projectName ] Init a project with default templete config < cmd > Taro config create Create page for project build Build a project with options update Update packages of taro info Diagnostics Taro env info doctor Diagnose taro project help [ cmd ] display help for [ cmd ]
编辑器
我们推荐使用
VSCode
或
WebStorm
(或其它支持 Web 开发的 Jetbrains IDE)。
当你使用 VSCode 时,推荐安装
ESLint 插件
,如果你使用 TypeScript,别忘了配置
eslint.probe
参数。如果使用 Vue,推荐安装
Vetur
插件。
如果你愿意花钱又懒得折腾可以选择
WebStorm
(或其它支持 Web 开发的 Jetbrains IDE),基本不需要配置。
不管使用 VSCode 还是 WebStrom,安装了上述插件之后使用 Taro 都实现自动补全和代码实时检查(linting)的功能。
终端
macOS/Linux
在
*
nix 系统中终端模拟器使用什么工具(Terminal/iTerm2/Konsole/Hyper/etc..)并不重要,但运行 Taro CLI 的 shell 我们推荐使用
bash
或
zsh
。
Windows
在 Windows 中我们推荐使用内置的
cmd
或
PowerShell
。如果有条件推荐安装
WSL
并使用 Linux 分发版的终端运行 Taro CLI。由于 Taro 的开发团队和 CI 都只运行或测试
*
nix 系统,部分 Windows 极端情况或许没能考虑到,导致出现 Bug。
寻求帮助
当你在开发过程中遇到问题时,你可以扫码加入
提问,或者去
Taro 社区
提问。当你确信是 Taro 有 Bug 时,请随时向
Taro GitHub Issue
提出你的问题。
基础教程
安装好 Taro CLI 之后可以通过
taro init
命令创建一个全新的项目,你可以根据你的项目需求填写各个选项,一个最小版本的 Taro 项目会包括以下文件:
├── babel.config.js ├── .eslintrc.js ├── config │ ├── dev.js │ ├── index.js │ └── prod.js ├── package.json ├── dist ├── project.config.json ├── src │ ├── app.config.js │ ├── app.css │ ├── app.js │ ├── index.html │ └── pages │ └── index │ ├── index.config.js │ ├── index.css │ └── index.jsx
我们以后将会讲解每一个文件的作用,但现在,我们先把注意力聚焦在
src
文件夹,也就是源码目录:
入口组件
每一个 Taro 项目都有一个入口组件和一个入口配置,我们可以在入口组件中设置全局状态/全局生命周期,一个最小化的入口组件会是这样:
src/app.js
import React , { Component } from 'react' import './app.css' class App extends Component { render ( ) { return this . props . children } } export default App
src/app.js
import Vue from 'vue' import './app.css' const App = { render ( h ) { return h ( 'block' , this . $slots . default ) } , } export default App
每一个入口组件(例如
app.js
)总是伴随一个全局配置文件(例如
app.config.js
),我们可以在全局配置文件中设置页面组件的路径、全局窗口、路由等信息,一个最简单的全局配置如下:
src/app.config.js
export default { pages : [ 'pages/index/index' ] , }
src/app.config.js
export default { pages : [ 'pages/index/index' ] , }
你可能会注意到,不管是
React
还是
Vue
,两者的全局配置是一样的。这是因为在配置文件中,Taro 并不关心框架的区别,Taro CLI 会直接在编译时在 Node.js 环境直接执行全局配置的代码,并把
export default
导出的对象序列化为一个 JSON 文件。接下来我们要讲到
页面配置
也是同样的执行逻辑。
因此,我们必须保证配置文件是在 Node.js 环境中是可以执行的,不能使用一些在 H5 环境或小程序环境才能运行的包或者代码,否则编译将会失败。
页面组件
页面组件是每一项路由将会渲染的页面,Taro 的页面默认放在
src/pages
中,每一个 Taro 项目至少有一个页面组件。在我们生成的项目中有一个页面组件:
src/pages/index/index
,细心的朋友可以发现,这个路径恰巧对应的就是我们
全局配置
的
pages
字段当中的值。一个简单的页面组件如下:
src/pages/index/index.jsx
import { View } from '@tarojs/components' class Index extends Component { state = { msg : 'Hello World!' , } onReady ( ) { console . log ( 'onReady' ) } render ( ) { return < View >
{ this . state . msg } </ View > } } export default Index
src/pages/index/index.vue
< template > < view > {{ msg }} </ view > </ template > < script > export default { data ( ) { return { msg : 'Hello World!' , } } , onReady ( ) { console . log ( 'onReady' ) } , } </ script >
这不正是我们熟悉的
React
和
Vue
组件吗!但还是有两点细微的差别:
onReady
生命周期函数。这是来源于微信小程序规范的生命周期,表示组件首次渲染完毕,准备好与视图交互。Taro 在运行时将大部分小程序规范页面生命周期注入到了页面组件中,同时 React 或 Vue 自带的生命周期也是完全可以正常使用的。
View
组件。这是来源于
@tarojs/components
的跨平台组件。相对于我们熟悉的
div
、
span
元素而言,在 Taro 中我们要全部使用这样的跨平台组件进行开发。
和入口组件一样,每一个页面组件(例如
index.vue
)也会有一个页面配置(例如
index.config.js
),我们可以在页面配置文件中设置页面的导航栏、背景颜色等参数,一个最简单的页面配置如下:
src/pages/index/index.config.js
export default { navigationBarTitleText : '首页' , }
Taro 的页面钩子函数和页面配置规范是基于微信小程序而制定的,并对全平台进行统一。
你可以通过访问
React 页面组件
和
Vue 页面组件
了解全部页面钩子函数和页面配置规范。
自定义组件
如果你看到这里,那不得不恭喜你,你已经理解了 Taro 中最复杂的概念:入口组件和页面组件,并了解了它们是如何(通过配置文件)交互的。接下来的内容,如果你已经熟悉了
React
或
Vue
以及 Web 开发的话,那就太简单了:
我们先把首页写好,首页的逻辑很简单:把论坛最新的帖子展示出来。
src/pages/index/index.jsx
import Taro from '@tarojs/taro' import React from 'react' import { View } from '@tarojs/components' import { ThreadList } from '../../components/thread_list' import api from '../../utils/api' import './index.css' class Index extends React . Component { config = { navigationBarTitleText : '首页' , } state = { loading : true , threads : [ ] , } async componentDidMount ( ) { try { const res = await Taro . request ( { url : api . getLatestTopic ( ) , } ) this . setState ( { threads : res . data , loading : false , } ) } catch ( error ) { Taro . showToast ( { title : '载入远程数据错误' , } ) } }
render ( ) { const { loading , threads } = this . state return ( < View className = " index " > < ThreadList threads = { threads } loading = { loading } /> </ View > ) } } export default Index
src/pages/index/index.vue
< template > < view class = " index " > < thread-list :threads = " threads " :loading = " loading " /> </ view > </ template > < script > import Vue from 'vue' import Taro from '@tarojs/taro' import api from '../../utils/api' import ThreadList from '../../components/thread_list.vue' export default { components : { 'thread-list' : ThreadList , } , data ( ) { return { loading : true , threads : [ ] , } } , async created ( ) { try {
const res = await Taro . request ( { url : api . getLatestTopic ( ) , } ) this . loading = false this . threads = res . data } catch ( error ) { Taro . showToast ( { title : '载入远程数据错误' , } ) } } , } </ script >
可能你会注意到在一个 Taro 应用中发送请求是
Taro.request()
完成的。
和页面配置、全局配置一样,Taro 的 API 规范也是基于微信小程序而制定的,并对全平台进行统一。
你可以通过在
API 文档
找到所有 API。
在我们的首页组件里,还引用了一个
ThreadList
组件,我们现在来实现它:
src/components/thread_list.jsx
import React from 'react' import { View , Text } from '@tarojs/components' import { Thread } from './thread' import { Loading } from './loading' import './thread.css' class ThreadList extends React . Component { static defaultProps = { threads : [ ] , loading : true , }
render ( ) { const { loading , threads } = this . props if ( loading ) { return < Loading /> } const element = threads . map ( ( thread , index ) => { return ( < Thread key = { thread . id } node = { thread . node } title = { thread . title } last_modified = { thread . last_modified } replies = { thread . replies } tid = { thread . id } member = { thread . member } /> ) } ) return < View className = " thread-list " > { element } </ View > } } export { ThreadList }
src/components/thread.jsx
import Taro , { eventCenter } from '@tarojs/taro' import React from 'react' import { View , Text , Navigator , Image } from '@tarojs/components' import api from '../utils/api' import { timeagoInst , Thread_DETAIL_NAVIGATE } from '../utils' class Thread extends React . Component
{ handleNavigate = ( ) => { const { tid , not_navi } = this . props if ( not_navi ) { return } eventCenter . trigger ( Thread_DETAIL_NAVIGATE , this . props ) Taro . navigateTo ( { url : '/pages/thread_detail/thread_detail' , } ) } render ( ) { const { title , member , last_modified , replies , node , not_navi } = this . props const time = timeagoInst . format ( last_modified * 1000 , 'zh' ) const usernameCls = ` author ${ not_navi ? 'bold' : '' } ` return ( < View className = " thread " onClick = { this . handleNavigate } > < View className = " info " > < View > < Image src = { member . avatar_large } className = " avatar " /> </ View > < View className = " middle " > < View className = { usernameCls } > { member . username } </ View > < View className = " replies " > < Text className = " mr10 " > { time } </ Text > < Text > 评论 { replies } </ Text > </ View >
</ View > < View className = " node " > < Text className = " tag " > { node . title } </ Text > </ View > </ View > < Text className = " title " > { title } </ Text > </ View > ) } } export { Thread }
src/components/thread_list.vue
< template > < view className = " thread-list " > < loading v-if = " loading " /> < thread v-else v-for = " t in threads " :key = " t.id " :node = " t.node " :title = " t.title " :last_modified = " t.last_modified " :replies = " t.replies " :tid = " t.id " :member = " t.member " /> </ view > </ template > < script > import Vue from 'vue' import Loading from './loading.vue' import Thread from './thread.vue' export default { components : { loading : Loading
, thread : Thread , } , props : { threads : { type : Array , default : [ ] , } , loading : { type : Boolean , default : true , } , } , } </ script >
src/components/thread.vue
< template > < view class = " thread " @tap = " handleNavigate " > < view class = " info " > < view > < image :src = " member.avatar_large | url " class = " avatar " /> </ view > < view class = " middle " > < view :class = " usernameCls " > {{member.username}} </ view > < view class = " replies " > < text class = " mr10 " > {{time}} </ text > < text > 评论 {{replies}} </ text > </ view > </ view > < view class = " node " > < text class = " tag " > {{node.title}} </ text > </
view > </ view > < text class = " title " > {{title}} </ text > </ view > </ template > < script > import Vue from 'vue' import { eventCenter } from '@tarojs/taro' import Taro from '@tarojs/taro' import { timeagoInst , Thread_DETAIL_NAVIGATE } from '../utils' import './thread.css' export default { props : [ 'title' , 'member' , 'last_modified' , 'replies' , 'node' , 'not_navi' , 'tid' ] , computed : { time ( ) { return timeagoInst . format ( this . last_modified * 1000 , 'zh' ) } , usernameCls ( ) { return ` author ${ this . not_navi ? 'bold' : '' } ` } , } , filters : { url ( val ) { return 'https:' + val } , } , methods : { handleNavigate ( ) {
const { tid , not_navi } = this . $props if ( not_navi ) { return } eventCenter . trigger ( Thread_DETAIL_NAVIGATE , this . $props ) Taro . navigateTo ( { url : '/pages/thread_detail/thread_detail' , } ) } , } , } </ script >
这里可以发现我们把论坛帖子渲染逻辑拆成了两个组件,并放在
src/components
文件中,因为这些组件是会在其它页面中多次用到。
拆分组件的力度是完全由开发者决定的,Taro 并没有规定组件一定要放在
components
文件夹,也没有规定页面一定要放在
pages
文件夹。
另外一个值得注意的点是:我们并没有使用
div
/
span
这样的 HTML 组件,而是使用了
View
/
Text
这样的跨平台组件。
Taro 文档的
跨平台组件库
包含了所有组件参数和用法。但目前组件库文档中的参数和组件名都是针对 React 的(除了 React 的点击事件是
onClick
之外)。
对于 Vue 而言,组件名和组件参数都采用短横线风格(kebab-case)的命名方式,例如:
<picker-view indicator-class="myclass" />
路由与 Tabbar
在
src/components/thread
组件中,我们通过
Taro . navigateTo ( { url : '/pages/thread_detail/thread_detail' } )
跳转到帖子详情,但这个页面仍未实现,现在我们去入口文件配置一个新的页面:
src/app.config.js
export default { pages : [ 'pages/index/index' , 'pages/thread_detail/thread_detail' ] , }
然后在路径
src/pages/thread_detail/thread_detail
实现帖子详情页面,路由就可以跳转,我们整个流程就跑起来了:
src/pages/thread_detail/thread_detail
import Taro from '@tarojs/taro' import React
from 'react' import { View , RichText , Image } from '@tarojs/components' import { Thread } from '../../components/thread' import { Loading } from '../../components/loading' import api from '../../utils/api' import { timeagoInst , GlobalState } from '../../utils' import './index.css' function prettyHTML ( str ) { const lines = [ 'p' , 'h1' , 'h2' , 'h3' , 'h4' , 'h5' , 'h6' ] lines . forEach ( line => { const regex = new RegExp ( ` < ${ line } ` , 'gi' ) str = str . replace ( regex , ` < ${ line } class="line" ` ) } ) return str . replace ( / <img / gi , '<img class="img"' ) } class ThreadDetail extends React . Component { state = { loading : true , replies : [ ] , content : '' , thread : { } } as IState config = { navigationBarTitleText : '话题' } componentWillMount ( ) { this . setState ( { thread : GlobalState . thread } ) } async componentDidMount ( ) { try { const id = GlobalState . thread . tid
const [ { data } , { data : [ { content_rendered } ] } ] = await Promise . all ( [ Taro . request ( { url : api . getReplies ( { 'topic_id' : id } ) } ) , Taro . request ( { url : api . getTopics ( { id } ) } ) ] ) this . setState ( { loading : false , replies : data , content : prettyHTML ( content_rendered ) } ) } catch ( error ) { Taro . showToast ( { title : '载入远程数据错误' } ) } } render ( ) { const { loading , replies , thread , content } = this . state const replieEl = replies . map ( ( reply , index ) => { const time = timeagoInst . format ( reply . last_modified * 1000 , 'zh' ) return ( < View className = ' reply ' key = { reply . id } > < Image src = { reply . member . avatar_large } className = ' avatar ' /> < View className = ' main ' > < View className = ' author ' > { reply .
member . username } </ View > < View className = ' time ' > { time } </ View > < RichText nodes = { reply . content } className = ' content ' /> < View className = ' floor ' > { index + 1 } 楼 </ View > </ View > </ View > ) } ) const contentEl = loading ? < Loading /> : ( < View > < View className = ' main-content ' > < RichText nodes = { content } /> </ View > < View className = ' replies ' > { replieEl } </ View > </ View > ) return ( < View className = ' detail ' > < Thread node = { thread . node } title = { thread . title } last_modified = { thread . last_modified } replies = { thread . replies } tid = { thread . id } member = { thread . member } not_navi = { true }
/> { contentEl } </ View > ) } } export default ThreadDetail
src/pages/thread_detail/thread_detail.vue
< template > < view class = " detail " > < thread :node = " topic.node " :title = " topic.title " :last_modified = " topic.last_modified " :replies = " topic.replies " :tid = " topic.id " :member = " topic.member " :not_navi = " true " /> < loading v-if = " loading " /> < view v-else > < view class = " main-content " > < rich-text :nodes = " content | html " /> </ view > < view class = " replies " > < view v-for = " (reply, index) in replies " class = " reply " :key = " reply.id " > < image :src = " reply.member.avatar_large " class = " avatar " /> < view class = " main " > < view class = " author " > {{reply.member.username}} </ view > < view class = " time " > {{reply.last_modified | time}} </ view >
< rich-text :nodes = " reply.content_rendered | html " class = " content " /> < view class = " floor " > {{index + 1}} 楼 </ view > </ view > </ view > </ view > </ view > </ view > </ template > < script > import Vue from 'vue' import Taro from '@tarojs/taro' import api from '../../utils/api' import { timeagoInst , GlobalState , IThreadProps , prettyHTML } from '../../utils' import Thread from '../../components/thread.vue' import Loading from '../../components/loading.vue' import './index.css' export default { components : { loading : Loading , thread : Thread , } , data ( ) { return { topic : GlobalState . thread , loading : true , replies : [ ] , content : '' , } } , async created ( ) { try { const id = GlobalState . thread . tid
const [ { data } , { data : [ { content_rendered } ] , } , ] = await Promise . all ( [ Taro . request ( { url : api . getReplies ( { topic_id : id , } ) , } ) , Taro . request ( { url : api . getTopics ( { id , } ) , } ) , ] ) this . loading = false this . replies = data this . content = content_rendered } catch ( error ) { Taro . showToast ( { title : '载入远程数据错误' , } ) } } , filters : { time ( val ) { return timeagoInst . format ( val * 1000 ) } , html ( val ) { return prettyHTML ( val ) } , } , }
</ script >
到目前为止,我们已经实现了这个应用的所有逻辑,除去「节点列表」页面(在进阶指南我们会讨论这个页面组件)之外,剩下的页面都可以通过我们已经讲解过的组件或页面快速抽象完成。按照我们的计划,这个应用会有五个页面,分别是:
首页,展示最新帖子(已完成)
节点列表
热门帖子(可通过组件复用)
节点帖子 (可通过组件复用)
帖子详情 (已完成)
其中前三个页面我们可以把它们规划在
tabBar
里,
tabBar
是 Taro 内置的导航栏,可以在
app.config.js
配置,配置完成之后处于的
tabBar
位置的页面会显示一个导航栏。最终我们的
app.config.js
会是这样:
app.config.js
export default { pages : [ 'pages/index/index' , 'pages/nodes/nodes' , 'pages/hot/hot' , 'pages/node_detail/node_detail' , 'pages/thread_detail/thread_detail' , ] , tabBar : { list : [ { iconPath : 'resource/latest.png' , selectedIconPath : 'resource/lastest_on.png' , pagePath : 'pages/index/index' , text : '最新' , } , { iconPath : 'resource/hotest.png' , selectedIconPath : 'resource/hotest_on.png' , pagePath : 'pages/hot/hot' , text : '热门' , } , { iconPath : 'resource/node.png' , selectedIconPath : 'resource/node_on.png' , pagePath : 'pages/nodes/nodes' , text : '节点' , } , ] , color : '#000' , selectedColor : '#56abe4' , backgroundColor : '#fff' , borderStyle : 'white' , } , window : { backgroundTextStyle : 'light' , navigationBarBackgroundColor : '#fff' , navigationBarTitleText : 'V2EX' , navigationBarTextStyle : 'black' , } , }
项目进阶与优化
状态管理
在我们实现帖子组件(
src/components/thread
)时,通过 Taro 内置的
eventCenter
发起了一个事件,把当前帖子的数据注入到一个全局的
GlobalState
中,然后在帖子详情页面再从
GlobalState
取出当前帖子的数据——这种简单的发布/订阅模式在处理简单逻辑时非常有效且清晰。
一旦我们的业务逻辑变得复杂,一个简单的发布订阅机制绑定到一个全局的
state
可能就会导致我们的数据流变得难以追踪。好在这个问题不管是在 React 还是 Vue 社区中都有很好的解决方案。我们会使用这两个社区最热门的状态管理工具:
Redux
和
Vuex
来解决这个问题。
首先安装
redux
和
react-redux
:
在入口文件使用
react-redux
的
Provider
注入
context
到我们的应用:
src/app.js
import React , { Component } from 'react' import { Provider } from 'react-redux' import { createStore , combineReducers } from 'redux' import './app.css' const reducers = combineReducers ( { thread : ( state = { } , action ) => { if ( action . type === 'SET_CURRENT_THREAD' ) { return { ... state , ... action . thread , } } return state } , } ) const store = createStore ( reducers ) class App extends Component { render ( ) { return < Provider store = { store } > { this . props . children } < / Provider > } } export default App
然后在帖子组件中我们就可以通过
connect
一个
dispatch
设置当前的帖子:
src/components/thread.jsx
- eventCenter.trigger(Thread_DETAIL_NAVIGATE, this.props) + this.props.setThread(this.props) - export default Thread + const mapDispatchToProps = dispatch => { + return { + setThread: thread => dispatch({ type: 'SET_CURRENT_THREAD', thread }) + } + } + export default connect(null, mapDispatchToProps)(Thread)
在帖子详情组件中通过
connect
一个
mapStateToProps
获取当前帖子的数据:
src/components/thread_detail.jsx
- const id = GlobalState.thread.tid + const id = this.props.thread.tid - export default ThreadDetail + function mapStateToProps(state) { + return { thread: state.thread } + } + export default connect(mapStateToProps)(ThreadDetail)
首先安装
vuex
:
在入口文件中注入 Vuex 的
store
:
src/app.js
import Vue from 'vue' import Vuex from 'vuex' import './app.css' const store = new Vuex . Store ( { state : { thread : { } , } , mutations : { setThread : ( state , thread ) => { state . thread = { ... thread } } , } , } ) const App = { store , render ( h ) { return h ( 'block' , this . $slots . default ) } , } export default App
然后在帖子组件中我们就可以通过
this.$store.setThread()
设置当前的帖子:
src/components/thread.vue
- eventCenter.trigger(Thread_DETAIL_NAVIGATE, this.props) + this.$store.setThread(this.$props)
在帖子详情组件中通过
computed
获取当前帖子的数据:
src/components/thread_detail.vue
{
data () { return { - topic: GlobalState.thread, loading: true, replies: [], content: '' } }, + computed: { + topic() { + return this.$store.state.thread + } + } }
此教程演示的是
Vuex
极简用法,而非最佳实践。详情请访问
Vuex 文档
。
其它状态管理工具
原理上来说,Taro 可以支持任何兼容 React 或 Vue 的状态管理工具,使用这类工具通常都会要求在入口组件注入
context
,而在 Taro 中入口文件是不能渲染 UI 的。只要注意这点即可。
在 Vue 生态圈我们推荐使用
Vuex
。React 生态圈状态管理工具百花齐放,考虑到使用 Taro 的开发者很多应用会编译到小程序,我们推荐几个在性能或体积上有优势的状态管理工具:
mobx-react
: 和 Vuex 一样响应式的状态管理工具
unstaged
: 基于 React Hooks 的极简状态管理工具,压缩体积只有 200 字节
Recoil
: Facebook 推出的基于 React Hooks 的状态管理工具
CSS 工具
在 Taro 中,我们可以自由地使用 CSS 预处理器和后处理器,使用的方法也非常简单,只要在编译配置添加相关的插件即可:
config/index.js
const config = { projectName : 'v2ex' , date : '2018-8-3' , designWidth : 750 , sourceRoot : 'src' , outputRoot : 'dist' , plugins : [ '@tarojs/plugin-sass' , ] , defineConstants : { } , mini : { } , h5 : { publicPath : '/' , staticDirectory : 'static' , module : { postcss : { autoprefixer : { enable : true , } , } , } , } , } module . exports = function ( merge ) { if ( process . env . NODE_ENV === 'development' ) { return merge ( { } , config , require ( './dev' ) ) } return merge ( { } , config , require ( './prod' ) ) }