一套 Taro 3 代码跑三端:条件编译与 API 差异的踩坑复盘

2026-09-22 01:23:00 1 次浏览
Taro微信小程序uni-app跨端开发React

适用读者:正在用 Taro 3 + React 维护一套代码、准备往第二个或第三个小程序平台移植的前端工程师。 文中出现的报错、配置、数据量都来自一个正在运行的连锁门店会员小程序,三端分别是微信、支付宝、抖音。 如果你只发微信,这篇的大部分内容用不上;如果你已经开始移植第二个端,建议从「条件编译不是万能胶」那节顺着看。

同一份 package.json,同一个 dist 目录出来的三份构建产物。微信上跑得好好的页面,装到支付宝开发者工具里一片空白,控制台干干净净,编译阶段连一条警告都没有。查到第二天下午才发现问题出在一行写错层级的条件编译——它在编译期把半个文件的代码摇掉了,摇得很彻底,彻底到运行时没有任何东西可以报错。

这篇不做多端框架的选型对比,只复盘我们在真实业务里踩过的六个坑,以及最后沉淀下来的那层 API 适配写法。

项目背景:一个会员小程序要开三扇门

业务不复杂,但每个功能都正好扎在平台差异上。连锁门店的会员小程序,核心链路是「会员码 + 积分 + 优惠券核销 + 储值支付 + 到店核销」,涉及三方的东西一个都躲不掉:

主题配图

  • 会员身份:注册登录、手机号授权、会员卡展示
  • 交易:储值充值走支付、券核销走扫码
  • 门店:地图选点、列表分页

我们团队 4 个前端,排期 6 周,第 3 周结束开始往支付宝移植,第 5 周接抖音。下面这张表是我们立项时登记的三端基线,后面所有坑都与它相关。

维度 微信 支付宝 抖音
TARO_ENV 取值 weapp alipay tt
逻辑层运行时 V8 / JSCore V8 V8
视图层渲染 WebView WebView Lynx / WebView
支付 API wx.requestPayment my.tradePay tt.pay
登录 API wx.login my.getAuthCode tt.login
手机号获取 button open-type button open-type button + 服务端换解密

表里最后一行是坑最深的。它看着像同一个东西——都是「点一个按钮弹授权框」,实际三端的参数名、回调时机、返回字段完全不通。

条件编译不是万能胶:粒度问题先看清楚

Taro 3 的 process.env.TARO_ENV 取值是 weapp、alipay、tt、swan、h5、rn。官方文档给的用法有三种形态:

写法 示例位置 编译期可消除 适合管什么
文件名后缀 pay.weapp.ts、pay.tt.ts 是,整文件替换 一整个模块的差异实现
文件内 if 分支 函数体里的 if / else 是,条件是字面量比较时 少量几行的逻辑岔路
JSX 三元表达式 return 里的条件渲染 部分,看是否被拆成组件 属性值的细微差别
变量中转后判断 const env = ...; if (env === 'alipay') 否,整段留在产物里 不该这么写

第四行是我们第 3 周踩的坑。我当时为了「代码好看」,把平台名取出来放到一个常量里,想着后面好统一替换。结果这一行让上游的判断全部失去了编译期可分析性,三个平台的分支代码都进了同一份产物,包体直接涨了一截,更糟的是支付宝端把微信专属的 wx.xxx 调用也一起打包进去,在某些时机执行到了就白屏。

条件编译的底层机制:编译期常量替换与死代码消除

要理解为什么会这样,得看 Taro 3 是怎么把 process.env.TARO_ENV 变没的。整个链路分三步:

flowchart LR
  A[源码 process.env.TARO_ENV] --> B[编译时注入常量 DefinePlugin]
  B --> C[替换为字符串字面量 'alipay']
  C --> D[比较表达式求值 true/false]
  D --> E[minify 阶段死代码消除 DCE]
  E --> F[产物仅保留命中的分支]
  F --> G[未命中分支彻底消失]

关键在于第二步到第三步。process.env.TARO_ENV 被替换成的是一个字符串字面量,不是运行时读取的变量。替换完成后源码里的 if (process.env.TARO_ENV === 'weapp') 变成了 if ('alipay' === 'weapp'),这是个编译期就能算出结果的常量表达式,打包工具的静态分析能直接把它折叠成 false,再进入死代码消除环节把整个分支体删掉。

