从Script开始理解模块化

60 阅读4分钟

关键字:script,ESModule,commonjs

前端发展至今已有十来年,充当的角色也变化很大,从web1.0时代简单的点击跳转到web2.0时代的万物互联;前端变得越来越不可或缺。

随着业务越来越大,不可能把所有代码写在一个地方了,毕竟变量名不注意就取重了😅,这个时候就必须模块化,把每个功能隔离开来,变量互不影响,一个模块一个模块把业务搭建起来。

什么是模块化

将一个大的项目分成有限几块,每个小块相互独立又相互依存,每个小块对外暴露自己的一些功能供其他小块使用

举个例子:网购桌子商家不会给你发完整的桌子,而是把桌子拆成桌子腿,桌板等小块,桌子腿和桌子腿是独立的,但会暴露顶部给螺丝拧紧,这就是模块化

模块化解决了什么问题

  1. 全局作用域:隔绝了命名冲突和作用域污染问题
  2. 自动化测试:每个模块都可以单独测试,无需从头到尾
  3. 性能优化:模块可以重复使用,减少打包体积

模块化1——Script

普通函数引用

通过script引入不同的文件来实现变量的引用,如以下代码,虽然实现了test模块中的代码,但是却没有隔绝作用域,造成了污染;尽管可以通过不同变量名或者多套几层的方式避免污染,但本质上是一样的,无法隔绝

// test.js
function getName(){
    name = '张三'
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>模块化</title>
</head>
<body>
    <script src="test.js"></script>
    <script>
        var name = '李四'
        getName()
        console.log(name) //张三
    </script>
</body>
</html>

// 多套几层
function getName(){
    var my_test_name = {
      name = '张三'
    }
}

立即执行函数引用

通过闭包的方式将变量控制在name函数作用域中,再通过window挂载将getName函数暴漏出来。

这种方式对于一些简单的项目是可以的,但是当业务复杂起来,一大连串的立即执行函数是非常难理解的

// iffe.js
(function name(){
    var age = 12
    window.getName = function getName(){
        var name = '张三'
    }
})()
console.log(age) //error

  
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>模块化</title>
</head>
<body>
    <script src="iffe.js"></script>
    <script>
        var name = '李四'
        getName()
        console.log(name) //李四
    </script>
</body>
</html>

模块化2——Commonjs

****通过<script>标签引入代码会显得杂乱无章、人们只能通过命名空间等方式人为限制约束代码、以达到安全的目的。

经历十多年的发展、社区为JavaScript制定了相应的规范、其中CommonJS规范就是最重要的里程碑

规范

commonjs的规范很简单,就三点

  1. 模块定义
  • Commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为module
  1. 模块引用
  • Commonjs规范中存在require()方法、参数为模块标识、以此引入一个模块到当前上下文中。
  1. 模块标识
  • 标识就是传递给require()方法的参数、必须符合小驼峰命名的字符串、或者以 .、 ..开头的相对路径或者绝对路径。

导出与引入

举个例子

var a = 1;

setTimeout(() => {
  console.log("异步:", a); // 异步: 1
}, 2000);
exports.a = a;
const AA = require('./module/a.js')
AA.a = 2
console.log(AA) // { a: 2 }

上面的例子我们可以得出几个结论

  1. 通过exports 导出,exports是一个对象
  2. 通过require 导入,导入的是一个对象
  3. 导入之后修改对象属性值,不会影响 模块内的 值

动态同步加载

require 可以在任意的上下文,动态加载模块,会阻塞进程

let index = 1
while(index <2) {
    console.log("开始")
    const a = require('./module/a')
    console.log("index:",a) // {a:1}
    console.log("结束")
    index++
}

// 开始
// index: { a: 1 }
// 结束
// 异步: 1

模块化3——ESModule

ESModule 规范是基于文件的,每个文件都是一个独立的模块。在浏览器中,可以使用<script type="module">标签来加载 ESModule 模块。在 Node.js 中,可以使用 import 关键字来加载 ESModule 模块。

在nodejs环境中必须在package.json 中设置type:"module",node版本最好在14以上

规范

  1. ESM 模块输出的是值的引用
  • 通过export 或者 export default 导出
  1. ESM 模块是编译时输出接口
  • 不能再块级作用域中运行,因为不是运行时加载的
  1. ESM 支持异步加载
  • 不阻塞进程

我们根据以上规范,举个例子证明

导出与引用

import {obj} from './esmodule/a.js';
obj.name = 'jack'
console.log(obj)
export const obj = {
    name:'linda'
}
setTimeout(() => {
console.log("aaa:",obj.name) // jack
}, 2000);

通过import 引入值后修改,原有的值也修改,说明是同一个值,是值的引用


let aa = 1
if(a) {
    import {obj} from './esmodule/a.js'; //error 
    obj.name = 'jack'
    console.log(obj)
}

异步加载

import 返回的是一个promise,返回then的是一个异步函数

const obj = 111
console.log(import('./esmodule/b.js').then(res => {res.asyncFun()}))
console.log(obj)

// Promise { <pending> }
// 111
// async
 const asyncFun = () =>{
    console.log("async")
 }

 export {
    asyncFun
 }

总结

除了上面这三种模块化,还有AMD,CMD;但基本没用到过,这里不做阐述,主要是commonjs和esmodule,这里总结一下两种模块化方法的特点

  1. commonjs是对值的拷贝,不是用一个对象了,esmodule是引用还是同一个对象
  2. require()是同步导入,阻塞进程;import()是异步导入,不阻塞进程

下篇会详细阐述commonjs的导入导出机制