來勢兇猛 看T60如何演繹皮卡新時代

在傳統思維上,也許你覺得智能化和皮卡根本毫無瓜葛。T60打破了這種陳建,首創在皮卡上開發了Normal/Eco/power三種駕駛模式。在輕載或行駛在平坦路面時,選擇ECO模式,以達到更好的燃油經濟性。在山路、工地等複雜路況或重載時,選擇power模式,帶來更強勁的動力。

在中國,皮卡一直以邊緣車型處在一個尷尬境地,各類歧視性政策更是讓實用價值很高的皮卡被人們敬而遠之,和國外濃厚的皮卡文化相比,我們仍然處於一個嬰兒般的市場狀態,如何培育,如何增長,如何推廣,都成了皮卡的廠商一個大難題,而且中國皮卡造型多以中庸的外觀出現,講究皮實耐用,與國內80后、90后追求外觀、個性的性格有些格格不入,所以,皮卡車企如何適應當前社會潮流,推出適合當下年輕人喜歡的中高端皮卡,將是刺激當前皮卡市場的一劑強心劑,也是一個對於廠商來說一個最大的突破口。

縱觀國內皮卡市場,雖然每年有着40萬輛左右的銷量,但產品的各方面整體較落後,市場上缺少一款性能卓越,讓用戶滿意的產品。在此背景下,上汽T60的誕生就是為了引領並推動國內皮卡市場。

12月21日,上汽大通邀經銷商夥伴聚集海南博鰲,回顧輝煌業績,共贏未來。回望2016年,在今年的廣州車展上,上汽大通推出了“划時代皮卡”——T60,當時只是公布了售價區間為9.98-19.98萬元。

與會期間,上汽大通為大家揭開了最後的謎底,官方公布T60各款車型的價格

T60柴油兩驅低底盤價格表

T60柴油兩驅高底盤價格表

T60柴油四驅高底盤價格表

突破傳統,時尚觀感遇見智能互聯

T60在外觀造型上有了重大突破,在我們之前的印象中,皮卡一向講求“內修”只要實用,空間大,力量足就行,但是T60確實是做到了“內外兼修”摒棄此前皮卡車過於單調、落後的特點,用迎合年輕一代用車群體的需求作為設計出發點,這款T60皮卡有着眾多獨特個性與亮點,稜角分明的前臉造型線條十分豐富,直瀑式進氣格柵突出了皮卡車型硬朗風格的同時也融入現代SUV的時尚元素。多邊形前大燈組造型,近光燈帶有透鏡結構,保險杠底部的霧燈則顯得十分秀氣。直觀的感受,T60帶來一款讓人耳目一新的皮卡。

在內飾設計上,T60整體造型顯得十分簡潔,全車黑色搭配銀色鍍鉻飾條,T形對稱式中控檯布局,大尺寸的液晶显示屏搭載了上汽與阿里共同開發的YunOS車聯網系統,提供智能車管家、在線互聯、遠程控制、語音識別等智能功能,這些科技的配置完全不輸一台B級車的配置了,而且皮卡上配備了pEpS無鑰匙進入一鍵啟動功能,這樣的皮卡配置確實有些無敵了。

作為一款皮卡,動力方面是其必須具備的,畢竟它的主要角色擔當是裝載及運輸,所以對於動力的要求會很高,那T60搭載了來自意大利VM的2.8T柴油發動機,其最大功率為110千瓦,最大扭矩360牛米,這樣的動力是足夠消費者輕鬆應對各種繁重任務了。

值得一提的,T60首次在皮卡上配備了6AT自動變速箱,這讓其在彎道或變速方面更加應付自如。

在傳統思維上,也許你覺得智能化和皮卡根本毫無瓜葛。T60打破了這種陳建,首創在皮卡上開發了Normal/Eco/power三種駕駛模式。在輕載或行駛在平坦路面時,選擇ECO模式,以達到更好的燃油經濟性;在山路、工地等複雜路況或重載時,選擇power模式,帶來更強勁的動力。這種改變,大大滿足了越來越多的年輕用戶希望有更輕鬆的駕駛體驗的需求。

最後,不得不提是T60在安全配置方面的專家級表現,T60以澳洲A-NCAp五星碰撞安全標準進行設計,首次將熱成型技術運用於皮卡,並通過激光焊接,大幅提升車身強度;T60還率先採用6安全氣囊,為駕乘人員提供全方位的保護。

T60首次在皮卡上配備,AFS隨動轉向LED大燈,根據車輛轉向,自動調節大燈照射角度,為用戶夜晚行車,提供更寬闊的照明視野。

在面對雨雪泥濘、崎嶇山路行駛時出現甩尾、側滑等險情時, T60採用BOSCH 9.0,ESp車身穩定控制系統,給皮卡用戶在各種工況下,帶來更好的行車穩定性。並把360環視影像、LDW車道偏離預警、疲勞駕駛提醒,等多項安全配置,首次應用在皮卡上,提供全方位的安全保障。

在美國,皮卡代表了張揚、自由與個性,涵蓋了美國英雄主義的西部文化,駕駛皮卡犹如之前的騎馬一樣輕鬆自然,而在中國恰恰缺少美國一樣的皮卡文化。

隨着皮卡市場的春天來臨,大家對皮卡的關注與討論與日俱增。同時,上汽大通T60更是憑藉行業首創的C2B理念,提供最全面的產品型譜和組合,上汽大通T60作為一輛外觀時尚、性能卓越、注重安全的皮卡,將會引領皮卡市場走向宜商宜家的高端路線。這樣一款划時代的皮卡,讓我們更加期待其精彩的市場表現。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

車輪上的家 上汽大通發布全新房車RV80

98萬的價格。而且RV80型譜包含B型和C型房車,擁有短軸/長軸/加長軸,低頂/中頂/高頂多種車身形式,用戶可通過C2B線上交互平台,可直接在線訂製房車,OTD高效透明,消費者可參与試駕、試住等全體驗式互動,享受真正的價值和樂趣。

買一輛房車,開到郊外,給自己一個遠離城市堵車和污染的樂土,傍晚一邊欣賞湖邊那一抹落日一邊和家人烹制一頓可口的晚餐;晚上透過大大的天窗看沒有污染的星空;亦或把車停在海邊,換上泳衣下海衝浪,回來后在寬敞的洗澡間沖個熱水澡這份嚮往其實不僅僅是一輛車,而是一種生活方式更是一種房車生活的文化,這才是一種說走就走的旅行,因為不用規劃路線,不用訂酒店,風景在那,走到那,家在路上。

近日,一場融新款房車上市發布、房車露營大會、草地音樂狂歡節、美酒佳肴於一體的“全體驗式”上市發布會登陸海南博鰲,由“中國房車第一品牌”上汽大通打造的2016“新旅程·心享受”房車之夜暨房車新品上市發布會亮相南海之濱,全新房車RV80在萬眾期待中揭開神秘面紗。

當晚,配合現場炫目的燈光秀,國內首款滿足國五排放標準的高端房車——上汽大通全新房車RV80正式發布。

新款房車憑藉六大產品優勢,定義房車行業新標杆,其搭載源自歐洲的VM柴油發動機,最高功率110KW,綜合油耗百公里僅為8.3L;全系國5排放標準;在國內房車行業中率先使用AMT技術;而且全車座椅是達乘用級安全標準。

消費者最為關心的價格也在上市會上正式揭曉,售:26.98萬至51.98萬的價格。

而且RV80型譜包含B型和C型房車,擁有短軸/長軸/加長軸,低頂/中頂/高頂多種車身形式,用戶可通過C2B線上交互平台,可直接在線訂製房車,OTD高效透明,消費者可參与試駕、試住等全體驗式互動,享受真正的價值和樂趣。

雖然國內房車起步晚,房車少、營地少,在心往來說,移動露營來說比較難實現,但隨着房車市場近幾年的發展,房車露營地的逐漸增多,露營地的規劃與擬建逐漸完善,移動露營的實現,在今天已經變的越來越便利了。

房車,又稱“車輪上的家”,兼具“房”與“車”兩大功能,但其屬性還是車,相比普通汽車,房車上的居家設施有爐具、冰箱、櫥櫃、沙發、餐桌椅、盥洗設施、空調、電視、音響等傢具和電器等,隨心而行,隨景而歇,這是一種房車的生活狀態。

購買房車的理由可以很多,可能是為了帶着父母去旅行,也可能是為了滿足夫妻雙方共同的旅行夢想,也可以一群朋友同事用懷揣探索的心踏上未知的旅途,無論是兩個人駕駛房車出行,要過一次久違的二人世界或帶着一家人遠離都市的喧鬧,這都是我們嚮往的生活,而房車是對於這種嚮往的一個載體,一個最適合的載體。

國內消費者對待房車的消費觀念正在發生轉變,讓目前的房車市場顯得更為火爆,現在很多人的旅遊觀念都在轉變,更加傾向於自駕出遊,而非報團旅行。而自駕的長途旅行,使得房車成為了旅行者的首選。

但相應的配套設施卻略顯單薄,最大的制約就是目前國內房車露營地較少,無法為房車提供停放場所。畢竟在房車內生活,做飯、取暖、照明等都需要電力和水源,雖然房車裡自帶有蓄電池和水源儲蓄,但容量的限制最多只能保證一兩個晚上的生活需求,這也是制約房車發展最重要的一個條件。

對於現在房車的各方面使用性來說,可能有些人覺的買了回來,使用率比較低,上汽大通也在2017年投入超過1200輛房車,試水房車租賃市場,與運營夥伴在全國鋪開租賃網絡。

讓用戶輕鬆體驗到大通房車落地自駕的樂趣;開發了“房車生活家”平台,為房車用戶和潛客用戶提供“@房車”、“@生活”、“@家”三個方面的服務,構建多個使用和體驗場景,實現在線房車自駕游線路推薦和預訂、營地推薦和預訂,閱讀攻略遊記、預約試駕體驗、參加自駕活動等功能。

也可掃二維碼了解更多房車生活及房車租賃信息,而且還有很多優惠信息喔!

@房車生活家本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

別唧唧歪歪了!年輕人第一台SUV就該選TA!

但相信2765mm的軸距將會給用戶一個滿意的乘坐空間。後排無論在橫向還是縱向空間都是足夠大而舒適,頭部空間則因車頂後排頭頂位置為內凹設計,也是很讓人滿意。“低”價格,高配置啟辰T90的內飾給人的感覺跟外觀還是有些不一樣,整體的風格偏向於穩重,但穩重中也沒有忽略運動感的塑造。

在目前的國內市場,SUV尤其的火爆,可以這麼說,當人們有需求的時候,市場就會自動做出調整生產符合消費者要求的產品。在這種情況下,很多廠家也紛紛都將矛頭指向了緊湊型的SUV,以及二胎政策捧紅的7座中型SUV。然而,啟辰獨辟蹊徑,瞄準了85后消費群體,打造了溜背式的中型SUV啟辰T90,憑藉媲美合資車的生產品質、顛覆傳統的驚艷設計、充滿競爭力的價格,勢必成為中型SUV現有市場格局的全新標杆。

12月25日,啟辰全新中型SUV啟辰T90正式上市,售價區間為10.98—15.48萬元,共推出6款車型。

個性時尚的外觀

