Coder Social home page Coder Social logo

blog's Introduction

blog's People

Contributors

hexh250786313 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

blog's Issues

Xfce4 桌面环境结合 i3wm 窗口管理器

不要点开, 博客网站用的
博文标题图片

pic

博文置顶图片

pic

pic

博文置顶说明
新版的 KDE 和 Gnome 都有类 i3 的窗口管理功能或者插件. 而 Xfce4 的 Xfwm 比较遗憾, 还没有这种类 i3 功能或插件, 本文将介绍如何用 i3wm 替换 Xfwm, 从而实现在 xfce4 环境下使用 i3wm

参考

事先说明

  • 文章开头的图片只是实例图, 并非按照本文介绍的步骤就能实现一样的外观, 这里只是要实现 Xfce4 DE + i3wm 的效果. 要做到实例图的效果, 可以参考我的 xfce4 配置:
  • 笔者系统为 Manjaro 上的 Xfce4, 不保证其他发行版如 Xubuntu的 Xfce4 能有同样的效果
  • 建议读者事先熟悉 i3wm 的使用方法以及其配置方法, 以免安装完后抓瞎

步骤

  1. 执行命令:
    yay -S i3-gaps-next-git
    sudo pacman --remove xfdesktop
    xfconf-query -c xfce4-session -p /sessions/Failsafe/Client0_Command -t string -sa i3
  2. 重启, 然后登录时还是选择 Xfce4, 但是进来后会发现进入了 i3wm, 且保留了 Xfce4 的面板栏, 至此, 大功告成
  3. 恢复方法:
    sudo pacman -S xfdesktop
    yay --remove i3-gaps-next-git
    xfconf-query -c xfce4-session -p /sessions/Failsafe/Client0_Command -t string -sa xfwm
  4. 删除 "键盘" 软件中的快捷键, 删多少读者自行定夺, 这一步主要目的是防止 i3 的默认的 $mod 键和 $sup 键被覆盖, 如果读者心里有数, 一个不删也可以

操作完成后 xfwm 的合成器 (用于实现过渡动画 / 透明效果的软件) 和桌面 xfdesktop (背景墙纸) 当然是不生效了, 那么这时候就推荐 picom 合成器和 feh 墙纸等软件和 i3 配套, 这里不提

xfce4-i3-workspaces-plugin

xfce4-i3-workspaces-plugin 是个可以在 xfce4-panel 上显示当前的 i3 workspace 的插件, 其实还挺有必要的

但这插件的问题在于会有严重的 bug, 就是会在刷新 i3 ($mod + shift + r) 的时候有时会断开与 i3wm 的连接, 于是会不断尝试重连, 又不断失败, 无限循环, 这过程中会不断对 ~/.xsession-errors 写入错误日志, 导致日志文件无限增加导致电脑硬盘容量爆满, 这个 bug 作者至今未修复, 十分让人蛋疼

不过也有个粗暴的解决方案, 就是在刷新 i3 时同时重启整个 xfce4-panel 的容器插件, 只需要在 ~/.config/i3/config 加入以下行即可 (注意 wrapper 的位置, 未必和我的位置一样, 具体以每个人的配置为准, 可以通过 htop 找到这个 wrapper 的位置):

exec_always --no-startup-id killall /usr/lib/xfce4/panel/wrapper-2.0

Linux 系统使用任天堂 Nintendo Pro 手柄以及连接 Steam 的方法 (有线及蓝牙)

不要点开, 博客网站用的
博文置顶说明
前段时间知道了 Proton 这个神器, 心想怎么把吃灰的 Pro 手柄利用起来, 因为默认情况下 Pro 手柄蓝牙连接 Linux 后不能正确识别, 于是有了这篇文章

参考

事先说明

Linux 平台上的 Steam 原本就可以有线识别 Pro 手柄, 所以如果不是为了用蓝牙连接或者玩 Cemu Hook 模拟器 的话, 没有必要使用本文教程, 因为弄了本教程后, Steam 就没法把 Pro 手柄识别为 Pro 手柄了, 而是识别成 Xbox360 手柄, 也就是无法使用体感

但我还是建议你食用本教程, 因为 Steam 上的游戏本来也对 Xbox360 手柄支持度更好, 而且用 joycond 模拟成 Xbox360 手柄后, 除了无法用体感外, 其他例如震动功能都是正常的, 而且 Steam 也支持改键, 更方便. 再说蓝牙连接比有线连接体验感高不少

本人 PC 平台, Manjaro 平台, 内核 5.15.16-1-MANJARO, 不过 5.10 和 5.13 的内核本人亲测都是有效的, 大胆推测以下应该是个通用的方法

基础准备

先装个 dkms 适配内核模块的更新, 下面是 Arch 的安装, 其他发行版都有对应的指令, 自行 Google

sudo pacman -Syu base-devel --needed
mhwd-kernel -li # 查看当前内核版本, 例如:linux513
sudo pacman -Syu $KERNELXYY-headers dkms # $KERNELXYY 就是上面那个内核, 所以是 linux513-headers

安装

  • 安装 dkms-hid-nintendo
    • Arch 可以用这个指令: yay -S hid-nintendo-dkms
  • 安装 joycond
    • Arch 可以用这个指令: yay -S joycond-git
    • 然后启动服务: sudo systemctl enable --now joycond

至此已经完成了所有安装工作, 已经可以正确地蓝牙连接 Pro 手柄

连接步骤

  1. 用电脑的蓝牙连接手柄
  2. 连上后 Pro 手柄下方的 4 个指示灯会一起闪动
    • 这时候如果同时按下左右肩键的话会连接为 Pro 手柄模式, 可以完全识别为 Pro 手柄, 完美支持 Cemu Hook 模拟器, 需配合 joycond-cemuhook 使用
    • 如果同时按下加减键, 则模拟为 Xbox360 模拟器, 支持 Steam

【自用】尝试用最简单的 Vim

不要点开, 博客网站用的
博文置顶说明
试下只用 fugitive 的 Vim 是怎样的。没什么技术含量,都是最简单的实现,注重提供思路。

为什么

Vim 装了一堆插件也比不上 VSC 对 JS 的支持(个人主观暴论),没必要折腾了,就只留个 fugitive 和外观主题,用来做一些简单的编辑工作吧。下面会探讨下怎样实现一些简单的文件导航功能 without plugin!

要实现的东西

全局搜索

