離職後,對專案的記錄、總結

93發表於2018-06-13

原文:離職後,對專案的記錄、總結(你的 star 是對我寫作的「正反饋」)。

一、寫這篇文章的目的

出於對自身職業發展上的規劃,我決定換一個工作環境,離開工作了三年的公司。

我並沒有急著找下家,而是決定裸辭。希望利用這段空檔期去總結過去三年的工作,並展望未來自身的發展。

過去的一年,我以「前端 Leader」的身份參與了一個交通出行專案的開發並上線了多款 APP 。雖然包括我在內前端只有三名開發人員,但在整個過程中,不管是在技術上還是團隊管理上,我都有很大的收穫。也是這一年管理團隊的經驗讓我養成了一個好習慣:「記錄、總結」。寫這篇文章的目的就是為了記錄這個專案架構的過程並總結經驗。

二、專案以及團隊背景

這是公司轉型的第一個專案,在敲定了專案技術選型之後我才被分配到了這個專案組,在此之前,我一直負責 WEB 端的開發,並沒有移動開發的經驗。

專案及團隊背景大致如下:

  • 我們需要開發一個公交出行 APP ,主要功能包括檢視實時公交資訊、離線掃碼上車、路徑規劃、線上購票和雲公交卡等功能,複雜度並不低。

  • 我們後續需要為多個城市開發功能上大同小異的 APP(在我離開公司時,已成功上線了衢州、賀州、舟山、天津和濟源這五所城市對應的 APP)。

  • 為了快速搶奪市場,我們決定使用 Ionic 1.x 進行開發,是一個 Hybrid APP,在後期還需要開發對應的小程式。

  • 我帶著兩個應屆的妹子負責前端的開發,另外還有三個後端同事和一個產品經理(老闆)。

起初,我們只是為衢州開發 APP 。由於時間非常趕,加上之前我並沒有足夠的專案架構和帶團隊的經驗,我們只是對 Ionic 腳手架提供的目錄結構做了簡單的修改後就進行了開發。

但隨著衢州 APP 的上線,我們陸續接到了賀州、天津等多個城市的開發任務。考慮到這幾個城市對應的產品在功能層面上的大同小異,我們決定將這幾個各城市的產品都整合到同一個專案目錄中,並對當時的專案進行一次幅度較大的「重構」。

下面就來大致講講我們做了那些事兒(由於是以記錄為主,並不會講的特別的細緻)。

三、擁抱 ES6: 提高開發體驗

在新的專案中,我們決定使用 ES6 進行開發。一是考慮到後期還要開發對應的小程式,我們希望將來的小程式能夠和當前的專案共用同一份核心程式碼(因為小程式也是使用 ES6 進行開發)。其次是 ES6 的諸多特性,如:class、箭頭函式、各個資料型別的擴充套件等等能夠極大的提高開發效率和體驗,我個人非常喜歡。

在試圖擁抱 ES6 的過程中,需要解決以下兩個問題:

  1. 搭建 ES6 的開發環境。

  2. 思考 AngularJS 與 ES6 的最佳實踐。

對於第一點,由於我業餘時間都有在關注業界主流框架的發展,在兩年前也有使用 VueJS 1.x 開發過一個完整的 部落格專案,當時就是使用 ES6 進行開發,所以很快就使用 webpack 搭建了 ES6 的開發環境。

而對於 AngularJS 與 ES6 的最佳實踐,我在網上查了許多的資料,發現了幾篇優秀的博文,例如:ES6 與 Angular 1.xAngular 1.x 和 ES6 的結合。專案中主要的調整就是基於 ES6 來改寫 AngularJS 中的那些概念,至於具體怎麼改寫,感興趣的可以閱讀上述兩篇博文。

四、專案分層:抽象業務模型

在重構之前的專案中,由於我個人對 MVC 的錯誤理解,我們將大量的業務邏輯都放在控制器中,導致控制層極度臃腫,程式碼的質量變的越來越差。

在我重新理解了 MVC 之後,藉著重構的機會,我們試圖通過抽象出核心的「業務模型」,對控制層進行「瘦身」。

對於業務模型的抽象,我認為需要清楚兩點:

  1. 抽象業務模型的初衷不是為了複用,而是為了方便管理。

  2. 希望業務模型中的程式碼能夠儘可能的隔離框架,最好都是「純」JS 。

