学习4

65 阅读43分钟

懒加载如何判断元素出现在视口内

在前端开发中,懒加载是一种优化策略,用于延迟加载那些在用户滚动到它们所在位置之前不需要立即显示的元素,以提高页面的加载性能。判断元素是否出现在视口内是实现懒加载的关键步骤,下面为你介绍几种常见的实现方法:

1. 使用getBoundingClientRect()方法

getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。通过检查元素的位置和视口的尺寸,可以判断元素是否在视口内。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device, initial-scale=1.0">
    <title>Lazy Load Example</title>
    <style>
        img {
            display: block;
            margin-bottom: 20px;
        }
    </style>
</head>

<body>
    <img data-src="https://via.placeholder.com/300" class="lazyload">
    <img data-src="https://via.placeholder.com/300" class="lazyload">
    <img data-src="https://via.placeholder.com/300" class="lazyload">
    <img data-src="https://via.placeholder.com/300" class="lazyload">
    <img data-src="https://via.placeholder.com300" class="lazyload">

    <script>
        const lazyImages = document.querySelectorAll('.lazyload');

        function isInViewport(element) {
            const rect = element.getBoundingClientRect();
            return (
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
                rect.right <= (window.innerWidth || document.documentElement.clientWidth)
            );
        }

        function loadImages() {
            lazyImages.forEach(image => {
                if (isInViewport(image)) {
                    image.src = image.dataset.src;
                    image.classList.remove('lazyload');
                }
            });
        }

        window.addEventListener('scroll', loadImages);
        window.addEventListener('resize', loadImages);
        loadImages();
    </script>
</body>

</html>

代码解释:

  • isInViewport 函数:该函数使用 getBoundingClientRect() 方法获取元素的位置信息,然后检查元素的顶部、左侧、底部和右侧是否在视口内。
  • loadImages 函数:遍历所有懒加载的图片元素,检查它们是否在视口内。如果是,则将 data - src 属性的值赋给 src 属性,并移除 lazyload 类。
  • 事件监听:监听 scrollresize 事件,当用户滚动页面或调整窗口大小时,调用 loadImages 函数。

2. 使用 IntersectionObserver API

IntersectionObserver 是一个现代的浏览器 API,用于异步观察目标元素与其祖先元素或顶级文档视口的交叉状态。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lazy Load Example with IntersectionObserver</title>
    <style>
        img {
            display: block;
            margin-bottom: 20px;
        }
    </style>
</head>

<body>
    <img data-src="https://via.placeholder.com/300" class="lazyload">
    <img data-src="https://via.placeholder.com/300" class="lazyload">
    <img data-src="https://via.placeholder.com/300" class="lazyload">
    <img data-src="https://via.placeholder.com/300" class="lazyload">
    <img data-src="https://via.placeholder.com/300" class="lazyload">

    <script>
        const lazyImages = document.querySelectorAll('.lazyload');

        const observer = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    img.src = img.dataset.src;
                    img.classList.remove('lazyload');
                    observer.unobserve(img);
                }
            });
        });

        lazyImages.forEach(image => {
            observer.observe(image);
        });
    </script>
</body>

</html>

代码解释:

  • 创建 IntersectionObserver 实例:传入一个回调函数,该函数在目标元素与视口的交叉状态发生变化时被调用。
  • 回调函数:遍历所有的观察项,如果元素与视口相交(isIntersectingtrue),则将 data - src 属性的值赋给 src 属性,并移除 lazyload 类,同时停止观察该元素。
  • 观察元素:遍历所有懒加载的图片元素,使用 observer.observe() 方法开始观察它们。

IntersectionObserver API 是一种更高效、更简洁的方法,因为它是异步的,不会阻塞主线程,并且浏览器会自动优化观察过程。建议优先使用这种方法来实现懒加载。

React 中的组件通信

在 React 中,组件通信是一个重要的概念,它允许不同组件之间共享数据和交互。以下是几种常见的 React 组件通信方式:

1. 父组件向子组件通信

父组件可以通过 props 向子组件传递数据。

示例代码

// 子组件
const ChildComponent = (props) => {
    return <p>{props.message}</p>;
};

// 父组件
const ParentComponent = () => {
    const message = "Hello from parent!";
    return <ChildComponent message={message} />;
};

2. 子组件向父组件通信

子组件可以通过调用父组件传递的回调函数来向父组件传递数据。

示例代码

// 子组件
const ChildComponent = (props) => {
    const handleClick = () => {
        props.onClick("Hello from child!");
    };
    return <button onClick={handleClick}>Send message to parent</button>;
};

// 父组件
const ParentComponent = () => {
    const handleChildMessage = (message) => {
        console.log(message);
    };
    return <ChildComponent onClick={handleChildMessage} />;
};

3. 兄弟组件之间通信

兄弟组件之间的通信可以通过共同的父组件来实现。一个兄弟组件通过父组件的回调函数传递数据,另一个兄弟组件从父组件的 props 中获取数据。

示例代码

// 子组件 1
const ChildComponent1 = (props) => {
    const handleClick = () => {
        props.onSendMessage("Message from ChildComponent1");
    };
    return <button onClick={handleClick}>Send message</button>;
};

// 子组件 2
const ChildComponent2 = (props) => {
    return <p>{props.message}</p>;
};

// 父组件
const ParentComponent = () => {
    const [message, setMessage] = React.useState("");
    const handleSendMessage = (newMessage) => {
        setMessage(newMessage);
    };
    return (
        <div>
            <ChildComponent1 onSendMessage={handleSendMessage} />
            <ChildComponent2 message={message} />
        </div>
    );
};

4. 跨层级组件通信(Context API)

当组件嵌套层级较深时,可以使用 React 的 Context API 来实现跨层级的组件通信。

示例代码

// 创建 Context
const MyContext = React.createContext();

// 子组件
const GrandChildComponent = () => {
    const value = React.useContext(MyContext);
    return <p>{value}</p>;
};

// 父组件
const ParentComponent = () => {
    const message = "Hello from grandparent!";
    return (
        <MyContext.Provider value={message}>
            <GrandComponent />
        </MyContext.Provider>
    );
};

5. 使用第三方库(如 Redux、MobX)

对于大型应用,使用第三方状态管理库可以更方便地实现组件之间的通信。以 Redux 为例:

示例代码

// 安装 redux 和 react-redux
// npm install redux react-redux

// actions.js
const sendMessage = (message) => {
    return {
        type: "SEND_MESSAGE",
        payload: message
    };
};

// reducer.js
const initialState = {
    message: ""
};
const messageReducer = (state = initialState, action) => {
    switch (action.type) {
        case "SEND_MESSAGE":
            return {
                ...state,
                message: action.payload
            };
        default:
            return state;
    }
};

// store.js
import { createStore } from 'redux';
const store = createStore(messageReducer);

// 子组件 1
import { useDispatch } from 'react-redux';
const ChildComponent1 = () => {
    const dispatch = useDispatch();
    const handleClick = () => {
        dispatch(sendMessage("Message from ChildComponent1"));
    };
    return <button onClick={handleClick}>Send message</button>;


// 子组件 2
import { useSelector } from 'react-redux';
const ChildComponent2 = () => {
    const message = useSelector(state => state.message);
    return <p>{message}</p>;
};

// 父组件
import { Provider } from 'react-redux';
const ParentComponent = () => {
    return (
        <Provider store={store}>
            <ChildComponent1 />
            <ChildComponent2 />
        </Provider>
    );
};

6. 使用 forwardRef

forwardRef 主要用于在组件间转发 ref,这在需要访问子组件的 DOM 节点时很有用。

jsx

// 子组件
const ChildComponent = React.forwardRef((props, ref) => {
    return <input ref={ref} type="text" />;
});

// 父组件
const ParentComponent = () => {
    const inputRef = React.createRef();
    const handleClick = () => {
        inputRef.current.focus();
    };
    return (
        <div>
            <ChildComponent ref={inputRef} />
            <button onClick={handleClick}>Focus input</button>
        </div>
    );
};

在这个例子中,父组件创建了一个 ref 并将其传递给子组件,子组件使用 forwardRef 接收这个 ref 并将其应用到 input 元素上,这样父组件就能直接操作子组件的 DOM 节点了。 以上就是 React 中常见的组件通信方式,你可以根据具体的应用场景选择合适的方式。

在 React 中,ref 是一个用于引用 DOM 节点或者组件实例的属性。它提供了一种方式,让你可以直接访问这些元素,从而执行一些操作,比如获取元素的尺寸、触发元素的焦点、调用组件的方法等。以下是关于 ref 的详细介绍:

  1. 创建 ref
    在 React 里,有两种主要的方式来创建 ref
使用 React.createRef()

这是在类组件中常用的创建 ref 的方法。

jsx

import React from 'react';

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        // 创建一个 ref 来存储 input 元素的引用
        this.inputRef = React.createRef();
    }

    componentDidMount() {
        // 在组件挂载后,通过 ref 访问 input 元素并聚焦
        this.inputRef.current.focus();
    }

    render() {
        return (
            <input type="text" ref={this.inputRef} />
        );
    }
}