啟辰T90最大的亮點可以說是外觀造型,無論是前臉還是性感的“背影”,開這麼一款SUV在路上,回頭率都會是極高的。其採用了全新的家族式前臉和溜背式SUV的風雕美學設計。不是華麗的簡單堆砌,而是經過設計師無數次對細節完美追求的結果。總體來看,不管是在研發設計還是產品定位上,啟辰T90的設計感並不亞於一些合資品牌。

寬敞舒適的空間

說到這裏,也許有人會問,採用溜背造型的設計,其後排空間會不會就因此受到影響,要說完全沒有,這個有點不現實。但相信2765mm的軸距將會給用戶一個滿意的乘坐空間。後排無論在橫向還是縱向空間都是足夠大而舒適,頭部空間則因車頂後排頭頂位置為內凹設計,也是很讓人滿意。

“低”價格,高配置

啟辰T90的內飾給人的感覺跟外觀還是有些不一樣,整體的風格偏向於穩重,但穩重中也沒有忽略運動感的塑造。值得一提的還有,中控台上12.3英寸的大屏,從布局上就可以看得出設計的用心,其呈環抱式位置分佈,主駕駛席無需起身即可完成所有操作;真皮座椅,搭配着藍色的氛圍燈,精密的雙縫線,各處接縫的高度契合,使內飾整體更有質感,是現代年輕一代受眾所青睞的。

除了以上提到的,還有智能互聯、高科技配置和安全配置等方面,啟辰T90也展示出非凡的實力,可以說是智超所值。IT-COMMANDER旋鈕式信息控制系統堪稱同級SUV罕有的“鼠標”式車載控制系統,集便利性、簡易性、精美性於一體,觸控隨心所欲;全彩3D平視信息显示系統以駕駛者為中心,操控簡潔。啟辰T90還具備駕駛提醒、胎壓監測、泊車輔助等功能,在白色高亮質感照明和多功能方向盤按鍵控制下,能夠帶來更清晰、更科技、更好操作的显示效果。

總結:如今中型SUV市場,早已是一片紅海,啟辰T90無論是獨樹一幟的外觀,還是空間、配置,以及親民的價格,都可以說,將會讓它在這片紅海中,逆襲上位、脫穎而出,成為業內的一個新標杆。期待在未來,啟辰T90成為下一个中型SUV爆款。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

akka-typed(6) – cluster:group router, cluster-load-balancing

先談談akka-typed的router actor。route 分pool router, group router兩類。我們先看看pool-router的使用示範:

      val pool = Routers.pool(poolSize = 4)( // make sure the workers are restarted if they fail
 Behaviors.supervise(WorkerRoutee()).onFailure[Exception](SupervisorStrategy.restart)) val router = ctx.spawn(pool, "worker-pool") (0 to 10).foreach { n => router ! WorkerRoutee.DoLog(s"msg $n")
      }

上面例子里的pool是個pool-router,意思是一個有4個routees的routee池。每個routee都是通過WorkerRoutee()構建的,意味着routee池中只有一個種類的actor。pool-router是通過工廠方法直接在本地(JVM)構建(spawn)所有的routee。也就是說所有routee都是router的子actor。

再看看group-router的使用例子:

val serviceKey = ServiceKey[Worker.Command]("log-worker") // this would likely happen elsewhere - if we create it locally we // can just as well use a pool
      val workerRoutee = ctx.spawn(WorkerRoutee(), "worker-route") ctx.system.receptionist ! Receptionist.Register(serviceKey, workerRoutee) val group = Routers.group(serviceKey) val router = ctx.spawn(group, "worker-group") // the group router will stash messages until it sees the first listing of registered // services from the receptionist, so it is safe to send messages right away
      (0 to 10).foreach { n => router ! WorkerRoutee.DoLog(s"msg $n") }

group-router與pool-router有較多分別:

1、routee是在router之外構建的,router是用一個key通過Receptionist獲取同key的actor清單作為routee group的

2、Receptionist是集群全局的。任何節點上的actor都可以發送註冊消息在Receptionist上登記

3、沒有size限制,任何actor一旦在Receptionist上登記即變成routee,接受router管理

應該說如果想把運算任務分配在集群里的各節點上并行運算實現load-balance效果,group-router是最合適的選擇。不過對不同的運算任務需要多少routee則需要用戶自行決定,不像以前akka-classic里通過cluster-metrics根據節點負載情況自動增減routee實例那麼方便。

Receptionist: 既然說到,那麼就再深入一點介紹Receptionist的應用:上面提到,Receptionist是集群全局的。就是說任何節點上的actor都可以在Receptonist上註冊形成一個生存在集群中不同節點的actor清單。如果Receptionist把這個清單提供給一個用戶,那麼這個用戶就可以把運算任務配置到各節點上,實現某種意義上的分佈式運算模式。Receptionist的使用方式是:通過向本節點的Receptionist發送消息去登記ActorRef,然後通過Receptionist發布的登記變化消息即可獲取最新的ActorRef清單:

  val WorkerServiceKey = ServiceKey[Worker.TransformText]("Worker") ctx.system.receptionist ! Receptionist.Register(WorkerServiceKey, ctx.self) ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter)

Receptionist的登記和清單獲取是以ServiceKey作為關聯的。那麼獲取的清單內應該全部是一種類型的actor,只不過它們的地址可能是跨節點的,但它們只能進行同一種運算。從另一個角度說,一項任務是分佈在不同節點的actor并行進行運算的。

在上篇討論里提過:如果發布-訂閱機制是在兩個actor之間進行的,那麼這兩個actor也需要在規定的信息交流協議框架下作業:我們必須注意消息類型,提供必要的消息類型轉換機制。下面是一個Receptionist登記示範:

object Worker { val WorkerServiceKey = ServiceKey[Worker.TransformText]("Worker") sealed trait Command final case class TransformText(text: String, replyTo: ActorRef[TextTransformed]) extends Command with CborSerializable final case class TextTransformed(text: String) extends CborSerializable def apply(): Behavior[Command] = Behaviors.setup { ctx =>
      // each worker registers themselves with the receptionist
      ctx.log.info("Registering myself with receptionist") ctx.system.receptionist ! Receptionist.Register(WorkerServiceKey, ctx.self) Behaviors.receiveMessage { case TransformText(text, replyTo) => replyTo ! TextTransformed(text.toUpperCase) Behaviors.same } } }

Receptionist登記比較直接:登記者不需要Receptionist返回消息,所以隨便用ctx.self作為消息的sender。注意TransformText的replyTo: ActorRef[TextTransformed],代表sender是個可以處理TextTransformed消息類型的actor。實際上,在sender方是通過ctx.ask提供了TextTransformed的類型轉換。

Receptionist.Subscribe需要Receptionist返回一個actor清單,所以是個request/response模式。那麼發送給Receptionist消息中的replyTo必須是發送者能處理的類型,如下:

  def apply(): Behavior[Event] = Behaviors.setup { ctx => Behaviors.withTimers { timers =>
      // subscribe to available workers
      val subscriptionAdapter = ctx.messageAdapter[Receptionist.Listing] { case Worker.WorkerServiceKey.Listing(workers) => WorkersUpdated(workers) } ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter) ... } 

ctx.messageAdapter進行了一個從Receptionist.Listing返回類型到WorkersUpdated類型的轉換機制登記:從Receptionist回復的List類型會被轉換成WorkersUpdated類型,如下:

... Behaviors.receiveMessage { case WorkersUpdated(newWorkers) => ctx.log.info("List of services registered with the receptionist changed: {}", newWorkers) ...

另外,上面提過的TextTransformed轉換如下:

 ctx.ask[Worker.TransformText,Worker.TextTransformed](selectedWorker, Worker.TransformText(text, _)) { case Success(transformedText) => TransformCompleted(transformedText.text, text) case Failure(ex) => JobFailed("Processing timed out", text) }

ctx.ask將TextTransformed轉換成TransformCompleted。完整的Behavior定義如下:

object Frontend { sealed trait Event private case object Tick extends Event private final case class WorkersUpdated(newWorkers: Set[ActorRef[Worker.TransformText]]) extends Event private final case class TransformCompleted(originalText: String, transformedText: String) extends Event private final case class JobFailed(why: String, text: String) extends Event def apply(): Behavior[Event] = Behaviors.setup { ctx => Behaviors.withTimers { timers =>
      // subscribe to available workers
      val subscriptionAdapter = ctx.messageAdapter[Receptionist.Listing] { case Worker.WorkerServiceKey.Listing(workers) => WorkersUpdated(workers) } ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter) timers.startTimerWithFixedDelay(Tick, Tick, 2.seconds) running(ctx, IndexedSeq.empty, jobCounter = 0) } } private def running(ctx: ActorContext[Event], workers: IndexedSeq[ActorRef[Worker.TransformText]], jobCounter: Int): Behavior[Event] = Behaviors.receiveMessage { case WorkersUpdated(newWorkers) => ctx.log.info("List of services registered with the receptionist changed: {}", newWorkers) running(ctx, newWorkers.toIndexedSeq, jobCounter) case Tick =>
        if (workers.isEmpty) { ctx.log.warn("Got tick request but no workers available, not sending any work") Behaviors.same } else { // how much time can pass before we consider a request failed
          implicit val timeout: Timeout = 5.seconds val selectedWorker = workers(jobCounter % workers.size) ctx.log.info("Sending work for processing to {}", selectedWorker) val text = s"hello-$jobCounter" ctx.ask[Worker.TransformText,Worker.TextTransformed](selectedWorker, Worker.TransformText(text, _)) { case Success(transformedText) => TransformCompleted(transformedText.text, text) case Failure(ex) => JobFailed("Processing timed out", text) } running(ctx, workers, jobCounter + 1) } case TransformCompleted(originalText, transformedText) => ctx.log.info("Got completed transform of {}: {}", originalText, transformedText) Behaviors.same case JobFailed(why, text) => ctx.log.warn("Transformation of text {} failed. Because: {}", text, why) Behaviors.same }

現在我們可以示範用group-router來實現某種跨節點的分佈式運算。因為group-router是通過Receptionist來實現對routees管理的,而Receptionist是集群全局的,意味着如果我們在各節點上構建routee,然後向Receptionist登記,就會形成一個跨節點的routee ActorRef清單。如果把任務分配到這個清單上的routee上去運算,應該能實現集群節點負載均衡的效果。下面我們就示範這個loadbalancer。流程很簡單:在一個接入點 (serviceActor)中構建workersRouter,然後3個workerRoutee並向Receptionist登記,把接到的任務分解成子任務逐個發送給workersRouter。每個workerRoutee完成任務后將結果發送給一個聚合器Aggregator,Aggregator在核對完成接收所有workerRoutee返回的結果后再把匯總結果返回serverActor。先看看這個serverActor:

object Service { val routerServiceKey = ServiceKey[WorkerRoutee.Command]("workers-router") sealed trait Command extends CborSerializable case class ProcessText(text: String) extends Command { require(text.nonEmpty) } case class WrappedResult(res: Aggregator.Response) extends Command def serviceBehavior(workersRouter: ActorRef[WorkerRoutee.Command]): Behavior[Command] = Behaviors.setup[Command] { ctx => val aggregator = ctx.spawn(Aggregator(), "aggregator") val aggregatorRef: ActorRef[Aggregator.Response] = ctx.messageAdapter(WrappedResult) Behaviors.receiveMessage { case ProcessText(text) => ctx.log.info("******************** received ProcessText command: {} ****************",text) val words = text.split(' ').toIndexedSeq aggregator ! Aggregator.CountText(words.size, aggregatorRef) words.foreach { word => workersRouter ! WorkerRoutee.Count(word, aggregator) } Behaviors.same case WrappedResult(msg) => msg match { case Aggregator.Result(res) => ctx.log.info("************** mean length of words = {} **********", res) } Behaviors.same } } def singletonService(ctx: ActorContext[Command], workersRouter: ActorRef[WorkerRoutee.Command]) = { val singletonSettings = ClusterSingletonSettings(ctx.system) .withRole("front") SingletonActor( Behaviors.supervise( serviceBehavior(workersRouter) ).onFailure( SupervisorStrategy .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1) .withMaxRestarts(3) .withResetBackoffAfter(10.seconds) ) , "singletonActor" ).withSettings(singletonSettings) } def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx => val cluster = Cluster(ctx.system) val workersRouter = ctx.spawn( Routers.group(routerServiceKey) .withRoundRobinRouting(), "workersRouter" ) (0 until 3).foreach { n => val routee = ctx.spawn(WorkerRoutee(cluster.selfMember.address.toString), s"work-routee-$n") ctx.system.receptionist ! Receptionist.register(routerServiceKey, routee) } val singletonActor = ClusterSingleton(ctx.system).init(singletonService(ctx, workersRouter)) Behaviors.receiveMessage { case job@ProcessText(text) => singletonActor ! job Behaviors.same } } }

