前端一些常见的业务场景

584 阅读39分钟

整理自牛客

注册功能前端到后端数据库这一套流程

几种常用的登录方式。

  • Cookie + Session 登录
  • Token 登录
  • SSO 单点登录
  • OAuth 第三方登录

Cookie + Session 登录

HTTP 是一种无状态的协议,客户端每次发送请求时,首先要和服务器端建立一个连接,在请求完成后又会断开这个连接。这种方式可以节省传输时占用的连接资源,但同时也存在一个问题:每次请求都是独立的,服务器端无法判断本次请求和上一次请求是否来自同一个用户,进而也就无法判断用户的登录状态。

为了解决 HTTP 无状态的问题,Lou Montulli 在 1994 年的时候,推出了 Cookie。

Cookie 是服务器端发送给客户端的一段特殊信息,这些信息以文本的方式存放在客户端,客户端每次向服务器端发送请求时都会带上这些特殊信息。

有了 Cookie 之后,服务器端就能够获取到客户端传递过来的信息了,如果需要对信息进行验证,还需要通过 Session。

客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个便是 Session 对象。

有了 Cookie 和 Session 之后,我们就可以进行登录认证了。

Cookie + Session 实现流程

Cookie + Session 的登录方式是最经典的一种登录方式,现在仍然有大量的企业在使用。

用户访问a.com/pageA,并输入密码登录。 2. 服务器验证密码无误后,会创建 SessionId,并将它保存起来。 3. 服务器端响应这个 HTTP 请求,并通过 Set-Cookie 头信息,将 SessionId 写入 Cookie 中。

服务器端的 SessionId 可能存放在很多地方,例如:内存、文件、数据库等。

第一次登录完成之后,后续的访问就可以直接使用 Cookie 进行身份验证了:

  1. 用户访问a.com/pageB页面时,会自动带上第一次登录时写入的 Cookie。
  2. 服务器端比对 Cookie 中的 SessionId 和保存在服务器端的 SessionId 是否一致。
  3. 如果一致,则身份验证成功。 Cookie + Session 存在的问题

虽然我们使用 Cookie + Session 的方式完成了登录验证,但仍然存在一些问题:

  • 由于服务器端需要对接大量的客户端,也就需要存放大量的 SessionId,这样会导致服务器压力过大。
  • 如果服务器端是一个集群,为了同步登录态,需要将 SessionId 同步到每一台机器上,无形中增加了服务器端维护成本。
  • 由于 SessionId 存放在 Cookie 中,所以无法避免 CSRF 攻击。

Token 登录

为了解决 Session + Cookie 机制暴露出的诸多问题,我们可以使用 Token 的登录方式。

Token 是服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。

Token 机制实现流程

用户首次登录时:

  • 用户输入账号密码,并点击登录。
  • 服务器端验证账号密码无误,创建 Token。
  • 服务器端将 Token 返回给客户端,由客户端自由保存。 后续页面访问时:
  • 用户访问a.com/pageB时,带上第一次登录时获取的 Token。
  • 服务器端验证 Token ,有效则身份验证成功。 Token 机制的特点

根据上面的案例,我们可以分析出 Token 的优缺点:

  • 服务器端不需要存放 Token,所以不会对服务器端造成压力,即使是服务器集群,也不需要增加维护成本。
  • Token 可以存放在前端任何地方,可以不用保存在 Cookie 中,提升了页面的安全性。
  • Token 下发之后,只要在生效时间之内,就一直有效,如果服务器端想收回此 Token 的权限,并不容易。 Token 的生成方式

最常见的 Token 生成方式是使用 JWT(Json Web Token),它是一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。

上文中我们说到,使用 Token 后,服务器端并不会存储 Token,那怎么判断客户端发过来的 Token 是合法有效的呢?

答案其实就在 Token 字符串中,其实 Token 并不是一串杂乱无章的字符串,而是通过多种算法拼接组合而成的字符串,我们来具体分析一下。

JWT 算法主要分为 3 个部分:header(头信息),playload(消息体),signature(签名)。

header 部分指定了该 JWT 使用的签名算法:

header = '{"alg":"HS256","typ":"JWT"}' // HS256 表示使用了 HMAC-SHA256 来生成签名。 playload 部分表明了 JWT 的意图

payload = '{"loggedInAs":"admin","iat":1422779638}' //iat 表示令牌生成的时间 signature 部分为 JWT 的签名,主要为了让 JWT 不能被随意篡改,签名的方法分为两个步骤:

  • 输入base64url编码的 header 部分、base64url编码的 playload 部分,输出 unsignedToken。
  • 输入服务器端私钥、unsignedToken,输出 signature 签名。
const base64Header = encodeBase64(header)
const base64Payload = encodeBase64(payload)
const unsignedToken = `${base64Header}.${base64Payload}`
const key = '服务器私钥'

signature = HMAC(key, unsignedToken)

最后的 Token 计算如下:

const base64Header = encodeBase64(header)
const base64Payload = encodeBase64(payload)
const base64Signature = encodeBase64(signature)
 
token = `${base64Header}.${base64Payload}.${base64Signature}`

服务器在判断 Token 时:

const [base64Header, base64Payload, base64Signature] = token.split('.')
 
const signature1 = decodeBase64(base64Signature)
const unsignedToken = `${base64Header}.${base64Payload}`
const signature2 = HMAC('服务器私钥', unsignedToken)
 
if(signature1 === signature2) {
  return '签名验证成功,token 没有被篡改'
}
 
const payload =  decodeBase64(base64Payload)
if(new Date() - payload.iat < 'token 有效期'){
  return 'token 有效'
}

有了 Token 之后,登录方式已经变得非常高效,接下来我们介绍另外两种登录方式。

SSO 单点登录

单点登录指的是在公司内部搭建一个公共的认证中心,公司下的所有产品的登录都可以在认证中心里完成,一个产品在认证中心登录后,再去访问另一个产品,可以不用再次登录,即可获取登录状态。

SSO 机制实现流程

用户首次访问时,需要在认证中心登录:

  • 用户访问网站a.com下的 pageA 页面。
  • 由于没有登录,则会重定向到认证中心,并带上回调地址www.sso.com?return_uri=a.com/pageA,以便登录后直接进入对应页面。
  • 用户在认证中心输入账号密码,提交登录。
  • 认证中心验证账号密码有效,然后重定向a.com?ticket=123带上授权码 ticket,并将认证中心sso.com的登录态写入 Cookie。
  • 在a.com服务器中,拿着 ticket 向认证中心确认,授权码 ticket 真实有效。
  • 验证成功后,服务器将登录信息写入 Cookie(此时客户端有 2 个 Cookie 分别存有a.com和sso.com的登录态)。
  • 认证中心登录完成之后,继续访问a.com下的其他页面:

这个时候,由于a.com存在已登录的 Cookie 信息,所以服务器端直接认证成功。

如果认证中心登录完成之后,访问b.com下的页面:

这个时候,由于认证中心存在之前登录过的 Cookie,所以也不用再次输入账号密码,直接返回第 4 步,下发 ticket 给b.com即可。

SSO 单点登录退出

目前我们已经完成了单点登录,在同一套认证中心的管理下,多个产品可以共享登录态。现在我们需要考虑退出了,即:在一个产品中退出了登录,怎么让其他的产品也都退出登录?

原理其实不难,可以回过头来看第 5 步,每一个产品在向认证中心验证 ticket 时,其实可以顺带将自己的退出登录 api 发送到认证中心。

当某个产品c.com退出登录时:

  • 清空c.com中的登录态 Cookie。
  • 请求认证中心sso.com中的退出 api。
  • 认证中心遍历下发过 ticket 的所有产品,并调用对应的退出 api,完成退出。

OAuth 第三方登录

在上文中,我们使用单点登录完成了多产品的登录态共享,但都是建立在一套统一的认证中心下,对于一些小型企业,未免太麻烦,有没有一种登录能够做到开箱即用?

OAuth 机制实现流程

这里以微信开放平台的接入流程为例:

  • 首先,a.com的运营者需要在微信开放平台注册账号,并向微信申请使用微信登录功能。
  • 申请成功后,得到申请的 appid、appsecret。
  • 用户在a.com上选择使用微信登录。
  • 这时会跳转微信的 OAuth 授权登录,并带上a.com的回调地址。
  • 用户输入微信账号和密码,登录成功后,需要选择具体的授权范围,如:授权用户的头像、昵称等。
  • 授权之后,微信会根据拉起a.com?code=123,这时带上了一个临时票据 code。
  • 获取 code 之后,a.com会拿着 code 、appid、appsecret,向微信服务器申请 token,验证成功后,微信会下发一个 token。
  • 有了 token 之后,a.com就可以凭借 token 拿到对应的微信用户头像,用户昵称等信息了。
  • a.com提示用户登录成功,并将登录状态写入 Cooke,以作为后续访问的凭证。

总结

本文介绍了 4 种常见的登录方式,原理应该大家都清楚了,总结一下这 4 种方案的使用场景:

  • Cookie + Session 历史悠久,适合于简单的后端架构,需开发人员自己处理好安全问题。
  • Token 方案对后端压力小,适合大型分布式的后端架构,但已分发出去的 token ,如果想收回权限,就不是很方便了。
  • SSO 单点登录,适用于中大型企业,想要统一内部所有产品的登录方式。
  • OAuth 第三方登录,简单易用,对用户和开发者都友好,但第三方平台很多,需要选择合适自己的第三方登录平台。

懒加载

什么是懒加载?

