跳到主要内容

关闭恢复 Ubuntu GUI

· 阅读需 1 分钟
素明诚
Full stack development

临时关闭 GUI​

如果你只是想临时关闭 GUI,可以通过以下命令停止显示管理器服务(例如 GDM、LightDM 等)。这会将你带回到命令行界面,而不会在下次启动时影响 GUI。

首先,需要确定你的 Ubuntu 使用的是哪种显示管理器。常见的有gdm3(GNOME Display Manager)和lightdm。

systemctl status gdm3

或者

systemctl status lightdm

停止显示管理器服务。例如,如果你的系统使用gdm3,可以使用以下命令

sudo systemctl stop gdm3

如果是lightdm,使用

sudo systemctl stop lightdm

永久不启动 GUI​

如果你想让 Ubuntu 系统永久不启动 GUI,只使用命令行界面,可以禁用显示管理器服务的自动启动。

sudo systemctl disable gdm3

或者

sudo systemctl disable lightdm

重启你的电脑

sudo reboot

系统将以命令行模式重新启动。

恢复 GUI​

如果你想恢复 GUI,可以通过以下命令恢复默认的图形界面启动目标

sudo systemctl set-default graphical.target
sudo reboot

jsdiff 使用指南

· 阅读需 3 分钟
素明诚
Full stack development

https://github.com/kpdecker/jsdiff 是一个用于计算文本差异的 JavaScript 库。它可以在浏览器和 Node.js 环境中运行,方便地比较两段文本并获取它们之间的差异信息。

本文将介绍如何在前端项目中使用 jsdiff,并通过一些示例来帮助你快速上手。

安装​

你可以通过 pnpm 或 yarn 安装 jsdiff

pnpm install diff
# 或
yarn add diff

在浏览器中,你也可以通过 CDN 引入

<script src="https://unpkg.com/diff/dist/diff.min.js"></script>

基本用法​

jsdiff 提供了多种比较文本的方法,常用的有

  • diffChars(oldStr, newStr[, options])按字符比较
  • diffWords(oldStr, newStr[, options])按单词比较
  • diffLines(oldStr, newStr[, options])按行比较

这些方法返回一个差异对象数组,你可以根据这些对象来展示差异。

示例按字符比较​

const Diff = require('diff');
​
const oldStr = 'Hello World';
const newStr = 'Hello Javascript World';
​
const diff = Diff.diffChars(oldStr, newStr);
​
diff.forEach((part) => {
// 添加部分用绿色显示,删除部分用红色显示,未变部分用灰色显示
const color = part.added ? 'green' :
part.removed ? 'red' : 'grey';
console.log(part.value, color);
});

输出

Hello  grey
Javascript green
World grey

示例按单词比较​

const Diff = require('diff');
​
const oldStr = 'I love programming.';
const newStr = 'We love coding.';
​
const diff = Diff.diffWords(oldStr, newStr);
​
diff.forEach((part) => {
const color = part.added ? 'green' :
part.removed ? 'red' : 'grey';
console.log(part.value, color);
});

输出

We green
I red
love grey
coding. green
programming. red

示例按行比较​

const Diff = require('diff');
​
const oldStr = `Line one
Line two
Line three`;
​
const newStr = `Line one
Line 2
Line three`;
​
const diff = Diff.diffLines(oldStr, newStr);
​
diff.forEach((part) => {
const color = part.added ? 'green' :
part.removed ? 'red' : 'grey';
console.log(part.value, color);
});

输出

Line one
grey
Line two
red
Line 2
green
Line three grey

创建补丁​

jsdiff 可以生成统一格式(unified format)的补丁,方便在版本控制中使用。

示例创建补丁​

const Diff = require('diff');
​
const oldStr = 'Hello World';
const newStr = 'Hello Javascript World';
​
const patch = Diff.createPatch('filename.txt', oldStr, newStr);
​
console.log(patch);

输出

Index: filename.txt
===================================================================
--- filename.txt
+++ filename.txt
@@ -1,1 +1,1 @@
-Hello World
+Hello Javascript World

应用补丁​

你也可以将补丁应用到原始文本上。

示例应用补丁​

const Diff = require('diff');
​
const oldStr = 'Hello World';
const patch = `Index: filename.txt
===================================================================
--- filename.txt
+++ filename.txt
@@ -1,1 +1,1 @@
-Hello World
+Hello Javascript World`;
​
const newStr = Diff.applyPatch(oldStr, patch);
​
console.log(newStr);

