TypeScript Mapping Types, Custom Lighthouse Audits, and React Portal Tips
This newsletter issue teaches frontend developers how to simplify TypeScript typings with mapped and index types, extend Chrome Lighthouse by adding custom gatherers and audits for static resources, and effectively use React portals for modals and other UI elements while handling events and styling correctly.
This issue of the knowledge newsletter presents three practical topics for frontend developers: simple applications of TypeScript mapped and index types, creating custom audits with Chrome Lighthouse, and useful tips for using React Portal.
TypeScript Mapping and Index Types
When defining input parameter types in TypeScript, changes to interface fields normally require manual updates to the corresponding types. By using index signatures or the built‑in Record utility type, additional keys can be added without type errors. Record generates an index type from supplied key and value types, which is a form of mapped type . For more complex structures, a recursive mapped type can automatically propagate new keys and values, eliminating the need to edit types manually each time the shape changes.
Custom Lighthouse Audits
Lighthouse is widely used for performance, best‑practice, SEO, and accessibility checks. To add custom metrics, you can supply a custom configuration and extend the default workflow with your own gatherers and audits.
Typical workflow:
Pass the browser port and custom config to Lighthouse.
Lighthouse’s gatherer collects page data.
The audit processes the data and returns a score.
All results are aggregated and displayed.
Example configuration and execution code:
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 // 新增的自定义配置
);Define a custom configuration that extends the default Lighthouse settings, adds a custom gatherer and audit, and creates a new category for static resources:
const config = require('./custom/config');
// config.js
const customResourceGatherer = require('./custom-resource-gatherer');
const customImageAudit = require('./custom-image-audit');
module.exports = {
extends: 'lighthouse:default', // inherit default config
settings: {
onlyAudits: [
'custom-image-audit',
],
},
passes: [
{
passName: 'defaultPass',
gatherers: [
customResourceGatherer, // custom gatherer
]
}
], // Object[] gatherers
audits: [
customImageAudit, // custom audit
],
categories: { // displayed alongside performance, seo, etc.
resource: {
title: '静态资源', // category name
description: '展示页面上的所有资源情况',
auditRefs: [
{
id: 'custom-image-audit',
weight: 1,
group: 'metrics'
}
]
},
},
groups: { // sub‑group configuration
metrics: {
title: '资源',
},
},
};Custom gatherer that extracts image resources from the Performance API:
const Gatherer = require('lighthouse').Gatherer;
class CustomResourceGatherer extends Gatherer {
beforePass() {
// set up before page load
}
afterPass(options) {
// collect after page load
const driver = options.driver; // inject custom 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; // return collected data
});
}
}Custom audit that scores the collected image data and presents it in a table:
const Audit = require('lighthouse').Audit;
class CustomRequestAudit extends Audit {
static get meta() {
return {
id: 'custom-image-audit', // matches config
title: '图片请求数据', // shown when score is high
failureTitle: '资源加载失败', // shown when score is low
description: '展示页面中加载的图片数据',
requiredArtifacts: ['CustomResourceGatherer'] // linked gatherer
};
}
static audit(artifacts) {
// obtain data from gatherer
const loadMetrics = JSON.parse(artifacts.CustomResourceGatherer);
if (!loadMetrics.length) {
return {
numericValue: 0,
score: 1,
displayValue: 'No image found'
};
}
// format data
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 custom score and table details
return {
score: 0.89,
details: Audit.makeTableDetails(headings, data),
displayValue: `页面中有 ${loadMetrics.length} 个图片`,
rawValue: ''
};
}
}React Portal Tips
React Portal allows rendering children into a DOM node that exists outside the parent component hierarchy, which is useful for modals, dialogs, toasts, etc. The API createPortal(children, container, key?) returns an object with properties such as $$typeof , key , children , containerInfo , and implementation .
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,
};
}Key points when using portals:
Event bubbling works as if the component were still in its original tree.
The portal’s lifecycle is tied to the lifecycle of its target DOM container.
Be cautious with global CSS; use module‑scoped styles to avoid unintended side effects.
Xianyu Technology
Official account of the Xianyu technology team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.