懒加载是一种在页面加载时延迟加载一些非关键资源的技术,换句话说就是按需加载。对于图片来说,非关键通常意味着离屏。 我们之前看到的懒加载一般是这样的形式:

浏览一个网页,准备往下拖动滚动条 拖动一个占位图片到视窗 占位图片被瞬间替换成最终的图片 网页首先用一张轻量级的图片占位,当占位图片被拖动到视窗,瞬间加载目标图片,然后替换占位图片。

为什么要懒加载而不直接加载?

  • 浪费流量。在不计流量收费的网络,这可能不重要;在按流量收费的网络中,毫无疑问,一次性加载大量图片就是在浪费用户的钱。
  • 消耗额外的电量和其他的系统资源,并且延长了浏览器解析的时间。因为媒体资源在被下载完成后,浏览器必须对它进行解码,然后渲染在视窗上,这些操作都需要一定的时间。 懒加载图片和视频,可以减少页面加载的时间、页面的大小和降低系统资源的占用,这些对于性能都有显著地提升。

懒加载图片

图片懒加载在技术上实现很简单,不过对于细节要求比较严格。目前有很多实现懒加载的方法,先从懒加载内联图片说起吧。

内联图片

最常见的懒加载方式就是利用标签。懒加载图片时,我们利用JavaScript检查标签是否在视窗中。如果在的src(有时候是srcset)就会设置为目标图片的url。

利用intersection observer

如果你之前用过懒加载,你很可能是通过监听一些事件比如scroll或者resize来检测元素出现在视窗,这种方法很成熟,能够兼容大部分的浏览器。但是,现代浏览器提供了一个更好的方法给我们Intersection observer

注意:Intersection observer目前只能在Chrome63+和firefox58+使用

比起事件监听,Intersection observer用起来比较简单,可阅读性也大大提高。开发者只需要注册一个observer去监控元素而不是写一大堆乱七八糟的视窗检测代码。注册observer之后我们只需要做的就是当元素可见时改变它的行为。举个例子吧:
<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="I'm an image!">
需要注意三个相关的属性

  • class:用于在JavaScript中关联元素
  • src属性:指向了一张占位图片,图片在页面的第一次加载会显现
  • data-src和data-srcset属性:这是占位属性,里面放的是目标图片的url ok,看一下怎么在JavaScript中使用Intersection observer吧:
document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
 
  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });
 
    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Possibly fall back to a more compatible method here
  }
});

当DOMContentLoaded触发后,js会查询class为lazy的img元素。然后我们检测浏览器支不支持intersection observer,如果可以用,先创建一个observer,然后传入回调函数,回调函数将会在元素可见性变化时被调用。 最后比较麻烦的是处理兼容性,在不支持intersection observer的浏览器,你需要引入polyfill,或者回退到更安全的方法。

利用事件

当你选择使用intersection observer来实现懒加载时,你要考虑它的兼容性,当然你可以使用polyfill,实际上这也非常简单。事实上你也可以针对低版本的浏览器使用事件来完成更安全地回退。你可以使用scroll、resize和orientationchange事件,再配合getBoundingClientRectAPI就可以实现懒加载了。 和上面一样的例子,现在JavaScript程序变成了这样:

document.addEventListener("DOMContentLoaded", function() {
  let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  let active = false;
 
  const lazyLoad = function() {
    if (active === false) {
      active = true;
 
      setTimeout(function() {
        lazyImages.forEach(function(lazyImage) {
          if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
            lazyImage.src = lazyImage.dataset.src;
            lazyImage.srcset = lazyImage.dataset.srcset;
            lazyImage.classList.remove("lazy");
 
            lazyImages = lazyImages.filter(function(image) {
              return image !== lazyImage;
            });
 
            if (lazyImages.length === 0) {
              document.removeEventListener("scroll", lazyLoad);
              window.removeEventListener("resize", lazyLoad);
              window.removeEventListener("orientationchange", lazyLoad);
            }
          }
        });
 
        active = false;
      }, 200);
    }
  };
 
  document.addEventListener("scroll", lazyLoad);
  window.addEventListener("resize", lazyLoad);
  window.addEventListener("orientationchange", lazyLoad);
});

上面的代码用了getBoundingClientRect,在scroll事件中检测img是否在视窗。setTimeout用于延迟执行操作,active变量代表了处理状态防止同时响应。当图片被懒加载完成后,事件处理程序将被移除,尽管上面这段代码可以在绝大部分的浏览器上运行,但存在显著的性能损耗。在此示例中,无论在视口中是否存在图像,文档滚动或窗口大小调整时都会每200毫秒执行一次检查。 另外,跟踪有多少元素留给延迟加载和解除事件处理程序的繁琐工作也留给了开发者。

建议:尽可能使用intersection observer,如果应用要求兼容低版本的浏览器才考虑利用事件

CSS图像

展示图像不是标签的特权,CSS利用background-image也可以做到。相比较而言,CSS加载图片比较容易控制。当文档对象模型、CSS对象模型和渲染树被构造完成后,开始请求外部资源之前,浏览器会检测CSS规则是怎么应用到DOM上的。如果浏览器检测到CSS引用的外部资源并没有应用到已存在的DOM节点上,浏览器就不会请求这些资源。 这个行为可用于延迟CSS图片资源的加载,思路是通过JavaScript检测到元素处于视窗中时,加一个class类名,这个class就引用了外部图片资源。 这可以实现图片按需加载而不是一次性全部加载。给个例子:

<div class="lazy-background">
  <h1>Here's a hero heading to get your attention!</h1>
  <p>Here's hero copy to convince you to buy a thing!</p>
  <a href="/buy-a-thing">Buy a thing!</a>
</div>

这个div.lazy-background元素会正常地显示CSS规则加载的占位图片。当元素处于可见状态时,我们可以添加一个类名完成懒加载:

.lazy-background {
  background-image: url("hero-placeholder.jpg"); /* 占位图片 */
}
 
.lazy-background.visible {
  background-image: url("hero.jpg"); /* 真正的图片 */
}

下面是利用JavaScript去检测元素是否处于视窗(intersection observer),如果可见就为它加上一个visible的类名。

document.addEventListener("DOMContentLoaded", function() {
  var lazyBackgrounds = [].slice.call(document.querySelectorAll(".lazy-background"));
 
  if ("IntersectionObserver" in window) {
    let lazyBackgroundObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          entry.target.classList.add("visible");
          lazyBackgroundObserver.unobserve(entry.target);
        }
      });
    });
 
    lazyBackgrounds.forEach(function(lazyBackground) {
      lazyBackgroundObserver.observe(lazyBackground);
    });
  }
});

懒加载视频

就像图片一样,我们同样可以懒加载视频,播放视频会用到``标签。如何懒加载视频取决于特定的场景,先来讨论几个需要不同解决方案的场景。

视频不需要自动播放

<video controls preload="none" poster="one-does-not-simply-placeholder.jpg">
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

我们还需要添加一个poster属性给preload标签,这相当于一个占位符。preload属性则规定是否在页面加载后载入视频。鉴于浏览器之间的preload默认值差异,显式定义会更具兼容性。在这种情况下,当用户点击播放视频时,视频才会被加载,预加载视频简单地实现了。不幸的是,当我们想用视频替代GIF动画时,这个方法就行不通了。

用视频模拟GIF

GIF在很多地方都不及视频,特别是文件大小方面。在相同质量下,视频的尺寸通常会比GIF文件小得多。因为GIF图片有三种要注意的行为:

  • 加载完后自动播放
  • 不停地循环播放
  • 没有音轨,要实现这些,HTML是这样的:
<video autoplay muted loop playsinline>
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

autoplay、muted和loop的作用是为了实现上述三个功能,playsinline是为了兼容IOS的autoplay。现在我们已经有了一个跨平台的视频模版用于取代GIF图片了。接下来怎么进行懒加载呢?Chrome会帮我们自动完成这项工作,但你不能保证所有浏览器都能做到这个。

<video autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
  <source data-src="one-does-not-simply.webm" type="video/webm">
  <source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>

注意到了吗?有一个奇怪的poster属性。这个属性其实是一个占位符,在被懒加载之前,poster里面指定的内容会在标签中显现


document.addEventListener("DOMContentLoaded", function() {
  var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));
 
  if ("IntersectionObserver" in window) {
    var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(video) {
        if (video.isIntersecting) {
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }
 
          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);
        }
      });
    });
 
    lazyVideos.forEach(function(lazyVideo) {
      lazyVideoObserver.observe(lazyVideo);
    });
  }
});

当懒加载一个视频的时,首先要迭代标签里面的每一个,然后将data-src中的url分配给src属性。然后调用元素的load方法,现在视频就可以自动播放了。 通过这个方法,我们有了一个模拟GIF动画的视频解决方案,不会消耗带宽加载不必要的媒体资源,而且还能实现懒加载。

懒加载库

如果你不关心懒加载背后是如何实现的,你只是想找一个库去实现这个功能,可供选择的有:

lazysizes 是一个功能十分强大的懒加载库,主要用于加载图片和iframes。你只需要指定data-src/data-srcset属性,lazysizes会帮你自动懒加载内容。值得注意的是,lazysizes基于intersection observer,因此你需要一个polyfill。你还可以通过一些插件扩展库的功能以用于懒加载视频。 lozad.js是一个轻量级、高性能的懒加载库,基于intersection observer,你同样需要提供一个相关的polyfill。 blazy是一个轻量级的懒加载库,大小仅为1.4KB。相对于lazysizes,它不需要任何的外部依赖,并且兼容IE7+。你可能猜测到了,blazy不支持intersection observer,性能相对较差。 yall.js是作者本人写的一个懒加载库,基于IntersectionObserver和事件,兼容IE11和大部分的浏览器。 如果你想寻找一个基于React的懒加载工具,react-lazyload可能是你的选择。 上述每个懒加载库的文档都写得很好,同时提供了大量的标记模式。如果你不想深究懒加载的技术细节,就选择任意一个去使用,这能节省你很多的时间和功夫。

