前端知识大纲
一、面试环节设置
- 一面/二面 测试基础知识(html js css,html5 ES6 css3)
- 二面/三面 高级工程师问基本原理(深入,往往根据简历和个人介绍来发问)
- 三面/四面 技术负责人/业务负责人 关注你的从业经历中,你推动了什么,改变了什么
- 终面 hr具有一票否决权,关注面试人的沟通、性格、潜力等
注意:
社会招聘:考察知识、能力、经验(抽象能力,把控能力)
二、面试准备
- JD分析
- 职位描述
- 业务分析,实战模拟
- 前端技术栈
- 源码分析
例如:vue源码 注意:可结合博客上的分析快速看
- 前端框架
vue angular react会用三个中的一到两种就可以。关于理论:对其中一个要有深度研究,找博客源码分析。关于实战:遇到什么问题,怎么解决的要准备。注意:nodeJs前提是要说好,说不好就不要提,除非面试官问,不要画蛇添足。
- 简历和自我介绍
-
简历
- 1.基本信息:姓名 年龄 手机 邮箱 籍贯
- 2.教育背景:学历
- 3.工作经历:时间 公司 岗位 项目背景 技术栈 职责 业绩
- 4.开源项目和博客 github和开源项目说明,博客地址 注意:不要在简历中放自我评价
-
自我陈述
- 1.把握面试沟通方向(为面试官提问埋伏笔)
- 2.豁达,自信,节奏适宜,适度发挥,务实谦虚 (面试中不存在标准答案,凡事不能太绝对) (不要因为自己比别人懂得多了一点而对面试官产生鄙视,那是玩火自焚)
-
三、一/二
目录
- 页面布局
- css盒模型
- DOM事件
- 原型链
- 面向对象 & 类
- http协议 & 跨域
- 安全:XSS CSRF
- 算法:没有标准
1. 页面布局
position相关属性1. static 默认定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)2. relative相对定位,不脱离文档流,相对于自身原本位置进行偏移3. absolute 绝对定位,使元素完全脱离文档流,相对非static的父元素偏移(若其父元素没有定位则逐层上找,直到document——页面文档对象)4.fixed 固定定位,相对浏览器窗口(视口,即正在浏览的文档的那一部分)进行定位(视口共包括三种:布局视口、视觉视口和理想视口)5. sticky粘性定位,不脱离文档流,它的行为就像 position:relative; 而当页面滚动超出目标区域时,它的表现就像 position:fixed;,它会固定在目标位置。
浮动:脱离文档流绝对定位:脱离文档流flex:比较完美的布局方式table:兼容性比较好grid:兼容性不是很好,一般用100%比模拟,例如bootstrap的网格布局
- 动画的实现: - dom变化 svg的path canvas(2d,3d) CSS3 - js相关 requestAnimationFrame - css3 GPU加速(transform: translate3d(0,0,0),translateZ(0))- web标准 ES6 CSS3 HTML5- 模块化 ES6中如何处理模块化,CommonJS之间的模块化区别(AMD,CMD不表)- 模板引擎 ejs undersocore vue lodash.js- css动态语言 sass stylus- 构建工具 webpack vite
2. css盒模型
概念:由外至内:margin->border->padding->content①标准模型+IE模型②两种模型区别:计算高度和宽度不同IE模型宽度和高度包括框和填充③如何设置:box-sizing: border-box, content-box④js如何设置盒模型对应的宽和高var dom = document.querySelector('.demo')dom.style.width //只有在style存在的时候才能获取到,style中width为什么就是什么dom.getBoundingClientRect().width //获取到的为数字,单位是pxwindow.getComputedStyle(dom).width //获取到的为带px单位的字符串
dom.currentStyle.width //ie下特有的属性 获取到的为带单位的字符串,单位为css中定义的单位⑤根据盒模型解释边距重叠父子元素 兄弟元素 空元素参考网址:https://www.cnblogs.com/zhangmingze/articles/4664074.html⑥BFC/IFC边距重叠解决方案
概念:块级元素格式化上下文
影响:1.阻止外边距重叠2.包含内部浮动3.排除外部浮动
创建BFC:overflow不为visiblefloat不为noneposition不为static和relativedisplay为table-cell table-caption inline-block
3. DOM事件类
①DOM事件的级别DOM0 element.onclick=function(){}DOM1 没有定义事件相关的内容DOM2 elment.addEventListener('click', function(){})DOM3 增加了事件类型例如keyup等②事件模型捕获和冒泡③事件流捕获阶段->目标阶段->冒泡阶段④描述DOM事件捕获阶段的具体流程window->document->html(documentElement)->body->...->目标⑤Event对象event.preventDefault()event.stopPropagation()event.stopImmediatePropagation()event.currentTarget // 当前绑定事件的元素event.target // 被点击的元素⑥自定义事件var eve = new Event('custom',{bubbles: false, cancelbable: false}); //第二个可省略dom.addEventListener('custom', function(){});dom.dispatchEvent(eve);CustomEvent和Event相同,但是可以带参数detail
4. 原型链
-
创建对象的几种方法?
点击查看代码
var o1 = {name: 'reamd'};var o2 = new Object({name: 'reamd'});var M = function(){this.name = 'reamd';};var o3 = new M();var p = {name: 'reamd'};var o4 = Object.create(p); // 把obj赋到o4的__proto__上 -
原型链图示
-
只有函数才有prototype(function的声明时产生)
-
只有实例对象才有__proto__
-
修改了构造函数的prototype,相当于改变了实例所指向的原型对象
-
-
instanceof 原理
- instanceof用来判断构造函数的prototype是否在实例对象的原型链上。
-
new运算符原理
-
新对象obj被创建,继承自foo.prototype,构造函数foo被执行。执行时传入相应的实参,同时上下文this会被指定为这个新实例obj,不传参的情况下,new foo等同于new foo()。
-
如果构造函数返回了一个”对象”,那么这个对象会取代整个new出来的结果。如果构造函数没有返回对象,那么new出来的结果为创建的对象obj。
-
5. 面向对象&类
- 类与实例
- 类的声明:构造函数、class
- 实例:new
- 类与继承
- 实现继承的方式-基于原型链的继承、extends
点击查看代码
class Parent { constructor (name, age) { this.name = name; this.age = age; }
speak () { console.log('my name is ', this.name); }}
class Child extends Parent { constructor (name, age=18) { super(name, age); }
speakAge () { super.speak(); console.log('my age is ', this.age); }}
let child = new Child('reamd');child.speakAge();
// 寄生组合式继承function P(name) { this.name = name;}P.prototype.speak = function() { console.log(this.name);}
function S(name) { P.call(this, name);}S.prototype = Object.create(P.prototype);S.prototype.constructor = S;
const s = new S('a');console.log(s.__proto__.__proto__.__proto__); // Object.prototype
const p= new P('b');console.log(p.__proto__.__proto__); // Object.prototype
const obj = { name: 'c'};console.log(obj.__proto__, obj.__proto__.__proto__); // Object.prototype, null
6. http协议类
http1.1新特性:1.默认持久连接2.管线化3.断点续传
https特性:内容加密+身份认证+完整性保护
http2特性:1.二进制传输2.头压缩(请求头缓存在客户端,存在描述符)3.多路复用的流4.服务端主动推送
【管线化和多路复用的区别】HTTP管线化主要解决的是请求的排队和等待时间,而多路复用则主要解决的是TCP连接的建立和销毁开销,两者结合可以更好地提高网络通信效率。
①http协议的主要特点简单快速(URI) 灵活(头配置) 无连接(连接一次就断开) 无状态(无非区分身份)②http报文组成请求报文:请求行 http方法/页面地址 http协议及版本请求头空行请求体
响应报文:状态行 协议/版本 状态码 状态码说明响应头空行响应体③http方法GET POST DELET PUT HEAD获取 传输 删除 更新 获取报文首部④POST和GET区别回缓历长传⑤状态码1xx:指示信息2xx:成功3xx:重定向4xx:客户端错误5xx:服务端错误200206 range301 永久重定向302 临时重定向304 缓存400 bad request401 未授权403 forbidden404 找不到页面500 运行错误503 负载高或宕机⑥持久连接keep-alive http1.1支持 判断请求是否结束?content-length or chunked 空chunked块⑦http管线化a.通过持久连接完成,仅http1.1支持此技术b.只有GET和HEAD请求可以进行管线化,POST则有所限制c.初次创建连接不应启动管线机制,服务器可能不支持http1.1
-
通信类
-
同源策略及限制
源: 协议 + 域名 + 端口 = 源
限制:不是同源的文档能操作另一源的资源
- cookie indexDB localstorage- dom无法获得- ajax请求不能发送 -
前后端如何通信
ajax websocket cors -
创建ajax
点击查看代码
var ajax = function(param) {var xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");var type = (param.type || 'get').toUpperCase();var url = param.url;if (!url) {return}var data = param.data,dataArr = [];for (var k in data) {dataArr.push(k + '=' + data[k]);}dataArr.push('_=' + Math.random());if (type == 'GET') {url = url + '?' + dataArr.join('&');xhr.open(type, url);xhr.send();} else {xhr.open(type, url);xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");xhr.send(dataArr.join('&'));}xhr.onload = function() {if (xhr.status == 200 || xhr.status == 304) {var res;if (param.success && param.success instanceof Function) {res = xhr.responseText;if (typeof res === 'string') {res = JSON.parse(res);param.success.call(xhr, res);}}}};}; -
跨域通信
1. JSONP2. cors3. websocket4. postMessage5. Image对象6. Hash & window.name7. cookie & localstorage8. form表单
-
7. 安全类
- CSRF 跨站请求伪造
- 攻击原理
要素:1. 接口存在漏洞2. 访问的网站登录过防御措施:1. Token验证2. Referer验证3. 隐藏令牌4. 验证码5. ip地址(比较老的不推荐) - 攻击原理
- XSS 跨站脚本攻击
-
攻击原理 向页面注入脚本运行,自动触发,引诱触发,iframe广告插入。 xss定义(反射型,存储型) 1.反射型: 发出请求时,xss代码出现在url中,作为输入提交到服务器端,服务器解析后相应,xss代码随响应内容一起传回给浏览器,最后浏览器解析执行xss代码。
2.存储型: 存储型xss与反射型xss差别仅在于提交的代码会存储在服务器端,下次请求页面不用再提交。
3.DOM型: 属于特殊的反射型。DOM 型 XSS攻击中,取出和执行恶意代码由浏览器端完成,属于前端JavaScript自身的安全漏洞。
-
防御措施
编码,过滤,校正1. 编码对HTML进行html entity编码2.过滤移除用户上传的dom属性,如onerror移除用户上传的style节点,script节点,iframe节点等3.校正避免直接对html entity解码使用dom parse转换,校正不配对的DOM标签
-
8. 算法类
-
排序 冒泡排序, 快速排序, 选择排序,希尔排序
-
数据结构 堆栈,队列,链表
-
递归 60%算法都要用递归(本质要抓住,中止条件,参数如何传,尾递归)
-
波兰式和逆波兰式
- (1 + 2 * (4 - 3) + 6/2)中缀表达式
- +1+*2-4 3/6 2 前缀表达式(波兰式,从右向左)
- 1 2 4 3-*+6 2/+ 后缀表达式(逆波兰式,从左向右)
-
哈夫曼编码
- 给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树。
- 哈夫曼编码示例 注意: 基本功和理解题意,让面试官提示,写清原理。
四、二/三
A. 渲染机制
-
什么是DOCTYPE及作用 DTD告诉浏览器我是什么类型 DOCTYPE声明文档类型
HTML5 <!DOCTYPE html>HTML4 传统和严格模式不包括展示性和弃用元素比如<font> -
浏览器过程
-
重排(Reflow)/ 回流
dom中各元素都有自己的盒子,根据样式计算,并根据计算结果将元素放到它该出现的位置。
触发:1. 增删改dom2.移动dom的位置3.修改css样式4.resize窗口或滚动5.修改网页默认字体
- 重绘(Repaint)
当盒子的位置,大小,颜色,字体大小都确定下来后,浏览器便把这些元素按照各自特性绘制一遍,于是页面内容出现了,这个过程称之为repaint。
触发:1.dom改动2.css改动
注意点: 一次添加节点
B. JS运行机制类
- js单线程:一个时间内js只能干一件事
- 任务队列:同步任务、异步任务
- event loop
- 微任务micro task是跟屁虫,总是跟在同步任务的后面
- 异步任务:
- setTimeout和setInterval
- Dom事件 点击的时候和时间到了才扔到异步任务中
- ES6中的promise
1. macrotasks和microtasks的区别?macrotasks: setTimeout, setInterval, setImmediate, I/O, UI renderingmicrotasks: process.nextTick, Promise, MutationObserver任务队列中,在每一次事件循环中,macrotask只会提取一个执行,而microtask会一直提取,直到microtask队列为空为止。2. 为啥要用microtask?根据 HTML Standrad, 在每个task运行完以后,UI都会重新渲染,那么在microtask中就完成数据更新,因此当前task结束就可以得到最新的UI了。反之:如果新建一个task来做数据更新的话,那么渲染会执行两次。
C. 页面性能
-
提升页面性能有哪些方式
- 资源压缩合并,减少http请求
- 非核心代码异步加载(异步加载方式?区别?)
- 利用浏览器缓存(缓存分类?缓存原理?)
- 使用CDN
- 预解析DNS
<meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//host.com"/>
-
异步加载
- 方式: 1.动态脚本加载 2.defer 3.async
- 区别:
- defer在HTML解析完才执行,多个按加载的顺序执行
- async加载完之后立即执行,多个执行顺序和加载顺序无关 注意:
- 普通的script(不使用async和defer)加载完会立即执行,会阻塞script标签下面的资源加载和dom的解析
- 使用async后,script加载完后会立即执行。(网络)资源的加载过程是异步的,不会阻塞后续资源的加载dom和解析。
- 使用defer后,script异步加载,html解析之后执行、DomContentLoaded之前执行。
- 浏览器缓存
缓存分类:
-
强缓存 1.Expires: Thu,21 Jan 2017 23:09:02 GMT (绝对时间,服务器下发的,与本地浏览器比较) 2.cache-control: max-age=3600(相对时间,单位s) 同时存在,以cache-control为准 ①public 所有内容都将被缓存(客户端和代理服务器都可缓存) ②private 内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存) ③no-cache 浏览器将不再使用强制缓存, 而是直接去请求服务器, 如果存在协商缓存这个时候就会用到了 ④no-store 浏览器则不会在使用缓存的数据也不缓存数据,即强制缓存和协商缓存都失效了
-
协商缓存 下发 请求
- Last-Modified If-Modified-Since
- Etag If-None-Match 浏览器默认优先强制缓存,强制缓存失效了,才使用协商缓存,协商缓存服务器两个都下发,根据客户端支持程度决定用啥,优先If-None-Match
-
D. 错误监控
-
实现一个埋点监控SDK? A: 一般埋点监控SDK满足三个功能:用户行为监控(pv/uv/点击行为)、页面性能监控(页面开始加载时间,白屏时间)、页面错误告警监控。 实现要素productID,send(image、navigator.sendBeacon),performance.timing(FP,DCL,LOADED) 错误告警监控分为JS原生错误和React/Vue的组件错误的处理。 js原生错误:try catch和未捕获的error及unhandledrejection vue和react可以使用错误边界组件解决错误问题。
-
错误上报
- 采用ajax通信方式上报
- 采用Image对象上报
五、三/四
业务能力 团队协作能力 事物推动能力 带人能力 其他能力(组织能力 学习能力 行业经验)
六、终
-
职业竞争力 业务能力 思考能力 学习能力 无上限付出
-
职业规划
- 目标:业务上成为专家,技术上成为大牛
- 近期目标: 不断学习积累各方面经验,学习为主
- 长期目标:做几件有价值的事情,开源作品,技术框架
- 方式方法:先完成业务上的主要问题,做到极致,然后逐步向目标靠拢。
-
总结
- 胜不骄,败不馁,总结经验,步步为营,多拿offer
- 30%技术 + 70%状态
七、常用知识点
# 常用1. gulp和grunt的对比高效:基于unix流的概念,管道连接高质量:gulp每个插件只完成一个功能易学:五个API, src dest watch task run易用:代码优于配置
Q: js三座大山A: 1. 作用域和闭包 2. 原型和原型链 3. 异步和单线程- 作用域主要是隔离变量,分为:全局作用域、模块作用域、函数作用域、块级作用域- 闭包: 函数A里包含了函数B,而函数B使用了函数A的变量,那么函数B被称为闭包或者闭包就是能够读取函数A内部变量的函数。a.变量常驻内存 b.改变外部函数的变量
Q:普通函数,箭头函数,call关于this的指向A:普通函数:this总是指向调用它的对象,如果用作构造函数,this指向创建的对象实例。箭头函数:箭头函数本身没有this,按照作用域链的就近原则到上级作用域查找,把执行时上下文的this供自己使用。call:改变this的指向,因为箭头函数没有this,改变不了。
Q: js优先级A: 括号 点 new参 函数调用 new无参
Q: CommonJS模块和ES模块区别?A:- CommonJS模块 导入: require 导出: module.exports or exports.xxxx- ES模块 导入:import 导出: export xxx or export default xxx- 其他说明1. CommonJS模块输出的是一个值的复制(一旦输出一个值,模块内部的变化就影响不到这个值),ES6模块输出的是值得引用(这个变量是只读的,对它进行重新赋值会报错)。2. CommonJS模块是运行时加载,ES6模块是编译时输出接口。3. ES6模块之中,顶层的this指向的是undefined,CommonJS模块的顶层this指向当前模块。
Q: type和interface区别A: 对象函数都适用,都能通过extends扩展type可以别名声明,可用于基础类型、联合类型、元组。interface可以同名合并,type不行。
Q: vue $nextTick实现原理A: nextTick在DOM更新完毕之后执行一个回调。当数据变化时,Vue不会立即更新 DOM,而是将需要更新组件标记为“脏”。当前事件循环结束后,Vue.js 会将所有“脏”组件更新合并到一起,在下一个事件循环中更新DOM。优先选择 Promise、MutationObserver 或 setImmediate,如果都不支持则回退到 setTimeout。
Q: Webpack 和 Vite区别?A: 1. 开发服务器:Webpack 使用 webpack-dev-server 进行整体打包,重新构建速度可能较慢;Vite 使用原生 ES 模块按需编译,启动和热更新速度更快。2. 构建速度:Webpack 使用依赖图策略打包,可能导致较慢的构建速度;Vite 在开发环境中按需编译,生产环境使用高性能的 Rollup 打包。3. 兼容性:Webpack 支持多种模块格式和旧版浏览器;Vite 侧重于现代浏览器和原生 ES 模块,可能对旧版浏览器支持不如 Webpack。4. 插件生态:Webpack 拥有庞大的插件生态;Vite 插件生态相对较小,但兼容大部分 Rollup 插件,正在快速成长。
Q: Vue2、Vue3、React diff 的区别A: React diff 特点 - 仅向右移动Vue2 diff 特点 - 双端比较Vue3 diff 特点 - 最长递增子序列Vue3 dom diff算法:1. 头部比较,不同break2. 尾部比较,不同break3. 经过1,2两个步骤,比较结束的话,如果旧节点数量 > 新节点数量,卸载;否则,新增;4. 如果是3步骤对立面,没比较完,则把剩余待比较多新节点遍历一遍记录newMap,key-index,然后遍历旧节点剩余部分,如果其key在newMap里面,记录到newIndexToOldMap中,否则卸载节点。5. newIndexToOldMap其实是一个数组,数组index是新节点索引,其值是旧节点索引,然后把旧节点采用最长递增子序列的方式进行移动。
Q: tree shakingA: 如果想要做到tree shaking,在引入模块时就应该避免将全部引入,应该引入局部才可以触发tree shaking机制。
Q:vue2和vue3的区别A:1. 选项式 vs 组合式2. webpack vs vite3. 双向绑定底层实现Object.defineProperty和Proxy,为什么改为proxy,a. 不用重写数组相关方法 b.不用遍历整个对象属性(随着对象属性规模变大,vue2性能也会下降)
Q: vue路由模式?A: 形式上:hash模式url里面永远带着#号,路由默认使用这个模式。history模式没有#号,是个正常的url,适合推广宣传。功能上:把vue做的页面,分享到第三方的app里,有的app里面url是不允许带有#号的,使用history模式,在二级页面做刷新操作,会出现404,需要运维同学配置一下apache或是nginx的url重定向,重定向到你的首页路由。技术上:哈希模式其实是利用了window.onhashchange事件,history模式是HTML5 History Interface 中新增的两个神器 pushState() 和 replaceState() 方法。
Q:JS的垃圾回收机制两种:1. 标记清除:函数内的变量在函数执行的时候记录为“进入环境”,函数执行结束,记录为“离开环境”,垃圾处理器会给每个变量打标,然后去掉在使用的变量的标记,回收完成后,变量块不连续,采用标记整理。2. 引用计数,容易引发内存泄漏,现在不怎么用了3. v8引擎基于标记清除回收机制的优化:分为新生代和老生代,新生代又分为使用区和空闲区,垃圾回收使用区的变量后,空闲区和使用区置换,这样就进行了内存整理,然后下一次回收变量的时候,把上一次没回收和这一次也没回收的变量放到老生代。
Q: 输入网址后,发生了什么?A: DNS解析:(1)递归查询DNS服务器接收客户机请求,必须使用一个准确的查询结果回复客户机。如果DNS服务器本地没有存储查询DNS信息,那么该服务器会询问其他服务器,并将返回的查询结果提交给客户机。(2)迭代查询DNS服务器会向客户机提供其他能够解析查询请求的DNS服务器地址,当客户机发送查询请求时,DNS 服务器并不直接回复查询结果,而是告诉客户机另一台DNS服务器地址,客户机再向这台DNS服务器提交请求,依次循环直到返回查询的结果为止。TCP三次握手:http请求http回应(重定向)服务器返回解析HTML*注:DNS解析原理:优先级=> dns缓存 > hosts > dns服务*
- ts常用示例
点击查看代码
// 1. 函数返回值声明interface Person { name: string; gender: string;}
const fetchPerson = async (): Promise<Person> => { const data = await fetch("http://test.com/api/people/1").then((res) => { return res.json(); }); return data;};
const fetchPerson2 = async () => { const data: Person = await fetch("http://test.com/api/people/1").then((res) => { return res.json(); }); return data;};
const fetchPerson3 = async () => { const data = await fetch("http://test.com/api/people/1").then((res) => { return res.json(); }); return data as Person;};
// 2. Recordconst cache: Record<string, string> = {};const cache2: {[id: string]: string;} = {};
// 3. try catch,catch的变量类型只能为any或unknowtry {
} catch (e: any) { console.log(e.message);}try {
} catch (e) { console.log((e as Error).message);}
// 4. extendsinterface Base { id: string;}
interface User extends Base { firstName: string; lastName: string;}// 相当于interface User { id: string; firstName: string; lastName: string;}
// 5. Omit && Pick && keyofinterface User { id: string; firstName: string; lastName: string;}/**type MyType = { firstName: string; lastName: string;};*/type MyType = Omit<User, 'id'>;type MyType2 = Pick<User, 'firstName' | 'lastName'>;
function setProp<T, K extends keyof T>(obj: T, key: K, val: T[K]): T { obj[key] = val; return obj;}interface Person { name: string; age: number;}const person: Person = { name: 'a', age: 18};setProp(person, 'name', 'b');
// 6. function typetype FocusListener = (isFocused: boolean) => void;const addListener = (onFocusChange: FocusListener) => {};
interface User { id: string; firstName: string; lastName: string;}interface CreateUserType { (): Promise<string>;}interface GetUserType { (id: string): Promise<User>;}const createThenGetUser = async ( createUser: CreateUserType, getUser: GetUserType,): Promise<User> => {};
// 7. 类型守卫- typeof- instanceof- in- 字面量 if (status === 'active') { } else {}- 用户定义 interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } type Shape = Square | Rectangle; function isSquare(shape: Shape): shape is Square { return shape.kind === "square"; } function getArea(shape: Shape): number { if (isSquare(shape)) { return shape.size * shape.size; // 这里 shape 被推断为 Square 类型 } else { return shape.width * shape.height; // 这里 shape 被推断为 Rectangle 类型 } }
- css 水平垂直居中