js模块化规范

656 阅读7分钟

概述

时间:211206-211207

内容

JS模块化的理解

1.什么是模块/模块化?

将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起。
块的内部数据/实现是私有的,只是向外部暴露一些接口(方法)与外部其它模块通信。

2.为什么要模块化?

  • Web sites are turning into Web Apps
  • Code complexity(复杂度) grows as the site gets bigger
  • Highly decoupled(解耦) JS files/modules is wanted
  • Deployment(部署) wants optimized(优化) code in few HTTP calls

3.模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离,按需加载
  • 更高复用性
  • 高可维护性

4.页面引入加载script

如果直接通过script标签引入多个js文件,会造成:

  • 请求过多
  • 依赖模糊
  • 难以维护

那么,如何才能做到:在享受模块化带来的好处的同时,又能避免模块化带来的弊端呢?模块化规范 应运而生。

模块化的进化史

1 . 最早,我们这么写代码:

整个网站只有一个js文件,全部js代码都写在这一个js文件中。
所有变量/函数都直接定义在全局作用域中。

缺点:Global被污染,很容易命名冲突。

//xxx.js
function foo(){
    //...
}
function bar(){
    //...
}

2 . 简单封装: Namespace模式

-减少Global上的变量数目
-本质是对象,一点都不安全

var MYAPP = {
    foo: function(){},
    bar: function(){},
}

MYAPP.foo()

3 . 匿名闭包: IIFE模式

IIFE:立即执行函数

函数是JavaScript唯一的Local Scope

var Module = (function(){
    var _private = "safe now"
    var foo = function(){
        console.log(_private)
    }
    return {
        foo: foo
    }
})()

4 . 再增强一点:引入依赖

这就是 模块模式

也是现代模块实现的基石

var Module = (function($){
    var _$body = $('body')  //we can use JQuery now?
    var foo = function(){
        console.log(_body)  //特权方法
    }

    //Revelation Pattern
    return {
        foo: foo
    }
})(jQuery)

Module.foo()

模块化规范

CommonJS

规范

说明
  • wiki.commonjs.org/wiki/Module…
  • 每个js文件都可当作一个模块
  • 在服务器端: 模块的加载是运行时同步加载的
  • 在浏览器端: 模块需要提前编译打包处理
基本语法
  • 暴露模块
module.exports = value 
exports.xxx = value 

问题: 暴露的模块到底是什么?

  • 引入模块
require(xxx) <br>

第三方模块: xxx为模块名
自定义模块: xxx为模块文件路径

实现

  • 服务器端实现
    Node.js
    nodejs.cn/

  • 浏览器端实现
    Browserify
    browserify.org/
    也称为CommonJS的浏览器端的打包工具

案例练习1:Cmmonjs模块化规范在服务端(Nodejs)的应用。

  1. 下载安装node.js

    查看node版本: node -v

  2. 创建项目结构

    执行指令 npm init -y,可以快速生成一个项目依赖描述文件 package.json。

commonjs-node-QQ截图20211206120946.png

  1. 下载第三方模块

    uniq:一个可以对数组去重的包。

    npm install uniq --save
    
  2. 模块化编码

    小细节:app.js中,先引入第三方库,再引入自定义的模块。

// module1.js
//暴露一个对象
module.exports = {
    msg: 'module1',
    foo(){
        console.log(this.msg)
    }
}

//module2.js
//暴露一个函数
module.exports = function(){
    console.log('module2')
}

//module3.js
//在暴露的对象上添加两个函数foo、bar
exports.foo = function(){
    console.log('foo() module3')
}

exports.bar = function(){
    console.log('bar() module3')
}

exports.arr = [1, 2, 1, 2, 3, 4, 5, 5]

//app.js
//导入第三方模块
let uniq = require('uniq')

//导入自定义模块
let module1 = require('./modules/module1.js')
let module2 = require('./modules/module2.js')
let module3 = require('./modules/module3.js')


