如何在不同的專案中共用前端資源,告別複製貼上

function0831發表於2017-12-21

前言

隨著公司前端專案的增多, 大家會發現各個專案中很多資源都是是大同小異的,這就引發了一個話題,如何跨專案共用前端資源, 這裡的資源泛指前端涉及到的所有靜態資源, 常見的有 HTML/CSS/JS/圖片等等.

所謂共用前端資源, 就是將公共的前端資源提取出來, 例如公共樣式/公共邏輯/公共元件/公共圖片資源等等, 讓多個專案來引用, 避免複製多份, 避免重複開發, 統一管理和維護。只要更新公共資源,其他引用的專案就可以同步更新,提升開發效率,降低開發成本。

前端資源共用的障礙因素

藍圖是巨集偉的,但要實現藍圖又是艱辛的, 要實現藍圖, 我們首先要認清現實存在的一些障礙.

前端資源的共用涉及到以下幾個方面, 關鍵是該如何使用和如何更新的問題

  • 提取公共資源: 如何模組化? 模組化的粒度控制?
  • 存放/管理公共資源: 公共資源應該統一放置在哪裡? 如何管理依賴?
  • 公共資源的釋出更新: 如何釋出一個公共資源, 又如果更新版本?
  • 公共資源的多版本共存: 如何表明我需要公共資源的哪個版本?
  • 使用(引用)公共資源: 如何在專案中使用公共資源, 還要考慮到打包優化的問題?

前端資源是由多種不同種類的資源組成的, 而且依賴關係錯綜複雜, 涉及到不同的資源路徑, 引用方式是個頭痛的問題, 一直是造成前端資源不方便共用的一大阻礙. 而且前端早些時候連依賴包管理器(例如 Java 的 Maven)的概念都沒有, 所有的依賴都是手工的複製粘帖, 更新的時候再覆蓋一次. 新專案開始的時候, 又是複製粘帖一遍.

你還記得你複製過多少份 jquery.js 嗎? 你還弄得清各個專案所用的 jQuery 版本嗎? 如果全專案需要統一升級一下 jQuery 的版本, 你該怎麼做? 公共資源以複製多份的方式來共用, 勢必變得難以維護, 最終很有可能造成公共資源存在各種版本不統一.

JAVA程式設計師是否還記得當年 Java 沒有 Maven 來管理專案的時候, 各種 jar 依賴在多個專案中複製粘帖, 是不是深有感觸啊?

為了避免複製粘帖帶來的痛苦, 統一維護公共資源, 在先不考慮優化的情況下, 我們可以將公共資源細粒度釋出到 CDN 上.

那麼各個專案中, 我們就可以這樣使用了

<!-- 公共元件的 CSS -->
<link rel="stylesheet" href="//cdn.com/lib/component/component.css">
<!-- 公共靜態資源: 例如整個公司級需要共用的資源, 常見的就是公司 LOGO 了 -->
<img src="//cdn.com/lib/logo.png">
<!-- 公共元件的 JS -->
<script src="//cdn.com/lib/component/component.js"></script>
複製程式碼

但是, 如果公共資源更新了呢? 由於涉及到瀏覽器快取的問題, 你需要將各個專案引用到公共資源的地方全部搜尋出來, 然後一個個修改引用資源的時間戳, 強制讓瀏覽器快取失效.

另外別忘了, 公共資源中如果引用到其他資源的也需要如此操作一番, 例如 CSS 中引用到的圖片如果更新了的話如下:

<link rel="stylesheet" href="//cdn.com/lib/component/component.css?v1">
<img src="//cdn.com/lib/logo.png?v2">
<script src="//cdn.com/lib/component/component.js?v3"></script>
複製程式碼

但是, 如果公共資源需要多版本共存呢? 我們可以使用版本資料夾來區分, 引用的公共資源更新時, 就需要修改下版本號:

<link rel="stylesheet" href="//cdn.com/lib/component/1.1.0/component.css">
<img src="//cdn.com/lib/logo/1.2.0/logo.png">
<script src="//cdn.com/lib/component/1.1.0/component.js"></script>
複製程式碼

但具體哪個專案使用了哪個版本, 還是得全域性搜尋. 而且公共資源版本更新之後, 如何通知各個專案來更新呢? 難道是靠大聲呼喊嗎?

但是, 如果 CDN 域名要更換呢? 又得全域性搜尋一遍, 這種全域性搜尋的方式, 是痛苦而原始的, 專案不多還好處理, 一旦專案越來越多, 管理起來就會痛苦不堪. 你可能想到一些變通的方式, 來統一管理公共資源的引用, 例如藉助後端配置管理系統之類的東西, 將這些引用都配置起來.

