1. 程式人生 > >使用react全家桶製作部落格後臺管理系統

使用react全家桶製作部落格後臺管理系統

前面的話

  筆者在做一個完整的部落格上線專案,包括前臺後臺後端介面和伺服器配置。本文將詳細介紹使用react全家桶製作的部落格後臺管理系統

概述

  該專案是基於react全家桶(React、React-router-dom、redux、styled-components)開發的一套部落格後臺管理系統,用於前端小站的管理,主要功能包括遊客瀏覽、文章管理、類別管理、評論通知、推薦設定和使用者管理

【訪問地址】

  或者可以直接掃描二維碼訪問

【專案介紹】

  採用移動優先的響應式佈局,移動端、桌面端均可適配;字型大小使用em單位,桌面端的文字相應變大;移動端大量使用滑屏操作,桌面端通過游標設定、自定義滾動條、回車確定等,提升互動體驗

  根據HTML標籤內容模型,使用語義化標籤,儘量減少標籤層級,儘量使用React.Fragment來代替div

  採用統一的色調處理,除了黑白兩色外,所有頁面共使用了8種顏色,保證了頁面顏色素雅、統一

  使用service worker實現了離線快取,配置了robots,禁止搜尋引擎抓取頁面

  使用styled-components外掛,實現css in JS。所有圖示資源均採用svg格式,並存儲到common/BaseImg元件中,方便管理,圖片資源均上傳到七牛雲圖床,使用外鏈訪問。最終,html、css、image都使用js管理

  沒有引用第三方元件庫,如bootstrap或螞蟻設計,而是自己開發了專案中所需的公共元件。在common目錄下,封裝了頭像、篩選框、全屏、loading、遮罩、搜尋框、滑屏、聯動選擇等元件,方便開發

  功能元件按照功能(Post、Comment...)而不是角色(controllers、models、views)分類,將展示元件component和容器元件container整合為一個檔案

  狀態管理借鑑了vuex的管理模式,action-types、action、reducer、selecter、state整合到每個模組目錄的module.js檔案下。為了方便擴充套件,所有的state都設定了filter欄位

  使用配置資料,實現了資料和應用分離,配置資料包括API呼叫地址和顏色值,以常量的形式儲存在constants目錄下

  使用esLint規範JS程式碼,程式碼風格參照airbnb規範,所有命名採用駝峰寫法,公共元件以Base為字首,函式大多以get或set為字首,事件函式以on為字首,非同步函式以async為字尾,布林值基本以do或is為字首

  使用styleLint規範CSS程式碼,按照佈局類屬性、盒模型屬性、文字類屬性、修飾類屬性的順序編寫程式碼,並使用order外掛進行校驗

  使用react最新版本的方法,包括createRef()、getDerivedStateFromProps生命週期、 React.Fragment語法糖等

  進行了程式碼優化,包括減少請求數量(檔案合併 、小圖片使用Base64、使用301而不是302重定向、靜態資源使用強快取、介面資源使用協商快取、使用離線快取、長快取優化、CSS內聯),減小資源大小(檔案壓縮、andriod下使用webp格式圖片、開啟gzip),優化網路連線(使用DNS預解析、使用keep-alive持久連線、使用HTTP2管道化連線),優化資源載入(優化資源載入位置、圖片懶載入),減少重繪迴流(減少兄弟選擇器、動畫元素硬體渲染、使用函式節流、及時清理環境)

  該專案的一個隱藏彩蛋是搖一搖功能,可以直接搖到前臺頁面,當然也可以再搖回來

  最終優化評分如下所示

功能演示

  功能主要包括遊客瀏覽、評論通知、使用者管理、推薦設定、文章管理和類別管理

【遊客瀏覽】

  在沒有管理員帳號的情況下,可以點選遊客瀏覽進入後臺。但是,遊客只有瀏覽許可權,沒有操作許可權

【評論通知】

  有新評論未檢視時,右上角快捷選單上會出現評論通知的按鈕。檢視評論後,通知按鈕消失

【使用者管理】

  使用者管理包括檢視所有使用者資訊、檢視使用者點贊情況、檢視使用者評論情況、按使用者名稱拼音排序、按點贊數排序、按評論數排序以及設定使用者狀態

【推薦管理】

  推薦管理包括文章推薦和專題推薦兩類

  1、文章推薦

  文章推薦的功能包括更改推薦文章、更改背景圖和更改次序

  2、專題推薦

  專題推薦的功能包括更改推薦專題、更改專題說明和更改次序