图片压缩算法

PNG图片的压缩,分两个阶段:

  • 预解析(Prediction):这个阶段就是对png图片进行一个预处理,处理后让它更方便后续的压缩。说白了,就是一个女神,在化妆前,会先打底,先涂乳液和精华,方便后续上妆、美白、眼影、打光等等。
  • 压缩(Compression):执行Deflate压缩,该算法结合了 LZ77 算法和 Huffman 算法对图片进行编码。 预解析(Prediction)

png图片用差分编码(Delta encoding)对图片进行预处理,处理每一个的像素点中每条通道的值,差分编码主要有几种:

  • 不过滤
  • X-A
  • X-B
  • X-(A+B)/2(又称平均值)
  • Paeth推断(这种比较复杂) 假设,一张png图片如下:

这张图片是一个红色逐渐增强的渐变色图,它的红色从左到右逐渐加强,映射成数组的值为[1,2,3,4,5,6,7,8],使用X-A的差分编码的话,那就是:

[2-1=1, 3-2=1, 4-3=1, 5-4=1, 6-5=1, 7-6=1, 8-7=1]

得到的结果为

[1,1,1,1,1,1,1]

最后的[1,1,1,1,1,1,1]这个结果出现了大量的重复数字,这样就非常适合进行压缩。

这就是为什么渐变色图片、颜色值变化不大并且颜色单一的图片更容易压缩的原理。

差分编码的目的,就是尽可能的将png图片数据值转换成一组重复的、低的值,这样的值更容易被压缩。

最后还要注意的是,差分编码处理的是每一个的像素点中每条颜色通道的值,R(红)、G(绿)、B(蓝)、A(透明)四个颜色通道的值分别进行处理。

压缩(Compression)

压缩阶段会将预处理阶段得到的结果进行Deflate压缩,它由 Huffman 编码 和 LZ77压缩构成。

如前面所说,Deflate压缩会标记图片所有的重复数据,并记录数据特征和结构,会得到一个压缩比最大的png图片 编码数据。

Deflate是一种压缩数据流的算法. 任何需要流式压缩的地方都可以用。

还有就是我们前面说过,一个png图片,是由很多的数据块构成的,但是数据块里面的一些信息其实是没有用的,比如用Photoshop保存了一张png图片,图片里就会有一个区块记录“这张图片是由photshop创建的”,很多类似这些信息都是无用的,如果用photoshop的“导出web格式”就能去掉这些无用信息。导出web格式前后对比效果如下图所示:

加载很多图片时的优化方法,页面加载时的交互优化

图片压缩

页面是由“小图”平铺来的,却需要加载大量原图,得不偿失。于是很自然的会想到,将“小图”变为真正的小图,当实际点击大图时再去请求原图,这样便会大大减少页面加载时间。

  1. 图片异源加载 HTML代码img标签中将真实图片地址写在 data-original 属性中,而 src 属性中的图片换成占位符的图片(压缩图)
<!--
添加 width 和 height 属性有助于在图片未加载时占满所需要的空间
-->
<img class="lazy" src="grey.gif" data-original="example.jpg" width="640" heigh="480">
  1. Java后台图片压缩
  • 利用Java原生的imageIO类进行裁剪
/**
     * 缩放图像(按比例缩放)
     *
     * @param src    源图像
     * @param output 输出流
     * @param scale  缩放比例
     * @param flag   缩放选择:true 放大; false 缩小;
     */
    public static final void zoomScale(BufferedImage src, OutputStream output, String type, double scale, boolean flag) {
        try {
            // 得到源图宽
            int width = src.getWidth();
            // 得到源图长
            int height = src.getHeight();
            if (flag) {
                // 放大
                width = Long.valueOf(Math.round(width * scale)).intValue();
                height = Long.valueOf(Math.round(height * scale)).intValue();
            } else {
                // 缩小
                width = Long.valueOf(Math.round(width / scale)).intValue();
                height = Long.valueOf(Math.round(height / scale)).intValue();
            }
            Image image = src.getScaledInstance(width, height, Image.SCALE_DEFAULT);
            BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics g = tag.getGraphics();
            // 绘制缩小后的图
            g.drawImage(image, 0, 0, null);
            g.dispose();
            // 输出为文件
            ImageIO.write(tag, type, output);
        } catch (IOException e) {
            throw new KitException(e);
        }
    }

使用原生imageIO类进行压缩图片,速度较快,但仅能对图片尺寸进行压缩,但不能压缩图片质量。

借助一些三方插件,如使用google开源工具Thumbnailator实现图片压缩

Thumbnailator是一个用java生成高质量缩略图的第三方库,可以用来:

1.生成缩率图;2.添加水印;3.图片旋转;4.图片大小缩放;5.图片压缩;

Thumbnailator库只有一个jar,不依赖第三方库,maven依赖

<dependency>
  <groupId>net.coobird</groupId>
  <artifactId>thumbnailator</artifactId>
  <version>0.4.8</version>
</dependency>

Thumbnailator接口链式风格写法,使用简单

Thumbnails.of("原图文件的路径").scale(1f).outputQuality(0.5f).toFile("压缩后文件的路径"); 前端JS实现图片压缩 HTML5 file API加canvas实现图片JS压缩

/**
 * 图片压缩,默认同比例压缩
 * @param {Object} path
 *   图片路径
 * @param {Object} obj
 *   obj 对象 有 width, height, quality(0-1)   不传width和 height,图片大小不变只改变像素值
 * @param {Object} callback
 *   回调函数有一个参数,base64的字符串数据
*/
function dealImage(path, obj, callback){
 
        var img = new Image();
        img.src = path;
        img.onload = function(){
            var that = this;
            // 默认按比例压缩
            var w = that.width,
            h = that.height,
            scale = w / h;
            w = obj.width || w;
            h = obj.height || (w / scale);
            var quality = 0.3;  // 默认图片质量为0.7
            //生成canvas
            var canvas = document.createElement('canvas');
            var ctx = canvas.getContext('2d');
            // 创建属性节点
            var anw = document.createAttribute("width");
            anw.nodeValue = w;
            var anh = document.createAttribute("height");
            anh.nodeValue = h;
            canvas.setAttributeNode(anw);
            canvas.setAttributeNode(anh);
            ctx.drawImage(that, 0, 0, w, h);
            // 图像质量
            if(obj.quality && obj.quality <= 1 && obj.quality > 0){
                quality = obj.quality;
            }
            // quality值越小,所绘制出的图像越模糊
            var base64 = canvas.toDataURL('image/jpeg', quality );
            // 回调函数返回base64的值
            callback(base64);
        }
    }      

TIPS:上述压缩图片的方式,存在资源争抢时效率会大大降低。压缩图片均需要读原图然后进行压缩生成图片流或文件,如果原图本身较大、数量较多且有多个线程同时进行压缩时,每张图片压缩的时长均会成倍的增长。

将图片转Base64格式来节约请求

当我们的一个页面中要传入很多图片时,特别是一些比较小的图片,十几K、几K,这些小图标都会增加HTTP请求。比如要下载一些一两K大的小图标,其实请求时带上的额外信息有可能比图标的大小还要大。所以,在请求越多时,在网络传输的数据自然就越多了,传输的数据自然也就变慢了。而这里,我们采用Base64的编码方式将图片直接嵌入到网页中,而不是从外部载入,这样就减少了HTTP请求。

Base64编码由来

为什么会有Base64编码呢?因为有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASCII码的控制字符就 不能通过邮件传送。这样用途就受到了很大的限制,比如图片二进制流的每个字节不可能全部是可见字符,所以就传送不了。最好的方法就是在不改变传统协议的情 况下,做一种扩展方案来支持二进制文件的传送。把不可打印的字符也能用可打印字符来表示,问题就解决了。Base64编码应运而生,Base64就是一种 基于64个可打印字符来表示二进制数据的表示方法。

  • Base64编码索引 Base64的索引表如下,字符选用了"A-Z、a-z、0-9、+、/" 64个可打印字符。数值代表字符的索引,这个是标准Base64协议规定的,不能更改。

  • Base64编码原理

Base64的码表只有64个字符, 如果要表达64个字符的话,使用6的bit即可完全表示(2的6次方为64)。因为Base64的编码只有6个bit即可表示,而正常的字符是使用8个bit表示, 8和6的最小公倍数是24,所以4个Base64字符可以表示3个标准的ascll字符; 对以某编码方式编码后的字节数组为对象,以3个字节为一组,按顺序排列24bit数据,然后以6bit一组分成4组;再在每组的最高位补2个0凑足一个字节。这时一组就有4个字节了。若字节数组不是3的倍数,那么最后一组就填充1到2个0字节。 然后按Base64编码方式(就是映射关系)对字节数组进行解码,就会得到平时看到的Base64编码文本。对于字节数组不是3的倍数,最后一组填充1到2个0字节的情况,填补的0字节对应的是=(等号)。以下为具体的解析过程案例:

把AB这两个字符转换为Base64的过程

  ①. 对AB进行ASCII编码:得到A(65)B(66)
  ②. 转成二进制形式:得到A01000001B01000010)
  ③. 以3个字节为一组,非3的倍数补0字节:010000010100001000000000
  ④. 以6bit为一组后高位补两个0:(00 010000)(00 010100)(00 001000)(00 000000)
  ⑤. 转为十进制:(16)(20)(8)(0)
  ⑥. 根据映射关系解码:QUI=

当转换到最后, 如果不足三个字符的话,我们直接在最后添加=号即可。

  • 图像和base64转换 图片的本质就是每个像素点都是一个数字,该数字表示颜色,然后把很多很多像素点的数字拼到一起,就是图像。图像转Base64,就是把图像的直方图所有数字转成Base64编码,反之,Base64也能转换回图像。

  • Data URI Scheme data URI scheme 允许我们使用内联(inline-code)的方式在网页中包含数据,目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。常用于将图片嵌入网页。

//传统的图片HTML是这样用的,
<img src="http://gpeng.win/test.png" />
//Data URI的图片内嵌式是这样用的,
<img src="data:image/gif;base64,R0lGODlhAwADAIABAL6+vv///yH5BAEAAAEALAAAAAADAAMAAAIDjA9WADs=" />
//目前,Data URI scheme支持的类型有:
data:,                            文本数据
data:text/plain,                    文本数据
data:text/html,                  HTML代码
data:text/html;base64,            base64编码的HTML代码
data:text/css,                    CSS代码
data:text/css;base64,              base64编码的CSS代码
data:text/javascript,              Javascript代码
data:text/javascript;base64,        base64编码的Javascript代码
data:image/gif;base64,            base64编码的gif图片数据
data:image/png;base64,            base64编码的png图片数据
data:image/jpeg;base64,          base64编码的jpeg图片数据
data:image/x-icon;base64,          base64编码的icon图片数据
Data URI Scheme优缺点:

优点:

  • 减少资源请求链接数。
  • 当访问外部资源很麻烦或受限时,可以很好的利用Data URI Scheme
  • 没有跨域问题,无需考虑缓存、文件头或者cookies问题

缺点:

  • Data URL形式的图片不会被浏览器缓存,这意味着每次访问这样页面时都被下载一次,
  • 但可通过在css文件的background-image样式规则使用Data URI Scheme,使其随css文件一同被浏览器缓存起来)。
  • Base64编码的数据体积通常是原数据的体积4/3,也就是Data URL形式的图片会比二进制格式的图片体积大1/3。
  • 仅适用于极小或者极简单图片,不适用于大图片。 移动端性能比较低。

图片预加载

图片预加载的主要思路就是把稍后需要用到的图片悄悄的提前加载到本地,因为浏览器有缓存的原因,如果稍后用到这个url的图片了,浏览器会优先从本地缓存找该url对应的图片,如果图片没过期的话,就使用这个图片其中图片预加载也分为三种,无序加载,有序加载,基于用户行为的预加载(点击某个按钮或者滚动的时候进行加载)。 预加载的实现很简单,其核心说到底就两句话:

var img = new Image(); 
img.src = "my_image.jpg";
  1. memory cache

MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。 目前Webkit资源分成两类,一类是主资源,比如HTML页面,或者下载项,一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接,分别对应代码中两个类:MainResourceLoader和SubresourceLoader。虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

  1. disk cache

diskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。它与memoryCache最大的区别在于,当退出进程时,内存中的数据会被清空,而磁盘的数据不会,所以,当下次再进入该进程时,该进程仍可以从diskCache中获得数据,而memoryCache则不行。 diskCache与memoryCache相似之处就是也只能存储一些派生类资源文件。它的存储形式为一个index.dat文件,记录存储数据的url,然后再分别存储该url的response信息和content内容。Response信息最大作用就是用于判断服务器上该url的content内容是否被修改。

其他常见优化

  1. 将图片服务和应用服务分离 对于服务器来说,图片是比较消耗系统资源的,如果将图片服务和应用服务放在同一服务器的话,应用服务器很容易会因为图片的高I/O负载而崩溃,所以当网站存在大量的图片读写操作时,建议使用图片服务器。

  2. 图片懒加载 页面加载后只让文档可视区内的图片显示,其它不显示,随着用户对页面的滚动,判断其区域位置,生成img标签,让到可视区的图片加载出来。jQuery的lazyload插件便是一个可以实现图片延迟加载的插件,在用户触发某个条件之后再加载对应的图片资源,这对网页的打开速度有很大提升。 引入lazyload.js,对我们想要延迟加载的图片添加lazy样式,用”data-original” 替换图片的引用路径

<!-- 对img标签使用 -->
<img class="lazy" data-original="img/example.jpg">
<!-- 延迟加载元素的背景图 -->
<div class="lazy" data-original="img/bg.jpg">
    ...
</div>

在JS文件中调用lazyload()方法

$().ready(function(){
    //可任意选择你想延迟加载的目标元素,例如直接使用样式名lazy作为选择条件
    $("img .lazy").lazyload({
         placeholder : "img/grey.gif", //占位图
         effect: "fadeIn", // 加载效果
         threshold: 200, // 提前加载
         event: 'click',  // trigger
         container: $("#container"),  //指定容器
         failurelimit : 5 // 发生混乱时的hack手段
    })
})
  1. CSS Sprites 当网站或者APP有大量小icon,要加载所有这些小icon将增加大量请求,这无疑将增加很多成本. CSS Sprites 技术就是将这些小icon合并成一张图片,只需要加载一次,每次通过background-position来控制显示icon,这样就可以节约大量请求,节约成本.

关于样式规范统一化的实现

css 指层叠样式表 (Cascading Style Sheets),定义如何显示 html 元素,但由于 css 天生全局性,随着项目复杂度增加,极易出现样式覆盖以及其它的问题。

  1. 通用规范 文件编码
  • 为了避免内容乱码,统一使用 UTF-8 编码保存。
  • 样式文件第一行设置字符集为 UTF-8

@charset 'UTF-8'; /* 注意字符集说明应在第一行 */

  • 缩进规范

统一使用两个空格缩进

  1. 初始化规范 各浏览器厂商的初始样式都不一样,为了消除不同浏览器对 html文本呈现的差异,我们常引入一些初始化样式,如 normalize.css、reset.css 等,当对于这些样式的引入我们需要注意下面几种情况:
  • 不使用 UI 框架,由零开始搭建 从零开始搭建的情况下,进行样式初始化,在项目最开始的时候就引入,不要在开发中途引入,避免不可预知的样式冲突。
  • 不使用 UI 框架,但使用了部分插件 插件往往都带有自己特有的样式,如富文本插件,在开发中途使用初始化样式有可能导致样式错乱,所以不建议大范围的初始化,只需简单进行初始化即可。
* {
  padding: 0;
  margin: 0;
}
  • 已使用 UI 框架 在明确知道需要使用 UI 框架的时候,不使用第三方初始化样式,不管是在项目开始前还是进行中,因为 UI 框架一般都自带初始化,额外引入了反而会影响原有效果。
  1. 代码规范
  • 命名规范

class 应以功能或内容命名,不以表现形式命名 class 与 id 单词字母小写,多个单词组成时,采用中划线-分隔 使用唯一的 id 作为 JavaScript hook, 同时避免创建无样式信息的 class

  • 代码风格

统一使用展开格式,不推荐紧凑格式

/* 展开格式 */

.test {
  color: red;
  font-size: 12px;
}
 
/* 紧凑格式 */
.test {
  color: red;
  font-size: 12px;
}

统一两个空格缩进

属性声明结尾加分号

选择器与左括号之间一个空格,属性冒号后一个空格


/* 推荐 */
.test {
  color: red;
  font-size: 12px;
}
 
/* 不推荐 */
.test {
  color: red;
  font-size: 12px;
}

不要为 0 指明单位

颜色值和属性值十六进制数值能用简写的尽量用简写

/* 推荐 */
.test {
  color: #fff;
}
 
/* 不推荐 */
.test {
  color: #ffffff;
}

引号使用

url() 、属性选择符、属性值使用单引号。

清除浮动

当元素需要撑起高度以包含内部的浮动元素时,通过对伪类设置 clear 或触发 BFC 的方式进行 clearfix。尽量不使用增加空标签的方式。

触发 BFC 的方式很多,常见的有:

float 非 none
position 非 static
overflow 非 visible

  • 字体规范

对外商用网站,不要用font-face引入微软雅黑字体,避免侵权(包括图片内容)

需要在 Windows 平台显示的中文内容,其字号应不小于 12px

网站上使用 微软雅黑 字体有三种形式:

  • 1、【侵权】图片中使用 微软雅黑 字体,比如网站头图

  • 2、【安全】网站 CSS 用 font-family 声明网站使用 微软雅黑 字体,比如文章标题和正文

  • 3、【侵权】网站通过 font-face 引用 微软雅黑 ,这种方式不常见

  • 选择器规范

在严格遵照BEM(Block Element Modifier)时,建议只使用类选择器,但 BEM 书写麻烦,所以建议如下

  • 禁用通用选择器 *

  • 不使用无具体语义定义的标签选择器

属性顺序

CSS 属性顺序是 CSS 良好编码风格的一部分,有助于提高代码可读性,便于发现代码问题,有利于团队合作,但在项目中发现部分同学在书写属性顺序时较为随意,想到一个属性就写一个。

