登录
本文主要内容为 js 的模块化功能,包含如下:
最初的 js 定位于前端简单的用户交互,没有考虑过它成规模后的模块化。
直到 2009 年出现了 node.js 这种用于后台的 js,作为后台语言,逻辑和功能的复杂度飙升,就必须考虑模块化了。
node.js 开发之初就遵守了 CommonJS 规范。
CommonJS 是一个规范,实现它的语言有很多,每个语言也并不是实现它的全部功能。
再往后,客户端设备性能飙升,功能愈加复杂、用户交互要求愈来愈高,前端慢慢也足够复杂,也到了需要考虑模块化引入功能模块的程度。
虽然后台的 node.js 和前端的 JavaScript 是同一门语言,但毕竟运行环境不同,后台服务器下载同一个版本的 node.js 就能统一,前端却有五花八门的浏览器、不同的版本等等,还有不同用户的网速不同、不同的设备、用的是 wifi 还是流量等等,太多条件的考虑和限制。
关键是前端不支持 CommonJS,但是前端也想拥有模块化加载的能力。。。
于是 2010 年左右,出现了 RequireJS 这个 js 工具库,而这个工具库,就是基于 AMD(Asynchronous Module Definition) 这个模块化开发规范,如果 node 是 CommonJS 的一种实现,那 RequireJS 就是 AMD 的实现。
RequireJS 出现后,2011 年左右,国内也出现了一个 SeaJS 的工具库,它基于的是 CMD(Common Moudle Definition) 规范。
最后,随着 js 语言本身的发展,当 js 发展到 ES6 时,js 的本身规范,也出现了一种模块化开发规范,但浏览器支持性很差,如果想要使用,最好还是使用工具,比如 babel 转成 ES5 的语法。
以上就是 js 的模块化发展,除了 RequireJS 和 SeaJS,还出现过其他类似功能的模块化工具,但都没有这两个流行。
导入模块的时候,会有一个依赖链逻辑,详情请移步:npm 功能使用
以下说的 CommonJS,都只是 nodejs 中的 CommonJS。
nodejs 中的 CommonJS 同步加载的,引入的模块是「单例模式」。
使用 module.exports 导出,使用 require 引入。
node 中每个 js 文件都是一个模块,会为每个 js 模块提供以下四个变量:
1. module:当前 js 文件这个模块的对象本身
打印 module 有以下主要字段
2. exports:
其实就是 module.exports,可以理解为,这个文件最上面有一行 let exports = module.exports
;
3. require:
用于引入其他模块,用法都知道。
4. global:
node 的全局对象,因为每个文件都是一个闭包的模块,global 是全局唯一且任意文件均可修改访问的对象,类似于前端页面中的 window
// util_1.js
module.exports.a = 10
exports.b = 20
// index.js
let util_1 = require('./util_1')
console.log(util_1) // { a: 10, b: 20 }
// util_1.js
// 注意下面不能写 exports = ...
// 因为 exports === module.exports
// 如果再给 exports 赋值,那 exports 就只相当于一个普通变量,和 module 无关了
module.exports = {
a: 10,
b: 20,
}
// index.js
let util_1 = require('./util_1')
console.log(util_1) // { a: 10, b: 20 }
HTML 原生支持的异步加载 js 文件,只能控制 HTML 中 script 标签引入 js 时的加载和执行,详情请移步:script 标签的异步属性。
<script src="./js/util_1.js"></script>
<script src="./js/util_2.js"></script>
<script src="./js/util_3.js"></script>
<script src="./js/util_4.js"></script>
<script src="./js/index.js"></script>
var scriptDom = document.createElement('script')
scriptDom.src = './js/util.js'
scriptDom.onload = scriptDom.onerror = function() {
// 加载成功或失败
document.querySelector('body').removeChild(scriptDom) // 删除 dom
// code
}
document.querySelector('body').appendChild(scriptDom)
前端需要模块化引入,一个原因是上面所述原因,前端的功能的复杂度提升,需要组件化、模块化,还有一个原因,就是前端进行开发时,需要的一些插件,大部分体积小的工具库,可以一开始就引入,但有一些体积大的库,如果也应开始就引入,会导致首屏或某页的文件体积巨大,延长页面加载时间,影响用户体检。
一些并不需要一开始就引入的场景:
同样,在开发 vue、react 等单页面应用时,虽然项目又很多页面,但使用率高的只有几个,使用率特别低的也有几个,用户在打开某页时,没有必要把所有页面资源都下载下来,这些框架有各自的方法进行资源的异步加载。
Requirejs 可以人为可控的引入 js 文件和使用 Requirejs 定义的模块。
npm install requirejs
Requirejs 的 CDN 链接复制地址:点击打开进行复制
简单使用,直接引入 require.js,然后使用 require() 引入需要的其他文件。
语法:require(assetsArr, callback)
<script src="./require.js"></script>
<script>
// 可以发现给 window 添加了至少 require 和 requirejs 两个字段
console.log(
require === window.require &&
require === requirejs &&
require === window.requirejs
) // true
// util_1.js 和 util_2.js 并行下载,下载完后立即执行,都执行完成后,执行回荡函数
require(['./util_1.js', '/js/util_2.js'], function(util_1, util_2) {
console.log('require 加载文件完成')
console.log(util_1, util_2) // 引入的两个资源对象
})
</script>
如果没有依赖项,则回调方法的默认参数为(require, exports, module)
可以给 引入 require.js 的 script 标签,添加一个 data-main 属性,data-main 本身是一个 HTML 全局属性。
data-main 值需要是一个 js 文件的地址,他在这里的作用,是指定网页程序的主模块,也就是 require.js 文件加载完成后,再去下载并执行的 js 文件。
可以把 require 的使用,移到 dataMain.js 文件中了。
<!-- html 文件中 -->
<script data-main="./dataMain.js" src="./require.js"></script>
// 文件 dataMain.js 中
require(['./util_1.js', '/js/util_2.js'], function(util_1, util_2) {
console.log('require 加载文件完成')
console.log(util_1, util_2) // 引入的两个资源对象
})
使用 require.js 提供的全局 define 方法,可以定义自己的模块了,像上面的 callback 回调函数中的参数,终于可以派上用上了。
语法:define(id?, dependencies?, factory)
如果同一文件多次调用 define 方法暴露模块,则以第一次执行的结果为准
以下展示常用用法:
// util_1.js 中
define({
a: 100,
b: 200
})
// util_2.js 中
define(['util_min'], function(util_min) {
return util_min
})
// util_min.js 中
define(function() {
return 'Name is util_min'
})
// 其他文件夹中使用 require 引入该模块
require(['./util_1.js', './util_2.js'], function(util_1, util_2) {
console.log(util_1) // {a: 100, b: 200}
console.log(util_2) // 'Name is util_min'
})
require.config(options) 顾名思义,就是用来配置 require,入参为 options 为配置对象,常用的有以下几个配置:
注意:
这其实是本来就存在的情况,这里只是重申一下:原生 js 开发时,因为所有的 js 最终都会引入到 html 文件中执行,所以 js 中所写的相对位置,都是基于这个 html 相对的。
所以,如果 require.config 写在单独的 js 文件中,它里面需要写路径的比如 baseUrl、paths 等,如果使用相对位置,就需要注意一下了。
使用的代码示例:
require.config({
baseUrl: "/js/",
paths: { // 预先写好几个模块的路径,使用时才引入
"module_1": "module_1", // 最终指向该文件 /js/module_1.js
"module_2": "module_2", // 最终指向该文件 /js/module_2.js
},
waitSeconds: 10, // 超时时间设置为 10 秒
})
// 因为 /js/module_1.js 已经在 require.config 中定义,所以这里可以直接写 module_1,即可引入
require(['module_1'], function(module_1) {
// code
})
// module_2 只定义了路径,使用最终没有使用,所以不会引入这个 js
ES6 的模块功能主要由两个命令构成:
export:模块输出的对外接口,一个模块就是一个独立的文件,文件内部的所有变量外部都无法获取,如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。
import:用于引入其他模块提供的功能
导出模块默认值
模块默认值只能有一个,所以这个导出写法一个文件只能写一次,也可以不写,那就没有模块默认值而已。
需要使用 export default
命令:
// 导出直接量或匿名方法
export default 100
export default function() {}
// 导出具名方法,此方法在此文件的其他位置可以调用
export default function a() {}
// 先声明,再导出
const a = 123
// or
const a = function() {}
// or
function a() {}
export default a
散装的变量导出
模块默认值导出,只能有一个,不够用,所以有了这个散装的导出写法。
两者的概念上的区别是:
写法上的区别是:
export default
。export
。下面是散装变量的导出写法
// 声明的同时导出
export const hello = 'hello'
export function b() {}
// 先声明再导出
const a = 100
const b = function() {}
// 先声明再导出可以单个单个的导出
export { a }
export { b }
// or 一起导出
export { a, b }
// or 导出时给变量重命名
export { a, b as c }
只执行,不引入值
// 只执行 js 文件或 css
import './util.js'
import './index.css'
引入模块的默认值
该写法只能引入使用 export default 导出的默认值,不能引入其他,如果未导出默认值,则为 undefined
// util.js 文件中
const a = 300
export default a
// index.js
import util from './util.js'
console.log(util) // 打印 300
// 因为 util.js 中的模块默认值只可能有一个,所以 index.js 中只需要一个形参来接受
// 变量名随便写,不用和 util.js 导出的变量名一致
引入模块散装输出的值
// 引入散装输入的值时,每一个值的变量名都是确定的,不能乱写
// 但仍然可以使用 as 给变量重命名
import { a, b, c as myC } from './util.js'
同时引入默认值和散装值
必须默认值在前,散装的解构取值在后
import util, { a, b, c as myC } from './util.js'
导入模块的全部内容
使用 * as
可以把模块默认值和散装值,全部放在后面的形参中,其中 util.default 放置的是默认输出值。
如果输出的散装值中有 default 这个字段,编译会报错
// util.js 文件中
export default 300
const a = 100
const b = 200
export { a, b }
// index.js
import * as utils from './util.js'
console.log(JSON.stringify(utils))
// 打印内容见下,可见除了模块默认值被放在了 default 字段中,其他散装值都直接放在了对象中:
{"a":100,"b":200,"default":300}
整合多个文件统一导出
有时,大量工具js放在一个文件夹中,但其他很多文件想使用这些工具时,想只引入一个文件,则可以使用一个文件把工具都引入,然后直接导出,充当一个中介:
// ./utils/util.js 文件中
export default 300
const a = 100
const b = 200
export { a, b }
// ./utils/index.js 中介文件中
export { default, a, b } from './util.js'
// 可以引入再导出其他文件的值
// ./index.js 可以只引入一个文件,下面是导入了模块的全部内容,也可以使用其他引入方式
import * as utils from './utils/index.js'
console.log(JSON.stringify(utils))
{"a":100,"b":200,"default":300}