
字节笔记本
2026年10月7日 · 约 9 分钟读完
框架还是原生 JS?一张小卡片背后的前端选型
写前端时有一类组件小到不值得立项:侧边栏里的导航卡片,一个标题、四个入口、图标加文字、悬停变色。可恰恰是这种小组件,最适合拿来把「用框架写」和「用原生写」两条路各走一遍——差异不在代码量,而在渲染模型、依赖策略和适用边界。本文就以一张导航卡片为样本,把两条路线摊开比较,最后给出一张按环境选型的决策清单。
这个选择从哪来的
前端技术圈对「要不要框架」的争论至少经历了两个来回。jQuery 时代,大家直接操作 DOM,框架被视作重型武器;随后 React、Vue 带着组件化和声明式渲染成为默认选项,连内部工具都用上了 SPA;近几年风向又转回来一部分——htmx、Alpine.js 这类轻量方案重新流行,不少人主张「先原生,撑不住了再上框架」。争论容易情绪化,落到具体组件上反而清楚:同一个需求,两条路各写一遍,各自的代价立刻显形。
框架版:数据驱动的标准姿势
React 版的写法是生态里的「标准答案」:导航项不写死在 JSX 里,而是收进一个数组,渲染时 map 出去。
const navItems = [
{ icon: Home, label: '主页' },
{ icon: User, label: '个人' },
{ icon: Settings, label: '设置' },
{ icon: HelpCircle, label: '帮助' },
];
<ul className="space-y-2">
{navItems.map((item, i) => (
<li key={i}>
<a href="#" className="flex items-center p-2 text-gray-700
hover:bg-gray-100 rounded-md transition-colors duration-200">
<item.icon className="w-5 h-5 mr-3" />
<span>{item.label}</span>
</a>
</li>
))}
</ul>图标用 lucide-react 这类 SVG 图标库按组件引入,样式交给 Tailwind 原子类,一行 hover:bg-gray-100 加 transition-colors 就有平滑的悬停反馈。这套写法真正的价值在后续演化:要加选中态、折叠动画,或者把菜单改成从接口拉取,改动都收敛在数据层,视图结构不动。代价则是工程链路:JSX 无法直接在浏览器里运行,哪怕只做一个卡片,也得配上打包器或转译工具;组件一多,还要跟着引入路由、状态管理等配套决策。框架的便利,是用一整套工程体系换来的。
原生版:零依赖一样能跑
同样的卡片,纯 HTML、CSS 加 JavaScript 完全够用。HTML 里只留一个空的列表容器,脚本在页面加载后遍历同一个配置数组,逐项生成节点再挂载:
const navItems = [
{ icon: '🏠', label: '主页' },
{ icon: '⚙️', label: '设置' },
];
const list = document.getElementById('navList');
navItems.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `<a href="#" class="nav-link">
<span class="nav-icon">${item.icon}</span>${item.label}</a>`;
list.appendChild(li);
});样式写在 style 标签里,:hover 伪类负责悬停变色;图标方案换成 emoji,一个字符就是一个图标,零依赖。
这版最大的优点是部署形态:没有构建工具、没有运行时,一个 HTML 文件双击浏览器就能跑。要把它塞进某个现成的后台页面、CMS 的模板块,或者给静态站补一小块交互,拷进去就能用,完全不必关心宿主环境是什么技术栈。代价同样清楚:交互逻辑散落在各个函数里,复用靠复制粘贴,规模一上去就容易失控。所以原生版的适用边界很明确——它服务的场景里,「能跑、轻量、随手可改」比「架构优雅」重要得多。

差异在三层
把两版并排放,真正的分野浮出三层。
第一层是渲染模型。 React 维护虚拟 DOM,数据变化后先在内存里做 diff,再最小化地更新真实 DOM,你只需要声明「界面应该是什么样」。举个最小的例子:给当前页加高亮,React 版改一个 state,框架自己算出该改哪个节点的 class;原生版则要自己拿到旧节点的引用、摘掉旧 class、再给新节点补上——步骤不多,但每一步都由你负责,漏一步就是界面与状态不一致的 bug。组件很小时两者体感没有差别,可一旦列表要频繁增删、排序、维护选中态,声明式模型明显省心。
第二层是图标。 SVG 图标库线条风格一致,颜色粗细可控,还能跟随主题的 currentColor;emoji 胜在零成本,但跨平台渲染不一致,Windows、macOS、Android 各有各的长相,也没有主题联动。原型阶段用 emoji 无妨,正式产品建议换回 SVG。
第三层是体积与约束。 原生版的体积约等于零;框架版哪怕组件只有几十行,也要背上 React 运行时和样式方案的体积。反过来,原生付出的是手动管理 DOM 的心智成本,以及项目变大后缺乏统一架构约束的散乱。
选型看环境,不看喜好
实际项目里,走哪条路多数时候不由代码美感决定,而由部署环境决定:
- 独立产品、交互频繁、状态会持续生长:框架用体积换状态管理和可维护性,值得;
- 嵌入第三方页面、CMS 模板、构建工具受限的环境:原生几乎是唯一解;
- 静态页上的一小块交互:原生更轻。但项目若已全面框架化,别为了省几十 KB 再引入第二套写法,一致性优先;
- 要把同一个组件分发给技术栈各异的多个页面:可以看 Web Components,用浏览器原生的自定义标签做到零框架依赖的组件化复用,代价是 API 略繁琐、生态不如主流框架顺手。
框架和原生之间还有中间地带:Alpine.js、htmx、petite-vue 这类方案保持「无构建、引一个 script 就能用」的原生部署形态,同时补上声明式绑定,适合交互比原生写起来吃力、又没到引入完整框架程度的场景。

三个容易踩的坑
一是 innerHTML 注入。 示例里拼接的是写死的配置,问题不大;但数据一旦来自 URL 参数、接口或用户输入,直接塞进 innerHTML 就是 XSS 的入口。常见的触发点往往不起眼,比如「从地址栏读个参数决定默认选中哪一项」,参数一拼进模板字符串就中招了,该用 textContent 的地方不要图省事。React 默认对插值做转义,这是框架默默帮你挡掉的一类事故。
二是列表的 key。 导航项将来若支持排序或删除,用数组下标当 key 会导致节点复用错乱、界面闪烁,尽量换稳定唯一的标识。
三是可访问性。 语义上用 nav 元素加 ul 和 li,当前页用 aria-current="page" 标记;键盘导航依赖原生的 Tab 焦点顺序,别随手把 outline 抹掉却又不补替代的焦点样式。
写在最后
导航卡片是最普通不过的组件,但「同一需求、两种技术栈」的对照,恰是前端选型的缩影:框架买的是状态管理与可维护性,原生省的是体积和依赖。先看清组件跑在什么环境里、将来会怎么变化,再决定用哪把刀——需求简单且环境受限时,几十行的原生脚本就是最好的方案;交互和状态开始生长时,框架的投入才会开始回本。还有一个新变量值得记下:AI 编程助手让两类实现的生成成本都趋近于零,随口一句「改用原生」就能拿到另一版。生成环节变便宜之后,真正的瓶颈移到了选型判断上——知道该要哪一版,比会写哪一版更值钱。



