阅读 94

前端模块化

模块化是一种规范,一种约束,这种约束会大大提升开发效率。将每个js文件看作是一个模块,每个模块通过固定的方式引入,并且通过固定的方式向外暴露指定的内容。 按照js模块化的设想,一个个模块按照其依赖关系组合,最终插入到主程序中。

B站模块化视频链接:www.bilibili.com/video/BV18s…

www.bilibili.com/video/BV1e7…

思否:segmentfault.com/a/119000001…

一、HTML中引入JavaScript脚本

1、默认情况

在HTML中先后引入以下两个脚本,这两个脚本将从上到下依次加载并解析,module1.js解析完毕再到module2解析,在解析过程中,页面构造会暂时中止。

<script src="module1.js"></script>
<script src="module2.js"></script>
复制代码

2、defer属性——延迟脚本

脚本引入时加上defer属性,则脚本在执行时不会影响页面构造。浏览器还是会立即下载module1.js和module2.js脚本,但是会等到页面构造完成,在从上到下解析执行脚本,module1.js会先于module2.js延迟执行。

<script src="module1.js" defer="defer"></script>
<script src="module2.js" defer="defer"></script>
复制代码

3、async属性——异步脚本

脚本引入时加上async属性,浏览器会立即下载文件,并解析执行,页面构造无需等待标本下载和执行,从而异步加载其他内容。但是不能保证先后顺序,也就是module1.js不一定先于module2.js执行,因此这两个模块不能互相依赖。

<script src="module1.js" async></script>
<script src="module2.js" async></script>
复制代码

二、JS模块化的历程

一、函数模式

1、定义:一个模块(.js)定义为一个函数

2、引入:在页面通过<script/>引入

3、存在问题:全局变量污染问题,在页面中引入模块后,模块中定义的函数名和函数外的变量名都是全局变量。

二、对象模式

1、定义:一个模块(.js)定义为一个对象

2、引入:在页面通过<script/>引入

3、存在问题:模块变量私有化问题,在页面中引入模块后,模块对象完全暴露在全局作用域,对象(模块)变量和函数可以任意访问甚至修改。

三、IIFE(立即执行函数表达式)模式

1、定义:一个模块定义为一个闭包,或者一个获得闭包函数返回值的变量

2、暴露:定义为闭包,将要暴露的变量或方法定义为window对象的属性:window.m=xxx。定义为一个获得闭包函数返回值的变量,则引入脚本后这个变量就进入全局作用域内。

3、引入:在页面通过<script/>引入,之后可以通过"m"使用模块暴露的内容。

4、存在问题:解决了全局作用域污染和变量私有化问题(就要暴露内容的变量名而且不暴露的变量是访问不到的),存在的问题是模块的依赖以及模块的扩展问题。

定义并暴露模块:

//方式一
(function(){
    uname="Alice";
    function sayName(){
        console.log("I am "+uname);
    }
    window.m=sayName;
})()
//方式二
let m=(function(){
    uname="Cindy";
    function sayName(){
        console.log("I am "+uname);
    }
    return sayName;
})()
复制代码

HTML代码(引入)

<script src="module1.js"></script>    
<script type="text/javascript">        
    m();    
</script>
复制代码

四、IIFE增强模式(扩展立即执行函数表达式)

这个模式实际上是在IIFE基础上给闭包函数传参,基于传参就可以传入其依赖的模块,也可以对原模块进行扩展;

1、定义:一个模块定义为一个闭包,并将依赖的模块作为参数传给闭包函数。

2、暴露:同上

3、引入:必须要让依赖模块在自定义的闭包模块前引入。在下面的例子中,jQuery库必须在引入modue2.js之前引入,因为其是module2模块的入参并在代码中使用,也就是其依赖的模块。

4、问题:严格保证脚本加载顺序,依赖关系模糊。

定义并暴露模块:

//代码中,jQuery就是这个模块依赖的库
(function(window,$){    
	uname="Cindy";    
	function sayName(){        
	console.log("I am "+uname);    
	}    
	$("body").css("background-color","pink");    
	window.m=sayName;
})(window,jQuery)
复制代码

HTML代码

<body>    
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.8.0/jquery-1.8.0.js"></script>    
    <script src="module2.js"></script>    
    <script type="text/javascript">       
         m();    
    </script>
</body>
复制代码

模块化进程至此,可以实现模块中变量的私有化,按需导出和导入,模块依赖的问题。但是需要严格保证脚本加载顺序,以来比较多时下载的脚本也很多,会发送过多请求耗费带宽。而且依赖关系模糊导致可维护性较差。

