JS模块化
本人是个新手,写下博客用于自我复习、自我总结。
如有错误之处,请各位大佬指出。
学习资料来源于:尚硅谷
说在最前面:需要注意的是,代码中涉及到了ES6,出现了一些新特性,也许放在编译器里会报错。(因为我之前用Eclipse,发现把代码写在其中报了错,在idea上就没问题了,因为idea上是可以设置使用版本的,目前没发现Eclipse如何改版本)
JS模块化介绍
当项目功能越来越多,代码量也会越来越多,后期的维护难度也会增大,此时如何对项目的代码进行一个管理,方便开发人员开发就是一个避不开的问题。虽然现在Vue-cli帮我们做好了相关的工作,在Vue中也有各种各样的办法,帮助我们实现代码的分层次的管理,但是这本身也是Vue、NodeJS帮我们封装好的,而在这其中的内涵和发展,就是一个需要去学习和了解的内容。
首先总结一下,JS模块化就是复杂的程序依据一定的规则(规范)封装成几个块(文件),再把它们组合在一起。块的内部数据是私有的,只是向外部暴露一些方法,从而让其与外部其它模块通信。这种模块化也是Vue目前的实现方式,比如:将axios封装好、将Vuex封装好、utils工具封装好等等,并将它们放在各自的文件夹中分别管理,最后组合在一起,通过暴露的模块进行通信。
在总结当前的模块化方式之前,也用一点时间简述一下JS模块化的进化史。
模块化的进化史
1、全局function模式:
这种模式也是我们最开始学习的方式,从现在来看,这种方式很明显的会污染全局作用域。从平常的开发中也可以发现,可能会出现很多同名的方法,所以此时通过以下方式进行使用,就很容易出现命名冲突,但已初具模块化的雏形。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>01_全局function模式</title>
</head>
<body>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
foo()
bar()
</script>
</body>
</html>
module1.js
/**
* 全局函数模式: 将不同的功能封装成不同的全局函数
* 问题: Global被污染了, 很容易引起命名冲突
*/
function foo() {
console.log('foo()')
}
function bar() {
console.log('bar()')
}
module2.js
function foo() { //与另一个模块中的函数冲突了
console.log('foo()2')
}
2、namespace模式
由于第一种方式会污染全局作用域,也容易出现命名冲突,因此对第一种方式就有了改进后的第二种方式:namespace模式。这种方式就是对原来的方法进行了一次简单的封装,这样就减少了全局作用域上的变量数目,也解决了命名冲突问题,各自只需要使用各自内部的方法即可。但是这种方式最大的问题就是不安全,我们可以直接修改对象内部的数据。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>02_namespace模式</title>
</head>
<body>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript">
myModule.foo()
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo()
</script>
</body>
</html>
module1.js
/**
* namespace模式: 简单对象封装
* 作用: 减少了全局变量
* 问题: 不安全(数据不是私有的, 外部可以直接修改)
*/
let myModule = {
data: 'hello',
foo: function(){
console.log(this.data);
}
}
3、IIFE模式:匿名函数自调用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>03_IIFE模式</title>
</head>
<body>
<script type="text/javascript" src="module3.js"></script>
<script type="text/javascript">
myModule.foo()
console.log(myModule.data) //undefined 不能访问模块内部数据
myModule.data = 'xxxx' //不是修改的模块内部的data
myModule.foo() //没有改变
</script>
</body>
</html>
/**
* IIFE模式: 匿名函数自调用(闭包)
* IIFE : immediately-invoked function expression(立即调用函数表达式)
* 作用: 数据是私有的, 外部只能通过暴露的方法操作
* 问题: 如果当前这个模块依赖另一个模块怎么办?
*/
(function (window) {
//数据
let data = 'hello'
//操作数据的函数
function foo() { //用于暴露有函数
console.log('foo()',data)
}
//暴露行为
window.myModule = {foo}
})(window)
针对第二种方式不安全的问题,对其改进就出现了第三种方式:匿名闭包。这种方式利用了匿名函数自调用的闭包安全性,通过上面图中的代码可以看到,在这种方式下,数据是私有的。如果外部想使用,就只能调用return暴露出来的方法进行操作,我们也无法对其内部的方法进行修改。但是这种方式也会有一个缺陷:通常多个模块间可能会互相调用,现在无法修改,所以就有了第四种方式。
4、IIFE模式增强
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>04_IIFE模式增强</title>
</head>
<body>
<!--引入的js必须有一定顺序-->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module4.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
</body>
</html>
module4.js
/**
* IIFE模式增强 : 引入依赖
* 这就是现代模块实现的基石
*/
(function (window, $) {
//数据
let data = 'hello'
//操作数据的函数
function foo() { //用于暴露有函数
console.log('foo()' ,data)
$('body').css('background', 'red')
}
//暴露行为
window.myModule = {foo}
})(window, jQuery)
改进方式很简单,就是如果需要什么其他的依赖,就在匿名函数自调用时传入即可。如上图中,传入一个jQuery,就可以在该匿名函数中使用jQuery语法了。这种方式就是现代模块化实现的基石。
模块化的好处
从整个模块化的发展进程中,不难看出模块化的好处:
① 避免了命名冲突,减少了命名空间的污染;
② 更好的实现了功能的分离,只需按需加载即可;
③ 更高程度的体现复用性,多个模块可以同时引用;
④ 高可维护性,需要改动时只需要改变各功能模块。
但是,从目前来看,以上方案最大的问题就是在script标签需要引入多个js文件,这样会导致请求过多、依赖多时还是会比较模糊,难以区分,难以维护:
由于直接用script标签引入,在模块较多的时候还是会比较麻烦,所以就有了多种形式的模块化,也提出了很多的规范。
CommonJS ※
(在这里建议先学习Node.JS,相关语法在Node.JS中也有所提及)
CommonJS的出现就是为了弥补当时JavaScript没有模块化标准的缺陷,虽然这个名词已经很少涉及,但是它为JavaScript指定了一个美好的愿景,希望让JS在任何地方运行,而NodeJS就借助它实现了这个愿景。
说明:
- 每个文件都可当作一个模块
- 在服务器端:模块的加载是运行时同步加载的(不会出现什么问题)
- 在浏览器端:模块需要提前编译打包处理(用户等待的时间可能会很长。需要提前编译是因为浏览器不知道require语法)
- CommonJS-Node就是用来服务器端实现。
CommonJS-Browserify就是用来浏览器端实现。它也称为CommonJS的浏览器端的打包工具。(但其实现在也不能这么说,因为ES6中也用到Browserify了)
首先,在Node中,一个js文件就是一个模块,每一个js文件中的js代码都独立运行在一个函数中,而不是全局作用域,所以一个模块中的变量和函数在其他模块中就无法访问。这和IIFE模式很像。而Node实现的方式是:
/*
模块化
- 在Node中,一个js文件就是一个模块
- 在Node中,每一个js文件中的js代码都是独立运行在一个函数中
而不是全局作用域,所以一个模块的中的变量和函数在其他模块中无法访问
*/
console.log("我是一个模块,我是02.module.js");
/*外部不可见*/
var a = 10 ;
/*
我们可以通过 exports 来向外部暴露变量和方法
只需要将需要暴露给外部的变量或方法设置为exports的属性即可
*/
//向外部暴露属性或方法
exports.x = "我是02.module.js中的x";
exports.y = "我是y";
exports.fn = function () {};
使用的时候,我们不再需要使用script标签导入,而是通过require()来导入外部的模块,这就是require函数的由来。在使用require()时,我们只需要以一个文件的路径作为参数,node将会自动根据该路径来引入外部模块。
//引入其他的模块
/*
在node中,通过require()函数来引入外部的模块
require()可以传递一个文件的路径作为参数,node将会自动根据该路径来引入外部模块
这里路径,如果使用相对路径,必须以.或..开头
使用require()引入模块以后,该函数会返回一个对象,这个对象代表的是引入的模块
*/
var md = require("./02.module");
console.log(md);
补充说明:require()中的标识除了填写路径外,也可以直接写核心模块(npm中下载的模块)的名字来引入。比如:
var fs = require("fs");
var express = require("express");
根据在Node中的使用,CommonJS对模块的定义就已可以总结出来:模块引用(require)、模块定义(exports)、模块标识(let fs = require(“fs”))。
除此以外,在node中还有一个不可忽视的用法。在项目中经常会使用到module.exports,比如项目中的vue.config.js等文件都是这样使用的。
为什么能这么用?module又是怎么出来的?这是因为node在执行模块中的代码时,它会首先用如下代码包裹模块中的所有代码:
function (exports, require, module, __filename, __dirname) {
}
传递进的这5个实参意义如下:
exports:该对象用来将变量或函数暴露到外部
require:函数,用来引入外部的模块
module:module代表的是当前模块本身,那么exports就是module的属性。所以我们可以使用 exports 导出,也可以使用module.exports导出
__filename:当前模块的完整路径。如:D:\Projects\01.node\04.module.js
__dirname:当前模块所在文件夹的完整路径。如:D:\Projects\01.node
而使用 exports
导出、module.exports
导出,它们之间的用法是有区别的:
helloModule.js
module.exports.name = "孙悟空";
module.exports.age = 18;
module.exports.sayName = function () {
console.log("我是孙悟空");
};
/* 无法使用这种方式,会报错。但module.exports就不会
exports = {
name:"猪八戒",
age:28,
sayName:function () {
console.log("我是猪八戒");
}
};
*/
var hello = require("./helloModule");
/*
exports 和 module.exports
- 通过exports只能使用.的方式来向外暴露内部变量
exports.xxx = xxx
- 而module.exports既可以通过.的形式,也可以直接赋值
module.exports.xxx = xxxx
module.exports = {}
*/
console.log(hello.name);
console.log(hello.age);
hello.sayName();
所以,在项目的配置文件中,通常会使用到module.exports。
CommonJS-Node ※
项目结构:
package.json可以用npm init
来创建。
"name"中不能有大写字母。(在package.json中,只保留了简单的信息,毕竟我们不需要上传项目)
在这里我们下载一个第三方模块用来演示:(在当前项目目录下安装)
npm install 包名 --save
:用来安装包,并添加到依赖中
这其中的uniq的用法:(就是用来去除数组中重复的内容)
安装好之后:
module1.js
//使用module.exports = value向外暴露一个对象
module.exports = {
msg: 'module1',
foo() {
console.log(this.msg);
}
}
module2.js
//使用module.exports = value向外暴露一个函数
module.exports = function () {
console.log('module2');
}
module3.js
//使用exports.xxx = value向外暴露一个对象
exports.foo = function () {
console.log('module3 foo()')
}
exports.bar = function () {
console.log('module3 bar()')
}
exports.arr = [2,4,5,2,3,5,1,11];
app.js
/**
1. 定义暴露模块:
module.exports = value;
exports.xxx = value;
但是exports不能使用exports = {}的方式,但module.exports可以
2. 引入模块:
var module = require(模块名或模块路径);
*/
let uniq = require('uniq');
//引用模块
let module1 = require('./modules/module1');
let module2 = require('./modules/module2');
let module3 = require('./modules/module3');
//使用模块
module1.foo();
module2();
module3.foo();
module3.bar();
let result = uniq(module3.arr);
console.log(result);
输出结果:
(uniq不仅能去掉重复元素,还能根据元素首位数字的大小进行排序)
CommonJS-Browserify ※
项目结构:
在当前项目目录下安装:
(直接save加依赖,加的是’运行依赖’的包,save-dev加依赖,加的是’开发依赖’的包,-g是全局下载包,给计算机用的)
安装后:
package.json:
{
"name": "commonjs-browserify",
"version": "1.0.0",
"dependencies": {
"uniq": "^1.0.1"
},
"devDependencies": {
"browserify": "^16.5.1"
}
}
module1.js
//使用module.exports = value向外暴露一个对象
module.exports = {
foo() {
console.log('module1 foo()')
}
}
module2.js
//使用module.exports = value向外暴露一个函数
module.exports = function () {
console.log('module2()')
}
module3.js
//使用exports.xxx = value向外暴露一个对象
exports.foo = function () {
console.log('module3 foo()')
}
exports.bar = function () {
console.log('module3 bar()')
}
app.js
//引用模块
let module1 = require('./module1')
let module2 = require('./module2')
let module3 = require('./module3')
let uniq = require('uniq')
//使用模块
module1.foo()
module2()
module3.foo()
module3.bar()
console.log(uniq([1, 3, 1, 4, 3]))
在之前已经说过了,浏览器是无法识别require语法的,需要提前编译打包处理,Browserify就是帮我们做这个事情的。
这段指令分成三部分。除去browserify。-o
:output,意味着输出。输出的左侧是需要处理的源文件,输出的右侧是作用后输出的文件位置及名字。(起什么名字都可以,只是bundle是打包的意思)
完成操作后:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript" src="js/dist/bundle.js"></script>
</body>
</html>
然后打开页面,就可以看到输出结果了:
AMD-RequireJS ※
AMD(Asynchronous Module Definition):异步模块定义,专门用于浏览器端。模块的加载是异步的。AMD规范的出现比CommonJS-Browserify要早。
基本语法:
- 定义暴露模块:
- 没有依赖的模块:
define(function(){return 模块对象})
- 有依赖的模块:
define([依赖模块名], function(){return 模块对象})
如:define(['module1','module2'],function(m1,m2){return 模块对象})
- 没有依赖的模块:
- 引入使用模块:
require(['module1', 'module2'], function(m1, m2){//使用模块对象})
requirejs(['module1', 'module2'], function(m1, m2){//使用模块对象})
演示:未使用AMD
(首先,我们不使用AMD规范,自己来写)
项目结构:
dataService.js
//定义一个没有依赖的模块
(function (window) {
let msg = 'dataService.js';
function getMsg() {
return msg;
}
window.dataService = {getMsg}
})(window)
alerter.js
//定义一个有依赖的模块
(function (window, dataService) {
let msg = 'alerter.js';
function showMsg() {
alert(dataService.getMsg() + ', ' + msg);
}
window.alerter = {showMsg}
})(window, dataService)
app.js
(function (alerter) {
alerter.showMsg();
})(alerter)
test1.html
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo 1</title>
</head>
<body>
<div>
<h1>Modular Demo 1: 未使用AMD(require.js)</h1>
</div>
<script type='text/javascript' src='js/modules/dataService.js'></script>
<script type='text/javascript' src='js/modules/alerter.js'></script>
<script type="text/javascript" src="./app.js"></script>
</body>
</html>
很明显的,我们需要通过script
标签,将所有依赖导入才能使用,显然很麻烦。
接下来使用 AMD 模块化规范👇
RequireJS ※
在使用之前,需要下载require.js,并引入:
- 官网下载: http://www.requirejs.cn/
- github下载 : https://github.com/requirejs/requirejs
(Node不需要下载是因为Node已经帮我们封装好了一切,这也就让我们感知不到它们底层细节的存在)
然后将require.js导入项目: js/libs/require.js
项目结构:
dataService.js
//定义没有依赖的模块
define(function () {
let name = 'dataService.js';
function getName() {
return name;
}
return {getName}
});
alerter.js
//定义有依赖的模块
define(['dataService', 'jquery'], function (dataService, $) {
let msg = 'alerter.js';
function showMsg() {
$('body').css('background', 'gray');
alert(dataService.getName() + ', ' + msg);
}
return {showMsg}
});
main.js
(function () {
//配置
requirejs.config({
//基本路径 (出发点在根目录下)
baseUrl: 'js/',
//映射: 模块标识名: 路径
paths: {
//注意:文件千万不要写 .js,规范会自动帮我们加
//自定义模块
alerter: './modules/alerter',
dataService: './modules/dataService',
//库模块
jquery: './libs/jquery-1.10.1',
angular: './libs/angular'
},
//暴露angular对象
shim:{
angular:{
exports: 'angular'
}
}
})
//引入模块使用
requirejs(['alerter','angular'], function (alerter,angular) {
alerter.showMsg();
console.log(angular);
})
})()
index2.html
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo 2</title>
</head>
<body>
<script type="text/javascript" src="js/libs/require.js" data-main="js/main.js"></script>
</body>
</html>
其中需要注意的是,要引入jQuery模块时,一定要使用这个名字:jquery
。
因为jQuery的源码中写到了:
从AMD-RequireJS规范中已经不难看出,此时已经初具了目前webpack的雏形,只可惜AMD不能够支持所有第三方库,比如想使用AngularJS还得需要用shim来单独暴露。除此以外,AMD也不支持服务器端,没有更高的想法和扩展性,从而被现在的Node + vue-cli + webpack所取代。
CMD-SeaJS
CMD在市面上用的不多。CMD也是专门用于浏览器端,模块的加载也是异步的。模块只在使用时才会加载执行。
基本语法:(像是AMD和CommonJS的合并)
- 定义暴露模块:
define(function(require, exports, module){
//通过require引入依赖模块
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3',function(m3){
})
//通过module/exports来暴露模块
exports.xxx = value
module.exports = value
})
- 引入使用模块:
define(function(require){
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
在使用之前,需要下载sea.js,并引入:
- 官网下载: http://seajs.org/
- github下载 : https://github.com/seajs/seajs
(需要说明的是,因为sea.js好像已经被卖了,网站应该已经点不开了,同时应该也找不到官方下载途径了,所以sea.js听人说已经快属于文物了。而且sea.js是阿里的大佬写的,目前只有阿里少部分在用。不过下载不到sea.js也没关系,其他模块化方法也可行,这里了解即可)
然后将sea.js导入项目: js/libs/sea.js
项目结构:
module1.js
//定义没有依赖的模块
define(function (require, exports, module) {
//内部变量数据
let msg = 'module1';
//内部函数
function foo() {
return msg;
}
//向外暴露
module.exports = {foo};
});
module2.js
define(function (require, exports, module) {
let msg = 'module2';
function bar(){
console.log(msg);
}
module.exports = bar;
});
module3.js
define(function (require, exports, module) {
let data = 'module3';
function fun(){
console.log(data);
}
exports.module3 = {fun};
});
module4.js
define(function (require, exports, module) {
let msg = 'module4';
//引入依赖模块(同步)
let module2 = require('./module2');
module2();
//引入依赖模块(异步)
require.async('./module3', function (m3) {
m3.module3.fun();
});
function fun2(){
console.log(msg);
}
exports.fun2 = fun2;
})
main.js
define(function (require) {
var m1 = require('./module1');
console.log(m1.foo());
var m4 = require('./module4');
m4.fun2();
})
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--
使用seajs:
1. 引入sea.js库
2. 如何定义导出模块 :
define()
exports
module.exports
3. 如何依赖模块:
require()
4. 如何使用模块:
seajs.use()
-->
<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
seajs.use('./js/modules/main')
</script>
</body>
</html>
ES6_Babel_Browserify ※※
ES6规范很重要,目前使用最多的就是它。而且ES6要比CommonJS和AMD好用的多。
ES6的依赖模块也需要编译打包处理,这是因为有一些浏览器没有ES6的语法,所以需要把它编译成ES5,来让浏览器识别。当然我们之前也提到过,浏览器也不识别require
语法,所以这就需要用到Browserify
。
基本语法:
- 导出模块:
export
- 引入模块:
import
实现(浏览器端):使用Babel
将ES6编译为ES5代码,再使用Browserify
编译打包成JS。
项目结构:
①首先定义package.json文件:
{
"name": "es6-babel-browserify",
"version": "1.0.0"
}
②安装:babel-cli
, babel-preset-es2015
和browserify
babel-cli
:command line interface (命令行接口),让我们能使用babel的命令。
babel-preset-es2015
:下载ES6转为ES5的所有插件。
③定义.babelrc文件
{
"presets": ["es2015"]
}
当babel运行的时候,它会先去读.babelrc文件,看看要做什么: "presets": ["es2015"]
。数组中放es2015,它就会转ES6。(也能放其他参数,比如"react"
,它就会转JSX)
(以后可能看到很多rc文件,rc:run control ,即运行时控制文件:运行的时候读这个文件)
④ 定义js文件
module1.js
//暴露模块 分别暴露
export function foo() {
console.log('module1 foo()');
}
export let bar = function () {
console.log('module1 bar()');
}
export let arr = [1, 2, 3, 4, 5];
module2.js
//统一暴露
function fun1() {
console.log('module2 fun1()');
}
function fun2() {
console.log('module2 fun2()');
}
export {fun1, fun2};
module3.js
//这种方式可以暴露任意数据类型,暴露什么数据接收到的就是什么数据
export default {
name: 'Tom',
setName: function (name) {
this.name = name
}
}
app.js
//引入其他模块
import {foo, bar} from './module1'
import {arr} from './module1'
import {fun1, fun2} from './module2'
foo();
bar();
console.log(arr);
fun1();
fun2();
//上述用法如果出现重名,显然用不了
import person from './module3'
person.setName('JACK');
console.log(person.name);
//引入第三方模块(jQuery)
import $ from 'jquery'
$('body').css('background', 'red');
⑤ 编译
⑥ 页面中引入测试
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript" src="js/lib/bundle.js"></script>
</body>
</html>
通过最终的这种JS模块化规范,不难看出,已经很接近目前的node + webpack 的形式了。之所以会出现webpack就是因为,在这种方式下,如果想要修改其中内容,每次都需要重新编译,先让其转化ES5,再让其编译js,最后才能执行,每次都需要反复敲命令,十分麻烦。而且在最终打包时,该如何处理css文件,又该如何处理图片等静态资源,又能否实现代码的压缩,实现懒加载等一系列复杂操作需要实现,这样就有了现在的webpack。