第8篇——Module的语法和加载实现

Module概述

  • 历史上,JavaScript一直没有模块系统,没法将一个大程序拆分红相互依赖的小文件,再用简单的方法拼装起来,这给前端工程化的开发带来了很大的阻碍。
  • 在ES6以前,前端社区指定了一些前端模块加载方案,主要有commonJS and AMDcommonJS用于服务器,在node.js中就是很好的体现,AMD主要用于浏览器,ES6在语言标准的层面上实现了模块功能,并且实现的很是简单,彻底能够取代commonJS and AMD 成为先后端通用的模块解决方案
  • 设计思想
    • 尽可能的静态化,在编译的时候就能肯定各模块之间的依赖关系
    • CommonJS and AMD 则都是在运行的时候肯定这些东西的
    • commonJS 模块是对象,咱们能够经过结构赋值的方式引出
let {stat, exists, readFile} = require('fs')
// 等同于
let _fs = require('fs')
let stat = _fs.stat;
let exists = _fs.exists;
let readFile = _fs.readFile;
复制代码
  • 上面代码的实质就是总体加载fs模块,生成一个对象_fs,而后在从这个对象上读取三个方法,这种加载方式称为运行时加载,由于只有在运行是才能获得这个对象,致使的问题就是在编译时彻底不能作静态优化
  • ES6模块不是对象,而是经过export指定输出的代码,再经过import的方式引入
  • import {stat, exist, readFile} from 'fs'这种方式是经过fs模块加载三个方法,其余的方法不加载,这种方式叫作“编译时加载”,或者叫作运行时加载,即ES6是在编译的时候加载模块,这样效率要比commonJS的效率高,
  • ES6的模块自动采用严格模式,无论咱们在文件最前面加不加‘use strict’默认就是严格模式
  • 严格模式的限制:
    • 变量必须声明以后才能使用
    • 函数的参数不能有同名的,不然报错
    • 不能使用with语句
    • 不能对只读属性赋值,不然报错
    • 不能使用前缀0表示八进制,不然报错
    • 不能删除不可删除的属性,不然报错
    • arguments不能被从新赋值
    • 不能使用arguments.callee
    • 不能使用arguments.caller
    • 禁止this指向全局变量,特别注意的是顶层的this是指向undefined,而不是window

export命令

  • export主要用于规定模块的对外接口,import命令主要用于输入其余模块提供的功能
  • 一个模块就是一个独立的文件,文件内部的变量其余外部的模块是没法使用的,若是模块内部的某个变量须要外部读取,这是咱们必须使用export命令
export const firstName = "GeekChenYi"
export const lastName = "hello world"
export const year = 2019
// 除了上面的这种写法外,还能够是下面的这种写法
const firstName = "GeekChenYi"
const lastName = "hello world"
const year = 2019
export {firstName, lastName, year};// 优先使用这种方法,清晰明了
// 输出函数的方法
export function show(){}
export () => {}
// 使用as重命名
const v1 = function(){};
const v2 = function(){};
const v3 = function(){};
export {
    v1 as stream1,
    v2 as stream2,
    v3 as stream3
}
复制代码
  • 须要特别注意的是,export提供的是对外的接口,必须与模块内部的变量创建一对一的关系,不然就会报错
  • export语句输出的接口与其对应的值是动态绑定的关系,即经过该接口就能够去到模块内部的值
  • commonJS则输出的是值的缓存,不存在动态更新

import命令

  • 使用export命令导出接口,其余的js文件就能够经过import命令加载这个模块
// 注意这地方,其实也是一个解构赋值的写法
import { firstName, lastName, year} from 'profile.js'
// 须要注意的是,大括号里面的变量名必须和被导入模块中的变量名保持一致
// 也能够经过as 进行重命名
import {firstName as name} from 'profile.js'
复制代码
  • import命令输入的变量是只读的,若是直接修改会报错
  • import命令具备提高效果,会把变量提高到模块的最前面
  • 因为import是静态执行,所以不能使用表达式和变量
