一名全棧工程師的技術實踐之路

一、前言

1.1 什麼是全棧

全棧開發是指開發人員掌握了前端、後端以及數據庫等多個領域的知識和技能,能夠獨立完成整個項目的開發工作。在需求交付過程中,可以負責從項目的前期分析、設計到後期開發、測試、發佈等整個過程,能夠快速定位和解決問題,提高開發效率和產品質量。

1.2 爲什麼做全棧

我認爲全棧的推進是環境變化、技術發展導致的必然結果,全棧帶來的好處主要有兩方面:

  • 降低溝通成本,提升交付效率:精細化分工導致的結果是協同成本大大增加,尤其是對於跨多個團隊的項目,每個開發可能找對接的同學都得找好幾個人才能找到,影響整體的需求交付效率。當下,由單人或單團隊完成需求的閉環開發,降低協同或許是提升產品交付效率的最快方式。
  • 從全局視角加深領域專業度、增強個人競爭力:首先,無論是前端技術、客戶端技術還是服務端技術,研發平臺、框架、規範都基本定型,學習成本降低;其次,通過學習全棧技術,可以增加技術視角的廣度,未來進行開發工作時,不再偏居一隅,可以從整個項目的角度出發,設計更合理的架構;最後,未來市場需要的也是全棧型開發同學,在《Stack Overflow 2021 全球8W名開發者調查報告》顯示排名前三分別是:全棧工程師(49.7%)、後端工程師(43.7%)、前端工程師(27.4%)。

二、全棧需求開發SOP

全棧技能不是無中生有,需要每個開發學習跨棧知識,並且不斷實踐,才能實現全棧開發、獨立交付需求。爲此,推行了一套比較標準的需求落地SOP,保證需求的交付質量。

2.1 需求分級

分級標準

分級流程

2.2 需求評估

對全棧需求的評估主要分爲以下四步:

  • 前端同學根據需求分級流程完成需求定級
  • 與專業前端協商後指定需求承接同學
  • 承接同學評估工時
  • 工時對焦及優化(專業前端、承接同學)

2.3 需求開發 & 發佈

需求開發 & 發佈將以一個全棧需求實踐爲例進行介紹,詳見第3章,主要可以分爲以下幾步:

  • 準備工作
  • 代碼開發
  • 本地調試
  • 發佈


2.4 需求質量保障

全棧推進需要建立在需求交付質量不變、效率提升的前提下才有意義,保障全棧需求的交付質量主要通過CR和上線追蹤的方式。

CR

  • 所有全棧研發的代碼由業務前端owner或師兄強制CR;
  • 發起CR時機,在完成項目開發和自測後,全棧同學需及時提交CR給師兄和業務前端owner;
  • 師兄和業務前端owner完成代碼review後,全棧同學按照要求進行代碼修復;

線上追蹤

  • 統計千行缺陷率、可自測發現率、線上問題數、預警量等指標,納入全棧交付質量統計;
  • 線上故障由業務前端owner和實際開發人共同處理,線上缺陷由全棧開發同學修復;

2.5 需求覆盤

全棧之路不可能一帆風順,中間總是會有各種磕磕絆絆,失敗是在所難免的,失敗不是成功之母,好的覆盤纔是,所以全棧路上,迭代覆盤必不可少。商家技術團隊每兩週會對全棧迭代進行一次覆盤,總結Highlight、Lowlight,以及後續的Action。

三、全棧需求實踐

3.1 背景

先知平臺服務於阿里巴巴國際站行業小二,通過全球趨勢洞察、站內供給分析,輸出品類策略,賦能行業小二品類規劃。本需求展示站外熱搜關鍵詞榜單,方便行業小二快速洞察站外趨勢,Demo如下:

3.2 準備工作

3.2.1 環境準備

進行前端開發工作前,需要準備本地編譯環境,按照我的理解,對前端的相關編譯工具類比到後端方便大家理解:

工具名稱 類型 類比後端工具 下載鏈接
Node 運行環境 JVM https://nodejs.org/en/download
npm 包管理器 Maven https://nodejs.org/zh-cn/download
VSCode 編譯器 IntelliJ Idea https://code.visualstudio.com/download
LightProxy 代理 -  

3.2.2 熟悉代碼

在正式開發之前,拉取前端項目代碼,學習項目代碼結構,遵循前端開發規範。

alita-xianzhi
    |--- hook                // 鉤子腳本
    |--- .eslintignore      // eslint格式校驗忽略文件
    |--- .eslintrc.js        // esliint格式配置文件
    |--- package.json        // 依賴包版本(類似pom.xml)
    |--- src                // 源代碼
          |-- common             // 定義項目常量,比如目錄,常用文字說明等
          |-- components         // 公共組件目錄
          |-- entry              // 跳轉頁
          |-- pages             // 頁面代碼,文件名與 URL 路徑相對應
          |-- service           // 服務,接口請求地址
          |-- utils             // 公用方法
          |-- index.jsx          // 主頁
          |-- index.scss        // 主頁樣式css
          |-- tab-config.js     // 路由配置

