Lưu ý: Hướng dẫn này chỉ là phần giới thiệu ngắn gọn về ReactDOM.hydrate()
và ReactDOMServer.renderToString()
, không dùng cho môi trường production.
Thay vào đó, Next.js cung cấp một phương pháp hiện đại để tạo các ứng dụng tĩnh và ứng dụng được dựng từ phía máy chủ (server-rendered) bằng React.
Server-side rendering (SSR) là gì?
Server-side rendering là kỹ thuật phổ biến để render ứng dụng single-page (SPA) phía client trên server, sau đó gửi trang đã được render hoàn chỉnh tới client. Phương pháp này cho phép các thành phần động được phục vụ dưới dạng HTML markup tĩnh.
Kỹ thuật này hữu ích cho SEO khi các công cụ tìm kiếm không xử lý JavaScript đúng cách và trong trường hợp tải xuống gói JavaScript lớn bị ảnh hưởng bởi kết nối mạng chậm.
Trong hướng dẫn này, bạn sẽ khởi tạo một ứng dụng React bằng Create React App và sau đó chỉnh sửa dự án để kích hoạt server-side rendering.
Sau khi kết thúc bài hướng dẫn này, bạn sẽ có thể xây dựng được một dự án hoạt động với ứng dụng React phía client và ứng dụng Express phía server.
Yêu cầu tiên quyết
Để hoàn thành hướng dẫn, bạn cần cài đặt Node.js trên máy.
Hướng dẫn đã được thử nghiệm và hoạt động tốt với các phiên bản sau:
- Node v16.13.1
- npm v8.1.2
- react v17.0.2
- @babel/core v7.16.0
- webpack v4.44.2
- express v4.17.1
- nodemon v2.0.15
- npm-run-all v4.1.5
Các bước kích hoạt server-side rendering
Bước 1 – Tạo ứng dụng React và chỉnh sửa Component App
Đầu tiên, sử dụng npx để khởi động một ứng dụng React mới bằng phiên bản mới nhất của Create React App.
Hãy gọi ứng dụng là react-ssr-example:
npx create-react-app react-ssr-example
Sau đó, di chuyển vào thư mục mới bằng lệnh:
cd react-ssr-example
Cuối cùng, khởi động ứng dụng client-side mới để xác minh quá trình cài đặt:
npm start
Bạn sẽ thấy một ứng dụng React ví dụ được hiển thị trong cửa sổ trình duyệt của mình.
Bây giờ, trong thư mục src
, hãy tạo một component <Home>
mới:
nano src/Home.js
Tiếp theo, thêm đoạn mã sau vào tệp Home.js
:
// src/Home.js function Home(props) { return <h1>Hello {props.name}!</h1>; } export default Home;
Thao tác này sẽ tạo ra một thẻ tiêu đề <h1>
với thông điệp “Hello” hướng đến một tên cụ thể.
Tiếp theo, chúng ta hãy render component <Home>
trong component <App>
. Mở tệp App.js
trong thư mục src
:
nano src/App.js
Sau đó, thay thế các dòng code hiện có bằng các dòng code mới sau:
`// src/App.js import Home from ‘./Home’;
function App() { return <Home name=”Sammy”/>; } export default App;`
Lệnh này sẽ truyền một name
cho component <Home>
và hiển thị thông điệp là:
Output:
"Hello Sammy!"
Trong tệp index.js
của ứng dụng, bạn sẽ sử dụng phương thức hydrate
của ReactDOM
thay vì render
để thể hiện cho bộ dựng DOM thấy rằng bạn có ý định rehydrate ứng dụng sau khi được dựng từ phía máy chủ (server-side render).
Hãy mở tệp index.js
trong thư mục src
:
nano src/index.js
Sau đó, thay thế nội dung của tệp index.js
bằng đoạn code sau:
`// src/index.js import React from ‘react’; import ReactDOM from ‘react-dom’; import App from ‘./App’;
ReactDOM.hydrate( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById(‘root’) );`
Việc thiết lập phía client đã hoàn tất, bạn có thể chuyển sang thiết lập phía server.
Bước 2 – Tạo Express Server và Render Component App
Bây giờ khi bạn đã có ứng dụng, hãy thiết lập một máy chủ để gửi kèm theo một phiên bản đã được render (dựng sẵn). Bạn sẽ sử dụng Express cho máy chủ này.
Lưu ý: react-scripts
của Create React App đã cài đặt một phiên bản của express
như một yêu cầu cho webpack-dev-server
. Để tránh xung đột cây phụ thuộc (dependency tree conflict), hướng dẫn này sẽ không bao gồm bước cài đặt này nữa.
Tiếp theo, hãy tạo một thư mục server
mới trong thư mục gốc của dự án:
mkdir server
Sau đó, bên trong thư mục server
, tạo một tệp index.js
mới để chứa code server Express:
nano server/index.js
Thêm các import
cần thiết và định nghĩa một số hằng số:
`// server/index.js import path from ‘path’; import fs from ‘fs’; import React from ‘react’; import ReactDOMServer from ‘react-dom/server’; import express from ‘express’; import App from ‘../src/App’; // Import component App từ phía client
const PORT = process.env.PORT || 3006; const app = express();`
Tiếp theo, thêm code server với một số xử lý lỗi:
// ...
app.get('/', (req, res) => {
const app = ReactDOMServer.renderToString(<App />);
const indexFile = path.resolve('./build/index.html');
fs.readFile(indexFile, 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops, better luck next time!');
}
return res.send(
data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
);
});
});
app.use(express.static('./build'));
app.listen(PORT, () => {
console.log(Server is listening on port ${PORT});
});
Có thể import component <App>
từ ứng dụng client trực tiếp vào máy chủ.
Ba điều quan trọng của quá trình trên là:
- Express được sử dụng để phục vụ nội dung từ thư mục
build
như các tệp tĩnh. ReactDOMServer.renderToString
được dùng để render ứng dụng thành một chuỗi HTML tĩnh.- Sau khi đọc tệp
index.html
tĩnh đã được build từ phía client, nội dung của ứng dụng sẽ được đưa vào bên trong thẻ<div>
cóid="root"
. Cuối cùng, toàn bộ nội dung này sẽ được gửi về làm phản hồi (response).
Bước 3 – Cấu hình webpack, Babel và npm Scripts
Để code server hoạt động, bạn sẽ cần đóng gói (bundle) và chuyển đổi mã (transpile) bằng cách sử dụng webpack và Babel.
Lưu ý: Phần trước của hướng dẫn này đã chỉ cách cài đặt babel-core
, babel-preset-env
, và babel-preset-react-app
. Các gói này đã được lưu trữ và các phiên bản mono repo đã được sử dụng thay thế.
react-scripts
của Create React App xử lý việc cài đặt các gói sau: webpack
, webpack-cli
, webpack-node-externals
, @babel/core
, babel-loader
, @babel/preset-env
, @babel/preset-react
. Để tránh dependency tree conflict, hướng dẫn này sẽ không đề cập đến bước cài đặt này nữa.
Tiếp theo, hãy tạo một tệp cấu hình Babel mới trong thư mục gốc của dự án:
nano .babelrc.json
Sau đó, thêm các preset
env
và react-app
:
// .babelrc.json { "presets": [ "@babel/preset-env", "@babel/preset-react" ] }
Lưu ý: Phần trước của hướng dẫn này đã sử dụng tệp .babelrc
(không có đuôi tệp .json
). Đây là tệp cấu hình cho Babel 6, nhưng cấu hình này không phù hợp với Babel 7.
Bây giờ, hãy tạo một cấu hình webpack cho server sử dụng Babel Loader để chuyển đổi code. Bắt đầu bằng cách tạo tệp webpack.server.js
trong thư mục gốc của dự án:
nano webpack.server.js
Sau đó, thêm các cấu hình sau vào tệp webpack.server.js
:
`// webpack.server.js const path = require(‘path’); const nodeExternals = require(‘webpack-node-externals’);
module.exports = { entry: ‘./server/index.js’, target: ‘node’, externals: [nodeExternals()], output: { path: path.resolve(‘server-build’), filename: ‘index.js’ }, module: { rules: [ { test: /\.js$/, use: ‘babel-loader’ } ] } };`
Với cấu hình này, gói server đã được chuyển đổi sẽ được xuất ra thư mục server-build
trong một tệp có tên index.js
.
Lưu ý việc sử dụng target: 'node'
và externals: [nodeExternals()]
từ webpack-node-externals
sẽ thực hiện bỏ qua các tệp từ node_modules
trong gói. Máy chủ có thể truy cập trực tiếp các tệp này.
Điều này hoàn tất việc cài đặt các phụ thuộc và cấu hình webpack và Babel.
Bây giờ, hãy xem lại tệp package.json
và thêm các script trợ giúp của npm
:
nano package.json
Thêm các script dev:build-server
, dev:start
, và dev
vào tệp package.json
để build và phục vụ ứng dụng SSR:
// package.json "scripts": { "dev:build-server": "NODE_ENV=development webpack --config webpack.server.js --mode=development -w", "dev:start": "nodemon ./server-build/index.js", "dev": "npm-run-all --parallel build dev:*", // ... (các script khác) },
Script dev:build-server
đặt môi trường thành "development"
và gọi webpack
với tệp cấu hình bạn đã tạo trước đó. Script dev:start
gọi nodemon
để phục vụ đầu ra đã build.
Script dev
gọi npm-run-all
để chạy song song script build
và tất cả các script bắt đầu bằng dev:*
– bao gồm dev:build-server
và dev:start
.
Lưu ý: Bạn không cần sửa đổi các script hiện có "start"
, "build"
, "test"
, và "eject"
trong tệp package.json
.
nodemon
được sử dụng để khởi động lại máy chủ khi có thay đổi. npm-run-all
được dùng để chạy nhiều lệnh song song.
Hãy cài đặt các gói đó ngay bây giờ bằng cách nhập các lệnh sau vào cửa sổ terminal của bạn:
npm install nodemon@2.0.15 --save-dev npm install npm-run-all@4.1.5 --save-dev
Với những thiết lập này, bạn có thể chạy lệnh sau để build ứng dụng client-side, đóng gói và chuyển đổi code server, và khởi động server trên cổng :3006
:
npm run dev
Cấu hình webpack của server bây giờ sẽ theo dõi các thay đổi và server sẽ tự khởi động lại khi có thay đổi. Tuy nhiên, đối với ứng dụng client, bạn sẽ cần build lại thủ công mỗi khi có thay đổi.
Bây giờ, hãy mở http://localhost:3006/
trong trình duyệt web của bạn và quan sát ứng dụng đã được render từ phía máy chủ.
Trước đây, khi xem code sẽ hiển thị:
Kết quả:
<div id="root"></div>
Nhưng bây giờ, với những thay đổi bạn đã thực hiện, code sẽ hiển thị như sau:
Kết quả:
<div id="root"><h1 data-reactroot="">Hello Sammy!</h1></div>
Việc render từ phía server đã thành công chuyển đổi component <App>
thành HTML.
Kết luận
Vậy là chúng ta đã hoàn thành việc kích hoạt server-side rendering cho ứng dụng React.
Với bài viết này, chúng ta chỉ mới thảo luận những khái niệm rất cơ bản. Đi sâu hơn vào chủ đề này thì mọi thứ trở nên phức tạp hơn một chút khi định tuyến (routing), tìm nạp dữ liệu (data fetching) hoặc Redux cũng cần được tích hợp vào một ứng dụng SSR.
Một ưu điểm lớn của việc dùng SSR là ứng dụng của bạn có thể được các công cụ tìm kiếm thu thập nội dung hiệu quả hơn, kể cả với những “bot” không chạy được JavaScript. Điều này rất hữu ích nếu bạn muốn tối ưu hóa công cụ tìm kiếm (SEO) và giúp bạn cung cấp siêu dữ liệu (metadata) đầy đủ cho các kênh mạng xã hội.
SSR cũng thường cải thiện hiệu suất vì ngay trong yêu cầu đầu tiên, server sẽ gửi về một ứng dụng đã được tải đầy đủ. Tuy nhiên, với các ứng dụng phức tạp, bạn có thể thấy hiệu quả không như mong đợi. Lý do là việc thiết lập SSR có thể khá phức tạp và nó tạo ra tải trọng lớn hơn cho server. Quyết định có nên dùng SSR cho ứng dụng React của bạn hay không phụ thuộc vào nhu cầu cụ thể và việc cân nhắc những ưu nhược điểm nào phù hợp nhất với dự án của bạn.