每日一题第三篇

Day103:多个 tab 只对应一个内容框,点击每个 tab 都会请求接口并渲染到内容框,怎么确保频繁点击 tab 但能够确保数据正常显示?
null

一、分析

因为每个请求处理时长不一致,可能会导致先发送的请求后响应,即请求响应顺序和请求发送顺序不一致,从而导致数据显示不正确。

即可以理解为连续触发多个请求,如何保证请求响应顺序和请求发送顺序一致。对于问题所在场景,用户只关心最后数据是否显示正确,即可以简化为:连续触发多个请求,如何保证最后响应的结果是最后发送的请求(不关注之前的请求是否发送或者响应成功)

类似场景:input输入框即时搜索,表格快速切换页码

二、解决方案

防抖(过滤掉一些非必要的请求) + 取消上次未完成的请求(保证最后一次请求的响应顺序)

取消请求方法:

  • XMLHttpRequest 使用 abort api 取消请求
  • axios 使用 cancel token 取消请求

伪代码(以 setTimeout 模拟请求,clearTimeout 取消请求)

/**
 * 函数防抖,一定时间内连续触发事件只执行一次
 * @param {*} func 需要防抖的函数
 * @param {*} delay 防抖延迟
 * @param {*} immediate 是否立即执行,为true表示连续触发时立即执行,即执行第一次,为false表示连续触发后delay ms后执行一次
 */
let debounce = function(func, delay = 100, immediate = false) {
  let timeoutId, last, context, args, result

  function later() {
    const interval = Date.now() - last
    if (interval < delay && interval >= 0) {
      timeoutId = setTimeout(later, delay - interval)
    } else {
      timeoutId = null
      if (!immediate) {
        result = func.apply(context, args)
        context = args = null
      }
    }
  }

  return function() {
    context = this
    args = arguments
    last = Date.now()

    if (immediate && !timeoutId) {
      result = func.apply(context, args)
      context = args = null // 解除引用
    }
    
    if (!timeoutId) {
      timeoutId = setTimeout(later, delay)
    }

    return result
  }
}


let flag = false   // 标志位,表示当前是否正在请求数据
let xhr = null

let request = (i) => {
    if (flag) {
        clearTimeout(xhr)
        console.log(`取消第${i - 1}次请求`)
    }
    flag = true
    console.log(`开始第${i}次请求`)
    xhr = setTimeout(() => {
        console.log(`请求${i}响应成功`)
        flag = false
    }, Math.random() * 200)
}

let fetchData = debounce(request, 50)  // 防抖

// 模拟连续触发的请求
let count = 1 
let getData = () => {
  setTimeout(() => {
    fetchData(count)
    count++
    if (count < 11) {
        getData()
    }
  }, Math.random() * 200)
}
getData()

/* 某次测试输出:
    开始第2次请求
    请求2响应成功
    开始第3次请求
    取消第3次请求
    开始第4次请求
    请求4响应成功
    开始第5次请求
    请求5响应成功
    开始第8次请求
    取消第8次请求
    开始第9次请求
    请求9响应成功
    开始第10次请求
    请求10响应成功
*/

Day104:项目中如何进行异常捕获
null

一、代码执行的错误捕获

1.try……catch

  • 能捕获到代码执行的错误
  • 捕获不到语法的错误
  • 无法处理异步中的错误
  • 使用try... catch 包裹,影响代码可读性

2.window.onerror

  • 无论是异步还是非异步错误,onerror 都能捕获到运行时错误
  • onerror 主要是来捕获预料之外的错误,而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。
  • window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx
  • 当我们遇到 报 404 网络请求异常的时候,onerror 是无法帮助我们捕获到异常的。

缺点: 监听不到资源加载的报错onerror,事件处理函数只能声明一次,不会重复执行多个回调:

3.window.addEventListener('error')

可以监听到资源加载报错,也可以注册多个事件处理函数。

window.addEventListener('error',(msg, url, row, col, error) => {}, true)

但是这种方式虽然可以捕捉到网络请求的异常,却无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

4.window.addEventListener('unhandledrejection')

捕获Promise错误,当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;这可能发生在 window 下,但也可能发生在 Worker 中。 这对于调试回退错误处理非常有用。

二、资源加载的错误捕获

  1. imgObj.onerror()
  2. performance.getEntries(),获取到成功加载的资源,对比可以间接的捕获错误
  3. window.addEventListener('error', fn, true), 会捕获但是不冒泡,所以window.onerror 不会触发,捕获阶段可以触发

三、Vue、React中

Vue有 errorHandler,React有 componentDidCatch 进行错误捕获

Day105:JavaScript 中如何模拟实现方法的重载,动手实现下
null

一、背景知识

JavaScript不支持重载的语法,它没有重载所需要的函数签名。
ECMAScript函数不能像传统意义上那样实现重载。而在其他语言(如 Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。如前所述,ECMAScirpt函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。 — JavaScript高级程序设计(第3版)

二、什么是函数重载

重载函数是函数的一种特殊情况,为方便使用,允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。这就是重载函数

三、模拟实现

利用闭包特性

addMethod函数接收3个参数:目标对象、目标方法名、函数体,当函数被调用时:

  1. 先将目标object[name]的值存入变量old中,因此起初old中的值可能不是一个函数;
  2. 接着向object[name]赋值一个代理函数,并且由于变量old、fnt在代理函数中被引用,所以old、fnt将常驻内存不被回收。
function addMethod(object, name, fnt) {
  var old = object[name];  // 保存前一个值,以便后续调用
  object[name] = function(){  // 向object[name]赋值一个代理函数
    // 判断fnt期望接收的参数与传入参数个数是否一致
    if (fnt.length === arguments.length)
      // 若是,则调用fnt
      return fnt.apply(this, arguments)
    else if (typeof old === 'function')  // 若否,则判断old的值是否为函数
      // 若是,则调用old
      return old.apply(this, arguments);
  };
}
//模拟重载add
var methods = {};
//添加方法,顺序无关
addMethod(methods, 'add', function(){return 0});
addMethod(methods, 'add', function(a,b){return a + b});
addMethod(methods, 'add', function(a,b,c){return a + b + c});
//执行
console.log(methods.add()); //0
console.log(methods.add(10,20)); //30
console.log(methods.add(10,20,30)); //60

Day106:Webpack 里面的插件是怎么实现的?
null

实现分析

  • webpack本质是一种事件流机制, 核心模块: tapable (Sync + Async)Hooks 构造出=> Compiler(编译) + Compilation(创建bundles)
  • compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
  • 创建一个插件函数, 在其prototype上定义apply方法; 指定一个绑定到webpack自身的事件钩子;
  • 函数内,处理webpack内部实例的特定数据
  • 处理完成后, 调用webpack提供的回调

代码示例

function MyExampleWebpackPlugin() {

};
// 在插件函数的 prototype 上定义一个 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  // 指定一个挂载到 webpack 自身的事件钩子。
  compiler.plugin('webpacksEventHook', function(compilation /* 处理 webpack 内部实例的特定数据。*/, callback) {
    console.log("This is an example plugin!!!");
    // 功能完成后调用 webpack 提供的回调。
    callback();
  });
};

