日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

大家好,我是Echa哥,這一次,讓我們來以React為例,把服務端渲染(Server Side Render,簡稱“SSR”)學個明明白白。

這里附上這個項目的github地址: https://github.com/sanyuan0704/react-ssr

歡迎大家點star,提issue,一起進步!

part1:實現一個基礎的React組件SSR

這一部分來簡要實現一個React組件的SSR。

一. SSR vs CSR

什么是服務端渲染?

廢話不多說,直接起一個express服務器。

var express = require('express')
var App = express()

app.get('/', (req, res) => {
 res.send(
 `
   <html>
     <head>
       <title>hello</title>
     </head>
     <body>
       <h1>hello</h1>
       <p>world</p>
     </body>
   </html>
 `
 )
})

app.listen(3001, () => {
 console.log('listen:3001')
})
復制代碼

啟動之后打開localhost:3001可以看到頁面顯示了hello world。而且打開網頁源代碼:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

也能夠完成顯示。

 

這就是服務端渲染。其實非常好理解,就是服務器返回一堆html字符串,然后讓瀏覽器顯示。

與服務端渲染相對的是客戶端渲染(Client Side Render)。那什么是客戶端渲染? 現在創建一個新的React項目,用腳手架生成項目,然后run起來。 這里你可以看到React腳手架自動生成的首頁。

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

然而打開網頁源代碼。

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

body中除了兼容處理的noscript標簽之外,只有一個id為root的標簽。那首頁的內容是從哪來的呢?很明顯,是下面的script中拉取的JS代碼控制的。

 

因此,CSR和SSR最大的區別在于前者的頁面渲染是JS負責進行的,而后者是服務器端直接返回HTML讓瀏覽器直接渲染。

為什么要使用服務端渲染呢?

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

傳統CSR的弊端:

 

  1. 由于頁面顯示過程要進行JS文件拉取和React代碼執行,首屏加載時間會比較慢。
  2. 對于seo(Search Engine Optimazition,即搜索引擎優化),完全無能為力,因為搜索引擎爬蟲只認識html結構的內容,而不能識別JS代碼內容。

SSR的出現,就是為了解決這些傳統CSR的弊端。

二、實現React組件的服務端渲染

剛剛起的express服務返回的只是一個普通的html字符串,但我們討論的是如何進行React的服務端渲染,那么怎么做呢? 首先寫一個簡單的React組件:

// containers/Home.js
import React from 'react';
const Home = () => {
  return (
    <div>
      <div>This is sanyuan</div>
    </div>
  )
}
export default Home
復制代碼

現在的任務就是將它轉換為html代碼返回給瀏覽器。 眾所周知,JSX中的標簽其實是基于虛擬DOM的,最終要通過一定的方法將其轉換為真實DOM。虛擬DOM也就是JS對象,可以看出整個服務端的渲染流程就是通過虛擬DOM的編譯來完成的,因此虛擬DOM巨大的表達力也可見一斑了。

而react-dom這個庫中剛好實現了編譯虛擬DOM的方法。做法如下:

// server/index.js
import express from 'express';
import { renderToString } from 'react-dom/server';
import Home from './containers/Home';

const app = express();
const content = renderToString(<Home />);
app.get('/', function (req, res) {
   res.send(
   `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id="root">${content}</div>
      </body>
    </html>
   `
   );
})
app.listen(3001, () => {
  console.log('listen:3001')
})

復制代碼

啟動express服務,再瀏覽器上打開對應端口,頁面顯示出"this is sanyuan"。 到此,就初步實現了一個React組件是服務端渲染。 當然,這只是一個非常簡陋的SSR,事實上對于復雜的項目而言是無能為力的,在之后會一步步完善,打造出一個功能完整的React的SSR框架。

part2: 初識同構

一.引入同構

其實前面的SSR是不完整的,平時在開發的過程中難免會有一些事件綁定,比如加一個button:

// containers/Home.js
import React from 'react';
const Home = () => {
  return (
    <div>
      <div>This is sanyuan</div>
      <button onClick={() => {alert('666')}}>click</button>
    </div>
  )
}
export default Home
復制代碼

再試一下,你會驚奇的發現,事件綁定無效!那這是為什么呢?原因很簡單,react-dom/server下的renderToString并沒有做事件相關的處理,因此返回給瀏覽器的內容不會有事件綁定。

那怎么解決這個問題呢?

這就需要進行同構了。所謂同構,通俗的講,就是一套React代碼在服務器上運行一遍,到達瀏覽器又運行一遍。服務端渲染完成頁面結構,瀏覽器端渲染完成事件綁定。

那如何進行瀏覽器端的事件綁定呢?

唯一的方式就是讓瀏覽器去拉取JS文件執行,讓JS代碼來控制。于是服務端返回的代碼變成了這樣:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

有沒有發現和之前的區別?區別就是多了一個script標簽。而它拉取的JS代碼就是來完成同構的。

 

那么這個index.js我們如何生產出來呢?

在這里,要用到react-dom。具體做法其實就很簡單了:

//client/index. js
import React from 'react';
import ReactDom from 'react-dom';
import Home from '../containers/Home';

ReactDom.hydrate(<Home />, document.getElementById('root'))
復制代碼

然后用webpack將其編譯打包成index.js:

//webpack.client.js
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const clientConfig = {
  mode: 'development',
  entry: './src/client/index.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'public')
  },
}

module.exports = merge(config, clientConfig);

//webpack.base.js
module.exports = {
  module: {
    rules: [{
      test: /.js$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      options: {
        presets: ['@babel/preset-react',  ['@babel/preset-env', {
          targets: {
            browsers: ['last 2 versions']
          }
        }]]
      }
    }]
  }
}

//package.json的script部分
  "scripts": {
    "dev": "npm-run-all --parallel dev:**",
    "dev:start": "nodemon --watch build --exec node "./build/bundle.js"",
    "dev:build:server": "webpack --config webpack.server.js --watch",
    "dev:build:client": "webpack --config webpack.client.js --watch"
  },
復制代碼

在這里需要開啟express的靜態文件服務:

const app = express();
app.use(express.static('public'));
復制代碼

現在前端的script就能拿到控制瀏覽器的JS代碼啦。

綁定事件完成!

現在來初步總結一下同構代碼執行的流程:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

二.同構中的路由問題

現在寫一個路由的配置文件:

// Routes.js
import React from 'react';
import {Route} from 'react-router-dom'
import Home from './containers/Home';
import Login from './containers/Login'

export default (
  <div>
    <Route path='/' exact component={Home}></Route>
    <Route path='/login' exact component={Login}></Route>
  </div>
)
復制代碼

在客戶端的控制代碼,也就是上面寫過的client/index.js中,要做相應的更改:

import React from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter } from 'react-router-dom'
import Routes from '../Routes'

const App = () => {
  return (
    <BrowserRouter>
      {Routes}
    </BrowserRouter>
  )
}
ReactDom.hydrate(<App />, document.getElementById('root'))
復制代碼

這時候控制臺會報錯,

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

因為在Routes.js中,每個Route組件外面包裹著一層div,但服務端返回的代碼中并沒有這個div,所以報錯。如何去解決這個問題?需要將服務端的路由邏輯執行一遍。

 

// server/index.js
import express from 'express';
import {render} from './utils';

const app = express();
app.use(express.static('public'));
//注意這里要換成*來匹配
app.get('*', function (req, res) {
   res.send(render(req));
});
 
app.listen(3001, () => {
  console.log('listen:3001')
});
復制代碼
// server/utils.js
import Routes from '../Routes'
import { renderToString } from 'react-dom/server';
//重要是要用到StaticRouter
import { StaticRouter } from 'react-router-dom'; 
import React from 'react'

export const render = (req) => {
  //構建服務端的路由
  const content = renderToString(
    <StaticRouter location={req.path} >
      {Routes}
    </StaticRouter>
  );
  return `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id="root">${content}</div>
        <script src="/index.js"></script>
      </body>
    </html>
  `
}
復制代碼

現在路由的跳轉就沒有任何問題啦。 注意,這里僅僅是一級路由的跳轉,多級路由的渲染在之后的系列中會用react-router-config中renderRoutes來處理。

part3: 同構項目中引入Redux

這一節主要是講述Redux如何被引入到同構項目中以及其中需要注意的問題。

重新回顧一下redux的運作流程:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

再回顧一下同構的概念,即在React代碼客戶端和服務器端各自運行一遍。

 

一、創建全局store

現在開始創建store。 在項目根目錄的store文件夾(總的store)下:

import {createStore, applyMiddleware, combineReducers} from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';
//合并項目組件中store的reducer
const reducer = combineReducers({
  home: homeReducer
})
//創建store,并引入中間件thunk進行異步操作的管理
const store = createStore(reducer, applyMiddleware(thunk));

//導出創建的store
export default store
復制代碼

二、組件內action和reducer的構建

Home文件夾下的工程文件結構如下:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

在Home的store目錄下的各個文件代碼示例:

 

//constants.js
export const CHANGE_LIST = 'HOME/CHANGE_LIST';
復制代碼
//actions.js
import axIOS from 'axios';
import { CHANGE_LIST } from "./constants";

//普通action
const changeList = list => ({
  type: CHANGE_LIST,
  list
});
//異步操作的action(采用thunk中間件)
export const getHomeList = () => {
  return (dispatch) => {
    return axios.get('xxx')
      .then((res) => {
        const list = res.data.data;
        console.log(list)
        dispatch(changeList(list))
      });
  };
}
復制代碼
//reducer.js
import { CHANGE_LIST } from "./constants";

const defaultState = {
  name: 'sanyuan',
  list: []
}

export default (state = defaultState, action) => {
  switch(action.type) {
    default:
      return state;
  }
}
復制代碼
//index.js
import  reducer  from "./reducer";
//這么做是為了導出reducer讓全局的store來進行合并
//那么在全局的store下的index.js中只需引入Home/store而不需要Home/store/reducer.js
//因為腳手架會自動識別文件夾下的index文件
export {reducer}
復制代碼

三、組件連接全局store

下面是Home組件的編寫示例。

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions'

class Home extends Component {
  render() {
    const { list } = this.props
    return list.map(item => <div key={item.id}>{item.title}</div>)
  }
}

const mapStateToProps = state => ({
  list: state.home.newsList,
})

const mapDispatchToProps = dispatch => ({
  getHomeList() {
    dispatch(getHomeList());
  }
})
//連接store
export default connect(mapStateToProps, mapDispatchToProps)(Home);
復制代碼

對于store的連接操作,在同構項目中分兩個部分,一個是與客戶端store的連接,另一部分是與服務端store的連接。都是通過react-redux中的Provider來傳遞store的。

客戶端:

//src/client/index.js
import React from 'react';
import ReactDom from 'react-dom';
import {BrowserRouter, Route} from 'react-router-dom';
import { Provider } from 'react-redux';
import store from '../store'
import routes from '../routes.js'

const App = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        {routes}
      </BrowserRouter>
    </Provider>
  )
}

ReactDom.hydrate(<App />, document.getElementById('root'))
復制代碼

服務端:

//src/server/index.js的內容保持不變
//下面是src/server/utils.js
import Routes from '../Routes'
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom'; 
import { Provider } from 'react-redux';
import React from 'react'

export const render = (req) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path} >
        {Routes}
      </StaticRouter>
    </Provider>
  );
  return `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id="root">${content}</div>
        <script src="/index.js"></script>
      </body>
    </html>
  `
}
復制代碼

四、潛在的坑

其實上面這樣的store創建方式是存在問題的,什么原因呢?

上面的store是一個單例,當這個單例導出去后,所有的用戶用的是同一份store,這是不應該的。那么這么解這個問題呢?

在全局的store/index.js下修改如下:

//導出部分修改
export default  () => {
  return createStore(reducer, applyMiddleware(thunk))
}
復制代碼

這樣在客戶端和服務端的js文件引入時其實引入了一個函數,把這個函數執行就會拿到一個新的store,這樣就能保證每個用戶訪問時都是用的一份新的store。

part4: 異步數據的服務端渲染方案(數據注水與脫水)

一、問題引入

在平常客戶端的React開發中,我們一般在組件的componentDidMount生命周期函數進行異步數據的獲取。但是,在服務端渲染中卻出現了問題。

現在我在componentDidMount鉤子函數中進行Ajax請求:

import { getHomeList } from './store/actions'
  //......
  componentDidMount() {
    this.props.getList();
  }
  //......
  const mapDispatchToProps = dispatch => ({
    getList() {
      dispatch(getHomeList());
    }
})
復制代碼
//actions.js
import { CHANGE_LIST } from "./constants";
import axios from 'axios'

const changeList = list => ({
  type: CHANGE_LIST,
  list
})

