子產品化開發
1. 子產品化開發概述
子產品化開發目前是當下最重要的前端開發範式之一,子產品化并不是一種技術,而是一種開發思想。
子產品化将我們的複雜的代碼根據功能劃分為不同的子產品單獨去維護,通過這種方式提升開發效率和降低維護成本。
2. 子產品化的演變過程
早期的前端技術标準根本沒有意識到前端應用能有今天這樣的規模,是以很多設計上的遺留問題就導緻今天去實作前端子產品化會遇到很多問題。當然了,如今都被一些标準或者工具幫助我們解決了這些問題,但是它的演進過程是值得我們去思考的。
-
stage1-檔案劃分
最早期的前端子產品化是根據檔案劃分實作的,就是将每個功能的代碼單獨存放在不同的檔案當中,我們約定每個檔案就是獨立的子產品,然後在開發檔案引入它們,調用子產品的全局成員。但是顯然這種方式的缺點是十分明顯的,子產品内所有的成員都可以在子產品外被通路使用或修改,子產品一旦多了的話,就難免産生命名沖突,而且我們也無法管理子產品之前的依賴關系。總的來說,這種方式全部依靠人為的約定,一旦項目上了體量,這種方式的缺陷就是緻命的。
-
stage2-命名空間方式
檔案劃分的方式缺陷太多了,是以就有了第二個階段的子產品化,那就是在子產品内暴露一個全局的對象,子產品内的所有資料都挂載在這個全局對象中。但是這種方式雖然減少了命名沖突的可能,但是仍然沒有私有空間,還是沒有解決 stage1 的根本問題
-
stage3-IIFE(Immediately-Invoked Function Expression)
為了解決變量私有化的問題,有些人想到了可以在子產品内定義個立即執行函數,私有成員都放在這個立即執行函數中,然後需要暴露出去的成員,我們挂載到一個全局對象中去,這樣就實作了私有成員的概念。私有成員我們隻能通過子產品内閉包的方式去通路,而在子產品外部是沒有辦法去使用的。而且我們還可以通過這個立即執行函數的參數去作為我們的依賴聲明去使用,這樣就使得我們每個子產品的依賴聲明顯得更加明顯了。
以上這些就是早期在沒有子產品化工具和規範的情況下,我們去實作前端子產品化的方式,雖然這些方式幫助我們解決了子產品化各種各樣的問題,但是它仍然存在一些沒有解決的問題。
3. 子產品化規範的出現
為了統一不同前端開發者和不同項目的差異,我們就需要一個标準去規範我們實作前端子產品化的方式。
-
CommonJS 規範
了解過子產品化的人,肯定知道 CommonJS 規範,它是 Node 實作的的前端子產品化規範,它有如下幾個約定
- 一個檔案就是一個子產品
- 每個子產品都有單獨的作用域
- 通過 module.exports 導出成員
-
通過 require 函數載入子產品
CommonJS 是以同步方式去加載子產品,因為 Node 是在啟動時去加載子產品,在執行過程中是不需要加載子產品的。但是這種方式在浏覽器中去使用就會出現問題,如果我們浏覽器每次加載頁面都有大量的同步子產品去加載的話,就會導緻浏覽器執行的效率特别低下。
-
AMD(Asynchromous Module Definition)規範
AMD 規範是通過 require.js 實作的,require.js 本身是一個強大的子產品加載器。每個子產品都需要通過 define 這個函數去定義,這個函數有三個參數,第一個參數是子產品的名稱;第二個參數是一個數組,用來聲明子產品的依賴項;第三個參數是一個函數,這個函數的參數與第二個參數的依賴項一一對應,這個函數的作用就是為目前這個子產品去提供一個私有空間。