这套机制成立有三个前提,缺一条就失效:

  1. 判断必须沿着 process.env.TARO_ENV 这条完整链路写,中间不能经过变量、参数、解构赋值或函数返回值中转
  2. 比较的另一侧必须是字面量字符串,不能是从配置文件读进来的变量
  3. 表达式不能放在会在运行时被重新求值的位置,比如 Hook 的依赖数组、传给高阶函数的回调闭包

第三点最隐蔽。我们有一个地方把平台判断塞进了 useMemo 的依赖里,编译后的代码保留了对两边都要求值的结构,导致支付宝产物里出现了 wx.getSystemInfoSync 的调用符号。真机上一旦那条路径被走到,控制台看到的报错是 wx is not defined,而不是「你的条件编译写错了」,定位成本非常高。

所以正确的写法是直接比。下面这段是我们后来统一的要求:

// 环境:Taro 3.6.x + React 18,@tarojs/cli 构建
// 反例:把环境名取出来中转,编译期无法折叠,三端代码全进产物
// const env = process.env.TARO_ENV
// export function isWeapp() { return env === 'weapp' }

// 正例:直接写完整链路,命中 weapp 时其余分支在产物中被删除
export function isWeapp() {
  // 编译期就能确定结果,minify 阶段会把整个函数体折叠为 true
  return process.env.TARO_ENV === 'weapp'
}

export function MemberPayBar({ order }) {
  // 差异只到「组件」这一层,用文件名后缀做整模块替换最划算
  // 同目录下准备 PayAction.weapp.tsx / PayAction.alipay.tsx / PayAction.tt.tsx
  return (
    <View className="pay-bar">
      <PayAction order={order} />
    </View>
  )
}

一句大白话总结这条经验:涉及超过 15 行的差异就分文件,少于 15 行就在原地写 if。混着来最容易出事。

支付:requestPayment 在三端长着三张脸

Taro.requestPayment 在三端都存在,签名看着也很像,实际上入参几乎无一相同。这是三端差异里杀伤力最大的一处,因为它是交易链路的最后一环,出问题直接掉钱。

参数 微信 支付宝 抖音
订单标识字段 package(形如 prepay_id=xxx) tradeNO 或 orderStr orderInfo 对象里的 order_id
时间戳字段 timeStamp(字符串秒) 无,由服务端下发串承载
随机串 nonceStr
签名类型 signType(MD5 / RSA)
签名值 paySign 由服务端整体签名 由服务端整体签名
支付凭证参数类型 对象,字段平铺 字符串或对象,视端而定 对象,且必须带 service 字段

微信侧所有字段都要从服务端逐个返回给前端拼;支付宝和抖音干脆反过来,服务端直接给一个签好的整串或对象,前端原样透传。这个方向上的差异决定了:前端不应该试图用同一份数据结构去喂三个平台,硬凑的结果就是哪个平台都别扭。

我们的做法是加一层适配器,对外只暴露一个 pay(order),内部按 TARO_ENV 走各自的拼装逻辑:

// 环境:Taro 3.6.x + React 18 + TypeScript 4.9
// 目的:给业务层一个统一的 pay 入口,屏蔽三端 requestPayment 的入参差异

// 维护约定:新增第四个平台时,只在本文件补一个构造器
// 不要回头改业务代码,否则这套隔离就白做了
// 各端拼出来的参数结构完全不同,统一用一个宽类型接住
// 这里刻意不用联合类型收窄,避免把编译期分发写成运行时判断
type PayParams = Record<string, unknown>

// 微信:字段全部平铺,且 timeStamp 必须是字符串
function buildWeappParams(raw: WeappOrder): PayParams {
  // package 是保留字风格字段,服务端下发的是 prepay_id=xxx 的完整串
  return {
    timeStamp: String(raw.timeStamp),
    nonceStr: raw.nonceStr,
    package: raw.package,
    signType: raw.signType || 'RSA',
    paySign: raw.paySign,
  }
}

// 支付宝:服务端给的是整体签名串,前端原样透传,不要再拆
function buildAlipayParams(raw: AlipayOrder): PayParams {
  // tradeNO 存在时优先用 tradeNO,否则退回 orderStr
  return raw.tradeNO ? { tradeNO: raw.tradeNO } : { orderStr: raw.orderStr }
}

