上一節(jié)用很簡單的代碼粗略的模擬了一下服務(wù)端渲染,這節(jié)來吧webpack加入進來。
首先安裝包,
webpack(核心打包應(yīng)用), webpack-cli(解析命令行參數(shù)), webpack-dev-server(在開發(fā)環(huán)境下提供一個運行環(huán)境,支持熱更新),html-webpack-plugin(將打包后的結(jié)果插入到html中)babel-loader(babel和webpack的橋梁), @babel/core(babel的核心模塊),@babel/preset-env(把高級語法轉(zhuǎn)化為es5(這是一個插件的集合))vue-style-loader(是style-loader的升級版,style-loader不支持服務(wù)端渲染), css-loadervue-loader(vue-loader和webpack的橋梁), vue-template-compiler(將template轉(zhuǎn)化為render)
yarn add webpack webpack-cli ... vue-template-compiler --save-dev
新建webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module.exports = {
entry: path.resolve(__dirname, "src/main.js"),
mode: "development",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "./dist"),
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
{
test: /\.vue$/,
use: "vue-loader",
},
{
test: /\.css$/,
use: ["vue-style-loader", "css-loader"],
},
],
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "./public/index.html"),
}),
],
};
package.json文件中scripts字段添加命令dev和build。
運行 yarn dev

以上就和我們平時做項目一樣,只是這個配置要比vue-cli的簡單很多。當前目錄結(jié)構(gòu):

接下來進入正題,服務(wù)端渲染(工程化),先來看張圖

現(xiàn)在的目錄結(jié)構(gòu):

