深度分析:前端中的後端-實現篇

2021-02-24 程序人生

當我有一個想法,並且這個想法很有意思,正好戳中我技能的盲區時,我便有一種強大的要將其實驗一番的衝動。自從上周做一個「前端中的後端」的想法出爐後,這周我幾乎寢食難安,隨時隨地都在想這件事,所以後來乾脆擼起袖子開幹,畢竟 Linus 大神告誡我們:

一旦開幹,就有些摟不住了,每日正常工作開會帶娃做飯之餘,我幾乎是 7-12-7 地將其一點點折騰出來,為了優化每一分時間,我甚至把哄小貝睡覺的時間從平均一個小時縮減到 25 分鐘(訣竅是:唱搖籃曲的時候不斷地假裝打哈欠 —— 哈欠是會傳染的)。

這種沉浸式的,集中精神全力以赴做一件事的感覺讓我很快樂。在這個過程中,我第一次正式寫 swift,就被迫在 Data,UsafeRawBufferPoiner 和 UnsafePointer<UInt8> 之間遊躥,不得不深入到 xcodebuild / swift package / xcframework 的細節去把一切東西自動化地在 CI 中完整地串聯起來。當你真正深入去做一件事情的時候,你會發現,你的認知和實際情況相差很大 —— 比如:和我花在 swift package 上編譯 static library 所花的巨大精力相比,在Rust 上構建 FFI 代碼的過程簡直就像閒庭信步,真是大大出乎了我的意料。

當我最終在 xcode 裡測試通過 swift 和 rust 交互的整個流程,並且將其運行在 github action(使用 ubuntu 而不是 osx)做了一個相對完整的 CI 後,可想而知,我有多麼興奮:

更令人興奮的是,在整個過程中,我學到了:

如何更好地定製化 prost build,讓生成的 rust 的 protobuf 代碼能夠完美兼容不夠嚴謹的 JSON 數據。

如何生成 rust 代碼的 flamegraph,來更好地剖析代碼中的低效的部分,然後結合 citerion 做 benchmark,來優化和提升代碼運行的效率 —— 通過這個過程,我把一個不起眼的函數的效率提升了幾乎一倍。

如何使用 Mozilla 提供的 ffi-support,讓跨語言調用時即便 Rust 側 panic,整個應用程式也不會崩潰。

如何更好地拆分 rust crate,讓 unit test 變得更加簡單。

如何更合理地使用 unsafe。這是我第一個真正使用 unsafe Rust 的項目。嗯,不少心得。

如何使用 tokio/future runtime,使其可以把任務從調用的線程(swift 線程)轉交給一組 Rust 的線程,並通過 callback 返回。這個其實很簡單的工作,由於我一開始思路錯了,導致走了很多彎路。

如何寫包含 unit test,formatter,linter 的嚴肅的 swift 代碼(嗯,我之前為了學語言寫過 playground 代碼和 swift UI,但沒有正經寫過包含單元測試的 Swift 代碼)。

如何使用 swift protobuf 和在 swift 上做 performance benchmark。

如何使用 swift package manager,以及如何在 xcode 裡連結靜態庫。

如何把靜態庫打包成 xcframework(很遺憾,arm 的靜態庫目前還無法成功打包進去)。

如何優雅地撰寫複雜的 Makefile。

這些學到的內容也許值得寫好幾篇文章,就看我有沒有時間,以及有沒有心情了。在做這個 POC 的時候,我糾結過,是用一套公開的 API 來撰寫一個開源的 POC 項目,還是特定對於 Tubi 的業務做一個更貼近生產環境的閉源 POC 項目。幾經思考之後,我決定還是做成一個閉源 POC 項目,因為這樣可以更好地通過已有的業務來更好地評估「前端中的後端」這件事情的難度以及意義。等一切坑都趟平後,我會在做 quenya client 端代碼自動生成時,將這個流程及代碼生成結合起來,做一套通過 OpenAPI spec 生成 Rust 代碼,用於 FFI 的 protobuf 定義,以及對應的 swift/kotlin/typescript 的 binding 的代碼。這將是另外一個故事了。

