JavaScript 没有“包”
前言
除了古老的 C/C++,几乎所有的编程语言都有模块系统,都有官方的包管理器。我们一般不自己实现所有的代码,实际应用开发过程中大量使用开源库和框架。这篇文章演示了如何把自己实现的库变成一个包,一个包就是你的应用,也是你的库。
随着程序越来越大,我们会将不同用途的代码放到不同的源文件。为了代码共享,我们会将部分代码提出来作为一个库。如果我们的项目越来越复杂的话,就会既有库又有可执行程序。如何组织项目的代码,如何理解一个复杂项目的代码结构。只需要掌握两点:
- 理解语言本身的模块或包的机制
- 理解包管理器或构建系统如何构建库/程序
JavaScript 语言本身没有包
JavaScript 的包不是一个语言层面的概念,是包管理器层面的概念。换句话说,JavaScript 语言本身没有包,包是 npm 的特性,让你构建、测试、分享 JS 模块。
JavaScript 只有模块,一个 .js
文件就是一个 JavaScript 模块。
JavaScript 的模块相当于是 Go 语言里面的包,只不过 Go 语言的包可以是单个目录下的多个 .go
文件。
Go语言的代码通过包(package)组织,包类似于其它语言里的库 (libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个
.go
源代码文件组成, 目录定义包的作用。每个源文件都以一条package
声明语句开始,这个例子里就 是package main
, 表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在 这个文件里的程序语句。
npm 包
BTW:Rust 也和 JavaScript 一样,Rust 语言本身没有包的概念,Rust 语言本身只有 Crate 和 Module。Rust 的 Module 是命名空间也是把代码分离到不同的源文件。Cargo 的包只能有一个 Library Crate,可以有多个 Binary Crate。rustc 一次考虑一个 crate。
node 一次考虑一个 JS 模块,node script.js
运行一个 JS 模块。npm 包只能有一个库,可以有多个可执行脚本。库的名字是 package.json
中的 name
,这也是包的名字,"main"
字段是这个库的入口。一个库也是一个包,package.json
描述了一个包。
我们来创建一个包,并使用它。
npm init
创建一个 JavaScript 的包,即创建 package.json
。创建 greeting
mkdir greeting
cd greeting
npm init -y
我们要修改 package.json
,type: module
告诉 NodeJS 这个包的 JS 文件都是 ES 模块。greeting 的用户要使用 import 来导入包或者模块就必须做这个修改。
--- i/package.json
+++ w/package.json
@@ -2,6 +2,7 @@
"name": "greeting",
"version": "1.0.0",
"main": "index.js",
+ "type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
main: index.js
是这个包的入口,我们要创建这个 index.js。index.js 是默认的 main,我们可以自定义 main。
// Filename: index.js
export function hello(name) {
return `Hi, ${name}. Welcome!`
}
这样我们就建立好了一个 JavaScript 的包,这个包提供一个 hello 函数。
创建一个名为 hello 的包,使用 greeting 这个包。
mkdir hello
cd hello
npm init -y
创建 hello 包后目录结构如下
<home>/
├── greeting
│ ├── index.js
│ └── package.json
└── hello
└── package.json
使用 greeting,我们就要安装这个包,在 hello 文件夹下执行:
npm i ../greeting
安装 greeting 包,npm 创建了一个 node_modules 目录,把 greeting 的代码放到了 node_modules。因为这是一个本地的包,npm 创建了一个符号链接,指向了 greeting 目录。
node_modules/
└── greeting -> ../../greeting
我们还没有在 hello 这个包里面写任何的代码,我们在 hello 这个包使用 greeting 提供的 hello 函数。
// Filename: hello/index.js
import { hello } from "greeting"
const message = hello("Mikami Yua")
console.log(message)
然后我们运行 hello 下的 index.js。
$ node index.js
(node:2544) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/user/hello/index.js is not specified and it doesn't parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
To eliminate this warning, add "type": "module" to /home/user/hello/package.json.
(Use `node --trace-warnings ...` to show where the warning was created)
Hi, Mikami Yua. Welcome!
index.js 就是我们 hello 程序的入口,NodeJS 会自动找到 JS 模块引用的其他 js 模块。
这里有一个警告,提示我们要消除这个警告就在 hello/package.json 中加上 "type": "module"
。NodeJS 默认使用 CommonJS,CommonJS 失败后会尝试 ES module。
npm 不仅仅是包管理器,也是包的构建工具的前端。npm build
构建这个项目,npm install
安装项目依赖。背后可能是调用 esbuild 或者其他的工具。
一个 package 就相当于是一个库,你可以引入库的某一个模块,所有的 JS 文件都是一个模块。你肯定不希望所有的 JS 文件都是公开的,有一部分代码是库内部使用的,不是 API,将来可能会改变文件的目录结构,甚至删除部分内部的函数。package.json
还有一个 "exports"
字段,显式声明这个包的哪些模块是公开的。
我们修改greeting包,使用 "exports"
,现代的 JS 项目推荐使用
--- i/package.json
+++ w/package.json
@@ -1,11 +1,11 @@
{
"name": "greeting",
"version": "1.0.0",
- "main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
+ "exports": "./index.js",
"keywords": [],
"author": "",
"license": "ISC",
把 index.js 中的 hello 函数移到 hello.js 中去。
// Filename: greeting/index.js
// re-rexport hello
export { hello } from "./hello.js"
// Filename: greeting/hello.js
export function hello(name) {
return `Hi, ${name}. Welcome!`
}
我们改变了 greeting 包的结构,但是仍然提供 hello 函数,greeting 改动后 hello 包的代码不需要做任何改动,还是可以使用 node index.js
运行。
我们作为 greeting 库的作者,知道 hello 函数是 greeting/hello.js 提供的,我要直接从对应的 JS 模块导入 hello 函数。
--- i/index.js
+++ w/index.js
@@ -1,5 +1,5 @@
// Filename: hello/index.js
-import { hello } from "greeting"
+import { hello } from "greeting/hello.js"
const message = hello("Mikami Yua")
console.log(message)
我们运行代码得到了 ERR_PACKAGE_PATH_NOT_EXPORTED 错误,"exports"
限定了有哪些模块是公开的。
$ node index.js
node:internal/modules/esm/resolve:314
return new ERR_PACKAGE_PATH_NOT_EXPORTED(
^
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './hello.js' is not defined by "exports" in /home/user/hello/node_modules/greeting/package.json imported from /home/user/hello/index.js
at exportsNotFound (node:internal/modules/esm/resolve:314:10)
at packageExportsResolve (node:internal/modules/esm/resolve:662:9)
at packageResolve (node:internal/modules/esm/resolve:842:14)
at moduleResolve (node:internal/modules/esm/resolve:926:18)
at defaultResolve (node:internal/modules/esm/resolve:1056:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:654:12)
at #cachedDefaultResolve (node:internal/modules/esm/loader:603:25)
at ModuleLoader.resolve (node:internal/modules/esm/loader:586:38)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:242:38)
at ModuleJob._link (node:internal/modules/esm/module_job:135:49) {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}
Node.js v22.13.0
如果我们把 exports 从 package.json 移除,那我们就能根据包的目录结构从任意一个模块中导入。
NodeJS 的 ES Module
文档介绍了 Node.js 会把什么东西当作是 ES Module。package.json
的 type
是 "module"
, Node.js 把输入当作是 ES Moudle。.js
文件内使用了 ES6 Module 的语法,那就是一个 ES Module。
总结
Take away message: JavaScript 本身只有模块,包的概念是 npm 和 Node.js 建立的。package.json
定义了一个 JavaScript 的包。exports 字段指定了这个包的哪些模块是公开的,公开的模块可以被用户导入。
JavaScript 的包管理方式和 Rust 的包管理的方式非常相似,一个包倾向于只是一个库,或者提供多个可执行文件。
阅读材料:
JavaScript 没有“包”的更多相关文章
- javascript 閉包
這兩種寫法都是可以的. 第一種: function a(){ var m=[]; for(var i=1; i<10; i++){ (function(i){ function b(){ con ...
- 构建javascript array包
//像一个数组添加数组. copyArray = function(inSrcArray,inDestArray){ var i; for(i=0;i<inSrcArray.length;i++ ...
- JavaScript包管理器综述
JavaScript包管理器综述 作者:chszs,未经博主同意不得转载.经许可的转载需注明作者和博客主页:http://blog.csdn.net/chszs 对于JavaScript来说.包管理器 ...
- JavaScript资源大全中文版(Awesome最新版--转载自张果老师博客)
JavaScript资源大全中文版(Awesome最新版) 目录 前端MVC 框架和库 包管理器 加载器 打包工具 测试框架 框架 断言 覆盖率 运行器 QA 工具 基于 Node 的 CMS 框 ...
- JavaScript代码模块化的正规方法
RequireJS-CommonJS-AMD-ES6 Import/Export详解 为什么起了一个这个抽象的名字呢,一下子提了四个名词分别是:RequireJS,CommonJS,AMD,ES6,答 ...
- Node.js入门:包结构
JavaScript缺少包结构.CommonJS致力于改变这种现状,于是定义了包的结构规范(http://wiki.commonjs.org/wiki/Packages/1.0 ).而NPM的 ...
- JavaScript资源大全
目录 前端MVC 框架和库 包管理器 加载器 打包工具 测试框架 框架 断言 覆盖率 运行器 QA 工具 基于 Node 的 CMS 框架 模板引擎 数据可视化 编辑器 UI 输入 日历 选择 文件上 ...
- 意外作出了一个javascript的服务器,可以通过js调用并执行任何java(包括 所有java 内核基本库)及C#类库,并最终由 C# 执行你提交的javascript代码! 不敢藏私,特与大家分
最近研发BDC 云开发部署平台的数据路由及服务管理器意外作出了一个javascript的服务器,可以通过js调用并执行任何java(包括 所有java 内核基本库)及C#类库,并最终由 C# 执行你提 ...
- Awesome Javascript(中文翻译版)
[导读]:GitHub 上有一个 Awesome – XXX 系列的资源整理.awesome-javascript 是 sorrycc 发起维护的 JS 资源列表,内容包括:包管理器.加载器.测试框架 ...
- 让你能看懂的 JavaScript 闭包
让你能看懂的 JavaScript 闭包 没有废话,直入主题,先看一段代码: var counter = (function() { var x = 1; return function() { re ...
随机推荐
- Git指南-从入门到精通
代码提交和同步命令 流程图如下: 第零步: 工作区与仓库保持一致 第一步: 文件增删改,变为已修改状态 第二步: git add ,变为已暂存状态 $ git status $ git add --a ...
- ReviOS - 专为游戏优化的 Win11 / Win10 精简版系统
ReviOS介绍 ReviOS 渴望重新创建作为操作系统的 Windows 应该是 - 简单.ReviOS 是一款功能强大.高效的私有操作系统,适用于游戏玩家.高级用户和发烧友.由于资源.占用内存和存 ...
- ARC101E题解
前言 此片题解大致按照笔者做题思路进行讲解. 简要题意 有一棵树,树上有偶数个节点.你需要给这些点两两配对,一组已经配对的点会将两点之间的树边进行一次覆盖.一组合法方案需要满足树上所有边都被覆盖至少一 ...
- 晶振测试仪GDS-80系列参数
晶振测试仪GDS-80系列 一.产品简介 晶振测试仪GDS-80系列是高性价比的晶振测试系统,采用网络分析技术,实现智能化测量,符合IEC-444标准.测量频率范围10KHz-200KHz,1MHz- ...
- 为什么将malloc()和printf()称为不可重入?
转载自https://mlog.club/article/1807704 在unix系统中,我们知道malloc()是一个不可重入的函数(系统调用).为什么? 类似地,printf()也被认为是不可重 ...
- k8s:The connection to the server localhost:8080 was refused - did you specify the right host or port?
前言 k8s 集群 node节点报错:The connection to the server localhost:8080 was refused - did you specify the rig ...
- Git Pull Failed:You have not concluded your merge.Exiting because of unfinished merge
前言 在拉取远程代码时,出现 Git Pull Failed:You have not concluded your merge.Exiting because of unfinished merge ...
- vue强制刷新页面
方法一 this.$router.go(0) // 会出现一段空白页,用户体验不好 方法二 在 app.vue 中定义 reload() 方法 <template> <div id= ...
- 使用Win32控制台实现boost共享内存通信
发送端: #define BOOST_DATE_TIME_NO_LIB #include <boost/interprocess/shared_memory_object.hpp> #in ...
- PX4 仿真环境开发整理
博客地址:https://www.cnblogs.com/zylyehuo/ (一)PX4 仿真开发 搭建仿真环境 概念介绍及环境建议 MAVROS安装(适用于ROS1.ROS2) Ubuntu安装Q ...