前端小白的成长之路 前端系列---项目组织

image

💡项目地址: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

  1. 像 vue 那样能够对象式配置路由
  2. 路由跳转鉴权等常规验证
  3. 组件加载时的骨架屏(后面体验升级专题再统一讲(mark))

第一步,配置文件以下: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? 咱们想怎样调用 api? 有哪些公共事务能够交给它统一处理?编程

  1. 一样的咱们但愿 api 能够写成对象式的配置文件;
  2. 具体实现能够经过配置文件建立对应的 api, 在调用的位置,咱们只关心入参和出参;
  3. 我但愿调用 api 时,根据配置自动弹出 loading,处理异常或失败场景;

配置文件以下: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
复制代码

传值方式一般是三种

  1. 路径传值
/game/user/${id} // 使用${id}占位,调用时进行匹配
复制代码
  1. params
/game/user?id=123456    // 处理params
复制代码
  1. body // 处理 data body 参数类型根据 header/Content-Type 设置设定参数的经常使用类型大体以下几种
  • application/x-www-form-urlencoded ==> 数据格式 (key1=value1&key2=value2)
  • multipart/form-data ==> 数据格式 (键值对使用 --boundary 分割) 值能够是 text 也能够是 file
  • text/plain(经常使用:application/json) ==> 数据格式 ({"a": "valueA"})
  • application/octet-stream ==> 二进制流 ...

若是后台架构混乱,或者对接不少不一样后台状况,会出现各类不一样的传参类型,也须要判断 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,屡次关闭, 触发次数 > 关闭次数 则显示 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 稍微定制化一下

icon/国际化及其余

字体图标安利一下猫厂的iconfont,若是的你设计师知道怎么给你矢量图的话,很好用。 并且字体库也很丰富,还能配合 antd-pro 一块儿使用 本项目所使用的 iconfont 都来自这里,至于使用也很简单,直接下载下来,若是不须要兼容的话,能够参考icon.less

国际化,react-i18next仍是挺好用的,而后封装了命令式语言翻译借助了有道智云,详见translate脚本

image

最后

写到这里,加之最近对图形的进一步认识,对项目又有了一些新的展望,但愿经过游戏为载体,进一步对js动画,css动画,canvas,webGL进行系统深刻的了解,固然涉及到体验交互,性能优化,网络也会开专题来说,有兴趣的同窗能够联系切图仔,结对编程。

占位符

前端小白的成长之路(序)
前端小白的成长之路 前端系列---项目搭建

相关文章
相关标签/搜索