ignite-note

Ignite简介

Ignite是一个基于ReactNative的手脚架和基础代码的生成工具, 使用它可以从0开始构建一个ReactNative的项目,并且可以为生成基础项目、为项目添加plugins,以及自动生成新页面和对应redux和相关sagas代码并自动插入到router等地方而不需要手动复制。

关于Ignite相关教程实际上早就想写一下,之前6月初通过Google搜索一直没有找到比较好用的step by step教程,然而到现在了暂时也还是没看到。索性我自己写一个吧。

安装和命令行的使用一瞥

安装

1
$ npm install -g ignite-cli

初始化新项目

1
$ ignite new PizzaApp

添加和移除plugins

1
2
3
4
$ ignite add maps
$ ignite add vector-icons
$ ignite add i18n
$ ignite remove i18n

自动构建代码

1
2
3
4
5
$ ignite generate screen PizzaLocationList
$ ignite generate component PizzaLocation
$ ignite generate map StoreLocator
$ ignite generate redux home
$ ignite generate saga home

前面的可能只需要知道就可以了,但是ignite generate这个命令很值得一说,因为项目开发过程中使用它可以提高很多效率避免了很多手动copy过程。

  • ignite generate screen 在App/Containers页面生成一个Container,这个相当于HTML里面的一个页面。
  • ignite generate component 在App/Components页面生成一个组件
  • ignite generate map 生成地图,这个暂时没用过
  • ignite generate redux 生成redux
  • ignite generate sagas 生成sagas

其次是运行环境要求
ignite需要先配置好reactNative的运行环境和node环境(node7.6+, 看文档8.0+有点小问题,所以最好还是用7.6+吧)。

运行机制

在用Ignite开始做项目之前我们需要先了解一下它的结构和运行机制。
我们先初始化一个Demo项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ignite new Demo
🔥 igniting app Demo
✔ using the Infinite Red boilerplate v2 (code name 'Andross')
✔ added React Native 0.47.2 in 193.29s
? Would you like Ignite Development Screens? No
? What vector icon library will you use? react-native-vector-icons
? What internationalization library will you use? none
? What animation library will you use? react-native-animatable
✔ added ignite-ir-boilerplate in 171.26s
✔ added ignite-vector-icons in 29.13s
✔ added ignite-animatable in 27.4s
✔ added ignite-standard in 64.23s
✔ configured git
✔ ignited Demo in 604.97s

项目结构

先看看第一层目录,它是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── App
├── README.md
├── Tests
├── android
├── app.json
├── ignite
├── index.android.js
├── index.ios.js
├── ios
├── node_modules
├── package.json
├── storybook
└── yarn.lock

所有的业务逻辑大抵都在App里面,所以重点说App里面的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.
├── Components 组件
│   ├── SearchBar.js
│   └── Styles
├── Config 配置
│   ├── AppConfig.js
│   ├── DebugConfig.js
│   ├── README.md
│   ├── ReactotronConfig.js
│   ├── ReduxPersist.js
│   └── index.js
├── Containers 容器&页面
│   ├── App.js
│   ├── LaunchScreen.js
│   ├── README.md
│   ├── RootContainer.js
│   └── Styles
├── Fixtures 数据mock的本地json
│   ├── README.md
│   ├── gantman.json
│   ├── rateLimit.json
│   ├── root.json
│   └── skellock.json
├── Images 图片
├── Lib 库
│   └── README.md
├── Navigation 导航、router react-navigation
│   ├── AppNavigation.js
│   ├── ReduxNavigation.js
│   └── Styles
├── Redux
│   ├── StartupRedux.js
│   └── index.js
├── Sagas
│   ├── GithubSagas.js
│   ├── StartupSagas.js
│   └── index.js
├── Services 服务 主要是ajax,数据mock,持久化相关的逻辑层
│   ├── Api.js
│   ├── ExamplesRegistry.js
│   ├── FixtureApi.js
│   ├── ImmutablePersistenceTransform.js
│   └── RehydrationServices.js
├── Themes 主题
│   ├── ApplicationStyles.js
│   ├── Colors.js
│   └── index.js
└── Transforms
├── ConvertFromKelvin.js
└── README.md

这里仅保留大致结构,很多文件对展示结构无关紧要就删掉了,实际上还有很多图片、组件等等。这里仅做原理的分析。
Ignite提供的框架做了很多工作,它提供的功能包括: 主题、国际化、redux、redux-sagas、数据本地模拟、数据持久化(redux-persist)、测试、Reactotron调试等等。

