Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu về cách để SSR hoạt động với Vue Router, tăng hiệu quả để tránh những thiếu sót có thể khiến bạn phải mất nhiều thời gian để giải quyết.
Trước khi chúng ta bắt đầu, có một vài khái niệm cơ bản bạn cần phải biết trước
- SSR liên quan đến việc tạo phiên bản ứng dụng được tải đầy đủ theo các route được yêu cầu trên Server. Trong khi đó trang web được render bằng client-side, code của client-side sẽ được tải về (không đầy đủ)
- Bạn sẽ cần sử dụng cả server side và client side trong ứng dụng của mình
Dependencies
Hãy cùng nhau tìm hiểu xem những Dependencies nào mà chúng ta cần phải cài đặt
Chúng ta sẽ sử dụng template có sẵn giúp tạo ứng dụng VueJS mà chứa các cấu hình cơ bản sử dụng Webpack. Đầu tiên chúng ta sẽ cần cài đặt vue-cli
# Cài đặt vue-cli npm install -g vue-cli # Tạo project sử dụng webpack-simple vue init webpack-simple vue-ssr
Tiếp theo chúng ta cần cài đặt toàn bộ Dependencies của webpack-simple template. Nhưng hãy nhớ rằng hiện tại chúng ta chưa làm gì để hỗ trợ SSR, chúng ta hiện tại chỉ cài đặt môi trường VueJS thông thường
# Đến thư mục project cd vue-cli # Cài đặt dependencies npm install
Giờ thì chúng ta đã có một VueJS project sẵn sàng cho cấu hình SSR. Nhưng trước khi bắt đầu, chúng ta cần cài đặt thêm 3 Dependency liên quan đến SSR
# Cài đặt vue-server-render, vue-router, express and webpack-merge npm install vue-server-renderer vue-router express webpack-merge --save
vue-server-render
: Thư viện Vue cho SSR.vue-router
: Thư viện Vue cho SPA.express
: Chúng ta cần một NodeJS server để chạywebpack-merge
: Chúng ta sẽ cần merge cấu hình của Webpack.
Cấu hình Webpack
Chúng ta sẽ cần 2 cấu hình của Webpack, 1 dùng để build các file cho client side và một được dùng để build các file cho server side.
Đầu tiên hãy cùng xem cấu hình của Webpack client, nó sẽ là cấu hình gốc của Webpack cho Server side. Chúng ta sẽ sử dụng chính cấu hình Webpack được sinh ra bởi template mà chúng ta đã cài đặt ở trên, tất nhiên nó sẽ không bao gồm những thông tin mà chúng ta đã thay đổi bên dưới đây: Sửa entry sử dụng entry-client.js
var path = require('path') var webpack = require('webpack') module.exports = { entry: './src/entry-client.js', output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'bundle.js' }, module: { rules: [ { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ], }, { test: /\.scss$/, use: [ 'vue-style-loader', 'css-loader', 'sass-loader' ], }, { test: /\.sass$/, use: [ 'vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax' ], }, { test: /\.vue$/, loader: 'vue-loader', options: { loaders: { // Since sass-loader (weirdly) has SCSS as its default parse mode, we map // the "scss" and "sass" values for the lang attribute to the right configs here. // other preprocessors should work out of the box, no loader config like this necessary. 'scss': [ 'vue-style-loader', 'css-loader', 'sass-loader' ], 'sass': [ 'vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax' ] } // other vue-loader options go here } }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, { test: /\.(png|jpg|gif|svg)$/, loader: 'file-loader', options: { name: '[name].[ext]?[hash]' } } ] }, resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' }, extensions: ['*', '.js', '.vue', '.json'] }, devServer: { historyApiFallback: true, noInfo: true, overlay: true }, performance: { hints: false }, devtool: '#eval-source-map' } if (process.env.NODE_ENV === 'production') { module.exports.devtool = '#source-map' // http://vue-loader.vuejs.org/en/workflow/production.html module.exports.plugins = (module.exports.plugins || []).concat([ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }), new webpack.LoaderOptionsPlugin({ minimize: true }) ]) }
Tiếp theo hãy tạo thêm một Webpack cho Server side với tên là webpack.server.config
var path = require('path') var webpack = require('webpack') var merge = require('webpack-merge') var baseWebpackConfig = require('./webpack.config') var webpackConfig = merge(baseWebpackConfig, { target: 'node', entry: { app: './src/entry-server.js' }, devtool: false, output: { path: path.resolve(__dirname, './dist'), filename: 'server.bundle.js', libraryTarget: 'commonjs2' }, externals: Object.keys(require('./package.json').dependencies), plugins: [ new webpack.DefinePlugin({ 'process.env': 'production' }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) ] }) module.exports = webpackConfig
Không có gì mới ở đây ngoại trừ 2 điều sau: Entry sẽ là entry-server.js
và trong output chúng ta sử dụng commonjs2
Như vậy chúng ta đã cấu hình xong Webpack. Tiếp theo hãy cùng nhau cấu hình các script build ứng dụng trong package.json
Cấu hình package.json
Bạn có thể cấu hình lại nếu bạn cần, nhưng có 3 bước mà bạn cần nhớ khi chạy ứng dụng của mình
- Bạn cần build client script:
build-client
- Bạn cần build server script:
build-server
- Bạn cần start một server để chạy:
start-server
"scripts": { "start": "npm run build && npm run start-server", "build": "npm run build-client && npm run build-server", "build-client": "cross-env NODE_ENV=production webpack --progress --hide-modules", "build-server": "cross-env NODE_ENV=production webpack --config webpack.server.config.js --progress --hide-modules", "start-server": "node server.js" }
Trong cấu hình bên trên, chúng ta sử dụng start
trong scripts
để thực hiện 3 bước bên trên. Nhưng chúng ta cũng có thể thiệt lập các scripts
để chạy chúng tách biệt nếu cần
Cấu trúc thư mục
- Thư mục dist được tạo bởi Webpack khi Webpack build
- node_modules cài đặt các Dependency
- Src chứa code Vue của chúng ta. Ở bên trong nó chứa cả source code của Server và Client,
main.js
của Vue, các component code của Vue, thư mục Router để chứa các router, thư mục ui chứa các trang hay component của vue và cuối cùng là thư mụcassets
- index.html là file HTML chính của project
- server.js là file cấu hình server và start ứng dụng
- Cuối cùng là 2 file cấu hình Webpack
Index HTML
Đây là file index.html
<!doctype html> <html lang="en"> <head> <!-- use triple mustache for non-HTML-escaped interpolation --> {{{ meta }}} <!-- use double mustache for HTML-escaped interpolation --> <title>{{ title }}</title> </head> <body> <!--vue-ssr-outlet--> <script src="dist/bundle.js"></script> </body> </html>
Có 2 điều cần chú ý
<!--vue-ssr-outlet-->
đây là nơi dữ liệu được điền vào từ server. Đây là một tính năng của Vue SSR mà chúng ta sẽ tìm hiểu sau đây- Chúng ta sử dụng
build.js
, file này được tạo ra bởi Webpack của client
App.vue
Component này là component gốc của ứng dụng và nó có một vài nhiệm vụ chính như sau:
- Cấu hình cho menu chứa các link của Router
- Cài đặt container cho các thành phần route để render
- Cài đặt các phần từ bên trong
div
với idapp
mà sẽ được mounting phần client side của ứng dụng
<template> <div id="app"> Server side render for Vue <p> <router-link to="/">Go To Home</router-link> <router-link to="/about">Go To About</router-link> </p> <router-view></router-view> </div> </template>
Cấu hình file Router
Bởi vì ứng dụng của chúng ta sẽ start một server, chúng ta cần cung cấp một instance mới của router cho mỗi reuqest của server. Bên trong thư mục Router, chúng ta sẽ có một file chứa thông tin cấu hình router
// router.js import Vue from 'vue'; import Router from 'vue-router'; import Home from '../ui/home/Home.vue'; import About from '../ui/about/About.vue'; Vue.use(Router); export function createRouter () { return new Router({ mode: 'history', routes: [ { path: '/', component: Home }, { path: '/about', component: About } ] }); }
Nào hãy cùng tìm hiểu đoạn code ở bên trên
- Chúng ta import tất cả các Dependency cần thiết
- Chúng ta sử dụng router của Vue
- Chúng ta export một hàm cung cấp một instance mới của cấu hình Router
- Chúng ta khởi tạo router với chế độ history và khai báo 2 router mà chúng ta cần kiểm soát một trỏ đến trang Home và 1 đến trang About
Như ở trên chúng ta thấy rằng router đang trỏ đến 2 page là Home va About thế nên chúng ta cần tạo 2 trang này trong thư mục ui/home và ui/about
Home.vue
<template> <div> {{ homeText }} </div> </template> <script> export default { data() { return { homeText: 'Home page' }; } }; </script>
About.vue
<template> <div> {{ aboutText }} </div> </template> <script> export default { data() { return { aboutText: 'About Page' }; } }; </script>
Cấu hình file main.js
Với lý do tương tự như ở trên, chúng ta cần cung cấp một instance mới của router, chúng ta cần cung cập một instance mới cho ứng dụng. File này có nhiệm vụ khởi tạo router và component gốc app.vue. Cả Server và Client sẽ đều sử dụng file này
// main.js import Vue from 'vue' import App from './App.vue' import { createRouter } from './router/router.js' // export a factory function for creating fresh app, router and store // instances export function createApp() { // create router instance const router = createRouter(); const app = new Vue({ router, // the root instance simply renders the App component. render: h => h(App) }); return { app, router }; }
Nào hãy cùng tìm hiểu đoạn code ở trên
- Chúng ta import tất cả các Dependency cần thiết
- Chúng ta export một hàm cung cấp một instance mới của app và router
- Chúng ta khởi tạo router sử dụng phương thức mà chúng ta đã tạo ở bên trên trong file router.js
- Chúng ta tạo một instance mới của app với router và hàm render, truyền vào component gốc app
- Chúng ta trả về cả hai app và router
Client
Code file này tương đối đơn giản. File này là file ban đầu cho Webpack client build cấu hình
//client-entry.js import { createApp } from './main.js'; const { app } = createApp() // Giả định App.vue element của template gốc có `id="app"` app.$mount('#app')
Nào hãy cùng tìm hiểu đoạn code bên trên
- Chúng ta import tất cả các Dependency cần thiết
- Chúng ta tạo
app
từ filemain.js
và dữ instaceapp
- Chúng ta mount app trong một node với id là
#app
. Trong ví dụ của chúng ta node mà chứa id đó chính là phần tử nằm trong template gốc của component App.vue
Server
File này là file ban đầu để cho Webpack Server build.
/main.js'; export default context => { // trả về một Promise do đó server có thể đợi cho tới khi // Tất cả đều sẵn sàng trước khi render. return new Promise((resolve, reject) => { const { app, router } = createApp(); // thiết lập ví trí của server-side router router.push(context.url); // Đợi cho đến khi router sẵn sàng wait until router.onReady(() => { const matchedComponents = router.getMatchedComponents(); if (!matchedComponents.length) { return reject({ code: 404 }); } resolve(app); }, reject); }); }
Nào hãy cùng tìm hiểu đoạn code bên trên
- Chúng ta import tất cả các Dependency cần thiết
- Chúng ta export một hàm mà nhận context như tham số
- Hàm trả về một promise
- Chúng ta khởi tạo app và router từ hàm
createApp()
trong filemain.js
- Lấy thông tin URL hiên tại từ context mục đích để một URL chính xác vào trong router
- Khi router đã sẵn sàng chúng ta sẽ kiểm tra route có giống với URL của context hay không. Nếu đúng chúng ta sẽ thực hiện
resolve(app)
và trả về một app instance. Nếu không thì reject Promise
Cấu hình và start Server
Chúng ta có hầu như tất cả mọi thứ sẵn sàng. Chỉ có một điều thiếu nữa là cấu hình và start Server dùng express.
//server.js const express = require('express'); const server = express(); const fs = require('fs'); const path = require('path'); //obtain bundle const bundle = require('./dist/server.bundle.js'); //get renderer from vue server renderer const renderer = require('vue-server-renderer').createRenderer({ //set template template: fs.readFileSync('./index.html', 'utf-8') }); server.use('/dist', express.static(path.join(__dirname, './dist'))); //start server server.get('*', (req, res) => { bundle.default({ url: req.url }).then((app) => { //context to use as data source //in the template for interpolation const context = { title: 'Vue JS - Server Render', meta: ` <meta description="vuejs server side render"> ` }; renderer.renderToString(app, context, function (err, html) { if (err) { if (err.code === 404) { res.status(404).end('Page not found') } else { res.status(500).end('Internal Server Error') } } else { res.end(html) } }); }, (err) => { console.log(err); }); }); server.listen(8080);
Đoạn code khá dài đúng không? Chúng ta sẽ cùng tìm hiểu xem nó hoạt động như thế nào
- Chúng ta đang import express để tạo Server. Chúng ta cũng đang import một vài tính năng của NodeJS
- Chúng ta cũng import file bundle của Server được tạo bởi Webpack.
- Chúng ta cũng import thư viện
vue-server-renderer
, để tạo ra renderer, cung cấp vị tríindex.html
cho template - Chúng ta cấu hình đường dẫn express
- Chúng ta start server
- bundle được tạo ra bởi Webpack và
entry-server.js
, nên chúng ta có thể sử dụng các hàm mặc định mà nhận context như tham số chứa URL. Sau đó nó trả về Promise, kết quả có thể thành công hoặc lỗi
Nếu kết quả trả về là thành công
- Chúng ta sẽ tạo một hằng số với dữ liệu mà sẽ được thêm vào trong index.html. (chúng ta đã nhìn thấy trong phần index.html ở trên)
- Chúng ta sẽ gọi hàm
renderToString
nhận app (được trả về bởi Promise), context chúng ta tạo và hàm callback nếu tất cả hoạt động tốt. - Hàm
renderToString
sẽ kiểm tra lỗi, nếu không có gì xảy ra, nó sẽ gửi một HTML được tạo ra như một response
Cuối cùng Server bắt đầu listen port 8080
Nào bây giờ chúng ta thử chạy script start và mở địa chỉ localhost:8080
trên trình duyệt bạn sẽ nhìn thất kết quả của SSR với Vue-router
Source code sử dụng branch vue-ssr