nb-web-cli
v0.0.21
Published
基于 Next.js 16的前端脚手架 nb-web-cli
Readme
nb-web-cli
基于 Next.js 16 App Router 的前端脚手架。通过命令行一键创建项目模板并安装依赖。
前置准备
- Node.js >= 20.9
- npm 镜像源:需配置为公司私有源
npm config set registry https://registry.npmjs.org使用方式
# 全局安装(或 npx 直接运行)
npm install -g nb-web-cli
# 创建新项目(在当前空目录,自定义项目名称为 my-project)
nb-web-cli my-project
# 创建新项目(在当前空目录,项目名称取当前目录名称)
nb-web-cli
# 跳过依赖安装
nb-web-cli my-project --no-install
# 指定包管理器
nb-web-cli my-project --package-manager pnpm创建完成后,项目中所有 nb-web-demo 占位符(包名、basePath 路由前缀等)会被替换为指定的项目名称。
本地开发(维护脚手架)
# 1. 安装 CLI 依赖(根目录)
npm install
# 2. 安装模板依赖并启动(packages/ 必须单独 install)
cd packages && npm install && npm run dev
# 或在 packages/ 已 install 的前提下,根目录执行:
npm run dev
# 3. 本地联调 CLI
npm link
mkdir /tmp/test && cd /tmp/test
nb-web-cli详细文档
下文为完整的脚手架使用指南。
nb-web-cli 脚手架使用指南
本文档参照 Next.js 官方文档 的 App Router 学习路径,结合模板样例(默认
basePath为/nb-web-demo),说明脚手架的整体设计与日常开发方式。
目录
- CLI 初始化项目
- 快速开始
- 项目结构
- 路由(Routing)
- 布局与页面(Layouts & Pages)
- 服务端与客户端组件
- 数据获取
- 特殊文件:Loading / Error / Not Found
- Proxy 与开发代理
- 环境变量
- 全局状态(Zustand)
- 用户鉴权
- 构建与部署
- 样例页面走读
- 新建业务模块 Checklist
0. CLI 初始化项目
命令说明
| 命令 | 场景 | 项目名来源 | 文件写入位置 |
|------|------|-----------|-------------|
| nb-web-cli | 已 cd 进入空目录 | 当前文件夹名 | 当前目录 |
| nb-web-cli my-app | 已 cd 进入空目录 | 参数 my-app | 当前目录 |
| nb-web-cli my-app --no-install | 同上,跳过依赖安装 | 参数 my-app | 当前目录 |
| nb-web-cli my-app -p pnpm | 指定 pnpm 安装依赖 | 参数 my-app | 当前目录 |
注意: 两种模式都是在当前空目录内初始化,不会在父目录下额外创建子文件夹。目录名本身需符合 npm 包名规范(小写、无空格等)。
初始化流程
- 进入空目录
- 执行
nb-web-cli - 复制
packages/模板到当前目录 - 替换
nb-web-demo占位符(basePath、包名、Proxy 匹配等) npm install(可用--no-install跳过)npm run dev
占位符替换范围
初始化时,所有 nb-web-demo 会被替换为指定项目名,包括但不限于:
| 类型 | 示例 |
|------|------|
| 包名 | package.json → "name": "my-app" |
| 路由前缀 | _constants/urls.ts → basePath = '/my-app' |
| Next.js basePath | next.config.ts → basePath: '/my-app' |
| Proxy 匹配 | proxy.ts → matcher: ['/my-app/:path*'] |
| 构建产物 | build/my-app.tar.gz |
| 部署配置 | Jenkinsfile 中的 name 字段 |
典型用法
# 方式一:目录名即项目名
mkdir my-project && cd my-project
nb-web-cli
npm run dev
# 方式二:自定义项目名(目录名可以不同)
mkdir workspace && cd workspace
nb-web-cli my-app
npm run dev1. 快速开始
对应官方文档:Installation
Next.js 16.2.4
环境要求
- Node.js >= 20.9
- npm / pnpm / yarn
安装与启动
终端用户(已通过 CLI 创建的项目):
# 若初始化时使用了 --no-install,需先安装依赖
npm install
# 本地开发(默认 dev 环境)
npm run dev脚手架维护者(本仓库):
# 根目录:安装 CLI 依赖
npm install
# packages/:安装模板依赖并启动
cd packages && npm install && npm run dev
# 或在根目录(需 packages/ 已 npm install)
npm run dev启动后访问 https://localhost:3000/nb-web-demo(初始化后则为 https://localhost:3000/<你的项目名>)。模板通过 Next.js basePath 配置路由前缀,无需额外的根路径重定向。
开发端口与 HTTPS
默认情况下,npm run dev 使用 HTTPS(自签名证书,浏览器首次访问需信任)、监听 3000 端口,对应 package.json 中的脚本为:
"dev": "cross-env RUNTIME_ENV=dev next dev --experimental-https"如需自定义,修改 package.json 的 scripts.dev 即可。
指定端口
通过环境变量 PORT 或 Next.js 的 -p 参数:
"dev": "cross-env RUNTIME_ENV=dev PORT=4000 next dev --experimental-https""dev": "cross-env RUNTIME_ENV=dev next dev -p 4000 --experimental-https"启动后访问 https://localhost:4000/nb-web-demo。
改用 HTTP
去掉 --experimental-https 即可:
"dev": "cross-env RUNTIME_ENV=dev next dev"启动后访问 http://localhost:3000/nb-web-demo。
常用脚本
| 命令 | 说明 |
|------|------|
| npm run dev | 开发模式,RUNTIME_ENV=dev,启用 API 代理 |
| npm run build | SPA 静态导出(qa + prd 双环境),打包为 build/*.tar.gz |
| npm run build:spa | 同 npm run build |
| npm run build:ssr | SSR 生产构建,产物在 .next/ |
| npm run start | 启动 SSR 生产服务(需先执行 build:ssr) |
| npm run clean | 清理 .next / dist / build / .spa-staging |
| npm run deploy | 同 npm run build,供 CI / pack_frontend.sh 调用 |
| npm run gen:nginx -- <port> | 根据模板生成 QA / PRD 两套 nginx 配置到 nginx/ |
| npm run lint | ESLint 检查 |
2. 项目结构
对应官方文档:Project Structure
脚手架仓库结构
维护模板时,代码位于 packages/ 目录:
nb-web-cli/ # 脚手架仓库(CLI 包)
├── bin/index.mjs # CLI 入口
├── scripts/constants.mjs # 占位符与复制规则
├── package.json # name: nb-web-cli
└── packages/ # Next.js 模板(维护入口)
├── app/
│ ├── layout.tsx # 根布局
│ ├── error.tsx # 全局 Error 边界
│ ├── not-found.tsx # 全局 404 页
│ ├── _api/ # 不以 _ 开头的目录才会成为路由
│ ├── _assets/ # 静态资源(如图标)
│ ├── _components/
│ ├── _constants/
│ ├── _stores/
│ ├── _utils/
│ ├── (protected)/ # 需登录业务页(Route Group)
│ └── (public)/login/ # 公开页(登录,含 loading.tsx)
├── config/
│ ├── env/ # .env.dev / .env.qa / .env.prd
│ └── rewrite-rules.ts # 开发环境 API 代理规则
├── scripts/
│ ├── runtime-env.mjs # 环境变量加载
│ ├── build-spa.mjs # SPA 多环境构建脚本
│ ├── gen-nginx.mjs # nginx 配置生成
│ └── nginx.conf.template # nginx 配置模板
├── Jenkinsfile # CI 部署配置(占位符会被替换)
├── pack_frontend.sh # 前端打包脚本(调用 npm run deploy)
├── proxy.ts
├── next.config.ts # basePath: '/nb-web-demo'
├── tsconfig.json
└── package.json # name: nb-web-demo初始化后的业务项目结构
执行 nb-web-cli 后,用户得到的项目结构与 packages/ 一致(不含 CLI 相关文件):
my-project/ # 用户项目
├── app/
│ ├── layout.tsx
│ ├── error.tsx
│ ├── not-found.tsx
│ ├── _api/
│ ├── _assets/
│ ├── _components/
│ ├── _constants/
│ ├── _stores/
│ ├── _utils/
│ ├── (protected)/ # 业务首页等受保护页面
│ └── (public)/login/
├── config/
├── scripts/
├── Jenkinsfile
├── pack_frontend.sh
├── proxy.ts
├── next.config.ts # basePath: '/my-project'
├── tsconfig.json
└── package.json # name: my-project架构总览
- 浏览器:页面组件、
'use client'客户端组件、Zustand Store - Next.js Server:根布局、受保护布局、
serverFetch服务端请求、proxy.ts请求拦截 - 后端 / BFF:
/node/api/*、/gw-nb/*、/sharedWorker-nb/*等(开发环境通过 rewrites 代理)
客户端组件通过 @sumslack/request-pro 请求 BFF;服务端组件通过 serverFetch 请求同源 BFF;开发环境下 proxy.ts 注入请求头,rewrite-rules.ts 转发 API 请求。
与官方约定的对应关系
| 官方概念 | 本脚手架实现 |
|----------|-------------|
| app/ 目录 | ✅ 使用 App Router |
| layout.tsx | 根布局 + 业务布局(鉴权) |
| page.tsx | 各路由页面 |
| loading.tsx / error.tsx / not-found.tsx | 根级 error.tsx / not-found.tsx;登录页 (public)/login/loading.tsx |
| Route Groups (folder) | (protected) / (public) 分组 |
| basePath | next.config.ts 统一配置路由前缀,业务页面直接放在 app/ 下 |
| proxy.ts(Next 16) | 注入请求来源路径,配合登录回跳 |
| 环境变量 NEXT_PUBLIC_* | config/env/.env.{dev,qa,prd} |
路径别名
tsconfig.json 中配置了 @_* 别名,避免深层相对路径:
"@_api": ["./app/_api"],
"@_stores": ["./app/_stores"],
"@_components": ["./app/_components"],
"@_constants": ["./app/_constants"],
"@_utils": ["./app/_utils"]以
_开头的目录(如_api、_components)不会被 Next.js 当作路由,可安全放在app/下。
3. 路由(Routing)
对应官方文档:Routing
文件系统路由
模板通过 next.config.ts 的 basePath 统一配置路由前缀。Route Group 目录 (protected) / (public) 不会出现在 URL 中:
app/(protected)/page.tsx → /nb-web-demo
app/(public)/login/page.tsx → /nb-web-demo/login初始化后 basePath 替换为项目名,例如 app/(protected)/page.tsx → /my-app。
Route Groups(路由组)
括号文件夹 (protected) / (public) 不会出现在 URL 中,仅用于组织代码和共享布局:
| 路由组 | 文件 | 实际 URL |
|--------|------|----------|
| (protected) | (protected)/page.tsx | /nb-web-demo |
| (public) | (public)/login/page.tsx | /nb-web-demo/login |
| 路由组 | 用途 | 布局行为 |
|--------|------|----------|
| (protected) | 需登录的业务页面 | 包裹 UserProvider + UserGuard |
| (public) | 登录等公开页面 | 无鉴权守卫 |
路由常量
统一在 app/_constants/urls.ts 维护,避免硬编码:
export const userUrl = '/node/api/user';
export const sharedWorkerUrl = '/sharedWorker-nb/SharedWork.v4.0.0.min.js';
export const k8sPrefix = '/node/api/k8s';
export const baseUrl = `${k8sPrefix}/demo`;
export const basePath = '/nb-web-demo'; // Next.js basePath,初始化后替换
export const loginPath = '/login'; // 相对 basePath 的路径,完整 URL 为 /nb-web-demo/login4. 布局与页面(Layouts & Pages)
对应官方文档:Layouts and Pages
布局嵌套关系
app/layout.tsx 根布局:HTML、全局样式、GlobalProviders
└── (protected)/layout.tsx UserProvider + UserGuard
└── page.tsx 业务页面
└── components/test.tsx 客户端组件根布局 app/layout.tsx
职责:
- 设置
<html>/<body>和metadata - 引入全局 SCSS
- 挂载
GlobalProviders(模块顶层初始化initRequestPro,挂载后恢复持久化 Store)
业务布局 (protected)/layout.tsx
职责:
- 用
UserProvider提供用户上下文 - 用
UserGuard校验登录态,未登录则跳转登录页
页面 page.tsx
默认是 Server Component。本样例中页面本身较薄,主要组合子组件:
// app/(protected)/page.tsx
import Test from './components/test';
import Test2 from './components/test2';
export default function NbWebDemo() {
return (
<div className='nb-web-demo'>
<div className='mb-16'>this is a nb-web-demo</div>
<Test text='this is a test component' />
<Test2 />
</div>
);
}5. 服务端与客户端组件
对应官方文档:Server and Client Components
如何选择
- 需要
useState/useEffect/ 事件处理 → 使用'use client'客户端组件 - 需要直接读 cookies / headers / 服务端 fetch → 使用默认的 Server Component
- 其余情况 → 优先 Server Component
| 类型 | 标识 | 本仓库样例 |
|------|------|-----------|
| Server Component | 默认,无需声明 | (protected)/layout.tsx、page.tsx |
| Client Component | 文件顶部 'use client' | test.tsx、UserGuard.tsx、login/page.tsx |
边界原则
- Server → Client:可以把 Client 组件当子组件引入 Server 组件 ✅
- Client → Server:不能把 Server 组件直接 import 进 Client 组件 ❌
- 交互逻辑、浏览器 API、Zustand、ahooks 等放在 Client 组件中
server-only / client-only
app/_utils/server/ 使用 server-only,app/_utils/client/ 使用 client-only,防止在错误的环境中引用:
_utils/
├── server/ # serverFetch、jumpLogin(redirect)
└── client/ # jumpLogin(location)、checkUserInQb6. 数据获取
对应官方文档:Fetching Data
本脚手架区分 客户端请求 和 服务端请求 两套方式:
- 客户端:
@sumslack/request-pro的requestPro→app/_api/ - 服务端:
serverFetch→app/_api/server/ - 两者均请求
/node/api/*等同源 BFF(样例testApi走/node/api/k8s/*)
客户端请求 — @sumslack/request-pro
在 GlobalProviders.tsx 模块顶层通过 initRequestPro 初始化后,Client Component 中使用:
// app/_api/user.ts
import {requestPro} from '@sumslack/request-pro';
export const login = ({username, password}): Promise<IUser> => {
return requestPro({
url: `${userUrl}/login`,
method: 'post',
data: {username, password, gatewayToken: true},
}).then((res) => res.data.content || {});
};
export const getUser = (): Promise<IUser> => {
return requestPro({ url: userUrl, method: 'get' })
.then((res) => res.data.content || {});
};
@sumslack/request-pro基于 axios,业务数据在res.data中;响应拦截器在res.data.status === 401时会自动jumpLogin。
// app/_api/index.ts
export const testApi = (): Promise<any[]> => {
return requestPro({
url: `${k8sPrefix}/bond-market-calendar-v2/api/v1/query-issue`,
method: 'get',
}).then((res) => res.data.text || []);
};test.tsx 中通过 ahooks 的 useRequest 调用 testApi 拉取数据。
服务端请求 — serverFetch
在 Server Component / Server Action 中请求同源 BFF,自动转发 Cookie:
// app/_api/server/user.ts
export const getServerUser = cache(async (): Promise<IUser | null> => {
const res = await serverFetch(userUrl);
if (!res?.ok) return null;
const {status, content} = await res.json();
return status === 401 || isEmpty(content) ? null : content;
});
cache()保证同一请求周期内去重,符合 React 推荐模式。
7. 特殊文件:Loading / Error / Not Found
对应官方文档:
| 文件 | 触发时机 | 样例位置 |
|------|----------|----------|
| loading.tsx | 路由段加载中(Suspense 边界) | (public)/login/loading.tsx |
| error.tsx | 子树抛出未捕获错误 | app/error.tsx(根级,覆盖全部路由) |
| not-found.tsx | 调用 notFound() 或路由不存在 | app/not-found.tsx(根级) |
Error 边界试用
test.tsx 中提供了「点击崩溃」按钮,将 count 设为 undefined 后访问 count.length 会触发错误,由 error.tsx 捕获并展示重试按钮。
8. Proxy 与开发代理
对应官方文档:Proxy(Next.js 16)
proxy.ts — 请求拦截
在匹配路径的请求头中注入来源路径,供服务端 jumpLogin 做登录后回跳:
// proxy.ts
export function proxy(request: NextRequest) {
requestHeaders.set(REQUEST_FROM_PATH_KEY, `${pathname}${search}`);
return NextResponse.next({ request: { headers: requestHeaders } });
}
export const config = {
matcher: ['/nb-web-demo/:path*'],
};config/rewrite-rules.ts — 开发环境 API 转发
仅在 RUNTIME_ENV=dev 时通过 next.config.ts 的 rewrites 生效:
// config/rewrite-rules.ts
export const rewriteRules = [
{ source: '/gw-nb/:path*', destination: 'https://s.sumslack.com/gw-nb/:path*' },
{ source: '/node/api/:path*', destination: 'http://s.sumslack.com/api/:path*' },
{ source: '/sharedWorker-nb/:path*', destination: 'https://s.sumslack.com/sharedWorker-nb/:path*' },
];开发环境下,浏览器请求 /node/api/*、/gw-nb/*、/sharedWorker-nb/* 等路径时,Next.js Dev Server 通过 rewrites 代理转发到 QA 后端。
9. 环境变量
对应官方文档:Environment Variables
加载机制
scripts/runtime-env.mjs 中的 loadRuntimeEnv() 根据 RUNTIME_ENV(dev / qa / prd)加载 config/env/.env.{dev,qa,prd}:
# config/env/.env.dev
NEXT_PUBLIC_RUNTIME_ENV=dev # 客户端可访问
NEXT_PUBLIC_WS_BASE_URL=dev-ws
# INTERNAL_API_BASE_URL=... # 仅服务端| 规则 | 说明 |
|------|------|
| NEXT_PUBLIC_* | 会打入客户端 bundle,可在组件中通过 process.env.NEXT_PUBLIC_* 访问 |
| 无此前缀 | 仅服务端可用 |
test.tsx 中展示了运行时环境变量的读取:
<div>runtime env: {process.env.NEXT_PUBLIC_RUNTIME_ENV}</div>
<div>ws base url: {process.env.NEXT_PUBLIC_WS_BASE_URL}</div>10. 全局状态(Zustand)
本脚手架基于 Zustand 封装了两种 Store 创建方式:
| Store | 创建方式 | 持久化 | 样例 |
|-------|----------|--------|------|
| useTestStore | createPlainStore | ❌ | isLoading 开关 |
| useTestPersistStore | createPersistStore | ✅ localStorage | aConfig 配置 |
createPlainStore:内存态,如useTestStorecreatePersistStore:持久化到 localStorage,如useTestPersistStoreGlobalProviders挂载后调用rehydratePersistStores()恢复持久化数据
持久化 Hydration
持久化 Store 设置了 skipHydration: true,在 GlobalProviders 挂载后统一调用 rehydratePersistStores(),避免 SSR 与客户端状态不一致。
跨组件共享演示
test.tsx:修改isLoading和aConfigtest2.tsx:只读展示同一 Store 的值
11. 用户鉴权
鉴权流程
- 用户访问受保护页面
UserGuard调用getUser()写入UserProvider上下文- 有
userId:执行checkUserInQb校验 QB 客户端一致性,通过后写入requestPro默认userId请求头并渲染子页面 - 无
userId:jumpLogin跳转登录页,携带from参数 requestPro响应拦截器在接口返回 401 时也会自动jumpLogin
核心组件
| 组件 / 工具 | 职责 |
|-------------|------|
| UserProvider | React Context,持有 user / setUser |
| UserGuard | 拉取用户信息、校验 QB 客户端一致性、写入请求头、未登录跳转 |
| jumpLogin(client) | 跳转 ${basePath}${loginPath}?from=...,携带完整来源路径 |
| jumpLogin(server) | redirect() 跳转,from 来自 proxy.ts 注入的请求头 |
| login/page.tsx | 登录表单,调用 _api/login(密码 MD5 后提交);成功后 router.replace(tarJumpPath)(自动剥离 basePath 前缀) |
登录回跳流程
- 用户访问
/nb-web-demo/foo?bar=1 proxy.ts将路径写入请求头x-from-path- 未登录时跳转
/nb-web-demo/login?from=%2Fnb-web-demo%2Ffoo%3Fbar%3D1 - 登录成功后
router.replace('/foo?bar=1'),Next.js 自动加上basePath回到原页面
12. 构建与部署
对应官方文档:Deploying
Jenkins 部署注意: 部署到 Jenkins 的分支名必须以
rel_为前缀,否则部署无法生效。
SPA 静态导出(默认)
npm run build
npm run build:spascripts/build-spa.mjs 会依次构建 qa、prd 两个环境,最终产物结构如下:
dist/
├── qa/ # QA 环境静态文件
└── prd/ # PRD 环境静态文件
build/
└── nb-web-demo.tar.gz # 打包后的部署产物(初始化后为 <项目名>.tar.gz)构建时 SPA_EXPORT=true,next.config.ts 启用:
output: 'export'distDir: 'dist'trailingSlash: truebasePath: '/nb-web-demo'(初始化后替换为项目名)
SSR 模式
需要 Node 服务运行时使用:
npm run build:ssr # 清理后构建,产物在 .next/
npm run start # 启动 SSR 生产服务生成 nginx 配置
SPA 静态资源部署到 nginx 时,可根据模板生成 QA / PRD 两套配置文件:
npm run gen:nginx -- 8080用法说明:
- 必须传入端口号(正整数),例如
8080 - 脚本会读取
package.json的name,并校验next.config.ts中的basePath是否为/<项目名>,两者需保持一致 - 每次执行会先清空
nginx/目录,再基于scripts/nginx.conf.template生成:nginx/<项目名>.qa.confnginx/<项目名>.prd.conf
- 生成目录已写入
.gitignore,不会提交到仓库
典型流程:
npm run build # 构建 SPA 静态产物
npm run gen:nginx -- 8080 # 生成 nginx 配置
# 将 dist/ 与 nginx/*.conf 按部署规范上传到服务器13. 样例页面走读
访问 /nb-web-demo(需登录),页面由以下部分组成:
test.tsx — 综合演示组件
| 功能 | 涉及技术 |
|------|----------|
| 展示环境变量 | NEXT_PUBLIC_* |
| 请求接口数据 | useRequest + testApi |
| 读取用户信息 | useUserContext |
| 本地 state | useState |
| 全局状态 | useTestStore / useTestPersistStore |
| UI 组件 | @sumslack/shadcn-pro 的 ProButton |
| 错误边界 | 「点击崩溃」按钮 |
test2.tsx — 跨组件状态订阅
只读订阅 useTestStore 和 useTestPersistStore,验证 Store 在兄弟组件间共享。
页面渲染链路
用户请求 /nb-web-demo
→ proxy.ts 注入 x-from-path
→ app/layout.tsx(GlobalProviders 初始化)
→ (protected)/layout.tsx(UserGuard 校验)
→ page.tsx(Server Component 组装)
→ test.tsx + test2.tsx(Client Component 交互)14. 新建业务模块 Checklist
通过 CLI 创建全新项目
- [ ] 创建空目录并进入,
nb-web-cli或nb-web-cli <项目名>初始化 - [ ] 确认
package.json的name、urls.ts与next.config.ts中的basePath均已替换
在已有项目中新增业务模块
路由直接放在 app/ 下,按 Route Group 或路径段组织:
- [ ] 在
app/下新增路由目录,如app/(protected)/settings/或app/dashboard/ - [ ] 需鉴权的页面放在
(protected)路由组内 - [ ] 公开页面放在
(public)路由组或独立路径段 - [ ] 在
_constants/urls.ts补充业务路径常量 - [ ] 更新
proxy.ts的matcher(若新增需拦截的路径段) - [ ] 在
config/rewrite-rules.ts补充开发环境代理(如需要) - [ ] 在
config/env/各环境文件中添加NEXT_PUBLIC_*变量 - [ ] 业务 API 放入
_api/,服务端专用放_api/server/ - [ ] 需全局状态的逻辑放入
_stores/,通过createPlainStore或createPersistStore创建 - [ ] Client 组件顶部加
'use client',服务端工具放_utils/server/
SPA 部署
- [ ]
npm run build生成dist/qa、dist/prd与build/<项目名>.tar.gz - [ ]
npm run gen:nginx -- <port>生成 nginx 配置(basePath需与package.json的name一致)
维护脚手架模板
修改 packages/ 下的代码即可,保存后重新执行 nb-web-cli 验证初始化结果。无需额外同步步骤。
