taro框架开发微信小程序

taro v3.3.12,react v17.0.0,taro-ui v3.0.0-alpha.3,typescript v4.1.0

项目主库的版本如上,在这基础上开发了一个产品需求的微信小程序。下面是开发过程遇到的一些问题+解决办法和开发总结。

分包

首先要说的是分包。这个无论开发大项目还是小项目都应该需要(毕竟小程序一个包就2m,基本上都需要用到分包技术),它可以减小主包的体积,让小程序初始化加载更快以外,也有助于合并和分解项目功能模块。

像h5、PC的单页面项目,打包后的文件给后端丢上服务器,后端再通过docker生成镜像,nginx编写路径代理,这时的项目路由可以完全交给前端管理。但小程序没有路由,它跳转是通过目录文件名这样的方式,它会更像服务端渲染框架(如next.js)使用的约定式路由。

实现分包的配置代码比较简单,这里直接上示例:

subPackages: [
  {
      root: "subPackage",
      name: "subPackage",
      pages: [
        "pages/aaa/index", // a页面对应的目录 => src/subPackage/pages/aaa/index.tsx
        "pages/bbb/index",
        "pages/ccc/index",
      ]
  },
]

具体的分包规则可以参考微信小程序分包文档

Cookie

这里说的是发起网络请求时携带的cookie,像一般PC或h5项目前端发起登录请求,由后端返回的response.header带着Set-Cookie设置在前端,这样之后再发起请求时后端只需要对该cookie进行判断是否过期,前端无需关注。

需要注意的是,小程序在发起网络请求时默认是不带上cookie的。如果你的团队是需要在请求时带上cookie的,那可以考虑在发起登录请求后,将后端返回的cookie放入到本地缓存里。每次与后端发起请求时都带上这个cookie,同时每次发起登录请求后都刷新本地缓存里cookie的值。

Config

这里是说项目的config目录,项目配置。

由于taro是跨平台框架,所以项目初始会有mini和h5两项,我这里就是用taro来写小程序的,h5就不说了。

mini初始配置时已经提供了cssModule的配置,我们只需要将enable设置为true。

postcss: {
      pxtransform: {
        enable: true,
        config: {}
      },
      url: {
        enable: true,
        config: {
          limit: 1024 // 设定转换尺寸上限
        }
      },
      cssModules: {
        enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true
        config: {
          namingPattern: "module", // 转换模式,取值为 global/module
          generateScopedName: "[name]__[local]___[hash:base64:5]"
        }
      }
}

因为是团队项目,所以我们在开发项目前会先编写一些公用函数或公用组件,样式同样不例外。关于样式,我们团队一般会编写主题样式、通用样式,如果是使用了scss或less等css拓展语言,还会编写样式通用变量。taro允许我们配置这些样式文件在config中,以便后面编写样式时无需再引入。

// 以sass为例
sass: {
    resource: ["src/styles/variable.scss", "src/styles/theme.scss"],
    projectDirectory: path.resolve(__dirname, "..")
}

通常,我们想要引入某组件或公用函数时,自动import进来后的路径会是相对路径。如果目录文件嵌套太深时就会出现多次../,为了代码整洁因此我们会使用别名alias。

alias: {
    "@/api": path.resolve(__dirname, "..", "src/api"), // 接口函数目录
    "@/assets": path.resolve(__dirname, "..", "src/assets"), // 资源目录,如图片
    "@/components": path.resolve(__dirname, "..", "src/components"), // 公用组件目录
    "@/pages": path.resolve(__dirname, "..", "src/pages"), // 主包页面目录
    "@/utils": path.resolve(__dirname, "..", "src/utils"), // 公用函数目录
    "@/styles": path.resolve(__dirname, "..", "src/styles"), // 公用样式目录
    "@/subPackage": path.resolve(__dirname, "..", "src/subPackage") // 副包页面目录(与上述分包路径对应)
}

由于使用了typescript,所以可以搭配tsconfig.json使用:

// 在compilerOptions里添加
"baseUrl": ".",
"paths": {
    "@/*": ["./src/*"],
}

上面代码提到的分包目录subPackage,需要注意的是,在分包页面使用的资源(如图片)需要将其放置在subPackage目录内,而不能放在assets。这是因为分包导致打包后资源不在同一个包内,就会报错资源未找到。

接下来的一个问题,写过小程序的各位大大可能也遇到过:

为了解决这个问题,执行压缩和ES6转ES5处理,也为了再降低主包的体积,在mini小程序配置项中我们会使用webpackChain对一些模块代码进行拆分。我优先选取拆分的是react和taro-ui。