Day107:对虚拟 DOM 的理解?虚拟 DOM 主要做了什么?虚拟 DOM 本身是什么?
null

一、什么是虚拟Dom

从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能

虚拟dom是对DOM的抽象,这个对象是更加轻量级的对DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟dom, 因为虚拟dom本身是js对象。

在代码渲染到页面之前,vue或者react会把代码转换成一个对象(虚拟DOM)。以对象的形式来描述真实dom结构,最终渲染到页面。在每次数据发生变化前,虚拟dom都会缓存一份,变化之时,现在的虚拟dom会与缓存的虚拟dom进行比较。

在vue或者react内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。

另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。

二、为什么要用 Virtual DOM

1.保证性能下限,在不进行手动优化的情况下,提供过得去的性能

看一下页面渲染的一个流程:

  • 解析HTNL ☞ 生成DOM? ☞ 生成 CSSOM ☞ Layout ☞ Paint ☞ Compiler

下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗:

  • 真实DOM: 生成HTML字符串 + 重建所有的DOM元素
  • Virtual DOM: 生成vNode + DOMDiff + 必要的dom更新

Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道: 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。

2.跨平台

Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。

三、Virtual DOM真的比真实DOM性能好吗

  1. 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
  2. 正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。

Day108:Webpack 为什么慢,如何进行优化
null

一、webpack 为什么慢

webpack是所谓的模块捆绑器,内部有循环引用来分析模块间之间的依赖,把文件解析成AST,通过一系类不同loader的加工,最后全部打包到一个js文件里。

webpack4以前在打包速度上没有做过多的优化手段,编译慢的大部分时间是花费在不同loader编译过程,webpack4以后,吸收借鉴了很多优秀工具的思路,

如支持0配置,多线程等功能,速度也大幅提升,但依然有一些优化手段。如合理的代码拆分,公共代码的提取,css资源的抽离

二、优化 Webpack 的构建速度

  • 使用高版本的 Webpack (使用webpack4)
  • 多线程/多实例构建:HappyPack(不维护了)、thread-loader
  • 缩小打包作用域:
    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    • IgnorePlugin (完全排除模块)
    • 合理使用alias
  • 充分利用缓存提升二次构建速度:
    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin
      注意:thread-loader 和 cache-loader 兩個要一起使用的話,請先放 cache-loader 接著是 thread-loader 最後才是 heavy-loader
  • DLL
    • 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。

三、使用Webpack4带来的优化

  • V8带来的优化(for of替代forEach、Map和Set替代Object、includes替代indexOf)
  • 默认使用更快的md4 hash算法
  • webpack AST可以直接从loader传递给AST,减少解析时间
  • 使用字符串方法替代正则表达式

来看下具体使用

1.noParse

  • 不去解析某个库内部的依赖关系
  • 比如jquery 这个库是独立的, 则不去解析这个库内部依赖的其他的东西
  • 在独立库的时候可以使用
module.exports = {
  module: {
    noParse: /jquery/,
    rules:[]
  }
}

2.IgnorePlugin

  • 忽略掉某些内容 不去解析依赖库内部引用的某些内容
  • 从moment中引用 ./local 则忽略掉
  • 如果要用local的话 则必须在项目中必须手动引入 import 'moment/locale/zh-cn'
module.exports = {
  plugins: [
    new Webpack.IgnorePlugin(/\.\/local/, /moment/),
  ]
}

3.dillPlugin

  • 不会多次打包, 优化打包时间
  • 先把依赖的不变的库打包
  • 生成 manifest.json文件
  • 然后在webpack.config中引入
  • webpack.DllPluginWebpack.DllReferencePlugin

4.happypack -> thread-loader

  • 大项目的时候开启多线程打包
  • 影响前端发布速度的有两个方面,一个是 构建 ,一个就是 压缩 ,把这两个东西优化起来,可以减少很多发布的时间。

5.thread-loader

thread-loader 会将您的 loader 放置在一个 worker 池里面运行,以达到多线程构建。

把这个 loader 放置在其他 loader 之前,放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          // 你的高开销的loader放置在此 (e.g babel-loader)
        ]
      }
    ]
  }
}

每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。请在高开销的loader中使用,否则效果不佳

6.压缩加速——开启多线程压缩

不推荐使用 webpack-paralle-uglify-plugin,项目基本处于没人维护的阶段,issue 没人处理,pr没人合并。

Webpack 4.0以前:uglifyjs-webpack-plugin,parallel参数

module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        parallel: true,
      }),
    ],
  },};

推荐使用 terser-webpack-plugin

module.exports = {
  optimization: {
    minimizer: [new TerserPlugin(
      parallel: true   // 多线程
    )],
  },
};

Day109:动画性能如何检测
null

1.Chrome 提供给开发者的功能十分强大,在开发者工具中,我们进行如下选择调出 FPS meter 选项:

image

通过这个选项,可以开启页面实时 Frame Rate (帧率) 观测及页面 GPU 使用率。但是缺点太多了,这个只能一次观测一到几个页面,而且需要人工实时观测。数据只能是主观感受,并没有一个十分精确的数据不断上报或者被收集

2.借助 Frame Timing API

Frame Timing API 是 Web Performance Timing API 标准中的其中一位成员。是通过一个接口获取帧相关的性能数据,例如每秒帧数和TTF.

以 Navigation Timing, Performance Timeline, Resource Timing 为例子,对于兼容它的浏览器,它以只读属性的形式对外暴露挂载在 window.performance 上。

其中Timing中的属性对应时间点如下:

image

通过window.performance.timing,就可以统计出页面每个重要节点的耗时

借助 Web Performance Timing API 中的 Frame Timing API,可以轻松的拿到每一帧中,主线程以及合成线程的时间。或者更加容易,直接拿到每一帧的耗时。

获取 Render 主线程和合成线程的记录,每条记录包含的信息基本如下,代码示意,(参考至Developer feedback needed: Frame Timing API):

var rendererEvents = window.performance.getEntriesByType("renderer");
var compositeThreadEvents = window.performance.getEntriesByType("composite");

//或者
var observer = new PerformanceObserver(function(list) {
    var perfEntries = list.getEntries();
    for (var i = 0; i < perfEntries.length; i++) {
        console.log("frame: ", perfEntries[i]);
    }
});
    
// subscribe to Frame Timing
observer.observe({entryTypes: ['frame']});

// 结果
// {
//  sourceFrameNumber: 120,
//  startTime: 1342.549374253
//  cpuTime: 6.454313323
// }

//每个记录都包括唯一的 Frame Number、Frame 开始时间以及 cpuTime 时间。通过计算每一条记录的 startTime ,我们就可以算出每两帧间的间隔,从而得到动画的帧率是否能够达到 60 FPS。

但是。(重点来了) 现在 Frame Timing API 的兼容性不太友好,还没有任何浏览器支持,属于宏观试验性阶段,抬走下一个

3.requestAnimationFrame API

requestAnimationFrame 告诉浏览器您希望执行动画并请求浏览器调用指定的函数在下一次重绘之前更新动画。

当准备好更新屏幕画面时你就应用此方法。这会要求动画函数在浏览器下次重绘前执行。回调的次数常是每秒 60 次,大多数浏览器通常匹配 W3C 所建议的刷新率。

