2023面试面经汇总

2023面试题目及知识点汇总

自我介绍部分

面试官您好,我叫xxx,xx年毕业于xx大学,自毕业以来一直从事着前端开发的相关工作。
我擅长的技术栈是 vue 全家桶,对 vue2 和 vue3 在使用上和源码都有一定程度的钻研;打包工具对 webpack 和 vite 都比较熟悉;有从零到一主导中大型项目落地的经验和能力。
在上家公司主要是新浪新闻产品线,主要职责是负责相关后台管理系统开发与维护,商业活动H5页面的开发,文章排版等爬虫服务
除了开发相关工作,还有一定的技术管理经验:比如担任需求评审、UI/UE交互评审评委,负责开发排期、成员协作、对成员代码进行review、组织例会等等
平常会在自己搭建的博客上记录一些学习文章或者学习笔记,也会写一些原创的技术文章发表到掘金、csdn上。喜欢折腾实用技术。

软实力部分

为什么跳槽

之前的工作陷入了舒适圈,做来做去也就那些东西,想要换个平台扩宽自己的技术广度,接触和学习一些新的技术体系,为后续的个人发展更有利

和普通前端相比,你有什么亮点

  1. 善于规划和总结,管理项目,我会对自己经手的项目进行一个全面的分析,一个是业务拆解,对个各模块的业务通过脑图进行拆解;另一个就是对代码模块的拆解,按功能去区分各个代码模块。再去进行开发。我觉得这是很多只会进行盲目业务开发的前端做不到的
  2. 喜欢钻研项目,并不局限于前端,只要能提高工作效率。例如用chatgpt做技术预研和写正则表达式(AI不会淘汰人,会用AI的人会)

有什么缺点

性子比较沉,更偏内向一点,所以我也会尝试让自己变得外向一点。

一个是要开各种评审会,作为前端代表需要我去准备各种材料和进行发言。

所以在团队内做比较多的技术分享,每周主持例会,也让我敢于去表达和探讨。

最近关注的新技术

  1. 打包工具 vite (极速的开发环境),arco脚手架提issue
  2. flutter (Google推出并开源的移动应用程序(App)开发框架,主打跨平台、高保真、高性能),做过demo

未来规划

远程更考验问题的解决能力,所以对于广度和深度都要求很高

3 - 5 年在提高自己的技术深度的同时,扩宽自己的知识面,就是深度和广度上都要有提升,主要是在广度上,充分对大前端有了认知才能更好的做出选择

js技术部分

什么是原型/原型链?

原型的本质就是一个对象。

当我们在创建一个构造函数之后,这个函数会默认带上一个prototype属性,而这个属性的值就指向这个函数的原型对象。

这个原型对象是用来为通过该构造函数创建的实例对象提供共享属性,即用来实现基于原型的继承和属性的共享

所以我们通过构造函数创建的实例对象都会从这个函数的原型对象上继承上面具有的属性

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止(最顶层就是Object.prototype的原型,值为null)。

所以通过原型一层层相互关联的链状结构就称为原型链。

闭包

定义:闭包是指引用了其他函数作用域中变量的函数,通常是在嵌套函数中实现的。

从技术角度上所有 js 函数都是闭包。
从实践角度来看,满足以下俩个条件的函数算闭包

  1. 即使创建它的上下文被销毁了,它依然存在。(比如从父函数中返回)
  2. 在代码中引用了自由变量(在函数中使用的既不是函数参数也不是函数局部变量的变量称作自由变量)

使用场景:

  1. 创建私有变量
    vue 中的data,需要是一个闭包,保证每个data中数据唯一,避免多次引用该组件造成的data共享

  2. 延长变量的生命周期
    一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

应用

柯里化函数
例如计数器、延迟调用、回调函数等

this 的指向

在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)
1、全局的this非严格模式指向window对象,严格模式指向 undefined
2、对象的属性方法中的this 指向对象本身
3、apply、call、bind 可以变更 this 指向为第一个传参(应用:写一个debounce防抖函数,调用传入的func的时候要用apply改this指向)
4、箭头函数中的this指向它的父级作用域,它自身不存在 this

浏览器的事件循环?

js 代码执行过程中,会创建对应的执行上下文并压入执行上下文栈中。

如果遇到异步任务就会将任务挂起,交给其他线程去处理异步任务,当异步任务处理完后,会将回调结果加入事件队列中。

当执行栈中所有任务执行完毕后,就是主线程处于闲置状态时,才会从事件队列中取出排在首位的事件回调结果,并把这个回调加入执行栈中然后执行其中的代码,如此反复,这个过程就被称为事件循环。

事件队列分为了宏任务队列和微任务队列,在当前执行栈为空时,主线程回先查看微任务队列是否有事件存在,存在则依次执行微任务队列中的事件回调,直至微任务队列为空;不存在再去宏任务队列中处理。

常见的宏任务有setTimeout()、setInterval()、setImmediate()、I/O、用户交互操作,UI渲染

常见的微任务有promise.then()、promise.catch()、new MutationObserver、process.nextTick()

宏任务和微任务的本质区别

宏任务有明确的异步任务需要执行和回调,需要其他异步线程支持
微任务没有明确的异步任务需要执行,只有回调,不需要其他异步线程支持。

javascript中数据在栈和堆中的存储方式

1、基本数据类型大小固定且操作简单,所以放入栈中存储
2、引用数据类型大小不确定,所以将它们放入堆内存中,让它们在申请内存的时候自己确定大小
3、这样分开存储可以使内存占用最小。栈的效率高于堆
4、栈内存中变量在执行环境结束后会立即进行垃圾回收,而堆内存中需要变量的所有引用都结束才会被回收

讲讲v8垃圾回收

1、根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同分代采用不同的回收算法
2、新生代采用空间换时间的 scavenge 算法:整个空间分为两块,变量仅存在其中一块,回收的时候将存活变量复制到另一块空间,不存活的回收掉,周而复始轮流操作
3、老生代使用标记清除和标记整理,标记清除:遍历所有对象标记标记可以访问到的对象(活着的),然后将不活的当做垃圾进行回收。回收完后避免内存的断层不连续,需要通过标记整理将活着的对象往内存一端进行移动,移动完成后再清理边界内存

defer和async的区别

一般情况下,当执行到 script 标签时会进行下载 + 执行两步操作,这两步会阻塞 HTML 的解析;
async 和 defer 能将script的下载阶段变成异步执行(和 html解析同步进行);
async下载完成后会立即执行js,此时会阻塞HTML解析;
defer会等全部HTML解析完成且在DOMContentLoaded 事件之前执行。

浏览器事件机制

DOM 事件流三阶段:

  1. 捕获阶段:事件最开始由不太具体的节点最早接受事件, 而最具体的节点(触发节点)最后接受事件。为了让事件到达最终目标之前拦截事件。
    比如点击一个div,则 click 事件会按这种顺序触发: document => => =>

    ,即由 document 捕获后沿着 DOM 树依次向下传播,并在各节点上触发捕获事件,直到到达实际目标元素。

  2. 目标阶段
    当事件到达目标节点的,事件就进入了目标阶段。事件在目标节点上被触发(执行事件对应的函数),然后会逆向回流,直到传播至最外层的文档节点。

  3. 冒泡阶段
    事件在目标元素上触发后,会继续随着 DOM 树一层层往上冒泡,直到到达最外层的根节点。

