发现问题

在 ubuntu 服务器里想安装 nodejs,进入 nodejs 官网,打开下载页,选择版本,linux。目前官网推荐的是使用 fnm,nvm,docker 来安装。我这里选择的是 fnm。

选择好后会显示 bash 命令行

1
2
3
4
5
6
7
8
# Download and install fnm:
curl -o- https://fnm.vercel.app/install | bash
# Download and install Node.js:
fnm install 22
# Verify the Node.js version:
node -v # Should print "v22.14.0".
# Verify npm version:
npm -v # Should print "10.9.2".

服务器里运行发现,curl 的时候出错了,可能是无法访问造成的。

然后我在可以访问这个连接的地方访问了这个网站,发现里面是 bash 脚本。理解了他是下载这个脚本,然后使用 bash 运行,所以我手动复制了这个脚本下来,传到服务器里。

在服务器里运行,发现还是在 download 的时候报错,打开脚本仔细看,在download_fnm函数里

1
2
3
4
5
6
7
8
9
10
if [ "$RELEASE" = "latest" ]; then
URL="https://github.com/Schniz/fnm/releases/latest/download/$FILENAME.zip"
else
URL="https://github.com/Schniz/fnm/releases/download/$RELEASE/$FILENAME.zip"
fi

if ! curl --progress-bar --fail -L "$URL" -o "$DOWNLOAD_DIR/$FILENAME.zip"; then
echo "Download failed. Check that the release/filename are correct."
exit 1
fi

他还是要通过 github 下载,无法访问的话自然会下载失败。但是他会提示你正在哪个链接下载,比如https://github.com/Schniz/fnm/releases/latest/download/fnm-linux.zip
后面的那个 zip 文件就是需要下载的文件,所以去 fnm 的 github releases 里手动下载这个 zip 文件,然后传到服务器里。

然后继续回到 bash 脚本里,把 set_filename 这个函数注释了,因为已经明确的知道文件名了。

1
2
3
4
5
6
7
parse_args "$@"
# set_filename
check_dependencies
download_fnm
if [ "$SKIP_SHELL" != "true" ]; then
setup_shell
fi

然后进入download_fnm函数里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 将下载相关的命令注释掉,不需要从github下载了
# if ! curl --progress-bar --fail -L "$URL" -o "$DOWNLOAD_DIR/$FILENAME.zip"; then
# echo "Download failed. Check that the release/filename are correct."
# exit 1
# fi

# 将 FILENAME 变量改为之前得到的文件名
FILENAME="fnm-linux"

# 新建一个 FILEPATH 变量,变量值是你传入到服务器里的zip文件路径
FILEPATH="$HOME/$FILENAME"

# 将你传到服务器里的文件,复制到临时目录里
cp "$FILEPATH.zip" "$DOWNLOAD_DIR"

# 随便写一个echo,提示正在解压文件
echo "Unziping $FILENAME"

unzip -q "$DOWNLOAD_DIR/$FILENAME.zip" -d "$DOWNLOAD_DIR"

然后 bash 里,运行最终修改好的脚本文件就安装好了

1
bash install.sh

完整代码,目前 fnm 版本是1.38.1,如果以后版本的安装脚本变动的话,需要根据需求重新改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#!/bin/bash

set -e

RELEASE="latest"
OS="$(uname -s)"

case "${OS}" in
MINGW* | Win*) OS="Windows" ;;
esac

if [ -d "$HOME/.fnm" ]; then
INSTALL_DIR="$HOME/.fnm"
elif [ -n "$XDG_DATA_HOME" ]; then
INSTALL_DIR="$XDG_DATA_HOME/fnm"
elif [ "$OS" = "Darwin" ]; then
INSTALL_DIR="$HOME/Library/Application Support/fnm"
else
INSTALL_DIR="$HOME/.local/share/fnm"
fi

# Parse Flags
parse_args() {
while [[ $# -gt 0 ]]; do
key="$1"

case $key in
-d | --install-dir)
INSTALL_DIR="$2"
shift # past argument
shift # past value
;;
-s | --skip-shell)
SKIP_SHELL="true"
shift # past argument
;;
--force-install | --force-no-brew)
echo "\`--force-install\`: I hope you know what you're doing." >&2
FORCE_INSTALL="true"
shift
;;
-r | --release)
RELEASE="$2"
shift # past release argument
shift # past release value
;;
*)
echo "Unrecognized argument $key"
exit 1
;;
esac
done
}

