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 ...
随机推荐
- FreeSql学习笔记——2.插入
前言 由于还没有表结构,就先从新增开始,插入一些数据后才好做查询.修改.删除操作. 初始化 前面注入FreeSql时设置过自动同步表结构,那么就不用管数据库了,只需要在项目中定义实体,就会自动生成表结 ...
- RabbitMQ(五)——发布订阅模式
RabbitMQ系列 RabbitMQ(一)--简介 RabbitMQ(二)--模式类型 RabbitMQ(三)--简单模式 RabbitMQ(四)--工作队列模式 RabbitMQ(五)--发布订阅 ...
- flutter-iOS数字键盘无法属于小数点
keyboardType:TextInputType.numberWithOptions(decimal: true),
- 使用 Visual Paradigm 的业务流程模型和符号 (BPMN) 综合指南
业务流程模型和符号 (BPMN) 是一种用于建模和记录业务流程的标准化图形符号.它被广泛采用,因为它能够提供一种清晰.通用的语言,所有利益相关者(业务分析师.技术开发人员和管理人员)都能理解.Visu ...
- 解决Typecho文章cid不连续的教程
Typecho下文章编号(cid)不连续,虽然不影响什么,也无关紧要,但是对于有强迫症的人(比如我)来说,真的是无法忍受.还好有大佬提供了解决办法. 将以下代码保存为php文件,上传至网站根目录,在浏 ...
- Flume - [04] Hive Sink
一.概述 HIVE Sink 将包含分割文本或JSON数据的事件直接流到Hive表或分区中.事件是使用Hive事务编写的.一旦一组事件被提交到Hive,它们就会立即对hive查询可见.流到其中的分 ...
- HttpClient 进行soap请求
当你在使用C#的HttpClient进行SOAP请求时,确保你的代码类似于以下示例: using System; using System.Net.Http; using System.Text; u ...
- docker配置Nvidia环境,使用GPU
前言 需要 nvdia driver 安装好,请参考 Ubuntu Nvidia driver驱动安装及卸载 docker 安装 配置 apt 阿里云的镜像源 sudo curl -fsSL http ...
- python list 差集
前言 有时候我们希望基于list得到一个集合C,该集合C的元素可以被描述为元素在集合A中而不在集合B中.即:差集. 基于set A = [1, 2, 3] B = [2, 3, 4] C = set( ...
- go 遍历修改切片数据
package main import "fmt" type good struct { id int64 sum int64 } func main() { good1 := g ...