所有事件都要经历捕获阶段和目标阶段,但有些事件会跳过冒泡阶段,比如元素获得焦点 focus 和失去焦点 blur 不会冒泡

扩展一:e.target 和 e.currentTarget 区别?

  • e.target 指向触发事件监听的对象。
  • e.currentTarget 指向添加监听事件的对象。
<ul>
  <li><span>hello 1</span></li>
</ul>
​
let ul = document.querySelectorAll('ul')[0]
let aLi = document.querySelectorAll('li')
ul.addEventListener('click',function(e){
  let oLi1 = e.target  
  let oLi2 = e.currentTarget
  console.log(oLi1)   //  被点击的li
  console.log(oLi2)   // ul
  console.og(oLi1===oLi2)  // false
})

给 ul 绑定了事件,点击其中 li 的时候,target 就是被点击的 li, currentTarget 就是被绑定事件的 ul
事件冒泡阶段(上述例子),e.currenttargete.target是不相等的,但是在事件的目标阶段,e.currenttargete.target是相等的

e.target可以用来实现事件委托,该原理是通过事件冒泡(或者事件捕获)给父元素添加事件监听,e.target指向引发触发事件的元素

扩展二:addEventListener 参数

语法:

addEventListener(type, listener);
addEventListener(type, listener, options || useCapture);
  • type: 监听事件的类型,如:’click’/‘scroll’/‘focus’
  • listener: 必须是一个实现了 EventListener 接口的对象,或者是一个函数。当监听的事件类型被触发时,会执行
  • options:指定 listerner 有关的可选参数对象
    • capture: 布尔值,表示 listener 是否在事件捕获阶段传播到 EventTarget 时触发
    • once:布尔值,表示 listener 添加之后最多调用一次,为 true 则 listener 在执行一次后会移除
    • passive: 布尔值,表示 listener 永远不会调用 preventDefault()
    • signal:可选,AbortSignal,当它的abort()方法被调用时,监听器会被移除
  • useCapture:布尔值,默认为 false,listener 在事件冒泡阶段结束时执行,true 则表示在捕获阶段开始时执行。作用就是更改事件作用的时机,方便拦截/不被拦截。

JS Bridge

连接 JS 和 Native 的桥接,也是 Hybrid App 里面的核心。一般分为 JS 调用 Native 和 Native 主动调用 JS 两种形式。