说是全局,其实指的还是 git 管理的项目文件夹内部,会借助到 git 插件(所以我说老折腾 Vim 其实没什么必要,不如多学习怎么利用好 git,虽说也不是非要用 git 来实现全局搜索就是了)

  • 字符串查询。Ggrep 是 fugitive 的指令,其实就是 git grep 只不过他帮你把结果输出到 Quickfix 窗口中而已,-i 是忽略大小写,个人感觉最常用,其他的 options 依需求自己加

  • 文件查询。很遗憾,git 并不支持文件查询,点这里可以看到各种搜索插件的功能对比,git-grep 在 File finding 类别中全是空白的(真惨),说明他并不支持文件查询。那么换个思路,把项目底下的文件全部列出来后用 grep 直接查好了(没错 Linux 自带的那个 grep

    1. 打开一个新的文件,再用 git ls-files 列出全部文件然后用 grep 找出目标文件(老实说这样挺蠢的,建议直接装个 ag 或者 rg 插件即可实现了,但是我想尽量用最少的插件来实现嘛,理解理解)
    2. 然后光标移动到要去的文件的文件名所在那一行,<C-w>gf 或者 gf 或者 <C-w>f 即可进入对应的文件。其实也可以把结果输出到 Quickfix 中的(了解下 Vim 的 makeprg),但我不想折腾了
:Ggrep -i "<regex>" # 字符串查询

:tabnew
:r! git ls-files | grep -i "<regex>" # 文件查询

窗口跳转

以前我跳转窗口都是直接把上下左右跳转的组合键 map 到了 <C-h/j/k/l> 上,但是后来感觉最好还是不要动原生的快捷键,于是就把窗口跳转绑定到了 <Leader> 上,我 <Leader> 用的是空格键(修改 .vimrc)。这样就能通过按空格 + 数字的组合跳到任意的窗口中(顺便我把 <C-^> map 到了 <Space><Tab> 上,这个组合真的很顺手)

let mapleader=" "
nnoremap <SPACE> <Nop>
let i = 1
while i <= 9
  execute 'nnoremap <Leader>' . i . ' :' . i . 'wincmd w<CR>'
  let i = i + 1
endwhile

Buffers

  • 跳转:跳转到特定的 buffer,最简单的做法是 :b 之后按 Tab 来选择要去的文件。如果要在两个 buffers 之间反复横跳,用 <C-^> 就行

  • 删除:删掉特定的 buffer 是用 :%bd <number-of-buffer>,但是一般要删都是全删或者保留当前窗口的,所以我用的方法是

    1. 先在当前 buffer 改变文件(例如直接按 o 插入一行)
    2. 然后用 :%bd 删除全部 buffers,这时候有更改但是没有保存的文件就会被保留下来,也就是我新插入行的那个文件没有从 buffers 移除,也就达到了仅保留当前 buffer 的目的

代码注释

  • 单行 I// (假设 // 是注释);多行 <C-v>jjjI//

  • 取消注释:单行 ^dw;多行 <C-v>jjjllld

打开当前文件夹

:split
:lcd %:h | explorer.exe .

指的是用系统解析器打开当前文件夹,lcd 可以单独对某个窗口执行 cd,也就是把某路径设置为工作路径。所以这一段的意思就是把当前文件所在路径设置为工作路径,然后用文件浏览器打开(因为我用的是 wsl,所以直接就是 explorer.exe . 打开了,另外其实 wsl 也提供了 explorer.exe 的直接打开某目录的指令::!explorer.exe `wslpath -w "%:p:h"`

其实可以通过 Vim 的路径获取指令来做很多事情。例如 :!prettier --write %:p 或者 !standard --parser babel-eslint --fix %:p 来对当前文件执行格式化

总结

也就这些了,没什么好整的了,高级点的自动补全、定义跳转这些已经是 IDE 的范围了,非要用 Vim 开发的话也只能上 CoC 这些 Vim 的语言服务了,但是我就是因为用多了插件觉得用 Vim 都没了那个轻灵的感觉了才毅然决然把插件都删掉的,开发还是交给 VSC 这些专门软件吧

Windows 10 增加按键重复速度和减少按键延迟的方法

不要点开, 博客网站用的
博文置顶说明
Win10 默认的可修改的按键延迟即使调到最高,键间延迟依然很高,对于 Vim 用户来说实在慢得受不了,其实通过修改注册表就可以非常简单地增加按键重复速度和减少按键延迟

原文

Increase keyboard repeat rate beyond control panel limits in Windows 10

默认修改方法

控制面板 - 键盘 - 速度:

  • 重复延迟:调到最高
  • 重复速度:最快

这是默认方法,但是即使调到最高,键间延迟依然很高

通过注册表修改

  1. 打开注册表 HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response
  2. 修改如下值:
    "AutoRepeatDelay"="200" 
    "AutoRepeatRate"="6" 
    "DelayBeforeAcceptance"="0" 
    "Flags"="59" 
    "BounceTime"="0"

这个是我自己的设置,每个人根据自己习惯自行调整,其中最影响重复延迟和按键速度的是 AutoRepeatDelayAutoRepeatRateDelayBeforeAcceptance

gh-repo-sync-cli: 一个同步所有 fork 仓库的 CLI 工具

不要点开, 博客网站用的
博文置顶说明
一个可以同时运行多个 "gh repo sync owner/cli-fork" 命令的命令行工具, 以帮助同步所有 fork 的 GitHub 仓库

关于

Required

确保环境中已经安装了 github-cli 并且已经登录了 github 帐号

安装

注意是 gh-repo-sync-cli, 而不是 gh-repo-cli!!! 因为已经有另一个名为 gh-repo-sync 的库了 🤷‍♀

npm install -g gh-repo-sync-cli

使用

gh-repo-sync --help

该工具有两种方法来传递仓库名参数给 gh repo sync

通过命令行

gh-repo-sync owner/repo1 owner/repo2

通过配置文件

配置文件位置: ~/.config/gh-repo-sync/config.json

{
  "repos": [
    "owner/repo1",
    "owner/repo2"
  ]
}

配置完后执行 gh-repo-sync 就会同步所有 repos 数组中的仓库

如果同步过程中有源仓库发生了变化需要进行 PR, 那么工具就会抛出错误

截图

2022-05-25_20-26

用 React.js 做的仿微信/小程序 Emoji 表情组件

不要点开, 博客网站用的
博文标题图片

pic

博文置顶图片

pic

博文置顶说明
用 React.js 做的仿微信/小程序 Emoji 表情组件

Source

react-wechat-emoji

Install

yarn add react-wechat-emoji

Usage

import React, { useState } from 'react'
import { Emoji, ContentWithEmoji, parseEmoji } from 'react-wechat-emoji'

function App () {
  const [text, setText] = useState('')

  const testText = '你好,世界[微笑]'
  const contents = parseEmoji(testText)
  console.log(contents)
  /** 打印结果:
   *   contents = [
   *     { type: 1, content: "你好,世界", imageClass: "" },
   *     { type: 2, content: "[微笑]", imageClass: "smiley_0" },
   *   ];
   *  */

  return (
    <div>
      输入一些东西并点击下方表情窗口的任意表情:
      <input
        type='text'
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <div>
        <Emoji
          recentUsed={[
            {
              cn: '[鸡]',
              code: '',
              hk: '[小雞]',
              id: 214,
              style: 'e2_14',
              us: '[Chick]',
              web_code: ''
            }
          ]}
          source='https://dev.azure.com/hexuhua/f6126346-6e87-4d62-aa80-ff9b88293af0/_apis/git/repositories/ebd79495-5cbb-4565-8573-fa73ee451b5e/items?path=/github.com/hexh250786313/blog/14/xuebi.png&versionDescriptor%5BversionOptions%5D=0&versionDescriptor%5BversionType%5D=0&versionDescriptor%5Bversion%5D=main&resolveLfs=true&%24format=octetStream&api-version=5.0'
          height={300}
          insertEmoji={(emojiText, recentUsed) => {
            setText(text + emojiText)
            console.log({ recentUsed })
          }}
        />
      </div>
      <p style={{ margin: '20px 0 0 0' }}>将会显示你的输入值和解析表情图片:</p>
      <div style={{ backgroundColor: '#eee', padding: 10 }}>
        <ContentWithEmoji
          source='https://dev.azure.com/hexuhua/f6126346-6e87-4d62-aa80-ff9b88293af0/_apis/git/repositories/ebd79495-5cbb-4565-8573-fa73ee451b5e/items?path=/github.com/hexh250786313/blog/14/xuebi.png&versionDescriptor%5BversionOptions%5D=0&versionDescriptor%5BversionType%5D=0&versionDescriptor%5Bversion%5D=main&resolveLfs=true&%24format=octetStream&api-version=5.0'
          emojiScale={0.5}
          content={text}
        />
      </div>
    </div>
  )
}

export default App

Components

types

const textType = {
  normal: 1,
  emoji: 2
}

const emoji = {
  id: 0,
  cn: "[微笑]",
  hk: "[微笑]",
  us: "[Smile]",
  code: "/::)",
  web_code: "/微笑",
  style: "smiley_0",
};

type EmojiContentType = {
  type: typeof textType.normal | typeof textType.emoji;
  content: string;
  imageClass: string;
};

Emoji props

prop default type description
height 300 number Emoji 面板高度
insertEmoji none (emojiText: string, recentUsed?: Array<typeof emoji>) => void 点击表情的回调,参数一是点击的 emoji text,参数二是最近使用表情(如果有开启最近使用功能的话)
source bilibili 图床地址 string Emoji 雪碧图地址,强烈建议使用自己的 CDN 地址,默认是 bilibili 图床地址,稳定性未知
recentUsed none Array<typeof emoji> 最近使用表情,参数仅用作初始化,无初始值传空数组;想关闭此功能则不传此参数

ContentWithEmoji props

prop default type description
content none string | Array<EmojiContentType> 内容
bodyStyle none React.CSSProperties 外层样式
textStyle none React.CSSProperties 文本样式
emojiScale 0.5 number 表情的显示大小
source bilibili 图床地址 string Emoji 雪碧图地址,强烈建议使用自己的 CDN 地址,默认是 bilibili 图床地址,稳定性未知

表情雪碧图

强烈建议使用自己的 CDN 地址,默认是 Azure Git 地址,稳定性未知。右键保存下方的图片到你的 CDN,组件的 source 填上你的 CDN 地址

xuebi

Api

parseEmoji

function parseEmoji(content: string): EmojiContentType[];

Dev

git clone https://github.com/hexh250786313/react-wechat-emoji.git
cd react-wechat-emoji
yarn && yarn start

LICENSE

MIT

一口气搞定 WSL2 的网络问题

文章简介
博文置顶说明
本文介绍 wsl2 的端口转发、网络代理等问题,不会或者少量涉及 wsl2 的安装部署,因为安装教程很容易能找到。这里着重于端口转发、网络代理等容易遇到的坑

一、相关

二、关于 WSL2 的 IP

2.1. 要解决的问题

  1. 把 wsl2 的端口暴露给局域网

2.2. 端口转发

上述问题,可以通过端口转发的方式,让 windows 把 wsl2 的端口转发暴露给局域网,因此需要搞定三件事情:

  1. windows 需要能获取到子系统的模拟 ip 地址(不再需要获取子系统 ip,可以利用 localhost 来映射)
  2. windows 转发对应子系统端口
  3. 破除防火墙的限制

以上三个问题的对应方案如下:

2.2.1 获取子系统的虚拟 IP 地址(这一步已废弃,请直接看下文的 2.2.2)

============================
※ 注意,本第三节已经废弃,不再需要使用这种迂回的方式来映射子系统的 ip

※ 对于获取子系统 ip 不感兴趣的可以直接跳到文章第 2.2.2 小节端口转发

※ 本节不删除只用作存档,修改时间:2024/1/20
============================

  1. go-wsl2-host 下载该软件,这个脚本可以帮忙在 windows 系统中生成一个服务用来获得子系统的虚拟 ip 地址
  2. 在 powershell(后面的章节简写为 pwsh)中用管理员权限安装他:.\wsl2host.exe install
  3. 然后按照提示输入你当前的 windows 服务账号和密码,如果以前没有添加过服务账号,请看第 6 步的补充说明
  4. 重启电脑
  5. 检查服务有无启动成功:
    • 查看 windows 服务中有没有 wsl2host
    • 查看 hosts 文件中有没有多出一行类似于:192.168.82.59 ubuntu.wsl # managed by wsl2-host
    • 都没问题即说明这时候已经可以在 pwsh 脚本中用 ubuntu.wsl 来获取到子系统的虚拟 ip
  6. 补充说明:
    • 第三步的账号密码可以通过以下方式添加:windows 管理工具-本地安全策略-本地策略-用户分配权限-作为服务登录-添加当前登录的微软账号或本地账号(然后检查名称),然后 install 的时候用添加上去的那个账号名,例如你的微软账号可能是 [email protected],但是添加上去之后的账号名可能是 12345,所以以显示的账号(12345)为准
    • 如果你的系统上没有“本地安全策略”,那么说明你的 windows 版本如家庭版不支持这一操作。但别慌,可以用以下这段脚本下载“本地安全策略”到系统中(保存为 .bat 文件并以管理员权限打开):
      pushd "%~dp0"
      dir /b C:\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtensions-Package~3*.mum >List.txt
      dir /b C:\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientTools-Package~3*.mum >>List.txt
      for /f %%i in ('findstr /i . List.txt 2^>nul') do dism /online /norestart /add-package:"C:\Windows\servicing\Packages\%%i"
    • 家庭版有时候更新完系统后 wsl2host 服务会失灵,这时候重新 install 即可,重新 install 的时候可能会提示 keys already exist,这时候 remove 一下即可,remove 时会提示错误(不存在 wsl2host 服务),不用在意,只需要知道这时候已经移除了帐号了就行
    • 有的朋友可能会遇到死活无法安装上 wsl2host 服务的情况, 那也有个麻烦点的做法, 就是使用 .\wsl2host.exe debug 指令, 这样也可以手动给 hosts 添加上新的 ip, 这样需要每次开机都手动调下指令 (原因是每次开机宿主机 ip 都不一样), 挺麻烦的

2.2.2. 端口转发

  1. pwsh 中执行:notepad $profile 打开 pwsh 的配置文件,接下来要写两段脚本

  2. 在配置文件中添加如下代码并保存。另外下面的脚本中有 sudo 指令,这个要求你的 pwsh 要事先安装了 sudo 插件,这个是用来获取管理员权限的,请自行搜索安装方法。对于脚本里面的 192.168.10.68 就是你需要映射的端口,一般就是你的宿主机的内网 ip,自行修改,下面是代码:

    function setWslNetsh {
        param (
            $Port
        )
        sudo netsh interface portproxy add v4tov4 listenport=$Port connectaddress=localhost connectport=$Port listenaddress=192.168.10.68 protocol=tcp
        Write-Output "✔ Port($Port) now is out!"
    }
    
    function unsetWslNetsh {
        param (
            $Port
        )
        sudo netsh interface portproxy delete v4tov4 listenport=$Port listenaddress=192.168.10.68 protocol=tcp
        Write-Output "✔ Port($Port) now is not out!"
    }
    
    Set-Alias wsl-netsh-set setWslNetsh
    Set-Alias wsl-netsh-unset unsetWslNetsh
  3. 上面这段代码就是实现端口转发的主要脚本,这时候在 pwsh 中执行:wsl-netsh-set <port> 就能让 windows 把子系统的端口转发出去。例如:wsl-netsh-set 8000 就能把子系统中端口号为 8000 的进程转发出去

  4. 同样的 wsl-netsh-unset <port> 则是取消转发

  5. 另外还可以在子系统中去执行这个指令:pwsh wsl-netsh-set <port>pwsh wsl-netsh-unset <port>

  6. 原理补充:wsl2 的流量转发原理就是会把 wsl2 中的使用到的端口通过 localhost 转发的宿主机,例如你在子系统起了一个 http://localhost:8000 的服务,那么你不需要做任何事情,就可以在宿主机通过 http://localhost:8000 访问到这个服务,而这个脚本就是基于这个原理,通过 netsh 进行流量转发,把特定的 <ip>:<port> 的访问流量转发到 localhost:<port>,这样就能把子系统的端口暴露给局域网了

  7. ipv6 说明,上面的脚本是针对使用了 ipv4 的服务的,从 v4tov4 你就可以看出来,如果你子系统使用的是 ivp6 服务,那么你也可以这样来设置转发和取消转发,你可以自行调整 v4tov6 这一处,看你的宿主机是想用什么 ip 协议来访问这个子系统端口

    sudo netsh interface portproxy delete v4tov6 listenport=8000 listenaddress=192.168.10.68
    sudo netsh interface portproxy add v4tov6 listenport=8000 listenaddress=192.168.10.68 connectport=8000 connectaddress=::1
    
  8. 每次宿主机开机以后你会发现明明上一次开机已经进行过端口映射了,但是为什么这次开机后不生效,这是预料之中的事情,你只需要每次开机完毕后都用 wsl-netsh-unset <port> 取消转发,然后再重新设置一下转发就行了,你可以通过 netsh interface portproxy show all 看到当前有哪些进行了流量转发的地址和端口

2.2.3. 破除防火墙限制

最简单的做法其实就是在防火墙中添加出站和入站规则,添加一下需要暴露到局域网的端口即可。当然也可以把这个写成脚本,需要暴露什么端口直接执行脚本就行,同样在 pwsh 的配置文件中添加代码:

function setFWPort {
    param (
        $Port
    )
    $Port4WSL = "Port4WSL-" + $Port
    $NetFirewallRule = Get-NetFirewallRule
    if (-not $NetFirewallRule.DisplayName.Contains($Port4WSL)) {
        # sudo Remove-NetFireWallRule -DisplayName $Port4WSL
        sudo New-NetFireWallRule -DisplayName $Port4WSL -Direction Outbound -LocalPort $Port -Action Allow -Protocol TCP
        sudo New-NetFireWallRule -DisplayName $Port4WSL -Direction Inbound -LocalPort $Port -Action Allow -Protocol TCP
        Write-Output "✔ New rule for WSL(Port: $Port)!"
    }
    else {
        Write-Output "✔ Rule for WSL(Port: $Port) exists!"
    }
}

function unsetFWPort {
    param (
        $Port
    )
    $Port4WSL = "Port4WSL-" + $Port
    $NetFirewallRule = Get-NetFirewallRule
    if (-not $NetFirewallRule.DisplayName.Contains($Port4WSL)) {
        Write-Output "✔ Rule for WSL(Port: $Port) not exists!"
    }
    else {
        sudo Remove-NetFireWallRule -DisplayName $Port4WSL
        Write-Output "✔ Rule for WSL(Port: $Port) removed!"
    }
}

Set-Alias fw-port-set setFWPort
Set-Alias fw-port-unset unsetFWPort

然后就能用 fw-port-set <port> 来创建新的出入站规则,用 fw-port-unset <port> 移除规则。规则名称定为了 Port4WSL-<port>,可以自行修改

完成上述步骤,就可以把子系统的进程端口暴露到局域网了,例如我在子系统起了一个服务,端口为 8000,windows 在局域网的地址为 192.168.1.215,那么局域网下的其他设备就能通过访问 http://192.168.1.215:8000 访问我子系统下的服务

coc-list-files-mru 中文说明

不要点开, 博客网站用的
博文置顶说明
又造了个 coc 玩具,大概的作用同 coc-list files,但是没有输入值时展示的是 mru 列表。但是使用这个插件需要 hack coc 的编译文件。

简介

在 hack 过的 coc.nvim 上运行的文件查找器插件。您可以打开一个和 coc-list files 一样的文件查找器,但如果没有输入值,它将显示默认的 mru 列表。

它依赖于支持动态切换列表数据的 coc.nvim 客户端。这意味着你必须手动修改 coc 的编译文件。

不优雅但有用。

大多数代码来自 https://github.com/neoclide/coc-lists

安装

您需要安装 coc.nvim 才能使用此扩展。然后运行:

:CocInstall @hexuhua/coc-list-files-mru

同时,因为coc不支持动态切换列表数据,你必须进行以下修改来 hack coc.nvim。

找到您的 coc.nvim 安装路径并在 /path/to/your/coc.nvim/build/index.js 中进行以下更改:

或者,如果您使用 sd,您可以执行这些命令来达到相同的结果:

sd '}\n.*set loading\(loading\)' '  this.mruFlag = true; } set loading(loading)' /path/to/your/coc.nvim/build/index.js
sd 'async drawItems\(\) \{' 'async drawItems(context) { var _a2; if (((_a2 = this.list) == null ? void 0 : _a2.name) === "filesMru") { if ((context == null ? void 0 : context.input.length) > 0 && this.mruFlag === true) { this.mruFlag = false; await this.loadItems(context); await this.drawItems(context); return; } if ((context == null ? void 0 : context.input.length) === 0 && this.mruFlag === false) { this.mruFlag = true; await this.loadItems(context); await this.drawItems(context); return; } }'  /path/to/your/coc.nvim/build/index.js
sd 'void this.worker.drawItems\(\);' 'void this.worker.drawItems(this.context);' /path/to/your/coc.nvim/build/index.js

使用

:CocList filesMru

配置

这插件会读取 coc-list files 和 mru 的配置。

perl 正则中后置约束贪婪匹配字符过长的问题

不要点开, 博客网站用的
博文标题图片

pic

博文置顶图片

pic

博文置顶说明
最近写一些文本处理脚本的时候遇到了使用 perl 提示 "Lookbehind longer than 255 not implemented in regex" 这样的错误, 不是什么大问题, StackOverflow 里也能找到答案, 但是中文互联网上却没有相关的条目, 于是这里稍微记录下

相关

背景

我的脚本调用了 perl 来做文本处理, 里面有一个正则用到了后置约束:

perl -0777 -i -pe "s/(?<!(.*\\S.*))name:.*/name:\ 'hexh'/gi" ./test.txt

目的是把不带任何非空前缀的 name 值改为 hexh, 例如:

// source:
  name: 'hexuhua'
  fullname: 'hexuhua'

// expected:
  name: 'hexh'
  fullname: 'hexuhua'

但是却报错了:

Lookbehind longer than 255 not implemented in regex m/(?<!(.*\S.*))name:.*/ at -e line 1.

方案

根据这篇博客: http://blogs.perl.org/users/tom_wyant/2019/03/native-variable-length-lookbehind.html

Now, there is at least one restriction. No lookbehind assertion can be more than 255 characters long. This limit has been around, as nearly as I can tell, ever since lookaround assertions were introduced in 5.005. But it has been lightly documented until now. This restriction means you can not use quantifiers * or +. But bracketed quantifiers are OK, as is ?.

大概翻译下: 任何 lookbehind 断言的长度都不能超过 255 个字符, 自从 5.005 版引入 lookbehind 断言以来, 这个限制就一直存在, 这个限制意味着你不能使用 .* 或者 .+ 这样的贪婪匹配, 而用非贪婪匹配如: 大括号限制 255 字符内或者 .? 的形式则是没问题的

那么也就是说对于上述正则: s/(?<!(.*\\S.*))name:.*/name:\ 'hexh'/gi 的问题就出在了后置约束 ?<!(.*\\S.*) 中, perl 要求约束中的字符不能用贪婪匹配且少于 255 个字符

由此分析, 可以改成类似这样的形式: ?<!({0,127}\\S.{0,127}), 保证括弧内的字符数量少于等于 255 个即可

最终命令如下:

perl -0777 -i -pe "s/(?<!(.{0,127}\\S.{0,127}))name:.*/name:\ 'hexh'/gi" ./test.txt

值得一提

值得一提的是, 后置约束对于 perl 来说依然属于实验性功能, 每次用后置断言后它都会有这样的提示:

Variable length negative lookbehind with capturing is experimental in regex;

基于 NextJS + Vercel + Github Issues 的博客开发(三)

不要点开, 博客网站用的
博文置顶说明
本文主要说明对博客项目进行 SEO 优化,让其可以出现在 Google Search 的搜索结果中,并且让网站能够通过 sitemap 测试,使所有的网页都可以被爬取出来。最后谈谈 Google Search 的收录机制

相关链接

  1. 基于 NextJS + Vercel + Github Issues 的博客开发(一)
  2. 基于 NextJS + Vercel + Github Issues 的博客开发(二)

源码和博客地址

  1. 源码
  2. 线上地址

让 Google Search 收录网站

Google Search Console

Google Search Console 是 Google 提供的网站收录和管理服务的控制台,在这里可以验证你需要让 Google 收录的网址并管理查看点击率和访问流量等信息

image

验证网站

以本博客为例子,来操作如何让 Google Search 收录网站。首先在在网址前缀那一边输入要验证的网站的网址

image

等待验证之后会出现下面的步骤

image

这里一共有5种验证方式,我用的是第一种文件验证。先把这个 html 文件下载下来,之后丢到本项目下的 📂 public 文件夹即可。然后再点击上图的验证,片刻后会出现验证成功

这时候没有意外的话等待 Google 去爬取下来就行,体感在1天多之后就可以在 Google Search 上搜索到了,在输入框中输入 site:<YOUR SITE> 即可看到出现在搜索结果中

image

接着可以回到 Google Search Console 中点击左上角的搜索资源,再点击刚验证网站

image

就可以进入网站管理,如下图

image

其中比较值得注意的是覆盖率,刚收录的网站一般没那么快记录得到覆盖率,需要过一段时间让 Google Search 完全爬取你的网站后,这里才会显示相关数据。当然在此之前我们可以手动上传 sitemap 地图,增强 Google Search 爬取网站的效率

站点地图

所谓站点地图就是标识了你的网站中能够被爬取到的网站,你需要给你网站的每一个页面提供引荐来源,才能让爬虫顺利进入对应的网页,最简单的做法就是使用 a 标签。例如下图,如果你想让你的这些被圈出来的页面被爬取到,你就需要在这些地方使用 a 标签来处理这些链接

image

然后我们可以对进行 sitemap 测试,用这个网站,把 url 输入进去等待片刻就能生成你的网站的 sitemap,可以查看到结果,结果列表显示了你的网站里能被正常爬取到的网站,如果没出现在上面,你可能需要对网站作进一步的优化,例如把所有的跳转链接都加上 a 标签

image

可以看到 sitemap 文件其实是一个 xml 格式的文件,sitemap.xml

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
    <!--  created with Free Online Sitemap Generator www.xml-sitemaps.com  -->
    <url>
        <loc>https://blog-front-git-dev.hexuhua.now.sh/</loc>
        <lastmod>2020-10-24T16:26:23+00:00</lastmod>
        <priority>1.00</priority>
    </url>
    <url>
        <loc>https://blog-front-git-dev.hexuhua.now.sh/post/list?page=1</loc>
        <lastmod>2020-10-24T16:26:23+00:00</lastmod>
        <priority>0.80</priority>
    </url>
</urlset>

其中 priority 是指这个网页对于网站的重要性,但是事实上这个值完全不会影响 Google Search 的搜索排名,所以不需要管

接着同样把你生成的 sitemap.xml 扔进项目的根目录(本项目为 📂 public 文件夹),然后在 Google Search Console 的站点地图中输入你的站点地图文件即可

image

项目 SEO 优化

上面提到,想要让 Google Search 爬取到你的每一个网页,那就要使用 a 标签来给你的网页添加引荐来源,所以我只需要把本项目使用 Router.push() 地方改造成用 <Link><a>链接</a></Link> 的形式即可,包括 antd 的分页器的点击事件也要改造成这个形式,幸好 antd 早就预料到此处的 SEO 优化的坑,所以是支持开发者自己重新渲染分页器的组件的,改变 Pagination 组件的 itemRender 即可:

import Link from 'next/link'

const itemRender = (current, type, originalElement) => {
  if (type === `page`) {
    return (
      <Link href={`/post/list?page=${current}`}>
        <a target="_self">{current}</a>
      </Link>
    )
  }
  return originalElement
}

关于服务端渲染

而这里其实我遇到了另一个坑,涉及到服务端渲染的

NextJS 的特性是可以在初始化页面时在服务端完成数据的获取,直接把渲染完成后的页面丢到浏览器,这跟目前主流的 Ajax 页面的渲染局部——再获取数据——再渲染剩下的部分的做法不一样。当然 NextJS 项目在浏览器以后其渲染行为也是采用 Ajax 的方式的,区别在于第一次渲染是服务端渲染而已

实现这一点用到了 NextJS 的 getInitialProps 方法

Home.getInitialProps = async props => {
  const { isServer, store } = props.ctx
  if (isServer) { // 可以根据这个值判断 process 是否处在服务端,是的话请求数据并渲染,不然的话说明处在客户端,此时交给 React 的生命周期组件来实现请求数据的逻辑
    // fetch data
  }
  return { isServer }
}

而坑点就出在了 props.ctx 上,这个对象可以获得当前页面的 url 和 search 等属性或者 redux 的 store 状态方法等,但是偏偏不能获得当前页面的 hash 值。所以就跟上一篇提到的用 hash 值来获取当前页数的做法就失效了,这时候就相当于无法在服务端得知当前列表页的页数,就失去了客户端渲染的意义了,无法得到正确页面数据,也就失去 SEO 优化的意义

其实解决办法也很简单,虽然无法获取 hash 值,但是 props.ctx 中依然能正确获得网址的 search 值,也就是网址上 ? 参数后的值,所以根据上一篇的内容,只需要稍作修改,把 hashChangeComplete 改成 routeChangeComplete ,然后把路由字符串中用到了#的地方换成 ? 即可

关于 SEO 的一些想法

在我把 sitemap 完成并上传后,理论上 Google Search 就会去爬取我的网页,但是事实上并不是所有的网页都被收录了,在覆盖率那里可以看到,我的有效网页只有首页和列表页,我的所有文章详情页都标识为已排除,实际搜索中也只有首页和列表页出现在了搜索结果中

image

这是很奇怪的事情,而且当我在控制台上方输入某个文章详情页的地址进行检查的时候,他会显示尚未收录,但是也没有提示是什么原因

image

后来我找了一些资料,发现了以下内容

根据 “SEO 学” 学家 Brian Dean 的说法(来自 Google Search Console):

“Crawled – currently not indexed”
Hmmm…

These are pages that Google has crawled, but (for some reason) are not indexed.

Google doesn’t give you the exact reason they won’t index the page.

But from my experience, this error means: the page isn’t good enough to warrant a spot in the search results.

So, what should you do to fix this?

My advice: work on improving the quality of any pages listed.

For example, if it’s a category page, add some content that describes that category. If the page has lots of duplicate content, make it unique. If the page doesn’t have much content on it, beef it up.

Basically, make the page worthy of Google’s index.

翻译一下:

“已抓取,尚未收录”
嗯...
有些页面会被 Google 抓取,但因为某些原因没有被收录
Google 并没有给出明确的说明为何他们不收录这些页面
但是根据我的经验,问题可能出在了:这些页面的质量不足以在搜索结果中占有一席之地
所以,改怎么解决这个问题呢?
我的建议是:致力于提高你的这些网页的质量
例如,如果有一个目录页面,你可以添加一些内容来描述这个目录;如果一个页面有大量的重复内容,你可以删掉重复的部分;如果页面的内容不够丰富,那就丰富他
结论就是,让你的页面有足以让 Google 去收录的价值

结合我那几个被收录和没被收录的来看一下吧:

  1. 被收录的页面://post/list
  2. 未被收录的页面:/post/6/post/7/about
页面地址 说明
/ 主页面,内容是我当前所有文章(共3篇)的标题 + 描述 + 描述图片
/page/list 列表页面,内容是当前带有 post (共2篇)标签的文章的标题 + 描述+ 描述图片
/about 博客说明页面,大约60个字
/post/6 自用软件清单,大约50个字
/post/7 测试页面,几张图 + 几个字

我不是很确定为什么主页面和列表页这种目录索引页也能收录进去,但是剩下那三个页面确实是没什么质量的页面,不收录也是情理之中

结语

所以以上就是我本次博客开发的所有体验,收获还是不少的,希望也能给正在浏览的同学提供一点帮助

记录一次失败了的在 Manjaro 系统安装 deb 包的过程

不要点开, 博客网站用的
博文置顶说明
deb 包是由软件 apt 包管理系统管理的的包, 常用于 Debian 系统的软件安装, pacman 系的系统无法直接安装 deb 包软件. 一般而言, 可以借助 debtap 这类工具来二次打包为 tar.zst 包, 再由 pacman 进行安装. 但是这种二次打包的方式并不为人所推荐, 即使是 debtap 的作者也不推荐利用这种方式来转包. 本文记录了笔者的一次转包过程 ( 结果是失败了 ), 探讨了这种转包方式的: 原理 / 可能遇到的问题 / 兼容性的处理

关于

转包 aTrust.deb 的可行性分析

aTrust 是深信服的一款产品, 主要用于在电脑中建立公司的工作环境网络, 类似 openvpn, Linux 版本只提供了 deb 包

我的需求是在我的工作环境 Manjaro 系统中装上该软件

aTrust.deb 官方兼容性

compatibility

上图 ( 来源 ) 可见, 官方支持的 Linux 系统只有 UOS 和 麒麟, 而 UOS 来自于 deepin 魔改, 可见 deepin 也支持

deb 包和 tar.zst 包

// sudo dpkg --install foo.deb
// foo.deb 解包后:
deb
├── DEBIAN
│   ├── control
│   ├── postinst
│   ├── postrm
│   ├── preinst
│   └── prerm
└── (others)
  • deb: 该包中的
    • DEBIAN 文件夹就是存放安装信息和安装脚本的地方
      • control 记录了包信息
      • postinst / postrm 等顾名思义是 hook, 里面是具体的脚本代码
      • 另外 DEBIAN 文件夹里可以放置自定义 sh 脚本文件, 供给 hook 调用
    • (others) 指待安装到系统的文件如 /opt/Something / /usr/bin/MyApp
// sudo pacman -U foo.tar.zst
// foo.tar.zst 解包后:
tar.zst
├── .INSTALL
├── .MTREE
├── .PKGINFO
└── (others)
  • tar.zst: 该包

    • .PKGINFO 是存放安装信息

    • .MTREE 我也不太理解, 请看这里

    • .INSTALL 同样是脚本, 里面的代码结构描述了和上面 deb 包的 hook 脚本一样的钩子

      post_install() {
        echo "do something" # 此处的代码理论上与 deb 中的 postinst 一致
      }
      
      pre_remove() {
        echo "do something" # 此处的代码理论上与 deb 中的 postrm 一致
      }
    • (others) 指待安装到系统的文件如 /opt/Something / /usr/bin/MyApp

由上文可得知, 虽然两种打包系统的安装包文件结构不同, 但是总体的安装包的设计思路还是十分类似的:

  • 安装信息 ( 包名 / 依赖等 )
  • 安装的生命周期

debtap 可以帮我们直接把 deb 包转成一个模板化的 tar.zst 包, 即:

  1. 按照转换规则进行转化 ( 例如把 postins 脚本的代码注入进 .INSTALLpost_install 函数中 )
  2. 以及帮我们解决依赖问题 ( 本文不提 )

这种方法的坑点如下:

  1. 不会打包 DEBIAN 中的内置脚本. 例如有一种情况: DEBIAN 中写有自定义脚本 helpers.sh, 钩子脚本 post_install 会调用这个 helpers.sh 来帮忙安装. 那么 debtap 在转的时候会无视这些自定义脚本, 统统不会打包进 tar.zst
  2. debian 和 arch 系统结构的差别可能会导致文件复制的时候找不到路径

因此只要先用 debtap 把 deb 包初步转成 tar.zst 包, 再根据需求修改里面的 .INSTALL 代码, 理论上可以实现本次需求

转包过程

准备

  • aTrust.deb
  • debtap
  • dpkg

解包 deb

这一步主要是查看源 deb 的打包脚本和取得 debtap 不会帮忙打包的冗余文件

mkdir -p extract/DEBIAN && dpkg -X aTrust.deb ./extract && dpkg -e aTrust.deb ./extract/DEBIAN

然后获得文件夹 extract, 里面就是源 deb 包的安装文件和脚本

debtap 转包

利用 debtap 初步转包

sudo debtap -u # 更新库
debtap aTrust.deb

过程中要求输入包名

debtap 会帮你分析出哪个依赖包你当前的系统不满足, 然后要求你编辑依赖信息, 你可以根据自己的需求进行删除

例如 aTrust 要求一个 deepin 的环境监测包 deepin-elf-verify, 根据博文 deepin-elf-verify 究竟是何物? 可知 deepin-elf-verify 为空包, 于是直接从依赖中删掉

最终得到 tar.zst 包 aTrust.pkg.tar.zst

然后解开他: mkdir pkg && cp aTrust.tar.zst ./pkg && cd ./pkg && tar -I zstd -xvf aTrust.pkg.tar.zst

得到 tar.zst 的所有文件和脚本

对比脚本

分别对比 deb 包里和 tar.zst 包里的钩子脚本

参数

首先发现 deb 的脚本中大量存在参数的获取, 例如脚本路径

SCRIPT_PATH=$(dirname "$0")

但是 pacman 的安装是没有参数传递的, 因此需要改造, 这里我直接写死了路径:

SCRIPT_PATH=/home/MyPath

同理, 所有的需要参数的地方都被我写死为具体的值

未被打包的文件

同时也发现了 deb 包中有几个 sh 脚本未被 debtap 打包进 tar.zst 中, 于是观察这些脚本被调用的方式, 发现是钩子直接调用了同级目录的脚本: sh -c ./xxx.sh, 于是我把这些类似的调用都改成了 sh -c ~/Desktop/aTrust/sh/xxx.sh 这样的形式, 也就是固定他的调用路径, 然后把这些脚本放到桌面中: ~/Desktop/aTrust/sh/, 然后给执行权限: chmod +x ~/Desktop/aTrust/sh/

重新打包

重新打包 deb 包得到 atrust2.deb

dpkg -b ./extract aTrust2.deb

再次转包

debtap aTrust2.deb

然后同样和上面一样也是删掉依赖: deepin-elf-verify, 然后得到 aTrust2.pkg.tar.zst, 安装:

sudo pacman -U aTrust2.pkg.tar.zst

结果

结果和一开始相比果然有了变化:

  1. 安装 log 显示能进行最后一步的安装了, 一开始因为脚本缺失的原因没有走到最后一步
  2. 能启动 aTrust 的 GUI 界面, 一开始点击启动没有任何变化

但是从本质上来说, 和一开始比也没有不同: 同样无法启动代理. 这一点我看 aTrust 的日志以后, 发现直接原因是有一个 (登录) 服务没法启动, 这点与我在 Ubuntu22 上的实验结果完全一样 (Ubuntu22 上也没成功, 但 Ubuntu20 成功了)

最后推测原因可能有两个:

  1. 依赖 deepin-elf-verify 被删除了, Ubuntu22 也是装不上这个包, 原因是 openssl 的版本问题
  2. openssl 的缘故, 我 manjaro 系统的 Ubuntu22 的 openssl 版本都是较新的, 考虑到这种代理软件多半需要调用 ssl 的库, 那么很可能就是这个原因

不过这些都是主观猜测, 没有任何证据

最终解决办法

最后起了一个 Ubuntu20 的虚拟机, 在虚拟机上启动 aTrust 后, 利用 tinyproxy + microsocks 给 host 机输出一个代理端口

一口气搞定 WSL 的文件共享(Samba)

不要点开, 博客网站用的
博文置顶说明
本文主要介绍如何取消 Win10 的文件共享,让 WSL 或 WSL2 的 Samba 共享服务代替 Win10 的共享,让局域网可以访问 WSL 中的共享文件

参考

关闭 Win10 中的相关端口

Linux 中的 Samba 服务主要用到 139 和 445 这两个端口,因此首先要取消在 Win10 中的这两个端口

  • 139 端口:网络和 Internet - 更改适配器选项 - 右键当前网络对象属性 - 选中 TCP/IPv4 属性 - 高级 - WINS - 禁用 TCP/IP 上的 NetBIOS
  • 445 端口:服务 - 找到 Server 服务 - 启动类型禁用 - 重启

WSL samba 配置

安装

sudo apt-get install samba

配置

sudo vi /etc/samba/smb.conf

在最末尾添加类似如下的代码(share 是共享文件夹的名字,path 是路径,自行替换):

[share]
  path = /home/me/share
  available = yes
  browseable = yes
  writable = yes
  public = yes

重启 samba

sudo service smbd restart

暴露端口(WSL2 需要,WSL1 不需要)

把上述的 139 和 445 端口暴露出去并创建防火墙规则,具体做法参考这篇:一口气搞定 WSL2 的网络问题

一些 samba 的使用

  • 分享的文件夹权限问题,一般来说需要 777 权限: chmod 777 /YourShare,但是需要在其更高级别的权限上设置,例如: 要分享 /home/my/Desktop 这个文件夹,需要执行 chmod 777 /home/my 保证其上级文件夹 /my 的权限(当然 Desktop 也有可能权限不够,可能还需要再执行一遍 chmod 777 /home/my/Desktop),结论是需要保证每一级的文件夹访问权限都是可访问的
  • 可以通过 smbclient //localhost/<SharedDirName> 指令来进入分享目录测试 smb 是否已经成功,需要输入访问密码,如果设置了 public = yes,此处可以不输入密码直接回车进入目录
  • 执行完 smbclient //localhost/<SharedDirName> 后进入分享目录,可以通过 ls 测试是否可以访问目录,如果有如下提示说明权限设置不足:

    NT_STATUS_ACCESS_DENIED

Sentry 在 React 端的上报格式规范和上报优化方案

不要点开, 博客网站用的
博文置顶说明
Sentry 在 web 端的主要应用集中在错误捕获和性能数据上报。本文旨在总结如何优化 Sentry 的上报方式,以实现更丰富的信息收集和减少接口请求量。本文将分享一些个人经验,以规范上报格式并提供实用的优化方案。请将本文视为一篇启发性的文章,希望能为你提供有价值的参考。

前言

Sentry 的搭建可以参考这个文章:一口气完成 Sentry Docker 部署,本文不会赘述

本文针对的是 Sentry 在 React 端的上报优化,对于 web 端的上报同样具有一定的参考意义,主要总结的是以下内容:

  1. Sentry 的异步初始化
  2. Sentry 性能上报优化
  3. 报文信息优化

一、Sentry 初始化优化方案

1.1. 异步初始化

根据 Sentry 的官方的使用例子,Sentry 会以同步的方式在程序启动时进行初始化。那么为了防止 Sentry 的初始化会堵塞进程或者 Sentry 加载过程中自身发生了错误导致程序初始化失败,那么我们可以把它变为了异步加载,如下:

async function runSentry() {
  const Sentry = await import("@sentry/react");
  Sentry.init(sentryOptions);
  // 给全局加上 Sengtry 实例,方便用于手动上报
  window.Sentry = Sentry;
}

runSentry();

1.2. 处理 Sentry 初始化前的错误

1.2.1. 捕获初始化前的错误

异步加载 Sentry 会带来另一个问题,那就是如果程序初始化时的同步任务发生了错误,这些错误就会丢失在 Sentry 初始化前。

为了解决这一点,可以在程序初始化的最开始先注册一个全局的错误监听,收集这些没被 Sentry 捕获的错误,然后在 Sentry 初始化完成后统一上报,如下:

const unhandledErrors: Error[] = [];

function addUnhandledErrors(e) {
  if (!window.Sentry) {
    unhandledErrors.push(e.error);
  }
}

window.addEventListener("error", addUnhandledErrors);

function reportUnhandledErrors() {
  if (unhandledErrors.length !== 0 && window.Sentry) {
    unhandledErrors.forEach((error) => {
      // 手动上报这些错误
      window.Sentry.captureException(error);
    });
  }
  // 上报结束后取消这个全局监听
  window.removeEventListener("error", addUnhandledErrors);
}

function runSentry() {
  const Sentry = await import("@sentry/react");
  Sentry.init(sentryOptions);
  window.Sentry = Sentry;
  // Sentry 初始化完成后消化未被捕获的错误
  reportUnhandledErrors();
}

runSentry();

1.2.2. 系统崩溃导致 Sentry 无法初始化的情况

还有一种情况就是在 runSentry 之前已经发生了会导致系统崩溃的错误(例如 SyntaxError),让整个程序停止执行,这时候连 Sentry 都不会被初始化。遇到这种情况,我们依然希望 Sentry 能被初始化,然后上报这个致命错误。

这种情况下因为无法判断 Sentry 完成加载的时机,因此可以增加一个倒计时来触发 Sentry 的初始化,在出现错误之后的 n(ms) 后如果 Sentry 还没有初始化完毕,则自动触发 runSentry

注:上方的 n(ms) 可以是任意值,只要你觉得正常情况下你的网站中 Sentry 可以在这个时间内完成加载即可。这里建议可以设置为正常情况下你的 web 程序的首屏渲染结束的平均时间(这需要你自己进行统计),可以使用 LCPFCP 的平均时间。

下方是示例:

const unhandledErrors: Error[] = [];

function createCountdown(
  countdownDuration: number,
  callback: () => void
): () => void {
  let timer: NodeJS.Timeout | null = null;

  return function startCountdown(): void {
    // 不允许重复生成倒计时
    if (timer) {
      return;
    }

    timer = setTimeout(() => {
      callback();
      timer = null;
    }, countdownDuration);
  };
}

// 15000 为一个月里统计的线上 web 程序的 lcp 平均时长
// 视你的 web 程序的实际情况而定
const sentryCountdown = createCountdown(15000, () => {
  if (unhandledErrors.length !== 0 && !window.Sentry) {
    runSentry();
  }
});

function addUnhandledErrors(e) {
  if (!window.Sentry) {
    unhandledErrors.push(e.error);
    // 出错后启动倒计时
    sentryCountdown();
  }
}

window.addEventListener("error", addUnhandledErrors);

function runSentry() {
  const Sentry = await import("@sentry/react");
  Sentry.init(sentryOptions);
  window.Sentry = Sentry;
  reportUnhandledErrors();
}

runSentry();

二、性能上报优化

2.1. 性能事务优化

Sentry 收集的 web 程序性能信息主要是通过插件 BrowserTracing 实现的,基本使用如下:

Sentry.init({
  ...sentryOptions,
  integrations: [...otherIntegrations, new BrowserTracing()],
});

这样使用的话,Sentry 会在首屏加载结束(记为 pageload 事件)和路由跳转事件(记为 navigation 事件)都会进行采集相应的性能数据,并上报。

而考虑到路由切换会比较频繁,并且性能数据量会较多(10kb ~ 30kb),所以可以选择关闭路由跳转的性能上报:

Sentry.init({
  ...sentryOptions,
  integrations: [
    ...otherIntegrations,
    new BrowserTracing({
      /** 文档说明:
       * Flag to enable/disable creation of `navigation` transaction on history changes.
       *
       * Default: true
       */
      startTransactionOnLocationChange: false,
    }),
  ],
});

或者

Sentry.init({
  ...sentryOptions,
  integrations: [
    ...otherIntegrations,
    new BrowserTracing({
      // 只触发 pageload 事件
      beforeNavigate: (context) => {
        if (context.op === "pageload") {
          return context;
        }
      },
    }),
  ],
});

不过关闭 navigation 事件的性能上报并不是绝对的,任何时候你都要考虑项目的需求,如果你需要记录路由跳转性能,自然是不用关闭的。

2.2. LCP 上报

LCP (Large Content Print)事件是评估 web 性能的一个重要标准,即首屏最大元素的渲染时间,但是它的记录时间很特殊,并不是在最大元素的渲染完成的时机,而是用户第一次操作(如点击页面)时才会正式记录下来,例如 LCP 时间为 15000ms,但是 web 性能监控并不会在 15000ms 时存储这个时间,而是在用户第一次操作页面后才会存下这个 15000ms

这就导致了 Sentry 一般无法在首屏性能上报之前获得 LCP 时间(除非用户在上报前操作了页面),所以大多数情况下看 Sentry 后台的性能数据中都是没有 LCP 的。对于旧版本的 Sentry,一般会延长上报等待时间 idleTimeout 来延迟上报尽可能在用户操作后进行性能上报:

Sentry.init({
  ...sentryOptions,
  integrations: [
    ...otherIntegrations,
    new BrowserTracing({
      /** 文档说明:
       * The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined
       * by a moment where there are no in-progress spans.
       *
       * The transaction will use the end timestamp of the last finished span as the endtime for the transaction.
       * If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset.
       * Time is in ms.
       *
       * Default: 1000
       */
      idleTimeout: 50000,
    }),
  ],
});

但是这种方法依然无法百分百保证 LCP 能被收集到,因为总是存在用户一直不操作页面的可能性。但是在 Sentry 7.42.0 以及之后版本,Sentry 进行了调整,它会模拟一次页面操作,从而让浏览器产生 LCP 记录。所以请尽可能使用 7.42.0 或更高版本的 Sentry。

三、报文信息优化

3.1. 过滤上下文信息

Sentry 在上报任何信息(错误日志、性能日志等)时都会携带这次事件的上下文,这些上下文包括打印信息、接口调用等,如果你的 web 程序没有对打印日志进行处理,例如在生产环境中也产生打印信息(console.log),那么就会导致 Sentry 的上报请求体积过大。

为了解决这个问题,可以使用 Sentry 的钩子 beforeBreadcrumb,对上下文信息进行过滤处理,以下是一个简单的示例:

Sentry.init({
  ...sentryOptions,
  beforeBreadcrumb: (breadcrumb) => {
    // 过滤 console.log 和 console.warning
    if (
      breadcrumb.category === "console" &&
      ["log", "warning"].includes(breadcrumb.level!)
    ) {
      return null;
    }
    return breadcrumb;
  },
});

注:这样的过滤同样不是必须的,如果你需要这些信息进行 debug,那么你就不需要过滤。但是保持 web 程序在线上环境的 console 的干净是一个好的规范。

3.2. 上报信息格式优化

为了对上报信息进行好的分类、增强 Sentry 后台上报信息的可读性,可以在 Sentry 上报前统一对这些信息进行处理、规范上报的格式。为了实现这一点,可以利用 Sentry 钩子 beforeSend

Sentry.init({
  ...sentryOptions,
  beforeSend: (event, hint) => {
    // hint.originalException 为捕获到的原始异常实例
    // 可以通过自定义通用的 handleError 来判断此次异常的各种数据
    // 从而修改上报事件的属性,提供更多有效信息
    const { tags, extra, level } = handleError(hint.originalException as any);
    event.tags = tags; // 定义这次上报的 tags
    event.extra = extra; // 添加附加信息
    event.level = level; // 定义上报级别
    return event;
  },
});

可以留意到,上面的示例中我们修改了 tagsextralevel 这几个值,下面会说明为什么本文会建议修改这几个。

3.2.1. 自定义哪些上报值?

这几个变量在 sentry 后台中的呈现分别为:

  • tags:标签(tags: Record<string, string>
    tags
  • extra:附加信息(extra: any
    extra
  • level:严重程度(level: "debug" | "error" | "fatal" | "log" | "info" | "warning"
    level
  • 除此以外,还应该注重【错误类型】和【错误值】的规范性:
    name

其中 tagsextralevel 均可以通过上面 beforeSend 的第一个参数 event 直接进行修改,而【错误类型】和【错误值】则建议使用下文要讲的自定义异常类型的方式进行自定义(即使这两个也可以直接修改 event 进行自定义)

3.2.2. 自定义异常类型

对于可控的异常,例如可以被拦截器捕获的网络错误、手动上报的业务埋点等异常,建议自行封装异常类型,并配合统一的 handleError 函数来规范上报格式,以下是一个网络错误的例子:

// 一、首先,axios 的网络拦截器捕获到了网络错误
axiosInstance.interceptors.response.use(
  (obj: AxiosResponse) => {
    // do something...
  },
  (obj: AxiosResponse) => {
    // 构造一个 NetworkError 实例,进行手动上报
    window.Sentry.captureException(new NetworkError(obj);
  }
);

// 二、构造函数 NetworkError 会根据 axios 的响应实例构造 Sentry 需要的异常信息
export class NetworkError extends Error {
    /** 附加值 */
    extra: any;
    /** tag */
    type: string = 'error.network';
    /** 错误等级 */
    level: string = 'error';

    constructor(response: AxiosResponse) {
        // 构造参数为【错误值】
        super(`${response.request.responseURL}`);
        // name 为【错误类型】
        this.name = `Network Error ${response.data.code}`;
        // 处理附加信息,getExtra 中可以任意取需要的值进行返回
        this.extra = getExtra(response);
    }
}

// 三、异常进入 beforeSend 进行统一处理
Sentry.init({
  ...sentryOptions,
  beforeSend: (event, hint) => {
    // hint.originalException 就是捕获的 NetworkError 实例
    const { tags, extra, level } = handleError(hint.originalException as any);
    event.tags = tags; // 定义这次上报的 tags
    event.extra = extra; // 添加附加信息
    event.level = level; // 定义上报级别
    return event;
  },
});
// 处理错误信息
const handleError = (error: NetworkError) => {
    const info: SentryInfo = {
        extra: undefined,
        tags: {},
        report: true,
        level: 'error',
    };
    if (error.extra) {
        info.extra = error.extra;
    }
    if (error.type) {
        info.tags.type = error.type;
    }
    if (error.level) {
        info.level = error.level || 'error';
    }
    return info;
};

经过上述封装,上报后的异常信息就会在 Sentry 后台中展示出有序分类、可读性强异常信息,从而提升排查故障的效率。

总结

以上是本文总结的一些 Sentry 上报优化的优化方案,从 Sentry 初始化、性能日志优化、上报信息优化三个方面提出了优化方案,可以为 React 端或 web 端的 Sentry 上报制定规范提供参考。

通过 Nginx 日志恢复失败请求的若干问题

不要点开, 博客网站用的
博文标题图片

pic

博文置顶说明
工作上遇到了一个需求, 有一个接口因为 body 量相当大, 很容易请求失败, 加上是个静默接口用户不会有任何感知, 不会手动重新发起请求. 于是就需要我们从 nginx 日志上根据收集的参数信息进行接口请求恢复, 本文即记录做该需求中遇到的若干问题和解决方案

相关

问题综述

由于需要恢复请求, 所以使用脚本, 考虑到后续维护, 用 node 脚本进行开发, 脚本功能如下:

  • 从日志文件 (.csv) 中解析出相关的参数
  • 根据参数恢复请求
  • 输出结果日志

开发中遇到的问题:

  • 工作环境需要开启代理才能请求得到目标 url, 明明环境已经具有 http_proxyhttps_proxy 但 node 脚本却无法发出这个请求
  • 日志的 body 被转义并转码了, 恢复这些编码遇到了问题

node 中的代理

根据 nodejs/node#1490 得知 node 原生不实现代理功能, 也不读取 http_proxy 变量, 原因是加入 proxy 的话 node 开发团队就需要考虑安全 / SOCKS / 证书等等一系列问题, 于是不做

但是依然可以通过很多库实现这个功能, 例如: node-global-proxy

import proxy from "node-global-proxy";

export function setupProxy(address: string) {
  proxy.setConfig({ http: address, https: address });

  proxy.start();
}

setupProxy(proxyAddress); // 程序初始化时设置代理

// ...其他代码

编码问题

下面的字符串 "x\xC2\x9C\xC3", 只是一个代指, 不是一个有意义的值, 只是为了方便解释

这个问题就比较复杂了, 主要体现在两个地方:

  1. 转码失败. 运维把 nginx 日志的编码格式设置成了 utf-16, 根据 https://juejin.cn/post/7122019278991458334#heading-2 可知, js 的编码也是 utf-16, 是可以直接使用从日志中得到的字符串, 然而却出现了两种情况
    1. 直接给 js 变量赋值例如: const body = "x\xC2\x9C\xC3" 再使用这个 body 是能成功发出请求的, js 自动完成了转码, 其参数和客户端的请求参数完全一样
    2. 从 .csv 日志中通过 readline 解析出字符串 "x\xC2\x9C\xC3" 再赋值给 body, 却没有自动转码, 传的参数是 "x\xC2\x9C\xC3" 这个字符串而不是转码后的字符串
  2. 转义失败. node 中的 url 转义恢复: 熟悉前端开发可知请求参数经常需要先通过 encodeURIComponent 转义再发请求, 原因这里不提, 问题在于这次使用 decodeURIComponent 后居然无法正确恢复解析转义

转义失败问题

先说第二个问题, 直接使用 decodeURIComponent("x\xC2\x9C\xC3") 无效 (报错了), 那么也就意味着 decodeURIComponent 对这些初始 utf-16 编码的字符串依然无法正确解析

然后通过搜索找到了另一个函数: querystring.escape, 有效, 但是不是完全有效, 我发现这个函数只对部分的 utf-16 有用, 有的就无效, 这就有点奇怪了, 再仔细找原因, 发现了 node:querystring 文档里就有提到: https://nodejs.org/api/querystring.html#querystringunescapestr

By default, the querystring.unescape() method will attempt to use the JavaScript built-in decodeURIComponent() method to decode. If that fails, a safer equivalent that does not throw on malformed URLs will be used.
大概翻译下:
默认情况下 querystring.unescape() 方法会尝试用 decodeURIComponent() 进行转义, 如果失败了, 那么使用更安全的方法来解决那些长得不像 url 的字符串

也就是 pass 掉

最终找到了这个: https://stackoverflow.com/questions/37670485/how-do-i-decode-this-string-xc3-x99-xc3-xa9-xc2-x87-bx-xc2

使用 decodeURIComponent(escape(s)) 即可, 原来是还需要用 escape 先做一层转换呀, 不过值得一提的是 escape 函数已经被标记 deprecated

转码失败问题

上面提到过, js 是 utf-16 编码的, 理论上直接支持来自 nginx 的日志参数, 直接赋值也证明了这一点 (下面 nextFetch 会直接使用 body 发出 fetch 请求)

const body = "x\xC2\x9C\xC3";
console.log(body); // 被转码的字符串

nextFetch(body); // 成功

然而如果是用 node:readline 读文件中的 'x\xC2\x9C\xC3' 却无法直接使用:

// nginx-log.csv
x\xC2\x9C\xC3
let body = "";

const fileStream = createReadStream("./nginx-log.csv");
const rl = createInterface({
  input: fileStream,
  crlfDelay: Infinity,
});

rl.on("line", function (line) {
  body = line;
});

rl.on("close", function () {
  console.log(body); // 没有被转码的字符串 "x\xC2\x9C\xC3"
  nextFetch(body); // 失败
});

造成这个结果的原因其实是, js 代码编译过程中其实已经把第一种情况的字符串转码了, 到 runtime 时使用的已经是转码后的字符串

而第二种情况是此时 js 程序已经是 runtime 了, 此时得到的 line 自然不是代码的一部分, 就没有经过编译这个过程而未被转码, 只得到了原始的字符串

那么我们直接转码即可, 参考: https://gist.github.com/kiinlam/176ce20707336fa8278726e869e59cb1

export function decodeUtf16(s: string) {
  return s.replace(/\\x(\w{2})/g, function (_, $1) {
    return String.fromCharCode(parseInt($1, 16));
  });
}

基于 NextJS + Vercel + Github Issues 的博客开发(一)

不要点开, 博客网站用的
博文置顶说明
一个既能利用 GitHub Issues 作为后台进行十分方便的文章发布和管理,也能尽可能利用 NextJS 的特性,让博客有单页面 web 程序的客户端体验同时能进行 SEO 优化的博客方案。还能获得一个免费域名。最大的缺点是不支持 IE 浏览器。

相关链接

  1. 基于 NextJS + Vercel + Github Issues 的博客开发(二)
  2. 基于 NextJS + Vercel + Github Issues 的博客开发(三)

源码和博客地址

  1. 源码
  2. 线上地址

为什么

为什么是 GitHub Issues

用 GitHub Issues 来写博客并不是什么新鲜事,它的优点很明显,GitHub Issues 的发布和管理是十分优秀的,尤其是利用标签和状态对 Issues 的管理满足了我们对博客的归档和分类的需求,而且也直接支持 Markdown 格式和文件上传。最重要的是 GitHub 提供了一套相对完备的 Api,让我们可以应用到开发中。

为什么是 NextJS

谈到博客,相信大部分想要建立博客的朋友不免都想分享自己的文章,希望让更多人能看到自己的想法,这时候搜索引擎的搜索结果就扮演了十分重要的角色,针对搜索引擎的 SEO 优化直接影响了一个博客能否被更多的人看到。

而 NextJS,一个基于 React 的、提供开箱即用的服务端渲染功能的开发框架就能满足我们对一个优秀博客的想象——既有单页面程序的客户端体验、也能利用服务端渲染的特性进行 SEO 搜索优化。

为什么是 Vercel

对于前端来说,一个好用的用于构建和测试前端项目的 Serverless 云平台可以让我们不再关心后端和服务器的事情,从而提高我们的开发效率。目前市面上的 Serverless 平台有不少,国内外都有,而这次会选用 Vercel 原因也很简单,因为 NextJS 目前就是由 Vercel 维护的,NextJS 项目可以不需要复杂的配置即可直接在 Vercel 平台上发布,而且还像 GitHub Page 那样白送一个域名。

开发准备

布局设计

设计大致上参考了猿料论坛的布局

网页方面定为主页、文章列表页、文章详情页、介绍页和搜索页

  1. 主页主要展示所有的文章索引,以 antd 的时间轴组件进行展示
  2. 文章列表页则展示带有 post 标签的文章,这些文章主要是想要被搜索引擎搜索到的文章,也就是需要做 SEO 优化的主要页面
  3. 文章详情页就是展示文章的详细信息
  4. 介绍页就是读取全部文章中带有 about 标签的那一篇,以文章详情页的样式进行展示
  5. 搜索页展示用户搜索结果,样式上采用和文章搜索页的样式,但是不需要做 SEO 优化

前端脚手架

这次我就直接找到了 luffyZh
next-antd-scaffold
,试了一下,除了不支持 IE 外,各方面都还不错,就直接应用起来了。

[app]
├── 📄 .babelrc
├── 📄 .eslintrc
├── 📄 .gitignore
├── 📄 next.config.js
├── 📄 package.json
├── 📄 server.js
├── 📄 pm2.config.js
├── 📂 assets
├── 📂 docs
├── 📂 public
│   ├── 📄 favicon.ico
│   └── 📂 static
└── 📂 src
    ├── 📂 components
    ├── 📂 constants
    │   ├── 📄 ActionsTypes.js
    │   └── 📄 ApiUrlForBE.js
    ├── 📂 containers
    ├── 📂 core
    │   ├── 📄 util.js
    │   └── 📄 nextFetch.js
    ├── 📂 middlewares
    │   ├── 📂 client
    │   └── 📂 server
    ├── 📂 pages
    └── 📂 redux
        ├── 📄 store.js
        ├── 📂 actions
        ├── 📂 reducers
        └── 📂 sagas

GitHub Issues 接口

GitHub 的 api 文档可以看这里,这次我基本上只用到了 issues 相关和 search 相关的接口,具体如下:

api 类型 主要参数 返回值 说明
https://api.github.com/repos/{owner}/{repo}/issues GET labels creator per_page page sort direction state Array 获取某个仓库的 issues 列表
https://api.github.com/repos/{owner}/{repo}/issues/{number} GET 不需要 一个带有某 issue 全部信息的对象 获取某个 issues 的全部信息
https://api.github.com/repos/{owner}/{repo} GET 不需要 一个带有仓库全部信息的对象 获取某个仓库的基本信息
https://api.github.com/search/issues GET order page per_page q sort { items: <Array>, incomplete_results: <bool>, total_count: <number> } 对整个 Github 进行搜索

根据这些接口获得的数据,就能拿到博客需要的数据了

首先是根据我要做的页面来确定一下需要哪些数据

页面 需要的数据
主页时间轴 因为要进行分页,所以需要知道全部的文章的数量;然后就是根据页码请求到每个页的文章列表
文章列表页 同上,不过是带有 post 文章
文章详情页 某篇文章的数据
介绍页 带有 about 标签的文章的数据
搜索页 搜索的文章列表

所以根据这些信息,在测试过这些接口后确定了以下方案:

  1. 主页的文章数量由 https://api.github.com/repos/{owner}/{repo} 获得,文章列表由 https://api.github.com/repos/{owner}/{repo}/issues 获得
  2. post 标签的文章列表由 https://api.github.com/repos/{owner}/{repo}/issues 获得,labels 参数改为 post 即可;至于文章列表,我决定用搜索的接口 https://api.github.com/search/issues 来请求,因为这个接口会提供 total_count
  3. 详情页就用 https://api.github.com/repos/{owner}/{repo}/issues/{number} 即可
  4. 搜索页用 https://api.github.com/search/issues 即可

以上就是初步的构思,然后就根据这些实施方案即可。在接下来的第二部分主要讲讲遇到的一些坑

【自用】Manjaro 初始环境

  1. sudo systemctl enable vmtoolsd.service && sudo systemctl restart vmtoolsd.service
  2. mkdir -p /home/hexh/desktop
  3. git clone https://github.com/hexh250786313/hexh.config /home/hexh/desktop/hexh.config
  4. ip: echo "185.199.108.133 raw.githubusercontent.com" | sudo tee -a /etc/hosts
  5. nvm
  6. source ~/.bashrc
  7. nvm install v16.10.0
  8. npm install -g yarn
  9. cd /home/hexh/desktop/hexh.config
  10. yarn && npm link
  11. hexh-config manjaro proxy
  12. source ~/.bashrc
  13. clash
  14. firefox
  15. clash
  16. hexh-config manjaro basic resetPacmanKey
  17. hexh-config manjaro basic needed
  18. yay -Syu --devel
  19. hexh-config manjaro basic dkms
  20. hexh-config manjaro git
  21. gh ssh keys
  22. ssh -T [email protected]: input yes
  23. hexh-config manjaro git ln
  24. hexh-config manjaro basic dotfiles
  25. hexh-config manjaro zsh
  26. sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
  27. back to bash
  28. hexh-config manjaro zsh ln
  29. hexh-config manjaro zsh colorls
  30. zsh
  31. rbenv shell 3.1.0 && rbenv global 3.1.0 && rbenv local 3.1.0
  32. gem install colorls
  33. hexh-config manjaro zsh fzf: ~/.fzf/install
  34. hexh-config manjaro proxy ln
  35. 重启
  36. sudo systemctl restart vmtoolsd.service
  37. curl www.google.com.hk
  38. hexh-config manjaro node
  39. npm list -g --depth=0
  40. hexh-config manjaro editor
  41. hexh-config manjaro editor ln
  42. hexh-config manjaro hosts
  43. nvim: :TSInstall css scss json lua tsx javascript dot bash yaml vim markdown regex html jsdoc vue rust typescript python
  44. nvim ~/.desktop/test.md: :CocCommand markdown-preview-enhanced.openKaTeXConfig, hexh-config manjaro editor latexConfig
  45. :Copilot setup
  46. hexh-config manjaro pamac config
  47. 重启
  48. hexh-config manjaro pamac flatpak
  49. flatpak update
  50. hexh-config manjaro pamac snap
  51. hexh-config manjaro nutstore 登录, ~/桌面/share
  52. hexh-config manjaro nutstore config
  53. hexh-config manjaro nutstore clipman: 同步路径 ~/.cache/xfce4/clipman
  54. hexh-config manjaro inputMethod
  55. hexh-config manjaro tmux
  56. hexh-config manjaro basic software
  57. hexh-config manjaro system config
  58. hexh-config manjaro system init
  59. 重启
  60. 启动 vmware, 再 hexh-config manjaro basic vmware
  61. 启动 code, 同步, 再执行 Enable Monkey Patch
  62. hexh-config manjaro howdy
  63. src/env/manjaro/modules/howdy/config.ts 改变设备路径, 然后
  64. hexh-config manjaro howdy config
  65. hexh-config manjaro asus: https://github.com/mohamed-badaoui/asus-touchpad-numpad-driver
  66. 重启

JS 用更函数式的方法实现树形结构和扁平数组间的转换

用这个网站来测试代码

树形结构展平为扁平数组

const tree = [ { name: "one", id: "1", list: [ { name: "oneOne", id: "11", list: [{ name: "oneOneOne", id: "111" }] }, { name: "oneTwo", id: "12", list: [ { name: "oneTwoOne", id: "121" }, { name: "oneTwoTwo", id: "122" }, { name: "oneTwoThree", id: "123" }, ], }, ], }, { name: "two", id: "2", list: [ { name: "twoOne", id: "21", list: [{ name: "twoOneOne", id: "211" }] }, { name: "twoTwo", id: "22", list: [ { name: "twoTwoOne", id: "221" }, { name: "twoTwoTwo", id: "222" }, ], }, ], }, ];

const flatten = (
  children: any[],
  parentId?: string | number,
  parentName?: string,
  level = 1,
  parentNodes?: any[]
) =>
  Array.prototype.concat.apply(
    children.map((x) => ({
      // ...x,
      name: x.name,
      id: x.id,
      parentId,
      parentName,
      level,
      parentNodes: JSON.parse(JSON.stringify(parentNodes || [])),
    })),
    children.map((x) =>
      flatten(
        x.list || [],
        x.id,
        x.name,
        (level || 1) + 1,
        Array.isArray(parentNodes)
          ? [...parentNodes, { id: x.id, name: x.name }]
          : x.id !== undefined
          ? [{ id: x.id, name: x.name }]
          : []
      )
    )
  );

const flat = flatten(tree);

console.log(flat);

扁平数组转树形结构

const flat = [ { name: "one", id: "1", parentId: undefined, parentName: undefined, }, { name: "two", id: "2", parentId: undefined, parentName: undefined, }, { name: "oneOne", id: "11", parentId: "1", parentName: "one", }, { name: "oneTwo", id: "12", parentId: "1", parentName: "one", }, { name: "oneOneOne", id: "111", parentId: "11", parentName: "oneOne", }, { name: "oneTwoOne", id: "121", parentId: "12", parentName: "oneTwo", }, { name: "oneTwoTwo", id: "122", parentId: "12", parentName: "oneTwo", }, { name: "oneTwoThree", id: "123", parentId: "12", parentName: "oneTwo", }, { name: "twoOne", id: "21", parentId: "2", parentName: "two", }, { name: "twoTwo", id: "22", parentId: "2", parentName: "two", }, { name: "twoOneOne", id: "211", parentId: "21", parentName: "twoOne", }, { name: "twoTwoOne", id: "221", parentId: "22", parentName: "twoTwo", }, { name: "twoTwoTwo", id: "222", parentId: "22", parentName: "twoTwo", }, ];

const buildTree = (data: typeof flat) =>
  data
    .reduce(
      (m, { id, name, parentId }) => (
        m.get(parentId)!.push({ id, name, list: m.set(id, []).get(id) }), m
      ),
      new Map<string | undefined, any[]>([[undefined, []]])
    )
    .get(undefined);

const tree = buildTree(flat);

console.log(tree);

Linux 下格式化 U 盘以及利用 Ventoy 制作系统启动盘

不要点开, 博客网站用的
博文置顶说明
最近折腾 Mnajro, 这篇文章主要介绍如何在 Linux 环境下格式化 U 盘以及制作一个系统启动盘 (不限制于 Mnajaro)

参考

格式化 U 盘

  1. 查看磁盘情况: sudo fdisk -l or lsblk, 找到对应的 U 盘路径
  2. 进入 fdisk 操作: sudo fdisk <U 盘路径>
    • m: 获取帮助命令
    • d: 删除分区
    • n: 新建分区
    • p: 打印分区表
    • t: 更改分区类型
    • w: 写入操作并退出
  3. 这里选择 d 指令, 删除所有分区, 最后 w 退出即可
  4. 格式化: sudo mkfs.fat -F 32 <U 盘路径>
  • 题外话1, 假设如果要格式化某个已经分好区的 U 盘中的某个分区, 做法
    1. 先删除该分区, 做法如上述
    2. 再新建一个分区, 指令是 n, 分区类型选择 e, 即从分区 ( p 是主分区 ), 其余默认
    3. 再选择更改分区类型指令 t, 输入 07 ( ntfs 格式 ), 再 w 即可, 但是这里选什么都无所谓, 因为下一步才是真正的格式化
    4. w 保存
    5. sudo mkfs.ntfs <U 盘路径>/<分区编号>, 完成

制作系统启动盘

  1. 安装 Ventoy
  2. 找到你的 U 盘路径, 参考上文格式化 U 盘的步骤 1
  3. 初始化 U 盘: sudo ventoy -i <U 盘路径>
    • 这一步可能提示需要卸载 U 盘, 则运行 umount <U 盘路径> 后再运行初始化指令
  4. 完成后复制你的 ISO 镜像到 U 盘中: sudo cp <ISO 镜像路径> <U 盘路径根目录>, 然后 ventoy 就会在复制的过程中自动运行脚本把 U 盘变为启动盘, 结束后可以看到 U 盘的名字变成了你的系统镜像名, 这说明已经制作完成了
    • 默认情况下制作的盘分区格式为 MBR, 如需要指定 GPT 格式的话加上 -g 参数即可: sudo ventoy -i -g <U 盘路径>
    • 如果你是用文件管理器手动复制粘贴的话, 则很难判断脚本运行过程, 不知道什么时候结束, 所以建议用 cp 指令
    • 步骤 3 完成后 U 盘会变成两个分区, 一般来说你需要把镜像复制到大容量的那个分区, cp 指令会自动复制到正确的分区, 所以不需要关心分区路径, 直接 cp 到 U 盘路径即可

当然, ventoy 的功能远不止于此, 可以参考他们的文档

  • 题外话2, 用指令复制东西进 U 盘可不是想上面那样 sudo cp something <U 盘路径根目录> 哦, 这其实是刻录操作, 会完全重写分区, 导致 U 盘无法使用, 正常用法:
    1. lsblk 查看 U 盘的分区信息, 会看到分区编号后面有个运行时路径如 /run/media/<用户名>/<分区名称>, 这个才是你要复制的路径
    2. 而且执行这时候执行 cp 也不需要 sudo: cp something /run/media/<用户名>/<分区名称>

关于笔记本安装 Linux 系统的温馨提示

  1. BIOS 中关闭 Secure Boot
  2. 安装程序没那么新的 Linux 安装程序如果无法识别, 可以试试把 SATA 接口转为 AHCI

father4 预处理编译 less 文件

不要点开, 博客网站用的
博文标题图片

pic

博文置顶说明
本文主要内容为,介绍前端开发中使用 father 进行打包组件库、工具库时,如何预处理编译 less 文件

一、引言

在前端开发中,我们经常会遇到需要对一些组件库进行二次封装的需求,特别是当我们使用阿里系的组件库,如 antd 或 antd-mobile 时。在这种情况下,我们通常会使用 fatherdumi 这一套官方推荐的方案。然而,father 官方并没有提供处理 scss 或 less 的示例,因此我们需要自行实现 father 的样式预处理。本文将介绍如何实现这一目标。

二、father 的工作原理

在深入讨论如何实现样式预处理之前,我们首先需要了解一下 father 在前端打包中的工作原理。

father 不仅可以用于打包前端代码,它也可以用于打包通用的 ts/js 代码库。实际上,father 是一个集成了多个打包工具的打包工具,例如,它集成了 babel 和 esbuild 两种编译器。

当我们需要打包的代码为前端代码(如前端组件库等)时,father 会使用 babel 作为 js/ts 的编译器。当我们需要打包的代码为 node 库时,father 则会使用速度更快的 esbuild。

此外,father 还提供了一些插件功能,例如 extraBabelPluginsaddLoaderextraBabelPlugins 可以让我们使用 babel 插件,而 addLoader 功能则类似于 webpack 的 loaders 功能,可以用于处理非 js/ts 文件。

三、实现样式预处理

了解了 father 的工作原理后,我们就可以开始实现样式预处理了。我们的任务可以分为两部分:

  1. addLoader 功能,实现一个 less 转 css 的插件。
  2. 使用 extraBabelPlugins,实现一个插件,将 js/ts 中的 .less 文件内容转为 .css

接下来,我们将详细介绍如何实现这两个任务。

3.1. 第一步、实现 babel 插件

这一步主要是为了把 js/ts 文件中的与 less 相关的文本替换为 css,例如:import "./index.less" 替换为 import "./index.css"

这一步比较简单,直接实现一个 babel 插件用于进行 .less 替换为 .css 的文本操作即可,代码如下:

// .fatherrc.ts
import { defineConfig } from 'father';

export default defineConfig({
    
    ...

    esm: { output: 'dist', transformer: 'babel' }, // 必须要使用 babel 模式
    extraBabelPlugins: [
        [
            './babel-less-to-css.js', // 把 js/ts 文件中的 '.less' 字符转为 '.css'
            {
                test: '\\.less',
            },
        ],
    ],

    ...

});
// babel-less-to-css.js
module.exports = function () {
    return {
        visitor: {
            ImportDeclaration(path) {
                if (/\.less$/.test(path.node.source.value)) {
                    path.node.source.value = path.node.source.value.replace(/\.less/, '.css');
                }
            },
        },
    };
};

3.2. 第二步、实现 loader 插件

这一步要实现一个把 less 文件转换为 css 文件的插件,其中:

  1. 使用 less.js 来进行对 less 到 css 的转义。
  2. 使用 postcss 来处理 less 中的相对路径和别名路径的处理以及处理浏览器兼容问题。

这两步本身其实已经和 father 没什么太大的关系,都是 webpack 中常用的对 less 文件的处理方法,所以这里直接给出示例代码,看代码就可以知道如何在 father 中实现这些功能:

// .fatherrc.ts
import { defineConfig } from 'father';

export default defineConfig({

    ...

    plugins: [
        './loader.ts', // 实现 loader 功能
    ],

    ...

});
// loader.ts
import type { IApi } from 'father';
import { addLoader, ILoaderItem } from 'father/dist/builder/bundless/loaders';

export default async (api: IApi) => {
    const loaders: ILoaderItem[] = await api.applyPlugins({
        key: 'addPostcssLoader',
        initialValue: [
            {
                key: 'less-to-css',
                test: /\.less$/,
                loader: require.resolve('./loader-less-to-css'), // less 文件转 css 文件
            },
        ],
    });

    loaders.forEach((loader) => addLoader(loader));
};
// loader-less-to-css.js
const path = require('path');
const less = require('less');
const postcss = require('postcss');
const syntax = require('postcss-less');
const atImport = require('postcss-import');
const autoprefixer = require('autoprefixer');

const loader = function (lessContent) {
    const cb = this.async();
    this.setOutputOptions({
        ext: '.css',
    });
    postcss([
        autoprefixer({
            // 提升兼容性
            overrideBrowserslist: ['last 10 versions'],
        }),
        atImport({
            resolve: (id) => {
                const currentPath = this.resource;
                if (id.startsWith('@')) {
                    // 处理别名路径,把 @ 替换成 src
                    const srcPath = path.join(__filename, './src');
                    const targetPath = id.replace(/^@/, srcPath);
                    return targetPath;
                } else {
                    // 处理相对路径
                    const relativePath = id;
                    const targetPath = path.resolve(currentPath, '..', relativePath);
                    return targetPath;
                }
            },
        }),
    ])
        .process(lessContent, { syntax })
        .then((result) => {
            // less 转 css
            less.render(result.content, (err, css) => {
                if (err) {
                    console.error(err);
                    return;
                }
                cb(null, css.css);
            });
        })
        .catch((err) => {
            console.error(err);
        });
};

module.exports = loader;

经过上述处理,就可以实现 less 的预处理,编译为 css 文件了。

四、补充

4.1 使用 babel 的原因

事实上 father 支持 babel、esbuild 和 SWC 三种构建方式,而本文使用的是 babel 模式。

father 的配置文档 中提到,platformbroswer 的时候,transformer 默认使用 babel 进行 js 的编译器,这意味着,father 官方也是推荐使用 babel 来编译前端的代码的。

即使使用 esbuild 有更快的打包速度,但是 esbuild 处理的是文件的二进制格式,很多现存的前端编译插件无法直接应用到其中;而 babel 则是生成 AST 语法树,其兼容性和拓展性也是 esbuild 和 SWC 无法比拟的。

不过最关键的还是:father 还没有实现自定义 esbuild 插件!它只提供了 babel 的自定义插件能力,这样一来其实我们别无选择。

coc-todo-tree 中文说明

不要点开, 博客网站用的
博文标题图片

pic

博文置顶图片

pic

博文置顶说明
coc-todo-tree 是 vscode-todo-tree 在 coc 上的实现, 本文是简体中文说明

coc-todo-tree

源码: https://github.com/hexh250786313/coc-todo-tree

coc-todo-tree 是基于 coc.nvim 的 todo tree 实现

灵感来自 vscode-todo-tree

安装

请先确保你的 vim 已经安装了 coc, 然后执行以下指令即可安装 coc-todo-tree

:CocInstall coc-todo-tree

使用

在 vim/neovim 中执行以下指令即可打开 todo-tree 面板:

:CocCommand coc-todo-tree.showTree

功能

  • 使用了 coc-tree, 可以让工作区中的 @todo / FIXME 等 tag 以树形样式呈现
  • 默认使用 ripgrep 来进行搜索
  • 可以自定义你的 tag 名字 / 图标 / 高亮 / 匹配正则等
  • 样式控制: 参考了 vscode-todo-tree, 你可以通过按大写 C 来在三种展示样式中切换: tags-only / flat / tree-view, 同时可以通过按小写的 c 来进行 tag group 归类

配置

所有配置: https://github.com/hexh250786313/coc-todo-tree/blob/master/.vim/coc-settings.json

一口气完成 Sentry Docker 部署

不要点开, 博客网站用的
博文置顶说明
本文介绍用 docker 部署 sentry 的相关步骤和一些值得注意的地方

相关

Sentry Docker

1. 下载

getsentry/onpremise 这个仓库是专提供来用 docker 启动本地服务的,也就是需要首先本地环境要能运行 docker 才行。另外 sentry 也可以用 python 启动服务,这里不提

sudo apt-get install apt-utils # Debian 系统下可选,不安装也行,对结果没有影响,只影响安装交互
git clone https://github.com/getsentry/onpremise

2. 开始安装

cd onpremise
chmod +x install.sh
./install.sh
  • 然后就会开始部署,这个过程相当花时间,国内的话最好提前配置 docker 代理
  • 内存不足警告,我在安装过程中最高飙升了将近 4g 的内存,注意。另外 sentry 官方也建议你的本地环境的内存最好大于等于 8GB
  • 如果实在太慢可以试试在执行 ./install.sh 前先下载下面这两个,这是淘宝的 docker 源,能把部署 sentry 需要的大部分镜像都下载下来,这样可以减少一点安装时间,但是需要开发者账号,hkoa9dfz 就是开发者账号对应的识别码
docker image pull hkoa9dfz.mirror.aliyuncs.com/getsentry/sentry
docker image pull hkoa9dfz.mirror.aliyuncs.com/viitanener/sentry-onpremise-local

3. 安装完成后

  • 注意过程中可能会让你创建管理员账号,不小心跳过了也没问题,可以在 sentry 安装完毕后重新创建,创建账号看第 4 步
  • 然后就可以在项目文件夹 📂 onpremise 中使用以下指令来拉起 sentry 服务了:
docker-compose up -d # 成功后访问 http://127.0.0.1:9000 即可进入 sentry 主界面

补充说明

因为是通过 docker 编译出来的,因此对应的指令都要在项目文件夹 📂 onpremise 中使用 docker-compose run 来执行,如果是用 python 来构建 sentry 的话,则应该要使用 sentry 指令,python 相关自行查看文档

也就是说假设在官方文档中查询到某条指令如:sentry createuser

那么如果你是通过本文介绍的用 docker 的方法来启动 sentry 服务的话,则应该要在项目文件夹 📂 onpremise 中使用:docker-compose run createuser 来替换上述指令

本文剩余章节的相关指令同样是通过这种 docker-compose 指令来使用的,注意要在 📂 onpremise 目录底下执行

4. 账号注册

  • 安装完毕后可以用以下指令创建用户:(创建用户,该用户为超级用户,不加 --superuser 则为普通用户,--force-update 可以用来覆盖已经存在的相同账号)
docker-compose run --rm web createuser --superuser --force-update 
  • 然后打开 sentry 主界面(http://127.0.0.1:9000 )用刚刚申请的账号登录,第一次登陆的时候可以进行一些基础配置,例如是否允许注册、隐私、邮箱服务器的配置等等(因为 sentry 中的团队管理会涉及到用发邮箱来邀请用户的部分,因此可以选择是否配置邮箱服务器),自行配置完毕后即可进入 sentry 管理的主界面

补充:如果进入页面登录时提示网络 CSRF 相关的报错,可能和这个 issue 有关:getsentry/self-hosted#2751 。解决办法是在 sentry/config.yml 中添加:

system.url-prefix: http://127.0.0.1:9000

5. 停止 sentry 服务

docker-compose down

Sentry 创建项目

  • 访问:http://127.0.0.1:9000 登录进入主界面,自行寻找创建项目入口并按提示一步步操作,出现代码那一段按提示把代码拷贝到前端项目对应的位置,一般会选择程序初始化的地方。这一步完成之后已经可以实现错误上报了,react 项目的代码大概长如下模样,sentry 的不同版本可能会有区别,但是大致上应该不会差太多:
// == app.js ==
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: "<dsn>",
  environment: "<ENV>",
  release: "<VERSION>"
});
  • 参数说明:(更多参数应自行查看官方文档)

    • dsn:错误上报的目标接口,也就是 sentry 服务部署的地方,如果是正式生产环境用了 nginx 等工具转发了部署项目的所在 ip 到具体域名,记得要把这里的地址也修改一下
    • environment:环境,不配置的话默认为 "production"
    • release:版本,通常是用 "<ENV>@1.0.1" 这样的形式,理论上是唯一值,同一个项目中不会同时存在两个相同的版本,会和下方上传代码地图的 release 对应,详情在下面章节会说明
  • 一旦有错误上报成功,就会在对应的项目、环境的对应版本下生成 issue 及其详情,可以到 sentry 主界面自行查看

  • 主动错误上报可以调用这个方法,更多 api 应自行查看官方文档

import * as Sentry from '@sentry/react';

Sentry.captureException(error);

账户绑定到本地

这一步的目的是让你的 sentry 账号和你的本地环境绑定,从而实现各种功能,例如下面章节会介绍的上传 source map 到 sentry 服务器从而实现抛错处的精确定位(但是要实现上传 source map 其实未必需要把账号和本地绑定,这点下面也会介绍)

1. sentry 管理工具

npm i -g @sentry/cli

因为安装过程需要运行脚本,可能会提示权限不够,那样的话用下面这个指令来安装

npm install -g --unsafe-perm=true --allow-root @sentry/cli

2. 开始绑定账号

  • 执行以下指令开始绑定账号:

    • 选择 n,则提示输入 token,token 在 sentry 的项目页面中点开账户下的 “API keys”,可以自行生成新的 token,然后按命令行的提示粘贴该 token
    • 选择 y,则会自动打开浏览器对应的登录页,登录后会直接进入 token 页,同样拷贝粘贴 token 到命令行中即可
sentry-cli --url http://127.0.0.1:9000 login
  • 之后会在用户目录(linux 通常为 📂 ~)下生成 📄 .sentryclirc 配置文件
  • 注:默认情况下 sentry-cli 会使用 https://sentry.io 作为服务器,因此需要额外用 --url 指定到本地的 sentry 上,可以通过 📄 .sentryclirc 配置文件进行修改默认服务器,具体配置下面的章节会说明
  • 执行以下指令查看绑定信息:(会展示登录信息,包括指定服务器、默认项目、默认版本、默认团队等信息,因为这是初始化,所以一般情况下除了 token 信息外,其他这些信息都是空的,需要自行在 📄 .sentryclirc 上进行配置。如果信息没问题,则说明 sentry 账号已经成功绑定到本地)
sentry-cli --url http://127.0.0.1:9000 info

Sentry 配置文件

sentry 的配置文件就是上一步生成的 📄 .sentryclirc 文件,这个文件默认情况下存在于用户目录 📂 ~ 中,表示本地账户的登录信息和配置

但是这个文件并非一定要放在 📂 ~ 下,就像上面章节提到的,要使用 sentry 的登录功能不需要本地环境也配置 📄 .sentryclirc,你可以把这个文件放在任何前端项目的目录里,从而对不同的项目实现不同的 sentry 配置

提到这个也要说明一下 sentry-cli 这个指令会优先读取指令执行目录底下的 📄 .sentryclirc,只有当当前目录下没有 📄 .sentryclirc 才会去读取用户目录 📂 ~ 下的 📄 .sentryclirc

根据 📄 .sentryclirc 的这些特点,就可以很灵活地为不同的前端项目配置不同的 sentry 配置

下面就是一份比较典型的 📄 .sentryclirc 的配置,更多属性应自行查阅文档

[auth]
token=<account token>

[defaults]
url=<server>
org=<org name>
project=<project name>
  • 属性说明:
    • token:就是认证 token
    • url:部署 sentry 的服务器地址,默认为 https://sentry.io
    • org:登录 sentry 主界面可以看到你自己的团队,选择你要设置的默认团队,注意名字没有井号,默认为 “sentry”
    • project: 设置默认项目

Source Map

代码地图在 sentry 中的作用是在上报的 issue 中显示报错代码对应的准确位置(精确到行列),有两种方式上传代码地图到 sentry

手动上传

1. 上传 source map

  • 假设已经编译好了项目并存在 📂 .dist 文件夹,执行如下指令即可上传到对应的项目:

    • 其中,如果已经在配置 📄 .sentryclirc 中配置好了 url orgproject,则可以省去 --url -o -p 这几个参数
    • <VERSION> 对应的是上面前端上报初始化对象里的 release,说明这里上传的 source map 对应的是和 release 一样的版本
    • --url-prefix 是项目前缀,默认是"~/" 也就是根目录,如果项目不是部署在域名的根目录,则可以用这个参数自行调整
sentry-cli --url <server> -o <org name> -p <project name> releases files "<VERSION>" upload-sourcemaps --url-prefix '~/' './dist'

2. 删除 source map

sentry-cli releases files "<VERSION>" delete --all

补充说明

  • 发布正式环境的前端项目时,上传完 map 后记得要删除掉,别发到正式环境去
rm -rf ./dist/*.map
  • 如果把 📄 .sentryclirc 文件直接放到要执行命令的前端项目的根目录的话,那么 sentry-cli 就会优先使用本地的配置。而也因此,可以针对不同的项目使用不同的 sentry 配置,这一点,对于接下来要说明的使用 webpack 上传 source map 的方法同样适用。如果是用这种方式的话就可以省去 sentry 和本地环境绑定的步骤,十分灵活

Webpack 上传

1. 安装插件

yarn add @sentry/webpack-plugin --dev

2. 配置 Webpack

配置完成后会在每次 build 项目的时候,source map 文件自动上传到对应的 sentry 服务。注意要开启生成源码,建议为 devtool: "source-map"

// webpack 配置文件 webpack.config.js
const SentryCliPlugin = require('@sentry/webpack-plugin');

const config = {
  plugins: [
    new SentryCliPlugin({
      include: "./dist",
      ignore: ["node_modules", "webpack.config.js"],
      release: "<VERSION>",
      urlPrefix: "~/",
      configFile: ".sentryclirc"
    }),
  ],
};

属性说明:(访问官方文档查看更多属性)

  • include:上传的目标文件夹,也可以指定 "." 上传整个根目录
  • ignore:上传时要忽略的文件夹或文件类型
  • release:和上述章节里介绍的 sentry 初始化对象中的 release 对应
  • urlPrefix:和上述章节里介绍的 --prefix-url 对应,默认为 "~/"
  • configFile:如果不配置的话默认使用环境中的配置,也就是 📄 ~/.sentryclirc,也可以像这样把 📄 .sentryclirc 文件放进前端项目的根目录中然后配置成 ".sentryclirc" 来使用项目自己的配置

一些心得

  • 一般来说在开发环境下不需要特意上报错误,也不需要上传源码,所以可以考虑在开发环境下关闭 sentry
  • 十分推荐项目用项目本身的 📄 .sentryclirc 配置,也就是用不同的 .sentryclirc 单独放到不同的项目根目录下,各个项目有各自的配置,这一点在上面的章节有说明。甚至可以利用 fs.existsSync() 方法判断到项目下有没有 .sentryclirc 文件,从而控制在什么环境下需要上传 source map

结束

以上就是在部署配置 sentry 的一些基础步骤和一些小提示,更多的内容应该要到 sentry 官方文档去了解,尤其是前端配置的部分有很多值得研究的 api

基于 NextJS + Vercel + Github Issues 的博客开发(二)

不要点开, 博客网站用的
博文置顶说明
这里主要记录开发本博客过程中的一些坑

相关链接

  1. 基于 NextJS + Vercel + Github Issues 的博客开发(一)
  2. 基于 NextJS + Vercel + Github Issues 的博客开发(三)

源码和博客地址

  1. 源码
  2. 线上地址

环境搭建时的坑

build 报错

根据 luffyZh 的文章,对项目进行了简单的部署后,到 Vercel 上 build 的时候却出现了报错,经过测试发现在本地的时候 build 也会出现问题,如下图所示出现了报错:

image

可见是 terser plugin 出现了问题,GitHub 寻找 issue 发现了有人提出来:luffyZh/next-antd-scaffold#12 ,根据 solution,作出以下修改即可:

image

build 完成后无法进入首页和列表页,却能进入关于页面

根据 Vecel 的控制台信息的提示,说是找不到 document 对象,而总所周知 document 对象是浏览器的一个对象不可能不存在。说明这里是服务端渲染时报的错,如下图:

image

可以看到引用了 document 的地方是 /node_modules/rc-notification/lib/Notification.js:216:13,但是我根本没有用 rc-notification 啊,很可能是被 antd 的组件封装了起来,最大的嫌疑是 antd/message 组件。然后就把项目中引用了 antd/message 的地方全部注释掉了,果然,这时候报错没了,也能成功进入主页了

但是这个组件被我用在了 redux-sagas 中间件中,用来提示报错的,还是很有必要保留下来的。于是加了如下的判断,当处在服务端的时候就用 console.log() 打印错误信息,处在客户端也就是浏览器时就使用 message.error()

export default () => next => action => {
  const ret = next(action)
  switch (action.type) {
    case REQUEST_FAIL: {
      if (!!process.browser) {  // 判断当前环境
        message.error(action.payload || ERROR_TEXT)
      } else {
        console.log(action.payload || ERROR_TEXT)
      }
      break
    }
    default:
  }
  return ret
}

GitHub 的接口请求次数限制

在开发的过程中,发现接口动不动就报错,返回的信息提示 github api rate limit,说明请求接口次数有限制,Google 了一下发现了:

For unauthenticated requests, the rate limit allows for up to 60 requests per hour. Unauthenticated requests are associated with the originating IP address, and not the user making requests.
对于没有认证的请求,GitHub Api 的每小时次数限制是60次,并且非认证请求根据的是当前请求的 IP 地址进行限制,而不是根据用户账号来限制

也就是说肯定是存在认证请求:

For API requests using Basic Authentication or OAuth, you can make up to 5000 requests per hour. Authenticated requests are associated with the authenticated user, regardless of whether Basic Authentication or an OAuth token was used.
你可以通过 Basic Authentication 或者 OAuth 让请求次数提升到每小时5000次,不管是 Basic Authentication 还是 OAuth 的请求认证都是根据认证用户进行次数限制

另外我还查到了搜索接口是每分钟15次,认证请求后会提升到每分钟30次

2021.02.14 更新:上面的描述 github 已经从文档中删除,认证方式也从 Basic Auth 和 OAuth 两种变成了 github apps、oauth apps 和 personal access tokens 三种,上面提到的 oauth 实际上变成了 person access tokens,下面提到的 token 方式其实指的也是 personal access tokens 的认证方式(今天发现博客上的接口全挂了才发现 gh 更新了这一部分,不过幸好没什么特别大的变动,重新进行下 personal access tokens 认证即可)

所以事不宜迟我马上用自己的账号进行了认证,这里采用的是 ,参考此处,权限范围一个都不用选,默认是只读权限

于是就获得了一个 token,只要把 token 放到请求头中就能发起认证请求,提升请求次数:

const opts = {
  method,
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    Accept: 'application/json',
    Authorization: `token ${githubToken}` // 把 token 放到这里
  },
  // credentials: 'include',
  timeout,
  mode: 'cors',
  cache: 'no-cache',
}

分别调用列表接口和搜索接口测试一下:

[列表接口的 Response Header 的一部分]
X-Accepted-OAuth-Scopes: repo
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-GitHub-Media-Type: github.v3
X-GitHub-Request-Id: EDA3:179E:6D4C9F:84AEDC:5F940AEC
X-OAuth-Scopes
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4997
X-RateLimit-Reset: 1603541229
X-RateLimit-Used: 3
X-XSS-Protection: 1; mode=block

[搜索接口的 Response Header 的一部分]
X-Accepted-OAuth-Scopes
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-GitHub-Media-Type: github.v3
X-GitHub-Request-Id: D763:5ED6:1B24C5F:2398D78:5F940C18
X-OAuth-Scopes
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 29
X-RateLimit-Reset: 1603538004
X-RateLimit-Used: 1
X-XSS-Protection: 1; mode=block

上面的 X-RateLimit-Limit 表示当前最高请求次数,X-RateLimit-Remaining 为剩余次数,可以看到两个接口的次数均已经提升到 GitHub App 的最高次数(注:OAuth 认证是给 GitHub App 开发用的一种认证方式)

但是,这也出现了另一个问题,就是这样会使我的 token 暴露到接口中,我不确定这样会不会有问题,于是我仔细看了下文档

Note: If you're building a GitHub App, you don’t need to provide scopes in your authorization request.
注意:如果你正在开发一个 GitHub App,你并不需要选择任何权限范围

事实上我也并没有选择权限范围,也就是使用默认的只读权限,但是我也并不确认只读范围有多大,万一会把我的 private 仓库暴露了就凉了,于是我就用认证请求请求了我的 private 仓库测试一下。最后发现其实并不会暴露 private 仓库,接口会直接说找不到:

image

所以,就可以放心使用该 token,这部分的问题也解决了

而这里事实上也有另一个坑,那就是如果你把 token 直接写死在你的代码中,那么 gh 会自动把你账户下的这个 token 给删掉,所以务必不要直接在你的代码中出现 token,有部署服务器的朋友可以通过写一个接口获取的方式来做,这里不提

markdown-navbar 插件相关的坑

core-js

会提示 core-js 版本不够,直接加到依赖中即可 "core-js": "^2.6.5"

markdown-navbar 自动改变 hash 导致的报错

markdown-nav 这个插件可以改变当前的页面的 hash 来锚定当前所处的 markdown 的位置,但是这引发了 NextJS 的一个问题:如果 hash 不是通过 Next 的 router 或者 <Link /> 来改变的话,那么这个 hash 就不会记录到 Next 的服务中,这时候如果使用了浏览器的前进或者后退按钮返回这个带有 hash 的页面的话就会报错:Cannot read property 'indexOf' of undefined in NextJS server

而且很遗憾的是,markdown-navbar 插件无法自定义 hash 更改行为,只能调用 hash 改变时的回调,要解决这个问题只能从两个地方去改:

  1. 首先是 markdown-navbar 的自动改变 hash 的行为,也就是滚动到页面对应位置时会自动变动 hash,这个可以通过 markdown-navbar 插件的 updateHashAuto 参数设置为 false 关闭
  2. 然后就是点击导航栏的 title 会跳到 markdown 文档在页面中对应的地方时,竟然也会自动添加 hash,还无法关闭,这点确实比较麻烦。经过一番 Google,只能采用一个退而求其次的方法——把浏览器实际的 hash 历史删除,具体代码如下:
// 在 markdown-navbar 的 onHashChange 中调用这个方法即可
const removeHash = e => {
  setTimeout(() => {
    window.location.replace(
      window.location.href.toString().replace(window.location.hash, '') +
        '#' +
        e,
    )
  }, 100)
}

列表页分页器的翻页优化

一般而言在 SPA 程序中实现分页,最主要的目的有两个:一是实时改变 url,让不同的 url 对应相应的分页,程序可以直接通过 url 进来列表后根据 url 上的参数跳到对应的页面而不用用户一页一页地去翻;二是可以监听浏览器的前进后退按键来恢复页面数据。

这两点在传统的静态页面中根本不是问题,但是在单页面程序中需要一些手段去实现,最主流的做法自然是 history.pushState
无刷新更改 url + 异步请求数据 + window.onpopstate 监听浏览器前进后退,具体做法这里不多说,因为在 NextJS 有另外的实现

首先先看下 NextJS 的与之相关的 api:

接口 作用
hashChangeStart(url) hashChangeComplete(url) 这两个接口监听 hash 的变化
routeChangeStart(url) routeChangeComplete(url) 这两个接口监听 url 的变化(包括 search 的变化)

首先先说明下在使用中这两者有什么具体差异,就像上面所说的当 hash 参数(#后的参数)改变的时候会触发 hashChangeStart hashChangeComplete 而不会触发 routeChangeStart(url) routeChangeComplete(url),而只有当 url 本身或者 search 参数(? 后的参数)改变时才会触发 routeChangeStart(url) routeChangeComplete(url)

而根据我的需求,我们只需要一种改变 url 参数和监听的手段就行,而在 NextJS 中改变 url 参数的方法只能通过路由来跳而不能用 window 的方法来改变,否则会出现上面 markdown-navbar 中的坑。也就是说 url 和 hash 都是通过路由来改变的,例如:

// 改变 url
Router.push(`/post/list?page=1`)

// 改变 hash
Router.push(`/post/list#page=1`)

根据上面说的,其实无论是监听 router change 还是监听 hash change 都是可以实现分页优化的。于是我就直接采用了 hashChangeComplete 来做:

[文章列表页 src/containers/post/list.js]
import Router from 'next/router'

Router.events.on('routeChangeComplete', _handleRouteChange)

然后就是分页器点击事件,调用 router 改变 hash 即可:

import Router from 'next/router'

const handleClick = (page, keyword) => {
  Router.push(`/search#q=${keyword}&page=${page}`)
}

于是就能实现 NextJS 中的分页监听了,下一篇会讲讲怎样对程序做 SEO 优化

coc-list-yanky 中文 README

不要点开, 博客网站用的
博文置顶说明
最近找到了coc-yank 的替代方案 yanky.nvim,可惜他的列表实现基于 Telescope,于是参照 coc-yank 搞了个 yanky.nvim 记录的 coc list 实现,下文是中文版 README

说明

源码:coc-list-yank

这个插件是 yanky.nvim 的一个 coc list 实现,如果你像我一样使用 coc.nvim 和 yanky.nvim 并且不喜欢 Telescope,那你可以用这个 coc 列表插件替代 Telescope 来列出所有 yanky 历史

需要

请安装这些插件:

安装

在 neovim 中运行:

:CocInstall coc-list-yanky

创建快捷键映射:

nnoremap <silent> <space>y  :<C-u>CocList -A --normal yanky<cr>

感谢

许可

MIT

动态切换 fzf 源之 mru/files 源切换

不要点开, 博客网站用的
博文标题图片

pic

博文置顶说明
本文介绍如何在 fzf 中根据搜索输入框有无输入值来动态切换 mru(最近打开的文件列表)和 files(文件搜索),这个操作简单来说就像 VSCode 中的 ctrl-p,当没有任何输入值的时候展示的列表是最近打开的文件列表,当输入了值以后就会变成文件搜索。我们可以利用简单的 shell 脚本来在 fzf 中实现这一操作,本文会使用 neovim + coc.nvim 进行举例

一、工具介绍

※ 主角

fzf:本文的主角,具体是什么我就不多介绍了,相信点进来文章的读者都知道。

※ 辅助工具

以下是用于说明如何实现这一动态切换的辅助工具,非必须,只是为了方便说明,必须的只有 fzf。

  • neovim:本文使用的编辑器,当然你也可以使用 vim,或者其他编辑器或者终端,只要支持 fzf 就行。
  • coc.nvim:一个 neovim 的插件,配合 neovim 使用,提供了很多功能,它可以在 neovim 中打开一个文件的时候把文件路径保存到一个列表文件中,这个列表就是 mru 文件列表。
  • fd:一个高性能文件搜索工具,本文使用它来提供文件搜索功能,当然你也可以使用其他的工具,比如 find、rg 等等。

二、原理说明

2.1. mru 和 files 列表的获取

※ mru

首先你需要一个保存最近打开文件列表的文件实现,例如结合 neovim 和 coc.nvim,我们可以在 neovim 中打开一个文件的时候 coc.nvim 会自动触发一个保存事件,把当前打开的文件的绝对路径保存到一个文件中,这个文件就是 mru 文件列表,格式如下:

~/.config/coc/mru 文件:

...
/home/xxx/.config/nvim/init.vim
/home/xxx/.config/nvim/coc-settings.json
/home/xxx/a.txt
/home/xxx/b.txt
/home/xxx/c.txt
...

※ files

其次你需要一个文件搜索工具,例如 fd,它可以搜索当前目录下的所有文件,然后把搜索结果输出到 stdout。例如 fd 的 fd --type f --hidden 或 find 的 find . -type f

结合 fzf,我们可以把 fd 的搜索结果作为 fzf 的输入,然后 fzf 会把搜索结果展示到 fzf 的界面上,然后我们就可以通过 fzf 的交互来选择我们想要的文件。例如:

fd --type f --hidden | fzf

相信大家都知道如何使用这些工具,这里就不多说了。

2.2. fzf 的 KEY/EVENT BINDINGS

通过 man fzf 我们可以查到和 key/event 相关的配置,其中有这么一段话:

KEY/EVENT BINDINGS
       --bind option allows you to bind a key or an event to one or more actions. You can use it to customize key bindings or implement dynamic behaviors.

       --bind takes a comma-separated list of binding expressions. Each binding expression is KEY:ACTION or EVENT:ACTION.

       e.g.
            fzf --bind=ctrl-j:accept,ctrl-k:kill-line

这段话的意思是说我们可以通过 --bind 来绑定一个 key 或者 event 到一个或者多个 action,这样我们就可以实现一些自定义的功能,比如我们可以绑定一个 key 到一个 shell 脚本,这样当我们按下这个 key 的时候就会执行这个 shell 脚本,这个 shell 脚本可以是任何我们想要执行的命令,比如打开一个文件、执行一个命令等等。

同理通过 --bind 绑定一个 event 到一个 action 也是一样的,只不过 event 是 fzf 内部的事件,比如 change 事件,当搜索输入框的值发生变化的时候就会触发这个事件,我们可以通过绑定这个事件到一个 shell 脚本来实现当搜索输入框的值发生变化的时候执行这个 shell 脚本。

而我们要实现的动态切换 mru 和 files 就是通过绑定 change 事件来实现的,当搜索输入框的值发生变化的时候,我们就会执行一个 shell 脚本,这个 shell 脚本会根据搜索输入框的值来判断是展示 mru 列表还是 files 列表。change 的文档说明是:

change
       Triggered whenever the query string is changed

       e.g.
            # Move cursor to the first entry whenever the query is changed
            fzf --bind change:first

而 action 也是指 fzf 内部的一些动作,比如 accept,当我们按下回车的时候就会触发这个 action,这个 action 会把当前选中的结果输出到 stdout,然后退出 fzf。这里我们需要用的 action 就是 reload,文档的说明是:

RELOAD INPUT
    reload(...) action is used to dynamically update the input list without restarting fzf. I
 takes the same command template with placeholder expressions as execute(...).

    See https://github.com/junegunn/fzf/issues/1750 for more info.

    e.g.
         # Update the list of processes by pressing CTRL-R
         ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
                      --header-lines=1 --layout=reverse

         # Integration with ripgrep
         RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
         INITIAL_QUERY="foobar"
         FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
           fzf --bind "change:reload:$RG_PREFIX {q} || true" \
               --ansi --disabled --query "$INITIAL_QUERY"

那么我们就可以通过 fzf --bind change:reload(...) 来实现当搜索输入框的值发生变化的时候重新加载输入列表,这样我们就可以在重新加载输入列表的时候根据搜索输入框的值来判断是展示 mru 列表还是 files 列表了。

三、实现

3.1. 初步实现

首先实现一个初步的版本,只在终端中实现切换,不考虑在 neovim 中的实现,主要是为了方便说明。

※ 首先事前工作

  • fzf
  • fd
  • 一份已经存在的 mru 文件列表

详细的上面已经说过了,这里就不再赘述。

※ 切换脚本

~/dynamic_fzf_source.sh 切换脚本:

#!/bin/bash

input=$1 # 获取用户输入

if [ -z "$input" ]; then # 检查是否有任何输入
    perl -ne 'print if !$seen{$_}++' ~/.config/coc/mru # 没有输入时,显示 mru 文件内容
else
    fd --type f --hidden # 有输入时,搜索当前目录
fi

这段脚本的作用就是当没有任何输入的时候显示 mru 文件列表,当有输入的时候搜索当前目录下的所有文件。

而中间那一段 perl 脚本则是用于处理过滤掉 mru 文件中的重复行,因为 mru 文件中可能会有重复的行,这样就会导致 fzf 中展示的列表中有重复的行,所以我们需要过滤掉重复的行,这里使用的是 perl 脚本,当然你也可以使用其他的工具,比如 awk、sort 等等。

※ 绑定 fzf 的 change 事件

fzf --bind "change:reload($HOME/dynamic_fzf_source.sh {q})" < <($HOME/dynamic_fzf_source.sh)

这个命令意思就是当搜索输入框的值发生变化的时候重新加载输入列表,这里的输入列表就是 ~/dynamic_fzf_source.sh 脚本的输出,然后把搜索输入框的值作为参数传递给 ~/dynamic_fzf_source.sh 脚本,这样我们就可以在 ~/dynamic_fzf_source.sh 脚本中根据搜索输入框的值来判断是展示 mru 列表还是 files 列表了。

其中 {q} 是 fzf 用于获取搜索输入框的值的占位符,详细的可以查看 man fzf

< <($HOME/dynamic_fzf_source.sh) 这一段则是把 ~/dynamic_fzf_source.sh 脚本的输出作为 fzf 的输入。意思就是先直接执行 ~/dynamic_fzf_source.sh 脚本(且不传任何参数),然后把它的输出作为 fzf 的输入。

※ 效果截图

当前 mru 列表文件展示:

3.png

无输入值时,fzf mru 展示:

1.png

有输入值时,fzf files 展示:

2.png

3.2. 在 neovim 中实现

※ 首先事前工作

  • fzf
  • neovim
  • fd
  • 一份已经存在的 mru 文件列表

※ 切换脚本

该脚本与上面的脚本几乎一样,但是我额外进行了对 cwd(当前工作目录)的处理,把不在当前工作目录下的文件移除掉,这样就可以使 mru 列表看起来更加简洁。

~/.vim/dynamic_fzf_source.sh 切换脚本:

#!/bin/bash

cwd=$1  # 获取传入的当前工作目录
input=$2 # 获取用户输入

if [ -z "$input" ]; then
    perl -ne 'print substr($_, length("'"$cwd"'/")) if m{^'"$cwd"'/} && !$seen{$_}++' ~/.config/coc/mru
else
    fd --type f --hidden
fi

基本逻辑和上面的脚本一致,只多了一步对 cwd 的处理。

※ 绑定 fzf 的 change 事件

这里需要先在 neovim 中加载 fzf 插件,然后使用以下 vim 代码来构造一个 vim 方法(习惯使用 lua 的同学可以自行转换):

function! s:FZF(...)
  let cwd = getcwd()
  " 这一行的核心代码和上面的命令是一样的都是 `--bind change:reload(...)`
  " 不过多传了一个 `cwd` 参数
  let opts = fzf#wrap('FZF', { 'options': ['--bind=' . 'change:reload($HOME/.vim/dynamic_fzf_source.sh ' . cwd . ' {q})'] })
  " 这一行则是用于替代上面的 `< <($HOME/dynamic_fzf_source.sh)`
  " 因为 vim 中调用无法使用管道传输,所以会先构造一个初始的 source
  let opts.source = "perl -ne 'print substr(\$_, length(\"" . cwd . "/\")) if m{^" . cwd . "/} && !$seen{\$_}++' ~/.config/coc/mru"
  call fzf#run(opts)
endfunction

" 绑定一个快捷键
nnoremap <silent> <leader>f :call <SID>FZF()<CR>

这样我们就可以在 neovim 中使用 <leader>f 来打开 fzf mru/files 了,当然你也可以使用其他的快捷键。

※ 效果截图

无输入值时,fzf mru 展示:

4.png

有输入值时,fzf files 展示:

5.png

四、总结

上面介绍了如何在 fzf 中根据搜索输入框的值来动态切换 mru 和 files 列表,这样我们就可以在 fzf 中实现一个类似 VSCode 中的 ctrl-p 的功能了,当然这只是一个简单的实现,你可以根据自己的需求来进行扩展。

其基本原理使用了 fzf 的 --bind change:reload(...) 来实现。

以上就是本文的全部内容,希望对你有所帮助,如果有任何问题欢迎在评论区留言,我会尽快回复:)。

Win 10 + WSL 安装细节

wd gamma-mobile && git pull && yarn gamma-build && wd bff && easy-deploy restart gammawd gamma-mobile && git pull && yarn gamma-build && wd bff && easy-deploy restart gammawd gamma-mobile && git pull && yarn gamma-build && wd bff && easy-deploy restart gammawd gamma-mobile && git pull && yarn gamma-build && wd bff && easy-deploy restart gamma

Win10 部分

安装

  1. 硬盘安装

    1. 首先前往 https://msdn.itellyou.cn 下载系统

    2. 直接使用已经制作好的那个硬盘,那个硬盘一般别动,然后用这个硬盘来启动。但是因为未知原因,可能会出现两个启动项,选择字儿少那个

    3. 删除分区用分盘助手,制作分区使用傲梅助手,分盘选择 GPT + UEFI

    4. 这种方式安装以后不知为何无法使用账户激活,也可能是因为用的笔记本没有上网驱动导致初始化设置的时候无法联网获取激活信息。下次用台式试验下。若无法用账户激活,请使用亦是美 http://www.yishimei.cn 另外对于笔记本,可能进去的时候无法联网,记得先装一个网卡驱动。而对于 XPS13 9350 直接用硬盘里那个来启动即可

  2. 直接安装

    1. 同上 a

    2. 直接双击安装程序即可

简单配置

测试

  1. 用户名统一使用 hexh,但是如果事先登录的账号,可能会变成 25078,所以尽量进去系统后再登录账号

  2. hexh 里面新建一个 App 文件夹用来放所有的个人软件

  3. 开放查看隐藏文件、显示文件类型

  4. 快速访问只需要留下桌面和下载和 hexh

  5. 关闭 OneDriver

  6. 开启子系统

  7. hexh 里面新建文件夹 Workspace,里面新建 Projects、Documents、Notes,Projects 里面建 Private 和 Work

软件安装

  1. 基础软件

    1. 由另一台电脑准备好 SSR,记得使用的是便携版,然后导入账号 https://www.billtsnet.app/auth/login

    2. 然后下载 Chrome https://www.google.com/intl/zh-CN/chrome/

    3. WinRAR https://www.mpyit.com/?s=winrar&x=0&y=0

    4. 去 91flac 下载下载管理器 https://www.91flac.com/

    5. AutoHotKey https://www.autohotkey.com/ 然后顺便这一步把 Something 给下载了 https://github.com/hexh250786313/Something 然后把 my-key 放进 App,运行,运行禁用 Win + I 的脚本

    6. utools https://www.u.tools/

    7. 常用运行库 https://www.mpyit.com/?s=%E5%B8%B8%E7%94%A8%E8%BF%90%E8%A1%8C&x=0&y=0

    8. 百度下载 https://www.mpyit.com/?s=%E7%99%BE%E5%BA%A6&x=0&y=0 现在用的是亿寻

  2. 软件

    1. DirectX https://www.mpyit.com/?s=directx&x=0&y=0 安装的时候记得关闭 VC++ 库,因为上面的常用运行库已经安装过了

    2. QQ https://www.mpyit.com/?s=qq&x=0&y=0

    3. 迅雷 https://www.mpyit.com/?s=%E8%BF%85%E9%9B%B7&x=0&y=0 装完后记得修改下载目录,放到 Download 中去

    4. Typora https://typora.io/

    5. 搜狗 https://www.mpyit.com/?s=%E6%90%9C%E7%8B%97&x=0&y=0 这玩儿设置有点复杂

      1. 先删除微软输入法

      2. 关闭表情

      3. 设置为竖排

      4. 在状态栏显示

      5. 关闭切换快捷键

      6. 默认设置为英文

    6. 截图工具 https://www.faststone.org/FSCaptureDownload.htm 然后设置为 Rectangle,开启开机自动启动,输出到剪贴板

    7. Defender Control https://www.google.com/search?q=defender%20control

    8. ThinkPad Keyboard Suite Updater https://support.lenovo.com/us/en/solutions/pd026745 然后设置首选 TP 滚动

    9. VSCode

WSL 部分

WSL

  1. Windows Terminal,商店下载,下载完后配置字体和设置 JSON,然后从最近添加那里拖出一个快捷方式整到 ahk 的快速启动上

  2. 下载 Ubuntu 账户:lllk@hexh

  3. 使用 Win 上的代理更新软件和系统(如果不能连接网络则尝试 https):

sudo apt-get -o Acquire::http::proxy="http://127.0.0.1:1080" update
sudo apt-get -o Acquire::http::proxy="http://127.0.0.1:1080" -y upgrade
  1. 下载 zsh:
sudo apt-get -o Acquire::http::proxy="http://127.0.0.1:1080" install zsh
  1. 设置代理(否则下面的步骤会连不上):
export http_proxy=http://127.0.0.1:1080
export https_proxy=http://127.0.0.1:1080
  1. 安装 oh-my-zsh,安装过程中会问你是否让 zsh 变为默认的终端,选 yes 即可:
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
  1. 然后安装 zsh-autosuggestions 和 zsh-syntax-highlighting 这两个插件:

    1. zsh-autosuggestions https://github.com/zsh-users/zsh-autosuggestions

    2. zsh-syntax-highlighting https://github.com/zsh-users/zsh-syntax-highlighting

    3. 如果提示权限问题尝试在 .zshrc 第一行添加:

ZSH_DISABLE_COMPFIX=true
  1. 然后把 zsh 的主题设置为 muse

  2. 安装 nvm 和 npm

    1. nvm,要把指令的 bash 改为 zsh https://github.com/nvm-sh/nvm

    2. npm:

    nvm install node
  3. 安装 nvim:

    1. 下载 https://github.com/neovim/neovim/releases

    2. 下载完毕后放入 ~/app/nvim 中

    3. 然后执行:

    ./nvim.appimage --appimage-extract
    1. 然后在 ~/.zshrc 中配置:
    alias nvim="~/app/nvim/squashfs-root/usr/bin/nvim"
  4. 设置 git https://github.com/hexh250786313/amai_mayoi.github.io/blob/bak-original/post/2017-05-15.html

  5. 为 nvim 安装 Plug https://github.com/junegunn/vim-plug

  6. 前往 ~/.config 文件夹,把我的 nvim 配置拉下来:

git clone [email protected]:hexh250786313/nvim.git
  1. 安装 pip3,Python3 的包管理软件:
sudo apt-get -o Acquire::http::proxy="http://127.0.0.1:1080" install python3-pip
  1. 安装 neovim 模块:
pip3 install pynvim
npm install -g neovim
npm install -g yarn
  1. 然后就可以安装 nvim 插件了,记得开始安装前要设置代理,并且等 coc 的插件都安装完再退出(右边的窗口可以看 coc 插件的安装状态)。然后最后要检查下 MarkdownPreview 这个插件有没有问题,如果有问题要亲手前往 ~/.vim/plugged/markdown-preview.nvim 这个文件夹中 yarn 一下

  2. 安装 rg:

    1. 下载包和相关依赖 https://packages.debian.org/sid/ripgrep
    2. 执行安装:
    sudo dpkg -i <包名 1> <包名 2>
  3. tmux 相关:

    1. 前往 ~/app 文件夹,然后执行:
    git clone https://github.com/tmux/tmux.git
    cd tmux
    sudo apt-get -o Acquire::http::proxy="http://127.0.0.1:1080" install automake pkg-config
    sh autogen.sh
    sudo apt-get -o Acquire::http::proxy="http://127.0.0.1:1080" install libevent-dev ncurses-dev byacc
    ./configure && make
    1. 配置:
    cd ~ && touch ~/.tmux.conf && nvim ~/.tmux.conf
    1. 然后内容从 Something 中拿即可,然后 source 一下
  4. OpenVPN https://openvpn.net/community-downloads/ 然后把我的配置文件夹(howard 文件夹)放进 hexh/OpenVPN/config 文件夹中即可,连接时只需要打开然后在右键右下角图标连接即可。记得关掉开机自启

  5. 为 zsh 设置颜色,下载这个文件夹 https://github.com/arcticicestudio/nord-dircolors/tree/develop/src 然后改名为 .dir_colors 放到 ~,如果拉回来的 .zshrc 生效的话应该会直接生效

  6. 共享剪贴板:

    1. 安装 VcXsrv https://www.google.com/search?q=VcXsrv 一路默认。最后不选 Native opengl

    2. 安装 xclip:

    sudo apt-get -o Acquire::http::proxy="http://127.0.0.1:1080" install xclip
  7. 最后从 Something 文件夹把 .zshrc 配置替换到本地,source 一下,上面全部都可以生效了

未完待续~

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.