各人好,我是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(`<head><title>hellotitle>head><body><h1>helloh1><p>worldp>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的短处:
因为页面显示过程要停止JS文件拉取和React代码施行,首屏加载时间会比力慢。关于SEO(Search Engine Optimazition,即搜刮引擎优化),完全力所不及,因为搜刮引擎爬虫只认识html构造的内容,而不克不及识别JS代码内容。SSR的呈现,就是为领会决那些传统CSR的短处。
二、实现React组件的办事端衬着刚刚起的express办事返回的只是一个通俗的html字符串,但我们讨论的是若何停止React的办事端衬着,那么怎么做呢? 起首写一个简单的React组件:
// containers/Home.jsimportReactfromreact;constHome =()=>{return(<div><div>This is sanyuandiv>div>)}exportdefaultHome复造代码如今的使命就是将它转换为html代码返回给阅读器。 寡所周知,JSX中的标签其实是基于虚拟DOM的,最末要通过必然的办法将其转换为实在DOM。虚拟DOM也就是JS对象,能够看出整个办事端的衬着流程就是通过虚拟DOM的编译来完成的,因而虚拟DOM庞大的表达力也可见一斑了。
而react-dom那个库中刚好实现了编译虚拟DOM的办法。做法如下:
//server/index.jsimportexpressfromexpress;import{ renderToString }fromreact-dom/server;importHomefrom./containers/Home;const app = express();const content = renderToString();app.get(/, function (req, res) {res.send(`<head><title>ssrtitle>head><body><divid="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.jsimportReactfromreact;constHome =()=>{return(<div><div>This is sanyuandiv><buttononClick={()=>{alert(666)}}>clickbutton>div>)}exportdefaultHome复造代码再试一下,你会诧异的发现,事务绑定无效!那那是为什么呢?原因很简单,react-dom/server下的renderToString并没有干事件相关的处置,因而返回给阅读器的内容不会有事务绑定。
那怎么处理那个问题呢?
那就需要停止同构了。所谓同构,通俗的讲,就是一套React代码在办事器上运行一遍,抵达阅读器又运行一遍。办事端衬着完成页面构造,阅读器端衬着完成事务绑定。
那若何停止阅读器端的事务绑定呢?
独一的体例就是让阅读器去拉取JS文件施行,让JS代码来控造。于是办事端返回的代码酿成了如许:
有没有发现和之前的区别?区别就是多了一个script标签。而它拉取的JS代码就是来完成同构的。
那么那个index.js我们若何消费出来呢?
在那里,要用到react-dom。详细做法其实就很简单了:
//client/index. jsimportReactfromreact;importReactDomfromreact-dom;importHomefrom../containers/Home;ReactDom.hydrate(,document.getElementById(root))复造代码然后用webpack将其编译打包成index.js:
//webpack.client.jsconstpath =require(path);constmerge =require(webpack-merge);constconfig =require(./webpack.base);constclientConfig = {mode:development,entry:./src/client/index.js,output: {filename:index.js,path: path.resolve(__dirname,public)},}module.exports = merge(config, clientConfig);//webpack.base.jsmodule.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的静态文件办事:
constapp = express();app.use(express.static(public));复造代码如今前端的script就能拿到控造阅读器的JS代码啦。
绑定事务完成!
如今来初步总结一下同构代码施行的流程:
二.同构中的路由问题如今写一个路由的设置装备摆设文件:
// Routes.jsimportReactfromreact;import{Route}fromreact-router-domimportHomefrom./containers/Home;importLoginfrom./containers/Loginexportdefault(<div><Routepath=/exactcomponent={Home}>Route><Routepath=/loginexactcomponent={Login}>Route>div>)复造代码在客户端的控造代码,也就是上面写过的client/index.js中,要做响应的更改:
importReactfromreact;importReactDomfromreact-dom;import{ BrowserRouter }fromreact-router-domimportRoutesfrom../RoutesconstApp =()=>{return(<BrowserRouter>{Routes}BrowserRouter>)}ReactDom.hydrate(<App/>, document.getElementById(root))复造代码那时候控造台会报错,
因为在Routes.js中,每个Route组件外面包裹着一层div,但办事端返回的代码中并没有那个div,所以报错。若何去处理那个问题?需要将办事端的路由逻辑施行一遍。
// server/index.jsimportexpressfromexpress;import{render}from./utils;constapp = express();app.use(express.static(public));//留意那里要换成*来婚配app.get(*,function(req, res){res.send(render(req));});app.listen(3001,()=>{console.log(listen:3001)});复造代码//server/utils.jsimportRoutesfrom../Routesimport{ renderToString }fromreact-dom/server;//重如果要用到StaticRouterimport{ StaticRouter }fromreact-router-dom;importReactfromreactexportconst render =(req)=>{//构建办事端的路由const content = renderToString({Routes});return`<head><title>ssrtitle>head><body><divid="root">${content}div><scriptsrc="/index.js">script>body>html>`}复造代码如今路由的跳转就没有任何问题啦。 留意,那里仅仅是一级路由的跳转,多级路由的衬着在之后的系列中会用react-router-config中renderRoutes来处置。
part3: 同构项目中引入Redux那一节次要是讲述Redux若何被引入到同构项目中以及此中需要留意的问题。
从头回忆一下redux的运做流程:
再回忆一下同构的概念,即在React代码客户端和办事器端各自运行一遍。
一、创建全局store如今起头创建store。 在项目根目次的store文件夹(总的store)下:
import{createStore, applyMiddleware, combineReducers}fromredux;importthunkfromredux-thunk;import{ reducerashomeReducer }from../containers/Home/store;//合并项目组件中store的reducerconstreducer = combineReducers({home: homeReducer})//创建store,并引入中间件thunk停止异步操做的办理conststore = createStore(reducer, applyMiddleware(thunk));//导出创建的storeexportdefaultstore复造代码二、组件内action和reducer的构建Home文件夹下的工程文件构造如下:
在Home的store目次下的各个文件代码示例:
//constants.jsexportconstCHANGE_LIST =HOME/CHANGE_LIST;复造代码//actions.jsimportaxiosfromaxios;import{ CHANGE_LIST }from"./constants";//通俗actionconstchangeList =list=>({type: CHANGE_LIST,list});//异步操做的action(接纳thunk中间件)exportconstgetHomeList =()=>{return(dispatch) =>{returnaxios.get(xxx).then((res) =>{constlist = res.data.data;console.log(list)dispatch(changeList(list))});};}复造代码//reducer.jsimport{ CHANGE_LIST }from"./constants";constdefaultState = {name:sanyuan,list: []}exportdefault(state = defaultState, action) => {switch(action.type) {default:returnstate;}}复造代码//index.jsimportreducerfrom"./reducer";//那么做是为了导出reducer让全局的store来停止合并//那么在全局的store下的index.js中只需引入Home/store而不需要Home/store/reducer.js//因为脚手架会主动识别文件夹下的index文件export{reducer}复造代码三、组件毗连全局store下面是Home组件的编写示例。
importReact, { Component }fromreact;import{ connect }fromreact-redux;import{ getHomeList }from./store/actionsclassHomeextendsComponent{render() {const{ list } =this.propsreturnlist.map(item=><divkey={item.id}>{item.title}div>)}}constmapStateToProps =state=>({list: state.home.newsList,})constmapDispatchToProps =dispatch=>({getHomeList() {dispatch(getHomeList());}})//毗连storeexportdefaultconnect(mapStateToProps, mapDispatchToProps)(Home);复造代码关于store的毗连操做,在同构项目平分两个部门,一个是与客户端store的毗连,另一部门是与办事端store的毗连。都是通过react-redux中的Provider来传递store的。
客户端:
//src/client/index.jsimportReactfromreact;importReactDomfromreact-dom;import{BrowserRouter, Route}fromreact-router-dom;import{ Provider }fromreact-redux;importstorefrom../storeimportroutesfrom../routes.jsconstApp =()=>{return(<Providerstore={store}><BrowserRouter>{routes}BrowserRouter>Provider>)}ReactDom.hydrate(<App/>, document.getElementById(root))复造代码办事端:
//src/server/index.js的内容连结稳定//下面是src/server/utils.jsimportRoutesfrom../Routesimport{ renderToString }fromreact-dom/server;import{ StaticRouter }fromreact-router-dom;import{ Provider }fromreact-redux;importReactfromreactexportconst render =(req)=>{const content = renderToString({Routes});return`<head><title>ssrtitle>head><body><divid="root">${content}div><scriptsrc="/index.js">script>body>html>`}复造代码四、潜在的坑其实上面如许的store创建体例是存在问题的,什么原因呢?
上面的store是一个单例,当那个单例导进来后,所有的用户用的是统一份store,那是不该该的。那么那么解那个问题呢?
在全局的store/index.js下修改如下:
//导出部门修改exportdefault()=> {returncreateStore(reducer, applyMiddleware(thunk))}复造代码如许在客户端和办事端的js文件引入时其实引入了一个函数,把那个函数施行就会拿到一个新的store,如许就能包管每个用户拜候时都是用的一份新的store。
part4: 异步数据的办事端衬着计划(数据灌水与脱水)一、问题引入在平常客户端的React开发中,我们一般在组件的componentDidMount生命周期函数停止异步数据的获取。但是,在办事端衬着中却呈现了问题。
如今我在componentDidMount钩子函数中停止Ajax恳求:
import{ getHomeList }from./store/actions//......componentDidMount() {this.props.getList();}//......constmapDispatchToProps =dispatch=>({getList() {dispatch(getHomeList());}})复造代码//actions.jsimport{ CHANGE_LIST }from"./constants";importaxiosfromaxiosconstchangeList =list=>({type: CHANGE_LIST,list})exportconstgetHomeList =()=>{returndispatch=>{//别的起的当地的后端办事returnaxiosInstance.get(localhost:4000/api/news.json).then((res) =>{constlist = res.data.data;dispatch(changeList(list))})}}//reducer.jsimport{ CHANGE_LIST }from"./constants";constdefaultState = {name:sanyuan,list: []}exportdefault(state = defaultState, action) => {switch(action.type) {caseCHANGE_LIST:constnewState = {...state,list: action.list}returnnewStatedefault:returnstate;}}复造代码好,如今启动办事。
如今页面可以一般衬着,但是翻开网页源代码。
源代码里面并没有那些列表数据啊!那那是为什么呢?
让我们来阐发一下客户端和办事端的运行流程,当阅读器发送恳求时,办事器承受到恳求,那时候办事器和客户端的store都是空的,紧接着客户端施行componentDidMount生命周期中的函数,获取到数据并衬着到页面,然而办事器端始末不会施行componentDidMount,因而不会拿到数据,那也招致办事器端的store始末是空的。换而言之,关于异步数据的操做始末只是客户端衬着。
如今的工做就是让办事端将获得数据的操做施行一遍,以到达实正的办事端衬着的效果。
二、革新路由在完成那个计划之前需要革新一下原有的路由,也就是routes.js
importHomefrom./containers/Home;importLoginfrom./containers/Login;exportdefault[{path:"/",component: Home,exact:true,loadData: Home.loadData,//办事端获取异步数据的函数key:home},{path:/login,component: Login,exact:true,key:login}}];复造代码此时客户端和办事端中编写的JSX代码也发作了响应变革
//客户端//以下的routes变量均指routes.js导出的数组<BrowserRouter><div>{routers.map(route => {<Route{...route} />})}div>BrowserRouter>Provider>复造代码//办事端<Providerstore={store}><StaticRouter><div>{routers.map(route => {<Route{...route} />})}div>StaticRouter>Provider>复造代码此中设置装备摆设了一个loadData参数,那个参数代表了办事端获取数据的函数。每次衬着一个组件获取异步数据时,城市挪用响应组件的那个函数。因而,在编写那个函数详细的代码之前,我们有需要想清晰若何来针对差别的路由来婚配差别的loadData函数。
在server/utils.js中参加以下逻辑
import{ matchRoutes }fromreact-router-config;//挪用matchRoutes用来婚配当前路由(撑持多级路由)constmatchedRoutes = matchRoutes(routes, req.path)//promise对象数组constpromises = [];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/actionsHome.loadData =(store)=>{returnstore.dispatch(getHomeList())}复造代码//actions.jsexportconstgetHomeList =()=>{returndispatch=>{returnaxios.get(xxxx).then((res) =>{constlist = 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.jsimport{createStore, applyMiddleware, combineReducers}fromredux;importthunkfromredux-thunk;import{ reducerashomeReducer }from../containers/Home/store;constreducer = combineReducers({home: homeReducer})//办事端的store创建函数exportconstgetStore =()=>{returncreateStore(reducer, applyMiddleware(thunk));}//客户端的store创建函数exportconstgetClientStore =()=>{constdefaultState =window.context ?window.context.state : {};returncreateStore(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办事端constgetUrl =(server) =>{returnserver ?xxxx(后端接口地址):/api/sanyuan.json(node接口);}//那个server参数是Home组件里面传过来的,//在componentDidMount中挪用那个action时传入false,//在loadData函数中挪用时传入true, 那里就不贴组件代码了exportconstgetHomeList =(server) =>{returndispatch=>{returnaxios.get(getUrl(server)).then((res) =>{constlist = res.data.data;dispatch(changeList(list))})}}复造代码在server/index.js应拿到前端的恳求做转发,那里是间接用proxy形式来做,也能够用node零丁向后端发送一次HTTP恳求。
//增加如下代码importproxyfromexpress-http-proxy;//相当于拦截到了前端恳求地址中的/api部门,然后换成另一个地址app.use(/api, proxy(http://xxxxxx(办事端地址), {proxyReqPathResolver:function(req){return/api+req.url;}}));复造代码三、恳求代码优化其实恳求的代码仍是有优化的余地的,认真想想,上面的server参数其实是不消传递的。
如今我们操纵axios的instance和thunk里面的withExtraArgument来做一些封拆。
//新建server/request.jsimportaxiosfromaxiosconstinstance = axios.create({baseURL:http://xxxxxx(办事端地址)})exportdefaultinstance//新建client/request.jsimportaxiosfromaxiosconstinstance = axios.create({//即当前途径的node办事baseURL:/})exportdefaultinstance复造代码然后对全局下store的代码做一个微调:
import{createStore, applyMiddleware, combineReducers}fromredux;importthunkfromredux-thunk;import{ reducerashomeReducer }from../containers/Home/store;importclientAxiosfrom../client/request;importserverAxiosfrom../server/request;constreducer = combineReducers({home: homeReducer})exportconstgetStore =()=>{//让thunk中间件带上serverAxiosreturncreateStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));}exportconstgetClientStore =()=>{constdefaultState =window.context ?window.context.state : {};//让thunk中间件带上clientAxiosreturncreateStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));}复造代码如今Home组件中恳求数据的action无需传参,actions.js中的恳求代码如下:
exportconstgetHomeList =()=>{//返回函数中的默认第三个参数是withExtraArgument传进来的axios实例return(dispatch, getState, axiosInstance) =>{returnaxiosInstance.get(/api/sanyuan.json).then((res) =>{constlist = res.data.data;console.log(res)dispatch(changeList(list))})}}复造代码至此,代码优化就做的差不多了,那种代码封拆的技巧其实能够用在其他的项目傍边,其实仍是比力文雅的。
part6: 多级路由衬着(renderRoutes)如今将routes.js的内容改动如下:
importHomefrom./containers/Home;importLoginfrom./containers/Login;importAppfrom./App//那里呈现了多级路由exportdefault[{path:/,component: App,routes: [{path:"/",component: Home,exact:true,loadData: Home.loadData,key:home,},{path:/login,component: Login,exact:true,key:login,}]}]复造代码如今的需求是让页面公用一个Header组件,App组件编写如下:
importReactfromreact;importHeaderfrom./components/Header;constApp =(props) =>{console.log(props.route)return(<div><Header>Header>div>)}exportdefaultApp;复造代码关于多级路由的衬着,需要办事端和客户端各施行一次。 因而编写的JSX代码都应有所实现:
//routes是指routes.js中返回的数组//办事端:<StaticRouterlocation={req.path}><div>{renderRoutes(routes)}div>StaticRouter>Provider>//客户端:<Providerstore={getClientStore()}><BrowserRouter><div>{renderRoutes(routes)}div>BrowserRouter>Provider>复造代码那里都用到了renderRoutes办法,其实它的工做十分简单,就是按照url衬着一层路由的组件(那里衬着的是App组件),然后将下一层的路由通过props传给目前的App组件,依次轮回。
那么,在App组件就能通过props.route.routes拿到下一层路由停止衬着:
importReactfromreact;importHeaderfrom./components/Header;//增加renderRoutes办法import{ renderRoutes }fromreact-router-config;constApp =(props) =>{console.log(props.route)return(<div><Header>Header>{renderRoutes(props.route.routes)}div>)}exportdefaultApp;复造代码至此,多级路由的衬着就完成啦。
part7: CSS的办事端衬着思绪(context钩子变量)一、客户端项目中引入CSS仍是以Home组件为例
//Home/style.cssbody{background: gray;}复造代码如今,在Home组件代码中引入:
importstylesfrom./style.css;复造代码要晓得如许的引入CSS代码的体例在一般情况下是运行不起来的,需要在webpack中做响应的设置装备摆设。 起首安拆响应的插件。
npminstallstyle-loader css-loader--D复造代码//webpack.client.jsconstpath =require(path);constmerge =require(webpack-merge);constconfig =require(./webpack.base);constclientConfig = {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的插件,
npminstall-D isomorphic-style-loader复造代码然后再webpack.server.js中做好响应的css设置装备摆设:
//webpack.server.jsconstpath =require(path);constnodeExternals =require(webpack-node-externals);constmerge =require(webpack-merge);constconfig =require(./webpack.base);constserverConfig = {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);复造代码它做了些什么工作?
再看看那行代码:
importstylesfrom./style.css;复造代码引入css文件时,那个isomorphic-style-loader帮我们在styles中挂了三个函数。输出styles看看:
如今我们的目的是拿到CSS代码,间接通过styles._getCss即可获得。
那我们拿到CSS代码后放到哪里呢?其实react-router-dom中的StaticRouter中已经帮我们筹办了一个钩子变量context。如下
//context从外界传入<StaticRouterlocation={req.path}context={context}><div>{renderRoutes(routes)}div>StaticRouter>复造代码那就意味着在路由设置装备摆设对象routes中的组件都能在办事端衬着的过程中拿到那个context,并且那个context关于组件来说,就相当于组件中的props.staticContext。而且,那个props.staticContext只会在办事端衬着的过程中存在,而客户端衬着的时候不会被定义。那就让我们可以通过那个变量来区分两种衬着情况啦。
如今,我们需要在办事端的render函数施行之前,初始化context变量的值:
letcontext = { css: [] }复造代码我们只需要在组件的componentWillMount生命周期中编写响应的逻辑即可:
componentWillMount() {//判断能否为办事端衬着情况if(this.props.staticContext) {this.props.staticContext.css.push(styles._getCss())}}复造代码办事端的renderToString施行完成后,context的CSS如今已经是一个有内容的数组,让我们来获取此中的CSS代码:
//拼接代码constcssStr = context.css.length ? context.css.join(\n) :;复造代码如今挂载到页面:
//放到返回的html字符串里的header里面<style>${cssStr}style>复造代码网页源代码中看到了CSS代码,效果也没有问题。CSS衬着完成!
三、操纵高阶组件优化代码也许你已经发现,关于每一个含有款式的组件,都需要在componentWillMount生命周期中施行完全不异的逻辑,关于那些逻辑我们能否可以把它封拆起来,不消频频呈现呢?
其实是能够实现的。操纵高阶组件就能够完成:
//根目次下创建withStyle.js文件importReact, { Component } fromreact;//函数返回组件//需要传入的第一个参数是需要粉饰的组件//第二个参数是styles对象exportdefault(DecoratedComponent, styles) => {returnclassNewComponentextendsComponent{componentWillMount() {//判断能否为办事端衬着过程if(this.props.staticContext) {this.props.staticContext.css.push(styles._getCss())}}render() {returntitleHelmetdivFragmentdivStaticRouterProvider
评论列表