console.log(module1.msg)
module1.foo()

module2()

module3.foo()
module3.bar()
console.log(module3.arr)
console.log(uniq(module3.arr))
  1. 执行app.js

    命令行通过node执行入口文件:node app.js
    小技巧:Tab键可以自动补全文件名。

输出的结果:

image.png

案例练习2:Cmmonjs模块化规范在浏览器端的应用。

Browserify模块化使用教程

1.创建项目结构
dist/或build/ 都表示打包输出目录。
src/ 表示源文件目录。

image.png

2.下载 browserify
-全局: npm install browserify@14.5.0 -g
-局部: npm install browserify@14.5.0 --save-dev

视频中browserify版本是:@14.5.0

注意:
1.browserify需要同时全局安装和局部安装才能使用,比较特别。
2.注意安装第三方包时,要区分开发依赖和运行依赖。

3.定义模块代码

//app.js
//导入第三方模块
let uniq = require('uniq')

//导入自定义模块
let module1 = require('./module1.js')
let module2 = require('./module2.js')
let module3 = require('./module3.js')


console.log(module1.msg)
module1.foo()

module2()

module3.foo()
module3.bar()
console.log(module3.arr)
console.log(uniq(module3.arr))

4.打包处理js
命令行执行指令: browserify ./src/app.js -o ./dist/bundle.js

./src/app.js 打包入口文件

./dist/bundle.js 打包输出文件

5.页面使用引入

<script src="./dist/bundle.js" type="text/javascript" charset="utf-8"></script>

在浏览器中打开index.html,F12打开控制台,输出如下:

image.png

AMD

规范

说明
  • Asynchronous Module Definition(异步模块定义)
  • github.com/amdjs/amdjs…
  • 专门用于浏览器端,模块的加载是异步的。
基本语法
  • 定义暴露模块
//定义没有依赖的模块
define(function(){
    return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
    return 模块
})
  • 引入使用模块
require(['module1', 'module2'], function(m1, m2){
    //使用m1/m2
})

实现(浏览器端)

案例1:不使用AMD规范,如何在浏览器端实现模块化?IIFE。

1.编写模块

image.png

//a.js
//定义一个没有依赖的模块
(function(window){
	let name = 'a.js'
	function getName(){
		return name
	}
	window.a = { getName }
})(window)

//b.js
//定义一个有依赖的模块
(function(window, a){
	let msg = 'b.js'
	function showMsg(){
		console.log(msg)
		console.log(a.getName())
	}
	window.b = {showMsg}
})(window, a)

//app.js
(function(b){
	b.showMsg()
})(b)

2.页面引入

//index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <script src="./src/a.js" type="text/javascript" charset="utf-8"></script>
        <script src="./src/b.js" type="text/javascript" charset="utf-8"></script>
        <script src="./src/app.js" type="text/javascript" charset="utf-8"></script>
    </body>
</html>

浏览器打开index.html,console控制台输出如下:

image.png

IIFE模块化 存在的缺点:a.js、b.js、app.js必须依次引入,顺序不能混乱,否则会报错。

案例2:使用AMD规范——自定义模块。

1.下载require.js,并引入

2.创建项目结构

libs/ 存放第三方包的目录

image.png

3.编写自定义模块代码

//a.js
//定义一个没有依赖的模块
define(function(){
    let name = 'a.js'
    function getName(){
        return name
    }

    //暴露模块
    return {getName}
})

//b.js
//定义一个有依赖的模块
define(['a'], function(a){
    let msg = 'b.js'
    function showMsg(){
        console.log(msg)
        console.log(a.getName())
    }

    //暴露模块
    return {showMsg}
})

4.编写入口文件 app.js 代码

(function(){
    requirejs.config({
        //baseUrl: 'src/modules', //基本路径 相对于根目录的路径

        //配置 模块名和模块路径 之间的映射关系
        paths: { //相对于入口文件的路径
            a: './modules/a',  //不要加.js后缀
            b: './modules/b'
        }
    });

    requirejs(['b'], function(b){
        b.showMsg()
    })
})()

