<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
本文只介紹template的語法和用法,關於template包的函數、方法、template的結構和原理,見:深入解析Go template模板使用詳解。
以下為test.html檔案的內容,裡面使用了一個template語法{{.}}
。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web</title> </head> <body> {{ . }} </body> </html>
以下是test.html同目錄下的一個go web程式:
package main import ( "html/template" "net/http" ) func tmpl(w http.ResponseWriter, r *http.Request) { t1, err := template.ParseFiles("test.html") if err != nil { panic(err) } t1.Execute(w, "hello world") } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/tmpl", tmpl) server.ListenAndServe() }
前面的html檔案中使用了一個template的語法{{.}}
,這部分是需要通過go的template引擎進行解析,然後替換成對應的內容。
在go程式中,handler函數中使用template.ParseFiles("test.html")
,它會自動建立一個模板(關聯到變數t1上),並解析一個或多個文字檔案(不僅僅是html檔案),解析之後就可以使用Execute(w,"hello world")
去執行解析後的模板物件,執行過程是合併、替換的過程。例如上面的{{.}}
中的.
會替換成當前物件"hello world",並和其它純字串內容進行合併,最後寫入w中,也就是傳送到瀏覽器"hello world"。
本文不解釋這些template包的函數、方法以及更底層的理論知識,本文只解釋template的語法,如果覺得這些無法理解,或者看不懂官方手冊,請看深入解析Go template模板使用詳解。
在寫template的時候,會經常用到"."。比如{{.}}
、{{len .}}
、{{.Name}}
、{{$x.Name}}
等等。
在template中,點"."代表當前作用域的當前物件。它類似於java/c++的this關鍵字,類似於perl/python的self。如果瞭解perl,它更可以簡單地理解為預設變數$_
。
例如,前面範例test.html中{{.}}
,這個點是頂級作用域範圍內的,它代表Execute(w,"hello worold")
的第二個引數"hello world"。也就是說它代表這個字串物件。
再例如,有一個Person struct。
type Person struct { Name string Age int } func main(){ p := Person{"longshuai",23} tmpl, _ := template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}") _ = tmpl.Execute(os.Stdout, p) }
這裡{{.Name}}
和{{.Age}}
中的點"."代表的是頂級作用域的物件p,所以Execute()方法執行的時候,會將{{.Name}}
替換成p.Name
,同理{{.Age}}
替換成{{p.Age}}
。
但是並非只有一個頂級作用域,range、with、if等內建action都有自己的本地作用域。它們的用法後文解釋,這裡僅引入它們的作用域來解釋"."。
例如下面的例子,如果看不懂也沒關係,只要從中理解"."即可。
package main import ( "os" "text/template" ) type Friend struct { Fname string } type Person struct { UserName string Emails []string Friends []*Friend } func main() { f1 := Friend{Fname: "xiaofang"} f2 := Friend{Fname: "wugui"} t := template.New("test") t = template.Must(t.Parse( `hello {{.UserName}}! {{ range .Emails }} an email {{ . }} {{- end }} {{ with .Friends }} {{- range . }} my friend name is {{.Fname}} {{- end }} {{ end }}`)) p := Person{UserName: "longshuai", Emails: []string{"a1@qq.com", "a2@gmail.com"}, Friends: []*Friend{&f1, &f2}} t.Execute(os.Stdout, p) }
輸出結果:
hello longshuai! an email a1@qq.com an email a2@gmail.com my friend name is xiaofang my friend name is wugui
這裡定義了一個Person結構,它有兩個slice結構的欄位。在Parse()方法中:
{{.UserName}}
、{{.Emails}}
、{{.Friends}}
中的點都代表Execute()的第二個引數,也就是Person物件p,它們在執行的時候會分別被替換成p.UserName、p.Emails、p.Friends。{{range .Emails}}...{{end}}
這一段結構內部an email {{.}}
,這個"."代表的是range迭代時的每個元素物件,也就是p.Emails這個slice中的每個元素。{{range .}}
的"."代表的是p.Friends,也就是各個,再此range中又有一層迭代,此內層{{.Fname}}
的點代表Friend結構的範例,分別是&f1
和&f2
,所以{{.Fname}}
代表範例物件的Fname欄位。template引擎在進行替換的時候,是完全按照文字格式進行替換的。除了需要評估和替換的地方,所有的行分隔符、空格等等空白都原樣保留。所以,對於要解析的內容,不要隨意縮排、隨意換行。
可以在{{
符號的後面加上短橫線並保留一個或多個空格"- "來去除它前面的空白(包括換行符、製表符、空格等),即{{- xxxx
。
在}}
的前面加上一個或多個空格以及一個短橫線"-"來去除它後面的空白,即xxxx -}}
。
例如:
{{23}} < {{45}} -> 23 < 45 {{23}} < {{- 45}} -> 23 <45 {{23 -}} < {{45}} -> 23< 45 {{23 -}} < {{- 45}} -> 23<45
其中{{23 -}}
中的短橫線去除了這個替換結構後面的空格,即}} <
中間的空白。同理{{- 45}}
的短橫線去除了< {{
中間的空白。
再看上一節的例子中:
t.Parse( `hello {{.UserName}}! {{ range .Emails }} an email {{ . }} {{- end }} {{ with .Friends }} {{- range . }} my friend name is {{.Fname}} {{- end }} {{ end }}`)
注意,上面沒有進行縮排。因為縮排的製表符或空格在替換的時候會保留。
第一行和第二行之間輸出時會換行輸出,不僅如此,range {{.Emails}}
自身也佔一行,在替換的時候它會被保留為空行。除非range前面沒加{{-
。由於range的{{- end
加上了去除字首空白,所以每次迭代的時候,每個元素之間都換行輸出但卻不多一空行,如果這裡的end去掉{{-
,則每個迭代的元素之間輸出的時候都會有空行。同理後面的with和range。
註釋方式:{{/* a comment */}}
。
註釋後的內容不會被引擎進行替換。但需要注意,註釋行在替換的時候也會佔用行,所以應該去除字首和字尾空白,否則會多一空行。
{{- /* a comment without prefix/suffix space */}} {{/* a comment without prefix/suffix space */ -}} {{- /* a comment without prefix/suffix space */ -}}
注意,應該只去除字首或字尾空白,不要同時都去除,否則會破壞原有的格式。例如:
t.Parse( `hello {{.UserName}}! {{- /* this line is a comment */}} {{ range .Emails }} an email {{ . }} {{- end }}
pipeline是指產生資料的操作。比如{{.}}
、{{.Name}}
、funcname args
等。
可以使用管道符號|
連結多個命令,用法和unix下的管道類似:|
前面的命令將運算結果(或返回值)傳遞給後一個命令的最後一個位置。
例如:
{{.}} | printf "%sn" "abcd"
{{.}}
的結果將傳遞給printf,且傳遞的引數位置是"abcd"之後。
命令可以有超過1個的返回值,這時第二個返回值必須為err型別。
需要注意的是,並非只有使用了|
才是pipeline。Go template中,pipeline的概念是傳遞資料,只要能產生資料的,都是pipeline。這使得某些操作可以作為另一些操作內部的表示式先執行得到結果,就像是Unix下的命令替換一樣。
例如,下面的(len "output")
是pipeline,它整體先執行。
{{println (len "output")}}
下面是Pipeline的幾種範例,它們都輸出"output"
:
{{`"output"`}} {{printf "%q" "output"}} {{"output" | printf "%q"}} {{printf "%q" (print "out" "put")}} {{"put" | printf "%s%s" "out" | printf "%q"}} {{"output" | printf "%s" | printf "%q"}}
可以在template中定義變數:
// 未定義過的變數 $var := pipeline // 已定義過的變數 $var = pipeline
例如:
{{- $how_long :=(len "output")}} {{- println $how_long}} // 輸出6
再例如:
tx := template.Must(template.New("hh").Parse( `{{range $x := . -}} {{$y := 333}} {{- if (gt $x 33)}}{{println $x $y ($z := 444)}}{{- end}} {{- end}} `)) s := []int{11, 22, 33, 44, 55} _ = tx.Execute(os.Stdout, s)
輸出結果:
44 333 444 55 333 444
上面的範例中,使用range迭代slice,每個元素都被賦值給變數$x
,每次迭代過程中,都新設定一個變數$y
,在內層巢狀的if結構中,可以使用這個兩個外層的變數。在if的條件表示式中,使用了一個內建的比較函數gt,如果$x
大於33,則為true。在println的引數中還定義了一個$z
,之所以能定義,是因為($z := 444)
的過程是一個Pipeline,可以先執行。
需要注意三點:
$
,它代表模板的最頂級作用域物件(通俗地理解,是以模板為全域性作用域的全域性變數),在Execute()執行的時候進行賦值,且一直不變。例如上面的範例中,$ = [11 22 33 44 55]
。再例如,define定義了一個模板t1,則t1中的$
作用域只屬於這個t1。例如:
func main() { t1 := template.New("test1") tmpl, _ := t1.Parse( ` {{- define "T1"}}ONE {{println .}}{{end}} {{- define "T2"}}{{template "T1" $}}{{end}} {{- template "T2" . -}} `) _ = tmpl.Execute(os.Stdout, "hello world") }
上面使用define額外定義了T1和T2兩個模板,T2中巢狀了T1。{{template "T2" .}}
的點代表頂級作用域的"hello world"物件。在T2中使用了特殊變數$
,這個$
的範圍是T2的,不會繼承頂級作用域"hello world"。但因為執行T2的時候,傳遞的是".",所以這裡的$
的值仍然是"hello world"。
不僅$
不會在模板之間繼承,.
也不會在模板之間繼承(其它所有變數都不會繼承)。實際上,template可以看作是一個函數,它的執行過程是template("T2",.)
。如果把上面的$
換成".",結果是一樣的。如果換成{{template "T2"}}
,則$=nil
如果看不懂這些,後文有解釋。
有以下幾種if條件判斷語句,其中第三和第四是等價的。
{{if pipeline}} T1 {{end}} {{if pipeline}} T1 {{else}} T0 {{end}} {{if pipeline}} T1 {{else if pipeline}} T0 {{end}} {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
需要注意的是,pipeline為false的情況是各種資料物件的0值:數值0,指標或介面是nil,陣列、slice、map或string則是len為0。
有兩種迭代表示式型別:
{{range pipeline}} T1 {{end}} {{range pipeline}} T1 {{else}} T0 {{end}}
range可以迭代slice、陣列、map或channel。迭代的時候,會設定"."為當前正在迭代的元素。
對於第一個表示式,當迭代物件的值為0值時,則range直接跳過,就像if一樣。對於第二個表示式,則在迭代到0值時執行else語句。
tx := template.Must(template.New("hh").Parse( `{{range $x := . -}} {{println $x}} {{- end}} `)) s := []int{11, 22, 33, 44, 55} _ = tx.Execute(os.Stdout, s)
需注意的是,range的引數部分是pipeline,所以在迭代的過程中是可以進行賦值的。但有兩種賦值情況:
{{range $value := .}} {{range $key,$value := .}}
如果range中只賦值給一個變數,則這個變數是當前正在迭代元素的值。如果賦值給兩個變數,則第一個變數是索引值(map/slice是數值,map是key),第二個變數是當前正在迭代元素的值。
下面是在html中使用range的一個範例。test.html檔案內容如下:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web</title> </head> <body> <ul> {{ range . }} <li>{{ . }}</li> {{ else }} <li> Nothing to show </li> {{ end}} </ul> </body> </html>
以下是test.html同目錄下的go程式檔案:
package main import ( "html/template" "net/http" ) func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/process", process) server.ListenAndServe() } func process(w http.ResponseWriter, r *http.Request) { t1 := template.Must(template.ParseFiles("test.html")) s := []string{ "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日",} t1.Execute(w, s) }
with用來設定"."的值。兩種格式:
{{with pipeline}} T1 {{end}} {{with pipeline}} T1 {{else}} T0 {{end}}
對於第一種格式,當pipeline不為0值的時候,點"."設定為pipeline運算的值,否則跳過。對於第二種格式,當pipeline為0值時,執行else語句塊,否則"."設定為pipeline運算的值,並執行T1。
例如:
{{with "xx"}}{{println .}}{{end}}
上面將輸出xx
,因為"."已經設定為"xx"。
template定義了一些內建函數,也支援自定義函數。關於如何自定義函數,見深入解析Go template模板使用詳解。
以下是內建的函數列表:
and 返回第一個為空的引數或最後一個引數。可以有任意多個引數。 and x y等價於if x then y else x not 布林取反。只能一個引數。 or 返回第一個不為空的引數或最後一個引數。可以有任意多個引數。 "or x y"等價於"if x then x else y"。 print printf println 分別等價於fmt包中的Sprint、Sprintf、Sprintln len 返回引數的length。 index 對可索引物件進行索引取值。第一個引數是索引物件,後面的引數是索引位。 "index x 1 2 3"代表的是x[1][2][3]。 可索引物件包括map、slice、array。 call 顯式呼叫函數。第一個引數必須是函數型別,且不是template中的函數,而是外部函數。 例如一個struct中的某個欄位是func型別的。 "call .X.Y 1 2"表示呼叫dot.X.Y(1, 2),Y必須是func型別,函數引數是1和2。 函數必須只能有一個或2個返回值,如果有第二個返回值,則必須為error型別。
除此之外,還內建一些用於比較的函數:
eq arg1 arg2: arg1 == arg2時為true ne arg1 arg2: arg1 != arg2時為true lt arg1 arg2: arg1 < arg2時為true le arg1 arg2: arg1 <= arg2時為true gt arg1 arg2: arg1 > arg2時為true ge arg1 arg2: arg1 >= arg2時為true
對於eq函數,支援多個引數:
eq arg1 arg2 arg3 arg4...
它們都和第一個引數arg1進行比較。它等價於:
arg1==arg2 || arg1==arg3 || arg1==arg4
範例:
{{ if (gt $x 33) }}{{println $x}}{{ end }}
define可以直接在待解析內容中定義一個模板,這個模板會加入到common結構組中,並關聯到關聯名稱上。如果不理解,還是建議閱讀深入解析Go template模板使用詳解。
定義了模板之後,可以使用template這個action來執行模板。template有兩種格式:
{{template "name"}} {{template "name" pipeline}}
第一種是直接執行名為name的template,點設定為nil。第二種是點"."設定為pipeline的值,並執行名為name的template。可以將template看作是函數:
template("name) template("name",pipeline)
例如:
func main() { t1 := template.New("test1") tmpl, _ := t1.Parse( `{{- define "T1"}}ONE {{println .}}{{end}} {{- define "T2"}}TWO {{println .}}{{end}} {{- define "T3"}}{{template "T1"}}{{template "T2" "haha"}}{{end}} {{- template "T3" -}} `) _ = tmpl.Execute(os.Stdout, "hello world") }
輸出結果:
ONE <nil> TWO haha
上面定義了4個模板,一個是test1,另外三個是使用define來定義的T1、T2、T3,其中t1是test1模板的關聯名稱。T1、T2、T3和test1共用一個common結構。其中T3中包含了執行T1和T2的語句。最後只要{{template T3}}
就可以執行T3,執行T3又會執行T1和T2。也就是實現了巢狀。此外,執行{{template "T1"}}
時,點設定為nil,而{{temlate "T2" "haha"}}
的點設定為了"haha"。
注意,模板之間的變數是不會繼承的。
下面是html檔案中巢狀模板的幾個範例。
t1.html檔案內容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=9"> <title>Go Web Programming</title> </head> <body> <div> This is t1.html before</div> <div>This is the value of the dot in t1.html - [{{ . }}]</div> <hr /> {{ template "t2.html" }} <hr /> <div> This is t1.html after</div> </body> </html>
因為內部有{{template "t2.html"}}
,且此處沒有使用define去定義名為"t2.html"的模板,所以需要載入解析名為t2.html的檔案。t2.html檔案內容如下:
<div style="background-color: yellow;"> This is t2.html<br/> This is the value of the dot in t2.html - [{{ . }}] </div>
處理這兩個檔案的handler函數如下:
func process(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("t1.html", "t2.html") t.Execute(w, "Hello World!") }
上面也可以不額外定義t2.html檔案,而是直接在t1.html檔案中使用define定義一個模板。修改t1.html檔案如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=9"> <title>Go Web Programming</title> </head> <body> <div> This is t1.html before</div> <div>This is the value of the dot in t1.html - [{{ . }}]</div> <hr /> {{ template "t2.html" }} <hr /> <div> This is t1.html after</div> </body> </html> {{define "t2.html"}} <div style="background-color: yellow;"> This is t2.html<br/> This is the value of the dot in t2.html - [{{ . }}] </div> {{end}}
然後在handler中,只需解析t1.html一個檔案即可。
func process(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("t1.html") t.Execute(w, "Hello World!") }
{{block "name" pipeline}} T1 {{end}} A block is shorthand for defining a template {{define "name"}} T1 {{end}} and then executing it in place {{template "name" pipeline}} The typical use is to define a set of root templates that are then customized by redefining the block templates within.
根據官方檔案的解釋:block等價於define定義一個名為name的模板,並在"有需要"的地方執行這個模板,執行時將"."設定為pipeline的值。
但應該注意,block的第一個動作是執行名為name的模板,如果不存在,則在此處自動定義這個模板,並執行這個臨時定義的模板。換句話說,block可以認為是設定一個預設模板。
例如:
{{block "T1" .}} one {{end}}
它首先表示{{template "T1" .}}
,也就是說先找到T1模板,如果T1存在,則執行找到的T1,如果沒找到T1,則臨時定義一個{{define "T1"}} one {{end}}
,並執行它。
下面是正常情況下不使用block的範例。
home.html檔案內容如下:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web Programming</title> </head> <body> {{ template "content" }} </body> </html>
在此檔案中指定了要執行一個名為"content"的模板,但此檔案中沒有使用define定義該模板,所以需要在其它檔案中定義名為content的模板。現在分別在兩個檔案中定義兩個content模板:
red.html檔案內容如下:
{{ define "content" }} <h1 style="color: red;">Hello World!</h1> {{ end }}
blue.html檔案內容如下:
{{ define "content" }} <h1 style="color: blue;">Hello World!</h1> {{ end }}
在handler中,除了解析home.html,還根據需要解析red.html或blue.html:
func process(w http.ResponseWriter, r *http.Request) { rand.Seed(time.Now().Unix()) t := template.New("test") if rand.Intn(10) > 5 { t, _ = template.ParseFiles("home.html", "red.html") } else { t, _ = template.ParseFiles("home.html", "blue.html") } t.Execute(w,"") }
如果使用block,那麼可以設定預設的content模板。例如將原本定義在blue.html中的content設定為預設模板。
修改home.html:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web Programming</title> </head> <body> {{ block "content" . }} <h1 style="color: blue;">Hello World!</h1> {{ end }} </body> </html>
然後修改handler:
func process(w http.ResponseWriter, r *http.Request) { rand.Seed(time.Now().Unix()) t := template.New("test") if rand.Intn(10) > 5 { t, _ = template.ParseFiles("home.html", "red.html") } else { t, _ = template.ParseFiles("home.html") } t.Execute(w,"") }
當執行else語句塊的時候,發現home.html中要執行名為content的模板,但在ParseFiles()中並沒有解析包含content模板的檔案。於是執行block定義的content模板。而執行非else語句的時候,因為red.html中定義了content,會直接執行red.html中的content。
block通常設定在頂級的根檔案中,例如上面的home.html中。
對於html/template包,有一個很好用的功能:上下文感知。text/template沒有該功能。
上下文感知具體指的是根據所處環境css、js、html、url的path、url的query,自動進行不同格式的跳脫。
例如,一個handler函數的程式碼如下:
func process(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("test.html") content := `I asked: <i>"What's up?"</i>` t.Execute(w, content) }
上面content是Execute的第二個引數,它的內容是包含了特殊符號的字串。
下面是test.html檔案的內容:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web Programming</title> </head> <body> <div>{{ . }}</div> <div><a href="/{{ . }}" rel="external nofollow" >Path</a></div> <div><a href="/?q={{ . }}" rel="external nofollow" >Query</a></div> <div><a onclick="f('{{ . }}')">Onclick</a></div> </body> </html>
上面test.html中有4個不同的環境,分別是html環境、url的path環境、url的query環境以及js環境。雖然物件都是{{.}}
,但解析執行後的值是不一樣的。如果使用curl獲取原始碼,結果將如下:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web Programming</title> </head> <body> <div>I asked: <i>"What's up?"</i></div> <div> <a href="/I%20asked:%20%3ci%3e%22What%27s%20up?%22%3c/i%3e" rel="external nofollow" > Path </a> </div> <div> <a href="/?q=I%20asked%3a%20%3ci%3e%22What%27s%20up%3f%22%3c%2fi%3e" rel="external nofollow" > Query </a> </div> <div> <a onclick="f('I asked: x3cix3ex22Whatx27s up?x22x3c/ix3e')"> Onclick </a> </div> </body> </html>
上下文感知的自動跳脫能讓程式更加安全,比如防止XSS攻擊(例如在表單中輸入帶有<script>...</script>
的內容並提交,會使得使用者提交的這部分script被執行)。
如果確實不想跳脫,可以進行型別轉換。
type CSS type HTML type JS type URL
轉換成指定個時候,字元都將是字面意義。
例如:
func process(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("tmpl.html") t.Execute(w, template.HTML(r.FormValue("comment"))) }
更多關於Go模板template用法詳解請檢視下面的相關連結
相關文章
<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