【文章管理】

  文章管理包括文章篩選、文章搜尋、新建文章、編輯文章、刪除文章、設定配圖、檢視點贊等功能

  1、文章篩選

  初始頁顯示全部文章,設定類別後,只顯示篩選後的文章,文章查閱完成後,可返回文章篩選頁

  2、文章搜尋

  初始頁只顯示搜尋框,設定搜尋詞後,顯示出相關文章,但每次只顯示16篇,下拉重新整理後,可繼續顯示。文章查閱完成後,可返回文章搜尋頁

  3、新建文章

  4、編輯文章

  5、設定配圖

  6、檢視點贊和評論並刪除文章

【類別管理】

  類別管理包括檢視類別、新增類別、編輯類別、刪除類別

目錄結構

  src目錄下,包括assets(靜態資源)、common(公共元件)、components(功能元件)、constants(常量配置)、store(redux)和utils(工具方法)這6個目錄

- assets // 存放靜態資源,包括通用CSS和圖片
    global.css // 全域性CSS
    login_bg.jpg // 登入框背景圖
- common // 存放公共元件
    BaseArticle.js // 文章元件
    BaseAvatar.js // 頭像元件
    ...
- components // 存放功能元件
    Category // 類別元件
      AddCategory.js // 類別新增元件
      DeleteCategory.js // 類別刪除元件
      UpdateCategory.js // 類別更新元件      
      Category.js // 類別路由元件
      CategoryForm.js // 類別基礎元件
      CategoryItem.js // 類別項元件
      CategoryItemList.js // 類別列表元件
      CategoryRootList.js // 類別根列表元件
      module.js //類別狀態管理
      ...
- constants // 存放常量配置
    API.js // 存放API呼叫地址
    Colors.js // 存放顏色值
- store // 存放redux
    index.js
- utils // 存放工具方法
    async.js // fetch方法
    history.js // 路由方法
    util.js // 其他工具方法

【公共元件】

  沒有引用第三方元件庫,如bootstrap或螞蟻設計,而是自己開發了專案中所需的公共元件

  封裝了文章元件、頭像元件、返回元件、徽章元件、按鈕元件、卡片元件、篩選框元件、全屏元件、圖片元件、輸入框元件、loading元件、遮罩元件、搜尋框元件、滑屏元件、多行輸入框元件、標題元件、麵包屑元件、按鈕組元件、反色按鈕元件、自適應按鈕元件、密碼框元件和聯動選擇元件

BaseArticle.js  // 文章元件
BaseAvatar.js // 頭像元件
BaseBack.js // 返回元件
BaseBadge.js  // 徽章元件
BaseButton.js // 按鈕元件
BaseCard.js // 卡片元件
BaseFilterList.js // 篩選框元件
BaseFullScreen.js // 全屏元件
BaseImg.js  // 圖片元件
BaseInput.js  // 輸入框元件
BaseLoading.js  // loading元件
BaseMask.js // 遮罩元件
BaseSearchBox.js  // 搜尋框元件
BaseSwipeItem.js  // 滑屏元件
BaseTextArea.js // 多行輸入框元件
BaseTitle.js  // 標題元件
BreadCrumb.js // 麵包屑元件
ButtonBox.js  // 按鈕組元件
ButtonInverted.js // 反色按鈕元件
ButtonWithAutoWidth.js  // 自適應按鈕元件
InputPassword.js  // 密碼框元件
LinkageSelector.js // 聯動選擇元件

【功能元件】

  按照功能來設定目錄,如下所示

彈出框(Alert)
登入框(Auth)
類別管理(Category)
評論管理(Comment)
主頁(Home)
點贊管理(Like)
文章管理(Post)
七牛傳圖(Qiniu)
推薦設定(Recommend)
頁面尺寸(Size)
使用者管理(User)

整體思路

【全屏佈局】

  使用設定高度的全屏佈局方式,主要通過calc來實現

  <section style={{ height: `${wrapHeight}px` }}>
    <HomeHeader />
    <Inner>
        ...
    </Inner>
    <HomeNav />
  </section>
const Header = styled.header`
  height: 50px;
`
const Inner = styled.main`
  height: calc(100% - 100px);
  background: ${PRIMARY_BG_COLOR};
`
const List = styled.nav`
  height: 50px;
`

【層級管理】

  專案的層級z-index,只使用0-3

  全屏的彈出框優化級最高,設定為3;側邊欄設定為2;頁面元素預設為0,如有需要,要設定為1

