ES6+ 模块化

1. 前言

JavaScript 在设计之初主要用来开发 Web 页面的交互、动画和表单验证等单一的功能,而且程序的体积很小,大多数都是独立执行的。随着前端的发展 JavaScript 承接的功能越来越多,Node 的出现让 JavaScript 可以作为一门后端开发语言,程序的复杂度瞬间提升,所以有必要提供一种将 JavaScript 程序拆分为可按需导入的单独模块的机制。Node 是 JavaScript 的先行者,它使用了 CommonJS 的规范来实现模块化的。在 ES6 没出来之前有很多模块化的实践,比较有名的有:CommonJS、AMD、CMD,每个规范都有自己独立的思考。

随着 ES6 模块的发布,AMD 和 CMD 慢慢地淡出了我们的视野。现在主要常见的场景是 Node 端还采用 CommonJS 规范,这是历史原因。前端使用的是 ES6 module 规范,但是不能直接在前端使用,需要通过打包工具进行编译如:Webpack、Babel、Rollup 等。本文中我们将使用 Webpack 进行模块化编译工具,源代码放在 GitHub 上,仅供参考。

2. 环境搭建

现在的高级浏览器还能完全地支持 ES6 的模块化,如何在浏览器中运行 ES6 模块呢?有两种方式:

  • 在浏览器中直接运行 ES6 的模块化,但是需要做一定的工作,不能像之前直接在本地浏览器中打开一个 html 中引入 JS 文件;
  • 使用 Webpack、rollup 等模块化打包工具,html 引入编译后的 js 文件。

这两种方式各有优缺点,第一种是原生的使用方式,浏览器兼容要求比较高,第二种使用的是第三方打包编译工具可以很好地解决浏览器兼容问题,但是会有一定的学习成本,并且不能直接在浏览器中运行,只能使用编译后的文件。

2.1 浏览器运行原生 ES6 模块

使用浏览器运行原生 ES6 模块的源码在 ES6-wiki 的 mjs 文件中,浏览器是不能直接运行 ES6 模块化的,需要做一些准备工作。

首先,在引入 js 文件时需要定义 script 的类型:type="module" 。另外,js 文件的后缀不能使用 .js 了,需要使用 .mjs 。这样还是不能在浏览器中运行,还需要最后一步。模块化会涉及到文件系统,而本地打开的 html 文件是没有服务的,所以我们要使用 node 服务的方式打开 html 文件,这里我们使用 node 的包 http-server 可以在相应的文件目录中启动 node 服务。安装如下:

npm install --global http-server

安装完启动服务的工具还是会有问题,依然打不开,这是需要在浏览器中打开一些配置:浏览器地址栏输入:chrome://flags/ 然后搜索 JavaScript 把 Experimental JavaScript 项选择 Enabled 启用状态。如下图。

图片描述
到这里我们就把前期的工作做完了,如何打开 html 文件呢?在控制台中进入对应的目录中执行:http-server 命令。本节的目录在 ES6-wiki/packages/module/mjs 下。在浏览器打开控制台返回的地址即可,本实例的地址是:http://127.0.0.1:8080/index.html

图片描述

2.2 使用 Webpack

Webpack 是模块化打包工具,它兼容现在很多模块化加载方式,本节课程也主要使用 Webpack 的方式来学习 ES6 的模块化。Webpack 需要一定的学习成本可以在官网 上学习,这里就不进行介绍了,下面给出 webpack.config.js 的配置如下:

let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html' // 模版文件
    })
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },
};

3. 基本使用

ES6 的模块化设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,只能在顶级作用域。模块系统中,每个文件都是一个模块,模块之间都是相互独立。在 ES6 模块中自动采用 严格模式 ,不知道的同学可以去看看,对于学习 JavaScript 语言有一定的帮助。

3.1 export/import

export 是导出语法,import 是导入语法。看下面的实例:

// a.js
export let x = 1;
export let y = 2;

