一次偶然的機會讓我有幸跨越瀏覽器的鴻溝來真真切切的體驗一次Node.js。
首先,我想說:“很榮幸在經歷了2個月的努力,第一個Node.js項目落地了”。整個項目做下來,還是算比較順暢的。
事情很簡單:Node.js做的是接入層。
事出有因
前端的技術革新是日新月異的,前端工程化已經離不開Node.js。現在大多數的項目使用的是前後端分離的架構,後端提供接口前端通過接口數據進行數據渲染。但是現在前端的代碼邏輯越來越複雜,場景也越來越多。這套架構是否適合所有的應用場景值得考慮了。大前端的出現,就是一種嘗試吧。試圖通過Node.js接入來應對各種應用場景。
不管是個人還是團隊,技術革新是必須的。現在我們團隊面臨的問題就是如此,所以必須有人邁出這一步。而我也很幸運的成爲第一個吃螃蟹的人。
始作俑者
不管什麼技術,不管怎樣的優秀,它的運用與否都是要經過慎重考慮的。但,總不能都不用吧。那怎麼辦呢。找項目試點唄,線上項目運行的好好的肯定不能重構,而且人力緊張啊。只能找新項目了。剛巧,公司需要做新的項目,本以爲按老路子前後分離做。可突然有一天…
組長說:“團隊不是要進行技術選型嗎?看這個項目使用Node.js做接入層可不可行?“。
經過慎重考慮,我回答說:“可以沒問題。”。(管他3721,應了再說。?)
借我老大的一句話:“技術這東西不落地,說了也白說”。
背景:其實團隊對Node.js一直都保持着高度的關注,包括我。之前我一直都有在對Node.js的源碼進行解讀和研究。基礎架構組也一直在進行Node.js技術框架進行調研,希望打造一套適用於團隊開發的集成項目框架。
所以我相信:機會總是會照顧有準備的人的。
就這樣我的Node.js之旅就開始了。
萬事開頭難
雖然我平時可能天天都會用Node.js跑命令,寫各種npm包,甚至還寫過一些自己的項目。但是要真正的用Node.js來真正開發項目還是有壓力的。因爲這種項目技術架構下要求我操心的東西變多了。平時的時候可能我只要寫一些前端邏輯代碼,做做前端工程化。但是這種架構下,要求我必須去學習和應用我不熟悉的東西。
我大致列了一些大的方向:
- 1.Node.js接入層的總體架構是怎樣的?
- 2.前端技術用什麼?
- 3.前端工程化如何做?
- 4.項目如何根據不同的環境(常有的環境:開發,測試,正式)運行?
- 5.前端自動化怎麼搞?
- 6.單元測試?
- 7.編碼風格?
- 8.Node.js如何和服務端對接?
- 9.日誌,上報,登錄服務接入,權限校驗等等我應該怎麼做?
- 10.項目如何發佈上線?
- 11.上線瞭如何保證服務穩定?
- 12.如何debug問題?
可能還有很多很多需要處理的問題但是這已經可以看出一下端倪了。瞬間感覺我懂的只有冰山一角。代碼碼的再漂亮感覺也無力。要求的不再是單一的編碼能力,而是大局觀,思維角度的轉變。
但不管怎樣,新建git倉庫開始搞唄。
如何得到一個合適的項目架構
這個確實是個問題,架構設計的合不合理。會影響到後期編碼是否可以做到快速開發,還會影響後期的功能迭代和維護。
那麼問題來了,我是預先設計還是預先編碼?
這裏我選擇了先編碼,然後重構。
背景:因爲上文已經說過,基礎架構組已經有一個簡單的Node.js集成框架,它是不完整的,但是它夠簡單。也就是說我在這上面重構出自己的項目架構是完全沒有問題的。
你可能會覺得還是要預先設計啊?
說的是側重點不一樣,側重於編碼實現,將這個項目跑起來,然後通過重構去尋找出合適的項目架構。
對於先編碼還是設計這個問題我借用重構裏面的是一句話:
“重構改變了預先設計的角色。如果沒有重構,你就必須保證預先做出的設計是正確無誤,這壓力太大了。這意味着如果將來需要對原始設計做任何修改,代價都將非常昂貴。因此你需要把更多的精力放在預先設計上,以避免日後的修改。如果選擇重構,問題的重點就轉變了。你任然做預先設計,但是不必一定要找出證正確的解決方案,此刻的你只需要得到一個合理的解決方案就夠了。“ –摘自《重構-改善既有代碼的設計》
把一個簡單的解決方法重構成一個靈活的解決方法有多難?答案是:“相當容易”。 –摘自《重構-改善既有代碼的設計》
實在不明白我推薦你去看看《重構-改善既有代碼的設計》這本書。
所以我將側重點放在了預先編碼上,讓後在整個項目demo跑起來之後再去尋找合適的架構。一個合理的架構體系就是把代碼放到它應該出現的位置上去。代碼是具有流失性的,就好比一個房間從來不整理的話,就會變的髒亂不堪。重構就是將代碼再次整理將它放回原位。
技術框架選型考慮
技術框架的選擇會影響着項目的總體架構,編碼,產出效益,以及後期人員維護的成本。
首先我想說:“不管前端還是後端用什麼框架我覺得還是要站在團隊的角度上去考慮這個問題,畢竟這不是個人的項目。總不能說我不在就沒人能維護這個項目吧”。
Node.js後端
koa2。爲什麼沒有使用koa或者express等框架,或者爲什麼團隊不自己開發。
Node.js v8LTS 已經快要來臨。koa已經升級到了koa2版本,沒有必要再用舊的express太老了。koa2在這兩年已經鋒芒畢露,現階段團隊沒有必要花費很多的人力去搞一套自己的框架,可以轉變思維在koa2的基礎上做一個集成的適合團隊項目使用的框架。
基於這個基礎架構團隊使用koa2作爲主框架使用在現階段是最合適的。特別是在Node.js v7.6+ 原生支持了async
和await
語法。
前端框架
jQuery的王朝已經漸漸被瓦解。angular.js,react和vue三足鼎立的時代已經到來。再次基於團隊的現狀,選擇了最有優勢的angular.js v1.x。
在這裏我並沒有說其他框架不好的意思,完全是基於團隊現狀的考慮,以及當前框架是否可以幫助我高效的完成開發的一種考慮。假如有一天我覺得angular.js已經不適合現階段項目開發需求,我會義不容辭的提出我的疑問。
比如:項目需要我們考慮加速頁面渲染時,要考慮服務器渲染;服務器壓力山大時,考慮前後端分離。同構作爲最合適的編碼方式react和vue都是不錯的選擇。
框架沒有對與錯,只有合不合適。
webpack2 作爲當紅炸子雞,我也是優先考慮的。至於爲什麼沒有選webpack3嘛。。。
其實是這樣的,我也有實際的去使用webpack3來做過測試,就是這個項目。我的衡量標準就是壓縮要比現在的要小。最後沒有達到預期效果所以沒有進行合併。
gulp 工作流處理,沒毛病。這裏可能會有的讓人疑惑,爲什麼使用了webpack2 還要使用gulp?爲什麼2個都要用?
其實對於這2個組件,它們沒有絕對的對立關係。在這裏它們是相輔相成的。
總的前端框架:angular.js v1.x + webpack2 + gulp。
babel用來編譯前端代碼。
項目使用的主要框架,如圖:
前端工程化
項目的總體架構和前端技術框架的選型勢必會對前端工程化產生深遠的影響。前端代碼放到哪裏,webpack打包如何做,產出文件放到哪裏。gulp需要做哪些事情,多還是少,煩不煩瑣。這種種問題都會對你項目的架構做出挑戰。這也就是我爲什麼先編碼然後通過重構來調整項目架構的原因之一。假如你預先就把項目的總體架構規定死了,那麼後期你的編碼就會想盡辦法的去套這個項目架構,寫出來的代碼可想而知——一定是不盡人意的。
那麼第一個問題就來了。
自己編寫的anglaur.js部分的源碼放到哪裏
對於這個問題,在使用Node.js開發初期,我就對基礎的架構做出了建議:前端源碼不能放到服務器靜態資源目錄。只有打包後的文件纔會放到靜態資源文件目錄,除非該文件可以直訪問。
這就意味着,我需要尋找一個文件目錄來放置前端源代碼。最合理的位置就是於服務器目錄平級放置。
webpack
通過webpack的編譯打包,將文件保存到靜態資源目錄。我這裏把所以和代碼相關的打包和編譯任務都交給了webpack,其中還包含公共文件的提取,版本控制,壓縮,以及模版文件注入。
如何進行版本控制
版本控制用的比較多的就2種:基於文件和基於hash。
基於文件就好比,每次打包的時候都會生成不同文件名的文件。有利於在線上跑多個版本的功能。
基於hash就意味着線上這個功能的文件永遠就只有一個,無法進行全量灰度。
這裏有個問題就是:基於文件的版本控制,難點就在於打包後的.js
或.css
文件名是不可控的,所以,並不能把引入的js或css文件路徑寫死在html模版文件裏面。所以通過webpack打包的時候,我需要指定模版文件是哪一個,通過webpack的模版文件注入插件完成js或css文件路徑的引入。
其它方式;通過在webpack打包完成之後,將返回值種的hash參數保存下來。這樣也可以完成基於文件的版本控制。
gulp的工作流
gulp結合webpack的應用如魚得水,webpack打包任務是gulp任務流裏最重要的一環。考慮到打包編譯,都交給webpack做了。那gulp所要做的就是保證前端各個任務正確的執行。包括何時執行webpack打包,完成打包以後做什麼。
前端自動化
這裏的自動化可能與你在別的地方所說的自動化可能有分歧。這裏的前端自動化主要指的是在前端代碼如何完成自動化打包編譯。其實項目中可以進行自動化的流程有很多,我在項目裏接入的是jenkins,主要用來自動完成前端打包編譯,然後通過zip命令對webpack打包編譯後的所有文件進行打包成.zip
文件。因爲打包後的文件不入庫。
這裏有疑惑是正常的。首先爲什麼不把weboack打包後生成的文件納入git版本庫?
道理很簡單,git版本庫裏面的任意一個文件產生變化,就會有下一個版本號產生。webpack每次打包編譯就勢必會產生文件變化,如果把打包文件納入版本庫就必須提交文件,從而產生版本號。也就是說我本地提交一次代碼到git庫後,jenkins會進行打包,然後打包文件又必須提交回git庫,這樣就相當於每次提交代碼否會產生2次提交記錄(一次我自己的提交,一次jenkins完成自動化打包後的提交。)。所以爲了不讓jenkins完成打包後向git代碼庫提交文件,所要做的就是把webpack打包後產生的文件都移除版本庫。
但問題沒有這麼簡單,webpack打包不納入版本庫,發佈的時候,這些webpack打包後產生文件怎麼發佈。這裏解決方案就是通過把所有和webpack打包相關的文件用zip命令打包成一個${commitId}.zip
包(commitId 是git每次提交參數的可以通過bash獲取:commitId=$(git rev-parse HEAD)
)。這樣發佈的時候就可以通過commitId找到${commitId}.zip
這個壓縮包,然後解壓它到指定位置即可。
爲什麼有2個打包任務?
第一次是webpack打包,前端代碼需要打包編譯。第二次是文件打包,發佈需要,原因很就是webpack打包文件不入庫的解決方法。
所以要求團隊中必須會搭建並且有使用過jenkins,這個工具對團隊的幫助是非常大的,預先打包文件並緩存,比在發佈項目的時候再進行打包要好很多。可以預先發現打包問題及時進行補救,以免發佈時打包出現問題而影響發佈進度和線上項目的正常運行。
git倉庫支持添加hooks。所以可以在git庫裏添加觸發事件。讓jenkins自動完成打包。
假如有一天,我需要寫單元測試的時候,也可以試着讓jenkins幫我跑自動化測試了。這算是我回答了單元測試的問題嗎?哈哈哈哈哈哈哈。。。。。。
前端問題基本解決了,現在問題拋到了服務端。
Node.js服務端運行環境配置
寫個項目,要跑起來很簡單,我的項目入口文件是server/index.js
。通過執行如下命令就可以啓動:
node server/index.js
但有時候,環境並沒有我想的那麼簡單。因爲項目需要針對不同的環境運行,所以必需對不同的運行環境使用不同的配置文件。這樣就需要我在啓動Node.js服務的時候,必須攜帶不同的參數。所以要求我在編碼的時候儘可能的做到環境參數的配置化——牽涉到與執行環境有關的參數儘量進行配置化。
Node.js接入層服務的接入,權限的校驗
其實對於一個小白來說,很擔心的是我如何才能在Node.js裏面往真正的服務器發起request請求。我項目站點的登錄服務鑑權如何去做,以及用戶登錄了,有沒有權限去訪問都是個問題。
http服務的接入
通過http模塊發起requset請求。其實開始的時候我也是一臉茫然的,如何在接入層請求後端服務,可想而知這是之前作爲前端的我從來沒有考慮過的。現在回想起來就那麼回事。有些事情想着可能很複雜,真正的做起來就好像有種:山重水複疑無路,柳暗花明又一春。
的感覺。
Node.js接入層請求後端服務簡單的代碼實現:
exports.example = async (ctx)=>{ let options = { port: 80, hostname: 'www.test.com', method:'GET', path:'/api/getuser?token=document.cookie.token' }; let getData = function (){ return new Promise((resolve , reject)=>{ let request = http.request(options , (socket)=>{ let data = ''; console.log('status: ' , socket.statusCode , socket.headers); socket.on('data' , (chunk)=>{ data += chunk; }); socket.on('end' , ()=>{ console.log('server call back get data: ' , data); return resolve(data); }); socket.on('error' , (e)=>{ return reject(data); }); }); request.end(); }); } ctx.body = await getData(); }
這裏我沒有考慮https的方式,因爲https是建立在SSL/TLS之上的,也就是說,需要有私鑰和公鑰和CA證書才行。CA證書雖說可以自己頒發但還是得本機自行安裝纔有效。對https自己頒發CA證書感興趣的可以看看這篇文章:HTTPS自簽發CA證書。
後端服務器(PHP/JAVA…)需要做的就是根據請求參數是否合法已經齊全,然後驗證調用者是否有權限使用該功能。這樣的案例比比皆是,比如使用第三方服務。
小到Number校驗
有可能最簡單的參數校驗都不知道如何校驗。這跟javascript語言以及前端的思維方式有關。我開始的時候也是這樣,感覺寫起代碼來怪怪的。
其實這是一個簡單的例子,在前端檢驗一個Number類型的值是不是有效,我一般是通過:
num = typeof num === 'number' && num === num && num !== Infinity ? num : 0;
這種思路和邏輯放在前端完全是沒有問題的,但是在Node.js接入層這麼寫感覺很尷尬。所以要轉變我的思維方式:
num = Number.isFinite(num) ? num : 0;
小到參數的校驗,我都要認真的考慮。是時候改變自己的思維方式了,考慮使用JavaScript原生的方式處理會比自己寫好很多。
權限的校驗
我並不希望所有的用戶都能訪問這個項目,即使他已經登錄了也不行。這就是我要解決的問題。
權限管理在這裏就顯得極其重要了。最好的方式就是把權限相關的功能進行服務化。
使命感覺纔剛剛開始!!!!!
項目的部署上線
可以說我對項目部署和運維基本上是沒有經驗。但是有一點就是項目上線後的可用率是必須要保證的。不能因爲一點小問題,就讓服務掛掉,然後還要人屁顛屁顛的重新手動重啓吧。也不能說服務器斷電了,重啓後也要手動啓動吧。這一些列的問題都是必須解決的。
pm2
很高效的開發完成了項目後,其實項目的真正使命纔要剛剛開始,如何保證服務在線上穩定的運行,保證高可用率。這就需要藉助其它組件來完成了。使用pm2管理確實是個好的方案。
- 首先通過
npm install -g pm2
進行安裝。 - 安裝完成了之後,就可以在項目中進行pm2相關配置。
案例:
//test.config.js 'use strict'; //pm2配置文件 module.exports = { apps:[{ name : 'test', script: './server/index.js',//應用入口 cwd: './', instances : 1, watch : ['server'], env: { 'NODE_ENV': 'development', }, env_production: { 'NODE_ENV': 'production', }, exec_mode : 'cluster', source_map_support : true, max_memory_restart : '1G', //日誌地址 error_file : '/data/logs/pm2/test_error.log', out_file : '/data/logs/pm2/test_access.log', listen_timeout : 8000, kill_timeout : 2000, restart_delay : 10000, //異常情況 max_restarts : 10 }] };
- 然後就可以通過命令啓動:
pm2 start test.config.js
nginx
Nginx 是俄羅斯人編寫的十分輕量級的 HTTP 服務器,Nginx,它的發音爲“engine X”,是一個高性能的HTTP和反向代理服務器。nginx配置也是必不可少的,80
端口就一個,所以我需要nginx進行轉發。
例如下面的案例:
upstream test_upstream { server 127.0.0.1:6666; keepalive 64; } server{ listen 80; server_name www.test.com; client_max_body_size 10M; index index.html index.htm; error_log /data/nginx/log/error_www.test.com.log; access_log /data/nginx/log/access_www.test.com.log combined; location / { proxy_store off; proxy_redirect off; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Remote-Host $remote_addr; proxy_set_header X-Nginx-Proxy true; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_http_version 1.1; proxy_pass http://test_upstream/; proxy_read_timeout 60s; } }
項目啓動的端口是本機的6666
端口,但是我不可能說訪問www.test.com
的時候後面還帶着端口號吧。這個時候就是nginx發揮作用的時候,訪問域名不帶端口默認使用80端口,由nginx做反向代理到我服務6666
端口。
這裏有一點post
請求時client_max_body_size
參數的設定直接會影響data
的大小。
日誌,上報,運營維護
項目的健康與否,都會在日誌和上報中體現。我只需要每天看看日誌,看看視圖就可以對當天項目的運行情況做一個大致的瞭解。如果沒有這些輔助的功能,兩眼一抹黑,發生啥事都不知道。
編碼風格
編碼風格方面遵循eslint的語法標準。使用了最新的async/await
和import
語法。
debug代碼
Node.js已經支持在chrome中直接調試Node.js代碼,只要在啓動項目的時候添加--inspact
參數。
node --inspect server/index.js
複製上面紅框的url鏈接到chrome裏面打開,然後點擊start
後,再訪問頁面,需要暫停的時候可以點擊stop
,進行代碼分析。
總結
作爲一個初學者,我只能說Node.js在做接入層上,確實是可以做到如魚得水,關鍵點就是契機。拋開Node.js接入層,前端的工程化是完全可以做的。但是服務器同構渲染是沒有辦法做到的,除非與後端同學配合;使用Node.js接入層,那麼前端在處理一些棘手的問題時就會遊刃有餘,而且後端服務會得到更深一層的保護,不至於說後端服務直面攻擊,因爲多了一層Node.js接入層在前面。
如果你正在考慮要不要使用Node.js,我是無法給出答案的。
原創:Jin
原文鏈接:https://futu.im/posts/2017-07-26-first-time-use-node.js