Maison  >  Article  >  interface Web  >  Explication détaillée de l'utilisation de la navigation par réaction-navigation

Explication détaillée de l'utilisation de la navigation par réaction-navigation

亚连
亚连original
2018-06-22 18:38:483038parcourir

Cet article présente principalement l'explication détaillée de l'utilisation de la navigation React Native. Il a une certaine valeur de référence. Ceux qui sont intéressés peuvent en savoir plus

1. aux bibliothèques open source

En janvier de cette année, la nouvelle bibliothèque open source de réaction-natvigation a attiré beaucoup d'attention. En moins de 3 mois, le nombre d'étoiles sur github a atteint plus de 4 000. Facebook recommande d'utiliser la bibliothèque et Navigator sera supprimé dans la dernière version de React Native 0.44. On dit que React-Navigation offre une expérience de performance de type natif. Il pourrait devenir le courant dominant des composants de navigation React Native à l’avenir. Cet article est basé sur la version [^1.0.0-beta.9] pour présenter l'utilisation et les compétences pratiques de cette bibliothèque. Comme vous pouvez le constater, bien qu'il s'agisse d'une version bêta, elle est fondamentalement stable et vous pouvez l'utiliser librement dans vos projets. Voici le document officiel de React-Navigation

Cette bibliothèque contient trois types de composants :
(1) StackNavigator : utilisé pour sauter des pages et passer des paramètres
(2) TabNavigator : Semblable à la barre de navigation inférieure, utilisée pour basculer entre différentes interfaces sur le même écran
(3) DrawerNavigator : Barre de navigation de menu coulissante latérale, utilisée pour configurer facilement un écran avec une navigation par tiroir

2, React-navigation utilise

Le contenu spécifique est grossièrement divisé comme suit :
(1) Attribut de la bibliothèque React-navigation introduction
(2) StackNavigator, TabNavigator implémente le saut entre les interfaces, le changement d'onglet
(3) StackNavigator saute, passe et obtient des valeurs entre les interfaces
(4) DrawerNavigator implémente le menu de navigation des tiroirs
(5 ) Fonction d'extension DrawerNavigator
( 6) Personnaliser la navigation de réaction

1 Introduction aux propriétés de StackNavigator
options de navigation : configurez certaines propriétés de StackNavigator.

  1. titre : titre. Si le titre de cette barre de navigation et de la barre d'onglets est défini, il deviendra le même. Il n'est pas recommandé d'utiliser

  2. header : Vous pouvez définir certaines propriétés de navigation. Si vous souhaitez masquer la barre de navigation supérieure, définissez simplement cette propriété sur null.

  3. headerTitle : Définissez le titre de la barre de navigation, recommandé.

  4. headerBackTitle : définissez le texte derrière la flèche de retour sur le côté gauche de la page de saut. La valeur par défaut est le titre de la page précédente. Peut être personnalisé ou défini sur null

  5. headerTruncatedBackTitle : défini lorsque le titre de la page précédente ne correspond pas au texte après la flèche de retour, la valeur par défaut est modifiée en "Retour"

  6. headerRight : Définissez le côté droit de la barre de navigation. Il peut s'agir d'un bouton ou d'un autre contrôle de vue

  7. headerLeft : définit le côté gauche de la barre de navigation. Il peut s'agir d'un bouton ou d'un autre contrôle de vue

  8. headerStyle : Définissez le style de la barre de navigation. Couleur d'arrière-plan, largeur et hauteur, etc.

  9. headerTitleStyle : Définir le style du texte de la barre de navigation

  10. headerBackTitleStyle : Définir le style de texte de la barre de navigation style de texte

  11. headerTintColor : définir la couleur de la barre de navigation

  12. headerPressColorAndroid : définir la texture de couleur unique à Android, nécessite une version Android supérieure à 5.0

  13. gesturesEnabled : s'il faut prendre en charge le geste de retour coulissant, iOS le prend en charge par défaut, Android le désactive par défaut

écran : nom de l'interface correspondant, vous il faut remplir la page après l'import

mode : Définir le style de saut

carte : Utiliser le style par défaut d'iOS et Android

modal : Unique à iOS, l'écran est tiré du bas. Semblable à l'effet présent sur iOS

