SegmentFault react-native最新的文章
2016-04-12T16:37:37+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
ReactNative导航设计与实现
https://segmentfault.com/a/1190000004923877
2016-04-12T16:37:37+08:00
2016-04-12T16:37:37+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
1
<blockquote><p><a href="https://link.segmentfault.com/?enc=0Udk0xBcrEdT7GQI0r5LPw%3D%3D.FFV1QctM%2FcV9uTYJ9QhxqA1zUmyipgAhT10d43%2Fd8vMVV4wWSbvYqRAvoR32WHNr6oX5VJ2jb2Xa6D7nWMeehsYTk8xBb3mj4Pg5maWRsL%2B5FpghVA6BIxyyEq6GPOzo%2BETxdQFvxhLJApxjz2p72iqxo%2FfaXcod075%2Fr1DZJCa0ZIhazpzcE6RhDY%2Fcwe6h8cgPZVnX%2FoB6BSbEgO0n3vx4RukZCKDVRLYZYQIQeBo%3D" rel="nofollow">原文地址</a></p></blockquote>
<h2>前言</h2>
<p>关于reactnaitve的导航,官方提供了2个组件,NavigatorIOS和Navigator,其中官方并不推荐使用NavigatorIOS,它不是官方维护的,不能保证及时的更新和维护。</p>
<p>所以本文中是以Navigator组件为基础,进行导航的设计和实现。</p>
<p>Navigator的劣势:Navigator组件是纯js的实现,所以在页面进行转场动画的过程中,如果js不能保证在16ms内完成其它操作的话,转场动画会有卡顿现象,后面会介绍优化的方案。</p>
<p>官方的Navigator组件使用方式较为灵活,本文的目的是选取一种最佳用法,并提取出通用功能应对常用场景,规范和设计项目中导航的使用。</p>
<h2>定义</h2>
<p>rn应用:全站rn应用,简称rn应用。</p>
<p>rn模块:部分模块使用rn,简称rn模块。</p>
<p>rn首页:无论是rn应用还是rn模块,进入rn页面的第一屏,简称rn首页。</p>
<p>nav:Navigator组件对象的简称,注意是实例化好的对象,不是类。页面间传递的导航对象统一使用此命名。</p>
<p>Header:自定义的导航栏组件。</p>
<h2>体系结构、设计原则</h2>
<p>一个rn应用或者一个rn模块,有且只有一个Navigator组件被定义。</p>
<p>在rn首页定义Navigator组件。</p>
<p>各个子页面统一使用首页定义的Navigator组件对象nav。</p>
<p>不要使用Navigator的navigationBar,请自定义导航栏组件,例如Header组件。</p>
<h2>Navigator组件的定义和初始化</h2>
<p>在rn首页中的render方法中,定义一个Navigator组件,并做好以下几件事:</p>
<ol>
<li><p>实现好通用的renderScene方法,</p></li>
<li><p>实现好android的物理返回按键</p></li>
<li><p>初始化真正的rn首页</p></li>
</ol>
<h3>实现统一路由函数renderScene</h3>
<p>renderScene函数是Navigator组件的必填函数,入参是route对象和当前的nav对象,返回值是jsx。</p>
<p>此函数的意思是根据传入的route,返回一个作为新页面的jsx,也就是说所有的路由算法都是在此函数中实现的。</p>
<p>其中route对象是一个自定义的对象,是nav.push方法中传入的对象。</p>
<p>此函数设计相当于门面模式,此函数是路由的统一处理器,所有的页面跳转请求都会通过此函数的计算来获得具体的jsx页面。</p>
<p>既然是统一的路由处理器,必然要求传入的route对象要满足统一的规则,否则无法实现统一的算法。</p>
<p>在此,设计route对象如下:</p>
<pre><code>{
name: 'page2', //名字用来做上下文判断和日志输出
page: <Page2 />, //jsx形式的page,作为新页面的jsx
// page: () => <Page2 />, //或者函数形式的page,此函数必须返回jsx,此jsx作为新页面的jsx
}</code></pre>
<p>根据route对象设计,设计统一的renderScene方法如下:</p>
<pre><code>_renderPage(route, nav) {
if (!route.page) {
console.error('页面导航请求没有传入page参数.');
return null;
}
let page;
if (typeof route.page === 'function') {
page = route.page();
} else {
page = route.page;
}
let name = route.name;
if (!name) {
if (page) {
name = page.type.name;
}
}
console.log(`in render page ${name}`);
return page;
}</code></pre>
<p>业务代码中,页面跳转的时候,只需要如下代码</p>
<pre><code>nav.push({
name: 'page2',
page: <Page2 nav={nav}/>,
});</code></pre>
<h3>android物理返回按键的处理</h3>
<p>如果你的应用需要支持android的话,那就要实现andorid的物理返回按键的对应处理。</p>
<p>一般按物理返回按键要么是返回上一页面,要么是返回页面的上一状态【例如,有打开的弹窗,按返回是关闭这个弹窗】。</p>
<p>返回上一页面因为有通用路由器的存在,所以可以通用处理,直接使用nav.pop()即可。</p>
<p>但是返回页面上一状态,并不容易统一处理,所以使用基于事件扩展的方式,交给业务代码自行实现。</p>
<p>在此重构route对象的规则,添加事件onHardwareBackPress,如下</p>
<pre><code>{
name: 'page2', //名字用来做上下文判断和日志输出
page: <Page2 />, //jsx形式的page,作为新页面的jsx
// page: () => <Page2 />, //或者函数形式的page,此函数必须返回jsx,此jsx作为新页面的jsx
onHardwareBackPress: () => alert('点物理按键会触发我'), // 返回false就终止统一路由器的默认动作,即终止页面返回动作,可以在此方法中实现返回页面上一状态的相关实现
}</code></pre>
<p>android物理返回按键的统一处理代码如下,</p>
<pre><code>componentWillMount() {
BackAndroid.addEventListener('hardwareBackPress', () => {
if (this.refs.nav) {
let routes = this.refs.nav.getCurrentRoutes();
let lastRoute = routes[routes.length - 1]; // 当前页面对应的route对象
if (lastRoute.onHardwareBackPress) {// 先执行route注册的事件
let flag = lastRoute.onHardwareBackPress();
if (flag === false) {// 返回值为false就终止后续操作
return true;
}
}
if (routes.length === 1) {// 在第一页了
// 此处可以根据情况实现 点2次就退出应用,或者弹出rn视图等
} else {
this.refs.nav.pop();
}
}
return true;
});
}
</code></pre>
<h3>初始化真正的rn首页</h3>
<p>此处较为简单,直接使用Navigator组件的initialRoute属性来指定初始化的route对象。</p>
<pre><code><Navigator initialRoute={{
page: <Home />, // Home为伪代码,自定义的首页组件
name: 'home',
}} /></code></pre>
<h2>页面跳转</h2>
<p>根据前面设计好的renderScene方法,直接使用如下代码,即可跳转到Page2,并将nav对象传递给了Page2.</p>
<pre><code>nav.push({
name: 'page2',
page: <Page2 nav={nav}/>,
});</code></pre>
<h2>页面返回</h2>
<p>页面返回直接使用</p>
<pre><code>nav.pop();</code></pre>
<h2>页面转场优化</h2>
<p>前面提到,Navigator组件完全使用js实现,由于js的单线程特点,如果在页面转场动画过程中,js干其他事情【比如渲染个某个jsx】超过了16ms,那么转场动画将不足60帧,给用户的感觉就是动画有卡顿。</p>
<p>为了避免这种情况,一种简单粗暴的办法就是在转场动画中不要让js来干别的事情。</p>
<p>那么我们如何知道转场动画什么时候结束呢,官方提供了动画交互管理器InteractionManager,示例伪代码如下:</p>
<pre><code>InteractionManager.runAfterInteractions(() => {
alert('哈哈 转场动画结束了!');
});</code></pre>
<p>大多数的场景:点击page1的某个按钮,要跳转到page2,并且page2要和服务器请求数据,根据返回的数据来渲染page2的部分or全部内容。</p>
<p>针对上述场景,解决方案如下,用伪代码描述:</p>
<ol>
<li><p>page2的state至少有2个值,转场动画进行中=true,服务器查询=true</p></li>
<li><p>page2的componentWillMount方法中发起异步服务器交互请求,当请求结束setState:服务器查询=false</p></li>
<li><p>page2的componentWillMount方法中注册InteractionManager.runAfterInteractions事件,当转场结束setState:转场动画进行中=false</p></li>
<li><p>page2的render方法中,先判断(转场动画进行中=true || 服务器查询=true)就返回一个loading的提示,否则返回真正的jsx,并且此时,服务器返回的数据已经可用了</p></li>
</ol>
<p>也可以参考官方文档: <a href="https://link.segmentfault.com/?enc=ifZyPF6nFoaENSGXlrD2zw%3D%3D.FSDm1OE3QAKoTP%2FRgJAj87YEfwmOukzyhlNNPKWXTzeDnPtHwqJnSSGniPe3EjuO4cJOS1wbZKsocGoMDHJ%2Bkg%3D%3D" rel="nofollow">http://reactnative.cn/docs/0.22/performa...</a></p>
<h2>刷新的实现</h2>
<p>目标:实现类似于html中window.reload的方法。</p>
<p>由于我们对route的规则限定,所以我们可以做到统一的刷新页面的逻辑。</p>
<p>思路是</p>
<ol>
<li><p>首先获得当前页面对应的route对象</p></li>
<li><p>然后获取route中的page属性,page属性可能是当前页面的jsx,也可能是可以产生当前页面jsx的方法</p></li>
<li><p>最后使用官方Navigator组件提供的replace方法,来用新的route替换掉原有的route</p></li>
</ol>
<p>示例参考代码如下:</p>
<pre><code>/**
* 刷新页面,route可以为空,会刷新当前页面
* @param nav
* @param route
*/
refresh(nav, route) {
if (!route) {
let routes = nav.getCurrentRoutes();
let length = routes.length;
route = routes[length - 1]; // 使用当前页对应的route
}
// todo 最好的方式是直接使用route.page,但是不好使,这种写法只支持一层节点,如果有多层会有问题
// todo 暂时未处理page是function的情况
let Tag = route.page.type;
nav.replace({
page: <Tag {...route.page.props} />,
});
}</code></pre>
<p>然后业务代码中这样调用,当前页面就被刷新了。</p>
<pre><code>Util.refresh(nav); //Util是伪代码,是你定义refresh方法的对应对象
</code></pre>
<h2>rn首页直接跳转子页面</h2>
<p>如果你开发的是rn模块【rn模块嵌入到已有app中,定义可以参考前面定义一节】,可能进入rn模块的入口会很多,比如,用rn开发一个论坛模块,正常入口进来是直接展现帖子列表,也可能会有点击某个其它按钮【此按钮是不是rn的】会直接跳转到某个帖子的详情页。</p>
<p>使用官方Navigator组件提供的initialRouteStack属性,可以完美的解决此问题,官方文档对此属性的说明如下:提供一个路由集合用来初始化。如果没有设置初始路由的话则必须设置该属性。如果没有提供该属性,它将被默认设置成一个只含有initialRoute的数组。</p>
<p>说白了就是,initialRouteStack要定义一个数组,里面是很多route对象,然后Navigator对象会展现到最后一个,而且数组中的其他route也都被初始化过了,你想返回到任何一个route都是可以的,是不是爽歪歪了。</p>
<p>给个示例代码吧,这是我项目中真正的代码,请当伪代码来阅读:</p>
<pre><code>getInitialRouteStack() {
let props = this.getProps();
let detailId = props.detailId;
if (detailId) { // 如果传入了详情id,那么跳转到详情页
return [{name: 'home', },
{
page: <AskDetail data={{id: detailId, }}/>,
backIsClose: true,
}];
}
let wantAsk = props.wantAsk;
if (wantAsk === true || wantAsk === 'true') { // 如果传入了提问属性=true,那么直接跳转到提问页面
return [{name: 'home', },
{
page: <WantAsk backIsClose={true}/>,
backIsClose: true,
}];
}
// 跳转到首页
return [{name: 'home', }];
}</code></pre>
<h2>实现代码参考</h2>
<p>根据以上设计思路,笔者封装了一个Navigator组件,是对官方的navigator组件进行了一层封装,供大家参考:</p>
<pre><code>import React from "react-native";
const {
Platform,
Animated,
View,
DeviceEventEmitter,
Dimensions,
Navigator,
BackAndroid,
} = React;
class Navigator2 extends React.Component {
componentWillMount() {
BackAndroid.addEventListener('hardwareBackPress', () => {
if (this.refs.nav) {
let routes = this.refs.nav.getCurrentRoutes();
let lastRoute = routes[routes.length - 1];
if (lastRoute.onHardwareBackPress) {// 先执行route注册的事件
let flag = lastRoute.onHardwareBackPress();
if (flag === false) {// 返回值为false就终止后续操作
return true;
}
}
if (routes.length === 1) {// 在第一页了
if (this.props.nav) {// 父页面仍有nav
this.props.nav.pop();
}
if (this.props.onHardwareBackPressInFirstPage) {
this.props.onHardwareBackPressInFirstPage();
}
} else {
if (lastRoute.backIsClose === true) {
if (this.props.onHardwareBackPressInFirstPage) {
this.props.onHardwareBackPressInFirstPage();
}
} else {
this.refs.nav.pop();
}
}
}
return true;
});
}
getLastRoute() {
if (this.refs.nav) {
let routes = this.getCurrentRoutes();
let lastRoute = routes[routes.length - 1];
return lastRoute;
}
return null;
}
render() {
return <Navigator renderScene={this._renderPage.bind(this)}
{...this.props}
ref='nav'
/>;
}
_renderPage(route, nav) {
if (!route.page) {
console.error('页面导航请求没有传入page参数.');
return null;
}
let page;
if (typeof route.page === 'function') {
page = route.page();
} else {
page = route.page;
}
let name = route.name;
if (!name) {
if (page) {
name = page.type.name;
}
}
console.log(`in render page ${name}`);
return page;
}
// todo 以下的方法为实现原版navigator的方法,这样做不好,但是没想到其它好办法
getCurrentRoutes() {
return this.refs.nav.getCurrentRoutes(...arguments);
}
jumpBack() {
return this.refs.nav.jumpBack(...arguments);
}
jumpForward() {
return this.refs.nav.jumpForward(...arguments);
}
jumpTo(route) {
return this.refs.nav.jumpTo(...arguments);
}
push(route) {
return this.refs.nav.push(...arguments);
}
pop() {
return this.refs.nav.pop(...arguments);
}
replace(route) {
return this.refs.nav.replace(...arguments);
}
replaceAtIndex(route, index) {
return this.refs.nav.replaceAtIndex(...arguments);
}
replacePrevious(route) {
return this.refs.nav.replacePrevious(...arguments);
}
immediatelyResetRouteStack(routeStack) {
return this.refs.nav.immediatelyResetRouteStack(...arguments);
}
popToRoute(route) {
return this.refs.nav.popToRoute(...arguments);
}
popToTop() {
return this.refs.nav.popToTop(...arguments);
}
}
module.exports = Navigator2;</code></pre>
<h2>参考地址</h2>
<p><a href="https://link.segmentfault.com/?enc=dP1PIWKL6qtXZHna0GFDpA%3D%3D.6pq7ZUSXkMIGXhVVYWwRvsuWFTh2u6Qp47LA2nKsqLoXqDPR%2BgLAE3xlFh3%2FPKgfB%2FR3uQfin48cuvhgnoA8Xw%3D%3D" rel="nofollow">http://reactnative.cn/docs/0.21/navigato...</a></p>
<p><a href="https://link.segmentfault.com/?enc=k0uIPs5mFycrOlKxABBXpQ%3D%3D.qHpX%2F9u0R1701BjnsbUkttikqEtF4Djd2dxJc7mOoKs%2BvYzgvTl3LM6T%2Bhs8diU%2FbrFWatjRD7aOUgCTeqHRQQ%3D%3D" rel="nofollow">http://reactnative.cn/docs/0.22/performa...</a></p>
ReactNative安卓首屏白屏优化
https://segmentfault.com/a/1190000004743424
2016-03-30T18:26:11+08:00
2016-03-30T18:26:11+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
0
<p>原文地址:<a href="https://link.segmentfault.com/?enc=tcEU8ZKgikFU6B3HWgqYwQ%3D%3D.kab%2BEvDsvo8Lsr95vHGfYSmXeXKnRiv8Hvy%2Fa931u97W2V34VWnCN%2F%2FPVF4ubghoeRppotjdfrG4knSH0JSdIKdyoj3b2kw92NNPiUerBqx8oLk5tSjBJKp0nAjnGltfoqDjAwGhceQpVfPKjX8m%2BEL3QmTagmRoc%2BQW343ltoNcYhRFqS%2BRR6Eg%2BGMAMF%2BKVkJh7jhIWUaFjsT%2Fw7iNvsgcr4bc%2BvqFfV6dXZGNXoW5KTLEZZDHSwRyUKRnmj2l" rel="nofollow">ReactNative安卓首屏白屏优化-github</a></p>
<h2>问题描述</h2>
<p>公司现有app中部分模块使用reactnative开发,在实施的过程中,rn良好的兼容性,极佳的加载、动画性能,提升了我们的开发、测试效率,提升了用户体验。</p>
<p>但是,在android中,当点击某个rn模块的入口按钮,弹出rn的activity到rn的页面展现出来的过程中,会有很明显的白屏现象,不同的机型不同(cpu好的白屏时间短),大概1s到2s的时间。</p>
<p>注意,只有在真机上才会有此现象,在模拟器上没有此现象完全是秒开。ios上也是秒开,测试的最低版本是ios7,iphone4s。</p>
<p>reactnative版本0.20.0。</p>
<p>jsbundle文件大小717kb。</p>
<h2>优化效果</h2>
<p>经过了大量的源码阅读,和网上资料查找,最终完美的解决了这个问题,无论什么机型,都可以达到秒开,如图(虽然下图是模拟器的截图,但是真机效果基本一样):</p>
<p><img src="/img/bVt38S" alt="clipboard.png" title="clipboard.png"></p>
<h2>优化过程</h2>
<h3>时间分布</h3>
<p>一般优化速度问题,首先就是要找到时间分布,然后根据二八原则,先优化耗时最长的部分。</p>
<p>android集成rn都会继承官方提供的ReactActivity</p>
<pre><code>public class MainActivity extends ReactActivity {</code></pre>
<p>然后只在自己的activity中覆盖一些配置项。</p>
<p>在官方的ReactActivity中的onCreate方法中</p>
<pre><code>protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(this)) {
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(serviceIntent);
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
}
}
mReactInstanceManager = createReactInstanceManager();
ReactRootView mReactRootView = createRootView();
mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions());
setContentView(mReactRootView);
}</code></pre>
<p>最慢的就是这两行代码,占了90%以上的时间。</p>
<pre><code>ReactRootView mReactRootView = createRootView();
mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions());</code></pre>
<p>这两行代码就是把jsbundle文件读入到内存中,并进行执行,然后初始化各个对象。</p>
<h3>优化思路---内存换时间</h3>
<p>在app启动时候,就将mReactRootView初始化出来,并缓存起来,在用的时候直接setContentView(mReactRootView),达到秒开。</p>
<h4>步骤1 缓存rootview管理器</h4>
<p>缓存rootview管理器主要用于初始化和缓存rootview对象。</p>
<pre><code>import android.app.Activity;
import android.os.Bundle;
import android.view.ViewParent;
import com.facebook.react.LifecycleState;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactRootView;
import java.lang.reflect.Field;
/**
* 缓存view管理
*/
public class RNCacheViewManager {
private static ReactRootView mRootView = null;
private static ReactInstanceManager mManager = null;
private static AbsRnInfo mRnInfo = null;
//初始化
public static void init(Activity act, AbsRnInfo rnInfo) {
init(act, rnInfo, null);
}
public static void init(Activity act, AbsRnInfo rnInfo, Bundle launchOptions) {
if (mManager == null) {
updateCache(act, rnInfo, launchOptions);
}
}
public static void updateCache(Activity act, AbsRnInfo rnInfo) {
updateCache(act, rnInfo, null);
}
//更新cache,适合于版本升级时候更新cache
public static void updateCache(Activity act, AbsRnInfo rnInfo, Bundle launchOptions) {
mRnInfo = rnInfo;
mManager = createReactInstanceManager(act);
mRootView = new ReactRootView(act);
mRootView.startReactApplication(mManager, rnInfo.getMainComponentName(), launchOptions);
}
//设置模块名称,因为是private,只能通过反射赋值
public static void setModuleName(String moduleName) {
try {
Field field = ReactRootView.class.getDeclaredField("mJSModuleName");
field.setAccessible(true);
field.set(getReactRootView(), moduleName);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
//设置启动参数,因为是private,只能通过反射赋值
public static void setLaunchOptions(Bundle launchOptions) {
try {
Field field = ReactRootView.class.getDeclaredField("mLaunchOptions");
field.setAccessible(true);
field.set(getReactRootView(), launchOptions);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
public static ReactRootView getReactRootView() {
if(mRootView==null){
throw new RuntimeException("缓存view管理器尚未初始化!");
}
return mRootView;
}
public static ReactInstanceManager getReactInstanceManager() {
if(mManager==null){
throw new RuntimeException("缓存view管理器尚未初始化!");
}
return mManager;
}
public static AbsRnInfo getRnInfo() {
if(mRnInfo==null){
throw new RuntimeException("缓存view管理器尚未初始化!");
}
return mRnInfo;
}
public static void onDestroy() {
try {
ViewParent parent = getReactRootView().getParent();
if (parent != null)
((android.view.ViewGroup) parent).removeView(getReactRootView());
} catch (Throwable e) {
e.printStackTrace();
}
}
public static void clear() {
try {
if (mManager != null) {
mManager.onDestroy();
mManager = null;
}
if (mRootView != null) {
onDestroy();
mRootView = null;
}
mRnInfo = null;
} catch (Throwable e) {
e.printStackTrace();
}
}
private static ReactInstanceManager createReactInstanceManager(Activity act) {
ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
.setApplication(act.getApplication())
.setJSMainModuleName(getRnInfo().getJSMainModuleName())
.setUseDeveloperSupport(getRnInfo().getUseDeveloperSupport())
.setInitialLifecycleState(LifecycleState.BEFORE_RESUME);
for (ReactPackage reactPackage : getRnInfo().getPackages()) {
builder.addPackage(reactPackage);
}
String jsBundleFile = getRnInfo().getJSBundleFile();
if (jsBundleFile != null) {
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName(getRnInfo().getBundleAssetName());
}
return builder.build();
}
}
</code></pre>
<h4>步骤2 重写ReactActivity</h4>
<p>将官方的ReactActivity粘出来,重写2个方法,onCreate和onDestroy,其余代码不动。</p>
<p>onCreate方法中使用缓存rootview管理器来获得rootview对象,而不是重新创建。</p>
<p>这里曾尝试继承ReactActivity,而不是重写这个类,但是子类覆盖onCreate方法时候,必须要调用super.onCreate,否则编译会报错,但是super.onCreate方法会重新创建rootview,所以实在是绕不过去了。</p>
<pre><code>protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (RNCacheViewManager.getRnInfo().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(this)) {
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(serviceIntent);
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
}
}
mReactInstanceManager = RNCacheViewManager.getReactInstanceManager();
ReactRootView mReactRootView = RNCacheViewManager.getReactRootView();
setContentView(mReactRootView);
}</code></pre>
<p>onDestroy方法中,不能再调用原有的mReactInstanceManager.destroy()方法了,否则rn初始化出来的对象会被销毁,下次就用不了了。同时,要卸载掉rootview的parent对象,否则下次再setContentView时候回报错。</p>
<pre><code>protected void onDestroy() {
RNCacheViewManager.onDestroy();
super.onDestroy();
}</code></pre>
<p>RNCacheViewManager.onDestroy的方法:</p>
<pre><code>public static void onDestroy() {
try {
ViewParent parent = getReactRootView().getParent();
if (parent != null)
((android.view.ViewGroup) parent).removeView(getReactRootView());
} catch (Throwable e) {
e.printStackTrace();
}
}</code></pre>
<h4>步骤3 在app启动时候初始化缓存rootview管理器</h4>
<pre><code>RNCacheViewManager.init((Activity) context, new RnInfo(moduleName, launchOptions));</code></pre>
<p>其中RnInfo如下:</p>
<pre><code>public class RnInfo extends AbsRnInfo {
private String mModuleName;
private Bundle mLaunchOptions;
public RnInfo(String moduleName) {
this.mModuleName = moduleName;
}
public RnInfo(String moduleName, Bundle launchOptions) {
this.mModuleName = moduleName;
this.mLaunchOptions = launchOptions;
}
@Nullable
@Override
public Bundle getLaunchOptions() {
return mLaunchOptions;
}
@Override
public String getMainComponentName() {
return mModuleName;
}
@Override
public String getJSMainModuleName() {
return RNKeys.Default.DEf_JS_MAIN_MODULE_NAME;
}
@Nullable
@Override
public String getJSBundleFile() {
return RNManager.getJsBundlePath();
}
@Override
public boolean getUseDeveloperSupport() {
return true;
}
@Override
public List<ReactPackage> getPackages() {
return Arrays.asList(
new MainReactPackage(),
new BBReactPackage()
);
}
}</code></pre>
<h2>结语</h2>
<p>希望本篇文档能帮助遇到类似问题的小伙伴们。</p>
<p>reactnative虽然不是银弹,但是在目前移动端浏览器兼容性弱爆了的情况下,还是能极大的提升开发测试效率的,性能也是极佳的,看好rn的未来。</p>
<h2>参考文章</h2>
<p><a href="https://link.segmentfault.com/?enc=EbO23muTVLUyMkGeDQpy1A%3D%3D.ffxPKQVTM0jEtM7BkI7%2Bs7g3ZdMrz1uM8YIlZ5ZUb6L6RqIKn1QPF49qp8cUG7a%2B" rel="nofollow">http://zhuanlan.zhihu.com/magilu/2058748...</a></p>
<p><a href="https://link.segmentfault.com/?enc=V3Q1%2FphjYkYCvZLxgUayvQ%3D%3D.xsh7LAwyGEOsh46SlSZ0uAzP2Rw3IiqXITnLeJCkv4IFLxWhVjfWiyh2XPKgHF6l" rel="nofollow">http://zhuanlan.zhihu.com/magilu/2025970...</a></p>
<p><a href="https://link.segmentfault.com/?enc=HvOm0Fyi9rCGuUaLQ8ttIA%3D%3D.HlWGNBmJLu9Jk8g6ir0Vw7taHp4EOQa3krjglu4djGN%2B40iy9Odj%2FcZj6FOPrGJrrvJx2%2BeB2FbcdicD9dlHQXubuAu2OXzC%2FglWOoJW5XL7aWlQq6EaAMLYItjeDDwFYf3Iygr4W1l%2BRuzDe7QM8PPvFBYKUk8WGyqRXYW7%2BD8%3D" rel="nofollow">https://yq.aliyun.com/articles/3208?spm=...</a></p>
ReactNative增量升级方案
https://segmentfault.com/a/1190000004352162
2016-01-22T17:21:42+08:00
2016-01-22T17:21:42+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
12
<p><a href="https://link.segmentfault.com/?enc=eLvSM4Ajeb2bMa7%2BeTO9Eg%3D%3D.vKdhAGvwnuzFnhQY%2BbBHIcKbbvqB1onK%2BnchgtkZN3HQtzD8YgINz9ehHLr1KAQv%2FCMstkFnTSE9O5pwv1WXMDZYwt8Gl4zxLEKP8KMQ8hJyjWHZvv9kdVUiTXEE8KQ1jibDITVz4HKnSH%2BFrx3%2BG%2B%2Bb7MnHpPN%2BenE7fMjoU5KaTbX%2FdcbugO84EN0k%2B9bNBzKGqdcxlI5SqQ%2FO4leR4PZgt3M3jOsIuCmqHjNku5E%3D" rel="nofollow">原文地址</a></p>
<h2>前言</h2>
<p>facebook的react-native给我们带来了用js写出原生应用的同时,也使得使用RN编写的代码的在线升级变得可能,终于可以不通过应用市场来进行升级,极大的提升了app修bug和赋予新功能的能力。----使用h5的方式也可以做到,但是rn的用户体验可要远远超过h5啊。</p>
<p>一般使用RN编写的app的线上使用方式,是将react-native bundle命令打出bundle文件和assets文件夹,直接内置到app中,app在viewcontroller或者activity中直接加载app内部的bundle文件,比如下图。</p>
<p><img src="/img/bVsql1" alt="clipboard.png" title="clipboard.png"></p>
<p>当修改了代码或者图片的时候,只要app使用新的bundle文件和assets文件夹,就完成了一次在线升级。</p>
<p>本文主要基于以上思路,讲解增量升级的解决方案。</p>
<h2>何为增量?</h2>
<p>一个完整的RN-app程序通常包含以下几个部分:</p>
<ol>
<li><p>native代码部分-objc或者java</p></li>
<li><p>js代码部分-rn代码、依赖的第三方库、业务代码等</p></li>
<li><p>图片资源部分</p></li>
</ol>
<p>native代码别想了,没法在线升级,要是能大家就都不使用应用市场ota升级了。</p>
<p>能进行在线升级的是js代码部分和图片资源部分,具体到代码就是bundle文件和assets文件夹。</p>
<p>因为在线升级是要走网络的,我们要想办法将网络消耗降到最低,所以要使用增量升级的方式。</p>
<p>针对js代码部分(即bundle文件)的增量指的是,代码的改动有多少,增量patch的补丁就有多少,那些没有改动的代码部分是不在补丁的范围内的。</p>
<p>针对图片部分(即assets)的增量指的是,升级补丁包中只包含新增的图片和有改动的图片。</p>
<p>那么在app端,下载升级补丁包,只需要和现有的版本进行合并,就能计算出最新版本的全量包。</p>
<p>总结下流程:()中为例子</p>
<p>首先,计算增量包:新版本(v10) - 旧版本(v1到v9) = 增量包 (会有9个包,v1~v10.zip,v2~v10.zip,,,,,v9-v10.zip)</p>
<p>然后,app根据自己的当前版本(比如V6),下载对应的增量包(V6-V10.zip)。</p>
<p>最后,app中通过 旧版本(v6) + 增量包(v6~v10.zip) = 新版本(v10) ,计算出了新版本的全量包。</p>
<h2>增量算法</h2>
<p>assets增量算法,比较简单,就是比对,可以很容易的比较出新增的文件,和不同的文件(使用md5)。</p>
<p>bundle文件的增量算法,确实比较复杂,刚开始没有什么头绪,后来在团队boss的指引下,很幸运的找到了google写的一个开源的库,可以对大字符串进行diff和patch,并且支持java、objc、js等等语言,完全的满足了我们的需求。</p>
<p>只用到2个接口,具体请参考github上的文档</p>
<ol>
<li><p>生成增量包时候:patch_make(text1, text2) => patches</p></li>
<li><p>app生成全量包时候:patch_apply(patches, text1) => [text2, results]</p></li>
</ol>
<p>google开源库地址:<a href="https://link.segmentfault.com/?enc=3m6uhU0We9gOo%2FYxEh5jig%3D%3D.HjkOxTqSwcR5z8hr2pMQ%2BSyjaP66KmUjfuCid%2Bw9Xg2%2B2S4Fs1o4H2k7dl5eDX8uhxQWcmS7TERhq40FxHYlPA%3D%3D" rel="nofollow">https://github.com/bystep15/google-diff-match-patch</a></p>
<h2>codepush</h2>
<p>微软的codepush也做了类似的事情,不过由于以下原因,我们团队没敢使用其作为解决方案。</p>
<ol>
<li><p>其增量升级仅仅是针对图片资源的</p></li>
<li><p>其升级服务器端程序并不开源</p></li>
<li><p>其升级服务器在美国,国内访问很慢且不稳定</p></li>
</ol>
<p>不过,codepush客户端的源码和文档也给我们提供了很多思路,在此感谢codepush团队。</p>
<p>codepush地址:<a href="https://link.segmentfault.com/?enc=b13e0AC8cEUd2Co7B8kqDg%3D%3D.y8WjtiGeo7Z0GSOOfziIk9YBfCulFn1tekh9kFsc8coenM8lJzn%2FbgPac302n3Z3" rel="nofollow">http://microsoft.github.io/code-push/</a></p>
<h2>bundle要求的app最小版本</h2>
<p>本文中一般用min-v或者appMinV表示。</p>
<p>因为js代码是依赖于native代码的,所以,jsbundle对app的版本有要求,所以有这个概念。</p>
<p>试想,如果bundle依赖了一个native的一个新的接口,这个接口在v3版本的app中才发布,如果v2版本的app升级了这个bundle,那么必然会报错,严重的可能会导致app的崩溃。</p>
<h2>系统结构设计与各模块职责</h2>
<p><img src="/img/bVsql7" alt="clipboard.png" title="clipboard.png"><br></p>
<h2>bundle仓库设计</h2>
<p>存储全量bundle和生成增量patch</p>
<h3>node patch 命令</h3>
<p>在bundle目录下放入一个符合要求【参考目录结构说明】的新版本目录,比如0.3.0,然后执行以下命令。</p>
<p>命令:node patch 版本号 , 示例:node patch 0.3.0</p>
<p>-d参数: node patch 版本号 -d ,如果加入-d参数,会先删除patch目录下的对应版本目录,然后进行patch生成</p>
<p>然后在patch目录中就会生成0.3.0的增量包,同时patch目录下的update.json文件也会重新生成.</p>
<h3>node update.json 命令</h3>
<p>在patch目录下重新生成update.json文件</p>
<h3>目录结构说明</h3>
<p><img src="/img/bVsqma" alt="clipboard.png" title="clipboard.png"></p>
<pre><code>bundle 存放全量bundle和全量assets的目录,里面的文件基本上是使用react-native bundle命令生成的
0.1.0
略
0.2.0
android
略,同ios
ios
config.json 此版本的配置信息,包含要求app的最低版本等,手动配置
index.jsbundle 全量jsbundle文件,使用react-native bundle命令生成
assets 全量图片目录,使用react-native bundle命令生成
0.3.0
略
patch 存放增量补丁的目录,里面文件都是命令生成的,无需手动维护
0.1.0
第一版本无文件
0.2.0
android
略,同ios
ios
0.1.0-0.2.0.zip 增量包.zip
0.3.0
android
略,同ios
ios
0.1.0-0.3.0.zip 增量包.zip
0.2.0-0.3.0.zip 增量包.zip
update.json 所有的升级包信息
src 存放打包用的源码
lib 存放打包用依赖的第三方的源码
patch.js patch命令入口
update.json.js update.json命令入口</code></pre>
<h3>config.json示例</h3>
<pre><code>{
"v": "0.3.0", //版本
"min-v": "4.0.0", //此版本要求的最小app版本
"date": "2016-01-01", //打包日期
"des": [
"修复xxbug", "添加xx功能"
]
}
</code></pre>
<h3>update.json示例</h3>
<pre><code>[
{
"v": "0.1.0",
"min-v": "4.0.0",
"date": "2016-01-01",
"des": [
"修复xxbug",
"添加xx功能"
],
"iosBundleMd5": "11f82563f8fd3f22dccb80ad2297f7bc",
"androidBundleMd5": "11f82563f8fd3f22dccb80ad2297f7bc"
},
{
"v": "0.2.0",
"min-v": "4.0.0",
"date": "2016-01-01",
"des": [
"修复xxbug",
"添加xx功能"
],
"iosBundleMd5": "3ca2824b008132cee515c0ea29938ff2",
"androidBundleMd5": "3ca2824b008132cee515c0ea29938ff2"
},
{
"v": "0.3.0",
"min-v": "4.0.0",
"date": "2016-01-01",
"des": [
"修复xxbug",
"添加xx功能"
],
"iosBundleMd5": "dbb81d2383112abb50eb19970c486acd",
"androidBundleMd5": "dbb81d2383112abb50eb19970c486acd"
}
]
</code></pre>
<h2>升级服务器设计</h2>
<h3>接口patch/query</h3>
<p>例如:<a href="https://link.segmentfault.com/?enc=NDZUtZ2dMTu22dgJkDuS9g%3D%3D.4xRrluteuKDd05nch9cjLUHGQlCSv8duoMr9vmNc7h4%3D" rel="nofollow">http://localhost</a>:3000/patch/query?bundleV=0.2.0&appV=4.0.0&platform=ios</p>
<p>此接口会有以下4个场景的使用情况,每个场景返回的json示例已经提供在后面文档中。</p>
<h4>输入参数</h4>
<pre><code>bundleV : app中的bundle版本
appV : app版本
platform : app的平台
</code></pre>
<h4>返回json各项说明</h4>
<ol>
<li><p>status : 本次请求后台是否发生了错误</p></li>
<li><p>msg : 给用户看的中文提示信息,静默升级时候没什么用</p></li>
<li><p>latestBundleV : 当前的最新bundle版本</p></li>
<li><p>latestAppMinV : 最新bundle要求的app最低版本</p></li>
<li><p>canUpdate : 能否升级,boolean</p></li>
<li><p>canUpdateBundleV : 能升级的bundle版本</p></li>
<li><p>canUpdateAppMinV : 能升级的bundle要求的app最低版本</p></li>
<li><p>patchUrl : 补丁包相对地址</p></li>
<li><p>platform : 平台:ios or android</p></li>
</ol>
<h4>场景1</h4>
<p>能升级,且能升级到最新版</p>
<pre><code>{
status: 'success',
msg: '可以升级,bundle最新版为0.3.0',
latestBundleV: '0.3.0',
latestAppMinV: '4.0.0',
canUpdate: true,
canUpdateBundleV: '0.3.0',
canUpdateAppMinV: '4.0.0',
patchUrl: 'patch/0.3.0/ios/0.2.0-0.3.0.zip',
platform: 'ios'
}</code></pre>
<h4>场景2</h4>
<p>无需升级,已经是最新版本</p>
<pre><code>{
status: 'success',
msg: '无需升级,已经是最新版本',
canUpdate: false,
platform: 'ios'
}</code></pre>
<h4>场景3</h4>
<p>能升级,能升级到当前appMinV的最新bundle版本,但不是最新的bundle,想升最新bundle,必须先升app</p>
<pre><code>{
status: 'success',
msg: '可以升级,但app版本3.0.0太低,只能升到bundleV0.2.0,bundleV最新为0.3.0',
latestBundleV: '0.3.0',
latestAppMinV: '4.0.0',
canUpdate: true,
canUpdateBundleV: '0.2.0',
canUpdateAppMinV: '3.0.0',
patchUrl: 'patch/0.2.0/ios/0.1.0-0.2.0.zip',
platform: 'ios'
}</code></pre>
<h4>场景4</h4>
<p>不能升级,已经是当前appMinV的最新bundle,但不是最新的bundle,想升最新bundle,必须先升app</p>
<pre><code>{
status: 'success',
msg: '不能升级,当前已经是app3.0.0的最新bundle了,但不是最新bundle0.3.0,想升级bundle到最新,请先升级app',
latestBundleV: '0.3.0',
latestAppMinV: '4.0.0',
canUpdate: false,
platform: 'ios'
}</code></pre>
<h2>native客户端设计</h2>
<p>客户端的主要工作就是发请求到升级服务器询问是否能升级,然后根据返回的信息,下载升级包,解压升级包,安装升级包。</p>
<p>过程中要保证下载的文件是正确的(md5校验),要保证补丁安装之后的全量bundle文件是正确的(md5校验)。</p>
<p>整个过程用户无感知。</p>
<p>此部分详细设计后边会补充。</p>
<h2>遗留问题</h2>
<p>目前rn将安卓的图片放入到res目录中,导致安卓图片不能使用在线升级的解决方案,但是codepush的作者已经重构了此部分内容,并提pr到rn,rn团队也接受了这个pr,会在近期的版本中发布。</p>
<p>参考地址:<a href="https://link.segmentfault.com/?enc=l0MWdubinlxzrISbgAE2ew%3D%3D.usSTv8qeT2uxLqu3%2Fs5h6hkaDQRFc4rrAqfYHc4rFEQe0%2Fd6XteM8X791HH1MTeiTmpnKP6FcLMynF8O9qUNwg%3D%3D" rel="nofollow">https://github.com/facebook/react-native/pull/4527</a></p>
<h2>近期开源</h2>
<p>本部分内容核心的几个部分都已经完成,近期完善之后,会开源出来。</p>
ReactNative集成到已有工程中-IOS
https://segmentfault.com/a/1190000004253916
2016-01-05T18:58:50+08:00
2016-01-05T18:58:50+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
0
<p>以下步骤为手动添加的方式,使用rnpm的方式请参考官方文档。</p>
<p>本文是对官方文档的一个补充,大部分内容来源于官网文档。</p>
<p>官方参考地址: <a href="https://link.segmentfault.com/?enc=k9xM7Mq7TNc8rnTaSOICWA%3D%3D.obqkDBtk6DY9qH4mvwtKCYD6T4vey8puCC%2BcL7mHq%2BXUGcetDnlumQjltpl90mgzcXNIvY2AvShQXkgjkiOSBa7keqGVJKQHOVyq%2BaNDt98%3D" rel="nofollow">http://facebook.github.io/react-native/docs/linking-libraries-ios.html#content</a></p>
<p>中文参考地址:<a href="https://link.segmentfault.com/?enc=t8rQMbex9OwDXwTuqECKMw%3D%3D.TYfJyC5ePHIxmJuEQMijsh1siI6Ypvekgfnxhebz%2FIn5caqISmgyW4w45cz8A7NRs5F8L32lv7K4Sc6mqi7HZQ%3D%3D" rel="nofollow">http://reactnative.cn/docs/linking-libraries-ios.html#content</a></p>
<h2>第一步:添加.xcodeproj文件</h2>
<p>把需要的.xcodeproj文件,拖到你的XCode工程下(通常拖到XCode的Libraries分组里)</p>
<p><img src="/img/bVr0NB" alt="clipboard.png" title="clipboard.png"></p>
<p>react-native需要集成的.xcodeproj文件清单如下:</p>
<pre><code>node_modules/react-native/React/React.xcodeproj
node_modules/react-native/Libraries/Image/RCTImage.xcodeproj
node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj
node_modules/react-native/Libraries/Text/RCTText.xcodeproj
node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj
node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj
node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj
node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj
</code></pre>
<h2>第二步添加.a文件</h2>
<p>点击你的主工程文件,选择Build Phases,然后把刚才所添加进去的.xcodeproj下的Products文件夹中的静态库文件(.a文件),拖到Link Binary With Libraries组内。</p>
<p><img src="/img/bVr0NE" alt="clipboard.png" title="clipboard.png"></p>
<h2>第三步注册头文件路径-不是所有依赖都需要,注册React.xcodeproj即可</h2>
<p>需要在原生代码中使用这个库,还是只需要通过JavaScript访问?<br>如果你只需要通过JavaScript访问这个库,你就可以跳过这步了。</p>
<p>打开你的工程文件,选择Build Settings,然后搜索Header Search Paths,然后添加库所在的目录(如果它还有像React这样的子目录需要包含,注意要选中recursive选项)</p>
<p><img src="/img/bVr0NG" alt="clipboard.png" title="clipboard.png"></p>
<p>注意:React.xcodeproj是需要注册的。下面的目录要改成自己的目录。</p>
<p>$(SRCROOT)/RN/node_modules/react-native/React </p>
<p>recursive</p>
<h2>报错: App Transport Security has blocked a cleartext HTTP (<a href="https://link.segmentfault.com/?enc=wYPcj2WHnIUbR9BkgaEpKQ%3D%3D.yNx85987KCoHrjOSTbJc5w%3D%3D" rel="nofollow">http://)</a> resource load since it is insecure</h2>
<p>报错原因是需要开启内网http的访问权限。</p>
<ol>
<li><p>在Info.plist中添加 NSAppTransportSecurity 类型 Dictionary ;</p></li>
<li><p>在 NSAppTransportSecurity 下添加 NSAllowsArbitraryLoads 类型Boolean ,值设为 YES;</p></li>
</ol>
<p>参考地址:</p>
<p><a href="https://link.segmentfault.com/?enc=R8VjQj%2Fvg56IwAG7E%2FpzJg%3D%3D.eicu2dLo8kzBTHCCR2WFI9%2FGBeqz5a61Bj683pjKLa8ogDZ4Q93GuoIhjx2lUsmF" rel="nofollow">http://www.cnblogs.com/chglog/p/4746683.html</a></p>
<h2>报错:unrecognized selector sent to instance</h2>
<p>解决方案:</p>
<p>Build Settings -> other linker flags -> -ObjC</p>
<p>参考地址1:<a href="https://link.segmentfault.com/?enc=0o%2BnyvnmmlEjrs%2FUTSboJg%3D%3D.qif%2FBtOGA640JD%2B7bsQx2SvBTlTSBSE0vd6Uon%2BduFEI8xCQssLi1SfiJFxoN2Q36HVckZ1RA2oOWaHUN58bIA%3D%3D" rel="nofollow">https://github.com/facebook/react-native/issues/2396</a></p>
<p>参考地址2:<a href="https://link.segmentfault.com/?enc=fsANuEamghs0crdzDu8AZw%3D%3D.vUXEofzF3zvOpMmI1dXlApFy9N1HuoA9nnvtwznfNdUjLRyJJwVmmFASWgwRlzdQMMMlsSILYr%2B8m%2BkB4vQJ%2FZLp7E12AqAbPr%2FDxy7IuSUgqQqbnz%2FJUZGBsQK2JGqfgo3VMQE4sbU6Ra64Dc8eEQ%3D%3D" rel="nofollow">http://stackoverflow.com/questions/32775481/rctbatchedbridge-perfstats-unrecognized-selector-sent-to-instance</a></p>
<h2>报错:RCTStatusBarManager module requires that the UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO</h2>
<p>错误信息说的很清楚了。</p>
<p>在Info.plist中添加View controller-based status bar appearance为NO。</p>
<h2>关闭自动启动调试服务器</h2>
<p>在React.xcodeproj的Build Phases中的第一个(有8081那个)Run Script。用 <<! 代码代码代码 ! 注释掉即可。</p>
<p><img src="/img/bVr0NH" alt="clipboard.png" title="clipboard.png"></p>
ReactNative打离线包-android篇
https://segmentfault.com/a/1190000004192816
2015-12-24T11:13:17+08:00
2015-12-24T11:13:17+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
0
<p>官方文档:<a href="https://link.segmentfault.com/?enc=amYS%2FWc%2BUBnfvTbx2qzHTA%3D%3D.4YxS%2FeuczYvoxWetotH6QCis%2BagNOdUUYWMdE%2BsKM87ZsI2nxPYrMLlKhm%2BX7LwaVBq3wJLsP%2F2uiDmuvLXT3YlI2D0TNOWWelx2a3E%2BQYuwI9gxNxClCGNTYbGenHm6" rel="nofollow">http://facebook.github.io/react-native/docs/running-on-device-android.html#content</a></p>
<p>官方文档2:<a href="https://link.segmentfault.com/?enc=4G7qM2MUxFy8My1zT31kGA%3D%3D.L1iGISyKTCeZY8vcX1JbbzuciFpS9dqdgViTyQ8%2BuNxRqPEo%2FlHkyUy%2BSUQ0Jlq4xGCI9xce7fO1IEJBovMdQH2r1CW6%2B1C00Xy4WhHWk9s%3D" rel="nofollow">http://facebook.github.io/react-native/docs/signed-apk-android.html#content</a></p>
<p>离线包就是把RN和你写的js图片等资源都打包放入app,不需要走网络下载。</p>
<h2>打包命令说明</h2>
<p>react-native bundle</p>
<p>Options:</p>
<p>--entry-file Path to the root JS file, either absolute or relative to JS root [required]</p>
<p>--platform Either "ios" or "android"</p>
<p>--transformer Specify a custom transformer to be used (absolute path) [default: "/Users/babytree-mbp13/projects/xcodeProjects/AwesomeProject/node_modules/react-native/packager/transformer.js"]</p>
<p>--dev If false, warnings are disabled and the bundle is minified [default: true]</p>
<p>--prepack If true, the output bundle will use the Prepack format. [default: false]</p>
<p>--bridge-config File name of a a JSON export of __fbBatchedBridgeConfig. Used by Prepack. Ex. ./bridgeconfig.json</p>
<p>--bundle-output File name where to store the resulting bundle, ex. /tmp/groups.bundle [required]</p>
<p>--bundle-encoding Encoding the bundle should be written in (<a href="https://link.segmentfault.com/?enc=0OH%2Fks9SAT2LxaulKy79OA%3D%3D.aF8kX%2BnNNkSaIRlPqBTSRW0%2BWmsQkak5ilGmsBGRoDs8yqN0i%2FXndCrc2mhYmnShPF1y74ilxQ5CANSZcvm5Og%3D%3D" rel="nofollow">https://nodejs.org/api/buffer.html#buffer_buffer).</a> [default: "utf8"]</p>
<p>--sourcemap-output File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map</p>
<p>--assets-dest Directory name where to store assets referenced in the bundle</p>
<p>--verbose Enables logging [default: false]</p>
<h2>安卓打包步骤</h2>
<ol>
<li><p>在工程根目录下执行打包命令,比如<code> react-native bundle --entry-file demo/index.js --bundle-output ./android/app/src/main/assets/index.android.jsbundle --platform android --assets-dest ./android/app/src/main/res/ --dev false </code>请参考上面命令说明,根据自己的情况进行修改再执行。注意要先保证[./android/app/src/main/assets/]文件夹存在。</p></li>
<li><p>命令执行完生成资源如图<br><img src="/img/bVrKUb" alt="clipboard.png" title="clipboard.png"></p></li>
<li><p>保证MainActivity.java中的setBundleAssetName与你的jsbundle文件名一致,比如<code>.setBundleAssetName("index.android.jsbundle")</code>就与我生成的资源名一致</p></li>
<li><p>一切OK 打包测试吧</p></li>
</ol>
<h2>To disable the developer menu for production builds:</h2>
<p>For iOS open your project in Xcode and select Product → Scheme → Edit Scheme... (or press ⌘ + <). Next, select Run from the menu on the left and change the Build Configuration to Release.</p>
<p>For Android, by default, developer menu will be disabled in release builds done by gradle (e.g with gradle assembleRelease task). Although this behavior can be customized by passing proper value to ReactInstanceManager#setUseDeveloperSupport.</p>
ReactNative打离线包-ios篇
https://segmentfault.com/a/1190000004189538
2015-12-23T18:07:23+08:00
2015-12-23T18:07:23+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
4
<p>官方文档,内容很旧:<a href="https://link.segmentfault.com/?enc=ymGPHA%2FkUA%2BYVsED80%2FgBw%3D%3D.64jxnGYMhPAXxFgnj24pbWQCxcfJXKXCrGnrjFikWTBzZe5kjlB2l6C5o3HT2KMAzraGwtuVC9m2bN308PI0Aino9nUhEj7AkmWsHVUFIFY%3D" rel="nofollow">http://facebook.github.io/react-native/docs/running-on-device-ios.html#content</a></p>
<p>相关链接:<a href="https://link.segmentfault.com/?enc=aTTwm0YOt8BBISgIyGQa4w%3D%3D.%2BATGCGsdBb%2FB69YuXBNFKlhvp44pD5YrpMd4kwLP%2FQthl%2BX4qlJUZ0W45seRHJfgOdF5a0jJRBGtrRU4QcrWNQ%3D%3D" rel="nofollow">https://github.com/facebook/react-native/issues/4084</a></p>
<p>离线包就是把RN和你写的js图片等资源都打包放入app,不需要走网络下载。</p>
<h2>打包命令说明</h2>
<p>react-native bundle</p>
<p>Options:</p>
<p>--entry-file Path to the root JS file, either absolute or relative to JS root [required]</p>
<p>--platform Either "ios" or "android"</p>
<p>--transformer Specify a custom transformer to be used (absolute path) [default: "/Users/babytree-mbp13/projects/xcodeProjects/AwesomeProject/node_modules/react-native/packager/transformer.js"]</p>
<p>--dev If false, warnings are disabled and the bundle is minified [default: true]</p>
<p>--prepack If true, the output bundle will use the Prepack format. [default: false]</p>
<p>--bridge-config File name of a a JSON export of __fbBatchedBridgeConfig. Used by Prepack. Ex. ./bridgeconfig.json</p>
<p>--bundle-output File name where to store the resulting bundle, ex. /tmp/groups.bundle [required]</p>
<p>--bundle-encoding Encoding the bundle should be written in (<a href="https://link.segmentfault.com/?enc=0PlCNpJD2e8j3NL3NWLtPw%3D%3D.ZsO4HummPye2dSefFe2Kd18BIaMS0xluS8Ww07DZN9Tzw8Yb5iWAtW8BeYlGF1j%2FAEJlSuGO5E%2BVWixXPW78Sw%3D%3D" rel="nofollow">https://nodejs.org/api/buffer.html#buffer_buffer).</a> [default: "utf8"]</p>
<p>--sourcemap-output File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map</p>
<p>--assets-dest Directory name where to store assets referenced in the bundle</p>
<p>--verbose Enables logging [default: false]</p>
<h2>ios打包步骤</h2>
<ol>
<li><p>在工程根目录下执行打包命令,比如react-native bundle --entry-file demo/index.js --bundle-output ./ios/bundle/index.ios.jsbundle --platform ios --assets-dest ./ios/bundle --dev false,请参考上面命令说明,根据自己的情况进行修改再执行。注意要先保证bundle文件夹存在。</p></li>
<li><p>命令执行完生成如下资源 <br><img src="/img/bVrJ3i" alt="clipboard.png" title="clipboard.png"></p></li>
</ol>
<ol>
<li><p>在xcode中添加assets【必须用Create folder references的方式,添加完是蓝色文件夹图标】和index.ios.jsbundle,如图<br><img src="/img/bVrJ3j" alt="clipboard.png" title="clipboard.png"></p></li>
<li>
<p>参考官方文档,修改AppDelegate.m文件,使用OPTION 2处的代码</p>
<pre><code>jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"index.ios" withExtension:@"jsbundle"];</code></pre>
</li>
<li><p>一切OK 运行模拟器看效果吧</p></li>
</ol>
<h2>ios打包遇到的问题</h2>
<ol>
<li><p>离线包如果开启了chrome调试,会访问调试服务器,而且会一直loading出不来。</p></li>
<li><p>如果bundle的名字是main.jsbundle,app会一直读取旧的,改名就好了。。。非常奇葩的问题,我重新删了app,clean工程都没用,就是不能用main.jsbundle这个名字。</p></li>
<li><p>必须用Create folder references【蓝色文件夹图标】的方式引入图片的assets,否则引用不到图片</p></li>
<li><p>执行bundle命令之前,要保证相关的文件夹都存在</p></li>
</ol>
<h2>To disable the developer menu for production builds:</h2>
<p>For iOS open your project in Xcode and select Product → Scheme → Edit Scheme... (or press ⌘ + <). Next, select Run from the menu on the left and change the Build Configuration to Release.</p>
<p>For Android, by default, developer menu will be disabled in release builds done by gradle (e.g with gradle assembleRelease task). Although this behavior can be customized by passing proper value to ReactInstanceManager#setUseDeveloperSupport.</p>
ReactNative变革无线前端-淘宝d2分享
https://segmentfault.com/a/1190000004186918
2015-12-23T11:33:35+08:00
2015-12-23T11:33:35+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
0
<p><img src="/img/bVrJkS" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVrJkT" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVrJkW" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVrJkX" alt="clipboard.png" title="clipboard.png"></p>
<p>这不能批量传图,我就贴了前几张,完整版请看这里[图多请wifi下看]:<a href="https://link.segmentfault.com/?enc=25TGP8s2qnVR1W6v69PQJA%3D%3D.zs4TqJhqtJvQb44W1WFEemr4dzl1%2FmVxjQZB5%2B3u%2BtetVfEH%2BgDG4Urx6c3Y0Tgm89p7oTR9jC0KBvmnUxfL9AmDoXUxTB6DQYRASZ78qkiywAkiJxuWgQe5siLXLvwMYi3LBUo6P1Fezoed2yaQizVWAOTy9MtZvrLNnRSLZop4kZUlEW8CZIAs8IAdxjUGFdZnK3xjidH62PNQM%2FsU41DbtKyOazRsCR8xyofDljJx94%2Fv%2BFrhxRlIyXamsKt2bkEmCBtTaPUcBE6yfit%2FyA%3D%3D" rel="nofollow">https://github.com/cnsnake11/blog/blob/master/ReactNative开发指导/淘宝d2分享-ReactNative变革无线前端.md</a></p>
ReactNative组件状态设计思考
https://segmentfault.com/a/1190000004180955
2015-12-22T10:59:19+08:00
2015-12-22T10:59:19+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
3
<p>这篇文章写的较早,是刚接触RN的时候进行的思考总结,是一个分析的过程,您还可以参考思想更成熟一点的这篇:RN组件架构设计:<a href="http://segmentfault.com/a/1190000004161358">http://segmentfault.com/a/1190000004161358</a></p>
<p>设计React组件与设计一个jquery组件或者一个原生js组件最大的区别就是状态的设计。</p>
<h2>术语定义</h2>
<ol>
<li>
<p>属性</p>
<ol>
<li><p>泛指用户在初始化组件时候可以传给组件的</p></li>
<li><p>有些框架也叫options</p></li>
<li><p>是public的</p></li>
<li><p>在RN体系中,叫props,其中还包含了事件</p></li>
</ol>
</li>
<li>
<p>事件</p>
<ol>
<li><p>是public的</p></li>
<li><p>是function类型的</p></li>
<li><p>在RN体系中使用props来引用</p></li>
</ol>
</li>
<li>
<p>接口</p>
<ol>
<li><p>是public的</p></li>
<li><p>是function类型的</p></li>
<li><p>通过组件实例化之后的对象,可以进行调用</p></li>
</ol>
</li>
<li>
<p>内部属性</p>
<ol>
<li><p>是private的</p></li>
<li><p>传统设计方案中一般用于存储组件的状态,数据等</p></li>
<li><p>RN体系中没有对其进行明确,可以自由设计</p></li>
</ol>
</li>
<li>
<p>状态</p>
<ol>
<li><p>RN体系中明确提出的概念</p></li>
<li><p>传统设计方案中一般使用内部属性来表示</p></li>
</ol>
</li>
</ol>
<h2>传统设计思路</h2>
<p>按照原来设计组件的方法论,一个UI组件对外应该具有属性、事件、接口,对内具有内部属性,内部接口。</p>
<ol>
<li><p>属性就像一份配置文件,描述了组件应该具有的功能、外观或者初始状态。</p></li>
<li><p>事件是组件在工作工程中,当满足某种条件的时候触发的回调函数</p></li>
<li><p>接口是组件对象的方法,可以让组件去执行某个任务</p></li>
</ol>
<p>在原来的设计理念中,并没有提出组件状态的概念,但是其也是一直存在的,通常是使用组件私有属性来存储。</p>
<p>比如,你设计一个按钮,那么他可能有正常状态,禁用状态,那么我们会设计一个属性disable={true|false}来通知组件初始化的具有的状态,设计2个接口disable、enable来赋予js有动态改变其状态的能力,设计2个事件onEnable、onDisable来通知回调函数组件状态发生了变化。伪代码如下:</p>
<pre><code>//组件定义
class button{
constructor(disable,pid){//构造函数
this.disable=disable,//属性:组件初始化使用这个作为状态
this.pid=pid;//属性:父容器的id
this._disable=null,//状态:私有属性
if(this.disable==true){//根据属性来决定初始化状态
this.disable();
}else{
this.enable();
}
this.render();//渲染组件
}
enable(){
this._disable=false;
if(this.el)this.el.set('disable',false);
this.fireEvent('onEnable');//触发事件
}
disable(){
this._disable=true;
if(this.el)this.el.set('disable',true);
this.fireEvent('onDisable');//触发事件
}
render(){
//渲染组件,状态直接影响了组件的表现和功能
$(this.pid).innerHTML='<button disable='+this._disable+' />';
//初始化对dom节点的引用
this.el=$(this.pid).getChild();
}
}
//父容器
<div id='a'></div>
//实例化组件并使用
new button(false,'a');
</code></pre>
<h2>RN设计思路</h2>
<p>上面的示例中,表示了一个传统UI组件的设计思路,_disable就是这个button组件的关键状态,它直接影响了组件的表现和行为。</p>
<p>而react架构直接把组件状态提升到了一个新的高度,主要有以下几点:</p>
<ol>
<li><p>使用固定的接口来声明组件状态和状态默认值</p></li>
<li><p>使用固定的接口来获得和改变组件状态的值</p></li>
<li><p>状态的改变一定会改变组件的表现,会导致组件的重新渲染</p></li>
</ol>
<p>前两项都不是问题,因为我们原来也得有这些东西,只不过写法发生了变化,关键是最后一项。<code>这里说的不是问题,指的是思路上比较容易接受。这种固定写法,问题也很明显,因为要想把一个状态从RN状态体系拿进拿出,会产生一定的工作成本,很是不爽。</code></p>
<p>我一直在思索,组件状态的变化一定要导致view的变化吗?</p>
<p>答案肯定是no,因为有些状态仅仅影响组件的行为,并不影响表现。<code>比如说,我赋予按钮一个新功能,它可以提交某个表单的数据,也就是需要一个新的状态formName,他就不影响表现,只影响行为。</code></p>
<p>考虑到性能的极致,我们就只能把这种状态放到RN的状态体系之外,作为对象的私有属性存在。</p>
<p>RN号称diff算法性能很高,但也不是0损耗,如果对RN的渲染理解很透彻,你当然也可以不这样设计,但是我这里还是倾向于保守一些,否则后期的调整也是有工作量的,因为定义、初始化、取值、改值这些操作代码都不一样。<code>写到这里我突然想到,如果可以,我们可以通过某种方法方便的修改某个状态是否影响表现,不用改其它代码的话,那么我们设计状态的时候就不用区分了,就能让我们更关注设计本身。</code></p>
<h2>结论</h2>
<p>说道这里,基本可以理清思路了。</p>
<p>组件按照传统方式设计,该怎么设计就怎么设计,在设计原来组件内部属性的时候,多考虑一步,哪些内部属性的改变是影响表现的,哪些内部属性是不影响表现的。将影响表现的放入到RN状态体系中,将不影响表现的,放入内部属性中。即可。</p>
<h2>tips</h2>
<ol>
<li><p>耦合性很强的父子组件,建议将状态统一放到父组件中,子组件中不要放状态了,无论是状态的跨组件共享,还是对view渲染的触发都很方便。除非你很确定某个状态只子组件内部使用,通常这种情况后边可能也会发生变化。</p></li>
<li>
<p>以下问题可以帮助识别是否是state,摘自官网</p>
<ol>
<li><p>是否是从父级通过 props 传入的?如果是,可能不是 state 。</p></li>
<li><p>是否会随着时间改变?如果不是,可能不是 state 。</p></li>
<li><p>能根据组件中其它 state 数据或者 props 计算出来吗?如果是,就不是 state 。</p></li>
</ol>
</li>
<li>
<p>根据state的特点进行识别,加入到上一条</p>
<ol><li><p>属性的改变会影响到view的变化就可能是state</p></li></ol>
</li>
<li>
<p>参考地址</p>
<ol>
<li><p><a href="https://link.segmentfault.com/?enc=kfX0gzzZ0Vs7EYxPqNZu7Q%3D%3D.O8TbrmbbafhDd8nE2xzvAPCSQRXLmDvVfYwky2LSo2i8cjmFsMd21iLFWEe7X4suafXHRz1g7p8G27wBnGy%2FvA%3D%3D" rel="nofollow">https://facebook.github.io/react/docs/thinking-in-react.html</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=F0E3i5nMOn3%2FDiAIZMxx%2FA%3D%3D.JhERYrgxfSTOfAQS8Go%2FqxxaYDJbgyDjvwU%2FYr1%2BXDux9xSXiJwTWdKZR9WkjK0CWjtp9PmjDW0H2JMxRS3oWk5D2TNlXejHrnv4Ta%2FKbN8%3D" rel="nofollow">http://wiki.jikexueyuan.com/project/react/thinking-in-react.html</a></p></li>
</ol>
</li>
</ol>
ReactNative的组件架构设计
https://segmentfault.com/a/1190000004161358
2015-12-17T18:17:10+08:00
2015-12-17T18:17:10+08:00
cnsnake11
https://segmentfault.com/u/cnsnake11
13
<p>还有一篇较早的文章,也是分析的过程,可以对本篇文章进行一个补全:RN组件状态设计思考:<a href="http://segmentfault.com/a/1190000004180955">http://segmentfault.com/a/1190000004180955</a></p>
<p>请注意,本篇写的是react native的架构设计,如果你用react来开发web程序,本篇文章只能仅供参考,问题都没有在web上去考虑过。</p>
<p>本篇较长,前面是目前flux开源框架的一些分析,后面是架构设计过程。您可以直奔主题。</p>
<p>用RN最大的难题是设计思想的转变,以前的设计方法论已经不太适用了。而RN仅仅提供了view的框架,构建完整app的架构并没有直接提供。</p>
<p>考虑目前遇到的如下问题,希望架构给出解决方案。</p>
<ol>
<li><p><strong>交互</strong>:如何解决组件间通信【父子、子父、兄弟等,特别是跨层or反向数据流动等】;用state还是接口操作组件;</p></li>
<li><p><strong>职责</strong>:组件状态放哪,业务逻辑放哪,数据放哪,因为太灵活了,怎么做都可以实现功能,但是怎么做才是最好的,才是最正确的呢?</p></li>
</ol>
<p><em>todo一个问题:由于react是面向状态编程,相当于react的组件只关注数据的最终状态,数据是怎么产生的并不关心,但是某些场景下,数据如何产生的是会影响到组件的一些行为的【比如一个新增行要求有动画效果,查询出的行就不需要等】,这在RN中很难描述。。。。。</em></p>
<p>RN架构就是为解决上述问题提供的指导和方法论,是通盘考虑整个开发、测试、运维的状况,做出的考虑最全面的抉择,或者为抉择提供依据。</p>
<p>目前为react服务的架构也有一些了,如Flux,Reflux,Redux,Relay,Marty。</p>
<h2>Flux</h2>
<p>flux是官方提供的架构,目的是分层解耦,职责划分清晰,谁负责干啥很明确。具体描述可以参考官方文档,这里不详述。</p>
<ol>
<li><p>action 封装请求</p></li>
<li><p>dispatcher 注册处理器、分发请求</p></li>
<li><p>store 是处理器,处理业务逻辑,保存数据</p></li>
<li><p>view 根据store提供的数据进行展现;接受用户的输入并发出action请求。</p></li>
</ol>
<p><img src="/img/bVrDul" alt="clipboard.png" title="clipboard.png"></p>
<p>数据流动:<br>Action-> Dispatcher -> Store -> Component</p>
<p>但我觉得解耦的太细了,干一个事,要做太多太多的额外工作了。</p>
<p>光注册监听动作就2次,一次是store注册到dispatcher,一次是view注册到store中。</p>
<p>而且,注册到dispatcher的监听应该都不叫注册,架构完全没有提供任何封装,直接暴露一个统一的回调方法,里面自行if else路由不同的store。</p>
<h2>Reflux</h2>
<p>结构上与flux架构基本一致,去掉了flux的一些冗余操作【比如没有了dispatcher】,架构更加简洁和紧凑,用到了一些约定大于配置的理念。</p>
<p>基本上将flux的架构冗余都简化了,可以说是flux的去冗余提升版,但是没有本质的变化。</p>
<pre><code>╔═════════╗ ╔════════╗ ╔═════════════════╗
║ Actions ║──────>║ Stores ║──────>║ View Components ║
╚═════════╝ ╚════════╝ ╚═════════════════╝
^ │
└──────────────────────────────────────┘
</code></pre>
<ol>
<li><p>更容易的监听。listenables和约定以on开头的方法。等。</p></li>
<li><p>去掉了dispatcher。</p></li>
<li><p>action可以进行aop编程。</p></li>
<li><p>去掉了waitfor。store可以监听store。</p></li>
<li><p>component提供了一系列mixin,方便注册\卸载到store的监听和与store交互等。</p></li>
</ol>
<h2>Redux</h2>
<p>社区内比较受推崇,因为用起来相对比较简单</p>
<p><img src="/img/bVrDum" alt="clipboard.png" title="clipboard.png"></p>
<p>特性:</p>
<ol>
<li><p>分层设计,职责清晰。</p></li>
<li><p>要求store reducer都是页面单例,易于管理。</p></li>
<li><p>action为请求dto对象,是请求类型,请求数据的载体。</p></li>
<li><p>reducer是处理请求的方法。不允许有状态,必须是纯方法。必须严格遵守输入输出,中间不允许有异步调用。不允许对state直接进行修改,要想修改必须返回新对象。</p></li>
<li>
<p>store</p>
<ol>
<li><p>维持应用的state;</p></li>
<li><p>提供 getState() 方法获取 state;</p></li>
<li><p>提供 dispatch(action) 方法分发请求来更新 state;门面模式,要求所有的请求满足统一的格式【可以进行路由、监控、日志等】,统一的调用方式。</p></li>
<li><p>通过 subscribe(listener) 注册监听器监听state的变化。</p></li>
</ol>
</li>
<li><p>官方文档写的较为详细,从设计到开发都有,比flux要好</p></li>
</ol>
<p>痛处如下,看能否接受或者解决:</p>
<ol>
<li>
<p>redux的原则1:state不能被修改。</p>
<ol>
<li><p>其实这个用react的state也会有同样的问题,最好把state设计的没有冗余,尽量少出这种情况</p></li>
<li><p><strong>解决方案:</strong>参考官方:因为我们不能直接修改却要更新数组中指定的一项数据,这里需要先把前面和后面都切开。如果经常需要这类的操作,可以选择使用帮助类 React.addons.update,updeep,或者使用原生支持深度更新的库 Immutable。最后,时刻谨记永远不要在克隆 state 前修改它。</p></li>
</ol>
</li>
<li>
<p>单一的庞大的reducer的拆分</p>
<ol>
<li><p>这块设计也不好做,会让人疑惑</p></li>
<li><p>官方给的demo中直接按state的内容区分,我觉得这样做不好,如果后期有跨内容的情况,就比较奇怪了。官方给的combineReducers方案,也只是减少代码量,本质没有变化,state还是拆分处理,路由还是业务逻辑自己来做。</p></li>
<li><p><strong>解决方案</strong>:还是处理一整个state,可以按照约定写reducer类而不是方法,类里按照actionType建方法,架构自动路由并调用。</p></li>
<li><p>以前做java架构,路由一定是架构来调用的,目前感觉各大flux框架都是解决问题不彻底。</p></li>
</ol>
</li>
<li><p>官方建议设计模式:顶层容器组件才对redux有依赖,组件间通过props来传递数据。按照这样设计还是没有解决组件间交互和数据传递的问题。官方react设计建议:react的设计建议:<a href="https://link.segmentfault.com/?enc=m3Qyuv%2BuZZ0M99CZ9h9toQ%3D%3D.p%2BSJ888dpuuvqpGFr5iVrcXqAE7APBQaeLSebbkz3RdSz0lfHBYkaWf8hVzovvrDijuR5Yl64mohVP1BuQ2%2BKM5HcxvXNf23hLdgJjAgiA0%3D" rel="nofollow">http://camsong.github.io/redux-in-chinese/docs/basics/UsageWithReact.html</a></p></li>
<li><p>使用connect将state绑定到component。此处有些黑盒了。</p></li>
<li><p>异步action用来请求服务端数据,利用middleware增强createStore的dispatch后即支持。</p></li>
</ol>
<h2>Relay</h2>
<p>没有时间,没做研究</p>
<h2>Marty</h2>
<p>没有时间,没做研究</p>
<h2>结论</h2>
<p>开源架构封装的简单的flux会产生较多的冗余代码。</p>
<p>开源架构封装的复杂的redux,其和RN绑定封装了一些东西,是一个黑盒,不易理解和维护。</p>
<p>介于上述两者之间的开源架构reflux,文档较上述2个少,不知道其可持续性如何。如果一定要用开源架构的话,我觉得他稍加封装是一个较为推荐的选择。</p>
<p>不是特复杂的程序【一般spa的程序会更复杂一些,而RN并不是spa】,这些概念只会增加你的开发难度,并且对后面维护的人要求更高。</p>
<p>我们继续头脑风暴,继续抽象总结一下flux系列框架, flux系列框架干了什么,没干什么,针对开篇提出的问题。</p>
<ol>
<li><p>【解决职责】flux系列框架都做到了解耦,分层,谁该干什么就干什么,不许干别的,让代码读起来更有预测性和一致性,方便维护</p></li>
<li><p>【解决通信】继续解耦,flux系列框架采用事件机制解决各层之间通信,采用props传递解决各组件之间通信。</p></li>
</ol>
<h4>事件系统是关键</h4>
<p>flux系列架构解决通信问题的方法是使用事件系统,事件系统中的回调函数是业务逻辑,redux是【store action reducer】,flux是【action dispacher store】。</p>
<p>我们真的需要事件系统吗?</p>
<p>事件系统的好处:</p>
<ol>
<li><p>一个事件可以注册多个回调函数</p></li>
<li><p>各回调函数间没有耦合。</p></li>
</ol>
<p>关于1</p>
<p>需要注册多个的这种情况并不多见,不信你去翻看你已经写好的代码,是不是大部分都是注册一个。</p>
<p>关于2 </p>
<p>解耦确实很彻底,但是当我需要控制执行顺序,需要等a执行完在执行b,怎么办?ok你可以先注册a在注册b啊。那a要是一个fetch或ajax操作呢?这时候只能乖乖的在a的请求结束回调函数中进行调用b了。又变成a依赖b了。当然,你可以继续dispatch(b),这就没有耦合了。但是你要知道注册一个事件是要有成本的,要写action,而且这种dispatch的方式,真的不太适合人类的阅读,dispatch一下,下一步都有谁来执行都不知道,这哪有直接调用来的爽快。</p>
<p>好吧说到这,最后的结论也出来了,不使用开源架构,借助其好的思想,替换其事件系统为面向对象结构,自行封装架构。</p>
<h2>架构设计</h2>
<p>再次强调:目前仅考虑如何应用于react native</p>
<h3>先扣题,针对开篇问题的解决方案如下</h3>
<h4>交互</h4>
<ol>
<li><p>组件对外发布:组件对外只允许使用props来暴露功能,不允许使用接口及其它一切方式</p></li>
<li><p>父子组件间:组件的子组件通过父组件传递的接口来与父组件通信</p></li>
<li>
<p>兄弟组件间:</p>
<ol>
<li><p>方案1:假设a要调用b,参考第一条的话,其实就是a要改变b的props,那么a只要改b的props的来源即可,b的props的来源一般就是根组件的state。那么根组件就要有组织和协调的能力。</p></li>
<li><p>方案2:利用事件机制,基本同flux架构。略复杂,且我们并不需要事件的特性,本架构设计不推荐。</p></li>
</ol>
</li>
</ol>
<h4>职责</h4>
<ol>
<li><p>root-存放state,组织子view组件,组织业务逻辑对象等</p></li>
<li><p>子view组件-根据this.props渲染view。</p></li>
<li><p>业务逻辑对象-提供业务逻辑方法</p></li>
</ol>
<p>根据以上推导,我将其命名为面向对象的ReactNative架构设计,它与flux系列架构的最大的不同之处在于,用业务逻辑对象来代替了【store action dispatcher】or【store reducer】的事件系统。业务逻辑对象就是一组对象,用面向对象的设计理念设计出的n个对象,其负责处理整个页面的业务逻辑。</p>
<p>以上为推导过程,干货才开始。。。。</p>
<h3>面向对象的ReactNative组件\页面架构设计</h3>
<p>一个独立完整的组件\页面一般由以下元素构成:</p>
<ol>
<li>
<p>root组件,1个,</p>
<ol>
<li><p>负责初始化state</p></li>
<li><p>负责提供对外props列表</p></li>
<li><p>负责组合子view组件形成页面效果</p></li>
<li><p>负责注册业务逻辑对象提供的业务逻辑方法</p></li>
<li><p>负责管理业务逻辑对象</p></li>
</ol>
</li>
<li>
<p>view子组件,0-n个,</p>
<ol><li><p>根据props进行视图的渲染</p></li></ol>
</li>
<li>
<p>业务逻辑对象,0-n个,</p>
<ol><li><p>提供业务逻辑方法</p></li></ol>
</li>
</ol>
<h4>root组件</h4>
<p>root组件由以下元素组成:</p>
<ol>
<li><p>props-公有属性</p></li>
<li><p>state-RN体系的状态,必须使用Immutable对象</p></li>
<li><p>私有属性</p></li>
<li><p>业务逻辑对象的引用-在componentWillMount中初始化</p></li>
<li><p>私有方法-以下划线开头,内部使用or传递给子组件使用</p></li>
<li><p>公有方法【不推荐】,子组件和外部组件都可以用,但不推荐用公有方法来对外发布功能,破坏了面向状态编程,尽可能的使用props来发布功能</p></li>
</ol>
<p><img src="/img/bVrHM5" alt="clipboard.png" title="clipboard.png"></p>
<h4>注意:定义root组件的state的时候,如果使用es6的方式,要把state的初始化放到componentWillMount中,如果在构造器中this.props为空。</h4>
<h4>子view组件</h4>
<p>子view组件中包含:</p>
<ol>
<li><p>props-公有属性</p></li>
<li>
<p>私有属性-如果你不能理解下面的要求,建议没有,统一放在父组件上</p>
<ol>
<li><p>绝对不允许和父组件的属性or状态有冗余。无论是显性冗余还是计算结果冗余,除非你能确定结算是性能的瓶颈。</p></li>
<li><p>此属性只有自己会用,父组件和兄弟组件不会使用,如果你不确定这点,请把这个组件放到父组件上,方便组件间通信</p></li>
</ol>
</li>
<li><p>私有方法-仅作为渲染view的使用,不许有业务逻辑</p></li>
<li><p>公有方法【不推荐,理由同root组件】</p></li>
</ol>
<p><img src="/img/bVr1A7" alt="clipboard.png" title="clipboard.png"></p>
<h4>业务逻辑对象</h4>
<p>业务逻辑对象由以下元素组成:</p>
<ol>
<li><p>root组件对象引用-this.root</p></li>
<li><p>构造器-初始化root对象,初始化私有属性</p></li>
<li><p>私有属性</p></li>
<li><p>公有方法-对外提供业务逻辑</p></li>
<li><p>私有方法-以下划线开头,内部使用</p></li>
</ol>
<p><img src="/img/bVr1Ba" alt="clipboard.png" title="clipboard.png"></p>
<h4>ps1:通用型组件只要求尽量满足上述架构设计</h4>
<p>通用型组件一般为不包含任何业务的纯技术组件,具有高复用价值、高定制性、通常不能直接使用需要代码定制等特点。</p>
<p>可以说是一个系统的各个基础零件,比如一个蒙板效果,或者一个模态弹出框。</p>
<p>架构的最终目的是保证系统整体结构良好,代码质量良好,易于维护。一般编写通用型组件的人也是经验较为丰富的工程师,代码质量会有保证。而且,作为零件的通用组件的使用场景和生命周期都和普通组件\页面不同,所以,仅要求通用组件编写尽量满足架构设计即可。</p>
<h4>ps2:view子组件复用问题</h4>
<p>抛出一个问题,设计的过程中,子组件是否需要复用?子组件是否需要复用会影响到组件设计。</p>
<ol>
<li><p>需复用,只暴露props,可以内部自行管理state【尽量避免除非业务需要】</p></li>
<li><p>不需复用,只暴露props,内部无state【因为不会单独使用,不需要setState来触发渲染】</p></li>
</ol>
<p>其实, 一般按照不需复用的情况设计,除非复用很明确,但这时候应该抽出去,变成独立的组件存在就可以了,所以这个问题是不存在的。</p>
<h2>适用场景分析</h2>
<h3>flux系列框架</h3>
<p>flux系列框架的适用场景我觉得应具有以下特点:</p>
<p><strong>一个页面中组件较多,组件之间较为独立,但是重叠使用模型,模型的变化会导致很多组件的展现和行为。</strong></p>
<p>比如,开发一个类似qq的聊天页面,左侧是联系人列表,右侧是与某人的消息对话框,当收到一个消息之后,1要刷新左侧联系人列表的最近联系人,2要右侧的消息对话框中显示这个消息,3要页面title要提示新消息。这就是典型的一个新消息到来事件【消息模型发生了变化】触发三个无关联的组件都有行为和展现的变化。如果用事件系统来开发就做到了解耦的极致,未来如果还要加入第4种处理也不用修改原来的逻辑,就直接注册一下就可以了,满足了开闭原则。</p>
<p><strong>需要对app运行过程进行监控,数据采样等</strong></p>
<p>flux系列框架是一个典型的门面模式,业务动作都要通过统一的门面dispatch进行,天生具有良好的监控解决方案。</p>
<h3>面向对象的RN组件架构</h3>
<p>面向对象的RN组件架构的使用场景特点我没有总结出来,我觉得所有场景都可以用,只要你业务逻辑对象设计的好,都不是问题。</p>
<p>还拿上面聊天界面举例子,面向对象的RN组件架构其实也可以解耦的写出写上述场景,你完全可以将业务逻辑对象之间的交互设计成一个小的事件系统,只是架构没有直接约束这种解耦,flux系列架构直接在架构中就强制编码人员做到了解耦,但是如果我不需要解耦的时候就相当于增加了复杂度,得不偿失了。</p>
<p>所以面向对象的RN组件架构要更灵活,也更简单更容易让人理解,更容易预测代码的执行流向,但同时因为灵活对业务逻辑对象设计者的要求也较高,针对较为复杂or重要页面建议进行详细设计并leader检查来保证质量。</p>
<h2>如何做监控</h2>
<p>因为面向对象的RN架构中去掉了统一的业务逻辑调用facade入口dispatch,那我们如何来做监控呢。</p>
<h3>方案1:在需要监控的地方人为加入监控点。</h3>
<p>这个方案对业务代码和监控代码的耦合确实有点大,是最差的解决方案了。不推荐。</p>
<h3>方案2:在基类BaseLogicObj的构造器中对对象的所有方法进行代理-todo待验证</h3>
<p>这个方案对业务代码透明,但是还只是个想法,未进行代码测试和验证。</p>
<h3>方案3.....还没有想出别的方案,有没有同学给点思路?</h3>
<h2>架构之美</h2>
<p>最后在分享demo代码之前,摘抄了天猫前端架构师团队对架构的认识,个人觉得十分认同。</p>
<p>简单:<br>简单的东西才能长久,HTML、CSS和JavaScript之所以能够活到现在,而其他类似的很牛的方案都死掉了,原因之一是简单,才有那么多人用它,所以我们需要把技术和产品方案朝着简单的思路发展,简单才是本质,复杂一定是临时的会过时的。天猫的前端技术架构为什么都基于Kissy,为什么是两层架构,就是朝着简单的方式去思考。看起来简单,用起来简单。</p>
<p>高效:<br>简单是高效的前提,复杂的高效都是临时的会过时的,技术架构一定要提高团队的工作效率,否则一定会被抛弃,因此把简单的规则自动化,把精确的重复的事情让机器去做,前端这么多年为什么开发环境不够成熟,就是自动化的工具太少,前端又很少能力驾驭编写工具的语言,而Nodejs的出现是一个前所未有的机会。</p>
<p>灵活:<br>高效往往和灵活是对立的,就像移动上Native和Web的关系,而我们就需要思考如何做到两者兼顾,既高效又灵活,所以要不断把事情做简单,思考本质、看到本质,基于本质去实现。比如Apple为什么敢于把鼠标和键盘去掉,是因为确信人直接和界面打交道比借助一个中间硬件更能够表达人机交互的本质。</p>
<p>新鲜:<br>面向未来,前端需要不停地更新自己,无论是思想还是技术。比如整个天猫基于Kissy,那么就使用最新的Kissy版本,基础设施能够升级是一种能力,如果有一天基础设施升不了啦,那么这套技术架构就老去了。比如发现Gulp比Grunt更能够代表未来,那么我们毫不犹豫地整个团队开始进行升级。</p>
<h2>完整demo代码</h2>
<p>此demo仿照redux提供的todolist demo编写。</p>
<p>redux demo 地址:<a href="https://link.segmentfault.com/?enc=LkI%2Bx1mn9tuXLMDwWsfbIA%3D%3D.cHSK%2BjI9MTimGlfKsudxAwwSKMO82rrwGJ%2BBq2LtVyk1XN8otZnNtuJYYIM%2BPmjix%2F4X6TDYyeCD6iTOFpLxabiRSrQZHhGN2TD9MlKLkAU%3D" rel="nofollow">http://camsong.github.io/redux-in-chinese/docs/basics/ExampleTodoList.html</a></p>
<p>demo截图:</p>
<p><img src="/img/bVrDuH" alt="clipboard.png" title="clipboard.png"></p>
<p>todolist页面:</p>
<pre><code>
'use strict'
let React=require('react-native');
let Immutable = require('immutable');
var BbtRN=require('../../../bbt-react-native');
var {
BaseLogicObj,
}=BbtRN;
let {
AppRegistry,
Component,
StyleSheet,
Text,
View,
Navigator,
TouchableHighlight,
TouchableOpacity,
Platform,
ListView,
TextInput,
ScrollView,
}=React;
//root组件开始-----------------
let Root =React.createClass({
//初始化模拟数据,
data:[{
name:'aaaaa',
completed:true,
},{
name:'bbbbb',
completed:false,
},{
name:'ccccc',
completed:false,
}
,{
name:'ddddd',
completed:true,
}],
componentWillMount(){
//初始化业务逻辑对象
this.addTodoObj=new AddTodoObj(this);
this.todoListObj=new TodoListObj(this);
this.filterObj=new FilterObj(this);
//下面可以继续做一些组件初始化动作,比如请求数据等.
//当然了这些动作最好是业务逻辑对象提供的,这样root组件将非常干净.
//例如这样:this.todoListObj.queryData();
},
//状态初始化
getInitialState(){
return {
data:Immutable.fromJS(this.data),//模拟的初始化数据
todoName:'',//新任务的text
curFilter:'all',//过滤条件 all no ok
}
},
//这里组合子view组件 并 注册业务逻辑对象提供的方法到各个子view组件上
render(){
return (
<View style={{marginTop:40,flex:1}}>
<AddTodo todoName={this.state.todoName}
changeText={this.addTodoObj.change.bind(this.addTodoObj)}
pressAdd={this.addTodoObj.press.bind(this.addTodoObj)} />
<TodoList todos={this.state.data}
onTodoPress={this.todoListObj.pressTodo.bind(this.todoListObj)} />
<Footer curFilter={this.state.curFilter}
onFilterPress={this.filterObj.filter.bind(this.filterObj)} />
</View>
);
},
});
//业务逻辑对象开始-------------------------可以使用OO的设计方式设计成多个对象
//业务逻辑对象要符合命名规范:以Obj结尾
//BaseLogicObj是架构提供的基类,里面封装了构造器和一些常用取值函数
class AddTodoObj extends BaseLogicObj{
press(){
if(!this.getState().todoName)return;
let list=this.getState().data;
let todo=Immutable.fromJS({name:this.getState().todoName,completed:false,});
this.setState({data:list.push(todo),todoName:''});
}
change(e){
this.setState({todoName:e.nativeEvent.text});
}
}
class TodoListObj extends BaseLogicObj {
pressTodo(todo){
let data=this.getState().data;
let i=data.indexOf(todo);
let todo2=todo.set('completed',!todo.get('completed'));
this.setState({data:data.set(i,todo2)});
}
}
class FilterObj extends BaseLogicObj {
filter(type){
let data=this.getState().data.toJS();
if(type=='all'){
data.map((todo)=>{
todo.show=true;
});
}else if(type=='no'){
data.map((todo)=>{
if(todo.completed)todo.show=false;
else todo.show=true;
});
}else if(type=='ok'){
data.map((todo)=>{
if(todo.completed)todo.show=true;
else todo.show=false;
});
}
this.setState({curFilter:type,data:Immutable.fromJS(data)});
}
}
//view子组件开始---------------------------
//子view对象中仅仅关注:从this.props转化成view
let Footer=React.createClass({
render(){
return (
<View style={{flexDirection:'row', justifyContent:'flex-end',marginBottom:10,}}>
<FooterBtn {...this.props} title='全部' name='all' cur={this.props.curFilter=='all'?true:false} />
<FooterBtn {...this.props} title='未完成' name='no' cur={this.props.curFilter=='no'?true:false} />
<FooterBtn {...this.props} title='已完成' name='ok' cur={this.props.curFilter=='ok'?true:false} />
</View>
);
},
});
let FooterBtn=React.createClass({
render(){
return (
<TouchableOpacity onPress={()=>this.props.onFilterPress(this.props.name)}
style={[{padding:10,marginRight:10},this.props.cur?{backgroundColor:'green'}:null]} >
<Text style={[this.props.cur?{color:'fff'}:null]}>
{this.props.title}
</Text>
</TouchableOpacity>
);
},
});
let AddTodo=React.createClass({
render(){
return (
<View style={{flexDirection:'row', alignItems:'center'}}>
<TextInput value={this.props.todoName}
onChange={this.props.changeText}
style={{width:200,height:40,borderWidth:1,borderColor:'e5e5e5',margin:10,}}></TextInput>
<TouchableOpacity onPress={this.props.pressAdd}
style={{backgroundColor:'green',padding:10}} >
<Text style={{color:'fff'}} >
添加任务
</Text>
</TouchableOpacity>
</View>
);
},
});
let Todo=React.createClass({
render(){
let todo=this.props.todo;
return (
todo.get("show")!=false?
<TouchableOpacity onPress={()=>this.props.onTodoPress(todo)}
style={{padding:10,borderBottomWidth:1,borderBottomColor:'#e5e5e5'}}>
<Text style={[todo.get('completed')==true?{textDecorationLine:'line-through',color:'#999'}:null]} >
{todo.get('completed')==true?'已完成 ':'未完成 '} {todo.get('name')}
</Text>
</TouchableOpacity>
:null
);
},
});
let TodoList=React.createClass({
render(){
return (
<ScrollView style={{flex:1}}>
{this.props.todos.reverse().map((todo, index) => <Todo {...this.props} todo={todo} key={index} />)}
</ScrollView>
);
},
});
module.exports=Root;
</code></pre>
<p>业务逻辑对象基类BaseLogicObj:</p>
<pre><code>
'use strict'
class BaseLogicObj{
constructor(root){
if(!root){
console.error('实例化BaseLogicObj必须传入root组件对象.');
}
this.root=root;
}
getState(){
return this.root.state;
}
setState(s){
this.root.setState(s);
}
getRefs(){
return this.root.refs;
}
getProps(){
return this.root.props;
}
}
module.exports=BaseLogicObj;
</code></pre>