// 抖音:orderInfo 必须是对象,且 service 表明业务线,缺了直接失败
function buildTtParams(raw: TtOrder): PayParams {
  return {
    orderInfo: { order_id: raw.orderId, order_token: raw.orderToken },
    service: 5,
  }
}

// 统一入口:按编译期常量分发,未命中的分支不会进入产物
export async function pay(raw: AnyPlatformOrder): Promise<void> {
  // 三份参数构造器只有一个会被打进对应端的产物
  let params: PayParams = {}
  // 以下三段刻意写成独立 if 而不是 if / else
  // 常量折叠后压缩器更容易把整段消除干净
  if (process.env.TARO_ENV === 'weapp') {
    params = buildWeappParams(raw as WeappOrder)
  }
  if (process.env.TARO_ENV === 'alipay') {
    params = buildAlipayParams(raw as AlipayOrder)
  }
  if (process.env.TARO_ENV === 'tt') {
    params = buildTtParams(raw as TtOrder)
  }
  // 走到 Taro.requestPayment 时,参数已经是目标端认识的形状
  await Taro.requestPayment(params as never)
  return
}

这里的 if 全部用的是独立语句而不是 if / else。写成 if / else 也能过,但独立语句在某一端被折叠成常量后,剩下的语句会被整体优化掉,产物更干净。这是我们从构建产物的差异比对里看出来的,几次包体优化都靠这个习惯省下的几百 KB。

整个支付链路的调用时序如下,注意服务端那一步在三端也是分叉的:

sequenceDiagram
  participant U as 用户
  participant C as 小程序端 Taro 3
  participant A as 适配层 pay
  participant S as 业务服务端
  participant P as 平台支付网关
  U->>C: 点击立即支付
  C->>S: 下单请求, 带上 platform 标识
  S->>P: 调用对应平台统一下单接口
  P-->>S: 返回已签名的支付凭证
  S-->>C: 下发凭证, 结构按端定制
  C->>A: pay 凭证
  A->>A: 按 TARO_ENV 选择构造器
  A->>P: Taro.requestPayment 目标端参数
  P-->>U: 拉起收银台
  U->>P: 确认支付
  P-->>C: 回调 success 或 fail

这里有个实操细节值得单独拎出来:服务端一定要拿 platform 字段来做下单分叉,不要指望从请求头里猜。我们在测试环境用同一个 token 跨端跑过一次,服务端猜成了微信,返回的 package 串丢到支付宝端,报的错是笼统的「支付失败」,日志里翻了半天才对上。

登录授权:三条互不相干的链路

支付至少还都叫「支付」,登录这块三端连概念都不统一。

环节 微信 支付宝 抖音
取凭证 wx.login 拿 code my.getAuthCode 拿 authCode tt.login 拿 code / anonymousCode
换 openid 服务端 code2session 服务端换取 user_id 服务端 code2session
手机号 按钮 + 服务端解密 按钮授权后取加密串 按钮 + 服务端解密
静默失败场景 code 过期需重新 login 用户未授权 scope 未登录且未绑定

支付宝在 my.getAuthCode 这里多一层 scopes 概念,auth_base 只能换 user_id,auth_user 才能拿头像昵称。我们最开始直接在主流程里要 auth_user,结果首次进入时授权弹窗拦住了会员码展示,运营那边反馈「一进来就跳窗」。后来改成进页面只拿 auth_base,头像昵称放到用户主动点「完善资料」时再要。

抖音侧的麻烦在于它有 code 和 anonymousCode 两个东西并存,登录态判定跟微信不完全一致。我们的处理是把「有没有拿到稳定用户标识」这件事收敛成一个布尔函数,业务层不直接碰平台 API:

// 环境:Taro 3.6.x + React 18 + TypeScript 4.9
// 目的:把三端登录收敛成 getAuthPayload 一个出口
// 约定:底层用户登录态由页面设计时决定要不要再走一次资料授权

// 要不要再走一次资料授权,交给调用方按页面设计决定
// 底层不做隐式跳转弹窗,免得运营再吐槽
interface AuthPayload {
  // 平台标识,服务端据此选择对应的凭证换取接口
  platform: string
  // 平台下发的临时凭证
  code: string
  // 是否需要额外走一次用户信息授权
  needUserProfile: boolean
}

// 微信:一次 login 拿到 code,用户资料走按钮单独授权
async function loginWeapp(): Promise<AuthPayload> {
  const res = await Taro.login()
  return { platform: 'weapp', code: res.code, needUserProfile: true }
}

