web前端之标签页相互通信、浏览器窗口的open方法的参数及防多开功能、本地存储之local与session和storage监听、动态自定义元素属性及选择器、伪类hover与active、样式混合器
智码帮MJ682517 2024-07-02 14:03:02 阅读 54
MENU
文章技能效果图公共代码和解析style-scssstyle-csshtmlwindow.open的解析localStorage和sessionStorage的监听区别和注意事项(坑)
openKeyOnly方式(壹)BroadcastChannel方式(贰)localStorage方式(叁)三种方式的区别彩蛋
文章技能
1、了解css混合器(mixin);
2、了解动态自定义元素(标签)属性,
el.setAttribute('data-activa', '');
,setAttribute
方法需要传递两个参数,第二个参数如果不传会报错;可以传空字符串,此时标签只显示属性,等号和属性值不会显示,标签结果<span data-activa></span>
;传递的参数值都会转成字符串,比如null
或undefined
都会转成'null'
或'undefined'
,标签结果<div data-activa="null"></div><p data-activa="undefined"></p>
;
3、了解自定义元素(标签)属性的选择器,
.item[data-activa]
|.item[data-activa='highlight ']
;
4、了解
window.open
第二个参数的作用,以及固有值和自定义值对页面交互效果的区别;
5、了解标签页之间传参的区别;
6、了解storage监听中localStorage和sessionStorage的区别;
7、了解浏览器防多开功能的新思路或方法;
8、了解hover和active伪类;
9、最终效果类似网页版的酷狗,第一次播放打开新的标签页,之后的播放只发送音乐数据到播放页,此方法适用于防多开的功能。
效果图
公共代码和解析
style-scss
@mixin bsbb() {
box-sizing: border-box;
}
html,
body {
margin: 0px;
padding: 0px;
@include bsbb();
}
body {
padding: 68px;
@include bsbb();
}
// 发送页
.box {
>.item {
cursor: pointer;
list-style-type: none;
padding: 8px 0px;
@include bsbb();
text-align: center;
font-size: 28px;
font-weight: bold;
background-color: #eeeeee;
border-radius: 4px;
}
>.item:not(:first-child) {
margin-top: 18px;
}
>.item:hover {
background-color: #f0f8ff;
}
>.item:active {
background-color: #00ffff;
}
>.item[data-activa] {
color: #00ff00;
background-color: #409eff;
}
}
// 接收页
#idEl {
height: calc(100vh - 136px);
line-height: calc(100vh - 136px);
margin: 0px;
padding: 0px;
text-align: center;
font-size: 68px;
font-weight: bold;
}
使用Scss(一种CSS预处理器)编写一些样式和一个名为
bsbb
的mixin
(混合器)。
1、
@mixin bsbb()
定义一个名为bsbb
的mixin,它设置box-sizing
属性为border-box
。box-sizing: border-box;
确保元素的宽度和高度包括内容、内边距和边框,而不是仅仅包括内容。
2、
html, body
选择器设置html
和body
元素的margin
和padding
为0px
,并调用bsbb
混合器。
3、
body
选择器设置body
元素的padding
为68px
,并再次调用bsbb
混合器。
4、
.box
选择器定义.box
类的样式,它包含一个.item
类的子元素。.box > .item
选择器设置.item
的样式,包括鼠标指针样式、无列表项标记、内边距、box-sizing
、文本居中、字体大小、字体粗细、背景颜色和边框圆角。
5、
.box > .item:not(:first-child)
选择器设置除了第一个.item
之外的所有.item
的margin-top
为18px
。
6、
.box > .item:hover
选择器定义鼠标悬停在.item
上时的背景颜色。
7、
.box > .item:active
选择器定义当.item
被激活(例如点击)时的背景颜色。
8、
.box > .item[data-activa]
选择器定义当.item
元素具有data-activa
属性时的文本颜色和背景颜色。
9、
#idEl
选择器定义具有id
为idEl
的元素样式,它设置高度和行高为视口高度减去136px
,margin
和padding
为0px
,文本居中,字体大小为68px
,字体粗细为粗体。
.box
类定义一个包含多个.item
的容器,每个.item
可以有不同的交互状态和样式。#idEl
定义一个特定的元素,其样式可能用于页面的特定部分,如发送页和接收页。
在实际使用中,这段代码需要被编译成普通的css文件,以便在网页中使用。Scss编译器会处理这些mixin和选择器,生成相应的CSS规则。
style-css
html,
body {
margin: 0px;
padding: 0px;
box-sizing: border-box;
}
body {
padding: 68px;
box-sizing: border-box;
}
.box>.item {
cursor: pointer;
list-style-type: none;
padding: 8px 0px;
box-sizing: border-box;
text-align: center;
font-size: 28px;
font-weight: bold;
background-color: #eeeeee;
border-radius: 4px;
}
.box>.item:not(:first-child) {
margin-top: 18px;
}
.box>.item:hover {
background-color: #f0f8ff;
}
.box>.item:active {
background-color: #00ffff;
}
.box>.item[data-activa] {
color: #00ff00;
background-color: #409eff;
}
#idEl {
height: calc(100vh - 136px);
line-height: calc(100vh - 136px);
margin: 0px;
padding: 0px;
text-align: center;
font-size: 68px;
font-weight: bold;
}
html
发送页(send)
<ul class="box">
<li class="item" onclick="handle(1)">1</li>
<li class="item" onclick="handle(2)">2</li>
<li class="item" onclick="handle(3)">3</li>
<li class="item" onclick="handle(4)">4</li>
<li class="item" onclick="handle(5)">5</li>
</ul>
接收页(receive)
<div id="idEl"></div>
window.open的解析
window.open
方法的第二个参数用于指定新窗口的名称,名称可以用来引用新窗口,例如在后续的代码中使用window.open
或window.close
方法来操作这个窗口。
第二个参数如果是
_blank
,则表示新窗口或标签页将在浏览器中打开,但不会与任何已存在的窗口或标签页关联。这通常用于打开一个全新的、独立的窗口或标签页。
第二个参数如果是
_self
,则表示新页面将在当前窗口或标签页中加载,这将替换当前页面的内容。
第二个参数如果是
_parent
,则表示新页面将在当前窗口或标签页的父窗口或标签页中加载。如果当前窗口或标签页没有父窗口或标签页(即它不是嵌套的),则效果与_self
相同。
第二个参数如果是
_top
,则表示新页面将在最顶层的窗口中加载,这将替换所有嵌套的窗口或标签页。
第二个参数如果是自定义(
keyOnly
),且不是一个标准的窗口名称,它不会影响新窗口的行为。如果自定义(keyOnly
)不是已存在的窗口名称,那么window.open
会创建一个新窗口,并将其命名为keyOnly
。如果keyOnly
已经是一个打开的窗口的名称,那么window.open
将不会创建新窗口,而是返回对已存在的窗口的引用。
在实际应用中,使用非标准的窗口名称(如
keyOnly
)可能不会达到预期的效果,因为浏览器可能不会识别这样的名称。通常建议使用_blank
、_self
、_parent
或_top
作为第二个参数,以确保与浏览器的兼容性。
localStorage和sessionStorage的监听区别和注意事项(坑)
localStorage和sessionStorage的区别
localStorage和sessionStorage都是Web Storage API提供的两种存储机制,用于在客户端存储数据。它们在存储方式、生命周期、作用域等方面有所不同。
1、生命周期
localStorage
,数据没有过期时间,除非被显式删除,否则数据会一直保存在浏览器中,即使关闭浏览器或重启计算机。
sessionStorage
,数据仅在当前浏览器会话中有效,一旦会话结束(例如,关闭浏览器标签页或窗口),数据就会被清除。
2、作用域
localStorage
和sessionStorage
都基于源(origin),这意味着它们只能访问相同协议、域名和端口的数据。不同源之间的数据存在隔离(跨域)。localStorage
的数据在所有同源的窗口和标签页中共享,而sessionStorage
的数据仅限于创建它的窗口或标签页。
3、存储大小
localStorage
和sessionStorage
的存储大小限制因浏览器而异,但通常localStorage
的存储空间比sessionStorage
大。在大多数现代浏览器中,localStorage
的大小限制大约为5MB到10MB。
4、API接口
两者都提供了相同的API方法,包括
setItem(key, value)
、getItem(key)
、removeItem(key)
、clear()
和key(index)
。
5、使用场景
localStorage
适合存储长期数据,如用户偏好设置、登录状态等。
sessionStorage
适合存储临时数据,如表单输入、购物车内容等,这些数据仅在当前会话中需要。
6、数据持久性
localStorage
提供持久化存储,即使关闭浏览器或重启计算机,数据仍然存在。
sessionStorage
提供临时存储,数据仅在当前会话期间有效。
7、事件监听
两者都支持
storage
事件,当数据发生变化时,所有同源的窗口都会接收到这个事件。但sessionStorage
事件只在当前会话的窗口中触发。在使用时,根据具体需求选择合适的存储机制。如果需要存储用户登录状态,localStorage
是一个很好的选择,因为它可以跨会话持久化数据。如果需要存储用户在单个页面会话中的临时数据,sessionStorage
可能更适合,因为它不会在会话结束后保留数据。
解决storage监听不被触发的问题
如果在两个页面中,只有使用
localStorage
时storage
事件才会被触发,而使用sessionStorage
时不会,这可能是因为storage
事件的触发机制和sessionStorage
的特性导致。
storage
事件被触发的条件
1、当
localStorage
或sessionStorage
中的任何数据被修改时(例如,通过setItem
、removeItem
或clear
方法)。
2、事件会在所有同源的页面中触发,无论这些页面是打开在同一个标签页还是不同的标签页。
sessionStorage
的特性
1、
sessionStorage
与特定的浏览器标签页或窗口关联。这意味着,如果在标签页A中修改sessionStorage
,只有标签页A会接收到storage
事件。其他标签页(即使它们是同一个源)不会接收到这个事件,因为它们的sessionStorage
独立。
2、当标签页关闭时,与该标签页关联的
sessionStorage
会被清除。
据上,如果在两个不同的标签页中分别修改
sessionStorage
,每个标签页只会接收到自己修改的事件,而不会接收到另一个标签页的事件。这就是为什么在两个页面中,只有使用localStorage
时storage
事件才会被触发的原因。
如果希望在两个标签页中都能监听到
sessionStorage
的变化,需要确保两个标签页都注册storage
事件监听器,并且它们都属于同一个源。这样,无论哪个标签页修改sessionStorage
,另一个标签页都能接收到storage
事件。
sessionStorage
的事件监听只在修改它的标签页中触发,而localStorage
的事件监听则在所有同源的标签页中触发。如果需要跨标签页监听sessionStorage
的变化,需要确保所有相关标签页都注册storage
事件监听器,并且它们都属于同一个源。
openKeyOnly方式(壹)
发送页(send)
function handle(val) {
window.open(`./receive.html?val=${ val}&type=keyOnly`, 'keyOnly');
}
接收页(receive)
const search = window.location.search;
const searchParams = new URLSearchParams(search);
const iterator = searchParams.keys();
const obj = { };
for (let item of iterator) obj[item] = searchParams.get(item);
idEl.textContent = obj.val;
解析
代码用于处理一个网页的URL查询参数,并将特定的参数值显示在页面上。
1、
function handle(val)
定义一个名为handle的函数,它接受一个参数val。
2、
window.open('./receive.html?val=${val}&type=keyOnly', 'keyOnly');
调用window.open
方法打开一个新的浏览器窗口或标签页。它将打开./receive.html
页面,并通过URL查询字符串传递两个参数val和type。val参数的值通过函数参数val赋值,type参数的值被硬编码为keyOnly。'keyOnly’是新窗口的名称。
3、
const search = window.location.search;
获取当前页面URL查询字符串部分,并将其赋值给变量search。如果当前URL是http://example.com/page.html?param1=value1¶m2=value2
,那么search的值将是?param1=value1¶m2=value2
。
4、
const searchParams = new URLSearchParams(search);
使用URLSearchParams
构造函数创建一个新的URLSearchParams
对象,该对象封装search字符串中的查询参数。URLSearchParams
对象提供一种方便的方式来处理URL的查询字符串。
5、
const iterator = searchParams.keys();
调用searchParams对象的keys()
方法,该方法返回一个迭代器,它包含查询字符串中的所有键(参数名)。
6、
const obj = {};
创建一个空对象obj,用于存储查询参数的键值对。
7、
for (let item of iterator) obj[item] = searchParams.get(item);
使用for...of
循环来遍历iterator
中的每个键(参数名)。对于每个键,它使用searchParams.get(item)
方法获取对应的值,并将这个键值对存储在obj对象中。
8、
idEl.textContent = obj.val;
将obj对象中val键对应的值赋给idEl元素的textContent
属性。idEl是一个页面上的DOM元素,其ID为idEl。假设val参数存在于URL查询字符串中,并且idEl元素存在。
总结来说,以上代码的目的是从当前页面的URL查询字符串中提取val参数的值,并将其显示在页面上。如果val参数不存在,
idEl.textContent
将不会被设置。此外,这段代码还定义一个handle
函数,用于打开一个新页面并传递参数。
BroadcastChannel方式(贰)
叙言
以下代码展示两个页面之间的通信机制,一个发送消息页面,一个接收消息页面。使用
BroadcastChannel
API来实现跨页面通信,以及localStorage
来存储和传递消息计数。
发送页(send)
const channel = new BroadcastChannel('keyOnly');
function handle(val) {
const n = +localStorage.getItem('keyOnly');
if (!isNaN(n) && n > 0) {
channel.postMessage({ val, type: 'keyOnly' });
} else {
window.open(`./receive.html?val=${ val}&type=keyOnly`, '_blank');
}
}
1、
const channel = new BroadcastChannel('keyOnly');
创建一个新的BroadcastChannel
对象,用于在同源的不同页面之间发送和接收消息。频道名称为keyOnly
。
2、
function handle(val) { ... }
定义一个handle
函数,它接收一个参数val,这个参数是需要发送的消息内容。
3、
const n = +localStorage.getItem('keyOnly');
从localStorage
中获取名为keyOnly
的项,并尝试将其转换为数字。+
操作符用于尝试将字符串转换为数字。
4、
if (!isNaN(n) && n > 0) { ... } else { ... }
如果localStorage
中的值存在,且为正数,则执行channel.postMessage
发送消息;否则,打开一个新的浏览器窗口或标签页,并加载./receive.html
页面。
5、
channel.postMessage({ val, type: 'keyOnly' });
如果条件满足,使用BroadcastChannel
发送一个包含val和type属性的对象。
接收页(receive)
const search = window.location.search;
const searchParams = new URLSearchParams(search);
const iterator = searchParams.keys();
const obj = { };
for (let item of iterator) obj[item] = searchParams.get(item);
idEl.textContent = obj.val;
let n = +localStorage.getItem('keyOnly');
const channel = new BroadcastChannel('keyOnly');
if (isNaN(n)) n = 0;
n += 1;
localStorage.setItem('keyOnly', n);
window.addEventListener('unload', () => {
let n = +localStorage.getItem('keyOnly');
if (isNaN(n)) n = 1;
n -= 1;
localStorage.setItem('keyOnly', n);
});
channel.addEventListener('message', ({ data: { val } }) => {
idEl.textContent = val;
});
1、
const search = window.location.search;
获取当前页面的查询字符串部分。
2、
const searchParams = new URLSearchParams(search);
使用URLSearchParams
解析查询字符串。
3、
const iterator = searchParams.keys();
获取查询参数的键的迭代器。
4、
const obj = {};
创建一个空对象obj用于存储查询参数。
5、
for (let item of iterator) obj[item] = searchParams.get(item);
遍历查询参数的键,并将它们及其对应的值存储在obj对象中。
6、
idEl.textContent = obj.val;
将obj对象中val键对应的值设置为idEl元素的文本内容。
7、
let n = +localStorage.getItem('keyOnly');
从localStorage
中获取名为keyOnly
的项,并尝试将其转换为数字。
8、
const channel = new BroadcastChannel('keyOnly');
创建一个新的BroadcastChannel
对象,用于接收消息。
9、
if (isNaN(n)) n = 0; n += 1; localStorage.setItem('keyOnly', n);
如果localStorage
中的值不存在或不是数字,则将其设置为1,并增加计数,用于计算当前打开的页面数。
10、
window.addEventListener('unload', () => { ... });
添加一个事件监听器,当页面卸载时(例如,用户关闭标签页或导航到另一个页面),减少计数并更新localStorage
。
11、
channel.addEventListener('message', ({ data: { val } }) => { ... });
添加一个事件监听器,当通过BroadcastChannel
接收到消息时,将消息内容设置为idEl元素的文本内容。
总结
发送页面通过
BroadcastChannel
发送消息,接收页面通过BroadcastChannel
接收消息。发送页面在发送消息前会检查localStorage
中的计数,如果计数为0,则通过打开新窗口的方式发送消息;否则,通过BroadcastChannel
发送消息。接收页面在接收到消息后,会更新页面上的idEl元素的文本内容。
此外,还跟踪接收页面打开的次数,当页面卸载时减少计数,并在每次打开新的接收页面时增加计数。这样可以确保发送页面在发送消息时,可以根据计数决定使用
BroadcastChannel
发送还是打开新窗口发送。
localStorage方式(叁)
叙言
以下代码展示两个页面之间的通信机制,一个页面发送消息,另一个页面接收消息。发送页面使用
localStorage
和window.open
来发送消息,接收页面使用localStorage
和window.addEventListener
来接收消息。
发送页(send)
const liEl = document.querySelectorAll('.item');
const val = JSON.parse(localStorage.getItem('pageItem'))?.val || 1;
handle(val);
function handle(val) {
const n = +localStorage.getItem('keyOnly');
const pageItem = { val, type: 'keyOnly' };
const setItem = (data) => localStorage.setItem('pageItem', JSON.stringify(data));
if (!isNaN(n) && n > 0) {
setItem(pageItem);
} else {
setItem(pageItem);
window.open('./receive.html', '_blank');
}
liEl.forEach(item => {
if (item.textContent == val) {
item.setAttribute('data-activa', '');
} else {
item.removeAttribute('data-activa');
}
});
}
window.addEventListener('storage', ({ key, storageArea, newValue }) => {
if (storageArea && key === 'keyOnly') {
if (newValue == 0) {
liEl.forEach(item => {
item.removeAttribute('data-activa');
});
localStorage.removeItem('pageItem');
localStorage.removeItem('keyOnly');
}
}
});
1、
const liEl = document.querySelectorAll('.item');
选择页面上所有类名为item的元素。
2、
const val = JSON.parse(localStorage.getItem('pageItem'))?.val || 1;
从localStorage
中获取名为pageItem的项,并尝试将其解析为JSON对象。如果解析失败或不存在,则val默认为1。
3、
handle(val);
调用handle函数并传入val作为参数。
4、
function handle(val) { ... }
定义handle函数,用于处理发送消息的逻辑。
5、
const n = +localStorage.getItem('keyOnly');
从localStorage
中获取名为keyOnly的项,并尝试将其转换为数字。
6、
const pageItem = { val, type: 'keyOnly' };
创建一个对象pageItem,包含val和type属性。
7、
const setItem = (data) => localStorage.setItem('pageItem', JSON.stringify(data));
定义一个函数setItem,用于将对象转换为JSON字符串并存储到localStorage
中。
8、
if (!isNaN(n) && n > 0) { ... } else { ... }
如果localStorage
中的keyOnly项存在且为正数,则调用setItem函数存储pageItem对象;否则,打开./receive.html
页面。
9、
liEl.forEach(item => { ... });
遍历所有item元素,如果元素的文本内容与val相等,则设置data-activa属性;否则,移除该属性。
10、
window.addEventListener('storage', ({ key, storageArea, newValue }) => { ... });
添加一个事件监听器,当localStorage
发生变化时触发。如果变化的键是keyOnly且新值为0,则移除所有item元素的data-activa属性,并清除pageItem和keyOnly的存储。
接收页(receive)
let pageItem = localStorage.getItem('pageItem');
let n = +localStorage.getItem('keyOnly');
let setEl = (info) => {
info = JSON.parse(info);
idEl.textContent = info.val;
};
setEl(pageItem);
window.addEventListener('storage', ({ key, storageArea, newValue }) => {
if (storageArea && key === 'pageItem') setEl(newValue);
});
if (isNaN(n)) n = 0;
n += 1;
localStorage.setItem('keyOnly', n);
window.addEventListener('unload', () => {
let n = +localStorage.getItem('keyOnly');
if (isNaN(n)) n = 1;
n -= 1;
localStorage.setItem('keyOnly', n);
});
1、
let pageItem = localStorage.getItem('pageItem');
从localStorage
中获取名为pageItem的项。
2、
let n = +localStorage.getItem('keyOnly');
从localStorage
中获取名为keyOnly的项,并尝试将其转换为数字。
3、
let setEl = (info) => { ... };
定义一个函数setEl,用于将传入的JSON字符串解析并更新页面上的idEl元素的文本内容。
4、
setEl(pageItem);
调用setEl函数,传入pageItem,更新页面。
5、
window.addEventListener('storage', ({ key, storageArea, newValue }) => { ... });
添加一个事件监听器,当localStorage
发生变化时触发。如果变化的键是pageItem,则调用setEl函数更新页面。
6、
if (isNaN(n)) n = 0; n += 1; localStorage.setItem('keyOnly', n);
如果localStorage
中的keyOnly项不存在或不是数字,则将其设置为0,并增加计数。
7、
window.addEventListener('unload', () => { ... });
添加一个事件监听器,当页面卸载时触发。如果localStorage
中的keyOnly项存在且为数字,则将其减1。
总结
发送页面通过
localStorage
和window.open
发送消息,接收页面通过localStorage
和window.addEventListener
接收消息。发送页面在发送消息前会检查localStorage
中的计数,如果计数为0,则通过打开新窗口的方式发送消息;否则,通过localStorage
发送消息。接收页面在接收到消息后,会更新页面上的idEl元素的文本内容。
此外,接收页面还跟踪发送页面发送消息的次数,当页面卸载时减少计数,并在每次发送消息时增加计数。这样可以确保发送页面在发送消息时能够根据计数决定是通过
localStorage
发送还是通过打开新窗口发送。
三种方式的区别
1、方式壹和方式贰的区别
1.1、方式壹通过自定义窗口(页面)名称来打开新窗口,并通过自定义名称检测是否已有相同名称的窗口(页面)存在;没有则打开新窗口(页面),有则刷整个窗口(页面)。
1.2、方式贰通过本地存储的keyOnly判断是否第一次打开新窗口(页面),如果是就直接使用
window.open
打开页面,否则通过BroadcastChannel
传参给接收页面,不用再次打开新窗口(页面)。
1.3、两种方式的传参区别,方式壹通过URL传参,并且自定义窗口(页面)名称,使其永远只打开一个相同名称的窗口(页面),但用户体验不是很好,因为每次打开都刷新整个页面(窗口);方式贰首次打开通过URL传参,第二次传参则通过
BroadcastChannel
实现,此操作对接收页面的代码编写不友好,接收页面需要编写两套代码来实现数据接收的工作,第一套是通过RUL接收参数;第二套是通过BroadcastChannel
接收参数。
2、方式贰和方式叁的区别
2.1、方式贰的实现本质已在第一大点中叙述,此处不在赘叙。
2.2、方式叁通过本地缓存的方式实现参数的传递;第一次打开新窗口之前先把参数缓存在本地,接收页加载的时候直接从缓存中获取即可;再次传参也是本地缓存,但接收页面则通过监听缓存变化来获取参数。
3、三种方式各自的优缺点
优点
方式壹,较好的解决窗口多开的问题;
方式贰,解决页面整体刷新的问题;
方式叁,解决接收页面写两套接收代码的问题。
缺点
方式壹,每次传参都会整体刷新页面,用户体验感不好;
方式贰,传参的方式复杂,接收参数也麻烦,首次打开时通过URL传参,二次传参则通过
BroadcastChannel
API;此方式增加接收页的代码量和代码逻辑复杂度。并且不能像方式壹一样很好的解决窗口多开问题;
方式叁,不能像方式壹一样很好的解决窗口多开问题,有时候会发生多开的情况。
4、总结
如果侧重防多开功能,建议使用第一种方式;
如果侧重传参和用户体验,建议使用第三种方式;
第三种方式存有可优化空间(待优化…),视情况选择合适自己的方式。
彩蛋
1、web前端实现多个元素标签页相互通信、html页面之间相互通信
上一篇: 【前端】vue在线编辑器
下一篇: vue3的响应式赋值中数组array,对象object,集合set的重新赋值怎么操作,问过Chatgpt的答案
本文标签
web前端之标签页相互通信、浏览器窗口的open方法的参数及防多开功能、本地存储之local与session和storage监听、动态自定义元素属性及选择器、伪类hover与active、样式混合器
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。