現(xiàn)在的package.json
"scripts": {
"client:dev": "webpack-dev-server --config build/webpack.client.js",
"client:build": "webpack --config build/webpack.client.js",
"server:build": "webpack --config build/webpack.server.js"
},
webpack.client.js和webpack.server.js均“繼承了” webpack.base.js,這里要用到包webpack-merge;
因為現(xiàn)在打包有不同的入口,因此entry字段就不要放在webpack.base里了,而要放到各自的配置文件中,模板也要改變,webpack各文件如下:
//--------webpack.base.js
const path = require("path");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module.exports = {
mode: "development",
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "./dist"),
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
{
test: /\.vue$/,
use: "vue-loader",
},
{
test: /\.css$/,
use: ["vue-style-loader", "css-loader"],
},
],
},
plugins: [new VueLoaderPlugin()],
};
//---------webpack.base.js結(jié)束----
//-------------webpack.client.js開始------------
const base = require("./webpack.base.js");
const { merge } = require("webpack-merge");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = merge(base, { //合并配置
entry: {
client: path.resolve(__dirname, "../src/client-entry.js"),
},
plugins: [
new HtmlWebpackPlugin({
filename: "client.html",
template: path.resolve(__dirname, "../public/client.html"),
}),
],
});
之前做純客戶端渲染時,main.js一般是這樣的:
import Vue from "vue";
import App from "./App.vue";
let app = new Vue({
el:"#app",
render: (h) => h(App),
})
但是上節(jié)說到過,服務(wù)端沒有dom的概念,而我們想把main.js做成一個公用入口配置文件,因此,去掉el選項,幸好,vue給我們提供了$mount方法來手動掛載,可以隨后在client-entry.js中手動掛載。
還有一點,在服務(wù)端渲染時,不能直接let app = new Vue(...,這樣,多個客戶端訪問時,接受到的都是同一個vue實例,這顯然是不合適的。因此考慮生成vue實例的那塊做成一個工廠函數(shù),每次都導(dǎo)出一個新的實例。
改造后:
import Vue from "vue";
import App from "./App.vue";
export default function() {
let app = new Vue({
render: (h) => h(App),
});
return { app };
}
先來完成入口js---client-entry
import createApp from "./main";
let { app } = createApp();
app.$mount("#app"); //記得要在public/client.html中增加id為app的元素哦
運行yarn client:dev

可以看到,文件,樣式,js都正常運行
然后,后臺入口server-entry.js, 后臺webpack配置
//-------server-entry.js
import createApp from "./main.js";
export default () => { //這里可以接收由render.renderToString傳遞的參數(shù)
let { app } = createApp();
return app;
};
//-------server-entry.js結(jié)束------
//-------webpack.server.js開始------
const base = require("./webpack.base.js");
const { merge } = require("webpack-merge");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueServerRenderer = require("vue-server-renderer/server-plugin"); // 給webpack.client.js中添加插件 const VueServerRenderer = require("vue-server-renderer/client-plugin");
module.exports = merge(base, {
entry: {
server: path.resolve(__dirname, "../src/server-entry.js"),
},
target: "node", //意思是輸出的文件是給node使用的,因此當碰到fs,path等node的模塊,不會打包
output: {
libraryTarget: "commonjs2", // 打包出的js是commonjs規(guī)范的寫法,即module.exports = ...
},
plugins: [
new VueServerRenderer(), // 用來與客戶端關(guān)聯(lián) (webpack.client.js中也要加,用來與服務(wù)端關(guān)聯(lián))
new HtmlWebpackPlugin({
filename: "server.html",
template: path.resolve(__dirname, "../public/server.html"),
excludeChunks: ["server"], // 服務(wù)端的代碼,不是以script標簽引入的,而是經(jīng)過vue-server-renderer解析后,插入到<!--vue-ssr-outlet-->處,因此,去掉引入的操作
minify: false,
}),
],
});
// 服務(wù)端打包出來的結(jié)果, 要給koa用,通過koa渲染成一個字符串插入到server.html中
// 需要將客戶端打包的js插入到server.html中(因為服務(wù)端渲染出來的只是字符串,而js操作打包在了客戶端的js中)
然后,啟動一個服務(wù),server.js
const Koa = require("koa");
const Router = require("koa-router");
// const Vue = require("vue");
const fs = require("fs");
const path = require("path");
const static = require("koa-static"); //
const VueServerRenderer = require("vue-server-renderer");
const router = new Router();
let template = fs.readFileSync(
path.resolve(__dirname, "dist/server.html"),
"utf8"
);
// 以下兩個文件分別是各自的webpack配置中的vue-server-renderer生成的(yarn client:build/server:build)
let ServerBundle = require("./dist/vue-ssr-server-bundle.json"); // 配置了服務(wù)端入口
let clientManifest = require("./dist/vue-ssr-client-manifest.json"); // 配置了客戶端入口
let render = VueServerRenderer.createBundleRenderer(ServerBundle, {
template, //模板
clientManifest, // 相應(yīng)的客戶端映射
});
router.get("/", async (ctx) => {
ctx.body = await new Promise((resolve, reject) => {
render.renderToString(
/*這里還可以接收參數(shù), 將會傳遞到server-entry中的函數(shù)中*/ (err, res) => {
if (err) {
console.log(err);
reject(err);
} else {
resolve(res);
}
}
);
});
});
let app = new Koa();
app.use(static(path.resolve(__dirname, "dist"))); //告訴靜態(tài)頁以哪個目錄來顯示
app.use(router.routes());
app.listen(3000);
到這里,還有一個問題,客戶端有app.$mount('#app')操作,讓vue組件可以掛在到ID為app的dom上,但是server.html中只有<!--vue-ssr-outlet-->, 因此前端代碼中的js操作還是不能掛載到相應(yīng)的dom上。解決辦法是,在App.vue中根元素加id:
// ----------App.vue
<template>
<div id="app">
<Bar></Bar>
<Foo></Foo>
</div>
</template>
...
現(xiàn)在,我們來改造一下上邊的代碼,引入vue-router
新建router.js。
//-----------router.js開始-------------
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
export default () => {
//同理,導(dǎo)出一個工廠函數(shù),每次都生成新的vueRouter實例
let router = new VueRouter({
mode: "history",
routes: [
{
path: "/",
component: () => import("./components/foo.vue"),
},
{
path: "/bar",
component: () => import("./components/bar.vue"),
},
],
});
return router;
};
//----------router.js結(jié)束-----------
//-----------main.js中增加router------
import Vue from "vue";
import App from "./App.vue";
import createRouter from "./router";
export default function() {
let router = createRouter();
let app = new Vue({
router, // 客戶端的router直接渲染
render: (h) => h(App),
});
return { app, router };
}
//---------------main.js結(jié)束-------------
//------------App.vue改為路由形式----
<template>
<div id="app">
<router-link to="/">foo</router-link>
<router-link to="/bar">bar</router-link>
<router-view></router-view>
</div>
</template>
<script>
import Bar from "./components/bar.vue";
import Foo from "./components/foo.vue";
export default {
name: "App",
components: {
Bar,
Foo
}
};
</script>
重新打包客戶端,服務(wù)端代碼, 打開localhost:3000

點擊bar

看樣子已經(jīng)完成了。但是刷新時...

第一次正常是因為,走的都是前端路由,而第二次刷新出故障,是因為頁面是ssr(由服務(wù)端返回的),而我們代碼中只有對
/路徑的處理,因此在后臺代碼中要多加一些路由映射
router.get("(.*)", async (ctx) => { // 此處注意,有大坑,不能使用通配符,之前的版本可以,要以(.*)代替。
try {
ctx.body = await new Promise((resolve, reject) => {
render.renderToString({ url: ctx.path }/*這里便將用戶訪問的路徑傳入到server-entry中了*/, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
} catch (e) {
console.log(e);
}
});
//----------------server-entry.js
import createApp from "./app.js";
// 這里服務(wù)端渲染要求打包后的結(jié)果需要返回一個函數(shù)
// 服務(wù)端稍后會調(diào)用函數(shù) 傳遞一些參數(shù)到這個函數(shù)中
export default (context) => { // 這里的context就是剛才上邊傳遞來的對象
let { app, router } = createApp();
router.push(context.url); // 渲染時 先讓路由跳轉(zhuǎn)到當前客戶請求的路徑
// router路由對象
return app; // 已經(jīng)渲染完成了 把當前路徑對應(yīng)的內(nèi)容渲染好了
};
大功告成?。。?/p>