Yếu tố ảnh hưởng lớn nhất đến quá trình khởi động (bootstrapping) của ứng dụng là quá trình compile TypeScript. May mắn là, với webpack HMR (Hot-Module Replacement), chúng ta không cần compile lại toàn bộ project mỗi khi có thay đổi. Điều này giảm đáng kể thời gian cần để khởi tạo ứng dụng, và giúp việc phát triển lặp (iterative development) dễ dàng hơn nhiều.
Cảnh báo Lưu ý là webpack sẽ không tự động copy asset của bạn (ví dụ file graphql) vào thư mục dist. Tương tự, webpack không tương thích với glob static path (ví dụ property entities trong TypeOrmModule).
Cảnh báo Từ NestJS v12, webpack builder đã bị deprecated (không còn khuyến nghị dùng) và Rspack trở thành bundler mặc định cho monorepo. Bài này nhắm tới các project dùng webpack với CommonJS, đó là lý do các ví dụ dưới đây dùng module.hot và một lời gọi bootstrap() thông thường, thay vì await ở top-level theo kiểu ESM như ở các phần khác của tài liệu này. Với project mới, nên dùng --builder rspack.
Ghi chú người dịch Nếu bạn từng dùng nodemon với Express, hot reload ở đây khác về bản chất: nodemon chỉ restart lại toàn bộ process Node khi có file thay đổi (tức chạy lại node index.js từ đầu), còn HMR của webpack thay thế (replace) các module đã thay đổi ngay trong process đang chạy, giữ lại state hiện có (ví dụ connection pool, cache trong memory). Vì thế tốc độ nhanh hơn hẳn, nhưng đổi lại logic bootstrap phức tạp hơn — bạn phải tự lo việc đóng app cũ trước khi app mới bind lại port, như đoạn module.hot.dispose ở dưới.
Dùng CLI
Nếu bạn dùng Nest CLI, quá trình cấu hình khá đơn giản. CLI bọc (wrap) webpack, cho phép dùng HotModuleReplacementPlugin.
Cài đặt
Đầu tiên, cài các package cần thiết:
$ npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack
Gợi ý Nếu bạn dùng Yarn Berry (không phải Yarn classic), cài package webpack-pnp-externals thay cho webpack-node-externals.
Cấu hình
Sau khi cài đặt xong, tạo file webpack-hmr.config.js ở thư mục gốc của ứng dụng.
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
module.exports = function (options, webpack) {
return {
...options,
entry: ['webpack/hot/poll?100', options.entry],
externals: [
nodeExternals({
allowlist: ['webpack/hot/poll?100'],
}),
],
plugins: [
...options.plugins,
new webpack.HotModuleReplacementPlugin(),
new webpack.WatchIgnorePlugin({
paths: [/\.js$/, /\.d\.ts$/],
}),
new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
],
};
};
Gợi ý Với Yarn Berry (không phải Yarn classic), thay vì dùng nodeExternals trong property cấu hình externals, hãy dùng WebpackPnpExternals từ package webpack-pnp-externals: WebpackPnpExternals({ exclude: ['webpack/hot/poll?100'] }).
Hàm này nhận object chứa cấu hình webpack mặc định làm đối số đầu tiên, và tham chiếu tới package webpack gốc mà Nest CLI đang dùng làm đối số thứ hai. Ngoài ra, nó trả về một cấu hình webpack đã được sửa đổi, có thêm plugin HotModuleReplacementPlugin, WatchIgnorePlugin, và RunScriptWebpackPlugin.
Hot-Module Replacement
Để bật HMR, mở file entry của ứng dụng (main.ts) và thêm các đoạn code liên quan đến webpack sau:
declare const module: any;
async function bootstrap() {
if (module.hot?.data?.closePromise) {
// đợi instance ứng dụng trước đó đóng hoàn toàn
await module.hot.data.closePromise;
}
const app = await NestFactory.create(AppModule, {
forceCloseConnections: !!module.hot,
});
await app.listen(process.env.PORT ?? 3000);
if (module.hot) {
module.hot.accept();
module.hot.dispose((data: any) => {
data.closePromise = app.close();
});
}
}
bootstrap();
Gợi ý app.close() là bất đồng bộ (asynchronous), nhưng webpack không đợi (await) callback dispose(). Việc lưu promise trả về vào module.hot.data cho phép instance ứng dụng tiếp theo đợi nó xong trước khi bind lại vào port, và (kết hợp với forceCloseConnections) giúp tránh lỗi EADDRINUSE khi reload.
Ghi chú người dịch EADDRINUSE chính là lỗi “address already in use” quen thuộc khi bạn chạy hai process cùng listen một port — ở đây nó xảy ra vì webpack thay module mới vào ngay lập tức, trong khi app cũ (đang giữ port) vẫn có thể còn đang đóng connection dở dang (ví dụ connection tới DB, WebSocket đang mở). Đây là cạm bẫy dễ gặp nếu bạn copy code này nhưng bỏ qua đoạn closePromise/forceCloseConnections: mọi thứ vẫn chạy được vài lần reload đầu, rồi bất ngờ báo lỗi bind port khi có connection “cứng đầu” chưa kịp đóng.
Để đơn giản hóa việc chạy, thêm một script vào file package.json.
"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch"
Giờ chỉ cần mở command line và chạy lệnh sau:
$ npm run start:dev
Không dùng CLI
Nếu bạn không dùng Nest CLI, việc cấu hình sẽ phức tạp hơn một chút (cần nhiều bước thủ công hơn).
Cài đặt
Đầu tiên, cài các package cần thiết:
$ npm i --save-dev webpack webpack-cli webpack-node-externals ts-loader run-script-webpack-plugin
Gợi ý Nếu bạn dùng Yarn Berry (không phải Yarn classic), cài package webpack-pnp-externals thay cho webpack-node-externals.
Cấu hình
Sau khi cài đặt xong, tạo file webpack.config.js ở thư mục gốc của ứng dụng.
const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
module.exports = {
entry: ['webpack/hot/poll?100', './src/main.ts'],
target: 'node',
externals: [
nodeExternals({
allowlist: ['webpack/hot/poll?100'],
}),
],
module: {
rules: [
{
test: /.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
mode: 'development',
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [new webpack.HotModuleReplacementPlugin(), new RunScriptWebpackPlugin({ name: 'server.js', autoRestart: false })],
output: {
path: path.join(__dirname, 'dist'),
filename: 'server.js',
},
};
Gợi ý Với Yarn Berry (không phải Yarn classic), thay vì dùng nodeExternals trong property cấu hình externals, hãy dùng WebpackPnpExternals từ package webpack-pnp-externals: WebpackPnpExternals({ exclude: ['webpack/hot/poll?100'] }).
Cấu hình này nói cho webpack biết một số điều cần thiết về ứng dụng của bạn: vị trí file entry, thư mục nào sẽ dùng để chứa các file đã compile, và loại loader nào sẽ dùng để compile source file. Nói chung, bạn có thể dùng file này gần như nguyên bản, dù không hiểu hết mọi option.
Hot-Module Replacement
Để bật HMR, mở file entry của ứng dụng (main.ts) và thêm các đoạn code liên quan đến webpack sau:
declare const module: any;
async function bootstrap() {
if (module.hot?.data?.closePromise) {
// đợi instance ứng dụng trước đó đóng hoàn toàn
await module.hot.data.closePromise;
}
const app = await NestFactory.create(AppModule, {
forceCloseConnections: !!module.hot,
});
await app.listen(process.env.PORT ?? 3000);
if (module.hot) {
module.hot.accept();
module.hot.dispose((data: any) => {
data.closePromise = app.close();
});
}
}
bootstrap();
Gợi ý app.close() là bất đồng bộ (asynchronous), nhưng webpack không đợi (await) callback dispose(). Việc lưu promise trả về vào module.hot.data cho phép instance ứng dụng tiếp theo đợi nó xong trước khi bind lại vào port, và (kết hợp với forceCloseConnections) giúp tránh lỗi EADDRINUSE khi reload.
Để đơn giản hóa việc chạy, thêm một script vào file package.json.
"start:dev": "webpack --config webpack.config.js --watch"
Giờ chỉ cần mở command line và chạy lệnh sau:
$ npm run start:dev
Ví dụ
Bạn có thể xem một ví dụ chạy được tại đây.