好,廢話不多說。我們來具體講講實現過程中我關於架構,設計,以及具體編碼過程中的一些思考。我寫的項目名字叫 olorin:olorin 是 Gandalf 的另外一個名字,就像 Gandalf 聯合起護戒小分隊一樣,我希望這個項目可以將 iOS/android/web/osx/windows 很好地聯合起來。

架構和設計

如果你看過上一篇文章,那麼你還大概記得這樣一個架構:

以及一個設想中的 API 的實現流程:

olorin 的實現幾乎完全按照這個架構完成:

更為具體的流程見下圖:

這裡面,FFI 接口是至關重要的,它包括下面幾個函數:

service_init

Rust 側的初始化。Swift 代碼提供一個用於初始化的 protobuf 字節流的指針和長度,Rust 側創建對應的運行時,然後返回給 Swift 一個句柄,供以後的請求使用。這個請求一般是 app 啟動時調用。Swift 可以提供一些基本的伺服器請求參數,比如設備 ID,平臺,用戶 ID,要請求的伺服器域名(prod/staging/dev)等信息。Rust 代碼會利用設備 ID 和用戶 ID(如果存在)在本地存儲裡查找是否有之前儲存的用戶狀態,如果有,就加載到 State 中;如果沒有,就創建新的 State。

service_dispatch/service_dispatch_block

這兩個函數一個用於異步請求,一個用於同步請求。同步請求會阻塞 Swift 代碼所在的線程;而異步請求則在不同的線程執行,完成之後調用 Swift 側提供的 callback,提交結果。

請求的時候會提供之前獲取的句柄,來找到對應的 Rust 運行時及狀態。此外,還要提供請求所包含的 protobuf 字節流的指針和長度。因為所有的請求都走這一個接口,所以它被封裝成為 protobuf 的一個 oneof message,如下所示(有刪減):

這種通過使用 oneof 來統一調用接口的方法,我是跟 Tendermint 的 ABCI 學的,非常好用。這樣,我們在處理請求的時候,就可以根據其類型進行相應的 dispatch 了:

之所以提供一個同步和一個異步的接口,完全是為了客戶端靈活而設置的。我自己沒有做過生產環境的客戶端,不知道哪種方式最適合客戶端使用,所以乾脆都提供了。好在對於 Tokio 來說,不過是 spawn 和 block_on 的區別而已。

我看了 Firefox sync 的部分代碼,它只提供了同步調用的接口,所以整體上的設計比我這裡所列的要簡單。其實同步調用挺好的,不容易出錯。

service_dispatch 接口具體在 Rust 中的實現並不困難。我們只需要了解如何做 Rust C FFI 即可。其實沒什麼神秘的,只需要注意三點:

一個完整流程

我們看一個從 Swift 到 Rust 的完整的 Ping/Pong 的代碼,看看具體是怎麼運作的。

首先在 Swift 側,我們先初始化 service 結構。初始化的時候會調用 Rust 側的初始化,生成上文我們所說的 runtime/state。

當我們在 Swift 裡調用 service.ping 時,會先生成一個 AbiRequestPing。這是我用 Apple 官方的 swift protobuf 庫,基於我定義的 protobuf 生成的結構。由於 Swift import 一個庫之後,所有的結構就無需 namespace 可以直接訪問,所以我加了一個前綴(在 protobuf 定義:option swift_prefix="Abi"),一來好找,二來避免和其它數據結構衝突。

生成好 AbiRequestPing 後,需要將其進一步封裝到 AbiNativeRequest(見上文的 protobuf 定義),然後將其序列化成字節流。因為接下來要將這個字節流傳給 Rust,所以我們需要將其轉換成  UnsafeByte<UInt8>。之後調用 service_dispatch_block,同步返回結果 —— 為了簡單起見,我們先不看異步的流程。這個結果是一個 ByteBuffer 結構。這是 Rust 傳給 Swift 的指針,所以我們需要將其處理成一個 UnsafeRawBufferPointer,封裝成 Data,再反序列化成 AbiResponsePong。

