iT邦幫忙

2024 iThome 鐵人賽

DAY 16
2
Software Development

透過 nestjs 框架,讓 nodejs 系統維護度增加系列 第 28

nestjs 系統設計 - 活動訂票管理系統 - client code part 2

  • 分享至 

  • xImage
  •  

nestjs 系統設計 - 活動訂票管理系統 - client code part 2

目標

昨天完成了登入後票倦的 layout。今天目標是建立登入頁面的 layout。

概念

因為使用的 ReactNative,所基本上都是要使用 component 的處理方式。為了能夠方便搭建大的元件,會從一些功能型元件,比如說自製的對齊元件 Stack,或是自行包裝的 Text 等等元件來作處理。由小而大的搭建元件。

今日最終目標

image

這個登入頁面,可以分成以下幾種屬性:

  1. 不具有互動性的 TextView: 只是單純的畫面展示
  2. 具有互動性 InputView: 具有資料狀態轉移
  3. 具有互動性 Button: 具有事件觸發與資料狀態轉移
  4. 具有互動性 TextView: 具有事件觸發與資料狀態轉移

在今天的實做中,先以沒有狀態的元件作處理。然後在透過一些條件式,來組成可以有狀態的元件。

用來組裝對齊元件的 Stack

import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { PropsWithChildren } from 'react';
import { ViewProps, View } from 'react-native';


export interface StackProps extends PropsWithChildren, ShortcutProps, ViewProps {
  flex?: number
  direction?: 'row'|'column'
  gap?: number
  alignItems?: 'flex-start'|'flex-end'|'center'|'stretch'|'baseline'
  justifyContent?: 'flex-start'|'flex-end'|'center'|'space-between'|'space-around'|'space-evenly'
}
export function Stack({
  flex,
  direction,
  gap,
  alignItems,
  justifyContent,
  children,
  style,
  ...restProps
}: StackProps)  {
  return (
    <View style={[defaultShortcuts(restProps), {
      flex,
      flexDirection: direction,
      gap,
      alignItems,
      justifyContent,
    },style]} {...restProps} >
      {children}
    </View>
  )
}

這個 Stack 單純就是用來作對齊的元件。

VStack 與 HStack

VStack 是用來作垂直對齊的 View,可以使用 Stack 來建構如下

import { Stack, StackProps } from './Stack';

interface VStackProps extends StackProps {}
export function VStack(props: VStackProps) {
  return (
    <Stack {...props} direction='column'/>
  )
}

HStack 是用來作水平對齊的 View,可以使用 Stack 來建構如下

import { Stack, StackProps } from './Stack';

interface HStackProps extends StackProps {}
export function HStack(props: HStackProps) {
  return (
    <Stack {...props} direction='row'/>
  )
}

Login layout 修改新增標題

import { HStack } from '@/components/HStack';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { VStack } from '@/components/VStack';
import { KeyboardAvoidingView, ScrollView } from 'react-native';

export default function Login() {
 
  return (
    <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
      <ScrollView contentContainerStyle={{flex: 1}}>
        <VStack flex={1} justifyContent='center' alignItems='center' p={40} gap={40}>
          <HStack gap={10}>
            <Text fontSize={30} bold mb={20}>Ticket Booking</Text>
            <TabBarIcon name='ticket' size={50}/>
          </HStack>
        </VStack>  
      </ScrollView>
    </KeyboardAvoidingView>
  );
}

調整狀態讓預設為非登入狀態

import { Redirect, Stack } from 'expo-router';

export default function AppLayout() {
  // check from context if user is logged in
  const isLogggedIn = false;
  if (!isLogggedIn) {
    return <Redirect href="/login" />
  }
  return <Stack screenOptions={{ headerShown: false }} />
}

當下結果就會如下:

image

TextView 實做

實做 TextView

import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { PropsWithChildren } from 'react';
import { TextProps, Text as RNText } from 'react-native';
interface CustomTextProps extends PropsWithChildren, ShortcutProps, TextProps {
  fontSize?: number;
  bold?: boolean;
  underline?: boolean;
  color?: string;
}
export function Text({
  fontSize = 18,
  bold,
  underline,
  color,
  children,
  style,
  ...restProps
}: CustomTextProps) {
  return (
    <RNText style={[defaultShortcuts(restProps), {
      fontSize,
      fontWeight: bold? 'bold': 'normal',
      textDecorationLine: underline? 'underline': 'none',
      color, 
    }, style]}
      {...restProps}
    >
      {children}
    </RNText>
  )
}

透過客制化的 TextView 去新增屬性,並且加入縮寫的轉換元件

InputView 實做

InputView 實做如下

import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { TextInput, TextInputProps } from 'react-native';

interface InputProps extends ShortcutProps, TextInputProps {

}
export function Input(props: InputProps) {
  return (
    <TextInput 
      style={[defaultShortcuts(props), {
        fontSize: 16,
        borderRadius: 16,
        backgroundColor: 'lightgray',
        color: 'black'
      }]}
      {...props}
    />
  )
}

這個 InputView 加入了客製化的 ShortcutProps ,讓開發人員可以透過簡寫的方式使用,並且設定好預設的樣式。

透過上面兩個 ViewComponent 就可以組合出 email 跟 password 欄位如下

