普通视图

发现新文章,点击刷新页面。
昨天以前九仞之行

如何让neovim 集成 jupyter

作者 Styunlen
2026年2月5日 12:28

前言

虽然可以为jupyter安装 jupyter-lsp 插件,实现 pythonlsp 相关功能,但是编码体验仍然不如我平时使用的 neovim,在查阅github后,发现已有neovim插件实现了这方面的功能,不过配置起来略微有些繁琐,顾决定将操作步骤记录下来,以便于后者学习参考。

前置环境

我的系统和相关工具链版本

  • OS: macOS 15.3 arm64
  • Kernel: 24.3.0
  • Packages: 171 (brew)
  • Shell: zsh 5.9
  • NVIM v0.10.4
  • Pyenv 2.4.23 + Python 3.12.8

博主 neovim 配置模板用的基于 lazy.nvimLazyVim ,所以一会儿也会将 molten-nvim插件中的教程转换为LazyVim插件的形式,其他配置文件模板请参考对应模板的语法。不过现阶段几乎所有的 neovim 模板都基于 lazy.vim,所以插件其实是通用的。

以下是一会儿需要用到的neovim 插件。

安装 molten 所需 python 依赖

此处参考文档1 和文档 2

其中,molten 会用到 python,博主使用 zinit 来管理 zsh 的相关插件,pyenv 是通过 zinit 来安装的,以下是我的配置参考

## pyenv
export PYENV_ROOT=$ZPFX/pyenv;
export WORKON_HOME=$PYENV_ROOT/versions
export PROJECT_HOME=$PYENV_ROOT/Devel
zinit ice wait lucid as"program" depth"1" cloneopts"" pick"$ZPFX/pyenv/libexec/pyenv" id-as'pyenv' run-atpull \
  atclone"echo pyenv installing at clone; export PYENV_ROOT=$ZPFX/pyenv; echo PYENV_ROOT: $ZPFX/pyenv; curl -fsSL https://pyenv.run | bash; $ZPFX/pyenv/libexec/pyenv init - | tee $ZPFX/pyenv/init.zsh" \
  atpull"echo pyenv updating at pull; %atclone" \
  atinit"source $ZPFX/pyenv/init.zsh;"
zinit light zdharma-continuum/null

molten的文档中可以注意到,他用的是 virtualenvs来管理插件所需要的 python 依赖包,这里我用的是 pyenv,所以使用的是 pyenv 自带的 pyenv-virtualenv插件来创建虚拟环境,原理大同小异,也可以自定义使用其他虚拟包环境管理器。

使用以下命令创建并激活一个名为 neovim 的虚拟环境,随后使用 pip 安装 molten 所需 python 依赖。

pyenv virtualenv neovim
pyenv activate neovim
pip install pynvim jupyter_client cairosvg plotly kaleido pnglatex pyperclip nbformat

接着按照文档,修改 neovim 的配置,使其能找到当前虚拟环境中的 python

这里要注意,请先使用以下命令查看自己的 neovim 虚拟环境安装目录中,python 的路径

echo $VIRTUAL_ENV/bin/python3

博主的输出是如下内容

/Users/styunlen/.zinit/polaris/pyenv/versions/neovim/bin/python3

所以接下来我就在 neovim 的配置里写以下内容,如果你也像我一样在 zshrc 中声明了WORKON_HOME变量为$PYENV_ROOT/versions,则可以使用下面注释掉的那个写法

vim.g.python3_host_prog = vim.fn.expand("~/.zinit/polaris/pyenv/versions/neovim/bin/python3")
-- vim.g.python3_host_prog = os.getenv("WORKON_HOME") .. "/neovim/bin/python3"

这些都操作完之后,我们进入工作目录,比如我的工作目录为~/WorkDir/Comp,然后重新使用 pyenv(可以自定义)创建并激活一个包含数据分析、人工智能、数据可视化等常见python模块的虚拟环境common

然后安装 ipykernel包,并添加一个名为 neovimipykernel核心,用于 jupyter 后期使用。

以下是相关命令:

pyenv virtualenv common
pyenv activate common
pip install ipykernel
python -m ipykernel install --user --name neovim

配置 neovim

此处参考文档 3

主要用于添加 neovimipynb 的直接编辑支持

由于我使用 LazyVim,所以单独编写一个 lua 文件来配置与 molten 相关插件。如不想看详细步骤,可以直接跳转到第二页复制完整配置

使用以下命令配置插件

nvim ~/.config/nvim/lua/plugins/molten.lua

然后将最开始提到过的插件都加入其中,搭建一个框架

return {
  {
    "benlubas/molten-nvim",
    version = "^1.0.0", -- use version <2.0.0 to avoid breaking changes
    dependencies = { "3rd/image.nvim" },
    build = ":UpdateRemotePlugins",
    init = function()
    end,
  },
  { -- requires plugins in lua/plugins/treesitter.lua and lua/plugins/lsp.lua
    -- for complete functionality (language features)
    "quarto-dev/quarto-nvim",
    ft = { "quarto", "markdown" },
    dev = false,
    config = function()
    end,
    dependencies = {
      -- for language features in code cells
      -- configured in lua/plugins/lsp.lua and
      -- added as a nvim-cmp source in lua/plugins/completion.lua
      "jmbuhr/otter.nvim",
      "nvim-treesitter/nvim-treesitter",
    },
  },
  {
    "GCBallesteros/jupytext.nvim",
    opts = {
      style = "markdown",
      output_extension = "md",
      force_ft = "markdown",
    },
  },
  { -- preview equations
    "jbyuki/nabla.nvim",
    keys = {
      { "<leader>qm", ':lua require"nabla".toggle_virt()<cr>', desc = "toggle [m]ath equations" },
    },
  },
  {
    "nvim-lualine/lualine.nvim",
    dependencies = {
      "benlubas/molten-nvim",
    },
    event = "VeryLazy",
    opts = function(_, opts)
      table.insert(opts.sections.lualine_y, {
        function()
          return require("molten.status").kernels() .. " (ipykernel)"
        end,
      })
    end,
  },
  {
    -- see the image.nvim readme for more information about configuring this plugin
    "3rd/image.nvim",
    opts = {
      backend = "kitty", -- whatever backend you would like to use
      processor = "magick_cli", -- or "magick_cli"
      max_width = 100,
      max_height = 12,
      max_height_window_percentage = math.huge,
      max_width_window_percentage = math.huge,
      window_overlap_clear_enabled = true, -- toggles images when windows are overlapped
      window_overlap_clear_ft_ignore = { "cmp_menu", "cmp_docs", "" },
    },
  },
}

