周常2 算法題4道、react ssr 原理實(shí)踐、koa-router 源碼閱讀

周常

  • 算法題 java 實(shí)現(xiàn)
    1.爬樓梯(斐波那契數(shù)列)
    2.位1的個(gè)數(shù)
    3.實(shí)現(xiàn) Pow(x, n) x 的 n 次冪函數(shù)
    4.第N個(gè)數(shù)字

  • react ssr 原理

  • koa-router 源碼閱讀

算法題

爬樓梯(斐波那契數(shù)列)

假設(shè)你正在爬樓梯。需要 n 階你才能到達(dá)樓頂。
每次你可以爬 1 或 2 個(gè)臺(tái)階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個(gè)正整數(shù)。

解題思路

  • 0層到1 層臺(tái)階,只有一種方法。
  • 0層到2 層臺(tái)階,有先跨1 層和跨2 層,兩種方法。
  • 0層到3 層臺(tái)階,可以理解為下面兩種情況相加:
    (1) 先從0層跨到2層,相當(dāng)于(3-1) = 2 層到3 層臺(tái)階, 只有一種方法
    (2) 先從0層跨到1層,相當(dāng)于(3-2) = 1 層到3 層臺(tái)階, 先跨1 層和跨2 層,兩種方法
  • 上面這種情況下,可以從 0 層選擇跨 1 或 2層對(duì)應(yīng) n-2或 n-1
  • 宏觀考慮 0層到n 層臺(tái)階 f(n),可以理解為 f(n-1) + f(n-2) 相加
  • 考慮到使用遞歸計(jì)算性能較差,采用循環(huán)的方法

代碼實(shí)現(xiàn)

public class ClimbStairs {

    public int climbStairs(int n) {
        if (n == 1) // 一階 1種
            return 1;

        int[] arr = new int[n + 1];
        arr[1] = 1;
        arr[2] = 2; 
        // n 最小為3
        for (int i = 3; i <= n; i++)
            arr[i] = arr[i - 1] + arr[i - 2];

        return arr[n];
    }
}

爬樓梯

位1的個(gè)數(shù)

解題思路

  • 解法1
    從 32 位 二進(jìn)制碼的第一位開始 ...000001 ,...000010, ...000100, 比與 n 比較。
  • 解法2
    1.根據(jù)二進(jìn)制的特性,...110100 減1 為 ...110011, 兩者使用 & 操作符結(jié)果為 ...110000。
  1. ...110000 這個(gè)結(jié)果把 ..110100 最后一位 1給刪除掉了。
  2. 通過(guò)每次減 1 再比較的這個(gè)方式,一直減到 n 為 ...000000 也就是 0 后結(jié)束。


代碼實(shí)現(xiàn)

public class HammingWeight1 {
    // 解法1 逐個(gè)比較
    public int s1(int n) {
        int sum = 0;
        int mark = 1;

        for (int i = 0; i < 32; i++) {
            if ((n & mark) != 0)
                sum++;
            mark <<= 1;
        }
        return sum;
    }
    // 解法2 二進(jìn)制數(shù) 減1來(lái)比較
    public int s2(int n) {
        int sum = 0;

        while (n != 0) {
            sum++;
            n &= (n - 1);
        }
        return sum;
    }

}

位1的個(gè)數(shù)

Pow(x, n)

實(shí)現(xiàn) pow(x, n) ,即計(jì)算 x 的 n 次冪函數(shù)。

解題思路

  1. 2 ^ 32 等于 2 ^ 16 再平方,也就是 (2 ^ 2) ^ 16
  2. 通過(guò)這個(gè)定律可知 2 ^ 32 == (2 ^ 2) ^ 16 == (2 ^ 2 ^ 2) ^ 8 == (2 ^ 2 ^ 2 ^ 2) ^ 4 == (2 ^ 2 ^ 2 ^ 2 ^ 2) ^ 2 == (2 ^ 2 ^ 2 ^ 2 ^ 2 ^ 2) ^ 1
  3. 通過(guò)這個(gè)方法可以根據(jù) N 來(lái)不停循環(huán), 每次循環(huán) N / 2, 同時(shí) x * x 自己跟自己相乘進(jìn)行平方計(jì)算,N 為 1 時(shí) 計(jì)算并返回 x 的結(jié)果。
  4. 如果 N 為奇數(shù),循環(huán)的最終結(jié)果 2 ^ 33 == (2 ^ 2 ^ 2 ^ 2 ^ 2 ^ 2) ^ 1 * 2, 需要乘一下最初的 x。
  5. 當(dāng) N < 0, 時(shí) 1 = 1/x, N = -N
  6. 不用遞歸的原因是因?yàn)楫?dāng)數(shù)很大時(shí)耗時(shí)很長(zhǎng).