// main.js
import {x, y} from './a.js';
console.log(x, y)

上面代码中,a.js 文件中使用 export 导出 x 和 y 两个变量,在 mian.js 文件中使用 import 进行导入。a.js 中还可以使用对象的方式导出:

let a = 1;
let b = 2;
export {
	a,
  b,
}

从上面的 main.js 文件中可以看出,export 使用的是引用方式进行导出的,导出的是一个接口,所以不能直接导出一个值。我们如下实例:

let a = 1;
export a;	// 编译报错
// 正确的方式如下
Export let a = 1;

虽然使用 export 不能直接导出一个值,但是可以使用 export default 导出一个特定的值:

export default 100;

export 模块导出的是一个接口,在模块内如果数据更新,则所依赖的地方的值都是最新的。

// a.js
let a = 1;
setInterval(() => {
  a++
})
export {
	a
}

// main.js
import { a } from './a.js';
setInterval(() => {
  consolog.log(a)
})

import 有声明的特点,类似 var 的特点,可以实现变量提升,但是不能修改变量对应的值。

// main.js
console.log(a)
import { a } from './a.js';
a = 100;	// 这样赋值是错误的

使用 export + from 命令的方式,提供了一种便捷的方式在当前的模块导出其他模块的内容,不能在当前模块下使用导出的变量。

// b.js
let a = 1;
let b = 2;
export {
	a,
  b,
}

// a.js
export {a,b} from './b.js';
export c = () => {}
// 等价于使用import 先导入,然后再使用 export 导出
import { a, b } from './b';
export {
	a,
  b,
}

// main.js
import {a, b, c} from './a.js'

export 和 import 命令规定要处于模块顶层,一旦出现在块级作用域内,就会报错。

// a.js
{
	export let a = 1;
}

// main.js
{
  import { a } from './a';
}
//控制台答应错误内容: 'import' and 'export' may only appear at the top level

上面的代码中 export 和 import 都放在块级作用域中的,执行时会报错,提升你 export 和 import 只能在顶级出现。

3.2 export default 命令

export default 命令用来导出模块中默认变量或方法,上面我们也提到了使用 export 导出的是一个对象不能导出一个值类型。

// a.js
export default 'imooc';
// main.js
import name from './a.js'
console.log(name);	// imooc

export default 命令声明的函数可以是匿名的。

export default function () {
  console.log('imooc');
}
// 等价
function fn() {
  console.log('imooc');
}
export default fn;

也可以是一个类:

// a.js
export default class {
  constructor() {
    console.log('imooc')
  }
  // ...
}

// main.js
import A from './a';
const a = new A();
console.log(a)

开可以导出的是一个对象:

const obj = {
  name: 'imooc',
  getLession: function() {
		console.log('ES6 imooc'); 
  }
}
export default obj;

3.3 as 命令

as 命令是用来重命名的,在使用 import 命令导入时可以使用 as 来更改变量名。

// a.js
let a = 1;
let b = 2;

export {
	a,
  b,
}

// main.js
import { a, b as y } from './a';
console.log(a, y);	// 1,2

如果模块中同时有 export default 导出和 export 导出时,在导入时可以使用 as 对默认导出进行重命名。

// a.js
let a = 1;
let b = 2;

export {
	a,
  b,
}

export default let c = 3;


// main.js
import { a, b, default as c } from './a'
// 等价于下面直接在外面进行使用
import c, { a, b } from './a'

默认导出的内容也可以放在 export 导出的对象中,但是需要使用 as default 命令:

// a.js
let a = 1;
let b = 2;
let c = 'imooc';

export {
	a,
  b,
  c as default,	// 相当于 export default 'imooc',给导出的对象增加一个default属性
}

4. 小结

本节主要讲解了 ES6 Module 的使用,通过对 export、import、default、as 命令的讲解学习了 ES6 Module 的基本用法,基本上涵盖了日常使用的场景。