整體goup-router和routee的構建是在apply()里,並把接到的任務轉發給singletonActor。singletonActor是以serviceBehavior為核心的一個actor。在servceBehavior里把收到的任務分解並分別發送給workersRouter。值得注意的是:serviceBehavior期望接收從Aggregator的回應,它們之間存在request/response模式信息交流,所以需要Aggregator.Response到WrappedResult的類型轉換機制。還有:子任務是通過workersRoute發送給個workerRoutee的,我們需要各workerRoutee把運算結果返給給Aggregator,所以發送給workersRouter的消息包含了Aggregator的ActorRef,如:workersRouter ! WorkerRoutee.Count(cnt,aggregatorRef)。

Aggregator是個persistentActor, 如下:

 

object Aggregator { sealed trait Command sealed trait Event extends CborSerializable sealed trait Response case class CountText(cnt: Int, replyTo: ActorRef[Response]) extends Command case class MarkLength(word: String, len: Int) extends Command case class TextCounted(cnt: Int) extends Event case class LengthMarked(word: String, len: Int) extends Event case class Result(meanWordLength: Double) extends Response case class State(expectedNum: Int = 0, lens: List[Int] = Nil) var replyTo: ActorRef[Response] = _ def commandHandler: (State,Command) => Effect[Event,State] = (st,cmd) => { cmd match { case CountText(cnt,ref) => replyTo = ref Effect.persist(TextCounted(cnt)) case MarkLength(word,len) => Effect.persist(LengthMarked(word,len)) } } def eventHandler: (State, Event) => State = (st,ev) => { ev match { case TextCounted(cnt) => st.copy(expectedNum = cnt, lens = Nil) case LengthMarked(word,len) => val state = st.copy(lens = len :: st.lens) if (state.lens.size >= state.expectedNum) { val meanWordLength = state.lens.sum.toDouble / state.lens.size replyTo ! Result(meanWordLength) State() } else state } } val takeSnapShot: (State,Event,Long) => Boolean = (st,ev,seq) => { if (st.lens.isEmpty) { if (ev.isInstanceOf[LengthMarked]) true
          else
            false } else
         false } def apply(): Behavior[Command] = Behaviors.supervise( Behaviors.setup[Command] { ctx => EventSourcedBehavior( persistenceId = PersistenceId("33","2333"), emptyState = State(), commandHandler = commandHandler, eventHandler = eventHandler ).onPersistFailure( SupervisorStrategy .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1) .withMaxRestarts(3) .withResetBackoffAfter(10.seconds) ).receiveSignal { case (state, RecoveryCompleted) => ctx.log.info("**************Recovery Completed with state: {}***************",state) case (state, SnapshotCompleted(meta))  => ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr) case (state,RecoveryFailed(err)) => ctx.log.error("*************recovery failed with: {}***************",err.getMessage) case (state,SnapshotFailed(meta,err)) => ctx.log.error("***************snapshoting failed with: {}*************",err.getMessage) }.snapshotWhen(takeSnapShot) } ).onFailure( SupervisorStrategy .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1) .withMaxRestarts(3) .withResetBackoffAfter(10.seconds) ) }

注意這個takeSnapShot函數:這個函數是在EventSourcedBehavior.snapshotWhen(takeSnapShot)調用的。傳入參數是(State,Event,seqenceNr),我們需要對State,Event的當前值進行分析后返回true代表做一次snapshot。

看看一部分显示就知道任務已經分配到幾個節點上的routee:

20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-15] INFO com.learn.akka.WorkerRoutee$ - ************** processing [this] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [text] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-36] INFO com.learn.akka.WorkerRoutee$ - ************** processing [be] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-16] INFO com.learn.akka.WorkerRoutee$ - ************** processing [will] on akka://ClusterSystem@127.0.0.1:51173 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-26] INFO com.learn.akka.WorkerRoutee$ - ************** processing [is] on akka://ClusterSystem@127.0.0.1:25251 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-13] INFO com.learn.akka.WorkerRoutee$ - ************** processing [the] on akka://ClusterSystem@127.0.0.1:51173 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [that] on akka://ClusterSystem@127.0.0.1:25251 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [analyzed] on akka://ClusterSystem@127.0.0.1:25251 ***********

這個例子的源代碼如下:

package com.learn.akka

import akka.actor.typed._
import akka.persistence.typed._
import akka.persistence.typed.scaladsl._
import scala.concurrent.duration._
import akka.actor.typed.receptionist._
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl._
import akka.cluster.typed.Cluster
import akka.cluster.typed.ClusterSingleton
import akka.cluster.typed.ClusterSingletonSettings
import akka.cluster.typed.SingletonActor
import com.typesafe.config.ConfigFactory

object WorkerRoutee {
  sealed trait Command extends CborSerializable
  case class Count(word: String, replyTo: ActorRef[Aggregator.Command]) extends Command

  def apply(nodeAddress: String): Behavior[Command] = Behaviors.setup {ctx =>
    Behaviors.receiveMessage[Command] {
      case Count(word,replyTo) =>
        ctx.log.info("************** processing [{}] on {} ***********",word,nodeAddress)
        replyTo ! Aggregator.MarkLength(word,word.length)
        Behaviors.same
    }
  }
}
object Aggregator {
  sealed trait Command
  sealed trait Event extends  CborSerializable
  sealed trait Response

  case class CountText(cnt: Int, replyTo: ActorRef[Response]) extends Command
  case class MarkLength(word: String, len: Int) extends Command
  case class TextCounted(cnt: Int) extends Event
  case class LengthMarked(word: String, len: Int) extends Event
  case class Result(meanWordLength: Double) extends Response

  case class State(expectedNum: Int = 0, lens: List[Int] = Nil)

  var replyTo: ActorRef[Response] = _

  def commandHandler: (State,Command) => Effect[Event,State] = (st,cmd) => {
    cmd match {
      case CountText(cnt,ref) =>
        replyTo = ref
        Effect.persist(TextCounted(cnt))
      case MarkLength(word,len) =>
        Effect.persist(LengthMarked(word,len))
    }
  }
  def eventHandler: (State, Event) => State = (st,ev) => {
    ev match {
      case TextCounted(cnt) =>
        st.copy(expectedNum = cnt, lens = Nil)
      case LengthMarked(word,len) =>
        val state = st.copy(lens = len :: st.lens)
        if (state.lens.size >= state.expectedNum) {
          val meanWordLength = state.lens.sum.toDouble / state.lens.size
          replyTo ! Result(meanWordLength)
          State()
        } else state
    }
  }
  val takeSnapShot: (State,Event,Long) => Boolean = (st,ev,seq) => {
      if (st.lens.isEmpty) {
          if (ev.isInstanceOf[LengthMarked])
            true
          else
            false
      } else
         false
  }
  def apply(): Behavior[Command] = Behaviors.supervise(
    Behaviors.setup[Command] { ctx =>
      EventSourcedBehavior(
        persistenceId = PersistenceId("33","2333"),
        emptyState = State(),
        commandHandler = commandHandler,
        eventHandler = eventHandler
      ).onPersistFailure(
        SupervisorStrategy
          .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
          .withMaxRestarts(3)
          .withResetBackoffAfter(10.seconds)
      ).receiveSignal {
        case (state, RecoveryCompleted) =>
          ctx.log.info("**************Recovery Completed with state: {}***************",state)
        case (state, SnapshotCompleted(meta))  =>
          ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
        case (state,RecoveryFailed(err)) =>
          ctx.log.error("*************recovery failed with: {}***************",err.getMessage)
        case (state,SnapshotFailed(meta,err)) =>
          ctx.log.error("***************snapshoting failed with: {}*************",err.getMessage)
      }.snapshotWhen(takeSnapShot)
    }
  ).onFailure(
    SupervisorStrategy
      .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
      .withMaxRestarts(3)
      .withResetBackoffAfter(10.seconds)
  )
}
object Service {
  val routerServiceKey = ServiceKey[WorkerRoutee.Command]("workers-router")

  sealed trait Command extends CborSerializable

  case class ProcessText(text: String) extends Command {
    require(text.nonEmpty)
  }

  case class WrappedResult(res: Aggregator.Response) extends Command

  def serviceBehavior(workersRouter: ActorRef[WorkerRoutee.Command]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
    val aggregator = ctx.spawn(Aggregator(), "aggregator")
    val aggregatorRef: ActorRef[Aggregator.Response] = ctx.messageAdapter(WrappedResult)
    Behaviors.receiveMessage {
      case ProcessText(text) =>
        ctx.log.info("******************** received ProcessText command: {} ****************",text)
        val words = text.split(' ').toIndexedSeq
        aggregator ! Aggregator.CountText(words.size, aggregatorRef)
        words.foreach { word =>
          workersRouter ! WorkerRoutee.Count(word, aggregator)
        }
        Behaviors.same
      case WrappedResult(msg) =>
        msg match {
          case Aggregator.Result(res) =>
            ctx.log.info("************** mean length of words = {} **********", res)
        }
        Behaviors.same
    }
  }

  def singletonService(ctx: ActorContext[Command], workersRouter: ActorRef[WorkerRoutee.Command]) = {
    val singletonSettings = ClusterSingletonSettings(ctx.system)
      .withRole("front")
    SingletonActor(
      Behaviors.supervise(
        serviceBehavior(workersRouter)
      ).onFailure(
        SupervisorStrategy
          .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
          .withMaxRestarts(3)
          .withResetBackoffAfter(10.seconds)
      )
      , "singletonActor"
    ).withSettings(singletonSettings)
  }

  def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx =>
    val cluster = Cluster(ctx.system)
    val workersRouter = ctx.spawn(
      Routers.group(routerServiceKey)
        .withRoundRobinRouting(),
      "workersRouter"
    )
    (0 until 3).foreach { n =>
      val routee = ctx.spawn(WorkerRoutee(cluster.selfMember.address.toString), s"work-routee-$n")
      ctx.system.receptionist ! Receptionist.register(routerServiceKey, routee)
    }
    val singletonActor = ClusterSingleton(ctx.system).init(singletonService(ctx, workersRouter))
    Behaviors.receiveMessage {
      case job@ProcessText(text) =>
        singletonActor ! job
        Behaviors.same
    }
  }

}

