前面的话
笔者在做一个完整的博客上线项目,包括前台、后台、后端接口和服务器配置。本文将详细介绍使用vue全家桶制作的博客网站
概述
该项目是基于vue全家桶(vue、vue-router、vuex、vue SSR)开发的一套博客前台页面,主要功能包括首页显示、认证系统、文章管理、评论管理和点赞管理
【访问地址】
域名:https://xiaohuochai.cc
Github: https://github.com/littlematch0123/blog-client
或者可以直接扫描二维码访问
【项目介绍】
该项目的内容以笔者自学前端的过程中写的600多篇博客为基础,对于同样学习前端的同学可能会有所帮助。许多博客都有直接可以操作的DEMO,对知识的理解可能会更直观
采用移动优先的响应式布局,移动端、桌面端均可适配;字体大小使用em单位,桌面端的文字相应变大;移动端可使用滑屏操作,桌面端通过光标设置、自定义滚动条、回车确定等,提升交互体验
全站采用服务器端渲染SSR的方式,有利于SEO,减少了首屏渲染时间;使用service worker和manifest实现了PWA方案的离线缓存和添加到桌面的功能
根据HTML标签内容模型,使用语义化标签,尽量减少标签层级,尽量减少无语义的div标签
CSS大量使用类选择器,尽量减少选择器层级,在vue组件中使用CSS module和postCSS,使用styleLint规范CSS代码,按照布局类属性、盒模型属性、文本类属性、修饰类属性的顺序编写代码,并使用order插件进行校验
使用esLint规范JS代码,代码风格参照airbnb规范,所有命名采用驼峰写法,公共组件以Base为前缀,事件函数以on为前缀,异步函数以async为后缀,布尔值基本以do或is为前缀
没有引用第三方组件库,如bootstrap或element组件,而是自己开发了项目中所需的公共组件。在common目录下,封装了头像、全屏、loading、遮罩、搜索框、联动选择等组件,方便开发
使用配置数据,实现了数据和应用分离,以常量的形式存储在constants目录下
使用了阿里云的短信模块,实现了短信验证功能
该项目有两个隐藏彩蛋,一个是摇一摇功能,可以直接摇到后台页面,另一个是陀螺仪功能,上下晃动手机时,头像会进行旋转
项目进行了代码优化,最终优化评分如下所示
功能演示
主要功能包括首页显示、认证系统、文章管理、评论管理和点赞管理
【首页显示】
首页包括可拖拽轮播图、专题推荐、文章推荐和类别推荐
【认证系统】
认证系统包括用户注册、用户登录、短信验证
1、用户处于未登录态时,可以阅读文章,但不能点赞和评论,否则会弹出登录框
2、用户注册
3、用户登录
【文章管理】
文章管理包括浏览推荐文章、按类别筛选、文章搜索、按目录查看
1、浏览推荐文章
2、文章筛选
3、文章搜索
4、按目录查看
【点赞管理】
【评论管理】
评论管理包括查看评论、添加评论、修改评论和删除评论
目录结构
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'} "
>
...
class=" $style.header " />
class=" $style.main " >
"transitionName " >
class=" $style.router " />
.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
<div
id =" root "
: class =" $style.wrap "
:style =" {height:wrapHeight+'px'} "
>
"doShowLoading " />
<AlertWithText
v -show=" alertText !== '' "
:text =" alertText "
:onClick =" () => {$store.commit(HIDE_ALERTTEXT)} "
/>
class=" $style.header " />
class=" $style.main " >
"transitionName " >
class=" $style.router " />
【路由管理】
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
1、props和自定义事件
// BaseInput
<input
: class =" $style.input "
:value =" value "
autocomplete =" off "
autocapitalize =" off "
@input =" $emit('input', $event.target.value) "
>
// InputPassword
<input
: class =" $style.input "
:placeholder =" placeholder "
:value =" value "
autocomplete =" off "
autocapitalize =" off "
type =" password "
@input =" $emit('input',$event.target.value) "
>
2、路由的params属性
// Post.vue
"$router.push($route.params.parentPath || '/') " >返回
// AuthSign.vue
link
:active-class =" $style.active "
:to =" { name: 'signin', params: { parentPath } } "
>登 录
3、使用vuex
// Category.vue
if=" category " :class =" $style.box " >
"$router.push('/categories') " >类别列表
{{ category.name }}知识体系
...
项目优化
【离线缓存】
通过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标签
"theme-color " content=" #fff " />
"apple-mobile-web-app-capable " content=" yes " >
"apple-mobile-web-app-status-bar-style " content=" black " >
"apple-mobile-web-app-title " content=" 前端小站 " >
"apple-touch-icon " href=" /logo/logo_256.png " >
"shortcut icon " href=" /logo/favicon.ico " >
"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
"add " />
// UpdateComment.vue
"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标签实现
"dns-prefetch " href=" //api.xiaohuochai.cc " />
"dns-prefetch " href=" //static.xiaohuochai.site " />
"dns-prefetch " href=" //demo.xiaohuochai.site " />
"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的方式实现
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
"#000000 " height=" 24 " viewBox=" 0 0 24 24 " width=" 24 " xmlns=" http://www.w3.org/2000/svg " >
"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 " />
"M0 0h24v24H0z " fill=" none " />
【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
class=" $style.list " >
<li
v -for =" (item, index) in titles "
:key =" item "
: class =" $style.item "
@click =" onChangeAnchor(`anchor${index+1}`) "
>
{{ index + 1 }}、{{ item }}
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,可取消放大效果
【圆角】
IOS下,input域只显示底边框时,会出现底边圆角效果,设置border-radius:0即可
【轮廓outline】
android浏览器下,input域处于焦点状态时,默认会有一圈淡黄色的轮廓outline效果
通过设置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不支持