正常的页面渲染是,开始接收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。