浏览器跨页面通信方案(好多种)

753 阅读7分钟

浏览器跨页面通信方案

作者: GZHDEV 2021/1/6

前言: 前几天搞了web worker的相关知识点, 今天来看看一个另一个比较有趣的东西, 跨页面通信的实现方案. 这篇文章的篇幅挺长的, 所以方法也是比较齐全的, 如果你想了解浏览器跨页面通信的方案, 读完这篇足够了.❤️

能想到的方案:

  • BroadcastChannel
  • postMessage
  • localStorage
  • SharedWorker
  • SSE
  • websocket
  • service worker
  • MQTT
  • cookie
  • cookieStore
  • indexedDB

1. Broadcast Channel

第一个要介绍的broadcast channel, 这个api可能知道它的人并不会特别多, 但是在跨页面通信的时候却十分的简单, 就几个简单的方法. 但是broadcast channel只支持同源下的跨页面通信, 多说无益, 来看看呗

The principle of the Broadcast Channel API

<!-- page1.html -->
<h1>page1</h1>
<script>
  const bc = new BroadcastChannel('first-channel');
  bc.onmessage = e => {
    console.log('page1: ', e);
  }
  setTimeout(() => {
    bc.postMessage('hello every one, this message from page1.html');
    bc.close();
  }, 2000);
</script>
<!-- page2.html -->
<h1>page2</h1>
<script>
  const bc = new BroadcastChannel('first-channel');
  bc.onmessage = e => {
    console.log('page2: ', e);

    bc.postMessage('this is message from page2.html');
  }
</script>

具体步骤:

  1. 通过BroadcastChannel构造函数实例化一个实例, 传入消息通道名称
  2. 创建一个消息通信通道, 通过两个简单的接受方法进行通信
  3. postMessge发送消息, onmessage接收消息

特别简单对不对~ 那继续

2. postMessage

接下来介绍下postMessge, 这货兼容性超级好, 还简单, 能满足需求就用它用它

image-20210111203000601

看看这兼容性, 稳得不行, 看看语法↓

otherWindow.postMessage(message, targetOrigin, [transfer]);

优缺点: 必须是自己打开的窗口, 并且能获取到窗口的句柄. 但是可以跨域通信.

鄙人也不是很善言辞, 直接就上demo看看吧

<!-- page1.html -->
<h1>page1</h1>
<button id="btn">send</button>
<script>
  const child = window.open('page2.html');
  const btn = document.querySelector('#btn');
  btn.addEventListener('click', () => {
    // 跨域需要填写origin
    // child.postMessage('hello', 'http://test2.local.com:8080');
    // child.postMessage('hello', '*')

    child.postMessage('hello');
    console.log('send ok!');
  });
</script>
<!-- page2.html -->
<h1>page2</h1> 
<script>
  window.addEventListener('message', e => {
    console.log('page2: ', e);
    // 不能直接获取, 但是可以e.source.postMessage回复消息到主窗口
    // console.log('source: ', e.source);
  })
  window.addEventListener('storage', e => {
    console.log('storage: ', e);
  })
</script>

postMessage可以安全的实现跨页面通信(支持跨域通信)

安全问题

如果不希望获取来自其他网站或者浏览器插件的信息, 不要在页面上监听messge事件就好啦.

3. localStorage

localstorage, 你没看错就是本地存储的localstorage, 他还有个兄弟叫sessionStorage, 区别是什么? 区别是localStorage你不手动的去清除, 就一直存在你的电脑硬盘里面, 而sessionStorage的生命周期只有会话期间存在, 当你把页面关闭只有就自动的清除掉了.

而利用localstorage的storage事件可以进行跨越通信, 当localstorage变化的时候就会触发storage事件, 但是得注意一点, localstorage的storage事件只有跨页面才能监听到, 本页面是无效的, 想实现本页面监听localstorage的话也挺简单, 自己重写下localstorage的api, 手动dispatch事件就好了, 说那么多废话, 不如直接来看看demo

