Es6中的模块默认导入导出及加载顺序

5,074 阅读22分钟
原文链接: www.jianshu.com

点击此链接阅读体验更好Es6中的模块默认导入导出及加载顺序

前言

在前面一Es6中的模块化Module,导入(import)导出(export)文中,我们已经知道如何让两个不同模块之间进行数据的绑定,通过export关键字对外暴露定义声明时变量对象,函数或者类,而通过import关键字在另一个模块导入所暴露时变量的对象,

通常引用变量对象与对外暴露的变量对象要一一对应,当然也可以在导入导出时通过as关键字进行重命名。

然而上述的都是我们已知对外暴露的变量对象,那么要是在不知道的情况下呢,通常我们在一些基于脚手架生成的代码里,这种写法非常常见,话说多了,都是故事,一码胜千言,继续领略Es6中的模块化..

您将在本篇中了解到如何导出模块的默认值,模块的加载,以及在web浏览器中使用模块加载,是引入包还是引入本地模块

正文从这开始~

模块(module)导出的默认值

在实际代码中,我们通过export关键字是能够对外暴露本模块中的变量对象,函数,类的,尽管使用如下方式export向外暴露多个变量对象,后面跟一大括号,变量名与变量名之间用逗号隔开:如下代码示例所示

exprot {identifier1,identifier2,...}

但是问题来了,如果我不想写这些变量对象呢?那么可以使用default关键字指定单个变量,函数或者类,但是要格外注意一点就是每个模块只能设置一个默认的导出值,也就是说你只能使用一次export default ,若在同一个模块中重复使用了default关键字,它就会报错,因为计算机不知道你具体要暴露哪一个,这是不允许的 使用方式:把下面这段代码命名为exportExample.js

export default function(num1,num2){
  return num1-num2;
}

当然也可以这么写,先定义好函数,命名一个有名函数,然后在默认导出

function sub(num1,num2){
  return num1-num2;
}
export default sub; // 如果不用default,则导出用export {sub},注意这个双大括号必须要加的,否则就会报错,而在另一模块导入的模块中使用import导入变量对象时,同样要用双大括号

注意1:当单独使用export暴露变量对象,函数,或者类时,要使用双大括号{}给包裹起来,否则的话就会报错,因为export后面若跟着的是一个常量那么就没有任何意义,使用双大括号正确后,在另一个模块中使用import导入变量对象时,仍然得使用双大括号包裹起来,否则仍然会报错
[图片上传失败...(image-cf7ced-1533186616588)]
正确的输出方式:

image
注意2:若是使用默认default输出的方式,单个变量对象暴露的话,可以不加双大括号{},但若是多个变量对象的话,那么就要加上双大括号{}

注意3:若使用export default导出默认值,在一个模块中,只能出现一次,不能出现两次,例如:

var name = "随笔川迹";
   var age = 18;
   var weChatPublic = "itclanCoder";
   function sub(num1,num2){
       return num1-num2;
   }
   export default{
       name,    // 也可以写成name:name,其中前面的变量名可以随意
       age,     
       weChatPublic
   }
   export default sub;
   // 在同一个模块中出现了两次export default系统时会报错的

正确的使用方式,注意凡是带有./,../等路径的,都属于本地模块,而不带的,一般都是包,其实包也是模块,只不过在node中通常都是通过命令行进行安装,放到node_module里面去了的,一些自动化打包工具帮我们做了一些路径匹配的事情了的

image

那么对应的另一个模块import导出的是什么?我们先不用默认导出方式来看看

import sub from "./js/exportExample.js"

这里要注意一点就是:若是使用export default的方式默认导出,此处的sub就不要加{}双大括号,否则就会报错

image
在第一段代码中是导出了一个函数作为默认值,default关键字表示这是一个默认的导出,也就是可以理解为把后面的匿名函数赋值给default作为默认值导出

而第二段代码中,先定义了sub()函数,然后将其导出为默认值,如果需要计算默认值,就可以使用这个方法

在上一篇中,我们知道可以通过as关键字对导出进行重命名,如下所示

function sub(num1,num2){
  return num1-num2;
}
export {sum as default}

在重命名导出时标识符default是具有特殊含义的,用来指示模块的默认值,加上default是javascript中的默认关键字,因此不能将其用作变量,函数或者类的名称,但是却可以,将其作为属性名称,所以用default来重命名模块时为了尽可能与非默认导出的定义一致,如果想在一条导出语句中同时制定多个导出,这个包括默认导出,这种用法就非常有用了

以下是在Node坏境中测试如下所示:

