前端入门到精通(五):前端安全、性能监控与国际化
引言
随着前端技术的不断发展和应用复杂度的提高,前端开发面临着越来越多的挑战,如安全威胁、性能问题和全球化需求等。在构建高质量的前端应用时,除了掌握基础技术和工程化流程外,还需要关注安全防护、性能监控和国际化等高级主题。本文将深入探讨这些主题,帮助您构建更加安全、高效和全球化的前端应用。
前端安全
前端安全是指保护前端应用免受各种安全威胁的措施和最佳实践。随着Web应用的普及和复杂度的增加,前端安全问题变得越来越重要。
常见的前端安全威胁
1. XSS (Cross-Site Scripting) 跨站脚本攻击
XSS攻击是指攻击者在网页中注入恶意脚本,当用户访问该网页时,恶意脚本会在用户的浏览器中执行。
XSS攻击类型:
- 存储型XSS:恶意脚本被存储在目标服务器上(如数据库中)
- 反射型XSS:恶意脚本包含在URL中,通过服务器反射到页面上
- DOM型XSS:不经过服务器,直接通过操作DOM触发
预防措施:
- 对输入进行验证和过滤
- 对输出进行转义
- 使用内容安全策略(CSP)
- 使用安全的API,如
textContent而不是innerHTML
代码示例 - 转义输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 不安全的方式
function renderUserInput(input) {
element.innerHTML = input; // 危险!可能导致XSS攻击
}
// 安全的方式
function renderUserInput(input) {
// 转义HTML特殊字符
const escapedInput = input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
element.textContent = escapedInput;
}
// 使用框架提供的安全机制(如React)
function SafeComponent({ userInput }) {
return <div>{userInput}</div>; // React会自动转义
}
|
内容安全策略(CSP)配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<!-- 通过meta标签设置CSP -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' https://trusted-cdn.com 'unsafe-inline';
img-src 'self' data: https://trusted-cdn.com;
connect-src 'self' https://api.yourdomain.com;
font-src 'self' https://trusted-cdn.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
">
|
2. CSRF (Cross-Site Request Forgery) 跨站请求伪造
CSRF攻击是指攻击者诱导用户执行非预期的操作,利用用户已认证的会话执行恶意请求。
预防措施:
- 使用CSRF令牌
- 验证请求来源(Referer/Origin头)
- 实现SameSite Cookie策略
- 要求重新认证敏感操作
代码示例 - CSRF令牌:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// 前端获取CSRF令牌并在请求中携带
async function fetchWithCsrfToken(url, options = {}) {
// 从Cookie或meta标签获取CSRF令牌
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
// 在请求头中添加CSRF令牌
const headers = {
...options.headers,
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
};
return fetch(url, {
...options,
credentials: 'same-origin', // 确保携带Cookie
headers
});
}
// 使用示例
async function submitForm() {
try {
const response = await fetchWithCsrfToken('/api/transfer', {
method: 'POST',
body: JSON.stringify({ amount: 1000, recipient: 'attacker' })
});
const data = await response.json();
console.log(data);
} catch (error) {
console.error('提交失败:', error);
}
}
|
SameSite Cookie配置:
1
2
3
4
5
6
7
8
9
10
11
12
|
// 服务器端设置SameSite Cookie(Express示例)
app.use(session({
cookie: {
sameSite: 'strict', // 或 'lax'
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 3600000
},
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
}));
|
3. 点击劫持 (Clickjacking)
点击劫持是指攻击者通过将目标网站嵌入iframe中,诱导用户点击隐藏的元素,执行非预期操作。
预防措施:
- 设置X-Frame-Options头
- 使用Content Security Policy的frame-ancestors指令
- 实现frame buster脚本
代码示例 - X-Frame-Options:
1
2
3
4
5
|
// 服务器端设置X-Frame-Options(Express示例)
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY'); // 或 'SAMEORIGIN'
next();
});
|
Frame Buster脚本:
1
2
3
4
5
|
// 检测是否在iframe中
if (window.self !== window.top) {
// 如果在iframe中,将自身设为顶层窗口
window.top.location = window.self.location;
}
|
4. 敏感信息泄露
敏感信息泄露是指前端代码中包含不应该暴露给用户的信息,如API密钥、密码等。
预防措施:
- 不在前端代码中硬编码敏感信息
- 使用环境变量管理配置
- 实现适当的权限控制
- 使用HTTPS传输所有数据
代码示例 - 环境变量使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 使用环境变量(React + dotenv示例)
const API_KEY = process.env.REACT_APP_API_KEY;
async function fetchData() {
const response = await fetch(`https://api.example.com/data?apiKey=${API_KEY}`);
const data = await response.json();
return data;
}
// 注意:这种方式仍然可能在构建后的代码中暴露API密钥
// 更好的做法是通过后端代理请求
async function fetchDataSecure() {
const response = await fetch('/api/proxy/data');
const data = await response.json();
return data;
}
|
前端安全最佳实践
1. 安全的Coding Practices
- 对所有用户输入进行验证和过滤
- 对所有输出进行适当的转义
- 使用参数化查询防止SQL注入
- 实现适当的错误处理,避免暴露敏感信息
- 定期更新依赖库,修复已知漏洞
设置适当的HTTP安全头可以增强应用的安全性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// Express中间件设置安全头
app.use((req, res, next) => {
// 防止MIME类型嗅探
res.setHeader('X-Content-Type-Options', 'nosniff');
// 防止点击劫持
res.setHeader('X-Frame-Options', 'DENY');
// 启用XSS保护
res.setHeader('X-XSS-Protection', '1; mode=block');
// 强制使用HTTPS
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// 设置CSP
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
// 隐藏服务器信息
res.removeHeader('X-Powered-By');
next();
});
|
3. 数据加密
对于敏感数据,应该在客户端进行适当的加密:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 使用Web Crypto API加密数据
async function encryptData(data, publicKey) {
// 将数据转换为ArrayBuffer
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
// 导入公钥
const importedPublicKey = await window.crypto.subtle.importKey(
'spki',
publicKey,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false,
['encrypt']
);
// 加密数据
const encryptedData = await window.crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
importedPublicKey,
dataBuffer
);
// 将加密数据转换为Base64
return btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
}
|
4. 安全的身份验证
实现安全的身份验证机制:
- 使用HTTPS传输所有认证数据
- 实现强密码策略
- 使用多因素认证
- 安全存储和管理令牌/会话
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
// JWT令牌管理示例
class TokenManager {
constructor() {
this.tokenKey = 'auth_token';
this.refreshTokenKey = 'refresh_token';
}
// 存储令牌(使用HttpOnly Cookie更好)
storeTokens(accessToken, refreshToken) {
// 设置较短的过期时间
const tokenExpiry = new Date().getTime() + (60 * 60 * 1000); // 1小时
localStorage.setItem(this.tokenKey, accessToken);
localStorage.setItem(this.refreshTokenKey, refreshToken);
localStorage.setItem('token_expiry', tokenExpiry.toString());
}
// 获取访问令牌
getAccessToken() {
const token = localStorage.getItem(this.tokenKey);
const expiry = localStorage.getItem('token_expiry');
// 检查令牌是否过期
if (!token || !expiry || new Date().getTime() > parseInt(expiry)) {
this.refreshToken().then(newToken => {
return newToken;
}).catch(() => {
this.logout();
return null;
});
}
return token;
}
// 刷新令牌
async refreshToken() {
const refreshToken = localStorage.getItem(this.refreshTokenKey);
if (!refreshToken) return null;
try {
const response = await fetch('/api/refresh-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (!response.ok) throw new Error('刷新令牌失败');
const data = await response.json();
this.storeTokens(data.accessToken, data.refreshToken);
return data.accessToken;
} catch (error) {
console.error('令牌刷新错误:', error);
throw error;
}
}
// 登出
logout() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey);
localStorage.removeItem('token_expiry');
window.location.href = '/login';
}
}
const tokenManager = new TokenManager();
|
性能监控
性能监控是前端开发中的重要环节,它可以帮助开发者发现和解决性能问题,提升用户体验。
性能指标
1. Core Web Vitals
Core Web Vitals是Google提出的一系列关键性能指标,用于衡量用户体验:
- LCP (Largest Contentful Paint):最大内容绘制,衡量页面加载速度,目标值<2.5秒
- FID (First Input Delay):首次输入延迟,衡量交互响应性,目标值<100ms
- CLS (Cumulative Layout Shift):累积布局偏移,衡量视觉稳定性,目标值<0.1
2. 其他重要指标
- FCP (First Contentful Paint):首次内容绘制
- TTFB (Time to First Byte):首字节时间
- FMP (First Meaningful Paint):首次有效绘制
- TTI (Time to Interactive):可交互时间
性能监控方法
现代浏览器提供了Performance API,可以用于获取各种性能指标:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// 监控LCP
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP:', entry.startTime);
// 可以将数据发送到监控服务器
sendToAnalytics('lcp', entry.startTime);
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
// 监控FID
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('FID:', entry.processingStart - entry.startTime);
sendToAnalytics('fid', entry.processingStart - entry.startTime);
}
}).observe({ type: 'first-input', buffered: true });
// 监控CLS
let clsValue = 0;
let clsEntries = [];
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// 只考虑没有最近用户输入的布局偏移
if (!entry.hadRecentInput) {
clsValue += entry.value;
clsEntries.push(entry);
console.log('CLS:', clsValue);
sendToAnalytics('cls', clsValue);
}
}
}).observe({ type: 'layout-shift', buffered: true });
// 获取TTFB
window.addEventListener('load', () => {
const timing = performance.timing;
const ttfb = timing.responseStart - timing.requestStart;
console.log('TTFB:', ttfb);
sendToAnalytics('ttfb', ttfb);
});
|
2. Web Vitals库
Web Vitals是Google提供的一个JavaScript库,用于简化Core Web Vitals的测量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// 安装:npm install web-vitals
import {getLCP, getFID, getCLS} from 'web-vitals';
// 监控LCP
getLCP(metric => {
console.log('LCP:', metric.value);
sendToAnalytics('lcp', metric.value);
});
// 监控FID
getFID(metric => {
console.log('FID:', metric.value);
sendToAnalytics('fid', metric.value);
});
// 监控CLS
getCLS(metric => {
console.log('CLS:', metric.value);
sendToAnalytics('cls', metric.value);
});
// 发送数据到分析服务器
function sendToAnalytics(name, value) {
const body = JSON.stringify({name, value, url: location.href});
navigator.sendBeacon('/analytics', body);
}
|
3. 性能分析工具
Chrome DevTools提供了多种性能分析工具:
- Performance面板:记录和分析页面运行时性能
- Network面板:分析网络请求和加载性能
- Lighthouse面板:生成全面的性能报告
Lighthouse CLI
可以通过命令行或CI/CD流程集成Lighthouse:
1
2
3
4
5
|
# 安装Lighthouse
npm install -g lighthouse
# 运行Lighthouse分析
lighthouse https://example.com --output json --output-path ./report.json
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 在Node.js中使用Lighthouse
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
async function runLighthouse(url) {
const chrome = await chromeLauncher.launch({chromeFlags: ['--headless']});
const options = {
logLevel: 'info',
output: 'json',
onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'],
port: chrome.port,
};
const runnerResult = await lighthouse(url, options);
const report = runnerResult.report;
const performanceScore = runnerResult.lhr.categories.performance.score * 100;
console.log('性能评分:', performanceScore);
await chrome.kill();
return { report, performanceScore };
}
|
前端监控系统
1. Sentry
Sentry是一个开源的错误跟踪平台,可以实时监控应用错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// 安装:npm install @sentry/browser @sentry/tracing
import * as Sentry from '@sentry/browser';
import { Integrations } from '@sentry/tracing';
Sentry.init({
dsn: 'https://your-sentry-dsn.example.com',
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
});
// 捕获异常
try {
// 可能抛出错误的代码
} catch (error) {
Sentry.captureException(error);
}
// 记录自定义消息
Sentry.captureMessage('用户登录失败');
// 添加用户上下文
Sentry.setUser({ id: '123', username: 'user123' });
// 添加标签
Sentry.setTag('page', 'login');
// 添加自定义数据
Sentry.setContext('transaction', {
id: transactionId,
amount: transactionAmount,
});
|
2. New Relic
New Relic提供全面的应用性能监控:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 安装New Relic浏览器代理
// 将以下脚本添加到HTML头部
/*
<script type="text/javascript">
window.NREUM||(NREUM={});NREUM.init={privacy:{cookies_enabled:true}};NREUM.init={applicationID:"your-app-id"};
(function(){var nr=window.NREUM;if(!nr.initialized){nr.init={privacy:{cookies_enabled:true}};var e=document.createElement("script");
e.src="https://js-agent.newrelic.com/nr-spa.min.js";e.async=1;var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)}})();
</script>
*/
// 记录自定义事件
window.newrelic.addPageAction('customEvent', {
actionType: 'userInteraction',
value: 'importantAction'
});
|
3. 自建监控系统
也可以构建自己的前端监控系统:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
class FrontendMonitor {
constructor(config) {
this.config = {
apiEndpoint: '/api/monitoring',
batchInterval: 5000,
sampleRate: 1.0,
...config
};
this.queue = [];
this.started = false;
}
// 启动监控
start() {
if (this.started) return;
// 只对一部分用户启用监控(根据采样率)
if (Math.random() > this.config.sampleRate) return;
this.started = true;
// 设置批处理定时器
setInterval(() => this.flush(), this.config.batchInterval);
// 监听错误
this.setupErrorMonitoring();
// 监听性能指标
this.setupPerformanceMonitoring();
// 页面卸载时发送剩余数据
window.addEventListener('beforeunload', () => this.flush());
}
// 设置错误监控
setupErrorMonitoring() {
// 监听全局错误
window.addEventListener('error', (event) => {
this.captureError(event.error, 'window.error');
});
// 监听Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
this.captureError(event.reason, 'unhandledrejection');
});
}
// 设置性能监控
setupPerformanceMonitoring() {
// 监控Core Web Vitals
if ('PerformanceObserver' in window) {
// 监控LCP
this.monitorLCP();
// 监控FID
this.monitorFID();
// 监控CLS
this.monitorCLS();
}
// 监控资源加载
this.monitorResources();
}
// 监控LCP
monitorLCP() {
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
this.captureMetric('lcp', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
}
// 监控FID
monitorFID() {
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
this.captureMetric('fid', lastEntry.processingStart - lastEntry.startTime);
}).observe({ type: 'first-input', buffered: true });
}
// 监控CLS
monitorCLS() {
let clsValue = 0;
let clsEntries = [];
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
for (const entry of entries) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
clsEntries.push(entry);
this.captureMetric('cls', clsValue);
}
}
}).observe({ type: 'layout-shift', buffered: true });
}
// 监控资源加载
monitorResources() {
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
for (const entry of entries) {
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
this.captureResource(entry);
}
}
}).observe({ type: 'resource', buffered: true });
}
// 捕获错误
captureError(error, type = 'error') {
// 过滤掉某些错误
if (this.shouldIgnoreError(error)) return;
const errorData = {
type,
message: error.message,
stack: error.stack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
};
this.queue.push({ type: 'error', data: errorData });
}
// 捕获性能指标
captureMetric(name, value) {
const metricData = {
name,
value,
url: window.location.href,
timestamp: Date.now(),
};
this.queue.push({ type: 'metric', data: metricData });
}
// 捕获资源信息
captureResource(entry) {
const resourceData = {
name: entry.name,
type: entry.initiatorType,
duration: entry.duration,
startTime: entry.startTime,
responseEnd: entry.responseEnd,
status: entry.responseStatus || 0,
timestamp: Date.now(),
};
// 只记录耗时较长的请求
if (entry.duration > 1000) {
this.queue.push({ type: 'resource', data: resourceData });
}
}
// 判断是否忽略错误
shouldIgnoreError(error) {
const ignoredErrors = [
'ResizeObserver loop limit exceeded',
'Script error',
];
return ignoredErrors.some(message => error.message?.includes(message));
}
// 发送数据到服务器
async flush() {
if (this.queue.length === 0) return;
const payload = [...this.queue];
this.queue = [];
try {
await fetch(this.config.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
} catch (error) {
console.error('发送监控数据失败:', error);
// 如果发送失败,将数据重新加入队列
this.queue = [...this.queue, ...payload];
}
}
}
// 使用示例
const monitor = new FrontendMonitor({
apiEndpoint: '/api/frontend-monitoring',
sampleRate: 0.1, // 只对10%的用户启用监控
});
monitor.start();
|
国际化 (i18n) 与本地化 (l10n)
随着应用的全球化,国际化和本地化变得越来越重要。国际化是指设计和开发能够适应不同语言和地区的应用,而本地化是指将应用调整为特定语言和地区的过程。
国际化基础
1. 国际化的关键考虑因素
- 文本翻译:界面文本需要支持多语言
- 日期和时间格式:不同地区有不同的日期时间格式
- 数字格式:不同地区的数字表示方式不同(如小数点、千位分隔符)
- 货币:货币符号和格式的差异
- 度量单位:公制vs英制
- 时区处理:不同时区的时间转换
- 排序规则:不同语言的字母排序规则不同
- 字符编码:确保支持各种语言的字符
- RTL支持:支持从右到左的文本布局(如阿拉伯语)
国际化工具库
1. i18next
i18next是一个功能强大的JavaScript国际化框架:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// 安装:npm install i18next react-i18next
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
// 初始化i18next
i18n
// 加载翻译资源
.use(HttpBackend)
// 自动检测用户语言
.use(LanguageDetector)
// 将i18next实例传递给react-i18next
.use(initReactI18next)
.init({
lng: 'zh-CN', // 默认语言
fallbackLng: 'en', // 回退语言
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React已经处理了XSS
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
ns: ['common', 'home', 'profile'], // 命名空间
defaultNS: 'common',
react: {
useSuspense: true, // 使用React Suspense
},
});
export default i18n;
|
创建翻译文件 (/locales/zh-CN/common.json):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
{
"greeting": "你好,{{name}}!",
"welcome": "欢迎使用我们的应用",
"menu": {
"home": "首页",
"about": "关于我们",
"contact": "联系我们"
},
"pagination": {
"previous": "上一页",
"next": "下一页",
"page": "第 {{page}} 页",
"of": "共 {{total}} 页"
},
"errors": {
"required": "此字段是必填项",
"minLength": "至少需要 {{length}} 个字符",
"networkError": "网络错误,请稍后重试"
}
}
|
在React组件中使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
function Header() {
const { t, i18n } = useTranslation('common');
const navigate = useNavigate();
// 切换语言
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<header>
<nav>
<ul>
<li onClick={() => navigate('/')}>{t('menu.home')}</li>
<li onClick={() => navigate('/about')}>{t('menu.about')}</li>
<li onClick={() => navigate('/contact')}>{t('menu.contact')}</li>
</ul>
</nav>
<div className="language-switcher">
<button onClick={() => changeLanguage('zh-CN')}>中文</button>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('ja')}>日本語</button>
</div>
</header>
);
}
function UserProfile({ user }) {
const { t } = useTranslation(['common', 'profile']); // 使用多个命名空间
return (
<div className="profile">
<h2>{t('greeting', { name: user.name })}</h2>
<p>{t('profile:memberSince', { date: new Date(user.createdAt).toLocaleDateString(i18n.language) })}</p>
<p>{t('profile:totalPosts', { count: user.postCount })}</p>
</div>
);
}
|
FormatJS是一个国际化工具集,其中react-intl是其React绑定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// 安装:npm install react-intl
import React from 'react';
import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
// 创建缓存以提高性能
const cache = createIntlCache();
// 翻译消息对象
const messages = {
'zh-CN': {
greeting: '你好,{name}!',
welcome: '欢迎使用我们的应用',
today: '今天是 {date, date, full}',
temperature: '温度:{temp, number, ::.0}°C',
price: '价格:{price, number, currency}',
products: '共有 {count, plural, =0 {没有产品} =1 {1 个产品} other {# 个产品}}',
userGender: '用户 {gender, select, male {他} female {她} other {他们}} 已登录'
},
'en': {
greeting: 'Hello, {name}!',
welcome: 'Welcome to our application',
today: 'Today is {date, date, full}',
temperature: 'Temperature: {temp, number, ::.0}°C',
price: 'Price: {price, number, currency}',
products: 'There {count, plural, =0 {are no products} =1 {is one product} other {are # products}}',
userGender: 'User {gender, select, male {he} female {she} other {they}} is logged in'
}
};
function IntlProvider({ children, locale = 'zh-CN' }) {
const intl = createIntl(
{
locale,
messages: messages[locale] || messages['en']
},
cache
);
return <RawIntlProvider value={intl}>{children}</RawIntlProvider>;
}
export default IntlProvider;
|
在React组件中使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import React, { useState } from 'react';
import { useIntl } from 'react-intl';
import IntlProvider from './IntlProvider';
function Greeting() {
const intl = useIntl();
return (
<div>
<h1>{intl.formatMessage({ id: 'greeting' }, { name: '用户' })}</h1>
<p>{intl.formatMessage({ id: 'welcome' })}</p>
<p>{intl.formatMessage({ id: 'today' }, { date: new Date() })}</p>
<p>{intl.formatMessage({ id: 'temperature' }, { temp: 25.5 })}</p>
<p>{intl.formatMessage({ id: 'price' }, { price: 99.99, currency: 'CNY' })}</p>
<p>{intl.formatMessage({ id: 'products' }, { count: 5 })}</p>
<p>{intl.formatMessage({ id: 'userGender' }, { gender: 'female' })}</p>
</div>
);
}
function App() {
const [locale, setLocale] = useState('zh-CN');
const changeLocale = (newLocale) => {
setLocale(newLocale);
};
return (
<IntlProvider locale={locale}>
<div>
<div>
<button onClick={() => changeLocale('zh-CN')}>中文</button>
<button onClick={() => changeLocale('en')}>English</button>
</div>
<Greeting />
</div>
</IntlProvider>
);
}
|
高级国际化功能
1. 日期和时间格式化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// 使用i18next格式化日期和时间
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en';
import relativeTime from 'dayjs/plugin/relativeTime';
// 配置dayjs插件
dayjs.extend(relativeTime);
function DateDisplay({ date }) {
const { t, i18n } = useTranslation();
// 设置dayjs语言
dayjs.locale(i18n.language === 'zh-CN' ? 'zh-cn' : 'en');
const formattedDate = dayjs(date).format('YYYY年MM月DD日'); // 中文格式
const formattedTime = dayjs(date).format('HH:mm:ss');
const relativeTimeStr = dayjs(date).fromNow();
return (
<div>
<p>{t('date', { date: formattedDate })}</p>
<p>{t('time', { time: formattedTime })}</p>
<p>{t('relativeTime', { time: relativeTimeStr })}</p>
</div>
);
}
// 或者使用react-intl的日期格式化
import { FormattedDate, FormattedTime, FormattedRelative } from 'react-intl';
function IntlDateDisplay({ date }) {
return (
<div>
<p><FormattedDate value={date} year="numeric" month="long" day="numeric" weekday="long" /></p>
<p><FormattedTime value={date} hour="2-digit" minute="2-digit" second="2-digit" /></p>
<p><FormattedRelative value={date} /></p>
</div>
);
}
|
2. 数字和货币格式化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
// 使用i18next格式化数字和货币
import { useTranslation } from 'react-i18next';
import numeral from 'numeral';
import 'numeral/locales/zh-cn';
import 'numeral/locales/en-gb';
function NumberDisplay({ number, type = 'number' }) {
const { t, i18n } = useTranslation();
// 设置numeral语言
if (i18n.language === 'zh-CN') {
numeral.locale('zh-cn');
} else {
numeral.locale('en-gb');
}
let formattedNumber;
switch (type) {
case 'currency':
formattedNumber = numeral(number).format('$0,0.00');
break;
case 'percentage':
formattedNumber = numeral(number).format('0.00%');
break;
case 'large':
formattedNumber = numeral(number).format('0.0a');
break;
default:
formattedNumber = numeral(number).format('0,0');
}
return <span>{formattedNumber}</span>;
}
// 使用react-intl格式化
import { FormattedNumber } from 'react-intl';
function IntlNumberDisplay({ number, type = 'number' }) {
switch (type) {
case 'currency':
return <FormattedNumber value={number} style="currency" currency="CNY" />;
case 'percentage':
return <FormattedNumber value={number} style="percent" maximumFractionDigits={2} />;
case 'large':
return <FormattedNumber value={number} notation="compact" />;
default:
return <FormattedNumber value={number} />;
}
}
|
3. RTL支持
对于从右到左的语言(如阿拉伯语、希伯来语),需要特殊处理样式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// 在i18next初始化中配置RTL
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { isRTL } from 'i18next-express-middleware';
i18n
.use(initReactI18next)
.init({
// ...
react: {
useSuspense: true,
// 添加RTL检测
transSupportBasicHtmlNodes: true,
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
},
});
// 在React组件中处理RTL
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
function App() {
const { i18n } = useTranslation();
useEffect(() => {
// 根据当前语言设置RTL属性
const dir = i18n.language === 'ar' ? 'rtl' : 'ltr';
document.documentElement.dir = dir;
document.documentElement.lang = i18n.language;
}, [i18n.language]);
// ...
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/* CSS中处理RTL */
.element {
padding-left: 20px;
text-align: left;
}
[dir="rtl"] .element {
padding-left: 0;
padding-right: 20px;
text-align: right;
}
/* 或者使用CSS变量 */
:root {
--padding-direction: padding-left;
--text-alignment: left;
}
[dir="rtl"] {
--padding-direction: padding-right;
--text-alignment: right;
}
.element {
var(--padding-direction): 20px;
text-align: var(--text-alignment);
}
|
本地化最佳实践
1. 文本提取和管理
使用工具自动提取代码中的翻译键:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
# 使用i18next-scanner提取键
npm install --save-dev i18next-scanner
# 创建配置文件 i18next-scanner.config.js
const fs = require('fs');
const chalk = require('chalk');
module.exports = {
input: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}',
],
output: './',
options: {
debug: true,
removeUnusedKeys: false,
func: {
list: ['t'],
extensions: [''],
},
trans: {
component: 'Trans',
i18nKey: 'i18nKey',
defaultsKey: 'defaults',
extensions: [],
fallbackKey: function(ns, value) {
return value;
},
},
lngs: ['zh-CN', 'en', 'ja'],
ns: ['common', 'home', 'profile'],
defaultLng: 'zh-CN',
defaultNs: 'common',
defaultValue: '',
resource: {
loadPath: 'src/locales/{{lng}}/{{ns}}.json',
savePath: 'src/locales/{{lng}}/{{ns}}.json',
jsonIndent: 2,
lineEnding: '\n',
},
keySeparator: '.',
nsSeparator: ':',
interpolation: {
prefix: '{{',
suffix: '}}',
},
},
transform: function customTransform(file, enc, done) {
const parser = this.parser;
const content = fs.readFileSync(file.path, enc);
let count = 0;
parser.parseFuncFromString(
content,
{ list: ['t'] },
(key, options) => {
parser.set(key, options);
count++;
}
);
if (count > 0) {
console.log(`i18next-scanner: ${chalk.cyan(file.path)}: ${count} keys extracted`);
}
done();
},
};
# 添加npm脚本
# "scripts": {
# "extract:locales": "i18next-scanner"
# }
|
2. 翻译工作流
- 提取:使用工具从代码中提取所有翻译键
- 导出:将提取的键导出为翻译文件(如JSON、XLIFF等)
- 翻译:将翻译文件发送给翻译人员或使用翻译服务
- 导入:将翻译完成的文件导入到项目中
- 验证:验证翻译是否完整、格式是否正确
- 集成:将翻译集成到应用中并测试
3. 动态加载翻译资源
对于大型应用,可以按需加载翻译资源以减少初始加载时间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// 动态导入翻译资源
i18n
.use(HttpBackend)
.use(initReactI18next)
.init({
// ...
partialBundledLanguages: true,
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
allowMultiLoading: true,
backendOptions: [
{ expirationTime: 60 * 60 * 1000 }, // 缓存1小时
],
},
});
// 按需加载命名空间
import { useTranslation } from 'react-i18next';
function AsyncComponent() {
const { t, i18n } = useTranslation('common');
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
// 动态加载其他命名空间
i18n.loadNamespaces('specificNamespace').then(() => {
setIsLoading(false);
});
}, [i18n]);
if (isLoading) return <div>Loading...</div>;
// 现在可以使用特定命名空间的翻译
const { t: tSpecific } = useTranslation('specificNamespace');
return (
<div>
<p>{t('commonText')}</p>
<p>{tSpecific('specificText')}</p>
</div>
);
}
|
实践项目:构建一个多语言电商应用
让我们结合前端安全、性能监控和国际化知识,构建一个多语言电商应用。
项目概述
我们将创建一个具有以下功能的电商应用:
- 多语言支持(中文、英文)
- 产品列表和详情页
- 购物车功能
- 性能监控
- 安全措施
技术栈
- 前端框架:React
- 路由:React Router
- 状态管理:Redux Toolkit
- 国际化:i18next
- UI组件库:Material-UI
- 性能监控:自定义监控 + Web Vitals
- 构建工具:Vite
项目结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/src
/assets # 静态资源
/components # 通用组件
/features # 功能模块
/products # 产品相关
/cart # 购物车相关
/auth # 认证相关
/i18n # 国际化配置
/locales # 翻译资源
/zh-CN
/en
/middleware # 中间件
/services # API服务
/utils # 工具函数
/monitoring # 性能监控
/security # 安全工具
/hooks # 自定义钩子
/pages # 页面组件
App.tsx # 应用入口组件
main.tsx # 应用入口文件
routes.tsx # 路由配置
store.ts # Redux存储配置
|
实现步骤
1. 设置基础项目
1
2
3
4
5
6
7
8
9
|
# 创建项目
npm create vite@latest multi-language-ecommerce -- --template react-ts
cd multi-language-ecommerce
# 安装依赖
npm install react-router-dom redux react-redux @reduxjs/toolkit axios i18next react-i18next i18next-http-backend i18next-browser-languagedetector @mui/material @mui/icons-material @emotion/react @emotion/styled web-vitals
# 安装开发依赖
npm install --save-dev @types/react-router-dom
|
2. 配置国际化
创建i18n配置文件 (src/i18n/index.ts):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
lng: 'zh-CN',
fallbackLng: 'en',
debug: import.meta.env.DEV,
interpolation: {
escapeValue: false,
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
ns: ['common', 'products', 'cart', 'checkout'],
defaultNS: 'common',
react: {
useSuspense: true,
},
});
export default i18n;
|
创建翻译资源文件 (src/locales/zh-CN/products.json):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
{
"productList": "产品列表",
"productDetails": "产品详情",
"addToCart": "加入购物车",
"buyNow": "立即购买",
"price": "价格",
"description": "描述",
"specifications": "规格",
"reviews": "评价",
"outOfStock": "缺货",
"inStock": "有货",
"sortBy": "排序方式",
"filterBy": "筛选",
"search": "搜索产品..."
}
|
3. 配置性能监控
创建性能监控工具 (src/utils/monitoring/index.ts):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
import { getLCP, getFID, getCLS, ReportHandler } from 'web-vitals';
// 发送数据到分析服务
const sendToAnalytics = (metric: { name: string; value: number }) => {
// 只在生产环境发送
if (import.meta.env.PROD) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
url: window.location.href,
timestamp: Date.now(),
});
// 使用navigator.sendBeacon发送数据
navigator.sendBeacon('/api/analytics', body);
}
};
// 监控Core Web Vitals
export const monitorWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getLCP(onPerfEntry);
}
};
// 错误监控
export const setupErrorMonitoring = () => {
// 监听全局错误
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
// 发送错误信息到服务器
sendErrorToServer(event.error, 'window.error');
});
// 监听Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
sendErrorToServer(event.reason, 'unhandledrejection');
});
};
// 发送错误到服务器
const sendErrorToServer = (error: Error, type: string) => {
if (import.meta.env.PROD) {
const body = JSON.stringify({
type,
message: error.message,
stack: error.stack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
});
navigator.sendBeacon('/api/errors', body);
}
};
// 资源加载监控
export const monitorResources = () => {
if ('PerformanceObserver' in window) {
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
for (const entry of entries) {
// 监控慢请求
if (
(entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') &&
entry.duration > 2000
) {
console.warn(`Slow request: ${entry.name} (${entry.duration.toFixed(2)}ms)`);
if (import.meta.env.PROD) {
const body = JSON.stringify({
name: entry.name,
duration: entry.duration,
type: entry.initiatorType,
timestamp: Date.now(),
});
navigator.sendBeacon('/api/slow-requests', body);
}
}
}
}).observe({ type: 'resource', buffered: true });
}
};
// 初始化所有监控
export const initMonitoring = () => {
monitorWebVitals(sendToAnalytics);
setupErrorMonitoring();
monitorResources();
};
|
4. 实现安全措施
创建安全工具 (src/utils/security/index.ts):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
// 输入验证
export const validateInput = {
// 验证邮箱
email: (email: string): boolean => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
},
// 验证密码强度
password: (password: string): { isValid: boolean; errors: string[] } => {
const errors: string[] = [];
if (password.length < 8) {
errors.push('密码长度至少为8个字符');
}
if (!/[A-Z]/.test(password)) {
errors.push('密码必须包含至少一个大写字母');
}
if (!/[a-z]/.test(password)) {
errors.push('密码必须包含至少一个小写字母');
}
if (!/[0-9]/.test(password)) {
errors.push('密码必须包含至少一个数字');
}
return { isValid: errors.length === 0, errors };
},
// 验证电话号码
phone: (phone: string): boolean => {
const re = /^[0-9]{10,11}$/;
return re.test(phone);
},
};
// 数据加密
export const encryptData = async (data: any, publicKey?: string): Promise<string> => {
try {
// 将数据转换为字符串
const dataString = JSON.stringify(data);
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(dataString);
// 如果提供了公钥,使用Web Crypto API加密
if (publicKey && 'crypto' in window) {
// 这里简化处理,实际使用需要正确导入和格式化公钥
// const importedKey = await crypto.subtle.importKey(...);
// const encrypted = await crypto.subtle.encrypt(..., importedKey, dataBuffer);
// return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}
// 简单编码(实际项目中应使用更强的加密)
return btoa(unescape(encodeURIComponent(dataString)));
} catch (error) {
console.error('数据加密失败:', error);
throw error;
}
};
// 数据解密
export const decryptData = async (encryptedData: string, privateKey?: string): Promise<any> => {
try {
// 如果提供了私钥,使用Web Crypto API解密
if (privateKey && 'crypto' in window) {
// 这里简化处理,实际使用需要正确导入和格式化私钥
// const importedKey = await crypto.subtle.importKey(...);
// const encryptedBuffer = new Uint8Array(atob(encryptedData).split('').map(c => c.charCodeAt(0)));
// const decrypted = await crypto.subtle.decrypt(..., importedKey, encryptedBuffer);
// const decoder = new TextDecoder();
// return JSON.parse(decoder.decode(decrypted));
}
// 简单解码
const dataString = decodeURIComponent(escape(atob(encryptedData)));
return JSON.parse(dataString);
} catch (error) {
console.error('数据解密失败:', error);
throw error;
}
};
// CSRF保护
export const fetchWithCsrfToken = async (url: string, options: RequestInit = {}) => {
// 从meta标签获取CSRF令牌
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// 在请求头中添加CSRF令牌
const headers = {
...options.headers,
'X-CSRF-Token': csrfToken || '',
'Content-Type': 'application/json',
};
return fetch(url, {
...options,
credentials: 'same-origin', // 确保携带Cookie
headers,
});
};
// 防止XSS攻击
export const sanitizeHTML = (html: string): string => {
// 使用DOMParser进行基本的HTML清理
const temp = document.createElement('div');
temp.textContent = html;
return temp.innerHTML;
};
// 安全存储
export const secureStorage = {
// 存储数据
setItem: (key: string, value: any, useSession = false): void => {
const storage = useSession ? sessionStorage : localStorage;
try {
const serializedValue = JSON.stringify(value);
storage.setItem(key, serializedValue);
} catch (error) {
console.error('存储数据失败:', error);
}
},
// 获取数据
getItem: (key: string, useSession = false): any => {
const storage = useSession ? sessionStorage : localStorage;
try {
const item = storage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error('获取数据失败:', error);
return null;
}
},
// 删除数据
removeItem: (key: string, useSession = false): void => {
const storage = useSession ? sessionStorage : localStorage;
storage.removeItem(key);
},
// 清除所有数据
clear: (useSession = false): void => {
const storage = useSession ? sessionStorage : localStorage;
storage.clear();
},
};
|
5. 创建产品组件
创建产品列表组件 (src/features/products/ProductList.tsx):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { Box, Grid, Typography, Button, Card, CardContent, CardMedia, CircularProgress } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { fetchProducts } from './productsSlice';
import { RootState } from '../../store';
import { sanitizeHTML } from '../../utils/security';
interface Product {
id: number;
name: string;
description: string;
price: number;
image: string;
inStock: boolean;
}
const ProductList: React.FC = () => {
const { t } = useTranslation('products');
const dispatch = useDispatch();
const navigate = useNavigate();
const { products, loading, error } = useSelector((state: RootState) => state.products);
useEffect(() => {
dispatch(fetchProducts() as any);
}, [dispatch]);
const handleAddToCart = (product: Product) => {
// 添加到购物车的逻辑
console.log('Adding to cart:', product);
};
const handleProductClick = (productId: number) => {
navigate(`/products/${productId}`);
};
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 5 }}>
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography color="error">{t('errorLoadingProducts')}</Typography>
<Button variant="contained" onClick={() => dispatch(fetchProducts() as any)} sx={{ mt: 2 }}>
{t('retry')}
</Button>
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>{t('productList')}</Typography>
<Grid container spacing={3}>
{products.map((product: Product) => (
<Grid item xs={12} sm={6} md={4} key={product.id}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardMedia
component="img"
height="180"
image={product.image}
alt={product.name}
onClick={() => handleProductClick(product.id)}
sx={{ cursor: 'pointer' }}
/>
<CardContent sx={{ flexGrow: 1 }}>
<Typography gutterBottom variant="h6" component="div" onClick={() => handleProductClick(product.id)} sx={{ cursor: 'pointer' }}>
{sanitizeHTML(product.name)}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{sanitizeHTML(product.description.substring(0, 100))}...
</Typography>
<Typography variant="h6" color="primary" sx={{ mb: 2 }}>
¥{product.price.toFixed(2)}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="contained"
fullWidth
onClick={() => handleAddToCart(product)}
disabled={!product.inStock}
>
{t('addToCart')}
</Button>
</Box>
<Typography variant="caption" color={product.inStock ? 'success.main' : 'error.main'} sx={{ display: 'block', mt: 1 }}>
{product.inStock ? t('inStock') : t('outOfStock')}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
);
};
export default ProductList;
|
6. 创建Redux Slice
创建产品Slice (src/features/products/productsSlice.ts):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
import { fetchWithCsrfToken } from '../../utils/security';
interface Product {
id: number;
name: string;
description: string;
price: number;
image: string;
inStock: boolean;
}
interface ProductsState {
products: Product[];
currentProduct: Product | null;
loading: boolean;
error: string | null;
}
const initialState: ProductsState = {
products: [],
currentProduct: null,
loading: false,
error: null,
};
// 异步获取产品列表
export const fetchProducts = createAsyncThunk(
'products/fetchProducts',
async (_, { rejectWithValue }) => {
try {
const response = await fetchWithCsrfToken('/api/products');
if (!response.ok) {
throw new Error('Failed to fetch products');
}
const data = await response.json();
return data;
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to fetch products');
}
}
);
// 异步获取单个产品
export const fetchProductById = createAsyncThunk(
'products/fetchProductById',
async (productId: number, { rejectWithValue }) => {
try {
const response = await fetchWithCsrfToken(`/api/products/${productId}`);
if (!response.ok) {
throw new Error('Failed to fetch product');
}
const data = await response.json();
return data;
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to fetch product');
}
}
);
const productsSlice = createSlice({
name: 'products',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
// 处理fetchProducts
.addCase(fetchProducts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProducts.fulfilled, (state, action: PayloadAction<Product[]>) => {
state.loading = false;
state.products = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
// 处理fetchProductById
.addCase(fetchProductById.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProductById.fulfilled, (state, action: PayloadAction<Product>) => {
state.loading = false;
state.currentProduct = action.payload;
})
.addCase(fetchProductById.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
},
});
export default productsSlice.reducer;
|
7. 配置路由
创建路由配置 (src/routes.tsx):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
import React, { lazy, Suspense } from 'react';
import { createBrowserRouter, RouteObject } from 'react-router-dom';
import App from './App';
import { CircularProgress, Box } from '@mui/material';
// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const ProductList = lazy(() => import('./features/products/ProductList'));
const ProductDetail = lazy(() => import('./features/products/ProductDetail'));
const Cart = lazy(() => import('./features/cart/Cart'));
const Checkout = lazy(() => import('./features/checkout/Checkout'));
const Login = lazy(() => import('./features/auth/Login'));
const Register = lazy(() => import('./features/auth/Register'));
const NotFound = lazy(() => import('./pages/NotFound'));
// 加载状态组件
const LoadingFallback = () => (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '80vh' }}>
<CircularProgress />
</Box>
);
const routes: RouteObject[] = [
{
path: '/',
element: <App />,
children: [
{
index: true,
element: (
<Suspense fallback={<LoadingFallback />}>
<Home />
</Suspense>
),
},
{
path: 'products',
element: (
<Suspense fallback={<LoadingFallback />}>
<ProductList />
</Suspense>
),
},
{
path: 'products/:id',
element: (
<Suspense fallback={<LoadingFallback />}>
<ProductDetail />
</Suspense>
),
},
{
path: 'cart',
element: (
<Suspense fallback={<LoadingFallback />}>
<Cart />
</Suspense>
),
},
{
path: 'checkout',
element: (
<Suspense fallback={<LoadingFallback />}>
<Checkout />
</Suspense>
),
},
{
path: 'login',
element: (
<Suspense fallback={<LoadingFallback />}>
<Login />
</Suspense>
),
},
{
path: 'register',
element: (
<Suspense fallback={<LoadingFallback />}>
<Register />
</Suspense>
),
},
{
path: '*',
element: (
<Suspense fallback={<LoadingFallback />}>
<NotFound />
</Suspense>
),
},
],
},
];
const router = createBrowserRouter(routes);
export default router;
|
8. 配置App组件
更新App组件 (src/App.tsx):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Outlet, useLocation } from 'react-router-dom';
import { AppBar, Toolbar, Typography, Button, Box, Container } from '@mui/material';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { useSelector } from 'react-redux';
import { RootState } from './store';
import { initMonitoring } from './utils/monitoring';
// 检测RTL
i18n.on('languageChanged', (lng) => {
document.documentElement.dir = lng === 'ar' ? 'rtl' : 'ltr';
});
// 创建Material-UI主题
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
},
});
function App() {
const { t, i18n } = useTranslation('common');
const location = useLocation();
const cartItems = useSelector((state: RootState) => state.cart.items);
// 初始化性能监控
useEffect(() => {
initMonitoring();
}, []);
// 切换语言
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<ThemeProvider theme={theme}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
{t('appName')}
</Typography>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button color="inherit" href="/">{t('home')}</Button>
<Button color="inherit" href="/products">{t('products')}</Button>
<Button color="inherit" href="/cart">
{t('cart')} ({cartItems.length})
</Button>
<Button color="inherit" href="/login">{t('login')}</Button>
<Button onClick={() => changeLanguage(i18n.language === 'en' ? 'zh-CN' : 'en')}>
{i18n.language === 'en' ? '中文' : 'English'}
</Button>
</Box>
</Toolbar>
</AppBar>
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Outlet />
</Container>
<Box sx={{ bgcolor: 'background.paper', py: 3, mt: 'auto' }}>
<Container maxWidth="lg">
<Typography variant="body2" color="text.secondary" align="center">
{t('footer')}
</Typography>
</Container>
</Box>
</ThemeProvider>
);
}
export default App;
|
9. 更新主入口文件
更新main.tsx (src/main.tsx):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from './store';
import router from './routes';
import './i18n';
import { CircularProgress, Box } from '@mui/material';
// 预加载i18n资源
const Preloader = () => (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<CircularProgress />
</Box>
);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<Suspense fallback={<Preloader />}>
<RouterProvider router={router} />
</Suspense>
</Provider>
</React.StrictMode>
);
|
10. 构建和部署
配置Vite (vite.config.ts):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
outDir: 'dist',
sourcemap: process.env.NODE_ENV !== 'production',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
redux: ['redux', 'react-redux', '@reduxjs/toolkit'],
i18n: ['i18next', 'react-i18next'],
mui: ['@mui/material', '@mui/icons-material'],
},
},
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false,
},
},
},
});
|
添加构建脚本 (package.json):
1
2
3
4
5
6
7
8
|
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"extract:locales": "i18next-scanner"
}
}
|
构建项目:
总结
本文深入探讨了前端开发中的三个重要高级主题:安全、性能监控和国际化。这些主题对于构建高质量、可扩展和用户友好的前端应用至关重要。
在前端安全部分,我们学习了常见的安全威胁(如XSS、CSRF、点击劫持等)及其防范措施,以及如何实施安全的编码实践和配置适当的安全头。
在性能监控部分,我们了解了关键性能指标(如Core Web Vitals)、性能监控方法和工具,以及如何构建自己的前端监控系统来跟踪应用性能和错误。
在国际化与本地化部分,我们探讨了国际化的基础概念、常用工具库(如i18next和react-intl)以及实现多语言支持的最佳实践。
最后,通过一个多语言电商应用的实践项目,我们将这些知识整合起来,展示了如何在实际项目中应用这些技术。
到这里,我们的「前端入门到精通」系列就全部结束了。在这个系列中,我们从HTML、CSS和JavaScript的基础开始,逐步深入到前端框架、状态管理、工程化、测试以及高级主题。希望这个系列能够帮助您全面掌握前端开发知识,成为一名优秀的前端开发者!
记住,前端开发是一个不断发展的领域,持续学习和实践是保持技能更新的关键。祝您在前端开发的道路上越走越远!