其中,第一點我在 深入理解 MVC 中的 M 與 C 中有提及,在此不再贅述。

對於第二點,由於專案後續可能會遷移到 Ionic 3.x 或是 React Native ,隔離框架的業務模型會提高專案在技術棧層面的「靈活性」,可以減少遷移到其他技術棧的成本。

接著,我們就需要解決一個問題:在業務模型中,如何儘可能的隔離 AngularJS ?

在抽象業務模型的過程中,我們發現當中的程式碼主要是依賴了 AngularJS 的服務,其中包括 AngularJS 內建的服務、第三方庫的服務和專案自身的服務。對此,我們分別採取了以下幾種做法:

  1. 替換掉 AngularJS 的內建服務。比如用 axios 替換 $http 、Promise 替換 $q 等等。

  2. 如果業務模型的某些行為所依賴的服務很難被替換,就建立一個繼承該模型的服務,然後在該服務中重寫某些行為。

第一種做法很好理解,對於第二種做法,我來舉個例子。

假如:業務模型 A 中的行為 X 需要使用到 UI-Route 服務的 $state.go( ) 方法(路由跳轉的方法)。

基本思路:我們將核心邏輯依舊封裝在 a.model.js 中,然後建立一個繼承於 a.model.jsa.service.js ,並在其中重寫行為 X 。

偽碼如下:

/* a.model.js */
export class A {
  behaviorX(cb) {
    // do some logic
    new Promise((resolve, reject) => {
      cb();
    });
  }
}

/* a.service.js  */
import { A } from 'app/models';
class ServiceA extends A {
  construct($state) {
    Object.assign(this, { $state });
  }

  behaviorX() {
    super.behaviorX(() => {
      this.$state.go('routeX');
    });
  }
}

ServiceA.$inject = ['$state'];

export ServiceA;

/* angular.service.js */
import { ServiceA } from 'app/services';

angular.module('app.services', [])
  .service('serviceA', ServiceA);
複製程式碼

如此一來我們就能夠保證了業務模型的「純度」。

小結:抽象業務模型一是為了對控制層進行瘦身,提高程式碼質量,其次通過在業務模型中儘可能的隔離框架來較少專案對框架的耦合度,提高專案在技術棧層面的靈活度。

效果:由於團隊整體的開發能力並不高,我們並沒有很好的抽象出專案的業務模型,反而是增加了程式碼的複雜度,我們也就不對這部分做強制的要求了,但之後我還會繼續實踐。

五、抽象功能模組:實現功能的「即插即用」

前面提有到,我們的專案其實有一點特別:我們需要為不同的城市分別上線功能上大同小異的 APP ,其中有些城市的功能可能多點,有些可能少點。

考慮到程式碼複用的最大化,當城市 A 需要增加城市 B 的某一個功能時,我們希望這兩個城市能夠共用該功能的程式碼。對於兩個城市在同一功能上的差異則通過「繼承、重寫」來解決。簡單的說,我們需要實現專案在功能層面的「即插即用」。

為了解決這個問題,我們首先要對「功能模組」有自己的理解。

如果一個問題是由多個子問題組合而成,那麼這個問題的複雜度將大於分別考慮每個子問題時的複雜度之和。

基於這個結論,開發者樂於將一個複雜的應用拆分成多個功能模組。這樣做有以下兩點好處:

  1. 利於協同開發,每一個開發者只需專注於自己所負責的模組。

  2. 由於每一個功能模組間的低耦合度,當我們不需要某一個功能或是增加某一功能時,我們只需刪除或者增加該功能模組的宣告程式碼。

其實這裡的第二點就和我們專案的需求有點相近了。接下來就說說我們具體是怎麼做的。

不同的開發者基於不同的專案,對功能模組的應用都會有些許的差異,而這裡的差異主要體現在抽象功能模組的粒度上。

在我們的專案中,我們抽象出來的功能模組的粒度都比較大,每一個功能模組都對應著一個「大需求」,模組內部同時也會包含著多個「小需求」,但我們並沒有繼續進行拆分,因為我們發現這些小需求對所在功能模組的耦合度極高。

