1. JSBridge 通信?

主要是给 JavaScript 提供调用 Native 功能的接口,让混合开发中的前端部分可以方便地使用 Native 的功能(例如:地址位置摄像头)。是 Native非 Native 之间的桥梁,它的核心是构建 Native非 Native消息通信的通道 ,而且这个通信的通道是双向的

  • JSBridge 的通信原理

    1. JavaScript 调用 Native 的方式: 主要有两种注入API拦截URL SCHEME

      • 注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScriptContext(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码 逻辑,达到 JavaScript 调用 Native 的目的

      • 拦截 URL SCHEME 的主要流程是:Web 端 通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作

        在时间过程中,这种方式有一定的缺陷

        1. 使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患。
        2. 创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长

      因此JavaScript 调用 Native 推荐使用: 注入 API 的方式

    2. Native 调用 JavaScript 的方式

      相比于 JavaScript 调用 NativeNative 调用 JavaScript较为简单,直接执行拼接好的JavaScript 代码即可。从外部调用JavaScript中的方法,因此JavaScript的方法必须在全局的window` 上。

扩展RNwebView 的通信

  • webview 中点击事件代码

    通过 postMessage()方法,传递 两个参数。一个是 type 值,是我们做判断时候用的。判断是哪种类型的消息。另一个是我们想传递的 其他参数。这里记得参数传递前做一次:JSON.stringify()方法,解析 json 字符串

  • RN 中获取传过来的参数 Webview 组件的 onMessage 方法中接受:JSON.parse(event.nativeEvent.data)

2. 小程序 与 Vue 有什么区别?

  • 生命周期:相比之下,小程序的钩子函数要简单得多。vue 的钩子函数在跳转新页面时,钩子函数都会触发,但是小程序的钩子函数,页面不同的跳转方式,触发的钩子并不一样

    1. onLoad页面加载: 一个页面只会调用一次,可以在 onLoad 中获取打开当前页面所调用的 query 参数

    2. onShow: 页面显示: 每次打开页面都会调用一次

    3. onReady: 页面初次渲染完成: 一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。对界面的设置如 wx.setNavigationBarTitle 请在 onReady 之后设置

    4. onHide: 页面隐藏: 当 navigateTo 或底部 tab 切换时调用

    5. onUnload: 页面卸载: 当 redirectTonavigateBack 的时候调用

  • 数据请求

    在页面加载请求数据时,两者钩子的使用有些类似,vue 一般会在 created 或者 mounted 中请求数据。而在小程序,会在 onLoad 或者 onShow 中请求数据

  • 数据绑定

    1. Vue: vue 动态绑定一个变量的值为元素的某个属性的时候,会在变量前面加上冒号

      <img :src="imgSrc" />
      
    2. 小程序: 绑定某个变量的值为元素属性时,会用 两个大括号 括起来,如果不加括号,为被认为是 字符串

      <image src="{{imgSrc}}"></image>
      
  • 列表渲染

    1. Vue: <li v-for="item in items"></li>
    2. 小程序: <text wx:for=""></text>
  • 显示与隐藏元素

    1. Vue: 使用 v-ifv-show 控制元素的显示和隐藏
    2. 小程序: 使用 wx-ifhidden 控制元素的显示和隐藏
  • 事件处理

    1. Vue: 使用 v-on:event 绑定事件,或者使用@event 绑定事件

      <button v-on:click="counter += 1">Add 1</button>
      <button @click="counter += 1">Add 1</button>
      
    2. 小程序: 全用 bindtap(bind+event),或者 catchtap(catch+event) 绑定事件

      <button bindtap="noWork">明天不上班</button>
      <button catchtap="noWork">明天不上班</button>
      
  • 数据双向绑定

    1. Vue: 表单元素上加 v-model
    2. 小程序: 通过监听输入框事件 bindinput,调用 setData({})方法进行模拟
  • 取值

    1. Vue: 通过 this.xxx 取值
    2. 小程序: 通过 this.data.xxx 取值
  • 绑定事件传参

    1. Vue: 绑定事件传参挺简单,只需要在触发事件的方法中,把需要传递的数据作为形参传入就可以了

      <button @click="say('明天不上班')"></button>
      
    2. 小程序: 不能直接在绑定事件的方法中传入参数,需要将参数作为属性值,绑定到元素上的 data-属性 上,然后在方法中,通过 e.currentTarget.dataset.* 的方式获取

      <view class="tr" bindtap="toApprove" data-id="{{item.id}}"></view>
      toApprove(e) { let id = e.currentTarget.dataset.id; }
      

小程序实现原理解析

  • 小程序目录结构

    • 一个完整的小程序主要由以下几部分组成:
      1. 一个入口文件:app.js
      2. 一个全局样式:app.wxss
      3. 一个全局配置:app.json

    页面:pages 下,每个页面再按文件夹划分,每个页面 4 个文件

    1. 视图:wxml, wxss
    2. 逻辑:js, json (页面配置, 不是必须)
  • 小程序架构

    微信小程序的框架包含两部分:View视图层App Service逻辑层. View 层 用来渲染页面结构,AppService 层 用来 逻辑处理数据请求接口调用,它们在两个进程(两个 Webview)里运行

    视图层逻辑层 通过系统层的 JSBridge 进行通信,逻辑层 把数据变化通知到 视图层,触发视图层页面更新,视图层把触发的事件通知到 逻辑层 进行业务处理

    扩展:小程序不允许打开超过 5 个层级的页面?

    小程序的视图和逻辑处理是用多个 webview 实现的,逻辑处理的 JS 代码全部加载到一个 Webview 里面,称之为 AppService,整个小程序只有一个,并且整个生命周期常驻内存。而所有的视图(wxmlwxss)都是单独的 Webview 来承载,称之为 AppView。所以一个小程序打开至少就会有 2 个 webview 进程,正式因为每个视图都是一个独立的 webview 进程,考虑到性能消耗

    原理上微信 App 里包含 javascript 运行引擎WXML/WXSS 处理引擎,最终会把界面翻译成系统原生的界面,并展示出来。这样做的目的是为了提供和原生 App 性能相当的用户体验。

    • ios 中,小程序的 javascript 运行在 javascriptCore
    • Android 中,小程序的 javascript 是通过 X5 内核 来解析的

小程序的优劣势

  • 优势
    1. 无需下载,通过搜索和扫一扫就可以打开
    2. 良好的用户体验:打开速度快
    3. 开发成本要比 App 要低
    4. 安卓上可以添加到桌面,与原生 App 差不多
    5. 为用户提供良好的安全保障。小程序的发布,微信拥有一套严格的审查流程, 不能通过审查的小程序是无法发布到线上的
  • 劣势:
    1. 限制较多: 页面大小不能超过 1M。不能打开超过 5 个层级 的页面
    2. 样式单一: 小程序的部分组件已经是成型的了,样式不可以修改。例如:幻灯片、导航
    3. 推广面窄: 不能分享朋友圈,只能通过分享给朋友,附近小程序推广。其中附近小程序也受到微信的限制
    4. 依托于微信: 无法开发后台管理功能

提高微信小程序的应用速度的手段有哪些?

小程序首次启动前,微信会在小程序启动前为小程序准备好通用的运行环境,如运行中的线程和一些基础库的初始化。然后才开始进入启动状态,展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作:

  1. 下载 小程序代码包
  2. 加载 小程序代码包
  3. 初始化 小程序首页

注意:下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包

围绕上图小程序的启动流程, 提高速度我们可以从加载渲染两个纬度进行切入:

  • 小程序启动加载性能:
    1. 控制代码包的大小
    2. 分包加载
    3. 首屏体验(预请求,利用缓存,避免白屏)
  • 小程序渲染性能
    1. 不要过于频繁调用 setData,应考虑将多次 setData 合并成一次 setData 调用
    2. 与界面渲染无关的数据最好不要设置在 data
    3. 自定义组件(各个组件也将具有各自独立的逻辑空间。分别拥有自己的独立的数据、setData 调用)

3. 团队的开发流程?

4. 如何防止首页白屏?

首屏时间First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容

  • 通过 DOMContentLoad 或者 performance 来计算出首屏时间?
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
    console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
  name: "first-contentful-paint",
  entryType: "paint",
  startTime: 507.80000002123415,
  duration: 0,
};

先说下 Spa 单页面 的加载过程: 首先就是 html,也就是 FP 阶段; 然后是静态资源 cssjs,之后解析 js,生成 HTML,也就是 FCP 阶段css, js 资源加载下来了,首次的内容绘制,有一个大结构了; 到最后,就是 FMP,ajax 请求数据之后,首次有效绘制,就是页面加载差不多了,但是可能图片还没加载出来

总结: 从 FPFMP 这个过程全是 白屏,可能你的 header 如果有啥大背景色啊,这个背景色或许会出来,ajax 之后,才会真正去解析我们的数据,把数据放入我们的 html 标签中

  • 加载慢的原因

    在页面渲染的过程,导致加载速度慢的因素可能如下:

    1. 网络延时问题
    2. 资源文件体积是否过大
    3. 资源是否重复发送请求去加载了
    4. 加载脚本的时候,渲染内容堵塞了
  • 解决方案:

    1. 减少入口文件体积

      常用的手段是 路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加

      vue-router 配置路由的时候,采用动态加载路由的形式:

      routes:[
         path: 'Test',
         name: 'Test',
         component: () => import('./components/Test.vue')
      ]
      
    2. 静态资源本地缓存

      后端返回资源问题:

      • 采用 HTTP 缓存,设置 Cache-ControlLast-ModifiedEtag 等响应头
      • 采用 Service Worker 离线缓存

      前端合理利用 localStorage

    3. UI 框架按需加载

    4. 图片资源的压缩

      对大图片进行压缩、对 icon 图可以使用在线字体图标或者雪碧图,将众多小图标合并到同一张图上,用以减轻 http 请求压力。

    5. 组件重复打包

      如果一个依赖库被多个文件引入会多次打包。解决方案:在 webpackconfig 文件中,修改 CommonsChunkPlugin 的配置。minChunks 为 3 表示会把使用 3 次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件

    6. 开启 GZip 压缩

      plugins: [
        new CompressionPlugin({
          test: /\.js$|\.html$|\.css/, // 匹配文件名
          threshold: 10240, // 对超过10k的数据进行压缩
          deleteOriginalAssets: false, // 是否删除原文件
        }),
      ]
      
    7. 使用 SSR

      SSR(Server side ),也就是 服务端渲染,组件或页面通过服务器生成 html 字符串,再发送到浏览器

      vue -> Nuxt.js, react -> Next.js

  • 减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化页面渲染优化

  • 解决办法:

    1. 预渲染

    2. SSR

    3. 路由懒加载

    4. 使用 Gzip 压缩

    5. webpack entry 多页应用

    6. 骨架屏(原理)

    7. loading

      在首页 index.html 里加一个 loading css 效果,当页面加载完成消失

5. 怎么理解前端工程化、模块化、组件化?

  • 工程化:是一种思想而不是某种技术,而模块化和组件化是为工程化思想下相对较具体的开发方式。可以简单的认为模块化和组件化是工程化的表现形式
  • 模块化开发:一个模块就是一个实现特定功能的文件,有了模块我们就可以更方便的使用别人的代码,要用什么功能就加载什么模块
    • 模块化开发的好处:
      1. 提高代码复用率和维护性
      2. 避免变量污染、命名冲突
  • 组件化开发:就是将一个页面拆分成很多的小组件

6. 微前端中的渲染、JS 沙箱、样式隔离、数据通信?

7. 如何实现扫码登录功能?

  • 访问 PC 端二维码生成页面,PC 端请求服务端获取 二维码ID
  • 服务端生成相应的二维码ID,设置二维码的过期时间,状态等。
  • PC 获取二维码ID,生成相应的二维码。
  • 手机端扫描二维码,获取二维码ID
  • 手机端将手机端token二维码ID发送给服务端,确认登录。
  • 服务端校验手机端token,根据手机端token二维码ID生成 PC 端token
  • PC 端通过轮询方式请求服务端,通过二维码 ID 获取二维码状态,如果已成功,返回 PC token,登录成功。

扩展:轮询方式有哪些,以及它们各自的优缺点?

  • 轮询

    基本思路: 就是浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。

    优点: 是比较简单,易于理解,实现起来也没有什么技术难点。

    缺点: 由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源. 人数越多,服务器端压力越大,这是很不合理的。

  • 长轮询

    基本思路: 服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制(服务器端设置)才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。

    优点: 减少了很多不必要的 http 请求次数,相比之下节约了资源

    缺点: 接挂起也会导致资源的浪费

    轮询与长轮询都是基于 HTTP 的

  • 长连接(SSE) SSEHTML5 新增的功能,全称为 Server-Sent Events。它可以允许服务推送数据到客户端。SSE 在本质上就与之前的长轮询、短轮询不同,虽然都是基于 http 协议的,但是轮询需要客户端先发送请求。而 SSE 最大的特点就是不需要客户端发送请求,可以实现只要服务器端数据有更新,就可以马上发送到客户端。

    优点: 它不需要建立或保持大量的客户端发往服务器端的请求,节约了很多资源,提升应用性能

  • WebSocket

    WebSocketHtml5 定义的一个新协议,与传统的 http 协议不同,该协议可以实现服务器与客户端之间全双工通信

8. 如何防止重复发送请求?

let count = 1
let promiseFunction = () =>
  new Promise((rs) =>
    window.setTimeout(() => {
      rs(count++)
    })
  )
let firstFn = firstPromise(promiseFunction)
firstFn().then(console.log) // 1
firstFn().then(console.log) // 1
firstFn().then(console.log) // 1

function firstPromise(promiseFunction) {
  let p = null
  return function(...args) {
    // 请求的实例,已存在意味着正在请求中,直接返回实例,不触发新的请求
    return p
      ? p
      : // 否则发送请求,且在finally时将p置空,那么下一次请求可以重新发起
        (p = promiseFunction.apply(this, args).finally(() => (p = null)))
  }
}

扩展:前端做后台管控系统,在某些接口请求时间过长的场景下,需要防止用户反复发起请求,实现方式也有好几种:

  1. 在按钮点击发起请求后,弹个蒙层,显示个 loading,等请求数据返回了将蒙层隐藏掉
  2. 在按钮点击发起请求后,将按钮禁用掉,同样等数据返回了将按钮禁用解除

以上两种方案优点仅仅是简单,但是每个需要处理的页面都要单独写一串重复的代码,哪怕利用 mixin 也要多不少冗余代码。利用指令的方式仅仅需要在合适的地方加上个一条 v-xxxx,其他都在指令的逻辑内统一处理。

let forbidClick = null
export default {
  bind(e) {
    const el = e
    let timer = null
    forbidClick = () => {
      el.disabled = true
      el.classList.add('is-disabled')
      timer = setInterval(() => {
        if (window.currentRes.done) {
          clearInterval(timer)
          el.disabled = false
          el.classList.remove('is-disabled')
        }
      }, 500)
    }
    el.addEventListener('click', forbidClick)
  },
  unbind() {
    document.removeEventListener('click', forbidClick)
  },
}
// 再考虑请求,记录当前请求是否完成。请求拦截器中,接口请求时长超过3s,则视为完成,不管请求结果成功或失败

这样就实现了只要在按钮上加上了 v-clickForbidden。按钮点击后就会被禁用,仅当某个请求返回数据或者 3s 后将按钮的禁用解除

9. 实现一个页面操作不会整页刷新的网站

实现一个页面操作不会整页刷新的网站,并且能在浏览器的前进,后退时正确响应。给出你的技术实现方案?

  • 方式一 用 cookie 或者 localStorage 来记录应用的状态即可,刷新页面时读取一下这个状态,然后发送相应 ajax 请求来改变页面即可(注意:ajax 可以无刷新改变页面内容,但无法改变页面 URL;ajax 的使用对搜索引擎很不友好)

  • 方式二 HTML5 里引用了新的 API,就是 history.pushState 和 history.replaceState,就是通过这个接口做到无刷新改变页面 URL 的

    通过 pushState 和 replaceState 接口操作浏览器历史,并且改变当前页面的 URL。pushState 是将指定的 URL 添加到浏览器历史里,replaceState 是将指定的 URL 替换当前的 URL。(注意:通过 onpopstate 事件来响应浏览器的前进和后退的操作)

  • 方式三 内容发生改变后,改变 URL 的 hash。(注意:hash 的方式不能很好的处理浏览器的前进、后退等问题,通过 onhashchange 事件监听 hash 的改变)

总结:结合 ajax 和 pushState 完美的进行无刷新浏览了

扩展

  1. window.history.back() -- 相当于用户在浏览器的工具栏上点击返回按钮

  2. window.history.forward() -- 相当于用户在浏览器的工具栏上点击前进按钮

  3. go()方法,指定一个相对于当前页面位置的数值,从当前会话的历史记录中加载页面(当前位置页面索引:0,上一页:-1,下一页:1) window.history.go(-2)--后退 2 页,相当于调用两次 back();

    window.history.go(1)--前进 1 页,相当于调用 forward();

  4. 通过 window.history.length,得到历史记录栈中一共有多少页

  5. pushState(stateObject,title,URL)。state: 对象,存放一些数据表示当前状态。当浏览器执行前进后退操作时触发 onpopstate 事件

window.onpopstate = function(event) {
  console.log(event.state) // 当前历史记录条目的状态
}

window.addEventListener('popstate', function(event) {
  // 做一些操作
})

10、如何优化单页面首屏加载白屏体验或加重速度慢问题 ?

  1. 将公用的 JS 库通过 script 标签外部引入,减小 app.bundle 的大小,让浏览器并行下载资源文件,提高下载速度
  2. 在配置 路由时,页面和组件使用懒加载的方式引入,进一步缩小 app.bundle 的体积,在调用某个组件时再加载对应的 js 文件
  3. root 中插入 loading 或者 骨架屏 prerender-spa-plugin,提升用户体验(saas项目中添加 loading 来防止白屏
  4. 如果在 webview 中的页面,可以进行页面预加载
  5. 独立打包异步组件公共 Bundle,以提高复用性&缓存命中率
  6. 静态文件本地缓存,有两种方式分别为 HTTP 缓存,设置 Cache-Control,Last-Modified,Etag 等响应头和 Service Worker 离线缓存
  7. 配合 PWA 使用
  8. SSR
  9. 使用 Tree Shaking 减少业务代码体积
  • 懒加载和预加载的区别?
    1. 预加载是指在页面加载完成之前,提前将所需资源下载,之后使用的时候从缓存中调用;懒加载是延迟加载,按照一定的条件或者需求等到满足条件的时候再加载对应的资源
    2. 预加载增加了服务器压力,换来的是用户体验的提升,典型例子是在一个图片较多的网页中,如果使用了预加载就可以避免网页加载出来是时,图片的位置一片空白(图片可能还没加载出来),造成不好的用户体验;懒加载的作用减少不要的请求,缓解了服务器压力

11. 如何设计一个 Vue 组件?

12. 如何开发一个开源组件库?

13. 如何统一对错误进行捕获的?

14. vue 的异步错误如何捕获?

15. 如如何用鉴权控制登陆时间设计保持七天登录状态?

16. 为什么 B 站的弹幕可以不挡人物?

利用 CSS 中新增的-webkit-mask-image 属性