前端流程自動化

lucifer210發表於2018-03-16

原文刊登在github

流程自動化

前端從誕生依賴可以說經歷了一個從不規範到規範,再到自動化的一系列過程。最開始的時候沒有前端這個領域,所有的前端職責都是由服務端來代替完成,當時的狀況是非常混亂的,派別叢生,瀏覽器廠商規範不統一,模組機制混亂,程式碼風格寫法各自為派,真有點群雄逐鹿的感覺。從HTML5,CSS3,ES6(536)的釋出開始,前端已經趨於規範化。瀏覽器的規範趨向一致,微軟的瀏覽器也逐漸跟上了步伐,瀏覽器相容性問題向前邁進了一大步。如今前端開發的過程也越來越規範,越來越有規律可循。因此大家在想這些有規律的東西能不能夠實現自動化(程式設計師最喜歡偷懶),能不能將枯燥無味的工作交給電腦來做,減少重複的工作,提高工作效率。 答案當然是可以的,而且目前業內已經有非常多的成功案例。從facebook的Waitir,google的自動化流程系統,到國內阿里的def,再到小公司使用的gitlab ci,發現很多公司都已經開始了流程自動化的探索。本章我會先講述前端的工作流程是怎樣的,然後對前端工作流程進行分析,分析哪些任務可以自動化,實現自動化的思路是什麼,最後我會講述如何搭建一個自動化的小平臺。

前端工作流程

讓我們開始之前,先來看下我們目前前端的工作流程是怎麼樣的,我分享的只是我個人的工作流程,不同公司和個人可能略有不同,但總的思路應該是差不多的,請不要太介意。

階段一 需求產生 - 準備開發

這個階段又可以分為如下三個小階段:

需求評審

正常情況下,從需求產生到準備開發,應該有一個需求評審會,這時候相關開發和產品經理會聚集在一起,商討需求是如何產生的(敏捷開發的需求通常是從客戶的反饋中產生的),我們做的功能有沒有解決客戶的問題,我們對需求的理解有沒有偏差和需求的優先順序等。經過徹底充分的討論之後,如果需求理解沒有問題,並且需求確實可以解決客戶的問題,我們會為工作安排時間和優先順序,將task列到看板(同時列到系統和白板上),並每天追蹤更新。

理解產品經理的真正意圖,明白需求產生的背景

拆分元件和模組

前端目前比較推薦的工作方法是元件式開發,即將頁面拆分成足夠粒度的小元件和模組。由單獨的人負責相關獨立模組和元件的開發,做到元件和模組的複用,這個已經在第二章講過了,不在此贅述。

建立分支

目前大多數網際網路公司的版本管理工具都是使用git,包括我們的公司,而且公司內部通常也有自己的git flow,我們做新功能的時候通常會建立一個feature分支,開發完畢後合併到release分支等待測試和釋出。當然就算你是SVN思路也是一樣的。

階段二 開始開發 - 提交測試

寫單元測試

這裡通常有兩個比較常見的問題。問題一是能不能不寫單元測試,或者說不寫單元測試有什麼不好的影響?第二個問題是為什麼要先寫單元測試而不是先寫程式碼?我們對這兩個問題一一進行解答。先說第一個問題,其實不寫單元測試是很容易做到的,就好像我們不努力鍛鍊身體一樣簡單,但是當我們面對自己的一大塊腹肌的時候,是不是有那麼一丁點兒後悔呢?單元測試也是一樣,當你開始寫的時候,沒有任何收效,據統計寫單元測試的時間要高於寫程式碼的時間,那麼我們為什麼還要“白”花那麼多時間寫測試用例呢?那是因為當隨著應用規模逐漸擴大,複雜度逐漸上升的時候,完善的測試用例,給你修改程式碼的勇氣。當單元測試顯示all pass的時候,彷彿有人跟你說“幹吧,哥們,沒毛病。”。你不會因為怕改壞了程式碼而躡手躡腳。但這裡要強調一點的是,覆蓋率低的單元測試不但不能夠起作用,反而會給人一種“幹吧,哥們,沒毛病。”的假象。這也是為什麼我後面強調使單元測試覆蓋率足夠高的原因。我們再來看下第二個問題,這個問題涉及到一個概念叫測試驅動開發(TDD)。TDD的理念就是先寫測試用例,然後寫具體實現。TDD的一個重大優點就是你將具體實現放到後面,這樣你就不會深陷細節的泥潭,你就擁有更清晰的視野,你會對業務或者邏輯理解更佳深刻。這就好像你看過很多高手寫程式碼,它們會先將思路寫下來,然後再寫程式碼是一個道理。

