💡項目地址:games.git
🎮開始遊戲:startcss
這篇主要講講搭建一個項目組織結構,封裝順手的方法,組件和腳本前端
src
└─── assets(公共資源)
│
└─── components(公共組件)
│
└─── config(項目配置)
│
└─── layouts(公共容器)
│
└─── locales(國際化)
│
└─── plugins(插件相關)
│
└─── routes(路由)
│
└─── service(服務)
│ │ api(接口)
│ │ data(基表)
│ │ store(全局數據)
│
└─── theme(全局樣式)
│ │ default(樣式重置)
│ │ theme(主題樣式)
│ │ icon(字體圖標)
│
└─── utils(工具)
│
└─── views
│ │
│ └───...(代碼)
│
└───...
複製代碼
這份結構算是時下比較流行的的結構,也是筆者平時用的,以前有前輩建議把組件分爲容器組件和複用組件(containers/components),容器組件操做 redux 的數據,這樣其實常常會有組件從考慮業務問題,從兩個文件夾來回轉移,這方面仁者見仁吧!vue
咱們想怎樣編寫路由,以及但願路由幫助咱們完成什麼樣的事情?react
第一步,配置文件以下:ios
const page = (name: string) =>
Loadable({
loader: () => import(`../views/${name}`),
loading: Loading
// delay: 200,
// timeout: 10000
})
const routeConfig: IroutesConfig[] = [
{
path: '/',
title: {
zh: '遊戲圈',
en: 'Games'
},
exact: true,
strict: true,
component: page('home/index.tsx')
// childRoutes: [
// // childRoutes..
// ]
},
{
path: '/testPage/permission',
permission: ['user'],
title: {
zh: '測試權限頁面',
en: 'Test permission page'
},
exact: true,
strict: true,
component: page('testPage/permission.tsx')
},
{
path: '/404',
title: {
zh: '404',
en: '404'
},
component: page('exception/index.tsx')
}
]
複製代碼
第二步, 利用 withRouter 建立路由組件git
export const RouteWithSubRoutes = (routes: any) => {
const { path, exact = false, strict = false, childRoutes } = routes
return (
<Route
path={path}
exact={exact}
strict={strict}
render={(props: any) => {
return (
<BaseLayout {...props} routes={routes}>
<routes.component {...props} routes={childRoutes} />
</BaseLayout>
)
}}
/>
)
}
const GenerateRoute = (props: any) => {
return (
<React.Fragment>
<Switch>
{props.config
.map((route: any, i: number) => {
return <RouteWithSubRoutes key={i} {...route} />
})
.reverse()}
{<Route component={() => <Exception type="404" />} />}
</Switch>
</React.Fragment>
)
}
export default withRouter(GenerateRoute)
複製代碼
第三步,經過高階組件作鑑權或重定向等操做github
const BaseLayout = (props: Iprops) => {
const { children, routes = {}, userPermission = [], setTitle } = props
const { permission: routePermission } = routes
const hasPermission = routePermission
? routePermission.some((rPermission: string) =>
userPermission.some((uPermission: string) => uPermission === rPermission)
)
: true
if (hasPermission) {
const { title = null } = routes
if (title) {
setTitle(title)
}
} else {
setTitle({
zh: '無權限',
en: 'No Access'
})
}
return <React.Fragment>{hasPermission ? children : <Exception type="401" />}</React.Fragment>
}
const mapStateToProps = (state: any) => ({
userPermission: state.user.permission
})
const mapDispatchToProps = (dispatch: any) => ({
setTitle: dispatch.base.setTitle
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(BaseLayout)
複製代碼
舒服,會想起剛剛接觸 react 的時候,嫌棄它的路由等等沒法自定義配置
後來發現函數式,高階組件真香web
咱們想怎樣編寫 api? 咱們想怎樣調用 api? 有哪些公共事務能夠交給它統一處理?編程
配置文件以下:json
import { crearApiProxy } from '../index.ts'
const userConfig = {
login: {
method: 'POST',
url: '/game/user/access/login'
},
logout: {
method: 'POST',
url: '/game/user/access/logout'
},
getUsers: {
url: '/game/user/${id}'
},
updateUser: {
method: 'POST',
url: '/game/user/update',
baseUrl: 'www.domain.com/',
headers: {
contentType: 'json'
}
}
}
const userApi = crearApiProxy(userConfig)
export default userApi
複製代碼
傳值方式一般是三種
/game/user/${id} // 使用${id}佔位,調用時進行匹配
複製代碼
/game/user?id=123456 // 處理params
複製代碼
若是後臺架構混亂,或者對接不少不一樣後臺狀況,會出現各類不一樣的傳參類型,也須要判斷 Content-Type 對 data 進行兼容處理
import axios from 'axios'
import { request, response } from './interceptors'
export const headers = {
'Content-Type': 'application/json',
'X-Session-Mode': 'header'
}
const service = axios.create({
headers,
method: 'GET',
baseURL: '/',
timeout: 5000
})
// Request interceptors
service.interceptors.request.use(...request)
// Response interceptors
service.interceptors.response.use(...response)
export default service
複製代碼
interceptors.ts
const methods = ['post', 'put', 'patch']
const urlPlaceholder = /\$\{\w+\}/
function repalceParams(str: string, obj: any) {
console.log(obj)
Object.keys(obj).map((key: string) => {
str = str.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), obj[key])
})
return str
}
export const request = [
(config: any): object => {
if (isString(config)) { // 只傳url
config = {
url: config
}
}
let { url, data, method = 'GET' } = config
if (urlPlaceholder.test(url)) {
url = repalceParams(url, data) // 替換路徑參數
}
const headers = {
'X-Token': `Bearer ${UserModule.token || null}`,
...config.headers
}
const dataName = method && methods.includes(method.toLowerCase()) ? 'data' : 'params'
loadingStatus.count++
return {
url: `${apiPrefix}${url}`,
[dataName]: data,
paramsSerializer(params: object) {
return stringify(params)
},
transformRequest: [(data: any) => JSON.stringify(data)],
method,
headers
}
},
(error: any) => {
loadingStatus.count--
console.log(error)
return Promise.reject(error)
}
]
export const response = [
// tslint:disable-next-line:no-shadowed-variable
(response: any) => {
loadingStatus.count--
const res = JSON.parse(response.data)
if (res.resCode !== 0 && !response.config.headers.hideMsg) {
Toast.fail(`error with resCode: ${res.resMsg}`) // 處理失敗或異常
if (res.resCode === 401) {
// 跳轉登陸頁面
}
console.log(res.resMsg)
// new Error(`error with resCode: ${res.resMsg}`)
// return Promise.reject(`error with resCode: ${res.resMsg}`)
return res
} else {
return res
}
},
(err: any) => {
loadingStatus.count--
console.log('err', err)
if (err && err.response) {
err.message = errorCodeMessage[err.response.status] || '請求錯誤'
}
Toast.fail(err.message) // 處理失敗或異常
return Promise.reject(err)
}
]
複製代碼
調用
import userApi from '@/services/api/modules/user.ts'
const login = async () => {
await const res = userApi.login({username: 'admin', password: 'admin'})
if (!res.resCode) {
// to do
}
}
複製代碼
這樣開發起來仍是很舒服
組件,大多從實際業務中抽出而 loading 一般的要求以下:
全局只存在一個 Loading; 遮罩不容許用戶屢次點擊;
本項目應用場景:
調用接口時支持屢次觸發 loading,屢次關閉, 觸發次數 > 關閉次數 則顯示 Loading,不然不顯示;
loadingIcon 採用的項目 logo.svg + c3 動畫效果如圖:
loading 做爲全局組件跟它組件不一樣,咱們能夠封裝成插件的形式
複製代碼
let loadingNode: Element | any
const randerLoadingDOM = () => {
// const loadingNode = document.createElement('div')
loadingNode = document.createElement('div')
loadingNode.id = `global-loading-${new Date().getTime()}`
document.body.appendChild(loadingNode)
ReactDOM.render(<PageLoading id="global-loading" className="global-loading" />, loadingNode)
}
const unmountLoadingDOM = () => {
ReactDOM.unmountComponentAtNode(loadingNode)
if (loadingNode && loadingNode.parentNode) {
loadingNode.parentNode.removeChild(loadingNode)
}
loadingNode = undefined
}
const loadingPlugin: IloadingPlugin = {
isVisible: false,
show() {
if (!loadingNode) {
randerLoadingDOM()
} else if (!this.isVisible) {
loadingNode.style.display = 'block'
}
this.isVisible = true
},
hide() {
if (loadingNode) {
loadingNode.style.display = 'none'
}
this.isVisible = false
},
remove() {
if (loadingNode) {
unmountLoadingDOM()
}
this.isVisible = false
}
}
export default loadingPlugin
複製代碼
多接口調用時,防止提早關閉或多 loading,作一層調用封裝:
import LoadingPlugin from '@/components/Loading/plugin.tsx'
const loadingStatus = {
_count: 0,
isShow: false
}
Object.defineProperty(loadingStatus, 'count', {
set(val) {
this._count = val
if (val) {
this.isShow = true
LoadingPlugin.show()
} else {
this.isShow = false
LoadingPlugin.hide()
}
},
get() {
return this._count
}
})
export default loadingStatus
複製代碼
loadingStatus.count++ // 觸發
loadingStatus.count-- // 關閉
複製代碼
這樣用起來仍是蠻舒服的, 固然也有接口調用不須要 loading, 這樣就須要在接口配置中多加個參數控制,同時須要把這個參數放到插入到接口參數中去 loadingStatus.count++,同時不觸發,在拿到的接口結果中再作判斷,是否 loadingStatus.count--
這樣感受比較雞肋,同時傳遞了多餘參數,倒不如再 new 一個 axios,interceptors 稍微定製化一下
字體圖標安利一下貓廠的iconfont,若是的你設計師知道怎麼給你矢量圖的話,很好用。 並且字體庫也很豐富,還能配合 antd-pro 一塊兒使用 本項目所使用的 iconfont 都來自這裏,至於使用也很簡單,直接下載下來,若是不須要兼容的話,能夠參考icon.less
國際化,react-i18next仍是挺好用的,而後封裝了命令式語言翻譯藉助了有道智雲,詳見translate腳本
寫到這裏,加之最近對圖形的進一步認識,對項目又有了一些新的展望,但願經過遊戲爲載體,進一步對js動畫,css動畫,canvas,webGL進行系統深刻的瞭解,固然涉及到體驗交互,性能優化,網絡也會開專題來說,有興趣的同窗能夠聯繫切圖仔,結對編程。