{"id":17322011,"url":"https://github.com/yukilzw/rn_web","last_synced_at":"2025-10-12T04:44:02.967Z","repository":{"id":48554722,"uuid":"108393677","full_name":"yukilzw/rn_web","owner":"yukilzw","description":"H5 \u0026 react-native 三端同构","archived":false,"fork":false,"pushed_at":"2022-10-20T21:59:18.000Z","size":709,"stargazers_count":16,"open_issues_count":2,"forks_count":3,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-03-28T04:51:12.397Z","etag":null,"topics":["react-native","react-native-web","react-navigation-v5","redux","webpack4"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yukilzw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-26T09:56:31.000Z","updated_at":"2024-10-23T13:40:13.000Z","dependencies_parsed_at":"2023-01-20T08:46:58.492Z","dependency_job_id":null,"html_url":"https://github.com/yukilzw/rn_web","commit_stats":null,"previous_names":[],"tags_count":0,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukilzw%2Frn_web","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukilzw%2Frn_web/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukilzw%2Frn_web/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukilzw%2Frn_web/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yukilzw","download_url":"https://codeload.github.com/yukilzw/rn_web/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248914116,"owners_count":21182359,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react-native","react-native-web","react-navigation-v5","redux","webpack4"],"created_at":"2024-10-15T13:40:40.791Z","updated_at":"2025-10-12T04:43:57.923Z","avatar_url":"https://github.com/yukilzw.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"### 什么是三端同构？\n\n对于刚接触rn开发的同学可能不是太了解，简单介绍一下。三端指的是安卓、IOS、H5。rn本身就是跨平台框架，为了让RN能在不改动代码的情况下同时兼容H5，只需要引入一些库和配置一些细节即可。\n\n本模板采用目前`react-native@0.6x`,`react-navigation@5.x`新版本，搭建ts开发环境。\n新版本相关库的API与老rn版本有些是不兼容的，请不要随意更改主版本！\n\n不管是对于刚入门RN的小白，还是熟练运用RN老版本的熟手，都能轻松上手\n\n### 搭建流程（从零起步）\n\n##### 1. 安装RN\n照着[官网](https://reactnative.dev/docs/environment-setup)配置开发环境，不熟悉客户端开发的前端可以选沙盒环境。但是建议一步步装客户端环境，毕竟现在跨平台时代。\n\n##### 2. 使用react-native来生成typescript模板\n首先新版本的RN已经不再使用`react-native-cli`，也就是说我们不需要全局安装任何包。只要你的npm版本在5.2+，可以使用npx命令（[npx介绍](http://www.ruanyifeng.com/blog/2019/02/npx.html)）即可。\n```\nnpx react-native init MyApp --template react-native-template-typescript\n```\nnpx生成rn for ts模板，安装完成后这就是一个官方可运行的RN项目了，接下来我们就来修改这个模板\n\n##### 3. 配置package.json\n为了使用新版本的`@react-navigation v5`搭建完整可用的RN开发项目、以及我们要做的三端同构 ，需要新增一些npm依赖的包。\n```\n\"dependencies\": {\n    // ...模板自带的包\n    // ...\n    \"@react-native-community/async-storage\": \"1.9.0\",  // 相当于localStorage（可选）\n    \"@react-native-community/masked-view\": \"0.1.9\",    // 路由所需要的包 (必须)\n    \"@react-navigation/stack\": \"5.2.10\",    // 路由所需要的包 (必须)\n    \"react\": \"16.11.0\",\n    \"react-dom\": \"16.11.0\",\n    \"react-native\": \"0.62.2\",\n    \"react-native-gesture-handler\": \"1.6.0\",      // 路由所需要的包，原生手势系统 (必须)\n    \"react-native-reanimated\": \"^1.8.0\",    // 路由所需要的包 (必须)\n    \"react-native-safe-area-context\": \"0.7.3\",    // 路由所需要的包 (必须)\n    \"react-native-screens\": \"^2.3.0\",    // 路由所需要的包 (必须)\n    \"react-native-webview\": \"8.0.0\",    // RN打开webView容器，原生调用 (可选)\n    \"react-redux\": \"5.0.7\",\n    \"redux\": \"4.0.0\",\n    \"redux-thunk\": \"2.3.0\"\n  },\n\"devDependencies\": {\n    // ...模板自带的包\n    // ...\n    \"@babel/plugin-transform-runtime\": \"^7.9.0\",    // babel的插件包（必须）\n    \"@babel/preset-typescript\": \"^7.9.0\",      // babel的编译ts（必须）\n    \"@react-navigation/core\": \"3.4.2\",        // rn路由在H5里运行的核心包（必须）\n    \"@react-navigation/web\": \"1.0.0-alpha.8\",  // rn路由在H5里运行的核心包（必须）\n    \"babel-loader\": \"^8.1.0\",    // webpack loader（必须）\n    \"file-loader\": \"3.0.1\",          // webpack loader（必须）\n    \"html-webpack-plugin\": \"^4.2.0\",          // webpack plugin（必须）\n    \"react-native-web\": \"0.12.2\",      // rn组件映射为WEB的dom（必须）\n    \"webpack\": \"4.42.1\",      // 打包rn项目到h5（必须）\n    \"webpack-cli\": \"3.3.2\",    // 打包rn项目到h5（必须）\n    \"webpack-dev-server\": \"3.5.1\"    // 调试rn项目到h5（必须）\n  },\n```\n该装的都装上别漏了，建议梭哈\n\n##### 4. 配置weback和文件入口\n首先新建一个`index.html`文件作为H5打包模板\n根目录下`index.ts`为RN为原生打包的入口，新建一个`index.web.ts`当做H5的打包入口，注意在webpack中添加\n```\nresolve: {\n        extensions: [\n            '.web.ts',\n            '.web.tsx',\n            '.ts',\n            '.tsx',\n            '.js',\n            '.jsx'\n        ],\n        alias: {\n            'react-native$': 'react-native-web'\n        }\n    },\n```\nextensions配置的目的是为了让我们的项目能够在`import XX from './xx'`引入文件时，能够编写两个不同后缀的文件`xx.web.ts`，`xx.ts`。一个给RN打包给原生，一个给webpack打包给H5，用来对不同平台的代码做定制化开发，这一点在路由上是极其有用的。.web.ts一定要配置在.ts前面，让webpack优先找到属于H5的文件打包。\n\n另外alias就是将`react-native`整个库的组件映射为`react-native-web`，实际上这个库的原理就是为RN每个组件写对应的dom然后传入props。因为你也可以自行拓展，有的原生组件`react-native-web`是没有的，比如一开始提到的webView。\n\n```\n{\n            test: /\\.(js|jsx|ts|tsx)$/,\n            use: [\n                {\n                    loader: 'babel-loader',\n                    options: {\n                        cacheDirectory: false,\n                        presets: [\n                            'module:metro-react-native-babel-preset',\n                            '@babel/preset-typescript'\n                        ],\n                        plugins: [\n                            '@babel/plugin-transform-runtime'\n                        ]\n                    }\n                }\n            ],\n            exclude: /node_modules/\n        }\n```\n其次配置`babel-loader`来编译ts，这里尤其要注意：\n不能使用`ts-loader`来编译！\n因为跟文件`tsconfig.json`中我们配的`jsx`识别为`react-native`，是用来编译给客户端的，但是实际上面我们将其代理成了dom元素，打包会产生冲突导致loader编译错误。解决方案为直接使用`babel-loader`进行配置编译TS，生成sourcemap\n\n##### 5. 配置React-navigation、Redux\nRedux状态管理和普通react一样，也可以用别的库，毕竟状态管理不涉及到渲染。主要说明React-navigation，这个RN的路由库，每个版本的API差异很大，V5的版本将API拆分为多个组`@react-navigation/`，有很多种路由配置选择，我这里提供一种：\n```\n// router.ts\nimport 'react-native-gesture-handler';\nimport React from 'react';\nimport { connect } from 'react-redux';\nimport AsyncStorage from '@react-native-community/async-storage';\nimport {\n    InitialState,\n    useLinking,\n    NavigationContainerRef,\n    NavigationContainer,\n    DefaultTheme,\n    DarkTheme\n} from '@react-navigation/native';\nimport {\n    createStackNavigator,\n    HeaderStyleInterpolators\n} from '@react-navigation/stack';\n\nimport routes from './config';\n\ntype RootDrawerParamList = {\n    [key: string]: any;\n};\n\nconst Stack = createStackNavigator\u003cRootDrawerParamList\u003e();\n\nconst HeaderNull = function(): React.ReactNode {\n    return null;\n};\n\nconst MyApp = function() {\n    const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';\n\n    const containerRef = React.useRef\u003cNavigationContainerRef\u003e(null);\n    const [initialState, setInitialState] = React.useState\u003c\n        InitialState | undefined\n    \u003e();\n    const [theme, setTheme] = React.useState(DefaultTheme);\n\n    return \u003cNavigationContainer\n        ref={containerRef}\n        initialState={initialState}\n        onStateChange={async (state) =\u003e {\n            try {\n                await AsyncStorage.setItem(\n                    NAVIGATION_PERSISTENCE_KEY,\n                    JSON.stringify(state)\n                );\n            } catch (e) {\n                console.log(e);\n            }\n        }}\n        theme={theme}\n    \u003e\n        \u003cStack.Navigator\u003e\n            {(Object.keys(routes) as (keyof typeof routes)[]).map(\n                (name) =\u003e (\n                    \u003cStack.Screen\n                        key={name}\n                        name={name}\n                        component={routes[name].screen}\n                        options={{\n                            header: props =\u003e HeaderNull()\n                        }}\n                    /\u003e\n                )\n            )}\n        \u003c/Stack.Navigator\u003e\n    \u003c/NavigationContainer\u003e;\n};\n\nconst mapStateToProps = (state: any) =\u003e state;\n\nexport default connect(mapStateToProps)(MyApp);\n```\n简单来说就是把路由配置导入就行了，但是这个`config`要抽离出来，因为我们在这个目录下还有一个`route.web.ts`，同样要引用这个配置，但是引用配置的库却是不同的，在H5里我们使用`@react-navigation/web`：\n```\n// router.web.ts\nimport { connect } from 'react-redux';\nimport { createSwitchNavigator } from '@react-navigation/core';\nimport { createBrowserApp } from '@react-navigation/web';\n\nimport routes from './config';\n\nconst MyNavigator = createSwitchNavigator(routes);\n\nconst MyApp = createBrowserApp(MyNavigator);\n\nconst mapStateToProps = state =\u003e state;\n\nexport default connect(mapStateToProps)(MyApp);\n```\n这样一来，我们就能保证路由的每个页面都是引用的相同的组件了\n\n##### 6. link所使用的原生组件库\n该项目最开始配置`package.json`有两个原生模块模块包，需要使用link进行关联操作，否则构建客户端报错。在`npm i`后执行：\n- `npx react-native link react-native-gesture-handler` 这个是react-navigation使用的原生的手势系统。\n- `npx react-native link @react-native-community/async-storage` 这个类似于浏览器的localStorage原生缓存。\n\n##### 7. 让模板启动起来吧~\n配置好npm启动命令\n```\n// package.json\n\"scripts\": {\n    \"android\": \"react-native run-android\",\n    \"ios\": \"react-native run-ios\",\n    \"start\": \"react-native start\",\n    \"web:dev\": \"webpack-dev-server --config webpack.config.web.ts --inline --hot --colors\",\n    \"web\": \"webpack -p --config webpack.config.web.ts\"\n  },\n```\n- 原生客户端使用`npm run android`或`npx react-native run-android`(npm v5.2+)启动\n- `npm run web:dev`启动webpack-dev-server调试，和普通移动端开发一样\n- `npm run web`打包为生产环境代码到dist-H5\n\n##### 8. 效果展示\n- Native:\n![native-0](https://user-gold-cdn.xitu.io/2020/4/18/1718c2f9ff901394?w=620\u0026h=1275\u0026f=jpeg\u0026s=134700)\n![native-1](https://user-gold-cdn.xitu.io/2020/4/18/1718c2f9ff61cef0?w=620\u0026h=1275\u0026f=jpeg\u0026s=180176)\n![native-2](https://user-gold-cdn.xitu.io/2020/4/18/1718c2f9fff1a424?w=620\u0026h=1275\u0026f=jpeg\u0026s=279633)\n\n- H5\n![H5-0](https://user-gold-cdn.xitu.io/2020/4/18/1718c2f9ff6384fd?w=514\u0026h=901\u0026f=png\u0026s=41745)\n![H5-1](https://user-gold-cdn.xitu.io/2020/4/18/1718c2fa01510391?w=1174\u0026h=839\u0026f=png\u0026s=140770)\n\n\n##### 9. 总结\n从上面的截图可以看到几点：\n1. UI样式上基本能一比一还原\n2. 路由动画不如原生(这个貌似图片看不到...)\n3. H5上首页有个红圈，这是我故意用了一个`react-native-web`不支持的组件，在H5就会标红这个区域。\n4. Native-3中有一个客户端半屏webView容器打开了第三方H5，是使用的原生RN组件，这个在H5里显然是没有的，所以遇到这种情况，需要单独对环境进行兼容处理。（我这里没有兼容，所以打包H5的时候你能看到webpack抛出的警告）\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyukilzw%2Frn_web","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyukilzw%2Frn_web","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyukilzw%2Frn_web/lists"}