【仓颉语言入门 · 第21课】
【仓颉语言入门 · 第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~20 | struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展 |
| 五、工程化与标准库 | 21~25 | cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试 |
| 六、并发编程 | 26~28 | 线程、Channel 通道与同步原语、并发实战 |
| 七、项目实战 | 29~30 | 命令行小工具、GeoJSON 数据处理实战 |
- 环境搭建与第一个仓颉程序
- 变量与常量:let / var 与基本数据类型
- 运算符与标准输入输出
- 分支结构:if 与 match 表达式
- 循环结构:while / for / Range
- 字符串详解与字符串插值
- 数组 Array 与区间 Range
- 集合框架:ArrayList、HashMap、HashSet
- 可空类型
?与 Option - 错误处理:异常机制与 Result
- 函数定义、参数与返回值
- Lambda 与高阶函数
- 闭包、作用域与函数类型
- 迭代器 Iterator 与 Sequence
- 结构体 struct 与类 class
- 构造函数、属性与方法
- 接口 interface 与实现
- 枚举 enum、代数数据类型与 match 模式匹配
- 泛型编程
- 扩展、类型别名与可见性控制
- cjpm 包管理与多文件项目组织(本文)
- 文件与目录 IO
- JSON 处理(结合 stdx 扩展库)
- 网络编程入门
- 单元测试
- 并发基础:线程的创建与等待
- Channel 通道与同步原语
- 并发实战:多线程任务处理
- 实战一:带文件持久化的命令行小工具
- 实战二: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
}
运行结果同上。注意三个要点:
- 子包里的类/函数要加
public——第 20 课讲过,跨包访问默认internal,包外不可见; - import 写完整路径:
import myproj.models.User,不是import models.User; - 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-type | executable(默认) | 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 壳项目。壳项目依赖库项目,只做参数解析和打印。
九、课后练习
- 用
cjpm init --name calc创建一个新项目,把第 11 课的加、减、乘、除四个函数拆到src/ops.cj,main.cj里调用并打印3 + 5、10 - 2、4 * 6、20 / 4的结果。 - 在
calc项目里新建src/advanced/子目录,写一个power(base: Int64, exp: Int64): Int64函数(幂运算),在main.cj里 import 并计算2^10。 - 创建两个项目:
stringutils(static 库,提供reverse(s: String): String)和myapp(可执行,本地路径依赖stringutils)。在myapp里调用reverse("hello")并打印结果。 - 把第 20 课的订单金额工具箱按本课 6.1 节的结构拆成
ordersys工程,确保能cjpm run出正确结果。 - 挑战:给
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
更多推荐




所有评论(0)