3.2.3 新建變更

每次前端開發都需要新建變更,最後形成一個獨立的版本號。在前端頁面展示時,通過版本號決定呈現的頁面版本。

3.3 代碼開發

3.3.1 模塊劃分

在正式開發代碼之前,先分析下頁面,理清開發思路,劃分待開發的組件,確定最終的代碼結構。頁面上方和左側的菜單欄爲通用組件,本次開發不需要修改。除此之外的頁面可以劃分爲兩大塊:

  1. 篩選欄:展示篩選項、搜索欄
  2. 詳情Tab:展示多個Tab,本次開發的關鍵詞榜單就是其中一個Tab頁面

由此,確定最終的組件結構,如下圖:

確定了組件結構,再結合項目目錄結構,不難確定代碼開發的位置:

  1. 篩選欄改造:改造query-header-container文件夾下的組件代碼
  2. 詳情Tab:在aaa文件夾下新增keyword-table文件夾,然後在裏面寫代碼
src                
 |-- pages         
       |-- outside-opportunity-discovery
               |-- components    // 站外機會發現組件
                       |-- query-header-container  // 篩選框組件
                             |-- index.jsx            // 篩選框組件主頁
                             |-- index.scss            // 篩選框組件主頁樣式
                       |-- aaa                  // aaa榜單
                             |-- keyword-table        // aaa關鍵詞榜單列表
                                     |-- index.jsx            // aaa關鍵詞榜單主頁
                                     |-- index.scss            // aaa關鍵詞榜單主頁樣式
                             |-- index.jsx            // aaa榜單主頁
                             |-- index.scss            // aaa榜單主頁樣式

3.3.2 篩選欄開發

原樣式

 

目標樣式

 

解決思路

可以看到,整個樣式幾乎是沒有變化的,區別是需要將“對比周期”隱藏,同時將“選擇行業”提到第一行,相對來說比較容易修改。只需要判斷當前選擇的Tab頁,然後確定渲染邏輯即可。React中支持根據條件判斷是否渲染某組件,只需要在條件表達式後跟&&,則表示若條件表達式成立,則渲染組件,否則不渲染。例如:

class QueryContainer extends Component {
  render(){
    return (
      {currentTab === 'keyword' && (
        <div>關鍵詞榜單篩選欄</div>
      )}
      {currentTab !== 'keyword' && (
        <div>品類榜單篩選欄</div>
      )}
    );
  }
}

上例中,相當於有兩個if條件語句,僞代碼如下:

if(currentTab == 'keyword'){
    展示關鍵詞榜單篩選欄
}
if(currentTab != 'keyword'){
    展示品類榜單篩選欄
}

同時,React也支持三目運算符,例如:

class QueryContainer extends Component {
  render(){
    return (
      {currentTab === 'keyword' ? (
        <div>關鍵詞榜單篩選欄</div>
      ) : (
        <div>品類榜單篩選欄</div>
      )}
    );
  }
}

通過以上邏輯判斷,可以實現對篩選欄的改造,根據不同Tab展示不同樣式。

3.3.3 詳情Tab開發

目標樣式

解決思路

整個列表可以被分爲兩部分:表頭和表數據。爲了保持整個站外商品庫榜單的風格統一,這裏沒有選擇直接使用Table組件,而是選擇了定製化程度更高的Grid柵格佈局,通過將頁面分爲10列多行,最後通過css調整表格的樣式。Grid柵格佈局支持按照行、列構建頁面,將頁面分爲多個網格,非常方便自定義頁面風格。例如:

class keywordTable extends Component {
  render(){
    return (
      <Row>
        <Col span="12" className="class1">第一列</Col>
        <Col span="12" className="class2">第二列</Col>
      </Row>
    );
  }
}

Grid柵格佈局將每一個行劃分爲24個小列,只需要指定span,即可按比例設置列的寬度。className可以輕鬆的定製每個網格的樣式。上例中,使用Grid柵格佈局渲染了一行,並將其平分爲兩列。

渲染表頭

表頭與表數據樣式不同,同時內容也是固定的,所以可以單獨渲染,我將其抽爲單獨的渲染函數,部分代碼如下:

class keywordTable extends Component {
  renderTableHeader(){
    return (
      <Row className="product-table-header">
        <Col className="header-keyword" span="3">
          關鍵詞
        </Col>
        <Col className="header-keyword-category" span="4">
          所屬類目
        </Col>
        <Col className={this.getTitleClassName('avgSold')} span="2">
          品均月銷量($)
          <Icon
            type="descending"
            size="xs"
            onClick={() => this.onSortChange('avgSold', 'avg_sold')}
          />
        </Col>
        ...
      <Row>
    );
  }
}

