本期知识小报的主要内容
• TS中映射类型和索引类型的简单应用
• lighthouse实现自定义检测
• React Portal相关使用Tips
TS映射类型和索引类型的简单应用
在TS中,当我们给出入参定义类型的时候,接口字段改变,对应的类型也要改变。那么如何能灵活添加额外的索引呢?这里可以用索引签名或者Record来实现,可以看到即使多了key值c和d,类型检查也不会报错。
Record是TS内置的高级类型,实际上是根据传入的key和value类型生成对应的索引类型。这种生成索引类型的语法叫做映射类型。
那如果是更复杂的场景呢?难道又要每次结构改变自己手动改类型吗?实际上是不需要的,这里我们结合上面的映射类型和索引类型,简单写一个递归处理即可。最简单的结构就是上面type Obj一样的,入参这边我们加上extends Record,在递归处理的时候,当发现value是索引类型,就往里面的结构寻找,如果是简单类型,直接返回就可以了。
让我们看看生成res的结构,发现每层确实已经加入了。
现在让我们使用看看。
这样我们使用映射类型和索引类型实现了较为灵活的类型配置。
总结:在写不确定结构的类型时,我们可以通过简单的索引类型,实现key和value随意扩展。而对于复杂的结构,我们也可以通过映射类型递归生成对应的索引类型,方便改动。
lighthouse实现自定义检测
Chrome的lighthouse大家想必非常熟悉,常被用来做web页面的检测,从performance、best-practice到seo等方面,其都能为我们给出详细的检测结果。如果需要自定义检测数据可以怎么做呢?
我们可以把进行自定义检测这一系列的工作流程简单想象为:
1. 将启动的浏览器的端口号以及对应自定义配置传给lighthouse,随后lighthouse开始工作
2. lighthouse先通过收集器(gatherer)来收集页面中的数据,随后将数据交给审查器
3. 审查器(audit)根据自己的规则处理数据,给出结果和对应的分数
4. 最后收集所有的审查结果,输出分数以及根据相应细节得出的建议,得到结果
下面就以一个收集页面加载图片数据为例说明。
自定义配置
通过额外传入配置config
完成自定义配置,
const config = require('./custom/config');
const options = {
logLevel: 'info',
output: 'html',
onlyCategories: ['performance', 'accessibility', 'best-practices', 'pwa', 'seo'],
port: chrome.port,
};
const runnerResult = await lighthouse(
'https://goofish.com',
options,
config // 新增的自定义配置
);
传入自定义收集器(gatherer)与审查器(audit)
// main.js
const config = require('./custom/config');
// config.js
const customResourceGatherer = require('./custom-resource-gatherer');
const customImageAudit = require('./custom-image-audit');
module.exports = {
extends: 'lighthouse:default', // 'lighthouse:default' | undefined 是否继承默认配置
settings: {
onlyAudits: [
'custom-image-audit',
],
},
passes: [
{
passName: 'defaultPass',
gatherers: [
customResourceGatherer, // 填入自己的收集器
]
}
], // Object[] 采集器
audits: [
customImageAudit, // 填入自己的审查器
],
categories: { // 这里是在lh报告上的展示的类别配置,和performance seo那些同级
resource: {
title: '静态资源', //定义一个叫静态资源的分组,下面规定有哪些审查器
description: '展示页面上的所有资源情况',
auditRefs: [
{
id: 'custom-image-audit',
weight: 1,
group: 'metrics'
}
]
},
},
groups: { // 这里是分组配置,属于类别的下一级
metrics: {
title: '资源',
},
},
}
编写收集器(gatherer)
定义收集数据的逻辑,在lighthouse启动后,会根据配置运行对应的收集器,收集器收集并返回数据。
const Gatherer = require('lighthouse').Gatherer;
class CustomResourceGatherer extends Gatherer {
beforePass() {
// 设定页面加载之前的采集内容
}
afterPass(options) {
// 设定页面加载之后的采集内容
const driver = options.driver; // 这里通过传入的变量注入自定义js,收集返回
return driver
.evaluateAsync(
'JSON.stringify(window.performance.getEntriesByType("resource").filter(item => item.initiatorType === "img" && /(png|apng|gif|webp|jpg|jpeg)$/.test(item.name)))'
)
.then((loadMetrics) => {
if (!loadMetrics) {
throw new Error('无法获取资源信息');
}
return loadMetrics; // 返回收集的数据
});
}
}
编写审查器(audit)
定义数据处理逻辑,对收集器返回的数据进行数据逻辑处理,返回分数以及处理后的数据结果。
const Audit = require('lighthouse').Audit;
class CustomRequestAudit extends Audit {
static get meta() {
return {
id: 'custom-image-audit', // 与配置中的audits数组内容对应
title: '图片请求数据', // 分数达标时的展示标题(score大于等于90分)
failureTitle: '资源加载失败', // 分数不达标时的展示标题
description: '展示页面中加载的图片数据', // 子标题
requiredArtifacts: ['CustomResourceGatherer'] // 所对应数据的采集器
};
}
static audit(artifacts) {
// 获得收集器的数据
const loadMetrics = JSON.parse(artifacts.CustomResourceGatherer);
if (!loadMetrics.length) {
return {
numericValue: 0,
score: 1,
displayValue: 'No image found'
};
}
// 整理数据
const data = loadMetrics.map((item) => {
return {
name: item.name,
duration: `${parseInt(item.duration)}ms`,
size: item.encodedBodySize
}
})
const headings = [
{
key: 'name',
itemType: 'url',
text: '请求'
},
{
key: 'duration',
itemType: 'text',
text: '耗时'
},
{
key: 'size',
itemType: 'text',
text: '大小'
}
];
// 根据自定义规则返回得分
return {
score: 0.89, // 自定定义分数
// 这里可以使用audit提供的样式展示格式,通过传入数据来控制展示
details: Audit.makeTableDetails(headings, data),
displayValue: `页面中有 ${loadMetrics.length} 个图片`,
rawValue: '',
}
}
结果展示
React Portal相关使用Tips
React Portal提供了将子节点渲染到父组件以外的DOM节点的解决方案
Portal常用于帮助子节点“跳出”当前父容器,从而避免被当前父容器的节点位置以及CSS属性等影响,因此日常开发中,mask、dialog、toast等的实现,都可以直接使用Portal。
createPortal接收三个参数:要渲染的子节点,指定的DOM节点和可选的键值(key)。然后React会创建一个新的React Portal对象,该对象包含以下属性:
• $$typeof:标识该对象为React Portal
• key:Portal的键值
• children:要渲染的子节点
• containerInfo:指定的DOM节点
• implementation:Portal的实现
function createPortal(
children: ReactNodeList,
container: Container,
key: ?string = null,
): ReactPortal {
return {
// This tag allow us to uniquely identify this as a React Portal
$$typeof: REACT_PORTAL_TYPE,
key: key == null ? null : '' + key,
children,
containerInfo: getContainer(container),
implementation: null,
};
}
React Portal是一种特殊的React元素,它具有独特的类型标识符REACT_PORTAL_TYPE,此外,Portal还需要一个指向实际DOM节点的引用,这个引用可以通过containerInfo属性获取。React Portal的渲染是通过ReactDOM.render方法实现的,具体的渲染过程与普通的React元素相同,只是将渲染结果插入到Portal的指定DOM节点中,而不是插入到根节点中。在Portal渲染之前,还需要将Portal元素从其父元素的子节点列表中移除。这是通过ReactChildReconciler模块中的moveChild方法实现的。移除操作完成后,Portal元素的containerInfo属性将被设置为null,以便在Portal被卸载时清除对DOM节点的引用。
使用Portal需要注意以下事项:
• creactPortal的冒泡依然存在。creactPortal与render不同,React Portal 之所以能够保留事件冒泡行为,是因为 Portal 并不是在全局创建一个独立的 DOM 节点,而是将组件的子树渲染到了指定的目标 DOM 节点中。因此,这些组件仍然保留了事件冒泡行为。
• React Portal 的生命周期可以通过创建 Portal 时指定的目标 DOM 容器的生命周期来控制。具体来说,在 Portal 的目标 DOM 容器的生命周期中,会触发 Portal 组件的生命周期方法,从而实现对 Portal 生命周期的控制。 例如,假设有一个包含一个 Portal 的父组件 ParentComponent,当 ParentComponent 被挂载时,Portal 组件的 componentDidMount() 生命周期方法也会被触发,而当 ParentComponent 被卸载时,Portal 组件的 componentWillUnmount() 生命周期方法也会被触发。
• 谨慎使用CSS。css的作用域是全局,通过Portal移动的的子元素依然会受CSS的影响。当父容器进行替换以后,加大了样式会覆盖别的地方的样式或是在新的父节点上被其他样式影响的可能性。正常情况下,我们希望一个样式只适用于单个对应的组件,所以为了避免样式混乱,建议portal使用module.css或是scss。