今回は React プロジェクトのサーバーサイドレンダリングの最適化についてお届けします。 React プロジェクトのサーバーサイドレンダリングの最適化に関する注意事項 について、実際のケースを見てみましょう。
Web ページの SEO の必要性のため、調査と研究を行った後、以前の React プロジェクトをサーバーサイド レンダリングに変換したいと考えました。罠を踏むことに成功した。
選択の考え方: サーバーサイドレンダリングを実装するには、Reactの最新バージョンを使用し、既存の記述方法に大きな変更を加えないでください。 最初からサーバーサイドでレンダリングするつもりであれば、それはです。 NEXT フレームワークを使用して直接記述することをお勧めします
プロジェクトアドレス: https://github.com/wlx200510/react_koa_ssr
スキャフォールディングの選択: webpack3.11.0 + React Router4 + Redux + koa2 + React16 + Node8.x
Main経験: React関連の知識を深め、拡張に成功しました 私自身の技術分野では、実際のプロジェクトでサーバーサイド技術の経験を積みました
注意: フレームワークを使用する前に、現在のWebpackバージョンが3.x およびノードは 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处理
。
ローカル開発では、webpack-dev-server を使用してホット アップデートを実現します。基本的なプロセスは以前の反応開発と似ていますが、ブラウザ側でレンダリングするため、コードを記述するときは、一連のロジックと 2 つのレンダリングを考慮する必要があります。環境。
フロントエンドページのレンダリングが完了すると、ルータージャンプはサーバーにリクエストを行わなくなり、サーバーへの負荷が軽減されます。したがって、ページに入る方法も2つあります。 2 つのレンダリング環境におけるルーティングの同型性が問題です。
実稼働環境では、オンデマンド読み込みを実装し、サーバー側でデータを取得し、HTML 全体をレンダリングするためにバックエンドサーバーとして koa を使用する必要があります。React16 の最新機能を使用して状態ツリー全体をマージします。サーバー側のレンダリングを実現します。
ローカル開発の概要
ローカル開発の表示に関係する主なファイルは、現在の実行環境を決定するための src ディレクトリ内のindex.js ファイルです。 module.hot の API は開発環境でのみ使用されます。 Reducer が変更されたときに現在のページのレンダリング更新通知を実装するために使用されます。 これは、v16 バージョンで特別に追加された新しい API メソッドであり、render メソッドをベースにして最大限の可能性を実現します。サーバーサイドコンテンツのレンダリングを再利用することで、静的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 つのメソッドを公開します。 router ファイルのコードの考え方と 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;
查看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
一部のReducerの関数記述とアクションの位置については、インターネット上の分析を元にまとめたものです。これに沿っている限り、さまざまな意見があります。自分自身の理解を深め、チーム開発に役立ててください。この記事の冒頭で私が設定した読者の背景を満たしているのであれば、この記事の説明は独自のサーバーサイド レンダリング テクノロジを理解するのに十分であると思います。 React についてあまり知らなくても問題ありません。React の基礎知識を補うためにここを参照してください。この記事の事例を読んだ後は、方法を習得したと思います。さらに興味深い情報については、お支払いください。 PHP 中国語 Web サイトの他の関連記事にも注目してください。
推奨読書:react-nativeパッケージプラグインswiperを使用する手順の詳細な説明
Nodejs mysqlデータベースに接続する手順の詳細な説明以上がReact プロジェクトのサーバー側レンダリングの最適化の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。