编程崽

登录

一叶在编程苦海沉沦的扁舟之上,我是那只激情自射的崽

js 的加载和模块化

js 的加载和模块化

本文主要内容为 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 功能使用

node 的 CommonJS

以下说的 CommonJS,都只是 nodejs 中的 CommonJS。

定义和介绍

nodejs 中的 CommonJS 同步加载的,引入的模块是「单例模式」。

使用 module.exports 导出,使用 require 引入。

node 中每个 js 文件都是一个模块,会为每个 js 模块提供以下四个变量:

1. module:当前 js 文件这个模块的对象本身

打印 module 有以下主要字段

  • id 模块的识别符,通常是带有绝对路径的模块文件名。
  • filename 模块的文件名,带有绝对路径。
  • loaded 布尔值,表示模块是否已经完成加载。
  • parent 对象,表示调用该模块的模块。
  • children 数组,表示该模块要用到的其他模块。
  • exports 关键,表示模块对外输出的值,默认值为一个空对象,其他模块引入该模块,得到的就是这个字段的值。
  • path 字符串,一个路径
  • paths 数组

2. exports:

其实就是 module.exports,可以理解为,这个文件最上面有一行 let exports = module.exports;

3. require:

用于引入其他模块,用法都知道。

4. global:

node 的全局对象,因为每个文件都是一个闭包的模块,global 是全局唯一且任意文件均可修改访问的对象,类似于前端页面中的 window

一个字段一个字段的输出

js 复制代码
// 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 }

直接输出一个对象

js 复制代码
// 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 }

前端原生引入 js 方法

HTML 原生支持的异步加载 js 文件,只能控制 HTML 中 script 标签引入 js 时的加载和执行,详情请移步:script 标签的异步属性

使用标签加载全部

html 复制代码
<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>

使用 js 控制加载其他 js

js 复制代码
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)

AMD 规范的 Requirejs

官方文档:https://requirejs.org/

前端需要模块化引入,一个原因是上面所述原因,前端的功能的复杂度提升,需要组件化、模块化,还有一个原因,就是前端进行开发时,需要的一些插件,大部分体积小的工具库,可以一开始就引入,但有一些体积大的库,如果也应开始就引入,会导致首屏或某页的文件体积巨大,延长页面加载时间,影响用户体检。

一些并不需要一开始就引入的场景:

  • 不同环境判断后引入:使用不同浏览器时,根据浏览器兼容情况,引入不同的兼容性 polyfill,或者不同网络环境,需要功能降级。
  • 当使用某个功能时才引入:比如点击编辑按钮时,引入编辑器插件;点击切换图表时,引入需要的图表库。
  • 控制不重要的大文件延后引入:首页默认就需要展示一个体积巨大的图表,但可以先加载处页面整体框架,再添加局部 loading,开始引入图表库再展示。

同样,在开发 vue、react 等单页面应用时,虽然项目又很多页面,但使用率高的只有几个,使用率特别低的也有几个,用户在打开某页时,没有必要把所有页面资源都下载下来,这些框架有各自的方法进行资源的异步加载。

Requirejs 可以人为可控的引入 js 文件和使用 Requirejs 定义的模块。

AMD 规范

基础用法一:引入 + 使用

使用npm

sh 复制代码
npm install requirejs

CND地址

Requirejs 的 CDN 链接复制地址:点击打开进行复制

简单使用,直接引入 require.js,然后使用 require() 引入需要的其他文件。

语法:require(assetsArr, callback)

  • assetsArr:资源地址的字符串的数组,比如,require(['./index.js', '/js/min.js']) 引入需要的其他 js,数组中的几个 js 文件,哪个先下载完就先执行,无先后顺序。
  • callback:可选,assetsArr中的资源全部引入并执行后的回调函数,会把引入的资源,按顺序传入(如果资源没有暴露出 Requirejs 支持的模块,那就没有了)。
html 复制代码
<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)

使用 data-main 属性