// 报错
import { 'fo' + 'o'} from 'module_name'
// 报错
let module = module_name;
import {foo} from module;
// 报错,由于在编译阶段是不会执行if语句的
if(x > 1){
    import { foo} from module1;
}else{
    import {foo1} from module2;
}
复制代码
  • import loadsh这种方法只是加载模块 不输出任何值
  • 模块除了加载指定的方法,也能够实现模块的总体加载,import * as cricle from cricle.js

export default命令

  • 有上面的import命令咱们能够知道,咱们在加载模块的时候,必需要知道加载模块名字,不然是没法加载的,为了使用方面,可使用export default function(){} 为模块指定默认的输出,其余模块加载该模块的时候,能够经过匿名的方式实现加载,可是import的匿名是不能使用{}
  • 一个模块只能有一个默认输出,所以只有一个export default,这也是为何import后面的命令不须要加{}的缘由
// export 的本质区别
export const name = "Geek" // 以前的正确写法
const name1 = "hello world"
export default name1;// 这种方法是正确的写法
// 本质
export {name1 as default}

// 错误的写法
export default const name2 = "hello world"
// 正确的写法
export default 43
// 错误的写法
export 43 // 它输出的是接口
复制代码
  • export defalut 的本质就是将后面的值赋值给default变量
  • export和import的复合写法
export {foo, bar} from module_name;
// 等价于下面的写法
import {foo, bar} from module_name;
export {foo, bar}
// 具名接口改写为default
export {foo as default} from module_name1
<==>
import {foo} from module_name1
export default foo
复制代码
  • 跨模块常量,若是咱们使用的常量是很是的多,这是咱们能够创建一个专门用来存放常量的js文件
// constants.js文件
export const A = 1;
export const B = 2;
export const C = 3;
export const D = 4;
// main.js文件
import * as constants from './constants.js'
console.log(constants.A);
console.log(constants.B);
// main1.js
import {A, B, C, D} from './constants.js'
console.log(A);
console.log(B);
console.log(C);
console.log(D);
复制代码
  • 因为import不能实现动态的加载,也就是说只有在条件成立的时候,我才开始加载,这也给import形成了一个很很差的效果,由于有时候咱们可能只在开发环境下导入某些模块,在生产环境下咱们是不须要加入的,例如:mock.js。为了解决这个问题,最新的提案是引入import()这个函数
// 这种判断的方式是在有在开发环境下才引入咱们mock.js,而在生成环境下,咱们就不须要引入了
if(process.env.NODE_ENV == 'development'){
    import('mock.js')
}
复制代码
  • import()函数返回的是一个promise对象,它能够运行在任何的地方,由于它是在运行的时候加载。import()相似于Node.js的require(),他们之间的区别就是:import()返回的是一个promise对象,是异步的,而requre()是同步的
  • import()使用场景:
    • 按需加载:在须要的时候只加载某个模块
    • 条件加载:只有判断条件成立的时候加载某个模块
    • 动态的模块路径:容许模块路径动态的生成
  • import()使用注意点:
    • import()加载成功以后会做为一个对象,成为then()放的的参数,所以可使用解构赋值的形式,获取须要的接口
    • 若是模块有default输出接口,这是可使用参数直接获取
    • 也可使用在async中
// import()作为then()方法的参数
import('./module').then(({export1, export2}) => {
    // ...
})
// 若是模块有default输出命令,
import('./module').then(myModule => {
    // some code
})
// 同时加载多个模块
Promise.all([
    import('./module1.js'),
    import('./module2.js'),
    import('./module3.js')
]).then(([module1,module2, module3]) => {
    // some code...
})
// async函数中的使用
async function main(){
    const myModule1 = await import('./module.js');
    const {export1, export2} = await import('./module.js');
    const [myModule1, myModule2, myModule3] = await Promise.all([
        import('./module1.js'),
        import('./module2.js'),
        import('./module3.js')
    ])
}
main()

复制代码