【全域性彈出層】

  在入口檔案app.js中設定全域性的彈出層和loading,所有元件都可以共用

// app.js
  render() {
    const { doShowLoading, alertText, hideAlertText } = this.props
    return (
      <React.Fragment>
        { doShowLoading && <AlertWithLoading /> }
        { !!alertText && <AlertWithText text={alertText} onExit={hideAlertText} />}
        <Router history={history} >
            ...
        </Router>
      </React.Fragment>
    )
  }

【路由管理】

  react-router-dom第四版採用了動態路由,在元件目錄內,以元件同名檔案儲存該元件內的路由

// category.js
const Category = () =>
  (
    <Switch>
      <Route exact path="/categories" component={CategoryRootList} />
      <Route exact path="/categories/:id" component={CategoryItemList} />
      <Route path="/categories/:id/add" component={AddCategory} />
      <Route path="/categories/:id/update" component={UpdateCategory} />
      <Route path="/categories/:id/delete" component={DeleteCategory} />
    </Switch>
  )

【狀態管理】

  參照vuex的狀態管理方式,將每個元件的狀態管理命名為module.js,儲存在當前元件目錄下

import auth from '@/components/Auth/module'
import size from '@/components/Size/module'
import alert from '@/components/Alert/module'
import categories from '@/components/Category/module'
import posts from '@/components/Post/PostsModule'
import post from '@/components/Post/PostModule'
import comments from '@/components/Comment/module'
import likes from '@/components/Like/module'
import qiniu from '@/components/Qiniu/module'
import users from '@/components/User/module'

const rootReducer = combineReducers({
  auth, size, alert, categories, posts, post, comments, likes, qiniu, users
})

  每個模組的狀態都設定有filter欄位,方便擴充套件

// action-types
export const SET_COMMENTS_FILTER = 'SET_COMMENTS_FILTER'

// state
const initialState = {
  filter: null,
  docs: []
}

// action
export const setCommentsFilter = filter => dispatch => new Promise(resolve => {
  resolve()
  dispatch({ type: SET_COMMENTS_FILTER, filter })
})

