手把手教你搭建Vue服務(wù)端渲染項(xiàng)目
建議先閱讀官方指南——SSR.vuejs.org/zh/" _fcksavedurl="https://SSR.vuejs.org/zh/">Vue.js 服務(wù)器端渲染指南,再回到本文開(kāi)始閱讀。
本文將分成以下兩部分:
- 簡(jiǎn)述 Vue SSR 過(guò)程
- 從零開(kāi)始搭建 SSR 項(xiàng)目
好了,下面開(kāi)始正文。
簡(jiǎn)述 Vue SSR 過(guò)程
客戶端渲染過(guò)程
- 訪問(wèn)客戶端渲染的網(wǎng)站。
- 服務(wù)器返回一個(gè)包含了引入資源語(yǔ)句和 <div id="app"></div> 的 HTML 文件。
- 客戶端通過(guò) HTTP 向服務(wù)器請(qǐng)求資源,當(dāng)必要的資源都加載完畢后,執(zhí)行 new Vue() 開(kāi)始實(shí)例化并渲染頁(yè)面。
服務(wù)端渲染過(guò)程
- 訪問(wèn)服務(wù)端渲染的網(wǎng)站。
- 服務(wù)器會(huì)查看當(dāng)前路由組件需要哪些資源文件,然后將這些文件的內(nèi)容填充到 HTML 文件。如果有 asyncData() 函數(shù),就會(huì)執(zhí)行它進(jìn)行數(shù)據(jù)預(yù)取并填充到 HTML 文件里,最后返回這個(gè) HTML 頁(yè)面。
3. 當(dāng)客戶端接收到這個(gè) HTML 頁(yè)面時(shí),可以馬上就開(kāi)始渲染頁(yè)面。與此同時(shí),頁(yè)面也會(huì)加載資源,當(dāng)必要的資源都加載完畢后,開(kāi)始執(zhí)行 new Vue() 開(kāi)始實(shí)例化并接管頁(yè)面。
從上述兩個(gè)過(guò)程中,可以看出,區(qū)別就在于第二步??蛻舳虽秩镜木W(wǎng)站會(huì)直接返回 HTML 文件,而服務(wù)端渲染的網(wǎng)站則會(huì)渲染完頁(yè)面再返回這個(gè) HTML 文件。
這樣做的好處是什么?是更快的內(nèi)容到達(dá)時(shí)間 (time-to-content)。
假設(shè)你的網(wǎng)站需要加載完 abcd 四個(gè)文件才能渲染完畢。并且每個(gè)文件大小為 1 M。
這樣一算:客戶端渲染的網(wǎng)站需要加載 4 個(gè)文件和 HTML 文件才能完成首頁(yè)渲染,總計(jì)大小為 4M(忽略 HTML 文件大?。6?wù)端渲染的網(wǎng)站只需要加載一個(gè)渲染完畢的 HTML 文件就能完成首頁(yè)渲染,總計(jì)大小為已經(jīng)渲染完畢的 HTML 文件(這種文件不會(huì)太大,一般為幾百K,我的個(gè)人博客網(wǎng)站(SSR)加載的 HTML 文件為 400K)。這就是服務(wù)端渲染更快的原因。
客戶端接管頁(yè)面
對(duì)于服務(wù)端返回來(lái)的 HTML 文件,客戶端必須進(jìn)行接管,對(duì)其進(jìn)行 new Vue() 實(shí)例化,用戶才能正常使用頁(yè)面。
如果不對(duì)其進(jìn)行激活的話,里面的內(nèi)容只是一串字符串而已,例如下面的代碼,點(diǎn)擊是無(wú)效的:
- <button @click="sayHi">如果不進(jìn)行激活,點(diǎn)我是不會(huì)觸發(fā)事件的</button>
那客戶端如何接管頁(yè)面呢?下面引用一篇文章中的內(nèi)容:
客戶端 new Vue() 時(shí),客戶端會(huì)和服務(wù)端生成的DOM進(jìn)行Hydration對(duì)比(判斷這個(gè)DOM和自己即將生成的DOM是否相同(vuex store 數(shù)據(jù)同步才能保持一致)
如果相同就調(diào)用app.$mount('#app')將客戶端的vue實(shí)例掛載到這個(gè)DOM上,即去“激活”這些服務(wù)端渲染的HTML之后,其變成了由Vue動(dòng)態(tài)管理的DOM,以便響應(yīng)后續(xù)數(shù)據(jù)的變化,即之后所有的交互和vue-router不同頁(yè)面之間的跳轉(zhuǎn)將全部在瀏覽器端運(yùn)行。
如果客戶端構(gòu)建的虛擬 DOM 樹(shù)與服務(wù)器渲染返回的HTML結(jié)構(gòu)不一致,這時(shí)候,客戶端會(huì)請(qǐng)求一次服務(wù)器再渲染整個(gè)應(yīng)用程序,這使得SSR失效了,達(dá)不到服務(wù)端渲染的目的了
小結(jié)
不管是客戶端渲染還是服務(wù)端渲染,都需要等待客戶端執(zhí)行 new Vue() 之后,用戶才能進(jìn)行交互操作。但服務(wù)端渲染的網(wǎng)站能讓用戶更快的看見(jiàn)頁(yè)面。
從零開(kāi)始搭建 SSR 項(xiàng)目
配置 weback
webpack 配置文件共有 3 個(gè):
- webpack.base.config.js,基礎(chǔ)配置文件,客戶端與服務(wù)端都需要它。
- webpack.client.config.js,客戶端配置文件,用于生成客戶端所需的資源。
- webpack.server.config.js,服務(wù)端配置文件,用于生成服務(wù)端所需的資源。
webpack.base.config.js 基礎(chǔ)配置文件
- const path = require('path')
- const { VueLoaderPlugin } = require('vue-loader')
- const isProd = process.env.NODE_ENV === 'production'
- function resolve(dir) {
- return path.join(__dirname, '..', dir)
- }
- module.exports = {
- context: path.resolve(__dirname, '../'),
- devtool: isProd ? 'source-map' : '#cheap-module-source-map',
- output: {
- path: path.resolve(__dirname, '../dist'),
- publicPath: '/dist/',
- // chunkhash 同屬一個(gè) chunk 中的文件修改了,文件名會(huì)發(fā)生變化
- // contenthash 只有文件自己的內(nèi)容變化了,文件名才會(huì)變化
- filename: '[name].[contenthash].js',
- // 此選項(xiàng)給打包后的非入口js文件命名,與 SplitChunksPlugin 配合使用
- chunkFilename: '[name].[contenthash].js',
- },
- resolve: {
- extensions: ['.js', '.vue', '.json', '.css'],
- alias: {
- public: resolve('public'),
- '@': resolve('src')
- }
- },
- module: {
- // https://juejin.im/post/6844903689103081485
- // 使用 `mini-css-extract-plugin` 插件打包的的 `server bundle` 會(huì)使用到 document。
- // 由于 node 環(huán)境中不存在 document 對(duì)象,所以報(bào)錯(cuò)。
- // 解決方案:樣式相關(guān)的 loader 不要放在 `webpack.base.config.js` 文件
- // 將其分拆到 `webpack.client.config.js` 和 `webpack.client.server.js` 文件
- // 其中 `mini-css-extract-plugin` 插件要放在 `webpack.client.config.js` 文件配置。
- rules: [
- {
- test: /\.vue$/,
- loader: 'vue-loader',
- options: {
- compilerOptions: {
- preserveWhitespace: false
- }
- }
- },
- {
- test: /\.js$/,
- loader: 'babel-loader',
- exclude: /node_modules/
- },
- {
- test: /\.(png|svg|jpg|gif|ico)$/,
- use: ['file-loader']
- },
- {
- test: /\.(woff|eot|ttf)\??.*$/,
- loader: 'url-loader?name=fonts/[name].[md5:hash:hex:7].[ext]'
- },
- ]
- },
- plugins: [new VueLoaderPlugin()],
- }
基礎(chǔ)配置文件比較簡(jiǎn)單,output 屬性的意思是打包時(shí)根據(jù)文件內(nèi)容生成文件名稱。module 屬性配置不同文件的解析 loader。
webpack.client.config.js 客戶端配置文件
- const webpack = require('webpack')
- const merge = require('webpack-merge')
- const base = require('./webpack.base.config')
- const CompressionPlugin = require('compression-webpack-plugin')
- const WebpackBar = require('webpackbar')
- const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
- const MiniCssExtractPlugin = require('mini-css-extract-plugin')
- const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
- const isProd = process.env.NODE_ENV === 'production'
- const plugins = [
- new webpack.DefinePlugin({
- 'process.env.NODE_ENV': JSON.stringify(
- process.env.NODE_ENV || 'development'
- ),
- 'process.env.VUE_ENV': '"client"'
- }),
- new VueSSRClientPlugin(),
- new MiniCssExtractPlugin({
- filename: 'style.css'
- })
- ]
- if (isProd) {
- plugins.push(
- // 開(kāi)啟 gzip 壓縮 https://github.com/woai3c/node-blog/blob/master/doc/optimize.md
- new CompressionPlugin(),
- // 該插件會(huì)根據(jù)模塊的相對(duì)路徑生成一個(gè)四位數(shù)的hash作為模塊id, 用于生產(chǎn)環(huán)境。
- new webpack.HashedModuleIdsPlugin(),
- new WebpackBar(),
- )
- }
- const config = {
- entry: {
- app: './src/entry-client.js'
- },
- plugins,
- optimization: {
- runtimeChunk: {
- name: 'manifest'
- },
- splitChunks: {
- cacheGroups: {
- vendor: {
- name: 'chunk-vendors',
- test: /[\\/]node_modules[\\/]/,
- priority: -10,
- chunks: 'initial',
- },
- common: {
- name: 'chunk-common',
- minChunks: 2,
- priority: -20,
- chunks: 'initial',
- reuseExistingChunk: true
- }
- },
- }
- },
- module: {
- rules: [
- {
- test: /\.css$/,
- use: [
- {
- loader: MiniCssExtractPlugin.loader,
- options: {
- // 解決 export 'default' (imported as 'mod') was not found
- // 啟用 CommonJS 語(yǔ)法
- esModule: false,
- },
- },
- 'css-loader'
- ]
- }
- ]
- },
- }
- if (isProd) {
- // 壓縮 css
- config.optimization.minimizer = [
- new CssMinimizerPlugin(),
- ]
- }
- module.exports = merge(base, config)
客戶端配置文件中的 config.optimization 屬性是打包時(shí)分割代碼用的。它的作用是將第三方庫(kù)都打包在一起。
其他插件作用:
- MiniCssExtractPlugin 插件, 將 css 提取出來(lái)單獨(dú)打包。
- CssMinimizerPlugin 插件,壓縮 css。
- CompressionPlugin 插件,將資源壓縮成 gzip 格式(大大提升傳輸效率)。另外還需要在 node 服務(wù)器上引入 compression 插件配合使用。
- WebpackBar 插件,打包時(shí)顯示進(jìn)度條。
webpack.server.config.js 服務(wù)端配置文件
- const webpack = require('webpack')
- const merge = require('webpack-merge')
- const base = require('./webpack.base.config')
- const nodeExternals = require('webpack-node-externals') // Webpack allows you to define externals - modules that should not be bundled.
- const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
- const WebpackBar = require('webpackbar')
- const plugins = [
- new webpack.DefinePlugin({
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
- 'process.env.VUE_ENV': '"server"'
- }),
- new VueSSRServerPlugin()
- ]
- if (process.env.NODE_ENV == 'production') {
- plugins.push(
- new WebpackBar()
- )
- }
- module.exports = merge(base, {
- target: 'node',
- devtool: '#source-map',
- entry: './src/entry-server.js',
- output: {
- filename: 'server-bundle.js',
- libraryTarget: 'commonjs2'
- },
- externals: nodeExternals({
- allowlist: /\.css$/ // 防止將某些 import 的包(package)打包到 bundle 中,而是在運(yùn)行時(shí)(runtime)再去從外部獲取這些擴(kuò)展依賴
- }),
- plugins,
- module: {
- rules: [
- {
- test: /\.css$/,
- use: [
- 'vue-style-loader',
- 'css-loader'
- ]
- }
- ]
- },
- })
服務(wù)端打包和客戶端不同,它將所有文件一起打包成一個(gè)文件 server-bundle.js。同時(shí)解析 css 需要使用 vue-style-loader,這一點(diǎn)在官方指南中有說(shuō)明:
配置服務(wù)器
生產(chǎn)環(huán)境
pro-server.js 生產(chǎn)環(huán)境服務(wù)器配置文件
- const fs = require('fs')
- const path = require('path')
- const express = require('express')
- const setApi = require('./api')
- const LRU = require('lru-cache') // 緩存
- const { createBundleRenderer } = require('vue-server-renderer')
- const favicon = require('serve-favicon')
- const resolve = file => path.resolve(__dirname, file)
- const app = express()
- // 開(kāi)啟 gzip 壓縮 https://github.com/woai3c/node-blog/blob/master/doc/optimize.md
- const compression = require('compression')
- app.use(compression())
- // 設(shè)置 favicon
- app.use(favicon(resolve('../public/favicon.ico')))
- // 新版本 需要加 new,舊版本不用
- const microCache = new LRU({
- max: 100,
- maxAge: 60 * 60 * 24 * 1000 // 重要提示:緩存資源將在 1 天后過(guò)期。
- })
- const serve = (path) => {
- return express.static(resolve(path), {
- maxAge: 1000 * 60 * 60 * 24 * 30
- })
- }
- app.use('/dist', serve('../dist', true))
- function createRenderer(bundle, options) {
- return createBundleRenderer(
- bundle,
- Object.assign(options, {
- basedir: resolve('../dist'),
- runInNewContext: false
- })
- )
- }
- function render(req, res) {
- const hit = microCache.get(req.url)
- if (hit) {
- console.log('Response from cache')
- return res.end(hit)
- }
- res.setHeader('Content-Type', 'text/html')
- const handleError = err => {
- if (err.url) {
- res.redirect(err.url)
- } else if (err.code === 404) {
- res.status(404).send('404 | Page Not Found')
- } else {
- res.status(500).send('500 | Internal Server Error~')
- console.log(err)
- }
- }
- const context = {
- title: 'SSR 測(cè)試', // default title
- url: req.url
- }
- renderer.renderToString(context, (err, html) => {
- if (err) {
- return handleError(err)
- }
- microCache.set(req.url, html)
- res.send(html)
- })
- }
- const templatePath = resolve('../public/index.template.html')
- const template = fs.readFileSync(templatePath, 'utf-8')
- const bundle = require('../dist/vue-SSR-server-bundle.json')
- const clientManifest = require('../dist/vue-SSR-client-manifest.json') // 將js文件注入到頁(yè)面中
- const renderer = createRenderer(bundle, {
- template,
- clientManifest
- })
- const port = 8080
- app.listen(port, () => {
- console.log(`server started at localhost:${ port }`)
- })
- setApi(app)
- app.get('*', render)
從代碼中可以看到,當(dāng)首次加載頁(yè)面時(shí),需要調(diào)用 createBundleRenderer() 生成一個(gè) renderer,它的參數(shù)是打包生成的 vue-SSR-server-bundle.json 和 vue-SSR-client-manifest.json 文件。當(dāng)返回 HTML 文件后,頁(yè)面將會(huì)被客戶端接管。
在文件的最后有一行代碼 app.get('*', render),它表示所有匹配不到的請(qǐng)求都交給它處理。所以如果你寫(xiě)了 ajax 請(qǐng)求處理函數(shù)必須放在前面,就像下面這樣:
- app.get('/fetchData', (req, res) => { ... })
- app.post('/changeData', (req, res) => { ... })
- app.get('*', render)
否則你的頁(yè)面會(huì)打不開(kāi)。
開(kāi)發(fā)環(huán)境
開(kāi)發(fā)環(huán)境的服務(wù)器配置和生產(chǎn)環(huán)境沒(méi)什么不同,區(qū)別在于開(kāi)發(fā)環(huán)境下的服務(wù)器有熱更新。
一般用 webpack 進(jìn)行開(kāi)發(fā)時(shí),簡(jiǎn)單的配置一下 dev server 參數(shù)就可以使用熱更新了,但是 SSR 項(xiàng)目需要自己配置。
由于 SSR 開(kāi)發(fā)環(huán)境服務(wù)器的配置文件 setup-dev-server.js 代碼太多,我對(duì)其進(jìn)行簡(jiǎn)化后,大致代碼如下:
- // dev-server.js
- const express = require('express')
- const webpack = require('webpack')
- const webpackConfig = require('../build/webpack.dev') // 獲取 webpack 配置文件
- const compiler = webpack(webpackConfig)
- const app = express()
- app.use(require('webpack-hot-middleware')(compiler))
- app.use(require('webpack-dev-middleware')(compiler, {
- noInfo: true,
- stats: {
- colors: true
- }
- }))
同時(shí)需要在 webpack 的入口文件加上這一行代碼 webpack-hot-middleware/client?reload=true。
- // webpack.dev.js
- const merge = require('webpack-merge')
- const webpackBaseConfig = require('./webpack.base.config.js') // 這個(gè)配置和熱更新無(wú)關(guān),可忽略
- module.exports = merge(webpackBaseConfig, {
- mode: 'development',
- entry: {
- app: ['webpack-hot-middleware/client?reload=true' , './client/main.js'] // 開(kāi)啟熱模塊更新
- },
- plugins: [new webpack.HotModuleReplacementPlugin()]
- })
然后使用 node dev-server.js 來(lái)開(kāi)啟前端代碼熱更新。
熱更新主要使用了兩個(gè)插件:webpack-dev-middleware 和 webpack-hot-middleware。顧名思義,看名稱就知道它們的作用,
webpack-dev-middleware 的作用是生成一個(gè)與 webpack 的 compiler 綁定的中間件,然后在 express 啟動(dòng)的 app 中調(diào)用這個(gè)中間件。
這個(gè)中間件的作用呢,簡(jiǎn)單總結(jié)為以下三點(diǎn):通過(guò)watch mode,監(jiān)聽(tīng)資源的變更,然后自動(dòng)打包; 快速編譯,走內(nèi)存;返回中間件,支持express 的 use 格式。
webpack-hot-middleware 插件的作用就是熱更新,它需要配合 HotModuleReplacementPlugin 和 webpack-dev-middleware 一起使用。
打包文件 vue-SSR-client-manifest.json 和 vue-SSR-server-bundle.json
webpack 需要對(duì)源碼打包兩次,一次是為客戶端環(huán)境打包的,一次是為服務(wù)端環(huán)境打包的。
為客戶端環(huán)境打包的文件,和以前我們打包的資源一樣,不過(guò)多出了一個(gè) vue-SSR-client-manifest.json 文件。服務(wù)端環(huán)境打包只輸出一個(gè) vue-SSR-server-bundle.json 文件。
vue-SSR-client-manifest.json 包含了客戶端環(huán)境所需的資源名稱:
從上圖中可以看到有三個(gè)關(guān)鍵詞:
- all,表示這是打包的所有資源。
- initial,表示首頁(yè)加載必須的資源。
- async,表示需要異步加載的資源。
vue-SSR-server-bundle.json 文件:
- entry, 服務(wù)端入口文件。
- files,服務(wù)端依賴的資源。
填坑記錄
1. [vue-router] failed to resolve async component default: referenceerror: window is not defined
由于在一些文件或第三方文件中可能會(huì)用到 window 對(duì)象,并且 node 中不存在 window 對(duì)象,所以會(huì)報(bào)錯(cuò)。
此時(shí)可在 src/app.js 文件加上以下代碼進(jìn)行判斷:
- // 在 app.js 文件添加上這段代碼,對(duì)環(huán)境進(jìn)行判斷
- if (typeof window === 'undefined') {
- global.window = {}
- }
2. mini-css-extract-plugin 插件造成 ReferenceError: document is not defined
使用 mini-css-extract-plugin 插件打包的的 server bundle, 會(huì)使用到 document。由于 node 環(huán)境中不存在 document 對(duì)象,所以報(bào)錯(cuò)。
解決方案:樣式相關(guān)的 loader 不要放在 webpack.base.config.js 文件,將其分拆到 webpack.client.config.js 和 webpack.client.server.js 文件。其中 mini-css-extract-plugin 插件要放在 webpack.client.config.js 文件配置。
base
- module: {
- rules: [
- {
- test: /\.vue$/,
- loader: 'vue-loader',
- options: {
- compilerOptions: {
- preserveWhitespace: false
- }
- }
- },
- {
- test: /\.js$/,
- loader: 'babel-loader',
- exclude: /node_modules/
- },
- {
- test: /\.(png|svg|jpg|gif|ico)$/,
- use: ['file-loader']
- },
- {
- test: /\.(woff|eot|ttf)\??.*$/,
- loader: 'url-loader?name=fonts/[name].[md5:hash:hex:7].[ext]'
- },
- ]
- }
client
- module: {
- rules: [
- {
- test: /\.css$/,
- use: [
- {
- loader: MiniCssExtractPlugin.loader,
- options: {
- // 解決 export 'default' (imported as 'mod') was not found
- esModule: false,
- },
- },
- 'css-loader'
- ]
- }
- ]
- }
server
- module: {
- rules: [
- {
- test: /\.css$/,
- use: [
- 'vue-style-loader',
- 'css-loader'
- ]
- }
- ]
- }
3. 開(kāi)發(fā)環(huán)境下跳轉(zhuǎn)頁(yè)面樣式不生效,但生產(chǎn)環(huán)境正常。
由于開(kāi)發(fā)環(huán)境使用的是 memory-fs 插件,打包文件是放在內(nèi)存中的。如果此時(shí) dist 文件夾有剛才打包留下的資源,就會(huì)使用 dist 文件夾中的資源,而不是內(nèi)存中的資源。并且開(kāi)發(fā)環(huán)境和打包環(huán)境生成的資源名稱是不一樣的,所以就造成了這個(gè) BUG。
解決方法是執(zhí)行 npm run dev 時(shí),刪除 dist 文件夾。所以要在 npm run dev 對(duì)應(yīng)的腳本中加上 rimraf dist。
- "dev": "rimraf dist && node ./server/dev-server.js --mode development",
4. [vue-router] Failed to resolve async component default: ReferenceError: document is not defined
不要在有可能使用到服務(wù)端渲染的頁(yè)面訪問(wèn) DOM,如果有這種操作請(qǐng)放在 mounted() 鉤子函數(shù)里。
如果你引入的數(shù)據(jù)或者接口有訪問(wèn) DOM 的操作也會(huì)報(bào)這種錯(cuò),在這種情況下可以使用 require()。因?yàn)?require() 是運(yùn)行時(shí)加載的,所以可以這樣使用:
- <script>
- // 原來(lái)報(bào)錯(cuò)的操作,這個(gè)接口有 DOM 操作,所以這樣使用的時(shí)候在服務(wù)端會(huì)報(bào)錯(cuò)。
- import { fetchArticles } from '@/api/client'
- export default {
- methods: {
- getAppointArticles() {
- fetchArticles({
- tags: this.tags,
- pageSize: this.pageSize,
- pageIndex: this.pageIndex,
- })
- .then(res => {
- this.$store.commit('setArticles', res)
- })
- },
- }
- }
- </script>
修改后:
- <script>
- // 先定義一個(gè)外部變量,在 mounted() 鉤子里賦值
- let fetchArticles
- export default {
- mounted() {
- // 由于服務(wù)端渲染不會(huì)有 mounted() 鉤子,所以在這里可以保證是在客戶端的情況下引入接口
- fetchArticles = require('@/api/client').fetchArticles
- },
- methods: {
- getAppointArticles() {
- fetchArticles({
- tags: this.tags,
- pageSize: this.pageSize,
- pageIndex: this.pageIndex,
- })
- .then(res => {
- this.$store.commit('setArticles', res)
- })
- },
- }
- }
- </script>
修改后可以正常使用。
5. 開(kāi)發(fā)環(huán)境下,開(kāi)啟服務(wù)器后無(wú)任何反應(yīng),也沒(méi)見(jiàn)控制臺(tái)輸出報(bào)錯(cuò)信息。
這個(gè)坑其實(shí)是有報(bào)錯(cuò)信息的,但是沒(méi)有輸出,導(dǎo)致以為沒(méi)有錯(cuò)誤。
在 setup-dev-server.js 文件中有一行代碼 if (stats.errors.length) return,如果有報(bào)錯(cuò)就直接返回,不執(zhí)行后續(xù)的操作。導(dǎo)致服務(wù)器沒(méi)任何反應(yīng),所以我們可以在這打一個(gè) console.log 語(yǔ)句,打印報(bào)錯(cuò)信息。
小結(jié)
這個(gè) DEMO 是基于官方 DEMO vue-hackernews-2.0 改造的。不過(guò)官方 DEMO 發(fā)表于 4 年前,最近修改時(shí)間是 2 年前,很多選項(xiàng)參數(shù)已經(jīng)過(guò)時(shí)了。并且官方 DEMO 需要翻墻才能使用。所以我在此基礎(chǔ)上對(duì)其進(jìn)行了改造,改造后的 DEMO 放在 SSR-demo" _fcksavedurl="https://github.com/woai3c/vue-SSR-demo">Github 上,它是一個(gè)比較完善的 DEMO,可以在此基礎(chǔ)上進(jìn)行二次開(kāi)發(fā)。
如果你不僅僅滿足于一個(gè) DEMO,建議看一看我的個(gè)人博客項(xiàng)目,它原來(lái)是客戶端渲染的項(xiàng)目,后來(lái)重構(gòu)為服務(wù)端渲染,絕對(duì)實(shí)戰(zhàn)。