<!-- page1.html -->
<h1>page1</h1>
<button class="btn">set</button>
<script>
  const btn = document.querySelector('.btn');
  // 修改localstorage的页面想监听storage变化得重写setItem
  window.addEventListener('storage', e => {
    console.log('storage: ', e);
  })
  // 重写setItem方法
  localStorage.setItem = (key, value) => {
    // 自定义事件
    const event = new CustomEvent('storage', {
      detail: {
        oldData: { [key]: localStorage.getItem(key) },
        newData: { [key]: value },
        localData: { ...localStorage },
      }
    });
    localStorage[key] = value;
    // 完成原生setItem的功能后, 手动dispatch事件
    window.dispatchEvent(event);
  }
  btn.addEventListener('click', () => {
    localStorage.setItem('hello', 'guozhenghong');
  })
</script>
<!-- page2.html -->
<h1>page2</h1>
<script>
  window.addEventListener('storage', e => {
    console.log(e);
  })
</script>

注意事项

  1. 触发写入操作的页面下的storage监听器不会被触发
  2. storage事件只有在发生改变的时候才会触发,即重复设置相同值不会触发storage监听
  3. safari隐身模式下无法设置localStorage值

4.SharedWorker

这第四个要介绍的跨页面通信方案, 就是这个SharedWorker了, 实现跨页面通信的原理是这个API会公用一个worker, 通过同一个port去通信, 注意了, 这货也不支持跨域通信, 想了解更多SharedWorker的细节可以去MDN官网查看, 或者我前面讲worker的文章了解. 上demo

<!-- page1.html -->
<h1>page1</h1>
<input type="text" class="value-box">
<h2 class="content"></h2>
<button class="set-message">setMessage</button>
<button class="get-message">getMessage</button>
<script>
  const $ = el => document.querySelector(el);
  const sw = new SharedWorker('worker.js');
  $('.set-message').addEventListener('click', () => {
    sw.port.postMessage($('.value-box').value);
    console.log('set ok!');
  });
  // 采用DOM3级别的addEventListener进行监听,必须port.start激活
  $('.get-message').addEventListener('click', () => {
    sw.port.postMessage('get');
    console.log('get ok!');
  });
  sw.port.addEventListener('message', ({data}) => {
    $('.content').textContent = `value: ${data}`;
    console.log(data);
  });
  sw.port.start();
</script>
<!-- page2.html -->
<h1>page2</h1>
<input type="text" class="value-box">
<h2 class="content"></h2>
<button class="set-message">setMessage</button>
<button class="get-message">getMessage</button>
<script>
  const $ = el => document.querySelector(el);
  const sw = new SharedWorker('worker.js');
  $('.set-message').addEventListener('click', () => {
    sw.port.postMessage($('.value-box').value);
    console.log('set ok!');
  });
  $('.get-message').addEventListener('click', () => {
    sw.port.postMessage('get')
    console.log('get ok!');
  });
  sw.port.addEventListener('message', ({data}) => {
    console.log(data);
    $('.content').textContent = `value: ${data}`;
  });
  sw.port.start();
</script>
// worker.js
let store = null;
onconnect = (e) => {
  const port = e.ports[0];
  handleMessage(port);
}
// 通过定义一个全局变量store来缓存来自页面的数据
const handleMessage = (port) => {
  port.onmessage = (e) => {
    switch(e.data) {
      case 'get': 
        port.postMessage(store);
        break;
      default:
        store = e.data;
        break;
    }
  }
}

上面的demo实现的功能是, 两个页面之间信息的发送和接受, 公用一个worker.js来处理逻辑.

5. Websocket

众所周知的, http请求是由客户端请求, 服务端进行响应, http请求是无状态, 走http协议, 服务器以前是没有能力主动往客户端推送数据的, 等下要说的SSE这货可以, 先由客户端发起一次请求, 但是现在要说的websocket就很强了, 它是建立在TCP连接上的应用程序协议, 和HTTP并没有太大关系, 但是在浏览器中, 需要走http进行握手连接和断开连接.

websocket使浏览器有了双向通信的能力, websocket也是html5规范的一部分, 支持HTML5的浏览器已经集成了websocket的API, 但是这里要展示的demo用的是经过二次封装的socket.io客户端, 服务端采用的是nodeJS,

