前端小白的成長之路 前端系列---項目組織

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進行系統深刻的瞭解,固然涉及到體驗交互,性能優化,網絡也會開專題來說,有興趣的同窗能夠聯繫切圖仔,結對編程。

佔位符

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

相關文章
相關標籤/搜索