由于这是一片step by step的教程式的笔记,所以主要从使用的角度做分析,不做过于复杂的分析。这个结构实际上依然是MVC结构,其中M用Redux实现、V用react-native实现、C用Sagas实现。简单说,reactNative负责了页面,redux负责向页面传递数据,而sogas则负责业务逻辑并更改redux最终实现页面的变更。

配置Ignite

这里对一些前期需要知道的文件做简要介绍。

首先是Config/DebugConfig.js,任何程序编写过程都无法跳过debug环节,对一个新程序的编写也必然需要debug来查看内部数据变化,

1
2
3
4
5
6
7
/*file:Config/DebugConfig.js*/
useFixtures: true, // 使用本地数据,这里设为true让我们可以在没有后端支持的情况实现数据mock
ezLogin: false, // 暂时还不知道这个ezlogin干嘛的
yellowBox: __DEV__, // 黄屏警告 开发模式默认打开
reduxLogging: __DEV__, // redux-logging当redux变更时打log 开发模式默认打开
includeExamples: __DEV__, // 载入初始的includeExamples 开发模式默认打开
useReactotron: __DEV__ // 使用useReactotron 开发模式默认打开

这里我们打开useFixtures设为true即可。

Ignite的Sagas

接下来是C层了解下这里怎么处理的。打开Sagas/index.js,里面的内容不算复杂。需要关注的代码就三行

1
2
3
4
5
6
...
const api = DebugConfig.useFixtures ? FixtureAPI : API.create()
...
takeLatest(StartupTypes.STARTUP, startup)
takeLatest(GithubTypes.USER_REQUEST, getUserAvatar, api)
...

这里注意到api变量, 这里是设置数据mock的关键地方, 当useFixtures设为true的时候使用FixtureAPI否则使用API.create(),这里api可以视为一个代理, 这也决定了FixtureAPI和API.create()实际上返回的数据具有相同的数据结构。

然后就是takeLatest这个辅助函数的事情了
Saga 辅助函数其实有两个,takeEvery也可以从import那里加上:

  • takeEvery :允许多个 fetchData实例同时启动。在某个特定时刻,我们可以启动一个新的 fetchData任务, 尽管之前还有一个或多个 fetchData尚未结束。我认为这个函数非常适合一次性从服务器拉取多个数据,然后放到redux保存备用。
  • takeLatest :只允许执行一个 fetchData任务。并且这个任务是最后被启动的那个。 如果之前已经有一个任务在执行,那之前的这个任务会自动被取消。

这里唯一需要说下的takeLatest参数问题了, takeLatest接受三个参数,第一个是ReduxAction, 第二个是这个ReduxAction触发的异步函数,第三个参数是可选的。当第三个参数存在,传入这个异步函数的第一个参数是传入的第三个实参,第二个是action, 如果不存在第一个就是action。就像这样:

1
2
function * startup (action) {}
function * getUserAvatar (api, action) {}

这里再简单分析下数据获取的问题,参考Sagas/GithubSagas.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Sagas/GithubSagas.js
import { call, put } from 'redux-saga/effects'
import { path } from 'ramda'
import GithubActions from '../Redux/GithubRedux'
export function * getUserAvatar (api, action) {
const { username } = action
// make the call to the api
const response = yield call(api.getUser, username)
if (response.ok) {
const firstUser = path(['data', 'items'], response)[0]
const avatar = firstUser.avatar_url
// do data conversion here if needed
yield put(GithubActions.userSuccess(avatar))
} else {
yield put(GithubActions.userFailure())
}
}

其中action是之前的state数据, 后面的则是直接根据异步获取的数据执行对应redux的Action。
接下来看看api.getUser相关,这里是获取数据的方法,我们看看里面是如何获取数据的。上面说过,这里获取数据有两个地方,一个是真实的一个mock的,我们先看真实的数据获取。

数据获取

