Vue框架作為當前主流的前端框架之一,作者尤雨溪更是國內開發者心中男神,作為開發工程師你是否知道Vue框架名字的由來?你是否知道Vue框架作者尤大大創作Vue過程中的幾個節點?今天就來帶領大家深入Vue背后的故事
其實尤大大最初專業為室內藝術,并非計算機,但是尤大大在后面到帕森斯設計學院進修藝術研究碩士學位,學習的是科技與設計項目,它涵蓋了編程、設計和新媒體藝術,也正是在這個時候尤大大自學了JavaScript,開始用JavaScript進行創作,畢業后就進入了谷歌,尤大大為谷歌瀏覽器開發了一些實驗性的互動場景,把它們放到了作品集中,尤大大在采訪中提到曾在學校期間開發了一個克隆版的Clear應用,它是一個有著很新穎的手勢操作的待辦事項應用程序,它應該算是“滑動來完成”手勢操作的開拓者了,所以就在網頁上重置了他的大部分功能,它在黑客新聞(Hacker News)上火了起來,被很多網站報道了,這件事讓谷歌的招聘官注意到了尤大大,那時候谷歌有很多為谷歌瀏覽器開發的實驗性場景,包括一些3D動效、2D的物理效果和可視化的處理等,都是尤大大他們在用的瀏覽器上實現的,這在當時是前所未有的,它真的體現了JavaScript的強大,不知什么緣故尤大大就接到了谷歌招聘官的電話她說,你有興趣加入我們的創新實驗室嗎,作為我們招收的五人之一,尤大大當時真的不敢相信,都沒給他們發我的簡歷就直接有人給打電話讓加入谷歌,那肯定是要去的,尤大大非常高興,自己不用也搬家,工作場地就在紐約;
尤大大聊出這段經歷:“工作兩年多,做一些有意思和實驗性的項目,稀奇古怪的點子,比如說,十年后的搜索界面是什么樣子的,或者如果整面墻都是可以互動屏幕的會怎樣?如果谷歌能夠聽懂你說的每一句話?諷刺的是這件事現在幾乎已經成為現實了,我們要做出一些關于這方面的概念視頻和這種交互操作的原型,在2012年。在很長的一段時間內,我都在用原生的JavaScript來手寫這些原型,因為這些原型不會使用傳統的UI界面,他們需要很多特殊的處理,這些處理手段在如今普通的應用中很難見到,我嘗試著去用一些現有的框架進行開發,但是很多時候我發現他們不能解決我的問題,我使用的第一個框架是Backbone,Backbone更像是一個為應用搭建的結構,他不會幫助你控制視圖,盡管Angular提供了數據綁定,能將視圖和數據狀態同步起來,但它太約束你寫代碼的方式,所以它也不能很好的處理我正在進行的工作項目,因為這些項目很注重交互而不是如何成為一個完整的應用程序,我開始思考如何才能讓我的工作變得更高效,就在那時我開始想或許我可以自己開發一個我自己的框架,最初我的目標就是開發一個非常簡單專一的工具,僅僅就是同步DOM和一些JavaScript對象,這是最初的想法,在這個初始項目有了一點人氣之后,我們開始擴大開發范圍,一點點加入一些新功能到這個生態環境中去,最終使他變成了一個框架。”
尤大大對Vue.js進行的首次提交是在2013年的6月份,它剛開始的名字是Seed.js,據尤大大說當時想不到一個更好的名字,但是當尤大大要在NPM上發布的時候,發現Seed.js在NPM上已經被占用了,所以只能再想一個,尤大大表述:“我覺得,這是一個視圖(View)的框架,但是就叫他View的話有點太直接了,所以我就把“View”放到谷歌翻譯里面,然后我找到了它法語的翻譯——vue,只有三個字母,看起來很好,也沒有在NPM上被占用,所以我就用了這個名字?!?
尤大大自述:“我基本上就是為我自己創建的這個框架,所以我的期望是,我希望,開發出一個我自己喜歡的框架,這也是我為什么要做這個框架的原因,我當然還在谷歌創意實驗室工作,所以我把它發布成了我的私人項目,應該有幾百名用戶,我覺的有幾百個GitHub的星星,給了我很多初始用戶?!?
你有沒有想過你會通過這個賺到錢?尤大大:“完全沒想到,當我出版的時候,并不是真的在說我想將其發布為一個可持續的項目,就像一個音樂愛好者為了樂趣而做一個專輯,你知道那種感覺不是為了賺錢,你發布這個音樂,只是因為你享受創作它的過程,并且你希望其他人能聽到它,所以開發Vue.js的過程就像是一個途徑像是我在做一個人們在真實世界中真的會用到的東西,相比我在谷歌創意實驗室的工作來說,我們把做的東西給經理們看,但誰知道這個東西什么時候會變成現實呢(這里主要的意思是在谷歌中想法的出現到真正的落地的周期都是不確定的,甚至都不知道能否落地)所以我有一種沖動,要開發一個人們真正會用到的東西,我當時并沒有想那么多,但有一個熱門的開源項目會幫助找到下一個工作,這就是我加入Meteor的方式,他們基本上很傾佩我在Vue上做出的工作,所以他們直接跳過了面試的過程,當場就給了我工作,所以我很高興,這才是你進行開源項目工作獲得的真正的好處之一;
尤大大在Meteor工作的時候,仍然在持續的開發Vue.js,因為尤大大覺得它在那時就像自己的孩子一樣,我們可以看到上面尤大大的提交記錄,在Github上看到一個非常綠的圖表,因為尤大大在每個周末都會完成很多提交,當時進行更多的工作也越來越有必要了,因為Vue的用戶越來越多了,據尤大大表述Vue一直在成長所以會出現更多的問題,更多的Bug要被修復。
就在那段時間,Taylor發了一條推特提到了Vue.js,那是2014年,Taylor Otwell 是Laravel的作者,Laravel是一個非常流行的PHP框架,大家都知道在2014年那段時間PHP火的一塌糊涂。
Taylor那天在推特上說:“目前自己在學習React,但是狀態很迷惑,React很難,我正在學習Vue.js,因為它看起來很簡單”,雖然看起來這不像是一個正式的對Vue的認可,他只是在說我正在學習Vue.js",然后很多人開始問他:你覺得Vue怎么樣,它好用嗎?基本上在每一條回復中,他都說,沒錯,它很棒,這開始看起來像是一個對Vue公開的認可,然后所有的Laravel用戶就會想,哇,如果Taylor喜歡Vue,那它肯定很好呀,所以我們也應該試一試,所以Vue就獲得了很多來之Laravel社區的用戶"
Taylor表述:“我當時在網上查找一個JavaScript框架來開發這個叫Laravel Spark的產品,它是一個快速開發腳手架,幫助你開發你自己的軟件和服務體系,幫助人們啟動他們自己的生意,就像我一樣,我感覺到不知所措,我看了其他的框架,他們有復雜的編譯流程,需要很多工具才能起步,我嘗試了Vue.js發現你可以用Windows筆記本就能寫出一個簡單的網頁應用你不需要其他的編譯工具,僅僅需要添加一個CDN鏈接,寫幾句JavaScript,你的網頁就跑起來了,對于我這個不是很熟悉JavaScript的人,至少可以說這么簡單就可以實現那么多功能,真的讓人耳目一新而且還不需要學習很多其他的工具,我記得我發布了一條推特說《我發現了這個很棒的新玩意叫Vue.js,像我一樣的新手,都可以寫出JavaScript應用程序,真的很令人驚嘆》因此我們為Laravel Spark采用了Vue.js,最后產生了,你知道的,為Laravel帶來了超過一百萬美金收入,成千上萬個用戶搭建了他們自己的生意,所以這真是一件對Vue.js和Laravel都很棒的事情,幫助簡化了不知道多少,使用Spark搭建的業務,都同時運用了Laravel和Vue.js”
上面說到的這些都發生在尤大大決定全職開發Vue.js之前,在那之前Vue.js還處于只是在前端社區中比較有名的階段,尤大大還沒想過要將它作為自己的全職工作,甚至是從中賺錢,直到尤大大學習到Taylor的做法之后,才知道Laravel是那么龐大,意識到或許自己真的能干點東西出來,但開始有人發起帖子和討論說:“我到底該用哪個?我應該用Vue.js嗎?”,你能看到的很多的回復都是:“不要用,因為它還是起步階段”、“它不穩定,它僅僅由一個人維護著,天知道它什么時候就消失了”,尤大大想證明這些人是錯誤的,要讓Vue.js變成一個V1.0版本的可生產的框架,所以尤大大花費了整個2015年的假期,用3個星期的時間把所有東西都開發好,更新文檔,然后在2015年10月,尤大大發布了1.0版本;
當時Vue.js剛剛發布了1.0版本,還不存在像今天這樣的核心團隊,只有大概三個人,他們都忙著弄好文檔和修復Bug,在1.0發布之后,當時的論壇就像是一個荒地;LinusBorg 就到論壇里去回答很多的問題,在論壇里非常的活躍,LinusBorg 了解到很多。
LinusBorg自述:“ 人們在他們項目中遇到的問題,我都在我空閑時間里干這件事,持續了三到四個月之后,Evan了解到了這個情況,他就給我發了一個Slack邀請,之前我們從未交流過,我也沒開啟任何問題討論,沒提出合并請求,我沒在代碼上做出任何貢獻,但我從一開始就注意到Vue.js框架不僅是正確的編碼,不僅是一個庫,還需要一個描述它工作原理的文檔,幫助你解決問題,它應該是我們需要的能想到的最好的文檔,我們必須要扎身到社區中去,我們需要額外的工具和支持,這就是我被邀請到Vue團隊的原因,因為我慢慢成長為社區領袖,我也確實加入到了Vue的核心團隊,即便在沒有對代碼和內容作出貢獻的情況下,不過之后我為了我所謂的自尊心,我學習了源碼,研究了一些代碼庫,看了一些問題,然后開始四處作出一些貢獻”。
尤大大自述:“我認為在1.0版本之后,我開始嘗試著進行全職工作,或者這個東西還真能給我一些收入,讓我能夠把它當作一個全職工作,這時我開始覺得我在Meteor和我在Vue.js上的工作存在一些差異,在Meteor我只是一個普通的開發者,我沒有做決策的權利,雖然我可以提出建議但我還遠沒到那種,說我們應該怎么做的那種地位,這讓我對Meteor的發展有一些悲觀,相比之下,Vue.js一直在快速成長。我注意到用Meteor的人比較少,而用Vue.js的人越來越多,所以那時我就覺得或許把Vue.js作為我的全職工作是值得的”
LevelUp教程的Scott Tolinski在Vue火了后,人們一直要求他做一個Vue.js的教程,在這個系列的教程中,會為大家講到所有關于Vue.js的知識。
Scott Tolinski表示:“Vue.js出現的正是時候,因為很多人從Angular一代出現或更早之前就開始用Angular,人們都覺得Angular一代很簡單,但當Angular二代推出的時候它幾乎是一個完全不同的版本了,那個時候React也正在被推出,所以很多開發者開始用React,他們覺得React是個能夠登上前端寶座的新東西,但這些開發者就會失去使用Angular一代的那種簡單,輕松的感覺,就會感覺到有一種差距,這個由Angular一代的缺失而導致的大洞,Vue.js正好在這個時候出現了,它填補了一個空白,因為人們想要更低的入門門檻,還想要React的這種強大和靈活性,而這就是Vue.js的強項了,它吸取了多個框架和平臺最強的部分”。
尤大大自從有了全職開發Vue.js的想法,就在探索能讓自己經濟上獨立的辦法,尤大大做的第一件事就是設立一個Patreon(眾籌平臺)賬號,結果發現還真的不錯,在建立那個賬號不久之后,就能拿到2000美元一個月,尤大大的一位朋友郭達峰,他是“上線了(Strikingly)”的CTO,一個YC投資的公司,他的公司有個小基金用來支持開源項目,純粹是出于做好事的想法,如果能拿到這個支持的話,尤大大就完全可以辭掉工作,并開始全職開發Vue.js了,這就足以讓尤大大邁出這一步,至少可以去嘗試一下。
Serah Drasner在CSS-tricks工作,是一個寫手,Serah Drasner問Chris Coyier:“我覺的我要寫一篇關于Vue.js的文章”,他說聽起來不錯,所以Serah Drasner開始寫關于Vue.js的一篇文章,感覺要寫挺長的,Serah Drasner問Chris Coyier:“什么情況下一篇文章需要分為一個系列來寫”他說,什么時候你覺的你需要寫一個目錄的時候,那個時候Serah Drasner就該分開寫了,Serah Drasner自述:這個時候我寫的谷歌文檔就已經有25頁長了,然后我寫完了最好一章,動畫,最后就有了一個共有五篇文章的一個系列,我接著不斷的寫關于Vue.js的東西,下個月我寫了更多的文章,然后做了一些Demo和一些開源項目,我覺得從那個時候開始,我就對這個框架感到非常激動,我想要投資更多的時間到其中去,所以Evan和Chris Fritz就邀請我和他們一起制作文檔,同時進行一個新的項目叫cookbook,然后我就開始領導cookbook項目并且加入了Vue.js核心團隊,現在我幫助運營Vue文檔的見面會,Vue團隊的見面會我真的很喜歡和這個團隊一起工作。
“在家里工作絕對是非常自由的,你可以自己定制時間表,除去不需要通勤的第二大好處可能就是你不需要穿的像Tom Dale一樣?!?
尤大大:“想象一個在中國的人想要學習一個新的框架,他們去官網發現只有英文的文檔,如果這個框架本身就非常難懂的話就更糟了,他們就會很難學會這個框架,但當他們到Vue.js官網的時候發現這里有中文版的文檔,他們讀了一下發現文檔是由母語是漢語的人寫的,一些技術性的內容直接用漢語表達的話會有些奇怪,因為你必須對技術名詞想出一些很好的譯名,比如說綁定(binding),引用(reference)或視圖模型(view model),有些東西你甚至都翻譯不了,因為漢語是我的母語然后英語版本的文檔也是我自己寫的,所以我能夠改寫一些術語,讓它的漢語翻譯更自然,這種語言上的熟悉讓他們能夠讀懂你的框架,比他們用自己不熟悉的第二語言來學習要快的多,制作這樣的中文文檔,絕對幫助提高了Vue.js在中國的使用度”;
Jinjiang Zhao:“在中國,很少有人在科技界很知名,所以在中國,人們把Evan看作是一個英雄,一個中國開發者開發的人人都喜歡的框架”;
Gu Yiliang:“這在中國真的很不尋常,我們看不到有人能在開源項目上對世界作出這么大的影響,不僅僅是在中國內部,Evan,他是世界頂尖的,而且他還是中國人,所以我覺得這就是他有這么多中國粉絲的原因”;
Taylor:“他是一個中國人,當中國人看到這一點的時候,他們覺得自己是這其中的一部分,覺得自己是Vue社區的一部分,這是一種很真實的感覺因為他們與Vue的創始人是聯系在一起的,Evan為中國社區的作為創造了一種強大的現象,它在中國的開發者之間建立了一種強大的聯系,每個人都希望成為這個群體的一員,它讓你感覺很棒,當你是這個群體中的一員時,Evan能融入這個群體,這種力量是非常強大的,這是他能在中國成功的巨大原因,它能夠融入到這個群體中真的很驚人”;
Jinjiang Zhao:“那個時候移動網絡非常的復雜并且不穩定,尤其是在中國,很多人還在用GPRS數據,所以我們嘗試搭建一些新的,小體積高性能框架,幫助我們開發成千上萬個網頁,來支持千萬級別的流量,這個量是非常非常大的,我在Github發現了Vue,所以我們的故事就開始了”;
尤大大被到阿里巴巴園區,做一個小的科技講座,介紹Vue背后的基本想法、是怎么開始做它的、用Vue的好處到底是什么、相比一些手寫的jQuery。
他們花費了很長時間,一年或一年半,一些開發者終于接受了Vue,并開始使用它,現在越來越多的人都在用Vue,在那之后,阿里巴巴也越來越多的用Vue了,人們也開始對Vue.js有更多的了解,因為尤大大在知乎上的存在,同時也因為Vue.js在中國以外也獲得了更多的關注,這些消息也反向流回中國,人們才發現有一個新的框架叫Vue.js,事實上很多人都是以一個開源項目的身份認識到Vue.js,然后他們才意識到原來這是一個中國人寫出來的,Gu Yiliang采訪中提到:“我們主要是在我們的企業管理應用上使用Vue.js,像是谷歌的AdWords,我想為什么不參與到Vue.js其中呢?它在發展,它是潮流,中國開發者不僅僅把他當作一個開源項目開發者,而是一個開源項目領導者?!?
Leo Deng:“我很熟悉Ember.js,而Vue.js看起來和它很像,所以我開始用Vue.js寫一些東西,確實起作用了,我感覺它像是我已經學過的東西,Vue.js有非常高質量的中文文檔,幫助了很多入門級的工程師進行開發,我們用它來開發生態系統,社區,我覺得這就是它這么流行的原因,在中國我們有很多的軟件工程師,但是我們沒有一個像他那樣的巨星?!?
Serah Drasner:“在軟件工程領域,不光是Vue.js,在所有軟件開發領域,我們都有一個概念叫做“終身仁慈獨裁者,描述有些人是項目行動策劃的關鍵大腦,來推動項目的前進,它并不意味著,有些人把它搞混了,認為這意味著沒有其他人為這個項目而工作,這完全是錯誤的,你可以看到有非常多的人在為Vue.js和Vue核心而共同工作,它確實意味著它背后有一種推進力量,我覺得當存在一個像Vue.js這樣不是產自大公司的項目,是有好處的因為它的內容是非常真實的,我們得到的好處就是,我們不是被某一個公司推動的",這意味著不是大公司在指定規則,是人們?!?
Taylor:“Evan有一個總體的巨大的圖景,Angular和React是由大公司搭建起來的,從一開始就有很多人參與其中,它們像是被一個委員會設計的,但對與Vue.js和Evan來說,在他大腦中有一個單一的圖景,他希望創造這個整體的框架,我覺得這對于創造一個非常好的產品是非常重要的,這也是Vue.js如此成功的原因?!?
LinusBorg:“前端市場被臉書的React統治,Angular是谷歌的,然后就是我們,像是一堆書呆子做他們自己的框架,完全是因為他們想做?!?
Scott Tolinski:“它給人的感覺不像是一個公司,這樣的話會讓人更激動,更多像是民間的,光是這種想法就足以讓人們感到興奮,讓人們想去使用它?!?
LinusBorg:“Vue.js仍然在成長,它不是在推翻前端領域成為統治者,這不是它的目標,我們只是很高興能為這個項目而工作,我們很高興看到它的成長,也對它未來的發展感到很激動,我也不是很確定。”
尤大大:“我很自豪當初邁出了那一步,離開了早9晚5的工作,并開始投身于我真正熱愛的事業上去,有時候我會看著那些星星,我們有了那么多用戶,那么多的下載,但是什么給了我最大的成就感和滿足感呢,是每次我看到這些人們",尤其是在結束一場集會之后,人們會走過通常握著我的手說謝謝你,Evan,謝謝你讓我的生活變得更輕松,就是這些場景讓我覺得,這就是我開發Vue.js的原因,我創造了這個東西,我把他分享給人們,希望它能讓人們的生活變得更輕松,人們向我走過來,感謝我做出了這個東西,這也算完成這個循環了"
如今Vue框架火爆IT行業,已成為前端工程師的必備技能,其實也是博主我現在飯碗依靠的技術,Vue現在的Star數近20w,使用者更是數不勝數,正如尤大大表示自己很自豪的邁出那一步,其實作為受益者的我們更是感恩尤大大當時所做的決定;在CSDN的《新程序員004》中也曾攥入“與Vue.js作者尤雨溪暢談他的程序人生”的篇幅,有興趣的朋友可以閱讀一下尤雨溪Vue登榜GitHub之路看似不難,加油每一位前端人。博主作為尤大大的忠實粉絲且作為Vue技術的熱愛者并在CSDN構建了Vue技能樹,歡迎大家打卡學習哦 https://edu.csdn.net/skill/vue
uni-app 是一個使用 Vue.js (opens new window)開發所有前端應用的框架,開發者編寫一套代碼,可發布到iOS、Android、Web(響應式)、以及各種小程序(微信/支付寶/百度/頭條/飛書/QQ/快手/釘釘/淘寶)、快應用等多個平臺。DCloud公司擁有900萬開發者、數百萬應用、12億手機端月活用戶、數千款uni-app插件、70+微信/qq群。阿里小程序工具官方內置uni-app(詳見 (opens new window)),騰訊課堂官方為uni-app錄制培訓課程(詳見 (opens new window)),開發者可以放心選擇。uni-app在手,做啥都不愁。即使不跨端,uni-app也是更好的小程序開發框架(詳見 (opens new window))、更好的App跨平臺框架、更方便的H5開發框架。不管領導安排什么樣的項目,你都可以快速交付,不需要轉換開發思維、不需要更改開發習慣。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
目錄
一、前言介紹:
隨著現在網絡的快速發展,網上管理系統也逐漸快速發展起來,網上管理模式很快融入到了許多商家的之中,隨之就產生了“網上圖書購物系統”,這樣就讓網上圖書購物系統更加方便簡單。
對于本網上圖書購物系統的設計來說,系統開發主要是采用java語言技術,在整個系統的設計中應用MySQL數據庫來完成數據存儲,具體根據網上圖書購物系統的現狀來進行開發的,具體根據現實的需求來實現網上圖書購物系統網絡化的管理,各類信息有序地進行存儲,進入網上圖書購物系統頁面之后,方可開始操作主控界面,主要功能包括管理員:首頁、個人中心、圖書分類管理、回收類別管理、新書榜管理、特價區管理、舊書回收管理、用戶管理、訂單評價管理、回收預約管理、圖書回收管理、管理員管理、系統管理、訂單管理。前臺使用:首頁、新書榜、特價區、舊書回收、公告資訊、個人中心、后臺管理、購物車、客服。用戶:首頁、個人中心、訂單評價管理、回收預約管理、圖書回收管理、我的收藏管理、訂單管理等功能。
本系統主要講述了網上圖書購物系統開發背景,該系統它主要是對需求分析和功能需求做了介紹,并且對系統做了詳細的測試和總結。具體從業務流程、數據庫設計和系統結構等多方面的問題。望能利用先進的計算機技術和網絡技術來改變目前的網上圖書購物系統狀況,提高管理效率。
二、系統分析:
可行性分析主要是針對這個項目開發是否有意義和價值觀來進行的全面分析,在分析的過程當中發現這個系統所存在的不足之處。就拿這次網上圖書購物系統的設計與實現來說主要是針對一些用戶在發布網上圖書購物系統信息時遇到不方便的操作和問題來進行解決問題的,最后能夠讓網上圖書購物系統開發得到最大的用處。而且對于用戶方面我們可以提供給一個簡單方便操作的網上圖書購物系統。所以我們要計算開發這個系統它能否有效的解決好這個系統經濟問題,在開發完成以后所帶來的利益是否大于開發過成當中的成本。所以可行性的研究與分析是這個系統在開發和設計上是必不可缺少的一部分。從該系統文章的全部來看,我們要從以下幾個方面進行分析:
技術可行性:在技術方面我們要從現有自己掌握的技術能否設計出我們當初所預定的目標。
經濟可行性:在這次系統開發和設計過程當中所用的經費是否大于以后給社會帶來的價值觀。
操作可行性:系統在用戶使用過程當中是否方便、簡單,能否達到大部分的用戶會使用。
技術的可行性分析主要是針對開發該系統所用到技術進行分析,對于網上圖書購物系統的設計,可以在任何一個地方都進行使用和管理。通過當前我們所學的程序開發和語言介紹利用以上的技術開發該系統是比較合適的。而且我們在使用的數據庫也是要保證這個系統的完整性、數據安全性好的條件。
經濟可行性主要是決定這個系統是否具有價值存在,是否具有開發意義,如果開發的項目不能夠節約物品和資源,反而使用的大量的人力、財力和物力不成正比甚至小于投資成本,那么該項目是不具備開發意義和價值的。在開發本項目的初期,節約成本是最基本的,設計和開發都是由本人一人完成的,并且在開發中使我學習到了很多的知識,也開拓了自己的眼界,在通過可行性分析之后,該項目的利大于弊,所以該項目是具有開發意義和價值的。
網上圖書購物系統的開發登錄界面它是我們最常見的一種登錄窗口來完成的,用戶可以使電腦來進行登錄并簡單的訪問不需要做任何的操作。對于此次的系統開發它主要是基于B/S結構和java技術及MySQL數據庫來完成,讓系統開發更加完美和完善,所以我們開出的系統界面更加人性化,用戶使用也更加方便。而且系統在使用過程當中也擁有方便操作、易管理等特點。
經過以上的敘述,所以開發此系統在經濟上、技術上是滿足開發條件的。
1.如果我們想要對前后臺處理的層次分明那么我們就要采用B/S模式來進行系統的開發這樣就可以方便用戶的使用。
2.對于系統的開發和設計我們就要采用大家日常所需要的要求,這樣一來可以提高系統的適用性也能保證系統利用價值。對于一個系統來說一個好的框架是很重要的,因為一個好的框架它可以提高系統的穩定和高效性。
3.在系統界面上也要設計一個方便快捷的登錄界面,這樣就可以提高用戶對系統操作性和適用性。
4.在系統模塊設計當中我們要對系統各個模塊進行合理簡化和設計,這樣就能提高系統使用性。
5.對于一個完整的系統來說對于它的測評和測試是比較重要的,所以我們在軟件設計程序中要保持軟件占用的時間和速度快的特點。
6.對于這個系統來說我們首先要考慮所設計出的系統它具有那些突破和體現,所以我們盡力去改進這個系統去適應用戶。
對于一個新的網站來說開發新網站我們就要做出這個系統的任務需求分析,因為對系統分析的質量好壞它可以決定這個網站開發的意義,俗話說得好一個好的開頭是成功的一半,對于開發這個網上圖書購物系統來說前期的分析是比較重要的,所以任務分析它可以決定這系統的開展和設計,這樣就可以保證用戶滿意性。
任務的需求它能決定這個系統開發過程當中一個重要環節,所以我們在系統開發過程所用質量是比較重要的,因為我們在系統應用過程當中不一定那會出現問題,所以我們在進行對系統分析是比較重要的,因為它可以決定這個系統功能和需求。
本課題要求實現一套網上圖書購物系統的開發與實現,主要實現功能包括管理員:首頁、個人中心、圖書分類管理、回收類別管理、新書榜管理、特價區管理、舊書回收管理、用戶管理、訂單評價管理、回收預約管理、圖書回收管理、管理員管理、系統管理、訂單管理,
前臺使用:首頁、新書榜、特價區、舊書回收、公告資訊、個人中心、后臺管理、購物車、客服,
用戶:首頁、個人中心、訂單評價管理、回收預約管理、圖書回收管理、我的收藏管理、訂單管理 網上圖書購物系統。
前端用戶發送登錄請求-驗證輸入的賬號虛線-執行數據查詢-返回查詢結果-判斷用戶是否存在-前端根據結果集執行不同的操作。
通過系統需求分析,本網上圖書購物系統主要實現功能包括;管理員:首頁、個人中心、圖書分類管理、回收類別管理、新書榜管理、特價區管理、舊書回收管理、用戶管理、訂單評價管理、回收預約管理、圖書回收管理、管理員管理、系統管理、訂單管理。用戶:首頁、個人中心、訂單評價管理、回收預約管理、圖書回收管理、我的收藏管理、訂單管理等功能。其功能結構圖如下圖所示。
在該系統的信息中,由于數據庫的支持,我們可以對數據庫進行收集、整理、更新和加工等操作。由于數據庫的存儲功能強大,所以數據庫已經成為了計算機必不可少的,一個數據庫的好壞直接影響該系統的質量和效率。一個系統中的數據庫是必不可少的,并且起著決定性因素。通過之前的系統分析,可以規劃出本系統中使用的主要等,下面設計出這幾個關鍵實體的實體關系圖。
訂單管理實體E-R圖如圖4-2所示
三、功能截圖:
通過填寫用戶名、密碼、角色等信息,輸入完成后選擇登錄即可進入網上圖書購物系統。
網上圖書購物系統,在系統首頁可以查看首頁、新書榜、特價區、舊書回收、公告資訊、個人中心、后臺管理、購物車、客服等內容。
新書榜,在新書榜頁面可以填寫圖書名稱、分類、標簽、圖片、作者、出版社、發行日期、價格等內容進行立即購買。
圖書頁面,可以收藏,加入購物車,查看詳情以及評論和購買等操作。
購物車詳情,可以添加數量和刪除。點擊購買進行模擬支付結賬。
選擇收貨地址后點擊支付下單:
支付成功后查看物流狀態信息:
查看回收詳情,點擊回收預約填寫相關信息,完成后管理員進行審核。
個人中心可以查看修改個人信息,查看訂單得各種狀態,以及對收貨地址進行查看修改和收藏進行查看。也可以收貨地址頁面可以填寫聯系人、手機號碼、地址、默認等內容進行添加地址
普通用戶后臺管理:
可以對客服進行留言聊天。
管理員對新書榜管理查看圖書名稱、分類、標簽、圖片、作者、出版社、發行日期、價格等信息進行詳情、刪除、修改、查看等操作。
四、代碼實現:
-
-
/**
-
* 新書榜
-
* 后端接口
-
* @author
-
* @email
-
* @date 2022-01-18 07:36:34
-
*/
-
@RestController
-
@RequestMapping("/xinshubang")
-
public class XinshubangController {
-
@Autowired
-
private XinshubangService xinshubangService;
-
-
/**
-
* 后端列表
-
*/
-
@RequestMapping("/page")
-
public R page(@RequestParam Map<String, Object> params,XinshubangEntity xinshubang, HttpServletRequest request){
-
-
EntityWrapper<XinshubangEntity> ew = new EntityWrapper<XinshubangEntity>();
-
PageUtils page = xinshubangService.queryPage(params, MPUtil.sort(MPUtil.between(MPUtil.likeOrEq(ew, xinshubang), params), params));
-
return R.ok().put("data", page);
-
}
-
-
/**
-
* 查詢
-
*/
-
@RequestMapping("/query")
-
public R query(XinshubangEntity xinshubang){
-
EntityWrapper< XinshubangEntity> ew = new EntityWrapper< XinshubangEntity>();
-
ew.allEq(MPUtil.allEQMapPre( xinshubang, "xinshubang"));
-
XinshubangView xinshubangView = xinshubangService.selectView(ew);
-
return R.ok("查詢新書榜成功").put("data", xinshubangView);
-
}
-
-
/**
-
* 后端詳情
-
*/
-
@RequestMapping("/info/{id}")
-
public R info(@PathVariable("id") Long id){
-
XinshubangEntity xinshubang = xinshubangService.selectById(id);
-
xinshubang.setClicknum(xinshubang.getClicknum()+1);
-
xinshubang.setClicktime(new Date());
-
xinshubangService.updateById(xinshubang);
-
return R.ok().put("data", xinshubang);
-
}
-
-
-
/**
-
* 前端保存
-
*/
-
@RequestMapping("/add")
-
public R add(@RequestBody XinshubangEntity xinshubang, HttpServletRequest request){
-
xinshubang.setId(new Date().getTime()+new Double(Math.floor(Math.random()*1000)).longValue());
-
//ValidatorUtils.validateEntity(xinshubang);
-
-
xinshubangService.insert(xinshubang);
-
return R.ok();
-
}
-
-
-
/**
-
* 刪除
-
*/
-
@RequestMapping("/delete")
-
public R delete(@RequestBody Long[] ids){
-
xinshubangService.deleteBatchIds(Arrays.asList(ids));
-
return R.ok();
-
}
-
-
/**
-
* 前端智能排序
-
*/
-
@IgnoreAuth
-
@RequestMapping("/autoSort")
-
public R autoSort(@RequestParam Map<String, Object> params,XinshubangEntity xinshubang, HttpServletRequest request,String pre){
-
EntityWrapper<XinshubangEntity> ew = new EntityWrapper<XinshubangEntity>();
-
Map<String, Object> newMap = new HashMap<String, Object>();
-
Map<String, Object> param = new HashMap<String, Object>();
-
Iterator<Map.Entry<String, Object>> it = param.entrySet().iterator();
-
while (it.hasNext()) {
-
Map.Entry<String, Object> entry = it.next();
-
String key = entry.getKey();
-
String newKey = entry.getKey();
-
if (pre.endsWith(".")) {
-
newMap.put(pre + newKey, entry.getValue());
-
} else if (StringUtils.isEmpty(pre)) {
-
newMap.put(newKey, entry.getValue());
-
} else {
-
newMap.put(pre + "." + newKey, entry.getValue());
-
}
-
}
-
params.put("sort", "clicknum");
-
-
params.put("order", "desc");
-
PageUtils page = xinshubangService.queryPage(params, MPUtil.sort(MPUtil.between(MPUtil.likeOrEq(ew, xinshubang), params), params));
-
return R.ok().put("data", page);
-
}
-
-
-
}
-
<!DOCTYPE html>
-
<html>
-
<head lang="en">
-
<meta charset="utf-8">
-
<title>新書榜</title>
-
<meta name="keywords" content="" />
-
<meta name="description" content="" />
-
<meta name="renderer" content="webkit">
-
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
-
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
-
<link rel="stylesheet" href="../../layui/css/layui.css">
-
<link rel="stylesheet" href="../../xznstatic/css/common.css"/>
-
<link rel="stylesheet" href="../../xznstatic/css/style.css"/>
-
<script type="text/javascript" src="../../xznstatic/js/jquery-1.11.3.min.js"></script>
-
<script type="text/javascript" src="../../xznstatic/js/jquery.SuperSlide.2.1.1.js"></script>
-
</head> </div>
-
<div class="pager" id="pager" :style="{textAlign:1==1?'left':1==2?'center':'right'}"></div>
-
</div>
-
</div
-
</div>
-
<script src="../../layui/layui.js"></script>
-
<script src="../../js/vue.js"></script>
-
<script src="../../js/config.js"></script>
-
<script src="../../modules/config.js"></script>
-
<script src="../../js/utils.js"></script>
-
<script>
-
// 獲取輪播圖 數據
-
http.request('config/list', 'get', {
-
page: 1,
-
limit: 5
-
}, function(res) {
-
if (res.data.list.length > 0) {
-
let swiperList = [];
-
res.data.list.forEach(element => {
-
if (element.value != null) {
-
swiperList.push({
-
img: element.value
-
});
-
}
-
});
-
vue.swiperList = swiperList;
-
-
vue.$nextTick(() => {
-
carousel.render({
-
elem: '#test1',
-
width: '100%',
-
height: '430px',
-
arrow: 'hover',
-
anim: 'default',
-
autoplay: 'true',
-
interval: '3000',
-
indicator: 'inside'
-
});
-
});
-
-
//類型查詢
-
categoryList();
-
//類型搜索
-
$(document).on("click", ".category-search", function(e){
-
vue.swiperIndex = $(this).attr('index')
-
pageList(e.target.innerText);
-
});
-
// 獲取列表數據
-
http.request('xinshubang/list', 'get', param, function(res) {
-
vue.dataList = res.data.list
-
// 分頁
-
laypage.render({
-
elem: 'pager',
-
count: res.data.total,
-
limit: limit,
-
groups: 3,
-
layout: ["prev","page","next"],
-
theme: '#065279',
-
jump: function(obj, first) {
-
param.page = obj.curr;
-
//首次不執行
-
if (!first) {
-
http.request('xinshubang/list', 'get', param, function(res) {
-
vue.dataList = res.data.list
-
})
-
}
-
});
-
window.xznSlide = function() {
-
jQuery(".banner").slide({mainCell:".bd ul",autoPlay:true,interTime:5000});
-
jQuery("#ifocus").slide({ titCell:"#ifocus_btn li", mainCell:"#ifocus_piclist ul",effect:"leftLoop", delayTime:200, autoPlay:true,triggerTime:0});
-
jQuery("#ifocus").slide({ titCell:"#ifocus_btn li", mainCell:"#ifocus_tx ul",delayTime:0, autoPlay:true});
-
jQuery(".product_list").slide({mainCell:".bd ul",autoPage:true,effect:"leftLoop",autoPlay:true,vis:5,trigger:"click",interTime:4000});
-
};
-
</script>
-
</body>
-
</html>
五、文檔目錄:
六、項目總結:
對于本次的系統開發來看,它主要是把我以前所學的知識進行了一次綜合的應用。經過這次畢業設計的制作它主要是把我以前所學的理論知識應用到社會實踐當中。通過這一次的網上圖書購物系統的設計與實現它能夠有效把計算機知識與實際問題相互應用,通過計算機網絡技術來解決用戶生活當中的實際問題,從而提高我的編程能力。雖然在這次畢業設計當中我遇到了很多的問題和困難,但是通過不斷的調試和老師的幫助讓我圓滿的完成了這次畢業設計。通過這次畢業設計的制作讓我對計算機實際應用得到了很強的鍛煉,同時也大大的提高了我的動手動腦能力,讓我也感受到了其中的樂趣和喜悅。
通過這次項目設計的撰寫把我在大學期間所學到的東西都應用上了,但是我覺得還是微不足道的,因為在這次畢業設計當中讓我深深的了解到對于軟件開發和學習理論知識它是兩個完全不同的概念。但是通過這次軟件的開發讓我在以后的工作當中打下了良好的基礎。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
作為后端開放人員,最煩的事就是自己寫接口文檔和別人沒有寫接口文檔,不管是前端還是后端開發,多多少少都會被接口文檔所折磨,前端會抱怨后端沒有及時更新接口文檔,而后端又會覺得編寫接口文檔太過麻煩。Swagger 可以較好的接口接口文檔的交互問題,以一套標準的規范定義接口以及相關的信息,就能做到生成各種格式的接口文檔,生成多種語言和客戶端和服務端的代碼,以及在線接口調試頁面等等。只需要更新 Swagger 描述文件,就能自動生成接口文檔,做到前端、后端聯調接口文檔的及時性和便利性。
Swagger 是一個規范且完整的框架,用于生成、描述、調用和可視化 RESTful 風格的 Web 服務。
Swagger 的目標是對 REST API 定義一個標準且和語言無關的接口,可以讓人和計算機擁有無須訪問源碼、文檔或網絡流量監測就可以發現和理解服務的能力。當通過 Swagger 進行正確定義,用戶可以理解遠程服務并使用最少實現邏輯與遠程服務進行交互。與為底層編程所實現的接口類似,Swagger 消除了調用服務時可能會有的猜測。
Swagger 的優勢
通過在項目中引入 Springfox,可以掃描相關的代碼,生成該描述文件,進而生成與代碼一致的接口文檔和客戶端代碼。
<!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-spring-web</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
![]()
在配置文件 config
目錄下,添加 swagger 的配置文件 SwaggerConfig.java
@Configuration // 配置類 @EnableSwagger2 // 開啟 swagger2 的自動配置 public class SwaggerConfig { }
這個時候 Swagger 已經算是整合到項目之中了,可以啟動下服務,輸入:http://localhost:8080/swagger-ui.html#
(這里我的項目端口是 8868 ,項目路徑是 /mike,所以我打開的文檔地址為 http://localhost:8868/mike/swagger-ui.html#
)即可查看 swagger 文檔。
可以看到 Swagger 文檔中大概有這四類信息
Swagger 有自己的實例 Docket,如果我們想要自定義基本信息,可以使用 docket 來配置 swagger 的基本信息,基本信息的設置在 ApiInfo
這個對象中。
Swagger 默認的基本信息展示
ApiInfo 中默認的基本設置
SwaggerConfig.java
配置文件添加以下內容:
@Bean public Docket docket() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) // 配置基本信息 .apiInfo(apiInfo()) ; } // 基本信息設置 private ApiInfo apiInfo() { Contact contact = new Contact( "米大傻", // 作者姓名 "https://blog.csdn.net/xhmico?type=blog", // 作者網址 "777777777@163.com"); // 作者郵箱 return new ApiInfoBuilder() .title("多加辣-接口文檔") // 標題 .description("眾里尋他千百度,慕然回首那人卻在燈火闌珊處") // 描述 .termsOfServiceUrl("https://www.baidu.com") // 跳轉連接 .version("1.0") // 版本 .license("Swagger-的使用(詳細教程)") .licenseUrl("https://blog.csdn.net/xhmico/article/details/125353535") .contact(contact) .build(); }
![]()
重啟服務,打開 Swagger 文檔,基本信息改變如下所示:
默認情況下,Swagger 是會展示所有的接口信息的,包括最基礎的 basic-error
相關的接口
有時候我們希望不要展示 basic-error-controller
相關的接口,或者是說這想要顯示某些接口,比如說:user-controller
下的接口,由該怎么去實現呢?這個時候就需要設置 掃描接口
@Bean public Docket docket() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) // 配置接口信息 .select() // 設置掃描接口 // 配置如何掃描接口 .apis(RequestHandlerSelectors //.any() // 掃描全部的接口,默認 //.none() // 全部不掃描 .basePackage("com.duojiala.mikeboot.controller") // 掃描指定包下的接口,最為常用 //.withClassAnnotation(RestController.class) // 掃描帶有指定注解的類下所有接口 //.withMethodAnnotation(PostMapping.class) // 掃描帶有只當注解的方法接口 ) .paths(PathSelectors .any() // 滿足條件的路徑,該斷言總為true //.none() // 不滿足條件的路徑,該斷言總為false(可用于生成環境屏蔽 swagger) //.ant("/user/**") // 滿足字符串表達式路徑 //.regex("") // 符合正則的路徑 ) .build(); }
![]()
可根據自己的需求去設置對應的配置,這里我就不再一一贅述了,以上是我所設置的配置,重啟服務,打開 Swagger 文檔,接口信息改變如下所示:
可以看到之前 basic-error-controller
相關的接口已經沒有了
Swagger 默認只有一個 default 分組選項,如果沒有設置,所有的接口都會顯示在 default
`分組下,如果功能模塊和接口數量一多,就會顯得比較凌亂,不方便查找和使用。
swagger 文檔中組名默認是 default
,可通過 .groupName(String )
@Bean public Docket docket() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改組名為 "mike" ; }
修改后:
如果需要配置多個組的話,就需要配置多個 docket() 方法
,這里我就簡單寫兩組,代碼如下:
/** * 展示 controller 包下所有的接口 */ @Bean public Docket docket1() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改組名為 "mike" // 配置接口信息 .select() // 設置掃描接口 // 配置如何掃描接口 .apis(RequestHandlerSelectors .basePackage("com.duojiala.mikeboot.controller") // 掃描指定包下的接口,最為常用 ) .paths(PathSelectors .any() // 滿足條件的路徑,該斷言總為true ) .build(); } /** * 展示路徑為 /error 的所有接口(基礎接口) */ @Bean public Docket docket2() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .groupName("yank") // 修改組名為 "yank" // 配置接口信息 .select() // 設置掃描接口 // 配置如何掃描接口 .apis(RequestHandlerSelectors .any() // 掃描全部的接口,默認 ) .paths(PathSelectors .ant("/error") // 滿足字符串表達式路徑 ) .build(); }
![]()
重啟服務,打開 Swagger 文檔,接口信息改變如下所示:
組名為 mike
的文檔中只有 user-controller
相關的接口信息
組名為 yank
的文檔中只有 basic-error-controller
相關的接口信息
在開發或者測試環境下,我們開啟 swagger 會方便前端和后端的交互,但是如果在生產環境下也開啟 swagger 的話,是會將接口暴露出去的,有極大風險,如何讓 swagger 根據不同的環境來決定是否開啟?
這里我準備了四個項目的配置文件,dev
、test
、pro
三個環境的配置文件僅是端口上的不同
application.yml
內容如下,用于指定選擇的環境:
spring: profiles: active: dev
可以通過代碼判斷此時是在什么環境:dev
、test
、pro
,如果是在 pro
生產環境,則關閉 swagger。
/** * swagger 配置 * @param environment 環境 */ @Bean public Docket docket(Environment environment) { // 設置環境范圍 Profiles profiles = Profiles.of("dev","test"); // 如果在該環境返回內則返回:true,反之返回 false boolean flag = environment.acceptsProfiles(profiles); // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .enable(flag) // 是否開啟 swagger:true -> 開啟,false -> 關閉 ; }
![]()
在 application.yml
全局配置文件中環境指向 dev
時,是可以打開 swagger 的
如果我將 application.yml
全局配置文件中環境指向 pro
時,就不能打開 swagger 了,提示 Could not render e, see the console
之前有說 Swagger 會將接口請求或者相應的實體類信息展示在 Models
下的,比如我 UserController.java
下有一個接口如下所示:
@PostMapping(value = "/query-user-info") public ResponseBean queryUserInfo(@RequestBody @Validated IdReq req) { return ResponseBean.success(userService.queryUserInfo(req)); }
它的請求體是 IdReq
,響應是 ResponseBean
,Models
展示這兩個實體類信息如下:
前端可通過看這個 Models
知道后端定義實體類的信息。
該注解是作用于類上面的,是用來描述類的一些基本信息的。
相關屬性:
value
:提供類的一個備用名,如果不設置,默認情況下將使用 class 類的名稱
譬如:這個為給 IdReq
這個類添加該注解
@Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "Id請求體") public class IdReq { private String id; }
可以看到這里的名字從 IdReq
變成 Id請求體
了
它的作用是添加和操作屬性模塊的數據。
該注解的使用詳情可參見博客:@ApiModelProperty注解的用法
這里我還是以 IdReq
類為例,為該類的屬性添加說明
@Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "Id請求體") public class IdReq { @ApiModelProperty("主鍵id") private String id; }
可以看到這里對該字段有一個備注說明。
該注解用來對某個方法/接口進行描述
該注解的使用詳情可參見博客:Swagger @ApiOperation 注解詳解
這里我以 UserController
下的接口為例:
@PostMapping(value = "/query-user-info") @ApiOperation(value = "根據id查詢用戶詳情") public ResponseBean queryUserInfo(@RequestBody @Validated IdReq req) { return ResponseBean.success(userService.queryUserInfo(req)); }
可以看見該接口就多了對其的描述信息。
該注解使用在方法上或者參數上,字段說明,表示對參數的添加元數據(說明或者是否必填等)
相關屬性:
這里我以 UserController
下的接口為例:
@PostMapping(value = "/query-user-infos") @ApiOperation(value = "條件查詢用戶信息") public ResponseBean queryUserInfos( // name 用戶名稱 不必填 @ApiParam(value = "用戶名稱", required = false) @RequestParam(required = false) String name, // gender 用戶性別 必填 @ApiParam(value = "用戶性別", required = true) @RequestParam(required = true) GenderEnum gender ) { return ResponseBean.success(userService.queryUserInfos(name,gender)); }
這里會展示請求參數的備注信息,以及是否必填等。
使用 swagger 除了讓前后端交互變得方便,也讓接口的請求變得簡單,只需要填寫好請求所需要的參數信息,便可直接發起請求。
比如說接口 /user/query-user-info
點擊 Try it out
設置好請求所需的參數,點擊 Execute
執行
就能看到接口響應的結果了
接口 /user/query-user-infos
也差不多
有時候我們的接口是需要獲取請求頭信息的,這樣的話就還需要在 swagger 配置中添加請求頭的配置。
@Bean public Docket docket() { // 設置請求頭 List<Parameter> parameters = new ArrayList<>(); parameters.add(new ParameterBuilder() .name("token") // 字段名 .description("token") // 描述 .modelRef(new ModelRef("string")) // 數據類型 .parameterType("header") // 參數類型 .defaultValue("default value") // 默認值:可自己設置 .hidden(true) // 是否隱藏 .required(false) // 是否必須 .build()); // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改組名為 "mike" // 配置接口信息 .select() // 設置掃描接口 // 配置如何掃描接口 .apis(RequestHandlerSelectors .basePackage("com.duojiala.mikeboot.controller") // 掃描指定包下的接口,最為常用 ) .paths(PathSelectors .any() // 滿足條件的路徑,該斷言總為true ) .build() // 添加請求頭參數 .globalOperationParameters(parameters); }
![]()
比如接口:
@GetMapping(value = "/get-token") @ApiOperation(value = "獲取請求頭中的token信息") public void getToken( @RequestHeader(value = "token",required = false) String token ) { // 直接獲取 token 信息 System.out.println("token = " + token); // 通過代碼獲取 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (servletRequestAttributes != null) { HttpServletRequest request = servletRequestAttributes.getRequest(); String header = request.getHeader("token"); System.err.println("header = " + header); } }
![]()
可以看到這個接口已經可以去設置請求頭了,調用接口
后端也能獲取到。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
項目 | 描述 |
---|---|
開發語言 | HTML、JavaScript、CSS |
庫 | dyCalendarJS、vanilla-tilt |
Edge | 108.0.1462.54 (正式版本) (64 位) |
該項目中需要使用到的庫有:
如果你在觀看本篇文章前并沒有對這兩個庫進行了解,歡迎移步至我的另外兩篇文章進行學習:
該項目文件中我已對代碼進行了注釋。如遇不懂的地方,請嘗試查看相關注釋。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>毛玻璃-傾斜-日歷</title> <!-- 導入自定義 CSS 文件 --> <link rel="stylesheet" href="./index.css"> <!-- 導入 dycalendar.css --> <link rel="stylesheet" href="../dycalendar.min.css"> </head> <body> <div id="calendar" class="dycalendar-container"></div> <!-- 導入 dycalendar.js --> <script src="../dycalendar.min.js"></script> <!-- 導入 vanilla-tilt.js --> <script src="../vanilla-tilt.js"></script> <script> // 繪制日歷 dycalendar.draw({ target: '#calendar', // 指定用于創建日歷的 HTML 容器 type: 'month', // 設置日歷的類型 prevnextbutton: 'show', // 顯示 "<" 及 ">" 按鈕 highlighttoday: true // 高亮顯示當前日期 }) // 為目標元素添加傾斜效果 VanillaTilt.init(document.querySelector('#calendar'), { target: '#calendar', // 指定需要添加傾斜效果的目標元素 scale: 0.8, // 鼠標懸停于目標元素上時,目標元素的放縮倍數 glare: true, // 是否設置反光效果 'max-glare': 0.6 // 設置反光效果的強度 }) </script> </body> </html>
![]()
*{ /* 去除元素默認的內外邊距 */ margin: 0px; padding: 0px; /* 設置邊框時將壓縮內容區域,而不會向外擴張。 也就是說,為某個元素設置邊框并不會改變其寬高。 */ box-sizing: border-box; } body{ /* 顯示區域的最小高度為顯示窗口的高度 */ min-height: 100vh; /* 設置該元素內部元素居中顯示 */ display: flex; justify-content: center; align-items: center; /* 設置該元素的背景顏色 */ background-color: #161623; } body::before{ /* 若需要正常使用偽元素,必須為其設置 content 屬性 */ content: ''; width: 400px; height: 400px; /* 設置顏色漸變效果 */ background: linear-gradient(#ffc107,#e91e63); /* 設置邊框圓角,當該屬性的值為 50% 時元素邊框將顯示為一個圓 */ border-radius: 50%; /* 為該元素設置絕對定位,阻止該元素遮擋日歷 (定位元素可以設置 z-index 來調節顯示順序, z-index 的值越高,顯示優先級越大)。 */ position: absolute; top: 10%; left: 20%; z-index: -1; } body::after{ content: ''; width: 300px; height: 300px; position: absolute; background: linear-gradient(#2196f3,#31ff38); border-radius: 50%; top: 45%; left: 55%; z-index: -1; } #calendar{ /* 設置日歷的寬高 */ width: 400px; height: 400px; color: #fff; /* 設置日歷的背景元素,為產生毛玻璃效果,這里將背景顏色設置為白色, 將透明度設置為 0.1(透明度的取值范圍為 0~1,取值越接近 1 ,顏色 越不透明)。 */ background-color: rgb(255, 255, 255, 0.1); /* 設置 blur 過濾器,該過濾器可以將背景模糊化,參數中的 像素值設定越高,顯示得越是模糊。 */ backdrop-filter: blur(50px); /* 分別設置日歷的四條邊框,使日歷顯示得更為立體 */ border-top: 1px solid rgb(255, 255, 255, 0.5); border-left: 1px solid rgb(255, 255, 255, 0.5); border-right: 1px solid rgb(255, 255, 255, 0.2); border-bottom: 1px solid rgb(255, 255, 255, 0.2); border-radius: 5px; /* 設置日歷的內邊距 */ padding: 0px 20px; /* 設置日歷周邊的陰影效果,box-shadow 接收的值(如下)分別為 陰影的 X 偏移量、陰影的 Y 偏移量、擴散半徑、陰影顏色。 */ box-shadow: 5px 10px 10px rgb(0, 0, 0, 0.1); } /* 這里存在許多在 HTML 文件中沒有看到的類名,這是因為這些標簽 是 dyCalendarJS 通過 JavaScript 動態創建的元素,如果有需要對 日歷中的某些元素的樣式進行改變,可以通過瀏覽器的 檢查 功能來查看 JavaScript 創建的元素并對其樣式進行適當的修改。 */ /* 有些元素需要通過修改傳遞給 dycalendar.draw() 的配置對象中的 部分屬性才能夠被發現。 */ /* 設置日歷的頭部部分的樣式 */ #calendar .dycalendar-header{ margin-top: 60px; font-size: 20px; } /* 設置日歷 "<" 及 ">" 按鈕的樣式,應用該樣式時請將 傳遞給 dycalendar.draw() 的配置對象中的 prevnextbutton 屬性的值設置為 true 。 */ #calendar .dycalendar-header .prev-btn, #calendar .dycalendar-header .next-btn{ width: 40px; height: 30px; background-color: rgb(255, 255, 255, 0.15); /* 設置文本對其方式及行高以使 ">" 及 "<" 居中顯示 */ text-align: center; line-height: 30px; /* 設置上下方向的外邊距為 0px,設置左右方向的外邊距為 5px */ margin: 0px 5px; } #calendar .dycalendar-body table{ width: 100%; height: 100%; margin-top: 50px; } /* tr:nth-child(1) 選擇 table 標簽中的第一個 tr 元素 */ /* 設置日歷中星期(星期幾)標識的樣式 */ #calendar .dycalendar-body table tr:nth-child(1) td{ background-color: rgb(255, 255, 255, 0.15); margin-bottom: 20px; } #calendar .dycalendar-body table td{ border-radius: 3px; /* 設置鼠標懸停時的指針樣式 */ cursor: pointer; } /* :hover 偽類選擇器用于設置鼠標懸停在指定元素時, 某個元素的樣式 */ #calendar .dycalendar-today-date, #calendar .dycalendar-body table td:hover{ color: #000; /* 使用 !important 提升該屬性在多個設置了該屬性的選擇器 中的權重 */ background-color: #fff !important; }
分享此文一切功德,皆悉回向給文章原作者及眾讀者. 免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
最近也是臨近期末了,各種的期末大作業,后臺管理也是很多地方需要用到的,為了方便大家能快速上手,快速搭建一個簡單的后臺管理,我花了兩天時間整理了一下
我會從0開始介紹,從數據庫的設計到前端頁面的引入最后到后端代碼的編寫,你只需要會一點前端的基礎和ssm的基礎就能快速上手搭建一個簡單的后臺管理
本次案例分兩篇文章教學:
(第一篇):數據表設計,前端框架引入和編寫前端頁面,搭建基本的springboot項目,引入前端到springboot項目中,在瀏覽器顯示
(第二篇):后端代碼的設計,這部分邏輯涉及的比較多,所以單獨放一篇出來講,代碼從0手敲講解,保證你能學會,完成增刪改查的功能
目錄
前言和環境介紹
無論是做app,網站,還是小程序,都少不了后臺管理
那么對于前端不是很會,后端也是只會一些的人來說,如何快速搭建一個簡單的后臺管理系統呢,哎別急,今天就來教大家簡單快速搭建一個后臺管理系統
首先,簡單介紹一下我的開發環境
工具 用處 H+ 前端框架,直觀的教程文檔,非常實用 SpringBoot 后端框架,簡單上手,搭建快 MySQL 數據庫 IDEA 非常強大的編譯器 Ajax 異步請求,前端向后端發送請求 thymeleaf 模板引擎,實時渲染頁面,基于HTML HBuild X 前端編譯器,用其他的也可以,看自己 好了,環境介紹完畢,我們先從前端界面做起
數據庫
一個后臺管理,肯定少不了數據,不然怎么叫后臺管理呢
這里我是用的是MySQL數據庫,當然你使用其他的也行,不過后面在SpringBoot中要做不同的配置
在MySQL中新建一個user數據庫,新建一個t_user表,字段如下,id,用戶名,昵稱,密碼(id記得設置為自增長模式)
![]()
H+前端框架
基本介紹
官網地址:H+ 后臺主題UI框架 - 主頁
![]()
H+是一個非常強大的前端開源框架,開箱即用,不需要過多的配置,里面有非常多組件,具體就不一一介紹了,有興趣的自己去看看
![]()
我們要做的是后臺管理,所以我們直接找到表格,可以看到有很多樣式選擇,我們選擇一個簡單點
這里為了方便快速搭建我選擇基本表格,當時你們可以根據自己喜歡來選擇
![]()
H+框架引入
打開HBuild X編譯器(你用其他的也可以,沒影響)
在左側空白處新建一個項目(快捷鍵ctrl+n),選擇基于HTML普通項目,添加項目名稱,選好路徑,點擊創建
![]()
一個基本的前端項目就創建好了
![]()
接著,將H+框架的css,js,font等靜態資源全部復制項目下
![]()
編寫后臺表格頁面
找到H+框架中基本表格的源碼,復制代碼到index.htm下
再對其進行一點修改,去掉右上角的工具欄,加上增刪改查的按鈕
![]()
修改之后的頁面如下,用到了H+框中的表格、表單、按鈕、字體圖標庫,就不詳細介紹了,有興趣可以自己看看前端代碼,我們著重講解js和后端的搭建
![]()
同時,我們還需要一個彈出框,當點擊添加和修改的時候會彈出一個表單框
我們在H+前端框架的表單中找到彈出框示例,復制代碼做點修改
![]()
修改后如下:
![]()
![]()
modal彈出框原理
HTML頁面代碼比較多,就不放上來了,底部Gitee倉庫完整的項目,主要講解一下上面這個modal彈出框怎么實現的就行
首先,每個modal彈出框都有唯一的標識ID屬性,這里我們有兩個,一個添加用戶,一個修改用戶(里面的表單代碼我沒放出來,比較多,文章底部Gitee倉庫我上傳了完整開源項目)
<!-- 添加用戶的彈出框 --> <div id="modal-form-add" class="modal fade" aria-hidden="true"> </div>
<!-- 修改用戶的彈出框 --> <div id="modal-form-update" class="modal fade" aria-hidden="true"> </div>我們為每個修改和刪除按鈕都添加一個類名標識(這里為什么不用ID,是因為ID只能唯一標識,添加按鈕可以用ID,但是修改和刪除不能用ID只能用class,因為有多個修改和刪除按鈕,添加按鈕只有一個)
![]()
我們在js文件夾下面新建一個myJS文件夾,存放自己編寫的js代碼,新建一個index.js,添加以下代碼
// 監聽添加按鈕事件(通過id屬性監聽) $('#addUserBtn').click(function() { // 添加按鈕被點擊之后,展示modal框 $('#modal-form-add').modal('show'); }) // 監聽修改按鈕事件(通過class屬性監聽) $('.updateUserBtn').click(function() { // 修改按鈕被點擊之后,展示modal框 $('#modal-form-update').modal('show'); })然后再index.html底部引入index.js即可
![]()
就可以實現點擊添加和修改按鈕會彈出modal表單框,簡單前端管理頁面就搭建完畢了,接下來是后端
搭建后端
基本介紹
SpringBoot是目前非常主流的后端框架,簡化新spring應用的初始搭建以及開發過程,搭建快,省去了編寫大量配置文件的過程
創建SpringBoot項目
打開IDEA,選擇spring Initializr,點擊下一步
![]()
添加主包名稱,java version版本選擇8,點擊下一步
![]()
在左側找到這五個依賴,勾選上,點擊下一步
![]()
添加項目名稱(默認為之前填寫的主包名稱),項目路徑自己選擇,點擊完成
![]()
右下角處選擇自動導入pom.xml依賴(沒有的話直接跳過)
![]()
一個基本的SpringBoot項目就創建完畢了
![]()
基本配置
在搭建項目之前,先做一些基本的配置,如數據源(連接MySQL)、static靜態資源映射(避免被攔截)、設置端口號(避免沖突)等
刪除原有的application.properties文件,新建一個application.yml文件(兩種都可以,yml文件方便一點),添加以下配置信息
![]()
spring: # 數據源,連接MySQL數據庫 datasource: url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true username: 數據庫用戶名 password: 數據庫密碼 driver-class-name: com.mysql.cj.jdbc.Driver # JPA配置,打印sql語句 jpa: show-sql: true properties: hibernate: format_sql: true # mvc配置,映射html頁面 mvc: static-path-pattern: /** view: prefix: / suffix: .html # thymeleaf模板引擎配置,設置編碼,false取消緩存 thymeleaf: encoding: UTF-8 cache: false server: # 修改啟動端口號 port: 8081 # 靜態資源映射路徑 web: resources: static-locations: classpath:/static/在項目目錄下新建一個config包(存在基本配置類),新建一個MyWebMVCConfig類,代碼如下
@Configuration public class MyWebMVCConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { System.out.println("==========靜態資源攔截!============"); //將所有/static/** 訪問都映射到classpath:/static/ 目錄下 registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); } }
引入前端
把剛剛在HBuildX寫好的前端頁面引入到IDEA中,首先復制除了html的其他所有文件到static文件夾下
![]()
接著復制index.html到templates文件夾下
![]()
然后打開index.html,在所有訪問靜態資源的鏈接前面加上/static/
![]()
測試項目(測試一下瀏覽器是否能顯示頁面)
想項目目錄下新建一個controller包,新建一個IndexCtroller類,添加以下代碼
![]()
@RestController public class IndexController { @RequestMapping(value = "/index") // 訪問路徑 public ModelAndView toIndex() { // 返回templates目錄下index.html ModelAndView view = new ModelAndView("index"); return view; } }點擊啟動項目,選哪個都可以,我一般選第二個debug模式啟動,方式debug調試
![]()
等待啟動完成之后,打開瀏覽器,輸入 localhost:8081/index,頁面成功顯示
![]()
第一篇文章到這里介紹完畢,第二篇正在火速撰寫中······
Gitee開源項目地址(本次項目源碼)
SpringBoot項目教學合集: CSDN中的所有SpringBoot項目開源,持續更新新項目、新教學文章
各大技術基礎教學、實戰項目開發教學
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
目錄
把函數當作一個參數傳到另外一個函數中,當需要用這個函數是,再回調運行()這個函數.
回調函數是一段可執行的代碼段,它作為一個參數傳遞給其他的代碼,其作用是在需要的時候方便調用這段(回調函數)代碼。(作為參數傳遞到另外一個函數中,這個作為參數的函數就是回調函數)
理解:函數可以作為一個參數傳遞到另外一個函數中。
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
callback(sum);
-
}
-
-
function print(num) {
-
console.log(num);
-
}
-
-
add(1, 2, print); //3
-
</script>
分析:add(1, 2, print);中,函數print作為一個參數傳入到add函數中,但并不是馬上起作用,而是var sum = num1 + num2;運行完之后需要打印輸出sum的時候才會調用這個函數。(這個作為參數傳遞到另外一個函數中,這個作為參數的函數就是回調函數.
匿名回調函數:
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
callback(sum);
-
}
-
-
add(1, 2, function (sum) {
-
console.log(sum); //=>3
-
});
-
</script>
1.不會立即執行
回調函數作為參數傳遞給一個函數的時候,傳遞的只是函數的定義并不會立即執行。和普通的函數一樣,回調函數在調用函數數中也要通過()
運算符調用才會執行。
2.回調函數是一個閉包
回調函數是一個閉包,也就是說它能訪問到其外層定義的變量。
3.執行前類型判斷
在執行回調函數前最好確認其是一個函數。
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
//判定callback接收到的數據是一個函數
-
if (typeof callback === 'function') {
-
//callback是一個函數,才能當回調函數使用
-
callback(sum);
-
}
-
}
-
</script>
注意在回調函數調用時this的執行上下文并不是回調函數定義時的那個上下文,而是調用它的函數所在的上下文。
舉例:
-
<script>
-
function createData(callback){
-
callback();
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
createData(function(n){
-
console.log(this,1111); //window 1111
-
})
-
}
-
}
-
obj.tool();
-
</script>
分析:this指向是 離它最近的或者嵌套級別的 function/方法的調用者,這里離它最近的function是
function(n),會回到上面的callback()中,這時候調用者就不是obj而是window。
解決回調函數this指向的方法1:箭頭函數
回調函數(若回調函數是普通函數時)當參數傳入另外的函數時,若不知道這個函數內部怎么調用回調函數,就會出現回調函數中的this指向不明確的問題(就比如上面例子中this指向的不是obj而是window)。所以 把箭頭函數當回調函數,然后作為參數傳入另外的函數中就不會出現this指向不明的問題。
-
<script>
-
function createData(callback){
-
callback();
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
createData((n)=>{
-
this.data = n;
-
})
-
}
-
}
-
obj.tool();
-
console.log(obj.data);
-
</script>
分析:回調函數用箭頭函數寫之后,this指向很明確,就是 離它最近的或者嵌套級別的 function/方法的調用者,所以這里是 obj 。
解決回調函數this指向的方法2:var self = this;
-
<script>
-
function createData(callback){
-
callback(999);
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
var self = this; //這里的this指向obj,然后當一個變量取用
-
createData(function(n){
-
self.data = n;
-
})
-
}
-
}
-
obj.tool();
-
console.log(obj.data);
-
</script>
有一個非常重要的原因 —— JavaScript 是事件驅動的語言。這意味著,JavaScript 不會因為要等待一個響應而停止當前運行,而是在監聽其他事件時繼續執行。來看一個基本的例子:
-
<script>
-
function first() {
-
console.log(1);
-
}
-
-
function second() {
-
console.log(2);
-
}
-
-
first();
-
second();
-
</script>
分析:正如你所料,first
函數首先被執行,隨后 second
被執行 —— 控制臺輸出:1 2
但如果函數 first
包含某種不能立即執行的代碼會如何呢?例如我們必須發送請求然后等待響應的 API 請求?為了模擬這種狀況,我們將使用 setTimeout
,它是一個在一段時間之后調用函數的 JavaScript 函數。我們將函數延遲 500 毫秒來模擬一個 API 請求,新代碼長這樣:
-
<script>
-
function first() {
-
// 模擬代碼延遲
-
setTimeout(function () { //所以function(){console.log(1)}是回調函數
-
console.log(1);
-
}, 500);
-
}
-
-
function second() {
-
console.log(2);
-
}
-
-
first();
-
second();
-
</script>
分析:這里 function(){console.log(1)}函數當作一個參數傳入setTimeout函數中,因為setTimeout是官方提供得一個函數,里面有很多復雜的業務程序,所以函數 function(){console.log(1)}傳入后,不一定馬上運行,要setTimeout里面要運行到function(){console.log(1)}時才會運行該函數參數,那是不是整個程序就一直等setTimeout運行?不是的?。?!
整個程序運行結果為: 2 1 ,并不是原先的1 2 .即使我們首先調用了 first()
函數,我們記錄的輸出結果卻在 second()
函數之后。
這不是 JavaScript 沒有按照我們想要的順序執行函數的問題,而是 JavaScript 在繼續向下執行 second()
之前沒有等待 first()
響應的問題。回調正是確保一段代碼執行完畢之后再執行另一段代碼的方式。
定義:回調函數被認為是一種高級函數,一種被作為參數傳遞給另一個函數的高級函數。回調函數的本質是一種模式(一種解決常見問題的模式),因此回調函數也被稱為回調模式。
簡而言之:一個函數在另一個函數中被調用。而且可以當參數傳給其他函數。
所以: 回調函數和異步操作的關系是沒有關系?。?!
那為什么很多的異步操作都有回填函數啊??
問:你所知道的異步操作,是回調的作用么??? 并不是。
回調:更多的可以理解為一種業務邏輯把 異步編程:JS代碼的執行順序
簡單理解:callback 顧名思義 打電話回來的意思
eg1:你點外賣,剛好你要吃的食物沒有了,于是你在店老板那里留下了你的電話,過了幾天店里有了,店員就打了你的電話,然后你接到電話后就跑到店里買了。在這個例子里,你的電話號碼就叫回調函數,你把電話留給店員就叫登記回調函數,店里后來有貨了叫做觸發了回調關聯的事件,店員給你打電話叫做調用回調函數,你到店里去取貨叫做響應回調事件。
eg2:再比如,你發送一個axios 請求,請求成功之后,觸發成功的回調函數,請求失敗觸發失敗的回調函數。這里面的回調函數更像是一個工具,后臺通過這個工具告訴你,你成功了抑或是失敗了。這里面的所有異步操作都和回調沒關系,真正的異步是then方法。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
概述
:基于WebGL
的三維引擎,目前是國內資料最多、使用最廣泛的三維引擎
,可以制作一些3D
可視化項目
目前隨著元宇宙
概念的爆火,THREE
技術已經深入到了物聯網、VR、游戲、數據可視化等多個平臺,今天我們主要基于THREE
實現一個三維的VR
看房小項目
Three.js
一般分為三個部分:場景、相機、渲染器,這三個主要的分支就構成了THREE.JS
的主要功能區,這三大部分還有許多細小的分支,這些留到我們后續抽出一些章節專門講解一下。
工作流程
:場景——相機——渲染器
從實際生活
中拍照角度立方體網格模型和光照組成了一個虛擬的三維場景
,相機對象就像你生活中使用的相機一樣可以拍照,只不過一個是拍攝真實的景物
,一個是拍攝虛擬的景物。拍攝一個物體的時候相機的位置和角度需要設置,虛擬的相機還需要設置投影方式
,當你創建好一個三維場景,相機也設置好,就差一個動作“咔”,通過渲染器
就可以執行拍照動作。
概述
:場景主要由網絡模型與光照組成,網絡模型分為幾何體與材質
幾何體就像我們小時候學我們就知道點線面體四種概念,點動成線,線動成面,面動成體
,而材質就像是是幾何體上面的涂鴉,有不同的顏色、圖案…
例子如下:
//打造酷炫三角形 for (let i = 0; i < 50; i++) { const geometry = new THREE.BufferGeometry(); const arr = new Float32Array(9); for (let j = 0; j < 9; j++) { arr[j] = Math.random() * 5; } geometry.setAttribute('position', new THREE.BufferAttribute(arr, 3)); let randomColor = new THREE.Color(Math.random(), Math.random(), Math.random()); const material = new THREE.MeshBasicMaterial({ color: randomColor, transparent: true, opacity:0.5, }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); }
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UlBSgxKr-1666681292595)(https://gitee.com/riskbaby/picgo/raw/master/blog/202209211037215.png#pic_center)]
const geometry = new THREE.BoxGeometry(100, 100, 100); const material = new THREE.MeshStandardMaterial({ color: 0x0000ff }); const cube = new THREE.Mesh(geometry, material); scene.add(cube);
const geometry = new THREE.ConeGeometry(5, 15, 32);//底面半徑 高 側邊三角分段 const material = new THREE.MeshStandardMaterial({ color: 0x0000ff }); const clone = new THREE.Mesh(geometry, material); scene.add(clone);
概念
:光照對three.js
的物體全表面進行光照測試,有可能會發生光照融合
//環境光 const ambient = new THREE.AmbientLight(0x404040); scene.add(ambient);
概念
:向特定方向發射的光,太陽光
也視作平行的一種,和上面比較,物體變亮了
//平行光 顏色 強度 const directionalLight = new THREE.DirectionalLight(0xffffff, 1); directionalLight.position.set(100, 100, 100);//光源位置 directionalLight.target = cube;//光源目標 默認 0 0 0 scene.add(directionalLight);
概念
:由中間向四周發射光、強度比平行光小
// 顏色 強度 距離 衰退量(默認1) const pointLight = new THREE.PointLight(0xff0000, 1, 100, 1); pointLight.position.set(50, 50, 50); scene.add(pointLight);
概念
:家里面的節能燈泡,強度較好
//聚光燈 const spotLigth = new THREE.PointLight(0xffffff); spotLigth.position.set(50, 50, 50); spotLigth.target = cube; spotLigth.angle = Math.PI / 6; scene.add(spotLigth);
概念
:光源直接放置于場景之上,光照顏色從天空光線顏色漸變到地面光線顏色
//半球光 const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);//天空 場景 scene.add(light);
參數(屬性) | 含義 |
---|---|
left | 渲染空間的左邊界 |
right | 渲染空間的右邊界 |
top | 渲染空間的上邊界 |
bottom | 渲染空間的下邊界 |
near | near屬性表示的是從距離相機多遠的位置開始渲染,一般情況會設置一個很小的值。 默認值0.1 |
far | far屬性表示的是距離相機多遠的位置截止渲染,如果設置的值偏小小,會有部分場景看不到。 默認值1000 |
let width = window.innerWidth; let height = window.innerHeight; const camera = new THREE.OrthographicCamera(width / - 2, width / 2, height / 2, height / - 2, 1, 1000); scene.add(camera); camera.position.set(100, 200, 100);
參數 | 含義 | 默認值 |
---|---|---|
fov | fov表示視場,所謂視場就是能夠看到的角度范圍,人的眼睛大約能夠看到180度的視場,視角大小設置要根據具體應用,一般游戲會設置60~90度 | 45 |
aspect | aspect表示渲染窗口的長寬比,如果一個網頁上只有一個全屏的canvas畫布且畫布上只有一個窗口,那么aspect的值就是網頁窗口客戶區的寬高比 | window.innerWidth/window.innerHeight |
near | near屬性表示的是從距離相機多遠的位置開始渲染,一般情況會設置一個很小的值。 | 0.1 |
far | far屬性表示的是距離相機多遠的位置截止渲染,如果設置的值偏小,會有部分場景看不到 | 1000 |
let width = window.innerWidth; let height = window.innerHeight; const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000); camera.position.set(150, 100, 300); camera.lookAt(scene.position);
概述
:從WEBGL
的角度來看,three
就是對它的進一步封裝,想要進一步了解渲染器
這方面的知識點還需要了解一下WEBGL
,這里我們就不做過多介紹了。
概述
:這部分對于我們是否能夠給別人呈現一個真實的渲染場景
來說,很重要,比如下面一個普普通通的正方體,我們只要一加上貼圖,立馬不一樣了。
以前
之后
概述
:目前有許許多多的貼圖
,比如基礎、透明、環境、法線、金屬、粗糙、置換等等,今天我們呢主要講解一下環境
和一點 HDR處理
在THREE
的世界里面,坐標抽x、y、z
的位置關系圖如下所示:
紅、綠、藍
分別代表x、z、y
,我們的貼圖就是在px nx py ny pz nz
這六個方向防止一張圖片,其中p就代表坐標軸的正方向
CubeTextureLoader
:加載CubeTexture
的一個類。 內部使用ImageLoader
來加載文件。
//場景貼圖 const sphereTexture = new THREE.CubeTextureLoader().setPath('./textures/course/environmentMaps/0/'); const envTexture= sphereTexture.load([ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ]); //場景添加背景 scene.background = envTexture; //場景的物體添加環境貼圖(無默認情況使用) scene.environment = envTexture; const sphereGeometry = new THREE.SphereGeometry(5, 30, 30); const sphereMaterial = new THREE.MeshStandardMaterial({ roughness: 0,//設置粗糙程度 metalness: 1,//金屬度 envMap:envTexture, }); const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(sphere);
gif
圖片有點大上傳不了,我就截了幾張圖
概述
:高動態范圍圖像,相比普通的圖像,能夠提供更多的動態范圍和圖像細節,一般被運用于電視顯示產品以及圖片視頻拍攝制作當中。
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader; const rgbeLoader = new RGBELoader().setPath('./textures/course/hdr/'); //異步加載 rgbeLoader.loadAsync('002.hdr').then((texture) => { //設置加載方式 等距圓柱投影的環境貼圖 texture.mapping = THREE.EquirectangularReflectionMapping; scene.background = texture; })
概述
:坐標軸能夠更好的反饋物體的位置信息,紅、綠、藍
分別代表x、z、y
const axesHelper = new THREE.AxesHelper(20);//里面的數字代表坐標抽長度 scene.add(axesHelper);
概述
:通過鼠標控制物體和相機的移動、旋轉、縮放
導包
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
應用
const controls = new OrbitControls(camera, renderer.domElement)
自旋轉
controls.autoRotate = true
必須在render
函數調用update
實時更新才奏效
概述
:根據屏幕大小自適應場景
//自適應屏幕 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) renderer.setPixelRatio(window.devicePixelRatio) })
設置相機的寬高比、重新更新渲染相機、渲染器的渲染大小、設備的像素比
概述
:雙擊進入全屏
,再次雙擊/ESC退出全屏
window.addEventListener('dblclick', () => { let isFullScreen = document.fullscreenElement if (!isFullScreen) { renderer.domElement.requestFullscreen() } else { document.exitFullscreen() } })
概述
;通過操作面板完成界面的移動物體
的相關應用
鏈接
:https://www.npmjs.com/package/dat.gui
//安裝npm npm install --save dat.gui //如果出現...標記錯誤,安裝到開發依賴就可以了 npm i --save-dev @types/dat.gui
//界面操作 const gui = new dat.GUI(); //操作物體位置 gui .add(cube.position, 'x') .min(0) .max(10) .step(0.1) .name('X軸移動') .onChange((value) => { console.log('修改的值為' + value); }) .onFinishChange((value) => { console.log('完全停止' + value); }); //操作物體顏色 const colors = { color: '#0000ff', }; gui .addColor(colors, 'color') .onChange((value) => { //修改物體顏色 cube.material.color.set(value); });
概述
:檢測幀率
導包
import Stats from 'three/addons/libs/stats.module.js';
應用
const stats = new Stats(); document.body.appendChild(stats.dom);
自變化
stats.update()
必須在render
函數調用update
實時更新才奏效
概述
:底部二維平面的網格化,幫助我們更好的創建場景
const gridHelper = new THREE.GridHelper(10, 20)//網格大小、細分次數 scene.add(gridHelper)
//導入包 import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import * as dat from 'dat.gui'; import Stats from 'three/addons/libs/stats.module.js'; let scene,camera,renderer //場景 scene = new THREE.Scene(); //坐標抽 const axesHelper = new THREE.AxesHelper(20); scene.add(axesHelper); //場景貼圖 const sphereTexture = new THREE.CubeTextureLoader().setPath('./textures/course/environmentMaps/0/'); const envTexture= sphereTexture.load([ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ]); //場景添加背景 scene.background = envTexture; //場景的物體添加環境貼圖(無默認情況使用) scene.environment = envTexture; const sphereGeometry = new THREE.SphereGeometry(5, 30, 30); const sphereMaterial = new THREE.MeshStandardMaterial({ roughness: 0,//設置粗糙程度 metalness: 1,//金屬度 envMap:envTexture, }); const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(sphere); //光照 const ambient = new THREE.AmbientLight(0xffffff); scene.add(ambient); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.05); directionalLight.position.set(10,10,10); directionalLight.lookAt(scene.position); scene.add( directionalLight ); //相機 camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 2000, ); camera.position.set(10,10,20); camera.lookAt(scene.position); scene.add(camera); //渲染器 renderer = new THREE.WebGLRenderer({ //防止鋸齒 antialias: true, }); renderer.setSize(window.innerWidth, window.innerHeight); // renderer.setClearColor(0xb9d3ff, 1); document.body.appendChild(renderer.domElement); //鼠標控制器 const controls = new OrbitControls(camera, renderer.domElement); //阻尼 必須在 render函數調用 controls.update(); controls.dampingFactor = true; controls.autoRotate=true const stats=new Stats() document.body.appendChild(stats.dom); function render () { renderer.render(scene, camera); requestAnimationFrame(render); controls.update();//調用 stats.update() } render(); //全屏操作 window.addEventListener('dblclick', () => { //查詢是否全屏 let isFullScene = document.fullscreenElement; console.log(isFullScene); if (!isFullScene) { renderer.domElement.requestFullscreen(); } else { document.exitFullscreen(); } }) //自適應 window.addEventListener('resize', () => { //寬高比 camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio);//設置像素比 }) //界面操作 const gui = new dat.GUI(); //操作物體位置 gui .add(sphere.position, 'x') .min(0) .max(10) .step(0.1) .name('X軸移動') .onChange((value) => { console.log('修改的值為' + value); }) .onFinishChange((value) => { console.log('完全停止' + value); }); //操作物體顏色 const colors = { color: '#0000ff', }; gui .addColor(colors, 'color') .onChange((value) => { //修改物體顏色 sphere.material.color.set(value); });
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
目錄
相同點:
- 都是基于tcp的,都是可靠性傳輸協議
- 都是應用層協議
不同點:
- WebSocket是雙向通信協議,模擬Socket協議,可以雙向發送或接受信息
- HTTP是單向的
- WebSocket是需要瀏覽器和服務器握手進行建立連接的
- 而http是瀏覽器發起向服務器的連接,服務器預先并不知道這個連接
聯系:
- WebSocket在建立握手時,數據是通過HTTP傳輸的。但是建立之后,在真正傳輸時候是不需要HTTP協議的
總結(總體過程):
- 首先,客戶端發起http請求,經過3次握手后,建立起TCP連接;http請求里存放WebSocket支持的版本號等信息,如:Upgrade、Connection、WebSocket-Version等;
- 然后,服務器收到客戶端的握手請求后,同樣采用HTTP協議回饋數據;
- 最后,客戶端收到連接成功的消息后,開始借助于TCP傳輸信道進行全雙工通信。
從例子上來看有個問題:
- 假如有好多人一起在快遞站等快遞,那么這個地方是否足夠大,(抽象解釋:需要有很高的并發,同時有很多請求等待在這里)
推送延遲。服務端數據發生變更后,長輪詢結束,立刻返回響應給客戶端。
服務端壓力。長輪詢的間隔期一般很長,例如 30s、60s,并且服務端 hold 住連接不會消耗太多服務端資源。
從例子上來看有兩個問題:
- 假如說,張三打電話的時間間隔為10分鐘,當他收到快遞前最后一次打電話,快遞員說沒到,他剛掛掉電話,快遞入庫了(就是到了),那么等下一次時間到了,張三打電話知道快遞到了,那么這樣的通訊算不算實時通訊?很顯然,不算,中間有十分鐘的時間差,還不算給快遞員打電話的等待時間(抽象的解釋:每次request的請求時間間隔等同于十分鐘,請求解析相當于等待)
- 假如說張三所在的小區每天要收很多快遞,每個人都采取主動給快遞員打電話的方式,那么快遞員需要以多快的速度接到,其他人打電話占線也是問題(抽象解釋:請求過多,服務端響應也會變慢)
推送延遲。
服務端壓力。配置一般不會發生變化,頻繁的輪詢會給服務端造成很大的壓力。
推送延遲和服務端壓力無法中和。降低輪詢的間隔,延遲降低,壓力增加;增加輪詢的間隔,壓力降低,延遲增高
一旦WebSocket連接建立后,后續數據都以幀序列的形式傳輸。在客戶端斷開WebSocket連接或Server端中斷連接前,不需要客戶端和服務端重新發起連接請求。在海量并發及客戶端與服務器交互負載流量大的情況下,極大的節省了網絡帶寬資源的消耗,有明顯的性能優勢,且客戶端發送和接受消息是在同一個持久連接上發起,實現了“真·長鏈接”,實時性優勢明顯。
WebSocket有以下特點:
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
目前主流的 Web 開發模式有兩種,分別是:
服務端渲染的概念:服務器發送給客戶端的 HTML 頁面,是在服務器通過字符串的拼接,動態生成的。因此,客戶端不需要使用 Ajax 這樣的技術額外請求頁面的數據。代碼示例如下:
優點:
缺點:
前后端分離的概念:前后端分離的開發模式,依賴于 Ajax 技術的廣泛應用。簡而言之,前后端分離的 Web 開發模式,就是后端只負責提供 API 接口,前端使用 Ajax 調用接口的開發模式。
優點:
缺點:
不利于 SEO。因為完整的 HTML 頁面需要在客戶端動態拼接完成,所以爬蟲對無法爬取頁面的有效信息。(解決方案:利用 Vue、React 等前端框架的 SSR (server side render)技術能夠很好的解決 SEO 問題!)
不談業務場景而盲目選擇使用何種開發模式都是耍流氓。
另外,具體使用何種開發模式并不是絕對的,為了同時兼顧了首頁的渲染速度和前后端分離的開發效率,一些網站采用了首屏服務器端渲染 + 其他頁面前后端分離的開發模式。
身份認證(Authentication)又稱“身份驗證”、“鑒權”,是指通過一定的手段,完成對用戶身份的確認。
身份認證的目的,是為了確認當前所聲稱為某種身份的用戶,確實是所聲稱的用戶。例如,你去找快遞員取快遞,你要怎么證明這份快遞是你的。
在互聯網項目開發中,如何對用戶的身份進行認證,是一個值得深入探討的問題。例如,如何才能保證網站不會錯誤的將“馬云的存款數額”顯示到“馬化騰的賬戶”上。
對于服務端渲染和前后端分離這兩種開發模式來說,分別有著不同的身份認證方案:
對于超市來說,為了方便收銀員在進行結算時給 VIP 用戶打折,超市可以為每個 VIP 用戶發放會員卡。
注意:現實生活中的會員卡身份認證方式,在 Web 開發中的專業術語叫做 Cookie
Cookie的幾大特性:
客戶端第一次請求服務器的時候,服務器通過響應頭的形式,向客戶端發送一個身份認證的 Cookie,客戶端會自動將 Cookie 保存在瀏覽器中。
隨后,當客戶端瀏覽器每次請求服務器的時候,瀏覽器會自動將身份認證相關的 Cookie,通過請求頭的形式發送給服務器,服務器即可驗明客戶端的身份。
由于 Cookie 是存儲在瀏覽器中的,而且瀏覽器也提供了讀寫 Cookie 的 API,因此 Cookie 很容易被偽造,不具有安全性。因此不建議服務器將重要的隱私數據,通過 Cookie 的形式發送給瀏覽器。
注意:千萬不要使用 Cookie 存儲重要且隱私的數據!比如用戶的身份信息、密碼等。
3.6、提高身份認證的安全性
為了防止客戶偽造會員卡,收銀員在拿到客戶出示的會員卡之后,可以在收銀機上進行刷卡認證。只有收銀機確認存在的會員卡,才能被正常使用。
這種“會員卡 + 刷卡認證”的設計理念,就是 Session 認證機制的精髓。
在 Express 項目中,只需要安裝 express-session 中間件,即可在項目中使用 Session 認證:
npm install express-session
express-session 中間件安裝成功后,需要通過 app.use() 來注冊 session 中間件,示例代碼如下:
-
// 導入 session 中間件
-
const session = require('express-session')
-
-
// 配置 session 中間件
-
app.use(
-
session({
-
secret: 'itheima', // secret 屬性的值可以為任意字符串
-
resave: false, // 固定寫法
-
saveUninitialized: true, // 固定寫法
-
})
-
)
當 express-session 中間件配置成功后,即可通過 req.session 來訪問和使用 session 對象,從而存儲用戶的關鍵信息:
-
// 登錄的 API 接口
-
app.post('/api/login', (req, res) => {
-
// 判斷用戶提交的登錄信息是否正確
-
if (req.body.username !== 'admin' || req.body.password !== '000000') {
-
return res.send({ status: 1, msg: '登錄失敗' })
-
}
-
-
// TODO_02:請將登錄成功后的用戶信息,保存到 Session 中
-
// 注意:只有成功配置了 express-session 這個中間件之后,才能夠通過 req 點出來 session 這個屬性
-
req.session.user = req.body // 用戶的信息
-
console.log(req.body)
-
req.session.islogin = true // 用戶的登錄狀態
-
-
res.send({ status: 0, msg: '登錄成功' })
-
})
可以直接從 req.session 對象上獲取之前存儲的數據,示例代碼如下:
-
// 獲取用戶姓名的接口
-
app.get('/api/username', (req, res) => {
-
// TODO_03:請從 Session 中獲取用戶的名稱,響應給客戶端
-
if (!req.session.islogin) {
-
return res.send({ status: 1, msg: 'fail' })
-
}
-
res.send({
-
status: 0,
-
msg: 'success',
-
username: req.session.user.username,
-
})
-
})
調用 req.session.destroy() 函數,即可清空服務器保存的 session 信息。
-
// 退出登錄的接口
-
app.post('/api/logout', (req, res) => {
-
// TODO_04:清空 Session 信息
-
req.session.destroy()
-
res.send({
-
status: 0,
-
msg: '退出登錄成功',
-
})
-
})
index.html
-
<!DOCTYPE html>
-
<html lang="en">
-
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>后臺主頁</title>
-
<script src="./jquery.js"></script>
-
</head>
-
-
<body>
-
<h1>首頁</h1>
-
-
<button id="btnLogout">退出登錄</button>
-
-
<script>
-
$(function () {
-
-
// 頁面加載完成后,自動發起請求,獲取用戶姓名
-
$.get('/api/username', function (res) {
-
// status 為 0 表示獲取用戶名稱成功;否則表示獲取用戶名稱失??!
-
if (res.status !== 0) {
-
alert('您尚未登錄,請登錄后再執行此操作!')
-
location.href = './login.html'
-
} else {
-
alert('歡迎您:' + res.username)
-
}
-
})
-
-
// 點擊按鈕退出登錄
-
$('#btnLogout').on('click', function () {
-
// 發起 POST 請求,退出登錄
-
$.post('/api/logout', function (res) {
-
if (res.status === 0) {
-
// 如果 status 為 0,則表示退出成功,重新跳轉到登錄頁面
-
location.href = './login.html'
-
}
-
})
-
})
-
})
-
</script>
-
</body>
-
-
</html>
login.html
-
<!DOCTYPE html>
-
<html lang="en">
-
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>登錄頁面</title>
-
<script src="./jquery.js"></script>
-
</head>
-
-
<body>
-
<!-- 登錄表單 -->
-
<form id="form1">
-
<div>賬號:<input type="text" name="username" autocomplete="off" /></div>
-
<div>密碼:<input type="password" name="password" /></div>
-
<button>登錄</button>
-
</form>
-
-
<script>
-
$(function () {
-
// 監聽表單的提交事件
-
$('#form1').on('submit', function (e) {
-
// 阻止默認提交行為
-
e.preventDefault()
-
// 發起 POST 登錄請求
-
$.post('/api/login', $(this).serialize(), function (res) {
-
// status 為 0 表示登錄成功;否則表示登錄失?。?/span>
-
if (res.status === 0) {
-
location.href = './index.html'
-
} else {
-
alert('登錄失??!')
-
}
-
})
-
})
-
})
-
</script>
-
</body>
-
-
</html>
app.js
-
// 導入 express 模塊
-
const express = require('express')
-
// 創建 express 的服務器實例
-
const app = express()
-
-
// TODO_01:請配置 Session 中間件
-
const session = require('express-session')
-
app.use(
-
session({
-
secret: 'itheima',
-
resave: false,
-
saveUninitialized: true,
-
})
-
)
-
-
// 托管靜態頁面
-
app.use(express.static('./pages'))
-
// 解析 POST 提交過來的表單數據
-
app.use(express.urlencoded({ extended: false }))
-
-
// 登錄的 API 接口
-
app.post('/api/login', (req, res) => {
-
// 判斷用戶提交的登錄信息是否正確
-
if (req.body.username !== 'admin' || req.body.password !== '000000') {
-
return res.send({ status: 1, msg: '登錄失敗' })
-
}
-
-
// TODO_02:請將登錄成功后的用戶信息,保存到 Session 中
-
// 注意:只有成功配置了 express-session 這個中間件之后,才能夠通過 req 點出來 session 這個屬性
-
req.session.user = req.body // 用戶的信息
-
console.log(req.body)
-
req.session.islogin = true // 用戶的登錄狀態
-
-
res.send({ status: 0, msg: '登錄成功' })
-
})
-
-
// 獲取用戶姓名的接口
-
app.get('/api/username', (req, res) => {
-
// TODO_03:請從 Session 中獲取用戶的名稱,響應給客戶端
-
if (!req.session.islogin) {
-
return res.send({ status: 1, msg: 'fail' })
-
}
-
res.send({
-
status: 0,
-
msg: 'success',
-
username: req.session.user.username,
-
})
-
})
-
-
// 退出登錄的接口
-
app.post('/api/logout', (req, res) => {
-
// TODO_04:清空 Session 信息
-
req.session.destroy()
-
res.send({
-
status: 0,
-
msg: '退出登錄成功',
-
})
-
})
-
-
// 調用 app.listen 方法,指定端口號并啟動web服務器
-
app.listen(80, function () {
-
console.log('Express server running at http://127.0.0.1:80')
-
})
Session 認證機制需要配合 Cookie 才能實現。由于 Cookie 默認不支持跨域訪問,所以,當涉及到前端跨域請求后端接口的時候,需要做很多額外的配置,才能實現跨域 Session 認證。
注意:
JWT(英文全稱:JSON Web Token)是目前最流行的跨域認證解決方案。
總結:用戶的信息通過 Token 字符串的形式,保存在客戶端瀏覽器中。服務器通過還原 Token 字符串的形式來認證用戶的身份。
JWT 通常由三部分組成,分別是 Header(頭部)、Payload(有效荷載)、Signature(簽名)。
三者之間使用英文的“.”分隔,格式如下:
下面是 JWT 字符串的示例:
JWT 的三個組成部分,從前到后分別是 Header、Payload、Signature。
其中:
客戶端收到服務器返回的 JWT 之后,通常會將它儲存在 localStorage 或 sessionStorage 中。
此后,客戶端每次與服務器通信,都要帶上這個 JWT 的字符串,從而進行身份認證。推薦的做法是把 JWT 放在 HTTP 請求頭的 Authorization 字段中,格式如下:
運行如下命令,安裝如下兩個 JWT 相關的包:
npm install jsonwebtoken express-jwt
其中:
使用 require() 函數,分別導入 JWT 相關的兩個包:
-
// 安裝并導入 JWT 相關的兩個包,分別是 jsonwebtoken 和 express-jwt
-
const jwt = require('jsonwebtoken')
-
const expressJWT = require('express-jwt')
為了保證 JWT 字符串的安全性,防止 JWT 字符串在網絡傳輸過程中被別人破解,我們需要專門定義一個用于加密和解密的 secret 密鑰:
-
// 定義 secret 密鑰,建議將密鑰命名為 secretKey,本質上就是一個字符串
-
const secretKey = 'itheima No1 ^_^'
調用 jsonwebtoken 包提供的 sign() 方法,將用戶的信息加密成 JWT 字符串,響應給客戶端:
-
// 登錄接口
-
app.post('/api/login', function (req, res) {
-
// 將 req.body 請求體中的數據,轉存為 userinfo 常量
-
const userinfo = req.body
-
// 登錄失敗
-
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
-
return res.send({
-
status: 400,
-
message: '登錄失??!',
-
})
-
}
-
// 登錄成功
-
// TODO_03:在登錄成功之后,調用 jwt.sign() 方法生成 JWT 字符串。并通過 token 屬性發送給客戶端
-
// 參數1:用戶的信息對象
-
// 參數2:加密的秘鑰
-
// 參數3:配置對象,可以配置當前 token 的有效期
-
// 記?。呵f不要把密碼加密到 token 字符中
-
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
-
res.send({
-
status: 200,
-
message: '登錄成功!',
-
token: tokenStr, // 要發送給客戶端的 token 字符串
-
})
-
})
-
// 使用 app.use() 來注冊中間件
-
// expressJWT({ secret: secretKey }) 就是用來解析 Token 的中間件
-
// .unless({ path: [/^\/api\//] }) 用來指定哪些接口不需要訪問權限
-
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
6.6、使用 req.user 獲取用戶信息
當 express-jwt 這個中間件配置成功之后,即可在那些有權限的接口中,使用 req.user 對象,來訪問從 JWT 字符串中解析出來的用戶信息了,示例代碼如下:
-
// 這是一個有權限的 API 接口
-
app.get('/admin/getinfo', function (req, res) {
-
// TODO_05:使用 req.user 獲取用戶信息,并使用 data 屬性將用戶信息發送給客戶端
-
console.log(req.user)
-
res.send({
-
status: 200,
-
message: '獲取用戶信息成功!',
-
data: req.user, // 要發送給客戶端的用戶信息
-
})
-
})
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
jsoup 是一款基于 Java 的HTML解析器,它提供了一套非常省力的API,不但能直接解析某個URL地址、HTML文本內容,而且還能通過類似于DOM、CSS或者jQuery的方法來操作數據,所以 jsoup 也可以被當做爬蟲工具使用。
Document
:文檔對象。每份HTML頁面都是一個文檔對象,Document 是 jsoup 體系中最頂層的結構。
Element
:元素對象。一個 Document 中可以著包含著多個 Element 對象,可以使用 Element 對象來遍歷節點提取數據或者直接操作HTML。
Elements
:元素對象集合,類似于List<Element>
。
Node
:節點對象。標簽名稱、屬性等都是節點對象,節點對象用來存儲數據。
獲得文檔對象 Document 一共有4種方法,分別對應不同的獲取方式。
正式開始之前,我們需要導入有關 jar 包。
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.15.1</version> </dependency>
–
使用 Jsoup.connect(String url).get()
方法獲?。ㄖ恢С?http 和 https 協議):
Document doc = Jsoup.connect("http://csdn.com/").get(); String title = doc.title(); System.out.println(title);
connect(String url)
方法創建一個新的 Connection并通過.get()
或者.post()
方法獲得數據。如果從該URL獲取HTML時發生錯誤,便會拋出 IOException,應適當處理。
Connection
接口還提供一個方法鏈來解決特殊請求,我們可以在發送請求時帶上請求的頭部參數,具體如下:
Document doc = Jsoup.connect("http://csdn.com") .data("query", "Java") .userAgent("Mozilla") .cookie("auth", "token") .timeout(8000) .post(); System.out.println(doc);
想獲得完整的響應對象和響應碼?我們可以使用execute()
方法:
// 獲得響應對象 Connection.Response response = Jsoup.connect("http://csdn.com").execute(); int code = response.statusCode(); // 輸出狀態碼:200 System.out.println(code);
–
可以使用靜態的Jsoup.parse(File in, String charsetName)
方法從文件中加載文檔。其中in
表示路徑,charsetName
表示編碼方式,示例代碼:
File input = new File("/tmp/input.html"); Document doc = Jsoup.parse(input, "UTF-8"); System.out.println(doc);
–
使用靜態的Jsoup.parse(String html)
方法可以從字符串文本中獲得文檔對象 Document ,示例代碼:
String html = "<html><head><title>First parse</title></head>" + "<body><p>Parsed HTML into a doc.</p></body></html>"; Document doc = Jsoup.parse(html); System.out.println(doc);
–
使用Jsoup.parseBodyFragment(String html)
方法.
String html = "<p>Lorem ipsum.</p>"; Document doc = Jsoup.parseBodyFragment(html); // doc 此時為:<body> <p>Lorem ipsum.</p></body> Element body = doc.body(); System.out.println(body);
parseBodyFragment
方法創建一個新的文檔,并插入解析過的HTML到body
元素中。假如你使用正常的 Jsoup.parse(String html)
方法,通常也能得到相同的結果,但是明確將用戶輸入作為 body 片段處理是個更好的方式。
Document.body()
方法能夠取得文檔body元素的所有子元素,與 doc.getElementsByTag("body")
相同。
解析文檔對象并獲取數據一共有 2 種方式,分別為 DOM方式、CSS選擇器方式,我們可以選擇一種自己喜歡的方式去獲取數據,效果一樣。
將HTML解析成一個Document
之后,就可以使用類似于DOM的方法進行操作。
// 獲取csdn首頁所有的鏈接 Document doc = Jsoup.connect("http://csdn.com").get(); Elements elements = doc.getElementsByTag("body"); Elements contents = elements.first().getElementsByTag("a"); for (Element content : contents) { String linkHref = content.attr("href"); String linkText = content.text(); System.out.print(linkText+"\t"); System.out.println(linkHref); }
說明
Elements
這個對象提供了一系列類似于DOM的方法來查找元素,抽取并處理其中的數據。具體如下:
4.1.1)查找元素
getElementById(String id)
:通過id來查找元素
getElementsByTag(String tag)
:通過標簽來查找元素
getElementsByClass(String className)
:通過類選擇器來查找元素
getElementsByAttribute(String key)
:通過屬性名稱來查找元素,例如查找帶有href元素的標簽。
siblingElements()
:獲取兄弟元素。如果元素沒有兄弟元素,則返回一個空列表。
firstElementSibling()
:獲取第一個兄弟元素。
lastElementSibling()
:獲取最后一個兄弟元素。
nextElementSibling()
:獲取下一個兄弟元素。
previousElementSibling()
:獲取上一個兄弟元素。
parent()
:獲取此節點的父節點。
children()
:獲取此節點的所有子節點。
child(int index)
:獲取此節點的指定子節點。
4.1.2)獲取元素數據
attr(String key)
:獲取單個屬性值
attributes()
:獲取所有屬性值
attr(String key, String value)
:設置屬性值
text()
:獲取文本內容
text(String value)
:設置文本內容
html()
:獲取元素內的HTML內容
html(String value)
:設置元素內的HTML內容
outerHtml()
:獲取元素外HTML內容
data()
:獲取數據內容(例如:script和style標簽)
id()
:獲得id值(例:<p id="goods">衣服</p>
)
className()
:獲得第一個類選擇器值
classNames()
:獲得所有的類選擇器值
tag()
:獲取元素標簽
tagName()
:獲取元素標簽名(如:<p>、<div>等)
4.1.3)操作HTML文本
append(String html)
:在末尾追加HTML文本
prepend(String html)
:在開頭追加HTML文本
html(String value)
:在匹配元素內部添加HTML文本。
–
可以使用類似于CSS選擇器的語法來查找和操作元素,常用的方法為select(String selector)
。
Document doc = Jsoup.connect("http://csdn.com").get(); // 獲取帶有 href 屬性的 a 元素 Elements elements = doc.select("a[href]"); for (Element content : elements) { String linkHref = content.attr("href"); String linkText = content.text(); System.out.print(linkText + "\t"); System.out.println(linkHref); }
4.2.1)說明
select()
方法在Document
、Element
或Elements
對象中都可以使用,而且是上下文相關的,因此可實現指定元素的過濾,或者采用鏈式訪問。
select()
方法將返回一個Elements
集合,并提供一組方法來抽取和處理結果。
4.2.2)select(String selector)
方法參數簡介
tagname
: 通過標簽查找元素,例如通過"a"
來查找<a>
標簽。
#id
: 通過ID查找元素,比如通過#logo
查找<p id="logo">
。
.class
: 通過class名稱查找元素,比如通過.titile
查找<p class="titile">
。
ns|tag
: 通過標簽在命名空間查找元素,比如使用 fb|name
來查找 <fb:name>
。
[attribute]
: 利用屬性查找元素,比如通過[href]
查找<a href="...">
。
[^attribute]
: 利用屬性名前綴來查找元素,比如:可以用[^data-]
來查找帶有HTML5 dataset屬性的元素。
[attribute=value]
: 利用屬性值來查找元素,比如:[width=500]
。
[attribute^=value]
, [attribute$=value]
, [attribute*=value]
: 利用匹配屬性值開頭、結尾或包含屬性值來查找元素,比如通過[href*=/path/]
來查找<a href="a/path/c.html">
。
[attribute~=regex]
: 利用屬性值匹配正則表達式來查找元素,比如通過 img[src~=(?i)\.(png|jpe?g)]
來匹配所有的png或者jpg、jpeg格式的圖片。
*
: 通配符,匹配所有元素。
4.2.3)參數屬性組合使用
el#id
: 元素+ID,比如: div#logo
el.class
: 元素+class,比如: div.masthead
el[attr]
: 元素+class,比如 a[href]
匹配所有帶有 href 屬性的 a 元素。
a[href].highlight
匹配所有帶有 href 屬性且class="highlight"的 a 元素。
ancestor child
: 查找某個元素下子元素,比如:可以用.body p
查找在"body"元素下的所有 p
元素
parent > child
: 查找某個父元素下的直接子元素,比如:可以用div.content > p
查找 p
元素,也可以用body > *
查找body標簽下所有直接子元素
siblingA + siblingB
: 查找在A元素之前第一個同級元素B,比如:div.head + div
siblingA ~ siblingX
: 查找A元素之前的同級X元素,比如:h1 ~ p
el, el, el
:多個選擇器組合,查找匹配任一選擇器的唯一元素,例如:div.masthead, div.logo
4.2.4)特殊參數:偽選擇器
:lt(n)
: 查找哪些元素的同級索引值(它的位置在DOM樹中是相對于它的父節點)小于n,比如:td:lt(3)
表示小于三列的元素
:gt(n)
:查找哪些元素的同級索引值大于n``,比如
: div p:gt(2)
表示哪些div中有包含2個以上的p元素
:eq(n)
: 查找哪些元素的同級索引值與n
相等,比如:form input:eq(1)
表示包含一個input標簽的Form元素
:has(seletor)
: 查找匹配選擇器包含元素的元素,比如:div:has(p)
表示哪些div包含了p元素
:not(selector)
: 查找與選擇器不匹配的元素,比如: div:not(.logo)
表示不包含 class=logo 元素的所有 div 列表
:contains(text)
: 查找包含給定文本的元素,搜索不區分大不寫,比如: p:contains(jsoup)
:containsOwn(text)
: 查找直接包含給定文本的元素
:matches(regex)
: 查找哪些元素的文本匹配指定的正則表達式,比如:div:matches((?i)login)
:matchesOwn(regex)
: 查找自身包含文本匹配指定正則表達式的元素
在獲得文檔對象并且指定查找元素后,我們就可以獲取元素中的數據。
這些訪問器方法都有相應的setter方法來更改數據。
.attr(String key)
:獲得屬性的值。
.text()
:獲得元素中的文本。
.html()
:獲得元素或屬性內部的HTML內容(不包括本身)。
.outerHtml()
:獲得元素或屬性完整的HTML內容。
.id()
:獲得元素id屬性值。
className()
:獲得元素類選擇器值。
.tagName()
:獲得元素標簽命名。
.hasClass(String className)
:檢查這個元素是否含有一個類選擇器(不區分大小寫)。
String html = "<p><a ><b>example</b></a> link.</p>"; Document doc = Jsoup.parse(html); // 查找第一個<a>元素 Element link = doc.select("a").first(); // 輸出:example String text = link.text(); // 輸出:http://csdn.com/ String href = link.attr("href"); // 輸出:<b>example</b> String aHtml = link.outerHtml(); // 輸出:<a ><b>example</b></a> String aOuterHtml = link.outerHtml();
–
在解析了一個Document對象之后,你可能想修改其中的某些屬性值,并把它輸出到前臺頁面或保存到其他地方,jsoup對此提供了一套非常簡便的接口(支持鏈式寫法)。
當以下方法針對Element
對象操作時,只有一個元素會受到影響。當針對Elements
對象進行操作時,可能會影響到多個元素。
.attr(String key, String value)
:設置標簽的屬性值。
.addClass(String className)
:增加類選擇器選項
.removeClass(String className)
:刪除對應的類選擇器
Document doc = Jsoup.connect("http://csdn.com").get(); // 復數,Elements Elements elements = doc.getElementsByClass("text"); // 單數,Element Element element = elements.first(); // 復數對象,所有 class="text" 的元素都將受到影響 elements.attr("name","goods"); // 單數對象,只有一個元素會受到影響(鏈式寫法) element.attr("name","shop") .addClass("red");
可以使用Element
中的HTML設置方法具體如下:
.html(String value)
:這個方法將先清除元素中的HTML內容,然后用傳入的HTML代替。
.prepend(String value)
:在元素前添加html內容。
.append(String value)
:在元素后添加html內容。
.wrap(String value)
:對元素包裹一個外部HTML內容,將元素置于新增的內容中間。
Document doc = Jsoup.connect("http://csdn.com").get(); Element div = doc.select("div").first(); div.html("<p>csdn</p>"); div.prepend("<p>a</p>"); div.append("<p>good</p>"); // 輸出:<div"> <p>a</p> <p>csdn</p> <p>good</p> </div> Element span = doc.select("span").first(); span.wrap("<li><a href='...'></a></li>"); // 輸出: <li><a href="..."> <span>csdn</span> </a></li>
對于傳入的文本,如果含有像 <
, >
等這樣的字符,將以文本處理,而非HTML。
.text(String text)
:清除元素內部的HTML內容,然后用提供的文本代替。
.prepend(String first)
:在元素后添加文本節點。
Element.append(String last)
:在元素前添加文本節點。
// <div></div> Element div = doc.select("div").first(); div.text(" one "); div.prepend(" two "); div.append(" three "); // 輸出: <div> two one three </div>
問題描述:
你有一個包含相對URLs路徑的HTML文檔,現在需要將這些相對路徑轉換成絕對路徑的URLs。
解決方式:
base URI
路徑。
abs:
屬性前綴來取得包含base URI
的絕對路徑。代碼如下:
Document doc = Jsoup.connect("http://www.open-open.com").get(); Element link = doc.select("a").first(); // 輸出:/ String relHref = link.attr("href"); // 輸出:http://www.open-open.com/ String absHref = link.attr("abs:href");
說明:
在HTML元素中,URLs經常寫成相對于文檔位置的相對路徑,如:<a href="/download">...</a>
。當你使用 .attr(String key)
方法來取得a元素的href屬性時,它將直接返回在HTML源碼中指定的值。
假如你需要取得一個絕對路徑,需要在屬性名前加 abs:
前綴,這樣就可以返回包含根路徑的URL地址attr("abs:href")
。因此在解析HTML文檔時,定義base URI非常重要。
如果你不想使用abs:
前綴,還有一個方法能夠實現同樣的功能 .absUrl(String key)
。
–
問題描述:
在某些網站中經常會提供用戶評論的功能,但是有些不懷好意的用戶,會搞一些腳本到評論內容中,而這些腳本可能會破壞整個頁面的行為,更嚴重的是獲取一些機要信息,此時需要清理該HTML,以避免跨站腳本攻擊(XSS)。
解決方式:
使用clean()
方法清除惡意代碼,但需要指定一個配置的 Safelist
(舊版本中是Whitelist
),通常使用Safelist.basic()
即可。Safelist
的工作原理是將輸入的 HTML 內容單獨隔離解析,然后遍歷解析樹,只允許已知的安全標簽和屬性輸出。
String unsafe = "<p><a οnclick='attack()'>Link</a></p>"; // 輸出: <p><a >Link</a></p> String safe = Jsoup.clean(unsafe, Safelist.basic()); System.out.println(safe);
說明:
Safelist
不僅能夠在服務器端對用戶輸入的HTML進行過濾,只輸出一些安全的標簽和屬性,還可以限制用戶可以輸入的標簽范圍。
問題描述:
在某些網站中經常會提供用戶評論的功能,但是有些不懷好意的用戶,會搞一些腳本到評論內容中,而這些腳本可能會破壞整個頁面的行為,更嚴重的是獲取一些機要信息,此時需要清理該HTML,以避免跨站腳本攻擊(XSS)。
解決方式:
使用clean()
方法清除惡意代碼,但需要指定一個配置的 Safelist
(舊版本中是Whitelist
),通常使用Safelist.basic()
即可。Safelist
的工作原理是將輸入的 HTML 內容單獨隔離解析,然后遍歷解析樹,只允許已知的安全標簽和屬性輸出。
String unsafe = "<p><a οnclick='attack()'>Link</a></p>"; // 輸出: <p><a >Link</a></p> String safe = Jsoup.clean(unsafe, Safelist.basic()); System.out.println(safe);
說明:
jsoup的Safelist
不僅能夠在服務器端對用戶輸入的HTML進行過濾,只輸出一些安全的標簽和屬性,還可以限制用戶可以輸入的標簽范圍。
–
Connection.Response execute = Jsoup.connect("http://csdn.net/") .proxy("12.12.12.12", 1080) // 使用代理 .execute();
如果你讀完覺得有收獲,不妨點個贊~
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
藍藍設計的小編 http://www.syprn.cn