使用 requestAnimationFrame 计算 FPS 原理

原理是,正常而言 requestAnimationFrame 这个方法在一秒内会执行 60 次,也就是不掉帧的情况下。假设动画在时间 A 开始执行,在时间 B 结束,耗时 x ms。而中间 requestAnimationFrame 一共执行了 n 次,则此段动画的帧率大致为:n / (B - A)

代码如下,能近似计算每秒页面帧率,以及我们额外记录一个 allFrameCount,用于记录 rAF 的执行次数,用于计算每次动画的帧率 :

var rAF = function () {
    return (
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 1000 / 60);
        }
    );
}();

var frame = 0;
var allFrameCount = 0;
var lastTime = Date.now();
var lastFameTime = Date.now();

var loop = function () {
    var now = Date.now();
    var fs = (now - lastFameTime);
    var fps = Math.round(1000 / fs);

    lastFameTime = now;
    // 不置 0,在动画的开头及结尾记录此值的差值算出 FPS
    allFrameCount++;
    frame++;

    if (now > 1000 + lastTime) {
        var fps = Math.round((frame * 1000) / (now - lastTime));
        console.log(`${new Date()} 1S内 FPS:`, fps);
        frame = 0;
        lastTime = now;
    };

    rAF(loop);
}

loop();

在大部分情况下,这种方法可以很好的得出 Web 动画的帧率。

如果需要统计某个特定动画过程的帧率,只需要在动画开始和结尾两处分别记录 allFrameCount 这个数值大小,再除以中间消耗的时间,也可以得出特定动画过程的 FPS 值。

这个方法计算的结果和真实的帧率是存在误差的,因为它是将每两次主线程执行 javascript 的时间间隔当成一帧,而非上面说的主线程加合成线程所消耗的时间为一帧。但是对于现阶段而言,算是一种可取的方法。

Day110:客户端缓存有几种方式?浏览器出现 from disk、from memory 的策略是啥
null

一、客户端缓存

浏览器缓存策略:

浏览器每次发起请求时,先在本地缓存中查找结果以及缓存标识,根据缓存标识来判断是否使用本地缓存。如果缓存有效,则使用本地缓存;否则,则向服务器发起请求并携带缓存标识。根据是否需向服务器发起HTTP请求,将缓存过程划分为两个部分:强制缓存和协商缓存,强缓优先于协商缓存

HTTP缓存都是从第二次请求开始的

  • 第一次请求资源时,服务器返回资源,并在response header中回传资源的缓存策略;
  • 第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。这是缓存运作的一个整体流程图:
img

1.强缓存

服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。

强缓存命中则直接读取浏览器本地的资源,在network中显示的是from memory或者from disk

控制强制缓存的字段有:Cache-Control(http1.1)和Expires(http1.0)

  • Cache-control是一个相对时间,用以表达自上次请求正确的资源之后的多少秒的时间段内缓存有效。
  • Expires是一个绝对时间。用以表达在这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求
  • Cache-Control的优先级比Expires的优先级高。前者的出现是为了解决Expires在浏览器时间被手动更改导致缓存判断错误的问题。
  • 如果同时存在则使用Cache-control。

1)强缓存-expires

该字段是服务器响应消息头字段,告诉浏览器在过期时间之前可以直接从浏览器缓存中存取数据。

Expires 是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间)。在响应消息头中,设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求。

由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。

优势特点:

  • HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。
  • 以时刻标识失效时间。

劣势问题:

  • 时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。
  • 存在版本问题,到期之前的修改客户端是不可知的。

2)强缓存-cache-control

已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求。

这两者的区别就是前者是绝对时间,而后者是相对时间。下面列举一些 Cache-control 字段常用的值:(完整的列表可以查看MDN)

  • max-age:即最大有效时间。
  • must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
  • no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜。
  • no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
  • public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
  • private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。

Cache-control 的优先级高于 Expires,为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段都可以设置。

该字段可以在请求头或者响应头设置,可组合使用多种指令:

  • 可缓存性
    • public:default,浏览器和缓存服务器都可以缓存页面信息
    • private:代理服务器不可缓存,只能被单个用户缓存
    • no-cache:浏览器器和服务器都不应该缓存页面信息,但仍可缓存,只是在缓存前需要向服务器确认资源是否被更改。可配合private,
      过期时间设置为过去时间。
    • only-if-cache:客户端只接受已缓存的响应
  • 到期
    • max-age=:缓存存储的最大周期,超过这个周期被认为过期。
    • s-maxage=:设置共享缓存,比如can。会覆盖max-age和expires。
    • max-stale[=]:客户端愿意接收一个已经过期的资源
    • min-fresh=:客户端希望在指定的时间内获取最新的响应
    • stale-while-revalidate=:客户端愿意接收陈旧的响应,并且在后台一部检查新的响应。时间代表客户端愿意接收陈旧响应
      的时间长度。
    • stale-if-error=:如新的检测失败,客户端则愿意接收陈旧的响应,时间代表等待时间。
  • 重新验证和重新加载
    • must-revalidate:如页面过期,则去服务器进行获取。
    • proxy-revalidate:用于共享缓存。
    • immutable:响应正文不随时间改变。
  • 其他
    • no-store:绝对禁止缓存
    • no-transform:不得对资源进行转换和转变。例如,不得对图像格式进行转换。

优势特点:

  • HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。
  • 比Expires多了很多选项设置。

劣势问题:

  • 存在版本问题,到期之前的修改客户端是不可知的。

2.协商缓存

让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的Etag和Last-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。

  • 协商缓存的状态码由服务器决策返回200或者304
  • 当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。
  • 对比缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。
  • 协商缓存有 2 组字段(不是两个),控制协商缓存的字段有:Last-Modified/If-Modified-since(http1.0)和Etag/If-None-match(http1.1)
  • Last-Modified/If-Modified-since表示的是服务器的资源最后一次修改的时间;Etag/If-None-match表示的是服务器资源的唯一标识,只要资源变化,Etag就会重新生成。
  • Etag/If-None-match的优先级比Last-Modified/If-Modified-since高。

1)协商缓存-协商缓存-Last-Modified/If-Modified-since

  1. 服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如 Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
  2. 浏览器将这个值和内容一起记录在缓存数据库中。
  3. 下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段
  4. 服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。

优势特点:

  • 不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。

劣势问题:

  1. 只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。
  2. 以时刻作为标识,无法识别一秒内进行多次修改的情况。 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
  3. 某些服务器不能精确的得到文件的最后修改时间。
  4. 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。

2)协商缓存-Etag/If-None-match

  • 为了解决上述问题,出现了一组新的字段 EtagIf-None-Match
  • Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。
  • 浏览器在发起请求时,服务器返回在Response header中返回请求资源的唯一标识。在下一次请求时,会将上一次返回的Etag值赋值给If-No-Matched并添加在Request Header中。服务器将浏览器传来的if-no-matched跟自己的本地的资源的ETag做对比,如果匹配,则返回304通知浏览器读取本地缓存,否则返回200和更新后的资源。
  • Etag 的优先级高于 Last-Modified

优势特点:

  • 可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。
  • 不存在版本问题,每次请求都回去服务器进行校验。