真实的数据获取使用apisauce包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实例化
const create = (baseURL = 'https://api.github.com/') => {
const api = apisauce.create({
// base URL is read from the "constructor"
baseURL,
// here are some default headers
headers: {
'Cache-Control': 'no-cache'
},
// 10 second timeout...
timeout: 10000
})
const getRate = () => api.get('rate_limit’)
return {
getRate
}
}
export default {
create
}

这里是不算复杂的实例化,创建一个数据获取接口,设定了url, baseURL, httpHeader, 以及超时时间。
一些使用例子, 更多的例子可以直接查阅apisauce文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const api = create({baseURL: 'https://api.github.com'})
const api = create({baseURL: 'https://example.com/api/v3'})
const api = create({
baseURL: '...',
headers: {
'X-API-KEY': '123',
'X-MARKS-THE-SPOT': 'yarrrrr'
}
})
const api = create({baseURL: '...', timeout: 30000}) // 30 seconds
api.get('/repos/skellock/apisauce/commits')
api.head('/me')
api.delete('/users/69')
api.post('/todos', {note: 'jump around'}, {headers: {'x-ray': 'machine'}})
api.patch('/servers/1', {live: false})
api.put('/servers/1', {live: true})
api.link('/images/my_dog.jpg', {}, {headers: {Link: '<http://example.com/profiles/joe>; rel="tag"'}})
api.unlink('/images/my_dog.jpg', {}, {headers: {Link: '<http://example.com/profiles/joe>; rel="tag"'}})

而mock的就更简单了, 直接就是就是写死的对象,里面包含若干方法,返回写死的数据。

调试

调试使用Reactotron, 为了使用它我们可以下载它的客户端,使用它不进可以直接console数据到面板,同时可以直接修改redux状态反馈到reactNative的UI层上。关于它的使用,在startup里面有几个例子,已经足够使用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.tron.log('Hello, I\'m an example of how to log via Reactotron.')
console.tron.log({
message: 'pass objects for better logging',
someGeneratorFunction: selectAvatar
})
console.tron.display({
name: '🔥 IGNITE 🔥',
preview: 'You should totally expand this',
value: {
'💃': 'Welcome to the future!',
subObject,
someInlineFunction: () => true,
someGeneratorFunction: startup,
someNormalFunction: selectAvatar
}
})

简单写个Demo App

LaunchScreen

正常app启动是这样的splash页面->LaunchPage(常见的就是滑屏简介)->主页。splash页面比较折腾,而且在很多博客都有说道,这里我们开始写一个LaunchPage页面作为开始。
首先我找了3个图片。放到了Images/目录备用 分别命名为launch1.png、launch2.png、launch3.png(仅项目展示使用,版权归作者所有-虽然我也不知道版权是谁的。。。),然后安装swiper组件 npm i react-native-swiper –save

最后修改一下LaunchScreen.js和最后修改一下LaunchScreenStyle.js

如上, 这样我们的LaunchScreen页面就算写好了。这里唯一需要写的就是flex布局问题。我在这里留了文本的位置,如果不需要完全可以删除只显示图片。

基本页面

这里打算做3个基本页面 Home OrderList MeInfo
Home页面这里来实践一把数据对接。不过在这之前我们需要先建立页面,然后处理一下导航, 使LaunchScreen可以跳到Home页面。

建立页面并处理redux和sagas

1
2
3
ignite generate screen home
ignite generate redux home
ignite generate saga home

这样,相关文件建立就结束了。接下来处理导航。常规的导航,实际上可以直接 this.props.navigation.navigate(‘HomeScreen’),不过这样的问题是返回会返回到LaunchScreen,这样就很不合理了。所以做些简要处理:

1
2
3
4
5
6
7
8
9
import { NavigationActions } from 'react-navigation'
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'HomeScreen'})
]
})
// 执行: this.props.navigation.dispatch(resetAction)}

这样,我们就可以跳转到HomeScreen了,不过现在HomeScreen只有孤零零的’HomeScreen’字样。我们先加一下导航条。

处理一下AppNavigation.js
主要是3个地方的修改:

  1. 引入react 和 Icon组件
  2. 将headerMode改成screen
  3. 修改navigationOptions
    1
    2
    3
    4
    5
    6
    7
    8
    9
    navigationOptions: ({navigation}) => {
    let {goBack, navigate} = navigation;
    return {
    headerStyle: styles.header,
    headerTitleStyle: styles.headerTitleStyle,
    headerLeft: <Icon name="chevron-left" onPress={()=>{goBack()}} size={24} color="#fff" style={{marginLeft: 20}} />,
    headerRight: <Icon name="home" onPress={()=>{navigate('HomeScreen')}} size={24} color="#fff" style={{marginRight: 20}} />,
    }
    }

这样,加上headerTitleStyle样式后,此时app就可以显示顶部导航条了。
现在项目是这样的

