Skip to content

如何解决 Taro 页面无法异步渲染问题

当使用 Taro 开发时,要实现拦截页面渲染做一些异步操作(例如单点登录,前置检查等)

举个自动登录场景,当访问页面链接上带有登录凭证 (ticket) 时,需要调用后端接口实现登录操作,在请求过程中展示一个 loading 样式,我们很容易想到通过入口组件(app.tsx)去拦截子元素(this.props.children)渲染实现

Taro H5 端实现

tsx
class App extends Component {
  state = {
    loading: false,
  };

  componentDidMount() {
    const ticket = getQuery('ticket');

    if (ticket) {
      this.setState({ loading: true });
      // 模拟异步过程
      setTimeout(() => {
        this.setState({ loading: false });
      }, 2000);
    }
  }

  render() {
    return this.state.loading ? <div>loading</div> : this.props.children;
  }
}

export default App;

当运行上面代码,会得到如下报错

Taro 遵守小程序设计,所以无法从入口组件拦截子元素的挂载
另一种方式是通过高阶组件去包裹每个页面实现

tsx
function hoc(Component) {
  return (props) => {
    const [loading, setLoading] = useState(false);

    useEffect(() => {
      const ticket = getQuery('ticket');

      if (ticket) {
        setLoading(true);
        setTimeout(() => {
          setLoading(false);
        }, 2000);
      }
    }, []);

    return loading ? <div>loading</div> : <Component {...props} />;
  };
}

export default hoc;

// 包裹页面组件
export default hoc(Home);

但是这样带来一个问题,通过约定无法保证每个人新建页面时都会使用这个 hoc
既然运行时我们无法通过入口组件拦截整个页面,那是不是可以通过编译时解决

通过 Babel 编译时转换页面导出

可以实现一个 babel 插件,获取页面路径,然后匹配到页面组件,转换导出内容,包裹上自定义的 hoc 函数

主要实现流程

step1:获取页面路径

Taro 的页面路径配置在 app.config.ts 文件的 pages 字段,可以通过 babel 解析拿到这个值

tsx
const srcPath = path.join(process.cwd(), './src');
const filenames = fs.readdirSync(srcPath);
// 查找到app配置文件路径
const appConfigPath = `${srcPath}/${filenames.find((m) => m.includes('app.config.'))}`;
let pages: string[] = [];

if (fs.existsSync(appConfigPath)) {
  const code = fs.readFileSync(appConfigPath).toString();
  // 读取文件,转换成 ast
  const ast = parse(code, {
    sourceType: 'module',
    plugins: ['typescript'],
  });
  traverse(ast, {
    // 查找数组元素
    ArrayExpression(path) {
      if (
        t.isObjectProperty(path.parent) &&
        t.isIdentifier(path.parent.key) &&
        // 判断属性名为 pages
        path.parent.key.name === 'pages'
      ) {
        // 获取 pages 的值
        pages = pages.concat(path.node.elements.map((m: any) => m.value));
      }
    },
  });
}

这样我们就可以拿到 Taro 所有的页面路径

step2:遍历默认导出找到页面组件

tsx
{
  ExportDefaultDeclaration(path, state) {
    // 当前代码的文件名
    const filename = state.filename;
    // 通过排除config文件及匹配上一步中获取的路径找到页面组件
    const isPage = filename.includes('.config.')
      ? false
      : pages.some((m) => state.filename.includes(m));
    }
  },
}

step3:转换

tsx
{
  ExportDefaultDeclaration(path, state) {
    const hocSource='src/component/hoc'
    const hocName='__hoc__'

    if (!t.isClassDeclaration(path.node.declaration) && isPage) {
      // 在前面插入导入语句
      path.insertBefore(
        t.importDeclaration(
          [t.importDefaultSpecifier(t.identifier(hocName))],
          t.stringLiteral(hocSource),
        ),
      );
      // hoc 包裹导出
      path.node.declaration = t.callExpression(t.identifier(hocName), [
        path.node.declaration as any,
      ]);
    }
  },
}

// 例如页面组件 Home 转换后输出
⬇️     ⬇️     ⬇️
import __hoc__ from 'src/component/hoc'
export default __hoc__(Home)

更完整的实现:https://github.com/epeejs/babel-plugin-taro-page-hoc