export default MyComponent;
使用 useRef Hook

这是在函数式组件中创建 ref 的方法。

jsx

import React, { useRef } from 'react';

const MyComponent = () => {
    // 创建一个 ref 来存储 input 元素的引用
    const inputRef = useRef(null);

    const handleClick = () => {
        // 通过 ref 访问 input 元素并聚焦
        inputRef.current.focus();
    };

    return (
        <div>
            <input type="text" ref={inputRef} />
            <button onClick={handleClick}>Focus Input</button>
        </div>
    );
};

export default MyComponent;

2. ref 的用途

访问 DOM 元素

通过 ref 可以直接访问 DOM 元素,从而执行一些 DOM 操作,比如获取元素的尺寸、滚动位置等。

jsx

import React, { useRef, useEffect } from 'react';

const MyComponent = () => {
    const divRef = useRef(null);

    useEffect(() => {
        // 获取 div 元素的宽度
        const width = divRef.current.offsetWidth;
        console.log('Div width:', width);
    }, []);

    return (
        <div ref={divRef}>This is a div.</div>
    );
};

export default MyCompone

- React Hook的副作用。[副作用钩子useEffect]

在 React 中,副作用是指那些会影响到外部世界的操作,比如数据获取、订阅事件、手动修改 DOM 等。在函数式组件里,这些副作用不能直接在组件的主体部分执行,因为函数式组件在每次渲染时都会重新执行,这可能会导致副作用被多次执行,从而引发问题。而 useEffect 就是 React 提供的一个 Hook,用于处理这些副作用。

useEffect 的基本用法

useEffect 接收两个参数:一个回调函数和一个可选的依赖项数组。

import React, { useEffect } from 'react';

const MyComponent = () => {
    useEffect(() => {
        // 这里是副作用代码
        console.log('Effect is running');

        // 可选的清理函数
        return () => {
            console.log('Effect is cleaning up');
        };
    }, []); // 依赖项数组

    return <div>My Component</div>;
};

export default MyComponent;

代码解释

  1. 回调函数:在这个回调函数里编写副作用代码。当组件渲染完成后,这个回调函数会被执行。
  2. 依赖项数组:这是一个可选参数。如果提供了这个数组,useEffect 会根据数组中的值来决定是否执行回调函数。
    • 空数组 []:表示副作用只在组件挂载时执行一次,在组件卸载时执行清理函数。
    • 有值的数组 [value1, value2]:当数组中的任何一个值发生变化时,useEffect 的回调函数会重新执行。
    • 不提供依赖项数组useEffect 会在每次组件渲染时都执行回调函数。
  3. 清理函数:回调函数可以返回一个清理函数,这个函数会在组件卸载时执行,用于清理副作用,比如取消订阅、清除定时器等。

常见使用场景

数据获取
import React, { useEffect, useState } from 'react';

const DataFetchingComponent = () => {
    const [data, setData] = useState(null);

    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch('https://api.example.com/data');
            const result = await response.json();
            setData(result);
        };

        fetchData();
    }, []);

    return (
        <div>
            {data ? <p>{data.message}</p> : <p>Loading...</p>}
        </div>
    );
};

export default DataFetchingComponent;
订阅事件
import React, { useEffect } from 'react';

const EventSubscriptionComponent = () => {
    useEffect(() => {
        const handleScroll = () => {
            console.log('Scroll event triggered');
        };

        window.addEventListener('scroll', handleScroll);

        return () => {
            window.removeEventListener('scroll', handleScroll);
        };
    }, []);

    return <div>Scroll this page to trigger event</div>;
};

export default EventSubscriptionComponent;
手动修改 DOM
import React, { useEffect, useRef } from 'react';

const DOMManipulationComponent = () => {
    const divRef = useRef(null);

    useEffect(() => {
        if (divRef.current) {
            divRef.current.style.color = 'red';
        }
    }, []);

    return <div ref={divRef}>This text will turn red</div>;
};

export default DOMManipulationComponent;

通过 useEffect,可以在式组件中处理各种副作用,同时保证代码的清晰和可维护性。

- React 里面的优化点。(其实是想引出useMemo,useCallBack

在 React 中,有很多优化点,而 useMemouseCallback 是其中重要的两个,它们主要用于优化性能,避免不必要的计算和组件重新渲染。下面详细介绍相关内容:

1. useMemo

useMemo 用于记忆化计算,它会缓存计算结果,只有当依赖项发生变化时才会重新计算。这在处理昂贵的计算时非常有用,可以避免每次渲染都进行重复计算。

基本语法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 第一个参数是一个函数,该函数返回需要记忆的值。
  • 第二个参数是一个依赖项数组,当数组中的值发生变化时,useMemo 会重新计算返回值。
示例代码
import React, { useMemo, useState } from 'react';

const ExpensiveComponent = () => {
    const [count, setCount] = useState(0);
    const [inputValue, setInputValue] = useState('');

    // 模拟一个昂贵的计算
    const expensiveValue = useMemo(() => {
        console.log('Performing expensive calculation');
        let sum = 0
        for (let i = 0; i < 1000000; i++) {
            sum += i;
        }
        return sum;
    }, []);

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            <input
                type="text"
                value={inputValue}
                onChange={(e) => setInputValue(e.target.value)}
            />
            <p>Expensive Value: {expensiveValue}</p>
        </div>
    );
};

export default ExpensiveComponent;

在这个例子中,expensiveValue 的计算只会在组件挂载时执行一次,因为依赖项数组为空。即使 countinputValue 发生变化,expensiveValue 也不会重新计算。

2. useCallback

useCallback 用于记忆化函数,它会返回一个记忆化的回调函数,只有当依赖项发生变化时才会返回一个新的函数。这在将函数作为 props 传递给子组件时非常有用,可以避免子组件因为函数引用的变化而不必要地重新渲染。

基本语法
const memoizedCallback = useCallback(() => {
    doSomething(a, b);
}, [a, b]);
  • 第一个参数是需要记忆的函数。
  • 第二个参数是一个依赖项数组,当数组中的值发生变化时,useCallback 会返回一个新的函数。
示例代码
import React, { useCallback, useState } from 'react';

const ChildComponent = React.memo(({ onClick }) => {
    console.log('Child component rendered');
    return <button onClick={onClick}>Click me</button>;
});

const ParentComponent = () => {
    const [count, setCount] = useState(0);

    const handleClick = useCallback(() => {
        setCount(count + 1);
    }, [count]);

    return (
        <div>
            <p>Count: {count}</p>
            <ChildComponent onClick={handleClick} />
        </div>
    );
};

export default ParentComponent;

在这个例子中,handleClick 函数使用 useCallback 进行记忆化。只有当 count 发生变化时,handleClick 才会是一个新的函数。由于 ChildComponent 使用了 React.memo 进行包裹,只有当 onClick 函数引用发生变化时,ChildComponent 才会重新渲染。

其他优化点

除了 useouseCallback,还有一些其他的 React 优化点:

  • 使用 React.memo:用于包裹函数组件,对组件进行浅比较,如果 props 没有变化,组件不会重新渲染。
