其實瀏覽器原生模組相關的支援也已經出了一兩年了(我第一次知道這個事情實在2016年下半年的時候)
可以拋開webpack
直接使用import
之類的語法
但因為算是一個比較新的東西,所以現在基本只能自己鬧著玩 :p
但這並不能成為不去了解它的藉口,還是要體驗一下的。
首先是各大瀏覽器從何時開始支援module
的:
- Safari 10.1
- Chrome 61
- Firefox 54 (有可能需要你在
about:config
頁面設定啟用dom.moduleScripts.enabled
) - Edge 16
使用方式
首先在使用上,唯一的區別就是需要在script
標籤上新增一個type="module"
的屬性來表示這個檔案是作為module
的方式來執行的。
<script type="module">
import message from './message.js'
console.log(message) // hello world
</script>
複製程式碼
然後在對應的module
檔案中就是經常會在webpack
中用到的那樣。
語法上並沒有什麼區別(本來webpack
也就是為了讓你提前用上新的語法:) )
message.js
export default 'hello world'
複製程式碼
優雅降級
這裡有一個類似於noscript
標籤的存在。
可以在script
標籤上新增nomodule
屬性來實現一個回退方案。
<script type="module">
import module from './module.js'
</script>
<script nomodule>
alert('your browsers can not supports es modules! please upgrade it.')
</script>
複製程式碼
nomodule
的處理方案是這樣的: 支援type="module"
的瀏覽器會忽略包含nomodule
屬性的script
指令碼執行。
而不支援type="module"
的瀏覽器則會忽略type="module"
指令碼的執行。
這是因為瀏覽器預設只解析type="text/javascript"
的指令碼,而如果不填寫type
屬性則預設為text/javascript
。
也就是說在瀏覽器不支援module
的情況下,nomodule
對應的指令碼檔案就會被執行。
一些要注意的細節
但畢竟是瀏覽器原生提供的,在使用方法上與webpack
的版本肯定還是會有一些區別的。
(至少一個是執行時解析的、一個是本地編譯)
有效的module路徑定義
因為是在瀏覽器端的實現,不會像在node
中,有全域性module
一說(全域性物件都在window
裡了)。
所以說,from 'XXX'
這個路徑的定義會與之前你所熟悉的稍微有些出入。
// 被支援的幾種路徑寫法
import module from 'http://XXX/module.js'
import module from '/XXX/module.js'
import module from './XXX/module.js'
import module from '../XXX/module.js'
// 不被支援的寫法
import module from 'XXX'
import module from 'XXX/module.js'
複製程式碼
在webpack
打包的檔案中,引用全域性包是通過import module from 'XXX'
來實現的。
這個實際是一個簡寫,webpack
會根據這個路徑去node_modules
中找到對應的module
並引入進來。
但是原生支援的module
是不存在node_modules
一說的。
所以,在使用原生module
的時候一定要切記,from
後邊的路徑一定要是一個有效的URL
,以及一定不能省略檔案字尾(是的,即使是遠端檔案也是可以使用的,而不像webpack
需要將本地檔案打包到一起)。
module的檔案預設為defer
這是script
的另一個屬性,用來將檔案標識為不會阻塞頁面渲染的檔案,並且會在頁面載入完成後按照文件的順序進行執行。
<script type="module" src="./defer/module.js"></script>
<script src="./defer/simple.js"></script>
<script defer src="./defer/defer.js"></script>
複製程式碼
為了測試上邊的觀點,在頁面中引入了這樣三個JS
檔案,三個檔案都會輸出一個字串,在Console
皮膚上看到的順序是這樣的:
行內script也會預設新增defer特性
因為在普通的指令碼中,defer
關鍵字是隻指標對指令碼檔案的,如果是inline-script
,新增屬性是不生效的。
但是在type="module"
的情況下,不管是檔案還是行內指令碼,都會具有defer
的特性。
可以對module型別的指令碼新增async屬性
async
可以作用於所有的module
型別的指令碼,無論是行內還是檔案形式的。
但是新增了async
關鍵字以後並不意味著瀏覽器在解析到這個指令碼檔案時就會執行,而是會等到這段指令碼所依賴的所有module
載入完畢後再執行。
import的約定,必須在一段程式碼內的起始位置進行宣告,且不能夠在函式內部進行
也就是說下邊的log
輸出順序完全取決於module.js
載入的時長。
<script async type="module" >
import * from './module.js'
console.log('module')
</script>
<script async src="./defer/async.js"></script>
複製程式碼
一個module只會載入一次
這個module
是否唯一的定義是資源對應的完整路徑是否一致。
如果當前頁面路徑為https://www.baidu.com/a/b/c.html
,則檔案中的/module.js
、../../module.js
與https://www.baidu.com/module.js
都會被認為是同一個module
。
但是像這個例子中的module1.js
與module1.js?a=1
就被認定為兩個module
,所以這個程式碼執行的結果就是會載入兩次module1.js
。
<script type="module" src="https://blog.jiasm.org/module-usage/example/modules/module1.js"></script>
<script type="module" src="/examples/modules/module1.js"></script>
<script type="module" src="./modules/module1.js"></script>
<script type="module" src="./modules/module1.js?a=1"></script>
<script type="module">
import * as module1 from './modules/module1.js'
</script>
複製程式碼
import和export在使用的一些小提示
不管是瀏覽器原生提供的版本,亦或者webpack
打包的版本。
import
和export
基本上還是共通的,語法上基本沒有什麼差別。
下邊列出了一些可能會幫到你更好的去使用modules
的一些技巧。
export的重新命名
在匯出某些模組時,也是可以像import
時使用as
關鍵字來重新命名你要匯出的某個值。
// info.js
let name = 'Niko'
let age = 18
export {
name as firstName,
age
}
// import
import {firstName, age} from './info.js'
複製程式碼
Tips: export的呼叫不像node中的module.exports = {}
可以進行多次呼叫,而且不會覆蓋(key重名除外)。
export { name as firstName }
export { age }
複製程式碼
這樣的寫法兩個key都會被匯出。
export匯出的屬性均為可讀的
也就是說export
匯出的屬性是不能夠修改的,如果試圖修改則會得到一個異常。
但是,類似const
的效果,如果某一個匯出的值是引用型別的,物件或者陣列之類的。
你可以操作該物件的一些屬性,例如對陣列進行push
之類的操作。
export {
firstName: 'Niko',
packs: [1, 2]
}
複製程式碼
import * as results from './export-editable.js'
results.firstName = 'Bellic' // error
results.packs.push(3) // success
複製程式碼
這樣的修改會導致其他引用該模組都會受到影響,因為使用的是一個地址。
export在程式碼中的順序並不影響最終匯出的結果
export const name = 'Niko'
export let age = 18
age = 20
複製程式碼
const 或者 let 對於 呼叫方來說沒有任何區別
import {name, age} from './module'
console.log(name, age) // Niko 20
複製程式碼
import獲取default模組的幾種姿勢
獲取default
有以下幾種方式都可以實現:
import defaultItem from './import/module.js'
import { default as defaultItem2 } from './import/module.js'
import _, { default as defaultItem3 } from './import/module.js'
console.log(defaultItem === defaultItem2) // true
console.log(defaultItem === defaultItem3) // true
複製程式碼
預設的規則是第一個為default
對應的別名,但如果第一個引數是一個解構的話,就會被解析為針對所有匯出項的一個匹配了。
P.S. 同時存在兩個參數列示第一個為default,第二個為全部模組
匯出全部的語法如下:
import * as allThings from './iport/module.js'
複製程式碼
類似index的export檔案編寫
如果你碰到了類似這樣的需求,在某些地方會用到十個module
,如果每次都import
十個,肯定是一種浪費,視覺上也會給人一個不好的感覺。
所以你可能會寫一個類似index.js
的檔案,在這個檔案中將其引入到一塊,然後使用時import index
即可。
一般來說可能會這麼寫:
import module1 from './module1.js'
import module2 from './module2.js'
export default {
module1,
module2
}
複製程式碼
將所有的module
引入,並匯出為一個Object
,這樣確實在使用時已經很方便了。
但是這個索引檔案依然是很醜陋,所以可以用下面的語法來實現類似的功能:
export {default as module1} from './module1.js'
export {default as module2} from './module2.js'
複製程式碼
然後在呼叫時修改為如下格式即可:
import * as modules from './index.js'
複製程式碼
小記
想到了最近爆紅的deno
,其中有一條特性也是提到了,沒有node_modules
,依賴的第三方庫直接通過網路請求的方式來獲取。
然後瀏覽器中原生提供的module
也是類似的實現,都是朝著更靈活的方向在走。
祝願拋棄webpack
來進行開發的那一天早日到來 :)
參考資料
文中示例程式碼的GitHub倉庫:傳送陣