输出

Hello Javascript World

比较数组​

除了字符串,jsdiff 还可以比较数组。

示例比较数组​

const Diff = require('diff');
​
const oldArr = ['apple', 'banana', 'cherry'];
const newArr = ['apple', 'blueberry', 'cherry', 'date'];
​
const diff = Diff.diffArrays(oldArr, newArr);
​
diff.forEach((part) => {
const added = part.added ? '+' : '';
const removed = part.removed ? '-' : '';
console.log(`${added}${removed}${part.value}`);
});

输出

apple
-banana
+blueberry
cherry
+date

选项参数​

jsdiff 的比较方法可以接受一个可选的 options 对象,常用的选项有

  • ignoreCase忽略大小写
  • ignoreWhitespace忽略空白字符

示例忽略空白字符​

const Diff = require('diff');
​
const oldStr = 'Hello World';
const newStr = 'Hello World';
​
const diff = Diff.diffWords(oldStr, newStr, { ignoreWhitespace: true });
​
diff.forEach((part) => {
const color = part.added ? 'green' :
part.removed ? 'red' : 'grey';
console.log(part.value, color);
});

输出

Hello World grey

在浏览器中使用​

在浏览器中,你可以直接使用 <script> 标签引入 jsdiff,然后使用全局变量 Diff。

<script src="https://unpkg.com/diff/dist/diff.min.js"></script>
<script>
const oldStr = 'Hello World';
const newStr = 'Hello Javascript World';
​
const diff = Diff.diffWords(oldStr, newStr);
​
diff.forEach((part) => {
const color = part.added ? 'green' :
part.removed ? 'red' : 'grey';
console.log(part.value, color);
});
</script>

原理​

http://www.xmailserver.org/diff2.pdf

Python 多线程编程实践

· 阅读需 4 分钟
素明诚
Full stack development

全局解释器锁(GIL)​

在 Python 的并发编程中,全局解释器锁(GIL)是一个重要的考量因素。它限制了在 CPython(解释器)中同时只有一个线程执行字节码,从而影响 CPU 密集型任务的性能。尽管如此,GIL 的存在对 I/O 密集型任务影响较小,因为线程在等待 I/O 操作时可以释放 GIL,允许其他线程运行。在此背景下,开发者通常根据任务性质选择合适的并发模式。

对于 I/O 密集型任务,Threading模块和ThreadPoolExecutor提供了较为简单的实现,允许多个线程并行执行 I/O 操作。而asyncio则利用事件循环和协程来处理高并发 I/O 请求,其非阻塞模型在处理大量连接时更为高效。

在 CPU 密集型任务的场景中,multiprocessing模块是更优的选择。它通过创建多个独立进程,每个进程都有自己的 Python 解释器和内存空间,从而绕过 GIL,实现真正的并行计算。这使得multiprocessing能充分利用多核处理器的能力。

并发编程方式​

特性threadingmultiprocessingThreadPoolExecutorasyncio
适用场景I/O 密集型CPU 密集型I/O 密集型I/O 密集型
并行性受 GIL 限制真正的并行受 GIL 限制真正的并行(基于协程)
资源管理共享内存进程间内存隔离线程池自动管理单线程事件循环
管理复杂性简单较复杂简单中等复杂(需要理解异步概念)
性能对于 I/O 效率较高对于计算效率较高对于 I/O 效率较高对于大量并发 I/O 操作效率高
适合并发量较小(线程上下文切换开销)大(多个进程)较大(可配置最大线程数)很大(可处理成千上万的连接)
编程模型基于线程基于进程基于线程基于协程

threading​

最适合处理 I/O 密集型任务,但在 CPU 密集型任务中性能受限于 GIL。管理简单,适合小规模并发场景。

import threading
import requests
​
​
# 下载单个 URL 的函数
def download_url(url):
try:
response = requests.get(url, timeout=5)
print(f"成功下载: {url}, 内容长度: {len(response.content)}")
except Exception as e:
print(f"下载失败: {url}, 错误: {e}")
​
​
def main(urls):
threads = []
for url in urls:
thread = threading.Thread(target=download_url, args=(url,))
threads.append(thread)
thread.start()
​
for thread in threads:
thread.join() # 等待所有线程完成
​
​
if __name__ == "__main__":
urls = [f"https://www.example.com/{i}" for i in range(10)]
main(urls)

