
一、如何 serve 静态资源
若是 koa 我们可以通过 koa-static
:
ts
const serve = require('koa-static');
const buildFolder = path.resolve(__dirname, '../client/dist');
app.use(serve(buildFolder));
nestjs 需要通过 module 引入。
解法 @nestjs/serve-static
ts
// nestjs-server\src\client-app.module.ts
import { Module } from '@nestjs/common';
import path from 'node:path';
import { ServeStaticModule } from '@nestjs/serve-static';
const buildFolder = path.resolve(__dirname, '../../client/dist');
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: buildFolder,
renderPath: '/',
}),
// 你可以 serve 多个
ServeStaticModule.forRoot({
rootPath: mediaFolder,
serveRoot: '/media',
}),
],
})
export class ClientAppModule {}
记得注入到 \src\app.module.ts 的
imports
。
rootPath
:被 serve 的本地文件目录,即文件在磁盘上的目录。serveRoot
: 浏览器访问时加在路径前面的虚拟 前缀。如设置/static
则- 访问
http://localhost:3000/static/js/app.js
- 实际读取的是
dist/js/app.js
- 访问
renderPath
: 指定浏览器访问的 URL 路径模式 ,决定哪些请求会被当作静态文件(或 SPA 的 index.html)处理。通过哪个 url path 进入可以访问当静态资源。如果是 /build 则需要从http:localhost:xxx/build
进入才会 serve。默认*
即任意 404 path 进入都会返回dist/index.html
。我们设置/
则只有根目录才会 serve。否则返回:
json
{
"message": "Cannot GET /xxx",
"error": "Not Found",
"statusCode": 404
}
serveRoot 用途详解
serveRoot 用途
我们本地开发前端代码 client port 为 3001,服务端 port 为 3002,/api/images
返回的图片 url 列表,比如 ['foo.png', ...]
此时如果前端代码直接将其当做 img 的 src 会找不到图片。因为浏览器将相对路径解析成 http://localhost:3001/foo.png
,而图片是被服务器 serve 的并不在客户端代码内,正确路径:http://localhost:3002/foo.png
。有两种解法:
解决方案 1:前端解决
ts
srcs = list.map(src => `http://localhost:3002/` + src)
适配生产环境:
ts
// 拼接当前 host + 根据 process.env.NODE_ENV 只有当本地才加 3002
const withoutPort = location.origin.replace(/:\d+$/, '')
const port = isDev ? `:3002` : ''
srcs = list.map(src => `${withoutPort}${port}/` + src)
优点代码量少 & 纯前端。缺点写死了服务端 port。
解决方案 2:前端 proxy
渲染的时候增加"临时"前缀 /photos/
:
ts
const prefix = isDev ? '/photos/' : ''
srcs = list.map(src => prefix + src)
上述代码并未指定 port 而是通过 dev server 的 proxy 来将其 proxy 到服务端,当然得 rewrite 将"临时"前缀去除,因为实际上服务端是没有这个前缀的。
ts
'/photos': {
target: `http://localhost:${SERVER_PORT}`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/photos/, ''),
},
完整代码:client\vite.config.ts
ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
// pnpm dev:server port
const SERVER_PORT = 6834;
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: `http://localhost:${SERVER_PORT}`,
changeOrigin: true,
},
'/photos': {
target: `http://localhost:${SERVER_PORT}`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/photos/, ''),
},
'/videos': {
target: `http://localhost:${SERVER_PORT}`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/videos/, ''),
},
},
},
});
解决方案 3:前端+服务端:proxy
+ serveRoot
之前我们讲过:
serveRoot
: 浏览器访问时加在路径前面的虚拟 前缀。如设置/static
则
- 访问
http://localhost:3000/static/js/app.js
- 实际读取的是
dist/js/app.js
也就是无需前端 proxy rewrite 因为服务端可以自动去除。 这样前端渲染代码无需改,proxy 也无需加。
第一步:前端渲染时
无需区分环境统一加前缀。
diff
- const prefix = isDev ? '/photos/' : ''
+ const prefix = '/photos/'
srcs = list.map(src => prefix + src)
第二步:前端 proxy
diff
'/photos': {
target: `http://localhost:${SERVER_PORT}`,
changeOrigin: true,
- rewrite: (path) => path.replace(/^\/photos/, ''),
},
第三步:服务端配置 serverRoot
nestjs-server\src\client-app.module.ts
ts
import { Module } from '@nestjs/common';
import path from 'node:path';
import { ServeStaticModule } from '@nestjs/serve-static';
import { mediaFolder } from './utils/program';
const buildFolder = path.resolve(__dirname, '../../client/dist');
@Module({
imports: [
// 图片资源
ServeStaticModule.forRoot({
rootPath: mediaFolder,
serverRoot: '/public/'
}),
],
})
export class ClientAppModule {}
这样开发环境访问 localhost:3001
/photos/foo.png 将 proxy 到 localhost:3002
/photos/foo.png。
生产环境访问 localhost:3002
/photos/foo.png @nestjs/serve-static 将自动对应到 localhost:3002
/foo.png 即正确访问到磁盘上面的图片资源。
二、如何统一给 api 增加前缀
nestjs 如何给所有的 api 统一增加前缀。比如 /api/images, /api/videos 我们知道可以通过 @Controller('/api/videos')
但是这样改太麻烦了 如果有多个controller 得逐个修改
解法 app.setGlobalPrefix('api');
nestjs-server\src\main.ts
ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
await app.listen(process.env.PORT ?? 3000);
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
bootstrap();
如果需要排除路径可以:
ts
app.setGlobalPrefix('api', {
exclude: ['health', 'metrics'], // 这些路由不会添加前缀
});