// index.js
const fs = require("fs");
const server = require("http").createServer((req, res) => {
  if (/html$/i.test(req.url.toLowerCase())) {
      fs.createReadStream('./public' + req.url).pipe(res);
  }
});
const io = require('socket.io')(server);

io.on('connection', client => {
  console.log('client connect to server!');
	// 使用io发送的是广播信息, 所有连接的客户端都能监听到
  client.on('broadcast-channel', data => {
    io.emit('broadcast-channel', data);
  });

  client.on('disconnect', e => {
    console.log('disconnect: ', e);
  });
});

server.listen(3000);
<!-- page1.html -->
<h1>page1</h1>
<input type="text" class="value-provider">
<h1>received ↓: <p class="content-box" style="color: rebeccapurple;"></p></h1>
<button class="send-button">send message</button>

<!-- 这是socket.io基于express暴露的js文件 -->
<script src="/socket.io/socket.io.js"></script>
<script>
  const contentBox = document.querySelector('.content-box');
  const sendButton = document.querySelector('.send-button');
  const valueProvider = document.querySelector('.value-provider');
  // 建立socket连接
  const socket = io('ws://localhost:3000');

  socket.on('connect', function() {
    console.log('connect to server.');
		// 监听broadcast-channel事件, 接收服务端发来的数据
    this.on('broadcast-channel', data => contentBox.textContent = data);
		// emit往服务端发送数据
    sendButton.addEventListener('click', () => socket.emit('broadcast-channel', valueProvider.value));
  });
</script>
<!-- page2.html -->
<h1>page2</h1>
<input type="text" class="value-provider">
<h1>received ↓: <p class="content-box" style="color: rebeccapurple;"></p></h1>
<button class="send-button">send message</button>

<script src="/socket.io/socket.io.js"></script>
<script>
  const contentBox = document.querySelector('.content-box');
  const sendButton = document.querySelector('.send-button');
  const valueProvider = document.querySelector('.value-provider');
  const socket = io('ws://localhost:3000');

  socket.on('connect', function() {
    console.log('connect to server.');

    this.on('broadcast-channel', data => contentBox.textContent = data);

    sendButton.addEventListener('click', () => this.emit('broadcast-channel', valueProvider.value));
  });
</script>

好了, 本文的主要内容是跨页面通信, websocket的知识点非常多, 这里就不展开聊了, 后面有空在单独的写写对websocket的理解, websocket的应用很广, 比如实时弹幕, 即时通讯聊天等等,都可以用websocket来实现.

6. Server-sent events(SSE)

日常进行前后端数据交互过程中, 大部分情况都是客户端主动的对服务端发起请求, 服务端才会返回数据, 而服务端随时随地主动的往客户端推送消息的技术, 在web里除了websocket之外, 这里提到的Server-Sent Events(SSE)也是, 而且和websocket不同的事

websocket只是连接和断开是时, 走http进行握手和挥手

Server-Sent Events是完全走的http, 但是严格来说http是不支持服务器主动推送数据的, 但是Server-Sent Events算是一种变通的做法, 在客户端发起请求后, 服务端返回的信息告诉客户端接下来的是流信息(Stream), 客户端收到就不关闭连接, 一直在等服务器的下一次流信息.

好了知道大致流程我们还是看看demo,

// index.js
const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, './public')));

let clients = {};
// 通过/register进行注册
app.get('/register', (req, res) => {
  const {id = 'un'} = req.query;
  clients[id] || (clients[id] = res);
  // SSE必须返回text/event-stream的content-type
  res.setHeader('Content-Type', 'text/event-stream');
  // 返回的数据格式, data开头
  res.write('data: register success!\n\n');
  req.on('aborted', () => clients = []);
});
// 需要通知的信息
app.get('/notice', (req, res) => {
  const {message} = req.query;
  // 通知register的客户端
  for (let ki in clients) {
    clients[ki].write(`data: ${message}\n\n`);
  }
  res.status(200).end();
})

app.listen(9999, console.log('http://localhost:9999'));
<!-- register.html -->
<h1>sse demo</h1>
<h2></h2>
<script>
  const es = new EventSource('/register?id=one', { withCredentials: true });
  const messageBox = document.querySelector('h2');
  es.onmessage = e => {
    // register后,监听sse发来的数据
    console.log('onmessage: ', e);
    messageBox.textContent = `message: ${e.data}`;
  }
  es.addEventListener('close', () => {
    es.close()
  }, false)