這裡面的核心是 rustCall 函數,它負責處理和內存安全相關的代碼,我們先放下不表。

Rust 側的 service_dispatch_block,會把傳入的指針轉換成 Vec<u8>,然後再反序列化成 NativeRequest,就可以正常使用了。

內存管理

這時候,你可能會想到:數據在 Swift 和 Rust 間傳來傳去,究竟誰應該負責清理內存?

答案是:誰原本擁有的內存,誰負責釋放。

Swift 側是調用方,其傳遞給 Rust 的內存都在 withUnsafeBytes 閉包中,Rust 函數調用棧結束後,對該內存的引用消失,所以沒有內存洩漏的危險,不需要手工處理。

Rust 是被調方,內存傳遞給 Swift 後,並不知道 Swift 會何時何地結束引用,所以 Rust 自己的所有權模型被略過(因為使用了 unsafe),需要手工「釋放」。釋放的原則:

任何 Rust 傳給 Swift 的 buffer,包括各種指針和字符串(字符串也是指針,但往往會被人忽略),都需要手工釋放。

所謂的「釋放」,只不過是把原來的指針再還給 Rust,並由 Rust 代碼從指針中構建數據結構來重新「擁有」這塊內存,這樣 Rust 的所有權模型會接管並在合適的時候進行釋放。

當「擁有」這塊內存的 Rust 函數結束後,內存被回收。

這也就意味著 Rust 代碼需要為自己傳出去的內存提供回收的方法,供 Swift 使用。上文中提到的 FFI 接口,有兩個函數:rust_bytebuffer_free 和 rust_str_free 是負責做這個事情的。因為我們兩個語言之間交互的主要接口就幾個,而涉及的指針,只有以下兩種,所以我們只需要相應地處理:

我們看剛才被忽略的 rustCall 代碼:

如果你仔細看這段 Swift 代碼,你可能會非常疑惑,這裡沒有調用 rust_str_free 的代碼釋放包含錯誤消息的字符串啊?

這裡用了 Swift 的一個很有用的模式:使用參數標籤來擴展已有的功能。Swift 有著非常強大的 extension 能力[2],輔以參數標籤,能力爆表:

這段代碼裡我只需擴展 String,為其 init 函數增加一個我自己的會「歸還」Rust 指針並初始化字符串的實現即可。

說句題外話,初學 Swift 的時候,我覺得函數的參數標籤是個非常雞肋的功能,邊寫邊吐槽它的繁瑣(對於一個不太使用 xcode,大部分時候在 vscode 寫代碼的人來說,需要額外敲很多鍵),後來發現參數標籤可以用作重載,臥槽,對我這個 Swift 小白來說,簡直就是如獲至寶。現在我已經離不開參數標籤,並且開始吐槽:為啥 Rust 不支持參數標籤(及重載)?

錯誤處理

跨語言的錯誤處理是一個很有意思的技術活。我們需要回答一個核心問題:如何把 Rust 代碼的錯誤 Resut<T, E>,優雅地轉化成 Swift 裡的 Exception?

一種思路是,把 Result<T, E> 中的 E ,也就是 Error,轉化成一個 C 的結構體,包含錯誤碼 (enum)和錯誤消息(char *),然後在 Swift 側,利用這個信息重組並拋出異常。

另一種思路是,Rust 代碼中返回的 protobuf 中包含錯誤信息,然後在 Swift 側,查看這一信息並在需要的時候拋出異常。

因為我已經在使用 protobuf 來傳遞數據,所以我更加喜歡第二種思路的處理方式:簡潔且沒有額外的內存需要釋放,然而,我使用的庫 ffi-support 在其封裝的 FFI 調用接口上,強行安置了 ExternalError 這個參數,使得我只能使用第一種思路。

