react 同構初步(4)
- 2020 年 1 月 2 日
- 筆記
這是一個即時短課程的系列筆記。本筆記系列進度已更新到:https://github.com/dangjingtao/react-ssr
axios代理
用代理規避跨域其實是很簡單的事情,在往期的文章中已經有過類似的案例。但現在需要用"中台"的角度去思考問題。當前的項目分為三大部分:客戶端(瀏覽器),同構服務端(nodejs中台,端口9000)和負責純粹後端邏輯的後端(mockjs,端口9001)。
到目前為止的代碼中,客戶端如果要發送請求,會直接請求到mock.js。現實中接口數據來源不一定是node服務器,很可能是java,php或是別的語言。因此,從客戶端直接請求會發生跨域問題。而要求後端為他的接口提供的跨域支持,並非是件一定能夠滿足到你的事。
如果從server端(中台)渲染,跨域就不會發生。於是就衍生了一個問題:客戶端能否通過中台獲取mockjs的信息?
解決的思路在於對axios也進行同構(區分客戶端和服務端)。
redux-chunk傳遞axios對象
在前面的實踐中,我們用到了redux-chunk。
redux-chunk是一個redux中間件,它可以把異步請求放到action中,它實現非常簡單,不妨打開node_modules去看看它的源碼:
// node_modules/redux-chunk/src/index function createThunkMiddleware(extraArgument) { // 高階函數 return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } // 注意以下兩句代碼: const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
發現thunk是createThunkMiddleware()的返回值。
我們之前引入chunk時,都是引入直接使用。但是它還有一個withExtraArgument
屬性,又剛好提供了createThunkMiddleware()
方法。
顧名思義,withExtraArgument
就是提供額外的參數。當你調用此方法時,createThunkMiddleware
就會被激活。非常適合拿來傳遞全局變量。
我們在store.js中添加兩個axios,分別對應客戶端和中台:
// 儲存的入口 import { createStore, applyMiddleware, combineReducers } from "redux"; import thunk from 'redux-thunk'; import axios from 'axios'; import indexReducer from './index'; import userReducer from './user'; const reducer = combineReducers({ index: indexReducer, user: userReducer }); // 創建兩個axios,作為參數傳遞進createStore const serverAxios=axios.create({ baseURL:'http://localhost:9001' }); // 客戶端直接請求服務端(中台),因此不需要再加個前綴 const clientAxios=axios.create({ baseURL:'/' }); // 創建store export const getServerStore = () => { return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios))); } export const getClientStore = () => { // 把初始狀態放到window.__context中,作為全局變量,以此來獲取數據。 const defaultState = window.__context ? window.__context : {}; return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios))) }
回到store/index.js和user.js,在定義請求的地方就會多出一個參數,就是我們定義的axios對象:
// store/index.js // 不再需要引入axios,直接用參數中的axios export const getIndexList = server => { return (dispatch, getState, $axios) => { return $axios.get('/api/course/list').then((res)=>{ const { list } = res.data; console.log('list',list) dispatch(changeList(list)); }).catch(e=>{ // 容錯 return dispatch(changeList({ errMsg:e.message })); }); } }
// store/user.js export const getUserInfo = server => { return (dispatch, getState, $axios) => { // 返回promise return $axios.get('/api/user/info').then((res) => { const { info } = res.data; console.log('info', info); dispatch(getInfo(info)); }).catch(e => { console.log(e) // 容錯 return dispatch(getInfo({ errMsg: e.message })); }) } }
留意到這裡接口多了一個/api/,是為了對路由做區分。我們在mockjs中也增加api。同時取消跨域設置
// mockjs單純模擬接口 const express=require('express'); const app=express(); app.get('/api/course/list',(req,res)=>{ res.json({ code:0, list:[ {id:1,name:'javascript 從helloworld到放棄'}, {id:2,name:'背鍋的藝術'}, {id:3,name:'擼絲程序員如何征服女測試'}, {id:4,name:'python從入門到跑路'} ] }); }); app.get('/api/user/info',(req,res)=>{ res.json({ code:0, info:{ name:'黨某某', honor:'首席背鍋工程師' } }); }); app.listen('9001',()=>{ console.log('mock has started..') });
此時,當數據為空時,前端就會對9000端口發起api請求。
請求轉發
現在來處理服務端(中台)的邏輯,在server/index.js下,你可以很直觀地這麼寫:
// 監聽所有頁面 app.get('*', (req, res) => { // 增加路由判斷:api下的路由全部做轉發處理: if(req.url.startWith('/api')){ // 轉發9001 } // ... });
但是這種面向過程編程的寫法並不是最好的實踐。因此考慮通過中間件處理這種邏輯。在express框架,http-proxy-middlewere可以幫助我們實現此功能。
文檔地址:https://github.com/chimurai/http-proxy-middleware
npm i http-proxy-middleware -S
// 使用方法 var express = require('express'); var proxy = require('http-proxy-middleware'); var app = express(); app.use( '/api', proxy({ target: 'http://www.example.org', changeOrigin: true }) ); app.listen(3000); // http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar
安裝好後,如法炮製:
// server/index.js import proxy from 'http-proxy-middleware'; // ... app.use( '/api', proxy({ target: 'http://localhost:9001', changeOrigin: true }) );
這時候在客戶端接口,就會看到中台9000轉發了後台9001的數據了:
由此,中台代理後台請求功能完成。
圖標/樣式
現在的同構應用,有個不大不小的問題:在network中,請求favicon.ico總是404。
我們從百度盜一個圖標過來:https://www.baidu.com/favicon.ico
下載下來然後塞到public中即可。
當前的應用實在太丑了。客戶說:"我喜歡字體那種冷冷的暖,暖暖的冷。"
在src下面創建style文件夾,然後創建user.css
* { color:red}
在container/user.js中引入css:
import '../style/user.css';
此時運行頁面還是報錯的,想要讓它支持css樣式,需要webpack層面的支持。
先配置客戶端和服務端webpack:
// webpack.client.js // webpack.server.js { test:/.css$/, use:['style-loader','css-loader'] }
配好之後,你滿心歡喜地npm start:
document對象在server 層根本是不存在的。因此需要安裝專門做同構應用的style-loader:isomorphic-style-loader(https://github.com/kriasoft/isomorphic-style-loader)
npm i isomorphic-style-loader -S
對server端webpack做單獨配置:
{ test: /.css$/, use: [ 'isomorphic-style-loader', { loader: 'css-loader', options: { importLoaders: 1 } } ] }
刷新:
你會發現整個頁面都紅了。查看源代碼,發現css是直接插入到header的style標籤中的,直接作用於全局。
如何對樣式進行模塊化(BEM)處理?將在後面解決。
狀態碼支持
當請求到一個不匹配的路由/接口,如何優雅地告訴用戶404?
現在把Index的匹配去掉,增加404NotFound組件:
// App.js import NotFound from './container/NotFound'; export default [ // ... { component:NotFound, key:'notFound' } ]
404頁面:
// container/NotFound.js import React from 'react'; function NotFound(props){ return <div> <h1>404 你來到了沒有知識的星球..</h1> <img id="notFound" src="404.jpeg" /> </div> } export default NotFound;
然後在header組件中加上一條404路由:
<Link to={`/${Math.random()}`}>404</Link>
刷新,看到了404的請求:
為什麼是200?此時應該是404才對。
去官網學習下:
https://reacttraining.com/react-router/web/guides/server-rendering
We can do the same thing as above. Create a component that adds some context and render it anywhere in the app to get a different status code.
function Status({ code, children }) { return ( <Route render={({ staticContext }) => { if (staticContext) staticContext.status = code; return children; }} /> ); } // Now you can render a Status anywhere in the app that you want to add the code to staticContext. function NotFound() { return ( <Status code={404}> <div> <h1>Sorry, can』t find that.</h1> </div> </Status> ); } function App() { return ( <Switch> <Route path="/about" component={About} /> <Route path="/dashboard" component={Dashboard} /> <Route component={NotFound} /> </Switch> ); }
你可以傳遞一個全局的context對象給你創建的notfound組件。
在server/index.js的promise循環中定義一個context空對象,傳遞給路由組件:
Promise.all(promises).then(data => { // 定義context空對象 const context={}; // react組件解析為html const content = renderToString( <Provider store={store}> <StaticRouter location={req.url} context={context}> ...
回到NotFound.js,看下它的props,客戶端多了一個undefined的staticContext。但是在server端打印的是{}。這是在服務端渲染路由StaticRouter
的獨有屬性:所有子路由都能訪問。
在Notfound中定義一個Status組件用來給staticContext賦值:
import React from 'react'; import { Route } from 'react-router-dom'; function Status({ code, children }) { return <Route render={(props) => { const { staticContext } = props; if (staticContext) { staticContext.statusCode = code; } return children; }} /> } function NotFound(props) { // props.staticContext // 給staticContext賦值 statusCode=404 console.log('props', props) return <Status code={404}> <h1>404 你來到了沒有知識的星球..</h1> <img id="notFound" src="404.jpeg" /> </Status> } export default NotFound;
回到server/index.js就可以在renderToString之後拿到帶有statusCode的context了。
const context = {}; const content = renderToString( <Provider store={store}> <StaticRouter location={req.url} context={context}> <Header /> {routes.map(route => <Route {...route} />)} </StaticRouter> </Provider> ); if (context.statusCode) { res.status(context.statusCode) }
這時候就看到404狀態了。現在404是非精確匹配的。想要渲染,可以用switch組件來實現
// server/index.js import { StaticRouter, matchPath, Route, Switch } from 'react-router-dom'; const content = renderToString( <Provider store={store}> <StaticRouter location={req.url} context={context}> <Header /> <Switch> {routes.map(route => <Route {...route} />)} </Switch> </StaticRouter> </Provider> );
然後在客戶端也做一個同構處理:
import { BrowserRouter, Switch} from 'react-router-dom'; const Page = (<Provider store={getClientStore()}> <BrowserRouter> <Header/> <Switch> {routes.map(route => <Route {...route} />)} </Switch> </BrowserRouter> </Provider>);
404功能搞定
又比如說我要對user頁面做登錄支持,當訪問user頁面時,做301重定向:
// container/User.js import {Redirect} from 'react-router-dom'; function User(props){ // 判斷cookie之類。。。 return <Redirect to={'/login'}></Redirect> // .. }
定義了redirect,此時context的action是替換("REPLACE"),url是跳轉的地址。因此在服務端可以這麼判斷
if (context.action=='REPLACE') { res.redirect(301,context.url);}
那麼服務端的跳轉邏輯就完成了。