在每個網格內,可以任意的渲染組件,比如上例中我就在其中一個網格內嵌入了下降圖標的Icon。React還支持動態的拼接每個組件的className,上例中,我封裝了一個動態獲取className的方法,用於實現點擊排序後,排序字段高亮的樣式。

渲染表數據

表數據的每一行對前端來說是相同的,只需要遍歷表數據,一行一行的進行渲染即可,部分代碼如下:

class KeywordTable extends Component {
  renderTableKeyword(){
    return (
      <div className="keyword-table-container">
        {dataList.map((item = {}, index) => {
          return (
            <Row key={index} className="keyword-table-item">
              <Col className="item" span="3">
                {item.keyword}
              </Col>
              <Col className="item" span="4">
                {item.cateName}
              </Col>
              ...
            </Row>
          );
        })}
      </div>
    );
  }
}

上例中,首先遍歷數據列表中的每一個對象,在遍歷時,需要給對象設置默認值,避免對象爲null導致頁面渲染異常,然後取對象中的各屬性值並渲染到網格中。

組裝渲染函數

上面將數據列表拆分爲表頭和表數據進行渲染,最後需要將兩者放到組件的渲染函數中才會生效,代碼如下:

class KeywordTable extends Component {
  render(){
    return (
      <React.Fragment>
        {this.renderTableHeader()}
        {this.renderTableKeyword()}
      </React.Fragment>
    );
  }
}

3.3.4 組件通信開發

上文提到篩選欄的樣式需要在原有基礎上進行改造,根據用戶選擇的不同詳情Tab確定展示的樣式,但在渲染篩選欄組件時,還無法確定當前用戶選擇的是哪個Tab;同時,篩選欄選擇不同的篩選項並通過請求後端接口返回不同數據後,需要讓數據列表展示不同的數據,但拉取數據和展示數據是在兩個組件中完成的。這些地方就涉及到組件之間的通信,需要讓篩選欄與下方的詳情Tab交互,從而執行不同的渲染邏輯。類比後端,每個組件相當於一個類,組件渲染出來的頁面效果即是一個對象,想要實現兩個對象之間的通信,可以有兩種思路:

  1. 兩個對象之間本身存在包含關係,例如對象A包含對象B,這時候A可以輕鬆的獲取B的屬性;
  2. 兩個對象之間存在一個媒介,通過媒介進行交互,例如對象C同時包含對象A和對象B,這時候A和B可以通過C輕鬆交互;

這裏我使用了第2種思路,也就是父組件。內容頁面同時包含了篩選欄和詳情Tab兩個組件,這就是天然的媒介。父組件中維護兩個子組件需要通信的變量作爲屬性,子組件通過讀寫父組件中的屬性來完成交互。

子組件讀父組件屬性

  • state

Java中的類屬性可以直接定義在類中;與之相似,每個前端的組件也可以維護自己的屬性,區別在於,前端組件的屬性需要放在state中。例如:

class AAA extends Component {
  state = {
    currentTab: 'keyword',
    dataList: []
  }
  render(){
    const {currentTab} = this.state
    <QueryContainer currentTab={currentTab}/>
  }
}
  • props

在Java中,類的初始化使用構造器,解析構造器中的參數實例化對象;與之相似,前端組件在渲染時,可以通過解析傳入參數初始化,這個參數統一封裝到props中,可以通過this.props訪問。例如:

class QueryContainer extends Component {
  render() {
    const {currentTab} = this.props;
  }
}

通過以上方法,可以讓子組件隨時感知到父組件的屬性值變化,只要currentTab值改變,React框架會檢測到props值改變,從而觸發子組件重新渲染,調用render()函數,最終實現根據不同Tab渲染不同篩選欄的效果。

子組件寫父組件屬性

Java支持將函數作爲Function對象進行參數傳遞,與之相同,React框架也支持將函數作爲參數,通過props傳遞。如果子組件希望寫父組件屬性,只需要將父組件中寫state值的操作封裝爲函數,然後傳遞給子組件即可。例如:

class AAA extends Component {
  state = {
    currentTab: 'keyword',
    dataList: []
  }
  setDataList(newDataList){
    this.setState({newDataList});
  }
  render(){
    const {currentTab} = this.state
      <QueryContainer 
          currentTab={currentTab}
          setDataList={this.setDataList}/>
  }
}

class QueryContainer extends Component {
  onSearchBtnClick(){
    // 構造新的dataList
    newDataList = ...;
    this.props.setDataList(newDataList);
  }
}

3.4 本地調試

3.4.1 代理配置