如果你再看一眼 service_dispatch_block 的實現,會對下面這個閉包式的調用感到困惑:call_with_result 為什麼要設計成這樣的形式?

這是因為其它語言調用 Rust 的時候,Rust 代碼有可能 panic(比如 unwrap() 失敗),這將會直接導致調用的線程崩潰,從而可能讓整個應用崩潰。從開發的角度,我們應該避免任何代碼主動產生 panic,而是要把所有錯誤封裝到 Result 中,但因為我們的代碼會調用第三方庫,我們無法保證所有第三方庫都嚴格這樣處理。對於 Swift 代碼來說,Rust 代碼所提供的庫是一個黑盒,它理應保證不會出現任何會導致崩潰的行為。所以,我們需要一旦遇到 panic 時,能夠進行棧展開(stack unwinding)。

我們知道,當函數正常調用結束後,其調用棧會返回到調用之前的狀態 —— 你可以寫一段簡單的 C 代碼,編譯成 .o,然後用 objdump 來查看編譯器自動插入的棧展開代碼。然而,當一層層調用,棧不斷累積的時候,如果內層的函數拋出了異常,而很外面的函數才捕獲這個異常,那麼,(支持異常處理的)編譯器會插入回溯代碼,一路把棧回溯到捕獲異常的位置。在這個過程中,涉及到的上下文中所有的棧對象和用智能指針管理的堆對象都會並回收,不會有內存洩漏(對於 C++ 來說,非智能指針分配出的對象會洩漏)。對於 Rust 來說,棧展開是內存安全的,不會有任何內存洩漏。下圖是我在 google image 裡找到的關於棧展開不錯的實例[3](我自己就懶得畫了):

所以 call_with_result 就是為了保證在 FFI 這一層,所有調用的代碼都有合適的棧展開代碼來把任何潛在的 panic 捕獲到並回溯堆棧,讓 Swift(或者其他語言)的代碼就像經歷了一次異常。只要 Swift 代碼捕獲這個異常,那麼程序依舊能夠正常處理。call_with_result 的具體實現如下,感興趣的可以深入了解:

單元測試

我們講了跨語言調用的解決方案,實現方法,以及內存管理和異常處理這些在實際開發中非常重要的部分。接下來,我們講講同樣非常重要卻往往被人忽視的部分:單元測試。

Rust FFI 接口之外的單元測試自不必說,該怎麼搞就怎麼搞,我們用單元測試(以及 property testing)保證純粹的 Rust 代碼在邏輯上的正確性。

Rust 提供給其它語言的 C FFI,需要妥善測試。這裡有幾個挑戰:

我們要為測試環境提供一個貼近於 Swift 調用 Rust 的運行環境,比如:所有的測試使用同一個 service_init 產生的 handle。這個,可以通過 std::sync::Once 來完成。

對於 service_dispatch,模擬 Swift callback 函數。

因為 service_dispatch 在其他線程中執行,因此測試結果出錯需要能夠被測試線程捕獲。

2 和 3 的實現方法可以參考以下實例:

可以看到,assert_eq! 在 on_result 回調中調用,而這個回調運行在 tokio 管理的若干個線程中的某個,因而有可能測試線程結束時,該線程還沒有結束。所以這裡我們需要不那麼優雅地通過 sleep 阻塞一下測試線程。這裡因為回調是一個 C 函數,無法做成 Rust 的閉包,因此,使用 channel 同步兩個線程的思路行不通。如果大家有比 sleep 更好的方法,歡迎跟我探討。我個人非常討厭在 test 中顯式地 sleep 來進行同步。