三、JS模块化规范

现在模块化过程是后端先行,先有CommonJS后端模块化,再有AMD,再有CommonJS前端模块化,最后是ES6规范。

一、AMD

AMD是前端模块化规范,是基于IIFE模式的的规范,模块加载是异步的。很好的体现了模块依赖关系,在index.html页面只需要引入一个js文件,没有请求多次的问题。

1、定义并暴露:每个文件当作一个模块,定义的方式如下,return的内容就是要暴露的内容。

  • 定义没有依赖的模块

    define(function(){ return 模块 })

  • 定义没有依赖的模块,第一个参数以数组形式指定要依赖的模块,第二个模块是函数,函数的参数与数组中的模块一一对应。

    define(['module1','module2'],function(m1,m2){ return 模块 })

2、引入:需要借助require.js加载模块

require(['module1','module2'],function(m1,m2){
	使用m1/m2
})
复制代码

使用AMD规范模块化编写代码如下

定义并暴露模块:

math.js:没有依赖的模块

define(function(){    
	var add=function(x,y){        
		return x+y;    
	}    
	return{        
		myadd:add,  //暴露的是一个对象    
	}
});
复制代码

tool.js:依赖于自定义的main.js模块和第三方jquery模块,jquery源码,支持amd规范,并自定义模块名为jquery

define(['math','jquery'],function(math,$){    
	var show=function(){        
		alert("done it!Look at me!"+math.myadd(10,30))        
		$('body').css('background','deeppink');    
	}    
	return{        
		myshow:show    
	}
});
复制代码

引入模块:

main.js:入口文件,这个文件引入所有要加载的模块

//main入口文件引入要使用模块,遵从AMD规范
require(['math','tool'], function (math,tool){    
	alert(math.myadd(1,3));    tool.myshow();
});
//配置路径,使得加载模块时不需要全路径,使用相对路径是相对main.js所在文件夹的路径
require.config({    
	paths:{        
		"math":"modules/math",        
		"tool":"modules/tool",        
		"jquery":"libs/jquery-3.6.0"  //引入第三方库    
	}
})
复制代码

index.html:指定require.js要加载的入口文件是mian.js

<!--1、data-main=""设置入口文件,这个文件会第一个被require.js加载        
2、每个html文件都要有一个入口文件,入口文件管理当前.html页面使用的所有.js代码        
3、后续引入的所有.js代码后缀需要省略。-->    
<script src="js/libs/require.js" async data-main="js/main.js"></script>
复制代码

AMD和CMD的区别:

共同点:两者都是前端的模块规范,都是异步加载模块

不同点:

1.第一个方面是在模块定义时对依赖的处理不同。AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。

2.第二个方面是对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,不过它们的区别在于模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD 在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句 的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。

二、CommonJS

这个是后端的模块化规范,Node.js就是基于这种规范编写的。如果我们想要在前端使用这种规范,就必须借助一些能够处理CommonJS语法的库来帮忙编译打包代码,比如:browserify。他会递归分析代码中所有require()调用,然后构建一个buddle.js文件,我们只需在页面通过

browserify will recursively analyze all the require() calls in your app in order to build a bundle you can serve up to the browser in a single

1、定义并暴露:每个文件当作一个模块,暴露的本质都是exports对象。

  • module.exports=value   (重写exports对象,重写的是什么暴露的就是什么)
  • exports.xxx=value    (为exports对象添加属性,暴露的就是export对象)

3、引入:require(xxx):如果是引入第三方库,则xxx就是模块名,如果是自定义模块,xxx是文件路径名;

使用CommonJS规范模块化编写代码如下:

定义并暴露的模块:

module1.js

//module.export=value,暴露的模块是对象
module.exports={
    msg:"module1 hello word",
    foo(){
        console.log(this.msg);
    }
}
复制代码

module2.js

//module.exports=value,暴露的模块是函数
module.exports=function(){
    console.log("module2 hellow world")
}
复制代码

module3.js

//exports.xxx=value,暴露的模块是对象,往exports中增加属性
exports.foo=function(){
    console.log('module3 foo');
};
exports.bar=function(){
    console.log('module3 bar');
};
exports.arr=[1,2,2,3,4,4,4,5,]
复制代码

引入模块:(后端,app.js文件:入口文件,这个文件引入所有要加载的模块)