将下面的js代码命名为exampleExprt.js,为es6写法,由于目前node暂不支持module模块化,所以得通过babel转化Es5代码然后在Node中执行,具体babel安装和转译,详情可看上篇内容,这里就不在重复了的

var name = "随笔川迹";
var age = 18;
var weChatPublic = "itclanCoder";

function sub(num1,num2){
  return num1-num2;
}

export default {
   name,
   age,
   weChatPublic,
   sub
}

通过babel转化后为Es5代码

"use strict";

Object.defineProperty(exports, "__esModule", {
   value: true
});
var name = "随笔川迹";
var age = 18;
var weChatPublic = "itclanCoder";

function sub(num1, num2) {
   return num1 - num2;
}

exports.default = {
   name: name,
   age: age,
   weChatPublic: weChatPublic,
   sub: sub
};

而在另一个模块中引入暴露的模块,将下面的代码命名为importExample.js

import message from "./exampleExportEs5.js";

console.log(message);
console.log("昵称是:",message.name);
console.log("年龄是:",message.age);
console.log("微信公众号是",message.weChatPublic);
console.log("10减5等于",message.sub(10,5));

通过babel转化后为Es5代码

"use strict";

var _exampleExportEs = require("./exampleExportEs5.js");

var _exampleExportEs2 = _interopRequireDefault(_exampleExportEs);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

console.log(_exampleExportEs2.default);
console.log("昵称是:", _exampleExportEs2.default.name);
console.log("年龄是:", _exampleExportEs2.default.age);
console.log("微信公众号是", _exampleExportEs2.default.weChatPublic);
console.log("10减5等于", _exampleExportEs2.default.sub(10, 5));

在node坏境中命令行终端下显示如下


image

模块中导入默认值

可以使用以下import关键字从一个模块导入一个默认值

import sub "./exampleExport.js"
console.log(sub(10,5));   // 5

这条import语句从模块exampleExport.js中导入了默认值,要特别注意的是,这里没有使用大括号{},这与非默认导入的情况是不一样的,本地名称sub用于表示模块导出的任何默认函数,这在Es6中是常见的做法,并且在一些脚手架里依然采用这种方式引入一些模块的方式非常流行

那么问题来了,如果是要导出默认值或者非默认绑定的模块呢?可以用一条语句导入所有导出的绑定,例如,如下所示:将下面的代码命名为exampleDefault.js

export var name = "川川";  // 非默认导出格式
export let desc = "一个靠前排的90后帅小伙"; // 同上
const age = 25;

function multiply(num1, num2) {
 return num1 * num2;
}
export default {  // 默认导出格式
 age,
 multiply
}

[图片上传失败...(image-3e79c-1533186616588)] 在另一个默认块中用import语句中,将默认值与非默认格式用逗号隔开,并且默认值的位置在非默认值前面,否则就会报错,示例代码如下所示

import message, { name, desc} from  "./js/exportExample.js" // 注意默认值必须得在非默认值前面,非默认值得用双大括号括起来

console.log(message);
console.log("昵称是:",name);
console.log("描述",desc);
console.log("年龄age",message.age);
console.log("10乘以5等于",message.multiply(10,5));

使用默认导出的变量对象在前面,非默认导出的变量对象在后面
[图片上传失败...(image-5a0ac-1533186616588)]
当颠倒过来后,非默认的导出变量对象在前面,则会报错,如下图所示
[图片上传失败...(image-3c628b-1533186616588)]
同样,在node坏境中测试可得


image

与导出默认值一样,也是可以在导入默认值是使用重命名语法的,具体如下所示

 import {default as message,name,desc};
  console.log(message.age);
  console.log("10乘以5等于",message.multiply(10,5));
  console.log("昵称是:",name);
  console.log("年龄age是:",age);

[图片上传失败...(image-c92f94-1533186616588)]
在上面这段代码中,默认导出export值被重命名为mesage,并且还导入了非默认导出的变量对象name,desc,但是通过这种方式,要注意的是:无论是非默认值位置在前还是在后,就无关紧要了,并不会报错,但是习惯性的,我们还是把默认值放在前面吧
[图片上传失败...(image-c1b34d-1533186616588)]

模块中重新导出一个绑定

有时候,当你在一个模块中已经导入了内容,这个时候,发现又要将导入的模块暴露给另外一个模块使用,那么就可以这么做

import {sub} from "./exampleExport.js"  // 这句话的意思是,从后面的本地模块中导入sub变量对象
export {sub}   // 从该模块中又把导入的sub变量对象暴露出去
image

