sourcemap

sourcemap介绍

什么是sourceMap

  • sourcemap是为了解决开发代码与实际运行代码不一致时帮助我们debug到原始开发代码的技术
  • webpack通过配置可以自动给我们source maps文件,map文件是一种对应编译文件和源文件的方法
类型含义
source-map原始代码 最好的sourcemap质量有完整的结果,但是会很慢
eval-source-map原始代码 同样道理,但是最高的质量和最低的性能(不生成单独的map文件)
cheap-module-eval-source-map原始代码(只有行内) 同样道理,但是更高的质量和更低的性能
cheap-eval-source-map转换代码(行内) 每个模块被eval执行,并且sourcemap作为eval的一个dataurl
eval生成代码 每个模块都被eval执行,并且存在@sourceURL,带eval的构建模式能cache SourceMap
cheap-source-map转换代码(行内) 生成的sourcemap没有列映射,从loaders生成的sourcemap没有被使用
cheap-module-source-map原始代码(只有行内) 与上面一样除了每行特点的从loader中进行映射
hidden-source-map隐藏sourcemap
nosources-source-map控制台能正确提示报错的位置而不暴露源码

配置项

  • 配置项其实只是五个关键字eval、source-map、cheap、module和inline的组合
关键字含义
source-map产生.map文件
eval使用eval包裹模块代码
cheap不包含列信息(关于列信息的解释下面会有详细介绍)也不包含loader的sourcemap
module包含loader的sourcemap(比如jsx to js ,babel的sourcemap),否则无法定义源文件
inline将.map作为DataURI嵌入,不单独生成.map文件

source-map

src\index.js

let a=1;
let b=2;
let c=3;

dist\main.js

   ({"./src/index.js":(function (module, exports) {let a = 1;let b = 2;let c = 3;})});
//# sourceMappingURL=main.js.map

eval

  • eval执行代码
  • whyeval
  ({"./src/index.js":(function (module, exports) {eval("let a=1;\r\nlet b=2;\r\nlet c=3;\n\n//# sourceURL=webpack:///./src/index.js?");})});
  • eval-source-map就会带上源码的sourceMap
  • 加了eval的配置生成的sourceMap会作为DataURI嵌入,不单独生成.map文件
  • 官方比较推荐开发场景下使用eval的构建模式,因为它能cache sourceMap,从而rebuild的速度会比较快
  ({"./src/index.js":(function (module, exports) {eval("let a=1;\r\nlet b=2;\r\nlet c=3;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,In0=\n//# sourceURL=webpack-internal:///./src/index.js\n");})});

devtool: “eval-source-map” is really as good as devtool: “source-map”, but can cache SourceMaps for modules. It’s much faster for rebuilds.

inline

  • inline就是将map作为DataURI嵌入,不单独生成.map文件
  • inline-source-map
({"./src/index.js":(function (module, exports) {let a = 1;let b = 2;let c = 3;})
});
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIj

cheap(低开销)

  • cheap(低开销)的sourcemap,因为它没有生成列映射(column mapping),只是映射行数
  • 开发时我们有行映射也够用了,开发时可以使用cheap
  • cheap-source-map

module

  • Webpack会利用loader将所有非js模块转化为webpack可处理的js模块,而增加上面的cheap配置后也不会有loader模块之间对应的sourceMap
  • 什么是模块之间的sourceMap呢?比如jsx文件会经历loader处理成js文件再混淆压缩, 如果没有loader之间的sourceMap,那么在debug的时候定义到上图中的压缩前的js处,而不能追踪到jsx中
  • 所以为了映射到loader处理前的代码,我们一般也会加上module配置
  • cheap-module-source-map

sourcemap生成及原理

  • compiler官方下载
  • base64vlq在线转换

生成sourcemap

script.js

let a=1;
let b=2;
let c=3;
java -jar compiler.jar --js script.js --create_source_map ./script-min.js.map --source_map_format=V3 --js_output_file script-min.js

script-min.js

var a=1,b=2,c=3;

script-min.js.map

{
"version":3,
"file":"script-min.js",
"lineCount":1,
"mappings":"AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE;",
"sources":["script.js"],
"names":["a","b","c"]
}
字段含义
version:SourceSource map的版本,目前为3
file:转换后的文件名。转换后的文件名
sourceRoot转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空
sources转换前的文件,该项是一个数组,表示可能存在多个文件合并
names转换前的所有变量名和属性名
mappings记录位置信息的字符串

mappings属性

  • 关键就是map文件的mappings属性。这是一个很长的字符串,它分成三层
