webpack ssr

236 阅读2分钟

正常的页面渲染是,开始接收html文件,但是还没有渲染时,先创建一个白屏,html边加载边渲染,加载成功后请求数据,然后将请求的数据渲染到页面上。这是浏览器端渲染的过程

ssr是服务端渲染,它的过程是当用户请求某一个路由时,服务端进行拦截,根据路由加载对应的html文件,该html文件=html+css+js,初始数据都已经加载好的html。

所以服务端的特点是:

  • 返回的只是一个html文件,包括css、js、以及初始化的数据。
  • 服务端渲染可以在请求的时候使用服务器ip地址进行拉取数据,更快。
  • 所有的模板都存在服务器端。

一、webpack配置

我将所有要加载的页面都放在src/components文件夹下。这里根据components文件夹下的目录打包生成对应的js文件,它们会共用同一个html模板。

const path = require("path");const merge = require("webpack-merge").merge;const HtmlWebpackPlugin = require("html-webpack-plugin");const glob = require("glob");const setMPA = () => {  const entry = {};  const htmlWebpackPlugins = [];  const entryFile = glob.sync(    path.join(__dirname, "../../src/components/*/index.js")  );  entryFile.forEach((key) => {    const arr = key.match(/src\/components\/(.*)\/index\.js/g);    const name = arr[0].split("/components/")[1].split("/")[0];    entry[name] = `./${arr[0]}`;  });  return { entry };};const { entry = {} } = setMPA();class BaseWebpackConfig {  constructor() {    this.__config = this.defaultConfig;  }  set config(data) {    this._config = merge({}, this.defaultConfig, data);    return this._config;  }  get config() {    return this.__config;  }  get defaultConfig() {    return {      mode: "development",      target: "node",      devtool: false,      entry: entry,      output: {        filename: "[name].js",        path: path.join(__dirname, "../../dist"),        libraryTarget: "umd",      },      module: {        rules: [          {            test: /\.js$/,            use: "babel-loader",            exclude: path.resolve(__dirname, "node_modules"),          },          {            test: /\.css/,            use: [              {                loader: "css-loader",                options: { esModule: false },              },              {                loader: "px2rem-loader",                options: { remUnit: 75, remPrecision: 8 },              },            ],          },        ],      },      plugins: [        new HtmlWebpackPlugin({          template: "./src/index.html",          inject: false,        })      ],    };  }}module.exports = BaseWebpackConfig;

二、服务器端配置

我在src的同级创建了一个server文件夹,server文件夹下有一个index.js文件,这个文件就是服务端渲染的代码。

const express = require("express");const { renderToString } = require("react-dom/server");const fs = require("fs");const path = require("path");const htmlTemplate = fs.readFileSync(  path.join(__dirname, "../dist/index.html"),  "utf-8");const server = (port) => {  const app = express();  app.use(express.static("dist"));  app.get("*", (req, res) => {    // 路由建议是例如/search,不带二级目录的。    const pageName = req.path.split("/")[1];    if (pageName === "favicon.ico") {      fs.readFileSync(path.join(__dirname, `../favicon.ico`));      res.status(200).send();    } else {      try {        const template = require(`../dist/${pageName}.js`).default;        console.log(template);        const html = renderMarkUp(renderToString(template));        res.status(200).send(html);      } catch (err) {        console.log("error:", err);        if (err.message.indexOf("no such file or directory") > -1) {          console.log("file is no found!");        }      }    }    // 请求数据时,路由前面加一个api,表示是在请求数据  });  app.listen(port, () => {    console.log("Server is run on port:", port);  });};const renderMarkUp = (str) => {  return htmlTemplate.replace("<!-- HTML_PLACEHOLDER -->", str);};server(process.env.PORT || 8081);

这里的template是加载出来的html文件内容,要使用renderToSTring方法转成html字符串,然后替换html模板的标志。

html模板:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="utf-8" />    <title>test</title>    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />    <meta name="description" content="" />    <meta      name="viewport"      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"    />  </head>  <body>    <!-- HTML_PLACEHOLDER -->  </body></html>

三、启动命令行配置

在package.json中增加启动命令:

'build': 'webpack --config ./config/webpack/webpack.build.config.js'
'server': 'npm run build & node ./server/index.js'

这里我的webpack都是放在config/webpack文件夹下的。

使用yarn server就可以启动了,在浏览器使用localhost:8081就可以访问了。

四、存在的问题

  • 因为是服务器端渲染,所以不能使用style-loader,里面的document会报错,可以使用isomorphic-style-loader,但是这个loader是代替浏览器端的style-loader的,所以它会将样式以style标签的格式插入到html模版的head中,样式过多时不太理想,而且使用也比较复杂。

  • 这个例子中,整个项目只打包出来一个html模版,在请求时,服务端将标志替换成要加载的内容,并且也没有想到更好的插入样式的办法。。。

  • 没有对请求的路由进行提出分离。

  • 没有增加热更新以及数据请求。

五、注意

  • 在使用require(`../dist/${pageName}.js`).default加载文件内容时,一定要设置webpack的devtool属性,否则获取到的一直是一个空对象。
  • 利用webpack和express自己搭建一个ssr项目,还是困难重重,非常耗费时间和精力,建议还是使用现有的框架搭建,比如egg.js。