深入学习 Project N.E.K.O. 项目的前端开发,包括基于 React 19 + Vite 7 的现代化前端架构和传统 HTML/JS 前端。
Project N.E.K.O. 项目采用混合架构:
frontend/ 目录static/bundles/templates/ 和 static/ 目录💡 提示:关于项目结构,请参考 项目架构与目录结构 - 前端模块。
frontend/
├── src/
│ ├── web/ # SPA 应用入口
│ │ ├── main.tsx # React 挂载点
│ │ ├── App.tsx # 主应用组件
│ │ └── styles.css # 全局样式
│ └── types/ # TypeScript 类型定义
├── packages/ # npm workspaces 子包
│ ├── components/ # UI 组件库
│ │ ├── src/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.css
│ │ │ ├── StatusToast.tsx
│ │ │ ├── StatusToast.css
│ │ │ └── Modal/ # 模态框组件
│ │ │ ├── BaseModal.tsx
│ │ │ ├── AlertDialog.tsx
│ │ │ ├── ConfirmDialog.tsx
│ │ │ ├── PromptDialog.tsx
│ │ │ └── index.tsx
│ │ └── index.ts # 组件导出入口
│ ├── request/ # HTTP 请求库(Axios 封装)
│ │ ├── __tests__/ # 单元测试
│ │ │ ├── requestClient.test.ts
│ │ │ ├── entrypoints.test.ts
│ │ │ └── nativeStorage.test.ts
│ │ ├── coverage/ # 测试覆盖率报告
│ │ ├── createClient.ts
│ │ ├── index.ts # 通用入口
│ │ ├── index.web.ts # Web 端入口(默认实例)
│ │ ├── index.native.ts # React Native 入口
│ │ └── src/
│ │ ├── request-client/ # 请求客户端核心
│ │ │ ├── requestQueue.ts # 请求队列管理
│ │ │ ├── tokenStorage.ts # Token 存储实现
│ │ │ └── types.ts # 类型定义
│ │ └── storage/ # 存储抽象层
│ │ ├── webStorage.ts # localStorage 封装
│ │ ├── nativeStorage.ts # AsyncStorage 封装
│ │ └── types.ts # Storage 接口
│ ├── web-bridge/ # 桥接层(将组件与请求能力暴露到 window)
│ │ ├── src/
│ │ │ ├── index.ts # 桥接函数
│ │ │ └── global.ts # 全局类型声明
│ │ └── vite.config.ts
│ └── common/ # 公共工具与类型
│ └── index.ts
├── scripts/ # 构建辅助脚本
├── vendor/ # 第三方库源文件
│ └── react/ # React/ReactDOM UMD 文件
├── index.html # 开发环境 HTML 模板
├── vite.web.config.ts # Web 应用 Vite 配置
├── tsconfig.json # TypeScript 配置
└── package.json # 工作区根配置
# 进入前端目录
cd frontend
# 安装依赖(会自动安装所有 workspaces 的依赖)
npm install
# 启动 Web 应用开发服务器
npm run dev:web
# 默认访问地址: http://localhost:5173
# 启动 Common 包调试(可选)
npm run dev:common
项目配置了以下路径别名,可在代码中直接使用:
@project_neko/components → packages/components/index.ts@project_neko/common → packages/common/index.ts@project_neko/request → packages/request/index.ts这些别名在 tsconfig.json 和 vite.web.config.ts 中均有配置。
在 packages/components/src/ 目录下创建组件:
// packages/components/src/MyComponent.tsx
import "./MyComponent.css";
type MyComponentProps = {
title: string;
onClick?: () => void;
};
export function MyComponent({ title, onClick }: MyComponentProps) {
return (
<div className="my-component">
<h1>{title}</h1>
{onClick && <button onClick={onClick}>点击</button>}
</div>
);
}
/* packages/components/src/MyComponent.css */
.my-component {
padding: 20px;
background: #f0f0f0;
}
在 packages/components/index.ts 中添加:
export { MyComponent } from "./src/MyComponent";
在 src/web/App.tsx 或其他组件中使用:
import { MyComponent } from "@project_neko/components";
function App() {
return (
<div>
<MyComponent title="我的组件" onClick={() => console.log("点击")} />
</div>
);
}
编辑 src/web/App.tsx 来修改主应用:
import "./styles.css";
import { Button } from "@project_neko/components";
import { createRequestClient, WebTokenStorage } from "@project_neko/request";
// 创建请求客户端
const request = createRequestClient({
baseURL: "http://localhost:48911",
storage: new WebTokenStorage(),
refreshApi: async () => {
// 实现 Token 刷新逻辑
throw new Error("refreshApi not implemented");
},
returnDataOnly: true
});
function App() {
const handleClick = async () => {
try {
const data = await request.get("/api/config/page_config");
console.log("数据:", data);
} catch (err) {
console.error("请求失败", err);
}
};
return (
<main className="app">
<h1>N.E.K.O 前端</h1>
<Button label="获取数据" onClick={handleClick} />
</main>
);
}
export default App;
传统前端位于 templates/ 和 static/ 目录,主要用于 Live2D 展示和基础交互。
💡 提示:关于传统前端结构,请参考 项目架构与目录结构 - HTML 模板 和 静态资源。
templates/
├── index.html # 主页面
├── api_key_settings.html # API 密钥设置
├── chara_manager.html # 角色管理
├── l2d_manager.html # Live2D 管理
└── ... # 其他页面
static/
├── app.js # 主应用逻辑
├── common_ui.js # UI 工具函数
├── common_dialogs.js # 对话框工具
├── i18n-i18next.js # 国际化
├── live2d-*.js # Live2D 相关脚本
├── libs/ # 第三方库
│ ├── live2d.min.js
│ ├── pixi.min.js
│ └── i18next.min.js
└── icons/ # 图标资源
编辑 templates/index.html 或其他模板文件:
<div id="my-section">
<h1>我的内容</h1>
<button id="my-button">点击</button>
</div>
<script>
// 在页面底部添加脚本
document.getElementById('my-button').addEventListener('click', () => {
console.log('按钮被点击');
});
</script>
编辑 static/app.js 或创建新文件:
// 使用全局工具函数
window.showDialog('提示', '这是一个对话框');
// 使用国际化
const text = window.i18next.t('app.title');
// Live2D 相关操作
if (window.live2dModel) {
window.live2dModel.setExpression('happy');
}
如果需要在传统前端中使用 React 组件,可以引用构建后的 UMD 格式:
<!-- 在 HTML 模板中引入 -->
<script src="/static/bundles/react.production.min.js"></script>
<script src="/static/bundles/react-dom.production.min.js"></script>
<link rel="stylesheet" href="/static/bundles/components.css" />
<script src="/static/bundles/components.js"></script>
<script type="module">
// 使用全局变量访问组件
const { Button } = ProjectNekoComponents;
const { createRoot } = ReactDOM;
// 挂载组件
const root = createRoot(document.getElementById('react-root'));
root.render(React.createElement(Button, {
label: '点击我',
onClick: () => alert('点击了按钮')
}));
</script>
使用函数组件和 TypeScript:
// packages/components/src/Button.tsx
import "./Button.css";
type ButtonProps = {
label: string;
onClick?: () => void;
disabled?: boolean;
};
export function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button
className="btn"
type="button"
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
import { useState, useEffect } from "react";
export function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `计数: ${count}`;
}, [count]);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
</div>
);
}
在 packages/common/ 中创建自定义 Hook:
// packages/common/hooks/useApi.ts
import { useState, useEffect } from "react";
import { request } from "@project_neko/request";
export function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
request
.get<T>(url)
.then((data) => {
setData(data);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
在 packages/common/index.ts 中导出:
export { useApi } from "./hooks/useApi";
使用:
import { useApi } from "@project_neko/common";
function MyComponent() {
const { data, loading, error } = useApi("/api/endpoint");
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}
在 packages/components/index.ts 中统一导出:
export { Button } from "./src/Button";
export { Counter } from "./src/Counter";
// ... 其他组件
N.E.K.O 项目提供了基于 Axios 封装的请求库,支持:
createRequestClient(options) 支持以下配置:
| 选项 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
baseURL |
string |
✅ | - | API 基础 URL |
storage |
TokenStorage |
✅ | - | Token 存储实现 |
refreshApi |
TokenRefreshFn |
✅ | - | Token 刷新函数 |
timeout |
number |
- | 15000 |
请求超时时间(毫秒) |
requestInterceptor |
Function |
- | - | 自定义请求拦截器 |
responseInterceptor |
Object |
- | - | 自定义响应拦截器 |
returnDataOnly |
boolean |
- | true |
是否只返回 response.data |
errorHandler |
Function |
- | - | 自定义错误处理器 |
logEnabled |
boolean |
- | auto | 是否启用请求日志 |
请求日志的启用优先级:
config.logEnabled(配置项覆盖)globalThis.NEKO_REQUEST_LOG_ENABLED(全局变量)import.meta.env.MODE(构建模式,development 时启用)方法一:使用默认实例(推荐)
import { request } from "@project_neko/request";
function MyComponent() {
const fetchData = async () => {
try {
// 默认实例已配置 baseURL: "/api" 和 Token 刷新
const data = await request.get("/config/page_config", {
params: { lanlan_name: "test" }
});
console.log("数据:", data);
} catch (error) {
console.error("请求失败:", error);
}
};
return <button onClick={fetchData}>获取数据</button>;
}
方法二:创建自定义实例
import { createRequestClient, WebTokenStorage } from "@project_neko/request";
// 创建自定义请求客户端
const customRequest = createRequestClient({
baseURL: "http://localhost:48911",
storage: new WebTokenStorage(),
refreshApi: async (refreshToken: string) => {
// 实现 Token 刷新逻辑
const response = await fetch("/api/auth/refresh", {
method: "POST",
body: JSON.stringify({ refreshToken }),
headers: { "Content-Type": "application/json" }
});
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token
};
},
returnDataOnly: true, // 只返回 data,不返回完整 response
logEnabled: true // 强制启用日志(可选)
});
// 使用自定义实例
const data = await customRequest.get("/api/endpoint");
传统前端可以直接使用构建后的 UMD 格式:
<!-- 在 HTML 模板中引入 -->
<script src="/static/bundles/request.js"></script>
<script>
// 使用全局变量
const request = ProjectNekoRequest.createRequestClient({
baseURL: "/api",
storage: new ProjectNekoRequest.WebTokenStorage(),
returnDataOnly: true
});
request.get("/api/endpoint")
.then(data => {
console.log("数据:", data);
})
.catch(error => {
console.error("请求失败:", error);
});
</script>
请求客户端支持所有 Axios 方法:
// GET 请求
const data = await request.get("/api/endpoint", {
params: { key: "value" }
});
// POST 请求
const result = await request.post("/api/endpoint", {
body: { key: "value" }
});
// PUT 请求
await request.put("/api/endpoint", { data: { key: "value" } });
// DELETE 请求
await request.delete("/api/endpoint");
桥接层(@project_neko/web-bridge)将 React 组件和请求能力暴露到 window 对象,供非 React 代码使用。
StatusToast 绑定到 window.showStatusToast()Modal 绑定到 window.showAlert()、window.showConfirm()、window.showPrompt()window.request,并提供 URL 构建工具<!-- 1. 引入请求库(不依赖 React,可先加载) -->
<script src="/static/bundles/request.js"></script>
<!-- 2. 引入桥接层(封装全局工具,自动绑定 window.request) -->
<script src="/static/bundles/web-bridge.js"></script>
<!-- 3. 引入 React/ReactDOM UMD(组件库依赖) -->
<script src="/static/bundles/react.production.min.js"></script>
<script src="/static/bundles/react-dom.production.min.js"></script>
<!-- 4. 引入组件库样式 -->
<link rel="stylesheet" href="/static/bundles/components.css">
<!-- 5. 引入组件库 UMD -->
<script src="/static/bundles/components.js"></script>
window.showStatusToast(message, duration);
// 示例: window.showStatusToast("操作成功", 3000);
// Alert(警告框)
await window.showAlert(message, title);
// Confirm(确认框)
const ok = await window.showConfirm(message, title, {
okText: "确定",
cancelText: "取消",
danger: false
});
// Prompt(输入框)
const value = await window.showPrompt(message, defaultValue, title);
// GET 请求
const data = await window.request.get("/api/endpoint", { params: { key: value } });
// POST 请求
const result = await window.request.post("/api/endpoint", { body: data });
// URL 构建
const apiUrl = window.buildApiUrl("/api/endpoint");
const staticUrl = window.buildStaticUrl("/static/resource.png");
const wsUrl = window.buildWebSocketUrl("/ws/chat");
// 请求客户端就绪
window.addEventListener('requestReady', () => {
console.log('window.request 已可用');
});
// 状态提示组件就绪
window.addEventListener('statusToastReady', () => {
console.log('window.showStatusToast 已可用');
});
// Modal 组件就绪
window.addEventListener('modalReady', () => {
console.log('window.showAlert/showConfirm/showPrompt 已可用');
});
在组件目录下创建同名的 CSS 文件:
/* packages/components/src/MyComponent.css */
.my-component {
padding: 20px;
background: #f0f0f0;
}
.my-component__title {
font-size: 24px;
color: #333;
}
在组件中导入:
// packages/components/src/MyComponent.tsx
import "./MyComponent.css";
export function MyComponent() {
return (
<div className="my-component">
<h1 className="my-component__title">标题</h1>
</div>
);
}
const style: React.CSSProperties = {
padding: "20px",
backgroundColor: "#f0f0f0"
};
<div style={style}>内容</div>
在 src/web/styles.css 中定义全局样式:
/* src/web/styles.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
在 src/web/App.tsx 中导入:
import "./styles.css";
请求库包含完整的单元测试套件,使用 Vitest 编写:
# 运行请求库测试
cd frontend/packages/request && npm test
# 或在 frontend 目录下使用 workspace 命令
cd frontend && npm run test -w @project_neko/request
生成测试覆盖率报告:
cd frontend/packages/request && npx vitest run --coverage
覆盖率报告将生成到 packages/request/coverage/ 目录。
| 文件 | 描述 |
|---|---|
requestClient.test.ts |
请求客户端核心功能测试(Token 刷新、请求队列、拦截器等) |
entrypoints.test.ts |
入口文件导出测试(index.ts、index.web.ts、index.native.ts) |
nativeStorage.test.ts |
React Native 存储抽象测试 |
使用 Vitest 编写测试:
// packages/request/__tests__/myTest.test.ts
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { createRequestClient } from "../createClient";
import type { TokenStorage } from "../src/request-client/types";
// 创建内存存储用于测试
const createMemoryStorage = (): TokenStorage => {
let accessToken: string | null = null;
let refreshToken: string | null = null;
return {
async getAccessToken() { return accessToken; },
async setAccessToken(token: string) { accessToken = token; },
async getRefreshToken() { return refreshToken; },
async setRefreshToken(token: string) { refreshToken = token; },
async clearTokens() { accessToken = null; refreshToken = null; }
};
};
describe("我的测试", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("应该正常工作", async () => {
const storage = createMemoryStorage();
const client = createRequestClient({
baseURL: "/api",
storage,
refreshApi: vi.fn()
});
expect(client).toBeDefined();
});
});
项目支持两种构建模式:
build:dev):用于本地开发和调试
build:prod):用于生产环境部署
执行完整构建流程:
cd frontend
npm run build
# 或
npm run build:prod
执行开发构建:
cd frontend
npm run build:dev
构建流程依次执行:
clean:bundles:清空 static/bundles 目录build:request:构建请求库,产出 ES/UMD 双格式build:common:构建通用工具包,产出 ES/UMD 双格式build:components:构建组件库,产出 ES/UMD 双格式,外部化 react/react-dombuild:web-bridge:构建桥接层,产出 ES/UMD 双格式build:web:构建 Web 应用入口,生成 react_web.js(ES 模块)copy:react-umd:复制 React UMD 文件到 static/bundles主要产物位于 static/bundles/(仓库根目录):
components.js / components.es.js:组件库(UMD/ES 格式)components.css:组件库样式文件common.js / common.es.js:通用工具(UMD/ES 格式)request.js / request.es.js:请求库(UMD/ES 格式)web-bridge.js / web-bridge.es.js:桥接层(UMD/ES 格式)react.production.min.js:React 生产环境 UMDreact-dom.production.min.js:ReactDOM 生产环境 UMDdist/webapp/(frontend 目录下):
react_web.js:SPA 入口(ESM 格式)frontend.css:Web 应用样式文件在服务端模板中按以下顺序引用构建产物:
<!-- 1. 引入请求库(不依赖 React) -->
<script src="/static/bundles/request.js"></script>
<!-- 2. 引入桥接层(自动绑定 window.request) -->
<script src="/static/bundles/web-bridge.js"></script>
<!-- 3. 引入 React/ReactDOM UMD(组件库依赖) -->
<script src="/static/bundles/react.production.min.js"></script>
<script src="/static/bundles/react-dom.production.min.js"></script>
<!-- 4. 引入组件库样式 -->
<link rel="stylesheet" href="/static/bundles/components.css" />
<!-- 5. 引入组件库 UMD(依赖全局 React/ReactDOM) -->
<script src="/static/bundles/components.js"></script>
确保页面中存在 <div id="root"></div> 作为挂载点(使用 SPA 时)。
可以单独构建某个包:
# 构建组件库
npm run build:components # 生产模式
npm run build:components:dev # 开发模式
# 构建请求库
npm run build:request
npm run build:request:dev
# 构建通用工具
npm run build:common
npm run build:common:dev
# 构建桥接层
npm run build:web-bridge
npm run build:web-bridge:dev
# 构建 Web 应用
npm run build:web
npm run build:web:dev
安装 React Developer Tools 浏览器扩展,可以:
console.log("调试信息", variable);
console.error("错误信息", error);
console.table(data); // 表格形式显示数据
console.group("分组", data); // 分组显示
在代码中添加 debugger; 语句,浏览器会在该处暂停:
function MyComponent() {
debugger; // 浏览器会在此处暂停
return <div>内容</div>;
}
运行类型检查,不生成文件:
cd frontend
npm run typecheck
Vite 提供了丰富的开发工具:
npm installstatic/bundles 目录权限vite.web.config.ts 配置正确npm run typecheck 查看详细错误#root 元素是否存在packages/request 目录下运行测试在开始前端开发之前,建议先阅读:
完成前端开发学习后,可以继续学习:
遇到问题? 在 GitHub Issues 提交你的问题,我们会及时解答!