小馬的大前端之路——Node.js初探

富途web開發團隊發表於2018-04-01

歡迎關注富途web開發團隊

夜已深,愚人節有沒有對中意的女生表白啊。哈哈。。。

小編在這裡先祝福大家。

這個週末,本來想把最近還沒有整理的幾篇文章這裡好發給大家的。無奈小編週末有點暈,還沒整理好。可能是週六晚上烤串吃多了。

木屋燒烤

最近有一篇是關於module(模組化) & babel的文章不過還沒整理出來。那先來一篇node.js熱熱身。這篇文章裡面的很多東西,現在富途前端還是在用,對後面大家去理解富途前端因該會有幫助。

有個好訊息是,富途的前端框架已經慢慢的被vue替代了(原來使用的是JQ , angular.js)。接入層node.js也在推進中,對外的服務指日可待。相信喜歡vue.js ,node.js的小夥伴已經蠢蠢欲動了。

沒錯,小編就是富途node.js服務推動者之一。

在富途做前端,不僅需要寫前端,還需要寫node.js服務的,這個大家不用想太多。

那還是迴歸正題,作為第一個吃螃蟹的小編,是如何在工作中學習運用node.js的吧。

注意:現在富途的前端框架已經往vue.js遷移。但不會影響大家理解閱讀。

原文章來源於富途WEB部落格: 原文連結

正文

一次偶然的機會讓我有幸跨越瀏覽器的鴻溝來真真切切的體驗一次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+ 原生支援了asyncawait語法。

前端框架

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,其中還包含公共檔案的提取,版本控制,壓縮,以及模版檔案注入。

webpack

如何進行版本控制

版本控制用的比較多的就2種:基於檔案和基於hash。

基於檔案就好比,每次打包的時候都會生成不同檔名的檔案。有利於線上上跑多個版本的功能。

基於hash就意味著線上這個功能的檔案永遠就只有一個,無法進行全量灰度。

這裡有個問題就是:基於檔案的版本控制,難點就在於打包後的.js.css檔名是不可控的,所以,並不能把引入的js或css檔案路徑寫死在html模版檔案裡面。所以通過webpack打包的時候,我需要指定模版檔案是哪一個,通過webpack的模版檔案注入外掛完成js或css檔案路徑的引入。

其它方式;通過在webpack打包完成之後,將返回值種的hash引數儲存下來。這樣也可以完成基於檔案的版本控制。

gulp的工作流

gulp結合webpack的應用如魚得水,webpack打包任務是gulp任務流裡最重要的一環。考慮到打包編譯,都交給webpack做了。那gulp所要做的就是保證前端各個任務正確的執行。包括何時執行webpack打包,完成打包以後做什麼。

gulp

前端自動化

這裡的自動化可能與你在別的地方所說的自動化可能有分歧。這裡的前端自動化主要指的是在前端程式碼如何完成自動化打包編譯。其實專案中可以進行自動化的流程有很多,我在專案裡接入的是jenkins,主要用來自動完成前端打包編譯,然後通過zip命令對webpack打包編譯後的所有檔案進行打包成.zip檔案。因為打包後的檔案不入庫。

這裡有疑惑是正常的。首先為什麼不把webpack打包後生成的檔案納入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,這個工具對團隊的幫助是非常大的,預先打包檔案並快取,比在釋出專案的時候再進行打包要好很多。可以預先發現打包問題及時進行補救,以免釋出時打包出現問題而影響釋出進度和線上專案的正常執行。

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管理確實是個好的方案。

  1. 首先通過npm install -g pm2進行安裝。

  2. 安裝完成了之後,就可以在專案中進行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
    }]
};
複製程式碼
  1. 然後就可以通過命令啟動:
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/awaitimport語法。

編碼

debug程式碼

Node.js已經支援在chrome中直接除錯Node.js程式碼,只要在啟動專案的時候新增--inspact引數。

node --inspect server/index.js
複製程式碼

debug

複製上面紅框的url連結到chrome裡面開啟,然後點選start後,再訪問頁面,需要暫停的時候可以點選stop,進行程式碼分析。

總結

作為一個初學者,我只能說Node.js在做接入層上,確實是可以做到如魚得水,關鍵點就是契機。拋開Node.js接入層,前端的工程化是完全可以做的。但是伺服器同構渲染是沒有辦法做到的,除非與後端同學配合;使用Node.js接入層,那麼前端在處理一些棘手的問題時就會遊刃有餘,而且後端服務會得到更深一層的保護,不至於說後端服務直面攻擊,因為多了一層Node.js接入層在前面。

相關文章