劣势问题:

  • 计算ETag值需要性能损耗。
  • 分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时现ETag不匹配的情况。

二、浏览器出现 from disk、from memory 的策略

强缓存:服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行其他缓存策略

  1. 浏览器发现缓存无数据,于是发送请求,向服务器获取资源
  2. 服务器响应请求,返回资源,同时标记资源的有效期Cache-Contrl: max-age=3000
  3. 浏览器缓存资源,等待下次重用

Day112:数组里面有 10 万个数据,取第一个元素和第 10 万个元素的时间相差多少
null

解析

数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复杂度都是 O(1)

JavaScript 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。

看一下chrome控制台下的结果

var arr = new Array(100000).fill(null)
console.time('arr1')
arr[0]
console.timeEnd('arr1')
// arr1: 0.003173828125ms
var arr = new Array(100000).fill(null)
console.time('arr100000')
arr[99999]
console.timeEnd('arr100000')
// arr100000: 0.002685546875ms

Day113:Import 和 CommonJS 在 webpack 打包过程中有什么不同
null

1.es6模块调用commonjs模块

可以直接使用commonjs模块,commonjs模块将不会被webpack的模块系统编译而是会原样输出,并且commonjs模块没有default属性

2.es6模块调用es6模块

被调用的es6模块不会添加{__esModule:true},只有调用者才会添加{__esModule:true},并且可以进行tree-shaking操作,如果被调用的es6模块只是import进来,但是并没有被用到,那么被调用的es6模块将会被标记为/* unused harmony default export */,在压缩时此模块将会被删除(例外:如果被调用的es6模块里有立即执行语句,那么这些语句将会被保留)

3.commonjs模块引用es6模块

es6模块编译后会添加{__esModule:true}。如果被调用的es6模块中恰好有export default语句,那么编译后的es6模块将会添加default属性。

4.commonjs模块调用commonjs模块

commonjs模块会原样输出

Day114:说一下Webpack 热更新的原理
null

一、基础概念

  1. Webpack Compiler: 将 JS 编译成 Bundle
  2. Bundle Server: 提供文件在浏览器的访问,实际上就是一个服务器
  3. HMR Server: 将热更新的文件输出给HMR Runtime
  4. HMR Runtime: 会被注入到bundle.js中,与HRM Server通过WebSocket链接,接收文件变化,并更新对应文件
  5. bundle.js: 构建输出的文件

二、原理

1.启动阶段

  1. Webpack Compiler 将对应文件打包成bundle.js(包含注入的HMR Server),发送给Bundler Server
  2. 浏览器即可以访问服务器的方式获取bundle.js

2.更新阶段(即文件发生了变化)

  1. Webpack Compiler 重新编译,发送给HMR Server
  2. HMR Server 可以知道有哪些资源、哪些模块发生了变化,通知HRM Runtime
  3. HRM Runtime更新代码

三、HMR原理详解

hmr

使用webpack-dev-server去启动本地服务,内部实现主要使用了webpack、express、websocket。

  • 使用express启动本地服务,当浏览器访问资源时对此做响应。
  • 服务端和客户端使用websocket实现长连接
  • webpack监听源文件的变化,即当开发者保存文件时触发webpack的重新编译。
    • 每次编译都会生成hash值、已改动模块的json文件、已改动模块代码的js文件
    • 编译完成后通过socket向客户端推送当前编译的hash戳
  • 客户端的websocket监听到有文件改动推送过来的hash戳,会和上一次对比
    • 一致则走缓存
    • 不一致则通过ajax和jsonp向服务端获取最新资源
  • 使用内存文件系统去替换有修改的内容实现局部刷新

1.server端

  • 启动webpack-dev-server服务器
  • 创建webpack实例
  • 创建Server服务器
  • 添加webpack的done事件回调
  • 编译完成向客户端发送消息
  • 创建express应用app
  • 设置文件系统为内存文件系统
  • 添加webpack-dev-middleware中间件
  • 中间件负责返回生成的文件
  • 启动webpack编译
  • 创建http服务器并启动服务
  • 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
  • 创建socket服务器

2.client端

  • webpack-dev-server/client端会监听到此hash消息
  • 客户端收到ok的消息后会执行reloadApp方法进行更新
  • 在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器
  • 在webpack/hot/dev-server.js会监听webpackHotUpdate事件
  • 在check方法里会调用module.hot.check方法
  • HotModuleReplacement.runtime请求Manifest
  • 它通过调用 JsonpMainTemplate.runtime的hotDownloadManifest方法
  • 调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码
  • 补丁JS取回来后会调用JsonpMainTemplate.runtime.js的webpackHotUpdate方法
  • 然后会调用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法动态更新模块代码
  • 然后调用hotApply方法进行热更新

Day115:说一下 vue-router 的原理
null

实现原理

vue-router的原理就是更新视图而不重新请求页面。

vue-router可以通过mode参数设置为三种模式:hash模式、history模式abstract模式

1.hash模式

默认是hash模式,基于浏览器history api,使用 window.addEventListener("hashchange",callback,false) 对浏览器地址进行监听。当调用push时,把新路由添加到浏览器访问历史的栈顶。使用replace时,把浏览器访问历史的栈顶路由替换成新路由。

hash值等于url中#及其以后的内容。浏览器是根据hash值的变化,将页面加载到相应的DOM位置。锚点变化只是浏览器的行为,每次锚点变化后依然会在浏览器中留下一条历史记录,可以通过浏览器的后退按钮回到上一个位置。

2.History

history模式,基于浏览器history api,使用 window.onpopstate 对浏览器地址进行监听。对浏览器history api中pushState()replaceState() 进行封装,
当方法调用,会对浏览器历史栈进行修改。从而实现URL的跳转而无需重新加载页面。

但是它的问题在于当刷新页面的时候会走后端路由,所以需要服务端的辅助来兜底,避免URL无法匹配到资源时能返回页面。

3.abstract

不涉及和浏览器地址的相关记录。流程跟hash模式一样,通过数组维护模拟浏览器的历史记录栈。

服务端下使用。使用一个不依赖于浏览器的浏览历史虚拟管理后台。

4.总结

hash模式和history模式都是通过 window.addEventListenter() 方法监听 hashchangepopState 进行相应路由的操作。可以通过back、foward、go等方法访问浏览器的历史记录栈,进行各种跳转。而abstract模式是自己维护一个模拟的浏览器历史记录栈的数组。

Day116:商城的列表页跳转到商品的详情页,详情页数据接口很慢,前端可以怎么优化用户体验?
null

一、优化简要版

1)懒加载:获取首屏数据,后边的数据进行滑动加载请求

  1. 首先,不要将图片地址放到src属性中,而是放到其它属性(data-original)中。
  2. 页面加载完成后,根据scrollTop判断图片是否在用户的视野内,如果在,则将data-original属性中的值取出存放到src属性中。
  3. 在滚动事件中重复判断图片是否进入视野,如果进入,则将data-original属性中的值取出存放到src属性中

2)利用骨架屏提升用户体验

3)PreloadJS预加载

使用PreloadJS库,PreloadJS提供了一种预加载内容的一致方式,以便在HTML应用程序中使用。预加载可以使用HTML标签以及XHR来完成。默认情况下,PreloadJS会尝试使用XHR加载内容,因为它提供了对进度和完成事件的更好支持,但是由于跨域问题,使用基于标记的加载可能更好。