回答了關於測試的常見問題,我們來看下單元測試究竟怎麼寫。其實也簡單,單元測試通常來自於測試人員整理的測試用例,當然一些特殊的演算法邏輯需要自己整理啦。一個原則就是能用測試同學就用測試同學,不用白不用,對吧?但是前端寫單元測試的時候,總會感覺很困難,我一開始也是這麼覺得的。後來我接觸了函數語言程式設計,整個人就感覺豁然開朗。傳統的物件導向程式設計,指令式程式設計有一個非常大的弊端,引用Joe Armstrong(Erlang語言的創造者)的一句話就是:

你想要一個香蕉,但得到的卻是一個拿著香蕉的大猩猩

沒錯,當我回顧我很久之前的程式碼,的確如此。這也不能完全怪我們,我們已經習慣了各種假設,各種外部依賴。我們將這些變成理所當然,我們不斷地改變狀態的狀態,導致狀態難以追蹤。因此我們寫單元測試非常困難。當你開始以函數語言程式設計理念寫程式碼的時候,你會發現程式碼非常好測試。比如目前比較流行的react的展示元件,就是一個純函式,對這樣的元件進行測試就很簡單。當你研究redux的程式碼的時候,你會發現redux的reducer設計的精妙,他將reduce在空間上的抽象變成reducer在時間上的抽象,並且reducer純函式的理念也讓程式碼更容易測試,具體可以看我的這篇博文

防禦性編碼

寫程式碼之前要先想有沒有輪子可用。如果沒有在開始造輪子。我喜歡將邏輯劃分為多個函式,然後在函式開頭對入參進行校驗,並對每一步可能出錯的地方進行校驗,典型的是npe(null pointer exception)。這就是典型的防禦性程式設計。

// String a -> Number b -> Boolean
function testA(a, b) {
   if (!a) return false;
   if (!isString(a)) return false;
   if (!b) return false;
   if(!isNumber(b)) return false
   return !!a.concat(b);
}

複製程式碼

這種方法在前端尤其有效,因為js是動態語言,如果你不使用typescript等增加靜態檢測的功能的話,程式碼會變得脆弱不堪。一個簡單的做法就是,假設每一行程式碼都會遇到異常情況,都會報錯。

使單元測試覆蓋率足夠高

前面講了單元測試的重要性,以及單元測試的寫法思路。這裡強調一下單元測試的覆蓋問題,低的單元測試覆蓋率毫無用處,甚至會起反作用,因此保持足夠高的單元測試覆蓋率顯得非常重要,業界普遍認可單元測試覆蓋率在95%以上是比較合適的。

階段三 測試完畢 - 可釋出狀態

這個階段我們的程式碼已經經過了自測和測試人員的迴歸,我們認為可以釋出上線了。通常我們還會讓產品經理進行驗收,看看是不是他們想要的效果。

編譯

目前寫的程式碼是在多個檔案的,我們的程式碼使用了很多瀏覽器不支援的特性,我們需要將css從js中提取出來等等。這些工作都需要通過編譯來完成。

包分析

我們的專案的程式碼是由許許多多的依賴構成的,我們會依賴一些框架如react,vue,我們會使用一些工具庫如lodash,ramdajs,mostjs等。這些都構成了專案的不穩定和不確定。這也就是為什麼大公司如阿里,會對外部依賴有著很強的執念。npm也察覺到了這一點,以至於現在的npm在安裝過後都是鎖定版本的。但這依舊無法保證依賴包的質量。 一個檢視包質量的原則就是文件足夠豐富,單元測試覆蓋率足夠高,受歡迎(start足夠多),雖然上述條件不是一個庫質量良好的充分條件,但卻是必要條件,我們可以通過它篩選一大批不合格的庫。

程式碼檢查

我們會對程式碼進行檢查,有沒有語法錯誤等。這一步可以通過eslint檢查,也可以通過flow或者typescript這樣的靜態檢查工具檢查。總之,這一步是檢查有沒有錯誤程式碼或者不符合規範的程式碼。

程式碼優化

程式碼已經通過了檢查,這個時候我們需要對程式碼進行優化,比如壓縮,合併,去空格去console等,或者提取公共依賴,再或者刪除殭屍程式碼(tree shaking)。

CodeReview

我們會組織相關人員進行程式碼評審,確保程式碼質量。這部分是人工完成,是對前面工作的最後把關。

階段四 準備釋出 - 釋出完成

釋出靜態資源到CDN

我們資源釋出到CDN等待最終的釋出,保證版本釋出之後,使用者可以直接獲得最新的CDN資源。