const MyComponent = React.memo((props) => {
    // 组件逻辑
});
  • 使用 shouldComponentUpdate(类组件):在类组件中,可以通过 shouldComponentUpdate 方法来控制组件是否需要重新渲染。
class MyClassComponent extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        // 根据条件返回 true 或 false
        return this.props.someProp !== nextProps.someProp;
    }

    render() {
        return <div>{this.props.someProp}</div>;
    }
}
  • 使用 PureComponent(类组件)PureComponent 是一个特殊的类组件,它会自动对 props 和 state 进行浅比较,如果没有变化,组件不会重新渲染。
class MyPureComponent extends React.PureComponent {
    render() {
        return <div>{this.props.someProp}</div>;
    }
}

通过合理使用这些优化点,可以显著提高 React 应用的性能。

React Portal 的理解与使用

理解

React Portal 是 React 提供的一种将子节点渲染到存在于父组件以外的 DOM 节点的方式。通常情况下,React 组件的渲染是基于组件树的层次结构,子组件会被渲染到其父组件对应的 DOM 节点内部。但在某些场景,我们可能希望将组件到 DOM 树的其他位置,比如模态框、提示框、下拉菜单等,这些组件可能需要脱离当前组件的层级,直接挂载到页面的顶层,以避免被父组件的样式(如 overflow: hidden)影响,或者为了实现一些全局的交互效果。React Portal 就可以帮助我们实现这样的需求。

使用

基本语法

ReactDOM.createPortal 是创建 Portal 的方法,其基本语法如下:

ReactDOM.createPortal(child, container);
  • child:是要渲染的子元素,可以是 React 元素、字符串、数字等。
  • container:是一个 DOM 节点,child 会被渲染到这个节点内部。
示例代码

以下是一个使用 React Portal 创建模态框的示例:

import React from 'react';
import ReactDOM from 'react-dom';

// 模态框组件
const Modal = ({ isOpen, onClose, children }) => {
    if (!isOpen) {
        return null;
    }

    // 创建一个 div 作为模态框的容器
    const modalRoot = document.getElementById('modal-root');
    if (!modalRoot) {
        return null;
    }

    return ReactDOM.createPortal(
        <div className="modal-overlay" onClick={onClose}>
            <div className="modal-content" onClick={(e) => e.stopPropagation()}>
                {children}
                <button onClick={onClose}>关闭</button>
            </div>
        </div>,
        modalRoot
    );
};

// 父组件
const App = () => {
    const [isModalOpen, setIsModalOpen] = React.useState(false);

    const openModal = () => {
        setIsModalOpen(true);
    };

    const closeModal = () => {
        setIsModalOpen(false);
    };

    return (
        <div>
            <button onClick={openModal}>打开模态框</button>
            <Modal isOpen={isModalOpen} onClose={closeModal}>
                <h2>这是一个模态框</h2>
                <p>模态框的内容可以是任意的 React 元素。</p>
            </Modal>
        </div>
    );
};

// 在 index.html 中添加模态框的根节点
// <div id="root"></div>
// <div id="modal-root"></div>

export default App;
代码解释
  1. Modal 组件:接收 isOpenonClosechildren 三个 props。当 isOpentrue 时,使用 ReactDOM.createPortal 将模态框内容渲染到 idmodal-root 的 DOM 节点中。
  2. App 组件:通过 useState 来控制模态框的显示和隐藏。点击“打开模态框”按钮时,调用 openModal 函数将 isModalOpen 设置为 true;点击模态框中的“关闭”按钮时,调用 closeModal 函数将 isModalOpen 设置为 false
  3. HTML 结构:在 index.html 中需要添加一个 idmodal-root 的 DOM 节点,作为模态框的容器。

通过这种方式,模态框可以脱离 App 组件的层级,直接渲染到页面的顶层,避免了被父组件的样式影响。

如何判断是手机端还是PC端?

在前端开发中,判断当前设备是手机端还是 PC 端是一个常见需求,正如文章中提到,比较常用的方法是通过 JavaScript 获取 navigator.userAgent 属性,并检查其中是否包含特定的关键词来判断。以下为你详细介绍:

方法一:使用正则表达式检查 navigator.userAgent

这是最常见的方法,通过正则表达式匹配 navigator.userAgent 中的关键词来判断设备类型。示例代码如下:

function isMobileDevice() {
    return /Mobi|Android|iPhone/i.test(navigator.userAgent);
}

if (isMobileDevice()) {
    console.log('当前设备是移动设备');
} else {
    console.log('当前设备是 PC 端');
}
代码解释:
  • navigator.userAgent:是一个字符串,包含了浏览器和操作系统的相关信息。
  • /Mobi|Android|iPhone/i:是一个正则表达式,i 表示忽略大小写,用于匹配 navigator.userAgent 中是否包含 MobiAndroidiPhone 这些关键词。
  • test() 方法:用于检测字符串是否匹配指定的正则表达式,如果匹配则返回 true,否则返回 false

方法二:使用媒体查询

在 CSS 中可以使用媒体查询来判断设备的屏幕宽度,从而区分手机端和 PC 端。示例代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* 手机端样式 */
        @media (max-width: 767px) {
            body::before {
                content: '当前设备是移动设备';
            }
        }

        /* PC 端样式 */
        @media (min-width: 768px) {
            body::before {
                content: '当前设备是 PC 端';
            }
        }
    </style>
</head>

<body>

</body>

</html>
代码解释:
  • @media (max-width: 767px):表示当屏幕宽度小于等于 767px 时,应用该媒体查询内的样式,这里通过 ::before 伪元素在页面上显示提示信息。
  • @media (min-width: 768px):表示当屏幕宽度大于等于 768px 时,应用该媒体查询内的样式。

方法三:结合 JavaScript 和媒体查询

可以结合 JavaScript 和媒体查询来动态判断设备类型。示例代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* 隐藏提示信息 */
        #device-info {
            display: none;
        }
    </style>
</head>

<body>
    <div id="device-info"></div>
    <script>
        const deviceInfo = document.getElementById('device-info');
        const isMobile = window.matchMedia('(max-width: 767px)').matches;

        if (isMobile) {
            deviceInfo.textContent = '当前设备是移动设备';
        } else {
            deviceInfo.textContent = '当前设备是 PC 端';
        }

        // 显示提示信息
        deviceInfo.style.display = 'block';
    </script>
</body>

</html>
代码解释:
  • window.matchMedia('(max-width: 767px)'):用于创建一个媒体查询对象,matches 属性返回一个布尔值,表示当前屏幕是否匹配该媒体查询。
  • 根据 matches 属性的值,在页面上显示相应的提示信息。

-- 移动端适配怎么做?

移动端适配有多种方法,以下为你详细介绍:

viewport 适配

拿到设计稿之后,设置布局视口宽度为设计稿宽度,然后量尺寸进行布局即可。

rem 适配

  • 方法一:配合媒体查询,根据不同的屏幕尺寸,设置不同的 html 字体大小,达到控制 rem 大小的效果。
  • 方法二:通过 JS 在窗口发生变化时重新计算 rem。

less + rem 适配

采用成比例尺寸的 font - size 的适配策略。步骤如下:

  1. 设置完美视口,通过 JS 进行页面适配,设置不同屏幕 html 的 font - size 值,计算公式为 font - size = 布局视口宽度 / 比例值。假定采用比例 10 : 1,在 less 中定义变量 @font - size 表示 html 的 font - size,如 @font size=(375 / 10 rem) (这里的“rem”只表示单位,与 font - size 无关)。
  2. 在 less 中设置元素样式,并转换为 rem 单位,大小计算公式为 大小 = 原本大小 / @font - size。注意:less 语言中除法要加括号,如 @font - size=(375/100rem)width=(375/@font - size)

百分比适配方式

这种方法只是宽度能适配,高度不能适配,只能设置某个高度固定死。

动态生成 viewport 适配方式

动态设置缩放比例,动态创建 meta 标签,并且将计算出来的缩放比例放到这个标签的 content 属性里面。

媒体查询适配

可以针对不同的媒体类型定义不同的样式,针对不同的尺寸设置不同的样式。也可以通过 媒体查询在不同屏幕设置不同的 html 字体大小。