4)除了添加前端loading和超时404页面外,接口部分可以添加接口缓存和接口的预加载

  1. 使用workbox对数据进行缓存 缓存优先
  2. 使用orm对本地离线数据进行缓存 优先请求本地。
  3. 采用预加载 再进入到详情页阶段使用quicklink预加载详情页
  4. 使用nodejs作为中间层将详情页数据缓存至redis等
    上面的方法,可以根据业务需求选择组合使用。

二、优化详细版

1.打开谷歌搜索为例

load和DOMContentLoad.png
  • 蓝色的分界线左边代表浏览器的 DOMContentLoaded,当初始 HTML 文档已完全加载和解析而无需等待样式表,图像和子帧完成加载时的标识;
  • 红色分界线代表 load, 当整个页面及所有依赖资源如样式表和图片都已完成加载时

所以我们可以大致分为在

  • TTFB 之前的优化
  • 浏览器上面渲染的优化

2.当网络过慢时在获取数据前的处理

首先先上一张经典到不能再经典的图

timing-overview.png

其中cnd在dns阶段, dom渲染在processing onload阶段

上图从 promot for unload 到 onload 的过程这么多步骤, 在用户体验来说, 一个页面从加载到展示超过 4 秒, 就会有一种非常直观的卡顿现象, 其中 load 对应的位置是 onLoad 事件结束后, 才开始构建 dom 树, 但是用户不一定是关心当前页面是否是完成了资源的下载;
往往是一个页面开始出现可见元素开始FCP 首次内容绘制或者是FC 首次绘制 此时用户视觉体验开始, 到TTI(可交互时间) , 可交互元素的出现, 意味着,用户交互体验开始, 这时候用户就可以愉快的浏览使用我们的页面啦;

所以这个问题的主要痛点是需要缩短到达 TTIFCP 的时间

但是这里已知进入我们详情页面时, 接口数据返回速度是很慢的, FCPFC , 以及加快到达 TTI , 就需要我们页面预处理了

3.页面数据缓存处理(缓存大法好)

第一次 进入详情页面, 可以使用骨架图进行模拟 FC 展示, 并且骨架图, 可使用背景图且行内样式的方式对首次进入详情页面进行展示, 对于请求过慢的详情接口使用 worker 进程, 对详情的接口请求丢到另外一个工作线程进行请求, 页面渲染其他已返回数据的元素; 当很慢的数据回来后, 需要对页面根据商品 id 签名为 key 进行 webp 或者是缩略图商品图的 cnd 路径 localStorage 的缓存, 商品 id 的签名由放在 cookie 并设置成 httpOnly

非第一次 进入详情页时, 前端可通过特定的接口请求回来对应的商品 id 签名的 cookieid, 读取 localStorage 的商品图片的缓存数据, 这样对于第一次骨架图的展示时间就可以缩短, 快速到达 TTI 与用户交互的时间, 再通过 worker 数据, 进行高清图片的切换

4.过期缓存数据的处理(后端控制为主, LRU 为辅)

对于缓存图片地址的处理, 虽说缓存图片是放在 localStorage 中, 不会用大小限制, 但是太多也是不好的, 这里使用 LRU 算法对图片以及其他 localStorage 进行清除处理, 对于超过 7 天的数据进行清理
localStorage 详情页的数据, 数据结构如下:

"读取后端的cookieID": {
  "path": "对应cdn图片的地址",
  "time": "缓存时间戳",
  "size": "大小"
}

5.数据缓存和过期缓存数据的处理主体流程

进入商品详情页,接口数据很慢时,对页面的优化

6.对于大请求量的请求(如详情页面中的猜你喜欢, 推荐商品等一些大数据量的静态资源)

  1. 由于这些不属于用户进入详情想第一时间获取的信息, 即不属于当前页面的目标主体, 所以这些可以使用 Intersection Observer API 进行主体元素的观察, 当当前主体元素被加载出来后, 在进行非主体元素的网络资源分配, 即网络空闲时再请求猜你喜欢, 推荐商品等资源, 处理请求优先级的问题
  2. 需要保证当前详情页的请求列表的请求数 不超过当前浏览器的请求一个 tcp 最大 http 请求数

7.当 worker 数据回来后, 出现 大量图片 替换对应元素的的 webp 或者缩略图出现的问题(静态资源过多)

这里有两种情景

  1. 移动端, 对于移动端, 一般不会出现大量图片, 一般一个商品详情页, 不会超过 100 张图片资源; 这时候, 选择懒加载方案; 根据 GitHub 现有的很多方案, 当前滑动到可被观察的元素后才加载当前可视区域的图片资源, 同样使用的是 Intersection Observer API ; 比如 vue 的一个库 vue-lazy , 这个库就是对 Intersection_Observer_API 进行封装, 对可视区域的 img 便签进行 data-src 和 src 属性替换

  2. 第二个情况, pc 端, 可能会出现大量的 img 标签, 可能多达 300~400 张, 这时候, 使用懒加载, 用户体验就不太好了; 比如说: 当用户在查看商品说明介绍时, 这些商品说明和介绍有可能只是一张张图片, 当用户很快速的滑动时, 页面还没懒加载完, 用户就有可能看不到想看的信息; 鉴于会出现这种情况, 这里给出一个方案就是, img 出现一张 load 一张; 实现如下:

// 这里针对非第一次进入详情页,
//当前localStorage已经有了当前详情页商品图片的缩略图
for(let i = 0; i < worker.img.length; i++) {
  // nodeList是对应img标签,
  // 注意, 这里对应的nodeList一定要使用内联style把位置大小设置好, 避免大量的重绘重排
  const img = nodeList[i]
  img.src = worker.img['path'];
  img.onerror = () => {
    // 将替换失败或者加载失败的图片降级到缩略图, 
    // 即缓存到localStorage的缩略图或者webp图
    // 兼容客户端处理webp失败的情况
  }
}

8.页面重绘重排处理

页面渲染流程

触发重排的操作主要是几何因素:

  1. 页面首次进入的渲染。
  2. 浏览器 resize
  3. 元素位置和尺寸发生改变的时候
  4. 可见元素的增删
  5. 内容发生改变
  6. 字体的 font 的改变。
  7. css 伪类激活。
    .....

尽量减少上面这些产生重绘重排的操作

比如说:

这里产生很大的重绘重排主要发生在 worker 回来的数据替换页面中的图片 src 这一步

// 该节点为img标签的父节点
const imgParent = docucment.getElementById('imgParent'); 
// 克隆当前需要替换img标签的父元素下所有的标签
const newImgParent = imgParent.cloneNode(true); 
const imgParentParent = docucment.getElementById('imgParentParent');
for(let i = 0; i < newImgParent.children.length; i++) { 
// 批量获取完所有img标签后, 再进行重绘
  newImgParent.children[i].src = worker.img[i].path;
}
// 通过img父节点的父节点, 来替换整个img父节点
// 包括对应的所有子节点, 只进行一次重绘操作
imgParentParent.replaceChild(newImgParent, imgParent); 

9.css代码处理

注意被阻塞的css资源

