Skip to content
On this page

前端实战案例


数组模拟dictionary与二重排序

  1. 参考链接:

    js 字典排序JS sort()排序及 JS sort()双重排序前 K 个高频单词

  2. 详解:

    • 关于 sort

      • sort([function(a,b){return ±num}])

      • 不传 function 则按照字典序升序排序,传 function 则按数字大小排序,负数为升序排序,正数为降序排序

        • 注意:

          上面的字典序指按 unicode 编码序,若要按拼音序,需要 str.sort (function(a,b){return a.localeCompare(b)})

          如果需要对象属性按拼音排序,可先用Object.keys,把key排序好,再赋值到新对象

      • sort 按照字典序降序排序方法,通过比较 if(a>b),返回正数还是负数,控制排序顺序

      • 多重排序也可通过 if 判断,返回正负数控制排序顺序,其中 if 里面的参数可以与原数组 array1 无关,如 array2[a]>array2[b]

    • 样例

      统计词频,按照词频降序排序,词频相同按照字典序升序排序

      javascript
      let words = [
        "plpaboutit",
        "jnoqzdute",
        "sfvkdqf",
        "mjc",
        "nkpllqzjzp",
        "foqqenbey",
        "ssnanizsav",
        "nkpllqzjzp",
        "sfvkdqf",
        "isnjmy",
        "pnqsz",
        "hhqpvvt",
        "fvvdtpnzx",
        "jkqonvenhx",
        "cyxwlef",
        "hhqpvvt",
        "fvvdtpnzx",
        "plpaboutit",
        "sfvkdqf",
        "mjc",
        "fvvdtpnzx",
        "bwumsj",
        "foqqenbey",
        "isnjmy",
        "nkpllqzjzp",
        "hhqpvvt",
        "foqqenbey",
        "fvvdtpnzx",
        "bwumsj",
        "hhqpvvt",
        "fvvdtpnzx",
        "jkqonvenhx",
        "jnoqzdute",
        "foqqenbey",
        "jnoqzdute",
        "foqqenbey",
        "hhqpvvt",
        "ssnanizsav",
        "mjc",
        "foqqenbey",
        "bwumsj",
        "ssnanizsav",
        "fvvdtpnzx",
        "nkpllqzjzp",
        "jkqonvenhx",
        "hhqpvvt",
        "mjc",
        "isnjmy",
        "bwumsj",
        "pnqsz",
        "hhqpvvt",
        "nkpllqzjzp",
        "jnoqzdute",
        "pnqsz",
        "nkpllqzjzp",
        "jnoqzdute",
        "foqqenbey",
        "nkpllqzjzp",
        "hhqpvvt",
        "fvvdtpnzx",
        "plpaboutit",
        "jnoqzdute",
        "sfvkdqf",
        "fvvdtpnzx",
        "jkqonvenhx",
        "jnoqzdute",
        "nkpllqzjzp",
        "jnoqzdute",
        "fvvdtpnzx",
        "jkqonvenhx",
        "hhqpvvt",
        "isnjmy",
        "jkqonvenhx",
        "ssnanizsav",
        "jnoqzdute",
        "jkqonvenhx",
        "fvvdtpnzx",
        "hhqpvvt",
        "bwumsj",
        "nkpllqzjzp",
        "bwumsj",
        "jkqonvenhx",
        "jnoqzdute",
        "pnqsz",
        "foqqenbey",
        "sfvkdqf",
        "sfvkdqf",
      ];
      let dictionary = new Array();
      for (let i = 0; i < words.length; i++) {
        if (!dictionary[words[i]]) {
          dictionary[words[i]] = 1;
        } else {
          dictionary[words[i]]++;
        }
      }
      let result = Object.keys(dictionary).sort((a, b) => {
        if (dictionary[a] == dictionary[b]) {
          if (a > b) {
            return 1;
          } else {
            return -1;
          }
        }
        return dictionary[b] - dictionary[a];
      });
      for (let value of result) {
        console.log(value, dictionary[value]);
      }
      

前端截图上传服务器实现

  1. 参考链接:

    浅析 js 实现网页截图的两种方式

  2. 详解:

    • canvas 思路:(html2canvas)
    text
    将dom转换成canvas图片。
    
    递归取出目标模版的所有DOM节点,填充到一个rederList,并附加是否为顶层元素/包含内容的容器 等信息
    
    通过z-index postion float等css属性和元素的层级信息将rederList排序,计算出一个canvas的renderQueue
    
    遍历renderQueue,将css样式转为setFillStyle可识别的参数,依据nodeType调用相对应canvas方法,如文本则调用fillText,图片drawImage,设置背景色的div调用fillRect等
    
    将画好的canvas填充进页面
    

    优缺点:复杂度高,稳定性强。

    • svg 思路:(rasterizeHTML.js)
    text
    svg的标签里有个foreignObject标签,可以加载其它命名空间的xml(xhtml)文档,只需要将要渲染的DOM扔进<foreignObject></foreignObject>,利用Blob构建svg图像。
    通过一系列的hack技巧替我们绕过了许多限制:
    1.将<img/>的url 转为 dataURI
    2.将background-color从style中取出,修改url后重新插入样式表
    3.将link的的样式通过ajax down下来然后注入<style></sytle>
    

    优缺点:简单,只能对已经存在的静态资源进行处理,而对 js 动态生成并不能实时处理。

    • 上传
    javascript
    var fd = new FormData();
    fd.append("img", imgBlob);
    $.ajax({
      type: "POST",
      url: "http://tmpfile.coding.io/img",
      dataType: "json",
      data: fd,
      crossDomain: true,
      processData: false,
      contentType: false,
      success: function (data) {
        if (data && data.path) {
          console.log("http://tmpfile.coding.io/tmp" + data.path);
        }
      },
    });
    

日历的实现

  1. 参考链接:

    Date

  2. 详解:

    • new Date()

      用法:

      • new Date();
      • new Date(value);
      • new Date(dateString);
      • new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);

      new Date(this.selectedYear,this.selectedMonth,0)会获得当月最后一天的日期

    • set 方法

    凡是 set 方法,传入数超出合理范围,会自动转为毫秒处理,再使用 get 获取信息,因此日月的加减不需要处理进位和退位问题。

    • 6x7 的日历显示

      • 新开长度为 42 的数组

      • 因为第一行一定会出现本月的数字,于是 getDay 计算本月 1 号时星期几,确定 1 号位于数组的位置

      • 从 1 号开始填充本月进数组

      • 通过日期加减,把数组剩余位置填满

      • 设置 ul 宽度,使 li 每 7 格换一次行

    javascript
    // 根据给定日期算出星期
    getDay(date){
        return new Date(date).getDay();
    }
    
    // 获取某月的天数
    getMonthNumber(){
        let d = new Date(this.selectedYear,this.selectedMonth,0);
        let num = d.getDate();
        return num;
    }
    
    // 获取某一天的昨天和明天
    // date 代表指定的日期,格式:2018-09-27
    // day 传-1表始前一天,传1表始后一天
    getNextDate(date,day) {
        var dd = new Date(date);
        dd.setDate(dd.getDate() + day);
        var y = dd.getFullYear();
        var m = dd.getMonth() + 1 < 10 ? "0" + (dd.getMonth() + 1) : dd.getMonth() + 1;
        var d = dd.getDate() < 10 ? "0" + dd.getDate() : dd.getDate();
        return y + "-" + m + "-" + d;
    }
    
    // 获取日历中某一天的昨天和明天的数字
    getNextDayNumber(date,day){
        var dd = new Date(date);
        dd.setDate(dd.getDate() + day);
        var d = dd.getDate();
        return d;
    }
    

图片懒加载

  1. 参考链接

    js 实现图片懒加载原理

    图片懒加载原理及实现

  2. 详解

    • 描述

      一个网页包含大量图片,并发加载会影响渲染速度和占用带宽,如果改为可视区域加载,则能优化性能。

    • 原理

      图片是否加载取决于 img 标签的 src,先不给 src 赋值,等到进入可视区域再赋值,这时候才请求图片

    • 思路

      1. loading 图片
      2. 判断可视区域:$img.offset().top <= $(window).height()+$(window).scrollTop() 元素距离顶部的距离<=可视区域高度+窗口滚动距离
      3. 替换图片
    • 实现

      html
      &lt;div class="imgList">
        &lt;img class="lazy" src="img/loading.gif" data-src="img/pic1" alt="pic" />
        &lt;img class="lazy" src="img/loading.gif" data-src="img/pic2" alt="pic" />
        &lt;img class="lazy" src="img/loading.gif" data-src="img/pic3" alt="pic" />
      &lt;/div>
      &lt;script>
        $(() => {
          let lazyload = () => {
            for (let i = 0; i &lt; $(".lazy").length; i++) {
              if (
                $(".lazy").eq(i).offset().top &lt;=
                $(window).height() + $(window).scrollTop()
              ) {
                $(".lazy").eq(i).attr("src", $(".lazy").eq(i).data("src"));
              }
            }
          };
          lazyload();
          $(window).on("scroll", function () {
            lazyload();
          });
        });
      &lt;/script>
      

获取URL参数

  1. 参考链接

    【干货】私藏的这些高级工具函数,你拥有几个?

    正则获取 URL 参数

  2. 详解

    • 获取指定 URL 参数
    javascript
    function getUrlParams(name) {
      //(^|&)从头开始或匹配字符&,([^&]*)匹配不是&的任何内容,(&|$)遇到下一个&或者结束
      //在正则表达式中,增加一个()代表着匹配数组中增加一个值, 因此代码中的正则匹配后数组中应包含4个值(完整匹配+3个括号)
      var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
      var r = window.location.search.substr(1).match(reg);
      if (r != null) return unescape(r[2]); //获取([^&]*)的结果
      return null;
    }
    
    window.location = "http://www.baidu.com?name=elephant&age=25&sex=male";
    var name = getUrlParams("name"); //elephant
    var age = getUrlParams("age"); //25
    var sex = getUrlParams("sex"); //male
    
    • 获取所有的 URL 参数
    javascript
    function parse_url(_url) {
      //定义函数
      var pattern = /(\w+)=(\w+)/gi; //定义正则表达式
      var parames = {}; //定义数组
      url.replace(pattern, function (a, b, c) {
        //替换函数(完整匹配+2个括号)
        parames[b] = c;
      });
      return parames; //返回这个数组.
    }
    
    var url = "http://www.baidu.com?name=elephant&age=25&sex=male";
    var params = parse_url(url); // ["name=elephant", "age=25", "sex=male"]
    
    • URLSearchParams和URL(IE不支持)
    javascript
    const urlSP = new URLSearchParams(location.search);
    function getQueryString(key){
        return urlSP.get(key)
    }
    
    const urlObj = new URL(location.href);
    function getQueryString(key){
        return urlObj.searchParams.get(key)
    }
    
    //测试地址: /index.html?pid=10
    
    const log = console.log;
    getQueryString
    
    log("pid", getQueryString("pid"));  // pid 10
    log("cid", getQueryString("cid"));  // cid null
    

js操作cookie

  1. 参考链接:
  1. 详解:
  • 获取
javascript
function getCookie(name) {
  var arr,
    reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
  if ((arr = document.cookie.match(reg))) return unescape(arr[2]);
  else return null;
}
  • 设置/添加
javascript
function setCookie(name, value) {
  var Days = 30;
  var exp = new Date();
  exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
  document.cookie =
    name + "=" + escape(value) + ";expires=" + exp.toGMTString();
}
  • 更新
javascript
function updateCookie(name, value) {
  var exp = new Date();
  exp.setTime(exp.getTime() - 1);
  var currentValue = getCookie(name);
  if (currentValue != null) {
    document.cookie =
      name + "=" + escape(value) + ";expires=" + exp.toGMTString();
  }
}
  • 删除
javascript
function delCookie(name) {
  var exp = new Date();
  exp.setTime(exp.getTime() - 1);
  var cval = getCookie(name);
  if (cval != null)
    document.cookie = name + "=" + cval + ";expires=" + exp.toGMTString();
}

文件切片上传

  1. 参考链接

    前端 h5 文件切片上传,后台 php 接收切片并合并

  2. 详解

    没法实现错误重传,中途断网则中断,因为:

    1. 重传则快速递归 ajax 卡死浏览器,没法实现休眠 3 秒(js 单线程原因)来使 ajax 间隔开,因为 settimeout 没法同步执行,await 也没用
    2. 如果改为保存状态,把后面的执行完,再回头执行,也无法避免快速递归卡死的情况
    3. 无法通过服务器响应来阻塞程序,因为断网服务器不会有响应

    解决的办法:上传文件前,通过文件名(文件名相同,文件不同,则自行负责)向服务器询问是否有此文件的片段,有则返回序号,从序号开始继续分片上传。

    上传内容:

    1. 文件名
    2. 分片名
    3. 文件二进制流
    4. 分片序号
    5. 分片大小
    6. (总文件大小)
    7. (分片总数)
    html
    &lt;input id="in" type="file" />
    &lt;script>
      $(function () {
        let pieceSize = 10;
        var totalSize = 0;
    
        $("#in").on("change", function () {
          handleFiles(this.files);
        });
    
        async function handleFiles(fileList) {
          var i = 0;
          while (i &lt; fileList.length) {
            console.log("=================================================");
            console.log(
              "开始处理第" +
                i +
                "个文件, 文件是" +
                fileList[i]["name"] +
                "大小是:" +
                fileList[i]["size"]
            );
            var targetFile = fileList[i];
            totalSize += targetFile.size;
            await uploadFile(targetFile, i);
            i++;
            if (i == fileList.length) return;
          }
        }
    
        async function uploadFile(targetFile, index) {
          //console.log(targetFile);
          var tmp = targetFile.name.split(".");
          //var filename = "file-" + guid() + '.' + tmp[tmp.length - 1];
          var fileSize = targetFile.size;
          var total = Math.ceil(fileSize / pieceSize);
    
          await handle();
    
          async function handle() {
            var i = 0;
            var start = (end = 0);
            while (i &lt; total) {
              end = start + pieceSize;
    
              if (end >= fileSize) {
                end = fileSize;
              }
    
              console.log(
                "文件的index:" + index + "| 处理文件切片 i:" + i,
                "start:" + start,
                "end:" + end
              );
              var frag = targetFile.slice(start, end);
    
              var filename = "file-" + i + "." + tmp[tmp.length - 1];
    
              await send(filename, frag, i, total, function () {
                console.log(
                  "文件的index:" + index + "| 切片上传完成 回调 res111",
                  i
                );
              });
    
              start = end;
              i++;
            }
          }
        }
    
        //send
        async function send(filename, frag, index, total, cb) {
          var formData = new FormData();
          var fragname = "frag-" + index;
    
          formData.append("filename", filename);
          formData.append("fragname", fragname);
          formData.append("file", frag);
          formData.append("fragindex", index);
          formData.append("total", total);
    
          await $.ajax({
            url: "/cms/test1",
            type: "POST",
            cache: false,
            data: formData,
            processData: false,
            contentType: false,
          })
            .done(function (res) {
              //console.log('res:' + index);
              cb && cb();
            })
            .fail(function (res) {});
        }
      });
    &lt;/script>
    

大文件切片并行下载

  1. 参考链接

JavaScript 中如何实现大文件并行下载?

HTTP之HEAD请求