object LoadBalance {
  def main(args: Array[String]): Unit = {
    if (args.isEmpty) {
      startup("compute", 25251)
      startup("compute", 25252)
      startup("compute", 25253)
      startup("front", 25254)
    } else {
      require(args.size == 2, "Usage: role port")
      startup(args(0), args(1).toInt)
    }
  }

  def startup(role: String, port: Int): Unit = {
    // Override the configuration of the port when specified as program argument
    val config = ConfigFactory
      .parseString(s"""
      akka.remote.artery.canonical.port=$port
      akka.cluster.roles = [$role]
      """)
      .withFallback(ConfigFactory.load("cluster-persistence"))

    val frontEnd = ActorSystem[Service.Command](Service(), "ClusterSystem", config)
    if (role == "front") {
      println("*************** sending ProcessText command  ************")
      frontEnd ! Service.ProcessText("this is the text that will be analyzed")
    }

  }

}

cluster-persistence.conf

akka.actor.allow-java-serialization = on
akka {
  loglevel = INFO
  actor {
    provider = cluster
    serialization-bindings {
      "com.learn.akka.CborSerializable" = jackson-cbor
    }
  }
 remote {
    artery {
      canonical.hostname = "127.0.0.1"
      canonical.port = 0
    }
  }
  cluster {
    seed-nodes = [
      "akka://ClusterSystem@127.0.0.1:25251",
      "akka://ClusterSystem@127.0.0.1:25252"]
  }
  # use Cassandra to store both snapshots and the events of the persistent actors
  persistence {
    journal.plugin = "akka.persistence.cassandra.journal"
    snapshot-store.plugin = "akka.persistence.cassandra.snapshot"
  }
}
akka.persistence.cassandra {
  # don't use autocreate in production
  journal.keyspace = "poc"
  journal.keyspace-autocreate = on
  journal.tables-autocreate = on
  snapshot.keyspace = "poc_snapshot"
  snapshot.keyspace-autocreate = on
  snapshot.tables-autocreate = on
}

datastax-java-driver {
  basic.contact-points = ["192.168.11.189:9042"]
  basic.load-balancing-policy.local-datacenter = "datacenter1"
}

build.sbt

name := "learn-akka-typed"

version := "0.1"

scalaVersion := "2.13.1"
scalacOptions in Compile ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlog-reflective-calls", "-Xlint")
javacOptions in Compile ++= Seq("-Xlint:unchecked", "-Xlint:deprecation")

val AkkaVersion = "2.6.5"
val AkkaPersistenceCassandraVersion = "1.0.0"


libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-cluster-sharding-typed" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-typed" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-query" % AkkaVersion,
  "com.typesafe.akka" %% "akka-serialization-jackson" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-cassandra" % AkkaPersistenceCassandraVersion,
  "com.typesafe.akka" %% "akka-slf4j" % AkkaVersion,
  "ch.qos.logback"     % "logback-classic"             % "1.2.3"
)

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

Telegraf和Grafana監控多平台上的SQL Server-自定義監控數據收集

問題

在上一篇文章中,我們使用Telegraf自帶的Plugin配置好了的監控,但是自帶的Plugin並不能完全覆蓋我們想要的監控指標,就需要收集額外的自定義的監控數據,實現的方法有:

  • 開發自己的Telegraf Plugin

  • 使用可以執行自定義腳本的inputs plugin

此處收集的監控項不多,收集間隔也不是很頻繁,所以我選擇Telegraf預置的Inputs.exec plugin實現。它非常靈活,可以執行任意命令和腳本。在腳本中實現獲取監控數據的邏輯,然後使用inputs.exec執行。獲取數據之後,需要按InfluxDB Line Protocol 格式組織數據,才能寫入到Influxdb。這種格式的組織方式:

[measurement],[tags] [fields] [timestamp]

  • measurement,類似於SQL中表的概念,數據存放的容器

  • tags,K-V格式用於標記數據記錄,一般它們的值不經常變化,如主機名。同時tags上會建立索引,查詢效率會好一些.

  • fields,K-V格式,表示真正收集的不同時間點的數據項,如CPU Load

  • timestamp,UNIX 時間戳,Influxdb是時序數據庫,所有數據都要與時間關聯起來。

  • measurement和tag之間用逗號分隔,fields 與它們用空格(whitespace)分隔

不管是運行在Linux還是Windows上的SQL,通常使用T-SQL查詢實例內部的數據和使用操作系統腳本查詢實例外部的數據以實現監控。接下來,以執行T-SQL獲取自定義監控數據為例,看看在Windos和Linux上分別如何實現。

解決方案

首先在被監控的實例上把相應的邏輯寫成存儲過程,然後通過inputs.exec調用之。

例如我在目標實例的influx庫中創建了一個存儲過程influx.usp_getInstanceInfo獲取一些實例的配置信息。然後需要在telegraf配置文件中啟用inputs.exec調用這個存儲過程。存儲過程的輸出數據如下:

sqlserver_property,host=SQL19N1,sql_instance=SQL19N1 host_platform="Linux",host_distribution="CentOS Linux",host_release=7,edition="Developer Edition (64-bit)",product_version="15.0.4033.1",collation="SQL_Latin1_General_CP1_CI_AS",is_clustered=f,is_hadr=t,cpu_count=2,scheduler_count=2,physical_memory_kb=6523904,max_workers_count=512,max_dop=0,max_memmory=2147483647 1590915136000000000

數據寫入到sqlserver_property,tags包括host,sql_instance,後面的全是fields。

  • SQL On Linux

使用SQLCMD調用存儲過程,把SQLCMD命令寫到一個bash文件中/telegraf/get_sqlproperty.sh

#!/bin/bash

/opt/mssql-tools/bin/sqlcmd -S SQL19N1 -U telegraf -P <yourpassword> -d influx -y 0 -Q "EXEC influx.usp_getInstanceInfo"

修改telegraf.conf中的inputs.exec, 然後重啟telegraf生效:

因為收集的是實例屬性信息,收集間隔設置的比較長。

 [[inputs.exec]]
#   ## Commands array
   commands = [
        "/telegraf/get_sqlproperty.sh"
        ]
        
   timeout = "5s"
   interval="24h"
   data_format = "influx"
  • SQL On Windows

Windows上首選使用PowerShell實現,把執行SQL的命令寫到C:\Monitoring\scripts\get_sqlproperty.ps1。col_res是存儲過程輸出的列名。

(Invoke-Sqlcmd -ServerInstance SQL17N1 -Username telegraf -Password "<yourpassword>" -Database influx -Query "exec influx.usp_getInstanceInfo" ).col_res

修改telegraf.conf中的inputs.exec, 然後重啟telegraf生效.

需要特別注意的問題:

  • 指定文件路徑時,要使用Linux路徑表達的forward slash(/), 而不是Windows中的 back slash(\)

  • ps1文件路徑使用單引號(single quote)

  • 避免文件路徑中有空格(whitespace)

 [[inputs.exec]]
#   ## Commands array
   commands = [
        "powershell 'C:/Monitoring/scripts/get_sqlproperty.ps1' "
        ]
        
   timeout = "5s"
   interval="24h"
   data_format = "influx"

配置完成后,看看measurement和數據:

總結

  • 在inputs.exec中最好是調用腳本,而不是命令。這樣當你需要變更數據收集邏輯,直接修改腳本即可,而不需要修改Telegraf的配置文件,避免重啟服務和配置干擾

  • 被調用的腳本的輸出,要是stdout,才能被正確寫入influxdb

  • Windows 上文件路徑和符號escape要特別注意

  • 如果對收集性能特別敏感或者收集頻率特別高時,使用Go自定義Plugin

  • 本文內容僅代表個人觀點,與任何公司和組織無關

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

前端也要懂物理 —— 慣性滾動篇

作者:凹凸曼-吖偉

我們在平時編程開發時,除了需要關注技術實現、算法、代碼效率等因素之外,更要把所學到的學科知識(如物理學、理論數學等等)靈活應用,畢竟理論和實踐相輔相成、密不可分,這無論是對於我們的方案選型、還是技術實踐理解都有非常大的幫助。今天就讓我們一起來回顧中學物理知識,並靈活運用到慣性滾動的動效實現當中。

慣性滾動(也叫 滾動回彈momentum-based scrolling)最早是出現在 iOS 系統中,是指 當用戶在終端上滑動頁面然後把手指挪開,頁面不會馬上停下而是繼續保持一定時間的滾動效果,並且滾動的速度和持續時間是與滑動手勢的強烈程度成正比。抽象地理解,就像高速行駛的列車制動后依然會往前行駛一段距離才會最終停下。而且在 iOS 系統中,當頁面滾動到頂/底部時,還有可能觸發 “回彈” 的效果。這裏錄製了微信 APP 【賬單】頁面中的 iOS 原生時間選擇器的慣性滾動效果:

熟悉 CSS 開發的同學或許會知道,在 Safari 瀏覽器中有這樣一條 CSS 規則:

-webkit-overflow-scrolling: touch;

當其樣式值為 touch 時,瀏覽器會使用具有回彈效果的滾動, 即“當手指從觸摸屏上移開,內容會繼續保持一段時間的滾動效果”。除此之外,在豐富多姿的 web 前端生態中,很多經典組件的交互都一定程度地沿用了慣性滾動的效果,譬如下面提到的幾個流行 H5 組件庫中的例子。

流行 UI 庫效果

為了方便對比,我們先來看看一個 H5 普通長列表在 iOS 系統下(開啟了滾動回彈)的滾動表現:

  • weui 的 picker 組件

    明顯可見,weui 選擇器的慣性滾動效果非常弱,基本上手從屏幕上移開后滾動就很快停止了,體驗較為不好。

  • vant 的 picker 組件

    相比之下,vant 選擇器的慣性滾動效果則明顯清晰得多,但是由於觸頂/底回彈時依然維持了普通滾動時的係數或持續時間,導致整體來說回彈的效果有點脫節。

應用物理學模型

慣性 一詞來源於物理學中的慣性定律(即 牛頓第一定律):一切物體在沒有受到力的作用的時候,運動狀態不會發生改變,物體所擁有的這種性質就被稱為慣性。可想而知,慣性滾動的本質就是物理學中的慣性現象,因此,我們可以恰當利用中學物理上的 滑塊模型 來描述慣性滾動全過程。

為了方便描述,我們把瀏覽器慣性滾動效果中的滾動目標(如瀏覽器中的頁面元素)模擬成滑塊模型中的 滑塊。而且分析得出,慣性滾動的全過程可以模擬為(人)使滑塊滑動一定距離然後釋放的過程,那麼,全流程可以拆解為以下兩個階段:

  • 第一階段,滑動滑塊使其從靜止開始做加速運動;

    在此階段,滑塊受到的 F拉 大於 F摩 使其從左到右勻加速前進。

    需要注意的是,對於瀏覽器的慣性滾動來說,我們一般關注的是用戶即將釋放手指前的一小階段,而非滾動的全流程(全流程意義不大),這一瞬間階段可以簡單模擬為滑塊均衡受力做 勻加速運動。

  • 第二階段,釋放滑塊使其在只受摩擦力的作用下繼續滑動,直至最終靜止;

    在此階段,滑塊只受到反向的摩擦力,會維持從左到右的運動方向減速前進然後停下。

