一篇文章彻底弄懂JavaScript模块化

说在前面

最早期的JavaScript并没有像现在一样承担着众多的“责任”,通常只需要在<script>标签中加入部分Javascript代码就可以满足页面的交互需求。随着Javascript的逐渐成熟,他所承担的工作也越来越多,之前那种“没有章法”的写法导致了页面逻辑混乱,维护起来也是异常吃力,所以大家提出了模块化编程的解决方案,说白了,也就是将代码分割成不同的模块,便于复用、维护以及按需加载。

发展历程

CommonJS

模块化技术最早应用于服务器端编程,因为早期认为网页程序复杂度有限,但是在服务器端就不一样了,为了与操作系统和其他应用程序互动,如果没有模块化将举步维艰。node.js的模块系统,就是参照CommonJS规范实现的。浏览器对于CommonJS 是不兼容的,node.js提供了四个环境变量:

  • module 代表当前模块,该变量是一个对象
  • exports module变量的属性,外界加载的内容,其实是module.exports的属性
  • require 用于加载模块
  • global 使用global定义的变量,可以被所有文件读取

新建A.js,B.js文件:

// A.js
var app = {
    name:'app',
    version:'1.0.0',
    sayName:function(name) {
       console.log(this.name);
    }
}
module.exports = app;
// B.js
var app = require('./A.js')
console.log(app.name + ' ' + app.version)

在这里插入图片描述

当我们尝试将此代码在浏览器上运行时,新建index.html:

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LEAFLET</title>
</head>
<body>
    <div id="app">ap</div>
    <script src="./A.js"></script>
    <script src="./B.js"></script>
</body>
</html>

在浏览器中直接打开index.html文件:

在这里插入图片描述

这也印证了我们之前所说的话,浏览器环境是不具备这些环境变量的

AMD(Asynchronous Module Definition)

服务器端的模块化得到了不错的反馈,但是由于CommonJS是同步加载的,对于浏览器来说,加载的时间受限于网速,而等待的期间,浏览器也不会有任何的“反应”,所以**AMD(异步模块加载机制)**应运而生。

实现了AMD规范的主要有require.jscurl.js,这里不介绍用法,总而言之,他能实现加载第三方资源库时,浏览器不会失去响应。

ES6

ES6在语言标准的层面上,实现了模块功能,可以取代CommonJS, AMD规范,成为浏览器和服务器通用的模块解决方案。

相较于之前,使用的方法从require - module.exports变成了 import - export

MDN的介绍来看,import只能在声明了type="module"script标签中使用。

// A.js
var app = {
    name:'app',
    version:'1.0.0',
    sayName:function(name) {
       console.log(this.name);
    }
}
 
export default app;
export var e = '123'
--------------------------------------------------------------------------------------------------
// B.js
import app,{e} from './A.js'
console.log(app.name)
 
console.log(e)
-------------------------------------------------------------------------------------------------
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LEAFLET</title>
</head>
<body>
    <div id="app">ap</div>
    <script src="./A.js" type="module"></script>
    <script src="./B.js" type="module"></script>
</body>
</html>

值得注意的是,使用了type="module"后,在浏览器中直接打开会报错跨域,这是因为file协议并不支持跨域请求,所以要在服务器中部署,然后通过http(s)来访问index.html文件。比如http://127.0.0.1:5500/index.html

以vue项目举例,为什么我们使用了这么多的import,而且我们在index.html里面也没有添加type="module"项目也能正常执行呢?

在这里插入图片描述

如上图所示:原因就是作为构建工具的webpackwebpack作为一个强大的工具,他“天生”支持(经过webapck打包后)如下类型:

运行时加载 vs 编译时加载

比如我们需要fs模块里的一些功能,现在我们项目里就需要引入fs模块,这个模块里有很多功能,比如stat,exists,readfile等…

运行时加载

CommonJS 和 AMD都是运行时加载,怎么去理解呢?

let { stat, exists, readfile } = require('fs');

这种引入方法,实际上是预加载整个fs模块,然后到了运行时才可以得到fs对象,此时再从对象上读取需要的方法。

编译时加载(静态加载)

ES6就属于编译时加载,下面的代码,会从fs模块加载3个方法,在编译时就可以确定。

import { stat, exists, readFile } from 'fs';

对比

编译时加载的特性会使被import的模块在加载时就被编译,这促使我们可以对工程代码进行静态分析,比如我们熟知的Element ui就支持基于ES modules的tree-shaking,比如我们只用到了antd中的少数几个组件,是可以不引入全部的组件的。我实现了一个demo去对比,按需引入Button,发现体积差了整整2MB

编译时加载(静态加载)的不足

上面介绍过,静态加载在编译时就会将内容都加载好。现在考虑一个场景,项目运行过程中按需加载某一个模块,项目正常运转不一定需要加载这个模块,但是一定条件下,又要加载这个模块。 实际场景:我们的系统有10个系统模块,我们本次登录系统只需要操作其中的3个模块,我们实际上是不需要将10个系统模块全部加载到内存中的。

比如下方的代码,当用户点击对应的超链接,才会加载超链接相关的资源。

<!DOCTYPE html>
<nav>
  <a href="books.html" data-entry-module="books">Books</a>
  <a href="movies.html" data-entry-module="movies">Movies</a>
  <a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>

<main>Content will load here!</main>

<script>
  const main = document.querySelector("main");
  for (const link of document.querySelectorAll("nav > a")) {
    link.addEventListener("click", e => {
      e.preventDefault();

      import(`./section-modules/${link.dataset.entryModule}.js`)
        .then(module => {
          module.loadPageInto(main);
        })
        .catch(err => {
          main.textContent = err.message;
        });
    });
  }
</script>

当我们希望按照一定的条件或者按需加载模块的时候,动态import() 是非常有用的。而静态型的 import 是初始化加载依赖项的最优选择,使用静态 import 更容易从代码静态分析工具和 tree shaking 中受益。

说到最后

文章的最后,不得不再提一下webpack,他对于动态导入也是十分支持的,篇幅问题,这里也不做展开讲,总的来说,webpack提供了手动代码分割,动态导入的支持以及splitChunk对公共代码进行分割,更多内容请参考webpack-import()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值