代碼實(shí)現(xiàn)

    public double pow(double x, int n) {
        long N = n; // 防止很大的數(shù)
        if (N == 0) return 1;

        if (N < 0) {
            x = 1 / x;
            N = -N;
        }

        double result = 1; // 會(huì)有小數(shù)的情況
        while (N > 0) {
            if (N % 2 == 1)
                result = result * x;

            N = N / 2; // 每次 / 2
            x = x * x; // x 都平方一次
        }

        return result;
    }

Pow(x, n)

第N個(gè)數(shù)字

在無(wú)限的整數(shù)序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...中找到第 n 個(gè)數(shù)字。
n 是正數(shù)且在32為整形范圍內(nèi) ( n < 231)。

輸入:
3
輸出:
3

輸入:
11
輸出:
0
說(shuō)明:
第11個(gè)數(shù)字在序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 里是0,它是10的一部分。

解題思路

  • 假設(shè):
    1 n 是1000
    2 一位數(shù)區(qū)間 [1, 9] 有 19=9個(gè)數(shù)字的字符串,二位數(shù)區(qū)間 [10, 99] 有290=180個(gè)數(shù)字的字符串, 三位數(shù)區(qū)間 [100, 999] 有 3*900=2700個(gè)數(shù)字的字符串。
    3 n 1000, 肯定是落在了 三位數(shù)區(qū)間 [100, 999] 中的某個(gè)位置。
    4 1000 - 9 - 180 = 811,題目給的序號(hào)是從 1開始計(jì)算的,實(shí)際應(yīng)該從序號(hào) 0開始,811 需要減去1 , 所以 810 是數(shù)字100 第一位 1 到 n 之間所有的字符數(shù) 。
    5 我們來(lái)找到 n 1000 所在的數(shù)字 k,數(shù)100 到數(shù) k 之間有 810,個(gè)字符,同時(shí)是在三位數(shù)區(qū)間內(nèi)。
    6 100 到 k 之間有 810 / 3 = 270 個(gè)數(shù)字
    7 270 個(gè)數(shù)字加上之前跳過(guò)的一位數(shù)和二位數(shù)區(qū)間 [1-100] 的個(gè)數(shù) 100, 可以確定 n 1000 落在370 這個(gè)數(shù)字上。
    8 最后我們需要確定 n 1000 是在 370 的哪個(gè)位置上。
    9 之前我們?cè)谌粩?shù)區(qū)間 [100, 999] 上獲得了100 的第一位 1 到 n 1000 落在數(shù)字 370 上的某個(gè)位置之間的所有字符一共有 810 個(gè)
    10 通過(guò)取模 % 的特性,對(duì)三位數(shù) 3 取模結(jié)果只會(huì) 0 1 2 是三個(gè)數(shù)中的一個(gè),現(xiàn)在用 810 % 3 === 0, 可知 810 的位置是某三位數(shù)的第一位,我們已經(jīng)知道這個(gè)數(shù)是 370,最后確定第一位的結(jié)果是 3
  • 總結(jié):
    1 確定 n 在幾位數(shù)的區(qū)間中
    2 找到 n 落在哪個(gè)數(shù)字上
    3 找到 n 在這個(gè)數(shù)字上的某個(gè)位置

代碼實(shí)現(xiàn)

    public int findNthDigit(int n) {
        int len = 1; // 位數(shù)計(jì)數(shù) 1-9:1, 10-99:2, 100-999:3
        long count = 9; // 9, 90, 900
        int start = 1;

        while (n > len * count) { // len * count = 9, 180... 1-9有(1*9)字符, 10-99有(2*90)字符
            n -= len * count;
            len += 1;
            count *= 10;
            start *= 10;
        }
        // len = 1, n 在 1-9=9個(gè)字符內(nèi),len = 2, n 在 10-99=180個(gè)字符內(nèi), len = 3,n 在 100-999=2700個(gè)字符內(nèi)
        // start 從1,10,100 開始

        /**
         * 1000 - 9 - 180 = 811
         * (811 - 1) / 3 = 270
         * 100(start) + 270 = 370
         */
        // n - 1是因?yàn)閺?1 位置開始計(jì)算而不是0
        start += (n - 1) / len; // start為100在n 811 內(nèi)
        String s = Integer.toString(start);
        return Character.getNumericValue(s.charAt((n - 1) % len)); // n - 1 是因?yàn)閺?1 開始而不是 0
    }