众所周知, css的加载会阻塞浏览器其他资源的加载, 直至CSSOM CSS OBJECT MODEL 构建完成, 然后再挂在DOM树上, 浏览器依次使用渲染树来布局和绘制网页。

很多人都下意识的知道, 将css文件一律放到head标签中是比较好的, 但是为什么将css放在head标签是最后了呢?

我们用淘宝做例子

没有加载css的淘宝页面

比如这种没有css样式的页面称之为FOUC(内容样式短暂失效), 但是这种情况一般出现在ie系列以及前期的浏览器身上; 就是当cssom在domtree生成后, 依然还没完成加载出来, 先展示纯html代码的页面一会再出现正确的带css样式的页面;

减少不同页面的css代码加载

对于电商页面, 有些在头部的css代码有些是首页展示的有些是特定情况才展示的, 比如当我们需要减少一些css文件大小但是当前网站又需要多屏展示, 这时候, 很多人都会想到是媒体查询, 没错方向是对的, 但是怎样的媒体查询才对css文件保持足够的小呢, 可以使用link标签媒体查询,看下边的的例子:



第一个css资源表示所有页面都会加载, 第二个css资源, 宽度在750px才会加载, 默认media="all"

在一些需求写css媒体查询的网站, 不要在css代码里面写, 最好写两套css代码, 通过link媒体查询去动态加载, 这样就能很好的减轻网站加载css文件的压力

10.静态js代码处理

这种js代码, 是那些关于埋点, 本地日记, 以及动态修改css代码, 读取页面成型后的信息的一些js代码, 这种一律放在同域下的localStorage上面, 什么是同域下的localStorage

这里还是以天猫为例

image

11.容错处理

  1. 页面在获取到 worker 回来的数据后, 通过拷贝整个html片段, 再将worker的img路径在替换对应的 img 资源后再进行追加到对应的dom节点
  2. 缓存 css 文件和 js 文件到 localStorage 中, 若当前没有对应的 css 文件或者 js 文件, 或者被恶意修改过的 css 文件或者 js 文件(可使用签名进行判断), 删除再获取对应文件的更新

12.推荐方案理由

  1. 使用了 worker 线程请求详情数据, 不占用浏览器主线程; 进而减少主进程消耗在网络的时间
  2. 使用 localStorage 的缓存机制, 因为当 worker 回来的数据后, 读取 localStorage 是同步读取的, 基本不会有太大的等待时间, 并且读取 localStorage 时, 使用的是后端返回来的 cookieID 进行读取, 且本地的 cookID 是 httpOnly 避免了第三方获取到 cookieID 进行读取商品信息
  3. 使用 LRU 清除过多的缓存数据
  4. 首次进入页面时, 保证已知页面布局情况下的快速渲染以及配置骨架图, 加快到达 FCP 和 FP 的时间
  5. 就算 img 静态资源过大, 在第二次进入该页面的时候, 也可以做到低次数重绘重排, 加快到底 TTI 的时间

13.方案不足

  1. 在网络依然很慢的情况下, 首次进入详情页面, 如果长时间的骨架图和已知布局下, 用户的体验依然是不好的, 这里可以考虑 PWA 方案, 对最近一次成功请求的内容进行劫持, 并在无网情况下, 做出相应的提示和展示处理
  2. 需要 UI 那边提供三套静态 img 资源

Day118:说一下单点登录实现原理
null

一、什么是单点登录

单点登录SSO(Single Sign On),是一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录得到其他所有系统的信任

比如现有业务系统A、B、C以及SSO系统,第一次访问A系统时,发现没有登录,引导用户到SSO系统登录,根据用户的登录信息,生成唯一的一个凭据token,返回给用户。后期用户访问B、C系统的时候,携带上对应的凭证到SSO系统去校验,校验通过后,就可以单点登录;

单点登录在大型网站中使用的非常频繁,例如,阿里旗下有淘宝、天猫、支付宝等网站,其背后的成百上千的子系统,用户操作一次或者交易可能涉及到很多子系统,每个子系统都需要验证,所以提出,用户登录一次就可以访问相互信任的应用系统

单点登录有一个独立的认证中心,只有认证中心才能接受用户的用户名和密码等信息进行认证,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,当用户提供的用户名和密码通过认证中心认证后,认证中心会创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌即得到了授权,然后创建局部会话。

二、单点登录原理

单点登录有同域和跨域两种场景

1)同域

适用场景:都是企业自己的系统,所有系统都使用同一个一级域名通过不同的二级域名来区分。

举个例子:公司有一个一级域名为 zlt.com ,我们有三个系统分别是:门户系统(sso.zlt.com)、应用1(app1.zlt.com)和应用2(app2.zlt.com),需要实现系统之间的单点登录,实现架构如下

核心原理:

  1. 门户系统设置的cookie的domain为一级域名也是zlt.com,这样就可以共享门户的cookie给所有的使用该域名xxx.alt.com的系统
  2. 使用Spring Session等技术让所有系统共享Session
  3. 这样只要门户系统登录之后无论跳转应用1或者应用2,都能通过门户Cookie中的sessionId读取到Session中的登录信息实现单点登录

2)跨域

单点登录之间的系统域名不一样,例如第三方系统。由于域名不一样不能共享Cookie了,需要的一个独立的授权系统,即一个独立的认证中心(passport),子系统的登录均可以通过passport,子系统本身将不参与登录操作,当一个系统登录成功后,passprot将会颁发一个令牌给子系统,子系统可以拿着令牌去获取各自的保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内无需再次向passport发起认证

基本原理

  1. 用户第一次访问应用系统的时候,因为没有登录,会被引导到认证系统中进行登录;
  2. 根据用户提供的登录信息,认证系统进行身份校验,如果通过,返回给用户一个认证凭据-令牌
  3. 用户再次访问别的应用的时候,带上令牌作为认证凭证
  4. 应用系统接收到请求后会把令牌送到认证服务器进行校验,如果通过,用户就可以在不用登录的情况下访问其他信任的业务服务器。

登录流程

登录流程
  1. 用户访问系统1的受保护资源,系统1发现用户没有登录,跳转到sso认证中心,并将自己的地址作为参数
  2. sso认证中心发现用户未登录,将用户引导到登录页面
  3. 用户提交用户名、密码进行登录
  4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称之为全局会话,同时创建授权令牌
  5. sso 带着令牌跳转回最初的请求的地址(系统1)
  6. 系统1拿着令牌,去sso认证中心校验令牌是否有效
  7. sso认证中心校验令牌,返回有效,注册系统1(也就是返回一个cookie)
  8. 系统一使用该令牌创建与用户的会话,成为局部会话,返回受保护的资源
  9. 用户访问系统2受保护的资源
  10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  11. sso认证中心发现用户已登录,跳转回系统2的地址,并且附上令牌
  12. 系统2拿到令牌,去sso中心验证令牌是否有效,返回有效,注册系统2
  13. 系统2使用该令牌创建与用户的局部会话,返回受保护资源
  14. 用户登录成功之后,会与sso认证中心以及各个子系统建立会话,用户与sso认证中心建立的会话称之为全局会话,用户与各个子系统建立的会话称之为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心

注销流程