除此之外,require.js 還幫助我們去實作了一個 require 的函數,這個函數幫助我們去載入一個子產品。
那麼一旦 require 去加載一個子產品的時候,他就會去建立一個 script 标簽,然後去請求這個腳本檔案,并且執行相應子產品的代碼。目前絕大多數的第三方庫都支援 AMD 規範,但是它也有很多缺點:
* AMD 使用起來相對複雜
* 子產品 js 檔案請求複雜
AMD 隻能算是前端子產品化演進道路上的一步,它是一個妥協的實作方式,并不能算是最終的解決方案,但是在當時的環境背景下,它還是很有意義的,因為畢竟它為前端子產品化提供了一種标準。
-
CMD(Common Module Definition)規範
CMD 規範是由淘寶的前端團隊推出的,它是通過 sea.js 去實作的。它的實作有點類似 CommonJS,在使用上和 require.js 差不多
開始這樣設計是為了減少學習成本,後來這種方式也被 require.js 相容了。
現在前端的子產品化标準已經非常清晰了,那就是在 Node 環境中我們遵循 CommonJS 規範,在浏覽器中環境中我們會采用一個叫做 ES Modules 的規範。ES Modules 是在 ES6 中實作的子產品化标準,是以它會存在各種各樣的環境相容問題,随着 Webpack 等打包工具的普及,這一規範才開始普及。現在 ES Modules 基本已經是前端最主流的子產品化規範了,相比于 CommonJS 這種社群規範,ES Modules 可以說是真正在語言層面上實作了子產品化。
4. ES Modules 常見特性
目前市面上絕大多數的浏覽器都支援 ES Modules,我們想要使用它的特性,隻需要在 html 檔案中,給 script 标簽添加一個 type = module 的屬性,就可以用 ES Modules 的标準去執行其中的代碼了。下面我們來看看 ES Modules 都有什麼特性。
-
- 需要注意的是,在 ES Modules 規範中,自動采用嚴格模式,不用你手動添加 ‘use strict’,嚴格模式有一個代表,就是不能在全局環境中使用 this
<script type="module">
console.log(this) // undefined
</script>
-
- 每個 ES Module 都是運作在單獨的私有作用域中
<script type="module">
var foo = 'bar'
console.log(foo) // bar
</script>
<script type="module">
console.log(foo) // Uncaught ReferenceError: foo is not defined
</script>
可以看到在 ES Module 規範下,我們在一個 script 中定義變量後,再在另一個 script 中是取不到這個變量的,說明每個 module 都是一個私有作用域,這樣我們就不用擔心變量污染的問題了。
-
-
ES Module 是通過 CORS 的方式請求外部 JS 子產品的
在 ES Module 規範下,請求伺服器端位址時,是通過 CORS 的方式去請求的,如果目前位址不支援 CORS,就會産生跨域,下面的位址就是不支援 CORS的
-
<script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js">
</script>
我們來看一下浏覽器的控制台
可以看到是報了一個跨域的錯誤的,下面我們來更換一個支援 CORS 的位址
<script type="module" src="https://unpkg.com/[email protected]/dist/jquery.min.js">
</script>
我們再來看一下控制台
這回我們可以看到請求結果是正常的了
-
-
ES Module 的 script 标簽會延遲執行腳本
正常如果我們在 html 之前引入的 script 标簽,如果 js 腳本有阻塞性的代碼,那麼就會影響 html 的渲染。例如我們建立一個 demo.js
-
// demo.js
alert('這是一個彈窗')
我們在另一個 html 中引入這個 demo.js 檔案
<script src="demo.js"></script>
<p>需要顯示的内容</p>
那麼它就會先彈出一個彈窗,等我們點選确定之後,才會顯示 p 标簽内的内容。但是當我們使用 ES Module 規範時,它就會延遲執行腳本,等待網頁渲染完成後再去執行腳本檔案,就相當于使用了 defer 屬性
<script type="module" src="demo.js"></script>
<p>需要顯示的内容</p>
// 等價于
<script defer src="demo.js"></script>
<p>需要顯示的内容</p>
5. ES Modules 導入導出
使用 ES Modules 就可以使用 import 和 export 關鍵字
-
export
(1)直接導出
export var name = 'this is module'
export function hello() {}
export class Person {}
(2)導出成員
var name = 'this is module'
function hello() {}
class Person {}
export { name, hello, Person }
這種方式更為常見一些,我們可以更加清晰看到各種導出的子產品成員
(3)重命名導出
// a.js
var name = 'this is module'
function hello() {}
export {
name as fooName,
hello as fooHello,
}
// 導入
import fooName from 'a.js'
import fooHello from 'a.js'
(4)預設導出
如果我們到導出成員的時候,将成員重命名為 default 的話,那麼就是預設導出成員
// a.js
var name = 'this is module'
export {
name as default
}
// 導入
import { default as name } from 'a.js'
在 ES Modules 規範中,也支援我們直接預設導出成員,而且我們在導入的時候,可以将它命名為任意名稱。
// a.js
var name = 'this is module'
export default name
// 導入
import anyName from 'a.js'
需要注意的是 export 導出成員的時候 export {} 是一個固定的文法,而不是去導出一個對象變量,在 import 的時候,也不是結構對象。
通過 export 導出的成員就是成員本身,是以說在子產品内部修改了這個成員,子產品外部導出的成員也會跟随改變的
// a.js
const name = 'jack'
export { name }
setTimeout(() => {
name = 'ben'
}, 1000)
// b.js
import { name } from 'a.js'
console.log(name) // jack
setTimeout(() => {
console.log(name) // ben
}, 1500)
而且 export 導出的成員是一個常量,不可以被修改
// a.js
export var name = 'jack'
// b.js
import { name } from 'a.js'
name = 'tom' // 報錯
-
import
(1)導入用法
值得注意得是,在ES Modules 規範下,import 導入的路徑的字尾名必須寫完整,要不然就會無法識别,包括預設的 index,而且在我們去引入相對路徑的時候,還必須将 “./” 寫上,否則會當成一個封包件引入
// 完整路徑
import { name } from './module.js'
當然了,這種缺陷可以通過像 webpack 這樣的建構工具去解決
我們也可以通過檔案的絕對路徑去引入
我們也可以通過 url 的方式去引入檔案,這也意味着我們可以去通路 cdn 上的檔案
import { name } from 'http://localhost:300/04-import/module.js'
當我們隻想去執行某個子產品,而并不需要去提取這個子產品中的成員的時候,我們就可以将 “{}” 中置為空就可以了
import {} from './module.js'
// 上面的寫法可以簡寫為
import './module.js'
如果我們想導入這個子產品的是以成員,我們可以使用 “*” 的形式
// mod 是一個對象,它的屬性包含所有成員
import * as mod from './module.js'
console.log(mod)
如果我們想動态導入一個子產品,我們可以将路徑放在 import 的執行函數中,這個執行結果傳回一個 promise,我們可以通過 then 那到它的成員
import('./module.js).then(function (module) {
...
})
如果在導出檔案中同時導出了部分成員和預設成員
// 導出檔案 module.js
var name = 'maoxiaoxing'
var age = '25'
export { name, age }
export default 'default export'
// 導入檔案中
import { name, age, default as title } from './module.js'
console.log(name, age, title)
上面的導入預設成員的方式,還有簡寫的方式
-
導出導入成員
如果我們想将導入的成員直接導出的,話我們隻需要将 import 換成 export 即可
6. ES Modules 環境相容
ES Modules 規範是2014年才被提出來的,是以早期的浏覽器是不可能支援這個特性的,那麼我們就要考慮浏覽器的相容性問題了。
在 ie 和一些國産浏覽器上,ES Modules 規範還沒有被支援
- 浏覽器環境 Polyfill
要想在各個浏覽器上相容 ES Modules 規範,我們可以使用 browser-es-module-loader 來相容我們的代碼。我們可以直接在 https://unpkg.com/ 上面通過 script 标簽引入這段代碼。
// module.js
export var foo = 'bar'
// 相容 promise
<script nomodule src="https://unpkg.com/[email protected]/dist/polyfill.min.js"></script>
// babel 即時運作在浏覽器
<script nomodule src="https://unpkg.com/[email protected]/dist/babel-browser-build.js"></script>
// browser-es-module-loader
<script nomodule src="https://unpkg.com/[email protected]/dist/browser-es-module-loader.js"></script>
<script type="module">
import { foo } from './module.js'
console.log(foo)
</script>
-
ES Modules in Node.js
Node.js 在8.5版本過後,也開始逐漸支援 ES Modules 規範了,但是考慮到 ES Modules 規範和 commonjs 規範的差距還是比較大的,是以目前這個特性還是處于一個過渡的狀态。
要想在 node 中使用 ES Modules 規範,第一件事我們需要将以 .js 結尾的檔案名改為 .mjs
// module.mjs
export const foo = 'hello'
export const bar = 'world'
import { foo, bar } from './module.mjs'
console.log(foo, bar) // hello world
需要注意的是,在我們執行 node 檔案時,我們需要 node XXX.js 修改為下面的指令
node --experimental-modules index.mjs
通過這樣的方式來告知 node 我們是用 ES Modules 規範執行我們的代碼。但是這隻是一個實驗特性,我們在生産環境不要這樣做。