👨‍💻
福禄技术笔记
记录前端开发学习过程,分享代码实践心得
Electron 桌面应用开发入门:从 HTML 到 EXE
最近在学习 Electron 框架,把平时用的网页工具打包成桌面应用。记录一下主进程通信、本地文件读写、以及 ASAR 打包的踩坑过程...

Electron 的本质是把 Chromium 浏览器和 Node.js 运行时打包在一起,让前端代码拥有操作本地文件、调用系统 API 的能力。

一、环境搭建

创建项目后,安装 Electron 到开发依赖:

npm init -y
npm install electron --save-dev

在 package.json 里指定入口:

"main": "main.js",
"scripts": {
  "start": "electron .",
  "build": "electron-builder"
}

二、主进程入口

main.js 负责创建窗口,webPreferences 里开启 nodeIntegration 才能在渲染进程使用 require:

const { app, BrowserWindow } = require('electron');
function createWindow() {
  const win = new BrowserWindow({
    width: 1200, height: 800,
    webPreferences: { nodeIntegration: true, contextIsolation: false }
  });
  win.loadFile('index.html');
}
app.whenReady().then(createWindow);

三、进程间通信

渲染进程通过 ipcRenderer 发送消息,主进程用 ipcMain 监听。这是实现"点击按钮保存文件"的核心机制:

// 渲染进程
const { ipcRenderer } = require('electron');
ipcRenderer.send('save-data', jsonString);

// 主进程
const { ipcMain } = require('electron');
ipcMain.on('save-data', (event, data) => {
  fs.writeFileSync(path.join(__dirname, 'data.json'), data);
});

四、打包踩坑

打包成 ASAR 后,__dirname 指向的是 ASAR 内部虚拟路径,直接用 fs 读取相对路径会报错。解决方法是区分开发环境和生产环境:

const isDev = !app.isPackaged;
const basePath = isDev ? __dirname : process.resourcesPath;

另外,Windows 下路径分隔符是反斜杠,一定要用 path.join() 拼接,不要手动拼字符串。

JavaScript 手机号正则验证与批量处理
整理了几种常见的手机号验证场景:11 位号码提取、重复检测、跨平台状态同步。用 Set 做排重比数组遍历效率高很多,特别是数据量大的时候...

手机号处理是很多业务系统的基础能力。中国大陆手机号有固定规律:1 开头,第二位 3-9,总长度 11 位。

一、正则验证

最严格的正则,覆盖所有正常号段:

const phoneRegex = /^1[3-9]\d{9}$/;

如果需要兼容物联网卡或虚拟运营商,可以把第二位放宽到 1-9,但一般业务场景用上面的就够了。

二、批量清洗

用户从 Excel 或微信复制过来的数据,经常带空格、换行、隐藏字符。我的清洗流程是:

function cleanPhones(text) {
  return text
    .replace(/[^\d\n]/g, '')      // 只保留数字和换行
    .split('\n')                  // 按行分割
    .map(s => s.trim())
    .filter(s => s.length === 11 && phoneRegex.test(s));
}

三、排重优化

几百个号码去重,用数组 includes 遍历是 O(n²),数据量上千就会卡。改用 Set,查询复杂度降到 O(1):

const seen = new Set();
const unique = phones.filter(p => {
  if (seen.has(p)) return false;
  seen.add(p);
  return true;
});

四、输入框实时校验

在 input 事件里拦截非数字输入,限制 11 位,比提交后报错体验好得多:

input.addEventListener('input', (e) => {
  e.target.value = e.target.value.replace(/\D/g, '').slice(0, 11);
});
Node.js + Express 搭建本地数据服务
用 Express 搭了一个简单的本地 API 服务,处理 JSON 数据读写。记录了 fs 异步操作、数据持久化、以及手写 multipart 解析的过程...

对于个人工具类项目,不需要上 MongoDB 或 MySQL,直接用 JSON 文件存数据更轻量,备份就是复制一个文件。

一、最简服务器

不用 Express 也可以,原生 http 模块更轻:

const http = require('http');
http.createServer((req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  // 路由处理...
}).listen(3456);

二、JSON 持久化

读写封装成两个函数,所有修改先写内存再同步落盘:

function loadData() {
  try {
    const raw = fs.readFileSync('data.json', 'utf8');
    return JSON.parse(raw);
  } catch(e) { return {}; }
}
function saveData(data) {
  fs.writeFileSync('data.json', JSON.stringify(data, null, 2));
}

三、手写文件上传

不想依赖 multer,可以用原生 Buffer 解析 multipart/form-data。核心是找到 boundary,按段分割,提取 header 里的 name/filename 和正文数据:

function parseMultipart(body, boundary) {
  const results = { fields: {}, files: [] };
  const parts = body.split('--' + boundary);
  // 解析每段的 header 和 data...
  return results;
}

四、静态资源托管

用 fs.readFileSync 读取本地 HTML 返回,比 express.static 更可控,适合需要加缓存头或鉴权的场景:

const html = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
res.end(html);
仿微信风格 UI 设计:圆角、阴影与过渡动画
研究了一下微信的界面设计语言,尝试用纯 CSS 还原类似的视觉效果。重点在 border-radius 的层级运用、box-shadow 柔和度、以及 transition 缓动函数...

微信的 UI 非常克制:浅灰背景、纯白卡片、大圆角、蓝色主按钮、灰色次按钮。这种设计耐看且不容易过时。

一、色彩体系

用 CSS 变量定义一套统一配色,方便后续维护和夜间模式切换:

:root {
  --bg: #f2f2f7;           /* 页面背景 */
  --card: #ffffff;         /* 卡片背景 */
  --primary: #007aff;      /* 主按钮 */
  --text: #1d1d1f;         /* 主文字 */
  --secondary: #8e8e93;    /* 次要文字 */
  --border: #e5e5ea;       /* 分割线 */
}

二、卡片层级

外层卡片用 16px 大圆角,内部按钮用 8px 小圆角,形成视觉层级。阴影要淡,不能喧宾夺主:

.card {
  background: var(--card);
  border-radius: 16px;
  padding: 20px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}

三、按钮交互

主按钮用实心蓝色,次按钮用浅灰背景,危险操作用红色。hover 时亮度降低 10%,移动端要加 active 状态反馈:

.btn {
  transition: all 0.2s ease;
}
.btn:active {
  transform: scale(0.96);
}

四、移动端适配

用 -apple-system 字体栈保证 iOS 和 Android 都有原生质感。输入框去掉默认 outline,focus 时改 border-color:

input:focus {
  outline: none;
  border-color: var(--primary);
}
用 JSON 文件做本地数据库:持久化与备份策略
不想引入 SQLite 或 LevelDB,直接用 JSON 文件存业务数据。研究了读写锁、定时备份、异常恢复,适合单机桌面应用...

桌面应用的数据量通常不大,几千条记录撑死。JSON 文件方案零依赖、可人工查看、方便调试。

一、读写封装

所有数据操作都走同一个入口,避免多个地方直接 fs.writeFile 导致竞态:

let cache = null;
function getData() {
  if (!cache) cache = loadData();
  return cache;
}
function setData(newData) {
  cache = newData;
  fs.writeFileSync('data.json', JSON.stringify(cache, null, 2));
}

二、定时备份

每次写入后自动复制一份带时间戳的备份,保留最近 24 份,防止误操作或文件损坏:

function backup() {
  const ts = Date.now();
  fs.copyFileSync('data.json', `backups/data_${ts}.json`);
  // 清理旧备份,只保留最近24个
}

三、异常恢复

启动时如果主文件损坏,自动尝试读取最新的备份文件恢复:

function loadWithFallback() {
  try { return loadData('data.json'); }
  catch(e) {
    const backups = fs.readdirSync('backups').sort().reverse();
    for (const f of backups) {
      try { return loadData('backups/' + f); }
      catch(e) { continue; }
    }
    return {}; // 全部损坏,返回空数据
  }
}
WebSocket 实时推送:SSE 降级与心跳检测
桌面应用需要实时刷新数据,WebSocket 最自然。但某些网络环境会拦截 ws 协议,所以同时实现了 SSE 作为降级方案...