注销流程
  1. 用户向系统提交注销操作
  2. 系统根据用户与系统1建立的会话,拿到令牌,向sso认证中心提交注销操作
  3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
  4. sso认证中心向所有注册系统发起注销请求,各注册系统销毁局部会话
  5. sso认证中心引导用户到登录页面

Day119:怎样判断一个对象是否是数组,如何处理类数组对象
null

判断数组方式

  • [] instanceof Array
  • Object.prototype.toString.call([]) === '[object Array]'
  • Array.prototype.isPrototypeOf([])
  • [].constructor === Array
  • Array.isArray([])

如何处理类数组对象

1)JavaScript 类数组对象的定义

  • 可以通过索引访问元素,并且拥有 length 属性;
  • 没有数组的其他方法,例如 pushforEachindexOf 等。
var foo = {
    0: 'JS',
    1: 'Node',
    2: 'TS',
    length: 3
}

2)转换方式

// 方式一
Array.prototype.slice.call(arguments);
Array.prototype.slice.apply(arguments)
[].slice.call(arguments)

// 方式二
Array.from(arguments);

// 方式三
// 这种方式要求 数据结构 必须有 遍历器接口
[...arguments] 

// 方式四
[].concat.apply([],arguments)

// 方式五:手动实现
function toArray(s){
  var arr = [];  
  for(var i = 0,len = s.length; i < len; i++){   
    arr[i] = s[i];   
  }  
  return arr;  
}

3)转换后注意几点

  • 数组长度由类数组的length属性决定
  • 索引不连续,会自动补位undefined
  • 仅考虑0和正整数索引;
  • slice会产生稀疏数组,内容是empty而不是undefined
  • 类数组push注意,push操作的是索引值为length的位置

Day120:说一下 CORS 的简单请求和复杂请求的区别
null

CORS

CORS即Cross Origin Resource Sharing(跨来源资源共享),通俗说就是我们所熟知的跨域请求。众所周知,在以前,跨域可以采用代理、JSONP等方式,而在Modern浏览器面前,这些终将成为过去式,因为有了CORS。

CORS在最初接触的时候只大概了解到,通过服务器端设置Access-Control-Allow-Origin响应头,即可使指定来源像访问同源接口一样访问跨域接口,最近在使用CORS的时候,由于需要传输自定义Header信息,发现原来CORS的规范定义远不止这些。

CORS可以分成两种:

1、简单请求
2、复杂请求

1.简单请求:

HTTP方法是下列之一

  • HEAD
  • GET
  • POST

HTTP头信息不超出以下几种字段

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type,但仅能是下列之一
  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

任何一个不满足上述要求的请求,即被认为是复杂请求。一个复杂请求不仅有包含通信内容的请求,同时也包含预请求(preflight request)。

简单请求的发送从代码上来看和普通的XHR没太大区别,但是HTTP头当中要求总是包含一个域(Origin)的信息。该域包含协议名、地址以及一个可选的端口。不过这一项实际上由浏览器代为发送,并不是开发者代码可以触及到的。

简单请求的部分响应头及解释如下:

  • Access-Control-Allow-Origin(必含)- 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写"*"。
  • Access-Control-Allow-Credentials(可选) – 该项标志着请求当中是否包含cookies信息,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。这一项与XmlHttpRequest2对象当中的withCredentials属性应保持一致,即withCredentials为true时该项也为true;withCredentials为false时,省略该项不写。反之则导致请求失败。
  • Access-Control-Expose-Headers(可选) – 该项确定XmlHttpRequest2对象当中getResponseHeader()方法所能获得的额外信息。通常情况下,getResponseHeader()方法只能获得如下的信息:
  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma
  • 当你需要访问额外的信息时,就需要在这一项当中填写并以逗号进行分隔

如果仅仅是简单请求,那么即便不用CORS也没有什么大不了,但CORS的复杂请求就令CORS显得更加有用了。简单来说,任何不满足上述简单请求要求的请求,都属于复杂请求。比如说你需要发送PUT、DELETE等HTTP动作,或者发送Content-Type: application/json的内容。

2.复杂请求

复杂请求表面上看起来和简单请求使用上差不多,但实际上浏览器发送了不止一个请求。其中最先发送的是一种"预请求",此时作为服务端,也需要返回"预回应"作为响应。预请求实际上是对服务端的一种权限请求,只有当预请求成功返回,实际请求才开始执行。

预请求以OPTIONS形式发送,当中同样包含域,并且还包含了两项CORS特有的内容:

  • Access-Control-Request-Method – 该项内容是实际请求的种类,可以是GET、POST之类的简单请求,也可以是PUT、DELETE等等。
  • Access-Control-Request-Headers – 该项是一个以逗号分隔的列表,当中是复杂请求所使用的头部。

显而易见,这个预请求实际上就是在为之后的实际请求发送一个权限请求,在预回应返回的内容当中,服务端应当对这两项进行回复,以让浏览器确定请求是否能够成功完成。

复杂请求的部分响应头及解释如下:

  • Access-Control-Allow-Origin(必含) – 和简单请求一样的,必须包含一个域。
  • Access-Control-Allow-Methods(必含) – 这是对预请求当中Access-Control-Request-Method的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。
  • Access-Control-Allow-Headers(当预请求中包含Access-Control-Request-Headers时必须包含) – 这是对预请求当中Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。这里在实际使用中有遇到,所有支持的头部一时可能不能完全写出来,而又不想在这一层做过多的判断,没关系,事实上通过request的header可以直接取到Access-Control-Request-Headers,直接把对应的value设置到Access-Control-Allow-Headers即可。
  • Access-Control-Allow-Credentials(可选) – 和简单请求当中作用相同。
  • Access-Control-Max-Age(可选) – 以秒为单位的缓存时间。预请求的的发送并非免费午餐,允许时应当尽可能缓存。

一旦预回应如期而至,所请求的权限也都已满足,则实际请求开始发送。

通过caniuse.com得知,目前大部分Modern浏览器已经支持完整的CORS,但IE直到IE11才完美支持,所以对于PC网站,还是建议采用其他解决方案,如果仅仅是移动端网站,大可放心使用。

Day121:说一下 在 map 中和 for 中调用异步函数的区别
null

map & for

  • map 会先把执行同步操作执行完,就返回,之后再一次一次的执行异步任务
  • for 是等待异步返回结果后再进入下一次循环

map

const arr = [1, 2, 3, 4, 5];
function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("data");
    }, 1000);
  });
}

(async () => {
  const result = arr.map(async () => {
    console.log("start");
    const data = await getData();
    console.log(data);
    return data;
  });
  console.log(result);
})();

// 5 start -> 遍历每一项开始
// (5) [Promise, Promise, Promise, Promise, Promise] -> 返回的结果
// 5 data -> 遍历每一项异步执行返回的结果

分析

map 函数的原理是:

  1. 循环数组,把数组每一项的值,传给回调函数
  2. 将回调函数处理后的结果 push 到一个新的数组
  3. 返回新数组

map 函数函数是同步执行的,循环每一项时,到给新数组值都是同步操作。

代码执行结果:

map 不会等到回调函数的异步函数返回结果,就会进入下一次循环。

执行完同步操作之后,就会返回结果,所以 map 返回的值都是 Promise