其次,我們將功能模組反應在了專案的目錄結構中,所有的功能模組都放在一個名為 module 的檔案中,每一個功能模組主要包含以下幾部分的程式碼:

  1. 模組下的各個檢視,包括檢視路由的定義、檢視對應的控制器、檢視的模板等。

  2. 模組下所有檢視有用到的私有的服務、元件等程式碼。

這裡要提一句,我們對服務、元件和指令等元素(方便起見,我將這些檔案型別同稱為元素)的管理。從能力上來講,這些元素是適用於全域性的(只要宣告瞭,整個專案都能呼叫),但是我還是決定根據它們的「通用程度」劃分成「全域性元素」和「私有元素」。這樣子做的好處有兩點:

  1. 方便管理。別的同事可不一定知道那個是全域性元件那個是私有元件。雖然這可以通過命名規範來區分,但是隨著元素越來越多,管理起來還是比較費勁的。

  2. 方便按需載入。為了提高 APP 的首屏載入速度,我們實現了 AngularJS 的按需載入。過程中我們發現,我們老是需要在多個模組中去宣告那些全域性元素。而當我們劃分了全域性元素和私有元素後,我們會在專案啟動時就去宣告全域性元素,在模組中則只需宣告對應的私有元素即可。

通過以上的描述可以看出,我們將所有理應屬於功能模組本身的程式碼都放在對應的資料夾內。當我們需要使用某一功能時,我們只需要在專案的入口檔案引入對應的檔案,並初始化對應的 AngularJS 模組。

如此一來,我們就便幾乎實現了專案中各個產品(APP)在功能層面的即插即用。

六、調整目錄結構:適應一個專案多個產品的場景

我們當時選擇重構專案最主要的目的就是整合多個城市的產品到同一個的專案中,而專案的目錄結構則是其中的關鍵。

image

上圖是我們專案大致的目錄結構。其中 ionic 目錄下包括了各個產品對應的 ionic 專案,這些專案都是通過 ionic 1.x 提供的腳手架建立的。而 scripts 目錄則是我們進行程式碼開發的目錄,我們通過 webpackscripts 下的程式碼打包到對應的 ionic 專案中。這裡,我主要談談 scripts 的目錄結構。

基於我們對「功能模組」的理解,我們將程式碼分成了 coreprojects 兩部分。

  • core 目錄:用於存放通用的功能模組以及這些功能模組有使用到的業務模型、AngularJS 元素和一些工具類。

  • projects 目錄:該目錄下的每一個資料夾都代表一個產品,這些資料夾中包含了產品的「入口檔案」以及一些產品的訂製的業務程式碼。

簡單的說,這樣劃分目錄結構的目的是:當我們在開發一個產品的某一功能時,我只需考慮該功能是不是在 core 中已經被開發過了,如果是,那就直接 import 到當前產品的入口檔案。如果不是,則考慮這個功能是否為一個通用功能,如果是,則把該功能的程式碼寫在 core 目錄下,否則寫在對應的產品目錄下。當然,還有很大的可能是:該功能在 core 中已經被開發過,但當前的這個產品和 core 的實現會有些許的「差異」,我們的做法是:繼承對應的功能模組,然後重寫那些「差異」,通過這種方法來儘可能的減少那些冗餘的程式碼。對應的流程圖如下:

work01

七、優化:提高產品效能及開發效率

由於人手不夠以及各個產品都希望儘快的上線,直到我離職時我們也沒有花太多的精力去考慮專案的優化以及產品的效能。這可能也是小公司在專案初期的無奈吧。

但在重構階段,我們還是做了些許優化,大致如下:

7.1 AngularJS 的按需載入

為了提高 APP 首屏載入的速度,我們基於 webpackui-routeocLazyLoad 實現了 AnuglarJS 的按需載入。

/* pageA.route.js */
function routeConfig($stateProvider) {
  $stateProvider.state('page-a', {
    url: '/page-a',
    // 按需載入檢視模板
    templateProvider: ['$q', ($q) => {
      return $q((resolve) => {
        require.ensure([], () => {
          resolve(require('./pageA.tmpl.html'));
        }, 'page-a');
      });
    }],
    // 按需載入當前檢視所依賴的模組
    resolve: {
      loadModule: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad) => {
        return $q((resolve) => {
          require.ensure([], () => {
            $ocLazyLoad.load({ name: require('./index').default.name });
            resolve();
          }, 'page-a');
        });
      }]
    }
  })  
}