set_filename() {
if [ "$OS" = "Linux" ]; then
# Based on https://stackoverflow.com/a/45125525
case "$(uname -m)" in
arm | armv7*)
FILENAME="fnm-arm32"
;;
aarch* | armv8*)
FILENAME="fnm-arm64"
;;
*)
FILENAME="fnm-linux"
esac
elif [ "$OS" = "Darwin" ] && [ "$FORCE_INSTALL" = "true" ]; then
FILENAME="fnm-macos"
USE_HOMEBREW="false"
echo "Downloading the latest fnm binary from GitHub..."
echo " Pro tip: it's easier to use Homebrew for managing fnm in macOS."
echo " Remove the \`--force-no-brew\` so it will be easy to upgrade."
elif [ "$OS" = "Darwin" ]; then
USE_HOMEBREW="true"
echo "Downloading fnm using Homebrew..."
elif [ "$OS" = "Windows" ] ; then
FILENAME="fnm-windows"
echo "Downloading the latest fnm binary from GitHub..."
else
echo "OS $OS is not supported."
echo "If you think that's a bug - please file an issue to https://github.com/Schniz/fnm/issues"
exit 1
fi
}

download_fnm() {
if [ "$USE_HOMEBREW" = "true" ]; then
brew install fnm
else
if [ "$RELEASE" = "latest" ]; then
URL="https://github.com/Schniz/fnm/releases/latest/download/$FILENAME.zip"
else
URL="https://github.com/Schniz/fnm/releases/download/$RELEASE/$FILENAME.zip"
fi

DOWNLOAD_DIR=$(mktemp -d)

# echo "Downloading $URL..."

mkdir -p "$INSTALL_DIR" &>/dev/null

# if ! curl --progress-bar --fail -L "$URL" -o "$DOWNLOAD_DIR/$FILENAME.zip"; then
# echo "Download failed. Check that the release/filename are correct."
# exit 1
# fi

FILENAME="fnm-linux"

FILEPATH="$HOME/$FILENAME"

cp "$FILEPATH.zip" "$DOWNLOAD_DIR"

echo "Unziping $FILENAME"

unzip -q "$DOWNLOAD_DIR/$FILENAME.zip" -d "$DOWNLOAD_DIR"

if [ -f "$DOWNLOAD_DIR/fnm" ]; then
mv "$DOWNLOAD_DIR/fnm" "$INSTALL_DIR/fnm"
else
mv "$DOWNLOAD_DIR/$FILENAME/fnm" "$INSTALL_DIR/fnm"
fi

chmod u+x "$INSTALL_DIR/fnm"
fi
}

check_dependencies() {
echo "Checking dependencies for the installation script..."

echo -n "Checking availability of unzip... "
if hash unzip 2>/dev/null; then
echo "OK!"
else
echo "Missing!"
SHOULD_EXIT="true"
fi

if [ "$USE_HOMEBREW" = "true" ]; then
echo -n "Checking availability of Homebrew (brew)... "
if hash brew 2>/dev/null; then
echo "OK!"
else
echo "Missing!"
SHOULD_EXIT="true"
fi
fi

if [ "$SHOULD_EXIT" = "true" ]; then
echo "Not installing fnm due to missing dependencies."
exit 1
fi
}

ensure_containing_dir_exists() {
local CONTAINING_DIR
CONTAINING_DIR="$(dirname "$1")"
if [ ! -d "$CONTAINING_DIR" ]; then
echo " >> Creating directory $CONTAINING_DIR"
mkdir -p "$CONTAINING_DIR"
fi
}