文件下载,搞懂这9种场景就够了

  1. 详解
  • range范围下载

    • http范围请求range

      如果在响应中存在 Accept-Ranges 首部(并且它的值不为 “none”),那么表示该服务器支持范围请求。

      在一个 Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。

      如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。

      假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。

      服务器允许忽略 Range 首部,从而返回整个文件,状态码用 200 。

      range请求头

      text
      Range: bytes=0-50, 100-150, 150-200
      Range: &lt;unit>=&lt;range-start>-&lt;range-end>, &lt;range-start>-&lt;range-end>, ...
      range-end如果不存在,表示此范围一直延伸到文档结束。
      
    • head请求

      HEAD方法与GET类似,但是HEAD并不返回消息体。

      这种方法可以用来获取请求中隐含的元信息,而无需传输实体本身。

      这个方法经常用来:

      1. 测试超链接的有效性,可用性和最近修改。
      2. 检查网页是否被串改。
      3. 获取下载文件大小。
      4. 获取网页的标志信息,获取rss种子信息,或者传递安全认证信息等。
    • 样例

    html
    &lt;!DOCTYPE html>
    &lt;html lang="zh-cn">
    
    &lt;head>
      &lt;meta charset="UTF-8" />
      &lt;meta http-equiv="X-UA-Compatible" content="IE=edge" />
      &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" />
      &lt;title>多线程下载示例&lt;/title>
      &lt;script>
        function concatenate(arrays) {
          if (!arrays.length) return null;
          let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
          let result = new Uint8Array(totalLength);
          let length = 0;
          for (let array of arrays) {
            result.set(array, length);
            length += array.length;
          }
          return result;
        }
    
        function getContentLength(url) {
          return new Promise((resolve, reject) => {
            let xhr = new XMLHttpRequest();
            xhr.open("HEAD", url);// 发送 HEAD 请求
            xhr.send();
            xhr.onload = function () {
              resolve(
                // xhr.getResponseHeader("Accept-Ranges") === "bytes" &&
                ~~xhr.getResponseHeader("Content-Length") // 获取当前 url 对应文件的内容长度
              );
            };
            xhr.onerror = reject;
          });
        }
    
        function getBinaryContent(url, start, end, i) {
          return new Promise((resolve, reject) => {
            try {
              let xhr = new XMLHttpRequest();
              xhr.open("GET", url, true);
              xhr.setRequestHeader("range", `bytes=${start}-${end}`); // 请求头上设置范围请求信息
              xhr.responseType = "arraybuffer"; // 设置返回的类型为arraybuffer
              xhr.onload = function () {
                resolve({
                  index: i, // 文件块的索引
                  buffer: xhr.response, // 范围请求对应的数据
                });
              };
              xhr.send();
            } catch (err) {
              reject(new Error(err));
            }
          });
        }
    
        function saveAs({ name, buffers, mime = "application/octet-stream" }) {
          const blob = new Blob([buffers], { type: mime });
          const blobUrl = URL.createObjectURL(blob);
          const a = document.createElement("a");
          a.download = name || Math.random();
          a.href = blobUrl;
          a.click();
          URL.revokeObjectURL(blob);
        }
    
        // poolLimit(数字类型):表示限制的并发数;
        // array(数组类型):表示任务数组;
        // iteratorFn(函数类型):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数。
        async function asyncPool(poolLimit, array, iteratorFn) {
          const ret = []; // 存储所有的异步任务
          const executing = []; // 存储正在执行的异步任务
          for (const item of array) {
            // 调用iteratorFn函数创建异步任务
            const p = Promise.resolve().then(() => iteratorFn(item, array));
            ret.push(p); // 保存新的异步任务
            console.log(`ret:${ret},executing:${executing}`);
            // 当poolLimit值小于或等于总任务个数时,进行并发控制
            if (poolLimit &lt;= array.length) {
              // 当任务完成后,从正在执行的任务数组中移除已完成的任务
              const e = p.then(() => executing.splice(executing.indexOf(e), 1));
              executing.push(e); // 保存正在执行的异步任务
              console.log(`e:${e}`);
              if (executing.length >= poolLimit) {
                await Promise.race(executing); // 等待较快的任务执行完成
              }
            }
          }
          return Promise.all(ret);
        }
    
        // url(字符串类型):预下载资源的地址;
        // chunkSize(数字类型):分块的大小,单位为字节;
        // poolLimit(数字类型):表示限制的并发数。
        async function download({ url, chunkSize, poolLimit = 1 }) {
          const contentLength = await getContentLength(url);
          const chunks = typeof chunkSize === "number" ? Math.ceil(contentLength / chunkSize) : 1;
          console.log(`contentLength:${contentLength},chunkSize:${chunkSize},chunks:${chunks}`);
          const results = await asyncPool(
            poolLimit,
            [...new Array(chunks).keys()],
            (i) => {
              let start = i * chunkSize;
              let end = i + 1 == chunks ? contentLength - 1 : (i + 1) * chunkSize - 1;
              console.log(`start:${start},end:${end}`);
              return getBinaryContent(url, start, end, i);
            }
          );
          console.log(`results${results}`)
          const sortedBuffers = results.map((item) => new Uint8Array(item.buffer));
          console.log(`sortedBuffers${sortedBuffers}`)
          return concatenate(sortedBuffers);
        }
      &lt;/script>
    &lt;/head>
    
    &lt;body>
      &lt;p>文件地址:&lt;input type="text" id="fileUrl" value="" />&lt;/p>
      &lt;div>
        &lt;h3>多线程下载&lt;/h3>
        &lt;button onclick="multiThreadedDownload()">多线程下载&lt;/button>
      &lt;/div>
      &lt;script>
        function multiThreadedDownload() {
          const url = document.querySelector("#fileUrl").value;
          if (!url || !/https?/.test(url)) return;
          console.log("多线程下载开始: " + +new Date());
          download({
            url,
            chunkSize: 2 * 1024 * 1024,
            poolLimit: 6,
          }).then((buffers) => {
            console.log("多线程下载结束: " + +new Date());
            saveAs({ buffers, name: "我的压缩包", mime: "application/zip" });
          });
        }
      &lt;/script>
    &lt;/body>
    
    &lt;/html>
    
    • 样例2

    range

    前端代码

    html
    &lt;h3>范围下载示例&lt;/h3>
    &lt;button onclick="download()">下载&lt;/button>
    &lt;script>
      async function download() {
        try {
          let rangeContent = await getBinaryContent(
            "http://localhost:3000/file.txt",
            0, 100, "text"
          );
          const blob = new Blob([rangeContent], {
            type: "text/plain;charset=utf-8",
          });
          saveAs(blob, "hello.txt");
        } catch (error) {
          console.error(error);
        }
      }
    
      function getBinaryContent(url, start, end, responseType = "arraybuffer") {
        return new Promise((resolve, reject) => {
          try {
            let xhr = new XMLHttpRequest();
            xhr.open("GET", url, true);
            xhr.setRequestHeader("range", `bytes=${start}-${end}`);
            xhr.responseType = responseType;
            xhr.onload = function () {
              resolve(xhr.response);
            };
              xhr.send();
          } catch (err) {
              reject(new Error(err));
          }
        });
      }
    &lt;/script>
    

    服务端代码

    javascript
    const Koa = require("koa");
    const cors = require("@koa/cors");
    const serve = require("koa-static");
    const range = require("koa-range");
    
    const PORT = 3000;
    const app = new Koa();
    
    // 注册中间件
    app.use(cors());
    app.use(range);
    app.use(serve("."));
    
    app.listen(PORT, () => {
      console.log(`应用已经启动:http://localhost:${PORT}/`);
    });
    
  • 大文件分块下载

    使用这个库来实现并发控制:asyncPool

    示例:big-file

  • chunked 下载

    分块传输编码主要应用于传输大量的数据,在请求在没有被处理完之前无法获得响应的长度,如从数据库中查询获得的数据生成一个大的 HTML 表格,或者需要传输大量的图片

    Transfer-Encoding 和 Content-Length 这两个字段是互斥的,也就是说响应报文中这两个字段不能同时出现。

    响应头配置 Transfer-Encoding

    text
    Transfer-Encoding: chunked
    Transfer-Encoding: gzip, chunked
    
    • 分块传输的编码规则

      • 每个分块包含分块长度和数据块两个部分;
      • 分块长度使用 16 进制数字表示,以 \r\n 结尾;
      • 数据块紧跟在分块长度后面,也使用 \r\n 结尾,但数据不包含 \r\n;
      • 终止块是一个常规的分块,表示块的结束。不同之处在于其长度为 0,即 0\r\n\r\n。
    • 样例

      chunked

      前端代码

      html
      &lt;h3>chunked 下载示例&lt;/h3>
      &lt;button onclick="download()">下载&lt;/button>
      &lt;script>
        const chunkedUrl = "http://localhost:3000/file?filename=file.txt";
      
        function download() {
          return fetch(chunkedUrl)
            .then(processChunkedResponse)
            .then(onChunkedResponseComplete)
            .catch(onChunkedResponseError);
        }
      
        function processChunkedResponse(response) {
          let text = "";
          let reader = response.body.getReader();
          let decoder = new TextDecoder();
      
          return readChunk();
      
          function readChunk() {
            return reader.read().then(appendChunks);
          }
      
          function appendChunks(result) {
            let chunk = decoder.decode(result.value || new Uint8Array(), {
              stream: !result.done,
            });
            console.log("已接收到的数据:", chunk);
            console.log("本次已成功接收", chunk.length, "bytes");
            text += chunk;
            console.log("目前为止共接收", text.length, "bytes\n");
            if (result.done) {
              return text;
            } else {
              return readChunk();
            }
          }
        }
      
        function onChunkedResponseComplete(result) {
          let blob = new Blob([result], {
            type: "text/plain;charset=utf-8",
          });
          saveAs(blob, "hello.txt");
        }
      
        function onChunkedResponseError(err) {
          console.error(err);
        }
      &lt;/script>
      

      服务端代码

      javascript
      const fs = require("fs");
      const path = require("path");
      const Koa = require("koa");
      const cors = require("@koa/cors");
      const Router = require("@koa/router");
      
      const app = new Koa();
      const router = new Router();
      const PORT = 3000;
      
      router.get("/file", async (ctx, next) => {
        const { filename } = ctx.query;
        const filePath = path.join(__dirname, filename);
        ctx.set({
          "Content-Type": "text/plain;charset=utf-8",
        });
        ctx.body = fs.createReadStream(filePath);
      });
      
      // 注册中间件
      app.use(async (ctx, next) => {
        try {
          await next();
        } catch (error) {
          // ENOENT(无此文件或目录):通常是由文件操作引起的,这表明在给定的路径上无法找到任何文件或目录
          ctx.status = error.code === "ENOENT" ? 404 : 500;
          ctx.body = error.code === "ENOENT" ? "文件不存在" : "服务器开小差";
        }
      });
      app.use(cors());
      app.use(router.routes()).use(router.allowedMethods());
      
      app.listen(PORT, () => {
        console.log(`应用已经启动:http://localhost:${PORT}/`);
      });
      
  • 其它非切片下载

    • base64 格式下载

      base64

      前端代码

      html
      &lt;h3>base64 下载示例&lt;/h3>
      &lt;img id="imgPreview" src="./static/body.png" />
      &lt;select id="picSelect">
        &lt;option value="body">body.png&lt;/option>
        &lt;option value="eyes">eyes.png&lt;/option>
        &lt;option value="mouth">mouth.png&lt;/option>
      &lt;/select>
      &lt;button onclick="download()">下载&lt;/button>
      &lt;script>
      const picSelectEle = document.querySelector("#picSelect");
      const imgPreviewEle = document.querySelector("#imgPreview");
      
      picSelectEle.addEventListener("change", (event) => {
        imgPreviewEle.src = "./static/" + picSelectEle.value + ".png";
      });
      
      const request = axios.create({
        baseURL: "http://localhost:3000",
        timeout: 60000,
      });
      
      function base64ToBlob(base64, mimeType) {
        let bytes = window.atob(base64);
        let ab = new ArrayBuffer(bytes.length);
        let ia = new Uint8Array(ab);
        for (let i = 0; i &lt; bytes.length; i++) {
          ia[i] = bytes.charCodeAt(i);
        }
        return new Blob([ab], { type: mimeType });
      }
      
      async function download() {
        const response = await request.get("/file", {
          params: {
            filename: picSelectEle.value + ".png",
          },
        });
        if (response && response.data && response.data.code === 1) {
          const fileData = response.data.data;
          const { name, type, content } = fileData;
          const imgBlob = base64ToBlob(content, type);
          saveAs(imgBlob, name);
        }
      }
      &lt;/script>
      

      服务端代码

      javascript
      // base64/file-server.js
      const fs = require("fs");
      const path = require("path");
      const mime = require("mime");
      const Koa = require("koa");
      const cors = require("@koa/cors");
      const Router = require("@koa/router");
      
      const app = new Koa();
      const router = new Router();
      const PORT = 3000;
      const STATIC_PATH = path.join(__dirname, "./static/");
      
      router.get("/file", async (ctx, next) => {
        const { filename } = ctx.query;
        const filePath = STATIC_PATH + filename;
        const fileBuffer = fs.readFileSync(filePath);
        ctx.body = {
          code: 1,
          data: {
            name: filename,
            type: mime.getType(filename),
            content: fileBuffer.toString("base64"),
          },
        };
      });
      
      // 注册中间件
      app.use(async (ctx, next) => {
        try {
          await next();
        } catch (error) {
          ctx.body = {
            code: 0,
            msg: "服务器开小差",
          };
        }
      });
      app.use(cors());
      app.use(router.routes()).use(router.allowedMethods());
      
      app.listen(PORT, () => {
        console.log(`应用已经启动:http://localhost:${PORT}/`);
      });
      
    • 附件形式下载

      在服务端下载的场景中,设置 Content-Disposition 响应头来指示响应的内容以何种形式展示,是以内联(inline)的形式,还是以附件(attachment)的形式下载并保存到本地。

      text
      Content-Disposition: inline
      Content-Disposition: attachment
      Content-Disposition: attachment; filename="mouth.png"
      

      在 HTTP 表单的场景下, Content-Disposition 也可以作为 multipart body 中的消息头

      text
      Content-Disposition: form-data
      Content-Disposition: form-data; name="fieldName"
      Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
      

      attachment

      javascript
      // attachment/file-server.js
      const fs = require("fs");
      const path = require("path");
      const Koa = require("koa");
      const Router = require("@koa/router");
      
      const app = new Koa();
      const router = new Router();
      const PORT = 3000;
      const STATIC_PATH = path.join(__dirname, "./static/");
      
      // http://localhost:3000/file?filename=mouth.png
      router.get("/file", async (ctx, next) => {
        const { filename } = ctx.query;
        const filePath = STATIC_PATH + filename;
        const fStats = fs.statSync(filePath);
        ctx.set({
          "Content-Type": "application/octet-stream",
          "Content-Disposition": `attachment; filename=${filename}`,
          "Content-Length": fStats.size,
        });
        ctx.body = fs.createReadStream(filePath);
      });
      
      // 注册中间件
      app.use(async (ctx, next) => {
        try {
          await next();
        } catch (error) {
          // ENOENT(无此文件或目录):通常是由文件操作引起的,这表明在给定的路径上无法找到任何文件或目录
          ctx.status = error.code === "ENOENT" ? 404 : 500;
          ctx.body = error.code === "ENOENT" ? "文件不存在" : "服务器开小差";
        }
      });
      app.use(router.routes()).use(router.allowedMethods());
      
      app.listen(PORT, () => {
        console.log(`应用已经启动:http://localhost:${PORT}/`);
      });
      
    • Zip 下载

      JSZip可供前端压缩文件,可压缩上传和下载压缩

      Zip

      html
      &lt;h3>Zip 下载示例&lt;/h3>
      &lt;div>
        &lt;img src="../images/body.png" />
        &lt;img src="../images/eyes.png" />
        &lt;img src="../images/mouth.png" />
      &lt;/div>
      &lt;button onclick="download()">打包下载&lt;/button>
      &lt;script>
      const images = ["body.png", "eyes.png", "mouth.png"];
      const imageUrls = images.map((name) => "../images/" + name);
      
      async function download() {
        let zip = new JSZip();
        Promise.all(imageUrls.map(getFileContent)).then((contents) => {
          contents.forEach((content, i) => {
            zip.file(images[i], content);
          });
          zip.generateAsync({ type: "blob" }).then(function (blob) {
            saveAs(blob, "material.zip");
          });
        });
      }
      
      // 从指定的url上下载文件内容
      function getFileContent(fileUrl) {
        return new JSZip.external.Promise(function (resolve, reject) {
          // 调用jszip-utils库提供的getBinaryContent方法获取文件内容
          JSZipUtils.getBinaryContent(fileUrl, function (err, data) {
            if (err) {
              reject(err);
            } else {
              resolve(data);
            }
          });
        });
      }
      &lt;/script>
      
    • FileSaver 下载

      FileSaver

      它是 HTML5 版本的 saveAs() FileSaver 实现,支持大多数主流的浏览器,非常适合在客户端上生成文件的 Web 应用程序

      可以使用它提供的 saveAs 方法来保存文件

      javascript
      FileSaver saveAs(
        Blob/File/Url, //支持 Blob/File/Url 三种类型
        optional DOMString filename, //文件名(可选)
        optional Object { autoBom }//配置对象(可选)自动提供 Unicode 文本编码提示,则需要设置 { autoBom: true}
      )
      

      保存文本

      javascript
      let blob = new Blob(["大家好,我是阿宝哥!"], { type: "text/plain;charset=utf-8" });
      saveAs(blob, "hello.txt");
      

      保存线上资源

      javascript
      //同域:使用 a[download] 方式下载
      //跨域:使用 同步的 HEAD 请求 来判断是否支持 CORS 机制
        //支持:数据下载并使用 Blob URL 实现文件下载
        //不支持:尝试使用 a[download] 方式下载
      saveAs("https://httpbin.org/image", "image.jpg");
      

      保存 canvas 画布内容:canvas.toBlob() 方法并非在所有浏览器中都可用,兼容处理canvas-toBlob.js

      javascript
      let canvas = document.getElementById("my-canvas");
      canvas.toBlob(function(blob) {
        saveAs(blob, "abao.png");
      });
      

      file-saver

      javascript
      function download() {
        if (!imgDataUrl) {
          alert("请先合成图片");
          return;
        }
        const imgBlob = dataUrlToBlob(imgDataUrl, "image/png");
        saveAs(imgBlob, "face.png");
      }
      
    • showSaveFilePicker API 下载

      兼容性要求较高(chrome 86+)

      save-file-picker

      javascript
      async function saveFile(blob, filename) {
        try {
          const handle = await window.showSaveFilePicker({
            suggestedName: filename,
            types: [
              {
                description: "PNG file",
                accept: {
                  "image/png": [".png"],
                },
              },
              {
                description: "Jpeg file",
                accept: {
                  "image/jpeg": [".jpeg"],
                },
              },
            ],
          });
          const writable = await handle.createWritable();
          await writable.write(blob);
          await writable.close();
          return handle;
        } catch (err) {
          console.error(err.name, err.message);
        }
      }
      
      function download() {
        if (!imgDataUrl) {
          alert("请先合成图片");
          return;
        }
        const imgBlob = dataUrlToBlob(imgDataUrl, "image/png");
        saveFile(imgBlob, "face.png");
      }
      
    • a 标签下载

      a-tag

      javascript
      function dataUrlToBlob(base64, mimeType) {
        let bytes = window.atob(base64.split(",")[1]);
        let ab = new ArrayBuffer(bytes.length);
        let ia = new Uint8Array(ab);
        for (let i = 0; i &lt; bytes.length; i++) {
          ia[i] = bytes.charCodeAt(i);
        }
        return new Blob([ab], { type: mimeType });
      }
      
      // 保存文件
      function saveFile(blob, filename) {
        const a = document.createElement("a");
        a.download = filename;
        a.href = URL.createObjectURL(blob);
        a.click();
        URL.revokeObjectURL(a.href)
      }
      

长列表优化

  1. 参考链接:

    简洁、巧妙、高效的长列表,无限下拉方案

    vue-virtual-scroll-list 源代码

    Intersection Observer

  2. 详解

    • 两个要素

      1. intersection observer

        • 作用

          IntersectionObserver 接口 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根

          异步查询元素相对于其他元素或视窗的位置,消除了昂贵的 DOM 查询和样式读取成本。兼容性较差,需要 polyfill。

        • 场景

          1. 页面滚动时的懒加载实现。
          2. 无限下拉(本文的实现)。
          3. 监测某些广告元素的曝光情况来做相关数据统计。
          4. 监测用户的滚动行为是否到达了目标位置来实现一些交互逻辑(比如视频元素滚动到隐藏位置时暂停播放)。
      2. padding

    • 长列表优化思路

      • 监听一个固定长度列表的首尾元素是否进入视窗;
      • 更新当前页面内渲染的第一个元素对应的序号;
      • 根据上述序号,获取目标数据元素,列表内容重新渲染成对应内容;
      • 容器 padding 调整,模拟滚动实现。

      核心:利用父元素的 padding 去填充随着无限下拉而本该有的、越来越多的 DOM 元素,仅仅保留视窗区域上下一定数量的 DOM 元素来进行数据渲染。

    • 实现

      1. 监听一个固定长度列表的首尾元素是否进入视窗

        javascript
        // 观察者创建
        this.observer = new IntersectionObserver(callback, options);
        
        // 观察列表第一个以及最后一个元素
        this.observer.observe(this.firstItem);
        this.observer.observe(this.lastItem);
        
        //当他们其中一个重新进入视窗时,callback 函数就会触发
        const callback = (entries) => {
          entries.forEach((entry) => {
            if (entry.target.id === firstItemId) {
              // 当第一个元素进入视窗
            } else if (entry.target.id === lastItemId) {
              // 当最后一个元素进入视窗
            }
          });
        };
        
      2. 更新当前页面渲染的第一个元素对应的序号 (firstIndex)

        • 用一个数组来维护需要渲染到页面中的数据。数组的长度会随着不断请求新的数据而不断变大,而渲染的始终是其中一定数量的元素,比如 20 个

        • 最开始渲染的是数组中序号为 0 - 19 的元素,即此时对应的 firstIndex 为 0

        • 当序号为 19 的元素(即上一步的 lastItem )进入视窗时,我们就会往后渲染 10 个元素,即渲染序号为 10 - 29 的元素,那么此时的 firstIndex 为 10

        • 下一次就是,当序号为 29 的元素进入视窗时,继续往后渲染 10 个元素,即渲染序号为 20 - 39 的元素,那么此时的 firstIndex 为 20,以此类推

        javascript
        // 我们对原先的 firstIndex 做了缓存
        const { currentIndex } = this.domDataCache;
        
        // 以全部容器内所有元素的一半作为每一次渲染的增量
        const increment = Math.floor(this.listSize / 2);
        
        let firstIndex;
        
        //更新 firstIndex,是为了根据页面的滚动情况,知道接下来哪些数据应该被获取、渲染
        if (isScrollDown) {
          // 向下滚动时序号增加
          firstIndex = currentIndex + increment;
        } else {
          // 向上滚动时序号减少
          firstIndex = currentIndex - increment;
        }
        
      3. 根据上述序号,获取对应数据元素,列表重新渲染成新的内容

        javascript
        //根据 firstIndex 查询数据,然后将目标数据渲染到页面上
        const renderFunction = (firstIndex) => {
          // offset = firstIndex, limit = 10 => getData
          // getData Done =>  new dataItems => render DOM
        };
        
      4. padding 调整,模拟滚动实现

        这 10 个新的数据元素,我们用原来已有的 DOM 元素去渲染,替换掉已经离开视窗、不可见的数据元素;而本该由更多 DOM 元素进一步撑开容器高度的部分,我们用 padding 填充来模拟实现。

        向下滚动

        javascript
        // padding的增量 = 每一个item的高度 x 新的数据项的数目
        const remPaddingsVal = itemHeight * Math.floor(this.listSize / 2);
        
        if (isScrollDown) {
          // paddingTop新增,填充顶部位置
          newCurrentPaddingTop = currentPaddingTop + remPaddingsVal;
        
          if (currentPaddingBottom === 0) {
            newCurrentPaddingBottom = 0;
          } else {
            // 如果原来有paddingBottom则减去,会有滚动到底部的元素进行替代
            newCurrentPaddingBottom = currentPaddingBottom - remPaddingsVal;
          }
        }
        

        向上滚动

        javascript
        // padding的增量 = 每一个item的高度 x 新的数据项的数目
        const remPaddingsVal = itemHeight * Math.floor(this.listSize / 2);
        
        if (!isScrollDown) {
          // paddingBottom新增,填充底部位置
          newCurrentPaddingBottom = currentPaddingBottom + remPaddingsVal;
        
          if (currentPaddingTop === 0) {
            newCurrentPaddingTop = 0;
          } else {
            // 如果原来有paddingTop则减去,会有滚动到顶部的元素进行替代
            newCurrentPaddingTop = currentPaddingTop - remPaddingsVal;
          }
        }
        

        最后是 padding 设置更新以及相关缓存数据更新

        javascript
        // 容器padding重新设置
        this.updateContainerPadding({
          newCurrentPaddingBottom,
          newCurrentPaddingTop,
        });
        
        // DOM元素相关数据缓存更新
        this.updateDomDataCache({
          currentPaddingTop: newCurrentPaddingTop,
          currentPaddingBottom: newCurrentPaddingBottom,
        });
        
      • 优势

        把同步触发的滚动事件变为异步,无需做防抖

      • 缺陷

        1. padding 计算依赖列表项固定高度
        2. 数据请求过程需要 loading 效果
        3. 需要兼容用户难以预测的滚动行为
    • 其它库

      • iScroll

        scroll 事件监听,translate 上下移,视窗外元素插入队尾,循环队列,无限下拉

活动倒计时

  1. 参考链接:

    js 计算两个时间时间差(天时分秒)

    js 计算两个时间差 年月日时分秒

  2. 详解

    1. 方法概述

      计算时间 getTime()的时间戳差值,换算为年月日时分秒,setInterval 调用一次逻辑

    2. 实现

      javascript
      var future = "2017-04-04";
      
      var calculationTime = function (future) {
        var s1 = new Date(future.replace(/-/g, "/")),
          s2 = new Date(),
          runTime = parseInt((s1.getTime() - s2.getTime()) / 1000);
        var year = Math.floor(runTime / 86400 / 365);
        runTime = runTime % (86400 * 365);
        var month = Math.floor(runTime / 86400 / 30);
        runTime = runTime % (86400 * 30);
        var day = Math.floor(runTime / 86400);
        runTime = runTime % 86400;
        var hour = Math.floor(runTime / 3600);
        runTime = runTime % 3600;
        var minute = Math.floor(runTime / 60);
        runTime = runTime % 60;
        var second = runTime;
        return [year, month, day, hour, minute, second];
      };
      setInterval(function () {
        var result = calculationTime(future);
        //更新视图
      }, 1000);
      