第N個(gè)數(shù)字

react ssr 原理

react ssr 問(wèn)題主要解決三個(gè)問(wèn)題

  1. server 端編譯 react 組件轉(zhuǎn)成 html 返回給瀏覽器
  2. 客戶端代碼接管 react 讓頁(yè)面邏輯執(zhí)行 - 同構(gòu)
  3. client 代碼接管 server 端返回的頁(yè)面后需要配置路由
  4. redux 狀態(tài)管理數(shù)據(jù)在 server 和 client 兩端統(tǒng)一 - 數(shù)據(jù)注水脫水

第一問(wèn)題,解決 server 端返回 react 編譯后的 html

  • 重點(diǎn)詞:
    webpack.server.js 文件 - 編譯打包 server 端的 js 代碼
    renderToString - react-dom/server 模塊下的方法

  • 實(shí)現(xiàn)方式:
    使用 webpack 配置打包 server 端 react 代碼的 webpack.server.js 文件,打包編譯成 html 字符串返回給前端

  • 代碼

server 端返回的 react 組件
把 jsx 編譯成字符串插入
import Home from './containers/Home';
import { renderToString } from 'react-dom/server';
const content = renderToString(<Home />);
 res.send(`
        <html>
            <head>
                <title>ssr</title>
            </head>
            <body>
                ${content}
            </body>
        </html>
  `);
// webpack.sever.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
    target: 'node', // 防止打包 node 原生模塊的代碼,比如 path
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'build')
    },
    externals: [nodeExternals()], // 此選項(xiàng)配置排除的模塊,nodeExternals 排除 node_modules 里面的模塊
    module: {
        rules: [{
            test: /\.js?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            options: {
                presets: ['react', 'stage-0', ['env', {
                    targets: {
                        browsers: ['last 2 versions']
                    }
                }]]
            }
        }]
    }
}

第二個(gè)問(wèn)題讓客戶端代碼接管 react 讓頁(yè)面邏輯執(zhí)行 - 同構(gòu)

server 端返回 html 還不夠,前端代碼還不能在這個(gè) html 里執(zhí)行

renderToString 只能把 react 組件編譯成字符串然后通過(guò) server 返回到瀏覽器。
而組件上綁定的實(shí)踐是無(wú)法編譯的。

        <div>
            <div>This is Dell Lee!</div>
            <button onClick={()=>{alert('click1')}}>
                click
            </button>
        </div>
        // onClick 事件是不會(huì)出現(xiàn)在瀏覽器上的

使用通過(guò),讓 react 代碼在服務(wù)端上執(zhí)行同時(shí)又在客戶端上執(zhí)行。

如何讓客戶端再執(zhí)行一遍

  • 服務(wù)端返回的頁(yè)面加載一個(gè) js 文件讓瀏覽器加載后自己執(zhí)行
  res.send(`
        <html>
            <head>
                <title>ssr</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src='/index.js'></script>
            </body>
        </html>
  `);
  • index.js文件如何來(lái)
    app.use(express.static('public')); 定義靜態(tài)文件
    /public/index.js 此目錄下放 webpack 打包后的 index.js
    /src/client/index.js 此目錄下放客戶端執(zhí)行的代碼
import React from 'react';
import ReactDom from 'react-dom';

import Home from '../containers/Home';
// ssr 使用 hydrate 而不是 render
ReactDom.hydrate(<Home />, document.getElementById('root'))

webpack.client.js 放打包客戶端代碼的配置

module.exports = {
    mode: 'development',
    entry: './src/client/index.js',
    output: {
        filename: 'index.js',
        path: path.resolve(__dirname, 'public')
    },
    module: {
        // ...
    }
}
  • 總結(jié)
    服務(wù)端運(yùn)行 React 代碼渲染出 HTML 結(jié)構(gòu)
    發(fā)送 HTML 給瀏覽器
    瀏覽器接受內(nèi)容展示 (只有 html)
    瀏覽器加載 js 文件
    js 中的運(yùn)行一樣的 React 代碼,在瀏覽器端重新執(zhí)行 (會(huì)執(zhí)行掛載,綁定事件等)
    JS 中的 React 代碼就接管了服務(wù)端發(fā)送來(lái)的HTML 頁(yè)面和操作。(正常執(zhí)行)

