Node.js 折腾日记:开发自用的 npm 包(下)

Node.js 折腾日记:开发自用的 npm 包(下)
Node.js 折腾日记:开发自用的 npm 包(上)
最近做一些知识梳理的时候,想着还是可以用 TypeScript 做一个自用的包,封装常用的工具函数(一直以来都是用的独立 js 文件),便于跨项目使用。

使用 moranutils

紧接上篇文章,接下来我们再创建一个项目来模拟如何使用开发的 npm 包。

# 创建项目目录
mkdir Mor_Test

# 进入项目目录
cd Mor_Test

# 初始化项目,直接使用默认设置
yarn init -y

# 安装 TypeScript
yarn add typescript --dev

# 初始化 TypeScript 配置文件
npx tsc --init

多的不说,直接上修改后的 tsconfig.json 文件内容:

{
  "compilerOptions": {
    // 指定 ECMAScript 目标版本,这里是 ES2017
    "target": "ES2017",
    // 使用最新的 ES 模块标准
    "module": "ESNext",
    // 允许导入符合 ES6 模块规范的模块,确保与 CommonJS 模块兼容
    "esModuleInterop": true,
    // 强制文件名的大小写必须一致,防止在大小写敏感的操作系统中出现问题
    "forceConsistentCasingInFileNames": true,
    // 启用所有严格类型检查选项
    "strict": true,
    // 跳过检查 '.d.ts' 声明文件
    "skipLibCheck": true,
    "moduleResolution": "node",
  },
  "include": [
    "src/**/*"
  ]
}

基本内容都有注释,特意说明一下 moduleResolution,因为我们会通过软链接(下文会说)的方式使用的 moranutils 包,在 package.json 中没有包记录,设置 moduleResolution 的是告诉解析器需要查找 node_modules 目录。

接着上修改后的 package.json 文件内容:

{
  "name": "mor_test",
  "version": "1.0.0",
  "type": "module",
  "license": "MIT",
  "devDependencies": {
    "typescript": "^5.4.4"
  },
  "scripts": {
    "build": "tsc --outDir dist/"
  }
}

这里我删除了默认配置中的 main,因为这是一个正式项目,并不会作为包对其他项目开放调用,所以不需要这个配置。type 设置同上篇文章讲解的那样,这里不过多介绍。build 中添加了构建的输出目录,这个没有放到 tsconfig.json 文件中,后面会说。

为了保证流程的完整性,这次先放出目录结构,然后再做说明:

.
├── node_modules
├── public
│   └── index.html
├── src
│   └── index.ts
├── package.json
├── tsconfig.json
└── yarn.lock

public 和 src 这里不多说,问就是约定。

接下来我们要在 index.ts 中使用 moranutils 包的函数,打印当前时间字符串。

正常我们要使用包的时候,会使用 npm install [package_name] 或者 yarn add [package_name] 安装,然后使用。

那么对于本地包,其实也可以,如下命令:

yarn add file:D:\Mor_Npm

npm 命令同理,这里就等于是安装了本地包。但是这种安装方式有一个缺点,就是每次修改了 moranutils 包的代码,都需要再次安装更新,偶尔疏忽还会被缓存坑。

这里推荐使用软链接的方式,具体操作步骤如下:

  1. 在 Mor_Npm 包项目中执行 yarn link 命令
  2. 接着在 Mor_Test 项目中执行 yarn link moranutils 命令

这样一来就通过软链接安装了本地包,下图:

我们更新 Mor_Test 的 index.ts 文件内容,如下:

// /src/index.ts

import { currentTime } from "moranutils";

console.log(currentTime.getDateTime());

注:如果使用的 VSCode 在使用包的时候出现错误提示,可以重启一下 VSCode,其他编辑器不知道。

再更新 index.html 文件内容,如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    Hello World!
    <script src="../dist/index.js"></script>
</body>
</html>

这里为什么是 ../dist/index.js?因为浏览器只能运行 js 文件,编译后的 js 文件在 dist 目录。接着我们使用 yarn build 编译一下,在浏览器中打开 index.html

浏览器打开后,打开控制台,面对的结果不是输出了日期,是报错:

Uncaught SyntaxError: Cannot use import statement outside a module

必然,为了更好的引出构建的概念,这里故意出错。我们细看 index.js 文件会发现,​依然使用的 import { currentTime } from "moranutils";,如下:

import { currentTime } from "moranutils";
console.log(currentTime.getDateTime());