optimizeMainPackage: {
    enable: true
},
webpackChain(chain) {
    chain.merge({
        /**
         * optimization 源码 https://github.com/NervJS/taro/blob/bc6af68bda2cbc9163fbda36c15878fc96aec8f1/packages/taro-mini-runner/src/webpack/build.conf.ts#L220-L254
         * splitChunk cacheGroups 参考 https://github.com/NervJS/taro/issues/9573#issuecomment-880320648
         */
        optimization: {
          splitChunks: {
            cacheGroups: {
              vendors: {
                name: "vendors",
                minChunks: 2,
                test: module => {
                  return /[\\/]node_modules[\\/]/.test(module.resource);
                },
                priority: 10
              },
              vendors_taro_ui: {
                name: "vendors_taro_ui",
                test: /[\\/]node_modules[\\/]taro-ui[\\/]/,
                priority: 20, // 优先级 > vendors的10,故优先优化
                enforce: true, // 始终为此缓存组创建chunk 忽略 minSize、minChunks、maxAsyncRequests、maxInitialRequests
                reuseExistingChunk: true // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块。
              },
              vendors_react: {
                name: "vendors_react",
                test: /[\\/]node_modules[\\/]react[\\/]/,
                priority: 20,
                enforce: true,
                reuseExistingChunk: true
              }
            }
          }
        }
    });
},
// 普通编译时的默认值:['runtime', 'vendors', 'taro', 'common']
commonChunks: [
  "runtime",
  "vendors",
  "vendors_taro_ui",
  "vendors_react",
  "taro",
  "common"
],

上述的配置都是基于taro编译配置详情文档

API

taro基本上做到了将微信小程序上的api都封装了,同时也紧随着小程序的更新。

经常用到且需要注意的是,路由api switchTab与reLaunch、redirectTo、navigateTo不一样,它无法在路径后带上参数,因此这里可以考虑在使用switchTab跳转到tab页面前先将要传递的参数传入本地缓存或context或store状态管理。

当然了,如果小程序api更新了而taro还未发布新版本,而恰好你需要使用新api那咋办?由于taro编译最终的结果是小程序代码,虽然taro还未提供taro.xxxapi,但我们可以使用wx.xxxapi。正巧,我的这个taro版本就有一个还未集成好的api showShareImageMenu,这里直接上代码:

// @ts-ignore taro没有集成的api可以直接使用wx.api
wx.showShareImageMenu({ path: res.tempFilePath });

Login & getUserProfile

login接口获取code,由于微信方面的改动,我们需要使用getUserProfile接口获取用户信息userInfo,其中userInfo.encryptedData是加密过的完整的用户数据。这时后端需要我们传递code、userInfo来对用户信息进一步地解密,因此我们会执行login后getUserProfile一起对后端发起登录验证,但这是会报错的。主要原因如下:

这说明了getUserProfile需要通过按钮触发,因此我们可以使用Promise.all平级调用来解决这个问题:

Promise.all([
    Taro.login(),
    Taro.getUserProfile({ desc: "用于完善资料" })
]).then(async res => {
    try {
        const [{ code }, { encryptedData, iv }] = res;
        Taro.showLoading({ title: "登录中" });
        const userRes = await AuthAPI.decryptUserInfo(code, encryptedData, iv);
        Taro.hideLoading();
        setUserInfo(userRes);
    } catch {}
});

APP

无法注册全局组件是小程序与h5比较大的区别。

小程序没有根,它的app.js就只渲染当前页面,因此无法像vue或下面这样注册全局组件:

/** 某modal组件内 modal.tsx */
const [visible, setVisible] = useState(false);

const [config, setConfig] = useState();

const on = useCallback(() => setVisible(true), []);
const off = useCallback(() => setVisible(false), []);

useImperativeHandle(ref, () => ({
    // 暴露的组件方法 ...
    open: (data) => {
      setConfig(data);
      on;
    },
    close: off
}));



/** modalManager.ts */
export default class modalManager {
  private static modal = null;

  public static setModal(modalRef) {
    this.modal = modalRef;
  }

  public static open(data) {
    this.modal?.open(data);
  }

  public static close() {
    this.modal?.close();
  }
}



/** app.js */
<>
    {this.props.children}
    <Modal ref={r => r && modalManager.setModal(r)} />
<>

/** 某页面内调用 index.tsx */
modalManager.open({ name: '小明' });
...
modalManager.close();

目前依然没有办法实现全局组件,因此只能在每个小程序页面都引入页面需要使用的组件(原生小程序可以在app.json里使用usingComponents引入组件,之后在页面里直接调用。而taro一般不需要配置,只有在需要与引用原生小程序组件的时候才需要在页面目录下的index.config.ts配置usingComponents,但都需要调用。)。

分享

首先需要在页面目录下的index.config.ts里加上

enableShareAppMessage: true

页面内使用taro提供的hook useShareAppMessage搭配Button的openType="share"。需要注意的是,分享不能异步请求,即不能异步请求到分享信息后再拉起分享模态框,因此分享信息需要事先准备好。

但如果某场景下,你存在两种分享信息,而你无法事先得知用户选择的是哪种,这种动态的该怎么处理呢?

我们的解决办法是在进入页面第一次渲染后,发起请求获取这两种分享信息先存于前端,这里提供一个比较好用的属性data-*,我们可以将上述的分享信息作为data-*属性的值分别传给两个按钮。

<Button
    openType="share"
    data-params={shareInfo}
>
    分享
</Button>

这样设置后,当我们点击其中一个按钮进行分享操作时,就能拿到对应的分享信息了。

useShareAppMessage((res: any) => {
    if (res.from === "button") {
        // 来自页面内转发按钮
        if (res.target?.dataset?.params) {
            return {
                path: ‘’,
                imageUrl: ‘‘,
                title: ‘’
            };
        }
    }
    // 来自右上角菜单“转发”按钮
    return {...};
});

我们当然也能为右上角菜单“转发”按钮的分享行为额外设置对应的分享信息。

至于动态消息,我们暂时没有使用,因此也没去了解,没有实际的使用场景。

发布于 2022-03-15 17:53