【深入浅出 Node + React 的微服务项目】11. 集成服务器端渲染的 React APP

集成服务器端渲染的 React APP

Starting the React App

在这里插入图片描述

⬆ back to top

客户端渲染和服务端渲染

客户端渲染
在这里插入图片描述
服务端渲染

在这里插入图片描述

⬆ back to top

Next JS 基础知识

  • install react, react-dom, next
  • create pages folder and add page components
    • next 在第一次启动的时候,会遍历一遍 page 的文件,其中 index 会被作为根路由,而其他同级文件和文件夹将一次作为 / 的路由
  • npm run dev

⬆ back to top

构建 Next 镜像

FROM node:alpine

WORKDIR /app
COPY package.json .
RUN npm install
COPY . .

CMD ["npm", "run", "dev"]
docker build -t heysirius/client .
docker push heysirius/client

⬆ back to top

在 Kubernetes 中运行 Next

# client-depl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-depl
spec:
  replicas: 1
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      containers:
        - name: client
          image: heysirius/client
---
apiVersion: v1
kind: Service
metadata:
  name: client-srv
spec:
  selector:
    app: client
  ports:
    - name: client
      protocol: TCP
      port: 3000
      targetPort: 3000
# skaffold.yaml
apiVersion: skaffold/v2alpha3
kind: Config
deploy:
  kubectl:
    manifests:
      - ./infra/k8s/*
build:
  local:
    push: false
  artifacts:
    - image: heysirius/auth
      context: auth
      docker:
        dockerfile: Dockerfile
      sync:
        manual:
          - src: 'src/**/*.ts'
            dest: .
    - image: heysirius/client
      context: client
      docker:
        dockerfile: Dockerfile
      sync:
        manual:
          - src: '**/*.js'
            dest: .
# ingress-srv.yaml.old
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-service
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/use-regex: 'true'
spec:
  rules:
    - host: ticketing.dev
      http:
        paths:
          - path: /api/users/?(.*)
            backend:
              serviceName: auth-srv
              servicePort: 3000
          - path: /?(.*)
            backend:
              serviceName: client-srv
              servicePort: 3000
# ingress-srv.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-service
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/use-regex: 'true'
spec:
  rules:
    - host: ticketing.dev
      http:
        paths:
 		  - path: /?(.*)
            pathType: Prefix
            backend:
              service:
                name: client-srv
                port:
                  number: 3000
skaffold dev
  • Goto chrome - https://ticketing.dev/
  • Type ‘thisisunsafe’

⬆ back to top

文件更改检测

// next.config.js
module.exports = {
  webpackDevMiddleware: config => {
    config.watchOptons.poll = 300;
    return config;
  }
};
kubectl get pods
kubectl delete pod client-depl-b955695bf-8ws8j
kubectl get pods

⬆ back to top

添加全局 CSS

Global CSS Must Be in Your Custom《 App》

// _app.js
import 'bootstrap/dist/css/bootstrap.css';

export default ({ Component, pageProps }) => {
  return <Component {...pageProps} />;
};

⬆ back to top

新增注册的表单

// signup.js
export default () => {
  return (
    <form>
      <h1>Sign Up</h1>
      <div className="form-group">
        <label>Email Address</label>
        <input className="form-control" />
      </div>
      <div className="form-group">
        <label>Password</label>
        <input type="password" className="form-control" />
      </div>
      <button className="btn btn-primary">Sign Up</button>
    </form>
  );
};

⬆ back to top

处理 Email 和 Password 输入

// signup.js
import { useState } from 'react';

export default () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const onSubmit = event => {
    event.preventDefault();

    console.log(email, password);
  };

  return (
    <form onSubmit={onSubmit}>
      <h1>Sign Up</h1>
      <div className="form-group">
        <label>Email Address</label>
        <input
          value={email}
          onChange={e => setEmail(e.target.value)}
          className="form-control"
        />
      </div>
      <div className="form-group">
        <label>Password</label>
        <input
          value={password}
          onChange={e => setPassword(e.target.value)}
          type="password"
          className="form-control"
        />
      </div>
      <button className="btn btn-primary">Sign Up</button>
    </form>
  );
};

⬆ back to top

Successful Account Signup

在这里插入图片描述

// signup.js
const onSubmit = async event => {
  event.preventDefault();

  const response = await axios.post('/api/users/signup', {
    email,
    password
  });

  console.log(response.data);
};

⬆ back to top

处理登录 Errors

// signup.js
const [errors, setErrors] = useState([]);