基於滑塊模型,我們需要找到適合的量化指標來建立慣性滾動的計算體系。結合模型和具體實現,我們需要關注 滾動距離速度曲線 以及 滾動時長 這幾個關鍵指標,下面會一一展開解析。

滾動距離

對於滑動模型的第一階段,滑塊做勻加速運動,我們不妨設滑塊的滑動距離為 s1,滑動的時間為 t1,結束時的臨界點速度(末速度)為 v1 ,根據位移公式

可以得出速度關係

對於第二階段,滑塊受摩擦力 F拉 做勻減速運動,我們不妨設滑動距離為 s2,滑動的時間為 t2,滑動加速度為 a,另外初速度為 v1,末速度為 0m/s,結合位移公式和加速度公式

可以推算出滑動距離 s2

由於勻減速運動的加速度為負(即 a < 0),不妨設一個加速度常量 A,使其滿足 A = -2a 的關係,那麼滑動距離

然而在瀏覽器實際應用時,v1 算平方會導致最終計算出的慣性滾動距離太大(即對滾動手勢的強度感應過於靈敏),我們不妨把平方運算去掉

所以,求慣性滾動的距離(即 s2)時,我們只需要記錄用戶滾動的 距離 s1滾動時長 t1,並設置一個合適的 加速度常量 A 即可。

經大量測試得出,加速度常量 A 的合適值為 0.003

另外,需要注意的是,對於真正的瀏覽器慣性滾動效果來說,這裏討論的滾動距離和時長是指能夠作用於慣性滾動的範圍內的距離和時長,而非用戶滾動頁面元素的全流程,詳細的可以看【啟停條件】這一節內容。

慣性滾動速度曲線

針對慣性滾動階段,也就是第二階段中的勻減速運動,根據位移公式可以得到位移差和時間間距 T 的關係

不難得出,在同等時間間距條件下,相鄰兩段位移差會越來越小,換句話說就是慣性滾動的偏移量增加速度會越來越小。這與 CSS3 transition-timing-function 中的 ease-out 速度曲線非常吻合,ease-out (即 cubic-bezier(0, 0, .58, 1))的貝塞爾曲線為

曲線圖來自 在線繪製貝塞爾曲線網站。

其中,圖表中的縱坐標是指 動畫推進的進程,橫坐標是指 時間,原點坐標為 (0, 0),終點坐標為 (1, 1),假設動畫持續時間為 2 秒,(1, 1) 坐標點則代表動畫啟動后 2 秒時動畫執行完畢(100%)。根據圖表可以得出,時間越往後動畫進程的推進速度越慢,符合勻減速運動的特性。

我們試試實踐應用 ease-out 速度曲線:

很明顯,這樣的速度曲線過於線性平滑,減速效果不明顯。我們參考 iOS 滾動回彈的效果重複測試,調整貝塞爾曲線的參數為 cubic-bezier(.17, .89, .45, 1)

調整曲線后的效果理想很多:

回彈

接下來模擬慣性滾動時觸碰到容器邊界觸發回彈的情況。

我們基於滑塊模型來模擬這樣的場景:滑塊左端與一根彈簧連接,彈簧另一端固定在牆體上,在滑塊向右滑動的過程中,當滑塊到達臨界點(彈簧即將發生形變時)而速度還沒有降到 0m/s 時,滑塊會繼續滑動並拉動彈簧使其發生形變,同時滑塊會受到彈簧的反拉力作減速運動(動能轉化為內能);當滑塊速度降為 0m/s 時,此時彈簧的形變量最大,由於彈性特質彈簧會恢復原狀(內能轉化成動能),並拉動滑塊反向(左)運動。

類似地,回彈過程也可以分為下面兩個階段:

  • 滑塊拉動彈簧往右做變減速運動;

此階段滑塊受到摩擦力 F摩 和越來越大的彈簧拉力 F彈 共同作用,加速度越來越大,導致速度降為 0m/s 的時間會非常短。

  • 彈簧恢復原狀,拉動滑塊向左做先變加速后變減速運動;

    此階段滑塊受到的摩擦力 F摩 和越來越小的彈簧拉力 F彈 相互抵消,剛開始 F彈 > F摩,滑塊做加速度越來越小的變加速運動;隨後 F彈 < F摩,滑塊做加速度越來越大的變減速運動,直至最終靜止。這裏為了方便實際計算,我們不妨假設一個理想狀態:當滑塊靜止時彈簧剛好恢復形變

回彈距離

根據上面的模型分析,回彈的第一階段做加速度越來越大的變減速直線運動,不妨設此階段的初速度為 v0,末速度為 v1,那麼可以與滑塊位移建立關係:

其中 a 為加速度變量,這裏暫不展開討論。那麼,根據物理學的彈性模型,第二階段的回彈距離為

微積分都來了,簡直沒法計算……

然而,我們可以根據運動模型適當簡化 S回彈 值的計算。由於 回彈第二階段的加速度 是大於 非回彈慣性滾動階段的加速度F彈 + F摩 > F摩)的,不妨設非回彈慣性滾動階段的總距離為 S滑,那麼

因此,我們可以設置一個較為合理的常量 B,使其滿足這樣的等式:

經大量實踐得出,常量 B 的合理值為 10。

回彈速度曲線

觸發回彈的整個慣性滾動軌跡可以拆分成三個運動階段:

然而,如果要把階段 a 和階段 b 準確描繪成 CSS 動畫是有很高的複雜度的:

  • 階段 b 中的變減速運動難以準確描繪;
  • 這兩個階段雖運動方向相同但動畫速度曲線不連貫,很容易造成用戶體驗斷層;

為了簡化流程,我們把階段 ab 合併成一個運動階段,那麼簡化后的軌跡就變成:

鑒於在階段 a 末端的反向加速度會越來越大,所以此階段滑塊的速度驟減同比非回彈慣性滾動更快,對應的貝塞爾曲線末端就會更陡。我們選擇一條較為合理的曲線 cubic-bezier(.25, .46, .45, .94)

對於階段 b,滑塊先變加速后變減速,與 ease-in-out 的曲線有點類似,實踐嘗試:

仔細觀察,我們發現階段 a 和階段 b 的銜接不夠流暢,這是由於 ease-in-out 曲線的前半段緩入導致的。所以,為了突出效果我們選擇只描繪變減速運動的階段 b 末段。貝塞爾曲線調整為 cubic-bezier(.165, .84, .44, 1)

實踐效果:

由於 gif 轉格式導致部分掉幀,示例效果看起來會有點卡頓,建議直接體驗 demo

CSS 動效時長

我們對 iOS 的滾動回彈效果做多次測量,定義出體驗良好的動效時長參數。在一次慣性滾動中,可能會出現下面兩種情況,對應的動效時間也不一樣:

  • 沒有觸發回彈

    慣性滾動的合理持續時間為 2500ms

  • 觸發回彈

    對於階段 a,當 S回彈 大於某個關鍵閾值時定義為 強回彈,動效時長為 400ms;反之則定義為 弱回彈,動效時長為 800ms

    而對於階段 b,反彈的持續時間為 500ms 較為合理。

啟停條件

前文中有提到,如果把用戶滾動頁面元素的整個過程都納入計算範圍是非常不合理的。不難想象,當用戶以非常緩慢的速度使元素滾動比較大的距離,這種情況下元素動量非常小,理應不觸發慣性滾動。因此,慣性滾動的觸發是有條件的。

  • 啟動條件

    慣性滾動的啟動需要有足夠的動量。我們可以簡單地認為,當用戶滾動的距離足夠大(大於 15px)和持續時間足夠短(小於 300ms)時,即可產生慣性滾動。換成編程語言就是,最後一次 touchmove 事件觸發的時間和 touchend 事件觸發的時間間隔小於 300ms,且兩者產生的距離差大於 15px 時認為可啟動慣性滾動。

  • 暫停時機

    當慣性滾動未結束(包括處於回彈過程),用戶再次觸碰滾動元素時,我們應該暫停元素的滾動。在實現原理上,我們需要通過 getComputedStylegetPropertyValue 方法獲取當前的 transform: matrix() 矩陣值,抽離出元素的水平 y 軸偏移量后重新調整 translate 的位置。

示例代碼

基於 vuejs 提供了部分關鍵代碼,也可以直接訪問 codepen demo 體驗效果(完整代碼)。

<html>
  <body>
    <div id="app"></div>
    <template id="tpl">
      <div
        ref="wrapper"
        @touchstart.prevent="onStart"
        @touchmove.prevent="onMove"
        @touchend.prevent="onEnd"
        @touchcancel.prevent="onEnd"
        @transitionend="onTransitionEnd">
        <ul ref="scroller" :style="scrollerStyle">
          <li v-for="item in list">{{item}}</li>
        </ul>
      </div>
    </template>
    <script>
      new Vue({
        el: '#app',
        template: '#tpl',
        computed: {
          list() {},
          scrollerStyle() {
            return {
              'transform': `translate3d(0, ${this.offsetY}px, 0)`,
              'transition-duration': `${this.duration}ms`,
              'transition-timing-function': this.bezier,
            };
          },
        },
        data() {
          return {
            minY: 0,
            maxY: 0,
            wrapperHeight: 0,
            duration: 0,
            bezier: 'linear',
            pointY: 0,                    // touchStart 手勢 y 坐標
            startY: 0,                    // touchStart 元素 y 偏移值
            offsetY: 0,                   // 元素實時 y 偏移值
            startTime: 0,                 // 慣性滑動範圍內的 startTime
            momentumStartY: 0,            // 慣性滑動範圍內的 startY
            momentumTimeThreshold: 300,   // 慣性滑動的啟動 時間閾值
            momentumYThreshold: 15,       // 慣性滑動的啟動 距離閾值
            isStarted: false,             // start鎖
          };
        },
        mounted() {
          this.$nextTick(() => {
            this.wrapperHeight = this.$refs.wrapper.getBoundingClientRect().height;
            this.minY = this.wrapperHeight - this.$refs.scroller.getBoundingClientRect().height;
          });
        },
        methods: {
          onStart(e) {
            const point = e.touches ? e.touches[0] : e;
            this.isStarted = true;
            this.duration = 0;
            this.stop();
            this.pointY = point.pageY;
            this.momentumStartY = this.startY = this.offsetY;
            this.startTime = new Date().getTime();
          },
          onMove(e) {
            if (!this.isStarted) return;
            const point = e.touches ? e.touches[0] : e;
            const deltaY = point.pageY - this.pointY;
            this.offsetY = Math.round(this.startY + deltaY);
            const now = new Date().getTime();
            // 記錄在觸發慣性滑動條件下的偏移值和時間
            if (now - this.startTime > this.momentumTimeThreshold) {
              this.momentumStartY = this.offsetY;
              this.startTime = now;
            }
          },
          onEnd(e) {
            if (!this.isStarted) return;
            this.isStarted = false;
            if (this.isNeedReset()) return;
            const absDeltaY = Math.abs(this.offsetY - this.momentumStartY);
            const duration = new Date().getTime() - this.startTime;
            // 啟動慣性滑動
            if (duration < this.momentumTimeThreshold && absDeltaY > this.momentumYThreshold) {
              const momentum = this.momentum(this.offsetY, this.momentumStartY, duration);
              this.offsetY = Math.round(momentum.destination);
              this.duration = momentum.duration;
              this.bezier = momentum.bezier;
            }
          },
          onTransitionEnd() {
            this.isNeedReset();
          },
          momentum(current, start, duration) {
            const durationMap = {
              'noBounce': 2500,
              'weekBounce': 800,
              'strongBounce': 400,
            };
            const bezierMap = {
              'noBounce': 'cubic-bezier(.17, .89, .45, 1)',
              'weekBounce': 'cubic-bezier(.25, .46, .45, .94)',
              'strongBounce': 'cubic-bezier(.25, .46, .45, .94)',
            };
            let type = 'noBounce';
            // 慣性滑動加速度
            const deceleration = 0.003;
            // 回彈阻力
            const bounceRate = 10;
            // 強弱回彈的分割值
            const bounceThreshold = 300;
            // 回彈的最大限度
            const maxOverflowY = this.wrapperHeight / 6;
            let overflowY;

            const distance = current - start;
            const speed = 2 * Math.abs(distance) / duration;
            let destination = current + speed / deceleration * (distance < 0 ? -1 : 1);
            if (destination < this.minY) {
              overflowY = this.minY - destination;
              type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
              destination = Math.max(this.minY - maxOverflowY, this.minY - overflowY / bounceRate);
            } else if (destination > this.maxY) {
              overflowY = destination - this.maxY;
              type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
              destination = Math.min(this.maxY + maxOverflowY, this.maxY + overflowY / bounceRate);
            }

            return {
              destination,
              duration: durationMap[type],
              bezier: bezierMap[type],
            };
          },
          // 超出邊界時需要重置位置
          isNeedReset() {
            let offsetY;
            if (this.offsetY < this.minY) {
              offsetY = this.minY;
            } else if (this.offsetY > this.maxY) {
              offsetY = this.maxY;
            }
            if (typeof offsetY !== 'undefined') {
              this.offsetY = offsetY;
              this.duration = 500;
              this.bezier = 'cubic-bezier(.165, .84, .44, 1)';
              return true;
            }
            return false;
          },
          // 停止滾動
          stop() {
            const matrix = window.getComputedStyle(this.$refs.scroller).getPropertyValue('transform');
            this.offsetY = Math.round(+matrix.split(')')[0].split(', ')[5]);
          },
        },
      });
    </script>
  </body>