实时推送有两种主流方案:WebSocket 双向通信,或 SSE 单向推送。前者更灵活,后者兼容性更好。

一、SSE 实现

Server-Sent Events 基于 HTTP,不需要额外协议握手,企业防火墙通常不会拦截:

res.writeHead(200, {
  'Content-Type': 'text/event-stream',
  'Cache-Control': 'no-cache',
  'Connection': 'keep-alive'
});
res.write('data: ' + JSON.stringify({type:'connected'}) + '\n\n');

二、WebSocket 心跳

长时间空闲连接会被运营商或路由器切断,需要定时发 ping/pong 保活:

const ws = new WebSocket('ws://localhost:3456/ws');
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({type: 'ping'}));
  }
}, 30000); // 30秒一次心跳

三、断线重连

监听 close 事件,指数退避重试,避免瞬间大量重连请求把服务器打挂:

let reconnectDelay = 1000;
ws.onclose = () => {
  setTimeout(() => {
    reconnect();
    reconnectDelay = Math.min(reconnectDelay * 2, 30000);
  }, reconnectDelay);
};
前端密码安全:本地存储的伪装与隔离策略
桌面应用没有后端鉴权,密码存在本地。研究了 localStorage、sessionStorage、以及 Electron 的 safeStorage 模块,最后选了一套折中方案...

单机应用的密码安全是个悖论:密码必须存在本地,而本地文件对用户是完全透明的。我们能做的只是增加破解成本。

一、不要明文存密码

哪怕只是做一层 Base64 编码,也能挡住 90% 的"不小心打开文件"泄露:

function obfuscate(str) {
  return Buffer.from(str).toString('base64');
}
function deobfuscate(str) {
  return Buffer.from(str, 'base64').toString('utf8');
}

二、隔离存储位置

不要把密码和业务数据放在同一个 JSON 文件里。业务数据可以随便看,密码单独存:

// data.json 存业务数据
// .config/auth 存密码(隐藏文件)

三、Electron safeStorage

Electron 提供了 safeStorage 模块,用操作系统级别的密钥链(macOS Keychain / Windows DPAPI)加密:

const { safeStorage } = require('electron');
const encrypted = safeStorage.encryptString(password);
const decrypted = safeStorage.decryptString(encrypted);

缺点是不可移植,换电脑后解密失败。所以我的方案是:safeStorage 优先,失败时降级到 Base64 伪装。

Node.js 自动化脚本:定时任务与文件批量处理
写了一些日常辅助脚本:定时备份数据、批量重命名截图、调用 Windows 通知中心。用 node-schedule 和 node-notifier 很方便...

程序员的时间应该花在解决问题上,而不是重复劳动。Node.js 写脚本比 Python 更顺手,因为语法和前端完全一致。

一、定时任务

最简单的定时器用 setInterval,但进程退出就失效。如果用 node-schedule,可以指定 cron 表达式:

const schedule = require('node-schedule');
// 每天凌晨2点执行备份
schedule.scheduleJob('0 2 * * *', () => {
  backupData();
});

二、文件批量重命名

截图文件默认名字是 timestamp.png,需要按业务规则重命名。用 fs.readdir + path.parse 遍历处理:

const files = fs.readdirSync('./screenshots');
files.forEach(f => {
  const { ext } = path.parse(f);
  const newName = `prefix_${Date.now()}${ext}`;
  fs.renameSync(`./screenshots/${f}`, `./screenshots/${newName}`);
});

三、系统通知

node-notifier 跨平台调用系统原生通知,Windows 下用 toaster,macOS 用 terminal-notifier:

const notifier = require('node-notifier');
notifier.notify({
  title: '任务完成',
  message: '数据备份成功',
  sound: true
});

四、进程守护

用 pm2 或 nohup 让脚本后台常驻,配合 log 文件排查问题:

nohup node server.js > app.log 2>&1 &