ホームページ  >  記事  >  ウェブフロントエンド  >  Reactプロジェクトのサーバーサイドレンダリング変換の詳細説明

Reactプロジェクトのサーバーサイドレンダリング変換の詳細説明

小云云
小云云オリジナル
2018-03-20 09:10:382612ブラウズ

Web ページの 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関連の知識を深め、実際のプロジェクトで技術分野を拡大し、サーバーサイド技術の経験を蓄積することに成功しました

注: フレームワークを使用する前に、現在のWebpackのバージョンが3.xであり、 Node は 8.x 以降です。読者は React を 3 か月以上使用し、実際の 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 つのレンダリングを考慮する必要があります。環境。

  2. フロントエンドページのレンダリングが完了すると、ルータージャンプはサーバーにリクエストを行わなくなり、サーバーへの負荷が軽減されます。したがって、ページに入る方法も2つあります。 2 つのレンダリング環境におけるルーティングの同型性が問題です。

  3. 実稼働環境では、オンデマンド読み込みを実装し、サーバー側でデータを取得し、HTML 全体をレンダリングするためにバックエンドサーバーとして koa を使用する必要があります。React16 の最新機能を使用して状態ツリー全体をマージします。サーバー側のレンダリングを実現します。

ローカル開発の概要

ローカル開発の表示に関係する主なファイルは、現在の実行環境を決定するための src ディレクトリ内のindex.js ファイルです。 module.hot の API は開発環境でのみ使用されます。 Reducer が変更されたときに現在のページのレンダリング更新通知を実現するために使用されます。 これは、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 と組み合わせると、この関数はすべてのスクリプトがロードされた後にトリガーされることがわかります。ページについて ルーティング設定と組み合わせてページを個別にパッケージ化する方法について説明します。アプリ ファイルの下で公開される 3 つのメソッドは、ブラウザ側とサーバー側で共通であることに注意してください。以下では主にこの部分のアイデアについて説明します。

ルート処理

次に、src/app ディレクトリ内の次のファイルを見てください。index.js は、主にサーバー側とブラウザー側の両方の開発で使用される 3 つのメソッドを公開します。ルーター ファイルのコードの考え方と、createApp.js ファイルのルーティングの処理について説明します。これは、両端のルート間の相互通信を実現するための重要なポイントです。 router フォルダーにある

routes.js は、各ページのルーティング設定をインポートし、設定配列を合成することで、オンライン ページとオフライン ページを柔軟に制御できます。同じディレクトリ内の Index.js は、RouterV4 を記述する標準的な方法です。 ConnectRouter は、ルーターをマージするために使用されるコンポーネントであり、パラメーターとして渡す必要があることに注意してください。 createApp.js ファイルに含める必要があります。別の処理を実行します。 Route コンポーネントのいくつかの構成項目を簡単に見てみましょう。注目に値するのは、バックエンドがデータを取得した後のレンダリングを実現するための重要なステップである、Next と同様のコンポーネントがデータを取得できるようにする属性です。ライフサイクルフックとその他のプロパティについては、関連する 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

一部のReducerの関数の書き方やアクションの位置については、人によって意見が異なるのでまとめています。自分自身の理解とチームの開発に役立つものであれば、問題ありません。この記事の冒頭で私が設定した読者の背景を満たしているのであれば、この記事の説明は独自のサーバーサイド レンダリング テクノロジを理解するのに十分であると思います。 React についてあまり知らなくても大丈夫です。React の基本的な知識を補うためにここを参照してください。

関連する推奨事項:

React サーバー側レンダリングの例の詳細な説明

webpack+react+nodejs サーバー側レンダリング_html/css_WEB-ITnose

Vue.js および ASP.NET Core サーバー側レンダリング関数

以上がReactプロジェクトのサーバーサイドレンダリング変換の詳細説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。