打tag

我們將程式碼打tag,一個個tag就像是一個個里程碑。 當我們需要對某一個版本程式碼進行修復的時候,tag的作用就顯示出來了。

修改線上版本號

我們的功能已經達到了可以釋出的狀態,然後我們會對版本進行釋出。修改線上的版本號,這樣我們的使用者就可以訪問到我們最新寫的程式碼了。

階段五 釋出上線 - 線上驗證

我們已經將程式碼釋出上線了,通常我們需要驗證下程式碼是否正確釋出,有沒有影響線上其他功能。

上面的過程可能是大多數網際網路公司的工作流程了。那麼下一節我會對每一個階段進行分析,找出可以自動化的點,並講述自動化的技術思路是怎樣的。

實現流程自動化的思路是什麼

上面講述了常規的需求產生到功能釋出的完整過程,通過上面的分析,我們發現階段三和階段四是可以高度自動化的,階段三和階段四進行做了什麼事,為了方便大家的理解,我整理了一個圖:

圖3.1

圖中的虛線表示自動完成,無需人工。實線表示需要人工操作。圖的中心點是dev,可以看出dev的操作有三個,分別是提交(commit)打tag,以及提交pr(pull request)。不同的操作會觸發不同的鉤子,完成不同的操作。我們一個節點一個節點進行分析,它們分別做了什麼事,以及設計實現的思路。圖中需要實現的系統有三個,第一個是包分析引擎(package analyser),第二個是CI中心,第三個是CD中心,我們分別來看。

package analyser

包分析工具這裡可以是前端的npm包分析,也可以是後端的比如maven包分析。這裡以npm包分析為例,maven等其他包分析同理,只是具體技術實現細節不同,npm包分析和maven包分析只是具體策略不同,我們可以通過策略模式將具體的分析演算法封裝起來。首先看下包分析引擎實現的功能,其實包分析引擎就是分析應用依賴的包,並逐個遞迴分析其依賴包,找到其中有風險的依賴,並通知給使用者(專案擁有者)。具體功能包括但不限於分析有安全風險的包,提示有補丁更新,有了這些依賴資料,我們甚至可以統計公司範圍內包的使用情況(包括各個版本),這些資料是很有用的。後面一節我們會具體分析包分析引擎的實現細節,使讀者可以自行搭建一個npm包分析引擎。

CI

CI(Continuous Integration)是持續將新功能整合到現有系統的一種做法,極限程式設計也借鑑了CI的基本思想。那麼我們是怎樣使用CI的呢?我剛才在圖中也體現了,開發者的提交會觸發CI,CI會做一些單元測試,程式碼檢測等工作,如果不通過則反饋給相關人員,否則將通過的程式碼合併到庫中。也就是說CI並不是一項技術,二是一種最佳實踐,更多可以參考wikipedia。後面我會介紹實現一個CI的基本思路。

CD

CD(Continuous Delivery)同樣也是一種最佳實踐,並不是一項技術。它的基本思想是保證程式碼隨時可釋出,它保證了程式碼釋出的可信賴性,同時持續整合減少了開發人員的工作量。更多可以參考wikipedia。後面我會介紹實現一個CD的基本思路。

當然我們的系統還比較不完善,我們還可以增加配置中心,方便對版本進行管理,還可以增加監測平臺(第四章有講),大家可以發揮自己的聰明才智。

我們還漏了什麼

上面講述了一個需求產生到功能上線的過程,我們也分析瞭如果進行自動化。我們還忽略了一個專案初期的一個過程,就是搭建腳手架的過程。如何將搭建腳手架的過程也自動化,配置化,最好還能在公司範圍內保持一致性。

腳手架做了什麼

當我們開始一個新專案的時候,要先進行技術調研和選型,當確定了技術方向的時候,我們需要一個架子,專案成員可以根據這個架子寫程式碼。這個架子可以手工生成,很早之前我就是這麼幹的,但是手工生成的有很多缺點。第一就是效率低,第二就是不利於統一。為了解決這個問題,我們引入了雲腳手架的概念。要明白雲腳手架我們需要先知道腳手架。我們先來看下腳手架做了什麼事,簡單來說腳手架就是生成專案的初始程式碼。我們通過目前比較火的react的配套腳手架工具create-react-app(以下簡稱cra)來認識一下腳手架的工作原理。我們先來看下cra的使用方法:

npm install -g create-react-app

create-react-app my-app
cd my-app/
npm start
複製程式碼

這樣我們就有了下面的專案結構:

圖3.2