接下来我们给它加一个Tabbar:ignite generate component TabBar
关于这个Tabbar我们使用react-native-tabs来做——当然,即使自己做一个这个也不算复杂,不过这里为了方便尽量都用现有的组件。
tabbar.js的主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
onSelect (el) {
if (el.props.name === this.state.page) {
return false
} else {
this.setState({page: el.props.name})
console.tron.log(el.props.name)
this.props.navTo(el.props.name)
}
}
render () {
return (
<Tabs selected={this.state.page} style={{ backgroundColor: 'white' }} selectedStyle={{ color: '#4F8EF7' }} onSelect={this.onSelect.bind(this)}>
<Text name='HomeScreen' style={styles.textAlignCenter} selectedIconStyle={TabStyles}>
<Icon name='ios-book-outline' size={30} color='#000' />{'\n'}首页
</Text>
<Text name='OrderListScreen' style={styles.textAlignCenter} selectedIconStyle={TabStyles}>
<Icon name='ios-paper-outline' size={30} color='#000' />{'\n'}订单
</Text>
<Text name='MeInfoScreen' style={styles.textAlignCenter} selectedIconStyle={TabStyles}>
<Icon name='ios-person-outline' size={30} color='#000' />{'\n'}我的
</Text>
</Tabs>
)

为了达到切换效果,我们再加两个Screen:

1
2
ignite generate screen orderList
ignite generate screen meInfo

并为这两个页面添加Tabbar等操作(实际上可以直接单页tab切换,不过此处暂时不考虑那么多),做好之后(具体代码可以直接爬git的提交记录),现在tab切换是这样的:

数据对接

简单完善下Home, 我们来做个非常普遍的2行4列图标,效果图是这样

这里主要就是flex布局,所以怎么布局这里不做讲解,只是贴出一下代码方便说数据对接这块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const menus = [
{
"name": '美食1',
"icon": 'https://img.la/333/fff/60x60'
},
...
]
<View style={styles.menuItemWrapper}>
{
menus.map(({name, icon},i)=>(
<TouchableOpacity style={styles.menuItem} key={`icon-${i}`}>
<Image source={{uri: icon}} style={styles.menuItemIcon}/>
<Text>{name}</Text>
</TouchableOpacity>
))
}
</View>

假设最终接口返回数据格式是:

1
2
3
4
5
6
7
{
code: 200,
message: 'success',
data: {
menus: [{name: "xxx", icon: "xxx"}]
}
}

修改sagas/HomeSages.js, 将 response.ok 改成 +response.code === 200, 添加Fixtrues/home.json文件,内部填写上面的数据结构。这里因为没有后端所以直接mock本地数据,修改完毕后,更改Services/FixtrueApi.js
添加gethome函数

1
2
3
4
gethome: (data) => {
const initData = require('../Fixtures/home.json')
return {...initData}
}

根据数据结构完善一下HomeRedux.js

1
2
3
4
5
6
7
8
9
10
11
const { Types, Creators } = createActions({
...
homeSuccess: ['data'], // 默认是payload
...
})
...
// successful api lookup
export const success = (state, action) => {
const { data } = action // 默认是payload
return state.merge({ fetching: false, error: null, data }) // 默认是payload
}

注册sagas(mapDispatchToProps需要), 修改sagas/index.js文件

1
2
3
4
5
6
/* ------------- Types ------------- */
import { HomeTypes} from '../Redux/HomeRedux'
/* ------------- Sagas ------------- */
import { getHome } from './HomeSagas'
/* ------------- Connect Types To Sagas ------------- */
takeLatest(HomeTypes.HOME_REQUEST, getHome, api)

注册Redux(mapStateToProps需要), 修改redux/index.j文件

1
2
3
4
5
6
const rootReducer = combineReducers({
nav: require('./NavigationRedux').reducer,
github: require('./GithubRedux').reducer,
search: require('./SearchRedux').reducer,
homeInitData: require('./HomeRedux').reducer,
})

引入homeRedux,并更新HomeScreen页面mapStateToProps和mapDispatchToProps,并在componentDidMount内部执行获取数据的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import homeAction from '../Redux/HomeRedux'
...
componentDidMount () {
this.props.fetchData()
}
...
const mapStateToProps = ({homeInitData}) => { // 这个在redux那里定义
return {
initData: homeInitData.data
}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchData: () => dispatch(homeAction.homeRequest())
}
}

最后,将HomeScreen内部写死的menus删除,并将menus.map设为this.props.initData.menus.map, 这个流程就算结束了。

当然,如果有线上的服务器,不要忘记最后联调阶段,需要关闭useFixtures选项,并在Services/Api.js里面填充对应的逻辑,例如此处,需要在这里加gethome函数并返回指定的数据。

最后的成品截图

End

到这里Ignite最基础的使用已经完毕了,作为入门我个人认为已经足够。项目代码我已传至Github,如果有需要可以自行检出取用。