likes
comments
collection

重学webpack系列(一) -- 前端模块化的演变历史

作者站长头像
站长
· 阅读数 18

前言

任何事物的产生都有他的必然性,就像是冥冥之中注定了一样,在JavaScript刀耕火种的时代,前端是被定义为切图的一项工作,页面逻辑交互全部由服务端工程师完成,前端开发几乎不受服务端开发重视,那时候也没有很好的制定前端的规范,原因归结于并没有想到现如今前端的快速发展与规模,现如今前端开发已经在web新时代占据了半壁江山,在前端演变的过程中,社区规范也在不断的完善,那在前端的发展过程中,前端模块化究竟经历了什么样的变革呢,这将是我们这一章将要探索的问题。

模块化的概念

站在现在的角度来解释模块化,我相信同学们都能够说出一二:模块是一个具有某种功能,能够解决某种问题的独立区块,模块化则是由多个模块组合,相互协调解决某类问题的一种方式

模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。--来自百度百科,

早期的模块化

上面说到,早期前端只是个"切图工程师",模块化的概念产生,还是源自于前后端交互的媒人 -- AJAX,因为AJAX得以让前后端分离,经过数年的发展,业务系统逐渐庞大,前端逻辑变得愈加复杂,那时候模块化表现在一个文件就是一个模块,通过script标签引入多个文件就能使用,一般一个脚本文件几千行代码,其中就会导致一些问题的产生:

  • 超多全局变量产生问题。
  • 变量、函数名冲突问题。
  • 无法管理模块与模块之前的依赖关系。
  • 其他问题。

重学webpack系列(一) -- 前端模块化的演变历史

出现了上述问题怎么办,接着社区规范继续约定,每一个文件只允许暴露出一个全局对象,文件里的所有属性都必须暴露在对象里面,以达到解决重复的问题。

重学webpack系列(一) -- 前端模块化的演变历史

紧接着社区又约定了一种IIFE的方式,也就是在第二种的形势下,在外层包裹了一层自执行函数。

重学webpack系列(一) -- 前端模块化的演变历史

这一种方式所带来的好处是什么呢?
  • 因为是在自执行函数里面执行的,也就是利用闭包的特性,形成了私有的属性成员,能够很好的避免全局变量的污染。
  • 自执行函数可以传参,那也就很好的描述了模块与模块之间的依赖关系,使得模块管理变得可行。

上述三种方式为最早的社区约定的模块化方式,确实能够解决一些问题,但是同学们有没有发现,这三种方式都是通过script标签进行引入到html页面的,也就是同步加载的,如果在很复杂的具有多个请求的场景下使用的话,那么这些ajax将会是同步发起的,会带来很大的性能问题,为了解决这个加载方式的问题,社区又提出了一个新的规范,那就是AMD (Asynchronous Module Definition)

模块化的青铜器时代

AMD的设计灵感来自于以往开发者的约定俗成的规范的统一:

  • 灵感一:用script引入一个文件到页面中,其余模块能不能够用代码控制按需加载。
  • 灵感二:前端行业蒸蒸日上,能不能设计一个符合行业的统一的规范。
  • 灵感三:设计的基础库应该能够实现模块的自动加载。
  • 灵感四:可以借鉴于Common.js实现的模块化规范。

既然这里提到了Common.js规范,那么就顺带的说一下:

Common.js规范 Common.js规范,是Node.js中所遵循的模块化规范,该规范规定,每一个文件就是一个模块,每个模块有自己单独的作用域,通过module.exports导出成员,通过require导入另外的模块。

因为Common.js加载模块的方式为同步加载,所以它并不适用于在浏览器环境,所以综上所述AMD规范就产生了,也产生了一个符合AMD规范的库require.js

扩展

为什么node.js使用同步加载不受影响?而浏览器中就不能使用同步加载?

因为在node.js中模块分为内置模块自定义模块第三方模块三种,node.js的加载方式为启动式加载(启动式加载:系统根据配置自行去加载依赖的行为),根据机制,node.js在运行时只是去使用模块,所以同步加载并不会给node.js造成性能问题。但是在浏览器端如果也是采用同步加载的策略,那么将会产生很多的同步请求,这样应用程序的执行效率将会变得很低很低

AMD基础的实践

AMD的基础实践需要用define()来实施,require.js则通过require函数进行模块加载。

// 如何使用AMD定义一个模块?
define(['module_1.js', 'module_2.js'], function(module_1, module_2){
  return {
   morning: function(){
     sayHello();// module_1.js中定义的函数
     callHello();// module_2.js中定义的函数
   }
  }
})

// require.js
require(['module_1.js'], function(module_1){
    sayHello();// module_1.js中定义的函数
})

上述代码表明,AMD不仅可以定义模块,还可以使用模块。require.js只是模块加载使用。这种加载方式的原理也就是当页面需要的时候,创建了一个script区引入模块到页面中。但是这一种方式和IIFE的方式并没有什么很大的差别。

