前言

最近一直忙于装修和开发新产品,文章都没时间更新,快速迭代的后果就是架构没有跟上功能增长的步伐,现在隐隐有脱离掌控的感觉,我这几天也把进度放慢下来,思考一下整体的规划。

也顺便整理一下笔记,没有输出心里很不踏实🤣

好了,说回正题,为什么标题叫“前端邪修”呢?在前端高度工程化的今天,各种工具层出不穷,大家都在说别更新了,学不动了,我直接反其道而行:

  • 不用 Vite
  • 不用 Webpack
  • 不跑 dev server
  • 不搞 HMR
  • 不维护 node_modules

直接在 HTML 文件里引入 React 写界面!

这听起来很像 2025 年还在用 Dreamweaver 写网页哈哈哈🤣

本文记录一次可以称之为「前端邪修」的反工程化开发实践。

第一阶段 Runtime JSX

一开始,我用的是最原始、也最“离经叛道”的 React 用法。

<script src="react.development.js"></script>
<script src="react-dom.development.js"></script>
<script src="babel.min.js"></script>

<script type="text/babel">
  function App() {
    return <h1>Hello World</h1>;
  }
</script>

特点很明显:

  • 不需要 Node
  • 不需要 npm / pnpm
  • 打开浏览器就能写 React
  • JSX 由 babel-standalone 在浏览器里实时编译

你别说,其实这种方法写起来还挺爽的(逃

  • 心智负担极低
  • 没有工具链焦虑
  • HTML 即入口,所见即所得

但缺点也很明显:

  • 各种依赖体积巨大,动辄几MB
  • JSX runtime 编译极慢
  • 页面一复杂,加载时间肉眼可见地上升

我这个简单的app,本地加载都要半分钟的时间🤣

其实这种用法,是 React 官方早期 Demo + 教学级用法,后来被官方明确标注为:Not recommended for production

我之前也写过一篇文章介绍:在HTML中引入React和JSX

实践也证明了,这种方式只能当本地demo测试一下,根本不能作为production发布。

第二阶段

gulp + babel,我只要 JSX 编译

我选择了一个在今天看来非常“复古”的工具:gulp

这个工具虽然简单,但非常好用,我一直非常喜欢这个工具,可以在我的很多项目里看到 gulp 的身影:

用法是这样的:

把 jsx 组件按照顺序,显示添加到待处理的列表里

const jsxComponents = [
  "Alert.jsx",
  "Toast.jsx",
  "Login.jsx",
  "Register.jsx",
  "ChatView.jsx",
  "DetailView.jsx",
  "App.jsx"
];

我放弃了自动依赖分析,这在现代前端里几乎是“原罪”。😂

但在一个边界清晰、规模可控的项目里,它反而是最可控的方式。

gulp 在这里做了简单的几个事情:

  • JSX → JS
  • 多文件 → 一个 bundle
  • 压缩
gulp.task("concat:jsx", () => {
    return gulp.src(jsxComponents, { base: "." })
        .pipe(babel({
        presets: ["@babel/preset-env", "@babel/preset-react"]
    }))
        .pipe(concat("dist/js/app.bundle.js"))
        .pipe(terser()) // Use terser for minification
        .pipe(gulp.dest(paths.root));
});

没有 loader,没有 plugin 地狱,没有配置互相打架。

最终产物只有一个:dist/js/app.bundle.js

最终形态

构建完成后,整个应用的入口是一个极其朴素的 HTML 文件。

<script src="lib/react/react.production.min.js"></script>
<script src="lib/react-dom/react-dom.production.min.js"></script>
<script src="lib/axios/axios.min.js"></script>
<script src="dist/js/app.bundle.js"></script>

几个我非常喜欢的点:

  • 所有依赖都是显式的

  • React 不需要 JSX 才能运行

  • 浏览器缓存是我自己控制的

    <script>
        (function () {
            const scripts = [
                "dist/js/app.bundle.js",
            ];
            const v = new Date().getTime();
            scripts.forEach(src => {
                document.write(`<script src="${src}?v=${v}"><\/script>`);
            });
        })();
    </script>
    

为什么?

这么做的起因其实很简单,我的后端需要一个简单的交互页面,而我既不想创建一个新的 Next.js/Vite 前端项目来做,也不想用后端模板渲染+HTMX/Alpine.js 这种方案。

  • 前者的问题是成本

    为了一个功能边界非常清晰的页面,引入一整套前端工程体系,意味着要额外维护一份项目结构、一套构建配置、以及一整条工具链生命周期。这种成本,对这个需求来说是过度的

  • 后者的问题则在表达能力

    后端模板渲染配合 HTMX 或 Alpine.js 确实轻量,但当页面开始出现较复杂的状态流转、组件复用和逻辑组合时,我很快就会开始“手动模拟一个组件系统”。与其在模板语法和指令里绕来绕去,不如直接使用一个我已经非常熟悉、心智模型也足够稳定的组件库(React生态)。

于是问题就变成了:

我能不能只使用 React 的“表达能力”,而不引入它背后的整套工程化体系?

这正是这套“前端邪修”方案的出发点。

价值

这套“前端邪修”方案解决了什么?

用一句话总结:如何在不引入额外工程复杂度的前提下,获得一个足够舒适的交互层。

具体来说,这个方案做到了几件事:

  • 不需要单独维护一个前端项目
  • 不需要 dev server、HMR 和复杂的构建配置
  • 不需要为一个小页面承担整套前端工程的长期成本
  • 但依然可以:
    • 使用组件化思维
    • 管理清晰的状态和交互逻辑
    • 写出结构清楚、可维护的 UI 代码

这不是在追求最轻量,而是在追求:复杂度与需求规模之间的匹配。

当需求足够简单时,工程化本身就应该是可以被拆解、被克制、甚至被拒绝的。

取舍

👍 我得到的

  • 极低的心智负担
  • 极稳定的构建过程
  • 无运行时构建依赖
  • 非常适合:
    • Admin 系统
    • AI 控制台
    • 内部工具
    • WebView / 嵌入页面

❌ 我主动放弃的

  • HMR
  • 自动 code splitting
  • TypeScript 全链路
  • 生态插件红利

小结

我做的事情其实很简单:把复杂度一层一层拆掉,直到只剩下业务真正需要的那一层。

我不推荐所有人这样做,但我会继续做一些反工程化的尝试,在AI时代反其道而行之,还挺有意思的😄