祝願各位過年回家的單身攻城獅相親成功,已脫單的找機會~~~~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…