首页
在线工具
搜索
1
使用Metrics指标度量工具监控Java应用程序性能(Gauges, Counters, Histograms, Meters和 Timers实例)
2
如何将Virtualbox和VMware虚拟机相互转换
3
Jumpserver的MFA配置
4
Markdown正确使用姿势
5
Kuboard与KubeSphere的区别:Kubernetes管理平台对比
杂谈与随笔
工具与效率
源码阅读
技术管理
运维
数据库
前端开发
后端开发
Search
标签搜索
Angular
Docker
Phabricator
SpringBoot
Java
Chrome
SpringSecurity
SpringCloud
DDD
Git
Mac
K8S
Kubernetes
ESLint
SSH
高并发
Eclipse
Javascript
Vim
Centos
Jonathan
累计撰写
86
篇文章
累计收到
0
条评论
首页
栏目
杂谈与随笔
工具与效率
源码阅读
技术管理
运维
数据库
前端开发
后端开发
页面
搜索到
86
篇与
的结果
2019-07-21
Spring Security OAuth2在微前端架构下实现统一退出
Spring Security OAuth2在微前端架构下实现统一退出 在微前端架构中,当一个应用退出登录时,实现所有应用同步退出的功能是一个常见需求。这个问题主要源于微前端架构下各个子应用之间的分离特性,导致一个应用注销后,其他应用仍保持登录状态。下面我将详细介绍如何解决这个问题。 问题分析 微前端架构下,登出问题主要有以下几个原因: 各子应用可能使用独立的会话管理 OAuth2 token可能在各应用中独立存储 缺乏统一的退出机制来协调各个子应用 解决方案 1. 实现单点登出(Single Sign-Out) @RestController public class LogoutController { private final TokenStore tokenStore; private final ClientRegistrationRepository clientRegistrationRepository; public LogoutController(TokenStore tokenStore, ClientRegistrationRepository clientRegistrationRepository) { this.tokenStore = tokenStore; this.clientRegistrationRepository = clientRegistrationRepository; } @PostMapping("/api/logout") public ResponseEntity<Map<String, String>> logout( @RequestParam(value = "token", required = false) String token, HttpServletRequest request, HttpServletResponse response, @AuthenticationPrincipal OAuth2User principal) { // 获取当前认证信息 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 如果存在认证信息,则进行处理 if (auth != null) { // 撤销token if (auth instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) auth; String clientRegistrationId = oauthToken.getAuthorizedClientRegistrationId(); // 根据需要调用OAuth2服务器的撤销接口 revokeToken(clientRegistrationId, token); } // 清除安全上下文 SecurityContextHolder.clearContext(); // 使session失效 HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } // 清除相关cookie deleteCookies(request, response); } Map<String, String> result = new HashMap<>(); result.put("message", "成功退出登录"); return ResponseEntity.ok(result); } private void revokeToken(String clientRegistrationId, String token) { try { ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(clientRegistrationId); if (clientRegistration != null && token != null) { // 根据OAuth2服务器提供的撤销端点进行token撤销 String revokeEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata().get("revocation_endpoint").toString(); RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add("token", token); map.add("client_id", clientRegistration.getClientId()); map.add("client_secret", clientRegistration.getClientSecret()); HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers); restTemplate.postForEntity(revokeEndpoint, request, String.class); } } catch (Exception e) { // 处理可能的异常 logger.error("撤销token失败", e); } } private void deleteCookies(HttpServletRequest request, HttpServletResponse response) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { // 删除认证相关的cookie if (cookie.getName().startsWith("OAUTH2_") || cookie.getName().equals("JSESSIONID") || cookie.getName().equals("SESSION")) { Cookie newCookie = new Cookie(cookie.getName(), null); newCookie.setPath("/"); newCookie.setMaxAge(0); newCookie.setHttpOnly(true); response.addCookie(newCookie); } } } } } 2. 创建统一的登出前端服务 // auth-service.js export class AuthService { constructor() { this.logoutListeners = []; this.setupLogoutListener(); } // 添加登出监听器 addLogoutListener(callback) { this.logoutListeners.push(callback); return () => { this.logoutListeners = this.logoutListeners.filter(cb => cb !== callback); }; } // 发送登出事件到所有注册的应用 notifyLogout() { // 通过BroadcastChannel发送消息 const bc = new BroadcastChannel('auth-logout-channel'); bc.postMessage({ type: 'LOGOUT' }); bc.close(); // 同时触发监听器 this.logoutListeners.forEach(callback => callback()); } // 监听其他应用的登出事件 setupLogoutListener() { const bc = new BroadcastChannel('auth-logout-channel'); bc.onmessage = (event) => { if (event.data.type === 'LOGOUT') { // 执行本地登出操作 this.logout(false); // 不广播,避免循环 } }; } // 登出方法 async logout(broadcast = true) { try { // 调用后端登出API const response = await fetch('/api/logout', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { // 清除本地存储 localStorage.removeItem('oauth2_token'); sessionStorage.removeItem('oauth2_token'); // 清除相关cookie this.clearAuthCookies(); // 通知其他应用 if (broadcast) { this.notifyLogout(); } return true; } else { console.error('登出失败:', await response.text()); return false; } } catch (error) { console.error('登出过程中发生错误:', error); return false; } } // 清除认证相关cookie clearAuthCookies() { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i]; const eqPos = cookie.indexOf('='); const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim(); if (name.startsWith('OAUTH2_') || name === 'JSESSIONID' || name === 'SESSION') { document.cookie = `${name}=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT`; } } } // 检查是否已登录 isAuthenticated() { // 根据实际存储方式检查认证状态 return localStorage.getItem('oauth2_token') !== null || sessionStorage.getItem('oauth2_token') !== null; } } // 导出单例 export const authService = new AuthService(); 3. 创建主应用的登出组件 // LogoutButton.jsx (React组件示例) import React from 'react'; import { authService } from './auth-service'; const LogoutButton = () => { const handleLogout = async () => { const success = await authService.logout(); if (success) { // 重定向到登录页或首页 window.location.href = '/login'; } else { alert('登出失败,请重试'); } }; return ( <button className="logout-button" onClick={handleLogout} > 退出登录 </button> ); }; export default LogoutButton; 4. 为各个微前端应用添加登出监听 // 在每个子应用的入口文件中添加 import { authService } from 'shared-lib/auth-service'; class MicroApp { constructor() { this.init(); } init() { // 注册登出监听器 this.unsubscribeLogout = authService.addLogoutListener(() => { // 当收到登出事件时,清理本地状态 this.handleLogout(); }); // 监听卸载事件,移除监听器 window.addEventListener('unload', () => { if (this.unsubscribeLogout) { this.unsubscribeLogout(); } }); // 设置全局登出处理方法 window.microAppLogout = () => { authService.logout(); }; } handleLogout() { // 清理本地存储 localStorage.removeItem('app_state'); sessionStorage.removeItem('app_state'); // 重置应用状态 this.resetAppState(); // 可选:重定向到登录页 window.location.href = '/login'; } resetAppState() { // 重置应用状态逻辑 // ... } } // 初始化应用 const app = new MicroApp(); 5. 使用Redis实现会话共享 为了确保后端会话的统一管理,应使用Redis存储会话: @Configuration @EnableRedisHttpSession public class RedisSessionConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 使用Jackson2为序列化和反序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); serializer.setObjectMapper(mapper); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } @Bean public HttpSessionIdResolver httpSessionIdResolver() { // 使用Cookie和Header双重策略,提高灵活性 return new CookieAndHeaderHttpSessionIdResolver(); } } // 自定义会话ID解析器,支持Cookie和Header class CookieAndHeaderHttpSessionIdResolver implements HttpSessionIdResolver { private static final String HEADER_NAME = "X-Auth-Token"; private final CookieHttpSessionIdResolver cookieResolver = new CookieHttpSessionIdResolver(); private final HeaderHttpSessionIdResolver headerResolver = HeaderHttpSessionIdResolver.xAuthToken(); @Override public List<String> resolveSessionIds(HttpServletRequest request) { List<String> sessionIds = cookieResolver.resolveSessionIds(request); if (sessionIds.isEmpty()) { sessionIds = headerResolver.resolveSessionIds(request); } return sessionIds; } @Override public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) { cookieResolver.setSessionId(request, response, sessionId); headerResolver.setSessionId(request, response, sessionId); } @Override public void expireSession(HttpServletRequest request, HttpServletResponse response) { cookieResolver.expireSession(request, response); headerResolver.expireSession(request, response); } } 6. 实现OAuth2统一退出端点 为了完全支持OAuth2退出,还需要实现一个注销端点连接到OAuth2服务器: @Configuration public class OAuth2LogoutConfig { private final ClientRegistrationRepository clientRegistrationRepository; public OAuth2LogoutConfig(ClientRegistrationRepository clientRegistrationRepository) { this.clientRegistrationRepository = clientRegistrationRepository; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/public/**", "/login", "/oauth2/authorization/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 .loginPage("/login") .defaultSuccessUrl("/dashboard") ) .logout(logout -> logout .logoutUrl("/api/logout") .logoutSuccessHandler(this.oidcLogoutSuccessHandler()) .invalidateHttpSession(true) .clearAuthentication(true) .deleteCookies("JSESSIONID", "SESSION") ); return http.build(); } private LogoutSuccessHandler oidcLogoutSuccessHandler() { OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository); // 设置退出后的重定向URL oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/login?logout=success"); return (request, response, authentication) -> { // 获取会话中的信息用于撤销token等操作 HttpSession session = request.getSession(false); if (session != null) { String idToken = (String) session.getAttribute("id_token"); if (idToken != null && authentication instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; ClientRegistration clientRegistration = this.clientRegistrationRepository .findByRegistrationId(oauthToken.getAuthorizedClientRegistrationId()); // 尝试撤销令牌 try { revokeTokens(clientRegistration, request); } catch (Exception e) { // 记录错误但继续退出流程 e.printStackTrace(); } } } // 调用OIDC退出处理器 oidcLogoutSuccessHandler.onLogoutSuccess(request, response, authentication); }; } private void revokeTokens(ClientRegistration clientRegistration, HttpServletRequest request) { // 实现令牌撤销逻辑 HttpSession session = request.getSession(false); if (session != null && clientRegistration != null) { String accessToken = (String) session.getAttribute("access_token"); String refreshToken = (String) session.getAttribute("refresh_token"); if (accessToken != null || refreshToken != null) { // 检查是否有撤销端点配置 Object revocationUri = clientRegistration.getProviderDetails() .getConfigurationMetadata().get("revocation_endpoint"); if (revocationUri != null) { RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); // 撤销访问令牌 if (accessToken != null) { MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("token", accessToken); body.add("token_type_hint", "access_token"); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers); restTemplate.postForEntity(revocationUri.toString(), entity, String.class); } // 撤销刷新令牌 if (refreshToken != null) { MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("token", refreshToken); body.add("token_type_hint", "refresh_token"); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers); restTemplate.postForEntity(revocationUri.toString(), entity, String.class); } } } } } } 实现要点解析 统一后端登出端点 清除服务器端会话 撤销OAuth2令牌 删除相关Cookie 通知OAuth2授权服务器 前端统一登出机制 使用BroadcastChannel进行跨应用通信 共享认证服务实现登出状态同步 提供全局登出方法 使用Redis共享会话 确保所有后端服务访问相同的会话存储 支持会话的快速失效 前端存储清理 清除localStorage和sessionStorage中的认证信息 删除认证相关Cookie 最佳实践 使用OpenID Connect(OIDC)标准 利用OIDC的标准登出流程 实现前后端退出的协调 使用统一的会话ID 为所有微前端应用使用相同的会话标识 避免使用多种会话管理方式 实现健壮的错误处理 当部分退出流程失败时仍能完成整体退出 提供清晰的用户反馈 安全考虑 确保退出端点的安全访问 防止CSRF攻击 结论 通过上述方案的实现,可以确保微前端架构下的统一退出机制,使得用户在任一子应用退出后,所有应用都同步退出登录状态。这种方案既保证了用户体验的一致性,也提高了系统的安全性,避免了部分应用仍保持登录状态可能带来的安全风险。 刚好再学react,就用react写前端了
2019年07月21日
2019-07-19
MySQL与PostgreSQL的区别与简单对比
MySQL与PostgreSQL的区别与对比 MySQL和PostgreSQL都是极其流行的关系型数据库管理系统,但它们在设计理念、功能特性和使用场景上存在显著差异。以下是它们之间的主要区别与对比: 基本概述 MySQL: 起源于1995年,现由Oracle公司拥有 以速度和简单性为设计重点 广泛应用于Web应用程序,特别是LAMP架构(Linux, Apache, MySQL, PHP) PostgreSQL: 始于1986年的Berkeley Postgres项目 强调标准合规性和扩展性 被称为"最先进的开源数据库" 主要差异对比 1. 架构与设计理念 MySQL: 多存储引擎架构(InnoDB, MyISAM等) 注重简单性和性能 适合读密集型应用 PostgreSQL: 单一存储引擎 注重数据完整性和功能丰富性 适合复杂查询和大型数据库 2. 事务和ACID支持 MySQL: 在InnoDB引擎中完全支持ACID 其他引擎如MyISAM不完全支持事务 PostgreSQL: 完全支持ACID 提供更强的事务隔离默认级别 3. SQL标准合规性 MySQL: 实现了SQL标准的子集 有一些自定义的SQL扩展 PostgreSQL: 高度遵循SQL标准 支持更多SQL高级特性 4. 数据类型支持 MySQL: 基本数据类型支持良好 对JSON支持较晚引入 PostgreSQL: 丰富的数据类型(如数组、hstore、地理信息类型等) 早期就支持JSON/JSONB,并提供丰富操作函数 支持自定义数据类型 5. 并发控制 MySQL: 主要使用行级锁(InnoDB) 对高并发读取优化较好 PostgreSQL: 使用多版本并发控制(MVCC) 读不阻塞写,写不阻塞读 6. 性能表现 MySQL: 简单查询性能通常更好 读操作性能优秀 PostgreSQL: 复杂查询性能更优 写密集型应用表现优异 大数据量处理能力强 7. 复制与高可用 MySQL: 支持主从复制、组复制 有多种高可用解决方案(MySQL Cluster等) PostgreSQL: 支持流复制、逻辑复制 提供强大的故障转移和高可用功能(如Patroni) 8. 扩展性 MySQL: 插件系统有限 存储过程功能较基础 PostgreSQL: 强大的扩展系统 支持多种编程语言创建存储过程(PL/pgSQL, PL/Python等) 支持自定义运算符和索引类型 适用场景 MySQL更适合: 读密集型网站和应用 需要简单配置和管理的项目 OLTP(联机事务处理)工作负载 对成本敏感的项目 PostgreSQL更适合: 需要复杂查询的数据密集型应用 需要严格数据完整性的场景 地理信息系统(GIS)应用 数据仓库和分析应用 需要自定义数据类型和函数的场景 MySQL与PostgreSQL的SQL编写细节差异与案例 在MySQL和PostgreSQL之间,SQL语法和编写方式存在一些显著差异。以下是一些主要区别和具体案例: 标识符引用方式 MySQL:使用反引号`引用表名和列名 SELECT `user_id`, `username` FROM `users` WHERE `status` = 'active'; PostgreSQL:使用双引号"引用表名和列名 SELECT "user_id", "username" FROM "users" WHERE "status" = 'active'; 自增列定义 MySQL: CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) ); PostgreSQL: CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100) ); 字符串连接 MySQL:使用CONCAT函数 SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM employees; PostgreSQL:可以使用||运算符 SELECT first_name || ' ' || last_name AS full_name FROM employees; 限制结果集 MySQL: SELECT * FROM products ORDER BY price DESC LIMIT 10; PostgreSQL:支持LIMIT但也支持标准SQL的FETCH -- 使用LIMIT(类似MySQL) SELECT * FROM products ORDER BY price DESC LIMIT 10; -- 使用FETCH(SQL标准) SELECT * FROM products ORDER BY price DESC FETCH FIRST 10 ROWS ONLY; 日期时间处理 MySQL: -- 当前日期时间 SELECT NOW(), CURDATE(), DATE_FORMAT(created_at, '%Y-%m-%d'); -- 日期计算 SELECT DATE_ADD(order_date, INTERVAL 30 DAY) FROM orders; PostgreSQL: -- 当前日期时间 SELECT CURRENT_TIMESTAMP, CURRENT_DATE, TO_CHAR(created_at, 'YYYY-MM-DD'); -- 日期计算 SELECT order_date + INTERVAL '30 days' FROM orders; 正则表达式 MySQL: SELECT * FROM products WHERE product_name REGEXP '^Apple'; PostgreSQL: SELECT * FROM products WHERE product_name ~ '^Apple'; UPSERT操作(插入或更新) MySQL: INSERT INTO customers (id, name, email) VALUES (1, 'John Doe', 'john@example.com') ON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email); PostgreSQL: INSERT INTO customers (id, name, email) VALUES (1, 'John Doe', 'john@example.com') ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email; 分页查询案例 MySQL: -- 第3页,每页20条记录 SELECT * FROM products ORDER BY created_at DESC LIMIT 20 OFFSET 40; PostgreSQL: -- 同样功能,但有多种写法 SELECT * FROM products ORDER BY created_at DESC LIMIT 20 OFFSET 40; -- 或使用FETCH SELECT * FROM products ORDER BY created_at DESC OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY; 全文搜索案例 MySQL: -- 首先添加全文索引 ALTER TABLE articles ADD FULLTEXT INDEX idx_content (title, content); -- 使用全文搜索 SELECT * FROM articles WHERE MATCH(title, content) AGAINST('database performance' IN BOOLEAN MODE); PostgreSQL: -- 创建tsvector列或索引 CREATE INDEX idx_fts ON articles USING GIN (to_tsvector('english', title || ' ' || content)); -- 使用全文搜索 SELECT * FROM articles WHERE to_tsvector('english', title || ' ' || content) @@ to_tsquery('english', 'database & performance'); JSON数据处理 MySQL: -- 创建表 CREATE TABLE user_data ( id INT PRIMARY KEY, profile JSON ); -- 插入JSON数据 INSERT INTO user_data VALUES (1, '{"name": "John", "preferences": {"theme": "dark", "notifications": true}}'); -- 查询JSON数据 SELECT id, JSON_EXTRACT(profile, '$.name') AS name, JSON_EXTRACT(profile, '$.preferences.theme') AS theme FROM user_data; PostgreSQL: -- 创建表 CREATE TABLE user_data ( id INT PRIMARY KEY, profile JSONB ); -- 插入JSON数据 INSERT INTO user_data VALUES (1, '{"name": "John", "preferences": {"theme": "dark", "notifications": true}}'); -- 查询JSON数据 SELECT id, profile->>'name' AS name, profile->'preferences'->>'theme' AS theme FROM user_data; -- 使用JSONB特有的包含操作符 SELECT * FROM user_data WHERE profile @> '{"preferences": {"theme": "dark"}}'; 递归查询案例(树形结构) MySQL:使用CTE (Common Table Expressions) -- 查询组织层次结构 WITH RECURSIVE org_hierarchy AS ( -- 基本情况:顶级节点 SELECT id, name, manager_id, 1 AS level FROM employees WHERE manager_id IS NULL UNION ALL -- 递归情况:子节点 SELECT e.id, e.name, e.manager_id, oh.level + 1 FROM employees e JOIN org_hierarchy oh ON e.manager_id = oh.id ) SELECT * FROM org_hierarchy ORDER BY level, id; PostgreSQL:同样使用CTE,但有更多功能 -- 查询组织层次结构 WITH RECURSIVE org_hierarchy AS ( -- 基本情况:顶级节点 SELECT id, name, manager_id, 1 AS level, ARRAY[name] AS path, name::text AS full_path FROM employees WHERE manager_id IS NULL UNION ALL -- 递归情况:子节点 SELECT e.id, e.name, e.manager_id, oh.level + 1, oh.path || e.name, oh.full_path || ' > ' || e.name FROM employees e JOIN org_hierarchy oh ON e.manager_id = oh.id ) SELECT id, name, level, full_path FROM org_hierarchy ORDER BY path; 结论 这些例子展示了MySQL和PostgreSQL在SQL编写上的细微但重要的差异。在迁移数据库或者开发跨数据库应用时,了解这些差异可以帮助开发人员避免常见的陷阱和错误。PostgreSQL通常更紧密地遵循SQL标准,而MySQL则有更多的专有语法和简化操作。 MySQL和PostgreSQL各有优势,选择哪一个应当基于具体项目需求。MySQL以其简单性、性能和广泛采用而闻名,而PostgreSQL则以其功能丰富性、可扩展性和对复杂数据操作的支持著称。随着两者的不断发展,它们之间的差距正在缩小,但基本设计理念的差异依然明显。
2019年07月19日
2019-06-12
Tencent Lemon:Mac 用户必备的系统清理与优化工具
Tencent Lemon:Mac 用户必备的系统清理与优化工具 对于 Mac 用户来说,长时间使用后系统中会积累大量无用文件,导致存储空间减少、系统运行变慢。这时,一款高效的系统清理工具就显得尤为重要。今天要推荐的是 Tencent Lemon,一款由腾讯推出的 Mac 系统优化软件,集垃圾清理、应用管理、系统监测等功能于一体,让你的 Mac 保持最佳状态。 Tencent Lemon 是什么? Tencent Lemon 是腾讯开发的一款专为 Mac 设计的免费系统优化工具。它不仅能快速清理系统垃圾,还提供软件卸载、重复文件查找、磁盘分析等多种实用功能,帮助用户高效管理 Mac 设备。 主要功能 1. 智能垃圾清理 一键扫描系统垃圾,包括缓存、日志文件、无用安装包等。 支持深度清理 Xcode、浏览器缓存、iTunes 垃圾文件等。 2. 软件管理与卸载 可查看已安装软件,并进行彻底卸载,避免残留文件。 支持批量卸载,节省时间。 3. 磁盘空间分析 直观显示磁盘占用情况,帮助找出占用空间过大的文件。 支持一键删除大文件,释放磁盘空间。 4. 重复文件查找 轻松找出 Mac 中的重复图片、文档、视频等,避免存储浪费。 预览后选择删除,确保数据安全。 5. 系统状态监测 显示 CPU、内存、网络流量等信息,帮助用户实时了解 Mac 运行状况。 可清理内存占用,提升运行速度。 6. 简单易用的 UI 设计 界面简洁直观,所有功能一目了然。 支持一键清理,适合小白用户。 使用方法 下载安装 Tencent Lemon 访问腾讯官网或应用商店下载并安装。 清理系统垃圾 打开软件,点击“智能清理”按钮,扫描垃圾文件。 选择要删除的文件,一键清理。 管理应用与磁盘空间 进入“软件管理”查看已安装应用,选择不需要的软件进行卸载。 进入“磁盘分析”,找到大文件或重复文件,释放存储空间。 Tencent Lemon VS 其他 Mac 清理工具 功能 Tencent Lemon CleanMyMac DaisyDisk 价格 免费 付费 付费 垃圾清理 ✅ ✅ ❌ 软件卸载 ✅ ✅ ❌ 磁盘分析 ✅ ✅ ✅ 重复文件查找 ✅ ✅ ❌ 为什么选择 Tencent Lemon? ✅ 完全免费,没有隐藏收费 ✅ 界面简洁,易上手 ✅ 清理+优化二合一,无需安装多个软件 ✅ 腾讯出品,兼容性强,长期维护 适用人群 Mac 运行缓慢的用户:定期清理垃圾文件,提升系统速度。 存储空间不足的用户:释放磁盘空间,管理大文件。 不懂手动清理的用户:一键智能清理,简单高效。 需要卸载顽固软件的用户:彻底删除应用,避免残留。 结语 如果你想让 Mac 更流畅、更整洁、更高效,那么 Tencent Lemon 绝对是你的最佳选择。它不仅功能强大,而且完全免费,让你轻松管理 Mac 设备。快去下载试试吧!
2019年06月12日
2019-04-28
React-Native学习笔记
React-Native学习笔记 起步注意 不要使用别人的demo code做练习,因为react-native更新很快,第三方插件更新也很快,如果你使用的是最新版本,别人代码嵌入你自己的demo将举步维艰。 基础看完重点放在官方插件上。 react-navigation重点:一般搭建app基础工程,app肯定需要导航,路由等,官方推荐react-navigation。注意线上中文相关文档帖子大部分都是3.*之前的版本,按其安装会报一些错,下面碰到的几个列一下。 常规错误 Failed to load bundle(http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false) with error:(Unable to resolve module `react-native-gesture-handler` from `/Users/apple/Code/04-IOS/pixeleye/node_modules/@react-navigation/native/src/Scrollables.js`: Module `react-native-gesture-handler` does not exist in the Haste module map This might be related to https://github.com/facebook/react-native/issues/4968 To resolve try the following: 1. Clear watchman watches: `watchman watch-del-all`. 2. Delete the `node_modules` folder: `rm -rf node_modules && npm install`. 3. Reset Metro Bundler cache: `rm -rf /tmp/metro-bundler-cache-*` or `npm start -- --reset-cache`. 4. Remove haste cache: `rm -rf /tmp/haste-map-react-native-packager-*`. (null)) __38-[RCTCxxBridge loadSource:onProgress:]_block_invoke.228 RCTCxxBridge.mm:414 ___ZL36attemptAsynchronousLoadOfBundleAtURLP5NSURLU13block_pointerFvP18RCTLoadingProgressEU13block_pointerFvP7NSErrorP9RCTSourceE_block_invoke.118 __80-[RCTMultipartDataTask URLSession:streamTask:didBecomeInputStream:outputStream:]_block_invoke -[RCTMultipartStreamReader emitChunk:headers:callback:done:] -[RCTMultipartStreamReader readAllPartsWithCompletionCallback:progressCallback:] -[RCTMultipartDataTask URLSession:streamTask:didBecomeInputStream:outputStream:] __88-[NSURLSession delegate_streamTask:didBecomeInputStream:outputStream:completionHandler:]_block_invoke __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ -[NSBlockOperation main] -[__NSOperationInternal _start:] __NSOQSchedule_f _dispatch_call_block_and_release _dispatch_client_callout _dispatch_continuation_pop _dispatch_async_redirect_invoke _dispatch_root_queue_drain _dispatch_worker_thread2 _pthread_wqthread start_wqthread 如果是react-navigation相关的,原因是没有安装react-native-gesture-handler,安装即可。 npm install --save react-native-gesture-handler 重新编译。 Unhandled JS Exception: undefined is not an object (evaluating '_react.default.PropTypes.string') RCTFatal -[RCTExceptionsManager reportFatalException:stack:exceptionId:] __invoking___ -[NSInvocation invoke] -[NSInvocation invokeWithTarget:] -[RCTModuleMethod invokeWithBridge:module:arguments:] facebook::react::invokeInner(RCTBridge*, RCTModuleData*, unsigned int, folly::dynamic const&) facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)::$_0::operator()() const invocation function for block in facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int) _dispatch_call_block_and_release _dispatch_client_callout _dispatch_lane_serial_drain _dispatch_lane_invoke _dispatch_workloop_worker_thread _pthread_wqthread start_wqthread 新版本prop-types独立出来,如果使用就接口会找不到,需要安装对应插件。 npm install prop-types --save react-navigation注意事项 避免导航器内部的screen在渲染另一个导航器。 参考https://reactnavigation.org/docs/en/common-mistakes.html说明。如果确实需要这么使用在组件标签内需要添加navigation={this.props.navigation}。 如果将AppContainer包装在View中,请确保View正在使用flex。这个不知道什么原理,贴出demo。 import React from 'react'; import { Text, View } from 'react-native'; import { createBottomTabNavigator, createAppContainer } from 'react-navigation'; class HomeScreen extends React.Component { render() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Home!</Text> </View> ); } } class SettingsScreen extends React.Component { render() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Settings!</Text> </View> ); } } const TabNavigator = createBottomTabNavigator({ Home: HomeScreen, Settings: SettingsScreen, }); const AppContainer = createAppContainer(TabNavigator); // without the style you will see a blank screen export default () =><View style={{flex: 1}}><AppContainer/></View>; react-navigation@2.14.0之前是all screens are essentially regular native View in each platform所以增加了内存的消耗 (官方原话就不翻译了,不知道platform指的是什么,是android/ios还是手机)。 react-navigation的动态路由:官方不推荐使用动态路由,React Navigation目前在静态定义路由的情况下效果最佳的。如果真的需要动态路由参考https://reactnavigation.org/docs/en/params.html 。 React-navigation在3D触控设备上不支持peek & pop功能。 注意:官方文档import Ionicons from 'react-native-vector-icons/Ionicons';替换成import Icon from 'react-native-vector-icons/Ionicons';。 高阶组件:withNavigation是一个高阶组件,它可以将navigation这个prop传递到一个包装的组件。 当你无法直接将navigation这个prop传递给组件,或者不想在深度嵌套的子组件中传递它时,它将非常有用。 屏幕进入或者推出时出发函数(比如比如停止播放视频或音频文件,或停止跟踪用户的位置。) 使用react-navigation提供的withNavigationFocus高阶组件。 使用事件监听器收听'didFocus'事件。 参考https://reactnavigation.org/docs/zh-Hans/function-after-focusing-screen.html 其他插件 React-native开发-Unrecognized font family ‘Ionicons’ 解决:https://blog.csdn.net/zhaolaoda2012/article/details/82627735 新版本的Ionicons已经替换为Icon。 import Icon from 'react-native-vector-icons/Ionicons';
2019年04月28日
2019-04-05
TIDB虚拟机部署笔记
TIDB学习笔记 tidb用户密码:tidb asd123ASD ssh 密钥 SHA256:YSA6u5H8Zn5qTL2XmIzivKeoTw0AAYWnQPfxBQBxx4s tidb@tidb-ansible-101 The key's randomart image is: +---[RSA 2048]----+ |*=.++++o.. | |+ o.o.+o. | |ooo ...+ | |.o + E o . | | * . S | | *. . | | oo=o + . | | +.+= * o | |+o**oo . | +----[SHA256]-----+ 部署注意 1.机器的计算机名不能相同 2.初始化参数时报错 This machine does not have sufficient CPU to run TiDB, at least 8 cores 类似相关硬件限制问题可修改roles/check_system_optional/defaults/main.yml文件里的参数来解决如上面问题可以修改tidb_min_cpu参数 注意,修改参数时不能在行尾部留空格,不然报错 3.报 fatal: [192.168.56.223]: FAILED! => {"changed": false, "failed_when_result": true, "rc": 0, "stderr": "Shared connection to 192.168.56.223 closed.\r\n", "stderr_lines": ["Shared connection to 192.168.56.223 closed."], "stdout": "/dev/mapper/centos-root 8.0G 8.0G 280K 100% /\r\n", "stdout_lines": ["/dev/mapper/centos-root 8.0G 8.0G 280K 100% /"]} 错误tikv虚拟机根目录分区太小,需要扩容 https://www.codetd.com/article/2284025 虚拟机安装可参考此篇 https://blog.csdn.net/sunny05296/article/details/65980897/ npt时间同步配置可参考此篇 执行:ansible-playbook bootstrap.yml 如果报 fio randread iops of deploy_dir disk is too low 原因是:硬盘太慢,tidb对硬盘的IOPS有一定的要求,要大于40000,生产环境官方建议使用 NVMe SSD 硬盘 使用dev模式 参考https://github.com/pingcap/tidb-ansible/issues/521 ansible-playbook bootstrap.yml --extra-vars "dev_mode=True" Grafana Web 默认账号: admin 密码: admin 参考此篇安装https://www.codetd.com/article/2284025 注意:主机名一定不能冲突,hostnamectl set-hostname tidb-server-191 常见错误:http://1bb.work/jxadd/article_detail/3426
2019年04月05日
1
...
5
6
7
...
18