第三個(gè)問(wèn)題配置前后端一致的路由

同構(gòu)的目的就是讓 JS 中的 react 代碼在瀏覽器上再執(zhí)行一次接管 html 頁(yè)面。
原來(lái)我們的 JS 文件用 react-router來(lái)識(shí)別瀏覽器的目錄來(lái)渲染不同的頁(yè)面
現(xiàn)在瀏覽器的 url 需要識(shí)別到底是后端請(qǐng)求還是前端頁(yè)面
都清楚瀏覽器執(zhí)行的代碼跟服務(wù)端執(zhí)行的代碼是有區(qū)別的
區(qū)別就是在 server 的 React 代碼中使用 StaticRouter, 而 client 的 React 代碼中使用 BrowserRouter

方法

使用 react-router
創(chuàng)建 Routes.js 文件

import React from 'react';
import { Route } from 'react-router-dom';
import Home from './containers/Home';
import Login from './containers/Login';

export default (
    <div>
        <Route path='/' exact component={Home}></Route>
        <Route path='/login' exact component={Login}></Route>
    </div>
)

在client/index.js 中掛載路由

import React from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Routes from '../Routes';

const App = () => {
    return (
        <BrowserRouter>
            {Routes}
        </BrowserRouter>
    )
}

ReactDom.hydrate(<App />, document.getElementById('root'))

改造 server/index.js 中路由

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Routes from '../Routes';

export const render = (req) => {
    const content = renderToString((
        <StaticRouter location={req.path} context={{}}>
            {Routes}
        </StaticRouter>
    ));

    return `
        <html>
            <head>
                <title>ssr</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src='/index.js'></script>
            </body>
        </html>
  `;
}

StaticRouter

StaticRouter 不像 BrowserRouter 可以直接感知瀏覽器路徑

        <StaticRouter location={req.path} context={{}}>
            {Routes}
        </StaticRouter>

context 屬性用于數(shù)據(jù)通信
location 用于感知瀏覽器請(qǐng)求的路徑,需要把 req.path 傳遞給 StaticRouter, 當(dāng)瀏覽器請(qǐng)求的路徑匹配到 Routes 時(shí),server 執(zhí)行的 StaticRouter 就會(huì)把相應(yīng)的 React 組件返回給瀏覽器
這時(shí)候又是服務(wù)端 通過(guò) StaticRouter 執(zhí)行一次,瀏覽器通過(guò) BrowserRouter 執(zhí)行一次。

服務(wù)端改造

改造 get 服務(wù)端路由,* 用來(lái)匹配所有請(qǐng)求

app.get('*', function (req, res) {
  res.send(render(req));
});

使用 Link 標(biāo)簽

const Header = () => {
  return (
    <div>
      <Link to='/'>home</Link>
      <br />
      <Link to='/login'>login</Link>
    </div>
  )
}
  • 回顧和總結(jié)
    僅僅在首次請(qǐng)求會(huì)同構(gòu)
    跳轉(zhuǎn)路由時(shí)候并不會(huì)再次請(qǐng)求
    StaticRouter 只是對(duì)應(yīng)了 BrowersRouter 同步切換

第四個(gè)問(wèn)題 redux 異步數(shù)據(jù)在前后端上的統(tǒng)一

瀏覽器的 client 代碼仍然使用 createStore 使用 Provider 組件進(jìn)行傳遞

服務(wù)端的 server 代碼需要把 store 再做一次傳遞到 服務(wù)端的 react 代碼,可以跟 clinet 代碼共用 createStore

步驟

共用 store 代碼

/store/index.js

import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';

const reducer = combineReducers({
    home: homeReducer
});

export const getStore = () => {
    return createStore(reducer, applyMiddleware(thunk));
}

export const getClientStore = () => {
    const defaultState = window.context.state;
    return createStore(reducer, defaultState, applyMiddleware(thunk));
}

防止 server 代碼使用 單例 store 導(dǎo)致每個(gè)用戶都用一套 store
要使用一個(gè) getStore 方法,讓每個(gè)用戶請(qǐng)求都重新創(chuàng)建一個(gè) store

        const content = renderToString((
            <Provider store={getStore()}>
                <StaticRouter location={req.path} context={{}}>
                    ...
                </StaticRouter>
            </Provider>
        ));
export const getStore = () => {
    return createStore(reducer, applyMiddleware(thunk));
}

服務(wù)端客戶端共用 redux store