</script>
<!-- notice.html -->
<script src="./axios.min.js"></script>
<script>
  const sendButton = document.querySelector('.sendButton');
  const input = document.querySelector('.inputBox');
  const sendMessage = async (message) => {
    const res = await axios.get('/notice', {
      params: {
        message,
      }
    });
    console.log('res: ', res);
  }

  sendButton.addEventListener('click', () => {
    console.time('send');
    sendMessage(input.value || 'this is message from page2.html');
    console.timeEnd('send');
  })

</script>

好了SSE这东西是HTML5规范的一部分, 在IE系列老旧的浏览器中是不支持的, 使用的时候得要根据业务场景具体分析哦.

7. ServiceWorker

Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

依赖

Service Worker 作为现代浏览器的高级特性,依赖于 fetch 、promise 、CacheStorage、Cache API、等浏览器的基础能力, Cache 提供了 Request / Response 对象对的存储机制。CacheStorage 则提供了存储 Cache 对象的机制。

功能和特性:

  • 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
  • 一旦被 install,就永远存在,除非被手动 unregister
  • 用到的时候可以直接唤醒,不用的时候自动睡眠
  • 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
  • 离线内容开发者可控
  • 能向客户端推送消息
  • 不能直接操作 DOM
  • 必须在 HTTPS 环境下才能工作
  • 异步实现,内部大都是通过 Promise 实现

service worker是pwa内容中的一部分, 这里主要讨论如何跨页面通信, 关于service worker的内容后面写pwa相关的知识点再详谈, 现在我们看看用service worker通信的小demo

<!-- page1.html -->
<h1>page1</h1> 
<button>send</button>

<script>
  // 注册service
  navigator.serviceWorker.register('sw.js').then(() => {
    console.log('register success!');
  });
  document.querySelector('button').addEventListener('click', () => {
    // 通过controller往worker发送信息
    navigator.serviceWorker.controller.postMessage('hello');
  });
</script>
<!-- page2.html -->
<h1>page2</h1>
  
<script>
  navigator.serviceWorker.register('sw.js').then(() => {
    console.log('register success!');
  });
  navigator.serviceWorker.addEventListener('message', ({data}) => {
    console.log('message: ', data);
  });
</script>
// sw.js
self.addEventListener('message', async event => {
  // 获取所有注册sw.js的clients
  const clients = await self.clients.matchAll();
  clients.forEach(client => client.postMessage('sw message: xx'));
})

除了上面的方式外, 还有一种一般骚的方法, 在service worker里跨页面通信--MessageChannel

这是一个平常使用频率不是很高的API, 实例化对象有两个通道, port1port2, 每个通道都可以监听message事件, 都可以通过postMessage发送消息.

port1发送的消息可以在port2进行message监听

port2发送的消息可以在port1进行message监听

在service worker里借助MessageChannel可以这样用

const mc = new MessageChannel();
// 监听消息
mc.port1.onmessage = function(e) {
  console.log(e.data);
}
navigator.serviceWorker.controller.postMessage('hello', [mc.port2]);
self.addEventListener('message', e => {
  // 通过e.ports获取mc.part2通道,发送信息
  e.ports[0].postMessage('xx');
});

以上就是基于service worker进行跨页面通信的方法, 但是值得注意的一点是, service woker进行跨页面通信是不支持跨域的.

8. MQTT消息队列

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,是 TCP/IP 的再封装,由 IBM 在 1999 年发布.MQTT 最大优点在于低开销少流量的实现网络通信, 物联网上MQTT协议用的就比较多, 是作为一名有追求的工程师, 我们得学.

这里就不自己搭建broker了, 直接使用在线的免费的消息队列看看吧, 有兴趣的可以自己用

activemq, rocketmq...这些消息队列去搭建自己的broker, 废话少说看demo

image-20210114164109532

MQTT协议中有以下几个主要的概念需要先了解:

  1. broker 就是消息队列代理服务器
  2. topic 主题, subscribe和publish时指定
  3. subscribe 进行主题的订阅
  4. publish 进行主题内容的发布