requirejs.config()方法:用来配置自定义模块的模块名模块路径的映射关系。

baseUrl选项:配置公共的基本路径。
该选项值一般设置成自定义模块的存放目录,从根路径出发。
如果配置了该选项,则会默认从该目录中寻找依赖。

paths选项:配置每个自定义模块的具体的映射关系。从相对于入口文件出发。如果配置了baseUrl选项,则会在paths选项路径前面拼接上baseUrl选项路径。

baseUrl选项、paths选项 都是用来配置依赖的查找路径的。可以单独使用其中一个,也可以配合在一起使用。

注意: RequireJS会自动拼接上.js后缀,所以paths选项路径只要写js文件名。

5.html中引入require.js,并通过data-main属性指定入口文件

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <!-- 引入require.js并指定js主文件的入口 -->
        <script src="libs/require.js" data-main="src/app.js" type="text/javascript" charset="utf-8"></script>
    </body>
</html>

在浏览器中,打开index.html文件,输出如下:

image.png

案例3:使用AMD规范——第三方模块。

如何在AMD模块中,使用第三方模块,如jquery?

先将jquery源码js文件下载到项目中,再在入口文件app.js中配置jquery模块的路径映射关系,最后在需要使用的模块中引入和使用即可。

image.png

//app.js   

(function(){
    requirejs.config({
        paths: {  
            a: './modules/a',   
            b: './modules/b',
            jquery: '../libs/jquery-3.6.0.min' //配置jQuery库的路径映射关系
        }
    });

    //导入、使用jQuery
    requirejs(['b', 'jquery'], function(b, $){
        b.showMsg()
        $('body').css('backgroundColor', 'red') 
    })
})()

注意:jQuery内部会判断自己是否是在AMD模块中使用,是则暴露出去的名字是小写的jquery。所以在paths选项配置路径映射关系时,key不能写成jQuery。

jquery: '../libs/jquery-3.6.0.min' //正确
jQuery: '../libs/jquery-3.6.0.min' //错误

浏览器效果:

image.png

CMD(了解即可)

规范

说明
  • Common Module Definition(通用模块定义)
  • github.com/seajs/seajs…
  • 专门用于浏览器端,模块的加载是异步的
  • 模块使用时才会加载执行
基本语法
  • 定义暴露模块
//定义没有依赖的模块
define(function(require, exports, module){
    exports.xxx = value
    module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
    //引入依赖模块(同步)
    var module2 = require('./module2')
    //引入依赖模块(异步)
    require.async( './module3' , function (m3) {})
    //暴露模块
    exports.xxx = value
})
  • 引入使用模块
define(function (require) {
    var m1 = require('./module1')
    var m4 = require('./module4')
    m1.show()
    m4.show()
})

实现(浏览器端)

案例1

1.下载Sea.js,并引入

  • 官网: seajs.org/
  • github: https:/lgithub.com/seajs/seajs
  • 将sea.js导入项目: js/libs/sea.js

2.创建项目结构

image.png

3.使用CMD语法编写自定义模块

一共有4个自定义模块,module1.js、module2.js、module3.js都是无依赖模块,module4.js 依赖了module1.js、module2.js这两个模块。

入口文件 app.js中 引入了module3.js、module4.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 msg = 'module3'
    function func(){
            console.log(msg)
    }

    //暴露模块
    exports.func = func
})

//module4.js
define(function(require, exports, module){
    let msg = 'module4'

    function func2(){
            console.log(msg)
    }

    //同步引入模块
    let module1 = require('./module1')
    console.log(module1.foo())

    //异步引入模块
    //async()方法 参数1:依赖的路径  参数2:依赖加载完成时的回调函数
    require.async('./module2', function(module2){
            module2()
    })

    //暴露模块
    exports.func2= func2
})

