>  기사  >  웹 프론트엔드  >  React 프로젝트의 서버사이드 렌더링 변환에 대한 자세한 설명

React 프로젝트의 서버사이드 렌더링 변환에 대한 자세한 설명

小云云
小云云원래의
2018-03-20 09:10:382654검색

웹 페이지 SEO의 필요성으로 인해 이전 React 프로젝트를 서버 측 렌더링으로 전환하고 싶었고 몇 가지 조사와 연구 끝에 많은 인터넷 정보를 참고했습니다. 함정을 성공적으로 밟았습니다. 이번 글은 주로 React 프로젝트(koa2+webpack3.11)의 서버사이드 렌더링 변환에 대한 자세한 설명을 소개하는 글입니다. 편집자께서 꽤 괜찮다고 하셔서 지금 공유하고 참고용으로 올려드리겠습니다. . 편집자를 따라가서 모두에게 도움이 되기를 바랍니다.

선택 아이디어: 서버 사이드 렌더링을 구현하려면 최신 버전의 React를 사용하고 싶고, 처음부터 서버 사이드에서 렌더링할 계획이라면 기존 작성 방법을 크게 바꾸지 않는 것이 좋습니다. 프로젝트를 작성하려면 NEXT 프레임워크를 직접 사용하는 것이 좋습니다

주소: https://github.com/wlx200510/react_koa_ssr

스캐폴딩 선택: webpack3.11.0 + React Router4 + Redux + koa2 + React16 + Node8.x

주요 경험: React 관련 지식을 더 익히고, 실제 프로젝트에서 성공적으로 기술 분야를 확장하고 서버 측 기술에 대한 경험을 축적했습니다.

참고: 프레임워크를 사용하기 전에 현재 웹팩 버전이 3인지 확인하세요. .x 및 Node는 8.x 이상입니다. 독자는 3개월 이상 React를 사용하고 실제 React 프로젝트 경험이 있는 것이 가장 좋습니다

프로젝트 디렉토리 소개


