前言
BUG总是改不完的,但是要在无限的BUG中寻找有限的知识,平时导入组件模块都会使用import,但是有没有疑惑 import 是怎样工作的喃?如果不懂,那还等什么,不去冲,难道等着被技术淘汰吗?
模块导出和导入
export 导出
export 导出语法是:export ①
①指要导出的语句,必须包含声明,可以不存在赋值
export 能够导出使用了 function 、 class 、 var 、 let 、 const 的关键字的语句
export var a = 5
export let b = 6
export const c = 7
export function fun() { }
export class obj { }
可能很多都会疑惑,为什么要着重去强调语句喃?这就要从我们常常使用过的错误导出入手了,先看几个错误例子
var a = 5
export a // 错误;不能是a变量的表达式,必须是完整的变量声明语句
let b
export b // 错误;不能是b变量的赋值表达式,必须是完整的变量声明语句
function fun() {}
export fun // 错误;同理变量,导出函数也必须是函数声明语句
export function () { } // 错误;导出函数匿名函数,不能进行导出
export () => { } // 错误;箭头函数因为没有名称,也不能进行导出
- 导出
function、class、var、let、const声明的变量或函数时,需要提供完整的声明过程,不能值导出表达式,也就是说只能导出语句 - 导出
function声明的函数时,必须采用命名函数,不能采用匿名函数,当然,就算是匿名函数天花板的箭头函数也是不能直接进行导出的
export{}导出
export{}导出语法是:export { ① }
①指要导出的变量名称,这里只能是名称,不能是语句和表达式
看完上面的错误例子,就用朋友想问了,如果只能导出语句,那怎么动态导出变量函数呢?那就要提出export另外一种导出规范了,使用export{}
var a = 5
let b
function fun() { }
let fun1 = () => { }
export {
a, b, fun, fun1
}
这种方式导出只需要在 {} 写上对应的名称就可以了,可能熟悉 ES6 语法的朋友,就会联想到 ES6 中的对象属性简写吧,但是 export{} 和对象属性简写没有写法没有任何关联,所以不要有下面这几种写法出现
export {
a:2
}
export {
fun(){ }
}
var a = 5
export {
a: a
}
import导入
ES6导入语法是:import { ① } from ②
① 指的是导入的参数名称列表
②是要导入模块的路径,可以是相对路径、绝对路径、包名;至于编译器如何区分不同类型模块和如何正确根据地址寻找到模块,这里就不细说了,要不又是长篇大论
// util.js
export var a = 5
export class obj { }
function fun() { }
export {
fun
}
// index.js
import { a, fun, obj } from "./util.js"
不要把 { a, fun, obj } 当做对 a.js 的结构运算,所以对于结构运算的默认值、深度结构等的功能都不能用在这里用
模块对象导出
上面的 import 导入方式有在多变量导入时存在缺陷
- 不知道需要导入变量的名称,就无法使用了
- 导入的变量很多,那就要写很多变量名称,又丑又长
要解决这些问题,可以采用下面的全部入,也就是常说的模块对象
import * as util from "./util.js"
console.log(util.a)
重命名导出和导入
其实在上面的导入全部变量的时候就已经使用了 as 重命名关键字
在使用导入时,导入的变量名碰巧与你使用的变量名发生冲突,就需要重命名
import { a as o1, fun as o2, obj as o3 } from "./util.js"
console.log(o1, o2, o3);
当然重命名之后访问导入变量,就不能再通过原来的变量名称了
导出变量重命名的方式和解决的问题也是有同理的(使用的很少)
var a = 5
export {
a as o1
}
export default默认导入和导出
使用过 Nodejs 的或者 webpack 的朋友应该都知道require模块化
// util.js
exports.a = 5
// index.js
var util = require('./util.js');
CommonJS 可以直接导出一个模块对象,ES6模块化当然也不例外,ES6模块化需要配合使用默认导出 export default
export default导入语法是:export default { ① }
①只能表达式,不能携带声明信息
// util.js
let a = 5
export default a
// index.js
import a from "./util.js"
console.log(a)
其实默认导入也可以理解为
// util.js
let a = 5
export {
a as default
}
// index.js
import { default as a } from "./util.js"
console.log(a)
既然只能导出表达式,那就来几个错误案例吧
export default var a = 5
export default let b = 6
export default const c = 7
export default function fun() { }
export default class obj { }
ES6模块化 是兼容已经存在的 CommonJS 。如果你有一个Node项目,并且你已经执行了 npm install lodash 。你使用 ES6 能够单独导入 Lodash 中的函数:
import {each, map} from "lodash";
简单总结一下,对于上面的几种不同的导入可以多种方式混用,当然导出也是可以的
聚合模块
这里假设场景,在util目录中存放着整个项目的所以工具模块,因为模块文件文件很多,所以每次在使用到来自不同模块的方法时都需要引用多个文件,十分不方便,现在团队老大说写一个集中管理工具模块的主模块,每次只需要导入这个主模块就可以使用所以工具模块导出的变量;
那这个 main.js ,该怎么写喃?
按照我们已知的知识可以这样来实现,在 mian.js 中先导入使用组件,再统一导出使用组件
// a模块
export default {
getName() {
return "名称"
}
}
// b模块
export default {
getAge() {
return "年龄"
}
}
// main.js
import { getName } from "./a.js"
import { getAge } from "./b.js"
export {
getName, getAge
}
在 ES6模块化 还给我们提供了一个更好的方式: export { ① } from ②
这里的 ①和 ②和 import xxx from xxx 所代表的是一样,这里就不赘述了
// main.js
export { getName } from "./a.js"
export * from "./b.js"
这里需要注意
- 在 mian.js 中是无法使用导入变量的,main.js 只是一个模块的中转站
*代表的导入模块中的所以变量,最好不要用,因为名称容易发生冲突- 聚合导入只能处理
export的内容,对于export default无效
ES6模块化的特点
声明提升顶部执行
import 的执行是同步的,并且 import 在运行阶段会提升到代码顶部提前执行,这也代表可以把 import 模块的任意顶层使用,不是顶级的位置不能使用,比如函数体内、判断体内、循环体内等
// util.js
console.log("util模块内");
var a = 5
export default a
// index.js
console.log("index执行");
import { a } from "./util"
> node ./index.js
util执行
index.js执行
可以发现在运行 index.js ,会发现在执行index.js的时候没有首先运行 index.js 内部代码,而是首先执行的是 util.js ,运行了 util.js 内部的代码,无论把 import 写在代码的什么位置,都会在运行阶段提前执行,但是一般还是要把 import 写在顶部,利于知道这个模块导入了那些组件
导入变量是引用值
import导入的模块对象是值的引用,并且无法在模块外对顶层数据进行修改
// util.js
var a = 5
export default a
// index.js
import util from "./util"
util = 6
> node ./index.js
util = 6
^
TypeError: Assignment to constant variable.
控制提示在a=6的位置出现错误,这是因为模块暴露出来的变量只能访问不允许修改的;但是凡事不是绝对的,看一下下面的例子
// util.js
export default {
name: "李白"
}
// index.js
import util from "./util.js"
console.log(util.name);
util.name = "杜甫"
console.log(util.name);
> node ./index.js
util模块内
index.js模块内
其实不难发现导入的模块变量如果是基本类型才不能改变,对于引用类型的属性也是可以改变的,但是一般不这样做
导出值响应
注意:只有使用 export 导出的变量才具有相应变化
当使用 export 导出时,就算导出的 基本类型 ,还是具有 响应变化,也就是在导出模块中海边导出值,会直接影响到导出结果
// util.js
export var a = 2;
setTimeout(() => {
a = 3;
});
// export { a }; 也是可以实现响应
// index.js
import { a } from "./util.js";
console.log("index.js中a的值", a);
setTimeout(() => {
console.log("index.js中a的值", a);
}, 500);
> node ./index.js
mian.js中a的值 2
index.js中a的值 2
mian.js中a的值 3
index.js中a的值 3
export default 导出的值不具有该特性
// util.js
var a = 2;
setTimeout(() => {
a = 3;
});
export default a;
// index.js
import a from "./util.js";
console.log("index.js中a的值", a);
setTimeout(() => {
console.log("index.js中a的值", a);
}, 500);
这里不代表default 不能实现响应,只要通过 ``export` 也是可以实现响应的
// util.js
var a = 2;
setTimeout(() => {
a = 3;
});
export {
a as default
} ;
// index.js
import { default as a } from "./util.js";
console.log("index.js中a的值", a);
setTimeout(() => {
console.log("index.js中a的值", a);
}, 500);
这里 default 也是响应的
// util.js
var a = 2;
setTimeout(() => {
a = 3;
});
export default a;
换成 export default 就不行了
只会执行一次
模块只会在第一次导入的时候执行,并且会且只会执行这一次,以后的导入对于不会再执行模块
// util.js
console.log("util.js执行了");
export default {}
// main.js
console.log("main.js执行了");
import util from "./util.js";
export default {}
// index.js
console.log("index.js执行了");
import main from "./main.js"
import util from "./util.js";
> node ./index.js
util.js执行了
main.js执行了
index.js执行了
就算 util.js 被 main.js 和 index.js 都引入了,但是还是只执行了一次
编译预处理
import会在编译阶段提升到代码顶部,方便在运行阶段提前执行
export会在编译阶段提前暴露当前模块的变量,这也是为什么 export 无法导出匿名的函数和箭头函数的原因,因为 export 在编译阶段就要提前确定模块暴露的变量,所以必须要确定名称,不能匿名或者动态生成的名称
因为是编译阶段发生的事情,这个特点只能依靠循环导入来证明了
// util.js
console.log("util.js执行了");
import { a, getName } from "./index.js";
console.log(a); // undefined
console.log(getName()); // name
export let b = 3
// index.js
console.log("index.js执行了");
import { b } from "./util.js";
console.log(b); // 3
export var a = 3
export function getName(){
return "name"
}
> node ./index.js
util.js执行了
undefined
name
index.js执行了
3
这里可以因为import会在编译阶段预先处理,提升到代码顶端,所以提前执行util.js,在util.js中首先执行import,因为index.js已经进入执行了,所以这里不会再次执行index.js,因为index.js已经提前通过export确定了模块的导出变量,所以继续往下执行,在输出a和getName不会报错,util.js执行完毕就继续执行index.js
按需加载模块
按需加载场景
通过ES6模块化的特点我们了解到import预编译和顶部提前执行,所以是无法动态引入模块的,也就是无法按需引入,无论你是否会使用,都会在代码中最先执行引入;但是在实际开发中,按需引入是也是开发中所必须的,比如在VUE路由中,如果在首屏加载的时候就把所以的路由组件都加载完毕,那么首屏渲染时间是很慢的,在这个时候只需在加载首屏时只加载所需要的路由组件就行!!!
在下文中我会把通过动态import导入的方式称为动态import,而非动态import导入的方式称为静态import
动态import可以理解为模块懒加载,而静态import就是模块的预加载
基本使用
在ES6模块化中还未有这种API,但是我们可以通过webpack来实现这一需求
// util.js
export default {
name: "李白"
}
// index.js
import("./util.js").then(res => {
console.log(res);
})
这里不能通过node去运行代码了,因为按需导入模块是webpack对ES6模块化的扩充,使用必须使用webpack进行打包运行
这里可以发现通过then方法获取的值,也可以推理出动态import采用了Promise的方式获取模块中的值,那就不难发现动态import的特点
- 动态import是异步加载模块获取值,而静态import是同步获取值
- 动态import不会在预提升到模块顶部最先执行,而是代码执行到具体位置再执行加载
不同导出的差异
我相信很多朋该或多或少都使用过这种用法,但是可能不知道使用这种方式导入,具体会带来怎样的效果,那我们先从打包出来的目录来探索吧
首先看看静态import浏览器所加载的文件
// index.js
import util from "./util.js"
可以发现只加载了index.js文件,因为同步import是在编译阶段就进行了模块加载,使用在webpack打包的时候就会预先处理模块直接的关系,打包成一个文件
再看看动态import所加载的文件
// index.js
import("./util.js").then(res => { })
不难看出加载了两个文件,这个是因为动态import需要代码执行到import才进行加载,如果打包成一个文件,那么就失去了使用动态import的意义,如果代码执行不到这一行,还可以减少加载文件
动态文件名(路径)
在讲静态import引入的时候说了,静态import会预加载,所以不能采用动态名称,但是动态import是在代码执行中才去加载的,显然可以动态名称
// index.js
let fileName = "util.js"
import(`./${fileName}`).then(res => {
console.log(res);
})
但是还是有两个注意项
- webpack 不支持完全的动态文件名
let path = "./util.js"
import(path).then(res => {
console.log(res);
})
控制台直接报错,找不到util.js,但是我们可以通过路径找到这个文件的存在,为什么webpack却找不到喃?
webpack 的工作原理是对文件进行静态扫描,然后根据一定规则处理的。webpack 在扫描到动态import语法时,会将路径中的变量转换成正则表达式的 .* ,然后根据这个正则匹配文件名,对匹配上的文件独立chunk模块打包输出。如果文件路径名只有一个变量,那么就是匹配目录下的所有文件,这明显是不合理的,所以 webpack 不支持完全的动态文件名
- 在使用动态变量时,符合webpack打包规则的文件最好都能被使用,否则就浪费打包效率