前端代碼在開發完後可以在本地直接啓動、並調用服務端的預發接口,從而實現本地調試,步驟如下:

  1. 確定待代理的URL
  2. 使用LightProxy配置前端代碼Host綁定,例如:
^dev.g.alicdn.com/pagani-assets/alita-xianzhi/**/** file://Users/zhangpoxi/webStormProject/alita-xianzhi/build/$2

本例中的綁定將所有包含路徑http://dev.g.alicdn.com/pagani-assets/alita-xianzhi的http訪問請求代理到前端代碼編譯得到的文件夾中,實現本地調試的效果

3.使用LightProxy配置後端預發接口代理,例如:

127.0.0.1 future.alibaba-inc.com

本例中將所有訪問域名的請求路由到指定ip的機器上,實現本地訪問預發接口的效果

4.啓動LightProxy代理,如下圖,如果LightProxy左側配置項上有綠點,且菜單欄中的系統代理項前有小勾,則表示代理生效中

3.4.2 如何判斷代理是否生效

  1. 打開瀏覽器,打開“開發者工具”,Chrome瀏覽器快捷鍵F12
  2. 訪問代理的URL
  3. 搜索被代理的前端請求中的任意字符串,比如xianzhi
  4. 查看訪問的ip地址,如果是127.0.0.1則說明代理生效

3.5 發佈

用戶看到的頁面本質上將就是文件,所以前端代碼打包完成後都會推送到CDN,通過版本號確定需要拉取的文件,如圖所示主要分爲以下幾步:

  1. 開發同學將前端代碼打包並推送到CDN上;
  2. 用戶訪問服務器,服務器確定當前使用的版本號;
  3. 服務器向CDN請求某版本號的前端代碼包;
  4. CDN找到對應的前端代碼並返回給服務器;
  5. 服務器向用戶響應頁面文件。

3.6 常見問題

3.6.1 npm安裝依賴失敗

現象

原因

npm本質上是一個類似於Maven的包管理器,如上報錯是因爲項目依賴版本出現衝突,導致npm不知道使用哪個版本,這時候可以直接強制安裝,使用package.json中配置的版本即可

解決辦法

使用命令行npm install --force替換tnpm install

3.6.2 代理不生效

現象訪問頁面時本地修改代碼不生效原因可能導致該現象的原因較多,比如:

  • LigthProxy代理配置錯誤
  • LigthProxy代理失敗或未啓動
  • 頁面緩存導致代理不生效

解決辦法

  • 檢查本地項目啓動是否成功
  • 訪問路徑是否正確
  • LightProxy開啓是否成功
  • LightProxy中配置的本地資源路徑是否正確
  • 清空緩存,重啓LightProxy,多刷新幾次試試

四、全棧開發心得

  • 培養全局視角,加深領域專業度
    • 在傳統的需求迭代交付中,開發總是站在某一個視角看整個業務,對業務整體框架缺乏認知。通過全棧交付,爲開發同學提供更高的維度、更全局的視角,這樣可以更好地理解業務模型、業務流程和不同模塊之間的關係,從而更好地把握業務的本質和目標,提升領域理解,加深領域專業度,更快成爲某個領域的專家。
  • 降低溝通成本,提升交付效率
    • 溝通成本在迭代需求交付過程中已經成爲不可忽視的一部分,尤其是跨團隊的需求對接,接口溝通、聯調將會花費大量時間。通過全棧進行需求交付,省去了多人之間的溝通成本,讓開發專注在需求上,而不是與外部溝通上。全棧交付也可以在一定程度上避免返工問題,比如在定義接口的時候,有些細節沒考慮清楚,導致出入參的結構存在一些問題。如果是非全棧需求,需要重新跟前端同學對接,針對接口是否合理的討論可能就會花費1個小時,然後前段再重新開發頁面,浪費大量時間。而如果是全棧需求,我可以直接修改接口,不需要跟前端同學再去交流一遍,前端頁面也可以在後端接口確認之後再開始搭建,不存在返工問題。
  • 拓展技術廣度,增強個人競爭力
    • 當下,AI能力越來越強,降低了跨棧開發的門檻,人人都可以藉助AI能力,進行跨棧開發,我們在深挖專業技能的同時,技術廣度也是不可獲取的一部分,全棧能力必然是大勢所趨。目前先知的全棧實踐侷限在前端、後端,未來的全棧可以繼續向數據開發、算法等方向發展,一人成軍,讓迭代需求交付沒有卡點。未來的技術發展需要擁有全棧能力的人才能夠更好地適應,跨界合作將成爲趨勢。因此,通過拓展技術廣度,不僅可以增強個人競爭力,還能夠在職場中獲得更多的機會和挑戰,實現自我價值的最大化。

作者 | 慕欽

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章