multiprocessing​

适合 CPU 密集型任务,能够实现真正的并行。进程间内存隔离,适合大规模计算,但管理较复杂。

import multiprocessing
import requests
​
​
# 下载单个 URL 的函数
def download_url(url):
try:
response = requests.get(url, timeout=5)
print(f"成功下载: {url}, 内容长度: {len(response.content)}")
except Exception as e:
print(f"下载失败: {url}, 错误: {e}")
​
​
def main(urls):
with multiprocessing.Pool(processes=5) as pool: # 使用进程池
pool.map(download_url, urls)
​
​
if __name__ == "__main__":
urls = [f"https://www.example.com/{i}" for i in range(10)]
main(urls)

ThreadPoolExecutor​

提供了简单的线程池管理,适合 I/O 密集型任务。更高效地管理线程,易于使用。

import requests
from concurrent.futures import ThreadPoolExecutor
​
​
# 下载单个 URL 的函数
def download_url(url):
try:
response = requests.get(url, timeout=5)
print(f"成功下载: {url}, 内容长度: {len(response.content)}")
except Exception as e:
print(f"下载失败: {url}, 错误: {e}")
​
​
def main(urls):
with ThreadPoolExecutor(max_workers=5) as executor:
executor.map(download_url, urls)
​
​
if __name__ == "__main__":
urls = [f"https://www.example.com/{i}" for i in range(10)]
main(urls)

asyncio​

语法类似于 JS,通过事件循环和协程处理 I/O 密集型任务,能处理大量并发。编程模型相对复杂,需要理解异步编程的概念,但能显著提升 I/O 性能。

import asyncio
import aiohttp
​
​
# 异步下载单个 URL 的函数
async def download_url(session, url):
try:
async with session.get(url, timeout=5) as response:
content = await response.read()
print(f"成功下载: {url}, 内容长度: {len(content)}")
except Exception as e:
print(f"下载失败: {url}, 错误: {e}")
​
​
async def main(urls):
async with aiohttp.ClientSession() as session:
tasks = [download_url(session, url) for url in urls]
await asyncio.gather(*tasks)
​
​
if __name__ == "__main__":
urls = [f"https://www.example.com/{i}" for i in range(10)]
asyncio.run(main(urls))

四种方式对比​

对于 I/O 密集型任务,asyncio 和 ThreadPoolExecutor 是优秀的选择,但 asyncio 更适合处理高并发连接。

对于 CPU 密集型任务,multiprocessing 模块允许在 Python 中创建多个进程,每个进程都有自己的 Python 解释器和内存空间。因此,它可以绕过全局解释器锁(GIL),实现真正的并行计算。这使得 multiprocessing 非常适合 CPU 密集型任务,因为它能够充分利用多核处理器的计算能力。

生成自签名 SSL 证书

· 阅读需 5 分钟
素明诚
Full stack development

生成自签名 SSL 证书​

生成私钥 (openssl genrsa)​

私钥是整个证书体系的基础,用于生成公钥和签署证书。它必须保密,因为拥有私钥就能够解密通过相应公钥加密的所有信息。

生成证书签名请求 (CSR) (openssl req)​

CSR 包含证书的相关信息(如组织名、常用名等),以及由对应私钥生成的公钥。CSR 本质上是请求某个证书授权机构(CA)发行证书的正式请求。尽管在自签名过程中不需要外部 CA 的介入,生成 CSR 仍是标准流程的一部分,以确保所有必要信息的包含和正确性。

生成自签名证书 (openssl x509)​

自签名证书意味着使用同一把私钥对 CSR 进行签名,即自己是自己的 CA。这样生成的证书可用于测试环境或内部网络,因为它不由外部信任的 CA 签名。在没有外部验证的情况下,自签名证书能够提供加密和服务器身份验证的功能。

生成一个 RSA 私钥​

openssl genrsa -out $KEY_FILE 2048

参数说明
genrsa生成 RSA 密钥命令。
-out指定输出文件,后面跟文件名。
$KEY_FILE被 -out 使用,指定私钥的文件名。
2048生成密钥的位数,这里为 2048 位。

创建一个证书签名请求(CSR)​

openssl req -new -key $KEY_FILE -out $CSR_FILE -subj "/C=CN/ST=Beijing/L=Beijing/O=Example Corp/OU=IT Department/CN=example.com"