├── assets
│ └── index.css //放置一些全局的资源文件 可以是js 图片等
├── config
│ ├── webpack.config.dev.js 开发环境webpack打包设置
│ └── webpack.config.prod.js 生产环境webpack打包设置
├── package.json
├── README.md
├── server server端渲染文件,如果对不是很了解,建议参考[koa教程](http://wlxadyl.cn/2018/02/11/koa-learn/)
│ ├── app.js
│ ├── clientRouter.js // 在此文件中包含了把服务端路由匹配到react路由的逻辑
│ ├── ignore.js
│ └── index.js
└── src
 ├── app 此文件夹下主要用于放置浏览器和服务端通用逻辑
 │ ├── configureStore.js //redux-thunk设置
 │ ├── createApp.js  //根据渲染环境不同来设置不同的router模式
 │ ├── index.js
 │ └── router
 │  ├── index.js
 │  └── routes.js  //路由配置文件! 重要
 ├── assets
 │ ├── css    放置一些公共的样式文件
 │ │ ├── _base.scss  //很多项目都会用到的初始化css
 │ │ ├── index.scss
 │ │ └── my.scss
 │ └── img
 ├── components    放置一些公共的组件
 │ ├── FloatDownloadBtn 公共组件样例写法
 │ │ ├── FloatDownloadBtn.js
 │ │ ├── FloatDownloadBtn.scss
 │ │ └── index.js
 │ ├── Loading.js
 │ └── Model.js   函数式组件的写法
 │
 ├── favicon.ico
 ├── index.ejs    //渲染的模板 如果项目需要,可以放一些公共文件进去
 ├── index.js    //包括热更新的逻辑
 ├── pages     页面组件文件夹
 │ ├── home
 │ │ ├── components  // 用于放置页面组件,主要逻辑
 │ │ │ └── homePage.js
 │ │ ├── containers  // 使用connect来封装出高阶组件 注入全局state数据
 │ │ │ └── homeContainer.js
 │ │ ├── index.js  // 页面路由配置文件 注意thunk属性
 │ │ └── reducer
 │ │  └── index.js // 页面的reducer 这里暴露出来给store统一处理 注意写法
 │ └── user
 │  ├── components
 │  │ └── userPage.js
 │  ├── containers
 │  │ └── userContainer.js
 │  └── index.js
 └── store
  ├── actions   // 各action存放地
  │ ├── home.js
  │ └── thunk.js
  ├── constants.js  // 各action名称汇集处 防止重名
  └── reducers
   └── index.js  // 引用各页面的所有reducer 在此处统一combine处理

프로젝트 구축 아이디어

  1. 핫 업데이트를 달성하려면 로컬 개발에 webpack-dev-server를 사용하세요. 기본 프로세스는 이전과 동일합니다. 개발은 유사하지만 여전히 브라우저 측 렌더링이므로 코드를 작성할 때 일련의 논리를 고려해야 합니다. 두 가지 렌더링 환경이 있습니다.

  2. 프런트 엔드 페이지 렌더링이 완료된 후에는 라우터 점프가 서버에 요청을 하지 않으므로 서버에 대한 부담이 줄어듭니다. 따라서 페이지에 들어가는 방법도 두 가지가 있습니다. 두 렌더링 환경의 라우팅 동형성 문제.

  3. 프로덕션 환경은 온디맨드 로딩을 ​​구현하고, 서버 측에서 데이터를 얻고, 전체 HTML을 렌더링하기 위해 Koa를 백엔드 서버로 사용해야 합니다. 전체 상태 트리를 병합하기 위해 React16의 최신 기능을 사용합니다. 서버 측 렌더링을 달성합니다.

로컬 개발 소개

로컬 개발 보기에 관련된 주요 파일은 현재 실행 환경을 결정하는 src 디렉터리의 index.js 파일입니다. 개발 환경에서만 module.hot의 API가 됩니다. 리듀서가 변경될 때 현재 페이지 렌더링 업데이트 알림을 달성하는 데 사용됩니다. 이는 v16 버전에서 서버 측 렌더링을 위해 특별히 추가된 새로운 API 메소드로, 가능한 최대치를 구현합니다. 서버 측 콘텐츠 렌더링 재사용은 정적 DOM에서 동적 NODES로의 프로세스를 실현합니다. 핵심은 v15 버전에서 체크섬 표시를 판단하는 프로세스를 대체하여 재사용 프로세스를 더욱 효율적이고 우아하게 만드는 것입니다.


const renderApp=()=>{
 let application=createApp({store,history});
 hydrate(application,document.getElementById('root'));
}
window.main = () => {
 Loadable.preloadReady().then(() => {
 renderApp()
 });
};

if(process.env.NODE_ENV==='development'){
 if(module.hot){
 module.hot.accept('./store/reducers/index.js',()=>{
  let newReducer=require('./store/reducers/index.js');
  store.replaceReducer(newReducer)
 })
 module.hot.accept('./app/index.js',()=>{
  let {createApp}=require('./app/index.js');
  let newReducer=require('./store/reducers/index.js');
  store.replaceReducer(newReducer)
  let application=createApp({store,history});
  hydrate(application,document.getElementById('root'));
 })
 }
}

window.main 함수 정의에 주의하세요. index.ejs와 결합하면 모든 스크립트가 로드된 후에 이 함수가 실행되는 것을 알 수 있습니다. 페이지를 개별적으로 패키징하는 방법은 라우팅 설정과 함께 설명해야 합니다. 앱 파일에 노출되는 세 가지 메소드는 브라우저 측과 서버 측에서 공통적으로 적용된다는 점에 유의해야 합니다. 다음은 주로 이 부분에 대한 아이디어입니다.

경로 처리

다음으로 src/app 디렉터리에 있는 다음 파일을 살펴보세요. index.js는 세 가지 메서드를 노출합니다. 이 부분은 주로 서버 측 및 브라우저 측 개발에 사용됩니다. 라우터 파일의 코드 아이디어와 createApp.js 파일의 라우팅 처리에 대해 설명합니다. 이것이 양쪽 경로 간의 상호 통신을 달성하는 핵심 포인트입니다.

routes.js는 라우터 폴더 아래에 있는 라우팅 구성 파일입니다. 각 페이지 아래의 라우팅 구성을 가져와 구성 배열을 합성합니다. 이 구성을 사용하면 온라인 및 오프라인 페이지를 유연하게 제어할 수 있습니다. 동일한 디렉터리에 있는 index.js는 RouterV4를 작성하는 표준 방법입니다. ConnectRouter는 라우터를 병합하는 데 사용되는 구성 요소이며 매개 변수로 전달되어야 합니다. createApp.js 파일에 있어야 합니다. Route 구성 요소의 여러 구성 항목을 간략하게 살펴보겠습니다. 주목할 만한 것은 썽크 속성입니다. 이는 백엔드가 데이터를 얻은 후 렌더링을 수행하는 핵심 단계입니다. 라이프사이클 후크 및 기타 속성은 관련 React-router 문서에서 찾을 수 있으며 여기서는 설명하지 않습니다.


import routesConfig from './routes';
const Routers=({history})=>(
 <ConnectedRouter history={history}>
 <p>
  {
  routesConfig.map(route=>(
   <Route key={route.path} exact={route.exact} path={route.path} component={route.component} thunk={route.thunk} />
  ))
  }
 </p>
 </ConnectedRouter>
)
export default Routers;

앱 디렉터리에 있는 createApp.js의 코드를 살펴보면 이 프레임워크가 작업 환경에 따라 다른 처리를 수행하는 것을 알 수 있습니다. 프로덕션 환경에서만 Loadable.Capture 메서드를 사용하여 지연 로딩을 구현합니다. . 다른 페이지에 해당하는 패키지된 js 파일을 동적으로 도입합니다. 이 시점에서는 홈 페이지 아래의 index.js를 예로 들어 컴포넌트에 라우팅 구성 파일을 작성하는 방법도 살펴봐야 합니다. /* webpackChunkName: 'Home' */ 이 문자열은 기본적으로 패키징 후 이 페이지에 해당하는 js 파일 이름을 지정하므로 다른 페이지의 경우 함께 패키징하지 않도록 이 주석도 수정해야 합니다. 로딩 구성 항목은 개발 환경에서만 적용되며 페이지가 로드되기 전에 표시됩니다. 이 구성 요소는 실제 프로젝트 개발에 필요하지 않은 경우 삭제할 수 있습니다.


import {homeThunk} from &#39;../../store/actions/thunk&#39;;

const LoadableHome = Loadable({
 loader: () =>import(/* webpackChunkName: &#39;Home&#39; */&#39;./containers/homeContainer.js&#39;),
 loading: Loading,
});

const HomeRouter = {
 path: &#39;/&#39;,
 exact: true,
 component: LoadableHome,
 thunk: homeThunk // 服务端渲染会开启并执行这个action,用于获取页面渲染所需数据
}
export default HomeRouter

这里多说一句,有时我们要改造的项目的页面文件里有从window.location里面获取参数的代码,改造成服务端渲染时要全部去掉,或者是要在render之后的生命周期中使用。并且页面级别组件都已经注入了相关路由信息,可以通过this.props.location来获取URL里面的参数。本项目用的是BrowserRouter,如果用HashRouter则包含参数可能略有不同,根据实际情况取用。

根据React16的服务端渲染的API介绍:

  1. 浏览器端使用的注入ConnectedRouter中的history为:import createHistory from 'history/createBrowserHistory'

  2. 服务器端使用的history为import createHistory from 'history/createMemoryHistory'

服务端渲染

这里就不会涉及到koa2的一些基础知识,如果对koa2框架不熟悉可以参考我的另外一篇博文。这里是看server文件夹下都是服务端的代码。首先是简洁的app.js用于保证每次连接都返回的是一个新的服务器端实例,这对于单线程的js语言是很关键的思路。需要重点介绍的就是clientRouter.js这个文件,结合/src/app/configureStore.js这个文件共同理解服务端渲染的数据获取流程和React的渲染机制。


/*configureStore.js*/
import {createStore, applyMiddleware,compose} from "redux";
import thunkMiddleware from "redux-thunk";
import createHistory from &#39;history/createMemoryHistory&#39;;
import { routerReducer, routerMiddleware } from &#39;react-router-redux&#39;
import rootReducer from &#39;../store/reducers/index.js&#39;;

const routerReducers=routerMiddleware(createHistory());//路由
const composeEnhancers = process.env.NODE_ENV==&#39;development&#39;?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;

const middleware=[thunkMiddleware,routerReducers]; //把路由注入到reducer,可以从reducer中直接获取路由信息

let configureStore=(initialState)=>createStore(rootReducer,initialState,composeEnhancers(applyMiddleware(...middleware)));

export default configureStore;

window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__这个变量是浏览器里面的Redux的开发者工具,开发React-redux应用时建议安装,否则会有报错提示。这里面大部分都是redux-thunk的示例代码,关于这部分如果看不懂建议看一下redux-thunk的官方文档,这里要注意的是configureStore这个方法要传入的initialState参数,这个渲染的具体思路是:在服务端判断路由的thunk方法,如果存在则需要执行这个获取数据逻辑,这是个阻塞过程,可以当作同步,获取后放到全局State中,在前端输出的HTML中注入window.__INITIAL_STATE__这个全局变量,当html载入完毕后,这个变量赋值已有数据的全局State作为initState提供给react应用,然后浏览器端的js加载完毕后会通过复用页面上已有的dom和初始的initState作为开始,合并到render后的生命周期中,从而在componentDidMount中已经可以从this.props中获取渲染所需数据。

但还要考虑到页面切换也有可能在前端执行跳转,此时作为React的应用不会触发对后端的请求,因此在componentDidMount这个生命周期里并没有获取数据,为了解决这个问题,我建议在这个生命周期中都调用props中传来的action触发函数,但在action内部进行一层逻辑判断,避免重复的请求,实际项目中请求数据往往会有个标识性ID,就可以将这个ID存入store中,然后就可以进行一次对比校验来提前返回,避免重复发送ajax请求,具体可看store/actions/home.js`中的逻辑处理。


import {ADD,GET_HOME_INFO} from &#39;../constants&#39;
export const add=(count)=>({type: ADD, count,})

export const getHomeInfo=(sendId=1)=>async(dispatch,getState)=>{
 let {name,age,id}=getState().HomeReducer.homeInfo;
 if (id === sendId) {
 return //是通过对请求id和已有数据的标识性id进行对比校验,避免重复获取数据。
 }
 console.log(&#39;footer&#39;.includes(&#39;foo&#39;))
 await new Promise(resolve=>{
 let homeInfo={name:&#39;wd2010&#39;,age:&#39;25&#39;,id:sendId}
 console.log(&#39;-----------请求getHomeInfo&#39;)
 setTimeout(()=>resolve(homeInfo),1000)
 }).then(homeInfo=>{
 dispatch({type:GET_HOME_INFO,data:{homeInfo}})
 })
}

注意这里的async/await写法,这里涉及到服务端koa2使用这个来做数据请求,因此需要统一返回async函数,这块不熟的同学建议看下ES7的知识,主要是async如何配合Promise实现异步流程改造,并且如果涉及koa2的服务端工作,对async函数用的更多,这也是本项目要求Node版本为8.x以上的原因,从8开始就可以直接用这两个关键字。

不过到具体项目中,往往会涉及到一些服务端参数的注入问题,但这块根据不同项目需求差异很大,并且不属于这个React服务端改造的一部分,没法统一分享,如果真是公司项目要用到对这块有需求咨询可以打赏后加我微信讨论。

以Home页面为例的渲染流程

为了方便大家理解,我以一个页面为例整理了一下数据流的整体过程,看一下思路:

  1. 服务端接收到请求,通过/home找到对应的路由配置

  2. 判断路由存在thunk方法,此时执行store/actions/thunk.js里面的暴露出的函数

  3. 异步获取的数据会注入到全局state中,此时的dispatch分发其实并不生效

  4. 要输出的HTML代码中会将获取到数据后的全局state放到window.__INITIAL_STATE__这个全局变量中,作为initState

  5. window.__INITIAL_STATE__将在react生命周期起作用前合并入全局state,此时react发现dom已经生成,不会再次触发render,并且数据状态得到同步


服务端直出HTML

기본 프로세스는 일부 리듀서의 기능적 작성 방법과 작업 위치에 대해 인터넷의 일부 분석을 참조하여 정리되었습니다. 자신의 이해와 팀 발전에 도움이 된다면 괜찮을 것입니다. 글의 서두에서 설정한 독자 배경을 만난다면, 이 글의 설명만으로도 여러분의 서버사이드 렌더링 기술을 조명하기에 충분하다고 생각합니다. React에 대해 많이 모르더라도 상관없습니다. 여기를 참조하여 React에 대한 기본 지식을 보충할 수 있습니다.

관련 권장 사항:

React 서버 측 렌더링 예제에 대한 자세한 설명

webpack+react+nodejs 서버 측 렌더링_html/css_WEB-ITnose

Vue.js 및 ASP.NET Core 서버 측 렌더링 기능

위 내용은 React 프로젝트의 서버사이드 렌더링 변환에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.