摄像头抓拍与RTC音视频会议

  1. 参考链接:

    js 调用摄像头拍照上传图片

    getUserMedia API 及 HTML5 调用摄像头和麦克风

    MediaDevices.getUserMedia` undefined 的问题

    基于webrtc的音视频聊天,视频会议的实现

  2. 详解

通过 MediaDevices.getUserMedia() 获取用户多媒体权限时,需要注意其只工作于以下三种环境:

  • localhost 域
  • 开启了 HTTPS 的域
  • 使用 file:/// 协议打开的本地文件
text
其他情况下,比如在一个 HTTP 站点上,navigator.mediaDevices 的值为 undefined。

如果想要 HTTP 环境下也能使用和调试 MediaDevices.getUserMedia(),可通过开启 Chrome 的相应参数。

通过相应参数启动 Chrome
传递相应参数来启动 Chrome,以 http://example.com 为例,

--unsafely-treat-insecure-origin-as-secure="http://example.com"
开启相应 flag
通过传递相应参数来启动 Chrome Insecure origins treated as secure flag 并填入相应白名单。

打开 chrome://flags/#unsafely-treat-insecure-origin-as-secure
将该 flag 切换成 enable 状态
输入框中填写需要开启的域名,譬如 http://example.com",多个以逗号分隔。
重启后生效。
html
&lt;!DOCTYPE html>
&lt;html lang="en">
  &lt;head>
    &lt;meta charset="UTF-8" />
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" />
    &lt;meta http-equiv="X-UA-Compatible" content="ie=edge" />
    &lt;title>摄像头拍照&lt;/title>
  &lt;/head>
  &lt;body>
    &lt;video id="video" width="480" height="320" controls>&lt;/video>
    &lt;div>
      &lt;button id="open">开启摄像头&lt;/button>
      &lt;button id="close">关闭摄像头&lt;/button>
      &lt;button id="capture">拍照&lt;/button>
    &lt;/div>
    &lt;canvas id="canvas" width="480" height="320">&lt;/canvas>
    &lt;img id="img" width="480" height="320" />
    &lt;script>
      let video = document.getElementById("video");
      let canvas = document.getElementById("canvas");
      let context = canvas.getContext("2d");

      //访问用户媒体设备的兼容方法
      function getUserMedia(constraints, success, error) {
        if (navigator.mediaDevices.getUserMedia) {
          //最新的标准API
          navigator.mediaDevices
            .getUserMedia(constraints)
            .then(success)
            .catch(error);
        } else if (navigator.webkitGetUserMedia) {
          //webkit核心浏览器
          navigator.webkitGetUserMedia(constraints, success, error);
        } else if (navigator.mozGetUserMedia) {
          //firfox浏览器
          navigator.mozGetUserMedia(constraints, success, error);
        } else if (navigator.getUserMedia) {
          //旧版API
          navigator.getUserMedia(constraints, success, error);
        }
      }

      // 打开摄像头成功回调
      function success(stream) {
        //兼容webkit核心浏览器
        let CompatibleURL = window.URL || window.webkitURL;
        //将视频流设置为video元素的源
        console.log(stream);

        //video.src = CompatibleURL.createObjectURL(stream);
        video.srcObject = stream;
        video.play();
      }

      // 打开摄像头失败回调
      function error(error) {
        console.log(`访问用户媒体设备失败${error.name}, ${error.message}`);
      }

      // 关闭摄像头
      function closeMedia() {
        let stream = video.srcObject;
        let tracks = stream.getTracks();

        tracks.forEach(function (track) {
          track.stop();
        });

        video.srcObject = null;
      }

      // 开启摄像头
      function openMedia() {
        if (
          navigator.mediaDevices.getUserMedia ||
          navigator.getUserMedia ||
          navigator.webkitGetUserMedia ||
          navigator.mozGetUserMedia
        ) {
          //调用用户媒体设备, 访问摄像头
          getUserMedia({ video: { width: 480, height: 320 } }, success, error);
        } else {
          alert("不支持访问用户媒体");
        }
      }

      document.getElementById("open").addEventListener("click", function () {
        openMedia();
      });

      document.getElementById("close").addEventListener("click", function () {
        closeMedia();
      });

      document.getElementById("capture").addEventListener("click", function () {
        context.drawImage(video, 0, 0, 480, 320);
        let src = canvas.toDataURL("image/png");
        document.getElementById("img").src = src; //上传src部分(base64)即可
        // $.ajax({
        //   url:"...",
        //   type:"post",
        //   data:{"imgData":src},
        //   success:function(data){
        //     console.log(data);
        //   },
        //   error:function(){
        //     console.log("服务端异常!");
        //   }
        // });
      });
    &lt;/script>
  &lt;/body>
&lt;/html>

采集本地摄像头和麦的媒体数据

javascript
navigator.mediaDevices.getUserMedia({video: true, audio: true}).then((stream) => {
  //这里的stream就是我们想要的视频流和音频流的集合了
  //如果要在本地预览视频和音频,则只需要在html中添加一个video标签,将stream流赋给video标签即可,代码如下:
  let localVideo = document.getElementById('video标签的id');
  localVideo.srcObject = stream;
  localVideo.muted = true;
  localVideo.play();
})

RTCPeerConnection

javascript

let rtcPeerConnection = new RTCPeerConnection({
  "iceServers":[{
      "url": "stun:stun.l.google.com:19302"
      },{
      "url": "turn:服务器IP", 
      "credential":"密码",
      "username":"账号"
  }]
});
//监听stun返回的NAT穿透信息:ICECandidate
rtcPeerConnection.onicecandidate = (event) => {
  //如果获取到了我的NAT穿透消息,则立马通过websocket将event.candidate传送到对方。一旦双方通过candidate成功建立连接,就会通过下面这个监听进行音视频流的传输。
  wx.send(event.candidate)
}
//监听传输轨道的传输数据
rtcPeerConnection.ontrack = (event) => {
  //通过event.streams就能获取到对方传送过来的音视频流,将他再添加到另一个video标签上,就是可以看到对方的画面啦
  let remoteVideo = document.getElementById("对方video标签的id");
  remoteVideo.srcObject = event.streams[0];
  remoteVideo.play();
}
//这里的localVideo是知识点解析的第1步获取到的本地音视频video标签,将他的流加入到传输轨道,连接一旦建立就立马触发ontrack事件,将本地的音视频流传到对方。
for (const track of localVideo.stream.getTracks()) {
  rtcPeerConnection.addTrack(track, this.videos[0].stream);
}

向对方发起offer

javascript
rtcPeerConnection.createOffer({iceRestart: true, offerToReceiveAudio: true, offerToReceiveVideo: true}).then(
  (sessionDescription) => {
    rtcPeerConnection.setLocalDescription(sessionDescription)
    //这里还需要将sessionDescription通过websocket发送给对方,对方才能收到视频邀请
    wx.send(sessionDescription)
  }
)

如果对方收到了offer,那么就设置对方sessionDescription,并回传一个应答answer

javascript
//这里的sessionDescription是通过offer传过来的
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(sessionDescription));
rtcPeerConnection.createAnswer({iceRestart: true, offerToReceiveAudio: true, offerToReceiveVideo: true}).then(
  (sessionDescription) => {
    rtcPeerConnection.setLocalDescription(sessionDescription)
    //这里将sessionDescription通过websocket回传给邀请方
    wx.send(sessionDescription)
  }
)

发送方收到了answer应答后也同样设置对方sessionDescription

javascript
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(sessionDescription));

当STUN服务器获取到icecandidate后并通过websocket传过来了,我们就需要将对方candidate设置一下,对应第一步的onicecandidate监听事件

javascript
rtcPeerConnection.addIceCandidate(candidate)

js加解密哈希编码

  1. 参考链接:

    js中常见的数据加密与解密的方法

    js中使用btoa和atob进行Base64的编码和解码

    JS实现Base64编码、解码,即window.atob,window.btoa功能

  2. 详解

html
&lt;script src="https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.js">&lt;/script>
&lt;script src="https://cdn.bootcss.com/jsencrypt/3.0.0-beta.1/jsencrypt.js">&lt;/script>
&lt;script src="https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.js">&lt;/script>
&lt;script type="text/javascript">
    //md5
    var hash = md5("111111"); // "96e79218965eb72c92a549dd5a330112"
    console.log(hash);

    //RSA
    //公钥
    var PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCADB+zg4Ou3fv6rY8159gw4fkJbuMPeM41ttw20leKjSKQWOgBixHJjXbkRvoMmUQkWq67xWzpMgKB7t8LIJx+n0dLP+6YDqbfFEJJ2i1Va4U1yJyGht0bEW0tpadKX3i5JwUwQIBPiC7VSWhtVyAKtzTYeun/fqpxTDAbulrj4QIDAQAB';
    //私钥
    var PRIVATE_KEY = 'MIICWwIBAAKBgQCADB+zg4Ou3fv6rY8159gw4fkJbuMPeM41ttw20leKjSKQWOgBixHJjXbkRvoMmUQkWq67xWzpMgKB7t8LIJx+n0dLP+6YDqbfFEJJ2i1Va4U1yJyGht0bEW0tpadKX3i5JwUwQIBPiC7VSWhtVyAKtzTYeun/fqpxTDAbulrj4QIDAQABAoGACj/Y2m0orBAfvHvfrpBtc9LlX2sX/g6M7wFr6hrMdWOBBJiL5Z0PTO39D3Ow +IjcyqN+62UiUnOK04IJKiJaSa1HNWagW2aAOblca1lYyYD6wlUotMV3bgk9lly0dD0lUTd8XWOmo1NdTEFW7y1OB4pYgMcT+iv4o0cr4sAtWisCQQCD6EmjEpMI5dcfZcrSXbT+WQGvdVCjAhivVMbNYeZq37ARt+9mTnaoA6Ss/QGQ5qvO9jMhx8x9/e8EfA+AX2rzAkEA+II3IXRXY3xbjDnK84kunlWpImH6XofN2V/TGEH1/Iqa909PHhuL4mhSt0iC70/y1g5kbmXyXE5s5gEsPqmC2wJAAU9uY9NMaJs33tT5Bcvuf1RNAvwsV+Iucpdp/iJJ0qf0LMjh9Oc0oIiguyMsP886x6yEZ4J/koTSOf4tfT31ZwJAMs28I5S7QNVtic9O1FbZNvlgKG1LWAP/a08RwsXJWiWj5KdMD2WmRVT6hAnI6s+3X1d15LPmxkQqMyNOPkk9PQJAJyPGWOjrCjzwojE0lN4NtS9brx6JbPy/sFkHX5LN8Xv45+XOKp14JgRcABTfWfvnnoWoWKha2cyJFlf8AdCIuQ==';
    //使用公钥加密
    var encrypt = new JSEncrypt();
      //encrypt.setPrivateKey('-----BEGIN RSA PRIVATE KEY-----'+PRIVATE_KEY+'-----END RSA PRIVATE KEY-----');
    encrypt.setPublicKey('-----BEGIN PUBLIC KEY-----' + PUBLIC_KEY + '-----END PUBLIC KEY-----');
    var encrypted = encrypt.encrypt('ceshi01');
    console.log('加密后数据:%o', encrypted);
    //使用私钥解密
    var decrypt = new JSEncrypt();
    //decrypt.setPublicKey('-----BEGIN PUBLIC KEY-----' + PUBLIC_KEY + '-----END PUBLIC KEY-----');
    decrypt.setPrivateKey('-----BEGIN RSA PRIVATE KEY-----'+PRIVATE_KEY+'-----END RSA PRIVATE KEY-----');
    var uncrypted = decrypt.decrypt(encrypted);
    console.log('解密后数据:%o', uncrypted);

    //AES
    var aseKey = "12345678"     //秘钥必须为:8/16/32位
    var message = "我是一个密码";
    //加密 DES/AES切换只需要修改 CryptoJS.AES &lt;=> CryptoJS.DES
    var encrypt = CryptoJS.AES.encrypt(message, CryptoJS.enc.Utf8.parse(aseKey), {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7
    }).toString();
    console.log(encrypt); // 0Gh9NGnwOpgmB525QS0JhVJlsn5Ev9cHbABgypzhGnM
    //解密
    var decrypt = CryptoJS.AES.decrypt(encrypt, CryptoJS.enc.Utf8.parse(aseKey), {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7
    }).toString(CryptoJS.enc.Utf8);
    console.log(decrypt); // 我是一个密码 

    //base64
    window.btoa('&lt;script src="test.js">&lt;/script>'); 
    window.atob("PHNjcmlwdCBzcmM9InRlc3QuanMiPjwvc2NyaXB0Pg=="); 
    //不能使用window的情况
    var Base64 = {
      _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
      _utf8_encode: function(string) {
        string = string.replace(/\r\n/g,"\n");
        let utftext = "";
        for (let n = 0; n &lt; string.length; n++) {
          let c = string.charCodeAt(n);
          if (c &lt; 128) {
            utftext += String.fromCharCode(c);
          } else if((c > 127) && (c &lt; 2048)) {
            utftext += String.fromCharCode((c >> 6) | 192);
            utftext += String.fromCharCode((c & 63) | 128);
          } else {
            utftext += String.fromCharCode((c >> 12) | 224);
            utftext += String.fromCharCode(((c >> 6) & 63) | 128);
            utftext += String.fromCharCode((c & 63) | 128);
          }

        }
        return utftext;
      },
      _utf8_decode: function(utftext) {
        let string = "";
        let i = 0;
        let c = 0;
        let c1 = 0;
        let c2 = 0;
        let c3 = 0;
        while ( i &lt; utftext.length ) {
          c = utftext.charCodeAt(i);
          if (c &lt; 128) {
            string += String.fromCharCode(c);
            i++;
          } else if((c > 191) && (c &lt; 224)) {
            c2 = utftext.charCodeAt(i+1);
            string += String.fromCharCode(((c & 31) &lt;&lt; 6) | (c2 & 63));
            i += 2;
          } else {
            c2 = utftext.charCodeAt(i+1);
            c3 = utftext.charCodeAt(i+2);
            string += String.fromCharCode(((c & 15) &lt;&lt; 12) | ((c2 & 63) &lt;&lt; 6) | (c3 & 63));
            i += 3;
          }
        }
        return string;
      },
      encode: function(input) {
        let output = "";
        let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
        let i = 0;
        input = this._utf8_encode(input);
        while (i &lt; input.length) {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);
            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) &lt;&lt; 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) &lt;&lt; 2) | (chr3 >> 6);
            enc4 = chr3 & 63;
            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            } else if (isNaN(chr3)) {
                enc4 = 64;
            }
            output = output +
            this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
            this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
        }
        return output;
      },
      decode: function(input) {
        let output = "";
        let chr1, chr2, chr3;
        let enc1, enc2, enc3, enc4;
        let i = 0;
        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
        while (i &lt; input.length) {
            enc1 = this._keyStr.indexOf(input.charAt(i++));
            enc2 = this._keyStr.indexOf(input.charAt(i++));
            enc3 = this._keyStr.indexOf(input.charAt(i++));
            enc4 = this._keyStr.indexOf(input.charAt(i++));
            chr1 = (enc1 &lt;&lt; 2) | (enc2 >> 4);
            chr2 = ((enc2 & 15) &lt;&lt; 4) | (enc3 >> 2);
            chr3 = ((enc3 & 3) &lt;&lt; 6) | enc4;
            output = output + String.fromCharCode(chr1);
            if (enc3 != 64) {
                output = output + String.fromCharCode(chr2);
            }
            if (enc4 != 64) {
                output = output + String.fromCharCode(chr3);
            }
        }
        output = this._utf8_decode(output);
        return output;
      }
    }
    // 定义字符串
    var string = 'Hello World!';
    // 加密
    var encodedString = Base64.encode(string);
    console.log(encodedString); // 输出: "SGVsbG8gV29ybGQh"
    // 解密
    var decodedString = Base64.decode(encodedString);
    console.log(decodedString); // 输出: "Hello World!"
&lt;/script>

文件内容读取

  1. 参考链接:

  2. 详解

    需要在 input 手动选择文件,GB2312 编码支持简体中文字,UTF-8 支持简体中文字、繁体中文字、英文、日文、韩文等语言,如出现中文乱码,可切换编码。

    html
    &lt;!DOCTYPE html>
    &lt;html lang="en">
      &lt;head>
        &lt;meta charset="UTF-8" />
        &lt;title>Document&lt;/title>
        &lt;script type="text/javascript">
          function upload(input) {
            //支持chrome IE10
            if (window.FileReader) {
              var file = input.files[0];
              filename = file.name.split(".")[0];
              var reader = new FileReader();
              reader.onload = function () {
                console.log(this.result, this);
              };
              reader.readAsText(file, "gb2312");
            }
            //支持IE 7 8 9 10
            else if (typeof window.ActiveXObject != "undefined") {
              var xmlDoc;
              xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
              xmlDoc.async = false;
              xmlDoc.load(input.value);
              console.log(xmlDoc.xml);
            }
            //支持FF
            else if (
              document.implementation &&
              document.implementation.createDocument
            ) {
              var xmlDoc;
              xmlDoc = document.implementation.createDocument("", "", null);
              xmlDoc.async = false;
              xmlDoc.load(input.value);
              console.log(xmlDoc.xml);
            } else {
              alert("error");
            }
          }
        &lt;/script>
      &lt;/head>
      &lt;body>
        &lt;input type="file" onchange="upload(this)" />
      &lt;/body>
    &lt;/html>
    

隐藏滚动条与伪元素控制

  1. 参考链接:
  1. 详解

    • CSS实现隐藏滚动条但是可以滚动

      css
      body::-webkit-scrollbar {
        display: none;
      }
      
    • JS实现隐藏滚动条但是可以滚动(不可撤销)

      javascript
      document.styleSheets[0].insertRule('body::-webkit-scrollbar{display:none}',0)
      //或
      document.styleSheets[0].addRule('body::-webkit-scrollbar','display:none')
      
    • JS实现隐藏滚动条但是可以滚动(可撤销)

      javascript
      var style = document.createElement("style");
      document.head.appendChild(style);
      style.sheet.addRule('body::-webkit-scrollbar','display:none');
      //或
      style.sheet.insertRule('body::-webkit-scrollbar{display:none}', 0);
      
      document.head.removeChild(style);
      

HTML5特性

  1. 参考链接:
  1. 详解
  • details

    向用户提供按需查看详细信息的效果

    html
    &lt;details>
      &lt;summary>Click Here to get the user details&lt;/summary>
      &lt;table>
        &lt;tr>
          &lt;th>#&lt;/th>
          &lt;th>Name&lt;/th>
          &lt;th>Location&lt;/th>
          &lt;th>Job&lt;/th>
        &lt;/tr>
        &lt;tr>
          &lt;td>1&lt;/td>
          &lt;td>Adam&lt;/td>
          &lt;td>Huston&lt;/td>
          &lt;td>UI/UX&lt;/td>
        &lt;/tr>
      &lt;/table>
    &lt;/details>
    
  • contenteditable属性

    在元素上设置以使内容可编辑的属性,适用于DIV,P,UL等元素。

    html
    &lt;h2> Shoppping List(Content Editable) &lt;/h2>
    &lt;ul class="content-editable" contenteditable="true">
        &lt;li> 1. Milk &lt;/li>
        &lt;li> 2. Bread &lt;/li>
        &lt;li> 3. Honey &lt;/li>
    &lt;/ul>
    
  • mark

    高亮

    html
    &lt;p> Did you know, you can &lt;mark>"Highlight something interesting"&lt;/mark> just with an HTML tag? &lt;/p>
    
  • map area

    定义一个图像映射(一个可点击的链接区域)。可点击的区域可以是这些形状中的任何一个,矩形,圆形或多边形区域。如果不指定任何形状,则会考虑整个图像。

    html
    &lt;div>
        &lt;img src="circus.jpg" width="500" height="500" alt="Circus" usemap="#circusmap">
    
        &lt;map name="circusmap">
            &lt;area shape="rect" coords="67,114,207,254" href="elephant.htm">
            &lt;area shape="rect" coords="222,141,318, 256" href="lion.htm">
            &lt;area shape="rect" coords="343,111,455, 267" href="horse.htm">
            &lt;area shape="rect" coords="35,328,143,500" href="clown.htm">
            &lt;area shape="circle" coords="426,409,100" href="clown.htm">
        &lt;/map>
    &lt;/div>
    
  • data-* 属性

    用于存储页面或应用程序专用的自定义数据。 可以在 JavaScript 代码中使用存储的数据来创建更多的用户体验。

    属性名不能包含任何大写字母,并且必须在前缀“data-”之后至少有一个字符

    属性值可以是任何字符串

    html
    &lt;h2> Know data attribute &lt;/h2>
    &lt;div 
        class="data-attribute" 
        id="data-attr" 
        data-custom-attr="You are just Awesome!"> 
    I have a hidden secret!
    &lt;/div>
    
  • output

    表示计算或用户操作的结果

    html
    &lt;form oninput="x.value=parseInt(a.value) * parseInt(b.value)">
      &lt;input type="number" id="a" value="0">
              * &lt;input type="number" id="b" value="0">
                    = &lt;output name="x" for="a b">&lt;/output>
    &lt;/form>
    
  • datalist

    包含了一组option元素,这些元素表示其它表单控件可选值,dataList的表现很像是一个select下拉列表,但它只是提示作用,并不限制用户在input输入框里输入什么

    html
    &lt;form action="" method="get">
        &lt;label for="fruit">Choose your fruit from the list:&lt;/label>
        &lt;input list="fruits" name="fruit" id="fruit">
            &lt;datalist id="fruits">
              &lt;option value="Apple">
              &lt;option value="Orange">
              &lt;option value="Banana">
              &lt;option value="Mango">
              &lt;option value="Avacado">
            &lt;/datalist>
        &lt;input type="submit">
    &lt;/form>  
    
  • input range

    给定一个滑块类型的范围选择器

    html
    &lt;form method="post">
        &lt;input 
            type="range" 
            name="range" 
            min="0" 
            max="100" 
            step="1" 
            value=""
            onchange="changeValue(event)"/>
    &lt;/form>
    &lt;div class="range">
          &lt;output id="output" name="result">  &lt;/output>
    &lt;/div>
    
  • meter

    用来显示已知范围的标量值或者分数值。

    html
    &lt;label for="home">/home/atapas&lt;/label>
    &lt;meter id="home" value="4" min="0" max="10">2 out of 10&lt;/meter>&lt;br>
    
    &lt;label for="root">/root&lt;/label>
    &lt;meter id="root" value="0.6">60%&lt;/meter>&lt;br>
    
  • progress

    进度条

    html
    &lt;label for="file">Downloading progress:&lt;/label>
    &lt;progress id="file" value="32" max="100"> 32% &lt;/progress>
    
  • input color

    颜色选择器

    html
    &lt;input type="color" onchange="showColor(event)">
    &lt;p id="colorMe">Color Me!&lt;/p>
    

微信扫码登录实现

  1. 参考链接:
  1. 详解

  2. 打开网页请求二维码

  3. 服务器生成uuid唯一标记和过期时间,存入redis数据库,uuid为key

  4. 服务器调用微信生成二维码接口,需要微信OAuth2.0协议参数(AppID 和 AppSecrect)

  5. 微信校验后返回二维码

  6. 手机已经登录过微信app,存在用户token,app服务器从中可以解密出userId

  7. 手机扫码发送登录请求(二维码信息+token),app服务器验证app token,通过后app服务器调用网页服务器后端回调接口,传入临时票据code,同时app服务器向app确认信息

  8. app显示登录确认框

  9. 确认后再次发送请求(二维码信息+token+ack),app服务器得到uuid和userId,redis中保存uuid-userId

  10. 接第6点,网页服务器拿到code,加上AppID和AppSecret请求微信开发平台换取access_token,则可请求获得用户账号数据

  11. 根据微信账号数据,找到注册时的用户的网站数据

Dom转图片

  1. 参考链接:
  1. 详解

  2. html2canvas

```html
&lt;!DOCTYPE html>
&lt;html lang="en">
&lt;head>
    &lt;meta charset="UTF-8">
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0">
    &lt;title>Document&lt;/title>
    &lt;style>
        #poster{
            width: 700px; 
            height: 500px;
            background-color: green;
        }
    &lt;/style>
    &lt;script type="text/javascript" src="http://html2canvas.hertzen.com/dist/html2canvas.min.js">&lt;/script>
    &lt;script type="text/javascript">
        function takeScreenshot() {
            const node = document.getElementById('poster')
            html2canvas(node, {
                useCORS: true,
                height: node.offsetHeight,
                width: node.offsetWidth,
                scrollY: 0,
                scrollX: 0
            }).then(async (canvas) => {
                let oImg = new Image();
                oImg.src = canvas.toDataURL();  // 导出图片
                document.body.appendChild(oImg);  // 将生成的图片添加到body
            })
        }
    &lt;/script>
