筆者在作一個完整的博客上線項目,包括前臺、後臺、後端接口和服務器配置。本文將詳細介紹使用vue全家桶製做的博客網站css
該項目是基於vue全家桶(vue、vue-router、vuex、vue SSR)開發的一套博客前臺頁面,主要功能包括首頁顯示、認證系統、文章管理、評論管理和點贊管理前端
【訪問地址】vue
域名:https://xiaohuochai.ccreact
Github: https://github.com/littlematch0123/blog-clientandroid
或者能夠直接掃描二維碼訪問webpack
【項目介紹】ios
該項目的內容以筆者自學前端的過程當中寫的600多篇博客爲基礎,對於一樣學習前端的同窗可能會有所幫助。許多博客都有直接能夠操做的DEMO,對知識的理解可能會更直觀git
採用移動優先的響應式佈局,移動端、桌面端都可適配;字體大小使用em單位,桌面端的文字相應變大;移動端可以使用滑屏操做,桌面端經過光標設置、自定義滾動條、回車肯定等,提高交互體驗github
全站採用服務器端渲染SSR的方式,有利於SEO,減小了首屏渲染時間;使用service worker和manifest實現了PWA方案的離線緩存和添加到桌面的功能web
根據HTML標籤內容模型,使用語義化標籤,儘可能減小標籤層級,儘可能減小無語義的div標籤
CSS大量使用類選擇器,儘可能減小選擇器層級,在vue組件中使用CSS module和postCSS,使用styleLint規範CSS代碼,按照佈局類屬性、盒模型屬性、文本類屬性、修飾類屬性的順序編寫代碼,並使用order插件進行校驗
使用esLint規範JS代碼,代碼風格參照airbnb規範,全部命名採用駝峯寫法,公共組件以Base爲前綴,事件函數以on爲前綴,異步函數以async爲後綴,布爾值基本以do或is爲前綴
沒有引用第三方組件庫,如bootstrap或element組件,而是本身開發了項目中所需的公共組件。在common目錄下,封裝了頭像、全屏、loading、遮罩、搜索框、聯動選擇等組件,方便開發
使用配置數據,實現了數據和應用分離,以常量的形式存儲在constants目錄下
使用了阿里雲的短信模塊,實現了短信驗證功能
該項目有兩個隱藏彩蛋,一個是搖一搖功能,能夠直接搖到後臺頁面,另外一個是陀螺儀功能,上下晃動手機時,頭像會進行旋轉
項目進行了代碼優化,最終優化評分以下所示
主要功能包括首頁顯示、認證系統、文章管理、評論管理和點贊管理
【首頁顯示】
首頁包括可拖拽輪播圖、專題推薦、文章推薦和類別推薦
【認證系統】
認證系統包括用戶註冊、用戶登陸、短信驗證
一、用戶處於未登陸態時,能夠閱讀文章,但不能點贊和評論,不然會彈出登陸框
二、用戶註冊
三、用戶登陸
【文章管理】
文章管理包括瀏覽推薦文章、按類別篩選、文章搜索、按目錄查看
一、瀏覽推薦文章
二、文章篩選
三、文章搜索
四、按目錄查看
【點贊管理】
【評論管理】
評論管理包括查看評論、添加評論、修改評論和刪除評論
src目錄下,包括assets(靜態資源)、common(公共組件)、components(功能組件)、constants(常量配置)、router(路由)、store(vuex)和utils(工具方法)這7個目錄
- assets // 存放靜態資源,主要是圖片 -imgs css.png // CSS文章背景圖 ... - common // 存放公共組件 -SVG // 存放VUE圖標組件 SVGAdd.vue // "添加到"按鈕 SVGBack.vue // "返回"按鈕 ... BaseArticle.vue // 文章組件 BaseAvatar.vue // 頭像組件 ... - components // 存放功能組件 -Post // 文章組件 module.js //文章狀態管理 Post.vue // 文章顯示組件 PostContent.vue // 文章目錄組件 PostList.vue // 文章列表組件 SearchPost.vue // 搜索文章組件 ... - constants // 存放常量配置 API.js // 存放API調用地址 - router // 存放路由 index.js - store // 存放vuex index.js - utils // 存放工具方法 async.js // axios方法 fnVarificate.js // 表單驗證方法 util.js // 其餘工具方法
【公共組件】
沒有引用第三方組件庫,如bootstrap或element組件,而是本身開發了項目中所需的公共組件
封裝了文章組件、頭像組件、返回組件、按鈕組件、卡片組件、全屏組件、輸入框組件、loading組件、遮罩組件、搜索框組件、多行輸入框組件、標題組件、麪包屑組件、按鈕組組件、反色按鈕組件、密碼框組件、包含檢測的輸入框組件和聯動選擇組件
BaseAdd.vue // "添加到"組件 BaseArticle.vue // 文章組件 BaseAvatar.vue // 頭像組件 BaseBack.vue // 返回組件 BaseButton.vue // 按鈕組件 BaseCard.vue // 卡片組件 BaseFullScreen.vue // 全屏組件 BaseInput.vue // 輸入框組件 BaseLoading.vue // loading組件 BaseMask.vue // 遮罩組件 BaseSearchBox.vue // 搜索框組件 BaseTextArea.vue // 多行輸入框組件 BaseTitle.vue // 標題組件 BreadCrumb.vue // 麪包屑組件 ButtonBox.vue // 按鈕組組件 ButtonInverted.vue // 反色按鈕組件 InputPassword.vue // 密碼框組件 InputWithTest.vue // 包含檢測的輸入框組件 LinkageSelector.vue // 聯動選擇組件
【功能組件】
按照功能來設置目錄,以下所示
彈出框(Alert)
類別管理(Category)
評論管理(Comment)
主頁(Home)
點贊管理(Like)
文章管理(Post)
頁面尺寸(Size)
公共頭部(TheHeader)
用戶管理(User)
【全屏佈局】
使用設置高度的全屏佈局方式,主要經過calc來實現
<div id="root" :class="$style.wrap" :style="{height:wrapHeight+'px'}" > ... <TheHeader :class="$style.header"/> <main :class="$style.main"> <transition :name="transitionName"> <router-view :class="$style.router" /> </transition> </main> </div>
.header { height: 40px; } .main { position: relative; height: calc(100% - 40px); overflow: auto; }
【層級管理】
項目的層級z-index,只使用0-3
全屏的彈出框優化級最高,設置爲3;側邊欄設置爲2;頁面元素默認爲0,若有須要,要設置爲1
【全局彈出層】
在入口文件App.vue中設置全局的彈出層和loading,全部組件均可以共用
// App.vue <template> <div id="root" :class="$style.wrap" :style="{height:wrapHeight+'px'}" > <AlertWithLoading v-show="doShowLoading" /> <AlertWithText v-show="alertText !== ''" :text="alertText" :onClick="() => {$store.commit(HIDE_ALERTTEXT)}" /> <TheHeader :class="$style.header"/> <main :class="$style.main"> <transition :name="transitionName"> <router-view :class="$style.router" /> </transition> </main> </div> </template>
【路由管理】
vue-router使用靜態路由表的形式對路由進行管理,雖然沒有react-router-dom靈活,但方便尋找,一目瞭然
按路由設置按需加載組件,並設置滾動行爲
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default function createRouter() { return new Router({ mode: 'history', routes: [ { path: '/', component: () => import(/* webpackChunkName:'home' */ '@/components/Home/Home'), name: 'home', meta: { index: 0 } }, { path: '/posts', component: () => import(/* webpackChunkName:'post' */ '@/components/Post/PostList'), name: 'postlist' }, { path: '/posts/search', component: () => import(/* webpackChunkName:'post' */ '@/components/Post/SearchPost'), name: 'searchpost' }, { path: '/posts/:postid', component: () => import(/* webpackChunkName:'post' */ '@/components/Post/Post'), name: 'post', children: [ { path: 'comments', name: 'commentlist', component: () => import(/* webpackChunkName:'comment' */ '@/components/Comment/CommentList'), children: [ { path: 'add', name: 'addcomment', component: () => import(/* webpackChunkName:'comment' */ '@/components/Comment/AddComment') }, { path: ':commentid/update', name: 'updatecomment', component: () => import(/* webpackChunkName:'comment' */ '@/components/Comment/UpdateComment') }, { path: ':commentid/delete', name: 'deletecomment', component: () => import(/* webpackChunkName:'comment' */ '@/components/Comment/DeleteComment') } ] } ] }, { path: '/categories', component: () => import(/* webpackChunkName:'category' */ '@/components/Category/CategoryList'), name: 'categorylist' }, { path: '/categories/:number', component: () => import(/* webpackChunkName:'category' */ '@/components/Category/Category'), name: 'category' }, { path: '/topics/:number', component: () => import(/* webpackChunkName:'category' */ '@/components/Category/CategoryTopic'), name: 'topic' }, // 註冊 { path: '/signup', component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSignup'), name: 'signup' }, // 按手機號登陸 { path: '/signin_by_phonenumber', component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSigninByPhoneNumber'), name: 'signin_by_phonenumber' }, // 按用戶名登陸 { path: '/signin_by_username', component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSigninByUsername'), name: 'signin_by_username' }, // 用戶頁面 { path: '/users/:userid', component: () => import(/* webpackChunkName:'user' */ '@/components/User/UserDesk'), name: 'user' } ], scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } return { x: 0, y: 0 } } }) }
【狀態管理】
每一個組件的狀態管理命名爲module.js,保存在當前組件目錄下
import Vue from 'vue' import Vuex from 'vuex' import auth from '@/components/User/module' import alert from '@/components/Alert/module' import post from '@/components/Post/module' import category from '@/components/Category/module' import like from '@/components/Like/module' import size from '@/components/Size/module' import comment from '@/components/Comment/module' Vue.use(Vuex) export default function createStore() { return new Vuex.Store({ modules: { auth, alert, post, category, like, size, comment } }) }
每一個組件的狀態包括state、getters、actions和mutations字段,以Category組件爲例
import { BASE_CATEGORY_URL } from '@/constants/API' import { getNumberWithoutPostPositiveZero, getCategoryNumbers } from '@/utils/util' export const LOAD_CATEGORIES = 'LOAD_CATEGORIES' export const LOAD_CATEGORIES_ASYNC = 'LOAD_CATEGORIES_ASYNC' const category = { state: { docs: [] }, getters: { categoryCount: state => state.docs.length, getCategoriesByNumber: state => state.docs.reduce((obj, t) => { obj[t.number] = t return obj }, {}), getCategoryByNumber: state => number => state.docs.find(doc => doc.number === number), getPosterityCategories: (state, getters) => number => { const reg = new RegExp(`^${getNumberWithoutPostPositiveZero(number)}`) return state.docs.filter(doc => { doc.titleDatas = getCategoryNumbers(doc.number).map(t => getters.getCategoriesByNumber[t].name) return String(doc.number).match(reg) && (doc.posts.length) }) }, getChildrenCategoryies: state => number => { const reference = String(getNumberWithoutPostPositiveZero(number)) const len = reference.length const regExp = new RegExp(`^${reference}(0[1-9]|[1-9][0-9])(0){${8 - len}}`) return state.docs.filter(doc => String(doc.number).match(regExp)) }, getCategoryRootDatas: state => state.docs.filter(doc => Number(String(doc.number).slice(2)) === 0), getRecommendedCategories: state => state.docs.filter(t => t.recommend).sort((a, b) => a.index - b.index) }, actions: { /* 獲取所有類別信息 */ [LOAD_CATEGORIES_ASYNC]({ commit }) { return new Promise((resolve, reject) => { this._vm.$axios({ commit, url: BASE_CATEGORY_URL, doHideAlert: true, success(result) { // 保存類別 commit(LOAD_CATEGORIES, result.docs) // 向前端通知操做成功 resolve(result.docs) }, fail(err) { // 向前端通知操做失敗 reject(err) } }) }) } }, mutations: { /* 保存類別信息 */ [LOAD_CATEGORIES](state, payload) { state.docs = payload } } } export default category
【數據傳遞】
組件間的數據傳遞方式通常有三種,一種是使用vue中的props和自定義事件,另外一種是使用路由的params屬性,還有一種是經過vuex
一、props和自定義事件
// BaseInput <template> <input :class="$style.input" :value="value" autocomplete="off" autocapitalize="off" @input="$emit('input', $event.target.value)" > </template> <script> export default { props: { value: { type: String, default: '' } } } </script> // InputPassword <input :class="$style.input" :placeholder="placeholder" :value="value" autocomplete="off" autocapitalize="off" type="password" @input="$emit('input',$event.target.value)" >
二、路由的params屬性
// Post.vue <BaseBack @click.native="$router.push($route.params.parentPath || '/')">返回</BaseBack> //AuthSign.vue <template> <router-link :active-class="$style.active" :to="{ name: 'signin', params: { parentPath } }" >登 錄</router-link> </template> <script> export default { computed: { parentPath() { const temp = this.$route.params.parentPath if (temp) { return temp } return '' } } } </script>
三、使用vuex
// Category.vue <template> <article v-if="category" :class="$style.box"> <BaseBack @click.native="$router.push('/categories')">類別列表</BaseBack> <BaseTitle>{{ category.name }}知識體系</BaseTitle> ... </article> </template> <script> export default { computed: { category() { return this.$store.getters.getCategoryByNumber(Number(this.paramsNumber)) } ... } } </script>
【離線緩存】
經過service worker實現離線緩存效果
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin') plugins: [ new SWPrecacheWebpackPlugin({ dontCacheBustUrlsMatching: /\.\w{8}\./, filename: 'service-worker.js', logger(message) { if (message.indexOf('Total precache size is') === 0) { return; } if (message.indexOf('Skipping static resource') === 0) { return; } console.log(message); }, navigateFallback: 'https://www.xiaohuochai.cc', minify: true, navigateFallbackWhitelist: [/^(?!\/__).*/], dontCacheBustUrlsMatching: /./, staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/], runtimeCaching: [{ urlPattern: '/', handler: 'networkFirst' }, { urlPattern: /\/(posts|categories|users|likes|comments)/, handler: 'networkFirst' } ] }) ]
【添加到桌面】
andriod下,經過設置manifest.json文件添加到桌面,而IOS則須要設置meta標籤
<meta name="theme-color" content="#fff"/> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="前端小站"> <link rel="apple-touch-icon" href="/logo/logo_256.png"> <link rel="shortcut icon" href="/logo/favicon.ico"> <link rel="manifest" href="/manifest.json" /> // manifest.json { "name": "小火柴的前端小站", "short_name": "前端小站", "start_url": "/", "display": "standalone", "description": "", "theme_color": "#fff", "background_color": "#d8d8d8", "icons": [{ "src": "./logo/logo_32.png", "sizes": "32x32", "type": "image/png" }, { "src": "./logo/logo_48.png", "sizes": "48x48", "type": "image/png" }, { "src": "./logo/logo_96.png", "sizes": "96x96", "type": "image/png" }, { "src": "./logo/logo_144.png", "sizes": "144x144", "type": "image/png" }, { "src": "./logo/logo_192.png", "sizes": "192x192", "type": "image/png" }, { "src": "./logo/logo_256.png", "sizes": "256x256", "type": "image/png" } ] }
【子頁面刷新】
子頁面刷新時,可能會出現得不到從父級傳遞過來的數據的狀況,筆者的處理是跳轉到父級頁面
mounted() { if (!this.comment && this.operate === 'update') { this.$router.push(`/posts/${this.postId}/comments`) } else { this.setTextAreaValue() } }
【promise】
爲actions添加Promise,方便狀態改變後的處理
[LOAD_COMMENTS_ASYNC]({ commit }, payload) { return new Promise((resolve, reject) => { this._vm.$axios({ commit, data: payload, url: BASE_COMMENT_URL, doHideAlert: true, success(result) { // 保存類別 commit(LOAD_COMMENTS, result.docs) // 向前端通知操做成功 resolve(result.docs) }, fail(err) { // 向前端通知操做失敗 reject(err) } }) }) }
【組件共用】
因爲編輯和新建組件用到的元素是同樣的,只不過,新建組件時內容爲空,編輯組件時須要添加內容,這時就能夠複用組件
// AddComment.vue <CommentForm operate="add" /> //UpdateComment.vue <CommentForm operate="update" />
【清理環境】
若是使用addEventListener綁定了事件處理函數,在組件銷燬的時候,要及時清理環境
mounted() { window.addEventListener('devicemotion', throttle(this.testShake)) } beforeDestroy() { window.removeEventListener('devicemotion', throttle(this.testShake)) }
【應用和數據分離】
使用配置數據,實現數據和應用分離,配置數據主要是API調用地址,以常量的形式存儲在constants目錄下
// API.js let API_HOSTNAME if (process.env.NODE_ENV === 'production') { API_HOSTNAME = 'https://api.xiaohuochai.cc' } else { API_HOSTNAME = '/api' } export const SIGNUP_URL = `${API_HOSTNAME}/auth/signup` export const SIGNIN_BYUSERNAME_URL = `${API_HOSTNAME}/auth/signin_by_username` export const SIGNIN_BYPHONENUMBER_URL = `${API_HOSTNAME}/auth/signin_by_phonenumber` export const VERIFICATE_URL = `${API_HOSTNAME}/auth/verificate` 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 ADMIN_URL = 'https://admin.xiaohuochai.cc'
【函數節流】
爲觸發頻率較高的函數使用函數節流
/**
* 函數節流
* @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) }
【DNS預解析】
DNS預解析經過設置meta標籤實現
<link rel="dns-prefetch" href="//api.xiaohuochai.cc" /> <link rel="dns-prefetch" href="//static.xiaohuochai.site" /> <link rel="dns-prefetch" href="//demo.xiaohuochai.site" /> <link rel="dns-prefetch" href="//pic.xiaohuochai.site" />
【圖片懶加載和webp】
經過vue-lazyload插件實現圖片懶加載和andriod系統下圖片轉換成webp格式
Vue.use(VueLazyload, { loading: require('./assets/imgs/loading.gif'), listenEvents: ['scroll'], filter: { webp(listener, options) { if (!options.supportWebp) return const isCDN = /xiaohuochai.site/ if (isCDN.test(listener.src)) { listener.src += '?imageView2/2/format/webp' } } } })
【搖一搖效果】
搖一搖效果主要經過監測devicemotion事件實現
mounted() { window.addEventListener('devicemotion', throttle(this.testShake)) }, beforeDestroy() { window.removeEventListener('devicemotion', throttle(this.testShake)) }, methods: { testShake(e) { const { x, y, z } = e.accelerationIncludingGravity const { lastX, lastY, lastZ } = this const nowRange = Math.abs(lastX - x) + Math.abs(lastY - y) + Math.abs(lastZ - z) if (nowRange > 80) { window.location.href = ADMIN_URL } this.lastX = x this.lastY = y this.lastZ = z } }
【陀螺儀效果】
陀螺儀效果主要經過監測deviceorientation事件實現
mounted() { // 監測陀螺儀 window.addEventListener('deviceorientation', throttle(this.changeBeta)) }, beforeDestroy() { // 取消監測 window.removeEventListener('deviceorientation', throttle(this.changeBeta)) }, methods: { changeBeta(e) { if (this.beta !== Math.round(e.beta)) { this.beta = Math.round(e.beta) } } }
【緩動彈出層】
過渡彈出層有兩種實現方式,包括transition和animation,該項目使用animation的方式實現
<UserMenuList v-if="doShowMenuList" :onExit="() => {doShowMenuList = false}"/>
@keyframes move { 100% { transform: translateY(0); } } @keyframes opacity { 100% { opacity: 1; } } .mask { opacity: 0; animation: opacity linear both .2s; } .list { transform: translateY(-100%); animation: move forwards .2s; }
【圖標管理】
全部的圖標都使用SVG格式,存儲在common/SVG目錄下
// SVGAdd.vue <template> <svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/> <path d="M0 0h24v24H0z" fill="none"/> </svg> </template>
【axios函數封裝】
封裝axios函數到utils目錄下的async.js文件中,將loading組件、alert組件整合到axios函數的整個數據獲取過程當中
import { SHOW_LOADING, HIDE_LOADING, SHOW_ALERTTEXT, HIDE_ALERTTEXT } from '@/components/Alert/module' import { SIGNOUT } from '@/components/User/module' import axios from 'axios' const async = { install(Vue) { Vue.prototype.$axios = ({ commit, url, method, data, headers, success, fail, doHideAlert }) => { // 顯示loading commit(SHOW_LOADING) let axiosObj = url if (method) { axiosObj = { method, url, data, headers } } axios(axiosObj) .then(res => { const { message, result } = res.data // 關閉loading commit(HIDE_LOADING) // 顯示成功提示 !doHideAlert && commit(SHOW_ALERTTEXT, message) // 1秒後自動關閉提示 setTimeout(() => { commit(HIDE_ALERTTEXT) }, 1000) // 成功後的回調函數 success && success(result) }) .catch(err => { // 關閉loading commit(HIDE_LOADING) if (err.response) { const { data } = err.response // 自定義錯誤 if (data.code === 1) { commit(SHOW_ALERTTEXT, data.message) // 系統錯誤 } else if (data.code === 2) { commit(SHOW_ALERTTEXT, data.message) fail && fail(err) // 認證錯誤 } else if (data.code === 3) { commit(SHOW_ALERTTEXT, data.message) commit(SIGNOUT) window.location.href = '/signin_by_username' } else { // 顯示錯誤提示 commit(SHOW_ALERTTEXT, '服務器故障') // 失敗後的回調函數 fail && fail(err) } } else { // 顯示錯誤提示 commit(SHOW_ALERTTEXT, '服務器故障') // 失敗後的回調函數 fail && fail(err) } }) } } } export default async
【目錄跳轉】
使用scrollIntoView()方法,點擊目錄時,文章跳轉到相關部分,且不改變URL
<ul :class="$style.list"> <li v-for="(item, index) in titles" :key="item" :class="$style.item" @click="onChangeAnchor(`anchor${index+1}`)" > {{ index + 1 }}、{{ item }} </li> </ul>
methods: { onChangeAnchor(id) { document.getElementById(id).scrollIntoView({ behavior: 'smooth' }) } }
【錨點】
使用錨點進行頁面內跳轉時,URL發生改變,頁面刷新,其餘瀏覽器沒有問題。可是,ISO下的PWA桌面圖標會跳轉到safari瀏覽器中
使用scrollIntoView()方法來替代錨點#,頁面內只跳轉不刷新。andriod下支持給scrollIntoView設置平滑滾動behavior: 'smooth',但IOS不支持
【頁面放大】
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下會出現灰色背景
能夠經過-webkt-tap-hightlight-color屬性的設置,取消點擊時出現的背景效果
* { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
【局部不滾動】
IOS下,可能會出現局部滾動不流暢,甚至局部不滾動的bug
經過在該元素上設置overflow-scrolling屬性爲touch便可解決
div { -webkit-overflow-scrolling: touch; }
【錨點】
使用錨點進行頁面內跳轉時,URL發生改變,頁面刷新,其餘瀏覽器沒有問題。可是,ISO下的PWA桌面圖標會跳轉到safari瀏覽器中
使用scrollIntoView()方法來替代錨點#,頁面內只跳轉不刷新。andriod下支持給scrollIntoView設置平滑滾動behavior: 'smooth',但IOS不支持