如果你想到处另一个模块中的所有值,可以通过*号模式,这也是我们常在一些脚手架工具常看到的

例如如下所示

export * from "./exampleExport.js"

上面的* 号代表所有,指导出默认值及所有的命名导出值,这种做法会影响你从模块中导出的内容,例如:exampleExport.js中有默认的export default的导出值,那么它无法定义一个新的默认导出的,当一模块中有指定的默认导出,那么上面的写法是会报错的

模块中无绑定导入

有时候,某些模块可能不导出任何变量对象,函数或类,但是,它可能会修改全局作用域中的对象,尽管模块中的顶层变量,函数和类不会自动的出现在全局作用域中,但是这并不意味模块无法访问全局作用域,内建(系统/内置)对象(如Array和object)的共享定义可以在模块中访问,对这些对象所做的更改将反映在其他模块中

例如,要向所有的数组添加pushAll()方法,则可以定义如下所示的模块:将下面的代码存储为exampleNobind.js

// 这个模块没有export或Import的模块代码
Array.prototype.pushAll = function(items){
    // items必须是一个数组
    if(!Array.isArray(items)){  // isArray是检测数组的一个方法
        throw new TypeError("参数必须是一个数组");
    }
   // 使用内置的push()方法和Es6中的展开拓展符
   return this.push(...items);
}

在上面的代码中,即使没有任何导出或导入的操作,这也是一个有效的模块,这段代码既可以用作模块,也可以用作单独脚本,由于它没有导出任何东西,所以,在另一个模块中,可以使用简化导入操作来执行该模块代码,并且不导入任何的绑定,示例代码如下

import "./exampleNobind.js"

let fruits = ["apple","banner","orange","peach"]
let items = [];
items.pushAll(fruits);

[图片上传失败...(image-40ebb-1533186616588)] 上面的代码导入并执行了模块中包含的pushAll()方法,所以pushAll()被添加到数组的原型,也就是说现在模块中的所有数组都可以使用pushAll()方法了,其实这个原理还是在原型上添加属性和方法,就是拓展嘛,还有就是改写对象下面的公用的方法或者属性

让公用的方法或者属性在内存中存在一份,可以看作是对象的基类,原型是为了提升性能而生的,当内置对象(例如Array,Data,RegExp,String,Error,Object)提供的方法不够用时,完成不了所需要的功能时,我们就可以通过原型进行额外的拓展,那些插件写法就是基于原有的对象额外拓展来的

注意:无绑定导入最有可能被应用于创建Polyfill(Polyfill 就是一系列的代码或者插件,它为开发者提供的技术特性,都是希望浏览器本就应该原生支持的,并且抹平了 api 之间的使用差异)和Shim(Shim 通常是一个代码库,它给旧环境(并不一定特指浏览器环境)带来的往往是全新的 api,而且这些 api 只能在这个环境当中运行)

根据资料可查:shim是一个库,它将一个新的API引入到一个旧的环境中,而且仅靠旧环境中已有的手段实现,而一个polyfill就是一个用在浏览器API上的shim.

我们通常的做法是先检查当前浏览器是否支持某个API,如果不支持的话就加载对应的polyfill.然后新旧浏览器就都可以使用这个API了。

也就是说shim是一个库,比如我们经常听说的es5-shim,它是在ecmascript3的引擎上实现了ecmascript5的特性,用到的技术都是ecmascript3的技术。而polyfill相当于一段代码,它先检查这个浏览器是否支持某个API,如果不支持就加载对应的polyfi

总结:看了这么多东西,也许你比较晕,对于导出与导入的绑定,什么时候加双大括号以及不加呢?没有关系,多测试一下,就知道了的,下面小结一下

  1. 导出变量对象,函数,类,用export关键字,后面跟着要对外暴露的变量对象,export关键字可以直接放在要暴露变量对象的前面,也可以先声明,然后在统一管理向外暴露,但是此时对外暴露变量对象一定得用{}双大括号给包裹起来,若是多个变量对象,函数等之间用逗号隔开,对于导出的变量对象,也可以通过as关键字进行重命名
  1. 在另一个模块中通过import关键字进行导入,import {indefined1,...} from '本地模块路径',注意导入时变量对象要与导出时一一对应,当然也可以通过as关键字进行重命名
  1. 若是模块中使用了默认导出default关键字对外暴露变量对象,那么在另一个导入模块中,此时的绑定变量对象就无须加双大括号{}了的,并且export defautl在导出的模块中只能出现一次,不能重复出现,否则就会报错,因为系统会找不到的,不知道你具体要导出哪个,不明确的话,就会报错

模块的加载