参数说明
req证书请求命令,可以用来创建 CSR 或自签名证书。
-new表示创建一个新的请求。
-key指定使用的私钥文件,用于签署这个请求。
$KEY_FILE由 -key 使用,指向存储私钥的文件。
-out指定输出文件,这里用于指定 CSR 的输出文件。
$CSR_FILE被 -out 使用,指定 CSR 的文件名。
-subj指定在 CSR 或证书中包含的主题详情,格式为 /C=.../ST=.../...

使用 CSR 和私钥生成自签名的证书​

openssl x509 -req -days 365 -in $CSR_FILE -signkey $KEY_FILE -out $CRT_FILE

参数说明
x509X.509 证书处理命令。
-req指示输入文件是一个 CSR。
-days指定证书的有效期,这里为 365 天。
365证书的有效天数。
-in指定输入文件,这里是 CSR 文件。
$CSR_FILE由 -in 使用,指定 CSR 的文件名。
-signkey指定用于签署证书的私钥文件。
$KEY_FILE被 -signkey 使用,指定私钥的文件名。
-out指定输出文件,这里用于指定证书的输出文件。
$CRT_FILE被 -out 使用,指定证书的文件名。

测试证书​

在使用自签名 SSL 证书时,你需要按照上述三步操作生成密钥、CSR 和证书后,然后在实际的服务器配置中应用这些生成的文件。下面是将生成的自签名证书和密钥应用到常见服务器设置的一些基本步骤:

生成密钥和证书​

root.key:私钥文件。

root.csr:证书签名请求。

root.crt:自签名的证书文件。

服务器配置​

取决于你的应用服务器或服务类型(如 Apache、Nginx、Tomcat 等),你需要在服务器的配置文件中指定证书和密钥的路径。这里以 Nginx 和 Apache 为例进行说明:

Nginx​

对于 Nginx,你可以在其配置文件(通常位于 /etc/nginx/nginx.conf 或某个特定的站点配置文件中)设置 SSL 证书和密钥路径:

server {
listen 443 ssl;
server_name example.com;
​
ssl_certificate /path/to/root.crt;
ssl_certificate_key /path/to/root.key;
​
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}

这里,ssl_certificate 和 ssl_certificate_key 指向你的证书文件和私钥文件。

测试和验证​

配置完成后,重启你的 Web 服务器以使更改生效。然后可以使用浏览器或工具(如 curl)来验证 HTTPS 是否正常工作

curl -k https://example.com

选项 -k 允许 curl 忽略 SSL 证书验证错误

OpenSSL 自签名脚本​

#!/bin/bash

# 配置变量
KEY_FILE="root.key" # 私钥文件名
CRT_FILE="root.crt" # 证书文件名
CSR_FILE="root.csr" # CSR 文件名
DAYS_VALID=365 # 证书有效天数
COUNTRY="CN" # 国家
STATE="Beijing" # 省份
LOCALITY="Beijing" # 城市
ORG="Example Corp" # 组织
ORG_UNIT="IT Department" # 组织单位
COMMON_NAME="harbor.com" # 公共名称

# 生成私钥
if openssl genrsa -out "$KEY_FILE" 2048; then
echo "私钥生成成功:$KEY_FILE"
else
echo "私钥生成失败" >&2
exit 1
fi

# 创建证书签名请求 (CSR)
if openssl req -new -key "$KEY_FILE" -out "$CSR_FILE" -subj "/C=$COUNTRY/ST=$STATE/L=$LOCALITY/O=$ORG/OU=$ORG_UNIT/CN=$COMMON_NAME"; then
echo "证书签名请求生成成功:$CSR_FILE"
else
echo "CSR 生成失败" >&2
exit 1
fi

# 自签名证书
if openssl x509 -req -days "$DAYS_VALID" -in "$CSR_FILE" -signkey "$KEY_FILE" -out "$CRT_FILE"; then
echo "自签名证书生成成功:$CRT_FILE"
else
echo "证书生成失败" >&2
exit 1
fi

echo "证书生成完毕:"
echo "私钥文件:$KEY_FILE"
echo "证书文件:$CRT_FILE"

Linux mc 文件管理工具快速删除文件

· 阅读需 3 分钟
素明诚
Full stack development

使用 mc 删除文件​

启动 mc​

在终端输入 mc 命令,打开 Midnight Commander 界面。

dcd0a1bcdd20daaa2cad92851e6435d8### 浏览文件系统