如果要實現以上功能,一個最簡單的方法就是執行create-react-app xxx 之後去遠端下載檔案到本地。其實cra的思路就是這樣的,我用一張圖來表示cra的基本過程:

圖3.3

每一步的實現也比較簡單,大家可以直接檢視原始碼

中心化,可配置的腳手架服務

上面介紹了腳手架的作用和實現原理。但是上述的腳手架無法實現中心化,也就是說不同團隊無法形成相互感知,具體來說就是不同專案的腳手架是不同的或者不能夠實現高度定製化,因此我們需要構建一箇中心化,可配置的腳手架服務,我稱之為雲腳手架。腳手架採用CS架構。客戶端可以是一個CLI,服務端則是一個配置中心,模組發現中心。如下圖是一個雲腳手架的架構圖:

圖3.4

客戶端通過命令告訴服務端想要初始化的模版資訊,服務端會從模板庫中查詢對應模板,如果有則返回,沒有則請求npm registry,如果有則返回並將其同步到模板庫中,供下一次使用。如果沒有返回失敗。 這樣對不同團隊來說,腳手架是透明的,團隊可以定製適合自己的腳手架,上傳到模板庫,供其他團隊使用,這就形成了一個閉環。

如何搭建一個自動化平臺

搭建package analyser

我將包分析引擎的工作過程分為以下三個階段

建立黑白名單

我們需要對包分析,分析的結果當然需要資料支撐。因此黑白名單是不可少的,我們可以自己補充黑白名單,我們甚至可以建立自己的黑白名單系統,當然也可以接入第三方的資料來源。不管怎樣,第一步我們需要有資料來源,這是第一步也是最重要的一步。為了簡單起見,我以JSON來描述一下我們的資料來源:

// 所有的key都是npm包的包名
{
 "whiteList": ["react", "redux", "ant-design"],
 "blackList": {
   "kid": ["insecure dependencies 'ssh-go'"]
 }
}

複製程式碼

遞迴分析包並匹配黑白名單

這一步需要遞迴分析包。 我們的輸入只是一個配置檔案,npm來說的話就是一個package.json檔案。我們需要提取package.json的兩個欄位dependencies和devDependencies,兩者就是專案的依賴包,區別在於後者是開發依賴。這個時候我們可以獲得專案的一個依賴陣列。形如:

 const dependencies = [{
   name: "react",
   version: "15.4.2"
 }, {
   name: "react-redux",
   version: "5.0.3"
 }]
複製程式碼

然後我們需要遍歷陣列,從npm registry(可以是官方的registry, 也可以是私有的映象源)獲取包的具體內容,並遞迴獲取依賴。這個時候我們獲取了專案所有的依賴的和深層依賴的包。最後我們需要根據包名去匹配黑白名單。我們還有一步需要做,就是獲取包的更新日誌,將有意義的日誌(這裡可以自己封裝演算法,究竟什麼樣的更新日誌是有意義的,留給大家思考)輸出給專案擁有者。

結果輸出

我們已經將所有的依賴包進行匹配,這個時候已經知道了系統依賴的白名單包,黑名單包和unknown包,我們有了匹配之後的資料來源。

 const result = {
    projectName: "demo"
    whiteList: ["react", "redux"],
    blackList: [{name: "kid", ["insecure dependencies 'ssh-go'"]}],
    changeLog: [{name: ""react-redux, logs: {url: '', content: ''}}]
 }
複製程式碼

我們要做到資料和顯示分離。這個時候我們將資料單獨存起來,然後採用友好的資訊展示出來,這部分應該比較簡單,不多說了。

搭建持續整合平臺

如果想要搭建持續平臺的話,最基礎的三個服務是要有的,lint,test以及report。其實lint,test和report的具體實現已經超出了CI的範疇,這裡就大致講以下。對於lint來說,本質上是對js文字的檢查,然後匹配一些規則,業界比較有名的是紅寶書的作者nzakas的eslint,關於eslint的整體架構可以查閱這裡zakas的初衷不是重複造一個輪子,而是在實際需求得不到JSHint團隊響應的情況下,自己開發並開源了eslint:一個支援可擴充套件、每條規則獨立、不內建編碼風格為理念編寫一個js lint工具。對於test,其實就是執行開發人員寫的測試用例,並保證執行正確且覆蓋率足夠高。如果上述步驟出錯,則會向相關人員告警。下面是CI的架構圖:

圖3.5

程式碼會經過lint,途中會從配置中心拉取專案的presets和plugins,通過後進入下一個流水線test,test會將程式碼分發到瀏覽器雲中進行單元測試和整合測試,並將結果發給相關人員,上述兩個步驟如果出錯也都會通過report service傳送資訊給相關人員。

