美文网首页程序员
vue服务端渲染之webpack配置

vue服务端渲染之webpack配置

作者: 成熟稳重的李先生 | 来源:发表于2020-07-11 22:42 被阅读0次

上一节用很简单的代码粗略的模拟了一下服务端渲染,这节来吧webpack加入进来。

首先安装包,

  • webpack(核心打包应用), webpack-cli(解析命令行参数), webpack-dev-server(在开发环境下提供一个运行环境,支持热更新),html-webpack-plugin(将打包后的结果插入到html中)
  • babel-loader(babel和webpack的桥梁), @babel/core(babel的核心模块),@babel/preset-env(把高级语法转化为es5(这是一个插件的集合))
  • vue-style-loader(是style-loader的升级版,style-loader不支持服务端渲染), css-loader
  • vue-loader(vue-loader和webpack的桥梁), vue-template-compiler(将template转化为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的简单很多。当前目录结构:
image.png

接下来进入正题,服务端渲染(工程化),先来看张图


image.png

现在的目录结构:


image.png
现在的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
因为现在打包有不同的入口,因此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结束----

//-------------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),
})

但是上节说到过,服务端没有dom的概念,而我们想把main.js做成一个公用入口配置文件,因此,去掉el选项,幸好,vue给我们提供了$mount方法来手动挂载,可以随后在client-entry.js中手动挂载。
还有一点,在服务端渲染时,不能直接 let app = new Vue(...,这样,多个客户端访问时,接受到的都是同一个vue实例,这显然是不合适的。因此考虑生成vue实例的那块做成一个工厂函数,每次都导出一个新的实例。

改造后:

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传递的参数 
  let { app } = createApp();
  return app;
};
//-------server-entry.js结束------

//-------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规范的写法,即module.exports = ...
  },
  plugins: [
    new VueServerRenderer(), // 用来与客户端关联 (webpack.client.js中也要加,用来与服务端关联)
    new HtmlWebpackPlugin({
      filename: "server.html",
      template: path.resolve(__dirname, "../public/server.html"),
      excludeChunks: ["server"], // 服务端的代码,不是以script标签引入的,而是经过vue-server-renderer解析后,插入到<!--vue-ssr-outlet-->处,因此,去掉引入的操作
      minify: false,
    }),
  ],
});

// 服务端打包出来的结果, 要给koa用,通过koa渲染成一个字符串插入到server.html中
// 需要将客户端打包的js插入到server.html中(因为服务端渲染出来的只是字符串,而js操作打包在了客户端的js中)

然后,启动一个服务,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");  //  配置了服务端入口
let clientManifest = require("./dist/vue-ssr-client-manifest.json");  // 配置了客户端入口

let render = VueServerRenderer.createBundleRenderer(ServerBundle, {
  template, //模板
  clientManifest,  // 相应的客户端映射
});

router.get("/", async (ctx) => {
  ctx.body = await new Promise((resolve, reject) => {
    render.renderToString(
      /*这里还可以接收参数, 将会传递到server-entry中的函数中*/ (err, res) => {
        if (err) {
          console.log(err);
          reject(err);
        } else {
          resolve(res);
        }
      }
    );
  });
});

let app = new Koa();

app.use(static(path.resolve(__dirname, "dist"))); //告诉静态页以哪个目录来显示
app.use(router.routes());

app.listen(3000);

到这里,还有一个问题,客户端有app.$mount('#app')操作,让vue组件可以挂在到ID为app的dom上,但是server.html中只有<!--vue-ssr-outlet-->, 因此前端代码中的js操作还是不能挂载到相应的dom上。解决办法是,在App.vue中根元素加id:

// ----------App.vue
<template>
  <div id="app">
    <Bar></Bar>
    <Foo></Foo>
  </div>
</template>
...

现在,我们来改造一下上边的代码,引入vue-router
新建router.js

//-----------router.js开始-------------
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);

export default () => {
  //同理,导出一个工厂函数,每次都生成新的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结束-----------
//-----------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结束-------------

//------------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>

重新打包客户端,服务端代码, 打开localhost:3000

image.png
点击bar
router.gif
看样子已经完成了。但是刷新时...
reload.gif
第一次正常是因为,走的都是前端路由,而第二次刷新出故障,是因为页面是ssr(由服务端返回的),而我们代码中只有对/路径的处理,因此在后台代码中要多加一些路由映射
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";

// 这里服务端渲染要求打包后的结果需要返回一个函数
// 服务端稍后会调用函数 传递一些参数到这个函数中
export default (context) => {  // 这里的context就是刚才上边传递来的对象
  let { app, router } = createApp();
  router.push(context.url); // 渲染时 先让路由跳转到当前客户请求的路径
  // router路由对象
  return app; // 已经渲染完成了 把当前路径对应的内容渲染好了
};

大功告成!!!

相关文章

网友评论

    本文标题:vue服务端渲染之webpack配置

    本文链接:https://www.haomeiwen.com/subject/pbgbcktx.html