headerMode : effet d'animation lors du retour à la page précédente

float : effet par défaut d'iOS

écran : pendant le processus de glissement, la page entière reviendra

aucun : Aucune animation

cardStyle : Personnaliser l'effet de saut

  1. transitionConfig : Personnaliser la configuration du retour coulissant

  2. onTransitionStart : La fonction qui sera appelée lorsque l'animation de transition est sur le point de démarrer

  3. onTransitionEnd : La fonction qui sera appelée lorsque l'animation de transition est terminée

path : configuration du mappage de superposition du chemin défini dans le routage

initialRouteName : définir le composant de page par défaut, qui doit être le composant de page enregistré ci-dessus

initialRouteParams : routage initial paramètres

Remarque : vous ne comprenez peut-être pas très bien le chemin. L'attribut path permet à d'autres applications ou navigateurs d'utiliser l'URL pour ouvrir cette application et accéder à la page spécifiée. L'attribut path est utilisé pour déclarer un chemin d'interface, par exemple : [/pages/Home]. À ce stade, nous pouvons saisir : nom de l'application://pages/Home dans le navigateur mobile pour démarrer l'application et accéder à l'interface d'accueil.

2. Introduction aux attributs de TabNavigator

écran : La fonction est la même que la navigation. Elle correspond au nom de l'interface. Vous pouvez transmettre des valeurs et parcourir. cet écran dans d'autres pages.

navigationOptions : configurez certaines propriétés de TabNavigator

title : title, qui définira le titre de la barre de navigation et de la barre d'onglets en même temps

tabBarVisible : s'il faut masquer la barre d'onglets. Non masqué par défaut (vrai)

tabBarIcon : Définissez l'icône de la barre d'onglets. Vous devez définir chacun d'entre eux

tabBarLabel : Définissez le titre de la barre d'onglets.

Configuration de la barre de navigation recommandée

tabBarPosition : Définissez la position de la barre d'onglets. La valeur par défaut est en bas pour iOS et en haut pour Android. (Valeur de l'attribut : 'top', 'bottom')

swipeEnabled : s'il faut autoriser le glissement entre les étiquettes

animationEnabled : s'il faut afficher une animation lors du changement d'étiquettes

lazy : si pour afficher paresseusement les étiquettes selon les besoins plutôt qu'à l'avance, ce qui signifie charger toute la barre d'étiquettes inférieure lorsque l'application est ouverte. La valeur par défaut est false et la recommandation est vraie

trueinitialRouteName : définissez le composant de page par défaut

.

backBehavior : s'il faut accéder au premier onglet (page d'accueil) en appuyant sur la touche retour, aucun signifie pas de saut

tabBarOptions : configurer certaines propriétés de la barre d'onglets Propriétés iOS

activeTintColor : label et icône Lorsque la couleur de premier plan est active

activeBackgroundColor : Lorsque la couleur d'arrière-plan de l'étiquette et de l'icône est active

inactiveTintColor : Lorsque la couleur de premier plan de l'étiquette et de l'icône est inactive

inactiveBackgroundColor : label Lorsque la couleur d'arrière-plan de l'icône est inactive

showLabel : afficher ou non l'étiquette, style activé par défaut : style de barre de tabulation

labelStyle : style d'étiquette, attribut Android

activeTintColor : label Lorsque la couleur de premier plan de l'étiquette et de l'icône est active

inactiveTintColor : lorsque la couleur de premier plan de l'étiquette et de l'icône est inactive

showIcon : s'il faut afficher l'icône, fermée par défaut

showLabel : s'il faut afficher l'étiquette, style activé par défaut : style de barre de tabulation

labelStyle : style d'étiquette upperCaseLabel : s'il faut mettre l'étiquette en majuscule, la valeur par défaut est vraie

pressColor : la couleur de l'effet d'entraînement de la matière (la version Android doit être supérieure à 5.0)

pressOpacity : le changement de transparence de l'étiquette pressée (la version Android doit être inférieure à 5.0)

scrollEnabled : s'il faut activer les onglets défilants tabStyle : style d'onglet

