yuuji 发表于 2022-7-15 14:39:51

前端模块化知识梳理

  一、背景

  作为前端开发,模块化我们已经耳熟能详,我们平时接触到的ES6的import,nodejs中的require他们有啥区别?我们也听过CommonJS、CMD、AMD、ES6模块系统,这些都有什么联系呢?本文将对这些问题进行归纳总结,可以对模块化有个清晰的认识。

  二、为何需要模块化?

  1.起源

  最开始js是没有模块化的概念的,就是普通的脚本语言放到script标签里,做些简单的校验,代码量比较少。随着ajax的出现,前端可以请求数据了,做的事情更多了,逻辑越来越复杂,就会出现很多问题。

  1.1全局变量冲突

  因为大家的代码都在一个作用域,不同人定义的变量名可能重复,导致覆盖。

  varnum=1;//一个人声明了...varnum=2;//其他人又声明了

  1.2依赖关系管理麻烦

  比如我们引入了3个js文件,他们直接相互依赖,我们需要按照依赖关系从上到下排序。

  <;scriptsrc='./one.js'>;<;/script>;<;scriptsrc='./two.js'>;<;/script>;<;scriptsrc='./three.js'>;<;/script>;

  如果文件有十多个,我们需要理清楚依赖关系再手动按顺序引入,会导致后续代码更加难以维护。

  2.早期解决方案

  针对前面说的问题,其实也有一些响应的解决方案。

  2.1命名空间

  命名空间是将一组实体、变量、函数、对象封装在一个空间的行为。这里展现了模块化思想雏形,通过简单的命名空间进行「块儿」的切分,体现了分离和内聚的思想。著名案例「YUI2」。

  //示例:constcar={name:'小汽车',start:()=>;{console.log('start')},stop:()=>;{console.log('stop')}}

  上面示例可以发现可能存在问题,比如我们修改了car的name,会导致原有的name被更改

  car.name='测试'console.log(car)//{name:'111',start:ƒ,stop:ƒ}

  2.2闭包

  再次提升模块化的解决方案,利用闭包使污染的问题得到解决,更加纯粹的内聚

  moduleA=function(){varname='小汽车';return{start:function(c){returnname+'启动';};}}()

  上面示例中function内部的变量就对全局隐藏了,达到了封装的目的。但是模块名称暴露在全局,还是存在命名冲突的问题。下面这个基于IIFE和闭包实现的效果:

  //moduleA.js(function(global){varname='小汽车';functionstart(){};global.moduleA={name,start};})(window)

  上面表达式中的变量name不能直接从外部访问。综上,所以模块化解决的问题有哪些:

  解决命名污染,全局污染,变量冲突等问题

  内聚私有,变量不能被外面访问到

  怎么引入其它模块,怎样暴露出接口给其它模块

  引入其他模块可能存在循环引用的问题

  三、主流模块化解决方案

  1.CommonJS

  可以点击CommonJS规范查看相关介绍。1)每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。2)CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。3)require方法用于加载模块。

  1.1加载模块

  varexample=require('./example.js');varconfig=require('config.js');varhttp=require('http');

  1.2对外暴露模块

  module.exports.example=function(){...}module.exports=function(x){console.log(x)}

  1.3Node.js的模块化

  说到CommonJS我们要提一下Node.js,Node.js的出现让我们可以用JavaScript来写服务端代码,而Node应用由模块组成,采用的是CommonJS模块规范,当然并非完全按照CommonJS来,它进行了取舍,增加了一些自身的特性。1)Node内部提供一个Module构建函数。所有模块都是Module的实例,每个模块内部,都有一个module对象,代表当前模块。包含以下属性:

  module.id模块的识别符,通常是带有绝对路径的模块文件名。

  module.filename模块的文件名,带有绝对路径。

  module.loaded返回一个布尔值,表示模块是否已经完成加载。

  module.parent返回一个对象,表示调用该模块的模块。

  module.children返回一个数组,表示该模块要用到的其他模块。

  module.exports表示模块对外输出的值。

  2)Node使用CommonJS模块规范,内置的require命令用于加载模块文件。3)第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。所有缓存的模块保存在require.cache之中。

  //a.jsvarname='Lucy'exports.name=name//b.jsvara=require('a.js')console.log(a.name)//"Lucy"a.name="hello";varb=require('./a.js')console.log(b.name)//"hello"

  上面第一次加载以后修改了name值,第二次加载的时候打印的name是上次修改的,证明是从缓存中读取的。想删除模块的缓存可以这样:

  deleterequire.cache;

  4)CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。

  //a.jsvarcounter=3exports.counter=counterexports.addCounter=function(a){counter++}//b.jsvara=require('a.js')console.log(a.counter)//3a.addCounter()console.log(a.age)//3

  这个例子说明a.js模块加载以后,模块内部的变化就影响不到a.counter了。这是因为a.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。


  来源|https://www.fly63.com/
页: [1]
查看完整版本: 前端模块化知识梳理