即便我們阻塞了足夠多的時間,這裡還有另一個問題:assert_eq! 產生的 panic 無法被測試線程捕獲到。所以我們在 FFI 代碼的測試初始化時,需要添加 panic 處理的 hook。這裡,我們讓 panic 發生後,做完正常的處理流程,就立刻結束整個進程。這樣,在 tokio 運行時某個線程中調用的 assert_eq! 被觸發並產生錯誤時,測試能夠正常退出並顯示測試錯誤。

同樣的,這個代碼也只需執行一次,所以也應該將其包裹在 std::sync::Once 中。

Rust 開發的心得

我認為 Rust 開發的一大好處是你可以不斷將代碼拆分成新的 crate,讓這些小的 crate 可以有自己完整的單元測試。這樣非常符合 SRP(Single Responsibility Principle)。在這個 POC 裡,我做的 Rust 側代碼:

你可以看到,我甚至為測試單獨創建了兩個 crate。我不敢說我的項目結構一定是合理的,但是類似的拆分思路可以讓我們很好地應對大型項目的需求,並且讓代碼很好擴展,很好測試。

我最大的心得還是在 protubuf 的使用上。

自從我在自己的一個實驗性質的項目 gitrocks 裡使用 protobuf 來做應用程式的主要的數據結構後,這一思想我已經運用得越來越嫻熟。對於 Rust 代碼來說,一個手工撰寫的 struct 和一個由 protobuf 生成出來的 struct,除了後者有一些限制外(比如不能用指針類的數據結構,如 Arc),本質是一樣的。而後者可以將數據高效地序列化/反序列化,並且在應用程式的多個版本之間安全無障礙地共享。

因此,現在我做任何一個新的 Rust 項目的流程是:

先定義項目中的 protos。項目都需要什麼數據結構,哪些結構可以用 protobuf 定義。項目中使用的所有 error 都在 protobuf 裡定義。

創建一個 protos crate。使用 prost 生成代碼並添加合適的 serde 支持。之後,為每個數據結構定義一些接口,如 new,以及各種 From 轉換,以便 into() 可以到處使用。

創建一個 errors crate。使用 thiserror 進行各種 error 的轉換,以及 protobuf 裡定義的 error 和 thiserror 定義的 error 的轉換(這下連 Error 也可以序列化並發送到其它地方)。

創建 fixtures crate。集中處理所有測試數據。

創建其它的項目邏輯,使用 protobuf 生成的數據結構。

Swift:被 apple 耽誤的好語言

最後,讓我好好吐槽一下 Swift 糟糕的生態。

作為一個 Swift 正式使用時間只能以天來計算的初學者來說,這個標題寫得對 apple 極為大不敬。

然而,我的 Swift 初體驗真的是可以用糟糕透頂來形容。

別會錯意,我不是說 Swift 語言本身。作為一個半吊子 Rust 開發者,當我寫了一兩百行真正的 Swift 代碼後,我便沉迷於這個語言的強大的表現力和簡單又優雅的語法。

但是,Swift 生態非常地支離破碎,稍微複雜一些的需求,就無法完成或者完成得非常彆扭。這和我學習 Rust 的體驗非常不一樣。

比如,連結一個 C 的靜態庫。Rust 你即便不知道怎麼做,stackoverflow 一下,你就能找到靠譜的答案,十分鐘搞定,毫無門檻。

Swift?OMG,讓人絕望。

至今我還沒有搞定在 Swift Package 裡如何使用一個靜態庫。

按照 apple 官方的說法,我可以創建 xcframework,然後在 Swift Package 裡引入 xcframework。

看似很簡單的任務。我用 Rust 編譯出了 linux / osx / iOS (arm) / iOS (x86_64 simulator) 幾個平臺的靜態庫,按照 apple 的官方文檔生成 xcframework,結果各種出錯。好吧,linux 在 aple 生態外,你不支持,無可厚非,我們暫且將其扔到一邊;iOS (arm) / iOS (x86_64) 也出錯,這是什麼鬼?同樣的靜態庫在 xcode 裡就可以正常編譯連結運行,為啥生成 xcframework 就報錯?難道 xcframework 不是親兒子?

