上來就對標 20k Star 的開源項目,是自不量力還是後起之秀?

先來一段緊箍咒:nvm、fvm、gvm、sdkman、fnm、n、g、rvm、jenv、phpbrew、rustup、swiftenv、pyenv、rbenv...

這些都是用來解決編程語言多版本管理的工具,如果你是個程序員肯定認識或是用過幾個,但是剛接觸編程的小白,就會有些撓頭了。

啥是編程語言版本管理工具?它們有什麼用呢?

舉個例子,用 Java 的開發者可能會遇見的問題,公司的項目是萬年不變 JDK 8,但個人項目用的是最新的 JDK 21。這種情況下,在一臺電腦上開發公司和個人項目的時候,就需要切換一下當前開發環境對應的 JDK 版本,否則項目跑不起來。編程語言版本管理工具就是用來切換/管理編程語言不同版本的工具,比如 Java 語言對應的工具是 jenv

每一種編程語言都有一個對應的版本管理工具,對於多語言開發者來說就需要安裝、配置、學習各種版本管理工具,記憶不同工具的使用命令,這和緊箍咒無異。那咋辦啊?

莫慌,今天 HelloGitHub 帶來的是一款跨平臺版本、支持多語言的版本管理工具——vfox,讓你無憂應對多編程語言、不同版本的開發環境。該項目由國人(99 年的小夥)開發,更貼合國內開發者的使用習慣。

GitHub 地址:https://github.com/version-fox/vfox

接下來,讓我們一起走近 vfox 瞭解它的功能、上手使用、技術原理和強大的插件系統吧!

一、介紹

vfox 是一個類 nvm、fvm、sdkman、asdf 的版本管理工具,具有跨平臺通用易拓展的特性:

  • 簡單:安裝簡單,一套命令管理所有語言
  • 跨平臺:支持 Windows、Linux、macOS
  • 人性化:換項目時自動切換到對應編程語言、支持自動補全
  • 擴展性:容易上手的插件系統,添加冷門的編程語言
  • 作用域:支持 Global、Project、Session 三種作用域

質疑聲:同類型的項目挺多的啊,不能一個國人開發、開源就來求 Star 吧?

下面,我們就來和在 GitHub 上有 20k Star 的同類型工具 asdf PK 一下,看看 vfox 是不是重複造輪子,到底能不能打!

二、對比 asdf

這裏主要從操作系統兼容性、性能和插件換源三個方面進行對比。

2.1 兼容性

兼容性 Windows Linux macOS
asdf
vfox

首先,asdf 是用 shell 腳本實現的工具,所以並不支持原生 Windows 環境。而 vfox 是用 Go + Lua 實現的,因此天生支持 Windows 和其他操作系統。

2.2 性能

上圖是對兩個工具最核心的切換版本功能進行基準測試的結果,很容易就能得出結論:vfox 比 asdf 快 5 倍

速度 平均 最快 最慢
asdf 158.7 ms 154 ms 168.4 ms
vfox 28.1ms 27.1 ms 32.3 ms

技術解析:asdf 執行切換版本的速度之所以較慢,主要是由於其墊片機制。簡單來說,當你嘗試運行如 node 這樣的命令時,asdf 會首先查找對應的墊片,然後根據 .tool-versions 文件或全局設置來確定使用哪個版本的 node 。這個查找和確定版本的過程會消耗一定的時間,從而影響了命令的執行速度。

相比之下,vfox 則採用了直接操作環境變量的方式來管理版本,它會直接設置和切換環境變量,從而避免了查找和確定版本的過程。因此,在執行速度上要比使用墊片機制的 asdf 快得多。

雖然 asdf 很強,但是它對 Windows 原生無能爲力。雖然 vfox 很新,但在性能和跨平臺方面做得更好

2.3 插件換源

大多數時候,我們會被網絡問題而困擾,所以切換下載源的操作是必不可少的。

下面以切換 Node.js 源爲例,對比 asdf 和 vfox 在換源時的區別。

asdf 是通過 asdf-vm/asdf-nodejs 插件實現了對於 Node.js 的支持,但該插件是需要手動預定義一個環境變量來修改下載源,多語言換源還需要設置多個不同的環境變量。

  • 優點:可以靈活切換任何鏡像源
  • 缺點:需要手動設置,操作不友好

vfox 選擇了另一種方法,即一個鏡像源對應一個插件。

$ vfox add nodejs/nodejs # 使用官方下載源
$ vfox add nodejs/npmmirror # 使用 npmmirror 鏡像

$ vfox add python/python # 官方下載源
$ vfox add python/npmmirror

