Taro多端开发踩坑记那些年遇到的奇葩问题和解决方案

Code°文瑞 框架 阅读 2,707
赞 7 收藏
二维码
手机扫码查看
反馈

组件状态管理的正确姿势

用了Taro快两年了,最头疼的就是状态管理这块儿。刚开始我也跟着网上教程走,用Redux、MobX各种复杂的东西,结果项目一上线就发现问题了——小程序端内存爆表,React Native端卡得要命。

Taro多端开发踩坑记那些年遇到的奇葩问题和解决方案

后来我重新思考了一下,其实大部分业务场景根本不需要那么重的状态管理方案。我的做法是:

  • 页面级数据用useState + useReducer
  • 跨页面共享的状态用Context + useGlobalData
  • 缓存数据统一放到localStorage,配合useStorage hooks

比如这个商品列表页,用户选择的商品需要在多个tab间保持状态:

import { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';

// 自定义hooks,封装缓存逻辑
const useSelectedItems = (key) => {
  const [selectedItems, setSelectedItems] = useState([]);

  useEffect(() => {
    try {
      const cached = Taro.getStorageSync(key);
      if (cached) {
        setSelectedItems(cached);
      }
    } catch (e) {
      console.log('读取缓存失败', e);
    }
  }, []);

  const updateSelected = (items) => {
    setSelectedItems(items);
    try {
      Taro.setStorageSync(key, items);
    } catch (e) {
      console.error('缓存设置失败', e);
    }
  };

  return [selectedItems, updateSelected];
};

// 页面组件
const ProductList = () => {
  const [selectedItems, updateSelected] = useSelectedItems('selected_products');

  const handleSelect = (productId) => {
    const newSelected = selectedItems.includes(productId)
      ? selectedItems.filter(id => id !== productId)
      : [...selectedItems, productId];
    
    updateSelected(newSelected);
  };

  return (
    <View className="product-list">
      {products.map(product => (
        <ProductItem 
          key={product.id}
          product={product}
          isSelected={selectedItems.includes(product.id)}
          onSelect={handleSelect}
        />
      ))}
    </View>
  );
};

这种写法的好处是轻量,而且天然支持Taro的跨端特性。不像Redux那种方案,光是中间件一堆,性能开销也不小。

样式兼容性,真的是个大坑

样式方面我踩过的坑太多了。最开始我按照Web那套来写CSS,结果在微信小程序里样式全乱了。Flex布局、定位、动画,每个端都有差异。

我的建议是这样的:

/* 基础样式重置 */
page {
  font-size: 28rpx;
  line-height: 1.5;
}

/* 容器布局用flex,但要小心单位 */
.container {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 20rpx;
  box-sizing: border-box; /* 小程序端必须写 */
}

.list-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20rpx;
  border-bottom: 1rpx solid #eee; /* 边框一定要用rpx */
}

/* 避免使用transform动画,RN端支持不好 */
.slide-in {
  transition: transform 0.3s ease;
  transform: translateX(0); /* 而不是用left/right */
}

/* 伪元素在某些小程序端不支持,用view替代 */
.divider::after {
  content: '';
  height: 1px;
  background: #ccc;
}

/* 改成这样 */
<div className="divider-line" style={{ height: '1px', backgroundColor: '#ccc' }} />

还有一个坑就是字体大小。我之前统一用px,结果在iPhone和Android上显示完全不一样。现在都改成rpx,配合Taro提供的单位转换工具:

// utils/unit.js
export const pxToRpx = (px) => {
  const systemInfo = Taro.getSystemInfoSync();
  const screenWidth = systemInfo.screenWidth;
  return (px / 750) * screenWidth;
};

// 在样式中使用
const styles = {
  fontSize: pxToRpx(16),
  padding: ${pxToRpx(20)}px
};

性能优化,别瞎优化

性能优化这块儿最容易走弯路。我见过有人为了优化列表渲染,硬是要上虚拟滚动,结果bug一堆。其实大多数情况下,简单的优化就够用了。

列表渲染的正确姿势:

// 错误写法 - 没有用key或者key不合适
{list.map((item, index) => (
  <View>{item.name}</View>
))}

// 错误写法 - 直接用index当key
{list.map((item, index) => (
  <View key={index}>{item.name}</View>
))}

// 正确写法 - 用唯一ID
{list.map(item => (
  <ProductCard 
    key={item.id} 
    data={item}
    onAction={handleAction}
  />
))}

// 如果列表数据频繁变动,考虑使用memo优化
const ProductCard = React.memo(({ data, onAction }) => {
  // 只有当data变化时才重新渲染
  return (
    <View className="product-card">
      <Image src={data.image} mode="aspectFill" />
      <Text>{data.name}</Text>
      <Button onClick={() => onAction(data)}>操作</Button>
    </View>
  );
});

还有就是网络请求的缓存策略。我一般会给API加一层缓存封装:

// utils/cacheApi.js
class CacheManager {
  constructor() {
    this.cache = new Map();
    this.timers = new Map();
  }

  async request(url, options = {}, cacheTime = 5 * 60 * 1000) {
    // 检查缓存
    const cached = this.cache.get(url);
    if (cached && Date.now() - cached.timestamp < cacheTime) {
      return cached.data;
    }

    try {
      const response = await fetch(url, options);
      const data = await response.json();
      
      // 存入缓存
      this.cache.set(url, {
        data,
        timestamp: Date.now()
      });

      // 设置过期定时器
      if (this.timers.has(url)) {
        clearTimeout(this.timers.get(url));
      }
      
      this.timers.set(url, setTimeout(() => {
        this.cache.delete(url);
      }, cacheTime));

      return data;
    } catch (error) {
      throw error;
    }
  }
}

const cacheManager = new CacheManager();

export const fetchWithCache = (url, options, cacheTime) => {
  return cacheManager.request(url, options, cacheTime);
};

// 在组件中使用
const useCachedData = (url, cacheTime = 5 * 60 * 1000) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchWithCache(url, undefined, cacheTime)
      .then(result => {
        setData(result);
        setLoading(false);
      })
      .catch(error => {
        console.error('请求失败:', error);
        setLoading(false);
      });
  }, [url]);

  return { data, loading };
};

这几种错误写法,别再踩坑了

最常见的错误就是生命周期混乱。很多人把Taro当纯React用,结果在RN端出问题:

// 错误写法 - 在普通函数组件中使用Taro路由
const WrongComponent = () => {
  // 这样在RN端会报错
  useEffect(() => {
    Taro.navigateTo({ url: '/pages/detail/index?id=1' });
  }, []);
  
  return <View>错误示例</View>;
};

// 正确写法 - 在事件处理器中使用
const RightComponent = () => {
  const navigateToDetail = () => {
    Taro.navigateTo({ url: '/pages/detail/index?id=1' });
  };

  return (
    <View>
      <Button onClick={navigateToDetail}>跳转详情</Button>
    </View>
  );
};

另一个大坑是环境判断。我之前经常遇到H5端正常,小程序端报错的情况:

// 错误写法 - 直接使用浏览器API
const WrongExample = () => {
  const handleClick = () => {
    window.location.href = '/other-page'; // 小程序里不存在window对象
  };
};

// 正确写法 - 用Taro提供的API
const RightExample = () => {
  const handleClick = () => {
    // 通过process.env判断当前环境
    if (process.env.TARO_ENV === 'h5') {
      // H5端特殊处理
      location.href = '/other-page';
    } else {
      // 其他端用Taro路由
      Taro.navigateTo({ url: '/pages/other/index' });
    }
  };
};

// 或者更简洁的方式
const useSafeNavigate = () => {
  return useCallback((url) => {
    if (process.env.TARO_ENV === 'h5') {
      location.href = url;
    } else {
      Taro.navigateTo({ url });
    }
  }, []);
};

实际项目中的坑

项目上线后发现的最大问题是内存泄漏。主要是因为事件监听没有及时清理,特别是在页面卸载的时候。

正确的做法是在useEffect中返回清理函数:

const MyComponent = () => {
  useEffect(() => {
    // 添加事件监听
    const handleResize = () => {
      // 处理resize逻辑
    };
    
    // 小程序端用Taro事件系统
    if (process.env.TARO_ENV === 'weapp') {
      Taro.eventCenter.on('onResize', handleResize);
    } else {
      window.addEventListener('resize', handleResize);
    }

    // 清理函数
    return () => {
      if (process.env.TARO_ENV === 'weapp') {
        Taro.eventCenter.off('onResize', handleResize);
      } else {
        window.removeEventListener('resize', handleResize);
      }
    };
  }, []);

  // 页面生命周期
  useDidShow(() => {
    console.log('页面显示');
  });

  useDidHide(() => {
    console.log('页面隐藏');
  });

  return <View>组件内容</View>;
};

还有一个头疼的问题是包体积。Taro项目的包很容易超限,特别是引入了UI库之后。我现在的做法是按需引入,同时做代码分割:

// babel.config.js
module.exports = {
  plugins: [
    [
      'import',
      {
        libraryName: '@nutui/nutui-react-taro',
        libraryDirectory: 'dist/es',
        style: true
      },
      '@nutui'
    ]
  ]
};

// 动态导入组件
const LazyComponent = lazy(() => import('./HeavyComponent'));

const Page = () => {
  return (
    <View>
      <Suspense fallback={<Loading />}>
        <LazyComponent />
      </Suspense>
    </View>
  );
};

以上是我使用Taro过程中总结的一些最佳实践,主要针对日常开发中最常见的场景。有些地方可能不是最优解,但都是经过实际项目验证的稳定方案。有更好的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论