解决问题

  • 使用 for、for..of 代替

简单实现一个

// 获取数据接口
function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("data");
    }, 1000);
  });
}
// 异步的map
async function selfMap(arr, fn) {
  let result = [];
  for (let i = 0, len = arr.length; i < len; i++) {
    const item = await fn(arr[i], i);
    result.push(item);
  }
  return result;
}
// 调用
(async () => {
  const res = await selfMap([1, 2, 3, 4, 5], async (item, i) => {
    const data = await getData();
    return `${item}_${data}`;
  });
  console.log(res, "res");
})();
// ["1_data", "2_data", "3_data", "4_data", "5_data"] "res"

for

function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("data");
    }, 1000);
  });
}

(async () => {
  for (let i = 0, len = arr.length; i < len; i++) {
    console.log(i);
    const data = await getData();
    console.log(data);
  }
})();

// 0
// data
// 1
// data
// 2
// data
// 3
// data
// 4
// data

Day122:说一下 import 的原理,与 require 有什么不同?
null

import原理(实际上就是ES6 module的原理)

  1. 简单来说就是闭包的运用
  2. 为了创建Module的内部作用域,会调用一个包装函数
  3. 包装函数的返回值也就是Module向外公开的API,也就是所有export出去的变量
  4. import也就是拿到module导出变量的引用

与require的不同

  • CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用
  • CommonJS模块是运行时加载,ES6模块是编译时输出接口

CommonJS是运行时加载对应模块,一旦输出一个值,即使模块内部对其做出改变,也不会影响输出值,如:

// a.js
var a = 1;
function changeA(val) {
    a = val;
}
module.exports = {
    a: a,
    changeA: changeA,
}

// b.js
var modA = require('./a.js');
console.log('before', modA.a); // 输出1
modA.changeA(2);
console.log('after', modA.a); // 还是1

而ES6模块则不同,import导入是在JS引擎对脚步静态分析时确定,获取到的是一个只读引用。等脚本增长运行时,会根据这个引用去对应模块中取值。所以引用对应的值改变时,其导入的值也会变化

Day123:说下 webpack 的 loader 和 plugin 的区别,都使用过哪些 loader 和 plugin
null

一、loader&plugin

1.1 loader

loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中,处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行,第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码

编写自己的loader时需要引用官方提供的loader-utils ,调用loaderUtils.getOptions(this)拿到webpack的配置参数,然后进行自己的处理。

Loader 本身仅仅只是一个函数,接收模块代码的内容,然后返回代码内容转化后的结果,并且一个文件还可以链式的经过多个loader转化(比如scss-loader => css-loader => style-loader)。

一个 Loader 的职责是单一的,只需要完成一种转化。 如果一个源文件需要经历多步转化才能正常使用,就通过多个 Loader 去转化。 在调用多个 Loader 去转化一个文件时,每个 Loader 会链式的顺序执行, 第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果会传给下一个接着处理,最后的 Loader 将处理后的最终结果返回给 Webpack。

一个最简单的loader例子:

module.exports = function(source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
  return source;
};

1.2 plugin

plugin功能更强大,Loader不能做的都是它做。它的功能要更加丰富。从打包优化和压缩,到重新定义环境变量,功能强大到可以用来处理各种各样的任务。

plugin让webpack的机制更加灵活,它在编译过程中留下的一系列生命周期的钩子,通过调用这些钩子来实现在不同编译结果时对源模块进行处理。它的编译是基于事件流来编译的,主要通过taptable来实现插件的绑定和执行的,taptable主要是基于发布订阅执行的插件架构,是用来创建声明周期钩子的库。调用complier.hooks.run.tap开始注册,创建compilation,基于配置创建chunks,在通过parser解析chunks,使用模块和依赖管理模块之间的依赖关系,最后使用template基于compilation数据生成结果代码

plugin 的实现可以是一个类,使用时传入相关配置来创建一个实例,然后放到配置的 plugins 字段中,而 plugin 实例中最重要的方法是 apply,该方法在 webpack compiler 安装插件时会被调用一次,apply 接收 webpack compiler 对象实例的引用,你可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程,以便完成更多其他的构建任务。

一个最简单的plugin例子:

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugi(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

开发 Plugin 最主要的就是理解 compiler 和 compilation,它们是Plugin 和 Webpack 之间的桥梁。这两者提供的各种 hooks 和 api,则是开发plugin 所必不可少的材料,通过 compiler 和 compilation 的生命周期 hooks,也可以更好地深入了解 webpack 的整个构建工作是如何进行的。

二、常见的plugin & loader

2.1 常见loader

  1. file-loader:文件加载
  2. url-loader:文件加载,可以设置阈值,小于时把文件base64编码
  3. image-loader:加载并压缩图片
  4. json-loader:webpack默认包含了
  5. babel-loader:ES6+ 转成ES5
  6. ts-loader:将ts转成js
  7. awesome-typescript-loader:比上面那个性能好
  8. css-loader:处理@import和url这样的外部资源
  9. style-loader:在head创建style标签把样式插入;
  10. postcss-loader:扩展css语法,使用postcss各种插件autoprefixer,cssnext,cssnano
  11. eslint-loader,tslint-loader:通过这两种检查代码,tslint不再维护,用的eslint
  12. vue-loader:加载vue单文件组件
  13. i18n-loader:国际化
  14. cache-loader:性能开销大的loader前添加,将结果缓存到磁盘;
  15. svg-inline-loader:压缩后的svg注入代码;
  16. source-map-loader:加载source Map文件,方便调试;
  17. expose-loader:暴露对象为全局变量
  18. imports-loader、exports-loader等可以向模块注入变量或者提供导出模块功能
  19. raw-loader可以将文件已字符串的形式返回
  20. 校验测试:mocha-loader、jshint-loader 、eslint-loader等

2.2 常见plugin

  • ignore-plugin:忽略文件
  • uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前使用)
  • terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
  • webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载
  • serviceworker-webpack-plugin:为网页应用增加离线缓存功能
  • clean-webpack-plugin: 目录清理
  • speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时
  • webpack内置UglifyJsPlugin,压缩和混淆代码。
  • webpack内置CommonsChunkPlugin,提高打包效率,将第三方库和业务代码分开打包。
  • ProvidePlugin:自动加载模块,代替require和import
  • html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件
  • extract-text-webpack-plugin 将js文件中引用的样式单独抽离成css文件
  • DefinePlugin 编译时配置全局变量,这对开发模式和发布模式的构建允许不同的行为非常有用。
  • HotModuleReplacementPlugin 热更新
  • DllPlugin和DllReferencePlugin相互配合,前者第三方包的构建,只构建业务代码,同时能解决Externals多次引用问题。DllReferencePlugin引用DllPlugin配置生成的manifest.json文件,manifest.json包含了依
    赖模块和module id的映射关系
  • optimize-css-assets-webpack-plugin 不同组件中重复的css可以快速去重
  • webpack-bundle-analyzer 一个webpack的bundle文件分析工具,将bundle文件以可交互缩放的treemap的形式展示。
  • compression-webpack-plugin 生产环境可采用gzip压缩JS和CSS
  • happypack:通过多进程模型,来加速代码构建

你可能感兴趣的:(每日一题第三篇)