流式布局(百分比布局)

针对容器宽度进行百分比设置,实现一定程度的适配。

通过 JS 配置文件实现适配

这是最常用的方式之一,可根据不同的屏幕尺寸动态调整页面布局和样式。

- H5 与手机是如何通信的?[webview]

H5 与手机(原生应用)通过 WebView 进行通信主要有以下几种常见方式:

1. 通过 WebView 提供的接口

在 Android 和 iOS 中,WebView 都提供了相应的接口来实现 H5 与原生代码的交互。

Android 示例

在 Android 中,可以通过 addJavascriptInterface 方法向 WebView 注入一个 Java 对象,H5 页面可以调用该对象的方法。

Java 代码

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @SuppressLint("JavascriptInterface")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WebView webView = findViewById(R.id.webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.addJavascriptInterface(new WebAppInterface(), "Android");
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("file:///android_asset/index.html");
    }

    public class WebAppInterface {
        @android.webkit.JavascriptInterface
        public void showToast(String message) {
            // 在这里可以处理从 H5 传来的消息
            android.widget.Toast.makeText(MainActivity.this, message, android.widget.Toast.LENGTH_SHORT).show();
        }
    }
}

HTML + JavaScript 代码(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebView Communication</title>
</head>
<body>
    <button onclick="sendMessageToAndroid()">Send Message to Android</button>
    <script>
        function sendMessageToAndroid() {
            Android.showToast('Hello from H5!');
        }
    </script>
</body>
</html>
iOS 示例

在 iOS 中,可以使用 WKWebViewevaluateJavaScript 方法来调用 H5 页面的 JavaScript 代码,同时通过 WKScriptMessageHandler 接收 H5 发送的消息。

Objective - C 代码

#import "ViewController.h"
#import <WebKit/WebKit.h>

@interface ViewController () <WKUIDelegate, WKScriptMessageHandler>

@property (nonatomic, strong) WKWebView *webView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *userContentController = [[WKUserContentController alloc] init];
    [userContentController addScriptMessageHandler:self name:@"iOS"];
    config.userContentController = userContentController;

    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    self.webView.uiDelegate = self;
    [self.view addSubview:self.webView];

    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
    NSURL *fileURL = [NSURL fileURLWithPath:filePath];
    [self.webView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"iOS"]) {
        NSString *messageBody = message.body;
        // 处理从 H5 传来的消息
        NSLog(@"Received message from H5: %@", messageBody);
    }
}

@end

HTML + JavaScript 代码(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebView Communication</title>
</head>
<body>
    <button onclick="sendMessageToiOS()">Send Message to iOS</button>
    <script>
        function sendMessageToiOS() {
            window.webkit.messageHandlers.iOS.postMessage('Hello from H5!');
        }
    </script>
</body>
</html>

2. 通过 URL Scheme

H5 页面可以通过修改当前页面的 URL,在 URL 中携带参数,原生应用监听 WebView 的 URL 变化,解析 URL 中的参数来获取 H5 传递的信息。

HTML + JavaScript 代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>URL Scheme Communication</title>
</head>
<body>
    <button onclick="sendMessage()">Send Message</button>
    <script>
        function sendMessage() {
            window.location.href = "app://message?content=Hello%20from%20H5!";
        }
    </script>
</body>
</html>

原生应用需要监听 WebView 的 URL 变化,解析 URL 中的参数。

3. 通过 Web Storage

H5 页面可以使用 localStoragesessionStorage 存储数据,原生应用可以读取这些数据,反之亦然。

HTML + JavaScript 代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Storage Communication</title>
</head>
<body>
    <button onclick="saveData()">Save Data</button>
    <script>
        function saveData() {
            localStorage.setItem('message', 'Hello from H5!');
        }
    </script>
</body>
</html>

原生应用可以通过 WebView 的接口读取 localStorage 中的数据。

- 样式覆盖如何处理?

在前端开发中,样式覆盖是一个常见的问题,以下是几种常见的处理方法:

1. 使用更具体的选择器

CSS 选择器的优先级是根据其特异性(Specificity)来决定的。特异性越高,样式规则越优先应用。可以通过增加选择器的特异性来覆盖已有的样式。

示例代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* 原始样式 */
        .container p {
            color: blue;
        }

        /* 覆盖样式,使用更具体的选择器 */
        .container .special p {
            color: red;
        }
    </style>
</head>

<body>
    <div class="container">
        <p>这是普通段落</p>
        <div class="special">
            <p>这是特殊段落</p>
        </div>
    </div>
</body>

</html>

在上述代码中,.container .special p 选择器比 .container p 更具体,因此特殊段落的文字颜色会被覆盖为红色。

2. !important 声明

!important 声明可以强制让某个样式规则优先于其他规则。但要谨慎使用,因为过度使用 !important 会使样式表难以维护。

示例代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* 原始样式 */
        p {
            color: blue;
        }

        /* 覆盖样式,使用 !important */
        .special {
            color: red !important;
        }
    </style>
</head>

<body>
    <p>这是普通段落</p>
    <p class="special">这是特殊段落</p>
</body>

</html>

在这个例子中,特殊段落的文字颜色会被强制设置为红色,即使 p 选择器的样式规则先定义。

3. 动态添加或修改样式

可以使用 JavaScript 动态地添加或修改元素的样式。

示例代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* 原始样式 */
        p {
            color: blue;
        }
    </style>
</head>

<body>
    <p id="myParagraph">这是一个段落</p>
    <button onclick="changeColor()">改变颜色</button>
    <script>
        function changeColor() {
            const paragraph = document.getElementById('myParagraph');
            paragraph.style.color = 'red';
        }
    </script>
</body>

</html>

在这个示例中,点击按钮时,JavaScript 代码会动态地将段落的文字颜色修改为红色,从而覆盖了原始的样式。

4. 使用 CSS Modules 或 CSS-in-JS

  • CSS Modules:CSS Modules 会将 CSS 类名局部化,避免全局样式冲突。每个组件都有自己独立的样式作用域,从而减少样式覆盖的问题。
  • CSS-in-JS:CSS-in-JS 是一种将 CSS 代码嵌入到 JavaScript 中的技术,它可以动态生成唯一的类名,确保样式的局部性。

这些方法可以根据具体的项目需求和场景选择使用,以有效地处理样式覆盖问题。

- 无状态组件、有状态组件。

在 React 中,无状态组件和有状态组件是两种不同类型的组件,下面为你详细介绍它们的特点和区别:

无状态组件(Stateless Component)

  • 定义:无状态组件也被称为纯函数组件,它只负责接收 props 并返回一个 React 元素,不维护自己的状态(state)。
  • 特点
    • 简单:代码结构简单,只包含一个函数,易于理解和维护。
    • 可复用性高:由于不依赖于内部状态,所以可以在多个地方复用。
    • 性能好:因为没有状态和生命周期方法,所以渲染速度快。
  • 示例代码
import React from 'react';

// 无状态组件
const Hello = (props) => {
    return <div>Hello, {props.name}div>;
};

export default Hello;

在上述代码中,Hello 组件接收一个 props 对象,其中包含一个 name 属性,并返回一个包含问候语的 div 元素。

有状态组件(Stateful Component)

  • 定义:有状态组件是基于类的组件,它可以维护自己的状态(state),并且可以使用生命周期方法。
  • 特点
    • 状态管理:可以通过 this.state 来管理组件的内部状态。
    • 生命周期方法:可以使用 componentDidMountcomponentDidUpdatecomponentWillUnmount 等生命周期方法来处理组件的挂载、更新和卸载等操作。
    • 复杂逻辑处理:适合处理复杂的业务逻辑和交互。
  • 代码
import React, { Component } from 'react';

// 有状态组件
class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    increment = () => {
        this.setState({
            count: this.state.count + 1
        });
    };

    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.increment}>Increment</button>
            </div>
        );
    }
}

export default Counter;

在上述代码中,Counter 组件是一个有状态组件,它维护了一个 count 状态,并提供了一个 increment 方法来增加 count 的值。在 render 方法中,会根据当前的 count 状态渲染相应的 UI。

