Vue-SSR
使用
-
在SSR-3目錄中 npm install 安裝依賴
-
打包服務端 npm run server:build
-
打包客戶端 npm run client:build
-
在dist目錄index.ssr.html中引入客戶端代碼
<script src="./client.bundle.js"></script> -
執(zhí)行服務端腳本
node server.js
webpack5.0嘗鮮 SSR+Vue+Koa+vue-router+vuex【排坑記錄】
一些包
webpack相關
- webpack
- webpack-cli 命令行解析工具 4.0之前是一起的 4.0之后拆開了 需要安裝
- webpack-dev-server
- html-webpack-plugin
- webpack-merge
es6轉es5
- babel-loader es6=>es5 webpack中接入babel
- @babel/core babel-loader依賴
- @babel/preset-env 加入新的語法特性 比如2015年加入的新特性 Env包括所有的新特性
- @babel/plugin-transform-runtime 減少冗余代碼 默認的polifill屬性已經(jīng)被廢除掉了
- @babel/runtime @babel/plugin-transform-runtime依賴
解析css的包
- vue-style-loader 支持服務端渲染 和style-loader功能一樣
- css-loader
vue相關
- vue-loader 處理.vue文件
- vue-template-loader 處理模板編譯
npx webpack === node_modules/bin/webpack 打包
webpack5.0 bug記錄
問題一
webpack5.0與webpack-dev-server不兼容
npm script 需把 "run":"webpack-dev-server --open"改成 "run":"webpack serve"
問題2
vue-style-loader與css-loader 在服務端打包時style樣式不生效問題解決方案
{ test: /.css$/, use: [ "vue-style-loader", { loader: "css-loader", options: { esModule: false //默認為true 需要設置為false } } ] }
問題三
build目錄中 webpack.server.js const ServerRenderPlugin = require("vue-server-renderer/server-plugin") 報錯
解決方案 修改vue-server-renderer包中的代碼
https://github.com/vuejs/vue/issues/11718#issuecomment-717786088
問題四
使用 const ClientRenderPlugin = require("vue-server-renderer/client-plugin"); const ServerRenderPlugin = require("vue-server-renderer/server-plugin"); 自動引入js文件時服務端會報錯 展示沒有解決辦法 需要手動引入js文件
npm run client:build -- --watch 文件變動執(zhí)行打包編譯
為什么服務端vue vuex vueRouter 都需要調(diào)用一個函數(shù),從函數(shù)中獲取實例?
因為Node.js 服務器是一個長期運行的進程 當代碼進入進程時,它將進行一次取值并留存在內(nèi)存中。也就是會把vue實例保存到內(nèi)存中 假如說不用函數(shù)返回新的實例,那么每個人訪問同一個頁面時都會共用同一個實例,那么就會造成數(shù)據(jù)的污染。
https://ssr.vuejs.org/zh/guide/structure.html
路由集成
首屏只渲染一個路由,但是其他路由的邏輯混淆再js文件中。如果需要分開 可以使用路由懶加載。 每個路由需要返回一個函數(shù) 每個服務器都要返回一個路由實例
此時的問題 此時在服務器中執(zhí)行構建, 訪問localhose:3000/ 應該渲染出對應的組件Foo 但是沒有渲染出來 并且控制臺會報錯 Failed to execute 'appendChild' on 'Node': This node type does not support this method.at Object.appendChild
解造成此問題的原因: 服務器訪問根目錄時 router.get("/") 渲染出來的時字符串 它并不知道哪個頁面對應哪個路由 所以只會渲染app.vue
解決方案:
server.js中 render.renderToString({url:'/' })
server.enter.js中
export default context => {
// 服務端需要調(diào)用當前這個文件 去產(chǎn)生一個vue實例
const { app, router } = createApp();
// context.url 服務端要把對應的路由和此url相匹配
router.push(context.url); // 渲染當前頁面對應的路由
return app; //拿這個實例去服務端查找渲染結果
};
切換到其他路由localhose:3000/bar然后刷新瀏覽器會報404錯誤 Not Fount
重點:如果此時是點擊切換 那就只是客戶端切換 并不是服務端切換。并不會造成服務端重新渲染。 也可以理解為服務端不認識router-link,解析不出對應的組件, localhose:3000/bar 只有刷新頁面時才會走服務端渲染。 但是此時會報404 因為器服務器根本沒有此路徑。 解決方法: 所以在服務器中還得再配其他路徑,使用中間件,使得每個路徑渲染對應的頁面
中間件 當找不到路由時會走此邏輯 如果匹配不到路由就會走此邏輯(當路由不是跟路徑時,要跳轉到對應的路徑,渲染對應的頁面) 如果服務器沒有此路徑,會渲染當前的app.vue(首頁)文件,在渲染時又會重新指向/bar路徑對應的頁面 然后 server.entry.js 中router.push(context.url)找對應的組件
app.use(async ctx => {
ctx.body = await new Promise((resolve, reject) => {
// 必須寫成回調(diào)函數(shù)的形式否則css樣式不生效
render.renderToString(
{
title: "服務",
url: ctx.url //比如當前請求的/bar 那就把/bar傳到server.entry.js中的content
},
(err, data) => {
if (err) reject(err);
resolve(data);
}
);
});
});
也就是先渲染app.vue---->找其他路由對應的組件
這也是history模式需要后端支持的原理
Vuex集成
為什么vuex需要此判斷?
if (typeof window !== "undefined" && window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
因為客戶端和服務端各自生成一個vuex實例 而他們兩個需要共用一個狀態(tài),因此需要服務端狀態(tài)改變之后傳給客戶端
服務端與客戶端各自的用處?
服務端用于渲染html 有利于seo 客戶端用于處理js邏輯比如 點擊事件 client.bundle.js
服務端調(diào)用在組件中調(diào)用asyncDate() vuex必須返回promise才能生效,并且只有等resolve()執(zhí)行完成之后才會返回結果
如果是服務端調(diào)用必須返回 promise 否則不生效 因為在service.entry.js的邏輯是Promise.all()
自己有里面的所有數(shù)據(jù)都獲取完之后才會渲染app.vue 所以會等待三秒才渲染數(shù)據(jù)
只寫setTimeout 不生效 因為setTimeout是異步的,在vuex實例渲染完成后才被調(diào)用,此時頁面已經(jīng)被當成字符串渲染到瀏覽器上了。
哪些請求放在ajax哪些放在服務端請求?
被爬蟲爬取,比如新聞列表的數(shù)據(jù) 由服務端返回
流程大致總結
主入口文件
- 服務端入口文件
- 客戶端入口文件
webpack
- base.js
- 服務端配置
- 客戶端配置
- merge-webpack
vue-server-renderer
-
createRender() createBundleRender()
-
renderTostring() renderToStream()
主流程
-
webpack.server.js -> 入口文件 server.entry.js(函數(shù)生成每個實例)-> npm run server:build->服務端文件打包到dist目錄
-
webpack.client.js->入口文件 server.entry.js ->npm run client:build->客戶端打包到dist目錄
-
Koa-> vue-server-renderer->render.createBundleRender()引入打包好的服務端文件和模板->render.renderToStream()配置模板屬性+生成html字符串->koa中間件監(jiān)聽dist目錄->在模板中手動引入客戶端打包好的bundle.js文件->掛載#app激活事件->返回給客戶端。
css->vue-style-loader
-配置meta標簽
- 在option中設置title
- 客戶端:document.title=this.$options.title
- 服務端:this.$ssrContext=title
配置路由
引入路由->每個路由都已函數(shù)的形式返回->koa中間件捕獲到history路徑->把url傳給render.renderToString->server.entry.js接受到路徑 router.push(context.url)渲染當前頁面對應的路由
router.onReady() router.getMatchedComponents()
配置vuex
引入vuex->每個路由都已函數(shù)的形式返回->在匹配到的每個組件中調(diào)用asyncData方法動態(tài)傳入store-> 改變store的狀態(tài)->更新(此時會等待promise執(zhí)行完成再渲染頁面)-> 再拿到狀態(tài)->context.state = store.state->把vuex的狀態(tài)掛載到上下文中,會將狀態(tài)掛載到window上-> 當客戶端執(zhí)行vuex時把狀態(tài)替換掉store.replaceState(window.INITIAL_STATE);
