이번에는 React의 서버 렌더링 변환에 대해 알려드리겠습니다. React의 서버 렌더링 변환 시 주의사항은 무엇인가요? 다음은 실제 사례입니다.
웹 페이지 SEO의 필요성으로 인해 이전 React 프로젝트를 서버 측 렌더링으로 전환하고 싶었고 몇 가지 조사와 연구 끝에 많은 인터넷 정보를 참고했습니다. 함정을 성공적으로 밟았습니다.
선택 아이디어: 서버 사이드 렌더링을 구현하려면 최신 버전의 React를 사용하고 싶고, 처음부터 서버 사이드에서 렌더링할 계획이라면 기존 작성 방법을 크게 변경하지 않는 것이 좋습니다. NEXT 프레임워크를 사용하여 직접 작성하는 것이 좋습니다
프로젝트 주소: https://github.com/wlx200510/react_koa_ssr
스캐폴딩 선택: webpack3.11.0 + React Router4 + Redux + koa2 + React16 + Node8.x
주요 경험: React 관련 지식을 익히고 자신의 지식 확장에 성공합니다. 기술 분야에서는 실제 프로젝트에서 서버 측 기술에 대한 경험을 축적했습니다
참고: 프레임워크를 사용하기 전에 현재 웹팩 버전이 최신 버전인지 확인하세요. 3.x이고 노드는 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处理
프로젝트 구축 아이디어
로컬 개발은 webpack-dev-server를 사용하여 핫 업데이트를 달성합니다. 기본 프로세스는 이전 반응 개발과 유사하지만 여전히 브라우저 측면 렌더링을 사용하므로 코드를 작성할 때 하나의 로직 세트와 두 개의 렌더링 환경이 필요합니다. 고려됩니다.
프런트 엔드 페이지 렌더링이 완료된 후에는 라우터 점프가 서버에 요청을 하지 않으므로 서버에 대한 부담이 줄어듭니다. 따라서 페이지에 들어가는 방법도 두 가지가 있습니다. 두 렌더링 환경의 라우팅 동형성 문제.
프로덕션 환경은 온디맨드 로딩을 구현하고, 서버 측에서 데이터를 얻고, 전체 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;
查看app目录下的createApp.js里面的代码可以发现,本框架是针对不同的工作环境做了不同的处理,只有在生产环境下才利用Loadable.Capture方法实现了懒加载,动态引入不同页面对应的打包之后的js文件。到这里还要看一下组件里面的路由配置文件的写法,以home页面下的index.js为例。注意/* webpackChunkName: 'Home' */这串字符,实质是指定了打包后此页面对应的js文件名,所以针对不同的页面,这个注释也需要修改,避免打包到一起。loading这个配置项只会在开发环境生效,当页面加载未完成前显示,这个实际项目开发如果不需要可以删除此组件。
import {homeThunk} from '../../store/actions/thunk'; const LoadableHome = Loadable({ loader: () =>import(/* webpackChunkName: 'Home' */'./containers/homeContainer.js'), loading: Loading, }); const HomeRouter = { path: '/', exact: true, component: LoadableHome, thunk: homeThunk // 服务端渲染会开启并执行这个action,用于获取页面渲染所需数据 } export default HomeRouter
这里多说一句,有时我们要改造的项目的页面文件里有从window.location里面获取参数的代码,改造成服务端渲染时要全部去掉,或者是要在render之后的生命周期中使用。并且页面级别组件都已经注入了相关路由信息,可以通过this.props.location来获取URL里面的参数。本项目用的是BrowserRouter,如果用HashRouter则包含参数可能略有不同,根据实际情况取用。
根据React16的服务端渲染的API介绍:
浏览器端使用的注入ConnectedRouter中的history为:import createHistory from 'history/createBrowserHistory'
服务器端使用的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 'history/createMemoryHistory'; import { routerReducer, routerMiddleware } from 'react-router-redux' import rootReducer from '../store/reducers/index.js'; const routerReducers=routerMiddleware(createHistory());//路由 const composeEnhancers = process.env.NODE_ENV=='development'?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 '../constants' 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('footer'.includes('foo')) await new Promise(resolve=>{ let homeInfo={name:'wd2010',age:'25',id:sendId} console.log('-----------请求getHomeInfo') 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页面为例的渲染流程
为了方便大家理解,我以一个页面为例整理了一下数据流的整体过程,看一下思路:
서버는 요청을 받고 /home
을 통해 해당 경로 구성을 찾습니다. 경로에 썽크 메소드가 있는지 확인한 후 store/actions/thunk.js
에서 노출된 함수를 실행합니다. 비동기적으로 데이터가 전역 상태에 주입됩니다. 이때 디스패치 배포는 실제로 적용되지 않습니다
출력될 HTML 코드는 데이터를 가져온 후 전역 상태를 전역 변수 window.__INITIAL_STATE__에 initState로 넣습니다.
window.__INITIAL_STATE__는 React 수명주기가 적용되기 전에 전역 상태로 병합됩니다. 이때 React는 DOM이 생성되었음을 발견하고 렌더링이 다시 트리거되지 않으며 데이터 상태가 동기화됩니다
server direct out HTML
의 기본 프로세스를 소개했는데, 일부 Reducer의 기능적 작성과 액션의 위치에 대해서는 인터넷상의 일부 분석을 참조하여 정리했습니다. . 이것이 자신의 이해와 일치하고 팀 개발에 도움이 되는 한 괜찮습니다. 글의 서두에서 설정한 독자 배경을 만난다면, 이 글의 설명만으로도 여러분의 서버사이드 렌더링 기술을 조명하기에 충분하다고 생각합니다. React에 대해 많이 모르더라도 상관 없습니다. 여기를 참조하여 React에 대한 기본 지식을 보충할 수 있습니다.
이 기사의 사례를 읽은 후 방법을 마스터했다고 생각합니다. PHP 중국어 웹사이트의 다른 관련 기사에 주목하세요!
추천 도서:
위 내용은 React는 서버 측에서 렌더링 변환을 수행합니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!