Module的加载实现

  • 传统方法: 只是将js脚本经过script标签引入的方式加载,可是这有一个问题就是若是存在多个脚本的时候,加载方式是同步加载,前一个加载未结束后面的脚本就不会加载,解决办法就是实现异步加载
<script src='example1.js' async><script>
<script src='example1.js' defer><script>
// 它们二者之间的区别
async: 只要加载完毕就会当即执行
defer: 加载完毕等到整个页面下载完毕才会执行
若是出现多个加载脚本,defer会按照页面中出现的顺序执行的,可是async就不能确保了
复制代码
  • 加载规则
    • 浏览器加载ES6模块的时候也是使用script标签,可是type='module'例如:<script type='module' src='example.js'></script> 这段代码表示加载一个名字为example.js的模块
    • 浏览器加载ES6模块的时候,默认就是异步实现的,并且默认是defer的形式
    • 对于外部的模块脚本加载注意点:
      • 代码是在模块做用域中运行的而不是在全局做用域中运行的,模块内部的顶层变量外部是不可见的
      • 模块脚本自动采用严格模式
      • type='module'的模块之中可使用import命令加载其余的模块,可是文件的后缀.js是不能省略的,路径必须是相对路径或者绝对路径,也可使用export输出对外的接口
      • 在模块中顶层的this关键字放回的undefined,而不是window
      • 同一个模块若是加载屡次将会只执行一次,特别注意
  • CommonJS和ES6模块之间的差别
    • CommonJS模块输出的是一个值的复制,而ES6模块输出的是一个值的引用
    • CommonJS模块是运行时加载,ES6模块是编译时输出对外的接口
    • CommonJS只因此时运行时加载,由于它导出的是一个对象,经过module.exports该脚本只会在脚本运行结束的时候生成,而ES6的模块是对外的接口是在编译的时候就会生成
  • Node加载
    • Node有本身的commonJS格式,与ES6模块的格式是不兼容的,目前的解决方案是将二者分开,ES6模块和commonJS采用各自的加载方式。
    • 目前掌握一种办法就是在Node中继续使用commonJS的规范,而在前端工程化中使用ES6的模块方案
  • CommonJS的加载原理:
    • commonJS的一个模块就是一个脚本文件,require命令第一次加载这个模块的时候就会执行这个脚本,而后在内存中生成一个对象
    • 因为ES6输入的是模块变量只是一个“符号连接”,因此这个变量是只读的,对它进行从新的赋值会报错
    • ES6的模块中,顶层的this指向undefined,CommonJS模块的顶层this指向当前模块,这是二者之间的重要差距
// 1.例如在main.js中加载foo.js模块
const temp = require('./foo.js');
// commonJS的原理:内存中生成一个对象
{
    id: foo.js, // id属性是模块的名字
    exports: {
        // 模块输出的各个接口都是存在这个对象中的
    },
    loaded: true, // 返回一个布尔值,表示该模块的脚本是否执行完毕
    ... 还有一些不重要的属性,这里省略了
}
// 2.下面的这种方法会报错
// lib.js
export let obj = {};
// main.js
import {obj} from './lib.js' // 后面的后缀是能够省略的
obj.prop = 123; // OK
obj = {}; // 报错

// 3.commonJS中模块加载的寻找方式
1.const foo = require('./foo')
//依次寻找
// ./foo.js
// ./foo/package.json
// ./foo/index.js
2.const baz = require('baz')
// 依次寻找
// ./node_modules/baz.js
// ./node_modules/baz/package.json
// ./node_modules/bar/index.js
// 找不到开始寻找上一级目录
// ../node_modules/baz.js
// ../node_modules/baz/package.json
// ../node_modules/baz/index.js
// 找不到再向上一级查找
复制代码
  • commonJS中exports和Module.exports的区别:
    • exports是模块对象Module.exports的一个属性
    • Module.exports才是真正的接口,也就是说最终返回给调用者的是Module.exports而不是exports
    • 全部的exports收集到的属性和方法最终都是赋值给了Module.exports
    • 若是赋值的方法和属性Module.exports有,则会忽略exports的方法和属性的
相关文章
相关标签/搜索