surmon.me.native代码分析笔记

本文是surmon.me.native项目的代码分析笔记,重点介绍基础样式、欢迎页、菜单页的实现以及API和Pages的组织结构。项目采用react-native,基础样式在style/size.js和style/fonts.js中统一管理,欢迎页welcome.js用于防止启动白屏,菜单页menu.js实现了屏幕左划出效果。此外,API封装提高了复用性,Pages包括文章列表、内容、项目和关于四个页面。

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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值