区别总结

  • 状态管理:无状态组件没有自己的状态,只依赖于 props;有状态组件可以维护自己的状态。
  • 代码结构:无状态组件是纯函数,代码结构简单;有状态组件是基于类的,代码结构相对复杂。
  • 生命周期方法:无状态组件没有生命周期方法;有状态组件可以使用生命周期方法来处理组件的不同阶段。
  • 使用场景:无状态组件适用于简单的展示性组件;有状态组件适用于需要处理复杂逻辑和交互的组件。

TypeScript的泛型

泛型的概念

在 TypeScript 里,泛型是一种创建可复用组件的强大工具,它允许你创建泛型类、泛型接口和泛型函数,这些组件可以处理多种数据类型,而不是单一的数据类型。泛型通过在定义时使用类型变量(通常用 T 表示)来实现,这个类型变量在使用时会被具体的类型所替代。

泛型函数

泛型函数可以处理多种类型的参数。以下是一个简单的泛型函数示例,用于返回传入的参数:

function identity<T>(arg: T): T {
    return arg;
}

// 使用泛型函数
let output1 = identity<string>("myString");
let output2 = identity<number>(100);console.log(output1); 
console.log(output2); 

在上述代码中,identity 函数是一个泛型函数,T 是类型变量。当调用 identity 函数时,我们可以显式地指定 T 的类型(如 stringnumber),也可以让 TypeScript 根据传入的参数自动推断类型。

泛型类

泛型类可以处理多种类型的数据。以下是一个简单的泛型类示例,用于存储和获取数据:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;

    constructor(zeroValue: T, addFunction: (x: T, y: T) => T) {
        this.zeroValue = zeroValue;
        this.add = addFunction;
    }
}

// 使用泛型类
let myGenericNumber = new GenericNumber<number>(0, function(x, y) { return x + y; });
let stringGenericNumber = new GenericNumber<string>("", function(x, y) { return x + y; });

console.log(myGenericNumber.add(1, 2)); 
console.log(stringGenericNumber.add("Hello, ", "World!")); 

在上述代码中,GenericNumber 是一个泛型类,T 是类型变量。我们可以创建不同类型的 GenericNumber 实例,如 number 类型和 string 类型。

泛型接口

泛型接口可以定义泛型函数的类型。以下是一个简单的泛型接口示例:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(10)); 

在上述代码中,GenericIdentityFn 是一个泛型接口,它定义了一个泛型函数的类型。我们可以将 identity 函数赋值给 myIdentity 变量指定 T 的类型为 number

泛型约束

有时候,我们希望泛型类型满足某些条件,这时可以使用泛型约束。以下是一个简单的泛型约束示例,要求泛型类型必须有 length 属性:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); 
    return arg;
}

// 可以传入具有 length 属性的对象
loggingIdentity("hello"); 
loggingIdentity([1, 2, 3]); 

// 不能传入没有 length 属性的对象
// loggingIdentity(10); // 报错

在上述代码中,loggingIdentity 函数使用了泛型约束 T extends Lengthwise,这意味着 T 必须是 Lengthwise 接口的子类型,即必须有 length 属性。

Redux工作流

Redux 是一个用于管理 JavaScript 应用程序中状态的可预测状态容器,其工作流主要包含以下几个核心部分和步骤:

核心概念

  • Action(动作):是一个描述状态变化的对象,通常包含 type 属性(用于描述动作的类型)和可选的 payload 属性(用于传递额外的数据)。
  • Reducer(归约器):是一个纯函数,接收当前的状态和一个动作作为参数,返回一个新的状态。
  • Store(存储):是一个对象,它持有应用的整个状态树,并且提供了一些方法来管理状态,如 dispatch 用于分发动作,subscribe 用于注册监听器。

工作流程

  1. 用户交互触发 Action:当用户在界面上进行操作(如点击按钮)时,会触发一个 Action。例如,用户点击一个增加计数器的按钮,会触发一个 INCREMENT_COUNTER 的 Action。
// 定义一个 Action 创建函数
const incrementCounter = () => {
    return {
        type: 'INCREMENT_COUNTER',
        payload: 1
    };
};
  1. Action 被分发到 Store:通过 store.dispatch() 方法将 Action 发送到 Store。
// 分发 Action
store.dispatch(incrementCounter());
  1. Store 调用 Reducer:Store 接收到 Action 后,会调用 Reducer 函数,并将当前的状态和 Action 作为参数传递给它。
// 定义 Reducer
const counterReducer = (state = { value: 0 }, action) => {
    switch (action.type) {
        case 'INCREMENT_COUNTER':
            return {
                ...state,
                value: state.value + action.payload
            };
        default:
            return state;
    }
};
  1. Reducer 返回新的状态:Reducer 根据 Action 的类型和 payload 计算并返回一个新的状态。
  2. Store 更新状态:Store 用 Reducer 返回的新状态替换当前的状态。
  3. UI 组件更新:当 Store 中的状态发生变化时,所有订阅了 Store 的 UI 组件会接收到通知,并根据新的状态更新自己的显示。
// 创建 Store
import { createStore } from 'redux';
const store = createStore(counterReducer);

// 订阅 Store 的变化
store.subscribe(() => {
    console.log('State has changed:', store.getState());
});

总结

Redux 的工作流可以概括为:用户交互触发 Action,Action 被分发到 Store,Store 调用 Reducer,Reducer 返回新的状态,Store 更新状态,最后 UI 组件根据新状态更新显示。这种单向数据流的设计使得状态的变化可预测,便于调试和维护。

- 浏览器如何做静态资源缓存?

解释选中的文本

“浏览器如何做静态资源缓存”问题聚焦于在浏览器下,实现对静态资源(如 CSS 文件、JavaScript 文件、图片等)进行缓存的。在网页开发中,静态资源通常不会频繁变动,通过缓存这些资源可以显著提升网页的加载速度和性能。当用户再次访问同一页面时,浏览器可以直接从本地缓存中读取这些资源,而无需重新从服务器下载,从而减少了网络请求节省了时间和带宽。

实现静态资源缓存的常见方法及示例代码

1. HTTP 缓存

HTTP 缓存是一种机制,用于减少服务器的请求,提高网站的性能和响应速度。它分为强缓存和协商缓存,下面为你详细介绍:

强缓存 强缓存通过设置响应头 Cache-ControlExpires 来控制。Cache-Control 是 HTTP/1.1 中用于控制缓存的通用指令,Expires 是 HTTP/1.0 中的。

1. Cache-Control

Cache-Control 有多个可选值,常见的有:

  • max-age:指定资源在本地缓存的有效时间,单位为秒。
  • no-cache:表示需要先与服务器验证资源是否有更新,再决定是否使用缓存。
  • no-store:表示不使用缓存,每次都从服务器获取资源。

服务器代码示例(使用 Node.js 和 Express)

const express = require('express');
const app = express();

// 设置静态资源目录,并设置缓存时间为 1 小时
app.use(express.static('public', {
    maxAge 3600 * 1000 // 单位是毫秒
}));

const port = 3000;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);});

在上述代码中,maxAge 选项设置了 Cache-Control 头的 max-age 值,浏览器在这个时间会直接使用本地缓存的资源。

2. Expires

Expires 指定了一个具体的时间点,在这个时间点之前,浏览器会直接使用本地缓存的资源。不过由于它使用的是服务器的绝对时间,可能会因为服务器和客户端时间不一致而出现问题,所以现在更推荐使用 Cache-Control

服务器代码示例(使用 Node.js 和 Express)

const express = require('express');
const = express();

