TS版react-native三端同构设计

什么是三端同构?

对于刚接触rn开发的同学可能不是太了解,简单介绍一下。三端指的是安卓、IOS、H5。rn本身就是跨平台框架,为了让RN能在不改动代码的情况下同时兼容H5,只需要引入一些库和配置一些细节即可。

本模板采用目前react-native@0.6x,react-navigation@5.x新版本,搭建ts开发环境。
新版本相关库的API与老rn版本有些是不兼容的,请不要随意更改主版本!

不管是对于刚入门RN的小白,还是熟练运用RN老版本的熟手,都能轻松上手
模板源码

搭建流程(从零起步)

1. 安装RN

照着官网配置开发环境,不熟悉客户端开发的前端可以选沙盒环境。但是建议一步步装客户端环境,毕竟现在跨平台时代。

2. 使用react-native来生成typescript模板

首先新版本的RN已经不再使用react-native-cli,也就是说我们不需要全局安装任何包。只要你的npm版本在5.2+,可以使用npx命令(npx介绍)即可。

npx react-native init MyApp --template react-native-template-typescript

npx生成rn for ts模板,安装完成后这就是一个官方可运行的RN项目了,接下来我们就来修改这个模板

3. 配置package.json

为了使用新版本的@react-navigation v5搭建完整可用的RN开发项目、以及我们要做的三端同构 ,需要新增一些npm依赖的包。

"dependencies": {
    // ...模板自带的包
    // ...
    "@react-native-community/async-storage": "1.9.0",  // 相当于localStorage(可选)
    "@react-native-community/masked-view": "0.1.9",    // 路由所需要的包 (必须)
    "@react-navigation/stack": "5.2.10",    // 路由所需要的包 (必须)
    "react": "16.11.0",
    "react-dom": "16.11.0",
    "react-native": "0.62.2",
    "react-native-gesture-handler": "1.6.0",      // 路由所需要的包,原生手势系统 (必须)
    "react-native-reanimated": "^1.8.0",    // 路由所需要的包 (必须)
    "react-native-safe-area-context": "0.7.3",    // 路由所需要的包 (必须)
    "react-native-screens": "^2.3.0",    // 路由所需要的包 (必须)
    "react-native-webview": "8.0.0",    // RN打开webView容器,原生调用 (可选)
    "react-redux": "5.0.7",
    "redux": "4.0.0",
    "redux-thunk": "2.3.0"
  },
"devDependencies": {
    // ...模板自带的包
    // ...
    "@babel/plugin-transform-runtime": "^7.9.0",    // babel的插件包(必须)
    "@babel/preset-typescript": "^7.9.0",      // babel的编译ts(必须)
    "@react-navigation/core": "3.4.2",        // rn路由在H5里运行的核心包(必须)
    "@react-navigation/web": "1.0.0-alpha.8",  // rn路由在H5里运行的核心包(必须)
    "babel-loader": "^8.1.0",    // webpack loader(必须)
    "file-loader": "3.0.1",          // webpack loader(必须)
    "html-webpack-plugin": "^4.2.0",          // webpack plugin(必须)
    "react-native-web": "0.12.2",      // rn组件映射为WEB的dom(必须)
    "webpack": "4.42.1",      // 打包rn项目到h5(必须)
    "webpack-cli": "3.3.2",    // 打包rn项目到h5(必须)
    "webpack-dev-server": "3.5.1"    // 调试rn项目到h5(必须)
  },

该装的都装上别漏了,建议梭哈

4. 配置weback和文件入口

首先新建一个index.html文件作为H5打包模板
根目录下index.ts为RN为原生打包的入口,新建一个index.web.ts当做H5的打包入口,注意在webpack中添加

resolve: {
        extensions: [
            '.web.ts',
            '.web.tsx',
            '.ts',
            '.tsx',
            '.js',
            '.jsx'
        ],
        alias: {
            'react-native$': 'react-native-web'
        }
    },

extensions配置的目的是为了让我们的项目能够在import XX from './xx'引入文件时,能够编写两个不同后缀的文件xx.web.tsxx.ts。一个给RN打包给原生,一个给webpack打包给H5,用来对不同平台的代码做定制化开发,这一点在路由上是极其有用的。.web.ts一定要配置在.ts前面,让webpack优先找到属于H5的文件打包。

另外alias就是将react-native整个库的组件映射为react-native-web,实际上这个库的原理就是为RN每个组件写对应的dom然后传入props。因为你也可以自行拓展,有的原生组件react-native-web是没有的,比如一开始提到的webView。

{
            test: /\.(js|jsx|ts|tsx)$/,
            use: [
                {
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: false,
                        presets: [
                            'module:metro-react-native-babel-preset',
                            '@babel/preset-typescript'
                        ],
                        plugins: [
                            '@babel/plugin-transform-runtime'
                        ]
                    }
                }
            ],
            exclude: /node_modules/
        }

