Ignite简介 Ignite是一个基于ReactNative的手脚架和基础代码的生成工具, 使用它可以从0开始构建一个ReactNative的项目,并且可以为生成基础项目、为项目添加plugins,以及自动生成新页面和对应redux和相关sagas代码并自动插入到router等地方而不需要手动复制。
关于Ignite相关教程实际上早就想写一下,之前6月初通过Google搜索一直没有找到比较好用的step by step教程,然而到现在了暂时也还是没看到。索性我自己写一个吧。
安装和命令行的使用一瞥 安装
1 $ npm install -g ignite-cli
初始化新项目
添加和移除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 10 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个地方的修改:
引入react 和 Icon组件
将headerMode改成screen
修改navigationOptions1 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 ,如果有需要可以自行检出取用。