</html>

參考資料

  • weui-picker
  • better-scroll

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

【優雅寫代碼系統】springboot+mybatis+pagehelper+mybatisplus+druid教你如何優雅寫代碼

目錄

  • spring基本搭建
  • 整合mybatis
  • pom配置
  • mybatis配置
    • 設置數據源
    • 設置sqlsessionfactory
    • 設置掃描
    • 設置開啟事務
    • 資源放行
    • 測試
    • 結果
  • 思考&&疑問
  • 使用通用mapper功能
  • 使用mybatis-plus
  • 使用分頁插件
    • mybatis-plus自帶分頁
    • github分頁插件
      • 使用常規版本
  • 總結
  • # 加入戰隊
    • 微信公眾號
  • 主題

springboot 融合了很多插件。springboot相比spring來說有一下有點

  • 自動配置: 針對很多spring的應用程序,springboot提供了很多自動配置
  • 起步依賴: 告訴springboot你需要什麼,他就會引入需要的庫
  • 命令行界面:springboot的可選特性
  • Autuator: 監控springboot項目的運行情況

spring基本搭建

  • springboot的配置很簡單。在pom中繼承springboot的pom .然後依賴一下pom就可以繼承所需的jar了

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
</parent>


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

  • 出了jar外。我們pom中配置一個插件就行了

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

整合mybatis

【詳見feature/0002/spring-mybatis 分支】

  • 之前我們梳理過mybatis的運行機制及注意點。javaweb的開發時離不開spring的。一個完整的框架是離不開spirng的。所以spring整合mybatis勢在必行。我們下面實現如何在spring下融合mybatis及其優秀的一些插件搭建架構

pom配置

  • 我們在springboot項目的pom中繼續添加我們mybatis的jar就完成了第一步。
  • 一個是mybatis與spring的整合jar 。 開啟springboot加載mybatis項目
  • 一個是spring的aopjar包。主要是實現mybatis的事務問題
  • 一個是mysql的jar包。這裏主要看你自己的需求。如果你的項目中使用oracle那麼久加入oracle的坐標就行了
  • druid是管理數據的數據源

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.10</version>
</dependency>

mybatis配置

  • 上面的pom設置已經準備了mybatis開發階段的基本jar。 有了上述的包就可以搭建mybatis . 正常我們在我們的springboot項目中配置一個bean配置的類 MybatisConfig

設置數據源


@Bean
@ConfigurationProperties("spring.datasource")
public DataSource primaryDataSource() {
    // 這裏為了演示,暫時寫死 。 實際上是可以通過autoConfigure裝配參數的
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://192.168.44.130:3306/others?useUnicode=true&amp;characterEncoding=utf8");
    return dataSource;
}

  • 在springboot中我們只需要在方法上添加Bean註解,就相當於我們在spring的xml中配置的bean標籤。在java代碼中我們可以進行我們業務處理。個人理解感覺更加的可控。 因為我們使用的mysql。所以這裏我們簡單的使用druid的數據源

設置sqlsessionfactory


