前端模块化(ES6和commonjs)

334 阅读5分钟

什么是模块?

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

模块化进程

  • 全局function模式
    • 不同功能封装成全局函数
    • 问题:污染全局命名空间,容易引起命名冲突,且模块成员之间看不出关系
function f1 () {
  console.log('f1');
}

function f2 () {
  console.log('f2');
}

image.png

  • namespace模式
    • 模块间有了各自的命名空间,避免了命名冲突
    • 问题:模块内的数据会被外部修改
const Module = {
  data: '模块化',
  f1 () {
    console.log('module.f1' + this.data);
  },
  f2 () {
    console.log('module.f2' + this.data);
  }
}

Module.data = '修改了'
Module.f1()

image.png

  • IIFE模式(函数自调用)
    • 将数据和行为封装到函数内部,通过给window添加属性向外暴露接口,数据私有外部只能通过暴露的方法操作。
    • 问题:如果模块依赖别的模块怎么办?
(function (window) {
  let data = 'IIFE模式'
  function f1 () {
    console.log('IIFE模式f1' + data);
  }
  function f2 () {
    f3()
  }
  function f3 () {
    console.log('IIFE模式f3' + data);
  }
  window.IIFE = { f1, f2 }
})(window)

IIFE.f1()
IIFE.data = '修改了'
IIFE.f2()

image.png

模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

引入多个<script>后出现出现问题

  • 请求过多
  • 依赖模糊
  • 难以维护

模块化规范

1.CommonJS

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

特点

  • 所有代码运行在模块作用域,不会污染全局作用域
  • 模块加载的顺序按照代码中出现的顺序
  • 模块可以多次加载,但只会在第一次加载运行,之后只会返回缓存结果。如果想再次运行需要清除缓存

基本语法

  • 暴露模块:module.exports = valueexports.xxx = value
  • 引入模块:reuqire('xxx')

每个模块内部module代表当前模块,这个模块是一个对象,它的exports属性是对外的接口。加载某个模块实际是加载模块的module.exports属性

const obj = {
  a: 1,
  b: 2,
  c: {
    a: 3
  }
}

let com = 4
const exFunc = () => {
  com++
  obj.b++
  obj.c.a++
  console.log(com, obj.b, obj.c.a);
}

module.exports = { exFunc, com, obj }
const im = require('./模块化.js')
im.obj.a = 2
im.com = 10
im.exFunc()

image.png 可以看到commonjs输出的是值的拷贝(浅拷贝)

2.ES6模块化

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

语法

export用于规定对外暴露的接口,import引入其他模块提供的功能

const obj = {
  a: 1,
  b: 2,
  c: {
    a: 3
  }
}

export let com = 4
const exFunc = () => {
  com++
  obj.b++
  obj.c.a++
  console.log(com, obj.b, obj.c.a);
}

export default { exFunc, com, obj }
import module2 from './模块化.js'
import { com } from './模块化.js'
module2.exFunc()
console.log(module2.com);
console.log(com)
module2.com = 10
console.log(module2.com);

image.png

可以看到export defaultexport导出的com在更改以后的值并不一样,因为 ES6模块输出的是值的引用,js引擎解析代码时遇到模块import生成一个只读引用,等到脚本执行的时候根据只读的引用到被加载的模块里取值。所以模块内部修改了com的值,export的com值也会相应的改变。而通过export default导出的com可以理解为将变量赋值给了default,基本数据类型com赋值给default做了一层拷贝,所以模块内部修改com的值并不会改变deafult里的com

import()动态加载

import是加载的模块都是静态的,但是ES同时也提供一种动态加载的方式,即import()函数。允许您仅在需要的时候加载模块而不必提前加载所有模块。

    function add() {
      modules.exFunc()
      if(com === 7) {
        import('./模块化2.js').then(res => {
          console.log(res);
        })
      }
    }
    document.getElementById('btn').onclick = add
    async function add() {
      modules.exFunc()
      if(com === 7) {
        const res =  await import('./模块化2.js')
      }
    }
    document.getElementById('btn').onclick = add

import()导出的是一个promise对象,所以可以通过.then或者await的方式来获取导出结果,需要注意的是如果是默认导出的方式,那么得到的结果实际是包含在default属性中的。export导出的结果可以通过解构来获取。

image.png

image.png

路由懒加载

懒加载前提:进行懒加载的子模块(子组件)需要是一个单独的文件。在需要用到组件的时候通过import()把组件动态引入

调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。
——摘自《webpack——模块方法》的import()小节methods/#import-)

也就是说import()导入的子模块被分离出来打成单独的chunk,当然这还不足以实现懒加载,还需要借助函数导出的方式导出import()模块,在需要的地方调用函数引入模块。

const routes = [{
    path: '/',
    name: 'Home',
    // 将子组件加载语句封装到一个function中,将function赋给component
    component: () => import( /* webpackChunkName: "home" */ '../views/Home.vue')
  }
]

这是我们在vue路由中懒加载的常见写法,可以看到就是利用了import()来动态加载相应的路由组件并通过函数赋值给component属性。

总结

  • CommonJS模块是运行时加载,ES6模块是编译时加载
    • ES6在编译的时候就确定了模块间的依赖关系,输入和输出的变量,对外的接口在代码静态解析阶段就生成。所以import必须在顶层作用域上,if(xx) {import xx from 'xxx'}是不能通过编译的。所以
    • CommonJS加载一个对象,对象只有在脚本运行完才生成,所以可以在任何地方引入CommonJS的模块
  • CommonJS模块输出是值得拷贝(浅拷贝),ES6模块输出的是值的引用
  • CommonJS是同步加载模块,ES6是异步加载模块

结语

这里只总结了一小部分,之后会总结其他的部分。写的不好,各位大佬多多指教,感恩家人🙏