# 王郁的小站 - 全部文章全文 > 王郁(Wang Yu)的个人博客 wycode.cn 所有文章的 markdown 全文拼接,按发布日期倒序排列。单篇文章的 agent 可读文档见 llms.txt 中的博客文章列表。 ## 目录 - [大模型高考测评:用一套自动化流水线,来测试大模型智力](#gaokao-01): 2026-04-29 - [AI 驱动的数据库可视化面板](#ai-generated-dashboard): 2026-03-21 - [如何使用 Docker 安全地部署 OpenClaw (龙虾)](#openclaw-docker): 2026-03-16 - [Rust SQLite WebUI - 极致轻量级的SQLite管理工具](#rust-sqlite-webui): 2026-01-01 - [《始于极限》标注](#start-from-limitation): 2025-08-20 - [这一波AI浪潮是否是泡沫?](#ai): 2024-10-30 - [Caddy的安装和使用](#caddy): 2024-09-24 - [使用Vite构建React项目](#vite): 2023-11-11 - [解决 invalid format git@github.com: Permission denied (publickey).](#ssh-key): 2023-09-11 - [ChatGPT是如何被训练出来的](#chat-gpt): 2022-12-01 - [升级到HTTP3](#http3): 2022-09-29 - [esModule + NodeJS + TypeScript 的工程配置](#esm-node-ts): 2022-09-22 - [《乌合之众:大众心理研究》热门标注](#the-crowd): 2021-07-22 - [基于Docker的自动化部署](#auto-deploy): 2021-05-09 - [理解Rust语言的所有权以及借用、引用、切片。](#rust-ownership): 2021-01-23 - [备份和还原Docker容器中的MongoDB数据](#docker-mongodb): 2020-12-05 - [使用Github Action进行持续集成](#github-action): 2020-10-28 - [用数学期望计算《和平精英》特斯拉(玛莎拉蒂)到底要多少钱?](#chicken-dinner-tesla): 2020-07-24 - [使用WebSocket和STOMP协议开发一个匿名聊天室](#websocket): 2020-03-09 - [使用Docker容器部署SpringBoot项目](#docker-spring): 2019-11-20 - [VirtualBox的虚拟机网络配置](#virtualbox-network): 2019-09-20 - [实现内网穿透的最新姿势](#ngrok): 2019-09-02 - [H2数据库修改密码](#h2-password): 2019-08-14 - [用树莓派4B做一个鱼缸水位报警](#raspberrypi-fish): 2019-08-07 - [树莓派4B系统安装及配置](#raspberrypi): 2019-08-07 - [《富爸爸,穷爸爸》热门标注](#rich-dad): 2019-07-26 - [使用Flutter开发Web应用](#flutter-web): 2019-07-25 - [SpringBoot工程为SwaggerUI添加认证](#swagger-auth): 2019-07-19 - [SpringBoot工程自定义错误JSON字段](#spring-error): 2019-07-15 - [OAuth2.0四种模式完整时序图](#oauth): 2019-06-14 - [学习和拆解Ant-Design-Pro](#ant-design-pro): 2019-06-09 - [使用SpringBoot发送SSL邮件](#email): 2019-06-07 - [从0开始开发一个评论系统](#comments-sql): 2019-05-21 - [使用TravisCI进行持续集成](#travis-ci): 2019-05-20 - [NGINX配置重定向](#nginx-redirect): 2019-05-17 - [使用Flutter开发跨平台应用](#flutter): 2019-03-28 - [使用Netty和阿里云函数写一个低成本的“梯子”](#netty-function): 2018-10-23 - [使用Netty开发高性能的网络服务应用](#netty): 2018-10-16 - [理解Spring WebFlux框架中的响应式类型](#web-flux): 2018-07-20 - [Ubuntu16.04安装配置NGINX服务](#nginx): 2018-06-23 - [SpringBoot配置Jar外部静态资源](#springboot-static-content): 2018-06-23 - [Spring JPA框架中的@ManyToMany多对多关系的理解和使用](#many-to-many): 2018-04-19 - [SwaggerUI GET请求中文乱码问题解决](#tomcat): 2018-04-11 - [数据结构与算法学习笔记03](#data-structures03): 2018-03-26 - [React Native源码解析(Android篇)](#react-native): 2018-03-19 - [数据结构与算法学习笔记02](#data-structures02): 2018-03-16 - [数据结构与算法学习笔记01](#data-structures01): 2018-03-14 - [OpenCV入门](#opencv): 2018-03-08 - [用React做一个跨平台剪切板](#clipboard-react): 2018-02-13 - [微信小程序入门](#clipboard-wechatapp): 2018-02-13 - [《未来简史》读书笔记](#home-deus): 2018-01-29 - [RxJava入门](#rxjava): 2018-01-20 - [React框架原理的思考](#react): 2018-01-06 - [Angular框架原理的思考](#angular): 2017-12-28 - [使用JWT和Spring Security结合开发一套认证鉴权系统](#jwt): 2017-11-17 - [使用Vue搭建一个后台管理系统](#vue): 2017-11-16 - [安装Node.js和npm的最佳实践](#nodejs): 2017-11-14 - [Spring Boot + Spring Security + H2 + Thymeleaf做一个后台管理系统](#web-framework): 2017-10-26 - [使用swagger为Spring REST API生成在线文档](#swagger): 2017-10-23 - [Android度量设计规范](#android-dp): 2017-09-04 - [Gradle构建变种版本(多渠道打包)](#gradle-build): 2017-09-04 - [Kotlin入门](#kotlin-learn): 2017-07-27 - [Spring中启用CORS](#spring-cors): 2017-07-27 - [在Unity中使用Android原生代码](#unity2android): 2017-07-26 - [自定义形状波浪进度](#wave-progress): 2017-07-24 - [Blender建模入门](#blender-learn): 2017-07-18 - [Unity中的JSON解析](#unity-json): 2017-07-12 - [Unity中的网络请求](#unity-web-request): 2017-07-12 - [使用SpringJPA框架](#spring-jpa): 2017-07-11 - [Spring中的application.properties](#spring-properties): 2017-07-11 - [Unity脚本生命周期](#unity-lifecycle): 2017-07-07 - [Unity平台判断](#unity-platform): 2017-06-27 - [可能是史上最简单的vi/vim教程](#linux-vi): 2017-06-21 - [SEO:优化网站排名](#SEO): 2017-06-19 - [Android原生分享及第三方分享](#Share): 2017-06-16 - [Android Studio工程应该忽略的文件及如何使用gitignore](#gitignore): 2017-06-16 - [为Hexo博客添加全文搜索](#search): 2017-06-15 - [自定义Hexo主题](#theme): 2017-06-15 - [为HTML中的代码添加语法高亮](#highlight): 2017-06-14 - [给自己的网站添加评论功能](#comments): 2017-06-09 - [我为什么开始写博客了?](#why-blog): 2017-06-08 --- ## 大模型高考测评:用一套自动化流水线,来测试大模型智力 > 原文: https://wycode.cn/blog/gaokao-01 | Agent 文档: https://wycode.cn/blog/gaokao-01.md | 日期: 2026-04-29 | 分类: AI > 本文更新于 2026-09-15。早期版本只覆盖 6 个模型、6 个科目,如今项目已演进为 70+ 模型、7 科目的持续测评,本文改为从整体介绍项目本身:考什么、怎么考、怎么判分,不再针对某个具体模型展开。 现在的模型榜单很多,但站在中文语境下看逻辑、知识和综合能力,没有什么比**高考**更合适:题目公开、难度稳定、有标准答案,还自带文理分科。 我做的 **llm-gaokao(大模型高考成绩榜)**,就是让大模型去做一整套高考卷:从抽题、答题、阅卷到榜单,全流程自动化,可重复、可追溯。在线榜单见 [https://wycode.cn/gaokao](https://wycode.cn/gaokao)。 ## 考什么:7 科、总分 750 目前榜单固定为 7 个科目,总分 750:数学 150、语文 150、英语 120(不含听力)、物理 100、化学 100、生物 100、编程 30。 卷源大致是:语数英用较新的全国新高考一卷,物化生用地方卷原始分(未赋分),编程则是 3 道自研的单轮代码题(覆盖方程组、插值、单纯形,4 门语言、120 个测试用例)。英语去掉的 30 分听力,正好由编程 30 分补齐,所以总分恒为 750,换卷也不变。 模型侧通过一个配置文件接入,目前覆盖 Claude、GPT、Gemini、Doubao、DeepSeek、Qwen、Kimi 等 30+ 模型,OpenAI 兼容接口即可接入,是否支持图片、价格、开源与否都会如实标注。 ## 测试方法 为排除harness影响,所有模型均使用API测试。一题一测。 ## 大模型已经训练过高考题了? 1. 高考试题有版权 2. 高考试题存在大量主观题 3. 高考题并不是高质量语料 4. 训题不如训课本 5. 大多数模型的知识库早于 2026年6月 6. 去除工具让模型回忆真题,没有模型可以回答正确 ## 架构:一条四段式流水线 整个项目可以理解为一条流水线,只有四个环节: 1. **结构化提取**:把试卷原卷(图文)转成统一的 JSON 题库,题型、满分、答案、配图都结构化。 2. **答题**:逐题调用模型,客观题只收最终答案,主观题要求写出完整过程,统一用标签约束格式以方便判分;支持断点续传和多模态图片输入。 3. **评分**:客观题程序自动判分,主观题由大模型对照标准答案给分,作文采用双模型平均以减少偏好。 4. **榜单生成**:汇总分科分、总分、Token、成本与用时,预计算派生指标,输出给前端渲染,并对历史版本做归档快照。 这套设计刻意把“算法”收敛到一个地方:所有折算和派生分数只在一处定义,榜单生成时连同参数快照一起写入结果,历史归档版本不再被新算法改写,保证跨时间可比。 ## 评分:不只看总分 总分之外,榜单还有三个视角,帮你回答不同的选型问题: - **分科分**:各科按目标满分折算,哪里偏科一目了然。 - **智力分**:分数越高、输出 Token 越少,得分越高,衡量“答得好还答得利索”。 - **性价比**:分数越高、花钱越少,得分越高,衡量“多少钱办多少事”。 另外 Token 用量、花费、耗时、错题与缺考都会完整记录。缺考、不支持图片等情况会在榜单上明确标出,不拿缺考当低分混过去。 ## 榜单怎么看 建议这样看榜:先看总分定档,再看分科雷达找偏科,然后看**答题卡**定位具体哪道题丢分,编程则看 12 宫格(3 题 × 4 语言)的用例矩阵。版本下拉框可以切回历史快照,对比换卷前后的变化。 公平性上做了几件小事:所有模型统一思考档位,判分模型固定且公开,归档快照不可变,重置和重测都有明确命令,避免新旧题目分数混在一起。 ## 结尾 这个项目会持续更新:加模型、换新卷、优化判分,榜单活数据随之滚动,历史版本则永久保留。如果你想看某个模型参加高考,或者对题库、判分有建议,欢迎在评论区留言。 **完整榜单地址**:[https://wycode.cn/gaokao](https://wycode.cn/gaokao) --- ## AI 驱动的数据库可视化面板 > 原文: https://wycode.cn/blog/ai-generated-dashboard | Agent 文档: https://wycode.cn/blog/ai-generated-dashboard.md | 日期: 2026-03-21 | 分类: AI ![AI-Powered Data Visualizer](/images/20260321_dashboard.jpg) 在日常开发和数据分析中,我们经常需要从数据库中提取关键信息并将其可视化。虽然有很多优秀的 BI 工具(如 Metabase, Superset 等),但它们对于个人开发者或小型项目来说,往往存在以下问题: 1. **学习成本高**:需要学习复杂的 SQL 编写和报表配置流程。 2. **部署繁琐**:通常需要配套复杂的后端服务和权限管理系统。 3. **响应不够直观**:从数据到图表的转化过程不够"智能",依然需要大量手动调整。 为了解决这些痛点,我开发了 **AI-Powered Data Visualizer**。它的目标非常明确:**让数据可视化变得像聊天一样简单**。 * **AI 智能驱动**:通过自然语言对话,自动生成 SQL 并转化为可视化图表。 * **全栈 Next.js 架构**:极致的响应速度和现代化的 UI 体验。 同时,这也是一个非常好的 `Next.js + AI` 全栈开发实践项目。 ## 技术栈选择 (Why this way?) 为了实现智能化和易用性的目标,技术栈的选择至关重要。 ### 核心框架:Next.js + TypeScript * **Next.js**:作为 React 生态中最成熟的全栈框架,提供了极佳的开发体验和性能表现。 * **TypeScript**:保证了代码的健壮性和可维护性,特别是在处理复杂的 AI 响应数据时。 ### AI 交互:Vercel AI SDK + OpenAI/DeepSeek * **Vercel AI SDK**:提供了流式输出(Streaming)和高度集成的 AI 钩子,让聊天界面极其流畅。 * **多模型支持**:支持 Doubao, OpenAI, Anthropic, DeepSeek 等主流 AI 提供商,灵活切换。 ### 数据可视化:Vega-Lite * **Vega-Lite**:一种声明式的高级可视化语法。AI 只需生成 JSON 配置,即可渲染出美观、交互性强的图表,非常适合与 AI 结合。 ### 数据库:SQLite + Knex.js * **SQLite**:轻量级且零配置,非常适合作为可视化工具的后端存储或分析对象。 * **Knex.js**:强大的 SQL 查询构建器,方便 AI 生成和执行安全的数据库操作。 ## 技术实现细节与亮点 ### 1. 智能数据架构分析 系统会自动扫描数据库表结构(Schema),并将关键信息作为上下文提供给 AI。这使得 AI 能够理解数据之间的关联,从而生成准确的分析建议,而无需用户手动输入复杂的 SQL。 ### 2. 声明式图表生成 不同于 ECharts 等命令式框架,Vega-Lite 允许 AI 通过描述"数据如何映射到图形属性"来生成图表。 这种方式极大地降低了 AI 生成错误代码的概率,同时也让生成的图表具备了统一的风格和高度的自定义能力。 ### 3. 灵活的持久化存储 用户通过对话生成的图表,可以一键保存到个人仪表盘(Dashboard)中。仪表盘状态使用 **Zustand** 进行轻量级状态管理,并持久化到本地数据库,方便随时查看。 ### 4. 安全性设计 (Read-Only) 数据安全至关重要。系统默认以**只读模式**(Read-Only)连接用户数据库,确保 AI 即使在极端情况下也不会修改或删除原始数据。同时内置了管理密码保护,防止未授权的配置访问。 ## 功能与使用 ### 主要功能 * **自然语言对话**:通过"帮我统计上个月的销售趋势"等指令生成图表。 * **多数据库支持**:适配 MySQL, PostgreSQL 和 SQLite。 * **可视化建议**:AI 会根据数据特征自动推荐合适的图表类型。 * **自定义仪表盘**:自由组织、编辑和保存 AI 生成的图表。 ### 快速部署 推荐使用 Docker 进行部署,极其简单: ```bash docker run -d \ -p 3000:3000 \ -e API_KEY=your_api_key \ -e ADMIN_PASSWORD=your_secure_password \ -v /path/to/your/db:/app/data/db/sqlite.db:ro \ --name ai-dashboard \ wangyucode/ai-generated-dashboard ``` ### 配置说明 * `API_KEY`:AI 服务的密钥。 * `ADMIN_PASSWORD`:管理界面的登录密码。 * 挂载目录:将你的 SQLite 数据库文件挂载到容器的 `/app/data/db/sqlite.db` 即可开始分析。 ## 总结 如果你正在寻找一个能帮你快速从数据库中"挖掘"价值,且无需编写代码的可视化工具,**AI-Powered Data Visualizer** 绝对值得一试。 项目地址:https://github.com/wangyucode/ai-generated-dashboard --- ## 如何使用 Docker 安全地部署 OpenClaw (龙虾) > 原文: https://wycode.cn/blog/openclaw-docker | Agent 文档: https://wycode.cn/blog/openclaw-docker.md | 日期: 2026-03-16 | 分类: AI ![OpenClaw](https://ts3.tc.mm.bing.net/th/id/OIP-C.62zMyXVhuvfDknsQBUJEZgHaEo?rs=1&pid=ImgDetMain&o=7&rm=3) > OpenClaw (龙虾) 是一个强大的个人 AI 助手,它可以连接各种消息平台并执行工具。本文介绍如何通过 Docker Compose 安全地部署 OpenClaw,并解析其中的关键配置。 OpenClaw 提供了强大的 AI 能力,但也意味着它需要访问你的文件、工具和 API 密钥。因此,**安全性**是部署时的首要考量。 参考这个 GitHub 仓库:[wangyucode/openclaw-docker-compose](https://github.com/wangyucode/openclaw-docker-compose) ## 部署步骤 ### 1. 克隆仓库与配置环境 首先,克隆部署配置仓库: ```bash git clone https://github.com/wangyucode/openclaw-docker-compose.git cd openclaw-docker-compose ``` ### 2. 执行初始化 (Onboarding) 在启动网关之前,需要运行一次初始化命令来生成必要的配置文件: ```bash docker compose run --rm openclaw-cli onboard ``` ### 3. 启动 OpenClaw 一切就绪后,启动 Docker 容器: ```bash docker compose up -d ``` ## 关键配置解析 为什么我们要这样编写 `docker-compose.yml`?以下是几个关键的安全配置点: ### 非 Root 用户运行 ```yaml user: "1000:1000" ``` 默认情况下,Docker 容器以 root 用户运行。如果容器被攻破,攻击者可能获得宿主机的 root 权限。通过指定 `user: "1000:1000"`(通常是宿主机的第一个普通用户),我们可以最小化权限。 ### 禁用权限提升 ```yaml security_opt: - no-new-privileges:true ``` 这项配置可以防止容器内的进程通过 `setuid` 或 `setgid` 二进制文件获得新的权限。即使攻击者在容器内找到了漏洞,也无法提升权限。 ### 移除不必要的 Capability ```yaml cap_drop: - ALL ``` Linux Capabilities 将 root 的特权细分为多个小的权限。默认情况下,Docker 会保留一些常用的权限。通过 `cap_drop: [ALL]`,我们移除了所有特权,只保留最基础的运行环境。 ### 持久化存储与权限 ```yaml volumes: - ./.openclaw:/home/node/.openclaw ``` 将配置文件和工作区映射到宿主机目录,方便备份和管理。注意宿主机目录的权限应与 `user` 配置一致(例如 `chown -R 1000:1000 .openclaw`)。 ## 访问控制 OpenClaw 默认运行在 `18789` 端口。访问 Dashboard 时需要一个 Token,你可以通过以下命令获取: ```bash docker compose run --rm openclaw-cli dashboard --no-open ``` ## 总结 通过 Docker 部署 OpenClaw 不仅简化了环境配置,更重要的是通过容器化技术提供了良好的安全隔离。遵循“最小权限原则”,我们可以更放心地让 AI 助手为我们工作。 --- ## Rust SQLite WebUI - 极致轻量级的SQLite管理工具 > 原文: https://wycode.cn/blog/rust-sqlite-webui | Agent 文档: https://wycode.cn/blog/rust-sqlite-webui.md | 日期: 2026-01-01 | 分类: Backend ![Rust SQLite WebUI](/apps/sqlite.png) 在日常开发和运维中,我们经常需要查看服务器上的 SQLite 数据库文件。虽然有很多优秀的 SQLite 管理工具(如 DBeaver, SQLite Browser 等),但它们大多是桌面端软件,无法直接在浏览器中运行。而现有的 Web 版 SQLite 管理工具,往往存在以下问题: 1. **体积过大**:动辄几百兆的 Docker 镜像,对于小型的 VPS 或者边缘设备来说,显得过于臃肿。 2. **内存占用高**:一些基于 Python 或 Node.js 的解决方案,运行时内存占用较高。 3. **部署复杂**:依赖繁多,配置麻烦。 为了解决这些痛点,我开发了 **Rust SQLite WebUI**。它的目标非常明确:**极致轻量** , **高性能** 并且 **支持AI** 。 * **Docker 镜像大小**:仅约 **10MB**。 * **运行时内存占用**:仅约 **1MB**。 * **支持 AI 智能助手**:自然语言转 SQL、SQL 纠错,兼容 OpenAI 及 DeepSeek 协议。 同时,这也是一个非常好的 **Rust (Axum) + Solid.js (Vite 7 + Tailwind 4)** 全栈开发实践项目。 项目已开源在 GitHub: [https://github.com/wangyucode/rust-sqlite-webui](https://github.com/wangyucode/rust-sqlite-webui) ## 技术栈:追求极致 (Cutting-edge Stack) 为了达到极致轻量和高性能的目标,技术栈的选择至关重要。 ### 后端:Rust + Axum 0.8 * **Rust**:提供了零成本抽象和内存安全保证,编译出的二进制文件极小且运行效率极高。 * **Axum 0.8**:Rust 生态中非常流行且高性能的 Web 框架,基于 Tokio 异步运行时。 * **SQLx 0.8**:提供了异步的数据库连接,支持编译期 SQL 检查(虽然本项目由于动态路径采用了运行期查询,但 SQLx 的连接池依然极具优势)。 ### 前端:Solid.js 1.9 + Vite 7 * **Solid.js**:不同于 React 的虚拟 DOM 机制,Solid.js 采用细粒度的响应式更新,直接编译成原生 DOM 操作,性能极高,且打包体积非常小。 * **Tailwind CSS 4 + DaisyUI 5**:采用了最新的 Tailwind 4 架构,更快的编译速度和更简洁的类名管理,配合 DaisyUI 5 提供的丰富组件。 ## 技术实现细节与亮点 ### 1. 极致的 Docker 镜像优化 为了实现 **不到 10MB** 的惊人镜像大小,项目使用了 Docker 的**多阶段构建**(Multi-stage Build)和 Rust 的**静态编译**技术: 1. **Frontend Builder**: 使用 `node:24-alpine` 镜像构建 Solid.js 前端资源。 2. **Backend Builder**: 使用 `rust:alpine` 镜像,配合 `musl-dev` 进行全静态链接编译,生成不依赖系统 C 库的独立二进制文件。 3. **Final Runtime**: 基于基础的 `alpine` 镜像作为最终运行时环境。 此外,在 `Cargo.toml` 中配置了: - `strip = true`: 移除二进制文件中的调试符号表。 - `lto = true`: 开启链接时优化(Link Time Optimization)。 - `codegen-units = 1`: 增加编译优化强度。 ### 2. 高效的内存与资源管理 Rust 的所有权机制保证了内存安全,配合 Axum 框架的零成本抽象,使得应用在运行时仅占用极少的内存(常态下约 1MB)。 数据库连接方面,使用了 **SQLx** 的连接池,复用数据库连接,避免了频繁创建销毁连接的开销。 ### 3. Solid.js 的细粒度响应式 前端选择了 Solid.js,主要看重其**无虚拟 DOM**(No Virtual DOM)的特性: * **更少的内存占用**:不需要在内存中维护两棵 DOM 树。 * **更快的更新速度**:状态变化直接触发 DOM 更新,没有 Diff 算法的计算开销。 ### 4. 安全性:轻量但不简陋 项目内置了基于中间件(Middleware)的认证机制: * 所有 API 请求都会经过认证中间件。 * 校验 `x-api-key` 请求头是否匹配环境变量中的 `API_KEY`。 * 有效防止了未授权的访问。 ### 5. AI 智能 SQL 助手 项目集成了 AI 能力,不仅能纠正 SQL,还能理解表结构: * **上下文感知**:自动获取目标表的 `PRAGMA table_info`(列名、类型、约束)和 `PRAGMA foreign_key_list`(外键关系)。 * **智能纠错**:结合表结构信息,AI 能够理解业务语义,给出准确的 SQL 建议。 * **自然语言操作**:支持自然语言转 SQL,大幅降低非技术人员的操作门槛。例如输入 "查询年龄大于 20 的用户",AI 会根据表结构自动生成 `SELECT * FROM users WHERE age > 20;`。 * **高度兼容**:支持 OpenAI 标准接口,可无缝对接 DeepSeek (支持 `reasoning_effort` 参数)、本地 LLM (如 Ollama) 等。 配置方式简单,只需设置环境变量即可启用: ```bash OPENAI_API_KEY=your_api_key # API 密钥(必需) OPENAI_BASE_URL=https://api.openai.com/v1 # API 地址(可选,默认 OpenAI) OPENAI_MODEL=gpt-4o-mini # 模型名称(可选,默认 gpt-3.5-turbo) ``` ## 功能与使用 ### 主要功能 * **多数据库支持**:自动扫描并列出 `/app/db` 目录下的所有 SQLite 数据库。 * **SQL 编辑器**:支持执行任意 SQL,提供实时结果反馈。 * **AI 辅助**:一键生成/纠正 SQL。 * **数据浏览**:快速查看表结构、字段类型、外键关系及数据。 * **响应式设计**:适配 PC 和移动端。 ### 快速部署 推荐使用 Docker 进行部署: ```bash docker run -d \ -p 3000:3000 \ -v /path/to/your/db:/app/db \ -e API_KEY=your_secret_key \ --name sqlite-webui \ wangyucode/rust-sqlite-webui ``` 或者使用 Docker Compose: ```yaml services: sqlite-webui: image: wangyucode/rust-sqlite-webui ports: - "3000:3000" volumes: - ./db:/app/db environment: - API_KEY=your_secret_key ``` ### 配置说明 * 挂载目录:将你的 SQLite 数据库文件所在的宿主机目录挂载到容器的 `/app/db` 目录即可。 * 环境变量: | 环境变量 | 默认值 | 说明 | | :--- | :--- | :--- | | `API_KEY` | `your-super-secure-key` | WebUI 的访问密钥 | | `OPENAI_API_KEY` | - | AI 接口密钥 | | `OPENAI_BASE_URL` | `https://api.openai.com/v1` | API 基础路径 | | `OPENAI_MODEL` | `gpt-3.5-turbo` | 使用的模型名称 | | `RUST_LOG` | `info` | 日志级别 (info, debug, warn) | ## 总结 如果你正在寻找一个资源占用极低、部署简单且响应速度极快的 SQLite Web 管理工具,不妨试试这个项目。同时也欢迎对 Rust 全栈开发感兴趣的朋友查看源码,共同交流。 本站已部署并正在使用此项目:[https://wycode.cn/manage/sqlite/](https://wycode.cn/manage/sqlite/) 项目地址:[https://github.com/wangyucode/rust-sqlite-webui](https://github.com/wangyucode/rust-sqlite-webui) --- ## 《始于极限》标注 > 原文: https://wycode.cn/blog/start-from-limitation | Agent 文档: https://wycode.cn/blog/start-from-limitation.md | 日期: 2025-08-20 | 分类: Life ![始于极限](https://pic.arkread.com/cover/ebook/f/407667944.1663903605.jpg!cover_default.jpg) > 在人生的旅途中,也许有人与你同行,也许没有。有旅伴也许是幸运的,也可能不幸。有时候,旅伴确实能为我们的人生增光添彩。不过到头来,终究是孤身一人。只要做好这样的思想准备,便不难做出选择了。 《始于极限》是上野千鹤子和铃木凉美在新冠疫情,历时一年,十二次通信,由书信整理的一本关于恋爱与性、婚姻、男人、工作、独立、自由等,围绕 12 大主题聊透女性内心深处的愤怒与困境的书。 本文摘抄了《始于极限》一些原文: ## 恋爱与性 > 没想到性经验极度丰富的男女在人生末尾回忆起的“最美好的性体验”竟是性爱合一的极致。 > 性是性,爱是爱,它们本不相同,偶尔会重合,有时则不会。一个人如果经历过性和爱偶然重合带来的至上幸福,那应该是非常幸运。而且一个人能分辨出高质量的性,也正说明他经历过许多质量不那么高的性 > “性欲降低以后,人生就变得平和了。” ## 能力 > 我这代人普遍认为性与爱显然是两码事,对我们来说,面临的挑战大概是如何面对孤独,而人们认为性爱一致时无须直面这种孤独。 > 想从事的工作与符合理想人生的工作有时是一致的,可一旦出现冲突,就得考虑把什么放在第一位、有没有什么东西被忽视了、三十岁时还能不能保持同样的价值观。 > 想象力无法超越自身的认知,而现实远超想象 > 教授问我:“那你硕士毕业出来打算做什么呀?”我老实回答:“老师,我是一点想法都没有。”教授便说:“女孩子嘛,就这样挺好的。”在那个年代,大家甚至不觉得这是性别歧视。 ## 团结 > 在人生的旅途中,也许有人与你同行,也许没有。有旅伴也许是幸运的,也可能不幸。有时候,旅伴确实能为我们的人生增光添彩。不过到头来,终究是孤身一人。只要做好这样的思想准备,便不难做出选择了。 ## 女性主义 > 不想被贴上“女权”的标签,但又想好好抗议,一看就是 21 世纪初成人的那代人特有的心态。但申明之后紧跟着的话往往又是不折不扣的女性主义言论,于是我想,也许很多女性并不是排斥女性主义本身,而是接受不了“女权”这个称呼。 > 全国各地的“女性服务中心”也都把招牌改成了“男女共同参与中心”。男性也被纳入到政策中,本就少得可怜的女性政策预算还得分一部分给男性,各地还办起了面向男性的烹饪班。 ## 自由 > 三岛由纪夫《假面的告白》是戴着“假面”却诚实到教人心头一颤的告白,川端的《睡美人》也是认识到衰老的男人对性不加修饰的坦白。 ## 男人 > 社会变革变的不是真心话,而是原则和场面话 > 其实活着就是孤独地面对自身的利己主义。只有建立起彼此自我对等的纠葛,男女之间才能有正经的恋爱。 以上为纯原文摘抄不代表本人看法,总体而言,此书没有想象中的深刻。 不想被贴上“女权”的标签,又想发表女性主义言论。 为了照顾女性而设立的全国的“女性服务中心”,又因为不想被特殊对待,改成“男女共同参与中心”。改完又觉得男性被纳入瓜分了她们的预算。啊?那我们全国各地的妇联岂不是有歧视女性之嫌?但愿我们不会有那一天。 --- ## 这一波AI浪潮是否是泡沫? > 原文: https://wycode.cn/blog/ai | Agent 文档: https://wycode.cn/blog/ai.md | 日期: 2024-10-30 | 分类: Life ![chatgpt](https://assets.datamation.com/uploads/2023/05/dm-how-to-use-chatgpt-696x464.png) > 今天是2024年10月30日,自从ChatGPT在2022年11月发布以来,人工智能(以下简称AI)的热度持续上升,它被认为是第四次工业革命的重要推动力。但也有人说AI是泡沫,做到实质性落地并且收益的公司屈指可数。这篇文章会结合历史,最新的前沿进展,分享一下目前我自己的观察和思考。 ## 回顾历史 > 历史总是惊人的相似 目前的AI浪潮,和20世纪90年代末的互联网浪潮非常相似。 ### 90年代互联网浪潮 90年代互联网开始商业化,经济方面1994年到1995年,为了防止经济过热,美联储连续加息7次,将联邦利率提高至6%,然而在这样的背景下,1995年到1998年,纳斯达克指数上涨了168%,这里面的重要原因就是互联网的出现。如今的互联网巨头都是在那个时候创立的,谷歌亚马逊是1995年左右,百度,腾讯,阿里99年左右。随后的2000年互联网泡沫破裂,大量公司破产倒闭,行业重新洗牌。普通人站在当时1999年的时间点上,也是看不清未来互联网能做什么。但是现在我们知道这些互联网公司不仅活下来了,而且成为了目前市值最高的互联网巨头。 当时的互联网,可以落地的应用并不多,无非就是浏览器,电子邮件。直到2000年互联网泡沫破裂后,我们才真正看清了互联网能做什么。2002年,我还在上学,当时的同班同学谁会注册QQ,谁就是交际花。2003年《梦幻西游》发布,2004年《魔兽世界》发布,2005年QQ空间发布。这时候互联网才开始真正进入大众视野。随着2007年,乔布斯掏出初代Iphone,又开启了移动互联网时代,互联网创造了无数新的工作岗位并实现了生产力的巨大飞跃。 再看看现在的AI,和90年代末的互联网非常相似。经济方面,美联储从2022年3月到2023年6月加息11次,但是以英伟达为首的科技巨头股价却屡创新高。站在当下的时间点上,目前的AI和2000年的搜索引擎非常相似。 ## 人工智能和搜索引擎 现在的人工智能应用还处于非常早期的阶段,目前使用最普遍的还是聊天机器人,比如`ChatGPT`,`Gemini`,`Claude`, `文心一言`,`通义千问`,`豆包`等等。他们都很强,但是对于很多人而言,你把ChatGPT摆到他面前,他也问不出什么有价值的问题,这个问题或者叫提示词(prompt)的质量决定了AI给你回复的质量,这个和早些年的搜索引擎其实是一样的,会百度一下,和Google一下, 以及关键词的选取,得到的结果质量是完全不同的。也说句不好听的,人与人之间使用搜索引擎的能力差距比人和猴子的差距还大。放到AI的时代还是一样。AI和搜索引擎都是8个字**遇强则强,遇弱则弱**。当然了,搜索引擎不是互联网的全部,聊天机器人也不是AI应用的全部。未来肯定会有更多面向更大众化的应用。 ## AI能否替代程序员 先说结论,AI无法代替程序员,但是会让很多程序员失业。从最早的`Github Copilot`以及国内的`Baidu Comate`,`Marscode`, `TONGYI lingma`到现在的AI 编辑器`Cursor`, 对于效率的提升是非常明显的。第一次使用`Github Copilot`,看到它真的明白我的业务,和并且给出符合我自己风格的代码提示的时候,让我有种“伯牙摔琴”,找到知己的感觉。这感觉是直冲天灵盖的那种震撼。这样的工具对于一些CURD工程师,调库侠,是巨大的威胁。特别是当你使用的语言和框架比较老而且非常普遍的话,这种冲击会更加明显。但是,如果你做的是真正的前沿创新,更高一级的抽象,大型应用的架构设计,AI永远无法替代你。 ## 目前的状态 ![mmlu](/images/20241030_mmlu.png) 从目前的趋势来看,`Scaling Law`没有问题,大模型就是参数越大,能力越强。但是由于大模型训练的算力需求巨大,可以预见的未来都是少数巨头和独角兽公司有能力自己训练,比如谷歌,meta,百度,阿里,OpenAI,月之暗面,清华智谱等。所以普通人小公司就没有机会了吗?并不是。先看下大模型的缺陷和痛点。 ### toB的痛点 目前大模型无法解决以下问题: 1. 知识局限性:通用的模型对于实时的,未公开的,私域的,离线的,无能为力。因为大模型一旦训练好它的知识库就锁定了。 2. 复杂业务场景: 现实中的业务场景非常复杂,比如金融,医疗,法律,教育等特定领域。它们往往拥有已经在使用的传统系统,目前的对话大模型无法对接这些复杂的系统和业务场景。 ### RAG和Agent 解决大模型的以上两个问题我们需要: 1. RAG(Retrieval Augmented Generation) 检索增强生成。原理就是给大模型外挂一个数据库,让它从数据库中检索出相关的信息。再总结生成回答。 2. Agent:类似与中间层,通过一个或多个Agent来构建一个workflow连接现有系统构和大模型。 基于以上的方向,目前看到的toC的应用是 - 知识库管理系统,例如Google的`NotebookLM`, `Obsidian`等。 - 问答引擎:例如`Perplexity`等。 这些东西目前还都是一些极客在用,离大众化还有很长一段路要走。其实和所有的技术革新浪潮一样,刚出来的时候都是比较“小众”。 ## AI和智能手机 toC很重要的一个方向就是智能手机和AI的结合,目前的大模型和现有的智能手机还在进一步融合,将来AI一定是系统级的,可以在任何时候无感使用。而不是像现在一样,需要安装一个APP然后尬聊。所以可以预见的将来,还会有新一波的换机潮,现在的所谓“智能手机”在和AI高度集成的手机面前,就跟以前的诺基亚一样。 ## 学术研究的进展 AI在学术领域的帮助,可以说是带来了一种全新的研究范式。为什么这么说呢?自从2017年那片神级的论文“Attention is all you need”,基于Transformer的新发现层出不穷,2024年诺贝尔物理学奖颁给人工神经网络,化学奖颁给了Deepmind用人工神经网络破解蛋白质结构的团队。在这之前还有研究团队用机器学习取得了在可控核聚变领域的进展,最近这几天还有团队利用Transformer发现了16万种新病毒。以前的学术研究是先提出设想,排除其它干扰做实验验证,然后得出结论。而结合AI的学术研究是直接面对混沌的现实世界得出理论和规律。这种改变是根本性的,并且跨学科的。而且未来的新技术还能和AI结合,例如量子计算,元宇宙,机器人,Web3.0。 那么回到开头的问题,你还怀疑AI是泡沫吗? --- ## Caddy的安装和使用 > 原文: https://wycode.cn/blog/caddy | Agent 文档: https://wycode.cn/blog/caddy.md | 日期: 2024-09-24 | 分类: DevOps ![Caddy](https://caddyserver.com/old/resources/images/caddy-logo.svg) > 之前一直使用Nginx作为反向代理和静态网站服务,Caddy作为替代,有诸多好处,比如:配置简单,支持API,Caddyfile, JSON多种配置方式;支持自动HTTPS,自动更新TLS证书;内置支持HTTP1, HTTP2, HTTP3, Websocket。 这篇文章安装和配置Caddy的全流程 ## 安装Caddy 当前安装最简单的方式当然是通过Docker了,这是docker-compose文件 ```yml services: caddy: image: caddy:2.8 container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" volumes: -./caddy/Caddyfile:/etc/caddy/Caddyfile:ro ``` 通过`docker compose up -d`命令启动容器即可 ## 配置Caddy 以下是`wycode.cn` 的 `Caddyfile` 配置。可以看到比nginx的配置简单了很多。 ``` wycode.cn { encode zstd gzip handle_path /upload/* { root * /srv/upload file_server browse } handle_path /swagger-ui/* { root * /srv/swagger-ui file_server } handle /api/v1/* { reverse_proxy "http://deno:8083" } handle /mongo/* { reverse_proxy "http://mongo-express:8081" } handle { root * /srv/wycode try_files {path} /{path}.html {path}/ /404.html file_server } } ``` - **HTTP Content压缩:** `encode zstd gzip` 启用zstd和gzip压缩 - **upload目录:** `handle_path /upload/*` 配置静态文件服务,`root * /srv/upload` 表示将 `/upload` 目录映射到 `/srv/upload` 目录 - **Swagger UI:**`handle_path /swagger-ui/*` 配置静态文件服务,`root * /srv/swagger-ui` 表示将 `/swagger-ui` 目录映射到 `/srv/swagger-ui` 目录 - **Deno API:**`handle /api/v1/*` 配置反向代理,`reverse_proxy "http://deno:8083"` 表示将 `/api/v1` 映射到 `http://deno:8083` 服务 - **MongoDB WebUI:**`handle /mongo/*` 配置反向代理,`reverse_proxy "http://mongo-express:8081"` 表示将 `/mongo` 映射到 `http://mongo-express:8081` 服务 - **NextJs主站:**`handle` 配置静态文件服务,`root * /srv/wycode` 表示将 `/` 目录映射到 `/srv/wycode` 目录,`try_files {path} /{path}.html {path}/ /404.html` 表示如果请求的路径不存在,则尝试查找 `/` 目录下的文件,如果文件不存在,则返回 404 错误。 --- ## 使用Vite构建React项目 > 原文: https://wycode.cn/blog/vite | Agent 文档: https://wycode.cn/blog/vite.md | 日期: 2023-11-11 | 分类: Frontend ![Vite](https://vitejs.cn/vite3-cn/logo-with-shadow.png) # 使用 Vite 构建 React 项目 在前端开发领域,构建工具一直是不可或缺的一部分。Vite 是一个快速、简单的构建工具,它可以帮助我们快速搭建 React 项目并提供出色的开发体验。本文将介绍如何使用 Vite 构建 React 项目,包括 Vite 的简介、项目搭建步骤以及适配老版本浏览器的方法。 ## 什么是 Vite [Vite](https://vitejs.dev/) 是一个由 Evan You(Vue.js 的创始人)创建的构建工具,它的目标是提供快速的开发体验。Vite 不仅支持 Vue.js,还可以用于构建 React、Preact,Svelte 和其他前端框架的项目。它采用了 ES 模块的方式加载模块,因此在开发过程中非常快速。 ## 项目搭建步骤 要使用 Vite 构建 React 项目,首先确保你已经安装了 [Node.js](https://nodejs.org/)。接下来,按照以下步骤进行操作: 1. **初始化项目**:`npm create vite@latest web-vite --template react` 2. **进入项目文件夹**: `cd web-vite` 3. **安装依赖**: `npm install` 4. **启动开发服务器**: `npm run dev` 至此,你可以在代码编辑器中修改 React 组件并实时查看更改。 ## 构建发布项目: 当项目准备好发布时,运行以下命令构建项目:`npm run build`, 构建后的文件将存储在 dist 文件夹中,可以用于部署到生产环境。 ## 配置静态资源公共基础路径 为了让 build 可以在任何目录下部署,这里将资源路径改为相对路径 ```js export default defineConfig({ base: "./", }); ``` ## 适配老版本浏览器 默认情况下,Vite 构建的项目将会生成适用于现代浏览器的代码。 - Chrome >=87 - Firefox >=78 - Safari >=14 - Edge >=88 如果你需要支持老版本浏览器,你可以按照以下步骤进行操作: 1. **安装插件**: `npm i -D @vitejs/plugin-legacy` 2. **配置插件**: 这里假设需要支持 chrome52 ```js import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import legacy from "@vitejs/plugin-legacy"; // https://vitejs.dev/config/ export default defineConfig({ base: "./", plugins: [ react(), legacy({ targets: ["chrome 52"], }), ], }); ``` 现在,你的 Vite React 项目应该可以在老版本浏览器中正常运行了。 使用 Vite 构建 React 项目是一种快速而愉快的开发体验,同时还能够通过简单的配置适配老版本浏览器。希望这篇博客对你有所帮助,让你更轻松地开始构建 React 应用程序。 --- ## 解决 invalid format git@github.com: Permission denied (publickey). > 原文: https://wycode.cn/blog/ssh-key | Agent 文档: https://wycode.cn/blog/ssh-key.md | 日期: 2023-09-11 | 分类: DevOps ![git](https://git-scm.com/images/logo@2x.png) git 在远端 pull 或者 push 时报: ``` invalid format git@github.com: Permission denied (publickey). ``` ## 问题场景: windows 上把 private key 的内容复制到文本编辑器时会出现。 ## 根本原因: 私钥的换行是单字符`LF`,但 windows 的默认换行符是`CRLF`,所以导致 git 读取私钥报格式错误。 ## 解决办法: 使用文本编辑器修改换行符,例如 VSCODE 中状态栏点击`CRLF`,然后切换成`LF`。 ## 笔记: 通过`ssh -vT git@github.com` 可以查看详细的 ssh log,来排查 ssh 的问题。 出现如下 log 说明 ssh key 已经正常配置: ``` Hi wangyucode! You've successfully authenticated, but GitHub does not provide shell access. ``` --- ## ChatGPT是如何被训练出来的 > 原文: https://wycode.cn/blog/chat-gpt | Agent 文档: https://wycode.cn/blog/chat-gpt.md | 日期: 2022-12-01 | 分类: AI ![ChatGPT](https://cdn.openai.com/chatgpt/draft-20221129c/ChatGPT_Diagram.svg) ChatGPT是OpenAI在2022年11月推出的新一代由人工智能技术推动的自然语言处理模型。ChatGPT公布没多久,各大搜索引擎厂商都宣布正在开发自己的新版NPL模型,也是因为他们感受到了来自ChatGPT的压力。它能够理解并生成人类语言,能够回答问题,撰写文案,写诗歌,编故事,写代码,做Excel表格,它拥有上下文对话能力,在典型的使用场景下难以分辨是真人还是机器。ChatGPT被认为是下一代的搜索引,将会引起新一轮的技术革新浪潮。 本文介绍ChatGPT背后的工作原理和训练过程。 ## 罗马不是一天建成的 ChatGPT脱胎于InstructGPT,而InstructGPT的前身是GPT-3,GPT-3的诞生就已经引起了业界的轰动,它的模型参数达到了1750亿,这比谷歌的BERT模型3.7亿参数大了400多倍。并且GPT-3可以使用很小的数据集来训练。 GPT-3的缺陷:由于GPT-3使用使用互联网上巨大的数据集训练来预测下一个词,而不是去符合用户的期待,所以它难免会输出不可信的,或者有害的信息,负面的情绪等。 InstructGPT的改进:通过基于人类反馈的强化学习(RLHF - reinforcement learning from human feedback),通过人类对模型的输出进行评分反馈来优化模型的行为。这使得模型更倾向于听人话而不是胡说八道。 ## InstructGPT的训练过程 1. 使用人工生成的示例 + 监督学习 + GPT-3训练一个模型出来 2. 然后不同模型生成多种输出 + 人工标记 训练一个奖励模型 3. 通过奖励模型 + 强化学习 + PPO算法 训练一开始的模型 由于步骤2要使用人工标记,这与预训练的模型数据量相比还不到2%,尽管对比GPT-3已经有了很大改进,InstructGPT也并不是完全安全的,它仍然会生成有偏见的输出,撒谎,输出有毒的内容。 OpenAI也提到,目前训练的数据基于讲英语的社会价值观,最终需要更负责任和包容的选择。 ## ChatGPT更进一步 - ChatGPT的基础模型来自于训练GPT-3.5, 这个GPT-3.5是由一个代码补全模型,经InstructGPT优化而来。 - 训练人员通过扮演用户和AI助手来生成模板对话,并将这些数据混合到InstructGPT的数据集中,然后将模型变成了一种对话的格式。 - 和InstructGPT一样地,通过对模型的输出进行随机采样和人工评分。然后用得到的奖励模型,使用强化学习对初始模型进行训练。并对这个过程进行数次迭代。 ## 持续进化 ChatGPT也不是完美的,但是它在持续进化,OpenAI做了一个网页聊天室,让ChatGPT接受来自更多用户的测试和评价。 此外集成了ChatGPT的新版Bing在公测中。 OpenAI肯定会基于收集的大量测试数据和用户反馈来升级和更新模型。 真正的通用人工智能,或是有自由意识的人工智能还有多远,我们拭目以待。 --- ## 升级到HTTP3 > 原文: https://wycode.cn/blog/http3 | Agent 文档: https://wycode.cn/blog/http3.md | 日期: 2022-09-29 | 分类: Frontend ![http3](https://www.zhengdexing.org/2020/10/20/HTTP%E5%8D%8F%E8%AE%AE-HTTP3/6301972-9482e8b53c342058.png) 基于 UDP 的 HTTP/3 协议规范[RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)于 2022 年 6 月发表。而 HTTP/2 协议规范[RFC 7540](https://httpwg.org/specs/rfc7540.html)早在 2015 年 5 月发表。但是仍然有很多网站在使用更加古老的 HTTP1.1。 HTTP/2 通过多路复用(multiplexing)和头部压缩,能够有效降低延迟。同时还提供了不同于`WebScoket`协议的服务端推送的新方法。 HTTP3 则使用基于 UDP 的 QUIC 协议,从根本上解决头部阻塞的问题,特别适用于移动网络不稳定情况下的网络效率改善。 本文介绍 HTTP2,HTTP3 协议的新特性,以及通过 NGINX 升级到 HTTP2 和 HTTP3 的方法。 ## 回顾 HTTP/1.0 和 HTTP/1.1 - HTTP/1.0 每发送一个请求,就要建立一个新的 TCP 连接。 - HTTP/1.1 加入了请求流水线(request pipelining),和持久连接。但多数情况下仍然会对不同的请求建立多个连接。由于 TCP 的特性,阻塞控制无法在多个连接之间共享,且浏览器限制了同服务器的总连接数。所以网络效率仍然较低。 - 此外 HTTP/1.1 和 1.0 都使用明文,用空格和回车进行字段分割,这导致了解析的复杂性和对不同变体的容忍度提升。 ## HTTP2 HTTP/2 引入了二进制帧和多路复用来改善延迟,但它没有修改传输层(TCP),由于 TCP 严格按照顺序发送数据包,当数据包发生丢失时,TCP 协议会重发数据包,所以在丢包率较高的情况下多路复用的机制反而会变慢。 ## HTTP3 HTTP/3 使用基于 UDP 的 QUIC 协议,拥有 HTTP2 所有的优良特性,并且由于 UDP 天生无需握手,且在网络环境发生变化时无需重新建立连接,例如切换移动网络到 WIFI。 ## 升级到 HTTP2 NGINX 默认使用 HTTP/1.1, 我们只需简单的添加`http2`到`listen`指令即可无痛升级到 HTTP2: ```nginx ... server { listen 443 ssl http2; ... } ``` ## 升级到 HTTP3 截止到今天(2022 年 10 月 1 日),NGINX 对 HTTP3 的支持还在开发中(nginx-quic 分支),官方还没有发布基于此分支的 docker 镜像,这里我使用民间编译的镜像`dasskelett/nginx-quic`。 修改配置: ```nginx ... server { listen 443 ssl http2; listen 443 http3 reuseport; ... ssl_early_data on; add_header Alt-Svc 'h3=":443"; ma=86400'; add_header QUIC-Status $http3; ... } ``` 至此本站已全面支持 HTTP3。 --- ## esModule + NodeJS + TypeScript 的工程配置 > 原文: https://wycode.cn/blog/esm-node-ts | Agent 文档: https://wycode.cn/blog/esm-node-ts.md | 日期: 2022-09-22 | 分类: Frontend ![nodejs](https://www.w3cschool.cn/attachments/image/20170808/1502178281486445.png) JavaScript的模块化方案经历了长时间的发展,最终于2015年在ES6实现了语言层面的标准化,即esModule(以下简称esm), 现在JS社区开始拥抱esm,很多npm包仅采用esm发布。 而NodeJS一直依赖采用CommonJS的模块化方案,在最近发布的版本中也开始支持ems, 由于巨大的历史包袱,NodeJS并没有抛弃CommonJS,所以在NodeJS中实际支持两种模块化方式,esm和CommonJS. 此外,现在很多前端工程使用TypeScript开发,而TS的模块解析方式也需要进行一些配置,在这3者结合的过程中有很多坑,本文介绍将三者完美结合的最佳实践。 ## 同时引入CommonJS包和ESM包 为了测试我们同时引入`lodash`的CommonJS版本和ESM版本 ```bash npm i lodash-es lodash ``` 在NodeJS中,模块作者除非显示指定使用esm方式加载模块: - 使用`.mjs` - 在`package.json`中的`type`字段指定为`module` - 使用`--input-type`标记 否则默认加载方式为CommonJS。 而上面的例子中`lodash-es`的`package.json`中的`type`就是`module`。 ## 使用CommonJS包 创建一个`common-js.js`文件: ```javascript const _ = require('lodash'); console.log(_.VERSION); // print '4.17.21' ``` 以上是CommonJS的写法,很习以为常,能够正确打印。 ## 使用esm包 创建一个`esm.js`文件: ```javascript import _ from 'lodash-es'; console.log(_.VERSION); ``` 第1个坑出现了, 报语法错误:`SyntaxError: Cannot use import statement outside a module` 说无法在模块外部使用import语句,一旦一个文件有顶级(top-level)的`import`或`export`,它会被当作一个模块,否则是一个脚本文件。 所以我们需要把这个文件变成esm,两种方式: 1. 重命名文件,修改扩展名为`.mjs`; 2. 在`package.json`中添加`"type": "module"` 但如果采用选项2,第2个坑就会出现了, 回去运行`common-js.js`报错:`ReferenceError: require is not defined in ES module scope, you can use import instead`。 说`require`在esm中不支持,此时可以重命名`common-js.js`后缀为`.cjs`,告诉模块加载器,此文件以CommonJS加载。 ## 引入Typescript 安装TS: ```bash npm i typescript ts-node -D ``` > ts-node 可以在nodejs中直接运行ts文件而无需编译 然后将刚才的`esm.js`和`common-js.cjs`扩展名改为`.ts` 使用`ts-node`命令直接运行`esm.ts` ```bash npx ts-node esm.ts ``` 第3个坑出现了,扩展名不支持:`TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for xxx` 这是因为esm的默认扩展名是js而不是ts,此时我们需要: 1. `ts-node --esm` 2. 添加 `"ts-node": {"esm": true}`到`tsconfig.json`。 如果采用方案1,第4个坑出现了,无法找到模块:`error TS7016: Could not find a declaration file for module 'lodash-es'`。 此时添加配置: `"esModuleInterop": true`到`tsconfig.json`的`compilerOptions`中。 添加完成后第5个坑出现了,引用错误:`ReferenceError: exports is not defined in ES module scope` 此时添加配置`module": "ESNext"`到`tsconfig.json`的`compilerOptions`中。 添加完成后第6个坑出现了,找不到模块:`ReferenceError: exports is not defined in ES module scope` 此时配置模块的解析方式为最新的`"moduleResolution": "Node16"`。 到此终于可以成功运行`esm.ts`了。 返回去运行`common-js.ts`: ```bash npx ts-node common-js.ts ``` 第7个坑出现了,它把我们的CommonJS模块当esm了:`ReferenceError: require is not defined in ES module scope, you can use import instead` 此时处于鱼和熊掌不可兼得,因为我们设置了`module": "ESNext"`, 所以ts的编译结果会添加`export {}`,所以如果要在纯esm的工程中仍然要使用CommonJS,则使用`.cjs`作为扩展名即可。 完整的代码在这里:https://github.com/wangyucode/esm-node-ts --- ## 《乌合之众:大众心理研究》热门标注 > 原文: https://wycode.cn/blog/the-crowd | Agent 文档: https://wycode.cn/blog/the-crowd.md | 日期: 2021-07-22 | 分类: Life ![The Crowd](https://p1.qhimg.com/t016f1e762c0161d220.jpg) > 闻有吏虽乱而有独善之民,不闻有乱民而有独治之吏 —《韩非子·外储说》 《乌合之众:大众心理研究》是一本研究大众心理学的著作。是法国社会心理学家古斯塔夫·勒庞创作的社会心理学著作,首次出版于1895年。勒庞指出:个人一旦融入群体,他们的个性将会被湮灭,群体的思想便会占据统治地位,而与此同时群体的行为也会变现为排斥异议、极端化、情绪化低智商化等特点,进而对社会产生破坏性的影响。 读了此书,你会明白过去、现在、以及将来全世界发生的各种群体事件背后的直接原因。 本文摘抄了《乌合之众:大众心理研究》在Kindle电子书中的热门标注: ## 热门标注 > 自觉的个性的消失,以及感情和思想转向一个不同的方向,就是要变成组织化群体的人所表现出的首要特征。 > 无意识现象不但在有机体的生活中,而且在智力活动中,都发挥着一种完全压倒性的作用。 > 群体在智力上总是低于孤立的个人,但是感情及其激起的行动这个角度看,群体可以比个人表现的更好或更差,全看环境如何。 > 孤立的个人具有主宰自己的反应的能力,群体则缺乏这种能力。 > 大凡能够成就大业的领袖人物,他最重要的品质不是博学多识,而是必须“具备强大而持久的意志力”,这是一种“极为罕见、极为强大的品质,它足以征服一切。 > 群体要想成为历史变迁的主角,它必须多多少少“为信仰而战”,也就是说,它的形成必须是为了某些简单而明确的信仰。 > 在观念简单化效应的作用下,凡是有抱着怀疑的精神、相信在政治和社会问题上极不易发现“确定性真理”的人,尤其是一个习惯于用推理和讨论的方式说明问题的人,在群体中是没有地位的;当面对群情激奋时,他尤其会生出苍白无力的感觉:因为他意识到他要与之作对的,不仅仅是一种错误的行为,而且还有“多数的力量”,还有贯彻这种行为时的偏执态度。我们更能理解,所谓专业精英,不管他智力多么高强,他陈明利害得失的理性努力,面对被空洞的观念冲昏了头脑的群体,反而会产生一种自己十分迂腐的无聊感觉。更为可悲的是,面对群众的荒谬与狂热,明智之士更有可能根本不会作出这样的努力,而是同群体一起陷入其中,事后又惊叹于自己连常识都已忘却的愚蠢。 > 因此在同人类的各种作为文明动力的感情——“譬如尊严、自我牺牲、宗教信仰、爱国主义以及对荣誉的爱”——的对抗中,理性在大多数时候都不是赢家。 > 群体只知道简单而极端的感情;提供给他们的各种意见、想法和信念,他们或者全盘接受,或者一概拒绝;将其视为绝对真理或绝对谬论。 > 进入了群体的个人,在“集体潜意识”机制的作用下,在心理上会产生一种本质性的变化。就像“动物、痴呆、社会主义者、幼儿和原始人”一样,这样的个人会不由自主地失去自我意识,完全变成另一种智力水平十分低下的生物。 > 自法国大革命以来,由领袖、意识形态和勒庞意义上的群体所组成的这种新的三位一体,便取代了宗教与皇权,成为一切民主宪政架构之外政治合法性运动的要件,尤其在勒庞之后的一百年里,这种新的三位一体更是上演了一幕幕规模宏大而惨烈的悲剧。 > 社会组织就像一切生命有机体一样复杂,我们还不具备强迫它们在突然之间发生深刻变革的智力”。因此他反对一个民族热衷于重大的政治和社会变革,他认为,这种变革的计划从理论上说无论多么出色,都不会使民族气质即刻出现变化(因为“只有时间具备这样的力量”)。采用激进的方式,借助于抽象的原则贯彻一种社会改造的蓝图,只会“使一个高度精致的文明倒退到社会进化更早期的阶段” > 人们在智力上差异最大,但他们却有着非常相似的本能和情感。在属于情感领域的每一种事情上——宗教、政治、道德、爱憎等等,最杰出的人士很少能比凡夫俗子高明多少。从智力上说,一个伟大的数学家和他的鞋匠之间可能有天壤之别,但是从性格的角度看,他们可能差别甚微或根本没有差别。 > 在集体心理中,个人的才智被削弱了,从而他们的个性也被削弱了。异质性被同质性所吞没,无意识的品质占了上风。 > 群体是个无名氏,因此也不必承担责任。这样一来,总是约束着个人的责任感便彻底消失了。 > 有意识人格的消失,无意识人格的得势,思想和感情因暗示和相互传染作用而转向一个共同的方向,以及立刻把暗示的观念转化为行动的倾向,是组成群体的个人所表现出来的主要特点。他不再是他自己,他变成了一个不再受自己意志支配的玩偶。 > 群体中的个人不但在行动上和他本人有着本质的差别,甚至在完全失去独立性之前,他的思想和感情就已经发生了变化,这种变化是如此深刻,它可以让一个守财奴变得挥霍无度,把怀疑论者改造成信徒,把老实人变成罪犯,把懦夫变成豪杰。 > 群体可以转瞬之间就从最血腥的狂热变成最极端的宽宏大量和英雄主义。群体很容易做出刽子手的举动,同样也很容易慷慨赴义。正是群体,为每一种信仰的胜利而不惜血流成河。 > 群体因为夸大自己的感情,因此它只会被极端感情所打动。希望感动群体的演说家,必须出言不逊,信誓旦旦。夸大其辞、言之凿凿、不断重复,绝对不以说理的方式证明任何事情 > 群体只知道简单而极端的感情;提供给他们的各种意见、想法和信念,他们或者全盘接受,或者一概拒绝;将其视为绝对真理或绝对谬论。用暗示的办法加以诱导而不是作出合理解释的信念,历来都是如此。与宗教信仰有关的偏执及其对人们的头脑实行的专制统治,早就为大家所知。 > 在公众集会上,演说者哪怕作出最轻微的反驳,立刻就会招来怒吼和粗野的叫骂。在一片嘘声和驱逐声中,演说者很快就会败下阵来。当然,假如现场缺少当权者的代表这种约束性因素,反驳者往往会被打死。 > 群体可以杀人放火,无恶不作,但是也能表现出极崇高的献身、牺牲和不计名利的举动,即孤立的个人根本做不到的极崇高的行为。以名誉、光荣和爱国主义作为号召,最有可能影响到组成群体的个人,而且经常可以达到使他慷慨赴死的地步。 > 群体推理的特点,是把彼此不同、只在表面上相似的事物搅在一起,并且立刻把具体的事物普遍化。 > 影响民众想象力的,并不是事实本身,而是它们发生和引起注意的方式。如果让我表明看法的话,我会说,必须对它们进行浓缩加工,它们才会形成一种令人瞠目结舌的惊人形象。掌握了影响群众想象力的艺术,也就掌握了统治他们的艺术。 > 一切政治、神学或社会信条,要想在群众中扎根,都必须采取宗教的形式——能够把危险的讨论排除在外的形式。即便有可能使群众接受无神论,这种信念也会表现出宗教情感中所有的偏执狂,它很快就会表现为一种崇拜。 > 词语的威力与它们所唤醒的形象有关,同时又独立于它们的真实含义。最不明确的词语,有时反而影响最大。例如像民主、社会主义、平等、自由等等,它们的含义极为模糊,即使一大堆专著也不足以确定它们的所指。然而这区区几个词语的确有着神奇的威力,它们似乎是解决一切问题的灵丹妙药。各种极不相同的潜意识中的抱负及其实现的希望,全被它们集于一身。 > 信徒们总是穷凶极恶地打碎他们以前神灵的塑像。 --- ## 基于Docker的自动化部署 > 原文: https://wycode.cn/blog/auto-deploy | Agent 文档: https://wycode.cn/blog/auto-deploy.md | 日期: 2021-05-09 | 分类: DevOps ![flow](/images/20200512_auto_deploy.png) > 之前写过一篇文章关于[使用Github Action进行持续集成](/2020-10-28-github-action.html)。 但是如何将构建好的软件包,分发到运行环境?托付给自动化服务后如何保证服务器安全?Docker化部署的话,服务器上的Docker引擎如何自动检查、拉取、运行最新的镜像? 本文介绍如何解决这些问题。 ## 镜像分发 对于Docker构建的镜像,我们可以将其推送到Docker hub之类的镜像仓库。 然后在服务器上运行`docker pull `或者`docker-compose pull `就可以将镜像拉下来。 然后再重新运行就可以了。 这种方式解决了服务器的安全问题。 但是如何将这一套流程自动化呢。 ## 自动检查镜像更新和重新运行 一个思路就是专门写一个守护程序,在接收到某种API请求时,执行以上的操作。但是这样它就脱离了Docker的管理,而且其本身也是需要维护和部署的。 但是如果将它也作为Docker镜像的话,`Container`又如何控制宿主机中的`Docker Engine`呢? 这也有办法,我们只需要将`Host`的Docker域套接字(`UNIX domain socket`),挂载到`Container`中: ```bash docker run ... -v /var/run/docker.sock:/var/run/docker.sock ``` 因为Docker是CS模式,这样`Container`就可以控制宿主机的Docker引擎,以实现上述操作了。有人将其称为`Docker in Docker`。 而以上这个思路已经有人造好轮子了。 ## Watchtower Watchtower本身就是一个Docker镜像,支持定时、或者API Hook等方式触发检查,在发现有更新的镜像时,它会停止当前运行的容器,自动拉取镜像,重新以之前运行的参数重启容器,完美解决我们的需求。 最终的流程参考文章开头的流程图。 这样一来,我们只需要push代码,Github Action会自动构建Docker镜像并push到Docker hub,然后curl一下我们的watchtower,它就会自动帮我们拉取并运行最新的镜像了。 省了不少时间,最重要的:**不要内卷,要work-life balance** --- ## 理解Rust语言的所有权以及借用、引用、切片。 > 原文: https://wycode.cn/blog/rust-ownership | Agent 文档: https://wycode.cn/blog/rust-ownership.md | 日期: 2021-01-23 | 分类: Backend ![Rust](https://www.rust-lang.org/static/images/rust-logo-blk.svg) > Rust由Mozilla基金会开发,没有虚拟机和垃圾回收,却又提供了内存安全,有着媲美C/C++的性能,又同时支持命令式函数式编程以及泛型等便利。优雅(上层抽象)和高性能(底层控制)往往是鱼和熊掌不可兼得,但Rust试图挑战这一矛盾。允许你做带垃圾回收的语言(例如Java)做不到的事,比如控制底层。而同时又避免了比如空指针,野指针在内的(例如C/C++)的问题。它被用于开发浏览器引擎(`Servo`),操作系统(`Redox OS`),并且特别适合开发`WebAssembly`和嵌入式,以及高性能的Web服务。 > 本文介绍Rust语言中和其它语言相比一个非常重要的特性`所有权(Ownership)`以及其相关的`借用(Borrowing)`,`引用(References)`,`切片(Slice)`的理解和使用。这个特性保证了Rust不需要垃圾回收以及强大的内存安全,同时还能在多线程编程中带来好处。 ## 所有权(Ownership) 废话不多说,先看代码: ```Rust let a = 5; let b = a; println!("a->{}, b->{}", a, b); ``` - 上面的代码会打印出`a->5, b->5`,和直觉(其它语言)相同。 - `a`和`b`是`i32`类型,属于基础数据类型,存储在**栈**上。 - 当把`b`赋值给`a`时,会进行拷贝。所以在栈上会有两个`5`。 但是: ```Rust let s1 = String::from("hello"); let s2 = s1; println!("s1->{}, s2->{}", s1, s2); ``` - 上面的代码无法通过编译。因为`s1`的**所有权**发生了转移(`move`)。 - 在把`s1`赋值给`s2`之后,`s1`就无法再使用了。 - `s1`和`s2`被存储在**栈**上,但是`hello`这个字符串存储在**堆**上 - `s1`和`s2`是`String`类型,`String`类型不是基础数据类型,它是实际字符串的基本信息,包含了指向堆内存的指针、长度、容量信息。 - 赋值时,栈上的`String`即指针,长度、容量被拷贝了,但堆上的`hello`没有被拷贝。 - 这样就保证了堆上的内存始终只有一个所有者`owner`。 - Rust会在`Owner`离开作用域后自动释放`Drop`它所指向的堆内存。同时避免二次释放带来的内存问题和安全漏洞。 ![Move](https://doc.rust-lang.org/book/img/trpl04-04.svg) ## 引用(References)和借用(Borrowing) 再看一个函数的例子: ```Rust fn main() { let s1 = String::from("hello"); let length = get_length(s1); println!("{}",s1); } fn get_length(str: String) -> usize { str.len() } ``` - 上面的代码无法通过编译,因为`s1`的**所有权**被转移到了`get_length`函数里面。并在出了函数之后就被释放了。 - 其中一个解决方案是让`get_length`通过元组`tuple`同时返回`str`。此时可以用`s1`继续接收`str`,即隐藏`shadowing`。 我们也可以通过传递**引用**`References`来解决这个问题: ```Rust fn main() { let s1 = String::from("hello"); let len = get_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn get_length(str: &String) -> usize { str.len() } ``` 将获取引用作为函数参数称为**借用**`borrowing` ## 可变引用(Mutable References) 如果要在函数内修改**借用的值**则必须使用可变引用: ```Rust fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); } ``` - Rust中所有的变量默认是不可变的,不可变代表了安全。 - 同一作用域中,不可变引用可以有多个,即多个**读引用**是ok的。但是不能同时存在可变引用和不可变引用。 - 同一作用域中,可变引用只能有一个,即**写引用**只能有一个。 这些规则保证了Rust不可能出现**悬垂指针**(dangling pointer)或**空指针**(null pointer)异常。 也减轻了多线程中的**竞争状态**(Race conditions)和**死锁**(Deadlocks)问题 ## 切片(Slice) 三句话说清楚切片`Slice`: - 切片没有所有权,因为它是对数组或者字符串等集合某个区间的不可变引用。 - 它的作用是,我们想使用集合中某个数据,又怕其他人对集合做了修改(比如清空了集合的元素)时能够提前发现bug。 - 原理是上一节提到的,同一作用域不能同时存在可变引用和不可变引用。 ## Rust的缺点 目前发现Rust最大的缺点就是:想写一个能够编译通过的代码实在是太难了,哈哈。 但正是因为Rust拥有强大的编译器,它能够提前把bug扼杀在摇篮里,在编译阶段就拒绝各种各样的bug,从而同时保证程序的性能和安全。 这也是为什么Typescript,敏捷开发,TDD,会火的原因了。**尽可能早的发现和解决问题。** --- ## 备份和还原Docker容器中的MongoDB数据 > 原文: https://wycode.cn/blog/docker-mongodb | Agent 文档: https://wycode.cn/blog/docker-mongodb.md | 日期: 2020-12-05 | 分类: Backend ![MongoDB](/images/20201204_mongodb.svg) > 本文介绍如何使用两行简单的命令来执行备份和还原Docker容器中MongoDB的数据库数据。 ## 备份 备份MongoDB有多种方法,其中比较简单的就是使用`mongodump`。 ```bash docker exec -i mongodump -d --archive > ./mongo/collections.archive ``` - 以上命令会将``容器中的``数据库dump到host机器的`collections.archive`文件中。 - `--archive`会dump所有`collection`到一个文件。 - `mongodump`命令不提供目标路径的话会将dump结果输出到标准输出,这里使用`>`重定向输出到host的文件。 ## 还原 这里使用host机器上的`collections.archive`文件进行还原。 ```bash docker exec -i mongorestore --archive < ./mongo/collections.archive ``` - `--archive`模式导出的备份文件还原时必须也要加上`--archive` - `mongorestore`命令不提供源路径的话会使用标准输入,这里使用`<`重定向host的文件作为`mongorestore`的输入。 --- ## 使用Github Action进行持续集成 > 原文: https://wycode.cn/blog/github-action | Agent 文档: https://wycode.cn/blog/github-action.md | 日期: 2020-10-28 | 分类: DevOps ![Github Action](https://blog-cdn.everhour.com/blog/wp-content/uploads/2023/02/h2jfrvzrbyh1yff2n3wfu2hkqqps6x_uvqo.jpg) > `Github Actions`是由 Github 发布的持续集成(CI)/持续交付(CD)服务,对于公开的仓库是免费使用的。之前我们使用`TravisCI`,现在用`Github Actions`更方便了。 ## Actions 通常一个持续集成的工作流通常包含很多的步骤,这个步骤做的事,称为一个`Action`。 我们会写很多的`Action`(配环境,拉代码,安装依赖,编译,测试,打包,部署),然后将它们组合到一起工作。 ## Github Marketplace 组合可以是多种多样的,但是这些`Action`通常都是类似的。所以 Github 利用自己开源的特点,所有人都可以将自己的`Action`发布到`Github Marketplace`中,供他人使用。 这意味着,你能想到的`Action`几乎都已经被写好了,直接拿来用就行了,不用重复造轮子。 当然自己再造一个轮子也是很简单的。 ## 一个典型的 workflow ```yml name: Node.js CI on: [push] # push代码时执行 jobs: build: # jobs名称 runs-on: ubuntu-latest # 操作系统 strategy: matrix: node-version: [8.x, 10.x, 12.x] # node版本 steps: - uses: actions/checkout@v2 # 引用action:拉代码 - name: Use Node.js ${{ matrix.node-version }} # 引用action:配置node环境 uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm install # 跑命令:安装依赖 - run: npm run build --if-present # 跑命令:安装依赖 - run: npm test # 跑命令:执行测试 ``` ## 使用密码参数 当我们要部署时,有时会用到服务器密码,我们不能直接配置在`workflow`脚本中。这时就要使用密码参数。 1. 在仓库设置(Settings)页面 2. 添加密码(Add a new secret) 3. 使用 `${{secrets.NAME}}` 引用密码(替换`NAME`为你设置的密码名称) 举个例子: ```yml # Deploy - name: SFTP uploader uses: wangyucode/sftp-upload-action@v1.1 with: host: "wycode.cn" port: 22 # optional, default is 22 username: ${{ secrets.SERVER_USERNAME }} # optional, default is root password: ${{ secrets.SERVER_PASSWORD }} localDir: "dist" remoteDir: "/var/www/dealer/" dryRun: false # optional ``` > 注意: 不要使用命令传递密码,应使用环境变量或 action 的输入。 ## 自定义 Action(造轮子) 可以使用`Docker`或`JavaScript` 1. 创建一个仓库 2. 编写 action 定义文件 3. 发布 action 我写的 sftp 部署 action 的例子: ```yml name: "SFTP uploader" color: "green" icon: "upload" description: "upload files to server via SFTP" inputs: host: required: true port: required: false default: 22 username: required: false default: "root" password: required: true localDir: required: true remoteDir: required: true dryRun: required: false default: false runs: using: "node12" main: "dist/index.js" ``` ## 完整例子 这是一个《谁是卧底发牌员》的网页应用,使用`AngularJS`,线上地址:。 提交代码后,Github Action 会自动编译部署到线上。 --- ## 用数学期望计算《和平精英》特斯拉(玛莎拉蒂)到底要多少钱? > 原文: https://wycode.cn/blog/chicken-dinner-tesla | Agent 文档: https://wycode.cn/blog/chicken-dinner-tesla.md | 日期: 2020-07-24 | 分类: Life ![tesela](https://game.gtimg.cn/images/gp/ingame/a20200724tslcar/intor_6.png) 今天我们用数学期望来计算一下《和平精英》的特斯拉(玛莎拉蒂)到底要多少钱?怎么抽最划算? ## 规则 扭蛋等级概率: | 1星 | 2星 | 3星 | | :-: | :-: | :-: | | 82% | 17% | 1% | 保护追加不会降低扭蛋等级,并且追加三次必定能提升扭蛋等级;保护追加需要消耗一定的幸运币,扭蛋等级越高,保护追加消耗的幸运币数量越多。 | 1星 | 2星 | 3星 | 4星 | 5星 | 6星 | | :-: | :-: | :-: | :-: | :-: | :-: | | 6 | 17 | 51 | 153 | 430 | 827 | ## 保底法/碎片兑换 ```bash 抽1次的碎片期望 = 12 * 82% + 36 * 17% + 54 * 1% = 16.5碎片 兑换一辆特斯拉需要 2880 碎片 所以需要抽 2880/16.5 ≈ 175次 175 * 6 = 1047点劵 紫色 * 3 = 3141点券 ``` ## 免费追加法 ```bash 成功期望 = 20% * 82% + 20% * 17% * 2 + 20% * 1% * 3 = 0.238 失败期望 = 80% * 75% * -1 + 80% * 75% * -2 = -1.8 免费追加期望为-1.56星 ``` ## 保护追加法 ```bash 一次成功概率 = 20% 两次成功概率 = 20% * 20% = 4% 其余都需要3次 即 76% 需要抽3次 每升一个星需要的次数 = 20% + 4% * 2 + 76% * 3 = 2.56次 每次升级的星星期望 = 82% * 1 + 17% * 2 + 1% * 3 = 1.19星 升到6星总共需要 = 5.04次 即大概率需要升星5次 ``` | 1星→2星 | 2星→3星 | 3星→4星 | 4星→5星 | 5星→6星 | 6星→7星 | | :-: | :-: | :-: | :-: | :-: | :-: | | 15.36 | 43.52 | 130.56 | 391.68 | 1100.8 | 1681.92 | ```bash 所以6星 = 15.36 + 43.52 + 130.56 + 391.68 + 1100.8 = 1681.92点劵 所以7星 = 15.36 + 43.52 + 130.56 + 391.68 + 1100.8 + 1681.92= 3363.84点劵 ``` ## 运气爆棚和脸最黑的情况 ### 保底法: 因为保底法需要操作175次,基本会接近期望数字。 **基本不存在**175次全3星和175次全1星的情况(小于一亿分之一),即基本没有运气爆棚的情况,和脸黑的情况。 ### 免费追加法: 免费升星法最好的情况(6元提车)只需要操作两次,所以幸存者偏差会**特别大**。 那么传说中6元提车的概率是多少呢: | 可能的抽法 | 排列可能 | 概率 | | :-: | :-: | :-: | | 3,3组合 | 1种 | 0.0000004 | | 1,2,3组合 | 6种 | 0.000066912 | | 1,1,1,1,2组合 | 5种 | 0.00361697 | | 1,1,2,2组合 | 6种 | 0.00018655 | | 2,2,2组合 | 1种 | 0.000039304 | | 1,1,1,1,1,1组合 | 1种 | 0.000019456 | 合计约千分之4,即你每尝试250次可能才能成功1次。 当你尝试250次的时候 你已经花了1500点券了。 至于最坏的情况,你可能要永远转下去了。 ## 保护追加法: 和免费追加一样,最好的情况只需操作2次,概率也是一样的。 不同之处在于最坏的情况,因为有保护,不至于一直转下去: ```bash ( 6 + 17 + 51 + 153 + 430 ) * 3 = 1971 七星需要再 + 827 * 3 = 4452 ``` > 总结:虽然保底法期望花费最少,但是很多人都忍不住去赌一把,原因就在于人性是喜爱赌博的。最后希望各位根据自己的实力,理性消费。 --- ## 使用WebSocket和STOMP协议开发一个匿名聊天室 > 原文: https://wycode.cn/blog/websocket | Agent 文档: https://wycode.cn/blog/websocket.md | 日期: 2020-03-09 | 分类: Backend ![chat](/apps/chat2.png) 有时候就想和其他人安全地聊个天,它需要有这些特征: - 基于web技术,只需一个网页,无需下载任何App,易于转发。 - 无需注册,使用简单,基于邀请码加入聊天,邀请码定时刷新。 - 完全匿名。 - 消息只保留在内存中,超时自动删除。 - 进行在线人数统计,能设置最大人数上限。 - 用户上下线和邀请码刷新时有系统提示。 - Web连接必然是不稳定的,所以断线必须能够重连。 线上Demo演示请直接访问: 技术方面: - 使用`WebSocket`协议提供Web上的全双工通信支持。 - 使用`STOMP`作为上层协议,它基于帧(`Frame`),和Http请求很类似,提供了一种发布订阅的模式,很适合需求的群聊模式。 - 后端采用`Kotlin` + `SpringBoot`,前端采用`Typescript` + `Angular` + `AntDesign`,均使用最新版本。 ## 准备SpringBoot工程 在SpringBoot中使用WebSocket和STOMP很简单,添加`spring-boot-starter-websocket`这个starter依赖就可以开始了。 工程基于Gradle初始化,核心的几个依赖如下: ```java implementation("org.springframework.boot:spring-boot-starter-websocket") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.+") ``` ## 配置WebSocket和STOMP 添加这么一个类: ```kotlin @Configuration @EnableWebSocketMessageBroker class WebSocketConfig(val chatService: ChatService, val taskScheduler: TaskScheduler) : WebSocketMessageBrokerConfigurer { override fun configureMessageBroker(config: MessageBrokerRegistry) { config.enableSimpleBroker("/topic/", "/queue/") // 1. .setTaskScheduler(taskScheduler) // 2. config.setApplicationDestinationPrefixes("/app") // 3. } override fun registerStompEndpoints(registry: StompEndpointRegistry) { registry.addEndpoint("/stomp") // 4. .setAllowedOrigins("*") // 5. } } ``` 这里配置了很多端点(`endpoint`),很多时候容易搞混: 1. 可以理解为是一些频道 2. 用于定时心跳,设置了taskSchedule才能启用心跳(对于没有启用SockJS的项目),不开启心跳的话,不管是客户端还是服务器都很容易断开连接。 3. 这里`/app`端点是应用的接收前缀,以app开头的路径会进入后端程序处理 4. 这里`/stomp`端点是用来握手的,所以前端connect时连接的是这个端点 5. 启用任意远程访问,防止前端出现跨域问题,易于调试,对于生产环境,可以删除 > 需要提到的是,官方文档上仍然启用了`SockJS`,其实已经不需要了,`SockJS`是对于不支持`WebSocket`的Polyfill,对于现在的浏览器,其实已经不需要了,参考这个地址:https://caniuse.com/#search=websockets ## 邀请码鉴权 主要思路: - 配置客户端入口Channel的interceptor。`configureClientInboundChannel()` - 在握手阶段,取header。`accessor.getNativeHeader("code")` - code合法就赋予一个用户。`accessor.user = ChatUser()` - 出错时,设置一个sessionAttributes。`accessor.sessionAttributes!!["error"] = "邀请码错误!"` > 这里我尝试了多种办法直接在握手阶段返回错误,但是都失败了,所以最终通过sessionAttributes来标记错误,在下一次通信时返回错误信息。 此处除了鉴权还做了三件事 1. 对于本身分配了id的用户握手时,应当视为重连,而不是新用户,这是断线重连实现的关键。 2. 新用户进入时要进行人数上限的判断,人满时也报错。 3. 因为只有进入聊天室的人才能看到邀请码,所以这里还判断了一个超级用户码,用户以超级用户码进入时,会把当前的邀请码以错误码的形式返回。这样管理员就能方便的知道当前的邀请码了。 ## 主要接口 `controller`只需要两个接口 1. 用来查询当前状态,用户刚进入时会调用,此时会如果有鉴权时的错误信息,会返回错误。如果正常会返回房间的基本信息,以及所有的聊天记录。同时发送系统信息。 ```kotlin @MessageMapping("/status") @SendToUser("/queue/status") fun status(accessor: SimpMessageHeaderAccessor): CommonMessage<*> { return if (accessor.user != null) { chatService.sendSystemMessage(200, accessor.user!!.name) CommonMessage.success(InitData(accessor.user!!.name.toInt(), chatService.users.size, chatService.messages, chatService.code,GEN_CODE_TIME_IN_MINUTES, REMOVE_MESSAGE_TIME_IN_MINUTES)) } else { CommonMessage.fail(accessor.sessionAttributes!!["error"] as String) } } ``` 2. 发送消息,添加到聊天记录即可。 ```kotlin @MessageMapping("/all") fun all(message: String, user: Principal): CommonMessage { val msg = ChatMessage(user.name.toInt(), Date(), message, 0) chatService.messages.add(msg) return CommonMessage.success(msg) } ``` ## 定时服务及系统消息 `ChatService`中要维护所有用户、历史消息记录、邀请码 1. 所有用户是一个HashSet,`var users = HashSet()`,`ChatUser`使用id作为`hashcode`。 2. 更新邀请码的频率较高,所以会同时检查过期消息并移除 ```kotlin fun generateCode() { this.code = randomString(16) logger.info("${Date().toLocaleString()}: $code") this.sendSystemMessage(100, this.code) removeOutdatedMessage() } ``` 3. 移除过期消息,消息列表是一个队列,如果队列头部的消息已过期,需要递归检查下一个 ```kotlin fun removeOutdatedMessage() { logger.info("${Date().toLocaleString()}: removeOutdatedMessage") if (messages.size > 0) { val message = messages[0] if (Date().time - message.time.time > REMOVE_MESSAGE_TIME_IN_MINUTES * 60L * 1000) { messages.removeAt(0) removeOutdatedMessage() } } } ``` 至此,后端核心技术要点已介绍完毕,最后放上github仓库,完整代码: 线上完整项目演示请直接访问: 基于Angular的前端项目将在下篇博客介绍。 --- ## 使用Docker容器部署SpringBoot项目 > 原文: https://wycode.cn/blog/docker-spring | Agent 文档: https://wycode.cn/blog/docker-spring.md | 日期: 2019-11-20 | 分类: Backend ![docker](https://d1q6f0aelx0por.cloudfront.net/product-logos/644d2f15-c5db-4731-a353-ace6235841fa-registry.png) Docker容器化技术,使得部署变得平滑。再也不需要维护软件运行所需的复杂环境。Docker和现代化的微服务,敏捷开发,快速迭代和交付的要求非常匹配。 这篇文章实践从安装Docker开始,到将一个SpringBoot项目部署到Docker容器中。 ## 安装Docker CE(Community edition) 我这里使用的环境是`Ubuntu18.04` ```bash $ curl -fsSL https://get.docker.com -o get-docker.sh $ sudo sh get-docker.sh $ systemctl enable docker $ systemctl start docker ``` ## 准备SpringBoot工程 这里我使用Spring官方的Docker示例项目 `git clone https://github.com/spring-guides/gs-spring-boot-docker.git` ## Dockerfile SpringBoot的`Dockerfile`有两种 简单版的如下: ``` FROM openjdk:8-jdk-alpine VOLUME /tmp ARG JAR_FILE COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"] ``` 这是典型的SpringBoot项目启动方式 复杂版的如下: ``` FROM openjdk:8-jdk-alpine VOLUME /tmp ARG DEPENDENCY=target/dependency COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib COPY ${DEPENDENCY}/META-INF /app/META-INF COPY ${DEPENDENCY}/BOOT-INF/classes /app ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"] ``` 这是将lib和src分离的方式,有点像展开的jar 我这里选择上面简单版的,简洁和性能之间我选简洁! ## 基于maven构建Docker镜像 使用Maven的话有Spotify构建的`dockerfile-maven-plugin`插件 ```xml com.spotify dockerfile-maven-plugin 1.4.9 default install build push ${docker.image.prefix}/${project.artifactId} ${project.build.finalName}.jar ``` - 添加了两个maven任务,build用户构建docker镜像,push用于推送docker镜像。 - 添加了构建参数`buildArgs`>`JAR_FILE`传递给Dockerfile。 - 添加了版本号作为镜像tag > 打包Docker镜像本身是需要docker环境的,于是有一种方法是开启remote API > 到这里其实就已经可以发布程序到DockerHub了,但实际项目中,我们不想发布私人镜像到DockerHub,那么我们需要建立私人的Docker仓库。 ## 开启remote API远程构建镜像 Docker是CS模式,Docker的服务是支持通过Http远程访问的。通过开启remote API即可远程进行访问。 ``` ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2376 ``` 但要注意的remote API是需要进行TSL加密的。否则存在未授权访问漏洞,会被黑客利用,植入挖矿程序(别问我怎么知道的)。 ## 建立Docker私人仓库(Docker Registry) 具体方式是添加一个仓库镜像 ```bash $ docker run -d -p 5000:5000 --restart always --name registry registry:2 ``` 然后就可以: ```bash $ docker pull ubuntu $ docker tag ubuntu localhost:5000/ubuntu $ docker push localhost:5000/ubuntu ``` > 为了安全,创建的私有仓库也要添加Https ## 总结 使用docker部署SpringBoot工程时,正规的流程应该是: 1. 为工程编写Dockerfile 2. 添加Docker插件,构建Docker镜像 3. push到公共或私有Docker仓库 4. 拉取最新镜像并运行 本地无法构建时,应使用某种持续集成方法让编译机去负责出包和推送,不应该使用remote API这不是常规做法,而且有安全风险。 此外Docker官方提供了安全的私有Docker仓库,少量使用是免费的。 --- ## VirtualBox的虚拟机网络配置 > 原文: https://wycode.cn/blog/virtualbox-network | Agent 文档: https://wycode.cn/blog/virtualbox-network.md | 日期: 2019-09-20 | 分类: DevOps ![virtual-box](https://www.virtualbox.org/graphics/vbox_logo2_gradient.png) VirtualBox是著名的开源虚拟机软件,最初由Sun公司出品。后来Sun公司被Oracle收购。Sun公司的包括Java和VirtualBox,现在都是Oracle公司的软件。 配置虚拟机时,对于如何选择网络类型一直是一头雾水,这篇文章将VirtualBox的7种网络模式全部都总结一下。 不同模式的原理,也适用于VMWare。 ## Not attached 这个模式会向虚拟机报告有网卡,但没有物理连接。 就像拔网线一个效果。 对于通知虚拟机重新配置网络很有用。 ## Network Address Translation (NAT) 如果你只是想在虚拟机内访问互联网,这个模式就足够了。 这种模式有很多限制,比如无法ping通。不支持UDP广播。除了TCP和UDP之外的协议不支持。 ## NAT Network 和NAT差不多,NAT网络就像虚拟机在一个路由器后面一样。外部访问需要设置端口转发。 ## Bridged Networking 即桥接,虚拟机将直接链接某个宿主机物理网卡,而不用通过宿主机操作系统的网络。 ## Internal networking 这种网络仅在虚拟机内部可见,宿主机及外部不可见。 ## Host-only networking 这种可以创建一个虚拟的网络组,包含宿主机和虚拟机,宿主机上会有一个虚拟的网卡,提供和虚拟机之间的连接。 ## Generic networking 这种相当于自定义,可以自由选择网卡驱动,一般情况下用不着这个。 ## 各种模式可访问性: | 模式 | 虚拟机→宿主机 | 宿主机→虚拟机 | 虚拟机A→虚拟机B | 虚拟机→互联网 | 互联网→虚拟机 | | ------ | ------------ | ------------ | -------------- | ------------ | ------------ | | Host-only | √ | √ | √ | X | X | | Internal | X | X | √ | X | X | | Bridged | √ | √ | √ | √ | √ | | NAT | √ | 端口转发 | X | √ | 端口转发 | | NATservice | √ | 端口转发 | √ | √ | 端口转发 | ## 总结 绝大多数情况下,我们需要虚拟机可以上网,这时设置一个`NAT`的模式。 然后我们还需要主机可以访问虚拟机的服务,这时我们可以为虚拟机再加一个网卡设置为`Host-only`模式。 --- ## 实现内网穿透的最新姿势 > 原文: https://wycode.cn/blog/ngrok | Agent 文档: https://wycode.cn/blog/ngrok.md | 日期: 2019-09-02 | 分类: DevOps ![Network](https://www.howtogeek.com/thumbcache/2/200/b47f24b18abd7d5411b75447849e04ff/wp-content/uploads/2017/03/why-is-a-network-tunnel-called-a-tunnel-00.jpg) 当我们需要在外网访问处于内网的服务时,就需要内网穿透这个技术了。 本文介绍实现内网穿透的多种方式 ## 有外网IP 如果你的宽带接入是有外网IP的,那很简单,只需要设置路由器的端口转发就可以了。 查看自己是不是有外网IP也很简单,查看路由器获取到的IP和真实的外网IP是否一致就知道了。 真实外网IP,通过搜索引擎搜索IP即可获取。 由于现在小区宽带都是接入到服务商的大路由上,IPV4的资源也很珍贵,所以基本都是没有外网IP的,本文重点讨论没有外网IP的情况。 ## 付费方案 [https://www.oray.com](https://www.oray.com) 花生壳提供软件解决方案,同时他们也提供能够实现穿透的硬件方案。 当然还有`NAT123`,但是他们的官网就像是上个世纪的网站,还是算了吧。 ## ngrok ![Network](https://camo.githubusercontent.com/f2d698991e6a0411680413ebcc15a6460b8beda3/68747470733a2f2f6e67726f6b2e636f6d2f7374617469632f696d672f6f766572766965772e706e67) ngrok1.x是开源的,地址在这:[https://github.com/inconshreveable/ngrok](https://github.com/inconshreveable/ngrok) ngrok2.x是闭源的,官网在这:[https://ngrok.com](https://ngrok.com) ngrok商业化之后估计1.x就很少维护了,2.x目前是有限制的免费。 ## frp ![Network](https://github.com/fatedier/frp/raw/master/doc/pic/architecture.png) `frp`是一个开源的内网穿透的反向代理应用,支持 tcp, udp 协议。 [https://github.com/fatedier/frp](https://github.com/fatedier/frp) 用户通过访问一个装有`frps`的公网服务器,由服务器代理访问处于内网,并安装了`frpc`的内网服务 配置方式,请参考他们的github仓库 ## v2ray `v2ray`通常用于科学上网,但是由于一个 V2Ray 进程可并发支持多个入站和出站协议,所以它也可以用于内网穿透。 反向代理的大致工作原理如下: - 假设在主机 A 中有一个网页服务器,这台主机没有公网 IP,无法在公网上直接访问。另有一台主机 B,它可以由公网访问。现在我们需要把 B 作为入口,把流量从 B 转发到 A。 - 在主机 A 中配置一个 V2Ray,称为bridge,在 B 中也配置一个 V2Ray,称为portal。 - bridge会向portal主动建立连接,此连接的目标地址可以自行设定。portal会收到两种连接,一是由bridge发来的连接,二是公网用户发来的连接。portal会自动将两类连接合并。于是bridge就可以收到公网流量了。 - bridge在收到公网流量之后,会将其原封不动地发给主机 A 中的网页服务器。当然,这一步需要路由的协作。 - bridge会根据流量的大小进行动态的负载均衡。 具体配置请参考官网及github [https://www.v2ray.com](https://www.v2ray.com) [https://github.com/V2Ray/v2ray-core](https://github.com/V2Ray/v2ray-core) --- ## H2数据库修改密码 > 原文: https://wycode.cn/blog/h2-password | Agent 文档: https://wycode.cn/blog/h2-password.md | 日期: 2019-08-14 | 分类: Backend ![H2](https://www.h2database.com/html/images/h2-logo-2.png) 使用中的H2数据库如何修改用户名密码呢?执行下面的SQL就可以了: ```sql alter user wayne set password '*****password*****' ``` --- ## 用树莓派4B做一个鱼缸水位报警 > 原文: https://wycode.cn/blog/raspberrypi-fish | Agent 文档: https://wycode.cn/blog/raspberrypi-fish.md | 日期: 2019-08-07 | 分类: DevOps 因为最近要出门一段时间,鱼缸没人照顾,树莓派的第一个实用需求出现了。 准备做一个液位报警仪,当鱼缸水位低于报警水位,发一封邮件给我,我就能在微信上收到通知,从而远程控制给鱼缸加水,防止鱼缸循环泵烧坏。 使用GPIO接口控制硬件和接收传感器的数据,实现过程如下: ## GPIO介绍 树莓派的一个强大功能是沿着电路板顶部边缘的一排`GPIO`(general-purpose input/output)引脚。 树莓派4B板上有一排40针GPIO接头。 ![GPIO](https://technicalustad.com/wp-content/uploads/2018/02/Raspberry-Pi-GPIO-Explained.png) 主板上有两个5V引脚和两个3V3引脚,以及一些不可配置的接地引脚(0V)。 其余引脚均为通用3V3引脚,意味着输出设置为3V3,输入为3V3容差。 ### 输出 指定为输出引脚的GPIO引脚可以设置为高(3V3)或低(0V)。 ### 输入 指定为输入引脚的GPIO引脚可以读为高(3V3)或低(0V)。 使用内部上拉或下拉电阻可以更轻松地实现这一点。 引脚GPIO2和GPIO3具有固定的上拉电阻,但对于其他引脚,可以使用软件配置。 ### 其它 除了简单的输入和输出设备外,GPIO引脚还可以使用多种替代功能,一些可用于所有引脚,另一些则可用于特定引脚。如:PWM、SPI、I2C、串行 ## 装备 - 面包板:用于免焊接线,边上两排是竖向连接的,中间是横向连接的。用杜邦线插入孔中就可以便捷接线了。 ![面包板](http://www.taichi-maker.com/wp-content/uploads/2017/03/breadboard.jpg) - GPIO扩展版:可以方便地将所有GPIO接到面包板上 ![面包板](http://g.search1.alicdn.com/img/bao/uploaded/i4/i3/TB1jpt3LXXXXXbnXpXXXXXXXXXX_!!0-item_pic.jpg_200x200.jpg) - LED二极管:用于展示状态 ![二极管](http://file.youboy.com/a/83/81/74/5/9777755.jpg) - 干簧管液位计:用于监测液位,其原理是浮球中有磁铁,可以磁化干簧管,从而短路电路 ![液位计](http://i01.yizimg.com/uploads_old/351305/2015112223270383_old.jpg) ## 接线 LED一端接GPIO17号引脚,一端接地。 液位开关一端接GPIO2号引脚,一端接地。 ![接线](/images/20190807_fish.jpg) ## 编程 这里我使用`GPIO Zero` 这是一个Python的GPIO库,树莓派默认已经安装了,可以直接使用。 核心脚本如下: ```python from gpiozero import LED from gpiozero import Button from time import sleep led = LED(17) #1 button = Button(2) #2 while True: sleep(0.5) #3 if button.is_pressed: #4 led.on() else: led.off() ``` 1. 创建一个LED模式的GPIO口用于展示状态 2. 创建一个Button模式的GPIO口 3. 每0.5秒检测一次 4. 开关打开则点亮LED,否则关闭LED ## Python发送邮件 ```python #coding=utf-8 from email.mime.text import MIMEText from email.header import Header from smtplib import SMTP_SSL import time #邮箱smtp服务器 host_server = 'smtp.exmail.qq.com' #sender用户名 sender_user = 'wangyu@wycode.cn' #邮箱登录密码 sender_pwd = '**********' #发件人的邮箱 sender_mail = sender_user #收件人邮箱 receiver = sender_user #邮件的正文内容 mail_content = '【水位警报】过低\n时间:'+time.strftime( '%Y-%m-%d %X', time.localtime()) #邮件标题 mail_title = '【水位警报】过低' #ssl登录 smtp = SMTP_SSL(host_server) #set_debuglevel()是用来调试的。参数值为1表示开启调试模式,参数值为0关闭调试模式 smtp.set_debuglevel(1) smtp.ehlo(host_server) smtp.login(sender_user, sender_pwd) msg = MIMEText(mail_content, "plain", 'utf-8') msg["Subject"] = Header(mail_title, 'utf-8') msg["From"] = sender_mail msg["To"] = receiver smtp.sendmail(sender_mail, receiver, msg.as_string()) smtp.quit() ``` --- ## 树莓派4B系统安装及配置 > 原文: https://wycode.cn/blog/raspberrypi | Agent 文档: https://wycode.cn/blog/raspberrypi.md | 日期: 2019-08-07 | 分类: DevOps 开启吃派之旅! ![树莓派4B](https://s3.ifanr.com/wp-content/uploads/2019/06/mousse-68.jpg!720) 2019年6月25日树莓派基金会发布了全新的Raspberry Pi 4 「树莓派4 代」。 相比上一代升级了很多,价格依旧250元(1G版),配置如下: | 项目 | 配置 | | --- | --- | | CPU | 巴龙BCM2711, 4核 Cortex-A72 (ARM v8) 64位 主频1.5GHz | | 内存 | 1GB/2GB/4GB LPDDR4-2400 SDRAM | | 有线网络 | 千兆以太网 | | 无线网络 | 2.4 GHz/5.0 GHz IEEE 802.11ac无线,蓝牙5.0,支持低功耗蓝牙 | | 显示接口 | 双micro HDMI,最高支持4K60Hz | | USB | 2个USB3.0+2个USB2.0 | | GPIO | 40pin兼容以前版本 | | 电源 | 5V0.3A通过USB type-C 或 GPIO接口 | | 存储 | Micro-SD加载操作系统和数据存储 | 这篇文章带你安装和配置树莓派: ## 烧录镜像 为了给树莓派安装操作系统,我们需要一张Micro-SD卡,然后将系统写进去。 根据官网介绍,有两个推荐的下载连接,一个是`NOOBS`,一个是`Raspbian`。 - `NOOBS`是一个操作系统安装程序,它已经包含了`Raspbian`和`LiberELEC`,也可以通过网络安装其它操作系统。 - `Raspbian`是树莓派基金会基于`Debian buster`开发的官方操作系统。 我开始尝试使用`NOOBS`时,树莓派无法启动。官网说可能是`SPI EEPROM`,可以下载bootloader重新启动。 但我没有再折腾这个`NOOBS`,而是采用了第二种,直接写入`Raspbian`镜像的方式,然后我就正常启动了树莓派系统。 ![Raspbian](/images/20190807_raspberrypi.jpg) ## 鼠标速度慢 系统启动后发现移动速度很慢,图形界面调了也没用 解决方案: 手动修改鼠标的轮询间隔: ```bash $ sudo nano /boot/cmdline.txt ``` 然后在最后添加`usbhid.mousepoll=0`。 ```bash $ reboot ``` 重启树莓派,鼠标终于可以正常使用了。 参考地址:https://whoisnian.com/2017/10/26/Raspberry图形界面下鼠标移动缓慢/ ## 远程连接 在`Raspberry Pi Configuration`里打开`SSH`和`VNC` 然后我们就可以通过局域网`SSH`或者`VNC Viewer`远程访问树莓派了 ## VNC `Cannot currently show the desktop` 使用`VNC Viewer`远程连接时显示`Cannot currently show the desktop` 解决方案: ```bash $ cd /boot/ $ sudo nano config.txt ``` 取消注释以下3行 ```yml # uncomment if hdmi display is not detected and composite is being output hdmi_force_hotplug=1 # uncomment to force a specific HDMI mode (this will force VGA) hdmi_group=1 hdmi_mode=1 ``` 什么?你想要通过互联网远程访问?那么我们需要**内网穿透**这门技术。下篇文章我们来搞这个。 --- ## 《富爸爸,穷爸爸》热门标注 > 原文: https://wycode.cn/blog/rich-dad | Agent 文档: https://wycode.cn/blog/rich-dad.md | 日期: 2019-07-26 | 分类: Life ![rich-dad](https://images-cn.ssl-images-amazon.com/images/I/51mZHfA%2B-6L._SX351_BO1,204,203,200_.jpg) > 人穷穷观念,人富钱生钱。 《富爸爸,穷爸爸》是一个真实的故事,作者罗伯特・清崎的亲生父亲和朋友的父亲对金钱的看法截然不同,这使他对认识金钱产生了兴趣,最终他接受了朋友的父亲的建议,也就是书中所说的。“富爸爸”的观念,即不要做金钱的奴隶,要让金钱为我们工作,并由此成为一名极富传奇色彩的成功的投资家。 本文摘抄了《富爸爸,穷爸爸》在Kindle电子书中的热门标注: ## 热门标注 > 贫穷和破产的区别是,破产是暂时的,而贫穷是永久的。 > 不如意的时候,如果你认为是别人的问题,你就会试图改变他人;如果你认为问题在自己,你就会改变自己,学习一些东西让自己变得更聪明。大多数人认为世界上除了自己之外,其他人都应该改变。其实,改变自己比改变他人更容易。 > 拥有公司的富人:赚钱->消费->纳税;在公司上班的穷人:赚钱->纳税->消费 以上,转载请注明出处! --- ## 使用Flutter开发Web应用 > 原文: https://wycode.cn/blog/flutter-web | Agent 文档: https://wycode.cn/blog/flutter-web.md | 日期: 2019-07-25 | 分类: Frontend ![flutter](https://flutter.dev/assets/flutter-lockup-4cb0ee072ab312e59784d9fbf4fb7ad42688a7fdaea1270ccf6bbf4f34b7e03f.svg) 在Google IO 2019开发者大会上,Flutter和Dart发表了超越移动端,为iOS,Android,ChromeOS和Web构建应用的主题演讲。 时至今日,想必Flutter已经可以开发Web应用了。 我们知道Flutter是使用叫`Skia`的C++库来渲染UI,但是Web上是使用`Dom`,`Canvas`,以及`WebGL`,那么Flutter到底会采用哪种方式呢? 这篇文章带你对Flutter for web做一个了解和实践: ## 架构设计 ![Architecture](https://flutter.dev/images/Dart-framework-v-browser-framework.png) ## 原理 目前联合了`DOM`, `Canvas`, `CSS`来进行渲染,将`Dart`代码编译为`JavaScript`来执行,这跟`Typescript`,`Kotlin`都是一样的。 ## 目前进度 截至今天(2019年7月25日)Flutter for Web还处于技术预览的阶段,还很不稳定。 开发团队创建了一个[Frok](https://github.com/flutter/flutter_web),但最终这些代码将会被merge到中。 ## 环境安装 1. ~~安装`Dart`~~ 2. 安装`Flutter`,Flutter 1.5 以上就可以支持以web作为target平台,我这里安装的是`1.7.8`。 3. 安装`webdev`,通过`flutter pub global active webdev`全局安装`webdev`工具包。 4. 为了能够全局执行`webdev`命令,建议将`flutter/.pub-cache/bin`目录添加到`path`环境变量中。 ## Hello World 克隆 ```bash $cd examples/hello_world/ $flutter pub upgrade $webdev serve ``` 打开,你就能看到红色的HelloWorld 经过测试,在Edge浏览器会报错,目前只在Chrome中支持比较好。 ## 新建一个项目 虽然未来的目标是用同一套代码来生成移动端和Web端,但目前的开发使用了不同的命名空间,所以还无法同时进行移动端和web端开发。 新建的Flutter工程最简单的方式是使用IDE - VScode: 使用View->Command Paltte->`Flutter: New Web Project`。 - IntelliJ:新建Dart项目,选择`Flutter for web`模板。 --- ## SpringBoot工程为SwaggerUI添加认证 > 原文: https://wycode.cn/blog/swagger-auth | Agent 文档: https://wycode.cn/blog/swagger-auth.md | 日期: 2019-07-19 | 分类: Backend ![swagger](https://swagger.io/swagger/media/assets/images/swagger_logo.svg) 当API添加了鉴权后,`swagger`如何进行调试呢,比如需要在请求头`header`中添加类似`token`或者`Authorization`之类的。 在`Spring`中进行如下简单的配置就可以了。 ```kotlin @Configuration @EnableSwagger2 class SwaggerConfig { @Bean fun generateDocket(): Docket { return Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.withClassAnnotation(Api::class.java)) .build() .securityContexts(listOf(securityContext())) // #1 .securitySchemes(listOf(apiKey())) // #2 .apiInfo(apiInfo()) } private fun securityContext(): SecurityContext { return SecurityContext.builder() .securityReferences(defaultAuth()) .forPaths(PathSelectors.regex("/api/public/admin/.*")) .build() } fun defaultAuth(): List { val authorizationScope = AuthorizationScope("global", "accessEverything") val authorizationScopes = arrayOf(authorizationScope) return listOf(SecurityReference("token", authorizationScopes)) } fun apiKey(): ApiKey { return ApiKey("token", "X-Auth-Token", "header") } private fun apiInfo(): ApiInfo { return ApiInfo("王郁的API文档", "用于App测试及独立APP研发", "2.0", "http://wycode.cn", Contact("王郁", "wycode.cn", "wangyu@wycode.cn"), "wycode.cn All Right Reserved", "https://wycode.cn", emptyList()) } } ``` 关键配置就是上面包含#1和#2注释的两行配置 1. 配置安全上下文,我这里配置了`/api/public/admin/.*`下的接口需要添加。 2. 配置鉴权类型,我这里使用`ApiKey`。 --- ## SpringBoot工程自定义错误JSON字段 > 原文: https://wycode.cn/blog/spring-error | Agent 文档: https://wycode.cn/blog/spring-error.md | 日期: 2019-07-15 | 分类: Backend ![springboot](/images/20180623_springboot.jpg) 通常我们为了统一,和前端方便处理,写的API返回JSON会有固定的结构,比如下面这种: ```json { "code":200, "message":"hello world!", "data":{ "user":{} } } ``` 但是当返回码为非200时,比如接口不存在`404`,服务器错误`500`,Spring默认会返回一个类似这样的结构: ```json { "timestamp": "2019-07-19 10:23:47", "status": 403, "error": "Forbidden", "message": "没有权限", "path": "/web/api/public/admin/dota/version" } ``` 下面进行简单配置即可实现这个JSON结构的自定义: ```kotlin import cn.wycode.web.DEV import org.springframework.boot.web.servlet.error.DefaultErrorAttributes import org.springframework.stereotype.Component import org.springframework.web.context.request.WebRequest @Component class WyErrorAttributes : DefaultErrorAttributes(DEV) { // #1 override fun getErrorAttributes(webRequest: WebRequest?, includeStackTrace: Boolean): MutableMap { val errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace) // #2 errorAttributes["success"] = false // #3 errorAttributes["error"] = errorAttributes["message"] // #4 errorAttributes["data"] = errorAttributes["exception"] // #5 return errorAttributes } } ``` 1. Spring默认的`DefaultErrorAttributes`就写的挺好,我们继承并扩展一下就可以了,当然你也可以删除或者完全替换里面的结构。 2. 先使用默认的方法构建一个`errorAttributes`。 3. 添加一个`success`属性,既然是`ErrorAttributes`当然是`false`了。 4. 覆盖`error`属性,改为`message`。 5. 添加`data`属性,在DEV环境时会添加错误的类信息。 做了这个配置之后,错误信息的JSON会变成下面这样: ```json { "timestamp": "2019-07-19 16:56:41", "status": 500, "error": "/ by zero", "exception": "java.lang.ArithmeticException", "message": "/ by zero", "path": "/web/api/public/admin/dota/version", "success": false, "data": "java.lang.ArithmeticException" } ``` --- ## OAuth2.0四种模式完整时序图 > 原文: https://wycode.cn/blog/oauth | Agent 文档: https://wycode.cn/blog/oauth.md | 日期: 2019-06-14 | 分类: Backend 关于认证和授权,最权威的流程就是`OAuth2.0`了,涵盖了用户认证授权的方方面面,现代的大型网站几乎都是基于`Oauth2.0`协议开发认证鉴权功能。 `OAuth2.0`被写入`RFC6749`,RFC中的流程图看了还是不明白的话,可以看看下面这张图,展示了`OAuth2.0`四种模式的完整流程。 ![OAuth](/images/20190614_oauth.png) --- ## 学习和拆解Ant-Design-Pro > 原文: https://wycode.cn/blog/ant-design-pro | Agent 文档: https://wycode.cn/blog/ant-design-pro.md | 日期: 2019-06-09 | 分类: Frontend ![ant-design](https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg) **Ant-Design**由阿里巴巴旗下蚂蚁金服团队设计开发,服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案。除了交互设计,还提供了开箱即用的高质量`React`和 `Angular`组件实现,用于开发和服务于企业级中后台产品。 **Ant-Design-Pro**是基于`Ant-Design`的功能完备的企业级中后台前端项目,里面可以学习和借鉴的内容很多。其核心基于以下几个技术集成。 - `React`:现代化的JavaScript界面库 - `dva`:类似`Redux`的数据流、状态管理框架 - `UmiJS`:类似`nextJS`的react应用框架 本文从基础使用开始,逐步解析里面用到的技术细节。 安装及启动在官网有说明,生成的基础工程目录结结构如下: ``` ├── config # umi 配置,包含路由,构建等配置 ├── mock # 本地模拟数据 ├── public # 公共资源目录 │ └── favicon.png # Favicon ├── src # 代码目录 │ ├── assets # 本地静态资源 │ ├── components # 业务通用组件 │ ├── e2e # 集成测试用例 │ ├── layouts # 通用布局 │ ├── models # 全局 dva model │ ├── pages # 业务页面入口和常用模板 │ ├── services # 后台接口服务 │ ├── utils # 工具库 │ ├── locales # 国际化资源 │ ├── global.less # 全局样式 │ └── global.js # 全局 JS ├── tests # 测试工具 ├── README.md # readme └── package.json # npm管理的清单 ``` ## 关于哈希路由 现代的前端框架都是单页面的(single-page application),即用户访问的永远是根目录的index.html,这时网页地址如何展现就是一个问题。解决方案有2个: - 前端解决:使用哈希路由,即用户看到的地址栏URL是`#`分割的,如`https://wycode.cn/admin/#/users/123`,实际访问仍然是`index.html`。`UmiJS`配置如下 ```js export default { ··· history: 'hash' ··· } as IConfig; ``` - 后端解决:使用重定向,即访问index以外的其它地址,都重定向到`index.html`。`nginx`配置如下: ```nginx location / { try_files $uri $uri/ /index.html; } ``` ## UmiJS非根目录部署 如果工程不是部署在根目录,UmiJS的config做如下配置即可: ```js export default { ··· base: 'admin' ··· } as IConfig; ``` > **但是:**当配置为hash路由时且非根目录部署时,有可能你希望的首页是`https://wycode.cn/admin/#/`,`/users`页是`https://wycode.cn/admin/#/users`则需要进行如下配置: ```js export default { ··· history: 'hash', publicPath: '/admin/' ··· } as IConfig; ``` ## 关于HTML模版 umi默认使用`src/pages/document.ejs`这个模版,采用`EJS`渲染,`Favicon`的路径改为`<%= context.publicPath %>favicon.png`可以修复Favicon路径不对的问题。 ## 关于区块`block` 区块可以理解为页面模板,这个模板是在`ant-design-pro`的预览网站中,如果你看中了某个功能,(默认创建的工程是空白的,只有一个Welcome页面)点击右下角的下载按钮,会弹出一个命令,执行这个命令就能将这个页面加入到你自己的工程中,非常方便。其实质是执行了`git clone`克隆预览工程的部分文件。 比如:添加一个类似预览工程的404页面 ```bash npx umi block add Exception404 --path=/exception/404 ``` 经过尝试,我发现目前区块引入时,并不是合并文件,而是创建一个目录,里面包括了这个页面用到的所有组件,甚至是国际化文本都是单独的一套。不管你工程内有没有这些组件,它都会重新创建一套。 比如刚加入了404页面又想加入403页面,本已经存在的Exception组件,会在403文件夹再创建一份一模一样的副本,这点,我觉得是不可接受的。 这样的话我更倾向与参考block的实现手动加入需要的文件,而不是使用`umi block add`功能,官方的block在这里:https://github.com/ant-design/pro-blocks ## 菜单处理 菜单是由路由自动生成,刚刚引入的404页面出现在了菜单栏,它是一个异常页,并不需要在菜单展示,我们只需在路由配置中加入`hideInMenu: true`,即可隐藏。 ## 404页面 输入一个不存在的地址,并不能跳转到刚刚加入的404页面。 我们需要将在路由配置中将404的其它配置项删除,仅保留`component: './exception/404'`,并将其挪到数组的最后,这样当路由无法匹配时,就会加载404这个组件了。 --- ## 使用SpringBoot发送SSL邮件 > 原文: https://wycode.cn/blog/email | Agent 文档: https://wycode.cn/blog/email.md | 日期: 2019-06-07 | 分类: Backend ![email](https://cms-assets.tutsplus.com/uploads/users/23/posts/26357/preview_image/professional-email-signature-icon.jpg) 当需要后台发送通知的时候,Email是一个比较好的选择,因为它免费啊! 这篇文章介绍如何使用SpringBoot发送邮件。 ## 添加依赖 如同其它`starter`工具一样。SpringBoot为我们提供了`spring-boot-starter-mail` ```js compile('org.springframework.boot:spring-boot-starter-mail') ``` ## 配置 在`application.properties`添加如下配置 ```js spring.mail.host=smtp.exmail.qq.com spring.mail.username=wangyu@wycode.cn spring.mail.password=********* spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true ``` - smtp域名,用户名,密码,根据自己的填写。 ## 创建Service ```kotlin package cn.wycode.web.service import org.apache.commons.logging.LogFactory import org.springframework.mail.SimpleMailMessage import org.springframework.mail.javamail.JavaMailSender import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @Service class MailService(private val mailSender: JavaMailSender) { private val log = LogFactory.getLog(this.javaClass) @Async fun sendSimpleMail(sendTo: String, subject: String, content: String) { val message = SimpleMailMessage() message.setFrom("wangyu@wycode.cn") message.setTo(sendTo) message.setSubject(subject) message.setText(content) try { mailSender.send(message) log.error("已发送邮件 $subject 到 $sendTo") } catch (e: Exception) { log.error("邮件发送失败!", e) } } } ``` - `JavaMailSender`由Spring自动注入 - `@Async`发送邮件这里采用异步方式,启用异步需要在Application使用`@EnableAsync` ## 关于SSL及发送端口 `Could not connect to SMTP host: smtp.163.com, port: 25` 本地运行ok,但是发布到阿里云ECS上就无法连接服务,这里是因为阿里云接上级通知禁用了25端口,客服建议使用安全的`465`端口。 网上的教程很复杂,其实对于正规的SMTP服务器,我们只需添加如下两个属性就好了。 ```js spring.mail.port=465 spring.mail.properties.mail.smtp.ssl.enable=true ``` - 端口改为`465` - 启用`SSL` 以上,转载请注明出处! --- ## 从0开始开发一个评论系统 > 原文: https://wycode.cn/blog/comments-sql | Agent 文档: https://wycode.cn/blog/comments-sql.md | 日期: 2019-05-21 | 分类: Backend ![网易云音乐](https://image.woshipm.com/wp-files/2018/04/QFTkYSkExyHKCzhTTl7V.jpg) 畅言的广告有点过于流氓,所以准备开发一套评论系统,除了博客,还能用在其它APP中。 基本需求类似网易云音乐的评论,可以对引用别人的评论回复,可以点赞。 ## 表结构 为了保证表结构尽可能简单的同时又支持类似网易云一样功能,同时又保证可扩展性,采用单表设计,表结构(映射实体)如下。 ```kotlin @Entity data class Comment( @Id @GeneratedValue(generator = "seq_comment") @SequenceGenerator(name = "seq_comment", sequenceName = "SEQ_COMMENT", allocationSize = 1, initialValue = 1) val id: Long = 0, @ApiModelProperty(value = "主题id") val topicId: String = "", @ManyToOne val app: CommentApp = CommentApp(), @Column(length = 1023) val content: String? = null, @ApiModelProperty(value = "评论类型,0.文字评论,1.点赞,2.图片评论") val type: Int = 0, val fromUserId: String = "", val fromUserName: String? = null, val fromUserIcon: String? = null, val toUserId: String? = null, val toUserName: String? = null, val toUserIcon: String? = null, val toContent: String? = null, val createTime: Date = Date(), var likeCount: Int = 0) ``` - 为了能够在多个app中使用,所以增加了一个`CommentApp`实体,里面包含一个name和accessKey,评论和app是多对一关系。 - `type`字段可以用来扩展为直接对文章点赞,以及图片的评论(`content`为图片地址)。 - 为了保持评论系统的独立,所以用户信息应该由使用评论的app提供,比如使用第三方登录。所以并不需要用户表。 - 这里冗余引用的评论信息,就不需要考虑嵌套查询,以及实体嵌套,循环引用的问题。只保留一层引用,当然也就只支持一对一的回复了。 - 页面展示上其实并不需要`UserId`,这里是考虑提高数据的可维护性。 ## 接口 ### 1.新增评论或点赞 ```kotlin @ApiOperation(value = "新增评论及点赞") @RequestMapping(path = ["/newComment"], method = [RequestMethod.POST]) fun newComment(@RequestParam accessKey: String, @RequestParam appName: String, @ApiParam("评论类型,0.文字评论,1.点赞,2.图片评论", defaultValue = "0",example = "0",allowableValues = "0,1,2") @RequestParam(required = false, defaultValue = "0") type: Int = 0, @RequestParam topicId: String, @RequestParam(required = false) content: String?, @RequestParam fromUserId: String, @RequestParam(required = false) fromUserName: String?, @RequestParam(required = false) fromUserIcon: String?, @RequestParam(required = false) toCommentId: Long? ): JsonResult { if (type < 0 || type > 2) return JsonResult.error("参数错误") var contentText = content //文字及图片评论内容限制 if (type == 0 || type == 2) { if (StringUtils.isEmpty(contentText)) return JsonResult.error("内容不能为空") if (contentText!!.length > 1023) return JsonResult.error("内容不能超过1000个字") } val app = commentAppRepository.findByNameAndAccessKey(appName, accessKey) ?: return JsonResult.error("app不存在,或key错误") //图片评论上传至OSS if (type == 2) { val file = storageService.loadTemp(contentText).toFile() if (!file.exists()) { return JsonResult.error("相片不存在") } contentText = ossService.putFile(OssService.COMMENT_BUCKET_NAME, appName, file) ?: return JsonResult.error("评论失败,请重试") } //处理回复 var toComment: Comment? = null if (toCommentId != null && toCommentId > 0) { toComment = commentRepository.findById(toCommentId).orElse(null) ?: return JsonResult.error("被回复的评论不存在") if (toComment.topicId != topicId) return JsonResult.error("不能跨主题回复") if (toComment.type == 1) return JsonResult.error("不能对点赞回复") //对评论点赞,直接对点赞数+1,不保存此条评论 if (type == 1) { toComment.likeCount++ return JsonResult.data(commentRepository.save(toComment)) } } //需要新增的comment val comment = Comment( topicId = topicId, app = app, content = contentText, type = type, fromUserId = fromUserId, fromUserName = fromUserName, fromUserIcon = fromUserIcon, toUserId = toComment?.fromUserId, toUserName = toComment?.fromUserName, toUserIcon = toComment?.fromUserIcon, toContent = toComment?.content) log.info("$fromUserName 评论了 ${app.name} 的 $topicId") return JsonResult.data(commentRepository.save(comment)) } ``` ### 2.获取评论列表 ```kotlin @ApiOperation(value = "获取评论列表") @RequestMapping(path = ["/getComments"], method = [RequestMethod.GET]) fun getComments(@RequestParam accessKey: String, @RequestParam appName: String, @RequestParam topicId: String): JsonResult> { commentAppRepository.findByNameAndAccessKey(appName, accessKey) ?: return JsonResult.error("app不存在,或key错误") return JsonResult.data(commentRepository.findAllByApp_NameAndTopicIdAndDeleted(appName, topicId)) } ``` 最终效果即为目前页面下方的评论功能。 以上,转载请注明出处! --- ## 使用TravisCI进行持续集成 > 原文: https://wycode.cn/blog/travis-ci | Agent 文档: https://wycode.cn/blog/travis-ci.md | 日期: 2019-05-20 | 分类: DevOps ![TravisCI](https://www.smartfile.com/blog/wp-content/uploads/2015/05/logo-travisci-wide1.jpg) > 持续集成(Continuous integration,简称CI)是一种软件开发实践。以每天小的代码改动集成替代在开发周期的末尾集成大量的代码,通过自动化的构建(包括编译,自动化测试,发布)来验证,从而尽快地发现和定位错误。让产品可以快速迭代,同时还能保持高质量。 > 持续集成往往和现代化的软件开发实践相结合,例如:Git工作流、敏捷开发、自动化测试、自动化部署等。 > 持续集成能够帮你:减少风险、减少重复过程、自动化生成可部署的软件、增强项目的可见性、建立团队对开发产品的信心。 这篇文章教你使用 [Travis CI](https://travis-ci.com "Travis CI") 自动化构建和发布你的软件产品。 ## 准备 Travis CI可以使用Github的OAuth登录,赋予其必要的权限,我们就能使用Github开源代码库开启构建了。 > 需要注意的是:Travis CI目前有两个域名`travis-ci.org`和`travis-ci.com`,推荐使用`com`,官方已经将`.org`合并到`.com`了。 ## 配置 Travis CI通过读取项目根目录的`.travis.yml`文件获取配置信息,所以需要在工程目录下配置`.travis.yml` ```yml # safelist branches: only: - master language: node_js node_js: - "8" script: ./node_modules/.bin/hexo clean && ./node_modules/.bin/hexo generate deploy: # deploy to github pages - provider: pages skip_cleanup: true target_branch: master github_token: $GITHUB_TOKEN # Set in the settings page of your repository, as a secure variable repo: wangyucode/wangyucode.github.io local_dir: public after_deploy: - node deploy/sftp.js $SERVER_PASSWORD ``` - `branches`可以配置需要构建的黑白名单,我这里设置了白名单,只构建`master`分支 - 语言环境选择`nodejs` - 一个Job包含两个主要部分:`install`和`script` - 我这里指定了node版本为v8 - `install`默认会执行`npm install`,在有`yarn.lock`的工程中会替代`npm`为`yarn` - `script`会在`install`完成后执行 - `deploy`中我部署了github pages,`skip_cleanup`表示跳过cleanup因为部署时我不需要清理任何文件,`target_branch`指定了要提交的分支,`github_token`因为比较敏感,所以作为变量配置在`Repository Settings`中,`repo`配置了要提交的仓库,`local_dir`表示只会部署`public`中的文件 - `after_deploy`在部署完成之后我还会将文件部署到我的linux服务器,这里用了`sftp-sync-deploy`开源库 ## 部署 push代码后即可触发集成,在Travis Dashboard可以看到如下log,说明构建及部署都成功了! ```bash Worker information Build system information docker stop/waiting resolvconf stop/waiting $ git clone --depth=50 --branch=master https://github.com/wangyucode/wycode.cn.git wangyucode/wycode.cn ... Logged in as @wangyucode (王郁) cd /tmp/d20190520-5677-1oocet1/work Preparing deploy Deploying application $ node deploy/sftp.js $SERVER_PASSWORD Done. Your build exited with 0. ``` 以上,转载请注明出处! --- ## NGINX配置重定向 > 原文: https://wycode.cn/blog/nginx-redirect | Agent 文档: https://wycode.cn/blog/nginx-redirect.md | 日期: 2019-05-17 | 分类: DevOps ![NGINX](https://nginx.org/nginx.png) > 服务器配置Https之后,我们希望同时支持http,但是80端口进来的请求直接重定向到443端口上。 > 有时我们会将某些网页永久迁移到别的地址,但是又不想让老用户直接无法访问,或者看到404,而是要将这些老地址的请求转发到新地址上。 这篇文章教你如何配置NGINX重定向。 NGINX安装及配置请移步这里:[Nginx安装](/2018-06-23-nginx.html "NGINX安装") ## 使用rewrite ``` server { #... rewrite (.*)$ https://wycode.cn$1 permanent; #... } ``` 这里将这个server的所有请求,永久定向到 https://wycode.cn 上,并将请求的path拼接在后面 ## 使用return 301 ``` server { listen 443 ssl; listen [::]:443 ssl; ssl_certificate /etc/nginx/cert/wycode.pem; ssl_certificate_key /etc/nginx/cert/wycode.key; server_name wycode.cn www.wycode.cn; error_page 404 = https://wycode.cn/404.html; location / { root /var/www/wycode.cn; index index.html; try_files $uri $uri/ =404; } } server { listen 80; listen [::]:80; server_name wycode.cn www.wycode.cn; return 301 https://$host$request_uri; } ``` 这里的第二个`server`监听80端口,在下面的`return 301`语句中将请求转发到第一个`server`中去,并且`host`和`request_uri`不变。 以上,转载请注明出处! --- ## 使用Flutter开发跨平台应用 > 原文: https://wycode.cn/blog/flutter | Agent 文档: https://wycode.cn/blog/flutter.md | 日期: 2019-03-28 | 分类: Mobile ![Flutter](https://blog.scottlogic.com/bquinn/assets/Flutter_logo_text.png) > Flutter是由Google开发的开源的跨平台应用开发框架,支持Android、iOS开发,能够开发媲美原生的高性能应用。按照2019年的开发路线图,Flutter即将支持Web和桌面端应用的开发。这篇文章带你领略Flutter的魅力。 [Flutter官网](https://flutter.dev/ "Flutter官网") ## 已经有ReactNative了,为什么还要Flutter ReactNative已经能够做到使用一份JavaScript代码同时发布Android和iOS平台。而且其并不使用WebView,而是将React组件转换为相应的原生组件从而提高性能。 ReactNative技术看起来很美好,因为对公司而言,并不需要很多的Android和iOS开发,只需要一两个技术支持,其它业务代码都是由前端工程师完成,节约了成本(?)提高了效率(?)。 然而,最近越来越多的公司开始放弃ReactNative而回归到原生开发,原因在于,当ReactNative实际应用到大型项目上时,面对复杂的、定制化的需求,其自带封装好的组件往往不能满足要求。为了满足这些需求,往往需要原生开发去定制相应的React组件。或者为JS提供接口。原生很简单就能实现的功能,因为要跨平台,所以必须抽象出来给React使用。此时React带来的收益和成本相比不再显得有优势了。 Flutter的优势在于,它完全抛弃了使用原生UI,而是从底层2D渲染就独立出来。使用了Google开发的叫`Skia`的C++渲染库。类似于游戏引擎一样,能够提供60FPS的性能。上层使用`Dart`提供了一套现代的响应式布局框架。抛弃了原生组件,同时也就免去了原生组件的定制。定制UI也是在Flutter框架的范围内,不涉及原生开发。 这就是Flutter的魅力所在。 ## Flutter技术架构 ![Flutter技术架构](https://docs.flutter.dev/assets/images/docs/arch-overview/archdiagram.png) ## 关于环境搭建 环境搭建官网都有说明,我这里不再赘述,需要注意的是,下载并配置好Flutter之后,要执行一个`flutter doctor`的命令,这个命令会检查本地环境,在线安装所需要的组件。由于众所周知的原因,国内使用我们需要额外配置两个类似仓库镜像的环境变量: ```bash $ export PUB_HOSTED_URL=https://pub.flutter-io.cn $ export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn ``` 然后再执行`flutter doctor`就能快一点了。 ## Hello Flutter 创建好第一个Flutter工程后,模版代码注释非常详细,Android部分看到只有一个Activity继承自FlutterActivity,用代理模式,接管了这个Activity的所有生命周期和事件回调。这些和Unity,ReactNative的原理都一样。以下是`Dart`部分的代码: ```dart import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked "final". final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { int _counter = 0; void _incrementCounter() { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed // _counter without calling setState(), then the build method would not be // called again, and so nothing would appear to happen. _counter++; }); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Column( // Column is also layout widget. It takes a list of children and // arranges them vertically. By default, it sizes itself to fit its // children horizontally, and tries to be as tall as its parent. // // Invoke "debug painting" (press "p" in the console, choose the // "Toggle Debug Paint" action from the Flutter Inspector in Android // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) // to see the wireframe for each widget. // // Column has various properties to control how it sizes itself and // how it positions its children. Here we use mainAxisAlignment to // center the children vertically; the main axis here is the vertical // axis because Columns are vertical (the cross axis would be // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.display1, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } ``` --- ## 使用Netty和阿里云函数写一个低成本的“梯子” > 原文: https://wycode.cn/blog/netty-function | Agent 文档: https://wycode.cn/blog/netty-function.md | 日期: 2018-10-23 | 分类: Backend ![流程](/images/20181023_liucheng.jpg "流程") > 函数计算(Function Compute)是事件驱动的全托管计算服务。函数计算无需租用和管理24小时运行的服务器(Serverless),无需搭建复杂的服务器环境,只需编写代码并上传就可以了,只需要为代码实际运行消耗的资源付费,非常便宜。 > 函数计算可以选择运行区域,也就是说,我们可以将函数运行的服务器区域选择到香港或者是美国,如此会带来什么样的好处,只可意会,不可言传。那么能否利用云函数写一个Proxy软件呢?请看本文! ## 在阿里云上新建函数计算 我们需要建立一个云函数,和一个HTTP触发器。 此过程请参考阿里云官网,我就不详细赘述。[官网在此](https://www.aliyun.com/product/fc "官网在此") ## Netty服务端Handle代码 ```kotlin package cn.wycode.lambda.proxy.inbound import cn.wycode.lambda.proxy.config.AliyunConfig import cn.wycode.lambda.proxy.outbound.ProxyOutboundInitializer import io.netty.bootstrap.Bootstrap import io.netty.buffer.ByteBuf import io.netty.buffer.Unpooled import io.netty.channel.* import io.netty.handler.codec.http.* import io.netty.util.CharsetUtil class ProxyInboundHandler(val aliyunConfig: AliyunConfig) : SimpleChannelInboundHandler() { // As we use inboundChannel.eventLoop() when building the Bootstrap this does not need to be volatile as // the outboundChannel will use the same EventLoop (and therefore Thread) as the inboundChannel. private var outboundChannel: Channel? = null override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) { println("ProxyInboundHandler<<<" + msg.toString(CharsetUtil.UTF_8)) // val headers = msg.headers() // val headerMap = HashMap(headers.size()) // headers.forEach { headerMap[it.key] = it.value } // val outboundBody = Request(msg.method().name(), msg.uri(), msg.protocolVersion().text(), headerMap) // val outboundJson = JSON.toJSONString(outboundBody) val request = DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, aliyunConfig.path) request.headers().set(HttpHeaderNames.HOST, aliyunConfig.host) request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE) request.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) request.headers().set(HttpHeaderNames.CONTENT_LENGTH, msg.writerIndex()) // request.content().writeCharSequence(outboundJson, CharsetUtil.UTF_8) request.content().writeBytes(msg) // if (outboundChannel!!.isActive) { println("ProxyInboundHandler>>>${request.content().toString(CharsetUtil.UTF_8)}") outboundChannel!!.writeAndFlush(request).addListener(object : ChannelFutureListener { override fun operationComplete(future: ChannelFuture) { if (future.isSuccess) { // was able to flush out data, start to read the next chunk ctx.channel().read() } else { future.channel().close() } } }) } } override fun channelActive(ctx: ChannelHandlerContext) { val inboundChannel = ctx.channel() // Start the connection attempt. val outboundClient = Bootstrap() outboundClient.group(inboundChannel.eventLoop()) .channel(inboundChannel.javaClass) .handler(ProxyOutboundInitializer(inboundChannel)) .option(ChannelOption.AUTO_READ, false) val f = outboundClient.connect(aliyunConfig.host, aliyunConfig.port) outboundChannel = f.channel() f.addListener { future -> if (future.isSuccess) { // connection complete start to read first data inboundChannel.read() } else { // Close the connection if the connection attempt has failed. inboundChannel.close() } } } override fun channelInactive(ctx: ChannelHandlerContext) { if (outboundChannel != null) { closeOnFlush(outboundChannel!!) } } override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { cause.printStackTrace() closeOnFlush(ctx.channel()) } companion object { fun closeOnFlush(ch: Channel) { if (ch.isActive) { ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE) } } } } ``` ## Netty客户端Handle代码 ```kotlin package cn.wycode.lambda.proxy.outbound import cn.wycode.lambda.proxy.inbound.ProxyInboundHandler import io.netty.channel.* import io.netty.handler.codec.http.FullHttpResponse import io.netty.util.CharsetUtil class ProxyOutboundHandler(private val inboundChannel: Channel) : SimpleChannelInboundHandler() { override fun channelActive(ctx: ChannelHandlerContext) { println("ProxyOutBoundHandle==") ctx.read() } override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpResponse) { println("ProxyOutBoundHandle<<<$msg") val body = msg.content().toString(CharsetUtil.UTF_8) println("ProxyOutBoundHandle body<<<$body") // val proxyResponse = try { // JSON.parseObject(body, Response::class.java) // } catch (e: Exception) { // e.printStackTrace() // msg.retain() // inboundChannel.writeAndFlush(msg).addListener(object : ChannelFutureListener { // override fun operationComplete(future: ChannelFuture) { // if (future.isSuccess) { // // was able to flush out data, start to read the next chunk // ctx.channel().read() // } else { // future.channel().close() // } // } // }) // return // } // val headers = CombinedHttpHeaders(true) // var outBytes:ByteArray? = null // if (proxyResponse.error == null) { // for (header in proxyResponse.headers!!) { // headers[header.key] = header.value // } // outBytes = Base64.getDecoder().decode(proxyResponse.content) // }else{ // outBytes = proxyResponse.error.toByteArray() // } // val response = DefaultFullHttpResponse(HttpVersion.HTTP_1_1, // HttpResponseStatus.valueOf(proxyResponse.code), // Unpooled.wrappedBuffer(outBytes), // headers, DefaultHttpHeaders()) msg.retain() inboundChannel.writeAndFlush(msg.content()).addListener(object : ChannelFutureListener { override fun operationComplete(future: ChannelFuture) { if (future.isSuccess) { // was able to flush out data, start to read the next chunk ctx.channel().read() } else { future.channel().close() } } }) } override fun channelInactive(ctx: ChannelHandlerContext) { println("ProxyOutBoundHandle!=") ProxyInboundHandler.closeOnFlush(inboundChannel) } override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { cause.printStackTrace() ProxyInboundHandler.closeOnFlush(ctx.channel()) } } ``` ## 云函数代码 ```java package cn.wycode.lambda; import com.aliyun.fc.runtime.Context; import com.aliyun.fc.runtime.FunctionComputeLogger; import com.aliyun.fc.runtime.StreamRequestHandler; import java.io.*; import java.net.Socket; import java.net.URL; import java.nio.charset.StandardCharsets; public class Lambda implements StreamRequestHandler { @Override public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { FunctionComputeLogger logger = context.getLogger(); StringBuilder sb = new StringBuilder(); byte[] buffer = new byte[1024]; int n; while ((n = input.read(buffer)) != -1) { sb.append(new String(buffer, 0, n, StandardCharsets.UTF_8)); } logger.info("handleRequest>>>" + sb.toString()); String firstLine = new BufferedReader(new StringReader(sb.toString())).readLine(); String host; int port; try { if (firstLine.startsWith("CONNECT ")) { String[] urlArray = firstLine.split(" ")[1].split(":"); host = urlArray[0]; port = Integer.parseInt(urlArray[1]); } else { URL url = new URL(firstLine.split(" ")[1]); host = url.getHost(); port = url.getPort(); if (port == -1) { if (url.getProtocol().equalsIgnoreCase("http")) { port = 80; } else if (url.getProtocol().equalsIgnoreCase("https")) { port = 443; } else { throw new IllegalArgumentException("端口无法解析"); } } } } catch (Exception e) { logger.error("url解析错误:" + e.getMessage()); return; } logger.info("host->" + host); logger.info("port->" + port); Socket s = new Socket(host, port); if (firstLine.startsWith("CONNECT ")) { String response = "HTTP/1.1 200 Connection Established"; output.write(response.getBytes(StandardCharsets.UTF_8)); output.flush(); sb.setLength(0); while ((n = input.read(buffer)) != -1) { sb.append(new String(buffer, 0, n, StandardCharsets.UTF_8)); } logger.info("handleRequest>>>" + sb.toString()); } OutputStream httpOutputStream = s.getOutputStream(); httpOutputStream.write(sb.toString().getBytes(StandardCharsets.UTF_8)); InputStream httpInputStream = s.getInputStream(); sb.setLength(0); while ((n = httpInputStream.read(buffer)) != -1) { output.write(buffer, 0, n); sb.append(new String(buffer, 0, n, StandardCharsets.UTF_8)); } httpInputStream.close(); httpOutputStream.close(); s.close(); input.close(); output.close(); logger.info("handleRequest<<<" + sb.toString()); } } ``` ## 遗留问题 - HTTP请求代理测试通过 - 可以在Netty客户端发送请求时,对请求体进行二进制加密,函数端解密;函数端收到真实响应加密,Netty客户端收到请求后解密。从而提高安全性 - 对于HTTP请求头为`Connection:keep-alive`的请求,函数端在读取真实响应时,会阻塞在流末尾的read方法而无法返回结果,`Connection:close`的请求正常 - HTTPS请求在有代理的情况下,浏览器会先发送一个HTTP CONNECT请求,以建立TSL通道。目前因为read阻塞问题还没有解决。 附:本项目全部源码地址: Netty端:[https://github.com/wangyucode/lambda-cross-java](https://github.com/wangyucode/lambda-cross-java "github") 函数端:[https://github.com/wangyucode/aliyun-lambda](https://github.com/wangyucode/aliyun-lambda "github") 以上,转载请联系作者! --- ## 使用Netty开发高性能的网络服务应用 > 原文: https://wycode.cn/blog/netty | Agent 文档: https://wycode.cn/blog/netty.md | 日期: 2018-10-16 | 分类: Backend ![Netty](https://issues.jboss.org/secure/attachment/12345694/netty_logo_600px.png) > Netty是一个基于异步NIO(non-blocking IO)模型的,事件驱动的网络应用程序框架。 > 不同于传统阻塞IO,非阻塞IO通常使用更少的线程,从而提高并发性能。 > Netty的灵活设计使得它能够开发几乎所有基于二进制流、文本应用协议的Web应用 这篇文章将简单介绍如何使用Netty开发一个HTTP服务器 ## 导入Netty 这里使用Gradle导入4.1.30.Final版本 ```json dependencies { compile "io.netty:netty-all:4.1.30.Final" } ``` ## 编写服务器 ```kotlin class Server(val port: Int) { @Throws(Exception::class) fun run() { val bossGroup = NioEventLoopGroup(1) val workerGroup = NioEventLoopGroup() try { val b = ServerBootstrap() b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel::class.java) .handler(LoggingHandler(LogLevel.INFO)) .childHandler(ServerInitializer()) val ch = b.bind(port).sync().channel() println("server start on $port") ch.closeFuture().sync() } finally { bossGroup.shutdownGracefully() workerGroup.shutdownGracefully() } } } ``` - 这里`bossGroup`是用来接受连接的父线程池,可以是单线程,也可以是多线程(推荐是CPU核心数的倍数) - 因为`bossGroup`接受连接后会立即返回,不会阻塞,所以即使单线程也能够处理并发(类似Node.JS) - `workerGroup`是工作线程 - `channel()`设置构建NIO Channel的类型 - `handle()`和`childHandle()`分别设置主处理,和子处理对象 ## 配置子处理流程 ```kotlin class ServerInitializer : ChannelInitializer() { override fun initChannel(ch: SocketChannel) { val p = ch.pipeline() p.addLast(HttpRequestDecoder()) // Uncomment the following line if you don't want to handle HttpChunks. //p.addLast(new HttpObjectAggregator(1048576)); p.addLast(HttpResponseEncoder()) // Remove the following line if you don't want automatic content compression. //p.addLast(new HttpContentCompressor()); p.addLast(HttpServerHandler()) } } ``` - Netty自带了很多的编解码器,上面的`HttpRequestDecoder`和`HttpResponseEncoder`用于HTTP协议的编解码 - 经过编解码后我们的`HttpServerHandler`将会实际处理HTTP请求 ## 处理HTTP请求 ```kotlin class HttpServerHandler : SimpleChannelInboundHandler() { val sb = StringBuilder() lateinit var request: HttpRequest override fun channelRead0(ctx: ChannelHandlerContext, msg: Any) { if (msg is HttpRequest) { if (HttpUtil.is100ContinueExpected(msg)) { ctx.write(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)) } request = msg sb.setLength(0) sb.append("Welcome\r\n") sb.append("=====================\r\n") sb.append("VERSION: ").append(msg.protocolVersion()).append("\r\n") sb.append("HOSTNAME: ").append(msg.headers().get(HttpHeaderNames.HOST, "unknown")).append("\r\n") sb.append("REQUEST_URI: ").append(msg.uri()).append("\r\n\r\n") val headers = msg.headers() if (!headers.isEmpty) { for (h in headers) { sb.append("HEADER: ").append(h.key).append(" = ").append(h.value).append("\r\n") } sb.append("\r\n") } val queryStringDecoder = QueryStringDecoder(msg.uri()) val params = queryStringDecoder.parameters() if (!params.isEmpty()) { for (p in params) { val vals = p.value for (value in vals) { sb.append("PARAM: ").append(p.key).append(" = ").append(value).append("\r\n") } } sb.append("\r\n") } appendDecoderResult(sb, msg) } if (msg is HttpContent) { val content = msg.content() if (content.isReadable) { sb.append("CONTENT: ") sb.append(content.toString(CharsetUtil.UTF_8)) sb.append("\r\n") appendDecoderResult(sb, msg) } if (msg is LastHttpContent) { sb.append("END OF CONTENT\r\n") if (!msg.trailingHeaders().isEmpty) { sb.append("\r\n") for (name in msg.trailingHeaders().names()) { for (value in msg.trailingHeaders().getAll(name)) { sb.append("TRAILING HEADER: ") sb.append(name).append(" = ").append(value).append("\r\n") } } sb.append("\r\n") } // Decide whether to close the connection or not. if (writeResponse(msg, ctx)) { // If keep-alive is off, close the connection once the content is fully written. ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE) } } } } override fun channelReadComplete(ctx: ChannelHandlerContext) { ctx.flush() } private fun writeResponse(currentObj: HttpObject, ctx: ChannelHandlerContext): Boolean { val keepAlive = HttpUtil.isKeepAlive(request) // Build the response object. val status = if (currentObj.decoderResult().isSuccess) { HttpResponseStatus.OK } else { HttpResponseStatus.BAD_REQUEST } val response = DefaultFullHttpResponse( HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(sb.toString(), CharsetUtil.UTF_8)) response.headers().set(HttpHeaderNames.CONTENT_TYPE, "${HttpHeaderValues.TEXT_PLAIN}; charset=UTF-8") if (keepAlive) { // Add 'Content-Length' header only for a keep-alive connection. response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()) // Add keep alive header as per: // - http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE) } // Encode the cookie. val cookieString = request.headers().get(HttpHeaderNames.COOKIE) if (cookieString != null) { val cookies = ServerCookieDecoder.STRICT.decode(cookieString) if (!cookies.isEmpty()) { // Reset the cookies if necessary. for (cookie in cookies) { response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)) } } } else { // Browser sent no cookie. Add some. response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode("key1", "value1")); response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode("key2", "value2")); } // Write the response. ctx.write(response) return keepAlive } override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { cause.printStackTrace() ctx.close() } companion object { fun appendDecoderResult(sb: StringBuilder, httpObj: HttpObject) { val result = httpObj.decoderResult() if (result.isSuccess) { return } sb.append(".. WITH DECODER FAILURE: ") sb.append(result.cause()) sb.append("\r\n") } } } ``` - 如果构造流程时没有使用`HttpObjectAggregator`类,`channelRead()`会调用很多次,每次传入的对象为`HttpRequest`,`HttpContent`,`LastHttpContent`的子类,他们分别有方法可以读取到HTTP请求的内容。 以上,转载请联系作者! --- ## 理解Spring WebFlux框架中的响应式类型 > 原文: https://wycode.cn/blog/web-flux | Agent 文档: https://wycode.cn/blog/web-flux.md | 日期: 2018-07-20 | 分类: Backend ![Spring Boot](/images/20180720_diagram-boot-reactor.svg) > `Spring Boot 2.0` 带来了`Spring Framework 5` 中的新的WebFlux技术,WebFlux是基于响应式非阻塞IO编程模型的技术栈。从而能够使用不同于以往基于Servlet API的阻塞式IO处理的新方式编写后台应用。 > 相比以往阻塞式IO编程,响应式编程能够获得更小的开销。因为它直接处理流,而不是单个数据。 > 响应式编程最早由 `.NET` 平台上的 `Reactive Extensions (Rx)` 库来实现。后来迁移到 Java 平台之后就产生了著名的 `RxJava` 库,并产生了很多其他编程语言上的对应实现。在这些实现的基础上产生了后来的响应式流(`Reactive Streams`)规范。该规范定义了响应式流的相关接口,并将集成到 Java 9 中。 ## 为什么要用响应式 在传统的编程范式中,我们一般通过迭代器(`Iterator`)模式来遍历一个序列。这种遍历方式是由调用者来控制节奏的,采用的是拉的方式。每次由调用者通过 `next()`方法来获取序列中的下一个值。使用响应式流时采用的则是推的方式,即常见的发布者-订阅者模式。当发布者有新的数据产生时,这些数据会被推送到订阅者来进行处理。在响应式流上可以添加各种不同的操作来对数据进行处理,形成数据处理链。这个以声明式的方式添加的处理链只在订阅者进行订阅操作时才会真正执行。 响应类型不是为了让你更快地处理请求或数据,实际上与常规阻塞处理相比,它们会带来更小的开销。 它们的优势在于它们能够同时提供更多请求,以及处理延迟操作,例如更有效地从远程服务器请求数据,或者处理来自客户端的请求。 它们允许你通过本地处理时间和延迟来提供更好的服务质量和可预测的容量规划,而无需消耗更多资源。与等待结果时阻止当前线程的传统处理不同,`Reactive` API没有等待成本,仅请求它能够处理的数据量并提供立刻返回,因为它处理数据流,而不仅仅是单个元素。 ## 为什么使用`Reactor`而不是`RxJava` `RxJava` 库是 JVM 上响应式编程的先驱,也是响应式流规范的基础。`RxJava 2` 在 `RxJava` 的基础上做了很多的更新。不过 `RxJava` 库也有其不足的地方。`RxJava` 产生于响应式流规范之前,虽然可以和响应式流的接口进行转换,但是由于底层实现的原因,使用起来并不是很直观。`RxJava 2` 在设计和实现时考虑到了与规范的整合,不过为了保持与 `RxJava` 的兼容性,很多地方在使用时也并不直观。`Reactor` 则是完全基于响应式流规范设计和实现的库,没有 `RxJava` 那样的历史包袱,在使用上更加的直观易懂。`Reactor` 也是 `Spring 5` 中响应式编程的基础。学习和掌握 `Reactor` 可以更好地理解 `Spring 5` 中的相关概念。 ## Reactive Streams `Reactor`建立在`Reactive Streams`规范之上。 `Reactive Streams`由4个简单的Java接口(`Publisher`,`Subscriber`,`Subscription`和`Processor`),文本规范和`TCK`组成。它是每个现代Reactive库的基石,也是互操作性的必备条件。 响应式流中第一个重要概念是背压(backpressure)。在基本的消息推送模式中,当消息发布者产生数据的速度过快时,会使得消息订阅者的处理速度无法跟上产生的速度,从而给订阅者造成很大的压力。当压力过大时,有可能造成订阅者本身的奔溃,所产生的级联效应甚至可能造成整个系统的瘫痪。背压的作用在于提供一种从订阅者到生产者的反馈渠道。订阅者可以通过 request()方法来声明其一次所能处理的消息数量,而生产者就只会产生相应数量的消息,直到下一次 request()方法调用。这实际上变成了推拉结合的模式。 例如,当HTTP连接速度太慢时,在从数据库扩展到HTTP响应的被动组件的管道中,数据存储库也可以减速或完全停止,直到网络容量释放为止。 > Java 9在`java.util.concurrent.Flow`容器类中包含了`Reactive Streams`接口 ## `Flux` 和 `Mono` `Flux` 和 `Mono` 是 Reactor 中的两个基本概念。 `Flux` 表示的是包含 0 到 N 个元素的异步序列。在该序列中可以包含三种不同类型的消息通知:正常的包含元素的消息、序列结束的消息和序列出错的消息。当消息通知产生时,订阅者中对应的方法 `onNext()`, `onComplete()`和 `onError()`会被调用。 `Mono` 表示的是包含 0 或者 1 个元素的异步序列。该序列中同样可以包含与 Flux 相同的三种类型的消息通知。Flux 和 Mono 之间可以进行转换。对一个 Flux 序列进行计数操作,得到的结果是一个 Mono对象。把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。 参考文献: 1.[Spring官方WebFlux文档](https://docs.spring.io/spring-framework/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/web-reactive.html "Spring官方WebFlux文档") 2.[Understanding Reactive types](https://spring.io/blog/2016/04/19/understanding-reactive-types "Understanding Reactive types") 3.[使用 Reactor 进行反应式编程](https://www.ibm.com/developerworks/cn/java/j-cn-with-reactor-response-encode/index.html "使用 Reactor 进行反应式编程") --- ## Ubuntu16.04安装配置NGINX服务 > 原文: https://wycode.cn/blog/nginx | Agent 文档: https://wycode.cn/blog/nginx.md | 日期: 2018-06-23 | 分类: DevOps ![NGINX](https://nginx.org/nginx.png) 当我们需要在Ubuntu上部署SpringBoot服务时,通常正式环境需要将监听端口切换到80端口,或443端口,但是Ubuntu为了安全起见,普通用户是不能监听1024以下端口的。另外我们需要同时支持HTTP和HTTPS,貌似SpringBoot只支持监听1个端口,这时有很多解决方案。比如使用`sudo`提升运行权限,也可以通过Linux防火墙iptables配置端口转发,把80端口转发到8080端口上等方式。 但是...这都不是最优的解决方案,既然用到了端口转发,不如直接安装一个NGINX服务器,做反向代理,同时也能提供静态服务能力,未来还能简单地配置升级到集群,做负载均衡。 > NGINX是一款高性能Web服务器。配置简单,易扩展,并发能力强,可用于Web服务器,反向代理,负载均衡,邮件等。 ## 安装NGINX ```bash $sudo apt install nginx ``` 使用这一行命令,其实就可以了。 这时直接访问我们的服务器ip,能看到NGINX的欢迎页面,就说明安装成功。 ## 配置nginx `/etc/nginx/`目录下有这么几个重要文件 - nginx.conf 是NGINX的配置文件,里面有一些基本配置,网上很多教程是直接修改这个文件配置服务什么的,看这个文件结构include了conf.d目录的所有`.conf`文件,我们自定义的配置应该写在conf.d目录中 - site-enabled 是当前启用的服务配置文件,里面有个`default`链接到`site-available`目录里。 - site-available 是备选的服务器配置文件 从注释和目录结构,我们可以看到其良苦用心,不应该直接修改nginx.conf文件,而是将需要添加的服务器配置文件放到`site-available`目录中。然后在`site-enabled`目录创建要启用服务的`symbolic link`到刚刚创建的配置文件。这样配置才能更方便,更具扩展性。 ## 示例 我这里配置了 静态网站服务: ``` server { listen 80; listen [::]:80; server_name wycode.cn www.wycode.cn; root /var/www/wycode.cn; index index.html; location / { try_files $uri $uri/ =404; } } ``` SpringBoot 后台api反向代理 ``` server { listen 80; listen [::]:80; server_name wycode.cn www.wycode.cn; location /api { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` 以上,转载请联系作者! --- ## SpringBoot配置Jar外部静态资源 > 原文: https://wycode.cn/blog/springboot-static-content | Agent 文档: https://wycode.cn/blog/springboot-static-content.md | 日期: 2018-06-23 | 分类: Backend ![springboot](/images/20180623_springboot.jpg) > 我们知道SpringBoot内置了tomcat服务器,从而一个jar包就能实现web服务。但有时我们又想像传统tomcat那样部署一些外部的静态资源,虽然静态资源放在`resource`文件夹的`static`就能解决问题,但是static文件夹的内容会作为jar的一部分,要替换的话只能替换整个jar包,极为不便。能不能配置jar包外部的文件夹作为静态资源目录呢。当然是可以的... 以下示例代码基于SpringBoot 2.0.3,采用Kotlin语言实现: ## WebMvcConfigurer > 新版Spring利用了Java8新特性,interface可以有default关键字,从而接口方法不用全部实现。 ```kotlin @Configuration class WebConfiguration : WebMvcConfigurer { override fun addResourceHandlers(registry: ResourceHandlerRegistry) { super.addResourceHandlers(registry) registry.addResourceHandler("/**") .addResourceLocations("file:static/") } } ``` - `@Configuration`注解表明这是一个配置类,会被Spring框架识别并管理。 - `WebConfiguration`类实现`WebMvcConfigurer`接口,并复写`addResourceHandlers`方法。 - `addResourceHandler("/**")`添加路由到服务器根目录。 - `.addResourceLocations("file:static/")`添加外部静态资源,这里路径表示和jar包同目录的`static`文件夹。这样能想tomcat一样在static文件夹中部署静态资源了。而且不用重启SpringBoot服务 --- ## Spring JPA框架中的@ManyToMany多对多关系的理解和使用 > 原文: https://wycode.cn/blog/many-to-many | Agent 文档: https://wycode.cn/blog/many-to-many.md | 日期: 2018-04-19 | 分类: Backend ![Spring JPA](https://tse3-mm.cn.bing.net/th/id/OIP-C.BJ09SzSPlUf5QzF_EryfdwAAAA?pid=ImgDet&rs=1) > JPA是Java Persistence API的简称是Java的ORM规范,实现这个规范的有著名的Hibernate等ORM框架。JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。 多对多关系是关联关系中最复杂的。本文使用Spring JPA框架总结多对多关系及`@ManyToMany`注解的详细使用。明白了`@ManyToMany`,其它 `@OneToMany` 和`@OneToOne`就简单很多了。 ## Java和JPA中的多对多关系 > 根据维基百科定义:Java中的多对多关系是指源对象具有目标对象集合的属性,并且(如果存在的话)这个目标对象与原对象具有反向关系,则它也将成为多对多关系。需要注意的是:Java和JPA中的所有关系都是单向的,因为存在源对象引用目标对象的情况下,也不能保证目标对象也与原对象有关系。这与关系数据库不同,在关系数据库中,通过外键定义并查询,使得反向查询总是存在的。 > JPA还定义了一个OneToMany关系,它与ManyToMany关系类似,但反向关系(如果已定义)是ManyToOne关系。 OneToMany与JPA中ManyToMany关系的主要区别在于,ManyToMany总是使用中间关系连接表来存储关系,OneToMany可以使用连接表或者目标对象的表引用中的外键源对象表的主键。 传统项目开发,会有一个设计表的过程,然后由DBA写一大堆`create table ***`。很麻烦也很慢。 JPA框架在运行时,会根据实体定义自动创建表,自动更新表字段,使用JPA可以说一行SQL都不用写,甚至不用关心底层数据库到底是`MySQL`还是`Oracle`还是其它。以下例子中我不定义表名,列名,所以没有`@Column`、`@Table`、`@JoinTable`、`@JoinColumn`等。由JPA框架自动根据实体名称生成表名,列名。我觉得Hibernate生成的表名和列名挺好也符合规范。就不多此一举了。 ## @ManyToMany 注解 在JPA中使用`@ManyToMany`注解表示多对多关系。 学生和老师,典型的多对多关系: ```java @Entity public class Student { @Id private String id; @ManyToMany private List teachers; } @Entity public class Teacher { @Id private String id; @ManyToMany private List students; } ``` 上面例子中的`@ManyToMany`即表示了老师和学生的多对多关系。`@ManyToMany`注解可以用在字段或方法上。 但这样做是有问题的,生成的中间表有两个,`TEACHER_STUDENTS`表和`STUDENT_TEACHERS`。原因是这里的多对多关系正是维基百科提到的单向原则。老师和学生虽然分别定义了自己的多对多关系,但这个关系都是单向的,要使关系成为双向。就需要下面的`mappedBy`属性。 ## mappedBy 属性 `mappedBy`是OneToOne、OneToMany和ManyToMany这三种关联关系的属性。 用来标注**拥有**这种关系的字段。 除非关系是单向的,否则是必需的。 那么什么叫拥有关联关系呢,假设是双向一对一的话,那么拥有关系的这一方有建立、解除和更新与另一方关系的能力。而另一方没有,只能被动管理。 由于`JoinTable`和`JoinColumn`一般定义在拥有关系的这一端,而`mappedBy`一定是定义在关系的被拥有方(the owned side),也就是跟定义`JoinTable`和`JoinColumn`互斥的一方,它的值指向拥有方中关于被拥有方的字段,可能是一个对象(`OneToMany`),也可能是一个对象集合(`ManyToMany`)。 上面的例子中,维护老师和学生的多对多关系,明显只需要一张中间表就可以了,我们假设老师是维护关系(拥有关系)的一端,则实体定义修改如下: ```java @Entity public class Student { @Id private String id; @ManyToMany(mappedBy = "students") private List teachers; } @Entity public class Teacher { @Id private String id; @ManyToMany private List students; } ``` 这时中间表只有一张`TEACHER_STUDENTS`,关联关系也变成了我们想要的双向关系。 ## Cascade 级联关系 实际业务中,我们通常会遇到以下情况: 1. 用户和用户的收货地址是一对多关系,当用户被删除时,这个用户的所有收货地址也应该一并删除。 2. 订单和订单中的商品也是一对多关系,但订单被删除时,订单所关联的商品肯定不能被删除。 此时只要配置正确的级联关系,就能达到想要的效果。 ### 级联关系类型: - CascadeType.REFRESH:级联刷新,当多个用户同时作操作一个实体,为了用户取到的数据是实时的,在用实体中的数据之前就可以调用一下refresh()方法 - CascadeType.REMOVE:级联删除,当调用remove()方法删除Order实体时会先级联删除OrderItem的相关数据 - CascadeType.MERGE:级联更新,当调用了Merge()方法,如果Order中的数据改变了会相应的更新OrderItem中的数据 - CascadeType.ALL:包含以上所有级联属性 - CascadeType.PERSIST:级联保存,当调用了Persist() 方法,会级联保存相应的数据 需要注意的是,`CascadeType.ALL`要谨慎使用,为了达到数据同步,很多人喜欢用`CascadeType.ALL`来实现。但上面订单和商品的例子就不适用。 ## 以上 - 未经同意禁止转载! - 不对的地方欢迎指正! --- ## SwaggerUI GET请求中文乱码问题解决 > 原文: https://wycode.cn/blog/tomcat | Agent 文档: https://wycode.cn/blog/tomcat.md | 日期: 2018-04-11 | 分类: Backend ![Tomcat](https://pic.baike.soso.com/ugc/baikepic2/18363/cut-20181002011705-1203719514_jpg_267_214_8556.jpg/300) > 今天又一次遇到了中文乱码问题。SwaggerUI上直接测试接口时本地LocalHost是OK的,部署到Linux服务器上就乱码了,查来查去很多文章都没说到点子上。既然是用了Spring,Tomcat等,肯定是哪里配置不对,改代码的方式解决是太low了。 ## Tomcat的Server配置 Tomcat`conf`目录下的`server.xml`(Linux系统可能在`/etc/tomcat/`下)定义了服务器的相关配置信息。 根据官方文档,这里的`Server`指的是整个`Catalina servlet`容器。它的属性代表整个servlet容器的特性。 常见的如:服务器端口,根项目路径等。 ## Connector属性 除了配置端口,协议版本,还可以配置`URIEncoding`,默认值是`ISO8859-1`所以不支持中文,我们改成`UTF-8`即可。即: ```xml ``` ## HTTPS 通常在配置了HTTPS的服务器上,我们会把80端口转发到443端口上,此时我们要对443端口的`Connector`也配置`URIEncoding="UTF-8"` 我的问题就出在这。 以上... --- ## 数据结构与算法学习笔记03 > 原文: https://wycode.cn/blog/data-structures03 | Agent 文档: https://wycode.cn/blog/data-structures03.md | 日期: 2018-03-26 | 分类: Life ![浙江大学](https://www.smpg888.com/resource/images/19b3b5c5671f44a4b9f4320dd8fcc734_34.jpg) > 树状结构的基础是二叉树,二叉树即度为2的树。 ## 二叉树的链表结构 ```c typedef struct TNode *Position; typedef Position BinTree; /* 二叉树类型 */ struct TNode{ /* 树结点定义 */ ElementType Data; /* 结点数据 */ BinTree Left; /* 指向左子树 */ BinTree Right; /* 指向右子树 */ }; ``` ## 作业1:树的同构 给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则我们称两棵树是“同构”的。例如图1给出的两棵树就是同构的,因为我们把其中一棵树的结点A、B、G的左右孩子互换后,就得到另外一棵树。而图2就不是同构的。 图1: ![图1](/images/20180326_data_structures03_1.png) 图2: ![图2](/images/20180326_data_structures03_2.png) 现给定两棵树,请你判断它们是否是同构的。 - 输入格式: 输入给出2棵二叉树树的信息。对于每棵树,首先在一行中给出一个非负整数N (≤10),即该树的结点数(此时假设结点从0到N−1编号);随后N行,第i行对应编号第i个结点,给出该结点中存储的1个英文大写字母、其左孩子结点的编号、右孩子结点的编号。如果孩子结点为空,则在相应位置上给出`-`。给出的数据间用一个空格分隔。注意:题目保证每个结点中存储的字母是不同的。 - 输出格式: 如果两棵树是同构的,输出`Yes`,否则输出`No`。 - 输入样例1(对应图1): ```bash 8 A 1 2 B 3 4 C 5 - D - - E 6 - G 7 - F - - H - - 8 G - 4 B 7 6 F - - A 5 1 H - - C 0 - D - - E 2 - ``` - 输出样例1: ```bash Yes ``` - 输入样例2(对应图2): ```bash 8 B 5 7 F - - A 0 3 C 6 - H - - D - - G 4 - E 1 - 8 D 6 - B 5 - E - - H - - C 0 2 G - 3 F - - A 1 4 ``` - 输出样例2: ```bash No ``` - 实现: ```java public class Main { public static void main(String args[]) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); //读第一个树节点 int n_a = 0; n_a = Integer.parseInt(reader.readLine()); String[] inputs_a = new String[n_a]; List nodes_a = new ArrayList<>(n_a); for (int i = 0; i < n_a; i++) { inputs_a[i] = reader.readLine(); } int n_b = 0; n_b = Integer.parseInt(reader.readLine()); String[] inputs_b = new String[n_b]; List nodes_b = new ArrayList<>(n_b); for (int i = 0; i < n_b; i++) { inputs_b[i] = reader.readLine(); } if (n_a != n_b) { System.out.println("No"); return; } addNodes(inputs_a, nodes_a); addNodes(inputs_b, nodes_b); if(inputs_a.length==1&&!Objects.equals(nodes_a.get(0).data,nodes_b.get(0).data)){ System.out.println("No"); return; } for (Node node_a : nodes_a) { for (Node node_b : nodes_b) { if (Objects.equals(node_a.data, node_b.data)) { if (!((Objects.equals(node_a.left, node_b.left) && Objects.equals(node_a.right, node_b.right)) || (Objects.equals(node_a.left, node_b.right) && Objects.equals(node_a.right, node_b.left)))) { System.out.println("No"); return; } } } } System.out.println("Yes"); } private static void addNodes(String[] inputs, List nodes) { for (String input : inputs) { Node n = new Node(); String[] lineArg = input.split(" "); n.data = lineArg[0]; if (!Objects.equals("-", lineArg[1])) { int left_num = Integer.parseInt(lineArg[1]); n.left = inputs[left_num].split(" ")[0]; } if (!Objects.equals("-", lineArg[2])) { int right_num = Integer.parseInt(lineArg[2]); n.right = inputs[right_num].split(" ")[0]; } nodes.add(n); } } static class Node { String data; String left; String right; } } ``` - 总结: 1. 利用二叉树的数据结构可以解决这个问题。 2. 这里我采用了投机取巧的办法,并没有建立树状结构,而是把每个节点进行比较,唯一需要处理的特例就是同唯一父节点值不同的情况。 ## 作业2:List Leaves(25 分) Given a tree, you are supposed to list all the leaves in the order of top down, and left to right. - Input Specification: Each input file contains one test case. For each case, the first line gives a positive integer N (≤10) which is the total number of nodes in the tree -- and hence the nodes are numbered from 0 to N−1. Then N lines follow, each corresponds to a node, and gives the indices of the left and right children of the node. If the child does not exist, a "-" will be put at the position. Any pair of children are separated by a space. - Output Specification: For each test case, print in one line all the leaves' indices in the order of top down, and left to right. There must be exactly one space between any adjacent numbers, and no extra space at the end of the line. - Sample Input: ```bash 8 1 - - - 0 - 2 7 - - - - 5 - 4 6 ``` Sample Output: ```bash 4 1 5 ``` - 实现: ```java public class Main { public static void main(String args[]) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); int sum; sum = Integer.parseInt(reader.readLine()); String[] inputs = new String[sum]; for (int i = 0; i < sum; i++) { inputs[i] = reader.readLine(); } int root_num = findRoot(inputs); StringBuilder sb = new StringBuilder(); Queue nodes = new LinkedList<>(); Node root = getNode(root_num,inputs[root_num]); nodes.add(root); checkTree(nodes, sb, inputs); if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } System.out.println(sb.toString()); } private static Node getNode(int root_num, String input) { Node node = new Node(); node.data = root_num; String[] lineArg = input.split(" "); if (!Objects.equals("-", lineArg[0])) { node.left = Integer.parseInt(lineArg[0]); } if (!Objects.equals("-", lineArg[1])) { node.right = Integer.parseInt(lineArg[1]); } return node; } private static void checkTree(Queue nodes, StringBuilder sb, String[] inputs) { while (!nodes.isEmpty()) { Node node = nodes.poll(); if (node.left<0 && node.right<0) { sb.append(node.data); sb.append(' '); } if (node.left>=0) { Node n_l = getNode(node.left,inputs[node.left]); nodes.add(n_l); } if (node.right>=0) { Node n_r = getNode(node.right,inputs[node.right]); nodes.add(n_r); } } } private static int findRoot(String[] inputs) { int[] link = new int[inputs.length]; for (String input : inputs) { String[] lineArg = input.split(" "); if (!Objects.equals("-", lineArg[0])) { int left_num = Integer.parseInt(lineArg[0]); link[left_num] = 1; } if (!Objects.equals("-", lineArg[1])) { int right_num = Integer.parseInt(lineArg[1]); link[right_num] = 1; } } int i; for (i = 0; i < link.length; i++) { if (link[i] == 0) { break; } } return i; } static class Node{ int data =-1; int left = -1; int right = -1; } } ``` - 总结: 1. 树的遍历最基本的方法是采用递归; 2. 使用堆栈可以免递归遍历树,每个对象有3次访问机会,调整访问时机可以分别实现前序、中序及后序遍历; 3. 采用队列可以层级遍历树,此题采用层级遍历。 ## 作业3:Tree Traversals Again(25 分) An inorder binary tree traversal can be implemented in a non-recursive way with a stack. For example, suppose that when a 6-node binary tree (with the keys numbered from 1 to 6) is traversed, the stack operations are: push(1); push(2); push(3); pop(); pop(); push(4); pop(); pop(); push(5); push(6); pop(); pop(). Then a unique binary tree (shown in Figure 1) can be generated from this sequence of operations. Your task is to give the postorder traversal sequence of this tree. - Figure 1 ![Figure 1](/images/20180327_data_structures03_3.png) - Input Specification: Each input file contains one test case. For each case, the first line contains a positive integer N (≤30) which is the total number of nodes in a tree (and hence the nodes are numbered from 1 to N). Then 2N lines follow, each describes a stack operation in the format: "Push X" where X is the index of the node being pushed onto the stack; or "Pop" meaning to pop one node from the stack. - Output Specification: For each test case, print the postorder traversal sequence of the corresponding tree in one line. A solution is guaranteed to exist. All the numbers must be separated by exactly one space, and there must be no extra space at the end of the line. - Sample Input: ```bash 6 Push 1 Push 2 Push 3 Pop Pop Push 4 Pop Pop Push 5 Push 6 Pop Pop ``` - Sample Output: ```bash 3 4 2 6 5 1 ``` - 实现: ```java package cn.wycode; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.LinkedList; import java.util.Objects; import java.util.Stack; public class Main { public static void main(String args[]) { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); //读第一行 String[] line1Array = getStringArray(reader); String firstAddress = line1Array[0]; int count = Integer.parseInt(line1Array[1]); int k = Integer.parseInt(line1Array[2]); //读取所有节点 ArrayList nodes = new ArrayList<>(count); for (int i = 0; i < count; i++) { String[] lineArray = getStringArray(reader); Node node = new Node(); node.address = lineArray[0]; node.data = Integer.parseInt(lineArray[1]); node.nextAddress = lineArray[2]; nodes.add(node); } //按链表顺序加入队列 LinkedList queue = new LinkedList<>(); String address = firstAddress; while (!address.equals("-1")) { for (int i = 0; i < nodes.size(); i++) { //循环匹配地址 Node n = nodes.get(i); if (Objects.equals(address, n.address)) { //找到则加入队列 queue.add(n); address = n.nextAddress; //查找地址赋值为当前节点的下一个地址 nodes.remove(n); //处理完的移出节点数组,减少循环量 break; } } } Stack stack = new Stack<>(); boolean isCanConvert = queue.size() >= k; //是否够反转 while (!queue.isEmpty()) { //循环出队列 Node n = queue.remove(); if (isCanConvert) { //够反转则压入堆栈 stack.push(n); if (stack.size() == k) { //压够反转数量就全部输出 isCanConvert = queue.size() >= k; //再次检查是否够反转 while (!stack.isEmpty()) { //重新赋值next Node nStack = stack.pop(); if (stack.isEmpty()) { //如果栈里没了,地址就等于队列的下一个 if (queue.isEmpty()) { //队列也没了,地址等于-1 nStack.nextAddress = "-1"; } else { //如果剩下的还能反转 if (isCanConvert) { nStack.nextAddress = queue.get(k - 1).address; } else { nStack.nextAddress = queue.peek().address; } } } else { nStack.nextAddress = stack.peek().address; } print(nStack); } } } else { //不够反转 直接输出节点 print(n); } } } private static void print(Node n) { System.out.println(n.address + " " + n.data + " " + n.nextAddress); } private static String[] getStringArray(BufferedReader reader) { String line1 = null; try { line1 = reader.readLine(); } catch (Exception ignore) { } return line1.split(" "); } static class Node { String address; int data; String nextAddress; } } ``` - 总结: 1. 利用队列和栈解决顺序问题 ## 作业2:Pop Sequence(25 分) Given a stack which can keep M numbers at most. Push N numbers in the order of 1, 2, 3, ..., N and pop randomly. You are supposed to tell if a given sequence of numbers is a possible pop sequence of the stack. For example, if M is 5 and N is 7, we can obtain 1, 2, 3, 4, 5, 6, 7 from the stack, but not 3, 2, 1, 7, 5, 6, 4. Input Specification: Each input file contains one test case. For each case, the first line contains 3 numbers (all no more than 1000): M (the maximum capacity of the stack), N (the length of push sequence), and K (the number of pop sequences to be checked). Then K lines follow, each contains a pop sequence of N numbers. All the numbers in a line are separated by a space. Output Specification: For each pop sequence, print in one line "YES" if it is indeed a possible pop sequence of the stack, or "NO" if not. Sample Input: ```bash 5 7 5 1 2 3 4 5 6 7 3 2 1 7 5 6 4 7 6 5 4 3 2 1 5 6 4 3 7 2 1 1 7 6 5 4 3 2 ``` Sample Output: ```bash YES NO NO YES NO ``` - 实现 ```java public class Main { public static void main(String args[]) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); int sum; sum = Integer.parseInt(reader.readLine()); String[] inputs = new String[sum*2]; for (int i = 0; i < inputs.length; i++) { inputs[i] = reader.readLine(); } StringBuilder sb = new StringBuilder(); //Build Tree Node root = buildTree(inputs); //后序遍历打印 traversalsTree(root,sb); if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } System.out.println(sb.toString()); } private static Node buildTree(String[] inputs) { Node root =null; Stack nodes = new Stack<>(); final String PUSH = "Push"; final String POP = "Pop"; Node lastPush = null; Node lastPop = null; String lastOperation = null; int level = 0; for (String input : inputs) { String[] lineArg = input.split(" "); if (Objects.equals(lineArg[0], PUSH)) { Node node = new Node(); node.data = Integer.parseInt(lineArg[1]); node.level = level; nodes.push(node); if (lastOperation == null) { root = node; } else if (Objects.equals(lastOperation, PUSH)) { lastPush.left = node; } else if (Objects.equals(lastOperation, POP)) { lastPop.right = node; } lastPush = node; lastOperation = PUSH; level++; } else { lastPop = nodes.pop(); lastOperation = POP; level--; } } return root; } private static void traversalsTree(Node root, StringBuilder sb) { if(root!=null) { traversalsTree(root.left, sb); traversalsTree(root.right, sb); sb.append(root.data); sb.append(' '); } } static class Node { int data = -1; Node left; Node right; int level = 0; @Override public String toString() { return "Node{" + "data=" + data + ", left=" + (left==null?"null":left.data) + ", right=" + (right==null?"null":right.data)+ ", level=" + level + '}'; } } } ``` - 总结: 1. 通过先构建出原始树再后序遍历的方法解决了问题。 2. 优化方向:将后序遍历的递归方式改为非递归方式。 3. 此题应该可以在构建树时直接输出正确答案。 - 非递归算法核心: 首先要搞清楚先序、中序、后序的非递归算法共同之处:用栈来保存先前走过的路径,以便可以在访问完子树后,可以利用栈中的信息,回退到当前节点的双亲节点,进行下一步操作。 后序遍历的非递归算法是三种顺序中最复杂的,原因在于,后序遍历是先访问左、右子树,再访问根节点,而在非递归算法中,利用栈回退到时,并不知道是从左子树回退到根节点,还是从右子树回退到根节点,如果从左子树回退到根节点,此时就应该去访问右子树,而如果从右子树回退到根节点,此时就应该访问根节点。所以相比前序和后序,必须得在压栈时添加信息,以便在退栈时可以知道是从左子树返回,还是从右子树返回进而决定下一步的操作。 --- ## React Native源码解析(Android篇) > 原文: https://wycode.cn/blog/react-native | Agent 文档: https://wycode.cn/blog/react-native.md | 日期: 2018-03-19 | 分类: Frontend ![React](/images/20180213_react.png) > Facebook的React Native是比基于WebView更先进的跨平台解决方案。实现了只编写一次JavaScript代码即可同时生成Android及iOS应用,并且同时拥有原生控件的性能。此篇文章我将从React Native Android端源码中探究它到底是如何做到的。以及实现的思路,架构的设计等。 ## 核心类ReactActivity 使用`react-native-cli`生成工程文件,打开工程目录,发现有一个入口Activity:`MainActivity`继承自`ReactActivity`抽象类。这个`ReactActivity`是React Native项目的唯一一个Activity,可以猜想其采用了单Activity的架构。回想Unity,LibGDX这样的游戏引擎,也是采用了这样的模式。 ```java ... /** * Base Activity for React Native applications. */ public abstract class ReactActivity extends Activity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity { private final ReactActivityDelegate mDelegate; protected ReactActivity() { mDelegate = createReactActivityDelegate(); } ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDelegate.onCreate(savedInstanceState); } ... ``` 可以发现在Android框架创建`ReactActivity`实例的同时会创建一个`ReactActivityDelegate`对象。 然后`ReactActivity`将Activity的生命周期方法和一些回调方法注入到这个`ReactActivityDelegate`对象中。 看看这个`ReactActivityDelegate`都在干啥。 ## 核心类ReactActivityDelegate ```java ... /** * Delegate class for {@link ReactActivity} and {@link ReactFragmentActivity}. You can subclass this * to provide custom implementations for e.g. {@link #getReactNativeHost()}, if your Application * class doesn't implement {@link ReactApplication}. */ public class ReactActivityDelegate { ··· private @Nullable ReactRootView mReactRootView; ... protected void onCreate(Bundle savedInstanceState) { if (mMainComponentName != null) { loadApp(mMainComponentName); } mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); } protected void loadApp(String appKey) { if (mReactRootView != null) { throw new IllegalStateException("Cannot loadApp while app is already running."); } mReactRootView = createRootView(); mReactRootView.startReactApplication( getReactNativeHost().getReactInstanceManager(), appKey, getLaunchOptions()); getPlainActivity().setContentView(mReactRootView); } ... } ``` `ReactActivityDelegate`在`OnCreate`方法中调用`loadApp`方法,然后创建了一个`ReactRootView`,并调用了`ReactRootView`的`startReactApplication`方法,最后将这个`ReactRootView`作为Activity的根View显示。 继续看这个`ReactRootView`。 ## 核心类ReactRootView ```java ··· public class ReactRootView extends SizeMonitoringFrameLayout implements RootView, MeasureSpecProvider { ··· /** * Schedule rendering of the react component rendered by the JS application from the given JS * module (@{param moduleName}) using provided {@param reactInstanceManager} to attach to the * JS context of that manager. Extra parameter {@param launchOptions} can be used to pass initial * properties for the react component. */ public void startReactApplication( ReactInstanceManager reactInstanceManager, String moduleName, @Nullable Bundle initialProperties) { ··· if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { mReactInstanceManager.createReactContextInBackground(); } ··· } ··· } ``` 这个`startReactApplication()`方法,最终调用了`ReactInstanceManager`对象的`createReactContextInBackground()`方法。 继续追这个`ReactInstanceManager`。 ## 核心类ReactInstanceManager ```java ··· @ThreadSafe public class ReactInstanceManager { ··· @ThreadConfined(UI) public void createReactContextInBackground() { ··· recreateReactContextInBackgroundInner(); } ··· @ThreadConfined(UI) public void recreateReactContextInBackground() { ··· recreateReactContextInBackgroundInner(); } @ThreadConfined(UI) private void recreateReactContextInBackgroundInner() { ··· recreateReactContextInBackgroundFromBundleLoader(); } @ThreadConfined(UI) private void recreateReactContextInBackgroundFromBundleLoader() { ··· recreateReactContextInBackground(mJavaScriptExecutorFactory, mBundleLoader); } ··· @ThreadConfined(UI) private void recreateReactContextInBackground( JavaScriptExecutorFactory jsExecutorFactory, JSBundleLoader jsBundleLoader) { ··· runCreateReactContextOnNewThread(initParams); ··· } @ThreadConfined(UI) private void runCreateReactContextOnNewThread(final ReactContextInitParams initParams) { ··· mCreateReactContextThread = new Thread( new Runnable() { @Override public void run() { ··· final ReactApplicationContext reactApplicationContext = createReactContext( initParams.getJsExecutorFactory().create(), initParams.getJsBundleLoader()); ··· Runnable setupReactContextRunnable = new Runnable() { @Override public void run() { try { setupReactContext(reactApplicationContext); } catch (Exception e) { mDevSupportManager.handleException(e); } } }; reactApplicationContext.runOnNativeModulesQueueThread(setupReactContextRunnable); ··· } }); ··· mCreateReactContextThread.start(); } ··· /** * @return instance of {@link ReactContext} configured a {@link CatalystInstance} set */ private ReactApplicationContext createReactContext( JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) { ··· final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); ··· CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder() .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault()) .setJSExecutor(jsExecutor) .setRegistry(nativeModuleRegistry) .setJSBundleLoader(jsBundleLoader) .setNativeModuleCallExceptionHandler(exceptionHandler); ··· catalystInstance = catalystInstanceBuilder.build(); ··· catalystInstance.runJSBundle(); reactContext.initializeWithInstance(catalystInstance); return reactContext; } ··· } ``` 这个`ReactInstanceManager`有点长,通过一系列方法调用,最后通过`createReactContext()`方法创建了`ReactApplicationContext`的实例,并创建了`CatalystInstance`用于初始化`reactContext`。 在初始化`reactContext`之前又调用了`catalystInstance.runJSBundle();`,猜想这里是调用JS代码的入口,这里的`CatalystInstance`是个接口,我们要查看`CatalystInstanceImpl`来看看具体实现。 ## 核心类CatalystInstanceImpl ```java ··· /** * This provides an implementation of the public CatalystInstance instance. It is public because * it is built by XReactInstanceManager which is in a different package. */ @DoNotStrip public class CatalystInstanceImpl implements CatalystInstance { ··· public static class PendingJSCall { ··· void call(CatalystInstanceImpl catalystInstance) { NativeArray arguments = mArguments != null ? mArguments : new WritableNativeArray(); catalystInstance.jniCallJSFunction(mModule, mMethod, arguments); } ··· } ··· private native void jniSetSourceURL(String sourceURL); private native void jniRegisterSegment(int segmentId, String path); private native void jniLoadScriptFromAssets(AssetManager assetManager, String assetURL, boolean loadSynchronously); private native void jniLoadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously); @Override public void runJSBundle() { static { ReactBridge.staticInit(); } ··· mJSBundleLoader.loadScript(CatalystInstanceImpl.this); synchronized (mJSCallsPendingInitLock) { // Loading the bundle is queued on the JS thread, but may not have // run yet. It's safe to set this here, though, since any work it // gates will be queued on the JS thread behind the load. mAcceptCalls = true; for (PendingJSCall function : mJSCallsPendingInit) { function.call(this); } mJSCallsPendingInit.clear(); mJSBundleHasLoaded = true; } ··· } private native void jniCallJSFunction( String module, String method, NativeArray arguments); ··· } ``` 这里通过`mJSBundleLoader.loadScript()`去加载js,然后把js函数构建为`PendingJSCall`放到一个列表里逐个执行,`PendingJSCall`的`call()`方法最终调用了C++层的`jniCallJSFunction()`函数。 文件开头使用静态代码块`ReactBridge.staticInit()`装载了名为`reactnativejni`的so文件。 至此java层源码就追完了,我们接着看c++层源码 ## C++层 ```c++ ··· namespace facebook { namespace react { ··· void CatalystInstanceImpl::initializeBridge( jni::alias_ref callback, // This executor is actually a factory holder. JavaScriptExecutorHolder* jseh, jni::alias_ref jsQueue, jni::alias_ref nativeModulesQueue, jni::alias_ref::javaobject> javaModules, jni::alias_ref::javaobject> cxxModules) { // TODO mhorowitz: how to assert here? // Assertions.assertCondition(mBridge == null, "initializeBridge should be called once"); moduleMessageQueue_ = std::make_shared(nativeModulesQueue); // This used to be: // // Java CatalystInstanceImpl -> C++ CatalystInstanceImpl -> Bridge -> Bridge::Callback // --weak--> ReactCallback -> Java CatalystInstanceImpl // // Now the weak ref is a global ref. So breaking the loop depends on // CatalystInstanceImpl#destroy() calling mHybridData.resetNative(), which // should cause all the C++ pointers to be cleaned up (except C++ // CatalystInstanceImpl might be kept alive for a short time by running // callbacks). This also means that all native calls need to be pre-checked // to avoid NPE. // See the comment in callJSFunction. Once js calls switch to strings, we // don't need jsModuleDescriptions any more, all the way up and down the // stack. moduleRegistry_ = std::make_shared( buildNativeModuleList( std::weak_ptr(instance_), javaModules, cxxModules, moduleMessageQueue_)); instance_->initializeBridge( folly::make_unique( callback, moduleMessageQueue_), jseh->getExecutorFactory(), folly::make_unique(jsQueue), moduleRegistry_); } ··· void CatalystInstanceImpl::jniCallJSFunction(std::string module, std::string method, NativeArray* arguments) { // We want to share the C++ code, and on iOS, modules pass module/method // names as strings all the way through to JS, and there's no way to do // string -> id mapping on the objc side. So on Android, we convert the // number to a string, here which gets passed as-is to JS. There, they they // used as ids if isFinite(), which handles this case, and looked up as // strings otherwise. Eventually, we'll probably want to modify the stack // from the JS proxy through here to use strings, too. instance_->callJSFunction(std::move(module), std::move(method), arguments->consume()); } ··· }} ``` 在这个`CatalystInstanceImpl.cpp`中可以发现c++对于js的调用。以及非常棒的两段注释。 React Navtive 通过C++层去解释执行JS,从而达到一个比较好的性能。 ## 总结 通过此文算是搞明白了,Android系统是怎么样去执行JavaScript代码的。 但React Native又是如何将JS中的组件渲染成Android原生组件的仍然是个疑问。 以后再研究吧... --- ## 数据结构与算法学习笔记02 > 原文: https://wycode.cn/blog/data-structures02 | Agent 文档: https://wycode.cn/blog/data-structures02.md | 日期: 2018-03-16 | 分类: Life ![浙江大学](https://www.smpg888.com/resource/images/19b3b5c5671f44a4b9f4320dd8fcc734_34.jpg) > 线性结构的基础是数组和链表,由数组和链表可以扩展出堆栈和队列。 ## 实验1:计算表达式的值 将中缀表达式转换为后缀表达式,并计算表达式的值。 - 实现: ```java import java.util.Stack; public class Test05 { public static void main(String[] args) { String s = "2*(6/3+4)-5"; System.out.println("中缀表达式:"+s); s= covert(s); System.out.println("转换为后缀表达式:"+s); int i = calculate(s); System.out.println("计算后缀表达式:"+i); } private static int calculate(String s) { Stack stack = new Stack<>(); for (char c : s.toCharArray()) { //从左到右遍历,如果是数字压入堆栈,如果是操作符,从堆栈中pop出两个数字计算,并将结果压入堆栈 if(c=='+') { stack.push(stack.pop() + stack.pop()); }else if(c=='*') { stack.push(stack.pop() * stack.pop()); }else if(c=='-') { int b = stack.pop(); int a = stack.pop(); stack.push(a - b); }else if(c=='/'){ int b = stack.pop(); int a = stack.pop(); stack.push(a / b); }else{ stack.push(Character.getNumericValue(c)); } } return stack.pop(); //循环结束时,栈顶的数字就是后缀表达式结果 } private static String covert(String s) { StringBuilder sb = new StringBuilder(); Stack stack = new Stack<>(); for (char c : s.toCharArray()) { if (c >= '0' && c <= '9') { //如果是数字直接输出 sb.append(c); } else if (c == '(') { //如果是左括号,压入操作堆栈 stack.push(c); } else if (c == ')') { //如果是右括号 while (true) { //寻找左括号 c = stack.pop(); if (c == '(') { //找到直接结束循环 break; } else { //不是左括号直接输出操作符 sb.append(c); } } } else { //如果是+ - * / while (true) { //寻找栈顶比当前操作优先级低的操作符 if (stack.isEmpty()) { //如果是空栈,直接压入操作符 stack.push(c); break; } char cInStack = stack.peek(); if (isStackPriority(cInStack, c)) { //如果栈顶操作符优先级高或相同,直接出栈并输出 sb.append(stack.pop()); } else { stack.push(c); //找到优先级低的地方,把当前操作符push进去 break; } } } //System.out.println(stack.size()); } while (!stack.isEmpty()){ //最后将栈里的操作符全部输出 sb.append(stack.pop()); } return sb.toString(); } private static boolean isStackPriority(char cInStack, char c2) { return cInStack != '(' && (cInStack == '*' || cInStack == '/' || c2 != '*' && c2 != '/'); } } ``` - 输出: ```bash 中缀表达式:2*(6/3+4)-5 转换为后缀表达式:263/4+*5- 计算后缀表达式:7 ``` - 总结: 1. 利用堆栈可以很优雅的解决表达式运算的问题 2. 以上程序有3个缺陷:不能计算小数;不能计算负数;只能计算个位数的运算 3. 改进方向:使用浮点型计算;后缀转换时要添加对于多位数、小数、负数的处理;将结果直接保存到堆栈里,堆栈内保存数字和操作符 ## 作业1:一元多项式的乘法与加法运算(20 分) 设计函数分别求两个一元多项式的乘积与和。 输入格式: 输入分2行,每行分别先给出多项式非零项的个数,再以指数递降方式输入一个多项式非零项系数和指数(绝对值均为不超过1000的整数)。数字间以空格分隔。 输出格式: 输出分2行,分别以指数递降方式输出乘积多项式以及和多项式非零项的系数和指数。数字间以空格分隔,但结尾不能有多余空格。零多项式应输出0 0。 输入样例: ```bash 5 3 4 -5 2 6 1 -2 0 0 0 5 5 20 -7 4 3 1 0 0 3 0 ``` 输出样例: ```bash 15 24 -25 22 30 21 -10 20 -21 8 35 6 -33 5 14 4 -15 3 18 2 -6 1 5 20 -4 4 -5 2 9 1 -2 0 ``` - 实现: ```java public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); PolyNode first1 = getInput(scanner); PolyNode first2 = getInput(scanner); PolyNode result1 = add(first1, first2); PolyNode result2 = multiply(first1, first2); print(result2); print(result1); } /** * 逐项插入算法 * * @param first1 * @param first2 * @return */ private static PolyNode multiply(PolyNode first1, PolyNode first2) { PolyNode p1 = first1; PolyNode p2 = first2; PolyNode first = new PolyNode(); //结果,先构造一个空的头节点 PolyNode rear = first; //将尾部指向这个头 while (p2 != null) { //先用第一项乘以p2构造一个初始多项式 PolyNode next = new PolyNode(); //构造一个新节点 next.coef = p1.coef * p2.coef; //系数相乘 next.expon = p1.expon + p2.expon; //指数相加 rear.next = next; //尾部指针指向新的节点 rear = next; //新尾部等于新添加的节点 p2 = p2.next; //移动p2指针 } p1 = p1.next; //把p1指针向后挪1位 while (p1 != null) { p2 = first2; //将p2指针归位 rear = first; //尾部指针归位 while (p2 != null) { int e = p1.expon + p2.expon; //指数相加 int c = p1.coef * p2.coef; //系数相乘 while (rear.next != null && rear.next.expon > e) { //寻找比当前指数小或者等的位置 rear = rear.next; //rear 往后挪 } if (rear.next != null && rear.next.expon == e) { //挪了之后如果下一项存在,且指数相等,则合并系数 if (rear.next.coef + c == 0) { //和下一项系数相加等于0,则删掉这一项 rear.next = rear.next.next; } else { //系数相加不为0,就要合并系数 rear.next.coef += c; } } else { //指数不等,需要创建一个新节点插入 PolyNode newPoly = new PolyNode(); //构造一个新节点 newPoly.coef = c; newPoly.expon = e; newPoly.next = rear.next;//新插入的节点下一个=当前尾部的下一个 rear.next = newPoly; //当前尾部的下一个等于新节点 } p2 = p2.next;//p2处理完毕,将p2挪到下一个 } p1 = p1.next;//p2遍历完毕,将p1往后挪 } first = first.next; //将头部挪到真实的头部位置,Java会自动回收没有引用的头节点 return first; } private static void print(PolyNode result) { PolyNode p = result; StringBuilder sb = new StringBuilder(); if (p == null) { //零多项式输出0 0 sb.append("0 0"); } boolean isFirst = true; while (p != null) { if (!isFirst) sb.append(' '); sb.append(p.coef); sb.append(' '); sb.append(p.expon); p = p.next; isFirst = false; } System.out.println(sb.toString()); } private static PolyNode add(PolyNode first1, PolyNode first2) { PolyNode p1 = first1; PolyNode p2 = first2; PolyNode first = new PolyNode(); //结果,先构造一个空的头节点 PolyNode rear = first; //将尾部指向这个头 while (p1 != null && p2 != null) { //当两个指针都不为空时 if (p1.coef == 0) { //系数为0,直接省略 p1 = p1.next; } else if (p2.coef == 0) { p2 = p2.next; } else if (p1.expon == p2.expon) { //指数相等,则系数相加后串到后面 int coef = p1.coef + p2.coef; //系数相加 if (coef != 0) { PolyNode next = new PolyNode(); //构造一个新节点 next.coef = coef; next.expon = p1.expon; rear.next = next; //尾部指针指向新的节点 rear = next; //新尾部等于新添加的节点 } p1 = p1.next; //移动指针 p2 = p2.next; } else if (p1.expon > p2.expon) {//指数大的串到后面 PolyNode next = new PolyNode(); //构造一个新节点 next.coef = p1.coef; next.expon = p1.expon; rear.next = next; //尾部指针指向新的节点 rear = next; //新尾部等于新添加的节点 p1 = p1.next; //移动指针 } else { PolyNode next = new PolyNode(); //构造一个新节点 next.coef = p2.coef; next.expon = p2.expon; rear.next = next; //尾部指针指向新的节点 rear = next; //新尾部等于新添加的节点 p2 = p2.next; //移动指针 } } while (p1 != null) { //p1没完,把所有p1追加到后面 if (p1.coef != 0) { //系数不为0才,添加,否则继续找因为后面可以能还有系数不为0的 PolyNode next = new PolyNode(); //构造一个新节点 next.coef = p1.coef; next.expon = p1.expon; rear.next = next; //尾部指针指向新的节点 rear = next; //新尾部等于新添加的节点 } p1 = p1.next; //移动指针 } while (p2 != null && p2.coef != 0) { //p2没完,把所有p2追加到后面 if (p2.coef != 0) { //系数不为0才,添加,否则继续找因为后面可以能还有系数不为0的 PolyNode next = new PolyNode(); //构造一个新节点 next.coef = p2.coef; next.expon = p2.expon; rear.next = next; //尾部指针指向新的节点 rear = next; //新尾部等于新添加的节点 } p2 = p2.next; //移动指针 } first = first.next; //将头部挪到真实的头部位置,Java会自动回收没有引用的头节点 return first; } private static PolyNode getInput(Scanner scanner) { int count = scanner.nextInt(); //得到总项数 PolyNode first = new PolyNode(); //构造一个空的头 PolyNode rear = first; //构造一个尾部的指针 while (count > 0) { PolyNode next = new PolyNode(); //读取一个节点 next.coef = scanner.nextInt(); next.expon = scanner.nextInt(); rear.next = next; //将尾部的指针指向读到的这个节点 rear = next; //将尾部指针挪到新添加的节点 count--; //循环 } first = first.next; //将头部挪到真实的头部位置,Java会自动回收没有引用的头节点 return first; } static class PolyNode { int coef; int expon; PolyNode next; } } ``` - 总结: 1. 逐项插值算法; 2. 单个对象保存一个当前类的引用可以当作链表来使用; 3. 原输入的链表不要取修改它的内部对象引用或者变量值; ## 作业1:Reversing Linked List(25 分) Given a constant K and a singly linked list L, you are supposed to reverse the links of every K elements on L. For example, given L being 1→2→3→4→5→6, if K=3, then you must output 3→2→1→6→5→4; if K=4, you must output 4→3→2→1→5→6. Input Specification: Each input file contains one test case. For each case, the first line contains the address of the first node, a positive N (≤10 ​5 ​​ ) which is the total number of nodes, and a positive K (≤N) which is the length of the sublist to be reversed. The address of a node is a 5-digit nonnegative integer, and NULL is represented by -1. Then N lines follow, each describes a node in the format: `Address Data Next` where `Address` is the position of the node, `Data` is an integer, and `Next` is the position of the next node. Output Specification: For each case, output the resulting ordered linked list. Each node occupies a line, and is printed in the same format as in the input. Sample Input: ```bash 00100 8 4 00000 4 99999 00100 1 12309 68237 6 77777 33218 3 00000 77777 7 88888 99999 5 68237 12309 2 33218 88888 8 -1 ``` Sample Output: ```bash 00000 4 33218 33218 3 12309 12309 2 00100 00100 1 99999 99999 5 68237 68237 6 -1 ``` - 实现: ```java package cn.wycode; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.LinkedList; import java.util.Objects; import java.util.Stack; public class Main { public static void main(String args[]) { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); //读第一行 String[] line1Array = getStringArray(reader); String firstAddress = line1Array[0]; int count = Integer.parseInt(line1Array[1]); int k = Integer.parseInt(line1Array[2]); //读取所有节点 ArrayList nodes = new ArrayList<>(count); for (int i = 0; i < count; i++) { String[] lineArray = getStringArray(reader); Node node = new Node(); node.address = lineArray[0]; node.data = Integer.parseInt(lineArray[1]); node.nextAddress = lineArray[2]; nodes.add(node); } //按链表顺序加入队列 LinkedList queue = new LinkedList<>(); String address = firstAddress; while (!address.equals("-1")) { for (int i = 0; i < nodes.size(); i++) { //循环匹配地址 Node n = nodes.get(i); if (Objects.equals(address, n.address)) { //找到则加入队列 queue.add(n); address = n.nextAddress; //查找地址赋值为当前节点的下一个地址 nodes.remove(n); //处理完的移出节点数组,减少循环量 break; } } } Stack stack = new Stack<>(); boolean isCanConvert = queue.size() >= k; //是否够反转 while (!queue.isEmpty()) { //循环出队列 Node n = queue.remove(); if (isCanConvert) { //够反转则压入堆栈 stack.push(n); if (stack.size() == k) { //压够反转数量就全部输出 isCanConvert = queue.size() >= k; //再次检查是否够反转 while (!stack.isEmpty()) { //重新赋值next Node nStack = stack.pop(); if (stack.isEmpty()) { //如果栈里没了,地址就等于队列的下一个 if (queue.isEmpty()) { //队列也没了,地址等于-1 nStack.nextAddress = "-1"; } else { //如果剩下的还能反转 if (isCanConvert) { nStack.nextAddress = queue.get(k - 1).address; } else { nStack.nextAddress = queue.peek().address; } } } else { nStack.nextAddress = stack.peek().address; } print(nStack); } } } else { //不够反转 直接输出节点 print(n); } } } private static void print(Node n) { System.out.println(n.address + " " + n.data + " " + n.nextAddress); } private static String[] getStringArray(BufferedReader reader) { String line1 = null; try { line1 = reader.readLine(); } catch (Exception ignore) { } return line1.split(" "); } static class Node { String address; int data; String nextAddress; } } ``` - 总结: 1. 利用队列和栈解决顺序问题 ## 作业2:Pop Sequence(25 分) Given a stack which can keep M numbers at most. Push N numbers in the order of 1, 2, 3, ..., N and pop randomly. You are supposed to tell if a given sequence of numbers is a possible pop sequence of the stack. For example, if M is 5 and N is 7, we can obtain 1, 2, 3, 4, 5, 6, 7 from the stack, but not 3, 2, 1, 7, 5, 6, 4. Input Specification: Each input file contains one test case. For each case, the first line contains 3 numbers (all no more than 1000): M (the maximum capacity of the stack), N (the length of push sequence), and K (the number of pop sequences to be checked). Then K lines follow, each contains a pop sequence of N numbers. All the numbers in a line are separated by a space. Output Specification: For each pop sequence, print in one line "YES" if it is indeed a possible pop sequence of the stack, or "NO" if not. Sample Input: ```bash 5 7 5 1 2 3 4 5 6 7 3 2 1 7 5 6 4 7 6 5 4 3 2 1 5 6 4 3 7 2 1 1 7 6 5 4 3 2 ``` Sample Output: ```bash YES NO NO YES NO ``` --- ## 数据结构与算法学习笔记01 > 原文: https://wycode.cn/blog/data-structures01 | Agent 文档: https://wycode.cn/blog/data-structures01.md | 日期: 2018-03-14 | 分类: Life ![浙江大学](https://www.smpg888.com/resource/images/19b3b5c5671f44a4b9f4320dd8fcc734_34.jpg) > 程序=数据结构+算法。 浙江大学数据结构是国家精品课程,于2018年3月开学,2018年6月学期结束。 ## 实验1:给出数字N,要求程序输出1~N的整数 使用Java测试:输出10000次,for循环耗时114ms,递归耗时61ms!100000次递归栈溢出崩溃,for循环耗时900ms。 ```java public class Main { public static void main(String[] args) { // write your code here Main m = new Main(); long time = System.currentTimeMillis(); m.printN(10000); System.out.println("time="+(System.currentTimeMillis()-time)); time = System.currentTimeMillis(); m.printNR(10000); System.out.println("time="+(System.currentTimeMillis()-time)); } private void printNR(int n) { if(n>0) { printNR(n - 1); System.out.println(n); } } private void printN(int n){ for(int i= 1;i<=n;i++){ System.out.println(i); } } } ``` 用C测试:输出100000次,for循环耗时149ms,递归耗时148ms,100000次递归耗时150ms,100万次 ```C #include #include void printN(int n); void printNC(int n); int main(){ clock_t startTime = clock(); printN(100000); //double diff = ; printf("%f seconds\n",difftime(clock(),startTime)/CLOCKS_PER_SEC); } void printN(int n){ for(int i=1;i<=n;i++){ printf("%d\n",i); } } void printNC(int n){ if(n){ printNC(n-1); printf("%d\n",n); } } ``` - 总结: 1. ~~C语言程序运行效率至少在Java的10倍以上~~(C程序计时不准,需要重新测试) 2. 递归的空间效率很差,容易堆栈溢出崩溃。 3. Java环境递归耗时是for循环的1/2。 4. C语言环境递归耗时和for循环差不多。 ## 实验2:计算多项式的值 ![多项式](/images/20180313_data_structure.png) ```java public class Test02 { public static void main(String[] args) { // write your code here Test02 t = new Test02(); long time = System.currentTimeMillis(); for(int i=0;i<100000;i++) { t.f1(1.1); } System.out.println("time=" + (System.currentTimeMillis() - time)); time = System.currentTimeMillis(); for(int i=0;i<100000;i++) { t.f2(1.1); } System.out.println("time=" + (System.currentTimeMillis() - time)); } private double f1(double x) { double v = 1f; for (int i = 1; i <= 100; i++) { v += Math.pow(x,i) / i; } return v; } //f(x) = 1+x(1/1+x(1/2+x(1/3+....x(1/100)))) private double f2(double x) { double v = x/100; int i = 100; while (i>1) { v = x*(1f/(i-1)+v); i--; } return v+1; } } ``` - 总结: 1. 循环10万次测试,方法1,耗时1016ms,方法2耗时16ms。约100倍的效率差距 2. 计算时一定要注意,整形和浮点型`v = x*(1f/(i-1)+v);` 3. 方法1,慢就慢在`pow`函数上 4. 替换pow函数为计数循环`x *=x`耗时500ms,差距缩小至约20倍。 ## 作业1 01-复杂度1 最大子列和问题(20 分) 给定K个整数组成的序列{ N​1, N2, ..., NK},“连续子列”被定义为{ N​i, Ni+1, ..., N​j},其中 1≤i≤j≤K。 “最大子列和”则被定义为所有连续子列元素的和中最大者。 例如给定序列{ -2, 11, -4, 13, -5, -2 },其连续子列{ 11, -4, 13 }有最大的和20。 现要求你编写程序,计算给定整数序列的最大子列和。 本题旨在测试各种不同的算法在各种数据情况下的表现。 各组测试数据特点如下: 数据1:与样例等价,测试基本正确性; 数据2:102个随机整数; 数据3:103个随机整数; 数据4:104个随机整数; 数据5:105个随机整数; 输入格式: 输入第1行给出正整数K (≤100000);第2行给出K个整数,其间以空格分隔。 输出格式: 在一行中输出最大子列和。如果序列中所有整数皆为负数,则输出0。 输入样例: ``` 6 -2 11 -4 13 -5 -2 ``` 输出样例: ``` 20 ``` 实现: ```java import java.util.Scanner; public class Main{ public static void main(String args[]) { Scanner scanner = new Scanner(System.in); int k = scanner.nextInt(); int[] n = new int[k]; for (int i = 0; i < k; i++) { if (scanner.hasNext()) { n[i] = scanner.nextInt(); } } System.out.println(maximumSubsequenceSum(n)); } /** * 在线处理算法,任意时刻给出的都是当前最大子数列和 * @param n * @return */ private static int maximumSubsequenceSum(int[] n) { int thisMax = 0; int maxSum = 0; for (int i = 0; i < n.length; i++) { thisMax += n[i]; if (thisMax < 0) { thisMax = 0; } else if (thisMax > maxSum) { maxSum = thisMax; } } return maxSum; } } ``` ## 作业2 01-复杂度2 Maximum Subsequence Sum(25 分) Given a sequence of K integers { N1, N2, ..., NK}. A continuous subsequence is defined to be { Ni, Ni+1, ..., Nj } where 1≤i≤j≤K. The Maximum Subsequence is the continuous subsequence which has the largest sum of its elements. For example, given sequence { -2, 11, -4, 13, -5, -2 }, its maximum subsequence is { 11, -4, 13 } with the largest sum being 20. Now you are supposed to find the largest sum, together with the first and the last numbers of the maximum subsequence. Input Specification: Each input file contains one test case. Each case occupies two lines. The first line contains a positive integer K (≤10000). The second line contains K numbers, separated by a space. Output Specification: For each test case, output in one line the largest sum, together with the first and the last numbers of the maximum subsequence. The numbers must be separated by one space, but there must be no extra space at the end of a line. In case that the maximum subsequence is not unique, output the one with the smallest indices i and j (as shown by the sample case). If all the K numbers are negative, then its maximum sum is defined to be 0, and you are supposed to output the first and the last numbers of the whole sequence. Sample Input: ``` 10 -10 1 2 3 4 -5 -23 3 7 -21 ``` Sample Output: ``` 10 1 4 ``` - 实现: ```java public class Main { public static void main(String[] args) { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String countString = null; String numbersString =null; try { countString = reader.readLine(); numbersString = reader.readLine(); }catch (Exception ignored){ } int[] n = new int[Integer.parseInt(countString)]; String[] numbersStringArray = numbersString.split(" "); for (int i = 0; i < n.length; i++) { n[i] = Integer.parseInt(numbersStringArray[i]); } // Scanner scanner = new Scanner(System.in); // int k = scanner.nextInt(); // int[] n = new int[k]; // // for (int i = 0; i < k; i++) { // n[i] = scanner.nextInt(); // } maximumSubsequenceSum(n); } /** * 在线处理算法,任意时刻给出的都是当前最大子数列和 * * @param n * @return */ private static void maximumSubsequenceSum(int[] n) { int thisMax = 0; int maxSum = 0; int startI = 0; int endI = n.length - 1; int temp = 0; boolean isMaxStart = true; for (int i = 0; i < n.length; i++) { thisMax += n[i]; if (thisMax < 0) { thisMax = 0; temp = 0; isMaxStart = true; } else { if(isMaxStart){ temp = i; isMaxStart = false; } if (thisMax > maxSum) { maxSum = thisMax; endI = i; startI = temp; } if(maxSum==0){ startI =temp; endI = i; } } } System.out.println(maxSum + " " + n[startI] + " " + n[endI]); } } ``` - 总结 1. 这题所有case要求在200ms内执行完毕,其中一个case比较大的N导致执行超时,调查发现Scanner这个东西贼慢...换成BufferReader就好了。 ## 作业3 01-复杂度3 二分查找(20 分) 本题要求实现二分查找算法。 函数接口定义: `Position BinarySearch( List L, ElementType X );` 其中List结构定义如下: ```c typedef int Position; typedef struct LNode *List; struct LNode { ElementType Data[MAXSIZE]; Position Last; /* 保存线性表中最后一个元素的位置 */ }; ``` L是用户传入的一个线性表,其中ElementType元素可以通过>、==、<进行比较,并且题目保证传入的数据是递增有序的。函数BinarySearch要查找X在Data中的位置,即数组下标(注意:元素从下标1开始存储)。找到则返回下标,否则返回一个特殊的失败标记NotFound。 裁判测试程序样例: ```c #include #include #define MAXSIZE 10 #define NotFound 0 typedef int ElementType; typedef int Position; typedef struct LNode *List; struct LNode { ElementType Data[MAXSIZE]; Position Last; /* 保存线性表中最后一个元素的位置 */ }; List ReadInput(); /* 裁判实现,细节不表。元素从下标1开始存储 */ Position BinarySearch( List L, ElementType X ); int main() { List L; ElementType X; Position P; L = ReadInput(); scanf("%d", &X); P = BinarySearch( L, X ); printf("%d\n", P); return 0; } /* 你的代码将被嵌在这里 */ ``` 输入样例1: ``` 5 12 31 55 89 101 31 ``` 输出样例1: ``` 2 ``` 输入样例2: ``` 3 26 78 233 31 ``` 输出样例2: ``` 0 ``` - 实现: --- ## OpenCV入门 > 原文: https://wycode.cn/blog/opencv | Agent 文档: https://wycode.cn/blog/opencv.md | 日期: 2018-03-08 | 分类: AI ![OpenCV](/images/20180308_OpenCV.png) > OpenCV是一个开源的计算机视觉库,广泛应用于各种图像处理,人脸识别,图像分析,自动驾驶等领域。目前最新版是3.4.1,本文基于最新版编写。 OpenCV使用C++编写,采用了模块化的结构,本文将逐个介绍每个模块的主要功能,并将介绍如何使用Java调用OpenCV库。 ## 1.模块 - **Core functionality**:**核心功能**,定义了一些其它模块需要使用的基本功能,以及包括多维数组`Mat`在内的众多数据结构。OpenCV的图像处理主要就是针对`Mat`对象。 - **Image processing**:**图像处理**,包括线性和非线性图像滤波,几何图像转换(调整大小,仿射和透视变形,通用基于表格的重新映射),色彩空间转换,直方图等。 - **imgcodecs**:**图片读写**。 - **calib3d**:**3D处理**,基本多视图几何算法,单个和立体相机校准,对象姿态估计,立体对应算法以及三维重建元素。 - **features2d**:**特征点处理**,显着特征检测器,描述符和描述符匹配器。 - **objdetect**:**物体检测**,物体检测和预定义的类别(例如,脸部,眼睛,杯子,人物,汽车等)。 - **highgui**:**UI**,一个简单易用的UI接口。 - **Video I/O**:**视频IO**,一个简单易用的视频捕捉和视频编解码器接口。 - **gpu**:**GPU加速**,不同OpenCV模块的GPU加速算法。 - **dnn**:**深度神经网络**。 - **ml**:**机器学习**。 - **flann**:**多维聚类和搜索**。包含大数据集中的快速最近邻搜索和高维特征而优化的算法集合。 - **photo**:**照片处理**,降噪,HDR,无缝克隆,细节增强等。 - **stitching**:**图像拼接**,降噪,HDR,无缝克隆,细节增强等。 - **shape**:**形状**,形状距离和匹配。 - **superres**:**超分辨率**。 - **videostab**:**视频防抖**。 - **viz**:**3D虚拟化**。 还有一些其它扩展模块。 ## 2.配置Java开发环境 1. 下载Windows版OpenCV 2. 解压后在`build`文件夹中找到`java`文件夹,复制到Java工程的libs文件夹里 3. 对`opencv-341.jar`添加依赖 4. VM option 添加本地库配置:`-Djava.library.path=./libs/x64` ## 3.Hello OpenCV ```java Mat mat = Imgcodecs.imread(file.getAbsolutePath()); Imgproc.cvtColor(mat, mat, Imgproc.COLOR_BGR2GRAY); Imgproc.Canny(mat, mat, 5, 20); Image img = Utils.mat2Image(mat); controller.setImage(img); ``` 运行结果如图: ![OpenCV](/images/20180309_opencv_test.png) --- ## 用React做一个跨平台剪切板 > 原文: https://wycode.cn/blog/clipboard-react | Agent 文档: https://wycode.cn/blog/clipboard-react.md | 日期: 2018-02-13 | 分类: Frontend ![React](/images/20180213_react.png) > 这篇文章我们将用React实现一个跨平台剪切板。 通常我们会遇到将手机上的文字发送到电脑上,或者反过来,或者是记录一些网址,你懂得。这时我们会用到一些云笔记之类的产品,或者干脆使用微信或QQ,它们都太重了,用起来没那么方便,还要登录注册啥的。我们使用React来开发一款手机和电脑上都能访问,并且无需注册登录的云便签应用。 ## 1.使用`create-react-app`创建工程 ```bash npm install -g create-react-app create-react-app my-app cd my-app npm start ``` ## 2.编写模版 ```html render() { return (
0 ? this.state.queryNumber : ''} disabled={this.state.isShowResult} onChange={this.handleNumberChange}/> {this.state.isShowResult ? null : } {this.state.isShowResult ? null : } {this.state.isShowResult ? ``` - `.wxml`后缀的是小程序的布局(模板)文件。 - 属性没什么好解释的,看一下就懂了。参考React版的教程就知道每个属性的详细用途。 ## 3.样式表 ```css /**index.wxss**/ input { border: 1px solid; border-radius: 10rpx; border-color: #ddd; padding: 10rpx; width: 96%; } button { margin-top: 24rpx; width: 100%; } textarea{ border: 1px solid; border-radius: 10rpx; border-color: #ddd; padding: 10rpx; width: 96%; margin-top: 24rpx; } ``` - 就是传统的CSS层叠样式表 - 这里rpx是dpi无关的单位,保证不同分辨率的相同观感 ## 4.逻辑 ```javascript //index.js //获取应用实例 const app = getApp() Page({ data: { queryNumber: -1, isShowResult: false, text: '' }, onLoad: function () { }, bindInput: function (e) { this.setData({ queryNumber: e.detail.value }) }, bindInputText: function (e) { this.setData({ text: e.detail.value }) }, query: function () { if (this.data.queryNumber <= 0) { wx.showToast({ title: '查询码不正确!', icon: 'none' }); return } var that = this; wx.showLoading({ title: '请稍后...', }) wx.request({ url: 'https://wycode.cn/web/api/public/clipboard/query', data: { 'id': this.data.queryNumber }, success: function (res) { if (!res.data.success || res.data.data == null) { wx.showToast({ title: '查询码不正确!', icon: 'none' }); } else { that.setData({ isShowResult: true, queryNumber: res.data.data.id, text: res.data.data.content }); } }, fail: function (res) { wx.showToast({ title: res, icon: 'none' }); }, complete:function(){ wx.hideLoading(); } }); }, create: function () { var that = this; wx.showLoading({ title: '请稍后...', }) wx.request({ url: 'https://wycode.cn/web/api/public/clipboard/create', method: 'POST', success: function (res) { that.setData({ isShowResult: true, queryNumber: res.data.data.id, text: res.data.data.content }); }, fail: function (res) { wx.showToast({ title: res.errMsg, icon: 'none' }); }, complete:function(){ wx.hideLoading(); } }); }, save: function () { var that = this; wx.showLoading({ title: '请稍后...', }) wx.request({ url: 'https://wycode.cn/web/api/public/clipboard/save', method: 'POST', data:{ id: that.data.queryNumber, content: that.data.text }, header: { 'content-type': 'application/x-www-form-urlencoded' }, success: function (res) { that.setData({ isShowResult: false, queryNumber: res.data.data.id, text: res.data.data.content }); wx.showToast({ title: '保存成功!', icon: 'success' }); }, fail: function (res) { wx.showToast({ title: res.errMsg, icon: 'none' }); }, complete: function () { wx.hideLoading(); } }); }, back: function () { this.setData({ isShowResult: false }) } }) ``` - 小程序的`data`,就是React的`state` - 小程序通过`this.setData()`刷新页面状态,如同React的`this.setState()` - 使用`wx.request()`做http请求 - 使用`wx.showToast()`显示一个提示 - 使用`wx.showLoading()`显示Loading - 这里发现一个小程序的bug,在`wx.hideLoading()`会将Toast也消失掉。 至此大功告成!打开微信扫一扫文章开头的小程序码即可使用。 访问 https://github.com/wangyucode/Clipboard 查看完整源码 --- ## 《未来简史》读书笔记 > 原文: https://wycode.cn/blog/home-deus | Agent 文档: https://wycode.cn/blog/home-deus.md | 日期: 2018-01-29 | 分类: Life ![未来简史](/images/20180129_homo_deus.jpg) ## 1%的精英和99%的无用的人 > 人类将失去在经济和军事上的用途,因此经济和政治制度可能将不再继续认同人类有太多价值。 > 21世纪经济学最重要的问题,可能就是多余的人能有什么用?一旦拥有高度智能的算法接手几乎一切工作,而且能比人类做的更好时,人类还能做什么? 前段时间看到一个TED演讲,未来世界各地可能像某些北欧国家一样,发基础工资,所有人都有基本工资来消费,而经济的马车由机器拉动。我们看到现在国家在搞扶贫,其实就是发社会基本工资。只不过还够不上发达国家而已。 社会的贫富差距会加大。绝大多数人将会沦为无用的人。我们看到整个社会逐渐淘汰了一些简单的工作。例如扫马路、看守停车场。不幸的是,从事这些岗位的恰恰是低收入者。这些事情都在身边真实发生。 除了计算机互联网的发展、另外一个进步巨大,却又常常被忽视的领域是生物科技,被称为上帝剪刀的基因编辑可能因为法律和伦理问题还需时日。但基于基因组测序和大数据做癌症筛查是现在的精英阶层已经流行的。由此超人类可能和传统的人产生历史上从未发生的**生理差距**。 ## 饥荒、瘟疫和战争 就在几十年前,饥荒、瘟疫和战争是人类的头号敌人。但是,到了今天: > 2010年,饥饿和营养不良合计夺走了约100万人的生命,但肥胖却让300万人丧命。 > 虽然我们不敢保证绝不会爆发新一波埃博拉或未知流感病毒疫情,横扫全球造成数百万人死亡,但至少我们不会认为这是不可避免的自然灾害。相反,我们会认为这是一个不可原谅的认为疏忽,要有人为此负起责任。 > 2012年,全球约有5600万人死亡,其中62万人死于暴力(战争致死12万,犯罪致死50万),相较之下,自杀的有80万,死于糖尿病的更是有150万。现在,糖比火药更致命。 ## 生命就是算法 > 人类历史就是数据处理过程。 人不仅在处理数据,还在产生数据。你每天都在看新闻,刷微博,同时也在发微博,发朋友圈,记录、上传和分享。 如今,各种微博,朋友圈,公众号,头条,推送。 以前,价值来源于获取信息的能力。现在,价值来源于过滤信息的能力。 ## 意识与智能分离 > 任何一匹马,不论是嗅觉、爱的能力、认人的能力、跳过栅栏的能力或是其他上千件事情上,绝对都远高于历史上首款平民汽车福特T型车,或是价值千万的兰博基尼。然而马还是被汽车取代,原因在于汽车在社会系统真正需要的少数几个领域胜出。 事实证明高度的智能并不需要意识的参与。 ## 大数据和人工智能将比我们更加了解自己 当机器的选择比我们更好时,我们还有自己的独立人格吗,亦或是裹挟在数据浪潮中的一小片涟漪。 --- ## RxJava入门 > 原文: https://wycode.cn/blog/rxjava | Agent 文档: https://wycode.cn/blog/rxjava.md | 日期: 2018-01-20 | 分类: Mobile ![ReactiveX](https://cdn-ssl-devio-img.classmethod.jp/wp-content/uploads/2021/08/ReactoveX.png) > RxJava是`ReactiveX`的Java分支,除了Java版,`ReactiveX`还支持包括JavaScript、C#、C++、Swift、Python、Android、Unity、Kotlin、Go、PHP在内的众多平台和语言。 > ReactiveX的核心是对观察者模式的扩展,并上升到了和命令式编程完全不同的编程方式的高度,称为`响应式编程`,优势在于处理异步、基于事件的应用程序。可以将数据或是事件都加队列中处理,优雅地解决了以前多线程交互、IO、并发、同步、异常处理这种编程的老大难问题。再加上`Lambda`表达式和链式编程,让程序变得更加简洁优雅。 看完了RactiveX的简介,觉得用它来封装一个网络请求框架应该是再好不过了,但是当我真正开始封装网络请求时,我发现:RxJava给的答案是,你并不需要去封装,而是**直接使用**就好了。实例代码如下。 ## Hello RxJava ```Java public void onClick(View v) { Observable.create((ObservableOnSubscribe) emitter -> emitter.onNext(httpRequest())) //1. .subscribeOn(Schedulers.io()) //2. .observeOn(AndroidSchedulers.mainThread()) //3. .subscribe(s -> textView.setText(s)); //4. } ``` 核心代码就是`OnClick`方法中的四行。 步骤如下: 1. 通过`create`函数创建`Observable`对象 2. 订阅调度到`Schedulers.io()`,`io`是一个有线程缓存的新线程调度器 3. 将事件消费调度到Android的主线程上 4. 消费者(观察者)代码 `httpRequest()`没用到任何复杂的框架,就用原生的`HttpURLConnection`做一个网络请求,返回请求结果的String,代码如下: ```Java private String httpRequest() throws Exception { URL url = new URL("http://wycode.cn/"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); String result; try { BufferedInputStream in = new BufferedInputStream(urlConnection.getInputStream()); result = readStream(in); } finally { urlConnection.disconnect(); } return result; } private String readStream(BufferedInputStream in) { BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuilder sb = new StringBuilder(); String line; try { while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } } catch (IOException e) { e.printStackTrace(); } finally { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } return sb.toString(); } ``` ## 变换操作 通常的,我们在请求网络后要对服务器返回的字符串进行解析,这时我们可以使用RxJava的`Transforming`操作,在返回前将JSON String转为实体类。 ```Java Observable.create((ObservableOnSubscribe) emitter -> emitter.onNext(httpRequest("https://wycode.cn/web/api/public/hello?message=Hello"))) //1. .map(s -> JSON.parseObject(s,Message.class)) //解析JSON .subscribeOn(Schedulers.io()) //2. .observeOn(AndroidSchedulers.mainThread()) //3. .subscribe(message -> textView.setText(message.message)); //4. ``` 可以看到在原有代码几乎没变的情况下,加上1行代码就实现了将Json转为实体再处理的需求。这就是RxJava的魅力。 完整源码在这里:https://github.com/wangyucode/rxjava 而以上也仅仅用到了RxJava的冰山一角,还有各种各样的函数来满足各种各样的需求。 函数列表在这里:http://reactivex.io/documentation/operators.html --- ## React框架原理的思考 > 原文: https://wycode.cn/blog/react | Agent 文档: https://wycode.cn/blog/react.md | 日期: 2018-01-06 | 分类: Frontend ![React](/images/20180213_react.png) > React框架的整体思路是将DOM元素彻底改为由JS代码渲染,通过组件(`Component`)的层层嵌套,最终由`ReactDOM.render()`渲染到页面上。 React框架的核心概念: - 元素(`Element`) Recat认为元素是不可变的,创建后不能修改。React使用`JSX`技术将HTML代码直接作为常量使用。 - 组件(`Component`) 组件是对元素的封装。拥有自身的生命周期函数。 - 属性(`Props`) 属性可以看作常量,用于构建特定的组件。 - 状态(`State`) 组件通过状态维护自身变化。组件通过调用`setState()`方法重新渲染。 React的理念是采取[单一责任原则](https://en.wikipedia.org/wiki/Single_responsibility_principle "Wiki"),自顶向下设计,一层层的拆分成只做一件事的`Component`。 在做一个实际的项目时,采取先使用假数据画页面的方式,这个过程中只需要关心数据模型和页面。而不去关心逻辑交互。 此时`Component`为什么要分成`Props`和`State`就显而易见了,做页面时只关心`Props`,做交互时只关心`State`。 --- ## Angular框架原理的思考 > 原文: https://wycode.cn/blog/angular | Agent 文档: https://wycode.cn/blog/angular.md | 日期: 2017-12-28 | 分类: Frontend ![Angular](https://angularjs.org/img/AngularJS-large.png) > Angular 框架的整体思路,很像 Spring 这样的后台框架。一层层的分解、解耦,天下框架莫不如此。 Angular 有 3 个重要概念: - 模块(Module) - 组件(Component) - 路由(Router) Angular 通过将大型项目分割成一个个`模块`,松耦合,便于扩展和维护。 所谓`组件`,就是一个 MVC 块,每个模块会有很多组件,组件代表了一个页面,可以嵌套,因此可以更加细分下去,通过元数据做双向绑定。 所谓`路由`,就是一个分发器,不同的路径加载不同的模块或组件即可。 所谓`元数据`,就是 Java 注解。 Spring 那套,控制反转(IOC)依赖注入(DI)也被引入进来。 将业务逻辑封装到`Service`里,然后注入到各个组件中使用。业务代码就彻底被隔离到 Service 里了,不会侵入框架整体设计。 关于 Webpack:Webpack 在打包 Web 工程时,可以找到资源之间的引用,最小化压缩合并 CSS,JavaScript 文件。 --- ## 使用JWT和Spring Security结合开发一套认证鉴权系统 > 原文: https://wycode.cn/blog/jwt | Agent 文档: https://wycode.cn/blog/jwt.md | 日期: 2017-11-17 | 分类: Backend ![jwt](https://ts1.cn.mm.bing.net/th/id/R-C.db4d0c221a205b33b6296fe6412bfe91?rik=%2fIH2gS%2fgcubddQ&riu=http%3a%2f%2fordina-jworks.github.io%2fimg%2fJWT%2fjwt-logo.png&ehk=jeV7Iwj5nNSP9Pl927GxkmOei90QHBPvQtKMY5A0KrM%3d&risl=&pid=ImgRaw&r=0&sres=1&sresct=1) 为使用前后端分离的后端管理框架做准备,使用JWT目前看来是最佳选择。 ## 知其然 - 无状态 - 服务化 - 移动、web统统支持,后端提供相关接口即可 ## 知其所以然 1. 认证:通过调用登录接口,服务器颁发一个`JWT`(JSON Web Token)给客户端。 2. 鉴权:客户端每个请求携带`JWT`,后端使用一个请求过滤器,从请求头中解析`JWT`,为本次请求授予相关权限 ## 导入`io.jsonwebtoken:jjwt` `jjwt`提供了JWT主要功能的Java版本实现,目前版本是0.9.0 ```json dependencies { compile 'io.jsonwebtoken:jjwt:0.9.0' } ``` ### 生成token ```java private String generateToken(Map claims) { return Jwts.builder() .setClaims(claims) //自定义声明 .setExpiration(generateExpirationDate()) //过期时间 .signWith(SignatureAlgorithm.HS512, secret) //加密 .compact(); } ``` ### 解析token ```java private Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { claims = null; } return claims; } ``` ## 鉴权过滤器 ```java package cn.wycode.web.config.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.header}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader(this.tokenHeader); if (authHeader != null && authHeader.startsWith(tokenHead)) { final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer " String account = jwtTokenUtil.getUsernameFromToken(authToken); logger.info("checking authentication " + account); if (account != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(account); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( request)); logger.info("authenticated user " + account + ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } } ``` ### 配置Spring security ```java httpSecurity.addFilterBefore(authenticationTokenFilterBean(),UsernamePasswordAuthenticationFilter.class); ``` --- ## 使用Vue搭建一个后台管理系统 > 原文: https://wycode.cn/blog/vue | Agent 文档: https://wycode.cn/blog/vue.md | 日期: 2017-11-16 | 分类: Frontend ![vue](https://tse4-mm.cn.bing.net/th/id/OIP-C.CPDIH8BWrGipHRJ6o6E2VwHaEc?pid=ImgDet&rs=1) > 选择往往比努力更加重要。 准备做一个前后端分离的Web管理系统。 ## Getting Started ### 从`vue-cli`开始 ```bash # 全局安装 vue-cli $ npm install --global vue-cli # 创建一个基于 webpack 模板的新项目 $ vue init webpack my-project # 安装依赖,走你 $ cd my-project $ npm install $ npm run dev ``` --- ## 安装Node.js和npm的最佳实践 > 原文: https://wycode.cn/blog/nodejs | Agent 文档: https://wycode.cn/blog/nodejs.md | 日期: 2017-11-14 | 分类: Frontend ![nodejs](https://www.metachris.com/images/posts/nodejs-logo-wide.png) ## Linux平台 使用cURL: ```bash $ curl https://raw.github.com/creationix/nvm/master/install.sh | sh ``` 使用Wget:(Ubuntu默认没有安装cURL,推荐使用Wget) ```bash $ wget -qO- https://raw.github.com/creationix/nvm/master/install.sh | sh ``` 安装完成后,重启终端并执行下列命令即可安装 Node.js。 ```bash $ nvm install stable ``` ## Windows平台 建议使用`.msi`安装程序进行安装。推荐勾选`Add to PATH`可以自动添加到环境变量,方便全局使用相关命令。 --- ## Spring Boot + Spring Security + H2 + Thymeleaf做一个后台管理系统 > 原文: https://wycode.cn/blog/web-framework | Agent 文档: https://wycode.cn/blog/web-framework.md | 日期: 2017-10-26 | 分类: Backend ![Spring](https://www.freelancinggig.com/blog/wp-content/uploads/2018/04/Spring-Framework-Guide.png) - Spring Boot能够创建独立的Spring应用程序,在云计算,分布式流行的今天,它提供了一种非常契合目前web开发需求的解决方案,是Java开发web应用的不二选择。 - Spring Security是一款功能强大,可定制的身份验证和访问控制框架。 - H2数据库是一个轻量级的数据库引擎,开源,快速,它可以嵌入到Java程序中,作为内存数据库,也可以持久化,并提供了一个web程序,可以可视化操作数据。使用H2的还有一个重要原因是我的服务器只有512M内存,我无法使用MySQL这样的重量级数据库。 - Thymeleaf是一个后台模版引擎,它是JSP的完美替代品,在快速渲染网页的基础上,它不会改变HTML代码结构,从而前端工程师仍然可以用假数据预览。Demo页面可以给后台人员直接使用。(当然并没有前端工程师,不过后期我会使用Vue做一个前后端分离的系统) 先偷个懒吧,直接上github仓库。。。 [https://github.com/wangyucode/WebFramework](https://github.com/wangyucode/WebFramework) --- ## 使用swagger为Spring REST API生成在线文档 > 原文: https://wycode.cn/blog/swagger | Agent 文档: https://wycode.cn/blog/swagger.md | 日期: 2017-10-23 | 分类: Backend ![Swagger](https://static1.smartbear.co/swagger/media/assets/images/swagger_logo.svg) > 本文介绍如何使用swaggerUI为Spring Boot项目创建动态的、从代码生成的REST API文档 ## 创建SwaggerUI渲染需要的JSON - 首先为项目添加生成JSON的库: ``` dependencies { compile "io.springfox:springfox-swagger2:2.7.0" } ``` - 添加`SwaggerConfig`类 ```java @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket generateDocket(){ return new Docket(DocumentationType.SWAGGER_2) .select() .paths(paths()) // and by paths .build() .apiInfo(apiInfo()); } //Here is an example where we select any api that matches one of these paths private Predicate paths() { return input -> input.matches(".api.*"); } private ApiInfo apiInfo() { return new ApiInfo("王郁的API文档", "提供App测试及独立APP研发", "2.0", "http://wycode.cn", new Contact("王郁","wycode.cn","wangyu0503@gmail.com"), "wycode.cn All Right Reserved", "http://wycode.cn", new ArrayList<>()); } } ``` - 访问`http://localhost:8080/v2/api-docs`可以看到JSON即说明配置成功: ```JSON { "swagger": "2.0", "info": { "description": "提供App测试及独立APP研发", "version": "2.0", "title": "王郁的API文档", "termsOfService": "http://wycode.cn", "contact": {}, "license": {} }, "host": "localhost:8080", "basePath": "/", "tags": [], "paths": { "/api/hello": { "get": {}, "head": {}, "post": {}, "put": {}, "delete": {}, "options": {}, "patch": {} } }, "definitions": {} } ``` - 添加SwaggerUI包 从`SwaggerUI`的github上下载最新版,将里面的`dist`文件夹,放入我们工程的静态页面目录 > 因为我们要修改`index.html`,所以不能使用`webjars` 对`index.html`的常量`ui`做如下更改 ```JavaScript // Build a system const ui = SwaggerUIBundle({ url: "/v2/api-docs", //API的JSON地址 dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }) ``` ## 大功告成! ## 添加文档注释 - 实体注解 ```Java @ApiModel(value = "Hello 消息",description = "Hello接口返回的消息实体") public class Hello { @ApiModelProperty("消息") private String message; ... } ``` - 接口注解 ```java @Api(value = "Hello", description = "第一个api",tags = "Hello") @RestController public class HelloController { @ApiOperation(value = "Say Hello", produces = "application/json ") @RequestMapping("/api/hello") public Hello hello(@ApiParam(value = "消息",defaultValue = "Hello World!") @RequestParam(name = "message",defaultValue = "Hello World!")String message){ return new Hello("message is : "+ message); } } ``` --- ## Android度量设计规范 > 原文: https://wycode.cn/blog/android-dp | Agent 文档: https://wycode.cn/blog/android-dp.md | 日期: 2017-09-04 | 分类: Mobile ![Android](https://img-blog.csdnimg.cn/c5c68cae3bf640eaa2a3ec03fc518b1c.png) > 有关 dpi、dp、px、sp、设计规范等等。 ## Pixel density (dpi) 像素密度,表示每英寸的长度上有多少像素 `dpi = sqrt(像素宽度^2+像素高度^2)/屏幕尺寸` 以下是常见手机的 dpi | 屏幕尺寸 | 720\*1280 | 1080\*1920 | 1440\*2560 | | :------: | :-------: | :--------: | :--------: | | 5 寸 | 293.7 | 440.6 | 587.4 | | 5.5 寸 | 267.0 | 400.5 | 534.0 | | 6 寸 | 244.8 | 367.2 | 489.5 | ## Density-independent pixels (dp) 引入 dpi 的概念就是为了使用像素无关的单位`dp`,从而使不同分辨率和尺寸的手机上达到一样的观感 `dp = 像素*160/dpi` 160dpi 的手机上,1px=1dp ### 图片缩放 | Screen resolution | dpi | Pixel ratio | Image size (pixels) | | :---------------: | :-: | :---------: | :-----------------: | | xxxhdpi | 640 | 4.0 | 400 x 400 | | xxhdpi | 480 | 3.0 | 300 x 300 | | xhdpi | 320 | 2.0 | 200 x 200 | | hdpi | 240 | 1.5 | 150 x 150 | | mdpi | 160 | 1.0 | 100 x 100 | ## Scaleable pixels (sp) sp 和 dp 差不多只是用于字体 14sp 为标准大小 ![sp](/images/20170904_sp.png) | xx_small | x_small | small | medium | large | x_large | xx_large | xxx_large | | :------: | :-----: | :---: | :----: | :---: | :-----: | :------: | :-------: | | 8sp | 10sp | 12sp | 14sp | 16sp | 18sp | 20sp | 22sp | ## 基准网络 所有组件都与间隔为 8dp 的基准网格对齐。排版/文字(Type)与间隔为 4dp 的基准网格对齐。在工具条中的图标同样与间隔为 4dp 的基准网格对齐。这些规则适用于移动设备、平板设备以及桌面应用程序。 ![baselinegrids](/images/20170905_layout-metrics-baselinegrids.png) --- ## Gradle构建变种版本(多渠道打包) > 原文: https://wycode.cn/blog/gradle-build | Agent 文档: https://wycode.cn/blog/gradle-build.md | 日期: 2017-09-04 | 分类: Mobile ![Gradle](https://ts1.cn.mm.bing.net/th/id/R-C.da7d3edf2afaf378bff91a21e6385091?rik=kWTVUo%2be2O8bbg&riu=http%3a%2f%2ftheeye.pe.kr%2fwp-content%2fuploads%2f2014%2f02%2fgradle.png&ehk=5NJHqrr5W7rLhp4CEUQIadjJ83TfKvlokTsc%2bzFFiLo%3d&risl=&pid=ImgRaw&r=0) > 构建变体是 Gradle 按照特定规则集合进行打包,以生成适应不同需求的Apk包。 例如,需要生成dev版,测试版、以及release版,它们的服务器地址,版本号,图标各不相同,以及多渠道打包等等。 ## productFlavors 可以定义多个`productFlavors`版本,其继承覆盖所有`defaultConfig`中的字段 主要的使用手段有一下几种 - 覆盖`defaultConfig`默认字段(包名、版本名等) - 往`AndroidManifast.xml`文件注入编译变量 - 修改包名,支持前后缀 - 使用`buildConfigField`动态改变编译时常量 - 使用`resValue`动态注入字符串资源 ## 示例 ``` android { ... defaultConfig { versionCode 200 versionName 'V2.0.0' manifestPlaceholders = [ scheme: "FindAR" id : "1565483848" ] buildConfigField "String", "ROOT_FOLDER_NAME", '"FindAR"' } productFlavors{ dev{ versionName 'V2.0.0 Dev' resValue "string", "user_id", "001" //user id } production{ versionName 'V2.0.0 Release' resValue "string", "user_id", "002" //user id } } } ``` --- ## Kotlin入门 > 原文: https://wycode.cn/blog/kotlin-learn | Agent 文档: https://wycode.cn/blog/kotlin-learn.md | 日期: 2017-07-27 | 分类: Mobile > 在Google I/O 2017上Google正式宣布Kotlin成为Android开发的官方新语言,Kotlin得到了Google的背书,随着Android O和Android Studio3.0的正式发布,Kotlin语言的春天可能马上就要来临了。 ![Kotlin](/images/20170815_kotlin.png) ## Kotlin Kotlin是基于JVM,静态类型的跨平台语言,在Java语言的基础上引入了很多现代语言的新特性。支持后台Server端、JavaScript前后端、及移动Android端开发。 ## 新特性 - Null Safety Kotlin解决了Java错误量最多的`NullPointerException` ``` var a: String = "abc" a = null // error!对于常量,编译时就不能为空,否则编译出错 var b: String? = "abc" //String? 可为空的 String 类型 b = null // ok! val l = b.length // error: variable 'b' can be null //除了传统的if(b!=null)判空后再调用之外还有安全引用方式 b?.length // //对于以下操作 val l: Int = if (b != null) b.length else -1 //可以简写为 ?: (Elvis Operator)后面也可以跟reture和throw语句 val l = b?.length ?: -1 //安全转型 val aInt: Int? = a as? Int //可以为空的数组,以及非空筛选 val nullableList: List = listOf(1, 2, null, 4) val intList: List = nullableList.filterNotNull() ``` - 基础数据类型没有`Character` - `Char`被单独处理,不是数字,不能和数字进行比较,可以使用`toInt()`函数 - 没有8进制 - 数组类型不可变,不能将`Array`赋给`Array` - 以下是位运算,只支持`Int`和`Long` ```kotlin shl(bits) – signed shift left (Java's <<) shr(bits) – signed shift right (Java's >>) ushr(bits) – unsigned shift right (Java's >>>) and(bits) – bitwise and or(bits) – bitwise or xor(bits) – bitwise xor inv() – bitwise inversion ``` - 没有静态方法,可以使用 `object declaration`替代 --- ## Spring中启用CORS > 原文: https://wycode.cn/blog/spring-cors | Agent 文档: https://wycode.cn/blog/spring-cors.md | 日期: 2017-07-27 | 分类: Backend ![Spring](https://tse3-mm.cn.bing.net/th/id/OIP-C.X0Up2Pdq1ZDskAe51Uac4gHaE8?pid=ImgDet&rs=1) > JavaScript在跨域请求时会碰到`No 'Access-Control-Allow-Origin' header is present on the requested resource.`。是因为服务器不支持`Cross-origin resource sharing (CORS)`这篇教程教你如何为Java Spring后台配置`CORS` ## 使用`@CrossOrigin`注解 注解相关方法即可。 ## 全局配置 ```java @Configuration public class MyConfiguration { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**"); } }; } } ``` --- ## 在Unity中使用Android原生代码 > 原文: https://wycode.cn/blog/unity2android | Agent 文档: https://wycode.cn/blog/unity2android.md | 日期: 2017-07-26 | 分类: Mobile ![Unity](https://tse1-mm.cn.bing.net/th/id/OIP-C.TPnSpv0PowWa1j_0SdXeWQHaEM?pid=ImgDet&rs=1) > 在某些情况下,我们需要使用原生代码来做一些事,比如集成一些第三方原生SDK, 以下这种方式介绍了如何将Unity导出成Android Studio工程,从而添加Android代码。 之后的教程中我还将介绍如何将Android写好的代码导回到Unity工程中。 ## Unity工程导出为Android工程 1. 在Unity菜单中选择:File——Build Settings 2. 为了能在Android Studio中更好的运行,构建系统选择Gradle 3. 选中Export Project选项 4. 选择你喜欢的目录导出 5. Unity发生了改动时,只需导出后替换asset文件夹内容即可 ![Export](/images/20170726_export.png) ## Unity调用Android方法 ```csharp private void sendMessageToAndroid(string method,params object[] args) { AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject jo = jc.GetStatic("currentActivity"); jo.Call(method, args); } ``` - 在UnityPlayerActivity中准备好Unity要调用的方法 - 调用上面的方法,传入方法名(不带括号),及参数(多参,也可不传)即可。 ## Android调用Unity方法 ```java UnityPlayer.UnitySendMessage("GameObject", "message", "args"); ``` - 相当于调用了Unity中的`sendMessage()`。 --- ## 自定义形状波浪进度 > 原文: https://wycode.cn/blog/wave-progress | Agent 文档: https://wycode.cn/blog/wave-progress.md | 日期: 2017-07-24 | 分类: Mobile > 今天碰到个需求,要求以波浪进度填充自定义透明形状,废话不多说,直接上最终效果图 ![波浪效果](/images/20170724_wave_progress.gif) ## 思路 通过判断`Bitmap`对象的像素颜色,对非透明像素进行颜色更改。波浪曲线采用sin曲线π到2π区间段,和将时间取余并和x坐标相加得到波浪y轴坐标,再加上百分比*Bitmap高度。核心算法如下: ![sin](/images/20170724_sin.png) ```java private int getWaveY(int x) { double sinX = Math.PI + (((((System.currentTimeMillis() / (1000 / waveSpeed)) + x) % waveLength)) / (double) waveLength * Math.PI); int waveHeight = (int) (waveHeightMax * (Math.sin(sinX) + 1)); int y = (int) ((100 - progress) / 100f * bitmap.getHeight()); y -= waveHeight; return y; } ``` 这里的`waveSpeed`、`waveLength`、`waveHeightMax`均定义成全局变量,可以很方便的定制波浪移动速度、波浪长度、波浪高度。 ## 使用SurfaceView 为了达到比较好的性能,使用`SurfaceView`在子线程绘图。 ## 性能优化 因为这里涉及逐个像素操作,所以遍历的运算量很大,一定要对遍历的量进行控制,否则会很卡。 我这里采用控制图片大小的方式 ```java *** bitmap = setBitmapSize(bitmap, calculateSize * bitmap.getWidth() / bitmap.getHeight(), calculateSize); *** /** * 缩放图片 * * @param bitmap 原图片 * @param newWidth * @param newHeight * @return */ private Bitmap setBitmapSize(Bitmap bitmap, int newWidth, int newHeight) { int width = bitmap.getWidth(); int height = bitmap.getHeight(); float scaleWidth = (newWidth * 1.0f) / width; float scaleHeight = (newHeight * 1.0f) / height; Matrix matrix = new Matrix(); matrix.postScale(scaleWidth, scaleHeight); return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true); } ``` 通过`calculateSize`控制循环次数在有限的范围内,并在最终绘制Bitmap时使用区域绘制函数: ```java private void draw() { Canvas canvas = mHolder.lockCanvas(); canvas.drawBitmap(bitmap, rectBitmap, rectCanvas, mPaint); mHolder.unlockCanvasAndPost(canvas); } ``` ## 注意事项 - 为了达到`SurfaceView`透明效果,需要加入以下两句代码 ```java setZOrderOnTop(true); mHolder.setFormat(TRANSLUCENT); ``` - 从`Drawable`中得到的`Bitmap`不能直接`setPixel()`,必须重新`Bitmap.createBitmap()`,或`Bitmap.copy()`,使得`isMutable`属性为`true`,得到新的`Bitmap`对象才能操作像素,否则会报`IllegalStateException` ## 完整源码: [https://github.com/wangyucode/WaveProgress](https://github.com/wangyucode/WaveProgress) --- ## Blender建模入门 > 原文: https://wycode.cn/blog/blender-learn | Agent 文档: https://wycode.cn/blog/blender-learn.md | 日期: 2017-07-18 | 分类: Life ![Blender](/images/20170717_blender_logo.png) > Blender 是一款开源的跨平台全能三维动画制作软件,永久免费。 在这篇文章中你将学到 Blender 3D 建模入门。 Blender 官方中文手册:[https://docs.blender.org/manual/zh.cn/dev/](https://docs.blender.org/manual/zh.cn/dev/) ## 用户界面 ![用户界面](/images/20170718_blender_interface.jpg) - `T`打开、关闭工具栏(左侧) - `N`打开、关闭属性栏(右侧) - `Shift`+`Space`最大化当前窗口 ## 设置为中文 - 在`File`-`User preferences`-`System`勾选`International Fonts`,并选择下方的`Interface`和`Tooltips` ## 选择 - 在 Blender 中**右键**是选择物体 - 可以在`File`-`User preferences`-`Input`中切换成左键或右键选择 ## 导航 - 按住`鼠标中键`移动鼠标可以旋转视角 - 按住`鼠标中键`+`Shift`可以上下左右移动(不旋转) - `Shift`+`F`可以进入飞行模式,可以使用`W`、`A`、`S`、`D`、`Q`、`E`移动视角 - `鼠标滚轮`缩放视角 --- ## Unity中的JSON解析 > 原文: https://wycode.cn/blog/unity-json | Agent 文档: https://wycode.cn/blog/unity-json.md | 日期: 2017-07-12 | 分类: Mobile ![Unity](https://tse1-mm.cn.bing.net/th/id/OIP-C.TPnSpv0PowWa1j_0SdXeWQHaEM?pid=ImgDet&rs=1) > JSON是一种应用广泛的数据交换格式。可用于和Web服务器交互,以及用于对象序列化。Unity使用`JsonUtility`类提供JSON文本和对象互转。 下面将基本的使用总结如下: ## 使用 `Serializable`标记对象 ```csharp [Serializable] public class WyResultGameScore{ public int code;//(integer, optional): code: 返回代码,1表示OK,其它的都有对应问题 , public List data;//(Array[GameScore], optional): data : code为1时返回结果集 , public string message;//(string, optional): message : code!=1时的错误信息 } [Serializable] public class GameScore { public int gameId;//(integer, optional): 游戏id 1=滚蛋吧 , public int id;//(integer, optional): 分数id , public string name;//(string, optional): 昵称 , public string platform;//(string, optional): 游戏平台 , public int score;//(integer, optional): 分数 , public string userId;//(string, optional): userId } ``` ## JSON string转Object ```csharp string json = "{"code":1,"message":"成功","data":[{"id":2,"name":"游客9527","score":10,"platform":"iOS","userId":"1","gameId":1}]}"; WyResultGameScore result = JsonUtility.FromJson(json); ``` ## Object转JSON string ```csharp string json = JsonUtility.ToJson(myObject); ``` --- ## Unity中的网络请求 > 原文: https://wycode.cn/blog/unity-web-request | Agent 文档: https://wycode.cn/blog/unity-web-request.md | 日期: 2017-07-12 | 分类: Mobile ![Unity](https://tse1-mm.cn.bing.net/th/id/OIP-C.TPnSpv0PowWa1j_0SdXeWQHaEM?pid=ImgDet&rs=1) > Unity推荐使用`UnityWebRequest`替代原有`WWW`类做网络请求,`UnityWebRequest`提供了非常简单的方法请求RESTful接口。 下面将基本的使用总结如下: ## 一个典型的POST请求 ```csharp public void showScore() { StartCoroutine(getTop10Score()); } private IEnumerator getTop10Score() { WWWForm form = new WWWForm(); form.AddField("gameId", "1"); using (UnityWebRequest www = UnityWebRequest.Post("http://wycode.cn/api/score/getTopScores", form)) { yield return www.Send(); if (www.isError) { Debug.Log(www.error); } else { Debug.Log(www.downloadHandler.text); } } } ``` - 使用协程处理网络请求。 - `WWWForm`中可以添加参数。 - `using`代码块表示结束立即回收其中的对象。 - `DownloadHandler`对象中可以找到服务器返回的结果。 --- ## 使用SpringJPA框架 > 原文: https://wycode.cn/blog/spring-jpa | Agent 文档: https://wycode.cn/blog/spring-jpa.md | 日期: 2017-07-11 | 分类: Backend ![Spring JPA](https://tse3-mm.cn.bing.net/th/id/OIP-C.BJ09SzSPlUf5QzF_EryfdwAAAA?pid=ImgDet&rs=1) > Spring JPA框架提供了非常强大的数据库持久化方案,仅仅通过定义接口就能实现很多查询功能,甚至不用写具体实现代码。通过接口定义的抽象方法Spring JPA就知道该做什么了。 下面将基本的使用总结如下: ## 接口能力 - 定义接口继承`CrudRepository`即可获得增删改查能力; - 定义接口继承`PagingAndSortingRepository`获得增删改查能力的同时增加分页查询能力; 声明后可以直接使用`@Autowired`注入使用,无需实现。 ## 关键词 - `find…By` - `count…By` - `Distinct` - `IgnoreCase` - `AllIgnoreCase` - `And` - `Or` - `Between` - `LessThan` - `GreaterThan` - `Like ` - `OrderBy` - `Asc` - `Desc` - `OrderBy` ## 一些例子 ```java //简单示例 List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); // 去重标志 List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); // 对某个字段忽略大小写 List findByLastnameIgnoreCase(String lastname); // 对所有字段忽略大小写 List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); // 静态 ORDER BY 查询 List findByLastnameOrderByFirstnameAsc(String lastname); List findByLastnameOrderByFirstnameDesc(String lastname); //分页 Page findByLastname(String lastname, Pageable pageable); Slice findByLastname(String lastname, Pageable pageable); List findByLastname(String lastname, Sort sort); List findByLastname(String lastname, Pageable pageable); //限制查询结果 User findFirstByOrderByLastnameAsc(); User findTopByOrderByAgeDesc(); Page queryFirst10ByLastname(String lastname, Pageable pageable); Slice findTop3ByLastname(String lastname, Pageable pageable); List findFirst10ByLastname(String lastname, Sort sort); List findTop10ByLastname(String lastname, Pageable pageable); //通过userId和gameId查询GameScore实体 GameScore findByUserIdAndGameId(String userId,int gameId); //通过gameId查询分页数据,并根据score字段倒序排序 List findByGameIdOrderByScoreDesc(int gameId,Pageable page); //通过gameId计数,条件是大于传入的score int countByGameIdAndScoreGreaterThan(int gameId,long score); ``` --- ## Spring中的application.properties > 原文: https://wycode.cn/blog/spring-properties | Agent 文档: https://wycode.cn/blog/spring-properties.md | 日期: 2017-07-11 | 分类: Backend ![Spring](https://tse3-mm.cn.bing.net/th/id/OIP-C.X0Up2Pdq1ZDskAe51Uac4gHaE8?pid=ImgDet&rs=1) > Spring框架在运行时会自动查找`application.properties`配置文件,在这个文件中进行项目相关配置属性,例如数据库地址密码等。当然也支持自定义属性。 ## 位置 `application.properties`可以放置在以下目录 - 当前目录下的`/config`目录中 - 当前目录 - classpath下的 /config包 - classpath - **resource文件夹** ## 自定义属性的使用 1. 在`application.properties`添加自定义属性`myProperties = myValue` 2. 使用`@Value`注解即可注入使用 ```java @Value("${myProperties}") String value; //value = "myValue"; ``` 3. 还可以使用`@ConfigurationProperties("foo")`注入POJO实体 ```java @ConfigurationProperties("foo") public class Foo{ private boolean enabled; public boolean isEnabled() { ... } public void setEnabled(boolean enabled) { ... } } ``` 在`application.properties`添加属性`foo.enabled = true`即可 --- ## Unity脚本生命周期 > 原文: https://wycode.cn/blog/unity-lifecycle | Agent 文档: https://wycode.cn/blog/unity-lifecycle.md | 日期: 2017-07-07 | 分类: Mobile ![Unity](https://tse1-mm.cn.bing.net/th/id/OIP-C.TPnSpv0PowWa1j_0SdXeWQHaEM?pid=ImgDet&rs=1) > Unity脚本的生命周期,协程、各关键函数的调用顺序如下图所示: ![Unity脚本生命周期](/images/20170707_monobehaviour_flowchart.svg) --- ## Unity平台判断 > 原文: https://wycode.cn/blog/unity-platform | Agent 文档: https://wycode.cn/blog/unity-platform.md | 日期: 2017-06-27 | 分类: Mobile ![Unity](https://tse1-mm.cn.bing.net/th/id/OIP-C.TPnSpv0PowWa1j_0SdXeWQHaEM?pid=ImgDet&rs=1) > Unity 平台的判断包含编译时判断和运行时判断,总结如下: ## 编译时 ```csharp #if UNITY_EDITOR Debug.Log("Unity Editor"); #endif #if UNITY_IOS Debug.Log("Iphone"); #endif #if UNITY_STANDALONE_OSX Debug.Log("Stand Alone OSX"); #endif #if UNITY_STANDALONE_WIN Debug.Log("Stand Alone Windows"); #endif ``` 可以使用`||`和`&&`进行组合 ### 平台定义 - UNITY_EDITOR - UNITY_EDITOR_WIN - UNITY_EDITOR_OSX - UNITY_STANDALONE_OSX - UNITY_STANDALONE_WIN - UNITY_STANDALONE_LINUX - UNITY_STANDALONE - UNITY_WII - UNITY_IOS - UNITY_IPHONE - UNITY_ANDROID - UNITY_PS4 - UNITY_SAMSUNGTV - UNITY_XBOXONE - UNITY_TIZEN - UNITY_TVOS - UNITY_WP_8_1 - UNITY_WSA - UNITY_WSA_8_1 - UNITY_WSA_10_0 - UNITY_WINRT - UNITY_WINRT_8_1 - UNITY_WINRT_10_0 - UNITY_WEBGL - UNITY_ADS - UNITY_ANALYTICS - UNITY_ASSERTIONS 官方文档:[https://docs.unity3d.com/Manual/PlatformDependentCompilation.html](https://docs.unity3d.com/Manual/PlatformDependentCompilation.html "https://docs.unity3d.com/Manual/PlatformDependentCompilation.html") ## 运行时 ```csharp if(Application.platform == RuntimePlatform.WindowsWebPlayer) { Debug.Log("Windows Web Player"); } ``` ### 平台枚举 - OSXEditor - OSXPlayer - WindowsPlayer - OSXDashboardPlayer - WindowsEditor - IPhonePlayer - Android - LinuxPlayer - LinuxEditor - WebGLPlayer - WSAPlayerX86 - WSAPlayerX64 - WSAPlayerARM - TizenPlayer - PSP2 - PS4 - XboxOne - SamsungTVPlayer - WiiU - tvOS - Switch 官方文档:[https://docs.unity3d.com/ScriptReference/RuntimePlatform.html](https://docs.unity3d.com/ScriptReference/RuntimePlatform.html "https://docs.unity3d.com/ScriptReference/RuntimePlatform.html") --- ## 可能是史上最简单的vi/vim教程 > 原文: https://wycode.cn/blog/linux-vi | Agent 文档: https://wycode.cn/blog/linux-vi.md | 日期: 2017-06-21 | 分类: DevOps ![vim](https://tse1-mm.cn.bing.net/th/id/OIP-C.S7PWetAEkXIKI3Fgncz96wHaFF?pid=ImgDet&rs=1) ## vi特性 - vi编辑器包含两种模式,命令模式和输入模式 - 刚进入vi编辑器时是出于命令模式下 - 命令模式下的按键是对文件的命令 - 按`i`进入输入模式 - 按`Esc`退回到命令模式 ## 常用命令 | 命令 | 作用 | | ------------ | ------------ | | `q` | 退出vi | | `wq` | 保存并退出 | | `q!` | 强制退出(不保存) | | `j` | 光标向下 | | `k` | 光标向上 | | `h` | 光标向左 | | `l` | 光标向右 | | `0` | 移动光标到行首 | | `$` | 移动光标到行末 | | `u` | 撤销 | | `x` | 删除单个字符 | | `dd` | 删除整行 | | `yy` | 复制整行 | | `p` | 粘贴 | --- ## SEO:优化网站排名 > 原文: https://wycode.cn/blog/SEO | Agent 文档: https://wycode.cn/blog/SEO.md | 日期: 2017-06-19 | 分类: Frontend > 本文介绍SEO相关知识和实践 如何对搜索引擎的爬虫友好引导是关键 ## robots.txt 这个文件告诉爬虫,哪里可以去,哪里不能去。 ## Sitemap 直接给爬虫一张地图,不用费劲乱爬了 --- ## Android原生分享及第三方分享 > 原文: https://wycode.cn/blog/Share | Agent 文档: https://wycode.cn/blog/Share.md | 日期: 2017-06-16 | 分类: Mobile > 本文介绍如何为Android应用添加分享功能。包含通过原生`Intent`和利用第三方SDK。 ## 原生Intent方式 大多平台都支持隐式Intent方式发起分享。 > 创建隐式 Intent 时,Android 系统通过将 Intent 的内容与在设备上其他应用的清单文件中声明的 Intent 过滤器进行比较,从而找到要启动的相应组件。 如果 Intent 与 Intent 过滤器匹配,则系统将启动该组件,并向其传递 Intent 对象。 如果多个 Intent 过滤器兼容,则系统会显示一个对话框,支持用户选取要使用的应用。 ```java Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage); sendIntent.setType("text/plain"); Intent chooser = Intent.createChooser(sendIntent, "title"); if (sendIntent.resolveActivity(getPackageManager()) != null) { startActivity(chooser); } ``` 这里Action设置为`Intent.ACTION_SEND`就是分享(发送)的方式。 然后通过`putExtra()` 方式传递附加信息,可以是文本、图片等。 为了每次分享时都选择平台,需要为Intent包装一个Chooser。 然后就可以启动Activity分享了。 ## 单独集成第三方(微信、微博)SDK 不同的平台有所不同,一般都包括以下流程 - 注册开发者账号 - 填写应用信息,获取应用ID,Key等 - 导入相关SDK依赖 - 集成分享代码 ## 集成ShareSDK 集成ShareSDK的好处是,提供了统一的接口,抹平了不同平台间API的差异,集成更简单,维护也方便。特别是当你需要集成很多平台的时候。 但是上面单独集成的前两步还是不可缺少。 ## 集成微信分享的一些注意事项 有时会遇到错误`{"req":"e","errCode":-6,"transaction":"webpage1498805832655"}` - 微信分享需要经过签名才可以 - 微信Android应用信息的签名那一栏需要填入签名的**MD5**值,通过以下命令获取 ```bash keytool -list -v -keystore <你的keystore路径> ``` >`keytool` 是jdk的自带一个命令,类似`javac` - 签名的**MD5**值必须全部小写,并去掉冒号 - 可以在gradle文件中配置以下属性,让开发版本也使用正式签名 ``` android { *** signingConfigs { debug{ storeFile file("你的keystore路径") storePassword "你的storePassword" keyAlias "你的keyAlias" keyPassword "keyPassword" } release { storeFile file("你的keystore路径") storePassword "你的storePassword" keyAlias "你的keyAlias" keyPassword "你的keyPassword" } } *** } ``` 有时会遇到错误`checkArgs fail, thumbData is invalid` - 图片太大,可以获取缩略图 ```java Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.app_icon); Bitmap thumb = Bitmap.createScaledBitmap(bmp,THUMB_SIZE,THUMB_SIZE,true); bmp.recycle(); ``` --- ## Android Studio工程应该忽略的文件及如何使用gitignore > 原文: https://wycode.cn/blog/gitignore | Agent 文档: https://wycode.cn/blog/gitignore.md | 日期: 2017-06-16 | 分类: Mobile ![gitignore](https://git-scm.com/images/logo@2x.png) > 本文介绍版本控制中,Android Studio 工程应该忽略的文件,以及如何使用`.gitignore`文件快速配置自动忽略,教程同时适用于 Git 和 SVN。 ## 应该忽略的文件: ``` *.iml .gradle /local.properties /.idea .DS_Store **/build /captures .externalNativeBuild ``` - `.iml`是 IDEA 的 module 描述文件,和工程配置相关,在每一个 module 都有,需要忽略。\*表示忽略所有的`.iml` ### 使用 gitignore 这是最简单的方式 在项目根目录下创建`.gitignore`文件,将上面应该忽略的文件,内容复制到`.gitignore`中。 用 Android Studio 打开`.gitignore`文件时会在右上角弹出安装 gitignore 插件,点击安装即可,安装完成后就自动设置好忽略了,如果要添加忽略文件,只需要在`.gitignore`中添加文件路径即可。 ### 自己设置忽略 右键点击相应文件,选择相应 VCS 操作即可。 ### `gitignore`的官方模板仓库 此外 github 官方提供了一个仓库,里面包含了绝大多数平台,语言的 gitignore 模板 https://github.com/github/gitignore --- ## 为Hexo博客添加全文搜索 > 原文: https://wycode.cn/blog/search | Agent 文档: https://wycode.cn/blog/search.md | 日期: 2017-06-15 | 分类: Frontend > 本文介绍如何利用`hexo-generator-search`插件为 Hexo 博客添加本地全文搜索 ## 原理 `hexo-generator-search`实际上是根据网页内容生成了一个数据文件,格式是`XML`或者`JSON`。然后通过 JS 代码去匹配这个文件的内容,从而达到搜索的目的。 ## 最终效果 ![最终效果](/images/20170616_search_01.png "最终效果") ## 步骤 ### 安装 hexo-generator-search ```bash $ npm install hexo-generator-search --save ``` ### 配置 在`_config.yml`中添加 ```yaml search: path: search.xml field: post ``` ### 使用 再次生成博客,就会发现`public`根目录下多了`search.xml`文件。 ### 写一个搜索框: 使用`Bootstrap`框架的`modal`应该是极好的。 修改 Hexo 主题,(对此不了解的可以查看我的自定义 Hexo 主题的相关内容),在`layout.ejs`中添加一个 modal: ```html <%- partial('_partial/search-modal') %> ``` > 根据 Bootstrap 文档,modal 需要尽量作为 body 标签的直接子元素。 `search-modal.ejs`的代码如下: ```html ``` > 链接加上`data-toggle="modal" href="#searchModal"`属性即可实现 modal 的展开 ### 搜索代码: ```javascript // A local search script with the help of [hexo-generator-search](https://github.com/PaicHyperionDev/hexo-generator-search) // Copyright (C) 2015 // Joseph Pan // Shuhao Mao // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation; either version 2.1 of the // License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA // 02110-1301 USA // var searchFunc = function (path, search_id, content_id) { "use strict"; $.ajax({ url: path, dataType: "xml", success: function (xmlResponse) { // get the contents from search data var datas = $("entry", xmlResponse) .map(function () { return { title: $("title", this).text(), content: $("content", this).text(), url: $("url", this).text(), }; }) .get(); var $input = document.getElementById(search_id); var $resultContent = document.getElementById(content_id); $input.addEventListener("input", function () { var str = '
    '; var keywords = this.value .trim() .toLowerCase() .split(/[\s\-]+/); $resultContent.innerHTML = ""; if (this.value.trim().length <= 0) { return; } // perform local searching datas.forEach(function (data) { var isMatch = true; var content_index = []; if (!data.title || data.title.trim() === "") { data.title = "Untitled"; } var data_title = data.title.trim().toLowerCase(); var data_content = data.content .trim() .replace(/<[^>]+>/g, "") .toLowerCase(); var data_url = data.url; var index_title = -1; var index_content = -1; var first_occur = -1; // only match artiles with not empty contents if (data_content !== "") { keywords.forEach(function (keyword, i) { index_title = data_title.indexOf(keyword); index_content = data_content.indexOf(keyword); if (index_title < 0 && index_content < 0) { isMatch = false; } else { if (index_content < 0) { index_content = 0; } if (i == 0) { first_occur = index_content; } // content_index.push({index_content:index_content, keyword_len:keyword_len}); } }); } else { isMatch = false; } // show search results if (isMatch) { str += "
  • " + data_title + ""; var content = data.content.trim().replace(/<[^>]+>/g, ""); if (first_occur >= 0) { // cut out 100 characters var start = first_occur - 20; var end = first_occur + 80; if (start < 0) { start = 0; } if (start == 0) { end = 100; } if (end > content.length) { end = content.length; } var match_content = content.substr(start, end); // highlight all keywords keywords.forEach(function (keyword) { var regS = new RegExp(keyword, "gi"); match_content = match_content.replace( regS, '' + keyword + "" ); }); str += '

    ' + match_content + "...

    "; } str += "
  • "; } }); str += "
"; $resultContent.innerHTML = str; }); }, }); }; ``` ### 添加链接 ```html <%- js('js/search') %> ``` ### 高亮关键词 ```javascript keywords.forEach(function (keyword) { var regS = new RegExp(keyword, "gi"); data_title = data_title.replace( regS, '' + keyword + "" ); }); ``` ```css .search-keyword { color: #c7254e; } ``` --- ## 自定义Hexo主题 > 原文: https://wycode.cn/blog/theme | Agent 文档: https://wycode.cn/blog/theme.md | 日期: 2017-06-15 | 分类: Frontend ![Hexo](https://ts1.cn.mm.bing.net/th/id/R-C.15156de8ecdaca0e208f755572b5217b?rik=SCL92Je0wYHVXg&riu=http%3a%2f%2fheibanbai.com.cn%2fimg%2fhexo.png&ehk=VywdH%2f1oS1u8iXWWEm11T%2bm0ntJ9pczrqS%2bktzxU3Mo%3d&risl=&pid=ImgRaw&r=0) > 刚刚完成了Hexo博客主题的自定义,现在总结如下,让我们一起来看看如何打造一款属于自己的Hexo主题。 如何创建一个新主题以及如何切换主题在官网的[doc](https://hexo.io/zh-cn/docs/themes.html "doc")中有说明,这里我们只讨论如何打造一个新的属于自己的模版。 ## 方法 > Hexo使用了**模版**进行渲染的方式来实现网页风格的自定义。其核心是一种嵌套、引用的风格实现对网页内容模块化管理。所以我也采取从外向里,抽丝剥茧,剥洋葱式的一层层展开来看其中的原理及如何去自定义。此文需要基本的HTML知识。 ## 模版文件 所有的模版文件都放置在`layout`文件夹中,所有用到的资源(CSS,JavaScript,图片等)都放置在`source`文件夹中。 模版的根入口文件是`layout`下的`layout.ejs`文件。 > 这里的`.ejs`是模版采用的引擎,除了EJS,还有Haml和Jade,我大概看过Jade,比EJS更简洁,EJS更像HTML,我自己选择了EJS,原因是无需新学规则,还有就是官方提供的`landscape`也是采用的EJS,我的模版就是根据官方的`landscape`修改而来。 ## layout.ejs 我们先来看看这个`layout.ejs`的内容: ```html <%- partial('_partial/head') %> <%- partial('_partial/wycode-nav') %> <%- partial('_partial/blog-nav') %>
<%- body %>
<%- partial('_partial/footer', null, {cache: !config.relative_link}) %> <%- partial('_partial/after-footer') %> ``` 可以看到以上就是典型的HTML文件框架。 ### 重点来了: 这里唯一的区别就是多了很多`<%- %>`,凡是在这个`<%- %>`里面的部分就是EJS模版的代码部分,而`partial('_partial/head')`的意思就是引用了一个模块,位置是`_partial`文件夹的`head.ejs`。 ## head.ejs 然后我们看看这个`head.ejs`的内容: ```html <% var title = page.title; if (is_archive()) { title = __('archive_a'); if (is_month()) { title += ': ' + page.year + '/' + page.month; } else if (is_year()) { title += ': ' + page.year; } } else if (is_category()) { title = __('category') + ': ' + page.category; } else if (is_tag()) { title = __('tag') + ': ' + page.tag; } %> <% if (title){ %> <%= title %> | <% } %> <%= config.title %> <% if (theme.rss){ %> <% } %> <% if (theme.favicon){ %> <% } %> <%- css('flat/dist/css/vendor/bootstrap.min.css') %> <%- css('flat/dist/css/flat-ui.min') %> <%- css('font-awesome-4.7.0/css/font-awesome.min') %> <%- css('highlight/styles/monokai-sublime') %> <%- css('css/blog') %> ``` 可以看到这个`head.ejs`就是HTML中``标签的内容,这里我替换了原有的CSS,使用了[Bootstrap](http://www.bootcss.com/ "Bootstrap")框架。 剩下的就是依次类推了。 附:layout文件结构 ``` layout │ archive.ejs │ category.ejs │ index.ejs │ layout.ejs │ page.ejs │ post.ejs │ tag.ejs │ ├─_partial │ │ after-footer.ejs │ │ archive-post.ejs │ │ archive.ejs │ │ article.ejs │ │ blog-nav.ejs │ │ footer.ejs │ │ head.ejs │ │ sidebar.ejs │ │ wycode-nav.ejs │ │ │ └─post │ category.ejs │ date.ejs │ nav.ejs │ tag.ejs │ title.ejs │ └─_widget archive.ejs category.ejs recent_posts.ejs tag.ejs tagcloud.ejs ``` --- ## 为HTML中的代码添加语法高亮 > 原文: https://wycode.cn/blog/highlight | Agent 文档: https://wycode.cn/blog/highlight.md | 日期: 2017-06-14 | 分类: Frontend ![highlight.js](https://avatars.githubusercontent.com/u/9039821?s=280&v=4) > Hexo 默认对于代码高亮的处理过于粗暴,改变了其原有的`
`标签形式,而且并没有达到良好的效果,所以尝试自己添加语法高亮功能。此文档也适用于其它HTML文档中代码的语法高亮显示。

**我这里使用highlight.js进行添加语法高亮**



### highlight.js优势:

- 176 languages and 79 styles
- automatic language detection
- multi-language code highlighting
- available for node.js
- works with any markup
- compatible with any js framework

## 下面开始集

添加引用:
```html



```
以上第一行是风格样式,我这里使用了`monokai-sublime`样式;
第二行是highlight的JS库;
第三行在文档加载时自动检测`
`标签进行格式化。

**highlight.js**虽然支持100多种语言,但默认并不包含所有,在下载页提供了语言选择,建议自己选择下载,毕竟每个人心目中的热门语言都不同,个人需要在页面中使用的语言也不同。

当然你也可以使用一些静态CDN库进行加速。

我的博客就是使用了这个库,以下是一段Java代码示例:

```java
/******************************************************************************
 *  Compilation:  javac HelloWorld.java
 *  Execution:    java HelloWorld
 *
 *  Prints "Hello, World". By tradition, this is everyone's first program.
 *
 *  % java HelloWorld
 *  Hello, World
 *
 ******************************************************************************/

public class HelloWorld {

    public static void main(String[] args) {
        // Prints "Hello, World" to the terminal window.
        System.out.println("Hello, World");
    }

}
```

---

## 给自己的网站添加评论功能

> 原文: https://wycode.cn/blog/comments | Agent 文档: https://wycode.cn/blog/comments.md | 日期: 2017-06-09 | 分类: Frontend



## 解决方案

### 动态自建
需要数据库存储,后台接口,以及前端页面等都自己实现。
**优点**:所有技术和数据都在自己手中,可控性强,安全。
**缺点**:细节很多,包括表结构,顶和踩计数、还有评论的评论(盖楼)、排序、敏感词过滤、审核等等。还是挺大的一个系统

### 静态集成第三方
目前流行的有以下几种:
【[Disqus](https://disqus.com/ "Disqus")】:国外的,因为众所周知的原因,可能被墙。
【多说】:之前很多人在用,不过已经停止服务了,他们的代码开源在[github.com/duoshuo](https://github.com/duoshuo "github.com/duoshuo"),有兴趣可以参考。
【[畅言](https://changyan.kuaizhan.com/ "畅言")】:搜狐出品。
【[网易云跟帖](https://gentie.163.com/ "网易云跟帖")】:网易出品。



## 选择

不想耗费太多精力,也不想重复造轮子,所以选择集成网易云跟帖,也是因为一直对网易的产品比较有好感。

## 过程

1.  使用网易账号登陆云跟帖后台;

2.  填写基本信息和相关信息:
![](/images/20170609_pinglun_01.jpg)
【图1】
3.  然后就可以获取代码集成了:
![](/images/20170609_pinglun_02.jpg)
【图2】
 代码如下:
```html
``` 云跟帖通过`url`、`sourceId`、`productKey`这三个参数确定唯一的一个网页,`productKey`一定要替换成你自己的; 因为我在[wangyucode.github.io](https://wangyucode.github.io "wangyucode.github.io")和[wycode.cn](http://wycode.cn "wycode.cn")都部署了一样的网页,所以我把`url: document.location.href`,替换为`url: "http://wycode.cn/about.html"`,使github和我自己的服务器上拥有一样的数据。 ## 后续 - 云跟帖支持数据审核,敏感词过滤等,你可以自主选择显示哪些评论。 - 云跟帖支持数据导出,以后要做迁移可以导出数据 - 还支持评论时回推数据到你指定的地址,此功能结合api可以做到产生评论立即推送一条消息到自己的手机,做到消息秒回! --- ## 我为什么开始写博客了? > 原文: https://wycode.cn/blog/why-blog | Agent 文档: https://wycode.cn/blog/why-blog.md | 日期: 2017-06-08 | 分类: Life ## 关于目的 > 奥威尔说作家写作的四大动机:纯粹的自我中心、审美方面的热情、历史方面的冲动以及政治方面的目的。 说起目的,作家们的目的都太大了,按照奥威尔的说法,我的目的可能就只是停留在第一种,纯粹的自我中心吧。 我觉得,就单纯第一种也有不一样的层次和境界,目前我的需求只是记录自己,沉淀技术,而希望有一天我写的东西能够帮助到自己,或者帮助到别人。最终有一天能够成为自我实现的必要条件。 > 有人问一位登山家为什么要去登山——谁都知道登山这件事既危险,又没什么实际的好处,他回答道:“因为那座山峰在那里。” ## 关于技术 以前总是不屑于写博客,觉得CSDN上鱼龙混杂,有很多误人子弟的文章。一方面怕自己学书浅薄,一方面也是太懒,觉得搜索比写作效率高太多。 为了逐渐形成自己的一套知识体系,构建自己的技能树,不断的增加枝叶,又不脱落的一个重要方式就是写作总结。 > 智慧又不失敏捷的方式只有一个:永远不要停止学习。 ## 后记 万事开头难,以后会慢慢将自己学到的东西记录下来。 > Ideas worth spreading ### 接下来的计划 1.自定义博客主题 2.为博客和网站添加评论功能 3.在博客中记录以上过程