模块化开发

384 阅读8分钟

模块化是思想

模块化演变过程

stage1 - 文件划分方式

  • 污染全局作用域
  • 命名冲突问题
  • 无法管理模块依赖关系
  • 原始方式完全依靠约定

stage2 - 命名空间方式

  • 创建一个全局的对象,将所有的属性和方法都挂在这个对象上。
  • 可以减少命名冲突问题。

stage3 - IIFE

  • 通过封闭作用域,将想要外界调用的方法和属性,挂载到全局对象上(例:window.)。
  • 模块与模块之间的依赖关系,可以通过参数来进行传递即可。

模块化规范

回顾

  • 在node.js中所有的模块代码必须遵循CommonJS规范
  • CommonJS规范
    • 一个文件就是一个模块
    • 每个模块都有单独的作用域
    • 通过module.exports导出成员
    • 通过require函数载入模块
  • CommonJS是以同步模式加载模块

浏览器端的模块化规范

  • AMD(Asynchronous Module Definition)异步模块化规范(使用起来比较麻烦)
    • require() 用来加载函数
    • define() 用来定义函数
  • Sea.js + CMD
  • ES Modules 在语言层面实现了模块化,更为完善。

ES Modules

ES Modules特性

1.ESM 自动采用严格模式,忽略'use strict'

  • 在非严格模式下,this指向window
  • 在严格模式下,this是undefined

2.每个ES Module都是运行在单独的私有作用域中

3.ESM 是通过CORS的请求外部的JS模块的

4.ESM 的script标签会延迟执行脚本

  • ESM支持:等待网页的渲染之后,再去执行脚本,就不会阻碍页面中元素的显示。
  • 浏览器中一般都是先执行,执行之处有渲染,就执行那个渲染,然后再加载执行后面的代码。

ES Modules导出

第一步:在html中引入ES Module

<script type="module" src="app.js"></script>

第二步:书写js文件

方法1:在声明的前面添加exports

方法2:在最后通过export{},在模块尾部去集中导出的方式。

  • 优点:便于外界理解
// module.js文件
// 通过module生成模块
var nameA = 'mahua';
function hello (){
    console.log('hello');
}

// 集中导出,语法规定必须要有一堆大括号。
export { nameA , hello }
  • 还可以对输出的成员进行重命名,通过as关键词来实现。
// module.js文件
// 通过module生成模块
var nameA = 'mahua';
function hello (){
    console.log('hello');
}

// 集中导出,语法规定必须要有一堆大括号。
export { 
    nameA as nameB, 
    hello 
}
  • 重命名中有一个特殊的情况,当导出的名字重命名为default,这个成员就会作为这个模块默认导出的成员。
exports {
    name as default
}
  • 但是在引用的时候,default是关键字不能使用,因此需要借助as关键字,将其重命名下。

      //---------module.js---------------
      // 通过module生成模块
      var nameA = 'mahua';
      function hello (){
          console.log('hello');
      }
    
      // 集中导出,语法规定必须要有一堆大括号。
      export { 
          nameA as nameB, 
          // 重命名为default的时候
          hello as default
      }
      //---------app.js---------------
      // 导入模块
      // 由于default是关键字,因此不能作为名字来使用,这里需要再次重命名下
      import { default as nameC } from './module.js'
      console.log(nameC);
    
  • default还有一个功能,就是另一种写法。

// 1. 导出
// 此时将default作为该模块默认导出的成员
exports default name;
// 2.导入
// 因为上面导出的格式,因此这里默认的给什么名字,都可以接收到
import nameA from './module.js'
console.log(nameA);