建议使用下列顺序进行书写

  • 定位属性(position、display、float、left、right)
  • 尺寸属性(width、height、padding、margin、border)
  • 字体属性(color、font、text-align)
  • 其他属性(background、cursor、outline) 目的是在浏览代码时,能逐步清晰目标元素的效果。
.test {
  display: block;
  position: relative;
  float: left;
  width: 100px;
  height: 100px;
  margin: 0 10px;
  padding: 20px 0;
  font-size: 12px;
  color: #333;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 10px;
}
  1. 注释规范

单行注释

注释以 /* 开始,以 */ 结束,注释内不能嵌套注释,注释内容前后空一个空格。

/* 推荐的单行注释 */
/*不推荐的单行注释*/

注:在 sass 和 less 等预处理语言上也可以使用双斜线注释,但编译后注释内容不会出现在 css 文件中,所以建议统一使用/\ /注释。

模块注释

有时候我们需要对一个模块(一段代码块)进行功能性说明,并希望能明显区分其它代码,我们可以模块注释的方式。

注释以 /* 开始,以 */ 结束,前后空一个空格,第一行填写描述,最后一行行填写分割线。

/* 推荐的模块注释
---------------------------------------------------- */
/* 不推荐的模块注释 ---------------------------------------------------- */

文件信息注释 如果需要对一个文件进行功能性说明,方便其他人快速明白该文件的作用,推荐在文件开头(字符集说明下)写入下列注释,注释内容包括文件描述、创建人、创建时间等。

@charset "UTF-8";
/**
 * @desc 文件功能描述,方便其他人快速理解
 * @author 创建人
 * @date 创建时间
 */

覆盖规范

  • 尽可能少用 importent
  • vue 单文件组件统一使用 css/less/sass scoped
  • 每个页面/组件需要有一个全局唯一的标识 id/class,属于它下面的样式都需要加上该唯一标识
  • 避免全局修改已有样式,必须具体到页面上(通过权重)
  • 禁用全匹配*选择器(特殊情况除外,如初始化) vue单文件组件修改样式不生效可使用 /deep/ 或 >>>

媒体查询

对于内部管理系统,商务多使用 ThinkPad 笔记本,屏幕分辨率为 1366*768。建议使用Resolution Test浏览器拓展进行浏览器窗口大小调试。

常用尺寸如下

大小描述
≥1366px大屏幕 大桌面显示器
≥1200px中等屏幕 桌面显示器
≥992px中等屏幕 桌面显示器
≥768px小屏幕 平板
<768px超小屏幕 手机

优先 PC 端

默认按最大尺寸进行布局,当尺寸缩小时逐步变成移动端布局

body {
  background: gray;
}
@media screen and (max-width: 1366px) {
  body {
    background: red;
  }
}
@media screen and (max-width: 1200px) {
  body {
    background: yellow;
  }
}
@media screen and (max-width: 920px) {
  body {
    background: green;
  }
}
@media screen and (max-width: 768px) {
  body {
    background: black;
  }
}

优先移动端

默认按最小尺寸进行布局,当尺寸放大时逐步变成 PC 端布局

body {
  background: gray;
}
@media (min-width: 768px) {
  body {
    background: red;
  }
}
@media (min-width: 920px) {
  body {
    background: green;
  }
}
@media (min-width: 1200px) {
  body {
    background: yellow;
  }
}
@media (min-width: 1366px) {
  body {
    background: red;
  }
}

如果需要做打印样式进行适配,需要使用@media print

@media print {
  body {
    background: #fff;
  }
}

单位规范

CSS 单位有两种,分别是绝对单位和相对单位。

  • 常用绝对单位
    • px:像素 (计算机屏幕上的一个点)
    • cm:厘米
    • in:英寸
    • pt:磅 (1 pt 等于 1/72 英寸)
  • 常用相对单位
    • %:父元素百分比
    • vw:视口宽度百分比
    • vh:视口高度百分比
    • em:当前字体倍数
    • rem:根元素字体倍数
    • * rpx:微信小程序专用,规定屏幕宽为 750rpx 使用较多的单位有 px、%、rem 三种,建议 PC 端用 px 单位、移动端用 rem,需要具体控制尺寸还是使用 px

备注:如果需要计算不同单位下的值,可以使用 css3 方法 calc()_

兼容性规范

私有属性的使用

正是由于浏览器厂商的不同,导致了一些样式需要加前缀才生效,下面的常见的浏览器内核和前缀

浏览器内核前缀
FirefoxGecko-moz-
ChromeWebKit-webkit-
IETrident-ms-
SafariWebKit-webkit-
OperaPresto-o-
国内知名浏览器WebKit-webkit-
常见手机浏览器WebKit-webkit-

CSS3 浏览器私有前缀在前,标准前缀在后

.test {
  -webkit-border-radius: 10px;
  -moz-border-radius: 10px;
  -o-border-radius: 10px;
  -ms-border-radius: 10px;
  border-radius: 10px;
}

备注:在 webpack环境下,可以使用 postcss-loader 自动添加私有前缀_

页面跳转的方式

window.location.href方式

<script language="JavaScript" type="text/javascript">
  window.location.href="http://www.dayanmei.com/";
</script>

window.navigate方式跳转

<script language="javascript">
  window.navigate("top.jsp");
</script>

window.loction.replace方式实现页面跳转,注意跟第一种方式的区别

<script language="javascript">
  window.location.replace("http://www.dayanmei.com");
</script>

有3个jsp页面(1.jsp, 2.jsp, 3.jsp), 进系统默认的是1.jsp ,当我进入2.jsp的时候, 2.jsp里面用window.location.replace("3.jsp"); 与用window.location.href ("3.jsp");从用户界面来看是没有什么区别的, 但是当3.jsp页面有一个"返回"按钮,调用window.history.Go(-1);wondow.history.back();方法的时候, 一点这个返回按钮就要返回2.jsp页面的话,区别就出来了, 当用 window.location.replace("3.jsp");连到3.jsp页面的话,3.jsp页面中的调用 window.history.go(-1);wondow.history.back();方法是不好用的,会返回到1.jsp 。

self.location方式实现页面跳转,和下面的top.location有小小区别


<script language="JavaScript">
  self.location='top.htm';
</script>

top.location

<script language="javascript">
      top.location='xx.jsp';
</script>

不推荐这种方式跳转

<script language="javascript">
  alert("返回");
  window.history.back(-1);
</script>

在php程序中,这种方式跳转前面不能有任何输出

<?php
    header("url.php");
?>

怎么监听内容的改变:监听oninput

原生方法

  • onchange事件
<input type="text" onchange="onc(this)">
<script>
function onc(data){
    console.log(data.value);
}
</script>

onchange事件在内容改变且失去焦点的时候触发。即,失去焦点了内容未变不触发,内容变了未失去焦点也不实时触发。 js直接更改value值时不触发

  • oninput事件
<input id="inp" type="text" oninput="inp(this)">
<script>
function inp(data) {
    console.log(data.value)
}
</script>

oninput事件在输入内容改变的时候实时触发。oninput事件是IE之外的大多数浏览器支持的事件,在value改变时实时触发。 js直接更改value值时不触发

  • onpropertychange事件
    onpropertychange事件是实时触发,每增加或删除一个字符就会触发,通过js改变也会触发该事件,但是该事件是IE专有。 当input设置为disable=true后,不会触发。

  • oninput事件与onpropertychange事件的区别:
    onpropertychange事件是任何属性改变都会触发,而oninput却只在value改变时触发,oninput要通过addEventListener()来注册,onpropertychange注册方法与一般事件相同。

  • oninput与onpropertychange联合使用
    oninput 是 HTML5 的标准事件,对于检测 textarea, input:text, input:password 和 input:search 这几个元素通过用户界面发生的内容变化非常有用,在内容修改后立即被触发,不像 onchange 事件需要失去焦点才触发。oninput 事件在 IE9 以下版本不支持,需要使用 IE 特有的 onpropertychange 事件替代,这个事件在用户界面改变或者使用脚本直接修改内容两种情况下都会触发,有以下几种情况:

    • 修改了 input:checkbox 或者 input:radio 元素的选择中状态, checked 属性发生变化。
    • 修改了 input:text 或者 textarea 元素的值,value 属性发生变化。
    • 修改了 select 元素的选中项,selectedIndex 属性发生变化。
    • 在监听到 onpropertychange 事件后,可以使用 event 的 propertyName 属性来获取发生变化的属性名称。

集合 oninput & onpropertychange 监听输入框内容变化的示例代码如下:

// Firefox, Google Chrome, Opera, Safari, Internet Explorer from version 9

function OnInput (event) {
    alert ("The new content: " + event.target.value);
}

// Internet Explorer

<script>
function OnPropChanged (event) {
    if (event.propertyName.toLowerCase () == "value") {
        alert ("The new content: " + event.srcElement.value);
    }
}
</script>
<input type="text" oninput="OnInput (event)" onpropertychange="OnPropChanged (event)" value="Te">

如何触发下拉刷新、上拉加载

  1. 下拉刷新

实现下拉刷新主要分为三步:

  • 监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY
  • 监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;
  • 监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置。 html
<main>
    <p class="refreshText"></p>
    <ul id="refreshContainer">
        <li>111</li>
        <li>222</li>
        <li>333</li>
        <li>444</li>
        <li>555</li>
        ...
    </ul>
</main>

javascript