搭建持續部署平臺

持續部署在持續整合的基礎上,將整合後的程式碼部署到更貼近真實執行環境的「類生產環境」(production-like environments)中。比如,我們完成單元測試後,可以把程式碼部署到連線資料庫的 Staging 環境中更多的測試。如果程式碼沒有問題,可以繼續手動部署到生產環境中。因此持續部署最小的單元就是將程式碼劃分為一個個可以釋出的狀態。下面是一個經典的企業級持續部署實現:

圖3.6

https://continuousdelivery.com/implementing/architecture/

可以看出持續整合,其實是將新模組融合到系統裡面,形成一個可釋出單元,它本身不涉及到釋出的流程。真正將程式碼釋出到線上,還是需要人工來操作。

可以通過配置中心實現程式碼釋出

要想實現持續部署的架構,是需要程式碼和系統架構的配合,這是和前面我講述的其他系統不一樣。持續部署要求程式碼的耦合度要足夠低,儘量少地影響其他模組。每當釋出一個新功能的時候,不需要將全部程式碼測試迴歸,而是採用mock,stub等方式模擬外部依賴。目前比較流行的微服務,也是一種將程式碼解耦合的一種實踐,它將系統劃分為若干獨立執行的服務,服務之間不知道彼此的存在,甚至服務之間的語言,技術架構都不相同。前面的章節講述了元件化和模組化,在這裡又可以看出元件化和模組化的重要性。

更多關於持續部署實現的介紹

搭建通知服務等其他可接入的第三方服務

前面反覆提到了反饋系統feedback。可能我們還需要其他第三方系統,比如資料視覺化系統等。這裡以接入通知服務為例,講解如何接入一個通用的第三方系統。在談通知服務具體細節之前,我們先來講下接入一個第三方服務需要做什麼。本質上接入不同的服務是服務治理的範疇,而目前服務治理當屬微服務佔據上風。這種通過不斷接入“第三方”服務的方式使得業務和應用分離。在微服務之前,大家普遍的做法是將不同的系統做成不同的應用,然後通過某些手段進行通訊。這種方式有一個顯著的缺點就是應用有很多重複冗餘的邏輯和程式碼。而微服務則不同,微服務將系統拆分成足夠小的塊,這樣能夠顯著減少冗餘。服務化有以下特點:

  • 應用按業務拆分成服務
  • 服務可獨立部署
  • 服務可被多個應用共享
  • 服務之間可以通訊

這裡並不打算討論服務治理的具體實施細節,但是需要明白的是通過這種微服務的思想。我們需要通知服務,只需要傳送一個訊號,告訴通知服務,通知服務返回一個訊號,表示輸出的結果。比如我需要接入郵件服務這個通知服務。程式碼大概是這樣的:

'use strict';
const nodemailer = require('nodemailer');
const promisify = require('promisify')

// Generate test SMTP service account from ethereal.email
// Only needed if you don't have a real mail account for testing
exports default mailer = async context => {

    // create reusable transporter object using the default SMTP transport
    let transporter = nodemailer.createTransport({
        host: 'smtp.ethereal.email',
        port: 587,
        secure: false, // true for 465, false for other ports
        auth: {
            user: account.user, // generated ethereal user
            pass: account.pass  // generated ethereal password
        }
    });

    // setup email data with unicode symbols
    let mailOptions = {
        from: '"Fred Foo ?" <foo@blurdybloop.com>', // sender address
        to: 'bar@blurdybloop.com, baz@blurdybloop.com', // list of receivers
        subject: 'Hello ✔', // Subject line
        text: 'Hello world?', // plain text body
        html: '<b>Hello world?</b>' // html body
    };

    // send mail with defined transport object
    await promisify(transporter.sendMail(mailOptions, (error, info) => {
        if (error) {
            return console.log(error);
        }
        console.log('Message sent: %s', info.messageId);
        // Preview only available when sending through an Ethereal account
        console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info));

        // Message sent: <b658f8ca-6296-ccf4-8306-87d57a0b4321@blurdybloop.com>
        // Preview URL: https://ethereal.email/message/WaQKMgKddxQDoou...
   
    }));
          
    return {
           status: 200,
           body: 'send sucessfully',
           headers: {
               'Foo': 'Bar'
           }
      }
};
複製程式碼

如果每一個應用需要使用郵件服務,就需要寫這樣的一堆程式碼,如果公司的系統不同導致語言不同,還需要在不同語言都實現一遍,很麻煩,而如果將傳送郵件抽象成通知服務的具體實現,就可以減少冗餘程式碼,甚至java也可以呼叫我們上面用js寫的郵件服務了。