入口文件 app.js

define(function(require){
    let module3 = require('./module3')
    module3.func()

    let module4 = require('./module4')
    module4.func2('')
})

4.html引入sea.js,执行seajs.use()方法

seajs.use()方法: 参数为入口文件的路径。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <script src="js/libs/sea.js" type="text/javascript" charset="utf-8"></script>
        <script type="text/javascript">
                seajs.use('./js/modules/main.js')
        </script>
    </body>
</html>

浏览器中打开index.html文件,控制台输出如下:

image.png

ES6(重点掌握)

规范

说明
基本语法
  • 导出模块: export

  • 引入模块: impors

实现(浏览器端)

  • 使用Babel将ES6编译为ES5代码
  • 使用Browserify编译打包js

初体验:ES6-Babel-Browserif使用教程

1.定义package.json文件

2.安装babel-cli, babel-preset-es2015browserify

  • npm install babel-cli browserify -g
  • npm install babel-preset-es2015 --save-dev
  • preset预设(将es6转换成es5的所有插件打包)

3.定义 .babelrc 文件

{
    "presets": ["es2015"]
}

4.编码

//module1.js
//暴露模块 分别暴露
export function foo(){
    console.log('foo() module1.js')
}

export function bar(){
    console.log('bar() module1.js')
}

export let arr1 = [1,2,3]

//module2.js
//暴露模块 统一暴露
function func1() {
    console.log('func1() module2.js')
}

function func2() {
    console.log('func2() module2.js')
}

let arr2 = [1, 2, 3]

export {
    func1,
    func2,
    arr2
}

//module3.js
//暴露模块 默认暴露
export default () => {
    console.log('module3.js 默认暴露 的箭头函数')
}

main.js

//引入其它模块
//语法: import {...} from '路径'

import {
    foo,
    bar,
    arr1
} from './module1.js'

import {
    func1,
    func2,
    arr2
} from './module2.js'

import module3 from './module3.js'

foo()
bar()
console.log(arr1)

func1()
func2()
console.log(arr2)

module3()

5.编译

  • 使用Babel将ES6编译为ES5代码(但包含CommonJs语法) :

    babel src/js -d build/
    
  • 使用Browserify编译js :

    browserify  build/main.js -o dist/bundle.js
    

image.png

image.png

image.png

6.页面中引入测试

<script src="../dist/bundle.js" type="text/javascript" charset="utf-8"></script>

7.在浏览器中打开index.html,控制台输出如下:

image.png

ES6 Module规范中,如何使用第三方模块?

1.安装jquery

npm install jquery@1

2.引入第三方模块

//app.js
import $ from 'jquery'
...
$('body').css('backgroundColor', 'red')

浏览器效果:

image.png

总结/问题