在 login.tsx 加入 email 與 password 欄位,如下:

<VStack w={'100%'} gap={30}>
<VStack gap={5}>
  <Text ml={10} fontSize={14} color='gray'>Email</Text>
  <Input 
	value={email}
	onChangeText={setEmail}
	placeholder='Email'
	placeholderTextColor='darkgray'
	autoCapitalize='none'
	autoCorrect={false}
	h={48}
	p={14}
  />
</VStack>
<VStack gap={5}>
  <Text ml={10} fontSize={14} color='gray'>Password</Text>
  <Input
	secureTextEntry
	value={password}
	onChangeText={setPassword}
	placeholder='Password'
	placeholderTextColor='darkgray'
	autoCapitalize='none'
	autoCorrect={false}
	h={48}
	p={14}
  />
</VStack>
</VStack>

並且加入 react hook 來控制狀態,使用 useState 來處理。如下

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

結果如下:
基本 layout:
image
輸入後如下
image

製作 Button 元件

import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { ActivityIndicator, StyleSheet, TouchableOpacity, TouchableOpacityProps } from 'react-native';
import { Text } from './Text';
interface ButtonProps extends ShortcutProps, TouchableOpacityProps {
  variant?:'contained'|'outlined'|'ghost',
  isLoading?: boolean,
}
export function Button({
  onPress,
  children,
  variant = 'contained',
  isLoading,
  ...restProp
}: ButtonProps) {
  return (
    <TouchableOpacity
      disabled={isLoading}
      onPress={onPress}
      style={[
        defaultShortcuts(restProp),
        styles[variant].button,
        isLoading && disabled.button
      ]}
      {...restProp}
    >
      {isLoading?
        <ActivityIndicator animating size={22}/>:
        <Text style={[styles[variant].text]}>{children}</Text>
      }
    </TouchableOpacity>
  )
}
const styles = {
  contained: StyleSheet.create({
    button: {
      padding: 14,
      borderRadius: 50,
      backgroundColor: 'black'
    },
    text: {
      textAlign: 'center',
      color: 'white',
      fontSize: 18,
    }
  }),
  outlined: StyleSheet.create({
    button: {
      padding: 14,
      borderRadius: 50,
      borderColor: 'darkgray',
      borderWidth: 1,
    },
    text: {
      textAlign: 'center',
      color: 'black',
      fontSize: 18,
    }
  }),
  ghost: StyleSheet.create({
    button: {
      padding: 14,
      borderRadius: 50,
      backgroundColor: 'transparent'
    },
    text: {
      textAlign: 'center',
      color: 'black',
      fontSize: 18,
    }
  })
}
const disabled = StyleSheet.create({
  button: {
    opacity: 0.5
  }
});

上面的寫法,是把 Button 包裝成樣式元件,樣式會根據傳入的狀態而變更。

Divider 元件實做

import { defaultShortcuts, ShortcutProps } from '@/styles/shortcuts';
import { View } from 'react-native';

interface DividerProps extends ShortcutProps {}
export function Divider(props: DividerProps) {
  return (
    <View
      style={[
        defaultShortcuts(props),
        {
          backgroundColor: 'lightgray',
          height: 1
        },
      ]}
    />
  )
}

這個元件只是單一的分隔線,用簡單的 View 即可。

透過 Button , Divider , Text 元件完成剩下的 layout

在 login.tsx 加入以下元件

<Button
	w={'100%'}
	isLoading={false} //TODO: finished once we have authenticated provider
	onPress={() =>{ }} //TODO: finished once we have authenticated provider
	>
	{authMode === 'login'? 'Login': 'Register'}
</Button>
<Divider w={'90%'}/>
<Text onPress={onToggleAuthMode} fontSize={16} underline>
	{authMode === 'login'? 'Register new account': 'Login to account'}
</Text>

並且設定以下控制函數

 const [authMode, setAuthMode] = useState<'login'|'register'>('login');
 function onToggleAuthMode() {
    setAuthMode(authMode === 'login'? 'register':'login');
  }

結果如下:
image

使用者可以透過分隔線下方的連結切換目前的是要作登入還是註冊。也就是點完連結登入就變成註冊如下圖:
image

到這邊,基本的登入頁面就大致完成。而控制登入的邏輯,在接下來的文章會繼續進行。

結論

經過這兩天的開發,可以發現手機應用程式與一般後端開發程式不同之處。一個元件同時包含著畫面顯示的宣告邏輯與事件觸發的處理邏輯。即便透過像是 react hook 這類方法最後還是會被匯集到某個 render 元件。這樣的邏輯混合 ui 的狀況比單純 api 的處理複雜許多。

另外是,手機端程式對於每個顯示的頁面 Activity 會有其生命週期。載入某個 Activity 的小元件 Fragement 也會依附於 Activity 生命週期,有屬於他自己的元件週期。這些都是當需要開發手機程式時所需要考量的。

總結是,不論是作哪種系統的開發工作。都一定要先經過設計,否則會造成一些重工或是做出一些增加認知負擔的元件。


上一篇
nestjs 系統設計 - 活動訂票管理系統 - client code part 1
下一篇
nestjs 系統設計 - 活動訂票管理系統 - client code part 3
系列文
透過 nestjs 框架,讓 nodejs 系統維護度增加31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言