模組化是一種處理複雜系統分解成為更好的可管理模組的方式,它可以把系統程式碼劃分為一系列職責單一,高度解耦且可替換的模組,系統中某一部分的變化將如何影響其它部分就會變得顯而易見,系統的可維護性更加簡單易得。
前端開發領域(JavaScript、CSS、Template)並沒有為開發者們提供以一種簡潔、有條理地的方式來管理模組的方法。CommonJS(致力於設計、規劃並標準化 JavaScript API)的誕生開啟了“ JavaScript 模組化的時代”。CommonJS 的模組提案為在伺服器端的 JavaScript 模組化做出了很大的貢獻,但是在瀏覽器下的 JavaScript 模組應用很有限。隨之而來又誕生了其它前端領域的模組化方案,像 requireJS、SeaJS 等,然而這些模組化方案並不是十分適用 ,並沒有從根本上解決模組化的問題。
前端模組化並不等於 JavaScript 模組化
前端開發相對其他語言來說比較特殊,因為我們實現一個頁面功能總是需要 JavaScript、CSS 和 Template 三種語言相互組織才行,如果一個功能僅僅只有 JavaScript 實現了模組化,CSS 和 Template 還是處於原始狀態,那我們呼叫這個功能的時候並不能完全通過模組化的方式,那麼這樣的模組化方案並不是完整的,所以我們真正需要的是一種可以將 JavaScript、CSS 和 Template 同時都考慮進去的模組化方案,而非僅僅 JavaScript 模組化方案。
JavaScript 模組化並不等於非同步模組化
主流的 JavaScript 模組化方案都使用“非同步模組定義”的方式,這種方式給開發帶來了極大的不便,所有的同步程式碼都需要修改為非同步的方式,我們是否可以在前端開發中使用“ CommonJS ”的方式,開發者可以使用自然、容易理解的模組定義和呼叫方式,不需要關注模組是否非同步,不需要改變開發者的開發行為。
前端模組化帶來的效能問題
很多主流的模組化解決方案通過 JavaScript 執行時來支援“匿名閉包”、“依賴分析”和“模組載入”等功能,例如“依賴分析”需要在 JavaScript 執行時通過正則匹配到模組的依賴關係,然後順著依賴鏈(也就是順著模組宣告的依賴層層進入,直到沒有依賴為止)把所有需要載入的模組按順序一一載入完畢,當模組很多、依賴關係複雜的情況下會嚴重影響頁面效能。
模組化為打包部署帶來的極大不便
傳統的模組化方案更多的考慮是如何將程式碼進行拆分,但是當我們部署上線的時候需要將靜態資源進行合併(打包),這個時候會發現困難重重,每個檔案裡只能有一個模組,因為模組使用的是“匿名定義”,經過一番研究,我們會發現一些解決方案,無論是“ combo 外掛”還是“ flush 外掛”,都需要我們修改模組化呼叫的程式碼,這無疑是雪上加霜,開發者不僅僅需要在本地開發關注模組化的拆分,在呼叫的時候還需要關注在一個請求裡面載入哪些模組比較合適,模組化的初衷是為了提高開發效率、降低維護成本,但我們發現這樣的模組化方案實際上並沒有降低維護成本,某種程度上來說使得整個專案更加複雜了。
一體化的前端模組化實踐方案
寫到這裡,其實我們的“前端工程之塊化”才正式開始,本文面向對前端模組化開發有所實踐或有所研究的同學,接下來我們所介紹的前端模組化解決方案, 有別於 JavaScript 模組化方案或 CSS 模組化方案,它是一種可以綜合處理前端各種資源的模組化方案;它可以極大提升開發者的開發體驗,併為效能優化提供良好的支援。下面讓我們來進一步來了解什麼是“一體化”的模組化實踐方案。
首先我們來看一下一個 web 專案是如何通過“一體化”的模組化方案來劃分目錄結構:
- 站點(site):一般指能獨立提供服務,具有單獨二級域名的產品線。如旅遊產品線或者特大站點的子站點(lv.baidu.com)。
- 子系統(module):具有較清晰業務邏輯關係的功能業務集合,一般也叫系統子模組,多個子系統構成一個站點。子系統(module)包括兩類: common 子系統, 為其他業務子系統提供規範、資源複用的通用模組;業務子系統:,根據業務、URI 等將站點進行劃分的子系統站點。
- 頁面(page): 具有獨立 URL 的輸出內容,多個頁面一般可組成子系統。
- 模組(widget):能獨立提供功能且能夠複用的模組化程式碼,根據複用的方式不同分為 Template 模組、JS 模組、CSS 模組三種型別。
- 靜態資源(static):非模組化資源目錄,包括模板頁面引用的靜態資源和其他靜態資源(favicon,crossdomain.xml 等)。
前端模組(widget),是能獨立提供功能且能夠複用的模組化程式碼,根據複用的方式不同分為 Template 模組、JS 模組、CSS 模組三種型別,CSS 元件,一般來說,CSS 模組是最簡單的模組,它只涉及 CSS 程式碼與 HTML 程式碼; JS 模組,稍為複雜,涉及 JS 程式碼,CSS 程式碼和 HTML 程式碼。一般,JS 元件可以封裝 CSS 元件的程式碼; Template 模組,涉及程式碼最多,可以綜合處理 HTML、JavaScript、CSS 等各種模組化資源,一般情況,Template 會將 JS 資源封裝成私有 JS 模組、CSS 資源封裝成自己的私有 CSS 模組。下面我們來一一介紹這幾種模組的模組化方案。
模板模組
我們可以將任何一段可複用的模板程式碼放到一個 smarty 檔案中,這樣就可以定義一個模板模組。在 widget 目錄下的 smarty 模板(本文僅以 Smarty 模板為例)即為模板模組,例如 common 子系統的 widget/nav/ 目錄
├── nav.css
├── nav.js
└── nav.tpl
下 nav.tpl 內容如下:
<nav id="nav" class="navigation" role="navigation">
<ul>
<%foreach $data as $doc%>
<li class="active">
<a href="#section-{$doc@index}">
<i class="icon-{$doc.icon} icon-white"></i><span>{$doc.title}</span>
</a>
</li>
<%/foreach%>
</ul>
</nav>
然後,我們只需要一行程式碼就可以呼叫這個包含 smarty、JS、CSS 資源的模板模組,
// 呼叫模組的路徑為 子系統名稱:模板在 widget 目錄下的路勁
{widget name="common:widget/nav/nav.tpl" }
這個模板模組(nav)目錄下有與模板同名的 JS、CSS 檔案,在模板被執行渲染時這些資源會被自動載入。如上所示,定義 template 模組的時候,只需要將 template 所依賴的 JS 模組、CSS 模組存放在同一目錄(預設 JavaScript 模組、CSS 模組與 Template 模組同名)下即可,呼叫者呼叫 Template 模組只需要寫一行程式碼即可,不需要關注所呼叫的 template 模組所依賴的靜態資源,模板模組會幫助我們自動處理依賴關係以及資源載入。
JavaScript 模組
上面我們介紹了一個模板模組是如何定義、呼叫以及處理依賴的,接下來我們來介紹一下模板模組所依賴的 JavaScript 模組是如何來處理模組互動的。我們可以將任何一段可複用的 JavaScript 程式碼放到一個 JS 檔案中,這樣就可以定義為一個 JavaScript 型別的模組,我們無須關心“ define ”閉包的問題,我們可以獲得“ CommonJS ”一樣的開發體驗,下面是 nav.js 中的原始碼.
// common/widget/nav/nav.js
var $ = require('common:widget/jquery/jquery.js');
exports.init = function() {
...
};
我們可以通過 require、require.async 的方式在任何一個地方(包括 html、JavaScript 模組內部)來呼叫我們需要的 JavaScript 型別模組,require 提供的是一種類似於後端語言的同步呼叫方式,呼叫的時候預設所需要的模組都已經載入完成,解決方案會負責完成靜態資源的載入。require.async 提供的是一種非同步載入方式,主要用來滿足“按需載入”的場景,在 require.async 被執行的時候才去載入所需要的模組,當模組載入回來會執行相應的回撥函式,語法如下:
// 模組名: 檔案所在 widget 中路徑
require.async(["common:widget/menu/menu.js"], function( menu ) {
menu.init();
});
一般 require 用於處理頁面首屏所需要的模組,require.async 用於處理首屏外的按需模組。
CSS 模組
在模板模組中以及 JS 模組中對應同名的 CSS 模組會自動與模板模組、JS 模組新增依賴關係,進行載入管理,使用者不需要顯示進行呼叫載入。那麼如何在一個 CSS 模組中宣告對另一個 CSS 模組的依賴關係呢,我們可以通過在註釋中的@require 欄位標記的依賴關係,這些分析處理對 html 的 style 標籤內容同樣有效,
/**
* demo.css
* @require reset.css
*/
非模組化資源
在實際開發過程中可能存在一些不適合做模組化的靜態資源,那麼我們依然可以通過宣告依賴關係來託管給靜態資源管理系統來統一管理和載入,
{require name="home:static/index/index.css" }
如果通過如上語法可以在頁面宣告對一個非模組化資源的依賴,在頁面執行時可以自動載入相關資源。
專案例項
下面我們來看一下在一個實際專案中,如果在通過頁面來呼叫各種型別的 widget,首先是目錄結構:
├── common
│ ├── fis-conf.js
│ ├── page
│ ├── plugin
│ ├── static
│ └── widget
└── photo
├── fis-conf.js
├── output
├── page
├── static
├── test
└── widget
我們有兩個子系統,一個 common 子系統(用作通用),一個業務子系統,page 目錄用來存放頁面,widget 目錄用來存放各種型別的模組,static 用於存放非模組化的靜態資源,首先我們來看一下 photo/page/index.tpl 頁面的原始碼,
{extends file="common/page/layout/layout.tpl"}
{block name="main"}
{require name="photo:static/index/index.css"}
{require name="photo:static/index/index.js"}
<h3>demo 1</h3>
<button id="btn">Button</button>
{script type="text/javascript"}
// 同步呼叫 jquery
var $ = require('common:widget/jquery/jquery.js');
$('#btn').click(function() {
// 非同步呼叫 respClick 模組
require.async(['/widget/ui/respClick/respClick.js'], function() {
respClick.hello();
});
});
{/script}
// 呼叫 renderBox 模組
{widget name="photo:widget/renderBox/renderBox.tpl"}
{/block}
第一處程式碼是對非模組化資源的呼叫方式;第二處是用 require 的方式呼叫一個 JavaScript 模組;第三處是通過 require.async 通過非同步的方式來呼叫一個 JavaScript 模組;最後一處是通過 widget 語法來呼叫一個模板模組。 respclick 模組的原始碼如下:
exports.hello = function() {
alert('hello world');
};
renderBox 模板模組的目錄結構如下:
└── widget
└── renderBox
├── renderBox.css
├── renderBox.js
├── renderBox.tpl
└── shell.jpeg
雖然 renderBox 下面包括 renderBox.js、renderBox.js、renderBox.tpl 等多種模組,我們再呼叫的時候只需要一行程式碼就可以了,並不需要關注內部的依賴,以及各種模組的初始化問題。
模組化基礎架構
總體架構
為了實現一種自然、便捷、高效能、一體化的模組化方案,我們需要解決以下一些問題,
- 模組靜態資源管理,一般模組總會包含 JavaScript、CSS 等其他靜態資源,需要記錄與管理這些靜態資源
- 模組依賴關係處理,模組間存在各種依賴關係,在載入模組的時候需要處理好這些依賴關係
- 模組載入,在模組初始化之前需要將模組的靜態資源以及所依賴的模組載入並準備好
- 模組沙箱(模組閉包),在 JavaScript 模組中我們需要自動對模組新增閉包用於解決作用域問題
** 使用編譯工具來管理模組 **
我們可以通過編譯工具(自動化工具) 對模組進行編譯處理,包括對靜態資源進行預處理(對 JavaScript 模組新增閉包、對 CSS 進行 LESS 預處理等)、記錄每個靜態資源的部署路徑以及依賴關係並生成資源表(resource map)。我們可以通過編譯工具來託管所有的靜態資源,這樣可以幫我們解決模組靜態資源管理、模組依賴關係、模組沙箱問題。
** 使用靜態資源載入框架來載入模組 **
那麼如何解決模組載入問題,我們可以通過靜態資源載入框架來解決,主要包含前端模組載入框架,用於 JavaScript 模組化支援,控制資源的非同步載入。後端模組化框架,用於解決 JavaScript 同步載入、CSS 和模板等模組資源的載入,靜態資源載入框架可以用於對頁面進行持續的自適應的前端效能優化,自動對頁面的不同情況投遞不同的資源載入方案,幫助開發者管理靜態資源,抹平本地開發到部署上線的效能溝壑。 編譯工具和靜態資源載入框架的流程圖如下:
編譯工具
自動化工具會掃描目錄下的模組進行編譯處理並輸出產出檔案:
靜態資源,經過編譯處理過的 JavaScript、CSS、Image 等檔案,部署在 CDN 伺服器自動新增閉包,我們希望工程師在開發 JavaScript 模組的時候不需要關心” define ”閉包的事情,所以採用工具自動幫工程師新增閉包支援,例如如上定義的 nav.js 模組在經過自動化工具處理後變成如下,
define('common:widget/nav/nav.js', function( require, exports, module ) {
// common/widget/nav/nav.js
var $ = require('common:widget/jquery/jquery.js');
exports.init = function() {
...
};
});
模板檔案,經過編譯處理過的 smarty 檔案,自動部署在模板伺服器
資源表,記錄每個靜態資源的部署路徑以及依賴關係,用於靜態資源載入框架 靜態資源載入框架(SR Management System)會載入 source maps 拿到頁面所需要的所有模組以及靜態資源的 url,然後組織資源輸出最終頁面。
靜態資源載入框架
下面我們會詳細講解如何載入模組,如下所示,
在流程開始前我們需要準備兩個資料結構:
- uris = [],陣列,順序存放要輸出資源的 uri
- has = {},hash 表,存放已收集的靜態資源,防止重複載入
-
載入資源表(resource map):
javascript { "res": { "A/A.tpl": { "uri": "/templates/A.tpl", "deps": ["A/A.css"] }, "A/A.css": { "uri": "/static/css/A_7defa41.css" }, "B/B.tpl": { "uri": "/templates/B.tpl", "deps": ["B/B.css"] }, "B/B.css": { "uri": "/static/css/B_33c5143.css" }, "C/C.tpl": { "uri": "/templates/C.tpl", "deps": ["C/C.css"] }, "C/C.css": { "uri": "/static/css/C_6a59c31.css" } } }
- 執行 {widget name=”A”}
- 在表中查詢 id 為 A/A.tpl 的資源,取得它的資源路徑 /template/A.tpl,記為 tpl_path,載入並渲染 tpl_path 所指向的模板檔案,即 /template/A.tpl,並輸出它的 html 內容
- 檢視 A/A.tpl 資源的 deps 屬性,發現它依賴資源 A/A.css,在表中查詢 id 為 A/A.css 的資源,取得它的資源路徑為 /static/css/A7defa41.css_,存入 uris 陣列 中,並在 has 表 裡標記已載入 A/A.css 資源,我們得到:
```javascript urls = [ ‘/static/css/A_7defa41.css’ ];
has = { “A/A.css”: true } ```
-
依次執行 {widget name=”B”}、{widget name=”c”},步驟與上述步驟 3 相同,得到,
```javascript urls = [ ‘/static/css/A_7defa41.css’, ‘/static/css/B_33c5143.css’, ‘/static/css/C_6a59c31.css’ ];
has = { “A/A.css”: true, “B/B.css”: true, “C/C.css”: true }
```
-
在要輸出的 html 前面,我們讀取 uris 陣列的資料,生成靜態資源外鏈,我們得到最終的 html 結果:
```html
html of Ahtml of Bhtml of C``` 上面講的是對模板和 CSS 資源的載入,用於描述靜態資源載入的流程,下面我們再來詳細講解下對於 JavaScript 模組的處理,要想在前端實現類似“ commonJS ”一樣的模組化開發體驗需要前端模組化框架和後端模組化框架一起作用來實現,
前端模組化框架,原理上大家可以選擇使用 requireJS 或 SeaJS 來作為模組化支援,但是我們並不建議這麼做,我們建議大家使用一個 mininal AMD API,例如 requireJS 的 almond 版本或者其他的精簡版本,requireJS 完整版有 2000 餘行,而精簡版模組化框架只需要 100 行程式碼左右就可以實現,只需要實現以下功能:
- 模組定義,只需要實現如下介面 define (id, factory),因為 define 閉包是工具生成,所以我們不需要考慮匿名閉包的實現,同時也不需要考慮“依賴前置”的支援,我們只需要支援一種最簡單直接的模組化定義即可
- 模組同步呼叫,require (id),靜態資源管理系統會保證所需的模組都已預先載入,因此 require 可以立即返回該模組
- 模組非同步呼叫,考慮到有些模組無需再啟動時載入,因此我們需要提供一個可以在執行時載入模組的介面 require.async (names, callback),names 可以是一個 id,或者是陣列形式的 id 列表。當所有都載入都完成時,callback 被呼叫,names 對應的模組例項將依次傳入。
- 模組自執行,即 AMD 規範的提前執行,之所選擇這樣做的原因是考慮到 Template 模組的特殊性,一般 Template 模組都會依賴 JavaScript 模組來做初始化工作,選擇模組自執行的方式我們就不需要顯式的在 Template 頁面上書寫 require 依賴,靜態資源系統會自動載入 Template 模組的依賴,當模組並行載入結束後會一次自執行。大家可能會認為如果頁面存在一些用不到的模組那都自執行豈不會浪費資源,這裡大家可以不用擔心,靜態資源系統投放到前端的模組都是頁面初始化所需要的,不存在浪費資源的情況。
-
Resource map 前端支援,主要用於為非同步模組呼叫提供 uri 支援,resourceMap 為靜態資源管理系統自動生成,無需人工呼叫,用於查詢一個非同步模組的真正 url,用於自動處理非同步模組的 CDN、資源打包合併、強快取問題,格式如下,
javascript require.resourceMap({ "res": { "common:widget/sidebar/sidebar.async.js": { "url": "/static/common/widget/sidebar/sidebar.async_449e169.js" } } });
-
處理迴圈引用,參照 nodeJS 處理迴圈引用的方式,在造成迴圈依賴的 require 之前把需要的東西 exports 出去,例如
```javascript // a.js console.log(‘a string’); exports.done = false; var b = require(‘./b.js’); console.log(‘in a, b.done = ‘ + b.done); exorts.done = true; console.log(‘b done’);
// b.js console.log(‘b starting’); exports.done = false;
var a = require(‘./a.js’); console.log(‘in b, a.done = ‘ + a.done); exports.done = true; console.log(‘b done’);
// main.js console.log(‘main starting’); var a = require(‘./a.js’); var b = require(‘./b.js’); console.log(‘in main. a.done = ‘ + a.done + ‘, b.done = ‘ + b.done); ```
如果在載入 a 的過程中,有其他的程式碼(假設為 b)require a.js 的話,那麼 b 可以從 cache 中直接取到 a 的 module,從而不會引起重複載入的死迴圈。但帶來的代價就是在 load 過程中,b 看到的是不完整的 a。
後端模組載入框架,主要用於處理模組的依賴並生成模組靜態資源外鏈,下面我們將以例項講解靜態資源管理系統是如何對 JavaScript 模組進行載入的,如下我們有一個 sidebar 模組,目錄下有如下資源
├── sidebar.async.js
├── sidebar.css
├── sidebar.js
└── sidebar.tpl
sidebar.tpl 中的內容如下,
<a id="btn-navbar" class="btn-navbar">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
{script}
$('a.btn-navbar').click(function() {
require.async('./sidebar.async.js', function( sidebar ) {
sidebar.run();
});
});
{/script}
對專案編譯後,自動化工具會分析模組的依賴關係,並生成 map.json,如下
"common:widget/sidebar/sidebar.tpl": {
"uri": "common/widget/sidebsr/sidebar.tpl",
"type": "tpl",
"extras": {
"async": [
"common:widget/sidebar/sidebar.async.js"
]
},
"deps": [
"common:widget/sidebar/sidebar.js",
"common:widget/sidebar/sidebar.css"
]
}
在 sidebar 模組被呼叫後,靜態資源管理系統通過查詢 map.json 可以得知,當前 sidebar 模組同步依賴 sidebar.js、sidebar.css,非同步依賴 sdebar.async.js,在要輸出的 html 前面,我們讀取 uris 陣列的資料,生成靜態資源外鏈,我們得到最終的 html
<script type="text/javascript">
require.resourceMap({
"res": {
"common:widget/sidebar/sidebar.async.js": {
"url": "/satic/common/widget/sidebar/sidebar.async_449e169.js"
}
}
});
</script>
<script type="text/javascript" src="/static/common/widget/sidebar/sidebar_$12cd4.js"></script>
如上可見,後端模組化框架將同步模組的 script url 統一生成到頁面底部,將 css url 統一生成在 head 中,對於非同步模組(require.async)註冊 resourceMap 程式碼,框架會通過{script}標籤收集到頁面所有 script,統一管理並按順序輸出 script 到相應位置。
自適應的效能優化
現在,當我們想對模組進行打包,該如何處理呢,我們首先使用一個 pack 配置項(下面是 fis 的打包配置項),對網站的靜態資源進行打包,配置檔案大致為,
fis.config.merge({
pack: {
'pkg/aio.css': '**.css'
}
});
我們編譯專案看一下產出的 map.json(resource map),有何變化,
{
"res": {
"A/A.tpl": {
"uri": "/template/A.tpl",
"deps": ["A/A.css"]
},
"A/A.css": {
"uri": "/static/csss/A_7defa41.css",
"pkg": "p0"
},
"B/B.tpl": {
"uri": "/template/B.tpl",
"deps": ["B/B.css"]
},
"B/B.css": {
"uri": "/static/csss/B_33c5143.css",
"pkg": "p0"
},
"C/C.tpl": {
"uri": "/template/C.tpl",
"deps": ["C/C.css"]
},
"C/C.css": {
"uri": "/static/csss/C_ba59c31.css",
"pkg": "p0"
},
},
"pkg": {
"p0": {
"uri": "/static/pkg/aio_0cb4a19.css",
"has": ["A/A.css", "B/B.css", "C/C.css"]
}
}
}
大家注意到了麼,表裡多了一張 pkg 表,所有被打包的資源會有一個 pkg 屬性 指向該表中的資源,而這個資源,正是我們配置的打包策略。這樣靜態資源管理系統在表中查詢 id 為 A/A.css 的資源,我們發現該資源有 pkg 屬性,表明它被備份在了一個打包檔案中。
我們使用它的 pkg 屬性值 p0 作為 key,在 pkg 表裡讀取資訊,取的這個包的資源路徑為 /static/pkg/aio0cb4a19.css_ 存入 uris 陣列 中將 p0 包的 has 屬性所宣告的資源加入到 has 表,在要輸出的 html 前面,我們讀取 uris 陣列 的資料,生成靜態資源外鏈,我們得到最終的 html 結果:
<html>
<link href="/static/pkg/aio_0cb4a19.css">
<div>html of A</div>
<div>html of B</div>
<div>html of C</div>
</html>
靜態資源管理系統可以十分靈活的適應各種效能優化場景,我們還可以統計 {widget} 外掛的呼叫情況,然後自動生成最優的打包配置,讓網站可以自適應優化,這樣工程師不用關心資源在哪,怎麼來的,怎麼沒的,所有資源定位的事情,都交給靜態資源管理系統就好了。靜態資源路徑都帶 md5 戳,這個值只跟內容有關,靜態資源伺服器從此可以放心開啟強快取了!還能實現靜態資源的分級釋出,輕鬆回滾!我們還可以繼續研究,比如根據國際化、皮膚,終端等資訊約定一種資源路徑規範,當後端適配到特定地區、特定機型的訪問時,靜態資源管理系統幫你送達不同的資源給不同的使用者。說到這裡,大家應該比較清楚整個“一體化”的模組化解決方案了,有人可能會問,這樣做豈不是增加了後端效能開銷?對於這個問題,我們實踐過的經驗是,這非常值得!其實這個後端開銷很少,演算法非常簡單直白,但他所換來的前端工程化水平提高非常大!
總結
本文是 fis 前端工程系列文章中的一部分,其實在前端開發工程管理領域還有很多細節值得探索和挖掘,提升前端團隊生產力水平並不是一句空話,它需要我們能對前端開發及程式碼執行有更深刻的認識,對效能優化原則有更細緻的分析與研究。fis 團隊一直致力於從架構而非經驗的角度實現效能優化原則,解決前端工程師開發、除錯、部署中遇到的工程問題,提供元件化框架,提高程式碼複用率,提供開發工具集,提升工程師的開發效率。在前端工業化開發的所有環節均有可節省的人力成本,這些成本非常可觀,相信現在很多大型網際網路公司也都有了這樣的共識。
本文只是將這個領域中很小的一部分知識的展開討論,拋磚引玉,希望能為業界相關領域的工作者提供一些不一樣的思路。歡迎關注fis專案,對本文有任何意見或建議都可以在fis開源專案中進行反饋和討論。