const onSubmit = async event => {
  event.preventDefault();

  try {
    const response = await axios.post('/api/users/signup', {
      email,
      password
    });

    console.log(response.data);
  } catch (err) {
    setErrors(err.response.data.errors);
  }
};

⬆ back to top

The useRequest Hook

在这里插入图片描述

// use-request.js
import axios from 'axios';
import { useState } from 'react';

export default ({ url, method, body }) => {
  const [errors, setErrors] = useState(null);

  const doRequest = async () => {
    try {
      setErrors(null);
      const response = await axios[method](url, body);
      return response.data;
    } catch (err) {
      setErrors(
        <div className="alert alert-danger">
          <h4>Ooops....</h4>
          <ul className="my-0">
            {err.response.data.errors.map(err => (
              <li key={err.message}>{err.message}</li>
            ))}
          </ul>
        </div>
      );
    }
  };

  return { doRequest, errors };
};

⬆ back to top

使用 useRequest Hook

// signup.js
const { doRequest, errors } = useRequest({
  url: '/api/users/signup',
  method: 'post',
  body: {
    email, password
  }
})

const onSubmit = async event => {
  event.preventDefault();

  doRequest();
};

⬆ back to top

新增 onSuccess Callback

// use-request.js
import axios from 'axios';
import { useState } from 'react';

export default ({ url, method, body, onSuccess }) => {
  const [errors, setErrors] = useState(null);

  const doRequest = async () => {
    try {
      setErrors(null);
      const response = await axios[method](url, body);

      if (onSuccess) {
        onSuccess(response.data);
      }

      return response.data;
    } catch (err) {
      setErrors(
        <div className="alert alert-danger">
          <h4>Ooops....</h4>
          <ul className="my-0">
            {err.response.data.errors.map(err => (
              <li key={err.message}>{err.message}</li>
            ))}
          </ul>
        </div>
      );
    }
  };

  return { doRequest, errors };
};
// signup.js
const { doRequest, errors } = useRequest({
  url: '/api/users/signup',
  method: 'post',
  body: {
    email, password
  },
  onSuccess: () => Router.push('/')
})

const onSubmit = async event => {
  event.preventDefault();

  await doRequest();
};

⬆ back to top

服务器端渲染的简介

  • 用户请求的时候,只返回完全渲染好了的 html
    在这里插入图片描述

  • nextjs 客户端
    在这里插入图片描述

// index.js
const LandingPage = ({ color }) => {
  console.log('I am in the component', color);
  return <h1>Landing Page</h1>;
};

LandingPage.getInitialProps = () => {
  console.log('I am on the server!');

  return { color: 'red' };
};

export default LandingPage;

⬆ back to top

在 SSR 期间获取数据

  • 如果写在 component 的话,就是在浏览器里面获取的,不是服务端
LandingPage.getInitialProps = async () => {
  const response = await axios.get('/api/users/currentuser');

  return response.data;
};

在这里插入图片描述

⬆ back to top

为什么会报错?

  • ssr Request from browser
const LandingPage = ({ currentUser }) => {
  console.log(currentUser);
  axios.get('/api/users/currentuser');

  return <h1>Landing Page</h1>;
};

在这里插入图片描述

  • ssr Request from server
LandingPage.getInitialProps = async () => {
  const response = await axios.get('/api/users/currentuser');

  return response.data;
};

在这里插入图片描述
在这里插入图片描述

  • 服务端渲染的时候,因为没在xx域名下,所以请求的路由自动转为 localhost
  • 在 k8s 中,每个container都有localhost,所以请求的路由请求的其实是 client container 该 port 的服务
  • 并没请求到 Auth Service,所以才会报 127.0.0.1:80 的错

在这里插入图片描述

⬆ back to top

两种解决方案

  1. 发送到 ingress 里面,ingress 帮忙转发
  2. 直接发送给 单独的 Pod 的 srv
    在这里插入图片描述
  • 不管是直接发 还是 间接发 都需要带上 cookie

在这里插入图片描述

⬆ back to top

跨 namespace 的交流

这里用方案一 直接发给 ingress nginx 更好,所以采取 Request Option #1

kubectl get services -n ingress-nginx
kubectl get namespace
  • service.namespace.svc.cluster.local
  • http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

⬆ back to top

getInitialProps 什么时候被调用?

Request from a componentRequest from getInitialProps
总是从浏览器中发出,所以直接默认带上域名有可能是浏览器和服务器,所以需要进行环境的判断

在这里插入图片描述

⬆ back to top

判断在 server 还是在 browser

LandingPage.getInitialProps = async () => {
  if(typeof window === 'undefined') {
    // we are on the server!
    // requests should be made to http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
  } else {
    // we are on the browser!
    // requests should be made with a base url of ''
  }
  return {};
};

