行业知识
前端模块化(commonJs、ES Module)
模块的概念及使⽤原因
模块化开发是我们开发当中用于组织和管理代码的方法,它的目的是将复杂的应用程序去拆分为更小和更好管理的模块单元,从而提高代码的复用性和可维护性。
在早期的前端开发中,javascript代码通常以脚本的形式直接嵌在html文件中,或者通过多个脚本文件呢去进行一个处置,那这种方式存在一些问题,比如全局命名冲突,代码复用困难,依赖关系难以管理等等,特别是在多人协作的场景下,这种情况尤为常见,直到模块化的出现才从根本上去解决这些问题,这也正是js为什么需要模块的原因。
那js的模块化是什么?怎么用?先说概念,js模块化允许我们将功能相同的代码封装在独立的模块中,并且通过导入和导出的机制来管理模块之间的依赖关系,模块可以是独立的文件,也可以是一个包含多个相关功能的文件集合。
模块化规范
模块化的规范它具体有哪些分类呢?
第一个就是 ES Module,它是 ES6 引入的官方模块化规范,它使用 import 和 export 关键字来导入和导出模块。它也支持静态分析,这使得浏览器或者nodejs环境能够对它进行优化,从而实现更高效的模块加载。
第二个就是commonJS,最初它是用于服务端的js模块化规范,当下也被广泛应用于前端开发当中,它使用require和module.exports来导入和导出模块,nodejs就是commonJS模块化规范的一种实现。
这两种方法规范在现阶段的前端开发中使用最为广泛,除了这些还有像amd、cmd、umd等等,这些我们也基本不会在项目中进行使用,就不多说了。
(一)commonJS
在Nodejs中每个.js文件就是一个模块,每个模块中的代码都是通过类似如下代码去进行包裹的。好比说它已经把这个模块的架子搭好了,我们直接填充代码进去就可以了。通常我们的代码是写在这个函数里面的,函数提供的这些参数我么可以直接在模块中去访问。接下来看看这些参数是什么。
1. module
新建一个index.js文件,先打印一下module看看是什么,是一个对象,展示当前模块对应的一些信息.
// index.jsconsole.log(module)// 执行结果{ id: '.', path: '/Users/jiawei/Projects/WebstormProjects/demo/learning', exports: {}, filename: '/Users/jiawei/Projects/WebstormProjects/demo/learning/index.js', loaded: false, children: [], paths: [ '/Users/jiawei/Projects/WebstormProjects/demo/learning/node_modules', '/Users/jiawei/Projects/WebstormProjects/demo/node_modules', '/Users/jiawei/Projects/WebstormProjects/node_modules', '/Users/jiawei/Projects/node_modules', '/Users/jiawei/node_modules', '/Users/node_modules', '/node_modules' ]} (1)id:当前模块唯一标识,本模块中使用。如果被导出则其值与module.filename相等。
(2)path:当前模块文件夹绝对路径。
(3)exports:默认空,Nodejs中,module.exports里面的内容作为我当前模块默认导出的内容,如果要导出当前模块里面的一些方法或者变量,一般就是通过module.exports等于一个对象,并且在这个对象里面写key和value,然后导出去,如果没有给它内容,那默认是一个空对象,那在其他文件中或者其他模块中去导入这个模块的时候,导入进去的它就是一个空对象,他默认导出的就是model exports它里面的内容。
(4)filename:我们当前模块所在的一个绝对路径。
(5)loaded:它是表示我们当前模块是否被引用加载过。
(6)children:它是一个数组,里面存放了我们当前模块引用的一个又一个的模块。比如说新建一个 child.js,不写内容,然后去index.js里导入,再打印 module。可以看到children放入了一个对象,id为对应文件的绝对路径。
// index.jsconst child = require('./child')console.log(module)// 打印结果{ id: '.', path: '/Users/jiawei/Projects/WebstormProjects/demo/learning', exports: {}, filename: '/Users/jiawei/Projects/WebstormProjects/demo/learning/index.js', loaded: false, children: [ { id: '/Users/jiawei/Projects/WebstormProjects/demo/learning/child.js', path: '/Users/jiawei/Projects/WebstormProjects/demo/learning', exports: {}, filename: '/Users/jiawei/Projects/WebstormProjects/demo/learning/child.js', loaded: true, children: [], paths: [Array] } ], paths: [ '/Users/jiawei/Projects/WebstormProjects/demo/learning/node_modules', '/Users/jiawei/Projects/WebstormProjects/demo/node_modules', '/Users/jiawei/Projects/WebstormProjects/node_modules', '/Users/jiawei/Projects/node_modules', '/Users/jiawei/node_modules', '/Users/node_modules', '/node_modules' ]} (7)paths,在当前模块中去引用其他模块的时候,去查找这个模块的路径列表时,首先会在当前这个文件夹下的到node_modules去找,如果没有,再去当前文件夹的父级的node_modules里找,依次往父级的父级去找,一直找到根路径的node_modules,如果还没有就报错。
(8)parent: 它返回一个对象,表示调用该模块的模块。但是在最新的Nodejs 14.6版本中module.parent被弃用了,官方推荐使用 require.main 或者 module.children代替。我当前使用的node版本是 18.20.3,所以不显示这个属性。
2. exports
打印 exports 是一个空对象,module也有一个exports,exports就是module.exports的一个引用,就是往module.exports添加key时,exports也会有,往exports添加key时,module.exports同样会有。如果说直接给exports赋值一个值或对象,那么它就指向新的对象了。
// child.jsconsole.log('exports:', exports)console.log('module.exports:', module.exports)console.log('1.exports == module.exports:', exports == module.exports)exports.username = 'jiawei'module.exports.age = 18exports = {}console.log('2.exports == module.exports:', exports == module.exports)// index.jsconst child = require('./child')console.log('child', child)// 打印结果:exports: {}module.exports: {}1.exports == module.exports: true2.exports == module.exports: falsechild { username: 'jiawei', age: 18 } 3. require
它是一个函数,也是对象,用于加载函数或者JSON,通常加载模块的path或者id标识,最终接收到的是module.exports导出的数据。
// index.jsconst child = require('./child')console.log(require)// 执行结果:[Function: require] { resolve: [Function: resolve] { paths: [Function: paths] }, main: { id: '.', path: '/Users/jiawei/Projects/WebstormProjects/demo/learning', exports: {}, filename: '/Users/jiawei/Projects/WebstormProjects/demo/learning/index.js', loaded: false, children: [ [Object] ], paths: [ '/Users/jiawei/Projects/WebstormProjects/demo/learning/node_modules', '/Users/jiawei/Projects/WebstormProjects/demo/node_modules', '/Users/jiawei/Projects/WebstormProjects/node_modules', '/Users/jiawei/Projects/node_modules', '/Users/jiawei/node_modules', '/Users/node_modules', '/node_modules' ] }, extensions: [Object: null prototype] { '.js': [Function (anonymous)], '.json': [Function (anonymous)], '.node': [Function (anonymous)] }, cache: [Object: null prototype] { '/Users/jiawei/Projects/WebstormProjects/demo/learning/index.js': { id: '.', path: '/Users/jiawei/Projects/WebstormProjects/demo/learning', exports: {}, filename: '/Users/jiawei/Projects/WebstormProjects/demo/learning/index.js', loaded: false, children: [Array], paths: [Array] }, '/Users/jiawei/Projects/WebstormProjects/demo/learning/child.js': { id: '/Users/jiawei/Projects/WebstormProjects/demo/learning/child.js', path: '/Users/jiawei/Projects/WebstormProjects/demo/learning', exports: {}, filename: '/Users/jiawei/Projects/WebstormProjects/demo/learning/child.js', loaded: true, children: [], paths: [Array] } }} (1)resolve:传进去一个模块的路径,它能解析出这个模块的绝对路径。
// index.jsconsole.log(require.resolve('./child')) // 执行结果/Users/jiawei/Projects/WebstormProjects/demo/learning/child.js (2)main: 也是一个module对象,表示nodejs进程启动时加载的入口脚本。
(3)extensions:已经废弃了。
(4)cache:缓存已加载模块的对象,key是路径,value是module对象,因为导入了child,所以它也会被缓存,这个有什么用呢?在我们重复加载某个模块的时候,他就可以从缓存里读取,加快读取速度。
require的导入:
// child.jsconst username = 'jiawei'const age = 18module.exports = { username, age}// index.js // 方式一输出:const child = require('./child')console.log(child)// 方式一输出结果:{ username: 'jiawei', age: 18 }// 方式二输出(解构):const { username, age } = require('./child')console.log(username, age)// 方式二输出结果:jiawei 18 4. __filename
当前模块文件绝对路径的文件名
console.log('__filename', __filename)console.log('module.filename === __filename:', module.filename === __filename)// 执行结果:__filename /Users/jiawei/Projects/WebstormProjects/demo/learning/index.jsmodule.filename === __filename: true 5. __dirname
当前文件所在目录的完整目录名(dir 即 directory)
console.log('__dirname', __dirname)console.log('module.path === __dirname', module.path === __dirname)// 执行结果:__dirname /Users/jiawei/Projects/WebstormProjects/demo/learningmodule.path === __dirname true (二)ES Module
使用import导入模块,使用export导出模块。
// child.jsexport const username = 'jiawei'export const age = 18export const sayName = function () { console.log('sayName', username)}// index.jsimport { username, age, sayName } from './child.mjs'console.log('username, age', username, age)sayName() 执行结果如下:
结果有个警告,如果要导入ES Module,在package.json设置 "type":"module",或者使用.mjs扩展名。
方法一:先使用.mjs扩展名。将child.js和index.js改为child.mjs和index.js,导入路径里.js也改为.mjs,再执行。
// child.mjsexport const username = 'jiawei'export const age = 18export const sayName = function () { console.log('sayName', username)}// index.mjsimport { username, age, sayName } from './child.mjs'console.log('username, age', username, age)sayName()// 执行结果:username, age jiawei 18sayName jiawei 但是我们基本不会去这么做。故将child.mjs和index.js改回child.js和index.js,导入路径里的.mjs也改回.js。
方法二:通过 命令行执行 npm init -y ,生成package.json,然后添加属性 "type": "module"。
{ "name": "learning", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", // 添加 "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC"} 再执行,结果如下:
// child.jsexport const username = 'jiawei'export const age = 18export const sayName = function () { console.log('sayName', username)}// index.jsimport {username, age, sayName } from './child.js'console.log('username, age', username, age)sayName()// 执行结果:username, age jiawei 18sayName jiawei 导入内容也可以起个别名,打印出来是个对象,对象里有我们声明的变量或方法,然后可以通过 . 的形式去使用。如下所示。
// index.jsimport * as child from './child.js'console.log(child)console.log(child.username)console.log(child.age)child.sayName()// 执行结果:[Module: null prototype] { age: 18, sayName: [Function: sayName], username: 'jiawei'}jiawei18sayName jiawei 我们也可以使用 export {} 做一个统一的导出。
// child.jsconst username = 'jiawei'const age = 18const sayName = function () { console.log('sayName', username)}export { username, age, sayName }// 第一种 index.jsimport { username, age, sayName } from './child.js'console.log('username, age', username, age)sayName()// 第一种 执行结果:username, age jiawei 18sayName jiawei// 第二种 index.jsimport * as child from './child.js'console.log(child.username)console.log(child.age)child.sayName()// 第二种 执行结果:jiawei18sayName jiawei 有时候我们会看到,模块里有 export default,作为当前模块默认导出的内容,可以给它任意值。整个模块里export default只能存在一个,但是 export 可以存在多个。
// child.jsconst username = 'jiawei'const age = 18const sayName = function () { console.log('sayName', username)}export { username, age }export {sayName}export default function () { console.log('export default age:', age)}// index.jsimport {username, age, sayName} from './child.js'import sayAge from './child.js'console.log('username, age', username, age)sayName()sayAge()// 执行结果:username, age jiawei 18sayName jiaweiexport default age: 18 两个 import 导入同一个文件,我们可以写一个如下的结合形式,结果相同。
import sayAge, {username, age, sayName} from './child.js'// 或者import { default as sayAge, username, age, sayName} from './child.js' 如果模块多了,也需要写很多个。我们也可以以更好的形式去处理模块间的依赖关系,我们可以把import和export结合起来使用,建一个模块,将它作为所有模块统一导出入口。
// child.jsconst username = 'jiawei'const age = 18const sayName = function () { console.log('sayName', username)}export { username, age }export {sayName}export default function () { console.log('export default age:', age)}// kid.jsconst kidName = 'jiawei'const kidAge = 10const sayKidName = function () { console.log('sayKidName', kidName)}export { kidName, kidAge, sayKidName }// main.jsexport { default as sayAge, username, age, sayName} from './child.js'export { kidName, kidAge, sayKidName } from './kid.js'// index.jsimport { sayAge, username, age, sayName, kidName, kidAge, sayKidName } from './main.js'console.log('username, age', username, age)sayName()sayAge()console.log('kidName, kidAge', kidName, kidAge)sayKidName()// 执行结果:username, age jiawei 18sayName jiaweiexport default age: 18kidName, kidAge jiawei 10sayKidName jiawei