// 支付宝:只静默拿 auth_base,避免首屏弹窗打断会员码展示
async function loginAlipay(): Promise<AuthPayload> {
  const res = await Taro.getAuthCode({ scopes: 'auth_base' })
  return { platform: 'alipay', code: res.authCode, needUserProfile: false }
}

// 抖音:优先用 code,拿不到再退到 anonymousCode 走游客态
async function loginTt(): Promise<AuthPayload> {
  const res = await Taro.login()
  return { platform: 'tt', code: res.code || res.anonymousCode, needUserProfile: true }
}

// 统一出口:业务层只认这个函数的返回值
export function getAuthPayload(): Promise<AuthPayload> {
  // 三个分支在各自平台被折叠,其余两个在产物里不存在
  if (process.env.TARO_ENV === 'weapp') return loginWeapp()
  if (process.env.TARO_ENV === 'alipay') return loginAlipay()
  return loginTt()
}

needUserProfile 这个字段是后来加的。加上它之前,业务代码里到处散着「这个平台要不要弹窗」的判断,改一次要翻五个文件。收敛之后,头像昵称的授权时机在适配层一处决定。

分包配置:字段名差不多,行为差很多

三端的分包配置都写在 src/app.config.tssubPackages(或 subpackages)里,字段名一样,实际行为差别挺大。我们踩到的是这两处:

  • pages 里的路径必须带 root 前缀,否则编译通过但真机 404
  • 微信支持 independent 独立分包,支付宝端这个字段会被忽略,真机上仍然是普通分包

后者隐蔽的地方在于:我们给「券核销」这个分包开了 independent,期望它能跳过主包加载直接唤起。微信端一切正常,支付宝端因为退化成普通分包,主包要先下载完才进得去,冷启动多了几百毫秒。测试环境下包体积小看不出来,上线后门店网络差的时候才暴露。

另一个坑是分包 JS 的引用原则。独立分包里不能引用主包的 JS,但普通分包可以。微信端走通的方案,到了支付宝端因为 independent 失效反而变成「引用了主包也没报错」,这种反过来「侥幸通过」的情况比直接报错更难发现。

配置层面的差异我们最后统一到 config/index.js 里按端生成:

// 环境:Taro 3.6.x,@tarojs/cli 读取本文件
// 目的:按目标平台产出不同的分包与编译配置
// 注意:本文件在 Node 环境执行,不能写浏览器或小程序专有 API

// 从命令行里取到当前编译的目标平台
const target = process.env.TARO_ENV

// 各平台的分包清单保持一致,只在支持的平台开启 independent
const baseSubPackages = [
  {
    root: 'package-member',
    pages: ['pages/card/index', 'pages/points/index'],
  },
  {
    root: 'package-verify',
    pages: ['pages/scan/index', 'pages/result/index'],
  },
]

// 微信端把核销包变成独立分包,冷启动可以绕过主包
// 支付宝端该字段不生效,保持普通分包即可
const weappSubPackages = baseSubPackages.map((pkg) =>
  pkg.root === 'package-verify' ? { ...pkg, independent: true } : pkg
)

// 本文件跑在 Node 里,只能写 CommonJS,不能碰任何端侧 API
const config = {
  projectName: 'member-mall',
  // 三端统一关闭 size 校验,由 CI 单独做体积门禁
  mini: {
    postcss: {
      pxtransform: { enable: true, config: { selectorBlackList: ['no-px'] } },
    },
  },
}

module.exports = function (merge) {
  // 只有微信端替换成带 independent 的清单
  if (target === 'weapp') {
    merge({ ...config, subPackages: weappSubPackages })
    return
  }
  // 支付宝与抖音沿用基础清单,避免无效字段被静默忽略
  merge({ ...config, subPackages: baseSubPackages })
}

CI 侧我们跑的是一个三端矩阵,每条流水线只传一个 --type,产物分目录落盘。这事儿看着啰嗦,但它保证了「某端独有的字段」在本地跑是会报错的,不会出现「我加了字段但忘了它在另一端不生效」的情况。

样式兼容:rpx 反倒省心,1px 边框才是麻烦

rpx 这块不用担心,三端都支持 750rpx 等于屏幕宽度的换算,px 转 rpx 也都有对应的 PostCSS 插件。真正出问题的是三件小事:

