【仓颉语言入门 · 第21课】cjpm 包管理与多文件项目组织:从单文件练习迈向工程化开发

前 20 课的代码都活在一个单独的 src/main.cj 里,靠 import std.* 使用标准库。但真实项目必然要拆文件、分包、复用别人写的库。本课带你掌握 cjpm 包管理:一个工程的目录结构、多文件如何协作、如何把自己的代码打成库包、如何通过本地路径引用第三方包,以及 cjpm.toml 的完整配置项。

本文所有代码与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。


目录(系列导航)

整套路线共 7 个模块、30 课:

模块课次内容
一、环境与入门01~05环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环
二、常用类型与数据组织06~10字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理
三、函数与函数式11~14函数、Lambda 与高阶函数、闭包、迭代器与惰性序列
四、面向对象与类型系统15~20struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展
五、工程化与标准库21~25cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试
六、并发编程26~28线程、Channel 通道与同步原语、并发实战
七、项目实战29~30命令行小工具、GeoJSON 数据处理实战
  1. 环境搭建与第一个仓颉程序
  2. 变量与常量:let / var 与基本数据类型
  3. 运算符与标准输入输出
  4. 分支结构:if 与 match 表达式
  5. 循环结构:while / for / Range
  6. 字符串详解与字符串插值
  7. 数组 Array 与区间 Range
  8. 集合框架:ArrayList、HashMap、HashSet
  9. 可空类型 ? 与 Option
  10. 错误处理:异常机制与 Result
  11. 函数定义、参数与返回值
  12. Lambda 与高阶函数
  13. 闭包、作用域与函数类型
  14. 迭代器 Iterator 与 Sequence
  15. 结构体 struct 与类 class
  16. 构造函数、属性与方法
  17. 接口 interface 与实现
  18. 枚举 enum、代数数据类型与 match 模式匹配
  19. 泛型编程
  20. 扩展、类型别名与可见性控制
  21. cjpm 包管理与多文件项目组织(本文)
  22. 文件与目录 IO
  23. JSON 处理(结合 stdx 扩展库)
  24. 网络编程入门
  25. 单元测试
  26. 并发基础:线程的创建与等待
  27. Channel 通道与同步原语
  28. 并发实战:多线程任务处理
  29. 实战一:带文件持久化的命令行小工具
  30. 实战二:GeoJSON 数据处理程序

一、单文件项目的瓶颈

回顾前 20 课,我们的代码都是这个结构:

myproj/
├── cjpm.toml
└── src/
    └── main.cj      ← 所有代码塞在这一个文件里

cjpm.toml 最少需要这几个必填字段(cjpm init 会自动生成完整配置):

[package]
  name = "myproj"
  version = "1.0.0"
  cjc-version = "1.2.0"
  output-type = "executable"

随着功能变多,单文件会迅速失控:

  • 一个文件几千行,找代码靠搜索;
  • 不同功能混在一起,改一处怕动全身;
  • 想复用之前写的工具类?只能复制粘贴;
  • 团队协作时,多人改同一个文件冲突不断。

工程化的第一步就是拆文件、分包。


二、cjpm 项目的标准目录结构

cjpm 是仓颉官方的包管理器兼构建工具(类似 Rust 的 cargo、Go 的 go mod)。一个标准 cjpm 项目的目录结构如下:

myproj/
├── cjpm.toml          # 项目配置文件(必须)
├── src/               # 源码目录(必须)
│   ├── main.cj        # 程序入口(可执行项目)
│   ├── utils.cj       # 工具函数
│   └── models.cj      # 数据模型
├── target/            # 编译输出目录(cjpm 自动生成,不用管)
└── cjpm.lock          # 依赖锁定文件(cjpm 自动生成,不用管)

2.1 cjpm init:一键创建项目

不用手动建目录,cjpm 提供了脚手架命令:

cjpm init --name myproj

生成的目录结构:

myproj/
├── cjpm.toml
└── src/
    └── main.cj

cjpm.toml 自带完整配置(cjc-version、version、output-type 等必填字段已填好),main.cj 是一个可运行的骨架:

package myproj

main(): Int64 {
    println("hello world")
    return 0
}

2.2 cjpm.toml 详解

cjpm.toml 是项目的"身份证",控制项目名、版本、依赖、编译选项。先看一个完整的可执行项目配置:

[package]
  name = "myproj"           # 项目名(包名),只能用小写字母/数字/下划线,不能含连字符
  version = "1.0.0"         # 语义化版本(必填)
  cjc-version = "1.2.0"     # 编译器版本要求(必填)
  output-type = "executable" # 输出类型:executable(可执行)或 static(静态库,必填)
  src-dir = "src"           # 源码目录,默认 "src"
  description = "我的第一个仓颉项目"
  license = "MIT"

[dependencies]
  # 本地路径依赖
  # mylib = { path = "../mylib" }

  # 远程仓库依赖(需要配置 registry)
  # cangjie-mysql-driver = { version = "0.1.0" }

注意:name、version、cjc-version、output-type 是必填字段,缺了 cjpm build 会报错。cjpm init 生成的配置已包含所有必填项。

关键字段说明:

字段说明常用值
name项目名,也是默认包名小写字母/数字/下划线,不能含 -
version语义化版本1.0.0
output-type输出类型executable(默认)/ static
src-dir源码目录src(默认)
[dependencies]依赖表见第四节

三、多文件协作:把代码拆出去

3.1 同一个包里的多文件

src/ 目录下所有 .cj 文件默认属于同一个包(包名 = cjpm.toml 里的 name)。拆文件不需要任何 import,直接引用即可:

src/models.cj:

package myproj

class User {
    let name: String
    let age: Int64

    init(name: String, age: Int64) {
        this.name = name
        this.age = age
    }

    func isAdult(): Bool {
        return this.age >= 18
    }
}

src/utils.cj:

package myproj

func formatUser(user: User): String {
    let status = if (user.isAdult()) { "成年" } else { "未成年" }
    return "${user.name}(${user.age}岁,${status})"
}

src/main.cj:

package myproj

main(): Int64 {
    let u = User("小明", 20)
    println(formatUser(u))
    return 0
}

运行 cjpm run,输出:

小明(20岁,成年)

注意:三个文件都写了 package myproj,它们共享同一个包作用域,不需要 import。

3.2 子目录 = 子包

当文件更多时,需要按功能分包。仓颉的规则是:src/ 下的子目录自动成为子包,包名 = 项目名 + 子目录路径。

myproj/
├── cjpm.toml
└── src/
    ├── main.cj           # package myproj
    ├── models/
    │   └── user.cj       # package myproj.models
    └── utils/
        └── format.cj     # package myproj.utils

src/models/user.cj:

package myproj.models

public class User {
    public let name: String
    public let age: Int64

    public init(name: String, age: Int64) {
        this.name = name
        this.age = age
    }

    public func isAdult(): Bool {
        return this.age >= 18
    }
}

src/utils/format.cj:

package myproj.utils

import myproj.models.User

public func formatUser(user: User): String {
    let status = if (user.isAdult()) { "成年" } else { "未成年" }
    return "${user.name}(${user.age}岁,${status})"
}

src/main.cj:

package myproj

import myproj.models.User
import myproj.utils.formatUser

main(): Int64 {
    let u = User("小明", 20)
    println(formatUser(u))
    return 0
}

运行结果同上。注意三个要点:

  1. 子包里的类/函数要加 public——第 20 课讲过,跨包访问默认 internal,包外不可见;
  2. import 写完整路径:import myproj.models.User,不是 import models.User;
  3. import 的是符号,不是文件——import myproj.utils.formatUser 导入的是 formatUser 这个函数,不是 format.cj 这个文件。

3.3 子包之间的依赖

子包之间也可以互相 import,但会形成依赖链。建议保持单向依赖:main → utils → models,避免循环引用。


四、依赖管理:引用别人的包

4.1 本地路径依赖

最常见的场景:你写了一个通用库 mylib,想在另一个项目 myproj 里用。