(function(window) {
    var _element = document.getElementById('refreshContainer'),
      _refreshText = document.querySelector('.refreshText'),
      _startPos = 0,
      _transitionHeight = 0;
 
    _element.addEventListener('touchstart', function(e) {
        console.log('初始位置:', e.touches[0].pageY);
        _startPos = e.touches[0].pageY;
        _element.style.position = 'relative';
        _element.style.transition = 'transform 0s';
    }, false);
 
    _element.addEventListener('touchmove', function(e) {
        console.log('当前位置:', e.touches[0].pageY);
        _transitionHeight = e.touches[0].pageY - _startPos;
 
        if (_transitionHeight > 0 && _transitionHeight < 60) {
            _refreshText.innerText = '下拉刷新';
            _element.style.transform = 'translateY('+_transitionHeight+'px)';
 
            if (_transitionHeight > 55) {
              _refreshText.innerText = '释放更新';
            }
        }               
    }, false);
 
    _element.addEventListener('touchend', function(e) {
        _element.style.transition = 'transform 0.5s ease 1s';
        _element.style.transform = 'translateY(0px)';
        _refreshText.innerText = '更新中...';
 
        // todo...
 
    }, false);
})(window);

在下拉到松手的过程中,经历了三个状态:

  • 当前手势滑动位置与初始位置差值大于零时,提示正在进行下拉刷新操作;
  • 下拉到一定值时,显示松手释放后的操作提示;
  • 下拉到达设定最大值松手时,执行回调,提示正在进行更新操作。
  1. 上拉加载

上拉加载更多数据是在页面滚动时触发的动作,一般是页面滚动到底部时触发,也可以选择在滚动到一定位置的时候触发。

以滚动到页面底部为例。实现原理是分别获得当前滚动条的scrollTop值、当前可视范围的高度值clientHeight以及文档的总高度scrollHeight。当scrollTop和clientHeight的值之和大于等于scrollHeight时,触发callback。

html

<main>
    <ul id="refreshContainer">
        <li>111</li>
        <li>222</li>
        <li>333</li>
        <li>444</li>
        <li>555</li>
        ...
    </ul>
    <p class="refreshText"></p>
</main>

javascript

(function(window) {
    // 获取当前滚动条的位置
    function getScrollTop() {
        var scrollTop = 0;
        if (document.documentElement && document.documentElement.scrollTop) {
            scrollTop = document.documentElement.scrollTop;
        } else if (document.body) {
            scrollTop = document.body.scrollTop;
        }
        return scrollTop;
    }
 
    // 获取当前可视范围的高度
    function getClientHeight() {
        var clientHeight = 0;
        if (document.body.clientHeight && document.documentElement.clientHeight) {
            clientHeight = Math.min(document.body.clientHeight, document.documentElement.clientHeight);
        }
        else {
            clientHeight = Math.max(document.body.clientHeight, document.documentElement.clientHeight);
        }
        return clientHeight;
    }
 
    // 获取文档完整的高度
    function getScrollHeight() {
        return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
    }
 
    var _text = document.querySelector('.refreshText'),
      _container = document.getElementById('refreshContainer');
 
    // 节流函数
    var throttle = function(method, context){
      clearTimeout(method.tId);
      method.tId = setTimeout(function(){
        method.call(context);
      }, 300);
    }
 
    function fetchData() {
        setTimeout(function() {
            _container.insertAdjacentHTML('beforeend', '<li>new add...</li>');
        }, 1000);
    }
 
    window.onscroll = function() {
      if (getScrollTop() + getClientHeight() == getScrollHeight()) {
          _text.innerText = '加载中...';
          throttle(fetchData);
      }
    };
 
})(window);

页面绑定onscroll事件时加入了节流函数,其作用就是忽略滚动条300毫秒内的连续多次触发。

扫描二维码登录的原理

什么是二维码

二维码又称二维条码,常见的二维码为QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。----来自百度百科

在商品上,一般都会有条形码,条形码也称为一维码,条形码只能表示一串数字。二维码要比条形码丰富很多,可以存储数字、字符串、图片、文件等,比如我们可以把www.baidu.com存储在二维码中,扫码二维码我们就可以获取到百度的地址。

可能用文字说起来还是比较难理解,您可以百度:草料二维码,一款二维码生成和解析工具,玩一玩你就知道二维码是个啥了。

  1. 移动端基于 token 的认证机制

在了解扫码登录原理之前,有必要先了解移动端基于 token 的认证机制,对理解扫码登录原理还是非常有帮助的。基于 token 的认证机制跟我们常用的账号密码认证方式有较大的不同,安全系数比账号密码要高,如果每次验证都传入账号密码,那么被劫持的概率就变大了。

基于 token 的认证机制流程图,如下图所示:

基于 token 的认证机制,只有在第一次使用需要输入账号密码,后续使用将不在输入账号密码。其实在登陆的时候不仅传入账号、密码,还传入了手机的设备信息。在服务端验证账号、密码正确后,服务端会做两件事。

第一,将账号与设备关联起来,在某种意义上,设备信息就代表着账号。

第二,生成一个 token 令牌,并且在 token 与账号、设备关联,类似于key/value,token 作为 key ,账号、设备信息作为value,持久化在磁盘上。

将 token 返回给移动端,移动端将 token 存入在本地,往后移动端都通过 token 访问服务端 API ,当然除了 token 之外,还需要携带设备信息,因为 token 可能会被劫持。带上设备信息之后,就算 token 被劫持也没有关系,因为设备信息是唯一的。

这就是基于 token 的认证机制,将账号密码换成了 token、设备信息,从而提高了安全系数,可别小看这个 token ,token 是身份凭证,在扫码登录的时候也会用到。

二维码扫码登录的原理 好了,知道了移动端基于 token 的认证机制后,接下来就进入我们的主题:二维码扫码登陆的原理。先上二维码扫码登录的流程图:

扫码登录可以分为三个阶段:待扫描、已扫描待确认、已确认。我们就一一来看看这三个阶段。

  1. 待扫描阶段

待扫描阶段也就是流程图中 1~5 阶段,即生成二维码阶段,这个阶段跟移动端没有关系,是 PC 端跟服务端的交互过程。

首先 PC 端携带设备信息想服务端发起生成二维码请求,服务端会生成唯一的二维码 ID,你可以理解为 UUID,并且将 二维码 ID 跟 PC 设备信息关联起来,这跟移动端登录有点相似。

PC 端接受到二维码 ID 之后,将二维码 ID 以二维码的形式展示,等待移动端扫码。此时在 PC 端会启动一个定时器,轮询查询二维码的状态。如果移动端未扫描的话,那么一段时间后二维码将会失效。

  1. 已扫描待确认阶段

流程图中第 6 ~ 10 阶段,我们在 PC 端登录微信时,手机扫码后,PC 端的二维码会变成已扫码,请在手机端确认。这个阶段是移动端跟服务端交互的过程。

首先移动端扫描二维码,获取二维码 ID,然后将手机端登录的信息凭证(token)和 二维码 ID 作为参数发送给服务端,此时的手机一定是登录的,不存在没登录的情况。

服务端接受请求后,会将 token 与二维码 ID 关联,为什么需要关联呢?你想想,我们使用微信时,移动端退出, PC 端是不是也需要退出,这个关联就有点把子作用了。然后会生成一个一次性 token,这个 token 会返回给移动端,一次性 token 用作确认时候的凭证。

PC 端的定时器,会轮询到二维码的状态已经发生变化,会将 PC 端的二维码更新为已扫描,请确认。

  1. 已确认

流程图中的 第 11 ~ 15 步骤,这是扫码登录的最后阶段,移动端携带上一步骤中获取的临时 token ,确认登录,服务端校对完成后,会更新二维码状态,并且给 PC 端生成一个正式的 token ,后续 PC 端就是持有这个 token 访问服务端。

PC 端的定时器,轮询到了二维码状态为登录状态,并且会获取到了生成的 token ,完成登录,后续访问都基于 token 完成。

在服务器端会跟手机端一样,维护着 token 跟二维码、PC 设备信息、账号等信息。

到此,二维码扫描登录原理就差不多了,二维码扫描登录在原理上不难理解,跟 OAuth2.0 有一丝的相似之处,但是实现起来可能就比较复杂。

页面资源预加载

preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。提供的好处主要是

  • 将加载和执行分离开,可不阻塞渲染和 document 的 onload 事件
  • 提前加载指定资源,不再出现依赖的font字体隔了一段时间才刷出

如何使用 preload

  • 使用 link 标签创建
<!-- 使用 link 标签静态标记需要预加载的资源 -->
<link rel="preload" href="/path/to/style.css" as="style">
 
<!-- 或使用脚本动态创建一个 link 标签后插入到 head 头部 -->
<script>
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/path/to/style.css';
document.head.appendChild(link);
</script>
  • 使用 HTTP 响应头的 Link 字段创建

Link: <https://example.com/other/styles.css>; rel=preload; as=style 如我们常用到的 antd 会依赖一个 CDN 上的 font.js 字体文件,我们可以设置为提前加载,以及有一些模块虽然是按需异步加载,但在某些场景下知道其必定会加载的,则可以设置 preload 进行预加载,如:

<link rel="preload" as="font"   href="https://at.alicdn.com/t/font_zck90zmlh7hf47vi.woff">
<link rel="preload" as="script" href="https://a.xxx.com/xxx/PcCommon.js">
<link rel="preload" as="script" href="https://a.xxx.com/xxx/TabsPc.js">

前端切换中英文

方法1:(中英文各做一份,然后用不同的文件夹区分开来,点击切换语言时,链接跳转到不同文件夹就行了)

优点:各自的版本是分离开来的,比较稳定,不会出现互相干扰(共用数据库资料的除外)

