Javascript最开始是怎样实现模块化呢?
我们知道javascript最开始是面向过程的思维编程,随着代码越来越庞大、复杂,在这种实际遇到的问题中,大佬们逐渐把面向对象、模块化的思想用在javascript当中。
一开始,我们是把不同功能写在不同函数当中
// 比如getCssAttr函数来获取Css属性,当我们需要获取Css属性的时候可以直接调用该方法
function getCssAttr(obj, attr) {
if (obj.currentStyle) {
return obj.currentStyle[attr];
} else {
return window.getComputedStyle(obj, null)[attr];
}
}
// 比如toJSON函数能够把url的query转为JSON对象
function toJSON(str) {
var obj = {}, allArr = [], splitArr = [];
str = str.indexOf('?') >= 0 ? str.substr(1) : str;
allArr = str.split('&');
for (var i = 0; i < allArr.length; i++) {
splitArr = allArr[i].split('=');
obj[splitArr[0]] = splitArr[1];
}
return obj;
}
这样getCssAttr函数和toJSON组成了模块,当需要使用的时候,直接调用即可,但是随着项目代码量越来越庞大和复杂,而且这种方式会对全局变量造成了污染。
为了解决上面的问题,会想到把这些方法、变量放到对象中
let utils = new Object({
getCssAttr:function(){...},
toJSON:function(){...}
})
当需要调用相应函数时,我们通过对象调用即可,utils.getCssAttr()
、utils.toJSON()
,但是这样会存在一个问题,就是可以直接通过外部修改内部方法属性。
utils.getCssAttr = null
那么我们有办法让内部方法属性不被修改吗?
答案是可以的,我们可以通过闭包的方式,使私有成员不暴露在外部。
let utils = (function(){
let getCssAttr = function(){...}
let toJSON = function(){...}
return {
getCssAttr,
toJSON
}
})()
这样的话,外部就无法改变内部的私有成员了。
CMD和AMD规范
试想一下,如果一个项目,所有轮子都自己造,在现在追求敏捷开发的环境下,我们有必要所有轮子都自己造吗?一些常用通用的功能,是否可以提取出来,供大家使用,提高开发效率?
正所谓,无规矩不成方圆,每个程序猿的代码风格肯定是有差异的,你写你的,我写我的,这样就很难流通了,但是如果大家都遵循一个规范编写代码,形成一个个模块,就显得非常重要了。
在这样的背景下,形成了两种规范,一种是以sea.js为代表的CMD规范,另外一种是以require.js为代表的AMD规范。
- CMD规范(Common Module Definition 通用模块定义)
- AMD规范(Asynchronous Module Definition 异步模块定义)
在node.js中是遵循commonJS规范的,在对模块的导入是同步的,为什么这样说?因为在服务器中,模块都是存在本地的,即使要导入模块,也只是耗费了从硬盘读取模块的时间,而且可控。
但是在浏览器中,模块是需要通过网络请求获取的,如果是同步获取的话,那么网络请求的时间没办法保证,会造成浏览器假死的,但是异步的话,是不会阻塞主线程,所以不管是CMD还是AMD,都是属于异步的,CMD和AMD都是属于异步加载模块,当所需要依赖的模块加载完毕后,才通过一个回调函数,写我们所需要的业务逻辑。
CMD和AMD的异同
- CMD是
延迟执行,依赖就近
,而AMD是提前执行,依赖前置
(require2.0开始可以改成延迟执行),怎么理解呢?看看下面代码1
2
3
4
5
6
7
8
9
10
11
12
13
14// CMD
define(function(require,exports,module){
var a = require('./a')
a.run()
var b = require('./b')
b.eat()
})
// AMD
define(['./a','./b'],function(a,b){
a.run()
b.eat()
})
CMD,AMD执行对比
— | CMD | AMD |
---|---|---|
执行回调函数的时机 | 快 | 慢 |
执行回调函数内的业务 | 慢 | 快 |
node.js遵循的commonJs规范
首先,我们来剖析一下commonJs的源码
我们分别创建两个文件useModule.js
、module.js
,并且打上断点。1
2
3
4
5
6
7
8
9
10
11
12
13// useModule.js
let utils = require('./module')
utils = require('./module')
utils.sayhello()
// module.js
let utils = {
sayhello:function(){
console.log('hello swr')
}
}
module.exports = utils
然后开始执行,我们首先会进入commonJs的源码了
在最上面可以看出是一个闭包的形式
1
2
3(function(exports,require,module,__filename,__dirname)){
//...
}
,这里可以看出__dirname
和__filename
并非是global
上的属性,而是每个模块对应的路径。
而且我们在模块当中this
并不是指向global
的,而是指向module.exports
,至于为什么会这样呢?下面会讲到。
在红框中,我们可以看到require
函数,exports.requireDepth
可以暂时不用管,是一个引用深度的变量,接下来我们往下看,return mod.require(path)
,这里的mod
就是每一个文件、模块,而里面都有一个require
方法,接下来我们看看mod.require
函数内部是怎么写的。
进来后,我们会看到2个assert
断言,用来判断path
参数是否传递了,path
是否字符串类型等等。
return Module._load(path,this,false)
,path
为我们传入的模块路径,this则是这个模块,false则不是主要模块,主要模块的意思是,如果a.js加载了b.js,那么a.js是主要模块,而b.js则是非主要模块。
接下来我们看看Module._load
这个静态方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
var filename = Module._resolveFilename(request, parent, isMain)
,这里的目的是解析出一个绝对路径,我们可以进去看看Module._resolveFilename
函数是怎么写的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21Module._resolveFilename = function(request, parent) {
// 第一步:如果是内置模块,不含路径返回
if (NativeModule.exists(request)) {
return request;
}
// 第二步:确定所有可能的路径
var resolvedModule = Module._resolveLookupPaths(request, parent);
var id = resolvedModule[0];
var paths = resolvedModule[1];
// 第三步:确定哪一个路径为真
var filename = Module._findPath(request, paths);
if (!filename) {
var err = new Error("Cannot find module '" + request + "'");
err.code = 'MODULE_NOT_FOUND';
throw err;
}
return filename;
};
上面代码中,在 Module.resolveFilrename 方法内部,又调用了两个方法 Module.reqolveLookPaths()和 Module._findPath(),前者用来列出可能的路径,后者用来确认哪一个路径为真。
有了可能的路径以后,下面就是 Module._findPath()的源码,用来确定到底哪一个是正确路径。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54Module._findPath = function(request, paths) {
// 列出所有可能的后缀名:.js,.json, .node
var exts = Object.keys(Module._extensions);
// 如果是绝对路径,就不再搜索
if (request.charAt(0) === '/') {
paths = [''];
}
// 是否有后缀的目录斜杠
var trailingSlash = (request.slice(-1) === '/');
// 第一步:如果当前路径已在缓存中,就直接返回缓存
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
// 第二步:依次遍历所有路径
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename;
if (!trailingSlash) {
// 第三步:是否存在该模块文件
filename = tryFile(basePath);
if (!filename && !trailingSlash) {
// 第四步:该模块文件加上后缀名,是否存在
filename = tryExtensions(basePath, exts);
}
}
// 第五步:目录中是否存在 package.json
if (!filename) {
filename = tryPackage(basePath, exts);
}
if (!filename) {
// 第六步:是否存在目录名 + index + 后缀名
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
// 第七步:将找到的文件路径存入返回缓存,然后返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 第八步:没有找到文件,返回false
return false;
};
有时在项目代码中,需要调用模块的绝对路径,那么除了 module.filename ,Node 还提供一个 require.resolve 方法,供外部调用,用于从模块名取到绝对路径。1
2
3
4
5
6
7require.resolve = function(request) {
return Module._resolveFilename(request, self);
};
// 用法
require.resolve('a.js')
// 返回 /Users/danlan/workspace/node-stu/ree/a.js
Module._resolveFilename
函数也没什么好说的,就是判断各种情况,然后解析出一个绝对路径出来,我们跳出这个函数,回到Module._load
中.
然后我们看到var cachedModule = Module._cache[filename]
,这是我们加载模块的缓存机制,就是说我们加载过一次模块后,会缓存到Module._cache这个对象中,并且是以filename
作为键名,因为路径是唯一的,所以以路径作为唯一标识,如果已经缓存过,则会直接返回这个缓存过的模块。
NativeModule.nonInternalExists(filename)
判断是否原生模块,是的话则直接返回模块。
经过上面两个判断,基本可以判定这个模块没被加载过,那么接下来看到var module = new Module(filename, parent)
,创建了一个模块,我们看看Module
这个构造函数有什么内容1
2
3
4
5
6module.id 模块的识别符,通常是带有绝对路径的模块文件名。
module.filename 模块的文件名,带有绝对路径。
module.loaded 返回一个布尔值,表示模块是否已经完成加载。
module.parent 返回一个对象,表示调用该模块的模块。
module.children 返回一个数组,表示该模块要用到的其他模块。
module.exports 表示模块对外输出的值。
下面是一个示例文件,最后一行输出module变量。
1 | // example.js |
执行这个文件,命令行会输出如下信息。
1 | { id: '.', |
这里的id
,实际上就是filename
唯一路径,另外一个很重要的是this.exports
,也就是将来用于暴露模块的。
我们接着往下看,在创建一个实例后,接下来把这个实例存在缓存当中,Module._cache[filename] = module
然后执行tryModuleLoad(module, filename)
,这个函数非常重要,是用来加载模块的,我们看看是怎么写的
这里有个module.load
,我们再往里面看看是怎么写的1
2
3
4
5
6Module.prototype.load = function (filename) {
var extension = path.extname(filename) || 'js'
if(!Module._extensions[extensions]) extension = '.js'
Module._extensions[extension](this, filename)
this.loaded = true
}
兜兜转转,终于来到最核心的地方了this.paths = Module._nodeModulePaths(path.dirname(filename))
,我们知道,我们安装npm包时,node会由里到外一层层找node_modules
文件夹,而这一步,则是路径一层层丢进数组里,我们可以看看this.paths
的数组
CommonJS 模块查找规范
新建一个b.js
1 | var a = require('./a.js') |
运行一下:
1 | module.id: /Users/danlan/workspace/node-stu/ree/a.js |
举例来说,脚本/home/user/projects/foo.js执行了require(‘bar.js’)命令,Node会依次搜索以下文件。
1 | /usr/local/lib/node/bar.js |
这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
- 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require(‘example-module/path/to/file’),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
- 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
- 如果想得到require命令加载的确切文件名,使用require.resolve()方法。
目录的加载规则
在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。
1 | // package.json |
继续往下看,var extension = path.extname(filename) || '.js'
是获取后缀名,如果没有后缀名的话,暂时默认添加一个.js
后缀名。
继续往下看,if (!Module._extensions[extension]) extension = '.js'
是判断Module._extensions
这个对象,是否有这个属性,如果没有的话,则让这个后缀名为.js
继续往下看,Module._extensions[extension](this, filename)
,根据后缀名,执行对应的函数,那么我们看一下Module._extensions
对象有哪几个函数
从这里我们可以看到,Module._extensions
中有3个函数,分别是.js
、.json
、.node
函数,意思是根据不同的后缀名,执行不同的函数,来解析不同的内容,我们可以留意到读取文件都是用fs.readFileSync
同步读取,因为这些文件都是保存在服务器硬盘中,读取这些文件耗费时间非常短,所以采用了同步而不是异步
其中.json
最为简单,读取出文件后,再通过JSON.parse
把字符串转化为JSON
对象,然后把结果赋值给module.exports
接下来看看.js
,也是一样先读取出文件内容,然后通过module._compile
这个函数来解析.js
的内容,我们看一下module._compile
函数怎么写的
var wrapper = Module.wrap(content)
这里对.js
文件的内容进行了一层处理,我们可以看看Module.wrap
怎么写的
在这里可以看出,NativeModule.wrapper
数组中有两个数组成员,是不是看起来似曾相识?没错,这就是闭包的形式,而Module.wrap
中,是直接把js文件的内容,和这个闭包拼接成一段字符串,对,就是在这里,把一个个模块,套一层闭包!实际上拼接出来的是
// 字符串
"(function(exports,require,module,__filename,__dirname){
let utils = {
sayhello:function(){
console.log('hello swr')
}
}
})"
我们跳出来,回到Module.prototype._compile
看看,接下来看到var compiledWrapper = vm.runInThisContext(wrapper,{...})
,1
2//虚拟机,帮我们创建一个黑箱执行代码,防止变量污染
const vm = require("vm");
在nodejs中是通过vm这个虚拟机,执行字符串,而且这样的好处是使内部完全是封闭的,不会被外在变量污染,而在前端的字符串模板则是通过new Function()
来执行字符串,达到不被外在变量污染
继续往下看,result = compiledWrapper.call(this.exports, this.exports, require, this,filename, dirname)
,其中compiledWrapper
就是我们通过vm虚拟机执行的字符串后返回的闭包,而且通过call
来把这个模块中的this
指向更改为当前模块,而不是全局的global
,这里就是为什么我们在模块当中打印this
时,指向的是当前的module.exports
而不是global
,然后后面依次把相应的参数传递过去
最终一层层跳出后Module._load
中,最后是return module.exports
,也就是说我们通过require
导入的模块,取的是module.exports
通过剖析commonJs源码,我们收获了什么?
懂得了模块加载的整个流程
- 第一步:解析出一个绝对路径,如果是核心模块,比如fs,就直接返回模块
- 第二步:如文件没添加后缀,则添加
.js
、.json
、.node
作为后缀,然后通过fs.existsSync
来判断文件是否存在- .js 解析为JavaScript 文本文件
- .json解析JSON对象
- .node解析为二进制插件模块
- 第三步:到缓存中找该模块是否被加载过(如果是带有路径的如/,./等等,则拼接出一个绝对路径,然后先读取缓存require.cache再读取文件。如果没有加后缀,则自动加后缀然后一一识别。)
- 第四步:new一个模块实例
- 第五步:把模块存到缓存当中(首次加载后的模块会缓存在require.cache之中,所以多次加载require,得到的对象是同一个。)
- 第六步:根据后缀名,加载这个模块
- 在执行模块代码的时候,会将模块包装成如下模式,以便于作用域在模块范围之内。
1
2
3(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});
- 在执行模块代码的时候,会将模块包装成如下模式,以便于作用域在模块范围之内。
知道如何实现由里到外一层层查找
node_modules
- 知道针对
.js
和.json
是怎么解析的.js
是通过拼接字符串,形成一个闭包形式的字符串.json
则是通过JSON.parse
转为JSON
对象
知道如何执行字符串,并且不受外部变量污染
nodejs中通过vm虚拟机来执行字符串
1
2
3let vm=require("vm")
let a='console.log("a")'
vm.runInThisContext(a)前端则是通过
new Function()
来执行字符串1
2var f = new Function('x', 'y', 'return x+y');
f( 3, 4 )
知道为什么模块中的
this
指向的是this.exports
而不是global
- 通过
call
把指针指向了this.exports
- 通过
接下来,我们手写一个简陋版的commonJs源码
commonJs其实在加载模块的时候,做了以下几个步骤
- 第一步:解析出一个绝对路径
- 第二步:如文件没添加后缀,则添加
.js
、.json
、.node
作为后缀,然后通过fs.existsSync
来判断文件是否存在 - 第三步:到缓存中找该模块是否被加载过
- 第四步:new一个模块实例
- 第五步:把模块存到缓存当中
- 第六步:根据后缀名,加载这个模块
那么我们根据这几个步骤,来手写一下源码~
// module.js
let utils = {
sayhello: function () {
console.log('hello swr')
}
}
console.log('执行了')
module.exports = utils
首先写出解析一个绝对路径以及如文件没添加后缀,则添加.js
、.json
作为后缀,然后通过fs.existsSync
来判断文件是否存在( .. 每个步骤我都会标识1、2、3…
// useModule.js
// 1.引入核心模块
let fs = require('fs')
let path = require('path')
// 3.声明一个Module构造函数
function Module(id) {
this.id = id
this.exports = {} // 将来暴露模块的内容
}
// 8.支持的后缀名类型
Module._extensions = {
".js":function(){},
".json":function(){}
}
// 5.解析出绝对路径,_resolveFilename是Module的静态方法
Module._resolveFilename = function (relativePath) {
// 6.返回一个路径
let p = path.resolve(__dirname,relativePath)
// 7.该路径是否存在文件,如果存在则直接返回
// 这种情况主要考虑用户自行添加了后缀名
// 如'./module.js'let exists = fs.existsSync(p)
if(exists) return p
// 9.如果relativePath传入的如'./module',没有添加后缀
// 那么我们给它添加后缀,并且判断添加后缀后是否存在该文件
let keys = Object.keys(Module._extensions)
let r = falsefor(let val of keys){ // 这里用for循环,是当找到文件后可以直接break跳出循环
let realPath = p + val // 拼接后缀
let exists = fs.existsSync(realPath)
if(exists){
r = realPath
break
}
}
if(!r){ // 如果找不到文件,则抛出错误
throw new Error('file not exists')
}
return r
}
// 2.为了不与require冲突,这个函数命名为req
// 传入一个参数p 路径
function req(p) {
// 10.因为Module._resolveFilename存在找不到文件
// 找不到文件时会抛出错误,所以我们这里捕获错误
try {
// 4.通过Module._resolveFilename解析出一个绝对路径
let filename = Module._resolveFilename(p)
} catch (e) {
console.log(e)
}
}
// 导入模块,并且导入两次,主要是校验是否加载过一次后
// 在有缓存的情况下,会不会直接返回缓存的模块
// 为此特意在module.js中添加了console.log("执行了")
// 来看打印了几次
let utils = req('./module')
utils = req('./module')
utils.sayhello()
然后到缓存中找该模块是否被加载过,如果没有加载过则new一个模块实例,把模块存到缓存当中,最后根据后缀名,加载这个模块( .. 每个步骤我都会标识1、2、3…
// useModule.js
// 1.引入核心模块
let fs = require('fs')
let path = require('path')
// 3.声明一个Module构造函数
function Module(id) {
this.id = id
this.exports = {} // 将来暴露模块的内容
}
// * 21.因为处理js文件时,需要包裹一个闭包,我们写一个数组
Module.wrapper = [
"(function(exports,require,module){",
"\n})"
]
// * 22.通过Module.wrap包裹成闭包的字符串形式
Module.wrap = function(script){
return Module.wrapper[0] + script + Module.wrapper[1]
}
// 8.支持的后缀名类型
Module._extensions = {
".js":function(module){ // * 20.其次看看js是如何处理的
let str = fs.readFileSync(module.id,'utf8')
// * 23.通过Module.wrap函数把内容包裹成闭包
let fnStr = Module.wrap(str)
// * 24.引入vm虚拟机来执行字符串
let vm = require('vm')
let fn = vm.runInThisContext(fnStr)
// 让产生的fn执行,并且把this指向更改为当前的module.exports
fn.call(this.exports,this.exports,req,module)
},
".json":function(module){ // * 18.首先看看json是如何处理的
let str = fs.readFileSync(module.id,'utf8')
// * 19.通过JSON.parse处理,并且赋值给module.exports
let json = JSON.parse(str)
module.exports = json
}
}
// * 15.加载
Module.prototype._load = function(filename){
// * 16.获取后缀名
let extension = path.extname(filename)
// * 17.根据不同后缀名 执行不同的方法
Module._extensions[extension](this)
}
// 5.解析出绝对路径,_resolveFilename是Module的静态方法
Module._resolveFilename = function (relativePath) {
// 6.返回一个路径
let p = path.resolve(__dirname,relativePath)
// 7.该路径是否存在文件,如果存在则直接返回
// 这种情况主要考虑用户自行添加了后缀名
// 如'./module.js'let exists = fs.existsSync(p)
if(exists) return p
// 9.如果relativePath传入的如'./module',没有添加后缀
// 那么我们给它添加后缀,并且判断添加后缀后是否存在该文件
let keys = Object.keys(Module._extensions)
let r = falsefor(let val of keys){ // 这里用for循环,是当找到文件后可以直接break跳出循环
let realPath = p + val // 拼接后缀
let exists = fs.existsSync(realPath)
if(exists){
r = realPath
break
}
}
if(!r){ // 如果找不到文件,则抛出错误
throw new Error('file not exists')
}
return r
}
// * 11.缓存对象
Module._cache = {}
// 2.为了不与require冲突,这个函数命名为req
// 传入一个参数p 路径
function req(p) {
// 10.因为Module._resolveFilename存在找不到文件
// 找不到文件时会抛出错误,所以我们这里捕获错误
try {
// 4.通过Module._resolveFilename解析出一个绝对路径
let filename = Module._resolveFilename(p)
// * 12.判断是否有缓存,如果有缓存的话,则直接返回缓存
if(Module._cache[filename]){
// * 因为实例的exports才是最终暴露出的内容
return Module._cache[filename].exports
}
// * 13.new一个Module实例
let module = new Module(filename)
// * 14.加载这个模块
module._load(filename)
// * 25.把module存到缓存
Module._cache[filename] = module
// * 26.返回module.exprots
return module.exports
} catch (e) {
console.log(e)
}
}
// 导入模块,并且导入两次,主要是校验是否加载过一次后
// 在有缓存的情况下,会不会直接返回缓存的模块
// 为此特意在module.js中添加了console.log("执行了")
// 来看打印了几次
let utils = req('./module')
utils = req('./module')
utils.sayhello()
这样我们就完成了一个简陋版的commonJs,而且我们多次导入这个模块,只会打印出一次执行了
,说明了只要缓存中有的,就直接返回,而不是重新加载这个模块
这里建议大家一个步骤一个步骤去理解,尝试敲一下代码,这样感悟会更加深
那么为什么exports = xxx 却失效了呢?
// 从上面源码我们可以看出,实际上
// exports = module.exports = {}
// 但是当我们exports = {name:"CommonJS"}时,
// require出来却获取不到这个对象,这是因为我们在上面源码中,
// req函数(即require)内部return出的是module.exports,而不是exports,
// 当我们exports = { name:"CommonJS" }时,实际上这个exports指向了一个新的对象,
// 而不是module.exports
// 那么我们的exports是不是多余的呢?肯定不是多余的,我们可以这样写
exports.name = "CommonJS"
// 这样写没有改变exports的指向,而是在exports指向的module.exports对象上新增了属性
// 那么什么时候用exports,什么时候用module.exports呢?
// 如果导出的东西是一个,那么可以用module.exports,如果导出多个属性可以用exports,
// 一般情况下是用module.exports
// 还有一种方式,就是把属性挂载到global上供全局访问,不过不推荐。
CommonJS模块总结
CommonJS模块只能运行再支持此规范的环境之中,nodejs是基于CommonJS规范开发的,因此可以很完美地运行CommonJS模块,然后nodejs不支持ES6的模块规范,所以nodejs的服务器开发大家一般使用CommonJS规范来写。
CommonJS模块导入用require,导出用module.exports。导出的对象需注意,如果是静态值,而且非常量,后期可能会有所改动的,请使用函数动态获取,否则无法获取修改值。导入的参数,是可以随意改动的,所以大家使用时要小心。