首先我们下载mqtt.js这个包

npm install mqtt
# 或者使用yarn进行安装
yarn add mqtt

然后就是巴拉巴拉的简单demo代码了

<!-- page1.html -->
<h1>page1</h1>
<input type="text" class="value-provider">
  <h1>content: <p class="content"></p></h1>
    <button class="send-button">send</button>
<script src="../node_modules/mqtt/dist/mqtt.js"></script>
<script>
const $ = el => document.querySelector(el);

const publishTopic = 'from-page1-message';
const subscribeTopic = 'from-page2-message';
// broker地址, 在浏览器里mqtt目前只能通过websocket进行连接了
const brokerAddress = 'ws://broker.emqx.io:8083/mqtt';
// 建立brocker连接 
const client = mqtt.connect(brokerAddress);

client.on('connect', () => {
  console.log('connected to broker!');
  // 订阅主题, 通过publish可以发布主题
  client.subscribe(subscribeTopic, (err) => {
    if (!err) {
      console.log('successfully connected to mqtt server');
      $('.send-button').addEventListener('click', () => client.publish(publishTopic, $('.value-provider').value));
    }
  });
});
// 监听到message的前提是, 已经订阅了主题
client.on('message', (topic, message, packet) => {
  console.log('received message: ', message.toString(), packet);
  $('.content').textContent = message.toString();
});
</script>
<!-- page2.html -->
<h1>page2</h1>
<input type="text" class="value-provider">
<h1>content: <p class="content"></p></h1>
<button class="send-button">send</button>
<script src="../node_modules/mqtt/dist/mqtt.js"></script>
<script>
  const $ = el => document.querySelector(el);

  const publishTopic = 'from-page2-message';
  const subscribeTopic = 'from-page1-message';
  const brokerAddress = 'ws://broker.emqx.io:8083/mqtt';
  const client = mqtt.connect(brokerAddress);

  client.on('connect', () => {
    console.log('connected to broker!');
    client.subscribe(subscribeTopic, (err) => {
      if (!err) {
        console.log('successfully connected to mqtt server');
        $('.send-button').addEventListener('click', () => client.publish(publishTopic, $('.value-provider').value));
      }
    });
  });

  client.on('message', (topic, message, packet) => {
    console.log('received message: ', message.toString(), packet);
    $('.content').textContent = message.toString();
  });
</script>

这里还可以推荐一款美观又好友的MQTT客户端软件, MQTTX

EMQ官网 www.emqx.io/cn/mqtt/pub…

9. IndexedDB

什么是IndexedDB? 看看的两个大写字母, DB耶, 就是DataBase, 网页自己的数据库, 它能用来干什么呢? 小傻瓜, 数据库当然是用来存储数据的啦, 你可能会说localstorage不是也可以用来做数据存储么? 还好这个IndexedDB干嘛, 因为localstorage对存储的数据量是有限制的, 几M到几十M之间, 不同浏览器的限制不一样, 但是IndexedDB的话就大了, 它的数据存储量差不多由你的硬盘大小来决定, IndexedDB 是一种底层 API, 用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs)), 该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。

indexedDB基本使用步骤

  1. 打开数据库
  2. 在数据库中创建一个对象仓库(object store)
  3. 启动一个事务,并发送一个请求来执行一些数据库操作,像增加或提取数据等
  4. 通过监听正确类型的 DOM 事件以等待操作完成
  5. 在操作结果上进行一些操作(可以在 request 对象中找到)

虽然这个方案可以实现跨页面通信, 但其实有点鸡肋的, indexDB的主要用途并不是在这里,

如果使用indexedDB实现跨页面通信呢, 大致思路就是一个同源的页面写入数据库, 另外一个同源的页面进行轮训读取indexedDB中的数据, 实现跨页面数据通信, 以及后面要说的cookie实现跨页面通信原理也是一样, 轮训, 又low又低效.

看是indexedDB还是比较牛的一个东西, 学些无妨, 看看具体怎么用吧