小提示。當我們需要使用郵件服務的時候,最好不要在程式碼中直接向郵件服務傳送訊息,而是向通知服務這種抽象層次更高的服務傳送。

有一個流行的概念是faas(function as a service)。它往往和無服務一起被談起,無服務不是說沒有伺服器,而是將服務架構透明,對於普通開發者來說就好像沒有伺服器一樣,這樣就可以將我們從伺服器環境中解放出來,專注於邏輯本身。fission(Fast Serverless Functions for Kubernetes)是一個基於k8s的無服務框架。通過它開發者可以只關注邏輯本身,我們可以直接將上面的mail方法作為部署單元。下面是一個例子:


  $ fission env create --name nodejs --image fission/node-env

  $ curl https://notification.severless.com/mailer.js > mailer.js

  # Upload your function code to fission
  $ fission function create --name mailer --env nodejs --code mailer.js

  # Map GET /mailer to your new function
  $ fission route create --method GET --url /mailer --function mailer

  # Run the function.  This takes about 100msec the first time.
 
  $ curl -H "Content-Type: application/json" -X POST -d '{"user":"user", "pass": "pass"}' http://$FISSION_ROUTER/mailer

複製程式碼

這樣如果有一個系統需要將mailer服務切換成sms服務就很簡單了:


 $ fission env create --name nodejs --image fission/node-env

 $ curl https://notification.severless.com/sms.js > sms.js

 # Upload your function code to fission
 $ fission function create --name sms --env nodejs --code sms.js

 # Map GET /mailer to your new function
 $ fission route create --method GET --url /sms --function sms

 # Run the function.  This takes about 100msec the first time.

 $ curl -H "Content-Type: application/json" -X POST -d '{"user":"user", "pass": "pass"}' http://$FISSION_ROUTER/sms

複製程式碼

服務的實現也足夠簡單,只需要關心具體邏輯就OK了。

自動化指令碼

哪些地方應該自動化

上面講述了軟體開發的過程,以及我們可以將哪些過程自動化。這一節,我們講述自動化的第二部分自動化指令碼。刨除軟體開發本身,計算機中其實也充滿了重複性工作,同樣也充滿了解決這些重複工作的自動化解決方案,這些解決方案可以是一個指令碼,可以是一個軟體或者外掛等,總之它將人們從重複性的工作中解脫了出來。舉個例子,我們都有過下載視訊的經歷,我們看上了某個網上的一個視訊,我們想下載下來,但是在下載的時候,發現只有VIP可以下載。我們就去網上查詢解決方案。我們按照教程歷經千辛萬苦終於將視訊下載了下來。下次我們又要下載視訊了,我們還要經歷了上面的步驟(我們甚至還要再看一遍教程)。於是自動下載線上網站視訊的自動化解決方法出現了,人們只需要簡單的操作就可以將自己心愛的視訊下載下來,多麼省心!類似的還有很多,比如批量處理工具,一鍵重灌系統工作等等,根本數不過來。

本質上,任何應該自動化的都應該被自動化。只要是重複性工作,都應該將其自動化。為什麼電腦可以處理的東西,非要人工來處理呢?開發者應該將精力花費在更值得花費的地方,一些有創造性的工作上。上面介紹了普通使用者的例子,現在我舉一個工作中的例子。比如之前我的公司的釋出,就是一個簡單的流程,每次釋出都要按照流程執行,這就很適合去自動化。我們當時的釋出流程是這樣的

git tag publish/版本號 
git push origin publish/版本號
複製程式碼