雖然這樣會使倉庫的插件變多,但使用起來降低了負擔,也沒有亂七八糟的環境變量需要配置,對用戶非常友好!

三、上手

說了這麼多,還沒上手玩一下簡直忍不了。

3.1. 安裝

Windows 用戶只需要下載安裝器進行安裝即可,Linux 用戶可以使用 APT 或 YUM 來快速安裝,macOS 用戶可以使用 Homebrew 安裝。更詳細的安裝方式可查看文檔

$ brew tap version-fox/tap
$ brew install vfox

安裝完成之後,需要將 vfox 掛載到你的 shell 中,從下麪條目中選擇一條適合你 shell 的。

echo 'eval "$(vfox activate bash)"' >> ~/.bashrc
echo 'eval "$(vfox activate zsh)"' >> ~/.zshrc
echo 'vfox activate fish | source' >> ~/.config/fish/config.fish

# 對於 Powershell 用戶,將下面行添加到你的 $PROFILE 文件中
Invoke-Expression "$(vfox activate pwsh)"

3.2 使用

安裝好了,但你還做不了任何事情,因爲 vfox 是使用插件作爲擴展,按需安裝。

不知道應該添加哪些插件,可以用 vfox available 命令查看所有可用插件

所以你還需要安裝插件,以 Node.js 爲例,爲了獲得更好的體驗,我們添加 npmmirror 鏡像源插件:vfox add nodejs/npmmirror

在插件成功安裝之後,你就可以玩起來了!

  • 安裝指定版本:vfox install nodejs@<version>
  • 安裝最新版本:vfox install nodejs@latest
  • 切換版本:vfox use nodejs[@<version>]

文字表達遠不如圖片來的更直觀,我們直接上效果圖。

四、技術原理

vfox 支持 Global、Session、Project 三種作用域,這三種作用域能夠滿足我們日常開發所需的場景。

作用域 命令 說明
Global vfox use -g <sdk-name> 全局範圍有效
Session vfox use -s <sdk-name> 當前 shell 會話有效
Project vfox use -p <sdk-name> 當前項目下有效

那麼你對它們的實現原理感興趣嗎?咱們廢話不多說,直接看原理圖!

vfox 是基於 shell 的 hook 機制實現的,hook 機制簡單來說就是每當我們執行完命令之後,shell 都會調用一下你配置的鉤子函數(hook),即 vfox env <shell-name> 命令,我們後面解釋這個命令是幹什麼的。

說回到作用域上來,vofox 是通過 .tool-versions 文件來記錄每個 SDK 對應的版本號信息。對於三種作用域,會分別在不同的地方創建 .tool-versions 文件,用於記錄作用域內所需要的 SDK 版本信息。

  • Global -> $HOME/.version-fox/.tool-versions
  • Project -> 當前項目目錄
  • Session -> $HOME/.version-fox/tmp/<shell-pid>/.tool-versions

代碼如下:

func newSdkManagerWithSource(sources ...RecordSource) *Manager {
    meta, err := newPathMeta()
    if err != nil {
       panic("Init path meta error")
    }
    var paths []string
    for _, source := range sources {
        // 根據不同的作用域選擇性加載不同位置的.tool-versions文件
       switch source {
       case GlobalRecordSource:
          paths = append(paths, meta.ConfigPath)
       case ProjectRecordSource:
           // 當前目錄
          curDir, err := os.Getwd()
          if err != nil {
             panic("Get current dir error")
          }
          paths = append(paths, curDir)
       case SessionRecordSource:
           // Shell會話臨時目錄
          paths = append(paths, meta.CurTmpPath)
       }
    }
    // env.Record是用來專門操作.tool-versions文件的, 增刪改查
    var record env.Record
    if len(paths) == 0 {
       record = env.EmptyRecord
    } else if len(paths) == 1 {
       r, err := env.NewRecord(paths[0])
       if err != nil {
          panic(err)
       }
       record = r
    } else {
       r, err := env.NewRecord(paths[0], paths[1:]...)
       if err != nil {
          panic(err)
       }
       record = r
    }
    // SdkManager是用來專門管理Sdk的組件, 到這裏Manager就可以通過Record來獲取和修改Sdk版本信息咯
    return newSdkManager(record, meta)
}

上面提到,最核心的其實是 hook 機制調用的 vfox env <shell-name> 命令,那它到底幹了件什麼事情呢?