app.get('/static/js.js', (req, res => {
    const now = new Date();
    const expires = new Date(now.getTime() + 3600 * 1000); // 1 小时后过期
    res.set('Expires', expires.toUTCString());
    res.sendFile(__dirname + '/public/js/main.js');
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

协商缓存

协商缓存通过 ETagLast-Modified 来实现。当强缓存失效时,浏览器会向服务器发送请求,携带 If-None-Match(对应 ag)或 If-Modified-Since(对应 Last-Modified)头,服务器根据这些信息判断资源是否有更新,如果没有更新则返回 304 状态码,让浏览器使用本地缓存。

1. ETag

ETag 是服务器为资源生成的一个唯一标识符,当资源发生变化时,ETag 也会相应改变。

服务器端代码示例(使用 Node.js 和 Express)

const express = require('express');
const app = express();

// 处理静态资源请求
app.get('/static/js/main.js', (req, res) => {
    // 模拟生成 ETag
    const etag = '123456';
    if (req.headers['if-none-match'] === etag) {
        res.status(304).send();
    } else {
        res.set('ETag', etag);
        res.sendFile(__dirname + '/public/js/main.js');
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});
2. Last-Modified

Last-Modified 表示资源的最后修改时间。浏览器在请求资源时,会发送 If-Modified-Since 头,服务器比较这个时间和资源的实际最后修改时间,如果一致返回 304 状态码。

服务器端代码示例(使用 Node.js 和 Express)

const express = require('express');
const app = express();
const fs = require('fs');
const path = require('path');

app.get('/static/js/main.js', (req, res) => {
    const filePath = path.join(__dirname, 'public/js/main.js');
    fs.stat(filePath, (err, stats) => {
        if (err) {
            res.status(500).send('Internal Server Error');
            return;
        }
        const lastModified = stats.mtime.toUTCString();
        if (req.headers['if-modified-since'] === lastModified) {
            res.status(304).send();
        } else {
            res.set('Last-Modified', lastModified);
            res.sendFile(filePath);
        }
    });
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

缓存策略总结

  • 当浏览器请求资源时,首先检查强缓存,如果 Cache-ControlExpires 表明资源未过期,则直接使用本地缓存。
  • 如果强缓存失效,浏览器会发送请求到服务器,携带 If-None-MatchIf-Modified-Since 头,进行协商缓存验证。
  • 如果服务器返回 304 状态码,浏览器使用本地缓存;如果资源有更新,服务器返回新的资源和状态码 200。
2. Service Worker

Service Worker 是一种在浏览器运行的脚本,它可以拦截网络请求,实现离线缓存和资源预缓存。

注册 Service Worker

if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js')
          .then(registration => {
                console.log('Service Worker registered with scope:', registration.scope
            })
          .catch(error => {
                console.error('Service Worker registration failed:', error);
            });
    });
}

Service Worker 代码示例(service - worker.js)

self.addEventListener('install', event => {
    event.waitUntil(
        c.open('static-resources-v1')          .then(cache => {
                return cache.addAll([
                    '/',
                   styles.css',
                    '/script.js',
                    '/image.jpg'
                ]);
            })
    );
});

self.addEventListener('fetch', event => {
    event.respondWith(
 caches.match(event.request)
          .then(response => {
                return response || fetch(event.request);
            })
    );
});

在上述代码中,install 事件用于缓存静态资源,fetch 事件用于拦截网络请求,如果中有对应的资源则直接返回,否则从网络获取。

SSR服务端渲染的理解

由于没有提供关于“SSR服务端渲染的理解”相关的具体文章内容,下面为你一般性地解释SSR服务端渲染。

什么是SSR服务端渲染

SSR即Server-Side Rendering,也就是服务端渲染。在传统的前端开发中,通常采用客户端渲染(CSR),浏览器从服务器拿到的是一个空的HTML骨架和一堆JavaScript文件,浏览器需要先加载HTML和JS文件,然后通过JS动态将内容填充到HTML中,这个过程在客户端完成。

而SSR则是在服务器端将组件或页面渲染成HTML字符串,然后将这个完整的HTML页面到客户端。客户端接收到的就是已经包含了完整内容的HTML页面,无需再通过JS去动态生成内容。

工作原理

  1. 请求阶段:客户端(如浏览器)向服务器发送请求。 . 服务器渲染:服务器接收到请求后,根据请求的路由信息,找到对应的组件或页面,将渲染成HTML字符串。
  2. 响应阶段:服务器将渲染好的HTML字符串作为响应发送给客户端。
  3. 客户端交互:客户端接收到完整的HTML页面后,可以直接显示内容。同时,客户端的JavaScript代码会“水合”(Hyd)操作,将事件绑定到已经渲染好的DOM元素上,使页面具备交互能力。

代码示例(使用.js和Express框架以及React)

// 引入必要的模块
const express = require('express');
const React = require('');
const ReactDOMServer = require('react-dom/server');
const app = express();

// 定义一个简单的React组件
const App () => {
    return <div>Hello SSR!</div>;
};

// 处理根路径的请求
app.get('/', (req, res) {
    // 在端渲染React组件
    const html = ReactDOM.renderToString(<App />);
    // 构建完整的HTML页面
    const page = `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>SSR Example</title>
        </head>
        <body>
            <div id="root">${html}</div>
            <!-- 这里可以引入客户端的JavaScript代码 -->
            <script src="client.jsscript>
        </body>
        </html>
    `;
    // 发送渲染好的HTML页面给客户端
    res.send(page);
});

// 启动服务器
const port = 3000;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

优点

  1. 更好的SEO:搜索引擎爬虫可以直接抓取到完整的HTML内容,有利于网站在搜索引擎中的排名。
  2. 更快的首屏加载速度:客户端可以直接显示已经渲染好的内容,无需等待JS文件加载和。
  3. 更好的用户体验:特别是在网络较慢情况下,用户可以更快地看到页面内容。

缺点

  1. 服务器压力较大:服务器需要承担渲染页面的工作,会增加服务器的CPU和内存开销。
  2. 开发复杂度较高:需要同时考虑服务器端和客户端的代码,调试和维护也相对复杂。

普通函数与自定义hook的区别

1. 调用规则

  • 普通函数:可以在任意地方被调用,包括其他函数内部、条件语句中、循环语句中等。调用时机和频率没有严格限制。 javascript function add(a, b) { return a + b; }

if (true) { const result =(1, 2); console.log(result); }

- **自定义 Hook**:必须遵循 React 的 Hook 调用规则,只能在 React 函数组件内部或其他自定义 Hook 内部调用。不能在普通 JavaScript 函数、条件语句、循环语句或嵌套函数中调用。
```javascript
import { useState } from 'react';

function useCounter() {
    const [count, setCount] = useState(0);
    const increment = () => setCount(count + 1);
    return { count, increment };
}

function MyComponent() {
    const { count, increment } = useCounter();
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={increment}>Increment</button>
        </div>
    );
}

2. 状态管理

  • 普通函数:本身不具备状态管理能力,每次调用时如果传入相同的参数,会返回相同的结果,是无状态的。如果需要管理状态,需要通过参数传递或外部变量来实现。
function calculateTotal(prices) {
    let total = 0;
    for (let price of prices) {
        total += price;
    }
    return total;
}
  • 自定义 Hook:可以使用 React 的状态管理钩子(如 StateuseReducer)来管理状态。每次调用自定义 Hook 时,它会有自己独立的状态,并且可以在组件的多次渲染之间保留状态。
import { useState } from 'react';

function useToggle(initialValue = false) {
    const [value, setValue] = useState(initialValue);
    const toggle = () => setValue(!value);
    return [value, toggle];
}

3. 副作用处理

  • 普通函数:一般不处理副作用,或者副作用的处理方式比较简单,例如在内部进行一些简单的日志记录或数据处理。如果需要处理异步操作,通常需要手动管理回调或使用 Promiseasync/await
function fetchData() {
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => console.error(error));
}
  • 自定义 Hook:可以使用 useEffect 钩子来处理副作用,如数据获取、订阅事件、手动修改 DOM 等。useEffect 会在组件渲染后执行,并且可以根据依赖项的变化来控制副作用的执行时机。
import { useEffect, useState } from 'react';

function useFetchData(url) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch(url);
                const json = await response.json();
                setData(json);
            } catch (error) {
                setError(error);
            } finally {
                setLoading(false);
            }
        };

        fetchData();
    }, [url]);

    return { data, loading, error };
}

4. 复用性和目的

  • 普通函数:主要用于封装一些通用的逻辑,提高代码的复用性,其目的是完成特定的计算或操作,不依赖于 React 组件的上下文。
function formatDate(date) {
    return new Intl.DateTimeFormat('en-US').format(date);
}
  • 自定义 Hook:专门用于复用 React 组件之间的状态逻辑和副作用逻辑,它可以将复杂的逻辑封装,使组件代码更加简洁和易于维护。
import { useEffect } from 'react';

function useDocumentTitle(title) {
    useEffect(() => {
        document.title = title;
        return () => {
            document.title = 'Default Title';
        };
    }, [title]);
}

AxiosAjax的区别

文章中未详细提及 Axios 与 Ajax 的区别,下面为你详细介绍二者的区别:

1. 概念

  • Ajax(Asynchronous JavaScript and XML):它并不是一种单一的技术,而是结合了 HTML 或 XHTML、CSS、JavaScript、DOM、XML、XSLT 以及最重要的 XMLHttpRequest 对象的一套综合技术。通过 Ajax 可以在不刷新整个页面的情况下与服务器进行异步通信并部分网页。
  • Axios:是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js。它是一个第三方库,封装了 XMLHttpRequest 对象,提供了更简洁、更强大的 API 来处理 HTTP 请求。

2. 兼容性

  • Ajax:由于是基于 XMLHttpRequest 对象,在一些较旧的浏览器中也能使用,但不同浏览器对 XMLHttpRequest 的实现可能存在差异,需要进行一些兼容性处理。
  • Axios:在现代浏览器中表现良好,但在一些非常旧的浏览器(如 IE8 及以下)中不支持因为它依赖于 Promise。不过可以通过引入 polyfill 来解决这个问题。

3. API 易用性

  • Ajax:使用 XMLHttpRequest 对象时,代码相对复杂,需要手动处理很多细节,创建请求对象、设置请求方法、监听状态变化等。
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();
// 打开请求
xhr.open('GET', 'https://api.com/data', true);
// 监听状态变化
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};
// 发送请求
xhr.send();
  • Axios:提供了简洁的 API,使用起来更加方便。可以直接使用 axios.getaxios.post 等方法来发送请求,并且返回的是 Promise 对象,便于处理异步操作。
axios.get('https://api.example.com/data')
  .then(response => {
        console.log(response.data);
    })
  .catch(error => {
        console.error(error);
    });

4. 拦截器

  • Ajax:XMLHttpRequest 本身没有提供拦截器的功能,如果需要实现请求和响应的拦截,需要手动编写复杂的逻辑。
  • Axios:提供了请求拦截器和响应拦截器,可以在请求发送前和响应返回后进行一些统一的处理,如添加请求头、处理错误等。
// 请求拦截器
axios.interceptors.use(config => {
    // 在发送请求之前做些什么
    config.headers['Authorization'] = 'Bearer token';
    return config;
}, error => {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 响应拦截器
axios.interceptors.response.use(response => {
    // 对响应数据做点什么
    return response;
}, error => {
    // 对响应错误做点什么
    return Promise.reject(error);});

5. 取消请求

  • Ajax:取消 XMLHttpRequest 请求相对复杂,需要手动调用 abort 方法,并且在不同浏览器中可能存在兼容性问题。
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
// 取消请求
xhr.abort();
  • Axios:提供了更方便的取消请求的方式,可以使用 CancelToken 来取消请求。
const CancelToken = axios.CancelToken;
let cancel;

axios.get('https://api.example.com/data', {
    cancelToken: new CancelToken(function executor(c) {
        cancel = c;
    })
}).catch(function(thrown) {
    if (axios.isCancel(rown)) {
        console.log('Request canceled', thrown.message
    } else {
        // 处理错误
    }
});

// 取消请求
cancel('Operation canceled by the user.');

6. 数据转换

  • Ajax:需要手动对请求数据和响应数据进行转换,如将 JavaScript 对象转换为 JSON 字符串,将 JSON 字符串转换为 JavaScript 对象。
const data = { name: 'John', age: 30 };
const jsonData = JSON.stringify(data);
xhr.send(jsonData);xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        const responseData = JSON.parse(xhr.responseText);
        console.log(responseData);
    }
};
  • Axios:会自动对请求数据和响应数据进行转换,默认情况下会将请求数据转换为 JSON 字符串,将响应数据转换为 JavaScript 对象。
const data = { name: 'John', age: 30 };
axios.post('https://api.example.com/data', data)
  .then(response => {
        console.log(response.data);
    })
  .catch(error => {
        console.error(error);
    });

JavaScript有哪些数据类型?如何判断这些类型

文章未详细提及 JavaScript 数据类型及判断方法,下面为你详细介绍:

JavaScript 数据类型

JavaScript 数据类型分为基本数据类型和引用数据类型:

基本数据类型
  • Number:用于表示数字,包括整数和浮点数。例如:13.14
  • String:用于表示文本,由零个或多个 Unicode 字符组成。例如:"hello"'world'
  • Boolean:只有两个值,truefalse,用于逻辑判断。
  • Null:只有一个值 null,表示一个空对象指针。
  • Undefined:当一个变量已声明但未赋值,或者函数没有返回值时,其值为 undefined
  • Symbol(ES6 新增):表示独一无二的值,主要用于创建对象的私有属性和方法。
  • BigInt(ES2020 新增):用于表示任意大整数,通过在整数后面加 n 来表示。例如:123456789012345678901234567890n
引用数据类型
  • Object:是一种无序的数据集合,由键值对组成。例如:{ name: 'John', age: 30 }
  • Array:是一种特殊的对象,用于存储有序的数据集合。例如:[1, 2, 3]
  • Function:是一种可执行的代码块,可以接受参数并返回值。例如:function add(a, b) { return a + b; }

判断数据类型的方法

1. typeof 运算符

typeof 运算符返回一个表示数据类型的字符串,适用于基本数据类型,但对于 null 和引用数据类型有局限性。

console.log(typeof 123); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined // "undefined"
console.log(typeof Symbol()); // "symbol"
console.log(typeof 123n); // "bigint"

console.log(typeof null); // "object" (这是 JavaScript 的一个历史遗留问题)
console.log(typeof []); // "object"
console.log(typeof {}); // "object"
console.log(typeof function() {}); // "function"
2. instanceof 运算符

instanceof 运算符用于判断一个对象是否是某个构造函数的实例,主要用于引用数据类型。

const arr = [1, 2, 3];
console.log(arr instanceof Array); // true

const obj = { name: 'John' };
console.log(obj instanceof Object); // true

function Person() {}
const person = new Person();
console.log(person instanceof Person); // true
3. Object.prototype.toString.call() 方法

这是一种比较通用的方法,可以准确判断所有数据类型。

console.log(Object.prototype.toString.call(123)); // "[object Number]"
console.log(Object.prototype.toString.call("")); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(Symbol())); // "[object Symbol]"
console.log(Object.prototype.toString.call(123n)); // "[object BigInt]"

console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[ Object]"
console.log(Object.prototype.toString.call(function() {})); // "[object Function]"
4. Array.isArray() 方法

专门用于判断一个值是否为数组。

const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true

const obj = { name: 'John' };
console.log(Array.isArray(obj)); // false

Promise的理解

文章中未详细提及对 Promise 的理解,下面为你详细介绍:

什么是 Promise

Promise 是 JavaScript 中用于处理异步操作的一种对象,它可以避免回调地狱,让异步代码的编写和维护清晰和可管理。Promise 有状态:

  • pending(进行中):初始状态,既不是成功,也不是失败状态。
  • fulfilled(已成功):意味着操作成功完成。
  • rejected(已失败):意味着操作失败。

Promise 的状态一旦从 pending 变为 fulfilled 或者 rejected,就不能再改变。

基本用法

Promise 对象是通过构造函数创建的,构造函数接受一个执行器函数,该函数有两个参数:resolvereject,分别用于将 Promise状态从 pending 变为 fulfilledrejected

// 创建一个 Promise 对象
const promise = new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
        const randomNumber = Math.random();
        if (randomNumber < 0.5) {
            // 操作成功,将 Promise 状态变为 fulfilled
            resolve(randomNumber);
        } else {
            // 操作失败,将 Promise 状态变为 rejected
            reject(new Error('Random number is greater 0.5'));
        }
    }, 1000);
});

// 处理 Promise 的结果
promise
  .thenresult) => {
        console.log('操作成功,结果是:', result);
    })
  .catch((error) => {
        console.log('操作失败,错误信息是:', error.message);
    });

链式调用

Promisethen 方法会返回一个新的 Promise 对象,因此可以进行链式调用,避免回调地狱。

function async1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('异步操作 1 完成');
            resolve(1);
        }, 1000);
    });
}

function asyncOperation2(result) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('异步操作 2 完成,上一步结果:', result);
            resolve(result + 1);
        }, 1000);
    });
}

function asyncOperation3(result) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('异步操作 3 完成,上一步结果是:', result);
            resolve(result + 1);
        }, 1000);
    });
}

asyncOperation1()
  .then(asyncOperation2)
  .then(asyncOperation3)
  .then((finalResult) => {
        console.log('最终结果是:', finalResult);
    })
  .catcherror) => {
        console.log('发生错误:', error.message);
    });

静态方法

Promise 还提供了一些静态方法,如 Promise.allPromise.race 等。

Promise Promise.all 接受一个 Promise 数组作为参数,当所有 Promise 都成功时,返回一个新的 Promise,其结果是一个包含所有 Promise 结果的数组;如果其中任何一个 Promise 失败,则整个 Promise 失败。

const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
  .then((results) => {
        console.log('所有 Promise 都成功,结果是:', results);
    })
  .catch((error) => {
        console.log('有 Promise 失败,错误信息是:', error.message);
    });
Promise.race

Promise.race 接受一个 Promise 数组作为参数,返回一个新的 Promise,当数组中的任何一个 Promise率先改变状态时,该Promise的结果就是率先改变状态的Promise` 的结果。

const promise4 = new Promise((resolve) => {
    setTimeout(() => {
        resolve(4);
    }, 2000);
});

const promise5 = new Promise((resolve => {
    setTimeout(() => {
        resolve(5);
    }, 1000);
});

Promise.race([promise4, promise5])
  .then((result) => {
        console.log('率先完成的 Promise 结果是:', result);
    })
  .catch((error) => {
        console.log('有 Promise 失败,错误信息是:', error.message);
    });

通过使用 Promise,可以更优雅地处理异步操作,提高代码的可读性和可维护性。

ES6中let、const与ES5 var的区别

文章中提及是基础问题,但未详细说明区别,下面为详细介绍:

1. 块级作用域

  • var:在 ES5 中,var 声明的变量只有函数作用域和全局作用域,没有块级作用域。例如:
function testVar() {
    if (true) {
        var x = 10;
    }
    console.log(x); // 输出 10,因为 x 在函数作用域内有效
}
testVar();
  • letconst:ES6 引入了块级作用域,letconst 声明的变量只在其所在的块(如 if 语句、for 循环等)内有效。例如:
function testLet() {
    if (true) {
        let y = 20;
    }
    console.log(y); // 报错,y 未定义,因为 y 只在 if 块内有效
}
testLet();
function testConst() {
    if (true) {
        const z = 30;
    }
    console.log(z); // 报错,z 未定义,因为 z 只在 if 块内有效
}
testConst();

2. 变量提升

  • var:存在变量提升,即变量可以在声明之前使用,值为 undefined。例如:
console.log(a); // 输出 undefined
var a = 5;
  • letconst:不存在变量提升在声明之前使用会导致 ReferenceError。例如:
console.log(b); // 报错,ReferenceError: b is not defined
let b = 6;
console.log(c); // 报错,ReferenceError: c is not defined
const c = 7;

3. 重复声明

  • var:可以在同一作用域内重复声明同一个变量,的声明会覆盖前面的声明。例如:
var d = 8;
var d = 9;
console.log(d); // 输出 9
  • letconst:在同一作用域内不允许重复声明同一个变量。例如:
let e = 10;
let e = 11; // 报错,SyntaxError: Identifier 'e' has already been declared
const f = 12;
const f = 13; // 报错,SyntaxError: Identifier 'f' has already been declared

4. 赋值

  • varlet:声明的变量可以在声明后进行赋值和重新赋值。例如:
var g;
g = 14;
g = 15;
console.log(g); // 输出 15

let h;
h = 16;
h = 17;
console.log(h); // 输出 17
  • const:声明常量时必须进行初始化赋值,且一旦赋值后就不能再重新赋值(如果是引用类型,可以修改其内部属性)。例如:
const i 18;
// i = 19; // 报错,TypeError: Assignment to constant variable.

const obj = { key: 'value' };
obj.key = 'new value'; // 可以修改对象内部属性
console.log(obj); // 输出 { key: 'new value' }

综上所述,letconst 解决了 var 在作用域和变量提升方面的一些问题,使得变量的使用更加安全和可控。在实际中,推荐优先使用 letconst

如何实现[全网置灰]

文章中提到这是一个 CSS 考察的问题,并且作者表示自己写过相关文章,但未给出具体实现方法。下面为你详细介绍如何实现全网置灰:

思路

要实现全网置灰,主要是利用 CSS 的 filter 属性,该属性可以对元素应用图形效果(如模糊、饱和度等)。通过将 filter 属性为 grayscale(100%),可以将元素的颜色转换为灰度,从而实现置灰效果。

代码示例

以下是几种不同场景下实现全网置灰的代码示例:

1. 针对整个 HTML 页面

在 HTML 文件的 <style> 标签或者外部 CSS 文件中添加如下代码:

html {
    filter: grayscale(100%);
    -webkit-filter: grayscale(100%);
    -moz-filter: grayscale(100%);
    -ms-filter: grayscale(100%);
    -o-filter: grayscale(100%);
    filter: url("data:image/svg+xml;utf8,<svg xmlns'http://www.w3.org/200/svg\'><filter id=\'grayscale\'><feColorMatrix type=\'matrix\' values=\'0.3333 0.3333 0.3333 0 0 0.333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/></filter></svg>#grayscale");
    filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale1);
    --filter: grayscale(1);
}

上述代码中,filter: grayscale(100%) 是标准的 CSS3 语法,用于将元素转换为灰度。为了兼容不同的浏览器,还添加了 -webkit--moz--ms--o- 等浏览器前缀,以及针对旧版 IE 浏览器的 filter: progid:DXImageTransform.Microsoft.BasicImage(gr=1)

2. 使用 JavaScript 动态添加样式

如果你想通过 JavaScript 动态控制页面的置灰效果,可以使用以下代码:

<!DOCTYPE html
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>全网置灰示例</title>
</head>

<body>
    <h1>这是一个示例页面</h1>
    <button id="grayButton">置灰页面</button>
    <button id="normalButton">恢复正常</button>

    <script>
        const htmlElement = document.documentElement;
        const grayButton = document.getElementById('grayButton');
        const normalButton = document.getElementById('normalButton');

        grayButton.addEventListener('click', function () {
            htmlElement.style.filter = 'grayscale(100%)';
            htmlElement.style.webkitFilter = 'grayscale(100%)';
            htmlElement.style.mozFilter = 'grayscale(100%)';
            htmlElement.style.msFilter = 'grayscale(100%)';
            htmlElement.style.oFilter = 'grayscale(100%)';
        });

        normalButton.addEventListener('click', function () {
            htmlElement.style.filter = 'none';
            htmlElement.style.webkitFilter = 'none';
            htmlElement.style.mozFilter = 'none';
            htmlElement.style.msFilter = '';
            htmlElement.style.oFilter = 'none';
        });
    </script>
</body>

</html>

在上述代码中,通过 JavaScript 为按钮添加了点击事件监听器,当点击“置灰页面”按钮时,将 HTML 元素的 filter 属性设置为 grayscale(100%);当点击“恢复正常”按钮时,将 filter 属性设置为 `none。

注意事项

  • 不同浏览器对 filter 属性的支持程度可能有所,因此在实际使用时需要进行充分的测试。
  • 如果页面中存在一些特殊的元素(如 canvas、video等),可能需要单独对这些元素应用filter` 属性才能实现置灰效果。