使用箭头键浏览文件和文件夹。左侧和右侧面板可以分别显示不同目录的内容。

选择文件​

使用方向键移动光标到要删除的文件或文件夹上。

删除文件​

按 F8 键,系统会弹出一个确认对话框,询问是否删除选中的文件。

确认后,文件将被删除。

44f8916b6a72e8bb193d17dba7205fd1### 批量删除

可以使用 Insert 键选择多个文件,然后按 F8 进行批量删除。

下面是每个功能的具体示例,帮助你更好地理解 Midnight Commander (MC) 的使用

其他操作​

浏览文件系统​

启动 MC,使用方向键在左侧和右侧窗格中浏览不同的目录。例如,按 Tab 键切换窗格。

文件复制和移动​

选中一个文件,按 F5 复制,或按 F6 移动到目标目录。在弹出的对话框中选择目标路径。

重命名文件​

选中要重命名的文件,按 F6,然后输入新名称,最后确认。

文件查看和编辑​

选中一个文本文件,按 F3 查看文件内容。要编辑,按 F4 打开内置编辑器。

搜索文件​

按 Ctrl + S,输入要搜索的文件名或模式(如 *.txt),然后 MC 会在当前目录及子目录中搜索。

批量选择文件​

按 Insert 键选中多个文件。完成选择后,按 F5 或 F6 进行批量复制或移动。

比较文件​

选中两个文件(按 Insert 选中),然后按 F9,选择“文件”菜单中的“比较”选项,查看它们的差异。

支持压缩和解压缩​

选中一个压缩文件(如 .zip),按 F4 解压或按 F5 创建新的压缩文件。

访问 FTP/SFTP 服务器​