componentDidMount 只在客戶端執(zhí)行 生命周期只在客戶端代碼中執(zhí)行,并沒在服務(wù)端代碼中執(zhí)行,雖然服務(wù)端和客戶端同時(shí)執(zhí)行了相關(guān)代碼實(shí)現(xiàn),但是實(shí)際渲染出來(lái)的代碼是不包含 redux store 里的數(shù)據(jù)的。

    componentDidMount() {
        if (!this.props.list.length) {
            this.props.getHomeList();
        }
    }

流程整理

1.請(qǐng)求項(xiàng)目 -> server
2.server 執(zhí)行 render 來(lái)渲染 react 代碼

export const render = (store, routes, req) => {

        const content = renderToString((
            <Provider store={store}>
                <StaticRouter location={req.path} context={{}}>
                    <div>
                        {routes.map(route => (
                    <Route {...route}/>
                    ))}
                </div>
                </StaticRouter>
            </Provider>
        ));

        return `
            <html>
                <head>
                    <title>ssr</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script>
                        window.context = {
                            state: ${JSON.stringify(store.getState())}
                        }
                    </script>
                    <script src='/index.js'></script>
                </body>
            </html>
      `;
    
}
  1. 這里面的 store 是由 getStore 創(chuàng)建
export const getStore = () => {
    return createStore(reducer, applyMiddleware(thunk));
}
  1. 這時(shí)候 store 還是個(gè)空的初始數(shù)據(jù),而客戶端可以執(zhí)行 生命周期獲取數(shù)據(jù)
    componentDidMount() {
        if (!this.props.list.length) {
            this.props.getHomeList();
        }
    }

5.雖然客戶端代碼執(zhí)行了 redux 中的請(qǐng)求獲取數(shù)據(jù)渲染,但現(xiàn)在服務(wù)端返回的 react 代碼還是空數(shù)據(jù)并沒有什么改變,也不會(huì)展示在 HTML 上。

讓服務(wù)端解決異步請(qǐng)求數(shù)據(jù)讓頁(yè)面上也展示請(qǐng)求數(shù)據(jù)

構(gòu)建 loadData 代替 componentDidMount

解決思路:
服務(wù)端和客戶端有兩個(gè) store
服務(wù)端的是用戶每次請(qǐng)求組件通過(guò) getStore() 執(zhí)行生成的