对应含义
第一层是行对应以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
第二层是位置对应以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
第三层是位置转换以VLQ编码表示,代表该位置对应的转换前的源码位置。
"mappings":"AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE;",

位置对应的原理

  • 每个位置使用五位,表示五个字段
位置含义
第一位表示这个位置在(转换后的代码的)的第几列
第二位表示这个位置属于sources属性中的哪一个文件
第三位表示这个位置属于转换前代码的第几行
第四位表示这个位置属于转换前代码的第几列
第五位表示这个位置属于names属性中的哪一个变量

首先,所有的值都是以0作为基数的。其次,第五位不是必需的,如果该位置没有对应names属性中的变量,可以省略第五位,再次,每一位都采用VLQ编码表示;由于VLQ编码是变长的,所以每一位可以由多个字符构成

如果某个位置是AAAAA,由于A在VLQ编码中表示0,因此这个位置的五个位实际上都是0。它的意思是,该位置在转换后代码的第0列,对应sources属性中第0个文件,属于转换前代码的第0行第0列,对应names属性中的第0个变量。

相对位置

  • 对于输出后的位置来说,到后边会发现它的列号特别大,为了避免这个问题,采用相对位置进行描述
  • 第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少

VLQ编码

  • VLQ是Variable-length quantity 的缩写,是一种通用的、使用任意位数的二进制来表示一个任意大的数字的一种编码方式
  • 这种编码需要用最高位表示连续性,如果是1,代表这组字节后面的一组字节也属于同一个数;如果是0,表示该数值到这就结束了
  • 如何对数值137进行VLQ编码
    • 将137改写成二进制形式 10001001
    • 七位一组做分组,不足的补0 0000001 0001001
    • 最后一组开头补0,其余补1 10000001 00001001
    • 137的VLQ编码形式为10000001 00001001
let binary = 137..toString(2);
console.log(binary);//10001001
let padded = binary.padStart(Math.ceil(binary.length / 7) * 7, '0');
console.log(padded);//00000010001001
let groups = padded.match(/\d{7}/g);
groups = groups.map((group,index)=>(index==0?'1':'0')+group);
console.log(groups);// ['10000001','00001001']

Base64 VLQ

  • 一个Base64字符只能表示6bit(2^6)的数据
  • Base64 VLQ需要能够表示负数,于是用最后一位来作为符号标志位
  • 由于只能用6位进行存储,而第一位表示是否连续的标志,最后一位表示正数/负数。中间只有4位,因此一个单元表示的范围为[-15,15],如果超过了就要用连续标识位了
  • 表示正负的方式
    • 如果这组数是某个数值的VLQ编码的第一组字节,那它的最后一位代表"符号",0为正,1为负;
    • 如果不是,这个位没有特殊含义,被算作数值的一部分
  • 在Base64 VLQ中,编码顺序是从低位到高位,而在VLQ中,编码顺序是从高位到低位
let base64 = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P','Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v','w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
];
/*** 1. 将137改写成二进制形式  10001001* 2. 127是正数,末位补0 100010010* 3. 五位一组做分组,不足的补0 01000 10010* 4. 将组倒序排序 10010 01000* 5. 最后一组开头补0,其余补1 110010 001000* 6. 转64进制 y和I*/
function encode(num) {//1. 将137改写成二进制形式,如果是负数的话是绝对值转二进制let binary = (Math.abs(num)).toString(2);//2.正数最后边补0,负数最右边补1,127是正数,末位补0 100010010binary = num >= 0 ? binary + '0' : binary + '1';//3.五位一组做分组,不足的补0   01000 10010 let zero = 5 - (binary.length % 5);if (zero > 0) {binary = binary.padStart(Math.ceil(binary.length / 5) * 5, '0');}let parts = [];for (let i = 0; i < binary.length; i += 5) {parts.push(binary.slice(i, i + 5));}// 01000 10010//4. 将组倒序排序 10010 01000parts.reverse();// ['00000','00001']//5. 最后一组开头补0,其余补1 110010 001000for (let i = 0; i < parts.length; i++) {if (i === parts.length - 1) {parts[i] = '0' + parts[i];} else {parts[i] = '1' + parts[i];}}//6.转64进制 y和Ilet chars = [];for (let i = 0; i < parts.length; i++) {chars.push(base64[parseInt(parts[i], 2)]);}return chars.join('')
}
let result = encode(137);
console.log(result);

计算位移

