@maiyunnet/kebab 9.18.2 → 9.19.1
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/doc/kebab-rag.md +181 -131
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/sys/ctr.d.ts +29 -33
- package/sys/ctr.js +53 -129
- package/sys/route.js +3 -1
- package/www/example/ctr/test.js +6 -6
- package/www/example/stc/view/hello.page.d.ts +2 -1
- package/www/example/stc/view/hello.page.js +2 -1
- package/www/example/stc/view/hello.page.tsx +2 -1
- package/www/example/stc/view/react-router.page.d.ts +3 -3
- package/www/example/stc/view/react-router.page.js +3 -3
- package/www/example/stc/view/react-router.page.tsx +3 -4
- package/www/example/stc/view/react.page.d.ts +5 -9
- package/www/example/stc/view/react.page.js +7 -11
- package/www/example/stc/view/react.page.tsx +8 -14
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.
|
|
9
|
+
export const VER = '9.19.1';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/package.json
CHANGED
package/sys/ctr.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export declare function clearLocaleData(): void;
|
|
|
19
19
|
export type TValibotResponse<TIssue extends v.BaseIssue<unknown>> = kebab.Json[] | ((issues: [TIssue, ...TIssue[]]) => kebab.Json[]);
|
|
20
20
|
/** --- 在 issue 路径完整后,根据 Kebab 生成的语言键翻译消息 --- */
|
|
21
21
|
export type TValibotTranslate = (key: string, issue: v.BaseIssue<unknown>) => string;
|
|
22
|
+
/** --- HTTP 重定向状态码 --- */
|
|
23
|
+
export type TRedirectHttpCode = 301 | 302 | 303 | 307 | 308;
|
|
22
24
|
/** --- Valibot 同步校验选项 --- */
|
|
23
25
|
export interface IValibotOptions<TIssue extends v.BaseIssue<unknown>> {
|
|
24
26
|
/** --- 传给 Valibot 的校验配置 --- */
|
|
@@ -58,6 +60,21 @@ export type TValibotResult<TSchema extends v.GenericSchema, TIssue extends v.Bas
|
|
|
58
60
|
readonly issues: [TIssue, ...TIssue[]];
|
|
59
61
|
readonly response: kebab.Json[];
|
|
60
62
|
};
|
|
63
|
+
/** --- React SSR 页面选项 --- */
|
|
64
|
+
export interface IReactPageOptions {
|
|
65
|
+
/** --- 是否注入客户端水合 bundle,默认 true;设为 false 时仅输出服务端渲染的 HTML --- */
|
|
66
|
+
'hydrate'?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* --- 路由模式,不传则不注入任何 Router,组件自行管理路由(如 MemoryRouter)或无路由 ---
|
|
69
|
+
* --- 'browser':服务端用 StaticRouter,客户端用 BrowserRouter,地址栏与路由联动 ---
|
|
70
|
+
* --- 组件本身只需使用 Routes/Route/Link 等,不要包含任何 Router 包裹层 ---
|
|
71
|
+
*/
|
|
72
|
+
'router'?: 'browser';
|
|
73
|
+
/** --- BrowserRouter 的 basename,相对于 urlBase,默认空字符串 --- */
|
|
74
|
+
'routerBase'?: string;
|
|
75
|
+
/** --- 静态资源基础路径,覆盖 config.set.staticPath(支持 CDN) --- */
|
|
76
|
+
'staticPath'?: string;
|
|
77
|
+
}
|
|
61
78
|
/**
|
|
62
79
|
* --- 判断当前 HTTP 请求是否仍可响应 ---
|
|
63
80
|
* @param req 请求对象
|
|
@@ -213,42 +230,20 @@ export declare class Ctr {
|
|
|
213
230
|
/**
|
|
214
231
|
* --- 加载 React 全页面进行 SSR 渲染,组件需渲染完整 HTML 文档(含 html/head/body),无需 EJS ---
|
|
215
232
|
* --- 框架自动注入 props:_urlBase/_urlFull/_urlStc/_staticVer/_staticPath/_staticPathFull ---
|
|
216
|
-
* --- 多语言:自动注入 _locale(当前语言名)和 _localeData
|
|
233
|
+
* --- 多语言:自动注入 _locale(当前语言名)和 _localeData(当前语种已载语言包的合并键值对) ---
|
|
217
234
|
* --- 组件内创建:const l = (key: string, ...args: string[]): string => { let i = 0; return (_localeData[key] ?? key).replace(/\?/g, () => args[i++] ?? ''); }; ---
|
|
218
235
|
* @param path 页面组件路径(相对于 stc/ 目录,不含扩展名,tsx 编译后的 .js)
|
|
219
236
|
* @param props 传入组件的 props,框架常量自动合并,整体序列化为内联 JSON 供客户端水合复用
|
|
220
237
|
* @param opt 可选配置
|
|
221
238
|
*/
|
|
222
|
-
protected _loadReactPage(path: string, props?: Record<string, kebab.Json>, opt?:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
* --- 组件本身只需使用 Routes/Route/Link 等,不要包含任何 Router 包裹层 ---
|
|
231
|
-
*/
|
|
232
|
-
'router'?: 'browser';
|
|
233
|
-
/**
|
|
234
|
-
* --- BrowserRouter 的 basename,相对于 urlBase,默认空字符串 ---
|
|
235
|
-
* --- 例如组件挂载在 /test/react-router-page,则填 'test/react-router-page' ---
|
|
236
|
-
*/
|
|
237
|
-
'routerBase'?: string;
|
|
238
|
-
/**
|
|
239
|
-
* --- 静态资源基础路径,覆盖 config.set.staticPath,用于指定 CDN 或自定义路径 ---
|
|
240
|
-
* --- 影响 _staticPath prop 以及水合脚本中 JS 文件的 URL 前缀 ---
|
|
241
|
-
*/
|
|
242
|
-
'staticPath'?: string;
|
|
243
|
-
}): Promise<string>;
|
|
244
|
-
/**
|
|
245
|
-
* --- 递归扫描 JS 文件中的 import 语句,收集第三方 bare specifier ---
|
|
246
|
-
* @param filePath 当前要扫描的文件绝对路径
|
|
247
|
-
* @param scannedFiles 已扫描文件集(去重用)
|
|
248
|
-
* @param extraImports 收集到的第三方包名集合
|
|
249
|
-
* @param builtinImports 内置 import map,已有条目不重复添加
|
|
250
|
-
*/
|
|
251
|
-
private _scanImports;
|
|
239
|
+
protected _loadReactPage(path: string, props?: Record<string, kebab.Json>, opt?: IReactPageOptions): Promise<string>;
|
|
240
|
+
/**
|
|
241
|
+
* --- 记录 React 页面错误并返回脱敏响应 ---
|
|
242
|
+
* @param message 调试信息
|
|
243
|
+
* @param detail 日志详情
|
|
244
|
+
* @returns 脱敏后的 500 页面
|
|
245
|
+
*/
|
|
246
|
+
private _reactPageError;
|
|
252
247
|
/**
|
|
253
248
|
* --- 设置校验错误返回值 ---
|
|
254
249
|
* @param rtn 返回值数组
|
|
@@ -343,10 +338,11 @@ export declare class Ctr {
|
|
|
343
338
|
*/
|
|
344
339
|
protected _loadData<T>(path: string): Promise<T | false | null>;
|
|
345
340
|
/**
|
|
346
|
-
* ---
|
|
341
|
+
* --- 跳转,支持相对本项目根路径的路径或绝对路径 ---
|
|
347
342
|
* @param location 相对或绝对网址
|
|
343
|
+
* @param httpCode HTTP 重定向状态码,默认 302
|
|
348
344
|
*/
|
|
349
|
-
protected _location(location: string): false;
|
|
345
|
+
protected _location(location: string, httpCode?: TRedirectHttpCode): false;
|
|
350
346
|
/**
|
|
351
347
|
* --- 开启 Session ---
|
|
352
348
|
* @param link Kv 或 Db 实例
|
package/sys/ctr.js
CHANGED
|
@@ -264,35 +264,35 @@ export class Ctr {
|
|
|
264
264
|
/**
|
|
265
265
|
* --- 加载 React 全页面进行 SSR 渲染,组件需渲染完整 HTML 文档(含 html/head/body),无需 EJS ---
|
|
266
266
|
* --- 框架自动注入 props:_urlBase/_urlFull/_urlStc/_staticVer/_staticPath/_staticPathFull ---
|
|
267
|
-
* --- 多语言:自动注入 _locale(当前语言名)和 _localeData
|
|
267
|
+
* --- 多语言:自动注入 _locale(当前语言名)和 _localeData(当前语种已载语言包的合并键值对) ---
|
|
268
268
|
* --- 组件内创建:const l = (key: string, ...args: string[]): string => { let i = 0; return (_localeData[key] ?? key).replace(/\?/g, () => args[i++] ?? ''); }; ---
|
|
269
269
|
* @param path 页面组件路径(相对于 stc/ 目录,不含扩展名,tsx 编译后的 .js)
|
|
270
270
|
* @param props 传入组件的 props,框架常量自动合并,整体序列化为内联 JSON 供客户端水合复用
|
|
271
271
|
* @param opt 可选配置
|
|
272
272
|
*/
|
|
273
273
|
async _loadReactPage(path, props = {}, opt = {}) {
|
|
274
|
-
// ---
|
|
275
|
-
|
|
276
|
-
const
|
|
274
|
+
// --- 校验服务端组件与客户端水合产物 ---
|
|
275
|
+
const hydrate = opt.hydrate !== false;
|
|
276
|
+
const pagePath = `${this._config.const.rootPath}stc/${path}.page`;
|
|
277
|
+
const componentPath = `${pagePath}.js`;
|
|
277
278
|
if (!await lFs.isFile(componentPath)) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
return this._reactPageError(`componentPath ${componentPath} is not a file`);
|
|
280
|
+
}
|
|
281
|
+
const bundlePath = `${pagePath}.bundle.js`;
|
|
282
|
+
if (hydrate && !await lFs.isFile(bundlePath)) {
|
|
283
|
+
return this._reactPageError(`bundlePath ${bundlePath} is not a file; run "npx kebab build -d ${this._config.const.rootPath}stc"`);
|
|
281
284
|
}
|
|
282
285
|
try {
|
|
283
|
-
|
|
286
|
+
// --- 并行加载 SSR 依赖与页面组件 ---
|
|
284
287
|
const importPath = componentPath.startsWith('/')
|
|
285
288
|
? componentPath
|
|
286
289
|
: `file:///${componentPath.replace(/\\/g, '/')}`;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
Object.assign(localeData, this._localeData[pkg]);
|
|
294
|
-
}
|
|
295
|
-
// --- 把框架常量合并进 props,与 _loadView 行为一致 ---
|
|
290
|
+
const [react, reactDomServer, page] = await Promise.all([
|
|
291
|
+
import('react'),
|
|
292
|
+
import('react-dom/server'),
|
|
293
|
+
import(importPath),
|
|
294
|
+
]);
|
|
295
|
+
// --- 合并框架 Props 与路由配置 ---
|
|
296
296
|
const staticPath = opt.staticPath ?? this._config.set.staticPath;
|
|
297
297
|
const fullProps = {
|
|
298
298
|
...props,
|
|
@@ -303,127 +303,49 @@ export class Ctr {
|
|
|
303
303
|
'_staticPath': staticPath,
|
|
304
304
|
'_staticPathFull': this._config.set.staticPathFull,
|
|
305
305
|
'_locale': this._locale,
|
|
306
|
-
'_localeData':
|
|
306
|
+
'_localeData': { ...(this._localeData[this._locale] ?? {}) },
|
|
307
307
|
};
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (opt.hydrate !== false) {
|
|
312
|
-
const reactVer = opt.reactVer ?? '19';
|
|
313
|
-
const esm = 'https://esm.sh/';
|
|
314
|
-
// --- 检查是否有 npx kebab build 生成的自包含预构建包 ---
|
|
315
|
-
const bundlePath = this._config.const.rootPath + 'stc/' + path + '.page.bundle.js';
|
|
316
|
-
const hasBundle = await lFs.isFile(bundlePath);
|
|
317
|
-
if (opt.router === 'browser') {
|
|
318
|
-
// --- BrowserRouter 模式:_routerBase 注入 props,供水合脚本读取 ---
|
|
319
|
-
const base = opt.routerBase ?? '';
|
|
320
|
-
const routerBase = this._config.const.urlBase + base.replace(/^\//, '');
|
|
321
|
-
fullProps['_routerBase'] = routerBase.replace(/\/$/, '');
|
|
322
|
-
}
|
|
323
|
-
// --- propsJson 在渲染前序列化,框架直接注入 HTML,组件无需手动渲染 ---
|
|
324
|
-
const propsJson = lText.stringifyJson(fullProps).replace(/<\/script>/gi, '<\\/script>');
|
|
325
|
-
let hydrateScript;
|
|
326
|
-
if (hasBundle) {
|
|
327
|
-
// --- bundle 模式:bundle 自包含 React + 水合逻辑,无需 import map ---
|
|
328
|
-
const clientUrl = `${staticPath}${path}.page.bundle.js?v=${this._config.set.staticVer}`;
|
|
329
|
-
hydrateScript = `import'${clientUrl}';`;
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
// --- 开发模式(tsc 编译 .js):通过 esm.sh import map 解析 bare import ---
|
|
333
|
-
const clientUrl = `${staticPath}${path}.page.js?v=${this._config.set.staticVer}`;
|
|
334
|
-
// --- 内置 import map 条目(React 生态核心包)---
|
|
335
|
-
const builtinImports = {
|
|
336
|
-
'react': `${esm}react@${reactVer}`,
|
|
337
|
-
'react-dom': `${esm}react-dom@${reactVer}`,
|
|
338
|
-
'react-dom/client': `${esm}react-dom@${reactVer}/client`,
|
|
339
|
-
'react/jsx-runtime': `${esm}react@${reactVer}/jsx-runtime`,
|
|
340
|
-
'react-router-dom': `${esm}react-router-dom@7?external=react,react-dom`,
|
|
341
|
-
};
|
|
342
|
-
// --- 自动扫描入口 JS 及其相对引用,收集所有第三方 bare specifier ---
|
|
343
|
-
const scannedFiles = new Set();
|
|
344
|
-
const extraImports = new Set();
|
|
345
|
-
await this._scanImports(componentPath, scannedFiles, extraImports, builtinImports);
|
|
346
|
-
// --- 第三方包统一通过 esm.sh 解析,external react/react-dom 避免重复加载 ---
|
|
347
|
-
for (const pkg of extraImports) {
|
|
348
|
-
builtinImports[pkg] = `${esm}${pkg}?external=react,react-dom`;
|
|
349
|
-
}
|
|
350
|
-
// --- import map 注入到 </head> 前 ---
|
|
351
|
-
headInject = `<script type="importmap">${lText.stringifyJson({ 'imports': builtinImports })}</script>`;
|
|
352
|
-
// --- BrowserRouter 模式多一段 Router 导入与包裹层 ---
|
|
353
|
-
const routerImport = opt.router === 'browser' ? `import{BrowserRouter}from'react-router-dom';` : '';
|
|
354
|
-
const routerCreate = opt.router === 'browser'
|
|
355
|
-
? `createElement(BrowserRouter,{basename:p._routerBase},createElement(App,p))`
|
|
356
|
-
: `createElement(App,p)`;
|
|
357
|
-
hydrateScript =
|
|
358
|
-
`import{hydrateRoot}from'react-dom/client';` +
|
|
359
|
-
`import{createElement}from'react';` +
|
|
360
|
-
routerImport +
|
|
361
|
-
`import App from'${clientUrl}';` +
|
|
362
|
-
`const p=JSON.parse(document.getElementById('__kebab_props__').textContent);` +
|
|
363
|
-
`hydrateRoot(document,${routerCreate});`;
|
|
364
|
-
}
|
|
365
|
-
// --- props JSON + 水合脚本注入到 </body> 前 ---
|
|
366
|
-
bodyInject =
|
|
367
|
-
`<script id="__kebab_props__" type="application/json">${propsJson}</script>` +
|
|
368
|
-
`<script type="module">${hydrateScript}</script>`;
|
|
308
|
+
if (opt.router === 'browser') {
|
|
309
|
+
const routerBase = `${this._config.const.urlBase}${(opt.routerBase ?? '').replace(/^\//, '')}`;
|
|
310
|
+
fullProps['_routerBase'] = routerBase.replace(/\/$/, '');
|
|
369
311
|
}
|
|
370
|
-
// --- BrowserRouter
|
|
371
|
-
|
|
372
|
-
|
|
312
|
+
// --- 创建页面元素,BrowserRouter 模式下由 StaticRouter 包裹 ---
|
|
313
|
+
const component = page.default;
|
|
314
|
+
const pageElement = react.createElement(component, fullProps);
|
|
315
|
+
let element = pageElement;
|
|
373
316
|
if (opt.router === 'browser') {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
element = react.createElement(lReactRouter.StaticRouter, {
|
|
378
|
-
'location': reqUrl,
|
|
317
|
+
const reactRouter = await import('react-router-dom');
|
|
318
|
+
element = react.createElement(reactRouter.StaticRouter, {
|
|
319
|
+
'location': this._req.url ?? '/',
|
|
379
320
|
'basename': fullProps['_routerBase'],
|
|
380
|
-
},
|
|
321
|
+
}, pageElement);
|
|
381
322
|
}
|
|
382
|
-
// ---
|
|
383
|
-
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
html = html.replace('</body>', bodyInject + '</body>');
|
|
323
|
+
// --- 渲染完整 HTML,并按需注入客户端水合数据与 bundle ---
|
|
324
|
+
const html = `<!DOCTYPE html>${reactDomServer.renderToString(element)}`;
|
|
325
|
+
if (!hydrate) {
|
|
326
|
+
return html;
|
|
387
327
|
}
|
|
388
|
-
|
|
328
|
+
const propsJson = lText.stringifyJson(fullProps).replace(/<\/script>/gi, '<\\/script>');
|
|
329
|
+
const clientUrl = `${staticPath}${path}.page.bundle.js?v=${this._config.set.staticVer}`;
|
|
330
|
+
const propsScript = `<script id="__kebab_props__" type="application/json">${propsJson}</script>`;
|
|
331
|
+
const hydrateScript = `<script type="module">import'${clientUrl}';</script>`;
|
|
332
|
+
return html.replace('</body>', `${propsScript}${hydrateScript}</body>`);
|
|
389
333
|
}
|
|
390
334
|
catch (e) {
|
|
391
|
-
|
|
392
|
-
lCore.log(this, '[CTR][_loadReactPage] ' + lText.stringifyError(e), '-error');
|
|
393
|
-
return '';
|
|
335
|
+
return this._reactPageError(e.message ?? '', lText.stringifyError(e));
|
|
394
336
|
}
|
|
395
337
|
}
|
|
396
338
|
/**
|
|
397
|
-
* ---
|
|
398
|
-
* @param
|
|
399
|
-
* @param
|
|
400
|
-
* @
|
|
401
|
-
* @param builtinImports 内置 import map,已有条目不重复添加
|
|
339
|
+
* --- 记录 React 页面错误并返回脱敏响应 ---
|
|
340
|
+
* @param message 调试信息
|
|
341
|
+
* @param detail 日志详情
|
|
342
|
+
* @returns 脱敏后的 500 页面
|
|
402
343
|
*/
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const src = await lFs.getContent(filePath, 'utf8');
|
|
409
|
-
if (!src) {
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
const re = /\bfrom\s*['"]([^'"]+)['"]/g;
|
|
413
|
-
let m;
|
|
414
|
-
while ((m = re.exec(src)) !== null) {
|
|
415
|
-
const spec = m[1];
|
|
416
|
-
if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
417
|
-
// --- 相对引用:解析为绝对路径后递归扫描 ---
|
|
418
|
-
const dir = filePath.substring(0, filePath.lastIndexOf('/') + 1);
|
|
419
|
-
const resolved = new URL(spec, 'file://' + dir).pathname;
|
|
420
|
-
await this._scanImports(resolved, scannedFiles, extraImports, builtinImports);
|
|
421
|
-
}
|
|
422
|
-
else if (!spec.startsWith('/') && !spec.startsWith('http') && !(spec in builtinImports)) {
|
|
423
|
-
// --- 第三方 bare specifier:加入 import map ---
|
|
424
|
-
extraImports.add(spec);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
344
|
+
_reactPageError(message, detail = message) {
|
|
345
|
+
lCore.debug(`[CTR][_loadReactPage] ${message}`);
|
|
346
|
+
lCore.log(this, `[CTR][_loadReactPage] ${detail}`, '-error');
|
|
347
|
+
this._httpCode = 500;
|
|
348
|
+
return '<h1>500 Server Error</h1><hr>Kebab';
|
|
427
349
|
}
|
|
428
350
|
/**
|
|
429
351
|
* --- 设置校验错误返回值 ---
|
|
@@ -854,13 +776,15 @@ export class Ctr {
|
|
|
854
776
|
return json;
|
|
855
777
|
}
|
|
856
778
|
/**
|
|
857
|
-
* ---
|
|
779
|
+
* --- 跳转,支持相对本项目根路径的路径或绝对路径 ---
|
|
858
780
|
* @param location 相对或绝对网址
|
|
781
|
+
* @param httpCode HTTP 重定向状态码,默认 302
|
|
859
782
|
*/
|
|
860
|
-
_location(location) {
|
|
783
|
+
_location(location, httpCode = 302) {
|
|
784
|
+
this._httpCode = httpCode;
|
|
861
785
|
if (this._res) {
|
|
862
786
|
this._res.setHeader('location', lText.urlResolve(this._config.const.urlBase, location));
|
|
863
|
-
// this._res.writeHead(
|
|
787
|
+
// this._res.writeHead(httpCode); Kebab 中要在最后设置,否则会报错:ERR_HTTP_HEADERS_SENT
|
|
864
788
|
}
|
|
865
789
|
return false;
|
|
866
790
|
}
|
package/sys/route.js
CHANGED
|
@@ -581,6 +581,8 @@ export async function run(data) {
|
|
|
581
581
|
data.res.setHeader('expires', new Date(Date.now() + cacheTTL * 1_000).toUTCString());
|
|
582
582
|
data.res.setHeader('cache-control', 'max-age=' + cacheTTL.toString());
|
|
583
583
|
}
|
|
584
|
+
// --- 记录控制器是否显式设置状态码;未设置时,带 Location 的空响应仍兼容为 302 ---
|
|
585
|
+
const hasCustomHttpCode = httpCode !== 0;
|
|
584
586
|
// --- 设置自定义 hcode ---
|
|
585
587
|
if (httpCode === 0) {
|
|
586
588
|
httpCode = 200;
|
|
@@ -591,7 +593,7 @@ export async function run(data) {
|
|
|
591
593
|
// --- 已经自行输出过 writeHead,可能自行处理了内容,如 pipe,则不再 writeHead ---
|
|
592
594
|
}
|
|
593
595
|
else {
|
|
594
|
-
lCore.writeHead(data.res, data.res.getHeader('location') ? 302 : httpCode);
|
|
596
|
+
lCore.writeHead(data.res, data.res.getHeader('location') && !hasCustomHttpCode ? 302 : httpCode);
|
|
595
597
|
}
|
|
596
598
|
if (!data.res.writableEnded) {
|
|
597
599
|
// --- 如果当前还没结束,则强制关闭连接,一切 pipe 请自行在方法中 await,否则会被中断 ---
|
package/www/example/ctr/test.js
CHANGED
|
@@ -2021,8 +2021,8 @@ error: ${JSON.stringify(res.error)}
|
|
|
2021
2021
|
}
|
|
2022
2022
|
async undiciPost() {
|
|
2023
2023
|
const echo = [];
|
|
2024
|
-
const res = await lUndici.post(this._internalUrl + 'test/
|
|
2025
|
-
echo.push(`<pre>lUndici.post('${this._internalUrl}test/
|
|
2024
|
+
const res = await lUndici.post(this._internalUrl + 'test/undiciPost1', { 'a': '1', 'b': '2', 'c': ['1', '2', '3'] });
|
|
2025
|
+
echo.push(`<pre>lUndici.post('${this._internalUrl}test/undiciPost1', { 'a': '1', 'b': '2', 'c': ['1', '2', '3'] });</pre>
|
|
2026
2026
|
headers: <pre>${JSON.stringify(res.headers, null, 4)}</pre>
|
|
2027
2027
|
content: <pre>${(await res.getContent())?.toString() ?? 'null'}</pre>
|
|
2028
2028
|
error: ${JSON.stringify(res.error)}`);
|
|
@@ -2033,8 +2033,8 @@ error: ${JSON.stringify(res.error)}`);
|
|
|
2033
2033
|
}
|
|
2034
2034
|
async undiciPostString() {
|
|
2035
2035
|
const echo = [];
|
|
2036
|
-
const res = await lUndici.post(this._internalUrl + 'test/
|
|
2037
|
-
echo.push(`<pre>lUndici.post('${this._internalUrl}test/
|
|
2036
|
+
const res = await lUndici.post(this._internalUrl + 'test/undiciPostString1', 'HeiHei');
|
|
2037
|
+
echo.push(`<pre>lUndici.post('${this._internalUrl}test/undiciPostString1', 'HeiHei');</pre>
|
|
2038
2038
|
headers: <pre>${JSON.stringify(res.headers, null, 4)}</pre>
|
|
2039
2039
|
content: <pre>${(await res.getContent())?.toString() ?? 'null'}</pre>
|
|
2040
2040
|
error: ${JSON.stringify(res.error)}`);
|
|
@@ -2045,8 +2045,8 @@ error: ${JSON.stringify(res.error)}`);
|
|
|
2045
2045
|
}
|
|
2046
2046
|
async undiciOpen() {
|
|
2047
2047
|
const echo = [];
|
|
2048
|
-
const res = await lUndici.open(this._internalUrl + 'test/
|
|
2049
|
-
echo.push(`<pre>lUndici.open('${this._internalUrl}test/
|
|
2048
|
+
const res = await lUndici.open(this._internalUrl + 'test/undiciPost1').post().data({ 'a': '2', 'b': '0', 'c': ['0', '1', '3'] }).request();
|
|
2049
|
+
echo.push(`<pre>lUndici.open('${this._internalUrl}test/undiciPost1').post().data({ 'a': '2', 'b': '0', 'c': ['0', '1', '3'] }).request();</pre>
|
|
2050
2050
|
headers: <pre>${JSON.stringify(res.headers, null, 4)}</pre>
|
|
2051
2051
|
content: <pre>${(await res.getContent())?.toString() ?? 'null'}</pre>
|
|
2052
2052
|
error: ${JSON.stringify(res.error)}`);
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* 1. 这个 .page.tsx 文件(组件 + IProps)
|
|
6
6
|
* 2. 一个 <link> 标签引入 CSS(如果不需要样式可省略)
|
|
7
7
|
* 3. 在 Ctr 方法里调用 _loadReactPage('view/hello', props)
|
|
8
|
-
*
|
|
8
|
+
* 4. 执行 node ./source/main build -d source/www/example/stc 生成客户端水合 bundle
|
|
9
|
+
* 其他一切由框架自动处理:SSR 渲染、props 注入、客户端水合。
|
|
9
10
|
*/
|
|
10
11
|
import { type ReactElement } from 'react';
|
|
11
12
|
interface IProps {
|
|
@@ -6,7 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* 1. 这个 .page.tsx 文件(组件 + IProps)
|
|
7
7
|
* 2. 一个 <link> 标签引入 CSS(如果不需要样式可省略)
|
|
8
8
|
* 3. 在 Ctr 方法里调用 _loadReactPage('view/hello', props)
|
|
9
|
-
*
|
|
9
|
+
* 4. 执行 node ./source/main build -d source/www/example/stc 生成客户端水合 bundle
|
|
10
|
+
* 其他一切由框架自动处理:SSR 渲染、props 注入、客户端水合。
|
|
10
11
|
*/
|
|
11
12
|
import { useState } from 'react';
|
|
12
13
|
export default function HelloPage({ _urlStc, _staticVer, greeting }) {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* 1. 这个 .page.tsx 文件(组件 + IProps)
|
|
6
6
|
* 2. 一个 <link> 标签引入 CSS(如果不需要样式可省略)
|
|
7
7
|
* 3. 在 Ctr 方法里调用 _loadReactPage('view/hello', props)
|
|
8
|
-
*
|
|
8
|
+
* 4. 执行 node ./source/main build -d source/www/example/stc 生成客户端水合 bundle
|
|
9
|
+
* 其他一切由框架自动处理:SSR 渲染、props 注入、客户端水合。
|
|
9
10
|
*/
|
|
10
11
|
import { useState, type ReactElement } from 'react';
|
|
11
12
|
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
* 3. 嵌套路由通过 Outlet 渲染子路由内容
|
|
16
16
|
*
|
|
17
17
|
* 【编译方式】
|
|
18
|
-
* tsc watch 会自动编译本文件为同路径的 .js
|
|
19
|
-
*
|
|
18
|
+
* tsc watch 会自动编译本文件为同路径的 .js,供服务端 SSR 使用。
|
|
19
|
+
* 客户端水合必须执行:node ./source/main build -d source/www/example/stc
|
|
20
20
|
*
|
|
21
21
|
* 【Tailwind CSS 构建】
|
|
22
22
|
* 需提前执行 node ./source/main build -d source/www/example/stc 生成 CSS 产物。
|
|
23
23
|
* 框架通过 _staticPath 和 _staticVer 自动拼接带版本号 URL;
|
|
24
|
-
*
|
|
24
|
+
* props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
|
|
25
25
|
*/
|
|
26
26
|
interface IUser {
|
|
27
27
|
'id': string;
|
|
@@ -16,13 +16,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
16
16
|
* 3. 嵌套路由通过 Outlet 渲染子路由内容
|
|
17
17
|
*
|
|
18
18
|
* 【编译方式】
|
|
19
|
-
* tsc watch 会自动编译本文件为同路径的 .js
|
|
20
|
-
*
|
|
19
|
+
* tsc watch 会自动编译本文件为同路径的 .js,供服务端 SSR 使用。
|
|
20
|
+
* 客户端水合必须执行:node ./source/main build -d source/www/example/stc
|
|
21
21
|
*
|
|
22
22
|
* 【Tailwind CSS 构建】
|
|
23
23
|
* 需提前执行 node ./source/main build -d source/www/example/stc 生成 CSS 产物。
|
|
24
24
|
* 框架通过 _staticPath 和 _staticVer 自动拼接带版本号 URL;
|
|
25
|
-
*
|
|
25
|
+
* props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
|
|
26
26
|
*/
|
|
27
27
|
import { useState, useEffect } from 'react';
|
|
28
28
|
import { Routes, Route, Link, NavLink, useParams, useNavigate, useLocation, Outlet } from 'react-router-dom';
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
* 3. 嵌套路由通过 Outlet 渲染子路由内容
|
|
16
16
|
*
|
|
17
17
|
* 【编译方式】
|
|
18
|
-
* tsc watch 会自动编译本文件为同路径的 .js
|
|
19
|
-
*
|
|
18
|
+
* tsc watch 会自动编译本文件为同路径的 .js,供服务端 SSR 使用。
|
|
19
|
+
* 客户端水合必须执行:node ./source/main build -d source/www/example/stc
|
|
20
20
|
*
|
|
21
21
|
* 【Tailwind CSS 构建】
|
|
22
22
|
* 需提前执行 node ./source/main build -d source/www/example/stc 生成 CSS 产物。
|
|
23
23
|
* 框架通过 _staticPath 和 _staticVer 自动拼接带版本号 URL;
|
|
24
|
-
*
|
|
24
|
+
* props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { useState, useEffect } from 'react';
|
|
@@ -336,7 +336,6 @@ export default function ReactRouterPage({
|
|
|
336
336
|
<meta charSet="UTF-8" />
|
|
337
337
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
338
338
|
<title suppressHydrationWarning>{title}</title>
|
|
339
|
-
{/* --- import map 由框架自动注入在此标签前,无需手动添加 --- */}
|
|
340
339
|
{/* --- dev: 需先执行 node ./source/main build -d source/www/example/stc 生成 CSS --- */}
|
|
341
340
|
<link rel="stylesheet" href={`${_urlStc}view/react-router.page.css?v=${_staticVer}`} />
|
|
342
341
|
</head>
|
|
@@ -9,26 +9,22 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 【使用方式】
|
|
11
11
|
* 在 Ctr 方法中调用 _loadReactPage('view/react', { ...props }),
|
|
12
|
-
* 框架自动将
|
|
12
|
+
* 框架自动将 props JSON、水合脚本注入 HTML,组件只写业务逻辑,无需任何框架样板代码。
|
|
13
13
|
*
|
|
14
14
|
* 【多页面 / 共用组件】
|
|
15
15
|
* 将公共 UI(如 Label/Input/Checkbox)放在 stc/lib/ui/ 目录,各页面 import 进来。
|
|
16
16
|
* 这和 shadcn/ui 推荐的 components/ui/ 目录结构完全一致。
|
|
17
|
-
* tsc watch 会自动将所有 .tsx 编译为同路径的 .js
|
|
17
|
+
* tsc watch 会自动将所有 .tsx 编译为同路径的 .js,供服务端 SSR 使用。
|
|
18
18
|
*
|
|
19
19
|
* 【编译方式】
|
|
20
20
|
* 本文件 (.tsx) 已集成到 source/tsconfig.json 的 include 中,
|
|
21
|
-
* 开启 `tsc: watch - source/tsconfig.json`
|
|
21
|
+
* 开启 `tsc: watch - source/tsconfig.json` 即可自动编译服务端文件。
|
|
22
22
|
* 编译输出:stc/view/react.page.js(由 tsc 覆盖写入,勿手动编辑 .js 文件)
|
|
23
|
+
* 客户端水合需执行:node ./source/main build -d source/www/example/stc
|
|
23
24
|
*
|
|
24
25
|
* 【运行时机】
|
|
25
26
|
* - 服务端(Node.js):_loadReactPage() 调用 renderToString(),产出完整 HTML 字符串。
|
|
26
|
-
* -
|
|
27
|
-
* - 两端使用同一份 JS 文件:服务端从磁盘读,浏览器从静态 URL 下载。
|
|
28
|
-
*
|
|
29
|
-
* 【第三方包 import map 自动解析】
|
|
30
|
-
* 开发模式下,框架会自动扫描入口 JS 及其相对引用中的所有 bare specifier,
|
|
31
|
-
* 并通过 esm.sh CDN 自动生成 import map 条目,无需手动配置。
|
|
27
|
+
* - 客户端(浏览器):加载 kebab build 生成的 bundle,由 hydrateRoot 接管 document。
|
|
32
28
|
*
|
|
33
29
|
* 【限制】
|
|
34
30
|
* - 不能包含 Node.js 专属代码(fs/path/lDb 等),数据必须通过 props 传入。
|