原生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仓库:传送阵

相关文章
相关标签/搜索