비즈니스 웹 애플리케이션을 위한 BaaS vte.입문

vte.cx(부표)걸음걸이
API 설계는 보편적이고 직관적인 규칙을 준수하며, 프런트엔드 엔지니어가 BFF 영역에서 서버 리소스를 안전하게 처리할 수 있도록 설계되었습니다.
API 서버의 구축, 유지보수, 확장, 운용은 전혀 힘들지 않다.
백엔드에 대한 최소한의 지식을 통해 다양한 비즈니스 논리를 가진 웹 응용 프로그램을 구축하고 발표할 수 있다
다음, vte.cx, React, Type Script, Material UI 기반
Create, 읽기(+페이지 문자, 데이터베이스 검색), Update, Delete 응용의 실현 기록

프로비저닝


─ src/components/
  ├─ index.tsx 3つのページをルーティング            ・・・(3)
  ├─ Register.tsx 新規登録ページ '/register'       ・・・(1)
  ├─ tableComponents/
  │    ├─ Table.tsx データ一覧表示ページ '/'        ・・・(2)
  │    │   表示データGET通信(ページネーション・・・(5)、データベース検索・・・(6))
  │    ├─ Edit.tsx 既存データ編集ページ '/edit'     ・・・(4)
  │    └─ Delete.tsx 既存データ削除
  ├─ Pagination.tsx                               ・・・(5)
  ├─ SearchField.tsx
  │   ユーザーの入力から、表示データGET通信にのせる検索用パラメータを生成します。
  │                                               ・・・(6)
  └─ formComponents/{~Input}.tsx
     フォーム項目ごとに雛形コンポーネント、ファイルを作成

(1) 복습~(6)


(1) 새 등록 형식인 Register.tsx


vte.cx, Axios, Controlled Form, Yup

API 준비 및 활용


vte.cx를 사용하여 API 서버를 준비하고 끝점 및 모드를 설정합니다.
axios.post('/d/{エンドポイント}', [ {/* スキーマ定義通りの内容のオブジェクト */} ])
vte.cx에서 요청을 보낼 때 다음과 같은 내용을 설정해야 합니다.
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'

마지막(6)까지 인코딩


Controlled Form
부모의 Register 구성 요소는 stateonChange에서 폼 입력을 관리(→ 상태 State)(→ 100단계 덮어쓰기)
사용자가 입력하면 다시 그릴 때마다 UI가 변경됩니다.
Register.tsx
import { useState } from 'react'
const Register = () => {
  const [firstname, setFirstname] = useState(undefined /* もしくは '' */)
  // vte.cxスキーマ定義からファイル生成される型を使うこと。
  // ...他項目
  const handleSubmit = ( event: React.FormEvent<HTMLFormElement> ) => {
    event.preventDefault()
    const user = { firstname /*,...*/ }
    // 前述の2行のaxiosのコード + 例外処理
  }
  return (
    <form onSubmit={handleSubmit} >
      <label></lable>
      <input value={firstname} onChange={setFirstname} />
      <input type="submit" />
    </form>
  )
}
Uncontrolled Form
입력 구성 요소마다 입력을 관리합니다. 주위의 UI를 바꾸지 않기 때문에useRef를 사용하지 않고 다시 그립니다.
const Uncontrolled_Form = () => {
  const firstnameRef = useRef( {} as HTMLInputElement )
  // ...他項目
  const handleSubmit = (event/*:型*/) => {
    const user = { firstname: inputRef.current.value /*,...*/ }
    // ...
  }
  // ...
  <input ref={firstnameRef} />
}
ref. Controlled and uncontrolled form
모든 입력 항목이 Proops와 행동을 바꾸었기 때문에 모든 폼 항목이 통용되었다.
UI를 분리한 값'register'|'edit'의 Proops를 프로젝트 구성 요소에 제출합니다.
Firstname.tsx
type Props = { mode: 'register', register='登録' /*,...*/ } | { mode: 'edit' /*,...*/ }
const Firstname: React.VFC<Props> = props => {
  if ( props.mode === 'register' ) {
    const { register } = props
    // ...
    return // ...
  } else if ( props.mode === 'edit' ) {
    // ...
  } else null
}
↓ 및 라이브러리React Final Form 추천
발리 데이터베이스 Yup
Register.tsx
import * as yup from 'yup'
// コンポーネントの外側
const schema = yup.object().shape({
  firstname: yup.string().required('必須')
  middlename: yup.string(),
  lastname: yup.string().required('必須'),
  email: yup.string().email('無効なメールアドレス')
})
// 内側
const [firstname, setFirstname] = useState(undefined)
const [error, setError] = useState({ firstname: '' /*,...*/ })
const handleSubmit = async (event) => {
  try {
    const result = await schema.validate({ firstname /*,...*/ })
    // 送信
  } catch (error) {
    setError({ [error.path]: error.message }) //
  }
}
return (
  <form onSubmit={handleSubmit} >
    <label></label>
    <input onChange={setFirstname} />
    {error.firstname || <></>}
    // ...
  </form>
)
validation 함수 thror 대상 사용하기
{
  name: 'ValidationError',
  value: {/* バリデーションされたオブジェクトのコピー*/},
  path: '最初にひっかかった項目の名前',
  type: 'yupライブラリ内部バリデーション処理で最初にひっかかったエラーの名前',
  errors: ['上のエラーの警告文'],
  inner: [] // validate(,第2引数{ abortEarly: false })と指定したとき
  // [ひっかかったすべての項目ごとに{第一階層の形式},{〃}, ...}
  message: '上コードのschemaに文errorsプロパティの文',
}
Yup 패턴을 바탕으로 해석과 검증을 위한 프로그램 라이브러리