⬆ back to top

记得带上 host

kubectl get services -n ingress-nginx
kubectl get namespace
  • service.namespace.svc.cluster.local
  • http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
LandingPage.getInitialProps = async () => {
  if(typeof window === 'undefined') {
    // we are on the server!
    // requests should be made to http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
    const { data } = await axios.get(
      'http://ingress-nginx-controller.ingress-nginx.svc.cluster.local/api/users/currentuser',
      {
        headers: {
          Host: 'ticketing.dev'
        }
      }
    );

    return data;
  } else {
    // we are on the browser!
    // requests should be made with a base url of ''
    const { data } = await axios.get('/api/users/currentuser');
    
    return data;
  }
  return {};
};

⬆ back to top

带上 Cookies

  • 所有的会话信息都在 header.cookie 里
  • 而 host 信息其实也在 header 里
  • 所以直接换成带 header

在这里插入图片描述
在这里插入图片描述

LandingPage.getInitialProps = async ({ req }) => {
  if(typeof window === 'undefined') {
    // we are on the server!
    // requests should be made to http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
    const { data } = await axios.get(
      'http://ingress-nginx-controller.ingress-nginx.svc.cluster.local/api/users/currentuser',
      {
        headers: req.headers
      }
    );

    return data;
  } else { ... }
  return {};
};

⬆ back to top

可复用的 SSR getInitialProps 请求

在这里插入图片描述

// build-client.js
import axios from 'axios';

export default ({ req }) => {
  if(typeof window === 'undefined') {
    // we are on the server

    return axios.create({
      baseURL: 'http://ingress-nginx-controller.ingress-nginx.svc.cluster.local',
      headers: req.headers
    });
  } else {
    // we are on the browser

    return axios.create({
      baseURL: ''
    });
  }
};
// index.js
LandingPage.getInitialProps = async (context) => {
  const client = buildClient(context);
  const { data } = await client.get('/api/users/currentuser');

  return data;
};

⬆ back to top

登陆页面上的内容

const LandingPage = ({ currentUser }) => {
  return currentUser ? (
    <h1>You are signed in</h1>
  ) : (
    <h1>You are NOT signed in</h1>
  );
};

⬆ back to top

登录的表单

// signin.js
import { useState, useEffect } from 'react';
import Router from 'next/router';
import useRequest from '../../hooks/use-request';

export default () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { doRequest, errors } = useRequest({
    url: '/api/users/signin',
    method: 'post',
    body: {
      email,
      password
    },
    onSuccess: () => Router.push('/')
  });

  const onSubmit = async event => {
    event.preventDefault();

    await doRequest();
  };

  return (
    <form onSubmit={onSubmit}>
      <h1>Sign In</h1>
      <div className="form-group">
        <label>Email Address</label>
        <input
          value={email}
          onChange={e => setEmail(e.target.value)}
          className="form-control"
        />
      </div>
      <div className="form-group">
        <label>Password</label>
        <input
          value={password}
          onChange={e => setPassword(e.target.value)}
          type="password"
          className="form-control"
        />
      </div>
      {errors}
      <button className="btn btn-primary">Sign In</button>
    </form>
  );
};

⬆ back to top

可复用的 Header

// _app.js
import 'bootstrap/dist/css/bootstrap.css';

export default ({ Component, pageProps }) => {
  return (
    <div>
      <h1>Header!</h1>
      <Component {...pageProps} />
    </div>
  );
};

⬆ back to top

增加 GetInitialProps

// _app.js
import 'bootstrap/dist/css/bootstrap.css';
import buildClient from '../api/build-client';

const AppComponent = ({ Component, pageProps }) => {
  return (
    <div>
      <h1>Header!</h1>
      <Component {...pageProps} />
    </div>
  );
};

AppComponent.getInitialProps = () => {};

export default AppComponent;

⬆ back to top

自定义 Custom APP GetInitialProps 的问题

  • 不同 component 的 getInitialProps 是不一样的

  • 因为我们自定义 APP component 下会包裹 page component

  • 所以 nextjs 的 getInitialProps 会有不同参数类型
    在这里插入图片描述
    在这里插入图片描述

  • 在添加 AppComponent.getInitialProps 的时候。LandingPage.getInitialProps不会被调用

AppComponent.getInitialProps = async appContext => {
  const client = buildClient(appContext.ctx);
  const { data } = await client.get('/api/users/currentuser');

  return data;
};

⬆ back to top

处理多个 GetInitialProps

