httpServer HTTP 服务
大约 8 分钟
httpServer HTTP 服务
httpServer 是 JSXHook 脚本内置的本地 HTTP 服务对象。
它解决的不是“我去请求别人”,而是“别人来请求我”。
这一页专门讲这块服务端能力:
- 怎么启动和停止本地服务
- 怎么注册路由
req.query、req.body、req.params到底分别是什么- 路由返回对象到底认哪些字段
token认证到底怎么生效
如果你要主动请求外部接口、调本机接口、调局域网接口,请去看 http HTTP 客户端。
如果你要把能力包装成 MCP 的 Tool / Resource / Prompt,请去看 mcpServer MCP 服务。
先记住这 8 条
httpServer.start()默认监听127.0.0.1:18765。allowLan: true只是帮你把默认 host 改成0.0.0.0。req.headers里的 header 名全部会被转成小写。req.params不是“路径参数专用”,它其实是query + body(map)合并结果。- 同名字段冲突时,
body会覆盖query。 - 路由返回完整响应对象时,真正认的字段是
status,不是statusCode。 mainThread: true只在你确实要碰 UI / Activity 时再开。- 只要你把服务放出本机,最好配
token。
先分清 httpServer / http / mcpServer
httpServer:你自己定义 URL、方法、请求体和返回结构,适合普通本地接口、局域网接口、Webhook、调试回调。http:你主动去请求别人,适合调用现成接口。mcpServer:也是“别人来调你”,但调用方不是按普通 REST 约定来,而是按 MCP 协议发现和调用能力。
你可以这么记:
http是“我出去请求”httpServer是“我开普通接口给别人请求”mcpServer是“我把能力注册成 MCP 能识别的接口给别人请求”
httpServer.start(config)
这个接口用来启动脚本侧本地 HTTP 服务。
支持 2 种写法
1. 只传端口
httpServer.start(18765);
等价于:
httpServer.start({
port: 18765
});
2. 传配置对象
httpServer.start({
port: 18765,
allowLan: false,
token: "demo-token"
});
配置字段
| 字段 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
host | string | IP / 主机地址 | 见下表 | 为空时由 allowLan 决定 |
port | number | 1 ~ 65535 | 18765 | 超出范围会被修正到合法区间 |
allowLan | boolean | true / false | false | 只是 host 的快捷开关 |
token | string | 任意字符串 | "" | 为空表示不鉴权 |
host 的默认规则
| 你的配置 | 实际 host |
|---|---|
没写 host,没写 allowLan | 127.0.0.1 |
没写 host,allowLan: false | 127.0.0.1 |
没写 host,allowLan: true | 0.0.0.0 |
明确写了 host | 按你写的值为准 |
返回值
{
started: true,
reused: false,
host: "127.0.0.1",
port: 18765
}
reused 是什么意思
false:真的新起了一个服务true:同样配置的服务已经在跑,直接复用
httpServer.stop()
返回值
boolean
规则
| 场景 | 返回值 |
|---|---|
| 当前有正在运行的服务并成功停止 | true |
| 当前本来就没有服务 | false |
例子
const stopped = httpServer.stop();
log(stopped);
httpServer.clear()
清空所有已注册路由。
返回值
number
表示这次清掉了多少条路由。
const removedCount = httpServer.clear();
log(removedCount);
httpServer.state()
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
started | boolean | 当前服务是否活着 |
host | string | 当前监听地址 |
port | number | 当前监听端口 |
routeCount | number | 当前已注册路由数量 |
例子
log(JSON.stringify(httpServer.state(), null, 2));
httpServer.listRoutes()
返回值
Array<object>
每一项字段
| 字段 | 类型 | 说明 |
|---|---|---|
method | string | 如 GET、POST、ANY |
path | string | 规范化后的路径 |
mainThread | boolean | 是否在主线程执行 |
例子
log(JSON.stringify(httpServer.listRoutes(), null, 2));
httpServer.remove(path, method?)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
path | string | 路径字符串 | 必填 | 会做规范化处理 |
method | string | HTTP 方法名 | ANY | 不传时按 ANY 删除 |
路径规范化规则
| 你传的值 | 最终路径 |
|---|---|
"ping" | "/ping" |
"/ping" | "/ping" |
"//ping//" | "/ping" |
"/" | "/" |
例子
httpServer.remove("/ping", "GET");
httpServer.remove("/echo");
httpServer.get/post/put/delete/patch/any(path, options?, handler)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
path | string | 路径字符串 | 必填 | 为空会报错 |
options | object | 目前主要是 { mainThread?: boolean } | 空对象 | 可选 |
handler | function | 路由处理函数 | 必填 | 不传会报错 |
options.mainThread
| 值 | 说明 |
|---|---|
true | 在主线程执行,适合要动 UI 的逻辑 |
false | 默认值,在普通工作线程执行 |
什么时候该开 mainThread: true
- 你要
toast(...) - 你要碰 Activity / View / UI
- 你调用的某些宿主 API 明确要求主线程
最简单写法
httpServer.get("/ping", req => {
return "pong";
});
带主线程选项
httpServer.post("/invoke", { mainThread: true }, req => {
toast("called from http");
return {
status: 200,
mimeType: "text/plain; charset=utf-8",
body: "ok"
};
});
req 请求对象
字段总表
| 字段 | 类型 | 说明 |
|---|---|---|
req.method | string | 请求方法 |
req.path | string | 规范化后的路径 |
req.uri | string | 原始 URI |
req.query | object | 只来自 URL 查询参数 |
req.params | object | query + body(map) 合并结果 |
req.body | any | 按 Content-Type 解析后的请求体 |
req.rawBody | string | 原始请求体文本 |
req.headers | object | 所有请求头,key 已转小写 |
req.ip | string | 来源 IP |
req.contentType | string | 请求头里的 Content-Type |
req.packageName | string | 当前脚本目标包名 |
req.processName | string | 当前脚本目标进程名 |
req.param(name) | function | 从 req.params 里快捷取值 |
req.header(name) | function | 按名字取 header,大小写不敏感 |
req.query
只看 URL 查询参数。
如果一个 key 只出现一次,通常是字符串:
// /echo?name=jsxhook
req.query.name === "jsxhook"
如果一个 key 重复出现多次,会变成数组:
// /echo?tag=a&tag=b
req.query.tag === ["a", "b"]
req.body
源码会按 Content-Type 解析:
Content-Type | req.body 会变成什么 |
|---|---|
application/json | 对象 / 数组 / JSON 基本类型;解析失败时退回原始字符串 |
application/x-www-form-urlencoded | map 对象 |
| 其他类型 | 原始字符串 |
| 空 body | null |
注意:
- 只有
POST、PUT、PATCH、DELETE会读取请求体。 GET默认不会去解析 body。
req.params
这个字段最适合“我不关心参数来自 query 还是 body,只想统一取值”的场景。
合并规则是:
- 先放
query - 如果
body是 map,再把body里的字段合进去 - 同名时
body覆盖query
例子:
POST /echo?from=query
Content-Type: application/json
{"from":"body","name":"jsxhook"}
那么:
req.query.from === "query"req.body.from === "body"req.params.from === "body"
req.headers
- 所有 key 都会被转成小写
- 所以直接读时请按小写想
const token = req.headers["x-module-token"];
const contentType = req.headers["content-type"];
req.header(name)
这是最省心的取法:
const token = req.header("X-Module-Token");
const contentType = req.header("content-type");
路由返回值规则
最关键的一句
如果你返回“完整响应对象”,字段名要写:
status
不是:
statusCode
规则 1:返回字符串
httpServer.get("/ping", req => "pong");
实际会自动变成:
| 字段 | 值 |
|---|---|
status | 200 |
mimeType | text/plain; charset=utf-8 |
body | "pong" |
规则 2:返回对象 / 数组 / 其他非字符串
httpServer.get("/info", req => {
return {
ok: true,
packageName: req.packageName
};
});
实际会自动变成:
| 字段 | 值 |
|---|---|
status | 200 |
mimeType | application/json; charset=utf-8 |
body | 自动 JSON 序列化 |
规则 3:返回完整响应对象
可识别字段如下:
| 字段 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
status | number | 任意 HTTP 状态码 | 200 | 注意字段名是 status |
mimeType | string | 任意 MIME | 自动推断 | 空时会根据 body 类型推断 |
body | any | 字符串 / 对象 / 数组 / null | null | JSON 类型会自动序列化 |
headers | object | 普通对象 / map | 空对象 | 所有值都会转成字符串 |
推断 mimeType 的规则
| body 类型 | 默认 mimeType |
|---|---|
string | text/plain; charset=utf-8 |
| 其他 | application/json; charset=utf-8 |
body: null 时会怎样
mimeType 是否包含 json | 实际响应体 |
|---|---|
| 是 | "null" |
| 否 | "" |
推荐写法
httpServer.get("/health", req => {
return {
status: 200,
mimeType: "application/json; charset=utf-8",
body: {
ok: true,
packageName: req.packageName,
processName: req.processName
},
headers: {
"X-Powered-By": "JSXHook"
}
};
});
认证与 token
如果启动服务时写了:
httpServer.start({
port: 18765,
token: "demo-token"
});
那么客户端必须二选一:
- 请求头带上
x-module-token: demo-token - 查询参数带上
?token=demo-token
否则会收到:
401 unauthorized
如果 token 是空字符串
- 完全不校验
- 任何人都能访问
所以只要你把服务暴露给局域网,最好配上 token。
实战例子
1. 本地健康检查路由
httpServer.start({
port: 18765,
allowLan: false
});
httpServer.get("/health", req => {
return {
status: 200,
mimeType: "application/json; charset=utf-8",
body: {
ok: true,
packageName: req.packageName,
processName: req.processName
}
};
});
2. 回显 query / body / params 差异
httpServer.post("/echo", req => {
return {
status: 200,
mimeType: "application/json; charset=utf-8",
body: {
query: req.query,
body: req.body,
params: req.params
}
};
});
3. 带 token 的本地服务
httpServer.start({
port: 18765,
token: "demo-token"
});
httpServer.get("/ping", req => {
return "pong";
});
外部调用时要么带 header:
x-module-token: demo-token
要么带 query:
/ping?token=demo-token
4. 主线程里执行 UI 逻辑
httpServer.post("/invoke", { mainThread: true }, req => {
toast("called from http");
return {
status: 200,
mimeType: "application/json; charset=utf-8",
body: {
ok: true
}
};
});
常见错误与排查
1. 明明返回了 statusCode: 500,客户端却还是 200
原因:
- 路由返回对象里用了
statusCode字段。 - 当前脚本接口认的是
status。
正确写法:
return {
status: 500,
body: { ok: false }
};
2. 为什么 req.headers["Authorization"] 取不到
原因:
- 这里的 header key 已经统一转成小写了。
正确取法:
req.headers["authorization"];
// 或者
req.header("Authorization");
3. 为什么 req.params 和 req.query 不一样
原因:
req.query只看 URL 查询参数。req.params会把query和body(map)合并。
4. 为什么局域网设备访问不到
原因通常有这几种:
- 你没有开
allowLan: true - 你虽然开了服务,但系统网络环境本身没通
- 你开了
token,但外部调用没带认证
实战建议
- 只在需要时才对外暴露端口,默认本机调试最省心。
- 凡是
allowLan: true的场景,最好顺手加上token。 - 需要区分参数来源就分别看
query/body,只想统一取值再看params。 - 需要动 UI 的路由明确开
mainThread: true。 - 返回完整响应对象时,先记住字段名是
status。
