비즈니스 웹 애플리케이션을 위한 BaaS vte.입문
76418 단어 ReactTypeScriptMaterial-UIvtecxtech
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 구성 요소는
state
및 onChange
에서 폼 입력을 관리(→ 상태 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.tsximport { 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의 Proops
mode
의columns
속성은 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
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={ページ数}
https://{ドメイン+ホスト}/{エンドポイント}/table?page={ページ数}
&search={検索句}
'/d/{エンドポイント}?c
&{検索用パラメータ}'
'/d/{エンドポイント}?_pagination=${cursorEnd.current},${ page + RANGE - 1 }&l=${LENGTH}
&{検索用パラメータ}'
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>
완성
Reference
이 문제에 관하여(비즈니스 웹 애플리케이션을 위한 BaaS vte.입문), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/sugimotohiro/articles/vtecx_crud_app텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)