目录结构:

workspace/
├── mylib/
│   ├── cjpm.toml
│   └── src/
│       └── lib.cj
└── myproj/
    ├── cjpm.toml
    └── src/
        └── main.cj

mylib/cjpm.toml:

[package]
  name = "mylib"
  version = "0.1.0"
  output-type = "static"    # 静态库

mylib/src/lib.cj:

package mylib

public func greet(name: String): String {
    return "Hello, ${name}!"
}

myproj/cjpm.toml:

[package]
  name = "myproj"

[dependencies]
  mylib = { path = "../mylib" }

myproj/src/main.cj:

package myproj

import mylib.greet

main(): Int64 {
    println(greet("仓颉"))
    return 0
}

运行 cjpm run,输出:

Hello, 仓颉!

cjpm 会自动编译 mylib,再编译 myproj 并链接。

4.2 远程仓库依赖

如果包发布到了 cjpm 官方仓库(或私服),可以用版本号引用:

[dependencies]
  cangjie-mysql-driver = { version = "0.1.0" }

首次 cjpm build 会自动下载并缓存到本地。

4.3 依赖锁定:cjpm.lock

cjpm build 后会在项目根目录生成 cjpm.lock,记录所有依赖(包括间接依赖)的精确版本。团队开发时应该把 cjpm.lock 提交到 git,保证所有人用同一套依赖版本。


五、静态库 vs 可执行程序

特性可执行程序(executable)静态库(static)
output-typeexecutable(默认)static
入口必须有 main(): Int64不需要 main
被依赖不能作为其他项目的依赖可以被其他项目依赖
输出生成 .exe生成 .a(Linux/macOS)或 .lib(Windows)

什么时候用 static?

  • 写通用工具库、SDK 封装、算法库;
  • 把一个大项目拆成"核心逻辑库 + 命令行壳"。

什么时候用 executable?

  • 写最终给用户运行的程序;
  • 写测试用的 demo。

六、实战:把订单系统拆成多文件工程

我们把第 20 课的订单金额工具箱拆成一个规范的 cjpm 工程。

6.1 目录结构

ordersys/
├── cjpm.toml
└── src/
    ├── main.cj
    ├── models/
    │   └── order.cj
    └── utils/
        └── money.cj

6.2 代码实现

ordersys/cjpm.toml:

[package]
  name = "ordersys"
  version = "1.0.0"
  cjc-version = "1.2.0"
  output-type = "executable"

src/models/order.cj:

package ordersys.models

public class Order {
    public let id: String
    private var paid: Bool = false
    private let amountFen: Int64

    public init(id: String, amountFen: Int64) {
        this.id = id
        this.amountFen = amountFen
    }

    public func pay(): Unit {
        this.paid = true
    }

    public func isPaid(): Bool {
        return this.paid
    }

    public func getAmountFen(): Int64 {
        return this.amountFen
    }
}

src/utils/money.cj:

package ordersys.utils

public func fenToYuan(fen: Int64): String {
    let sign = if (fen < 0) { "-" } else { "" }
    let abs = if (fen < 0) { -fen } else { fen }
    let yuan = abs / 100
    let cents = abs % 100
    let centsText = if (cents < 10) { "0${cents}" } else { "${cents}" }
    return "${sign}${yuan}.${centsText}"
}

public func yuanText(fen: Int64): String {
    return fenToYuan(fen)
}

src/main.cj:

package ordersys

import ordersys.models.Order
import ordersys.utils.yuanText

main(): Int64 {
    let orders = [
        Order("NO.1001", 1250),
        Order("NO.1002", 800),
        Order("NO.1003", 99)
    ]

    var total = 0
    for (o in orders) {
        o.pay()
        let status = if (o.isPaid()) { "已支付" } else { "待支付" }
        println("订单(${o.id}) ${yuanText(o.getAmountFen())} 元 [${status}]")
        total += o.getAmountFen()
    }
    println("实收合计:${yuanText(total)} 元")
    return 0
}

运行 cjpm run,输出:

订单(NO.1001) 12.50 元 [已支付]
订单(NO.1002) 8.00 元 [已支付]
订单(NO.1003) 0.99 元 [已支付]
实收合计:21.49 元

6.3 设计要点

  • 金额用整数分存储,yuanText() 负责展示层格式化——和第 20 课的方案一致;
  • paid 是 private,外部只能通过 pay() 改状态,不能绕过;
  • models 和 utils 单向依赖:main → models + utils,utils 不依赖 models;
  • 每个子包只暴露必要的 public 成员,内部实现细节保持默认可见性。

七、常用 cjpm 命令速查

命令作用
cjpm init --name xxx创建新项目
cjpm build编译项目
cjpm run编译并运行(可执行项目)
cjpm clean清理编译输出
cjpm check类型检查,不生成二进制
cjpm test运行单元测试(第 25 课讲)

八、常见问题 FAQ

Q1:子目录里的文件一定要写 package 项目名.子目录 吗?

是的。仓颉按目录推导包名,src/models/ 下的文件必须写 package myproj.models,写 package myproj 会报包声明冲突(第 464 号错误)。

Q2:我可以把多个类塞在一个文件里吗?

可以,一个 .cj 文件里可以写任意多个类、函数、扩展。但建议一个文件只放一类功能,方便维护。

Q3:import 写 myproj.models.User 还是 myproj.models?

import 的是符号(类名、函数名),不是包名。import myproj.models.User 导入 User 这个类;如果写 import myproj.models,编译器会报"找不到符号"。

Q4:本地路径依赖写相对路径还是绝对路径?

推荐相对路径(如 ../mylib),因为绝对路径在别人电脑上会失效。相对路径以当前项目的 cjpm.toml 所在目录为基准。

Q5:静态库能直接运行吗?

不能。output-type = "static" 的项目没有 main 函数,编译产物是 .lib/.a,只能被可执行项目链接。

Q6:一个项目能同时是库和可执行程序吗?

可以,但仓颉的做法是拆成两个项目:一个 static 库项目 + 一个 executable 壳项目。壳项目依赖库项目,只做参数解析和打印。


九、课后练习

  1. 用 cjpm init --name calc 创建一个新项目,把第 11 课的加、减、乘、除四个函数拆到 src/ops.cj,main.cj 里调用并打印 3 + 5、10 - 2、4 * 6、20 / 4 的结果。
  2. 在 calc 项目里新建 src/advanced/ 子目录,写一个 power(base: Int64, exp: Int64): Int64 函数(幂运算),在 main.cj 里 import 并计算 2^10。
  3. 创建两个项目:stringutils(static 库,提供 reverse(s: String): String)和 myapp(可执行,本地路径依赖 stringutils)。在 myapp 里调用 reverse("hello") 并打印结果。
  4. 把第 20 课的订单金额工具箱按本课 6.1 节的结构拆成 ordersys 工程,确保能 cjpm run 出正确结果。
  5. 挑战:给 stringutils 加 isPalindrome(s: String): Bool(判断回文),在 myapp 里测试 "level" 和 "hello";思考为什么 stringutils 里的函数要加 public。

下节预告

工程能拆分了,但程序还活在内存里——重启就丢数据。第 22 课 文件与目录 IO 将讲解:如何用 std.fs 读写文本文件、遍历目录、处理路径,以及把本课的订单数据持久化到 JSON 文件,让程序真正"记住"东西。


系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.2.0)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。


💬 遇到问题?扫码联系作者

跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信直接咨询(请备注"仓颉课程"):
在这里插入图片描述

离线环境下图片可能加载不出来,也可以在 CIDE 菜单 Help ▸ 联系作者 / Contact 中查看同一张二维码(应用内置兜底图,无需联网)。


📥 工具下载

本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):

  • GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
  • 打开页面后进入 发行版(Releases),两种包任选其一:
    • 安装版:下载 CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用;
    • 免安装版(Portable):下载 CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
  • 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn
Logo

一站式 AI 云服务平台

更多推荐