setup_shell() {
CURRENT_SHELL="$(basename "$SHELL")"

if [ "$CURRENT_SHELL" = "zsh" ]; then
CONF_FILE=${ZDOTDIR:-$HOME}/.zshrc
ensure_containing_dir_exists "$CONF_FILE"
echo "Installing for Zsh. Appending the following to $CONF_FILE:"
{
echo ''
echo '# fnm'
echo 'FNM_PATH="'"$INSTALL_DIR"'"'
echo 'if [ -d "$FNM_PATH" ]; then'
echo ' export PATH="'$INSTALL_DIR':$PATH"'
echo ' eval "`fnm env`"'
echo 'fi'
} | tee -a "$CONF_FILE"

elif [ "$CURRENT_SHELL" = "fish" ]; then
CONF_FILE=$HOME/.config/fish/conf.d/fnm.fish
ensure_containing_dir_exists "$CONF_FILE"
echo "Installing for Fish. Appending the following to $CONF_FILE:"
{
echo ''
echo '# fnm'
echo 'set FNM_PATH "'"$INSTALL_DIR"'"'
echo 'if [ -d "$FNM_PATH" ]'
echo ' set PATH "$FNM_PATH" $PATH'
echo ' fnm env | source'
echo 'end'
} | tee -a "$CONF_FILE"

elif [ "$CURRENT_SHELL" = "bash" ]; then
if [ "$OS" = "Darwin" ]; then
CONF_FILE=$HOME/.profile
else
CONF_FILE=$HOME/.bashrc
fi
ensure_containing_dir_exists "$CONF_FILE"
echo "Installing for Bash. Appending the following to $CONF_FILE:"
{
echo ''
echo '# fnm'
echo 'FNM_PATH="'"$INSTALL_DIR"'"'
echo 'if [ -d "$FNM_PATH" ]; then'
echo ' export PATH="$FNM_PATH:$PATH"'
echo ' eval "`fnm env`"'
echo 'fi'
} | tee -a "$CONF_FILE"

else
echo "Could not infer shell type. Please set up manually."
exit 1
fi

echo ""
echo "In order to apply the changes, open a new terminal or run the following command:"
echo ""
echo " source $CONF_FILE"
}

parse_args "$@"
# set_filename
check_dependencies
download_fnm
if [ "$SKIP_SHELL" != "true" ]; then
setup_shell
fi

过程

在配置 Windows Server 下的 SSH 服务端的时候,需要配置可以使用公钥连接,但是搜索了半天资料配置半天还是不行,最后发现应该是权限的问题。

搜索的大部分教程里都是使用的 linux 服务器。部分教程有 Windows Server 的教程,要输好几行命令,而且测试半天也没成功。

解决方法

打开配置文件,Windows Server 下一般是在
C:\ProgramData\ssh\sshd._config
修改
StrictMode no