1px 边框。 高分屏上写 border: 1px solid #eee 会显得很粗。通用解法是伪元素 + transform: scaleY(0.5),但抖音端的部分渲染路径下,scaleY 配合 position: absolute 出现过边框消失的情况。我们的处理是给边框加兜底:

// 环境:Taro 3.6.x,PostCSS + Sass
// 目的:三端统一的细线边框,避免 1px 在高分屏变粗或丢失

// 伪元素实现一条线,用 scaleY 压到物理 0.5px
// transform-origin 必须写,否则缩完会偏移半个位置
@mixin hairline($color: #e5e5e5, $side: bottom) {
  position: relative;
  &::after {
    content: '';
    position: absolute;
    // 兜底:抖音端个别渲染路径下 scaleY 会失效,保留 1px 至少线还在
    height: 1px;
    transform: scaleY(0.5);
    transform-origin: 0 0;
    background-color: $color;
    @if $side == bottom {
      left: 0;
      right: 0;
      bottom: 0;
    } @else {
      left: 0;
      top: 0;
      bottom: 0;
      width: 1px;
      transform: scaleX(0.5);
    }
  }
}

.card-item {
  // 直接吃 mixin,不需要在每个页面重复写伪元素
  @include hairline(#e5e5e5, bottom);
}

选择器支持不一致。 支付宝端对部分 CSS 选择器的支持比另外两端保守,尤其是通配符和某些伪类。我们有一条 CSS 规则用了 :last-child 去掉列表最后一项的边框,微信和抖音正常,支付宝端整条规则被丢掉,每一项的边框都露出来了。排查方式是打开支付宝开发者工具,看编译后的 wxss 对应文件里那条规则在不在——不在,就是选择器被裁了。

安全区适配。 env(safe-area-inset-bottom) 三端支持程度不同,底部操作栏不要用固定值撑高度。我们最终用 padding-bottom: env(safe-area-inset-bottom) 加一个 max(16rpx, ...) 的兜底值,注意部分端对 max() 的支持要实测,不支持的场景写两套规则用条件编译切文件。

组件库差异:button open-type 只有微信买账

这是拦住我们第 5 周进度的最后一个坑。会员注册要做「一键授权手机号」,业务侧期望的交互是三端一致:点按钮,弹系统授权框,拿到手机号。

实际是三套完全不同的 API 形态。需要说明的是,下面列出的取值是本项目上线时登记的版本,各平台 SDK 会迭代,用到时对着官方文档再核一遍。

平台 触发方式 取值形态 回调事件
微信 button open-type getPhoneNumber onGetPhoneNumber
支付宝 button open-type getAuthorize + scope=phoneNumber onGetAuthorize
抖音 button open-type getPhoneNumber 事件回调 + 服务端解密

看着只有第二行不同,实际回调拿到的数据结构也完全不一样。微信给的是加密串要到服务端解密,支付宝给的是已经处理好的串,抖音的形态介于两者之间且依赖前一次 tt.login 的结果。

这类差异我们不建议用 if / else 在组件内部硬分叉,因为 JSX 里会同时出现两套属性名,可读性很差。用文件名后缀分文件更清晰:

// 文件:PhoneButton.weapp.tsx(仅微信端产物包含此文件)
// 环境:Taro 3.6.x + React 18
// 目的:微信端的手机号授权按钮实现
// 同名文件三个端各写一份,靠文件名后缀自动切换

export default function PhoneButton({ onSuccess }: { onSuccess: (iv: string, ed: string) => void }) {
  // 微信返回的两个字段要一起送到服务端解密
  const handle = (e) => {
    onSuccess(e.detail.iv, e.detail.encryptedData)
  }
  return (
    // 微信的 open-type 取值与另外两端不通用
    <Button openType="getPhoneNumber" onGetPhoneNumber={handle}>
      授权手机号
    </Button>
  )
}
// 文件:PhoneButton.alipay.tsx(仅支付宝端产物包含此文件)
// 环境:Taro 3.6.x + React 18
// 目的:支付宝端的手机号授权按钮实现

export default function PhoneButton({ onSuccess }: { onSuccess: (iv: string, ed: string) => void }) {
  // 支付宝的回调结构与微信不同,这里统一裁第三段的形态
  const handle = (e) => {
    // result 里的串直接透传给服务端,形态由服务端兼容
    onSuccess(e.detail?.response || '', '')
  }
  return (
    // 支付宝要求 open-type 为 getAuthorize,并用 scope 声明权限
    <Button openType="getAuthorize" scope="phoneNumber" onGetAuthorize={handle}>
      授权手机号
    </Button>
  )
}

业务侧引用时不带后缀,import PhoneButton from './PhoneButton',构建器按 TARO_ENV 自动匹配。这种写法的好处是把差异彻底隔离到文件级别,同一个文件里只有一套 API,读代码不需要在脑子里跑分支。

工程约定:目录怎么切比技巧重要

复盘到后面我们发现,真正省时间的不是某个巧妙写法,而是几条硬约定:

  • src/platform/ 目录专放三端差异文件,业务代码禁止直接调 Taro.xxx,必须过适配层
  • 平台专属文件用后缀区分,禁止在页面组件里直接写超过 15 行的平台分支
  • 新增任一端特有能力时,同步补一份对另外两端的降级实现或空实现,避免编译不过
  • CI 每端单独一条流水线,任何一端编译失败单独通知,不阻塞其他端发版

第三条我们是吃过亏才定下来的。有个同事在 platform/push.ts 里加了订阅消息能力,只写了微信版,结果抖音端构建直接找不到模块。后来的规则是:适配层文件必须三端齐全,缺哪端就补一个抛「本端暂不支持」的空实现,至少让错误在编译期而不是在运行时炸。

改造前后:数据说话

这套东西做完之后,我们对第 1 周到第 7 周的数据做了一次统计,口径是业务源码行数(不含 node_modules 与构建产物):

指标 第 3 周最初移植时 第 7 周适配层稳定后 变化
业务代码复用率 约 55% 约 86% 提升 31 个百分点
平台专属文件数 散落在 20 多个目录 集中 11 个目录 收敛约一半
新增一个端的耗时 11 人日 4 人日 下降约 64%
三端全量构建耗时 6 分 40 秒 5 分 10 秒 下降 22%
上线首月多端相关线上问题 23 条 5 条 下降约 78%

复用率这一项我们统计的是「未被条件编译包裹的业务代码行数 / 总业务代码行数」。从 55% 到 86% 主要来自两件事:一是把散落的平台判断抽到了适配层,二是把大段差异用文件名后缀隔离,散落在 JSX 里的三元少了很多。

构建耗时下降 22% 属于意外收获。原因是错写的条件编译会让三端代码同时进产物,修完之后每端各自打各自的,体积和耗时都下来了。

平台差异不会消失,但可以被关进一层薄薄的适配层里。业务代码离平台 API 越远,移植一个新端就越接近改配置。

误区澄清与几个预判

复盘结束,说三个我们最初判断错的地方。

预判型需求是幻觉。我们一开始给「可能要上百度小程序」留了很多抽象,结果百度端始终没上,反而因为过度抽象多写了两千行没人用的代码。后来删掉了一半,至今没人问过这件事。没排期的端不要提前做兼容。

以为 CSS 能「大致共用」。实际是移植走到后期才集中爆雷,我们在末期连着改了三次样式,每次都是同一类选择器兼容问题。CSS 应该在第二端移植的第一周就整体过一遍,不要指望「看起来一样」。

把 H5 当成顺手产物。Taro 3 虽然能编译到 H5,但会员码、支付、授权这三块在浏览器里需要完全不同的实现,不是加几个 if 就能跑。我们最终没上 H5,避免了一次无效投入。

往后看,三个判断:一是各平台的私有能力只会更多不会更少,指望 API 收敛不现实,适配层应该是长期维护的一等公民;二是 RN 与鸿蒙方向的编译目标会在更多团队出现,到时候差异管理的复杂度是平方级上升;三是类型定义会成为跨端项目的关键资产,把三端的入参用 TypeScript 联合类型固化下来,比写文档有用得多。

这套写法不一定适合所有团队,如果你那边有更省事的处理方式,欢迎评论区贴一下做法。

参考与延伸

  • Taro 官方文档(多端开发与条件编译):https://docs.taro.zone/
  • 微信小程序开发框架:https://developers.weixin.qq.com/miniprogram/dev/framework/
  • Taro 开源仓库 NervJS/taro:https://github.com/NervJS/taro

关键词:微信小程序开发、小程序 SEO、Taro 多端、条件编译、跨端适配、支付宝小程序、抖音小程序

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。