@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/com/github/zxhtom.**./*.xml"));
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

  • 在mybatis中sqlsesson是我們操作數據庫最直接的java類。管理sqlsession就是上面的sqlsessionFactory 。 所以我們配置它也是必須的。因為和spring整合。Mybatis的SqlsessionFactory交由給spring的SqlsessionfactoryBean管理。所以我們先構建SqlSessionFactoryBean。然後配置必須的數據源、Configuration、mybatis的xml路徑等等信息
  • SqlSessionFactoryBean 設置出了spring的FactoryBean屬性意外。最重要的是可以設置Mybatis的sqlsessionfactory的屬性。上面我們只是簡單的設置了。後期可以根據架構的需求進行不斷的完善。
  • 可以設置插件、緩存、別名、映射處理器、事務、環境等等所有mybatis的配置

設置掃描


@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    mapperScannerConfigurer.setSqlSessionFactory(primarySqlSessionFactory());
    //每張表對應的*.java,interface類型的Java文件
    mapperScannerConfigurer.setBasePackage("com.github.zxhtom.**.mapper");
    return mapperScannerConfigurer;
}

  • springboot 中我們的mapper接口也是交由spring來掃描的。之前mybatis程序我們是一個一個手動加入的。spring的特性可以直接掃描指定路徑的所有java類。這裏我們就設置了一下mapper的路徑。

設置開啟事務

  • 優秀的架構沒有事務是能使用的。我們配置事務也是很簡單的。在springboot中我們推薦使用java代碼來配置事務的。下面的配置我們為了容易閱讀。配置在TransactionConfig類中

@Bean
public DataSourceTransactionManager primaryTransactionManager() {
    return new DataSourceTransactionManager(dataSource);
}

  • 首先是配置事務管理器。事務管理的是數據源。所以這裏我們需要將MybatisConfig中的DataSource加載進來。這裏不多說
  • 然後配置我們的通知點

@Bean
public TransactionInterceptor txAdvice() {
    DefaultTransactionAttribute txAttr_REQUIRED = new DefaultTransactionAttribute();
    txAttr_REQUIRED.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

    DefaultTransactionAttribute txAttr_REQUIRED_READONLY = new DefaultTransactionAttribute();
    txAttr_REQUIRED_READONLY.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    txAttr_REQUIRED_READONLY.setReadOnly(true);

    NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
    source.addTransactionalMethod("add*", txAttr_REQUIRED);
    source.addTransactionalMethod("save*", txAttr_REQUIRED);
    source.addTransactionalMethod("delete*", txAttr_REQUIRED);
    source.addTransactionalMethod("update*", txAttr_REQUIRED);
    source.addTransactionalMethod("exec*", txAttr_REQUIRED);
    source.addTransactionalMethod("set*", txAttr_REQUIRED);
    source.addTransactionalMethod("get*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("query*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("find*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("list*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("count*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("is*", txAttr_REQUIRED_READONLY);
    return new TransactionInterceptor(primaryTransactionManager(), source);
}

  • 最後我們配置一下我們的切面、切點

@Bean
public Advisor txAdviceAdvisor() {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
    return new DefaultPointcutAdvisor(pointcut, txAdvice());
}@Bean
public Advisor txAdviceAdvisor() {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
    return new DefaultPointcutAdvisor(pointcut, txAdvice());
}

  • 最後說明下。TransactionConfig這個類因為配置事務、攔截所以我們需要加入如下註解在雷傷

@Configuration
@Aspect
@EnableTransactionManagement

資源放行

  • 以上就是mybatis基本的一個配置了。但是這個時候還是不能使用的。因為我們的mapper對應的xml是放在java目錄下的。正常src下都是java。xml文件在maven編譯時不放行的。我們需要特殊處理下

  • 在pom的build下加入放行配置


<resources>
    <resource>
        <directory>src/main/java</directory>
        <includes>
            <include>**/*.xml</include>
            <include>**/*.properties</include>
            <include>**/*.yaml</include>
        </includes>
        <filtering>true</filtering>
    </resource>
    <resource>
        <directory>src/main/resources</directory>
        <includes>
            <include>**/*.*</include>
        </includes>
        <filtering>false</filtering>
    </resource>
</resources>

  • 加入如下配置后別忘記了clean一下在運行。防止緩存。clean之後看看target下文件

測試

  • 為了方便測試我們需要編寫測試類。

@SpringBootTest(classes = Application.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class TestDataSource {
  
    @Autowired
    private TestMapper testMapper;
    
    @Test
    public void test() {
        List<Map<String, Object>> test = testMapper.test();
        System.out.println(test);
    }
}

結果

  • 這裡是我查詢數據庫中的一條數據。主要是測試springboot整合mybatis的流程。到這裏mybatis就整合完了。

思考&&疑問

  • 上面的配置我們是接入mybatis了。但是在筆者其他架構上測試過。在MybatisConfig這個類中配置數據源的時候可以直接new出DruidDataSource就可以了。springboot會自動通過DataSourceProperties這個類獲取到數據源信息的。但是筆者這裏一直沒有嘗試成功。至於原理更是沒有搞懂了。這裏留下一個彩蛋希望知道的朋友能說說這事怎麼回事

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource primaryDataSource() {
    return new DruidDataSource();
}

  • 上述代碼和本案例中相比就少了很多配置。雖然有的朋友說可以通過映射實體來做。但是直接new對象對我們而言更簡單。疑問

使用通用mapper功能

【詳見feature/0002/spring-mybatis-tk-page 分支】


<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
    <version>3.3.9</version>
</dependency>

  • 這些插件其實就是改寫我們之前學習的myabtis四大組件中的某一個組件而實現的。所以不管是通用mapper還是後面要說的myabtis-plus都是需要重新改寫我們的myabtis配置類的。

  • 首先我們想接入通用mapper時,我們需要改用tk提供的掃包配置


@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    tk.mybatis.spring.mapper.MapperScannerConfigurer mapperScannerConfigurer = new tk.mybatis.spring.mapper.MapperScannerConfigurer();
    //MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    mapperScannerConfigurer.setSqlSessionFactory(primarySqlSessionFactory());
    //每張表對應的*.java,interface類型的Java文件
    mapperScannerConfigurer.setBasePackage("com.github.zxhtom.**.mapper");
    return mapperScannerConfigurer;
}

  • 然後添加通用mapper配置

@Bean()
public MapperHelper primaryMapperHelper() throws Exception {
    MapperHelper mapperHelper = new MapperHelper();

    Config config = mapperHelper.getConfig();
    //表名字段名使用駝峰轉下劃線大寫形式
    config.setStyle(Style.camelhumpAndUppercase);

    mapperHelper.registerMapper(Mapper.class);
    mapperHelper.processConfiguration(primarySqlSessionFactory().getConfiguration());
    return mapperHelper;
}

  • 測試代碼就很簡單了。

User user = tkMapper.selectByPrimaryKey(1);

使用mybatis-plus

【詳見feature/0002/spring-mybatisplus 分支】

  • mybatis-plus 實際上就是通用mapper 。這裏可以理解就是不同的實現。接入mybatis-plus其實很簡單。首先我們引入坐標


<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

  • 然後我們創建實體

@Data
@TableName(value = "person_info_large")
public class User {
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String account;
    private String name;
    private String area;
    private String title;
    private String motto;
}

  • 然後就是編寫我們的Mapper 。需要繼承BaseMapper

public interface PlusMapper extends BaseMapper<User> {
}

  • 上述的編寫之後基本就可以查詢了。但是注意一下我們需要修改上面的myabtisCOnfog這個類。因為接入mybatisplus后需要我們用mybatisplus中的sqlsessionbean。

@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/github/zxhtom.**./*.xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.github.zxhtom.**.model");
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

  • 和通用mapper一樣。就這樣我們就可以調用BaseMapper為我們提供的方法操作數據庫了。

@Test
public void plusTest() {
    User user = plusMapper.selectById(1);
    System.out.println(user);
}

使用分頁插件

mybatis-plus自帶分頁

【詳見feature/0002/spring-mybatisplus 分支】

  • 首先我們使用mybatis plus自帶的分頁插件,將插件類注入到容器中

@Bean
public PaginationInterceptor paginationInterceptor() {
    return new PaginationInterceptor();
}

  • 然後我們查詢的構造一個Page對象就行了。參數分別是第幾頁、展示條數

return plusMapper.selectPage(page, null);

  • 就是這麼簡單。使用簡單原理才是我們學習的重點。喜歡myabtis插件原理的可以留言。抽空可以研究

github分頁插件

【詳見feature/0002/spring-mybatis-tk-page 分支】

  • 因為myatis-plus自帶了分頁插件。上面也展示了如何使用myabtis-plus插件。還有一個github上開元的分頁插件。這裏就和通用mapper進行組合使用

  • pagehelper官方更新還是挺勤奮的。提供了pagehelper 和springboot auto裝配兩種。 筆者這裏測試了pagehelper-spring-boot-starter這個包不適合本案例中。因為本案例中掃描mapper路徑是通過MapperScannerConfigurer註冊的。如果使用pagehelper-spring-boot-starter的話就會導致分頁攔截器失效。我們看看源碼

  • 這是因為這版本提供了springboot自動裝配。但是自動裝配的代碼中進行添加攔截器的時候sqlsessionfactory這個時候還沒有進行掃描mapper.也就沒有進行addMapper 。 所以這個時候添加的攔截器攔截不到我們的mapper . 如果非要使用這個版本的話。我們掃描mapper就不能通過MapperScannerConfigurer . 經過筆者測試。需要在MybatisConfig類上添加掃描註解@MapperScan(basePackages = {"com.github.zxhtom.**.mapper"}, sqlSessionFactoryRef = "primarySqlSessionFactory")

使用常規版本

  • 為了符合本版本宗旨 。 我們這裏使用如下坐標

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.7</version>
</dependency>

  • 使用這個版本pagehelper。 沒有了springboot的自動裝配了。我們可以自己添加插件。添加插件有兩種方式。想自動裝配版本一樣通過Configuration進行添加不過我們通過Condition條件選擇添加的時機 。
  • 還有一個簡單的方式就是通過SqlSessionFactoryBean設置sqlSessionFactoryBean.setPlugins(new Interceptor[]{new PageInterceptor()});
  • 因為通過sqlsessionFactoryBean添加的插件和在settings文件中添加是一樣的。在通過XmlFactory.build Configuration對象是會自動將插件裝在上。這個時候mapper也都掃描過了。

@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/github/zxhtom.**./*.xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.github.zxhtom.**.model");
        sqlSessionFactoryBean.setPlugins(new Interceptor[]{new PageInterceptor()});
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

總結

  • 這裏簡單闡述下為什麼不適用註解掃描mapper、或者不在配置文件中配置掃包路徑。因為通過MapperScannerConfigurer我們可以動態控制路徑。這樣顯得比較有逼格。在實際開發中筆者推薦使用註解方式掃描。因為這樣可以避免不必要的坑。

  • 到這裏我們整理了【springboot整合mybaits】、【mybatis-plus】、【通用mapper】、【pagehelper插件整合】 這四個模塊的整理中我們發現離不開myabtis的四大組件。springboot其實我們還未接觸到深的內容。這篇文章主要偏向myabtis . 後續還會繼續衍生探索 【myabtis+druid】監控數據信息 。

  • 喜歡研究源碼和開元框架小試牛刀的歡迎關注我

加入戰隊

# 加入戰隊

微信公眾號

主題

非xml配置springboot+mybatis事務管理

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

上一篇完成了博客的主題切換,菜單和二維碼的显示與隱藏功能,本篇繼續完成分頁查詢文章列表的數據展示。

添加頁面

現在點擊頁面上的鏈接,都會提示錯誤消息,因為沒有找到對應的路由地址。先在Pages下創建五個文件夾:Posts、Categories、Tags、Apps、FriendLinks。

然後在對應的文件夾下添加Razor組件。

  • Posts文件夾:文章列表頁面Posts.razor、根據分類查詢文章列表頁面Posts.Category.razor、根據標籤查詢文章列表頁面Posts.Tag.razor、文章詳情頁Post.razor
  • Categories文件夾:分類列表頁面Categories.razor
  • Tags文件夾:標籤列表頁面Tags.razor
  • Apps文件夾:Apps.razor準備將友情鏈接入口放在裏面
  • FriendLinks文件夾:友情鏈接列表頁面FriendLinks.razor

先分別創建上面這些Razor組件,差不多除了後台CURD的頁面就這些了,現在來逐個突破。

不管三七二十一,先把所有頁面的路由給確定了,指定頁面路由使用 @page 指令,官方文檔說不支持可選參數,但是可以支持多個路由規則。

默認先什麼都不显示,可以將之前的加載中圈圈寫成一個組件,供每個頁面使用。

在Shared文件夾添加組件Loading.razor

<!--Loading.razor-->
<div class="loader"></div>
//Posts.razor
@page "/posts/"
@page "/posts/page/{page:int}"
@page "/posts/{page:int}"

<Loading />

@code {
    /// <summary>
    /// 當前頁碼
    /// </summary>
    [Parameter]
    public int? page { get; set; }
}

這裏我加了三條,可以匹配沒有page參數,帶page參數的,/posts/page/{page:int}這個大家可以不用加,我是用來兼容目前線上的博客路由的。總的來說可以匹配到:/posts/posts/1/posts/page/1這樣的路由。

//Posts.Category.razor
@page "/category/{name}"

<Loading />

@code {
    /// <summary>
    /// 分類名稱參數
    /// </summary>
    [Parameter]
    public string name { get; set; }
}

根據分類名稱查詢文章列表頁面,name當作分類名稱參數,可以匹配到類似於:/category/aaa/category/bbb這樣的路由。

//Posts.Tag.razor
@page "/tag/{name}"

<Loading />

@code {
    /// <summary>
    /// 標籤名稱參數
    /// </summary>
    [Parameter]
    public string name { get; set; }
}

這個根據標籤名稱查詢文章列表頁面和上面差不多一樣,可以匹配到:/tag/aaa/tag/bbb這樣的路由。

//Post.razor
@page "/post/{year:int}/{month:int}/{day:int}/{name}"

<Loading />

@code {
    [Parameter]
    public int year { get; set; }

    [Parameter]
    public int month { get; set; }

    [Parameter]
    public int day { get; set; }

    [Parameter]
    public string name { get; set; }
}

文章詳情頁面的路由有點點複雜,以/post/開頭,加上年月日和當前文章的語義化名稱組成。分別添加了四個參數年月日和名稱,用來接收URL的規則,使用int來設置路由的約束,最終可以匹配到路由:/post/2020/06/09/aaa/post/2020/06/9/bbb這樣的。

//Categories.razor
@page "/categories"

<Loading />

//Tags.razor
@page "/tags"

<Loading />

//FriendLinks.razor
@page "/friendlinks"

<Loading />

分類、標籤、友情鏈接都是固定的路由,像上面這樣就不多說了,然後還剩一個Apps.razor

//Apps.razor
@page "/apps"

<div class="container">
    <div class="post-wrap">
        <h2 class="post-title">-&nbsp;Apps&nbsp;-</h2>
        <ul>
            <li>
                <a target="_blank" href="https://support.qq.com/products/75616"><h3>吐個槽_留言板</h3></a>
            </li>
            <li>
                <NavLink href="/friendlinks"><h3>友情鏈接</h3></NavLink>
            </li>
        </ul>
    </div>
</div>

在裏面添加了一個友情鏈接的入口,和一個 騰訊兔小巢 的鏈接,歡迎大家吐槽留言噢。

現在可以運行一下看看,點擊所有的鏈接都不會提示錯誤,只要路由匹配正確就會出現加載中的圈圈了。

文章列表

在做文章列表的數據綁定的時候遇到了大坑,有前端開發經驗的都知道,JavaScript弱類型語言中接收json數據隨便玩,但是在Blazor中我試了下動態接受傳遞過來的JSON數據,一直報錯壓根運行不起來。所以在請求api接收數據的時候需要指定接收對象,那就好辦了我就直接引用API中的.Application.Contracts就行了啊,但是緊接着坑又來了,目標框架對不上,引用之後也運行不起來,這裏應該是之前沒有設計好。

於是,我就想了一個折中的辦法吧,將API中的返回對象可以用到的DTO先手動拷貝一份到Blazor項目中,後續可以考慮將公共的返回模型做成Nuget包,方便使用。

那麼,最終就是在Blazor中添加一個Response文件夾,用來放接收對象,裏面的內容看圖:

有點傻,先這樣解決,後面在做進一步的優化吧。

將我們複製進來的東東,在_Imports.razor中添加引用。

//_Imports.razor
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Meowv.Blog.BlazorApp.Shared
@using Response.Base
@using Response.Blog

@inject HttpClient Http
@inject Commons.Common Common

@inject HttpClient Http:注入HttpClient,用它來請求API數據。

現在有了接收對象,接下來就好辦了,來實現分頁查詢文章列表吧。

先添加三個私有變量,限制條數,就是一次加載文章的數量,總頁碼用來計算分頁,還有就是API的返回數據的接收類型參數。

/// <summary>
/// 限制條數
/// </summary>
private int Limit = 15;

/// <summary>
/// 總頁碼
/// </summary>
private int TotalPage;

/// <summary>
/// 文章列表數據
/// </summary>
private ServiceResult<PagedList<QueryPostDto>> posts;

然後當頁面初始化的時候,去加載數據,渲染頁面,因為page參數可能存在為空的情況,所以要考慮進去,當為空的時候給他一個默認值1。

/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
    // 設置默認值
    page = page.HasValue ? page : 1;

    await RenderPage(page);
}

/// <summary>
/// 點擊頁碼重新渲染數據
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private async Task RenderPage(int? page)
{
    // 獲取數據
    posts = await Http.GetFromJsonAsync<ServiceResult<PagedList<QueryPostDto>>>($"/blog/posts?page={page}&limit={Limit}");

    // 計算總頁碼
    TotalPage = (int)Math.Ceiling((posts.Result.Total / (double)Limit));
}

在初始化方法中設置默認值,調用RenderPage(...)獲取到API返回來的數據,並根據返回數據計算出頁碼,這樣就可以綁定數據了。

@if (posts == null)
{
    <Loading />
}
else
{
    <div class="post-wrap archive">
        @if (posts.Success && posts.Result.Item.Any())
        {
            @foreach (var item in posts.Result.Item)
            {
                <h3>@item.Year</h3>
                @foreach (var post in item.Posts)
                {
                    <article class="archive-item">
                        <NavLink href="@("/post" + post.Url)">@post.Title</NavLink>
                        <span class="archive-item-date">@post.CreationTime</span>
                    </article>
                }
            }
            <nav class="pagination">
                @for (int i = 1; i <= TotalPage; i++)
                {
                    var _page = i;

                    if (page == _page)
                    {
                        <span class="page-number current">@_page</span>
                    }
                    else
                    {
                        <a class="page-number" @onclick="@(() => RenderPage(_page))" href="/posts/@_page">@_page</a>
                    }
                }
            </nav>
        }
        else
        {
            <ErrorTip />
        }
    </div>
}

在加載數據的時候肯定是需要一個等待時間的,因為不可抗拒的原因數據還沒加載出來的時候,可以讓它先轉一會圈圈,當posts不為空的時候,再去綁定數據。

在綁定數據,for循環頁碼的時候我又遇到了一個坑,這裏不能直接去使用變量i,必須新建一個變量去接受它,不然我傳遞給RenderPage(...)的參數就會是錯的,始終會取到最後一次循環的i值。

當判斷數據出錯或者沒有數據的時候,在把錯誤提示<ErrorTip />扔出來显示。

做到這裏,可以去運行看看了,肯定會報錯,因為還有一個重要的東西沒有改,就是我們接口的BaseAddress,在Program.cs中,默認是當前Blazor項目的運行地址。

我們需要先將API項目運行起來,拿到地址配置在Program.cs中,因為現在還是本地開發,有多種辦法可以解決,可以將.HttpApi.Hosting設為啟動項目直接運行起來,也可以使用命令直接dotnet run

我這裏為了方便,直接發布在IIS中,後續只要電腦打開就可以訪問了,你甚至選擇其它任何你能想到的方式。

關於如何發布這裏先不做展開,有機會的話寫一篇將.net core開發的項目發布到 Windows、Linux、Docker 的教程吧。

所以我的Program.cs中配置如下:

//Program.cs
using Meowv.Blog.BlazorApp.Commons;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Meowv.Blog.BlazorApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            var baseAddress = "https://localhost";

            if (builder.HostEnvironment.IsProduction())
                baseAddress = "https://api.meowv.com";

            builder.Services.AddTransient(sp => new HttpClient
            {
                BaseAddress = new Uri(baseAddress)
            });

            builder.Services.AddSingleton(typeof(Common));

            await builder.Build().RunAsync();
        }
    }
}

baseAddress默認為本地開發地址,使用builder.HostEnvironment.IsProduction()判斷是否為線上正式生產環境,改變baseAddress地址。

現在可以看到已經可以正常獲取數據,並且翻頁也是OK的,然後又出現了一個新的BUG。

解決BUG

細心的可以發現,當我點擊頭部組件的Postsa 標籤菜單時候,頁面沒有發生變化,只是路由改變了。

思來想去,我決定使用NavigationManager這個URI和導航狀態幫助程序來解決,當點擊頭部的Postsa 標籤菜單直接刷新頁面得了。

Common.cs中使用構造函數注入NavigationManager,然後添加一個跳轉指定URL的方法。

/// <summary>
/// 跳轉指定URL
/// </summary>
/// <param name="uri"></param>
/// <param name="forceLoad">true,繞過路由刷新頁面</param>
/// <returns></returns>
public async Task RenderPage(string url, bool forceLoad = true)
{
    _navigationManager.NavigateTo(url, forceLoad);

    await Task.CompletedTask;
}

forceLoad = true的時候,將會繞過路由直接強制刷新頁面,如果forceLoad = false,則不會刷新頁面。

緊接着在Header.razor中修改代碼,添加點擊事件。

@*<NavLink class="menu-item" href="posts">Posts</NavLink>*@

<NavLink class="menu-item" href="posts" @onclick="@(async () => await Common.RenderPage("posts"))">Posts</NavLink>

總算是搞定,完成了分頁查詢文章列表的數據綁定,今天就到這裏吧,未完待續…

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

【翻譯】.NET 5 Preview5發布

今天,發布了.NET 5.0 Preview5。主要對它進行了一小部分新功能和性能的改進。.NET 5.0 Preview 4包含了一些計劃和.NET 5.0要交付的內容。
現在,大多數的功能都已經包含在裏面,但是有許多功能還未到最終狀態。預計這個版本在Preview 7中完善。

可以下載適用於Windows,macOS和Linux的.NET 5.0 Preview 5:

  • Windows and macOS installers
  • Binaries
  • Docker images
  • Snap installer

ASP.NET Core和EF Core也在今天發布了

我們需要使用Visual Studio 2019 16.7才能使用.NET 5.0。 安裝最新版本的C#擴展,以將.NET 5.0與Visual Studio Code結合使用。 Mac的Visual Studio尚不支持.NET 5.0。

發布說明:

  • .NET 5.0 release notes
  • .NET 5.0 known issues
  • GitHub release
  • GitHub tracking issue

RyuJIT改進

對RyuJIT JIT編譯器進行了以下改進

  • 新的、更快的、可移植的tailcall helper實現。
  • ARM64硬件內部物理的實現進程
    • 實現ASIMD Extract Insert ExtractVector64 ExtractVector128
    • 實現 ASIMD widening, narrowing, saturating intrinsics
    • 添加VectorTableList和TableVectorExtension內部函數
    • 添加對ARM64內部函數的CreateScalarUnsafe()支持
    • ARM64對Vector64.Create()和Vector128.Create()的內在支持
    • 使用arm64 intrinsic優化BitOperations.PopCount ()
  • 在影響正則表達式編譯的情況下改進了JIT的速度
  • 使用新的硬件特性BSF/BSR改進英特爾架構性能
  • 實現Vector{Size}.AllBitsSet

Native exports

曾要求為導出本機二進制文件啟用導出功能,這些功能已在很長一段時間內調用.NET代碼。 這是一個很好的方案,現在通過.NET 5.0啟用它。 該功能的構建塊是託管對UnmanagedCallersOnlyAttribute的API支持。

這個功能是創建更高級別體驗的基礎。 團隊的Aaron Robinson一直在從事.NET Native Exports項目,該項目為將.NET組件作為本機庫發布提供了更完整的體驗。 目前正在尋求有關此功能的反饋,以幫助我們確定該方法是否應包含在產品中。

.NET本地導出項目使我們能夠:

  • 公開自定義的 native exports。
  • 不需要像COM這樣的高級互操作技術。
  • 跨平台工作

現有的項目可以實現類似的方案,例如:

  • Unmanaged Exports
  • DllExport

[重大變化]刪除.NET 5.0中內置的WinRT支持

注意:這個變化將出現在預覽6中。這是一個早期的通知。

Windows Runtime(WinRT)是Windows中公開使用的新API的技術和ABI。 您可以通過.NET代碼調用這些API,類似於使用C ++的方法。 在.NET Core 3.0中添加了對WinRT interop的支持,這是對Windows桌面客戶端框架(Windows Forms和WPF)添加支持的一部分。

最近,一直在與Windows團隊緊密合作,以更改和改進WinRT interop與.NET的協作方式。 我們已用Windows團隊在.NET 5.0中提供的C#/WinRT工具鏈替換了內置的WinRT支持。 WinRT互操作中的此更改是一項重大更改,使用WinRT的.NET Core 3.x應用將需要重新編譯。 我們將在即將來臨的預覽中提供更多信息。

在.NET 5中的支持WinRT API中明確指出了這些好處:

  • WinRT互操作可以獨立於.NET runtime進行開發和改進。
  • 使WinRT互操作與為其他操作系統(如iOS和Android)提供的互操作系統對稱。
  • 可以利用許多其他。net特性(AOT, c#特性, IL linking)。
  • 簡化.NET運行時代碼庫(刪除6萬行代碼)。

將System.DirectoryServices.Protocols擴展到Linux和macOS

添加對System.DirectoryServices.Protocols的跨平台支持。 在Preview 5中,添加了對Linux的支持,並在Preview 6中添加了對macOS的支持。對Windows支持已經存在。

System.DirectoryServices.Protocols是比System.DirectoryServices更低級別的API,並且啟用更多方案。 System.DirectoryServices包含僅適用Windows的概念/實現,因此製作跨平台並不是一個很容易的事情。
這兩個API都支持對目錄服務服務器(如LDAP或Active Directory)進行控制和交互。

Alpine 3.12

本周,增加了對Alpine 3.12,.NET Core 3.1和.NET 5的支持。 Alpine Linux的維護者於5月29日宣布發布Alpine 3.12。 與過去相比,以更快,更可預測的方式增加對新Linux發行版的支持。

會發現他們已經開始使用一種新的發布模型來發布新發行版的問題。 這就是我們對Alpine 3.12所做的。 將來,我們計劃更早地發布這些問題。 例如,需要跟蹤的下一個發行版可能是Ubuntu 20.10。 尚未決定,但可能會在7月或8月發布該發行版的類似問題,以準備10月發行的新Ubuntu版本。

最後

感謝大家對.NET 5.0預覽版的反饋以及您的早期反饋。 正如文章的簡介中所建議的那樣,現在才發布大約一半。 現在包括了大多數功能,但是我們希望在接下來的幾個預覽中會進行許多更改,以完成體驗並消除仍然存在的粗糙邊緣。

來源:https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-5/

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

熱浪和雷雨襲擊法國 高溫乾旱助長森林大火

摘錄自2020年08月01日中央通訊社法國報導

法國今天(1日)同時發布熱浪及大雷雨橘色警戒,巴黎、里昂等地氣溫飆至40度。極端氣候導致法國缺水危機日益嚴重,多個省分今天下令限水。

氣象局表示,今天是「強烈熱度的高峰」,而熱浪又遇上週末及8月暑假出遊車潮,出現熱度加乘效應。這波熱浪於今日達到高峰,並會持續整個週末。面對熱浪與大雷雨,今天全法國96個省中有32個已經發出橘色警戒,其中13個熱浪警報、19個大雷雨警報。

法國近日接連發生森林大火。27日波爾多北部吉倫特省(Gironde)和巴黎南方盧瓦雷省(Loiret)兩處的惡火燒掉超過500公頃的森林。30日晚間,西南部鄰近大西洋的安格雷鎮(Anglet)的森林也出現熊熊大火,燒毀165公頃森林。所幸幾起事故都沒有傷及人員性命。雖然3起森林大火起火原因仍在調查中,但據信連日高溫及季節乾旱,皆助長火勢。

尤其是聖讓德呂(Saint-Jean-de-Luz)30日下午出現超高溫41.9度,創下當地有史以來最高溫紀錄。

生物多樣性
土地利用
國際新聞
法國
森林大火
乾旱
棲地保育
森林
災害

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化