export const getHomeList = () => {
  return dispatch => {
    //另外起的本地的后端服務
    return axiosInstance.get('localhost:4000/api/news.json')
      .then((res) => {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
//reducer.js
import { CHANGE_LIST } from "./constants";

const defaultState = {
  name: 'sanyuan',
  list: []
}

export default (state = defaultState, action) => {
  switch(action.type) {
    case CHANGE_LIST:
      const newState = {
        ...state,
        list: action.list
      }
      return newState
    default:
      return state;
  }
}
復制代碼

好,現在啟動服務。

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

現在頁面能夠正常渲染,但是打開網頁源代碼。

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

源代碼里面并沒有這些列表數據啊!那這是為什么呢?

 

讓我們來分析一下客戶端和服務端的運行流程,當瀏覽器發送請求時,服務器接受到請求,這時候服務器和客戶端的store都是空的,緊接著客戶端執行componentDidMount生命周期中的函數,獲取到數據并渲染到頁面,然而服務器端始終不會執行componentDidMount,因此不會拿到數據,這也導致服務器端的store始終是空的。換而言之,關于異步數據的操作始終只是客戶端渲染。

現在的工作就是讓服務端將獲得數據的操作執行一遍,以達到真正的服務端渲染的效果。

二、改造路由

在完成這個方案之前需要改造一下原有的路由,也就是routes.js

import Home from './containers/Home';
import Login from './containers/Login';

export default [
{
  path: "/",
  component: Home,
  exact: true,
  loadData: Home.loadData,//服務端獲取異步數據的函數
  key: 'home'
},
{
  path: '/login',
  component: Login,
  exact: true,
  key: 'login'
}
}];
復制代碼

此時客戶端和服務端中編寫的JSX代碼也發生了相應變化

//客戶端
//以下的routes變量均指routes.js導出的數組
<Provider store={store}>
  <BrowserRouter>
      <div>
        {
            routers.map(route => {
                <Route {...route} />
            })
        }
      </div>
  </BrowserRouter>
</Provider>
復制代碼
//服務端
<Provider store={store}>
  <StaticRouter>
      <div>
        {
            routers.map(route => {
                <Route {...route} />
            })
        }
      </div>
  </StaticRouter>
</Provider>
復制代碼

其中配置了一個loadData參數,這個參數代表了服務端獲取數據的函數。每次渲染一個組件獲取異步數據時,都會調用相應組件的這個函數。因此,在編寫這個函數具體的代碼之前,我們有必要想清楚如何來針對不同的路由來匹配不同的loadData函數。

在server/utils.js中加入以下邏輯

  import { matchRoutes } from 'react-router-config';
  //調用matchRoutes用來匹配當前路由(支持多級路由)
  const matchedRoutes = matchRoutes(routes, req.path)
  //promise對象數組
  const promises = [];
  matchedRoutes.forEach(item => {
    //如果這個路由對應的組件有loadData方法
    if (item.route.loadData) {
      //那么就執行一次,并將store傳進去
      //注意loadData函數調用后需要返回Promise對象
      promises.push(item.route.loadData(store))
    }
  })
  Promise.all(promises).then(() => {
      //此時該有的數據都已經到store里面去了
      //執行渲染的過程(res.send操作)
  }
  )
復制代碼

現在就可以安心的寫我們的loadData函數,其實前面的鋪墊工作做好后,這個函數是相當容易的。

import { getHomeList } from './store/actions'

Home.loadData = (store) => {
    return store.dispatch(getHomeList())
}
復制代碼
//actions.js
export const getHomeList = () => {
  return dispatch => {
    return axios.get('xxxx')
      .then((res) => {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
復制代碼

根據這個思路,服務端渲染中異步數據的獲取功能就完成啦。

三、數據的注水和脫水

其實目前做了這里還是存在一些細節問題的。比如當我將生命周期鉤子里面的異步請求函數注釋,現在頁面中不會有任何的數據,但是打開網頁源代碼,卻發現:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

數據已經掛載到了服務端返回的HTML代碼中。那這就說明服務端和客戶端的store不同步的問題。

 

其實也很好理解。當服務端拿到store并獲取數據后,客戶端的js代碼又執行一遍,在客戶端代碼執行的時候又創建了一個空的store,兩個store的數據不能同步。

那如何才能讓這兩個store的數據同步變化呢?

首先,在服務端獲取獲取之后,在返回的html代碼中加入這樣一個script標簽:

<script>
  window.context = {
    state: ${JSON.stringify(store.getState())}
  }
</script>
復制代碼

這叫做數據的“注水”操作,即把服務端的store數據注入到window全局環境中。 接下來是“脫水”處理,換句話說也就是把window上綁定的數據給到客戶端的store,可以在客戶端store產生的源頭進行,即在全局的store/index.js中進行。

//store/index.js
import {createStore, applyMiddleware, combineReducers} from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';

const reducer = combineReducers({
  home: homeReducer
})
//服務端的store創建函數
export const getStore = () => {
  return createStore(reducer, applyMiddleware(thunk));
}
//客戶端的store創建函數
export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
  return createStore(reducer, defaultState, applyMiddleware(thunk));
}
復制代碼

至此,數據的脫水和注水操作完成。但是還是有一些瑕疵,其實當服務端獲取數據之后,客戶端并不需要再發送Ajax請求了,而客戶端的React代碼仍然存在這樣的浪費性能的代碼。怎么辦呢?

還是在Home組件中,做如下的修改:

componentDidMount() {
  //判斷當前的數據是否已經從服務端獲取
  //要知道,如果是首次渲染的時候就渲染了這個組件,則不會重復發請求
  //若首次渲染頁面的時候未將這個組件渲染出來,則一定要執行異步請求的代碼
  //這兩種情況對于同一組件是都是有可能發生的
  if (!this.props.list.length) {
    this.props.getHomeList()
  }
}
復制代碼

一路做下來,異步數據的服務端渲染還是比較復雜的,但是難度并不是很大,需要耐心地理清思路。

至此一個比較完整的SSR框架就搭建的差不多了,但是還有一些內容需要補充,之后會繼續更新的。加油吧!

part5: node作中間層及請求代碼優化

一、為什么要引入node中間層?

其實任何技術都是與它的應用場景息息相關的。這里我們反復談的SSR,其實不到萬不得已我們是用不著它的,SSR所解決的最大的痛點在于SEO,但它同時帶來了更昂貴的成本。不僅因為服務端渲染需要更加復雜的處理邏輯,還因為同構的過程需要服務端和客戶端都執行一遍代碼,這雖然對于客戶端并沒有什么大礙,但對于服務端卻是巨大的壓力,因為數量龐大的訪問量,對于每一次訪問都要另外在服務器端執行一遍代碼進行計算和編譯,大大地消耗了服務器端的性能,成本隨之增加。如果訪問量足夠大的時候,以前不用SSR的時候一臺服務器能夠承受的壓力現在或許要增加到10臺才能抗住。痛點在于SEO,但如果實際上對SEO要求并不高的時候,那使用SSR就大可不必了。

那同樣地,為什么要引入node作為中間層呢?它是處在哪兩者的中間?又是解決了什么場景下的問題?

在不用中間層的前后端分離開發模式下,前端一般直接請求后端的接口。但真實場景下,后端所給的數據格式并不是前端想要的,但處于性能原因或者其他的因素接口格式不能更改,這時候需要在前端做一些額外的數據處理操作。前端來操作數據本身無可厚非,但是當數據量變得龐大起來,那么在客戶端就是產生巨大的性能損耗,甚至影響到用戶體驗。在這個時候,node中間層的概念便應運而生。

它最終解決的前后端協作的問題。

一般的中間層工作流是這樣的:前端每次發送請求都是去請求node層的接口,然后node對于相應的前端請求做轉發,用node去請求真正的后端接口獲取數據,獲取后再由node層做對應的數據計算等處理操作,然后返回給前端。這就相當于讓node層替前端接管了對數據的操作。

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

 

二、SSR框架中引入中間層

在之前搭建的SSR框架中,服務端和客戶端請求利用的是同一套請求后端接口的代碼,但這是不科學的。

對客戶端而言,最好通過node中間層。而對于這個SSR項目而言,node開啟的服務器本來就是一個中間層的角色,因而對于服務器端執行數據請求而言,就可以直接請求真正的后端接口啦。

//actions.js
//參數server表示當前請求是否發生在node服務端
const getUrl = (server) => {
    return server ? 'xxxx(后端接口地址)' : '/api/sanyuan.json(node接口)';
}
//這個server參數是Home組件里面傳過來的,
//在componentDidMount中調用這個action時傳入false,
//在loadData函數中調用時傳入true, 這里就不貼組件代碼了
export const getHomeList = (server) => {
  return dispatch => {
    return axios.get(getUrl(server))
      .then((res) => {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
復制代碼

在server/index.js應拿到前端的請求做轉發,這里是直接用proxy形式來做,也可以用node單獨向后端發送一次HTTP請求。

//增加如下代碼
import proxy from 'express-http-proxy';
//相當于攔截到了前端請求地址中的/api部分,然后換成另一個地址
app.use('/api', proxy('http://xxxxxx(服務端地址)', {
  proxyReqPathResolver: function(req) {
    return '/api'+req.url;
  }
}));
復制代碼

三、請求代碼優化

其實請求的代碼還是有優化的余地的,仔細想想,上面的server參數其實是不用傳遞的。

現在我們利用axios的instance和thunk里面的withExtraArgument來做一些封裝。

//新建server/request.js
import axios from 'axios'

const instance = axios.create({
  baseURL: 'http://xxxxxx(服務端地址)'
})

export default instance


//新建client/request.js
import axios from 'axios'

const instance = axios.create({
  //即當前路徑的node服務
  baseURL: '/'
})

export default instance
復制代碼

然后對全局下store的代碼做一個微調:

import {createStore, applyMiddleware, combineReducers} from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';
import clientAxios from '../client/request';
import serverAxios from '../server/request';

const reducer = combineReducers({
  home: homeReducer
})

export const getStore = () => {
  //讓thunk中間件帶上serverAxios
  return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));
}
export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
   //讓thunk中間件帶上clientAxios
  return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}

復制代碼

現在Home組件中請求數據的action無需傳參,actions.js中的請求代碼如下:

export const getHomeList = () => {
  //返回函數中的默認第三個參數是withExtraArgument傳進來的axios實例
  return (dispatch, getState, axiosInstance) => {
    return axiosInstance.get('/api/sanyuan.json')
      .then((res) => {
        const list = res.data.data;
        console.log(res)
        dispatch(changeList(list))
      })
  }
}
復制代碼

至此,代碼優化就做的差不多了,這種代碼封裝的技巧其實可以用在其他的項目當中,其實還是比較優雅的。

part6: 多級路由渲染(renderRoutes)

現在將routes.js的內容改變如下:

import Home from './containers/Home';
import Login from './containers/Login';
import App from './App'

//這里出現了多級路由
export default [{
  path: '/',
  component: App,
  routes: [
    {
      path: "/",
      component: Home,
      exact: true,
      loadData: Home.loadData,
      key: 'home',
    },
    {
      path: '/login',
      component: Login,
      exact: true,
      key: 'login',
    }
  ]
}]

復制代碼

現在的需求是讓頁面公用一個Header組件,App組件編寫如下:

import React from 'react';
import Header from './components/Header';

const  App = (props) => {
  console.log(props.route)
  return (
    <div>
      <Header></Header>
    </div>
  )
}

export default App;
復制代碼

對于多級路由的渲染,需要服務端和客戶端各執行一次。 因此編寫的JSX代碼都應有所實現:

//routes是指routes.js中返回的數組
//服務端:
<Provider store={store}>
  <StaticRouter location={req.path} >
    <div>
      {renderRoutes(routes)}
    </div>
  </StaticRouter>
</Provider>

//客戶端:
<Provider store={getClientStore()}>
  <BrowserRouter>
  <div>
    {renderRoutes(routes)}
  </div>
  </BrowserRouter>
</Provider>
復制代碼

這里都用到了renderRoutes方法,其實它的工作非常簡單,就是根據url渲染一層路由的組件(這里渲染的是App組件),然后將下一層的路由通過props傳給目前的App組件,依次循環。

那么,在App組件就能通過props.route.routes拿到下一層路由進行渲染:

import React from 'react';
import Header from './components/Header';
//增加renderRoutes方法
import { renderRoutes } from 'react-router-config';

const  App = (props) => {
  console.log(props.route)
  return (
    <div>
      <Header></Header>
      <!--拿到Login和Home組件的路由-->
      {renderRoutes(props.route.routes)}
    </div>
  )
}

export default App;
復制代碼

至此,多級路由的渲染就完成啦。

part7: css的服務端渲染思路(context鉤子變量)

一、客戶端項目中引入CSS

還是以Home組件為例

//Home/style.css
body {
  background: gray;
}

復制代碼

現在,在Home組件代碼中引入:

import styles from './style.css';
復制代碼

要知道這樣的引入CSS代碼的方式在一般環境下是運行不起來的,需要在webpack中做相應的配置。 首先安裝相應的插件。

npm install style-loader css-loader --D
復制代碼
//webpack.client.js
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const clientConfig = {
  mode: 'development',
  entry: './src/client/index.js',
  module: {
    rules: [{
      test: /.css?$/,
      use: ['style-loader', {
        loader: 'css-loader',
        options: {
          modules: true
        }
      }]
    }]
  },
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'public')
  },
}

module.exports = merge(config, clientConfig);
復制代碼
//webpack.base.js代碼,回顧一下,配置了ES語法相關的內容
module.exports = {
  module: {
    rules: [{
      test: /.js$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      options: {
        presets: ['@babel/preset-react',  ['@babel/preset-env', {
          targets: {
            browsers: ['last 2 versions']
          }
        }]]
      }
    }]
  }
}
復制代碼

好,現在在客戶端CSS已經產生了效果。

手把手從頭開始教你,徹底理解服務端渲染原理

 

可是打開網頁源代碼:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

咦?里面并沒有出現任何有關CSS樣式的代碼啊!那這是什么原因呢?很簡單,其實我們的服務端的CSS加載還沒有做。接下來我們來完成CSS代碼的服務端的處理。

 

二、服務端CSS的引入

首先,來安裝一個webpack的插件,

npm install -D isomorphic-style-loader
復制代碼

然后再webpack.server.js中做好相應的css配置:

//webpack.server.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const serverConfig = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  externals: [nodeExternals()],
  module: {
    rules: [{
      test: /.css?$/,
      use: ['isomorphic-style-loader', {
        loader: 'css-loader',
        options: {
          modules: true
        }
      }]
    }]
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'build')
  }
}

module.exports = merge(config, serverConfig);
復制代碼

它做了些什么事情?

再看看這行代碼:

import styles from './style.css';
復制代碼

引入css文件時,這個isomorphic-style-loader幫我們在styles中掛了三個函數。輸出styles看看:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

現在我們的目標是拿到CSS代碼,直接通過styles._getCss即可獲得。

 

那我們拿到CSS代碼后放到哪里呢?其實react-router-dom中的StaticRouter中已經幫我們準備了一個鉤子變量context。如下

//context從外界傳入
<StaticRouter location={req.path} context={context}>
    <div>
        {renderRoutes(routes)}
    </div>
</StaticRouter>
復制代碼

這就意味著在路由配置對象routes中的組件都能在服務端渲染的過程中拿到這個context,而且這個context對于組件來說,就相當于組件中的props.staticContext。并且,這個props.staticContext只會在服務端渲染的過程中存在,而客戶端渲染的時候不會被定義。這就讓我們能夠通過這個變量來區分兩種渲染環境啦。

現在,我們需要在服務端的render函數執行之前,初始化context變量的值:

let context = { css: [] }
復制代碼

我們只需要在組件的componentWillMount生命周期中編寫相應的邏輯即可:

componentWillMount() {
  //判斷是否為服務端渲染環境
  if (this.props.staticContext) {
    this.props.staticContext.css.push(styles._getCss())
  }
}
復制代碼

服務端的renderToString執行完成后,context的CSS現在已經是一個有內容的數組,讓我們來獲取其中的CSS代碼:

//拼接代碼
const cssStr = context.css.length ? context.css.join('n') : '';
復制代碼

現在掛載到頁面:

//放到返回的html字符串里的header里面
<style>${cssStr}</style>
復制代碼

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

網頁源代碼中看到了CSS代碼,效果也沒有問題。CSS渲染完成!

 

三、利用高階組件優化代碼

也許你已經發現,對于每一個含有樣式的組件,都需要在componentWillMount生命周期中執行完全相同的邏輯,對于這些邏輯我們是否能夠把它封裝起來,不用反復出現呢?

其實是可以實現的。利用高階組件就可以完成:

//根目錄下創建withStyle.js文件
import React, { Component } from 'react';
//函數返回組件
//需要傳入的第一個參數是需要裝飾的組件
//第二個參數是styles對象
export default (DecoratedComponent, styles) => {
  return class NewComponent extends Component {
    componentWillMount() {
      //判斷是否為服務端渲染過程
      if (this.props.staticContext) {
        this.props.staticContext.css.push(styles._getCss())
      }
    }
    render() {
      return <DecoratedComponent {...this.props} />
    }
  }
}
復制代碼

然后讓這個導出的函數包裹我們的Home組件。

import WithStyle from '../../withStyle';
//......
const exportHome = connect(mapStateToProps, mapDispatchToProps)(withStyle(Home, styles));
export default exportHome;
復制代碼

這樣是不是簡潔很多了呢?將來對于越來越多的組件,采用這種方式也是完全可以的。

part8: 做好SEO的一些技巧,引入react-helmet

這一節我們來簡單的聊一點SEO相關的內容。

一、SEO技巧分享

所謂SEO(Search Engine Optimization),指的是利用搜索引擎的規則提高網站在有關搜索引擎內的自然排名。現在的搜索引擎爬蟲一般是全文分析的模式,分析內容涵蓋了一個網站主要3個部分的內容:文本、多媒體(主要是圖片)和外部鏈接,通過這些來判斷網站的類型和主題。因此,在做SEO優化的時候,可以圍繞這三個角度來展開。

對于文本來說,盡量不要抄襲已經存在的文章,以寫技術博客為例,東拼西湊抄來的文章排名一般不會高,如果需要引用別人的文章要記得聲明出處,不過最好是原創,這樣排名效果會比較好。多媒體包含了視頻、圖片等文件形式,現在比較權威的搜索引擎爬蟲比如google做到對圖片的分析是基本沒有問題的,因此高質量的圖片也是加分項。另外是外部鏈接,也就是網站中a標簽的指向,最好也是和當前網站相關的一些鏈接,更容易讓爬蟲分析。

當然,做好網站的門面,也就是標題和描述也是至關重要的。如:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

網站標題中不僅僅包含了關鍵詞,而且有比較詳細和靠譜的描述,這讓用戶一看到就覺得非常親切和可靠,有一種想要點擊的沖動,這就表明網站的轉化率比較高。

 

二、引入react-helmet

而React項目中,開發的是單頁面的應用,頁面始終只有一份title和description,如何根據不同的組件顯示來對應不同的網站標題和描述呢?

其實是可以做到的。

npm install react-helmet --save
復制代碼

組件代碼:(還是以Home組件為例)

import { Helmet } from 'react-helmet';

//...
render() { 
    return (
      <Fragment>
        <!--Helmet標簽中的內容會被放到客戶端的head部分-->
        <Helmet>
          <title>這是三元的技術博客,分享前端知識</title>
          <meta name="description" content="這是三元的技術博客,分享前端知識"/>
        </Helmet>
        <div className="test">
          {
            this.getList()
          }
        </div>
      </Fragment>
     
    );
//...
復制代碼

這只是做了客戶端的部分,在服務端仍需要做相應的處理。

其實也非常簡單:

//server/utils.js
import { renderToString } from 'react-dom/server';
import {  StaticRouter } from 'react-router-dom'; 
import React from 'react';
import { Provider } from "react-redux";
import { renderRoutes } from 'react-router-config';
import { Helmet } from 'react-helmet';

export const render = (store, routes, req, context) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path} context={context}>
        <div>
          {renderRoutes(routes)}
        </div>
      </StaticRouter>
    </Provider>
  );
  //拿到helmet對象,然后在html字符串中引入
  const helmet = Helmet.renderStatic();

  const cssStr = context.css.length ? context.css.join('n') : '';

  return  `
    <html>
      <head>
        <style>${cssStr}</style>
        ${helmet.title.toString()}
        ${helmet.meta.toString()}
      </head>
      <body>
        <div id="root">${content}</div>
        <script>
          window.context = {
            state: ${JSON.stringify(store.getState())}
          }
        </script>
        <script src="/index.js"></script>
      </body>
    </html>
  `
};
復制代碼

現在來看看效果:

 

手把手從頭開始教你,徹底理解服務端渲染原理

 

網頁源代碼中顯示出對應的title和description, 客戶端的顯示也沒有任何問題,大功告成!

 

關于React的服務端渲染原理,就先分享到這里,內容還是比較復雜的,對于前端的綜合能力要求也比較高,但是堅持跟著學下來,一定會大有裨益的。相信你看了這一系列之后也有能力造出自己的SSR輪子,更加深刻地理解這一方面的技術。


原鏈接:https://juejin.im/post/5d1fe6be51882579db031a6d

分享到:
標簽:渲染 服務端
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定