在这里插入图片描述
在这里插入图片描述

AppComponent.getInitialProps = async appContext => {
  const client = buildClient(appContext.ctx);
  const { data } = await client.get('/api/users/currentuser');

  let pageProps = {};
  if(appContext.Component.getInitialProps) {
    pageProps = await appContext.Component.getInitialProps(appContext.ctx);
  }

  console.log(pageProps);

  return data;
};

⬆ back to top

传递 Props

import 'bootstrap/dist/css/bootstrap.css';
import buildClient from '../api/build-client';

const AppComponent = ({ Component, pageProps, currentUser }) => {
  return (
    <div>
      <h1>Header! {currentUser.email} </h1>
      <Component {...pageProps} />
    </div>
  );
};

AppComponent.getInitialProps = async appContext => {
  const client = buildClient(appContext.ctx);
  const { data } = await client.get('/api/users/currentuser');

  let pageProps = {};
  if(appContext.Component.getInitialProps) {
    pageProps = await appContext.Component.getInitialProps(appContext.ctx);
  }

  return {
    pageProps,
    ...data
  };
};

export default AppComponent;

⬆ back to top

构建 Header

// header.js
import Link from 'next/link';

export default ({ currentUser }) => {
  return (
    <nav className="navbar navbar-light bg-light">
      <Link href="/">
        <a className="navbar-brand">GitTix</a>
      </Link>

      <div className="d-flex justify-content-end">
        <ul className="nav d-flex align-items-center">
          {currentUser ? 'Sign out' : 'Sign in/up'}
        </ul>
      </div>
    </nav>
  );
};

⬆ back to top

header 中根据条件分配 Link

import Link from 'next/link';

export default ({ currentUser }) => {
  const links = [
    !currentUser && { label: 'Sign Up', href: '/auth/signup' },
    !currentUser && { label: 'Sign In', href: '/auth/signin' },
    currentUser && { label: 'Sign Out', href: '/auth/signout' }
  ]
    .filter(linkConfig => linkConfig)
    .map(({ label, href }) => {
      return (
        <li key={href} className="nav-item">
          <Link href={href}>
            <a className="nav-link">{label}</a>
          </Link>
        </li>
      );
    });

  return (
    <nav className="navbar navbar-light bg-light">
      <Link href="/">
        <a className="navbar-brand">GitTix</a>
      </Link>

      <div className="d-flex justify-content-end">
        <ul className="nav d-flex align-items-center">{links}</ul>
      </div>
    </nav>
  );
};

⬆ back to top

登出

import { useEffect } from 'react';
import Router from 'next/router';
import useRequest from '../../hooks/use-request';

export default () => {
  const { doRequest } = useRequest({
    url: '/api/users/signout',
    method: 'post',
    body: {},
    onSuccess: () => Router.push('/')
  });

  useEffect(() => {
    doRequest();
  }, []);

  return <div>Signing you out...</div>;
};

⬆ back to top

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
假设你的 Java 服务端代码监听在 `http://localhost:3000` 上,下面是一个使用 `socket.io-client` 连接 Java 服务端的前端代码示例: ```jsx import React, { useEffect, useState } from 'react'; import io from 'socket.io-client'; const SOCKET_SERVER_URL = 'http://localhost:3000'; // Java 服务端地址 function App() { const [messages, setMessages] = useState([]); useEffect(() => { const socket = io(SOCKET_SERVER_URL); // 监听来自 Java 服务端的消息 socket.on('message', (message) => { setMessages((messages) => [...messages, message]); }); // 在组件销毁时断开连接 return () => { socket.disconnect(); }; }, []); return ( <div> <h1>Messages:</h1> <ul> {messages.map((message, index) => ( <li key={index}>{message}</li> ))} </ul> </div> ); } export default App; ``` 在上述代码中,我们先通过 `import` 语句导入了 `socket.io-client` 库,然后在组件中创建了一个 `socket` 对象,通过调用 `io()` 方法并传入 Java 服务端的地址来连接服务端。在 `useEffect()` 钩子函数中,我们监听了来自服务端的 `message` 事件,并将消息添加到 `messages` 数组中。在组件销毁时,我们调用 `socket.disconnect()` 方法断开与 Java 服务端的连接。最后,我们在组件的 JSX 中渲染了消息列表。 需要注意的是,Java 服务端必须使用 `socket.io` 库来与前端建立连接,否则无法使用 `socket.io-client` 连接。如果你的 Java 服务端没有使用 `socket.io` 库,可以参考 `socket.io` 官方文档来实现 Java 版本的 `socket.io` 服务端。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值