但是, 如果引用的公共資源太多, 想優化請求的數量, 做資源的合併呢? 這沒法通過全域性搜尋的方式來解決了吧. 起初為了便於公共資源的按需使用, 我們細粒度規劃了公共資源, 勢必會增加頁面的請求數量, 要想做優化, 可以使用combo 機制 在服務端做資源的動態合併.

開發階段我們可以細粒度的引用公共資源

<link rel="stylesheet" href="//cdn.com/lib/component1/1.0.0/component1.css">
<link rel="stylesheet" href="//cdn.com/lib/component2/1.1.0/component2.css">
<link rel="stylesheet" href="//cdn.com/lib/component3/1.2.0/component3.css">
<link rel="stylesheet" href="//cdn.com/lib/component4/1.3.0/component4.css">
<img src="//cdn.com/lib/logo/1.0.0/logo.png">
<script src="//cdn.com/lib/component1/1.0.0/component1.js"></script>
<script src="//cdn.com/lib/component2/1.1.0/component2.js"></script>
<script src="//cdn.com/lib/component3/1.2.0/component3.js"></script>
<script src="//cdn.com/lib/component4/1.3.0/component4.js"></script>
複製程式碼

上線階段我們可以使用 combo 機制合併資源減少請求的數量, 以優化前端效能

<link rel="stylesheet" href="//cdn.com/combo?lib/component1/1.0.0/component1.css,lib/component2/1.1.0/component2.css,lib/component3/1.2.0/component3.css,lib/component4/1.3.0/component4.css">
<img src="//cdn.com/lib/logo/1.0.0/logo.png">
<script src="//cdn.com/combo?lib/component1/1.0.0/component1.js,lib/component2/1.1.0/component2.js,lib/component3/1.2.0/component3.js,lib/component4/1.3.0/component4.js"></script>
複製程式碼

這麼長的 URL, 就像老太婆的裹腳布又臭又長, 維護起來那是相當辣眼睛啊.

而且大家有沒有注意到, 公共元件的引用方式, 也有點麻煩, 是分兩步來走的, 先引用 CSS, 再引用 JS, 這個 CSS 明顯屬於元件的依賴. 那麼問題來了, 我使用一個元件, 為什麼還要關注它的依賴呢? 如果元件的 CSS 或者元件的 JS 再有別的依賴呢? 是不是也需要使用者自己來處理呢? 可見前端資源的引用方式, 最好能夠自動處理依賴關係的, 使用者只需要關注到他想用的資源(元件)這一層即可.

前端資源的依賴關係

要想簡化前端資源的引用方式, 就必須處理好前端資源的依賴關係, 就讓我們來好好地理一理前端資源的各種依賴關係吧.

首先頁面是所有資源引用的入口, 即依賴的起點(源頭), 依賴的種類主要分為兩種

  • 靜態依賴: 通過靜態分析程式碼, 就可以找出來的明確依賴關係, 可能涉及到多層依賴
  • 動態依賴: 程式碼在執行的過程中才會載入的依賴 下面的示例說明了前端可能存在的各種依賴關係
index.html                                      -- 頁面入口, 所有依賴的起點
├── <link rel="stylesheet" href="index.css">    -- CSS 依賴(可能又依賴其他 CSS 和其他靜態資源)
|   |── @import url(a.css);                     -- 引用資源的相對路徑是相對於當前 CSS 的路徑
|   |   |── .a {background: url(res/a.png)}     -- 依賴的資源又會牽涉出依賴的依賴
|   |── .index {background: url(res/index.png)}
|
├── <img src="res/foo.png">                     -- 靜態資源依賴
|
├── <script src="index.js"></script>            -- JS 預覽(可能(動態)依賴其他 CSS/JS 和其他靜態資源)
|   |── img.src = 'res/bar.png';                -- 引用資源的相對路徑是相對於當前 HTML 的路徑
|   |── link.href = 'b.css';
|   |   |── ...
|   |── js.src = 'a.js';
|   |   |── ...
複製程式碼

這麼多依賴關係, 該如何處理呢? 手工處理肯定不現實, 我們需要一個 module bundler(模組打包器) 來幫助我們分析並打包這些依賴.

面對這些重重的阻礙和困擾,要如何實現前端資源共用這藍圖呢?在資深技術總監龍總的啟發下,借鑑國內大牛的思路.......

實現方案

基於上面的分析結果, 要想共用前端資源, 必須先將公用的前端資源以包的形式統一地管理起來, 形成一個公共倉庫, 在專案中宣告依賴的前端資源包, 通過工具來下載依賴的具體資原始檔.

開發時使用模組打包器分析出具體的依賴檔案, 打包出專案依賴的所有前端資源.

因此我們需要一個包管理器和模組打包器, 即可實現夢寐以求地共用前端資源.

  • 包管理器: npm