这里其实完全可以想到,dist 目录下没有任何与 moranutils 相关的代码文件,这个调用完全没有来由。并且 js 文件中也不支持 import 导入的形式,更多的是在 html 页面中使用类似 <script type="module" src="path/to/your/script.js"></script> 的方式导入模块,这可太复杂了。

构建代码

script 标签导入的方式不是本文主题,所以不深入说明。其实做这个我才看明白了,之前导入外部模块的时候,老让我用 <script type="module" src="path/to/your/script.js"></script>,真的看着就头疼,也是写这篇文章前弄明白了,顺带明白了构建的意义。

常用的构建工具一般就是 Webpack、Rollup、Vite 等,这里使用 Vite。之前使用 Vue 的时候,每次都是使用 Vite 初始化项目,对于我这种浅层玩家,略过了很多细节,就迷糊。

这次反过来了,先把项目创建好了,Vite 对我来说就是一个开发依赖工具,更能加深对构建工具的理解。在 Mor_Test 中安装 Vite,如下:

yarn add vite --dev

这里不是全局安装,并且只是开发依赖。安装完成后,在项目中创建 vite.config.ts 配置文件,编辑如下内容:

import { defineConfig } from "vite";

export default defineConfig({
	build: {
		// 指定输出目录,默认是 'dist'
		outDir: "dist",
		// 每次构建时清空输出目录
		emptyOutDir: true,
		rollupOptions: {
			// 构建时候的入口文件,这里就是构建我们的 index.ts 文件
			input: {
				index: "src/index.ts",
			},
			// 输出配置
			output: {
				// 输出文件的模块格式
				format: "esm",
				// 输出文件不使用 hash,直接以其原始名称命名
				entryFileNames: `assets/[name].js`,
			},
		},
	},
});

基本注释我都写的很详细了,其实构建的目的简单理解就是:不管用的是 JavaScript 还是 TypeScript 开发,项目随着开发进度,会有越来越多的 js/ts 文件,同时也会引入外部模块,管理起来很不方便。通过构建,能将这些模块打包成一个或多个 js 文件,同时还能处理代码优化、混淆、解决兼容性问题等,具体的只需要对比构建后的 index.js 代码就能明白了。

我们还需要修改一下 package.json 的脚本内容,如下:

{
  "name": "mor_test",
  "version": "1.0.0",
  "type": "module",
  "license": "MIT",
  "devDependencies": {
    "typescript": "^5.4.4",
    "vite": "^5.2.8"
  },
  "scripts": {
    "build": "vite build"
  }
}

这里我们修改了 build 中的内容,直接使用 Vite 构建,这也是为什么 tsconfig.json 文件中的配置内容少了一些的原因。

还需要修改一下 index.html 的内容,如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    Hello World!
    <script src="assets/index.js"></script>
</body>
</html>

这里修改了 script 的 src 内容,因为我们上面 Vite 的设置是将编译后的 js 文件放在 dist/assets 中的,index.html 会在构建的时候从 public 目录复制到 dist 目录(问就是默认约定)。

执行 yarn build 开始构建,构建完成后的最终目录结构如下:

.
├── dist
│   ├── assets
│   │   └── index.js
│   └── index.html
├── node_modules
├── package.json
├── public
│   └── index.html
├── src
│   └── index.ts
├── tsconfig.json
├── vite.config.ts
└── yarn.lock

我们再看看 index.js 的代码,如下:

const g={getDateTime:()=>{const t=new Date,e=t.getFullYear(),n=t.getMonth()+1,o=t.getDate(),r=t.getHours(),a=t.getMinutes(),s=t.getSeconds();return`${e}-${n.toString().padStart(2,"0")}-${o.toString().padStart(2,"0")} ${r.toString().padStart(2,"0")}:${a.toString().padStart(2,"0")}:${s.toString().padStart(2,"0")}`}};console.log(g.getDateTime());

完全不一样了,已经将我们要使用的函数混淆后放到了一起,这就是构建的意义。运行 dist 目录下的 index.html,就能在控制台中看到打印的内容了。


以上,其实通过这个过程,我更多理清的是构建的意义和整体逻辑,非常有价值,也希望对看到本文的你有价值。

整个过程的源码可以通过 https://wqmoran.com/software-download-guide-tips/ 获取,“源码”目录下的“Moran_Study.zip”就是,记得使用 yarn install 命令安装依赖后,创建软链接才能使用。