(2) 데이터 일람은 테이블을 나타낸다.tsx


Data Grid 구성 요소(Material UI)
Table.tsx
useEffect(()=>{ /* 一覧表示するデータをGETリクエストしてstateに入れる */ },[])
실패 시
Table.tsx
import { GridOverlay } from '@mui/x-data-grid'
<DataGrid
  // ...
  components={{
    NoRowsOverlay: ()=>
      <GridOverlay>{ state.errMsg || 'データ0件' }</GridOverlay>}
  }}
/>
성공 시
// ↓のレスポンスを整形してstateに持たせた
{
  data: [
    {
      user,
      id,
      link: [ { ___href:'{更新先}', ___rel:'{←hrefの説明。selfだとかalias}'} /* 1 or 複数 ,... */ ]
      // ,...
    } // ,...
  ] // ,... ←ここは不要
}
// ↓ 整形
[ {...user, id, link /*,...*/ } ] // 型も定義する
Table.tsx
import { DataGrid } from '@mui/x-data-grid'
//Tableコンポーネントの外
// 一覧表示される項目(=列column)ごとの設定
const columns = [
  // 一覧表示されるstate.feed配列の中のオブジェクト(=行row)のageプロパティについての設定
  {
    field: 'age',
    headerName: '年齢',
    width: '100px',
    renderCell: catchUnregistered(
      (params: GridValueFormatterParams & { value: string }) => value + '歳'
    ) // 後述
  } // ,...
]
// Tableコンポーネント内
<DataGrid
  columns={columns}
  rows={state.feed} // 整形済み[{ ...user, id, link ,... }]
/>
DataGrid의 Proops columns에 대한 renderCell 속성 설명
DataGrid에서 행에 속성이 없으면 빈 막대가 됩니다.
그러나 미등록을 명시하기 위해 고급 함수를 사용하여 코드를 더욱 쉽게 읽을 수 있다
DataGrid의 Proopsmodecolumns 속성은 renderCell형의 params가 매개 변수이고 원시형과 JSX라는 내용이다.Element형은 값을 반환하는 함수입니다. 이 반환값은 목록에 표시됩니다.
Table.tsx
import { GridCellValue, GridValueFormatterParams } from '@mui/x-data-grid'
// コンポーネントの外側
const catchUnregistered =
  (formatterFunc?: (params: GridValueFormatterParams) => GridCellValue) =>
  // このcatchUnregistered関数は以下を返す。
  (params: GridValueFormatterParams) =>
    params.value && params.value !== '' ? ( // 未登録か?
      formatterFunc ? ( // 関数が渡されたか?
        formatterFunc(params) // 渡された関数でフォーマットされた登録値を返す関数
      ) : (
        params.value // 登録値をそのまま返す関数
      )
    ) : (
      <p style={{ color: 'silver' }}>Not Registered</p>
      // 未登録を明示するhtml要素を返す関数
    )

(3) index는 (1) 및 (2) 라우팅됩니다.tsx


React Router
index.tsx
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'
// ...
<HashRouter hashType="noslash">
  <Menu /> // メニューコンポーネント 省略
  <Switch>
    <Route exact path="/" >
      <Redirect to="/table" />
    </Route >
    <Route exact path="/table" exact>
      <Table /> // 一覧コンポーネント
    </Route>
    <Route path="/register">
      <Register /> // 登録コンポーネント
    </Route>
    <Route path="/edit">
      <Edit />  // 編集コンポーネント (4)
    </Route>
  </Switch>
