iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
0
Software Development

線上娃娃機-js開發篇系列 第 5

線上娃娃機-前端功能介紹

這次第一版方便快速開發選擇了 Material-Ui 套件來當作佈景主題開發 Next.js 為主軸框架 支援SSR

https://material-ui.com/

https://nextjs.org/

前端會用到打 api的部分 使用了 apollo client

https://www.apollographql.com/

next.js 目前新版都有支援自動產出tsconfig 設定檔 只要他偵測規定目錄中 有ts檔案就會幫你自動設定好好的

佈景主題部分設定
在pages 目錄底下 新增 _documnet.tsx 他會覆蓋原來的既有的 _document.tsx檔案
因為要使用material Ui 主要在 SSR 時候 ,使用getInitialProps 去要到第一次需要的值 確保class與前端render出來的class一致保持正確性

pages/_documnet.tsx

import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '@material-ui/core/styles';
import theme from '../src/theme';

export default class MyDocument extends Document {
    render() {
        return (
            <Html lang="zh-TW">
                <Head>
                    {/* PWA primary color */}
                    <meta name="theme-color" content={theme.palette.primary.main} />
                    <link
                        rel="stylesheet"
                        href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
                    />
                </Head>
                <body>
                    <Main />
                    <NextScript />
                    <script type="text/javascript" src="/workfile/jsmpeg.min.js"></script>
                    <script src="https://js.tappaysdk.com/tpdirect/v5.4.0"></script>
                    <script src="https://pay.google.com/gp/p/js/pay.js"></script>

                </body>
            </Html>
        );
    }
}
 
MyDocument.getInitialProps = async (ctx) => {
    const sheets = new ServerStyleSheets();
    const originalRenderPage = ctx.renderPage;
    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
        });

    const initialProps = await Document.getInitialProps(ctx);

    return {
        ...initialProps,
         styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
    };
};

在pages/_app.tsx中設定 ApolloProvider

export const PagePermissionContext = React.createContext({ pagePermission: {} })
interface Props { apolloClient: ApolloClient<any> }
const MyApp = (props: any) => {
    const { Component, apolloClient,   } = props
    return (
        <ApolloProvider client={apolloClient}>
                 <CssBaseline />
                    <Layout ><Component /></Layout>             
         </ApolloProvider>
    )
}
export default withApollo(MyApp)
 

Next.js 中設定 ApolloClient 與 SubscriptionClient
這個部分有點繁雜 因為Next.js有SSR 要同時考慮到 呼叫時是前後端哪種模式
設定檔官方也有提供範例 可以參考,主要是httpLink他是結合很多midddleware 這部分比較繁瑣可以參考以下程式
後面章節再來細說運行內容


import { ApolloClient } from 'apollo-client'
import { setContext } from 'apollo-link-context'
import fetch from 'isomorphic-unfetch'
import { createUploadLink } from 'apollo-upload-client'
import config from '../config'
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from 'apollo-link-ws';
import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import ws from 'ws';
let apolloClient: any = null
const WS_URI = `wss://clawfun.online/graphql`;
const cache = new InMemoryCache();
if (!process.browser) { global["fetch"] = fetch }
const ssrMode = (typeof window === 'undefined');
function create(initialState: any, { getToken }: { getToken: any }) {
  const token = getToken()
  let link: any, linkServer: any, linkClient: any, wsLink: any
  const httpLink = createUploadLink({ uri: `${config.serverURL()}/graphql`, credentials: 'include' })
  const authLink = setContext((_, { headers }) => { const token = getToken(); return { headers: { ...headers, authorization: token ? `Bearer ${token}` : '' } } })
   try {
    if (!ssrMode) {  //client part
      wsLink = new SubscriptionClient(WS_URI, { reconnect: true, connectionParams: { headers: { authorization: token ? `Bearer ${token}` : '' } } });

      linkClient = split(({ query }) => {
        const definition = getMainDefinition(query);
        return (definition.kind === 'OperationDefinition' && definition.operation === 'subscription');
      }, wsLink, httpLink); console.log('linkClient');

    } else {
      linkServer = authLink.concat(httpLink); console.log('linkServer');
    }
  } catch (err) { console.log('err', err);  }


  const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    link: ssrMode ? linkServer : linkClient,
    cache: new InMemoryCache({ dataIdFromObject: (object: any) => object["key"] || null }).restore(initialState || {})
  })

  return client
}