func envCmd(ctx *cli.Context) error {
    ...
        // 拿到對應shell的組件
       s := shell.NewShell(shellName)
       if s == nil {
          return fmt.Errorf("unknow target shell %s", shellName)
       }
       // 上面提到的加載.tool-versions信息到Manager中
       manager := internal.NewSdkManagerWithSource(internal.SessionRecordSource, internal.ProjectRecordSource)
       defer manager.Close()
       // 獲取需要配置的環境變量信息
       envKeys, err := manager.EnvKeys()
       if err != nil {
          return err
       }
       // 將環境變量信息, 翻譯成符合對應shell的命令
       exportStr := s.Export(envKeys)
       fmt.Println(exportStr)
       return nil
    }
}

func (m *Manager) EnvKeys() (env.Envs, error) {
    shellEnvs := make(env.Envs)
    var paths []string
    // 這裏就是前面說的, Record包含了所有的版本信息, 只需要取出來即可
    for k, v := range m.Record.Export() {
       if lookupSdk, err := m.LookupSdk(k); err == nil {
          if keys, err := lookupSdk.EnvKeys(Version(v)); err == nil {
             for key, value := range keys {
                if key == "PATH" {
                   paths = append(paths, *value)
                } else {
                   shellEnvs[key] = value
                }
             }
          }
       }
    }
   ...
    return shellEnvs, nil
}

沒看懂代碼沒關係,用一句話概括這段代碼的功能:.tool-versions 記錄的 SDK 版本信息,翻譯成具體 shell 可執行的命令,其實核心技術就這麼樸實無華。

五、插件系統

插件系統是 vfox 的核心,它賦予 vfox 無限的可能性,不僅僅侷限於單一的 SDK。通過插件系統,vfox 能夠靈活地適應任何 SDK 的需求,無論是現有的還是未來可能出現的。

更重要的是,插件系統使用 Lua 作爲插件的開發語言,內置了一些常用模塊,如 httpjsonhtmlfile 等,這使得插件系統不僅功能強大,而且易於開發和自定義。用戶可以根據自己的需求,輕鬆編寫和定製自己的腳本,從而實現更多的功能。

口說無憑,我們直接寫一個簡單的插件來體驗一下,以寫一個 Windows 環境下可用的 Python 插件爲例。

5.1 插件模板結構

在開工之前,我們首先需要了解一下插件結構是什麼樣子,以及都提供了哪些鉤子函數供我們實現。

--- 內置全局變量: 操作系統和架構類型
OS_TYPE = ""
ARCH_TYPE = ""
--- 描述當前插件的基本信息, 插件名稱、版本、最低運行時版本等信息
PLUGIN = {
    name = "xxx",
    author = "xxx",
    version = "0.0.1",
    description = "xxx",
    updateUrl = "https://localhost/xxx.lua",
    minRuntimeVersion = "0.2.3",
}
--- 1.預安裝鉤子函數。vfox 會根據提供的元信息, 幫你提前下載好所需的文件(如果是壓縮包,會幫你解壓)放到指定目錄。
function PLUGIN:PreInstall(ctx)
    return {
      version = "0.1.1",
      sha256 = "xxx", --- 可選
      sha1 = "xxx", --- 可選
      url = "文件地址"
    }
end
--- 2.後置鉤子函數。這裏主要是做一些額外操作, 例如編譯源碼。
function PLUGIN:PostInstall(ctx)
end
--- 3.可用鉤子函數。 告訴 vfox 當前插件都有哪些可用版本。
function PLUGIN:Available(ctx) 
end
--- 4.環境信息鉤子函數。 告訴 vfox 當前SDK所需要配置的環境變量有哪些。
function PLUGIN:EnvKeys(ctx)
end

總共就 4 個鉤子函數,是不是非常簡單。

5.2 Python 插件實現

OK,萬事俱備那我們正式開始實現 Python 插件咯~

--- vfox 提供的庫
local http = require("http") --- 發起 http 請求
local html = require("html") --- 解析 html
OS_TYPE = ""
ARCH_TYPE = ""

--- python 下載源地址信息
local PYTHON_URL = "https://www.python.org/ftp/python/"
local DOWNLOAD_SOURCE = {
    --- ...
    EXE = "https://www.python.org/ftp/python/%s/python-%s%s.exe",
    SOURCE = "https://www.python.org/ftp/python/%s/Python-%s.tar.xz"
}

PLUGIN = {
    name = "python",
    author = "aooohan",
    version = "0.0.1",
    minRuntimeVersion = "0.2.3", 
}

function PLUGIN:PreInstall(ctx)
    --- 拿到用戶輸入版本號, 解析成具體版本號
    local version = ctx.version
    if version == "latest" then
        version = self:Available({})[1].version
    end
    if OS_TYPE == "windows" then
        local url, filename = checkAvailableReleaseForWindows(version)
        return {
            version = version,
            url = url,
            note = filename
        }
    else
        --- 非 Windows 環境實現, 略
    end