export default angular.module('app.routes.pageA', [])
  .config(routeConfig);


/* index.js */
import PageAController from './pageA.controller';
import PageAService from './pageA.service';

export default angular.module('app.pages.pageA', [])
  .service('PageAService', PageaService)
  .controller('PageAController', PageAController);
複製程式碼

7.2 各個產品的自動化構建

由於我們的專案中包含了多個產品,因此我們需要一個高效的自動化構建版本的方式。

產品的構建主要分為兩部分:

  1. 打包 scripts 目錄下的 JS 程式碼到對應的 ionic 目錄中。

  2. ionic 資料夾下的對應產品目錄下執行: ionic build android && ionic build ios

整個自動化構建過程,我們使用了 webpackgulp 來實現。

7.3 專案開發的規範化

不知是因為團隊規模小還是其他的什麼原因,我們公司一直沒有一些硬性的開發規範來約束專案的程式碼。這使得我在重構同事程式碼的時候感到各種的「不適」。因此,我決定讓同事閱讀以下幾個開發規範,並在以後的開發中儘可能的遵守。

在具體的實施當中,我們使用 ESLint 對 JS 程式碼做了強制的約束,而其他的規範主要還是看開發者的自覺性。從後來的程式碼來看,程式碼的質量還是有明顯的提高,沒有以前那般雜亂了。

八、感悟

這是我第一次以前端 Leader 的身份來參與一個新專案的開發,見證了一個產品的從無到有,雖然各種加班(有一次竟加班到凌晨四點~),但過程中的確學習了蠻多的。除此之外,也意識到了自身的許多不足。

這裡,我主要有以下兩點「大」的感悟:

8.1 職業發展:方向以及Foundation(基本功)

在這個專案中,我所參與的工作其實並不只是「WEB 前端」而已。 由於公司當時並沒有 iOS 端和 Android 端的開發者,雖然專案是一個 Hybrid App ,但過程中還是需要寫蠻多 Native 的程式碼。

其中,我花了很多的時間學習在 iOS 開發。在整個 iOS 的學習過程中,我有了下面兩點感悟:

  1. 良好英文水平是成為一個優秀的開發者的前提。

  2. 要成為一名優秀的「軟體工程師」而不只是一名「WEB 前端工程師」。

眾所周知,要成為「卓越」,就必須有非常紮實的「Foundation」。與上述的感悟就對應了兩個 Foundation

  1. 英文能力:聽、說、讀和寫。

  2. 軟體開發基礎:包括資料結構、作業系統、計算機組成原理等等。

這可能有點「後知後覺」的感覺,因為這些都是以前讀書時該學的東西。不過不管怎麼樣,還是很慶幸自己找到了努力的方向。有了明確的方向,相信今後的路會變的愈加有趣。

8.2 許多的不足:路還很長

有時,對比那些野球場上打球的朋友和那些打職業比賽的球員,後者會顯得「職業」很多。

但是,雖說我現在所做的工作是我的「職業」,但我時常感覺自己並不夠「職業」,顯然非常的「業餘」。

這段時間我總結了下,發現兩點問題:

  1. 技術上,「踏實度」很低,容易產生「技術浮躁」,缺少對技術的「匠心」,基本功薄弱。

  2. 態度上,很難長期保持對公司、對產品的「責任感」。

對於前一點,其本質就是:「浮躁」。最好的解決辦法就是:瞄準方向,然後把眼光放長遠一點。我們總會感嘆要學的東西是在太多了,但如果我們把眼光放長遠一點,五年、十年、又或是二十年,或許我們就沒有那麼浮躁了。我認為浮躁的背後是一種急於求成,一種急功近利的表現,這樣的狀態顯然很難讓我們變的踏實。

而對於第二點,這就涉及到了自身對工作和對職業的「態度」了。我認為這是一個基本的「職業素養」,又或者可以理解為「職業原則」。就我個人而言,我的想法是:

在成為一名優秀的軟體工程師之前,我也希望,我是一名「優秀的員工」,能夠發揮自己最大的價值,僅此而已。

相關文章