好吧,osx 能夠正常打包,我們就在 xcframework 裡(暫時)只支持 osx 吧。

按照 apple 的官方文檔,我把 xcframework 放在 Swift Package 裡作為一個 binaryTarget,並在 target 中引用,照理來說該大功告成了吧?可 swift build 報錯。搜索了半天未果,後來我不得不就著錯誤消息查看了 Swift Package 的原始碼才解決了這個問題:

你敢相信這麼業餘的代碼是 apple 的工程師寫的麼?我們判斷一個庫是不是一個 static lib 竟然要靠它的命名是不是以 lib 開始?難道非標的靜態庫命名方式你就不工作了?好吧,我暫且認了,可是我用的是打包好的 xcframework 啊,我在創建 xcframework 時使用非標的 lib 命名方式,為啥你當時不給報個錯,讓我糾正過來,或者把 lib 名改成標準的名字呢?

吐槽歸吐槽,這不重要,我在 Rust 側構建時按照你要求改回來還不行麼?

這下,編譯通過了。然而,一旦我在代碼中引用靜態庫裡的函數,還是各種 symbol undefined 錯誤。我嘗試了各種論壇上幾乎各種方法,從 module.modulemap 到 bridging header,都無法正常編譯通過。

而如果我為這個 Swift package 創建一個 xcode 項目(swift package generate-xcodeproj),在 xcode 裡打開,添加 bridging header 就可以成功編譯。但是 xcode ... 不支持 linux 啊,你讓我如何開心地做 CI?畢竟,github action 等 CI 工具,osx 的價格是 linux 的十倍左右啊。

所以,我現在只能很無奈地本地用 xcodebuild test 做 precommit check,然後 CI 中禁用了 Swift 代碼的 build/test。讓一個 POC 代碼這麼消耗錢糧,不值當。

好的工具是很容易上手使用,而很難誤用。就我這兩天的體驗來說,在 WWDC 上大吹特吹的 xcframework 和被寄予希望的 Swift Package module,也許在整個 apple 的生態系統裡,工作得很好,然而一旦和更大的開源生態結合起來,還有很多路要走。

賢者時刻

上篇文章我引用了別人做的 JSON parsing 的數據,27M 的 JSON,Swift 花了 3s,而 Rust 花了 0.18s,二者 17 倍的差距。對於這個結果,不但有些讀者不相信,我自己也不敢相信。於是我弄了一個大 JSON,然後用 app.quicktype.io 上生成的數據結構,分別用 Rust 的 serde_json 和 Swift 自帶的 JSONDecoder() 測試,Rust 3.95ms,Swift 49.2ms,依然有 12 倍的差距。

參考資料

Unsafe Swift: Using Pointers and Interacting With C:https://www.raywenderlich.com/7181017-unsafe-swift-using-pointers-and-interacting-with-c

The power of extensions in Swift: https://www.swiftbysundell.com/articles/the-power-of-extensions-in-swift/

stack unwinding: https://www.bogotobogo.com/cplusplus/stackunwinding.php