indicatorStyle : objet de style d'indicateur d'onglet (la ligne en bas de l'onglet). Il y aura une ligne supplémentaire en bas d'Android, vous pouvez définir la hauteur sur 0 pour résoudre temporairement ce problème

labelStyle : style d'étiquette

iconStyle : style d'icône

3, Introduction aux propriétés de DrawerNavigator

DrawerNavigatorConfig

  1. drawerWidth - la largeur du tiroir

  2. drawerPosition - l'option est gauche ou droite. La valeur par défaut est la position gauche

  3. contentComponent - le composant utilisé pour restituer le contenu du tiroir, tel que les éléments de navigation. Recevez la navigation du tiroir. La valeur par défaut est DrawerItems

  4. contentOptions - Configurer le contenu du tiroir

initialRouteName - Le nom de la route initiale

order - Définir l'ordre des éléments du tiroir. Le tableau routeNames.

Chemin - Fournit un mappage de routeName à la configuration de la route, qui remplace le chemin défini dans routeConfigs.

backBehavior - Le bouton de retour passera-t-il à l'itinéraire initial ? Si tel est le cas, définissez-le sur initialRoute, sinon aucun. La valeur par défaut est le comportement initialRoute

L'attribut contentOptions de DrawerItems

  1. activeTintColor - la couleur de l'étiquette et de l'icône de l'étiquette active

  2. activeBackgroundColor - l'activité La couleur d'arrière-plan du label

  3. inactiveTintColor - la couleur du label et de l'icône du label inactif

  4. inactiveBackgroundColor - le couleur d'arrière-plan de l'étiquette inactive

Objet Style pour la section de contenu

labelStyle - Un objet style pour remplacer le style du texte dans la section de contenu lorsque votre étiquette est une chaîne
Ayez une idée générale de ce qui précède Nous avons appris quelques propriétés de base des trois composants de React-Navigation, nous avons donc retroussé nos manches et enroulé le code pour assister au miracle.

4. Utilisez StackNavigator + TabNavigator pour implémenter la commutation d'interface d'onglet et la navigation entre les interfaces

Définition de l'API : StackNavigator(RouteConfigs, StackNavigatorConfig), TabNavigator(RouteConfigs, TabNavigatorConfig)

( 3) Définir TabNavigator :

import {StackNavigator,TabNavigator,TabBarBottom} from 'react-navigation'; 
import HomeScreen from './pages/HomePage'; 
import MineScreen from './pages/MinePage';
TabBarItem est un composant encapsulé :

const Tab = TabNavigator( 
 { 
  Home:{ 
   screen:HomeScreen, 
   navigationOptions:({navigation}) => ({ 
    tabBarLabel:'首页', 
    tabBarIcon:({focused,tintColor}) => ( 
     <TabBarItem 
      tintColor={tintColor} 
      focused={focused} 
      normalImage={require(&#39;./imgs/nav_fav@2x.png&#39;)} 
      selectedImage={require(&#39;./imgs/nav_fav_actived@3x.png&#39;)} 
     /> 
    ) 
   }), 
  }, 
 
  Mine:{ 
     screen:MineScreen, 
     navigationOptions:({navigation}) => ({ 
     tabBarLabel:&#39;我&#39;, 
     tabBarIcon:({focused,tintColor}) => ( 
      <TabBarItem 
       tintColor={tintColor} 
       focused={focused} 
       normalImage={require(&#39;./imgs/tab_me_nor@3x.png&#39;)} 
       selectedImage={require(&#39;./imgs/tab_me_selected@2x.png&#39;)} 
      /> 
     ) 
    }), 
   }, 
  }, 
 
  { 
   tabBarComponent:TabBarBottom, 
   tabBarPosition:&#39;bottom&#39;, 
   swipeEnabled:false, 
   animationEnabled:false, 
   lazy:true, 
   tabBarOptions:{ 
    activeTintColor:&#39;#06c1ae&#39;, 
    inactiveTintColor:&#39;#979797&#39;, 
    style:{backgroundColor:&#39;#ffffff&#39;,}, 
    labelStyle: { 
       fontSize: 20, // 文字大小 
     }, 
   } 
    
  } 
 
 );
Comme vous pouvez le constater, nous avons défini un composant de navigation nommé TabNavigator nommé [Tab]. Dans le composant, il est divisé en deux couches de paramètres :

(1) La première couche de paramètres définit l'interface à commuter, c'est-à-dire les deux composants d'interface [Home] et [Me], qui sont spécifiés via l’attribut screen. Et définissez les paramètres d'attribut pertinents via l'attribut navigationOptions.
import React,{Component} from &#39;react&#39;; 
import {Image} from &#39;react-native&#39;; 
 
export default class TabBarItem extends Component { 
 
  render() { 
    return( 
      <Image source={ this.props.focused ? this.props.selectedImage : this.props.normalImage } 
        style={ { tintColor:this.props.tintColor,width:25,height:25 } } 
      /> 
    ) 
  } 
   
}

(2) Définissez les paramètres d'attribut de la barre de navigation.

Une fois le TabNavigator défini, vous devez utiliser StackNavigator Comme son nom l'indique, StackNavigator stocke l'intégralité de l'interface dans une pile, et

.

TabNavigator是作为一个界面内不同子界面之间切换。所以还需要我们定义StackNavigator:

const Navigator = StackNavigator( 
  
 { 
  Tab:{screen:Tab}, 
  Product:{screen:ProductScreen} 
 }, 
 
 { 
  navigationOptions:{ 
   headerBackTitle:null, 
   headerTintColor:&#39;#333333&#39;, 
   showIcon:true, 
   swipeEnabled:false, 
   animationEnabled:false, 
  }, 
 
  mode:&#39;card&#39;, 
 });

看起来和TabNavigator很相似,同样是指定了两个参数:

(1)指定要跳转的界面组件。同样是screen属性标识界面组件,不多赘述。

(2)定义跳转属性参数,即顶部导航栏的一些参数设置和跳转方式。

可以看到,我们将Tab作为一个界面设置到了StackNavigator。这样就可以实现Tab导航和界面间跳转的效果了。

最后就是在render中引用StackNavigator:

export default class Demo extends Component { 
 
 render() { 
    return ( 
     <Navigator /> 
    ); 
 } 
}

StackNavigator还提供了onNavigationStateChange回调方法,用来监听导航状态的改变。具体不再赘述。实现了界面跳转和切换,那么就该来增加下界面之间的感情了,来看看如何实现界面之间的传值和取值。

5、界面间跳转、传值、取值

在界面组件注入到StackNavigator中时,界面组件就被赋予了navigation属性,即在界面组件中可以通过【this.props.navigation】获取并进行一些操作。

navigation属性中提供了很多的函数简化界面间操作,简单列举几点:

(1)通过navigate函数实现界面之间跳转:

this.props.navigation.navigate(&#39;Mine&#39;);

参数为我们在StackNavigator注册界面组件时的名称。同样也可以从当前页面返回到上一页:

// 返回上一页 
this.props.navigation.goBack();

(2)跳转时传值:

this.props.navigation.navigate(&#39;Mine&#39;,{info:&#39;传值过去&#39;});

第一个参数同样为要跳转的界面组件名称,第二个参数为要传递的参数,info可以理解为key,后面即传递的参数。

(3)获取值:

{this.props.navigation.state.params.info}

通过state.params来获取传来的参数,后面为key值。此处为info。

以上实现完成,我们就可以愉快的玩耍啦~~ 什么?忽然发现在Android上的效果和IOS效果不一样。老板要界面一致哇~ 怎么办?那就需要我们进行简单的适配了。

三、DrawerNavigator实现抽屉导航

1、导航实现

API定义:DrawerNavigator(RouteConfigs,DrawerNavigatorConfig)

(1)界面中定义DrawerNavigator:

import {StackNavigator,TabNavigator,DrawerNavigator} from 'react-navigation'; 
import HomeScreen from './pages/HomePage'; 
import MineScreen from './pages/MinePage'; 
 
export default class Demo extends Component { 
 
 render() { 
    return ( 
     <Navigator /> 
    ); 
 } 
} 
 
const Navigator = DrawerNavigator({ 
 
  Home:{screen:HomeScreen}, 
  Mine:{screen:MineScreen}, 
}); 
 
const styles = StyleSheet.create({ 
 
  container: { 
    flex: 1, 
  }, 
}); 
 
AppRegistry.registerComponent('Demo', () => Demo);

定义方式和StackNavigator基本类似,不再赘述。

(2)HomeScreen界面和MineScreen界面:

export default class HomePage extends Component { 
 
  static navigationOptions = { 
    drawerLabel: &#39;首页&#39;, 
    drawerIcon:({tintColor}) => ( 
      <Image 
        source={require(&#39;./../imgs/ic_happy.png&#39;)} 
        style={[styles.icon, {tintColor: tintColor}]}/> 
    ), 
  }; 
 
  render() { 
    return( 
      <View style={{flex:1}}> 
        <Text onPress={this._skip.bind(this)}>点击跳转</Text> 
      </View> 
    ); 
  } 
 
  _skip() { 
    this.props.navigation.navigate("Mine"); 
  } 
} 
 
 
export default class MinePage extends Component { 
 
  static navigationOptions = { 
    drawerLabel:&#39;我&#39;, 
     drawerIcon: ({ tintColor }) => ( 
      <Image 
        source={require(&#39;./../imgs/ic_h.png&#39;)} 
        style={[styles.icon, {tintColor: tintColor}]} 
      /> 
    ), 
  }; 
 
  render() { 
    return( 
      <View style={{flex:1}}> 
        <Text onPress={this._skip.bind(this)}>返回上一界面</Text> 
      </View> 
    ); 
  } 
 
  /** 
   * 跳转 
   */ 
  _skip() { 
    this.props.navigation.goBack(); 
  } 
}

代码很简单,实现了界面之间的跳转。

2、扩展功能

(1)默认DrawerView不可滚动。要实现可滚动视图,必须使用contentComponent自定义容器,如下所示:

{ 
 drawerWidth:200, 
 抽屉位置:“对” 
 contentComponent:props => <ScrollView> <DrawerItems {... props} /> </ ScrollView> 
}

(2)可以覆盖导航使用的默认组件,使用DrawerItems自定义导航组件:

import {DrawerItems} from &#39;react-navigation&#39;; 
 
const CustomDrawerContentComponent = (props) => ( 
 <View style = {style.container}> 
  <DrawerItems {... props} /> 
 </View>  
);

(3)嵌套抽屉导航

如果您嵌套DrawerNavigation,抽屉将显示在父导航下方。

四、自定义react-navigation

(1)适配顶部导航栏标题:

测试中发现,在iphone上标题栏的标题为居中状态,而在Android上则是居左对齐。所以需要我们修改源码,进行适配。
【node_modules -- react-navigation -- src -- views -- Header.js】的326行代码处,修改为如下:

title: { 
  bottom: 0, 
  left: TITLE_OFFSET, 
  right: TITLE_OFFSET, 
  top: 0, 
  position: &#39;absolute&#39;, 
  alignItems: &#39;center&#39;, 
 }

上面方法通过修改源码的方式其实略有弊端,毕竟扩展性不好。还有另外一种方式就是,在navigationOptions中设置headerTitleStyle的alignSelf为 ' center '即可解决。

(2)去除返回键文字显示:

【node_modules -- react-navigation -- src -- views -- HeaderBackButton.js】的91行代码处,修改为如下即可。

{Platform.OS === &#39;ios&#39; && 
   title && 
   <Text 
    onLayout={this._onTextLayout} 
    style={[styles.title, { color: tintColor }]} 
    numberOfLines={1} 
   > 
    {backButtonTitle} 
   </Text>}

将上述代码删除即可。

(3)动态设置头部按钮事件:

当我们在头部设置左右按钮时,肯定避免不了要设置按钮的单击事件,但是此时会有一个问题,navigationOptions是被修饰为static类型的,所以我们在按钮的onPress的方法中不能直接通过this来调用Component中的方法。如何解决呢?在官方文档中,作者给出利用设置params的思想来动态设置头部标题。那么我们可以利用这种方式,将单击回调函数以参数的方式传递到params,然后在navigationOption中利用navigation来取出设置到onPress即可:

componentDidMount () { 
  /** 
   * 将单击回调函数作为参数传递 
   */ 
  this.props.navigation.setParams({ 
      switch: () => this.switchView() 
  }); 
}
/** 
 * 切换视图 
 */ 
switchView() { 
  alert(&#39;切换&#39;) 
}
static navigationOptions = ({navigation,screenProps}) => ({ 
  headerTitle: &#39;企业服务&#39;, 
  headerTitleStyle: CommonStyles.headerTitleStyle, 
  headerRight: ( 
    <NavigatorItem icon={ Images.ic_navigator } onPress={ ()=> navigation.state.params.switch() }/> 
  ), 
  headerStyle: CommonStyles.headerStyle 
});

(4)结合BackHandler处理返回和点击返回键两次退出App效果

点击返回键两次退出App效果的需求屡见不鲜。相信很多人在react-navigation下实现该功能都遇到了很多问题,例如,其他界面不能返回。也就是手机本身返回事件在react-navigation之前拦截了。如何结合react-natigation实现呢?和大家分享两种实现方式:

(1)在注册StackNavigator的界面中,注册BackHandler:

componentWillMount(){ 
  BackHandler.addEventListener(&#39;hardwareBackPress&#39;, this._onBackAndroid ); 
} 
 
 
componentUnWillMount(){ 
  BackHandler.addEventListener(&#39;hardwareBackPress&#39;, this._onBackAndroid); 
} 
 
_onBackAndroid=()=>{ 
  let now = new Date().getTime(); 
  if(now - lastBackPressed < 2500) { 
    return false; 
  } 
  lastBackPressed = now; 
  ToastAndroid.show(&#39;再点击一次退出应用&#39;,ToastAndroid.SHORT); 
  return true; 
}

(2)监听react-navigation的Router

/** 
 * 处理安卓返回键 
 */ 
const defaultStateAction = AppNavigator.router.getStateForAction; 
AppNavigator.router.getStateForAction = (action,state) => { 
  if(state && action.type === NavigationActions.BACK && state.routes.length === 1) { 
    if (lastBackPressed + 2000 < Date.now()) { 
      ToastAndroid.show(Constant.hint_exit,ToastAndroid.SHORT); 
      lastBackPressed = Date.now(); 
      const routes = [...state.routes]; 
      return { 
        ...state, 
        ...state.routes, 
        index: routes.length - 1, 
      }; 
    } 
  } 
  return defaultStateAction(action,state); 
};

(5)实现Android中界面跳转左右切换动画

react-navigation在Android中默认的界面切换动画是上下。如何实现左右切换呢?很简单的配置即可:

import CardStackStyleInterpolator from &#39;react-navigation/src/views/CardStackStyleInterpolator&#39;;

然后在StackNavigator的配置下添加如下代码:

transitionConfig:()=>({ 
  screenInterpolator: CardStackStyleInterpolator.forHorizontal, 
})

(6)解决快速点击多次跳转

当我们快速点击跳转时,会开启多个重复的界面,如何解决呢。其实在官方git中也有提示,解决这个问题需要修改react-navigation源码:

找到scr文件夹中的addNavigationHelpers.js文件,替换为如下文本即可:

export default function<S: *>(navigation: NavigationProp<S, NavigationAction>) { 
 // 添加点击判断 
 let debounce = true; 
 return { 
   ...navigation, 
   goBack: (key?: ?string): boolean => 
     navigation.dispatch( 
       NavigationActions.back({ 
         key: key === undefined ? navigation.state.key : key, 
       }), 
     ), 
   navigate: (routeName: string, 
         params?: NavigationParams, 
         action?: NavigationAction,): boolean => { 
     if (debounce) { 
       debounce = false; 
       navigation.dispatch( 
         NavigationActions.navigate({ 
           routeName, 
           params, 
           action, 
         }), 
       ); 
       setTimeout( 
         () => { 
           debounce = true; 
         }, 
       500, 
       ); 
       return true; 
     } 
     return false; 
   }, 
  /** 
   * For updating current route params. For example the nav bar title and 
   * buttons are based on the route params. 
   * This means `setParams` can be used to update nav bar for example. 
   */ 
  setParams: (params: NavigationParams): boolean => 
   navigation.dispatch( 
    NavigationActions.setParams({ 
     params, 
     key: navigation.state.key, 
    }), 
   ), 
 }; 
}

五、效果图

抽屉导航:

 

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

使用JS如何实现去除重复json

详细解读vue中的$mount

在vue中如何使用refs定位

在js中如何转换bool值?

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn