<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                [TOC] # 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 ~~~ <br> 現在的任務就是將它轉換為html代碼返回給瀏覽器。 總所周知,JSX中的標簽其實是基于虛擬DOM的,最終要通過一定的方法將其轉換為真實DOM。虛擬DOM也就是JS對象,可以看出整個服務端的渲染流程就是通過虛擬DOM的編譯來完成的,因此虛擬DOM巨大的表達力也可見一斑了。 <br> 而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') }) ~~~ 然而還有一個問題,node不能解析 `containers/Home.js` 中的組件內容,需要使用babel進行編譯。這里需要先配置webpack: ~~~ // 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'] } }]] } }] } } ~~~ <br> ~~~ // 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', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'build') }, externals: [nodeExternals()], module: { rules: [{ test: /\.css?$/, use: ['isomorphic-style-loader', { loader: 'css-loader', options: { modules: true } }] }] } } module.exports = merge.merge(config, serverConfig) ~~~ package.json添加下面的腳本 ~~~ "scripts": { "dev": "npm-run-all --parallel dev:**", "dev:build:server": "webpack --config webpack.server.js --watch", "dev:start": "nodemon --watch build --exec node \"./build/bundle.js\"" }, ~~~ <br> 啟動express服務,再瀏覽器上打開對應端口,頁面顯示出"this is sanyuan"。 到此,就初步實現了一個React組件是服務端渲染。 當然,這只是一個非常簡陋的SSR,事實上對于復雜的項目而言是無能為力的,在之后會一步步完善,打造出一個功能完整的React的SSR框架。 <br> <br> # 同構 ## 引入同構 其實前面的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代碼來控制。于是服務端返回的代碼變成了這樣: ![](https://user-gold-cdn.xitu.io/2019/7/2/16bb2a334ab13aed?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 有沒有發現和之前的區別?區別就是多了一個script標簽。而它拉取的JS代碼就是來完成同構的。 添加新文件 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')) ~~~ <br> 然后用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')); ~~~ <br> 現在來初步總結一下同構代碼執行的流程: ![](https://user-gold-cdn.xitu.io/2019/7/2/16bb2b6d8dc96733?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> <br> ## 同構中的路由問題 現在寫一個路由的配置文件: ~~~ // 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')) ~~~ 服務器端,`BrowserRouter`變成了`StaticRouter`。 React Router 無法根據 location 自動判斷當前所在頁面,而需要你把`req.url`傳給`StaticRouter`,后續的路由渲染邏輯雙端都是通用的。 <br> 這時候控制臺會報錯, ![](https://user-gold-cdn.xitu.io/2019/7/2/16bb2c18be02ec94?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 因為在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來處理。 <br> <br> # 同構項目中引入Redux 這一節主要是講述Redux如何被引入到同構項目中以及其中需要注意的問題。 重新回顧一下redux的運作流程: ![](https://user-gold-cdn.xitu.io/2019/7/4/16bbc59406a49fad?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 再回顧一下同構的概念,即在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文件夾下的工程文件結構如下: ![](https://user-gold-cdn.xitu.io/2019/7/4/16bbc579b0ddd386?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 在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。 <br> <br> # 異步數據的服務端渲染方案(數據注水與脫水) ## 問題引入 在平常客戶端的React開發中,我們一般在組件的componentDidMount生命周期函數進行異步數據的獲取。但是,在服務端渲染中卻出現了問題。 現在我在componentDidMount鉤子函數中進行Ajax請求: ~~~ import { getHomeList } from './store/actions' //...... componentDidMount() { this.props.getList(); } //...... const mapDispatchToProps = dispatch => ({ getList() { dispatch(getHomeList()); } }) ~~~ <br> ~~~ //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; } } ~~~ 好,現在啟動服務。 ![](https://user-gold-cdn.xitu.io/2019/7/4/16bbce109248ae2d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 現在頁面能夠正常渲染,但是打開網頁源代碼。 <br> ![](https://user-gold-cdn.xitu.io/2019/7/4/16bbce5ce6d84c12?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 源代碼里面并沒有這些列表數據啊!那這是為什么呢? <br> 讓我們來分析一下客戶端和服務端的運行流程,當瀏覽器發送請求時,服務器接受到請求,這時候服務器和客戶端的store都是空的,緊接著客戶端執行componentDidMount生命周期中的函數,獲取到數據并渲染到頁面,然而服務器端始終不會執行componentDidMount,因此不會拿到數據,這也導致服務器端的store始終是空的。換而言之,關于異步數據的操作始終只是客戶端渲染。 <br> 現在的工作就是讓服務端將獲得數據的操作執行一遍,以達到真正的服務端渲染的效果。 <br> ## 改造路由 在完成這個方案之前需要改造一下原有的路由,也就是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> ~~~ <br> ~~~ //服務端 <Provider store={store}> <StaticRouter> <div> { routers.map(route => { <Route {...route} /> }) } </div> </StaticRouter> </Provider> ~~~ <br> 其中配置了一個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操作) } ) ~~~ <br> 現在就可以安心的寫我們的loadData函數,其實前面的鋪墊工作做好后,這個函數是相當容易的。 ~~~ import { getHomeList } from './store/actions' Home.loadData = (store) => { return store.dispatch(getHomeList()) } ~~~ <br> ~~~ //actions.js export const getHomeList = () => { return dispatch => { return axios.get('xxxx') .then((res) => { const list = res.data.data; dispatch(changeList(list)) }) } } ~~~ 根據這個思路,服務端渲染中異步數據的獲取功能就完成啦。 <br> ## 數據的注水和脫水 其實目前做了這里還是存在一些細節問題的。比如當我將生命周期鉤子里面的異步請求函數注釋,現在頁面中不會有任何的數據,但是打開網頁源代碼,卻發現: ![](https://user-gold-cdn.xitu.io/2019/7/4/16bbd2754b461c80?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 數據已經掛載到了服務端返回的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() } } ~~~ <br> <br> # node作中間層及請求代碼優化 ## 為什么要引入node中間層? 其實任何技術都是與它的應用場景息息相關的。這里我們反復談的SSR,其實不到萬不得已我們是用不著它的,SSR所解決的最大的痛點在于SEO,但它同時帶來了更昂貴的成本。不僅因為服務端渲染需要更加復雜的處理邏輯,還因為同構的過程需要服務端和客戶端都執行一遍代碼,這雖然對于客戶端并沒有什么大礙,但對于服務端卻是巨大的壓力,因為數量龐大的訪問量,對于每一次訪問都要另外在服務器端執行一遍代碼進行計算和編譯,大大地消耗了服務器端的性能,成本隨之增加。如果訪問量足夠大的時候,以前不用SSR的時候一臺服務器能夠承受的壓力現在或許要增加到10臺才能抗住。痛點在于SEO,但如果實際上對SEO要求并不高的時候,那使用SSR就大可不必了。 <br> 那同樣地,為什么要引入node作為中間層呢?它是處在哪兩者的中間?又是解決了什么場景下的問題? <br> 在不用中間層的前后端分離開發模式下,前端一般直接請求后端的接口。但真實場景下,后端所給的數據格式并不是前端想要的,但處于性能原因或者其他的因素接口格式不能更改,這時候需要在前端做一些額外的數據處理操作。前端來操作數據本身無可厚非,但是當數據量變得龐大起來,那么在客戶端就是產生巨大的性能損耗,甚至影響到用戶體驗。在這個時候,node中間層的概念便應運而生。 <br> 它最終解決的前后端協作的問題。 <br> 一般的中間層工作流是這樣的:前端每次發送請求都是去請求node層的接口,然后node對于相應的前端請求做轉發,用node去請求真正的后端接口獲取數據,獲取后再由node層做對應的數據計算等處理操作,然后返回給前端。這就相當于讓node層替前端接管了對數據的操作。 ![](https://user-gold-cdn.xitu.io/2019/7/5/16bbff86c9c72be5?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> ## SSR框架中引入中間層 在之前搭建的SSR框架中,服務端和客戶端請求利用的是同一套請求后端接口的代碼,但這是不科學的。 <br> 對客戶端而言,最好通過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; } })); ~~~ <br> ## 請求代碼優化 其實請求的代碼還是有優化的余地的,仔細想想,上面的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 ~~~ <br> 然后對全局下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)) }) } } ~~~ <br> <br> # 多級路由渲染(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組件,依次循環。 <br> 那么,在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; ~~~ <br> <br> # CSS的服務端渲染思路 ## 客戶端項目中引入CSS 還是以Home組件為例 ~~~ //Home/style.css body { background: gray; } ~~~ <br> 現在,在Home組件代碼中引入: ~~~ import styles from './style.css'; ~~~ <br> 要知道這樣的引入CSS代碼的方式在一般環境下是運行不起來的,需要在webpack中做相應的配置。 首先安裝相應的插件。 <br> ~~~ npm install style-loader css-loader --D ~~~ <br> ~~~ //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); ~~~ <br> 好,現在在客戶端CSS已經產生了效果。 ![](https://user-gold-cdn.xitu.io/2019/7/5/16bc216db9c719da?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 可是打開網頁源代碼: ![](https://user-gold-cdn.xitu.io/2019/7/5/16bc2195465a5fc8?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 咦?里面并沒有出現任何有關CSS樣式的代碼啊!那這是什么原因呢?很簡單,其實我們的服務端的CSS加載還沒有做。接下來我們來完成CSS代碼的服務端的處理。 <br> ## 服務端CSS的引入 首先,來安裝一個webpack的插件, ~~~ npm install -D isomorphic-style-loader ~~~ 然后中做好相應的css配置: > 注意, css-loader 中要加上 esModule: false,否則輸出`<style><style>` 標簽時CSS內容會顯示為 `[Object Module]` > ~~~ // webpack.server.js // ... const serverConfig = { // ... module: { rules: [{ test: /\.css?$/, use: ['isomorphic-style-loader', { loader: 'css-loader', options: { modules: true } }] }] }, } ~~~ ~~~ // webpack.client.js // ... const clientConfig = { // ... module: { rules: [{ test: /\.css$/, use: ['isomorphic-style-loader', 'style-loader', { loader: 'css-loader', options: { esModule: false, } }] }] } } ~~~ <br> 然后,分別在客戶端入口和服務端入口添加配置: ~~~ // client/index.js import StyleContext from 'isomorphic-style-loader/StyleContext' const insertCss = (...styles) => { const removeCss = styles.map(style => style._insertCss()) return () => removeCss.forEach(dispose => dispose()) } const App = () => { return ( <Provider store={getClientStore()}> // 添加 StyleContext <StyleContext.Provider value={{ insertCss }}> <BrowserRouter> <div> {renderRoutes(Routes)} </div> </BrowserRouter> </StyleContext.Provider> </Provider> ) } ~~~ <br> ~~~ // server/index.js import?StyleContext?from'isomorphic-style-loader/StyleContext' export const render = (store, routes, req, context) => { const css = new Set(); const insertCss = (...styles) => styles.forEach(style => { css.add(style._getCss()) }) //構建服務端的路由 const content = renderToString( <Provider store={store}> // 添加 StyleContext <StyleContext.Provider value={{ insertCss }}> <StaticRouter location={req.path} > <div> {renderRoutes(routes)} </div> </StaticRouter> </StyleContext.Provider> </Provider> ); return ` <html> <head> <title>ssr</title> // 添加 style 標簽 <style>${[...css].join('')}</style> </head> <body> <div id="root">${content}</div> <script> window.context = { state: ${JSON.stringify(store.getState())} } </script> <script src="/index.js"></script> </body> </html> ` } ~~~ 最后,修改 組件 ~~~ // container/Home/index.js import styles from './style.css'; import withStyles from 'isomorphic-style-loader/withStyles'; class Home extends Component { render() { const { list } = this.props return list.map(item => <div key={item.id}>{item.title}</div>) } } // 連接store export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Home)); ~~~ <br> <br> # 做好SEO的一些技巧,引入react-helmet ## 引入react-helmet ~~~ npm install react-helmet --save ~~~ 組件代碼:(還是以Home組件為例) ~~~ import { Helmet } from 'react-helmet'; //... render() { return ( <React.Fragment> <!--Helmet標簽中的內容會被放到客戶端的head部分--> <Helmet> <title>這是三元的技術博客,分享前端知識</title> <meta name="description" content="這是三元的技術博客,分享前端知識"/> </Helmet> <div className="test"> { this.getList() } </div> </React.Fragment> ); //... ~~~ 這只是做了客戶端的部分,在服務端仍需要做相應的處理。 ~~~ //server/utils.js // ... import { Helmet } from 'react-helmet'; export const render = (store, routes, req, context) => { // ... //拿到helmet對象,然后在html字符串中引入 const helmet = Helmet.renderStatic(); 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> ` }; ~~~ 現在來看看效果: ![](https://user-gold-cdn.xitu.io/2019/7/5/16bc2ab358a4bf3d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> <br> # 參考資料 [從頭開始,徹底理解服務端渲染原理(8千字匯總長文)](https://juejin.cn/post/6844903881390964744)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看