相關焦點

  • web前端和後端的區別 web前端開發薪資
    web前端和後端的區別 Web前端: 顧名思義是來做Web的前端的。我們這裡所說的前端泛指Web前端,也就是在Web應用中用戶可以看得見碰得著的東西。包括Web頁面的結構、Web的外觀視覺表現以及Web層面的交互實現。
  • 24歲轉行選擇前端還是後端?看了西安匯傑的分析,我不再迷茫了
    也因為這場疫情,讓人們對網際網路IT行業有了高度的認識和讚賞,也引來了很多想要轉行從事這行的人,一般0基礎學員首先考慮的是IT行業最為基礎的崗位技術,比如前端和後端。西安匯傑今天來跟大家分析,轉行學習前端和後端哪個好?首先我們需要清楚,前端和後端的區別。
  • 初學編程,前端、後端開發哪個更有優勢?
    當你有這些疑問的時候,可以來看這篇文章,這篇文章詳細介紹了前端和後端開發技術的對比,從使用程式語言的不同,到對開發技能的要求,甚至未來的發展規劃都有詳細的介紹。什麼是前端開發?前端開發主要涉及到網站和APP,用戶能夠從App、瀏覽器上看到的東西都屬於前端。
  • 2020還分不清前端和後端的差別?看這篇就夠了
    2、前端特點:前端技術一般分為前端設計和前端開發,前端設計一般可以理解為網站的視覺設計,前端開發則是網站的前臺代碼實現,包括基本的HTML和CSS以及JavaScript/ajax,最新的高級版本HTML5、CSS3,以及SVG等。3、前端主要技術:HTML、CSS、JavaScript這三個是前端開發中最基本也是最必須的三個技能。
  • 為什麼越來越多前端工程師都轉向後端?
    前端工程師的地位在產品主導型的公司中,前端工程師的地位比較高,像騰訊、小米等,因為他們面向的是C端用戶,用戶體驗至上是他們的目標。而絕大部分的非C端用戶,對C端產品的要求則是得過且過。我覺個很常見的例子,面向政府業務的很多公司,最基本的產品配色都醜得一塌糊塗,那麼前端工程師的地位可想而知。
  • 前端和後端哪個工資高?
    IT行業的前端和後端哪個行業工資會更高一點兒呢?這個問題對於想要進入這個行業的人可能是有些不太清楚的,今天就詳細來說一下前端和後端的薪資情況。在整體這個行業上,大部分人都會覺得後端的薪資會比前端的要高,光從數據上看是沒有問題的。但是由於前端的反正沒有後端早,前端主要是10年左右才開始快速發展。在高級工程師上數量來說肯定是後端的工程師數量更多,整體的薪資也會更高一點兒,但是具體隨著工作時間的薪資變化是什麼樣的呢?
  • 一文看懂前端和後端開發
    這篇文章將對前端和後端開發技術做一個對比,先從基本的開始,然後逐步比較它們的不同點,比如對開發技能的要求、發展潛力、職業生涯的發展和薪水,等等。1、什麼是前端開發?前端開發主要涉及網站和 App,用戶能夠從 App 屏幕或瀏覽器上看到東西。簡單地說,能夠從 App 屏幕和瀏覽器上看到的東西都屬於前端。
  • 從事前端真的沒有後端工資高?
    知乎原題是這樣的:題主是一個大二學生,今天老師在講課時說到了以後的工作方向,然後說從工資來說,後端要比前端高,並且掌握公司核心技術的都是後端。有同學認為:產品的體驗重在產品的想法,前端只是實現而已。我認為,產品的體驗在於產品的想法確實也沒錯,但前端不僅僅只是實現而已。最終的產品形態往往是產品、交互、技術多方磨合的結果。
  • 在web開發中,為什麼前端比後端更得到轉行程式設計師的青睞?必看
    1、Web開發分類與區別 人們通常將Web分為前端和後端,前端相關的職位有前端設計師(UI/UE),前端開發工程師,後端相關的有後端開發工程師。
  • 為什麼web前端比後端薪資高?看完你就知道了!
    再看看後端開發工程師的薪資:看看後端這薪資真心不低呀!當然,這還取決於你的技術。在一線城市,有2年工作經驗的前端開發工程師工資差不多10K~15K,如果你會些後臺技術,並且前端技術很牛逼的話,20K是沒有問題的。
  • 長沙黑馬程式設計師:轉行IT,首先你得分得清前端、後端、全棧!
    前端的工作內容就是將美工設計的效果圖的設計成瀏覽器可以運行的網頁,並配合後端做網頁的數據顯示和交互等可視方面的工作內容。使用這些技術,前端開發者能連接起網站設計者和後端開發者之間的橋梁。他們能提供用戶體驗方面的分析,構建模型和線框,給設計團隊提出建議。他們能給後端編寫的服務應用賦予生命,提升格調,營造美感。
  • 數字IC後端實現TOP Floorplan專家秘籍
    今天想跟大家分享數字IC後端實現中關於top floorplan規劃的相關內容。Die Size確定我們知道,一個chip的形狀要麼是長方形,要麼是正方形,因此晶片的尺寸取決於它的長和寬的大小。對於相同大小的晶片,可以做成方形的,可以做成瘦高型,也可以做成矮胖型。
  • 酷工作丨安徳醫智科技招前端/後端/測試等崗位、堅果智能校招/社招、Sea Group 前端/後端/測試/數據
    (移動端、web端)進行功能、兼容性等測試,保障產品質量;2.根據產品需求和設計文檔,設計多角度、高覆蓋率的測試計劃和用例;3.快速高效執行測試用例,跟蹤測試中發現的問題,積極推動相關人員解決;4.輸出測試報告,分析測試結果,提高測試質量和效率。
  • 前端和後端哪個工資高,哪個比較好學-開課吧
    首先在了解前端和後端薪資之前需要明確這兩者分別是什麼,在公司中起什麼作用。零基礎學編程Web前端:web前端開發主要是通過html、css、js、ajax、DOM等前端技術,實現網站在客服端的正確顯示及交互功能
  • 數字後端實現place過程進階
    那麼我們在利用工具做place時,數字後端實現工程師應該注意哪些事項,如何引導工具去實現一個相對」完美「的結果呢?今天,小編就基於以往的項目經驗做一個簡要的分享。在數字後端布局布線APR中,placement階段處於Design Planning和CTS之間,如圖1所示。
  • 想找IT培訓,前端好還是後端好?
    圍繞這個問題前端和後端項目部進行了一場「辯論賽」我在這兒整理並分享給大家關於後端:1後端是一個系統通常是看不見的但是卻很重要類似電腦的核心處理器2如果一個後端系統出現不穩定調試是非常麻煩通常你可以首選日誌
  • 瀏覽器中實現深度學習?有人分析了7個基於JS語言的DL框架
    不過,是不是我們現在就可以隨心所欲的在瀏覽器中運行 DL 的模型或算法了?我們已經成功邁入在瀏覽器中實現深度學習的時代了麼?儘管上面介紹的內容似乎意味著使在瀏覽器中運行 DL 任務成為可能,但是目前對於可以執行哪些 DL 任務以及 DL 在瀏覽器中的工作效果卻缺少深入的研究和分析。
  • python前端和後端數據交互,tornado框架入門,初學小試牛刀!
    Python前端和後端是如何交互的,怎麼用tornado框架快速搭建前端和後端數據交互?前端與後端的數據交互,最常用的就是GET、POST,比較常用的用法是:提交表單數據到後端,後端返回json前端的數據發送與接收1)提交表單數據2)提交JSON數據後端的數據接收與響應
  • 前端1年學了node等7門技術感覺累!網友:這些我做後端的都知道!
    關於web開發,在第一代web開發時,基本上都是靜態網頁,大多數是html,連js都很少寫,那時候還是後端開發工程師的天下,前端還稱不上開發工程師,當時覺得前端很弱的樣子,可什麼事情都不是一成不變,自從移動網際網路時代的到來和html5問世,前端頁面的表現形式越來越多樣化,各種前端技術,前端框架紛紛出現,關於前後端的技術比重又重新洗牌,前端的地位逐漸與後端均衡甚至有超越後端開發的趨勢
  • 前端開發的初學者是否需要學習後端知識
    首先,前端開發可以劃分為三個開發階段,第一個階段的前端開發任務往往由後端開發人員來完成,此時的前端開發並不複雜,任務量也相對比較小,第二個階段是Web2.0時代,前端開發的團隊規模有了明顯的擴大,前端開發崗位得到了快速的發展,整個Web開發也開始走角色化開發路線,第三個階段是移動網際網路時代背景下