&lt;/head>
&lt;body>
    &lt;div id="poster">
        &lt;input type="button" value="截图" onclick="takeScreenshot()">
    &lt;/div>
&lt;/body>
&lt;/html>
```

注意事项:

1. 生成的图片模糊

  导出的图片局部有些图片没有原图那么清晰,因为使用背景图片的原因。解决方法是直接使用标签

2. 生成出来的图片有白色边框

  在配置项中设置backgroundColor: null

3. 生成图片不显示

  图片素材出现跨域,在方法上调用时增加两个配置:allowTaint: true,useCORS: true 

  还有一个方法就是,把跨域的图片转为base64。

4. PNG图片不透明

  用到透明的PNG图片作为背景图,最后生成的图片却并不透明,因为html2canvas生成的canvas背景颜色默认为白色。

  添加一个配置项就好:backgroundColor: 'transparent'

5. 生成的图片加载闪动效果

  先让生成的图片隐藏,添加图片加载状态,等图片生成好以后再展示。
  1. dom-to-image
```html
&lt;!DOCTYPE html>
&lt;html lang="en">
&lt;head>
    &lt;meta charset="UTF-8">
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0">
    &lt;title>Document&lt;/title>
    &lt;style>
        #poster{
            width: 700px; 
            height: 500px;
            background-color: green;
        }
    &lt;/style>
    &lt;script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dom-to-image/2.6.0/dom-to-image.min.js">&lt;/script>
    &lt;script type="text/javascript">
        function filter (node) {
          return (node.tagName !== 'i');
        }
        function takeScreenshot() {
            const node = document.getElementById('poster')
            domtoimage.toPng(node)
            .then((dataUrl) => {
                var img = new Image();
                img.src = dataUrl;
                document.body.appendChild(img);
            })
            .catch((error) => {
                console.error('oops, something went wrong!', error);
            });
            //将节点转化为jpg格式的图片
            // domtoimage.toJpeg(node, { quality: 0.95 })
            // .then((dataUrl) => {
            //     var img = new Image();
            //     img.src = dataUrl;
            //     document.body.appendChild(img);
            // })
            // .catch((error) => {
            //     console.error('oops, something went wrong!', error);
            // });
            //将节点转化为svg格式的图片,生成的图片的格式都是base64格式
            // domtoimage.toSvg(node, { filter: filter })
            // .then((dataUrl) => {
            //     var img = new Image();
            //     img.src = dataUrl;
            //     document.body.appendChild(img);
            // })
            // .catch((error) => {
            //     console.error('oops, something went wrong!', error);
            // });
            //将节点转化为二进制格式,这个可以直接将图片下载
            // domtoimage.toBlob(node)
            // .then((blob) => {
            //   window.saveAs(blob, 'poster.png');
            // })
            // .catch((error) => {
            //     console.error('oops, something went wrong!', error);
            // });
            //获取原始像素值,以Uint8Array 数组的形式返回,每4个数组元素表示一个像素点,即rgba值。这个方法也是挺实用的,可以用于WebGL中编写着色器颜色。
            // domtoimage.toPixelData(node)
            // .then((pixels) => {
            //   for (var y = 0; y &lt; node.scrollHeight; ++y) {
            //     for (var x = 0; x &lt; node.scrollWidth; ++x) {
            //       pixelAtXYOffset = (4 * y * node.scrollHeight) + (4 * x);
            //       /* pixelAtXY is a Uint8Array[4] containing RGBA values of the pixel at (x, y) in the range 0..255 */
            //       pixelAtXY = pixels.slice(pixelAtXYOffset, pixelAtXYOffset + 4);
            //     }
            //   }
            // })
            // .catch((error) => {
            //     console.error('oops, something went wrong!', error);
            // });
        }
    &lt;/script>
&lt;/head>
&lt;body>
    &lt;div id="poster">
        &lt;input type="button" value="截图" onclick="takeScreenshot()">
    &lt;/div>
&lt;/body>
&lt;/html>
```

注意事项:

1. 用于生成图片的dom元素不能display:none或opacity:0。可以用z-index放到其它元素下面;或者用绝对定位,将其放到某个div容器的可视区之外,然后容器设置overflow:hidden
2. 用于生成图片的dom元素的父元素们不能display:none
3. 综合1、2,即用于生成图片的dom元素要本身是可见的,且能计算到尺寸值,但不需要出现在视野中
4. 页面不能出现隐藏iframe的样式,例如iframe{display:none}
5. 生成后的图,会自动追加到用于生成的dom元素后面,类名是dom2img-result,如果不想把图片的样子展示给用户,可将其样式设置为opacity:0

html5相关

  1. 参考链接:
  1. 详解

  2. 对 Html5 的离线储存资源管理和加载

* 使⽤

  1. ⻚⾯头部像下⾯⼀样加⼊⼀个 manifest 的属性;

    ```html
    &lt;!DOCTYPE HTML>
    &lt;html manifest = "cache.manifest">
    ...
    &lt;/html>
    ```

  2. 在 cache.manifest ⽂件的编写离线存储的资源

    ```text
    CACHE MANIFEST
    #v0.11

    CACHE:

    js/app.js
    css/style.css

    NETWORK:
    resourse/logo.png

    FALLBACK:
    / /offline.html
    ```
    * CACHE:表示需要离线存储的资源列表,由于包含manifest文件的页面将被自动离线存储,所以不需要把页面自身也列出来。
    * NETWORK:表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离线存储,所以在离线情况下无法使用这些资源。不过,如果在CACHE和NETWORK中有一个相同的资源,那么这个资源还是会被离线存储,也就是说CACHE的优先级更高。
    * FALLBACK:表示如果访问第一个资源失败,那么就使用第二个资源来替换他,比如上面这个文件表示的就是如果访问根目录下任何一个资源失败了,那么就去访问offline.html。

  3. 在离线状态时,操作 window.applicationCache 进⾏需求实现

    缓存立即执行
    ```javascript
    /*code1,简单粗暴的*/
    applicationCache.onupdateready = function(){
      applicationCache.swapCache();
      location.reload();
    };
    /*code2,缓存公用方法*/
    // var EventUtil = {
    // addHandler: function(element, type, handler) {
    // if (element.addEventListener) {
    // element.addEventListener(type, handler, false);
    // } else if (element.attachEvent) {
    // element.attachEvent(“on” + type, handler);
    // } else {
    // element["on" + type] = handler;
    // }
    // }
    // };
    // EventUtil.addHandler(applicationCache, “updateready”, function() { //缓存更新并已下载,要在下次进入页面生效
    // applicationCache.update(); //检查缓存manifest文件是否更新,ps:页面加载默认检查一次。
    // applicationCache.swapCache(); //交换到新的缓存项中,交换了要下次进入页面才生效
    // location.reload(); //重新载入页面
    // });
    ```

* 概念

  在线的情况下,浏览器发现 html 头部有 manifest 属性,它会请求 manifest ⽂件,如 果是第⼀次访问 app ,那么浏览器就会根据manifest⽂件的内容下载相应的资源并且进⾏ 离线存储。

  如果已经访问过 app 并且资源已经离线存储了,那么浏览器就会使⽤离线的资源加载⻚⾯,然后浏览器会对⽐新的 manifest ⽂件与旧的 manifest ⽂件,如果⽂件没有发⽣改变,就不做任何操作,如果⽂件改变了,那么就会重新下载⽂件中的资源并进⾏离线存储。

* 优势

  1. 离线浏览--用户可在离线时使用它们
  2. 速度--已经缓存的资源加载得更快
  3. 减少服务器负载--浏览器将只从服务器下载更改过的资源

* 原理和环境

  HTML5的离线存储是基于一个新建的.appcache文件的,通过这个文件上的解析清单离线存储资源,这些资源就会像cookie一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示。

  引入manifest的页面,即使没有被列入缓存清单中,仍然会被用户代理缓存。因此,把.appcache文件更新下(如头部版本号),刷新页面后才能更新离线HTML

* 注意事项

  * 站点离线存储的容量限制是5M
  * 如果manifest文件,或者内部列举的某一个文件不能正常下载,整个更新过程将视为失败,浏览器继续全部使用老的缓存
  * 引用manifest的html必须与manifest文件同源,在同一个域下
  * 在manifest中使用的相对路径,相对参照物为manifest文件
  * CACHE MANIFEST字符串应在第一行,且必不可少
  * 系统会自动缓存引用清单文件的 HTML 文件
  * manifest文件中CACHE则与NETWORK,FALLBACK的位置顺序没有关系,如果是隐式声明需要在最前面
  * FALLBACK中的资源必须和manifest文件同源
  * 当一个资源被缓存后,该浏览器直接请求这个绝对路径也会访问缓存中的资源。
  * 站点中的其他页面即使没有设置manifest属性,请求的资源如果在缓存中也从缓存中访问
  * 当manifest文件发生改变时,资源请求本身也会触发更新
  1. 严格模式与混杂模式
!DOCTYPE声明位于⽂档中的最前⾯,处于 html 标签之前。告知浏览器的解析器, ⽤什么⽂档类型 规范来解析这个⽂档

严格模式的排版和 JS 运作模式是 以该浏览器⽀持的最⾼标准运⾏

在混杂模式中,⻚⾯以宽松的向后兼容的⽅式显示。模拟⽼式浏览器的⾏为以防⽌站点⽆法⼯作。DOCTYPE 不存在或格式不正确会导致⽂档以混杂模式呈现
  1. HTML全局属性
* class :为元素设置类标识
* data-* : 为元素增加⾃定义属性
* draggable : 设置元素是否可拖拽
* id : 元素 id ,⽂档内唯⼀
* lang : 元素内容的的语⾔
* style : ⾏内 css 样式
* title : 元素相关的建议信息
  1. viewport的content属性作用
```html
&lt;meta name="viewport" content="" />
width viewport的宽度[device-width | pixel_value]width如果直接设置pixel_value数值,大部分的安卓手机不支持,但是ios支持;
height – viewport 的高度 (范围从 223 到 10,000 )
user-scalable [yes | no]是否允许缩放
initial-scale [数值] 初始化比例(范围从 > 0 到 10)
minimum-scale [数值] 允许缩放的最小比例
maximum-scale [数值] 允许缩放的最大比例
target-densitydpi 值有以下(一般推荐设置中等响度密度或者低像素密度,后者设置具体的值dpi_value,另外webkit内核已不准备再支持此属性)
     -- dpi_value 一般是70-400//没英寸像素点的个数
     -- device-dpi设备默认像素密度
     -- high-dpi 高像素密度
     -- medium-dpi 中等像素密度
     -- low-dpi 低像素密度