let base64 = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P','Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v','w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
];
function getValue(char) {let index = base64.findIndex(item => item == char);//先找这个字符的索引let str = (index).toString(2);//索引转成2进制str = str.padStart(6, '0');//在前面补0补到6位//最后一位是符号位,正数最后一位是0,负数最后一位为1let sign = str.slice(-1)=='0'?1:-1;//最后一组第一位为0,其它的第一位为1str = str.slice(1, -1);return parseInt(str, 2)*sign;
}
function decode(values) {let parts = values.split(',');//分开每一个位置let positions = [];for(let i=0;i<parts.length;i++){let part = parts[i];let chars = part.split('');//得到每一个字符let position = [];for (let i = 0; i < chars.length; i++) {position.push(getValue(chars[i]));//获取此编写对应的值}positions.push(position);}return positions;
}
let positions = decode('AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE');
//后列,哪个源文件,前行,前列,变量
console.log('positions',positions);
let offsets = positions.map(item=>[item[2],item[3],0,item[0],]);
console.log('offsets',offsets);
let origin = {x:0,y:0};
let target = {x:0,y:0};
let mapping=[];
for(let i=0;i<offsets.length;i++){let [originX,originY,targetX,targetY] = offsets[i];origin.x += originX;origin.y += originY;target.x += targetX;target.y += targetY;mapping.push(`[${origin.x},${origin.y}]=>[${target.x},${target.y}]`);
}
console.log('mapping',mapping);
positions [[ 0, 0, 0, 0 ],[ 4, 0, 0, 4, 0 ],[ 2, 0, 0, 2 ],[ 1, 0, 0, -6 ],[ 1, 0, 1, 4, 1 ],[ 2, 0, 0, 2 ],[ 1, 0, -1, -6 ],[ 1, 0, 2, 4, 1 ],[ 2, 0, 0, 2 ]
]
offsets [[ 0, 0, 0, 0 ],[ 0, 4, 0, 4 ],[ 0, 2, 0, 2 ],[ 0, -6, 0, 1 ],[ 1, 4, 0, 1 ],[ 0, 2, 0, 2 ],[ -1, -6, 0, 1 ],[ 2, 4, 0, 1 ],[ 0, 2, 0, 2 ]
]
mapping ['[0,0]=>[0,0]','[0,4]=>[0,4]','[0,6]=>[0,6]','[0,0]=>[0,7]','[1,4]=>[0,8]','[1,6]=>[0,10]','[0,0]=>[0,11]','[2,4]=>[0,12]','[2,6]=>[0,14]'
]

调试代码

