国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

從頭開始,徹底理解服務(wù)端渲染原理(8千字匯總長文)

hiyang / 2092人閱讀

摘要:到此,就初步實(shí)現(xiàn)了一個(gè)組件是服務(wù)端渲染。服務(wù)端渲染完成頁面結(jié)構(gòu),瀏覽器端渲染完成事件綁定。但是,在服務(wù)端渲染中卻出現(xiàn)了問題。根據(jù)這個(gè)思路,服務(wù)端渲染中異步數(shù)據(jù)的獲取功能就完成啦。

大家好,我是神三元,這一次,讓我們來以React為例,把服務(wù)端渲染(Server Side Render,簡稱“SSR”)學(xué)個(gè)明明白白。

這里附上這個(gè)項(xiàng)目的github地址:
https://github.com/sanyuan070...

歡迎大家點(diǎn)star,提issue,一起進(jìn)步!

## part1:實(shí)現(xiàn)一個(gè)基礎(chǔ)的React組件SSR
這一部分來簡要實(shí)現(xiàn)一個(gè)React組件的SSR。

### 一. SSR vs CSR
什么是服務(wù)端渲染?

廢話不多說,直接起一個(gè)express服務(wù)器。

var express = require("express")
var app = express()

app.get("/", (req, res) => {
 res.send(
 `
   
     
       hello
     
     
       

hello

world

` ) }) app.listen(3001, () => { console.log("listen:3001") })

啟動(dòng)之后打開localhost:3001可以看到頁面顯示了hello world。而且打開網(wǎng)頁源代碼:


也能夠完成顯示。

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

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


然而打開網(wǎng)頁源代碼。


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

因此,CSR和SSR最大的區(qū)別在于前者的頁面渲染是JS負(fù)責(zé)進(jìn)行的,而后者是服務(wù)器端直接返回HTML讓瀏覽器直接渲染。

為什么要使用服務(wù)端渲染呢?


傳統(tǒng)CSR的弊端:

由于頁面顯示過程要進(jìn)行JS文件拉取和React代碼執(zhí)行,首屏加載時(shí)間會比較慢。

對于SEO(Search Engine Optimazition,即搜索引擎優(yōu)化),完全無能為力,因?yàn)樗阉饕媾老x只認(rèn)識html結(jié)構(gòu)的內(nèi)容,而不能識別JS代碼內(nèi)容。

SSR的出現(xiàn),就是為了解決這些傳統(tǒng)CSR的弊端。

二、實(shí)現(xiàn)React組件的服務(wù)端渲染

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

// containers/Home.js
import React from "react";
const Home = () => {
  return (
    
This is sanyuan
) } export default Home

現(xiàn)在的任務(wù)就是將它轉(zhuǎn)換為html代碼返回給瀏覽器。
總所周知,JSX中的標(biāo)簽其實(shí)是基于虛擬DOM的,最終要通過一定的方法將其轉(zhuǎn)換為真實(shí)DOM。虛擬DOM也就是JS對象,可以看出整個(gè)服務(wù)端的渲染流程就是通過虛擬DOM的編譯來完成的,因此虛擬DOM巨大的表達(dá)力也可見一斑了。

而react-dom這個(gè)庫中剛好實(shí)現(xiàn)了編譯虛擬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();
app.get("/", function (req, res) {
   res.send(
   `
    
      
        ssr
      
      
        
${content}
` ); }) app.listen(3001, () => { console.log("listen:3001") })

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

part2: 初識同構(gòu) 一.引入同構(gòu)

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

// containers/Home.js
import React from "react";
const Home = () => {
  return (
    
This is sanyuan
) } export default Home

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

那怎么解決這個(gè)問題呢?

這就需要進(jìn)行同構(gòu)了。所謂同構(gòu),通俗的講,就是一套React代碼在服務(wù)器上運(yùn)行一遍,到達(dá)瀏覽器又運(yùn)行一遍。服務(wù)端渲染完成頁面結(jié)構(gòu),瀏覽器端渲染完成事件綁定。

那如何進(jìn)行瀏覽器端的事件綁定呢?

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


有沒有發(fā)現(xiàn)和之前的區(qū)別?區(qū)別就是多了一個(gè)script標(biāo)簽。而它拉取的JS代碼就是來完成同構(gòu)的。

那么這個(gè)index.js我們?nèi)绾紊a(chǎn)出來呢?

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

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

ReactDom.hydrate(, 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的靜態(tài)文件服務(wù):

const app = express();
app.use(express.static("public"));

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

綁定事件完成!

現(xiàn)在來初步總結(jié)一下同構(gòu)代碼執(zhí)行的流程:

二.同構(gòu)中的路由問題

現(xiàn)在寫一個(gè)路由的配置文件:

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

export default (
  
)

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

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

const App = () => {
  return (
    
      {Routes}
    
  )
}
ReactDom.hydrate(, document.getElementById("root"))

這時(shí)候控制臺會報(bào)錯(cuò),


因?yàn)樵赗outes.js中,每個(gè)Route組件外面包裹著一層div,但服務(wù)端返回的代碼中并沒有這個(gè)div,所以報(bào)錯(cuò)。如何去解決這個(gè)問題?需要將服務(wù)端的路由邏輯執(zhí)行一遍。

// 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) => {
  //構(gòu)建服務(wù)端的路由
  const content = renderToString(
    
      {Routes}
    
  );
  return `
    
      
        ssr
      
      
        
${content}
` }

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

part3: 同構(gòu)項(xiàng)目中引入Redux

這一節(jié)主要是講述Redux如何被引入到同構(gòu)項(xiàng)目中以及其中需要注意的問題。

重新回顧一下redux的運(yùn)作流程:


再回顧一下同構(gòu)的概念,即在React代碼客戶端和服務(wù)器端各自運(yùn)行一遍。

一、創(chuàng)建全局store

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

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

//導(dǎo)出創(chuàng)建的store
export default store
二、組件內(nèi)action和reducer的構(gòu)建

Home文件夾下的工程文件結(jié)構(gòu)如下:


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

//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";
//這么做是為了導(dǎo)出reducer讓全局的store來進(jìn)行合并
//那么在全局的store下的index.js中只需引入Home/store而不需要Home/store/reducer.js
//因?yàn)槟_手架會自動(dòng)識別文件夾下的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 => 
{item.title}
) } } const mapStateToProps = state => ({ list: state.home.newsList, }) const mapDispatchToProps = dispatch => ({ getHomeList() { dispatch(getHomeList()); } }) //連接store export default connect(mapStateToProps, mapDispatchToProps)(Home);

對于store的連接操作,在同構(gòu)項(xiàng)目中分兩個(gè)部分,一個(gè)是與客戶端store的連接,另一部分是與服務(wù)端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 (
    
      
        {routes}
      
    
  )
}

ReactDom.hydrate(, document.getElementById("root"))

服務(wù)端:

//src/server/index.js的內(nèi)容保持不變
//下面是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(
    
      
        {Routes}
      
    
  );
  return `
    
      
        ssr
      
      
        
${content}
` }
四、潛在的坑

其實(shí)上面這樣的store創(chuàng)建方式是存在問題的,什么原因呢?

上面的store是一個(gè)單例,當(dāng)這個(gè)單例導(dǎo)出去后,所有的用戶用的是同一份store,這是不應(yīng)該的。那么這么解這個(gè)問題呢?

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

//導(dǎo)出部分修改
export default  () => {
  return createStore(reducer, applyMiddleware(thunk))
}

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

part4: 異步數(shù)據(jù)的服務(wù)端渲染方案(數(shù)據(jù)注水與脫水) 一、問題引入

在平常客戶端的React開發(fā)中,我們一般在組件的componentDidMount生命周期函數(shù)進(jìn)行異步數(shù)據(jù)的獲取。但是,在服務(wù)端渲染中卻出現(xiàn)了問題。

現(xiàn)在我在componentDidMount鉤子函數(shù)中進(jìn)行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 => {
    //另外起的本地的后端服務(wù)
    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;
  }
}

好,現(xiàn)在啟動(dòng)服務(wù)。


現(xiàn)在頁面能夠正常渲染,但是打開網(wǎng)頁源代碼。


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

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

現(xiàn)在的工作就是讓服務(wù)端將獲得數(shù)據(jù)的操作執(zhí)行一遍,以達(dá)到真正的服務(wù)端渲染的效果。

二、改造路由

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

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

export default [
{
  path: "/",
  component: Home,
  exact: true,
  loadData: Home.loadData,//服務(wù)端獲取異步數(shù)據(jù)的函數(shù)
  key: "home"
},
{
  path: "/login",
  component: Login,
  exact: true,
  key: "login"
}
}];

此時(shí)客戶端和服務(wù)端中編寫的JSX代碼也發(fā)生了相應(yīng)變化

//客戶端
//以下的routes變量均指routes.js導(dǎo)出的數(shù)組

  
      
{ routers.map(route => { }) }
//服務(wù)端

  
      
{ routers.map(route => { }) }

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

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

  import { matchRoutes } from "react-router-config";
  //調(diào)用matchRoutes用來匹配當(dāng)前路由(支持多級路由)
  const matchedRoutes = matchRoutes(routes, req.path)
  //promise對象數(shù)組
  const promises = [];
  matchedRoutes.forEach(item => {
    //如果這個(gè)路由對應(yīng)的組件有l(wèi)oadData方法
    if (item.route.loadData) {
      //那么就執(zhí)行一次,并將store傳進(jìn)去
      //注意loadData函數(shù)調(diào)用后需要返回Promise對象
      promises.push(item.route.loadData(store))
    }
  })
  Promise.all(promises).then(() => {
      //此時(shí)該有的數(shù)據(jù)都已經(jīng)到store里面去了
      //執(zhí)行渲染的過程(res.send操作)
  }
  )

現(xiàn)在就可以安心的寫我們的loadData函數(shù),其實(shí)前面的鋪墊工作做好后,這個(gè)函數(shù)是相當(dāng)容易的。

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))
      })
  }
}

根據(jù)這個(gè)思路,服務(wù)端渲染中異步數(shù)據(jù)的獲取功能就完成啦。

三、數(shù)據(jù)的注水和脫水

其實(shí)目前做了這里還是存在一些細(xì)節(jié)問題的。比如當(dāng)我將生命周期鉤子里面的異步請求函數(shù)注釋,現(xiàn)在頁面中不會有任何的數(shù)據(jù),但是打開網(wǎng)頁源代碼,卻發(fā)現(xiàn):


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

其實(shí)也很好理解。當(dāng)服務(wù)端拿到store并獲取數(shù)據(jù)后,客戶端的js代碼又執(zhí)行一遍,在客戶端代碼執(zhí)行的時(shí)候又創(chuàng)建了一個(gè)空的store,兩個(gè)store的數(shù)據(jù)不能同步。

那如何才能讓這兩個(gè)store的數(shù)據(jù)同步變化呢?

首先,在服務(wù)端獲取獲取之后,在返回的html代碼中加入這樣一個(gè)script標(biāo)簽:

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

//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
})
//服務(wù)端的store創(chuàng)建函數(shù)
export const getStore = () => {
  return createStore(reducer, applyMiddleware(thunk));
}
//客戶端的store創(chuàng)建函數(shù)
export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
  return createStore(reducer, defaultState, applyMiddleware(thunk));
}

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

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

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

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

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

part5: node作中間層及請求代碼優(yōu)化 一、為什么要引入node中間層?

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

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

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

它最終解決的前后端協(xié)作的問題。

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

二、SSR框架中引入中間層

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

對客戶端而言,最好通過node中間層。而對于這個(gè)SSR項(xiàng)目而言,node開啟的服務(wù)器本來就是一個(gè)中間層的角色,因而對于服務(wù)器端執(zhí)行數(shù)據(jù)請求而言,就可以直接請求真正的后端接口啦。

//actions.js
//參數(shù)server表示當(dāng)前請求是否發(fā)生在node服務(wù)端
const getUrl = (server) => {
    return server ? "xxxx(后端接口地址)" : "/api/sanyuan.json(node接口)";
}
//這個(gè)server參數(shù)是Home組件里面?zhèn)鬟^來的,
//在componentDidMount中調(diào)用這個(gè)action時(shí)傳入false,
//在loadData函數(shù)中調(diào)用時(shí)傳入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應(yīng)拿到前端的請求做轉(zhuǎn)發(fā),這里是直接用proxy形式來做,也可以用node多帶帶向后端發(fā)送一次HTTP請求。

//增加如下代碼
import proxy from "express-http-proxy";
//相當(dāng)于攔截到了前端請求地址中的/api部分,然后換成另一個(gè)地址
app.use("/api", proxy("http://xxxxxx(服務(wù)端地址)", {
  proxyReqPathResolver: function(req) {
    return "/api"+req.url;
  }
}));
三、請求代碼優(yōu)化

其實(shí)請求的代碼還是有優(yōu)化的余地的,仔細(xì)想想,上面的server參數(shù)其實(shí)是不用傳遞的。

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

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

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

export default instance


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

const instance = axios.create({
  //即當(dāng)前路徑的node服務(wù)
  baseURL: "/"
})

export default instance

然后對全局下store的代碼做一個(gè)微調(diào):

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)));
}

現(xiàn)在Home組件中請求數(shù)據(jù)的action無需傳參,actions.js中的請求代碼如下:

export const getHomeList = () => {
  //返回函數(shù)中的默認(rèn)第三個(gè)參數(shù)是withExtraArgument傳進(jìn)來的axios實(shí)例
  return (dispatch, getState, axiosInstance) => {
    return axiosInstance.get("/api/sanyuan.json")
      .then((res) => {
        const list = res.data.data;
        console.log(res)
        dispatch(changeList(list))
      })
  }
}

至此,代碼優(yōu)化就做的差不多了,這種代碼封裝的技巧其實(shí)可以用在其他的項(xiàng)目當(dāng)中,其實(shí)還是比較優(yōu)雅的。

part6: 多級路由渲染(renderRoutes)

現(xiàn)在將routes.js的內(nèi)容改變?nèi)缦?

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

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

現(xiàn)在的需求是讓頁面公用一個(gè)Header組件,App組件編寫如下:

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

const  App = (props) => {
  console.log(props.route)
  return (
    
) } export default App;

對于多級路由的渲染,需要服務(wù)端和客戶端各執(zhí)行一次。
因此編寫的JSX代碼都應(yīng)有所實(shí)現(xiàn):

//routes是指routes.js中返回的數(shù)組
//服務(wù)端:

  
    
{renderRoutes(routes)}
//客戶端:
{renderRoutes(routes)}

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

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

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

const  App = (props) => {
  console.log(props.route)
  return (
    
{renderRoutes(props.route.routes)}
) } export default App;

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

part7: CSS的服務(wù)端渲染思路(context鉤子變量) 一、客戶端項(xiàng)目中引入CSS

還是以Home組件為例

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

現(xiàn)在,在Home組件代碼中引入:

import styles from "./style.css";

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

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語法相關(guān)的內(nèi)容
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"]
          }
        }]]
      }
    }]
  }
}

好,現(xiàn)在在客戶端CSS已經(jīng)產(chǎn)生了效果。

可是打開網(wǎng)頁源代碼:


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

二、服務(wù)端CSS的引入

首先,來安裝一個(gè)webpack的插件,

npm install -D isomorphic-style-loader

然后再webpack.server.js中做好相應(yīng)的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文件時(shí),這個(gè)isomorphic-style-loader幫我們在styles中掛了三個(gè)函數(shù)。輸出styles看看:


現(xiàn)在我們的目標(biāo)是拿到CSS代碼,直接通過styles._getCss即可獲得。

那我們拿到CSS代碼后放到哪里呢?其實(shí)react-router-dom中的StaticRouter中已經(jīng)幫我們準(zhǔn)備了一個(gè)鉤子變量context。如下

//context從外界傳入

    
{renderRoutes(routes)}

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

現(xiàn)在,我們需要在服務(wù)端的render函數(shù)執(zhí)行之前,初始化context變量的值:

let context = { css: [] }

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

componentWillMount() {
  //判斷是否為服務(wù)端渲染環(huán)境
  if (this.props.staticContext) {
    this.props.staticContext.css.push(styles._getCss())
  }
}

服務(wù)端的renderToString執(zhí)行完成后,context的CSS現(xiàn)在已經(jīng)是一個(gè)有內(nèi)容的數(shù)組,讓我們來獲取其中的CSS代碼:

//拼接代碼
const cssStr = context.css.length ? context.css.join("
") : "";

現(xiàn)在掛載到頁面:

//放到返回的html字符串里的header里面


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

三、利用高階組件優(yōu)化代碼

也許你已經(jīng)發(fā)現(xiàn),對于每一個(gè)含有樣式的組件,都需要在componentWillMount生命周期中執(zhí)行完全相同的邏輯,對于這些邏輯我們是否能夠把它封裝起來,不用反復(fù)出現(xiàn)呢?

其實(shí)是可以實(shí)現(xiàn)的。利用高階組件就可以完成:

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

然后讓這個(gè)導(dǎo)出的函數(shù)包裹我們的Home組件。

import WithStyle from "../../withStyle";
//......
const exportHome = connect(mapStateToProps, mapDispatchToProps)(withStyle(Home, styles));
export default exportHome;

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

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

這一節(jié)我們來簡單的聊一點(diǎn)SEO相關(guān)的內(nèi)容。

一、SEO技巧分享

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

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

當(dāng)然,做好網(wǎng)站的門面,也就是標(biāo)題和描述也是至關(guān)重要的。如:


網(wǎng)站標(biāo)題中不僅僅包含了關(guān)鍵詞,而且有比較詳細(xì)和靠譜的描述,這讓用戶一看到就覺得非常親切和可靠,有一種想要點(diǎn)擊的沖動(dòng),這就表明網(wǎng)站的轉(zhuǎn)化率比較高。

二、引入react-helmet

而React項(xiàng)目中,開發(fā)的是單頁面的應(yīng)用,頁面始終只有一份title和description,如何根據(jù)不同的組件顯示來對應(yīng)不同的網(wǎng)站標(biāo)題和描述呢?

其實(shí)是可以做到的。

npm install react-helmet --save

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

import { Helmet } from "react-helmet";

//...
render() { 
    return (
      
        
        
          這是三元的技術(shù)博客,分享前端知識
          
        
        
{ this.getList() }
); //...

這只是做了客戶端的部分,在服務(wù)端仍需要做相應(yīng)的處理。

其實(shí)也非常簡單:

//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(
    
      
        
{renderRoutes(routes)}
); //拿到helmet對象,然后在html字符串中引入 const helmet = Helmet.renderStatic(); const cssStr = context.css.length ? context.css.join(" ") : ""; return ` ${helmet.title.toString()} ${helmet.meta.toString()}
${content}
` };

現(xiàn)在來看看效果:


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

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

參考資料:

慕課網(wǎng)《React服務(wù)器渲染原理解析與實(shí)踐》課程

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/106619.html

相關(guān)文章

  • 平時(shí)積累的前資源,持續(xù)更新中。。。

    本文收集學(xué)習(xí)過程中使用到的資源。 持續(xù)更新中…… 項(xiàng)目地址 https://github.com/abc-club/f... 目錄 vue react react-native Weex typescript Taro nodejs 常用庫 css js es6 移動(dòng)端 微信公眾號 小程序 webpack GraphQL 性能與監(jiān)控 高質(zhì)文章 趨勢 動(dòng)效 數(shù)據(jù)結(jié)構(gòu)與算法 js core 代碼規(guī)范...

    acrazing 評論0 收藏0
  • JavasScript重難點(diǎn)知識

    摘要:忍者級別的函數(shù)操作對于什么是匿名函數(shù),這里就不做過多介紹了。我們需要知道的是,對于而言,匿名函數(shù)是一個(gè)很重要且具有邏輯性的特性。通常,匿名函數(shù)的使用情況是創(chuàng)建一個(gè)供以后使用的函數(shù)。 JS 中的遞歸 遞歸, 遞歸基礎(chǔ), 斐波那契數(shù)列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執(zhí)行機(jī)制 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機(jī)制,如果...

    forsigner 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<