指尖前端重构React技术调研分析

一、为什么选择ReactReact是当前前端应用最广泛的框架。三大SPA框架 Angular、React、Vue比较。 Angular出现最早,但其在原理上并没有React创新的性能优化,且自身相对来说显得笨重。Vue出现最晚,其核心原理学习了React,只是语法形式的变化,关系上来说React是开拓者,Vue是学习者。React社区有强大活力与创新能力,不断涌现革命性的创新产品,其中包括可以使用JS操作原生控件的React Native,Vue后来跟进学习出了类似的Weex,但两者成熟度差很多。目前来看React的生态系统要比Vue大的多,在github、stackoverflow等最大的技术社区搜索两者,React的搜索结果是Vue的十倍左右,另外据近期统计使用React的站点是Vue的几百倍以上。更大的生态意味着更多可用的资源,以及遇到问题可以得到更多的有效参考与帮助,这也是除了性能之外选择React的核心原因。 选择React之后,应用会在以下几个方面有提升。 第一,原先的html间跳转会有短暂的白屏现象,这一点在安卓性能较差的机器上尤为明显,而React作为单页应用没有这个问题。第二,React 提供的虚拟DOM包含Diff算法,即将原dom copy一份,与改动后的dom对比,只渲染不同的dom节点,实现最小代价渲染,vdom创新的性能优化方式对性能的提升毋庸置疑。第三,React中核心组件化技术,更加容易的绑定事件行为,动态更新特定的dom,代码更加模块化,重用代码更容易,结构清晰易维护。二、在移动端使用React三大框架在移动端分别有自己的东西。Angular的ionic,React的React Native,Vue的Weex。其中ionic 是基于cordova技术,依然是浏览器应用。而后两者已上升到操作原生控件的层面,做出来的是原生界面,其中React Native的成熟度远高于Weex,已经被很多公司使用,而Weex使用者很少。 综合来看选择React 生态明显最佳,由当前的cordova过渡为cordova+Reactjs,然后可以平滑地过渡到React Native,媲美原生性能的最优混合开发方式。之所以说平滑是因为React Native中近90%的代码(JS)可以在IOS和Android端使用,剩余的涉及原生的代码也基本可以找到可用的资源,就像cordova 的插件一样。毕竟如果需要同时掌握JS, OC(或swift),java(或kotlin)才能开发React Native的话,那这门技术不会有人用;当然反过来如果有原生开发知识的话会对开发React Native有一定帮助。 直接转型为React native的话涉及了应用底层架构的变动,有比较大的跨度,而转为cordova+Reactjs相对容易,而由cordova+Reactjs到React Native同样容易不少,因为其中大部分Reactjs代码可以重用。 三、Reactjs开发工具的选择首先开发脚手架官方出了Create-react-app,集成了webpack-当前最流行的打包工具,babel-提高js版本兼容性的转码器,以及ESLint-代码检测工具和其它一些常用工具,同时对这些工具进行了比较优的配置。值得一提的是该脚手架将这些工具的配置文件进行了隐藏,本意是让使用者专注于编码即可,但实际使用时通常会有自己配置的需求,此时执行npm run eject即可出现被隐藏配置文件。 React-router 是官方推荐的路由管理工具,由于是单页应用区别于原先的html界面间跳转,跳转实质是在组件间进行,所以需要有路由管理工具来统一化管理。这里值得一提的是,React-router配合webpack可以实现代码的按需加载。 一般来说,webpack打包后会在生成一个压缩的js文件,在单页应用打开会整体加载这个文件,由于该js文件包含之前所有的js代码,虽然进行了压缩,一般仍至少有几百kb,当应用稍微复杂点,打包后文件会相应变大。而加载的时候,不管那些代码有没有执行到,都会下载下来并进行加载,造成性能浪费,这一点在显然在web端很重要,而在cordova中是将js代码直接打包在本地,等于跳过了下载步骤但仍然会有加载过程。通过在router中写require.ensure代码并在webpack中相应地修改配置即可将js分成多个文件,在需要时加载对应的js文件,实现按需加载。 Redux 是应用最广泛的第三方状态管理工具,其作用是当应用中多数据状态交互时,可以更有方便且代码结构清晰地统一管理状态,下图给出了形象的阐释。由于在实际开发中一般是分人员/分功能模块独立开发,考虑引入redux的成本(redux本身略复杂),可以在没有多数据交互的模块不使用redux,而在类似涉及增删改查的表单以及即时通讯websocket等契合redux的模块使用。 为项目选取合适UI组件库,一定程度上简化UI样式的开发并且考虑使用其提供的过渡动画效果。这方面有比较多的选择,Google Material Design 风格的Material-UI在github上最受欢迎,但其设计语言与我们当前APP截然不同,腾讯的weui和阿里的antd-mobile 较为相近,其中antd-mobile与create-react-app脚手架配合使用时配置项比较繁杂,因为阿里本意是用来配合自己的脚手架dva(封装了react-router和redux),因此暂时选择weui,后期开发有特定组件需求可结合其他ui库使用。 至于页面跳转时的过渡动画,有些UI库给出了一些过渡样式,比如touchstone。但该库已不再维护,文档不佳,且与新版本的react-router配合使用有不兼容情况。后来浏览官方文档发现官方有动画库react-addons-css-transition-group,使用该库结合css3的动画三件套animation,transition,transform即可实现各种动画效果包括基本的过渡效果,比如渐进平移等。 另外关于css,因为是单页应用,所以如果不加处理,直接import css文件的话最终打包生成一个css文件会导致样式应用到全局,造成同类名样式相互污染影响。解决这个问题有很多种方案。Facebook积极探索css in js方式,但直接写内联样式代码可读性太差。目前解决方案中应用最广泛的是css-modules,即在webpack配置中开启module选项,使用styles对象来写样式。 解决的原理是将css类名在打包后编译成哈希字符串,保持其唯一性。但当想要使用全局样式时要再配置,稍显繁杂,且它类名编写的方式为对象的方式,需要整体修改,另外在使用它时,发现不支持-横线的类命名方式,支持下划线方式,推荐驼峰式,而我们之前html中的样式类名大多是横线命名,这意味着原html和css中的类名都要对应修改,考虑到样式类名非常多,这一方式舍弃。 另外有基于css-modules使用高阶组件的react-css-modules使用人数也比较多,允许横线命名方式且全局本地样式区分简单,但有benchmark测试表明其会较大程度拖累性能,所以也舍弃。解决这个问题要最大程度兼容原先css的写法,即改动最小,因为之前的css类样式数量庞大。 Webpack css-loader 有个属性 :local 加上之后类会变成局部作用域,即webpack会对该类型的类进行自动哈希转码处理,但显然类名一个个加:local 是有些呆板的工作,于是想到可以利用scss的嵌套属性将:local在一个css文件中统一加到类名前。这里涉及到在脚手架create-react-app 添加对scss的支持,在命令行执行安装,并在package.json的scripts中添加watch-css指令,将原css文件改为scss文件,然后在最外层添加:local,执行watch-css命令,即可在scss文件旁自动产生css文件,且类名前自动添加:local 前缀,这种方法实践中发现并非所有类的样式都与:local 兼容良好,相应的可以使用文件名代替:local,要做的就是保持文件名的唯一性,这一点原工程下的文件名已满足。这样原先文件中引入css的方式,全局css引入的方式都不需要变化,做到最小代价。 scss 是 sass 3 引入新的语法,其语法完全兼容 css3,并且继承了 sass 的强大功能,sass和less是前端扩充css常用的方式,添加了嵌套,变量,继承等语法,但需要编译成css来最终使用(稳定性考虑)。 四、Reactjs 和cordova结合有哪些需要注意的开发Reactjs使用官方提供的脚手架Create-react-app,最终通过npm run build生成一个单页网页应用,放入cordova的www目录下即可。由于这两部分开发时独立,而原先开发是在含www目录的cordova工程目录下直接开发,这种不同会产生一些问题。比如cordova中某些插件安装后export函数或者变量供引入使用,因为一开始是分离的,在create-react-app中并找不到这些变量,就造成在build的时候产生变量undefined的错误,尽管最终放到cordova工程中后可以找到变量并正常运行,但在第一步react开发时控制台报一堆error会妨碍调试,影响开发体验。 在github上有一些react cordova 库,但实质上它们都需要通过npm run build来打包,所以并没有解决引入插件变量的问题,且会与create-react-app 有相斥的地方。所以要想办法使插件提供的变量在React中不报错,这里在不影响ESLint 检错机制的情况下可以采取迂回的方式。Build时控制台报错仅针对src文件夹下的代码,而在public文件夹下还有个index.html这个文件会最终被打包放到www目录下,因此可以在这个文件中deviceready时添加全局的插件变量(注意该类全局变量的唯一性,可以添加plugin前缀或使用命名空间等方式保证),并将值传给src目录下的代码中,这样即可绕过控制台build以及调试时的报错。 ...

June 26, 2019 · 1 min · jiezi

reactnative-安全数字键盘

react-native,安全 纯数字键盘组件;所需依赖原生react-native和ant-design RN; 代码比较简单,有这方面需求的小伙伴可以copy随时使用; 可以根据自己实际业务场景作修改 使用 <Keyboard type='deal'//verify,deal title={'請輸入交易密碼'} keyboardVisible={this.state.keyboardVisible} toClose={this.onKeyboard} getPasswordstr={(str) => { console.log(str) }} getPasswordArr={(arr) => { console.log(arr) }} forgot={() => { console.log(1111) }} />源码 import React, { Component } from 'react'; import { StyleSheet, Text, View, Modal, Dimensions } from 'react-native'; import { Grid, Icon } from '@ant-design/react-native'; let width = Dimensions.get('window').width;//获取设备的宽高 let height = Dimensions.get('window').height; let i = 0; export default class Keyboard extends Component { static navigationOptions = { header: null } constructor(props) { super(props); this.state = { keyData: [], hideBg: [], currentIndex: undefined } } componentDidMount() { this.defaultHideDom(); } configItemEl(_el, index) { const { currentIndex } = this.state; if (index <= 8) { return (<Text style={currentIndex === index ? styles.active : null}>{index + 1}</Text>); } if (index === 9) { return (<Text style={{ width: width / 3, height: 50, backgroundColor: '#CCC' }}></Text>); } if (index === 10) { return (<Text style={currentIndex === index ? styles.active : null}>0</Text>); } if (index === 11) { return (<Text style={{ color: '#0080FF', width: width / 3, height: 50, backgroundColor: '#CCC', textAlign: 'center', lineHeight: 50 }}>DELETE</Text>); } } defaultHideDom() { this.setState({ hideBg: Array.from(new Array(6)).map((_val, i) => ({ text: <View style={styles.showPW} key={i}></View>, })) }) }onKeyClick(index, i) { const getPasswordstr = this.props.getPasswordstr; const getPasswordArr = this.props.getPasswordArr; if (index !== 12 && index !== 10 && this.state.keyData.length < 6) { this.setState({ keyData: [...this.state.keyData, index === 11 ? 0 : index], hideBg: this.state.hideBg.map((item, indexKey) => { if (indexKey === i - 1) { item.text = (<View style={styles.showPW}> <Text style={styles.passWorld}></Text> </View>) } return item }) }, () => { if (i === 6) { getPasswordstr(this.state.keyData.join('')); } getPasswordArr(this.state.keyData); }); } if (index === 12 && this.state.keyData.length >= 0) { const arr = this.state.keyData.filter((item, indexKey) => i !== indexKey) this.setState({ keyData: arr, hideBg: this.state.hideBg.map((item, indexKey) => { if (indexKey === i) { item.text = (<View style={styles.showPW}></View>) } return item }) }, () => { getPasswordstr(this.state.keyData.join('')); getPasswordArr(this.state.keyData); }) } }render() { const visible = this.props.keyboardVisible; const type = this.props.type; const title = this.props.title; const toClose = this.props.toClose; const forgot = this.props.forgot; const { hideBg } = this.state; return ( <Modal visible={visible} animationType="slide" transparent={true} onRequestClose={() => { i = 0; this.defaultHideDom(); this.setState({ keyData: [] }); return toClose(); }} > <View style={styles.container}> <View style={styles.header}> <Text onPress={() => { i = 0; this.defaultHideDom(); this.setState({ keyData: [] }); toClose() }} style={styles.iconContainer}><Icon name='close' /></Text> <Text>{title}</Text> <Text style={styles.forgot} onPress={() => { forgot() }}>忘記密碼</Text> </View> {type === 'deal' && (<Grid data={hideBg} columnNum={6} hasLine={false} itemStyle={{ justifyContent: 'center', alignItems: 'center', flex: 1, height: 80, backgroundColor: '#FFF', }} renderItem={(_el, index) => _el.text} />)}<Grid data={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]} columnNum={3} onPress={(_el, index) => { this.setState({ currentIndex: index }, () => { setTimeout(() => { this.setState({ currentIndex: undefined }) }, 10) }); if (_el !== 12 && _el !== 10 && i < 6) { i++ } if (_el === 12 && i > 0) { i-- } this.onKeyClick(_el, i) }} itemStyle={{ justifyContent: 'center', alignItems: 'center', flex: 1, height: 50, backgroundColor: '#FFF' }} renderItem={(_el, index) => this.configItemEl(_el, index) } /> </View> </Modal> ); }}const styles = StyleSheet.create({ active: { width: width / 3, height: 50, backgroundColor: '#0080FF', color: '#FFF', textAlign: 'center', lineHeight: 50 }, forgot: { marginRight: 10, color: '#0080FF', fontSize: 12, fontWeight: null }, passWorld: { borderRadius: 10, width: 10, height: 10, backgroundColor: '#ccc' }, showPW: { justifyContent: 'center', alignItems: 'center', borderColor: '#CCC', borderWidth: 1, borderStyle: 'solid', width: 25, height: 25 }, iconContainer: { width: 25, height: 25, marginLeft: 10 }, header: { width: width, height: 45, flexWrap: 'nowrap', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#FFF', flexDirection: 'row', alignContent: 'space-around', borderBottomWidth: 1, borderStyle: 'solid', borderBottomColor: '#EAEAEA' }, container: { width: width, height: height, justifyContent: 'flex-end', alignItems: 'flex-end', backgroundColor: 'rgba(0,0,0,0.4)', position: 'absolute', top: -25 },});

June 26, 2019 · 3 min · jiezi

reactnative原生loading动画组件

react-native原生loading动画组件代码比较简单,有这方面需求的小伙伴可以copy随时使用; 使用 <Loader visible={this.state.loaderVisible} />源码import React, { Component } from 'react';import { StyleSheet, Text, View, Modal, Animated, Easing, Dimensions, findNodeHandle } from 'react-native';let width = Dimensions.get('window').width;//获取设备的宽高let height = Dimensions.get('window').height;export default class Loader extends Component { static navigationOptions = { header: null } constructor(props) { super(props); this.state = { rotateValue1: new Animated.Value(0), rotateValue2: new Animated.Value(0), rotateValue3: new Animated.Value(0), rotateValue4: new Animated.Value(0), rotateValue5: new Animated.Value(0), rotateValue6: new Animated.Value(0), rotateValue7: new Animated.Value(0), rotateValue8: new Animated.Value(0), fadeAnim1: new Animated.Value(1), fadeAnim2: new Animated.Value(1), fadeAnim3: new Animated.Value(1), fadeAnim4: new Animated.Value(1), fadeAnim5: new Animated.Value(1), fadeAnim6: new Animated.Value(1), fadeAnim7: new Animated.Value(1), fadeAnim8: new Animated.Value(1), viewRef: null } } componentDidMount() { this.startAnimation(); } startAnimation() { this.state.rotateValue1.setValue(0); this.state.rotateValue2.setValue(0); this.state.rotateValue3.setValue(0); this.state.rotateValue4.setValue(0); this.state.rotateValue5.setValue(0); this.state.rotateValue6.setValue(0); this.state.rotateValue7.setValue(0); this.state.rotateValue8.setValue(0); this.state.fadeAnim1.setValue(1); this.state.fadeAnim2.setValue(1); this.state.fadeAnim3.setValue(1); this.state.fadeAnim4.setValue(1); this.state.fadeAnim5.setValue(1); this.state.fadeAnim6.setValue(1); this.state.fadeAnim7.setValue(1); this.state.fadeAnim8.setValue(1); let rotateValue = [ { rotate: this.state.rotateValue1, fade: this.state.fadeAnim1 }, { rotate: this.state.rotateValue2, fade: this.state.fadeAnim2 }, { rotate: this.state.rotateValue3, fade: this.state.fadeAnim3 }, { rotate: this.state.rotateValue4, fade: this.state.fadeAnim4 }, { rotate: this.state.rotateValue5, fade: this.state.fadeAnim5 }, { rotate: this.state.rotateValue6, fade: this.state.fadeAnim6 }, { rotate: this.state.rotateValue7, fade: this.state.fadeAnim7 }, { rotate: this.state.rotateValue8, fade: this.state.fadeAnim8 }, ]; let times = [1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900] let ratateValues = rotateValue.map( (item, i) => Animated.timing(item.rotate, { toValue: 1, duration: times[i], easing: Easing.in(Easing.quad), }) ); let fadeAnims = rotateValue.map( (item, i) => Animated.timing(item.fade, { toValue: 0, duration: times[i], easing: Easing.in(Easing.quad), }) );let parallelArr = fadeAnims.concat(ratateValues); Animated.parallel(parallelArr).start(() => { setTimeout(() => this.startAnimation(), 500) }); } imageLoaded() { console.log(findNodeHandle(this.backgroundImage)) this.setState({ viewRef: findNodeHandle(this.backgroundImage) }); } showComponents() { let Components = [ { rotate: this.state.rotateValue1, fade: this.state.fadeAnim1 }, { rotate: this.state.rotateValue2, fade: this.state.fadeAnim2 }, { rotate: this.state.rotateValue3, fade: this.state.fadeAnim3 }, { rotate: this.state.rotateValue4, fade: this.state.fadeAnim4 }, { rotate: this.state.rotateValue5, fade: this.state.fadeAnim5 }, { rotate: this.state.rotateValue6, fade: this.state.fadeAnim6 }, { rotate: this.state.rotateValue7, fade: this.state.fadeAnim7 }, { rotate: this.state.rotateValue8, fade: this.state.fadeAnim8 }, ]; return Components.map((item, i) => ( <Animated.View key={i} style={{ width: 40, height: 40, top: '50%', left: '50%', marginTop: -40, marginLeft: -25, position: 'absolute', marginBottom: 20, opacity: item.fade, transform: [ { rotateZ: item.rotate.interpolate({ inputRange: [0, 1], outputRange: ['-220deg', '180deg'] }) } ] }}> <View style={styles.dot} /> </Animated.View> )) }render() { const visible = this.props.visible; return ( <Modal visible={visible} animationType="fade" transparent={true} // onRequestClose={() => console.log('onRequestClose...')} > <View style={styles.container}> <Animated.View style={styles.loader} > {this.showComponents()} <Text style={styles.loaderText}>loading...</Text> </Animated.View> </View> </Modal> ); }}const styles = StyleSheet.create({ container: { width: width, height: height, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.2)', }, loader: { flex: 1, justifyContent: 'center', alignItems: 'center', position: 'absolute', width: 120, height: 120, borderRadius: 10, backgroundColor: 'rgba(0,0,0,0.6)', }, dot: { width: 6, height: 6, position: 'absolute', top: 0, left: 24, backgroundColor: '#FFF', borderRadius: 10, }, loaderText: { color: '#FFF', marginTop: 50, }});

June 26, 2019 · 3 min · jiezi

reactnative纯数字键盘组件

示例代码:/*** @auther:wujh@数字键盘组件*/ import React, { Component } from 'react';import { StyleSheet, Text, View, Modal, Dimensions } from 'react-native';import { Grid, Icon } from '@ant-design/react-native'; let width = Dimensions.get('window').width;let height = Dimensions.get('window').height;let i = 0;export default class Keyboard extends Component {static navigationOptions = {header: null}constructor(props) {super(props);this.state = {keyData: [],hideBg: [],currentIndex: undefined}} componentDidMount() {this.defaultHideDom();} configItemEl(_el, index) {const { currentIndex } = this.state;if (index <= 8) {return (<Text style={currentIndex === index ? styles.active : null}>{index + 1}</Text>);}if (index === 9) {return (<Text style={{ width: width / 3, height: 50, backgroundColor: '#CCC' }}></Text>);}if (index === 10) {return (<Text style={currentIndex === index ? styles.active : null}>0</Text>);}if (index === 11) {return (<Text style={{ color: '#0080FF', width: width / 3, height: 50, backgroundColor: '#CCC', textAlign: 'center', lineHeight: 50 }}>DELETE</Text>);}} ...

June 26, 2019 · 2 min · jiezi

React-Native快速入门

准备学习React Native之前,需要了解一下其他知识,帮助你更快的理解RNReact:React中文文档ES6:ES6入门教程 环境搭建本人搭建的是mac+Android环境,具体过程参考:React Native中文网--搭建环境。搭建结束后,运行项目 cd AwesomeProjectreact-native run-android当在手机或模拟器上出现如下页面,则说明配置成功。我在搭建结束后,出现红屏,报错如下: Unable to load script.Make sure you're either running a metro server(run 'react-native start' ) or thatyour bundle 'index.android.bundle' is packaged correctly for release.解决方法我记录在这里:点这里。如果遇到同样的问题可以看下。 语法简介Hello World先看下大概长什么样 import React, { Component } from 'react';import { Text, View } from 'react-native';export default class HelloWorldApp extends Component { state={ text : 'this is state.name' } render() { let pic = { uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' }; const stateName = this.state.name; return ( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <Text>Hello, world!</Text> <Image source={pic} style={{width: 193, height: 110}} /> <Text>{stateName}</Text> </View> ); }}基本的用法Props,State等 与React是一样的,只不过基础组件不同。React里使用常规的html标签,div span等,使用了一些RN自己的原生组件Image,Text等。 ...

June 25, 2019 · 4 min · jiezi

reactnatvie-andriod-名称-图标-启动页-打包发布

react-native andriod 名字 图标 启动页 打包发布名字编辑 android/app/src/main/res/values/strings.xml 文件: <resources>- <string name="app_name">test</string>+ <string name="app_name">测试程序</string> </resources>图标1.替换这个目录下方的突变2.把同级目录下该文件的round删除 启动页使用react-native-splash-screen 打包1.生成签名文件 2.创建key 记住别名 密码 保存到路径 3.编辑~/.gradle/gradle.properties或../android/gradle.properties(一个是全局gradle.properties,一个是项目中的gradle.properties MYAPP_RELEASE_STORE_FILE=your keystore filename MYAPP_RELEASE_KEY_ALIAS=your keystore alias MYAPP_RELEASE_STORE_PASSWORD=***** MYAPP_RELEASE_KEY_PASSWORD=***** 4.编辑android / app / build.gradle文件添加如下代码: ... android { ... defaultConfig { ... } signingConfigs { release { storeFile file(MYAPP_RELEASE_STORE_FILE) storePassword MYAPP_RELEASE_STORE_PASSWORD keyAlias MYAPP_RELEASE_KEY_ALIAS keyPassword MYAPP_RELEASE_KEY_PASSWORD } } buildTypes { release { ... signingConfig signingConfigs.release } } } ... 5.终端进入项目下的机器人目录,运行如下代码:./gradlew assembleRelease ...

June 25, 2019 · 1 min · jiezi

reactnative项目修改名称图标启动页-IOS

react-native项目修改名称、图标、启动页 IOS项目名称info.plist 文件下的 Bundle display name 图标 启动页1.添加启动页2.取消横屏的3.点击launchImage 添加并选择刚开始加的那个launchImage

June 22, 2019 · 1 min · jiezi

纯reactnative-ios打包发布

react-native 发布打包第一步: 导出js bundle包和图片资源1.创建release_ios目录mkdir release_ios 2.在React Native项目的根目录下执行:react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/ 通过上述命令,我们可以将JS部分的代码和图片资源等打包导出到根目录下的release_ios目录下: 第二步:将js包包和图片资源导入到iOS项目中1.把release_ios 下的文件都拉到xcode下的跟目录下,要选第一个和第三个选项,使得图标为蓝色 第三步: 打包发布1.选择开发者账号2.点击xcode 菜单栏product -> Archive 等待打ipa包3.打完包之后,点击xcode菜单栏,4.最后有四个发布选项,第一项发布appstore,第二项hoc100个测试包,然后一直默认到底,最后导出

June 22, 2019 · 1 min · jiezi

React-Native热更新之CodePush

CodePush简介作为一个跨平台应用开发框架,React Native虽然在动态更新方面提供了动态更新的基础,但是动态更新技术并没有想象的那么完善。好在微软开发了CodePush,填补了React Native应用在动态更新方面的空白。 CodePush是微软提供的一套用于React Native和Cordova的热更新服务,借助CodePush,开发者可以直接部署移动应用更新并快速实现代码的热更新,CodePush的官方地址为https://microsoft.github.io/c...。 CodePush作为一个中央仓库,开发者可以实时推送更新,然后客户端应用可以在应用启动时查询更新。借助CodePush,不需要重新审核和安装应用,就可以解决应用的缺陷和添加新特性。CodePush支持的功能如下:• 支持直接对用户部署代码更新;• 管理Alpha、Beta和生产环境应用;• 支持React Native和Cordova跨平台技术的热更新;• 支持JavaScript文件与图片资源的更新; CodePush安装与账号注册使用CodePush之前,需要先安装CodePush命令行工具,并注册CodePush账号和应用,安装命令如下: npm install -g code-push-cli安装完成后可以通过code-push -v命令进行验证。然后,在终端输入命令 code-push register,会打开注册页面让开发者选择授权账号,如图11-11所示。授权通过之后,CodePush会生成一个access key,复制此key到终端即可完成注册,如图11-12所示。除了code-push register命令外,CodePush常用的命令还有:• code-push login:登录CodePush• code-push logout: 注销CodePush• code-push access-key ls:列出access-key• code-push access-key rm <accessKey>:删除某个access-key为了让CodePush服务器知道创建的应用,还需要向服务器进行注册,注册的命令如下: code-push app add <appName> <platform> react-native其中,appName表示应用的名称,platform表示应用的平台。在终端输入命令后即可完成应用的注册,如图11-13所示。 向CodePush添加应用时需要指明应用的平台,成功注册CodePush应用后,每个应用都会生成两个deployment key。其中,Production是用于生产环境的deployment key,Staging则是用于模拟环境的deployment key。注册成功后,可以通过https://appcenter.ms/apps来查...,如图11-14所示。需要说明的是,如果需要同时发布Android和iOS两个平台的热更新,那么在注册CodePush应用时需要注册两个应用,并获取两套deployment key。除了code-push app add命令外,CodePush用于应用管理的命令还有:• code-push app add:在登录账号中添加一个新的应用。• code-push app remove <appName>:在登录账号中删除一个存在的应用。• code-push app rename:重命名一个存在的应用。• code-push app list:列出登录账号下所有的应用。• code-push app transfer:把应用的所有权转移到另外一个账号。 集成CodePush SDK完成CodePush账号的创建和应用的注册操作之后,接下来还需要集成CodePush SDK到React Native应用中。首先,使用react-native init命令新建一个React Native项目,如下所示: ...

June 20, 2019 · 2 min · jiezi

ReactNative搭建报错Unable-to-load-scriptMake-sure

报错Unable to load script.Make sure you're either running a metro server(run 'react-native start' ) or thatyour bundle 'index.android.bundle' is packaged correctly for release.原因我自己这里报错原因是没有找到index.android.bundle。 解决方法方法一创建android/app/src/main/assets文件夹执行命令react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res 重新执行 react-native run-android参考文档:https://stackoverflow.com/que... 方法二修改 项目目录/android/app/build.gradle里主要是设置bundleInDebug等于true,使得开发时也会打包index.android.bundle。参考文档:https://github.com/facebook/r...

June 19, 2019 · 1 min · jiezi

使用-Electrode-OTA-Server-创建私有-Code-Push-服务

一直用着 Microsoft 的 AppCenter.ms 服务都不错,功能强大,但是最近总是抽风,没办法,只能自己部署私有 Code Push Server了,如果直接搜索 Code Push Server,一般得到的结果都是 https://github.com/lisong/code-push-server 这个,我安装过,不过并没有实现去测试,因为发现它并没有完美的实现 Code Push 的逻辑,在各种坛里面找了好几天之后,终于发现了 http://Electrode.io,Walmart Labs 的东西总是这么难发现, Hapijs 也是。 什么是 Electrode ,大家可以直接上官方去了解,我们只使用 Electrode OTA Server 功能,我本身就是一个长期的 HapiJS 用户,所以一看到这货,还是很亲切的。 安装运行环境安装 Node安装 nvmnvm 是一个很不错的 Node 版本管理工具,使用下面任何一个命令安装即可,如果在安装过程中有任何疑问,请直接自行解决 https://github.com/nvm-sh/nvm。 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash或者 wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash安装最新版本 Nodenvm install node安装 Docker这个不是必须的,但是如果只是在本地测试的话,建议安装,Electrode OTA Server 默认使用的是 Apache Cassandra 数据库,有了 Docker 之后,数据库的问题更好解决,否则需要在本机安装个 Cassandra 也是很烦人的一件事情,当然,如果不使用 Cassandra 的话,也可以直接使用 MariaDB 数据,这个下面都会说,因为我的机器配置不高,所以,最终还是选择了 MariaDB 数据库。 ...

June 12, 2019 · 4 min · jiezi

React-Native开发之reactnavigation库详解

众所周知,在多页面应用程序中,页面的跳转是通过路由或导航器来实现的。在0.44版本之前,开发者可以直接使用官方提供的Navigator组件来实现页面的跳转,不过从0.44版本开始,Navigator被官方从react native的核心组件库中剥离出来,放到react-native-deprecated-custom-components的模块中。如果开发者需要继续使用Navigator,则需要先使用yarn add react-native-deprecated-custom-components命令安装后再使用。不过,官方并不建议开发者这么做,而是建议开发者直接使用导航库react-navigation。react-navigation是React Native社区非常著名的页面导航库,可以用来实现各种页面的跳转操作。目前,react-navigation支持三种类型的导航器,分别是StackNavigator、TabNavigator和DrawerNavigator。具体区别如下: StackNavigator:包含导航栏的页面导航组件,类似于官方的Navigator组件。TabNavigator:底部展示tabBar的页面导航组件。DrawerNavigator:用于实现侧边栏抽屉页面的导航组件。需要说明的是,由于react-navigation在3.x版本进行了较大的升级,所以在使用方式上与2.x版本会有很多的不同。和其他的第三方插件库一样,使用之前需要先在项目汇中添加react-navigation依赖,安装的命令如下: yarn add react-navigation//或者npm install react-navigation --save安装完成之后,可以在package.json文件的dependencies节点看到react-navigation的依赖信息。 "react-navigation": "^3.8.1"由于react-navigation依赖于react-native-gesture-handler库,所以还需要安装react-native-gesture-handler,安装的命令如下: yarn add react-native-gesture-handler//获取npm install --save react-native-gesture-handle同时,由于react-native-gesture-handler需要依赖原生环境,所以在需要使用link命令链接原生依赖,命令如下: react-native link react-native-gesture-handler为了保证react-native-gesture-handler能够成功的运行在Android系统上,需要在Android工程的MainActivity.java中添加如下代码: public class MainActivity extends ReactActivity { ... @Override protected ReactActivityDelegate createReactActivityDelegate() { return new ReactActivityDelegate(this, getMainComponentName()) { @Override protected ReactRootView createRootView() { return new RNGestureHandlerEnabledRootView(MainActivity.this); } }; }}然后,就可以使用react-navigation进行页面导航功能开发,如图7-12所示,是使用createStackNavigator实现页面导航的示例。在createStackNavigator模式下,为了方便对页面进行统一管理,首先新建一个RouterConfig.js文件,并使用createStackNavigator注册页面。对于应用的初始页面还需要使用initialRouteName进行申明。同时,导航器栈还需要使用createAppContainer函数进行包裹。例如: import {createAppContainer,createStackNavigator} from 'react-navigation';import MainPage from './MainPage'import DetailPage from "./DetailPage";const AppNavigator = createStackNavigator({ MainPage: MainPage, DetailPage:DetailPage},{ initialRouteName: "MainPage",},);export default createAppContainer(AppNavigator);其中,createStackNavigator用于配置栈管理的页面,它支持的配置选项有: ...

June 4, 2019 · 2 min · jiezi

移动端跨平台方案如何选择

May 31, 2019 · 0 min · jiezi

2019-最新-ReactNativeTypeScriptReduxSaga-实践

最近研究 React Native、Redux Saga 以及 TypeScript 相关的内容,整理成了一个 React Native Template,可以直接使用下面的命令创建一个新的应用: react-native init MyApp --template=parcmg初始化完成之后,按下面的方式执行命令: cd MyAppnode setup.jsnpm installreact-native link react-native-gesture-handler完成之后,即可像往常一样开发了: react-native run-ios模板还在完善中,另外,相关技术要点与总结,稍后有时间再整理一下。

May 31, 2019 · 1 min · jiezi

业内首个-React-Native转微信小程序引擎-Alita-正式发布

作者:京东ARES多端技术团队前言Alita是一套由京东ARES多端技术团队打造的React Native代码转换引擎工具。它对React语法有全新的处理方式,支持在运行时处理React语法,实现了React Native和微信小程序之间的主要组件对齐,可以用简洁、高效的方式把React Native代码转换成微信小程序代码。 Alita不是新的框架,也没有提出新的语法规则,她只做一件事,就是把你的React Native代码运行在微信小程序端。所以Alita的侵入性很低,选用与否,并不会对你的原有React Native开发方式造成太大影响。 Alita项目开源地址: https://github.com/areslabs/alitaReact Native 微信小程序 Alita 具备哪些能力完备的React语法支持Alita的设计目标是要尽可能无损的转换RN应用,即使是已经存在的RN应用,我们也希望只做少量的修改就可以在微信小程序平台运行,所以这就要求Alita必须对React语法有足够的支持,包括 JSX 语法,React生命周期等 JSX语法Alita 支持大部分 JSX 语法,这意味着什么呢?意味着你可以使用React自由的代码方式以及强大的组件化支持,意味着你可以延用自己的编程习惯,不需要对已有的RN代码进行过多修改。这主要得益于 Alita 是在运行时处理 JSX语法,而不是现在社区上常见的编译时处理。 因此 Alita 没有诸如以下社区其他方案的限制: JSX 只允许出现的组件的 render 方法中不能通过 props 传递 JSX 片段或者返回 JSX 的函数不支持在属性上传递函数Alita 转换以下代码毫无压力: 生命周期Alita 支持所有的 React 生命周期。微信小程序本身给组件提供了生命周期,但是这些生命周期在写法和调用上与 React 存在着一些的差异,另外 React 生命周期更加丰富。Alita 在支持 React 生命周期的时候,把它们分为了两类,第一类: componentDidMount,componentDidUpdate,componentWillUnmount 这3个生命周期在微信小程序上有相应的触发时机,比如ready, detached,只需要在微信小程序相关回调触发的时候,调用 React 组件对应的方法即可。另外一类,在微信小程序端没有直接对应的生命周期,对于这一类生命周期,主要是借助于 Alita 内部嵌入的 mini-react,触发相应的回调。通过这两种方式,Alita 实现了 React 生命周期的对齐。 此外,Alita 抹平了 RN 和微信小程序之间的事件及样式差异,能够无损得将RN事件和样式传递到微信小程序中。 RN基本组件和APIRN 提供了很多基本的组件和 API,这些组件加上 React 开发方式,共同构成了 RN 应用。Alita 除了要对 React 语法进行处理,还必须在预先在微信小程序平台对齐出一套与 RN 等效的组件和 API。比如在 RN 端,请求网络的方式是通过 fetch 方式,但是微信小程序本身并不存在 fetch 方法,就这要求 Alita 必须基于微信小程序的网络 API,在微信小程序上实现一个 fetch 方法。 同样的以 RN 组件 FlatList 为例,当 Alita 把 RN 应用转化为微信小程序代码之后,FlatList 在微信小程序平台并不存在,需要预先在微信小程序平台实现小程序版本的 FlatList 。这个预先处理的过程,我们称之为对齐,对齐的过程包括组件,组件属性,API 等。 ...

May 30, 2019 · 1 min · jiezi

国际化i18n-各国语言缩写

国际化开发的各国语言标识 **国家地区** **语言标识**简体中文(中国) zh_CN繁体中文(台湾地区) zh_TW繁体中文(香港) zh_HK英语(香港) en_HK英语(美国) en_US英语(英国) en_GB英语(全球) en_WW英语(加拿大) en_CA英语(澳大利亚) en_AU英语(爱尔兰) en_IE英语(芬兰) en_FI芬兰语(芬兰) fi_FI英语(丹麦) en_DK丹麦语(丹麦) da_DK英语(以色列) en_IL希伯来语(以色列) he_IL英语(南非) en_ZA英语(印度) en_IN英语(挪威) en_NO英语(新加坡) en_SG英语(新西兰) en_NZ英语(印度尼西亚) en_ID英语(菲律宾) en_PH英语(泰国) en_TH英语(马来西亚) en_MY英语(阿拉伯) en_XA韩文(韩国) ko_KR日语(日本) ja_JP荷兰语(荷兰) nl_NL荷兰语(比利时) nl_BE葡萄牙语(葡萄牙) pt_PT葡萄牙语(巴西) pt_BR法语(法国) fr_FR法语(卢森堡) fr_LU法语(瑞士) fr_CH法语(比利时) fr_BE法语(加拿大) fr_CA西班牙语(拉丁美洲) es_LA西班牙语(西班牙) es_ES西班牙语(阿根廷) es_AR西班牙语(美国) es_US西班牙语(墨西哥) es_MX西班牙语(哥伦比亚) es_CO西班牙语(波多黎各) es_PR德语(德国) de_DE德语(奥地利) de_AT德语(瑞士) de_CH俄语(俄罗斯) ru_RU意大利语(意大利) it_IT希腊语(希腊) el_GR挪威语(挪威) no_NO匈牙利语(匈牙利) hu_HU土耳其语(土耳其) tr_TR捷克语(捷克共和国) cs_CZ斯洛文尼亚语 sl_SL波兰语(波兰) pl_PL瑞典语(瑞典) sv_SE西班牙语 (智利) es_CL

May 24, 2019 · 1 min · jiezi

在-ReactNative-中持久化-redux-数据

在最近的一个项目中,要求对 redux 数据做持久化处理,经过研究后成功实现,在此记录一下过程 我们可以使用 redux-persist 对数据做持久化处理 安装npm i --save redux-persist使用安装成功后,我们需要对 store 代码进行修改,这是我的 store 生成文件 import {applyMiddleware, createStore, compose} from 'redux';import {createLogger} from 'redux-logger';import thunk from 'redux-thunk';import reducers from '../reducers';import {persistStore, persistReducer} from 'redux-persist';import storage from 'redux-persist/lib/storage'const persistConfig = { key: 'milk', # 对于数据 key 的定义 storage, # 选择的存储引擎}# 对 reducers 的封装处理const persistedReducer = persistReducer(persistConfig, reducers)let loggerMiddleware = createLogger();export default function configureStore() { const enhancers = compose( applyMiddleware(thunk, loggerMiddleware), ); # 处理后的 reducers 需要作为参数传递在 createStore 中 const store = createStore(persistedReducer, enhancers) # 持久化 store let persistor = persistStore(store) return {store, persistor}}在 react-native 中,存储引擎默认为 AsyncStorage Android是以key=>value的形式存储在本地sqlite中iOS 是直接存沙盒文件 ...

May 22, 2019 · 1 min · jiezi

React-Native-Linking

最近,我尝试实现在 React Native APP 中调用浏览器打开链接的功能,现记录一下实现过程。React Native 的 Linking API 提供了一个通用的接口来与传入和传出的 App 链接进行交互,例如浏览器。 如果要在 App 启动后也监听传入的 App 链接1. Linking Libraries (iOS) - RCTLinking在 node_modules/Libraries/LinkingIOS 中找到 RCTlinking.xcodeproj,拖到 XCode 工程下的 Libraries 分组里(参考 Linking Libraries - Manual linking)如果XCode 工程下的 Libraries 分组里已经有 RCTlinking.xcodeproj,就不需要再加了。 2 AppDelegate.m 文件中添加// iOS 9.x or newer#import <React/RCTLinkingManager.h>- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{ return [RCTLinkingManager application:application openURL:url options:options];}// iOS 8.x or older#import <React/RCTLinkingManager.h>- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{ return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation];}// Universal Links- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler{ return [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];}使用 Linkingimport { Linking } from 'react-native'如果 APP 被其注册过的外部 url 调起componentDidMount() { Linking.getInitialURL().then((url) => { if (url) { console.log('Initial url is: ' + url); } }).catch(err => console.error('An error occurred', err));}监听事件componentDidMount() { Linking.addEventListener('url', this._handleOpenURL);},componentWillUnmount() { Linking.removeEventListener('url', this._handleOpenURL);},_handleOpenURL(event) { console.log(event.url);}打开链接 url如果系统不知道如何处理给定的 URL,则{@code openURL}方法会调用失败。如果你传入的 URL 不是一个 http 链接,则最好先通过{@code canOpenURL}方法检查一下。对于 web 链接来说,协议头("http://", "https://")不能省略! ...

May 15, 2019 · 1 min · jiezi

如何使用ReactNative快速开发一个APP

从去年的10月份开始,我的大部分工作重心从传统的前端开发转向了使用ReactNative开发APP,在这个过程当中,走过了不少弯路,也遇到了一些技术相关的问题,但总算没有辜负那些对我信任的人。经历过痛苦和无助,终于坚持了下来,一个月的时间把产品成功部署上线了。想想这些日子其中不乏有一些经验,先愿意拿出来和大家交流交流,其中难免有一些不是最优的方法和方案,还望大家多提意见。 背景一开始为了快速的开展业务,我们决定把产品先通过H5的形式进行线上运行,终于在两周的高效率工作情况下,我们产品上线了,但相比较APP,使用H5开发,不能满足我们的产品需要,随后就决定开发APP,但公司这个时候没有APP开发的相关人员,我只好硬着头皮上了,通过学习ReactNative相关的基本知识,然后不断的向身边一些做过RN开发的朋友取经,使用RN开发APP的工作,才慢慢的走上正轨,起初,我打算IOS和安卓都用RN开发,但考虑到工作量和日常的一些事情,我一个人难免会耽误大家的进度。鉴于我平常使用的是window系统,就决定我只开发安卓客户端,IOS客户端我们又招了一个小伙伴。这样我的工作就有APP开发,H5开发,管理后台相关的支持等事情需要做。 基础工作开发APP的基础框架包,一开始使用create-react-app,再后来使用react-native-dva-starter作为基础的框架包。相比较create-react-app这个基础的框架,后者增加了dva和react-navigation模块,其中dva是一个基于redux和redux-saga的数据流方案,主要是为了管理我们项目当中的数据的,其中包括,数据请求,数据模型,数据存储,react-navigation是一套路由系统,可以帮助我们实现页面跳转,并管理历史跳转数据。数据的请求我们可以使用HTML5提供的fetch,也可以像通常开发H5页面那样使用Axios,毕竟请求数据这件事情,只不过是为了发起一个ajax请求,然后把数据拿回来就好,使用什么不太紧要,我在项目当中实际使用Axios来完成这部分的事情。准备好了上面相关的内容之后,我们最最基础的代码内容算是弄好了,后面就可以通过一些第三方的npm包,为你的项目加砖添瓦了;以下是我的项目当中用到的第三方包列表: react-native-splash-screen 开屏广告react-native-swiper 图片轮播react-native-pdf 支持显示PDF文件react-native-picker 列表选择react-native-root-tips toast提示框react-native-dialog dialog模态框react-native-checkbox-component checkbox组件react-native-linear-gradient 实现渐变react-native-version-number APP版本号管理react-native-device-info 获取设备信息react-native-contacts-wrapper-pro 获取用户联系人react-native-code-push APP热更新react-native-image-picker 通过图片列表和拍照选择图片以上不是全部,有些可能没有列出来,一个包的需不需要,往往是根据我们的需求来的,如果可以,你可以添加其他的包进来。 代码结构 以上是不完全的目录结构,具体的内容,各位看官可以去我的代码仓库中去下载,查看详细的内容。我会在文章的底部附上代码相关的地址。 预备知识和环境工欲善其事必先利其器,以上我忽略了一个重要的部分,就是环境搭建的过程。这部分工作说起来不容小觑,没有这一步的胜利,后面所有的事情,都是白搭。关于环境,我们需要一个安卓的模拟器和打包和运营的JAVA环境,以及开发安卓APP相关版本的SDK包。具体环境的搭建详情,大家可以去这里看,然后大家需要有react,webpack,redux的基础知识,以及对MVVM设计思想的初步了解,这样后续的事情,开展起来会顺利一些,不然就会一步三坑,看的一脸懵逼。对了,开发安卓APP,大家一定要了解安卓各个版本在现在的安卓手机中使用的情况,比如说,3年前我们安卓的客户端,最低只支持安卓4.0的系统,然后向上兼容,如果你现在用的是安卓手机,你可以查看下你自己的机器系统版本是多少。一般来讲,安卓8.0系统是这一两年市面上常用机型配置的系统。我的项目当中,是基于安卓8.0系统进行开发的,所以说说,创建安卓虚拟机的时候,我会下载相关版本的SDK,明白了这些,你在开发时候下载SDK的时候,就可以有选择了,不用一股脑的把所有版本的SDK下载到本地,毫不夸张的说,所有安卓版本的SDK资源的大小应该不会小于50G,而且这些资源是从国外那边下载的,如果你真的不小心下载了所有的SDK包,我相信,你会哭的。我配置的安卓模拟器是使用Android Studio中带的,下面是我配置的安卓模拟器的一些信息: 其他开发的过程当中,难免会遇到一些问题,建议大家多看看API文档,如果是第三方包,多看看他们的案例代码信息,如果实在解决不了,您也可以私聊我,我们一起探讨下。以下是APP产品的一些截图 代码地址:https://github.com/mmcai/reac...dva.js地址:https://dvajs.com/React-Native中文文档地址:https://reactnative.cn/react-navigation地址:https://reactnavigation.org/d...

May 15, 2019 · 1 min · jiezi

快速入门-React-hooks-后端集成

作者:LeanCloud 江宏 2019 年 2 月发布的 React 16.8 正式引入了 hook 的功能。它使得 function 组件也像 class 组件一样能维护状态,所有的组件都可以写成函数的形式,比起原有的以 class 的多个方法来维护组件生命周期的方式,简化了代码,也基本消除了因为 this 绑定的问题造成的难以发现的 bug。这篇文章就介绍一下最常用的 state hook,以及在这种新的方式下怎么与后端 API 通讯。 本文以一个管理任务的 Todo list 应用为例,可以增加新的任务,点击可以把任务标记为完成。部署好的效果可以在这里看到,代码在这个 GitHub repo。这个 demo 使用 LeanCloud 作为存储数据的后端,用的是一个 LeanCloud 开发版应用,所以可能遇到请求数超限的情况,建议在本地运行并替换进自己的 AppId 和 AppKey。 这个应用只有一个叫 App 的组件: function App() { const [inputValue, setInputValue] = useState(''); const [todos, setTodos] = useState(undefined); const [error, setError] = useState('');开头先定义了它使用的状态。useState 的参数是状态的初始值,它会返回一对结果:用来读取这个状态的一个只读引用,以及一个设置状态新值的函数。这里创建了三个状态: - inputValue: 输入新任务的 <input> 元素的当前值 - todos: 当前显示的任务。这里初始值设为 undefined 表示尚未加载,而 [] 则意味着已经加载过,但是为空。 - error: 当前显示的状态信息。 ...

May 14, 2019 · 2 min · jiezi

从-React-Native-到-Flutter移动跨平台方案的真相

作者:LeanCloud 郑鹏 2018 年 12 月,Google 发布了 Flutter 1.0 正式版,似乎再次点燃了人们对移动跨平台开发的热情。上一次出现类似的情况,是在 15 年年初,Facebook 发布 React Native 的时候。四年不到的时间里,有两家大公司相继推出了自己的移动跨平台方案(当然还有 16 年的时候,微软收购了 Xamarin,不过没有前两个那么引人注目罢了),同时这些方案也受到了市场的追逐。这些现象,似乎预示着,跨平台开发才是移动开发的未来,或者说,跨平台开发才是一种更好的开发方式。 既然它是热点,那肯定有可以讨论的地方。不过,在说 React Native 和 Flutter 之前,我觉得要先谈一谈「跨平台开发」。 移动跨平台方案那什么是「跨平台开发」呢? 通常意义上来说,如果你想在 iOS 以及 Android 系统里,提供有相同内容的 App,那么使用 Apple 提供的构建工具,开发一个 App,然后上架到 AppStore,同时使用 Google 提供的构建工具,开发一个 App,然后上架到 Google Play。这两个 App 的实现,除了使用的工具不同之外,大部分业务逻辑是相同的。你可以发现,在这个过程中,产生了「重复」。 在重构时,如果项目里有大量的重复代码,或者重复逻辑,我们一般会将这些代码或逻辑以函数,模块或库的形式做封装,这个过程最大化的消除了重复的代码,最终达到简化项目的代码这一目的。 所以在我看来,「跨平台开发」也是基于这个思想而产生的,人们想要一套减少甚至不用写重复逻辑的解决方案,然后市场给予了人们期望的方案。跨平台方案的最大特点,可以用 Sun 当年在推广 Java 时,所使用的一句口号:”Write once, run anywhere” 作为总结。这一句话,也被如今的 React Native 以及 Flutter 引用或继承。 React NativeReact Native 是由 Facebook 所主导的跨平台方案,得益于 Javascript 以及 ReactJS 的流行,React Native 在推出时,便受到了大量的追捧。除了跨平台的特性,React Native 最大的特点就是,可以使用 Javascript 来构建移动应用,并且最终应用的表现形式,可以做到和使用原生开发套件开发的应用相差无几。 ...

May 13, 2019 · 2 min · jiezi

React-Navigation-导航栏样式调整底部角标消息提示

五一佳节匆匆而过,有人选择在外面看人山人海,有人选择宅在家中度过五一,也有人依然坚守在第一线,致敬! 这是坚持学习react-native的第二篇文章,可能会迟到,但是绝不会缺席,这篇要涉及到的是react-navigation,也是rn社区主推的一个导航库。 网上关于react-navigation的基本使用也是一抓一大把,这里对于它的使用不做过多介绍,主要记录使用过程中的其他问题。 因为android 和iOS 手机的不同,导航栏的显示也不太一样,而这篇文章会尽量的配置属性,让两端的导航栏样式、页面跳转的动画保持一致,同时还会介绍底部导航栏添加角标的方法。 这里使用的是3.9.1版本,网上好多文章是2.x版本的,用法基本大同小异。 android 导航栏标题居中适配默认情况下,iOS的标题居中显示,而android的则不!!! 解决:createStackNavigator的defaultNavigationOptions属性里配置textAlign和flex const AppStackNavigator = createStackNavigator({ HomeScreen: {screen: HomeScreen}, RainScreen: {screen: RainScreen}},{ defaultNavigationOptions:{ ... headerTitleStyle: { ... textAlign: "center", //用于android 机型标题居中显示 flex:1 } }})注:android机型标题默认不居中,textAlign和flex的属性配置用于android机型标题居中显示。 在这种情况下,如果配置了headerLeft或者headerRight 属性,会出现标题偏移的现象。 直接在defaultNavigationOptions里配置空view的headerLeft和headerRight defaultNavigationOptions:{ ... headerTitleStyle: { ... textAlign: "center", //用于android 机型标题居中显示 flex:1, }, headerRight: <View/>, headerLeft: <View/> }这时候标题居中,同时可以在各自的页面里面去重写headerLeft的样式。 android 导航栏去除阴影样式android的导航栏还有阴影的样式,添加elevation 设置阴影的偏移量 defaultNavigationOptions:{ headerStyle:{ backgroundColor:"#fff", elevation:0.5 }, ...}至此的导航栏的效果跟iOS基本保持一致。 android 页面跳转动画,自右向左打开默认的android页面跳转是自下而上打开页面,而要与iOS的保持一致的自右向左,配置transitionConfig属性。 const AppStackNavigator = createStackNavigator({ HomeScreen: {screen: HomeScreen}, ...},{ defaultNavigationOptions:{ ... }, transitionConfig: () => ({ screenInterpolator: (sceneProps) => { return StackViewStyleInterpolator.forHorizontal(sceneProps) }, }),})底部导航添加消息角标有时候我们会遇到这样的需求,在底部导航处添加消息的角标,提醒用户阅读的。 在tabBarIcon的属性里直接添加图标显示的,这里的msg变量数值是全局的,只做演示使用,实际项目里可以根据接口返回数据,可以搭配mobx 一起使用。 ...

May 6, 2019 · 1 min · jiezi

reactnativebaidumap使用及注意问题

使用组件: react-native-baidu-map获取百度地图API_KEY地址:http://lbsyun.baidu.com,在控制台成功创建应用后,就可以看到应用的api key了 安装yarn add react-native-baidu-map原生部分Android配置react-native link react-native-baidu-map配置AndroidManifest.xml文件1.在<application>中加入如下代码配置开发密钥(AK) <application> <meta-data android:name="com.baidu.lbsapi.API_KEY" android:value="开发者 key" /> </application>2.在<application/>外部添加如下权限声明: //获取设备网络状态,禁用后无法获取网络状态<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />//网络权限,当禁用后,无法进行检索等相关业务<uses-permission android:name="android.permission.INTERNET" />//读取设备硬件信息,统计数据<uses-permission android:name="android.permission.READ_PHONE_STATE" />//读取系统信息,包含系统版本等信息,用作统计<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />//获取设备的网络状态,鉴权所需网络代理<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />//允许sd卡写权限,需写入地图数据,禁用后无法显示地图<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />//这个权限用于进行网络定位<uses-permission android:name="android.permission.WRITE_SETTINGS" />//这个权限用于访问GPS定位<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />//获取统计数据<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />//使用步行AR导航,配置Camera权限<uses-permission android:name="android.permission.CAMERA" />//程序在手机屏幕关闭后后台进程仍然运行<uses-permission android:name="android.permission.WAKE_LOCK" />IOS配置使用pod,Podfile文件中添加 pod 'React', :path => '../node_modules/react-native', :subspecs => [ 'Core', 'CxxBridge', 'DevSupport', 'RCTText', 'RCTNetwork', 'RCTWebSocket', 'RCTAnimation' ] pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga' pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' pod 'react-native-baidu-map', :podspec => '../node_modules/react-native-baidu-map/ios/react-native-baidu-map.podspec'注意!!!:AppDelegate.m init 初始化,使用如下代码,可以解决RCTBaiduMapViewManager.h文件找不到的问题#import <react-native-baidu-map/BaiduMapViewManager.h>- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ ... // 地图 ak 注册 [BaiduMapViewManager initSDK:@""]; ...}使用导入import { MapView, MapTypes, Geolocation, Overlay } from 'react-native-baidu-map'const { Marker } = Overlay;<MapView width={deviceWidth} height={200} zoom={18} trafficEnabled={true} zoomControlsVisible={true} mapType={MapTypes.SATELLITE} center={{ longitude: 116.465175, latitude: 39.938522 }}> <Marker title='中心' location={{longitude: 116.465175, latitude: 39.938522}} /></MapView>效果,上图 ...

May 6, 2019 · 1 min · jiezi

Reactnative-集成reactnativegetui-爬坑

问题:集成后react-native run-android 可以运行,但是打包的时候报错。 解决方法 修改react-native-getui包下面android目录下的 build.gradle 1.首先在node_modules中找到报错的包里面的build.gradle,比如我这个就是node_modulesreact-native-getuiandroidbuild.gradle;2.修改这个build.gradle,使其与android/build.gradle(也可能是android/app/build.gradle)里面的SDK版本保持一致 android { compileSdkVersion 28 buildToolsVersion "28.0.2" ...}3.将build.gradle里的compile改为implementation,因为compile已过时。 dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:support-v4:25+' implementation 'com.facebook.react:react-native:+'}4.重新打包 附图:修改后的代码

April 29, 2019 · 1 min · jiezi

ReactNative-官方示例演示-ios-android

React-Native-Demo是基于 react-native 官方文档,把文档所列出的基础组件(简洁、易用、高效) 和 API 实现一遍,并以演示的形式呈现出来。目的是为了直观的熟悉官方提供了哪些能力,使之更熟悉 react-native ,为开发做好充分的准备。将持续更新,保持和英文文档进度一致;另还将收录一些第三方库和项目沉淀的一些组件,若有任何问题欢迎交流讨论。具体参见项目演示图例 <details><summary>查看更多图例</summary> </details> 通过下面的二维码,可以在手机中安装体验 React-Native-Demo: 二维码描述Android reactNativeDemo.apkIOS...????注:微信等扫码跳转,在浏览器打开允许下载即可。第三方框架及开发环境开发环境: macOS 10.14.3node "v8.8.0"react-native-cli "2.0.1"Android Studio "3.2"Xcode "10.2.1"第三方框架 [react-native(0.59.4)]()[react-native-fs]()[react-native-vector-icons]()[react-native-webview]()[react-navigation]()更新进度基础组件 [x] View[x] Text[x] TextInput[x] Button[x] Image[x] ImageBackground[x] Slider[x] Switch通用组件 [x] ActivityIndicator[x] ScrollView[x] FlatList[x] SectionList[x] Modal[x] Picker[x] StatusBar[x] ViewPagerAndroid[x] TouchableHighlight[x] TouchableOpacity[x] TouchableWithoutFeedbackComponent - Android [x] DrawerLayoutAndroid[x] ProgressBarAndroid[ ] ToolbarAndroidComponent - IOS [x] DatePickerIOS[x] MaskedViewIOS[x] PickerIOS[x] ProgressViewIOS[x] SegmentedControlIOS[ ] SafeAreaView[ ] SnapshotViewIOSAPI - 交互 [x] Alert[x] AccessibilityInfo[x] AppState[x] ToastAndroid[x] CameraRoll[x] Clipboard[x] Dimensions[x] DatePickerAndroid[ ] Geolocation[ ] AsyncStorage[x] ActionSheetIOS[ ] AppRegistry[ ] BackHandler[ ] ImageEditor[x] ImagePickerIOS[ ] ImageStore[ ] InteractionManager[ ] Keyboard[ ] Linking[ ] NetInfo[ ] PanResponder[x] PermissionsAndroid[ ] Settings[x] Share[ ] Systrace[x] TimePickerAndroid[ ] VibrationAPI - 布局 ...

April 28, 2019 · 1 min · jiezi

reactnativewechat安卓点击登录没有回调问题

项目中难免会用到第三方登录和分享,本项目中微信登录使用的第三方组件:# react-native-wechat 使用yarn add react-native-wechatreact-native link react-native-wechat在包名下新建wxapi文件夹,文件夹下新建文件WXEntryActivity.java package com.xxx.wxapi;import android.app.Activity;import android.os.Bundle;import com.theweflex.react.WeChatModule;public class WXEntryActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); WeChatModule.handleIntent(getIntent()); finish(); }}授权登录首先注册 WeChat.registerApp(WechatAppID)授权登录 WeChat.sendAuthRequest('snsapi_userinfo', 'ares') .then((response) => { console.log('-------------------- get wechat data is:', response) getWechatOpenId(response.code) }) .catch((error) => { let errorCode = Number(error.code); if (errorCode === -2) { dispatch(showDropdownAlert('error', '提示', '已取消授权登录')) // errorCode = -2 表示用户主动取消的情况,下同 } else { dispatch(showDropdownAlert('error', '提示', WechatAuthFailed)) // errorCode = -2 表示用户主动取消的情况,下同 } })注意问题打包使用签名文件keystore文件的签名要和微信开发平台中填写的一一致WXEntryActivity.java 中的package com.xxx.wxapi; 包名必须和微信开发平台中填写的包名一直,不然就会出现点击授权登录,回调没有反应的问题,可以解决这个问题:https://github.com/yorkie/rea...

April 25, 2019 · 1 min · jiezi

reactnative打包报错Daemon-AAPT2-aapt23214818971

笔者在工作开发任务中,最近在进行Android打release包测试时,遇到了如下报错,鼓捣了好久(甚是郁闷),终于解决了。 ReactNative版本环境如下 问题描述直接使用react-native run-android运行debug没有问题在没有添加react-native-spinkit这个第三方库是打包也正常添加react-native-spinkit第三库,进行run-android debug运行也正常但是使用cd android && ./gradlew assembleRelease命令打正式包就build失败了报错信息如下:于是开始Google这个错误, Daemon: AAPT2 aapt2-3.2.1-4818971-osx Daemon #0但是各种答案都不能解决这个问题,而且还牵涉出其他的新问题。思来想去,应该是添加的第三库react-native-spinkit出现了问题,终于在issues中找到了答案。原来是第三库中的buildTools,compileSdk 和targetSdk的版本和项目中的对应的版本号不一致导致的。 解决方案如下在项目中androidbuild.gradle文件中的'allProjects'的下方添加如下代码 allprojects { repositories { // Add jitpack repository (added by react-native-spinkit) maven { url "https://jitpack.io" } mavenLocal() google() jcenter() maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url "$rootDir/../node_modules/react-native/android" } }}在allprojects下方添加如下代码 subprojects { afterEvaluate { project -> if (project.hasProperty("android")) { android { compileSdkVersion = rootProject.compileSdkVersion buildToolsVersion = rootProject.buildToolsVersion } } } }打包添加完成后,重新使用cd android && ./gradlew assembleRelease 命令进行打包就顺利成功的打包了,成功截图如下 ...

April 24, 2019 · 1 min · jiezi

时间轴组件reactnativestepindicator使用

最近在写公司的项目,因产品设计需求,需要类似如下的效果,像是一个时间轴 本着不重复造轮子的目标,在最喜欢的github上找寻合适的组件,终于发现了一个非常棒的组件:react-native-step-indicator使用就非常简单了第一步,添加组件依赖,这里墙裂建议大家使用yarn管理项目依赖 yarn add react-native-step-indicator第二步,在需要的页面导入react-native-step-indicator import StepIndicator from 'react-native-step-indicator';第三步,就是定义需要的数据及样式 const labels = ["填写认证信息","资料审核","认证完成"];const customStyles = { stepIndicatorSize: 35, currentStepIndicatorSize:40, separatorStrokeWidth: 2, currentStepStrokeWidth: 3, stepStrokeCurrentColor: '#fe7013', stepStrokeWidth: 3, stepStrokeFinishedColor: '#fe7013', stepStrokeUnFinishedColor: '#aaaaaa', separatorFinishedColor: '#fe7013', separatorUnFinishedColor: '#aaaaaa', stepIndicatorFinishedColor: '#fe7013', stepIndicatorUnFinishedColor: '#ffffff', stepIndicatorCurrentColor: '#ffffff', stepIndicatorLabelFontSize: 13, currentStepIndicatorLabelFontSize: 13, stepIndicatorLabelCurrentColor: '#fe7013', stepIndicatorLabelFinishedColor: '#ffffff', stepIndicatorLabelUnFinishedColor: '#aaaaaa', labelColor: '#999999', labelSize: 13, currentStepLabelColor: '#fe7013'} labels数组定义的是时间轴上的节点值 第四步,在render函数中添加组件就完事了 <StepIndicator stepCount={3} customStyles={customStyles} currentPosition={this.state.currentPosition} labels={labels} /> ...

April 24, 2019 · 1 min · jiezi

在原生-React-Native-应用中使用-Expo-API

本文翻译自 Expo 的一篇博客:You can now use Expo APIs in any React Native app 注: 本文最初于 2019 年 2 月 28 日发布,随后于 2019 年 3 月 14 日更新,以反映 Workflow 的改进。从今天开始,你可以在任何 React Native 应用程序中使用尽可能少或尽可能多的 Expo SDK。 我们已经花了很多时间构建和维护这些包含原生应用特性的跨平台 API,我们很高兴最终实现了向整个 React Native 生态共享这些 API,并将它们作为一个整体继续优化。 Expo 未来的两个主要的工程流分别是 Managed 和 Bare。 Managed 应用程序是通过 expo-cli、移动设备上的 Expo 客户端和我们的各种服务: Push Notifications、构建服务和 无线(OTA)更新 构建的。 Expo 试图尽可能多地为你管理构建应用程序的复杂性,所以我们称之为 Managed Workflow。 另一方面,Bare 应用程序将所有的控制权(以及随之而来的复杂性)交给了开发人员。 关于 Bare Workflow,可以参考:“Hello World” guide for bare projects in the Expo docs我们称这个初始版本为预览版,因为还它没有我们希望的那样足够简化,但是我们希望尽快把这些功能交到用户手中,因为它们已经是一个很大的改进了。 ...

April 24, 2019 · 2 min · jiezi

React Native 0.59.x新特性解读

概述众所周知,在现在的前端技术开发栈中,跨平台开发是一个重要的课题,不管是老牌的Hybird还是最近流行的RN、Weex还是Flutter,不得不说,现在前端和客户端的界限越来越模糊。最近在写《React Native跨平台开发进阶》一书,也是对之前的《React Native移动跨平台开发实战》的升级(ps,由于之前的写作功底较浅,所以写的并不是很好)。最近,RN发布了 0.59.x 系列版本,可以发现上层设计出现了比较大的调整,经过体验之后,就想聊聊RN新版本的升级体验和新特性。相信从事移动开发的同学都清楚,最近两三年来,跨平台开发的技术可以说是越来越盛行,前端和客户端的界限也越来越模糊。我对RN的认识是2016年携程的一次技术分享,当时只是觉得使用js来写客户端很快,虽然当时的性能并不是很好,抱着拥抱新技术的态度,我在跨平台的方向上也越走越远、越走越深。曾经,Aribnb 的 “为什么 Airbnb 放弃了 React Native” 让我一度怀疑RN是不是要凉凉了,不过好在Facebook 并没有放弃 RN,甚至官宣《Facebook 正在重构 React Native,将重写大量底层》 ,虽然重构后的RN还没有对外发布,但是可以遇见,重构后的RN将变得越来越好。最近,Facebook更新了最新版本0.59.4,新版本主要更新了以下新特性:1、减轻了 React-Native 自身框架,将 webView 、viewPager、netinfo、async-storage 等内置包拆分,通过社区独立维护,并逐步模糊 React 和 React-Native 的界限。2、更新 JavaScriptCore 、upgrade 和 CLI 工具。3、支持 React Hooks 。4、修复了 FlatList 等列表控件中的诸多问题。按照这一趋势,未来React Native还将在以下方便加大力度:1、减轻 JSBridge 的依赖。2、通过 Fabric UI架构,将 Shadow 层、 UIManager 、NativeModule 从 Java 移到 C++ 中,从而支持 双向的同步和异步渲染与调用 。关于这方面的精简的知识,大家可以参考京东的 《庖丁解牛!深入剖析 React Native 下一代架构重构》和携程的 《携程开源RN开发框架CRN》新特性在0.59.0版本发布以来,RN最近都在经历小版本的迭代,最近的版本为0.59.4。在0.59.x版本中主要有一些新的功能和特性:React HooksReact Hooks 是此版本的一部分,它允许跨组件重用有状态逻辑。Hooks的内容可以参考下面的内容:Introducing Hooks:解释了为什么在 React 添加 Hook。Hooks at a Glance:对内置 Hooks 的快速预览。Building Your Own Hooks:演示了使用自定义 Hooks 重用代码。[**]Making Sense of React Hooks](medium.com/@dan_abramo…):探索了 Hooks 解锁的新可能性。useHooks.com:展示社区维护的 Hooks 清单和 demo。JavaScriptCoreReact Native 使用新的 JSC(JavaScriptCore)为应用程序提供支持。众所周知,Android 上的 JSC 已经存在了几年,这意味着很多现代 JavaScript 功能都不受支持。更糟糕的是,与 iOS 的现代 JSC 相比,它表现不佳。随着这个版本的发布,Android的JSC将带来革命性的改变。并且在 @DanielZlotin,@ dulmandakh,@ gengjiawen,@kmagiera 和 @kudo 等大神的努力下,Android 的 JSC 还将支持 64 位芯片,同时性能也大幅改进。更快的启动与内联需求新版本带来了更高的性能,因此应用的启动速度更快,这是因为新版本允许开发者根据需要加载资源。此功能称为“内联需求”(inline requires),因为它允许 Metro 识别延迟加载的组件。并且具有深入和多样化组件架构的应用程序将获得最大的改进。当升级到 0.59.0版本时,工程会有一个新的 metro.config.js 文件;将选项设置为 true 并向我们提供反馈!更多信息可以参考文档 Performance 。精简核心库React Native重构的重要内容就是精简核心库,对于一些非核心的组件和内容,React Native会将其作为插件包方式进行隔离。众所周知,React Native 是一个庞大而复杂的项目,具有复杂的 repository,为了加快应用的启动速度,精简核心库是我们所做的一些努力,通过将代码迁移到单独的库以更好地管理来解决这些问题。改进的CliReact Native 的命令行工具是开发人员进入生态系统的入口点,但它们长期存在问题并且缺乏官方支持。新版本的Cli脚手架 工具已移至新的 repository,由一组专门的维护人员进行维护和开发。React Hooks此次版本,让我最感兴趣的就是React Hooks。具体来说,React Hooks是前端的技术,但是React Native也支持React Hooks,可以说FaceBook是在模糊React和React Native的界限,未来这二者的区别将越来越小。所谓React Hooks,就是在 react 函数组件中,也可以使用类组件(classes components)的 state 和 组件生命周期,而不需要在 mixin、 函数组件、HOC组件和 render props 之间来回切换,使得函数组件的功能更加实在,更加方便我们在业务中实现业务逻辑代码的分离和组件的复用。事实上,设计React Hooks是为了解决以下几个问题:很难复用逻辑(只能用HOC,或者render props),会导致组件树层级很深;会产生巨大的组件(指很多代码必须写在类里面);类组件很难理解,比如方法需要bind,this指向不明确。…并且,React Hooks确实有着明显的优势:可以更好的减少代码量;同时降低代码在生命周期执行过程中造成的阻塞;自定义 Hooks 可以在一定程度上解耦,增加复用,减少嵌套;函数式编程的风格让函数功能独立,代码简洁更好阅读。具体来说,在React Native新版本中,React Hooks 提供了以下几个最常用默认接口:useState:在函数中快速添加状态;useEffect:快速添加生命周期处理;useImperativeHandle:快速对外暴露接口借用React Hooks,开发者可以在一定程度上节省大量的代码,并且提供清晰的状态管理逻辑。例如:import React, {Component, useReducer, useRef, useImperativeHandle, forwardRef} from ‘react’;import {Text, View, TouchableOpacity,} from ‘react-native’;const initialState = {count: 0};function reducer(state, action) { switch (action.type) { case ‘reset’: return initialState; case ‘increment’: return {count: state.count + 1}; case ‘decrement’: return {count: state.count - 1}; default: return state; }}export function DemoCounter({initialCount}) { const [state, dispatch] = useReducer(reducer, {count: initialCount}); return ( <View> <Text>Count: {state.count}</Text> <TouchableOpacity onPress={() => dispatch({type: ‘reset’})}> <Text>Reset</Text> </TouchableOpacity> <TouchableOpacity onPress={() => dispatch({type: ‘increment’})}> <Text>+</Text> </TouchableOpacity> <TouchableOpacity onPress={() => dispatch({type: ‘decrement’})}> <Text>-</Text> </TouchableOpacity> </View> )}由于Hooks 内部利用了数组来实现状态数据的顺序更新,所以使用Hooks 时不能在循环或者条件判断中使用。因为 Hooks 内的数组每次都是顺序的调用的,如果在条件判断中打乱了顺序,将导致游标无法匹配到正确的数据,所以约定了不要在 if 或者 for 中使用 useState 等行为。由于React Hooks是一个新的内容,我对React Hook理解也不是很深入,具体可以参考 React Hooks 官网或者深入理解React Hooks。参考: React HooksFacebook 正在重构 React Native,将重写大量底层庖丁解牛!深入剖析 React Native 下一代架构重构携程开源RN开发框架CRN ...

April 17, 2019 · 2 min · jiezi

React Native 常用的 15 个库

本篇 React native 库列表不是从网上随便找的, 这些是我在我的应用中亲自使用的库。 这些库功能可能跟其它库也有,但经过大量研究并在我的程序中尝试后,我选择了这些库。15. React Native Animatable这个库非常适合快速地向 React Native 应用程序添加简单的动画和转换。这个库有两种使用方式:声明式和命令式。声明式用法只需使用动画的名称,该动画将在加载该元素时立即生效。打开页面时,标题应该从左边滑进去。如果你想手动播放动画,这个wgy命令式用法就很好用。当有人喜欢某个帖子时,摇动一个心形图标。你也可以定义你自己的动画!对于复杂的动画,可以查找 React Native 的 Animated 的 API。实际案例14. React Native Push Notification这个库支持本地推送通知功能比较全面。它具有日程通知、基于日、周、时间的重复通知等其他库中没有的功能。如果你的应用程序具有离线可用并且需要推送通知,则此库是你的选择。13. React Native FCM如果你的应用程序需要使用 GCM 或 FCM 从服务器发送远程通知,那么这个库就你选择之一,FCM 只是 GCM 的最新版本。这个库还支持带有调度和重复支持的本地通知。因此,如果你同时需要远程和本地通知,那么可以使用 response-native-fcm 12.React Native Hyperlink一个简单的 react-native 超链接组件的可以让 url,模糊链接,电子邮件等可点击。它还支持样式化链接。只要将 Text 组件作为子组件传递给 Hyperlink 组件,库就会处理一切。实际案例11. React Native Sound你需要在应用中播放声音或音乐的库。 我使用这个库来播放应用程序声音并播放录制的答案。实际案例下面是React native应用程序声音的演示视频:https://youtu.be/DpE_8j-aq0I10. React Native loading spinner overlay一个简单但非常有用的组件。当你希望阻止用户在处理某些内容时执行任何其他操作时,你可以使用此组件。 通过在 Android 中处理后退按钮,该组件也做得很好。 示例:提交帖子9. React Native Progress在应用程序中,显示加载或任何其他操作的进度是很重要的。这个库通过支持5个不同的组件,如线性进度条、圆形、饼状图等,可以很容易地显示进度。实际案例8. React Native SwiperReact Native swiper对于实现App intro,Image carousel和Image Galleries非常有用。下面是React native swiper 的演示视频:https://www.youtube.com/watch…7. React Native Share与UI自定义分享组件,它还支持分享文件。实际案例6. React Native Photo View具有缩放支持,onload 回调,缩放以适应和滚动指示器支持的 Image 组件。 此组件存在高分辨率图像问题。 当然,这不是React Native 的特定问题。 当存在高分辨率图像时,内存问题在 Android 上很常见。5. React Native Image Picker这是图像上传或图像处理的基本库。 它支持从图库中选择,从相机拍摄照片。 我喜欢这个库中另一个有用的功能是选择图像分辨率的选项,此功能解决了由于高分辨率图像导致的内存问题。4. React Native Simple Store这个库只是 React Native 的内置 AsyncStorage API的封装,但它非常有用,因为它具有Promises、l链式调用和超级简单的 API 等特性。3. React Native Vector Icons这是最好的 Icon 组件。 它捆绑了 10 个图标集,图标按钮组件,还允许你使用字形图,Fontello 和 TTF 文件导入自定义图标集。捆绑图标集:Entypo by Daniel Bruce (411 icons)EvilIcons by Alexander Madyankin & Roman Shamin (v1.8.0, 70 icons)FontAwesome by Dave Gandy (v4.7.0, 675 icons)Foundation by ZURB, Inc. (v3.0, 283 icons)Ionicons by Ben Sperry (v3.0.0, 859 icons)MaterialIcons by Google, Inc. (v3.0.1, 932 icons)MaterialCommunityIcons by MaterialDesignIcons.com (v2.0.46, 2046 icons)Octicons by Github, Inc. (v5.0.1, 176 icons)Zocial by Sam Collins (v1.0, 100 icons)SimpleLineIcons by Sabbir & Contributors (v2.4.1, 189 icons)2. React Native Modalbox这个 Modal 库是基于 React Native 的 Modal组件构建的,但附带了许多自定义和功能。 它具有在应用程序中使用 Modals 所需的所有功能。实际案例1. React Native Router Flux导航是 React Native 社区中的主要问题之一,因为它没有默认导航系统。 无论 React Native 出现什么导航系统总是有变化或不稳定。这个库帮助我使用一个非常简单的声明性API快速实现导航。 它维护一堆路线并从应用程序中的任何场景导航到任何场景就像调用函数一样简单。它也支持选项卡式导航,侧边栏和模态框。 可以将模态框定义为场景,以便可以从任何场景调用模态。你可以已经在用 React-Navigation 了,并想知道我为什么要使用 React Native Router Flux? 不要担心 React Native Router flux v4 基于 React-Navigation 并且具有更简单的 API!上面的大多数应用程序演示都使用 React-native-router-Flux 作为导航系统。总结如果你使用一个不在上面列表中的真棒React Native库,请在下面的评论中告诉我!你的点赞是我持续分享好东西的动力,欢迎点赞!欢迎加入前端大家庭,里面会经常分享一些技术资源。 ...

April 16, 2019 · 2 min · jiezi

react-native 金币彩带雨下落动画

日常项目中,经常遇到一些表情雨/金币雨/彩带雨 等下落的动画,之前做android原生的时候,写过类似的效果,主要通过自定义view 在onDraw里绘制下落的过程,具体可以看下我的这篇github地址android 仿微信表情雨下落,现在转战 react-native,同样可以实现这样的效果,主要用到的动画库 react-native-animatable 安装 yarn add react-native-animatable库 主要用到的动画是移动下落,即translateY,从屏幕顶部下落至底部,同时过程中可以左右摇摆,每次随机图片下落。_FailAnimation = ({style,duration,delay,startY,speed,children}) => { return <Animatable.View //下落动画 style={style} duration={duration} delay={delay} animation={{ from: {translateY: startY}, to: {translateY: this.state.parentHeight + speed} }} easing={t => Math.pow(t, 1.2)} useNativeDriver> {children} </Animatable.View>}_SwingAnimation = ({delay, duration, children}) => { return <Animatable.View //左右摇摆动画 animation={{ 0: { translateX: -12, rotate: ‘10deg’, }, 0.5: { translateX: 0, rotate: ‘0deg’, }, 1: { translateX: 12, rotate: ‘-10deg’, }, }} delay={delay} duration={duration} direction=“alternate” easing=“ease-in-out” iterationCount=“infinite” useNativeDriver> {children} </Animatable.View>}主要用到的动画,动画是可以相互嵌套的range(count).map((i) =>( <FailAnimation key={i} startY={startY} speed={speed} style={{ position: “absolute”, left: Math.random() * this.state.parentWidth }} duration={duration} delay={i * (duration / count)} > <SwingAnimation delay={Math.random() * duration } duration={duration} > {this._imgRandom(imgs)} </SwingAnimation> </FailAnimation>))通过外部传属性imgs图片数组<Rain imgs={imgs} count={35} duration={1500}></Rain>源代码github地址 https://github.com/taixiang/reactNativeDemo 这个github地址里后续会记录平时学习工作中用到的rn方面的知识点,这会是一个长期的过程,我自己也会坚持下去。 欢迎关注我的个人博客:https://www.manjiexiang.cn/ 更多精彩欢迎关注微信号:春风十里不如认识你 一起学习,一起进步,欢迎上车,有问题随时联系,一起解决!!! ...

April 14, 2019 · 1 min · jiezi

使用React Native构建App

原文地址:使用React Native构建App最近因为项目需要,深入研究React和React Native,React已经掌握得差不多了,现在集中精力在ReactNative的项目开发。这里需要记录在学习过程中的技术细节,好记性真的不如烂笔头,多写文档总会有好处的。[坑太多,一个个填]本文重点记录使用React Native构建双平台App的过程,同时进一步掌握构建过程中运用的技术。【持续更新,坚持不懈…】搭建开发环境安装react-native-cli:npm i -g react-native-cliAndroid SDK安装Android SDK并启动进行配置:配置环境变量export ANDROID_HOME=~/Library/Android/sdkexport PATH=${PATH}:${ANDROID_HOME}/toolsexport PATH=${PATH}:${ANDROID_HOME}/platform-toolsAndroid 虚拟机设定Genymotion的Android SDK 位置(Android SDK 的路径可以在 SDK Manager 上找到)。模拟器有多款模拟器可供选择,Android Studio自带,Genymotion和夜神模拟器,推荐选择夜神模拟器。配置方法:在Nox/bin目录运行nox_adb.exe connect 127.0.0.1:62001,如果失败,使用adb devices查询,出现版本不一致的情况,可以把Android/sdk目录下的adb.exe拷贝到Nox/bin下,并改名为nox_adb.exe,反过来操作也是可以的。分别打开Android Studio和夜神模拟器,此时运行命令nox_adb.exe connect 127.0.0.1:62001基本上都会成功。新建React Native项目运行react-native init project-name,进入project-name文件夹安装依赖npm i并运行react-native run-android或react-native run-ios构建App。以Android App为例,在Android Studio打开Android文件夹(注意:此处是Android文件夹,不是project-name项目文件夹)。运行项目这一步很关键,配置java的环境变量,首先是JAVA_HOME和ANDROID_HOME:JAVA_HOME,变量值为D:\Android\sdk;ANDROID_HOME,变量值为D:\Android\sdk;然后在Path项中添加jdk和jre下的bin目录;以上是用户变量配置,下面进行系统变量配置:在Path项中添加下图中变量:同时打开Android Studio、Nox并在AS中打开项目中的Android文件夹。运行nox_adb.exe connect 127.0.0.1:62001连接AS和Nox,然后再运行react-native run-android,此时就会构建Android App,

April 13, 2019 · 1 min · jiezi

react native 拖动 小球 边界控制

PanResponder类可以将多点触摸操作协调成一个手势。它使得一个单点触摸可以接受更多的触摸操作,也可以用于识别简单的多点触摸手势。官方地址:https://reactnative.cn/docs/0…import React, {Component} from “react”;import {View, Text, TouchableOpacity, TouchableWithoutFeedback, Dimensions, PanResponder} from “react-native”;var pickerWidth = Dimensions.get(“window”).width - 20;var pickerHeight = 200;export default class PickerPage extends Component { constructor(props) { super(props); this.state = { left: -15, top: -15 } } componentWillMount() { this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetResponderCapture: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderGrant: (e, gestureState) => { this.touchX = this.state.left; this.touchY = this.state.top; }, onPanResponderMove: (e, g) => { let left = this.touchX + g.dx; let top = this.touchY + g.dy; console.log(left, top); if(left >= (pickerWidth - 15)) { left = (pickerWidth - 15); } if(left <= -15) { left = -15 } if(top <= -15) { top = -15; } if(top >= 185) { top = 185 } this.setState({ left: left, top: top }) }, onPanResponderRelease: (e, g) => { this.touchX = this.state.left; this.touchY = this.state.top; } }); } render() { return ( <View> <View style={{width: pickerWidth, height: pickerHeight, backgroundColor: “#468dcc”, position: “relative”, overflow: “hidden”}}> <View style={{ width: 30, height: 30, borderWidth: 4, borderColor: “#000000”, borderRadius: 30, position: “absolute”, left: this.state.left, top: this.state.top, zIndex: 2, // transform: [ // {translateX: this.state.left}, // {translateY: this.state.top} // ] }} {…this._panResponder.panHandlers}></View> </View> </View> ) } componentDidMount() {} componentWillUnmount() {}} ...

April 11, 2019 · 1 min · jiezi

应用现代CSS创建React App项目

来源 | 愿码(ChainDesk.CN)内容编辑愿码Slogan | 连接每个程序员的故事网站 | http://chaindesk.cn愿码愿景 | 打造全学科IT系统免费课程,助力小白用户、初级工程师0成本免费系统学习、低成本进阶,帮助BAT一线资深工程师成长并利用自身优势创造睡后收入。官方公众号 | 愿码 | 愿码服务号 | 区块链部落免费加入愿码全思维工程师社群 | 任一公众号回复“愿码”两个字获取入群二维码本文阅读时长:13min以前使用Create React App,你实际上没有很多选项可以直观地清理。经常处于随机级联样式表 (CSS)项目维护者的心血来潮之中,并且试图让项目编译过程中涉及的其他库,框架或预处理器经常成为一场噩梦。Create React App上下文中的预处理器基本上是构建过程中的一个步骤。在这种情况下,我们讨论的是采用某些样式代码(CSS或其他格式)的东西,将其编译为基本CSS,并将其添加到构建过程的输出中。在本文的篇幅中,我们将介绍涵盖与样式相关的功能的各种材料,并突出显示在我看来,Create React App中最好的新功能之一:支持CSS模块和SASS。介绍CSS模块CSS模块能够以防止引入全局重叠命名空间的方式模块化你所导入的任何CSS代码,尽管最终结果仍然只是一个巨大的CSS文件。更好的项目组织让我们首先清理一下我们项目中的目录结构。我们要做的就是将每个具有CSS和JavaScript代码的组件分离到自己的文件夹中。先来创建NewTodo,Todo,App,TodoList,和Divider文件夹,并将它们所有相关的代码放在其中的每一个文件夹中。我们还需要在每个被调用的目录中创建一个新文件,该文件index.js只负责导入和导出相应的组件。例如,App索引文件(src/App/index.js)将如下所示:import App from “./App”;export default App;Todo (src/Todo/index.js)的新索引文件 如下所示:import Todo from “./Todo”;export default Todo;你可以根据此模式猜测索引文件的内容NewTodo,TodoList以及它们的Divider外观。接下来,我们需要更改引用这些文件的每个位置,以便更轻松地导入所有这些文件。不幸的是,这将是一些繁琐的工作,但我们需要做同样的事情,以确保我们不会破坏过程中的任何事情。首先,在中src/App/App.js,将TodoList import 组件更改为以下内容:import TodoList from “../TodoList”;我们不需要做任何事情,Divider因为它是一个没有导入的组件。NewTodo 并且Todo 是类似的类型,所以我们也可以跳过它们。src/TodoList/TodoList.js另一方面,我们需要处理很多事情,因为它是我们最高级别的组件之一并且进口很多:import Todo from “../Todo”;import NewTodo from “../NewTodo”;import Divider from “../Divider”;但那还不是全部。我们的测试文件src/TodoList/TodoList.test.js也需要修改为包含文件的这些新路径,否则我们的测试将失败!我们需要几乎与之前相同的导入列表:import TodoList from “./TodoList”;import NewTodo from “../NewTodo”;import Todo from “../Todo”;当你重新加载你的应用程序时,你的代码应该仍然正常工作,测试应该全部通过,一切都应该干净利落!我们的完整项目结构现在应如下所示:src/ App/ App.css App.js App.test.js index.js Divider/ Divider.css Divider.js index.js NewTodo/ NewTodo.css NewTodo.js NewTodo.test.js index.js Todo/ Todo.css Todo.js Todo.test.js index.js TodoList/ TodoList.css TodoList.js TodoList.test.js index.js index.css index.js setupTests.js … etc …将CSS模块引入我们的应用程序如果我们想使用CSS模块,我们需要遵循一些简单的指南。首先,我们需要命名我们的文件[whatever].module.css,而不是[whatever].css。接下来我们需要做的是确保我们的样式简单命名并且易于引用。让我们首先遵循这些约定并将我们的CSS文件重命名为Todoas src/Todo/Todo.module.css,然后我们将稍微改变一下内容:.todo { border: 2px solid black; text-align: center; background: #f5f5f5; color: #333; margin: 20px; padding: 20px;}.done {background: #f5a5a5;}接下来,我们将开放src/Todo/Todo.js以利用CSS模块。我们在我们的Todo组件中创建了一个辅助函数cssClasses(),它返回我们应该在组件中使用的样式,并且我们不需要进行更改以使所有工作与之前完全相同。我们还需要import在顶部更改我们的语句,因为我们重命名了文件并且正在改变我们的CSS加载到代码中的方式!看看以下内容:import styles from “./Todo.module.css”;这使我们的代码可以Todo.module.css通过引用它们来利用定义的任何类名styles.[className]。例如,在前一个文件中,我们定义了两个CSS类名:todo和done,所以我们现在可以通过styles.Todo和在组件中引用它们styles.done。我们需要更改cssClasses()函数才能使用它,所以让我们现在进行那些确切的更改。在src/Todo/Todo.js,我们的cssClasses()功能现在应该如下所示: cssClasses() { let classes = [styles.todo]; if (this.state.done) { classes = […classes, styles.done]; } return classes.join(’ ‘); }保存并重新加载,我们的应用程序应该恢复正常!接下来,让我们更改组件hr内部的标签todo以拥有自己的样式和效果。返回src/Todo/Todo.module.css并为我们的hr标签添加以下块,我们将给出一个新类redDivider:.redDivider { border: 2px solid red;}最后,返回我们的render()函数src/Todo/Todo.js,并将render()函数的hr标记包含更改保存并重新加载,现在我们应该完全划分CSS代码而不必担心冲突和全局命名空间!这是输出的样子:与CSS模块的可组合性这并不是CSS模块给我们的全部,尽管它肯定是CSS模块的重要组成部分之一,我们立即得到并毫不费力。我们还获得了CSS可组合性,它能够从其他类继承CSS类,无论它们是否在主文件中。当您设置更复杂的嵌套组件时,这可能非常有用,这些组件都需要处理略有不同的样式表,但彼此之间并没有太大的不同。假设我们希望能够将某些组件标记为关键组件而不仅仅是常规Todos。我们不想对组件做太多改变; 我们希望它继承与所有其他Todos相同的基本规则。我们需要设置一些代码来实现这一目标。回到后面src/Todo/Todo.js,我们将进行一些修改以允许一个名为的新状态属性critical。我们将从constructor 组件开始,我们将添加新state属性和bind 函数标记: constructor(props) { super(props); this.state = { done: false, critical: false };this.markAsDone = this.markAsDone.bind(this);this.removeTodo = this.removeTodo.bind(this);this.markCritical = this.markCritical.bind(this);}我们在critical属性中添加一个新属性,state 并将其设置为默认值false。然后我们还引用了一个函数(我们还没有编写)markCritical,并且我们绑定了this,因为我们稍后将在事件处理程序中使用它。接下来,我们将解决这个问题markCritical(): markCritical() { this.setState({ critical: true }); }我们还需要修改我们的cssClasses()函数,以便它可以对这个新state属性做出反应。为了演示CSS模块的可组合性功能,我们将其设置classes为原来是一个空数组,然后第一个项目变为critical或todo,取决于项目是否标记为critical: cssClasses() { let classes = []; if (this.state.critical) { classes = [styles.critical]; } else { classes = [styles.todo]; } if (this.state.done) { classes = […classes, styles.done]; } return classes.join(’ ‘); }最后,在我们的render函数中,我们将创建button 标记以将项目标记为critical: render() { return ( {this.props.description} Mark as Done Remove Me Mark as Critical ); }我们还没有完成,尽管我们至少有90%的方式。我们还想回到src/Todo/Todo.module.css并为critical类名添加一个新块,我们也将使用我们的可组合属性:.critical { composes: todo; border: 4px dashed red;}要使用合成,您需要做的就是添加一个新的CSS属性,composes并为其指定一个您希望它组成的类名(或多个类名)。在这种情况下,撰写是一种奇特的方式,它表示它继承了其他类名的行为,并允许您覆盖其他类名。在前面的例子中,我们说的critical是一个CSS模块类,它由一个todo 模型作为基础,并添加border 一个大的红色虚线的组件,因为,我们只是说这意味着它是关键的。像往常一样保存并重新加载,您应该能够将项目标记为标记为完成,标记为严重或两者,或通过单击删除我删除它们,如以下屏幕截图所示:这就是我们对CSS模块的简要介绍!在继续之前,您还需要通过在屏幕中点击U来快速更新测试快照yarn test。将SASS引入我们的项目SASS本质上是具有扩展功能支持的CSS。当我在这里说扩展功能支持时,我的意思是它!SASS支持以下功能集,CSS中缺少这些功能集:· 变量· 嵌套· 部分CSS文件· 导入支持· 混入· 扩展和继承· 运算符和计算安装和配置SASS好消息是,在Create React App项目中获得SASS支持非常简单。我们首先需要通过yarn或安装它npm。$ yarn add node-sass我们将看到它的大量输出,但假设没有错误并且一切顺利,我们应该能够重新启动我们的开发服务器并开始使用一些SASS。让我们创建一个更通用的实用程序SASS文件,它将负责存储我们想要在整个应用程序中使用的标准化颜色,以及存储整齐渐变hr模式的东西,以防我们想在其他地方使用它。我们还将更改我们正在使用的一些颜色,以便有一些红色,绿色和蓝色,这取决于项目是分别是关键,完成还是两者都不是。此外,我们需要稍微改变我们的项目,并添加一个新文件,以获得一些共享样式和颜色的概念。那么,让我们开始吧:src/shared.scss在我们的项目中创建一个新文件,并为其提供以下主体:$todo-critical: #f5a5a5;$todo-normal: #a5a5f5;$todo-complete: #a5f5a5;$fancy-gradient: linear-gradient( to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));接下来,跳转到src/Divider/Divider.css并将文件重命名为src/Divider/Divider.scss。接下来,我们将更改对Divider.cssin 的引用src/Divider/Divider.js,如下所示:import “./Divider.scss”;现在我们需要更改代码Divider.scss以在我们的共享变量文件中导入并使用变量作为其中的一部分:@import “../shared”;hr {border: 0;height: 1px;background-image: $fancy-gradient;}因此,我们在新的共享SASS文件中导入src/,然后该background-image值只引用$fancy-gradient我们创建的变量,这意味着我们现在可以在需要时重新创建那个花哨的渐变而无需反复重写它。保存并重新加载,您应该看到没有任何重大变化。混合SASS和CSS模块好消息是,在Create React App中将SASS引入CSS模块基本上并不复杂。事实上,这些步骤是相同的!所以,如果我们想要开始混合这两者,我们需要做的就是重命名一些文件并改变我们的导入处理方式。让我们看看这个行动:首先,返回我们的src/Todo/Todo.module.css文件并进行一个非常小的修改。具体来说,让我们重命名它 src/Todo/Todo.module.scss。接下来,我们需要改变我们的import陈述src/Todo/Todo.js,否则整个事情将崩溃:import styles from “./Todo.module.scss”;现在,我们应该让我们的SASS使用Todo组件的CSS模块,所以让我们开始利用它。再次,我们需要import我们的shared文件放到此文件SASS也。请注意以下内容src/Todo/Todo.module.scss:@import ‘../shared’;接下来,我们需要开始更改对各种背景颜色的引用。我们将常规Todos的背景更改为 $todo-normal。然后,我们将完成的Todo背景更改为 $todo-complete。最后,我们要将critical项目更改为 $todo-critical:.todo { border: 2px solid black; text-align: center; background: $todo-normal; color: #333; margin: 20px; padding: 20px;}.done {background: $todo-complete;}.hr {border: 2px solid red;}.critical {composes: todo;background: $todo-critical;}保存并重新加载我们的项目,让我们确保新的配色方案得到尊重:现在,我们在Create React App项目中很好地集成了CSS模块和SASS,而无需安装单个新依赖项。我们让他们一起玩得很好 ,这是一个更大的成就! ...

April 10, 2019 · 2 min · jiezi

你轻松在React Native中集成统计(umeng)的功能(最新版)

文章地址:https://www.cnblogs.com/songd…

April 10, 2019 · 1 min · jiezi

如何在React Native应用程序中保持动画以60 FPS运行

来源 | 愿码(ChainDesk.CN)内容编辑愿码Slogan | 连接每个程序员的故事网站 | http://chaindesk.cn愿码愿景 | 打造全学科IT系统免费课程,助力小白用户、初级工程师0成本免费系统学习、低成本进阶,帮助BAT一线资深工程师成长并利用自身优势创造睡后收入。官方公众号 | 愿码 | 愿码服务号 | 区块链部落免费加入愿码全思维工程师社群 | 任一公众号回复“愿码”两个字获取入群二维码本文阅读时长:4min任何高质量移动应用程序的一个重要方面是用户界面的流动性。动画用于提供丰富的用户体验,任何jank或抖动都会对此产生负面影响。动画可能会用于各种交互,从视图之间的转换,到对用户在组件上的触摸交互作出反应。高质量动画的第二个最重要的因素是确保它们不会阻止 JavaScript线程。为了使动画保持流畅而不中断UI交互,渲染循环必须在16.67 ms内渲染每个帧,以便可以实现60 FPS。在本文中,我们将介绍几种在React Native移动应用程序中提高动画性能的技术 。这些技术将防止 JavaScript执行中断主线程。对于这篇文章,我们假设你有一个React Native应用程序,它定义了一些动画。怎么做首先,在React Native中调试动画性能时,我们需要启用性能监视器。为此,请显示开发菜单(从模拟器中摇动设备或cmd + D),然后点击显示Perf监视器。iOS中的输出类似于以下屏幕截图:Android中的输出类似于以下屏幕截图:如果您要为组件的过渡(opacity)或尺寸(width,height)设置动画,请确保使用LayoutAnimation。如果要LayoutAnimation在 Android上使用,则需要在应用程序启动时添加以下代码: UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true)。如果您需要对动画进行有限控制,建议您使用AnimatedReact Native附带的库。此库允许您将所有动画工作卸载到本机UI线程上。为此,我们必须将useNativeDriver属性添加到我们的Animated调用中。我们来看一个示例Animated示例并将其卸载到本机线程:componentWillMount() { this.setState({ fadeAnimimation: new Animated.Value(0) }); }componentDidMount() { Animated.timing(this.state.fadeAnimimation, { toValue: 1, useNativeDriver: true }).start(); }如果您无法将动画工作卸载到本机线程上,则仍然可以提供顺畅体验的解决方案。我们可以InteractionManager在动画完成后使用它来执行任务:componentWillMount() { this.setState({ isAnimationDone: false }); } componentWillUpdate() { LayoutAnimation.easeInAndOut(); }componentDidMount() { InteractionManager.runAfterInteractions(() => { this.setState({ isAnimationDone: true }); }) }render() { if (!this.state.isAnimationDone) { return this.renderPlaceholder(); } return this.renderMainScene(); }最后,如果您仍然遇到性能不佳的问题,则必须重新考虑动画策略或将效果不佳的视图实现为目标平台上的自定义UI视图组件。您必须使用iOS和/或Android SDK 本地实现视图和动画 。这个怎么运作本文中的提示着重于防止JavaScript线程锁定的简单目标。我们的JavaScript线程开始丢帧(锁定)的那一刻,我们失去了与我们的应用程序交互的能力,即使它只是一小段时间。这可能看起来无关紧要,但精明的用户会立即感受到这种效果。本文中提示的重点是将动画卸载到GPU上。当动画在主线程(由GPU渲染的本机层)上运行时,用户可以自由地与应用交互,而不会出现口吃,悬挂,抖动或抖动。其他这里有一个useNativeDriver可用的快速参考:功能iOS版Android的style,value,propertys√√decay √timing√√spring √add√√multiply√√modulo√ diffClamp√√interpoloate√√event √division√√transform√√ ...

April 9, 2019 · 1 min · jiezi

React-native 集成react-native-puti-pay 爬坑

问题1:按照官网后运行正常,第二次运行突然报错,让人措手不及WARNING: The specified Android SDK Build Tools version (28.0.2) is ignored, as it is below the minimum supported version (28.0.3) for Android Gradle Plugin 3.2.1.Android SDK Build Tools 28.0.3 will be used.To suppress this warning, remove “buildToolsVersion ‘28.0.2’” from your build.gradle file, as each version of the Android Gradle Plugin now has a default version of the build tools.> Task :app:checkDebugClasspath FAILEDFAILURE: Build failed with an exception.* What went wrong:Could not resolve all files for configuration ‘:app:debugCompileClasspath’.> Could not resolve com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+. Required by: project :app > project :react-native-puti-pay > Failed to list versions for com.tencent.mm.opensdk:wechat-sdk-android-without-mta. > Unable to load Maven meta-data from https://jcenter.bintray.com/com/tencent/mm/opensdk/wechat-sdk-android-without-mta/maven-metadata.xml. > Could not HEAD ‘https://jcenter.bintray.com/com/tencent/mm/opensdk/wechat-sdk-android-without-mta/maven-metadata.xml'. > sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target > Failed to list versions for com.tencent.mm.opensdk:wechat-sdk-android-without-mta. > Unable to load Maven meta-data from https://dl.google.com/dl/android/maven2/com/tencent/mm/opensdk/wechat-sdk-android-without-mta/maven-metadata.xml. > Could not get resource ‘https://dl.google.com/dl/android/maven2/com/tencent/mm/opensdk/wechat-sdk-android-without-mta/maven-metadata.xml'. > Could not GET ‘https://dl.google.com/dl/android/maven2/com/tencent/mm/opensdk/wechat-sdk-android-without-mta/maven-metadata.xml'. > sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target > Failed to list versions for com.tencent.mm.opensdk:wechat-sdk-android-without-mta. > Unable to load Maven meta-data from https://maven.google.com/com/tencent/mm/opensdk/wechat-sdk-android-without-mta/maven-metadata.xml. > Could not get resource ‘https://maven.goo解决办法修改react-native-puti-pay下android目录的build.gradle文件,指明明确的版本号;https://bintray.com/wechat-sd…,可以查看到wechat-sdk的最新版本号,指定为最新版本号,就gradle就成功sync了//将 compile ‘com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+’//里面的加号修改成微信sdk的最新版本 compile ‘com.tencent.mm.opensdk:wechat-sdk-android-without-mta:5.3.1’//最新版本 ...

April 8, 2019 · 1 min · jiezi

React-native 集成react-native-yunpeng-alipay 爬坑

问题1:按照官网添加link后运行报错FAILURE: Build failed with an exception.* What went wrong:A problem occurred configuring project ‘:react-native-yunpeng-alipay’.> Could not resolve all artifacts for configuration ‘:react-native-yunpeng-alipay:classpath’. > Could not resolve com.android.tools.build:gradle:1.3.1. Required by: project :react-native-yunpeng-alipay > Could not resolve com.android.tools.build:gradle:1.3.1. > Could not get resource ‘https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/1.3.1/gradle-1.3.1.pom'. > Could not GET ‘https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/1.3.1/gradle-1.3.1.pom'. > sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target > Could not resolve com.android.tools.build:gradle:1.3.1. > Could not get resource ‘https://jcenter.bintray.com/com/android/tools/build/gradle/1.3.1/gradle-1.3.1.pom'. > Could not GET ‘https://jcenter.bintray.com/com/android/tools/build/gradle/1.3.1/gradle-1.3.1.pom'. > sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target* Try:Run with –stacktrace option to get the stack trace. Run with –info or –debug option to get more log output. Run with –scan to get full insights.解决办法修改react-native-yunpeng-alipay下的build.gradle文件buildscript { repositories { mavenLocal() google() jcenter() } dependencies { // classpath ‘com.android.tools.build:gradle:1.3.1’ classpath ‘com.android.tools.build:gradle:3.2.1’ }}修改com.android.tools.build:gradle:1.3.1和项目android的版本保持一致添加 mavenLocal() google()重新运行 ,问题解决 ...

April 6, 2019 · 1 min · jiezi

React Native开发工具:Expo,React Native CLI,CocoaPods

React Native开发工具:Expo,React Native CLI,CocoaPods来源 | 愿码(ChainDesk.CN)内容编辑愿码Slogan | 连接每个程序员的故事网站 | http://chaindesk.cn愿码愿景 | 打造全学科IT系统免费课程,助力小白用户、初级工程师0成本免费系统学习、低成本进阶,帮助BAT一线资深工程师成长并利用自身优势创造睡后收入。官方公众号 | 愿码 | 愿码服务号 | 区块链部落免费加入愿码全思维工程师社群 | 任一公众号回复“愿码”两个字获取入群二维码本文阅读时长:6min在本文中,你将了解各种React Native开发工具 - Expo,React Native CLI,CocoaPods。也将学习如何设置Expo和React Native CLI。在大量的React Native开发工具中Expo, React Native CLI,CocoaPods是比较受欢迎的。与任何开发工具一样,在灵活性和易用性之间需要进行权衡。建议首先使用Expo进行React Native开发工作流程,除非您确定需要访问本机代码。ExpoExpo是一个免费的开源工具链,围绕React Native构建,可帮助您使用JavaScript和React 构建原生iOS和Android项目 。Expo正在成为一个独立的生态系统,由五个相互关联的工具组成:Expo CLI:Expo的命令行界面。我们将使用Expo CLI来创建,构建和提供应用程序。Expo开发人员工具:这是一个基于浏览器的工具,只要通过expo start命令从终端启动Expo应用程序,它就会自动运行 。为开发中的应用程序提供活动日志,并快速访问本地运行应用程序并与其他开发人员共享应用程序。Expo Client:适用于Android和 iOS的应用程序 。此应用程序允许您在设备上的Expo应用程序中运行React Native项目,而无需安装它。这允许开发人员在真实设备上热重载,或与其他任何人共享开发代码,而无需安装它。Expo Snack:这个网络应用程序允许在浏览器中处理React Native应用程序,并预览您正在处理的代码。如果你曾经使用过CodePen或JSFiddle,那么Snack与应用于React Native应用程序的概念相同。Expo SDK:这是一个包含大量JavaScript API 的SDK, 它提供了基本React Native软件包中没有的Native功能,包括使用设备的加速计,摄像头,通知,地理位置等等。这个SDK随着使用Expo创建的每个新项目一起出现。这些工具共同构成了Expo工作流程。使用Expo CLI,您可以创建和构建具有Expo SDK支持的新应用程序 XDE / CLI 还提供了一种简单的方法来为您的开发中应用程序提供服务,方法是将代码自动推送到Amazon S3并为项目生成URL。从那里,CLI生成链接到托管代码的QR代码。在您的iPhone或Android设备上打开Expo Client应用程序,扫描QR码,然后BOOM那里有您的应用程序,配备实时/热重载!由于该应用程序托管在Amazon S3上,您甚至可以与其他开发人员实时共享开发中的应用程序。React Native CLI使用该命令创建新React Native应用程序的原始引导方法如下:react-native init这是由React Native CLI提供的。如果确定需要访问应用程序的本机层,可能只会使用这种引导新应用程序的方法。在React Native社区中,使用此方法创建的应用程序被称为纯React Native应用程序,因为所有开发和本机代码文件都向开发人员公开。虽然这提供了最大的自由度,但它也迫使开发人员维护本机代码。如果你是一名JavaScript开发人员,因为你打算仅使用JavaScript编写本机应用程序而跳到React Native上,那么必须在React Native项目中维护本机代码可能是此方法的最大缺点。另一方面,在处理使用以下命令引导的应用程序时,将可以访问第三方插件:react-native init直接访问代码库的本机部分。还可以回避当前Expo的一些限制,特别是无法使用背景音频或后台GPS服务。CocoaPods一旦开始使用具有使用本机代码的组件的应用程序,你将在开发中使用CocoaPods。CocoaPods是Swift和Objective-C Cocoa项目的依赖管理器 。它与npm几乎相同,但管理本机iOS代码而不是JavaScript代码的开源依赖项 。React Native使用CocoaPods进行一些 iOS集成,因此对管理器有一个基本的了解可能会有所帮助。同样,可以使用以下命令安装这些依赖项:pod installRuby是CocoaPods运行所必需的。在命令行运行该命令以验证Ruby已安装:ruby -v 如果没有,可以使用以下命令与Homebrew一起安装: brew install ruby安装Ruby后,可以通过以下命令安装CocoaPods:sudo gem install cocoapods规划应用并选择工作流程在尝试选择最适合应用需求的开发工作流程时,您需要考虑以下几点:我是否需要访问代码库的本机部分?我的应用程序中是否需要任何不受Expo支持的第三方软件包?我的应用程序不在前台时是否需要播放音频?我的应用程序不在前台时是否需要定位服务?需要推送通知支持吗?我在Xcode和Android Studio工作是否适应?根据我的经验,Expo通常是最好的起点。它为开发过程提供了很多好处,如果应用程序超出原始要求,则可以在弹出过程中提供一个逃生舱。如果确定应用程序需要某个Expo应用程序无法提供的内容,或者确定需要使用Native代码,我建议仅使用React Native CLI开始开发。React Native CLI设置将从应用程序的React Native CLI设置开始,创建一个新的纯React Native应用程序,从而可以访问所有Native代码,但也需要安装Xcode和Android Studio。首先,我们将安装使用纯React Native应用程序所需的所有依赖项,从用于macOS 的Homebrew包管理器开始。如项目主页上所述,可以通过以下命令从终端轻松安装Homebrew:/usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"一旦安装了Homebrew,它就可以用来安装React Native开发所需的依赖项: Node.js和nodemon。如果你是JavaScript开发人员,那么你可能已经安装了Node.js. 请注意,对于React Native开发,您将需要Node.js版本8或更高版本。如果尚未安装Node.js,可以使用Hombrew通过以下命令安装它:brew install node我们还需要nodemon React Native在后台使用的 软件包,以便在开发过程中实现重新加载等功能。nodemon使用Homebrew通过以下命令安装:brew install watchman我们当然还需要React Native CLI来运行引导 React Native应用程序的命令 。可以npm通过以下命令全局安装:npm install -g react-native-cli安装CLI后,创建新的纯React Native应用程序所需的全部内容如下:react-native init name-of-project这将在新name-of-project目录中创建一个新项目。该项目公开了所有Native代码,并且需要Xcode来运行iOS应用程序和Android Studio以运行Android应用程序。幸运的是,安装Xcode以支持iOS React Native开发是一个简单的过程。第一步是从App Store下载Xcode并进行安装。第二步是安装Xcode命令行工具。要执行此操作,请打开Xcode,从Xcode菜单中选择Preferences …,打开Locations面板,然后从Command Line Tools下拉列表中安装最新版本:不幸的是,设置Android Studio以支持Android React Native开发并不是那么简单,需要一些非常具体的安装步骤。既然已经安装了所有依赖项,我们就可以通过命令行运行纯React Native项目了。iOS应用程序可以通过以下方式执行:Andriod应用程序可以通过以下方式启动:react-native run-ios这些命令中的每一个都应该为正确的平台启动相关的模拟器,安装我们的新应用程序,并在模拟器中运行应用程序。react-native run-androidExpo CLI设置可以使用终端npm通过以下命令安装Expo CLI :npm install -g expoExpo CLI可用于完成Expo GUI客户端可以执行的所有重要操作。对于可以使用CLI运行的所有命令,关注公众号“愿码”,后台回复“Expo CLI”查看完整文档。 ...

April 4, 2019 · 1 min · jiezi

React Native 实现下拉自动切换分类

如图所示,整体为 FlatList,顶部分类栏吸顶,底部为 feed 流。要实现下拉商品列表到底后,继续下拉,自动切换到一下个分类的效果。代码层面,可以在 FlatList 的 onScrollEndDrag 中添加自动 Tab 切换函数,借助 FlatList 实例的内容区高度 contentLength,滑动偏移量 offset 和可视区高度 visibleLength 三者关系,实现下拉自动切换Tab功能。//页面布局相关 <FlatList data = {[{banner:[]},{tab:[]},{goodList:[]}]} renderItem={this.renderItem} stickyHeaderIndices={(Platform.OS !== ‘web’)?[1]:null} ListFooterComponent={this._renderFooter} onScroll={this._onScroll} //滑动监听 ref={this._setScrollRef} keyExtractor = {(item, index) => { hScrollView-${index} }} refreshing={this.state.isRefreshing} onRefresh={this._onRefresh.bind(this)} //下拉刷新 getItemLayout={(data, index) => ( {length: 305, offset: 305 * index, index} )} onScrollEndDrag = {()=>{ //滑动到底监听函数 if(Platform.OS != ‘web’){ this._onScrollEndDragFun(); } }}/>/** * 获取flatList 实例 * @param ref * @private /_setScrollRef = (ref) => { this._secondGoodFlatListRef = ref;};import isEmpty from “lodash/isEmpty”;import {Platform} from “react-native”;import {JDDevice} from “@jdreact/jdreact-core-lib”;/* * 底部列表页滑动事件 实现上拉切换品类功能 * @param e * @private */_onScrollEndDragFun = (e) => { let scrollMetrics = (this._secondGoodFlatListRef && this._secondGoodFlatListRef._listRef && this._secondGoodFlatListRef._listRef._scrollMetrics) || null; let {contentLength = 0, offset = 0, visibleLength = 0} = scrollMetrics; // console.log(’===scrollMetrics’,scrollMetrics); // //判断是否最后一Tab 如果是就不却换下个目录 // console.log(’===this.props’,this.props); // console.log(’===this.state’,this.state); if (contentLength && offset && visibleLength) { let {selectedIndex = 0} = this.state; //当前选中的三级分类index let {tabListData = []} = this.props; if (!isEmpty(tabListData) && (selectedIndex + 1) < tabListData.length) { //排除最后一个分类 let item = tabListData[selectedIndex + 1]; if (Platform.OS === ‘ios’) { //IOS 系统存在弹性上拉 if (offset + visibleLength > contentLength + JDDevice.getRpx(100)) { this._secondGoodFlatListRef.scrollToIndex({ animated: false, index: 0, viewOffset: 1, viewPosition: 0 }); this.ItemCategory._clickCategoryTab2 && this.ItemCategory._clickCategoryTab2(item, selectedIndex + 1); } } else { //android 无弹性滑动,滑动到底时,offset + visibleLength = contentLength。IOS有弹性滑动,需要改变判断条件 lbs 2019-03-10 if (offset + visibleLength > contentLength - JDDevice.getRpx(10)) { this._secondGoodFlatListRef.scrollToIndex({ animated: false, index: 0, viewOffset: 1, viewPosition: 0 }); this.ItemCategory._clickCategoryTab2 && this.ItemCategory._clickCategoryTab2(item, selectedIndex + 1); } } } }};其中,contentLength 为内容区高度,offset 为滑动偏移量,visibleLength 为可视区高度。关于三种高度定义,可参考 React Native中ListView和ScrollView实现上拉加载let scrollMetrics = (this._secondGoodFlatListRef && this._secondGoodFlatListRef._listRef && this._secondGoodFlatListRef._listRef._scrollMetrics) || null;let { contentLength = 0, // 内容区高度 offset = 0, // 滑动偏移量 visibleLength = 0 // 可视区高度} = scrollMetrics;对于 Android 平台,当 offset + visibleLength = contentLength 时,表示滑动到底部。为了以前进行切换,对条件进行修正,当滑动到距离底部 10px 时,触发切换 Tab 函数,如下代码所示//android 无弹性滑动,滑动到底时,offset + visibleLength = contentLength。IOS有弹性滑动,需要改变判断条件 lbs 2019-03-10if (offset + visibleLength > contentLength - JDDevice.getRpx(10)) { this._secondGoodFlatListRef.scrollToIndex({ animated: false, index: 0, viewOffset: 1, viewPosition: 0 }); //切换Tab this.ItemCategory._clickCategoryTab2 && this.ItemCategory._clickCategoryTab2(item, selectedIndex + 1);}对于 IOS 平台,因为 IOS 系统存在弹性上拉,如下图所示。因此对滑动到底条件修正为 offset + visibleLength > contentLength + JDDevice.getRpx(100)。其中,JDDevice.getRpx(100) 表示弹性上拉的高度,即下图中红色框的高度。if (Platform.OS === ‘ios’) { if (offset + visibleLength > contentLength + JDDevice.getRpx(100)) { this._secondGoodFlatListRef.scrollToIndex({ animated: false, index: 0, viewOffset: 1, viewPosition: 0 }); this.ItemCategory._clickCategoryTab2 && this.ItemCategory._clickCategoryTab2(item, selectedIndex + 1); } } ...

April 3, 2019 · 2 min · jiezi

react native拖动上方显示值,改变背景颜色的slider

效果如图:使用的是react-native-slider插件1、安装npm i –save react-native-slider2、具体参数查阅git文档https://github.com/jeanregisser/react-native-slider3、我们主要讲怎么实现背景图片功能和拖动显示具体值minimumTrackTintColor={“transparent”}maximumTrackTintColor={“transparent”}这两个参数可以使滑块的轨道透明,那么我们只需要给滑块设置一个背景图片就可以了<ImageBackground resizeMethod={‘scale’} style={[styles.imageStyle, {width: this.state.width}]} source={this.state.sliderBgImg} imageStyle={{borderRadius: 2}}>使用ImageBackground标签包裹 Slider标签添加背景图片最后修改一些css就可以实现了具体代码封装组件 slider-widget.js/** * 用法 * 1. 默认用法,不传递任何参数,就是默认主题颜色 * 2. 如果想要使用背景图片,必须设置三个参数 * bgImage={require("./p.jpeg")} 背景图片 * minColor={“transparent”} 左边轨道颜色透明 * maxColor={“transparent”} 右边轨道颜色颜色透明 */import React, {Component} from “react”;import {View, Text, Dimensions, StyleSheet, ImageBackground} from “react-native”;import Slider from “react-native-slider”;export default class SliderWidget extends Component { constructor(props) { super(props); this.state = { initSliderValue: this.props.value ? this.props.value : 0, // 初始化值 maxValue: this.props.maxValue ? this.props.maxValue : 100, // 滑块最大值 minValue: this.props.minValue ? this.props.minValue : 0, // 滑块最小值 step: this.props.step ? this.props.step : 0, // 步调 width: this.props.width ? this.props.width : Dimensions.get(“window”).width - 20, // 设备宽度 showFloat: false, // 拖动的时候上面显示值 minColor: this.props.minColor ? this.props.minColor : “#c53c2c”, // 左边的轨迹颜色 maxColor: this.props.maxColor ? this.props.maxColor : “#dddddd”, // 右边的轨迹颜色 sliderBgImg: this.props.bgImage // 轨迹北京图片 } } componentWillMount() { } render() { return( <View> <ImageBackground resizeMethod={‘scale’} style={[styles.imageStyle, {width: this.state.width}]} source={this.state.sliderBgImg} imageStyle={{borderRadius: 2}} > {this.state.showFloat ? <View style={[styles.floatValue, {left: this.state.initSliderValue / this.state.maxValue * this.state.width - (this.state.initSliderValue * this.state.maxValue / this.state.width)}]}><Text>{this.state.initSliderValue}</Text></View> : <View></View> } <Slider style={[styles.silder, {width: this.state.width}]} maximumValue={100} minimumValue={0} step={1} value={this.state.initSliderValue} onValueChange={this.onValueChangeFun} onSlidingComplete={this.onSlidingCompleteFun} minimumTrackTintColor={this.state.minColor} maximumTrackTintColor={this.state.maxColor} // thumbImage={this.state.sliderBgImg} thumbTintColor={"#ffffff"} thumbStyle={{width: 30, height: 30, overflow: “hidden”, borderRadius: 30, shadowColor: “#aaaaaa”, shadowOffset: {width: 4, height: 4}, shadowOpacity: 0.8, shadowRadius: 4, elevation: 4, opacity: 1}} > </Slider> </ImageBackground> </View> ); } componentDidMount() { } componentWillUnmount() { } // 拖动事件 onValueChangeFun = (event) => { console.log(event, “改变事件”); this.setState({ initSliderValue: event, showFloat: true }); } // 拖动完成事件 onSlidingCompleteFun = (event) => { this.setState({ showFloat: false }); this.props.getSliderValue(event); }}const styles = StyleSheet.create({ silder: { position: “absolute”, top: -18, left: 0 }, imageStyle: { height: 4, marginTop: 35, marginBottom: 15, borderRadius: 2, position: “relative” }, floatValue: { width: 30, height: 20, borderRadius: 5, backgroundColor: “#fff”, borderWidth: 1, borderColor: “#dddddd”, position: “absolute”, top: -35, left: 0, alignItems: “center”, justifyContent: “center” }})使用组件 app.jsimport React, {Component} from ‘react’;import {StyleSheet, Text, View} from ‘react-native’;import SliderWidget from ‘./slider-widget’;export default class App extends Component { constructor(props) { super(props); this.state = { sliderValue: 50, sliderBgValue: 30 } } render() { return ( <View style={styles.container}> <Text>亮度: {this.state.sliderValue}%</Text> <SliderWidget value={this.state.sliderValue} getSliderValue={this.showSliderValueFun}></SliderWidget> <Text>色温: {this.state.sliderBgValue}%</Text> <SliderWidget value={this.state.sliderValue} getSliderValue={this.showSliderBgValueFun} bgImage={require("./p.jpeg")} minColor={“transparent”} maxColor={“transparent”}></SliderWidget> </View> ); } // 获取颜色值 showSelectColorRgbFun = (color) => { console.log(color); } // 获取slider值 showSliderValueFun = (value) => { console.log(value); this.setState({ sliderValue: value }) } showSliderBgValueFun = (value) => { console.log(value); this.setState({ sliderBgValue: value }) }}const styles = StyleSheet.create({ container: { flex: 1, justifyContent: ‘center’, alignItems: ‘center’, backgroundColor: ‘#F5FCFF’, }, welcome: { fontSize: 20, textAlign: ‘center’, margin: 10, }, instructions: { textAlign: ‘center’, color: ‘#333333’, marginBottom: 5, },}); ...

April 1, 2019 · 2 min · jiezi

React Native 升级指南 从0.55升级到0.59

前言本月早些时候facebook发布了React Native 0.59,个人感觉算是RN的里程碑,主要有:增加了对hooks的支持更新了Android端JSC(JavaScript Core)inline requires精简代码:一些组件交给社区来维护CLI的提升,同时也从rn库中独立了出来详细内容升级到0.59核心就是参考RN diff PURGE来手动升级,这也是官方推荐的方式。此外,rn0.59中移除了react-native-git-upgrade,取而代之的是react-native upgrade,而react-native upgrade就是基于RN diff PURGE。如何查看指定版本之间的diff?手动编辑URL:https://github.com/react-nati…[当前版本号]…version/[目标版本号]。比如我是从0.55.4到0.59.2 那么就是https://github.com/react-nati…然后查看变更(点击Files changed)来手动更改。如果你习惯在本地查看变更,比如在vs code中,那么可以在releases中下载对应的版本,在本地创建一个git仓库,再用目标版本的文件替换之。后来发现了一个网站。。(捂脸 和方法1其实是一样的遇到的一些坑iOS的project.pbxproj变更非常多(0.55.4 -> 0.59.2),其实根本不用管他。(有待验证,我改吐了,后来不管他项目也跑起来了)cocoaPods: pod install时报错could not find compatible versions for pod “Folly”,解决方法:Podfile中添加 # Third party deps podspec link pod ‘DoubleConversion’, :podspec => ‘../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec’ pod ‘glog’, :podspec => ‘../node_modules/react-native/third-party-podspecs/glog.podspec’ pod ‘Folly’, :podspec => ‘../node_modules/react-native/third-party-podspecs/Folly.podspec’官方说明iOS打包完毕后启动app,报错:can’t find variable requireNativeComponent。 Google无果。一番排查后发现是rn源码(Modal.js)“出错了”,本以为可以pr走一波,后来才发现GitHub上的源码没问题。OK,npm接锅。删除node_modules下的rn文件夹,npm i react-native, 解决。报错2: decorator相关。因为用了babel7,transform-decorators-legacy并不适用,使用@babel/plugin-proposal-decorators,同时带上@babel/plugin-proposal-class-properties。继续报错: TypeError: undefined is not an object (evaluating ‘props.getItem’): 这个是@babel/plugin-proposal-class-properties的锅相关issue。如果你不需要这个,直接移除即可。又或者,添加新的babel plugin@babel/plugin-transform-flow-strip-types并且保证顺序在@babel/plugin-proposal-class-properties之上。附:本人项目中.babelrcplugins部分"plugins": [ ["@babel/plugin-transform-flow-strip-types"], ["@babel/plugin-proposal-decorators", { “legacy”: true }], ["@babel/plugin-proposal-class-properties", { “loose”: true }]]Enjoy new version React Native至此,你的rn项目应该可以跑起来了。一进去你(可能)会发现非常多的Yellow Box Warning按照提示慢慢去改进吧~也希望这篇文章对那些和我一样没有原生经验的小伙伴有所帮助。Thanks for reading ...

March 30, 2019 · 1 min · jiezi

一步步实现一个自适应的react-native拖拽排序

2019.2: 优化拖拽不移动时自动恢复,现在这个插件应该没有任何问题。新加一个实战演示例子,后面有时间会对这个例子进行加动画,删除时item向下到待选的item动画,和待选到item。还有滑动时自动向下滑动动画。最近由于业务需求需要实现一个功能需要实现图片的上传和排序和删除,在网上搜索了几款发现都需要固定列数,感觉不太友好,所以自己实现了一个可以不需要设定列数的排序,而且布局高度实现自适应。源码链接效果图对比(固定列数和自适应流布局)![[图片上传中…(iphone.jpg-9f7224-1533711885416-0)]](https://user-gold-cdn.xitu.io…动态图实现其实拖拽排序在大多数编程语言里已经有很多中三方插件可以使用,实现方法都差不多,而且例如Android和iOS或者现在的React-Native他们逻辑几乎是可以共用,你会写一个语言的拖拽排序,其他的都差不多。梳理一下步骤开始触发: 长按或触摸到达一定时间时触发开始排序,这时可以进行把被单机的item放大、透明、抖动动画。开始滑动:(1) 被拖拽的item随着手指的滑动而滑动(2) 被拖动的item滑动到第x个时,item到x之间的item进行左滑右滑一个位置的动画。松开手指:(1) 被拖拽的这个item通过四舍五入进入相应的位置。(2) 数据进行替换并重绘加布局矫正。tip: 滑动逻辑,例如当你把index=1拖到index=3,不是将1和3替换(0,3,2,1,4),而是(0,3,1,2,4)这才是拖拽后结果,只将被拖拽的一个替换到要去的位置,其他的向前和向后移动主要代码// 触摸事件的监听this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: (evt, gestureState) => this.props.sortable, onStartShouldSetPanResponderCapture: (evt, gestureState) => { this.isMovePanResponder = false return false }, // 接管触摸加滑动事件 onMoveShouldSetPanResponder: (evt, gestureState) => this.isMovePanResponder, onMoveShouldSetPanResponderCapture: (evt, gestureState) => this.isMovePanResponder, onPanResponderGrant: (evt, gestureState) => {}, onPanResponderMove: (evt, gestureState) => this.moveTouch(evt,gestureState), onPanResponderRelease: (evt, gestureState) => this.endTouch(evt), onPanResponderTerminationRequest: (evt, gestureState) => false, onShouldBlockNativeResponder: (evt, gestureState) => false, })//这里使用长按触发开发拖拽事件,其实开始是使用触摸一定时间后触发事件的,但和View的单机事件有冲突不好解决,所以选择了长按触发事件startTouch(touchIndex) { // 接管滑动 this.isMovePanResponder = true //if (this.measureTimeOut) clearTimeout(this.measureTimeOut) if (sortRefs.has(touchIndex)) { if (this.props.onDragStart) { this.props.onDragStart(touchIndex) } //变大和加透明 Animated.timing( this.state.dataSource[touchIndex].scaleValue, { toValue: maxScale, duration: scaleDuration, } ).start(()=>{ // 备份被触摸的事件 this.touchCurItem = { ref: sortRefs.get(touchIndex), index: touchIndex, // 记录之前的位置 originLeft: this.state.dataSource[touchIndex].originLeft, originTop: this.state.dataSource[touchIndex].originTop, moveToIndex: touchIndex, } }) } }//滑动moveTouch (nativeEvent,gestureState) { if (this.touchCurItem) { let dx = gestureState.dx let dy = gestureState.dy const rowNum = parseInt(this.props.parentWidth/this.itemWidth); const maxWidth = this.props.parentWidth-this.itemWidth const maxHeight = this.itemHeightMath.ceil(this.state.dataSource.length/rowNum) - this.itemHeight //出界后取最大或最小值防止出界 if (this.touchCurItem.originLeft + dx < 0) { dx = -this.touchCurItem.originLeft } else if (this.touchCurItem.originLeft + dx > maxWidth) { dx = maxWidth - this.touchCurItem.originLeft } if (this.touchCurItem.originTop + dy < 0) { dy = -this.touchCurItem.originTop } else if (this.touchCurItem.originTop + dy > maxHeight) { dy = maxHeight - this.touchCurItem.originTop } let left = this.touchCurItem.originLeft + dx let top = this.touchCurItem.originTop + dy //置于最上层 this.touchCurItem.ref.setNativeProps({ style: { zIndex: touchZIndex, } }) //滑动时刷新布局,这里直接刷新Animated的数字就可以进行局部刷新了this.state.dataSource[this.touchCurItem.index].position.setValue({ x: left, y: top, }) let moveToIndex = 0 let moveXNum = dx/this.itemWidth let moveYNum = dy/this.itemHeight if (moveXNum > 0) { moveXNum = parseInt(moveXNum+0.5) } else if (moveXNum < 0) { moveXNum = parseInt(moveXNum-0.5) } if (moveYNum > 0) { moveYNum = parseInt(moveYNum+0.5) } else if (moveYNum < 0) { moveYNum = parseInt(moveYNum-0.5) } moveToIndex = this.touchCurItem.index+moveXNum+moveYNumrowNum if (moveToIndex > this.state.dataSource.length-1) moveToIndex = this.state.dataSource.length-1 // 其他item向左和向右滑动 if (this.touchCurItem.moveToIndex != moveToIndex ) { this.touchCurItem.moveToIndex = moveToIndex this.state.dataSource.forEach((item,index)=>{ let nextItem = null if (index > this.touchCurItem.index && index <= moveToIndex) { nextItem = this.state.dataSource[index-1] } else if (index >= moveToIndex && index < this.touchCurItem.index) { nextItem = this.state.dataSource[index+1] } else if (index != this.touchCurItem.index && (item.position.x._value != item.originLeft || item.position.y._value != item.originTop)) { nextItem = this.state.dataSource[index] //有时前一个或者后一个数据有个动画差的原因无法回到正确位置,这里进行矫正 } else if ((this.touchCurItem.index-moveToIndex > 0 && moveToIndex == index+1) || (this.touchCurItem.index-moveToIndex < 0 && moveToIndex == index-1)) { nextItem = this.state.dataSource[index] } //需要滑动的就进行滑动动画 if (nextItem != null) { Animated.timing( item.position, { toValue: {x: parseInt(nextItem.originLeft+0.5),y: parseInt(nextItem.originTop+0.5)}, duration: slideDuration, easing: Easing.out(Easing.quad), } ).start() } }) } } }//触摸事件 endTouch (nativeEvent) { //clear if (this.measureTimeOut) clearTimeout(this.measureTimeOut) if (this.touchCurItem) { if (this.props.onDragEnd) { this.props.onDragEnd(this.touchCurItem.index,this.touchCurItem.moveToIndex) } //this.state.dataSource[this.touchCurItem.index].scaleValue.setValue(1) Animated.timing( this.state.dataSource[this.touchCurItem.index].scaleValue, { toValue: 1, duration: scaleDuration, } ).start() this.touchCurItem.ref.setNativeProps({ style: { zIndex: defaultZIndex, } }) this.changePosition(this.touchCurItem.index,this.touchCurItem.moveToIndex) this.touchCurItem = null } }//刷新数据 changePosition(startIndex,endIndex) { if (startIndex == endIndex) { const curItem = this.state.dataSource[startIndex] this.state.dataSource[startIndex].position.setValue({ x: parseInt(curItem.originLeft+0.5), y: parseInt(curItem.originTop+0.5), }) return; } let isCommon = true if (startIndex > endIndex) { isCommon = false let tempIndex = startIndex startIndex = endIndex endIndex = tempIndex } const newDataSource = […this.state.dataSource].map((item,index)=>{ let newIndex = null if (isCommon) { if (endIndex > index && index >= startIndex) { newIndex = index+1 } else if (endIndex == index) { newIndex = startIndex } } else { if (endIndex >= index && index > startIndex) { newIndex = index-1 } else if (startIndex == index) { newIndex = endIndex } } if (newIndex != null) { const newItem = {…this.state.dataSource[newIndex]} newItem.originLeft = item.originLeft newItem.originTop = item.originTop newItem.position = new Animated.ValueXY({ x: parseInt(item.originLeft+0.5), y: parseInt(item.originTop+0.5), }) item = newItem } return item }) this.setState({ dataSource: newDataSource },()=>{ if (this.props.onDataChange) { this.props.onDataChange(this.getOriginalData()) } //防止RN不绘制开头和结尾 const startItem = this.state.dataSource[startIndex] this.state.dataSource[startIndex].position.setValue({ x: parseInt(startItem.originLeft+0.5), y: parseInt(startItem.originTop+0.5), }) const endItem = this.state.dataSource[endIndex] this.state.dataSource[endIndex].position.setValue({ x: parseInt(endItem.originLeft+0.5), y: parseInt(endItem.originTop+0.5), }) }) }后续会加上添加和删除Item渐变动画源码链接 ...

March 18, 2019 · 3 min · jiezi

更全、更好的 React-Native Toast组件

在React-Native如果用到提示(网络请求失败等短暂的显示提示用户)的话大多数在项目可能用的react-native-root-toast、react-native-easy-toast,如果用到加载,可能就得自己写Modal加载,在开发了几个React-Native项目时,发现一些提示缺陷,所以就写了一个组件react-native-smart-tip。react-native-smart-tip现有提示框的一些问题Toast提示重叠之前在项目使用发现,单击登录提示登录中,登录成功后提示登录成功。由于速度过快,出现重影问题,两个提示Toast都集中在中心位置,让人看着很不舒服。没有动画现在的一些版本安卓手机自带的进场是有一个从下到上的动画的,但React-Native现有的Toast组件暂时都没有,所以当时开发项目时对着react-native-root-toast改了一下,当时也就没再看了。有Modal提示框时Toast被遮盖在显示了Modal框时Toast将会被遮盖,这个是由于视图层优先级的关系,之前的解决办法是在Modal上加一些提示。组件优点解决了Toast提示重复问题。提示新增动画。提示新增文字模式和文字加图片模式。新增安卓SnackBar提示功能。新增Modal提示,可以实现加载提示和Modal状态下提示。静态效果演示GIF演示组件地址

March 18, 2019 · 1 min · jiezi

react-native 开荒记(一) 开发环境的搭建

环境搭建安装依赖所用平台: macOS 故文章内容基于macOS平台进行,官方最新react-native 0.58版本目标平台所需安装的依赖不同1.当目标平台为ios时 必须安装的依赖有:Node、Watchman 和 React Native 命令行工具以及 Xcode(苹果公司目前只允许在Mac电脑上开发iOS应用。如果你没有Mac,那么只能考虑使用沙盒环境,或者去开发Android应用)。2.当目标平台为Android时 必须安装的依赖有:Node、Watchman 和 React Native 命令行工具以及 JDK 和 Android Studio(虽然你可以使用任何编辑器来开发应用(编写 js 代码),但你仍然必须安装 Android Studio 来获得编译 Android 应用所需的工具和环境)。以下为官方教程安装brew install nodebrew install watchmannpm config set registry https://registry.npm.taobao.org –globalnpm config set disturl https://npm.taobao.org/dist –globalnpm install -g yarn react-native-cliyarn config set registry https://registry.npm.taobao.org –globalyarn config set disturl https://npm.taobao.org/dist –global注:官方推荐使用brew和yarniosXcodeReact Native 目前需要Xcode 9.4 或更高版本。你可以通过 App Store 或是到Apple 开发者官网上下载。这一步骤会同时安装 Xcode IDE、Xcode 的命令行工具和 iOS 模拟器。Xcode 的命令行工具启动 Xcode,并在Xcode | Preferences | Locations菜单中检查一下是否装有某个版本的Command Line Tools。Xcode 的命令行工具中包含一些必须的工具,比如git等。Android1.安装 Java Development Kit和Android Studio 首先下载和安装 Java Development Kit和Android Studio,这个就自行百度吧(这版React Native 需要 Java Development Kit [JDK] 1.8)。2.安装 Android SDK Android Studio 默认会安装最新版本的 Android SDK。目前编译 React Native 应用需要的是Android 9 (Pie)版本的 SDK(注意 SDK 版本不等于终端系统版本,RN 目前支持 android4.1 以上设备)。你可以在 Android Studio 的 SDK Manager 中选择安装各版本的 SDK。 SDK Manager 还可以在 Android Studio 的"Preferences"菜单中找到。具体路径是Appearance & Behavior → System Settings → Android SDK。 在 SDK Manager 中选择"SDK Platforms"选项卡,然后在右下角勾选"Show Package Details"。展开Android 9 (Pie)选项,选中。然后点击"SDK Tools"选项卡,同样勾中右下角的"Show Package Details"。展开"Android SDK Build-Tools"选项,确保选中了 React Native 所必须的28.0.3版本。(你可以同时安装多个其他版本)。最后点击"Apply"来下载和安装这些组件。3.配置 ANDROID_HOME 环境变量React Native 需要通过环境变量来了解你的 Android SDK 装在什么路径,从而正常进行编译。具体的做法是把下面的命令加入到~/.bash_profile文件中:注:~表示用户目录,即/Users/你的用户名/,而小数点开头的文件在 Finder 中是隐藏的,并且这个文件有可能并不存在。可在终端下使用vi ~/.bash_profile命令创建或编辑。(vim基本命令 输入i 退出输入esc 命令模式:wq 保存退出)。如果你不是通过Android Studio安装的sdk,则其路径可能不同,请自行确定清楚。export ANDROID_HOME=$HOME/Library/Android/sdkexport PATH=$PATH:$ANDROID_HOME/toolsexport PATH=$PATH:$ANDROID_HOME/tools/binexport PATH=$PATH:$ANDROID_HOME/platform-toolsexport PATH=$PATH:$ANDROID_HOME/emulator`如果你的命令行不是 bash,而是例如 zsh 等其他,请使用对应的配置文件。使用source $HOME/.bash_profile命令来使环境变量设置立即生效(否则重启后才生效)。可以使用echo $ANDROID_HOME检查此变量是否已正确设置。请确保你正常指定了 Android SDK 路径。你可以在 Android Studio 的"Preferences"菜单中查看 SDK 的真实路径,具体是Appearance & Behavior → System Settings → Android SDK。创建新项目react-native init TestProject可以使用–version 创建指定版本的项目。例如react-native init TestProject –version 0.57.3。注意版本号必须精确到两个小数点运行你刚创建的项目 以Android为例,用Android Studio打开项目下的android文件,点击虚拟机图标运行虚拟机,当然也可以使用真机或者其他,在这里省事用了Android Studio自带的虚拟机,实际中我推荐使用其他。之后执行cd TestProjectreact-native run-android当看到恭喜你已经运行了第一个 React Native 应用。笔者环境版本 react-native@0.58 node@8.11.3 watchman@4.9.0 react-native-cli@2.0.1 Xcode@10.1 javac@1.8.0_201 yarn@1.13.0 ...

March 18, 2019 · 1 min · jiezi

react-navigation 监听顶部导航栏点击/滑动状态

问题描述:使用createMaterialTopTabNavigator创建顶部导航栏,希望实现切换到指定的Tab再获取数据,查看官方文档只能找到tabBarOnPress 方法监听点击回调,但是无法监听滑动切换import React from ‘react’;import {DeviceEventEmitter,View} from ‘react-native’;import { createMaterialTopTabNavigator } from ‘react-navigation’;export default class TestPage extends React.Component{ constructor(props) { super(props); this.topTabList = [‘All’,‘Java’, ‘Javascript’, ‘PHP’]; } getTopBar() { let topBars = {} this.topTabList.forEach((item, index) => { topBars[‘TopBar’ + item] = { screen: (props)=><ChildComponent {…props} tabName={item} InitTabName={this.topTabList[0]}/>, navigationOptions: { title: item, } } }) return topBars } getTopTabList(){ if(!this.createTopNavigator){ this.createTopNavigator = createMaterialTopTabNavigator( this.getTopBar(), { //code… } ); } return this.createTopNavigator; } render(){ const TopTabList = this.getTopTabList(); // 在导航栏组件render位置使用onNavigationStateChange方法监听顶部导航切换-可监听滑动+点击 return <View> <TopTabList onNavigationStateChange={(prevState, currentState)=>{ let {index} = currentState; let TabName = currentState.routes[index].routeName; TabName = TabName.split(’’)[1]; //触发事件监听器,更新列表数据 //tip:如果希望切换已经加载过一次之后不重新获取数据,可以通过setParams设一个flag,判断flag是否需要触发监听器 DeviceEventEmitter.emit(‘TabChange’,TabName); }} /> </View> }}class ChildComponent extends React.Component{ constructor(props){ super(props); this.tabName= this.props.tabName; //当前tabName this.InitTabName = this.props.InitTabName; //初始化列表 } componentWillMount(){ // 加载初始化Tab列表 if(this.storeName===this.InitTabName){ this.updateList(); } // 监听Tab切换 this.TopBarChangeListener = DeviceEventEmitter.addListener(‘TabChange’,tabName=>{ if(this.tabName===tabName){ //更新列表 this.updateList(); } }) } // 更新列表 updateList(){ let {navigation} = this.props; navigation.setParams({hasList:true}); this.loadData(); } loadData(){ //Send Request } componentWillUnmount(){ //移除事件监听器 if(this.TopBarChangeListener){ this.TopBarChangeListener.remove(); } } } render(){ return <View> {/* code… */} </View> }} ...

March 13, 2019 · 1 min · jiezi

Mac上可测试开发的安卓模拟器

android 开发mac 上用什么模拟器调试比较好?这个问题用个一个晚上的时间解决了。百度了很多也尝试了很多,终于找到用个好用、不大、稳定性好的软件了。尝试找了,也试了好几款,其他的都不理想,但大多官网已经没有mac版了,如:蓝叠、夜游、腾讯小助手……唯一官网上有mac版本,并有测试的“摇一摇”功能的就只有——网易“mumu”。官网地址:http://mumu.163.com使用方法1. 打开软件可以看出来模拟器的UI还是很不错的。2. 下载app如果你是自己测试开发软件,可打开MMAP,使用浏览器,访问ip地址下载本地app如图:找到apk,点击下载安装即可完成后,效果如图:3. 配置端口这一步很关键。双击打开刚刚下载的app,会报错。报错如图:解决方法:首先:点击上方模拟器-摇一摇然后:选择Dev Setting继续:点击倒数第三个选项,填写ip地址和端口号即可。(在VSCode记得启动app的服务)4. 完成

March 11, 2019 · 1 min · jiezi

How to upgrade to the latest RN

How to upgrade to the latest RNI think the best way to do this is to create a new RN project that has the same name as your existing project.Then there are two parts you will have to deal with.package.json file related.non-package.json file related.Package.json thingsCopy newest dependencies and replace your existing ones and keep others.Basically these stuff. “dependencies”: { “react”: “16.6.3”, “react-native”: “0.58.3” }, “devDependencies”: { “babel-core”: “^7.0.0-bridge.0”, “babel-jest”: “24.0.0”, “jest”: “24.0.0”, “metro-react-native-babel-preset”: “0.51.1”, “react-test-renderer”: “16.6.3” }, “jest”: { “preset”: “react-native” }Non-package.json thingsIf your package is really old that you still have index.ios.js / index.android.js. You will have to do something to your codes to morden styles, like there might have App.js and app.json and index.js. Generally files like this.Then copy files like:.buckconfig.flowconfig.gitattributes.gitignore, DO REMEMBER to merge this file, you have to keep you own modification..watchmanconfigUpgrade Babel ConfigMaybe it’s OK to keep your babel config file name as .babelrc, but the current one is named babel.config.js. So let’s name it as the new one.Change the content of the config file to:module.exports = { presets: [“module:metro-react-native-babel-preset”]}Upgrade iOSYou might going to upgrade swift version. Follow the swift way. I met some problems in swift 4.2. The class method can not be called in OC. As there’s a modification in Swift 4.2.Upgrade AndroidJust open your Android Studio and let the IDE do the upgrade job.If you used NDK, well downlad a new version which is specified in RN doc. ...

March 10, 2019 · 2 min · jiezi

Web 性能优化:缓存 React 事件来提高性能

这是 Web 性能优化的第三篇,上一篇在下面看点击查看:Web 性能优化: 使用 Webpack 分离数据的正确方法Web 性能优化: 图片优化让网站大小减少 62%JavaScript中一个不被重视的概念是对象和函数是如何引用的,并且直接影响 React性能。 如果创建两个完全相同的函数,它们仍然不相等,试试下面的例子:const functionOne = function() { alert(‘Hello world!’); };const functionTwo = function() { alert(‘Hello world!’); };functionOne === functionTwo; // false但是,如果将变量指向一个已存在的函数,看看它们的差异:const functionThree = function() { alert(‘Hello world!’); };const functionFour = functionThree;functionThree === functionFour; // true对象的工作方式也是一样的。const object1 = {};const object2 = {};const object3 = object1;object1 === object2; // falseobject1 === object3; // true如果人有其他语言的经验,你可能熟悉指针。每次创建一个对象,计算机会为这个对象分配了一些内存。当声明 object1 ={} 时,已经在用户电脑中的 RAM(随机存取存储器) 中创建了一个专门用于object1 的字节块。可以将 object1 想象成一个地址,其中包含其键-值对在 RAM 中的位置。当声明 object2 ={} 时,在用户的电脑中的 RAM 中创建了一个专门用于 object2 的不同字节块。object1 的地址与 object2 的地址是不一样的。这就是为什么这两个变量的等式检查没有通过的原因。它们的键值对可能完全相同,但是内存中的地址不同,这才是会被比较的地方。当我赋值 object3 = object1 时,我将 object3 的值赋值为 object1 的地址,它不是一个新对象。它们在内存中的位置是相同的,可以这样验证:const object1 = { x: true };const object3 = object1;object3.x = false;object1.x; // false在本例中,我在内存中创建了一个对象并取名为 object1。然后将 object3 指向 object1 这时它们的内存的地址中是相同的。通过修改 object3,可以改变对应内存中的值,这也意味着所有指向该内存的变量都会被修改。obect1 的值也被改变了。对于初级开发人员来说,这是一个非常常见的错误,可能需要一个更别深入的教程,但是本广是关于React 性能的,只是本文是讨论 React 性能的,甚至是对变量引用有较深资历的开发者也可能需要学习。这与 React 有什么关系? React 有一种节省处理时间以提高性能的智能方法:如果组件的 props 和 state 没有改变,那么render 的输出也一定没有改变。 显然,如果所有的都一样,那就意味着没有变化,如果没有任何改变,render 必须返回相同的输出,因此我们不必执行它。 这就是 React 快速的原因,它只在需要时渲染。React 采用和 JavaScript 一样的方式,通过简单的 == 操作符来判断 props 和 state 是否有变化。 React不会深入比较对象以确定它们是否相等。浅比较用于比较对象的每个键值对,而不是比较内存地址。深比较更进一步,如果键-值对中的任何值也是对象,那么也对这些键-值对进行比较。React 都不是:它只是检查引用是否相同。如果要将组件的 prop 从 {x:1} 更改为另一个对象 {x:1},则 React 将重新渲染,因为这两个对象不会引用内存中的相同位置。 如果要将组件的 prop 从 object1(上面的例子)更改为 o bject3,则 React 不会重新呈现,因为这两个对象具有相同的引用。在 JavaScript 中,函数的处理方式是相同的。如果 React 接收到具有不同内存地址的相同函数,它将重新呈现。如果 React 接收到相同的函数引用,则不会。不幸的是,这是我在代码评审过程中遇到的常见场景:class SomeComponent extends React.PureComponent { get instructions () { if (this.props.do) { return ‘click the button: ’ } return ‘Do NOT click the button: ’ } render() { return ( <div> {this.instructions} <Button onClick={() => alert(’!’)} /> </div> ) }}这是一个非常简单的组件。 有一个按钮,当它被点击时,就 alert。 instructions 用来表示是否点击了按钮,这是通过 SomeComponent 的 prop 的 do={true} 或 do={false} 来控制。这里所发生的是,每当重新渲染 SomeComponent 组件(例如 do 从 true 切换到 false)时,按钮也会重新渲染,尽管每次 onClick 方法都是相同的,但是每次渲染都会被重新创建。每次渲染时,都会在内存中创建一个新函数(因为它是在 render 函数中创建的),并将对内存中新地址的新引用传递给 <Button />,虽然输入完全没有变化,该 Button 组件还是会重新渲染。修复如果函数不依赖于的组件(没有 this 上下文),则可以在组件外部定义它。 组件的所有实例都将使用相同的函数引用,因为该函数在所有情况下都是相同的。const createAlertBox = () => alert(’!’);class SomeComponent extends React.PureComponent { get instructions() { if (this.props.do) { return ‘Click the button: ‘; } return ‘Do NOT click the button: ‘; } render() { return ( <div> {this.instructions} <Button onClick={createAlertBox} /> </div> ); }}和前面的例子相反,createAlertBox 在每次渲染中仍然有着有相同的引用,因此按钮就不会重新渲染了。虽然 Button 是一个小型,快速渲染的组件,但你可能会在大型,复杂,渲染速度慢的组件上看到这些内联定义,它可能会让你的 React 应用程序陷入囧境,所以最好不要在 render 方法中定义这些函数。如果函数确实依赖于组件,以至于无法在组件外部定义它,你可以将组件的方法作为事件处理传递过去:class SomeComponent extends React.PureComponent { createAlertBox = () => { alert(this.props.message); }; get instructions() { if (this.props.do) { return ‘Click the button: ‘; } return ‘Do NOT click the button: ‘; } render() { return ( <div> {this.instructions} <Button onClick={this.createAlertBox} /> </div> ); }}在这种情况下,SomeComponent 的每个实例都有一个不同的警告框。 Button 的click事件侦听器需要独立于 SomeComponent。 通过传递 createAlertBox 方法,它就和 SomeComponent 重新渲染无关了,甚至和 message 这个属性是否修改也没有关系。createAlertBox 内存中的地址不会改变,这意味着 Button 不需要重新渲染,节省了处理时间并提高了应用程序的渲染速度但如果函数是动态的呢?修复(高级)这里有个非常常见的使用情况,在简单的组件里面,有很多独立的动态事件监听器,例如在遍历数组的时候:class SomeComponent extends React.PureComponent { render() { return ( <ul> {this.props.list.map(listItem => <li key={listItem.text}> <Button onClick={() => alert(listItem.text)} /> </li> )} </ul> ); }}在本例中,有一个可变数量的按钮,生成一个可变数量的事件监听器,每个监听器都有一个独特的函数,在创建 SomeComponent 时不可能知道它是什么。怎样才能解决这个难题呢?输入记忆,或者简单地称为缓存。 对于每个唯一值,创建并缓存一个函数; 对于将来对该唯一值的所有引用,返回先前缓存的函数。这就是我将如何实现上面的示例。class SomeComponent extends React.PureComponent { // SomeComponent的每个实例都有一个单击处理程序缓存,这些处理程序是惟一的。 clickHandlers = {}; // 在给定唯一标识符的情况下生成或返回单击处理程序。 getClickHandler(key) { // 如果不存在此唯一标识符的单击处理程序,则创建 if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) { this.clickHandlers[key] = () => alert(key); } return this.clickHandlers[key]; } render() { return ( <ul> {this.props.list.map(listItem => <li key={listItem.text}> <Button onClick={this.getClickHandler(listItem.text)} /> </li> )} </ul> ); }}数组中的每一项都通过 getClickHandler 方法传递。所述方法将在第一次使用值调用它时创建该值的唯一函数,然后返回该函数。以后对该方法的所有调用都不会创建一个新函数;相反,它将返回对先前在内存中创建的函数的引用。因此,重新渲染 SomeComponent 不会导致按钮重新渲染。类似地,相似的,在 list 里面添加项也会为按钮动态地创建事件监听器。当多个处理程序由多个变量确定时,可能需要使用自己的聪明才智为每个处理程序生成唯一标识符,但是在遍历里面,没有比每个 JSX 对象生成的 key 更简单得了。这里使用 index 作为唯一标识会有个警告:如果列表更改顺序或删除项目,可能会得到错误的结果。 当数组从 [‘soda’,‘pizza’] 更改为 [‘pizza’] 并且已经缓存了事件监听器为 listeners[0] = () => alert(‘soda’) ,您会发现 用户点击提醒苏打水的披萨的now-index-0按钮。 但点击 index 为 0 的按钮 pizza 的时候,它将会弹出 soda。这也是 React 建议不要使用数组的索引作为 key 的原因。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

March 10, 2019 · 3 min · jiezi

React Native 开发环境部署

1.java官方下载安装https://www.oracle.com/techne…检查环境变量C:UsersAdministrator>java -versionjava version “1.8.0_191"Java(TM) SE Runtime Environment (build 1.8.0_191-b12)Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)2.nodejs推荐使用nvm 管理nodejs版本 安装包地址 https://github.com/coreybutle...windows nvm-noinstall.zip: 绿色免安装版本,但是使用之前需要配置环境变量nvm-setup.zip:安装包,下载之后安装,无需配置就可以使用。LinuxSource code(zip):zip压缩的源码Sourc code(tar.gz):tar.gz的源码,Linux系统检查nvm 安装是否成功C:UsersAdministrator>nvm version1.1.7安装指定nodejs 版本nvm install 10.15.3选择nodejs版本nvm use 10.15.3安装 Android Studiohttps://dl.google.com/dl/andr…安装 Android sdk tool下载地址https://gsf-fl.softonic.com/c…://android-sdk.en.softonic.com&Filename=installer_r24-4-windows.exe安装 react-native-clinpm install -g react-native-cli npm下载慢使用淘宝 cnpm 或者挂代理创建项目react-native init 项目名称创建指定版本的react-native项目react-native init 项目名称 –version 0.55.4指定创建项目目录切换到指定的项目后 执行 react-native init 项目名称执行失败进入项目目录 执行 npm/cnpm install 后运行

March 6, 2019 · 1 min · jiezi

React-Native从搭建环境到 发布 APP 指北

开始前的话语:1、由于andriod studio不易下载,并且占用内存大,运行的AVD模拟器非常迟钝。所以本文采用genymotion模拟器搭建,它更加轻量,运行更流畅。2、由于很多学习react的用户,都是在windows电脑上开发,完了顺便学习下react-native,所以本文是用于搭建android环境的(mac电脑没钱买,但不好意思说)开发环境要求:Node 的版本必须高于 8.3,Python 的版本必须为 2.x(不支持 3.x),而 JDK 的版本必须是 1.8(目前不支持 1.9 及更高版本)android版本为9(由于最新的react native默认为9,其实其他版本也行,但要改配置,比较麻烦)一、环境搭建1、jdk下载及其环境变量配置如果学过java,则忽略本步骤。如果小白,则继续阅读点击JDK官网,下载对应版本的jdk,然后双击安装。然后一路“下一步”。默认会安装在C盘 C:Program FilesJavajdk1.8.0_201 路径下配置环境变量桌面–>我的电脑–>右键——>属性点击"高级系统设置"点击"环境变量"编辑用户变量Path新建,将刚才安装的jdk路径复制到输入框中。最后别忘了,点击“确定”关闭对话框。然后打开cmd,输入>java -version如果出现版本号,则说明jdk配置完毕。2、python下载及其环境变量配置进入Python官网下载对应版本,然后双击安装,默认一路“下一步”类似jdk环境变量,将python的安装路径,配置到环境变量中。3、SDK下载虽然我们不需要android studio来开发react-native,但是在启动react时,如果启动的是android,则还是要sdk包的支持和编译。才能将app安装到genymotion模拟器中运行。我们只需要下载sdk manage来管理sdk包,可以不用任何翻墙和代理,即可下载。速度还很快这是我的sdk manage的百度网盘地址:链接:https://pan.baidu.com/s/1uUmz… 提取码:m3fl 下载完,双击安装,一路"下一步"。然后配置sdk的环境变量,但是需要注意,不是加入到path中,而是新建个名为ANDROID_HOME的变量,然后将刚才sdk安装的路径设置到变量值中然后,进入到sdk安装目录,找到s并双击打开。找到android 9,展开勾选这两个,然后点击右下角install packages,稍微等待会,即可看到这两个包后面的"not installed"变成了"installed"二、Genymotion模拟器下载genymotion官网下载前,必须要注册账号,并登陆。登陆成功后,点击右上角红色的"Download"由于我们是个人用户,所以往页面下方拉,会看到"Get Genymotion personal version",点击进入下载。下载上方的包含有VirtualBox的版本。下载成功并安装。启动桌面上的"Genymotion"快捷方式,打开genymotion.进入setting首先登陆配置ADB中的sdk为刚才安装sdk的目录。然后关闭选择android 9版本的模拟器,并安装模拟器安装成功后,start启动出现如下的页面,即可表明启动成功。三、react native项目创建首先安装react-native-cli>npm install -g yarn react-native-cli然后使用init,创建项目,官网实例名为AwesomeProject,咱们也用这个吧。react-native init AwesomeProject进入AwesomeProject项目cd AwesomeProjectreact-native run-android即可在模拟器上看到react-native中的内容使用vscode打开AwesomeProject项目,打开App.js,在render中稍微修改文字,并打开genymotion模拟器,双击键盘R键(自己办公桌的键盘,不是genymotion模拟器的软键盘),即可刷新模拟器上的页面。同时,在android的outputs文件夹下,可以编译好的apk,但是这个apk是没有经过数字证书认证的,无法发布到应用商店的。四、采用数字证书编译App使用管理员权限打开cmd命令行工具,然后进入到jdk的bin目录:C:Program FilesJavajdk1.8.0_201bin 目录然后再控制台输入如下命令:keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000然后会提示你输入秘钥密码,地区,行政区,国家等等,完成后。在bin目录下会有个my-release-key.keystore文件将次秘钥文件复制到react native项目的androiud/app下修改gradle.properties文件,加入如下代码:MYAPP_RELEASE_STORE_FILE=my-release-key.keystoreMYAPP_RELEASE_KEY_ALIAS=my-key-aliasMYAPP_RELEASE_STORE_PASSWORD=*****MYAPP_RELEASE_KEY_PASSWORD=***注意部分要用你刚才申请秘钥时输入的密码替换进入android/app/build.gradle中,编辑文件,新增红色部分。然后进入到android目录下,输入命令。gradlew assembleRelease即可使用数字证书来打包app,成功后,会在apk下多出来一个realese文件夹(如果没有,点击右上角刷新)为了确保发布到应用商店的apk没有问题,我们还得把这个apk在genymotion模拟器上运行下,来简单测试下输入命令>react-native run-android –variant=release即可在模拟器上,看到有个app被安装上了。

March 5, 2019 · 1 min · jiezi

React Native开发环境搭建

前言本篇文章主要讲解React Native(简称RN)开发环境的搭建,学习这篇文章可能需要开发者要懂Android原生开发环境搭建、node环境搭建和mac基本使用。系统:macReact Native:0.58HomebrewHomebrew是Mac OSX上的软件包管理工具,能在Mac中方便的安装软件或者卸载软件,相当于linux下的apt-get、yum神器。Homebre可以在Mac上安装一些OS X没有的UNIX工具,Homebrew将这些工具统统安装到了 /usr/local/Cellar 目录中,并在 /usr/local/bin 中创建符号链接。Homebrew官网:https://brew.sh/index_zh-cn.html 。Homebrew的安装通过以下命令来安装Homebrew。/usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"Homebrew的使用安装软件:brew install 软件名,例:brew install wget搜索软件:brew search 软件名,例:brew search wget卸载软件:brew uninstall 软件名,例:brew uninstall wget更新所有软件:brew update,通过 update 可以把包信息更新到最新,不过包更新是通过git命令,所以要先通过 brew install git 命令安装git。更新具体软件:brew upgrade 软件名 ,例:brew upgrade git显示已安装软件:brew list查看软件信息:brew info/home 软件名 ,例:brew info git查看那些已安装的程序需要更新: brew outdated显示包依赖:brew reps安装Node、watchman我们使用mac上软件包管理工具安装Node和watchman。brew install nodebrew install watchmanNode.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。 Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。 Node.js 的包管理器 npm,是全球最大的开源库生态系统,前端开发人员通过使用npm来管理依赖包。node版本需要在v8.3以上,安装完node后建议科学上网加速项目的搭建过程。Watchman则是由 Facebook 提供的监视文件系统变更的工具。安装此工具可以提高开发时的性能(packager 可以快速捕捉文件的变化从而实现实时刷新)。安装React Native命令行工具通过安装react-native-cli命令行工具来生成React Native项目。npm install -g react-native-cliJava开发环境由于Android开发依赖于Java开发环境,所以需要去下载JDK。React Native目前支持Java Development Kit [JDK] 1.8(暂不支持 1.9 及更高版本)。所以建议去官网下载1.8版本的jdk。官网地址: https://www.oracle.com/techne… 。Android开发环境由于我是一名Android开发人员,所以在这里简单介绍一下Android开发环境搭建过程,可能下载的开发工具需要翻墙。Android studioAndroid studio是Android官方的开发工具,Android studio下载可以去官网(https://developer.android.com… studio,也可以去AndroidDevTools(https://www.androiddevtools.cn/),里面有很多不需要翻墙就可以下载的工具。如果你是一名新手,注意安装Android studio的时候,安装界面选择"custom"选项,选择安装一下模块。Android SDKAndroid SDK PlatformPerformance (Intel ® HAXM)Android Virtual Device然后,点击Next即可安装所选择的模块。安装完成后即可看到欢迎页面。官方教程:https://developer.android.com…Android SDKAndroid Studio默认会安装最新版本的Android SDK。你可以在Android Studio的SDKManager中选择安装各版本的SDK。由于目前编译 React Native 应用需要的是Android 9 (Pie)版本的 SDK,所以在SDK Manager中选择"SDK Platforms"选项卡,然后在右下角勾选"Show Package Details”。展开Android 9 (Pie)选项,确保勾选了下面 Android SDK Platform 28 组件(重申你必须使用稳定的翻墙工具,否则可能都看不到这个界面)。然后点击"SDK Tools"选项卡,同样勾中右下角的"Show Package Details"。展开"Android SDK Build-Tools"选项,确保选中了 React Native 所必须的28.0.3版本。你可以同时安装多个其他版本。然后点击"Apply"来下载和安装这些组件。配置Android SDK环境变量React Native需要通过环境变量来了解你的Android SDK装在什么路径,从而正常进行编译。Mac上配置环境变量时经常要创建、编辑 .bash_profile文件,所以我们需要在.bash_profile中添加Android SDK的环境变量配置。首先,在命令行中输入命令编辑.bash_profile,vim和vi都是文本编辑器,不了解的可以去http://www.runoob.com/linux/l… 学习一下vim或者vi的使用。vim/vi ~/.bash_profile然后,点击i进行编辑,添加以下配置。export ANDROID_HOME=$HOME/Library/Android/sdkexport PATH=$PATH:$ANDROID_HOME/toolsexport PATH=$PATH:$ANDROID_HOME/tools/binexport PATH=$PATH:$ANDROID_HOME/platform-toolsexport PATH=$PATH:$ANDROID_HOME/emulatorAndroid Studio官方下载的SDK会放在$HOME/Library/Android/sdk下,如果是自定义下载的Android SDK,则需要更改ANDROID_HOME的值。然后使用命令来使环境变量设置立即生效。source $HOME/.bash_profile创建项目首先使用之前安装的React Native命令工具来创建一个新工程。react-native init MyApp使用visual studio code打开,目录如下:然后准备Android设备,在这里可以使用usb连接Android真机,也可以使用Android模拟器,像genymotion、Android Virtual Device。使用真机需要打开开发者模式,并启动usb调试。准备好设备后,进入到项目根目录中,cd MyApp,然后输入命令行运行程序。react-native run-android也可以使用Android Studio直接打开工程根目录下的Android程序,运行即可。参考:React Native官方文档:https://reactnative.cn/docs/g… ...

February 28, 2019 · 1 min · jiezi

IOS开发错误library not found for -lXXX

最近在使用ReactNative技术搭建新项目的过程中,在集成神策分析时,在进行IOS端配置的时候Xcode进行build项目的时候遇到如下报错:library not found for -lRNSensorsAnalyticsModule,忘记截图了,用如下这张代替一下甚是烦恼,不知道是咋回事。。。最后找到了原因,是因为在如下libararies中无意添加了一个无效的libRNSensorsAnalyticsModule.a文件解决方法:选中这个文件,点击图中的减号即可使用Xcode重新clean product后,重新build即可。其他常见问题的处理方法1:library not found for -XXX 的问题有可能是你的某个库的连接引用有问题,解决的办法就是在项目的target里,选中Link Binary With Libraries 里的.a或framework 取消再加入,就可以了。2:warning:directory not found option 的问题可能是framework search paths 还有Library Search Paths 里面并没有这个路径,删除即可。3:duplicate就是重复的意思,那么看代码 要删除重复的openUDID,以及libwoa_two和sbjson冲突了,删除sbjson的点m即可.

February 27, 2019 · 1 min · jiezi

RN基础

生命周期1. getDefaultProps() 初始化一些默认的属性2. constructor() 状态进行初始化3. componentWillMount() render() 之前4. render() 渲染组件5. componentDidMount() 组件状态改变后执行一般会将网络请求等加载数据的操作,放在这个函数里进行,来保证不会出现UI上的错误。6. componentWillReceiveProps(nextProps)当组件接收到新的props时,会触发该函数7. shouldComponentUpdate() 控制状态改变后渲染视图8. componentWillUpdate() 视图即将改变9. componentDidUpdate() 视图已经改变后执行10. componentWillUnmount() 即将销毁安卓和ios的一些兼容问题1.textinputandroid 默认有下划线 ios 无2.textandroid 默认背景透明 ios 无text 在ios下无法设置圆角,必须套view3.overflow:hiddenandroid 没效果 超出部分不可见 ios 可以4.image的 borderTopLeftRadiusios 不可以 可以外面 套一层5.scrollview scrollEventThrottle ios 默认16 android 默认0 嵌套listview时 list 滑动 ios 有些时list滑 android 是 先滑scrollview 滑完了 在 滑 list 建议修改布局直接使用list 套所有6. statusbarandroid 可定制沉浸 ios 默认 沉浸性能优化一.简单粗暴 setNativeProp它直接在底层(DOM、UIView等)而不是React组件中记录state(这样会使代码逻辑难以理清)二、使用setState => shouldComponentUpdate三、清楚console.log四、InteractionManager.runAfterInteractions先动画后渲染数据 TouchableOpacity五、使用transform: [{scale}]调整容器(图片)的宽高六、点击变化,将作何的动画包装在requestAnimationFrame处理器中

February 27, 2019 · 1 min · jiezi

React Native组件开发指南

React Native的组件开发一直处在一个比较尴尬的处境。在官方未给予相关示例与脚手架的情况下,社区中依然诞生了许许多多的React Native组件。因为缺少示例与规范,很多组件库仅含有一个index.js文件。这种基础的目录结构也导致了一些显而易见的问题,例如“如何测试”,“如何预览”,“如何开发”……本文将为各位提供一种React Native组件开发的示例目录结构及相关配置指南。示例目录结构.├── src│ └── index.js├── test│ └── index.test.js ├── demo│ ├── .gitignore│ ├── .watchmanconfig│ ├── App.js│ ├── app.json│ ├── babel.config.js│ ├── metro.config.js│ └── package.json├── .eslintrc.js├── babel.config.js├── README.md├── .gitignore└── package.json目录结构主要区分为4块内容根目录,src目录,test目录,demo目录。根目录包含了eslint配置,babel配置,README, gitignore, package.json。其中babel配置与package.json中的依赖定义是为了运行测试用例而存在的。src目录包含了当前React Native组件的源码,是组件开发最主要的目录。test目录包含了当前React Native组件的测试相关代码。demo目录包含了一个独立的Expo项目,其中App.js文件是开发组件示例最主要文件,其中会引用src目录中提供的组件来进行开发与展示。该目录的配置详情会在下文中继续展开。为什么用Expo来进行开发与展示?Expo是一个基于React Native包裹的React Native应用开发框架。许多React Native的开发者对于Expo依然持怀疑态度。不可否认的是用Expo来开发React Native应用确实存在一些问题,例如:引入Expo SDK后,应用体积过大的问题缺乏应用在后台运行的能力…但是绝大多数Expo的弊端是我们在组件开发中不会遇到或者可以避开的,那么随之而来的便是Expo的优点:快速安装与上手快速在网页、模拟器、实机上预览或测试与React Native的无缝兼容性相信开发过React Native的同学一定会抱怨它沉重的依赖安装,与繁琐的调试过程,而Expo正好轻量化了这两个过程,不仅加速了我们的组件开发与预览,也在我们的组件目录中去除了Native端相关的代码,轻量化了我们的目录结构。相关配置指南引入Expo为组件项目引入Expo可能没有听上去这么容易,因为我们在上文的目录结构中将src目录定义成与demo目录平行的目录结构,这就导致了metro(React Native打包工具)的默认配置将无法正常打包demo目录中的React Native代码。为了解决这个问题,我们就需要手动去调整metro的配置文件,而metro配置文档又以“精简”著称,于是配置metro便成了一个极大的困难点。准备工作首先我们需要安装Expo CLI工具$ npm install -g expo-cli在组件库的根目录中运行$ expo init demo然后选择blank templatemanaged workflow你便在demo目录中生成了一个可运行的Expo项目, 可以通过运行以下命令来预览当前的Expo项目$ cd demo$ yarn start配置metro旧版本metro通常使用rn-cli.config.js作为配置文件名,而新版本则使用metro.config.js作为配置文件名。旧版本metro的配置文件格式也与新版本有较大的差别。本文将重点关注新版本metro的配置。在demo目录中创建名为metro.config.js的metro配置文件,并在Expo的应用配置文件app.json中添加如下字段用于重置项目根目录配置与注入自定义的metro配置文件"packagerOpts": { “projectRoots”: “”, “config”: “metro.config.js”}在metro.config.js中添加如下内容const path = require(‘path’);const blacklist = require(‘metro-config/src/defaults/blacklist’);const escapeRegexString = require(’escape-regex-string’);module.exports = { resolver: { blacklistRE: blacklist([ new RegExp( ^${escapeRegexString(path.resolve(__dirname, '..', 'node_modules'))}\\/.*$, ), ]), providesModuleNodeModules: [ ‘react-native’, ‘react’, ‘prop-types’, ], extraNodeModules: { ‘@babel/runtime’: path.resolve(__dirname, ’node_modules/@babel/runtime’), }, }, projectRoot: path.resolve(__dirname), watchFolders: [ path.resolve(__dirname, ‘..’), ],};来仔细解析一下上面的配置项providesModuleNodeModules: 该配置项为当前项目提供额外的providesModule路径解析名。providesModule简单来说就是一个提供文件路径别名的手段。例如在一个文件头部添加如下的注释,你就可以在项目别处通过import test from ’test’直接引入该文件。/***/```在这里我们将注入在src目录中被引用的三个库react-native, react, prop-types,使得src目录中的引用能正确被metro解析。extraNodeModules: 该配置旨在为当前项目提供额外引入的模块,配置格式为[{ 模块名 : 路径 }]。我们在这里配置src目录中需要的额外模块,例如运行测试时所需要的@babel/runtime模块。blackListRE: 配置一个正则,打包时忽略掉正则匹配到的路径。在这里我们将根目录中的node_modules路径下的所有内容忽略,目的是因为在根目录下的node_modules中会存在与demo目录下node_modules中相同的库,例如react-native, react, prop-types。这就会使得providesModule在解析时产生重名,从而导致jest-haste-map报错。projectRoot: 配置项目的根目录。watchFolders: 为项目引入除projectRoot外额外的目录,在这里我们将上层的根目录加入metro的关注列表。配置完metro,即可在App.js中引入src目录中的组件import React from ‘react’;import { StyleSheet, View } from ‘react-native’;import Component from ‘../src’;const App = () => ( <View style={styles.container}> <Component /> </View>);const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: ‘#fff’, alignItems: ‘center’, justifyContent: ‘center’, },});export default App;现在运行yarn start,就能顺利看到你的组件在Expo中展示了。小结本文主要提供了一种React Native组件的目录结构,与“如何在一个React Native组件工程中引入一个含Expo工程的子目录”的相关配置指南。这里还需要需要说明的一点是,React Native组件的目录结构可以有千万种,本文只是提供一种可行的思路供大家参考,如有更好的方案也欢迎交流与学习。本文将重点放在了引入Expo的配置指南上,如需查看该目录结构的所有文件配置,请转至Github。相关react-native-component-cli - 快速生成该目录结构的脚手架工具react-native-hsv-color-picker - 基于该目录结构的组件案例 ...

February 26, 2019 · 1 min · jiezi

开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?

开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?原创: 嘉宾-张楠 开源中国 以往我们说某一功能跨多端,往往是指在诸如 PC、移动等不同类型的设备之间都能实现;或者更加具体一点,指的是“跨平台”,可能是大到跨操作系统,比如 Windows、macOS、Linux、iOS 与 Android 等,可能是小到跨某个具体技术的不同实现库。但是今天我们要介绍的是关于跨 MVVM 架构模式各种环境的场景。Chameleon 是一套开源跨端解决方案,它的目标是让 MVVM 跨端环境大一统,实现任意使用 MVVM 架构设计的终端,都能使用其进行开发并运行。 在这样一个 MVVM 环境中,涉及到了 Weex、React-Native、WebView/浏览器与 Flutter 等各种跨端技术,还有它们实现的具体业务产品,比如微信小程序、快应用、支付宝小程序、百度智能小程序、今日头条小程序与其它各类小程序。也许你发现了,这里提到了许多种“小程序”,虽然最早微信小程序的概念甚至早期版本出现的时候,有过不少不看好的声音,但是随着它不断发展,目前已经成为了大众生活不可或缺的应用形态。马化腾透露过,截至 2018 年 11 月有 150 万微信小程序开发者,小程序应用数量超过 100 万,覆盖 200 多个细分行业,日活用户达到 2 亿。这样的成功经验与几乎触及到生活方方面面的巨大流量入口,大家都想入场,于是可以看到后来其它公司纷纷给出了类似的小程序方案。另一方面,除了小程序百花齐放,2018 年小米、华为、OPPO 等 10 家安卓手机厂商还结成了快应用联盟,并且先后发布了一系列快应用。Chameleon 目标就是要跨这些端,而随着各家不同实现越来越多,跨端场景也不断变得更加复杂。我们采访了 Chameleon 创始人张楠,请他为读者具体分享了 Chameleon 在这个过程中的成长。项目地址:https://github.com/didi/chame…本文是 Chameleon 首次对外公开实现原理!干货超多,包括:终端开发未来的开发模式Chameleon 跨端实现原理当前各种跨端方案原理对比(各种小程序、快应用等)与 Taro 的对比演进过程中遇到的困难与思考当初为什么去研发 Chameleon?关于这个问题可以从行业背景讲起。中国互联网络信息中心(CNNIC)发布的《中国互联网络发展状况统计报告》显示,截至 2018 年 6 月,我国网民规模达 8.02 亿人,微信月活 10 亿 、支付宝月活 4 亿、百度月活 3.3 亿;另一方面,2018 Q3 中国 Android 手机占智能手机整体的比例超过 80%,月活约 6 亿。BAT 与 Android 成为了中国互联网真正的用户入口。但凡流量高的入口级别 APP 都希望做平台,成为一个生态平台和互联网流量入口,大量第三方应用的接入,从业务层让公司 APP 关联上更多企业的利益,并且拥有更强的生命力;从技术层面可以利用“本地能力接口层”收集大量用户数据,从消费互联网到产业互联网需要大量各行各业基础用户数据线索进行驱动和决策。在这么一种背景下,再结合计算机技术的发展历史,我们知道每一种新技术的出现都会经历“各自为政”的阶段,小程序技术也不例外,所以我们看到了其它各种小程序平台出现。微信小程序作为首创者,虽然其它小程序都有在技术实现原理、接口设计上刻意模仿,但是作为一线开发者在不同平台发布小程序,往往还是需要重复开发、测试,从前 1 单位的工作量变成了 N 单位的工作量。而这还没算上快应用等其它入口。这种情况下,滴滴的研发工程师是其中最显著的“受害者”之一,滴滴出行在微信钱包、支付宝、Android 快应用都有相关入口,而且用户流量占比不低。研发同学在端内既追求 H5 的灵活性,也要追求性能趋近于原生。面对入口扩张,主端、独立端、微信小程序、支付宝小程序、百度小程序、安卓厂商联盟快应用,单一功能在各平台都要重复实现,开发和维护成本成倍增加。迫切需要一个只维护一套代码就可以构建多入口的解决方案,于是我们着手去打造了 Chameleon(CML,卡梅龙)这么一个项目,真正专注于让一套代码运行多端。Chameleon 核心是运用了 MVVM 架构,为什么它可以实现跨多端?MVVM 也就是 Model View ViewModel,它本质上是 MVC( Model View Controller)的进化版本,将 View 的状态和行为抽象化,使得视图 UI 和业务逻辑分开。它是一种让数据驱动反射视图的模式,发展到现在可能会偏离它的初衷了,更像是一个视图数据间的“通信协议”,让终端开发变得更加单纯,这是一种趋势,面向未来框架都采用这种模式。Facebook 在 2013 年开源 React,React 这个项目本身是一个 Web UI 引擎,随着不断发展,它衍生出 React Native 项目,用来编写原生移动应用。正是它给跨端方向带来了 MVVM 模式。Vue.js 于 2014 年左右发布,逆流而上占据了大量用户群体,2016 阿里巴巴也基于它发布了 Weex 项目,使得可以用 Vue 编写 Native App。Google 在 2018 年末正式发布了面向未来的跨 Android、iOS 端的 Flutter 1.0.0。原理我们知道终端开发离不开三大要素——界面表现(结构、外观)层、逻辑处理层与系统接口层(网络、存储与媒体等)。开发者编写代码时在初始化阶段(生命周期)调用“界面表现层”界面模型的接口绘制界面,当用户触摸界面时,“界面表现层”将事件发送给用户“逻辑处理层”,后者经过条件判断再处理并反馈到用户界面,处理过程可能需要调用“系统接口层”,反馈过程需要调用“界面表现层”的接口。常规的终端开发架构模式下,无论是 Web 端、Android 端还是 iOS 端的项目开发,都强依赖各端的环境接口,特别是依赖界面相关模型设计。iOS 系统下绘制界面基于 Objective-C 语言环境下的 UIKit 框架;Android 系统下用户绘制界面基于 Java 语言环境,由 LayoutInflater 处理 XML 结构层次树;Web 端使用 DOM 模型和 CSS 来描述绘制界面。 MVVM 中的关键是它通过 ViewModel 这一层将界面和逻辑层彻底隔离开来,负责关联界面表现和逻辑处理层的响应事件(update/notify)关系,这一“隔离层”上下通信足够规范、足够纯净单一。 Model 进行逻辑处理是纯业务响应逻辑,任何一种语言都可以实现,你可以用 Android 的 Java,也可以用 iOS 的 Objective-C,你心情好用“世界第一语言 PHP”也能实现。之所以普遍选择 JavaScript,很大程度是因为在这个领域内它的优点显著,如学习成本低、天生具备跨端属性、虚拟机(V8、JavaScriptCore)和各方向组件建设较好、生态活跃。而系统接口层则更简单了,只需穷举统一基础接口+可扩展接口能力即可。各种 MVVM 方案具体来看看各种 MVVM 方案都是怎么样的。React Native、Weex 与快应用的 MVVM开发者编写的代码在虚拟机(V8、JavaScriptCore)里面运行,虚拟机容器里面包含扩展的系统基础接口。运行时,将描述界面的数据(主要是 CSS+DSL 所描述内容)通过通信层传递给 Android、iOS 端的渲染引擎,用户触摸界面时,通过通信层传递给虚拟机里面的业务处理代码,业务处理代码可能调用网络、储存与媒体等接口,最后再次反馈到界面。Flutter 的 MVVMFlutter 和 RN 的最大区别在于将“JavascriptCore/V8+JS”替换成“C++ 实现的 engine+Dart 实现的 Framework+静态类型 Dart+编译成机器码”。Flutter 的方案如下图所示:Service 其实就是本地能力接口层,Widget 树是视图层模型。Flutter 和 RN 的使用面设计上类似,Flutter 文档中提到“In Flutter, almost everything is a widget.”,widget 的调用从 RN 的 JSX 变成 Flutter 的 widget 调用,UI 的外观描述从 RN 的 CSS(文本样式、布局模型、盒模型)到定制化 Flutter Widget(textStyle 、Layout Widget、Widget)。本质上 Flutter 也是 MVVM 架构,逻辑层通过 setState 通知视图层更新,一定程度上这也是为什么 Flutter 敢说能转成 Web 框架的原因,核心还是基于这类数据驱动视图架构模式,业务代码不会深度依赖任何一端特有的“视图模型”。各类小程序的 MVVM小程序本质上和 Weex、React Native 的设计思路基本一样,最大区别在于前者还是用浏览器 WebView 做渲染引擎,而后者是单独实现了渲染引擎(所以大量的 CSS 布局模型不支持)。具体到 Chameleon 上是怎么实现的?首先任何一份应用层的高级语言代码块分成几层:语言层(Language)、框架层(Framewrok)与库层(Library):Language —— 通俗来说,实现程序所需的基本逻辑命令:逻辑判断(if)、循环(for)与函数调用(foo())等。Framewrok —— 通俗来说,完成一个 App 应用交互任务所需规范,例如生命周期(onLoad、onShow)、模块化与数据管理等。Library —— 可以理解就是“方法封装集合”。比如 Web 前端中 Vue 更适合叫框架,而 jQuery 更适合叫库;Android 系统下 activity manager + window Manager View System 等的集合叫框架,而 SQLite 、libc 更适合叫库。对应到 Chameleon 就是这样:具体到实现原理全景架构图如下:你可以理解 Chameleon 为了实现“让 MVVM 跨端环境大统一”的目标做了以下工作:定义了标准的 Language(CML DSL)、Framework 与 Library(内置组件和 API)协议层。在线下编译时将 DSL 转译成各端 DSL,只编译 Language 层面足够基础且稳定的代码。在各个端运行时分别实现了 Framework 统一,在各个端尽量使用原有框架,方便利用其生态,这样很多组件可以直接用起来。在各个端运行时分别实现了 Library(内置组件和 API)。为用户提供多态协议,方便扩展以上几方面的内容,触达底层端特殊属性,同时提升可维护性。实现思路很简单,所有设计为了 MVVM 标准化,不做多余设计,所以宏观的角度就像 Node.js(libuv)同时运行在 Windows 和 macOS 系统,都提供了一个跨平台抽象层。从 MVVM 角度来看的话:View(展现层)第三方 Render Engine:各类框架已有框架,浏览器的 Vue、Webview 里的小程序引擎、Android、iOS 里面的 React Native/Weex 引擎、甚至 Flutter 里面的 Dart Framework。Chameleon 内置组件库:多态协议定义统一组件 view、input、text、block 与 cell 等,它是界面组层的原始基类,衍生出多复杂界面功能。ViewModel(关联层)Chameleon 语法转译组件调用循环条件判断事件回调关联父子关系……Model(逻辑响应层)JavaScript 代码CML Runtime 框架Chameleon API:多态协议定义统一接口,cml.request、cml.store 等Chameleon 的跨多端方案给开发者的开发带来了极大的便利,具体表现是怎么样的?一句话:基于 Chameleon 开发,效率会越来越高。各个端的涌现,让原本是 1 的工作量因为多端存在而变成 N 倍,使用 Chameleon,工作量会变回 1.2。这多出来的 0.2 工作量是要处理各端的差异化功能,比如以下场景:某业务线迁入 Chameleon 时,发现没有“passport登录组件”,在各类小程序里面能免密登录了,在 Web、Native 端是弹出登录框登录,不同业务用户交互形态不一样所以 Chameleon 没有提供组件;开发者需要基于多态协议扩展单独一个登录组件<passport/>,无论如何最后返回一个登录后的回调 token 即可,外部无需组件关心里面如何操作。用户需要分享功能,发现没有“share组件”,在微信 Web 端可以引导右上角分享,在小程序直接分享,不同业务用户交互形态不一样,用户需要基于多态协议扩展单独一个登录组件<share/>。这种各端差异较大的例子,随着业务的积累,可以变成了一个个业务组件单独维护,后面也不需要重复开发了,且反推产品体验一致化,组件三层结构“CML框架内置组件->CML扩展组件->业务开发者自己扩展的多态组件”达成 100% 统一。随着组件积累业务开发工作量越来少,工程师可以专注做更加有意义的事情,这就是 Chameleon 存在的目的。基于统一的跨端抽象,用户在 Chameleon 项目持续维护过程中,Chameleon 发布新增一个端之后,你的业务代码基本不用改动即可无缝发布成新端。比如这个 cml-yanxuan 项目开发时支持 3 个端,后面新增了百度、支付宝小程序端,原有代码直接能跑起来运行 5 个端,一端所见即多端所见。开发时只能跑 3 个端原有代码无缝支持 5 个端另外特别强调的是,对于大公司团队,如果有很强的技术能力,希望开发的代码掌控在自己手里,对输出结果有更好控制能力。其实 Chameleon 内置组件和内置 API 是可以替换的,那么所有组件都是业务方自己开发了,哪天不想用了直接导出原生组件即可离开 Chameleon,如下图:目前跨多端统一的方案中,Taro 是比较亮眼的,能否具体对比一下 Chameleon 与 Taro。我们觉得 Chameleon 与其它解决方案的最大区别在于其它框架都是小程序增强,即用 Vue 或者 React 写小程序,这些框架官方给的已接入例子也都是跑微信小程序。它们更加类似 Chameleon 的前身 MPV(Mini Program View),即考虑如何增强小程序开发。2017 年微信小程序发布时,滴滴作为白名单用户首先开始尝试接入,开始面对重复开发的难题。这时候我们专门成立了一个小项目组,完成一个名为 MPV 的项目,一期目标是“不影响用户发挥,不依赖框架方的原则性实现一套代码运行 Web 和微信小程序”。看着很美好,用这样的方案实现 Web 端和小程序端,也确实完成了超过 90% 代码重用,总体上开发效率和测试效率都有了一定提升,但是却不是真正意义上的跨多端统一。单独说到 Chameleon 与 Taro 的区别,总体上看,可以归为这样一个表:表中每一项都是在做跨端方案时需要考虑到的。我们说除了 Chameleon,其它方案都只是在对小程序进行增强,或者说是模仿微信小程序的 API 和组件的接口设计。Taro 是通过将 JSX 转成小程序模板,在其它端模拟微信小程序的接口和组件,让其它端更像微信小程序,业务开发时不一致的地方需要环境变量判断差异分别调用,会造成端差异逻辑和产品逻辑混合在一起。此外,它要跟随小程序更新,业务方会有双重依赖;其它端的和小程序不能保持一致,用户要各种差异化兼容,不利于维护。那 Chameleon 呢?Chameleon 把这些问题都考虑到了,所以在早期伪跨端 MiniProgram View 成型之后不断演进的过程中,把它发展成为一个真正的跨多端方案。前边的表格显示了,Chameleon 既考虑统一性,又考虑差异性,且差异性不会影响可维护性;当各端差异确实太大,那就不要用一套代码实现多个端同一页面,而是统一公用组件。这还只是拿 Chameleon 与 Taro 的重合点进行了对比,但是别忘了 Chameleon 不仅仅是前端框架,它:还有统一的 Chameleon Native SDK,Chameleon 不仅仅希望统一各类小程序,还要覆盖自家 APP,会持续通过 Native SDK 扩展 API 和组件,期望有与小程序一样的本地能力。理想情况下,一套代码就能在各类小程序、自家 APP 里面无缝平滑运行。还有待开源的后台管理系统。还有待开源的 XEdtior 非研发用编辑器,可以直接编辑跨端页面、直接发布。另外,未来还将带来以下能力:后端统一接口(消息推送、分享与支付等)基于统一的 MVVM 标准,更有基于 Flutter 的原生 APP当前的各类小程序和 Native 跨端框架,类似当年多个浏览器时,Safari、Chrome、Firefox、IE 6/7/8/9、Android 浏览器等盛行的时代。以这个来类比,那么 Chameleon 的接口组件设计上更像一个 jQuery。网络请求有的是 XHRHttprequest 有的是 ActiveXObject,jQuery 考虑的是用户需要什么,需要一个网路请求接口,支持 get、post 等,所以 jQuery 写一个既非 ActiveXObject 又非 XHRHttprequest 的名为 $.ajax 接口,提供一个封装网络接口,你不用关心内部在不同端怎么调用的,jQuery 内部会帮你兼容。Chameleon 也是一样的思路,所有的接口设计都是真正能兼容跨所有的端,没有差异性,而且只保留当前所在端的接口调用代码:IE 里面只保留 ActiveXObject,Chrome 只保留 XHRHttprequest。Chameleon 的接口设计上比 jQuery 更强的地方在于,使用标准的多态协议,保障可维护性,性能上只保留当前端代码,且将多态协议暴露出来,让用户也能扩展自己想要的 API(类比 $.xxx)。当然时代已经变了,监听视图不在是 $(’#xxx’).click(fn),而是 MVVM 数据驱动视图方式了,所以提供了 Chameleon 双向绑定这样的 VM 层。前边讲到了 Chameleon 的前身 MPV,那具体分享一下 Chameleon 的整个演进过程吧。出生期:选择转译还是模拟小程序环境?前面讲到,2017 年的时候,我们完成一个名为 MPV 的项目,一期目标是不影响用户发挥,不依赖框架方的原则性实现一套代码运行 Web 和微信小程序。当时缺乏小程序资料是遇到的最大问题(就更别提今天讲到的业内这么多解决方案了),当时唯一一个可以参考的开源项目是 WEPT,WEPT 是一个微信小程序实时开发环境,它的目标是为小程序开发提供高效、稳定、友好、无限制的运行环境。它的设计思路是在 Web 端模仿小程序环境执行。于是我们在开发 MPV 时考虑了两种实现策略:1、在 Web 端像 WEPT 一样 mock 小程序环境;就像微信开发者工具里面也模拟了小程序执行环境,WAServie、WAWebview 提供的两套环境源码做底层,在页面中开启三个独立运行环境运行并用 iframe 通讯模拟微信小程序的 3 个 Webview 之间的联通关系。2、逐个转译代码支持小程序,缺点是可能会有 edge case 需要处理以及潜在的 bug 会比较多。最终在看完 WEPT 源码和微信开发者工具的情况下,我们明确放弃了第 1 条实现策略,选择了逐个转译代码支持小程序的路线,主要原因是于 Web 端兼容微信所有的功能,尺寸过于庞大。经过三个月紧锣密鼓的开发终于实现了第一版本 MPV: 经过实现几个 demo 之后,开始执行迁移计划: MPV 在 Webapp 上实践最终实现效果如下:最终实现效果挺美好,也确实完成了超过 90% 的代码重用,总体上开发效率和测试效率都有了明显提升。但是在后续实践过程中,发现存在大量的问题,并且项目越大问题越凸显出来,总结如下:可维护性问题,没有隔离公用代码和各端差异代码。项目中不止有业务逻辑,还混杂着 Web 端和小程序端产品功能差异化逻辑。比如前边举过的例子,分享功能 Web 端无法实现(引导分享),小程序可以实现,意味着各种环境判断各种差异化逻辑,牵一发动全身,还要来回测试。方向选择错误,MPV 使用了小程序语法标准(小程序的生命周期、API 接口等),导致用户使用上无法清晰理解。不能直接使用各端已有生态组件,即缺乏标准规范接入某个端已有开源组件。比如 Web 端 pick.js 组件缺乏快速接入规范,用户要么重新开发,或者在模板和 js 代码中使用环境判断的方式针对引入。最终导致同一功能不同端的调用方式、输入与输出不一致。业务项目依赖 MPV 框架。框架依赖微信小程序接口(模板、生命周期与接口),扩展了统一接口。例如微信小程序更新了 wx.request 时,业务项目方无法立刻使用,需要等框架更新。文件夹结构混乱,混杂着多个端代码文件,且识别成本高。不支持 vuex、redux 等高效数据管理方式尺寸单位不统一,px 和 rpx 不一致周边小型差异点太多:协议不一致,例如 Web 端可以用 //:www.didiglobal.com/passenger/create ,小程序只能用 https://:www.didiglobal.com/passenger/create打开一个新页面时链接不统一,例如打开发单页时,Web 端是 //:www.didiglobal.com/passenger/create,小程序是 /page/create页面之间跳转时,传参不统一debug 成本高,修改完代码之后两端需要测试两端界面效果不一致,基础内置组件统一性建设不足工程化建设落后,例如不支持 liveroload、数据 mock、资源定位、proxy、多端统一预览接口设计不完整,生命周期、组件分层、本地 API 设计等模板 DSL 语法不规范成长期:从伪统一到大一统在 MPV 的实践积累下,有了一定的底气和把握,后续的规划更加明确。2018 年 4 月我们把跨端项目规模进一步扩大,想要做一个真正跨 N 端的解决方案,目标是提供标准的 MVVM 架构开发模式统一各类终端。这就是 Chameleon 的出现契机。Chameleon 真正想要一套代码运行多端,总结下来要解决几大问题:要全面完成端开发的所有细节的统一性,特别是界面统一性有大量细节要做要在完成上一条的前提下考虑差异化定制空间持续可维护目标理想业务形态是这样的:图中上半部分是传统开发方式,下半部分 Chameleon 的模式抽象出了 UI 渲染层和本地接口能力层,业务代码一部分简单页面由 XEditor(h5Editor 的前身)编辑工具产出,另一部分工程师使用 Chameleon 开发,不止解决跨端问题,还弥补改进了工程开发过程中的效率、质量、性能与稳定性问题,让工程师专注有意义的业务,成长更快。首个 Native 渲染引擎选择——小程序架构、RN/Weex 架构从 MPV 到 Chameleon,外界看来最明显的变化是从跨 2 端(Web、小程序)升级到跨多端(Web、小程序、Android、iOS),最开始纠结于首个端上版本的渲染引擎使用小程序架构还是 RN/Weex 架构。RN/Weex 网上有大量资料可查,但是小程序方面则不然。千辛万苦搜索之后,根据一位知道内情的朋友的描述分享,才有了一定的了解。 这里分享几个印象深刻的要点:小程序展现层使用 Webview,里面内置了一套 JS 框架用来和 Native 通信,真正业务代码执行在单独 JS 虚拟机容器实例中JS 虚拟机容器使用情况,iOS 系统是 JavaScriptCore,Android 系统使用 QQ 浏览器的 X5 内核小程序的各个 TAG 组件使用的数据驱动用的是 Web Components显而易见,部分性能要求较高的使用原生控件(视频、键盘等等)插入到 Webview 里面。原生控件的具体位置 Native 怎么获取?答案是由嵌入到 Webview 的一套小程序框架通知给原生层原生控件怎么保证在内部可滚动的元素(Scroll-view)里面正常滚动?答案是 CSS 设置 -webkit-over-scroll:touch 时,iOS 的实现是原生的 UIScrollView,Native 可以通过一些黑科技找到视图层级中的 UIScrollView,然后对原生控件进行插入和处理;而 Android 直接绘制没办法做到这点。现在(截至 4 月)仅仅是直接覆盖到 Webview 最外层的 scrollview 上,由内置到 Webview 的一套 JS 框架控制原生控件位置最终多方面分析如下:虽然小程序方案看起来很简单,但其实很多细节点需要大量打磨,从确认方案到真正可以跑起来可以线上发布,仅仅花费在终端上的研发人力为 20P*6 个月,微信小程序团队的目标和我们跨端目标不一样,他们投入这么多成本是值得的,我们为了跨端没必要投入这么高成本。所以我们选择放弃小程序渲染方案,而使用已开源的 RN/Weex 方案。第一个版本最终使用 Weex,包括团队同学去看了 Weex 源码实现。在整体设计上仅仅使用 Weex 渲染功能,外层包装接口,保障后续能有更高扩展性。Chameleon Native SDK针对 Native SDK 我们主要从原生能力扩展、性能与稳定等三个方面做了工作。 原生能力扩展:无论是 Webview 还是 React Native、Weex 甚至 Flutter 都只提供渲染能力(以及一些最基础本地接口),更多完成业务功能所需本地环境的能力(例如分享到微信)需要 Android 和 iOS 的 Native 往容器去扩展。本地能力包含 2 种,涉及 UI 界面的统一叫组件(UI 组件如登录、支付),涉及到纯能力调用的统一叫 API(网络、存储等)性能:界面展现和交互耗时关键取决于 2 块,资源加载耗时(非打包到安装包部分代码)、执行耗时稳定:主要关注灰度发布(风险可控)和线上止损,主要工作是按用户灰度发布、可以快速降级到 H5以下是性能方向中的首屏加载时间的优化数据,原有 H5 使用 SSR(Server Side Render)已经算是最快的 Web 首屏技术方案了(不考虑优化后端多模块耗时的 BIGPIPE),它保持在 1.5 秒以下,在优化后降到 0.5 秒左右。 性能优化中我们有一个关于执行速度的 TODO 计划。同样是跨端,Flutter 之所以比 Weex 和 RN 执行速度快,主要原因是前者是编译型,客户端机器运行前已经是 CPU 可识别的机器码;后者是解释型,到客户端运行前是字符串,边编译边执行,虽然做了 JIT 尽量优化,差距还是较大。其实在这中间还有一个抹平了不同 CPU 架构下机器码差异的中间码;当然前提是开发语言改成静态类型,这里不作展开。原本分 5 次开发的 Web 端、支付宝小程序、快应用、微信小程序、Native 端变成了 1.2 次左右开发了。最重要的是随着业务级别各端差异化的多态组件和跨端组件积累,后续 1.2 工作量也会变成 0.8,0.4 的优化主要来自两个方面:0.2 是普通跨端组件的积累,复用度变高0.2 是各类业务级别的差异化多态组件,例如登录功能,在 Web端、Native 端和小程序端实现和交互是不一致的,这时候业务形态不一样,设计的 <passport> 组件也不一样,只能各业务线去封装。介绍一下接下来的 roadmap。我们的最终目标是提供标准的 MVVM 架构开发模式统一各类终端。接下来的具体 roadmap 如下表所示:欢迎有共同愿景的同学加入我们一起共建,往仓库贡献自己的代码。项目地址:https://github.com/didi/chame…QQ 群:公众号:采访嘉宾介绍张楠,Chameleon 创始人,技术团队负责人,前百度资深工程师,终身学习者。 ...

February 26, 2019 · 4 min · jiezi

react native 原生模块桥接的简单说明

原文出自:https://github.com/prscX/awes…博客链接:https://ssshooter.com/2019-02…Android创建原生模块包通过继承 ReactPackage 为你的原生模块包创建 Java 类,可以这么写:覆盖 createNativeModules 和 createViewManagers 方法public class MyNativePackage implements ReactPackage { @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { }}在 createNativeModules 方法中添加原生模块public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new MyNativeModule(reactContext)); return modules;}在 createViewManagers 方法中添加原生 UI 组件public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { List<ViewManager> components = new ArrayList<>(); components.add(new RNNativeComponent()); return components;}创建原生模块先通过继承 ReactContextBaseJavaModule 创建 MyNativeModule 类。public class MyNativeModule extends ReactContextBaseJavaModule { public MyNativeModule(ReactApplicationContext reactContext) { super(reactContext); }}为了让 React Native 在 NativeModules 中找到我们的模块,我们还需要覆盖 getName 方法。@Overridepublic String getName() { return “MyNativeModule”;}现在我们已经拥有一个可以导入到 JavaScript 代码的原生模块,现在可以向其中加入功能。暴露模块方法:定义一个接受设置参数、成功回调和失败回调的 Show 方法。public class MyNativeModule extends ReactContextBaseJavaModule { @ReactMethod public void Show(ReadableMap config, Callback successCallback, Callback cancelCallback) { Activity currentActivity = getCurrentActivity(); if (currentActivity == null) { cancelCallback.invoke(“Activity doesn’t exist”); return; } }}在 JavaScript 中调用模块方法import { NativeModules } from ‘react-native’const { MyNativeModule } = NativeModulesMyNativeModule.Show( {}, //Config Parameters () => {}, //Success Callback () => {} //Cancel Callback)注意:模块方法只提供静态引用,不包含于 react 树中。创建原生 UI 组件如果你需要在 react 树中渲染一个原声 UI 组件创建一个继承 ReactNative ViewGroupManager 的 Java 类public class RNNativeComponent extends ViewGroupManager<ViewGroup> { public static final String REACT_CLASS = “RNNativeComponent”;}覆盖 getName 方法:@Overridepublic String getName() { return REACT_CLASS;}覆盖 createViewInstance 方法,返回你的自定义原生组件@Override protected FrameLayout createViewInstance(final ThemedReactContext reactContext) { return //用 FrameLayout 包裹的自定义原生组件 }创建原生 prop 方法 @ReactProp(name = “prop_name”) public void setPropName(NativeComponent nativeComponent, propDataType prop) { }JavaScript 中使用import { requireNativeComponent } from “react-native"const MyNativeComponent = requireNativeComponent(“RNNativeComponent”, RNNativeComponent, { nativeOnly: { }})<MyNativeComponent prop={this.props.prop}>iOSMacroRCTBridgeModule: RCTBridgeModule 是一个 protocol,它为注册 bridge 模块 RCTBridgeModule @protocol RCTBridgeModule 提供了一个接口RCT_EXPORT_MODULE(js_name): 在 class implementation 时使用 bridge 注册你的模块。参数 js_name 是可选的,用作 JS 模块的名称,若不定义,将会默认使用 Objective-C 的 class 名RCT_EXPORT_METHOD(method)RCT_REMAP_METHOD(, method): bridge 模块亦可定义方法,这些方法可以作为 NativeModules.ModuleName.methodName 输出到 JavaScript。RCT_EXPORT_METHOD(funcName:(NSString *)onlyString secondName:(NSInteger)argInteger) { … }上面的例子暴露到 JavaScript 是 NativeModules.ModuleName.funcName创建原生模块包我们需要在项目中添加两个文件:头文件和源文件。- MyNativePackage.h#import “RCTBridgeModule.h”@interface MyNativePackage : NSObject <RCTBridgeModule>@end- MyNativePackage.m#import “MyNativePackage.h”@implementation MyNativePackageRCT_EXPORT_MODULE();@end创建模块方法RCT_EXPORT_METHOD(Show:(RCTResponseSenderBlock)callback) {}JavaScript 中引入模块方法import { NativeModules } from ‘react-native’const MyNativeModule = NativeModules.MyNativeModuleMyNativeModule.Show(() => {})创建原生 View 组件创建 view 方法,返回你的原声组件- (UIView *)view { //Initialize & return your native component view}创建原生 prop 方法RCT_CUSTOM_VIEW_PROPERTY(prop, DATA_TYPE_OF_PROP, YOUR_NATIVE_COMPONENT_CLASS) {}在 JavaScript 中使用import { requireNativeComponent } from “react-native"const MyNativeComponent = requireNativeComponent(“RNNativeComponent”, RNNativeComponent, { nativeOnly: { }})<MyNativeComponent prop={this.props.prop}> ...

February 26, 2019 · 2 min · jiezi

Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)

前言笔者所在的趣店 FED 早在去年 10 月份就已全面使用 Taro 框架开发小程序(当时版本为 1.1.0-beta.4),至今也上线了 2 个微信小程序、2 个支付宝小程序。之所以选用 Taro,解决微信小程序原生开发的痛点是一方面,另一方面团队也有多端统一开发的诉求,Taro 无疑是当时支持最好的。另外 React 也符合个人及团队的整体技术栈,可显著降低团队学习成本。可以说,Taro 在小程序端、H5 端支持程度已经不错,也有不少上线实例可以查看,但在 React Native 的支持上,Github 中公开的项目在 RN 这块均未适配:这种现况可以理解,毕竟要做到多端统一是有一定难度的,需准确把握各端差异,并做出合理取舍,而 Taro 虽以多端为设计目标,可重心在小程序端,没有对多端做出一定的开发约束,无从下手也便正常。笔者曾在 2018 iWeb 峰会 - 厦门站做过《多端统一开发实践》的分享,提到用 Taro 开发 RN 端的坑与大体思路,并加以实践。结合趣店 FED 在过去小半年的实践经验,我们开发了首个 Taro 三端统一应用:taro-yanxuan(高仿网易严选微信小程序),用以探讨本文的重点:Taro 开发多端应用的正确姿势。相关代码已开源:https://github.com/js-newbee/…。在线预览可在线预览 H5、RN 端(直接调用了网易严选接口,若要体验登录、购物车功能,请使用网易邮箱账号登录):微信小程序H5 - 访问链接React Native请 clone 代码本地运行Expo Snacks如下是 React Native 的运行截图:首页、分类二级分类、详情购物车、个人样式管理样式管理是多端开发的首要挑战,因为 React Native 与一般 Web 样式支持度差异较大,上述几个未适配 RN 的多端项目多数已栽在样式上了,用到了大量 RN 不支持的样式,这种情况再要去兼容 RN 无异于重写页面,想必也是有心无力了。这也是本文所强调的,需把握正确的多端开发姿势。样式上 H5 最为灵活,小程序次之,RN 最弱,统一多端样式即是对齐短板,也就是要以 RN 的约束来管理样式,同时兼顾小程序的限制,核心可以用三点来概括:使用 Flex 布局基于 BEM 写样式采用 style 属性覆盖组件样式使用 Flex 布局在进一步阐述之前,需先了解 RN 端几个影响样式方案的主要差异:display 只有 flex / none,position 只有 relative / absolute;不支持标签选择器、子代选择器、伪元素,不支持 background: url() 等;文本要用 Text 标签包裹,文本的样式不能加在 View 标签上,只能加在 Text 标签上。使用 Flex 布局,不单单是因为 RN 的 View 标签有默认样式 display: flex; flex-direction: column,更重要的是 Flex 可以解决幽灵空白问题:// View 标签高度不会是 100px,图片下方会有几像素空白,称为幽灵空白<View> <Image src={…} style={{ height: ‘100px’ }}</View>常规解决方案是在 View 标签上设置 font-size / line-height: 0, 或 Image 标签 display: inline-block 等,但这些在 RN 中都不支持,给 View 标签设置 display: flex 算是唯一可靠方案了。何况 Flex 布局能力强大,为啥不用呢?只需要注意一点,RN 中 View 标签默认主轴方向是 column,如果不将其他端改成与 RN 一致,就需要在所有用到 display: flex 的地方都显式声明主轴方向。基于 BEM 写样式RN 实际上只支持一种样式声明方式,即声明 style 属性:<View style={{ height: ‘100%’ }}这也导致 Taro 在 RN 端基本只支持 class 选择器这一种写法(最终编译成对象字面量),BEM(Block Element Modifier)在此处就恰如其分的发挥了作用:避免样式冲突(RN、小程序样式独立,但 H5 不是)自解释、语义化例如每行 2 个元素的列表,每行最后 1 个元素有特定样式,用伪元素选择器 :nth-child(even) 很容易实现,在 RN 中就需要自行计算了:{list.map((item, index) => ( <View className={classNames(‘block__element’, index % 2 === 1 && ‘block__element–even’ )} />)}基于 BEM 写 class 样式,不依赖其他选择器,虽然会让代码稍显繁琐,但也能保证多端都是行得通的,不存在支持问题。采用 style 属性覆盖组件样式小程序、RN 在页面、组件间传递样式时均有问题:// 目前 Taro RN 端还未实现往组件传递 className 对应样式<CompA compClass=‘my-style’ />// CompA,样式不生效<View className={this.props.compClass} />上述场景小程序虽可通过组件外部样式 externalClasses 实现,但官网文档有强调 “在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此最好避免这种情况”;用全局样式倒是可以,但这样样式就不好维护了。那么,通过 style 传递、覆盖组件样式也就成了唯一可选方案了。需要注意一点,样式文件是会经过编译处理兼容多端的,但 style 方式需要运行时兼容:<Comp style={postcss({ background: ‘#fff’ })} />// 简单演示,如 RN 不支持 background,需改成 background-colorfunction postcss(style) { const { background, …restStyle } = style const newStyle = {} if (background) { newStyle.backgroundColor = background } return { …newStyle, …restStyle }}从这个角度看,styled-components 或许是多端开发的最佳样式方案,然而 Taro 还不支持,且微信小程序官方文档中也提到 “尽量避免将静态的样式写进 style 中,以免影响渲染速度”,全部样式都用写到 style 中恐怕就不靠谱了,但只用来覆盖少量样式不见得会有太大影响。样式兼容即便是把握了如上样式管理思路,多端样式差异的问题依然存在,例如 white-space: nowrap 这个样式在 RN 端会报错,Taro 有提供解决方案:.text { /postcss-pxtransform rn eject enable/ white-space: nowrap; /postcss-pxtransform rn eject disable/}但项目中不止一处会有这个问题,都这样写实在不太美观,可以用 Sass mixins 稍微封装下:@mixin eject($attr, $value) { /postcss-pxtransform rn eject enable/ #{$attr}: $value; /postcss-pxtransform rn eject disable/}.text { @includes eject(white-soace, nowrap);}Sass mixins 并不能解决差异,但对于部分各端不兼容的样式,通过 Sass mixins 统一处理是比较合理的方式,代码相对美观也方便维护。端能力差异相较于样式,端能力的差异倒是还好,各端差异是客观存在的,更不用说 RN 在 iOS 与 Android 上就已存在大量差异。应对端能力差异,要么改变实现思路,例如 RN 端还不支持 Taro.(get/set)StorageSync,那就改用 async / await + Taro.(get/set)Storage 实现,要么就得使用环境判断方式了。Taro 提供 process.env.TARO_ENV 用于环境判断,多数小的差异都可以用这种方式来解决:function foo() { if (process.env.TARO_ENV === ‘weapp’) { // 微信小程序逻辑 } if (process.env.TARO_ENV === ‘h5’) { // H5 逻辑 } if (process.env.TARO_ENV === ‘rn’) { // RN 逻辑 }}这个时候也比较考验开发者的封装能力了,一般是建议将这些差异逻辑的判断统一起来,例如在 src/utils 中进行封装,对外提供一致的接口,尽量不要在业务页面中杂糅太多的判断。而对于简单的环境判断处理不了的问题,就只能动用原生开发了,例如 Taro 还不支持 RN 端的 WebView 组件,就需要自己用原生 RN 实现:// Taro 页面,根据环境引入 RN 原生页面import { WebView } from ‘@tarojs/components’const WebViewRN = process.env.TARO_ENV === ‘rn’ ? require(’./rn’).default : nullexport default class extends Component { render() { return process.env.TARO_ENV === ‘rn’ ? <WebViewRN src={this.url} /> : <WebView src={this.url} /> }}// 原生 RN 页面,从 react-native 引入 WebViewimport Taro, { Component } from ‘@tarojs/taro’import { WebView } from ‘react-native’export default class WebViewRN extends Component { render() { return <WebView source={{ uri: this.props.src }} /> }}process.env.TARO_ENV 的处理是编译时而不是运行时,也就是说若不是编译 RN,上述用原生写的 RN 页面不会被打包,保证了编译成其他端时不会引入不支持的内容。原生页面能够引入,多端问题也就有了基本的实现保障。Taro RN 端的坑Taro RN 端目前小问题还是不少的,本项目开发过程中也顺带解了几个 bug:除此之外还有好几个问题,时间关系还未提 pr 解决,暂且先绕过,但其中有两个坑还是值得一说的。onClickRN 的 View 标签不支持 onClick ,但这又是很通常的需求,原生解决方式是套一层 Touchable 组件,如:<TouchableOpacity onPress={this.handlePress}> <View>{…}</View></TouchableOpacity>而 Taro 是引入 PanResponder 响应用户操作:<View {…PanResponder.carete({ …})} style={wrapperStyle}> <WrappedComponent style={innerStyle} /></View>问题在于这样多嵌套了一层 View,并把样式拆分成 wrapperStyle、innerStyle 分别应用,但样式拆分有问题,导致绑定 onClick 之后元素的样式错乱了,这点在开发过程中还是相当坑的。宽高自适应onClick 的问题也还好,改改样式能绕过去,宽高自适应的坑就比较尴尬了。小程序、H5 可用 rpx / em 实现自适应,而 RN 的自适应方案麻烦些,一般需通过 Dimensions 获取宽高再进行换算。Taro.pxTransform() 可解决该问题,但编译 RN 端样式文件时并没有考虑这点,即 width: 100px 会被编译成 width: 50,而不是 width: Taro.pxTransform(100),无法适配屏幕不同的屏幕尺寸。因此,目前 Taro RN 端还不好做到自适应,要么非百分比的宽高都用 style + Taro.pxTransform(),要么就得自己写个脚本去处理编译后的样式文件。这两个问题都提了 issue 2204 2205,有需要的可以关注下解决进度其他要做到多端统一,能说的细节点实在太多,上述实现思路虽然简单,但背后也都是隐含着对各端差异的斗争与取舍,本文也仅是列出最基本的几点,用于阐述 Taro 多端开发的核心思路。本项目代码没有做过多封装,方便阅读,也实现了足够多的样式细节进行踩坑,具体涉及的踩坑点、注意事项都在代码中以注释 // TODO(Taro 还未支持的)、// NOTE(开发技巧、注意事项)注明了,更多内容就有待各位去实践、体会了。总结如前言所说,Taro 虽然是以多端为设计目标,但重心是小程序端,RN 端目前的支持情况不算特别理想。但充分理解多端差异、掌握正确的多端开发姿势(特别是样式管理方面,避免项目成型后再去兼容需要大动刀斧)之后,在简单的项目上是完全可以一展拳脚的。若说 2 个礼拜开发一个小程序,是稀疏平常的事,但 2 个礼拜即搞定了小程序端(微信、支付宝、百度等等),还搞定了 H5、React Native 端,后续更新也只要改一处地方,这产出、维护效率就实在太惊人了,这大抵也就是 “Write once, run anywhere” 的魅力所在(虽然在前端领域极容易发展成 “Write once, debug everywhere” ????)相信随着小程序热度不断上升,还会有更多优秀的开源框架、解决方案涌现。而我们不倾向于造轮子,更关注基于现有方案如何更好地去开发多端应用。若有兴趣的前端小伙伴,不妨加入我们,一起搞事 caiminxing#qudian.com ????本文由趣店 FED 出品,首发于趣店技术学院;项目开源地址 https://github.com/js-newbee/…。 ...

February 19, 2019 · 3 min · jiezi

React Native项目使用react-apollo实现更新缓存的两种方式

背景:举个例子:在显示动态的页面中删除某一条动态之后退出该页,当再进入该页之后这个被删除的动态是否还显示?显示! 为啥? cache!cache是为了增强用户体验,如果每一次进入一个页面都需要从网络获取数据,当数据量很大时却迟迟加载不出来,麻爪了吧…..但是现在cache的存在却给我们造成了很大的困扰:我虽然删除了这条动态,并配合使用react-native的state进行状态变化将这个动态在视觉上被删除掉了,但我没有重新获取数据,更新数据。当我从外界再次进入这个页面之后页面上显示的数据还是从cache中获取的数据。因此必须要更新cache!!在前面的博客中提到,GraphQL是一个API查询语言,他可以将使用PostgreSQL写的server代码自动生成Query或者Mutation,非常的方便。而Apollo Client就是一个强大的JavaScript GraphQL客户端。对于cache,在Apollo Client中有着强大的管理策略。在近阶段的使用过程中,我总结了两种管理缓存的办法:手动更新缓存自动更新缓存一:手动更新缓存在不断的搜索中我在文档中找到了他:https://www.apollographql.com…一个可以自定义访问,或者直接访问apollo缓存的指南看到这的时候我似乎有些明白了,人家都给你说的很明白了。你管理缓存的方式有两种一种是自定义,另一种是自动。fuck。武林秘籍都放在这,我却因为看不懂武林秘籍上的字迟迟不能升级????应用场景:如图一个消息隐藏的选择开关,当进行选择之后就会自动触发react-apollo的mutation操作,将这种变化传递到数据库,但是如果不更新缓存,当你退出本页面,再进来时就会发现消息隐藏的开关显示和原来还是一样的。因此需要进行缓存的更新。第一段代码:GraphQL定义mutationexport const UPDATE_PERSON_SETTING = gqlmutation updatePersonById($input: UpdatePersonByIdInput!) { updatePersonById(input: $input) { clientMutationId person { hideSpeaker } }}第二段代码: Mutation组件mutate操作(请先阅读官网相关部分之后再看)<Mutation mutation={UPDATE_PERSON_SETTING} variables={{ input: { id: currentPerson.id, personPatch: { hideSpeaker: true } } } } update={(cache, { data: { updatePersonById } }) => { this.updateCacheAfterSwitchHideSpeak(cache, updatePersonById.person.hideSpeaker) } } > {updatePersonById => ( <Switch value={currentPerson.hideSpeaker} onValueChange={value => { updatePersonById({ variables: { input: { id: currentPerson.id, personPatch: { hideSpeaker: value } } } }) }} /> )} </Mutation>分析:采用UPDATE_PERSON_SETTING这段GraphQL mutation操作在对开关进行更改,同时返回了更改后的数据hideSpeaker。在Mutation这个组件中第三个参数update是一个箭头函数,函数的第一个参数是cache,第二个参数data是用来更改缓存的数据。这个data就来自于第一段代码中mutation操作的返回值。在函数体中,调用用于更改缓存的函数updateCacheAfterSwitchHideSpeak,一并将cache和data传入其中。接下来分析一下第三段代码第三段代码:更新缓存函数updateCacheAfterSwitchHideSpeak = (cache, value) => { const data = cache.readQuery({ query: CURRENT_PERSON }) data.currentPerson.hideSpeaker = value cache.writeQuery({ query: CURRENT_PERSON, data }) }当接收到cache和value之后,输出一下cache,发现里面存在两个方法:readQuery,writeQuery,这两个方法就是我们用来进行读取缓存和更改缓存的办法。注意:这里的query参数必须要和渲染这个组件时所获取数据的query来源是一致的。也就是说,必须是同一个GraphQL API。如果存在variables,那么variables也必须是一样的。结合实际情况:在进入这个设置页面时,通过调用一个GraphQL 查询API并且将hideSpeaker查询出来,渲染出页面。而查询的结果也就形成了一个缓存。一个项目中有很多的查询,有些页面使用同一个GraphQL API进行查询,但是他们的condition却不同这就会造成cache的不同,因此在查询过程中如果存在variables,就必须进行严格的限制,确保从cache中readQuery出来的data就是你梦寐以求的那个Ta -_-。下面是一段带有variables的readQuery代码。 const variables = { personPostCondition: { personId: personId }, likeCondition: { personId: personId }, orderBy: ‘CREATED_AT_DESC’ } const data = cache.readQuery({ variables, query: PERSON_DYNAMICS })在读取完cache中的data之后,你可以输出一下,看是不是当时你在渲染页面时query的数据,但是此时消息隐藏已经进行了调整,相应的hideSpeaker却还是false,此时单独将这个属性拿出来进行调整:data.currentPerson.hideSpeaker = value 在修改完后再使用writeQuery将新的cache写进去。此时就完成了cache的更改。query和readQuery的区别query的数据查询来源有两个:1:服务器2:缓存readQuery的数据查询来源只有一个:1:缓存如果缓存中不存在,他就会报错,因此使用这个方法的前提就是已经使用了query将数据从服务器获取到了。当然据官网上的描述,手动更改缓存的方式还有几种,但是目前还没有仔细的看过。日后再进行解释说明。二:自动更新缓存与手动更新相对的自然就是自动更新了。既然有自动更新功能,他肯定是借助了什么逆天的“工具”!apollo-cache-inmemory 这一点在官网也已经有了详细的说明了:安排的明明白白的了,在Apollo Client 2.0中apollo-cache-inmemory 他是默认实现的。因此,只要使用了Apollo Client 2.0 在npm时他是会进行相应的自动安装的。缓存的标准化管理是实现自动更新缓存的前提!!!inmemory是一个规范化的数据存储,他是咋规范化的呢???在query到数据之后,InMemoryCache对查询来的数据进行分割成单个的对象并保存。而且为这些单个的对象都设置唯一的标识符,如果在query数据时将那些可以作为唯一标识符的字段例如id也一并获取到了,那么就将这个id作为分割后对象的唯一标识符。上面这个简单的例子说明,如果id相同,那么score在缓存中的数据也会自动进行更新。因此结合我们之前的实例做一个简单的更改:export const UPDATE_PERSON_SETTING = gqlmutation updatePersonById($input: UpdatePersonByIdInput!) { updatePersonById(input: $input) { clientMutationId person { id hideSpeaker } }}我们在进行mutation之后的返回值中存在id,这就符合上面的要求。他就会自动进行缓存的更新。如果你还心存疑虑,你大可在readQuery后将data输出一下,此时你就会发现 hideSpeaker已经更改成目前的状态true。这就是自动更新的快捷之处。此时你就不必使用readQuery和writeQuery这种费时费力的方法了。三:最后的话1:不论是自动更新还是手动更新,都必须将更改之后的数据返回出来,就像hideSpeaker,更改他之后,你必须将它返回出来。这不论是在自动更新还是在手动更新上都是有必要的。2:关于两者的选择使用,毫无疑问,通常情况下使用自动更新,特殊情况下使用手动更新,在明白原理后,有时你可以使用手动更新进行一些投机取巧的更新缓存的操作。3:难,都难。爬,一起爬。 ...

February 19, 2019 · 1 min · jiezi

ES6 类继承 和 super的使用

ES6继承 详细类容参考:http://es6.ruanyifeng.com/#do…1、super()使用class A { construcor(a, b) { this.a = a; this.b = b; }}class B extends A { constructor(x, y, z) { super(x, y); this.z = z; console.log(x, ‘—’, y, ‘—-’, z, ‘—-’); }}let b = new B(1, 2, 8);// 1 “—” 2 “—-” 8 “—-“注意:ES6中继承的子类中,如果使用构造函数constructor()那么就必须使用 super()方法初始化,这样下面才可以调用this关键字。super()只能用在子类的构造函数之中,用在其他地方就会报错。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。 —阮一峰2、父类中的静态方法,子类中可以通过类名直接调用class A2 { static hello() { console.log(“hello world”); }}class B2 extends A2 { constructor() { super(); }}B2.hello();// hello world3、Object.getPrototypeOf()判断子类继承的父类Object.getPrototypeOf(B2);// A2

February 12, 2019 · 1 min · jiezi

解决由于export,import错误导致的元素类型无效【Element type is invalid】

前些日子做项目时有一个报错,虽然解决了,但是对于导致的原因,还是一知半解。今天突然发现一篇博客,大受启发,决定将这个问题系统的总结一下。报错信息:提示元素类型无效,可能是忘记从你定义的文件中导出来组件,或者是你弄混了要导入的组件的默认名字,没有和你导入时的名字相对应。解决方式:1:在导出文件中使用export class 组件类名称 extends Component 将组件导出。此时可以在要导入的地方使用 import {组件类名称} from “路径” 进行导入使用。2:在导出文件类中,使用class 组件类名称 extends Component 对组件类进行定义,并在定义之后使用 export default 组件类名称的方式将组件进行导出。此时可以在要导入的地方使用 import 组件类名称 from “路径” 方式将需要的组件类导入进来。原因分析:在初识react-native时就在想为什么每个文件最上面的导入外部组件的方式会有不同import { View, Text } from “react-native"import TestCompontent from “../../TestCompontent"为什么一个带大括号,另一个就不带大括号呢???第一种方式:带大括号的表示在导出文件中使用的是export class 组件类名称 extends Component 而我们常见的带大括号引入的组件,都是从一些第三方库中引入的组件文件,例如从react-native中引入的View,Text组件。这些都是固定的组件名字,他是需要查看文档,我想引入一个View,就必须知道在这个第三方库中存在View这个第三方组件,况且一个第三方库中有许许多多的组件,因此必须知道确切的名称才能进行导入,而不能自己随意的起名。第二种方式:不带大括号是使用 export default 组件类名称的方式将组件类进行导出。此时一般都是一些自定义的组件,况且在自定义组件中一般一个文件只有一个组件,因此只要import 的来源是正确的,就可以将组件类的名称进行自定义取名 import Test from “../../TestCompontent"总结:1:其实上述两种方式的区别主要就是export class 和export default class 的区别。使用export default的方式将组件导出时就可以将组件类的名称进行自定义。如果使用export的方式进行自定义,那么就必须按照定义组件类时的名称进行导入。2:有帮助的博客链接:https://segmentfault.com/a/11…

February 5, 2019 · 1 min · jiezi

ES6 export 和 export default的区别

ES6中 export 和 export default 与 import使用的区别,使用 react native 代码详解一、使用export 和 import1、export 定义导出一个子组件 Greetingimport React, { Component } from “react”;import { View, Text } from “react-native”;export class Greeting extends Component { render() { return( <View> <Text>{this.props.name}</Text> <View> ) }}2、在父组件中导入子组件import React, { Component } from “react”;import { View, Text } from “react-native”;// greeting文件存储在src目录下import { Greeting } from “./src/greeting”;import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(greeting.js)对外接口的名称Greeting相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名import { bieming as Greeting } from “./src/greeting”;3、export default 场景:从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名,否则无法加载。但是用户肯定不愿意去阅读子组件看看导出名称叫啥,然后回来导入,所以就有了 export default。import React, { Component } from “react”;import { View, Text } from “react-native”;export default class Greeting extends Component { render() { return( <View> <Text>{this.props.name}</Text> <View> ) }}4、import 导入模块import React, { Component } from “react”;import { View, Text } from “react-native”;// greeting文件存储在src目录下import Greeting from “./src/greeting”;// 或者import AnyName from “./src/greeting”;上面代码的import命令,可以用任意名称指向greeting.js输出的方法,这时就不需要知道原模块输出的变量名。需要注意的是,这时import命令后面,不使用大括号。总结:现在流行的前端框架,angular+ 主要使用 export 导出模块,react native 中使用 export default 导出模块,如今编辑器非常强大,安装插件会自动弹出模块名称,知道其导出怎么使用就可以了 ...

January 30, 2019 · 1 min · jiezi

App常用开发模式

App常用开发模式Native App传统的原生App开发模式,有iOS和安卓两大系统,需要各自语言开发各自App。优点:性能和体验都是最好。缺点:开发和发布成本高, 维持多个版本的更新升级比较麻烦,用户的安装门槛也高WebApp移动端的网站, h5开发和发布成本最低, 性能和体验较差,受到浏览器处理能力的限制Hybrid App混合模式移动应用,介于Web App、Native App这两之间App开发技术, 兼具“Native App良好交互体验的优势”和“Web App跨平台开发的优势”原理: 由Native通过JSBridge等方法提供统一的API,用html,css实现界面,JS写逻辑调用API,最终页面在Webview中显示,这种模式下,Android、iOS的API一般有一致性,HybridApp所以有跨平台效果开发者可以像开发WebApp一样开发app的视觉UI,当需要使用原生功能(如摄像头,陀螺仪等功能)时,只需要调用官方的API就可以实现Native的效果。至于JS和Native的通信,常用的有URL监听和Hybrid厂商使用的JSBridge通信,两者原理相近。Hybird App 的常见跨平台开发工具有PhoneGap,Ionic, 国内有AppCan缺点:Hybird严重受限于WebView的解析渲染效率,需要原生配合。JSBridge调用方式React Native AppFacebook发现Hybrid存在很多缺陷和不足,然后自己开发了一套RN, 使用JSX写原生界面,js通过JSBridge调用原生API渲染UI交互通信。支持flexBox布局, 采用DOM 结构;优点:效率体验接近Native App,发布和开发成本低于Native App。缺点:新东西,更新迭代快,api后期同早期变化很大,需要踩坑。。Weex App阿里开发团队在RN的成功案例上,设计的一套开发模式,2016年4月正式开源,并在v2.0版本官方支持Vue.js优点:单页开发模式效率极高,热更新发包体积小,并且跨平台性强。缺点:同RN, 且社区没有RN活跃,已捐献给 Apache 基金会孵化管理。。。奔溃。。。

January 28, 2019 · 1 min · jiezi

React Native 0.58 正式版发布

原文地址:https://github.com/react-nati…本文由简书作者凌宇之蓝翻译,因本人水平有限,难免翻译有误,还望各位见谅。[0.58.0]欢迎阅读2019年1月发布的React Native。此版本有许多重大变化,我们特别提请您注意:核心组件的流程类型的现代化和加强中断对ScrollView,CameraRollView和SwipeableRow的更改,使其在某些方法中不再绑定到组件实例支持WebKit中的相互TLS从/ assets之外的目录提供的资产针对意外行为的大量崩溃修复和解决方案感谢那些对我们的发布候选人提供反馈的人。如果您有兴趣帮助评估我们的下一个版本,请在此处查看我们的跟踪问题。新增添加对publicPath的支持以启用来自不同位置的静态资产(0b31496 by @gdborton)Android现在可以使用Android系统属性设置Bundler服务器主机,以便在多个应用程序或应用程序安装中更轻松地进行调试adb shell setprop metro.host(@stepanhruda的e02a154)Native Modules现在可以使用额外的属性(userInfo)附加WritableMap arg来拒绝承诺。请参阅Promise.java中定义的接口以获取可用的方法。这可以在JavaScript中以Error.userInfo形式访问。这是为了匹配iOS现有的Error.userInfo行为。有关示例,请参阅PR。(@Salakar#20940)Native Modules现在将nativeStackAndroid属性暴露给使用Exception / Throwable拒绝的promise - 使Javascript内的本机错误堆栈可用:Error.nativeStackAndroid。这是为了匹配iOS现有的Error.nativeStackIOS支持。有关示例,请参阅PR。(@Salakar#20940)IOS将moduleForName:lazilyLoadIfNecessary添加到RCTBridge.h以按名称查找模块并强制加载它们,以及对@dhahidehpour,@ fkgozali和@mmmulani进行的LazyLoading的各种改进当使用WebKit = {true}进行相互TLS身份验证时,将WebView的功能添加到setClientAuthenticationCredential(8911353 by @mjhu)Changed核心组件的Flow类型的主要改进许多公共组件都转换为ES6类Flow依赖现在为v0.86.0metro依赖现在是v0.49.1jest依赖现在是v24.0.0-alpha.6fbjs-scripts依赖现在是v1.0.0(#21880)folly的依赖现在是v2018.10.22.00React sync for revisions热重新加载时清理的错误消息允许CxxModules实现需要两次回调的函数突破性变化转换为ES6类的组件的公共方法不再绑定到其组件实例。对于ScrollView,受影响的方法是setNativeProps,getScrollResponder,getScrollableNode,getInnerViewNode,scrollTo,scrollToEnd,scrollWithoutAnimationTo和flashScrollIndicators。对于CameraRollView,受影响的方法是:rendererChanged。对于SwipeableRow,受影响的方法是:close。因此,通过引用将这些方法作为回调传递给函数已不再安全。组件实例的自动绑定方法是createReactClass的一种行为,我们决定在切换到ES6类时不保留这种行为。Android优化PlatformConstants.ServerHost,PlatformConstants.isTesting和PlatformConstants.androidID以获得性能IOS禁止关于本地模块缺少导出的黄色框移除移除 UIManager.measureViewsInRect()修复bug修复Yoga JNI绑定中潜在的UI线程停顿方案修复因桥接cxx模块注册表周围的竞争条件而发生崩溃的问题修复视图和文本的displayName;显示特定名称而不是通用“组件”修复react-native init –help,使其不返回undefined修复react-native –sourceExts修复当可见道具未定义或为空时意外显示模态修复VirtualizedList分页期间的崩溃修复使用远程调试和Delta捆绑包删除模块可能导致堆栈跟踪不正确的情况Android具体修复bug:删除根节点时修复崩溃修复各种ReactInstanceManager死锁和竞争条件解除ReactModalHostView和DialogManager时修复IllegalArgumentException使用Android Gradle Plugin 3.2修复不正确的合并资产路径在onoutout回调时修复HTTP连接当远程服务器启动关闭时,修复websocket正确关闭修复Android 16设备的兼容性问题修复了在加载源时不遵守Image.resizeMode的问题,从而导致意外填充修复Android 28的倒置ScrollView,使动量处于正确的方向IOS具体修复bug:修复内联视图内容未被重新传输的情况修复使用前置摄像头时ImagePickerIOS图像不一致的问题修复竞争条件并在关闭iOS 11及更早版本的JSC时崩溃修复NetInfo的_firstTimeReachability中的崩溃修复内联视图可见的情况,即使它应该被截断使用与内容偏移相关的ScrollView修复崩溃我的网站:https://wayne214.github.io

January 25, 2019 · 1 min · jiezi

移动跨平台技术方案总结

“得移动端者得天下”,移动端取代PC端,成为了互联网行业最大的流量分发入口,因此不少公司制定了“移动优先”的发展策略。为了帮助读者更好地学习WEEX,本节将对React Native、Weex和Flutter等主流的跨平台方案进行简单的介绍和对比。React NativeReact Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的React框架在原生移动应用平台的衍生产物,目前主要支持iOS和安卓两大平台。RN使用Javascript语言来开发移动应用,但UI渲染、网络请求等均由原生端实现。具体来说,开发者编写的Javascript代码,通过中间层转化为原生控件后再执行,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域,并可以在不牺牲用户体验的前提下提高开发效率。作为一个跨平台技术框架,RN从上到下可以分为Javascript层、C++层和Native层。其中,C++层主要用于实现动态连结库(.so),作为中间适配层桥接,实现js端与原生端的双向通信交互,如下图所示是RN在Android平台上的通信原理图。在RN的三层架构中,最核心的就是中间的C++层,C++层最核心的功能就是封装JavaScriptCore,用于执行对js的解析。同时,原生端提供的各种Native Module(如网络请求,ViewGroup控件模块)和JS端提供的各种JS Module(如JS EventEmiter模块)都会在C++实现的so文件中保存起来,最终通过C++层中的保存的映射实现两端的交互。在RN开发过程中,大多数情况下开发人员并不需要需要了解RN框架的具体细节,只需要专注JS端的逻辑代码实现即可。但是需要注意的是,由于js代码是运行在独立的JS线程中,所以在js中不能处理耗时的操作,如fetch、图片加载和数据持久化等操作。最终,JS代码会被打包成一个bundle文件并自动添加到应用程序的资源目录下,而应用程序最终加载的也是打包后的bundle文件。RN的打包脚本位于“/node_modules/react-native/local-cli”目录下,打包后通过metro模块压缩成bundle文件,而bundle文件只包含打包js的代码,并不包含图片、多媒体等静态资源,而打包后的静态资源会是被拷贝到对应的平台资源文件夹中。总的来说,RN使用Javascript来编写应用程序,然后调用原生组件执行页面渲染操作,在提高了开发效率的同时又保留了Native的用户体验。并且,伴随着Facebook重构RN工作的完成,RN也将变得更快、更轻量、性能更好。Weex作为一套前端跨平台技术框架,Weex建立了一套源码转换以及Native与Js通信的机制。Weex表面上是一个客户端框架,但实际上它串联起了从本地开发、云端部署到分发的整个链路。具体来说,在开发阶段编写一个.we文件,然后使用Weex提供的weex-toolkit转换工具将.we文件转换为JS bundle,并将生成的JS bundle上传部署到云端,最后通过网络请求或预下发的方式加载至用户的移动应用客户端。当集成了Weex SDK的客户端接收到JS bundle文件后,调用本地的JavaScript引擎执行环境执行相应的JS bundle,并将执行过程中产生的各种命令发送到native端进行界面渲染、数据存储、网络通信以及用户交互响应。由上图可知,Weex框架中最核心的部分就是JavaScript Runtime。具体来说,当需要执行渲染操作时,在iOS环境下选择基于JavaScriptCore内核的iOS系统提供的JSContext,在Android环境下使用基于JavaScriptCore内核的JavaScript引擎。当JS bundle从服务器下载完成之后,Weex的Android、iOS和H5会运行一个JavaScript引擎来执行JS bundle,同时向各终端的渲染层发送渲染指令,并调度客户端的渲染引擎实现视图渲染、事件绑定和处理用户交互等操作。由于Android、iOS和H5等终端最终使用的是native渲染引擎,也就是说使用同一套代码在不同终端上展示的样式是相同的,并且Weex使用native引擎渲染的是native组件,所以在性能上比传统的WebView方案要好很多。当然,尽管Weex已经提供了开发者所需要的最常用的组件和模块,但面对丰富多样的移动应用研发需求,这些常用基础组件还是远远不能满足开发的需要,因此Weex提供了灵活自由的扩展能力,开发者可以根据自身的情况定制属于自己客户端的组件和模块,从而丰富Weex生态。FlutterFlutter是Google开源的移动跨平台框架,其历史最早可以追溯到2015年的Sky项目,该项目可以同时运行在Android、iOS和fuchsia等包含Dart虚拟机的平台上,并且性能无限接近原生。相较于RN和Weex使用Javascript作为编程语言与使用平台自身引擎渲染界面不同,Flutter直接选择2D绘图引擎库skia来渲染界面。如上图所示,Flutter框架主要由Framework和Engine层组成,而我们基于Framework开发App最终会运行在Engine上。其中,Engine是Flutter提供的独立虚拟机,正是由于它的存在Flutter程序才能运行在不同的平台上,实现跨平台运行的能力。与RN和Weex使用原生控件渲染界面不同,Flutter并不需要使用原生控件来渲染界面,而是使用Engine来绘制Widget(Flutter显示单元),并且Dart代码会通过AOT编译为平台的原生代码,实现与平台的直接通信,不需要JS引擎的桥接,也不需要原生平台的Dalvik虚拟机,如图1-5所示。同时,Flutter的Widget采用现代响应式框架来构建,而Widget是不可变的,仅支持一帧,并且每一帧上的内容不能直接更新,需要通过Widget的状态来间接更新。在Flutter中,无状态和有状态Widget的核心特性是相同的,视图的每一帧Flutter都会重新构建,通过State对象Flutter就可以跨帧存储状态数据并恢复它。总的来说,Flutter是目前跨平台开发中最好的方案,它以一套代码即可生成Android和iOS平台两种应用,很大程度上减少了App开发和维护的成本,同时Dart语言强大的性能表现和丰富的特性,也使得跨平台开发变得更加便利。而不足的是,Flutter还处于Alpha阶段,许多功能还不是特别完善,而全新的Dart语言也带来了学习上的成本,如果想要完全替代Android和iOS开发还有比较长的路要走。PWAPWA,全称Progressive Web App,是Google在2015年提出渐进式的网页技术。PWA结合了一系列的现代Web技术,并使用多种技术来增强Web App的功能,最终可以让网页应用呈现和原生应用相似的体验。相比于传统的网页技术,渐进式Web技术是可以横跨Web技术及Native APP开发的技术解决方案,具有可靠、快速且可参与等诸多特点。具体来说,当用户从手机主屏幕启动时,不用考虑网络的状态就可以立刻加载出PWA。并且,相比传统的网页加载速度,PWA的加载速度是非常快的,因为PWA使用了Service Worker 等先进技术。除此之外,PWA还可以被添加在用户的主屏幕上,不用从应用商店进行下载即可通过网络应用程序Manifest file提供类似于APP的使用体验。作为一种全新Web技术方案,PWA的正常工作需要一些重要的技术组件,它们协同工作并为传统的Web应用程序注入活力,如图1-8所示。其中,Service Worker表示离线缓存文件,其本质是Web应用程序与浏览器之间的代理服务器,可以在网络可用时作为浏览器和网络间的代理,也可以在离线或者网络极差的环境下使用离线的缓冲文件。Manifest则是W3C一个技术规范,它定义了基于JSON的清单,为开发人员提供一个放置与Web应用程序关联的元数据的集中地点。Manifest是PWA 开发中的重要一环,它为开发人员控制应用程序提供了可能。目前,渐进式Web应用还处于起步阶段,使用的厂商也是诸如Twitter、淘宝、微博等大平台。不过,PWA作为Google主推的一项技术标准,Edge、Safari和FireFox等主流浏览器也都开始支持渐进式Web应用。因此,可以预见的是,PWA必将成为继移动之后的又一革命性技术方案。对比在当前诸多的跨平台方案中,RN、Weex和Flutter无疑是最优秀的。而从不同的细节来看,三大跨平台框架又有各自的优点和缺点,可以通过表1-1来查看。对比类型React NativeWeexFlutter支持平台Android/IOSAndroid/IOS/WebAndroid/IOS实现技术JavaScriptJavaScript原生编码/渲染引擎JS V8JSCoreFlutter Engine编程语言ReactVueDartbundle包大小单一、较大较小、多页面不需要框架程度较重较轻重社区活跃、FB维护不活跃活跃如上表所示,RN、Weex采用的技术方案大体相同,它们都使用JavaScript作为编程语言,然后通过中间层转换为原生的组件后再利用Native渲染引擎执行渲染操作。而Flutter直接使用skia来渲染视图,而Flutter Widget则使用现代响应式框架来构建,和平台没有直接的关系。就目前跨平台技术来看,JavaScript在跨平台开发中可谓占据半壁江山,大有“一统天下”的趋势。从性能方面来说,Flutter的性能理论上是最好的,RN和Weex次之,并且都好于传统的WebView方案。但从目前的实际应用来看却并没有太大的差距,特别是和0.5.0版本以上的RN对比性能体验上差异并不明显。而从社群和社区的活跃来看,RN和Flutter无疑是最活跃的,RN经过4年多的发展已经成长为跨平台开发的实际领导者,并拥有各类丰富的第三方库和开发群体。Flutter作为最近才火起来的跨平台技术方案,不过目前还处在beta阶段,商用的实例也很少,不过应该看到google的号召力一直是很强,未来究竟如何发展让我们拭目以待。示例eros-yanxuan简介eros-yanxuan 是基于 eros 开发的Weex项目,部分页面参考了项目网易严选 weex 版本,欢迎star或fork。eros 文档eros github运行确保你本地已经集成了 eros 开发所需的环境。clone 项目到本地:$ git clone https://github.com/xiangzhihong/eros-yanxuan.git进入目录,下载前端所需的依赖:$ cd eros-yanxuan$ npm installiOS SDK打开platforms目录下的WeexEros项目,在WeexEros中使用pod添加依赖。$ cd platforms/ios/WeexEros$ pod update // 下载 iOS 依赖$ open WeexEros.xcworkspace // 自动打开项目选中模拟器,点击绿色箭头运行 app 即可。Android对于Android工程来说,使用Android Studio打开platforms目录下的WeexFrameworkWrapper的Android工程,然后使用install.sh安装Android工程的需要依赖包nexus和wxframework。具体可以参考自行导入项目,便可运行起来。运行项目根目录下运行 eros dev关闭调试,拦截器,打开热更新重新 build app效果Question运行过程中出现问题在以下地址解决方法,如果没有找到,请加群:515980159eros issueeros Q&A

January 22, 2019 · 1 min · jiezi

React Native工程中TSLint静态检查工具的探索之路

背景建立的代码规范没人遵守,项目中遍地风格迥异的代码,你会不会抓狂?通过测试用例的程序还会出现Bug,而原因仅仅是自己犯下的低级错误,你会不会抓狂?某种代码写法存在问题导致崩溃时,只能全工程检查代码,这需要人工花费大量时间Review代码,你会不会抓狂?以上这些问题,可以通过静态检查有效地缓解!静态检查(Static Program Analysis)主要是以不运行程序的方式对于程序源代码进行检查分析的技术,而与之相反的就是动态检查(Dynamic Program Analysis),通过实际运行程序输入测试数据产生预期结果的技术。通过代码静态检查,我们可以快速定位代码的错误与缺陷,可以减少逐行阅读代码浪费的时间,可以(根据需要)快速扫描代码中可能存在的漏洞等。代码静态检查可以在代码的规范性、安全性、可靠性、可维护性等方面起到重要作用。在客户端中,Android可以使用CheckStyle、Lint、Findbugs、PMD等工具,iOS可以使用Clang Static Analyzer、OCLint等工具。而在React Native的开发过程中,针对于JavaScript的ESLint,与TypeScript的TSLint,则成为了主要代码静态检查的工具。本文将按照使用TSLint的原因、使用TSLint的方法、自定义TSLint的步骤进行探究分析。一、使用TSLint的原因在客户端团队进入React Native项目的开发过程中,面临着如下问题:由于大家从客户端转入到React Native开发过程中,容易出现低级语法错误;开发者之前从事Android、iOS、前端等工作,因此代码风格不同,导致项目代码风格不统一;客户端效果不一致,有可能Android端显示正常、iOS端显示异常,或者相反的情况出现。虽然以上问题可以通过多次不断将雷点标记出,并不断地分享经验与强化代码Review过程等方式来进行缓解,但是仍面临着React Native开发者掌握的技术水平千差万别,知识分享传播的速度缓慢等问题,既导致了开发成本的不断增加和开发效率持续低下的问题,还难以避免一个坑被踩了多次的情况出现。这时急需一款可以满足以下目标的工具:可检测代码低级语法错误;规范项目代码风格;根据需要可自定义检查代码的逻辑;工具使用者可以“傻瓜式”的接入部署到开发IDE环境;可以快速高效地将检查工具最新检查逻辑同步到开发IDE环境中;对于检查出的问题可以快速定位。根据上述要求的描述,静态检查工具TSLint可以较为有效地达成目标。二、TSLint介绍TSLint是硅谷企业Palantir的一个项目,它是一款可以检查TypeScript代码可读性、可维护性以及功能性错误的静态检查工具,当前许多编辑器(Editors)和构建系统(Build Systems)支持这一工具,同时支持自定义编写Lint规则、配置、格式化等。当前TSLint已经包含了上百条规则,这些规则构筑了当前TSLint检查的基础。在代码开发阶段中,通过这些配置好的规则可以给工程一个完整的检查,并随时可以提示出可能存在的问题。本文内容参考了TSLint官方文档https://palantir.github.io/tslint/。2.1 TSLint常见规则以下规则主要来源于TSLint规则,是某些规则的简单介绍。2.2 常用TSLint规则包上述2.1所列出的规则来源于Palantir官方TSLint规则。实际还有多种,可能会用到的有以下:我们在项目的规则配置过程中,一般采用上述规则包其中一种或者若干种同时配置,那如何配置呢?请看下文。三、如何进行TSLint规则配置与检查首先,在工程package.json文件中配置TSLint包:在根目录中的tslint.json文件中可以根据需要配置已有规则,例如:其中extends数组内放置继承的TSLint规则包,上图包括了airbnb配置的规则包、tslint-react的规则包,而rules用于配置规则的开关。TSLint规则目前只有true和false的选项,这导致了结果要么正常,要么报错ERROR,而不会出现WARNING等警告。有些时候,虽然配置某些规则开启,但是某个文件内可能会关闭某些甚至全部规则检查,这时候可以通过规则注释来配置,如:/* tslint:disable /上述注释表示本文件自此注释所在行开始,以下的所有区域关闭TSLint规则检查。/ tslint:enable /上述注释表示本文件自此注释所在行开始,以下的所有区域开启TSLint规则检查。/ tslint:disable:rule1 rule2 rule3… /上述注释表示本文件自此注释所在行开始,以下的所有区域关闭规则rule1 rule2 rule3…的检查。/ tslint:enable:rule1 rule2 rule3… /上述注释表示本文件自此注释所在行开始,以下的所有区域开启规则rule1 rule2 rule3…的检查。// tslint:disable-next-line上述注释表示此注释所在行的下一行关闭TSLint规则检查。someCode(); // tslint:disable-line上述注释表示此注释所在行关闭TSLint规则检查。// tslint:disable-next-line:rule1 rule2 rule3…上述注释表示此注释所在行的下一行关闭规则rule1 rule2 rule3…的检查检查。以上配置信息,这里具体参考了https://palantir.github.io/tslint/usage/rule-flags/。3.1 本地检查在完成工程配置后,需要下载所需要依赖包,要在工程所在根目录使用npm install命令完成下载依赖包。IDE环境提示在完成下载依赖包后,IDE环境可以根据对应配置文件进行提示,可以实时地提示出存在问题代码的错误信息,以VSCode为例:本地命令检查VSCode目前还有继续完善的空间,如果部分文件未在窗口打开的情况下,可能存在其中错误未提示出的情况,这时候,我们可以通过本地命令进行全工程的检查,在React Native工程的根目录下,通过以下命令行执行:tslint –project tsconfig.json –config tslint.json(此命令如果不正确运行,可在之前加入./node_modules/.bin/)即为:./node_modules/.bin/tslint –project tsconfig.json –config tslint.json从而会提示出类似以下错误的信息:src/Components/test.ts[1, 7]: Class name must be in pascal case3.2 在线CI检查本地进行代码检查的过程也会存在被人遗忘的可能性,通过技术的保障,可以避免人为遗忘,作为代码提交的标准流程,通过CI检查后再合并代码,可以有效避免代码错误的问题。CI系统可以为理解为一个云端的环境,环境配置与本地一致,在这种情况下,可以生成与本地一致的报告,在美团内部可以使用基于Jenkins的Castle CI系统, 生成结果与本地结果一致:3.3 其他方式代码检查不止局限上述阶段,在代码commit、pull request、打包等阶段均可触发。代码commit阶段,通过Hook方式可以触发代码检查,可以有效地将在线CI检查阶段强制提前,基本保证了在线CI检查的完全正确性。代码pull request阶段,通过在线CI检查可以触发代码检查,可以有效保证合入分支尤其是主分支的正确性。代码打包阶段,通过在线CI检查可以触发代码检查,可以有效保证打包代码的正确性。四、自定义编写TSLint规则4.1 为什么要自定义TSLint规则当前的TSLint规则虽然涵盖了比较普遍问题的一些代码检查,但是实践中还是存在一些问题的:团队中的个性化需求难以满足。例如,saga中的异步函数需要在最外层加try-catch,且catch块中需要加异常上报,这个明显在官方的TSLint规则无法实现,为此需要自定义的开发。官方规则的开启与配置不符合当前团队情况。基于以上原因其他团队也有自定义TSLint的先例,例如上文提到的tslint-microsoft-contrib、tslint-eslint-rules等。4.2 自定义规则步骤那自定义TSLint大概需要什么步骤呢,首先规则文件根据规范进行按部就班的编写规则信息,然后根据代码检查逻辑对语法树进行分析并编写逻辑代码,这也是自定义规则的核心部分了,最后就是自定义规则的使用了。自定义规则的示例直接参考官方的规则是最直接的,我们能这里参考一个比较简单的规则"class-name"。“class-name"规则上文已经提到,它的意思是对类命名进行规范,当团队中类相关的命名不规范,会导致项目代码风格不统一甚至其他出现的问题,而"class-name"规则可以有效解决这个问题。我们可以看下具体的源码文件:https://github.com/palantir/tslint/blob/master/src/rules/classNameRule.ts。然后将分步对此自定义规则进行讲解。第一步,文件命名规则命名必须是符合以下2个规则:驼峰命名。以’Rule’为后缀。第二步,类命名规则的类名是Rule,并且要继承Lint.Rules.AbstractRule这个类型,当然也可能有继承TypedRule这个类的时候,但是我们通过阅读源码发现,其实它也是继承自Lint.Rules.AbstractRule这个类。第三步,填写metadata信息metadata包含了配置参数,定义了规则的信息以及配置规则的定义。ruleName 是规则名,使用烤串命名法,一般是将类名转为烤串命名格式。description 一个简短的规则说明。descriptionDetails 详细的规则说明。rationale 理论基础。options 配置参数形式,如果没有可以配置为null。optionExamples 参数范例 ,如没有参数无需配置。typescriptOnly true/false 是否只适用于TypeScript。hasFix true/false 是否带有修复方式。requiresTypeInfo 是否需要类型信息。optionsDescrition options的介绍。type 规则的类型。规则类型有四种,分别为:“functionality”、“maintainability”、“style”、“typescript”。functionality : 针对于语句问题以及功能问题。maintainability:主要以代码简洁、可读、可维护为目标的规则。style:以维护代码风格基本统一的规则。typescript:针对于TypeScript进行提示。第四步,定义错误提示信息这个主要是在检查出问题的时候进行提示的文字,并不局限于使用一个静态变量的形式,但是大部分官方规则都是这么编写,这里对此进行介绍,防止引起歧义。第五步,实现apply方法apply主要是进行静态检查的核心方法,通过返回applyWithFunction方法或者返回applyWithWalker来进行代码检查,其实applyWithFunction方法与applyWithWalker方法的主要区别在于applyWithWalker可以通过IWalker实现一个自定义的IWaker类,区别如下:其中实现IWaker的抽象类AbstractWalker里面也继承了WalkContext,而这个WalkContext就是上面提到的applyWithFunction的内部实现类。第六步,语法树解析无论是applyWithFunction方法还是applyWithWalker方法中的IWaker实现都传入了sourceFile这个参数,这个相当于文件的根节点,然后通过ts.forEachChild方法遍历整个语法树节点。这里有两个查看AST语法树的工具:AST Explorer:https://astexplorer.net/ 对应源码:https://github.com/fkling/astexplorerTypeScript AST Viewer:https://ts-ast-viewer.com/ 对应源码:https://github.com/dsherret/ts-ast-viewerAST Explorer优点:在AST Explorer可以高亮显示所选中代码对应的AST语法树信息。缺点:不能选择对应版本的解析器,导致显示的语法树代码版本固定。语法树显示的信息相对较少。TypeScript AST Viewer优点:解析器对应版本可以动态选择:语法树显示的信息不仅显示对应的数字代码,还可为对应的实际信息:每个版本对应对kind信息数值可能会变动,但是对应的枚举名字是固定的,如下图:从而这个工具可以避免频繁根据其数值查找对应信息。缺点: 不能高亮显示代码对应的AST语法树区域,定位效率较低。综上,通过同时使用上述两个工具定位分析,可以有效地提高分析效率。第七步,检查规则代码编写通过ts.forEachChild方法对于语法树所有的节点进行遍历,在遍历的方法里可以实现自己的逻辑,其中节点的类为ts.Node:其中kind为当前节点的类型,当然Node是所有节点的基类,它的实现还包括Statement、Expression、Declaration等,回到开头这个"class-name"规则,我们的所有声明类主要是class与interface关键字,分别对应ClassExpression、ClassDeclaration、InterfaceDeclaration,我们可以通过上步提到的AST语法树工具,在语法树中看到其为一一对应的。在规则代码中主要通过isClassLikeDeclaration、isInterfaceDeclaration这两个方法进行判断的。其中isClassLikeDeclaration、isInterfaceDeclaration对应的方法我们可以在node.js文件中找到:判断是对应的类型时,调用addFailureAtNode方法把错误信息和节点传入,当然还可以调用addFailureAt、addFailure方法。最终这个规则编写结束了,有一点再次强调下,因为每个版本所对应的类型代码可能不相同,当判断kind的时候,一定不要直接使用各个类型对应的数字。第八步,规则配置使用完成规则代码后,是ts后缀的文件,而ts规则文件实际还是要用js文件,这时候我们需要用命令将ts转化为js文件:tsc ./src/.ts –outDir dist将ts规则生成到dist文件夹(这个文件夹命名用户自定),然后在tslint.json文件中配置生成的规则文件即可。之后在项目的根目录里面,使用以下命令既可进行检查:tslint –project tsconfig.json –config tslint.json同时为了未来新增规则以及规则配置的更好的操作性,建议可以封装到自己的规则包,以便与规则的管理与传播。总结TSLint的优点:速度快。相对于动态代码检查,检查速度较快,现有项目无论是在本地检查,还是在CI检查,对于由十余个页面组成的React Native工程,可以在1到2分钟内完成;灵活。通过配置规则,可以有效地避免常见代码错误与潜在的Bug;易扩展。通过编写配置自定义规则,可以及时准确快速查找出代码中特定风险点。TSLint缺点:规则的结果只有对与错两种等级结果,没有警告等级的的提示结果;无法直接报告规则报错数量,只能依赖其他手段统计;TSLint规则针对于当前单一文件可以有效地通过语法树进行分析判定,但对于引用到的其他文件中的变量、类、方法等,则难以通过AST语法树进行判定。使用结果及分析在美团,有十余个页面的单个工程首次接入TSLint后,检查出的问题有近百条。但是由于开启的规则不同,配置规则包的差异,检查后的数量可能为几十条到几千条甚至更多。现在已开发十余条自定义规则,在单个工程内,处理优化了数百处可能存在问题的代码。最终TSLint接入了相关React Native开发团队,成为了代码提交阶段的必要步骤。通过团队内部的验证,文章开头遇到的问题得到了有效地缓解,目标基本达到预期。TSLint在React Native开发过程中既保证了代码风格的统一,又保证了React Native开发人员的开发质量,避免了许多低级错误,有效地节省了问题排查和人员沟通的成本。同时利用自定义规则,能够将一些兼容性问题在内的个性化问题进行总结与预防,提高了开发效率,不用花费大量时间查找问题代码,又避免了在一个问题上跌倒多次的情况出现。对于不同经验的开发者而言,不仅可以进行友好的提示,也可以帮助快速地定位问题,将一个人遇到的经验教训,用极低的成本扩散到其他团队之中,将开发状态从“亡羊补牢”进化到“防患未然”。作者简介家正,美团点评Android高级工程师。2017 年加入美团点评,负责美团大交通的业务开发。 ...

January 22, 2019 · 1 min · jiezi

[ 一起学React系列 -- 11 ] React-Router4 (1)

2019年不知不觉已经过去19天了,有没有给自己做个总结?有没有给明年做个计划?当然笔者已经做好了明年的工作、学习计划;同时也包括今年剩下的博文计划,在这里透露下:年前(对,没有写错,是年前)完成该系列博客,目前还剩4篇:分别是两篇React-Router4和两篇immutability-helper。本篇是React-Router4的第一篇,在正式开始之前大家可以先看下实际效果,这是笔者用React-Router4写的例子。React-Router4实际上,笔者接触的第一个React中的路由模块就是React-Router,只不过当时还是v3版本。因为没有太多深入得学习研究,所以在这里不会对v4之前的版本作介绍或者评价(其实并不代表笔者对v4版本有深入的研究学习,汗…)。下面列举些React-Router4中需要知道的一些概念或者emmmm…小知识点。React-Router4中的包React-Router4中的包主要有三个react-router、react-router-dom和react-router-native。据考证(手动斜眼笑),React-Router4已经将前身切分了出来分别整合成了单独的module。其实笔者一开始看到也挺懵逼的,这三个包到底是什么玩意?react-router:“The core of React Router”。简单说就是逻辑代码。react-router-dom:“DOM bindings for React Router”。这个模块不仅仅包含了react-router的模块,还包含了页面渲染功能,也就是可以在页面上显示。react-router-native:“React Native bindings for React Router”。这个也很好理解,就是可以在React-Native中使用。路由根节点React-Router4的理念是一切皆组件。React-router-dom则提供了两个路由根节点:HashRouter和BrowserRouter。HashRouter: 通过hash值来对路由进行控制,而且你会发现一个现象就是url中会有个#,例如localhost:3000/#。对于笔者这种有强迫症的人来说怎么能忍?所以笔者就没再碰这个组件。BrowserRouter: BrowserRouter就相对舒服点。它的原理是使用HTML5 history API (pushState, replaceState, popState)来使你的内容随着url动态改变的,而不会出现莫名其妙的#。React-Router4的理念上面提到:React-Router4的理念是一切皆组件(以下统一称“组件”)。我们v4之前的版本都需要在一个js文件中配置整个项目的路由信息然后在App.js(以create-react-app脚手架创建的React项目为例)引入并使用。而在React-Router4中则将所有的功能以React组件的形式提供出来,意思就是说我们可以像使用普通组件一样使用路由组件并最终在项目中形成一棵路由树。在这里笔者要注重说下,一个项目中尽量只有一棵路由树,除非有特殊需求,否则的话会造成一些奇怪的问题。通俗点说就是不要在一棵树上栽另一棵树。下面用一张图来简单展示下React-Router4的使用:如图是React-Router4使用规则和最简单的使用实践。当然这种写法(我称它叫组件型路由,下同)和之前的配置型路由哪个更好用是青菜萝卜的问题,不需要太多纠结,自己喜欢就好。接下来的介绍都是笔者通过学习官方文档并做了一些实践后的心得。如有不当或者错误,欢迎指正。对于第一次学习该模块的童鞋,笔者建议将代码下载下来一遍看代码一遍看本文,也可以打开笔者的demo,这样会更深刻。常用路由组件介绍下面这段代码是该demo根路由配置的代码(其实不存在什么根路由概念,只是笔者喜欢这么叫它)。从这里就能明显看出来react-router3和4两个版本的差异。react-router3有自己独立的一个配置中心文件,而react-router4则把跳转的规则嵌入到实际代码中,不需要额外维护一个路由配置文件。从这点来说的确方便了不少,也迎合React一切皆组件的理念。<Router> <div className={AppStyle[“Container-body”]}> <nav className={AppStyle[“App-nav-list”]}> <ul> <li><NavLink activeStyle={{ fontWeight: ‘bold’, color: ‘red’ }} to="/basic">基础路由</NavLink></li> <li><NavLink activeStyle={{ fontWeight: ‘bold’, color: ‘red’ }} to="/param">带参路由</NavLink></li> <li><NavLink activeStyle={{ fontWeight: ‘bold’, color: ‘red’ }} to="/nesting">嵌套路由</NavLink></li> <li><NavLink activeStyle={{ fontWeight: ‘bold’, color: ‘red’ }} to="/ambiguous">暧昧匹配</NavLink></li> <li><NavLink activeStyle={{ fontWeight: ‘bold’, color: ‘red’ }} to="/auth">权限路由</NavLink></li> <li><NavLink activeStyle={{ fontWeight: ‘bold’, color: ‘red’ }} to="/404">404</NavLink></li> </ul> </nav> <div className={AppStyle.content}> <Switch> <Redirect exact from="/" to="/basic" /> <Route path="/basic" component={renderBasicRouter} /> <Route path="/param" component={RouterWithParameters} /> <Route path="/ambiguous" component={renderAmbiguousRouter} /> <Route path="/nesting" component={renderNestingRouter} /> <Route path="/auth" component={authenticationRouter} /> <Route component={NoMatch} /> </Switch> </div> </div></Router>从这段代码中需要了解的概念包括:Router、NavLink、Route、Redirect、exact、Switch。Router上面已经介绍过了,这里笔者用的是BrowserRouterNavLinkNavLink可以触发路由的跳转,当然类似的组件还有Link。它们都可以通过指定属性to的值来告诉Router我们要跳转到那个Route,实际上NavLink(Link)和Route本就已经通过to和path两个属性建立起关系了。而NavLink与Link的区别在于各自API的数量,因为NavLink可用的API相对较多,所以笔者更青睐NavLink。具体API有兴趣的可以自行Google,常用的API笔者已经在项目中使用。RouteRoute组件是React Router4中主要的组成单位,可以认为是NavLink或Link的路由入口。在这里笔者要重点Tip一下,上一条说NavLink或Link可以触发路由的跳转,实际上它们的实现流程是这样的:NavLink(Link)改变地址栏的pathname,Router会根据pathname去匹配它子组件中的Route中的path属性,一旦匹配上就会渲染该Route对应的组件。所以NavLink(Link)与Route并没有并存的关系,因为NavLink(Link)只是用来改变pathname,不会直接去调用任何API。所以当我们在地址栏直接手动输入路由,也会发生路由渲染。具体匹配规则请参考path-to-regexp,或者通过这个网站进行测试。RedirectRedirect相当于转发器。Router内部去匹配路由入口的时候也会去匹配Redirect的from属性值。一旦匹配到了Redirect,Redirect就会将这个跳转请求转向自己的to属性值对应的路由。所以这个过程可以这样理解:当我们访问页面路径的时候,比如:http://ip:3001/,就会捕获到'/'这个路由跳转请求,Router开始在Route中匹配随后匹配到了Redirect,Redirect自行发起路由跳转请求'/basic',Router开始像往常一样在Route中匹配直到匹配到了path为"/basic"的Route,随后对Route对应的component进行渲染。exactexact将该Route标示为严格匹配路由。什么叫严格匹配路由?就是pathname必须与Route的path属性值完全一致才算匹配上。例如:pathnamepath匹配结果/home/home可以匹配/home/child/home无法匹配这里Tip下:如果某个Route对应的组件中也有Route,那么千万不要 在这个Route中加 exact,不然你会发现完全匹配不到子路由。切记,因为笔者最近刚踩过这个坑。所以这里笔者建议大家只在叶子Route中使用exact,也就是最后一级Route中使用exact,当然 exact '/' 除外。SwitchSwitch可以将多个Route包裹成一组Route,当进行路由匹配时候,该组中的路由一旦发生匹配那么就不会匹配改组中剩下的路由。也就是说该组中的路由最多只会被匹配到一个。Route的component属性追加一条。Route的component属性对应的属性值通常是一个组件或者一个方法。而如果是方法的话,比如例子中:const renderBasicRouter = ({ match }) =&gt; &lt;Basic url={match.url} path={match.path} /&gt;;那么传入的参数为:所以如果在跳转前有什么特殊逻辑需要处理,比如我们想让不同的页面有不同的前缀,比如例子中的/basic/Home、/param/home等,那么就可以如例子一样处理。但如果不需要特殊处理的话,直接把组件放到component属性下即可。当然router相关的参数也会通过props传给该组件。这里Tip下:1、Route中还有一个render属性,也可以用来渲染组件,但是当我们渲染被Redux处理过的组件时候可能会有问题,因为Redux会在原组件基础上多包裹一层。2、如果当然常用路由组件还不仅仅这些,后续例子会有补充。基础路由基础路由的使用比较简单,前面的介绍其实已经把它基础使用方法已经说了。不过笔者这里有个小tip:match.url 常用于NavLink或者Link中拼接路由路径,比如:&lt;NavLink to={${URL}/Home}&gt;&lt;/NavLink&gt;match.path常用于Route中拼接路由入口路径,比如:&lt;Route path={${PATH}/Home} component={HomePage} /&gt;这里Tip下:当URL中不带有任何参数的时候,match.path和match.url完全一致。如果带有参数的话可能会有点编码上的差异。带参路由带参路由在实际开发中用的比较多,这里只介绍location.pathname中的参数,location.search笔者没有研究过所以就不说了,免得误导大家。我们可以先看下demo例子的部分代码:&lt;nav className={Style["Params-nav-list"]}&gt; &lt;ul&gt; &lt;li&gt;&lt;NavLink exact activeStyle={{ fontWeight: 'bold', color: 'red' }} to={${URL}/name}&gt;/name&lt;/NavLink&gt;&lt;/li&gt; &lt;li&gt;&lt;NavLink exact activeStyle={{ fontWeight: 'bold', color: 'red' }} to={${URL}/name/Mario}&gt;/name/Mario&lt;/NavLink&gt;&lt;/li&gt; &lt;li&gt;&lt;NavLink activeStyle={{ fontWeight: 'bold', color: 'red' }} to={${URL}/check/true}&gt;/check/true&lt;/NavLink&gt;&lt;/li&gt; &lt;/ul&gt;&lt;/nav&gt;&lt;div className={Style.content}&gt; &lt;Switch&gt; &lt;Route exact path={${PATH}/name/:name} component={Name} /&gt; &lt;Route exact path={${PATH}/name} component={Name1} /&gt; &lt;Route exact path={${PATH}/check/:check(true|false)} component={Check} /&gt; &lt;Route component={NoMatch} /&gt; &lt;/Switch&gt;&lt;/div&gt;这里可以看到Route的配置有点不同,用过express的朋友都知道,路由中通过 :xxx 来标示这是一个参数。其实这里也一样。如例子所示我们在 path={${PATH}/name/:name}通过 :name 来标示这是一个参数。然后对应的导航也有 to={${URL}/name/Mario}。所以这个流程就相当于:导航到/name路由并且传 name为Mario的参数。这个参数可以在对应组件的props中拿到(this.props.match.prams.name)。我们还看到 path={${PATH}/check/:check(true|false)},在参数后有个括号并且里面还有管道符,其实这是限定check的参数值必须为true或者false这两个。细心的朋友可能注意到,我在每个Route中加了exact,这样做的好处是可以不用考虑Route的放置次序。举个例子解释下:如果我们想查看某个人的信息,那么跳转路由应该是 /user/4,但如果Route中有:&lt;Switch&gt; &lt;Route exact path={/user} component={User} /&gt; &lt;Route exact path={/user/:id} component={User} /&gt;&lt;/Switch&gt;那么/user/4就会在匹配到/user后停下渲染User组件并且忽略了参数。有人说把Switch去掉不就行了吗?并不是,那么/user/4会同时匹配上这两个路由并且什么都不会渲染,因为它懵逼了,不知道渲染哪个。所以这种情况下需要调整它们的位置&lt;Switch&gt; &lt;Route exact path={/user/:id} component={User} /&gt; &lt;Route exact path={/user} component={User} /&gt;&lt;/Switch&gt;这样就不会出现上述问题了。但是如果在每一个Route中使用exact(前提是这个Route是叶子Route),就不用考虑Route的次序问题了。404路由有时候会由于各种问题出现匹配不到任何Route的情况,这个时候为了更好的用户体验,我们会配置一个404路由,形如:&lt;div className={Style.content}&gt; &lt;Switch&gt; &lt;Route exact path={${PATH}/name/:name} component={Name} /&gt; &lt;Route exact path={${PATH}/name} component={Name1} /&gt; &lt;Route exact path={${PATH}/check/:check(true|false)`} component={Check} /> <Route component={NoMatch} /> </Switch></div>不过笔者发现在根路由中配置一个404后无法全局抓取路由404,不知道是本就如此还是用法有误,所以笔者在每一级路由中都配置了 <Route component={NoMatch} />。写法很简单,照着写就好了,component中传入显示404信息的组件即可。顺便加一下demo源码 ...

January 19, 2019 · 2 min · jiezi

React Native 性能优化 (官网指南搬运)

最近在写React-Native 趁着这两天需求差不多完成了,实践了一些优化项。记录于此Life sucksPerformance参考 React native Performance查看性能打开开发者菜单(摇晃手机打开)???? 打开Show Perf Monitor 可以看到下图显示框UI 和 JS 的帧数都稳定保持在60 为最优情况。JS 的单线程所有的事件处理,API请求,等操作都在这个线程上,在this.setState大量数据时,状态的变动会导致re-render,这期间所有由JavaScript 控制的动画都会出现卡顿掉帧比如在切换路由时,帧数会有明显抖动。此时如果有一些在componentDidMount 执行的操作就会使得路由过渡动画非常卡顿。(后面会介绍一些可以尝试的解决方案开发环境性能比生产环境差开发环境下框架会有很多别的操作比如warning error 的输出,类型检测等等。如果要测试性能,最好在release 包测试。这样更加精准。生产环境移除console.开发时,会有很多console. 指令来帮助调试。并且一些依赖库也会有console.* 这些语句对JavaScript 线程来说是一个极大的消耗。可以通过Babel 在生产环境中移除掉console.*。安装插件npm i babel-plugin-transform-remove-console –save-dev配置.babelrc 文件{ “env”: { “production”: { “plugins”: [“transform-remove-console”] } }}处理大量数据列表时使用<FlatList />FlatList 组件更加适合来展示长列表,并且指定合适的 getItemLayout 方法, getItemLayout 会跳过渲染Item 时的布局计算,直接使用给定的配置(详情查看链接☝️)依赖懒加载在框架执行编写好的业务代码前,需要把在内存中加载并解析代码,代码量越大这个过程就更耗时,导致首屏渲染速度过慢。而且往往会出现一些页面或者组件根本不会被用户访问到。这时可以通过懒加载来优化。官网有给出例子VeryExpensive.jsimport React, { Component } from ‘react’;import { Text } from ‘react-native’;// … import some very expensive modules// You may want to log at the file level to verify when this is happeningconsole.log(‘VeryExpensive component loaded’);export default class VeryExpensive extends Component { // lots and lots of code render() { return <Text>Very Expensive Component</Text>; }}Optimized.jsimport React, { Component } from ‘react’;import { TouchableOpacity, View, Text } from ‘react-native’;let VeryExpensive = null; //定义变量export default class Optimized extends Component { state = { needsExpensive: false }; // 定义内部状态来控制组件是否需要加载 didPress = () => { // 在触发需要加载组件的事件时 if (VeryExpensive == null) { // 不重复引用 VeryExpensive = require(’./VeryExpensive’).default; // 把组件的引用赋给定义好的变量 } this.setState(() => ({ needsExpensive: true, // 更改控制的状态,触发组件re-render })); }; render() { return ( <View style={{ marginTop: 20 }}> <TouchableOpacity onPress={this.didPress}> <Text>Load</Text> </TouchableOpacity> {this.state.needsExpensive ? <VeryExpensive /> : null} </View> ); }}优化组件渲染次数React 在内部state 或者外部传入的props 发生改变时,会重新渲染组件。如果在短时间内有大量的组件要重新渲染就会造成严重的性能问题。这里有一个可以优化的点。使用PureComponent 让组件自己比较props 的变化来控制渲染次数,实践下来这种可控的方式比纯函数组件要靠谱。或者在Component 中使用 shouldComponentUpdate 方法,通过条件判断来控制组件的更新/重新渲染。使用PureComponent 时要注意这个组件内部是浅比较状态,如果props 的有大量引用类型对象,则这些对象的内部变化不会被比较出来。所以在编写代码时尽量避免复杂的数据结构细粒度组件,拆分动态/静态组件。需要在项目稳定并有一定规模后来统一规划。学习 immutable-js 异步,回调JavaScript 单线程,要利用好它的异步特性,和一些钩子回调。比如上面提到路由切换时componentDidMount 中的操作会导致卡顿,这里可以使用 InteractionManager.runAfterInteractions() 将需要执行的操作放到runAfterInteractions 的回调中执行。componentDidMount() { InteractionManager.runAfterInteractions(() => { // your actions })}需要注意的是 InteractionManager 是监听所有的动画/交互 完成之后才会触发 runAfterInteractions 中的回调,如果项目中有一些长时间动画或者交互,可能会出现长时间等待。所以 由于 InteractionManager 的不可控性,使用的时候要根据实际情况调整。在react-native 中的一些动画反馈,比如TouchableOpacity 在触摸时会响应 onPress 并且 自身的透明度会发生变化,这个过程中如果 onPress 中有复杂的操作,很可能会导致组件的透明反馈卡顿,这时可以将onPress 中的操作包裹在 requestAnimationFrame 中。这里给出一个我的实践(利用styled-component)import styled from ‘styled-components’export const TouchableOpacity = styled.TouchableOpacity.attrs({ onPress: props => () => { requestAnimationFrame(() => { props.onPressAsync && props.onPressAsync() }, 0) }})``这里把onPress 改成在 requestAnimationFrame 的回调中执行onPressAsync 传入的操作。同理,还在FlatList 的onReachEnd实践了这个操作,来避免iOS 中滚动回弹时执行操作的卡顿。以上,记录了近期写React-Native 的一些实践过的优化项。最后路漫漫其修远兮,吾将上下而求索May love & peace be with you参考React native Performance本文作者: Roy Luo本文链接: React Native 性能优化 (官网指南搬运) ...

January 18, 2019 · 2 min · jiezi

在React Native中集成热更新

最近,在项目DYTT集成了热更新,简单来说,就是不用重新下载安装包即可达到更新应用的目的,也不算教程吧,这里记录一下。1.热更新方案目前网上大概有两个比较广泛的方式,分别是react-native-pushyreact-native-code-push前者是由ReactNative中文网推出的代码热更新服务,后者是由微软老大哥推出的,当然不仅仅是为React Native,还包括其他原生方式。综合考虑之下,选择了react-native-code-push。2.安装code-push1.安装code-pushnpm install -g code-push-cli2.注册登录账号code-push register这时候会自动启动浏览器打开网页并提供一个codePush AccessKey,然后命令行里出现需要输入access key输入之后就登录成功了。(貌似在本机上以后都不用登录了,暂不清楚保持登录持续多久)3.添加一个CodePush应用code-push app add myProject android react-native注意填写app的名称,OS(android/ios),平台(react-native),并且android和ios需要创建两个应用创建完成会出现两个keynameDeployment KeyProduction(一串37位的key)Staging(一串37位的key)Production是对应生产环境的,Staging是对应开发环境的。这个对于我们来说其实没什么区别,只是为了方便测试,所以搞了两个环境3.react-native应用接入code-push1.安装react-native-code-pushyarn add react-native-code-push# linkreact-native link react-native-code-push2.原生配置目前只测试了android,ios有兴趣的可以自行测试上面提到了两个key值,现在需要配置在原生目录里1.打开android/app/build.gradleandroid { … buildTypes { debug { … // Note: CodePush updates should not be tested in Debug mode as they are overriden by the RN packager. However, because CodePush checks for updates in all modes, we must supply a key. buildConfigField “String”, “CODEPUSH_KEY”, ‘""’ … } releaseStaging { … buildConfigField “String”, “CODEPUSH_KEY”, ‘"<INSERT_STAGING_KEY>"’//注意这里的引号 … } release { … buildConfigField “String”, “CODEPUSH_KEY”, ‘"<INSERT_PRODUCTION_KEY>"’ … } } …}如果遇到打包错误,可加上matchingFallbacks = [‘release’, ‘debug’],不知道是不是个别情况,如果没有的请忽略。修改versionName为3位数的版本号(code-push要求)defaultConfig { applicationId “com.dytt” minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 2 versionName “2.1.0”//默认为2位版本号 // ndk { // abiFilters “armeabi-v7a”, “x86” // } }release { //… matchingFallbacks = [‘release’, ‘debug’]//加上这一句 buildConfigField “String”, “CODEPUSH_KEY”, ‘"<INSERT_PRODUCTION_KEY>"’ //… }2.打开MainApplication.java@Overrideprotected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( … new CodePush(BuildConfig.CODEPUSH_KEY, MainApplication.this, BuildConfig.DEBUG), // Add/change this line. … );}这样就实现了key的动态部署,即在什么环境下使用什么key以上文档参考自https://github.com/Microsoft/react-native-code-push/blob/master/docs/multi-deployment-testing-android.md4.客户端更新策略1.导入react-native-code-push这里需要在应用的根组件上添加CodePush配置import CodePush from “react-native-code-push”;如果你的环境支持Decorator(修饰符),可以这样@codePush(options: CodePushOptions)class MyApp extends Component<{}> {}普通的写法class MyApp extends Component<{}> {}MyApp = codePush(codePushOptions)(MyApp);export default MyApp;这里的codePushOptions是更新的配置选项checkFrequency (codePush.CheckFrequency) 指定您要检查更新的时间,默认为codePush.CheckFrequency.ON_APP_START。installMode (codePush.InstallMode) 指定何时安装可选更新,默认为codePush.InstallMode.ON_NEXT_RESTART。…详细的配置可参考https://github.com/Microsoft/react-native-code-push/blob/master/docs/api-js.md2.更新策略默认情况下,CodePush会在app每次启动的时候去检测是否有更新,如果有,app会自动下载并在下次打开app时安装这种更新方式是静默的,用户根本察觉不到。如果我们需要给一点更新提示,可以使用默认的弹出框,也就是react-native自带的Alert,点击后立即安装class MyApp extends Component {}MyApp = codePush({ updateDialog: true, installMode: codePush.InstallMode.IMMEDIATE})(MyApp);当然,你可以对弹出框做少量的自定义,比如标题,按钮的文字等updateDialog: { optionalIgnoreButtonLabel: ‘稍后’, optionalInstallButtonLabel: ‘立即更新’, optionalUpdateMessage: ‘有新版本了,是否更新?’, title: ‘更新提示’},这些是默认的更新方式,那么如何自定义呢。我们可以用到CodePush.checkForUpdate来手动检查更新,然后弹出一个自定义窗口const RemotePackage = await CodePush.checkForUpdate(deploymentKey);if(RemotePackage){ this.modal.init(RemotePackage);//打开弹窗}这里需要注意的是,在checkForUpdate(或其他需要填写deploymentKey的地方)的时候,如果在debug模式下,如果不填写deploymentKey,会提示缺少deploymentKey,我们可以临时写一个固定的方便测试。在正式环境下,这里是不需要填写,它会根据系统自动获取我们在之前配置的那些deploymentKey值然后可以通过RemotePackage.download和LocalPackage.install来完成下载和安装install = async () => { LayoutAnimation.easeInEaseOut(); this.setState({status:1})//download const LocalPackage = await this.RemotePackage.download((progress)=>{ this.setState({ receivedBytes:progress.receivedBytes }) Animated.timing( this.width, { toValue: parseFloat(progress.receivedBytes / progress.totalBytes).toFixed(2), duration: 150 } ).start(); }) this.setState({status:2})//downloadComplete await LocalPackage.install(LocalPackage.isMandatory?CodePush.InstallMode.IMMEDIATE:CodePush.InstallMode.ON_NEXT_RESUME); if(!LocalPackage.isMandatory){ this.setState({status:3}) this.setVisible(false); }else{ ToastAndroid && ToastAndroid.show(‘下次启动完成更新’, ToastAndroid.SHORT); }}具体实现可以参考项目DYTT3.打包Releasecd android# 生成Release(Production)包gradlew assembleRelease# 生成Release(Staging)包gradlew assembleReleaseStaging其实都一样,只是环境区别5.发布code-push更新这一步很简单,集成了打包和发布code-push release-react dyttAndroid android –t 2.1.0 –dev false –d Production –des “1.修复了已知BUG\n 2.测试code push” –m true这里注意–t 2.1.0,有以下几种规则1.2.3 仅仅只有1.2.3的版本* 所有版本1.2.x 主要版本1,次要版本2的任何修补程序版本1.2.3 - 1.2.7 1.2.3版本到1.2.7版本>=1.2.3 <1.2.7 大于等于1.2.3版本小于1.2.7的版本~1.2.3 大于等于1.2.3版本小于1.3.0的版本^1.2.3 大于等于1.2.3版本小于2.0.0的版本–d表示开发版本,可选择Production和Staging–m表示是否强制更新当然还有很多操作,比如删除某些更新,回滚等,可以参考官方文档https://github.com/Microsoft/react-native-code-push小节总的来说,这次热更新集成还是挺容易,里面碰到的几个误区在上面也已经提到过,欢迎大家多多关注我的项目DYTT^^ ...

January 11, 2019 · 2 min · jiezi

(踩坑回忆录)React-Native踩坑与解决方案

前言问题textInput相关设置键盘隐藏与换行(ios)软键盘遮住文本框总结未完待续…长期追更…

January 11, 2019 · 1 min · jiezi

「译」setState如何知道它该做什么?

本文翻译自:How Does setState Know What to Do?原作者:Dan Abramov如果有任何版权问题,请联系shuirong1997@icloud.com当你在组件中调用setState时,你觉得会发生什么?import React from ‘react’;import ReactDOM from ‘react-dom’;class Button extends React.Component { constructor(props) { super(props); this.state = { clicked: false }; this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState({ clicked: true }); } render() { if (this.state.clicked) { return <h1>Thanks</h1>; } return ( <button onClick={this.handleClick}> Click me! </button> ); }}ReactDOM.render(<Button />, document.getElementById(‘container’));当然,React会用{ clicked: true} 这条状态重新渲染组件并且更新匹配到的DOM,然后返回<h1>Thanks</h1>元素。听起来似乎简洁明了。但别急,React(或者说React DOM)是怎么做的?更新DOM听起来像是React DOM的事儿,但别忘了我们调用的可是this.setState(),它是React的东西,可不是React DOM的。另外,我们的基类React.Component是被定义在React内部。所以问题来了:React.Component内部的setState怎么能去更新DOM呢?事先声明:就像我的其他博客,你不需要熟练掌握React。这篇博客是为那些想要看看面纱之后是什么东西的人准备的。完全可选!我们或许会认为React.Component类已经包含了DOM更新逻辑。但如果这是事实,那this.setState是如何工作在其他环境中呢?比如:在React Native App中的组件也能继承React.Component,他们也能像上面一样调用this.setState(),并且React Native工作在Android和iOS的原生视图而不是DOM中。你可能也对React Test Renderer 或 Shallow Renderer比较熟悉。这两个测试渲染器让你可以渲染一般的组件并且也能在他们中调用this.setState,但他们可都不使用DOM。如果你之前使用过一些渲染器比如说React ART,你可能知道在页面中使用超过一个渲染器是没什么问题的。(比如:ART组件工作在React DOM 树的内部。)这会产生一个不可维持的全局标志或变量。所以React.Component以某种方式将state的更新委托为具体的平台(译者注:比如Android, iOS),在我们理解这是如何发生之前,让我们对包是如何被分离和其原因挖得更深一点吧!这有一个常见的错误理解:React “引擎"在react包的内部。这不是事实。事实上,从 React 0.14开始对包进行分割时,React包就有意地仅导出关于如何定义组件的API了。React的大部分实现其实在“渲染器”中。渲染器的其中一些例子包括:react-dom,react-dom/server,react-native,react-test-renderer,react-art(另外,你也可以构建自己的)。这就是为什么react包帮助很大而不管作用在什么平台上。所有它导出的模块,比如React.Component,React.createElement,React.Children和Hooks,都是平台无关的。无论你的代码运行在React DOM、React DOM Server、还是React Native,你的组件都可以以一种相同的方式导入并且使用它们。与之相对的是,渲染器会暴露出平台相关的接口,比如ReactDOM.render(),它会让你可以把React挂载在DOM节点中。每个渲染器都提供像这样的接口,但理想情况是:大多数组件都不需要从渲染器中导入任何东西。这能使它们更精简。大多数人都认为React“引擎”是位于每个独立的渲染器中的。许多渲染器都包含一份相同的代码—我们叫它“调节器”,为了表现的更好,遵循这个步骤 可以让调节器的代码和渲染器的代码在打包时归到一处。(拷贝代码通常不是优化“打包后文件”(bundle)体积的好办法,但大多数React的使用者一次只需要一个渲染器,比如:react-dom(译者注:因此可以忽略调节器的存在))The takeaway here 是react包仅仅让你知道如何使用React的特性而无需了解他们是如何被实现的。渲染器(react-dom,react-native等等)会提供React特性的实现和平台相关的逻辑;一些关于调节器的代码被分享出来了,但那只是单独渲染器的实现细节而已。现在我们知道了为什么react和react-dom包需要为新特定更新代码了。比如:当React16.3新增了Context接口时,React.createContext()方法会在React包中被暴露出来。但是React.createContext()实际上不会实现具体的逻辑(译者注:只定义接口,由其他渲染器来实现逻辑)。并且,在React DOM和React DOM Server上实现的逻辑也会有区别。所以createContext()会返回一些纯粹的对象(定义如何实现):// 一个简单例子function createContext(defaultValue) { let context = { _currentValue: defaultValue, Provider: null, Consumer: null }; context.Provider = { $$typeof: Symbol.for(‘react.provider’), _context: context }; context.Consumer = { $$typeof: Symbol.for(‘react.context’), _context: context, }; return context;}你会在某处代码中使用<MyContext.Provider>或<MyContext.Consumer>,那里就是决定着如何处理他们的渲染器。React DOM会用A方法追踪context值,但React DOM Server或许会用另一个不同的方法实现。所以如果你将react升级到16.3+,但没有升级react-dom,你将使用一个还不知道Provider和Consumer类型的渲染器,这也就旧版的react-dom可能会报错:fail saying these types are invalid的原因。同样的警告也会出现在React Native中,但是不同于React DOM,一个新的React版本不会立即产生一个对应的React Native版本。他们(React Native)有自己的发布时间表。大概几周后,渲染器代码才会单独更新到React Native库中。这就是为什么新特性在React Native生效的时间会和React DOM不同。Okay,那么现在我们知道了react包不包含任何好玩的东西,并且具体的实现都在像react-dom,react-native这样的渲染器中。但这并不能回答我们开头提出的问题。React.Component里的setState()是如何和对应的渲染器通信的呢?答案是每个渲染器都会在创建的类中添加一个特殊的东西,这个东西叫updater。它不是你添加的东西—恰恰相反,它是React DOM,React DOM Server 或者React Native在创建了一个类的实例后添加的:// React DOM 中是这样const inst = new YourComponent();inst.props = props;inst.updater = ReactDOMUpdater;// React DOM Server 中是这样const inst = new YourComponent();inst.props = props;inst.updater = ReactDOMServerUpdater;// React Native 中是这样const inst = new YourComponent();inst.props = props;inst.updater = ReactNativeUpdater;从 setState的实现就可以看出,它做的所有的工作就是把任务委托给在这个组件实例中创建的渲染器:// 简单例子setState(partialState, callback) { // 使用updater去和渲染器通信 this.updater.enqueueSetState(this, partialState, callback);}React DOM Server 可能想忽略状态更新并且警告你,然而React DOM和React Native将会让调节器的拷贝部分去 处理它。这就是尽管this.setState()被定义在React包中也可以更新DOM的原因。它调用被React DOM添加的this.updater并且让React DOM来处理更新。现在我们都比较了解“类”了,但“钩子”(Hooks)呢?当人们第一次看到 钩子接口的提案时,他们常回想:useState是怎么知道该做什么呢?这一假设简直比对this.setState()的疑问还要迷人。但就像我们如今看到的那样,setState()的实现一直以来都是模糊不清的。它除了传递调用给当前的渲染器外什么都不做。所以,useState钩子做的事也是如此。这次不是updater,钩子(Hooks)使用一个叫做“分配器”(dispatcher)的对象,当你调用React.useState()、React.useEffect()或者其他自带的钩子时,这些调用会被推送给当前的分配器。// In React (simplified a bit)const React = { // Real property is hidden a bit deeper, see if you can find it! __currentDispatcher: null, useState(initialState) { return React.__currentDispatcher.useState(initialState); }, useEffect(initialState) { return React.__currentDispatcher.useEffect(initialState); }, // …};单独的渲染器会在渲染你的组件之前设置分配器(dispatcher)。// In React DOMconst prevDispatcher = React.__currentDispatcher;React.__currentDispatcher = ReactDOMDispatcher;let result;try { result = YourComponent(props);} finally { // Restore it back React.__currentDispatcher = prevDispatcher;}React DOM Server的实现在这里。由React DOM和React Native共享的调节器实现在这里。这就是为什么像react-dom这样的渲染器需要访问和你调用的钩子所使用的react一样的包。否则你的组件将找不到分配器!如果你有多个React的拷贝在相同的组件树中,代码可能不会正常工作。然而,这总是造成复杂的Bug,因此钩子会在它耗光你的精力前强制你去解决包的副本问题。如果你不觉得这有什么,你可以在工具使用它们前精巧地覆盖掉原先的分配器(__currentDispatcher的名字其实我自己编的但你可以在React仓库中找到它真正的名字)。比如:React DevTools会使用一个特殊的内建分配器来通过捕获JavaScript调用栈来反映(introspect)钩子。不要在家里重复这个(Don’t repeat this at home.)(译者注:可能是“不要在家里模仿某项实验”的衍生体。可能是个笑话,但我get到)这也意味着钩子不是React固有的东西。如果在将来有很多类库想要重用相同的基础钩子,理论上来说分配器可能会被移到分离的包中并且被塑造成优秀的接口—会有更少让人望而生畏的名称—暴露出来。在实际中,我们更偏向去避免过于仓促地将某物抽象,直到我们的确需要这么做。updater和__currentDispatcher都是泛型程序设计(依赖注入/dependency injection)的绝佳实例。渲染器“注入”特性的实现。就像setState可以让你的组件看起来简单明了。当你使用React时,你不需要考虑它是如何工作的。我们期望React用户去花费更多的时间去考虑它们的应用代码而不是一些抽象的概念比如:依赖注入。但如果你曾好奇this.setState()或useState()是怎么知道它们该做什么的,那我希望这篇文章将帮助到你。 ...

January 9, 2019 · 2 min · jiezi

RN 经验文档

React Native 返回并刷新页面RN 在进行A页面定义回调方法this.props.navigation.goBack()并不会触发前一个页面的生命钩子,然而有时我们在新页面进行了修改操作,同时之前的页面也发生了相应的改变this.props.navigation.navigate(“newPage”, { id: this.state.id, refresh: function () { this._init(); }});newPage页面返回操作执行后会执行 refresh 方法里的 _init();<View onPress={() => { this.props.navigation.state.params.refresh(); this.props.navigation.goBack();}}> <Text>goback</Text></View>React Native 项目名称修改修改应用显示名(手机上的APP名称)androidappsrcmainresvaluesstrings.xml //IOS下可以在 ios{project}Info.plist中直接更改修改包名package.jsonindex.{os}.jsandroidsettings.gradleandroidappbuild.gradleandroidappsrcmainjavacom{project}androidappsrcmainjavacom{project}MainActivity.javaandroidappsrcmainjavacom{project}MainApplication.java<key>CFBundleIdentifier</key><key>CFBundleName</key>//下的string直接更改项目名称。//不过在XCode下更方便,开发IOS的话,还是来台MAC吧 - -React Native 应用图标修改替换android/app/src/main/res/mipmap-XXX文件夹里面的图片,名字不能修改React Native 启动页设置替换android/app/src/main/res/drawable-XXX文件夹里面的图片,名字不能修改注:gradlew assembleRelease或者react-native run-android之前最好先gradlew cleanAndroid如何查看应用签名信息打开密钥放置文件夹(android/app/),输入以下命令,my-release-key.jks为密钥文件名keytool -list -keystore my-release-key.jks

January 9, 2019 · 1 min · jiezi

关于ReactNative0.56版本Flatlist列表内容跳动的问题

Reactnative的版本升级一直是一个工作量比较的大的事情,每次升级都可能伴随着很多的坑。前段时间在升级到0.56版本的时候发现一个问题,在flatlist使用中,加载多页后,列表项内容开始进行上下抖动的乱跳,疯了一样。于是开始上react-native的issues上寻找答案,有通过查看官方的版本升级日志找到了答案:react-native升级日志0.57在其中看到如下bugFix描述:因为Flatlist继承自VirtualizedList,所以就豁朗开朗了。解决方案:将ReactNative Version升级到0.57以上版本就好了官方升级方案,推荐第一种“基于 Git 的自动合并更新”的升级方案。我的网站:https://wayne214.github.io

January 4, 2019 · 1 min · jiezi

为什么 React Elements 会有 $$typeof 这个属性?

简评:debug 的时候看到 element 对象中有 $$typeof 这属性,于是查了一下这到底干嘛的。我们知道,通过 JSX 创建一个 React Elements 时:<marquee bgcolor="#ffa7c4">hi</marquee>实际上调用的是 React.createElement 方法:React.createElement( /* type / ‘marquee’, / props / { bgcolor: ‘#ffa7c4’ }, / children / ‘hi’)该方法会返回一个 React Element 对象,大概长这样:{ type: ‘marquee’, props: { bgcolor: ‘#ffa7c4’, children: ‘hi’, }, key: null, ref: null, $$typeof: Symbol.for(‘react.element’), // ???? Who dis}这个 $$typeof 是什么? 各种前端框架出现之前,应用通常会构造 HTML 并将它们插入到 DOM 中,例如:const messageEl = document.getElementById(‘message’);messageEl.innerHTML = ‘<p>’ + message.text + ‘</p>’;这代码一般能正常工作,除非你的 message.text 返回 ’<img src onerror=“stealYourPassword()">’ 这样的字符串。为了防止这种情况,可以使用类似于 document.createTextNode()或textContent 仅处理 text 的 api。也可以通过预处理替换 < > 这类特殊的字符。但是这样还是有很多潜在的问题。所以现代前端框架像 React 会为字符串转义成文字内容:<p> {message.text}</p>就算 message.text 返回 <img> 也会当成字符串处理。要在 React 元素中呈现 HTML ,则需要编写dangerouslySetInnerHTML={{ __html: message.text }},这有助于你在审查代码的时候重视它。但是这样仍然无法完全禁止注入攻击,例如:<a href={user.website}>,注意 website 为 ‘javascript: stealYourPassword()‘的情况。 对用户输入使用 … 运算符也很危险 <div {…userData}>。 为什么需要 $$typeof通过上面的介绍你可能已经猜到了,$$typeof 是为了防止 XSS 攻击。前面介绍了,React 元素大概是这样的(没有包含 $$typeof 这个属性的情况):{ type: ‘marquee’, props: { bgcolor: ‘#ffa7c4’, children: ‘hi’, }, key: null, ref: null}试想这样一种情况,服务器允许用户储存任意的 JSON 数据。那么就可以手动构建 React Element 对象传入到元素中。例如:// Server could have a hole that lets user store JSONlet expectedTextButGotJSON = { type: ‘div’, props: { dangerouslySetInnerHTML: { __html: ‘/ put your exploit here */’ }, }, // …};let message = { text: expectedTextButGotJSON };// Dangerous in React 0.13<p> {message.text}</p>所以 React 0.14 使用 Symbol 标记每个 React 元素。{ type: ‘marquee’, props: { bgcolor: ‘#ffa7c4’, children: ‘hi’, }, key: null, ref: null, $$typeof: Symbol.for(‘react.element’),}这样就可以规避这个问题,使用 Symbol 类型是因为 JSON 中无法传递 Symbol。React 会检查 element.$$typeof 然后拒绝处理非法的元素。原文: Why Do React Elements Have a $$typeof Property? ...

January 2, 2019 · 1 min · jiezi

React Native项目搭建(react-native + typescript)

React Native项目搭建(react-native + typescript)1、安装react-nativecli工具npm i -g react-native或者yarn add global react-native2、创建react-native项目react-native init MyRnProject项目名称不允许有中划线,如:my-rn-project3、添加Typescript安装typescript包yarn add –dev typescript安装 react-native-typescript-transformer 包yarn add –dev react-native-typescript-transformer初始化创建tsconfig.jsonyarn tsc –init –pretty –jsx react创建Native TypeScript Transformer的配置文件touch rn-cli.config.js添加React和React Native的类型包yarn add –dev @types/react @types/react-native注释tsconfig.json中的以下一行:{ … // “allowSyntheticDefaultImports”: true, …}向rn-cli.config.js中添加以下内容:module.exports = { getTransformModulePath() { return require.resolve(“react-native-typescript-transformer”); }, getSourceExts() { return [“ts”, “tsx”]; }};4、集成react-native的typescript测试添加 ts-jest依赖yarn add –dev ts-jest 替换package.json中的jest字段为以下内容:“jest”: { “preset”: “react-native”, “moduleFileExtensions”: [ “ts”, “tsx”, “js” ], “transform”: { “^.+\.(js)$”: “<rootDir>/node_modules/babel-jest”, “\.(ts|tsx)$”: “<rootDir>/node_modules/ts-jest/preprocessor.js” }, “testRegex”: “(/tests/.*|\.(test|spec))\.(ts|tsx|js)$”, “testPathIgnorePatterns”: [ “\.snap$”, “<rootDir>/node_modules/” ], “cacheDirectory”: “.jest/cache”}安装类型声明包yarn add –dev @types/jest @types/react @types/react-native @types/react-test-renderer忽略.jest 文件夹# Jest#.jest/至此React Native + TypeScript 项目就搭建完成了 ...

December 29, 2018 · 1 min · jiezi

ReactNative: 使用Animted API实现向上滚动时隐藏Header组件

想先推荐一下近期在写的一个React Native项目,名字叫 Gakki :是一个Mastodon的第三方客户端 (Android App)预览写在前面本来我也不想造这个轮子的,奈何没找到合适的组件。只能自己上了~思路很清楚: 监听滚动事件,动态修改Header组件和Content组件的top值(当然,他们默认都是position:relative)。接下来实现的时候遇到了问题,我第一个版本是通过动态设置state来实现,即:/** * 每次滚动时,重新设置headerTop的值 /onScroll = event =>{ const y = event.nativeEvent.contentOffset.y if (y >= 270) return // headerTop即是Header和Content的top样式对应的值 this.setState({ headerTop: y })}这样虽然能实现,但是效果不好:明显可以看到在上滑的过程中,Header组件一卡一卡地向上方移动(一点都不流畅)。因为就只能另寻他法了:动画React Native 提供了两个互补的动画系统:用于创建精细的交互控制的动画Animated和用于全局的布局动画LayoutAnimation (笔者注:这次没有用到它)Animated 相关API介绍首先,这儿有一个简单“逐渐显示”动画的DEMO,需要你先看完(文档很简单明了且注释清楚,没必要Copy过来)。在看懂了DEMO的基础上,我们还需要了解两个关键的API才能实现完整的效果:1. interpolate插值函数。用来对不同类型的数值做映射处理。当然,这儿是文档说明,可能看了更不清楚:Each property can be run through an interpolation first. An interpolation maps input ranges to output ranges, typically using a linear interpolation but also supports easing functions. By default, it will extrapolate the curve beyond the ranges given, but you can also have it clamp the output value.翻译:每个属性可以先经过插值处理。插值对输入范围和输出范围之间做一个映射,通常使用线性插值,但也支持缓和函数。默认情况下,如果给定数据超出范围,他也可以自行推断出对于的曲线,但您也可以让它箝位输出值(P.S. 最后一句可能翻译错误,因为没搞懂clamp value指的是什么, sigh…)举个例子:在实现一个图片旋转动画时,输入值只能是这样的:this.state = { rotate: new Animated.Value(0) // 初始化用到的动画变量}…// 这么映射是因为style样式需要的是0deg这样的值,你给它0这样的值,它可不能正常工作。因为必定需要一个映射处理。this.state.rotate.interpolate({ // 将0映射成0deg,1映射成360deg。当然中间的数据也是如此映射。 inputRange: [0, 1], outputRange: [‘0deg’, ‘360deg’]})2. Animated.event一般动画的输入值都是默认设定好的,比如前面DEMO中的逐渐显示动画中的透明度:开始是0,最后是1。这是已经写死了的。但如果有些动画效果需要的不是写死的值,而是动态输入的呢,比如:手势(上滑、下滑,左滑,右滑…)、其它事件。那就用到了Animated.event。直接看一个将滚动事件的y值(滚动条距离顶部高度)和我们的动画变量绑定起来的例子:// 这段代码表示:在滚动事件触发时,将event.nativeEvent.contentOffset.y 的值动态绑定到this.state.headerTop上// 和最前面我通过this.setState动态设置的目的一样,但交给Animated.event做就不会造成视觉上的卡顿了。onScroll={Animated.event([ { nativeEvent: { contentOffset: { y: this.state.headerTop } } }])}关于API更多的说明请移步文档完整代码import React, { Component } from ‘react’import { StyleSheet, Text, View, Animated, FlatList } from ‘react-native’class List extends Component { onScroll = event => { // 显示和隐藏Header组件的动画在 滚动条距离顶部距离小于270 时起作用 // 移除这个限制就是另一种效果了,可以自己想一想 if (this.props.onScroll) { if (event.nativeEvent.contentOffset.y >= 270) return this.props.onScroll(event) } } render() { // 模拟列表数据 const mockData = [ ‘富强’, ‘民主’, ‘文明’, ‘和谐’, ‘自由’, ‘平等’, ‘公正’, ‘法治’, ‘爱国’, ‘敬业’, ‘诚信’, ‘友善’ ] return ( <FlatList onScroll={this.onScroll} data={mockData} renderItem={({ item }) => ( <View style={styles.list}> <Text>{item}</Text> </View> )} /> ) }}export default class AnimatedScrollDemo extends Component { constructor(props) { super(props) this.state = { headerTop: new Animated.Value(0) } } onScroll = event => { if (event.nativeEvent.contentOffset.y >= 270) return Animated.event([ { nativeEvent: { contentOffset: { y: this.state.headerTop } } } ]) } render() { const top = this.state.headerTop.interpolate({ inputRange: [0, 270], outputRange: [0, -50] }) return ( <View style={styles.container}> <Animated.View style={{ top: top }}> <View style={styles.header}> <Text style={style.text}>linshuirong.cn</Text> </View> </Animated.View> {/ 在oHeader组件上移的同时,列表容器也需要同时向上移动,需要注意下。 */} <Animated.View style={{ top: top }}> <List onScroll={Animated.event([ { nativeEvent: { contentOffset: { y: this.state.headerTop } } } ])} /> </Animated.View> </View> ) }}const styles = StyleSheet.create({ container: { flex: 1 }, list: { height: 80, backgroundColor: ‘pink’, marginBottom: 1, alignItems: ‘center’, justifyContent: ‘center’, color: ‘white’ }, header: { height: 50, backgroundColor: ‘#3F51B5’, alignItems: ‘center’, justifyContent: ‘center’ }, text: { color: ‘white’ }}) ...

December 25, 2018 · 2 min · jiezi

React-Native实际开发过程中的一些注意点

1、ES6语法不兼容for of语法,会产生一些不可预知的bug。2、从服务器获取到的配置文件XXX.bytes,直接根据pb文件解析即可,千万不要进行json解析,否则pb里的枚举值就会被转成字符串,导致获取不到预想的结果。3、做本地缓存的时候,react-native提供的AsyncStoreage只能存储字符串,所以存入缓存之前数据都要进行json序列化,但是当从缓存中取出数据,进行json解析,加入缓存之前的对象的一些默认(例如:boolean类型)值,都变成undefined了,会影响原有的判断逻辑,需要对数据进行逻辑判断的补充。4、在开发中肯定会遇到需要获取时间戳的问题,React-native中获取的时间戳是毫秒为单位,而且后面还有几位小数,打印了几条数据观察,发现都是1位小数,毫秒到的秒的转换,需要再除以1000,于是就按4位小数进行截取,let timeStamp = timeStamp.substring(0, timeStamp.length - 4);,但是在后来的程序运行中同事发现,有的时候,后面会有没有小数的情况,结果截取到的时间戳就少了一位,结果就出错了。于是他把获取时间戳的代码修改如:let timeStamp = Math.floor(new Date().getTime() / 1000).toString();,这样获取到的就是时间戳的整数部分了,且是用秒为单位的。5、react-native中的text组件,如果不设置宽度的话,默认是父组件的宽度,显示不下才会进行换行,开发中遇到遇到两种情况:一个image & text 采用横向布局,如果不设置text的宽度的话,超过父组件的宽度,右侧的文本会有丢失的情况;一个text & image 采用横向布局,如果不设置text的宽度的话,text会默认占用父组件的全部宽度,结果右侧的image被挤到了父组件之外。6、数据埋点遇到的坑点:由于服务器和客户端不在同一个时区,服务器接收到打点数据,因为时间差的原因,对客户端上报的一条数据进行了多次入库,导致数据量异常。经过商量,服务器对接收到的数据进行去重,且后续时间以服务器时间为准。7、AsyncStoreage的数据存储速度有待怀疑。在性能调优的时候发现,一些方法的执行,快的是1毫秒,大部分的发放执行在40毫秒左右,到了账号信息存储的时候,就执行了一个AsyncStorage.multiSet方法,其中是5条数据,耗时一下就变成了800毫秒左右。截图如下:上面这个方法的执行耗时是783毫秒,所以对于这里的信息存储,还是有很大的提升空间的。 AsyncStoreage给我们在开发中提供了一些数据存储的便利,但是只适合于存储少量数据,且对耗时感知不是很强的场景,对于登录和注册这种关键性步骤,AsyncStoreage的性能还有有欠缺的。

December 24, 2018 · 1 min · jiezi

react-native 上传图片和视频到阿里云, 带进度条

实现上面的效果需要的库如下react-native-image-pickerreact-native-progress先来说下是如何实现点击上传图标弹出optionModal上传选项的动画的.整个optionModal区域是一个Animated.View, render 的时候他的position 是 absolute的, 整个位置是隐藏在屏幕下的, bottom: - 200. 点击按钮之后,用Animated.spring 把 bottom 的值 调整到 0 , 这样隐藏的部分就弹出来了。const styles = StyleSheet.create({ container: { backgroundColor: theme.screen_background_color, zIndex: 100, height: 200, width: ‘100%’, position: ‘absolute’, bottom: - 200), backgroundColor: ‘rgb(255,255,255)’, alignItems: ‘center’, borderTopWidth: 1, borderTopColor: theme.divider_color, } showOptionModal() { Animated.spring( this.modalPos, { toValue: 0, } ).start();<Animated.View style={[styles.container, { bottom: this.modalPos }]}> </Animated.View>还有就是区域里面的upload video 和 upload photos 图标动画有个先后顺序,这里的处理就是upload video 这个图标用了个settimeout, 让他的动画触发晚了个 0.1秒而已。实现的方法也是跟optionModal弹出的方法一样。接下来就是选择手机里面的图片或者视频了,这里我们会用一个库叫做 react-native-image-picker, 这个库应该是大家很经常会使用到的, 就是选择手机里面的视频和图片,或者可以用摄像头拍摄图片. 当我们点击 upload photos 的时候, 触发下面的代码就可以开始选择图片了 ImagePicker.showImagePicker({ title: null, cancelButtonTitle: ‘cancel’, allowsEditing: true, chooseFromLibraryButtonTitle: ‘choose from camera roll’, takePhotoButtonTitle: type===‘video’?null:‘open camera’, mediaType: type, noData: true, quality: 0.5, videoQuality: ‘medium’, storageOptions: { skipBackup: true, cameraRoll: true, waitUntilSaved: true, }, }, (response) => { if (!response.didCancel && !response.error) { //可以在这里上传图片了 this.startUpload(response); } return null; }); }当图片选择完毕之后, 就会触发this.startUpload(response); 了, 下面就是如何处理图片上传阿里云的部分了。 在上传文件到阿里云oss 之前呢 需要从Oss获取几个配置项,OSSAccessKeyId,Signature,key, police, 可以看下Oss 文档,这个需要你自己搞定啦。startUpload(response){ const uploadMediaData = new FormData(); uploadMediaData.append(‘OSSAccessKeyId’, accessKeyId); uploadMediaData.append(‘policy’, policy); uploadMediaData.append(‘Signature’, signature); uploadMediaData.append(‘key’, key); uploadMediaData.append(‘success_action_status’, 201); uploadMediaData.append(‘file’, { uri: response.uri, type: ‘multipart/form-data’, name: response.fileName, }); // 上传成功 successResponse = (xhr) => { let returnKey = xhr.responseText.match(/<Key>([^<]*)</Key>/)[1]; //这个key 就是你上传文件在oss 的地址了, }; //上传失败 failResponse = () => { // to do }; //开始上传 const OSS_UPLOAD_URI = ‘http://xxxxx.oss-us-east-1.aliyuncs.com’ futch(OSS_UPLOAD_URI, { method: ‘POST’, body: uploadMediaData, extra: null, }, (progressEvent) => { // progress 就是上穿的进度, 更新 state 里面的uploadProgress const progress = (progressEvent.loaded / progressEvent.total); this.setState({ uploadProgress: progress, }) }, (xhr) => successResponse(xhr), failResponse) .then((res) => console.log(res), (err) => console.log(’error’ + err));}//这个方法就是具体上传的代码了futch = (url, opts = {}, onProgress, successResponse, failResponse) => { return new Promise((res, rej) => { let xhr = new XMLHttpRequest(); xhr.open(opts.method || ‘get’, url); for (let k in opts.headers || {}) xhr.setRequestHeader(k, opts.headers[k]); xhr.onload = e => res(e); xhr.onreadystatechange = (e) => { if (xhr.readyState !== 4) { return; } //阿里云的状态码201 才有返回的信息 if (xhr.status === 201) { xhr.extra = opts.extra; successResponse(xhr); } else { xhr.extra = opts.extra; failResponse(xhr); } }; xhr.onerror = rej; if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress; xhr.setRequestHeader(‘Content-Type’, ‘multipart/form-data’); xhr.send(opts.body); });}如果一切都ok 的话, 这个时候在上传的过程中就应该出现进度条了,上面提到了,this.state.uploadProgress 是用来存储上传进度的, 我们就利用这个作为进度条的数据源了, 因为我们用的是圆形的进度条,这里需要另外一个库, ’ react-native-progress’.import ProgressCircle from ‘react-native-progress/Circle’;//render 的时候只要把this.state.uploadProgress 丢进去就行了 <ProgressCircle size={30} showsText={true} size={100} progress={uploadProgress} color={‘#32CDFF’} thickness={4} borderWidth={2} style={styles.progressCircle} />这里特别提出一下,oss 的上传成功的返回代码是201。这样整个过程就结束了, 其中有些细节没提出来,需要大家自己完善,但是大概的流程应该都写出来了. 希望对大大家有用。 ...

December 21, 2018 · 2 min · jiezi

电影天堂RN客户端V2.0发布

电影天堂RN客户端V2.0重新开始!重新开始两年前发布了第一个版本。现在,使用最新的react-native 0.57和全新的设计完成了V2.0免责声明本项目仅供学习交流使用,不得用于其他商业行为,数据来源于第三方网站,与本人无关为什么要重新开始呢有很多小伙伴发邮件问我为什么之前的项目运行不起来。其实这个是我自己的原因,之前做的时候没什么经验,很多时候就直接修改了第三方库,所以就运行不起来了还有就是第三方api也做了很大的变动react-native和其他第三方库都更新了许多,正好重新开始,把一些新特性都利用起来(比如context),完整的来做一个项目,这比单纯的学习看文章要有效的多正常的app本来就是需要长期维护更新的,只不过由于是个人项目,很多时候完成一个阶段就会因为各种原因而被耽搁,精力有限实属无奈特色大概是全网个人影视类项目最漂亮、体验最好的了吧(下方有截图~)。最为一名偏体验偏设计的前端开发者,对界面和用户体验都有极高的重视。(见过很多类似的,功能算是出来了,但是界面一看就是程序员风格)演示视频项目依赖依赖项不多,大部分都是用原生自带组件完成{ “name”: “DYTT”, “version”: “2.0.0”, “private”: true, “scripts”: { “start”: “node node_modules/react-native/local-cli/cli.js start”, “test”: “jest” }, “dependencies”: { “react”: “16.6.1”, “react-native”: “0.57.5”, “react-native-gesture-handler”: “^1.0.9”, “react-native-scrollviewpager”: “^1.0.3”, “react-native-splash-screen”: “^3.1.1”, “react-native-swiper”: “^1.5.14”, “react-native-vector-icons”: “^6.1.0”, “react-native-video”: “^4.0.1”, “react-navigation”: “^3.0.0” }, “devDependencies”: { “babel-jest”: “23.6.0”, “jest”: “23.6.0”, “metro-react-native-babel-preset”: “0.49.2”, “react-test-renderer”: “16.6.1” }, “jest”: { “preset”: “react-native” }}安装github 项目地址本项目适用于相关技术人员学习交流,请自行编译安装git clone https://github.com/XboxYan/DYTT.gitcd DYTTyarnreact-native run-android下载目前只有安卓版本下载,需要ios的可以自行编译安装下载链接二维码(微信扫码可能不支持,建议用其他扫描工具或者直接用浏览器打开上面链接)考虑到安全问题,暂不提供安装包,可通过上述方式安装,或者与我联系提供安装包相关截图欢迎页首页功能菜单历史记录收藏主题颜色搜索搜索结果影片筛选影片详情影片播放更新记录记录一些页面的关键点20181123使用react-navigation作为导航/App.js由于新版导航用到了原生手势库,所以需要yarn add react-native-gesture-handlerreact-native link react-native-gesture-handler整体导航结构如下const Drawer = createDrawerNavigator({ Index: Index, History: History, Follow: Follow, Theme: Theme,},DrawerNavigatorConfig);const App = createAppContainer(createStackNavigator({ Drawer: Drawer, Search: Search, MovieContent: MovieContent, MovieDetail: MovieDetail, Comment: Comment,}, StackNavigatorConfig));tab切换使用的是本人封装导航器react-native-scrollviewpagerhttps://github.com/XboxYan/react-native-scrollviewpager有兴趣的可以给个star使用方式比较简单yarn add react-native-scrollviewpagerimport Scrollviewpager from ‘react-native-scrollviewpager’;const tabBarOptions = (themeColor) => ({ style:{ paddingHorizontal:10, height:40, backgroundColor:’#fff’ }, labelStyle:{ color:’#666’ }, activeTintColor:themeColor, indicatorStyle:{ width:20, borderRadius:4, height:3, backgroundColor:themeColor, bottom:2 }})//<Scrollviewpager tabBarOptions={tabBarOptions(themeColor)} > <Text tablabel=“首页”>111</Text> <Text tablabel=“电影”>111</Text> <Text tablabel=“动漫”>111</Text></Scrollviewpager> //20181125使用context管理全局数据/util/store.js历史记录,收藏,主题(废弃,下面有其他方式实现)export const Store = createContext(initialStore);<Store.Provider value={{ …initialStore}}> {this.props.children}</Store.Provider>20181127影视详情页面./src/page/MovieDetail.js头部滚动跟随效果使用Animated.ScrollView实现scrollTop = new Animated.Value(0);//…<Animated.ScrollView scrollEventThrottle={1} onScroll={Animated.event( [{ nativeEvent: { contentOffset: { y: this.scrollTop } } }], { useNativeDriver: true } )}></Animated.ScrollView>//…视频播放器自定义外观./src/components/Video.js使用开源播放器react-native-videohttps://github.com/react-native-community/react-native-video这里有一个bugsource={{uri:uri}},uri不能为空字符串,否则切换资源部生效支持手势快进快退,自动隐藏播放栏还未完成的功能全屏切换20181203主题颜色./App.js、./src/page/Theme.jsreact-navigation内置属性screenProps,其原理仍然使用的context特性<App screenProps={{themeColor:themeColor, setTheme:this.setTheme}} />调用方式const {navigation,screenProps:{themeColor}} = this.props;20181204历史记录./src/page/History.js通过context传递数据,需设置contextTypeimport { Store } from ‘../../util/store’;export default class History extends PureComponent { render() { const { historyList } = this.context; return ( //… ) }}History.contextType = Store;20181205收藏页面./src/page/Follow.js与’历史记录’基本一致20181206本地存储./util/storage.js使用原生AsyncStorageclass Storage { /** * 获取 / static get = async (key) => { try { const value = await AsyncStorage.getItem(key); if (value !== null) { // We have data!! return JSON.parse(value) } else { return false } } catch (error) { return false } } /* * 保存 */ static save = async (key, value) => { try { await AsyncStorage.setItem(key, JSON.stringify(value)); return true } catch (error) { // Error saving data return false } }}export default Storage;20181209搜索./src/page/Search.js20181211影片筛选./src/page/MovieContent.js使用侧边导航栏,调用方式与原生DrawerLayoutAndroid一致import DrawerLayout from ‘react-native-gesture-handler/DrawerLayout’;20181214图标,启动图使用开源库react-native-splash-screenhttps://github.com/crazycodeboy/react-native-splash-screen#readme如果需要白底深色的状态栏文字<style name=“SplashScreenTheme” parent=“SplashScreen_SplashTheme”> <item name=“android:windowIsTranslucent”>true</item> <item name=“colorPrimaryDark”>@color/status_bar_color</item> <item name=“android:windowLightStatusBar”>true</item><!–加上这一句–></style>2.0 基本完成20181217安卓打包./android/build.gradle修改一下配置注释jcenter(),添加maven{ url ‘http://maven.aliyun.com/nexus/content/groups/public/'}maven{ url ‘https://jitpack.io’ }不然会卡在下载阶段…allprojects { repositories { mavenLocal() google() //jcenter() //更换国内镜像 maven{ url ‘http://maven.aliyun.com/nexus/content/groups/public/'} maven{ url ‘https://jitpack.io’ } maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url “$rootDir/../node_modules/react-native/android” } }}…常用命令# 卸载安装包adb uninstall com.packgeName# 生成Release包gradlew assembleRelease# 安装Release包gradlew installRelease注意:在 debug 和 release 版本间来回切换安装时可能会报错签名不匹配,此时需要先卸载前一个版本再尝试安装。可通过 adb uninstall com.packgeName 方式来卸载,直接通过长按桌面图标有可能卸载不干净还未完成的还接下来要做的视频播放做全屏切换没有适配ios,不过代码中没有使用安卓专有的库,理论上可以直接运行(可能有少部分需要适配),有兴趣的小伙伴可以fork下来自己适配一下会新增设置选项,进行网络设置,播放设置等(会参考其他视频软件的功能)目前历史记录和收藏均保存在本地,意味着如果卸载app将导致数据丢失,如果可能的话,将来把数据保存在自己的服务器上react-navigation在页面切换时略微卡顿,还有一个react-native-navigation,如果可能的话,可以用来替代react-navigation目前在网上找的api可能不够理想,如果谁有更好的设计和更好的api可以参考一下~如果有提供后台服务的就更好了react-native确实性能略显不足,特别是长列表的情况,准备学习flutter,一种新的渲染方式(类似于web中的canvas)联系方式有什么问题可以与我联系yanwenbin1991@live.com或者直接提 issue原文地址https://blog.codelabo.cn/article/5c18911f8aab210ff34d0147https://github.com/XboxYan/DYTT打赏精神支撑一下,给个 star 呗如果体验觉得还不错的话,大佬们可以随意打赏,金额不限微信支付宝 ...

December 18, 2018 · 2 min · jiezi

React Native 结合友盟实现分享

前言如今,分享已经是app必不可少的一项功能,前段时间在react native中实现了社会化分享的功能,在这里记录一下实现过程,希望可以帮到有需要的人。此文章会记录安卓和IOS 两个平台分别实现分享的步骤,内容可能会有点长,可根据需要查看。技术调研在搜罗了网上大部分 react native实现分享的技术文章中,大部分文章较为老旧,实现方式也跟原生app相仿,需要自己编写大量的Object-C 以及java代码,对于纯前端开发的人来说时间成本和难度有点高,有幸,在友盟官网上发现了有针对于react native的社会化分享sdk,研究了一番,可行,开始。准备这里默认你已经搭建好react native的开发环境。注册友盟账号去友盟官网注册账号,建议使用公司邮箱。关于开发者认证,暂时不需要企业认证可以免费试用大部分功能。点击立即使用即可前往控制台,首先要新建你的应用。不同平台的应用禁止使用相同的Appkey,需要分开注册,应用名与实际应用名和包名无关,建议命名为应用名+平台(iOS/Android)目的是拿到AppKey。下载SDK在弹框选择对应的分享平台,本文只针对 微信、QQ、新浪微博的精简版说明。在各大平台注册应用关于申请的流程官网文档有介绍,有几点需要注意:各大平台申请服务所需要等待的时间不等,通常是1-5天即可通过,申请的同时可以集成sdk。新浪的开发者账号申请比较麻烦,但也是最快可以拿到appkey的。React native的集成关于集成文档,友盟已经给出,按照官方文档的步骤集成即可,只不过集成完react native 的部分才算一小部分,大部分集成都需要按照ios和安卓平台的文档分别集成,鉴于官方文档写的云里雾里,新手可直接参考我这里的步骤。友盟集成文档React Native Android 集成安卓集成比较麻烦,建议clone 官方示例demo对照设置。初始化用android studio 打开你的Android目录,app目录中新建libs文件夹,将下载的jar放入libs中。首先需要新建一个文件夹来存放这些.java文件,在你的项目如下目录中新建 umeng文件夹:拷贝common_android文件夹中的文件拷贝到umeng文件夹:然后再将对应平台的桥接文件拷入umeng文件夹:注意:官方示例中桥接文件的路径默认是 com.umeng.soexample.invokenative,要修改成自己的路径。改为自己的路径:打开MainApplication.java文件,在new MainReactPackage()后添加一行 new DplusReactPackage(); private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new DplusReactPackage() ); } };并在onCreate()中进行初始化: @Override public void onCreate() { super.onCreate(); SoLoader.init(this, /* native exopackage / false); RNUMConfigure.init(this, “59892f08310c9307b60023d0”, “Umeng”, UMConfigure.DEVICE_TYPE_PHONE, “669c30a9584623e70e8cd01b0381dcb4”); }RNUMConfture.init接口一共五个参数,其中第一个参数为Context,第二个参数为友盟Appkey,第三个参数为channel,第四个参数为应用类型(手机或平板),第五个参数为push的secret(如果没有使用push,可以为空)。在友盟后台注册完app后即可把 android 的appkey填入。至此,react native 的安卓工程配置已经完成,接下来要按照Android的 U-share文档集成。Android 部分集成官方文档说Android集成包含快速集成和手动集成,但是关于快速集成毛都没讲,我们使用手动集成。导入res将 main 和platfroms文件夹下的res资源全部导入工程中。res下没有相关目录的话直接拷贝文件夹过去。添加完毕:微信的回调需要新建文件夹在包名目录下创建wxapi文件夹,新建文件WXEntryActivity的activity.java写入以下内容(com.share.umeng要改成你的包路径):package com.share.umeng;import com.umeng.socialize.weixin.view.WXCallbackActivity;public class WXEntryActivity extends WXCallbackActivity {}QQ与新浪QQ与新浪不需要添加Activity,但需要在MainActivity.java文件中修改如下:public class MainActivity extends ReactActivity { /* * Returns the name of the main component registered from JavaScript. * This is used to schedule rendering of the component. */ @Override protected String getMainComponentName() { return “share”; } // 添加以下代码 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); UMShareAPI.get(this).onActivityResult(requestCode, resultCode, data); }}注意onActivityResult不可在fragment中实现,如果在fragment中调用登录或分享,需要在fragment依赖的Activity中实现。配置Android Manifest XMLsdk中需要的Activity新浪: <activity android:name=“com.umeng.socialize.media.WBShareCallBackActivity” android:configChanges=“keyboardHidden|orientation” android:theme="@android:style/Theme.Translucent.NoTitleBar" android:exported=“false” > </activity> <activity android:name=“com.sina.weibo.sdk.web.WeiboSdkWebActivity” android:configChanges=“keyboardHidden|orientation” android:exported=“false” android:windowSoftInputMode=“adjustResize” > </activity> <activity android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen" android:launchMode=“singleTask” android:name=“com.sina.weibo.sdk.share.WbShareTransActivity”> <intent-filter> <action android:name=“com.sina.weibo.sdk.action.ACTION_SDK_REQ_ACTIVITY” /> <category android:name=“android.intent.category.DEFAULT” /> </intent-filter> </activity>微信: <activity android:name=".wxapi.WXEntryActivity" android:configChanges=“keyboardHidden|orientation|screenSize” android:exported=“true” android:theme="@android:style/Theme.Translucent.NoTitleBar" />权限添加在AndroidManifest.xml中添加如下权限:<uses-permission android:name=“android.permission.ACCESS_NETWORK_STATE” /><uses-permission android:name=“android.permission.ACCESS_WIFI_STATE” /><uses-permission android:name=“android.permission.INTERNET” />如果需要使用QQ纯图分享或避免其它平台纯图分享的时候图片不被压缩,可以增加以下权限: <uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE”/> <uses-permission android:name=“android.permission.READ_EXTERNAL_STORAGE”/>Android6.0权限适配查看你的build.gradle文件,如果 targetSdkVersion小于或等于22,可以忽略这一步,如果大于或等于23,需要做权限的动态申请:if(Build.VERSION.SDK_INT>=23){ String[] mPermissionList = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.CALL_PHONE,Manifest.permission.READ_LOGS,Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.SET_DEBUG_APP,Manifest.permission.SYSTEM_ALERT_WINDOW,Manifest.permission.GET_ACCOUNTS,Manifest.permission.WRITE_APN_SETTINGS}; ActivityCompat.requestPermissions(this,mPermissionList,123); }其中123是requestcode,可以根据这个code判断,用户是否同意了授权。如果没有同意,可以根据回调进行相应处理:@Overridepublic void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {}初始化设置由于之前已经在在onCreate()方法中替换友盟的appkey,还需要替换各大平台的Appkey 和 App Secret打开MainApplication.java文件,在onCreate()方法后添加各平台的appkey:{//豆瓣RENREN平台目前只能在服务器端配置 PlatformConfig.setWeixin(“wxdc1e388c3822c80b”, “3baf1193c85774b3fd9d18447d76cab0”); PlatformConfig.setSinaWeibo(“3921700954”, “04b48b094faeb16683c32669824ebdad”, “http://sns.whalecloud.com”); PlatformConfig.setQQZone(“100424468”, “c7394704798a158208a74ab60104f0ba”); }签名配置将文件夹中的签名文件放入到工程中,一般是debug.keystore如何生成签名? 打开build.gradle然后增加签名文件的密码:signingConfigs { debug { storeFile file(‘debug.keystore’) storePassword “android” keyAlias “androiddebugkey” keyPassword “android” }}然后在buildTypes中将这个signingConfigs配置进去,如下图所示:至此,android 部分的 集成结束,更多高级功能请去参考官方文档。React Native IOS 集成ios集成文档ios的集成也非分为手动集成和自动集成,自动集成使用Cocoapods。通过 Cocoapods 方式集成请参考文档 Cocoapods集成分享SDK。由于我之前是通过手动集成的,所以本文讲解手动集成步骤,但是对于ios来说更推荐使用 Cocoapods。进入ios > 项目目录,新建两个文件夹UMReactBridge和UMComponent。打开之前下载的文件,把share目录中的最后所有文件,拷入UMComponent中的新建文件夹UMShare中。这里有一个问题。通过选择react native 下载的sdk包中ios目录中缺少common部分的文件,所以还需要去sdk下载中心去下载ios的sdk。下载之后,将ios目录下的common中的framework,拷入UMComponent中。同时有两个log相关的文件,一并导入,便于开发时可以在xcode中查看详细的日志。进入下载文件的ReactNative目录,找到commonshare目录中对应的ios平台中的桥接.h .m文件,全部拷贝至我们项目刚刚新建的UMReactBridge文件夹。xcode中打开工程目录,右键黄色项目名Add Files to “xxx”,options中选中Create groups Copy items if needed找到我们新建的UMReactBridge和UMComponent,add添加。在Other Linker Flags加入-ObjC ,注意不要写为-Objc,注:-ObjC属于链接库必备参数,如果不加此项,会导致库文件无法被正确链接,SDK无法正常运行。加入依赖系统库加入以下系统库:libsqlite3.tbdCoreGraphics.framework日志依赖库:Foundation.framework[日志详细配置](https://developer.umeng.com/d…第三方平台库添加(精简版): 新浪微博(精简版)Photos.framework第三方平台配置配置SSO白名单如果你的应用使用了如SSO授权登录或跳转到第三方分享功能,在iOS9/10下就需要增加一个可跳转的白名单,即LSApplicationQueriesSchemes,否则将在SDK判断是否跳转时用到的canOpenURL时返回NO,进而只进行webview授权或授权/分享失败。在项目中的info.plist中加入应用白名单,右键info.plist选择source code打开,请根据选择的平台对以下配置进行裁剪:<key>LSApplicationQueriesSchemes</key><array> <!– 微信 URL Scheme 白名单–> <string>wechat</string> <string>weixin</string> <!– 新浪微博 URL Scheme 白名单–> <string>sinaweibohd</string> <string>sinaweibo</string> <string>sinaweibosso</string> <string>weibosdk</string> <string>weibosdk2.5</string> <!– QQ、Qzone URL Scheme 白名单–> <string>mqqapi</string> <string>mqq</string> <string>mqqOpensdkSSoLogin</string> <string>mqqconnect</string> <string>mqqopensdkdataline</string> <string>mqqopensdkgrouptribeshare</string> <string>mqqopensdkfriend</string> <string>mqqopensdkapi</string> <string>mqqopensdkapiV2</string> <string>mqqopensdkapiV3</string> <string>mqqopensdkapiV4</string> <string>mqzoneopensdk</string> <string>wtloginmqq</string> <string>wtloginmqq2</string> <string>mqqwpa</string> <string>mqzone</string> <string>mqzonev2</string> <string>mqzoneshare</string> <string>wtloginqzone</string> <string>mqzonewx</string> <string>mqzoneopensdkapiV2</string> <string>mqzoneopensdkapi19</string> <string>mqzoneopensdkapi</string> <string>mqqbrowser</string> <string>mttbrowser</string> <string>tim</string> <string>timapi</string> <string>timopensdkfriend</string> <string>timwpa</string> <string>timgamebindinggroup</string> <string>timapiwallet</string> <string>timOpensdkSSoLogin</string> <string>wtlogintim</string> <string>timopensdkgrouptribeshare</string> <string>timopensdkapiV4</string> <string>timgamebindinggroup</string> <string>timopensdkdataline</string> <string>wtlogintimV1</string> <string>timapiV1</string> <!– 支付宝 URL Scheme 白名单–> <string>alipay</string> <string>alipayshare</string> <!– 钉钉 URL Scheme 白名单–> <string>dingtalk</string> <string>dingtalk-open</string> <!–Linkedin URL Scheme 白名单–> <string>linkedin</string> <string>linkedin-sdk2</string> <string>linkedin-sdk</string> <!– 点点虫 URL Scheme 白名单–> <string>laiwangsso</string> <!– 易信 URL Scheme 白名单–> <string>yixin</string> <string>yixinopenapi</string> <!– instagram URL Scheme 白名单–> <string>instagram</string> <!– whatsapp URL Scheme 白名单–> <string>whatsapp</string> <!– line URL Scheme 白名单–> <string>line</string> <!– Facebook URL Scheme 白名单–> <string>fbapi</string> <string>fb-messenger-api</string> <string>fb-messenger-share-api</string> <string>fbauth2</string> <string>fbshareextension</string> <!– Kakao URL Scheme 白名单–> <!– 注:以下第一个参数需替换为自己的kakao appkey–> <!– 格式为 kakao + “kakao appkey”–> <string>kakaofa63a0b2356e923f3edd6512d531f546</string> <string>kakaokompassauth</string> <string>storykompassauth</string> <string>kakaolink</string> <string>kakaotalk-4.5.0</string> <string>kakaostory-2.9.0</string> <!– pinterest URL Scheme 白名单–> <string>pinterestsdk.v1</string> <!– Tumblr URL Scheme 白名单–> <string>tumblr</string> <!– 印象笔记 –> <string>evernote</string> <string>en</string> <string>enx</string> <string>evernotecid</string> <string>evernotemsg</string> <!– 有道云笔记–> <string>youdaonote</string> <string>ynotedictfav</string> <string>com.youdao.note.todayViewNote</string> <string>ynotesharesdk</string> <!– Google+–> <string>gplus</string> <!– Pocket–> <string>pocket</string> <string>readitlater</string> <string>pocket-oauth-v1</string> <string>fb131450656879143</string> <string>en-readitlater-5776</string> <string>com.ideashower.ReadItLaterPro3</string> <string>com.ideashower.ReadItLaterPro</string> <string>com.ideashower.ReadItLaterProAlpha</string> <string>com.ideashower.ReadItLaterProEnterprise</string> <!– VKontakte–> <string>vk</string> <string>vk-share</string> <string>vkauthorize</string> <!– Twitter–> <string>twitter</string> <string>twitterauth</string></array>配置URL SchemeURL Scheme是通过系统找到并跳转对应app的一类设置,通过向项目中的info.plist文件中加入URL types可使用第三方平台所注册的appkey信息向系统注册你的app,当跳转到第三方应用授权或分享后,可直接跳转回你的app。部分规则:微信:微信appKey wxdc1e388c3822c80bQQ/Qzone: 需要添加两项URL Scheme:1、“tencent”+腾讯QQ互联应用appID2、“QQ”+腾讯QQ互联应用appID转换成十六进制(不足8位前面补0)如appID:100424468 1、tencent100424468 2、QQ05fc5b14在线转换新浪微博: “wb”+新浪appKey wb3921700954权限配置在 info.plist 文件中配置相册权限: <key>NSPhotoLibraryUsageDescription</key> <string>App需要您的同意,才能访问相册</string>初始化设置AppDelegate.m设置友盟appkey以及各个平台的appkey和secret。#import <UMShare/UMShare.h>- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { // UMConfigure 通用设置,请参考SDKs集成做统一初始化。 // 以下仅列出U-Share初始化部分 // U-Share 平台设置 [self configUSharePlatforms]; [self confitUShareSettings]; // Custom code return YES;}- (void)confitUShareSettings{ / * 打开图片水印 / //[UMSocialGlobal shareInstance].isUsingWaterMark = YES; / * 关闭强制验证https,可允许http图片分享,但需要在info.plist设置安全域名 <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> / //[UMSocialGlobal shareInstance].isUsingHttpsWhenShareContent = NO;}- (void)configUSharePlatforms{ / 设置微信的appKey和appSecret / [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_WechatSession appKey:@“wxdc1e388c3822c80b” appSecret:@“3baf1193c85774b3fd9d18447d76cab0” redirectURL:@“http://mobile.umeng.com/social”]; / * 移除相应平台的分享,如微信收藏 / //[[UMSocialManager defaultManager] removePlatformProviderWithPlatformTypes:@[@(UMSocialPlatformType_WechatFavorite)]]; / 设置分享到QQ互联的appID * U-Share SDK为了兼容大部分平台命名,统一用appKey和appSecret进行参数设置,而QQ平台仅需将appID作为U-Share的appKey参数传进即可。 / [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_QQ appKey:@“1105821097”/设置QQ平台的appID/ appSecret:nil redirectURL:@“http://mobile.umeng.com/social”]; / 设置新浪的appKey和appSecret / [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Sina appKey:@“3921700954” appSecret:@“04b48b094faeb16683c32669824ebdad” redirectURL:@“https://sns.whalecloud.com/sina2/callback”]; / 钉钉的appKey / [[UMSocialManager defaultManager] setPlaform: UMSocialPlatformType_DingDing appKey:@“dingoalmlnohc0wggfedpk” appSecret:nil redirectURL:nil]; / 支付宝的appKey / [[UMSocialManager defaultManager] setPlaform: UMSocialPlatformType_AlipaySession appKey:@“2015111700822536” appSecret:nil redirectURL:@“http://mobile.umeng.com/social”]; / 设置易信的appKey / [[UMSocialManager defaultManager] setPlaform: UMSocialPlatformType_YixinSession appKey:@“yx35664bdff4db42c2b7be1e29390c1a06” appSecret:nil redirectURL:@“http://mobile.umeng.com/social”]; / 设置点点虫(原来往)的appKey和appSecret / [[UMSocialManager defaultManager] setPlaform: UMSocialPlatformType_LaiWangSession appKey:@“8112117817424282305” appSecret:@“9996ed5039e641658de7b83345fee6c9” redirectURL:@“http://mobile.umeng.com/social”]; / 设置领英的appKey和appSecret / [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Linkedin appKey:@“81t5eiem37d2sc” appSecret:@“7dgUXPLH8kA8WHMV” redirectURL:@“https://api.linkedin.com/v1/people”]; / 设置Twitter的appKey和appSecret / [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Twitter appKey:@“fB5tvRpna1CKK97xZUslbxiet” appSecret:@“YcbSvseLIwZ4hZg9YmgJPP5uWzd4zr6BpBKGZhf07zzh3oj62K” redirectURL:nil]; / 设置Facebook的appKey和UrlString / [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Facebook appKey:@“506027402887373” appSecret:nil redirectURL:@“http://www.umeng.com/social”]; / 设置Pinterest的appKey / [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Pinterest appKey:@“4864546872699668063” appSecret:nil redirectURL:nil]; / dropbox的appKey / [[UMSocialManager defaultManager] setPlaform: UMSocialPlatformType_DropBox appKey:@“k4pn9gdwygpy4av” appSecret:@“td28zkbyb9p49xu” redirectURL:@“https://mobile.umeng.com/social”]; / vk的appkey */ [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_VKontakte appKey:@“5786123” appSecret:nil redirectURL:nil];}- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url]; if (!result) { // 其他如支付等SDK的回调 } return result; }- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url]; if (!result) { // 其他如支付等SDK的回调 } return result; }对应的方法在UMShareModule.m中查看。调试模式与日志之前已经加过日志库的资源文件了。开启日志: [UMConfigure setLogEnabled:YES];使用分享至此,ios和adnroid平台的集成和工程配置基本完成,最后一步就是使用原生代码导出的分享模块供js调用。进入下载目录的ReactNative找到common下的js中的ShareUtil.js,拷贝到我们RN目录下,放入libs文件夹。android与ios平台回调中的code值不一致,ios成功时code:200,android成功时code:0。首先需要引入ShareUtil文件:import ShareUtile from ‘./ShareUtil’授权授权代码可以直接使用ShareUtile.auth(platform,callback),其中platform为平台id,callback为回调内容。[详细对应关系](https://developer.umeng.com/d…官方文档给出了三种分享回调,在这里使用 ShareUtile.shareboard调起分享面板实现分享。 ShareUtile.shareboard(text,img,url,title,list,(code,message) =>{ this.setState({result:message}); });text 为分享内容img 为图片地址,可以为链接,本地地址以及res图片(如果使用res,请使用如下写法:res/icon.png)url 为分享链接,可以为空title 为分享链接的标题list 为分享平台数组,如:var list = [0,1,2]callback中code为错误码,当为0时,标记成功。-message为错误信息 ...

December 17, 2018 · 4 min · jiezi

react-native-storage文档介绍

中文doc:仅供参考import Storage from ‘react-native-storage’;import {AsyncStorage} from ‘react-native’;var storage = new Storage({ // 最大容量,默认值1000条数据循环存储 size: 1000, // 存储引擎:对于RN使用AsyncStorage,对于web使用window.localStorage // 如果不指定则数据只会保存在内存中,重启后即丢失 storageBackend: AsyncStorage, // 数据过期时间,默认一整天(1000 * 3600 * 24 毫秒),设为null则永不过期 defaultExpires: 1000 * 3600 * 24, // 读写时在内存中缓存数据。默认启用。 enableCache: true, // 如果storage中没有相应数据,或数据已过期, // 则会调用相应的sync方法,无缝返回最新数据。 // sync方法的具体说明会在后文提到 // 你可以在构造函数这里就写好sync的方法 // 或是在任何时候,直接对storage.sync进行赋值修改 // 或是写到另一个文件里,这里require引入 // sync: require(‘你可以另外写一个文件专门处理sync’)})// 最好在全局范围内创建一个(且只有一个)storage实例,方便直接调用// 对于web// window.storage = storage;// 对于react native// global.storage = storage;// 这样,在此之后的任意位置即可以直接调用storage// 注意:全局变量一定是先声明,后使用// 如果你在某处调用storage报错未定义// 请检查global.storage = storage语句是否确实已经执行过了// 使用key来保存数据。这些数据一般是全局独有的,常常需要调用的。// 除非你手动移除,这些数据会被永久保存,而且默认不会过期。storage.save({ key: ’loginState’, // 注意:请不要在key中使用_下划线符号! data: { from: ‘some other site’, userid: ‘some userid’, token: ‘some token’ }, // 如果不指定过期时间,则会使用defaultExpires参数 // 如果设为null,则永不过期 expires: 1000 * 3600});// 读取storage.load({ key: ’loginState’, // autoSync(默认为true)意味着在没有找到数据或数据过期时自动调用相应的sync方法 autoSync: true, // syncInBackground(默认为true)意味着如果数据过期, // 在调用sync方法的同时先返回已经过期的数据。 // 设置为false的话,则等待sync方法提供的最新数据(当然会需要更多时间)。 syncInBackground: true, // 你还可以给sync方法传递额外的参数 syncParams: { extraFetchOptions: { // 各种参数 }, someFlag: true, },}).then(ret => { // 如果找到数据,则在then方法中返回 // 注意:这是异步返回的结果(不了解异步请自行搜索学习) // 你只能在then这个方法内继续处理ret数据 // 而不能在then以外处理 // 也没有办法“变成”同步返回 // 你也可以使用“看似”同步的async/await语法 console.log(ret.userid); this.setState({user: ret});}).catch(err => { //如果没有找到数据且没有sync方法, //或者有其他异常,则在catch中返回 console.warn(err.message); switch (err.name) { case ‘NotFoundError’: // TODO; break; case ‘ExpiredError’: // TODO break; }})// 使用key和id来保存数据,一般是保存同类别(key)的大量数据。// 所有这些"key-id"数据共有一个保存上限(无论是否相同key)// 即在初始化storage时传入的size参数。// 在默认上限参数下,第1001个数据会覆盖第1个数据。// 覆盖之后,再读取第1个数据,会返回catch或是相应的sync方法。var userA = { name: ‘A’, age: 20, tags: [ ‘geek’, ’nerd’, ‘otaku’ ]};storage.save({ key: ‘user’, // 注意:请不要在key中使用_下划线符号! id: ‘1001’, // 注意:请不要在id中使用_下划线符号! data: userA, expires: 1000 * 60});//load 读取storage.load({ key: ‘user’, id: ‘1001’}).then(ret => { // 如果找到数据,则在then方法中返回 console.log(ret.userid);}).catch(err => { // 如果没有找到数据且没有sync方法, // 或者有其他异常,则在catch中返回 console.warn(err.message); switch (err.name) { case ‘NotFoundError’: // TODO; break; case ‘ExpiredError’: // TODO break; }})// ————————————————–// 获取某个key下的所有idstorage.getIdsForKey(‘user’).then(ids => { console.log(ids);});// 获取某个key下的所有数据storage.getAllDataForKey(‘user’).then(users => { console.log(users);});// !! 清除某个key下的所有数据storage.clearMapForKey(‘user’);// ————————————————–// 删除单个数据storage.remove({ key: ’lastPage’});storage.remove({ key: ‘user’, id: ‘1001’});// !! 清空map,移除所有"key-id"数据(但会保留只有key的数据)storage.clearMap();//同步远程数据(刷新)storage.sync = { // sync方法的名字必须和所存数据的key完全相同 // 方法接受的参数为一整个object,所有参数从object中解构取出 // 这里可以使用promise。或是使用普通回调函数,但需要调用resolve或reject。 user(params){ let {id, resolve, reject, syncParams: {extraFetchOptions, someFlag}} = params; fetch(‘user/’, { method: ‘GET’, body: ‘id=’ + id, …extraFetchOptions, }).then(response => { return response.json(); }).then(json => { //console.log(json); if (json && json.user) { storage.save({ key: ‘user’, id, data: json.user }); if (someFlag) { // 根据syncParams中的额外参数做对应处理 } // 成功则调用resolve resolve && resolve(json.user); } else { // 失败则调用reject reject && reject(new Error(‘data parse error’)); } }).catch(err => { console.warn(err); reject && reject(err); }); }}//有了上面这个sync方法,以后再调用storage.load时,如果本地并没有存储相应的user,那么会自动触发storage.sync.user去远程取回数据并无缝返回。storage.load({ key: ‘user’, id: ‘1002’}).then()//读取批量数据// 使用和load方法一样的参数读取批量数据,但是参数是以数组的方式提供。// 会在需要时分别调用相应的sync方法,最后统一返回一个有序数组。storage.getBatchData([ {key: ’loginState’}, {key: ‘checkPoint’, syncInBackground: false}, {key: ‘balance’}, {key: ‘user’, id: ‘1009’}]) .then(results => { results.forEach(result => { console.log(result); }) })//根据key和一个id数组来读取批量数据storage.getBatchDataWithIds({ key: ‘user’, ids: [‘1001’, ‘1002’, ‘1003’]}) .then()/* 这两个方法除了参数形式不同,还有个值得注意的差异。getBatchData会在数据缺失时挨个调用不同的sync方法(因为key不同)。 但是getBatchDataWithIds却会把缺失的数据统计起来,将它们的id收集到一个数组中,然后一次传递给对应的sync方法(避免挨个查询导致同时发起大量请求), 所以你需要在服务端实现通过数组来查询返回,还要注意对应的sync方法的参数处理(因为id参数可能是一个字符串,也可能是一个数组的字符串)。 */ ...

December 17, 2018 · 2 min · jiezi

分享一个 React-Native 气泡球组件

github 地址分享一个 React Native 气泡球组件,球在屏幕内按照随机速度移动,球可以由图片和 <View /> 生成。特征:球的数量可配置球的大小可配置球的速度可配置样式可配置效果如下:

December 14, 2018 · 1 min · jiezi

ReactNative入门教程-组件生命周期函数

1.组件实例化阶段defaultProps:设置组件的初始属性值,比如设置默认Color,width等,可以在通过this.props获取相应的值constructor(props):这里通过this.props可以获取defaultProps设置的默认属性值,同时这里用于初始化控件的可变化的变量,通过this.state设置变量的初始值,通过this.setState()函数修改变量的值,调用render()函数重新渲染页面,得到新的页面componentWillMount:组件将要被加载到视图之前调用render(): 第一次被调用,用于渲染页面componentDidMount:在调用了render方法,组件加载完成并被成功渲染出来之后,所要执行的后续操作,一般都会在这个函数中进行,比如经常要面对的网络请求等加载数据操作,因为UI渲染是异步的,所以在这个函数里面进行网络请求,能够避免出现UI错误。2.组件运行时阶段组件的属性prop和状态state任何一个改变都可能会触发render()函数渲染页面componentWillReceiveProps:指父元素对组件的props进行了修改shouldComponentUpdate一般用于优化性能,通过业务逻辑判断返回true或false,来决定页面是否进行重新绘制,默认返回true,执行后面两个周期函数componentWillUpdate:组件刷新前调用componentDidUpdate:更新后3.页面卸载页面:componentWillUnmount一般用于清理工作,比如移除事件监听,取消定时器等4.生命周期函数调用次数特别提示:更新state必须使用setState()函数,setState是一个异步的函数:setState(update,[callback])setState()不是立刻更新组件。其可能是批处理或推迟更新。这使得在调用setState()后立刻读取this.state的一个潜在陷阱。代替地,使用componentDidUpdate或一个setState回调(setState(updater, callback)),当中的每个方法都会保证在更新被应用之后触发。参考文档:https://react.docschina.org/d…个人网站:https://wayne214.github.io

December 14, 2018 · 1 min · jiezi

React Native JSBundle拆包之原理篇

概述RN作为一款非常优秀的移动端跨平台开发框架,在近几年得到众多开发者的认可。纵观现在接入RN的大厂,如qq音乐、菜鸟、去哪儿,无疑不是将RN作为重点技术栈进行研发。不过,熟悉RN的开发者也知道,早期的RN版本中打出来的包都只有一个jsbundle,而这个jsbundle里面包含了所有代码(RN源码、第三方库代码和自己的业务代码)。如果是纯RN代码倒没什么关系,但大部分的大厂都是在原生应用内接入RN的,而且一个RN中又包含许多不同的业务,这些不同的业务很可能是不同部门开发的,这样一个库中就有许许多多的重复的RN代码和第三方库代码。所以,一般做法都是将重复的RN代码和第三方库打包成一个基础包,然后各个业务在基础包的基础上进行开发,这样做的好处是可以降低对内存的占用,减少加载时间,减少热更新时流量带宽等,在优化方面起到了非常大的作用。拆包流派moles-packermoles-packer 是由携程框架团队研发的,与携程moles框架配套使用的React Native 打包和拆包工具,同时支持原生的 React Native 项目。特点:重写了react native自带的打包工具,适合RN0.4.0版本之前的分包。维护少,现在基本没有多少人使用,兼容性差。diff patchdiff patch大致的做法就是先打个正常的完整的jsbundle,然后再打个只包含了基础引用的基础包,比对一下patch,得出业务包,这样基础包和业务包都有了,更新时更新业务包即可。差分包的工具可以google-diff-match-patchmetro bundle目前,最好的RN分包方案还是facebook官方提供的metro bundle,此方案是fb在0.50版本引入的,并随着RN版本的迭代不断完善。也即是说,只要你使用的是0.50以上的RN版本,就可以使用metro bundle进行差分包进行热更新。配置内容比较多,这里主要看createModuleIdFactory和processModuleFilte两个配置参数。原理篇RN启动分析为了更好的理解RN的分包和和加载机制,下面通过源码来看看RN的启动过程。“dependencies”: { “react”: “16.6.1”, “react-native”: “0.57.7”, “react-navigation”: “^2.0.1” },注:本篇使用基于最新的0.57.7版本进行分析1,JS端启动流程index.js 作为RN应用的默认入口,源码如下:import {AppRegistry} from ‘react-native’;import App from ‘./App’;import {name as appName} from ‘./app.json’;AppRegistry.registerComponent(appName, () => App);AppRegistry是所有 RN应用的 JS 入口,应用的根组件通过AppRegistry.registerComponent方法注册自己,然后原生系统才可以加载应用的代码包并且在启动完成之后通过调用AppRegistry.runApplication来真正运行应用。registerComponent对应的源码如下:/** * Registers an app’s root component. * * See http://facebook.github.io/react-native/docs/appregistry.html#registercomponent / registerComponent( appKey: string, componentProvider: ComponentProvider, section?: boolean, ): string { runnables[appKey] = { componentProvider, run: appParameters => { renderApplication( componentProviderInstrumentationHook(componentProvider), appParameters.initialProps, appParameters.rootTag, wrapperComponentProvider && wrapperComponentProvider(appParameters), appParameters.fabric, ); }, }; if (section) { sections[appKey] = runnables[appKey]; } return appKey; },RN项目index.js文件的在调用registerComponent 方法时默认传入了 appKey、ComponentProvider 两个参数,而section是可以不用传的。然后,registerComponent方法会调用renderApplication方法参数并调用了 renderApplication 方法。renderApplication的源码如下:function renderApplication<Props: Object>( RootComponent: React.ComponentType<Props>, initialProps: Props, rootTag: any, WrapperComponent?: ?React.ComponentType<>, fabric?: boolean, showFabricIndicator?: boolean,) { invariant(rootTag, ‘Expect to have a valid rootTag, instead got ‘, rootTag); let renderable = ( <AppContainer rootTag={rootTag} WrapperComponent={WrapperComponent}> <RootComponent {…initialProps} rootTag={rootTag} /> {fabric === true && showFabricIndicator === true ? ( <ReactFabricIndicator /> ) : null} </AppContainer> );renderApplication中调用了AppContainer组件来封装当前rootVIew组件,并最终调用AppRegistry.runApplication来启动应用程序。/** * Loads the JavaScript bundle and runs the app. * * See http://facebook.github.io/react-native/docs/appregistry.html#runapplication / runApplication(appKey: string, appParameters: any): void { const msg = ‘Running application “’ + appKey + ‘” with appParams: ’ + JSON.stringify(appParameters) + ‘. ’ + ‘DEV === ’ + String(DEV) + ‘, development-level warning are ’ + (DEV ? ‘ON’ : ‘OFF’) + ‘, performance optimizations are ’ + (DEV ? ‘OFF’ : ‘ON’); infoLog(msg); BugReporting.addSource( ‘AppRegistry.runApplication’ + runCount++, () => msg, ); invariant( runnables[appKey] && runnables[appKey].run, ‘Application ’ + appKey + ’ has not been registered.\n\n’ + “Hint: This error often happens when you’re running the packager " + ‘(local dev server) from a wrong folder. For example you have ’ + ‘multiple apps and the packager is still running for the app you ’ + ‘were working on before.\nIf this is the case, simply kill the old ’ + ‘packager instance (e.g. close the packager terminal window) ’ + ‘and start the packager in the correct app folder (e.g. cd into app ’ + “folder and run ’npm start’).\n\n” + ‘This error can also happen due to a require() error during ’ + ‘initialization or failure to call AppRegistry.registerComponent.\n\n’, ); SceneTracker.setActiveScene({name: appKey}); runnables[appKey].run(appParameters); },在 runApplication 方法中,RN会通过 runnables[appKey] && runnables[appKey].run 来检查是否可以找到appKey对应的module组件,如果没有,则会抛出异常。那么,RN编写的页面又是如何在Android系统中显示的呢?那就得看看RN的Android端源码了。2,Android启动流程打开RN的Android项目,可以发现,Android的src目录下就只有MainActivity和 MainApplication 两个Java类。其中,MainActivity 为原生层应用程序的入口文件,MainApplication为Android应用程序入口文件。MainActivity.java文件的源码如下:import com.facebook.react.ReactActivity;public class MainActivity extends ReactActivity { @Override protected String getMainComponentName() { return “RNDemo”; }}MainActivity类的代码很简单,该类继承自 ReactActivity 并实现 getMainComponentName 方法,getMainComponentName方法返回与 AppRegistry.registerComponent 的 appKey 相同的名称。MainApplication类也比较简单,源码如下:import android.app.Application;import com.facebook.react.ReactApplication;import com.facebook.react.ReactNativeHost;import com.facebook.react.ReactPackage;import com.facebook.react.shell.MainReactPackage;import com.facebook.soloader.SoLoader;import java.util.Arrays;import java.util.List;public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage() ); } @Override protected String getJSMainModuleName() { return “index”; } }; @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } @Override public void onCreate() { super.onCreate(); SoLoader.init(this, / native exopackage / false); }}MainApplication 主要完成了三件事:实现 ReactApplication 接口,重写 getReactNativeHost 方法,返回ReactNativeHost实例。定义并初始化 ReactNativeHost,实现getUseDeveloperSupport、getPackages、getJSMainModuleName 方法,完成初始化设置。在 onCreate 方法中,调用SoLoader的init方法,启动C++层逻辑代码的初始化加载。ReactActivityMainActivity 继承 ReactActivity 类,并重写了getMainComponentName 方法,并且方法的返回值需要和我们在JS端的值保持一致。ReactActivity最核心的就是ReactActivityDelegate。 protected ReactActivity() { mDelegate = createReactActivityDelegate(); }/* * 在构造时调用,如果您有自定义委托实现,则覆盖. / protected ReactActivityDelegate createReactActivityDelegate() { return new ReactActivityDelegate(this, getMainComponentName()); }很明显,ReactActivity 类采用了委托的方式,将所有行为全权交给了 ReactActivityDelegate 去处理。这样做的好处是,降低代码耦合,提升了可扩展性。下面我们看一下ReactActivityDelegate类:public class ReactActivityDelegate { private final @Nullable Activity mActivity; private final @Nullable FragmentActivity mFragmentActivity; private final @Nullable String mMainComponentName; private @Nullable ReactRootView mReactRootView; private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer; … public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) { mActivity = activity; mMainComponentName = mainComponentName; mFragmentActivity = null; } public ReactActivityDelegate( FragmentActivity fragmentActivity, @Nullable String mainComponentName) { mFragmentActivity = fragmentActivity; mMainComponentName = mainComponentName; mActivity = null; } protected @Nullable Bundle getLaunchOptions() { return null; } protected ReactRootView createRootView() { return new ReactRootView(getContext()); } /* * Get the {@link ReactNativeHost} used by this app. By default, assumes * {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls * {@link ReactApplication#getReactNativeHost()}. Override this method if your application class * does not implement {@code ReactApplication} or you simply have a different mechanism for * storing a {@code ReactNativeHost}, e.g. as a static field somewhere. / protected ReactNativeHost getReactNativeHost() { return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost(); } public ReactInstanceManager getReactInstanceManager() { return getReactNativeHost().getReactInstanceManager(); } protected void onCreate(Bundle savedInstanceState) { if (mMainComponentName != null) { loadApp(mMainComponentName); } mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); } protected void loadApp(String appKey) { if (mReactRootView != null) { throw new IllegalStateException(“Cannot loadApp while app is already running.”); } mReactRootView = createRootView(); mReactRootView.startReactApplication( getReactNativeHost().getReactInstanceManager(), appKey, getLaunchOptions()); getPlainActivity().setContentView(mReactRootView); } … 中间省略生命周期、返回事件、权限请求的方法 private Context getContext() { if (mActivity != null) { return mActivity; } return Assertions.assertNotNull(mFragmentActivity); } private Activity getPlainActivity() { return ((Activity) getContext()); }}ReactActivityDelegate类重点关注loadApp方法, loadApp 方法主要做了三件事:创建 RootView实例;调用 RootView 实例的 startReactApplication 方法,将ReactInstanceManager实例、appKey、启动时初始化参数作为参数传递过去;将 ReactRootView 设置为 MainActivity 布局视图;ReactRootView在loadApp里面,首先创建一个ReactRootView,这个mReactRootView继承自FrameLayout,作为界面的跟布局,然后调用mReactRootView.startReactApplication()方法启动RN应用。涉及的源码如下:public void startReactApplication( ReactInstanceManager reactInstanceManager, String moduleName, @Nullable Bundle launchOptions) { UiThreadUtil.assertOnUiThread(); Assertions.assertCondition( mReactInstanceManager == null, “This root view has already been attached to a catalyst instance manager”); mReactInstanceManager = reactInstanceManager; mJSModuleName = moduleName; mLaunchOptions = launchOptions; if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { mReactInstanceManager.createReactContextInBackground(); } //执行界面测量 if (mWasMeasured) { attachToReactInstanceManager(); } }这里会执行到mReactInstanceManager.createReactContextInBackground()这个方法去生成reactnative的上下文对象。public void createReactContextInBackground() { Assertions.assertCondition( !mHasStartedCreatingInitialContext, “createReactContextInBackground should only be called when creating the react " + “application for the first time. When reloading JS, e.g. from a new file, explicitly” + “use recreateReactContextInBackground”); mHasStartedCreatingInitialContext = true; recreateReactContextInBackgroundInner(); }上面代码首先将mHasStartedCreatingInitialContext置为true,然后在 startReactApplication 方法中调用了 ReactInstanceManager 实例的 createReactContextInBackground 方法。ReactInstanceManager@ThreadConfined(UI) private void recreateReactContextInBackgroundInner() { if (mUseDeveloperSupport && mJSMainModulePath != null && !Systrace.isTracing(TRACE_TAG_REACT_APPS | TRACE_TAG_REACT_JS_VM_CALLS)) { final DeveloperSettings devSettings = mDevSupportManager.getDevSettings(); // 如果启用了远程JS调试,从dev服务器加载。 if (mDevSupportManager.hasUpToDateJSBundleInCache() && !devSettings.isRemoteJSDebugEnabled()) { // 如果从服务器下载了最新的捆绑包,禁用远程JS调试,始终使用它。 onJSBundleLoadedFromServer(null); } else if (mBundleLoader == null) { mDevSupportManager.handleReloadJS(); } else { mDevSupportManager.isPackagerRunning( new PackagerStatusCallback() { @Override public void onPackagerStatusFetched(final boolean packagerIsRunning) { UiThreadUtil.runOnUiThread( new Runnable() { @Override public void run() { if (packagerIsRunning) { mDevSupportManager.handleReloadJS(); } else { //如果dev服务器关闭,请禁用远程JS调试。 devSettings.setRemoteJSDebugEnabled(false); recreateReactContextInBackgroundFromBundleLoader(); } } }); } }); } return; } // 从 本地路径 加载 jsBundle recreateReactContextInBackgroundFromBundleLoader(); } @ThreadConfined(UI) private void recreateReactContextInBackgroundFromBundleLoader() { // 从BundleLoader加载 recreateReactContextInBackground(mJavaScriptExecutorFactory, mBundleLoader); }在 recreateReactContextInBackgroundInner 方法中,首先判断当前环境是否为开发者模式,在开发者模式下会执行 onJSBundleLoadedFromServer 方法从服务器加载 jsBundle文件。否则执行 recreateReactContextInBackgroundFromBundleLoader 方法从本地目录加载。在 recreateReactContextInBackgroundFromBundleLoader 方法中调用了 recreateReactContextInBackground(mJavaScriptExecutorFactory, mBundleLoader) 方法。jsExecutorFactory 为 C++ 和 JS 双向通信的中转站。jsBundleLoader 为 bundle 加载器,根据 ReactNativeHost 中的配置决定从哪里加载bundle文件。private void recreateReactContextInBackground( JavaScriptExecutorFactory jsExecutorFactory, JSBundleLoader jsBundleLoader) { // 创建 ReactContextInitParams 对象 final ReactContextInitParams initParams = new ReactContextInitParams( jsExecutorFactory, jsBundleLoader); if (mCreateReactContextThread == null) { // 开启一个新的线程创建 ReactContext runCreateReactContextOnNewThread(initParams); } else { mPendingReactContextInitParams = initParams; } }接下来,我们看一下 runCreateReactContextOnNewThread 方法。private void runCreateReactContextOnNewThread(final ReactContextInitParams initParams) { … mCreateReactContextThread = new Thread( new Runnable() { @Override public void run() { …. //由于 destroy() 可能已经运行并将其设置为false,因此在创建之前确保它为true mHasStartedCreatingInitialContext = true; try { // 标准显示系统优先级,主要是改善UI的刷新 Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); // 创建 ReactApplicationContext 实例 final ReactApplicationContext reactApplicationContext = createReactContext( initParams.getJsExecutorFactory().create(), initParams.getJsBundleLoader()); mCreateReactContextThread = null; final Runnable maybeRecreateReactContextRunnable = new Runnable() { @Override public void run() { if (mPendingReactContextInitParams != null) { runCreateReactContextOnNewThread(mPendingReactContextInitParams); mPendingReactContextInitParams = null; } } }; Runnable setupReactContextRunnable = new Runnable() { @Override public void run() { try { setupReactContext(reactApplicationContext); } catch (Exception e) { mDevSupportManager.handleException(e); } } }; // 开启线程执行 reactApplicationContext.runOnNativeModulesQueueThread(setupReactContextRunnable); UiThreadUtil.runOnUiThread(maybeRecreateReactContextRunnable); } catch (Exception e) { mDevSupportManager.handleException(e); } } }); // 开启线程执行 mCreateReactContextThread.start(); }执行到这个的时候,系统最终会开启异步任务ReactContextInitAsyncTask来创建上下文ReactApplicationContext,在ReactContextInitAsyncTask的doInBackground会调用createReactContext方法ReactInstanceManagerprivate ReactApplicationContext createReactContext( JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) { // 创建 ReactApplicationContext 实例 final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); … // 把各自的Module添加到对应的注册表中,processPackages方法通过遍历方式将在MainApplication 中 重写的ReactNativeHost的getPackages方法中的packages加入到注册表中 NativeModuleRegistry nativeModuleRegistry = processPackages(reactContext, mPackages, false); // 构建CatalystInstanceImpl实例 CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder() .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault()) // JS 执行通信类 .setJSExecutor(jsExecutor) // 注册 Java 模块 .setRegistry(nativeModuleRegistry) // 设置JSBundle 加载方式 .setJSBundleLoader(jsBundleLoader) // 设置异常处理器 .setNativeModuleCallExceptionHandler(exceptionHandler); final CatalystInstance catalystInstance; // 创建 CatalystInstance 实例 try { catalystInstance = catalystInstanceBuilder.build(); } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } if (mJSIModulePackage != null) { catalystInstance.addJSIModules(mJSIModulePackage .getJSIModules(reactContext, catalystInstance.getJavaScriptContextHolder())); } if (mBridgeIdleDebugListener != null) { catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener); } // 调用 C++ 层代码,把 Java Registry 转换为Json,再由 C++ 层传送到 JS 层 if (Systrace.isTracing(TRACE_TAG_REACT_APPS | TRACE_TAG_REACT_JS_VM_CALLS)) { catalystInstance.setGlobalVariable(”__RCTProfileIsProfiling”, “true”); } // 开始加载JSBundle catalystInstance.runJSBundle(); // 关联 ReactContext 与 CatalystInstance reactContext.initializeWithInstance(catalystInstance); return reactContext; }在这个方法里面,先生成nativeModuleRegistryBuilder和jsModulesBuilder,nativeModuleRegistryBuilder用来创建JavaModule注册表,JavaModule注册表将所有的JavaModule注册到CatalystInstance中;jsModulesBuilder用来创建JavaScriptModule注册表,JavaScriptModule注册表将所有的JavaScriptModule注册到CatalystInstance中。接着会执行下到 processPackage(coreModulesPackage,nativeModuleRegistryBuilder,jsModulesBuilder)代码,CoreModulesPackage里面封装了RN Framework(包括native和js端)核心功能,包括:通信、调试等,调用processPackage将coreModulesPackage里面对应的NativeModules注册到JavaModule注册表中,对应的JSModules注册到JavaScriptModule注册表中,底下就会执行用户自定义的ReactPackage,将对应的modules注册到相应的注册表中,JavaModule注册表和JavaScriptModule注册表注册完毕之后,就是去生成一个catalystInstance,这个类主要是负责三端的通信(通过ReactBridge,在catalystInstance的构造函数中调用initializeBridge方法生成),接着调用setGlobalVariable(Native方法)把Java Registry转换为Json,再由C++层传送到JS层。catalystInstance相应的处理执行完之后,将其与reactContext关联起来,最后通过catalystInstance加载bundle文件。总的来说,createReactContext方法主要完成了以下操作:构建 ReactApplicationContext;注册 Packages 原生模块;构建 CatalystInstance 实例;通过 CatalystInstance 实例调用C++层代码逻辑;调用 CatalystInstance 实例的 runJSBundle 方法加载 JSBundle。到这里,我们基本开清楚了原生是如何加载JSBundle的:即通过 CatalystInstance 来加载 JSBundle 文件。下面让我们继续看 runJSBundle方法:@Override public void runJSBundle() { Assertions.assertCondition(!mJSBundleHasLoaded, “JS bundle was already loaded!”); // 通过 JSBundleLoader 去执行加载,不同的加载方式 JSBundleLoader 实现方式不同 mJSBundleLoader.loadScript(CatalystInstanceImpl.this); synchronized (mJSCallsPendingInitLock) { // 在 JS 线程上排队加载 bundle,此时可能还没有运行。 在这里设置它是安全的,因为它所关联的任何工作都将在加载完成之后的JS线程上排队执行。 mAcceptCalls = true; for (PendingJSCall function : mJSCallsPendingInit) { function.call(this); } mJSCallsPendingInit.clear(); mJSBundleHasLoaded = true; } Systrace.registerListener(mTraceListener); }在 runJSBundle 方法中通过JSBundleLoader的 loadScript 方法去加载JSBundle,不同的加载方式 JSBundleLoader 实现方式不同。JSBundleLoaderJSBundleLoader主要用于存储 JS 包的信息,允许 CatalystInstance 通过 ReactBridge 加载正确的包。public abstract class JSBundleLoader { /* * 建议将此加载程序用于应用程序的发布版本。 在这种情况下,应该使用本地JS执行程序。 将从本机代码中的资源读取JS包,以节省将大型字符串从java传递到本机内存。 / public static JSBundleLoader createAssetLoader( final Context context, final String assetUrl, final boolean loadSynchronously) { return new JSBundleLoader() { @Override public String loadScript(CatalystInstanceImpl instance) { instance.loadScriptFromAssets(context.getAssets(), assetUrl, loadSynchronously); return assetUrl; } }; } /* * 此加载程序从文件系统加载包。 将使用本机代码读取该包,以节省将大型字符串从java传递到本机内存。 / public static JSBundleLoader createFileLoader(final String fileName) { return createFileLoader(fileName, fileName, false); } public static JSBundleLoader createFileLoader( final String fileName, final String assetUrl, final boolean loadSynchronously) { return new JSBundleLoader() { @Override public String loadScript(CatalystInstanceImpl instance) { instance.loadScriptFromFile(fileName, assetUrl, loadSynchronously); return fileName; } }; } /* * 从dev服务器重新加载bundle时使用此加载器。 在这种情况下,加载器期望预取JS包并存储在本地文件中。 * 我们这样做是为了避免在java和本机代码之间传递大字符串,并避免在java中分配内存以适应整个JS包。 * 为了使JS堆栈跟踪能够正常工作并允许源映射正确地对其进行符号化,需要提供正确的下载bundle的sourceURL。 / public static JSBundleLoader createCachedBundleFromNetworkLoader( final String sourceURL, final String cachedFileLocation) { return new JSBundleLoader() { @Override public String loadScript(CatalystInstanceImpl instance) { try { instance.loadScriptFromFile(cachedFileLocation, sourceURL, false); return sourceURL; } catch (Exception e) { throw DebugServerException.makeGeneric(e.getMessage(), e); } } }; } /* * 此加载程序用于从开发服务器加载增量包。 我们将每个delta消息传递给加载器并在C ++中处理它。 * 将其作为字符串传递会由于内存副本而导致效率低下,这必须在后续处理中解决。 / public static JSBundleLoader createDeltaFromNetworkLoader( final String sourceURL, final NativeDeltaClient nativeDeltaClient) { return new JSBundleLoader() { @Override public String loadScript(CatalystInstanceImpl instance) { try { instance.loadScriptFromDeltaBundle(sourceURL, nativeDeltaClient, false); return sourceURL; } catch (Exception e) { throw DebugServerException.makeGeneric(e.getMessage(), e); } } }; } /* * 启用代理调试时使用此加载程序。 在这种情况下,从设备获取捆绑包是没有意义的,因为远程执行器无论如何都必须这样做。 / public static JSBundleLoader createRemoteDebuggerBundleLoader( final String proxySourceURL, final String realSourceURL) { return new JSBundleLoader() { @Override public String loadScript(CatalystInstanceImpl instance) { instance.setSourceURLs(realSourceURL, proxySourceURL); return realSourceURL; } }; } /* * 加载脚本,返回其加载的源的URL。 / public abstract String loadScript(CatalystInstanceImpl instance);}runJSBundle的源码如下:@Override public void runJSBundle() { Log.d(ReactConstants.TAG, “CatalystInstanceImpl.runJSBundle()”); Assertions.assertCondition(!mJSBundleHasLoaded, “JS bundle was already loaded!”); // incrementPendingJSCalls(); mJSBundleLoader.loadScript(CatalystInstanceImpl.this); synchronized (mJSCallsPendingInitLock) { // Loading the bundle is queued on the JS thread, but may not have // run yet. It’s safe to set this here, though, since any work it // gates will be queued on the JS thread behind the load. mAcceptCalls = true; for (PendingJSCall function : mJSCallsPendingInit) { function.call(this); } mJSCallsPendingInit.clear(); mJSBundleHasLoaded = true; } // This is registered after JS starts since it makes a JS call Systrace.registerListener(mTraceListener); }JSBundleLoader 类中提供了很多种 JSBundle 文件的加载方式,并且可以看到每种加载方式都是借助了CatalystInstanceImpl实例来实现。来看 CatalystInstanceImpl 中的具体实现: / package / void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean loadSynchronously) { mSourceURL = assetURL; jniLoadScriptFromAssets(assetManager, assetURL, loadSynchronously); } / package / void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) { mSourceURL = sourceURL; jniLoadScriptFromFile(fileName, sourceURL, loadSynchronously); } / package */ void loadScriptFromDeltaBundle( String sourceURL, NativeDeltaClient deltaClient, boolean loadSynchronously) { mSourceURL = sourceURL; jniLoadScriptFromDeltaBundle(sourceURL, deltaClient, loadSynchronously); } private native void jniSetSourceURL(String sourceURL); private native void jniRegisterSegment(int segmentId, String path); private native void jniLoadScriptFromAssets(AssetManager assetManager, String assetURL, boolean loadSynchronously); private native void jniLoadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously); private native void jniLoadScriptFromDeltaBundle(String sourceURL, NativeDeltaClient deltaClient, boolean loadSynchronously);可以看到,下面就调用jni层面的代码CatalystInstanceImpl.cpp里面的代码,C++层的代码我们不用太关心,只需要知道,经过这一步之后,js和java层面就可以相互调用类。如果想要看c++的实现,可以在node_modules的ReactAndroid目录中查看。在之前的 runCreateReactContextOnNewThread 方法中,在creatReactContext之后还有一句核心的代码。 Runnable setupReactContextRunnable = new Runnable() { @Override public void run() { try { setupReactContext(reactApplicationContext); } catch (Exception e) { mDevSupportManager.handleException(e); } }ReactContext创建完毕之后,ReactContextInitAsyncTask就会执行onPostExecute中的setupReactContext(reactContext)方法。 private void setupReactContext(final ReactApplicationContext reactContext) { Log.d(ReactConstants.TAG, “ReactInstanceManager.setupReactContext()”); synchronized (mReactContextLock) { mCurrentReactContext = Assertions.assertNotNull(reactContext); } CatalystInstance catalystInstance = Assertions.assertNotNull(reactContext.getCatalystInstance()); catalystInstance.initialize(); mDevSupportManager.onNewReactContextCreated(reactContext); mMemoryPressureRouter.addMemoryPressureListener(catalystInstance); // 重置生命周期 moveReactContextToCurrentLifecycleState(); ReactMarker.logMarker(ATTACH_MEASURED_ROOT_VIEWS_START); // mAttachedRootViews 保存的是ReactRootView synchronized (mAttachedRootViews) { for (ReactRootView rootView : mAttachedRootViews) { // 将rootview测量并连接到 catalystInstance attachRootViewToInstance(rootView, catalystInstance); } } ReactMarker.logMarker(ATTACH_MEASURED_ROOT_VIEWS_END); … 代码省略 }attachRootViewToInstance方法会将 rootview 与 catalystInstance 进行绑定。private void attachRootViewToInstance( final ReactRootView rootView, CatalystInstance catalystInstance) { Log.d(ReactConstants.TAG, “ReactInstanceManager.attachRootViewToInstance()”); // 获取 UIManager UIManager uiManagerModule = UIManagerHelper.getUIManager(mCurrentReactContext, rootView.getUIManagerType()); // 设置 rootView final int rootTag = uiManagerModule.addRootView(rootView); rootView.setRootViewTag(rootTag); rootView.runApplication(); UiThreadUtil.runOnUiThread(new Runnable() { @Override public void run() { rootView.onAttachedToReactInstance(); } }); }private void attachRootViewToInstance( final ReactRootView rootView) { Log.d(ReactConstants.TAG, “ReactInstanceManager.attachRootViewToInstance()”); Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, “attachRootViewToInstance”); // 获取 UIManager UIManager uiManagerModule = UIManagerHelper.getUIManager(mCurrentReactContext, rootView.getUIManagerType()); // 设置 rootView final int rootTag = uiManagerModule.addRootView(rootView); rootView.setRootViewTag(rootTag); rootView.runApplication(); Systrace.beginAsyncSection( TRACE_TAG_REACT_JAVA_BRIDGE, “pre_rootView.onAttachedToReactInstance”, rootTag); UiThreadUtil.runOnUiThread(new Runnable() { @Override public void run() { Systrace.endAsyncSection( TRACE_TAG_REACT_JAVA_BRIDGE, “pre_rootView.onAttachedToReactInstance”, rootTag); rootView.onAttachedToReactInstance(); } }); Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); }在 attachRootViewToInstance 方法中 设置 rootView tag,并执行 runApplication 方法。void runApplication() { try { if (mReactInstanceManager == null || !mIsAttachedToInstance) { return; } // 此时 ReactContext 创建已完成 ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); if (reactContext == null) { return; } // 获取 catalystInstance CatalystInstance catalystInstance = reactContext.getCatalystInstance(); // 将启动时到初始化参数封装成 Bundle WritableNativeMap appParams = new WritableNativeMap(); appParams.putDouble(“rootTag”, getRootViewTag()); @Nullable Bundle appProperties = getAppProperties(); if (appProperties != null) { appParams.putMap(“initialProps”, Arguments.fromBundle(appProperties)); } if (getUIManagerType() == FABRIC) { appParams.putBoolean(“fabric”, true); } // 获取 moduleName, 设置加载状态 mShouldLogContentAppeared = true; String jsAppModuleName = getJSModuleName(); // 调用 catalystInstance 的 getJSModule 方法获取 AppRegistry,由Java层调用启动流程入口,执行其中的 runApplication 方法 catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams); } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } }runApplication最终调用的是catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams), AppRegistry.class是JS层暴露给Java层的接口方法。它的真正实现在AppRegistry.js里,在文章开始时,我们已经对它进行了简单介绍,AppRegistry.js 是运行所有RN应用的JS层入口。此时调用JS进行渲染,在通过UIManagerModule将JS组件转换成Android组件,最终显示在ReactRootView上。 ...

December 14, 2018 · 10 min · jiezi

Flutter入坑分享

本文只适合初次接触 Flutter 的开发者。原文链接: http://blog.myweb.kim/flutter/Flutter%E5%85%A5%E5%9D%91%E5%88%86%E4%BA%AB/简介Flutter 是 Google 推出并开源的移动端开发框架(基于「Dart」语言)。使用 Flutter 开发的APP可以同时运行在 IOS 与 Android 平台上。并且 Flutter 默认带有 Material 风格 与 Cupertino 风格的主题包(前者Android,后者IOS),可以快速开发一个IOS 风格或者 Android 风格的…Demo…跨平台Flutter 不使用 WebView 也不使用操作系统的原生控件,而是自己有用一个 高性能 的渲染引擎,可以非常高效的进行组件绘制UI渲染。这样 Flutter 可以保证在 IOS 与 Android 上的UI表现一致性 ,开发者无需过多关注平台差异性上的问题。对于初创公司来说,前期节约开发成本就是最好的融资。。。高性能与 React Native (以下简称RN)的跨平台不同的是,RN是会将JS编写的对应组件转换为原生组件去渲染,而 Flutter 是基于最底层 Skia 的图形库去渲染(我觉得有点类似于 DOM 中的 canvas , 从平台上得到一个画布,自己在画布上去渲染),所有的渲染都有 Skia 来完成。Skia 延伸…Flutter使用Skia作为其2D渲染引擎,Skia是Google的一个2D图形处理函数库,包含字型、坐标转换,以及点阵图都有高效能且简洁的表现,Skia是跨平台的,并提供了非常友好的API,目前Google Chrome浏览器和Android均采用Skia作为其绘图引擎,值得一提的是,由于Android系统已经内置了Skia,所以Flutter在打包APK(Android应用安装包)时,不需要再将Skia打入APK中,但iOS系统并未内置Skia,所以构建iPA时,也必须将Skia一起打包,这也是为什么Flutter APP的Android安装包比iOS安装包小的主要原因。正是因为基于自己的渲染机制,不需要与原生平台之间频繁通信,才体现出来他的高效率、高性能。Flutter 的布局、渲染都是 Dart 直接控制,在一些交互中,比如滑动的时候它的高性能就会体现出来。而RN在这方面的渲染则是与原生平台进行通信,不断的进行信息同步,这部分的开销放到手机上还是很大的。而且在渲染层,Flutter 底层也有一个类似虚拟DOM的组件,在UI进行变化后,会进行diff算法。开发高效率Flutter 在开发的时候有一个特点,热重载。 就像在webpack 与 浏览器,在编辑器中保存后,界面立马就能看到变化。Flutter 也是这样,当将 APP 在虚拟容器中或者真机设备中调试时,保存后,APP会立刻响应。节省了大量时间。Dart 初步了解因为 Flutter 是基于 Dart 语言开发的,所以我们多多少少也要了解下 Dart 这玩意怎么写,他的语法与结构是个怎样的。虽然官网的 Demo 有提到说:「如果您熟悉面向对象和基本编程概念(如变量、循环和条件控制),则可以完成本教程,您无需要了解Dart或拥有移动开发的经验。」emmmm… 纯属扯淡…如果不了解 Dart,那也仅限于看 Demo 是怎么写的…Dart 出自Google。是一种面向对象编程的强类型语言,语法有点像 Java 与 JavaScript 的集合体。官方学习资料以下是使用 Flutter 需要掌握的 Dart 基础语法:(以下内容摘抄来至 官网文档 , 没必要细看,可快速的过一遍,只做了解。)变量声明var类似于JavaScript中的var,它可以接收任何类型的变量,但最大的不同是Dart中var变量一旦赋值,类型便会确定,则不能再改变其类型,如:var t;t=“hi world”;// 下面代码在dart中会报错,应为变量t的类型已经确定为String,// 类型一旦确定后则不能再更改其类型。t=1000;上面的代码在JavaScript是没有问题的,前端开发者需要注意一下,之所以有此差异是因为Dart本身是一个强类型语言,任何变量都是有确定类型的,在Dart中,当用var声明一个变量后,Dart在编译时会根据第一次赋值数据的类型来推断其类型,编译结束后其类型就已经被确定,而JavaScript是纯粹的弱类型脚本语言,var只是变量的声明方式而已。dynamic和ObjectDynamic和Object 与 var功能相似,都会在赋值时自动进行类型推断,不同在于,赋值后可以改变其类型,如:dynamic t;t=“hi world”;//下面代码没有问题t=1000;Object 是dart所有对象的根基类,也就是说所有类型都是Object的子类,所以任何类型的数据都可以赋值给Object声明的对象,所以表现效果和dynamic相似。final和const如果您从未打算更改一个变量,那么使用 final 或 const,不是var,也不是一个类型。 一个 final 变量只能被设置一次,两者区别在于:const 变量是一个编译时常量,final变量在第一次使用时被初始化。被final或者const修饰的变量,变量类型可以省略,如://可以省略String这个类型声明final str = “hi world”;//final str = “hi world”; const str1 = “hi world”;//const String str1 = “hi world”;函数Dart是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型Function。这意味着函数可以赋值给变量或作为参数传递给其他函数,这是函数式编程的典型特征。函数声明bool isNoble(int atomicNumber) { return _nobleGases[atomicNumber] != null;}dart函数声明如果没有显示申明返回值类型时会默认当做dynamic处理,注意,函数返回值没有类型推断:typedef bool CALLBACK();//不指定返回类型,此时默认为dynamic,不是boolisNoble(int atomicNumber) { return _nobleGases[atomicNumber] != null;}void test(CALLBACK cb){ print(cb()); }//报错,isNoble不是bool类型test(isNoble);对于只包含一个表达式的函数,可以使用简写语法bool isNoble (int atomicNumber )=> _nobleGases [ atomicNumber ] != null ; 函数作为变量var say= (str){ print(str);};say(“hi world”);函数作为参数传递void execute(var callback){ callback();}execute(()=>print(“xxx”))可选的位置参数包装一组函数参数,用[]标记为可选的位置参数:String say(String from, String msg, [String device]) { var result = ‘$from says $msg’; if (device != null) { result = ‘$result with a $device’; } return result;}下面是一个不带可选参数调用这个函数的例子:say(‘Bob’, ‘Howdy’); //结果是: Bob says Howdy下面是用第三个参数调用这个函数的例子:say(‘Bob’, ‘Howdy’, ‘smoke signal’); //结果是:Bob says Howdy with a smoke signal可选的命名参数定义函数时,使用{param1, param2, …},用于指定命名参数。例如://设置[bold]和[hidden]标志void enableFlags({bool bold, bool hidden}) { // … }调用函数时,可以使用指定命名参数。例如:paramName: valueenableFlags(bold: true, hidden: false);可选命名参数在Flutter中使用非常多。异步支持Dart类库有非常多的返回Future或者Stream对象的函数。 这些函数被称为异步函数:它们只会在设置好一些需要消耗一定时间的操作之后返回,比如像 IO操作。而不是等到这个操作完成。async和await关键词支持了异步编程,运行您写出和同步代码很像的异步代码。FutureFuture与JavaScript中的Promise非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。由于本身功能较多,这里我们只介绍其常用的API及特性。还有,请记住,Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。Future.then为了方便示例,在本例中我们使用Future.delayed 创建了一个延时任务(实际场景会是一个真正的耗时任务,比如一次网络请求),即2秒后返回结果字符串"hi world!",然后我们在then中接收异步结果并打印结果,代码如下:Future.delayed(new Duration(seconds: 2),(){ return “hi world!”;}).then((data){ print(data);});Future.catchError如果异步任务发生错误,我们可以在catchError中捕获错误,我们将上面示例改为:Future.delayed(new Duration(seconds: 2),(){ //return “hi world!”; throw AssertionError(“Error”); }).then((data){ //执行成功会走到这里 print(“success”);}).catchError((e){ //执行失败会走到这里 print(e);});在本示例中,我们在异步任务中抛出了一个异常,then 的回调函数将不会被执行,取而代之的是 catchError回调函数将被调用;但是,并不是只有 catchError回调才能捕获错误,then方法还有一个可选参数onError,我们也可以它来捕获异常:Future.delayed(new Duration(seconds: 2), () { //return “hi world!”; throw AssertionError(“Error”);}).then((data) { print(“success”);}, onError: (e) { print(e);});Future.whenComplete有些时候,我们会遇到无论异步任务执行成功或失败都需要做一些事的场景,比如在网络请求前弹出加载对话框,在请求结束后关闭对话框。这种场景,有两种方法,第一种是分别在then或catch中关闭一下对话框,第二种就是使用Future的whenComplete回调,我们将上面示例改一下:Future.delayed(new Duration(seconds: 2),(){ //return “hi world!”; throw AssertionError(“Error”);}).then((data){ //执行成功会走到这里 print(data);}).catchError((e){ //执行失败会走到这里 print(e);}).whenComplete((){ //无论成功或失败都会走到这里});Future.wait有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作,比如我们有一个界面,需要先分别从两个网络接口获取数据,获取成功后,我们需要将两个接口数据进行特定的处理后再显示到UI界面上,应该怎么做?答案是Future.wait,它接受一个Future数组参数,只有数组中所有Future都执行成功后,才会触发then的成功回调,只要有一个Future执行失败,就会触发错误回调。下面,我们通过模拟Future.delayed 来模拟两个数据获取的异步任务,等两个异步任务都执行成功时,将两个异步任务的结果拼接打印出来,代码如下:Future.wait([ // 2秒后返回结果 Future.delayed(new Duration(seconds: 2), () { return “hello”; }), // 4秒后返回结果 Future.delayed(new Duration(seconds: 4), () { return " world"; })]).then((results){ print(results[0]+results[1]);}).catchError((e){ print(e);});执行上面代码,4秒后你会在控制台中看到“hello world”。Async/awaitDart中的async/await 和JavaScript中的async/await功能和用法是一模一样的,如果你已经了解JavaScript中的async/await的用法,可以直接跳过本节。回调地狱(Callback hell)如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现Future.then回调中套回调情况。举个例子,比如现在有个需求场景是用户先登录,登录成功后会获得用户Id,然后通过用户Id,再去请求用户个人信息,获取到用户个人信息后,为了使用方便,我们需要将其缓存在本地文件系统,代码如下://先分别定义各个异步任务Future<String> login(String userName, String pwd){ … //用户登录};Future<String> getUserInfo(String id){ … //获取用户信息 };Future saveUserInfo(String userInfo){ … // 保存用户信息 }; 接下来,执行整个任务流:login(“alice”,"").then((id){ //登录成功后通过,id获取用户信息 getUserInfo(id).then((userInfo){ //获取用户信息后保存 saveUserInfo(userInfo).then((){ //保存用户信息,接下来执行其它操作 … }); });})可以感受一下,如果业务逻辑中有大量异步依赖的情况,将会出现上面这种在回调里面套回调的情况,过多的嵌套会导致的代码可读性下降以及出错率提高,并且非常难维护,这个问题被形象的称为回调地狱(Callback hell)。回调地狱问题在之前JavaScript中非常突出,也是JavaScript被吐槽最多的点,但随着ECMAScript6和ECMAScript7标准发布后,这个问题得到了非常好的解决,而解决回调地狱的两大神器正是ECMAScript6引入了Promise,以及ECMAScript7中引入的async/await。 而在Dart中几乎是完全平移了JavaScript中的这两者:Future相当于Promise,而async/await连名字都没改。接下来我们看看通过Future和async/await如何消除上面示例中的嵌套问题。使用Future消除callback helllogin(“alice”,"").then((id){ return getUserInfo(id);}).then((userInfo){ return saveUserInfo(userInfo);}).then((e){ //执行接下来的操作 }).catchError((e){ //错误处理 print(e);});正如上文所述, “Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用” ,如果在then中返回的是一个Future的话,该future会执行,执行结束后会触发后面的then回调,这样依次向下,就避免了层层嵌套。使用async/await消除callback hell通过Future回调中再返回Future的方式虽然能避免层层嵌套,但是还是有一层回调,有没有一种方式能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式?答案是肯定的,这就要使用async/await了,下面我们先直接看代码,然后再解释,代码如下:task() async { try{ String id = await login(“alice”,"******"); String userInfo = await getUserInfo(id); await saveUserInfo(userInfo); //执行接下来的操作 } catch(e){ //错误处理 print(e); } }async用来表示函数是异步的,定义的函数会返回一个Future对象,可以使用then方法添加回调函数。await 后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走;await必须出现在 async 函数内部。可以看到,我们通过async/await将一个异步流用同步的代码表示出来了。其实,无论是在JavaScript还是Dart中,async/await都只是一个语法糖,编译器或解释器最终都会将其转化为一个Promise(Future)的调用链。StreamStream 也是用于接收异步事件数据,和Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件而传递结果数据或错误异常。 Stream 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:Stream.fromFutures([ // 1秒后返回结果 Future.delayed(new Duration(seconds: 1), () { return “hello 1”; }), // 抛出一个异常 Future.delayed(new Duration(seconds: 2),(){ throw AssertionError(“Error”); }), // 3秒后返回结果 Future.delayed(new Duration(seconds: 3), () { return “hello 3”; })]).listen((data){ print(data);}, onError: (e){ print(e.message);},onDone: (){});上面的代码依次会输出:I/flutter (17666): hello 1I/flutter (17666): ErrorI/flutter (17666): hello 3代码很简单,就不赘述了。思考题:既然Stream可以接收多次事件,那能不能用Stream来实现一个订阅者模式的事件总线?总结通过上面介绍,相信你对Dart应该有了一个初步的印象,由于笔者平时也使用Java和JavaScript,下面笔者根据自己的经验,结合Java和JavaScript,谈一下自己的看法。之所以将Dart与Java和JavaScript对比,是因为,这两者分别是强类型语言和弱类型语言的典型代表,并且Dart 语法中很多地方也都借鉴了Java和JavaScript。Dart vs Java客观的来讲,Dart在语法层面确实比Java更有表现力;在VM层面,Dart VM在内存回收和吞吐量都进行了反复的优化,但具体的性能对比,笔者没有找到相关测试数据,但在笔者看来,只要Dart语言能流行,VM的性能就不用担心,毕竟Google在go(没用vm但有GC)、javascript(v8)、dalvik(android上的java vm)上已经有了很多技术积淀。值得注意的是Dart在Flutter中已经可以将GC做到10ms以内,所以Dart和Java相比,决胜因素并不会是在性能方面。而在语法层面,Dart要比java更有表现力,最重要的是Dart对函数式编程支持要远强于Java(目前只停留在lamda表达式),而Dart目前真正的不足是生态,但笔者相信,随着Futter的逐渐火热,会回过头来反推Dart生态加速发展,对于Dart来说,现在需要的是时间。Dart vs JavaScriptJavaScript的弱类型一直被抓短,所以TypeScript、Coffeescript甚至是Facebook的flow(虽然并不能算JavaScript的一个超集,但也通过标注和打包工具提供了静态类型检查)才有市场。就笔者使用过的脚本语言中(笔者曾使用过Python、PHP),JavaScript无疑是动态化支持最好的脚本语言,比如在JavaScript中,可以给任何对象在任何时候动态扩展属性,对于精通JavaScript的高手来说,这无疑是一把利剑。但是,任何事物都有两面性,JavaScript的强大的动态化特性也是把双刃剑,你可经常听到另一个声音,认为JavaScript的这种动态性糟糕透了,太过灵活反而导致代码很难预期,无法限制不被期望的修改。毕竟有些人总是对自己或别人写的代码不放心,他们希望能够让代码变得可控,并期望有一套静态类型检查系统来帮助自己减少错误。正因如此,在Flutter中,Dart几乎放弃了脚本语言动态化的特性,如不支持反射、也不支持动态创建函数等。并且Dart在2.0强制开启了类型检查(Strong Mode),原先的检查模式(checked mode)和可选类型(optional type)将淡出,所以在类型安全这个层面来说,Dart和TypeScript、Coffeescript是差不多的,所以单从这一点来看,Dart并不具备什么明显优势,但综合起来看,dart既能进行服务端脚本、APP开发、web开发,这就有优势了!官方PPT宣传截图Flutter 底层架构的一个大概示意图:Material 和 Cupertino 是 Flutter 官方提供的两个不同的 UI 风格组件库(前者Android,后者IOS)。在 Flutter 中,一切皆是 Widget 。 一个按钮是 Widget,一段文字也是 Widget,一个图片也是 Widget,一个路由导航 也是 Widget。所以前期接触 Flutter 可以先学习这两个UI库如何使用即可。(个人见解)基础组件库Material 组件库Cupertino 组件库搭建开发环境windows上的搭建macOS上的搭建linux上的搭建搭建过程很简单,下载 SDK 包,然后配置下环境变量就ok了。编辑器推荐VScode,轻巧、简洁。配置好 Flutter环境,只需要在安装一个 Flutter 插件就好了。官方配置教程第一个Demo在 VScode 中安装好插件后,按下shift + command + p 输入 flutter ,选择 New Project。第一次创建时可能需要选择 Flutter SDK 的位置。下面的Demo是官网上的给出的代码,整理出来的一个完整的。先在 pubspec.yaml 中添加一个依赖: english_words 它是 Dart 语言编写的一个随机生成英文单词的工具包。pubspec.yaml 是 Flutter 配置文件,可以理解为 npm 中的 package.json找到文件的第21行:dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 # 在这里添加 版本号遵循 语义化(Semantic Versioning) english_words: ^3.1.5dev_dependencies: flutter_test: sdk: flutterFlutter 有一个官方的包管理平台,pub.dartlang.org 类似于npm添加完成后,在控制台输入flutter packages get 或者在编辑器中右键点击 pubspes.yaml 选择 Get Packages也就是安装新的依赖。替换Demo代码这个Demo是一个随机生成英文名字的程序,有一个可以无限滚动的列表,可以让用户对喜欢的名字进行红心标记搜藏,然后点击右上角,可以查看已收藏的名字(路由跳转来实现的)。将lib/main.dart 中的所有代码删除,替换成下面的代码:下面的代码是将官网Demo中的代码整理好的,可以先不去管它什么样的结果或者具体每句代码什么意思,先将Demo在模拟器中跑起来再说。import ‘package:flutter/material.dart’;import ‘package:english_words/english_words.dart’;// 程序入口void main() => runApp(new MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: ‘Startup Name Generator’, home: new RandomWords(), theme: new ThemeData( primaryColor: Colors.white, ), ); }}class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState();}class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = new Set<WordPair>(); final _biggerFont = const TextStyle(fontSize: 18.0); @override Widget build(BuildContext context) { return new Scaffold ( appBar: new AppBar( title: new Text(‘Startup Name Generator’), actions: <Widget>[ new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved), ], ), body: _buildSuggestions(), ); } void _pushSaved() { Navigator.of(context).push( new MaterialPageRoute( builder: (context) { final tiles = _saved.map( (pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); }, ); final divided = ListTile .divideTiles( context: context, tiles: tiles, ) .toList(); return new Scaffold( appBar: new AppBar( title: new Text(‘Saved Suggestions’), ), body: new ListView(children: divided), ); }, ) ); } Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), trailing: new Icon( alreadySaved ? Icons.favorite : Icons.favorite_border, color: alreadySaved ? Colors.red : null, ), onTap: () { setState(() { if (alreadySaved) { _saved.remove(pair); } else { _saved.add(pair); } }); }, ); } Widget _buildSuggestions() { return new ListView.builder( padding: const EdgeInsets.all(16.0), // 对于每个建议的单词对都会调用一次itemBuilder,然后将单词对添加到ListTile行中 // 在偶数行,该函数会为单词对添加一个ListTile row. // 在奇数行,该行书湖添加一个分割线widget,来分隔相邻的词对。 // 注意,在小屏幕上,分割线看起来可能比较吃力。 itemBuilder: (context, i) { // 在每一列之前,添加一个1像素高的分隔线widget if (i.isOdd) return new Divider(); // 语法 “i ~/ 2” 表示i除以2,但返回值是整形(向下取整),比如i为:1, 2, 3, 4, 5 // 时,结果为0, 1, 1, 2, 2, 这可以计算出ListView中减去分隔线后的实际单词对数量 final index = i ~/ 2; // 如果是建议列表中最后一个单词对 if (index >= _suggestions.length) { // …接着再生成10个单词对,然后添加到建议列表 _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); } ); }}选择调试 -> 启动调试 然后选择 ios emulator , 等待启动即可。(这个是macOS上的操作,windows只能选择Android的模拟器,当前所有的前提是你的 Flutter 环境确保搭建成功了。)运行成功后如下图所示:官方学习资料链接Flutter 中文网Flutter 实战以上,致那颗骚动的心…… ...

December 12, 2018 · 4 min · jiezi

react-native中生成二维码和分享图片

在react-native中展示二维码是一个非常麻烦的过程。最好的方法莫过于原生支持画二维码。但是这有一个副作用,需要在原生中添加新的代码。对于不打算很快升级的项目是一个很通过的选择。这里我介绍一种不使用原生的方式来生成二维码,副作用仅仅是需要联网下载一个js文件。当然可以使用本地缓存了,这样就和原生几乎一致了。npm地址实现自定义二维码不添加原生代码的情况下实现react-native中展示二维码,其实利用的正是WebView组件。在前端开发的过程中已经有大神实现了js生成二维码的功能。它就是qrcode.js,熟悉的人已经可以很快想到实现方式了。如果将WebView看做一个类似View的组件,那么在WebView加载完成之后它就已经在展现上与普通组件没有什么区别了。在这个时候就可以看做已经完成了二维码的生成。<WebView automaticallyAdjustContentInsets={false} scalesPageToFit={Platform.OS === ‘android’} contentInset={{ top: 0, right: 0, bottom: 0, left: 0 }} source={{ html: this.html() }} opaque={false} underlayColor={’transparent’} style={{ height: size, width: size }} javaScriptEnabled={true} scrollEnabled={false} onLoad={this.props.onLoad} onLoadEnd={this.props.onLoadEnd} originWhitelist={[’*’]}/>其中的html方法其实就是在生成我们需要的html代码。我们再这里定义几个参数,方便我们使用static defaultProps = { value: “”, size: 100, bgColor: “#fff”, fgColor: “#000”, onLoad: () => { }, onLoadEnd: () => { }, }最终效果如下:产生组件快照很多时候我们还是需要将APP中的某个部分截图保存的。在react-native中,我们可以利用takeSnapshot方法,将组件保存在临时目录中,同时使用CameraRoll.saveToCameraRoll方法将图片放入相册中。有没有想到什么?是的,我们可以在客户端自己生成分享图片。如果你的分享图片用到了很多动态数据。比如:用户不同图片不同,产品或者渠道不同图片也不同。这个时候服务端生成图片会非常的耗资源。同时用户在等待图片生成的过程中也会有很大的延迟。这个时候如果图片能够在客户端中生成岂不是非常的好。速度又快,效果又好。利用上面的二个组件,我们就可以自定义分享图片并下载到用户的相册中。npm地址源代码地址

December 6, 2018 · 1 min · jiezi

React16.7 hooks初试之setTimeout引发的bug

前言 周末尝试了一下React新的hooks功能,来封装一个组件,遇到一个bug,所以记录一下过程!报错如下:Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.in Notification大概意思是组件已经卸载了,但在卸载之后还执行了一个对组件更新的操作,这是一个无效的操作,但它表示应用程序中存在内存泄漏。要修复,请取消useEffect cleanup function.in Notification 中的所有订阅和异步任务组件核心代码如下:function Notification(props){ var timer = null; const [visible, setVisible] = useState(false); let {title,description,duration,theme,onClose,}= props; let leave = (source=’’) => { clearTimeout(timer); setVisible(false); console.log(“注意这里是 leave方法里,timer的id:"+timer,“事件的来源:",source); console.log(“leave result:",timer); onClose&&onClose(); } let enter = () => { setVisible(true); if( duration > 0 ){ let timer = setTimeout(() => { console.log(auto carried out,timer) //timer Number Id leave(Time to); }, duration1000); console.log(enter方法里,timer的id:,timer) //timer Number Id } } useEffect(()=>{ enter(); },[]) return ( <div className={${prefixCls}-notice} style={{display:${visible?'':'none'}}}> {!!theme&&<p className={${prefixCls}-notice-icon}><Svg iconId={svg-${theme}} /></p>} <div className={${prefixCls}-notice-content}> ……//首席填坑官∙苏南的专栏 交流:912594095、公众号:honeyBadger8 </div> <p className={${prefixCls}-notice-colse} title=“关闭” onClick={()=>leave(“手动点击的关闭”)}><Svg/></p> </div> );};简单分析:首先useEffect方法,是react新增的,它是componentDidMount,componentDidUpdate、componentWillUnmount三个生命周期的合集,也就是之前的写法,上面三生命周期里会执行到的操作,useEffect都会去做;enter、leave方法很好理解,进场、出场两函数,进场:加了个定时器,在N秒后执行出场即leave方法,这个逻辑是正常的,问题就出在手动执行leave,也就是onclick事件上,问题原因:其实就是在点击事件的时候,没有获取到 timer的id,导致了定时器没有清除掉;!!看图说话:解决思路:当然是看官方文档,hooks对我来说也是个新玩意,不会~1、useEffect方法里return 一个方法,它是可以在组件卸载时执行的,2、清除定时器它有自己的方式,const intervalRef = useRef();指定赋值后能同步更新,之前的timer手动执行没有拿到timer所以没有清除掉;参考链接:中文,英文的没有找到文档英文的也补一下吧react github也有人提到这个问题,学习了完美解决:function Notification(props){ var timer = null; const [visible, setVisible] = useState(false); let {title,description,duration,theme,onClose,}= props; const intervalRef = useRef(null); let leave = (source=’’) => { clearTimeout(intervalRef.current); setVisible(false); console.log(“leave result:",source,intervalRef); onClose&&onClose(); } let enter = () => { setVisible(true); if( duration > 0 ){ let id = setTimeout(() => { console.log(auto carried out,intervalRef) //timer Number Id leave(Time to); }, duration1000);//首席填坑官∙苏南的专栏 交流:912594095、公众号:honeyBadger8 intervalRef.current = id; } } useEffect(()=>{ enter(); return ()=>clearTimeout(intervalRef.current); },[]) return ( <div className={${prefixCls}-notice} style={{display:${visible?'':'none'}}}> {!!theme&&<p className={${prefixCls}-notice-icon}><Svg iconId={svg-${theme}} /></p>} <div className={${prefixCls}-notice-content}> ……//首席填坑官∙苏南的专栏 交流:912594095、公众号:honeyBadger8 </div> <p className={${prefixCls}-notice-colse} title=“关闭” onClick={()=>leave(“手动点击的关闭”)}><Svg/></p> </div> );};热门推荐资源共享,一起学习团队解散,我们该何去何从?如何给localStorage设置一个有效期?作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin_…交流:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

December 6, 2018 · 2 min · jiezi

看完React文档后,重写了下CnodeJS社区,感觉看上去还不错

Github DEMO 欢迎Star一个 React 的初/中级练习项目:重构 CnodeJS 社区这里简述下我学习 React 的方式:看了一边文档,把‘主要概念’全部看完,‘高级指引’里的根据个人兴趣挑了一些看了看。紧接着就动手写此项目(大概花了 3 天的业余时间完成)在此期间根据需要,看了 React-Router 文档,了解了 Redux 的实现原理(但本项目并没有用 Redux)(歪个楼:“我相信任何复杂技术背后的原理都是简单易懂的”)P.S. 如果你想入门 React,但苦于没有难度合适的项目的话,It’s For You!(考虑到别人可能会看我源码来学习,我补充了“非常详细的注释”)P.P.S. 朋友看到后吐槽了句“怎么不是cnode,就是v2ex”(我补充了下“爬虫的话就是煎蛋? ”)XD

December 4, 2018 · 1 min · jiezi

在 React Native 使用阿里 iconfont 图标

熟悉前端开发的大家都一定知道 iconfont.cn,在网站下载图标集,会自带教程告诉你如何在网页使用 iconfont。但是在 React Native 中,跟网页使用的步骤就不同了。我最开始百度出来的文章,不少都推荐借用 react-native-vector-icons,但是我觉得这一步还是增加了不少无用代码。其实使用 iconfont,本质上就是使用一种“图标形状的字体”,所以解决问题只需要三步:安装字体找到图标对应的 unicode,直接放到 <Text> 标签中在该标签应用你的“图标字体”安装字体这是一个把配置都交给 react-native 的一个简单方法:首先在 package.json 添加代码"rnpm": { “assets”: [ “./assets/fonts/” ]},然后运行react-native link你就可以在 plist 文件(iOS)或 android/app/src/main/assets/fonts(安卓)中看到对应配置安装后需要重启 react-native添加图标打开在 iconfont 下载的图标集会有这个文件里面可以看到对应图标的 unicode把你使用的图标放到标签中 <Text>{’\ue936’}</Text>应用字体<Text style={{ fontFamily: “iconfont”, fontSize: FONTSIZE, marginRight: 9}}>{’\ue936’}</Text>有一点需要注意,写在 fontFamily 的字体名称要使用全名(而不是文件名)不过 iconfont 三个名字都一样就是了参考文章:https://medium.com/react-nati…http://www.ruanyifeng.com/blo…

November 3, 2018 · 1 min · jiezi

beeshell:开源的 React Native 组件库

介绍beeshell 是一个 React Native 应用的基础组件库,基于 0.53.3 版本,提供一整套开箱即用的高质量组件,包含 JavaScript(以下简称 JS)组件和复合组件(包含 Native 代码),涉及前端(FE)、iOS、Android 三端技术,兼顾通用性和定制化,支持自定义主题,用于开发和服务企业级移动应用。现在已经在 GitHub 上开源,地址:https://github.com/meituan/be…截止目前,beeshell 中的组件已经在美团外卖移动端应用蜜蜂 App 中广泛应用,而且已经持续了一年多时间,通过了各种业务场景、操作系统、机型的实战考验,具备很好的稳定性、安全性和易用性,所以我们将其开源,以期发挥出更大的应用价值。特性UI 样式的一致性和定制化。通用性。主要使用 JS 来实现,保证跨平台通用性。定制化。我们在比较细的粒度上对组件进行拆分,通过继承的方式层层依赖,功能渐进式增强,为在任意层级上的继承扩展、个性化定制提供了可能。原生功能支持。组件库中的复合组件包含 Native 代码,支持图片选择、定位等原生功能。功能丰富。不仅仅提供组件,还提供了基础工具、动画以及 UI 规范。完善的文档和使用示例。对比在开源之前,我们对业界已经开源的组件库进行了调研,这里主要对比了 beeshell 与其他组件库的优势与劣势,为大家选择组件库提供参考意见。目前,业界开源的组件库比较多,我们在这里仅选取 Github Star 数 5000 以上的组件库,并从组件数量、通用性、定制化、是否包含原生功能、文档完善程度五个维度来进行对比分析组件库组件数量通用性定制化是否包含原生功能文档完善程度react-native-elements16强,提供一套风格一致的 UI 控件弱,若要定制化可能需要重写否高NativeBase28强,提供一套风格一致的 UI 控件中,支持主题变量是高ant-design-mobile41强,提供一套风格一致的 UI 控件中,部分可以支持定制化需求是低beeshell25强,提供一套风格一致的 UI 控件强,不仅支持主题变量,还支持使用继承的方式进行定制化扩展是高通过对比可以看出,beeshell 只在组件数量上稍有劣势,在其他方面都一致或者优于其他项目。因为 beeshell 具备了良好的系统架构,所以丰富组件数量只时间问题,而且我们团队也已经有了详细的规划来完善数量上的不足。系统设计系统设计是将一个实际问题转换成相应解决方案的主动过程,是解决办法的描述。在通用的软件工程模型中,需求分析完成后的第一步就是系统设计。一个项目最终的稳定性、易用性在很大程度上也取决于系统设计这一步。beeshell 组件库是为了更加快速的搭建移动端应用,为业务开发提供基础技术支持,大幅提升开发人效。然而,面对不同的业务方、不同的功能需求、不同的 UI 规范与交互方式,如何有效的兼顾所有的需求?这对系统设计提出了更高的要求,下面以抽象层次逐层降低的方式来详细介绍 beeshell 的系统设计。框架设计这些年,React Native 的出现为移动端开发提供了一种新的选择。React Native 相比原生开发有着更高的开发效率,同时比 HTML5、Hybrid 的性能更好,所以能够脱颖而出,这也使得越来越多的开发者开始学习和使用 React Native。beeshell 组件库基于 React Native,向下通过 React Native 与 iOS、Android 平台进行系统层面的交互,向上提供开发者友好的统一接口,抹平平台差异,为用户开发业务功能提供服务支持。beeshell 扮演了一个中间者的角色,从而保证了移动端应用基础功能的稳定性、易用性。框架设计确定了 beeshell 的系统边界,指明了包含的功能与不包含的功能之间的界限。明确了系统边界,我们才能继续进行下面的分析、设计等工作。设计原则在进行组件库的详细设计之前,我们提出了几个设计原则:JS 实现优先。使用 JS 来实现功能有几个好处:跨平台通用性、更高的开发效率、更低的学习和使用成本。继承与组合灵活运用。继承和组合都是实现功能复用、代码复用的有效的设计技巧,都是设计模式中的基础结构。继承允许子类覆盖重写父类的实现细节,父类的实现对于子类是可见的,一般称之为“白盒复用”,这对组件的定制化扩展很有效,beeshell 强大的定制化扩展的能力就是基于继承实现;组合是 React 推荐的方式,React 组件具有强大的组合模型,整体类和部分类之间不会去关心各自的实现细节,它们之间的实现细节是不可见的,一般称之为“黑盒复用”。beeshell 也广泛使用了组合,基于通用型的组件组合出更加丰富、强大、个性化的功能,在一定程度上提高了 beeshell 的定制化的能力。低耦合、高内聚。一个 beeshell 组件本质上就是一个 React 组件,React 组件之间主要通过 Props 通信,这属于数据耦合,相比于内容耦合、控制耦合等其他耦合方式,数据耦合是耦合程度最低的一种,受益于 React 的实现,beeshell 组件低耦合是自然而然的;而要做的高内聚,则对组件的编码实现方式有一定的要求,我们推行内聚方式中内聚程度比较高的交互内聚和顺序内聚。使用单一数据源,使各个元素操作相同的数据结构,实现交互内聚。使用不可变数据更新的方式,上一个环节的输出是下一个环节的输入,像流水线一样处理逻辑,这便是顺序内聚。方案设计整体上使用 JS 作为统一入口,多层封装隐藏实现细节,抹平 JS 与 Native、iOS 平台与 Android 平台的差异,开箱即用,降低了用户的学习和使用成本。局部上基于 React Native 的技术特点,分成 JS 组件部分和复合组件部分,两部分推行“松耦合”的开发模式,使得 Native 部分拥有替换变更的能力,提升组件库的灵活性。复合组件部分可以直接暴露 JS 接口,如果有需要,也可以在 JS 组件部分进行定制化封装。我们尽量保证 Native 部分功能的原子性、简洁性,有任何定制化需求都使用 JS 来统一实现,遵循 JS 实现优先的设计原则,保证跨平台通用的特性。下面分别介绍 JS 组件部分和复合组件部分的设计。JS 组件部分设计一个软件的设计分为三个设计层次:体系结构、代码设计和可执行设计。我们使用自上而下的方法,从体系结构开始进行 JS 组件部分的设计。软件的体系结构的风格通常有 7 种:管道和过滤器,面向对象,隐式请求,层次化,知识库,解释程序和过程控制。JS 组件部分使用了层次化的体系结构风格,整体分成三层:基础工具、通用组件、扩展组件,从上到下通用性逐渐减弱、定制化逐渐增强,功能渐进式增强,通过分层设计,各层各司其职,兼顾通用性和定制化。基础工具(common):最基础的、通用的部分,包含 JS Utils、动画定义、UI 规范等。通用组件(components):把功能相似的组件进行归类,整理成一个个系列,每个系列内部使用继承的方式实现,层层依赖,功能渐进式增强,该部分专注通用性,不考虑定制化需求,保证代码的简洁性。同时,在比较细的粒度对组件进行拆分,提供了良好的可扩展性。扩展组件(modules):是对通用组件的继承扩展、组合应用,该部分专注定制化,在最大程度上满足业务上的需求,通用性较低。我们扩展组件部分会提供大量的定制化组件,如果仍然不能满足需求,用户就可以借鉴扩展组件的实现,根据自己业务需求,在某一继承层级上继承通用组件,自行进行定制化扩展,这点充分体现了 beeshell 定制化的能力。复合组件部分设计既然是 React Native 组件库当然少不了 Native 部分,复合组件包含 Native 的功能。beeshell 组件库已经完成了 Native 部分的集成方案与规范,有良好的开发与使用体验,可以不断的集成原生功能。复合组件部分通过 JS 封装接口,保证了跨平台。Native 部分主要分成 Native Bridge 和纯 Native 两大部分,Bridge 是针对 React Native 的封装,必须在组件库中实现;而纯 Native 部分则可以通过 Pods/Gradle 依赖三方实现,有效的吸收利用原生开发的技术积累。组件库实现跨平台通用性保障React Native 提供了一些内置组件,我们能使用 JS 来实现功能都是基于这些内置组件,这些内置的组件一些是跨平台通用的组件,如:View、Text、TextInput;而另一些是两个平台分别实现的,如 DatePickerIOS 和 DatePickerAndroid、AlertIOS 和 ToastAndroid。跨平台组件当然没有什么问题,我们可以专注业务功能的开发,问题是这些非跨平台的组件,给我们的业务功能开发带来极大困扰,下面举例说明。iOS 平台的 DatePickerIOS 组件:Android 平台的 DatePickerAndroid 组件:不仅功能交互完全不同,而且类名、调用方式各异,这不仅满足不了业务需求,而且也有很高的学习和使用成本。这样类似的组件还有很多,如何抹平平台的差异,实现跨平台?我们提出的方案是优先使用 JS 来实现功能,这也是我们组件库的设计原则。针对上面的问题我们开发了基于 ScrollView 的 Datepicker 组件,统一类名与调用方式,保证了跨平台通用性。iOS 平台的 Datepicker 组件:Android 平台的 Datepicker 组件:Datepicker 是使用 JS 完全实现了一个完整功能,但是有的情况不需要实现完整的功能,我们可以通过 React Native 提供的 Platform 来进行局部的跨平台处理,例如 TextInput 组件。iOS 平台的 TextInput 组件:Android 平台的 TextInput 组件:我们可以看到,在 Andriod 平台并没有清空图标,为了抹平平台的差异,提供更好的通用性,我们开发了 Input 组件,对 TextInput 进行封装与优化,利用 Platform 定位 Android 平台提供清空功能,Input 组件在 Android 平台的效果:总之,beeshell 对跨平台通用性做了进一步的优化,遵循 JS 实现优先的原则,配合 Platform 平台定位 API 为组件的易用性、通用性提供了更好的保障。定制化支持随着移动互联网的快速发展,各类移动端产品涌现并且不断发展,这也让软件知识不断被普及,业务方对产品功能的定位逐渐从厂商主导转变为用户主导。产品功能更加精准,个性化、细化、深化是必然趋势,通过定制化服务来满足产品发展的要求也应运而生。不同行业、不同类型的产品,功能、特点各不相同,用某一种既定的软件产品来满足不同类型的需求,其适用性可想而知。定制化有良好的技术架构和技术优势,可定制、可扩展、可集成、跨平台,在个性化需求的处理方面,有着很好的优势,所以我们需要定制化。综上所述,beeshell 把定制化作为核心特性,力求满足不同产品的定制化需求,下文将从组件的样式定制化和功能定制化两方面来进行阐述。样式定制化beeshell 的设计规范支持一定程度的样式定制,以满足业务和品牌上多样化的视觉需求,包括但不限于品牌色、圆角、边框等的视觉定制。在组件库设计之初,就已经统一好了 UI 规范。我们根据 UI 规范,统一定义样式变量并放置在基础工具层中,即 beeshell/common/styles/varibles.js 文件中,在 React Native 应用中,样式变量其实就是普通的 JS 变量,可以很方便的进行复用与重写操作。React Native 提供了 StyleSheet 通过创建一个样式表,使用 ID 来引用样式,减少频繁创建新的样式对象,在组件库的样式变量应用中灵活使用 StyleSheet.create 和 StyleSheet.flatten 来获取样式 ID 和样式对象。在每个组的实现中,会事先引入基础工具层中的样式变量,使用统一的变量对象而不是在组件中自行定义,这样就保证了 UI 样式的一致性。同时,beeshell 提供了重置样式变量的 API,可以实现一键换肤。我们推荐 beeshell 的用户在开发移动应用时,事先定义好样式变量。一方面使用自己的样式变量重置 beeshell 的样式变量;另一方面在业务功能开发时,使用自己定义好的样式变量,从而保证整体 UI 的一致性。功能定制化样式定制化可以从宏观和整体的角度来实现,而功能的定制化则需要具体问题具体分析,从微观和局部的角度来分析和实现。下文将以 Modal 系列的实现为例,来详细介绍功能定制化。在移动端的弹窗交互,与 PC 端相比一般会比较简单,我们把模态框、下拉菜单、信息提示等交互类似的组件统一归类为 Modal 系列,使用继承的方式实现。有人可能会问为什么使用继承而不用使用组合?前文已经讲过,组合的主要目的是代码复用,而继承的主要目的是扩展。考虑到弹窗交互有很多定制化的可能性,为了满足更好的扩展性,我们选择了继承。首先我们看下几个组件的实现效果图,对 Modal 系列先有一个直观的认识。Modal 组件:提供了遮罩、弹出容器以及淡入淡出(Fade)动画效果,弹出内容部分完全由用户自定义。这个组件通用性极强,没有任何定制化的功能。这里需要说明下,动画部分独立实现,提供了 FadeAnimated 和 SlideAnimated 两个子类,使用了策略模式与 Modal 系列集成,Modal 组件默认集成 FadeAnimated。ConfirmModal 组件:继承 Modal 组件,对弹出内容做了一定程度的定制化扩展,支持标题、确认按钮、取消按钮以及自定义 body 部分的功能,通用性减弱,定制化增强。SlideModal 组件:继承 Modal 组件,对动画、弹出容器做了重写,在初始化时实例化 SlideAnimated 类型对象,完成上拉、下拉动画,同时支持了自定义弹出位置的功能。PageModal 组件:继承 SlideModal 组件,对弹出内容做了定制化扩展,支持标题、确认按钮、取消按钮以及自定义 body 功能,通用性减弱,定制化增强。CheckboxModal 组件:CheckboxModal 组件由 PageModal 和 Checkbox 两个组件使用组合的方式实现,基于通用型组件组合出了更加强大功能,遵循继承与组合灵活运用的设计原则。通过以上部分,我们已经对 Modal 系列已经有了直观的认识,然后我们来看下 Modal 系列的类图以及分层:动画部分在基础工具(common)中实现;在通用组件(components)中 Modal 组件聚合 FadeAnimated 动画,同时因为 SlideModal、ConfirmModal 比较通用,也在该部分实现;CheckboxModal 则定制化比较强,归类到扩展组件(modules)中。通过这种方式的分层,三层各司其职,使得组件库的层次结构更加清晰,不仅实现了定制化,还保证了通用部分的简洁性和可维护性。复杂 Case 处理相互递归处理异步渲染React Native 应用的 JS 线程和 UI 线程是两个线程,与浏览器中共用一个线程的实现不同,所以我们可以看到 React Native 提供的操作 UI 元素的 API,都是通过回调函数的方式进行调用。受益于 React,我们一般不需要直接操作 UI 元素,但是有的组件确实需要复杂的 UI 操作,例如完全由 JS 实现的 Scrollerpicker 组件:我们需要精确的计算容器以及每一项元素的高度,才能正确得到当前选中的项在数据模型(数组)中的索引。现在面临的问题是:在组件渲染完成后的生命周期 componentDidMount 并不能拿到正确容器的高度为,而使用 setTimeout 也会有延迟时长设置为多少的问题。我们选择使用递归来解决,一次 setTimeout 不行就执行多次。这里使用了交互递归,反复执行,直到得到有效的元素尺寸。UI 尺寸容错机制React Native 为用户提供了 style 属性来控制元素的样式,我们可以手动设置相关 UI 元素的尺寸。但是,在一些 Android 机器上,我们设置的元素尺寸与 measure 方法获取的尺寸信息不一致,经过大量 Android 机器的实际的测试,我们得到的结论是:有零点几像素的误差。我们把通过 measure 方法得到尺寸信息进行向上与向下取整,得到一个阈值范围,手动设置的尺寸信息只要在这个阈值范围内,就认为是有效尺寸,这种容错机制有效的兼容了极端情况,提高了组件的稳定性。精细化布局控制在使用 Form 组件时,最常见的需求就是校验功能,通常组件库的 Form 组件都会内置校验功能。然而,因为校验方式有同步与异步两种,校验结果展示的样式、位置五花八门,这就导致了校验功能的复杂度变得很高。绝对定位:Static 定位:自定义位置如何有效的兼顾不同的需求?我们提出了校验独立实现的方式,在使用 Form 组件的父组件中,使用 CVD 来定义、配置校验规则,校验结果输出到统一的数据结构(单一数据源),基于这个数据结构,我们就能在任意时机、任意位置、使用任意样式来展示校验信息。下面我们先介绍下 CVD:CVD 是一个针对复杂表单录入场景的分层解决方案,轻量级、跨平台、易扩展,内置在 beeshell 组件库中,可以直接使用。CVD 把表单某个控件的录入的流程分成三层:Connector 连接器,把用户输入的信息转化成所需的数据格式。Validator 校验器,对格式化的数据进行校验。Dependency 依赖处理器,处理当前控件与其他控件的依赖关系。每一层都对单一数据源 Store 进行不可变数据更新,符合交互内聚和顺序内聚,内聚程度高。每一层使用函数式组合的方式,定义 key(表单控件的唯一标志)与 key 对应的回调函数,避免了批量 if else,可以有效降低程序的圆环复杂度。下面以 Input 组件录入姓名为例,来具体说明,代码如下:在 onChange 中获取用户输入,调用 cvd.flow 然后就可以通过 cvd.getStore 获取到结果:通过校验功能独立实现,把校验信息输出到 Store 中,在需要的时候从 Store 中获取校验信息,可以更加精细化的控制元素的样式、位置与布局,兼容各种定制化需求。很多时候,只有我们想不到,没有做不到。测试代码的终极目标有两个,第一个是实现需求,第二个是提高代码质量和可维护性。测试是为了提高代码质量和可维护性,是实现代码的第二个目标的一种方法。单元测试单元测试(Unit Testing),是指对软件中的最小可测试单元进行检查和验证。在结构化编程的时代,单元测试中单元指的就是函数。beeshell 组件库全面使用单元测试,由组件的开发者完成。研究成果表明,无论什么时候作出修改都需要进行完整的回归测试,对于提供基础功能的组件来说更是如此,在生命周期中尽早地对软件产品进行测试将使效率和质量都得到最好的保证。Bug 发现的越晚,修改它所需的成本就越高,单元测试是一个在早期抓住 Bug 的机会。单元测试的优点有以下几点:是一种验证行为。程序的每一项功能是测试来验证正确性,为后期的增加功能、代码重构提供了保障。是一种设计行为。单元测试使得我们从调用者的角度观察、思考,迫使开发者把程序设计成易于调用和可测试的,在一定程度上降低耦合性。是一种编写文档的行为。是展示函数、类使用的最佳文档。beeshell 组件库使用 Jest 做为单元测试的工具,自带断言、测试覆盖率工具,实现开箱即用。测试用例设计测试用例的核心是输入数据,我们会选择具有代表性的数据作为输入数据,主要有三种:正常输入,边界输入,非法输入,下面以组件库中提供的 isLeapYear 工具函数来举例说明,代码如下:Jest 使用 test 函数来描述一个测试用例,其中的 toBe 边是一句断言。函数使用了外部数据,正常输入肯定会有,这里的 2000 和 ‘2000’ 都是正常输入;边界输入和非法输入并不是所有的函数都有,这里为了说明使用了有这两种输入的例子,边界输入是有效输入的极限值,这里 0 和 Infinity 是边界输入;非法输入是正常取值范围以外的数据, ‘xx’ 和 false 则是非法输入。一般情况下,考虑以上三种输入可以找出函数的基本功能点,单元测试与代码编写是“一体两面”的关系,编码时对上述三种输入都是应该考虑的,否则代码的健壮性就会出现问题。上文所说的测试是针对程序的功能来设计的,就是所谓的“黑盒测试”。单元测试还需要从另一个角度来设计测试数据,即针对程序的逻辑结构来设计测试用例,就是所谓的“白盒测试”。还是以 isLeapYear 函数来进行说明,其代码如下:这里有一个 if else 语句,如果我们只提供一个 2000 的输入,只会测试到 if 语句,而不会测试 else 语句。虽然,在黑盒测试足够充分的情况下,白盒测试没有必要,可惜“足够充分”只是一种理想状态,难于衡量测试的完整性是黑盒测试的主要缺陷。而白盒测试恰恰具有易于衡量测试完整性的优点,两者之间具有极好的互补性,例如:完成功能测试后统计语句覆盖率,如果语句覆盖未完成,很可能是未覆盖的语句所对应的功能点未测试。白盒测试也是比较常见的需求,Jest 内置了测试覆盖率工具,可以直接在命令中添加 –coverage 参数便可以输出单元测试覆盖率的报告,结果如下:可以看到代码的每一行都覆盖到了 Coverage 为 100%,在很大程度上保证了功能的稳定性。UI 自动化测试想要确保组件库的 UI 不会意外被更改,快照测试(Snapshot Testing)是非常有用的工具。一个典型的移动 App 快照测试案例过程是,先渲染 UI 组件,然后截图,最后和独立于测试存储的参考图像进行比较。使用 Jest 进行在快照测试,在 beeshell 中第一次对某个组件进行测试时,会在测试目录下创建一个 snapshots 文件夹,并将快照结果存放在该文件夹中。快照结果文件以 <组件名>.js.snap 命名,其内容为某个状态下的 UI 组件树。下面以 Button 组件快照测试为例来说明:运行命令后得到快照结果:静态分析经常与单元测试联系起来的开发活动还有静态分析(Static analysis)。静态分析就是对软件的源代码进行研读,查找错误或收集一些度量数据,并不需要对代码进行编译和执行。静态分析效果较好而且快速,可以发现 30%~70% 的代码问题,可以在几分钟内检查一遍,成本低、收益高。beeshell 使用 SonarQube 进行静态代码检查。SonarQube 是一个开源的代码质量管理系统,支持 25+ 种语言,可以通过使用插件机制与 Eclipse、VSCode 等工具集成,实现对代码的质量的全面自动化分析和管理。SonarQube 通过对 Reliability(可靠性)、Security(安全性)、Maintainability(可维护性)、Coverage(测试覆盖率)、Duplications(重复)几个维度,对代码进行全方位的分析,通过设置 Quality Gates 保证代码质量。beeshell 组件库的分析结果概况如图:可靠性达到 A 级别,是最高等级,表示无 Bug:安全性达到 A 级别,是最高等级,表示无漏洞:测试覆盖率平均达到 70% 以上开发与使用一致性beeshell 组件库使用 npm 包的形式下载使用,下载成功后会放置在项目根目录的 node_modules 目录,然后在项目中通过引入模块的方式,引入 beeshell 的组件来使用。那我们如何开发组件库?如何保证组件库的开发与使用的体验一致性?首先,我们需要一个 demo 项目,这个项目是 beeshell 组件库的开发环境,是一个 React Native 应用。然后,我们把 beeshell 做为 demo 项目的依赖,在 demo 项目中下载安装。现在,我们的问题就变成了 node_modules 目录中的 beeshell 如何和本地的 beeshell 源码进行同步。npm link我们知道可以使用 npm link 来开发 npm 包,原理如下:本质是就是使用 Symbol link,但是我们建立好软链接后,运行打包命令却报错了,错误信息为 Expected path ‘/xxx/xxx/index.js’ to be relative to one of project roots我们前端开发通常会用 Webpack 做为打包工具,而 React Native 应用使用的是 Metro,我们需要分析 Metro 来定位问题。Webpack vs Metro经过 Metro 的源码分析,我们发现 Metro 的打包方案与 Webpack 有较大差异,Webpack 是根据入口文件,即配置中的 entry 属性,递归解析依赖,构建依赖关系图而 Metro 是爬取特定路径下的所有文件来构建依赖关系图。分析发现 Metro 的特定路径默认是运行打包命里的路径,以及 node_modules 下第一层目录,这样我们就定位到了问题的所在:Metro 在爬取文件的时候,通过软链接找到了全局的 beeshell 但是并没有继续判断全局的 beeshell 是否有软链接,所以无法爬取 beeshell 源码部分。直接使用软链接通过 ln -s 命令,直接建立 demo 项目 node_modules 下 beeshell 包 与 beeshell 源码的软链接:这种方式同时支持 Native 部分 iOS、Android 的源码开发,注意 Android 部分的需要在 setting.gradle 中调用 getCanonicalPath 方法获取建立软链接后的路径。通过试验、发现问题、分析源码、定位问题、解决问题、方案完善这几个步骤,完整的实现了 beeshell 组件库的开发与使用的体验一致性,同时提升了组件库的开发效率。未来展望我们的目标是把 beeshell 建设成为一个大而全的组件库,不仅会不断丰富 JS 组件,而且会不断加强复合组件去支持更多的底层功能。因为我们支持全部引入和按需引入两种方式,用户不需要担心会引入过多无用组件而使得包体积过大,影响开发和使用效率。beeshell 目前提供了 20+ 组件以及基础工具,基于良好的架构设计、开发体验,为我们不断地丰富组件库提供了良好的基础。同时在开发 React Native 应用的几年时间中,我们已经积累了 50+ 基础以及业务组件,我们后续会把积累的组件进行梳理与调整,全部迁移到 beeshell 中。因为我们的组件主要来源于我们的业务需求,但是业务场景有限,可能会使得 beeshell 的发展受到限制,所以我们将其开源。希望借助社区的力量不断丰富组件库的功能,尽最大努力覆盖到移动应用方方面面的功能,欢迎大家献计献策,多多支持。我们为组件库发展规划了三个阶段:第一阶段,即我们现在所处的阶段,开源 20+ 组件,主要提供基础功能。第二阶段,对我们在开发 React Native 应用几年时间积累的组件进行整理,开源 50+ 组件。第三阶段,调研移动端 App 常用的功能,分析与整理,然后在 beeshell 中实现,开源 100+ 组件。开源相关Git 地址beeshell核心贡献者前端:小龙,孟谦Native:渊博,杨超 ...

September 29, 2018 · 3 min · jiezi

React Native Fetch封装那点事...

每一门语言都离不开网络请求,有自己的一套Networking Api。React Native使用的是Fetch。 今天我们来谈谈与Fetch相关的一些事情。purpose通过这篇文章,你将了解到以下几点关于Fetch的独家报道Fetch的简单运用Fetch的主要ApiFetch使用注意事项Fetch的Promise封装fetchfetch的使用非常简单,只需传入请求的urlfetch(‘https://facebook.github.io/react-native/movies.json');当然是否请求成功与数据的处理,我们还需处理成功与失败的回调function getMoviesFromApiAsync() { return fetch(‘https://facebook.github.io/react-native/movies.json') .then((response) => response.json()) .then((responseJson) => { return responseJson.movies; }) .catch((error) => { console.error(error); });}通过response.json()将请求的返回数据转化成json数据以便使用。通过.then来对数据进行转化处理或最终暴露给调用者;.catch对异常的处理。以上就是一个简单的网络请求,该请求默认是get方式。那么post又该如何请求呢?Api & Note在fetch中我们直接传入url进行请求,其实内部本质是使用了Request对象,只是将url出入到了Request对象中。const myRequest = new Request(‘https://facebook.github.io/react-native/movies.json'); const myURL = myRequest.url; // https://facebook.github.io/react-native/movies.jsonflowers.jpgconst myMethod = myRequest.method; // GET fetch(myRequest) .then(response => response.json()) .then(responseJson => { //todo });如果我们需要请求post,需要改变Request的method属性。fetch(‘https://mywebsite.com/endpoint/', { method: ‘POST’, headers: { Accept: ‘application/json’, ‘Content-Type’: ‘application/json’, }, body: JSON.stringify({ firstParam: ‘yourValue’, secondParam: ‘yourOtherValue’, }),});非常简单,在url后直接传入{}对象,其中指定method使用post。相信大家应该都知道get与post的一个主要区别是get可以在url上直接添加参数,而post为了安全都不采用直接将参数追加到url上,而是使用body来传给service端。在使用body前,这里还需知道headers。下面某个post请求的headers信息需要注意的是Content-Type字段,它代表的是service端接收的数据类型,图片中使用的是application/x-www-form-urlencoded。这对于我们的body来说是非常重要的。只有匹配Content-Type的类型才能正确的传递参数信息。示例的代码使用的是application/json,所以body使用Json.stringify()进行参数转换,而对于Content-Type为application/x-www-form-urlencoded,需要使用queryString.stringify()。Request中除了method、headers与body,还有以下属性Request.cache: 请求的缓存模式(default/reload/no-cache)Request.context: 请求的上下文(audio/image/iframe)Request.credentials: 请求的证书(omit/same-origin/include)Request.destination: 请求的内容描述类型Request.integrity: 请求的 subresource integrityRequest.mode: 请求的模式(cors/no-cors/same-origin/navigate)Request.redirect: 请求的重定向方式(follow/error/manual)Request.referrer: 请求的来源(client)Request.referrerPolicy: 请求的来源政策(no-referrer)Request.bodyUsed: 声明body是否使用在response中请求成功之后,使用.then来转换数据,使用最多的是Body.json(),当然你也可以使用以下的几种数据转换类型Body.arrayBufferBody.blobBody.formDataBody.text以上是fetch请求相关的属性与方法。如果你已经有所了解,那么恭喜你对fetch的基本使用已经过关了,下面对fetch的使用进行封装。封装在实际开发中,url的host都是相同的,不同的是请求的方法名与参数。而对于不同的环境(debug|release)请求的方式也可能不同。例如:在debug环境中为了方便调试查看请求的参数是否正确,我们会使用get来进行请求。所以在封装之前要明确什么是不变的,什么是变化的,成功与失败的响应处理。经过上面的分析,罗列一下封装需要做的事情。不变的: host,headers,body.json()变化的: url,method,body响应方式: Promise(resolve/reject)function convertUrl(url, params) { let realUrl = ApiModule.isDebug? url + “?” + queryString.stringify(Object.assign({}, params, commonParams)) : url; if (ApiModule.isDebug) { console.log(“request: " + realUrl); } return realUrl;}首先对url与参数params进行拼接,如果为debug模式将params拼接到url后。这里使用到了Object.assign()将params与commonParams组合成一个{}对象。最终通过queryString.stringify转化成string。ApiModule.isDebug是原生传递过来的值,对于Android/IOS只需传递自己的ApiModule即可。function getMethod() { return ApiModule.isDebug? “get”: “post”;}上述提到的get与post的请求时机。const headers = { Accept: ‘application/json’, “Content-Type”: “application/x-www-form-urlencoded;charset=UTF-8”};在headers中Content-Type类型为application/x-www-form-urlencodefunction convertBody(params) { return ApiModule.isDebug? undefined : queryString.stringify(Object.assign({}, params, commonParams));}由于debug模式使用的是get方式,但get规定是不能有body的,所以这里使用了undefined来标识。同时为了匹配headers中的Content-Type,params的转化必须使用queryString.stringify;如果接受的是json,可以使用JSON.stringify。定义完之后fetch对外只需接受params参数即可。async function fetchRequest(params){ let body = convertBody(params); fetch(convertUrl(baseUrl, params),{ method: method, headers: headers, body: body }) .then((response) => response.json()) .then((responseJson) => { //todo success }) .catch((error) => { if (ApiModule.isDebug) { console.error(“request error: " + error); }; //todo error });}fetch的请求封装完成,但我们的成功与失败的状态并没有通知给调用者,所以还需要一个回调机制。Promise是一个异步操作最终完成或者失败的对象。它可以接受两个函数resolve、rejectconst p = new Promise((resolve, reject){ … //success resolve(‘success’) //error reject(’error’)});//usep.then(success => { console.log(success); }, error => { console.log(error) });将fetch请求放入到Promise的异步操作中,这样一旦数据成功返回就调用resolve函数回调给调用者;失败调用reject函数,返回失败信息。而调用者只需使用Promise的.then方法等候数据的回调通知。下面来看下完整的fetch封装。async function fetchRequest(params){ let body = convertBody(params); return new Promise(function(resolve, reject){ fetch(convertUrl(baseUrl, params),{ method: method, headers: headers, body: body }) .then((response) => response.json()) .then((responseJson) => { resolve(responseJson); }) .catch((error) => { if (ApiModule.isDebug) { console.error(“request error: " + error); }; reject(error); }); });}之后对fetch的使用就非常简单了,只需传入需要的参数即可。fetchRequest({method: “goods.getInfo”, goodsId: 27021599158370074}).then(res =>{ this.setState({ shareInfo: res.data.shareInfo });});以上是有关fetch的全部内容,当然在React Native中还有其它的第三方请求库:XMLHttpRequest,同时也支持WebSockets。感兴趣的也可以去了解一下,相信会有不错的收获。精选文章5分钟吃透React Native FlexboxViewDragHelper之手势操作神器tensorflow-梯度下降,有这一篇就足够了七大排序算法总结(java)拉粉环节:感觉不错的可以来一波关注,扫描下方二维码,关注公众号,及时获取最新知识技巧。 ...

August 30, 2018 · 2 min · jiezi

react-native flatlist上拉加载onEndReached不能正确触发的解决办法

问题 在写flatlist复用组件时,调用的时候如果父组件是不定高的组件,会造成组件无法显示 如果父组件样式{flex:1},则会出现下拉方法频繁触发或不正常触发的问题(我这里出现的问题是在列表第6个项目在底部时,缓慢上拉会多次触发flatlist的onEndReached监听) 原因 推测是因为{flex:1}不适合做动态高度组件的父组件样式,会错误的判断高度导致onEndReached多次不正常触发。 解决 可以把列表上方所需的组件做成header属性传入组件当做flatlist的头部组件,这样就可以直接调用封装好的组件。 也可以把父元素的样式设成{height: ‘100%’},这样就可以正确的触发onEndReached监听。

August 27, 2018 · 1 min · jiezi