app.get('*', function (req, res) {
    const store = getStore();
    ...
}

客戶端的是又客戶端 js 代碼生成的

使用 loadData:
在頁(yè)面組件里創(chuàng)建 loadData

Home.loadData = (store) => {
    // 這個(gè)函數(shù),負(fù)責(zé)在服務(wù)器端渲染之前,把這個(gè)路由需要的數(shù)據(jù)提前加載好
    return store.dispatch(getHomeList())
}

路由重構(gòu)

目的:
訪問(wèn) / 獲取 home 的異步數(shù)據(jù)
訪問(wèn) /login 獲取 login 的異步數(shù)據(jù)

使用 react-router 中 matchPath matchRouter 方法:
改造路由對(duì)象

const routes = [
    { 
        path: '/',
        component: Home,
        exact: true,
        loadData: Home.loadData, // 告訴路由匹配渲染時(shí)執(zhí)行組件的 loadData 方法,用來(lái)讓 server 端獲取異步數(shù)據(jù)返回到 html 頁(yè)面上
        key: 'home'
  }, 
  { 
        path: '/login',
        component: Login,
        exact: true,
        key: 'login'
  }
];
服務(wù)端
            <Provider store={store}>
                <StaticRouter location={req.path} context={{}}>
                    <div>
                        {routes.map(route => (
                    <Route {...route}/>
                    ))}
                </div>
                </StaticRouter>
            </Provider>
客戶端 
const App = () => {
    return (
        <Provider store={store}>
            <BrowserRouter>
                <div>
                    {routes.map(route => (
                <Route {...route}/>
                ))}
            </div>
            </BrowserRouter>
        </Provider>
    )
}

現(xiàn)在我們已經(jīng)匹配好了路由的重構(gòu)讓服務(wù)端代碼匹配到路由時(shí)可以請(qǐng)求頁(yè)面異步數(shù)據(jù),但是還不夠
還需要在服務(wù)端被 app.get() 請(qǐng)求時(shí)返回頁(yè)面前把請(qǐng)求完畢的 store 里的數(shù)據(jù)傳遞到 server 的 Provider 里再返回到瀏覽器上.

使用 matchRoutes 匹配多層路由

export default [
    { 
        path: '/',
    component: Home,
    // exact: true,
    loadData: Home.loadData,
    key: 'home',
    routers: [{
          path: '/ttt',
      component: Login,
      exact: true,
      key: 'ttt'
    }]
  }, {
        path: '/login',
    component: Login,
    exact: true,
    key: 'login'
  }
];

server 根據(jù)路由的路徑,來(lái)往 store 里加數(shù)據(jù)

import { matchRoutes } from 'react-router-config'

app.get('*', function (req, res) {
    const store = getStore();
    // 根據(jù)路由的路徑,來(lái)往store里面加數(shù)據(jù), matchedRoutes 存放所有匹配到的路由信息
    const matchedRoutes = matchRoutes(routes, req.path);
    // 讓matchRoutes里面所有的組件,對(duì)應(yīng)的loadData方法執(zhí)行一次
    const promises = [];
    matchedRoutes.forEach(item => {
        if (item.route.loadData) { // 判斷匹配的路由有 loadData 就執(zhí)行
            // 執(zhí)行 loadData, 讓 loadData 具有 store 來(lái) dispatch 把所有 loadData 異步請(qǐng)求回來(lái)的結(jié)果給 push 到 promises 里。解決 axios 是異步數(shù)據(jù)的問(wèn)題
            promises.push(item.route.loadData(store))
        }
    })
    // 讓所有異步數(shù)據(jù)都執(zhí)行成功后才返回 HTML,保證 loadData 的數(shù)據(jù)獲取完后才執(zhí)行 render 返回.
    Promise.all(promises).then(() => {
        res.send(render(store, routes, req));
    })
});

服務(wù)端客戶端 store 數(shù)據(jù)統(tǒng)一

做到現(xiàn)在當(dāng) 開啟 js 執(zhí)行時(shí),訪問(wèn)頁(yè)面還是會(huì)出現(xiàn)白屏,雖然 server 的異步數(shù)據(jù)返回了,但是瀏覽器還是渲染了 client 的異步數(shù)據(jù)再渲染

  • 原因
    客戶端代碼接管時(shí)剛開始加載時(shí)客戶端的 store 是空的,客戶端代碼仍然是要等到生命周期請(qǐng)求后才能獲取到渲染數(shù)據(jù)。
    而服務(wù)端 store 是有數(shù)據(jù)的,和客戶端不同, 沒有做到統(tǒng)一

  • 解決 脫水注水
    改寫服務(wù)端代碼的數(shù)據(jù)

        return `
            <html>
                <head>
                    <title>ssr</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script>
                        // 把 server store 數(shù)據(jù)放到全局變量下
                        window.context = {
                            state: ${JSON.stringify(store.getState())}
                        }
                    </script>
                    <script src='/index.js'></script>
                </body>
            </html>
      `;
    

改寫 client 客戶端代碼中的 store

export const getClientStore = () => {
    // 獲取 server store 放在全局變量里的數(shù)據(jù)
    const defaultState = window.context.state; 
    // 把這些數(shù)據(jù)作為 client store 的默認(rèn)數(shù)據(jù), 解決統(tǒng)一問(wèn)題
    return createStore(reducer, defaultState, applyMiddleware(thunk));
}
  • 流程
    服務(wù)端通過(guò) loadData 來(lái)獲取 store 后,返回 html 時(shí)把 server 的 store 數(shù)據(jù)寫在全局變量下。
    客戶端執(zhí)行代碼時(shí),讓 client 的 store 獲取全局變量的數(shù)據(jù)作為 client 的 store 的默認(rèn)值,這樣客戶端的 store 就不是一個(gè)初始數(shù)據(jù),解決了 server client store 的統(tǒng)一

  • 注意
    注水脫水雖然解決了問(wèn)題,但是不能省略 componentDidMount ,不然非首屏?xí)r是無(wú)法獲取數(shù)據(jù)的,脫水注水只解決的首屏數(shù)據(jù)統(tǒng)一的問(wèn)題, 但是生命周期的請(qǐng)求和數(shù)據(jù)注水又重復(fù)消耗性能。
    折中方案在 componentDidMount 中判斷是否重復(fù)請(qǐng)求

    componentDidMount() {
        if (!this.props.list.length) {
            this.props.getHomeList();
        }
    }

koa-router 源碼閱讀

koa-router 使用

var Koa = require('koa');
var Router = require('koa-router');

var app = new Koa();
var router = new Router();

router.get('/', (ctx, next) => {
  // ctx.router available
});

app.use(router.routes())