第三步:安装插件(发布到服务器上查看)

  • Browser sync
  • 安装完成后执行命令:browser-sync . --file **/*.js(监控任意文件夹下的所有js文件的变化)

ES Modules导入同上

  • 将导出的exports修改为import即可

ES Modules导入导出的注意事项

ES Modules导出不是字面量对象、解构,其都是固定的语法

// ES Modules导出不是字面量对象、解构,其都是固定的语法
// 语法:export 后面一定会跟着一队花括号
export{ nameA , hello }

//对象字面量
var obj = {
    // ES6中,属性名和属性值相同时,可以只写一个
    nameA,
    // 写两个 nameA : nameA
    hello
}

// 因此想要导出对象字面量,只能使用
export default {name,age}

导出的是引用关系,并不是将值拷贝一份

// --------------module.js--------------
// 通过module生成模块
var nameA = 'mahua';
function hello (){
    console.log('hello');
}

// 一次性延时器
setTimeout(() => {
    nameA = 'haha';
},1000);

// 集中导出,语法规定必须要有一堆大括号。
export { 
    nameA as nameB, 
    // 重命名为default的时候
    hello as default
}

// ----------------app.js--------------
// 导入模块
// 由于default是关键字,因此不能作为名字来使用,这里需要再次重命名下
import { nameB } from './module.js'
setTimeout(() => {
    console.log(nameB);
},1500);

// 最终输出结果:haha

在模块外面暴露出来的引用是只读的,并不能进行修改

// ---------app.js-----------------
// 导入模块
// 由于default是关键字,因此不能作为名字来使用,这里需要再次重命名下
import { nameB } from './module.js'

//想要修改模块内部声明的参数的值,会报错
nameB = 'xiaofang';

ES Modules导入用法(5点)

第一点:路径

  • 必须要写完整的文件名称(包含扩展名)
  • 可使用相对路径中,不能省略路径前方的./,如果省略,程序会以为是在加载第三方的模块。
import {name} from './module.js'
  • 与common.js相同
  • 可以使用绝对的路径或完整的url
import {name} from 'http://localhost:3000/04-import/module.js'

第二点:参数省略

  • 只需要加载模块,不需要提取模块内部的成员
    • 适用:不需要外界调用的子功能模块
      // 引入文件
      //import后面的花括号里面什么都不写,即可
      import {} from './module.js'
      // 简写
      import './module.js'
      

第三点:*号,提取所有

  • 如果需要提取的成员比较多,就可以用*
  • 多个成员放在一个对象中,通过对象打点调用即可。
// import文件
import * as mod from './module.js'
// 调用成员
console.log(mod.name);

第四点:动态导入模块的机制

  • import不能将变量路径进行导入
  • import必须在最外层的作用域中,不能写在if等判断语句内部
  • 那么,此时动态的导入模块,就可以使用import()函数
// import文件
import('./module.js').then(function (module){
  console.log(module);
});

第五点:同时导出匿名成员和默认成员同时导出

  • 演示代码
// import文件
import {name,age,default as title} from './module.js'
// 简写,默认的成员名称可以随便写
import abc,{name,age} from './module.js'
console.log(abc,name,age);

ES Modules 直接导出导入成员

案例

  • 第一个组件js文件
// ---------------button.js---------
export var Button = 'button';
  • 第二个组件js文件
// --------------avatar.js---------
export var avatar = 'avator';
  • index.js:相当于是一个盒子,将所有组件都组合在一起
// ------------index.js-------------
// 通过import配合export,简化app.js文件中的书写
// import {Button} from './button.js'
// import {avatar} from './avatar.js'
// export {Button,avatar}

export {Button} from './button.js'
export {avatar} from './avatar.js'
  • 使用组件的js文件
// ----------------app.js----------
import {Button,avatar} from './index.js'

如果导出的组件中存在default,要对其进行重命名

// -------------button.js-----------
var Button = 'button';
export default Button;

// ------------index.js-----------
// 就可以接着使用了
export {default as Button} from './button.js'

ES Modules浏览器环境Polyfill

  • Polyfill兼容方案(ES Module是2014年提出来的)
  • 工作原理:通过es-module-loader将代码读取出来,再通过babel转换,就可以正常运行了。如果有需要请求的,会通过ajax去请求数据回来,再通过babel转换,之后正常运行。

使用Polyfill

// 不支持promise
<script src = "https://unpkg.com/promise-polyfill@8.2.0/dist/polyfill.min.js"></script>
// 不支持ES Module
<script src = "https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script src = "https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>

为了避免支持module的浏览器执行两次代码,给其添加nomodule

  • nomodule只会在不支持的浏览器上去工作
// 不支持promise
<script nomodule src = "https://unpkg.com/promise-polyfill@8.2.0/dist/polyfill.min.js"></script>
// 不支持ES Module
<script nomodule src = "https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src = "https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>

性能问题

  • 这些方式只适合开发阶段,在生产阶段不要使用,因为他的原理是在运行阶段,动态的去解析脚本,那效率就会非常差。
  • 在真正的生产阶段,我们应该提前将这些代码编译出来,让其可以直接在浏览器中去工作。

ES Modules in Node.js

  • 在node中使用module,需要两步

第一步:修改文件后缀名:mjs

  • index.js -> index.mjs

第二步: node命令行中输入命令

  • node --experimental-modules index.mjs
  • 执行命令后,会显示一个warn(警告),ESM module还在试验阶段,在生产阶段最好不要使用。

案例

  • 可以通过ESM module载入原生的模块

ESM_module载入原生模块.png

  • 也可以载入第三方的模块

  • 不支持,因为第三方模块都是导出默认成员,因此必须要使用默认导入的方式去导入成员

import {camelcase} from 'lodash'
  • 成功提取系统内置模块的成员
  • 开发人员做了兼容,就是1. 对每个模块单独导出一次;2.再把整体作为一个对象,再做一个默认的导出。

ESM_module系统内置模块.png

ES Modules in Node.js 与 common.js交互

ES Module 中可以导入CommonJS 模块

ESM_module可以载入common.js中的成员.png

CommonJS 模块始终只会导出一个默认成员

ESM_Modules只会导出CommonJS模块的一个默认成员.png

CommonJS 中不能导入ES Modules模块

不能再CommonJs中载入ES模块.png

ES Modules in Node.js 与 CommonJs 模块的差异

ESM_Modules与CommonJS的差异.png

ES Modules in Node.js 新版本进一步支持

  • 在命令行切换版本号: nvm use 12.10.0
  • node --version
  • 新版本中,生成一个package.json,在其中写type:module,之后所有的js文件就会以ES Module去工作,也就是说不用改后缀名了。
    • index.mjs -> index.js

    • 文件当中的路径也要修改过来

      package_json.png

    • 此时,执行common.js不能成功,说明还是以ES Mdolues执行的;修改后缀名:.cjs

      commonJS.png

ES Modules in Node.js Babel兼容方案

  • preset就是一组插件,我们需要用到哪个插件,安装哪个就可以。