缺点:修改一个样式或功能,要把变更的操作(代码逻辑、更换图片、修改样式等)在所有的语言版 本上重复一次,加重了工作量

场景:个人认为符合下面2种场景可以考虑使用这种方法

*注: *如果切换的语言版本很少,并且本身网站不复杂(比如电商网站不推荐)

整体内容相对固定,布局比较简洁,扁平化,改动不会太频繁的(比如新闻类网站不推荐)

方法2:借助 jquery 插件——jquery.i18n.properties

方法3:使用微软字典整站翻译

贪吃蛇是怎么完成碰撞触发的

实现碰撞:

蛇头撞到墙,需要判断蛇头的坐标是否和墙的坐标重合 撞到自己的身体,需要判断蛇头的坐标和蛇的某一节坐标是否重合。 所以在蛇移动之后我们用蛇头的坐标去遍历蛇自己的所有坐标并且判断蛇的X或者Y是否大于或者小于地图边界了,这样就能知道是否发生碰撞,发生碰撞之后直接break不在刷新视图就好。

贪吃蛇实现思路:

  • 需要一张地图,中间是空的四周有墙体。

  • 需要一条蛇,这条蛇由蛇头和蛇身组成。

  • 需要食物,并且在蛇吃掉食物之后将蛇的身体变长,而且重新生成一个食物。

  • 蛇需要移动,这应该是最难实现的。

  • 蛇撞到墙或者撞到自己的身体就会死亡。

  • 需要能用键盘控制蛇的运动方向,这个会和蛇的移动有一些联系。

compositionStart,compositionEnd处理中文输入

问题: 在 Web 开发中,经常要对表单元素的输入进行限制,比如说不允许输入特殊字符,标点。通常我们会监听 input 事件:

inputElement.addEventListener('input', function(event) {
  let regex = /[^1-9a-zA-Z]/g;
  event.target.value = event.target.value.replace(regex, '');
  event.returnValue = false
});

这段代码在 Android 上是没有问题的,但是在 iOS 中,input 事件会截断非直接输入,什么是非直接输入呢,在我们输入汉字的时候,比如说「喜茶」,中间过程中会输入拼音,每次输入一个字母都会触发 input 事件,然而在没有点选候选字或者点击「选定」按钮前,都属于非直接输入。

解决:

这显然不是我们想要的结果,我们希望在直接输入之后才触发 input 事件,这就需要引出我要说的两个事件——compositionstart和compositionend。

compositionstart事件在用户开始进行非直接输入的时候触发,而在非直接输入结束,也即用户点选候选词或者点击「选定」按钮之后,会触发compositionend事件。

var inputLock = false;
function do(inputElement) {
  var regex = /[^1-9a-zA-Z]/g;
    inputElement.value = inputElement.value.replace(regex, '');
}
inputElement.addEventListener('compositionstart', function() {
  inputLock = true;
});
inputElement.addEventListener('compositionend', function(event) {
  inputLock = false;
  do(event.target);
})
inputElement.addEventListener('input', function(event) {
  if (!inputLock) {
    do(event.target);
    event.returnValue = false;
  }
});

添加一个 inputLock 变量,当用户未完成直接输入前,inputLock 为 true,不触发 input 事件中的逻辑,当用户完成有效输入之后,inputLock 设置为 false,触发 input 事件的逻辑。这里需要注意的一点是,compositionend 事件是在 input 事件后触发的,所以在 compositionend事件触发时,也要调用 input 事件处理逻辑

1000 万 个ip,如何做到O(1) 查找

bitmap来做这个问题。首先对数据进行预处理。定义1000 万bit位个int.在32位计算机下,一个int是32位,1000 万位的话,就需要1000 万除以32个int整数。大概有很多个。第一个int标记0-31这个数字范围的ip存不存在,比如说0000001这个ip,我就把第一个int的第1位置1。第二个int能够标记32-63这个范围的ip存不存在,以此类推。把这1000 万个ip号预处理一遍。然后计算你给我的这个ip,它是在哪个int里面,然后找到相应的数据位,看是1还是0,就能在O(1)的时间里找到

如何实现 pwa

实现:

准备工作:建议安装 [http-server]和 [ngrok]以便调试和查看。

准备一个 HTML 文件, 以及相应的 CSS 等:

<head>
  <title>Minimal PWA</title>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
  <h3>Revision 1</h3>
  <div class="main-text">Minimal PWA, open Console for more~~~</div>
</body>

添加 manifest.json 文件

为了让 PWA 应用被添加到主屏幕, 使用 manifest.json 定义应用的名称, 图标等等信息。

{
  "name": "Minimal app to try PWA",
  "short_name": "Minimal PWA",
  "display": "standalone",
  "start_url": "/",
  "theme_color": "#8888ff",
  "background_color": "#aaaaff",
  "icons": [
    {
      "src": "e.png",
      "sizes": "256x256",
      "type": "image/png"
    }
  ]
}

然后在 HTML 文件当中引入配置:

<link rel="manifest" href="manifest.json" />

添加 Service Worker

Service Worker 在网页已经关闭的情况下还可以运行, 用来实现页面的缓存和离线, 后台通知等等功能。sw.js 文件需要在 HTML 当中引入:

<script>
  if (navigator.serviceWorker != null) {
    navigator.serviceWorker.register('sw.js')
    .then(function(registration) {
      console.log('Registered events at scope: ', registration.scope);
    });
  }
</script>

后面我们会往 sw.js 文件当中添加逻辑代码。在 Service Worker 当中会用到一些全局变量:

  • self: 表示 Service Worker 作用域, 也是全局变量
  • caches: 表示缓存
  • skipWaiting: 表示强制当前处在 waiting 状态的脚本进入 activate 状态
  • clients: 表示 Service Worker 接管的页面 处理静态缓存

首先定义需要缓存的路径, 以及需要缓存的静态文件的列表, 这个列表也可以通过 Webpack 插件生成。

var cacheStorageKey = 'minimal-pwa-1'
 
var cacheList = [
  '/',
  "index.html",
  "main.css",
  "e.png"
]

借助 Service Worker, 可以在注册完成安装 Service Worker 时, 抓取资源写入缓存:

self.addEventListener('install', e => {
  e.waitUntil(
    caches.open(cacheStorageKey)
    .then(cache => cache.addAll(cacheList))
    .then(() => self.skipWaiting())
  )
})

调用 self.skipWaiting() 方法是为了在页面更新的过程当中, 新的 Service Worker 脚本能立即激活和生效。

处理动态缓存

网页抓取资源的过程中, 在 Service Worker 可以捕获到 fetch 事件, 可以编写代码决定如何响应资源的请求:

self.addEventListener('fetch', function(e) {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response != null) {
        return response
      }
      return fetch(e.request.url)
    })
  )
})

真实的项目当中, 可以根据资源的类型, 站点的特点, 可以专门设计复杂的策略。fetch 事件当中甚至可以手动生成 Response 返回给页面。

更新静态资源

缓存的资源随着版本的更新会过期, 所以会根据缓存的字符串名称(这里变量为 cacheStorageKey, 值用了 "minimal-pwa-1")清除旧缓存, 可以遍历所有的缓存名称逐一判断决决定是否清除(备注: 简化的写法, Promise.all 中 return undefined 可能出错, 见评论):

self.addEventListener('activate', function(e) {
  e.waitUntil(
    Promise.all(
      caches.keys().then(cacheNames => {
        return cacheNames.map(name => {
          if (name !== cacheStorageKey) {
            return caches.delete(name)
          }
        })
      })
    ).then(() => {
      return self.clients.claim()
    })
  )
})

在新安装的 Service Worker 中通过调用 self.clients.claim() 取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本不再控制着页面之后会被停止。

扩展:

概念:

Google 提出 PWA 的时候,并没有给它一个准确的定义,经过我们的实践和总结, PWA 它不是特指某一项技术,而是应用多项技术来改善用户体验的 Web App,其核心技术包括 Web App Manifest,Service Worker,Web Push 等,用户体验才是 PWA 的核心。

PWA 主要特点如下:

  • 可靠 - 即使在网络不稳定甚至断网的环境下,也能瞬间加载并展现

  • 用户体验 - 快速响应,具有平滑的过渡动画及用户操作的反馈

  • 用户黏性 - 和 Native App 一样,可以被添加到桌面,能接受离线通知,具有沉浸式的用户体验 PWA的特性:

  • 渐进式 - 适用于所有浏览器,因为它是以渐进式增强作为宗旨开发的

  • 连接无关性 - 能够借助 Service Worker 在离线或者网络较差的情况下正常访问

  • 类原生应用 - 由于是在 App Shell 模型基础上开发,因此应具有 Native App 的交互,给用户 Native App 的体验

  • 持续更新 - 始终是最新的,无版本和更新问题

  • 安全 - 通过 HTTPS 协议提供服务,防止窥探,确保内容不被篡改

  • 可索引 - manifest 文件和 Service Worker 可以让搜索引擎索引到,从而将其识别为『应用』

  • 黏性 - 通过推送离线通知等,可以让用户回流

  • 可安装 - 用户可以添加常用的 Web App 到桌面,免去到应用商店下载的麻烦

  • 可链接 - 通过链接即可分享内容,无需下载安装

用户体验优化

页面整体颜值和结构如何给用户好的印象?

用户第一眼看颜色,切记不是颜色越多越好看,而是要注意整体色调应该一致;不同页面相同功能操作方式应统一;图标使用统一,还有就是一个页面不能放太多的信息和模块太过密集,这样用户会反感,并且关键信息用户也发现不了。

