祝愿各位过年回家的单身攻城狮相亲成功,已脱单的找机会~~~~css
服务端渲染听起来高大上,其实也就那么回事,若是网站不是用于商业用途,也不须要被网站收录,那就仍是乖乖用正常普通的方式写写就完事了,除非本身想装逼一下,那能够玩一下。如下就是个人装逼时间了~~🙃html
项目地址: github.com/chenjiaobin…前端
js/css下载-请求数据-页面渲染
这几个步骤,服务端渲染(SSR)和客户端(CSR)的区别就在于以上几个步骤的顺序,后面有图说明(多说一句,JS/CSS是并行下载的,可是CSS影响JS的执行,即CSS没下载完成和解析完成以前JS执行是被阻塞的,CSS前面的JS不会。CSS不会影响DOM的解析,可是影响DOM的渲染,由于DOM的渲染须要JS DOM和CSS DOM结合成Renderdom后才被渲染。而JS文件的下载会阻塞DOM和CSS的的解析和渲染,可是不会阻塞前面的HTML和CSS的解析)<div id="root"></div>
,而获取不到咱们页面具体的内容,可是服务端渲染返回完整的可视页面,即不包括交互,交互须要等待后续JS下载完成进行绑定可参考https://www.jdon.com/50088node
上文中描述的客户端渲染和服务端渲染,实际上对应了两种Web构建模式:先后分离模式和直出模式react
不管是客户端渲染,服务端渲染,它们都包含三个主体过程:webpack
客户端渲染:a -> b ->c (a,b,c都在客户端进行)git
服务端渲染:b -> c ->a (b,c在服务端进行,最后的a在客户端进行)github
服务端渲染改变了a,b,c三个过程的执行顺序和执行方web
由于浏览器对于一些新的JS用法和react的语法糖没法识别,所以咱们须要安装一下webpack包来对源代码进行打包处理,以保证代码能在浏览器运行,具体包的做用就不细讲,主要贴了几个比较重要的包,细看请前往express-react-ssrexpress
// webpack主要的打包依赖
cnpm i webpack webpack-cli webpack-merge html-webpack-plugin autoprefixer -D
// 安装babel主要用于编辑ES6和jsx语法,转换代码的做用
cnpm i @babel/cli @babel/core @babel/preset-env @babel/preset-react babel-loader -D
// 主要用于打包css
cnpm i css-loader style-loader postcss-loader -D
复制代码
执行npm init
建立一个带项目信息的package.json,并新建build、client和server文件夹,build主要存放webpack打包配置,client存放客户端文件,即前端react页面文件,server则存放node服务端代码,主要用于服务端渲染
// 用于合并webpack的配置
const merge = require('webpack-merge')
// 用户导出html文件
const HTMLplugin = require('html-webpack-plugin')
const { resolvePath } = require('./webpack-util')
// webpack公共配置文件,主要用于服务端打包和客户端打包的公用配置
const baseConfig = require('./webpack-base')
// 分离CSS为单独的问题
var ExtractTextPlugin = require("extract-text-webpack-plugin")
module.exports = merge(baseConfig, {
// 用于调试
devtool: 'inline-source-map',
mode: 'development',
entry: {
app: resolvePath('../client/client.js')
},
output: {
filename: 'js/[name].[hash].js',
path: resolvePath('../dist'),
// 服务端的publicPath要跟这里的一致,做用是在最后打包出来的静态资源路径都是在/public下,这里主要的做用是由于服务端渲染时,打包后的js文件也返回了html文件,因此须要经过设置一个静态资源文件的路径来区分
publicPath: '/public'
},
devServer: {
port: 8060,
contentBase: '../client', //src文件夹里面的内容改变就会从新打包
// 路由使用history,所以有个问题就是,一步路由没有缓存在页面中,第一次进入页面会找不到,
// 所以在开发环境能够配置historyApiFallback恢复正常
historyApiFallback: true,
hot: true,
inline: true
},
module: {
rules: [
{
test: /\.css$/,
exclude: [/node_modules/],
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{ loader: 'css-loader',
// https://stackoverflow.com/questions/57899750/error-while-configuring-css-modules-with-webpack
// Syntax of css-loader options has changed in version 3.0.0. localIdentName was moved under modules //option. 意思大概是css-loader3.0.0版本的localIndentName属性被移除了
// 所以不能写成 options: { modules: true, importLoaders: 1, localIdentName: '[name]___[hash:base64:5]' }
// 只能写成如下方式
options: { modules: { localIdentName: '[name]___[hash:base64:5]' }, importLoaders: 1 }
},
'postcss-loader'
]
})
}
]
},
plugins: [
new HTMLplugin({
filename: 'index.html',
template: resolvePath("../template.html")
}),
// 注意:若是打包的时候报错,那从新npm install –save-dev extract-text-webpack-plugin@next安装一下,由于webpack版本较高,因此老版本的extract-text-webpack-plugin有问题
new ExtractTextPlugin('./css/[name]-[hash:8].css')
]
})
复制代码
基础webpack-config-base.js就去看项目就行了哈😁没什么特别
注:这里主要的坑就是ExtractTextPlugin
的使用,即在配置css-loader的时候若是跟不使用它的时候同样,那可能会出现问题,缘由是版本css-loader 3.0.0版本的时候移除了localIdentName
(做用:自定义样式打包规则)属性
错误配置
options: { modules: true, importLoaders: 1, localIdentName: '[name]___[hash:base64:5]'
复制代码
正确配置
options: { modules: { localIdentName: '[name]___[hash:base64:5]' }, importLoaders: 1 }
复制代码
// 获取文件路径
const path = require('path')
exports.resolvePath = (filePath) => path.join(__dirname, filePath)
复制代码
{
"presets": ["@babel/preset-react"]
}
复制代码
// app.js
export default class App extends Component {
render () {
return (
<div>
<p>服务端渲染测试</p>
</div>
)
}
}
// client.js
export class Home extends React.Component {
render () {
return (
<App/>
</Provider>
)
}
}
ReactDom.render(<Home/>, document.getElementById('app'))
复制代码
webpack --config build/webpack-client-config.js
,打包正常你会在根目录生成了一个dist目录,不正常的话本身再调试调试把,或者拉个人项目去看下,传送门ReactDom.render()会将后端返回的dom节点全部子节点所有清除,再从新生成子节点。而ReactDom.hydrate()则会复用dom节点的子节点,将其与virtualDom关联
可见,第一种方式明显是作了重复工,影响效率,所以,react16版本也放弃了用render,也可能将会在react17版本中不能用ReactDOM.render()去混合服务端渲染出来的标签
import Express from 'express'
import path from 'path'
import { renderToString } from 'react-dom/server'
import fs from 'fs'
import React from 'react'
import { StaticRouter } from 'react-router-dom'
// const App = require('../dist/server').default
import App from '../client/app'
import { Provider } from 'react-redux'
import createStore from '../client/redux/store'
const server = Express()
// 静态资源路径
server.use('/public', Express.static(path.join(__dirname, "../dist")))
// 这个函数主要用于匹配模板文件的{{}}标签的内容,替换成咱们后端给出的数据
function templating(props) {
const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf-8');
return template.replace(/{{([\s\S]*?)}}/g, (_, key) => props[ key.trim() ]);
}
server.use('/', (req, res) => {
const store = createStore({
list: {
list: ['关羽', '张飞', '赵云']
},
home: {
title: '我是小菜鸡,请赐教'
}
})
// 核心代码
const html = renderToString(
<Provider store={ store }>
//若是咱们页面上使用到了路由那就须要这个来包含
<StaticRouter location={req.url}>
<App/>
</StaticRouter>
</Provider>
)
res.send(templating({html,store: JSON.stringify(store.getState())}))
})
server.listen('8888', () => {
console.log('server is started, port is 8888')
})
复制代码
关键点:
首先咱们从浏览器输入url,无论你的url是匹配的哪一个路由,后端通通都给你index.html,而后加载js匹配对应的路由组件,渲染对应的路由。
那咱们的ssr路由是怎么样的模式呢?
首先咱们从浏览器输入url,后端匹配对应的路由获取到对应的路由组件,获取对应的数据填充路由组件,将组件转成html返回给浏览器,浏览器直接渲染。当这个时候若是你在页面中点击跳转,咱们依旧仍是不会发送请求,由js匹配对应的路由渲染
const merge = require('webpack-merge')
const { resolvePath } = require('./webpack-util')
const baseConfig = require('./webpack-base')
// 打包忽略重复文件,挺重要的,以前打包没加报了Critical dependency: the request of a dependency is an // //expression的警告,不将node_modules里面的包打进去
const nodeExternals = require('webpack-node-externals')
// 打包server node的配置
module.exports = merge(baseConfig, {
mode: 'production',
// 表示是node环境,必须加
target: 'node',
node: {
// 使用__filename变量获取当前模块文件的带有完整绝对路径的文件名
__filename: true,
// 使用__dirname变量得到当前文件所在目录的完整目录名
__dirname: true
},
context: resolvePath('..'),
entry: {
app: resolvePath('../server/index.js')
},
output: {
filename: '[name].js',
path: resolvePath('../dist/server'),
// 必须跟客户端的路径同样
publicPath: '/public'
},
module: {
rules: [
{
test: /\.css?$/,
use: ['isomorphic-style-loader', {
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]___[hash:base64:5]'
},
}
}]
}
]
},
externals: [
nodeExternals()
],
})
复制代码
注:isomorphic-style-loader主要是用于解决css在服务端打包时候的问题,由于app.js文件在服务端引进去,app.js里面有css的文件应用,那么这个时候打包就会出现document is not defined
的错误,所以咱们就须要解决css文件在服务端的问题,此时咱们只须要提取css样式名字到标签上就好了,不须要额外打包出css文件,那么isomorphic-style-loader这个包就派上用场了,配置如上。localIdentName自定义配置要和客户端的一致 4. 这个时候基本就已经完成服务端打包了,执行webpack --config build/webpack-server-pro.js
,会在dist下生成一个server文件夹,且文件夹里面有个app.js文件,这个文件就是node的启动文件,咱们经过终端进入文件夹,而后执行node app.js
这样就能够启动咱们的node服务了,前提是咱们按前面的客户端打包步骤打包好前端代码,那么咱们就能够经过在浏览器访问localhost:8888
访问到后端渲染的页面了
✔🤣敬上,项目地址 github.com/chenjiaobin…