目前做的商城項(xiàng)目處于優(yōu)化階段展懈,客戶提出SEO優(yōu)化销睁,讓我們給出ssr服務(wù)端渲染的方案;此時(shí)我的內(nèi)心是拒絕的存崖;項(xiàng)目做了幾年冻记,做完了弄服務(wù)端渲染?伊克斯扣斯密来惧?用nuxt嗎冗栗?重新開(kāi)發(fā)?哦不!怎樣增量的嵌入式的修改現(xiàn)有的代碼贞瞒,能做到服務(wù)端渲染偶房,是我這段時(shí)間來(lái)苦心研究的目標(biāo);雖然目前客戶沒(méi)有提這個(gè)需求了军浆,也是為自己做一個(gè)技術(shù)儲(chǔ)備吧棕洋,下面記錄我是如何跟著官網(wǎng)給出的文檔一步步走下去,實(shí)現(xiàn)了一個(gè)ssr的小demo的
vue官方ssr指南:https://ssr.vuejs.org/zh/
思路
所謂服務(wù)端渲染乒融,個(gè)人理解就是數(shù)據(jù)是從后端獲取掰盘,然后前端訪問(wèn)url,后端把獲取的數(shù)據(jù)插入到html中然后直接給前端返回HTML頁(yè)面赞季,這樣有利于搜索引擎優(yōu)化愧捕,能夠被瀏覽器收錄;同時(shí)也加快了頁(yè)面的打開(kāi)時(shí)間申钩,降低白屏?xí)r間次绘,帶來(lái)更好的用戶體驗(yàn)。
第一步:webpack快速搭建vue項(xiàng)目&進(jìn)行改造
本著從零開(kāi)始撒遣,再一次重新認(rèn)識(shí)webpack的心態(tài)邮偎,沒(méi)有用vue-cli搭建項(xiàng)目;畢竟后期如果是真的要改造項(xiàng)目的話义黎,還是要對(duì)webpack有比較深入的認(rèn)識(shí)禾进。
上圖是Vue官方ssr原理的介紹圖,從這張圖我們可以知道廉涕,最后webpack打包后會(huì)生成兩個(gè)bundle文件泻云,這兩個(gè)文件分別作用于不同的渲染。
- Client Bundle:用于瀏覽器渲染狐蜕,這個(gè)就是我們正常項(xiàng)目的普通打包
- Server Bundle:用于服務(wù)端渲染
vue項(xiàng)目做ssr宠纯,不管是用腳手架搭建還是自己純手工搭建,都需要用到vue-server-renderer這個(gè)庫(kù)
下面是手工搭建的項(xiàng)目結(jié)構(gòu)圖:
webpack.config.js: webpack打包的基礎(chǔ)配置
webpack.client.conf.js: 瀏覽器渲染打包配置
webpack.server.conf.js: ssr打包配配置
index.ssr.html: 顧名思義這個(gè)是用來(lái)做ssr的
server.js: 是服務(wù)端的配置
entry-client.js: 客戶端打包入口
entry-server.js: 服務(wù)端打包入口
這里我把index.html拆分成了兩個(gè)层释,這樣可以區(qū)別出ssr征椒,進(jìn)行單獨(dú)的配置。
index.ssr.html
<body>
<div id="app">
<!--vue-ssr-outlet--> ssr
</div>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>
webpack.config.js主要配置:
···
const config = {
// entry: path.join(__dirname, 'src/app.js'),
output: {
filename: '[name].bundle.js',
publicPath: '/', // history模式刷新報(bào)錯(cuò)
path: path.join(__dirname, '/dist')
},
resolve: {
alias: {
vue: 'vue/dist/vue.js'
},
extensions: ['.vue', '.ts', '.tsx', '.js', '.json']
},
plugins: [...plugins],
module: {
rules:[...rules],
}
}
if(isDev){
config.devtool = '#cheap-module-eval-source-map'
config.devServer = {
port: 8005,
host: '0.0.0.0',
overlay: {
errors: true // 將webpack編譯的錯(cuò)誤顯示在網(wǎng)頁(yè)上面
},
open: true // 在啟用webpack-dev-server時(shí)湃累,自動(dòng)打開(kāi)瀏覽器
}
config.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
}
module.exports = config;
webpack.client.conf.js核心配置:
···
module.exports = merge(base, {
entry: {
client: './entry-client.js',
},
plugins: [
new HTMLWebpackPlugin({
template: './index.html',
files: {
js: '/client.bundle.js',
},
filename: 'index.html',
}),
]
})
webpack.server.conf.js核心配置:
···
module.exports = merge(base, {
target: 'node',
entry: {
server: './entry-server.js',
},
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
},
plugins: [
new HtmlWebpackPlugin({
template: './index.ssr.html',
filename: 'index.ssr.html',
files: {
js: '/client.bundle.js',
},
excludeChunks: ['server']
}),
]
})
上面這三個(gè)是主要的webpack的配置勃救,主要是做了入口的分別打包處理,分別用client.js和server.js作為入口治力;這兩個(gè)就是我們正常項(xiàng)目app.js或者main.js中打包的入口文件
entry-server.js和entry-client.js區(qū)別:
entry-client.js是在瀏覽器端執(zhí)行蒙秒,需要掛載dom,啟動(dòng)瀏覽器渲染宵统,所以需要手動(dòng)的調(diào)用$mount()方法晕讲。
entry-server.js是在服務(wù)端調(diào)用覆获,因此需要導(dǎo)入一個(gè)函數(shù),返回一個(gè)vue的實(shí)例瓢省。
export default function createApp() {
const app = new Vue({
render: h => h(App)
});
return app;
};
關(guān)于 webpack.server.conf.js弄息,有兩個(gè)注意點(diǎn):
libraryTarget: 'commonjs2' → 因?yàn)榉?wù)器是 Node,所以必須按照 commonjs 規(guī)范打包才能被服務(wù)器調(diào)用勤婚。
target: 'node' → 指定 Node 環(huán)境摹量,避免非 Node 環(huán)境特定 API 報(bào)錯(cuò),如 document 等馒胆。
編寫(xiě)服務(wù)端渲染主要邏輯
- Vue SSR 依賴于包 vue-server-render缨称,它的調(diào)用支持兩種入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 組件為入口祝迂,后者以打包后的 JS 文件為入口睦尽,這里采用第二種。
server.js:
server.use(express.static('dist'));
// server.js 服務(wù)端渲染主體邏輯
// dist/server.js 就是以 entry-server.js 為入口打包出來(lái)的 JS
const bundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf-8')
});
server.get('*', (req, res) => {
console.log(req.url)
const context = { url: req.url, pageTitle: 'default-title' }
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
if (err) {
console.log(err);
res.status(500).end('服務(wù)器內(nèi)部錯(cuò)誤');
return;
}
res.status = 200
res.type = 'text/html; charset=utf-8'
res.body = html
res.end(html);
resolve(html);
})
});
})
server.listen(8002, () => {
console.log('后端渲染服務(wù)器啟動(dòng)型雳, 端口為:8002');
})
- 兩個(gè)打包入口(entry)当凡,重構(gòu)app, store, router, 為每個(gè)對(duì)象增加工廠方法createXXX
每個(gè)用戶通過(guò)瀏覽器訪問(wèn)Vue頁(yè)面時(shí),都是一個(gè)全新的上下文纠俭,但在服務(wù)端沿量,應(yīng)用啟動(dòng)后就一直運(yùn)行著,處理每個(gè)用戶請(qǐng)求的都是在同一個(gè)應(yīng)用上下文中柑晒。為了不串?dāng)?shù)據(jù)欧瘪,需要為每次SSR請(qǐng)求眷射,創(chuàng)建全新的app, store, router匙赞。
index.js核心代碼
// createApp工廠方法
export default function (ssrContext) {
const store = createStore(); // 創(chuàng)建全新store實(shí)例
const router = createRouter();
// 創(chuàng)建Vue應(yīng)用
const app = new Vue({
store,
router,
ssrContext,
render: (h) => h(App)
})
return { app, store, router }
}
router.js創(chuàng)建router工廠函數(shù)
export function createRouter () {
return new VueRouter({
mode: 'history',
fallback: false,
routes: [
{
path: '/index',
name: 'index',
component: Index
},
]
})
}
store.js 創(chuàng)建store工廠函數(shù)
export default function createStore() {
return new Vuex.Store({
state: {
detail: '',
},
getters: {
getDetail(state) {
return state.detail;
}
},
mutations: {
detail(state, arg) {
state.detail = arg;
console.log(state)
}
},
actions: {
getDetail({commit}, payload) {
var p = new Promise((resolve) => {
setTimeout(() => {
resolve({data: '我是數(shù)據(jù)'})
})
})
// action必須返回promise
return p.then(data => {
console.log(data);
commit('detail', data.data);
})
}
}
})
entry-client.js:
// 創(chuàng)建所需要的app實(shí)例
const { app, router, store } = createApp();
// 當(dāng)路由加載完后掛載dom,渲染
router.onReady(() => {
// 將Vue實(shí)例掛載到dom中妖碉,完成瀏覽器端應(yīng)用啟動(dòng)
app.$mount('#app')
})
entry-server.js:
export default function (context) {
return new Promise((resolve, reject) => {
const { app, router, store} = createApp(context);
// 設(shè)置路由
router.push(context.url)
router.onReady(() => {
context.state = store.state;
resolve(app);
})
}
做到這里基本的ssr的工作已經(jīng)完成了涌庭,使用npm run build打包,然后node server.js啟動(dòng)服務(wù)欧宜,在瀏覽器訪問(wèn)localhost:8002坐榆,就能看到效果啦~完美!訪問(wèn)首localhost:8002/index在network中可以看到返回html中有提前插入的數(shù)據(jù)冗茸;而普通的是沒(méi)有任何內(nèi)容的只有<div id="app"></div>
-
ssr:
image.png -
普通:
image.png
總結(jié)
這篇主要講了項(xiàng)目的搭建的一些webpack配置席镀,服務(wù)端代碼,多入口配置以及router夏漱,store豪诲,app的改造;在這個(gè)過(guò)程中主要碰到幾個(gè)問(wèn)題:
- 詳情頁(yè)面:localhost:8002/detail/1,跳轉(zhuǎn)正常挂绰,刷新頁(yè)面報(bào)錯(cuò)屎篱。查看network,發(fā)現(xiàn)刷新后會(huì)去加載localhost:8002/detail/1/client.boundle.js;而我們的這個(gè)js文件是與index.ssr.html同級(jí)的, 改publicPath:'./'為publicPath:'/'交播。
-
啟動(dòng)服務(wù)之后重虑,打開(kāi)url,頁(yè)面一直在請(qǐng)求
服務(wù)端沒(méi)有返回html秦士;在服務(wù)端缺厉,express框架在攔截到所有路由之后,查閱了別人的處理返回一個(gè)promise伍宦,然后再resolve(html)就可以了; 我也照做了芽死,但是瀏覽器會(huì)出現(xiàn)一直轉(zhuǎn)圈圈,解決辦法在resolve(html)之前加上res.end(html)就好了次洼。
image.png
這篇初步試探ssr到這里就告一段落了关贵,下一篇記錄下,如何將異步的數(shù)據(jù)插入到頁(yè)面中~