在Es6中定义模块的语法,但是它并没有定义是如何加载这些模块的,在Es6中只是规定了语法,其实它将加载机制抽象到一个未定义的内部方法HostResolveImportedModule了,web浏览器和Node.js开发者可以通过对各自的坏境的认知来决定如何实现这个东东的

在web浏览器中使用模块

在web浏览器中,我们通常要加载外部的一个javascript脚本,通常有以下几种方法

  1. 在<script>元素中通过src属性指定一个加载代码的地止来加载javascript代码文件

  2. 将javascript代码内嵌到没有src属性的<script>元素中(动态的插入)

  3. 通过web Worker(它是运行在后台的javascript代码,new Worker("code.js")适用于那些处理纯数据, 或者与浏览器UI无关的长时间运行脚本,解析一个很大的JSON字符串)或server worker(可以实现消息推动、地理围栏、离线应用等功能,相当于在浏览器端建立了一个代理服务)的方法加载并执行javascript代码文件

具体实现可以Gooogle或者社区里找一些文档学习学习,关于web Worker和server worker这方面知识我也是知之甚少,这东西绝地在以后能派上得上用场,当然在后面也会提到

在script中使用模块

script元素默认行为是将javascript文件作为脚本加载,而非模块加载,也就是当你不写type="text/javascript"时,它也会默认是这个,script标签元素可以执行内联代码(放在script标签里面的代码是可以被执行的,称为内联代码)或者加载src中的指定的文件

但是当type属性值为module时就支持加载模块了,将type设置为module时,就可以让浏览器将所有内联代码或包含在src指定的文件中的代码按照模块而非脚本的方式加载,如下示例代码所示

<!--加载一个javascript模块文件-->
<script type="module" src="module.js"></script>
<!-- 内联一个模块 -->
<script type="module">
     import {sub} from "./example.js"

     let result = sum(30,10);

</script>

这里要注意的是:这种模式必须在服务器坏境下才支持,对于从硬盘下直接在浏览器中预览是不支持的,因为有限制,你可以用hbuilder或者webstorm,或者appserver,wamp,xammp把示例脚本放进去测试


image

在服务器坏境下测试结果如下


image
上面的示例代码中,第一个script标签元素使用了src属性加载了一个外部的模块文件,它与加载脚本之间唯一的区别是type的值是module,第二个script元素包含了直接嵌入在网页中的模块,变量result没有不暴露到全局作用域中去,它只存在于模块中script元素定义,所以,它是不会被添加到window作为它的属性

在web页面中引入模块的过程类似于引入脚本,但是,模块实际的加载过程却有一些不同

注意:module与text/javascript这样的内容类型并不相同,JavaScript模块文件与javascript脚本文件具有相同的内容类型,因此无法根据内容类型进行区分,此外,当无法识别type的值时,浏览器会忽略script元素,因此不支持模块的浏览器将自动忽略<script type="module"></script>来提供良好的向后兼容性,在高版本浏览器中,支持Es6中模块化写法,但是在低版本中,就不支持了

web浏览器中模块加载顺序

模块与脚本时不同的,它是独一无二的,可以通过import关键字来指明其所依赖的其他文件,并且这些必须被加载进该模块才能正确的执行

代码是从上往下进行解析的,模块按照它们出现在HTML文件中的顺序执行,也就是说,无论模块中包含的是内联代码还是指定src属性,<第一个script type="module">总是在第二个之前执行,例如:

<!--先执行这个标签-->
  <script type="module" src="module1.js"></script>

  <!-- 在执行这个标签 -->
  <script type="module">
          import {sub} from "./example.js"
          let result = sub(10,2);
  </script>
  <!-- 最后执行这个标签 -->
  <script type="module" src="module2.js"></script>

[图片上传失败...(image-d18b1a-1533186616588)] 在上面的示例代码中,代码的执行顺序是从上往下依次顺序执行的,在浏览器中加载脚本是非常快的,并且它们是同步执行的,module1.js会在example.js内联模块代码前面执行,而内联模块又会在module2.js前面执行

首先解析模块以识别所有导入(也就是import)语句,然后每个导入语句都触发一次获取过程(从网络或从缓存),并且在所有导入资源都被加载和执行后才会执行当前模块

用<script type="module"></script>显示引入和import隐式导入的所有模块都是按需加载并执行的,这跟require()导入模块是不同的,后者是全部引入,在上面的这个示例中,完整的加载顺序如下所示

  1. 下载并解析module1.js

  2. 下载并解析module1.js总导入的资源

  3. 解析内联模块(也就是上面第二个script标签)

  4. 下载并解析内联模块中导入的资源

  5. 下载并解析module2.js

  6. 下载并解析module2.js中导入的资源

