Basic media query looks like in CSS
@media only screen and (max-width: 576px) {
// do something
}
// set the width range
@media only screen and (min-width: 360px) and (max-width: 768px) {
// do something
}
How to response to width changes in javascript
The answer is MediaQueryList
定义基本类型以及相应方案
const responsiveMap = {
xs: "(max-width: 575px)",
sm: "(min-width: 576px) and (max-width: 767px)",
md: "(min-width: 768px) and (max-width: 991px)",
lg: "(min-width: 992px)",
} as const;
export type Breakpoint = keyof typeof responsiveMap;
export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
export type SubscribeFunc = (
screens: ScreenMap,
breakpointChecked: Breakpoint
) => void;
export type MediaQueryResult = { matches: boolean };
export type MediaQueryListener = ({ matches }: MediaQueryResult) => void;
export type responsiveMapValue<T extends Object> = {
[key in keyof T]: T[key];
}[keyof T];
第一部分
class ResponsiveObserver {
#uid: number;
#subscribers: Array<{
token: string;
func: SubscribeFunc;
}>;
// 存储token及对应的回调
#screens: ScreenMap = {};
/* 监听的MediaQueryList对象及监听函数listener
** listener会执行#subscribers中的回调func
*/
#matchHandlers: Partial<
Record<
responsiveMapValue<typeof responsiveMap>,
{
mql: MediaQueryList;
listener: MediaQueryListener;
}
>
>;
constructor() {
this.#subscribers = [];
this.#matchHandlers = {};
this.#screens = {};
this.#uid = 0;
}
第二部分
#registerLisener() {
(Object.keys(responsiveMap) as Breakpoint[]).forEach((e) => {
const matchMediaQuery = responsiveMap[e];
if (!matchMediaQuery) return;
const listener = ({ matches }: MediaQueryResult) => {
this.#dispath(matches, e);
};
const mql = window.matchMedia(matchMediaQuery);
if (mql.addEventListener) {
mql.addEventListener("change", listener);
}
this.#matchHandlers[matchMediaQuery] = {
mql,
listener,
};
listener(mql);
});
}
#dispath(matches: boolean, screen: Breakpoint) {
this.#screens = { ...this.#screens, [screen]: matches };
this.#subscribers.forEach((item) => {
item.func(this.#screens, screen);
});
return true;
}
第三部分
#registerLisener() {
(Object.keys(responsiveMap) as Breakpoint[]).forEach((e) => {
const matchMediaQuery = responsiveMap[e];
if (!matchMediaQuery) return;
const listener = ({ matches }: MediaQueryResult) => {
this.#dispath(matches, e);
};
const mql = window.matchMedia(matchMediaQuery);
if (mql.addEventListener) {
mql.addEventListener("change", listener);
}
this.#matchHandlers[matchMediaQuery] = {
mql,
listener,
};
listener(mql);
});
}
#unRegisterListener() {
(Object.keys(responsiveMap) as Breakpoint[]).forEach((screen) => {
const matchMediaQuery = responsiveMap[screen];
if (!matchMediaQuery) return;
const handler = this.#matchHandlers[matchMediaQuery];
if (handler && handler.mql && handler.listener) {
if (handler.mql.removeEventListener) {
handler.mql.removeEventListener("change", handler.listener);
}
}
});
}
第四部分
实例入口
subscribe(func: SubscribeFunc) {
if (this.#subscribers.length === 0) {
this.#registerLisener()
}
const token = "uid" + ++this.#uid
this.#subscribers.push({
token, func
})
func(this.#screens, null as unknown as Breakpoint);
return token
}
unSubscribe(token: string) {
const _subscriber = this.#subscribers.filter(item => {item.token !== token})
if(_subscriber.length === 0) {
this.#unRegisterListener()
}
}
}
const responsiveObserverInstance = new ResponsiveObserver()
export default responsiveObserverInstance