接下来的步骤主要是分布完成 neovimjupyter 以下四个功能的集成

  • 代码运行
  • 结果预览
  • markdownPython LSP 的相关功能 (自动补全, 定义引用跳转, 快速重命名, 代码格式化等)
  • 文件格式转换

上述框架中已经完成了插件集成和联动,比如 lualine 显示当前激活的 ipykernel,但还没有添加快捷键映射、文件自动处理等功能,我们接下来根据上述框架,从上到下一一完善这个配置。

benlubas/molten-nvim

首先完善benlubas/molten-nvim插件的 init 方法,主要实现了以下几个功能

  • neovim 能找到我们为 molten 插件单独创建的虚拟环境,上面有个地方就是添加此处配置,只不过我把他集成到了此处
  • 开启一些官方推荐的插件选项
  • 官方推荐的快捷键绑定
  • 打开 ipynb 文件时,自动初始化 moltenquarto插件,并导入已有的运行结果数据;关闭时,自动导出运行结果数据
  • 添加新建 jupyter notebook的指令

代码如下

init = function()
  local function getenv(var)
    local v = os.getenv(var)
    if v == nil then
      return ""
    else
      return v
    end
  end
  -- vim.g.python3_host_prog=vim.fn.expand("")
  vim.g.python3_host_prog = getenv("WORKON_HOME") .. "/neovim/bin/python3"

  -- I find auto open annoying, keep in mind setting this option will require setting
  -- a keybind for `:noautocmd MoltenEnterOutput` to open the output again
  vim.g.molten_auto_open_output = false

  -- this guide will be using image.nvim
  -- Don't forget to setup and install the plugin if you want to view image outputs
  vim.g.molten_image_provider = "image.nvim"

  -- optional, I like wrapping. works for virt text and the output window
  vim.g.molten_wrap_output = true

  -- Output as virtual text. Allows outputs to always be shown, works with images, but can
  -- be buggy with longer images
  vim.g.molten_virt_text_output = true

  -- this will make it so the output shows up below the \`\`\` cell delimiter
  vim.g.molten_virt_lines_off_by_1 = true
  -- these are examples, not defaults. Please see the readme
  vim.g.molten_image_provider = "image.nvim"
  vim.g.molten_output_win_max_height = 20
  vim.keymap.set("n", "<localleader>mi", ":MoltenInit<CR>", { silent = true, desc = "Initialize the plugin" })
  vim.keymap.set(
    "n",
    "<localleader>e",
    ":MoltenEvaluateOperator<CR>",
    { silent = true, desc = "run operator selection" }
  )
  vim.keymap.set("n", "<localleader>rl", ":MoltenEvaluateLine<CR>", { silent = true, desc = "evaluate line" })
  vim.keymap.set("n", "<localleader>rr", ":MoltenReevaluateCell<CR>", { silent = true, desc = "re-evaluate cell" })
  vim.keymap.set(
    "v",
    "<localleader>rv",
    ":<C-u>MoltenEvaluateVisual<CR>gv",
    { silent = true, desc = "evaluate visual selection" }
  )
  vim.keymap.set("n", "<localleader>rd", ":MoltenDelete<CR>", { silent = true, desc = "molten delete cell" })
  vim.keymap.set("n", "<localleader>oh", ":MoltenHideOutput<CR>", { silent = true, desc = "hide output" })
  vim.keymap.set(
    "n",
    "<localleader>os",
    ":noautocmd MoltenEnterOutput<CR>",
    { silent = true, desc = "show/enter output" }
  )
  -- if you work with html outputs:
  vim.keymap.set(
    "n",
    "<localleader>mx",
    ":MoltenOpenInBrowser<CR>",
    { desc = "open output in browser", silent = true }
  )
  local imb = function(e) -- init molten buffer
    vim.schedule(function()
      vim.cmd("MoltenInit")
      vim.cmd("MoltenImportOutput")
      vim.cmd("QuartoActivate")
    end)
  end
  -- automatically import output chunks from a jupyter notebook
  vim.api.nvim_create_autocmd("BufAdd", {
    pattern = { "*.ipynb" },
    callback = imb,
  })

  -- we have to do this as well so that we catch files opened like nvim ./hi.ipynb
  vim.api.nvim_create_autocmd("BufEnter", {
    pattern = { "*.ipynb" },
    callback = function(e)
      if vim.api.nvim_get_vvar("vim_did_enter") ~= 1 then
        imb(e)
      end
    end,
  })
  -- automatically export output chunks to a jupyter notebook on write
  vim.api.nvim_create_autocmd("BufWritePost", {
    pattern = { "*.ipynb" },
    callback = function()
      if require("molten.status").initialized() == "Molten" then
        vim.cmd("MoltenExportOutput!")
      end
    end,
  })

  -- Provide a command to create a blank new Python notebook
  -- note: the metadata is needed for Jupytext to understand how to parse the notebook.
  -- if you use another language than Python, you should change it in the template.
  local default_notebook = [[
      {
      "cells": [
      {
        "cell_type": "markdown",
        "metadata": {},
        "source": [
          ""
        ]
      }
      ],
      "metadata": {
      "kernelspec": {
        "display_name": "Python 3",
        "language": "python",
        "name": "python3"
      },
      "language_info": {
        "codemirror_mode": {
          "name": "ipython"
        },
        "file_extension": ".py",
        "mimetype": "text/x-python",
        "name": "python",
        "nbconvert_exporter": "python",
        "pygments_lexer": "ipython3"
      }
      },
      "nbformat": 4,
      "nbformat_minor": 5
    }
  ]]

  local function new_notebook(filename)
    local path = filename .. ".ipynb"
    local file = io.open(path, "w")
    if file then
      file:write(default_notebook)
      file:close()
      vim.cmd("edit " .. path)
    else
      print("Error: Could not open new notebook file for writing.")
    end
  end

  vim.api.nvim_create_user_command("NewNotebook", function(opts)
    new_notebook(opts.args)
  end, {
    nargs = 1,
    complete = "file",
  })
end,

quarto-dev/quarto-nvim

文档中在做这一步之前,需要先为 nvim-treesitter 添加一个新的选择器,方便我们在编辑ipynb时,快速上下跳转,可以输入以下命令快捷完成

cat >~/.config/nvim/after/queries/markdown/textobjects.scm <<EOF
;; extends

(fenced_code_block (code_fence_content) @code_cell.inner) @code_cell.outer
EOF

然后配置 quarto-dev/quarto-nvim插件的config函数,主要实现以下几个功能

  • code runner 设置为 molten,连接两个插件
  • 使用一些官方文档推荐的配置和快捷键
  • 扩展nvim-treesitter插件

代码如下

config = function()
  require("quarto").setup({
    lspFeatures = {
      -- NOTE: put whatever languages you want here:
      languages = { "r", "python", "rust" },
      chunks = "all",
      diagnostics = {
        enabled = true,
        triggers = { "BufWritePost" },
      },
      completion = {
        enabled = true,
      },
    },
    keymap = {
      -- NOTE: setup your own keymaps:
      hover = "H",
      definition = "gd",
      rename = "<leader>rn",
      references = "gr",
      format = "<leader>gf",
    },
    codeRunner = {
      enabled = true,
      default_method = "molten",
    },
  })
  local runner = require("quarto.runner")
  vim.keymap.set("n", "<localleader>rc", runner.run_cell, { desc = "run cell", silent = true })
  vim.keymap.set("n", "<localleader>ra", runner.run_above, { desc = "run cell and above", silent = true })
  vim.keymap.set("n", "<localleader>rA", runner.run_all, { desc = "run all cells", silent = true })
  vim.keymap.set("n", "<localleader>rl", runner.run_line, { desc = "run line", silent = true })
  vim.keymap.set("v", "<localleader>r", runner.run_range, { desc = "run visual range", silent = true })
  vim.keymap.set("n", "<localleader>RA", function()
    runner.run_all(true)
  end, { desc = "run all cells of all languages", silent = true })
  require("nvim-treesitter.configs").setup({
    -- ... other ts config
    textobjects = {
      move = {
        enable = true,
        set_jumps = false, -- you can change this if you want.
        goto_next_start = {
          --- ... other keymaps
          ["]b"] = { query = "@code_cell.inner", desc = "next code block" },
        },
        goto_previous_start = {
          --- ... other keymaps
          ["[b"] = { query = "@code_cell.inner", desc = "previous code block" },
        },
      },
      select = {
        enable = true,
        lookahead = true, -- you can change this if you want
        keymaps = {
          --- ... other keymaps
          ["ib"] = { query = "@code_cell.inner", desc = "in block" },
          ["ab"] = { query = "@code_cell.outer", desc = "around block" },
        },
      },
      swap = { -- Swap only works with code blocks that are under the same
        -- markdown header
        enable = true,
        swap_next = {
          --- ... other keymap
          ["<leader>sbl"] = "@code_cell.outer",
        },
        swap_previous = {
          --- ... other keymap
          ["<leader>sbh"] = "@code_cell.outer",
        },
      },
    },
  })
end,

总结

完成以上内容后,就基本完成了所有配置,我们进入工作目录随意打开一个jupyter notebook试试,OK,完成!

参考文档

  1. molten-nvim/docs/Not-So-Quick-Start-Guide.md at main · benlubas/molten-nvim · GitHub
  2. molten-nvim/docs/Virtual-Environments.md at main · benlubas/molten-nvim · GitHub
  3. molten-nvim/docs/Notebook-Setup.md at main · benlubas/molten-nvim · GitHub

放弃吧?不!Hue 在 Linux ARM64 上的绝境求生指南

作者 Styunlen
2025年11月30日 20:05

😇 前言

大数据课的坑娃实录:当 Hue 遇上 "薛定谔的环境,本以为 Docker 是避风港,结果它把我推向了更深的技术深渊..."

对象大数据课要求部署 Hue 可视化工具,看着官方 GitHub 上明晃晃的Dockerfile,我拍着胸脯说 "这有何难"—— 毕竟 Docker 的 slogan 不就是 "一次构建,到处运行" 嘛?结果她电脑复杂的环境,差点让我原地轮回。以下是一些问题实录。

🕵️‍♂️ 坑娃破案时刻实录

🔧 Docker 镜像编译历险记:当 Hue 遇上 DNS 迷局

💥 经典剧情:我就知道会出问题

官方说支持 ARM 但不给镜像?没关系,我自己造!—— 直到被 DNS 按在地上摩擦

遇事不决,先 Google 一下,检索问题相关信息,我找到了以下三篇 "秘籍" :

  • ✅ GitHub PR#4149:有人提交了 ARM64 的 CI 流程
  • ✅ Discourse 论坛:官方明确答复 "ARM64 是支持的"(还好没白折腾手动编译)
  • ✅ 官方文档:直接甩出构建命令!抄起键盘就是干

从以上两个链接可以知道,官方其实是支持运行在 arm 上的。不过官方没有提供 arm64docker 镜像,我看有人提交了对应 cicdpr,不过没被 merge

docker build https://github.com/cloudera/hue.git#release-4.11.0 -t gethue/hue:4.11.0 -f tools/docker/hue/Dockerfile
docker tag gethue/hue:4.11.0 gethue/hue:latest

本以为复制粘贴上述代码,编译完镜像就能下班,结果ports.ubuntu.com给我表演了个 "查无此站"——
🌐 科学上网?没用!🔧 换国内源?试过了!📚 翻到 Medium 神文说要改 DNS,测试命令直接给我暴击:

本以为复制粘贴就能下班,结果ports.ubuntu.com给我表演了个"查无此站"——

🌐 科学上网?没用! 🔧 换国内源?试过了!

📚 翻到Medium神文,说要改DNS,测试命令直接给我暴击:

docker run busybox nslookup google.com
;; connection timed out; no servers could be reached

🎯 绝处逢生:给Docker开"后门"

灵机一动加上--network=host参数(感谢Docker网络玄学):

docker run --network=host busybox nslookup google.com

居然活了!活了!赶紧给构建命令也加上这个"续命符",编译进度条终于开始欢快跳动~

docker build --network=host https://github.com/cloudera/hue.git#release-4.11.0 -t gethue/hue:4.11.0 -f tools/docker/hue/Dockerfile
docker tag gethue/hue:4.11.0 gethue/hue:latest

🚀 续命成功但未完待续

搞定镜像编译后启动容器:

docker run \
 -it \
 -p 8888:8888 \
 -v ~/docker/hue/config/hue.ini:/usr/share/hue/desktop/conf/hue.ini \
 --name my-hue-instance \
 --add-host=host.docker.internal:host-gateway \
 --add-host=$(hostname):$(hostname -I | awk '{print $1}') \
 gethue/hue:latest

(内心OS:按照技术折腾守恒定律,这波操作后应该...又要出幺蛾子了吧?)

🌐 网桥崩坏现场:Docker 网络的 "背叛" 与救赎

镜像编译成功≠万事大吉,Docker 用实际行动告诉我:网络问题才是终极 BOSS

🕵️‍♂️ 破案线索:隔离网络的致命破绽

明明--network=host模式下一切正常,换回容器网络立刻"DNS罢工"——这症状像极了Docker网桥在背后捅刀子!

🔍 现场勘查

  • 容器内 ping bilibili.com 不通,curl google.com 也未响应
  • 宿主机网络正常,其他容器也不能上网

结论:docker0网桥接口黑化实锤

⚠️ 高危:网桥重置仪式

(友情提示:以下操作需穿戴"数据防护甲",生产环境请先焚香祷告)

sudo service docker stop # 给Docker断个电
sudo pkill docker # 确保连进程尸体都别剩
# sudo iptables -t nat -F # 【生产环境慎点!】这行是清除所有NAT规则的致命咒语
sudo ifconfig docker0 down # 把故障网桥打入冷宫
sudo brctl delbr docker0 # 物理删除这个叛徒
sudo service docker start # Docker重生仪式

🎉 复活时刻:网络通畅的治愈瞬间

重启后,Docker会自动重建纯洁的docker0网桥,再次运行容器测试网络时——

# 执行
docker run busybox nslookup google.com

DNS解析成功的那一刻,世界都明亮了✨

继续马不停蹄,尝试运行 Hue

🎮 终局之战:Hue 魔丸降服仪式

历经九九八十一难,终于听到了胜利的 BGM!

🔄 重启仪式:给容器最后一次机会

docker restart my-hue-instance

(内心 OS:这次要是再崩,我就把电脑打包送给 Docker 总部当祭品, hhh 开玩笑,我可舍不得😂)

🌐 浏览器朝圣时刻

颤抖着在地址栏输入localhost:8888

⏳ 加载动画像一个世纪那么长

突然!熟悉的 Hue 登录界面带着圣光出现!

(此时应有《权力的游戏》主题曲响起,配合弹幕 "恭迎陛下登基")

😌 总结 & 参考

回顾这趟踩坑之旅:

编译时就被DNS 解析玩起了捉迷藏,运行容器后,网桥接口还是贼心不死。如今看着登录框里闪烁的光标,突然理解了什么叫 "技术人的快乐如此简单"。

以下是我在解决问题时查阅的资料列表,感谢互联网,让我的问题能解决得如此迅速,OK,收工。又水了一篇博文QAQ

HomeAssistant 折腾日记——nginx 反向代理配置

作者 Styunlen
2025年11月25日 08:19

前言

最近在折腾改造我们的宿舍,用 docker 搭建了 HomeAssistant,接入米家和一些 esp 设备。我用 frp 技术将 HA 发布到了公网上,以便于我远程控制设备和接受与设备相关的通知。不过为了安全起见,我通过 nginxHomeAssistant 配置了 HTTPS 加密访问。不过,在配置完成之后,出现了一些小问题,这篇博客用来记录一下解决过程

正文

400 Bad Request

server {
    listen       $port ssl;
    server_name  HomeAssistant_https;
    client_max_body_size   1G;

    ssl_certificate      /usr/share/nginx/certs/ssl.crt;
    ssl_certificate_key  /usr/share/nginx/certs/ssl.key;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    add_header serverhello $http_host;
    proxy_buffering off;
    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-Ip $remote_addr;
        add_header serverhello $http_host;
        proxy_pass http://host.docker.internal:8123;
    }
    error_page 497 https://$http_host$request_uri; #302跳转
}


以上是我的 nginx 反代配置,配置完成了之后发现虽然网页能访问,但是返回 400

原来是 HomeAssistant 为了抵御恶意攻击,要求必须经过信任的 ip 网段发来的代理请求才能被正常响应,因此需要我们修改 HomeAssistant 的配置来允许本地 nginx 的访问。如下图所示

我们在 configuration.yaml 配置文件中添加以下字段,其中 XXX.XXX.XXX.XXX 替换为 nginx 代理的地址

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - XXX.XXX.XXX.XXX # Add the IP address of the proxy server

之后访问就 OK 了

websocket 代理

由于 HomeAssistant 还使用了 websocket,而我上述的 nginx 配置中没有开启 ws 的代理,参考Reverse proxy using NGINX - Community Guides - Home Assistant Community 可知,还需要增加以下字段到 location 配置中:

        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

这篇帖子里面给了我们一段可以参考的 nginx 配置,如下:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    # Update this line to be your domain
    server_name example.com;

    # These shouldn't need to be changed
    listen [::]:80 default_server ipv6only=off;
    return 301 https://$host$request_uri;
}

server {
    # Update this line to be your domain
    server_name example.com;

    # Ensure these lines point to your SSL certificate and key
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    # Use these lines instead if you created a self-signed certificate
    # ssl_certificate /etc/nginx/ssl/cert.pem;
    # ssl_certificate_key /etc/nginx/ssl/key.pem;

    # Ensure this line points to your dhparams file
    ssl_dhparam /etc/nginx/ssl/dhparams.pem;


    # These shouldn't need to be changed
    listen [::]:443 ssl default_server ipv6only=off; # if your nginx version is >= 1.9.5 you can also add the "http2" flag here
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
    # ssl on; # Uncomment if you are using nginx < 1.15.0
    ssl_protocols TLSv1.2;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    proxy_buffering off;

    location / {
        proxy_pass http://127.0.0.1:8123;
        proxy_set_header Host $host;
        proxy_redirect http:// https://;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

我参考上述配置修改后,总配置如下:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen       $port ssl;
    server_name  HomeAssistant_https;
    client_max_body_size   1G;

    ssl_certificate      /usr/share/nginx/certs/ssl.crt;
    ssl_certificate_key  /usr/share/nginx/certs/ssl.key;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    add_header serverhello $http_host;
    proxy_buffering off;
    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-Ip $remote_addr;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        add_header serverhello $http_host;
        proxy_pass http://host.docker.internal:8123;
    }
    error_page 497 https://$http_host$request_uri; #302跳转
}

结语

折腾了俩小时,成功接入米家,终于能通过 HomeAssistant 操控米家的设备啦!

接下来就是采购各种 esp 板子和传感器,进一步实现智能化寝室啦!

参考

《震耳欲聋》观后感(带剧透)

作者 Styunlen
2025年10月3日 23:37

昨天看了《震耳欲聋》,片如其名,激起了我对法律本质内涵的思考,以下思考带有部分剧透,未看此影片的同志可以观看后一起思考讨论。

从内涵看,马克思主义法学强调法的本质是阶级统治工具与社会经济结构的统一,法是统治阶级意志的体现,这样的定义毫无疑问是公式化的,没有人情味。而我们生活中的每一个人都是鲜活的存在,各自心中都有自己的道德尺度,都对法律的公平正义有着最朴素的理解与期待,影片的多处情节通过法律与人们心中朴素期待之间的矛盾勾起了我心中难过的情绪,泪水无声宣泄着对片中既得利益者得意的愤怒和对受害者无力发声的同情。

李淇(檀健次饰)与汤宇轩(王戈饰)在最初毫无疑问是市侩律师和坚守法理纯真本质律师的代表。但仔细回溯看李淇的心理发展路线可以看到,他本不如此,作为聋人的健听后代(Coda),求学路上的他敢于为尊严发声,拒绝了消费他 Coda 身份而募捐来的助学金,可在委托张小蕊(兰西雅饰)案件时,却为他当事人张小蕊争取来了靠诈骗聋人群体牟利的邪恶资本启航金融的助学金,这无疑是在追名逐利过程中忘记了自己的初心和作为法律工作者的尊严与良知。

这之间还有个有意思的矛盾对比,李淇作为健听人,所作所为都想努力脱离原生家庭的圈子,活出一个他个人所认为的人样,可在面对现实的不公时,他最初的选择却是与坏人同流合污沆瀣一气,收钱闭嘴不发声,甚至为了自洽自己的行为,美其名曰是为了委托人好。而张小蕊的做法却是积聚力量,“发声”对抗不公。我想他的觉醒不是偶然,小蕊那些无声的比划虽然不动风雨,却比任何有声的慷慨陈词都更有力量,因为每一个手势都是小蕊在努力讨回属于自己的公道,也正是在他作为健听人一次次装聋作哑和小蕊作为听障人一次次努力发声之间的强烈对比,让李淇终于明白了,他来自听障家庭,本该是连接无声世界与有声世界的桥梁,他所谓的 “装聋作哑” 不是妥协,而是对自己身份和公平正义的背叛,于是他找回了自己的良善和初心,选择了为自己的尊严和立场勇敢发声。

电影的结局,李淇虽然成功完成了案件的民转刑,将不法分子送进了大牢,维护住了法律的公平正义,可现实里,理想主义的胜利需要我们在复杂的现实考验中不忘初心,时刻警惕诱惑的腐蚀和理想信念的松懈。以上仅为个人感悟,好电影需自行观看评鉴,希望各位看完之后也能启发各位对法治的思考,也愿每个人未来都能在各行各业为祖国的法治建设贡献自己的力量。

让SDKMAN的JDK在macOS上「合法上岗」的全套骚操作

作者 Styunlen
2025年5月12日 13:45

🚨 事故现场:当SDKMAN遇上java_home

我使用 sdkman 来管理多个 java 版本,每次在终端输入java -version时都岁月静好,直到今天使用 gradle 编译项目时,却突然举着告示牌抗议:「找不到JDK,罢工了!」

重新查看日志,原来是 macos 自带的 /usr/libexec/java_home 程序不识别 sdkman 安装的 java,所以需要我们通过一些小 trick 来骗过 macos,使得 sdkman 安装的 java 也能被系统识别到。

为了增强文章阅读的趣味性,这篇文章我试着使用 ai 来润色和生成文本,今后我的博客也会更多的借助 aigc 的力量,来辅助我讲清楚一些容易被我遗漏的知识细节,让后来的初学者也能更容易理解我的解决思路,并复现我的技术解决方案。

两大阵营的冲突根源

  1. 🍎 苹果原住民JDK 住在/Library/Java/JavaVirtualMachines高档小区,每户都有:
    • 🏠 Contents/Home(真实住宅)
    • 📄 Info.plist(房产证明)
  2. 🚀 SDKMAN太空移民 蜗居在~/.sdkman的集装箱公寓,只有:
    • 🧳 精简版JDK文件
    • ❌ 没有房产证和门牌号

🔧 偷天换日三步走

第1步:伪造身份档案

# 在苹果JDK小区买个虚拟房产
sudo mkdir -p /Library/Java/JavaVirtualMachines/sdkman-current/Contents

第2步:制造时空隧道

# 创建跨次元传送门(<YOUR_USERNAME>替换为你的用户名)
sudo ln -s /Users/<YOUR_USERNAME>/.sdkman/candidates/java/current \
    /Library/Java/JavaVirtualMachines/sdkman-current/Contents/Home

第3步:办理假身份证

<!-- 伪造Info.plist文件 -->

cat <<EOF | sudo tee /Library/Java/JavaVirtualMachines/sdkman-current/Contents/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>sdkman.current</string>
    <key>CFBundleName</key>
    <string>SDKMAN Current JDK</string>
    <key>JavaVM</key>
    <dict>
        <key>JVMPlatformVersion</key>
        <string>9999</string>
        <key>JVMVendor</key>
        <string>Homebrew</string>
        <key>JVMVersion</key>
        <string>9999</string>
    </dict>
</dict>
</plist>
EOF

🧪 验收测试

# 查看当前JDK路径
/usr/libexec/java_home

应该输出:

/Library/Java/JavaVirtualMachines/sdkman-current/Contents/Home

查看所有已识别JDK

/usr/libexec/java_home -V

输出示例:

Matching Java Virtual Machines (1):
    9999 (arm64) "Homebrew" - "SDKMAN Current JDK" /Library/Java/JavaVirtualMachines/sdkman-current/Contents/Home
/Library/Java/JavaVirtualMachines/sdkman-current/Contents/Home

💡 原理黑匣子

系统行为我们的伪装术
扫描标准JDK目录创建虚拟目录结构
检查Info.plist合法性伪造包含必要字段的配置文件
验证JVM实际存在符号链接指向真实SDKMAN路径

版本号9999的阴谋:通过设置超高版本号,确保系统总是优先选择我们的「超级JDK」

⚠️ 注意事项

  1. 如果升级macOS系统,可能需要重新配置

💥 常见翻车现场急救包

症状1:操作后java_home依然装瞎

三连诊断术

ls -l /Library/Java/JavaVirtualMachines/sdkman-current/Contents/Home  # 检查传送门是否畅通
plutil -lint Info.plist  # 验证身份证是否伪造成功
java -version  # 确认SDKMAN当前JDK是否正常

🌟 结语

🚀 神操作总结(一张图看懂)

# 总操作流程图
+------------------+       +------------------+       +------------------+
|  伪造JDK别墅      |       | 创建传送门         |       | 办理9999号身份证  |
|  mkdir -p        |------>| ln -s            |------>| Info.plist      |
+------------------+       +------------------+       +------------------+

🌈 哲学时间

为什么java_home这么固执?
就像机场安检员坚持要检查登机牌,java_home的设计哲学是:

  1. 只相信官方认证的JDK(防止有人携带危险品)
  2. 严格检查目录结构(登机口必须符合IATA标准)
  3. 版本号就是VIP等级(所以我们的9999是超级黑卡)

🎉 最终效果演示

# 在XCode中优雅调用(Build Phase脚本示例)
export JAVA_HOME=$(/usr/libexec/java_home)
./gradlew build --stacktrace # 现在可以愉快甩锅给Android Studio了

📚 引用

告别误触复制!tmux 鼠标滚动的正确开启姿势

作者 Styunlen
2025年3月18日 19:19

🐒人类早期驯服tmux实录(错误示范)

最近,因为校园网不稳定,使用 ssh 时会频繁掉线,然后我就需要频繁恢复会话,因此被这个事儿弄得烦躁后,老老实实学习了 tmux 的用法,其中,tmux 能通过以下命令或配置开启鼠标模式

tmux set -g mouse on  # 打开潘多拉魔盒

然而当你想要优雅地通过 set -g mouse on 准备享受丝滑滚动时~,下一秒选中文本突然触发「量子纠缠式复制」。。。

明明只是误触,居然把我误触选中的东西给复制下来了。我目前都在用 Mac 写代码, 那平时肯定都用触摸板,单击也是经常有的事儿嘛,怎么我单击一下就判定我是选中状态呢,非常不河狸🦫啊!

🎮 原理级操作指南

在翻阅了一下 tmuxdocs 后,发现他的 mouse-mode 主要有以下功能:

  • 鼠标三件套行为分解:
    • 🖱️ 滚动浏览(想要)
    • 🪟 窗格调整大小(想要)
    • 🪓 选中即复制(想砍)

但同时,他又能通过 unbind 方法,禁用掉部分作用域下的鼠标滚动事件,那思路就很清晰啦

  1. mouse on 先开总闸门
  2. 用 unbind 劫持滚轮事件,悄悄封印 MouseDrag1Pane 事件的复制诅咒

以下是 tmux 配置实现

set -g mouse on
unbind -T root MouseDrag1Pane

当然啦,这样只是关闭了常规模式下的选中复制功能,如果想在 copy 模式,以及开启了 vim 按键映射下的 copy-vi 模式下也关闭选中复制功能,只需要再在配置中加上以下两行就 OK 啦!

unbind -T copy-mode MouseDrag1Pane
unbind -T copy-mode-vi MouseDrag1Pane

至此,终端再也不会在摸鱼时自动复制老板消息啦~

☁️结语 && 引用

⭐️今日成就:获得「鼠标模式调教师」称号(系统认证)

[星球打卡] React Hooks

作者 Styunlen
2023年3月3日 20:16

【前端】面试题挑战 Day12 你常用的 React Hooks 有哪些? - 编程导航 (code-nav.cn)

React为函数组件提供了一些React Hooks,来让函数组件也能拥有类组件的一些特性。

常用Hooks

  • useState 在函数中使用state
  • useEffect 用于在函数组件中使用生命周期
  • useContext 不使用组件嵌套就可以订阅 ReactContext
  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
  • useMemo 返回一个memoized值。,它仅会在某个依赖项改变时才重新计算,有助于避免在每次渲染时都进行高开销的计算。
  • useCallback 返回一个memoized回调函数。把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

部分用法示例

useState

首先看他在type.d.ts中的原型

// Unlike the class component setState, the updates are not allowed to be partial
type SetStateAction<S> = S | ((prevState: S) => S);
// this technically does accept a second argument, but it's already under a deprecation warning
// and it's not even released so probably better to not define it.
type Dispatch<A> = (value: A) => void;
/**
* Returns a stateful value, and a function to update it.
*
* @version 16.8.0
* @see https://reactjs.org/docs/hooks-reference.html#usestate
*/
useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

可以推测出他的用法为

const [var, setVar] = useState(initValue);

其中var为变量名,setVar为设置var的回调函数,修改var的值都需要通过该回调函数。

initValue为var的初始值。

示例

export const DemoComponent: React.FC = () => {
    const [time, setTime] = useState([new Date().toString()]);
    return <div>{time}</div>;
};

测试输出

Fri Mar 03 2023 19:02:05 GMT+0800 (China Standard Time)

useEffect

还是看他的原型

// NOTE: callbacks are _only_ allowed to return either void, or a destructor.
type EffectCallback = () => (void | Destructor);
type DependencyList = ReadonlyArray<unknown>;
/**
 * Accepts a function that contains imperative, possibly effectful code.
 *
 * @param effect Imperative function that can return a cleanup function
 * @param deps If present, effect will only activate if the values in the list change.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#useeffect
 */
function useEffect(effect: EffectCallback, deps?: DependencyList): void;

示例

export const DemoComponent: React.FC = () => {
    const [count, setCount] = useState(0);
    useEffect(() => {
        return () => {
            console.log('unMounted');
        };
    }, []);
    useEffect(() => {
        console.log('count++');
    }, [count]);
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>Count {count}</button>
        </div>
    );
};

其中以下代码可以被用作onMount和unMount生命周期函数

useEffect(()=>{
  console.log('onMount')
  return ()=>{
        console.log('unMounte')
  }
},[])

[星球打卡] ES6 有哪些新特性?

作者 Styunlen
2023年2月22日 17:01

【前端】面试题挑战 Day3 ES6 有哪些新特性? - 编程导航 (code-nav.cn)

本文用示例来展现一些新特性的方便用法

1. 模板字符串(Template Literals)

const data = [1,2,3,4,5]
const result = `
{
  code: 200,
  data: [${data}]
}
`

模板字符串中所以类似于"${}"的字符串都将视为可执行的js语句。并且也会被语句中的实际值给替换。

像上面的示例,若没有模板化字符串这个特性,就得手动用一堆加号来拼接了。

2. 解构赋值(Destructuring)

假设示例1是后端返回的数据,用解构赋值可以快速解构出对象中的变量。

const {code,data} = Api.getData()

函数传参时也可以快速解构参数

const res = Api.getData()
const resolveData = ({data})=>{
  return data;
}
const data = resolveData(res);

3. 箭头函数(Arrow Functions)

箭头函数表面上看是function的替换,但是他有一个非常实用的特性,那就是自动绑定定义时上下文的this指针。

在一些特殊场景中,我们需要使用this指针,而对于function定义的函数,this的指向是令人迷惑的,常常要分析代码。

而箭头函数定义的函数则会自动将它的this和定义它时的上下文中的this绑定。

比如,以下代码,调用getThis时,会返回当前模块中的app变量(只是个演示)

this = app;
const getThis = ()=>this;

其实可以把它看做是以下代码的语法糖,省事儿了不少。

/// Error
function a()
{
  return this; // unknown this
}
/// Right
var a =  (function (){return this}).bind(this)
/// ES6
const a = ()=>this

4. 对象字面量增强(Object Literal Enhancements)

以下示例可以展现这个语法特性的方便之处

/// Old
let a = {
  type:"Object", 
  funcProp: function(){return "result"}
}
let DynamicProp = Math.random();
a[DynamicProp] = "test";

/// ES6
const DynamicProp = Math.random();
const a = {
  type:"Object", 
  funcProp() {return "result"},
  [DynamicProp]: "test"
}

5. Promise 对象

这个特性可以让我们用.then链替换掉以前的地狱嵌套式callback写法。

getData()
  .then(/* Do something */)
  .then(/* Do something */)
  .catch(/* Catch error */)

6. async/await

这个特性可以很方便的将同步函数变为异步函数。

不过我经常使用这个特性做其他一些事儿。比如上面提到的Promise可以使用.then链解决多层callback的问题。但是,如果我需要顺序执行一系列返回Promise的异步函数,即后一个异步函数需要前一个函数的返回值作为参数或作为参考。这时如果继续用.then链,则还是会不可避免的形成多层嵌套。

解决办法就是在try语句中使用await关键字。

try{
  const ret1 = await asyncFunc1();
  if(!ret1)
  {
    return false;
  }
  const ret2 = await asyncFunc2(ret1)
  ...
}
catch(err)
{
  console.log(err);
  return false
}

之所以要用try是为了捕获原来需要用.catch链捕获的异常

...

剩下特性有的用得不多,有的要写的话篇幅太长了,就不写了。以后有空在写笔记。

[星球打卡] JavaScript 中如何中止网络请求?

作者 Styunlen
2023年2月21日 11:43

【前端】面试题挑战 Day2 JavaScript 中如何中止网络请求? - 编程导航 (code-nav.cn)

const controller = new AbortController();
const signal = controller.signal;

fetch(url, { signal }).then(response => {
  // Handle the response
}).catch(error => {
  if (error.name === 'AbortError') {
    console.log('Request was cancelled');
  } else {
    console.log('Request failed:', error);
  }
});

// To abort the request, call the following:
controller.abort();

js给的api让我们可以随时中断一个fetch请求,那么,如果我想造轮子实现这个功能该怎么做呢?

Promise

首先能想到的是用一个Promise,来包装fetch返回的promise,再使用包装的Promise.reject方法来实现中断请求

不过函数返回时我使用了Promise.race而不直接返回abortPromise,原因是因为fetch可能返回异常,直接返回包装的promise则可能产生无法捕获的异常

以下是示例代码

const fetchAbort = (url: string) => {
  let res, abort;
  const abortPromise = new Promise<Response>((resolve, reject) => {
    res = resolve;
    abort = reject.bind(this, `Fetch "${url}" has been aborted`);
  });
  return {
    response: Promise.race([fetch(url).then(ret => res(ret)), abortPromise]),
    abort
  };
};
const { response, abort } = fetchAbort("./serverConfig.json");
abort();
// abort后,response的promise链将catch到fetch abort消息
// 而不会返回fetch返回的消息
response
  .then(ret => ret.text())
  .then(txt => console.log(txt))
  .catch(err => {
    console.log(err);
  });

输出:

Fetch "./serverConfig.json" has been aborted

代码缺陷也显而易见,这段代码虽然能拦截fetch的返回,但是fetch占用的网络资源并不会被释放。

也就是说,如果api请求还在pending状态,控制台network列表仍然能看到fetch还在请求状态,占用着io资源。

所以,如果想要abort的同时,释放fetch占用的资源,单靠Promise是不够的。

XMLHttpRequest.abort()

可以使用XML代替fetch,反正造轮子就是为了绕开fetch,封装就完了

const fetchAbort = (url: string) => {
  let res, abort, xhr;
  const xhrPromise = new Promise((resolve, reject) => {
    xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (this.status == 200) {
          return resolve(this.response);
        }
        return reject({
          xhr: this,
          msg: "Error",
          status: this.status,
          statusText: this.statusText
        });
      }
    };
    xhr.open("Get", url);
    xhr.send();
  });
  const abortPromise = new Promise((resolve, reject) => {
    res = resolve;
    abort = () => {
      xhr.abort();
      return reject(`Fetch "${url}" has been aborted`);
    };
  });
  return {
    response: Promise.race([xhrPromise.then(ret => res(ret)), abortPromise]),
    abort
  };
};
const { response, abort } = fetchAbort("./serverConfig.json");
abort();
response
  .then(ret => console.log(ret))
  .catch(err => {
    console.log(err);
  });

输出:

Fetch "./serverConfig.json" has been aborted

fetch的原生abort代码对比,可以看到请求大小为0B,说明请求虽然发出去了,但是浏览器直接断开了连接,并没有选择接收。于是实现了一个简单的fetchAbort

不过封装程度还不够,剩下的轮子就不造了,了解原理才是本次造轮子的目的。

工程化前端神器—— husky + eslint + prettier - 提升前端代码质量指南

作者 Styunlen
2023年1月20日 11:25

开发前端项目时,经常会使用一些前端工具来帮助规范代码,eslintprettier就是两个非常好用的工具,两个一起用时可以让 eslint 负责检查代码,prettier负责格式化代码。 其次,git commit 时,也希望 commit message 能好看些,以及规范些,更希望能在 commit 之前通过 eslint检查并用 prettier格式化代码,来防止 commit的代码不合规范,导致后期维护困难。那么如何初始化一个这样的项目呢?阅读本篇文章,将带你实战了解整个过程。

项目初始化

首先新建一个文件夹 Test(改成项目名称),用作项目根目录。然后在VSCode中打开这个文件夹。

在Vscode的终端中输入,将当前目录初始化为一个git仓库

小芝士点:

  1. git checkout -b maingit switch -c main都可以切换到main分支
  2. 主分支可以是main也可以是master,不过为了工程化项目,后期可能还会使用git flow工具来规范git仓库提交,所以推荐使用main作为主分支。
git init
git checkout -b main

接下来我们输入pnpm init,来初始化Node.js项目,当然也可以使用其他包管理器,比如npmyarnpnpm比较省空间且高效,所以我用他。具体可以上知乎搜索三者的区别

pnpm yarn npm - 搜索结果 - 知乎 (zhihu.com)

pnpm init

然后对package.json的内容稍作修改

接着我们要开始安装huskyeslintprettier这三个工具了。

工具安装

husky

首先是hsuky的简单介绍,以及为什么推荐用他的小理由

这是官方的readme

Modern native Git hooks made easy

Husky improves your commits and more 🐶 woof!

平时使用的git工具为我们提供了一系列hook接口,方便我们拦截并更改git提交时的一些操作,但修改git hook是一件麻烦的事儿,所以出现了这个git hooks的管理工具

废话不说,根据readme可知我们只需三句指令就能安装他

pnpm install husky -D
pnpm pkg set scripts.prepare="husky install"
pnpm run prepare

这样我们就安装好了,我们等eslintprettier都装好时在回过来一起配置他。

eslint & prettier

这两货放一起时会冲突,每次都需要单独配置,不过好在逛github的时候发现有人已经写了个配置脚本,能一键初始化配置,真方便。

leggsimon/create-prettier-eslint: Generates eslint and prettier config files for you (github.com)

我们输入pnpx create-prettier-eslint跟着命令行回答个人风格快速配置一个eslintprettier同时集成的项目

报错了?没关系,毕竟是个19年的项目,Node底层有api变动也是正常的,至少配置文件已经帮我们自动生成了,观察报错发现脚本想要自动安装以下依赖。

eslint
prettier
eslint-config-prettier
eslint-plugin-prettier
eslint-plugin-react

我们输入指令手动安装依赖。

pnpm install -D eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react

简简单单,我们又装好了一个工具

工具配置

husky

我们虽然安装了husky,但是当我们在git commit时,并未激活任何git hook,因为我们还没配置husky

通过观察发现,husky官方库的git提交都很规范,都是像chore: message这样的type: commit message式的提交方式,能不能让huskygit提交commit时自动阻拦下不是这样提交方式的commit,从而阻止不规范的提交呢?

commitlint

答案是肯定的,不过这之前我们还需要安装一个能够检查git commit的工具,他就是类似于eslintcommitlint

咱预览一下安装之后的效果

conventional-changelog/commitlint: 📓 Lint commit messages (github.com)

这也太方便了吧。根据readme的提示,几行命令安装他。

# Install commitlint cli and conventional config
pnpm install --save-dev @commitlint/config-conventional @commitlint/cli
# For Windows:
pnpm install --save-dev @commitlint/config-conventional @commitlint/cli

# Configure commitlint to use conventional config
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

接着通过huskycommitlint加到commit之前的检查脚本里,拦截不规范的commit消息

pnpm husky add .husky/commit-msg  'pnpm commitlint --edit ${1}'

咱们随便提个git commit看看效果

可以看到,即使我们在外部控制台,husky依然是起作用的,给了commit消息不合规的报错。

不过,我又想偷懒了,有没有另一个小工具,能够通过命令行选项的方式,帮我快速创建commit,而不用每次手输入呢?

嘿嘿,他就是Commitizen

Commitizen

这个工具要全局安装,全局安装后,就能在任意环境里(包括没有安装commitlint的项目)通过选项帮助快速生成格式化的commit message,真是懒人福利啊。

所以啊,以后再需要这些能偷懒又还没有人写的轮子,就自己造一个然后开源,没准不久后就要变成开源界小有名气的红人啦。

输入pnpm install -g commitizen全局安装一下

pnpm install -g commitizen
commitizen init cz-conventional-changelog --save-dev --save-exact

这里同时安装了cz-conventional-changelog适配器,方便我们根据自己的代码风格自定义Commitizen生成的commit

具体如何修改可以参考cz-conventional-changelog仓库的readme

commitizen/cz-conventional-changelog: A commitizen adapter for the angular preset of https://github.com/conventional-changelog/conventional-changelog

输入czgit czgit-cz 三个指令中的任意之一即可使用一下试试

可以看到已经有选项了

eslint & prettier

其实此时的eslint与prettier的基础配置已经能够正常使用了,但仍然可以根据自己的代码风格对prettier进行配置

比如,我会将trailingComma: 'all'改为trailingComma: 'none',这样对于数组或者对象,prettier不会在最后一个元素后自动添加逗号。

以及添加endOfLine: "auto",解决windows换行问题

但是前面提到了我还希望能在使用git提交代码时,能使用eslintprettier,所以我们还需要借助一下husky,集成这两个工具。

lint-staged & pretty-quick

当然啦,直接使用eslint或者prettier其实会有一些小问题,我们每次修改代码,提交commit,其实只希望eslint或者prettier对增量代码执行检查和格式化,但他俩的默认行为确实对所有代码执行这些操作,那么对于拥有祖传代码的工程项目将会是一个灾难,毕竟大项目多多少少可能会有一些不能动的屎山代码,所以这里还需要安装两个小工具🚫💩lint-stagedpretty-quick ,这两个小工具还很好的优化了eslint或者prettier的输出信息,以及可以快速配置对不同文件执行的操作。是选择eslint检查后prettier格式化,还是直接prettier格式化,都可以通过lint-stage进行简单配置。

安装

okonet/lint-staged: 🚫💩 — Run linters on git staged files (github.com)

看到lint-staged官方仓库标题的小细节了嘛,拒绝💩山,🚫💩哈哈哈

还是简单的两行指令

pnpm install --save-dev pretty-quick
pnpm install --save-dev lint-staged # requires further setup

再通过以下指令将两个工具添加进项目的执行脚本里,方便后面执行

pnpm pkg set scripts.lint="pnpm lint-staged"
pnpm pkg set scripts.pretty="pnpm pretty-quick --staged"

同时还要添加lint-staged的设置,来选择针对不同文件执行的操作(只格式化,还是两个都要)

打开package.json,添加以下字段,这是所有文件都要eslintprettier的意思

"lint-staged": {
    "*": ["eslint --fix","prettier --write"]
}

其他配置可以查看lint-stagedreadme

okonet/lint-staged: 🚫💩 — Run linters on git staged files (github.com)

测试

在这之后,输入pnpm lintpnpm pretty来看看效果

可以看到我们留的一个未使用的变量正常被eslint揪出来并且报错了。但是为什么还有几个报错?还有来自package.json文件的,能忽略这些文件嘛?

其实eslint也可以像git一样,选择忽略几个文件,那就是创建.eslintignore,然后在里面填上忽略的文件就行了

package.json
pnpm-lock.yaml
.eslintrc.js
.prettierrc.js
commitlint.config.js

后续项目有什么新增的需要忽略的文件,往里面加就行了。

pretty-quick也正常工作了

接下来就是将他们通过husky,放置在commit的检查中了

输入以下指令通过husky,将这两个小工具添加进执行commit之前的操作脚本中

pnpm husky add .husky/pre-commit "pnpm lint"
pnpm husky add .husky/pre-commit "pnpm pretty"

完成后我们提交一个commit试试,可以看到我们带着错误的代码提交时成功被拦截了

不过注释掉我们预留的错误代码后,还是报错,为啥呢?那是因为我们刚才配置的lint-staged默认会对所有文件执行prettier,经过实践,这样显然是不行的,所以我们要回归package.json修改lint-staged的策略,将通配符*改为*.js来让lint-staged只对js文件执行lint

"lint-staged": {
  "*.js": [
    "eslint --fix",
    "prettier --write"
  ]
}

再提交时,也如预期那样提交成功了,至此,这个项目就可以正常编写代码,并且在commit的时候自动审查不规范代码并拦截提示了。

结语

这个项目模板我已经同步更新在我的Github上了,传送门:Styunlen/husky-eslint-prettier-template: A project template with husky,eslint and prettier installed (github.com)

可以输入以下指令快速复制

git clone https://github.com/Styunlen/husky-eslint-prettier-template.git

依赖项目

❌
❌