end

function checkAvailableReleaseForWindows(version)
    --- 處理架構類型, 同一架構的不同名稱
    local archType = ARCH_TYPE
    if ARCH_TYPE == "386" then
        archType = ""
    else
        archType = "-" .. archType
    end
    --- 檢查是否存在 exe 安裝器, 當然 Python 還提供了其他安裝器, 例如 msi、web-installer 等
    local url = DOWNLOAD_SOURCE.EXE:format(version, version, archType)
    local resp, err = http.head({
        url = url
    })
    if err ~= nil or resp.status_code ~= 200 then
        error("No available installer found for current version")
    end
    return url, "python-" .. version .. archType .. ".exe"
end


--- vfox 會在 PreInstall 執行完之後, 執行當前鉤子函數.
function PLUGIN:PostInstall(ctx)
    if OS_TYPE == "windows" then
        return windowsCompile(ctx)
    else
        --- 略
    end
end

function windowsCompile(ctx)
    local sdkInfo = ctx.sdkInfo['python']
    --- vfox 分配的安裝路徑
    local path = sdkInfo.path
    local filename = sdkInfo.note
    --- exe 安裝器路徑
    local qInstallFile = path .. "\\" .. filename
    local qInstallPath = path
    --- 執行安裝器
    local exitCode = os.execute(qInstallFile .. ' /quiet InstallAllUsers=0 PrependPath=0 TargetDir=' .. qInstallPath)
    if exitCode ~= 0 then
        error("error installing python")
    end
    --- 清理安裝器
    os.remove(qInstallFile)
end

--- 告訴 vfox 可用版本
function PLUGIN:Available(ctx)
    return parseVersion()
end

function parseVersion()
    --- 這裏就是解析對應的 html 頁面, 通過正則匹配具體版本號了
    local resp, err = http.get({
        url = PYTHON_URL
    })
    if err ~= nil or resp.status_code ~= 200 then
        error("paring release info failed." .. err)
    end
    local result = {}
    --- 解析 html 略 
    return result
end

--- 配置環境變量, 主要是 PATH, 但是注意 Windows 和 Unix-like 路徑不一致, 所以要區分
function PLUGIN:EnvKeys(ctx)
    local mainPath = ctx.path
    if OS_TYPE == "windows" then
        return {
            {
                key = "PATH",
                value = mainPath
            }
        }
    else
        return {
            {
                key = "PATH",
                value = mainPath .. "/bin"
            }
        }
    end
end

至此,我們就完成了一個 Windows 環境下可用的 Python 插件啦~🎉

當然,這只是爲了方便演示如何自己實現插件,vfox 目前已經提供了完善的 Python 插件,可以通過 vfox add python/npmmirror 命令直接安裝使用哦。

vfox 目前已支持 12 種插件,還在努力豐富中💪💪💪

  • Python ✅ -> python/npmmirror
  • Nodejs ✅ -> nodejs/npmmirror
  • Java ✅ -> java/adoptium-jdk
  • Golang ✅ -> golang/golang
  • Dart ✅ -> dart/dart
  • Flutter ✅ -> flutter/flutter-cn
  • .Net ✅ -> dotnet/dotnet
  • Deno ✅ -> deno/deno
  • Zig ✅ -> zig/zig
  • Maven ✅ -> maven/maven
  • Graalvm ✅ -> java/graalvm
  • Kotlin ✅ -> kotlin/kotlin
  • Ruby ⌛️
  • PHP ⌛️

六、結束

我的初衷是不管什麼語言,只要是需要版本管理,只需要一個工具就能簡單高效的完成。所以我創建了 vfox,它是一款專注於多語言多版本管理的生態工具,目標只有一個:讓所有的編程語言版本管理變得簡單易用。無論你是 JavaScript、Java 還是 Python 的開發者,vfox 都能爲你提供一站式的解決方案。

我們的願景是創建一個適合國人使用的、簡單易用的多語言多版本管理工具。我們相信,只有真正理解開發者的需求,才能創造出真正有價值的工具。vfox 就是這樣的工具,它是爲了解決開發者在日常工作中遇到的版本管理問題而生。

GitHub 地址:https://github.com/version-fox/vfox

最後,感謝 HelloGitHub 提供的機會,讓我能向更多人介紹 vfox。作爲一個開源項目的創作者,我深感開源的力量。它不僅僅是代碼的共享,更是知識和經驗的共享。希望 vfox 能成爲我們溝通的橋樑,歡迎各種形式的反饋和建議,讓我們一起變強!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章