ES6 |Module 的加载实现

在这里插入图片描述

浏览器加载

传统方法

<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>
  • 由于浏览器脚本的默认语言是 JavaScript,因此type="application/javascript"可以省略
  • 默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
  • 如果脚本体积很大,下载和执行的时间就会很长,因此成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。

异步加载

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
  • <script>标签打开defer或async属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
  • deferasync的区别是:前者要等到整个页面正常渲染结束,才会执行;后者一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的

ES6模块加载规则

<script type="module" src="foo.js"></script>
  • 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。

  • 异步加载,即等到整个页面渲染完,再执行模块脚本,等同于默认打开了<script>标签的defer属性

  • <script>标签的async属性也可以打开

  • ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致

      <script type="module">
        import utils from "./utils.js";
      </script>
      ```
    
    

注意事项

import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
delete x; // 句法错误,严格模式禁止删除变量
  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的

ES6 模块与 CommonJS 模块的差异

两大差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第一个差异

  • CommonJS 模块输出的是值的拷贝,即一旦输出一个值,模块内部的变化就影响不到这个值。

    // 模块文件——lib.js
    var counter = 3;
    function incCounter() {counter++}
    module.exports = {
      counter: counter,
      incCounter: incCounter,
    };
    
    // 加载模块——main.js
    var mod = require('./lib');
    console.log(mod.counter);  // 3
    mod.incCounter();	
    //执行了函数counter也不会改变
    //这是因为counter是一个原始类型的值,会被缓存。
    console.log(mod.counter); // 3
    
    //一种办法就是把counter写成函数
    get counter() {
      return counter
    },
    
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    // lib.js
    export let counter = 3;
    export function incCounter() {
      counter++;
    }
    
    // main.js
    import { counter, incCounter } from './lib';
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
    

    由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错

    // lib.js
    export let obj = {};
    
    // main.js
    import { obj } from './lib';	
    obj.prop = 123; // //变量obj指向的地址是只读的,不能重新赋值
    obj = {}; // TypeError
    

第二个差异

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

Node 加载

Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。解决方案是:将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。

在静态分析阶段,一个模块脚本只要有一行importexport语句,Node 就会认为该脚本为 ES6 模块,否则就为 CommonJS 模块。

如果不输出任何接口,但是希望被 Node 认为是 ES6 模块,可以写成如下代码。这不是输出一个空对象,而是不输出任何接口的 ES6 标准写法

export {};

如何不指定绝对路径,Node 加载 ES6 模块会依次寻找以下脚本,与require()的规则一致

import 'baz';
// 依次寻找
//   ./node_modules/baz.js
//   ./node_modules/baz/package.json
//   ./node_modules/baz/index.js
// 寻找上一级目录
//   ../node_modules/baz.js
//   ../node_modules/baz/package.json
//   ../node_modules/baz/index.js
// 再上一级目录

import 命令加载 CommonJS 模块

Node 采用 CommonJS 模块格式,模块的输出都定义在module.exports这个属性上面。

在 Node 环境中,Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default,使用import命令加载该 CommonJS 模块时,module.exports会被视为默认输出

// CommonJS 模块
// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
};

// 在node环境中,自动当作模块的默认输出,等同于
export default {
  foo: 'hello',
  bar: 'world'
};

如果采用整体输入的写法(import * as xxx from someModule),default会取代module.exports,作为输入的接口

// c.js
module.exports = function two() {
  return 2;
};

// es.js
import foo from './c';
foo(); // 2

import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function
//bar本身是一个对象,不能当作函数调用,只能通过bar.default调用

CommonJS 模块的输出缓存机制,在 ES6 加载方式下依然有效

// foo.js
module.exports = 123;
setTimeout(_ => module.exports = null); //一直都会是123

由于 ES6 模块是编译时确定输出接口,CommonJS 模块是运行时确定输出接口,所以采用import命令加载 CommonJS 模块时,不允许采用下面的写法。

import {readfile} from 'fs';
//因为fs是 CommonJS 格式,只有在运行时才能确定readfile接口,而import命令要求编译时就确定这个接口

解决方法就是改为整体输入

import * as express from 'express';
const app = express.default();

import express from 'express';
const app = express();

require 命令加载 ES6 模块

采用require命令加载 ES6 模块时,ES6 模块的所有输出接口,会成为输入对象的属性。

// es.js
let foo = {bar:'my-default'};
export default foo;
foo = null;

// cjs.js
const es_namespace = require('./es');
console.log(es_namespace.default);
// {bar:'my-default'}
//default接口变成了es_namespace.default属性。另外,由于存在缓存机制,es.js对foo的重新赋值没有在模块外部反映出来。

ES6模块的转码

浏览器目前还不支持ES6模块,为了现在就能使用,可以将转为ES5的写法。除了Babel可以用来转码之外,还有以下两个方法,也可以用来转码。

ES6 module transpiler

ES6 module transpiler是 square 公司开源的一个转码器,可以将 ES6 模块转为 CommonJS 模块或 AMD 模块的写法,从而在浏览器中使用

首先,安装这个转码器。

$ npm install -g es6-module-transpiler

然后,使用compile-modules convert命令,将 ES6 模块文件转码。

$ compile-modules convert file1.js file2.js

-o参数可以指定转码后的文件名

$ compile-modules convert -o out.js file1.js

SystemJS

另一种解决方法是使用 SystemJS。它是一个垫片库(polyfill),可以在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。

使用时,先在网页内载入system.js文件。

<script src="system.js"></script>

然后,使用System.import方法加载模块文件。

<script>
  System.import('./app.js');
</script>

上面代码中的./app,指的是当前目录下的app.js文件。它可以是ES6模块文件,System.import会自动将其转码。

需要注意的是,System.import使用异步加载,返回一个 Promise 对象,可以针对这个对象编程。下面是一个模块文件。

// app/es6-file.js:

export class q {
  constructor() {
    this.es6 = 'hello';
  }
}

然后,在网页内加载这个模块文件。

<script>
System.import('app/es6-file').then(function(m) {
  console.log(new m.q().es6); // hello
});
</script>

ESLint的使用

ESLint是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。

首先,安装ESLint。

$ npm i -g eslint

然后,安装Airbnb语法规则。

$ npm i -g eslint-config-airbnb

最后,在项目的根目录下新建一个.eslintrc文件,配置ESLint。

{
  "extends": "eslint-config-airbnb"
}

现在就可以检查,当前项目的代码是否符合预设的规则。

index.js文件的代码如下。

var unusued = 'I have no purpose!';

function greet() {
    var message = 'Hello, World!';
    alert(message);
}

greet();

使用ESLint检查这个文件。

$ eslint index.js
index.js
  1:5  error  unusued is defined but never used                 no-unused-vars
  4:5  error  Expected indentation of 2 characters but found 4  indent
  5:5  error  Expected indentation of 2 characters but found 4  indent

✖ 3 problems (3 errors, 0 warnings)

上面代码说明,原文件有三个错误,一个是定义了变量,却没有使用,另外两个是行首缩进为4个空格,而不是规定的2个空格。

posted @ 2020-09-01 14:02  sanhuamao  阅读(188)  评论(0编辑  收藏  举报