从一个React Native项目回顾几个技术点

Author Avatar
killua167 7月 30, 2018

前段时间公司碰巧有个较小的项目,我们老大想试下前段时间比较火的React Native。于是我很荣幸作为项目的主程开始我的第一个RN项目。以下是记录了这个项目中用到的技术点。

React Navigation

项目首页
项目使用了React Navigation作为整个项目的入口和导航栏。由于项目是典型的多标签页设计,所以用了TabNavigator嵌套多个StackNavigator的方式实现。

const DigStack = StackNavigator({
    Home: {screen: DigPage},
    Login: {screen: Login},
    Register: {screen: RegisterPage},
    Invite: {screen: InvitePage},
    ...
}, {
    headerMode: 'screen',
});

const MineStack = StackNavigator({
    Home: {screen: MinePage},
    UserSetting: {screen: UserSetting},
    Assets: {screen: AssetsPage},
    Login: {screen: Login},
    ...
}, {
    headerMode: 'screen',
});

const MineStack = StackNavigator({
    Home: {screen: MinePage},
    UserSetting: {screen: UserSetting},
    Assets: {screen: AssetsPage},
    ...
}, {
    headerMode: 'screen',
});

export default TabNavi = TabNavigator({
    Dig: {
        screen: DigStack,
    },
    Cybertron: {
        screen: CybertronStack,
    },
    Mine: {
        screen: MineStack,
}, {
    tabBarComponent: TabBarBottom,
    tabBarPosition: 'bottom',
    swipeEnabled: false,
    animationEnabled: false,
    navigationOptions: ({navigation}) => ({
        tabBarVisible: navigation.state.index === 0
    }),
    tabBarOptions: {
        activeTintColor: '#34c4ff',
        inactiveTintColor: '#979797',
        style: {backgroundColor: '#ffffff'},
        labelStyle: {
            fontSize: 14 // 文字大小
    }}
})

这样的好处是整个页面的结构和各个页面的关系十分清晰,当项目迭代需要添加新的页面时,添加页面的路由也十分方便。由于是TabNavigator嵌套StackNavigator,navigate后的页面会默认有TabBar,所以要在TabNavigator配置

navigationOptions: ({navigation}) => ({
        tabBarVisible: navigation.state.index === 0
    })

网络请求

通过Promise.race实现请求超时处理。
首先了解一下Promise.race():

监视多个Promise。接受一个包含需监视的Promise的可迭代对象,并返回一个新的Promise,但一旦来源Promise中有一个被解决,所返回的Promise就会立刻被解决。

通过这样一个类似竞赛的原理,先创建一个会reject的Promise,然后再和网络请求的Promise”比赛”一下,最后用setTimeout进行处理。

//默认10秒超时
const timeout = 10000;
export function post (url, paramsString) {

    let timeout_fn = null;

    //这是一个可以被reject的promise
    let timeout_promise = new Promise(function(resolve, reject) {
        timeout_fn = function() {
            reject('请求超时,请检查网络');
        };
    });
    //这里使用Promise.race,以最快 resolve 或 reject 的结果来传入后续绑定的回调
    let abortable_promise = Promise.race([
        fetch_promise(url, paramsString),
        timeout_promise
    ]);

    setTimeout(function() {
        timeout_fn();
    }, timeout);

    return abortable_promise ;
}

function fetch_promise(url, paramsString) {
    return new Promise((resolve, reject) => {
        fetch (BaseURLString + url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: paramsString
        })
            .then((response) => response.json())
            .then((responseJson) => {
                console.log(responseJson);
                switch (responseJson.code) {
                    case 1002: User.clearUser(); reject(responseJson.msg); break;
                    case 1001: reject(responseJson.msg); break;
                    default: resolve(responseJson.result); break;
                }
            })
            // .catch(error => {
            //     console.log(error);
            //     reject(error)
            // });
    })
}

另外,fetch_promise中会根据返回的json的code处理各种情况,如登录过期需要清空用户信息等。

使用async/await解决异步执行导致的顺序错乱问题

项目中使用了react-native-storage储存用户的信息,每次App启动会在磁盘中读取,读取的请求是异步的,然后根据读取的用户信息再进行网络请求,最后返回特定的信息。那么问题来了,多个异步请求,怎么保证执行顺序呢?就是用async/await。
首先看下async/await的基本语法:

const f = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(123);
    }, 2000);
  });
};

const testAsync = async () => {
  const t = await f();
  console.log(t);
};

testAsync();

首先定义了一个函数 f,这个函数返回一个 Promise,并且会延时 2 秒,resolve 并且传入值 123。testAsync 函数在定义时使用了关键字 async,然后函数体中配合使用了 await,最后执行 testAsync。整个程序会在 2 秒后输出 123,也就是说 testAsync 中常量 t 取得了 f 中 resolve 的值,并且通过 await 阻塞了后面代码的执行,直到 f 这个异步函数执行完。

以下是项目中具体的使用:

//User.js
static async getUser () {
        let user = {};
        await storage.load({
            key: 'user',
            autoSync: true,
            syncInBackground: true
        }).then(ret => {
            user = ret;
        }).catch(err => {
            switch (err.name) {
                case 'NotFoundError':
                    break;
                case 'ExpiredError':
                    // TODO
                    break;
            }
        });
        return user
}
//home.js
User.getUser().then(ret => {
            if (ret.user_token) { //获取成功
                this.user = ret;
                //网络请求
                this.getUserInfo(); 
            } else { //获取失败或者为空
            //用户信息情况
            this.user = {}; 
            //标记用户未登陆
            this.setState({isLogin: false}); 
            }
        });

几个小技巧

深拷贝

大家都知道深拷贝的重要性,方法也有不少,但在这次项目中,发现最有效的还是利用JSON 深拷贝

JSON.parse(JSON.stringify(obj));

对于一般的需求是可以满足的,但是它有缺点。下例中,可以看到JSON复制会忽略掉值为undefined以及函数表达式。

var obj = {
    a: 1,
    b: 2,
    c: undefined,
    sum: function() { return a + b; }
};

var obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2);
//Object {a: 1, b: 2}

px to Dp

UI给的原始尺寸的px值和RN项目中的Dp值是需要转换的,否则换个设备布局又乱了。尝试了网上几个转换方案,以下转换方法最为简单粗暴:

// app 只有竖屏模式,所以可以只获取一次 width
const deviceWidthDp = Dimensions.get('window').width;

// UI 默认给图是 750
const uiWidthPx = 750;

function pxToDp (uiElementPx) {
    const transferNumb = uiElementPx * deviceWidthDp / uiWidthPx;

    if (transferNumb >= 1) {
        // 避免出现循环小数
        return Math.ceil(transferNumb);
    } else if (Platform.OS === 'android') {
        // 如果是安卓,最小为1,避免边框出现锯齿
        return 1;
    }
    return 0.5;
}