從 koa-router 的調(diào)用 api 來(lái)看,是 koa-router 的實(shí)例 router 調(diào)用了 routes() 方法開啟了 http 路由模式

查看調(diào)用的 router.js

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;

  var dispatch = function dispatch(ctx, next) {
    // ...

    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
}

從 router.js 里的 routes 中可以看到,這個(gè)實(shí)例方法創(chuàng)建了一個(gè) layerChain 的數(shù)組,通過(guò) compose 方法給每個(gè)數(shù)組里的元素傳遞 ctx next 參數(shù)。
而我們的 koa.use(router.routes()) 可以看做 koa.use(compose(layerChain)(ctx, next))
koa.use 方法主要執(zhí)行的就是 this.middleware.push(middleware) 這個(gè)方法,這樣可以知道 routes 方法就是通過(guò) layerChain 生成了多個(gè)中間件掛載到 koa 的中間件模型中。

尋找 router 是如何使用 layer 的

methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    if (typeof path === 'string' || path instanceof RegExp) {
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }

    this.register(path, [method], middleware, {
      name: name
    });

    return this;
  };
});

在 router.js 中有這么一端代碼,用處很簡(jiǎn)單就是給 Router 構(gòu)造函數(shù)創(chuàng)建 HTTP 請(qǐng)求 router.get, router.post 等方法函數(shù)供使用者調(diào)用,每個(gè) HTTP 請(qǐng)求的方法都執(zhí)行了 this.register

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};

  var router = this;
  var stack = this.stack;

  // support array of paths
  if (Array.isArray(path)) {
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });

    return this;
  }

  // create route
  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true,
    name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false,
    strict: opts.strict || this.opts.strict || false,
    prefix: opts.prefix || this.opts.prefix || "",
    ignoreCaptures: opts.ignoreCaptures
  });

  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix);
  }

  // add parameter middleware
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);

  stack.push(route);

  return route;
};

this.register 方法的主要作用就在創(chuàng)建和注冊(cè)一個(gè)路由。它創(chuàng)建路由的方法就是把router.get() 等方法傳遞的參數(shù)來(lái)創(chuàng)建一個(gè) Layer 實(shí)例。最后把每個(gè) layer 實(shí)例都存到了 router 實(shí)例的 stack 中

查看 layer.js 了解 Layer 的作用

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null;
  this.methods = [];
  this.paramNames = [];
  this.stack = Array.isArray(middleware) ? middleware : [middleware];

  // ...

  this.path = path;
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};

這里可以看出 Layer 實(shí)例生成把,router.get() 方法中傳遞的中間件 middleware 存到了 layer 實(shí)例的 stack 中

koa-router 如何匹配路徑

Router.prototype.match = function (path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };

  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);

    if (layer.match(path)) {
      matched.path.push(layer);

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

我們知道在 Router 會(huì)執(zhí)行 register 把每個(gè) route - layer 實(shí)例都 存入 router 實(shí)例的 stack 中。
在 match 中,把 http 請(qǐng)求來(lái)的路徑和所有 router 實(shí)例存在 stack 的 layer 比較,再返回出去。

回顧下 koa-router

  • 生成 koa-router 實(shí)例 router
  • 寫 router.get(), router.post() 方法, 執(zhí)行 Router.prototype[method] 方法,執(zhí)行Router.prototype.register ,生成 layer 實(shí)例存到 router 實(shí)例的 stack 中。
  • layer 實(shí)例把每個(gè) http 方法的中間件也 concat 合并到一起,并存到自己的 stack 中。
  • app.use(router. routes()) 執(zhí)行 koa-router 實(shí)例方法 Router.prototype.routes
  • 在 Router.prototype.routes 方法中返回一個(gè) dispatch 函數(shù)
  • 這個(gè) dispatch 函數(shù)執(zhí)行時(shí)會(huì)執(zhí)行 Router.prototype.match 把 router 實(shí)例存放的所有 layer 實(shí)例拿出來(lái)進(jìn)行匹配,匹配上的所有 layer 實(shí)例組成 layerChain。
  • 最后 layerChain 通過(guò) compose 生成多個(gè)標(biāo)準(zhǔn)的 koa 中間件供 app.use()。

查看 Router.prototype.use 方法

// Router.prototype.use 用法
 router
   .use(session())
   .use(authorize());

 router.use('/users', userAuth());

 router.use(['/users', '/admin'], userAuth());
Router.prototype.use = function () {
  var router = this;
  var middleware = Array.prototype.slice.call(arguments);
  var path;

  // support array of paths
  if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
    middleware[0].forEach(function (p) {
      router.use.apply(router, [p].concat(middleware.slice(1)));
    });

    return this;
  }

  var hasPath = typeof middleware[0] === 'string';
  if (hasPath) {
    path = middleware.shift();
  }

  middleware.forEach(function (m) {
    if (m.router) {
      m.router.stack.forEach(function (nestedLayer) {
        if (path) nestedLayer.setPrefix(path);
        if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);
        router.stack.push(nestedLayer);
      });

      if (router.params) {
        Object.keys(router.params).forEach(function (key) {
          m.router.param(key, router.params[key]);
        });
      }
    } else {
      router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
    }
  });

  return this;
};

