序言
项目选用技术栈时,由于前期规划需要发布多个平台问题,所以采用了taro跨端框架来开发APP,后期方便跨平台迁移,但在项目上线后一段时间后采用taro框架开发RN的问题就慢慢暴露出来,一个是升级taro版本需要连同react-native
,这风险太大,二是taro-router
耦合封装了react-navigation
这类组件,当我使用其他组件也用到时,版本之间会冲突导致有崩溃的情况,所以对项目的改造迫在眉睫。
计划
在沟通理解了基本不会发布多平台的可能性后,我们便开始考虑剥离taro框架,避免后续开发过程中技术栈和打包工具深度耦合,导致后期维护困难,而为了尽可能减少框架剥离给项目带来的不稳定性,实施分成了三个步骤:
- 运行时剥离:将代码中使用到的taro方法统一迁移到项目中,进而剥离代码和框架间的依赖;
- 编译时剥离:移除taro预编译代码,彻底将技术栈和打包工具解耦;
- 编译器剥离:彻底移除taro框架,采用
metro
方法启动react-native
,移除taro-cli
等能力
一、运行时剥离
所谓运行时剥离,就是将原本taro框架提供的方法,组件统一迁移到项目再进行改造,从而剥离运行时和框架的耦合,但这里注意,我们剥离的只是运行时的引用,对于taro预编译的代码,无法迁移的,我们先将其保留原有的引用,将放到下一个步骤去实现剥离。
我们在 src
下建立一个 tarojs
文件夹,并在文件夹下,实现项目中使用到的各类组件,例如
src
├── tarojs
├── components
├── View
├── Image
├── Text
├── taro
├── getSystemInfoSync
├── showToast
├── showLoading
而对应的 import
也进行批量修改
注:这里也能利用
alias
,将@tarojs
指向到src/tarojs
import { View } from '@tarojs/components';
=>
import { View } from '@/tarojs/components';
修改过程中也并非完全顺利的,一开始以为
View
和Text
这里组件只需要使用react-native
自身的组件就行,后面看了源码发现View
针对类型TextNode
之类的节点又额外用Text
进行包裹,否则react-native
编译不过,所以最后大部分实现都是迁移源码再稍加修改,确保源代码的稳定。
二、编译时剥离
类似taro,umi这种技术栈+编译器一体的框架,都会存在编译时预处理,例如taro框架的router
和model
这类公共方法
由于为了适配小程序语法,都会在项目编译过程中加入编译代码来实现效果,所以第二步我们将技术栈彻底和taro进行剥离,将项目中使用的生命周期和路由等耦合代码彻底剥离。
1. 比如 taro 中的 showToast
,本质上是使用 react-native-root-siblings
去实现的,而使用 react-native-root-siblings
实现
就需要向 App层 注入 RootSiblingParent
来实现,而这里就需要我们改造 App.jsx 来满足原逻辑
const App = (props) => {
const { children } = props;
//...其他代码
return children;
}
=>
const App = (props) => {
const { children } = props;
//...其他代码
return <RootSiblingParent>
{children}
</RootSiblingParent>;
}
2. 除此之外,最为复杂的便是剥离路由和生命周期这类深度绑定的代码,由于taro是源于小程序的,所以其各类写法是基于小程序的路由配置和生命周期去实现的,因此这里需要对其逐步修改;
- 移除
children
,直接使用@react-navigation
方法去实现路由功能。
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="pagesHomeIndex" component={HomeScreen} />
<Stack.Screen name="pagesLoginIndex" component={LoginScreen} />
</Stack.Navigator>
</NavigationContainer>
)
}
- 通过
createRef
获取路由实例,进而实现getCurrentPages
这类能力。
const navigationRef = createRef();
const App = () => {
return (
<NavigationContainer ref={navigationRef}>
</NavigationContainer>
)
}
// 其他地方
const pages = Taro.getCurrentPages();
const route = pages[pages.length - 1].route;
consonle.log(route); // pages/login/index
=>
const route = navigationRef.current?.getCurrentRoute();
consonle.log(route.name); // pagesLoginIndex
这里由于项目总我们使用的地方不多,所以直接是改代码的,如果项目用的地方太多,可以跟第一点那样,通过在同一方法转化去处理。
- 使用
react-native
和react-navigation
这类的方法去实现生命周期,例如useDidShow
import { useFocusEffect } from '@react-navigation/native';
import { useCallback, useEffect } from 'react';
import { AppState } from 'react-native';
export function useDidShow(callback) {
const onShow = useCallback(() => callback(), []);
useFocusEffect(onShow);
// 切换到前台时调用
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'active') {
onShow(); /
}
});
// 在组件卸载时移除监听
return () => {
subscription.remove();
};
}, []);
}
三、编译器剥离
到了这个步骤,我们基本可以将 tarojs 的相关的代码依赖移除,大致只剩下 命令行
,编译
,启动
这三个东西了
1. 这里最简单的就是命令行了,这里通过查代码和打断点,替换掉原有的 taro-cli
命令行。
注:这里我用了fastlane进行打包,所以在Fastfile实现了build命令
2. 打包处理注意的是样式处理,由于我们项目中使用的是 less
,而 react-native
是不支持的,所以这里为了项目稳定,就依然沿用 taro-css-to-react-native
插件,实现 less
和 StyleSheet
的转化
3. 最后移除 metro.config.js
中的 @tarojs/rn-supporter
,并实现样式转化等能力
最后
这就是最后跑起来的效果了~