一文はReact SSRサービス端のレンダリングと同構造の原理を徹底します


前に書く


この間ずっと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を取得した後、既存のルーティングテーブル内で対応するコンポーネントを検索し、要求するデータを取得し、propscontextまたは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 と組み合わせてサービス側レンダリングの直出を実現し、jsxejsに置き換え、以前は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方法があることがより望ましいことが分かった.
  • サービス側htmlノードは
  • を再利用できません.
    コンポーネントはサービス側でデータを取得し、ブラウザ内にもレンダリングされますが、ブラウザ側でコンポーネントレンダリングを行うと、まっすぐに出た内容が一瞬にして消えてしまいます.
    はい、問題がありました.次はこれらの問題を一歩一歩解決します.

    同構造こそが核心だ

    react ssrのコアは同構造であり,同構造のないssrは意味がない.
    同構造とは、一連のコードを採用し、両端(serverとclient)ロジックを構築し、2つのコードを維持することなく、コードを最大限に再利用することである.従来のサービス側レンダリングでは不可能であり、reactの出現はこのボトルネックを破り、現在では比較的広範な応用が得られている.

    ルーティングどうけい


    両端は同じルーティング規則を使用し、node serverreq 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 ,