组件构建:从 IIFE 注入到 shadcn 复刻的工程范式
本节是 ne101_camera MVP 阶段的组件构建参考页(MVP 核心三页收尾),覆盖 IIFE 注入范式、四键命名导出、五层模块结构、React hooks 在 IIFE 中的三大陷阱以及 shadcn CSS 类复刻策略。
IIFE 注入范式
ne101_camera 的 bundle.js 第一行不是 import 语句,而是一个 IIFE(立即调用函数表达式)的入口。查看源码:bundle.js L1-L5。
var NE101CameraPanel = (function () {
var React = window.React;
var jsx = window.jsxRuntime.jsx;
var jsxs = window.jsxRuntime.jsxs;
// ... 1965 行组件逻辑 ...
})();
「注入三连」的工作机制:这三行 var React = window.React + var jsx = window.jsxRuntime.jsx + var jsxs = window.jsxRuntime.jsxs 是 NeoMind 组件市场的「注入三连」。平台在加载组件之前,会先在宿主页面上挂载全局的 window.React(单例 React 实例)和 window.jsxRuntime(提供 jsx / jsxs 两个 JSX 转换函数)。组件在 IIFE 求值时(<script> 标签执行瞬间)从 window 上「借」一份引用。
这意味着 bundle 不打包 React,整个 1972 行文件只包含业务逻辑,没有 framework 运行时——unminified 约 80KB,minified 后更小。
与 ESM 打包组件的根本差异在于React 实例的唯一性。ESM 打包方案通常把 React 作为 external(不打进 bundle)或 peerDependency,但这依赖打包配置正确——一旦某个组件配置失误把 React 打进了 chunk,就会产生第二个 React 实例,导致 hooks 跨实例失效(useContext 返回 undefined、useRef 报错、useState 状态丢失)。
IIFE + window.React 范式从机制上杜绝了这个问题:平台保证只加载一份 React,所有 6 个市场组件共享同一个实例,没有任何打包配置可以让组件「自带」另一份。
IIFE 的收尾与导出:IIFE 的最后一行是闭包的收尾和导出,位于 bundle.js L1971-L1972:return { default: NE101CameraPanel, ... }; 紧跟 })();。return 的对象被赋值给 var NE101CameraPanel,再由平台的 <script> 加载器挂到 window.NE101CameraPanel。
IIFE 内部所有 function / var 声明都被闭包作用域保护,不泄漏到 window 上——这是 IIFE 替代模块系统的核心价值:零依赖的私有命名空间。
// bundle.js L1971-L1972
return { default: NE101CameraPanel, NE101CameraPanel: NE101CameraPanel, ConfigPanel: ConfigPanel, AdvancedPanel: AdvancedPanel };
})();
Source: bundle.js L1971-L1972
设计决策:IIFE + 全局注入 vs ESM bundle vs UMD
- 选择:
var Name = (function(){ var React = window.React; ... })()的 IIFE + 全局注入范式。 - 备选方案 A:ESM bundle(Vite / Rollup 产物,
import React from 'react')。否决理由:ESM 需要平台支持<script type="module">+ import map 或 bundler 解析,而 NeoMind 平台用普通<script>标签加载组件;更重要的是 ESM 的external配置一旦失误,React 会被打进 chunk,破坏单实例约定。 - 备选方案 B:UMD(Universal Module Definition)。否决理由:UMD 会探测
define(AMD)/module.exports(CommonJS)/window(global)三个分支,对「只用<script>加载」的 NeoMind 平台是冗余的——三个分支中有两个永远不会被触发,白白增加 bundle 体积。 - 理由:IIFE 是唯一能用「函数作用域 + 闭包」模拟私有命名空间的零依赖手段,且与平台的
<script>加载方式天然契合。这个生态的核心前提是「平台保证一个 React 实例」,IIFE 范式让组件代码无法违反这个前提。 - 代价:没有 tree-shaking(helper 层的未使用函数也会进 bundle)、没有 TypeScript 类型检查、没有 ESLint 的
rules-of-hooks插件(这正是 6.4 那个 hooks 顺序 bug 的根因)。ne101_camera 接受这个代价,靠test_bundle.js做逻辑测试兜底。
命名导出对象
IIFE 的 return 语句返回的是一个多键对象,不是单个函数。查看源码:bundle.js L1971。
return {
default: NE101CameraPanel,
NE101CameraPanel: NE101CameraPanel,
ConfigPanel: ConfigPanel,
AdvancedPanel: AdvancedPanel
};