// reducer
const comments = (state = initialState, action) => {
  switch (action.type) {
  case SET_COMMENTS_FILTER:
    return { ...state, filter: action.filter }

}
export default comments

// selector
export const getCommentsFilter = state => state.comments.filter

【資料傳遞】

  元件間的資料傳遞方式一般有三種,一種是使用react中的函式傳參,另一種是使用路由的location屬性,還有一種是通過redux

  1、函式傳參

// PostRecommendItem
<BaseSearchBox
  searchText={title}
  datas={posts}
  onInput={this.onInput}
  onBack={() => { this.setState({ doShowSearchBox: false }) }}
/>

  onInput = data => {
    this.setState({ doShowSearchBox: false })
    const { updatePostAsync, showAlertText } = this.props
    const { prevData, datas } = this.statethis.setState({
        datas: datas.map(t => {
          if (t.number === data.number) return data
          return t
        })
      })
    ...
  }

// BaseSearchBox
<List innerRef={this.scrollRef}>
  {resultDatas.map(t =>
    <Item key={t._id} onClick={() => { onInput && onInput(t) }}>{t.title}</Item>)}
  {resultDatas.length >= limitNumber && !doNeedMoreDatas &&
    <ExtendedItem>已經到底了...</ExtendedItem>}
</List>

  2、location傳遞state

// CommentForm
  constructor(props) {
    super(props)
    const { operate, location } = props
    if (operate === 'update' && location.state) {
      const { content } = location.state.comment
      this.state = { content }
    } else {
      this.state = { content: '' }
    }
  }

// CommentList
history.push({ pathname: `${BasePostUrl}/comments/${t._id}/update`, state: { comment: t } })

  3、使用redux

//CategoryForm.js
  componentDidMount() {
    const { operate, match, setCategoriesFilter } = this.props
    setCategoriesFilter(Number(match.params.id)).then(() => {
      if (operate === 'update') {
        const { category } = this.props
        const { name, description } = category
        if (name) {
          this.setState({ name, description })
        } else {
          history.push(`/categories/${getParentNumber(Number(match.params.id))}`)
        }
      }
    })
  }
const mapStateToProps = state => ({
  category: getCategoryByFilter(state)
})
export default connect(mapStateToProps, { setCategoriesFilter })(CategoryForm)

專案優化

【子頁面重新整理】

  子頁面重新整理時,可能會出現得不到從父級傳遞過來的資料的情況,筆者的處理是跳轉到父級頁面

  componentDidMount() {
    const { operate, location, match } = this.props
    if (operate === 'update' && !location.state) {
      history.push(`/posts/${match.params.postId}/comments`)
    }
  }

【reselect】

  通過reselect來儲存狀態,減少狀態查詢,提升效能

export const getRecommendedCategories = createSelector(getCategories,
  datas => datas.filter(t => t.recommend).sort((a, b) => a.index - b.index))

【promise】

  為action新增Promise,方便狀態改變後的處理

export const setCategoriesFilter = filter => dispatch => new Promise(resolve => {
  resolve()
  dispatch({ type: SET_CATEGORIES_FILTER, filter })
})

【元件共用】

  由於編輯和新建元件用到的元素是一樣的,只不過,新建元件時內容為空,編輯元件時需要新增內容,這時就可以複用元件

const AddCategory = ({ match }) => <CategoryForm match={match} operate="add" />
const UpdateCategory = ({ match }) => <CategoryForm match={match} operate="update" />

【清理環境】

  如果使用addEventListener綁定了事件處理函式,在元件銷燬的時候,要及時清理環境

  componentDidMount() {
    this.scrollRef.current.addEventListener('scroll', throttle(this.onScroll))
  }
  componentWillUnmount() {
    this.scrollRef.current.removeEventListener('scroll', throttle(this.onScroll))
  }

【生命週期函式】

  1、使用getDerivedStateFromProps生命週期函式時,如果不設定constructor,會有如下警告

Did not properly initialize state during construction. Expected state to be an object, but it was undefined.

  新增空state即可解決

  constructor(props) {
    super(props)
    this.state = {}
  }

  2、使用componentDidMount生命週期函式時,如果在該函式中直接使用this.setState(),會有如下警告

Do not use setState in componentDidMount  react/no-did-mount-set-state

  將state設定轉移到then方法,或者另一個函式中即可

componentDidMount() {
  this.test()
}
test() {
  this.setState({ name: '' })
}

【應用和資料分離】

  使用配置資料,實現資料和應用分離,配置資料包括API呼叫地址和顏色值,以常量的形式儲存在constants目錄下

// API.js
let API_HOSTNAME
if (process.env.NODE_ENV === 'development') {
  API_HOSTNAME = '/local'
} else {
  API_HOSTNAME = '/api'
}

export const BASE_AUTH_URL = `${API_HOSTNAME}/auth/admin`
export const BASE_USER_URL = `${API_HOSTNAME}/users`
export const BASE_POST_URL = `${API_HOSTNAME}/posts`
export const BASE_TOPIC_URL = `${API_HOSTNAME}/topics`
export const BASE_CATEGORY_URL = `${API_HOSTNAME}/categories`
export const BASE_LIKE_URL = `${API_HOSTNAME}/likes`
export const BASE_COMMENT_URL = `${API_HOSTNAME}/comments`
export const BASE_RECOMMEND_URL = `${API_HOSTNAME}/recommends`
export const BASE_QINIU_URL = `${API_HOSTNAME}/qiniu`
export const STATIC = 'https://static.xiaohuochai.site'
export const CLIENT_URL = 'https://www.xiaohuochai.cc'

// Colors.js
export const PRIMARY_COLOR = '#00a8e5'
export const DARK_COLOR = '#0066cc'
export const ERROR_COLOR = '#f67280'
export const PRIMARY_BG_COLOR = '#fafafa'
export const TRANSPARENT_BG_COLOR = 'rgba(7, 17, 27, .4)'
export const DARK_BG_COLOR = '#f5f5f5'
export const PRIMARY_LINE_COLOR = '#eee'
export const DARK_LINE_COLOR = '#ebedf0'

【函式節流】

  為觸發頻率較高的函式使用函式節流

/**
 * 函式節流
 * @param {fn} function test(){}
 * @return {fn} function test(){}
 */
export const throttle = (fn, wait = 100) => function func(...args) {
  if (fn.timer) return
  fn.timer = setTimeout(() => {
    fn.apply(this, args)
    fn.timer = null
  }, wait)
}

功能實現

【登入設定】

  將使用者資訊儲存到sessionStorage中並檢測,如果不存在,則跳轉到登入頁面

<Router history={history} >
  <Switch>
    <Route path="/login" component={AuthLogin} />
    <Route
      path="/"
      render={props => {
        if (sessionStorage.getItem('token') && sessionStorage.getItem('user')) {
          return <Home {...props} />
        }
        return <Redirect to="/login" />
      }}
    />
  </Switch>
</Router>

【全形空格佔位】

  使用全形空格佔位,從而使文字對齊

<Label htmlFor="username">使用者名稱:</Label>
<Label htmlFor="password">&emsp;密碼:</Label>

【一畫素邊框】

  將偽元素高度設定為1px,然後用 transform縮小到原來的一半

div {
  position: relative;
  &::after {
    position: absolute;
    left: 0;
    right: 0;
    height: 1px;
    transform: scaleY(.5);
    content: '';
  }
`

【緩動彈出層】

  過渡彈出層有兩種實現方式,包括transition和animation,該專案使用transition的方式實現

<StyledMask className={doShowMenuList ? 'mask-show' : ''} />
<StyledList className={doShowMenuList ? 'transform-show' : ''} />
const StyledList = styled(HomeMenuList)`
  transform: translateY(-100%);
  transition: .2s;
`
const StyledMask = styled(BaseMask)`
  z-index: 2;
  display: none;
`
const MenuBox = styled.div`
  cursor: pointer;
  & .transform-show {
    transform: translateY(0);
  }
  & .mask-show {
    display: block;
  }
`

【圖示管理】

  所有的圖示都使用SVG格式,儲存在common/BaseImg.js檔案中

// BaseImg.js
...
export const Home = props => (
  <svg height={24} viewBox="0 0 24 24" width={24} {...props}>
    <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
    <path d="M0 0h24v24H0z" fill="none" />
  </svg>
)

【搜尋實現】

  處理搜尋功能時,需要特別處理正則表示式中的元字元

  static getReg(searchText) {
    return new RegExp(searchText.replace(/[[(){}^$|?*+.\\-]/g, '\\$&'), 'ig')
  }

  如果將間隔符-放在中間,大寫字母,如V會被匹配為/V

return new RegExp(searchText.replace(/[[(){}^$|?*+.-\\]/g, '\\$&'), 'ig')

  此時的-被識別為範圍間隔符,相當於.到\之間的字元,正好包括了所有的大寫字母,所以。一定要把-放在最後

【滑屏實現】

  滑屏主要通過touch事件來實現,一般地,有兩種形式。一種是當前元素滑動,另一種是其他元素滑動。該專案採用較簡單的第二種

  static checkSwipe(absMove, duration) {
    const THRESHOLD = 10
    const SHORTESTTIME = 300
    // 距離大於10,且時間小於300ms,才算做一次滑動
    return Boolean(absMove > THRESHOLD && duration < SHORTESTTIME)
  }
  onTouchStart = e => {
    this.startTime = new Date().getTime()
    this.startX = e.targetTouches[0].pageX
    this.startY = e.targetTouches[0].pageY
  }
  onTouchEnd = e => {
    const { pageX, pageY } = e.changedTouches[0]
    // 如果y軸移動距離大於元素高度,說明手指已經移出元素本身,則取消滑動
    if (pageY - this.startY > this.clientHeight) {
      return false
    }
    const moveX = pageX - this.startX
    const duration = new Date().getTime() - this.startTime
    // 如果符合滑動要求,且向左滑動,則控制條滑出
    if (BaseSwipeItem.checkSwipe(Math.abs(moveX), duration) && moveX < 0) {
      this.setState({ doShowControlBox: true })
    } else {
      this.setState({ doShowControlBox: false })
    }
    return true
  }

【密碼框實現】

  密碼框的右側一般都有一個小圖示用於顯示密碼

<Wrap className={className} {...rest} >
  <StyledInput
    id="password"
    textIndent={textIndent}
    color={color}
    value={value}
    onChange={onChange}
    type={doShowPassword ? 'password' : 'text'}
  />
  { doShowPassword ?
    <Visibility onClick={onChangeStatus} />
    : <VisibilityOff onClick={onChangeStatus} />
  }
</Wrap>

【fetch函式封裝】

  該專案是基於create-react-app構建的,自帶fetch功能。封裝fetch函式到utils目錄下的async.js檔案中,將loading元件、alert元件整合到fetch函式的整個資料獲取過程中

import { showLoading, hideLoading, showAlertText, hideAlertText } from '@/components/Alert/module'
import { logout } from '@/components/Auth/module'

const async = ({ dispatch, url, method, data, headers, success, fail, doHideAlert }) => {
  // 顯示loading
  dispatch(showLoading())
  let fetchObj = {}
  if (method) {
    fetchObj = {
      method,
      body: JSON.stringify(data),
      headers: new Headers({ ...headers, 'Content-Type': 'application/json' })
    }
  }
  fetch(url, fetchObj).then(res => {
    // 關閉loading
    dispatch(hideLoading())
    return res.json()
  }).then(json => {
    // 成功
    if (json.code === 0) {
      !doHideAlert && dispatch(showAlertText(json.message))
      setTimeout(() => {
        dispatch(hideAlertText())
      }, 1000)
      success && success(json.result)
      // 自定義錯誤
    } else if (json.code === 1) {
      dispatch(showAlertText(json.message))
      // 系統錯誤
    } else if (json.code === 2) {
      dispatch(showAlertText(json.message))
      fail && fail(json.err)
      // 認證失敗
    } else if (json.code === 3) {
      dispatch(showAlertText(json.message))
      dispatch(logout)
      // 許可權不足
    } else if (json.code === 4) {
      dispatch(showAlertText(json.message))
    }
  }).catch(() => {
    dispatch(showAlertText('伺服器故障'))
  })
}

export default async

【元件內路由】

  如果要在元件內使用路由功能,可封裝utils/history.js檔案

// utils/history.js
import createBrowserHistory from 'history/createBrowserHistory'
const customHistory = createBrowserHistory()
export default customHistory

  Router中使用history={history},而不是BrowserRouter

// app.js
import history from '@/utils/history'
<Router history={history} >
  <Switch>
    <Route path="/login" component={AuthLogin} />
    <Route
      path="/"
      render={props => {
        if (sessionStorage.getItem('token') && sessionStorage.getItem('user')) {
          return <Home {...props} />
        }
        return <Redirect to="/login" />
      }}
    />
  </Switch>
</Router>

  然後,在元件中引用即可

import  history  from '@/utils/history'
// 跳轉到首頁
history.push('/')

相容處理

【虛擬鍵盤】

  andriod下,虛擬鍵盤會影響可視區域的高度;而IOS下,不會影響

可視區域高度 = document.documentElement.clientHeight - 虛擬鍵盤的高度;

  bug重現如下:

  所以,要將包含input域的頁面高度設為固定

  在頁面初始化時,獲取頁面高度

// app.js
  componentDidMount() {
    const { setWrapSize } = this.props
    const { clientHeight, clientWidth } = document.documentElement
    setWrapSize({ clientHeight, clientWidth })
    window.addEventListener('orientationchange', this.setSize)
  }

  然後通過行間樣式,將此高度設定到包含input域的頁面上

// BaseFullScreen
<Wrap className={className} style={{ height: `${wrapHeight}px` }} {...rest}>{children}</Wrap>

【取消自動大寫】

  IOS下,input域會自動大寫首字母,設定autoCapitallize="off"即可

const BaseInput = ({ value, onChange, ...rest }) =>
  <Input {...rest} value={value} onChange={onChange} autoComplete="off" autoCapitalize="off" />

【游標顏色】

  預設情況下,游標顏色與字型顏色color相同,但也可以通過caret-color屬性來單獨設定

  但是,IOS的游標不支援caret-color,與字型顏色無關,預設為紫藍色。所以,儘量不要設定藍色或紫色背景,否則游標看不清楚

【頁面放大】

  IOS下,input獲取焦點時會放大,meta設定user-scalable=no,可取消放大效果

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, shrink-to-fit=no">

【圓角】

  IOS下,input域只顯示底邊框時,會出現底邊圓角效果,設定border-radius:0即可

border-radius:0

【輪廓outline】

  android瀏覽器下,input域處於焦點狀態時,預設會有一圈淡黃色的輪廓outline效果

  通過設定outline:none可將其去除

outline: none

【點選背景】

  在移動端,點選可點選元素時,android下會出現淡藍色背景,IOS下會出現灰色背景

  bug重現如下:

  可以通過-webkt-tap-hightlight-color屬性的設定,取消點選時出現的背景效果

* {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

【區域性不滾動】

  IOS下,可能會出現區域性滾動不流暢,甚至區域性不滾動的bug

  通過在該元素上設定overflow-scrolling屬性為touch即可解決

div {
  -webkit-overflow-scrolling: touch;
}

【高度無效】

  在IOS下,設定height:100%,如果父級的flex值為1,而沒有設定具體高度,則100%高度設定無效

  處理方法是,在父級通過計算來設定具體高度height,如height: calc(100% - 100px)