export default function initApollo(initialState: any, options: any) {
  if (!ssrMode) { return create(initialState, options) }
  if (!apolloClient) { apolloClient = create(initialState, options) }
  return apolloClient
}

前端讀取娃娃機的影像程式片段 roomCamera 元件

使用套件 為 https://github.com/phoboslab/jsmpeg
使用要先在 _document設定 讓他append在window底下

     <script type="text/javascript" src="/workfile/jsmpeg.min.js"></script>

這邊在 useLayoutEffect 新增一個 window.JSMpeg.Player 物件 + return時可以釋放資源 請參考下面程式碼

    const WEBSITE_STREAM_HOST = process.env.WEBSITE_STREAM_HOST ? process.env.WEBSITE_STREAM_HOST : '192.168.0.27'
    function onStreamReady() { return { firstFrame: true } }
    var player1: any; var player2: any;
    useLayoutEffect(() => {
        let canvas1 = document.getElementById('video-canvas1'); let canvas2 = document.getElementById('video-canvas2');
        let urlBack = rtmpUrl1;
        let urlFront = rtmpUrl2;
        player1 = new window.JSMpeg.Player(urlBack, { canvas: canvas1, onStreamReady });
        player2 = new window.JSMpeg.Player(urlFront, { canvas: canvas2, onStreamReady });
        player1.play(); player2.play();
        return () => { try { player1 = null; player2 = null; } catch (err) { console.log('err', err) } }
    }, []);

Apollo Subscription 這邊使用hook useSubscription 監聽server傳來的資料.配合useState server資料傳來事後再重新render

以下片段是監聽server 例如A玩家發出投幣請求,機器後端驗證無誤就會發改變通知到前端,並且帶uuid回來如果是正在玩的玩家就設定開局,並改變遊玩會用到的參數state, 其他人就設定基本無遊玩 並通知目前有多少人在正在排隊
自己如果有在排隊序列中 也會告知排在第幾個

大致寫法如下 定義一組 Subscription Gql

export const machineRefreshSubscriptionGql = gql`subscription machineRefresh($machineId:Int){machineRefresh(machineId:$machineId){    
    currentMemberUuid, machineId ,machineStatus,
    machineMemberQueue{
        machine{id }
        member{id,uuid}             
    }
}}`

透過socket 在娃娃機房間的每個人 依照狀況改變state 觸發 render


  useSubscription(machineRefreshSubscriptionGql, {     
        variables: { machineId }, onSubscriptionData: ({ client, subscriptionData: { data } }) => {
            if (data?.machineRefresh?.currentMemberUuid === uuid) {
                setRoomState({ ...roomState, isPlayer: true, machineStatus: data?.machineRefresh?.machineStatus, queueList: data?.machineRefresh?.machineMemberQueue })
            } else {
                setRoomState({ ...roomState, isPlayer: false, machineStatus: data?.machineRefresh?.machineStatus, queueList: data?.machineRefresh?.machineMemberQueue })
            }
        }
    })

前端登入功能 , 以下是判斷是否有無登入的graph document

export const meQuery = gql`
query meQuery{
  meQuery{ 
    id     
    name    
    account
    picture
    email
    uuid
    ....略
  }

因為不需要每次都判斷是否已經登入 ,判斷一次後就cache狀況 所以 fetchPolicy設定cache-and-network
_app是初始 apollo地方 而判斷切入點放在layout

<ApolloProvider client={apolloClient}>
            <ThemeProvider theme={theme}>
                <CssBaseline />
                   <Layout assignLang={assignLang} ><Component /></Layout>              
            </ThemeProvider>
        </ApolloProvider>
const LayoutTemplate = (props: any) => {
    const result = useQuery(meQuery, { fetchPolicy: 'cache-and-network' })
    const { loading, error, data } = result;
    if (loading) { return <div>loading</div> };
    if (error) { return <div>error</div> };
    const loginInfo = data['meQuery']
    return <MainContainer   />
}

而fb登入與google登入部分則直接對應express api 對應 passport使用

後端express api

    app.get('/fblogin', passport.authenticate('facebook', { scope: 'email' }))

以上介紹了 前端會用到的功能 主要包括
1.apollo client 設定
2.wss://讀取影像串流 jsmpeg 設定
3.apollo Subscription 設定
4.登入介紹


上一篇
線上娃娃機-後端功能
下一篇
Next.js 9.x
系列文
線上娃娃機-js開發篇11
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言