原生ES-Module在瀏覽器中的嘗試

其實瀏覽器原生模塊相關的支持也已經出了一兩年了(我第一次知道這個事情實在2016年下半年的時候)
能夠拋開 webpack直接使用 import之類的語法
但由於算是一個比較新的東西,因此如今基本只能本身鬧着玩 :p
但這並不能成爲不去了解它的藉口,仍是要體驗一下的。

首先是各大瀏覽器從什麼時候開始支持module的:javascript

  • Safari 10.1
  • Chrome 61
  • Firefox 54 (有可能須要你在about:config頁面設置啓用dom.moduleScripts.enabled)
  • Edge 16
數據來自 https://jakearchibald.com/2017/es-modules-in-browsers/

使用方式

首先在使用上,惟一的區別就是須要在script標籤上添加一個type="module"的屬性來表示這個文件是做爲module的方式來運行的。html

<script type="module">
  import message from './message.js'

  console.log(message) // hello world
</script>

而後在對應的module文件中就是常常會在webpack中用到的那樣。
語法上並無什麼區別(原本webpack也就是爲了讓你提早用上新的語法:)java

message.jsnode

export default 'hello world'

優雅降級

這裏有一個相似於noscript標籤的存在。
能夠在script標籤上添加nomodule屬性來實現一個回退方案。webpack

<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的版本確定仍是會有一些區別的。
(至少一個是運行時解析的、一個是本地編譯)git

有效的module路徑定義

由於是在瀏覽器端的實現,不會像在node中,有全局module一說(全局對象都在window裏了)。
因此說,from 'XXX'這個路徑的定義會與以前你所熟悉的稍微有些出入。es6

// 被支持的幾種路徑寫法

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須要將本地文件打包到一塊兒)。github

module的文件默認爲defer

這是script的另外一個屬性,用來將文件標識爲不會阻塞頁面渲染的文件,而且會在頁面加載完成後按照文檔的順序進行執行。web

<script type="module" src="./defer/module.js"></script>
<script src="./defer/simple.js"></script>
<script defer src="./defer/defer.js"></script>

爲了測試上邊的觀點,在頁面中引入了這樣三個JS文件,三個文件都會輸出一個字符串,在Console面板上看到的順序是這樣的:數組

918j.png

行內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.jshttps://www.baidu.com/module.js都會被認爲是同一個module
可是像這個例子中的module1.jsmodule1.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>
在線Demo

import和export在使用的一些小提示

不論是瀏覽器原生提供的版本,亦或者webpack打包的版本。
importexport基本上仍是共通的,語法上基本沒有什麼差異。

下邊列出了一些可能會幫到你更好的去使用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'
在線Demo

小記

想到了最近爆紅的deno,其中有一條特性也是提到了,沒有node_modules,依賴的第三方庫直接經過網絡請求的方式來獲取。
而後瀏覽器中原生提供的module也是相似的實現,都是朝着更靈活的方向在走。
祝願拋棄webpack來進行開發的那一天早日到來 :)

參考資料

  1. es modules in browsers
  2. es6 modules in depth
  3. export - JavaScript | MDN
  4. import - JavaScript | MDN

文中示例代碼的GitHub倉庫:傳送陣

相關文章
相關標籤/搜索