然後去cdn(https://g.alicdn.com/dingding/react-hrm-h5/version/index.js) 上檢視是否釋出成功。 我覺得這已經花費我一定的時間了,而且新來的同學不得不學習這種繁瑣的東西。 為什麼不能自動化呢? 借用墨菲定律下, 該自動化的終將實現自動化。 自動化真的不止是減少時間,更重要的是減少出錯的可能。 軟體工程領域有這麼一句話,減少bug有兩種方式,一種是少寫程式碼以至於沒有明顯bug。 二是寫很多程式碼以至於沒有明顯bug。

少寫程式碼,少做重複的事,這是我的信條。

如果你認真觀察的話,你會發現可以自動化的東西實在是太多了。當你真正將自動化貫徹到實際編碼生活中去的時候,你會發現你花費在重複工作上的時間在縮少,並且你的幸福感會提升,你會很有成就感。之前看過一個文章,文章裡面說它會將任何可以自動化的東西自動化(不僅限於工作),它會編碼控制如果在晚上下班的時候,自己的session還在的話(還沒下班),就發自動給老婆發簡訊,簡訊內容是從預先設定好的短語中隨機選取的。

之前我在微博上看到一個研究,它通過分析女朋友的微博,來分析女朋友的情緒。我覺得上面傳送簡訊的時候,如果可以根據老婆的情緒對應智慧回覆不同內容會更棒。

運維

如果大家有運維經驗的話,會知道運維經常需要開啟,停止,重啟服務。這些工作有很強的規律性,很適合做自動化。幸運的是,我們藉助shell,可以與作業系統深度對話,輕鬆實現上面提到的運維需求。下面是一個shell指令碼實現服務啟動,停止和重啟的例子。

#! /bin/sh
DIR= `pwd`;
NODE= `which node`

// 第一個引數是action,是start,stop和restart中的一個。
ACTION=$1

# utils
get_pid() {
   echo `ps x|grep my-server-name  |awk '{print $1}'`
}

# 啟動
start() {
 pid= `get_pid`;
 
 if [ -z $pid ]; then
   echo 'server is already running';
 else
   $NODE $DIR/server.js 2>&1 &
   echo 'server is running now'
 fi
}
// stop和restart程式碼省略
case "$ACTION" in
   start)
      start
   ;;
   stop)
      stop
   ;;
 esac
複製程式碼

藉助shell強大的程式設計能力,還有將資料抽象成流,並通過組合流完成複雜任務的能力,我們可以構建非常複雜的指令碼。我們甚至可以將檢測三方庫潛在風險資訊功能做成指令碼,然後整合到CI中,只有想不到,沒做做不到。這裡只是給大家提供思路,希望大家可以根據這個思路進行延伸,從而將一切應該被自動化的東西全部自動化。

開發流程自動化

上面講述了哪些地方應該被自動化。大家看了後很可能是拿了錘子的瘋子,你會發現所有的東西都是釘子,是不是任何東西都可以自動化?當然不是! 可以自動化的應該是有著極強規律的枯燥活動,而充滿創新的任務還是要讓開發者自己享受。因此當你覺得某項工作枯燥無味,並且有著很強的規律和判別指標的時候就是你拿起手裡錘子的時候。比如我每天都要回報我的工作,將自己的進度同步給組裡其他人,雖然很枯燥乏味(希望我的領導不會看到),但是並沒有規律性。因此不應該被自動化。

這裡再舉一個例子。我將開發過程中需要處理的事情進行了分類,我稱為元指令碼(meta-script),分別有如下內容:

  1. concat-readme (將專案中所有的readme組合起來,形成一個完整的readme)

  2. generate-changelog (根據commit msg 生成 changelog)

  3. serve-markdown (根據markdown生成靜態網站)

  4. lint (程式碼質量檢測)

  5. start server (開啟開發伺服器)

  6. stop server (停止執行開發伺服器)

  7. restart server (重啟開發伺服器)

  8. start-attach (attach到瀏覽器,以在編輯器中進行除錯)

每一個meta-script都是一個小的指令碼或者一個外部庫(external library),由於我使用的是npm作為包管理工具,因此我將meta-script放到了package.json檔案中的script裡面。這樣我就可以通過執行npm run xxx 執行對應的指令碼或者外部庫了。但是別忘了,有時候我們需要做一些複雜的任務,比如我需要在瀏覽器中檢視專案中所有的readme。那麼我需要先把專案中的readme全部concat起來,然後將concat的內容作為資料來源,傳給serve-markdown,然後serve-markdown傳給start-server。程式碼大概是這樣的:


npm run concat-readme > npm run serve-markdown > npm run start-server --port 1089

複製程式碼

我稱上面的程式碼task,然後我們把上面的程式碼也放到package.json的script中,似乎這種做法很好地解決了問題。但是它有幾個缺點。

  • meta-script 和 task 混雜在一起,這樣本身並不可怕,但是此時並不是所有的script都可以很好重用,我們的task並不是為了重用。
  • package.json是作為版本控制的一部分存在,如果某個開發者希望根據自己的情況定製一個task,就不應該放到這裡了。

因此我的做法是將meta-script放到版本庫(這個例子我們放到了package.json中),然後將task放到編輯器中控制。我使用的編輯器是VSCODE,它有一個task manager功能,也可以下載第三方外掛進行擴充套件。然後我們可以自己定義task,比如上面的我們可以作為個人配置儲存起來,命名為start doc-site。

我們可以繼續組合:


npm run changelog > npm run serve-markdown > npm run start-server --port 1089

複製程式碼

我們通過meta-script又增加了一個很好用的task,我們可以命名為start changelog-site。

