本文是直接着手SSR部分的並經過實戰講述本身遇到的一些問題和方案,須要你們有必定的React,node和webpack基礎能力。skr,skr。css
Server Slide Rendering
服務端渲染,又簡寫爲SSR
,他通常被用在咱們的SPA(Single-Page Application)
,即單頁應用。html
首先咱們須要知道SSR對於SPA的好處
,優點
是什麼。前端
SEO(Search Engine Optimization)
,SEO
是搜索引擎優化,簡而言之就是針對百度這些搜索引擎,可讓他們搜索到咱們的應用。這裏可能會有誤區,就是我也能夠在index.html上寫SEO
,爲何會不起做用。由於React、Vue的原理是客戶端渲染,經過瀏覽器去加載js、css,有一個時間上的延遲
,而搜索引擎不會管你的延遲
,他就以爲你若是沒加載出來就是沒有的,因此是搜不到的。白屏渲染
,上面講了React的渲染原理,而SSR服務端渲染是經過服務端請求數據,由於服務端內網的請求快,性能好因此會更快的加載全部的文件,最後把下載渲染後的頁面返回給客戶端。客戶端渲染路線:node
服務端渲染路線:react
其主要區別就在於,客戶端從
無到有的
渲染,服務端是先在服務端渲染一部分
,在再客戶端渲染一小部分
。webpack
咱們這裏是用express框架,node作中間層進行服務端渲染。經過將首頁進行同構處理
,讓服務端,經過調用ReactDOMServer.renderToNodeStream
方法把Virtual DOM
轉換成HTML字符串
返回給客戶端,從而達到服務端渲染的目的。nginx
這裏項目起步是已經作完前端和後端,是把已經寫好的React Demo直接拿來用git
既然是首頁SSR,首先咱們要把首頁對應的index.js
抽離出來放入咱們服務端對應的server.js
,那麼index.js
中組件對應的靜態css和js文件
咱們須要打包出來。es6
咱們來運行npm run build
web
咱們能夠看到兩個重要的文件夾
,一個是js文件夾,一個是css文件夾,他就是咱們項目的js和css靜態資源文件
build
文件能在服務端server.js
中訪問到由於是服務端,咱們須要用到express
import express from 'express'
import reducers from '../src/reducer';
import userRouter from './routes/user'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import model from './model'
import path from 'path'
import https from 'http'
import socketIo from 'socket.io'
const Chat = model.getModel('chat')
//新建app
const app = express()
//work with express
const server = https.Server(app)
const io = socketIo(server)
io.on('connection',function(socket){
socket.on('sendmsg',function(data){
let {from,to,msg} = data
let chatid = [from,to].sort().join('_')
Chat.create({chatid,from,to,content:msg},function(e,d){
io.emit('recvmsg',Object.assign({},d._doc))
})
// console.log(data)
// //廣播給全局
// io.emit('recvmsg',data)
})
})
app.use(cookieParser())
app.use(bodyParser.json())
app.use('/user',userRouter)
app.use(function(req,res,next){
if(req.url.startsWith('/user/') || req.url.startsWith('/static/')){
return next()
}
//若是訪問url根路徑是user或者static就返回打包後的主頁面
return res.sendFile(path.resolve('build/index.html'))
})
//映射build文件路徑,項目上要使用
app.use('/',express.static(path.resolve('build')))
server.listen(8088, function () {
console.log('開啓成功')
})
複製代碼
app.use('/',express.static(path.resolve('build')))
和res.sendFile(path.resolve('build/index.html'))
這兩段代碼。import
代碼,因此咱們在開發環境中須要用到babel-cli
裏的babel-node
來編譯。npm --registry https://registry.npm.taobao.org
i babel-cli -S`,你們若是以爲這樣切換源麻煩,能夠下個nrm,360度無死角切換各類源,好用!package.json
的啓動服務器的npm scripts
。"server": "NODE_ENV=test nodemon --exec babel-node server/server.js"
cross-env
跨平臺設置node環境變量的插件。熱重載
。nodemon更輕量
npm run server
,就能看到服務端跑起來了。瀏覽器中
,React.createElement
把React的類進行實例化
,實例化後的組件能夠進行mount
,最後經過React.render
渲染到咱們的客戶端瀏覽器界面。renderToString
或者renderToNodeStream
方法把React實例化的組件,直接渲染生成html標籤。那麼這倆個有什麼區別呢?renderToNodeStream
是React 16最新發布的東西,它支持直接渲染到節點流。渲染到流能夠減小你的內容的第一個字節(TTFB)
的時間,在文檔的下一部分生成以前,將文檔的開頭至結尾發送到瀏覽器。 當內容從服務器流式傳輸時,瀏覽器將開始解析HTML文檔。速度是renderToString的三倍
,因此咱們在這裏使用renderToNodeStream
import express from 'express'
import React from 'react'
import {renderToStaticMarkup,renderToNodeStream} from 'react-dom/server'
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import {StaticRouter} from 'react-router-dom'
import {
createStore,
applyMiddleware,
//組合函數用的
compose
} from 'redux';
import App from '../src/App'
import reducers from '../src/reducer';
import userRouter from './routes/user'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import model from './model'
import path from 'path'
import https from 'http'
import socketIo from 'socket.io'
const Chat = model.getModel('chat')
//新建app
const app = express()
//work with express
const server = https.Server(app)
const io = socketIo(server)
io.on('connection',function(socket){
socket.on('sendmsg',function(data){
let {from,to,msg} = data
let chatid = [from,to].sort().join('_')
Chat.create({chatid,from,to,content:msg},function(e,d){
io.emit('recvmsg',Object.assign({},d._doc))
})
// console.log(data)
// //廣播給全局
// io.emit('recvmsg',data)
})
})
app.use(cookieParser())
app.use(bodyParser.json())
app.use('/user',userRouter)
app.use(function(req,res,next){
if(req.url.startsWith('/user/') || req.url.startsWith('/static/')){
return next()
}
const store = createStore(reducers,compose(
applyMiddleware(thunk)
))
//這個 context 對象包含了渲染的結果
let context = {}
const root = (<Provider store={store}> <StaticRouter location={req.url} context={context} > <App></App> </StaticRouter> </Provider>)
const markupStream = renderToNodeStream(root)
markupStream.pipe(res,{end:false})
markupStream.on('end',()=>{
res.end()
})
})
//映射build文件路徑,項目上要使用
app.use('/',express.static(path.resolve('build')))
server.listen(8088, function () {
console.log('開啓成功')
})
複製代碼
此時將服務端renderToNodeStream後的代碼返回給前端,可是這個時候仍是不行,咱們執行一下npm run server
,能夠看到報錯了。
不認識
咱們的css文件,咱們須要安裝一個包,來讓服務端處理css文件。npm i css-modules-require-hook -S
安裝在生產環境下。crmh.conf.js
鉤子文件進行配置,看下圖。寫入代碼
// css-modules-require-hook
module.exports = {
generateScopedName: '[name]__[local]___[hash:base64:5]',
//下面的代碼在本項目中暫時用不到,可是如下配置在我另外一個項目中有用到,我來說一下他的配置
//擴展名
//extensions: ['.scss','.css'],
//鉤子,這裏主要作一些預處理的scss或者less文件
//preprocessCss: (data, filename) =>
// require('node-sass').renderSync({
// data,
// file: filename
// }).css,
//是否導出css類名,主要用於CSSModule
//camelCase: true,
};
複製代碼
server.js
文件,添加import csshook from 'css-modules-require-hook/preset'
,注意⚠️,必定要把這行代碼放在導入App模塊以前
。import csshook from 'css-modules-require-hook/preset'
//咱們的首頁入口
import App from '../src/App'
複製代碼
此時在運行server.js
,會發現又報了個錯。
npm i asset-require-hook -S
,這個插件用來讓服務端處理圖片,注意⚠️,前提是客戶端代碼,引用圖片都須要require
server.js
寫入代碼//解決圖片問題,客戶端代碼引用圖片都須要require
import assethook from 'asset-require-hook'
assethook({
extensions:['png'],
//圖片大小下於10000的圖片會直接base64編碼
limit: 10000
})
複製代碼
運行以後發現又報錯了,這個很簡單,由於咱們只有image的引用名字,卻沒有地址
靜態js、css文件
引入進去,添加html、head這些標籤。來看完整代碼import 'babel-polyfill'
import express from 'express'
import React from 'react'
import {renderToString,renderToStaticMarkup,renderToNodeStream} from 'react-dom/server'
//引入css文件和js文件
import staticPath from '../build/asset-manifest.json'
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import {StaticRouter} from 'react-router-dom'
import {
createStore,
applyMiddleware,
//組合函數用的
compose
} from 'redux';
//解決服務端渲染的圖片問題 必須放在App以前
import csshook from 'css-modules-require-hook/preset'
//解決圖片問題,須要require
import assethook from 'asset-require-hook'
assethook({
extensions:['png'],
limit: 10000
})
import App from '../src/App'
import reducers from '../src/reducer';
import userRouter from './routes/user'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import model from './model'
import path from 'path'
import https from 'http'
import socketIo from 'socket.io'
const Chat = model.getModel('chat')
//新建app
const app = express()
//work with express
const server = https.Server(app)
const io = socketIo(server)
io.on('connection',function(socket){
socket.on('sendmsg',function(data){
let {from,to,msg} = data
let chatid = [from,to].sort().join('_')
Chat.create({chatid,from,to,content:msg},function(e,d){
io.emit('recvmsg',Object.assign({},d._doc))
})
// console.log(data)
// //廣播給全局
// io.emit('recvmsg',data)
})
})
app.use(cookieParser())
app.use(bodyParser.json())
app.use('/user',userRouter)
app.use(function(req,res,next){
if(req.url.startsWith('/user/') || req.url.startsWith('/static/')){
return next()
}
const store = createStore(reducers,compose(
applyMiddleware(thunk)
))
const obj = {
'/msg':'聊天消息列表',
'/me':'我的中心列表'
}
//這個 context 對象包含了渲染的結果
let context = {}
res.write(`<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="theme-color" content="#000000"> <meta name="description" content="${obj[req.url]}"/> <meta name="keywords" content="SSR"> <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> <link rel="stylesheet" href="/${staticPath['main.css']}"> <title>React App</title> </head> <body> <noscript> You need to enable JavaScript to run this app. </noscript> <div id="root">`)
const root = (<Provider store={store}> <StaticRouter location={req.url} context={context} > <App></App> </StaticRouter> </Provider>)
const markupStream = renderToNodeStream(root)
markupStream.pipe(res,{end:false})
markupStream.on('end',()=>{
res.write(`</div> <script src="/${staticPath['main.js']}"></script> </body> </html>`)
res.end()
})
})
//映射build文件路徑,項目上要使用
app.use('/',express.static(path.resolve('build')))
server.listen(8088, function () {
console.log('開啓成功')
})
複製代碼
<meta name="keywords" content="SSR">
index.js
文件中的渲染機制改爲hydrate
,不用render
,他們之間的區別能夠看這個(傳送門☞render !== hydrate)ReactDOM.hydrate(
(<Provider store={store}> <BrowserRouter> <App></App> </BrowserRouter> </Provider>),
document.getElementById('root')
)
複製代碼
到此爲止咱們開發模式下的SSR搭建完畢,接下來生產模式的坑我來說一下。
咱們上面所講的只是開發模式下的SSR,由於咱們是經過
babel-node
編譯jsx和es6代碼
的,只要一脫離babel-node
就會全錯,因此咱們須要webpack打包服務端代碼
咱們須要建立一個webserver.config.js
,用來打包server的代碼
const path = require('path'),
fs = require('fs'),
webpack = require('webpack'),
autoprefixer = require('autoprefixer'),
HtmlWebpackPlugin = require('html-webpack-plugin'),
ExtractTextPlugin = require('extract-text-webpack-plugin')
cssFilename = 'static/css/[name].[contenthash:8].css';
CleanWebpackPlugin = require('clean-webpack-plugin');
nodeExternals = require('webpack-node-externals');
serverConfig = {
context: path.resolve(__dirname, '..'),
entry: {server: './server/server'},
output: {
libraryTarget: 'commonjs2',
path: path.resolve(__dirname, '../build/server'),
filename: 'static/js/[name].js',
chunkFilename: 'static/js/chunk.[name].js'
},
// target: 'node' 指明構建出的代碼是要運行在node環境裏.
// 不把 Node.js 內置的模塊打包進輸出文件中,例如 fs net 模塊等
target: 'node',
//指定在node環境中是否要這些模塊
node: {
__filename: true,
__dirname: true,
// module:true
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader?cacheDirectory=true',
options: {
presets: ['es2015', 'react-app', 'stage-0'],
plugins: ['add-module-exports',
[
"import",
{
"libraryName": "antd-mobile",
"style": "css"
}
],"transform-decorators-legacy"]
},
},{
test: /\.css$/,
exclude: /node_modules|antd-mobile\.css/,
loader: ExtractTextPlugin.extract(
Object.assign(
{
fallback: {
loader: require.resolve('style-loader'),
options: {
hmr: false,
},
},
use: [
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
minimize: true,
modules: false,
localIdentName:"[name]-[local]-[hash:base64:8]",
// sourceMap: shouldUseSourceMap,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009',
}),
],
},
},
],
},
)
),
},
{
test: /\.css$/,
include: /node_modules|antd-mobile\.css/,
use: ExtractTextPlugin.extract({
fallback: require.resolve('style-loader'),
use: [{
loader: require.resolve('css-loader'),
options: {
modules:false
},
}]
})
}, {
test: /\.(jpg|png|gif|webp)$/,
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
}, {
test: /\.json$/,
loader: 'json-loader',
}]
},
// 不把 node_modules 目錄下的第三方模塊打包進輸出文件中,
externals: [nodeExternals()],
resolve: {extensions: ['*', '.js', '.json', '.scss']},
plugins: [
new CleanWebpackPlugin(['../build/server']),
new webpack.optimize.OccurrenceOrderPlugin(),
//把第三方庫從js文件中分離出來
new webpack.optimize.CommonsChunkPlugin({
//抽離相應chunk的共同node_module
minChunks(module) {
return /node_modules/.test(module.context);
},
//從要抽離的chunk中的子chunk抽離相同的模塊
children: true,
//是否異步抽離公共模塊,參數boolean||string
async: false,
}),
new webpack.optimize.CommonsChunkPlugin({
children:true,
//若參數是string即爲抽離出來後的文件名
async: 'shine',
//最小打包的文件模塊數,即要抽離的公共模塊中的公共數,好比三個chunk只有1個用到就不算公共的
//若爲Infinity,則會把webpack runtime的代碼放入其中(webpack 再也不自動抽離公共模塊)
minChunks:2
}),
//壓縮
new webpack.optimize.UglifyJsPlugin(),
//分離css文件
new ExtractTextPlugin({
filename: cssFilename,
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
}
module.exports = serverConfig
複製代碼
重點⚠️
node_modules
包打包,由於此項目運行在服務端,直接用外面的node_modules
就行。否則打包後會很大。ok,如今來咱們改一下package.json的
npm scripts
,添加一個packServer
,順便改一下build
的scripts
"scripts": {
"clean": "rm -rf build/",
"dev": "node scripts/start.js",
"start": "cross-env NODE_ENV=development npm run server & npm run dev",
"build": "npm run clean && node scripts/build.js && npm run packServer",
"test": "nodemon scripts/test.js --env=jsdom",
"server": "cross-env NODE_ENV=test nodemon --exec babel-node server/server.js",
"gulp": "cross-env NODE_ENV=production gulp",
"packServer": "cross-env NODE_ENV=production webpack --config ./config/webserver.config.js"
},
複製代碼
packServer
指定了生產環境,這在以後會用到。build
是先clean掉build文件夾,在去打包客戶端的代碼
,打包完以後再去打包服務端的代碼
那麼到這裏爲止咱們差很少能夠本身試試了
npm run build
,會生成打包後的build文件夾,裏面包含了咱們的服務端和客戶端代碼
build/server/static/js
目錄下,可直接node文件啓動。這就解決了咱們生產環境下的問題。如今咱們要把咱們的項目部署到服務器上,並用pm2守護進程。
const env = process.env.NODE_ENV || 'development'
//當生產環境時,須要改變mongodb的鏈接端口,根據你服務器的mongodb端口來,我這裏是19999
const BASE_URL = env == 'development'?"mongodb://localhost:27017/chat":"mongodb://127.0.0.1:19999/chat";
複製代碼
socket.io
的連接地址const socket = io('ws://host:port')
,改爲你本身的服務器地址和端口號ssh目錄
下複製id_rsa.pub
裏的公鑰放在碼雲的ssh公鑰
中,可進入設置
,具體看圖ssh公鑰
在碼雲中設置,我這裏是mac,在本身的用戶目錄下,能夠按cmd+shift+.
看隱藏文件(若是你設置過了,這一步就不要了)。(若是服務器已經安裝過了,就不須要了)
ecosystem.json
文件,這個文件是pm2的配置文件,具體的我就不說了,你們若是感興趣能夠去官網看看,(傳送門☞pm2官網){
"apps": [
{
//應用名稱
"name": "chat",
//執行文件的路徑
"script": "./build/server/static/js/server.js",
"env": {
"COMMON_VARIABLE": "true"
},
"env_production": {
"NODE_ENV": "production"
}
}
],
"deploy": {
"production": {
//服務器用戶
"user": "xxx",
//服務器地址
"host": ["xxx"],
//服務器端口
"port": "xxx",
"ref": "origin/master",
//這裏填你的項目git ssh
"repo": "xxx",
//服務器的存放項目路徑
"path": "/www/chat/production",
"ssh_options": "StrictHostKeyChecking=no",
//鉤子
"post-deploy": "npm --registry https://registry.npm.taobao.org install && npm run build && pm2 startOrRestart ecosystem.json --env production",
"env": {
//環境
"NODE_ENV": "production"
}
}
}
}
複製代碼
/www/chat/
文件夾。pm2 deploy ecosystem.json production setup
chat
文件夾的權限不夠,須要進入服務器的www
文件夾,執行sudo chmod 777 chat
。source .bashrc
從新載入一下.bashrc
文件chmod 666 pm2
pm2 list
,看到成功跑起來了重啓
,說明開啓失敗
了,須要pm2 logs
看看日誌服務器地址:8088
,並看到應用跑起來了upstream chat {
server 127.0.0.1:8088;
}
server {
listen 80;
server_name www.webman.vip;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://chat;
proxy_redirect off;
}
# 靜態文件地址
location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js|pdf|txt){
root /www/website/production/current/build;
}
}
複製代碼
在服務器執行sudo nginx -s reload
,重啓nginx。此時咱們就能夠經過咱們的域名地址訪問到咱們的應用了。
這裏可能訪問會404
,這個時候咱們須要看一下咱們服務器的防火牆,sudo vi /etc/iptables.up.rules
,修改mongodb的對外端口,而且重啓防火牆sudo iptables-restore < /etc/iptables.up.rules
-A INPUT -s 127.0.0.1 -p tcp --destination-port 8088 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -d 127.0.0.1 -p tcp --source-port 8088 -m state --state ESTABLISHED -j ACCEPT
複製代碼
最後最後!!!,終於成功了。能夠點擊連接查看一下。 走你!
固然下次若是你想直接更新項目,能夠在項目對應的路徑提交到git
上,而後再使用pm2 deploy ecosystem.json production
便可在服務器上自動部署
。