一些总结

  • 在node端,可以直接使用Commonjs模块化规范进行模块化开发。开发完成后,直接通过node来执行入口js文件即可。

  • 浏览器不认识Commonjs模块化语法,需要使用rowserify工具对源代码js进行打包处理后才能,通过script标签引入打包输出js文件到html页面中运行。

  • 源代码 js文件 可以使用Commonjs模块化语法进行编写,但是如果要在浏览器中运行,则需要先使用Browserify工具 对源代码进行处理,处理后输出的js文件就可以在浏览器中运行了。

  • 区别Node与Browserify

    Nodejs:可以直接执行 Commonjs模块化语法编写的js文件。
    Browserify:将 Commonjs模块化语法编写的js文件进行打包处理,输出一个新的js文件,新js文件可以在浏览器中运行。

  • 所有的第三方库都可以在AMD模块中使用吗?
    不是,第三方库自身使用AMD语法进行开发,才行。
    jQuery内部支持了AMD规范,而有的库不支持。不是所有的库都支持AMD规范。

  • CMD 结合了 CommonJSAMD 两种规范。

  • 为什么需要全局按Babel-cli呢?

    CLI: command line interface 命令行接口。

    一般cli都是用来为第三方包提供全局指令的。

    全局安装nodejs后,在任意目录都可以执行node指令,这是因为node内置了 node-cli,node-cli的作用是提供全局可用的node指令。

    但是,babel-preset-es2015 内部没有内置CLI,所以需要额外全局安装Babel-cli,来提供全局可用的babel指令。

  • babel-preset-es2015 的作用?

    Babel不仅可以将ES6语法转换为ES5语法,还可以转换其它语法(如React的JSX)。不同的语法需要使用不同的插件来转换。babel-preset-es2015的作用就是只将转换ES6语法的插件都下载下来使用。

  • 为什么要定义 .babelrc 文件?
    .babelrc 文件 本质是一个json文件。

    Babel工作时会先去读取 .babelrc 这个配置文件,presets数组中的字符串"es2015"告诉Babel要去转换ES6的语法。即可以通过手动为presets数组添加不同元素的方式来告诉Babel它要去进行哪些处理。

    rc: run control 运行控制
    rc后缀的文件:一般表示运行时控制文件,即运行时需要读的文件。

  • BabelES6模块化语法转换为CommonJS模块化语法,所以还需要使用Browserify将CommonJS模块化语法转换为浏览器认识的语法。

  • Babel vs Browserify

    它们处理js文件时有什么区别?

    Babel 会一一处理每个js文件,然后一一单独输出。

    Browserify会从入口文件出发,将依赖的所有模块的代码都打包到一个文件中,最终只输出一个文件。

    相同点:如果指定的输出目录不存在,则会自动创建。

  • ES6 Module规范中,导入模块时的注意事项:

    一个模块如果是通过分别暴露统一暴露语法向外暴露变量/函数时,在导入这个模块时必须使用对象解构赋值的方式来进行导入。

    import from表达式语法: import {...} from '路径'

//错误的导入方式:不能直接赋值为一个变量。
import module1 from './module1.js'
import module2 from './module2.js'
//正确的导入方式:通过对象解构赋值语法分别接收导出的变量/函数。
import {
    foo,
    bar,
    arr1
} from './module1.js'

import {
    func1,
    func2,
    arr2
} from './module2.js'
  • ES6模块化开发时,如果修改了源代码,则需要重新执行Babel和Browserify指令,重新进行转换、打包,才能看到最新的代码效果。

  • ES6 Module规范中,有三种方式可以向外暴露模块:分别暴露统一暴露默认暴露

    不同的暴露方式,引入的方式也有所不同。

    默认暴露:可以暴露任意数据类型,暴露什么数据接收到的就是什么数据。

    分别暴露、统一暴露:export xxx
    默认暴露:export default xxx

    引入分别暴露、统一暴露的模块:import {xxx, xxx, ...} from 'xxx'
    xxx不能随意写,必须使用依赖模块内部起的名字。

    引入默认暴露的模块:import xxx from 'xxx'
    xxx可以随意起。

  • jQuery不同版本的区别?

    jQuery2.x、3.x版本不支持低版本浏览器。 在开发中,一般使用jQuery 1.x版本,因为它兼容低版本的浏览器。

  • npm安装包时,如何指定版本?

npm安装第三方包时,如果不指定版本,则默认安装最新的版本。

npm install jquery

如何指定版本呢?可以通过@来指定版本。

 npm install jquery@1  //安装1.x系列中最新的版本
 
 npm install jquery@1.11.3  //安装1.11.3版本

学习资源

官方文档

视频教程

教程概述:

当项目功能越来越多,代码量便也会越来越多,后期的维护难度会增大,此时在JS方面就会考虑使用模块化规范去管理。

本视频内容涵盖:理解模块化,为什么要模块化,模块化的优缺点以及模块化规范。并且将带领大家学习开发中非常流行的commonjs, AMD, ES6、CMD规范。

建议同学们学习完模块化规范以后再学习项目构建,以更好的武装自己的技能。

参考文章