vue服務(wù)端渲染之webpack配置

上一節(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-loader
  • vue-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字段添加命令devbuild。
運行 yarn dev

image.png

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

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


image.png

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


image.png

現(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.jswebpack.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

image.png

可以看到,文件,樣式,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

image.png

點擊bar
router.gif

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

第一次正常是因為,走的都是前端路由,而第二次刷新出故障,是因為頁面是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>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容