```

怎样处理 移动端 1px 被 渲染成 2px 问题?

* 局部处理

  mate 标签中的 viewport 属性 , initial-scale 设置为 1

  rem 按照设计稿标准⾛,外加利⽤ transfrome 的 scale(0.5) 缩⼩⼀倍即可;

* 全局处理

  mate 标签中的 viewport 属性 , initial-scale 设置为 0.5

  rem 按照设计稿标准⾛即可
  1. meta 相关
```html
&lt;!DOCTYPE html> &lt;!--H5标准声明,使⽤ HTML5 doctype,不区分⼤⼩写-->
&lt;head lang=”en”> &lt;!--标准的 lang 属性写法-->
&lt;meta charset=’utf-8′> &lt;!--声明⽂档使⽤的字符编码-->
&lt;!-- 让IE浏览器用最高级内核渲染页面 还有用 Chrome 框架的页面用webkit 内核-->
&lt;meta http-equiv=”X-UA-Compatible” content=”IE=edge,chrome=1″/> &lt;!--优先使用指定浏览器使用特定的文档模式-->
&lt;meta name=”description” content=”不超过150个字符”/> &lt;!--⻚⾯描述-->
&lt;meta name=”keywords” content=””/> &lt;!-- ⻚⾯关键词-->
&lt;meta name=”author” content=”name, [email protected]”/> &lt;!--⽹⻚作者-->
&lt;meta name=”robots” content=”index,follow”/> &lt;!--搜索引擎抓取-->
&lt;meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
&lt;meta name=”apple-mobile-web-app-title” content=”标题”> &lt;!--iOS 设备 begin-->
&lt;meta name=”apple-mobile-web-app-capable” content=”yes”/> &lt;!--添加到主屏后的标 是否启⽤ WebApp 全屏模式,删除苹果默认的⼯具栏和菜单栏-->
&lt;meta name=”apple-mobile-web-app-status-bar-style” content=”black”/>
&lt;meta name=”renderer” content=”webkit”> &lt;!-- 启⽤360浏览器的极速模式(webkit)-->
&lt;meta http-equiv=”X-UA-Compatible” content=”IE=edge”> &lt;!--避免IE使⽤兼容模式-->
&lt;meta http-equiv=”Cache-Control” content=”no-siteapp” /> &lt;!--不让百度转码-->
&lt;meta name=”HandheldFriendly” content=”true”> &lt;!--针对⼿持设备优化,主要是针对一些老的不识别viewport的浏览器-->
&lt;meta name=”MobileOptimized” content=”320″> &lt;!--微软的⽼式浏览器-->
&lt;meta name=”screen-orientation” content=”portrait”> &lt;!--uc强制竖屏-->
&lt;meta name=”x5-orientation” content=”portrait”> &lt;!--QQ强制竖屏-->
&lt;meta name=”full-screen” content=”yes”> &lt;!--UC强制全屏-->
&lt;meta name=”x5-fullscreen” content=”true”> &lt;!--QQ强制全屏-->
&lt;meta name=”browsermode” content=”application”> &lt;!--UC应⽤模式-->
&lt;meta name=”x5-page-mode” content=”app”> &lt;!-- QQ应⽤模式-->
&lt;meta name=”msapplication-tap-highlight” content=”no”> &lt;!--windows phone 设置⻚⾯不缓存-->
&lt;meta http-equiv=”pragma” content=”no-cache”>
&lt;meta http-equiv=”cache-control” content=”no-cache”>
&lt;meta http-equiv=”expires” content=”0″>
```

在线预览文件

  1. 参考链接:
  1. 详解

移动端开发指南

  1. 参考链接:
  1. 详解
  • 调用系统功能

    html
    &lt;a href="tel:10086">拨打电话给10086&lt;/a>
    &lt;a href="sms:10086">发送短信给10086&lt;/a>
    &lt;a href="mailto:[email protected]">发送邮件给[email protected]&lt;/a>
    &lt;input type="file" accept="image/*">&lt;!-- 选择指定类型文件 -->
    &lt;input type="file" multiple>&lt;!-- 多选文件 -->
    
  • 忽略自动识别

    有些移动端浏览器会自动将数字字母符号识别为电话/邮箱并将其渲染成上述调用系统功能里的a

    html
    &lt;!-- 忽略自动识别电话 -->
    &lt;meta name="format-detection" content="telephone=no">
    &lt;!-- 忽略自动识别电话和邮箱 -->
    &lt;meta name="format-detection" content="telephone=no, email=no">
    
  • 弹出数字键盘

    html
    &lt;!-- 纯数字带#和* -->
    &lt;input type="tel">
    
    &lt;!-- 纯数字 -->
    &lt;input pattern="\d*">
    
  • 唤醒原生应用

    通过location.href与原生应用建立通讯渠道,这种页面与客户端的通讯方式称为URL Scheme,其基本格式为scheme://[path][?query]

    • scheme:应用标识,表示应用在系统里的唯一标识
    • path:应用行为,表示应用某个页面或功能
    • query:应用参数,表示应用页面或应用功能所需的条件参数

    URL Scheme一般由前端与客户端共同协商。唤醒原生应用的前提是必须在移动设备里安装了该应用,有些移动端浏览器即使安装了该应用也无法唤醒原生应用,因为它认为URL Scheme是一种潜在的危险行为而禁用它,像Safari和微信浏览器。还好微信浏览器可开启白名单让URL Scheme有效。

    若在页面引用第三方原生应用的URL Schema,可通过抓包第三方原生应用获取其URL。

    html
    &lt;!-- 打开微信 -->
    &lt;a href="weixin://">打开微信&lt;/a>
    
    &lt;!-- 打开支付宝 -->
    &lt;a href="alipays://">打开支付宝&lt;/a>
    
    &lt;!-- 打开支付宝的扫一扫 -->
    &lt;a href="alipays://platformapi/startapp?saId=10000007">打开支付宝的扫一扫&lt;/a>
    
    &lt;!-- 打开支付宝的蚂蚁森林 -->
    &lt;a href="alipays://platformapi/startapp?appId=60000002">打开支付宝的蚂蚁森林&lt;/a>
    
  • 禁止页面缩放

    具体见html5相关,或css常见问题-flexible与高清屏

    html
    &lt;meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, minimum-scale=1, maximum-scale=1">
    
  • 禁止页面缓存

    解析见网络相关问题-强制缓存和协商缓存

    html
    &lt;meta http-equiv="Cache-Control" content="no-cache">
    
  • 禁止字母大写

    有时在输入框里输入文本会默认开启首字母大写纠正,就是输入首字母小写会被自动纠正成大写,直接声明autocapitalize=off关闭首字母大写功能和autocorrect=off关闭纠正功能。

    html
    &lt;input autocapitalize="off" autocorrect="off">
    
  • 浏览器配置

    具体见html5相关

    针对Safari配置

    html
    &lt;!-- 设置Safari全屏,在iOS7+无效 -->
    &lt;meta name="apple-mobile-web-app-capable" content="yes">
    
    &lt;!-- 改变Safari状态栏样式,可选default/black/black-translucent,需在上述全屏模式下才有效 -->
    &lt;meta name="apple-mobile-web-app-status-bar-style" content="black">
    
    &lt;!-- 添加页面启动占位图 -->
    &lt;link rel="apple-touch-startup-image" href="pig.jpg" media="(device-width: 375px)">
    
    &lt;!-- 保存网站到桌面时添加图标 -->
    &lt;link rel="apple-touch-icon" sizes="76x76" href="pig.jpg">
    
    &lt;!-- 保存网站到桌面时添加图标且清除默认光泽 -->
    &lt;link rel="apple-touch-icon-precomposed" href="pig.jpg">
    

    针对其他浏览器配置

    html
    &lt;!-- 强制QQ浏览器竖屏 -->
    &lt;meta name="x5-orientation" content="portrait">
    
    &lt;!-- 强制QQ浏览器全屏 -->
    &lt;meta name="x5-fullscreen" content="true">
    
    &lt;!-- 开启QQ浏览器应用模式 -->
    &lt;meta name="x5-page-mode" content="app">
    
    &lt;!-- 强制UC浏览器竖屏 -->
    &lt;meta name="screen-orientation" content="portrait">
    
    &lt;!-- 强制UC浏览器全屏 -->
    &lt;meta name="full-screen" content="yes">
    
    &lt;!-- 开启UC浏览器应用模式 -->
    &lt;meta name="browsermode" content="application">
    
    &lt;!-- 开启360浏览器极速模式 -->
    &lt;meta name="renderer" content="webkit">
    
  • 让:active有效,让:hover无效

    有些元素的:active可能会无效,而元素的:hover在点击后会一直处于点击状态,需点击其他位置才能解除点击状态。给body注册一个空的touchstart事件可将两种状态反转。

    html
    &lt;body ontouchstart>&lt;/body>
    
  • 优化弹性滚动

    在苹果系统上非body元素的滚动操作可能会存在卡顿,但安卓系统不会出现该情况。通过声明overflow-scrolling:touch调用系统原生滚动事件优化弹性滚动,增加页面滚动的流畅度。

    css
    body {
        -webkit-overflow-scrolling: touch;
    }
    .elem {
        overflow: auto;
    }
    
  • 禁止滚动传播

    移动端浏览器有一个奇怪行为:当页面包含多个滚动区域时,滚完一个区域后若还存在滚动动量则会将这些剩余动量传播到下一个滚动区域,造成该区域也滚动起来。这种行为称为滚动传播。若不想产生这种奇怪行为可直接禁止。

    css
    .elem {
        overscroll-behavior: contain;
    }
    
  • 禁止屏幕抖动

    对于一些突然出现滚动条的页面,可能会产生左右抖动的不良影响。在一个滚动容器里,打开弹窗就隐藏滚动条,关闭弹窗就显示滚动条,来回操作会让屏幕抖动起来。提前声明滚动容器的padding-right为滚动条宽度,就能有效消除这个不良影响。

    每个移动端浏览器的滚动条宽度都有可能不一致,甚至不一定占位置,通过以下方式能间接计算出滚动条的宽度。100vw为视窗宽度,100%为滚动容器内容宽度,相减就是滚动条宽度,妥妥的动态计算。

    css
    body {
        padding-right: calc(100vw - 100%);
    }
    
  • 禁止长按操作

    有时不想用户长按元素呼出菜单进行点链接、打电话、发邮件、保存图片或扫描二维码等操作,声明touch-callout:none禁止用户长按操作。

    有时不想用户复制粘贴盗文案,声明user-select:none禁止用户长按操作和选择复制。

    但声明user-select:none会让input和textarea无法输入文本,可对其声明user-select:auto排除在外。

    css
    * {
        /* pointer-events: none; */ /* 微信浏览器还需附加该属性才有效 */
        user-select: none; /* 禁止长按选择文字 */
        -webkit-touch-callout: none;
    }
    input,textarea {
        user-select: auto;
    }
    
  • 禁止字体调整

    旋转屏幕可能会改变字体大小,声明text-size-adjust:100%让字体大小保持不变。

    css
    * {
        text-size-adjust: 100%;
    }
    
  • 禁止高亮显示

    触摸元素会出现半透明灰色遮罩

    css
    * {
        -webkit-tap-highlight-color: transparent;
    }
    
  • 禁止动画闪屏

    在移动设备上添加动画,多数情况会出现闪屏,给动画元素的父元素构造一个3D环境就能让动画稳定运行了。

    css
    .elem {
        perspective: 1000;
        backface-visibility: hidden;
        transform-style: preserve-3d;
    }
    
  • 美化滚动条

    • ::-webkit-scrollbar:滚动条整体部分
    • ::-webkit-scrollbar-track:滚动条轨道部分
    • ::-webkit-scrollbar-thumb:滚动条滑块部分
    css
    ::-webkit-scrollbar {
        width: 6px;
        height: 6px;
        background-color: transparent;
    }
    ::-webkit-scrollbar-track {
        background-color: transparent;
    }
    ::-webkit-scrollbar-thumb {
        border-radius: 3px;
        background-image: linear-gradient(135deg, #09f, #3c9);
    }
    
  • placeholder颜色

    css
    input::-webkit-input-placeholder {
        color: #66f;
    }
    
  • input文本偏上问题

    桌面端浏览器里声明line-height等于height就能解决,但移动端浏览器里还是未能解决,需将line-height声明为normal才行。

    css
    input {
        line-height: normal;
    }
    
  • 下拉选项右对齐

    css
    select option {
        direction: rtl;
    }
    
  • 修复点击无效

    在苹果系统上有些情况下非可点击元素监听click事件可能会无效,针对该情况只需对不触发click事件的元素声明cursor:pointer就能解决。

    css
    .elem {
        cursor: pointer;
    }
    
  • 文本换行

    若接口返回字段包含\n或br,千万别替换掉,可声明white-space:pre-line交由浏览器做断行处理。

    css
    * {
        white-space: pre-line;
    }
    
  • 描绘一像素边框

    详见css常见问题-flexible与高清屏

    css
    .elem {
        position: relative;
        width: 200px;
        height: 80px;
        &::after {
            position: absolute;
            left: 0;
            top: 0;
            border: 1px solid #f66;
            width: 200%;
            height: 200%;
            content: "";
            transform: scale(.5);
            transform-origin: left top;
        }
    }
    
  • 溢出文本省略号

    css
    .elem {
        width: 400px;
        line-height: 30px;
        font-size: 20px;
        &.sl-ellipsis {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        &.ml-ellipsis {
            display: -webkit-box;
            overflow: hidden;
            text-overflow: ellipsis;
            -webkit-line-clamp: 3;
            -webkit-box-orient: vertical;
        }
    }
    
  • 禁止点击穿透

    当用户执行第一次单击后会预留300ms检测用户是否继续执行单击,若是则执行缩放操作,若否则执行点击操作。

    在移动端浏览器上不使用click事件而使用touch事件是因为click事件有着明显的延迟,后续又出现fastclick。该解决方案监听用户是否做了双击操作,所以还是可直接使用click事件,而点击穿透就交给该fastclick自动判断。

    javascript
    import Fastclick from "fastclick";
    
    FastClick.attach(document.body);
    
  • 禁止滑动穿透

    移动端浏览器里出现弹窗时,若在屏幕上滑动能触发弹窗底下的内容跟着滚动。

    禁止body滚动又会引发其它问题:

    • 弹窗打开后内部内容无法滚动
    • 弹窗关闭后页面滚动位置丢失
    • Webview能上下滑动露出底色
    css
    body.static {
        position: fixed;
        left: 0;
        width: 100%;
    }
    
    javascript
    const body = document.body;
    const openBtn = document.getElementById("open-btn");
    const closeBtn = document.getElementById("close-btn");
    openBtn.addEventListener("click", e => {
        e.stopPropagation();
        const scrollTop = document.scrollingElement.scrollTop;
        body.classList.add("static");
        body.style.top = `-${scrollTop}px`;
    });
    closeBtn.addEventListener("click", e => {
        e.stopPropagation();
        body.classList.remove("static");
        body.style.top = "";
    });
    
  • 支持往返刷新

    点击移动端浏览器的前进按钮或后退按钮,有时不会自动执行旧页面的JS代码,这与往返缓存有关。

    往返缓存指浏览器为了在页面间执行前进后退操作时能拥有更流畅体验的一种策略,以下简称BFCache。

    该策略具体表现为:当用户前往新页面前将旧页面的DOM状态保存在BFCache里,当用户返回旧页面前将旧页面的DOM状态从BFCache里取出并加载。大部分移动端浏览器都会部署BFCache,可大大节省接口请求的时间和带宽。

    若在Vue SPA上使用keep-alive也不能让页面刷新,可将接口请求放到beforeRouteEnter()里。

    解决方案1

    javascript
    // 在新页面监听页面销毁事件
    window.addEventListener("onunload", () => {
        // 执行旧页面代码
    });
    

    解决方案2:pageshow事件在每次页面加载时都会触发,无论是首次加载还是再次加载都会触发,这就是它与load事件的区别。pageshow事件暴露的persisted可判断页面是否从BFCache里取出。

    javascript
    window.addEventListener("pageshow", e => e.persisted && location.reload());
    
  • 解析有效日期

    在苹果系统上解析YYYY-MM-DD HH:mm:ss这种日期格式会报错Invalid Date,但在安卓系统上解析这种日期格式完全无问题。可用YYYY/MM/DD HH:mm:ss这种日期格式

    javascript
    const date = "2019-03-31 21:30:00";
    new Date(date.replace(/\-/g, "-"));
    
  • 修复高度坍塌

    出现以下三个条件时,输入框失焦后页面未回弹:

    • 页面高度过小
    • 输入框在页面底部或视窗中下方
    • 输入框聚焦输入文本

    只要保持前后滚动条偏移量一致就不会出现上述问题。在输入框聚焦时获取页面当前滚动条偏移量,在输入框失焦时赋值页面之前获取的滚动条偏移量,这样就能间接还原页面滚动条偏移量解决页面高度坍塌。

    javascript
    const input = document.getElementById("input");
    let scrollTop = 0;
    input.addEventListener("focus", () => {
        scrollTop = document.scrollingElement.scrollTop;
    });
    input.addEventListener("blur", () => {
        document.scrollingElement.scrollTo(0, this.scrollTop);
    });
    
  • 修复输入监听

    在苹果系统上的输入框输入文本,keyup/keydown/keypress事件可能会无效。当输入框监听keyup事件时,逐个输入英文和数字会有效,但逐个输入中文不会有效,需按回车键才会有效。此时可用input事件代替输入框的keyup/keydown/keypress事件。

  • 简化回到顶部

    编写一个返回顶部函数需scrollTop、定时器和条件判断三者配合才能完成。其实DOM对象里隐藏了一个很好用的函数可完成上述功能,一行核心代码就能搞定。

    • scrollIntoView

      • behavior:动画过渡效果,默认auto无,可选smooth平滑
      • inline:水平方向对齐方式,默认nearest就近对齐,可选start顶部对齐、center中间对齐和end底部对齐
      • block:垂直方向对齐方式,默认start顶部对齐,可选center中间对齐、end底部对齐和nearest就近对齐
    javascript
    const gotopBtn = document.getElementById("gotop-btn");
    openBtn.addEventListener("click", () => document.body.scrollIntoView({ behavior: "smooth" }));
    
  • 简化懒加载

    编写一个懒性加载函数也同样需scrollTop、定时器和条件判断三者配合才能完成。其实DOM对象里隐藏了一个很好用的函数可完成上述功能,该函数无需监听容器的scroll事件,通过浏览器自身机制完成滚动监听。

    懒加载

    html
    &lt;img data-src="pig.jpg">
    &lt;!-- 很多&lt;img> -->
    
    javascript
    const imgs = document.querySelectorAll("img.lazyload");
    const observer = new IntersectionObserver(nodes => {
        nodes.forEach(v => {
            if (v.isIntersecting) { // 判断是否进入可视区域
                v.target.src = v.target.dataset.src; // 赋值加载图片
                observer.unobserve(v.target); // 停止监听已加载的图片
            }
        });
    });
    imgs.forEach(v => observer.observe(v));
    

    下拉加载

    html
    &lt;ul>
        &lt;li>&lt;/li>
        &lt;!-- 很多&lt;li> -->
    &lt;/ul>
    &lt;!-- 也可将#bottom以&lt;li>的形式插入到&lt;ul>内部的最后位置 -->
    &lt;div id="bottom">&lt;/div>
    
    javascript
    const bottom = document.getElementById("bottom");
    const observer = new IntersectionObserver(nodes => {
        const tgt = nodes[0]; // 反正只有一个
        if (item.isIntersecting) {
            console.log("已到底部,请求接口");
            // 执行接口请求代码
        }
    })
    bottom.observe(bottom);
    
  • 优化扫码识别

    通常移动端浏览器都会配备长按二维码图片识别链接的功能,但长按二维码可能无法识别或错误识别。

    二维码生成方式有以下三种:

    • 使用img渲染
    • 使用svg渲染
    • 使用canvas渲染

    大部分移动端浏览器只能识别img渲染的二维码,若使用SVG和Canvas的方式生成二维码,那就想方设法把二维码数据转换成Base64再赋值到img的src上。

    一个页面可能存在多个二维码,若长按二维码只能识别最后一个,那只能控制每个页面只存在一个二维码。

  • 自动播放媒体

    少部分浏览器autoplay即可

    一般浏览器需js控制

    javascript
    const audio = document.getElementById("audio");
    const video = document.getElementById("video");
    audio.play();
    video.play();
    

    微信浏览器需监听其应用SDK加载完成才能触发

    javascript
    document.addEventListener("WeixinJSBridgeReady", () => {
        // 执行上述媒体自动播放代码
    });
    

    苹果系统上明确规定用户交互操作开始后才能播放媒体,未得到用户响应会被Safari自动拦截,因此需监听用户首次触摸操作并触发媒体自动播放,而该监听仅此一次。

    javascript
    document.body.addEventListener("touchstart", () => {
        // 执行上述媒体自动播放代码
    }, { once: true });
    

导入ics日历日程

  1. 参考链接:
  1. 详解
text
Calendar文件模板
SUMMARY:主题,修改称自己想要的即可
日期:20191112替换成开始日期,20191113提传承结束日期
UID:如果需要添加多个的话UID一直增加就可以了
多个时间的就增加从BEGIN:VEVENT到END:VEVENT中间的就可以了
VALARM可以加也可以不加
html
BEGIN:VCALENDAR
METHOD:PUBLISH
VERSION:2.0
X-WR-CALNAME:个人
PRODID:-//Apple Inc.//Mac OS X 10.14.5//EN
X-APPLE-CALENDAR-COLOR:#34AADC
X-WR-TIMEZONE:Asia/Shanghai
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20191112T155023Z
UID:0
RRULE:FREQ=YEARLY;INTERVAL=1
DTEND;VALUE=DATE:20191113
TRANSP:TRANSPARENT
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:生日
LAST-MODIFIED:20191112T160624Z
DTSTAMP:20191112T155045Z
DTSTART;VALUE=DATE:20191112
SEQUENCE:0
BEGIN:VALARM
X-WR-ALARMUID:4A310C10-E78D-4B17-95F4-005E529D0E7D
UID:1
TRIGGER;VALUE=DATE-TIME:19760401T005545Z
ACTION:NONE
END:VALARM
BEGIN:VALARM
X-WR-ALARMUID:53D56D83-9C94-4F29-9436-93878ACE68A5
UID:2
TRIGGER:PT9H
ATTACH;VALUE=URI:Chord
ACTION:AUDIO
END:VALARM
END:VEVENT
END:VCALENDAR
html
BEGIN:VCALENDAR
METHOD:PUBLISH
VERSION:2.0
X-WR-CALNAME:2021 消费者权益日发布会。
PRODID:-//Apple Inc.//Mac OS X 10.14.5//EN
X-APPLE-CALENDAR-COLOR:#34AADC
X-WR-TIMEZONE:Asia/Shanghai
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20210314T155023Z
UID:0
DTEND;VALUE=DATE:20210315T110000
TRANSP:TRANSPARENT
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
LOCATION:微信视频号、抖音、斗鱼、京东、天猫各大平台同步直播
DESCRIPTION: 观看2021 消费者权益日发布会。
URL;VALUE=URI:https://xiaofeizhequanyi
SUMMARY:2021 消费者权益日发布会
LAST-MODIFIED:20210315T160624Z
DTSTAMP:20210315T155045Z
DTSTART;VALUE=DATE:20210315T100000
SEQUENCE:0
BEGIN:VALARM
X-WR-ALARMUID:4A310C10-E78D-4B17-95F4-005E529D0E7D
UID:1
TRIGGER;VALUE=DATE-TIME:19760401T005545Z
ACTION:NONE
END:VALARM
BEGIN:VALARM
X-WR-ALARMUID:53D56D83-9C94-4F29-9436-93878ACE68A5
UID:2
TRIGGER:-PT10M
ATTACH;VALUE=URI:Chord
ACTION:AUDIO
END:VALARM
END:VEVENT
END:VCALENDAR

注意:

* 时区没有Asia/Beijing,但有Asia/Shanghai
* 安卓手机可能无法删除导入后的日程,因为导入后文件变为只读,可考虑删除导入日程的账号

流程可视化

  1. 参考链接:
  1. 详解
  • 市场横向对比-BPMN.js、X6、Jsplumb、G6-editor

    • activiti 作为工作流引擎提供了前后端的解决方案,简单二次开发就可以部署一套业务流程的管理平台

    • Bpmn.js:基于 BPMN2.0 规范,设计的流程图编辑器

    • G6:antv 旗下专注图形可视化,各类分析类图表。比如生态树、脑图、辐射图、缩进图等等

    • X6:图编辑引擎,核心能力是节点、连线和画布。不仅支持了流程图,还有 Dag 图、ER 图

    • BMPN.js、Jsplumb 的拓展能力不足,自定义节点支持成本很高;只能全量引入,各系统无法按需引入

    • 与后端配套的流程引擎适配,成本较高。均不支持数据转换、不支持流程的校验等业务定制需求。

    • 文档、示例不健全。X6 和 BPMN 的文档不健全,示例少(2020 初调研结论)

阿里云直播

  1. 参考链接:
  1. 详解
  • 视频直播架构

    主播obs/服务方服务器 推视频流到直播中心(阿里云、斗鱼、虎牙等),直播中心包含各种视频服务(直播、点播、导播、截图、转码、流量监控、安全等),再通过rtmp协议拉流到各cdn节点,播放端通过HLS/HTTP-FLV/RTMP拉流播放

  • 应用场景

    在线教育、娱乐直播、电商带货、赛事直播、广电新媒体、企业直播

  • 基本概念

    • 推流

      推流是把采集阶段封装好的音视频直播流推送到阿里云直播服务中心的过程。

    • 拉流

      拉流是将第三方直播流地址拉取到阿里直播中心进行加速分发的过程。

    • 播流

      播流是将直播服务中心已有直播内容,分发到播放器进行播放过程。

    • 窄带高清™

      窄带高清™技术,根据画面内容进行编码优化,使用户在同等带宽情况下观看更清晰的视频。从人眼视觉模型出发,在节省码率的同时,也能提供更加清晰的观看体验,同等视频质量下最高节省20%~40%带宽。

    • 推流域名

      推流域名是用于推送直播流的域名,为必选配置,您必须在使用直播服务前完成该域名的注册并备案。经过解析域名(CNAME解析)、关联推播流域名和配置鉴权(可选)操作后,可以使用控制台的地址生成器生成对应的推流地址。

    • 播流域名

      播流域名是用于拉取直播流的域名,为必选配置,您必须在使用直播服务前完成该域名的注册并备案。经过解析域名(CNAME解析)、关联推播流域名和配置鉴权(可选)操作后,可以使用控制台的地址生成器生成对应的播放地址。

    • CNAME域名

      CNAME域名是在阿里云控制台添加加速域名后,给您分配的一个域名。该CNAME域名的形式为*.kunlun.com。 您需要在您的DNS解析服务商添加一条CNAME记录,将自己的加速域名指向*.kunlun.com的域名。记录生效后,域名解析的工作就正式转向阿里云直播,该域名所有的请求都将转向阿里云直播的边缘节点,达到加速效果。

    • H.264

      H.264是由ITU-T视频编码专家组(VCEG)和ISO/IEC动态图像专家组(MPEG)联合组成的联合视频组(JVT)提出的,高度压缩数字视频编解码器标准,同时也是MPEG-4第十部分。拥有低码率、图像质量高、容错能力强和网络适应性强等优点。

    • H.265

      H.265是ITU-T视频编码专家组(VCEG)继H.264之后所制定的新的视频编码标准。H.265标准围绕着现有的视频编码标准H.264,保留原来的某些技术,同时对一些相关的技术加以改进。新技术使用先进的技术用以改善码流、编码质量、延时和算法复杂度之间的关系,达到最优化设置。

    • 直播地址

      直播地址包含推流地址和播放地址,由域名、AppName、StreamName和鉴权串(可选)组成,每个域名下可以创建多个应用,每个应用下可以创建多个直播流。控制台提供地址生成器功能,支持快速生成推流地址和播放地址,可用于第三方软件(如OBS)推流。

    • RTMP

      RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。是Adobe公司开发的一个基于TCP的应用层协议,也就是说,RTMP是和HTTP/HTTPS一样,是应用层的一个协议族。RTMP在TCP通道上一般传输的是flv 格式流。请注意,RTMP是网络传输协议,而flv则是视频的封装格式。

      • RTMP工作在TCP之上,默认使用端口1935,这个是基本形态
      • RTMPE在RTMP的基础上增加了加密功能
      • RTMPT封装在HTTP请求之上,可穿透防火墙
      • RTMPS类似RTMPT,增加了TLS/SSL的安全功能
      • RTMFP使用UDP进行传输的RTMP
  • 准备工作

    实名认证的阿里云账号,2个已完成备案的域名(用于推流和播流)

  • 备案流程

    备案是指向主管机关报告事由存案以备查考。

    1. 购买一台阿里云服务器:准备备案服务器
    2. 备案所需资料,身份证、域名证书、其它监管部门要求的资料:准备备案所需资料
    3. PC端和移动端备案
  • 直播配置

    快速入门

    1. 购买阿里云直播服务
    2. 添加推流域名和播流域名
    3. 配置CNAME
    4. 关联推流域名和播流域名
    5. 配置鉴权
    6. 生成推流地址和播放地址
    7. 推流与播流

    推流OBS/ffmpeg

    播流 VLC media player

    推流地址rtmp://push.aliyunlive.com/app/stream?auth_key=1543302081-0-0-9c6e7c8190c10bdfb3c0***********

  • 前端播放器

    html
    &lt;!DOCTYPE html>
    &lt;html>
    &lt;head>
    &lt;meta charset="utf-8">
    &lt;meta http-equiv="x-ua-compatible" content="IE=edge" >
    &lt;meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"/>
    &lt;title>Aliplayer在线配置&lt;/title>
    &lt;link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.1/skins/default/aliplayer-min.css" />
    &lt;script type="text/javascript" charset="utf-8" src="https://g.alicdn.com/de/prismplayer/2.8.1/aliplayer-min.js">&lt;/script>
    &lt;/head>
    &lt;body>
    &lt;div class="prism-player" id="player-con">&lt;/div>
    &lt;script>
    var player = new Aliplayer({
      "id": "player-con",
      "source": "//player.alicdn.com/video/aliyunmedia.mp4",
      "width": "100%",
      "height": "500px",
      "autoplay": true,
      "isLive": false,
      "rePlay": true,
      "playsinline": true,
      "preload": true,
      "controlBarVisibility": "hover",
      "useH5Prism": true
    }, function (player) {
        player._switchLevel = 0;
        console.log("播放器创建了。");
      }
    );
    &lt;/script>
    &lt;/body>
    
  • 关于斗鱼首页视频,在chrome中无法自动播放的提示

    html
    &lt;video id="video" src="..." controls autoplay>&lt;/video>
    &lt;div id="mask">&lt;/div>
    &lt;script>
        window.onload = function () {
            document.getElementById('video').addEventListener('canplay', function (e) {
                console.log(1, e)
                document.getElementById('mask').innerHTML="遮罩";
                document.getElementById('video').addEventListener('playing', function (e) {
                    console.log(2, e)
                    document.getElementById('mask').innerHTML="取消遮罩";
                },{once:true})
            },{once:true})
        }
    &lt;/script>
    

生成指定范围不重复随机数最快捷方法

  1. 参考链接:
  1. 详解
javascript
const fn = (len, from = 0, to = 100) => {
  const ratio = (to - from) / len
  let result = []
  
  //生成随机数量占比较大,采用随机下标法生成
  if (ratio > 0.3) {
    const allNums = Array.from({ length: to - from }, (_, i) => i + from)

    for (let i = len; i-- > 0;) {
      result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
    }
  }
  //生成随机数量占比较小(稀疏),采用随机数去重法生成
  else {
    for (let i = len; i-- > 0;) {
      result.push(Math.round(Math.random() * to + from))
    }

    result = [...new Set(result)]

    let length = result.length

    while (len - length > 0) {
      result = [...new Set(result.concat(fn(len - length, from, to)))]
      length = result.length
    }
  }

  return result
}

Array.from的三种用法:

  1. Array.from (obj, mapFn)
Array.from(new Set([1,2,3,4]), x => x*2) //[2,4,6,8]
  1. Array.from ({length:n}, Fn)
Array.from({length:3}, () => 'jack') //["jack", "jack", "jack"]
  1. Array.from(string)
Array.from('abc') //&lsqb;[a','b','c']

页面崩溃通信

  1. 参考链接:
  1. 详解
  • 页面通信方式

    1. onbeforeunload + url 传参
    html
    &lt;!-- A.html -->
    &lt;!DOCTYPE html>
    &lt;html lang="en">
    &lt;head>
        &lt;meta charset="UTF-8">
        &lt;meta name="viewport" content="width=device-width, initial-scale=1.0">
        &lt;title>A&lt;/title>
    &lt;/head>
    &lt;body>
        &lt;h1>A 页面&lt;/h1>
        &lt;button type="button" onclick="openB()">B&lt;/button>
        &lt;script>
            window.name = 'A'
            function openB() {
                window.open("B.html", "B")
            }
    
            window.addEventListener('hashchange', function () {// 监听 hash
                alert(window.location.hash)
            }, false);
        &lt;/script>
    &lt;/body>
    &lt;/html>
    
    html
    &lt;!-- B.html -->
    &lt;!DOCTYPE html>
    &lt;html lang="en">
    &lt;head>
        &lt;meta charset="UTF-8">
        &lt;meta name="viewport" content="width=device-width, initial-scale=1.0">
        &lt;title>B&lt;/title>
        &lt;button type="button" onclick="sendA()">发送A页面消息&lt;/button>
    &lt;/head>
    &lt;body>
        &lt;h1>B 页面&lt;/h1>
        &lt;span>&lt;/span>
        &lt;script>
            window.name = 'B'
            window.onbeforeunload = function (e) {
                window.open('A.html#close', "A")
                return '确定离开此页吗?';
            }
        &lt;/script>
    &lt;/body>
    &lt;/html>
    
    1. postmessage(也可用于iframe跨域通信)
    html
    &lt;!-- A.html -->
    &lt;!DOCTYPE html>
    &lt;html lang="en">
    &lt;head>
        &lt;meta charset="UTF-8">
        &lt;meta name="viewport" content="width=device-width, initial-scale=1.0">
        &lt;title>A&lt;/title>
    &lt;/head>
    &lt;body>
        &lt;h1>A 页面&lt;/h1>
        &lt;button type="button" onclick="openB()">B&lt;/button>
        &lt;script>
            window.name = 'A'
            function openB() {
                window.open("B.html?code=123", "B")
            }
            window.addEventListener("message", receiveMessage, false);
            function receiveMessage(event) {
                console.log('收到消息:', event.data)
            }
        &lt;/script>
    &lt;/body>
    &lt;/html>
    
    html
    &lt;!-- B.html -->
    &lt;!DOCTYPE html>
    &lt;html lang="en">
    &lt;head>
        &lt;meta charset="UTF-8">
        &lt;meta name="viewport" content="width=device-width, initial-scale=1.0">
        &lt;title>B&lt;/title>
        &lt;button type="button" onclick="sendA()">发送A页面消息&lt;/button>
    &lt;/head>
    &lt;body>
        &lt;h1>B 页面&lt;/h1>
        &lt;span>&lt;/span>
        &lt;script>
            window.name = 'B'
            function sendA() {
                let targetWindow = window.opener
                targetWindow.postMessage('Hello A', "http://localhost:3000");
            }
        &lt;/script>
    &lt;/body>
    &lt;/html>
    
    1. localStorage
    javascript
    // A
    localStorage.setItem('testB', 'sisterAn');
    
    // B
    let testB = localStorage.getItem('testB');
    console.log(testB)
    // sisterAn
    
    1. WebSocket

    2. SharedWorker

    SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域, SharedWorkerGlobalScope 。

    javascript
    // A.html
    var sharedworker = new SharedWorker('worker.js')
    sharedworker.port.start()
    sharedworker.port.onmessage = evt => {
      // evt.data
        console.log(evt.data) // hello A
    }
    
    // B.html
    var sharedworker = new SharedWorker('worker.js')
    sharedworker.port.start()
    sharedworker.port.postMessage('hello A')
    
    // worker.js
    const ports = []
    onconnect = e => {
    const port = e.ports[0]
      ports.push(port)
      port.onmessage = evt => {
          ports.filter(v => v!== port) // 此处为了贴近其他方案的实现,剔除自己
          .forEach(p => p.postMessage(evt.data))
      }
    }
    
    1. Service Worker

    Service Worker 是一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。

    javascript
    // 注册
    navigator.serviceWorker.register('./sw.js').then(function () {
        console.log('Service Worker 注册成功');
    })
    // A
    navigator.serviceWorker.addEventListener('message', function (e) {
        console.log(e.data)
    });
    // B
    navigator.serviceWorker.controller.postMessage('Hello A');
    
  • B 页面正常关闭,如何通知 A 页面?

    onbeforeunload + url 传参

  • B 页面意外崩溃,又该如何通知 A 页面

    心跳检测

    1. B 页面加载后,通过 postMessage API 每 5s 给 sw 发送一个心跳,表示自己的在线,sw 将在线的网页登记下来,更新登记时间;
    2. B 页面在 beforeunload 时,通过 postMessage API 告知自己已经正常关闭,sw 将登记的网页清除;
    3. 如果 B页面在运行的过程中 crash 了,sw 中的 running 状态将不会被清除,更新时间停留在奔溃前的最后一次心跳;
    4. A 页面 Service Worker 每 10s 查看一遍登记中的网页,发现登记时间已经超出了一定时间(比如 15s)即可判定该网页 crash 了。

    实现

    javascript
    // B
    if (navigator.serviceWorker.controller !== null) {
      let HEARTBEAT_INTERVAL = 5 * 1000 // 每五秒发一次心跳
      let sessionId = uuid() // B页面会话的唯一 id
      let heartbeat = function () {
        navigator.serviceWorker.controller.postMessage({
          type: 'heartbeat',
          id: sessionId,
          data: {} // 附加信息,如果页面 crash,上报的附加数据
        })
      }
      window.addEventListener("beforeunload", function() {
        navigator.serviceWorker.controller.postMessage({
          type: 'unload',
          id: sessionId
        })
      })
      setInterval(heartbeat, HEARTBEAT_INTERVAL);
      heartbeat();
    }
    
    javascript
    // A
    // 每 10s 检查一次,超过15s没有心跳则认为已经 crash
    const CHECK_CRASH_INTERVAL = 10 * 1000 
    const CRASH_THRESHOLD = 15 * 1000
    const pages = {}
    let timer
    function checkCrash() {
      const now = Date.now()
      for (var id in pages) {
        let page = pages[id]
        if ((now - page.t) > CRASH_THRESHOLD) {
          // 上报 crash
          delete pages[id]
        }
      }
      if (Object.keys(pages).length == 0) {
        clearInterval(timer)
        timer = null
      }
    }
    
    worker.addEventListener('message', (e) => {
      const data = e.data;
      if (data.type === 'heartbeat') {
        pages[data.id] = {
          t: Date.now()
        }
        if (!timer) {
          timer = setInterval(function () {
            checkCrash()
          }, CHECK_CRASH_INTERVAL)
        }
      } else if (data.type === 'unload') {
        delete pages[data.id]
      }
    })
    

报表可视化

  1. 参考链接:
  1. 详解
  • 难点

    • 并发

      频繁地上传、下载文档,服务器带宽承受了很大的压力

      所有 Excel 解析、提取的操作都在服务器端,频繁的 IO 操作让服务器不堪重负

      硬堆服务器配置带来运维的压力

    • 培训成本和兼容性要求较高

    • 报表格式灵活多变

    • 对计算公式的种类和性能要求较高

    • 对文件服务器的带来了很大压力,后台也不得不定期做批量的数据拆分和维护

  • 选型

    • 云文档类型

      WPS、石墨文档、office online具备较高的完成度,支持一定程度的二次开发而且可以私有化部署,但按时间、按并发量、用户数量等方式授权,价格昂贵

    • 控件类型

      LuckySheet、Handsontable、SpreadJS

  • SpreadJS

    • 渲染性能

      50 万条数据加载耗时 200 ms左右,实时渲染 + 双层缓存,用 Canvas 渲染表格部分,并且只渲染用户看到的部分内容

      Double buffering 在图形学里,一般称作双缓冲,实际上的绘图指令是在一个缓冲区完成,这里的绘图非常的快,在绘图指令完成之后,再通过交换指令把完成的图形立即显示在屏幕上,这就避免了出现绘图的不完整,同时效率很高。

    • 计算引擎

      • 引用数据原生化
      • AST 树解析公式计算
      • 性能接近原生JS的计算性能极限
    • 使用

      支持纯js,vue,react,angular

WebGIS

  1. 参考链接:
  1. 详解
  • openlayers

    • 样例

      html
      &lt;!doctype html>
      &lt;html lang="en">
        &lt;head>
          &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/css/ol.css" type="text/css">
          &lt;style>
            .map {
              height: 400px;
              width: 100%;
            }
          &lt;/style>
          &lt;script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/build/ol.js">&lt;/script>
          &lt;title>OpenLayers example&lt;/title>
        &lt;/head>
        &lt;body>
          &lt;h2>My Map&lt;/h2>
          &lt;div id="map" class="map">&lt;/div>
          &lt;script type="text/javascript">
            var map = new ol.Map({
              target: 'map',
              layers: [
                new ol.layer.Tile({
                  source: new ol.source.OSM()
                })
              ],
              view: new ol.View({
                center: ol.proj.fromLonLat([37.41, 8.82]),
                zoom: 4
              })
            });
          &lt;/script>
        &lt;/body>
      &lt;/html>
      
  • cesium

    1. 下载cesium:https://cesium.com/downloads/

    2. 部署到服务器

    3. 项目

    index.html

    html
    &lt;!DOCTYPE html>
    &lt;html lang="en">
    &lt;head>
      &lt;!-- Use correct character set. -->
      &lt;meta charset="utf-8">
      &lt;!-- Tell IE to use the latest, best version. -->
      &lt;meta http-equiv="X-UA-Compatible" content="IE=edge">
      &lt;!-- Make the application on mobile take up the full browser screen and disable user scaling. -->
      &lt;meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
      &lt;title>Hello World!&lt;/title>
      &lt;!--引入cesium.js。该文件定义了Cesium对象,它包含了我们需要的一切。-->
      &lt;script src="../Build/Cesium/Cesium.js">&lt;/script>
      &lt;style>
          /*为了能使用Cesium各个可视化控件,我们需要引入widgets.css。*/
          @import url(../Build/Cesium/Widgets/widgets.css);
          html, body, #cesiumContainer {
              width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden;
          }
      &lt;/style>
    &lt;/head>
    &lt;body>
      &lt;!--在HTML的body中我们创建一个DIV,用来作为三维地球的容器。-->
      &lt;div id="cesiumContainer">&lt;/div>
      &lt;script>
        var viewer = new Cesium.Viewer('cesiumContainer');
      &lt;/script>
    &lt;/body>
    &lt;/html>
    
    • Turf.js

      地理空间分析库,处理各种地图算法

禁用外链

  1. 参考链接:
  1. 详解

    javascript
    window.onload = function () {
        //禁用外链
        var a = document.getElementsByTagName("a");
        for (var i = 0; i &lt; a.length; i++) {
            a[i].onclick = function (e) {
                e.preventDefault();
                if(!new RegExp('^'+window.location.origin).test(this.href)){
                    return false;
                }
                else{
                    window.location.href = this.href;
                }
            }
        }
    }
    

禁用控制台

  1. 参考链接:
  1. 详解
  • 开启控制台,有三种方法

    • F12
    • 右键==》检查
    • 浏览器==》更多工具==》开发者工具(快捷键:ctrl+shift+i)
  • 阻止F12事件

    javascript
    window.onkeydown = window.onkeyup = window.onkeypress = function (event) {  
        // 判断是否按下F12,F12键码为123  
        if (event.keyCode = 123) {  
            event.preventDefault() // 阻止默认事件行为  
            window.event.returnValue = false  
        }  
    }
    
  • 阻止右键事件

    javascript
    window.oncontextmenu = function() {  
        event.preventDefault() // 阻止默认事件行为  
        return false  
    }
    
  • 定时检查浏览器窗口变化(漏洞:浮窗)

    javascript
    let threshold = 160 // 打开控制台的宽或高阈值  
    window.setInterval(function() {  
        if (window.outerWidth - window.innerWidth > threshold ||   
        window.outerHeight - window.innerHeight > threshold) {  
            // 如果打开控制台,则刷新页面  
            window.location.reload()  
        }  
    }, 1000)
    
  • 打开控制台弹窗,启用调试debug调试

    javascript
    setInterval(function() {
        check()
    }, 1000);
    var check = function() {
        function doCheck(a) {
            if (("" + a / a)["length"] !== 1 || a % 20 === 0) {
                (function() {}
                ["constructor"]("debugger")())
            } else {
                (function() {}
                ["constructor"]("debugger")())
            }
            doCheck(++a)
        }
        try {
            doCheck(0)
        } catch (err) {}
    };
    check();
    
    javascript
    if(window.location.href.indexOf('#debug')==-1){
        setInterval(function(){
            (function (a) {return (function (a) {return (Function('Function(arguments[0]+"' + a + '")()'))})(a)})('bugger')('de', 0, 0, (0, 0))
      }, 1000)
    }
    
  • 打开控制台提示(可实现跳转url或刷新)

    javascript
    function toDevtools(){
    
        let num = 0
        var devtools = new Date()
        devtools.toString = function() {
            num++;
            if(num>0){
                alert('控制台打开了')
                // 可以写刷新或者跳转的逻辑
            }
        }
        console.log(devtools);
    }
    toDevtools()
    

pdf预览

  1. 参考链接:
  1. 详解
  • 实现原理

    canvas画底图,div/span铺在上层,供文字选择

  • 使用

    • 从参考链接1中下载打包好的pdf.js压缩包(不要从github下载源码打包,会有各种报错)
    • 把文件放上服务器,打开/web/viewer.html,即可预览demo的pdf
    • 打开/web/viewer.html?file=***.pdf,即可预览自己指定的pdf
    • 可考虑使用iframe嵌入主页面
  • vue-pdf在IOS下会出现问题

web打印

  1. 参考链接:
  1. 详解
  • 浏览器打印

    通过 window.print() 、document.execCommand('print’) 调用浏览器打印

    不同浏览器的区别:在Safari和Chrome都会弹起打印预览的窗口,FireFox(老版本)和 IE 没有预览而是直接让你选择打印机

    • 存在的问题

      1. 打印不支持自定义分页行为,默认不支持批量打印;
      2. 打印的时候样式有问题,所见非所得;
      3. 打印可以准确识别的样式单位是绝对单位(如pt、mm、cm),对相对单位识别不同打印机可能会得到意想不到的结果;
    • 局部打印

      • innerHtml

        javascript
        function innerHtmlPrint(){
            // 缓存页面内容
            const bodyHtml = window.document.body.innerHTML;
            // 获取要打印的dom
            const printContentHtml = document.getElementById("print").innerHTML;
            // 替换页面内容
            window.document.body.innerHTML = printContentHtml;
            // 全局打印
            window.print();
            // 还原页面内容
            window.document.body.innerHTML = bodyHtml;
            // 页面事件会丢失,需要刷新
            window.location.reload();
        }
        
      • iframe插件

        也可用于html转pdf

        vue-iframe-print

        demo

        javascript
        function onIframePrint(printId) {
          const printContentHtml = document.getElementById("printId").innerHTML;
          const iframe = document.createElement("iframe");
          iframe.setAttribute(
            "style",
            "position:absolute;width:0px;height:0px;left:-500px;top:-500px;"
          );
          document.body.appendChild(iframe);
          iframe.contentDocument.write(printContentHtml);
          iframe.contentDocument.close();
          iframe.contentWindow.print();
          document.body.removeChild(iframe);
        }
        
      • canvas

        将打印内容转为图片,一倍清晰度模糊,可以用2倍canvas。

        缺点:pdf需要下载,有的产品需求需要一键打印。html2canvas不支持ie,兼容性也是个问题

        javascript
        function print() {
          var target = document.getElementsByClassName("right-aside")[0];
          target.style.background = "#FFFFFF";
        
          html2canvas(target, {
            onrendered:function(canvas) {
                var contentWidth = canvas.width;
                var contentHeight = canvas.height;
        
                //一页pdf显示html页面生成的canvas高度;
                var pageHeight = contentWidth / 592.28 * 841.89;
                //未生成pdf的html页面高度
                var leftHeight = contentHeight;
                //页面偏移
                var position = 0;
                //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
                var imgWidth = 595.28;
                var imgHeight = 592.28/contentWidth * contentHeight;
        
                var pageData = canvas.toDataURL('image/jpeg', 1.0);
        
                var pdf = new jsPDF('', 'pt', 'a4');
        
                //有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
                //当内容未超过pdf一页显示的范围,无需分页
                if (leftHeight &lt; pageHeight) {
                  pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight );
                } else {
                    while(leftHeight > 0) {
                        pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
                        leftHeight -= pageHeight;
                        position -= 841.89;
                        //避免添加空白页
                        if(leftHeight > 0) {
                          pdf.addPage();
                        }
                    }
                }
        
                pdf.save("content.pdf");
            }
          })
        }
        

        完美还原

        html
        &lt;object
          type="application/pdf"
          data="./滴滴出行行程报销单A.pdf"
          width="100%"
          height="700"
        >&lt;/object>
        
        &lt;embed 
          type="application/pdf" 
          src="./滴滴出行行程报销单A.pdf" 
          width="100%" 
          height="700px"
        />
        
  • 插件打印

    一般是通过项目里面嵌入脚本,或者安装本地插件来完成,优缺点也都很明显

    • 优点

      • 功能强大,可以调用到系统底层的东西,比如获取系统打印机列表,设置默认打印机等
      • 可以实现无预览打印
    • 缺点

      • 需要安装客户端,大多收费
      • 第三方插件,无技术支持,出现问题难以解决(版本问题,chrome84升级导致的证书问题)
      • 本地插件的方式基本只有window系统版本
    • 插件

      • C-lodop:功能强大,兼容性不好,使用之前需研究下官网给的demo,原理是在页面嵌入一段js,和本地客户端通过webscoket进行通信

      • HttpPrinter:同C-lodop

      • HiPrint:不需要安装客户端,没有npm包,依赖于jQuery

      • NW.js

        javascript
        const printer = require('./printer.js')
        
        function getPrinterList() {  
            const list = printer.getPrinterList()  
            const res = []  
            list.forEach(item => res.push(item.name))  
            return res
        }
        
        // 获取当前打印机列表
        const printerList = getPrinterList()
        // 暂定使用打印机为第一个
        const printerName = printerList[0]
        // mock订单数据
        const mockData = {  id: 001,  delivery_way: '外送',  deliver_time: '立即送达',  sku_detail: [{    quantity: 10,    sku_name: '火米饼套餐',    price: 20  }],  description: '多放火贝 火火火火',  invoiced: '',  package_fee: 1,  deliver_fee: 10,  total_price: 31,  receiver_address: '火星1区101路1号',  receiver_name: '火星人',  receiver_phone: 00001,  create_time: '0001-01-01',  tagg_shop_name: '火星1号商店'}
        
        // 封装打印订单函数,传参为打印机名称和订单数据
        function printOrderRecive(name = '', data = {}) {  
            const Buffer = require('./escpos.js')  
            let buffer = new Buffer()
            buffer = buffer.setLineHeight(70)    
                          .setTextSize(2).setLineHeight(50).setText(data.id, 'center')    
                          .setTextSize(1).setLineHeight(100).setText(`${data.delivery_way} ${data.deliver_time}`, 'center')    
                          .setLineHeight(70).setDecLine()    
                          .setBoldOn()    
                          .setLineHeight(70)  
            data.sku_detail && data.sku_detail.forEach(item => {    
                buffer = buffer.setThreeCol(item.quantity, item.sku_name, `${item.price}`)  })  
                buffer = buffer.setLine()    
                              .setLineHeight(100).setText(`备注:${data.description}`).setBoldOff()    
                              .setLineHeight(50).setDecLine()    
                              .setLineHeight(70)    
                              .setTwoCol('开具发票', data.invoiced)    
                              .setTwoCol('包装费', `${data.package_fee}`)    
                              .setTwoCol('配送费', `${data.deliver_fee}`)    
                              .setLineHeight(50)    
                              .setDecLine()    
                              .setBoldOn().setText(`合计:¥${data.total_price}  `, 'right').setBoldOff()    
                              .setDecLine()    
                              .setLineHeight(70)    
                              .setText(`送货地址:${data.receiver_address}`)    
                              .setText(`客户:${data.receiver_name} ${data.receiver_phone}`)    
                              .setDecLine()    
                              .setText(`下单时间: ${data.create_time}`, 'center')    
                              .setLine(2)    
                              .setBoldOn().setText(`${data.tagg_shop_name} \n \n`, 'center').setBoldOff()    
                              .setLine(2)    
                              .cut()    
                              .getBuffer()
            printer.print(name, buffer)
        }
        
        // 调用打印功能
        printOrderRecive(printerName, mockData)
        
  • 打印样式

    • @media print

      可以控制打印时的样式,仅在打印生效,可以实现一些特殊需求。

      css
      /*媒体查询*/
      @media print{
        body{
          background-color:red;
        }
      }
      
      /*css import*/
      @import url("print.css") print;
      
      /*html link*/
      &lt;link rel="stylesheet" media="print" href="print.css">
      
    • @page

      设置页面大小(A3,A4,A5)、边距(margin)、方向(auto、landscape、portrait)等。

      css
      /*去除页眉*/
      @page {
        margin-top:0;
      }
      /*去除页脚*/
      @page {
        margin-bottom:0;
      }
      /*去除页眉页脚*/
      @page {
        margin:0;
      }
      @page {
        /*auto:浏览器控制landscape:横向portrait:竖向*/
        size:A4 portrait;
        margin:1cm 3cm;
      }
      /*双面打印时会用到左右页不同样式,左右页面距为装订留出空间*/
      @page :left{
        margin-left:2.5cm;
        margin-right:2.7cm;
      }
      @page :right{
        margin-left:2.7cm;
        margin-right:2.5cm;
      }
      
    • page-break-xxx

      • page-break-before( after ) 用于设置元素前( 后 )的分页行为,可取值:

        • auto默认值。如果必要则在元素前插入分页符。
        • always在元素前插入分页符。
        • avoid避免在元素前插入分页符。
        • left在元素之前足够的分页符,一直到一张空白的左页为止。
        • right在元素之前足够的分页符,一直到一张空白的右页为止。
        • inherit规定应该从父元素继承 page-break-before 属性的设置。
      • page-break-inside设置元素内部的分页行为。取值如下:

        • auto默认。如果必要则在元素内部插入分页符。
        • avoid避免在元素内部插入分页符。
        • inherit规定应该从父元素继承 page-break-inside 属性的设置。

      orphans设置当元素内部发生分页时必须在页面底部保留的最少行数。

      widows设置当元素内部发生分页时必须在页面顶部保留的最少行数。

      css
      @media print{
        .img-test{
          width: 200vh;
          page-break-before:always;
        }
        h1{
          page-break-before:always;
          page-break-after:always;
        }
        p{
          page-break-inside:avoid;
        }
      }
      
  • 云打印(node + ipp)

    • 打印机类型

      • 激光打印机

        办公室常见的打印机,一般用打印普通文档材料。利用激光加热将墨粉固定在纸上,从而实现打印功能。平常的耗材是墨粉,使用的纸张是普通纸,一般是打印黑白色。打印速度快 后期耗材便宜

      • 针式打印机

        一般用于打印票据,或者需要按压打印的纸张。将色带上的墨水压在纸上,从而实现打印功能。平常的耗材是色带,使用的纸张是多联纸,比起其他两个分类针式打印机可以说是元老级别的,它是是市场上较早出现的种类。主要有9针、24针、72针、144针等多种针式打印机。其特点比较鲜明结构简单、技术成熟、性能价格比好、消耗费用低。

      • 热敏打印机

        使用专用纸张,靠高温显示需要打印的信息.主要用于打印小票.

      • 喷墨打印机

        一般用于打印彩色材料。将墨水喷射在纸上,从而实现打印功能。平常的耗材是墨水,使用的纸张是普通纸,一般可以打印彩色。(另外也有一个耗材是墨盒,有些机型不必频繁更换)

    • 概念

      • 互联网打印协议 (IPP;Internet Printing Protocol)

        一个在互联网上打印的标准网络协议,它容许用户可以透过互联网作遥距打印及管理打印工作等工作。用户可以透过相关界面来控制打印品所使用的纸张种类、分辨率等各种参数。

      • 无头浏览器

        使用脚本来执行以上过程的浏览器,能模拟真实的浏览器使用场景。如puppeteer

    • 实现

      Egg + Puppeteer 实现Html转PDF(已开源)

阿里云视频点播

  1. 参考链接:
  1. 详解

阿里云视频点播(VOD)是集音视频上传、自动化转码处理、媒体资源管理、分发加速于一体的全链路音视频点播服务。

  • 应用场景

    • 短视频:电商,母婴,社交等服务平台
    • 音视频网站:新闻网站,视频网站
    • 在线教育:在线培训,在线教育
    • 广电传媒:广播电视,新闻机构
  • 功能

    • 媒体上传

      • 控制台上传,SDK上传,服务端(JAVA)上传,客户端上传:Web端(JavaScript)、移动端(Android,iOS),离线拉取上传,PC客户端工具上传,并支持直播录制转点播。
      • 视频格式:3gp, asf, avi,dat, dv, flv,f4v, gif,m2t, m3u8, m4v,mj2,mjpeg,mkv,mov,mp4, mpe, mpg,mpeg, mts, ogg,qt,rm,rmvb, swf,ts,vob,wmv,webm。
      • 音频格式:aac,ac3,acm, amr,,ape,caf, flac,m4a, mp3, ra,wav, wma。
      • 图片格式:png,jpg,jpeg。
    • 媒资管理

      • 多重冗余备份,提供异地容灾和资源隔离
      • 媒资增删改查
      • 指纹特征的提取和比对,提供视频版权保护
      • 媒体智能审核,极大降低内容违规风险
    • 媒体处理

      • 音视频转码
      • 视频截图
      • 区间内容截取
      • 视频水印
      • 自定义处理流程
      • 多码率自适应
    • 智能生产

      • 云剪辑:剪切拼接、混音、字幕、图片叠加、遮标、转场特效
      • 自动输出视频的多维度内容标签
      • 智能提取封面和动图封面
    • 视频加密

    • 分发加速

      • Referer防盗链
      • URL鉴权防盗,防下载
      • 播放鉴权,AK安全认证基础上的二次鉴权机制
    • 音视频播放

      • 支持点播和直播
      • 加密播放
      • 安全下载
      • 清晰度切换
      • 直播答题
    • 统计分析

      • 支持查询最近30天内、流量带宽、存储空间及转码时长
      • 运营统计:播放UV、播放VV、人均观看时长、热门视频
  • 基本概念

    • 封装格式

      封装格式(Format)也称多媒体容器(Multimedia Container),是将已编码压缩好的视频轨道、音频轨道和元数据(视频基本信息如标题、字幕等)按照一定的格式规范,打包放到一个文件中,形成特定文件格式的视频文件。

      • 封装格式主要分为两大类:面向存储的和面向流媒体的。

        • 面向存储的,常见的有AVI、ASF(WMA/WMV)、MP4、MKV、RMVB(RM/RA)等;
        • 面向流媒体的,常见的有FLV、TS(需要配合流媒体网络传输协议,如HLS、RTMP等),MP4也支持流媒体方式(配合HTTP等)。
      • 下面以流媒体传输协议的视角重点介绍面向流媒体的封装格式:

        • MP4

          经典的视频封装格式,移动端(iOS、Android)、PC Web多终端都能良好支持。

          但MP4的视频文件头太大,结构复杂;如果视频较长(如数小时),则其文件头会过大,影响视频加载速度,故更适合短视频场景。

          MP4由一个个的box(以前叫atom)组成,所有的Metadata(媒体描述元数据),包括定义媒体的排列和时间信息的数据都包含在这样的一些结构box中。Metadata 对媒体数据(比如视频帧)引用说明,而媒体数据在这些引用文件中的排列关系全部在第一个主文件中的metadata描述,这样就会导致视频时长越大文件头就会越大、加载越慢。

        • HLS(HTTP Live Streaming)

          苹果公司推出的基于HTTP的流媒体网络传输协议,视频的默认封装格式是TS,除了多个TS分片文件,还定义了用来控制播放的m3u8索引文件(文本文件),可以规避普通MP4 长时间缓冲头部数据的问题,比较适合点播场景。移动端(iOS、Android)支持较好,但PC端IE存在兼容性问题依赖播放器的二次开发(建议使用阿里云Web播放器)。

        • FLV

          Adobe 公司推出的标准,在 PC 端有Flash的强力支持,但在移动终端只有App实现播放器才能支持(建议使用阿里云播放器),大部分手机端浏览器均不支持,特别是苹果的移动设备都不支持。

        • DASH(Dynamic Adaptive Streaming over HTTP)

          使用fragmented MP4f(MP4)格式,将MP4视频分割为多个分片,每个分片可以存在不同的编码形式(如分辨率、码率等);播放器端可自由选择需要播放的视频分片,实现自适应多码率、不同画质内容的无缝切换,提供更好的播放体验。其中MPD文件类似HLS的m3u8文件,国外视频网站如YouTube、Netflix等较多使用DASH。

        • HLS+fMP4(HTTP Live Streaming with fragmented MP4)

          本质上还是HLS协议。苹果公司于WWDC 2016宣布新的HLS标准支持文件封装格式为fragmented MP4,使用方法与TS分片类似,意味着一次转码可同时打包成DASH和HLS。

          HLS(包括HLS+fMP4)和DASH是最常用的自适应流媒体传输技术(Adaptive Video Streaming),推荐使用。

    • 编码方式

      视频编码方式(Codec)是指能够对数字视频进行压缩或解压缩(视频解码)的程序或者设备。通常这种压缩属于有损数据压缩。也可以指通过过特定的压缩技术,将某个视频格式转换成另一种视频格式。

      • 常见的编码方式有:

        1. H.26X系列:由ITU(国际电信联盟)主导,包括H.261、H.262、H.263、H.264、H.265。
        • H.261:主要在老的视频会议和视频电话产品中使用。
        • H.263:主要用在视频会议、视频电话和网络视频上。
        • H.264:H.264/MPEG-4第十部分,或称AVC(Advanced Video Coding,高级视频编码),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。
        • H.265:高效率视频编码(High Efficiency Video Coding,简称HEVC)是一种视频压缩标准,H.264/MPEG-4 AVC的继任者。HEVC不仅提升图像质量,同时也能达到H.264/MPEG-4 AVC两倍的压缩率(等同于同样画面质量下码率减少50%),可支持4K分辨率甚至超高画质电视,最高分辨率可达8192×4320(8K分辨率),这是目前发展的趋势。
        1. MPEG系列:由ISO(国际标准组织机构)下属的MPEG(运动图象专家组)主导,视频编码方面主要有:
        • MPEG-1第二部分:主要使用在VCD上,有些在线视频也使用这种格式,该编解码器的质量大致上和原有的VHS录像带相当。
        • MPEG-2第二部分:等同于H.262,使用在DVD、SVCD和大多数数字视频广播系统和有线分布系统(Cable Distribution Systems)中。
        • MPEG-4第二部分:可以使用在网络传输、广播和媒体存储上,比起MPEG-2和第一版的H.263,它的压缩性能有所提高。
        • MPEG-4第十部分:技术上和ITU-TH.264是相同的标准,二者合作,诞生了H.264/AVC标准,ITU-T将其命名为H.264,而ISO/IEC称它为MPEG-4高级视频编码(Advanced Video Coding,AVC)。
        1. AVS(Audio Video coding Standard):我国自主知识产权的信源编码标准,是《信息技术先进音视频编码》系列标准的简称,目前已完成两代AVS标准的制定。
        • 第一代AVS标准包括国家标准《信息技术先进音视频编码第2部分:视频》(简称AVS1)和《信息技术先进音视频编码第16部分:广播电视视频》(简称AVS+)。AVS+的压缩效率与国际同类标准H.264/AVC最高档次(High Profile)相当。
        • 第二代AVS标准,简称AVS2,首要应用目标是超高清晰度视频,支持超高分辨率(4K以上)、高动态范围视频的高效压缩。AVS2的压缩效率比上一代标准AVS+和H.264/AVC提高了一倍,超过国际同类型标准HEVC/H.265。
        1. 其他系列,如,VP8、VP9(Google 主导),RealVideo(RealNetworks推出)等编码方式,在互联网视频使用较少,此处不再介绍。

      选择编码方式要充分考虑播放终端(如移动端APP、Web浏览器等)的兼容性,尽量使用最常见和广泛支持的。阿里云视频点播支持视频编码格式:H.264/AVC(默认)、 H.265/HEVC,音频编码格式:MP3(默认)、AAC、VORBIS、FLAC。

    • 转码

      将已经压缩编码的视频码流转换成另一个视频码流,以适应不同的网络带宽、不同的终端处理能力和不同的用户需求。本质上是一个先解码、再编码的过程。

    • 转封装

      将视频或音频的封装格式进行转换,如将AVI的视频转换为MP4。处理速度极快。音视频质量无损。

    • 码率、码流

      视频文件在单位时间内使用的数据流量,是视频编码中画面质量控制最重要的部分。

      量度单位为“比特每秒”(bit/s或bps),常使用Kbps(每秒多少千个比特)或Mbps。

      一般来说同样分辨率下,视频文件的码率越大,压缩比就越小,画面质量就越高。

      码率越大,说明单位时间内取样率越大,数据流精度就越高,处理出来的文件就越接近原始文件,图像质量越好,画质越清晰,要求播放设备的解码能力也越高。

      码率越大,文件体积也越大,其计算公式是文件体积=时间×码率/8。例如,网络上常见的一部60分钟的码率为1Mbps的720P的视频文件,其体积就大概为3600秒×1Mb/8=450MB。

    • 分辨率

      用来描述视频对细节的分辨能力,通常表示为每一个方向上的像素数量,比如1280x720等。

      分辨率决定了视频画面细节的精细程度。通常情况下,视频的分辨率越高,所包含的像素就越多,画面就越清晰。

      视频的分辨率越高,所要求的码率也越大。

      不同分辨率都有合理的码率选择范围,如果低于这个范围,视频画面质量会很差;如果高于这个范围,画面提升有限甚至几乎无提升,且浪费网络流量和存储空间。

    • 帧率

      单位时间内视频显示帧数的量度单位,也就是每秒钟刷新的图片的帧数,量度单位为“每秒显示帧数”(Frame Per Second,FPS)或“赫兹”。

      高的帧率可以得到更流畅、更逼真的画面效果。一般来说25~30fps就可接受,提升至60fps则可以明显提升交互感和逼真感,但一般来说超过75fps就不容易察觉到有明显的流畅度提升了。

      如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为显示设备不能以这么快的速度更新。

      在分辨率不变的情况下,帧率越高,则对显卡的处理能力要求越高。

    • GOP(关键帧间隔)

      GOP(Group of Pictures)是一组以 MPEG 编码的影片或视讯串流内部的连续图像,以 I 帧开头,到下一个 I 帧结束。

      • 一个 GOP 包含如下图像类型:

        • I 帧(Intra Coded Picture):又称帧内编码帧,为关键帧,是一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态画面。视频序列中的第一个帧始终都是I 帧,每个 GOP 由I 帧开始。
        • P 帧(Predictive Coded Picture):又称帧间预测编码帧,需要参考前面的I帧才能进行编码。表示的是当前帧画面与前一帧(前一帧可能是I帧也可能是P帧)的差别。解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。与I帧相比,P帧通常占用更少的数据位,但不足是,由于P帧对前面的P和I参考帧有着复杂的依赖性,因此对传输错误非常敏感。
        • B 帧(Bidirectionally Predictive Coded Pictures):又称双向预测编码帧,也就是B帧记录的是本帧与前后帧的差别。也就是说要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是对解码性能要求较高。

      GOP值表示关键帧的间隔(即两个关键帧之间的帧数),也就是两个IDR帧之间的距离,一个帧组的最大帧数。

      每一秒视频至少需要使用 1 个关键帧。

      增加关键帧个数可改善视频质量,但会同时增加带宽和网络负载。

      GOP值(帧数)除以帧率即为时间间隔,如阿里云视频点播默认的GOP值为250帧,帧率为25fps,则时间间隔为10秒。

      • GOP值需要控制在合理范围,以平衡视频质量、文件大小(网络带宽)和seek效果(拖动、快进的响应速度)等:

        • 加大GOP值有利于减小视频文件大小,但也不宜设置过大,太大则会导致GOP后部帧的画面失真,影响视频质量。
        • GOP值也是影响视频seek响应速度的关键因素,seek时播放器需要定位到离指定位置最近的前一个关键帧,如果GOP太大意味着距离指定位置可能越远(需要解码的预测帧就越多)、seek响应的时间(缓冲时间)也越长。
        • 由于P、B帧的复杂度大于I帧,GOP值过大,过多的P、B帧会影响编码效率,使编码效率降低。
        • 但如果设置过小的GOP值,则需要提高视频的输出码率,以确保画面质量不会降低,故会增加网络带宽。
    • IDR 帧对齐

      IDR帧(Instantaneous Decoding Refresh Picture),即时解码刷新帧,是 I 帧的一种。

      与普通 I 帧的区别在于,一个 IDR 帧之后的所有帧都不能引用该 IDR 帧之前的帧的内容

      在编码和解码中为了方便,将首个I帧和其他I帧区别开,称为IDR,这样就方便控制编码和解码流程。

      IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。

      普通I帧不具有随机访问的能力,这个功能是由IDR承担。

      视频播放时,播放器一般都支持随机seek(拖动)到指定位置,而播放器直接选择到指定位置附近的 IDR 帧进行播放最为便捷,因为可以明确知道该 IDR 帧之后的所有帧都不会引用其之前的其他 I 帧,从而避免较为复杂的反向解析。

      在对同一个视频进行多码率转码时,如果指定 IDR 帧对齐(IDR Frame Alignment),则意味着所有输出视频的 IDR 帧在时间点、帧内容方面都保持精确同步,此时播放器便可实现多码率视频平滑切换,从而不会出现较为明显的切换卡顿。

    • 编码级别

      编码档次(Profile)是针对特定应用场景的特定编码功能的集合。

      • H.264 规定了三种主要级别:

        • Baseline:支持 I/P 帧,只支持无交错(Progressive)和 CAVLC,一般用于低阶或需要额外容错的应用,比如视频通话、手机视频等即时通信领域;
        • Main:提供 I/P/B 帧,支持无交错(Progressive)和交错(Interlaced),同样提供对于 CAVLC 和 CABAC 的支持,用于主流消费类电子产品规格如低解码(相对而言)的 MP4、便携的视频播放器、PSP 和 iPod 等;
        • High:在 Main 的基础上增加了 8x8 内部预测、自定义量化、无损视频编码和更多的 YUV 格式(如 4:4:4),用于广播及视频碟片存储(蓝光影片),高清电视的应用。
    • 比特率

      每秒传送的比特(bit)数,单位为bps(Bit Per Second),比特率越高,传送的数据越大。

      在视频领域,比特率等同于码率。比特率表示经过编码(压缩)后的音、视频数据每秒钟需要用多少个比特来表示,而比特就是二进制里面最小的单位,要么是0,要么是1。

      与码率类似,比特率与音、视频压缩的关系,简单的说就是比特率越高,音、视频的质量就越好,但编码后的文件就越大

    • 码率控制方法

      • VBR(Variable Bitrate)

        动态比特率,也就是没有固定的比特率,音视频压缩软件在压缩时根据音视频数据的复杂程度即时确定使用什么比特率,这是以质量为前提兼顾文件大小的方式。

      • CBR(Constant Bitrate)

        固定比特率,指文件从头到尾都是一种位速率。相对于VBR和ABR来讲,它压缩出来的文件体积很大,而且视频质量相对于VBR和ABR不会有明显的提高。

      • ABR(Average Bitrate)

        平均比特率,是VBR的一种插值参数。LAME针对CBR不佳的文件体积比和VBR生成文件大小不定的特点独创了这种编码模式。ABR在指定的文件大小内,以每50帧(30帧约1秒)为一段,低频和不敏感频率使用相对低的流量,高频和大动态表现时使用高流量,可以做为VBR和CBR的一种折衷选择。

        ABR在一定的时间范围内达到设定的码率,但是局部码率峰值可以超过设定的码率,平均码率恒定。ABR是VBR的改良版,能确保输出平均码率在合理范围,且在这个范围内,还是动态根据复杂度编码,也是阿里云默认的编码控制方式。

    • 采样率

      采样速度或者采样频率,定义了每秒从连续信号中提取并组成离散信号的采样个数,单位为赫兹(HZ)。

      采样率是指将模拟信号转换成数字信号时的采样频率,也就是单位时间内采样多少点,采样频率越高声音的还原就越真实越自然。

    • 声道、声道数

      声音在录制(或播放)时,在不同空间位置采集(或播放)的相互独立的音频信号。

      所谓声道数,也就是声音录制时的音源数量或播放时的扬声器数量。

    • UTC时间(ISO 8601标准时间格式)

      世界标准时间,由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC。

      ISO8601标准格式为:YYYY-MM-DDThh:mm:ssZ。例如:2017-01-11T12:00:00Z 表示北京时间2017年1月11日20点0分0秒。

      注:北京时间与UTC的时差为+8,也就是UTC+8。

  • 使用

    开通服务->上传资源->资源管理->配置管理

    • 域名管理

      • 配置域名、CNAME

      • 回源、缓存、加速配置

      • 访问控制

        • Refer防盗链配置黑白名单
        • URL鉴权
          • 实现原理
            1. CDN客户站点提供加密URL,URL中包含权限验证信息。
            2. 用户使用加密后的URL向加速节点发起请求。
            3. 加速节点对加密URL中的权限信息进行验证,判断请求的合法性。正常响应合法请求,拒绝非法请求。
        • IP黑白名单
      • 过滤参数(URL问号)

      • 带宽峰值监控

      • 拖拽播放(mp4/flv)

      • HLS标准加密参数透传

      • 安全下载

    • 资源监控

      带宽、流量。回源带宽、回源流量。请求次数、QPS。HTTP Code。

      PV、UV、用户区域分布、运营商占比。VV、人均播放次数、人均播放时长、单次观看时长分布。

      排名、区域、总流量、流量占比、访问次数、访问占比、响应时间。

      各个加速域名的访问排名。

      查询指定域名的流量和带宽峰值数据。

      查询当前账号下从不同的点播存储区域直接下载视频源文件产生(未经过CDN加速)的累计流量。

      查询当前账号下存储空间占用大小以及变化情况。

      查询不同清晰度的转码时长统计。

      查询智能审核和视频DNA处理的视频时长数据。

    • 日志管理

    • 阿里云播放器

canvas实现刮刮卡

  1. 参考链接:
  1. 详解
  • 思路

    • canvas一开始盖住div
    • 鼠标经过的路径都画圆形开路,并且设置globalCompositeOperation为destination-out,使鼠标经过的路径都变成透明
  • 实现

    html
    &lt;canvas id="canvas" width="400" height="100">&lt;/canvas>
    &lt;div class="text">恭喜您获得100w&lt;/div>
    &lt;style>
            * {
                margin: 0;
                padding: 0;
            }
            .text {
                position: absolute;
                left: 130px;
                top: 35px;
                z-index: -1;
            }
    &lt;/style>
    
    
    &lt;script>
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    
    // 填充的颜色
    ctx.fillStyle = 'darkgray'
    // 填充矩形 fillRect(起始X,起始Y,终点X,终点Y)
    ctx.fillRect(0, 0, 400, 100)
    ctx.fillStyle = '#fff'
    // 绘制填充文字
    ctx.fillText('刮刮卡', 180, 50)
    
    let isDraw = false
    canvas.onmousedown = function () {
        isDraw = true
    }
    canvas.onmousemove = function (e) {
        if (!isDraw) return
        // 计算鼠标在canvas里的位置
        const x = e.pageX - canvas.offsetLeft
        const y = e.pageY - canvas.offsetTop
        // 设置globalCompositeOperation
        ctx.globalCompositeOperation = 'destination-out'
        // 画圆
        ctx.arc(x, y, 10, 0, 2 * Math.PI)
        // 填充圆形
        ctx.fill()
    }
    canvas.onmouseup = function () {
        isDraw = false
    }
    &lt;/script>
    

老虎机数字滚动实现

  1. 参考链接:
  1. 详解
  • 思路

    1. 做垂直轮播图
    2. filter: blur()沿着y轴模糊
    3. 快速滚动,利用视觉模糊,最终无论滚动到哪里,直接定位到结果
  • 实现

    • 组件

      html
      &lt;template>
        &lt;component
          :is="as"
          class="scroll-num"
          :style="{ '--i': i, '--delay': delay }"
        >
          &lt;ul ref="ul">
            &lt;li>0&lt;/li>
            &lt;li>1&lt;/li>
            &lt;li>2&lt;/li>
            &lt;li>3&lt;/li>
            &lt;li>4&lt;/li>
            &lt;li>5&lt;/li>
            &lt;li>6&lt;/li>
            &lt;li>7&lt;/li>
            &lt;li>8&lt;/li>
            &lt;li>9&lt;/li>
            &lt;li>0&lt;/li>
          &lt;/ul>
      
          &lt;svg width="0" height="0">
            &lt;filter id="blur">
              &lt;feGaussianBlur
                in="SourceGraphic"
                :stdDeviation="`0 ${blur}`"
              />
            &lt;/filter>
          &lt;/svg>
        &lt;/component>
      &lt;/template>
      
      &lt;script>
      export default {
        name: 'ScrollNum',
        props: {
          as: {
            type: String,
            default: 'div'
          },
          i: {
            type: Number,
            default: 0,
            validator: v => v &lt; 10 && v >= 0 && Number.isInteger(v)
          },
          delay: {
            type: Number,
            default: 1
          },
          blur: {
            type: Number,
            default: 2
          }
        },
        data: () => ({ timer: null }),
        mounted () {
          const ua = navigator.userAgent.toLowerCase()
          const testUA = regexp => regexp.test(ua)
          const isSafari = testUA(/safari/g) && !testUA(/chrome/g)
      
          // Safari浏览器的兼容代码
          isSafari && (this.timer = setTimeout(() => {
            this.$refs.ul.setAttribute('style', `
              animation: none;
              transform: translateY(calc(var(--i) * -9.09%))
            `)
          }, this.delay * 1000))
        },
        beforeDestroy () { clearTimeout(this.timer) }
      }
      &lt;/script>
      
      &lt;style scoped>
      .scroll-num {
        width: var(--width, 20px);
        height: var(--height, calc(var(--width, 20px) * 1.8));
        color: var(--color, #333);
        font-size: var(--height, calc(var(--width, 20px) * 1.1));
        line-height: var(--height, calc(var(--width, 20px) * 1.8));
        text-align: center;
        overflow: hidden;
        animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards;
      }
      
      ul {
        padding: 0;
        margin: 0;
        list-style: none;
        animation: move .3s linear infinite,
        bounce-in-down 1s calc(var(--delay) * 1s) forwards
      }
      
      @keyframes move {
        from {
          transform: translateY(-90%);
          filter: url(#blur)
        }
        to {
          transform: translateY(1%);
          filter: url(#blur)
        }
      }
      
      @keyframes bounce-in-down {
        from {
          transform: translateY(calc(var(--i) * -9.09% - 7%));
          filter: none
        }
        25% { transform: translateY(calc(var(--i) * -9.09% + 3%)) }
        50% { transform: translateY(calc(var(--i) * -9.09% - 1%)) }
        70% { transform: translateY(calc(var(--i) * -9.09% + .6%)) }
        85% { transform: translateY(calc(var(--i) * -9.09% - .3%)) }
        to { transform: translateY(calc(var(--i) * -9.09%)) }
      }
      
      @keyframes enhance-bounce-in-down {
        25% { transform: translateY(8%) }
        50% { transform: translateY(-4%) }
        70% { transform: translateY(2%) }
        85% { transform: translateY(-1%) }
        to { transform: translateY(0) }
      }
      &lt;/style>
      
    • 调用

      html
      &lt;template>
        &lt;ul class="flex">
          &lt;ScrollNum
            v-for="(number, idx) of numArr"
            :key="idx"
            :i="number"
            :delay="idx + 2.5"
            as="li"
            class="num"
          />
        &lt;/ul>
      &lt;/template>
      
      &lt;script>
      import ScrollNum from './components/ScrollNum.vue'
      
      export default {
        name: 'App',
        components: { ScrollNum },
        data: () => ({ num: 886 }),
        computed: {
          numArr () {
            const str = String(this.num)
      
            return [parseInt(str[0]), parseInt(str[1]), parseInt(str[2])]
          }
        }
      }
      &lt;/script>
      
      &lt;style>
      .flex {
        display: flex;
      }
      ul {
        padding: 0;
        margin: 0;
        list-style: none;
      }
      .num {
        --width: 26px;
        margin-right: 6px;
        border: 1px solid black;
        border-radius: 8px
      }
      &lt;/style>
      

带图带事件的桌面通知

  1. 参考链接:
  1. 详解
javascript
function doNotify(title, options = {}, events = {}) {
    const notification = new Notification(title, options);
    for (let event in events) {
        notification[event] = events[event];
    }
}

function notify(title, options = {}, events = {}) {
    if (!("Notification" in window)) {
        return console.error("This browser does not support desktop notification");
    }
    else if (Notification.permission === "granted") {
        doNotify(title, options, events);
    } else if (Notification.permission !== "denied") {
        Notification.requestPermission().then(function (permission) {           
            if (permission === "granted") {
                doNotify(title, options, events);
            }
        });
    }
}

notify("中奖提示", {
      icon: "https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/f1a9f122e925aeef5e4534ff7f706729~300x300.image",
      body: "恭喜你,掘金签到一等奖",
      tag: "prize"
  }, {
      onclick(ev) {
          console.log(ev);
          ev.target.close();
          window.focus();
      }
  })

生成UUID

  1. 参考链接:
  1. 详解
javascript
function genUUID() {
    const url = URL.createObjectURL(new Blob([]));
    // const uuid = url.split("/").pop();
    const uuid = url.substring(url.lastIndexOf('/')+ 1);
    URL.revokeObjectURL(url);
    return uuid;
}

genUUID() // cd205467-0120-47b0-9444-894736d873c7

function uuidv4() {
  return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
}

uuidv4() // 38aa1602-ba78-4368-9235-d8703cdb6037

非活跃状态淡出

  1. 参考链接:
  1. 详解
css
.vjs-fade-out {
  display: block;
  visibility: hidden;
  opacity: 0;

  -webkit-transition: visibility 1.5s, opacity 1.5s;
    -moz-transition: visibility 1.5s, opacity 1.5s;
      -ms-transition: visibility 1.5s, opacity 1.5s;
      -o-transition: visibility 1.5s, opacity 1.5s;
          transition: visibility 1.5s, opacity 1.5s;

  /* 等一会儿,然后淡出控制栏 */
  -webkit-transition-delay: 2s;
    -moz-transition-delay: 2s;
      -ms-transition-delay: 2s;
      -o-transition-delay: 2s;
          transition-delay: 2s;
}
javascript
player.on('mouseout', function(){ 
  controlBar.addClass('vjs-fade-out'); 
});

player.on('mouseover', function(){ 
  controlBar.removeClass('vjs-fade-out'); 
});

浏览器播放m3u8

  1. 参考链接:
  1. 详解
  • 写在前面

    • 视频需同域,否则需添加CORS请求头允许跨域

    • 涉及hls的浏览器兼容性:

      • Chrome 39+ for Android
      • Chrome 39+ for Desktop
      • Firefox 41+ for Android
      • Firefox 42+ for Desktop
      • IE11 for Windows 8.1+
      • Edge for Windows 10+
      • Safari 8+ for MacOS 10.10+
      • Safari for ipadOS 13+
    • IOS Safari 已经支持video的hls播放,但不支持MediaSource的API。因此如果一个平台既不支持MediaSource,也没原生HLS,则无法播放hls。

  • hls(HTTP Live Streaming)

    html
    &lt;script src="https://cdn.jsdelivr.net/npm/hls.js@latest">&lt;/script>
    &lt;video id="video" controls>&lt;/video>
    &lt;script>
        var video = document.getElementById('video');
        var videoSrc = './p1.m3u8';
        if (Hls.isSupported()) {
            var hls = new Hls();
            hls.loadSource(videoSrc);
            hls.attachMedia(video);
        }
        else if (video.canPlayType('application/vnd.apple.mpegurl')) {
            video.src = videoSrc;
        }
    &lt;/script>
    
  • vhs(videojs-http-streaming)

    html
    &lt;video-js id="vid1" class="vjs-default-skin">
        &lt;source src="./p1.m3u8" type="application/x-mpegURL">
    &lt;/video-js>
    &lt;link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
    &lt;script src="https://unpkg.com/video.js/dist/video.min.js">&lt;/script>
    &lt;script src="https://unpkg.com/browse/@videojs/[email protected]/dist/videojs-http-streaming.min.js">&lt;/script>
    &lt;script>
        var player = videojs('vid1');
        player.play();
    &lt;/script>
    
  • vch(videojs-contrib-hls,在videojs7中废弃)

    html
    &lt;script src="https://vjs.zencdn.net/ie8/1.1.1/videojs-ie8.min.js">&lt;/script>
    &lt;link href="https://vjs.zencdn.net/5.5.3/video-js.css" rel="stylesheet">
    &lt;video id="example-video" class="video-js vjs-default-skin" data-setup="{}" controls>
        &lt;source src="./p1.m3u8" type="application/x-mpegURL">
    &lt;/video>
    &lt;script src="https://unpkg.com/video.js/dist/video.min.js">&lt;/script>
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.12.2/videojs-contrib-hls.js" type="text/javascript">&lt;/script>
    &lt;script>
        var player = videojs('example-video');
        player.play();
    &lt;/script>
    

效率库

  1. 参考链接:
  1. 详解

拖拽上传

  1. 参考链接:
  1. 详解
html
&lt;!DOCTYPE html>
&lt;html>

&lt;head>
    &lt;meta charset="UTF-8">
    &lt;title>Document&lt;/title>
&lt;/head>

&lt;body>

    &lt;img src="" alt="" height="100px" width="100px">

    &lt;script>
        document.addEventListener("drop", preventDe);//松开拖拽触发
        document.addEventListener("dragleave", preventDe);//拖拽离开页面触发
        document.addEventListener("dragover", preventDe);//拖拽在页面滑动触发
        document.addEventListener("dragenter", preventDe);//拖拽进入页面触发

        function preventDe(e) {
            e.preventDefault();//不阻止默认行为,chrome对于图片、mp4会直接打开,对于其它文件会下载
            console.log(e,e.type,e.dataTransfer,e.dataTransfer.files[0])
        }

        document.addEventListener("drop", function (e) {
            e.preventDefault();
            console.log(e,1)
            var file = e.dataTransfer.files[0];
            //file.type; 文件类型
            //file.name;文件名
            //file.size; 文件大小 btye

            var img = document.getElementsByTagName("img")[0];
            var dataURL = URL.createObjectURL(file);
            img.src = dataURL;

            var formData = new FormData();
            formData.append("file", file);
            // 发送XHR
            //XHR.send(formData);
        })

    &lt;/script>

&lt;/body>

&lt;/html>

高并发页面处理

  1. 参考链接:
  1. 详解
  • 背景

    核酸查询服务

    静态页面

    现象:拒绝服务

  • 问题

    1. 页面引用了过多的静态资源

    页面使用jquery+weui,引入了3个css文件以及5个js文件,服务器使用了gzip+http2协议,页面所有资源加起来不到400kB。

    这个页面并没有使用cdn分发,所有静态文件都由同一个服务器返回,如果代理服务器没有做优化,那么每次访问都会有8次io,在高并发情况下,导致服务器不堪重负。

    优化:

    • css/js全部写在页面内,至少可以让用户在html加载完成的时候就能看到整个页面了,而不是等待css/js文件的加载。
    • 干掉favicon,这个页面的favicon有17Kb之大,浪费用户流量且增加了服务器的io。
    1. 页面的http缓存配置问题

    nginx没配置缓存策略,使用默认配置,导致浏览器强缓存,导致页面有修改,用户必须手动清缓存

    优化:

    • 为html页面配置expires:epoch。这样浏览器访问该页面的时候会使用304缓存策略。
    • 为js/css等静态文件配置expires:1y。这样静态文件将直接在本地加载而不是去服务器获取。
    1. 页面安全问题

    页面请求以明文query string传输,请求所经过的所有中间层均可以获取用户信息。

    优化:

    • 改为post请求,body提交用户数据
    • 如果非要使用query string,保证使用安全的加密方法(不推荐)