</HashRouter>
// ...
GridValueFormatterParams 바깥쪽<Switch></Switch>은 경로와 무관

(4) 데이터 편집 Edittsx


onSelectionModelChange (Data Grid Props)
목록 UI의 각 행의 맨 왼쪽 열에는 Proops를 통해 제어되는 확인란이 표시됩니다.
여러 데이터 편집 및 삭제 시도
Table.tsx
import { Button } from '@material-ui/core'
import { Link } from 'react-router-dom'
// ...
const [ids, setIds] = useState([])
const handleIdsChange = (ids: GridSelectionModel) =>
 setIds( Array.from( new Set(ids).values() ) )
const selection = state.feed.filter( entry => ids.has(entry.id) )
}
return (
  <DataGrid
    // ...
    selectionModel={ids}
    onSelectionModelChange={handleIdsChange}
    components={{
      Footer: ()=> // selectionを編集コンポーネントに渡すこと。
        <div class="レイアウト調整">
          <Button
            component={Link}
            to={{ pathname: '/edit', state: { selection }}
          >
            編集
          </button>
          <button onClick={/*削除処理*/} >削除</button>
        </div>
    }}
  />
)
먼저 삭제 처리
Table.tsx
const delete = async () => {
  if ( selection.length > 20 ) {
    alert('一度に削除できるのは20件まで。') // サーバーへの負荷対策
    return
  }
  const feed = selection.map( ({ id, link }) => ({ id: id + '?_delete', link }) )
  try {
    const r = await axios.put('/d', feed) // このように1度のトランザクションで複数削除できる
    return { success: `削除されました: ${feed.join(', ')}` }
  } catch (e) {
      if ( e === '500') { // エラー種類ごとに対応すること
        return { error: 'サーバーに問題があります'}
      } else // ...
      }
    })
  )
  const error_msg = results.flatMap((r: any)=>'error' in r.value? r.value.error: []) .join('\n')
  // 一覧データのGETリクエストを送り再描画すること。 データベース更新までのラグがある
}
여러 Edit 어셈블리 편집하기
Edit.tsx
import { useLocation } from 'react-router-dom'

const initial/*:Tableコンポーネントのstate.feedの型*/ = {}

const Edit = () => {
  const { entries: _entries } = useLocation<{ entries/*:state.feedの型*/ }>().state || { entries: [] }
 // リダイレクトすること。
  if (_entries.length < 1 || _entries.length > 10) history.push('/')
 // stateの初期値を動的に生成すること。
  _entries.forEach( ({ entry }) => initial[entry.id] = entry )
  const [entries, setEntries] = useState(initial)
 // UI
  const entries_form = Object.values(entries).map(props =>
      <div class="レイアウト" key={props.id} >
        <div>{props.id}</div>
        <label></label>
        <input
          value={props.firstname}
          onChange={(e/*:型*/)=>
            setEntries(prev=>( { ...prev, [props.id]: { ...prev[id], firstname: e.target.value } } ))
          }
        />
        // ...
      </div>
    )
  return <form onSubmit={/*更新処理*/}> {entries_form} </form>
}
업데이트 처리의 구조는 삭제 처리와 같다
Edit.tsx
axios.put('/d/', [
  { // スキーマ定義の形にすること。
    user: {
      firstname,
      age: age || undefined
      // ,...
    },
    link
  } //,...複数のエントリ
])

(5) 페이지 스타일