在所有的资源加载完成后,只有当文档完全被解析之后才会执行其他操作,文档解析完成后,会发生以下操作

  1. 执行module1.js中导入的资源

  2. 执行module1.js

  3. 执行内联模块中导入的资源

  4. 执行内联模块

  5. 执行module2.js中导入的资源

  6. 执行module2.js

这里要注意的是:内联模块与其他两个模块不同的是,它不必先下载模块代码,否则,加载导入资源和执行模块的顺序就是一样的,其实加载模块的过程就是对数据的读操作,而后续对变量对象的赋值就是写操作

上面的是同步代码执行操作,但是有时候,我们想要后面的代码在前面的代码执行,也就是不按照位置的顺序执行,那应该怎么做?

将模块作为worker加载

在前文中提到一次worker,它分别为web Worker(是运行在后台的 JavaScript,不会影响页面的性能)和server worker(滚到上面可看解释),它们可以在网页上下文之外执行javascript代码,创建一个新的Worker步骤包括:创建一个新的Worker实例(或者其他的类),传入javascritp文件的地止,默认的加载机制是按照脚本的方式加载文件,比如:

// 按照脚本的方式加载script.js 
 let worker  =  new Worker("script.js");

为了支持加载模块,HTML标准的开发者向这这个构造函数添加来第二个参数,第二个参数是一个对象,其type属性的默认值是script,可以将type设置为module来加载模块文件

/ 按照模块的方式加载module.js 
let worker =  new Worker("module.js",{type:"module"});

在上面的代码中第一个参数是加载的模块,第二个参数传入一个对象,设置type值为module,按照模块而不是脚本的方式加载module.js,用以区分加载脚本还是模块,在所有的浏览器中Worker类型都支持第二个参数

Worker模块通常与Worker脚本一起使用,但也有一些例外,Worker脚本只能从与引用相同的源加载,但是worker模块不完全受限,尽管Worker模块具有相同的默认限制,但它们还是可以加载并访问具有适当的跨域资源共享(CORS)头的文件,另外,Worker脚本可以使用self.importScripts()方法将其它脚本加载到Worker中,但self.importScripts()是始终无法加载Worker模块的,因为要用improt将外部的模块进行导入

是引入包还是引入本地模块

但凡有路径斜杠./,或者../的都是本地模块,而没有的都是包,以nodejs中为例:在一些基于脚手架搭建的项目,当你通过npm安装依赖一些包时,而往往在模块中需要通过import引入时,不用加一些文件后缀名的,因为一些自动打包工具例如webpack帮我们做了的,在浏览器中加载模块有下面几种方式

以/开头的解析为从根目录开始

以./开头的解析为从当前目录开始

以../开头的解析为从父目录开始(上上级目录)

URL 格式 下面一个模块文件位于www.baidu.com/modules/mod… 为例

// 从当前目录引入模块
import {one} from "./example1.js"

// 从上上级目录中引入模块 
import {two} from "../example2.js"
// 从根目录中引入模块
import {three} from "/example3.js"
// 从线上引入模块
import {fourth} from "https://www.baidu.com/example4.js"

注意最后一个从第三方引入模块的时候,需要将该网址的CORS进行一个配置,否则是无法正确引入的,因为同源策略的问题

注意引入本地模块时,路径前需要加上资源位置的说明符,比如./或../,/之类的,否则是无法被浏览器正确的加载模块的,虽然从src中引入是可以正常加载使用,但是只要使用import这种方式引入模块,资源的路径前面就得加上起始的位置字符

总结

整篇内容主要是当模块以设置默认对外暴露对象导出时应使用default关键字,而在另一模块导入绑定变量对象时,不用加双{}大括号,并且若是有默认导出和非默认导出时,在导入绑定变量对象时,默认导出的绑定放在前面,而非默认的绑定放在后面,对于非默认导出时,在导入绑定变量对象与导出暴露的变量对象要一一对应,需要用双大括号{}把要暴露的变量对象和绑定的变量对象包裹起来,否则就会报错

当然也可以通过as关键字进行导出导入重命名,关于Es6中的模块化,非常重要,只要接触过利用脚手架XXX-cli自动构建的项目,各个模块的依赖关系,必然离不开Es6重的模块化,涉及到export模块的暴露和import模块的导入 初学者笔记学习心得,如果内容有误导的地方,谢谢路过的老师多提意见和指正

点击此链接阅读体验更好Es6中的模块默认导入导出及加载顺序