1. 組件 & 文件
說明: 組件是 UI 單元,RN 社區(qū)約定用 PascalCase(每個單詞首字母大寫)。文件名最好和組件名一致,這樣在編輯器里搜 Detail 能同時找到文件和組件。
// page/Detail/index.js
import React from 'react'
import { View, Text } from 'react-native'
function Detail({ navigation }) {
return (
<View>
<Text>詳情頁</Text>
</View>
)
}
export default Detail
-
Detail→ 組件名,PascalCase -
index.js→ 目錄名Detail與組件名對應(yīng),這是 RN 項目常見寫法
2. Hook
說明: 自定義 Hook 必須以 use 開頭,這是 React 的規(guī)定。React 靠這個名字判斷你是否在 Hook 里調(diào)用了 useState、useEffect 等。命名用 camelCase,后面跟「這個 Hook 做什么」。
// request/detail.js
import { useQuery } from '@tanstack/react-query'
import fetchApi from '../useHooks/useFetch'
function useDetail(cookId) {
return useQuery({
queryKey: ['cook', 'detail', cookId],
queryFn: () => fetchApi.get(`/cookDetail/${cookId}`),
})
}
export default { useDetail }
-
useDetail→use表示 Hook,Detail表示「獲取詳情數(shù)據(jù)」 - 調(diào)用時:
const detailQuery = useDetail(params.id)
3. export / import(最容易混)
說明: 導(dǎo)出有兩種方式,決定了導(dǎo)入時名字能不能自己取。
3.1 默認(rèn)導(dǎo)出 export default
導(dǎo)出的是一個「默認(rèn)模塊」,導(dǎo)入時名字隨便起。
// request/detail.js — 導(dǎo)出方
function useDetail(cookId) {
return useQuery({
queryKey: ['cook', 'detail', cookId],
queryFn: () => fetchApi.get(`/cookDetail/${cookId}`),
})
}
function useCollection() {
const queryClient = useQueryClient()
const collection = useMutation({
mutationFn: (cookId) => fetchApi.get(`/userCollection/${cookId}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cook', 'detail'] })
},
})
return { collection }
}
export default {
useDetail,
useCollection,
}
// page/Detail/index.js — 使用方
import useData from '../../request/detail.js'
function DetailPage({ navigation }) {
const { params } = navigation.state
// useData 只是 import 時起的別名
// detail.js 里并沒有名叫 useData 的變量
const detailQuery = useData.useDetail(params.id)
const { collection } = useData.useCollection()
return null
}
下面三種寫法導(dǎo)入的是同一個對象:
import useData from '../../request/detail.js'
import detailApi from '../../request/detail.js'
import request from '../../request/detail.js'
useData.useDetail(1) // 一樣
detailApi.useDetail(1) // 一樣
request.useDetail(1) // 一樣
3.2 命名導(dǎo)出 export { xxx }
導(dǎo)出時用 {} 包住名字,導(dǎo)入時必須用相同名字(或用 as 改名)。
// utils/format.js — 導(dǎo)出方
export function formatTime(minutes) {
return `${minutes}分鐘`
}
export const MAX_COOK_TIME = 120
// 使用方
import { formatTime, MAX_COOK_TIME } from '../utils/format.js'
formatTime(30) // "30分鐘"
MAX_COOK_TIME // 120
// 如果想改名,用 as
import { formatTime as fmtTime } from '../utils/format.js'
fmtTime(30) // "30分鐘"
3.3 兩種方式對比
| 導(dǎo)出方式 | 導(dǎo)入寫法 | 名字能否自取 |
|---|---|---|
export default obj |
import 任意名 from '...' |
能 |
export function foo() |
import { foo } from '...' |
不能,必須叫 foo
|
export function foo() |
import { foo as bar } from '...' |
用 as 改成 bar
|
4. Props(父子組件傳參)
說明: Props 是父組件傳給子組件的參數(shù),用 camelCase。約定俗成的后綴:
- 普通數(shù)據(jù):直接名詞,如
name、id - 布爾值:
is/has/show開頭,如isVisible、userCollected - 回調(diào)函數(shù):
on開頭,如onPress、onCollection(表示「當(dāng) xxx 發(fā)生時調(diào)用我」)
子組件 — 聲明接收哪些 props:
// page/Detail/components/Bottom.js
import React from 'react'
import { View, Text, TouchableOpacity } from 'react-native'
function Bottom({
id, // 普通數(shù)據(jù)
name, // 普通數(shù)據(jù)
userCollected, // 布爾:是否已收藏
onComment, // 回調(diào):點擊評論時觸發(fā)
onCollection, // 回調(diào):點擊收藏時觸發(fā)
}) {
return (
<View>
<Text>{name}</Text>
<TouchableOpacity onPress={onComment}>
<Text>評論</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => onCollection(() => {})}>
<Text>{userCollected ? '已收藏' : '收藏'}</Text>
</TouchableOpacity>
</View>
)
}
export default Bottom
父組件 — 傳入具體值:
// page/Detail/index.js
import Bottom from './components/Bottom'
import useData from '../../request/detail.js'
function DetailPage({ navigation }) {
const { params } = navigation.state
const detailQuery = useData.useDetail(params.id)
const { collection, cancelCollection } = useData.useCollection()
const bottomProps = {
id: params.id,
name: detailQuery.data?.name,
userCollected: detailQuery.data?.isCollection,
onComment: () => {
setModalVisible(true)
},
onCollection: (callback) => {
if (detailQuery.data?.isCollection) {
cancelCollection.mutateAsync(params.id).then(callback)
} else {
collection.mutateAsync(params.id).then(callback)
}
},
}
return <Bottom {...bottomProps} />
}
5. 事件函數(shù)
說明: 同一個函數(shù),在組件內(nèi)部定義和作為 prop 傳給子組件時,常用不同前綴,方便區(qū)分「誰定義的」和「誰觸發(fā)的」:
| 位置 | 前綴 | 含義 |
|---|---|---|
| 組件內(nèi)部 | handle |
我自己寫的處理邏輯 |
| 傳給子組件 | on |
告訴子組件「發(fā)生 xxx 時調(diào)這個」 |
function DetailPage() {
const [modalVisible, setModalVisible] = useState(false)
// 內(nèi)部定義:handle 開頭
const handleComment = () => {
setModalVisible(true)
}
const handleCollection = (callback) => {
collection.mutateAsync(params.id).then(callback)
}
return (
<Bottom
onComment={handleComment} // prop 名用 on
onCollection={handleCollection} // 函數(shù)體用 handle
/>
)
}
6. State & Ref
說明:
-
State 用
useState,返回[當(dāng)前值, 修改函數(shù)],修改函數(shù)固定為set+ 首字母大寫的值名 -
Ref 用
useRef,命名常以Ref結(jié)尾,表示「某個 DOM/組件的引用」
function DetailPage({ navigation }) {
const { params } = navigation.state
const detailQuery = useData.useDetail(params.id)
// state:存會變化、觸發(fā)重渲染的數(shù)據(jù)
const [detail, setDetail] = useState(null)
const [modalVisible, setModalVisible] = useState(false)
// ref:存不觸發(fā)重渲染的引用(DOM、定時器、臨時對象等)
const scrollViewRef = useRef(null)
const commentData = useRef({ stars: 1 })
// 接口數(shù)據(jù)回來后寫入 state
useEffect(() => {
if (detailQuery.data) {
setDetail(detailQuery.data)
}
}, [detailQuery.data])
return (
<ScrollView ref={scrollViewRef}>
<Text>{detail?.name}</Text>
</ScrollView>
)
}
7. Style 樣式
說明: RN 的樣式對象是 JS 對象,屬性名必須用 camelCase(不能寫 CSS 的 font-size,要寫 fontSize)。樣式名按用途起,不要按顏色值起。
import { StyleSheet, View, Text } from 'react-native'
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16,
},
title: {
fontSize: 18,
color: '#121212',
},
subtitle: {
fontSize: 14,
color: '#777777',
},
activeTab: {
fontWeight: '500',
color: '#AB8C5E',
},
})
function DetailPage() {
const isSelected = true
return (
<View style={styles.container}>
<Text style={styles.title}>紅燒肉</Text>
<Text style={[styles.subtitle, isSelected && styles.activeTab]}>
簡單
</Text>
</View>
)
}
-
styles.title→ 標(biāo)題樣式 -
[styles.subtitle, isSelected && styles.activeTab]→ 數(shù)組寫法,可疊加多個樣式
8. 常量
說明: 整個應(yīng)用不變、多處復(fù)用的值,用 UPPER_SNAKE_CASE(全大寫 + 下劃線),一眼看出「這是常量,不要改」。
// useHooks/deviceProtocol/protocol.js
export const EVENT_LIST_NAME = {
collectionCookControl: 'collectionCookControl',
}
// useHooks/useFetch/index.js
const API_BASE_URL = 'https://dreamecook.xin/cookbook/appPlugin'
// 使用
import { EVENT_LIST_NAME } from '../../useHooks/deviceProtocol/protocol.js'
deviceState.sendAction({
name: EVENT_LIST_NAME.collectionCookControl,
value: `1,${params.cIndex}`,
})
React Query 的 queryKey 雖不是常量寫法,但也是固定字符串?dāng)?shù)組,用于標(biāo)識緩存:
queryKey: ['cook', 'detail', cookId]
queryKey: ['category', 'cook', params]
9. 目錄結(jié)構(gòu)
說明: 目錄名反映內(nèi)容類型。頁面和組件用 PascalCase(和組件名一致),工具、Hook、請求層用 camelCase。
main/
├── page/
│ └── Detail/ # 頁面,PascalCase
│ ├── index.js # 導(dǎo)出 Detail 組件
│ └── components/
│ └── Bottom.js # 頁面私有子組件
├── components/
│ └── MyModal/ # 公共組件,PascalCase
│ └── index.js
├── request/
│ └── detail.js # 接口 Hook,camelCase
├── useHooks/
│ └── useFetch/
│ └── index.js # 請求封裝
└── utils/
└── UIConfig.js # 工具函數(shù)
10. 速查表
| 場景 | 風(fēng)格 | 示例 | 說明 |
|---|---|---|---|
| 組件 | PascalCase | function Detail() {} |
UI 單元 |
| Hook |
use + camelCase |
function useDetail(id) {} |
必須以 use 開頭 |
| default import | 自取 | import useData from './detail.js' |
名字與導(dǎo)出方無關(guān) |
| 命名 import | 固定 | import { useQuery } from '...' |
名字必須對應(yīng) |
| props 數(shù)據(jù) | camelCase | name={data.name} |
普通傳參 |
| props 布爾 | is/has | userCollected={true} |
表示 true/false |
| props 回調(diào) | on | onCollection={fn} |
事件回調(diào) |
| 內(nèi)部函數(shù) | handle | const handlePress = () => {} |
組件內(nèi)定義 |
| state | camelCase | [detail, setDetail] |
會觸發(fā)重渲染 |
| ref | xxxRef | scrollViewRef |
不觸發(fā)重渲染 |
| style | camelCase | title: { fontSize: 18 } |
RN 樣式對象 |
| 常量 | UPPER_SNAKE | API_BASE_URL |
全局不變值 |