//引入自定义模块
let m1=require('./modules/module1') //对象
let m2=require('./modules/module2') //函数
let m3=require('./modules/module3') //对象
//导入第三方模块
let uniq=require('uniq');  //函数 https://www.npmjs.com/package/uniq
m1.foo();
m2();
m3.foo();
m3.bar();
var arr=uniq(m3.arr);
console.log(arr);
复制代码

引入模块:(前端,index.html)

在以上module1.js、module2.js、module3.js、app.js(路径:js/src/app.js,入口文件)的代码基础上,还需要做些其他工作:

  • 下载并安装(全局&局部)安装browserify:

    npm install browserify npm install browserify --save-dev

  • browserify编译打包代码

    browserify js/src/app.js -o js/dist/bundle.js

然后,就在index.html直接引入打包完的buddle.js文件即可

<script src="./js/dist/bundle.js"></script>
复制代码

三、ES6模块规范

ES6模块规范是前端最流行的模块规范,定义、暴露和引入模块比AMD和CommonJS都简单得多。只是额外工作多了些,不仅需要使用babel等库将ES6转换成ES5,以便浏览器识别,还要使用browserify等库编译打包代码。browserify编译打包的用法和上面一致,babel的使用如下:

  • 安装babel

    npm install babel-cli browserify -g npm install babel-preset-es2015 --save-dev

  • ES6转ES5预设设置:配置.babelrc文件(无则新增)

    { "presets": ["es2015"] }

  • babel将es6编译为es5代码(包含commonJS语法),控制台输入(js/src是所有模块文件所在的目录)

babel js/src -d js/lib

下面是ES6模块化定义、暴露和引用的相关内容

1、定义并暴露:export,有分别暴露,统一暴露,默认暴露三种方式

2、引入:import *** from "模块路径/第三方库名

使用ES6规范模块化编写代码如下:

定义并暴露的模块:

module1.js

//分别暴露函数、变量任意数据类型,暴露内容打包成对象
export function foo(){
    console.log("foo module1")
}
export function bar(){
    console.log("bar module1")
}
export let arr=[1,2,3,4]
复制代码

module2.js

//统一暴露
function fun1(){
    console.log("fun1 module2")
}
function fun2(){
    console.log("fun2 module2")

}
export {fun1,fun2}  //打包成一个对象暴露
复制代码

module3.js

//默认暴露,可以暴露任何数据类型,暴露什么数据接收到的就是什么数据
//默认暴露只能暴露一次,所以一般用对象组织数据以暴露多数据
//export default value
export default {    
	msg:"HELLO WORLD MODULE3",    
	show(){        
		console.log("默认暴露的"+this.msg)    
	}
}
复制代码

引入模块:

mian.js(入口文件)

import {foo,bar} from "./module1";  //解构赋值按需接收分别暴露的内容
import {fun1,fun2} from "./module2";
import m3 from "./module3" ;
foo();
bar();
fun1();
fun2();
m3.show();
复制代码

index.html:browserify打包之后,引入buddle.js文件

<script src="./js/dist/bundle.js"></script>
复制代码

可以看到,ES6模块化规范虽然用起来很爽,但是,需要借助各种库来协助干活就是很累,需要借助更强大的工具,帮助我们完成语法转换和编译打包的工作,所以webpack就粉墨登场了。

四、webpack

webpack是一个前端资源构建工具,它可以做的工作很多,这里只关注它怎么帮忙完成ES6规范下的一些语法转换和编译打包工作。

使用ES6模块化规范,定义模块module1.js、module2.js、module3.js和入口文件index.js和上面一致(在src目录下)。

1、初始化项目,并安装webpack和webpack-cli

npm init
npm install webpack webpack-cli --save-dev
复制代码

2、控制台输入下面指令:将入口文件打包到build/main.js文件中。

webpack ./src/index.js -o ./build --mode=development
复制代码

或者,webpack.config.js配置入口和出口如下,然后控制台输入webpack指令

    const {resolve}=require('path')
    module.exports={    
        //入口    
        entry:'./src/index.js',   
         //出口    
        output:{        
            filename:"main.js",        
            //path取值要绝对路径,__dirname时nodejs变量表示当前文件所在目录的绝对路径        
        path:resolve(__dirname,'build')    
        }
   }
复制代码

3、index.html引入打包后的main.js

    <script src="./build/main.js"></script>
复制代码

可以看到,之前要下载babel和browserfily做的工作,有一个webpack就可以一站式解决了,只需要专注写js代码即可,而且,webpack还可以做更多的工作,所以在前端工程化中很重要。

文章分类
前端
文章标签