2025年前端面试题~ 【前端面试】更新
前言
金三银四的招聘季即将来临,对于怀揣前端梦想的求职者而言,这是机遇与挑战并存的黄金时段。前端面试可不只是简单的问答,它是一场对综合能力的深度检验。面试官会从多个维度考量,比如扎实的 HTML、CSS 和 JavaScript 基础,能否灵活运用它们构建出既美观又实用的页面 ,能否高效解决复杂的兼容性问题;是否熟练掌握 React、Vue 等主流框架,明白它们的设计理念与核心原理;还会关注你对性能优化的理解,像如何减少页面加载时间、提升用户交互体验等。
面试过程中,清晰的逻辑思维和良好的沟通能力同样重要。面对算法和实际问题场景,能否有条不紊地分析并给出解决方案;在阐述项目经历时,能否精准地表达技术难点和自己的应对策略,都将影响面试官对你的评价。所以,提前做好充分准备,深入复习专业知识,回顾优化过往项目,多进行模拟面试,保持自信从容,相信你定能在前端面试中脱颖而出,开启职业生涯的新篇章。
以下汇总了各类常见的面试题及答案:
一、JavaScript篇
1. JavaScript 有哪些数据类型,它们的区别?
JavaScript 共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是 ES6 中新增的数据类型:这些数据可以分为原始数据类型和引用数据类型:
● 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
● 堆:引用数据类型(对象Object、数组Array 和 函数 Function)在操作系统中,内存被分为栈区和堆区:
● 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的
值等。其操作方式类似于数据结构中的栈。
● 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
2. 数据类型检测的方式有哪些
(1)typeof
其中数组、对象、null 都会被判断为 object,其他判断都正确。
(2)instanceof
instanceof 可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。可以看到,instanceof 只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
(3) constructor
constructor 有两个作用,一是判断数据的类型,二是对象实例通过constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor 就不能用来判断数据类型了
(4)Object.prototype.toString.call()
Object.prototype.toString.call() 使用 Object 对象的原型方法toString 来判断数据类型:
同样是检测对象 obj 调用 toString 方法,obj.toString()的结果和
Object.prototype.toString.call(obj)的结果不一样,这是为什么?
这是因为 toString 是 Object 的原型方法,而 Array、function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法(function 类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…)
3. null 和 undefined 区别
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义,null 代表的含义是空对象。
一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
undefined 在 JavaScript 中不是一个保留字,这意味着可以使用undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的undefined 值,比如说 void 0。
当对这两种类型使用 typeof 进行判断时,Null 类型化会返回“object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
4. intanceof 操作符的实现原理及实现
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
5. Object.is() 与比较操作符 “===”、“==” 的区别?
使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,
它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN是相等的。
6、JavaScript 原型,原型链? 有什么特点?
在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们是不应该能够获取到这个值的,但是现在浏览器中都实现了 __proto__ 属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对象的原型。当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性, 这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头 一般来说都是 Object.prototype 所以这就是我们新建的对象为什么能够使用 toString() 等方法的原因。特点:JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
7、如何判断一个对象是空对象
使用 JSON 自带的.stringify 方法来判断
JSON.stringify(obj) === '{}'
使用 ES6 新增的方法 Object.keys()来判断if (Object.keys(obj).length === 0) { }
8. 什么情况下会发生布尔值的隐式强制类型转换?
( 1 ) if (..) 语句中的条件判断表达式。( 2 ) for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。( 3 ) while (..) 和 do..while(..) 循环中的条件判断表达式。( 4 ) ? : 中的条件判断表达式。( 5 ) 逻辑运算符 || (逻辑或)和 && (逻辑与)左边的操作数(作为条件判断表达式)。
9. || 和 && 操作符的返回值?
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件判断。对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。&& 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果。
10. const 对象的属性可以修改吗
const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const 只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。
11. 如果 new 一个箭头函数的会怎么样
箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用 arguments 参数,所以不能 New 一个箭头函数。
new 操作符的实现步骤如下:
1.创建一个对象
2.将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的 prototype 属性)
3.指向构造函数中的代码,构造函数中的 this 指向该对象(也就是为这个对象添加属性和方法)
4.返回新的对象所以,上面的第二、三步,箭头函数都是没有办法执行的。
12、说几条写 JavaScript 的基本规范?
在平常项目开发中,我们遵守一些这样的基本规范,比如说:( 1 )一个函数作用域中所有的变量声明应该尽量提到函数首部,用一个 var 声明,不允许出现两个连续的 var 声明,声明时如果变量没有值,应该给该变量赋值对应类型的初始值,便于他人阅读代码时,能够一目了然的知道变量对应的类型值。( 2 )代码中出现地址、时间等字符串时需要使用常量代替。( 3 )在进行比较的时候吧,尽量使用 '===', '!==' 代替 '==', '!=' 。( 4 )不要在内置对象的原型上添加方法,如 Array, Date 。( 5 ) switch 语句必须带有 default 分支。( 6 ) for 循环必须使用大括号。( 7 ) if 语句必须使用大括号
13. 扩展运算符的作用及使用场景
(1)对象扩展运算符
对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷
贝到当前对象之中。
上述方法实际上等价于:
Object.assign 方法用于对象的合并,将源对象(source)的所有可
枚举属性,复制到目标对象(target)。
利用上述特性就可以很方便的修改对象的部分属性。在 redux 中的
reducer 函数规定必须是一个纯函数,reducer 中的 state 对象要求
不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,
然后产生一个新的对象返回。
需要注意:扩展运算符对对象实例的拷贝属于浅拷贝。
(2)数组扩展运算符
数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每
次只能展开一层数组。
下面是数组的扩展运算符的应用:
将数组转换为参数序列
复制数组
要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷
贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都
是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。
合并数组
如果想在数组内合并数组,可以这样:
扩展运算符与解构赋值结合起来,用于生成数组
需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一
位,否则会报错。
将字符串转为真正的数组
任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组
比较常见的应用是可以将某些数据结构转为数组:
用于替换 es5 中的 Array.prototype.slice.call(arguments)写法。
使用 Math 函数获取数组中特定的值
14. Proxy 可以实现什么功能?
在 Vue3.0 中通过 Proxy 来替换原本的 Object.defineProperty
来实现数据响应式。
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
代表需要添加代理的对象,handler 用来自定义对象中的操作,比如
可以用来自定义 set 或者 get 函数。
下面来通过 Proxy 来实现一个数据响应式:在上述代码中,通过自定义 set 和 get 函数的方式,在原本的逻辑
中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出
通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,
需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用
Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属
性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现
有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式
的数据改变,唯一缺陷就是浏览器的兼容性不好。
15. 常用的正则表达式有哪些?
16、栈溢出及解决方法?
栈溢出(stack Overflow)缓冲区溢出是由于C语言系列设有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。
栈溢出就是缓冲区溢出的一种。 由于缓冲区溢出而使得有用的存储单元被改写, 往往会引发不可预料的后果。程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,通常称这些空间为缓冲区。如果向缓冲区中写入超过其本身长度的数据,以致于缓冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢出。缓冲区长度一般与用户自己定义的缓冲变量的类型有关。由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。向这些单元写入任意的数据,一般只会导致程序崩溃之类的事故,对这种情况我们也至多说这个程序有Bug。但如果向这些单元写入的是精心准备好的数据,就可能使得程序流程被劫持,致使不希望的代码被执行,落入攻击者的掌控之中,这就不仅仅是bug,而是漏洞(exploit)了。
栈溢出的解决方法
减少栈空间的需求,不要定义占用内存较多的auto变量,应该将此类变量修改成指针,从堆空间分配内存。
函数参数中不要传递大型结构/联合/对象,应该使用引用或指针作为函数参数。
减少函数调用层次,慎用递归函数,例如A->B->C->A环式调用。
17、深拷贝与浅拷贝
一、常见的 “浅” 拷贝方法:
除了上面我们演示的对于赋值操作,下面将介绍一些开发中可能会用到,当然也可以会被面试官问到的实现深浅拷贝的方法。
1. Object.assign()
方法解释:方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象,它将返回目标对象可以实现一个浅拷贝的效果。
参数一:目标对象
参数二:源对象
var obj1 = {
a: 1,
b: 2,
c: ['c', 't', 'r']
}
var obj2 = Object.assign({}, obj1);
obj2.c[1] = 5;
obj2.b = 3
console.log(obj1); // {a:1,b:2,c:["c", 5, "r"]}
console.log(obj2); // {a:1,b:3,c:["c", 5, "r"]}
console.log(obj1.c); // ["c", 5, "r"]
console.log(obj2.c); // ["c", 5, "r"]
注意:可见Object.assign()方法对于一维数据是深拷贝效果,但是对于多维数据是浅拷贝效果。Object.assign是一个浅拷贝,它只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是仍是对象的话依然是浅拷贝,
2. slice()
方法解释:数组进行截取,如果不传参数,会使用默认值,得到一个与原数组元素相同的新数组。
参数一:截取的起始位置
参数二:截取的结束位置
var a = [1, [1, 2], 3, 4];
var b = a.slice();
a[0] = 99
b[1][0] = 2;
console.log(a); // [99,[2,2],3,4]
console.log(b); // [1,[2,2],3,4]
注意:可见slice()方法也只是对一维数据进行深拷贝,但是对于多维的数据还是浅拷贝效果。
3. concat()方法
方法解释:数组的拼接(将多个数组或元素拼接形成一个新的数组),不改变原数组,如果不传参数,会使用默认值,得到一个与原数组元素相同的新数组 (复制数组)。
var a = [1, 2, [3, 4]]
var c = [];
var b = c.concat(a);
a[0] = 99
b[2][1] = 88
console.log(a); // [99,2,[3,88]]
console.log(b); // [1,2,[3,88]]
注意:可见concat()方法也只对一维数据具有深拷贝效果,对于多维的数据任然只是浅拷贝
4. ES6拓展运算符
var a = [1, 2, [3, 4]]
var b = [...a];
a[2][1] = 88
b[1] = 99
console.log(a); // [1,2,[3,88]]
console.log(b); // [1,99,[3,88]]
注意: 可见ES6的展开运算符对于一维数据是深拷贝效果,但是对于多维数据任然是浅拷贝效果。
二、实现 “深” 拷贝常见方法:
1. JSON.parse(JSON.stringify(obj))
JSON.stringify()是目前前端开发过程中最常用的深拷贝方式,原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象
JSON.stringfy() 将对象序列化成json对象
JSON.parse() 反序列化——将json对象反序列化成js对象
function deepCopy(obj1){
let _obj = JSON.stringify(obj1);
let obj2 = JSON.parse(_obj);
return obj2;
}
var a = [1, [1, 2], 3, 4];
var b = deepCopy(a);
b[1][0] = 2;
console.log(a); // 1,1,2,3,4
console.log(b); // 1,2,2,3,4
注意:它会抛弃对象的constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object类型,这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,
也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。
2. 使用第三方库实现对象的深拷贝,比如:lodash、jQuery
import lodash from 'lodash'
var objects = [1,{ 'a': 1 }, { 'b': 2 }];
var deep = lodash.cloneDeep(objects);
deep[0] = 2;
deep[1].a = 2;
console.log(objects); // [1,{ 'a': 1 }, { 'b': 2 }]
console.log(deep); //[2,{ 'a': 2 }, { 'b': 2 }]
3. 递归
这里简单封装了一个deepClone的函数,for in遍历传入参数的值,如果值是引用类型则再次调用deepClone函数,并且传入第一次调用deepClone参数的值作为第二次调用deepClone的参数,如果不是引用类型就直接复制
var obj1 = {
a:{
b:1
}
};
function deepClone(obj) {
var cloneObj = {}; //在堆内存中新建一个对象
for(var key in obj){ //遍历参数的键
if(typeof obj[key] ==='object'){
cloneObj[key] = deepClone(obj[key]) //值是对象就再次调用函数
}else{
cloneObj[key] = obj[key] //基本类型直接复制值
}
}
return cloneObj
}
var obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); //{a:{b:1}}
但是还有很多问题
- 首先这个deepClone函数并不能复制不可枚举的属性以及Symbol类型
- 这里只是针对Object引用类型的值做的循环迭代,而对于Array,Date,RegExp,Error,Function引用类型无法正确拷贝
- 对象循环引用成环了的情况
二、Vue篇
1. Vue 的基本原理
当 一 个 Vue 实 例 创 建 时 , Vue 会 遍 历 data 中 的 属 性 , 用Object.defineProperty ( vue3.0 使 用 proxy ) 将 它 们 转 为getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
2. 双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过
Object.defineProperty()来劫持各个属性的 setter,getter,在数
据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
1.需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,
都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会
触发 setter,那么就能监听到了数据变化
2.compile 解析模板指令,将模板中的变量替换成数据,然后初始化
渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数
据的订阅者,一旦数据有变动,收到通知,更新视图
3.Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做
的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②
自身必须有一个 update()方法 ③待属性变动 dep.notice()通知时,
能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功
成身退。
4.MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher
三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile
来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile
之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input)
-> 数据 model 变更的双向绑定效果。
3. MVVM、MVC、MVP 的区别
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离
关注点的方式来组织代码结构,优化开发效率。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有
的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户
事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,
可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗
长、混乱,这样对项目开发和后期的项目维护是非常不利的。
(1)MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。
其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,
以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,
当 Model 层发生改变的时候它会通知有关 View 层更新页面。
Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应
用的响应操作,当用户与页面产生交互的时候,Controller 中的事
件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修
改,然后 Model 层再去通知 View 层更新。
(2)MVVM
MVVM 分为 Model、View、ViewModel:
Model 代表数据模型,数据和业务逻辑都在 Model 层中定义;
View 代表 UI 视图,负责数据的展示;
ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理
用户交互操作;
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,
Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中
的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改
变的数据也会在 Model 中同步。
这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要
专注于数据的维护操作即可,而不需要自己操作 DOM。
(3)MVP
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在
MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时
候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,
当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对
代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实
现对 View 层和 Model 层的解耦。MVC 中的 Controller 只知道
Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,
View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将
Model 的变化和 View 的变化绑定在一起,以此来实现 View 和
Model 的同步更新。这样就实现了对 View 和 Model 的解耦,
Presenter 还包含了其他的响应逻辑。
4. slot 是什么?有什么作用?原理是什么?
slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用
slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板
标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决
定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。默认插槽:又名匿名插槽,当 slot 没有指定 name 属性值的时候一个
默认显示插槽,一个组件内只有有一个匿名插槽。
具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一
个组件可以出现多个具名插槽。作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也
可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可
以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过
来的数据决定如何渲染该插槽。
实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的
内容,存放在 vm.$slot 中,默认插槽为 vm.$slot.default,具名插
槽为 vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇
到 slot 标签,使用$slot 中的内容进行替换,此时可以为插槽传递
数据,若存在数据,则可称该插槽为作用域插槽。
5. $nextTick 原理及作用
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的
一种应用。
nextTick 的 核 心 是 利 用 了 如 Promise 、 MutationObserver 、
setImmediate、setTimeout 的原生 JavaScript 方法来模拟对应的
微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发
者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时
机的后续逻辑处理
nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中
的示例,引入异步更新队列机制的原因∶
如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM
的渲染,可以减少一些无用渲染
同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的
信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更
新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后
的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所
以异步渲染变得更加至关重要
Vue 采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作
DOM。有时候,可能遇到这样的情况,DOM1 的数据发生了变化,而 DOM2
需要从 DOM1 中获取数据,那这时就会发现 DOM2 的视图并没有更新,
这时就需要用到了 nextTick 了。
由于 Vue 的 DOM 操作是异步的,所以,在上面的情况中,就要将 DOM2
获取数据的操作写在$nextTick 中。
所以,在以下情况下,会用到 nextTick:
在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变
化的 DOM 结构的时候,这个操作就需要方法在 nextTick()的回调函
数中。
在 vue 生命周期中,如果在 created()钩子进行 DOM 操作,也一定要
放在 nextTick()的回调函数中。
因为在 created()钩子函数中,页面的 DOM 还未渲染,这时候也没办
法操作 DOM,所以,此时如果想要操作 DOM,必须将操作的代码放在
nextTick()的回调函数中。
6、Vue2.0和Vue3.0的区别?
前言
vue经历从2.0到3.0更新之后,简⽽⾔之就是变得更轻,更快,使⽤起来更加⽅便,每⼀次的版本迭代都是对上⼀个版本的升级优化,不管 是对于我们开发者还是对于⽤户体验都是不断地在越来越⽅便,接下来我会着重于开发者来说⼀下两个不同版本的区别详解
Vue2和vue3的初始化就存在着⼀定区别,⽐如vue3.0可以在安装脚⼿架同时提前安装好⼀些项⽬开发必备的插件,并且3.0提供了可视化创建脚⼿架,可以更加⽅便的对插件和依赖进⾏管理和配置,同时两个版本的⽬录结构也是有些许差别的。(比如:Vue3相对于Vue2,打包工具Vite替代了webpack;TS替代了JS,pinia替代了vuex;Element-plus替代了Element等等)
在开发过程中两个版本的使⽤⽅法虽然在表⾯上没有太⼤的⼀个区别,但是在他的底层⽅⾯去看的话区别还是很⼤的,其中就包括渲染⽅式,数据监听,双向绑定,⽣命周期,vue3更精准变更通知,这⾥着重说⼀下关于双向绑定的更新,
vue2 的双向数据绑定是利⽤ES5的⼀个 API ,Object.definePropert()对数据进⾏劫持 结合发布订阅模式的⽅式来实现的。
vue3 中使⽤了 ES6 的 ProxyAPI 对数据代理,通过 reactive() 函数给每⼀个对象都包⼀层 Proxy,通过 Proxy 监听属性的变化,从⽽实现对数据的监控。
7、Vue3带来了什么改变?
性能的提升打包大小减少41% , 初次渲染快55%, 更新渲染快133% ,内存减少54%
- 源码的升级
- 使用Proxy代替defineProperty实现响应式
- 重写虚拟DOM的实现和Tree-Shaking
- Vue3可以更好的支持TypeScript
- 新的特性
- Composition API(组合API)
- (1)setup配置
- (2)ref与reactive
- (3)watch与watchEffect
- (4)provide与inject
- 新的内置组件
- (1)Fragment
- (2)Teleport
- (3)Suspense
- 其他改变
- (1)新的生命周期钩子
- (2)data 选项应始终被声明为一个函数
- (3)移除keyCode支持作为 v-on 的修饰符
8、生命周期(vue2和vue3的生命周期对比)有哪些?
vue2.x的生命周期
vue3.0的生命周期
Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:
beforeDestroy改名为 beforeUnmount
destroyed改名为 unmountedVue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
beforeCreate===>setup()
created=======>setup()
beforeMount ===>onBeforeMount
mounted=======>onMounted
beforeUpdate===>onBeforeUpdate
updated =======>onUpdated
beforeUnmount ==>onBeforeUnmount
unmounted =====>onUnmounted
9. Vue 子组件和父组件执行顺序
加载渲染过程:
1.父组件 beforeCreate
2.父组件 created
3.父组件 beforeMount
4.子组件 beforeCreate
5.子组件 created
6.子组件 beforeMount
7.子组件 mounted
8.父组件 mounted
更新过程:
1. 父组件 beforeUpdate
2.子组件 beforeUpdate
3.子组件 updated
4.父组件 updated
销毁过程:
1. 父组件 beforeDestroy
2.子组件 beforeDestroy
3.子组件 destroyed
4.父组件 destoryed
10. created 和 mounted 的区别
created:在模板渲染成 html 前调用,即通常初始化某些属性值,然
后再渲染成视图。
mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再
对 html 的 dom 节点进行一些需要的操作
4. 一般在哪个生命周期请求异步数据
我们可以在钩子函数 created、beforeMount、mounted 中进行调用,
因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的
数据进行赋值。
推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函
数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面加载时间,用户体验更好;
SSR 不支持 beforeMount 、mounted 钩子函数,放在 created 中有
助于一致性。
11. keep-alive 中的生命周期哪些
keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——
在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:
deactivated、activated。同时,beforeDestroy 和 destroyed 就
不会再被触发了,因为组件不会被真正销毁。
当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;
当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子
函数。
12. 路由的 hash 和 history 模式的区别
Vue-Router 有两种模式:hash 模式和 history 模式。默认的路由模
式是 hash 模式。
1. hash 模式
简介: hash 模式是开发中默认的模式,它的 URL 带着一个#,例如http://www.abc.com/#/vue,它的 hash 值就是#/vue。特点:hash 值会出现在 URL 里面,但是不会出现在 HTTP 请求中,对后端完全没有影响。所以改变 hash 值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的 IE 浏览器也支持这种模式。hash路由被称为是前端路由,已经成为 SPA(单页面应用)的标配。
原理: hash 模式的主要原理就是 onhashchange()事件:使用 onhashchange()事件的好处就是,在页面的 hash 值发生变化时,无需向后端发起请求,window 就可以监听事件的改变,并按规则加
载相应的代码。除此之外,hash 值变化对应的 URL 都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后
端服务器,但是页面的 hash 值和对应的 URL 关联起来了。
2. history 模式
简介: history 模式的 URL 中没有#,它使用的是传统的路由分发模
式,即用户在输入一个 URL 时,服务器会接收这个请求,并解析这个
URL,然后做出相应的逻辑处理。
特 点 : 当 使 用 history 模 式 时 , URL 就 像 这 样 :
http://abc.com/user/id。相比 hash 模式更加好看。但是,history
模式需要后台配置支持。如果后台没有正确配置,访问时会返回 404。
API: history api 可以分为两大部分,切换历史状态和修改历史状态:
修 改 历 史 状 态 : 包 括 了 HTML5 History Interface 中 新 增 的
pushState() 和 replaceState() 方法,这两个方法应用于浏览器的
历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修
改时,虽然修改了 url,但浏览器不会立即向后端发送请求。如果要
做到改变 url 但又不刷新页面的效果,就需要前端用上这两个 API。
切换历史状态: 包括 forward()、back()、go()三个方法,对应浏
览器的前进,后退,跳转操作。
虽然 history 模式丢弃了丑陋的#。但是,它也有自己的缺点,就是
在刷新页面的时候,如果没有相应的路由或资源,就会刷出 404 来。
如果想要切换到 history 模式,就要进行以下配置(后端也要进行配
置):
3. 两种模式对比
调用 history.pushState() 相比于直接修改 hash,存在以下优势:
pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而
hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的
URL;
pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把
记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动
作将记录添加到栈中;
pushState() 通过 stateObject 参数可以添加任意类型的数据到记
录中;而 hash 只可添加短字符串;
pushState() 可额外设置 title 属性供后续使用。
hash 模式下,仅 hash 符号之前的 url 会被包含在请求中,后端如果
没有做到对路由的全覆盖,也不会返回 404 错误;history 模式下,
前端的 url 必须和实际向后端发起请求的 url 一致,如果没有对用的
路由处理,将返回 404 错误。
hash 模式和 history 模式都有各自的优势和缺陷,还是要根据实际
情况选择性的使用。
三、React篇
1、什么是 JSX ?
当 Facebook 第一次发布 React 时,他们还引入了一种新的 JS 方言 JSX,将原始 HTML 模板嵌入到 JS 代码中。JSX 代码本身不能被浏览器读取,必须使用Babel和webpack等工具将其转换为传统的JS。很多开发人员就能无意识使用 JSX,因为它已经与 React 结合在一起了。
class MyComponent extends React.Component { render() { let props = this.props; return ( <div className="my-component"> <a href={props.url}>{props.name}</a> </div> ); } }
JSX的特点
- 可以将HTML语言直接写在JavaScript语言之中,不加任何引号,这就是JSX的语法,它允许HTML与JavaScript的混写。
- JSX允许直接在模板插入JavaScript变量。如果这个变量是一个数组,则会展开这个数组的所有成员。
- 防注入攻击
- 在JSX中嵌入用户输入是安全的;
- React DOM在渲染之前默认会过滤所有传入的值。它可以确保应用不会被注入攻击。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止XSS(跨站脚本攻击)
- Babel转译器会把JSX转换成一个名为React.createElement()的方法调用。
- 如果在普通的html里面要写jsx语法,要将script的type改成text/jsx,这是因为React独有的JSX语法跟JavaScript不兼容。凡是使用JSX的地方,都要加上type=“text/jsx”。其次,React提供俩个库:react.js和JSXTransformer.js,它们必须首先加载。其中,JSXTransformer.js的作用是将JSX语法转为JavaScript语法。这一步很消耗时间,实际上线的时候,应该把它放到服务器完成。
2. React 如何判断什么时候重新渲染组件?
组件状态的改变可以因为 props 的改变,或者直接通过 setState 方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的 shouldComponentUpdate 方法默认返回 true,这就是导致每次更新都重新渲染的原因。当React将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回 true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate 方法让它根据情况返回 true 或者 false 来告诉 React 什么时候重新渲染什么时候跳过重新渲染。
3. React 中可以在 render 访问 refs 吗?为什么?
不可以,render 阶段 DOM 还没有生成,无法获取 DOM。DOM 的获取
需要在 pre-commit 阶段和 commit 阶段:
4、React 的生命周期方法有哪些?
React的生命周期方法有很多,并且在不同的React版本中可能会有一些变化。为了更好地理解React的生命周期,我将它们按照React 16版本及其之后的版本进行整理,以帮助你更好地理解。
在React 16版本及其之后,React的生命周期方法可分为三个阶段:挂载阶段、更新阶段和卸载阶段。以下是React 16版本及其之后的生命周期方法列表:
挂载阶段:
constructor:组件实例化时调用,用于初始化状态和绑定方法。
static getDerivedStateFromProps:在渲染之前调用,用于根据新的属性值计算并返回一个新的状态。
render:渲染组件的内容。
componentDidMount:组件挂载后调用,可以进行异步操作、订阅事件等。
更新阶段:
static getDerivedStateFromProps:在渲染之前调用,用于根据新的属性值计算并返回一个新的状态。
shouldComponentUpdate:在渲染之前调用,用于决定是否重新渲染组件,默认返回true。
render:渲染组件的内容。
getSnapshotBeforeUpdate:在最终将内容渲染到DOM之前调用,用于获取DOM更新前的快照。
componentDidUpdate:组件更新后调用,可以进行DOM操作、发起网络请求等。
卸载阶段:
componentWillUnmount:组件卸载前调用,可以进行清理操作,如取消订阅、清除定时器等。
另外,React 16.3版本后引入了以下生命周期方法:static getDerivedStateFromError:在子组件渲染过程中,如果发生错误,会调用该方法,返回一个新的状态。
componentDidCatch:在子组件渲染过程中,如果发生错误,会调用该方法,用于记录错误信息或上报错误。
需要注意的是,React 17版本之后,一些生命周期方法被标记为过时,并推荐使用其他替代方法来实现相应的功能。在使用React时,可以根据具体的需求和React版本来选择合适的生命周期方法。同时,React还提供了钩子函数的方式(如useEffect钩子)来完成与生命周期相关的操作,这也是React 16.8版本及其之后的新特性。
5、React和Vue.js的相似性和差异性是什么?
相似性如下。(1)都是用于创建UI的 JavaScript库。
(2)都是快速和轻量级的代码库(这里指 React核心库)。
(3)都有基于组件的架构。
(4)都使用虚拟DOM。
(5)都可以放在单独的HTML文件中,或者放在 Webpack设置的一个更复杂的模块中。
(6)都有独立但常用的路由器和状态管理库。
它们最大的区别在于 Vue. js通常使用HTML模板文件,而 React完全使用 JavaScript创建虚拟DOM。 Vue. js还具有对于“可变状态”的“ reactivity”的重新渲染的自动化检测系统。(React一般适用于大型项目)
6、React的Hooks详解
Hook是什么Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
React 内置了一些像 useState 这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。React提供了多个常用的Hooks,用于在函数组件中管理状态、处理副作用和访问React的上下文等。以下是React所有的常用Hooks列表:
useState:用于在函数组件中添加状态。
useEffect:用于在函数组件中执行副作用操作。
useContext:用于在函数组件中访问React的上下文。
useReducer:用于在函数组件中使用Reducer模式来管理状态。
useRef:用于在函数组件中创建可变的引用。
useMemo:用于在函数组件中缓存计算的值。
useCallback:用于在函数组件中缓存函数。
useLayoutEffect:类似于useEffect,但在DOM更新之后同步执行。
useImperativeHandle:用于在函数组件中自定义外部组件实例的暴露。
useDebugValue:用于在自定义Hooks中显示自定义的调试值。
这些Hooks可以帮助你更方便地编写和管理React函数组件。同时,你还可以根据需要自定义自己的Hooks来封装和复用逻辑。React的Hooks特性在React 16.8版本及其之后引入,它们提供了一种更简洁、灵活的方式来编写可复用的React组件逻辑。
7. React setState 调用之后发生了什么?是同步还是异步?
(1)React 中 setState 后发生了什么
在代码中调用 setState 函数之后,React 会将传入的参数对象与组
件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和
过程,React 会以相对高效的方式根据新的状态构建 React 元素树
并且着手重新渲染整个 UI 界面。
在 React 得到元素树之后,React 会自动计算出新的树与老树的节
点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,
React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,
这就保证了按需更新,而不是全部重新渲染。
如果在短时间内频繁 setState。React 会将 state 的改变压入栈中,
在合适的时机,批量更新 state 和视图,达到提高性能的效果。(2)setState 是同步还是异步的
假如所有 setState 是同步的,意味着每执行一次 setState 时(有可
能一个同步代码中,多次 setState),都重新 vnode diff + dom 修
改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代
码中的多个 setState 合并成一次组件更新。所以默认是异步的,但
是在一些情况下是同步的。
setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而
不同。在源码中,通过 isBatchingUpdates 来判断 setState 是先
存进 state 队列还是直接更新,如果值为 true 则执行异步操作,
为 false 则直接更新。
异步:在 React 可以控制的地方,就为 true,比如在 React 生命
周期事件和合成事件中,都会走合并操作,延迟更新的策略。
同步:在 React 无法控制的地方,比如原生事件,具体就是在
addEventListener 、setTimeout、setInterval 等事件中,就只能
同步更新。
一般认为,做异步设计是为了性能优化、减少渲染次数:
setState设计为异步,可以显著的提升性能。如果每次调用 setState
都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新
渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后
进行批量更新;
如果同步更新了 state,但是还没有执行 render 函数,那么 state
和 props 不能保持同步。state 和 props 不能保持一致性,会在开发
中产生很多的问题;
8. React 中的 props 为什么是只读的?
this.props 是组件之间沟通的一个接口,原则上来讲,它只能从父组件流向子组件。React 具有浓重的函数式编程的思想。提到函数式编程就要提一个概念:纯函数。它有几个特点:
- 给定相同的输入,总是返回相同的输出。
- 过程没有副作用。
- 不依赖外部状态。
this.props 就是汲取了纯函数的思想。props 的不可以变性就保证的相同的输入,页面显示的内容是一样的,并且不会产生副作用
9、如何理解React State不可变性的原则
在 React 中,不可变性是指数据一旦被创建,就不能被修改。React 推崇使用不可变数据的原则,这意味着在更新数据时,应该创建新的数据对象而不是直接修改现有的数据。
以下是理解 React 中不可变性原则的几个关键点:
数据一旦创建就不能被修改:在 React 中,组件的状态(state)和属性(props)应该被视为不可变的。一旦创建了状态或属性对象,就不应该直接修改它们的值。这样可以确保组件的数据在更新时是不可变的,从而避免意外的数据改变和副作用。
创建新的数据对象:当需要更新状态或属性时,应该创建新的数据对象。这可以通过使用对象展开运算符、数组的 concat()、slice() 等方法,或者使用不可变数据库(如Immutable.js、Immer 等)来创建新的数据副本。
比较数据变化:React 使用 Virtual DOM 来比较前后两个状态树的差异,并仅更新需要更新的部分。通过使用不可变数据,React 可以更高效地进行比较,因为它可以简单地比较对象引用是否相等,而不必逐个比较对象的属性。
性能优化:使用不可变数据可以带来性能上的优势。由于 React 可以更轻松地比较前后状态的差异,可以减少不必要的重新渲染和组件更新,提高应用的性能和响应性。不可变性的原则在 React 中有以下好处:
简化数据变更追踪:由于数据不可变,可以更轻松地追踪数据的变化。这样可以更好地理解代码的行为和数据的流动。
避免副作用:可变数据容易引发副作用和难以追踪的 bug。通过使用不可变数据,可以避免许多与副作用相关的问题。
方便的历史记录和回滚:不可变数据使得记录和回滚应用状态的历史变得更容易。可以在不改变原始数据的情况下,创建和保存不同时间点的数据快照
10. React 16.X 中 props 改变后在哪个生命周期中处理
在 getDerivedStateFromProps 中进行处理。
这个生命周期函数是为了替代 componentWillReceiveProps 存在的,
所以在需要使用 componentWillReceiveProps 时,就可以考虑使用
getDerivedStateFromProps 来进行替代。
两者的参数是不相同的,而 getDerivedStateFromProps 是一个静态
函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不
推荐直接访问属性。而是应该通过参数提供的 nextProps 以及
prevState 来进行判断,根据新传入的 props 来映射到 state。
需要注意的是,如果 props 传入的内容不需要影响到你的 state,那
么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到
函数的末尾:
四、HTML篇
1. 对 HTML 语义化的理解
语义化是指根据内容的结构化(内容语义化),选择合适的标签(代码语义化)。通俗来讲就是用正确的标签做正确的事情。
语义化的优点如下:
对机器友好,带有语义的文字表现力丰富,更适合搜索引擎的爬虫爬取有效信息,有利于 SEO。除此之外,语义类还支持读屏软件,根据文章可以自动生成目录;
对开发者友好,使用语义类标签增强了可读性,结构更加清晰,开发者能清晰的看出网页的结构,便于团队的开发与维护。
常见的语义化标签:
2. DOCTYPE(⽂档类型) 的作⽤
DOCTYPE 是 HTML5 中一种标准通用标记语言的文档类型声明,它的目的是告诉浏览器(解析器)应该以什么样(html 或 xhtml)的文档类型定义来解析文档,不同的渲染模式会影响浏览器对 CSS 代码甚⾄JavaScript 脚本的解析。它必须声明在 HTML⽂档的第⼀⾏。
浏览器渲染页面的两种模式(可通过 document.compatMode 获取,比如,语雀官网的文档类型是 CSS1Compat):
CSS1Compat:标准模式(Strick mode),默认模式,浏览器使用 W3C
的标准解析渲染页面。在标准模式中,浏览器以其支持的最高标准呈
现页面。
BackCompat:怪异模式(混杂模式)(Quick mode),浏览器使用自己的
怪异模式解析渲染页面。在怪异模式中,页面以一种比较宽松的向后
兼容的方式显示。
3. script 标签中 defer 和 async 的区别
如果没有defer或async属性,浏览器会立即加载并执行相应的脚本。
它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样
就阻塞了后续文档的加载。
下图可以直观的看出三者之间的区别:
其中蓝色代表 js 脚本网络加载时间,红色代表 js 脚本执行时间,绿
色代表 html 解析。
defer 和 async 属性都是去异步加载外部的 JS 脚本文件,它们都不
会阻塞页面的解析,其区别如下:
执行顺序:多个带 async 属性的标签,不能保证加载的顺序;多个带
defer 属性的标签,按照加载顺序执行;
脚本是否并行执行:async 属性,表示后续文档的加载和执行与 js
脚本的加载和执行是并行进行的,即异步执行;defer 属性,加载后
续文档的过程和 js 脚本的加载(此时仅加载不执行)是并行进行的
(异步),js 脚本需要等到文档所有元素解析完成之后才执行,
DOMContentLoaded 事件触发执行之前。
4. 行内元素有哪些?块级元素有哪些? 空(void)元素有那些?
行内元素有:a b span img input select strong;
块级元素有:div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p;
空元素,即没有内容的 HTML 元素。空元素是在开始标签中关闭的,
也就是空元素没有闭合标签:
常见的有:<br>、<hr>、<img>、<input>、<link>、<meta>;
鲜见的有:<area>、<base>、<col>、<colgroup>、<command>、<embed>、
<keygen>、<param>、<source>、<track>、<wbr>。
5. 浏览器是如何对 HTML5 的离线储存资源进行管理和加载?
在线的情况下,浏览器发现 html 头部有 manifest 属性,它会请求manifest 文件,如果是第一次访问页面 ,那么浏览器就会根据manifest 文件的内容下载相应的资源并且进行离线存储。如果已经
访问过页面并且资源已经进行离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的 manifest 文件与旧的manifest 文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,就会重新下载文件中的资源并进行离线存储。离线的情况下,浏览器会直接使用离线存储的资源。
6. Canvas 和 SVG 的区别
(1)SVG:
SVG 可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标
记语言 XML 描述的 2D 图形的语言,SVG 基于 XML 就意味着 SVG DOM
中的每个元素都是可用的,可以为某个元素附加 Javascript 事件处
理器。在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象
的属性发生变化,那么浏览器能够自动重现图形。其特点如下:
- 不依赖分辨率
- 支持事件处理器
- 最适合带有大型渲染区域的应用程序(比如谷歌地图)
- 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)
- 不适合游戏应用
(2)Canvas:
Canvas 是画布,通过 Javascript 来绘制 2D 图形,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。其特点如下:
- 依赖分辨率
- 不支持事件处理器
- 弱的文本渲染能力
- 能够以 .png 或 .jpg 格式保存结果图像
- 最适合图像密集型的游戏,其中的许多对象会被频繁重绘
注:矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。
7、 SGML 、 HTML 、XML 和 XHTML 的区别?
8、 行内元素定义
9、 块级元素定义
10、 行内元素与块级元素的区别?
11、HTML5 元素的分类
12、 空元素定义
13、 页面导入样式时,使用 link 和 @import 有什么区别?
14、 浏览器的渲染原理?
( 1 )首先解析收到的文档,根据文档定义构建一棵 DOM 树, DOM 树是由 DOM 元素及属性节点组成的。( 2 )然后对 CSS 进行解析,生成 CSSOM 规则树。( 3 )根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM 元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。( 4 )当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为 “ 自动重排 ” 。( 5 )布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。详细资料可以参考: 《浏览器渲染原理》 《浏览器的渲染原理简介》 《前端必读:浏览器内部工作原理》 《深入浅出浏览器渲染原理》
五、CSS篇
1. display 的 block、inline 和 inline-block 的区别
(1)block:会独占一行,多个元素会另起一行,可以设置 width、
height、margin 和 padding 属性;
(2)inline:元素不会独占一行,设置 width、height 属性无效。
但可以设置水平方向的 margin 和 padding 属性,不能设置垂直方向
的 padding 和 margin;
(3)inline-block:将对象设置为 inline 对象,但对象的内容作为
block 对象呈现,之后的内联对象会被排列在同一行内。
对于行内元素和块级元素,其特点如下:
(1)行内元素
- 设置宽高无效;
- 可以设置水平方向的 margin 和 padding 属性,不能设置垂直方向的
- padding 和 margin;
- 不会自动换行;
(2)块级元素
- 可以设置宽高;
- 设置 margin 和 padding 都有效;
- 可以自动换行;
- 多个块状,默认排列从上到下。
2. link 和@import 的区别
两者都是外部引用 CSS 的方式,它们的区别如下:
- link 是 XHTML 标签,除了加载 CSS 外,还可以定义 RSS 等其他事务;
- @import 属于 CSS 范畴,只能加载 CSS。
- link 引用 CSS 时,在页面载入时同时加载;@import 需要页面网页完全载入以后加载。
- link 是 XHTML 标签,无兼容问题;@import 是在 CSS2.1 提出的,低版本的浏览器不支持。
- link 支持使用 Javascript 控制 DOM 去改变样式;而@import 不支持。
3. CSS3 中有哪些新特性
- 新增各种 CSS 选择器 (: not(.input):所有 class 不是“input”
- 的节点)
- 圆角 (border-radius:8px)
- 多列布局 (multi-column layout)
- 阴影和反射 (Shadoweflect)
- 文字特效 (text-shadow)
- 文字渲染 (Text-decoration)
- 线性渐变 (gradient)
- 旋转 (transform)
- 增加了旋转,缩放,定位,倾斜,动画,多背景
4. 对 CSS Sprites 的理解
CSSSprites(精灵图),将一个页面涉及到的所有图片都包含到一张大图中去,然后利用 CSS 的 background-image,background-repeat,background-position 属性的组合进行背景定位。
优点:
利用 CSS Sprites 能很好地减少网页的 http 请求,从而大大提高了页面的性能,这是 CSS Sprites 最大的优点;CSS Sprites 能减少图片的字节,把 3 张图片合并成 1 张图片的字节总是小于这 3 张图片的字节总和。缺点:
在图片合并时,要把多张图片有序的、合理的合并成一张图片,还要留好足够的空间,防止板块内出现不必要的背景。在宽屏及高分辨率下的自适应页面,如果背景不够宽,很容易出现背景断裂;CSSSprites 在开发的时候相对来说有点麻烦,需要借助 photoshop或其他工具来对每个背景单元测量其准确的位置。
维护方面:CSS Sprites 在维护的时候比较麻烦,页面背景有少许改动时,就要改这张合并的图片,无需改的地方尽量不要动,这样避免改动更多的 CSS,如果在原来的地方放不下,又只能(最好)往下加图片,这样图片的字节就增加了,还要改动 CSS。
5. CSS 优化和提高性能的方法有哪些?
加载性能:
(1)css 压缩:将写好的 css 进行打包压缩,可以减小文件体积。
(2)css 单一样式:当需要下边距和左边距的时候,很多时候会选择 使 用 margin:top 0 bottom 0 ; 但margin-bottom:bottom;margin-left:left;执行效率会更高。
(3)减少使用@import,建议使用 link,因为后者在页面加载时一起加载,前者是等待页面加载完成之后再进行加载。
选择器性能:
(1)关键选择器(key selector)。选择器的最后面的部分为关键选择器(即用来匹配目标元素的部分)。CSS 选择符是从右到左进行匹配的。当使用后代选择器的时候,浏览器会遍历所有子元来确定是否是指定的元素等等;
(2)如果规则拥有 ID 选择器作为其关键选择器,则不要为规则增加标签。过滤掉无关的规则(这样样式系统就不会浪费时间去匹配它们了)。
(3)避免使用通配规则,如*{}计算次数惊人,只对需要用到的元素
进行选择。
(4)尽量少的去对标签进行选择,而是用 class。
(5)尽量少的去使用后代选择器,降低选择器的权重值。后代选择器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三层,更多的使用类来关联每一个标签元素。
(6)了解哪些属性是可以通过继承而来的,然后避免对这些属性重复指定规则。
渲染性能:
(1)慎重使用高性能属性:浮动、定位。
(2)尽量减少页面重排、重绘。
(3)去除空规则:{}。空规则的产生原因一般来说是为了预留样
式。去除这些空规则无疑能减少 css 文档体积。
(4)属性值为 0 时,不加单位。
(5)属性值为浮动小数 0.**,可以省略小数点之前的 0。
(6)标准化各种浏览器前缀:带浏览器前缀的在前。标准属性在后。
(7)不使用@import 前缀,它会影响 css 的加载速度。
(8)选择器优化嵌套,尽量避免层级过深。
(9)css 雪碧图,同一页面相近部分的小图标,方便使用,减少页面的请求次数,但是同时图片本身会变大,使用时,优劣考虑清楚,再使用。
(10)正确使用 display 的属性,由于 display 的作用,某些样式组合会无效,徒增样式体积的同时也影响解析性能。
(11)不滥用 web 字体。对于中文网站来说 WebFonts 可能很陌生,国外却很流行。web fonts 通常体积庞大,而且一些浏览器在下载 webfonts 时会阻塞页面渲染损伤性能。
可维护性、健壮性:
(1)将具有相同属性的样式抽离出来,整合并通过 class 在页面中
进行使用,提高 css 的可维护性。
(2)样式与内容分离:将 css 代码定义到外部 css 中。
6. 对 CSS 工程化的理解
CSS 工程化是为了解决以下问题:
1.宏观设计:CSS 代码如何组织、如何拆分、模块结构怎样设计?
2.编码优化:怎样写出更好的 CSS?
3.构建:如何处理我的 CSS,才能让它的打包结果最优?
4.可维护性:代码写完了,如何最小化它后续的变更成本?如何确保
任何一个同事都能轻松接手?
以下三个方向都是时下比较流行的、普适性非常好的 CSS 工程化实
践:
预处理器:Less、 Sass 等;
重要的工程化插件: PostCss;
Webpack loader 等 。
基于这三个方向,可以衍生出一些具有典型意义的子问题,这里我们
逐个来看:
(1)预处理器:为什么要用预处理器?它的出现是为了解决什么问
题?
预处理器,其实就是 CSS 世界的“轮子”。预处理器支持我们写一
种类似 CSS、但实际并不是 CSS 的语言,然后把它编译成 CSS 代码:
那为什么写 CSS 代码写得好好的,偏偏要转去写“类 CSS”呢?这
就和本来用 JS 也可以实现所有功能,但最后却写 React 的 jsx 或
者 Vue 的模板语法一样——为了爽!要想知道有了预处理器有多爽,
首先要知道的是传统 CSS 有多不爽。随着前端业务复杂度的提高,
前端工程中对 CSS 提出了以下的诉求:
1.宏观设计上:我们希望能优化 CSS 文件的目录结构,对现有的 CSS
文件实现复用;
2.编码优化上:我们希望能写出结构清晰、简明易懂的 CSS,需要它
具有一目了然的嵌套层级关系,而不是无差别的一铺到底写法;我们
希望它具有变量特征、计算能力、循环能力等等更强的可编程性,这
样我们可以少写一些无用的代码;
3.可维护性上:更强的可编程性意味着更优质的代码结构,实现复用
意味着更简单的目录结构和更强的拓展能力,这两点如果能做到,自
然会带来更强的可维护性。
这三点是传统 CSS 所做不到的,也正是预处理器所解决掉的问题。
预处理器普遍会具备这样的特性:
嵌套代码的能力,通过嵌套来反映不同 css 属性之间的层级关系 ;
支持定义 css 变量;
提供计算函数;
允许对代码片段进行 extend 和 mixin;
支持循环语句的使用;
支持将 CSS 文件模块化,实现复用。
(2)PostCss:PostCss 是如何工作的?我们在什么场景下会使用
PostCss?
PostCss 仍然是一个对 CSS 进行解析和处理的工具,它会对 CSS 做
这样的事情:
它和预处理器的不同就在于,预处理器处理的是 类 CSS,而 PostCss
处理的就是 CSS 本身。Babel 可以将高版本的 JS 代码转换为低版
本的 JS 代码。PostCss 做的是类似的事情:它可以编译尚未被浏览
器广泛支持的先进的 CSS 语法,还可以自动为一些需要额外兼容的
语法增加前缀。更强的是,由于 PostCss 有着强大的插件机制,支
持各种各样的扩展,极大地强化了 CSS 的能力。
PostCss 在业务中的使用场景非常多:
提高 CSS 代码的可读性:PostCss 其实可以做类似预处理器能做的
工作;
当 我 们 的 CSS 代 码 需 要 适 配 低 版 本 浏 览 器 时 , PostCss 的
Autoprefixer 插件可以帮助我们自动增加浏览器前缀;
允许我们编写面向未来的 CSS:PostCss 能够帮助我们编译 CSS next 代码;
(3)Webpack 能处理 CSS 吗?如何实现?
Webpack 能处理 CSS 吗:
Webpack 在裸奔的状态下,是不能处理 CSS 的,Webpack 本身是一个面向 JavaScript 且只能处理 JavaScript 代码的模块化打包工具;
Webpack 在 loader 的辅助下,是可以处理 CSS 的。
如何用 Webpack 实现对 CSS 的处理:
Webpack 中操作 CSS 需要使用的两个关键的 loader:css-loader和 style-loader
注意,答出“用什么”有时候可能还不够,面试官会怀疑你是不是在背答案,所以你还需要了解每个 loader 都做了什么事情:
css-loader:导入 CSS 模块,对 CSS 代码进行编译处理;
style-loader:创建 style 标签,把 CSS 内容写入标签。
在实际使用中,css-loader 的执行顺序一定要安排在 style-loader的前面。因为只有完成了编译过程,才可以对 css 代码进行插入;若提前插入了未编译的代码,那么 webpack 是无法理解这坨东西的,它会无情报错。
7. 常见的 CSS 布局单位
常用的布局单位包括像素(px),百分比(%),em,rem,vw/vh。
8. 水平垂直居中的实现
- 利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 translate 来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。
- 利用绝对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况:
- 利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 margin 负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况
- 使 用 flex 布 局 , 通 过 align-items:center 和justify-content:center设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要考虑兼容的问题,该方法在移动端用的较多:
10. 元素的层叠顺序
层叠顺序,英文称作 stacking order,表示元素发生层叠时有着特
定的垂直显示顺序。下面是盒模型的层叠规则:
179
对于上图,由上到下分别是:
(1)背景和边框:建立当前层叠上下文元素的背景和边框。
(2)负的 z-index:当前层叠上下文中,z-index 属性值为负的元素。
(3)块级盒:文档流内非行内级非定位后代元素。
(4)浮动盒:非定位浮动元素。
(5)行内盒:文档流内行内级非定位后代元素。
(6)z-index:0:层叠级数为 0 的定位元素。
(7)正 z-index:z-index 属性值为正的定位元素。
注意: 当定位元素 z-index:auto,生成盒在当前层叠上下文中的层
级为 0,不会建立新的层叠上下文,除非是根元素。
11. 如何解决 1px 问题?
1px 问题指的是:在一些 Retina 屏幕 的机型上,移动端页面的 1px
会变得很粗,呈现出不止 1px 的效果。原因很简单——CSS 中的 1px并不能和移动设备上的 1px 划等号。它们之间的比例关系有一个专门的属性来描述:
打开 Chrome 浏览器,启动移动端调试模式,在控制台去输出这个
devicePixelRatio 的值。这里选中 iPhone6/7/8 这系列的机型,输
出的结果就是 2:
这就意味着设置的 1px CSS 像素,在这个设备上实际会用 2 个物理像素单元来进行渲染,所以实际看到的一定会比 1px 粗一些。
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .text { display: inline-block; margin: 30px; /*border: 1px solid red;*/ /*如果直接给text设置transform: scale(0.5); 则这里面的所有元素都会缩小*/ /*transform: scale(0.5);*/ position: relative; } .text::after { content: ''; position: absolute; left: 0; top: 0; width: 200%; height: 200%; border: 1px solid red; /*transform: scale(0.5);会将宽高边框等都缩小0.5 所以宽高需要先扩大一倍*/ transform: scale(0.5); /*从左上角开始缩放 而不是元素的中心*/ transform-origin: 0 0; } </style> </head> <body> <!--添加0.5px 的边框--> <div class="text">满300减30</div> </body> </html>
六、前端工程化篇
1. webpack 与 grunt、gulp 的不同?
Grunt、Gulp 是基于任务运⾏的⼯具: 它们会⾃动执⾏指定的任务,
就像流⽔线,把资源放上去然后通过不同插件进⾏加⼯,它们包含活
跃的社区,丰富的插件,能⽅便的打造各种⼯作流。
Webpack 是基于模块化打包的⼯具: ⾃动化处理模块,webpack 把⼀
切当成模块,当 webpack 处理应⽤程序时,它会递归地构建⼀个依
赖关系图 (dependency graph),其中包含应⽤程序需要的每个模块,
然后将所有这些模块打包成⼀个或多个 bundle。
因此这是完全不同的两类⼯具,⽽现在主流的⽅式是⽤npm script 代
替 Grunt、Gulp,npm script 同样可以打造任务流。
2. webpack、rollup、parcel 优劣?
webpack 适⽤于⼤型复杂的前端站点构建: webpack 有强⼤的 loader
和插件⽣态,打包后的⽂件实际上就是⼀个⽴即执⾏函数,这个⽴即
执⾏函数接收⼀个参数,这个参数是模块对象,键为各个模块的路径,
值为模块内容。⽴即执⾏函数内部则处理模块之间的引⽤,执⾏模块
等,这种情况更适合⽂件依赖复杂的应⽤开发。
rollup 适⽤于基础库的打包,如 vue、d3 等: Rollup 就是将各个模
块打包进⼀个⽂件中,并且通过 Tree-shaking 来删除⽆⽤的代码, 可以最⼤程度上降低代码体积,但是rollup没有webpack如此多的的
如代码分割、按需加载等⾼级功能,其更聚焦于库的打包,因此更适
合库的开发。
parcel 适⽤于简单的实验性项⽬: 他可以满⾜低⻔槛的快速看到效
果,但是⽣态差、报错信息不够全⾯都是他的硬伤,除了⼀些玩具项
⽬或者实验项⽬不建议使⽤。
3. 有哪些常⻅的 Loader?
file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL
去引⽤输出的⽂件
url-loader:和 file-loader 类似,但是能在⽂件很⼩的情况下以
base64 的⽅式把⽂件内容注⼊到代码中去
source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调
试
image-loader:加载并且压缩图⽚⽂件
babel-loader:把 ES6 转换成 ES5
css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性
style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作
去加载 CSS。
eslint-loader:通过 ESLint 检查 JavaScript 代码
注意:在 Webpack 中,loader 的执行顺序是从右向左执行的。因为
webpack 选择了 compose 这样的函数式编程方式,这种方式的表达式
执行是从右向左的。
4. 有哪些常⻅的 Plugin?
define-plugin:定义环境变量
html-webpack-plugin:简化 html⽂件创建
uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度
webpack-bundle-analyzer: 可视化 webpack 输出⽂件的体积
mini-css-extract-plugin: CSS 提取到单独的⽂件中,⽀持按需加
载
5. bundle,chunk,module 是什么?
bundle:是由 webpack 打包出来的⽂件;
chunk:代码块,⼀个 chunk 由多个模块组合⽽成,⽤于代码的合并
和分割;
module:是开发中的单个模块,在 webpack 的世界,⼀切皆模块,⼀
个模块对应⼀个⽂件,webpack 会从配置的 entry 中递归开始找出所
有依赖的模块。
6. Loader 和 Plugin 的不同?
不同的作⽤:
Loader 直译为"加载器"。Webpack 将⼀切⽂件视为模块,但是 webpack
原⽣是只能解析 js⽂件,如果想将其他⽂件也打包的话,就会⽤到
loader 。 所以 Loader 的作⽤是让 webpack 拥有了加载和解析⾮
JavaScript⽂件的能⼒。
Plugin 直译为"插件"。Plugin 可以扩展 webpack 的功能,让 webpack
具有更多的灵活性。在 Webpack 运⾏的⽣命周期中会⼴播出许多事
件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的
API 改变输出结果。
不同的⽤法:
Loader 在 module.rules 中配置,也就是说他作为模块的解析规则
⽽存在。 类型为数组,每⼀项都是⼀个 Object ,⾥⾯描述了对于
什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数
( options )
Plugin在 plugins 中单独配置。类型为数组,每⼀项是⼀个 plugin
的实例,参数都通过构造函数传⼊。
7. webpack 热更新的实现原理?
webpack 的热更新⼜称热替换(Hot Module Replacement),缩写为
HMR。这个机制可以做到不⽤刷新浏览器⽽将新变更的模块替换掉旧
的模块。
原理:
⾸先要知道 server 端和 client 端都做了处理⼯作:
第⼀步,在 webpack 的 watch 模式下,⽂件系统中某⼀个⽂件发⽣
修改,webpack 监听到⽂件变化,根据配置⽂
件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript
对象保存在内存中。
第⼆步是 webpack-dev-server 和 webpack 之间的接⼝交互,⽽在
这⼀步,主要是 dev-server 的中间件 webpack- dev-middleware
和 webpack 之间的交互,webpack-dev-middleware 调⽤ webpack
暴露的 API 对代码变化进⾏监 控,并且告诉 webpack,将代码打包
到内存中。
第三步是 webpack-dev-server 对⽂件变化的⼀个监控,这⼀步不同于第⼀步,并不是监控代码变化重新打包。当我们在配置⽂件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置⽂件夹中静态⽂件的变化,变化后会通知浏览器端对应⽤进⾏ live reload。注意,这⼉是浏览器刷新,和 HMR 是两个概念。
第四步也是 webpack-dev-server 代码的⼯作,该步骤主要是通过
sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建
⽴⼀个 websocket ⻓连接,将 webpack 编译打包的各个阶段的状态
信息告知浏览器端,同时也包括第三步中 Server 监听静态⽂件变化
的信息。浏览器端根据这些 socket 消息进⾏不同的操作。当然服务
端传递的最主要信息还是新模块的 hash 值,后⾯的步骤根据这⼀
hash 值来进⾏模块热替换。
webpack-dev-server/client 端并不能够请求更新的代码,也不会执
⾏ 热 更 模 块 操 作 , ⽽ 把 这 些 ⼯ 作 ⼜ 交 回 给 了 webpack ,
webpack/hot/dev-server 的 ⼯ 作 就 是 根 据
webpack-dev-server/client 传给它的信息以及 dev-server 的配
置决定是刷新浏览器呢还是进⾏模块热更新。当然如果仅仅是刷新浏
览器,也就没有后⾯那些步骤了。
HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上
⼀ 步 传 递 给 他 的 新 模 块 的 hash 值 , 它 通 过
JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端
返回⼀个 json,该 json 包含了所有要更新的模块的 hash 值,获
取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块
代码。这就是上图中 7、8、9 步骤。
⽽第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,
HotModulePlugin 将会对新旧模块进⾏对⽐,决定是否更新模块,在
决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模
块间的依赖引⽤。
最后⼀步,当 HMR 失败后,回退到 live reload 操作,也就是进⾏
浏览器刷新来获取最新打包代码。
8. Babel 的原理是什么?
babel 的转译过程也分为三个阶段,这三步具体是:
解析 Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语
法分析的过程;
转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得
到 AST 并通过 babel-traverse 对其进⾏遍历,在此过程中进⾏添
加、更新及移除等操作;
⽣成 Generate: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模
块是 babel-generator。
10. 经常使用的 git 命令?
![](https://i-blog.csdnimg.cn/direct/d2e920637c534dfd89bd0522ad0c7f59.png)
11. git pull 和 git fetch 的区别
12. git rebase 和 git merge 的区别
七、浏览器篇
1. 什么是 XSS 攻击?
(1)概念
XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意
代码的执行。攻击者可以通过这种攻击方式可以进行以下操作:
- 获取页面的数据,如 DOM、cookie、localStorage;
- DOS 攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器;
- 破坏页面结构;
- 流量劫持(将链接指向某网站);
(2)攻击类型
XSS 可以分为存储型、反射型和 DOM 型:
存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行。
反射型指的是攻击者诱导用户访问一个带有恶意代码的 URL 后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有 XSS 代码的数据后当做脚本执行,最终完成XSS 攻击。
DOM 型指的通过修改页面的 DOM 节点形成的 XSS。
1)存储型 XSS 的攻击步骤:
1.攻击者将恶意代码提交到⽬标⽹站的数据库中。
2.⽤户打开⽬标⽹站时,⽹站服务端将恶意代码从数据库取出,拼接
在 HTML 中返回给浏览器。
3.⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
4.恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏
为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。
这种攻击常⻅于带有⽤户保存数据的⽹站功能,如论坛发帖、商品评
论、⽤户私信等。
2)反射型 XSS 的攻击步骤:
1.攻击者构造出特殊的 URL,其中包含恶意代码。
2.⽤户打开带有恶意代码的 URL 时,⽹站服务端将恶意代码从 URL
中取出,拼接在 HTML 中返回给浏览器。
3.⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
4.恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏
为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在
数据库⾥,反射型 XSS 的恶意代码存在 URL ⾥。
反射型 XSS 漏洞常⻅于通过 URL 传递参数的功能,如⽹站搜索、跳
转等。 由于需要⽤户主动打开恶意的 URL 才能⽣效,攻击者往往会
结合多种⼿段诱导⽤户点击。
3)DOM 型 XSS 的攻击步骤:
1.攻击者构造出特殊的 URL,其中包含恶意代码。
2.⽤户打开带有恶意代码的 URL。
3.⽤户浏览器接收到响应后解析执⾏,前端 JavaScript 取出 URL中的恶意代码并执⾏。
4.恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执⾏恶意代码由浏览器端完成,属于前端JavaScript ⾃身的安全漏洞,
⽽其他两种 XSS 都属于服务端的安全漏洞。
2. 如何防御 XSS 攻击?
可以看到 XSS 危害如此之大,那么在开发网站时就要做好防御措施,
具体措施如下:可以从浏览器的执行来进行预防,
一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。
另一种是对需要插入到HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚
本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对
可能出现的恶意代码情况进行判断。
使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资
源可以加载和执行,从而防止恶意代码的注入攻击。
1.CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
2.通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的Content-Security-Policy,一种是设置 meta 标签的方式 <meta http-equiv="Content-Security-Policy">
对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。
3. 什么是 CSRF 攻击?
(1)概念
CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三
方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击
网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过
后台的用户验证,冒充用户向服务器执行一些操作。
CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器
的特点,以此来实现用户的冒充。
(2)攻击类型
常见的 CSRF 攻击有三种:
GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个
请求,当用户打开这个网站的时候就会自动发起提交。
POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户
进入页面时,自动提交这个表单。
链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请
求,然后诱导用户去点击。
4. 如何防御 CSRF 攻击?
CSRF 攻击可以使用以下方法来防护:
进行同源检测,服务器根据 http 请求头中 origin 或者 referer
信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当
origin 或者 referer 信息都不存在的时候,直接阻止请求。这种方
式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎
的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是
相应的页面请求这种请求方式也可能被攻击者给利用。(Referer 字
段会告诉服务器该网页是从哪个页面链接过来的)
使用 CSRF Token 进行验证,服务器向用户返回一个随机数 Token ,
当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,
然后服务器对这个 token 进行验证。这种方法解决了使用 cookie
单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点
就是,我们需要给网站中的所有请求都添加上这个 token,操作比较
繁琐。还有一个问题是一般不会只有一台网站服务器,如果请求经过
负载平衡转移到了其他的服务器,但是这个服务器的 session 中没
有保留这个 token 的话,就没有办法验证了。这种情况可以通过改
变 token 的构建方式来解决。
对 Cookie 进行双重验证,服务器在用户访问网站页面时,向请求域
名注入一个 Cookie,内容为随机字符串,然后当用户再次向服务器
发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数
中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,
来进行验证。使用这种方式是利用了攻击者只能利用 cookie,但是
不能访问获取 cookie 的特点。并且这种方法比 CSRF Token 的方法
更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果
网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能做
到子域名的隔离。
在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能作为
被第三方使用,从而可以避免被攻击者利用。Samesite 一共有两种
模式,一种是严格模式,在严格模式下 cookie 在任何情况下都不可
能作为第三方 Cookie 使用,在宽松模式下,cookie 可以被请求是
GET 请求,且会发生页面跳转的请求所使用。
5. 有哪些可能引起前端安全的问题?
跨站脚本 (Cross-Site Scripting, XSS): ⼀种代码注⼊⽅式, 为了
与 CSS 区分所以被称作 XSS。早期常⻅于⽹络论坛, 起因是⽹站没
有对⽤户的输⼊进⾏严格的限制, 使得攻击者可以将脚本上传到帖
⼦让其他⼈浏览到有恶意脚本的⻚⾯, 其注⼊⽅式很简单包括但不
限于 JavaScript / CSS / Flash 等;
iframe 的滥⽤: iframe 中的内容是由第三⽅来提供的,默认情况下
他们不受控制,他们可以在 iframe 中运⾏JavaScirpt 脚本、Flash
插件、弹出对话框等等,这可能会破坏前端⽤户体验;
跨站点请求伪造(Cross-Site Request Forgeries,CSRF): 指攻击
者通过设置好的陷阱,强制对已完成认证的⽤户进⾏⾮预期的个⼈信
息或设定信息等某些状态更新,属于被动攻击
恶意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤多数
时候都是在借助开发框架和各种类库进⾏快速开发,⼀旦第三⽅库被
植⼊恶意代码很容易引起安全问题。
6. 网络劫持有哪几种,如何防范?
⽹络劫持分为两种:
(1)DNS 劫持: (输⼊京东被强制跳转到淘宝这就属于 dns 劫持)DNS 强制解析: 通过修改运营商的本地 DNS 记录,来引导⽤户流量到缓存服务器
302 跳转的⽅式: 通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起 302 跳转的回复,引导⽤户获取内容
(2)HTTP 劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于 http
明⽂传输,运营商会修改你的 http 响应内容(即加⼴告)
(3)DNS 劫持由于涉嫌违法,已经被监管起来,现在很少会有 DNS
劫持,⽽http 劫持依然⾮常盛⾏,最有效的办法就是全站 HTTPS,将
HTTP 加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。
7. 浏览器渲染进程的线程有哪些
浏览器的渲染进程的线程总共有五种:
(1)GUI 渲染线程
负责渲染浏览器页面,解析 HTML、CSS,构建 DOM 树、构建 CSSOM 树、
构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,
该线程就会执行。
注意:GUI 渲染线程和 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI
线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时
立即被执行。
(2)JS 引擎线程
JS 引擎线程也称为 JS 内核,负责处理 Javascript 脚本程序,解析
Javascript 脚本,运行代码;JS 引擎线程一直等待着任务队列中任
务的到来,然后加以处理,一个 Tab 页中无论什么时候都只有一个
JS 引擎线程在运行 JS 程序;
注意:GUI 渲染线程与 JS 引擎线程的互斥关系,所以如果 JS 执行的
时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
(3)事件触发线程
事件触发线程属于浏览器而不是 JS 引擎,用来控制事件循环;当 JS
引擎执行代码块如 setTimeOut 时(也可是来自浏览器内核的其他线
程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件触发
线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加
到待处理队列的队尾,等待 JS 引擎的处理;
注意:由于 JS 的单线程关系,所以这些待处理队列中的事件都得排
队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行);
(4)定时器触发进程
定时器触发进程即 setInterval 与 setTimeout 所在线程;浏览器定
时计数器并不是由 JS 引擎计数的,因为 JS 引擎是单线程的,如果处
于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时
并触发定时器,计时完毕后,添加到事件队列中,等待 JS 引擎空闲
后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,
定时器只是在指定时间点将任务添加到事件队列中;
注意:W3C 在 HTML 标准中规定,定时器的定时时间不能小于 4ms,如
果是小于 4ms,则默认为 4ms。
(5)异步 http 请求线程
XMLHttpRequest 连接后通过浏览器新开一个线程请求;
检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更
事件,将回调函数放入事件队列中,等待 JS 引擎空闲后执行;
8. 僵尸进程和孤儿进程是什么?
孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些
子进程都会成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所
收养,并由 init 进程对它们完成状态收集工作。
僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用
的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之
为僵死进程。
9. 如何实现浏览器内多个标签页之间的通信?
实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让 标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通 信方法如下:使用 websocket 协议,因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。使用 ShareWorker 的方式,shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。使用localStorage 的方式,我们可以在一个标签页对 localStorage的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。这个时候 localStorage 对象就是充当的中介者的角色。使用 postMessage 方法,如果我们能够获得对应标签页的引用,就可以使用 postMessage 方法,进行通信。
10. 对浏览器的缓存机制的理解
![](https://i-blog.csdnimg.cn/direct/d9d3ef6909f74e7fa4c0940e6cbc9249.png)
- 下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上 一次返回 200 时的时间差,
- 如果没有超过 cache-control 设置的 max-age,则没有过期,并命中强缓存,直接从本地读取资源。
- 如果 浏览器不支持 HTTP1.1,则使用 expires 头判断是否过期;
- 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向 服务器发送带有 If-None-Match 和 If-Modified-Since 的请求;
- 服务器收到请求后,优先根据 Etag 的值判断被请求的文件有没有做 修改,Etag 值一致则没有修改,命中协商缓存,返回 304;
- 如果不 一致则有改动,直接返回新的资源文件带上新的 Etag 值并返回 200;
- 如果服务器收到的请求没有 Etag 值,则将 If-Modified-Since 和 被请求文件的最后修改时间做比对,一致则命中协商缓存,返回 304;
- 不一致则返回新的 last-modified 和文件并返回 200;
11. 协商缓存和强缓存的区别
(1)强缓存
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必
再向服务器发起请求。
强缓存策略可以通过两种方式来设置,分别是 http 头信息中的
Expires 属性和 Cache-Control 属性
(1)服务器通过在响应头中添加 Expires 属性,来指定资源的过期
时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发
送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存
在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用
户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的
结果。
(2)Expires 是 http1.0 中的方式,因为它的一些缺点,在 HTTP
1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供
了对资源的缓存的更精确的控制。它有很多不同的值,
Cache-Control 可设置的字段:
public:设置了该字段值的资源表示可以被任何对象(包括:发送请
求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是
使用 max-age=来精确控制;
private:设置了该字段值的资源只能被用户浏览器缓存,不允许任
何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,
通常都要设置这个字段值,避免代理服务器(CDN)缓存;
no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了
变化,如果资源未发生变化,则直接使用缓存好的资源;
135
no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起
新的请求,拉取最新的资源;
max-age=:设置缓存的最大有效期,单位为秒;
s-maxage=:优先级高于 max-age=,仅适用于共享缓存(CDN),优先
级高于 max-age 或者 Expires 头;
max-stale[=]:设置了该字段表明客户端愿意接收已经过期的资源,
但是不能超过给定的时间限制。
一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方
式一起使用时,Cache-Control 的优先级要高于 Expires。
no-cache 和 no-store 很容易混淆:
no-cache 是指先要和服务器确认是否有资源更新,在进行判断。也
就是说没有强缓存,但是会有协商缓存;
no-store 是指不使用任何缓存,每次请求都直接从服务器获取资源。
(2)协商缓存
如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如
果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会
发挥作用了。
上面已经说到了,命中协商缓存的条件有两个:
max-age=xxx 过期了
值为 no-store
使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发
生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如
果资源发生了修改,则返回修改后的资源。
协商缓存也可以通过两种方式来设置,分别是 http 头信息中的
Etag 和 Last-Modified 属性。
(1)服务器通过在响应头中添加 Last-Modified 属性来指出资源最
后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加
一个 If-Modified-Since 的属性,属性值为上一次资源返回时的
Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性
来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做
了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本
地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种
方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精
确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,那么文件
已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中
的不准确。
(2)因为 Last-Modified 的这种可能发生的不准确性,http 中提
供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,
在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,
当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,
浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值
就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值
来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改
变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式
更加精确。
当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更
高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多
个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器
上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag
属性。
总结:
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存
副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命
中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强
缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求
的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命
中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命
中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如
果协商缓存不命中,则浏览器返回最新的资源给浏览器。
12. 点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?
点击刷新按钮或者按 F5:浏览器直接对本地的缓存文件过期,但是
会带上 If-Modifed-Since,If-None-Match,这就意味着服务器会对
文件检查新鲜度,返回结果可能是 304,也有可能是 200。
用户按 Ctrl+F5(强制刷新):浏览器不仅会对本地文件过期,而且
不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有
请求过,返回结果是 200。
地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,
然后服务器检查新鲜度,最后返回内容。
八、移动端开发、Android、IOS混合开发篇
1. 移动端适配
为什么要做适配?为了适应各种移动端设备,完美呈现应有的布局各个移动端设备,分辨率大小不一致,网页想铺满整个屏幕,并在各种分辨下等比缩放
适配方案
根据设置的大小去设置高度,单位可以用 px 百分比 auto
1、常用 Flex 布局
2、百分比宽度
3、rem适配(常用)
4、vw\vh
根据屏幕的分辨率动态设置html的文字大小,达到等比缩放的功能
保证html最终算出来的字体大小,不能小于12px
在不同的移动端显示不同的元素比例效果如果html的font-size:20px的时候,那么此时的1rem = 20px
把设计图的宽度分成多少分之一,根据实际情况rem做盒子的宽度,viewport 缩放像素比适配
window.devicePixelRatio
物理像素是手机屏幕分辨率
独立像素 指css像素 屏幕宽度
像素比 = 物理像素 / css宽度
获取设备的像素比 window.devicePixelRatio
2. 移动端300ms延迟
3. 如何解决移动端 Retina 屏 1px 像素问题
tranform: scale(0.5)
![](https://i-blog.csdnimg.cn/direct/f3dbb3fb06274dc6a3eea8ec32ccade6.png)
4、HTML5和 Native的交互如何实现?
WebView本来就支持 JavaScript和Java相互调用,只需要开启 WebView的 JavaScript脚本执行功能,并通过代码 mWebView. addJavascriptInterface(new JsBridge( )," bxbxbai")向HTML5页面中注入一个Java对象,然后就可以在HTML5页面中调用 Native的功能了。
5、微信是用 Hybrid开发做得最好的App之一,它是如何做交互的?
在微信开发者文档中可以看到,微信JS-SDK封装了微信的各种功能,比如分享到朋友圈、图像接口、音频接口、支付接口、地理位置接口等。
开发者只需要调用微信JS-SDK中的函数,然后统一由JS-SDK调用微信中的功能这样的好处就是,开发者写了一个HTML5的应用或网页,在 Android和iOS版本的微信中都可以正常运行。
6、Hybrid开发适用于哪些功能?
Hybrid开发就是在 Native客户端中嵌入了 HTML App的功能,这方面微信应该是做得最好的。
由于HTML5的效率以及耗电问题,可能用户对 Web App的体验不满意,Hybrid App也只适用于某些场景。
把一些基础的功能(比如调用手机的摄像头、获取地理位置、登录注册等)做成 Native的功能,让HTML5来调用更好,这样的体验也更好。
如果把一个登录和注册功能也做成HTML5版本的App,在弱网络环境下,这个体验应该会非常差,或许用户等半天还没加载出页面。
一些活动页面(比如“秒杀”、团购等)适合采用HTML5开发,因为这些页面可能设计得非常炫而且复杂。HTML5开发非常简单,并且这些页面时效性短,更新更快,因为一个活动说不定就一周时间,下周就下线了。而如果用 Native开发,成本是很高的。
7、Web App和混合App的区别是什么?
区别如下。
(1) Web App指采用HTML5语言写的App,需要安装触屏版网页应用。
优点包括:开发成本低,迭代速度快,能够跨平台终端。
缺点包括:入口临时,获取系统级别的通知和提醒效率低,用户留存率低,设计受限制,体验较差。
(2)混合App指半原生半Web的混合App,需要安装它才能访问Web内容。
例如新闻类App、视频类App普遍采取Native框架web内容,混合App极力打造类似于原生App的体验,但仍受限于技术和网速等诸多因素。
8、什么是 Android混合开发?如何申请权限?
Android混合开发使用Java和H5共同开发界面,通过 JsBridge进行通信,一部分界面首先在本地写好,然后通过网络请求获取数据,进行展示。当然,也可以完全是H5界面,在 WebView中进行展示。
权限可以在 Manifest.xml中申请, Android 6.0以上版本可以通过代码动态申请。
9、什么是混合开发?
混合开发(HTML5开发)相当于一种框架开发。该模式通常由“HTML5云网站+App应用客户端”两部分构成,App应用客户端只须安装应用的框架部分,而在每次打开App的时候,从云端取数据并呈现给手机用户混合开发的另一种形式是套壳App。
套壳App就是用H5的网页打包成App。
虽然App能安装到手机上,但是每个界面都是通过HTML5开发的网页。这种App数据都保存在云端,用户每次访问都需要从云端调取全部内容,这样就容易导致反应慢,每打开个网页或单击一个按钮,加载网页都需要等很长时间。
10、混合App开发的优势是什么?
优势如下。
时间短。基本都是直接嵌套模板或打包成App,这会节省很大一部分时间。
价格便宜。代码不需要重新写,界面不用重新设计,这些都是固定的,可替换的地方很少,所以价格相对便宜。
11、混合App开发的劣势是什么?
劣势如下。
(1)功能、界面无法自定义。所有内容都是固定的,所以要换一个界面,或增加个功能,都是不可以的。
(2)加载缓慢、网络要求高。混合App数据全部需要从服务器调取,每个页面都需要重新下载,所以打开速度慢,占用的网络带宽高,缓冲时间长,容易让用户反感。
(3)安全性比较低。代码都是以前的代码,不能很好地兼容最新的手机系统,且安全性较低。网络发展快,病毒多,如果不实时更新,定期检查,容易产生漏洞,造成经济损失。
12、开发原生App还是混合App,你是如何选择的?
选择方法如下。
(1)根据预算选择:现在预算有多少?在应用转型上打算花多少金钱、时间、精力?
如果预算在几千元到一万元之间,建议选择混合App。混合App有它存在的道理,并非一文不值,很多混合App发展好了再转型成原生App。
(2)根据需要选择:如果只是简单地卖个小商品,那么可以选择混合App;如果想做类似淘宝的大型店铺,有很多用户、很多店、很多现金流,可以选择原生App。
九、uni-app开发篇
1. 什么是 UniApp?它有什么特点?
答案:UniApp 是一个基于 Vue.js 的跨平台应用开发框架,可以使用 Vue.js 的开发语法编写一次代码,然后通过编译生成可以在多个平台(包括iOS、Android、H5 等)上运行的应用。UniApp 具有以下特点:
跨平台:开发者可以使用相同的代码基底构建多个平台的应用,避免了针对不同平台的重复开发。
高性能:UniApp 在运行时使用原生渲染技术,具有接近原生应用的性能表现。
开放生态:UniApp 支持原生插件和原生能力的扩展,可以调用设备的硬件功能和第三方原生 SDK。
开发便捷:UniApp 提供了丰富的组件和开发工具,简化了应用开发和调试的流程。
2. 请解释 UniApp 中的生命周期钩子函数及其执行顺序。
答案:在 UniApp 中,每个页面和组件都有一系列的生命周期钩子函数,用于在特定的时机执行代码。以下是 UniApp 中常用的生命周期钩子函数及其执行顺序:
onLoad:页面/组件加载时触发。
onShow:页面/组件显示在前台时触发。
onReady:页面/组件初次渲染完成时触发。
onHide:页面/组件被隐藏在后台时触发。
onUnload:页面/组件被销毁时触发。
执行顺序为:onLoad -> onShow -> onReady -> onHide -> onUnload。
3. 请解释 UniApp 中的全局组件和页面组件的区别。
答案:在 UniApp 中,全局组件和页面组件是两种不同类型的组件。
全局组件:在 App.vue 中注册的组件,可以在应用的所有页面和组件中使用。可以通过 Vue.component 方法进行全局注册。
页面组件:每个页面都有自己的组件,用于描述页面的结构和交互。页面组件只在当前页面有效,不能在其他页面中直接使用,但可以通过组件引用的方式进行复用。
4. 请解释 UniApp 中的条件编译是如何工作的。
答案:UniApp 中的条件编译允许开发者根据不同的平台或条件编译指令来编写不同的代码。在编译过程中,指定的平台或条件将会被处理,并最终生成对应平台的可执行代码。条件编译通过在代码中使用 #ifdef、#ifndef、#endif 等指令进行控制。例如,可以使用 #ifdef H5 来编写只在 H5 平台生效的代码块。
5. 请解释 UniApp 中的跨平台兼容性问题和解决方案。
答案:由于不同平台的差异,UniApp 在跨平台开发时可能会遇到一些兼容性问题。为了解决这些问题,可以采取以下几个方面的策略:
使用条件编译:根据不同的平台,编写对应平台的代码,使用条件编译指令来控制代码块的执行。
使用平台 API:UniApp 提供了一些平台 API,可以通过条件编译指令来使用特定平台的功能和能力。
样式适配:不同平台的样式表现可能有差异,使用 uni-app-plus 插件中的 upx2px 方法来进行样式适配,使得在不同平台上显示一致。
原生扩展:使用原生插件和扩展来调用设备的原生功能和第三方 SDK,以解决特定平台的需求。
6、uniApp中如何进行页面跳转?
答案:可以使用uni.navigateTo、uni.redirectTo和uni.reLaunch等方法进行页面跳转。其中,uni.navigateTo可以实现页面的普通跳转,uni.redirectTo可以实现页面的重定向跳转,uni.reLaunch可以实现关闭所有页面,打开到应用内的某个页面。
// 在某个页面的点击事件中跳转到其他页面
uni.navigateTo({
url: '/pages/otherPage/otherPage'
});
7、Uniapp 应用的生命周期、页面的生命周期、组件的生命周期.
一、应用的生命周期1.onLaunch——当uni-app 初始化完成时触发(全局只触发一次)
2.onShow——当 uni-app 启动,或从后台进入前台显示
3.onHide——当 uni-app 从前台进入后台
4.onError——当 uni-app 报错时触发
5.onUniNViewMessage——对 nvue 页面发送的数据进行监听,可参考 nvue 向 vue 通讯
6.onUnhandledRejection——对未处理的 Promise 拒绝事件监听函数(2.8.1+)
7.onPageNotFound——页面不存在监听函数
8.onThemeChange——监听系统主题变化
二、页面的生命周期
1.onInit——监听页面初始化,其参数同 onLoad 参数,为上个页面传递的数据,参数类型为 Object(用于页面传参),触发时机早于 onLoad
2.onLoad——监听页面加载,其参数为上个页面传递的数据,参数类型为 Object(用于页面传参),参考示例
3.onShow——监听页面显示。页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面
4.onReady——监听页面初次渲染完成。注意如果渲染速度快,会在页面进入动画完成前触发
5.onHide——监听页面隐藏
6.onUnload——监听页面卸载
7.onResize——监听窗口尺寸变化
三、组件的生命周期
uni-app 组件支持的生命周期,与vue标准组件的生命周期相同
1.beforeCreate——在实例初始化之后被调用。
2.created——在实例创建完成后被立即调用。
3.beforeMount——在挂载开始之前被调用。
4.mounted——挂载到实例上去之后调用。详见 注意:此处并不能确定子组件被全部挂载,如果需要子组件完全挂载之后在执行操作可以使用$nextTickVue官方文档
5.beforeUpdate——数据更新时调用,发生在虚拟 DOM 打补丁之前。
6.updated——由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
7.beforeDestroy——实例销毁之前调用。在这一步,实例仍然完全可用。
8.destroyed——Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
8. uni-app中的路由管理是如何工作的?如何传递参数?
1.1 路由跳转方法
uni.navigateTo(OBJECT):保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面。
uni.redirectTo(OBJECT):关闭当前页面,跳转到应用内的某个页面。
uni.switchTab(OBJECT):跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。
uni.reLaunch(OBJECT):关闭所有页面,打开到应用内的某个页面。
uni.navigateBack(OBJECT): 关闭当前页面,返回上一页面或多级页面。
1.2 传递参数
在uni-app中,你可以通过以下方式传递参数:
URL参数
在调用跳转API时,可以在OBJECT参数中指定url属性,并在URL后面拼接参数:
uni.navigateTo({
url: '/pages/detail/detail?id=123&name=uni-app'
});
在目标页面的onLoad生命周期函数中接收参数:
export default {
onLoad(options) {
console.log(options.id); // 输出:123
console.log(options.name); // 输出:uni-app
}
};
EventChannel
如果需要在页面间传递更复杂的数据或事件,可以使用EventChannel。在跳转页面时创建一个EventChannel实例,然后在目标页面中获取这个实例来接收数据。
跳转页面时:
const eventChannel = this.$scope.$createEventChannel();
uni.navigateTo({
url: '/pages/detail/detail',
success(res) {
eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' });
}
});
在目标页面接收数据:
export default {
onShow() {
const eventChannel = this.getOpenerEventChannel();
eventChannel.on('acceptDataFromOpenerPage', function(data) {
console.log(data); // 输出:{ data: 'test' }
});
}
};
注意事项
URL参数的长度限制是1024字节,请勿传递过长的参数。
EventChannel适合传递更复杂的数据,且不受长度限制。
使用uni.navigateTo和uni.redirectTo跳转时,目标页面需要在pages.json中注册。