<!-- demo.html -->
<script>
  const dbName = 'demo_db';
  // 打开数据库, 没有就自动新建, 版本指定为2, 不指定为默认值1, 不要填浮点数
  const request = indexedDB.open(dbName, 2);
  // 监听事件
  request.onerror = e => console.error('error: ', e);
  request.onsuccess = e => console.log('success: ', e);
  request.onupgradeneeded = e => {
    console.log('upgradeneeded: ', e);
    // 建立连接后获取 IDBOpenDBRequest 对象
    const db = e.target.result;
    // 创建objectStore存储空间
    const objectStore = db.createObjectStore('firstCollection', {keyPath: 'id', autoIncrement: true});
    /**
       * 创建索引
       * createIndex能够给当前的存储空间设置一个索引。它接受三个参数:
       * 第一个参数,索引的名称。
       * 第二个参数,指定根据存储数据的哪一个属性来构建索引。
       * 第三个属性, options对象,其中属性unique的值为true表示不允许索引值相等。
       */
    objectStore.createIndex('age', 'age', {unique: false});
    objectStore.createIndex('name', 'name', {unique: true});
    /**
       * 创建数据库事务
       * 事务支持三种模式:
       * readOnly,只读。
       * readwrite,读写。
       * versionchange,数据库版本变化。
       */
    objectStore.transaction.oncomplete = e => {
      console.log('transaction complete: ', e);

      const collectionTransaction = db.transaction(['firstCollection'], 'readwrite').objectStore('firstCollection');
      // 新增数据
      const awaitInsertData = [
        {name: 'gzh', age: 18},
        {name: 'xx', age: 19},
        {name: 'hello', age: 3},
      ]
      awaitInsertData.forEach(data => collectionTransaction.add(data));
      // 查询数据(通过组件查询数据)
      const queryByKey = collectionTransaction.get(2);
      queryByKey.onsuccess = e => {
        const queryResult = e.target.result;
        console.log('queryResult(key): ', queryResult);
      }
      // 查询数据(通过索引查询数据)
      const index = collectionTransaction.index('name');
      console.log('index: ', index);
      const queryByIndex = index.get('gzh');
      queryByIndex.onsuccess = e => {
        const queryResult = e.target.result;
        console.log('queryResult(index): ', queryResult);
        // 修改数据
        queryResult.age = 19;
        collectionTransaction.put(queryResult);
        // 删除数据
        collectionTransaction.delete(3);
      }
    }
  }
</script>

demo代码都有点长了, 但是也合理, 毕竟indexedDB-----前端自己的数据库, 数据库这么强大的功能, 东西多点也正常.

10. Cookie & cookieStore

前面输过cookie和indexedDB一样跨页面通讯的实现是基于轮训实现的, 大多数情况比较鸡肋, 所以就不说了, 而且cookie在浏览器里的操作也是比较的怪异, 长久以来深受开发者们的鄙视和吐槽, 可是最近我关注到, chrome推出了一个叫cookieStore的API来解决这个问题, 而且cookieStore在cookie改变时change是跨页面个监听的, 这里也提一下, cookieStore也挺简洁易学的. 就是这个兼容性目前太差了, 但是相信在chrome的牵头下, 以后一定会火的, 成为新的cookie管理api指日可待.

注意点:

需要在https下才能使用

image-20210107150322507

talk is cheap, 看看我写的小demo

<!-- page1.html -->
<h1>page1</h1>
<button class="btn">set cookie</button>
<script>
  const btn = document.querySelector('.btn');
  cookieStore.addEventListener('change', e => {
    console.log("page1: ", e);
  });
  btn.addEventListener('click', function() {
    cookieStore.set({name: 'xx', value: 'xx'});
  });
</script>
<!-- page2.html -->
<h1>page2</h1> 
<script>
  cookieStore.addEventListener('change', e => {
    console.log("page2: ", e);
  });
</script>

11. 总结

这篇字数比较多, 写的时间的挺久, 但是写的过程中学到了很多东西, 并且对很多旧的知识也加深了记忆和理解, 以后有空会多写技术文章, 在输出中总结和提升, 希望在技术上能走得更高更远.❤️第一次在掘金写发文章有点紧张!, 还有那个有兴趣一起学习可以关注我微信公众哈!