張鼎南 發表於 2025-5-4 16:45:00

JavaScript 没有“包”

<h2 id="前言">前言</h2>
<p>除了古老的 C/C++,几乎所有的编程语言都有模块系统,都有官方的包管理器。我们一般不自己实现所有的代码,实际应用开发过程中大量使用开源库和框架。这篇文章演示了如何把自己实现的库变成一个包,一个包就是你的应用,也是你的库。</p>
<p>随着程序越来越大,我们会将不同用途的代码放到不同的源文件。为了代码共享,我们会将部分代码提出来作为一个库。如果我们的项目越来越复杂的话,就会既有库又有可执行程序。如何组织项目的代码,如何理解一个复杂项目的代码结构。只需要掌握两点:</p>
<ul>
<li>理解语言本身的模块或包的机制</li>
<li>理解包管理器或构建系统如何构建库/程序</li>
</ul>
<h2 id="javascript-语言本身没有包">JavaScript 语言本身没有包</h2>
<p>JavaScript 的包不是一个语言层面的概念,是包管理器层面的概念。换句话说,JavaScript 语言本身没有包,包是 npm 的特性,让你构建、测试、分享 JS 模块。</p>
<p>JavaScript 只有模块,一个 <code>.js</code> 文件就是一个 JavaScript 模块。</p>
<p>JavaScript 的模块相当于是 Go 语言里面的包,只不过 Go 语言的包可以是单个目录下的多个 <code>.go</code> 文件。</p>
<blockquote>
<p>Go语言的代码通过包(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个<code>.go</code>源代码文件组成, 目录定义包的作用。每个源文件都以一条 <code>package</code> 声明语句开始,这个例子里就是 <code>package main</code> , 表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。</p>
</blockquote>
<h2 id="npm-包">npm 包</h2>
<p>BTW:Rust 也和 JavaScript 一样,Rust 语言本身没有包的概念,Rust 语言本身只有 Crate 和 Module。Rust 的 Module 是命名空间也是把代码分离到不同的源文件。Cargo 的包只能有一个 Library Crate,可以有多个 Binary Crate。rustc 一次考虑一个 crate。</p>
<p>node 一次考虑一个 JS 模块,<code>node script.js</code> 运行一个 JS 模块。npm 包只能有一个库,可以有多个可执行脚本。库的名字是 <code>package.json</code> 中的 <code>name</code>,这也是包的名字,<code>"main"</code> 字段是这个库的入口。一个库也是一个包,<code>package.json</code> 描述了一个包。</p>
<p>我们来创建一个包,并使用它。</p>
<p><code>npm init</code> 创建一个 JavaScript 的包,即创建 <code>package.json</code>。创建 greeting</p>
<pre><code class="language-sh">mkdir greeting
cd greeting
npm init -y
</code></pre>
<p>我们要修改 <code>package.json</code>,<code>type: module</code> 告诉 NodeJS 这个包的 JS 文件都是 ES 模块。greeting 的用户要使用 import 来导入包或者模块就必须做这个修改。</p>
<pre><code class="language-diff">--- 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\" &amp;&amp; exit 1"
   },
</code></pre>
<p><code>main: index.js</code> 是这个包的入口,我们要创建这个 index.js。index.js 是默认的 main,我们可以自定义 main。</p>
<pre><code>// Filename: index.js

export function hello(name) {
    return `Hi, ${name}. Welcome!`
}
</code></pre>
<p>这样我们就建立好了一个 JavaScript 的包,这个包提供一个 hello 函数。</p>
<p>创建一个名为 hello 的包,使用 greeting 这个包。</p>
<pre><code class="language-sh">mkdir hello
cd hello
npm init -y
</code></pre>
<p>创建 hello 包后目录结构如下</p>
<pre><code>&lt;home&gt;/
├── greeting
│&nbsp;&nbsp; ├── index.js
│&nbsp;&nbsp; └── package.json
└── hello
    └── package.json
</code></pre>
<p>使用 greeting,我们就要安装这个包,在 hello 文件夹下执行:</p>
<pre><code class="language-sh">npm i ../greeting
</code></pre>
<p>安装 greeting 包,npm 创建了一个 node_modules 目录,把 greeting 的代码放到了 node_modules。因为这是一个本地的包,npm 创建了一个符号链接,指向了 greeting 目录。</p>
<pre><code>node_modules/
└── greeting -&gt; ../../greeting
</code></pre>
<p>我们还没有在 hello 这个包里面写任何的代码,我们在 hello 这个包使用 greeting 提供的 hello 函数。</p>
<pre><code class="language-js">// Filename: hello/index.js
import { hello } from "greeting"