npm is the package manager for JavaScript

manage dependencies in your projects

        管理專案級別的依賴, 必要時搭建一套npm 私服

webpack is a module bundler for modern JavaScript applications

it recursively builds a dependency graph that includes every module your application needs, then packages all of those modules into a small number of bundles - often only one - to be loaded by the browser.

構建工具分析並打包依賴, 現在你可以放心大膽地修改和刪除專案歷史遺留的靜態資原始檔了

  • 任何資源都可以視為一種依賴, 在構建時分析出資源的引用關係
  • 如果某個資源是殭屍資源, 刪除後不會引發構建失敗, 因為已經沒有任何檔案引用(依賴)它了
  • 在沒有構建工具分析依賴的年代,你只能通過全域性搜尋的方式來確定一個檔案還有沒有用但可能還是沒有辦法下達刪除的決定(萬一其他專案引用了呢?)

接下來, 共用前端資源的關鍵就是將公共的模組釋出到 npm 裡面, 然後就是在各個專案宣告依賴實際使用了.

實際使用的方式就落在了: 如何通過 webpack 在 HTML/CSS/JS 檔案中引用 npm 模組(即 node_modules 資料夾), 或者是 npm 模組中的檔案

  • HTML
<!-- 引用 npm 模組 ionicons 中的檔案 -->
<img src="~ionicons/dist/svg/ios-sunny.svg" width="50" height="50">
複製程式碼
  • CSS
/* 匯入 npm 模組: normalize.css, 會自動去找 main 檔案 */
@import url(~normalize.css);

.test-npm-res {
    /* 引用 npm 模組 ionicons 中的檔案 */
    background-image: url(~ionicons/dist/svg/ios-partly-sunny-outline.svg);
}
複製程式碼
  • JS:
// 匯入 npm 模組 ionicons 中的檔案
import svg from 'ionicons/dist/svg/ios-sunny-outline.svg';
複製程式碼

示例

我們只需要關注使用的模組, 不再需要關注模組的依賴了

  • 共用靜態資原始檔: 在各個專案中引用公共的公司 Logo 圖片

       將公共靜態資源在 npm 上釋出為一個 company-common-res 的模組包

company-common-res/
├── src/
|   |── logo.psd
|   └── ...
|
├── dist/
|   |── logo.png
|   └── ...
|
└── package.json
複製程式碼

       在專案的 package.json 中宣告依賴這個模組

"dependencies": {
  "company-common-res": "^1.0.0",
}
複製程式碼

       然後就是在 HTML/CSS/JS 檔案中引用這個靜態資源了, 以 HTML 檔案中為例

<img src="https://user-gold-cdn.xitu.io/2017/12/21/1607716e35b27686" width="50" height="50">
複製程式碼
  • 共用 CSS 元件: 在各個專案中引用公共的 CSS 樣式

       將公共的 CSS 基礎樣式在 npm 上釋出為一個 company-component-base 的模組包, 在專案的 package.json 中宣告依賴這個         模組

       company-component-base模組的 package.json 應該宣告 main 為元件的 CSS 檔案

"main": "company-component-base.css"
複製程式碼

       在專案中使用這個元件, 以 CSS 檔案中為例

@import url(~company-component-base);
複製程式碼

       將公共的 CSS 元件在 npm 上釋出為一個 company-component-button 的模組包

@import url(~company-component-button);
複製程式碼
  • 公共 JS 模組(純邏輯模組, 不包含樣式): 在各個專案中引用公共的 JS 工具方法

       將公共的 JS 工具方法在 npm 上釋出為一個 company-util 的模組包, 在專案的 package.json 中宣告依賴這個模組

import util from 'company-util';
util.log('test');
複製程式碼
  • 公共 JS 元件(包含樣式): 在各個專案中引用公共的 toast 元件

將 toast 元件在 npm 上釋出為一個 company-component-toast 的模組包, 在專案的 package.json 中宣告依賴這個模組

import Toast from 'company-component-toast';

new Toast({
    content: '提示內容'
}).show();
複製程式碼

總結

基於前端資源共用的障礙

  • 多種不同種類的資源
  • 資源的依賴關係複雜(HTML依賴/CSS依賴/JS依賴/圖片依賴/元件依賴)
  • 沒有包管理器來統一維護專案的依賴
  • 如何引用依賴以及打包

要實現前端資源共用, 我們需要一個包管理器模組打包器, 以解決上面的這些問題.

接下來的重點就是, 如何形成一個元件的平臺了, 讓大家可以方便的知道有哪些元件, 以及如何使用這些元件

  • 規範化的包名: 例如元件包名都以 component 開頭
  • 規範化的專案目錄結構
  • 元件文件和可展示的使用示例

相關文章