還有更多:


npm run stop > npm run start-attach

複製程式碼

我們又實現了一個“放棄”瀏覽器除錯,而用editor除錯的task,我們稱之為editor-debug。

我們可以增加更多的meta-script,我們可以根據meta-script組合更多task。然後我們只需要one-key就可以實現任意中組合的功能,是不是很棒?自己動手試試把!

無處不在的自動化

上面舉的例子是一個典型的開發流程,那麼其他日常的自動化怎麼去做呢?比如我要控制電腦傳送郵件,比如我要控制電腦睡眠,我要調整電腦的音量等。雖然我們也可以按照上面的思路,寫一個元指令碼,然後將元指令碼組合。但是這裡的元指令碼似乎並不能通過node的cli程式或者shell指令碼實現。但是這恰好是自動化必不可少的一環。因此我們面臨一個GUI的自動化執行過程,將我們繁瑣重複的UI工作中解脫出來。這裡介紹在mac下自動化的例子。

JXA

說到mac中自動化,jxa是一種使用javaScript與mac中的app進行通訊的技術。通過它開發者可以通過js獲取到app的例項,以及例項的屬性和方法。通過jxa我們可以輕鬆通過JavaScript來自動化指令碼完成諸如給某人傳送郵件,開啟特定軟體,獲取iTunes的播放資訊等功能。下面舉個傳送郵件的例子:


const Mail = Application("Mail");
 
const body = "body";

let message = Mail.OutgoingMessage().make();
message.visible = true;
message.content = body;
message.subject = "Hello World";
message.visible = true;
 
message.toRecipients.push(Mail.Recipient({address: "a@duiba.com.cn", name: "zhangsan"}));
message.toRecipients.push(Mail.Recipient({address: "b@duiba.com.cn", name: "lisi"}));
 
message.attachments.push(Mail.Attachment({ fileName: "/Users/lucifer/Downloads/sample.txt"}));
 
Mail.outgoingMessages.push(message);
Mail.activate();

複製程式碼

有兩種方式執行上面的例子,一種是命令列方式,另一種是直接作為指令碼執行。

  1. 命令列方式執行

osascript -l JavaScript -e 'Application("iTunes").isrunning()'

複製程式碼
  1. 作為指令碼執行

一種方式是儲存為檔案之後執行


osascript /Users/luxiaopeng/jxa/hello.js

複製程式碼

另一種是用蘋果自帶的指令碼編輯器:

圖3.8

執行之後效果是這樣的:

圖3.7

jxa提供了豐富的api供我們使用。詳細可以檢視指令碼編輯器-檔案-字典:

圖3.9

比如我要檢視dash的api:

圖3.10

但是遺憾的是並不是所有的app都提供了很多有用的api,比如釘釘,也並不是所有的程式都有字典,比如qq,微信。好訊息是mac自帶的程式介面還是比較豐富的。但是我們發現儘管如此,我們想要實現某些功能,還是會比較複雜。對於不想太深入瞭解並且想要自動化的開發者來說一款簡單的工具是有必要的,下面我介紹一款在mac下的神器。

Alfred

JXA的功能非常強大,但是其功能比較繁瑣。如果你只是想簡單地寫一個自動化指令碼,做一些簡單的瞭解。介紹大家一個更加簡單卻不失強大的工具-alfred- workflow。 你可以自定義自己的工作流,支援GUI,shell指令碼甚至前面提到的jxa寫工作流。其簡單易用性,以及其獨特的流式處理,各種組合特性使得它功能非常強大。下面是我的alfred-workflow:

圖3.11

你可以像我拆分開發流程一樣將你的工作流拆解,每一部分實現自己的功能,設定語言可以不一樣。比如處理使用者輸入用bash,然後bash將輸入流重定向到perl指令碼等都是可以的。 alfred-workflow可以允許你簡單地新增檔案操作,web操作,剪貼簿等,設定不用寫任何程式碼。

圖3.12

總結

本章通過前端工作流程入手,講解了前端開發中的工作,並且試圖將其中可以自動化的步驟進行自動化整合。然後講述了完善的一個自動化平臺系統是怎樣的,以及各個子系統實現的具體思路是怎樣的,通過我的講解,我相信大家應該已經理解了自動化的工作內容,甚至可以自己動手搭建一個簡單的自動化平臺了。但是程式設計師中的自動化遠不止將實現需求的流程自動化,我們還會搞一些提高效率的小工具,本質上它們也是自動化。只不過他不屬於工程化,在本書的附錄部分,我也會提供一些自動化小指令碼。

相關文章