💡项目地址: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进行系统深刻的了解,固然涉及到体验交互,性能优化,网络也会开专题来说,有兴趣的同窗能够联系切图仔,结对编程。