<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Viper是適用於Go應用程式的完整設定解決方案。它被設計用於在應用程式中工作,並且可以處理所有型別的設定需求和格式。
鑑於viper
庫本身的README已經寫得十分詳細,這裡就將其翻譯成中文,並在最後附上兩個專案中使用viper
的範例程式碼以供參考。
go get github.com/spf13/viper
Viper是適用於Go應用程式(包括Twelve-Factor App
)的完整設定解決方案。它被設計用於在應用程式中工作,並且可以處理所有型別的設定需求和格式。它支援以下特性:
JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式的組態檔讀取設定資訊在構建現代應用程式時,你無需擔心組態檔格式;你想要專注於構建出色的軟體。Viper的出現就是為了在這方面幫助你的。
Viper能夠為你執行下列操作:
JSON
、TOML
、YAML
、HCL
、INI
、envfile
和Java properties
格式的組態檔。Viper會按照下面的優先順序。每個專案的優先順序都高於它下面的專案:
Set
設定值重要: 目前Viper設定的鍵(Key)是大小寫不敏感的。目前正在討論是否將這一選項設為可選。
一個好的設定系統應該支援預設值。鍵不需要預設值,但如果沒有通過組態檔、環境變數、遠端設定或命令列標誌(flag)設定鍵,則預設值非常有用。
例如:
viper.SetDefault("ContentDir", "content") viper.SetDefault("LayoutDir", "layouts") viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
Viper需要最少知道在哪裡查詢組態檔的設定。Viper支援JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式的組態檔。Viper可以搜尋多個路徑,但目前單個Viper範例只支援單個組態檔。Viper不預設任何設定搜尋路徑,將預設決策留給應用程式。
下面是一個如何使用Viper搜尋和讀取組態檔的範例。不需要任何特定的路徑,但是至少應該提供一個組態檔預期出現的路徑。
viper.SetConfigFile("./config.yaml") // 指定組態檔路徑 viper.SetConfigName("config") // 組態檔名稱(無擴充套件名) viper.SetConfigType("yaml") // 如果組態檔的名稱中沒有擴充套件名,則需要設定此項 viper.AddConfigPath("/etc/appname/") // 查詢組態檔所在的路徑 viper.AddConfigPath("$HOME/.appname") // 多次呼叫以新增多個搜尋路徑 viper.AddConfigPath(".") // 還可以在工作目錄中查詢設定 err := viper.ReadInConfig() // 查詢並讀取組態檔 if err != nil { // 處理讀取組態檔的錯誤 panic(fmt.Errorf("Fatal error config file: %s n", err)) }
在載入組態檔出錯時,你可以像下面這樣處理找不到組態檔的特定情況:
if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { // 組態檔未找到錯誤;如果需要可以忽略 } else { // 組態檔被找到,但產生了另外的錯誤 } } // 組態檔找到併成功解析
注意[自1.6起]: 你也可以有不帶擴充套件名的檔案,並以程式設計方式指定其格式。對於位於使用者$HOME
目錄中的組態檔沒有任何擴充套件名,如.bashrc
。
這裡補充兩個問題供讀者解答並自行驗證
當你使用如下方式讀取設定時,viper會從./conf
目錄下查詢任何以config
為檔名的組態檔,如果同時存在./conf/config.json
和./conf/config.yaml
兩個組態檔的話,viper
會從哪個組態檔載入設定呢?
viper.SetConfigName("config") viper.AddConfigPath("./conf")
在上面兩個語句下搭配使用viper.SetConfigType("yaml")
指定組態檔型別可以實現預期的效果嗎?
從組態檔中讀取組態檔是有用的,但是有時你想要儲存在執行時所做的所有修改。為此,可以使用下面一組命令,每個命令都有自己的用途:
viper
設定寫入預定義的路徑並覆蓋(如果存在的話)。如果沒有預定義的路徑,則報錯。viper
設定寫入預定義的路徑。如果沒有預定義的路徑,則報錯。如果存在,將不會覆蓋當前的組態檔。viper
設定寫入給定的檔案路徑。將覆蓋給定的檔案(如果它存在的話)。viper
設定寫入給定的檔案路徑。不會覆蓋給定的檔案(如果它存在的話)。根據經驗,標記為safe
的所有方法都不會覆蓋任何檔案,而是直接建立(如果不存在),而預設行為是建立或截斷。
一個小范例:
viper.WriteConfig() // 將當前設定寫入「viper.AddConfigPath()」和「viper.SetConfigName」設定的預定義路徑 viper.SafeWriteConfig() viper.WriteConfigAs("/path/to/my/.config") viper.SafeWriteConfigAs("/path/to/my/.config") // 因為該組態檔寫入過,所以會報錯 viper.SafeWriteConfigAs("/path/to/my/.other_config")
Viper支援在執行時實時讀取組態檔的功能。
需要重新啟動伺服器以使設定生效的日子已經一去不復返了,viper驅動的應用程式可以在執行時讀取組態檔的更新,而不會錯過任何訊息。
只需告訴viper範例watchConfig。可選地,你可以為Viper提供一個回撥函數,以便在每次發生更改時執行。
確保在呼叫WatchConfig()
之前新增了所有的設定路徑。
viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { // 組態檔發生變更之後會呼叫的回撥函數 fmt.Println("Config file changed:", e.Name) })
Viper預先定義了許多設定源,如檔案、環境變數、標誌和遠端K/V儲存,但你不受其約束。你還可以實現自己所需的設定源並將其提供給viper。
viper.SetConfigType("yaml") // 或者 viper.SetConfigType("YAML") // 任何需要將此設定新增到程式中的方法。 var yamlExample = []byte(` Hacker: true name: steve hobbies: - skateboarding - snowboarding - go clothing: jacket: leather trousers: denim age: 35 eyes : brown beard: true `) viper.ReadConfig(bytes.NewBuffer(yamlExample)) viper.Get("name") // 這裡會得到 "steve"
這些可能來自命令列標誌,也可能來自你自己的應用程式邏輯。
viper.Set("Verbose", true) viper.Set("LogFile", LogFile)
別名允許多個鍵參照單個值
viper.RegisterAlias("loud", "Verbose") // 註冊別名(此處loud和Verbose建立了別名) viper.Set("verbose", true) // 結果與下一行相同 viper.Set("loud", true) // 結果與前一行相同 viper.GetBool("loud") // true viper.GetBool("verbose") // true
Viper完全支援環境變數。這使Twelve-Factor App
開箱即用。有五種方法可以幫助與ENV共同作業:
AutomaticEnv()
BindEnv(string...) : error
SetEnvPrefix(string)
SetEnvKeyReplacer(string...) *strings.Replacer
AllowEmptyEnv(bool)
使用ENV變數時,務必要意識到Viper將ENV變數視為區分大小寫。
Viper提供了一種機制來確保ENV變數是惟一的。通過使用SetEnvPrefix
,你可以告訴Viper在讀取環境變數時使用字首。BindEnv
和AutomaticEnv
都將使用這個字首。
BindEnv
使用一個或兩個引數。第一個引數是鍵名稱,第二個是環境變數的名稱。環境變數的名稱區分大小寫。如果沒有提供ENV變數名,那麼Viper將自動假設ENV變數與以下格式匹配:字首+ “_” +鍵名全部大寫。當你顯式提供ENV變數名(第二個引數)時,它 不會 自動新增字首。例如,如果第二個引數是“id”,Viper將查詢環境變數“ID”。
在使用ENV變數時,需要注意的一件重要事情是,每次存取該值時都將讀取它。Viper在呼叫BindEnv
時不固定該值。
AutomaticEnv
是一個強大的助手,尤其是與SetEnvPrefix
結合使用時。呼叫時,Viper會在發出viper.Get
請求時隨時檢查環境變數。它將應用以下規則。它將檢查環境變數的名稱是否與鍵匹配(如果設定了EnvPrefix
)。
SetEnvKeyReplacer
允許你使用strings.Replacer
物件在一定程度上重寫 Env 鍵。如果你希望在Get()
呼叫中使用-
或者其他什麼符號,但是環境變數裡使用_
分隔符,那麼這個功能是非常有用的。可以在viper_test.go
中找到它的使用範例。
或者,你可以使用帶有NewWithOptions
工廠函數的EnvKeyReplacer
。與SetEnvKeyReplacer
不同,它接受StringReplacer
介面,允許你編寫自定義字串替換邏輯。
預設情況下,空環境變數被認為是未設定的,並將返回到下一個設定源。若要將空環境變數視為已設定,請使用AllowEmptyEnv
方法。
SetEnvPrefix("spf") // 將自動轉為大寫 BindEnv("id") os.Setenv("SPF_ID", "13") // 通常是在應用程式之外完成的 id := Get("id") // 13
Viper 具有繫結到標誌的能力。具體來說,Viper支援Cobra庫中使用的Pflag
。
與BindEnv
類似,該值不是在呼叫繫結方法時設定的,而是在存取該方法時設定的。這意味著你可以根據需要儘早進行繫結,即使在init()
函數中也是如此。
對於單個標誌,BindPFlag()
方法提供此功能。
例如:
serverCmd.Flags().Int("port", 1138, "Port to run Application server on") viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
你還可以繫結一組現有的pflags (pflag.FlagSet):
舉個例子:
pflag.Int("flagname", 1234, "help message for flagname") pflag.Parse() viper.BindPFlags(pflag.CommandLine) i := viper.GetInt("flagname") // 從viper而不是從pflag檢索值
在 Viper 中使用 pflag 並不阻礙其他包中使用標準庫中的 flag 包。pflag 包可以通過匯入這些 flags 來處理flag包定義的flags。這是通過呼叫pflag包提供的便利函數AddGoFlagSet()
來實現的。
例如:
package main import ( "flag" "github.com/spf13/pflag" ) func main() { // 使用標準庫 "flag" 包 flag.Int("flagname", 1234, "help message for flagname") pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.Parse() viper.BindPFlags(pflag.CommandLine) i := viper.GetInt("flagname") // 從 viper 檢索值 ... }
如果你不使用Pflag
,Viper 提供了兩個Go介面來繫結其他 flag 系統。
FlagValue
表示單個flag。這是一個關於如何實現這個介面的非常簡單的例子:
type myFlag struct {} func (f myFlag) HasChanged() bool { return false } func (f myFlag) Name() string { return "my-flag-name" } func (f myFlag) ValueString() string { return "my-flag-value" } func (f myFlag) ValueType() string { return "string" }
一旦你的 flag 實現了這個介面,你可以很方便地告訴Viper繫結它:
viper.BindFlagValue("my-flag-name", myFlag{})
FlagValueSet
代表一組 flags 。這是一個關於如何實現這個介面的非常簡單的例子:
type myFlagSet struct { flags []myFlag } func (f myFlagSet) VisitAll(fn func(FlagValue)) { for _, flag := range flags { fn(flag) } }
一旦你的flag set實現了這個介面,你就可以很方便地告訴Viper繫結它:
fSet := myFlagSet{ flags: []myFlag{myFlag{}, myFlag{}}, } viper.BindFlagValues("my-flags", fSet)
在Viper中啟用遠端支援,需要在程式碼中匿名匯入viper/remote
這個包。
import _ "github.com/spf13/viper/remote"
Viper將讀取從Key/Value儲存(例如etcd或Consul)中的路徑檢索到的設定字串(如JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式)。這些值的優先順序高於預設值,但是會被從磁碟、flag或環境變數檢索到的設定值覆蓋。(譯註:也就是說Viper載入設定值的優先順序為:磁碟上的組態檔>命令列標誌位>環境變數>遠端Key/Value儲存>預設值。)
Viper使用crypt從K/V儲存中檢索設定,這意味著如果你有正確的gpg密匙,你可以將設定值加密儲存並自動解密。加密是可選的。
你可以將遠端設定與本地設定結合使用,也可以獨立使用。
crypt
有一個命令列助手,你可以使用它將設定放入K/V儲存中。crypt
預設使用在http://127.0.0.1:4001的etcd。
$ go get github.com/bketelsen/crypt/bin/crypt $ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json
確認值已經設定:
$ crypt get -plaintext /config/hugo.json
有關如何設定加密值或如何使用Consul的範例,請參見crypt
檔案。
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json") viper.SetConfigType("json") // 因為在位元組流中沒有副檔名,所以這裡需要設定下型別。支援的擴充套件名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" err := viper.ReadRemoteConfig()
你需要 Consul Key/Value儲存中設定一個Key儲存包含所需設定的JSON值。例如,建立一個keyMY_CONSUL_KEY
將下面的值存入Consul key/value 儲存:
{ "port": 8080, "hostname": "liwenzhou.com" } viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY") viper.SetConfigType("json") // 需要顯示設定成json err := viper.ReadRemoteConfig() fmt.Println(viper.Get("port")) // 8080 fmt.Println(viper.Get("hostname")) // liwenzhou.com
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document") viper.SetConfigType("json") // 設定的格式: "json", "toml", "yaml", "yml" err := viper.ReadRemoteConfig()
當然,你也可以使用SecureRemoteProvider
。
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg") viper.SetConfigType("json") // 因為在位元組流中沒有副檔名,所以這裡需要設定下型別。支援的擴充套件名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" err := viper.ReadRemoteConfig()
// 或者你可以建立一個新的viper範例 var runtime_viper = viper.New() runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml") runtime_viper.SetConfigType("yaml") // 因為在位元組流中沒有副檔名,所以這裡需要設定下型別。支援的擴充套件名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" // 第一次從遠端讀取設定 err := runtime_viper.ReadRemoteConfig() // 反序列化 runtime_viper.Unmarshal(&runtime_conf) // 開啟一個單獨的goroutine一直監控遠端的變更 go func(){ for { time.Sleep(time.Second * 5) // 每次請求後延遲一下 // 目前只測試了etcd支援 err := runtime_viper.WatchRemoteConfig() if err != nil { log.Errorf("unable to read remote config: %v", err) continue } // 將新設定反序列化到我們執行時的設定結構體中。你還可以藉助channel實現一個通知系統更改的訊號 runtime_viper.Unmarshal(&runtime_conf) } }()
在Viper中,有幾種方法可以根據值的型別獲取值。存在以下功能和方法:
Get(key string) : interface{}
GetBool(key string) : bool
GetFloat64(key string) : float64
GetInt(key string) : int
GetIntSlice(key string) : []int
GetString(key string) : string
GetStringMap(key string) : map[string]interface{}
GetStringMapString(key string) : map[string]string
GetStringSlice(key string) : []string
GetTime(key string) : time.Time
GetDuration(key string) : time.Duration
IsSet(key string) : bool
AllSettings() : map[string]interface{}
需要認識到的一件重要事情是,每一個Get方法在找不到值的時候都會返回零值。為了檢查給定的鍵是否存在,提供了IsSet()
方法。
例如:
viper.GetString("logfile") // 不區分大小寫的設定和獲取 if viper.GetBool("verbose") { fmt.Println("verbose enabled") }
存取器方法也接受深度巢狀鍵的格式化路徑。例如,如果載入下面的JSON檔案:
{ "host": { "address": "localhost", "port": 5799 }, "datastore": { "metric": { "host": "127.0.0.1", "port": 3099 }, "warehouse": { "host": "198.0.0.1", "port": 2112 } } }
Viper可以通過傳入.
分隔的路徑來存取巢狀欄位:
GetString("datastore.metric.host") // (返回 "127.0.0.1")
這遵守上面建立的優先規則;搜尋路徑將遍歷其餘設定登入檔,直到找到為止。(譯註:因為Viper支援從多種設定來源,例如磁碟上的組態檔>命令列標誌位>環境變數>遠端Key/Value儲存>預設值,我們在查詢一個設定的時候如果在當前設定源中沒找到,就會繼續從後續的設定源查詢,直到找到為止。)
例如,在給定此組態檔的情況下,datastore.metric.host和datastore.metric.port均已定義(並且可以被覆蓋)。如果另外在預設值中定義了datastore.metric.protocol,Viper也會找到它。
然而,如果datastore.metric被直接賦值覆蓋(被flag,環境變數,set()
方法等等…),那麼datastore.metric的所有子鍵都將變為未定義狀態,它們被高優先順序設定級別“遮蔽”(shadowed)了。
最後,如果存在與分隔的鍵路徑匹配的鍵,則返回其值。例如:
{ "datastore.metric.host": "0.0.0.0", "host": { "address": "localhost", "port": 5799 }, "datastore": { "metric": { "host": "127.0.0.1", "port": 3099 }, "warehouse": { "host": "198.0.0.1", "port": 2112 } } } GetString("datastore.metric.host") // 返回 "0.0.0.0"
從Viper中提取子樹。
例如,viper
範例現在代表了以下設定:
app: cache1: max-items: 100 item-size: 64 cache2: max-items: 200 item-size: 80
執行後:
subv := viper.Sub("app.cache1")
subv
現在就代表:
max-items: 100 item-size: 64
假設我們現在有這麼一個函數:
func NewCache(cfg *Viper) *Cache {...}
它基於subv
格式的設定資訊建立快取。現在,可以輕鬆地分別建立這兩個快取,如下所示:
cfg1 := viper.Sub("app.cache1") cache1 := NewCache(cfg1) cfg2 := viper.Sub("app.cache2") cache2 := NewCache(cfg2)
你還可以選擇將所有或特定的值解析到結構體、map等。
有兩種方法可以做到這一點:
Unmarshal(rawVal interface{}) : error
UnmarshalKey(key string, rawVal interface{}) : error
舉個例子:
type config struct { Port int Name string PathMap string `mapstructure:"path_map"` } var C config err := viper.Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v", err) }
如果你想要解析那些鍵本身就包含.
(預設的鍵分隔符)的設定,你需要修改分隔符:
v := viper.NewWithOptions(viper.KeyDelimiter("::")) v.SetDefault("chart::values", map[string]interface{}{ "ingress": map[string]interface{}{ "annotations": map[string]interface{}{ "traefik.frontend.rule.type": "PathPrefix", "traefik.ingress.kubernetes.io/ssl-redirect": "true", }, }, }) type config struct { Chart struct{ Values map[string]interface{} } } var C config v.Unmarshal(&C)
Viper還支援解析到嵌入的結構體:
/* Example config: module: enabled: true token: 89h4f98hbwf987h4f98wenf89ehf */ type config struct { Module struct { Enabled bool moduleConfig `mapstructure:",squash"` } } // moduleConfig could be in a module specific package type moduleConfig struct { Token string } var C config err := viper.Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v", err) }
Viper在後臺使用github.com/mitchellh/mapstructure來解析值,其預設情況下使用mapstructure
tag。
注意 當我們需要將viper讀取的設定反序列到我們定義的結構體變數中時,一定要使用mapstructure
tag哦!
你可能需要將viper中儲存的所有設定序列化到一個字串中,而不是將它們寫入到一個檔案中。你可以將自己喜歡的格式的序列化器與AllSettings()
返回的設定一起使用。
import ( yaml "gopkg.in/yaml.v2" // ... ) func yamlStringSettings() string { c := viper.AllSettings() bs, err := yaml.Marshal(c) if err != nil { log.Fatalf("unable to marshal config to YAML: %v", err) } return string(bs) }
Viper是開箱即用的。你不需要設定或初始化即可開始使用Viper。由於大多數應用程式都希望使用單箇中央儲存庫管理它們的設定資訊,所以viper包提供了這個功能。它類似於單例模式。
在上面的所有範例中,它們都以其單例風格的方法演示瞭如何使用viper。
你還可以在應用程式中建立許多不同的viper範例。每個都有自己獨特的一組設定和值。每個人都可以從不同的組態檔,key value儲存區等讀取資料。每個都可以從不同的組態檔、鍵值儲存等中讀取。viper包支援的所有功能都被映象為viper範例的方法。
例如:
x := viper.New() y := viper.New() x.SetDefault("ContentDir", "content") y.SetDefault("ContentDir", "foobar") //...
當使用多個viper範例時,由使用者來管理不同的viper範例。
假設我們的專案現在有一個./conf/config.yaml
組態檔,內容如下:
port: 8123 version: "v1.2.3"
接下來通過範例程式碼演示兩種在專案中使用viper
管理專案設定資訊的方式。
這裡用一個demo演示如何在gin框架搭建的web專案中使用viper
,使用viper載入組態檔中的資訊,並在程式碼中直接使用viper.GetXXX()
方法獲取對應的設定值。
package main import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/spf13/viper" ) func main() { viper.SetConfigFile("config.yaml") // 指定組態檔 viper.AddConfigPath("./conf/") // 指定查詢組態檔的路徑 err := viper.ReadInConfig() // 讀取設定資訊 if err != nil { // 讀取設定資訊失敗 panic(fmt.Errorf("Fatal error config file: %s n", err)) } // 監控組態檔變化 viper.WatchConfig() r := gin.Default() // 存取/version的返回值會隨組態檔的變化而變化 r.GET("/version", func(c *gin.Context) { c.String(http.StatusOK, viper.GetString("version")) }) if err := r.Run( fmt.Sprintf(":%d", viper.GetInt("port"))); err != nil { panic(err) } }
除了上面的用法外,我們還可以在專案中定義與組態檔對應的結構體,viper
載入完設定資訊後使用結構體變數儲存設定資訊。
package main import ( "fmt" "net/http" "github.com/fsnotify/fsnotify" "github.com/gin-gonic/gin" "github.com/spf13/viper" ) type Config struct { Port int `mapstructure:"port"` Version string `mapstructure:"version"` } var Conf = new(Config) func main() { viper.SetConfigFile("./conf/config.yaml") // 指定組態檔路徑 err := viper.ReadInConfig() // 讀取設定資訊 if err != nil { // 讀取設定資訊失敗 panic(fmt.Errorf("Fatal error config file: %s n", err)) } // 將讀取的設定資訊儲存至全域性變數Conf if err := viper.Unmarshal(Conf); err != nil { panic(fmt.Errorf("unmarshal conf failed, err:%s n", err)) } // 監控組態檔變化 viper.WatchConfig() // 注意!!!組態檔發生變化後要同步到全域性變數Conf viper.OnConfigChange(func(in fsnotify.Event) { fmt.Println("夭壽啦~組態檔被人修改啦...") if err := viper.Unmarshal(Conf); err != nil { panic(fmt.Errorf("unmarshal conf failed, err:%s n", err)) } }) r := gin.Default() // 存取/version的返回值會隨組態檔的變化而變化 r.GET("/version", func(c *gin.Context) { c.String(http.StatusOK, Conf.Version) }) if err := r.Run(fmt.Sprintf(":%d", Conf.Port)); err != nil { panic(err) } }
以上就是golang設定管理神器Viper使用教學的詳細內容,更多關於golang設定管理Viper教學的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45