其次配置babel-loader来编译ts,这里尤其要注意:
不能使用ts-loader来编译!
因为跟文件tsconfig.json中我们配的jsx识别为react-native,是用来编译给客户端的,但是实际上面我们将其代理成了dom元素,打包会产生冲突导致loader编译错误。解决方案为直接使用babel-loader进行配置编译TS,生成sourcemap

5. 配置React-navigation、Redux

Redux状态管理和普通react一样,也可以用别的库,毕竟状态管理不涉及到渲染。主要说明React-navigation,这个RN的路由库,每个版本的API差异很大,V5的版本将API拆分为多个组@react-navigation/,有很多种路由配置选择,我这里提供一种:

// router.ts
import 'react-native-gesture-handler';
import React from 'react';
import { connect } from 'react-redux';
import AsyncStorage from '@react-native-community/async-storage';
import {
    InitialState,
    useLinking,
    NavigationContainerRef,
    NavigationContainer,
    DefaultTheme,
    DarkTheme
} from '@react-navigation/native';
import {
    createStackNavigator,
    HeaderStyleInterpolators
} from '@react-navigation/stack';

import routes from './config';

type RootDrawerParamList = {
    [key: string]: any;
};

const Stack = createStackNavigator<RootDrawerParamList>();

const HeaderNull = function(): React.ReactNode {
    return null;
};

const MyApp = function() {
    const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';

    const containerRef = React.useRef<NavigationContainerRef>(null);
    const [initialState, setInitialState] = React.useState<
        InitialState | undefined
    >();
    const [theme, setTheme] = React.useState(DefaultTheme);

    return <NavigationContainer
        ref={containerRef}
        initialState={initialState}
        onStateChange={async (state) => {
            try {
                await AsyncStorage.setItem(
                    NAVIGATION_PERSISTENCE_KEY,
                    JSON.stringify(state)
                );
            } catch (e) {
                console.log(e);
            }
        }}
        theme={theme}
    >
        <Stack.Navigator>
            {(Object.keys(routes) as (keyof typeof routes)[]).map(
                (name) => (
                    <Stack.Screen
                        key={name}
                        name={name}
                        component={routes[name].screen}
                        options={{
                            header: props => HeaderNull()
                        }}
                    />
                )
            )}
        </Stack.Navigator>
    </NavigationContainer>;
};

const mapStateToProps = (state: any) => state;

export default connect(mapStateToProps)(MyApp);

简单来说就是把路由配置导入就行了,但是这个config要抽离出来,因为我们在这个目录下还有一个route.web.ts,同样要引用这个配置,但是引用配置的库却是不同的,在H5里我们使用@react-navigation/web

// router.web.ts
import { connect } from 'react-redux';
import { createSwitchNavigator } from '@react-navigation/core';
import { createBrowserApp } from '@react-navigation/web';

import routes from './config';

const MyNavigator = createSwitchNavigator(routes);

const MyApp = createBrowserApp(MyNavigator);

const mapStateToProps = state => state;

export default connect(mapStateToProps)(MyApp);

这样一来,我们就能保证路由的每个页面都是引用的相同的组件了

6. link所使用的原生组件库

该项目最开始配置package.json有两个原生模块模块包,需要使用link进行关联操作,否则构建客户端报错。在npm i后执行:

  • npx react-native link react-native-gesture-handler 这个是react-navigation使用的原生的手势系统。
  • npx react-native link @react-native-community/async-storage 这个类似于浏览器的localStorage原生缓存。
7. 让模板启动起来吧~

配置好npm启动命令

// package.json
"scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "web:dev": "webpack-dev-server --config webpack.config.web.ts --inline --hot --colors",
    "web": "webpack -p --config webpack.config.web.ts"
  },
  • 原生客户端使用npm run androidnpx react-native run-android(npm v5.2+)启动
  • npm run web:dev启动webpack-dev-server调试,和普通移动端开发一样
  • npm run web打包为生产环境代码到dist-H5
8. 效果展示
  • Native:


    native-0

    native-1

    native-2
  • H5


    H5-0

    H5-1
9. 总结

从上面的截图可以看到几点:

  1. UI样式上基本能一比一还原
  2. 路由动画不如原生(这个貌似图片看不到...)
  3. H5上首页有个红圈,这是我故意用了一个react-native-web不支持的组件,在H5就会标红这个区域。
  4. Native-3中有一个客户端半屏webView容器打开了第三方H5,是使用的原生RN组件,这个在H5里显然是没有的,所以遇到这种情况,需要单独对环境进行兼容处理。(我这里没有兼容,所以打包H5的时候你能看到webpack抛出的警告)

其实总体来说还是OK的,生产环境下只建议使用UI堆积类应用,H5硬件交互还是要走JSBridge桥接原生。但是用来在浏览器调试RN开发的视图也不错~

如果对项目配置细节(路由,webpack,ts等)有疑惑的,可以直接参考已经搭好的模板:
github.com/yukilzw/rn_web

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容