美文网首页
周常3 算法题5道、react ssr 补充

周常3 算法题5道、react ssr 补充

作者: coolheadedY | 来源:发表于2019-01-02 21:17 被阅读23次

    周常

    • 算法题 java 实现
      1.调整数组顺序使奇数位于偶数前面
      2.链表中倒数第k个结点
      3.翻转链表
      4.合并两个排序的链表
      5.树的子结构

    • react ssr 补充

    算法题

    调整数组顺序使奇数位于偶数前面

    输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变

    解题思路

    1.前后两个指针从头尾出发
    2.判断两个指针序号的奇偶数
    3.左右指针不接触时进行运算
    4.左指针序号自增直到找到偶数,右指针序号自减直到找到奇数。
    5.左指针序号小于右指针序号则两者未接触,交换位置。

    代码实现

    public class ReorderArray {
    
        public void reorderArray(int[] arr) {
            int left = 0;
            int right = arr.length - 1;
    
            while (left < right) {
                while (left < right && (arr[left] % 2 != 0))
                    left++;
                while (left < right && (arr[right] % 2 == 0))
                    right--;
                if (left < right)
                    swap(left, right, arr);
            }
        }
    
        private void swap(int n, int m, int[] arr) {
            int temp = arr[n];
            arr[n] = arr[m];
            arr[m] = temp;
        }
    
        public static void main(String[] args) {
            int[] arr = {1, 3, 54, 1, 4, 65, 546, 2, 3, 5, 6};
            new ReorderArray().reorderArray(arr);
            for (int anArr : arr) System.out.print(anArr + " ");
        }
    }
    

    链表中倒数第k个结点

    链表中倒数第k个节点

    解题思路

    用两个链表使用双指针解法
    1.第一个指针先走 k 步
    2.第二个指针和第一个指针同时走,当第一个指针走到最后一位时一起停止
    3.这时候第二个指针还有 k 步没走,第二个指针当前的位置就是倒数的 k 的位置

    代码实现

    public class FindKthToTail {
    
        private class ListNode {
            int val;
            ListNode next;
            ListNode(int x) { val = x; }
        }
    
        public ListNode findKthToTail(ListNode head, int k) {
            if (head == null || k <= 0)
                return null;
    
            ListNode nodeP = head;
            ListNode nodeQ = head;
    
            // k - 1 是因为 遍历到 k - 2 下个节点 next 并不为 null 赋值后刚好是 k - 1位置
            for (int i = 0; i < k - 1; i++) {
                if (nodeP.next != null)
                    nodeP = nodeP.next;
                else
                    return null;
            }
    
            while (nodeP.next != null) {
                nodeP = nodeP.next;
                nodeQ = nodeQ.next;
            }
    
            return nodeQ;
        }
    }
    

    翻转链表

    输入: 1->2->3->4->5->NULL
    输出: 5->4->3->2->1->NULL

    解题思路

    1.创建一个 null 链表 pre
    2.每次把原链表 cur 的第一位放在pre 的第一位
    3.直到原链表 cur 为 null 时

    // 第一次
    cur: 2->3->4->5->NULL
    pre: 1->NULL
    // 第二次
    cur: 3->4->5->NULL
    pre: 2->1->NULL
    // 第三次
    cur: 4->5->NULL
    pre: 3->2->1->NULL
    // 第四次
    cur: 5->NULL
    pre: 4->3->2->1->NULL
    // 第五次
    cur: NULL
    pre: 5->4->3->2->1->NULL
    // 返回 pre
    

    代码实现

    public class ReverseList {
    
     public class ListNode {
       int val;
       ListNode next;
       ListNode(int x) { val = x; }
     }
    
      public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
    
        while (cur != null) {
          ListNode temp = cur.next; // 保留除 cur 第1位的链表 2 -> 3 -> 4 ,cur 还没变
          cur.next = pre; // cur.next 指向 pre 截断 cur 当前第1位 1 -> null, 此处截断原链表生成新链表
          pre = cur; // pre 变成 1 -> null
          cur = temp; // 去除 cur 第1位, cur 变成 2 -> 3 -> 4
        }
    
        return pre;
      }
    }
    

    合并两个排序的链表

    解题思路

    • 解法1 使用循环
      1.创建虚拟头 dummy
      2.dummy 引用赋值给 cur
      3.当 l1 l2 都不为 null 时循环
      4.比较 l1.val 和 l2.val 谁小谁接入到 cur.next 上,ln = ln.next 继续比较下一个
    1. cur = cur.next 同时向下准备介接入
    2. 当l1 或 l2 其中一个为空时 cur.next 为剩下不为空的链表
      7.最后返回 dummy 引用的 dummy.next
    • 解法2 使用递归
      1.基础问题:当其中一个链表为空时,剩下的节点肯定来自另一个链表
      2.基本问题:比较 l1.val 和 l2.val,谁小谁的 next 就递归比较其 next 节点与另一个链表的合并.

    代码实现

    public class MergeTwoSortedLists {
    
        public class ListNode {
            int val;
            ListNode next;
            ListNode(int x) { val = x; }
        }
    
        public ListNode mergeTwoLists1(ListNode l1, ListNode l2) {
            ListNode dummy = new ListNode(-1);
            ListNode cur = dummy;
    
            while (l1 != null && l2 != null) {
                if (l1.val < l2.val) {
                    cur.next = l1;
                    l1 = l1.next;
                } else {
                    cur.next = l2;
                    l2 = l2.next;
                }
                cur = cur.next;
            }
    
            cur.next = (l1 != null) ? l1 : l2;
            return dummy.next;
        }
    
        public ListNode mergeTwoLists2(ListNode l1, ListNode l2) {
            // 当其中一个链表为空时,剩下的节点肯定来自另一个链表
            if (l1 == null) return l2;
            if (l2 == null) return l1;
            // 开始递归
            if (l1.val < l2.val) { // l1 后面应该接节点
                l1.next = mergeTwoLists2(l1.next, l2); // 传入 l1.next l2, 决定 l1.next l2 如果合并
                return l1;
            } else { // l1.val >= l2.val
                l2.next = mergeTwoLists2(l1, l2.next);
                return l2;
            }
        }
    }
    

    树的子结构

    给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。

    解题思路

    1.创建 equals 函数,比较两个 TreeNode, 比较两个 TreeNode 的 val,如果相等递归比较 left 和 right 节点
    2.创建 traverse 函数,这个函数用来在 s 的每个节点中比较 t ,同时进行比较当前节点, 递归的在 s 的 left 和 right 里比较 t

    代码实现

    public class SubtreeOfAnotherTree {
    
        public class TreeNode {
            int val;
            TreeNode left;
            TreeNode right;
    
            TreeNode(int x) {
                val = x;
            }
        }
    
        public boolean isSubtree(TreeNode s, TreeNode t) {
            return traverse(s, t); // 此函数用来递归遍历树
        }
    
        // 这个函数用来在 s 的每个节点中比较 t ,同时进行比较当前节点
        private boolean traverse(TreeNode s, TreeNode t) {
            return s != null // 主树不为空
                    && (
                      equals(s, t) // 两个树一样
                        || traverse(s.left, t) // 从左子节点开始比较
                        || traverse(s.right, t) // 从右子节点开始比较
            );
        }
    
        private boolean equals(TreeNode x, TreeNode y) {
            if (x == null && y == null) // base 基础条件,比较到最后都没子节点了 true
                return true;
            if (x == null || y == null) // base 基础条件,还有子节点 false
                return false;
            // 递归实际比较
            return x.val == y.val && equals(x.left, y.left) && equals(x.right, y.right);
        }
    }
    

    react ssr 补充

    node server 中间层

    之前的代码在渲染时 node server 请求了一次 api 返回了数据插入到页面上,client 的代码在 componentDidMount 里也是请求了 api ,这次改造成 client 请求 node server,node server 再去请求 api,让 node server 只做代理而不是客户端还去请求外部服务。

    使用 express-http-proxy

    设置代理 client 请求本地服务 /api/news.json?secret=abcd 时代理到 http://47.95.113.63/ssr/api/news.json?secret=abcd

    import proxy from 'express-http-proxy';
    
    app.use('/api', proxy('http://47.95.113.63', {
      proxyReqPathResolver: function (req) {
        return '/ssr/api' + req.url;
      }
    }));
    

    区分 server client axios 请求的 baseURL

    • server client axios 配置
    // client
    import axios from 'axios';
    
    const instance = axios.create({
        baseURL: '/'
    });
    
    export default instance;
    
    // server
    import axios from 'axios';
    
    const createInstance = (req) => axios.create({
        baseURL: 'http://47.95.113.63/ssr',
        headers: {
            cookie: req.get('cookie') || ''
        }
    });
    
    export default createInstance;
    
    • 使用 thunk.withExtraArgument
    export const getStore = (req) => {
        // 改变服务器端store的内容,那么就一定要使用serverAxios
        return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req))));
    }
    
    export const getClientStore = () => {
        const defaultState = window.context.state;
        // 改变客户端store的内容,一定要使用clientAxios
        return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));
    }
    

    action 上就会增加额外的参数

    // action
    export const getHomeList = () => {
        return (dispatch, getState, axiosInstance) => {
            return axiosInstance.get('/api/news.json?secret=abcd')
                .then((res) => {
                    const list = res.data.data;
                    dispatch(changeList(list))
                });
        }
    } 
    
    • 或者使用 webpack.DefinePlugin 插件,给server 端代码增加环境变量
    // 通过插件传递的环境变量只在 server 打包的代码里使用
    const baseUrl = process.env.API_BASE || ''
    
    // webpack.server.js
      plugins: [
        new webpack.DefinePlugin({
          'process.env.API_BASE': '"http://127.0.0.1:3333"'
        })
      ]
    

    多层路由展示

    // Routes.js
    export default [{
      path: '/',
      component: App,
      loadData: App.loadData,
      routes: [
        { 
          path: '/',
          component: Home,
          exact: true,
          loadData: Home.loadData,
          key: 'home'
        }, { 
          path: '/translation',
          component: Translation,
          loadData: Translation.loadData,
          exact: true,
          key: 'translation'
        }
      ]
    }];
    
    import { renderRoutes } from 'react-router-config';
    
    // client
    const App = () => {
        return (
            <Provider store={store}>
                <BrowserRouter>
                    <div>
                        {renderRoutes(routes)}
                </div>
                </BrowserRouter>
            </Provider>
        )
    }
    
    // server
    export const render = (store, routes, req) => {
    
            const content = renderToString((
                <Provider store={store}>
                    <StaticRouter location={req.path} context={{}}>
                        <div>
                            {renderRoutes(routes)}
                    </div>
                    </StaticRouter>
                </Provider>
            ));
            // return ...
    }
    

    一级路由组件要渲染二级路由 routes 子路由数组

    routes: [
        { 
          path: '/',
          component: Home,
          exact: true,
          loadData: Home.loadData,
          key: 'home'
        }, { 
          path: '/translation',
          component: Translation,
          loadData: Translation.loadData,
          exact: true,
          key: 'translation'
        }
      ]
    

    一级路由组件渲染后 route 属性可以在 props 里获取到,在一级路由组件里使用 renderRoutes 渲染一级路由的 props.route.routes

    // 一级路由的组件
    // client server 端共用
    const App = (props) => {
        return (
            <div>
                <Header />
                {renderRoutes(props.route.routes)}
            </div>
        )
    }
    
    App.loadData = (store) => {
        return store.dispatch(actions.getHeaderInfo());
    }
    

    解决 cookie 携带问题

    当浏览器请求本地 node 服务时(携带 cookie)
    node server 进行服务端渲染转发浏览器的请求
    node server 请求 api server 获取数据(cookie 需要手动携带)
    express-http-proxy 中间件 只转发了请求路径,请求内容最终还是 axios 请求的。

    app.use('/api', proxy('http://47.95.113.63', {
      proxyReqPathResolver: function (req) {
        return '/ssr/api' + req.url;
      }
    }));
    

    通过把 express req 对象传递给 getStore 最后改造 createInstance 为高阶函数,这样cookie 就可以给 axios 获取cookie了

    app.get('*', function (req, res) {
        const store = getStore(req);
        // ..
    }
    export const getStore = (req) => {
        // 改变服务器端store的内容,那么就一定要使用serverAxios
        return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req))));
    }
    
    // axios 实例
    import axios from 'axios';
    
    const createInstance = (req) => axios.create({
        baseURL: 'http://47.95.113.63/ssr',
        headers: {
            cookie: req.get('cookie') || ''
        }
    });
    
    export default createInstance;
    

    生成404页面

    路由配置

    export default [{
      path: '/',
      component: App,
      loadData: App.loadData,
      routes: [
        { 
          path: '/',
          component: Home,
          exact: true,
          loadData: Home.loadData,
          key: 'home'
        }, { 
          path: '/translation',
          component: Translation,
          loadData: Translation.loadData,
          exact: true,
          key: 'translation'
        },
        {
          component: NotFound
        }
      ]
    }];
    

    解决请求返回的页面 status 404 返回

    在 server 使用 StaticRouter 传递 context, context 可以从 props.staticContext 中获取

    修改 server ssr 路由配置,给 render 函数传递 context

    app.get('*', function (req, res) {
        Promise.all(promises).then(() => {
            const context = {};
            const html = render(store, routes, req, context);
            res.send(html)
        })
    }
    

    给 render 函数增加 context 参数传递到 StaticRouter 组件中,等到 props.staticContext 使用

    
    export const render = (store, routes, req, context) => {
    
            const content = renderToString((
                <Provider store={store}>
                    <StaticRouter location={req.path} context={context}>
                        <div>
                            {renderRoutes(routes)}
                    </div>
                    </StaticRouter>
                </Provider>
            ));
            return `...`
    }
    

    修改 NotFound 页面, 当没有匹配路由访问到 NotFound 页面时,staticContext 增加 NOT_FOUND
    因为 server client 都会执行,client 不存在 staticContext 记得判断一下防止报错

    import React, { Component } from 'react';
    
    class NotFound extends Component {
    
        componentWillMount() {
            const { staticContext } = this.props;
            staticContext && (staticContext.NOT_FOUND = true);
        }
    
        render() {
            return <div>404, sorry, page not found</div>
        }
    
    }
    
    export default NotFound;
    

    这样我们可以通过判断 context.NOT_FOUND 来判断设置 status 404再返回页面

    app.get('*', function (req, res) {
        Promise.all(promises).then(() => {
            const context = {};
            const html = render(store, routes, req, context);
            if (context.NOT_FOUND) {
                res.status(404);
                res.send(html);
            }else {
                res.send(html);
            }
        })
    }
    

    实现 301 重定向

    没登录的情况下访问需要授权的页面 虽然 client 的逻辑重定向了,但是server 端代码没有重定向。

    // client 页面执行的 Redirect
        render() {
            return this.props.login ? (
                <div>
                    {this.getList()}
                </div>
            ) : <Redirect to='/'/>;
        }
    

    不过 renderRoutes 方法在发现 Redirect 组件执行时,就是在 server 上发现时会帮我们的 context 上塞一段数据用来判断

    // context: { url: '', action: '', location: { pathname: '', search: '', hash: '', state: undefined } }
    app.get('*', function (req, res) {
        Promise.all(promises).then(() => {
            const context = {};
            const html = render(store, routes, req, context);
            if (context.action === 'REPLACE') {
                res.redirect(301, context.url)
            }else if (context.NOT_FOUND) {
                res.status(404);
                res.send(html);
            }else {
                res.send(html);
            }
        })
    }
    

    这样就可以实现301了

    错误获取

    当 node server 代理的请求报错时页面就崩溃了,我们喜欢 node server 代理的请求有错时还是能返回没报错的数据同时把页面渲染出来

    app.get('*', function (req, res) {
        const store = getStore(req);
        const matchedRoutes = matchRoutes(routes, req.path);
        const promises = [];
        matchedRoutes.forEach(item => {
            if (item.route.loadData) {
                // 封装一个 promise,当 loadData catch 时也能让外层的 promise 返回 resolve 能继续执行,保证返回页面的 promise.all 能继续执行
                const promise = new Promise((resolve, reject) => {
                    item.route.loadData(store).then(resolve).catch(resolve);
                })
                promises.push(promise);
            }
        })
        Promise.all(promises).then(() => {
            const context = {};
            const html = render(store, routes, req, context);
            if (context.action === 'REPLACE') {
                res.redirect(301, context.url)
            }else if (context.NOT_FOUND) {
                res.status(404);
                res.send(html);
            }else {
                res.send(html);
            }
        })
    }
    

    处理 CSS

    server 打包 处理 css

    使用isomorphic-style-loader, 处理样式的 loader 需要 window 对象,server 端是没有 window 的,使用同构样式的 loader, 这里使用 modules 模式需要执行 JS 才有样式,会有白屏问题

            rules: [{
                test: /\.css?$/,
                use: ['isomorphic-style-loader', {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 1,
                        modules: true,
                        localIdentName: '[name]_[local]_[hash:base64:5]'
                    }
                }]
            }
    

    实现 ssr 样式

    • 创建传递 styles.getCss() 对象到 staticContext.css 里的高阶组件
    import React, { Component } from 'react';
    
    export default (DecoratedComponent, styles) => {
        
        return class NewComponent extends Component {
    
            componentWillMount() {
                if (this.props.staticContext) {
                    this.props.staticContext.css.push(styles._getCss());
                }
            }
    
            render() {
                return <DecoratedComponent {...this.props} />
            }
    
        }
    }
    
    • ssr 样式的页面使用高阶组件
      这里同时把 loadData 挂载到到处的页面对象上。
    const ExportHome = connect(mapStateToProps, mapDispatchToProps)(withStyle(Home, styles)); // 使用 withStyle
    
    ExportHome.loadData = (store) => {
        return store.dispatch(getHomeList())
    }
    
    export default ExportHome;
    
    • 最后在 server 里修改返回的模板
      通过传入的 staticContext 对象里找到 css 对象数组,转换成字符串后,插入到 head 里
    export const render = (store, routes, req, context) => {
    
            const content = renderToString((
                <Provider store={store}>
                    <StaticRouter location={req.path} context={context}>
                        <div>
                            {renderRoutes(routes)}
                    </div>
                    </StaticRouter>
                </Provider>
            ));
    
            const cssStr = context.css.length ? context.css.join('\n') : '';
    
            return `
                <html>
                    <head>
                        <title>ssr</title>
                        <style>${cssStr}</style>
                    </head>
                    <body>
                        <div id="root">${content}</div>
                        <script>
                            window.context = {
                                state: ${JSON.stringify(store.getState())}
                            }
                        </script>
                        <script src='/index.js'></script>
                    </body>
                </html>
          `;
        
    }
    

    SEO

    Title 和 Description

    <title> 标题
    <meta name="Description" content="搜索踹的简介">

    React-Helment

    • page 上使用
    // pages/home.js
    import { Helmet } from "react-helmet";
    
    class Home extends Component {
        render() {
            return (
                <Fragment>
                    <Helmet>
                        <title>新闻页面 - 丰富多彩的资讯</title>
                        <meta name="description" content="新闻页面 - 丰富多彩的资讯" />
                    </Helmet>
                    <div className={styles.container}>
                        {list}
                    </div>
                </Fragment>
            )
        }
        
    }
    
    • node server 上使用
      注意要在 react.renderToString() 后使用 const helmet = Helmet.renderStatic(); 来获取每个 page 中 react 组件设置的 Helmet 对象
    import { Helmet } from "react-helmet";
    
    export const render = (store, routes, req, context) => {
    
            const content = renderToString((
                <Provider store={store}>
                    <StaticRouter location={req.path} context={context}>
                        <div>
                            {renderRoutes(routes)}
                    </div>
                    </StaticRouter>
                </Provider>
            ));
            const helmet = Helmet.renderStatic();
    
            const cssStr = context.css.length ? context.css.join('\n') : '';
    
            return `
                <html>
                    <head>
                        ${helmet.title.toString()}
              ${helmet.meta.toString()}
                        <style>${cssStr}</style>
                    </head>
                    <body>
                        <div id="root">${content}</div>
                        <script>
                            window.context = {
                                state: ${JSON.stringify(store.getState())}
                            }
                        </script>
                        <script src='/index.js'></script>
                    </body>
                </html>
          `;
        
    }
    

    预渲染解决 SEO

    访问一个普通 react 项目链接,爬虫蜘蛛也访问项目链接
    可以通过识别爬虫蜘蛛访问的时候预渲染 dom 返回给爬虫。

    prerender 模块

    • 创建 prerender 服务器
      访问时加上查询参数 ?url=${url} 预渲染服务就会先去访问 url 再返回给爬虫
    const prerender = require('prerender');
    const server = prerender({
        port: 8000
    });
    server.start();
    
    • 使用 nginx 识别用户和爬虫
      nginx 识别爬虫就访问 prerender,识别是人就访问 react 项目

    • prerender 官网参考

    引用

    https://prerender.io/

    相关文章

      网友评论

          本文标题:周常3 算法题5道、react ssr 补充

          本文链接:https://www.haomeiwen.com/subject/ftrtrqtx.html