手把手教你如何優(yōu)雅的書(shū)寫NestJS服務(wù)配置
前言
開(kāi)發(fā)服務(wù)端應(yīng)用,配置肯定是少不了的。端口號(hào)、數(shù)據(jù)庫(kù)賬號(hào)、Redis 密碼、各種業(yè)務(wù)白名單……這些東西如果硬編碼在代碼里,不僅不安全,每次切環(huán)境還得改代碼,簡(jiǎn)直是災(zāi)難。
配置的來(lái)源五花八門:簡(jiǎn)單的可以直接讀環(huán)境變量(env),稍微復(fù)雜點(diǎn)的用 JSON、YAML 配置文件,遇到敏感信息或者微服務(wù)架構(gòu),可能還得去 Nacos、Apollo 這樣的遠(yuǎn)端配置中心拉取。
在 NestJS 里,寫配置的方式有很多。如果團(tuán)隊(duì)沒(méi)個(gè)統(tǒng)一的規(guī)范,大家各寫各的,雖然代碼也能跑,但后期維護(hù)起來(lái)絕對(duì)會(huì)讓人抓狂。這幾年再使用 NestJS 過(guò)程中踩了不少坑,摸索出了一套比較舒服的配置管理姿勢(shì),今天就來(lái)系統(tǒng)地梳理一遍。
1. 刀耕火種:直接硬剛 process.env
最原始、最暴力的寫法,就是直接在代碼里讀 process.env:
// 這種代碼散落在項(xiàng)目的各個(gè)角落,后期維護(hù)極其痛苦 const port = process.env.PORT || 3000; const dbHost = process.env.DATABASE_HOST;
這種寫法最大的問(wèn)題是毫無(wú)約束。字段名字全靠腦子記,類型全都是 string | undefined,還得自己手動(dòng)轉(zhuǎn)數(shù)字、轉(zhuǎn)布爾值。哪天要是改個(gè)環(huán)境變量的名字,你得全局搜索一遍才能確認(rèn)是不是都改全了。所以,這只能算是個(gè)起點(diǎn),正經(jīng)項(xiàng)目千萬(wàn)別這么搞。
2. 官方標(biāo)配:@nestjs/config 模塊
為了解決配置問(wèn)題,NestJS 官方提供了一個(gè) @nestjs/config 包。它的底層其實(shí)就是我們熟悉的 dotenv,這也是目前處理配置的標(biāo)準(zhǔn)入口。
先裝個(gè)包(注意:它要求 TypeScript 4.1 及以上版本):
npm i --save @nestjs/config
2.1 怎么用起來(lái)?
最基礎(chǔ)的用法,就是在根模塊 AppModule 里引入 ConfigModule,調(diào)一下 forRoot()。它會(huì)自動(dòng)去項(xiàng)目根目錄找 .env 文件并解析:
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 強(qiáng)烈建議設(shè)為全局,這樣其他模塊就不用反復(fù) import 了
}),
],
})
export class AppModule {}?? 踩坑預(yù)警:當(dāng)系統(tǒng)的環(huán)境變量和
.env文件里出現(xiàn)了同名的 key 時(shí),系統(tǒng)環(huán)境變量的優(yōu)先級(jí)更高,.env里的值會(huì)被無(wú)情覆蓋。這是 dotenv 的默認(rèn)規(guī)則,在線上部署排查問(wèn)題時(shí)一定要記住這一點(diǎn)。
設(shè)置了 isGlobal: true 后,你在任何 Service 里都可以直接注入 ConfigService 來(lái)拿配置了。
2.2 應(yīng)對(duì)多環(huán)境
真實(shí)項(xiàng)目肯定不止一個(gè)環(huán)境,起碼有開(kāi)發(fā)(dev)、測(cè)試(staging)、生產(chǎn)(prod)。我們可以傳個(gè)數(shù)組給 envFilePath,讓它按需加載。通常配合 NODE_ENV 動(dòng)態(tài)決定加載哪個(gè)文件:
ConfigModule.forRoot({
envFilePath: [
`.env.${process.env.NODE_ENV}.local`, // 優(yōu)先級(jí)最高:本地覆蓋文件
`.env.${process.env.NODE_ENV}`, // 當(dāng)前環(huán)境配置
'.env', // 兜底默認(rèn)配置
],
isGlobal: true,
});?? 踩坑預(yù)警:數(shù)組里越靠前的文件優(yōu)先級(jí)越高。如果幾個(gè)文件里有相同的 key,它只會(huì)認(rèn)第一個(gè)找到的值,后面的文件里重復(fù)的 key 會(huì)被靜默忽略。
3. 進(jìn)階:把"散裝字符串"變成結(jié)構(gòu)化對(duì)象
只用 .env 有個(gè)很煩人的點(diǎn):讀出來(lái)的全是一層扁平的字符串。如果配置一多,找起來(lái)費(fèi)勁,而且到處都要寫 parseInt。
NestJS 允許我們寫配置工廠函數(shù),把這些零散的環(huán)境變量組裝成有結(jié)構(gòu)、有類型、有默認(rèn)值的對(duì)象。
// config/database.config.ts
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
},
});然后在 AppModule 里通過(guò) load 屬性把這個(gè)工廠函數(shù)塞進(jìn)去:
import databaseConfig from './config/database.config';
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig], // 這里是個(gè)數(shù)組,可以塞多個(gè)配置進(jìn)去,互不干擾
});用的時(shí)候,就可以用點(diǎn)號(hào)(.)來(lái)讀取嵌套屬性了:
const dbHost = this.configService.get<string>('database.host');
const dbPort = this.configService.get<number>('database.port');這比到處散落的 process.env.DATABASE_HOST 好多了,至少做到了集中管理。但這種寫法還有個(gè)問(wèn)題:字符串路徑 'database.host' 沒(méi)有類型提示,寫錯(cuò)了只能靠運(yùn)行時(shí)暴露。下面這個(gè)方案才是真正的終極解法。
4. 終極利器:命名空間(Namespace)
registerAs() 函數(shù)是 @nestjs/config 里最值得推薦的功能。它不只是給配置起個(gè)名字,更重要的是它返回的對(duì)象帶有 .KEY 屬性,可以直接用于依賴注入——這意味著你可以注入整個(gè)配置對(duì)象,而不是一個(gè)個(gè)字符串地去 get()。
4.1 劃分命名空間
把不同業(yè)務(wù)的配置拆到各自的文件里:
// config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
name: process.env.DATABASE_NAME,
}));// config/redis.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('redis', () => ({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
}));然后在根模塊統(tǒng)一加載:
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, redisConfig],
});4.2 享受強(qiáng)類型的快感
用了命名空間后,可以直接注入整個(gè)配置對(duì)象,TypeScript 類型推導(dǎo)完整,IDE 有完整提示,字段名寫錯(cuò)了編譯階段就報(bào)錯(cuò):
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import databaseConfig from './config/database.config';
@Injectable()
export class DatabaseService {
constructor(
@Inject(databaseConfig.KEY)
private readonly dbConfig: ConfigType<typeof databaseConfig>,
) {}
getConnection() {
// 敲下 this.dbConfig. 的時(shí)候,IDE 會(huì)完整提示 host, port, name
return `${this.dbConfig.host}:${this.dbConfig.port}/${this.dbConfig.name}`;
}
}?? 小貼士:
ConfigType<typeof databaseConfig>是官方提供的工具類型,它能自動(dòng)反推工廠函數(shù)的返回值結(jié)構(gòu),省得你再去手寫一遍 Interface。
4.3 絲滑對(duì)接第三方模塊
命名空間配置還有個(gè)殺手锏:.asProvider() 方法。
平時(shí)配置 TypeORM 或 Redis 這類第三方模塊時(shí),往往要寫一長(zhǎng)串 useFactory、inject、imports。用了 .asProvider() 后,這些樣板代碼全部省掉——它的本質(zhì)是把依賴聲明和工廠函數(shù)打包成了標(biāo)準(zhǔn)的 forRootAsync 入?yún)?,NestJS 會(huì)據(jù)此建立正確的模塊初始化順序:
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [databaseConfig] }),
// 等價(jià)于手寫 imports/inject/useFactory 的完整異步配置
TypeOrmModule.forRootAsync(databaseConfig.asProvider()),
],
})
export class AppModule {}關(guān)于為什么這能保證初始化順序,以及手寫 useFactory 時(shí)要注意什么,我們?cè)诘?7.2 節(jié)會(huì)展開(kāi)說(shuō)。
5. 換個(gè)口味:YAML、JSON 怎么搞?
.env 用起來(lái)最順手,但它有個(gè)天然的短板:所有值讀出來(lái)都是字符串,端口、超時(shí)時(shí)間這些數(shù)字都得自己手動(dòng) parseInt,布爾值也要寫 === 'true' 來(lái)判斷,一不小心就出錯(cuò)。
相比之下,YAML 和 JSON 本身就支持?jǐn)?shù)字、布爾等原生類型,寫配置的時(shí)候是什么類型讀出來(lái)還是什么類型,完全不需要轉(zhuǎn)換。而且層級(jí)結(jié)構(gòu)更清晰,改起來(lái)更直觀,后期維護(hù)成本也低得多。如果項(xiàng)目的配置項(xiàng)比較多,主動(dòng)選擇 YAML 或 JSON 來(lái)管理是個(gè)很合理的決定。
@nestjs/config 的 load 機(jī)制非常靈活,只要你的工廠函數(shù)最終返回一個(gè)普通對(duì)象,它才不管數(shù)據(jù)是從哪來(lái)的。
5.1 玩轉(zhuǎn) YAML
先裝解析庫(kù):npm i js-yaml 和 npm i -D @types/js-yaml。
// config/yaml.config.ts
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import * as yaml from 'js-yaml';
export default () => {
return yaml.load(
readFileSync(join(__dirname, 'config.yaml'), 'utf8'),
) as Record<string, any>;
};?? 踩坑預(yù)警:Nest CLI 默認(rèn)打包時(shí)不會(huì)拷貝非 TS 文件。如果你用了 YAML,一定要去
nest-cli.json里配置assets,不然打完包跑起來(lái)絕對(duì)報(bào)找不到文件的錯(cuò)。{ "compilerOptions": { "assets": [{ "include": "../config/*.yaml", "outDir": "./dist/config" }] } }
5.2 遠(yuǎn)端配置中心(如 Nacos、Apollo)
如果你們用了配置中心,最常見(jiàn)的有兩種接入方式:
方式一:CD 流程在容器啟動(dòng)前下發(fā)配置文件,把配置寫到本地磁盤,應(yīng)用啟動(dòng)時(shí)當(dāng)普通本地文件讀,和 5.1 的 YAML 方式一模一樣,沒(méi)有額外復(fù)雜度。
方式二:應(yīng)用啟動(dòng)時(shí)主動(dòng)拉取,把工廠函數(shù)寫成異步的,在 ConfigModule 初始化期間完成拉?。?/p>
import { registerAs } from '@nestjs/config';
export default registerAs('secret', async () => {
// 去遠(yuǎn)端拉配置,拉不到就拋異常,阻止應(yīng)用啟動(dòng)
const data = await fetchFromConfigCenter('/api/config/secret');
return { apiKey: data.apiKey, jwtSecret: data.jwtSecret };
});兩種方式的選擇取決于你們的部署流程,不存在好壞之分。
6. 防患未然:配置校驗(yàn)
沒(méi)有校驗(yàn)的配置就像是一顆定時(shí)炸彈。萬(wàn)一線上環(huán)境少配了一個(gè)數(shù)據(jù)庫(kù)密碼,應(yīng)用可能照樣啟動(dòng),直到用戶發(fā)起請(qǐng)求才原地爆炸。
我們必須讓應(yīng)用在啟動(dòng)階段就把缺少的配置暴露出來(lái)。NestJS 給了兩套方案:
6.1 簡(jiǎn)單粗暴的 Joi 校驗(yàn)
import * as Joi from 'joi';
ConfigModule.forRoot({
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('dev', 'prod').default('dev'),
PORT: Joi.number().default(3000),
DATABASE_HOST: Joi.string().required(), // 必填項(xiàng),沒(méi)有就不準(zhǔn)啟動(dòng)!
}),
validationOptions: {
allowUnknown: true, // 允許出現(xiàn)沒(méi)在 schema 里定義的變量
abortEarly: false, // 別一遇到錯(cuò)就停,把所有錯(cuò)一塊報(bào)出來(lái)
},
});?? 踩坑預(yù)警:如果你傳了
validationOptions,那些沒(méi)寫的選項(xiàng)會(huì)回退到 Joi 的默認(rèn)值,而不是@nestjs/config的默認(rèn)值!比如allowUnknown,Joi 默認(rèn)是false,但 Nest 默認(rèn)是true。為了不被坑,建議把這倆屬性老老實(shí)實(shí)寫清楚。
6.2 面向?qū)ο蟮?class-validator
如果你更喜歡用類和裝飾器,也可以自己寫個(gè)校驗(yàn)函數(shù):
import { plainToInstance } from 'class-transformer';
import { IsNumber, IsString, validateSync } from 'class-validator';
class EnvVariables {
@IsNumber() PORT: number;
@IsString() DATABASE_HOST: string;
}
export function validate(config: Record<string, unknown>) {
const validated = plainToInstance(EnvVariables, config, { enableImplicitConversion: true });
const errors = validateSync(validated, { skipMissingProperties: false });
if (errors.length > 0) throw new Error(errors.toString());
return validated;
}// app.module.ts
ConfigModule.forRoot({ validate });兩種校驗(yàn)方式效果相同,Joi 更簡(jiǎn)潔,class-validator 在項(xiàng)目里已經(jīng)有 DTO 校驗(yàn)體系時(shí)風(fēng)格更統(tǒng)一,按喜好選就行。
7. 特殊場(chǎng)景
7.1 局部注冊(cè)(forFeature)的生命周期坑
如果你的項(xiàng)目比較大,不想把所有配置都塞在 AppModule 里,可以在各自的業(yè)務(wù)模塊里按需加載:
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}?? 踩坑預(yù)警:用
forFeature注冊(cè)的配置,千萬(wàn)別在構(gòu)造函數(shù)(constructor)里去讀!因?yàn)槟K初始化的順序是不確定的,這時(shí)候配置可能還沒(méi)加載完。正確的做法是用屬性注入,然后在onModuleInit生命周期鉤子里讀取。
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import databaseConfig from './database.config';
@Injectable()
export class DatabaseService implements OnModuleInit {
// 屬性注入,不在構(gòu)造函數(shù)里觸碰配置
@Inject(databaseConfig.KEY)
private readonly dbConfig: ConfigType<typeof databaseConfig>;
onModuleInit() {
// ? 到這里所有依賴模塊都已初始化完畢,安全讀取
console.log('db host:', this.dbConfig.host);
}
// ? 下面這種寫法會(huì)在某些場(chǎng)景下讀到 undefined
// constructor(
// @Inject(databaseConfig.KEY)
// private readonly dbConfig: ConfigType<typeof databaseConfig>,
// ) {
// console.log(this.dbConfig.host); // 危險(xiǎn)!配置可能還沒(méi)就緒
// }
}7.2 配置之間的依賴鏈問(wèn)題
這是一個(gè)很容易被忽略但實(shí)際開(kāi)發(fā)中會(huì)真實(shí)踩到的坑。
以 TypeORM 為例,完整的啟動(dòng)依賴鏈?zhǔn)沁@樣的:
ConfigModule 加載 .env / 配置文件
↓
databaseConfig 工廠函數(shù)讀取環(huán)境變量,生成配置對(duì)象
↓
TypeOrmModule 拿到配置,建立數(shù)據(jù)庫(kù)連接池
↓
各業(yè)務(wù) Service / Repository 可以正常使用
↓
應(yīng)用啟動(dòng)完成,開(kāi)始接受請(qǐng)求這條鏈上的順序必須是確定的。如果配置還沒(méi)就緒,TypeOrmModule 就開(kāi)始初始化,輕則連接參數(shù)是 undefined,重則應(yīng)用直接起不來(lái)。
第 4.3 節(jié)提到的 .asProvider() 能自動(dòng)保證這個(gè)順序,因?yàn)樗归_(kāi)后等價(jià)于:
{
imports: [ConfigModule.forFeature(databaseConfig)], // 聲明:我依賴這個(gè)配置
useFactory: (config: ConfigType<typeof databaseConfig>) => config,
inject: [databaseConfig.KEY],
}imports 字段就是向 NestJS 聲明依賴關(guān)系的地方??蚣芸吹竭@個(gè)聲明,就會(huì)等 databaseConfig 就緒后再初始化 TypeOrmModule,順序由框架保證,不需要你操心。
如果不用 .asProvider(),自己手寫 useFactory,一定要記得寫 imports,不然就是在賭運(yùn)氣:
TypeOrmModule.forRootAsync({
imports: [ConfigModule], // ← 必須寫!告訴框架:等 ConfigModule 好了再來(lái)初始化我
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get<string>('database.host'),
port: configService.get<number>('database.port'),
// ...
}),
}),還有一個(gè)更細(xì)的場(chǎng)景:兩個(gè)配置文件之間能互相依賴嗎?比如 databaseConfig 想引用 appConfig 里的某個(gè)值。答案是不行,@nestjs/config 不支持配置工廠之間的注入。正確的做法是所有工廠函數(shù)都平級(jí)地從 process.env 取原始值,誰(shuí)需要哪個(gè)環(huán)境變量就自己讀,不要試圖跨工廠函數(shù)共享:
// ? 錯(cuò)誤:試圖在配置工廠里引用另一個(gè)配置對(duì)象
export default registerAs('database', () => {
const appConfig = someHowGetAppConfig(); // 根本不存在這種 API
return { host: appConfig.defaultHost };
});
// ? 正確:直接從 process.env 取,各配置工廠平級(jí)獨(dú)立
export default registerAs('database', () => ({
host: process.env.APP_DEFAULT_HOST || 'localhost',
}));7.3 在 main.ts 里怎么拿配置?
像監(jiān)聽(tīng)端口、CORS 域名這種配置,在 main.ts 里就要用到,這時(shí)候可以通過(guò) app.get() 拿到 ConfigService 實(shí)例:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const port = configService.get<number>('PORT') ?? 3000;
await app.listen(port);
}8. 總結(jié):到底該怎么選?
寫了這么多,咱們來(lái)拉個(gè)表對(duì)比一下:
| 姿勢(shì) | 類型安全 | 結(jié)構(gòu)化 | 評(píng)價(jià) |
|---|---|---|---|
直接硬剛 process.env | ? 純盲寫 | ? 扁平 | 適合寫完就扔的臨時(shí)腳本 |
ConfigModule + .env | 弱(全靠泛型強(qiáng)轉(zhuǎn)) | ? 扁平 | 適合小型項(xiàng)目快速起步 |
| 自定義工廠函數(shù) | ?(需手寫類型) | ? 嵌套對(duì)象 | 中規(guī)中矩,能處理默認(rèn)值和類型轉(zhuǎn)換 |
| registerAs() 命名空間 | ??(全自動(dòng)推導(dǎo)) | ? 按業(yè)務(wù)隔離 | 強(qiáng)烈推薦!中大型項(xiàng)目的標(biāo)準(zhǔn)答案 |
?? 個(gè)人最推薦的實(shí)戰(zhàn)組合: registerAs() 命名空間 + isGlobal: true + 啟動(dòng)時(shí) Joi 強(qiáng)校驗(yàn)。
這套組合的核心邏輯是:配置在哪定義,就在哪描述它的結(jié)構(gòu);業(yè)務(wù)代碼只管用,根本不需要關(guān)心底層到底是 .env 還是 YAML。 這樣哪怕以后要把配置遷移到云端,業(yè)務(wù)代碼也一行都不用改,這才叫真正的優(yōu)雅。
到此這篇關(guān)于如何優(yōu)雅的書(shū)寫NestJS服務(wù)配置的文章就介紹到這了,更多相關(guān)NestJS服務(wù)配置內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于NodeJS開(kāi)發(fā)釘釘回調(diào)接口實(shí)現(xiàn)AES-CBC加解密
這篇文章主要介紹了基于NodeJS開(kāi)發(fā)釘釘回調(diào)接口 實(shí)現(xiàn)AES-CBC加解密,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
Node.js?實(shí)現(xiàn)簡(jiǎn)單爬蟲(chóng)的示例代碼
本文主要介紹了Node.js?實(shí)現(xiàn)簡(jiǎn)單爬蟲(chóng),爬取美食網(wǎng)站的菜品標(biāo)題和圖片鏈接,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
node.js express框架實(shí)現(xiàn)文件上傳與下載功能實(shí)例詳解
這篇文章主要介紹了node.js express框架實(shí)現(xiàn)文件上傳與下載功能,結(jié)合具體實(shí)例形式詳細(xì)分析了node.js express框架針對(duì)文件上傳與下載的前后臺(tái)相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-10-10
一文詳解NodeJS和Javascript之間有什么區(qū)別
在前端和后端開(kāi)發(fā)的技術(shù)棧中,JavaScript?和?Node.js?經(jīng)常是新手和經(jīng)驗(yàn)豐富的開(kāi)發(fā)者討論的熱門話題,這篇文章主要介紹了NodeJS和Javascript之間有什么區(qū)別的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-11-11
Node學(xué)習(xí)筆記:Node.js安裝及環(huán)境配置 史詩(shī)級(jí)詳細(xì)版【含測(cè)試與鏡像說(shuō)明】
這篇文章主要介紹了Node學(xué)習(xí)筆記之Node.js安裝及環(huán)境配置方法,詳細(xì)分析了node.js的基本安裝、配置、環(huán)境變量設(shè)置、以及環(huán)境測(cè)試與鏡像使用說(shuō)明,需要的朋友可以參考下2023-05-05
Node安裝教程&環(huán)境變量配置方式(Window11)
這篇文章詳細(xì)介紹了如何在Windows 11上安裝和配置Node.js,包括下載、安裝、驗(yàn)證安裝、創(chuàng)建目錄、配置環(huán)境變量、下載驗(yàn)證以及更換淘寶鏡像等步驟2026-03-03
nodejs簡(jiǎn)單實(shí)現(xiàn)操作arduino
本文給大家分享的是使用nodejs來(lái)驅(qū)動(dòng)arduino,主要是基于cylonjs 和 gort,有需要的小伙伴可以參考下2016-09-09

