第1章 1.1 uniapp 介绍与跨端原理


🎯 开场:做一个 App 到底有多难?
你有没有过这种经历——
想做一个「校园二手交易」的小 App,给同学用。结果你发现:
- 做 iOS 版要找 Mac,要学 Swift
- 做安卓版要找 Windows,要学 Java/Kotlin
- 做小程序版又要学 WXML/WXSS
- 老板说「再加个 Web 端吧」……
一个需求,三套代码,三套bug,三次维护。这就是传统开发的噩梦。
uniapp 就是来救场的。 它喊出一个口号:「写一套代码,运行在 7 个平台」——iOS、Android、Web、微信小程序、支付宝小程序、抖音小程序、鸿蒙系统。
这一章,我们不急着写代码。先搞清楚两件事:
- uniapp 到底是什么? 它是怎么做到「写一套代码,多端运行」的?
- 它背后的原理是什么? 为什么这种方式是可行的?
学完这一章,你会对跨平台开发有一个全局认知,下一章就能直接动手创建你的第一个 uniapp 项目了。
🧱 基础:跨平台的本质是什么?
1.1 什么是「跨平台」?
先打个比方。假设你是餐厅老板,想让顾客「用任何手机都能点餐」:
| 方案 | 操作方式 | 成本 |
|---|---|---|
| 方案 A | iOS 用户用 iPhone 点,安卓用户用安卓点 | 需要两套厨师团队 |
| 方案 B | 做一个网页版,所有手机浏览器都能打开 | 一套厨师团队,服务所有人 |
uniapp 就是「方案 B」的思路——写一套代码,中间翻译成各平台能懂的语言。
1.2 uniapp 的技术架构
uniapp 不是凭空创造一门语言,它站在巨人的肩膀上:
┌─────────────────────────────────────┐
│ 你的代码(Vue 语法) │ ← 你写的
├─────────────────────────────────────┤
│ uniapp 核心框架(JS 引擎) │ ← 统一处理
├──────────┬──────────┬───────────────┤
│ iOS │ Android │ Web/小程序 │ ← 各平台定制
│ SDK │ SDK │ 适配层 │
└──────────┴──────────┴───────────────┘
用人话解释:uniapp 像一个「翻译官」。你说的是 Vue 语法,翻译官负责把你说的话,翻译成 iOS 听得懂的 Swift、Android 听得懂的 Kotlin、小程序听得懂的 WXML。
1.3 为什么是 Vue?
很多跨平台框架选择 Vue,不是巧合:
Vue 的设计思路是「渐进式」——你想用多少就用多少,从简单页面到复杂应用都能 hold 住。
Python 教学博主来类比一下:
# Python 的「列表推导式」就像 Vue 的「简洁语法」
# 两者都追求:用最少的代码,表达最清晰的意思
# Python 版本
numbers = [1, 2, 3, 4, 5]
squares = [x**2 for x in numbers]
# Vue/uniapp 版本(伪代码)
# squares = numbers.map(x => x * x)
uniapp 选择 Vue,还有一个关键原因:Vue 的生态好,社区活跃,文档健全。学一套语法,能同时对接前端生态和 uniapp 生态,血赚。
1.4 两套开发工具:HBuilderX vs CLI
uniapp 给了你两种开发方式,就像学车可以学「自动挡」或「手动挡」:
| 工具 | 特点 | 适合人群 |
|---|---|---|
| HBuilderX | 图形界面,点按钮就能跑,有可视化调试 | 新手、小团队、快速开发 |
| CLI | 命令行操作,可自定义配置,灵活 | 熟悉前端的开发者、深度定制需求 |
我的建议:第一阶段用 HBuilderX,先跑起来再说。等熟悉了,再切换 CLI 不迟。
# 打个不严谨的比方,帮助你理解:
# HBuilderX = 自动挡汽车(简单好上手)
# CLI = 手动挡汽车(灵活但需要更多操作知识)
1.5 跨平台的原理:WebView vs 原生渲染
这是理解 uniapp 的关键。很多人踩坑,就是没搞明白这个。
uniapp 有两种渲染模式:
| 渲染模式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| WebView | 用浏览器内核渲染页面 | 开发快,兼容性好 | 性能稍弱,动效复杂的场景卡 |
| 原生渲染 | 直接调用系统原生组件 | 性能强,体验好 | 需要针对平台做适配 |
用人话解释:
-
WebView 模式:像在餐厅里用「平板点餐」——餐厅给你一个公版平板,你只能在这个平板的规则里操作。流畅度取决于平板本身,不取决于餐厅厨房。
-
原生渲染模式:像直接坐在餐桌前跟服务员点菜——服务员是「原生」的,你的体验更直接,但服务员需要懂你的语言。
uniapp 默认使用 WebView 渲染,但如果你的设备支持(如 App 端),可以开启「原生渲染」获得更好性能。
1.6 uniapp 能做什么?不能做什么?
能做:
- 简单页面展示类 App(新闻、博客、电商列表)
- 工具类应用(计算器、倒计时、备忘录)
- 小程序生态(微信、支付宝、抖音小程序)
- 企业内部管理系统
不太能做:
- 大型游戏(性能要求高的)
- 需要深度系统权限的功能
- 极致性能的图像/视频处理
说白了:uniapp 解决的是「大多数普通 App」的需求,不是所有人。选技术栈之前,先问自己:「我的 App 有没有极致的性能要求?」如果没有,uniapp 完全够用。
🔥 实战:理解跨平台的「翻译」过程
下面我们用 Python 代码来模拟 uniapp 的跨平台编译过程,帮助你理解它到底在干什么。
项目 1:模拟 uniapp 的「代码翻译」机制
# uniapp_compiler.py
# 模拟 uniapp 如何把一套代码「翻译」成各平台
# 第一步:定义你的「通用代码」——就像你用 Vue 写的组件
universal_code = {
"template": "<view class='container'><text>Hello uniapp</text></view>",
"style": ".container { padding: 20px; }",
"script": "export default { data() { return { msg: 'Hello' } } }"
}
# 第二步:模拟「编译」过程——翻译成各平台
def compile_for_platform(code, platform):
"""把通用代码编译成指定平台的代码"""
if platform == "ios":
# iOS 使用 SwiftUI 语法(简化版模拟)
return f"""
import SwiftUI
struct ContentView: View {{
var body: some View {{
Text("{code['script'].split("'")[1]}").padding()
}}
}}
"""
elif platform == "android":
# Android 使用 Jetpack Compose 语法(简化版模拟)
return f"""
@Composable
fun MainScreen() {{
Text(text = "{code['script'].split("'")[1]}", modifier = Modifier.padding(20.dp))
}}
"""
elif platform == "web":
# Web 端输出 Vue 组件
return f"""
<template>
<div class="container" style="padding: 20px;">
{{{{ message }}}}
</div>
</template>
<script>
export default {{
data() {{
return {{ message: '{code['script'].split("'")[1]}' }}
}}
}}
</script>
"""
else:
return "Unsupported platform"
# 编译并输出
for platform in ["ios", "android", "web"]:
print(f"=== {platform.upper()} 平台编译结果 ===")
print(compile_for_platform(universal_code, platform))
print()
预期输出:
=== IOS 平台编译结果 ===
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello").padding()
}
}
=== ANDROID 平台编译结果 ===
@Composable
fun MainScreen() {
Text(text = "Hello", modifier = Modifier.padding(20.dp))
}
=== WEB 平台编译结果 ===
<template>
<div class="container" style="padding: 20px;">
{{ message }}
</div>
</template>
<script>
export default {
data() {
return { message: 'Hello' }
}
}
</script>
一句话解释:这个脚本演示了 uniapp 的核心思路——「写一次,翻译成多份」。你写的 Vue 代码,经过编译器的「翻译」,变成了 iOS 能懂的 Swift、Android 能懂的 Kotlin、Web 能懂的 HTML/CSS/JS。
项目 2:模拟数据在不同平台间流通
跨平台不仅是「界面翻译」,还有「数据互通」。我们用 Python 模拟一个场景:
# data_sync.py
# 模拟 uniapp 如何处理多端数据同步
import json
from datetime import datetime
# 场景:做一个「校园二手市场」App,卖家发布商品,所有平台都能看到
# 商品数据结构(你定义一次,各端通用)
product = {
"id": "P001",
"title": "二手教材《Python 入门》",
"price": 25.0,
"seller": "计算机学院 小明",
"contact": "138xxxx1234",
"images": ["img1.jpg", "img2.jpg"],
"posted_at": "2024-03-15 10:30:00",
"status": "available" # available / sold / reserved
}
# 模拟「平台适配层」——把通用数据格式转换成各平台能用的格式
class PlatformAdapter:
"""平台适配器:把统一数据格式转换成各平台专用格式"""
@staticmethod
def for_ios(product):
"""iOS 端格式:Swift 字典风格"""
return f"""
let product = [
"id": "{product['id']}",
"title": "{product['title']}",
"price": {product['price']},
"seller": "{product['seller']}",
"contact": "{product['contact']}",
"status": "{product['status']}"
]
"""
@staticmethod
def for_android(product):
"""Android 端格式:Kotlin Map 风格"""
return f"""
val product = mapOf(
"id" to "{product['id']}",
"title" to "{product['title']}",
"price" to {product['price']},
"seller" to "{product['seller']}",
"contact" to "{product['contact']}",
"status" to "{product['status']}"
)
"""
@staticmethod
def for_web(product):
"""Web 端格式:JSON"""
web_product = product.copy()
web_product["posted_at"] = product["posted_at"]
return json.dumps(web_product, indent=2, ensure_ascii=False)
# 测试:同一个商品,三种输出
print("=== iOS 端数据格式 ===")
print(PlatformAdapter.for_ios(product))
print("=== Android 端数据格式 ===")
print(PlatformAdapter.for_android(product))
print("=== Web 端数据格式 ===")
print(PlatformAdapter.for_web(product))
# 模拟「数据校验」——各平台上传数据前先校验格式
def validate_product(product):
"""校验商品数据格式(统一校验逻辑)"""
errors = []
if not product.get("title"):
errors.append("商品标题不能为空")
if not product.get("price") or product["price"] < 0:
errors.append("商品价格必须大于 0")
if not product.get("seller"):
errors.append("卖家信息不能为空")
if product.get("status") not in ["available", "sold", "reserved"]:
errors.append("商品状态必须是 available/sold/reserved")
return errors
# 测试校验
test_product = {"title": "", "price": -10, "status": "invalid_status"}
errors = validate_product(test_product)
print("=== 数据校验测试 ===")
print(f"错误数量: {len(errors)}")
for error in errors:
print(f" - {error}")
预期输出:
=== iOS 端数据格式 ===
let product = [
"id": "P001",
"title": "二手教材《Python 入门》",
"price": 25.0,
"seller": "计算机学院 小明",
"contact": "138xxxx1234",
"status": "available"
]
=== Android 端数据格式 ===
val product = mapOf(
"id" to "P001",
"title" to "二手教材《Python 入门》",
"price" to 25.0,
"seller" to "计算机学院 小明",
"contact" to "138xxxx1234",
"status" to "available"
)
=== Web 端数据格式 ===
{
"id": "P001",
"title": "二手教材《Python 入门》",
"price": 25.0,
"seller": "计算机学院 小明",
"contact": "138xxxx1234",
"posted_at": "2024-03-15 10:30:00",
"status": "available"
}
=== 数据校验测试 ===
错误数量: 3
- 商品标题不能为空
- 商品价格必须大于 0
- 商品状态必须是 available/sold/reserved
一句话解释:uniapp 的「数据层」是统一的,不管用户用 iPhone 还是安卓,数据结构只有一份。你只写一次校验逻辑,所有平台都生效——这就是「写一套代码」的真实含义。
项目 3:组合实战——做一个跨平台配置中心
# config_center.py
# 模拟 uniapp 项目中的「统一配置中心」
# 场景:你做了一个 App,需要在「开发环境」「测试环境」「生产环境」之间切换
import json
from enum import Enum
class Env(Enum):
"""环境枚举"""
DEV = "development"
TEST = "testing"
PROD = "production"
class ConfigCenter:
"""统一配置中心:管理多环境的配置"""
def __init__(self, env: Env):
self.env = env
self.config = self._load_config()
def _load_config(self):
"""根据环境加载对应配置"""
configs = {
Env.DEV: {
"api_base": "http://localhost:3000/api",
"debug": True,
"log_level": "DEBUG",
"timeout": 5000,
"enable_cache": False,
"mock_data": True # 开发环境用模拟数据
},
Env.TEST: {
"api_base": "https://test-api.yourapp.com/api",
"debug": True,
"log_level": "INFO",
"timeout": 10000,
"enable_cache": True,
"mock_data": False
},
Env.PROD: {
"api_base": "https://api.yourapp.com/api",
"debug": False,
"log_level": "WARNING",
"timeout": 15000,
"enable_cache": True,
"mock_data": False
}
}
return configs[self.env]
def get(self, key):
"""获取配置项"""
return self.config.get(key)
def generate_platform_config(self, platform):
"""生成各平台专用配置"""
base = self.config.copy()
if platform == "ios":
base["platform"] = "iOS"
base["native_module"] = "uni.UNIPaysModule"
elif platform == "android":
base["platform"] = "Android"
base["native_module"] = "io.dcloud.feature.payment.WechatPay"
elif platform == "mp-weixin":
base["platform"] = "WeChat MiniProgram"
base["native_module"] = "wx.requestPayment"
else:
base["platform"] = "Unknown"
base["native_module"] = None
return base
def __str__(self):
return f"ConfigCenter({self.env.value})"
# 使用示例
print("=" * 50)
print("场景:一键切换环境配置,生成各平台配置清单")
print("=" * 50)
# 切换到开发环境
dev_config = ConfigCenter(Env.DEV)
print(f"\n当前环境: {dev_config}")
print(f"API 地址: {dev_config.get('api_base')}")
print(f"调试模式: {dev_config.get('debug')}")
print(f"模拟数据: {dev_config.get('mock_data')}")
print("\n--- 生成各平台配置 ---")
for platform in ["ios", "android", "mp-weixin"]:
p_config = dev_config.generate_platform_config(platform)
print(f"\n【{platform}】")
print(json.dumps(p_config, indent=2, ensure_ascii=False))
# 一键切换到生产环境
print("\n" + "=" * 50)
print("切换到生产环境(运维操作,一行代码)")
print("=" * 50)
prod_config = ConfigCenter(Env.PROD)
print(f"\n当前环境: {prod_config}")
print(f"API 地址: {prod_config.get('api_base')}")
print(f"调试模式: {prod_config.get('debug')}")
print(f"模拟数据: {prod_config.get('mock_data')}")
预期输出:
==================================================
场景:一键切换环境配置,生成各平台配置清单
==================================================
当前环境: ConfigCenter(development)
API 地址: http://localhost:3000/api
调试模式: True
模拟数据: True
--- 生成各平台配置 ---
{
"api_base": "http://localhost:3000/api",
"debug": true,
"log_level": "DEBUG",
"timeout": 5000,
"enable_cache": false,
"mock_data": true,
"platform": "iOS",
"native_module": "uni.UNIPaysModule"
}
{
"api_base": "http://localhost:3000/api",
"debug": true,
"log_level": "DEBUG",
"timeout": 5000,
"enable_cache": false,
"mock_data": true,
"platform": "Android",
"native_module": "io.dcloud.feature.payment.WechatPay"
}
{
"api_base": "http://localhost:3000/api",
"debug": true,
"log_level": "DEBUG",
"timeout": 5000,
"enable_cache": false,
"mock_data": true,
"platform": "WeChat MiniProgram",
"native_module": "wx.requestPayment"
}
==================================================
切换到生产环境(运维操作,一行代码)
==================================================
当前环境: ConfigCenter(production)
API 地址: https://api.yourapp.com/api
调试模式: False
模拟数据: False
一句话解释:这个配置中心的思路,就是 uniapp「条件编译」功能的简化版。你写一套配置,根据环境变量和目标平台,自动生成对应代码——不用维护三套代码库,一个项目搞定。
💪 进阶:新手最容易踩的坑
❌ 坑 1:以为 uniapp 能解决所有性能问题
# ❌ 错误理解:用了 uniapp,性能问题就不存在了
# 真相:uniapp 只是帮你少写代码,不是不写代码
# 性能优化还是得自己做
# 举个例子:加载 10000 条数据
bad_code = """
<!-- 一次性渲染 10000 条——卡死你 -->
<template>
<view>
<view v-for="item in allItems">{{ item.name }}</view>
</view>
</template>
"""
good_code = """
<!-- 正确做法:分页 + 懒加载 -->
<template>
<uni-list>
<uni-list-item v-for="item in visibleItems" :key="item.id">
{{ item.name }}
</uni-list-item>
</uni-list>
<button @click="loadMore">加载更多</button>
</template>
"""
说白了:uniapp 是「省力工具」,不是「性能魔法」。大数据量、复杂动效,该优化的还是要优化。
❌ 坑 2:混淆「跨平台能力」和「平台差异」
# ❌ 错误写法:假设所有平台 API 行为完全一致
def upload_image(image_path):
# 微信小程序没有这个方法,会报错!
wx.chooseImage({count: 1})
# ... 上传逻辑
# ✅ 正确写法:使用条件编译或 uni API 统一封装
def upload_image(image_path):
# uni.chooseImage 是 uniapp 统一 API,各平台都支持
uni.chooseImage({
count: 1,
success: (res) => {
# 统一上传逻辑
uni.uploadFile({
url: API_BASE + '/upload',
filePath: res.tempFilePaths[0]
})
}
})
注意:uniapp 提供「统一 API」封装了平台差异,但不是所有平台特性都能封装。遇到地图、支付、推送这种平台强相关功能,要查文档确认。
❌ 坑 3:直接用 width: 750rpx 当作万能方案
# ❌ 错误理解:rpx 是万能的,任何场景都适用
# 真相:rpx 是为了适配不同屏幕尺寸,但有局限性
# ❌ 错误写法:在大屏上显示正常,小屏上超出边界
box = """
.view {
width: 700rpx; /* 小屏手机可能显示不下 */
height: 500rpx;
}
"""
# ✅ 正确做法:用 flex 布局 + max-width 限制
correct_box = """
.view {
width: 100%;
max-width: 700rpx; /* 最大宽度限制 */
height: auto;
box-sizing: border-box;
}
"""
❌ 坑 4:忽略条件编译,导致平台专属代码报错
# ❌ 错误写法:在 H5 端调用小程序专属 API
def onShareAppMessage():
# 小程序有这个功能,H5 没有!
return wx.showShareMenu() # H5 端直接报错
# ✅ 正确写法:用条件编译,只在有该功能的平台执行
correct = """
// #ifdef MP-WEIXIN
wx.showShareMenu()
// #endif
// #ifndef H5
// 这里写非 H5 平台的代码
// #endif
"""
调试技巧:uniapp 的条件编译用 // #ifdef 语法,写完代码后,用 HBuilderX 的「运行到不同平台」功能逐个测试。
❌ 坑 5:以为学会 Vue 就能精通 uniapp
# ❌ 错误理解:Vue 会了,uniapp 就会了
# 真相:uniapp 有自己的「扩展组件」和「API」,Vue 语法只是基础
# uniapp 特有概念(Vue 里没有):
# - pages.json(路由配置)
# - manifest.json(应用配置)
# - uni.xxx API(支付、推送、扫码...)
# - nvue 页面(原生渲染)
# - 条件编译(#ifdef / #ifndef)
性能小贴士:合理使用 onReachBottom 替代watch
# ❌ 低效写法:用 watch 监听分页变化
"""
watch: {
page() {
this.loadMore(); // 每次 page 变化都请求
}
}
"""
# ✅ 高效写法:用 onReachBottom 监听触底事件
"""
onReachBottom() {
if (!this.loading && this.hasMore) {
this.page++;
this.loadMore();
}
}
"""
# onReachBottom 是页面级别的事件,只有用户滑到底部才触发
# watch 是数据级别的,任何变化都会触发,可能导致不必要请求
调试技巧:用 console.info 替代 console.log
# uniapp 提供增强版日志,能显示更多信息
debug_code = """
// 普通日志(发布后会被移除)
console.log('普通日志');
// info 日志(发布后保留)
console.info('调试信息', { page: 1, data: res });
// 警告(提示潜在问题)
console.warn('这个 API 在某些平台表现不一致');
// 错误(代码有 bug)
console.error('请求失败', error);
"""
# 技巧:在关键节点打日志,用 uni.showToast 配合调试
toast_debug = """
uni.request({
url: 'xxx',
success: (res) => {
console.info('请求成功', res);
if (res.data.code !== 0) {
uni.showToast({ title: '数据异常', icon: 'none' });
}
},
fail: (err) => {
console.error('请求失败', err);
uni.showToast({ title: '网络错误', icon: 'error' });
}
});
"""
✏️ 练习题
练习 1(2 分钟):修改环境配置
# 已知以下配置对象,修改它使 api_base 指向测试环境
config = {
"api_base": "https://api.example.com",
"debug": False,
"timeout": 15000
}
- 预期输出:打印出修改后的
api_base,值应为"https://test-api.example.com" - 提示:直接赋值即可
练习 2(3 分钟):添加平台判断
# 在以下代码中添加 if 判断:当 platform 为 "ios" 时,输出 "苹果平台"
def check_platform(platform):
# 你的代码
pass
check_platform("ios") # 预期输出:"苹果平台"
check_platform("android") # 预期输出:"其他平台"
- 提示:用
if platform == "ios"判断
练习 3(5 分钟):用适配器模式转换数据
# 已有商品数据,仿照项目 2 的 PlatformAdapter,
# 新增一个 "mp-alipay"(支付宝小程序)的输出格式
product = {
"id": "P002",
"title": "二手自行车",
"price": 200.0,
"seller": "机械学院 小红"
}
# 支付宝小程序格式要求:
# - 用 my.xxx API(不是 wx.xxx)
# - 额外字段:alipay_trade_no(模拟交易号)
- 预期输出:打印出支付宝小程序格式的商品数据
- 提示:参考项目 2 的
for_wechat写法,替换wx为my
练习 4(5 分钟):组合配置与环境
# 已知 Env 枚举和 ConfigCenter 类,
# 添加一个新环境 "STAGING"(预发布环境),
# 要求:API 指向 test-api,debug=True,mock_data=False
class Env(Enum):
DEV = "development"
TEST = "testing"
PROD = "production"
# 添加 STAGING
- 预期输出:能成功创建
ConfigCenter(Env.STAGING)并获取正确配置 - 提示:在
configs字典中添加新键值对
练习 5(10 分钟):分析报错原因
# 用户运行代码后,控制台报错:
# "TypeError: Cannot read property 'title' of undefined"
# 相关代码:
data = fetch_product_from_server() # 假设这个函数返回了 undefined
print(data.title) # 第 2 行报错
# 问题:请分析原因,并写出修复代码
- 预期输出:修复后,无论
fetch_product_from_server返回什么,代码都不会崩溃 - 提示:
if data is None或data and data.title
作业:做一个「跨平台 API 调试工具」
需求描述:做一个命令行工具,模拟 uniapp 的「统一 API 调用 + 平台适配」功能。用户输入一个接口名称,工具自动生成各平台的调用代码。
功能点:
1. 支持 4 个 API:getUserInfo(获取用户信息)、login(登录)、uploadFile(上传文件)、pay(支付)
2. 输入 API 名称后,自动生成 iOS(Swift)、Android(Kotlin)、Web(JavaScript)三份代码
3. 每份代码用注释标注「uniapp 统一 API」和「平台专属实现」的区别
加分项:
1. 支持从 JSON 文件读取 API 列表
2. 支持指定输出目录,自动生成 .swift、.kt、.js 文件
验收标准:
- 运行 python api_debugger.py getUserInfo 能输出三份代码
- 代码有中文注释,解释每一步在干什么
- 能实际运行(语法正确)
提交方式:评论区贴代码或 GitHub 链接
📚 总结
这一章我们学了 3 个核心点:
- uniapp 是「翻译官」:你写 Vue,它翻译成各平台代码,实现「写一套,跑七端」
- 两种渲染模式:WebView(快但卡)、原生渲染(流畅但需要适配)
- 开发工具二选一:新手用 HBuilderX 快速上手,熟悉后用 CLI 深度定制
下一章预告:
现在你知道了 uniapp 是什么、怎么工作的。但「知道」和「做到」之间,还差一个「第一个项目」。
下一章,我们会手把手创建第一个 uniapp 项目,从安装工具到运行「Hello World」,完整走一遍流程。学完下一章,你就能写出真正可以跑在手机上的代码了。
推荐资源:
- uniapp 官方文档 —— 最好的教程,文档即教程
- Vue3 官方教程 —— uniapp 的语法基础
- 《跨平台桌面应用开发》—— 理解跨平台思想的书,有助于举一反三
互动钩子:
你有没有遇到过「做了一个 App,结果要维护三套代码」的情况?或者你现在正在用其他跨平台框架?评论区聊聊,老粉优先回复!

评论(0)