一文はReact SSRサービス端のレンダリングと同構造の原理を徹底します
13027 ワード
前に書く
この間ずっとreact ssr
技術を研究して、それから1つの完全なssr
開発骨格を書きました.今日文章を書くのは、主に私の研究成果の精華の内容を整理して着地して、また再び整理することを通じてもっと最適化の場所を発見することを望んで、もっと多くの人に少し穴を踏まないで、多くの人にこの技術を理解して掌握させることを望んでいます.
本文(前提はあなたの食欲に対して、同じく比較的に良い消化吸収)を見たことがあると信じて、あなたはきっと react ssr
サービス端のレンダリング技術に対して1つの深い理解があって、自分の足場を作ることができて、更に自分の実際のプロジェクトを改造することができて、もちろんこれはreact
に限らず、その他のフレームワークはすべて同じで、結局原理はすべて似ています.
なぜサービス側レンダリング(ssr)
なぜ実務的にレンダリングするのかについては、みんなが聞いたことがあると信じています.そして、誰もがいくつかのことを言うことができます.
ヘッドスクリーン待ち
SPAモードでは、すべてのデータ要求とDomレンダリングがブラウザ側で完了するため、私たちが初めてページにアクセスしたときに「白画面」が待機する可能性が高いが、サービス側がすべてのデータ要求とhtmlコンテンツをレンダリングするのはサービス側で処理が完了し、ブラウザが受け取ったのは完全なhtmlコンテンツで、レンダリング内容をより速く見ることができる.サービス側でデータ要求を完了することは、ブラウザ側よりも効率的であるに違いありません.
SEOの気持ちなんて考えてない
一部のサイトのトラフィックソースは主に検索エンジンに頼っているので、サイトのSEOはまだ重要ですが、SPAモデルは検索エンジンに友好的ではありません.この問題を徹底的に解決するには、サービス端を採用するしかありません.他人を変えることはできず、自分を変えるしかない.
SSR+SPA体験アップグレード
SSR
を実現するだけでは意味がなく、技術的には何の発展も進歩もありません.そうしないと、SPA
技術は現れません.
しかし、単純なSPA
は完璧ではないので、最高の案はこの2つの体験と技術の結合であり、最初のアクセスページはサービス側レンダリングであり、最初のアクセスの後続のインタラクションに基づいてSPA
の効果と体験であり、SEO
の効果に影響を与えないので、これは少し完璧です.
単純にssr
を実現するのは簡単で、結局これは伝統的な技術で、言語を問わず、php、jsp、asp、nodeなどを勝手に使って実現することができます.
しかし、2つの技術の結合を実現し、コード(同構造)を最大限に再利用し、開発メンテナンスコストを削減するには、react
またはvue
などのフロントエンドフレームワークを組み合わせたnode (ssr)
を採用する必要がある.
本文は主にReact SSR
と言いますが、もちろんvue
も同じですが、技術スタックが違うだけです.
コア原理
全体的にreact
サービス側レンダリングの原理は複雑ではなく、その中で最も核心的な内容は同構造である.node server
クライアント要求を受信し、現在のreq url path
を取得した後、既存のルーティングテーブル内で対応するコンポーネントを検索し、要求するデータを取得し、props
、context
またはstore
の形式でコンポーネントにデータを転送し、react
に内蔵されたサービス側レンダリングapi renderToString() or renderToNodeStream()
に基づいてコンポーネントをhtml
またはstream
にレンダリングし、最終的なhtml
を出力する前にブラウザ端(注水)にデータを注入する必要があり、server出力(response)後にブラウザ端はデータ(脱水)を得ることができ、ブラウザはレンダリングとノード比較を開始し、その後、コンポーネントのcomponentDidMount
を実行してコンポーネント内のイベントバインドといくつかのインタラクションを完了し、ブラウザはサービス端出力のhtml
を再利用し、プロセス全体が終了する.
技術点は確かに少なくありませんが、アーキテクチャとエンジニアリングの面では、各知識点をリンクし、統合する必要があります.
ここにアーキテクチャ図を置きます
react ssr
ejsから
ssrを実現するのは簡単で、まずnode ejs
の栗を見ます.// index.html
react ssr
//node ssr
const ejs = require('ejs');
const http = require('http');
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, {
'Content-Type': 'text/html'
});
// index.ejs
ejs.renderFile('./views/index.ejs', {
title: 'react ssr',
data: ' '},
(err, data) => {
if (err ) {
console.log(err);
} else {
res.end(data);
}
})
}
}).listen(8080);
jsxから文字列
ejs
と組み合わせて、クライアントに直接出力するサービス側レンダリングの出力を実現しました.
以上を参考に、react
と組み合わせてサービス側レンダリングの直出を実現し、jsx
をejs
に置き換え、以前はhtmlでejs
を使用してデータをバインドしていたが、現在はjsx
を使用してデータをバインドするように書き換えられ、react内蔵apiを使用してコンポーネントをhtml文字列にレンダリングするなど、他に差はない.
なぜreactコンポーネントはhtml文字列に変換できるのですか?
簡単に言えば、私たちが書いたjsxはhtml(実際にはオブジェクト)のラベルを書いているように見えますが、実はコンパイルされてReact.createElement
メソッドに変換され、最終的には1つのオブジェクト(仮想DOM)に変換され、プラットフォームとは関係なく、このオブジェクトがあれば、何に変換したいのかは気持ち次第です.const React = require('react');
const { renderToString} = require( 'react-dom/server');
const http = require('http');
//
class Index extends React.Component{
constructor(props){
super(props);
}
render(){
return {this.props.data.title}
}
}
//
const fetch = function () {
return {
title:'react ssr',
data:[]
}
}
//
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, {
'Content-Type': 'text/html'
});
const data = fetch();
const html = renderToString();
res.end(html);
}
}).listen(8080);
ps:以上のコードは直接実行できません.babelと組み合わせて@babel/preset-reactを使用して変換する必要があります.
npx babel script.js --out-file script-compiled.js --presets=@babel/preset-react
問題を引き起こす.
上で非常に簡単なのはreact ssr
を実現し、jsx
をテンプレートエンジンとして、上の小さなコードを軽視しないでください.彼は私たちに一連の問題を引き出すことができます.これもreact ssr
を完全に実現する礎です.
なぜ実務的にレンダリングするのかについては、みんなが聞いたことがあると信じています.そして、誰もがいくつかのことを言うことができます.
ヘッドスクリーン待ち
SPAモードでは、すべてのデータ要求とDomレンダリングがブラウザ側で完了するため、私たちが初めてページにアクセスしたときに「白画面」が待機する可能性が高いが、サービス側がすべてのデータ要求とhtmlコンテンツをレンダリングするのはサービス側で処理が完了し、ブラウザが受け取ったのは完全なhtmlコンテンツで、レンダリング内容をより速く見ることができる.サービス側でデータ要求を完了することは、ブラウザ側よりも効率的であるに違いありません.
SEOの気持ちなんて考えてない
一部のサイトのトラフィックソースは主に検索エンジンに頼っているので、サイトのSEOはまだ重要ですが、SPAモデルは検索エンジンに友好的ではありません.この問題を徹底的に解決するには、サービス端を採用するしかありません.他人を変えることはできず、自分を変えるしかない.
SSR+SPA体験アップグレード
SSR
を実現するだけでは意味がなく、技術的には何の発展も進歩もありません.そうしないと、SPA
技術は現れません.しかし、単純な
SPA
は完璧ではないので、最高の案はこの2つの体験と技術の結合であり、最初のアクセスページはサービス側レンダリングであり、最初のアクセスの後続のインタラクションに基づいてSPA
の効果と体験であり、SEO
の効果に影響を与えないので、これは少し完璧です.単純に
ssr
を実現するのは簡単で、結局これは伝統的な技術で、言語を問わず、php、jsp、asp、nodeなどを勝手に使って実現することができます.しかし、2つの技術の結合を実現し、コード(同構造)を最大限に再利用し、開発メンテナンスコストを削減するには、
react
またはvue
などのフロントエンドフレームワークを組み合わせたnode (ssr)
を採用する必要がある.本文は主に
React SSR
と言いますが、もちろんvue
も同じですが、技術スタックが違うだけです.コア原理
全体的にreact
サービス側レンダリングの原理は複雑ではなく、その中で最も核心的な内容は同構造である.node server
クライアント要求を受信し、現在のreq url path
を取得した後、既存のルーティングテーブル内で対応するコンポーネントを検索し、要求するデータを取得し、props
、context
またはstore
の形式でコンポーネントにデータを転送し、react
に内蔵されたサービス側レンダリングapi renderToString() or renderToNodeStream()
に基づいてコンポーネントをhtml
またはstream
にレンダリングし、最終的なhtml
を出力する前にブラウザ端(注水)にデータを注入する必要があり、server出力(response)後にブラウザ端はデータ(脱水)を得ることができ、ブラウザはレンダリングとノード比較を開始し、その後、コンポーネントのcomponentDidMount
を実行してコンポーネント内のイベントバインドといくつかのインタラクションを完了し、ブラウザはサービス端出力のhtml
を再利用し、プロセス全体が終了する.
技術点は確かに少なくありませんが、アーキテクチャとエンジニアリングの面では、各知識点をリンクし、統合する必要があります.
ここにアーキテクチャ図を置きます
react ssr
ejsから
ssrを実現するのは簡単で、まずnode ejs
の栗を見ます.// index.html
react ssr
//node ssr
const ejs = require('ejs');
const http = require('http');
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, {
'Content-Type': 'text/html'
});
// index.ejs
ejs.renderFile('./views/index.ejs', {
title: 'react ssr',
data: ' '},
(err, data) => {
if (err ) {
console.log(err);
} else {
res.end(data);
}
})
}
}).listen(8080);
jsxから文字列
ejs
と組み合わせて、クライアントに直接出力するサービス側レンダリングの出力を実現しました.
以上を参考に、react
と組み合わせてサービス側レンダリングの直出を実現し、jsx
をejs
に置き換え、以前はhtmlでejs
を使用してデータをバインドしていたが、現在はjsx
を使用してデータをバインドするように書き換えられ、react内蔵apiを使用してコンポーネントをhtml文字列にレンダリングするなど、他に差はない.
なぜreactコンポーネントはhtml文字列に変換できるのですか?
簡単に言えば、私たちが書いたjsxはhtml(実際にはオブジェクト)のラベルを書いているように見えますが、実はコンパイルされてReact.createElement
メソッドに変換され、最終的には1つのオブジェクト(仮想DOM)に変換され、プラットフォームとは関係なく、このオブジェクトがあれば、何に変換したいのかは気持ち次第です.const React = require('react');
const { renderToString} = require( 'react-dom/server');
const http = require('http');
//
class Index extends React.Component{
constructor(props){
super(props);
}
render(){
return {this.props.data.title}
}
}
//
const fetch = function () {
return {
title:'react ssr',
data:[]
}
}
//
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, {
'Content-Type': 'text/html'
});
const data = fetch();
const html = renderToString();
res.end(html);
}
}).listen(8080);
ps:以上のコードは直接実行できません.babelと組み合わせて@babel/preset-reactを使用して変換する必要があります.
npx babel script.js --out-file script-compiled.js --presets=@babel/preset-react
問題を引き起こす.
上で非常に簡単なのはreact ssr
を実現し、jsx
をテンプレートエンジンとして、上の小さなコードを軽視しないでください.彼は私たちに一連の問題を引き出すことができます.これもreact ssr
を完全に実現する礎です.
ejsから
ssrを実現するのは簡単で、まず
node ejs
の栗を見ます.// index.html
react ssr
//node ssr
const ejs = require('ejs');
const http = require('http');
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, {
'Content-Type': 'text/html'
});
// index.ejs
ejs.renderFile('./views/index.ejs', {
title: 'react ssr',
data: ' '},
(err, data) => {
if (err ) {
console.log(err);
} else {
res.end(data);
}
})
}
}).listen(8080);
jsxから文字列
ejs
と組み合わせて、クライアントに直接出力するサービス側レンダリングの出力を実現しました.以上を参考に、
react
と組み合わせてサービス側レンダリングの直出を実現し、jsx
をejs
に置き換え、以前はhtmlでejs
を使用してデータをバインドしていたが、現在はjsx
を使用してデータをバインドするように書き換えられ、react内蔵apiを使用してコンポーネントをhtml文字列にレンダリングするなど、他に差はない.なぜreactコンポーネントはhtml文字列に変換できるのですか?
簡単に言えば、私たちが書いたjsxはhtml(実際にはオブジェクト)のラベルを書いているように見えますが、実はコンパイルされて
React.createElement
メソッドに変換され、最終的には1つのオブジェクト(仮想DOM)に変換され、プラットフォームとは関係なく、このオブジェクトがあれば、何に変換したいのかは気持ち次第です.const React = require('react');
const { renderToString} = require( 'react-dom/server');
const http = require('http');
//
class Index extends React.Component{
constructor(props){
super(props);
}
render(){
return {this.props.data.title}
}
}
//
const fetch = function () {
return {
title:'react ssr',
data:[]
}
}
//
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, {
'Content-Type': 'text/html'
});
const data = fetch();
const html = renderToString();
res.end(html);
}
}).listen(8080);
ps:以上のコードは直接実行できません.babelと組み合わせて@babel/preset-reactを使用して変換する必要があります.
npx babel script.js --out-file script-compiled.js --presets=@babel/preset-react
問題を引き起こす.
上で非常に簡単なのはreact ssr
を実現し、jsx
をテンプレートエンジンとして、上の小さなコードを軽視しないでください.彼は私たちに一連の問題を引き出すことができます.これもreact ssr
を完全に実現する礎です.
まず、
server
エンドでルーティング'/'を定義したことに気づきますが、react SPA
モードではreact-router
を使用してルーティングを定義する必要があります.それは2つのルートを維持する必要があるのではないでしょうか.データ取得の
fetch
に書かれた独立した方法は,コンポーネントとは何の関連もなく,各ルーティングに独自のfetch方法があることがより望ましいことが分かった.コンポーネントはサービス側でデータを取得し、ブラウザ内にもレンダリングされますが、ブラウザ側でコンポーネントレンダリングを行うと、まっすぐに出た内容が一瞬にして消えてしまいます.
はい、問題がありました.次はこれらの問題を一歩一歩解決します.
同構造こそが核心だ react ssr
のコアは同構造であり,同構造のないssrは意味がない.
同構造とは、一連のコードを採用し、両端(serverとclient)ロジックを構築し、2つのコードを維持することなく、コードを最大限に再利用することである.従来のサービス側レンダリングでは不可能であり、reactの出現はこのボトルネックを破り、現在では比較的広範な応用が得られている.
ルーティングどうけい
両端は同じルーティング規則を使用し、node server
はreq url path
によってコンポーネントの検索を行い、レンダリングが必要なコンポーネントを得る.
//コンポーネントとルーティング構成、両端でroutes-configを使用する.js
class Detail extends React.Component{
render(){
return detail
}
}
class Index extends React.Component {
render() {
return index
}
}
const routes = [
{
path: "/",
exact: true,
component: Home
},
{
path: '/detail', exact: true,
component:Detail,
},
{
path: '/detail/:a/:b', exact: true,
component: Detail
}
];
//
export default routes;
//クライアントルーティングコンポーネントimport routes from './routes-config.js';
function App(){
return (
{
routes.map((item,index)=>{
return
})
}
);
}
export default App;
Node serverによるコンポーネント検索
ルーティングマッチングは、コンポーネントpath
のルールのマッチングであり、ルールが複雑でなければ自分で書くことができ、状況が多ければ公式に提供されたライブラリを使用して完了することができる.matchRoutes(routes, pathname)
//
import { matchRoutes } from "react-router-config";
import routes from './routes-config.js';
const path = req.path;
const branch = matchRoutes(routes, path);
//
const Component = branch[0].route.component;
//node server
http.createServer((req, res) => {
const url = req.url;
// ,
if(url.indexOf('.')>-1) { res.end(''); return false;}
res.writeHead(200, {
'Content-Type': 'text/html'
});
const data = fetch();
//
const branch = matchRoutes(routes,url);
//
const Component = branch[0].route.component;
// html
const html = renderToString();
res.end(html);
}).listen(8080);
matchRoutes
の戻り値が表示されます.ここで、route.component
はレンダリングするコンポーネントです.
[
{
route:
{ path: '/detail', exact: true, component: [Function: Detail] },
match:
{ path: '/detail', url: '/detail', isExact: true, params: {} }
}
]
react-router-config
このライブラリはreactによって公式にメンテナンスされ、機能はネストされたルーティングの検索を実現し、コードはあまりなく、興味があれば見ることができます.
文章はここまで歩いて、あなたはすでにルートの同构を知っていると信じて、だから上の第1の问题:【両端のルートはどのように维持しますか?】解決しました.
データどうけい
ここでは、私たちが最初に発見した2つ目の問題を解決し始めました.「データを取得する方法と論理はどこに書かれていますか?」
データプリフェッチ同構造は、両端が同じデータ要求方法を使用してデータ要求を行う方法を解決します.
まず、レンダリングするコンポーネントを検索した後、そのコンポーネントに必要なデータを事前に取得してから、コンポーネントにデータを渡してから、コンポーネントのレンダリングを行います.
コンポーネントに静的メソッドを定義することによって処理することができ、コンポーネント内で非同期データ要求を定義する方法も合理的であり、同時に静的(static)と宣言し、server側とコンポーネント内でも直接コンポーネント(function)を通じてアクセスすることができる.
例えばIndex.getInitialProps
//
class Index extends React.Component{
constructor(props){
super(props);
}
//
static async getInitialProps(opt) {
const fetch1 =await fetch('/xxx.com/a');
const fetch2 = await fetch('/xxx.com/b');
return {
res:[fetch1,fetch2]
}
}
render(){
return {this.props.data.title}
}
}
//node server
http.createServer((req, res) => {
const url = req.url;
if(url.indexOf('.')>-1) { res.end(''); return false;}
res.writeHead(200, {
'Content-Type': 'text/html'
});
//
const branch = matchRoutes(routes,url);
//
const Component = branch[0].route.component;
//
const data = Component.getInitialProps(branch[0].match.params);
// , html
const html = renderToString();
res.end(html);
}).listen(8080);
また,ルーティングを宣言する際にデータ要求メソッドをルーティングに関連付け,例えばloadDataメソッドを定め,ルーティングを検索した後にloadData
というメソッドが存在するか否かを判断する.
リファレンスコードを見て
const loadBranchData = (location) => {
const branch = matchRoutes(routes, location.pathname)
const promises = branch.map(({ route, match }) => {
return route.loadData
? route.loadData(match)
: Promise.resolve(null)
})
return Promise.all(promises)
}
上記の方法では問題ありませんが、職責区分の観点から言えばはっきりしていないので、コンポーネントを直接通じて非同期の方法を得るのが好きです.
では、ここで2つ目の質問です.「データを取得する方法と論理はどこに書きますか?」解決しました.
レンダー同構造
上で実装したコードに基づいて、webpackを使用して構成し、コードを変換してパッケージ化し、サービス全体を走ることができると仮定します.
ルートは正しく一致することができて、データは正常に取って、サービス端はコンポーネントのhtmlをまっすぐ出すことができて、ブラウザはjsコードをロードして正常で、ホームページのソースコードを見てhtmlの内容を見ることができて、私达の全体の流れがすでに終わったようです.
しかし、ブラウザ側のjsの実行が完了すると、データが再要求され、コンポーネントの再レンダリングによってページが点滅しているように見えます.
これは、ブラウザ側では、両端ノードの比較に失敗し、コンポーネントが再レンダリングされるためです.つまり、サービス側とブラウザ側でレンダリングされたコンポーネントが同じprops
とDOM構造を持っている場合にのみ、コンポーネントは一度だけレンダリングされます.
デュアルエンドのデータプリフェッチ同構造を実現したばかりですが、データもサービス側にしかありません.ブラウザ側にはこのデータはありません.クライアントが最初のコンポーネントレンダリングを行うときに初期化されたデータはありません.レンダリングされたノードはサービス側がまっすぐ出ているノードとは異なり、コンポーネントの再レンダリングを招きます.
データ注水
サービス側でプリフェッチされたデータをブラウザに注入し、ブラウザ側がアクセスできるようにし、クライアントがレンダリングする前に対応するコンポーネントにデータを転送すれば、props
の一貫性が保証される.
//node server
http.createServer((req, res) => {
const url = req.url;
if(url.indexOf('.')>-1) { res.end(''); return false;}
res.writeHead(200, {
'Content-Type': 'text/html'
});
console.log(url);
//
const branch = matchRoutes(routes,url);
//
const Component = branch[0].route.component;
//
const data = Component.getInitialProps(branch[0].match.params);
// html
const html = renderToString();
//
const propsData = ``;
// ejs
ejs.renderFile('./index.html', {
htmlContent: html,
propsData
}, // key: ejs index
(err, data) => {
if (err) {
console.log(err);
} else {
console.log(data);
res.end(data);
}
})
}).listen(8080);
//node ejs html
// html
// init state ,
class Detail extends React.Component{
render(){
return detail
}
}
class Index extends React.Component {
render() {
return index
}
}
const routes = [
{
path: "/",
exact: true,
component: Home
},
{
path: '/detail', exact: true,
component:Detail,
},
{
path: '/detail/:a/:b', exact: true,
component: Detail
}
];
//
export default routes;
import routes from './routes-config.js';
function App(){
return (
{
routes.map((item,index)=>{
return
})
}
);
}
export default App;
//
import { matchRoutes } from "react-router-config";
import routes from './routes-config.js';
const path = req.path;
const branch = matchRoutes(routes, path);
//
const Component = branch[0].route.component;
//node server
http.createServer((req, res) => {
const url = req.url;
// ,
if(url.indexOf('.')>-1) { res.end(''); return false;}
res.writeHead(200, {
'Content-Type': 'text/html'
});
const data = fetch();
//
const branch = matchRoutes(routes,url);
//
const Component = branch[0].route.component;
// html
const html = renderToString();
res.end(html);
}).listen(8080);
[
{
route:
{ path: '/detail', exact: true, component: [Function: Detail] },
match:
{ path: '/detail', url: '/detail', isExact: true, params: {} }
}
]
//
class Index extends React.Component{
constructor(props){
super(props);
}
//
static async getInitialProps(opt) {
const fetch1 =await fetch('/xxx.com/a');
const fetch2 = await fetch('/xxx.com/b');
return {
res:[fetch1,fetch2]
}
}
render(){
return {this.props.data.title}
}
}
//node server
http.createServer((req, res) => {
const url = req.url;
if(url.indexOf('.')>-1) { res.end(''); return false;}
res.writeHead(200, {
'Content-Type': 'text/html'
});
//
const branch = matchRoutes(routes,url);
//
const Component = branch[0].route.component;
//
const data = Component.getInitialProps(branch[0].match.params);
// , html
const html = renderToString();
res.end(html);
}).listen(8080);
const loadBranchData = (location) => {
const branch = matchRoutes(routes, location.pathname)
const promises = branch.map(({ route, match }) => {
return route.loadData
? route.loadData(match)
: Promise.resolve(null)
})
return Promise.all(promises)
}
//node server
http.createServer((req, res) => {
const url = req.url;
if(url.indexOf('.')>-1) { res.end(''); return false;}
res.writeHead(200, {
'Content-Type': 'text/html'
});
console.log(url);
//
const branch = matchRoutes(routes,url);
//
const Component = branch[0].route.component;
//
const data = Component.getInitialProps(branch[0].match.params);
// html
const html = renderToString();
//
const propsData = ``;
// ejs
ejs.renderFile('./index.html', {
htmlContent: html,
propsData
}, // key: ejs index
(err, data) => {
if (err) {
console.log(err);
} else {
console.log(data);
res.end(data);
}
})
}).listen(8080);
//node ejs html
// html
// init state ,