对于checkbox选择多,全选按钮真的够用吗?

当需要选多条数据时,用户一般比较反感一条一条的选,当然全选功能能有所缓解,其实你可以再加一点亮点在里面,如双击选同类型或者其他什么属性的,这个具体可以看用户的侧重点,再具体去优化。

一个维护数据的页面,增、删、改数据后应该怎做?

这个大部分人应该都知道在操作过后默认加一个再查询的事件;当然这个你必须同项目经理或者用户讨论,毕竟这样做不是用户主观意愿去刷新页面的。

需要频繁操作,而且要多步骤的优化?

比喻你需要看一条数据的详情正常操作可能是选一条数据,再点击详情按钮;其实这样的操作我们可以进行快捷按键操作,例如我们可以双击这条数据,然后立刻勾选此数据(并清除本条外的其他勾选),并展示详情,这对用户就是亮点哦。

数据多而且复杂时,怎样才能让用户用起来欢乐?

对于数据多而复杂时,多条件组合输入查询是必须要的,当页面不能牺牲太多地方放多条件查询时,我们可以将少用的那些条件合并进行下拉选择你需要使用的条件;还有就是当填了好几个输入框时,想重新填写条件时,我们应该有一个clear按钮来清掉所有查询输入框的值;最后用户可能会有按其中一个字段进行排序的需求,可以通过点击title去实现。

查询失败时,你是怎么向用户发通知的?

在查询数据时,如果没有任何反应,情况会有很多种,为了增加辨识度,我们应该在get查询时采用异步方式,当数据没有完全渲染之前,用一张gif图片loading显示在页面,让用户知道正在查询;当查询数据为空的,返回数据可能在msg中告诉了你,可能也没有,我建议在前端判断,当数据数组长度为0时,告诉用户没有查询到数据。

部分页面不同用户需要注重的字段不同?

我遇到过此类问题,由于共用字段多,不能兼顾显示多者关注的字段,所以我选择了让用户自己做决定显示什么字段;我让用户自己选择显示什么字段,怎么排序,宽度多少等都让用户解决,此处注意,最好在用户编辑的时候,让他看到及时的效果。

当用户提出提出的优化需要牺牲性能时,怎么处理?

如果用户针对一些极少出现情况而需要牺牲性能来优化时,可以自行斟酌拒绝,因为这样没有考虑到其他大多数使用情况,这就是浪费;如果是常用的功能应该去实现,这个要具体问题具体对待。

当有很多输入框需要用户输入时,我们能做什么?

我们针对有固定关联的一些字段可以实现自动填写或者自动生成一部分,尽量让用户少动手,因为用户就是‘懒’,你能让用户可以懒起来用你的产品,那么 你就赢了。

当用户新增数据有特殊需要时,例如:批量、同类型新增?

批量新增时,可以通过前台上传文件,让后台去处理;当需要在前台批量新增时,我们应该先了解到批量肯定是要流水的,怎么流水就要看用户需求了,大部分肯定是让关键字段批量生成流水号,然后按要求数量进行新增,这里关键字段肯定后缀需要数字结尾,然后在输入框后面留一个小输入框来输入批量的数量; 如果是同类型新增,我们可以使用勾选一个同类型,进行复制新增,然后让用户自行在复制的数据上进行部分修改,免去了大部分相同的输入。

在一些子任务新增时,相同的字段都填一样的,怎么让用户欢乐操作?

当有一个大任务下,要新增多个小任务时,而且是一行一条的这种最明显,第一种比喻操作日期都是今天,我可以只填写第一个,然后在当前输入框一个回车或者双击,让下面相同的字段都写入相同的字段内容;如果从上至下都不同时,用户数据如果在excel中时,可以通过复制->粘贴->回车,直接将多条数据规整的填入多个输入框。

数据展示优化

优化方案:

  • 减少 HTTP 请求

  • 使用 HTTP2:解析速度快、多路复用、首部压缩、优先级高、可以对不同的流的流量进行精确控制、服务器可以对一个客户端请求发送多个响应

  • 使用服务端渲染:首屏渲染快,SEO 好

  • 静态资源使用 CDN:在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间

  • 将 CSS 放在文件头部,JavaScript 文件放在底部:所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件

  • 使用字体图标 iconfont 代替图片图标:不会失真,生成的文件特别小。

  • 善用缓存,不重复加载相同的资源:为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires 。

  • 压缩文件:压缩文件可以减少文件下载时间

  • 图片优化:

    • 图片延迟加载
    • 响应式图片
    • 调整图片大小
    • 降低图片质量
    • 尽可能利用 CSS3 效果代替图片
    • 使用 webp 格式的图片
  • 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码

  • 减少重绘重排

静态代码扫描,如何设计

静态代码扫描存在的价值

  • 研发过程,发现BUG越晚,修复的成本越大
  • 缺陷引入的大部分是在编码阶段,但发现的更多是在单元测试、集成测试、功能测试阶段
  • 统计证明,在整个软件开发生命周期中,30% 至 70% 的代码逻辑设计和编码缺陷是可以通过静态代码分析来发现和修复的

以上三点证明了,静态代码扫描在整个安全开发的流程中起着十分关键的作用,且实施这件事情的时间点需要尽量前移,因为扫描的节点左移能够大幅度的降低开发以及修复的成本,能够帮助开发人减轻开发和修复的负担,许多公司在推行静态代码扫描工具的时候会遇到大幅度的阻力,这方面阻力主要来自于开发人员,由于工具能力的有限性,会产生大量的误报,这就导致了开发人员很可能在做BUG确认的工作时花费了大量的无用时间。因此选择一款合适的静态代码分析工具变得尤为重要,合适的工具能够真正达到降低开发成本的效果。

静态代码分析理论基础和主要技术

静态代码分析原理分为两种:分析源代码编译后的中间文件(如Java的字节码);分析源文件。主要分析技术如下:

  • 缺陷模式匹配
    事先从代码分析经验中收集足够多的共性缺陷模式,将待分析代码与已有的共性缺陷模式进行匹配,从而完成软件安全分析。优点:简单方便;缺点:需要内置足够多的缺陷模式,容易产生误报。
  • 类型推断/类型推断
    类型推断技术是指通过对代码中运算对象类型进行推理,从而保证代码中每条语句都针对正确的类型执行。
  • 模型检查
    建立于有限状态自动机的概念基础上。将每条语句产生的影响抽象为有限状态自动机的一个状态,再通过分析有限状态机达到分析代码目的。 校验程序并发等时序特性。
  • 数据流分析
    从程序代码中收集程序语义信息,抽象成控制流图,可以通过控制流图,不必真实的运行程序,可以分析发现程序运行时的行为。

我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。

解决一:普通方法

var t;
function foo() {
    if (t) return t;
    t = new Date()
    return t;
}

问题有两个,一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断。

解决二:闭包

我们很容易想到用闭包避免污染全局变量。

var foo = (function() {
    var t;
    return function() {
        if (t) return t;
        t = new Date();
        return t;
    }
})();

然而还是没有解决调用时都必须进行一次判断的问题。

解决三:函数对象

函数也是一种对象,利用这个特性,我们也可以解决这个问题。

function foo() {
    if (foo.t) return foo.t;
    foo.t = new Date();
    return foo.t;
}

依旧没有解决调用时都必须进行一次判断的问题。

解决四:惰性函数

不错,惰性函数就是解决每次都要进行判断的这个问题,解决原理很简单,重写函数。

var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
};

解析 URL Params 为对象

题目:

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 结果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true, // 未指定值得 key 约定为 true
}
*/

答案:

function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
  const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
  let paramsObj = {};
  // 将 params 存到对象中
  paramsArr.forEach(param => {
    if (/=/.test(param)) { // 处理有 value 的参数
      let [key, val] = param.split('='); // 分割 key 和 value
      val = decodeURIComponent(val); // 解码
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
 
      if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 如果对象没有这个 key,创建 key 并设置值
        paramsObj[key] = val;
      }
    } else { // 处理没有 value 的参数
      paramsObj[param] = true;
    }
  })
 
  return paramsObj;
}

数据结构处理

题目:

有一个祖先树状 json 对象,当一个人有一个儿子的时候,其 child 为其儿子对象,如果有多个儿子,child 为儿子对象的数组。

请实现一个函数,找出这个家族中所有有多个儿子的人的名字(name),输出一个数组。

// 样例数据
let data = {
  name: 'jack',
  child: [
    { name: 'jack1' },
    {
      name: 'jack2',
      child: [{
        name: 'jack2-1',
        child: { name: 'jack2-1-1' }
      }, {
        name: 'jack2-2'
      }]
    },
    {
      name: 'jack3',
      child: { name: 'jack3-1' }
    }
  ]
}

答案:

用递归

function findMultiChildPerson(data) {
  let nameList = [];
 
  function tmp(data) {
    if (data.hasOwnProperty('child')) {
      if (Array.isArray(data.child)) {
        nameList.push(data.name);
        data.child.forEach(child => tmp(child));
      } else {
        tmp(data.child);
      }
    }
  }
  tmp(data);
  return nameList;
}

非递归


function findMultiChildPerson(data) {
  let list = [data];
  let nameList = [];
 
  while (list.length > 0) {
    const obj = list.shift();
    if (obj.hasOwnProperty('child')) {
      if (Array.isArray(obj.child)) {
        nameList.push(obj.name);
        list = list.concat(obj.child);
      } else {
        list.push(obj.child);
      }
    }
  }
  return nameList;
}