可以给 引入 require.js 的 script 标签,添加一个 data-main 属性,data-main 本身是一个 HTML 全局属性

data-main 值需要是一个 js 文件的地址,他在这里的作用,是指定网页程序的主模块,也就是 require.js 文件加载完成后,再去下载并执行的 js 文件。

可以把 require 的使用,移到 dataMain.js 文件中了。

html 复制代码

<!-- html 文件中 -->
<script data-main="./dataMain.js" src="./require.js"></script>
js 复制代码
// 文件 dataMain.js 中
require(['./util_1.js', '/js/util_2.js'], function(util_1, util_2) {
  console.log('require 加载文件完成')
  console.log(util_1, util_2) // 引入的两个资源对象
})

使用 define 定义模块

使用 require.js 提供的全局 define 方法,可以定义自己的模块了,像上面的 callback 回调函数中的参数,终于可以派上用上了。

语法:define(id?, dependencies?, factory)

  • id:可选,字符串,声明的这个模块的模块名
  • dependencies:可选,此模块的依赖项,字符串数组,默认为["require", "exports", "module"]
  • factory:模块初始化要执行的函数或对象,如果为函数,它应该只被执行一次,返回值为模块输出的值;如果是对象,则此对象就是该模块的输出值

如果同一文件多次调用 define 方法暴露模块,则以第一次执行的结果为准

以下展示常用用法:

js 复制代码
// 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 配置

require.config(options) 顾名思义,就是用来配置 require,入参为 options 为配置对象,常用的有以下几个配置:

  • baseUrl:路径,在 require.config 中,引入模块时的 base 路径。
  • waitSeconds:引入文件时的超时时间,单位为秒,默认 7 秒。
  • paths:对象,可以预先声明一些模块名和模块路径,之后引入需要的模块时,依赖数组中直接写模块名即可引入。
  • shim:对于一些符不符合 AMD 模块规范的 js,可以使用此字段进行处理,详情可至官网或网上学习了解。

注意:

这其实是本来就存在的情况,这里只是重申一下:原生 js 开发时,因为所有的 js 最终都会引入到 html 文件中执行,所以 js 中所写的相对位置,都是基于这个 html 相对的。

所以,如果 require.config 写在单独的 js 文件中,它里面需要写路径的比如 baseUrl、paths 等,如果使用相对位置,就需要注意一下了。

使用的代码示例:

js 复制代码
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 的模块化

ES6 的模块功能主要由两个命令构成:

export:模块输出的对外接口,一个模块就是一个独立的文件,文件内部的所有变量外部都无法获取,如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。

import:用于引入其他模块提供的功能

导出模式 export

导出模块默认值

模块默认值只能有一个,所以这个导出写法一个文件只能写一次,也可以不写,那就没有模块默认值而已。

需要使用 export default 命令:

js 复制代码
// 导出直接量或匿名方法
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

下面是散装变量的导出写法

js 复制代码
// 声明的同时导出
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 }

导入模式 import

只执行,不引入值

js 复制代码
// 只执行 js 文件或 css
import './util.js'
import './index.css'

引入模块的默认值

该写法只能引入使用 export default 导出的默认值,不能引入其他,如果未导出默认值,则为 undefined

js 复制代码
// 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 导出的变量名一致

引入模块散装输出的值

js 复制代码
// 引入散装输入的值时,每一个值的变量名都是确定的,不能乱写
// 但仍然可以使用 as 给变量重命名
import { a, b, c as myC } from './util.js'

同时引入默认值和散装值

必须默认值在前,散装的解构取值在后

js 复制代码
import util, { a, b, c as myC } from './util.js'

导入模块的全部内容

使用 * as 可以把模块默认值和散装值,全部放在后面的形参中,其中 util.default 放置的是默认输出值。

如果输出的散装值中有 default 这个字段,编译会报错

js 复制代码
// 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放在一个文件夹中,但其他很多文件想使用这些工具时,想只引入一个文件,则可以使用一个文件把工具都引入,然后直接导出,充当一个中介

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}