在 MC 中,按 Ctrl + \,输入 FTP/SFTP 服务器地址(如 ftp//example.com),然后输入用户名和密码,连接后可以管理远程文件。

文件属性和权限管理​

选中一个文件,按 F9,选择“文件”菜单中的“属性”,可以查看和修改文件的权限和所有者。

执行外部命令​

在 MC 界面中,按 Ctrl + O 切换到命令行模式,输入所需的命令(如 ls -l),然后按 Enter 执行。

通过这些示例,你可以更全面地了解如何使用 MC 进行各种文件管理操作。

注意事项​

隐藏文件默认情况下,mc 可能不显示以 . 开头的隐藏文件。如果需要查看和删除这些文件,可以在 mc 的设置中调整显示选项。

Linux 挂载磁盘的步骤

· 阅读需 2 分钟
素明诚
Full stack development

查看磁盘信息​

lsblk

# 输出如下
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 4K 1 loop /snap/bare/5
loop1 7:1 0 269.6M 1 loop /snap/firefox/4173
loop2 7:2 0 74.2M 1 loop /snap/core22/1380
loop3 7:3 0 505.1M 1 loop /snap/gnome-42-2204/176
loop4 7:4 0 10.7M 1 loop /snap/firmware-updater/127
loop5 7:5 0 10.3M 1 loop /snap/snap-store/1124
loop6 7:6 0 91.7M 1 loop /snap/gtk-common-themes/1535
loop7 7:7 0 38.7M 1 loop /snap/snapd/21465
loop8 7:8 0 476K 1 loop /snap/snapd-desktop-integration/157
loop9 7:9 0 137.3M 1 loop /snap/thunderbird/470
sda 8:0 0 238.5G 0 disk
├─sda1 8:1 0 1G 0 part /boot/efi
└─sda2 8:2 0 237.4G 0 part /var/snap/firefox/common/host-hunspell
/
sdb 8:16 0 465.8G 0 disk
└─sdb1 8:17 0 465.8G 0 part

目录内容​

/dev 目录是 Linux 系统中的一个虚拟文件系统,用于表示设备文件。它包含了系统中所有设备的节点,比如硬盘、USB 设备、终端等。设备文件通常由操作系统在启动时自动生成,它们通过内核管理设备的输入和输出。

在 lsblk 输出中,sdb 表示第二个硬盘(第一个是 sda),而 1 表示这是第一个分区。part 代表该设备是一个分区,区别于 disk(整个磁盘)或 loop(环回设备)。所以,sdb1 是你硬盘 sdb 上的一个分区

手动挂载/dev/sdb1 到/media/hhd 目录​

创建挂载目录​

首先创建挂载点 /media/hhd

sudo mkdir -p /media/hhd

挂载分区到该目录​

使用以下命令将分区 /dev/sdb1 挂载到 /media/hhd

sudo mount /dev/sdb1 /media/hhd

验证挂载是否成功​

您可以使用以下命令检查挂载情况

df -h | grep /media/hhd

⚡ root@smc-GK41  /  df -h | grep /media/hhd
/dev/sdb1 466G 292G 175G 63% /media/hhd

或者直接查看该目录中的内容

ls /media/hhd

⚡ root@smc-GK41  /  ls /media/hhd
'$RECYCLE.BIN' Down Downloads GK41-Drivers GK41-Drivers.zip 'System Volume Information'
⚡ root@smc-GK41  / 

设置开机自动挂载​

如果您希望系统在每次启动时自动挂载该分区,可以将其添加到/etc/fstab文件中。设置自动挂载就不用设置上面的手动了

打开/etc/fstab文件进行编辑

sudo nano /etc/fstab

在文件末尾添加以下行

/dev/sdb1  /media/hhd  auto  defaults  0  0

保存并关闭文件。

测试配置是否正确,可以使用以下命令检查是否有错误

sudo mount -a

分区/dev/sdb1将挂载到/media/hhd目录下,并且在系统重启时也会自动挂载。

Go 面向接口和依赖注入的编写逻辑

· 阅读需 3 分钟
素明诚
Full stack development

已完成一个消息通知系统为例,这里自上而下的思考步骤如下

明确系统目标和功能​

目标:创建一个灵活的通知系统,能够通过不同的方式(如电子邮件和短信等)发送通知。

功能:系统应支持发送通知、管理配置、选择消息发送方式等。

确定主要组件​

main:入口点,负责配置和启动服务。

NotificationService:负责发送通知的核心逻辑。

MessageSender 接口及其实现:定义和实现不同的消息发送方式(如 EmailSender 和 SmsSender)。

Config 结构体:管理和存储配置信息。

定义核心接口​

确定 MessageSender 接口,它应该包含 Send(message string) error 方法。这个接口为后续具体实现提供了基础。

实现具体功能​

开始实现具体的功能模块

配置管理:创建 Config 结构体和相应的方法来读取和管理配置信息。

消息发送实现:实现 EmailSender 和 SmsSender,确保它们符合 MessageSender 接口。

通知服务:实现 NotificationService,该服务使用 MessageSender 接口进行通知发送。

实现主程序逻辑​

在 main 函数中,根据配置选择消息发送者,创建 NotificationService 实例,并调用发送通知的方法。

实现​

main.go​

package main

import (
"log"
)

func main() {
// 配置管理
config := Config{NotificationType: "email"} // 可以改为 "sms"

// 根据配置创建消息发送者
sender, err := CreateMessageSender(config)
if err != nil {
log.Fatalf("Error creating message sender: %v", err)
}

// 创建通知服务实例
notificationService := NewNotificationService(sender)

// 发送通知
if err := notificationService.Notify("Hello via Notification Service"); err != nil {
log.Fatalf("Error sending notification: %v", err)
}

// 发送空消息,触发错误
if err := notificationService.Notify(""); err != nil {
log.Printf("Expected error: %v", err)
}
}

config.go​

package main

// Config 用于管理配置信息
type Config struct {
NotificationType string
}

message_sender.go​

package main

import "errors"

// MessageSender 是一个接口,定义了发送消息的方法
type MessageSender interface {
Send(message string) error
}

// CreateMessageSender 根据配置创建对应的 MessageSender 实现
func CreateMessageSender(config Config) (MessageSender, error) {
switch config.NotificationType {
case "email":
return &EmailSender{}, nil
case "sms":
return &SmsSender{}, nil
default:
return nil, errors.New("unsupported notification type")
}
}

notification_service.go​

package main

import (
"errors"
)

// NotificationService 是一个结构体,包含了一个 MessageSender 类型的字段 sender
type NotificationService struct {
sender MessageSender
}

// NewNotificationService 是一个构造函数,用于创建 NotificationService 实例
func NewNotificationService(sender MessageSender) *NotificationService {
return &NotificationService{sender: sender}
}

// Notify 是 NotificationService 结构体的一个方法,用于发送通知
func (n *NotificationService) Notify(message string) error {
if message == "" {
return errors.New("message cannot be empty")
}
return n.sender.Send(message)
}

senders.go​

package main

import (
"log"
)

// EmailSender 发送电子邮件
type EmailSender struct{}

// Send 实现了 MessageSender 接口
func (e *EmailSender) Send(message string) error {
// 模拟发送电子邮件的逻辑
log.Printf("Sending email: %s", message)
return nil // 假设发送成功
}

// SmsSender 发送短信
type SmsSender struct{}

// Send 实现了 MessageSender 接口
func (s *SmsSender) Send(message string) error {
// 模拟发送短信的逻辑
log.Printf("Sending SMS: %s", message)
return nil // 假设发送成功
}

解决 Vue 中的事件冒泡问题

· 阅读需 2 分钟
素明诚
Full stack development

在开发 Vue 应用时,经常需要处理事件冒泡问题,特别是在多层嵌套的组件结构中。以下是两种有效的方法来阻止事件冒泡,确保组件行为按预期执行。

使用 Vue 的.stop 修饰符​

Vue 提供了 .stop 修饰符,这是一个简洁且直接的方法来阻止事件冒泡。使用 .stop 修饰符,可以直接在模板的事件绑定上操作,无需修改 JavaScript 逻辑。这种方法非常适合大多数场景,尤其是在简单的父子组件通信中。

实现示例​

在 Vue 模板中,将 .stop 修饰符添加到事件绑定上,如下所示

<button @click.stop="toggleDescription">展开/收起</button>

在这个例子中,当点击按钮时,.stop 修饰符会阻止事件继续向上冒泡至父元素。这样可以避免触发父元素或其他上层元素的事件监听器。

在事件处理函数中调用 event.stopPropagation()​

如果需要更精细的控制,或者想在执行某些条件逻辑后才决定是否阻止冒泡,可以在事件处理函数中调用 event.stopPropagation() 方法。这种方法提供了更高的灵活性,允许开发者根据运行时的条件来动态决定是否阻止事件冒泡。

实现示例​

首先,在方法中接收事件对象,然后调用 stopPropagation() 方法

const toggleDescription = (event) => {
event.stopPropagation();
// 可以在这里添加其他逻辑
isExpanded.value = !isExpanded.value;
}

然后,在模板中传递事件对象到该方法

<button @click="toggleDescription($event)">展开/收起</button>

这种方式允许在函数内部根据具体的逻辑来决定是否停止事件的进一步传播。例如,可以基于用户的权限级别或应用的状态来决定是否阻止冒泡。

Linux 不输入密码获取 root 权限

· 阅读需 2 分钟
素明诚
Full stack development

修改 sudoers 文件以免密码执行 sudo(推荐)​

可以通过修改 sudoers 文件为特定用户配置无密码 sudo 权限

使用 visudo 命令安全编辑 sudoers 文件

sudo visudo

在文件中添加或修改以下行,替换 your_username 为实际的用户名

your_username ALL=(ALL) NOPASSWD: ALL

保存并退出编辑器。

这样配置后,用户在执行 sudo 命令时无需输入密码,例如使用 sudo su 可以直接切换到 root 用户。

设置 root 用户的自动登录​

对于部分 Linux 发行版,可以设置在启动时自动登录 root 用户

对于使用 SysVinit 的系统,编辑 /etc/inittab 文件

打开 /etc/inittab,找到以下行

1:2345:respawn:/sbin/getty 38400 tty1

修改为

1:2345:respawn:/sbin/getty --autologin root 38400 tty1

对于使用 systemd 的系统,修改 getty 服务配置

创建覆盖文件

sudo mkdir -p /etc/systemd/system/getty@tty1.service.d/
sudo nano /etc/systemd/system/getty@tty1.service.d/override.conf

添加以下内容

[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM

刷新 systemd 配置并重启服务

sudo systemctl daemon-reload
sudo systemctl restart getty@tty1.service

禁用 root 用户密码​

不建议此方法,但也可以通过删除 root 用户的密码实现无密码登录

使用以下命令删除 root 用户密码

sudo passwd -d root

使用 ssh 公钥认证​

通过配置 ssh 允许使用公钥进行身份验证,从而无需密码直接登录到 root 用户

确保 ssh 服务的配置文件 /etc/ssh/sshd_config 中启用了公钥认证

PubkeyAuthentication yes

在 root 用户的home目录下,将用户的公钥添加到 ~/.ssh/authorized_keys 文件中。

确保客户端的私钥安全,且不被未授权用户访问。