测试环境调试

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const webpack = require('webpack');
module.exports = {mode: 'production',devtool: false,entry: './src/index.js',resolveLoader:{modules:['node_modules','loaders']},module: {rules: [{test: /\.js$/,use: [{loader: 'babel-loader',options: {presets: ["@babel/preset-env"]}}]},{test: /\.scss$/,use: [{ loader: 'style-loader' },{loader: 'css-loader',options: { sourceMap: true,importLoaders:2}},//{ loader: "resolve-url-loader" },{ loader: "resolve-scss-url-loader" },{loader: 'sass-loader',options: { sourceMap: true }}]},{test: /\.(jpg|png|gif|bmp)$/,use: [{ loader: 'url-loader' }]}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'}),new webpack.SourceMapDevToolPlugin({append: '//# sourceMappingURL=http://127.0.0.1:8081/[url]',filename: '[file].map',}),new FileManagerPlugin({onEnd: {copy: [{source: './dist/*.map',destination: 'C:/aprepare/hssourcemap/sourcemap',}],delete: ['./dist/*.map'],archive: [{ source: './dist',destination: './dist/dist.zip',}]}})]
}

生产环境调试

  • webpack打包仍然生成sourceMap,但是将map文件挑出放到本地服务器,将不含有map文件的部署到服务器,借助第三方软件(例如fiddler),将浏览器对map文件的请求拦截到本地服务器,就可以实现本地sourceMap调试
regex:(?inx)http:\/\/localhost:8080\/(?<name>.+)$
*redir:http://127.0.0.1:8081/${name}

source-map-loader

  • source-map-loader从当前存在的源码(从sourceMappingURL)中提供出map源码
cnpm i source-map-loader -D

script.js

let a=1;
let b=2;
let c=3;
java -jar compiler.jar --js script.js --create_source_map ./script-min.js.map --source_map_format=V3 --js_output_file script-min.js

script.min.js

var a=1,b=2,c=3;
//# sourceMappingURL=script-min.js.map

script.min.map.js

{
"version":3,
"file":"script-min.js",
"lineCount":1,
"mappings":"AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE;",
"sources":["script.js"],
"names":["a","b","c"]
}

src\index.js

import './script-min.js';

webpack.config.js

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const webpack = require('webpack');
module.exports = {mode: 'development',devtool: 'inline-source-map',entry: './src/index.js',resolveLoader:{modules:['node_modules','loaders']},module: {rules: [
+      {
+        test: /\.js$/,
+        use: ["source-map-loader"],
+        enforce: "pre"
+      },{test: /\.js$/,use: [{loader: 'babel-loader',options: {presets: ["@babel/preset-env"]}}]},{test: /\.scss$/,use: [{ loader: 'style-loader' },{loader: 'css-loader',options: { sourceMap: true,importLoaders:2}},//{ loader: "resolve-url-loader" },
+          { loader: "resolve-scss-url-loader" },{loader: 'sass-loader',options: { sourceMap: true }}]},{test: /\.(jpg|png|gif|bmp)$/,use: [{ loader: 'url-loader' }]}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'})]
}

参考

  • javascript_source_map算法
  • devtool

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/26329.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

element table 点击某一行中按钮加载

在Element UI中&#xff0c;实现表格&#xff08;element-table&#xff09;中的这种功能通常涉及到数据处理和状态管理。当你点击某一行的按钮时&#xff0c;其他行的按钮需要动态地切换为加载状态&#xff0c;这可以通过以下步骤实现&#xff1a; 1.表格组件&#xff1a;使用…

一文读懂Web Codecs API:浏览器背后的媒体魔术师

引言 ​在早期的Web 网页中&#xff0c;视频播放通常要依靠 Flash 和 Silverlight 等插件来完成&#xff0c;浏览器是不支持直接播放视频的。 随着网络技术的发展&#xff0c;视频这种媒体方式的需求变得普遍&#xff0c;HTML5中&#xff0c;出现了一个新的元素Video&#xf…

【全开源】旅行吧旅游门票预订系统源码(FastAdmin+ThinkPHP+Uniapp)

&#x1f30d;旅游门票预订系统&#xff1a;畅游世界&#xff0c;一键预订 一款基于FastAdminThinkPHPUniapp开发的旅游门票预订系统&#xff0c;支持景点门票、导游产品便捷预订、美食打卡、景点分享、旅游笔记分享等综合系统&#xff0c;提供前后台无加密源码&#xff0c;支…

RabbitMQ延迟消息(通过死信交换机实现)

延迟消息&#xff1a;生产者发送消息时指定一个时间&#xff0c;消费者不会立刻收到消息&#xff0c;而是在指定时间后才收到消息 通过DLX和TTL模拟出延迟队列的功能&#xff0c;即&#xff0c;消息发送以后&#xff0c;不让消费者拿到&#xff0c;而是等待过期时间&#xff0…

山东大学软件学院多核平台上的并行计算期末回忆版

&#xff08;2021级&#xff0c;大数据专业&#xff0c;老师是lwg和yzk&#xff0c;考题全是考前老师说的原题&#xff0c;毫无变化&#xff0c;最终期末分还是看实验情况多一些&#xff0c;但是老师到底是怎么比较的大家的实验性能&#xff0c;让我很头大&#xff0c;晕~&…

linux驱动学习(十三)之锁

需要板子一起学习的可以这里购买&#xff08;含资料&#xff09;&#xff1a;点击跳转 一、锁的作用 1、同步和互斥 1)同步:同一件事情的依次处理&#xff0c;数据的接收---> 数据的处理 --->数据的发送 2)互斥---- 防止对临界资源的竞争&#xff0c;在一个时刻&#…

18. SDP协议

SDP协议描述 SDP(Session Description Protocol)它只是一种信息格式的描述标准&#xff0c;本身不属于传输协议&#xff0c;但是可以被其他传输协议用来交换必要的信息。 SDP规范 多个媒体级描述 一个会话级描述 由多个<type><value>组成 会话层 会话的名称与目…

如何选择加噪使用的噪声尺度:超参数(alpha,beta)噪声尺度的设定

如何选择加噪使用的噪声尺度&#xff1a;超参数&#xff08;alpha&#xff0c;beta&#xff09;噪声尺度的设定【论文精读】 论文&#xff1a;Score-Based Generative Modeling through Stochastic Differential Equations 地址&#xff1a;https://doi.org/10.48550/arXiv.201…

开发移动端常见的问题:VW适配问题,基于 postcss 插件 实现项目vw适配

当你开发移动端的时候有一个问题是避免不了的&#xff0c;那就是当屏幕大小无论怎么变化时&#xff0c;内部尺寸也要随之发生改变&#xff0c;也就是适配问题。这里我们讲的是最新的VW适配&#xff0c;也就是用vw作为单位&#xff0c;100vw是整个页面的大小。而在开发的设计图中…

23.Dropout

在深度学习的训练过程中&#xff0c;过拟合是一个常见的问题。为了解决这个问题&#xff0c;研究者们提出了多种正则化技术&#xff0c;其中Dropout技术因其简单而有效的特点&#xff0c;得到了广泛的应用。本文将对Dropout技术的工作原理、主要优点、潜在缺点以及应用场景进行…

特种设备气瓶充装作业题库分享

1、【单选题】气瓶颜色标志是气瓶外表面涂敷的字样内容、色环数目和( )按充装气体的特性作规定的组合。(B) A、颜色 B、涂膜颜色 C、漆色 2、【多选题】( )和( )有权对于违反《中华人民共和国特种设备安全法》规定的&#xff0c;向负责特种设备安全监督管理的部门和有…

【Flask 系统教程 7】数据库使用 SQLAlchemy

SQLAlchemy 是一个功能强大的 SQL 工具包和对象关系映射 (ORM) 库。使用 Flask 和 SQLAlchemy 可以方便地连接和操作数据库。 SQLAlchemy基本操作 连接数据库 环境准备 首先&#xff0c;确保你已经安装了SQLAlchemy 。可以使用以下命令安装这些库&#xff1a; pip install…

快来!AI绘画Stable Diffusion 3终于开源了,更强的文字渲染和理解力,12G显卡可跑!

大家好&#xff0c;我是设计师阿威 Stable Diffusion 3终于开源了&#xff0c;2B参数的Stable Diffusion 3 Medium模型已经可以在HuggingFace上下载了&#xff01;如无法科学上网的小伙伴我也准备好了网盘资料&#xff0c;请看文末扫描获取哦&#xff01; Stable Diffusion 3 …

PostgreSQL 多表连接不同维度聚合统计查询

摘要:在本文中,你将学习到如何使用 PostgreSQL 完全外连接,从两个或多个表中聚合维度统计数据。 文章目录 一、常用的连接类型图示二、数据库表设计示例三、连接查询示例1. inner join 内连接(不能满足维度统计需求)2. full join 完全外连接(满足维度统计需求)一、常用的…

Vue3【二十】Vue3 路由和组件页面切换

Vue3【二十】Vue3 路由和组件页面切换 Vue3【二十】Vue3 路由和组件页面切换 Vue3 路由的创建 路由的引入 路由的配置 路由的导出 路由的url模式 带# 或不带 案例截图 目录结构 案例代码 app.vue <template><div class"app"><h2 class"title&q…

CPN Tools实现hello world小案例

新建一个net&#xff0c;创建两个输入P1,P2&#xff0c;一个输出P3&#xff0c;一个转换T1&#xff0c;并对输入输出place使用字符串颜色集。&#xff08;这里是左键单击P&#xff0c;然后tab键输入String即可&#xff09;。 为地点指定颜色集需要&#xff1a; 1) 通过左键单击…

定义input_password函数,提示用户输入密码.如果用户输入长度<8,抛出异常,如果用户输入长度>=8,返回输入的密码

def input_password(password):str1passwordlen1len(str1)try:if len1<8:raise ValueError("密码长度不能小于8")else:return print(f"你的密码为:{password},请确认")except ValueError as e:print(f":Error is {e}")number1input("请…

【详解Python文件: .py、.ipynb、.pyi、.pyc、​.pyd !】

今天同事给我扔了一个.pyd文件&#xff0c;说让我跑个数据。然后我就傻了。。 不知道多少粉丝小伙伴会run .pyd代码文件&#xff1f;如果你也懵懵的&#xff0c;请继续往下读吧。。 Python文件是存储Python代码或数据的文本文件&#xff0c;通常以.py作为文件扩展名。这些文件…

KIVY Canvas¶

Canvas Jump to API ⇓ Module: kivy.graphics.instructions Added in 1.0.0 The Canvas is the root object used for drawing by a Widget. Check the class documentation for more information about the usage of Canvas. 画布是一个 基类对象 被用来以一个组件的方式画…

3. 打造个性化可爱怪物表情包:详细步骤教学

表情符号已经成为当今互联网对话中不可或缺的元素&#xff0c;一句话加上一个笑脸符号&#xff0c;语气就大不同。表情符号与我们一道稳步发展&#xff0c;成为鲜活和丰富情感的必要交流工具。通过表情符号&#xff0c;几个像素就能以有趣、清晰、能引起情感共鸣的方式表达我们…