JS调用Native

  1. 拦截 Scheme:js按照已定格式请求scheme,客户端拦截并调用对应功能(js侧最常用iframe跳转,
  2. 弹窗拦截: js利用弹窗会触发 WebView 相应事件,客户端拦截的
  3. 注入 JS 上下文:这种方式不依赖拦截,主要是通过 WebView 向 JS 的上下文注入对象和方法,可以让 JS 直接调用原生。

Native调用JS

Native 调用 JS 一般就是直接 JS 代码字符串,有些类似我们调用 JS 中的 eval 去执行一串代码。一般有 loadUrlevaluateJavascript 等几种方法。

但是不管哪种方式,客户端都只能拿到挂载到 window 对象上面的属性和方法。

呼端组件封装

TS

什么是泛型

泛型中的 T 就像一个占位符、或者说一个变量,在使用的时候可以把定义的类型像参数一样传入,它可以原封不动地输出。

约束泛型

比如现在我想约束某个泛型,一定要有 length 属性,怎么办?

可以和 interface 结合,来约束类型。

interface ILength {
    length: number
}

function printLength<T extends ILength>(arg: T): T {
    console.log(arg.length)
    return arg
}

这其中的关键就是 <T extends ILength>,让这个泛型继承接口 ILength,这样就能约束泛型。

我们定义的变量一定要有 length 属性,比如下面的 str、arr 和 obj,才可以通过 TS 编译。

interface 和 type 究竟有什么区别

interfacetype 是不同的东西,一个叫接口,一个叫类型别名

interface

interface(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。

type

type (类型别名),顾名思义,类型别名只是给类型起一个新名字。它并不是一个类型,只是一个别名而已

相同点

interface 使用 extends 实现继承, type 使用交叉类型(&)实现继承

不同点

  • type 可以,interface 不行:type可以声明基本类型、联合类型、交叉类型、元组
  • interface可以,type 不行:合并重复声明,重复interface会自动合并,重复type会报错

Vue篇

vue和react的区别

1、数据可变性

  • React 推崇函数式编程,数据不可变以及单向数据流,只能通过setState或者onchange来实现视图更新
  • Vue 基于数据可变,设计了响应式数据,通过监听数据的变化自动更新视图

2、写法

  • React 推荐使用 jsx + inline style的形式,就是 all in js
  • Vue 是单文件组件(SFC)形式,在一个组件内分模块(tmplate/script/style),当然vue也支持jsx形式,可以在开发vue的ui组件库时使用

3、diff 算法

  • Vue2采用双端比较,Vue3采用快速比较
  • react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。,需要使用shouldComponentUpdate()来手动优化react的渲染。

扩展:了解 react hooks吗

组件类的写法很重,层级一多很难维护。
函数组件是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类。
React Hooks 的设计目的,就是加强版函数组件,完全不使用”类”,就能写出一个全功能的组件
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。

vue组件通信方式

props / $emit
ref / $refs
parent/root
attrs / listeners
eventBus / vuex / pinia / localStorage / sessionStorage / Cookie / window
provide / inject

vue 渲染列表为什么要加key?

Vue 在处理更新同类型 vnode 的一组子节点(比如v-for渲染的列表节点)的过程中,为了减少 DOM 频繁创建和销毁的性能开销:

对没有 key 的子节点数组更新是通过就地更新的策略。它会通过对比新旧子节点数组的长度,先以比较短的那部分长度为基准,将新子节点的那一部分直接 patch 上去。然后再判断,如果是新子节点数组的长度更长,就直接将新子节点数组剩余部分挂载;如果是新子节点数组更短,就把旧子节点多出来的那部分给卸载掉)。所以如果子节点是组件或者有状态的 DOM 元素,原有的状态会保留,就会出现渲染不正确的问题。

有 key 的子节点更新是调用的patchKeyedChildren,这个函数就是大家熟悉的实现核心 diff 算法的地方,大概流程就是同步头部节点、同步尾部节点、处理新增和删除的节点,最后用求解最长递增子序列的方法区处理未知子序列。是为了最大程度实现对已有节点的复用,减少 DOM 操作的性能开销,同时避免了就地更新带来的子节点状态错误的问题。

综上,如果是用 v-for 去遍历常量或者子节点是诸如纯文本这类没有”状态“的节点,是可以使用不加 key 的写法的。但是实际开发过程中更推荐统一加上 key,能够实现更广泛场景的同时,避免了可能发生的状态更新错误,我们一般可以使用 ESlint 配置 key 为 v-for 的必需元素。

vue3 相对 vue2的响应式优化

vue2使用的是Object.defineProperty去监听对象属性值的变化,但是它不能监听对象属性的新增和删除,所以需要使用$set、$delete这种语法糖去实现,这其实是一种设计上的不足。

所以 vue3 采用了proxy去实现响应式监听对象属性的增删查改。

其实从api的原生性能上proxy是比Object.defineProperty要差的。

而 vue 做的响应式性能优化主要是在将嵌套层级比较深的对象变成响应式的这一过程。

vue2的做法是在组件初始化的时候就递归执行Object.defineProperty把子对象变成响应式的;

而vue3是在访问到子对象属性的时候,才会去将它转换为响应式。这种延时定义子对象响应式会对性能有一定的提升

Vue 核心diff流程

前提:当同类型的 vnode 的子节点都是一组节点(数组类型)的时候,

步骤:会走核心 diff 流程

Vue3是快速选择算法

同步头部节点
同步尾部节点
新增新的节点
删除多余节点
处理未知子序列(贪心 + 二分处理最长递增子序列)

Vue2是双端比较算法

在新旧字节点的头尾节点,也就是四个节点之间进行对比,找到可复用的节点,不断向中间靠拢的过程

diff目的:diff 算法的目的就是为了尽可能地复用节点,减少 DOM 频繁创建和删除带来的性能开销

vue双向绑定原理

基于 MVVM 模型,viewModel(业务逻辑层)提供了数据变化后更新视图和视图变化后更新数据这样一个功能,就是传统意义上的双向绑定。

Vue2.x 实现双向绑定核心是通过三个模块:Observer监听器、Watcher订阅者和Compile编译器。

首先监听器会监听所有的响应式对象属性,编译器会将模板进行编译,找到里面动态绑定的响应式数据并初始化视图;watchr 会去收集这些依赖;当响应式数据发生变更时Observer就会通知 Watcher;watcher接收到监听器的信号就会执行更新函数去更新视图;

vue3的变更是数据劫持部分使用了porxy 替代 Object.defineProperty,收集的依赖使用组件的副作用渲染函数替代watcher

vue插槽

插槽就是子组件中的提供给父组件使用的一个占位符,用<slot></slot> 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<slot></slot>标签。

作用域插槽(v-slot):作用域插槽其实就是带数据的插槽,即带参数的插槽,简单的来说就是子组件提供给父组件的参数,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容。

vue2 v-model 原理剖析

V-model 是用来监听用户事件然后更新数据的语法糖。

其本质还是单向数据流,内部是通过绑定元素的 value 值向下传递数据,然后通过绑定 input 事件,向上接收并处理更新数据。给组件添加 v-model 属性时,默认会把value 作为组件的属性,把 input作为给组件绑定事件时的事件名

单向数据流:父组件传递给子组件的值子组件不能修改,只能通过emit事件让父组件自个改。

// 比如
<input v-model="sth" />
// 等价于
<input :value="sth" @input="sth = $event.target.value" />

vue3 v-model 原理

实现和 vue2 基本一致

<Son v-model="modalValue"/>
// 等价于
<Son :modalValue="modalValue" @update:modalValue="modalUpdate=$event.target.value"/>

vue 响应式原理

不管vue2 还是 vue3,响应式的核心就是观察者模式 + 劫持数据的变化,在访问的时候做依赖收集和在修改数据的时候执行收集的依赖并更新数据。具体点就是:

vue2 的话采用的是 Object.definePorperty劫持对象的 getset 方法,每个组件实例都会在渲染时初始化一个 watcher 实例,它会将组件渲染过程中所接触的响应式变量记为依赖,并且保存了组件的更新方法 update。当依赖的 setter 触发时,会通知 watcher 触发组件的 update 方法,从而更新视图。

Vue3 使用的是 ES6 的 proxyproxy 不仅能够追踪属性的获取和修改,还可以追踪对象的增删,这在 vue2中需要 set/delete 才能实现。然后就是收集的依赖是用组件的副作用渲染函数替代 watcher 实例。

性能方面,从原生 api 角度,proxy 这个方法的性能是不如 Object.property,但是 vue3 强就强在一个是上面提到的可以追踪对象的增删,第二个是对嵌套对象的处理上是访问到具体属性才会把那个对象属性给转换成响应式,而 vue2 是在初始化的时候就递归调用将整个对象和他的属性都变成响应式,这部分就差了。

computed 和 watch

Computed 的大体实现和普通的响应式数据是一致的,不过加了延时计算和缓存的功能

在访问computed对象的时候,会触发 getter ,初始化的时候将 computed 属性创建的 watcher (vue3是副作用渲染函数)添加到与之相关的响应式数据的依赖收集器中(dep),然后根据里面一个叫 dirty 的属性判断是否要收集依赖,不需要的话直接返回上一次的计算结果,需要的话就执行更新重新渲染视图。

watchEffect会自动收集回调函数中响应式变量的依赖。并在首次自动执行,可以执行比computed对象复杂的逻辑,不需要return返回值

推荐在大部分时候用 watch 显式的指定依赖以避免不必要的重复触发,也避免在后续代码修改或重构时不小心引入新的依赖。watchEffect 适用于一些逻辑相对简单,依赖源和逻辑强相关的场景(或者懒惰的场景 )

$nextTick 原理?

vue有个机制,更新 DOM 是异步执行的,当数据变化会产生一个异步更行队列,要等异步队列结束后才会统一进行更新视图,所以改了数据之后立即去拿 dom 还没有更新就会拿不到最新数据。所以提供了一个 nextTick 函数,它的回调函数会在DOM 更新后立即执行。

nextTick 本质上是个异步任务,由于事件循环机制,异步任务的回调总是在同步任务执行完成后才得到执行。所以源码实现就是根据环境创建异步函数比如 Promise.then(浏览器不支持promise就会用MutationObserver,浏览器不支持MutationObserver就会用setTimeout),然后调用异步函数执行回调队列。

所以项目中不使用$nextTick的话也可以直接使用Promise.then或者SetTimeout实现相同的效果

Vuex 流程 & 原理

Vuex 利用 vue 的mixin 机制,在beforeCreate 钩子前混入了 vuexinit 方法,这个方法实现了将 store 注入 vue 实例当中,并注册了 store 的引用属性 store,所以可以使用 this.store.xxx去引入vuex中定义的内容。

然后 state 是利用 vue 的 data,通过new Vue({data: {$$state: state}} 将 state 转换成响应式对象,然后使用 computed 函数实时计算 getter

Vue.use函数里面具体做了哪些事

可以通过全局方法Vue.use()注册插件,并能阻止多次注册相同插件,它需要在new Vue之前使用.

该方法第一个参数必须是Object或Function类型的参数。如果是Object那么该Object需要定义一个install方法;如果是Function那么这个函数就被当做install方法。

Vue.use()执行就是执行install方法,其他传参会作为install方法的参数执行。例如实现一个v-debounce命令,需要给Vue.use传入含install方法的对象。

所以Vue.use()本质就是执行需要注入插件的install方法

编写一个vue插件

Myplugin.install = function(Vue, options = {}) {
  // 1、添加全局方法或属性
  Vue.myGlobalMethod = function() {}
  // 2、添加全局服务
  Vue.directive('my-directive', {
    bind(el, binding, vnode, pldVnode) {}
  })
  // 3、注入组件选项
  Vue.mixin({
    created: function() {}
  })
  // 4、添加实例方法
  Vue.prototype.$myMethod = function(methodOptions) {}
}

举例:

export function debounce(func: any, wait: number, immediate = true) {
  let timeout: any = null;
  return function (...params) {
    if (timeout) {
      clearTimeout(timeout);
    }
    if (immediate) {
      if (!timeout) {
        func.apply(this, params);
      }
      timeout = setTimeout(() => {
        timeout = null;
      }, wait);
    } else {
      timeout = setTimeout(() => {
        func.apply(this, params);
      }, wait);
    }
  };
}

export const VueDebounce = {
  install(Vue) {
    Vue.directive('debounce', {
      bind(el: any, binding: any) {
        let execFunc;
        if (binding.value instanceof Array) {
          const [func, time = 500] = binding.value;
          execFunc = debounce(func, time);
        } else {
          execFunc = debounce(binding.value, 500);
        }
        el.addEventListener('click', execFunc);
      }
    });
  }
};

CSS知识点

什么是 BFC

Block Formatting context,块级格式上下文

BFC 是一个独立的渲染区域,相当于一个容器,在这个容器中的样式布局不会受到外界的影响。

比如浮动元素、绝对定位、overflow 除 visble 以外的值、display 为 inline/tabel-cells/flex 都能构建 BFC。

常常用于解决

  1. 处于同一个 BFC 的元素外边距会产生重叠(此时需要将它们放在不同 BFC 中);
  2. 清除浮动(float),使用 BFC 包裹浮动的元素即可
  3. 阻止元素被浮动元素覆盖,应用于两列式布局,左边宽度固定,右边内容自适应宽度(左边float,右边 overflow)

伪类和伪元素及使用场景

伪类

伪类即:当元素处于特定状态时才会运用的特殊类

开头为冒号的选择器,用于选择处于特定状态的元素。比如

  • :first-child选择第一个子元素;
  • :hover悬浮在元素上会显示;
  • :focus用键盘选定元素时激活;
  • :link + :visted点击过的链接的样式;
  • :not用于匹配不符合参数选择器的元素;
  • :fist-child匹配元素的第一个子元素;
  • :disabled 匹配禁用的表单元素

伪元素

伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过::before 来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。示例:::before 在被选元素前插入内容。需要使用 content 属性来指定要插入的内容。被插入的内容实际上不在文档树中。::first-line 匹配元素中第一行的文本

src 和 href 区别

  • href是Hypertext Reference的简写,表示超文本引用,指向网络资源所在位置。href 用于在当前文档和引用资源之间确立联系

  • src是source的简写,目的是要把文件下载到html页面中去。src 用于替换当前内容

浏览器解析方式

  • 当浏览器遇到href会并行下载资源并且不会停止对当前文档的处理。(同时也是为什么建议使用 link 方式加载 CSS,而不是使用 @import 方式)
  • 当浏览器解析到src ,会暂停其他资源的下载和处理,直到将该资源加载或执行完毕。(这也是script标签为什么放在底部而不是头部的原因)

flex布局、弹性布局

容器的属性

  • flex-direction: row | row-reverse | column | column-reverse; (决定主轴方向)
  • flex-wrap: nowrap | wrap | wrap-reverse;(如果一条轴线排不下,如何换行)
  • flex-flow: <flex-direction> || <flex-wrap>;
  • justify-content: flex-start | flex-end | center | space-between | space-around; (定义了项目在主轴上的对齐方式)
  • align-items: flex-start | flex-end | center | baseline | stretch; (定义项目在交叉轴上如何对齐。)
  • align-content: flex-start | flex-end | center | space-between | space-around | stretch; (定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。)

项目的属性

  • order: <integer>; (定义项目的排列顺序。数值越小,排列越靠前,默认为0)
  • flex-grow: <number>; /* default 0 */ (定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大)
  • flex-shrink: <number>; /* default 1 */ (义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小)
  • flex-basis: <length> | auto; /* default auto */(定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小)
  • flex: none | [ <’flex-grow’> <’flex-shrink’>? || <’flex-basis’> ] (flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。)
  • align-self: auto | flex-start | flex-end | center | baseline | stretch; (允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性)

浏览器和网络知识点

跨页面通信的方法?

这里分了同源页面和不同源页面的通信。

不同源页面可以通过 iframe 作为一个桥梁,因为 iframe 可以指定 origin 来忽略同源限制,所以可以在每个页面都嵌入同一个 iframe 然后监听 iframe 中传递的 message 就可以了。

同源页面的通信大致分为了三类:广播模式、共享存储模式和口口相传模式

第一种广播模式,就是可以通过 BroadCast Channel、Service Worker 或者 localStorage 作为广播,然后去监听广播事件中消息的变化,达到页面通信的效果。

第二种是共享存储模式,我们可以通过Shared Worker 或者 IndexedDB,创建全局共享的数据存储。然后再通过轮询去定时获取这些被存储的数据是否有变更,达到一个的通信效果。像常见cookie 也可以作为实现共享存储达到页面通信的一种方式

最后一种是口口相传模式,这个主要是在使用 window.open 的时候,会返回被打开页面的 window 的引用,而在被打开的页面可以通过 window.opener 获取打开它的页面的 window 点引用,这样,多个页面之间的 window 是能够相互获取到的,传递消息的话通过 postMessage 去传递再做一个事件监听就可以了

详细说说 HTTP 缓存

在浏览器第一次发起请求服务的过程中,会根据响应报文中的缓存标识决定是否缓存结果,是否将缓存标识和请求结果存入到浏览器缓存中。

HTTP 缓存分为强制缓存和协商缓存两类。协商缓存会问一下服务器,

强制缓存就是请求的时候浏览器向缓存查找这次请求的结果,这里分了三种情况

  • 没查找到直接发起请求(和第一次请求一致);
  • 查找到了并且缓存结果还没有失效就直接使用缓存结果;
  • 查找到但是缓存结果失效了就会使用协商缓存。

强制缓存有 Expires 和 Cache-control 两个缓存标识,Expires 是http/1.0 的字段,是用来指定过期的具体的一个时间(如 Fri, 02 Sep 2022 08:03:35 GMT),当服务器时间和浏览器时间不一致的话,就会出现问题。所以在 http1.1 添加了 cache-control 这个字段,它的值规定了缓存的范围(public/private/no-cache/no-store),也可以规定缓存在xxx时间内失效(max-age=xxx)是个相对值,就能避免了 expires带来的问题。

协商缓存就是强制缓存的缓存结果失效了,浏览器携带缓存标识向服务器发起请求,有服务器通过缓存标识决定是否使用缓存的过程。

控制协商缓存的字段有 last-modified / if-modified-since 和 Etag / if-none-match,后者优先级更高。

大致过程就是通过请求报文传递 last-modified 或 Etag 的值给服务器与服务器中对应值作对比,若和响应报文中的 if-modified-since 或 if-none-match 结果一致,则协商缓存有效,使用缓存结果,返回304;否则失效,重新请求结果,返回200

输入 URL 到页面展现的全过程

用户输入一段内容后,浏览器会先去判断这段内容是搜索内容还是 URL ,是搜索内容的话就会接合默认的搜索引擎生成 URL,比如 google 浏览器是goole.com/search?xxxx,如果是 URL 会拼接协议,比如 http/https。当页面没有监听 beforeupload 时间或者同意了继续执行流程,浏览器图标栏会进入加载中的状态。

接下来浏览器进程会通过 IPC 进程间通信将 URL 请求发送给网络进程,网络进程会先去缓存中查找该资源,如果有则拦截请求并直接200返回,没有的话会进入网络请求流程。

网络请求流程是网络进程请求 DNS 服务器返回域名对应的IP和端口号(如果这些之前有缓存也是直接返回缓存结果),如果没有端口号,http默认为80,https默认为443,如果是https还需要建立 TLS 安全连接创建加密的数据通道。

接着就是 TCP 三次握手建立浏览器和服务器连接,然后进行数据传输,数据传输完成四次挥手断开连接,如果设置了connection: keep-alive就可以一直保持连接。

网络进程将通过TCP获取的数据包进行解析,首先是根据响应头的content-type来判断数据类型,如果是字节流或者文件类型的话,会交给下载管理器进行下载,这时候导航流程就结束了。如果是 text/html 类型,就会通知到浏览器进程获取文档进行渲染。

浏览器进程获取到渲染的通知,会根据当前页面和新输入的页面判断是否是同一个站点,是的话就复用之前网页创建的渲染进程,否则的话会新创建一个单独的渲染进程。

浏览器进程将“提交文档”的消息给渲染进程,渲染进程接收到消息就会和网络进程建立传输数据的通道,数据传输完成后就返回“确认提交”的信息给浏览器进程。

浏览器接收到渲染进程的“确认提交“的消息后,就会更新浏览器的页面状态:安全状态、地址栏 URL、前进后退的历史消息,并更新 web页面,此时页面是空白页面(白屏)。

页面渲染过程(重点记忆)

最后是渲染进程对文档进行页面解析和子资源加载,渲染进程会将 HTML 转换成 DOM 树结构,将 css 转换成 styleSeets ( CSSOM)。然后复制 DOM 树过滤掉不显示的元素创建基本的渲染树,接着计算每个 DOM 节点的样式和计算每个节点的位置布局信息构建成布局树。

具有层叠上下文或者需要要裁剪的地方会独立创建图层,这就是分层,最终会形成一个分层树,渲染进程会给每个图层生成绘制列表并提交给合成线程,合成线程将图层分成图块(避免一次性绘制图层所有内容,可以根据图块优先需渲染视口部分),并在光栅化线程池中将图块转换成位图。

转换完毕后合成线程发送绘制图块命令 DrawQuard 给浏览器进程,浏览器根据 DrawQuard 消息生成页面,并显示到浏览器上。

速记:

浏览器的渲染进程将 html 解析成 dom树,将 css 解析成 cssom 树,然后会先复制一份 DOM 树过滤掉不显示的元素(比如 display: none),再和 cssom 结合进行计算每个 dom 节点的布局信息构建成一个布局树。

布局树生成完毕就会根据图层的层叠上下文或者裁剪部分进行分层,形成一个分层树。

渲染进程再将每个图层生成绘制列表并提交给合成线程,合成线程为了避免一次性渲染,就是分块渲染,会将图层分成图块,并通过光栅化线程池将图块转换成位图。

转换完毕后,合成线程将绘制图块的命令发送给浏览器进行显示

TCP 和 UDP 的区别

UDP 是用户数据包协议(User Dataprogram Protocol),IP 通过 IP 地址信息把数据包传送给指定电脑后,UDP 可以通过端口号把数据包分发给正确的程序。UDP 可以校验数据是否正确,但没有重发的机制,只会丢弃错误的数据包,同时 UDP 在发送之后无法确认是否到达目的地。UDP 不能保证数据的可靠性,但是传输的速度非常快,通常运用于在线视频、互动游戏这些不那么严格保证数据完整性的领域。

TCP 是为了解决 UDP 的数据容易丢失,且无法正确组装数据包二引入的传输控制协议(Transmission Control Protocol),是一种面向连接的,可靠的,基于字节流的传输层通信协议。TCP 在处理数据包丢失的情况,提供了重传机制;并且 TCP 引入了数据包排序机制,可以将乱序的数据包组合成完整的文件。

TCP 头除了包含目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包。

http和https

HTTPS 是更安全的 HTTP 协议,它在 TCP(负责网络数据传输)和 HTTP层 之间,增加了一个 SSL 层。这一层通过数字证书和加密算法对 HTTP 请求进行加密。

TCP 生命周期

一个 TCP 连接的生命周期会经历链接阶段,数据传输和断开连接阶段三个阶段。

连接阶段

用来建立客户端和服务器之间的链接,通过三次握手用来确认客户端、服务端相互之间的数据包收发能力。

1、客户端先发送 SYN 报文用来确认服务端能够发数据,并进入 SYN_SENT 状态等待服务端确认
2、服务端收到 SYN 报文,会向客户端发送一个 ACK 确认报文,同时服务端也会向客户端发送 SYN 报文用来确认客户端是否能够发送数据,此时服务端进入 SYN_RCVD 状态
3、客户端接收到 ACK + SYN 的报文,就会向服务端发送数据包并进入 ESTABLISHED 状态(建立连接);服务端接收到客户端发送的 ACK 包也会进入 ESTABLISHED 状态,完成三次握手

传输数据阶段

该阶段,接收端需要对每个包进行确认操作;
所以当发送端发送了一个数据包之后,在规定时间内没有接收到接收端反馈的确认消息,就会判断为包丢失,从而触发重发机制;
一个大文件在传输过程中会分为很多个小数据包,数据包到达接收端后会根据 TCP 头中的序号为其排序,保证数据的完整。

断开连接阶段

通过四次挥手,来保证双方的建立的连接能够断开

1、客户端向服务器发起 FIN 包,并进入 FIN_WAIT_1 状态
2、服务端收到 FIN 包,发出确认包 ACK,并带上自己的序号,服务端进入 CLOSE_WAIT 状态。这时候客户端已经没有数据要发给服务端了,但是服务端如果有数据要发给客户端,客户端还是需要接收。客户端收到 ACK 后进入 FIN_WAIT_2 状态
3、服务端数据发送完毕后,向客户端发送 FIN 包,此时服务器进入 LAST_ACK 状态
4、客户端收到 FIN 包发出确认包 ACK ,此时客户端进入 TIME_WAIT 状态,等待 2 MSL 后进入 CLOSED 状态;服务端接收到客户端的 ACK 后就进入 CLOSED 状态了。

对于四次挥手,因为 TCP 是全双工通信,在主动关闭方发送 FIN 包后,接收端可能还要发送数据,不能立即关闭服务器端到客户端的数据通道,所以也就不能将服务器端的 FIN 包与对客户端的 ACK 包合并发送,只能先确认 ACK,然后服务器待无需发送数据时再发送 FIN 包,所以四次挥手时必须是四次数据包的交互

Content-length 了解吗?

Content-length 是 http 消息长度,用十进制数字表示的字节的数目。

如果 content-length > 实际长度,服务端/客户端读取到消息队尾时会继续等待下一个字节,会出现无响应超时的情况

如果 content-length < 实际长度,首次请求的消息会被截取,然后会导致后续的数据解析混乱。

当不确定content-length的值应该使用Transfer-Encoding: chunked,能够将需要返回的数据分成多个数据块,直到返回长度为 0 的终止块

跨域常用方案

什么是跨域?

协议 + 域名 + 端口号均相同时则为同域,任意一个不同则为跨域

解决方案

1、 传统的jsonp:利用<script>标签没有跨域限制的特点,仅支持 get接口,应该没有人用这个了
2、 一般使用 cors(跨域资源共享)来解决跨域问题,浏览器在请求头中发送origin字段指明请求发起的地址,服务端返回Access-control-allow-orign,如果一致的话就可以进行跨域访问
3、 Iframe 解决主域名相同,子域名不同的跨域请求
4、 浏览器关闭跨域限制的功能
5、 http-proxy-middleware 代理

预检

补充:http会在跨域的时候发起一次预检请求,“需预检的请求”要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

withCredentials为 true不会产生预请求;content-type为application/json会产生预请求;设置了用户自定义请求头会产生预检请求;delete方法会产生预检请求;

websocket

websocket是一种支持双向通信的协议,就是服务器可以主动向客户端发消息,客户端也可以主动向服务器发消息。

它是基于 HTTP 协议来建立连接的的,与http协议的兼容性很好,所以能通过 HTTP 代理服务器;没有同源限制。

WebSocket 是一种事件驱动的协议,这意味着可以将其用于真正的实时通信。与 HTTP 不同(必须不断地请求更新),而使用 websockets,更新在可用时就会立即发送

当连接终止时,WebSockets 不会自动恢复,这是应用开发中需要自己实现的机制,也是存在许多客户端开源库的原因之一。

像webpack和vite的devServer就使用了websocket实现热更新

连接过程

  • 先用带有 Upgrade:Websocket头Header的特殊HTTP request来实现与服务端握手HandShake;
  • 握手成功后,协议升级成Websocket,进行长连接通讯;
  • 整个过程可理解为:小锤抠缝,大锤搞定

为什么不使用HTTP长连接来实现即时通讯?

事实上,在Websocket之前就是使用HTTP长连接这种方式,如Comet。但是它有如下弊端:

  • HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞。
  • 对于服务端来说,每个长连接都占有一个用户线程,在NIO或者异步编程之前,服务端开销太大

性能优化篇

性能优化常用手段

1. 从缓存的角度

  • 将一些不常变的大数据通过localstorage/sessionStorage/indexdDB 进行读取
  • 活用 http 缓存(强缓存和协商缓存),将内容存储在内存或者硬盘中,减少对服务器端的请求

2. 网络方面比较常用的是静态资源使用 CDN

3. 打包方面

  • 路由的按需加载
  • 优化打包后资源的大小
  • 开启 gzip 压缩资源
  • 按需加载三方库

4. 代码层面

  • 减少不必要的请求,删除不必要的代码
  • 避免耗时过长的js处理阻塞主线程(耗时且无关 DOM 可以丢到web worker去处理或者拆分成小的任务)
  • 图片可以使用懒加载的方式,长列表使用虚拟滚动

5. 首屏速度提升

  • 代码压缩,减少打包的静态资源体积(Terser plugin/MiniCssExtratplugin)
  • 路由懒加载,首屏就只会请求第一个路由的相关资源
  • 使用 cdn加速第三方库,我们是toB的产品,会需要部署在内网,所以一般不用,toC用的多
  • ssr 服务端渲染,由服务器直接返回拼接好的html页面

6. vue 常见的性能优化方式

  • 图片懒加载: vue-lazyLoad
  • 虚拟滚动
  • 函数式组件
  • v-show/ keep-alive 复用 dom
  • deffer延时渲染组件(requestIdleCallback)
  • 时间切片 time slicing

前端监控 SDK 技术要点

  • 可以通过window.performance获取各项性能指标数据

  • 完整的前端监控平台包括:数据采集和上报、数据整理和存储、数据展示

  • 网页性能指标,通过PerformanceObserver获取:

    • FP(first-paint)从页面加载到第一个像素绘制到屏幕上的时间
    • FCP(first-contentful-paint),从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间
    • LCP(largest-contentful-paint),从页面加载到最大文本或图像元素在屏幕上完成渲染的时间
  • 首屏渲染时间计算:通过MutationObserver监听document对象的属性变化

如何减少回流、重绘,充分利用 GPU 加速渲染?

首先应该避免直接使用 DOM API 操作 DOM,像 vue react 虚拟 DOM 让对 DOM 的多次操作合并成了一次。

  1. 样式集中改变,好的方式是使用动态 class
  2. 读写操作分离,避免读后写,写后又读
  3. Css 中的transform、opacity、filter、will-change能触发硬件加速
  4. 使用display: none后元素不会存在渲染树中,这时对它进行各种操作,然后更改 display 显示即可(示例:向2000个div中插入一个div)
  5. 通过 documentFragment 创建 dom 片段,在它上面批量操作 dom ,操作完后再添加到文档中,这样只有一次重排(示例:一次性插入2000个div)
  6. 使用 BFC 脱离文档流,重排开销小

前端工程化

webpack的执行流程和生命周期

webpack 是为现代 JS 应用提供静态资源打包功能的 bundle。

核心流程有三个阶段: 初始化阶段、构建阶段和生成阶段

  1. 初始化阶段,会从配置文件、配置对象和Shell参数中读取初始化的参数并与默认配置结合成最终的参数,之以及创建 compiler 编译器对象和初始化它的运行环境
  2. 构建阶段,编译器会执行它的 run()方法开始编译的过程,其中会先确认 entry 入口文件,从入口文件开始搜索和入口文件有直接或者简介关联的所有文件创建依赖对象,之后再根据依赖对象创建 module 对象,这时候会使用 loader 将模块转换标准的 js 内容,再调用 js 的解释器将内容转换成 AST 对象,再从 AST 中找到该模块依赖的模块,递归本步骤知道所有入口依赖文件都经过了本步骤的处理。最后完成模块编译,得到了每个模块被翻译的内容和他们之间的关系依赖图(dependency graph),这个依赖图就是项目所有用到的模块的映射关系。
  3. 生成阶段,将编译后的 module 组合成 chunk ,再把每个 chunk 转换成一个单独的文件输出到文件列表,确定好输出内容后,根据配置确定输出路径和文件名,就把文件内容写入文件系统

webpack的plugin 和loader

loader

webpack只能理解 JS 和 JSON 文件,loader 本质上就是个转换器,能将其他类型的文件转换成 webpack 识别的东西

loader 会在 webpack 的构建阶段将依赖对象创建的 module 转换成标准的 js 内容的东西。比如 vue-loader 将vue文件转换成 js 模块,图片字体通过 url-loader 转换成 data URL,这些 webpack 能够识别的东西。

可以在 module.rules 中配置不同的 loader 解析不同的文件

plugin

插件本质是一个带有 apply 函数的类class myPlugin { apply(compiler) {} },这个apply 函数有个参数 compiler 是webpack 初始化阶段生成的编译器对象,可以调用编译器对象中的 hooks 注册各种钩子的回调这些 hooks 是贯穿整个编译的生命周期。所以开发者可以通过钩子回调在里面插入特定的代码,实现特定的功能。

比如stylelint plugin可以指定 stylelint 的需要检查文件类型和文件范围;HtmlWebpackPlugin 用来生成打包后的模板文件;MiniCssExtactPlugin会将所有的css提取成独立的chunks,stylelintplugin可以在开发阶段提供样式的检查功能。

vite原理

Vite 主要由两个部分组成

开发环境

Vite 利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用(就相当于把我们在开发的文件转换成 ESM 格式直接发送给浏览器)

当浏览器解析 import HelloWorld from ‘./components/HelloWorld.vue’ 时,会向当前域名发送一个请求获取对应的资源(ESM支持解析相对路径),浏览器直接下载对应的文件然后解析成模块记录(打开 network 面板可以看到响应数据都是 ESM 类型的 js)。然后实例化为模块分配内存,按照导入导出语句建立模块和内存的映射关系。最后运行代码。

vite 会启动一个 koa 服务器拦截浏览器对 ESM 的请求,通过请求路径找到目录下对应的文件并处理成 ESM 格式返回给客户端。

vite的热加载是在客户端和服务端之间建立了 websocket 连接,代码修改后服务端发送消息通知客户端去请求修改模块的代码,完成热更新,就是改了哪个 view 文件就重新请求那个文件,这样保证了热更新速度不受项目大小影响。

开发环境会使用 esbuild 对依赖进行个预构建缓存,第一次启动会慢一点,后面的启动会直接读取缓存

生产环境

使用 rollup 来构建代码,提供指令可以用来优化构建过程。缺点就是开发环境和生产环境可能不一致;

webpack 和 vite 对比

Webpack 的热更新原理简单来说就是,一旦发生某个依赖(比如 a.js )改变,就将这个依赖所处的 module 的更新,并将新的 module 发送给浏览器重新执行。每次热更新都会重新生成 bundle。试想如果依赖越来越多,就算只修改一个文件,理论上热更新的速度也会越来越慢

Vite 利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用,热更新是在客户端和服务端之间建立了 websocket 连接,代码修改后服务端发送消息通知客户端去请求修改模块的代码,完成热更新,就是改了哪个文件就重新请求那个文件,这样保证了热更新速度不受项目大小影响。

所以vite目前的最大亮点在于开发体验上,服务启动快、热更新快,明显地优化了开发者体验,生产环境因为底层是 rollup ,rollup更适合小的代码库,从扩展和功能上都是不如 webpack 的,可以使用vite作为一个开发服务器dev server使用,生产打包用webpack这样的模式。

做过哪些 wewbpack 的优化

3、loaders的运行是同步的,同各模块会执行全部的loaders

  • 可以使用oneOf,只要匹配上对应的loader就不会继续执行loader
  • 使用 happyPack 将loader的同步执行转换成并行比如(style-loader,css-loader,less-loader合并起来执行)

正则表达式

正则表达式是什么数据类型?

是对象,let re = /ab+c/等价于let re = new RegExp('ab+c')

贪婪模式和非贪婪模式?

贪婪模式

正则中,表示次数的量词默认是贪婪的,会尽可能匹配最大长度,比如a*会从第一个匹配到a的时候向后匹配尽可能多的a,直到没有a为止。

非贪婪模式

在量词后面加?就能变成非贪婪模式,非贪婪即找出长度最小且满足条件的

babel原理和用途

babel 用途

转义 esnext、typescript 到目标环境支持 js (高级语言到到低级语言叫编译,高级语言到高级语言叫转译)
代码转换(taro)
代码分析(模块分析、tree-shaking、linter 等)

bebel 如何转换的?

对源码字符串 parse 生成 AST,然后对 AST 进行增删改,然后输出目标代码字符串

小程序相关

简述微信小程序原理

小程序本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口;

微信小程序采用JavaScript、WXML、WXSS三种技术进行开发,从技术讲和现有的前端开发差不多,但深入挖掘的话却又有所不同。

JavaScript:首先JavaScript的代码是运行在微信App中的,并不是运行在浏览器中,因此一些H5技术的应用,需要微信App提供对应的API支持,而这限制住了H5技术的应用,且其不能称为严格的H5,可以称其为伪H5,同理,微信提供的独有的某些API,H5也不支持或支持的不是特别好。

WXML:WXML微信自己基于XML语法开发的,因此开发时,只能使用微信提供的现有标签,HTML的标签是无法使用的。

WXSS:WXSS具有CSS的大部分特性,但并不是所有的都支持,而且支持哪些,不支持哪些并没有详细的文档。

简单描述下微信小程序的相关文件类型

  • WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。内部主要是微信自己定义的一套组件
  • WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式
  • js 逻辑处理,网络请求
  • json 小程序设置,如页面注册,页面标题及tabBar

主要文件

  • app.json 微信框架把这个作为配置文件入口,整个小程序的全局配置。包括页面注册,网络设置,以及小程序的 window 背景色,配置导航条样式,配置默认标题
  • app.js 可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量,设置一些全局的基础数据等
  • app.wxss 公共样式,引入iconfont等
  • project.config.json:项目配置文件,用的最多的就是配置是否开启https校验
  • pages:里面包含一个个具体的页面
  • index.json:配置当前页面标题和引入组件
  • index.wxml:页面结构
  • index.wxss:页面样式表
  • index.js:页面的逻辑,请求和数据处理

小程序的双向绑定和vue哪里不一样

小程序直接 this.data 的属性是不可以同步到视图的,必须调用:

this.setData({
    // 这里设置
})

小程序页面间有哪些传递数据的方法

使用全局变量

在 app.js 文件中定义全局变量 globalData, 将需要存储的信息存放在里面

// app.js
App({
     // 全局变量
  globalData: {
    userInfo: null
  }
})

使用的时候,直接使用 getApp() 拿到存储的信息

使用 wx.navigateTo 与 wx.redirectTo 的时候,可以将部分数据放在 url 里面,并在新页面 onLoad 的时候初始化

// Navigate
wx.navigateTo({
  url: '../pageD/pageD?name=raymond&gender=male',
})

// Redirect
wx.redirectTo({
  url: '../pageD/pageD?name=raymond&gender=male',
})

// pageB.js
...
Page({
  onLoad: function(option){
    console.log(option.name + 'is' + option.gender)
    this.setData({
      option: option
    })
  }
})

需要注意的问题:wx.navigateTo 和 wx.redirectTo 不允许跳转到 tab 所包含的页面 onLoad 只执行一次

使用本地缓存 Storage 相关

使用组件模板 template传递参数

给html元素添加data-属性来传递值,然后通过e.currentTarget.dataset或onload的param参数获取(data- 名称不能有大写字母,不可以存放对象)

设置id 的方法标识来传值,通过e.currentTarget.id获取设置的id值,然后通过设置全局对象的方式来传递数据

在navigator中添加参数数值

小程序的生命周期函数

  • onLoad 页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数
  • onShow() 页面显示/切入前台时触发
  • onReady() 页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互
  • onHide() 页面隐藏/切入后台时触发。 如 navigateTo 或底部 tab 切换到其他页面,小程序切入后台等
  • onUnload() 页面卸载时触发。如 redirectTo 或 navigateBack 到其他页面时

微信小程序的优劣势

优势

  • 即用即走,不用安装,省流量,省安装时间,不占用桌面

  • 依托微信流量,天生推广传播优势

  • 开发成本比 App 低

    缺点

  • 用户留存,即用即走是优势,也存在一些问题

  • 入口相对传统 App 要深很多

  • 限制较多,页面大小不能超过2M。不能打开超过10个层级的页面

小程序经典功能实现

下拉刷新

  1. 首先在全局 config 中的 window 配置 enablePullDownRefresh
  2. Page 中定义 onPullDownRefresh 钩子函数,到达下拉刷新条件后,该钩子函数执行,发起请求方法
  3. 请求返回后,调用 wx.stopPullDownRefresh 停止下拉刷新

bindtap和catchtap的区别是什么?

相同点:首先他们都是作为点击事件函数,就是点击时触发。在这个作用上他们是一样的,可以不做区分

不同点:他们的不同点主要是bindtap是不会阻止冒泡事件的,catchtap是阻值冒泡的

简述下 wx.navigateTo(), wx.redirectTo(), wx.switchTab(), wx.navigateBack(), wx.reLaunch()的区别?

  • wx.navigateTo():保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面
  • wx.redirectTo():关闭当前页面,跳转到新的页面(类似重定向)。但是不允许跳转到 tabbar 页面
  • wx.switchTab():跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
  • wx.navigateBack():关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层
  • wx.reLaunch():关闭所有页面,打开到应用内的某个页面

登录流程

登录流程是调wx.login获取code传给后台服务器获取微信用户唯一标识openid及本次登录的会话密钥(session_key)等)。拿到开发者服务器传回来的会话密钥(session_key)之后,前端要保存wx.setStorageSync('sessionKey', 'value')

持久登录状态:session信息存放在cookie中以请求头的方式带回给服务端,放到request.js里的wx.request的header里

小程序支付

  1. 小程序注册,要以公司的身份去注册一个小程序,才有微信支付权限
  2. 绑定商户号
  3. 在小程序填写合法域
  4. 调用wx.login()获取appid
  5. 调用
wx.requestPayment(
{
    'timeStamp': '',//时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
    'nonceStr': '',//随机字符串,长度为32个字符以下。
    'package': '',//统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
    'signType': 'MD5',//签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
    'paySign': '',//签名,具体签名方案参见微信公众号支付帮助文档;
    'success':function(res){},//成功回调
    'fail':function(res){},//失败
    'complete':function(res){}//接口调用结束的回调函数(调用成功、失败都会执行)
})

提高小程序运行速度的方法

1.提高页面加载速度:

首先用户在页面切换也就是路由跳转时,会有一个100ms-300ms的一个时间,我们可以利用这个时间段,预先发起新页面所需要的网络加载时间。

例如从A页面预加载B页面时,我们可以通过在A页面通过this.$route的方法向B页面传递参数,B页面接收到参数之后利用我们封装好的全局put take的方法,利用缓存来进行ajax请求

疑难:为什么我在A页面时会访问到B页面的实例呢?此时B页面不是未创建吗?

解答:

  1. 首先根据微信小程序的机制来说,在我们小程序启动时,会把所有的page()方法内的object存在一个队列中,每次页面访问时,就会创建一个新的对象实例,简单理解就是深拷贝。

  2. 在我们的A页面进行点击响应事件的时候,B页面实例此时未加载,所以此时调用onload方法(onNavigate)方法时,此时this指向还是在page对象的原型,也就是小程序启动时刚创建时的那个实例。

  3. 而马上被创建好的B页面此时又是另外一个obeject对象,此时this指向不是同一个对象,不能把我们临时的数据存储住,因此我们可以封装一个全局的put take缓存方法进行存储。

  4. 为了通用性,我们将所有公共的方法 比如put take route都定义在了page这个基类之中,基类同时还保存了所有的list页面,这样就可以根据需求来通过onNavigate方法预加载我们的页面了。哪个页面有onNavigate方法就执行预加载,没有则不执行。

    2.用户行为预测:

  5. 什么是用户行为预测,就是根据用户可能点击某一个界面的机率来预先加载数据,从而实现界面秒开的效果,提高用户的浏览体验。

  6. 与提高页面加载速度原理大致相同,我们需要给page对象拓展一个方法,在我们每次预载的页面之中调用这个方法,与上一个方法不同的是,本方法的临时数据会储存在storge之中,因为用户有可能不点击这个页面,而把数据全部存储在全局变量中,会影响小程序本身的内存,小程序本身一个也就支持11M的数据。

    3.减少默认data的大小:

    从一个面跳转到下一个页面时,此时我们跳转后的页面会深拷贝一个page对象,所以我们为了提高性能理应减少data的体积,比如data有100个属性时,就会有一个150ms的延迟。

    4.组件化方案:

    我们将一些公共的组件一个封装,在我们需要的时候来进行一个组件的调用,提高复用性,避免内存的重复占用。

小程序常见问题

rpx:

小程序的尺寸单位,规定屏幕为750rpx,可适配不同分辨率屏幕

本地资源无法通过wxss获取:

background-image:可以使用网络图片,或者base64,或者使用标签

wx.navigateTo无法打开页面:

一个应用同时只能打开5个页面,请避免多层级的交互方式,或使用wx.redirectTo

tabBar设置不显示:

1.tabBar的数量少于2项或超过5项都不会显示。
2.tabBar写法错误导致不会显示。
3.tabBar没有写pagePath字段(程序启动后显示的第一个页面)

小程序关联微信公众号如何确定用户的唯一性?

使用wx.getUserlnfo方法 withCredentials为true时,可获取encryptedData,里面有union_id,后端需要进行对称解密

小程序调用后台接口遇到那些问题?

数据的大小限制,超过范围会直接导致整个小程序崩溃,除非重启小程序
小程序不可以直接渲染文章内容这类型的html文本,显示需要借助插件
注:插件渲染会导致页面加载变慢,建议在后台对文章内容的html进行过滤,后台直接处理批量替换p标签div标签为view标签,然后其他的标签让插件来做

webview中的页面怎么跳转回小程序?

wx.miniProgram.navigateTo({
    url:'pages/login/login'+'$params'
})
//跳转到小程序导航页面
wx.miniProgram.switchTab({
    url:'/pages/index/index'
})

// 首先,需要在你的html页面中引用一个js文件
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.0.js"></script>
//然后为你的按钮标签注册一个点击事件
$(".kaiqi").click(function(){
        wx.miniProgram.redirectTo({url: '/pages/indexTwo/indexTwo'})
});
// 这里的redirectTo跟小程序的wx.redirectTo()跳转页面是一样的,会关闭当前跳转到页面,换成navigateTo,跳转页面就不会关闭当前页面

使用webview直接加载要注意那些事项?

必须要在小程序后台使用管理员添加业务域名
h5页面跳转至小程序的脚步必须是1.3.1以上
微信分享只可以是小程序的主名称,如要自定义分享内容,需小程序版本在1.7.1以上
h5的支付不可以是微信公众号的appid,必须是小程序的appid,而且用户的openid也必须是用户和小程序的

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。