vte.cx 쿼리 매개 변수
  • 페이지 스타일의 페이지 수를 브라우저 히스토리에 유지합니다.
    ↓ 질의 매개 변수에서 페이지 수를 관리합니다.<Menu /> onSelectionModelChange
  • 3단계별 요청에 오류가 표시되고 각각 재시도
  • Table.tsx
    import { useLocation } from 'react-router-dom' // ←URLが変わるたびにコンポーネントを再レンダーします。
    // Pagination Config
    const RANGE = 50 // カーソルの範囲が50 →範囲50ページ
    const LENGTH = 5 // 1回のレスポンスに入るエントリの数 →1ページに5件
    
    const query = new URLSearchParams( useLocation().search ) // URL'.../table?page={ページ数}'の{ページ数}を取得する。
    const page = Number( query.get('page') ) || 1 // ない時は1。 number型キャストして数値比較できるようにすること。
    const total = useRef(0)
    const cursorEnd = useRef(1)
    
    const getFeed = async () => {
     // 総件数
      await fallback( async ()=>{
        const count = await axios.get(`d/{エンドポイント}?c`)
        total.current = count // 本当はcount.data.feed.title
      })
     // "カーソルを作る"
      await fallback( async ()=>{
      // 最初と、ページがカーソルを超えるとき
        if (cursorEnd.current === 1 || cursorEnd.current < page) {
          // カーソルを更新する
          // 前のカーソル終わり位置cursorEndを次のカーソル始め位置にする _pagination={始め,終わり} 最初は{1,50程度}にすること
          // 総件数がLENGTH*RANGEより少ないときは、ちょうど件数分のカーソルが作られる
          // RANGE(→50)ずつ上げていく
          const cursor = await axios.get( `/d/{エンドポイント}?_pagination=${cursorEnd.current},${ page + RANGE - 1 }&l=${LENGTH}`
          )
          cursorEnd.current = cursor.cursorEnd // 本当はcursor.data.feed.subtitle
        }
      })
     // フィードを取得する。
      await fallback( async ()=>{
        const feed = await axios.get(`/d/{エンドポイント}?n=${page}&l=${LENGTH}`)
        setState({ feed: SEIKEI(feed.data) }) // 整形する (2)参照
      })
    }
    
    const fallback = async (func: ()=>Promise<void>) => {
      const LIMIT = 10
      let retry = 0
      while (retry++ < LIMIT) {
        try {
          await func()
          retry = LIMIT
        } catch(e) {
          if (LIMIT < retry) {
            setState({ errMsg: e.response.message }) // エラー処理 (2)参照
            throw 'Error: 試行回数10超え'
          }
          : await new Promise(resolve => setTimeout(resolve, 500))
        }
      }
    }
    
    페이지를 이동할 때 조회 파라미터를 생성합니다.
    Table.tsx
    import { useHistory } from 'react-router-dom'
    const history = useHistory()
    useEffect(getFeed, [page])
    
    <button onClick={()=>history.push(`?page=${page+1}`)}>進む</button>
    <button onClick={()=>history.push(`?page=${page-1}`)}>戻る</button>
    

    (6) 찾기


    vte.cx 쿼리 매개 변수
  • 검색 결과를 기록에 남깁니다.검색 매개 변수로 검색 문장을 관리하게 하기↓https://{ドメイン名+ホスト名}/table ?page={ページ数}
  • (5)의 세 개의 axios가 요청한 검색 매개 변수에 검색 매개 변수를 추가합니다.
  • 총 검색 매개 변수 가져오기https://{ドメイン+ホスト}/{エンドポイント}/table?page={ページ数} &search={検索句}
  • 커서의 조회 매개 변수 만들기'/d/{エンドポイント}?c &{検索用パラメータ}'
  • 피드의 검색 매개 변수 가져오기'/d/{エンドポイント}?_pagination=${cursorEnd.current},${ page + RANGE - 1 }&l=${LENGTH} &{検索用パラメータ}'
  • Table.tsx
      const query = new URLSearchParams( useLocation().search )
    - const page = Number( query.get('page') ) || 1
    + const _page = Number( query.get('page') ) || 1
    
    + const search = query.get('search') || '' // URL'https://.../table?search={検索句}'の{検索句}を取得する。
    + const searchRef = useRef('')
    
      const getFeed = () => {
    +   if ( search !== searchRef.current ) { // 検索句が新しいとき
    +     cursorEnd.current = 1 // カーソルを新しくします。
    +     searchRef.current = search
    +   }
       // search(→検索句)から検索用パラメータを生成する
    +   const search_name = `user.name-rg-.*${search}.*`
        fallback(()=>{
    -     const count = await axios.get('d/{エンドポイント}?c')
    +     const count = await axios.get(`d/{エンドポイント}?c&${search_name}`)
        // ...
    
    vte.cx 검색용 매개 변수의 상세한 내용 참조주법의 제4장.
    ↑ vte.cx 포트에 매개 변수 값 조회 요청 (→ 매개 변수 검색)
    ↓ 검색 표시줄의 입력 값 → URL의 검색 매개 변수 검색 값 (→ 검색 문장) 에서 생성됩니다.
    Table.tsx
    const history = useHistory()
    const [search, setSearch] = useState('')
    const handleSearch = () => history.push(`?page=1&search=${search}`)
    useEffect(getFeed, [page, search])
    
    <input value={search} onChange={setSearch} />
    <button onClick={handleSearch}>検索</button>
    

    완성

    좋은 웹페이지 즐겨찾기