Router.prototype.use 方法的作用就是給 router 實(shí)例里可以匹配的路徑里添加中間件,對(duì) layer 進(jìn)行重新注冊(cè)。通過(guò) use 的方法添加的中間件都是在原匹配路徑的其他中間件和路由前執(zhí)行。

查看 Router.prototype.allowedMethod

Router.prototype.allowedMethod 是用來(lái)處理路由執(zhí)行的錯(cuò)誤的,通過(guò)傳入的配置來(lái)自定義錯(cuò)誤處理

/*
 * @param {Object=} options
 * @param {Boolean=} options.throw 開啟自定義處理錯(cuò)誤
 * @param {Function=} options.notImplemented 處理 router 實(shí)例中 this.methods 不存在的方法
 * @param {Function=} options.methodNotAllowed 處理路由未定義方法的錯(cuò)誤函數(shù)(只定義了get 沒定義 post ,處理post 請(qǐng)求報(bào)錯(cuò))
 */
Router.prototype.allowedMethods = function (options) {
  options = options || {};
  var implemented = this.methods; // array

  return function allowedMethods(ctx, next) {
    return next().then(function() {
      var allowed = {};

      if (!ctx.status || ctx.status === 404) {
        ctx.matched.forEach(function (route) {
          route.methods.forEach(function (method) {
            allowed[method] = method;
          });
        });

        var allowedArr = Object.keys(allowed);

        if (!~implemented.indexOf(ctx.method)) {
          if (options.throw) {
            var notImplementedThrowable;
            if (typeof options.notImplemented === 'function') {
              notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function
            } else {
              notImplementedThrowable = new HttpError.NotImplemented();
            }
            throw notImplementedThrowable;
          } else {
            ctx.status = 501;
            ctx.set('Allow', allowedArr.join(', '));
          }
        } else if (allowedArr.length) {
          if (ctx.method === 'OPTIONS') {
            ctx.status = 200;
            ctx.body = '';
            ctx.set('Allow', allowedArr.join(', '));
          } else if (!allowed[ctx.method]) {
            if (options.throw) {
              var notAllowedThrowable;
              if (typeof options.methodNotAllowed === 'function') {
                notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function
              } else {
                notAllowedThrowable = new HttpError.MethodNotAllowed();
              }
              throw notAllowedThrowable;
            } else {
              ctx.status = 405;
              ctx.set('Allow', allowedArr.join(', '));
            }
          }
        }
      }
    });
  };
};

allowedMethod 返回一個(gè)可以生成中間件函數(shù),當(dāng)返回的 http 響應(yīng)是 404 或 status 不存在時(shí), 遍歷每個(gè) matched 到的 layer,來(lái)執(zhí)行相應(yīng)的錯(cuò)誤邏輯。


請(qǐng)求流程

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 32,289評(píng)論 2 89
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,639評(píng)論 1 32
  • 今夜的風(fēng)擁抱著窗欞 不忍離開 如泣如訴的敲打著玻璃窗 也許 是貪戀屋內(nèi)那溫暖的燈光 燈影下的雪 舞得如此的賣力 也...
    心安何所歸閱讀 949評(píng)論 12 27
  • 選擇困難癥為世界上幾大疑難雜癥之一。 并且,無(wú)藥可醫(yī)。 想治???只能自救。 去逛街,看到這件衣服好看,想要。另一件...
    曬太陽(yáng)的冬菇閱讀 534評(píng)論 0 0
  • 同學(xué)們互相的信任和幫助,能讓彼此拉近距離,傳遞感情, 也讓大家的快樂得到了延續(xù)。 也許,同學(xué)們?yōu)槟阕龀?..
    DIS魚閱讀 530評(píng)論 0 1

友情鏈接更多精彩內(nèi)容