现代模块化规范

经过几年的发展,前端模块化规范逐渐趋于完善,最后也形成了统一:

  • Node.js遵循Common.js规范。
  • 浏览器遵循ESM规范。

ESMECMAScript2015(es6)才被制定出来的一个统一规范,因为前端必须跟浏览器打交道,在推出之初并不是所有浏览器都支持的,于是乎webpack等一些比较强大的工具的产生,就解决了兼容性的问题。

ESM的特性

  • 自动采用严格模式(在严格模式中this不会默认指向window对象)。
  • 每个ESM都有单独的私有作用域。
  • ESM是通过CORS去请求外部js模块的,被访问的地址需要支持CORS
  • ESMscript标签会延迟执行脚本,不会阻塞浏览器渲染,相当于加了defer属性。
  • ESM的模块导入导出。
// 直接使用export关键词导出想暴露给其他模块的成员
export const word = "hello"
 
export const fn = () => {}
 
 
// 先声明,再集中导出
const word = "hello"
 
const fn = () => {}
 
export { word, fn }
 
export default fn // 将成员导出为默认成员
 
// 导出时重命名,以及将某个成员作为默认导出成员
 
export { word as wordRename, fn as default }
import { word } from './code1.js' // 导入命名成员
 
import fn from './code1.js' // 导入默认成员
 
// 重命名
import { word as wordNewName } from './code1.js' // 导入时重命名
 
import { default as fn } from './code1.js' // 给默认成员重命名
 
// 同时导入命名成员和默认成员的两种方式
 
import fn, { word } from './code1.js'
 
import { word, default as fn} from './code1.js'
 
import * from '/code1.js' // 导入子模块的所有成员
 
import '/code1.js' // 仅执行子模块,不需要使用子模块导出的成员
 
 
import('./code1.js').then(function(module) {}) // 动态加载模块

扩展

关于script标签加载模块,会阻碍浏览器UI线程执行,我们一般可以给script标签加上defer / asyncMdn上面也有关于模块延迟加载的介绍:

重学webpack系列(一) -- 前端模块化的演变历史

  • 单独设置defer,浏览器解析到模块的时候会与UI线程并行处理,解析完毕之后在UI线程空闲的时候继续执行。不会阻碍UI线程渲染。
  • 单独设置async,浏览器解析到模块的时候会与UI线程并行处理,解析完毕之后会立即加入到UI线程执行,会阻碍UI线程渲染。
  • 如果加上了type='module'字段,则会和defer达到一样的效果。

扩展

Common.js规范与ESM的区别总结,参考ECMAScript 6 入门

  • CommonJS模块输出的是一个值的拷贝,ESM模块输出的是值的引用。
    • 因为CommonJ输出的值是一个静态值,而且这个值会被缓存到,所以每一次获取到的都是缓存值,改变原来的值,引用值不会改变。ESM输出的是一个只读引用,模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
  • CommonJS模块是运行时加载,ESM模块是编译时输出接口。
    • 因为CommonJS加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而ESM模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  • CommonJS模块的require()是同步加载模块,ESM模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

模块化打包工具

因为最新的模块化规范依然存在着浏览器兼容的问题,并且前端模块化划分出的文件过多,诸多文件全部是来自于网络请求,这无疑是加重了浏览器的工作负担,进而影响到了开发者的工作效率。再者模块化并不是JavaScript文件仅仅独有的,在前端项目日益复杂的过程中,HtmlCss也会面临着模块化的趋势。面对这些问题,我们应该会有一些想法:

  • 想法一:高版本的语法代码能不能通过某种办法编译成低版本的代码,以达到兼容的效果。
  • 想法二:诸多文件编译后能不能只生成一个多个入口文件,以此来降低网络请求,降低部分浏览器压力。
  • 想法三:对于不同类型的文件,能不能通过某种方式处理成模块,以便于当做模块使用。

如果能够满足上述的三个想法,这样的工具也就满足了社区的规范的统一,所有的模块的加载都可以通过代码进行控制。所以我们要介绍的主角webpack就诞生了。

扩展

你觉得webpack它的定位是什么呢?

重学webpack系列(一) -- 前端模块化的演变历史 就目前来看webpack已经不以前的简简单单的构建工具了,得益于前端框架生态的快速发展,它已经成为了前端项目的构建系统,它可以让模块化思想贯彻整个前端项目,webpack之下,万物皆可模块。

总结

这一章我们探索了前端模块化的演变历史,也学习了Common.js规范与ESM规范的落地实践,初步揭开了webpack打包工具的神秘面纱,下一章我们将去学习webpack到底解决了目前前端开发中遇到的什么问题,webpack又是怎么样去解决的,直通车 >>> 重学webpack系列(二) -- webpack解决的问题与实现模块化的具体实践