用 Node 写过什么工具或 npm 包
在实际开发中,使用 Node 编写过多种实用工具和 npm 包。
自动化构建工具
开发了一个简单的自动化构建工具,用于处理前端项目的资源压缩和合并。在前端项目中,为了优化性能,需要对 CSS 和 JavaScript 文件进行压缩,减少文件体积,同时将多个小文件合并成一个大文件,减少 HTTP 请求。这个工具使用 Node 的 fs
模块进行文件的读写操作,通过 terser
库对 JavaScript 文件进行压缩,使用 cssnano
对 CSS 文件进行压缩。
const fs = require('fs');
const { minify } = require('terser');
const cssnano = require('cssnano');
async function minifyJS(inputPath, outputPath) {
const code = fs.readFileSync(inputPath, 'utf8');
const result = await minify(code);
fs.writeFileSync(outputPath, result.code);
}
async function minifyCSS(inputPath, outputPath) {
const css = fs.readFileSync(inputPath, 'utf8');
const { css: minifiedCss } = await cssnano.process(css);
fs.writeFileSync(outputPath, minifiedCss);
}
// 使用示例
minifyJS('src/main.js', 'dist/main.min.js');
minifyCSS('src/main.css', 'dist/main.min.css');
npm 包:日期格式化工具
编写了一个 npm 包,用于实现日期的格式化。在很多项目中,都需要将日期对象格式化成特定的字符串格式,如 YYYY-MM-DD HH:mm:ss
。这个包接收一个日期对象和一个格式化字符串作为参数,根据格式化字符串中的占位符将日期对象转换为对应的字符串。
function formatDate(date, format) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
module.exports = formatDate;
这个包可以方便地在其他项目中使用,只需要安装并引入即可。
React 用过没
在实际项目中使用过 React 进行前端开发。React 是一个用于构建用户界面的 JavaScript 库,由 Facebook 开发和维护,具有高效、灵活等特点。
项目实践
使用 React 开发过一个简单的博客系统。在这个项目中,将页面拆分成多个组件,每个组件负责特定的功能。例如,创建了 Header
组件用于显示博客的标题和导航栏,ArticleList
组件用于显示文章列表,ArticleDetail
组件用于显示文章的详细内容。通过组件化的开发方式,提高了代码的可维护性和复用性。
import React from 'react';
// Header 组件
const Header = () => {
return (
<header>
<h1>My Blog</h1>
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
</ul>
</nav>
</header>
);
};
// ArticleList 组件
const ArticleList = ({ articles }) => {
return (
<div>
{articles.map(article => (
<div key={article.id}>
<h2>{article.title}</h2>
<p>{article.summary}</p>
</div>
))}
</div>
);
};
// 主组件
const App = () => {
const articles = [
{ id: 1, title: 'Article 1', summary: 'This is the summary of article 1.' },
{ id: 2, title: 'Article 2', summary: 'This is the summary of article 2.' }
];
return (
<div>
<Header />
<ArticleList articles={articles} />
</div>
);
};
export default App;
状态管理
在复杂的项目中,使用了 Redux
进行状态管理。例如,在博客系统中,用户的登录状态、文章的收藏状态等都可以通过 Redux
进行统一管理。Redux
的单向数据流设计使得状态的变化更加可预测,便于调试和维护。
路由管理
使用 React Router
实现了单页面应用的路由功能。通过配置不同的路由规则,实现了不同页面之间的切换。例如,当用户访问 /article/1
时,显示文章 1 的详细内容。
Vue 双向绑定原理
Vue 的双向绑定是其核心特性之一,它使得数据的变化能够自动更新到视图,视图的变化也能自动更新到数据。其实现原理主要基于 Object.defineProperty()
方法和发布 - 订阅模式。
数据劫持
Vue 通过 Object.defineProperty()
方法对数据对象的属性进行劫持,当这些属性的值发生变化时,会触发相应的 setter
方法。例如:
const data = {
message: 'Hello, Vue!'
};
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`Getting value: ${val}`);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log(`Setting value: ${newVal}`);
val = newVal;
// 通知所有订阅者数据发生了变化
notify();
}
}
});
}
function notify() {
// 通知所有订阅者更新视图
console.log('Notifying subscribers...');
}
defineReactive(data, 'message', data.message);
// 读取数据
console.log(data.message);
// 修改数据
data.message = 'New message';
在上述代码中,defineReactive
函数对 data
对象的 message
属性进行了劫持,当读取 message
属性时,会触发 getter
方法,当修改 message
属性时,会触发 setter
方法。
发布 - 订阅模式
Vue 内部使用了发布 - 订阅模式来实现数据变化的通知。每个数据对象都有一个 Dep
对象,它维护了一个订阅者列表。当数据发生变化时,Dep
对象会通知所有订阅者更新视图。同时,每个视图节点都有一个 Watcher
对象,它会订阅数据的变化。当数据变化时,Watcher
对象会收到通知并更新视图。
编译过程
在 Vue 实例初始化时,会对模板进行编译,将模板中的指令和表达式解析成对应的 Watcher
对象。例如,当模板中有 v-model
指令时,会创建一个 Watcher
对象来监听输入框的变化,并将变化同步到数据对象中。
了解 react 原理的吗,比如 fiber、hooks(讲了下 fiber 的渲染机制、常用的 hooks 以及与类组件的区别)
Fiber 渲染机制
React Fiber 是 React 16.x 版本之后引入的协调算法,它的主要目标是解决旧版协调算法在处理大型复杂组件树时可能出现的性能问题。旧版的协调算法是基于递归的,一旦开始渲染就会一直执行直到完成,这可能会导致页面卡顿。
Fiber 采用了一种增量渲染的方式,将渲染过程拆分成多个小的任务单元。每个任务单元可以暂停、恢复和重新执行。Fiber 会根据任务的优先级来决定先执行哪些任务,例如用户交互产生的任务优先级较高,会优先执行。
Fiber 渲染过程主要分为两个阶段:协调阶段和提交阶段。在协调阶段,Fiber 会遍历组件树,比较新旧虚拟 DOM 的差异,这个过程是可以中断的。在提交阶段,Fiber 会将协调阶段计算出的差异应用到实际的 DOM 上,这个过程是不可中断的。
常用的 Hooks
-
useState
:用于在函数组件中添加状态。例如:import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> );
};
export default Counter;
-
useEffect
:用于处理副作用,如数据获取、订阅和 DOM 操作等。例如:import React, { useState, useEffect } from 'react';
const DataFetching = () => {
const [data, setData] = useState(null);useEffect(() => { fetch('https://api.example.com/data') .then(response => response.json()) .then(data => setData(data)); }, []); return ( <div> {data ? <p>{data.message}</p> : <p>Loading...</p>} </div> );
};
export default DataFetching;
-
useContext
:用于在组件之间共享数据,避免逐层传递props
。
与类组件的区别
- 语法:类组件使用
class
关键字定义,需要继承React.Component
或React.PureComponent
,并实现render
方法。函数组件则是普通的 JavaScript 函数。 - 状态管理:类组件使用
this.state
和this.setState
来管理状态,而函数组件使用useState
Hook。 - 生命周期方法:类组件有多个生命周期方法,如
componentDidMount
、componentDidUpdate
等。函数组件通过useEffect
Hook 来模拟这些生命周期方法。 - 代码复用:Hooks 使得代码复用更加方便,可以将逻辑封装在自定义 Hook 中,而类组件的代码复用相对较复杂。
跨域?如何解决?
跨域的概念
跨域是指浏览器从一个域名的网页去请求另一个域名的资源时,由于浏览器的同源策略,会受到限制。同源策略是指浏览器只允许访问同源(协议、域名、端口都相同)的资源,以保证用户信息的安全。例如,在 http://example.com
域名下的页面无法直接请求 http://another.com
域名下的资源。
解决跨域的方法
-
JSONP(JSON with Padding) :JSONP 是一种古老的跨域解决方案,它的原理是利用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> </head> <body> <script> function handleData(data) { console.log(data); } </script> <script src="http://example.com/api?callback=handleData"></script> </body> </html><script>
标签的src
属性不受同源策略限制的特点。服务器返回的数据会被包装在一个回调函数中,客户端通过动态创建<script>
标签来请求服务器数据,并在回调函数中处理返回的数据。
服务器端代码示例(Node.js):
const http = require('http');
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const callback = url.searchParams.get('callback');
const data = { message: 'Hello, JSONP!' };
const response = `${callback}(${JSON.stringify(data)})`;
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(response);
});
server.listen(3000, () => {
console.log('Server is running on port 3000');
});
-
CORS(Cross-Origin Resource Sharing) :CORS 是一种现代的跨域解决方案,它通过在服务器端设置响应头来允许跨域请求。服务器可以设置
Access-Control-Allow-Origin
头来指定允许访问的域名,还可以设置其他相关的头信息,如Access-Control-Allow-Methods
、Access-Control-Allow-Headers
等。const express = require('express');
const app = express();app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});app.get('/api', (req, res) => {
res.json({ message: 'Hello, CORS!' });
});app.listen(3000, () => {
console.log('Server is running on port 3000');
}); -
代理服务器 :在开发环境中,可以使用代理服务器来解决跨域问题。例如,在使用
create-react-app
创建的项目中,可以在package.json
中配置代理。{
"name": "my-app",
"proxy": "http://example.com"
}
这样,当项目中的请求路径匹配到代理服务器的地址时,请求会被转发到代理服务器,从而绕过浏览器的同源策略。在生产环境中,可以使用 Nginx 等服务器软件来配置反向代理。
get 请求和 post 请求的区别、优缺点、使用场景
区别
从 HTTP 协议层面来看,GET 和 POST 是两种不同的请求方法,它们在语义、参数传递、数据安全等方面存在明显区别。
语义上,GET 请求通常用于获取资源,而 POST 请求用于向服务器提交数据,可能会对服务器上的资源产生影响,比如创建新的资源。
参数传递方面,GET 请求会将参数附加在 URL 的查询字符串中,例如 http://example.com/api?param1=value1¶m2=value2
。而 POST 请求则将参数放在请求体中,不会显示在 URL 里。
在数据安全上,由于 GET 请求的参数暴露在 URL 中,所以不适合传递敏感信息,如密码等。而 POST 请求的参数在请求体中,相对更安全一些。
优缺点
GET 请求的优点是简单方便,可直接在浏览器地址栏输入 URL 发起请求,并且请求可以被缓存,提高响应速度。缺点是参数有长度限制,不同浏览器和服务器的限制不同,一般为 2KB 到 8KB 不等,而且安全性较低。
POST 请求的优点是可以传递大量数据,不受 URL 长度限制,安全性较高,适合传递敏感信息。缺点是请求相对复杂,不会被缓存,每次请求都会向服务器发送数据。
使用场景
GET 请求适用于获取数据,如查询商品信息、搜索文章等。例如,在一个电商网站中,用户通过搜索框输入关键词,使用 GET 请求向服务器获取相关商品列表。
// 使用 fetch 发起 GET 请求
fetch('http://example.com/api/products?keyword=phone')
.then(response => response.json())
.then(data => console.log(data));
POST 请求适用于提交数据,如用户注册、登录、提交表单等。例如,用户在注册页面填写信息后,使用 POST 请求将信息发送到服务器进行处理。
// 使用 fetch 发起 POST 请求
const formData = {
username: 'testuser',
password: 'testpassword'
};
fetch('http://example.com/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => console.log(data));
CSS 左固定右自适应
浮动实现
使用浮动布局可以实现左固定右自适应的效果。将左侧元素设置为浮动元素,右侧元素设置 margin-left
为左侧元素的宽度。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.left {
float: left;
width: 200px;
background-color: #f0f0f0;
}
.right {
margin-left: 200px;
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="left">Left fixed</div>
<div class="right">Right adaptive</div>
</body>
</html>
Flexbox 实现
Flexbox 是一种现代的布局方式,使用 display: flex
可以轻松实现左固定右自适应。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.container {
display: flex;
}
.left {
width: 200px;
background-color: #f0f0f0;
}
.right {
flex: 1;
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<div class="left">Left fixed</div>
<div class="right">Right adaptive</div>
</div>
</body>
</html>
Grid 实现
CSS Grid 也是一种强大的布局方式,通过设置网格模板列可以实现左固定右自适应。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.container {
display: grid;
grid-template-columns: 200px 1fr;
}
.left {
background-color: #f0f0f0;
}
.right {
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<div class="left">Left fixed</div>
<div class="right">Right adaptive</div>
</div>
</body>
</html>
CSS3 实现幻灯片
原理
CSS3 实现幻灯片主要利用 @keyframes
动画和 input
元素的 checked
状态来控制幻灯片的切换。
代码示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.slider {
width: 500px;
height: 300px;
position: relative;
overflow: hidden;
}
.slides {
width: 300%;
height: 100%;
display: flex;
transition: transform 0.5s ease-in-out;
}
.slide {
width: 33.333%;
height: 100%;
}
.slide:nth-child(1) {
background-color: #f00;
}
.slide:nth-child(2) {
background-color: #0f0;
}
.slide:nth-child(3) {
background-color: #00f;
}
input[type="radio"] {
display: none;
}
#slide1:checked ~ .slides {
transform: translateX(0);
}
#slide2:checked ~ .slides {
transform: translateX(-33.333%);
}
#slide3:checked ~ .slides {
transform: translateX(-66.666%);
}
.navigation {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
}
.navigation label {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #fff;
margin: 0 5px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="slider">
<input type="radio" name="slide" id="slide1" checked>
<input type="radio" name="slide" id="slide2">
<input type="radio" name="slide" id="slide3">
<div class="slides">
<div class="slide"></div>
<div class="slide"></div>
<div class="slide"></div>
</div>
<div class="navigation">
<label for="slide1"></label>
<label for="slide2"></label>
<label for="slide3"></label>
</div>
</div>
</body>
</html>
在上述代码中,通过 input
元素的 checked
状态来控制 .slides
元素的 transform
属性,从而实现幻灯片的切换。同时,使用 @keyframes
动画可以实现更复杂的切换效果。
CSS 中如何实现水平垂直居中?请列举至少四种方法。
方法一:Flexbox 实现
Flexbox 是一种简单高效的布局方式,可以轻松实现水平垂直居中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.parent {
display: flex;
justify-content: center;
align-items: center;
width: 300px;
height: 300px;
background-color: #f0f0f0;
}
.child {
width: 100px;
height: 100px;
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="parent">
<div class="child"></div>
</div>
</body>
</html>
方法二:Grid 实现
CSS Grid 也可以实现水平垂直居中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.parent {
display: grid;
place-items: center;
width: 300px;
height: 300px;
background-color: #f0f0f0;
}
.child {
width: 100px;
height: 100px;
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="parent">
<div class="child"></div>
</div>
</body>
</html>
方法三:绝对定位和负边距实现
使用绝对定位和负边距可以实现水平垂直居中,但要求子元素的宽度和高度已知。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.parent {
position: relative;
width: 300px;
height: 300px;
background-color: #f0f0f0;
}
.child {
position: absolute;
top: 50%;
left: 50%;
width: 100px;
height: 100px;
margin-top: -50px;
margin-left: -50px;
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="parent">
<div class="child"></div>
</div>
</body>
</html>
方法四:绝对定位和 transform 实现
使用绝对定位和 transform
属性可以实现水平垂直居中,且不需要知道子元素的宽度和高度。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.parent {
position: relative;
width: 300px;
height: 300px;
background-color: #f0f0f0;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="parent">
<div class="child"></div>
</div>
</body>
</html>
实现两列定宽中间自适应布局
Flexbox 实现
使用 Flexbox 可以方便地实现两列定宽中间自适应布局。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.container {
display: flex;
}
.left,
.right {
width: 200px;
background-color: #f0f0f0;
}
.middle {
flex: 1;
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<div class="left">Left fixed</div>
<div class="middle">Middle adaptive</div>
<div class="right">Right fixed</div>
</div>
</body>
</html>
Grid 实现
CSS Grid 也能很好地实现该布局。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.container {
display: grid;
grid-template-columns: 200px 1fr 200px;
}
.left,
.right {
background-color: #f0f0f0;
}
.middle {
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<div class="left">Left fixed</div>
<div class="middle">Middle adaptive</div>
<div class="right">Right fixed</div>
</div>
</body>
</html>
浮动实现
使用浮动布局也可以实现两列定宽中间自适应布局,但需要处理浮动元素的清除问题。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.left {
float: left;
width: 200px;
background-color: #f0f0f0;
}
.right {
float: right;
width: 200px;
background-color: #f0f0f0;
}
.middle {
margin: 0 200px;
background-color: #e0e0e0;
}
.clearfix::after {
content: "";
display: block;
clear: both;
}
</style>
</head>
<body>
<div class="clearfix">
<div class="left">Left fixed</div>
<div class="right">Right fixed</div>
<div class="middle">Middle adaptive</div>
</div>
</body>
</html>
介绍一下 HTTP 和 HTTPS 的区别,为什么 HTTPS 更安全?

HTTP 和 HTTPS 有以下区别:
- 连接方式:HTTP 连接相对简单,客户端向服务器发送请求,服务器响应请求后连接即结束。HTTPS 在连接时需要先进行 SSL/TLS 握手,以验证服务器身份和协商加密算法等参数,然后再进行数据传输。
- 端口:HTTP 默认使用 80 端口,而 HTTPS 默认使用 443 端口。
- 安全性:HTTP 的数据以明文形式传输,容易被监听、篡改和窃取,存在较大的安全风险。HTTPS 对数据进行加密处理,使数据在传输过程中变成密文,只有合法的接收方才能解密和读取,大大提高了数据的安全性。
- 证书:HTTPS 需要服务器拥有由权威证书颁发机构颁发的数字证书,客户端可以通过验证证书来确认服务器的身份是否可信。HTTP 则不需要证书。
HTTPS 更安全的原因主要有以下几点:
- 数据加密:通过 SSL/TLS 协议对数据进行加密,将数据转化为密文传输,即使数据被拦截,攻击者也难以理解其中的内容。例如,用户在 HTTPS 的购物网站上输入的账号密码等信息,会被加密后传输,防止被窃取。
- 身份验证:服务器的数字证书可以验证服务器的身份,确保用户连接到的是合法的服务器,而不是被假冒的服务器。这可以防止用户遭受钓鱼攻击,避免用户的信息被恶意网站骗取。
- 数据完整性保护:SSL/TLS 协议会对传输的数据进行完整性校验,通过添加校验码等方式,确保数据在传输过程中没有被篡改。如果数据被篡改,接收方可以检测到并拒绝接受。
在浏览器输入一个网址按下回车后,发生了什么?请详细介绍渲染过程。
当在浏览器中输入一个网址按下回车后,会经历以下一系列过程,其中渲染过程是关键部分:
- 域名解析:浏览器首先会检查本地的 DNS 缓存,如果缓存中存在该域名对应的 IP 地址,则直接使用;如果没有,则向本地 DNS 服务器发送请求,本地 DNS 服务器再向根 DNS 服务器、顶级域名服务器等逐级查询,最终获取到目标域名对应的 IP 地址。
- 建立连接:浏览器与服务器通过 TCP 协议建立连接,经历三次握手过程,确保连接的可靠性。
- 发送请求:浏览器根据用户输入的网址和请求方法(如 GET、POST 等),构建 HTTP 请求报文,并将其发送给服务器。请求报文中包含请求头字段,如 User - Agent、Accept 等,可能还包含请求体(如 POST 请求时)。
- 接收响应:服务器接收到请求后,根据请求的内容进行处理,生成 HTTP 响应报文,包含响应头字段(如 Content - Type、Content - Length 等)和响应体(通常是 HTML、CSS、JavaScript 等资源),然后将响应发送给浏览器。
接下来是渲染过程:
- 解析 HTML :浏览器接收到 HTML 文件后,开始解析 HTML 代码,构建 DOM 树。它会从根标签开始,逐个解析标签,并将其转化为 DOM 节点,形成树形结构。例如,遇到
<div>
标签就创建一个对应的 DOM 节点,并将其子节点按照顺序添加到该节点下。 - 解析 CSS:同时,浏览器会解析 CSS 样式表,构建 CSSOM 树。它会根据 CSS 规则,将样式与对应的 DOM 节点关联起来,确定每个元素的样式属性。
- 构建渲染树 :将 DOM 树和 CSSOM 树结合起来,构建渲染树。渲染树只包含需要显示的节点和其样式信息,像
display: none
的元素不会出现在渲染树中。 - 布局计算:根据渲染树,浏览器计算每个节点在页面中的位置和大小,确定元素的布局。例如,根据盒模型计算每个元素的外边距、内边距、边框等,以及它们在文档流中的位置。
- 绘制渲染:最后,浏览器根据布局计算的结果,将各个元素绘制到屏幕上。它会按照从左到右、从上到下的顺序,逐个绘制元素,包括文本、图像、边框等,最终呈现出完整的页面。
介绍一下跨域以及 CORS 为什么分简单请求和非简单请求,JSONP 的原理是什么?
跨域是指浏览器试图从一个源(协议、域名、端口号均相同)去请求另一个不同源的资源。由于浏览器的同源策略限制,不允许跨域访问资源,这是一种安全机制,防止恶意网站窃取用户信息。
CORS(跨源资源共享)区分简单请求和非简单请求的原因如下:
- 简单请求 :对于一些常见的、对服务器影响较小的请求,如 GET、HEAD、POST(Content - Type 为 application/x - www - form - urlencoded、multipart/form - data 或 text/plain)请求,浏览器会直接发送请求,并在响应头中检查是否包含允许跨域的相关信息。如果服务器返回的响应头中包含
Access - Control - Allow - Origin
等相关字段,允许当前源访问,则浏览器允许该请求,否则会报错。这样可以简化常见场景下的跨域请求流程,提高效率。 - 非简单请求 :对于 PUT、DELETE 等方法,或者 Content - Type 为其他类型(如 application/json)的 POST 请求等,浏览器会先发送一个预检请求(OPTIONS 请求),询问服务器是否允许该类型的请求。服务器通过响应头中的
Access - Control - Allow - Methods
、Access - Control - Allow - Headers
等字段来告知浏览器是否允许该请求。如果允许,浏览器才会发送实际的请求;如果不允许,浏览器则会阻止该请求。这样做是为了让服务器有机会对可能对服务器产生较大影响的非简单请求进行验证和授权,增强安全性。
JSONP 的原理是利用<script>
标签的跨域特性。由于浏览器允许<script>
标签从不同源加载脚本,JSONP 通过动态创建<script>
标签,将请求的 URL 作为<script>
标签的src
属性值。服务器接收到请求后,将数据以 JavaScript 函数调用的形式返回,例如callback({data: 'xxx'})
,其中callback
是客户端传递给服务器的回调函数名。浏览器接收到响应后,会执行该脚本,从而触发回调函数,将服务器返回的数据传递给回调函数进行处理,实现了跨域获取数据的目的。
打包时如何压缩图片?
在项目打包过程中,压缩图片是优化项目性能、减少文件体积的重要手段,以下是一些常见的图片压缩方法:
-
使用图像压缩工具 :可以使用专门的图像压缩工具,如 TinyPNG、ImageOptim 等。这些工具通常采用先进的压缩算法,能够在不明显损失图像质量的前提下,大幅减小图片文件的大小。例如,TinyPNG 利用有损压缩算法,对 PNG 和 JPEG 图片进行优化,通过减少颜色数量、压缩图像数据等方式来压缩图片。可以在项目构建脚本中集成这些工具,在打包时自动对图片进行压缩。比如在 Node.js 项目中,可以使用
tiny - png - npm
插件,在构建脚本中添加相应的任务,指定要压缩的图片目录,插件会自动遍历目录下的图片并进行压缩。 -
配置打包工具进行压缩 :大多数前端打包工具,如 Webpack、Parcel 等,都有相应的图片压缩插件。以 Webpack 为例,可以使用
image - webpack - loader
插件来实现图片压缩。首先安装该插件,然后在 Webpack 的配置文件中进行如下配置:module.exports = {
module: {
rules: [
{
test: /.(png|jpe?g|gif)$/i,
use: [
{
loader: 'image - webpack - loader',
options: {
mozjpeg: {
progressive: true,
quality: 65
},
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
},
},
],
},
],
},
};
上述配置中,针对不同类型的图片设置了相应的压缩选项,如对 JPEG 图片使用mozjpeg
设置压缩质量为 65,对 PNG 图片使用pngquant
设置质量范围等。这样在 Webpack 打包时,就会自动对符合条件的图片进行压缩。
- 采用图片格式转换 :根据图片的使用场景,选择合适的图片格式也可以达到压缩的目的。例如,对于一些色彩丰富的照片,可以使用 JPEG 格式,它具有较高的压缩比,但可能会有一定的质量损失;对于图标等简单图形,使用 SVG 格式可能更好,SVG 是矢量图形,无论如何缩放都不会失真,且文件体积通常较小。另外,WebP 格式是一种新兴的图片格式,它在相同的视觉质量下,文件大小比 JPEG 和 PNG 都要小很多,可以将部分图片转换为 WebP 格式来减少体积。可以使用工具如
cwebp
来进行图片格式的转换,在打包过程中通过脚本调用该工具将图片转换为 WebP 格式,并在页面中使用<picture>
标签来兼容不同浏览器对 WebP 格式的支持。
热更新是如何实现的?
热更新是指在不重新加载整个页面或应用程序的情况下,实时更新部分代码或资源,以提供更好的用户体验和开发效率。以下是一些常见的热更新实现方式:
在 Web 开发中,基于 Webpack 的热模块替换(HMR)是一种常用的方式。Webpack-dev-server 会在开发环境中启动一个本地服务器,当代码发生变化时,它会监测到文件的更改,并将更新的模块发送到浏览器。浏览器通过 WebSocket 与服务器建立连接,接收更新的模块信息。然后,HMR 运行时会在浏览器中找到对应的模块,并将其替换为新的模块,同时保留应用程序的状态。例如,当修改了一个 CSS 文件,Webpack 会将更新后的 CSS 模块发送到浏览器,浏览器会直接更新页面的样式,而不会重新加载整个页面。对于 JavaScript 模块,HMR 会根据模块的依赖关系,智能地更新相关模块,确保应用程序能够正确运行。
在移动端开发中,以 React Native 为例,热更新可以通过 React Native 的热加载功能实现。当代码发生变化时,开发服务器会将更新的 JavaScript bundle 发送到移动设备。移动设备上的 React Native 应用通过与开发服务器的连接,接收并解析更新的 bundle。然后,React Native 框架会根据更新的内容,更新相应的组件和状态,实现界面的实时更新。这样,开发者可以在不重新安装应用的情况下,快速看到代码更改的效果。
另外,一些混合开发框架如 Cordova 也支持热更新。通常是通过在应用中集成一个热更新插件,该插件会在应用启动时检查服务器上是否有更新的资源。如果有更新,它会将更新的文件下载到本地,并替换旧的文件。然后,应用会重新加载相关的页面或模块,以应用更新后的内容。这种方式可以实现对 HTML、CSS 和 JavaScript 等资源的热更新,让应用能够在运行时及时获取到最新的功能和修复。
在一些桌面应用开发中,如 Electron 应用,也可以实现热更新。Electron 应用通常由前端的 Web 技术(HTML、CSS、JavaScript)和后端的 Node.js 组成。可以利用 Electron 的模块热替换功能,类似于 Webpack 的 HMR,当代码发生变化时,通过特定的机制更新相关模块,实现热更新。例如,使用electron - hot
等插件来实现 Electron 应用的热更新,它会监测代码的变化,并在不重启应用的情况下更新界面和逻辑。
不同的平台和框架实现热更新的具体方式和细节可能会有所不同,但总体思路都是通过监测代码或资源的变化,将更新的内容发送到运行环境中,并在不影响整体应用状态的情况下替换旧的模块或资源,以实现实时更新的效果。
Vue 的双向绑定的核心设计原理是什么?
Vue 的双向绑定是其重要特性之一,它允许数据的变化自动更新到视图,视图的变化也能自动更新到数据,核心原理基于 Object.defineProperty () 方法和发布 - 订阅模式。
在 Vue 实例创建时,会对 data 选项中的所有属性使用 Object.defineProperty () 进行数据劫持。该方法可以劫持对象属性的 getter 和 setter,当属性被访问时触发 getter,当属性被修改时触发 setter。例如:
let data = {
message: 'Hello, Vue!'
};
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('Getting value:', val);
return val;
},
set(newVal) {
if (newVal!== val) {
console.log('Setting value:', newVal);
val = newVal;
// 通知订阅者更新视图
}
}
});
}
defineReactive(data, 'message', data.message);
在上述代码中,当访问 data.message 时会触发 getter 方法,修改 data.message 时会触发 setter 方法。
Vue 还使用了发布 - 订阅模式。每个数据对象都有一个 Dep 对象,它是一个依赖收集器,维护一个订阅者数组。当有地方访问该数据属性时,会触发 getter 方法,此时会将订阅者(Watcher 对象)添加到 Dep 的订阅者数组中。当数据属性发生变化时,会触发 setter 方法,Dep 对象会通知所有订阅者更新视图。Watcher 对象会重新计算表达式的值,并更新到对应的 DOM 节点上。
在模板编译阶段,Vue 会将模板中的指令和表达式解析成对应的 Watcher 对象。例如,使用 v - model 指令时,会创建一个 Watcher 对象来监听输入框的变化,并将变化同步到数据对象中;同时,当数据对象的属性发生变化时,也会通过 Watcher 对象更新输入框的值。
Vue 组件传值的方式有哪些,具体业务场景下如何选择?
Vue 组件传值方式主要有以下几种:
父传子:props
通过 props 可以将父组件的数据传递给子组件。在父组件中,通过在子组件标签上绑定属性来传递数据;在子组件中,使用 props 选项来接收数据。例如:
<!-- 父组件 -->
<template>
<ChildComponent :message="parentMessage" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Hello from parent'
};
}
};
</script>
<!-- 子组件 -->
<template>
<p>{{ message }}</p>
</template>
<script>
export default {
props: ['message']
};
</script>
适用于父组件需要向子组件传递数据的场景,如展示列表项的详细信息等。
子传父:$emit
子组件可以通过 $emit 触发自定义事件,将数据传递给父组件。在子组件中,使用 $emit 方法触发事件并传递数据;在父组件中,监听该事件并接收数据。例如:
<!-- 子组件 -->
<template>
<button @click="sendDataToParent">Send Data</button>
</template>
<script>
export default {
methods: {
sendDataToParent() {
this.$emit('childEvent', 'Data from child');
}
}
};
</script>
<!-- 父组件 -->
<template>
<ChildComponent @childEvent="handleChildEvent" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
handleChildEvent(data) {
console.log(data);
}
}
};
</script>
适用于子组件需要向父组件反馈信息的场景,如用户在子组件中的操作结果等。
事件总线(Event Bus)
创建一个全局的事件总线对象,用于组件之间的通信。任何组件都可以在该事件总线上触发事件和监听事件。例如:
// event-bus.js
import Vue from 'vue';
export const eventBus = new Vue();
<!-- 发送组件 -->
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
import { eventBus } from './event-bus.js';
export default {
methods: {
sendMessage() {
eventBus.$emit('messageEvent', 'Message from sender');
}
}
};
</script>
<!-- 接收组件 -->
<template>
<p>{{ receivedMessage }}</p>
</template>
<script>
import { eventBus } from './event-bus.js';
export default {
data() {
return {
receivedMessage: ''
};
},
created() {
eventBus.$on('messageEvent', (message) => {
this.receivedMessage = message;
});
}
};
</script>
适用于非父子关系组件之间的通信,如不同模块之间的交互。
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。适用于大型项目中多个组件共享状态的场景,如用户登录状态、购物车数据等。
Vue 的渲染过程是怎样的?
Vue 的渲染过程主要包括以下几个步骤:
初始化实例
当创建一个 Vue 实例时,Vue 会初始化实例的生命周期钩子、事件系统、响应式数据等。例如,调用 beforeCreate 和 created 钩子函数,在 created 钩子中可以访问 data、methods 等选项。
模板编译
Vue 会将模板字符串编译成渲染函数(render function)。这个过程分为三个阶段:
- 解析阶段:将模板字符串解析成抽象语法树(AST),通过正则表达式或解析器将模板中的标签、属性、文本等解析成树状结构。
- 优化阶段:对 AST 进行静态节点标记,标记出那些在渲染过程中不会发生变化的节点,这样在后续的渲染过程中可以跳过这些节点的比较,提高性能。
- 生成阶段:将优化后的 AST 转换为渲染函数代码。
挂载阶段
调用 beforeMount 钩子函数,然后创建虚拟 DOM 树。虚拟 DOM 是一种轻量级的 JavaScript 对象,它是真实 DOM 的抽象表示。通过调用渲染函数生成虚拟 DOM 树,然后将虚拟 DOM 树转换为真实 DOM 并插入到页面中。最后调用 mounted 钩子函数,表示组件已经挂载到页面上。
数据更新与重新渲染
当数据发生变化时,会触发数据劫持的 setter 方法,通知所有依赖该数据的 Watcher 对象。Watcher 对象会重新计算表达式的值,并更新对应的虚拟 DOM 树。然后通过对比新旧虚拟 DOM 树的差异(diff 算法),只更新需要更新的真实 DOM 节点,而不是重新渲染整个页面。这个过程中会调用 beforeUpdate 和 updated 钩子函数。
销毁阶段
当组件需要销毁时,会调用 beforeDestroy 钩子函数,然后销毁组件的所有事件监听器和子实例,最后调用 destroyed 钩子函数。
Vue 中 v-for 指令的 key 属性有什么作用?
在 Vue 中,v - for 指令用于渲染列表数据。key 属性在使用 v - for 指令时起着重要的作用,主要有以下几个方面:
提高渲染效率
Vue 在更新使用 v - for 渲染的列表时,会使用 diff 算法来比较新旧虚拟 DOM 树的差异。当列表中的元素有唯一的 key 时,Vue 可以根据 key 来准确地识别每个元素,从而进行更高效的更新操作。如果没有 key,Vue 会采用一种简单的默认策略,即就地复用元素,可能会导致一些不必要的重新渲染。例如,当在列表头部插入一个新元素时,如果没有 key,Vue 会将原列表中的元素依次向后移动,而不是直接插入新元素;而有 key 时,Vue 可以准确地识别新元素并插入到正确的位置。
保持组件状态
当列表中的元素是组件时,key 可以帮助 Vue 保持组件的状态。如果没有 key,当列表数据发生变化时,Vue 可能会复用已有的组件实例,导致组件的状态被错误地保留。而有 key 时,Vue 会根据 key 来判断是否需要创建新的组件实例,从而保证组件状态的正确性。例如,在一个待办事项列表中,每个待办事项都是一个组件,使用唯一的 key 可以确保当某个待办事项的状态发生变化时,不会影响其他待办事项的组件状态。
避免过渡效果异常
在使用过渡效果时,key 可以避免过渡效果出现异常。当列表中的元素有唯一的 key 时,Vue 可以正确地处理元素的插入、删除和移动等操作,从而保证过渡效果的正常显示。如果没有 key,过渡效果可能会出现闪烁或异常的情况。
示例代码
<template>
<ul>
<li v - for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
};
}
};
</script>
在上述代码中,使用 item.id 作为 key,确保每个列表项都有唯一的标识。
Webpack 的原理是什么?
Webpack 是一个模块打包工具,其核心原理是通过分析模块之间的依赖关系,将所有模块打包成一个或多个文件。主要过程如下:
入口文件分析
Webpack 从配置文件中指定的入口文件开始,递归地分析入口文件及其依赖的所有模块。它会读取每个模块的内容,并根据模块的类型(如 JavaScript、CSS、图片等)进行相应的处理。
模块解析
Webpack 会根据配置的解析规则,将模块路径解析为实际的文件路径。例如,对于相对路径和绝对路径的处理,以及对模块后缀名的处理。同时,Webpack 支持使用不同的解析器(如 babel - loader、css - loader 等)来处理不同类型的模块。
模块转换
对于不同类型的模块,Webpack 使用相应的 loader 进行转换。loader 是 Webpack 的核心特性之一,它可以将非 JavaScript 模块转换为 JavaScript 模块,或者对 JavaScript 模块进行转换和优化。例如,使用 babel - loader 将 ES6+ 代码转换为 ES5 代码,使用 css - loader 和 style - loader 处理 CSS 文件。示例配置如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel - loader',
options: {
presets: ['@babel/preset - env']
}
}
},
{
test: /\.css$/,
use: ['style - loader', 'css - loader']
}
]
}
};
依赖图构建
在分析和转换模块的过程中,Webpack 会构建一个依赖图,记录模块之间的依赖关系。这个依赖图可以帮助 Webpack 确定模块的加载顺序和打包方式。
代码分割与打包
根据配置的输出选项,Webpack 会将所有模块打包成一个或多个文件。它支持代码分割功能,可以将代码分割成多个小块,实现按需加载,提高应用的性能。例如,使用动态导入(import ())语法可以实现代码的按需加载。
输出文件
最后,Webpack 将打包好的文件输出到指定的目录中。可以通过配置输出文件名、路径等选项来控制输出文件的格式和位置。
Webpack 中的 alias 如何进行配置?
在 Webpack 中,alias
用于创建模块路径的别名,通过配置 alias
,可以更方便地引用模块,提高代码的可维护性和可读性。
在 Webpack 的配置文件(通常是 webpack.config.js
)中进行 alias
的配置。以下是一个基本的示例:
const path = require('path');
module.exports = {
// 其他配置项...
resolve: {
alias: {
// 将 @ 符号映射到 src 目录
'@': path.resolve(__dirname, 'src'),
// 将 components 映射到 src/components 目录
'components': path.resolve(__dirname, 'src/components'),
// 可以根据需要添加更多的别名映射
'utils': path.resolve(__dirname, 'src/utils')
}
}
};
在上述代码中,使用了 path.resolve()
方法来获取绝对路径。__dirname
表示当前文件所在的目录。通过这样的配置,在项目中就可以使用 @
来代替 src
目录的路径,使用 components
来代替 src/components
目录的路径,以此类推。
例如,在一个组件中需要引入 src/components/Button.jsx
文件,就可以这样写:
import Button from 'components/Button';
而不是使用相对路径 ../../components/Button
,这样可以避免因文件位置变动而导致的路径错误,同时也使代码更加简洁和易读。
还可以为一些常用的模块或库设置别名。比如,将 react
库的路径设置为一个特定的版本路径,或者将一些第三方组件库的入口文件设置为别名,方便在项目中引用。
Webpack 的 proxy 和 object.defineProperty () 的原理是什么,如何实现对对象属性的监听?
- Webpack 的 proxy 原理
Webpack 的 proxy
主要用于解决开发环境中的跨域问题。它的原理是在开发服务器(如 webpack-dev-server
)上设置一个代理服务器,当浏览器向服务器发送请求时,如果请求的目标地址与当前服务器的地址不同(即跨域),代理服务器会拦截这个请求,并将其转发到目标服务器上,然后将目标服务器返回的结果再转发给浏览器。这样,对于浏览器来说,所有的请求都是在同一个域下进行的,从而避免了跨域限制。
在 Webpack 中,可以通过在 devServer
配置项中设置 proxy
来实现代理。例如:
module.exports = {
// 其他配置项...
devServer: {
proxy: {
'/api': {
target: 'http://example.com',
changeOrigin: true
}
}
}
};
上述配置表示,当请求的路径以 /api
开头时,会将请求转发到 http://example.com
服务器上。changeOrigin
设置为 true
表示修改请求头中的 Origin
字段,使其与目标服务器的域名一致。
- object.defineProperty () 原理及对象属性监听实现
object.defineProperty()
是 JavaScript 中的一个方法,用于在对象上定义新属性或修改现有属性的特性。它的原理是通过描述符对象来精确控制属性的行为,如是否可读写、可枚举、可配置等。
要实现对对象属性的监听,可以利用 object.defineProperty()
的 get
和 set
访问器属性。例如:
let obj = {};
let value;
Object.defineProperty(obj, 'name', {
get() {
console.log('获取name属性');
return value;
},
set(newValue) {
console.log('设置name属性');
value = newValue;
}
});
obj.name = '张三'; // 输出:设置name属性
console.log(obj.name); // 输出:获取name属性,张三
在上述代码中,通过 Object.defineProperty()
为 obj
对象的 name
属性设置了 get
和 set
方法。当读取 name
属性时,会触发 get
方法;当设置 name
属性时,会触发 set
方法。这样就可以在属性被访问或修改时执行一些额外的逻辑,从而实现对对象属性的监听。
Webpack 的 Eslint 有什么作用,如何配置?
- Eslint 的作用
Eslint 是一个用于 JavaScript 和 JSX 代码的静态代码分析工具,在 Webpack 项目中具有重要作用。
首先,它可以帮助开发者保持代码风格的一致性。通过定义统一的代码风格规则,如缩进、引号使用、换行符等,确保团队成员编写的代码具有相同的风格,提高代码的可读性和可维护性。
其次,Eslint 能够检测代码中的潜在错误。它可以检查语法错误、变量未定义、函数未调用等问题,帮助开发者在开发过程中尽早发现并解决这些问题,减少运行时错误的发生。
此外,Eslint 还可以强制执行一些最佳实践和代码规范。例如,要求使用严格模式、避免全局变量的滥用、合理使用 const
和 let
等,有助于提高代码的质量和性能。
- Eslint 的配置
一般来说,在项目的根目录下创建一个 .eslintrc
文件来配置 Eslint。以下是一个基本的配置示例:
{
"env": {
"browser": true,
"node": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"react"
],
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
在上述配置中,env
定义了代码运行的环境,这里表示代码运行在浏览器和 Node 环境中,并且支持 ES6 语法。extends
用于继承一些现有的规则集,这里继承了 eslint:recommended
和 plugin:react/recommended
,分别是 Eslint 的推荐规则和 React 相关的推荐规则。parserOptions
配置了解析器的选项,指定了 ECMAScript 版本、模块类型以及是否支持 JSX。plugins
声明了要使用的插件,这里使用了 react
插件。rules
中定义了具体的规则,如 semi
规则要求语句必须以分号结尾,quotes
规则要求使用双引号。
还可以根据项目的具体需求,在 .eslintrc
文件中添加更多的规则和配置选项,以满足团队的代码规范和项目要求。同时,也可以在 Webpack 的配置文件中集成 Eslint,使其在打包过程中自动检查代码。
项目中用到了 WebSocket,WebSocket 与长轮询、短轮询的区别是什么?
- WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务器主动向客户端推送数据,客户端也可以主动向服务器发送数据,实现了实时的双向通信。一旦 WebSocket 连接建立,服务器和客户端可以随时相互发送消息,而不需要客户端频繁地发送请求。这种方式大大减少了网络开销,提高了通信效率,非常适合实时性要求高的应用场景,如在线聊天、实时游戏、股票行情推送等。
例如,在一个在线聊天应用中,当一个用户发送消息时,服务器可以立即通过 WebSocket 将消息推送给其他在线用户,而不需要其他用户不断地发送请求来获取新消息。
- 长轮询
长轮询是一种在客户端和服务器之间实现实时数据传输的技术。客户端向服务器发送请求,服务器在收到请求后,如果没有新的数据,不会立即返回响应,而是会保持连接一段时间,等待有新数据时再将数据返回给客户端。客户端在收到响应后,会立即再次发送请求,重复这个过程。长轮询的优点是兼容性较好,大多数浏览器都支持。缺点是在没有数据更新时,连接会一直保持,占用服务器资源,而且每次请求和响应都会有一定的延迟。
比如,在一个新闻推送应用中,客户端通过长轮询向服务器请求最新的新闻。服务器在有新新闻发布时,将新闻数据返回给客户端。如果没有新新闻,服务器会等待一段时间,直到有新新闻或者达到超时时间才返回响应。
- 短轮询
短轮询是最基本的轮询方式。客户端每隔一段时间就向服务器发送请求,询问是否有新的数据。服务器无论是否有新数据,都会立即响应。如果有新数据,就返回给客户端;如果没有,就返回一个空响应或者提示没有新数据。短轮询的优点是实现简单,缺点是频繁地发送请求会增加网络开销,而且实时性较差,因为客户端只能按照固定的时间间隔来获取数据,在两次请求之间如果有新数据更新,客户端无法及时得知。
例如,在一个简单的网页应用中,客户端每隔 30 秒向服务器发送请求获取最新的公告信息。即使在这 30 秒内服务器有新的公告发布,客户端也只能等到下一次请求时才能获取到。
总的来说,WebSocket 适用于对实时性要求极高、需要频繁双向通信的场景;长轮询适用于实时性要求较高,但对兼容性有一定要求的场景;短轮询适用于实时性要求不高,且数据更新频率较低的场景。
前端性能优化的方法有哪些?
前端性能优化是一个复杂但重要的工作,可以从多个方面入手:
-
优化资源加载
- 压缩和合并文件:对 HTML、CSS 和 JavaScript 文件进行压缩,去除不必要的空格、注释等,减小文件体积,加快加载速度。同时,将多个 CSS 和 JavaScript 文件进行合并,减少浏览器的请求次数。例如,可以使用 Webpack 等构建工具来实现文件的压缩和合并。
- 图片优化:选择合适的图片格式,如 JPEG 适用于照片,PNG 适用于透明度要求高的图片,SVG 适用于图标等矢量图形。对图片进行压缩,可以使用工具如 ImageOptim、TinyPNG 等。还可以采用图片懒加载,当图片进入浏览器的可视区域时再进行加载,避免一次性加载大量图片,提高页面的初始加载速度。
- 使用 CDN(内容分发网络):将静态资源(如图片、脚本、样式表等)分发到离用户最近的服务器上,根据用户的地理位置,CDN 会选择最优的服务器节点来提供资源,减少数据传输的距离和时间,加快资源的加载速度。
-
优化页面渲染
- 减少 DOM 操作:DOM 操作是比较耗时的,尽量减少对 DOM 的频繁更新和修改。可以使用文档片段(DocumentFragment)来批量操作 DOM,将多个 DOM 操作合并为一次,减少重排和重绘的次数。例如,在添加多个列表项时,先将所有列表项创建在一个文档片段中,然后一次性将文档片段添加到 DOM 树中。
- 合理使用 CSS 选择器:避免使用复杂的 CSS 选择器,如后代选择器、通配符选择器等,因为这些选择器会增加浏览器的渲染成本。尽量使用简单的类选择器和标签选择器,提高 CSS 的解析速度。
- 首屏优化 :对首屏内容进行优先加载和渲染,将关键的 CSS 样式放在
<head>
标签内,确保首屏的样式能够尽快加载,避免页面出现无样式闪烁的情况。同时,将首屏所需的 JavaScript 脚本放在页面底部,避免阻塞页面的渲染。
-
优化代码逻辑
- 避免内联 JavaScript 和 CSS:虽然内联代码可以减少文件请求,但会使 HTML 文件体积增大,不利于缓存和维护。尽量将 JavaScript 和 CSS 代码放在独立的文件中,并引用到 HTML 页面中。
- 优化 JavaScript 代码:避免使用全局变量,减少变量的作用域链查找时间。合理使用事件委托,将事件绑定在父元素上,通过事件冒泡来处理子元素的事件,减少事件处理程序的数量。同时,对复杂的计算和操作进行优化,避免在主线程中执行耗时过长的任务,以免造成页面卡顿。
-
其他优化
- 缓存策略 :合理设置缓存头信息,对不经常变化的静态资源设置较长的缓存时间,让浏览器能够缓存这些资源,下次访问时直接从缓存中获取,减少服务器的请求次数和响应时间。可以使用
Cache-Control
、Expires
等 HTTP 头字段来设置缓存策略。 - 响应式设计:确保网站在不同设备和屏幕尺寸上都能有良好的显示效果和性能。采用弹性布局、媒体查询等技术,根据设备的屏幕宽度和分辨率来调整页面的布局和样式,避免出现页面错乱或加载过多不必要的资源。
- 缓存策略 :合理设置缓存头信息,对不经常变化的静态资源设置较长的缓存时间,让浏览器能够缓存这些资源,下次访问时直接从缓存中获取,减少服务器的请求次数和响应时间。可以使用
前端攻击 XSS 和 CSRF 的原理及避免方法有哪些?
XSS(跨站脚本攻击)
原理
XSS 攻击是指攻击者通过在目标网站注入恶意脚本,当用户访问该网站时,这些恶意脚本会在用户的浏览器中执行,从而获取用户的敏感信息,如 Cookie、会话令牌等,或者进行其他恶意操作,如篡改页面内容、重定向到恶意网站等。
XSS 攻击主要分为以下几种类型:
- 反射型 XSS :攻击者将恶意脚本作为参数嵌入到 URL 中,当用户点击包含该 URL 的链接时,服务器会将恶意脚本反射到响应中,从而在用户的浏览器中执行。例如,一个搜索页面的 URL 为
http://example.com/search?keyword=xxx
,攻击者可以构造一个恶意 URLhttp://example.com/search?keyword=<script>alert('XSS')</script>
,当用户点击该链接时,恶意脚本就会在浏览器中执行。 - 存储型 XSS:攻击者将恶意脚本存储到目标网站的数据库中,当其他用户访问包含该恶意脚本的页面时,脚本会在他们的浏览器中执行。比如,在一个留言板应用中,攻击者可以在留言内容中插入恶意脚本,当其他用户查看留言时,恶意脚本就会被执行。
- DOM 型 XSS:这种攻击是基于 DOM(文档对象模型)的,攻击者通过修改页面的 DOM 结构,使得恶意脚本在浏览器中执行。例如,当页面根据 URL 参数动态更新 DOM 内容时,攻击者可以构造包含恶意脚本的 URL,从而触发 DOM 型 XSS 攻击。
避免方法
-
输入验证和过滤:对用户输入的数据进行严格的验证和过滤,只允许合法的字符和格式。例如,对于用户输入的文本,去除其中的 HTML 标签和 JavaScript 代码。可以使用正则表达式或一些现有的过滤库来实现。
function sanitizeInput(input) {
return input.replace(/<[^>]*>/g, '');
} -
输出编码 :在将用户输入的数据输出到页面时,对其进行编码,将特殊字符转换为 HTML 实体。这样可以确保恶意脚本不会被浏览器解析和执行。例如,将
<
转换为<
,将>
转换为>
。在 JavaScript 中,可以使用encodeURIComponent
函数对 URL 参数进行编码,使用DOMPurify
库对 HTML 内容进行净化。const userInput = '<script>alert("XSS")</script>';
const encodedInput = DOMPurify.sanitize(userInput); -
设置 CSP(内容安全策略) :CSP 是一种额外的安全层,用于帮助检测和缓解某些类型的 XSS 攻击。通过设置 CSP 响应头,服务器可以指定哪些资源可以被加载,从而限制恶意脚本的执行。例如,设置
Content-Security-Policy: default-src'self'
表示只允许从当前域名加载资源。
CSRF(跨站请求伪造)
原理
CSRF 攻击是指攻击者通过诱导用户在已登录的目标网站上执行非预期的操作。攻击者利用用户在目标网站的会话信息,伪装成合法用户向目标网站发送恶意请求。例如,用户在银行网站登录后,未退出会话,此时攻击者诱导用户访问一个包含恶意请求的页面,该请求会以用户的身份向银行网站发送转账请求,由于用户的会话信息仍然有效,银行网站会误认为是用户本人的操作而执行该请求。
避免方法
-
使用验证码:在关键操作(如转账、修改密码等)中要求用户输入验证码,这样可以确保操作是用户本人发起的,而不是由 CSRF 攻击导致的。
-
验证请求来源 :服务器可以通过检查请求的
Referer
头或Origin
头来验证请求的来源。Referer
头包含了请求的来源页面的 URL,Origin
头包含了请求的源域名。服务器可以根据这些信息判断请求是否来自合法的页面。// 在Node.js中验证Referer头
app.post('/transfer', (req, res) => {
const referer = req.headers.referer;
if (referer && referer.startsWith('http://example.com')) {
// 处理转账请求
} else {
res.status(403).send('Invalid request');
}
}); -
使用 CSRF 令牌:在用户登录后,服务器为用户生成一个唯一的 CSRF 令牌,并将其存储在用户的会话中。在页面中,将该令牌作为隐藏字段或请求头的一部分包含在表单或请求中。当服务器接收到请求时,会验证请求中的 CSRF 令牌是否与会话中的令牌一致。如果不一致,则拒绝该请求。
<form action="/transfer" method="post"> <button type="submit">转账</button> </form>// 在服务器端验证CSRF令牌
app.post('/transfer', (req, res) => {
const csrfToken = req.body.csrf_token;
const sessionToken = req.session.csrf_token;
if (csrfToken === sessionToken) {
// 处理转账请求
} else {
res.status(403).send('Invalid CSRF token');
}
});