const message = hello("Mikami Yua")
console.log(message)
</code></pre>
<p>然后我们运行 hello 下的 index.js。</p>
<pre><code class="language-sh">$ node index.js
(node:2544) 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!
</code></pre>
<p>index.js 就是我们 hello 程序的入口,NodeJS 会自动找到 JS 模块引用的其他 js 模块。</p>
<p>这里有一个警告,提示我们要消除这个警告就在 hello/package.json 中加上 <code>"type": "module"</code>。NodeJS 默认使用 CommonJS,CommonJS 失败后会尝试 ES module。</p>
<ul>
<li>See also: Modules: Packages | Node.js v23.11.0 Documentation</li>
</ul>
<p>npm 不仅仅是包管理器,也是包的构建工具的前端。<code>npm build</code> 构建这个项目,<code>npm install</code> 安装项目依赖。背后可能是调用 esbuild 或者其他的工具。</p>
<p>一个 package 就相当于是一个库,你可以引入库的某一个模块,所有的 JS 文件都是一个模块。你肯定不希望所有的 JS 文件都是公开的,有一部分代码是库内部使用的,不是 API,将来可能会改变文件的目录结构,甚至删除部分内部的函数。<code>package.json</code> 还有一个 <code>"exports"</code> 字段,显式声明这个包的哪些模块是公开的。</p>
<p>我们修改greeting包,使用 <code>"exports"</code>,现代的 JS 项目推荐使用</p>
<pre><code class="language-diff">--- 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\" &amp;&amp; exit 1"
   },
+"exports": "./index.js",
   "keywords": [],
   "author": "",
   "license": "ISC",
</code></pre>
<p>把 index.js 中的 hello 函数移到 hello.js 中去。</p>
<pre><code class="language-js">// Filename: greeting/index.js
// re-rexport hello
export { hello } from "./hello.js"
</code></pre>
<pre><code class="language-js">// Filename: greeting/hello.js

export function hello(name) {
    return `Hi, ${name}. Welcome!`
}
</code></pre>
<p>我们改变了 greeting 包的结构,但是仍然提供 hello 函数,greeting 改动后 hello 包的代码不需要做任何改动,还是可以使用 <code>node index.js</code> 运行。</p>
<p>我们作为 greeting 库的作者,知道 hello 函数是 greeting/hello.js 提供的,我要直接从对应的 JS 模块导入 hello 函数。</p>
<pre><code class="language-diff">--- 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)
</code></pre>
<p>我们运行代码得到了 ERR_PACKAGE_PATH_NOT_EXPORTED 错误,<code>"exports"</code> 限定了有哪些模块是公开的。</p>
<pre><code>$ node index.js
node:internal/modules/esm/resolve:314
return new ERR_PACKAGE_PATH_NOT_EXPORTED(
         ^

Error : 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
</code></pre>
<p>如果我们把 exports 从 package.json 移除,那我们就能根据包的目录结构从任意一个模块中导入。</p>
<h2 id="nodejs-的-es-module">NodeJS 的 ES Module</h2>
<ul>
<li>Modules: Packages | Node.js v23.11.0 Documentation</li>
</ul>
<p>文档介绍了 Node.js 会把什么东西当作是 ES Module。<code>package.json</code> 的 <code>type</code> 是 <code>"module"</code>, Node.js 把输入当作是 ES Moudle。<code>.js</code> 文件内使用了 ES6 Module 的语法,那就是一个 ES Module。</p>
<h2 id="总结">总结</h2>
<p><strong>Take away message</strong>: JavaScript 本身只有模块,包的概念是 npm 和 Node.js 建立的。<code>package.json</code> 定义了一个 JavaScript 的包。exports 字段指定了这个包的哪些模块是公开的,公开的模块可以被用户导入。<br>
JavaScript 的包管理方式和 Rust 的包管理的方式非常相似,一个包倾向于只是一个库,或者提供多个可执行文件。</p>
<p>阅读材料:</p>
<ul>
<li>About packages and modules | npm Docs</li>
<li>Modules: Packages | Node.js v23.11.0 Documentation</li>
</ul><br><br>
来源:https://www.cnblogs.com/wngtk/p/18859340
頁: [1]
查看完整版本: JavaScript 没有“包”