devfab 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +512 -290
- package/dist/common/models.esm.js +2 -2
- package/dist/common/models.js +2 -2
- package/dist/common/require.esm.js +5 -0
- package/dist/common/require.js +5 -0
- package/dist/common/utils.esm.js +3 -1
- package/dist/common/utils.js +3 -1
- package/dist/index.esm.js +6 -2
- package/dist/index.js +6 -2
- package/dist/index.umd.js +6 -2
- package/dist/react/index.js +1 -1
- package/lib/devfab.js +4188 -1073
- package/lib/devfab.min.js +6 -2
- package/lib/devfab.require.js +3285 -0
- package/lib/devfab.require.min.js +5 -0
- package/package.json +10 -5
- package/dist/react/_virtual/_rollupPluginBabelHelpers.js +0 -3
- package/dist/react/adapter/common/dataSourceAdapter.js +0 -1
- package/dist/react/adapter/common/labelAdapter.js +0 -1
- package/dist/react/adapter/common/loadMetaAdapter.js +0 -1
- package/dist/react/adapter/common/refAdapter.js +0 -1
- package/dist/react/adapter/index.js +0 -1
- package/dist/react/adapter/model/dataSourceAdapter.js +0 -1
- package/dist/react/adapter/model/filterAdapter.js +0 -1
- package/dist/react/adapter/model/modelAdapter.js +0 -1
- package/dist/react/adapter/model/pageAdapter.js +0 -1
- package/lib/requirejs.js +0 -2286
package/README.md
CHANGED
|
@@ -1,290 +1,512 @@
|
|
|
1
|
-
# devfab
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
- React
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
-
|
|
284
|
-
-
|
|
285
|
-
|
|
286
|
-
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
1
|
+
# devfab
|
|
2
|
+
|
|
3
|
+
> 面向**实施 / 交付**团队的前端**客开(二次开发)**基础库 —— Developer Factory for Low-Code。
|
|
4
|
+
|
|
5
|
+
`devfab` 是一套运行在宿主应用(如 frontend)里的**运行时客开机制**。它的核心只有一件事:让实施人员能在**不改宿主、不发版**的前提下完成前端二次开发。做法是宿主内嵌一个运行时模块加载器 **FileRequire**,实施人员交付的**扩展脚本**与**自定义组件**都通过它在运行时动态加载、动态注册、动态使用。
|
|
6
|
+
|
|
7
|
+
- 二开产物不参与宿主构建:改了扩展脚本 / 组件,替换静态资源即生效。
|
|
8
|
+
- 一套机制,多框架:Vue 3、React、以及 **jQuery + 原生 ES Module** 宿主都能用,写法和加载方式一致。
|
|
9
|
+
- 统一的结果回调契约,加载成功 / 失败可感知、可兜底。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 目录
|
|
14
|
+
|
|
15
|
+
- [简介](#简介)
|
|
16
|
+
- [二开能力](#二开能力)
|
|
17
|
+
- [快速开始](#快速开始)
|
|
18
|
+
- [项目结构](#项目结构)
|
|
19
|
+
- [架构](#架构)
|
|
20
|
+
- [FileRequire 模块加载器](#filerequire-模块加载器)
|
|
21
|
+
- [加载策略与合规](#加载策略与合规)
|
|
22
|
+
- [客开使用文档](#客开使用文档)
|
|
23
|
+
- [开发与构建](#开发与构建)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 简介
|
|
29
|
+
|
|
30
|
+
### 一句话定位
|
|
31
|
+
|
|
32
|
+
`devfab` 解决的是「实施人员在客户现场做前端二开」这个场景:宿主的正式产品已经定版,实施需要为某个客户加上一点定制逻辑或定制控件,但又不能因此去改宿主代码、走宿主发版。
|
|
33
|
+
|
|
34
|
+
`devfab` 给出的答案是**运行时装配**:
|
|
35
|
+
|
|
36
|
+
| 二开交付物 | 形态 | 加载方式 |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| 扩展脚本 | `xxx_FE.Extend.js` | `define([], factory)` 注册 → 宿主 `require` 加载 |
|
|
39
|
+
| 自定义组件 | `components.js`(JSX 打包产物) | `provideScript(components)` 注册 → 宿主 `injectScript` 加载 |
|
|
40
|
+
|
|
41
|
+
这两类产物都通过 **FileRequire**(内嵌 RequireJS 的 AMD 加载器)在运行时按需加载,从而把「改代码」变成「换脚本 / 换组件」。
|
|
42
|
+
|
|
43
|
+
### 核心概念
|
|
44
|
+
|
|
45
|
+
- **FileRequire**:AMD 模块加载器。实施侧用 `define` / `provideScript` 注册二开产物,宿主侧用 `require` / `injectScript` 加载使用。
|
|
46
|
+
- **扩展脚本(Extend.js)**:一段返回 `{ doAction(name, viewModel) }` 的脚本,加载后被宿主按约定触发(如 `doAction('init', vm)`),用来注入业务逻辑。
|
|
47
|
+
- **自定义组件**:用 JSX 写的普通组件,整体注册成一个模块,宿主加载后按组件名渲染。
|
|
48
|
+
|
|
49
|
+
> 说明:devfab 仓库内 `src/_utils/` 下还有模型、渲染等内部实现,本次文档聚焦实施 / 客开 / 二开链路,不展开。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 二开能力
|
|
54
|
+
|
|
55
|
+
- ✅ 扩展脚本二开:脚本按 `domain` 组织,运行时动态加载,加载后回调 `doAction` 注入逻辑。
|
|
56
|
+
- ✅ 自定义组件二开:JSX 组件打包后 `provideScript` 注册,宿主 `injectScript` 加载即用。
|
|
57
|
+
- ✅ 目录化加载:`require(path, files, cb)` 以「目录 + 文件列表」拼模块路径,无需关心 AMD 模块名规则。
|
|
58
|
+
- ✅ 统一结果契约:`{ code, message, data, error }`,加载 / 执行失败可感知。
|
|
59
|
+
- ✅ 不改宿主不发版:二开产物是独立静态资源,替换即生效。
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 快速开始
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 安装依赖
|
|
67
|
+
npm install
|
|
68
|
+
|
|
69
|
+
# 构建库产物(dist/ 与 lib/)
|
|
70
|
+
npm run build
|
|
71
|
+
|
|
72
|
+
# 跑冒烟测试
|
|
73
|
+
npm run smoke
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 运行示例
|
|
77
|
+
|
|
78
|
+
示例演示了「扩展脚本 + 自定义组件」两条二开链路:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Vue 3 宿主示例(默认端口 9010,dev 模式含 live reload)
|
|
82
|
+
npm run example:vue3
|
|
83
|
+
|
|
84
|
+
# React 宿主示例(默认端口 9011)
|
|
85
|
+
npm run example:react
|
|
86
|
+
|
|
87
|
+
# jQuery + 原生 ESM 宿主示例(默认端口 9012,无构建步骤,自带 mock 接口)
|
|
88
|
+
npm run example:html5
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
子命令:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm run example:vue3 -- start # 构建 + 起静态服务
|
|
95
|
+
npm run example:vue3 -- build # 只构建一次
|
|
96
|
+
npm run example:vue3 -- serve # 只起静态服务(加 --live 开自动刷新)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> `example:html5` 只有 `dev`(默认) / `serve` / `build` 三种跑法,其中 `build` 只打印一句「本示例无构建」——
|
|
100
|
+
> 它的宿主页面是 `index.html` + 原生 ES Module,不需要打包。详细的对比与目录说明见
|
|
101
|
+
> [`example/html5/README.md`](./example/html5/README.md)。
|
|
102
|
+
|
|
103
|
+
端口可用 `EXAMPLE_PORT=9020 npm run example:vue3` 覆盖。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 项目结构
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
devfab/
|
|
111
|
+
├── src/
|
|
112
|
+
│ ├── index.js # 入口:挂载 window.__devfab,暴露 define / require / utils
|
|
113
|
+
│ └── FileRequire/ # 运行时模块加载器(二开核心)
|
|
114
|
+
│ ├── file.js # FileRequire 类:define / provideScript / require / injectScript / loadModules
|
|
115
|
+
│ ├── policy.js # 加载策略:清单登记、域名白名单、版本锁、sha256 校验
|
|
116
|
+
│ ├── guard.js # 全局污染检测:加载前后 diff window
|
|
117
|
+
│ ├── requirejs.js # 内嵌 RequireJS 2.3.2 + 本地 domain 适配层
|
|
118
|
+
│ └── index.js # 导出并挂载 window.__devfab.FileRequire
|
|
119
|
+
├── example/
|
|
120
|
+
│ ├── vue3/ # Vue 3 宿主示例
|
|
121
|
+
│ │ ├── src/components/ # 自定义组件源码(provideScript 注册)
|
|
122
|
+
│ │ └── static/extendjs/ # 扩展脚本(define 注册,运行时按 domain 加载)
|
|
123
|
+
│ ├── react/ # React 宿主示例(结构同上,需 rollup 构建)
|
|
124
|
+
│ └── html5/ # jQuery + 原生 ESM 宿主示例(无构建)
|
|
125
|
+
│ ├── index.html # 宿主页面:<script> 引 jQuery 与 /vendor/devfab.js
|
|
126
|
+
│ ├── dev.js # 静态服务 + live reload + mock 接口
|
|
127
|
+
│ ├── lib/ # vendored jQuery
|
|
128
|
+
│ ├── css/example.css # 手写的控件样式(等价于 antd 那一层)
|
|
129
|
+
│ ├── src/ # 宿主源码(ui-model / views / components)
|
|
130
|
+
│ └── static/extendjs/ # 扩展脚本(与 vue3 / react 同一套路径约定)
|
|
131
|
+
├── scripts/
|
|
132
|
+
│ ├── smoke.js # 零依赖冒烟测试
|
|
133
|
+
│ ├── manifest.js # 生成二开包清单 manifest.json(含 sha256)
|
|
134
|
+
│ └── fixtures/ # 冒烟测试夹具(扩展脚本 + ESM 模块)
|
|
135
|
+
├── dist/ # 构建产物(cjs / es / umd)
|
|
136
|
+
├── lib/ # 可 <script> 直引的 UMD 运行时
|
|
137
|
+
├── rollup.config.js
|
|
138
|
+
└── package.json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 架构
|
|
144
|
+
|
|
145
|
+
### 客开二开架构图
|
|
146
|
+
|
|
147
|
+
```mermaid
|
|
148
|
+
flowchart TB
|
|
149
|
+
subgraph HOST["宿主应用(运行时内嵌 devfab)"]
|
|
150
|
+
HTML["页面加载 lib/devfab.js<br/>window.devfab / window.__devfab"]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
subgraph RT["devfab 运行时"]
|
|
154
|
+
FR["FileRequire<br/>AMD 加载器(内嵌 RequireJS + domain 适配)"]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
subgraph CUSTOM["实施交付物(运行时动态加载,不参与宿主构建)"]
|
|
158
|
+
EXTEND["扩展脚本<br/>xxx_FE.Extend.js<br/>define([], factory)"]
|
|
159
|
+
COMP["自定义组件<br/>components.js<br/>provideScript(components)"]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
HOST --> FR
|
|
163
|
+
FR -- "define / require" --> EXTEND
|
|
164
|
+
FR -- "provideScript / injectScript" --> COMP
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- **宿主**:只负责内嵌 devfab 运行时,提供扩展脚本 / 组件的存放位置与加载入口。
|
|
168
|
+
- **FileRequire**:唯一的装配通道,二开产物的注册与加载都经过它。
|
|
169
|
+
- **实施交付物**:扩展脚本(注入业务逻辑)与自定义组件(扩展 UI),与宿主解耦。
|
|
170
|
+
|
|
171
|
+
### 二开加载时序
|
|
172
|
+
|
|
173
|
+
```mermaid
|
|
174
|
+
sequenceDiagram
|
|
175
|
+
participant Host as 宿主页面
|
|
176
|
+
participant FR as FileRequire
|
|
177
|
+
participant RS as RequireJS(内嵌)
|
|
178
|
+
participant Ext as 扩展脚本 / 组件产物
|
|
179
|
+
|
|
180
|
+
Note over Ext: 脚本内 window.__devfab.define([], factory)<br/>组件内 provideScript(components)
|
|
181
|
+
Host->>FR: require(path, files, cb)<br/>或 injectScript([url], cb)
|
|
182
|
+
FR->>RS: 按 domain 拼接路径,动态注入 ⟨script⟩ 加载
|
|
183
|
+
Ext-->>RS: define() 注册模块
|
|
184
|
+
RS-->>FR: 返回模块内容 contents[]
|
|
185
|
+
FR-->>Host: result = { code, message, data, error }
|
|
186
|
+
Note over Host: code===0 时遍历 data:<br/>扩展脚本 → doAction('init', vm)<br/>自定义组件 → 直接渲染
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## FileRequire 模块加载器
|
|
192
|
+
|
|
193
|
+
`FileRequire` 是 devfab 的二开核心:一个内嵌 **RequireJS 2.3.2** 的 AMD 加载器,并在其上加了一层「domain 路径适配」,让你以「目录 + 文件列表」的方式加载脚本,而不必关心 AMD 的模块名规则。
|
|
194
|
+
|
|
195
|
+
### 全局挂载
|
|
196
|
+
|
|
197
|
+
加载 `lib/devfab.js`(UMD)后,浏览器环境可拿到:
|
|
198
|
+
|
|
199
|
+
| 全局对象 | 说明 |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| `window.devfab` | UMD 导出对象,含 `define` / `require` / `FileRequire` / `utils` / `debug` |
|
|
202
|
+
| `window.__devfab` | 源码入口显式挂载的同一份能力(`Object.assign({}, window.__devfab, devfab)`) |
|
|
203
|
+
| `window.__devfab.FileRequire` | `FileRequire` 类 |
|
|
204
|
+
| `window.__devfab_requirejs` | 内嵌 RequireJS 的 `require` / `define`(含 domain 适配) |
|
|
205
|
+
|
|
206
|
+
> 扩展脚本里统一写 `window.__devfab.define(...)`。`window.devfab` 是 UMD 导出对象,
|
|
207
|
+
> 只导出 `default`(`src/index.js` 没有具名导出),所以 `window.devfab.define` 是
|
|
208
|
+
> `undefined` —— 两者**并不等价**,别照抄网上的写法。
|
|
209
|
+
|
|
210
|
+
### 核心 API
|
|
211
|
+
|
|
212
|
+
`FileRequire.create(args?)` 返回一个实例。`args` 同时接受两类配置:
|
|
213
|
+
|
|
214
|
+
| 分区 | 字段 | 说明 |
|
|
215
|
+
| --- | --- | --- |
|
|
216
|
+
| 底层加载器 | `define` / `require` / `undef` / `requireConfig` / `importer` | 覆盖默认加载器,默认取 `window.__devfab_requirejs` |
|
|
217
|
+
| 加载策略 | `version` / `timeout` / `retry` / `strict` / `baseUrl` | 见下方「加载策略与合规」 |
|
|
218
|
+
| 合规 | `manifest` / `origins` / `lockVersion` / `verifyHash` / `fetch` | 见下方「加载策略与合规」 |
|
|
219
|
+
| 污染检测 | `pollution` / `watch` / `maxPollution` / `target` | `pollution: false` 可关闭 |
|
|
220
|
+
|
|
221
|
+
| 方法 | 签名 | 作用 |
|
|
222
|
+
| --- | --- | --- |
|
|
223
|
+
| `define` | `define(deps, factory)` | 注册 AMD 模块(匿名,`deps` 为依赖数组) |
|
|
224
|
+
| `provideScript` | `provideScript(objOrFn)` | 把对象 / 函数包装成无依赖匿名模块注册 |
|
|
225
|
+
| `require` | `require(path, files, callback)` | 加载模块,`callback` 收到统一结果对象 |
|
|
226
|
+
| `load` | `load(path, files, callback)` | 与 `require` 等价,名字用来标记「这一处走完整策略」 |
|
|
227
|
+
| `injectScript` | `injectScript(pathOrFiles, callback)` | 加载脚本,对每个模块分别回调 `(content, index)` |
|
|
228
|
+
| `loadModules` | `loadModules(pathOrFiles, callback?)` | **ESM 通道**:原生 `import()`,返回 Promise |
|
|
229
|
+
| `configure` | `configure(options)` | 运行时改策略(切版本、换清单、调重试次数) |
|
|
230
|
+
| `setVersion` | `setVersion(version)` | 设置二开包版本,写进 RequireJS 的 `urlArgs` |
|
|
231
|
+
| `describe` | `describe()` | 输出当前策略摘要,宿主启动时打一行日志 |
|
|
232
|
+
|
|
233
|
+
### 加载结果契约
|
|
234
|
+
|
|
235
|
+
`require` / `load` / `injectScript` / `loadModules` 的回调拿到统一结构:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
{
|
|
239
|
+
code: 0, // 0 成功 | 403 策略拒绝 | 404 加载器缺失 | 500 加载/执行失败 | 504 超时
|
|
240
|
+
message: '', // 失败原因
|
|
241
|
+
data: [], // 加载到的模块内容数组
|
|
242
|
+
error: null, // 失败时的原始错误对象
|
|
243
|
+
warnings: [], // 全局污染告警(见下)
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- `code === 0`:`data` 里是各脚本 `define` 返回的模块内容;
|
|
248
|
+
- `code === 403`:**未在清单中登记 / 来源不在白名单 / 版本锁不匹配 / sha256 校验失败**,
|
|
249
|
+
这种情况下加载器不会被触达,脚本不会执行;
|
|
250
|
+
- `code === 404`:底层 `requirejs` 未就绪;
|
|
251
|
+
- `code === 500`:脚本加载或执行出错,`message` 会带上文件列表,`error` 带原始异常;
|
|
252
|
+
- `code === 504`:超过 `timeout` 仍未回调(默认 15s,`timeout: 0` 关闭,改由 RequireJS 的
|
|
253
|
+
`waitSeconds` 兜底)。
|
|
254
|
+
|
|
255
|
+
> 老宿主只判 `code === 0` 即可,新增的 `403` / `504` 一定伴随失败分支,不会误判为成功。
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 加载策略与合规
|
|
260
|
+
|
|
261
|
+
这一层回答的是「这段代码该不该被执行」。**它不做代码隔离**(那是沙箱的事),
|
|
262
|
+
只做四件企业交付必须有的判断,且默认全部**不启用**(零影响),需要时显式打开。
|
|
263
|
+
|
|
264
|
+
### 版本:让「换了文件」真的生效
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
const fileRequire = window.__devfab.FileRequire.create({ version: '2026.10.20' })
|
|
268
|
+
// 或运行时切:fileRequire.setVersion('2026.10.21')
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
版本会作为 `?v=` 交给 RequireJS 的 `urlArgs`,进而进入模块缓存键。
|
|
272
|
+
没有它,二开文件替换后浏览器仍命中旧缓存 —— 「明明换了脚本,行为没变」的典型成因。
|
|
273
|
+
|
|
274
|
+
> 这是 RequireJS **context 级**配置,一个页面内以最后一次设置为准。这与实际情况相符:
|
|
275
|
+
> 整包本来就应该同一个版本,分模块各自版本只会让缓存失效变成玄学。
|
|
276
|
+
|
|
277
|
+
### 失败兜底:超时 + 重试
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
const fileRequire = window.__devfab.FileRequire.create({ timeout: 8000, retry: 2 })
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- 超时先于 RequireJS 的 `waitSeconds`(默认 30s)触发,回 `code: 504`;
|
|
284
|
+
- 重试前会对失败模块调 `undef()` —— 不 undef 的话 RequireJS 会直接复用失败状态,
|
|
285
|
+
重试形同虚设;
|
|
286
|
+
- 重试仍失败才回 `500`,`error` 保留最后一次原始异常。
|
|
287
|
+
|
|
288
|
+
### 清单、白名单、版本锁
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
// 1) 构建期产出清单(含 sha256)
|
|
292
|
+
// node scripts/manifest.js example/vue3/static --name frontend-extends --version 1.0.0
|
|
293
|
+
|
|
294
|
+
// 2) 运行时加载清单并启用检查
|
|
295
|
+
const fileRequire = window.__devfab.FileRequire.create({
|
|
296
|
+
manifest, // 也可以传 URL 自己 fetch
|
|
297
|
+
lockVersion: '1.0.0', // 锁定版本,防止「悄悄换文件」
|
|
298
|
+
origins: ['https://cdn.example.com'], // 只允许从这些来源加载
|
|
299
|
+
verifyHash: true, // ESM 通道默认校验 sha256
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
| 检查 | AMD 通道(`require` / `injectScript`) | ESM 通道(`loadModules`) |
|
|
304
|
+
| --- | --- | --- |
|
|
305
|
+
| 清单登记制 | ✅ 未登记直接 `403` | ✅ |
|
|
306
|
+
| 域名白名单 | ✅ | ✅ |
|
|
307
|
+
| 版本锁 | ✅ | ✅ |
|
|
308
|
+
| sha256 完整性 | ❌ 拿不到脚本文本 | ✅ 加载前 `fetch` 预检 |
|
|
309
|
+
|
|
310
|
+
> AMD 通道做不了 sha256 是**结构性限制**:RequireJS 自己插 `<script>`,加载器拿不到文本。
|
|
311
|
+
> 所以 AMD 侧的完整性靠「构建期校验 + 版本锁 + 清单登记」组合,别指望运行时会话级校验。
|
|
312
|
+
> `crypto.subtle` 只在安全上下文(https / localhost)可用,不可用时会跳过并在结果里标注,
|
|
313
|
+
> 不会静默通过。
|
|
314
|
+
|
|
315
|
+
### 全局污染检测
|
|
316
|
+
|
|
317
|
+
二开脚本直接跑在宿主 `window` 上,A 客户的脚本改了一个全局变量,B 客户就跟着崩,
|
|
318
|
+
而且现场无法归因。加载器会在加载前后 diff `window` 的 key 集合:
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
fileRequire.require(['extendjs', 'frontend'], ['xxx.js'], (result) => {
|
|
322
|
+
if (result.warnings.length) {
|
|
323
|
+
console.warn('[devfab] 二开脚本动了这些全局:', result.warnings)
|
|
324
|
+
// [{ type: 'added', key: '__someSdk', watched: false }, ...]
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- 命中盯防名单(`$` / `jQuery` / `define` / `window.__devfab` 等)立即告警;
|
|
330
|
+
- 新增全局超过 `maxPollution`(默认 3)告警;
|
|
331
|
+
- `strict: true` 时把告警升级为 `code: 500`(**开发期用**:脚本已经执行过了,
|
|
332
|
+
这是事后报错,不是拦截);
|
|
333
|
+
- 这是**观测**不是隔离,并发加载时归因可能不准。
|
|
334
|
+
|
|
335
|
+
### ESM 通道:给 RequireJS 的技术债留一条退路
|
|
336
|
+
|
|
337
|
+
RequireJS 2.3.2 是 2018 年的技术,新宿主清一色 Vite + ESM。所以加载器提供第二条通道,
|
|
338
|
+
让 AMD 从「唯一实现」降级成「兼容层之一」:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
// 新:原生 import(),返回 Promise
|
|
342
|
+
const result = await fileRequire.loadModules(['extendjs/frontend/xxx.mjs'])
|
|
343
|
+
result.data // 各模块的导出对象
|
|
344
|
+
|
|
345
|
+
// 旧:AMD,保留给存量宿主
|
|
346
|
+
fileRequire.require(['extendjs', 'frontend'], ['xxx.js'], (result) => {})
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- ESM 通道要求产物是**标准 ESM**,不参与 AMD 的匿名 `define` 匹配;
|
|
350
|
+
- 相对路径以 `baseUrl`(缺省 `document.baseURI`)为基准解析;
|
|
351
|
+
- 依赖 `new Function` 构造原生 `import()`(rollup 会把字面量 `import()` 改写成 `require()`),
|
|
352
|
+
因此要求 CSP 允许 `unsafe-eval` —— 和内嵌 RequireJS 本身是同一个前提。
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 客开使用文档
|
|
357
|
+
|
|
358
|
+
以下示例均取自 `example/`,只涉及 FileRequire,是实施 / 二开的标准写法。
|
|
359
|
+
|
|
360
|
+
### 场景一:扩展脚本二开
|
|
361
|
+
|
|
362
|
+
**第一步:写扩展脚本,用 `define` 注册。**
|
|
363
|
+
|
|
364
|
+
`example/vue3/static/extendjs/frontend/frontend_view_list_FE.Extend.js`:
|
|
365
|
+
|
|
366
|
+
```js
|
|
367
|
+
/**
|
|
368
|
+
* 扩展脚本示例
|
|
369
|
+
* 运行时契约:用 window.__devfab.define([], factory) 注册匿名模块,
|
|
370
|
+
* 模块返回 { doAction(name, viewModel) },加载后由宿主按约定调用。
|
|
371
|
+
*/
|
|
372
|
+
window.__devfab.define([], function () {
|
|
373
|
+
return {
|
|
374
|
+
doAction(name, viewModel) {
|
|
375
|
+
if (name !== 'init') return
|
|
376
|
+
viewModel.setCache('extendLoaded', 'frontend_view_list')
|
|
377
|
+
console.info('[extend] frontend_view_list_FE.Extend.js init')
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
> 扩展脚本按 `extendjs/{domainKey}/{pageKey}_FE.Extend.js` 的路径约定存放,`domainKey` 用来把脚本归类到不同客户 / 业务域。
|
|
384
|
+
|
|
385
|
+
**第二步:宿主用 `require` 加载并触发。**
|
|
386
|
+
|
|
387
|
+
`require(path, files, callback)` 里 `path` 是目录前缀(可传数组),`files` 是文件列表,二者会被拼接成模块路径:
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
window.__devfab.FileRequire.create().require(
|
|
391
|
+
['extendjs', 'frontend'], // path:目录前缀 → 'extendjs/frontend'
|
|
392
|
+
['frontend_view_list_FE.Extend.js'], // files:文件列表
|
|
393
|
+
function (result) {
|
|
394
|
+
if (result.code === 0) {
|
|
395
|
+
// 对每个加载到的扩展模块执行 doAction
|
|
396
|
+
result.data.forEach((mod) => mod?.doAction?.('init', viewModel))
|
|
397
|
+
} else {
|
|
398
|
+
console.error(result.message, result.error)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### 场景二:自定义组件二开
|
|
405
|
+
|
|
406
|
+
**第一步:写组件,用 `provideScript` 注册。**
|
|
407
|
+
|
|
408
|
+
`example/vue3/src/components/index.js`(React 侧写法一致):
|
|
409
|
+
|
|
410
|
+
```js
|
|
411
|
+
function Test(props) {
|
|
412
|
+
return <div>I am a test vue component, props={props.value}</div>
|
|
413
|
+
}
|
|
414
|
+
function Message(props) {
|
|
415
|
+
return <div>I am a message extend component, props={props.value}</div>
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const components = { Test, Message }
|
|
419
|
+
|
|
420
|
+
// 把整个组件表注册成一个无依赖的匿名模块
|
|
421
|
+
window.__devfab?.FileRequire?.create().provideScript(components)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
> 组件源码正常写 JSX,构建时打进一个独立的 `components.js` 产物(`example/vue3/dist/components/vue-components.js`),加载该文件即完成注册。
|
|
425
|
+
|
|
426
|
+
不带构建的宿主同样只认这条契约,组件返回什么由宿主自己决定 —— jQuery 侧直接返回一段 DOM:
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
// example/html5/src/components/html5-components.js(无 JSX、无打包)
|
|
430
|
+
function Test(props) {
|
|
431
|
+
return window.jQuery('<span class="df-tag df-tag-geekblue"/>')
|
|
432
|
+
.text(`I am a test html5 component, props=${props.value}`)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
window.__devfab?.FileRequire?.create().provideScript({ Test })
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**第二步:宿主用 `injectScript` 加载并使用。**
|
|
439
|
+
|
|
440
|
+
`example/vue3/src/views/index.js`(页面挂载时):
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
window.__devfab.FileRequire.create().injectScript(
|
|
445
|
+
['http://localhost:9010/components/vue-components.js'],
|
|
446
|
+
function (result) {
|
|
447
|
+
// provideScript 注册的是整个 components 对象,
|
|
448
|
+
// 这里 result 即 { Test, Message },可直接作为组件使用
|
|
449
|
+
setState(result)
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
}, [])
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
之后按组件名渲染即可(此处 `Test` 即第二步回调里拿到的组件):
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
const TestCom = state.Test
|
|
459
|
+
// ...
|
|
460
|
+
{TestCom && <TestCom value={'12345'} />}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 二开交付清单
|
|
464
|
+
|
|
465
|
+
一次典型的现场二开,交付物就两类,全部无需改动宿主构建:
|
|
466
|
+
|
|
467
|
+
1. **扩展脚本**:`extendjs/{domainKey}/{pageKey}_FE.Extend.js`,`define([], () => ({ doAction }))`,替换静态资源即生效;
|
|
468
|
+
2. **自定义组件**:JSX 组件打包成 `components.js`,`provideScript(components)` 注册,宿主 `injectScript([url], cb)` 加载使用。
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## 开发与构建
|
|
473
|
+
|
|
474
|
+
### 构建
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
npm run build # 生产构建(clean + rollup,压缩产物)
|
|
478
|
+
npm run dev # 开发构建(clean + rollup,保持可读)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
产物分布:
|
|
482
|
+
|
|
483
|
+
| 路径 | 内容 |
|
|
484
|
+
| --- | --- |
|
|
485
|
+
| `dist/index.js` / `dist/index.esm.js` / `dist/index.umd.js` | 库主体(CJS / ESM / UMD) |
|
|
486
|
+
| `dist/common/require.js` / `require.esm.js` | FileRequire 独立产物 |
|
|
487
|
+
| `lib/devfab.js` / `lib/devfab.min.js` | 宿主 `<script>` 直引的 UMD 运行时(可读 / 压缩) |
|
|
488
|
+
| `lib/devfab.require.js` / `.min.js` | FileRequire 的 UMD 运行时 |
|
|
489
|
+
|
|
490
|
+
> `lib/devfab.js` 刻意不压缩,便于宿主排查;压缩版见 `lib/devfab.min.js`。宿主只需 `<script>` 引入它,即可拿到 `window.devfab` / `window.__devfab`。
|
|
491
|
+
|
|
492
|
+
### 脚本
|
|
493
|
+
|
|
494
|
+
| 命令 | 说明 |
|
|
495
|
+
| --- | --- |
|
|
496
|
+
| `npm run smoke` | 零依赖冒烟测试 |
|
|
497
|
+
| `npm run manifest -- <目录> --version 1.0.0` | 生成二开包清单 `manifest.json`(含 sha256),加 `--check` 只校验不写盘 |
|
|
498
|
+
| `npm run example:vue3` | 启动 Vue 3 示例 |
|
|
499
|
+
| `npm run example:react` | 启动 React 示例 |
|
|
500
|
+
| `npm run example:html5` | 启动 jQuery + html5 示例 |
|
|
501
|
+
|
|
502
|
+
### 技术栈
|
|
503
|
+
|
|
504
|
+
- 构建:Rollup + Babel(JSX 双编译:React 走 `@babel/preset-react`,Vue 走 `@vue/babel-plugin-jsx`)
|
|
505
|
+
- 宿主框架:Vue 3 / React / jQuery(`example/html5` 不经过构建,浏览器直接跑 ES Module)
|
|
506
|
+
- 语言:JavaScript(`.js` / `.jsx`)
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## License
|
|
511
|
+
|
|
512
|
+
[ISC](./LICENSE)
|