• websocket通用封装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    import store from '@/store';

    export function WebSocketFunc(ws, wsUrl, callback, callback1) {
    var lockReconnect = false; // 避免重复连接
    function createWebSocket(url) {
    try {
    console.log('wsUrl', wsUrl);
    if ('WebSocket' in window) {
    ws = new WebSocket(url);
    } else if ('MozWebSocket' in window) {
    ws = new MozWebSocket(url);
    } else {
    ws = new SockJS(url);
    }
    callback1 && callback1(ws);
    initEventHandle();
    } catch (e) {
    reconnect(wsUrl);
    }
    }

    function initEventHandle() {
    ws.onclose = function (evnt) {
    // console.log('websocket服务关闭了');
    console.log(
    'websocket 断开: ' +
    'code:' +
    evnt.code +
    ' ' +
    'reason:' +
    evnt.reason +
    ' ' +
    'wasClean:' +
    evnt.wasClean
    );
    // reconnect(evnt.target.url);
    // 1.前端主动关闭连接 2.运营商关闭连接
    // if (evnt.url === wsUrl) {
    // console.log('前端主动连接中断');
    // } else {
    // // 可能是运营商关闭连接
    // reconnect(evnt.target.url);
    // console.log('心跳重连...');
    // }
    if (store.state.websocket.isClose) {
    console.log('前端主动连接中断');
    store.dispatch('websocket/getWsClose', false);
    } else {
    reconnect(evnt.target.url);
    console.log('心跳重连...');
    }
    };
    ws.onerror = function () {
    console.log('websocket服务出错了');
    reconnect(wsUrl);
    };
    ws.onopen = function () {
    console.log('send device sn: ', store.getters['user/curDevice'].sn);
    ws.send(store.getters['user/curDevice'].sn);
    // 心跳检测重置
    heartCheck.reset().start();
    };
    ws.onmessage = function (evnt) {
    // 如果获取到消息,心跳检测重置
    // 拿到任何消息都说明当前连接是正常的
    // console.log('websocket服务获得数据了')
    // 接受消息后的UI变化
    doWithMsg(evnt.data);
    heartCheck.reset().start();
    };

    // 收到消息推送
    function doWithMsg(msg) {
    console.log(
    "websocket's msg",
    msg.indexOf('成功建立websocket连接') == 0 ||
    !msg.indexOf('Pong') ||
    msg.indexOf('设备') != -1 ||
    msg.indexOf('连接成功') != -1
    ? msg
    : JSON.parse(msg)
    );
    callback && callback(msg);
    }
    }

    function reconnect(url) {
    console.log('正在重连...');
    if (lockReconnect) return;
    lockReconnect = true;
    // 没连接上会一直重连,设置延迟避免请求过多
    setTimeout(function () {
    createWebSocket(url);
    lockReconnect = false;
    }, 2000);
    }

    // 心跳检测
    var heartCheck = {
    timeout: 10000, // 5秒
    timeoutObj: null,
    serverTimeoutObj: null,
    reset: function () {
    clearTimeout(this.timeoutObj);
    clearTimeout(this.serverTimeoutObj);
    return this;
    },
    start: function () {
    var self = this;
    this.timeoutObj = setTimeout(function () {
    // 这里发送一个心跳,后端收到后,返回一个心跳消息
    // onmessage拿到返回的心跳就说明连接正常
    ws.send('Ping');
    console.log('Ping');
    self.serverTimeoutObj = setTimeout(function () {
    // 如果超过一定时间还没重置,说明后端主动断开了
    ws.close(); // 如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
    }, self.timeout);
    }, this.timeout);
    }
    };

    // 初始化websocket
    createWebSocket(wsUrl);
    }
  • vue基于Blob.js和 Export2Excel.js做前端导出

    1. 安装三个依赖包

    1
    2
    3
    npm install -S file-saver
    npm install -S xlsx
    npm install -D script-loader

    2. 导入两个js

    下载Blob.js和Export2Excel.js,在src目录下新建Excel文件夹,里面放入Blob.js和Export2Excel.js两个JS文件,下面有

    3. main.js 引入文件

    1
    2
    import Blob from './Excel/Blob'
    import Export2Excel from './Excel/Export2Excel.js'

    4. 组件中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //导出的方法
    exportExcel() {
    require.ensure([], () => {
    const { export_json_to_excel } = require('../Excel/Export2Excel');//注意这个Export2Excel路径
    const tHeader = ['序号', '昵称', '姓名']; // 上面设置Excel的表格第一行的标题
    const filterVal = ['index', 'nickName', 'name']; // 上面的index、nickName、name是tableData里对象的属性key值
    const list = this.tableData; //把要导出的数据tableData存到list
    const data = this.formatJson(filterVal, list);
    export_json_to_excel(tHeader, data, '列表excel');//最后一个是表名字
    })
    },
    formatJson(filterVal, jsonData) {
    return jsonData.map(v => filterVal.map(j => v[j]))
    }

    5.如果报错,可能路径问题

    把Export2Excel.js里面require(‘script-loader!vendor/Blob’)改为 require(’./Blob.js’)

    6 Blob.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    /* eslint-disable */
    /* Blob.js
    * A Blob implementation.
    * 2014-05-27
    *
    * By Eli Grey, http://eligrey.com
    * By Devin Samarin, https://github.com/eboyjr
    * License: X11/MIT
    * See LICENSE.md
    */

    /*global self, unescape */
    /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
    plusplus: true */

    /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */

    (function (view) {
    "use strict";

    view.URL = view.URL || view.webkitURL;

    if (view.Blob && view.URL) {
    try {
    new Blob;
    return;
    } catch (e) {}
    }

    // Internally we use a BlobBuilder implementation to base Blob off of
    // in order to support older browsers that only have BlobBuilder
    var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
    var
    get_class = function(object) {
    return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
    }
    , FakeBlobBuilder = function BlobBuilder() {
    this.data = [];
    }
    , FakeBlob = function Blob(data, type, encoding) {
    this.data = data;
    this.size = data.length;
    this.type = type;
    this.encoding = encoding;
    }
    , FBB_proto = FakeBlobBuilder.prototype
    , FB_proto = FakeBlob.prototype
    , FileReaderSync = view.FileReaderSync
    , FileException = function(type) {
    this.code = this[this.name = type];
    }
    , file_ex_codes = (
    "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
    + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
    ).split(" ")
    , file_ex_code = file_ex_codes.length
    , real_URL = view.URL || view.webkitURL || view
    , real_create_object_URL = real_URL.createObjectURL
    , real_revoke_object_URL = real_URL.revokeObjectURL
    , URL = real_URL
    , btoa = view.btoa
    , atob = view.atob

    , ArrayBuffer = view.ArrayBuffer
    , Uint8Array = view.Uint8Array
    ;
    FakeBlob.fake = FB_proto.fake = true;
    while (file_ex_code--) {
    FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
    }
    if (!real_URL.createObjectURL) {
    URL = view.URL = {};
    }
    URL.createObjectURL = function(blob) {
    var
    type = blob.type
    , data_URI_header
    ;
    if (type === null) {
    type = "application/octet-stream";
    }
    if (blob instanceof FakeBlob) {
    data_URI_header = "data:" + type;
    if (blob.encoding === "base64") {
    return data_URI_header + ";base64," + blob.data;
    } else if (blob.encoding === "URI") {
    return data_URI_header + "," + decodeURIComponent(blob.data);
    } if (btoa) {
    return data_URI_header + ";base64," + btoa(blob.data);
    } else {
    return data_URI_header + "," + encodeURIComponent(blob.data);
    }
    } else if (real_create_object_URL) {
    return real_create_object_URL.call(real_URL, blob);
    }
    };
    URL.revokeObjectURL = function(object_URL) {
    if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
    real_revoke_object_URL.call(real_URL, object_URL);
    }
    };
    FBB_proto.append = function(data/*, endings*/) {
    var bb = this.data;
    // decode data to a binary string
    if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
    var
    str = ""
    , buf = new Uint8Array(data)
    , i = 0
    , buf_len = buf.length
    ;
    for (; i < buf_len; i++) {
    str += String.fromCharCode(buf[i]);
    }
    bb.push(str);
    } else if (get_class(data) === "Blob" || get_class(data) === "File") {
    if (FileReaderSync) {
    var fr = new FileReaderSync;
    bb.push(fr.readAsBinaryString(data));
    } else {
    // async FileReader won't work as BlobBuilder is sync
    throw new FileException("NOT_READABLE_ERR");
    }
    } else if (data instanceof FakeBlob) {
    if (data.encoding === "base64" && atob) {
    bb.push(atob(data.data));
    } else if (data.encoding === "URI") {
    bb.push(decodeURIComponent(data.data));
    } else if (data.encoding === "raw") {
    bb.push(data.data);
    }
    } else {
    if (typeof data !== "string") {
    data += ""; // convert unsupported types to strings
    }
    // decode UTF-16 to binary string
    bb.push(unescape(encodeURIComponent(data)));
    }
    };
    FBB_proto.getBlob = function(type) {
    if (!arguments.length) {
    type = null;
    }
    return new FakeBlob(this.data.join(""), type, "raw");
    };
    FBB_proto.toString = function() {
    return "[object BlobBuilder]";
    };
    FB_proto.slice = function(start, end, type) {
    var args = arguments.length;
    if (args < 3) {
    type = null;
    }
    return new FakeBlob(
    this.data.slice(start, args > 1 ? end : this.data.length)
    , type
    , this.encoding
    );
    };
    FB_proto.toString = function() {
    return "[object Blob]";
    };
    FB_proto.close = function() {
    this.size = this.data.length = 0;
    };
    return FakeBlobBuilder;
    }(view));

    view.Blob = function Blob(blobParts, options) {
    var type = options ? (options.type || "") : "";
    var builder = new BlobBuilder();
    if (blobParts) {
    for (var i = 0, len = blobParts.length; i < len; i++) {
    builder.append(blobParts[i]);
    }
    }
    return builder.getBlob(type);
    };
    }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

    7Export2Excel.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    //Export2Excel.js
    /* eslint-disable */
    require('script-loader!file-saver');
    require('script-loader!../Excel/Blob');//转二进制用 这边要写你的blob的实际地址
    require('script-loader!xlsx/dist/xlsx.core.min');

    function generateArray(table) {
    var out = [];
    var rows = table.querySelectorAll('tr');
    var ranges = [];
    for (var R = 0; R < rows.length; ++R) {
    var outRow = [];
    var row = rows[R];
    var columns = row.querySelectorAll('td');
    for (var C = 0; C < columns.length; ++C) {
    var cell = columns[C];
    var colspan = cell.getAttribute('colspan');
    var rowspan = cell.getAttribute('rowspan');
    var cellValue = cell.innerText;
    if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;

    //Skip ranges
    ranges.forEach(function(range) {
    if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
    for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
    }
    });

    //Handle Row Span
    if (rowspan || colspan) {
    rowspan = rowspan || 1;
    colspan = colspan || 1;
    ranges.push({
    s: {
    r: R,
    c: outRow.length
    },
    e: {
    r: R + rowspan - 1,
    c: outRow.length + colspan - 1
    }
    });
    };

    //Handle Value
    outRow.push(cellValue !== "" ? cellValue : null);

    //Handle Colspan
    if (colspan)
    for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
    }
    out.push(outRow);
    }
    return [out, ranges];
    };

    function datenum(v, date1904) {
    if (date1904) v += 1462;
    var epoch = Date.parse(v);
    return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
    }

    function sheet_from_array_of_arrays(data, opts) {
    var ws = {};
    var range = {
    s: {
    c: 10000000,
    r: 10000000
    },
    e: {
    c: 0,
    r: 0
    }
    };
    for (var R = 0; R != data.length; ++R) {
    for (var C = 0; C != data[R].length; ++C) {
    if (range.s.r > R) range.s.r = R;
    if (range.s.c > C) range.s.c = C;
    if (range.e.r < R) range.e.r = R;
    if (range.e.c < C) range.e.c = C;
    var cell = {
    v: data[R][C]
    };
    if (cell.v == null) continue;
    var cell_ref = XLSX.utils.encode_cell({
    c: C,
    r: R
    });

    if (typeof cell.v === 'number') cell.t = 'n';
    else if (typeof cell.v === 'boolean') cell.t = 'b';
    else if (cell.v instanceof Date) {
    cell.t = 'n';
    cell.z = XLSX.SSF._table[14];
    cell.v = datenum(cell.v);
    } else cell.t = 's';

    ws[cell_ref] = cell;
    }
    }
    if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
    return ws;
    }

    function Workbook() {
    if (!(this instanceof Workbook)) return new Workbook();
    this.SheetNames = [];
    this.Sheets = {};
    }

    function s2ab(s) {
    var buf = new ArrayBuffer(s.length);
    var view = new Uint8Array(buf);
    for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
    return buf;
    }

    export function export_table_to_excel(id) {
    var theTable = document.getElementById(id);
    console.log('a')
    var oo = generateArray(theTable);
    var ranges = oo[1];

    /* original data */
    var data = oo[0];
    var ws_name = "SheetJS";
    console.log(data);

    var wb = new Workbook(),
    ws = sheet_from_array_of_arrays(data);

    /* add ranges to worksheet */
    // ws['!cols'] = ['apple', 'banan'];
    ws['!merges'] = ranges;

    /* add worksheet to workbook */
    wb.SheetNames.push(ws_name);
    wb.Sheets[ws_name] = ws;

    var wbout = XLSX.write(wb, {
    bookType: 'xlsx',
    bookSST: false,
    type: 'binary'
    });

    saveAs(new Blob([s2ab(wbout)], {
    type: "application/octet-stream"
    }), "test.xlsx")
    }

    function formatJson(jsonData) {
    console.log(jsonData)
    }
    export function export_json_to_excel(th, jsonData, defaultTitle) {

    /* original data */

    var data = jsonData;
    data.unshift(th);
    var ws_name = "SheetJS";

    var wb = new Workbook(),
    ws = sheet_from_array_of_arrays(data);


    /* add worksheet to workbook */
    wb.SheetNames.push(ws_name);
    wb.Sheets[ws_name] = ws;

    var wbout = XLSX.write(wb, {
    bookType: 'xlsx',
    bookSST: false,
    type: 'binary'
    });
    var title = defaultTitle || '列表'
    saveAs(new Blob([s2ab(wbout)], {
    type: "application/octet-stream"
    }), title + ".xlsx")
    };
  • localStorage容量太小?试试它们

    localStorage 是前端本地存储的一种,其容量一般在 5M-10M 左右,用来缓存一些简单的数据基本够用,毕竟定位也不是大数据量的存储。

    在某些场景下 localStorage 的容量就会有点捉襟见肘,其实浏览器是有提供大数据量的本地存储的如 IndexedDB 存储数据大小一般在 250M 以上。

    弥补了localStorage容量的缺陷,但是使用要比localStorage复杂一些 mdn IndexedDB

    不过已经有大佬造了轮子封装了一些调用过程使其使用相对简单,下面我们一起来看一下

    localforage

    localforage 拥有类似 localStorage API,它能存储多种类型的数据如 **Array ArrayBuffer Blob Number Object String**,而不仅仅是字符串。

    这意味着我们可以直接存 对象、数组类型的数据避免了 JSON.stringify 转换数据的一些问题。

    存储其他数据类型时需要转换成上边对应的类型,比如vue3中使用 reactive 定义的数据需要使用toRaw 转换成原始数据进行保存, ref 则直接保存 xxx.value 数据即可。

    安装

    下载最新版本 或使用 npm bower 进行安装使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 引入下载的 localforage 即可使用
    <script src="localforage.js"></script>
    <script>console.log('localforage is: ', localforage);</script>

    # 通过 npm 安装:
    npm install localforage

    # 或通过 bower:
    bower install localforage

    使用

    提供了与 localStorage 相同的api,不同的是它是异步的调用返回一个 Promise 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    localforage.getItem('somekey').then(function(value) {
    // 当离线仓库中的值被载入时,此处代码运行
    console.log(value);
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 回调版本:
    localforage.getItem('somekey', function(err, value) {
    // 当离线仓库中的值被载入时,此处代码运行
    console.log(value);
    });

    提供的方法有

    • getItem 根据数据的 key 获取数据 差不多返回 null
    • setItem 根据数据的 key 设置数据(存储undefined时getItem获取会返回 null
    • removeItem 根据key删除数据
    • length 获取key的数量
    • key 根据 key 的索引获取其名
    • keys 获取数据仓库中所有的 key。
    • iterate 迭代数据仓库中的所有 value/key 键值对。

    配置

    完整配置可查看文档 这里说个作者觉得有用的

    localforage.config({ name: 'My-localStorage' }); 设置仓库的名字,不同的名字代表不同的仓库,当一个应用需要多个本地仓库隔离数据的时候就很有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const store = localforage.createInstance({
    name: "nameHere"
    });

    const otherStore = localforage.createInstance({
    name: "otherName"
    });

    // 设置某个数据仓库 key 的值不会影响到另一个数据仓库
    store.setItem("key", "value");
    otherStore.setItem("key", "value2");

    同时也支持删除仓库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 调用时,若不传参,将删除当前实例的 “数据仓库” 。
    localforage.dropInstance().then(function() {
    console.log('Dropped the store of the current instance').
    });

    // 调用时,若参数为一个指定了 name 和 storeName 属性的对象,会删除指定的 “数据仓库”。
    localforage.dropInstance({
    name: "otherName",
    storeName: "otherStore"
    }).then(function() {
    console.log('Dropped otherStore').
    });

    // 调用时,若参数为一个仅指定了 name 属性的对象,将删除指定的 “数据库”(及其所有数据仓库)。
    localforage.dropInstance({
    name: "otherName"
    }).then(function() {
    console.log('Dropped otherName database').
    });

    idb-keyval

    idb-keyval是用IndexedDB实现的一个超级简单的基于 promise 的键值存储。

    安装

    npm npm install idb-keyval

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 全部引入
    import idbKeyval from 'idb-keyval';

    idbKeyval.set('hello', 'world')
    .then(() => console.log('It worked!'))
    .catch((err) => console.log('It failed!', err));

    // 按需引入会摇树
    import { get, set } from 'idb-keyval';

    set('hello', 'world')
    .then(() => console.log('It worked!'))
    .catch((err) => console.log('It failed!', err));

    get('hello').then((val) => console.log(val));

    浏览器直接引入 <script src="https://cdn.jsdelivr.net/npm/idb-keyval@6/dist/umd.js"></script>

    暴露的全局变量是 idbKeyval 直接使用即可。

    提供的方法

    由于其没有中文的官网,会把例子及自己的理解附上

    set 设置数据

    值可以是 数字、数组、对象、日期、Blobs等,尽管老Edge不支持null。

    键可以是数字、字符串、日期,(IDB也允许这些值的数组,但IE不支持)。

    1
    2
    3
    4
    5
    6
    import { set } from 'idb-keyval';

    set('hello', 'world')
    .then(() => console.log('It worked!'))
    .catch((err) => console.log('It failed!', err));

    setMany 设置多个数据

    一个设置多个值,比一个一个的设置更快

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { set, setMany } from 'idb-keyval';

    // 不应该:
    Promise.all([set(123, 456), set('hello', 'world')])
    .then(() => console.log('It worked!'))
    .catch((err) => console.log('It failed!', err));

    // 这样做更快:
    setMany([
    [123, 456],
    ['hello', 'world'],
    ])
    .then(() => console.log('It worked!'))
    .catch((err) => console.log('It failed!', err));

    get 获取数据

    如果没有键,那么val将返回undefined的。

    1
    2
    3
    4
    5
    import { get } from 'idb-keyval';

    // logs: "world"
    get('hello').then((val) => console.log(val));

    getMany 获取多个数据

    一次获取多个数据,比一个一个获取数据更快

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { get, getMany } from 'idb-keyval';

    // 不应该:
    Promise.all([get(123), get('hello')]).then(([firstVal, secondVal]) =>
    console.log(firstVal, secondVal),
    );

    // 这样做更快:
    getMany([123, 'hello']).then(([firstVal, secondVal]) =>
    console.log(firstVal, secondVal),
    );

    del 删除数据

    根据 key 删除数据

    1
    2
    3
    4
    import { del } from 'idb-keyval';

    del('hello');

    delMany 删除多个数据

    一次删除多个键,比一个一个删除要快

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { del, delMany } from 'idb-keyval';

    // 不应该:
    Promise.all([del(123), del('hello')])
    .then(() => console.log('It worked!'))
    .catch((err) => console.log('It failed!', err));

    // 这样做更快:
    delMany([123, 'hello'])
    .then(() => console.log('It worked!'))
    .catch((err) => console.log('It failed!', err));

    update 排队更新数据,防止由于异步导致数据更新问题

    因为 getset 都是异步的使用他们来更新数据可能会存在问题如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Don't do this:
    import { get, set } from 'idb-keyval';

    get('counter').then((val) =>
    set('counter', (val || 0) + 1);
    );

    get('counter').then((val) =>
    set('counter', (val || 0) + 1);
    );

    上述代码我们期望的是 2 但实际结果是 1,我们可以在第一个回调执行第二次操作。

    更好的方法是使用 update 来更新数据

    1
    2
    3
    4
    5
    6
    // Instead:
    import { update } from 'idb-keyval';

    update('counter', (val) => (val || 0) + 1);
    update('counter', (val) => (val || 0) + 1);

    将自动排队更新,所以第一次更新将计数器设置为1,第二次更新将其设置为2

    clear 清除所有数据

    1
    2
    3
    4
    import { clear } from 'idb-keyval';

    clear();

    entries 返回 [key, value] 形式的数据

    1
    2
    3
    4
    5
    import { entries } from 'idb-keyval';

    // logs: [[123, 456], ['hello', 'world']]
    entries().then((entries) => console.log(entries));

    keys 获取所有数据的 key

    1
    2
    3
    4
    5
    import { keys } from 'idb-keyval';

    // logs: [123, 'hello']
    keys().then((keys) => console.log(keys));

    values 获取所有数据 value

    1
    2
    3
    4
    5
    import { values } from 'idb-keyval';

    // logs: [456, 'world']
    values().then((values) => console.log(values));

    createStore 自定义仓库

    文字解释:表 === store === 商店 一个意思

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 自定义数据库名称及表名称
    // 创建一个数据库: 数据库名称为 tang_shi, 表名为 table1
    const tang_shi_table1 = idbKeyval.createStore('tang_shi', 'table1')

    // 向对应仓库添加数据
    idbKeyval.set('add', 'table1 的数据', tang_shi_table1)

    // 默认创建的仓库名称为 keyval-store 表名为 keyval
    idbKeyval.set('add', '默认的数据')

    截屏2022-11-06 下午8.40.58.png

    使用 createStore 创建的数据库一个库只会创建一个表即:

    1
    2
    3
    4
    5
    6
    7
    8
    // 同一个库有不可以有两个表,custom-store-2 不会创建成功:
    const customStore = createStore('custom-db-name', 'custom-store-name');
    const customStore2 = createStore('custom-db-name', 'custom-store-2');

    // 不同的库 有相同的表名 这是可以的:
    const customStore3 = createStore('db3', 'keyval');
    const customStore4 = createStore('db4', 'keyval');

    promisifyRequest

    自己管理定制商店,这个没搞太明白,看文档中说既然都用到这个了不如直接使用idb 这个库

    总结

    本文介绍了两个 IndexedDB 的库,用来解决 localStorage 存储容量太小的问题

    localforageidb-keyval 之间我更喜欢 localforage 因为其与 localStorage 相似的api几乎没有上手成本。

    如果需要更加灵活的库可以看一下 dexie.jsPouchDBidbJsStore 或者 lovefield 之类的库

  • 你真的熟练运用HTML5了吗,这10个酷炫的H5特性你会几个?

    HTML5不是什么新鲜事。自初始版本(2008 年 1 月)以来,我们一直在使用它的几个功能。我再次仔细查看了 HTML5 功能列表。看看我发现了什么?到目前为止,我还没有真正使用过很多!

    在本文中,我列出了 10 个这样的HTML5功能,这些功能过去我用得不多,但现在发现它们很有用。我还创建了一个工作示例流程并托管在GitHub. 希望你也觉得它有用。让我们开始了解有关它们中的每一个的解释、代码和快速提示。

    🍖 一、详情标签

    <details>标签向用户提供按需详细信息。如果您需要按需向用户显示内容,请使用此标签。默认情况下,小部件是关闭的。打开时,它会展开并显示其中的内容。

    <summary>标签用于<details>为它指定一个可见的标题。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <details>
    <summary>Click Here to get the user details</summary>
    <table>
    <tr>
    <th>#</th>
    <th>Name</th>
    <th>Location</th>
    <th>Job</th>
    </tr>
    <tr>
    <td>1</td>
    <td>Adam</td>
    <td>Huston</td>
    <td>UI/UX</td>
    </tr>
    <tr>
    <td>2</td>
    <td>Bob</td>
    <td>London</td>
    <td>Machine Learning</td>
    </tr>
    <tr>
    <td>3</td>
    <td>Jack</td>
    <td>Australia</td>
    <td>UI Designer</td>
    </tr>
    <tr>
    <td>4</td>
    <td>Tapas</td>
    <td>India</td>
    <td>Blogger</td>
    </tr>
    </table>
    </details>

    看看它如何工作

    🎶 二、内容可编辑

    contenteditable是可以在元素上设置以使内容可编辑的属性。它适用于 DIV、P、UL 等元素。您必须指定它,例如,<element contenteditable="true|false">

    注意: 当contenteditable元素上没有设置属性时,它将从其父元素继承。

    代码

    1
    2
    3
    4
    5
    6
    <h2> Shoppping List(Content Editable) </h2>
    <ul class="content-editable" contenteditable="true">
    <li> 1. Milk </li>
    <li> 2. Bread </li>
    <li> 3. Honey </li>
    </ul>

    看看它如何工作

    快速提示

    span 或 div 元素可以使用它进行编辑,您可以使用 CSS 样式向其中添加任何丰富的内容。这将比使用输入字段处理它要好得多。去试一试!

    ✨ 三、地图

    <map>标签有助于定义图像映射。图像映射是其中包含一个或多个可点击区域的图像。地图标签带有一个<area>标签来确定可点击区域。可点击区域可以是这些形状、矩形、圆形或多边形区域之一。如果您不指定任何形状,它会考虑整个图像。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <div>
    <img src="circus.jpg" width="500" height="500" alt="Circus" usemap="#circusmap">

    <map name="circusmap">
    <area shape="rect" coords="67,114,207,254" href="elephant.htm">
    <area shape="rect" coords="222,141,318, 256" href="lion.htm">
    <area shape="rect" coords="343,111,455, 267" href="horse.htm">
    <area shape="rect" coords="35,328,143,500" href="clown.htm">
    <area shape="circle" coords="426,409,100" href="clown.htm">
    </map>
    </div>

    看看它如何工作

    快速提示

    图像地图有其自身的缺点,但您可以将其用于视觉演示。试试看一张全家福怎么样,然后深入到个人的照片(可以是我们一直珍视的旧照片!)。

    🏀 四、标记内容

    使用<mark>标签突出显示任何文本内容。

    1
    <p> 你知道吗,你可以仅使用 HTML 标签 <mark>"突出显示有趣的东西"</mark></p>

    看看它如何工作

    快速提示

    您可以随时使用 css 更改高亮颜色

    1
    2
    3
    4
    mark {
    background-color: green;
    color: #FFFFFF;
    }

    🎥 五、data-* 属性

    这些data-*属性用于存储页面或应用程序私有的自定义数据。存储的数据可用于 JavaScript 代码以创建进一步的用户体验。

    data-* 属性由两部分组成:

    • 属性名称不应包含任何大写字母,并且必须在前缀“data-”之后至少长一个字符
    • 属性值可以是任何字符串

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <h2> Know data attribute </h2>
    <div
    class="data-attribute"
    id="data-attr"
    data-custom-attr="You are just Awesome!">
    I have a hidden secret!
    </div>

    <button onclick="reveal()">Reveal</button>

    然后在 JavaScript 中,

    1
    2
    3
    4
    5
    function reveal() {
    let dataDiv = document.getElementById('data-attr');
    let value = dataDiv.dataset['customAttr'];
    document.getElementById('msg').innerHTML = `<mark>${value}</mark>`;
    }

    注意:要在 JavaScript 中读取这些属性的值,您可以使用getAttribute()它们的完整 HTML 名称(即 data-custom-attr),但标准定义了一种更简单的方法:使用dataset属性。

    看看它如何工作

    快速提示

    您可以使用它在页面上存储一些数据,然后使用 REST 调用将其传递给服务器。

    🏆 六、输出标签

    <output>标签表示的运算的结果。通常,此元素定义将用于显示某些计算的文本输出的区域。

    代码

    1
    2
    3
    4
    5
    <form oninput="x.value=parseInt(a.value) * parseInt(b.value)">
    <input type="number" id="a" value="0">
    * <input type="number" id="b" value="0">
    = <output name="x" for="a b"></output>
    </form>

    看看它如何工作

    快速提示

    如果您在客户端 JavaScript 中执行任何计算,并且希望结果反映在页面上,请使用<output>标记。您不必执行使用 获取元素的额外步骤getElementById()。

    🎻 七、数据列表

    <datalist>标签指定了一个预定义选项列表,并允许用户向其中添加更多选项。它提供了一项autocomplete功能,允许您通过预先输入获得所需的选项。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <form action="" method="get">
    <label for="fruit">Choose your fruit from the list:</label>
    <input list="fruits" name="fruit" id="fruit">
    <datalist id="fruits">
    <option value="Apple">
    <option value="Orange">
    <option value="Banana">
    <option value="Mango">
    <option value="Avacado">
    </datalist>
    <input type="submit">
    </form>

    看看它如何工作

    快速提示

    它与传统<select>-<option>标签有何不同?选择标签用于从您需要浏览列表的选项中选择一项或多项。Datalist是具有自动完成支持的高级功能。

    🧿 八、范围(滑块)

    range是给定滑块类型范围选择器的输入类型。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <form method="post">
    <input
    type="range"
    name="range"
    min="0"
    max="100"
    step="1"
    value=""
    onchange="changeValue(event)"/>
    </form>
    <div class="range">
    <output id="output" name="result"> </output>
    </div>

    看看它如何工作

    快速提示

    HTML5 中没有叫slider的!

    ⏰ 九、Meter

    使用<meter>标签测量给定范围内的数据。

    代码

    1
    2
    3
    4
    5
    <label for="home">/home/atapas</label>
    <meter id="home" value="4" min="0" max="10">2 out of 10</meter><br>

    <label for="root">/root</label>
    <meter id="root" value="0.6">60%</meter><br>

    看看它如何工作

    快速提示

    不要将<meter>标签用于进度指示器类型的用户体验。我们有来自 HTML5的<Progress>标签。

    1
    2
    <label for="file">Downloading progress:</label>
    <progress id="file" value="32" max="100"> 32% </progress>

    💌 十、Inputs

    这部分是我们最熟悉的输入类型的用法,如文本、密码等。输入类型的特殊用法很少

    代码

    必需的 将输入字段标记为必填字段。

    1
    <input type="text" id="username1" name="username" required>

    自动对焦 通过将光标放在输入元素上自动提供焦点。

    1
    <input type="text" id="username2" name="username" required autofocus>

    使用正则表达式验证 您可以使用正则表达式指定模式来验证输入。

    1
    2
    3
    4
    5
    <input type="password" 
    name="password"
    id="password"
    placeholder="6-20 chars, at least 1 digit, 1 uppercase and one lowercase letter"
    pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$" autofocus required>

    颜色选择器 一个简单的颜色选择器。

    1
    2
    <input type="color" onchange="showColor(event)">
    <p id="colorMe">Color Me!</p>

  • js原型,原型链知多少

    构造函数创建对象

    1
    2
    3
    4
    5
    6
    function Person() {

    }
    let person = new Person();
    person.name = 'jimmy';
    console.log(person.name) // jimmy

    在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象。person是实例对象

    prototype

    每个 函数 都有一个 prototype 属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Person() {

    }
    // **prototype 是函数才会有的属性**
    Person.prototype.name = 'Kevin';
    var person1 = new Person();
    var person2 = new Person();
    console.log(person1.name) // Kevin
    console.log(person2.name) // Kevin

    原型定义:
    每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。

    proto

    每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型

    1
    2
    3
    4
    5
    function Person() {

    }
    var person = new Person();
    console.log(person.__proto__ === Person.prototype); // true

    constructor

    每个原型都有一个constructor 属性指向关联的构造函数。

    1
    2
    3
    4
    function Person() {

    }
    console.log(Person === Person.prototype.constructor); // true

    以上就是 构造函数,原型和实例对象之间的关系。

    综上我们已经可以得出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person() {

    }

    var person = new Person();

    console.log(person.__proto__ === Person.prototype) // true
    console.log(Person.prototype.constructor === Person) // true
    // 顺便学习一个ES5的方法,可以获得对象的原型
    console.log(Object.getPrototypeOf(person) === Person.prototype) // true

    实例对象与原型

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person() {

    }

    Person.prototype.name = 'jimmy';
    let person = new Person();
    person.name = 'chimmy';
    console.log(person.name) // chimmy
    delete person.name;
    console.log(person.name) // jimmy

    在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 chimmy。但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 jimmy。
    但是万一还没有找到呢?原型的原型又是什么呢?

    原型的原型

    原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它。

    1
    2
    3
    let obj = new Object();
    obj.name = 'jimmy'
    console.log(obj.name) // jimmy

    其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 proto 指向构造函数的 prototype ,所以我们更新下关系图

    原型链

    那 Object.prototype 的原型呢?是null,我们可以打印

    1
    console.log(Object.prototype.__proto__ === null) // true

    所以查找属性的时候查到 Object.prototype 就可以停止查找了。null没有原型。
    下图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。

    补充

    constructor

    1
    2
    3
    4
    5
    function Person() {

    }
    var person = new Person();
    console.log(person.constructor === Person); // true

    当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:person.constructor === Person.prototype.constructor

    真的是继承吗?

    每一个对象都会从原型继承属性,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是: 继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

  • LeetCode(day01)

    1. 两数之和

    给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

    你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

    你可以按任意顺序返回答案。

    示例 1:

    1
    2
    3
    输入:nums = [2,7,11,15], target = 9
    输出:[0,1]
    解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

    示例 2:

    1
    2
    输入:nums = [3,2,4], target = 6
    输出:[1,2]

    示例 3:

    1
    2
    输入:nums = [3,3], target = 6
    输出:[0,1]

    提示:

    • 2 <= nums.length <= 104
    • -109 <= nums[i] <= 109
    • -109 <= target <= 109
    • 只会存在一个有效答案

    思路

    使用冒泡法逐一与前一个相加,如果两数相加之和等于target则返回,反之继续

    个人解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * @param {number[]} nums
    * @param {number} target
    * @return {number[]}
    */
    var twoSum = function(nums, target) {
    for (let i = 0; i < nums.length; i ++) {
    for (let k = i + 1; k <= nums.length; k ++) {
    if (nums[i] + nums[k] == target) {
    return [i, k]
    }
    }
    }
    };

    2. 整数反转

    给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。

    如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。

    假设环境不允许存储 64 位整数(有符号或无符号)。

    示例 1:

    1
    2
    输入:x = 123
    输出:321

    示例 2:

    1
    2
    输入:x = -123
    输出:-321

    示例 3:

    1
    2
    输入:x = 120
    输出:21

    示例 4:

    1
    2
    输入:x = 0
    输出:0

    提示:

    • -231 <= x <= 231 - 1

    思路

    将参数转化为String类型,然后判断该参数是否大于0,如果大于0,则转化为数组再翻转转化为String类型,如果小于0,则现将负号剔除,进行前面操作

    个人解答

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * @param {number} x
    * @return {number}
    */
    var reverse = function(x) {
    x = String(x);
    let target = [];
    if (Number(x) < 0) {
    target[0] = x.split("")[0];
    target.push(x.split("").splice(1).reverse().join(""));
    target = target.join('')
    } else {
    target = x.split("").reverse().join("");
    }
    if (Number(target) <= 2147483648 && Number(target) >= -2147483648) {
    return target;
    } else {
    return 0;
    }
    };

    3. 回文数

    给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false

    回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。

    示例 1:

    1
    2
    输入:x = 121
    输出:true

    示例 2:

    1
    2
    3
    输入:x = -121
    输出:false
    解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。

    示例 3:

    1
    2
    3
    输入:x = 10
    输出:false
    解释:从右向左读, 为 01 。因此它不是一个回文数。

    示例 4:

    1
    2
    输入:x = -101
    输出:false

    提示:

    • -231 <= x <= 231 - 1

    思路

    如果是正数,则将数据化为数组、翻转,如果相等则true,反之false,如果为负数,则先将负号剔除,进行前面操作

    个人解答

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * @param {number} x
    * @return {boolean}
    */
    var isPalindrome = function (x) {
    x1 = String(x);
    x2 = String(x).split('').reverse().join("")
    return x1 == x2
    };

    4. 最长公共前缀

    编写一个函数来查找字符串数组中的最长公共前缀。

    如果不存在公共前缀,返回空字符串 ""

    示例 1:

    1
    2
    输入:strs = ["flower","flow","flight"]
    输出:"fl"

    示例 2:

    1
    2
    3
    输入:strs = ["dog","racecar","car"]
    输出:""
    解释:输入不存在公共前缀。

    提示:

    • 0 <= strs.length <= 200
    • 0 <= strs[i].length <= 200
    • strs[i] 仅由小写英文字母组成

    思路

    使用冒泡法,将除数组第一个元素与数组第一各元素的各个子元素进行对比,如果相等则将子元素记录下来,反之则返回记录的数据

    个人解答

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * @param {string[]} strs
    * @return {string}
    */
    var longestCommonPrefix = function (strs) {
    var re = ''
    if (!strs.length || strs.includes('')) return re;
    if (strs.length == 1) return strs[0]
    for (let i = 0; i < strs[0].length; i++) {
    for (let j = 0; j < strs.length; j++) {
    if (strs[j][i] != strs[0][i]) return re;
    }
    re += strs[0][i]
    }
    return re
    };

    5. 有效括号

    给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

    有效字符串需满足:

    1. 左括号必须用相同类型的右括号闭合。
    2. 左括号必须以正确的顺序闭合。

    示例 1:

    1
    2
    输入:s = "()"
    输出:true

    示例 2:

    1
    2
    输入:s = "()[]{}"
    输出:true

    示例 3:

    1
    2
    输入:s = "(]"
    输出:false

    示例 4:

    1
    2
    输入:s = "([)]"
    输出:false

    示例 5:

    1
    2
    输入:s = "{[]}"
    输出:true

    提示:

    • 1 <= s.length <= 104
    • s 仅由括号 '()[]{}' 组成

    思路

    首先建立一个Map对象,将括号放入其中,如果Map对象中不含有该string对象中的括号,则将该元素放入栈stk中,反之则判断Map类型中对应value是否为栈中最后一个元素,如果是,则从栈stk中取出,如果不是则return false,最后如果栈stk的长度大于0,则代表括号没有一一对应,返回false,反之则返回true

    个人解答

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * @param {string} s
    * @return {boolean}
    */
    var isValid = function(s) {
    if (s.length == 1) {
    return false;
    }

    let mapList = new Map([
    [')', '('],
    ['}', '{'],
    [']', '[']
    ])

    let stk = []

    for (let ch of s) {
    if (mapList.has(ch)) {
    if (!stk.length || stk[stk.length - 1] !== mapList.get(ch)) {
    return false;
    }
    stk.pop()
    } else {
    stk.push(ch)
    }
    }

    return !stk.length
    };
  • 分享一些前端常用功能集合

    描述

    1.以下功能主要是以移动端为主

    2.使用到的ES6在移动端中没有不兼容情况,这里我基本应用在微信端,手机浏览器的话也不用担心

    3.所有功能均由原生JavaScript实现,没有任何依赖,我一贯的做法是用最少的代码,造最高效的事情

    我在做一些H5单页(活动页)的时候,像我这种最求极致加载速度,且不喜欢用第三方库的人,所以决定自己动手做一些无依赖精简高效的东西,然后按需应用在实际项目中,同时为了比百度上搜到更好用的代码分享给大家。

    这里推荐前端使用vs code这个代码编辑器,理由是在声明的时候写好标准的JSDoc注释,在调用时会有很全面的代码提示,让弱类型的javascript也有类型提示

    1. http请求

    前端必备技能,也是使用最多的功能。我个人不喜欢用axios这个东西(懒得去看文档,而且觉得很鸡肋),几乎所有的web项目都是用的这个轮子。

    第一种:fetch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /**
    * 基于`fetch`请求 [MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)
    * @param {"GET"|"POST"|"PUT"|"DELETE"} method 请求方法
    * @param {string} url 请求路径
    * @param {object} data 请求参数对象
    * @param {number} timeout 超时毫秒
    */
    function fetchRequest(method, url, data = {}, timeout = 5000) {
    let body = null;
    let query = "";
    if (method === "GET") {
    // 解析对象传参
    for (const key in data) {
    query += `&${key}=${data[key]}`;
    }
    if (query) {
    query = "?" + query.slice(1);
    }
    } else {
    // 若后台没设置接收 JSON 则不行 需要跟 GET 一样的解析对象传参
    body = JSON.stringify(data);
    }
    return new Promise((resolve, reject) => {
    fetch(url + query, {
    // credentials: "include", // 携带cookie配合后台用
    // mode: "cors", // 貌似也是配合后台设置用的跨域模式
    method: method,
    headers: {
    // "Content-Type": "application/json"
    "Content-Type": "application/x-www-form-urlencoded"
    },
    body: body
    }).then(response => {
    // 把响应的信息转为`json`
    return response.json();
    }).then(res => {
    resolve(res);
    }).catch(error => {
    reject(error);
    });
    setTimeout(reject.bind(this, "fetch is timeout"), timeout);
    });
    }

    特别说明一下:我在H5单页的一些简单GET请求时通常用得最多,因为代码极少,就像下面这样

    1
    2
    3
    fetch("http://xxx.com/api/get").then(response => response.text()).then(res => {
    console.log("请求成功", res);
    })

    第二种:XMLHttpRequest,需要Promise用法在外面包多一层function做二次封装即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    /**
    * `XMLHttpRequest`请求 [MDN文档](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)
    * @param {object} params 传参对象
    * @param {string} params.url 请求路径
    * @param {"GET"|"POST"|"PUT"|"DELETE"} params.method 请求方法
    * @param {object} params.data 传参对象(json)
    * @param {FormData|string} params.formData `form`表单式传参:上传图片就是使用这种传参方式;使用`formData`时将覆盖`data`
    * @param {{ [key: string]: string }} params.headers `XMLHttpRequest.header`设置对象
    * @param {number?} params.overtime 超时检测毫秒数
    * @param {(result?: any, response: XMLHttpRequest) => void} params.success 成功回调
    * @param {(error?: XMLHttpRequest) => void} params.fail 失败回调
    * @param {(info?: XMLHttpRequest) => void} params.timeout 超时回调
    * @param {(res?: ProgressEvent<XMLHttpRequestEventTarget>) => void} params.progress 进度回调(暂时没用到)
    */
    function ajax(params) {
    if (typeof params !== "object") return console.error("ajax 缺少请求传参");
    if (!params.method) return console.error("ajax 缺少请求类型 GET 或者 POST");
    if (!params.url) return console.error("ajax 缺少请求 url");
    if (typeof params.data !== "object") return console.error("请求参数类型必须为 object");

    const XHR = new XMLHttpRequest();
    /** 请求方法 */
    const method = params.method;
    /** 超时检测 */
    const overtime = typeof params.overtime === "number" ? params.overtime : 0;
    /** 请求链接 */
    let url = params.url;
    /** 非`GET`请求传参 */
    let body = null;
    /** `GET`请求传参 */
    let query = "";

    // 传参处理
    if (method === "GET") {
    // 解析对象传参
    for (const key in params.data) {
    query += "&" + key + "=" + params.data[key];
    }
    if (query) {
    query = "?" + query.slice(1);
    url += query;
    }
    } else {
    body = JSON.stringify(params.data); // 若后台没设置接收 JSON 则不行,需要使用`params.formData`方式传参
    }

    // 监听请求变化;XHR.status learn: http://tool.oschina.net/commons?type=5
    XHR.onreadystatechange = function () {
    if (XHR.readyState !== 4) return;
    if (XHR.status === 200 || XHR.status === 304) {
    typeof params.success === "function" && params.success(JSON.parse(XHR.response), XHR);
    } else {
    typeof params.fail === "function" && params.fail(XHR);
    }
    }

    // 判断请求进度
    if (params.progress) {
    XHR.addEventListener("progress", params.progress);
    }

    // XHR.responseType = "json"; // 设置响应结果为`json`这个一般由后台返回指定格式,前端无配置
    // XHR.withCredentials = true; // 是否Access-Control应使用cookie或授权标头等凭据进行跨站点请求。
    XHR.open(method, url, true);

    // 判断传参类型,`json`或者`form`表单
    if (params.formData) {
    body = params.formData;
    XHR.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // 默认就是这个,设置不设置都可以
    } else {
    XHR.setRequestHeader("Content-Type", "application/json");
    }

    // 判断设置配置头信息
    if (params.headers) {
    for (const key in params.headers) {
    const value = params.headers[key];
    XHR.setRequestHeader(key, value);
    }
    }

    // 在IE中,超时属性只能在调用 open() 方法之后且在调用 send() 方法之前设置。
    if (overtime > 0) {
    XHR.timeout = overtime;
    XHR.ontimeout = function () {
    console.warn("XMLHttpRequest 请求超时 !!!");
    XHR.abort();
    typeof params.timeout === "function" && params.timeout(XHR);
    }
    }

    XHR.send(body);
    }

    2. swiper轮播图组件

    这是我写的第一个web功能组件,拖拽回弹物理效果是参照开源项目Swiper.js做的,效果功能保持一致,代码实现均由自己完成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    /**
    * 轮播组件
    * @param {object} params 配置传参
    * @param {string} params.el 组件节点 class|id|<label>
    * @param {number} params.moveTime 过渡时间(毫秒)默认 300
    * @param {number} params.interval 自动播放间隔(毫秒)默认 3000
    * @param {boolean} params.loop 是否需要回路
    * @param {boolean} params.vertical 是否垂直滚动
    * @param {boolean} params.autoPaly 是否需要自动播放
    * @param {boolean} params.pagination 是否需要底部圆点
    * @param {(index: number) => void} params.slideCallback 滑动/切换结束回调
    * @author https://github.com/Hansen-hjs
    * @description
    * 移动端`swiper`组件,如果需要兼容`pc`自行修改对应的`touch`到`mouse`事件即可。现成效果预览:https://huangjingsheng.gitee.io/hjs/cv/demo/face/
    */
    function swiper(params) {
    /**
    * css class 命名列表
    * @dec ["滑动列表","滑动item","圆点容器","底部圆点","圆点高亮"]
    */
    const classNames = [".swiper_list", ".swiper_item", ".swiper_pagination", ".swiper_dot", ".swiper_dot_active"];
    /** 滑动结束函数 */
    const slideEnd = params.slideCallback || function() {};
    /**
    * 组件节点
    * @type {HTMLElement}
    */
    let node = null;
    /**
    * item列表容器
    * @type {HTMLElement}
    */
    let nodeItem = null;
    /**
    * item节点列表
    * @type {Array<HTMLElement>}
    */
    let nodeItems = [];
    /**
    * 圆点容器
    * @type {HTMLElement}
    */
    let nodePagination = null;
    /**
    * 圆点节点列表
    * @type {Array<HTMLElement>}
    */
    let nodePaginationItems = [];
    /** 是否需要底部圆点 */
    let pagination = false;
    /** 是否需要回路 */
    let isLoop = false;
    /** 方向 `X => true` | `Y => false` */
    let direction = false;
    /** 是否需要自动播放 */
    let autoPaly = false;
    /** 自动播放间隔(毫秒)默认 3000 */
    let interval = 3000;
    /** 过渡时间(毫秒)默认 300 */
    let moveTime = 300;

    /** 设置动画 */
    function startAnimation() {
    nodeItem.style.transition = `${moveTime / 1000}s all`;
    }

    /** 关闭动画 */
    function stopAnimation() {
    nodeItem.style.transition = "0s all";
    }

    /**
    * 属性样式滑动
    * @param {number} n 移动的距离
    */
    function slideStyle(n) {
    let x = 0, y = 0;
    if (direction) {
    y = n;
    } else {
    x = n;
    }
    nodeItem.style.transform = `translate3d(${x}px, ${y}px, 0px)`;
    }

    /**
    * 事件开始
    * @param {number} width 滚动容器的宽度
    * @param {number} height 滚动容器的高度
    */
    function main(width, height) {
    /**
    * 动画帧
    * @type {requestAnimationFrame}
    */
    const animation = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
    /** 触摸开始时间 */
    let startTime = 0;
    /** 触摸结束时间 */
    let endTime = 0;
    /** 开始的距离 */
    let startDistance = 0;
    /** 结束的距离 */
    let endDistance = 0;
    /** 结束距离状态 */
    let endState = 0;
    /** 移动的距离 */
    let moveDistance = 0;
    /** 圆点位置 && 当前 item 索引 */
    let index = 0;
    /** 动画帧计数 */
    let count = 0;
    /** loop 帧计数 */
    let loopCount = 0;
    /** 移动范围 */
    let range = direction ? height : width;

    /** 获取拖动距离 */
    function getDragDistance() {
    /** 拖动距离 */
    let dragDistance = 0;
    // 默认这个公式
    dragDistance = moveDistance + (endDistance - startDistance);
    // 判断最大正负值
    if ((endDistance - startDistance) >= range) {
    dragDistance = moveDistance + range;
    } else if ((endDistance - startDistance) <= -range) {
    dragDistance = moveDistance - range;
    }
    // 没有 loop 的时候惯性拖拽
    if (!isLoop) {
    if ((endDistance - startDistance) > 0 && index === 0) {
    // console.log("到达最初");
    dragDistance = moveDistance + ((endDistance - startDistance) - ((endDistance - startDistance) * 0.6));
    } else if ((endDistance - startDistance) < 0 && index === nodeItems.length - 1) {
    // console.log("到达最后");
    dragDistance = moveDistance + ((endDistance - startDistance) - ((endDistance - startDistance) * 0.6));
    }
    }
    return dragDistance;
    }

    /**
    * 判断触摸处理函数
    * @param {number} slideDistance 滑动的距离
    */
    function judgeTouch(slideDistance) {
    // 这里我设置了200毫秒的有效拖拽间隔
    if ((endTime - startTime) < 200) return true;
    // 这里判断方向(正值和负值)
    if (slideDistance < 0) {
    if ((endDistance - startDistance) < (slideDistance / 2)) return true;
    return false;
    } else {
    if ((endDistance - startDistance) > (slideDistance / 2)) return true;
    return false;
    }
    }

    /** 返回原来位置 */
    function backLocation() {
    startAnimation();
    slideStyle(moveDistance);
    }

    /**
    * 滑动
    * @param {number} slideDistance 滑动的距离
    */
    function slideMove(slideDistance) {
    startAnimation();
    slideStyle(slideDistance);
    loopCount = 0;
    // 判断 loop 时回到第一张或最后一张
    if (isLoop && index < 0) {
    // 我这里是想让滑块过渡完之后再重置位置所以加的延迟 (之前用setTimeout,快速滑动有问题,然后换成 requestAnimationFrame解决了这类问题)
    function loopMoveMin() {
    loopCount += 1;
    if (loopCount < moveTime / 1000 * 60) return animation(loopMoveMin);
    stopAnimation();
    slideStyle(range * -(nodeItems.length - 3));
    // 重置一下位置
    moveDistance = range * -(nodeItems.length - 3);
    }
    loopMoveMin();
    index = nodeItems.length - 3;
    } else if (isLoop && index > nodeItems.length - 3) {
    function loopMoveMax() {
    loopCount += 1;
    if (loopCount < moveTime / 1000 * 60) return animation(loopMoveMax);
    stopAnimation();
    slideStyle(0);
    moveDistance = 0;
    }
    loopMoveMax();
    index = 0;
    }
    // console.log(`第${ index+1 }张`); // 这里可以做滑动结束回调
    if (pagination) {
    nodePagination.querySelector(classNames[4]).className = classNames[3].slice(1);
    nodePaginationItems[index].classList.add(classNames[4].slice(1));
    }
    }

    /** 判断移动 */
    function judgeMove() {
    // 判断是否需要执行过渡
    if (endDistance < startDistance) {
    // 往上滑动 or 向左滑动
    if (judgeTouch(-range)) {
    // 判断有loop的时候不需要执行下面的事件
    if (!isLoop && moveDistance === (-(nodeItems.length - 1) * range)) return backLocation();
    index += 1;
    slideMove(moveDistance - range);
    moveDistance -= range;
    slideEnd(index);
    } else {
    backLocation();
    }
    } else {
    // 往下滑动 or 向右滑动
    if (judgeTouch(range)) {
    if (!isLoop && moveDistance === 0) return backLocation();
    index -= 1;
    slideMove(moveDistance + range);
    moveDistance += range;
    slideEnd(index)
    } else {
    backLocation();
    }
    }
    }

    /** 自动播放移动 */
    function autoMove() {
    // 这里判断 loop 的自动播放
    if (isLoop) {
    index += 1;
    slideMove(moveDistance - range);
    moveDistance -= range;
    } else {
    if (index >= nodeItems.length - 1) {
    index = 0;
    slideMove(0);
    moveDistance = 0;
    } else {
    index += 1;
    slideMove(moveDistance - range);
    moveDistance -= range;
    }
    }
    slideEnd(index);
    }

    /** 开始自动播放 */
    function startAuto() {
    count += 1;
    if (count < interval / 1000 * 60) return animation(startAuto);
    count = 0;
    autoMove();
    startAuto();
    }

    // 判断是否需要开启自动播放
    if (autoPaly && nodeItems.length > 1) startAuto();

    // 开始触摸
    nodeItem.addEventListener("touchstart", ev => {
    startTime = Date.now();
    count = 0;
    loopCount = moveTime / 1000 * 60;
    stopAnimation();
    startDistance = direction ? ev.touches[0].clientY : ev.touches[0].clientX;
    });

    // 触摸移动
    nodeItem.addEventListener("touchmove", ev => {
    ev.preventDefault();
    count = 0;
    endDistance = direction ? ev.touches[0].clientY : ev.touches[0].clientX;
    slideStyle(getDragDistance());
    });

    // 触摸离开
    nodeItem.addEventListener("touchend", () => {
    endTime = Date.now();
    // 判断是否点击
    if (endState !== endDistance) {
    judgeMove();
    } else {
    backLocation();
    }
    // 更新位置
    endState = endDistance;
    // 重新打开自动播
    count = 0;
    });
    }

    /**
    * 输出回路:如果要回路的话前后增加元素
    * @param {number} width 滚动容器的宽度
    * @param {number} height 滚动容器的高度
    */
    function outputLoop(width, height) {
    const first = nodeItems[0].cloneNode(true), last = nodeItems[nodeItems.length - 1].cloneNode(true);
    nodeItem.insertBefore(last, nodeItems[0]);
    nodeItem.appendChild(first);
    nodeItems.unshift(last);
    nodeItems.push(first);
    if (direction) {
    nodeItem.style.top = `${-height}px`;
    } else {
    nodeItem.style.left = `${-width}px`;
    }
    }

    /**
    * 输出动态布局
    * @param {number} width 滚动容器的宽度
    * @param {number} height 滚动容器的高度
    */
    function outputLayout(width, height) {
    if (direction) {
    for (let i = 0; i < nodeItems.length; i++) {
    nodeItems[i].style.height = `${height}px`;
    }
    } else {
    nodeItem.style.width = `${width * nodeItems.length}px`;
    for (let i = 0; i < nodeItems.length; i++) {
    nodeItems[i].style.width = `${width}px`;
    }
    }
    }

    /** 输出底部圆点 */
    function outputPagination() {
    let paginations = "";
    nodePagination = node.querySelector(classNames[2]);
    // 如果没有找到对应节点则创建一个
    if (!nodePagination) {
    nodePagination = document.createElement("div");
    nodePagination.className = classNames[2].slice(1);
    node.appendChild(nodePagination);
    }
    for (let i = 0; i < nodeItems.length; i++) {
    paginations += `<div class="${classNames[3].slice(1)}"></div>`;
    }
    nodePagination.innerHTML = paginations;
    nodePaginationItems = [...nodePagination.querySelectorAll(classNames[3])];
    nodePagination.querySelector(classNames[3]).classList.add(classNames[4].slice(1));
    }

    /** 初始化动态布局 */
    function initLayout() {
    node = document.querySelector(params.el);
    if (!node) return console.warn("没有可执行的节点!");
    nodeItem = node.querySelector(classNames[0]);
    if (!nodeItem) return console.warn(`缺少"${classNames[0]}"节点!`);
    nodeItems = [...node.querySelectorAll(classNames[1])];
    if (nodeItems.length == 0) return console.warn("滑动节点个数必须大于0!");
    const moveWidth = node.offsetWidth, moveHeight = node.offsetHeight;
    if (pagination) outputPagination();
    if (isLoop) outputLoop(moveWidth, moveHeight);
    outputLayout(moveWidth, moveHeight);
    main(moveWidth, moveHeight);
    }

    /** 初始化参数 */
    function initParams() {
    if (typeof params !== "object") return console.warn("传参有误");
    pagination = params.pagination || false;
    direction = params.vertical || false;
    autoPaly = params.autoPaly || false;
    isLoop = params.loop || false;
    moveTime = params.moveTime || 300;
    interval = params.interval || 3000;
    initLayout();
    }
    initParams();
    }

    3. 图片懒加载

    非传统实现方式,性能最优

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    /**
    * 懒加载
    * @description 可加载`<img>`、`<video>`、`<audio>`等一些引用资源路径的标签
    * @param {object} params 传参对象
    * @param {string?} params.lazyAttr 自定义加载的属性(可选)
    * @param {"src"|"background"} params.loadType 加载的类型(默认为`src`)
    * @param {string?} params.errorPath 加载失败时显示的资源路径,仅在`loadType`设置为`src`中可用(可选)
    */
    function lazyLoad(params) {
    const attr = params.lazyAttr || "lazy";
    const type = params.loadType || "src";

    /** 更新整个文档的懒加载节点 */
    function update() {
    const els = document.querySelectorAll(`[${attr}]`);
    for (let i = 0; i < els.length; i++) {
    const el = els[i];
    observer.observe(el);
    }
    }

    /**
    * 加载图片
    * @param {HTMLImageElement} el 图片节点
    */
    function loadImage(el) {
    const cache = el.src; // 缓存当前`src`加载失败时候用
    el.src = el.getAttribute(attr);
    el.onerror = function () {
    el.src = params.errorPath || cache;
    }
    }

    /**
    * 加载单个节点
    * @param {HTMLElement} el
    */
    function loadElement(el) {
    switch (type) {
    case "src":
    loadImage(el);
    break;
    case "background":
    el.style.backgroundImage = `url(${el.getAttribute(attr)})`;
    break;
    }
    el.removeAttribute(attr);
    observer.unobserve(el);
    }

    /**
    * 监听器
    * [MDN说明](https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver)
    */
    const observer = new IntersectionObserver(function(entries) {
    for (let i = 0; i < entries.length; i++) {
    const item = entries[i];
    if (item.isIntersecting) {
    loadElement(item.target);
    }
    }
    })

    update();

    return {
    observer,
    update
    }
    }

    vue中使用指令去使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import Vue from "vue";

    /** 添加一个加载`src`的指令 */
    const lazySrc = lazyLoad({
    lazyAttr: "vlazy",
    errorPath: "./img/error.jpg"
    })

    Vue.directive("v-lazy", {
    inserted(el, binding) {
    el.setAttribute("vlazy", binding.value); // 跟上面的对应
    lazySrc.observer.observe(el);
    }
    })

    /** 添加一个加载`background`的指令 */
    const lazyBg = lazyLoad({
    lazyAttr: "vlazybg",
    loadType: "background"
    })

    Vue.directive("v-lazybg", {
    inserted(el, binding) {
    el.setAttribute("vlazybg", binding.value); // 跟上面的对应
    lazyBg.observer.observe(el);
    }
    })

    4. 上传图片

    这个超简单,没啥好说的

    1
    2
    <!-- 先准备好一个input标签,然后设置type="file",最后挂载一个onchange事件 -->
    <input class="upload-input" type="file" name="picture" onchange="upLoadImage(this)">
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    /**
    * input上传图片
    * @param {HTMLInputElement} el
    */
    function upLoadImage(el) {
    /** 上传文件 */
    const file = el.files[0];
    /** 上传类型数组 */
    const types = ["image/jpg", "image/png", "image/jpeg", "image/gif"];
    // 判断文件类型
    if (types.indexOf(file.type) < 0) {
    file.value = null; // 这里一定要清空当前错误的内容
    return alert("文件格式只支持:jpg 和 png");
    }
    // 判断大小
    if (file.size > 2 * 1024 * 1024) {
    file.value = null;
    return alert("上传的文件不能大于2M");
    }

    const formData = new FormData(); // 这个是传给后台的数据
    formData.append("img", file); // 这里`img`是跟后台约定好的`key`字段
    console.log(formData, file);
    // 最后POST给后台,这里我用上面的方法
    ajax({
    url: "http://xxx.com/uploadImg",
    method: "POST",
    data: {},
    formData: formData,
    overtime: 5000,
    success(res) {
    console.log("上传成功", res);
    },
    fail(err) {
    console.log("上传失败", err);
    },
    timeout() {
    console.warn("XMLHttpRequest 请求超时 !!!");
    }
    });
    }

    5. 下拉刷新组件

    拖拽效果参考上面swiper的实现方式,下拉中的效果是可以自己定义的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    // 这里我做的不是用 window 的滚动事件,而是用最外层的绑定触摸下拉事件去实现
    // 好处是我用在Vue这类单页应用的时候,组件销毁时不用去解绑 window 的 scroll 事件
    // 但是滑动到底部事件就必须要用 window 的 scroll 事件,这点需要注意

    /**
    * 下拉刷新组件
    * @param {object} option 配置
    * @param {HTMLElement} option.el 下拉元素(必选)
    * @param {number} option.distance 下拉距离[px](可选)
    * @param {number} option.deviation 顶部往下偏移量[px](可选)
    * @param {string} option.loadIcon 下拉中的 icon html(可选)
    */
    function dropDownRefresh(option) {
    const doc = document;
    /** 整体节点 */
    const page = option.el;
    /** 下拉距离 */
    const distance = option.distance || 88;
    /** 顶部往下偏移量 */
    const deviation = option.deviation || 0;
    /** 顶层节点 */
    const topNode = doc.createElement("div");
    /** 下拉时遮罩 */
    const maskNode = doc.createElement("div");

    topNode.innerHTML = `<div refresh-icon style="transition: .2s all;"><svg style="transform: rotate(90deg); display: block;" t="1570593064555" viewBox="0 0 1575 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="26089" width="48" height="48"><path d="M1013.76 0v339.968H484.115692V679.778462h529.644308v339.968l529.644308-485.612308v-48.600616L1013.76 0zM243.396923 679.857231h144.462769V339.968H243.396923V679.778462z m-240.797538 0h144.462769V339.968H2.599385V679.778462z" fill="#000000" fill-opacity=".203" p-id="26090"></path></svg></div><div refresh-loading style="display: none; animation: refresh-loading 1s linear infinite;">${option.loadIcon || '<p style="font-size: 15px; color: #666;">loading...</p>'}</div>`;
    topNode.style.cssText = `width: 100%; height: ${distance}px; position: fixed; top: ${-distance + deviation}px; left: 0; z-index: 10; display: flex; flex-wrap: wrap; align-items: center; justify-content: center; box-sizing: border-box; margin: 0; padding: 0;`;
    maskNode.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100vh; box-sizing: border-box; margin: 0; padding: 0; background-color: rgba(0,0,0,0); z-index: 999;";
    page.parentNode.insertBefore(topNode, page);

    /**
    * 设置动画时间
    * @param {number} n 秒数
    */
    function setAnimation(n) {
    page.style.transition = topNode.style.transition = n + "s all";
    }

    /**
    * 设置滑动距离
    * @param {number} n 滑动的距离(像素)
    */
    function setSlide(n) {
    page.style.transform = topNode.style.transform = `translate3d(0px, ${n}px, 0px)`;
    }

    /** 下拉提示 icon */
    const icon = topNode.querySelector("[refresh-icon]");
    /** 下拉 loading 动画 */
    const loading = topNode.querySelector("[refresh-loading]");

    return {
    /**
    * 监听开始刷新
    * @param {Function} callback 下拉结束回调
    * @param {(n: number) => void} rangeCallback 下拉状态回调
    */
    onRefresh(callback, rangeCallback = null) {
    /** 顶部距离 */
    let scrollTop = 0;
    /** 开始距离 */
    let startDistance = 0;
    /** 结束距离 */
    let endDistance = 0;
    /** 最后移动的距离 */
    let range = 0;

    // 触摸开始
    page.addEventListener("touchstart", function (e) {
    startDistance = e.touches[0].pageY;
    scrollTop = 1;
    setAnimation(0);
    });

    // 触摸移动
    page.addEventListener("touchmove", function (e) {
    scrollTop = doc.documentElement.scrollTop === 0 ? doc.body.scrollTop : doc.documentElement.scrollTop;
    // 没到达顶部就停止
    if (scrollTop != 0) return;
    endDistance = e.touches[0].pageY;
    range = Math.floor(endDistance - startDistance);
    // 判断如果是下滑才执行
    if (range > 0) {
    // 阻止浏览自带的下拉效果
    e.preventDefault();
    // 物理回弹公式计算距离
    range = range - (range * 0.5);
    // 下拉时icon旋转
    if (range > distance) {
    icon.style.transform = "rotate(180deg)";
    } else {
    icon.style.transform = "rotate(0deg)";
    }
    setSlide(range);
    // 回调距离函数 如果有需要
    if (typeof rangeCallback === "function") rangeCallback(range);
    }
    });

    // 触摸结束
    page.addEventListener("touchend", function () {
    setAnimation(0.3);
    // console.log(`移动的距离:${range}, 最大距离:${distance}`);
    if (range > distance && range > 1 && scrollTop === 0) {
    setSlide(distance);
    doc.body.appendChild(maskNode);
    // 阻止往上滑动
    maskNode.ontouchmove = e => e.preventDefault();
    // 回调成功下拉到最大距离并松开函数
    if (typeof callback === "function") callback();
    icon.style.display = "none";
    loading.style.display = "block";
    } else {
    setSlide(0);
    }
    });

    },
    /** 结束下拉 */
    end() {
    maskNode.parentNode.removeChild(maskNode);
    setAnimation(0.3);
    setSlide(0);
    icon.style.display = "block";
    loading.style.display = "none";
    }
    }
    }

    6. 监听滚动到底部

    就几行代码的一个方法,另外监听元素滚动到底部可以参考代码笔记

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /**
    * 监听滚动到底部
    * @param {object} options 传参对象
    * @param {number} options.distance 距离底部多少像素触发(px)
    * @param {boolean} options.once 是否为一次性(防止重复用)
    * @param {() => void} options.callback 到达底部回调函数
    */
    function onScrollToBottom(options) {
    const { distance = 0, once = false, callback = null } = options;
    const doc = document;
    /** 滚动事件 */
    function onScroll() {
    /** 滚动的高度 */
    let scrollTop = doc.documentElement.scrollTop === 0 ? doc.body.scrollTop : doc.documentElement.scrollTop;
    /** 滚动条高度 */
    let scrollHeight = doc.documentElement.scrollTop === 0 ? doc.body.scrollHeight : doc.documentElement.scrollHeight;
    if (scrollHeight - scrollTop - distance <= window.innerHeight) {
    if (typeof callback === "function") callback();
    if (once) window.removeEventListener("scroll", onScroll);
    }
    }
    window.addEventListener("scroll", onScroll);
    // 必要时先执行一次
    // onScroll();
    }

    7. 音频播放组件

    这里需要说明一下应用场景:我先前做H5活动页(红包雨)的时候遇到一个问题,就是在移动端快速点击节点并播放音频的时候,aduio标签播放的速度会有很严重的延迟。后来搜了下相关资料发现一个音频API:new AudioContext,和我之前做小游戏时用到的引擎(cocos creator)音频API是一样的。然后找了挺久发现这个API的使用资料、教程还是挺少的可能是除了做H5游戏引擎的人会用到吧,比较详细的也只有MDN官网,剩下的就是一些基于这个APIJavaScript库,但是我需要用到的功能比较简单,就是点击播放无延迟。所以自己去实现一个基于new AudioContext常用的音频组件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    /**
    * `AudioContext`音频组件
    * [资料参考](https://www.cnblogs.com/Wayou/p/html5_audio_api_visualizer.html)
    * @description 解决在移动端网页上标签播放音频延迟的方案 貌似`H5`游戏引擎也是使用这个实现
    */
    function audioComponent() {
    /**
    * 音频上下文
    * @type {AudioContext}
    */
    const context = new (window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext)();
    /**
    * @type {AnalyserNode}
    */
    const analyser = context.createAnalyser();;
    /**
    * @type {AudioBufferSourceNode}
    */
    let bufferNode = null;
    /**
    * @type {AudioBuffer}
    */
    let buffer = null;
    /** 是否加载完成 */
    let loaded = false;

    analyser.fftSize = 256;

    return {
    /**
    * 加载路径音频文件
    * @param {string} url 音频路径
    * @param {(res: AnalyserNode) => void} callback 加载完成回调
    */
    loadPath(url, callback) {
    const XHR = new XMLHttpRequest();
    XHR.open("GET", url, true);
    XHR.responseType = "arraybuffer";
    // 先加载音频文件
    XHR.onload = () => {
    context.decodeAudioData(XHR.response, audioBuffer => {
    // 最后缓存音频资源
    buffer = audioBuffer;
    loaded = true;
    typeof callback === "function" && callback(analyser);
    });
    }
    XHR.send(null);
    },

    /**
    * 加载 input 音频文件
    * @param {File} file 音频文件
    * @param {(res: AnalyserNode) => void} callback 加载完成回调
    */
    loadFile(file, callback) {
    const FR = new FileReader();
    // 先加载音频文件
    FR.onload = e => {
    const res = e.target.result;
    // 然后解码
    context.decodeAudioData(res, audioBuffer => {
    // 最后缓存音频资源
    buffer = audioBuffer;
    loaded = true;
    typeof callback === "function" && callback(analyser);
    });
    }
    FR.readAsArrayBuffer(file);
    },

    /** 播放音频 */
    play() {
    if (!loaded) return console.warn("音频未加载完成 !!!");
    // 这里有个问题,就是创建的音频对象不能缓存下来然后多次执行 start , 所以每次都要创建然后 start()
    bufferNode = context.createBufferSource();
    bufferNode.connect(analyser);
    analyser.connect(context.destination);
    bufferNode.buffer = buffer;
    bufferNode.start(0);
    },

    /** 停止播放 */
    stop() {
    if (!bufferNode) return console.warn("音频未播放 !!!");
    bufferNode.stop();
    }
    }
    }

    8. 全局监听图片错误并替换到默认图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    window.addEventListener("error", e => {
    /** 默认`base64`图片 */
    const defaultImg = 'data:image/png;base64,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';
    /**
    * @type {HTMLImageElement}
    */
    const node = e.target;
    if (node.nodeName && node.nodeName.toLocaleLowerCase() === "img") {
    node.style.objectFit = "cover";
    node.src = defaultImg;
    }
    }, true);

    9. 复制功能

    我在翻 Clipboard.js 这个插件库源码的时候找到核心代码 setSelectionRange(start: number, end: number),百度上搜到的复制功能全部都少了这个操作,所以搜到的复制文本代码在 iosIE 等一些浏览器上复制不了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    /**
    * 复制文本
    * @param {string} text 复制的内容
    * @param {() => void} success 成功回调
    * @param {(tip: string) => void} fail 出错回调
    */
    function copyText(text, success = null, fail = null) {
    text = text.replace(/(^\s*)|(\s*$)/g, "");
    if (!text) {
    typeof fail === "function" && fail("复制的内容不能为空!");
    return;
    }
    const id = "the-clipboard";
    /**
    * 粘贴板节点
    * @type {HTMLTextAreaElement}
    */
    let clipboard = document.getElementById(id);
    if (!clipboard) {
    clipboard = document.createElement("textarea");
    clipboard.id = id;
    clipboard.readOnly = true
    clipboard.style.cssText = "font-size: 15px; position: fixed; top: -1000%; left: -1000%;";
    document.body.appendChild(clipboard);
    }
    clipboard.value = text;
    clipboard.select();
    clipboard.setSelectionRange(0, text.length);
    const state = document.execCommand("copy");
    if (state) {
    typeof success === "function" && success();
    } else {
    typeof fail === "function" && fail("复制失败");
    }
    }

    10. 检测类型

    可检测所有类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 检测类型
    * @param {any} target 检测的目标
    * @returns {"string"|"number"|"array"|"object"|"function"|"null"|"undefined"} 只枚举一些常用的类型
    */
    function checkType(target) {
    /** @type {string} */
    const value = Object.prototype.toString.call(target);
    const result = value.match(/\[object (\S*)\]/)[1];
    return result.toLocaleLowerCase();
    }

    11. 生成时间戳(代码极少版)

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 获取指定日期时间戳
    * @param {number} time 毫秒数
    */
    function getDateFormat(time = Date.now()) {
    const date = new Date(time);
    return `${date.toLocaleDateString()} ${date.toTimeString().slice(0, 8)}`;
    }

    12. JavaScript小数精度计算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    /**
    * 数字运算(主要用于小数点精度问题)
    * @param {number} a 前面的值
    * @param {"+"|"-"|"*"|"/"} type 计算方式
    * @param {number} b 后面的值
    * @example
    * ```js
    * // 可链式调用
    * const res = computeNumber(1.3, "-", 1.2).next("+", 1.5).next("*", 2.3).next("/", 0.2).result;
    * console.log(res);
    * ```
    */
    function computeNumber(a, type, b) {
    /**
    * 获取数字小数点的长度
    * @param {number} n 数字
    */
    function getDecimalLength(n) {
    const decimal = n.toString().split(".")[1];
    return decimal ? decimal.length : 0;
    }
    /**
    * 修正小数点
    * @description 防止出现 `33.33333*100000 = 3333332.9999999995` && `33.33*10 = 333.29999999999995` 这类情况做的处理
    * @param {number} n
    */
    const amend = (n, precision = 15) => parseFloat(Number(n).toPrecision(precision));
    const power = Math.pow(10, Math.max(getDecimalLength(a), getDecimalLength(b)));
    let result = 0;

    a = amend(a * power);
    b = amend(b * power);

    switch (type) {
    case "+":
    result = (a + b) / power;
    break;
    case "-":
    result = (a - b) / power;
    break;
    case "*":
    result = (a * b) / (power * power);
    break;
    case "/":
    result = a / b;
    break;
    }

    result = amend(result);

    return {
    /** 计算结果 */
    result,
    /**
    * 继续计算
    * @param {"+"|"-"|"*"|"/"} nextType 继续计算方式
    * @param {number} nextValue 继续计算的值
    */
    next(nextType, nextValue) {
    return computeNumber(result, nextType, nextValue);
    }
    };
    }

    13. 一行css适配rem

    750是设计稿的宽度:之后的单位直接1:1使用设计稿的大小,单位是rem

    1
    html{ font-size: calc(100vw / 750); }

    14. 好用的格式化日期方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    * 格式化日期
    * @param {string | number | Date} value 指定日期
    * @param {string} format 格式化的规则
    * @example
    * ```js
    * formatDate();
    * formatDate(1603264465956);
    * formatDate(1603264465956, "h:m:s");
    * formatDate(1603264465956, "Y年M月D日");
    * ```
    */
    function formatDate(value = Date.now(), format = "Y-M-D h:m:s") {
    const formatNumber = n => `0${n}`.slice(-2);
    const date = new Date(value);
    const formatList = ["Y", "M", "D", "h", "m", "s"];
    const resultList = [];
    resultList.push(date.getFullYear().toString());
    resultList.push(formatNumber(date.getMonth() + 1));
    resultList.push(formatNumber(date.getDate()));
    resultList.push(formatNumber(date.getHours()));
    resultList.push(formatNumber(date.getMinutes()));
    resultList.push(formatNumber(date.getSeconds()));
    for (let i = 0; i < resultList.length; i++) {
    format = format.replace(formatList[i], resultList[i]);
    }
    return format;
    }

    15. 网页定位

    这里使用百度定位,无论代码封装、调用方式还是位置准确性都比微信sdk那个好用太多了,包括在任何网页端;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    /**
    * 插入脚本
    * @param {string} link 脚本路径
    * @param {Function} callback 脚本加载完成回调
    */
    function insertScript(link, callback) {
    const label = document.createElement("script");
    label.src = link;
    label.onload = function () {
    if (label.parentNode) label.parentNode.removeChild(label);
    if (typeof callback === "function") callback();
    }
    document.body.appendChild(label);
    }

    /**
    * 获取定位信息
    * @returns {Promise<{ city: string, districtName: string, province: string, longitude: number, latitude: number }>}
    */
    function getLocationInfo() {
    /**
    * 使用百度定位
    * @param {(value: any) => void} callback
    */
    function useBaiduLocation(callback) {
    const geolocation = new BMap.Geolocation({
    maximumAge: 10
    })
    geolocation.getCurrentPosition(function(res) {
    console.log("%c 使用百度定位 >>", "background-color: #4e6ef2; padding: 2px 6px; color: #fff; border-radius: 2px", res);
    callback({
    city: res.address.city,
    districtName: res.address.district,
    province: res.address.province,
    longitude: Number(res.longitude),
    latitude: Number(res.latitude)
    })
    })
    }

    return new Promise(function (resolve, reject) {
    if (!window._baiduLocation) {
    window._baiduLocation = function () {
    useBaiduLocation(resolve);
    }
    // ak=你自己的key
    insertScript("https://api.map.baidu.com/api?v=2.0&ak=66vCKv7PtNlOprFEe9kneTHEHl8DY1mR&callback=_baiduLocation");
    } else {
    useBaiduLocation(resolve);
    }
    })
    }

    16. 输入保留数字 <input type="text">

    使用场景:用户在输入框输入内容时,实时过滤保持数字值显示;

    tips:在Firefox中设置 <input type="number"> 会有样式 bug

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 输入只能是数字
    * @param {string | number} value 输入的值
    * @param {boolean} decimal 是否要保留小数
    * @param {boolean} negative 是否可以为负数
    */
    function inputOnlyNumber(value, decimal, negative) {
    let result = value.toString().trim();
    if (result.length === 0) return "";
    const minus = (negative && result[0] == "-") ? "-" : "";
    if (decimal) {
    result = result.replace(/[^0-9.]+/ig, "");
    let array = result.split(".");
    if (array.length > 1) {
    result = array[0] + "." + array[1];
    }
    } else {
    result = result.replace(/[^0-9]+/ig, "");
    }
    return minus + result;
    }
  • 仅使用CSS提高页面渲染速度

    用户在访问一个Web网站(页面)或应用时,总是希望它的加载速度快,功能流畅。如果过于慢,用户就很有可能失去耐心而离开你的Web网站或应用。作为开发人员,给自己应用提供更快的访问速度,提供很好的用户体验是必备的基础技能,而且Web开发者在开发中也可以做很多事情来改善用户体验。那我们今天就来和大家聊聊,在CSS方面有哪些技巧可以帮助我们来提高Web页面的渲染速度。

    内容可见性(content-visibility)

    一般来说,大多数Web应用都有复杂的UI元素,而且有的内容会在设备可视区域之外(内容超出了用户浏览器可视区域),比如下图中红色区域就在手机设备屏幕可视区域之外:

    在这种场合下,我们可以使用CSS的content-visibility来跳过屏幕外的内容渲染。也就是说,如果你有大量的离屏内容(Off-screen Content),这将会大幅减少页面渲染时间。

    这个功能是CSS新增的特性,隶属于 W3C 的 CSS Containment Module Level 2 模块。也是对提高渲染性能影响最大的功能之一。content-visibility可以接受visibleautohidden三个属性值,但我们可以在一个元素上使用content-visibility:auto来直接的提升页面的渲染性能。

    假设我们有一个像下面的页面,整个页面有个卡片列表,大约有375张,大约在屏幕可视区域能显示12张卡片。正如下图所示,渲染这个页面浏览器用时大约1037ms

    你可以给所有卡片添加content-visibility

    1
    2
    3
    .card {
    content-visibility: auto;
    }

    所有卡片加入content-visibility样式之后,页面的渲染时间下降到150ms,差不多提高了六倍的渲染性能:

    正如你所看到的,content-visibility非常强大,提高页面渲染非常有用。换然话说,有了CSS的content-visibility属性,影响浏览器的渲染过程就变得更加容易。本质上,这个属性 改变了一个元素的可见性,并管理其渲染状态

    content-visibility有点类似于CSS的displayvisibility属性,然而,content-visibility的操作方式与这些属性不同。

    content-visibility的关键能力是,它允许我们推迟我们选择的HTML元素渲染。 默认情况之下,浏览器会渲染DOM树内所有可以被用户查看的元素。用户可以看到视窗可视区域中所有元素,并通过滚动查看页面内其他元素。一次渲染所有的元素(包括视窗可视区域之外不可见的HTML元素)可以让浏览器正确计算页面的尺寸,同时保持整个页面的布局和滚动条的一致性。

    如果浏览器不渲染页面内的一些元素,滚动将是一场噩梦,因为无法正确计算页面高度。这是因为,content-visibility会将分配给它的元素的高度(height)视为0,浏览器在渲染之前会将这个元素的高度变为0,从而使我们的页面高度和滚动变得混乱。但如果已经为元素或其子元素显式设置了高度,那这种行为就会被覆盖。如果你的元素中没显式设置高度,并且因为显式设置height可能会带来一定的副作用而没设置,那么我们可以使用contain-intrinsic-size来确保元素的正确渲染,同时也保留延迟渲染的好处。

    1
    2
    3
    4
    .card {
    content-visibility: auto;
    contain-intrinsic-size: 200px;
    }

    这也意味着它将像有一个“固有尺寸”(Intrinsic-size)的单一子元素一样布局,确保你没设置尺寸的div(示例中的.card)仍然占据空间。contain-intrinsic-size作为一个占位符尺寸来替代渲染内容。

    虽然contain-intrinsic-size能让元素有一个占位空间,但如果有大量的元素都设置了content-visibility: auto,滚动条仍然会有较小的问题。

    content-visibility提供的另外两个值visiblehidden可以让我们实现像元素的显式和隐藏,类似于displaynone和非none值的切换:

    在这种情况下,content-visibility可以提高频繁显示或隐藏的元素的渲染性能,例如模态框的显示和隐藏。content-visibility可以提供这种性能提升,这要归功于其隐藏值(hidden)的功能与其他值的不同:

    • display: none:隐藏元素并破坏其渲染状态。 这意味着取消隐藏元素与渲染具有相同内容的新元素一样昂贵
    • visibility: hidden:隐藏元素并保持其渲染状态。 这并不能真正从文档中删除该元素,因为它(及其子树)仍占据页面上的几何空间,并且仍然可以单击。 它也可以在需要时随时更新渲染状态,即使隐藏也是如此
    • content-visibility: hidden:隐藏元素并保留其渲染状态。这意味着该元素隐藏时行为和display: none一样,但再次显示它的成本要低得多

    content-visibility属性的扩展阅读:

    合理使用will-change

    CSS渲染器(CSS Renderer)在渲染CSS样式之前需要一个准备过程,因为有些CSS属性需要CSS渲染器事先做很多准备才能实现渲染。这就很容易导致页面出现卡顿,给用户带来不好的体验。

    比如Web上的动效,通常情况之下,Web动画(在动的元素)是和其他元素一起定期渲染的,以往在动画开发时,会使用CSS的3D变换(transform中的translate3d()translateZ())来开启GPU加速,让动画变得更流畅,但这样做是一种黑魔法,会将元素和它的上下文提到另一个“层”,独立于其他元素被渲染。可这种将元素提取到一个新层,相对来说代价也是昂贵的,这可能会使transform动画延迟几百毫秒。

    不过,现在我可以不使用transform这样的Hack手段来开启GPU加速,可以直接使用CSS的will-change属性,该属性可以表明元素将修改特定的属性,让浏览器事先进行必要的优化。也就是说,will-change是一个UA提示,它不会对你使用它的元素产生任何样式上的影响。但值得注意的是,如果创建了新的层叠上下文,它可以产生外观效果。

    比如下面这样的一个动画示例:

    1
    2
    3
    4
    5
    6
    7
    <!-- HTML -->
    <div class="animate"></div>

    /* CSS */
    .animate {
    will-change: opacity
    }

    浏览器渲染上面的代码时,浏览器将为该元素创建一个单独的层。之后,它将该元素的渲染与其他优化一起委托给GPU,即,浏览器会识别will-change属性,并优化未来与不透明相关的变化。这将使动画变得更加流畅,因为GPU加速接管了动画的渲染。

    根据 @Maximillian Laumeister 所做的性能基准,可以看到,他通过这种单行变化获得了超过120FPS的渲染速度,和最初的渲染速度(大约50FPS)相比,提高70FPS左右。

    will-change的使用并不复杂,它能接受的值有:

    • auto:默认值,浏览器会根据具体情况,自行进行优化
    • scroll-position:表示开发者将要改变元素的滚动位置,比如浏览器通常仅渲染可滚动元素“滚动窗口”中的内容。而某些内容超过该窗口(不在浏览器的可视区域内)。如果will-change显式设置了该值,将扩展渲染“滚动窗口”周围的内容,从而顺利地进行更长,更快的滚动(让元素的滚动更流畅)
    • content:表示开发者将要改变元素的内容,比如浏览器常将大部分不经常改变的元素缓存下来。但如果一个元素的内容不断发生改变,那么产生和维护这个缓存就是在浪费时间。如果will-change显式设置了该值,可以减少浏览器对元素的缓存,或者完全避免缓存。变为从始至终都重新渲染元素。使用该值时需要尽量在文档树最末尾上使用,因为该值会被应用到它所声明元素的子节点,要是在文档树较高的节点上使用的话,可能会对页面性能造成较大的影响
    • <custom-ident>:表示开发者将要改变的元素属性。如果给定的值是缩写,则默认被扩展全,比如,will-change设置的值是padding,那么会补全所有padding的属性,如 will-change: padding-top, padding-right, padding-bottom, padding-left;

    详细的使用,请参阅:

    虽然说will-change能提高性能,但这个属性应该被认为是最后的手段,它不是为了过早的优化。只有消退你必须处理性能问题时,你才应该使用它。如果你滥用的话,反而会降低Web的性能。比如:

    使用will-change表示该元素在未来会发生变化

    因此,如果你试图将will-change和动画同时使用,它将不会给你带来优化。因此,建议在父元素上使用will-change,在子元素上使用动画。

    1
    2
    3
    4
    5
    6
    7
    .animate-element-parent {
    will-change: opacity;
    }

    .animate-element {
    transition: opacity .2s linear
    }

    不要使用非动画元素

    当你在一个元素上使用will-change时,浏览器会尝试通过将元素移动到一个新的图层并将转换工作交互GPU来优化它。如果你没有任何要转换的内容,则会导致资源浪费。

    除此之外,要用好will-change也不是件易事,MDN在这方面做出了相应的描述

    • 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。比如 *{will-change: transform, opacity;}
    • 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值
    • 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。
    • 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

    最后需要注意的是,建议在完成所有动画后,将元素的will-change删除。下面这个示例展示如何使用脚本正确地应用 will-change 属性的示例,在大部分的场景中,你都应该这样做。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var el = document.getElementById('element');

    // 当鼠标移动到该元素上时给该元素设置 will-change 属性
    el.addEventListener('mouseenter', hintBrowser);
    // 当 CSS 动画结束后清除 will-change 属性
    el.addEventListener('animationEnd', removeHint);

    function hintBrowser() {
    // 填写上那些你知道的,会在 CSS 动画中发生改变的 CSS 属性名们
    this.style.willChange = 'transform, opacity';
    }

    function removeHint() {
    this.style.willChange = 'auto';
    }

    在实际使用will-change可以记作以下几个规则,即 五可做,三不可做

    • 在样式表中少用will-change
    • will-change足够的时间令其发挥该有的作用
    • 使用<custom-ident>来针对超特定的变化(如,left, opacity等)
    • 如果需要的话,可以JavaScript中使用它(添加和删除)
    • 修改完成后,删除will-change
    • 不要同时声明太多的属性
    • 不要应用在太多元素上
    • 不要把资源浪费在已停止变化的元素上

    让元素及其内容尽可能独立于文档树的其余部分(contain)

    W3C的CSS Containment Module Level 2除了提供前面介绍的content-visibility属性之外,还有另一个属性contain。该属性允许我们指定特定的DOM元素和它的子元素,让它们能够独立于整个DOM树结构之外。目的是能够让浏览器有能力只对部分元素进行重绘、重排,而不必每次针对整个页面。即,允许浏览器针对DOM的有限区域而不是整个页面重新计算布局,样式,绘画,大小或它们的任意组合。

    在实际使用的时候,我们可以通过contain设置下面五个值中的某一个来规定元素以何种方式独立于文档树:

    • layout :该值表示元素的内部布局不受外部的任何影响,同时该元素以及其内容也不会影响以上级
    • paint :该值表示元素的子级不能在该元素的范围外显示,该元素不会有任何内容溢出(或者即使溢出了,也不会被显示)
    • size :该值表示元素盒子的大小是独立于其内容,也就是说在计算该元素盒子大小的时候是会忽略其子元素
    • content :该值是contain: layout paint的简写
    • strict :该值是contain: layout paint size的简写

    在上述这几个值中,sizelayoutpaint可以单独使用,也可以相互组合使用;另外contentstrict是组合值,即contentlayout paint的组合,strictlayout paint size的组合。

    containsizelayoutpaint提供了不同的方式来影响浏览器渲染计算:

    • size: 告诉浏览器,当其内容发生变化时,该容器不应导致页面上的位置移动
    • layout:告诉浏览器,容器的后代不应该导致其容器外元素的布局改变,反之亦然
    • paint:告诉浏览器,容器的内容将永远不会绘制超出容器的尺寸,如果容器是模糊的,那么就根本不会绘制内容

    @Manuel Rego Casasnovas提供了一个示例,向大家阐述和演示了contain是如何提高Web页面渲染性能。这个示例中,有10000个像下面这样的DOM元素:

    1
    2
    3
    <div class="item">
    <div>Lorem ipsum...</div>
    </div>

    使用JavaScript的textContent这个API来动态更改div.item > div的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    const NUM_ITEMS = 10000;
    const NUM_REPETITIONS = 10;

    function log(text) {
    let log = document.getElementById("log");
    log.textContent += text;
    }

    function changeTargetContent() {
    log("Change \"targetInner\" content...");

    // Force layout.
    document.body.offsetLeft;

    let start = window.performance.now();

    let targetInner = document.getElementById("targetInner");
    targetInner.textContent = targetInner.textContent == "Hello World!" ? "BYE" : "Hello World!";

    // Force layout.
    document.body.offsetLeft;

    let end = window.performance.now();
    let time = window.performance.now() - start;
    log(" Time (ms): " + time + "\n");
    return time;
    }

    function setup() {
    for (let i = 0; i < NUM_ITEMS; i++) {
    let item = document.createElement("div");
    item.classList.add("item");

    let inner = document.createElement("div");
    inner.style.backgroundColor = "#" + Math.random().toString(16).slice(-6);
    inner.textContent = "Lorem ipsum...";
    item.appendChild(inner);

    wrapper.appendChild(item);
    }
    }

    如果不使用contain,即使更改是在单个元素上,浏览器在布局上的渲染也会花费大量的时间,因为它会遍历整个DOM树(在本例中,DOM树很大,因为它有10000个DOM元素):

    在本例中,div的大小是固定的,我们在内部div中更改的内容不会溢出它。因此,我们可以将contain: strict应用到项目上,这样当项目内部发生变化时,浏览器就不需要访问其他节点,它可以停止检查该元素上的内容,并避免到外部去。

    尽管这个例子中的每一项都很简单,但通过使用contain,Web性能得到很大的改变,从~4ms降到了~0.04ms,这是一个巨大的差异。想象一下,如果DOM树具有非常复杂的结构和内容,但只修改了页面的一小部分,如果可以将其与页面的其他部分隔离开来,那么将会发生什么情况呢?

    有关于contain的更多内容:

    使用font-display解决由于字体造成的布局偏移(FOUT)

    在Web开发的过程中,难免会使用@font-face技术引用一些特殊字体(系统没有的字体),同时也可能会配合变量字体特性,使用更具个性化的字体。

    使用@font-face加载字体策略大概如下图所示:

    上图来自于@zachleat的《A COMPREHENSIVE GUIDE TO FONT LOADING STRATEGIES》一文。

    Web中使用非系统字体(@font-face规则引入的字体)时,浏览器可能没有及时得到Web字体,就会让它用后备系统字体渲染,然后优化我们的字体。这个时候很容易引起未编排(Unstyled)的文本引起闪烁,整个排版本布局也看上去会偏移一下(FOUT)。

    幸运的是,根据@font-face规则,font-display属性定义了浏览器如何加载和显示字体文件,允许文本在字体加载或加载失败时显示回退字体。可以通过依靠折中无样式文本闪现使文本可见替代白屏来提高性能。

    CSS的font-display属性有五个不同的值:

    • auto :默认值。典型的浏览器字体加载的行为会发生,也就是使用自定义字体的文本会先被隐藏,直到字体加载结束才会显示。即字体展示策略与浏览器一致,当前,大多数浏览器的默认策略类似block
    • block :给予字体一个较短的阻塞时间(大多数情况下推荐使用 3s)和无限大的交换时间。换言之,如果字体未加载完成,浏览器将首先绘制“隐形”文本;一旦字体加载完成,立即切换字体。为此,浏览器将创建一个匿名字体,其类型与所选字体相似,但所有字形都不含“墨水”。使用特定字体渲染文本之后页面方才可用,只有这种情况下才应该使用 block
    • swap :使用 swap,则阻塞阶段时间为 0,交换阶段时间无限大。也就是说,如果字体没有完成加载,浏览器会立即绘制文字,一旦字体加载成功,立即切换字体。与 block 类似,如果使用特定字体渲染文本对页面很重要,且使用其他字体渲染仍将显示正确的信息,才应使用 swap
    • fallback :这个可以说是autoswap的一种折中方式。需要使用自定义字体渲染的文本会在较短的时间不可见,如果自定义字体还没有加载结束,那么就先加载无样式的文本。一旦自定义字体加载结束,那么文本就会被正确赋予样式。使用 fallback时,阻塞阶段时间将非常小(多数情况下推荐小于 100ms),交换阶段也比较短(多数情况下建议使用 3s)。换言之,如果字体没有加载,则首先会使用后备字体渲染。一旦加载成功,就会切换字体。但如果等待时间过久,则页面将一直使用后备字体。如果希望用户尽快开始阅读,而且不因新字体的载入导致文本样式发生变动而干扰用户体验,fallback 是一个很好的选择。
    • optional :效果和fallback几乎一样,都是先在极短的时间内文本不可见,然后再加载无样式的文本。不过optional选项可以让浏览器自由决定是否使用自定义字体,而这个决定很大程度上取决于浏览器的连接速度。如果速度很慢,那你的自定义字体可能就不会被使用。使用 optional 时,阻塞阶段时间会非常小(多数情况下建议低于 100ms),交换阶段时间为 0

    下面是使用swap值的一个例子:

    1
    2
    3
    4
    5
    6
    7
    @font-face {
    font-family: "Open Sans Regular";
    font-weight: 400;
    font-style: normal;
    src: url("fonts/OpenSans-Regular-BasicLatin.woff2") format("woff2");
    font-display: swap;
    }

    在这个例子里我们通过只使用WOFF2文件来缩写字体。另外我们使用了swap作为font-display的值,页面的加载情况将如下图所示:

    注意,font-display一般放在@font-face规则中使用

    有关于字体加载和font-display更多的介绍,可以阅读:

    scroll-behavior让滚动更流畅

    早前在滚动的特性和改变用户体验的滚动新特性中向大家介绍了几个可以用来改变用户体验的滚动特性,比如滚动捕捉、overscroll-behaviorscroll-behavior

    scroll-behavior是CSSOM View Module提供的一个新特性,可以轻易的帮助我们实现丝滑般的滚动效果。该属性可以为一个滚动框指定滚动行为,其他任何的滚动,例如那些由于用户行为而产生的滚动,不受这个属性的影响。

    scroll-behavior接受两个值:

    • auto :滚动框立即滚动
    • smooth :滚动框通过一个用户代理定义的时间段使用定义的时间函数来实现平稳的滚动,用户代理平台应遵循约定,如果有的话

    除此之外,其还有三个全局的值:inheritinitialunset

    使用起来很简单,只需要这个元素上使用scroll-behavior:smooth。因此,很多时候为了让页面滚动更平滑,建议在html中直接这样设置一个样式:

    1
    2
    3
    html {
    scroll-behavior:smooth;
    }

    口说无凭,来看个效果对比,你会有更好的感觉:

    有关于scroll-behavior属性更多的介绍可以再花点时间阅读下面这些文章:

    开启GPU渲染动画

    浏览器针对处理CSS动画和不会很好地触发重排(因此也导致绘)的动画属性进行了优化。为了提高性能,可以将被动画化的节点从主线程移到GPU上。将导致合成的属性包括 3D transforms (transform: translateZ(), rotate3d(),等),animatingtransformopacity, position: fixedwill-change,和 filter。一些元素,例如 <video>, <canvas><iframe>,也位于各自的图层上。 将元素提升为图层(也称为合成)时,动画转换属性将在GPU中完成,从而改善性能,尤其是在移动设备上。

    减少渲染阻止时间

    今天,许多Web应用必须满足多种形式的需求,包括PC、平板电脑和手机等。为了完成这种响应式的特性,我们必须根据媒体尺寸编写新的样式。当涉及页面渲染时,它无法启动渲染阶段,直到 CSS对象模型(CSSOM)已准备就绪。根据你的Web应用,你可能会有一个大的样式表来满足所有设备的形式因素。

    但是,假设我们根据表单因素将其拆分为多个样式表。在这种情况下,我们可以只让主CSS文件阻塞关键路径,并以高优先级下载它,而让其他样式表以低优先级方式下载。

    1
    <link rel="stylesheet" href="styles.css">

    将其分解为多个样式表后:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- style.css contains only the minimal styles needed for the page rendering -->
    <link rel="stylesheet" href="styles.css" media="all" />

    <!-- Following stylesheets have only the styles necessary for the form factor -->
    <link rel="stylesheet" href="sm.css" media="(min-width: 20em)" />
    <link rel="stylesheet" href="md.css" media="(min-width: 64em)" />
    <link rel="stylesheet" href="lg.css" media="(min-width: 90em)" />
    <link rel="stylesheet" href="ex.css" media="(min-width: 120em)" />
    <link rel="stylesheet" href="print.css" media="print" />

    默认情况下,浏览器假设每个指定的样式表都是阻塞渲染的。通过添加 media属性附加媒体查询,告诉浏览器何时应用样式表。当浏览器看到一个它知道只会用于特定场景的样式表时,它仍会下载样式,但不会阻塞渲染。通过将 CSS 分成多个文件,主要的 阻塞渲染 文件(本例中为 styles.css)的大小变得更小,从而减少了渲染被阻塞的时间。

    避免@import包含多个样式表

    通过 @import,我们可以在另一个样式表中包含一个样式表。当我们在处理一个大型项目时,使用 @import 可以使代码更加简洁。

    关于 @import 的关键事实是,它是一个阻塞调用,因为它必须通过网络请求来获取文件,解析文件,并将其包含在样式表中。如果我们在样式表中嵌套了 @import,就会妨碍渲染性能。

    1
    2
    3
    4
    5
    /* style.css */
    @import url("windows.css");

    /* windows.css */
    @import url("componenets.css");

    与使用 @import 相比,我们可以通过多个 link 来实现同样的功能,但性能要好得多,因为它允许我们并行加载样式表。

    注意动态修改自定义属性方式

    CSS自定义属性又名CSS变量,该特性已经是非常成熟的特性了,可以在Web的开发中大胆的使用该特性:

    1
    2
    3
    4
    5
    :root { --color: red; }

    button {
    color: var(--color);
    }

    在使用CSS自定义属性时,时常在root(根元素)上注册自定义属性,这种方式注册的自定义属性是个全局的自定义属性(全局变量),可以被所有嵌套的子元素继承。就上例而言,--color属性允许任何button样式将其作为变量使用。

    熟悉CSS自定义属性的同学都知道,可以使用style.setProperty来重新设置已注册好的自定义属性的值。但在修改根自定义属性时,需要注意,因为它会影响Web的性能。早在2017年@Lisi Linhart 在《Performance of CSS Variables》中阐述过。

    • 在使用CSS变量时,我们总是要注意我们的变量是在哪个范围内定义的,如果改变它,将影响许多子代,从而产生大量的样式重新计算。

    • 结合CSS变量使用calc()是一个很好的方法,可以获得更多的灵活性,限制我们需要定义的变量数量。在不同的浏览器中测试calc()与CSS变量的结合,并没有发现任何大的性能问题。然而在一些浏览器中对一些单位的支持还是有限的,比如degms,所以我们必须记住这一点。

    • 如果我们比较一下在JavaScript中通过内联样式设置变量与setProperty方法的性能标志,浏览器之间有一些明显的差异。在Safari中通过内联样式设置属性的速度非常快,而在Firefox中则非常慢,所以使用setProperty设置变量是首选

    有关于这方面的具体细节就不在这阐述了,如果你对这方面感兴趣的话,可以阅读下面这几篇文章:

    小结

    可能很多人会说,5G已到来,终端设备性能越来越好,网络环境也越来越强,Web性能已不是问题了,但事实上在Web开发过程中总是难免碰到性能是的问题。而且我们为用户提供更流畅的体验也是我们必备技术之一。时至今日,优化Web性能的方式和手段很多,但在开发时注重每个细节,可以让我们把性能做得更好。正如文章中提到这些。

    除了文章提到的这几个点,还有一些其他的方法可以使用CSS来提高网页的性能。当然,文章中提到的一些特性还没有得到所有浏览器支持,比如content-visibilitycontain等,但在未来它们肯定能让页面渲染带来更快的渲染。另外,文章中提到的一些技巧并没有深入阐述,比如CSS的引用方式,CSS的阻塞等。

  • 这一次彻底弄懂函数防抖与函数节流

    函数节流和函数防抖,两者都是优化高频率执行js代码的一种手段。

    我们了解一下什么是防抖

    函数防抖: 频繁触发的情况下,只有等待足够的空闲时间,才可以执行代码一次。

    函数防抖的影响:防止函数在极短的时间内反复调用,造成资源的浪费

    就比如在页面上的某些事件触发频率非常高,比如滚动条滚动、窗口尺寸变化、鼠标移动等,如果我们需要注册这类事件,不得不考虑效率问题,又特别是事件处理中涉及到了大量的操作,让我们为之头痛。 当窗口尺寸发生变化时,哪怕只变化了一点点,都有可能造成成百上千次对处理函数的调用, 这对网页性能的影响是极其巨大的,很可能就造成页面的阻塞。于是,我们可以考虑,每次窗口尺寸变化、滚动条滚动、鼠标移动,不要立即执行相关操作,而是等一段时间,以窗口尺寸停止变化、滚动条不再滚动、鼠标不再移动为计时起点,一段时间后再去执行操作,就像电梯关门那样。

    也就是说触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时,重新开始计时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 简单版本
    function debounce(func, wait) {
    // 使用闭包来维护timer
    var timeout = null;

    return function () {
    var context = this;
    var args = arguments;
    clearTimeout(timeout)
    // 注册回调函数
    timeout = setTimeout(function(){
    func.apply(context, args)
    }, wait);
    }
    }
    function testout(e, content) {
    console.log(e, content);
    }
    var fn = debounce(testout, 2000); // 防抖函数
    document.onmousemove = function (e) {
    fn(e, 'debounce'); // 给防抖函数传参
    }

    鼠标一直移动(一直在触发onmousemove里面的事件,但是有debounce约束),则不输出,若停止移动,则在间隔1S后输出一次(唯一的一次哦!!!))

    1
    2
    3
    setInterval(debounce(fn,500),1000) // 第一次在1500ms后触发打印,之后每1000ms触发一次
    // 打印一次debounce
    setInterval(debounce(fn,2000),1000) // 不会触发一次(我把函数防抖看出技能读条,如果读条没完成就用技能,便会失败而且重新读条)

    应用场景有表单的防止重复提交,搜索框提示发送多次HTTP请求 有兴趣的可以输入的了解一下哦!

    参考JavaScript专题之跟着underscore学防抖

    什么是节流

    节流函数: 让一个函数无法在短时间内连续调用,只有当上一次函数执行后,过了规定的时间间隔,才能进行下一次该函数的调用。或者说你在操作的时候不会马上执行该函数,而是等你不操作的时候才会执行。 对于函数节流,有如下几个场景:

    1. 游戏中的刷新率
    2. DOM元素拖拽
    3. Canvas画笔功能

    函数节流

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function throttle(fn, gapTime) {
    let _lastTime = null;

    return function () {
    let _nowTime = + new Date()
    if (_nowTime - _lastTime > gapTime || !_lastTime) {
    fn();
    _lastTime = _nowTime
    }
    }
    }

    let fn = ()=>{
    console.log('boom')
    }

    setInterval(throttle(fn,1000),100)

    每0.1s在任务队列中注册一个throttle函数,实现的一个简单的函数节流,结果却是一秒打出一次boom

    小结::函数防抖和节流,都是控制事件触发频率的方法。

  • 按钮权限实现方案

    背景需求:ERP系统需增加 ”按钮权限控制“ 功能,对权限的控制粒度要普及到按钮层级。

    预期

    按钮权限控制的交互方式无非两种:**”不可见”** 和 **”可见不可点”**。

    不可见

    不可见的交互方式相对简单,我们可使用 v-if 控制其是否显示。使用 v-show 也行,但不够保险,毕竟 v-show 只是把样式改成 display: none,在真实的 DOM 渲染还是存在的,所以更推荐 v-if 来控制不可见。

    可见不可点

    “看是能看了,但你不行了”。

    • 样式控制(得加个禁用样式),什么 cursor: not-allowed ,置灰之类的云云;
    • 不可点击,即要禁用或屏蔽点击事件,好像有 preventDefault/stopProgration 可实现;

    最终产品需求选择了 “可见不可点”,原因可能就觉得不可见太简单了。(¬_¬)

    思路探索

    1. 给按钮点击事件的回调函数,加个包装函数,对其权限控制,进行事件拦截与触发。相当是做了个代理,有点高阶组件那意思(但对现有业务改动太大,得对每个@click绑定函数逐个修改,遂放弃该方案);
    2. 阻止按钮点击事件冒泡与触发,貌似能用上 preventDefautl/stopProgration, 感觉能用指令的方式对 DOM 元素进行事件监听,允许的话则让事件正常执行,不允许则拦截屏蔽;

    实践方案

    最终选择了指令的方式,最小成本扩展,避免改动现有业务代码逻辑。

    针对权限控制需做点击劫持的元素:

    • el-button
    • btn-wrapper(自封装组件)
    • div/span/a 等标签

    具体实现方案请看下文:

    权限入口:Vuex 控制,全局使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 用户登陆后,获取该用户权限 CODE 码,并存储至 store
    this.$store.commit('SET_AUTH_CODE', authCodeList);

    SET_AUTH_CODE: (state, acthCode) => {
    if (acthCode) {
    state.autoCodeList = acthCode;
    }
    setStore({
    name: 'autoCodeList',
    content: state.autoCodeList || [],
    });
    }

    定义权限指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const disableClickFn = (event) => {
    event && event.stopImmediatePropagation();
    }

    export const hasPermission = () => {
    Vue.directive('permission', {
    bind(el, binding) {
    let disalbe = true;
    if (autoCodeList.length && autoCodeList.includes(binding.value)) {
    disable = false;
    }

    if (disable) {
    el.classList.add('permission-disabled');
    el.setAttribute('disabled', 'disabled');
    el.addEventListener('click', disableClickFn, true);
    }
    },
    unbind(el) {
    el.removeEventListener('click', disableClickFn);
    }
    });
    };
    1. 首先 addEventListener 第三个参数我们使用 useCapturetrue 让其在捕获阶段触发,因此这里的事件监听器会优先 @click 触发回调;
    2. 其次使用了 stopImmediatePropagation 阻止事件冒泡和其它相同事件监听器的触发;

    如果多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件监听器中执行 stopImmediatePropagation() ,那么剩下的事件监听器都不会被调用。MSDN - stopImmediatePropagation

    增加禁用的 CSS 样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    .permission-disabled {
    position: relative;
    cursor: not-allowed !important;
    pointer-events: none; // 阻止元素成为鼠标事件
    border:none;
    background-image: none;
    &::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0px;
    right: 0px;
    height: 100%;
    z-index: 9;
    background: rgba(255, 255, 255, 0.5);
    }
    }

    这里使用了一个比较陌生的 CSS 属性, pointer-events

    CSS3 的 pointer-events 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target。 更多用法参考:MSDN - pointer-events

    这里使用 pointer-events 只是一个辅助功能,并不一定意味着元素上的事件监听器永远不会触发,如果后代元素有指定 pointer-events 并允许成为事件目标的话,是可以触发父元素事件,而且单纯依靠 CSS 属性来控制不点击,还是有风险,因此这里仅作辅助作用。

    全局 “权限判断” 工具函数

    1
    2
    3
    4
    5
    6
    import { getStore, } from '@/util/store';
    const autoCodeList = getStore({ name: 'autoCodeList', }) || [];

    export function hasPermission(authName) {
    return !(autoCodeList.length > 0 && autoCodeList.includes(authName));
    }

    具体使用

    1
    2
    3
    4
    5
    // 指令方式(这里的 oms/order/save 就是对应用户登陆时 CODE 权限码)
    <el-button v-permission="'oms:order:save'">保存</el-button>

    // 函数方式
    <el-button :disabled="hasPermission('oms:order:save')"></el-button>