1)、服務器端渲染:這種技術方案在前端領域處於蠻荒時代就已出現,當時的解決方案主要是後臺開發經過模板引擎來設計(如:Java Web的JSP);簡而言之,就是模板頁面在後臺獲取到數據並填充,而後響應返回模板頁面html字符串給瀏覽器渲染;javascript
1)、優化SEO:它主要解決搜索引擎SEO優化(Ajax異步請求是SEO優化的一大阻力;例如:去超市買A商品(數據),正好A商品沒有,須要等待一段時間去倉庫調取;做爲消費者(搜索引擎)你能等嗎?);css
2)、優化單頁應用的首屏加載時間:現今單頁面應用大行其道,單頁面應用解決了頁面無感加載,可是帶來了首屏加載緩慢;經過服務器端渲染機制能夠很好的解決首屏頁面加載問題。固然這不是惟一的解決方案(合理拆分紅多頁面應用也能夠解決);html
1)、純後臺技術實現:利用後臺語言模板引擎進行服務器端渲染方案落地。對於前端來講,能夠利用node做爲中間件,而後利用node的ejs模板引擎負責數據填充,最後經過node路由響應機制輸出html字符串給客戶端瀏覽器進行渲染;前端
2)、構建同構應用:同構應用就是能夠同時運行在客戶端和服務器端的Web應用;這種通常採用webpack構建工具和開源工具進行實現;如下會以Vue來介紹同構應用;這種實現方式相對於上面的方案更復雜,開發難度大;可是能夠享受到Vue框架帶來的便利(響應式數據,路由無感切換等便利)。前提須要「客戶端激活」。vue
客戶端激活:官方術語,能夠理解爲服務器端渲染成html字符串給瀏覽器以後,須要引入客戶端的bundleClient文件,這個環節就交給客戶端處理了;java
1)、準備什麼?node
2)、注意什麼?webpack
一、事先準備:工欲善其事必先安裝包,話很少說;構建同步應用所須要的package.json包。這裏不解釋包的做用;git
"dependencies": {
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"autoprefixer": "^9.5.1",
"babel-loader": "^8.0.6",
"babel-plugin-dynamic-import-webpack": "^1.1.0",
"css-loader": "^2.1.1",
"extract-text-webpack-plugin": "^3.0.2",
"html-webpack-plugin": "^3.2.0",
"koa": "^2.7.0",
"koa-router": "^7.4.0",
"koa-static": "^5.0.0",
"mini-css-extract-plugin": "^0.7.0",
"postcss-loader": "^3.0.0",
"url-loader": "^1.1.2",
"vue": "^2.6.10",
"vue-loader": "^15.7.0",
"vue-router": "^3.0.6",
"vue-server-renderer": "^2.6.10",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"vuex": "^3.1.1",
"vuex-router-sync": "^5.0.0",
"webpack": "^4.32.2",
"webpack-cli": "^3.3.2",
"webpack-dev-server": "^3.4.1",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
}
複製代碼
在項目根目錄下建立.babelrc文件github
{
"presets": [
"@babel/preset-env"
],
"plugins": [
// 支持路由動態加載的寫法 const Foo = () => import('../components/Foo.vue')
"dynamic-import-webpack"
]
}
複製代碼
二、webpack配置:因爲同構應用同時支持客戶端和服務器端,對於webpack配置要根據平臺不一樣而作不一樣的配置(建議將公共配置抽離處理)。這裏分了3個webpack配置文件:webpack.base.conf.js、webpack.client.conf.js、webpack.server.conf.js;
webpack.base.conf.js
const path=require("path");
const VueLoaderPlugin = require('vue-loader/lib/plugin');
//在webpack4.x版本中mini-css-extract-plugin插件代替extract-text-webpack-plugin插件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports={
mode:"development",
output:{
path:path.resolve(__dirname,"../dist"),
filename:"[name].bundle.js"
},
resolve: {
extensions: ['.js', '.vue']
},
module:{
rules:[
{
test:/\.js$/,
use:'babel-loader'
},{
test:/\.vue$/,
use:'vue-loader'
},{
test:/\.(jpg|jpeg|png|gif|svg)$/,
use:{
loader: 'url-loader',
options: {
limit: 20000
}
}
},{
test:/\.css$/,
use:[
{
loader: MiniCssExtractPlugin.loader,
options:{
publicPath:path.resolve(__dirname,"../dist")
}
},
"css-loader",
"postcss-loader"
]
}
]
},
plugins:[
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename:"[name].client.css",
chunkFilename:"[id].client.css"
})
]
}
複製代碼
webpack.client.conf.js
const path=require("path");
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = require('./webpack.base.config');
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const config=merge(base,{
entry:{
client:path.resolve(__dirname,"../src/entry-client.js")
},
output:{
path:path.resolve(__dirname,"../dist")
},
plugins:[
new VueSSRClientPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,"../index.html"),
filename:"index.client.html"
})
]
});
module.exports=config;
複製代碼
webpack.server.conf.js
const path=require("path");
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = require('./webpack.base.config');
const nodeExternals = require('webpack-node-externals');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
module.exports=merge(base,{
target:"node",
output: {
path:path.resolve(__dirname,"../dist"),
libraryTarget: 'commonjs2'
},
devtool: '#source-map',
externals:[nodeExternals()], //排除node_modules
entry:path.resolve(__dirname,"../src/entry-server.js"),
plugins:[
new VueSSRServerPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,"../index.ssr.html"),
filename:"index.ssr.html",
excludeChunks: ['main','client']
})
]
});
複製代碼
三、node後臺服務搭建:這裏主要利用koa框架搭建node後臺服務層,實現過程採用了koa、koa-router和koa-static依賴包;文件命名爲server.js
const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const appOne = new Koa();
const appTwo = new Koa();
const routerOne = new Router();
const routerTwo = new Router();
// 後端Server
routerOne.get('/index', (ctx, next) => {
ctx.type = 'html';
ctx.status = 200;
//先這樣展現,後續須要服務器端渲染代碼
ctx.body = '<h1>服務器端渲染機制</h1>';
});
appOne.use(serve(path.resolve(__dirname, '../dist')));
appOne.use(routerOne.routes())
.use(routerOne.allowedMethods()); //處理跨域問題
appOne.listen(3001, () => {
console.log('服務器端渲染地址: http://localhost:3001');
});
// 前端Server
routerTwo.get('/index', (ctx, next) => {
let html = fs.readFileSync(path.resolve(__dirname, '../dist/index.client.html'), 'utf-8');
ctx.type = 'html';
ctx.status = 200;
ctx.body = html;
});
appTwo.use(serve(path.resolve(__dirname, '../dist')));
appTwo.use(routerTwo.routes())
.use(routerTwo.allowedMethods()); //處理跨域問題
appTwo.listen(3002, () => {
console.log('瀏覽器端渲染地址: http://localhost:3002');
});
複製代碼
四、使用vue-server-renderer包:上述提到過該包通常在node後臺服務端使用,也就是繼承到server.js代碼中。它主要解決將Vue組件渲染成html字符串;須要注意點,將Vue渲染成html字符串時間不肯定,因此須要使用async/await關鍵字等待渲染完成,才能返回響應。否則這裏還沒渲染完,響應就開始了;
const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const appOne = new Koa();
const appTwo = new Koa();
const routerOne = new Router();
const routerTwo = new Router();
//不一樣點AAAAAAAAA——start
const serverBundle = require(path.resolve(__dirname, '../dist/vue-ssr-server-bundle.json'));
const clientManifest = require(path.resolve(__dirname, '../dist/vue-ssr-client-manifest.json'));
const template = fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
runInNewContext: false,
template: template,
clientManifest: clientManifest
});
//不一樣點AAAAAAAAA——end
// 後端Server
//不一樣點BBBBBBBBBBB——start
routerOne.get('/index', async (ctx, next) => {
//vue-server-renderer標記點——
try {
//因爲渲染時間不肯定,因此須要async/await關鍵字等待渲染完成
let html = await new Promise((resolve, reject) => {
renderer.renderToString((err, html) => {
if (err) {
reject(err);
} else {
resolve(html);
}
});
});
ctx.type = 'html';
ctx.status = 200;
ctx.body = html;
} catch (err) {
console.log(err);
ctx.status = 500;
ctx.body = '服務器內部錯誤';
}
//不一樣點BBBBBBBBBBB——end
});
appOne.use(serve(path.resolve(__dirname, '../dist')));
appOne.use(routerOne.routes())
.use(routerOne.allowedMethods()); //處理跨域問題
appOne.listen(3001, () => {
console.log('服務器端渲染地址: http://localhost:3001');
});
// 前端Server
routerTwo.get('/index', (ctx, next) => {
let html = fs.readFileSync(path.resolve(__dirname, '../dist/index.client.html'), 'utf-8');
ctx.type = 'html';
ctx.status = 200;
ctx.body = html;
});
appTwo.use(serve(path.resolve(__dirname, '../dist')));
appTwo.use(routerTwo.routes())
.use(routerTwo.allowedMethods()); //處理跨域問題
appTwo.listen(3002, () => {
console.log('瀏覽器端渲染地址: http://localhost:3002');
});
複製代碼
五、業務代碼設計:在Vue-cli建立的項目中,main.js文件爲主入口文件。對於同構應用,須要將實例Vue封裝到函數中(app.js)給不一樣平臺設置不一樣主入口文件(entry-client.js和entry-server.js)。記住它們的區別:vue的生命週期不同和初始化Vue實例不同;
普通的vue實例化(main.js)
//main.js文件
import Vue from "vue";
import App from "./App.vue";
import {initRouter} from "./router";
import {initStore} from "./store";
new Vue({
el:"#app",
router,
store,
render:h=>h(App)
});
複製代碼
同構應用Vue實例化(app.js、entry-client.js、entry-server.js)
//app.js文件
import Vue from "vue";
import App from "./App.vue";
import {initRouter} from "./router";
import {initStore} from "./store";
export function initVue(){
const {router}=initRouter();
const {store}=initStore();
const app=new Vue({
router,
store,
render:h=>h(App)
});
return {app,store,router,App};
}
複製代碼
//entry-client.js文件
import {initVue} from "./app";
const {app,store,router}=initVue();
router.onReady(()=>{
app.$mount("#app");
});
複製代碼
//entry-server.js文件
import {initVue} from "./app";
export default context => {
const { app } = initVue();
return new Promise((resolve, reject) => {
const { app, store, router, App } = initVue();
let components = App.components;
//判斷組件是否有asyncData方法,執行asyncData方法
Object.values(components).forEach((component) => {
if (component.asyncData) {
component.asyncData({ store });
}
});
resolve(app);
});
}
複製代碼
實例項目地址:github.com/song199210/…