在 Node.js 模块系统中,每个文件都被视为独立的模块。
例如,假设一个名为 foo.js
的文件:
const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);
在第一行,foo.js
加载了与 foo.js
位于同一目录中的模块 circle.js
。
以下是 circle.js
的内容:
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
模块 circle.js
已导出函数 area()
和 circumference()
。
通过在特殊的 exports
对象上指定额外的属性,将函数和对象添加到模块的根部。
模块的本地变量将是私有的,因为模块被 Node.js 封装在函数中(参见模块封装器)。
在此示例中,变量 PI
是 circle.js
私有的。
可以为 module.exports
属性分配新的值(例如函数或对象)。
下面,bar.js
使用了导出 Square 类的 square
模块:
const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`The area of mySquare is ${mySquare.area()}`);
square
模块在 square.js
中定义:
// 赋值给 exports 不会修改模块,必须使用 module.exports
module.exports = class Square {
constructor(width) {
this.width = width;
}
area() {
return this.width ** 2;
}
};
CommonJS 模块系统在 module
核心模块中实现。
Node.js 有两个模块系统:CommonJS 模块和 ECMAScript 模块。
默认情况下,Node.js 会将以下内容视为 CommonJS 模块:
扩展名为 .cjs
的文件;
当最近的父 package.json
文件包含值为 "commonjs"
的顶层字段 "type"
时,则扩展名为 .js
的文件。
当最近的父 package.json
文件不包含顶层字段 "type"
时,则扩展名为 .js
的文件。
包作者应该包括 "type"
字段,即使在所有源都是 CommonJS 的包中也是如此。
明确包的 type
将使构建工具和加载器更容易确定包中的文件应该如何解释。
扩展名不是 .mjs
、.cjs
、.json
、.node
、或 .js
的文件(当最近的父 package.json
文件包含值为 "module"
的顶层字段 "type"
时,这些文件只有在它们是 require
的,而不是用作程序的命令行入口点)。
参阅确定模块系统了解更多详细信息。
调用 require()
始终使用 CommonJS 模块加载器。
调用 import()
始终使用 ECMAScript 模块加载器。
当文件直接从 Node.js 运行时,则 require.main
被设置为其 module
。
这意味着可以通过测试 require.main === module
来确定文件是否被直接运行。
对于文件 foo.js
,如果通过 node foo.js
运行,则为 true
,如果通过 require('./foo')
运行,则为 false
。
当入口点不是 CommonJS 模块时,则 require.main
为 undefined
,且主模块不可达。
Node.js require()
函数的语义被设计为足够通用以支持合理的目录结构。
诸如 dpkg
、rpm
和 npm
之类的包管理器程序有望发现无需修改即可从 Node.js 模块构建本机包。
下面给出了一个可行的建议目录结构:
假设想让位于 /usr/lib/node/<some-package>/<some-version>
的文件夹保存特定版本包的内容。
包可以相互依赖。
为了安装包 foo
,可能需要安装包 bar
的特定版本。
bar
包本身可能存在依赖关系,在某些情况下,这些甚至可能发生冲突或形成循环依赖关系。
因为 Node.js 会查找其加载的任何模块的 realpath
(即,其解析符号链接),然后在 node_modules
文件夹中查找其依赖项,这种情况可以使用以下架构解决:
/usr/lib/node/foo/1.2.3/
: foo
包的内容,版本 1.2.3。/usr/lib/node/bar/4.3.2/
: foo
依赖的 bar
包的内容。/usr/lib/node/foo/1.2.3/node_modules/bar
: /usr/lib/node/bar/4.3.2/
的符号链接。/usr/lib/node/bar/4.3.2/node_modules/*
: bar
依赖的包的符号链接。因此,即使遇到循环,或者如果存在依赖冲突,每个模块都将能够获得它可以使用的依赖版本。
当 foo
包中的代码执行 require('bar')
时,它将获得符号链接到 /usr/lib/node/foo/1.2.3/node_modules/bar
的版本。
然后,当 bar
包中的代码调用 require('quux')
时,它将获得符号链接到 /usr/lib/node/bar/4.3.2/node_modules/quux
的版本。
此外,为了使模块查找过程更加优化,与其将包直接放在 /usr/lib/node
中,还可以将它们放在 /usr/lib/node_modules/<name>/<version>
中。
这样 Node.js 就不会费心寻找 /usr/node_modules
或 /node_modules
中缺失的依赖项了。
为了使模块可用于 Node.js 交互式解释器,将 /usr/lib/node_modules
文件夹添加到 $NODE_PATH
环境变量可能会很有用。
由于使用 node_modules
文件夹的模块查找都是相对的,并且基于调用 require()
的文件的真实路径,因此包本身可以位于任何位置。
由于 require()
的同步特性,无法使用它来加载 ECMAScript 模块文件。
尝试这样做会抛出 ERR_REQUIRE_ESM
错误。
改用 import()
。
.mjs
扩展名是为无法通过 require()
加载的 ECMAScript 模块保留的。
有关哪些文件被解析为 ECMAScript 模块的更多信息,请参阅确定模块系统章节。
要获取调用 require()
时将加载的确切文件名,则使用 require.resolve()
函数。
综上所述,这里是 require()
的伪代码高级算法:
require(X) from module at path Y 1. If X is a core module, a. return the core module b. STOP 2. If X begins with '/' a. set Y to be the filesystem root 3. If X begins with './' or '/' or '../' a. LOAD_AS_FILE(Y + X) b. LOAD_AS_DIRECTORY(Y + X) c. THROW "not found" 4. If X begins with '#' a. LOAD_PACKAGE_IMPORTS(X, dirname(Y)) 5. LOAD_PACKAGE_SELF(X, dirname(Y)) 6. LOAD_NODE_MODULES(X, dirname(Y)) 7. THROW "not found" LOAD_AS_FILE(X) 1. If X is a file, load X as its file extension format. STOP 2. If X.js is a file, load X.js as JavaScript text. STOP 3. If X.json is a file, parse X.json to a JavaScript Object. STOP 4. If X.node is a file, load X.node as binary addon. STOP LOAD_INDEX(X) 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP 3. If X/index.node is a file, load X/index.node as binary addon. STOP LOAD_AS_DIRECTORY(X) 1. If X/package.json is a file, a. Parse X/package.json, and look for "main" field. b. If "main" is a falsy value, GOTO 2. c. let M = X + (json main field) d. LOAD_AS_FILE(M) e. LOAD_INDEX(M) f. LOAD_INDEX(X) DEPRECATED g. THROW "not found" 2. LOAD_INDEX(X) LOAD_NODE_MODULES(X, START) 1. let DIRS = NODE_MODULES_PATHS(START) 2. for each DIR in DIRS: a. LOAD_PACKAGE_EXPORTS(X, DIR) b. LOAD_AS_FILE(DIR/X) c. LOAD_AS_DIRECTORY(DIR/X) NODE_MODULES_PATHS(START) 1. let PARTS = path split(START) 2. let I = count of PARTS - 1 3. let DIRS = [] 4. while I >= 0, a. if PARTS[I] = "node_modules" CONTINUE b. DIR = path join(PARTS[0 .. I] + "node_modules") c. DIRS = DIR + DIRS d. let I = I - 1 5. return DIRS + GLOBAL_FOLDERS LOAD_PACKAGE_IMPORTS(X, DIR) 1. Find the closest package scope SCOPE to DIR. 2. If no scope was found, return. 3. If the SCOPE/package.json "imports" is null or undefined, return. 4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), ["node", "require"]) defined in the ESM resolver. 5. RESOLVE_ESM_MATCH(MATCH). LOAD_PACKAGE_EXPORTS(X, DIR) 1. Try to interpret X as a combination of NAME and SUBPATH where the name may have a @scope/ prefix and the subpath begins with a slash (`/`). 2. If X does not match this pattern or DIR/NAME/package.json is not a file, return. 3. Parse DIR/NAME/package.json, and look for "exports" field. 4. If "exports" is null or undefined, return. 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, `package.json` "exports", ["node", "require"]) defined in the ESM resolver. 6. RESOLVE_ESM_MATCH(MATCH) LOAD_PACKAGE_SELF(X, DIR) 1. Find the closest package scope SCOPE to DIR. 2. If no scope was found, return. 3. If the SCOPE/package.json "exports" is null or undefined, return. 4. If the SCOPE/package.json "name" is not the first segment of X, return. 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE), "." + X.slice("name".length), `package.json` "exports", ["node", "require"]) defined in the ESM resolver. 6. RESOLVE_ESM_MATCH(MATCH) RESOLVE_ESM_MATCH(MATCH) 1. let { RESOLVED, EXACT } = MATCH 2. let RESOLVED_PATH = fileURLToPath(RESOLVED) 3. If EXACT is true, a. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension format. STOP 4. Otherwise, if EXACT is false, a. LOAD_AS_FILE(RESOLVED_PATH) b. LOAD_AS_DIRECTORY(RESOLVED_PATH) 5. THROW "not found"
模块在第一次加载后被缓存。
这意味着(类似其他缓存)每次调用 require('foo')
都会返回完全相同的对象(如果解析为相同的文件)。
如果 require.cache
没有被修改,则多次调用 require('foo')
不会导致模块代码被多次执行。
这是重要的特征。
有了它,可以返回“部分完成”的对象,从而允许加载传递依赖项,即使它们会导致循环。
要让模块多次执行代码,则导出函数,然后调用该函数。
模块根据其解析的文件名进行缓存。
由于模块可能会根据调用模块的位置(从 node_modules
文件夹加载)解析为不同的文件名,因此如果 require('foo')
解析为不同的文件,则不能保证 require('foo')
将始终返回完全相同的对象。
此外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向同一个文件,但缓存仍会将它们视为不同的模块,并将多次重新加载文件。
例如,require('./foo')
和 require('./FOO')
返回两个不同的对象,而不管 ./foo
和 ./FOO
是否是同一个文件。
Node.js 有些模块编译成二进制文件。 这些模块在本文档的其他地方有更详细的描述。
核心模块在 Node.js 源代码中定义,位于 lib/
文件夹中。
如果将核心模块的标识符传给 require()
,则始终优先加载核心模块。
例如,require('http')
将始终返回内置的 HTTP 模块,即使存在该名称的文件。
核心模块也可以使用 node:
前缀来识别,在这种情况下,它会绕过 require
缓存。
例如,require('node:http')
将始终返回内置的 HTTP 模块,即使有该名称的 require.cache
条目。
当有循环 require()
调用时,模块在返回时可能尚未完成执行。
考虑这种情况:
a.js
:
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js
:
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
main.js
:
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
当 main.js
加载 a.js
时,a.js
依次加载 b.js
。
此时,b.js
尝试加载 a.js
。
为了防止无限循环,将 a.js
导出对象的未完成副本返回给 b.js
模块。
然后 b.js
完成加载,并将其 exports
对象提供给 a.js
模块。
到 main.js
加载这两个模块时,它们都已完成。
因此,该程序的输出将是:
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
需要仔细规划以允许循环模块依赖项在应用程序中正常工作。
如果找不到确切的文件名,Node.js 将尝试加载所需的文件名,并添加扩展名:.js
、.json
,最后是 .node
。
当加载具有不同扩展名的文件(例如 .cjs
)时,则必须将其全名传给 require()
,包括其文件扩展名(例如 require('./file.cjs')
)。
.json
文件被解析为 JSON 文本文件,.node
文件被解释为加载了 process.dlopen()
的已编译插件模块。
使用任何其他扩展名(或根本没有扩展名)的文件被解析为 JavaScript 文本文件。
请参阅确定模块系统章节以了解将使用什么解析目标。
以 '/'
为前缀的必需模块是文件的绝对路径。
例如,require('/home/marco/foo.js')
将在 /home/marco/foo.js
加载文件。
以 './'
为前缀的必需模块与调用 require()
的文件相关。
也就是说,circle.js
必须和 foo.js
在同一个目录下,require('./circle')
才能找到它。
如果没有前导 '/'
、'./'
或 '../'
来指示文件,则该模块必须是核心模块或从 node_modules
文件夹加载。
如果给定路径不存在,则 require()
将抛出 MODULE_NOT_FOUND
错误。
可以通过三种方式将文件夹作为参数传给 require()
。
首先是在文件夹的根目录创建 package.json
文件,指定 main
模块。
一个示例 package.json
文件可能如下所示:
{ "name" : "some-library",
"main" : "./lib/some-library.js" }
如果这是在 ./some-library
的文件夹中,则 require('./some-library')
将尝试加载 ./some-library/lib/some-library.js
。
如果目录中不存在 package.json
文件,或者 "main"
条目丢失或无法解析,则 Node.js 将尝试从该目录中加载 index.js
或 index.node
文件。
例如,如果前面的示例中没有 package.json
文件,则 require('./some-library')
将尝试加载:
./some-library/index.js
./some-library/index.node
如果这些尝试失败,Node.js 将报告整个模块丢失,并显示默认错误:
Error: Cannot find module 'some-library'
在上述所有三种情况下,import('./some-library')
调用都将导致 ERR_UNSUPPORTED_DIR_IMPORT
错误。
使用包子路径导出或子路径导入可以提供与文件夹作为模块相同的包含组织优势,并且适用于 require
和 import
。
如果传给 require()
的模块标识符不是核心模块,并且不以 '/'
、'../'
或 './'
开头,则 Node.js 从当前模块的目录开始,并添加 /node_modules
,并尝试从该位置加载模块。
Node.js 不会将 node_modules
附加到已经以 node_modules
结尾的路径。
如果在那里找不到它,则它移动到父目录,依此类推,直到到达文件系统的根目录。
例如,如果 '/home/ry/projects/foo.js'
处的文件调用 require('bar.js')
,则 Node.js 将按以下顺序查找以下位置:
/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
这允许程序本地化它们的依赖项,这样它们就不会发生冲突。
通过在模块名称后包含路径后缀,可以要求与模块一起分发的特定文件或子模块。
例如,require('example-module/path/to/file')
将相对于 example-module
所在的位置解析 path/to/file
。
后缀路径遵循相同的模块解析语义。
如果 NODE_PATH
环境变量设置为以冒号分隔的绝对路径列表,则 Node.js 将在这些路径中搜索模块(如果它们在其他地方找不到)。
在 Windows 上,NODE_PATH
由分号 (;
) 而不是冒号分隔。
在定义当前的模块解析算法之前,NODE_PATH
最初是为了支持从不同路径加载模块而创建的。
NODE_PATH
仍然受支持,但现在 Node.js 生态系统已经确定了用于定位依赖模块的约定,因此不太必要。
有时,当不知道必须设置 NODE_PATH
时,依赖 NODE_PATH
的部署会表现出意外的行为。
有时,模块的依赖项会发生变化,导致在搜索 NODE_PATH
时加载不同的版本(甚至不同的模块)。
此外,Node.js 将在以下 GLOBAL_FOLDERS 列表中搜索:
$HOME/.node_modules
$HOME/.node_libraries
$PREFIX/lib/node
其中 $HOME
是用户的主目录,$PREFIX
是 Node.js 配置的 node_prefix
。
这些主要是出于历史原因。
强烈建议将依赖项放在本地 node_modules
文件夹中。
这些将加载得更快,更可靠。
在执行模块代码之前,Node.js 将使用如下所示的函数封装器对其进行封装:
(function(exports, require, module, __filename, __dirname) {
// 模块代码实际存在于此处
});
通过这样做,Node.js 实现了以下几点:
var
、const
或 let
定义)保持在模块而不是全局对象的范围内。module
和 exports
对象,实现者可以用来从模块中导出值。__filename
和 __dirname
,包含模块的绝对文件名和目录路径。__dirname
#当前模块的目录名。
这与 __filename
的 path.dirname()
相同。
示例:从 /Users/mjr
运行 node example.js
console.log(__dirname);
// 打印: /Users/mjr
console.log(path.dirname(__filename));
// 打印: /Users/mjr
__filename
#当前模块的文件名。 这是当前模块文件的已解析符号链接的绝对路径。
对于主程序,这不一定与命令行中使用的文件名相同。
当前模块的目录名见 __dirname
。
示例:
从 /Users/mjr
运行 node example.js
console.log(__filename);
// 打印: /Users/mjr/example.js
console.log(__dirname);
// 打印: /Users/mjr
给定两个模块:a
和 b
,其中 b
是 a
的依赖项,且目录结构为:
/Users/mjr/app/a.js
/Users/mjr/app/node_modules/b/b.js
在 b.js
中对 __filename
的引用将返回 /Users/mjr/app/node_modules/b/b.js
,而在 a.js
中对 __filename
的引用将返回 /Users/mjr/app/a.js
。
exports
#对 module.exports
的引用,其输入更短。
有关何时使用 exports
和何时使用 module.exports
的详细信息,请参阅有关导出的快捷方式的章节。
module
#对当前模块的引用,请参阅有关 module
对象的部分。
特别是,module.exports
用于定义模块通过 require()
导出和提供的内容。
require(id)
#用于导入模块、JSON
和本地文件。
模块可以从 node_modules
导入。
可以使用相对路径(例如 ./
、./foo
、./bar/baz
、../foo
)导入本地模块和 JSON 文件,该路径将根据 __dirname
(如果有定义)命名的目录或当前工作目录进行解析。
POSIX 风格的相对路径以独立于操作系统的方式解析,这意味着上面的示例将在 Windows 上以与在 Unix 系统上相同的方式工作。
// 使用相对于 `__dirname` 或当前工作目录的路径导入本地模块。
//(在 Windows 上,这将解析为 .\path\myLocalModule。)
const myLocalModule = require('./path/myLocalModule');
// 导入 JSON 文件:
const jsonData = require('./path/filename.json');
// 从 node_modules 或 Node.js 内置模块导入模块:
const crypto = require('crypto');
require.cache
#模块在需要时缓存在此对象中。
通过从此对象中删除键值,下一次 require
将重新加载模块。
这不适用于原生插件,因为重新加载会导致错误。
添加或替换条目也是可能的。
在本地模块之前检查此缓存,如果将匹配本地模块的名称添加到缓存中,则只有 node:
前缀的 require 调用将接收本地模块。
小心使用!
const assert = require('assert');
const realFs = require('fs');
const fakeFs = {};
require.cache.fs = { exports: fakeFs };
assert.strictEqual(require('fs'), fakeFs);
assert.strictEqual(require('node:fs'), realFs);
require.extensions
#指导 require
如何处理某些文件扩展名。
将扩展名为 .sjs
的文件处理为 .js
:
require.extensions['.sjs'] = require.extensions['.js'];
已弃用。 过去,此列表用于通过按需编译将非 JavaScript 模块加载到 Node.js 中。 但是,在实践中,有很多更好的方法可以做到这一点,例如通过其他一些 Node.js 程序加载模块,或者提前将它们编译为 JavaScript。
避免使用 require.extensions
。
使用可能会导致细微的错误,并且每个注册的扩展程序解决扩展程序的速度都会变慢。
require.main
#Module
对象代表 Node.js 进程启动时加载的入口脚本,如果程序的入口点不是 CommonJS 模块,则为 undefined
。
请参阅“访问主模块”。
在 entry.js
脚本中:
console.log(require.main);
node entry.js
Module {
id: '.',
path: '/absolute/path/to',
exports: {},
filename: '/absolute/path/to/entry.js',
loaded: false,
children: [],
paths:
[ '/absolute/path/to/node_modules',
'/absolute/path/node_modules',
'/absolute/node_modules',
'/node_modules' ] }
require.resolve(request[, options])
#request
<string> 要解析的模块路径。options
<Object>
paths
<string[]> 从中解析模块位置的路径。
如果存在,则使用这些路径而不是默认的解析路径,除了 GLOBAL_FOLDERS(例如 $HOME/.node_modules
,其总是被包含在内)。
这些路径中的每一个都用作模块解析算法的起点,这意味着从此位置检查 node_modules
层级。使用内部的 require()
工具查找模块的位置,但不加载模块,只返回解析的文件名。
如果找不到模块,则会抛出 MODULE_NOT_FOUND
错误。
require.resolve.paths(request)
#request
<string> 正在检索其查找路径的模块路径。如果 request
字符串引用核心模块,例如 http
或 fs
,则返回包含在解析 request
或 null
期间搜索的路径的数组。
在每个模块中,module
自由变量是对代表当前模块的对象的引用。
为方便起见,module.exports
也可通过 exports
模块全局访问。
module
实际上不是全局的,而是每个模块本地的。
module.children
#这个模块首次需要的对象。
module.exports
#module.exports
对象由 Module
系统创建。
有时这是不可接受的;许多人希望他们的模块成为某个类的实例。
为此,则将所需的导出对象赋值给 module.exports
。
将所需的对象赋值给 exports
只会重新绑定本地的 exports
变量,这可能不是想要的。
例如,假设正在制作一个名为 a.js
的模块:
const EventEmitter = require('events');
module.exports = new EventEmitter();
// 做一些工作,一段时间后从模块本身触发 'ready' 事件。
setTimeout(() => {
module.exports.emit('ready');
}, 1000);
然后在另一个文件中可以这样做:
const a = require('./a');
a.on('ready', () => {
console.log('module "a" is ready');
});
赋值给 module.exports
必须立即完成。
不能在任何回调中完成。
以下不起作用:
x.js
:
setTimeout(() => {
module.exports = { a: 'hello' };
}, 0);
y.js
:
const x = require('./x');
console.log(x.a);
exports
变量在模块的文件级作用域内可用,并在评估模块之前被分配 module.exports
的值。
它允许一个快捷方式,以便 module.exports.f = ...
可以更简洁地写成 exports.f = ...
。 但是,请注意,与任何变量一样,如果将新值分配给 exports
,则它就不再绑定到 module.exports
:
module.exports.hello = true; // 从模块的 require 中导出
exports = { hello: false }; // 未导出,仅在模块中可用
当 module.exports
属性被新对象完全替换时,通常也会重新分配 exports
:
module.exports = exports = function Constructor() {
// ... 等等。
};
为了阐明该行为,想象一下 require()
的这个假设实现,它与 require()
的实际实现非常相似:
function require(/* ... */) {
const module = { exports: {} };
((module, exports) => {
// 模块代码在这里。 在本例中,定义一个函数。
function someFunc() {}
exports = someFunc;
// 此时,exports 不再是 module.exports 的快捷方式,
// 并且此模块仍然会导出空的默认对象。
module.exports = someFunc;
// 此时,该模块现在将导出 someFunc,
// 而不是默认对象。
})(module, module.exports);
return module.exports;
}
module.filename
#模块的完全解析文件名。
module.id
#模块的标识符。 通常这是完全解析的文件名。
module.isPreloading
#true
。module.loaded
#模块是否已完成加载,或正在加载。
module.parent
#首先需要这个模块的模块,如果当前模块是当前进程的入口点,则为 null
,如果模块是由不是 CommonJS 模块的东西(例如:REPL 或 import
)加载的,则为 undefined
。
module.path
#模块的目录名称。
这通常与 module.id
的 path.dirname()
相同。
module.paths
#模块的搜索路径。
module.require(id)
#module.require()
方法提供了一种加载模块的方法,就像从原始模块调用 require()
一样。
为此,必须获得对 module
对象的引用。
由于 require()
返回 module.exports
,而 module
通常仅在特定模块的代码中可用,因此必须明确导出才能使用。
本章节移至模块:module
核心模块。
本章节移至模块:module
核心模块。
自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习
京ICP备14009008号@版权所有www.zixuephp.com
网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com