StrictModes no #修改为 no,默认为 yes.如果不修改用 key 登陆是出现 server refused our key(如果 StrictModes 为 yes 必需保证存放公钥的文件夹的拥有与登陆用户名是相同的.
“StrictModes”设置 ssh 在接收登录请求之前是否检查用户家目录和 rhosts 文件的权限和所有权。这通常是必要的,因为新手经常会把自己的目录和文件设成任何人都有写权限。)
(来源http://matt-u.iteye.com/blog/851158)

试过设置.ssh 文件夹和 authorized_keys 文件的权限,但是都不行,所以暂时用这种方法临时解决一下

发现问题

在使用新设备的时候访问域名,直接输入域名访问,会默认访问 http,但是 web 服务没开启 http 的话就会访问不了。需要手动输入 https://域名..

HSTS

deepseek 搜索结果:

HSTS(HTTP Strict Transport Security) 是一种安全策略机制,用于强制浏览器通过 HTTPS 与服务器通信,防止 HTTP 协议下的中间人攻击(如 SSL Stripping 攻击)。它通过 HTTP 响应头告诉浏览器,在指定时间内(通过 max-age 参数设置),所有与该域名的通信都必须使用 HTTPS。

HSTS 的工作原理

  1. 首次访问:

当用户首次访问网站时,服务器会在 HTTP 响应头中包含 Strict-Transport-Security 字段。

浏览器收到该字段后,会在指定时间内(max-age)记住该网站必须使用 HTTPS。

  1. 后续访问:

在 max-age 有效期内,即使用户手动输入 http://,浏览器也会自动将请求转换为 https://。

如果 HTTPS 连接失败(例如证书无效),浏览器会直接拒绝连接,而不是降级为 HTTP。

HSTS 响应头格式

HSTS 通过 HTTP 响应头 Strict-Transport-Security 来启用,常见的格式如下:

1
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

max-age

指定 HSTS 的有效时间(以秒为单位)。

例如,max-age=31536000 表示有效期为 1 年。

includeSubDomains(可选):

表示 HSTS 策略适用于当前域名及其所有子域名。

例如,如果主域名是 example.com,启用该选项后,sub.example.com 也会强制使用 HTTPS。

preload(可选):

表示希望将域名加入浏览器的 HSTS 预加载列表。

预加载后,即使首次访问,浏览器也会强制使用 HTTPS。

HSTS 的优点

  1. 防止 SSL Stripping 攻击:

攻击者无法将 HTTPS 连接降级为 HTTP,从而保护用户数据安全。

  1. 提升安全性:

强制使用 HTTPS,避免用户意外使用不安全的 HTTP 连接。

  1. 减少重定向延迟:

浏览器直接使用 HTTPS,无需先尝试 HTTP 再重定向。

HSTS 的缺点

  1. 首次访问风险:

在首次访问时,如果用户通过 HTTP 连接,仍然可能受到攻击。

可以通过加入 HSTS 预加载列表来解决。

  1. 配置错误可能导致网站不可访问:

如果服务器配置错误(例如 HTTPS 证书无效),浏览器会拒绝连接,用户无法访问网站。

  1. 回退困难:

一旦启用 HSTS 并设置了较长的 max-age,在有效期内无法回退到 HTTP。

IIS 设置

IIS 网站的绑定里添加 http,然后在 IIS 网站空白页右键,管理网站,HSTS。
启用,将 HTTP 重定向到 HTTPS

需求是要用指定的字符串限制 url 的输入,比如限制 url 只能输入'/user'或者/home,然后想到了之后可能会用动态的 param,'/user/:userId'这种

express 里可以使用app.post("/user/:userId", ...)这样,在下面用req.params来获取userId.

但是在前端发送请求的时候,需要填像axios.get({url: "/user/123"})这样,所以写了一个类型来声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type AllowedPath = "/user/:userId" | "/user"; // 路径

/** 检查字符串是否为空 */
type IsEmptyString<S extends string> = S extends "" ? true : false;

/** 将带/:的路径,转换为'/路径/字符串' */
type Path<P extends AllowedPath> = P extends `/${infer MainPath}/:${string}`
? `/${MainPath}/${string}`
: P;

/** 检查第二个/后面的字符串是否为空,如果为空就返回never */
type Param<S extends string> = S extends `/${infer MainPath}/${infer Param}`
? IsEmptyString<Param> extends true
? never
: `/${MainPath}/${Param}`
: S;

function path<P extends AllowedPath, T extends Path<P> = Path<P>>(
path: Param<T>
) {
console.log(path);
}

path("/user"); // valid
path("/user/123"); // valid
path("/user/"); // 报错,类型“"/user/"”的参数不能赋给类型“never”的参数。

踩坑的一个点是

1
2
3
4
// 刚开始写成这样,发现ts判断不了,导致写 /user/ 也不会报错,需要把Path<P>拆分到泛型参数里才行
function path<P extends AllowedPath>(path: Param<Path<P>>) {
console.log(path);
}

目前只匹配到第二个斜杠 ‘/‘ ,不知道之后有没有更多 param 斜杠的需求。暂时没想到多个 param 的时候有没有更好的写法,应该可以用多个三元判断,或者把拆分 :/ 做成工具函数,然后递归一下。之后遇到了再补充

需求是要拓展 axios 的 get 方法的类型

首先遇到的第一个坑是 declare 会覆盖原有类型的问题

1
declare module "axios" {}

如果这样直接这样写的话,vscode 会报错,axios 自带的类型会被覆盖。记得之前也有过同样的需求,也是这样做的,搞了半天才发现

1
2
import axios from "axios"; // 要把axios import进来,vscode就没报错了
declare module "axios" {}

然后是 axios 的 get 方法是写在 class Axios 里的

1
2
3
4
5
6
import axios from "axios";
declare module "axios" {
class Axios {
get(): void; // 这样不生效
}
}

我项目是用的 axios.create 方法创建出来的,类型是 AxiosInstance,AxiosInstance 是一个 interface,所以就可以拓展了

1
2
3
4
5
6
import axios from "axios";
declare module "axios" {
interface AxiosInstance {
get(): void; // 写具体的get类型
}
}

axios 默认导出的是 AxiosStatic 类型,所以没有用 axios.create 的话,拓展 AxiosStatic 就行了

1
declare const axios: AxiosStatic;

补充一个 axios 类型的另外一个问题
axios 的 AxiosRequestConfig 里的 params 属性,平时我用 get 方法的时候传 query 一般是用这个属性传,但是 AxiosRequestConfig 里 params 是 any 类型

如果需要声明 params 的类型的话可以这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import axios from "axios";
declare module "axios" {
interface CustomRequestConfig<D> extends AxiosRequestConfig<D> {
params?: D;
}
interface AxiosInstance {
get<
T = any, // 这里是response的body类型
R = AxiosResponse<T>,
D = any // 这里填需要的params类型
>(
url: string,
config?: CustomRequestConfig<D>
): Promise<R>;
}
}

HTML 语义化标签

边翻文档边记录一下语义化标签,有些标签的默认样式可以直接用,不用写 CSS 实现了

a

HTML <a> 元素(或称锚元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。

MDN 页面

1
<a href="https://dongmin.fan" target="_blank">这是一个链接</a>

这是一个链接

abbr

<abbr> HTML 元素表示一个缩写词或首字母缩略词。

MDN 页面

1
<abbr>CSS</abbr> (Cascading Style Sheets)

CSS (Cascading Style Sheets)

article

HTML <article> 元素表示文档、页面、应用或网站中的独立结构,其意在成为可独立分配的或可复用的结构,如在发布中,它可能是论坛帖子、杂志或新闻文章、博客、用户提交的评论、交互式组件,或者其他独立的内容项目。

给定文档中可以包含多篇文章;例如,阅读器在博客上滚动时一个接一个地显示每篇文章的文本,每个帖子将包含在 <article> 元素中,可能包含一个或多个 <section>。

MDN 页面

blockquote

HTML <blockquote> 元素(或者 HTML 块级引用元素),代表其中的文字是引用内容。通常在渲染时,这部分的内容会有一定的缩进(注 中说明了如何更改)。若引文来源于网络,则可以将原内容的出处 URL 地址设置到 cite 特性上,若要以文本的形式告知读者引文的出处时,可以通过 <cite> 元素。

MDN 页面

details

HTML <details> 元素可创建一个组件,仅在被切换成展开状态时,它才会显示内含的信息。<summary> 元素可为该部件提供概要或者标签。

MDN 页面

1
2
3
4
<details>
<summary>Details</summary>
Something small enough to escape casual notice.
</details>
Details Something small enough to escape casual notice.

details 元素 summary 上会默认会箭头,可以用 list-style-type: none 来去掉

1
2
3
4
<details>
<summary style="color:red">Details</summary>
Something small enough to escape casual notice.
</details>
Details Something small enough to escape casual notice.

git 常用命令

合并

先切换到目标分支(比如从 dev 合并到 main,main 就是目标库)

1
git checkout main

然后合并,参数是合并的分支(dev 合并到 main,合并的分支就是 dev)

1
git merge dev

冲突的话需要解决冲突,单人开发暂时没遇到,遇到之后补充

prettier 配置相关

配置文件

优先级从高到底

  • package.json中的 prettier 配置
1
2
3
4
// package.json
"prettier": {
// config
}
  • .prettierrc 文件

使用 JSON 或 YAML 格式

  • .prettierrc.json .prettierrc.yml, prettierrc.yaml, 或者 prettierrc.json5
  • .prettierrc.js, prettier.config.js, .prettierrc.ts, 或者 prettier.config.ts 中使用 export default 或者 modules.exports
  • .prettierrc.mjs, prettier.config.mjs, .prettierrc.mts, 或者 prettier.config.mts 中使用 export default.
  • .prettierrc.cjs, prettier.config.cjs, .prettierrc.cts, 或者 prettier.config.cts 中使用 module.exports.
  • .prettierrc.toml

配置

行宽度。默认 80

Tab Width

缩进宽度。默认 2

Tabs

使用制表符缩进而不是空格。默认 false

Semicolons

在语句末尾加上分号。默认 true

Quotes

使用单引号而不是双引号。默认 false

Quote Props

JSX Quotes

Trailing Commas

Bracket Spacing

Bracket Line

[Deprecated] JSX Brackets

Arrow Function Parentheses

Range

Parser

File Path

Require Pragma

Insert Pragma

Prose Wrap

HTML Whitespace Sensitivity

Vue files script and style tags indentation

End of Line

Embedded Language Formatting

Single Attribute Per Line

tsc编译时无法转换路径别名的问题

在tsconfig里使用paths来配置路径别名,使用tsc编译之后的文件里没有转换成正确的路径。
可以使用tsc-alias这个包

1
npm install tsc-alias -D

需要配合tsc来使用,先用tsc编译,再用tsc-alias来转换路径别名

推荐用concurrently来同时开启tsc和tsc-alias

1
npm install concurrently -D
1
2
3
4
5
// package.json
"scripts": {
"dev": "concurrently \"tsc -w\" \"tsc-alias -w\"",
"build": "&& concurrently \"tsc\" \"tsc-alias\"",
},
1
2
3
4
5
6
// tsconfig.json
{
"paths": {
"@shared/*": ["core/shared/*"]
}
}
1
2
// ts文件中使用路径别名导入一个模块
import shared from "@shared/index.ts"
0%