surmon.me.native代码分析笔记
surmon.me.native是一个适合入门学习的react-native博客内容展示项目,代码组织优良值得借鉴。
基础样式
作者把基础样式进行了统一编写,方便维护修改。
style/size.js
import { Dimensions, Platform } from 'react-native';
const { width, height } = Dimensions.get('window');
const screenHeight = width < height ? height : width;
const screenWidth = width < height ? width : height;
export default {
// Window Dimensions
screen: {
height: screenHeight,
width: screenWidth,
widthHalf: screenWidth * 0.5,
widthThird: screenWidth * 0.333,
widthTwoThirds: screenWidth * 0.666,
widthQuarter: screenWidth * 0.25,
widthThreeQuarters: screenWidth * 0.75,
},
// Navbar
navbarHeight: (Platform.OS === 'ios') ? 50 : 50,
statusBarHeight: (Platform.OS === 'ios') ? 16 : 24,
// Padding
padding: 20
};
style/fonts.js
import { Platform } from 'react-native';
function lineHeight(fontSize) {
const multiplier = (fontSize > 20) ? 0.1 : 0.33;
return parseInt(fontSize + (fontSize * multiplier), 10);
}
const base = {
fontSize: 14,
lineHeight: lineHeight(14),
...Platform.select({
ios: {
// fontFamily: 'HelveticaNeue',
},
android: {
fontFamily: 'Roboto',
},
}),
};
export default {
base: { ...base },
h1: { ...base, fontSize: base.fontSize * 1.75, lineHeight: lineHeight(base.fontSize * 2) },
h2: { ...base, fontSize: base.fontSize * 1.5, lineHeight: lineHeight(base.fontSize * 1.75) },
h3: { ...base, fontSize: base.fontSize * 1.25, lineHeight: lineHeight(base.fontSize * 1.5) },
h4: { ...base, fontSize: base.fontSize * 1.1, lineHeight: lineHeight(base.fontSize * 1.25) },
h5: { ...base },
};
欢迎页 welcome.js
编写欢迎页面提高用户体验,据说可以防止启动白屏。
// Init Layout
import Layout from './layout.js';
// Styles
import { AppColors, AppSizes } from '@app/style';
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
alignItems: 'center',
justifyContent: 'center'
},
launchImage: {
position: 'absolute',
left: 0,
top: 0,
width: AppSizes.screen.width,
height: AppSizes.screen.height
}
});
class Welcome extends Component {
componentWillMount () {
var navigator = this.props.navigator;
setTimeout (() => {
navigator.replace({component: Layout, passProps: { navigator }});
}, 1666);
}
render () {
return (
<View style={styles.container}>
<StatusBar
translucent={true}
backgroundColor={'#rgba(0, 0, 0, 0)'}
barStyle="light-content"
showHideTransition='slide'
hidden={false}
/>
<Image style={styles.launchImage} source={require('@app/images/android-launch/launch-image.png')}></Image>
</View>
);
}
}
export default Welcome;
菜单页 menu.js
作者实现了安卓版本的屏幕左划出效果,编写子组件MneuHeader(菜单头部),MenuList(菜单列表)、MenuItem(菜单列表项)组合成Menu组件。
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selectedItem: props.initialEntry || props.entries[0].id//默认指向第一个列表
}
}
//点击菜单任意列表时,关闭菜单
_onSectionChange = (section) => {
this.setState({selectedItem: section});
this._drawer.closeDrawer();
}
//打开菜单
_openMenu = () => {
this._drawer.openDrawer();
}
//渲染菜单
_renderNavigationView = () => {
return (
<View style={this.props.containerStyle}>
<MneuHeader userInfo={this.props.userInfo}/>
<MenuList
items={this.props.entries}
selectedItem={this.state.selectedItem}
tintColor={this.props.tintColor}
onSectionChange={this._onSectionChange}
/>
</View>
)
}
//渲染菜单内容
_renderContent() {
const element = this.props.entries.find(entry => entry.id === this.state.selectedItem).element;
if (element) {
return React.cloneElement(element, {openMenu: this._openMenu});
}
}
//使用DrawerLayoutAndroid组件,渲染菜单
render() {
return (
<DrawerLayoutAndroid
ref={(ref) => {this._drawer = ref}}
{...this.props}
renderNavigationView={this._renderNavigationView}>
{this._renderContent()}
</DrawerLayoutAndroid>
)
}
}
export default Menu;
API
作者将网络请求进行封装,增加了API的复用性。
import showToast from '@app/utils/toast';
// api
const baseApi = 'https://api.surmon.me';
const fetchService = (url, options = {}) => {
return fetch(url, options)
.then(response => {
return response.json();
})
.then(json => {
showToast(json.message);
return json;
})
.catch(error => {
showToast('网络错误');
console.warn(error);
});
};
// apis
export default class Api {
// 获取文章列表
static getArticleList(page) {
const queryParams = page ? `?page=${page}` : '';
return fetchService(`${baseApi}/article${queryParams}`);
}
// 获取文章详情
static getArticleDetail(article_id) {
return fetchService(`${baseApi}/article/${article_id}`);
}
// 给文章或主站点赞
static likeArticleOrSite(like_data) {
return fetchService(`${baseApi}/like`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(like_data)
})
}
// 获取用户信息
static getUserInfo() {
return fetchService(`${baseApi}/auth`)
}
}
Pages
程序主要包含文章列表页、文章内容页、项目页、关于页等4个页面。
文章列表页:
class ArticleList extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
firstLoader: true
};
// 获取本地存储的点赞记录
AsyncStorage.getItem('user_like_history')
.then(historyLikes => {
this.historyLikes = historyLikes ? JSON.parse(historyLikes) : []
}).catch(err => {
console.log(err)
})
}
// 文章列表项目渲染
renderRowView(article, sectionID, rowID) {
let liked = false;
if (this.historyLikes && this.historyLikes.length && this.historyLikes.includes(article.id)) {
liked = true;
}
return (
<ArticleListItem article={article}
rowID={rowID}
liked={liked}
key={`sep:${sectionID}:${rowID}`}
navigator={this.props.navigator} />
)
}
// 请求文章数据
getArticles(page = 1, callback, options) {
this.setState({ loading: true });
Api.getArticleList(page)
.then(data => {
this.setState({ loading: false, firstLoader: false });
const pagination = data.result.pagination;
callback(data.result.data, {
allLoaded: pagination.total_page < 2 || pagination.current_page >= pagination.total_page,
});
})
.catch(err => {
console.log(err);
});
}
// 在第一次读取时没有行显示时呈现视图,refreshcallback函数的函数调用刷新列表
renderEmptyView(refreshCallback) {
return (
<View style={customListStyles.defaultView}>
<Text style={customListStyles.defaultViewTitle}>暂无数据,下拉刷新重试</Text>
<TouchableHighlight underlayColor={AppColors.textDefault} onPress={refreshCallback}>
<Ionicon name="md-refresh" size={22} style={{color: AppColors.textDefault}}/>
</TouchableHighlight>
</View>
);
}
// 翻页正常状态
renderPaginationWaitingView(paginateCallback) {
return (
<TouchableHighlight
onPress={paginateCallback}
underlayColor={AppColors.textMuted}
style={customListStyles.paginationView}>
<Text style={
[customListStyles.actionsLabel, {
fontSize: AppFonts.base.fontSize,
color: AppColors.textDefault
}]
}>加载更多</Text>
</TouchableHighlight>
);
}
// 翻页在请求时的状态
renderPaginationFetchingView() {
return (
<View style={[customListStyles.paginationView, { backgroundColor: 'transparent' }]}>
<AutoActivityIndicator size={'small'} />
</View>
)
}
// 翻页在文章全部加载完时的状态
renderPaginationAllLoadedView() {
return (
<View style={[customListStyles.paginationView, {
height: AppSizes.padding * 1.5,
marginBottom: AppSizes.padding / 2,
backgroundColor: AppColors.background
}]}>
<Text style={
[customListStyles.actionsLabel, {
fontSize: AppFonts.base.fontSize,
color: AppColors.textMuted
}]
}>到底啦~</Text>
</View>
)
}
render(){
return (
<View style={styles.listViewContainer}>
<GiftedListView
style={styles.ArticleListView}
firstLoader={true}
initialListSize={10}
withSections={false}
enableEmptySections={true}
rowView={this.renderRowView.bind(this)}
onFetch={this.getArticles.bind(this)}
rowHasChanged={(r1,r2) => { r1.id !== r2.id }}
emptyView={this.renderEmptyView}
refreshable={true}
refreshableTitle={'更新数据...'}
refreshableTintColor={AppColors.brand.black}
refreshableColors={[AppColors.brand.primary]}
pagination={true}
paginationFetchingView={this.renderPaginationFetchingView}
paginationWaitingView={this.renderPaginationWaitingView}
paginationAllLoadedView={this.renderPaginationAllLoadedView}
/>
{
this.state.firstLoader
? <View style={styles.ArticleListIndicator}>
<AutoActivityIndicator />
{
Platform.OS == 'ios'
? <Text style={styles.ArticleListIndicatorTitle}>数据加载中...</Text>
: null
}
</View>
: null
}
</View>
)
}
}
export default ArticleList;
文章内容页使用react-native-simple-markdown组件展示文章内容。
// component
class Detail extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
article: this.props.article,
articleContent: '',
liked: false
}
// 获取本地存储记录
AsyncStorage.getItem('user_like_history')
.then(historyLikes => {
this.historyLikes = historyLikes ? JSON.parse(historyLikes) : []
}).catch(err => {
console.log(err)
})
}
// 组件加载完成
componentDidMount() {
BackAndroid.addEventListener('hardwareBackPress', HandleBackBtnPress.bind(this));
this.setState({loading: true});
Api.getArticleDetail(this.props.article.id).then(data => {
this.setState({
loading: false,
article: data.result,
articleContent: data.result.content
})
if (this.historyLikes.length) {
if (this.historyLikes.includes(this.state.article.id)) {
this.setState({ liked: true });
}
}
}).catch(err => {
console.log(err);
})
}
// 喜欢文章
likeArticle() {
if (this.state.liked) return false;
Api.likeArticleOrSite({
type: 2,
id: this.props.article.id
}).then(data => {
this.state.liked = true;
this.state.article.meta.likes += 1;
this.forceUpdate();
this.historyLikes.push(this.state.article.id);
AsyncStorage.setItem('user_like_history', JSON.stringify(this.historyLikes))
}).catch(err => {
console.log(err);
})
}
// 去留言板
toComment() {
Alert.alert('功能还没做');
}
// 组件即将释放
componentWillUnmount() {
BackAndroid.removeEventListener('hardwareBackPress', HandleBackBtnPress.bind(this));
}
render() {
const { detail, article, loading, articleContent } = this.state;
const _navigator = this.props.navigator;
return (
<View style={{flex: 1, backgroundColor: '#fff'}}>
<ScrollView style={{height: AppSizes.screen.height - 200}}>
<Image source={buildThumb(article.thumb)} style={styles.image}>
<View style={styles.innerImage}>
<Text style={styles.title}>{ article.title }</Text>
<View style={styles.meta}>
{ article.category.length
? <View style={[styles.metaItem, styles.metaItemLeft]}>
<CommunityIcon name="book-open-variant" size={17} style={[styles.metaIcon, styles.metaText]}/>
<Text style={styles.metaText}>{ String(article.category.map(c => c.name).join('、')) }</Text>
</View>
: null
}
<View style={[styles.metaItem, styles.metaItemLeft]}>
<CommunityIcon name="eye" size={17} style={[styles.metaIcon, styles.metaText]}/>
<Text style={styles.metaText}>{ article.meta.views }</Text>
</View>
<View style={styles.metaItem}>
<CommunityIcon name="clock" size={17} style={[styles.metaIcon, styles.metaText, styles.metaDateIcon]}/>
<Text style={styles.metaText}>{ toYMD(article.create_at) }</Text>
</View>
</View>
</View>
</Image>
{ loading
? <AutoActivityIndicator style={styles.indicator}/>
: <View style={styles.content}>
<Markdown styles={markdownStyles}
rules={markdownRules}
blacklist={['list']}>{articleContent}</Markdown>
</View>
}
<NavBar leftOn={true}
navigator={this.props.navigator}
containerStyle={{backgroundColor: 'transparent'}} />
</ScrollView>
<View style={styles.footer}>
<TouchableOpacity style={styles.footerItem} onPress={this.toComment}>
<Icon name="comment" size={17} style={styles.footerItemIcon}/>
<Text style={styles.footerItemIconText}>{ `评论 (${article.meta.comments})` }</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.footerItem} onPress={this.likeArticle.bind(this)}>
<Icon name={this.state.liked ? 'favorite' : 'favorite-border'}
size={17}
style={[styles.footerItemIcon, {
color: this.state.liked ? 'red' : AppColors.textTitle
}]}/>
<Text style={[styles.footerItemIconText, {
color: this.state.liked ? 'red' : AppColors.textTitle
}]}>{ `${this.state.liked ? '已' : ''}喜欢 (${article.meta.likes})` }</Text>
</TouchableOpacity>
</View>
</View>
)
}
}
export default Detail;
项目页通过WebView组件链接到作者的github仓储目录下。
class Projects extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
canGoBack: false
};
}
componentDidMount() {
BackAndroid.addEventListener('hardwareBackPress', this.handleBackAction.bind(this));
}
componentWillUnmount() {
BackAndroid.removeEventListener('hardwareBackPress', this.handleBackAction.bind(this));
}
// webview状态改变
onNavigationStateChange(navState) {
this.setState({ canGoBack: navState.canGoBack });
// console.log(this.refs.WEBVIEW_REF);
}
// 返回上一页Webview
backWebViewPrevPage() {
this.refs.WEBVIEW_REF.goBack();
}
// 刷新当前webview
reloadWebView() {
this.refs.WEBVIEW_REF.reload();
}
handleBackAction() {
// webview有回退页则返回
if (this.state.canGoBack) {
this.backWebViewPrevPage();
// 否则执行路由返回解析
} else {
HandleBackBtnPress.bind(this)();
}
}
render() {
return (
<View style={styles.container}>
<WebView style={styles.webview}
ref="WEBVIEW_REF"
onNavigationStateChange={this.onNavigationStateChange.bind(this)}
source={{uri:'https://github.com/surmon-china?tab=repositories'}}
onLoad={() => this.setState({loading: true})}
onLoadEnd={() => this.setState({loading: false})}
startInLoadingState={true}
domStorageEnabled={true}
javaScriptEnabled={true}
/>
<NavBar leftOn={true}
title={this.props.title}
leftIsBack={Platform.OS === 'ios' && this.state.canGoBack}
onLeftPress={
Platform.OS === 'ios'
? this.handleBackAction.bind(this)
: this.props.openMenu
}
rightOn={true}
rightText={
Platform.OS === 'ios'
? <Ionicons name='ios-refresh' size={32} color={AppColors.textPrimary} />
: <Ionicons name='md-refresh' size={24} color={AppColors.textPrimary} />
}
onRightPress={this.reloadWebView.bind(this)}
/>
</View>
)
}
}
export default Projects;
关于页面代码相对简单。
class About extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
BackAndroid.addEventListener('hardwareBackPress', HandleBackBtnPress.bind(this));
}
componentWillUnmount() {
BackAndroid.removeEventListener('hardwareBackPress', HandleBackBtnPress.bind(this));
}
openSocial(url) {
Linking.openURL(url).catch(error => console.warn('An error occurred: ', error))
}
render() {
const userInfo = this.props.userInfo;
return (
<View style={styles.container}>
<View style={[styles.container, styles.userContent]}>
<Image style={styles.userGravatar} source={
userInfo ? {uri: userInfo.gravatar} : require('@app/images/gravatar.jpg')
}/>
<Text style={styles.userName}>{ userInfo ? userInfo.name : 'Surmon' }</Text>
<Text style={styles.userSlogan}>{ userInfo ? userInfo.slogan : 'Talk is cheap. Show me the code.' }</Text>
<View style={styles.userSocials}>
<TouchableOpacity style={styles.userSocialItem} onPress={() => { this.openSocial('https://github.com/surmon-china')}}>
<Ionicon name="logo-github" size={26} style={styles.userSocialIcon}/>
</TouchableOpacity>
<TouchableOpacity style={styles.userSocialItem} onPress={() => { this.openSocial('https://stackoverflow.com/users/6222535/surmon?tab=profile')}}>
<FontAwesome name="stack-overflow" size={22} style={styles.userSocialIcon}/>
</TouchableOpacity>
<TouchableOpacity style={styles.userSocialItem} onPress={() => { this.openSocial('https://weibo.com/nocower')}}>
<FontAwesome name="weibo" size={27} style={styles.userSocialIcon}/>
</TouchableOpacity>
<TouchableOpacity style={styles.userSocialItem} onPress={() => { this.openSocial('https://www.facebook.com/surmon.me')}}>
<Ionicon name="logo-facebook" size={30} style={styles.userSocialIcon}/>
</TouchableOpacity>
<TouchableOpacity style={styles.userSocialItem} onPress={() => { this.openSocial('https://twitter.com/surmon_me')}}>
<Ionicon name="logo-twitter" size={28} style={styles.userSocialIcon}/>
</TouchableOpacity>
<TouchableOpacity style={styles.userSocialItem} onPress={() => { this.openSocial('http://www.linkedin.com/in/surmon-ma-713bb6a2/')}}>
<Ionicon name="logo-linkedin" size={30} style={styles.userSocialIcon}/>
</TouchableOpacity>
</View>
</View>
<Navbar leftOn={true}
title={this.props.title}
onLeftPress={ () => {
Platform.OS === 'android' && this.props.openMenu();
}}/>
</View>
)
}
}
export default About;