Files
conti-docs/tech-selection/index.html
T
2026-08-26 15:57:06 +08:00

294 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0d2438">
<title>技术路线评估 | Flutter vs uni-app x</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;600;700;800&display=swap');
:root {
--ink: #1b1b1b;
--muted: #5f6368;
--paper: #f7f7f8;
--paper-deep: #e6e6e9;
--line: #d4d4d8;
--orange: #e20074;
--teal: #1f1f1f;
--lime: #e20074;
--blue: #4b4b50;
--shadow: 0 22px 55px rgba(27, 27, 27, .16);
}
* { box-sizing: border-box; }
html { background: var(--ink); }
body {
min-width: 320px;
margin: 0;
color: var(--ink);
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
background:
linear-gradient(90deg, rgba(16, 45, 67, .035) 1px, transparent 1px),
linear-gradient(rgba(16, 45, 67, .035) 1px, transparent 1px),
var(--paper);
background-size: 24px 24px;
}
button { font: inherit; }
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1220px;
min-height: 82px;
padding: 16px 28px;
margin: auto;
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 13px; font-weight: 800; letter-spacing: 0; }
.brand-mark { display: grid; width: 30px; height: 30px; place-items: center; color: white; background: var(--orange); font-family: "DM Mono", monospace; font-size: 13px; }
.meta { color: var(--muted); font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: .04em; }
.shell { width: min(1164px, calc(100% - 48px)); margin: 28px auto 0; }
.chapter-strip { display: flex; gap: 6px; margin-bottom: 18px; overflow-x: auto; scrollbar-width: thin; }
.chapter-tab {
flex: 0 0 auto;
padding: 8px 12px;
border: 1px solid transparent;
color: var(--muted);
background: transparent;
cursor: pointer;
font-size: 13px;
transition: color .2s, background .2s, border-color .2s;
}
.chapter-tab:hover, .chapter-tab[aria-selected="true"] { color: var(--ink); border-color: var(--ink); background: #fffdf8; }
.chapter-tab[aria-selected="true"] { font-weight: 700; }
.deck { position: relative; min-height: 602px; }
.page {
display: none;
min-height: 602px;
padding: 42px clamp(25px, 5vw, 64px) 36px;
border-top: 7px solid var(--teal);
background: #fffdf8;
box-shadow: var(--shadow);
animation: enter .35s ease both;
}
.page.active { display: block; }
.page.orange { border-color: var(--orange); }
.page.blue { border-color: var(--blue); }
.page.lime { border-color: var(--lime); }
@keyframes enter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.eyebrow { margin: 0 0 11px; color: var(--teal); font-family: "DM Mono", monospace; font-size: 12px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; }
.orange .eyebrow { color: #a00052; }
.blue .eyebrow { color: #4b4b50; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 920px; margin-bottom: 18px; font-size: clamp(34px, 5vw, 62px); line-height: 1.12; letter-spacing: 0; }
h2 { max-width: 760px; margin-bottom: 26px; font-size: clamp(28px, 4vw, 46px); line-height: 1.18; letter-spacing: 0; }
h3 { margin-bottom: 10px; font-size: 17px; }
.lede { max-width: 760px; color: #385266; font-size: 17px; line-height: 1.8; }
.rule { height: 1px; margin: 27px 0; background: var(--line); }
.key-decision { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(240px, .65fr); gap: 24px; align-items: stretch; }
.decision { display: flex; flex-direction: column; justify-content: space-between; padding: 26px; color: #fffdf8; background: var(--ink); }
.decision-label { color: var(--lime); font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: .08em; }
.decision strong { display: block; margin: 26px 0 12px; font-size: clamp(32px, 4.4vw, 55px); line-height: 1; }
.decision p { max-width: 500px; margin-bottom: 0; color: #d6e2e8; line-height: 1.7; }
.facts { display: grid; gap: 1px; background: var(--line); }
.fact { padding: 17px 20px; background: #ffffff; }
.fact span { display: block; color: var(--muted); font-size: 12px; }
.fact b { display: block; margin-top: 5px; font-family: "DM Mono", monospace; font-size: 20px; }
.architecture { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--line); border-left: 1px solid var(--line); }
.route { min-height: 203px; padding: 19px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.route-no { color: var(--orange); font-family: "DM Mono", monospace; font-size: 13px; }
.route h3 { margin: 16px 0 8px; }
.route p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.65; }
.route.highlight { color: #fffdf8; background: var(--teal); }
.route.highlight .route-no { color: var(--lime); }
.route.highlight p { color: #d4f0e7; }
.compare { width: 100%; border-collapse: collapse; font-size: 14px; }
.compare th, .compare td { padding: 13px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; line-height: 1.55; }
.compare th { color: var(--muted); font-size: 12px; font-weight: 500; }
.compare th:last-child, .compare td:last-child { color: var(--teal); font-weight: 700; }
.callout { margin-top: 24px; padding: 18px 21px; border-left: 4px solid var(--orange); color: #3d5263; background: #fff0e8; line-height: 1.7; }
.bridge { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.bridge-way { padding: 21px; border: 1px solid var(--line); }
.bridge-way.good { border-color: var(--teal); background: #f3f3f4; }
.bridge-way.risk { border-color: #e20074; background: #fff0f7; }
.bridge-way h3 { display: flex; align-items: center; gap: 8px; }
.tag { display: inline-block; padding: 2px 6px; color: white; background: var(--teal); font-family: "DM Mono", monospace; font-size: 11px; font-weight: 400; }
.risk .tag { background: var(--orange); }
.bridge-way p { margin-bottom: 0; color: #425b6c; font-size: 14px; line-height: 1.7; }
.capabilities { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
.capability { min-height: 84px; padding: 14px; border-top: 3px solid var(--blue); background: #f1f1f3; font-size: 14px; font-weight: 700; }
.capability:nth-child(2n) { border-color: var(--orange); background: #fff0f7; }
.capability:nth-child(3n) { border-color: var(--teal); background: #eeeeef; }
.numbers { display: grid; grid-template-columns: 1.1fr .9fr .9fr; gap: 1px; background: var(--line); }
.number { min-height: 175px; padding: 24px; background: #fffdf8; }
.number b { display: block; color: var(--teal); font-family: "DM Mono", monospace; font-size: clamp(34px, 5vw, 61px); line-height: 1; }
.number p { margin: 14px 0 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
.split-note { display: grid; grid-template-columns: .9fr 1.1fr; gap: 26px; margin-top: 24px; }
.split-note p { margin: 0; color: #3c5566; line-height: 1.75; }
.case-studies { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-top: 24px; }
.case-study { display: grid; grid-template-columns: 152px 1fr; min-height: 168px; border: 1px solid var(--line); background: #fffdf8; overflow: hidden; }
.case-study img { width: 100%; height: 100%; min-height: 168px; object-fit: cover; }
.case-copy { padding: 18px; }
.case-copy small { color: var(--teal); font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: .06em; }
.case-copy h3 { margin: 9px 0 7px; }
.case-copy p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.65; }
.page-note { max-width: 820px; margin: 0 0 22px; color: var(--muted); font-size: 15px; line-height: 1.75; }
.signal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.signal { min-height: 150px; padding: 20px; border-top: 4px solid var(--ink); background: #f1f1f3; }
.signal.emphasis { color: white; border-color: var(--orange); background: var(--ink); }
.signal small { display: block; margin-bottom: 16px; color: var(--muted); font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: .06em; }
.signal.emphasis small { color: #ffb5d9; }
.signal b { display: block; margin-bottom: 8px; font-size: 18px; }
.signal p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
.signal.emphasis p { color: #f3d8e6; }
.timeline { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--line); }
.timeline div { min-height: 145px; padding: 18px; background: #fff; }
.timeline b { display: block; margin-bottom: 12px; color: var(--orange); font-family: "DM Mono", monospace; font-size: 14px; }
.timeline p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.55; }
.decision-list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.decision-item { padding: 18px; border-left: 4px solid var(--orange); background: #fff0f7; line-height: 1.65; }
.decision-item b { display: block; margin-bottom: 5px; }
.pipeline { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 28px; }
.pipeline-step { position: relative; min-height: 155px; padding: 20px; border: 1px solid var(--line); }
.pipeline-step b { display: block; margin-bottom: 12px; font-family: "DM Mono", monospace; font-size: 12px; }
.pipeline-step p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
.pipeline-step.ok { border-top: 5px solid var(--teal); }
.pipeline-step.block { border-top: 5px solid var(--orange); background: #fff7f2; }
.cost-bars { display: grid; gap: 13px; margin: 27px 0; }
.cost-row { display: grid; grid-template-columns: 150px 1fr 160px; gap: 14px; align-items: center; font-size: 14px; }
.bar { height: 14px; background: #e1e7e4; }
.bar span { display: block; height: 100%; background: var(--teal); }
.cost-row:nth-child(2) .bar span { background: var(--blue); }
.cost-row:nth-child(3) .bar span, .cost-row:nth-child(4) .bar span { background: var(--orange); }
.cost-impact { color: var(--muted); font-size: 13px; }
.finale { display: grid; grid-template-columns: 1.1fr .9fr; gap: 32px; align-items: end; }
.reasons { padding: 0; margin: 0; list-style: none; counter-reset: reason; }
.reasons li { position: relative; min-height: 55px; padding: 12px 0 12px 49px; border-top: 1px solid var(--line); line-height: 1.6; }
.reasons li::before { position: absolute; left: 0; top: 12px; color: var(--orange); content: "0" counter(reason); counter-increment: reason; font-family: "DM Mono", monospace; font-weight: 500; }
.stamp { padding: 27px; color: #fffdf8; background: var(--ink); }
.stamp small { color: var(--lime); font-family: "DM Mono", monospace; letter-spacing: .08em; }
.stamp b { display: block; margin: 17px 0 9px; font-size: 31px; }
.stamp p { margin: 0; color: #d6e2e8; font-size: 14px; line-height: 1.7; }
.controls { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 20px; padding: 23px 0 30px; }
.pager-button { display: inline-grid; width: 43px; height: 43px; place-items: center; border: 1px solid var(--ink); color: var(--ink); background: transparent; cursor: pointer; font-size: 24px; transition: background .2s, color .2s; }
.pager-button:hover:not(:disabled) { color: #fffdf8; background: var(--ink); }
.pager-button:disabled { opacity: .25; cursor: not-allowed; }
#next { justify-self: end; }
.page-count { color: var(--muted); font-family: "DM Mono", monospace; font-size: 13px; }
.page-dots { display: flex; justify-content: center; gap: 7px; }
.dot { width: 8px; height: 8px; padding: 0; border: 0; background: #aebeba; cursor: pointer; }
.dot.active { width: 26px; background: var(--orange); }
.source { padding: 0 28px 28px; color: #c1d0d6; font-size: 12px; text-align: center; }
.source a { color: #d9ed8f; }
@media (max-width: 760px) {
.topbar { min-height: 64px; padding: 13px 18px; }
.meta { display: none; }
.shell { width: min(100% - 28px, 680px); margin-top: 14px; }
.page { min-height: 0; padding: 30px 20px; }
.key-decision, .bridge, .split-note, .finale, .case-studies, .signal-grid, .decision-list { grid-template-columns: 1fr; }
.case-study { grid-template-columns: 128px 1fr; }
.architecture { grid-template-columns: repeat(2, 1fr); }
.capabilities { grid-template-columns: repeat(2, 1fr); }
.numbers, .timeline { grid-template-columns: 1fr; }
.pipeline { grid-template-columns: repeat(2, 1fr); }
.cost-row { grid-template-columns: 92px 1fr; }
.cost-impact { grid-column: 2; }
.compare { min-width: 610px; }
.table-scroll { overflow-x: auto; }
}
@media (max-width: 420px) { .architecture, .pipeline { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<header class="topbar">
<div class="brand"><span class="brand-mark">CR</span><span>Continental Retail APP</span></div>
<span class="meta">TECHNOLOGY ROUTE REVIEW / 2026.08</span>
</header>
<main class="shell">
<div class="chapter-strip" aria-label="章节导航" role="tablist">
<button class="chapter-tab" role="tab" aria-selected="true" aria-controls="page-1" data-page="0">0.1 摘要</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-2" data-page="1">0.2 结论</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-3" data-page="2">0.3 总览</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-4" data-page="3">1.1 栈盘点</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-5" data-page="4">1.2 四路线</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-6" data-page="5">1.3 天花板</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-7" data-page="6">2.1 uni-app</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-8" data-page="7">2.2 uni-app x</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-9" data-page="8">2.3 Flutter</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-10" data-page="9">3.1 F6 集成</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-11" data-page="10">3.2 原生能力</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-12" data-page="11">3.3 工程边界</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-13" data-page="12">4.1 开放性</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-14" data-page="13">4.2 落地案例</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-15" data-page="14">4.3 AI 生态</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-16" data-page="15">5.1 CI/CD</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-17" data-page="16">5.2 打包边界</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-18" data-page="17">5.3 扩展能力</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-19" data-page="18">6.1 成本</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-20" data-page="19">6.2 公平比较</button><button class="chapter-tab" role="tab" aria-selected="false" aria-controls="page-21" data-page="20">6.3 建议</button>
</div>
<section class="deck" aria-live="polite">
<article class="page active orange" id="page-1" role="tabpanel"><p class="eyebrow">0.1 / Executive Summary</p><h1>面向五年周期的<br>独立 App 技术路线选择。</h1><p class="lede">Continental Retail APP 将整合并关停六套小程序。首版只面向 Android 与 iOS;评估重点不在“画页面快不快”,而在系统能力、第三方网页集成与长期工程主动权。</p><div class="key-decision"><section class="decision"><span class="decision-label">ASSESSMENT DECISION</span><strong>推荐 Flutter</strong><p>首版投入与 uni-app x 基本持平。真正拉开差距的是后续系统级需求、自动化交付和运行时问题的处置空间。</p></section><aside class="facts" aria-label="评估信息"><div class="fact"><span>评估对象</span><b>Flutter / uni-app / uni-app x</b></div><div class="fact"><span>首版平台</span><b>Android + iOS</b></div><div class="fact"><span>外部数据核验</span><b>2026.08.25</b></div></aside></div></article>
<article class="page" id="page-2" role="tabpanel" hidden><p class="eyebrow">0.2 / Decision Logic</p><h2>四条可核验的推荐理由。</h2><div class="signal-grid"><section class="signal emphasis"><small>01 / ARCHITECTURE</small><b>适配独立 App</b><p>项目正在关停小程序,不获得 uni-app 系最核心的多端复用收益。</p></section><section class="signal"><small>02 / NATIVE DENSITY</small><b>原生能力密集</b><p>扫码、OCR、上传、定位、离线、权限和 SDK 集成不会因更换框架而消失。</p></section><section class="signal"><small>03 / OWNERSHIP</small><b>能力上限可控</b><p>Flutter 的边界是自身工程能力;uni-app x 的关键边界仍受 Runtime、插件和打包体系影响。</p></section></div><div class="callout"><b>关键判断:</b>这不是“哪种技术更流行”的偏好选择,而是当前项目是否愿意为不使用的多端复用能力,承担工具链和运行时上的约束。</div></article>
<article class="page blue" id="page-3" role="tabpanel" hidden><p class="eyebrow">0.3 / Comparison At A Glance</p><h2>优势与代价,都应放到同一张桌子上。</h2><div class="table-scroll"><table class="compare"><thead><tr><th>维度</th><th>Flutter</th><th>uni-app x</th></tr></thead><tbody><tr><td>架构路线</td><td>自绘引擎</td><td>编译到系统控件</td></tr><tr><td>F6 网页</td><td>App 侧建立通道,F6 零改动</td><td>F6 需引入 DCloud JS 文件</td></tr><tr><td>原生能力</td><td>Pigeon 强类型生成,支持 CLI</td><td>UTS 插件,工具链绑定 HBuilderX</td></tr><tr><td>工程归属</td><td>完整自有 Android / iOS 工程</td><td>围绕闭源 Runtime 的壳工程</td></tr><tr><td>客观优势</td><td>生态、开放性与界面一致性</td><td>鸿蒙、一套代码复用、包体与内存</td></tr></tbody></table></div><p class="page-note">包体、内存与鸿蒙是 uni-app x 的真实优势,但均不在本项目首版的敏感维度。</p></article>
<article class="page" id="page-4" role="tabpanel" hidden><p class="eyebrow">1.1 / Market Landscape</p><h2>十一种候选技术栈,先完整摆上台面。</h2><div class="table-scroll"><table class="compare"><thead><tr><th>路线</th><th>代表方案</th><th>适用前提</th></tr></thead><tbody><tr><td>原生双端</td><td>Kotlin / Swift</td><td>能力上限优先,接受双端团队</td></tr><tr><td>网页套壳</td><td>uni-app、Taro、Cordova、Capacitor</td><td>小程序 / H5 为主,App 为补充</td></tr><tr><td>编译到系统控件</td><td>uni-app x、React Native、Kuikly、MAUI</td><td>翻译层与插件覆盖需求范围</td></tr><tr><td>自绘引擎</td><td>Flutter</td><td>需要跨端一致且长期演进的独立 App</td></tr><tr><td>逻辑层共享</td><td>Kotlin Multiplatform</td><td>愿意保留两端 UI 开发</td></tr></tbody></table></div><div class="callout"><b>本轮不展开的方案:</b>React Native 的核心前提是已有 React 人力;Taro、Cordova、Capacitor 的重心仍是网页路线;KMP 只共享逻辑;Kuikly 则对外生态仍处早期。</div></article>
<article class="page orange" id="page-5" role="tabpanel" hidden><p class="eyebrow">1.2 / Four Architectural Routes</p><h2>品牌不同,能力上限来自同一套底层规律。</h2><div class="architecture"><section class="route"><span class="route-no">01 / NATIVE</span><h3>原生双端</h3><p>系统给什么就能用什么;代价是两份 UI 和两组工程协作。</p></section><section class="route"><span class="route-no">02 / WEBVIEW</span><h3>网页套壳</h3><p>一套 Web 代码覆盖多端;系统能力必须借插件从网页环境打洞。</p></section><section class="route"><span class="route-no">03 / COMPILE</span><h3>编译到控件</h3><p>由语言和词典翻译到 Kotlin / Swift;词典外的能力需要自行补齐。</p></section><section class="route highlight"><span class="route-no">04 / RENDER</span><h3>自绘引擎</h3><p>Flutter 自己绘制像素,系统能力通过标准原生插件直接连接。</p></section></div><p class="page-note">Kotlin Multiplatform 是例外:它默认只共享逻辑层;配上 Compose Multiplatform 才进入 UI 跨端路线。</p></article>
<article class="page blue" id="page-6" role="tabpanel" hidden><p class="eyebrow">1.3 / Capability Ceiling</p><h2>五年后真正要问的:天花板在谁手里?</h2><div class="signal-grid"><section class="signal"><small>WEBVIEW</small><b>插件生态</b><p>没有人提供或持续维护的能力,就会成为业务路线的阻塞点。</p></section><section class="signal"><small>COMPILE TO NATIVE</small><b>词典 + 插件</b><p>既受平台映射能力影响,也受插件和 IDE 工具链制约。</p></section><section class="signal emphasis"><small>FLUTTER</small><b>自身工程能力</b><p>原生工程、渲染框架和桥接协议都在项目可掌控范围内。</p></section></div><div class="callout"><b>本项目的结论:</b>需求只会增加,能力上限归属不是技术偏好,而是五年周期的风险管理问题。</div></article>
<article class="page" id="page-7" role="tabpanel" hidden><p class="eyebrow">2.1 / uni-app</p><h2>uni-app 的价值,在小程序主导的多端复用。</h2><div class="split-note"><p><b>它是什么。</b><br>使用 Vue / JavaScript 开发,App 端运行在系统 WebView。它的目标是让小程序团队顺便发布 App 与 H5。</p><p><b>它适合什么。</b><br>小程序是主要入口、App 只是辅助触点的商城或业务工具。此时同一套页面复用有明确回报。</p></div><div class="decision-list"><div class="decision-item"><b>项目不匹配点</b>当前项目正在整合并关闭六套小程序,首版也不发布 H5。</div><div class="decision-item"><b>直接后果</b>界面本质仍是网页,重原生能力需要反复通过插件打通。</div></div></article>
<article class="page orange" id="page-8" role="tabpanel" hidden><p class="eyebrow">2.2 / uni-app x</p><h2>uni-app x 不是传统 uni-app 的简单升级。</h2><p class="page-note">它使用 UTS / UVue,编译到 Android Kotlin、iOS Swift、鸿蒙 ArkTS,并使用系统原生控件渲染。设计方向值得肯定,但其能力边界仍由映射层、插件和工具链共同决定。</p><div class="signal-grid"><section class="signal"><small>STRENGTH</small><b>UTS 桥接思路清晰</b><p>直接编译目标端语言,相比字符串式方法调用更具类型表达力。</p></section><section class="signal"><small>CONSTRAINT</small><b>旧插件不能直接迁移</b><p>传统 uni-app 原生插件不兼容,旧市场也已停止接受新增上架。</p></section><section class="signal emphasis"><small>PROJECT FIT</small><b>部分适合</b><p>可完成 App,但无法消除本项目中原生集成与自动化交付的复杂度。</p></section></div></article>
<article class="page blue" id="page-9" role="tabpanel" hidden><p class="eyebrow">2.3 / Flutter</p><h2>Flutter 的出发点,就是构建独立 App。</h2><div class="split-note"><p><b>一致性。</b><br>Dart 提前编译;界面由自带引擎逐像素绘制,不依赖 Android 与 iOS 控件的默认差异。</p><p><b>连接原生。</b><br>每项系统能力可封装为独立插件。Pigeon 从一份定义生成 Dart、Kotlin、Swift 三端强类型接口,接口失配在编译期暴露。</p></div><div class="callout"><b>架构判断:</b>本项目要做的是一个独立 App,而不是一个小程序附属品。仅从架构选型,Flutter 已满足最直接的适配条件。</div></article>
<article class="page orange" id="page-10" role="tabpanel" hidden><p class="eyebrow">3.1 / F6 Web Integration</p><h2>嵌入 F6 网页,是项目最高风险项。</h2><p class="page-note">报价开单、施工查车、结算收银和提醒模块都依赖嵌入的 F6 页面。网页需要调用扫码、拍照、上传、拨号、登录态和门店上下文等 13 项能力、15 个方法,App 也必须反向通知网页。</p><div class="bridge"><section class="bridge-way good"><h3><span class="tag">Flutter</span> 单一双向通道</h3><p>在页面加载完成后由 App 自动注入胶水代码。请求与回调共用协议,例如 <code>{ id, method, params }</code>F6 无需修改。</p></section><section class="bridge-way risk"><h3><span class="tag">uni-app x</span> 两条单向通道</h3><p>网页须引入 <code>uni.webview.js</code>;网页到 App 用 postMessageApp 到网页用 evalJS,需自行补齐协议与异常处理。</p></section></div></article>
<article class="page" id="page-11" role="tabpanel" hidden><p class="eyebrow">3.2 / Native Capability Density</p><h2>真正的工作重心,不是页面,而是系统集成。</h2><div class="capabilities"><div class="capability">扫码 / VIN OCR</div><div class="capability">相机 / 相册多选</div><div class="capability">文件上传进度</div><div class="capability">弱网离线与重传</div><div class="capability">图片压缩</div><div class="capability">定位与权限</div><div class="capability">Bugly 崩溃桥</div><div class="capability">神策 SDK</div><div class="capability">返回键拦截</div><div class="capability">WebView 文件选择</div></div><div class="callout"><b>成本含义:</b>上述能力无论选择哪条路线都需要实现、维护、双端验证。框架对 UI 的效率影响,只覆盖整体工作中较小的一部分。</div></article>
<article class="page blue" id="page-12" role="tabpanel" hidden><p class="eyebrow">3.3 / Module Boundaries</p><h2>十九个模块并行开发,边界必须能被强制执行。</h2><div class="timeline"><div><b>19 模块</b><p>近三百条需求,长期多人并行开发。</p></div><div><b>21 包</b><p>Flutter 以 Melos 组织基础设施、业务模块与原生能力层。</p></div><div><b>编译期</b><p>包依赖机制可阻止业务模块直接引用彼此内部实现。</p></div><div><b>第二年</b><p>边界约束的价值,会在重构、替换和新增能力时放大。</p></div></div><p class="page-note">uni-app x 也可以组织工程化目录,但模块边界更多依赖团队自觉和评审纪律,缺少同等强度的物理阻断。</p></article>
<article class="page" id="page-13" role="tabpanel" hidden><p class="eyebrow">4.1 / Openness & Continuity</p><h2>关键不是“是否开源”,而是开到哪一层。</h2><div class="numbers"><section class="number"><b>91,171</b><p>Flutter 主仓库公开提交数,包含渲染引擎到框架层。</p></section><section class="number"><b>31,002</b><p>Flutter Fork 数,体现公开协作与问题排查广度。</p></section><section class="number"><b>~8x</b><p>相对 uni-app 主仓库提交数与 Fork 数的量级差异。</p></section></div><div class="split-note"><p><b>Flutter。</b><br>引擎和框架均以 BSD 开源,原生工程由项目自身掌握。</p><p><b>uni-app x。</b><br>组件与 API 开源;Runtime、SDK 与 HBuilderX 为 DCloud 自有闭源能力。</p></div></article>
<article class="page orange" id="page-14" role="tabpanel" hidden><p class="eyebrow">4.2 / Production Evidence</p><h2>大型 App 的真实形态,是混合底座加跨端业务主干。</h2><div class="case-studies"><article class="case-study"><img src="assets/bmw1.png" alt="My BMW App 的车辆状态和车控界面"><div class="case-copy"><small>BMW / 甲档案例</small><h3>96 个变体,自动化交付</h3><p>My BMW App 覆盖 47 国。Mobile 2.0 Platform 每次构建可自动生成、测试并部署 96 个变体。</p></div></article><article class="case-study"><img src="assets/vw4.png" alt="ID. UNYX App 的车辆状态和远程车控界面"><div class="case-copy"><small>ID. UNYX / 乙档案例</small><h3>主干页面承载车控场景</h3><p>大众安徽法定 SDK 披露将 Flutter 列为 App 全部 L1、L2、L3 页面开发语言。</p></div></article></div><div class="callout"><b>同一形态的公开案例:</b>字节 90+ 款应用、闲鱼主链路、美团商家端、京东、携程、BMW 与 ID. UNYX 都证明:原生能力仍在原生侧,业务页面可以稳定由 Flutter 承担。</div></article>
<article class="page blue" id="page-15" role="tabpanel" hidden><p class="eyebrow">4.3 / AI Toolchain</p><h2>AI 辅助效率,来自开放生态与公开代码沉淀。</h2><div class="table-scroll"><table class="compare"><thead><tr><th>维度</th><th>Flutter / Dart</th><th>uni-app x</th></tr></thead><tbody><tr><td>官方 Skills</td><td>独立开源仓库,可 Fork / PR</td><td>主要内置于 uni-agent</td></tr><tr><td>MCP</td><td>Dart & Flutter MCP Server</td><td>uni-app-x-mcp</td></tr><tr><td>工具适配</td><td>可接入多种 AI 编程工具</td><td>以自家生态和少数工具为主</td></tr><tr><td>公开语料</td><td>近十年全球 Dart / Flutter 项目</td><td>UTS / UVue 较新,公开沉淀有限</td></tr></tbody></table></div><div class="callout"><b>实际影响:</b>当开发者需要生成、调试、重构或验证原生桥接与业务组件时,公开生态的深度会直接反映在生成质量和返工率上。</div></article>
<article class="page" id="page-16" role="tabpanel" hidden><p class="eyebrow">5.1 / Delivery Pipeline</p><h2>目标是每个版本都能从一次提交中重建。</h2><p class="lede">既定工程链路:GitLab CI/CDlint / 格式化 / 测试卡点,dev / uat / prod 三环境,远程 Mac 出 iOS 包,Android OTA 与 iOS TestFlight 分发。</p><div class="pipeline"><section class="pipeline-step ok"><b>01 / COMMIT</b><p>提交触发静态检查、格式化和测试。</p></section><section class="pipeline-step ok"><b>02 / BUILD</b><p>Android / iOS 在自有节点完成可追溯构建。</p></section><section class="pipeline-step block"><b>03 / UTS PLUGIN</b><p>UTS 插件创建和使用绑定 HBuilderX,官方不支持 CLI。</p></section><section class="pipeline-step block"><b>04 / PACKAGE</b><p>云打包、离线打包和加密插件之间存在结构性取舍。</p></section></div></article>
<article class="page orange" id="page-17" role="tabpanel" hidden><p class="eyebrow">5.2 / Packaging Boundaries</p><h2>“离线打包”给的是壳,不是完全可控的原生工程。</h2><div class="table-scroll"><table class="compare"><thead><tr><th>范围</th><th>可以修改</th><th>不能修改</th></tr></thead><tbody><tr><td>工程外层</td><td>包名、图标、权限、ABI、原生模块</td><td>运行时页面容器和生命周期</td></tr><tr><td>Android</td><td>Manifest 与 Gradle 配置</td><td>闭源 AAR 内部实现与 UTS 通信机制</td></tr><tr><td>应用入口</td><td>可在约束下定制</td><td>Application 必须继承 DCloudApplication</td></tr><tr><td>语言选择</td><td>Flutter 标准 Kotlin / Swift</td><td>传统离线 SDK 官方声明不支持 Kotlin</td></tr></tbody></table></div><div class="callout"><b>核心区别:</b>Flutter 以模块挂入自己的标准 Android / iOS 工程;uni-app x 离线 / 嵌入路径则是把自定义内容放入围绕闭源运行时构成的宿主壳中。</div></article>
<article class="page blue" id="page-18" role="tabpanel" hidden><p class="eyebrow">5.3 / Extensions</p><h2>桌面小组件、Siri 和分享扩展,最终都会回到原生。</h2><div class="signal-grid"><section class="signal"><small>WIDGET</small><b>小组件</b><p>iOS 需要 Xcode、SwiftUI 与 WidgetKit 的 .appexAndroid 同样需要 Kotlin、Manifest 和 XML。</p></section><section class="signal"><small>SIRI / INTENTS</small><b>系统语音能力</b><p>官方建议离线打包后在原生工程实现 AppShortcutsProvider,再通过 UTS 衔接业务。</p></section><section class="signal emphasis"><small>CONFLICT</small><b>加密插件与离线打包</b><p>加密 UTS 插件仅支持云端传统打包,和部分扩展所需的离线路径互斥。</p></section></div><p class="page-note">做得到不等于低成本:两端仍需写 Swift / Kotlin,并额外承受一层厂商打包与基座规则。</p></article>
<article class="page" id="page-19" role="tabpanel" hidden><p class="eyebrow">6.1 / First Release Cost</p><h2>首版成本基本持平,差异在 ±10% 内。</h2><div class="cost-bars"><div class="cost-row"><b>业务集成 4555%</b><div class="bar"><span style="width: 88%"></span></div><span class="cost-impact">框架基本无差异</span></div><div class="cost-row"><b>UI 实现 2025%</b><div class="bar"><span style="width: 40%"></span></div><span class="cost-impact">uni-app x 略省</span></div><div class="cost-row"><b>原生桥接 1015%</b><div class="bar"><span style="width: 27%"></span></div><span class="cost-impact">uni-app x 更贵</span></div><div class="cost-row"><b>工程与可观测 1015%</b><div class="bar"><span style="width: 27%"></span></div><span class="cost-impact">uni-app x 更贵</span></div></div><div class="callout"><b>成本不是决策理由。</b> UI 熟悉度带来的节省被原生桥接、插件工具链与 CI / 发布成本抵消,首版不应以工时差异作为主导依据。</div></article>
<article class="page orange" id="page-20" role="tabpanel" hidden><p class="eyebrow">6.2 / Fair Comparison</p><h2>uni-app x 的三项真实优势,应当被正面计入。</h2><div class="decision-list"><div class="decision-item"><b>鸿蒙支持更完整</b>UTS 可直接编译 ArkTS;Flutter 主线不支持鸿蒙,社区分支正在跟进。</div><div class="decision-item"><b>小程序与 H5 复用</b>若未来需要 App、小程序、H5 同时发布,价值会立即显现。</div><div class="decision-item"><b>UTS 的语言方向正确</b>直接编译 Kotlin / Swift 的桥接方式比传统字符串式调用更干净。</div><div class="decision-item"><b>包体、内存与热更新</b>uni-app x 在这些方面有实际优势,但本项目没有包体 KPI,发版也采用正规分发链路。</div></div><p class="page-note">这些优势不应被回避;它们只是没有改变本项目在 F6 集成、原生能力密度和 CI 可控性上的关键判断。</p></article>
<article class="page blue" id="page-21" role="tabpanel" hidden><p class="eyebrow">6.3 / Final Recommendation</p><h2>推荐 Flutter,把能力上限留在自己的工程里。</h2><div class="finale"><ol class="reasons"><li><b>架构适配:</b>项目是独立 App,无法获得多端小程序复用收益。</li><li><b>业务风险:</b>F6 集成和十余项原生能力需要直接、可控的通道。</li><li><b>长期治理:</b>开放生态与完整原生工程为问题处置保留主动权。</li><li><b>交付能力:</b>Flutter 匹配既有 GitLab CI/CD 与标准原生扩展方式。</li></ol><aside class="stamp" aria-label="最终技术路线建议"><small>FINAL DECISION</small><b>推荐 Flutter</b><p>若最终仍选择 uni-app x,应在开工前落实 F6 改造承诺、神策 SDK、Bugly UTS 封装、CLI/CI 路径以及云端与离线打包的取舍。</p></aside></div></article>
</section>
<footer class="controls">
<button class="pager-button" id="previous" type="button" aria-label="上一章" title="上一章">&#8592;</button>
<div><div class="page-dots" aria-label="分页"></div><div class="page-count">01 / 06</div></div>
<button class="pager-button" id="next" type="button" aria-label="下一章" title="下一章">&#8594;</button>
</footer>
</main>
<script>
const pages = Array.from(document.querySelectorAll('.page'));
const tabs = Array.from(document.querySelectorAll('.chapter-tab'));
const dotsHost = document.querySelector('.page-dots');
const count = document.querySelector('.page-count');
const previous = document.querySelector('#previous');
const next = document.querySelector('#next');
let currentPage = 0;
let lastPagerAction = 0;
pages.forEach((page, index) => {
const dot = document.createElement('button');
dot.className = 'dot';
dot.type = 'button';
dot.setAttribute('aria-label', `跳转至第 ${index + 1} 章`);
dot.addEventListener('click', () => showPage(index));
dotsHost.append(dot);
});
const dots = Array.from(document.querySelectorAll('.dot'));
function showPage(index) {
currentPage = Math.max(0, Math.min(index, pages.length - 1));
pages.forEach((page, itemIndex) => {
const active = itemIndex === currentPage;
page.classList.toggle('active', active);
page.hidden = !active;
tabs[itemIndex].setAttribute('aria-selected', String(active));
dots[itemIndex].classList.toggle('active', active);
});
count.textContent = `${String(currentPage + 1).padStart(2, '0')} / ${String(pages.length).padStart(2, '0')}`;
previous.disabled = currentPage === 0;
next.disabled = currentPage === pages.length - 1;
}
tabs.forEach((tab, index) => tab.addEventListener('click', () => showPage(index)));
function movePage(direction) {
const now = Date.now();
if (now - lastPagerAction < 250) return;
lastPagerAction = now;
showPage(currentPage + direction);
}
previous.addEventListener('click', () => movePage(-1));
next.addEventListener('click', () => movePage(1));
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft') showPage(currentPage - 1);
if (event.key === 'ArrowRight') showPage(currentPage + 1);
});
showPage(0);
</script>
</body>
</html>