第1章 1.2 创建第一个 uniapp 项目
🎯 开场:磨刀不误砍柴工,工欲善其事
上一章我们聊了 uniapp 是什么、它是怎么实现「写一套代码跑 7 个平台」的。你现在脑子里应该有个画面:uniapp 就像一个万能翻译官,把你的代码翻译成各平台能听懂的话。
但光有翻译官没用,你得有原料给他翻对吧?
今天这章,就是教你怎么搭建一个 uniapp 的「厨房」——也就是创建一个能写代码、能运行、能打包的项目。
你有没有遇到过这种情况:
- 下载了个软件,点开报错「缺少运行环境」
- 看教程说「用 npm 安装」,结果电脑提示「npm 不是内部命令」
- 终于装好了,跑起来一片空白,不知道哪步出了问题
这感觉就像买了烤箱但没插电,看着别人做出蛋糕,自己只能干着急。
学完这章,你能:
1. 2 分钟内用两种方式创建 uniapp 项目(HBuilderX 可视化 + 命令行)
2. 清楚项目里每个文件夹是干嘛的
3. 跑起你的第一个「Hello Uniapp」页面
4. 理解项目结构和配置文件的作用
废话不多说,厨房搭起来。
🧱 基础 25 分钟:核心概念
1.2.1 两种创建立门户:HBuilderX vs CLI
uniapp 创建项目有两种方式,就像做菜你可以用傻瓜食谱盒(HBuilderX)或者自己买菜备料(CLI 命令行)。两种都能做出饭,区别在于你想不想折腾、想不想个性化。
方式一:HBuilderX 可视化创建(推荐新手)
是什么:HBuilderX 是 DCloud 官方出的编辑器,专门为 uniapp 优化。创建项目就像点外卖——打开 App → 选套餐 → 等送达。
为什么用:省心。语法提示、运行调试、打包发布,一条龙服务。不用担心命令行报错。
怎么用:
1. 下载安装 HBuilderX:https://www.dcloud.io/hbuilderx.html
2. 打开 HBuilderX → 菜单栏 → 文件 → 新建 → 项目
3. 选择「uni-app」→ 选模板(选「Hello UniApp」)→ 填项目名 → 点创建
就这几步,一个带完整示例的项目就创建好了。简单到不敢相信对吧?
方式二:CLI 命令行创建(适合进阶玩家)
是什么:通过终端输入命令创建项目,就像自己买食材照着菜谱做。
为什么用:更灵活,可以对接更多前端工具链,适合想深度定制的同学。
怎么用:
# 1. 确保装了 Node.js(没装的去 https://nodejs.org/ 下载)
# 2. 打开终端,输入以下命令
# 创建项目(vue3 版本,用 Vite 做构建工具)
npx degit dcloudio/uni-preset-vue#vite 项目名字
# 进入项目目录
cd 项目名字
# 安装依赖
npm install
# 启动开发服务器
npm run dev:h5 # 跑 Web 版(H5)
npm run dev:mp-weixin # 跑微信小程序
⚠️ 注意!CLI 创建的项目是「纯净版」,不带任何示例代码。你会看到一个空白页面,别慌,下一节我们往里面填内容。
两种方式怎么选:
| 对比项 | HBuilderX | CLI |
|---|---|---|
| 上手难度 | ⭐ 简单 | ⭐⭐⭐ 中等 |
| 配置复杂度 | 内置,一键搞定 | 需要自己配 |
| 灵活性 | 一般 | 高 |
| 语法提示 | 很强 | 需装插件 |
| 适合人群 | 新手、快速开发 | 进阶、深度定制 |
我的建议:新手先用 HBuilderX 跑起来,有了手感再玩 CLI。
1.2.2 项目目录结构:每个文件夹都是什么
创建完项目,你会看到一堆文件夹和文件。让我带你逛一圈,像新房带看一样——
my-uniapp/ ← 项目根目录(就是你的「厨房」)
├── src/ ← 源代码目录(真正的食材区)
│ ├── pages/ ← 页面文件夹(每个 .vue 文件就是一个页面)
│ │ └── index/
│ │ └── index.vue ← 首页的代码
│ ├── static/ ← 静态资源(图片、字体等不会变的文件)
│ ├── App.vue ← 根组件(整个应用的「天花板」)
│ ├── main.js ← 入口文件(厨房的「总电闸」)
│ ├── manifest.json ← 应用配置(App 名称、图标、平台权限等)
│ ├── pages.json ← 页面路由配置(页面之间的「导航地图」)
│ └── uni.scss ← 全局样式变量(公共「调料」)
├── node_modules/ ← 依赖包(别人帮你处理好的半成品食材)
├── package.json ← 项目配置清单(就像冰箱里的存货列表)
└── vite.config.js ← Vite 构建配置(厨房电器说明书)
重点盯这几个:
| 文件/文件夹 | 干嘛的 | 类比 |
|---|---|---|
pages/ |
放页面 .vue 文件 | 厨房里的各个灶台 |
static/ |
放图片/字体 | 冰箱里的原材料 |
App.vue |
根组件,所有页面的「老爸」 | 整栋楼的地基 |
main.js |
入口文件 | 总电闸 |
pages.json |
配置页面路由 | 电梯按钮面板 |
下一章我们会重点讲 pages.json,它决定了页面之间怎么跳转。现在先眼熟一下。

1.2.3 pages.json:你的第一个配置文件
虽然下一章会细讲,但这里先混个脸熟。
是什么:pages.json 是 uniapp 的路由配置文件,告诉程序「有哪些页面、首页是谁、长什么样」。
为什么用:你不可能把厨房所有灶台同时开着,pages.json 就是决定「现在开哪个灶台」的清单。
最简示例:
{
"pages": [
{
"path": "pages/index/index", // 页面路径(相对于 src/pages)
"style": {
"navigationBarTitleText": "我的首页" // 导航栏标题
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black", // 导航栏文字颜色
"navigationBarTitleText": "uniapp", // 默认标题
"navigationBarBackgroundColor": "#F8F8F8" // 导航栏背景色
}
}
说白了:这个文件就是「菜单」,告诉 uniapp 你的 App 有几页、每页叫什么名字。
1.2.4 App.vue:根组件,页面们的「老祖宗」
是什么:App.vue 是整个应用的根组件。你可以理解为所有页面的公共区域,比如你要给每个页面顶部加同一个导航栏,写在这里就行。
为什么用:有些东西是所有页面共用的——比如全局样式、底部 TabBar、全局数据。把它们放 App.vue,所有页面都能「继承」。
最简示例:
<script>
// 逻辑部分(这里写 JavaScript)
export default {
onLaunch() {
console.log('App 启动了')
},
onShow() {
console.log('App 显示了')
},
onHide() {
console.log('App 隐藏了')
}
}
</script>
<style>
/* 全局样式,所有页面都生效 */
page {
background-color: #f5f5f5;
font-family: -apple-system, sans-serif;
}
</style>
这 3 个生命周期函数啥意思?
- onLaunch:App 第一次打开时执行一次(像「开门迎客」)
- onShow:App 从后台切回来时执行(像「客人回来了」)
- onHide:App 切到后台时执行(像「店关门了」)
1.2.5 main.js:入口文件,「总电闸」
是什么:main.js 是 uniapp 程序的入口文件,比 App.vue 还要更「根」。
为什么用:它是第一个被执行的文件,负责创建 Vue 实例、挂载 App.vue。
最简示例:
// main.js
import App from './App.vue' // 导入根组件(就是 App.vue)
// 下面的代码是固定写法,先记住
const app = createApp(App)
app.mount('#app')
注意! 这段代码你基本不用改,但得知道它是干嘛的。就像你知道电闸在哪,但一般不动它。
1.2.6 运行你的项目:H5 预览和小程序模拟
项目创建好了,怎么看效果?
用 HBuilderX 运行
- 在 HBuilderX 中打开你的项目
- 按
Ctrl + R(或Cmd + R)运行 - 选择「运行到浏览器」或「运行到小程序模拟器」
用 CLI 运行
# H5 网页版
npm run dev:h5
# 然后浏览器打开 http://localhost:5173
# 微信小程序(需要安装微信开发者工具)
npm run dev:mp-weixin
# 打开微信开发者工具,导入 dist/dev/mp-weixin 目录

🔥 实战 35 分钟:3 个递进的小项目
学习公式:看懂 10 遍,不如自己动手跑 1 遍。
下面的项目,我都帮你写好了完整代码。复制 → 粘贴 → 运行 → 看结果。
项目 1:改改看!让你的首页显示「Hello Uniapp」(5 分钟)
目标:修改首页文字,验证项目能跑起来。
步骤:
- 打开
src/pages/index/index.vue - 找到
<template>里的文字,修改成你想显示的内容 - 保存,按
Ctrl + R运行
完整代码(src/pages/index/index.vue):
<template>
<view class="container">
<text class="title">Hello Uniapp!</text>
<text class="subtitle">我的第一个 uniapp 页面</text>
</view>
</template>
<script>
export default {
data() {
return {
title: 'Hello Uniapp',
subtitle: '我成功运行了!'
}
}
}
</script>
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 50rpx;
}
.title {
font-size: 50rpx;
font-weight: bold;
color: #007AFF;
margin-bottom: 30rpx;
}
.subtitle {
font-size: 28rpx;
color: #666;
}
</style>
预期输出:
┌─────────────────────────────┐
│ │
│ Hello Uniapp! │
│ 我的第一个 uniapp 页面 │
│ │
└─────────────────────────────┘
一句话解释:template 是页面骨架,script 是大脑(逻辑),style 是外貌(样式)。三姐妹缺一不可。
项目 2:读取本地 JSON 数据,展示一个「小明的购物清单」(15 分钟)
目标:学会从本地 JSON 文件读取数据,用 v-for 循环展示列表。
为什么要用 JSON:真实项目中,数据往往来自文件或接口。这里先学把数据「喂」给页面。
步骤:
- 在
src/static/下新建文件shopping.json:
{
"owner": "小明",
"items": [
{ "name": "牛奶", "price": 5.5, "count": 2 },
{ "name": "面包", "price": 3.0, "count": 1 },
{ "name": "鸡蛋", "price": 8.8, "count": 1 },
{ "name": "苹果", "price": 12.0, "count": 3 }
]
}
- 修改
src/pages/index/index.vue:
<template>
<view class="container">
<text class="header">{{ shoppingData.owner }}的购物清单</text>
<view class="list">
<view
class="item"
v-for="(item, index) in shoppingData.items"
:key="index"
>
<text class="item-name">{{ item.name }}</text>
<text class="item-price">¥{{ item.price }}</text>
<text class="item-count">x{{ item.count }}</text>
<text class="item-total">小计:{{ (item.price * item.count).toFixed(1) }}</text>
</view>
</view>
<view class="footer">
<text class="total">总计:¥{{ totalPrice }}</text>
</view>
</view>
</template>
<script>
import shoppingData from '@/static/shopping.json'
export default {
data() {
return {
shoppingData: shoppingData
}
},
computed: {
totalPrice() {
return this.shoppingData.items
.reduce((sum, item) => sum + item.price * item.count, 0)
.toFixed(1)
}
}
}
</script>
<style>
.container {
padding: 40rpx;
}
.header {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 40rpx;
display: block;
}
.list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
background: #f9f9f9;
padding: 25rpx;
border-radius: 10rpx;
}
.item-name {
font-size: 30rpx;
color: #333;
flex: 1;
}
.item-price {
font-size: 28rpx;
color: #666;
margin-right: 30rpx;
}
.item-count {
font-size: 28rpx;
color: #999;
margin-right: 30rpx;
}
.item-total {
font-size: 28rpx;
color: #007AFF;
font-weight: bold;
}
.footer {
margin-top: 40rpx;
text-align: right;
}
.total {
font-size: 32rpx;
font-weight: bold;
color: #ff5500;
}
</style>
预期输出:
┌─────────────────────────────┐
│ 小明的购物清单 │
├─────────────────────────────┤
│ 牛奶 ¥5.5 x2 小计:11.0│
│ 面包 ¥3.0 x1 小计:3.0 │
│ 鸡蛋 ¥8.8 x1 小计:8.8 │
│ 苹果 ¥12.0 x3 小计:36.0│
├─────────────────────────────┤
│ 总计:¥58.8 │
└─────────────────────────────┘
一句话解释:v-for 就像「点名」,把数组里的每个元素依次拿出来处理。computed 是「计算属性」,会根据数据自动算出结果。
项目 3:把购物清单改造成「勾选完成」待办清单(15 分钟)
目标:在项目 2 基础上加「勾选」功能,实现待办清单的核心逻辑。
新增功能点:
1. 点击商品可以「勾选完成」
2. 勾选的商品划线显示,总价不包含已勾选商品
<template>
<view class="container">
<text class="header">{{ shoppingData.owner }}的购物清单</text>
<view class="list">
<view
class="item"
v-for="(item, index) in shoppingData.items"
:key="index"
@click="toggleDone(index)"
>
<view class="checkbox" :class="{ checked: item.done }">
<text v-if="item.done">✓</text>
</view>
<text class="item-name" :class="{ strike: item.done }">{{ item.name }}</text>
<text class="item-price" :class="{ gray: item.done }">¥{{ item.price }}</text>
<text class="item-count">x{{ item.count }}</text>
<text class="item-total" :class="{ gray: item.done }">
小计:{{ (item.price * item.count).toFixed(1) }}
</text>
</view>
</view>
<view class="footer">
<text class="total">总计:¥{{ totalPrice }}</text>
<text class="done-count">已完成:{{ doneCount }} / {{ totalCount }}</text>
</view>
</view>
</template>
<script>
import shoppingData from '@/static/shopping.json'
export default {
data() {
return {
shoppingData: shoppingData
}
},
computed: {
totalPrice() {
return this.shoppingData.items
.filter(item => !item.done)
.reduce((sum, item) => sum + item.price * item.count, 0)
.toFixed(1)
},
doneCount() {
return this.shoppingData.items.filter(item => item.done).length
},
totalCount() {
return this.shoppingData.items.length
}
},
methods: {
toggleDone(index) {
this.shoppingData.items[index].done = !this.shoppingData.items[index].done
}
}
}
</script>
<style>
.container {
padding: 40rpx;
}
.header {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 40rpx;
display: block;
}
.list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
background: #f9f9f9;
padding: 25rpx;
border-radius: 10rpx;
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 3rpx solid #007AFF;
border-radius: 8rpx;
margin-right: 20rpx;
display: flex;
align-items: center;
justify-content: center;
color: #007AFF;
font-weight: bold;
}
.checkbox.checked {
background: #007AFF;
color: white;
}
.item-name {
font-size: 30rpx;
color: #333;
flex: 1;
}
.item-name.strike {
text-decoration: line-through;
color: #999;
}
.item-price {
font-size: 28rpx;
color: #666;
margin-right: 30rpx;
}
.item-count {
font-size: 28rpx;
color: #999;
margin-right: 30rpx;
}
.item-total {
font-size: 28rpx;
color: #007AFF;
font-weight: bold;
}
.item-total.gray, .item-price.gray {
color: #ccc;
}
.footer {
margin-top: 40rpx;
display: flex;
justify-content: space-between;
}
.total {
font-size: 32rpx;
font-weight: bold;
color: #ff5500;
}
.done-count {
font-size: 28rpx;
color: #666;
}
</style>
预期输出(点击「牛奶」后):
┌─────────────────────────────┐
│ 小明的购物清单 │
├─────────────────────────────┤
│ ☑ 牛奶 ¥5.5 x2 小计:11.0│ ← 划线了
│ □ 面包 ¥3.0 x1 小计:3.0 │
│ □ 鸡蛋 ¥8.8 x1 小计:8.8 │
│ □ 苹果 ¥12.0 x3 小计:36.0│
├─────────────────────────────┤
│ 总计:¥47.8 已完成:1/4 │
└─────────────────────────────┘
一句话解释:数据驱动视图——你改 item.done,页面自动变。filter 帮你「过滤」掉已完成的,累加时就不算它们了。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:路径大小写问题
// ❌ 错误:路径区分大小写
import data from '@/Static/shopping.json' // 报错:找不到文件
// ✅ 正确:保持和实际文件夹一致
import data from '@/static/shopping.json'
为什么踩:Mac/Linux 系统文件名严格区分大小写,Static 和 static 是两个文件夹。Windows 不报错,上线到 Linux 服务器就挂了。
坑 2:v-for 必须加 key
<!-- ❌ 错误:没加 key,控制台会报警告 -->
<view v-for="item in list">{{ item.name }}</view>
<!-- ✅ 正确:加唯一的 key -->
<view v-for="(item, index) in list" :key="index">{{ item.name }}</view>
为什么踩:不加 key,列表更新时可能会出现「乱序、闪烁」等问题。Vue 需要 key 来追踪每个节点。
坑 3:style 里别漏了 scoped
<!-- ❌ 错误:全局污染,其他页面也受影响 -->
<style>
.item { color: red; }
</style>
<!-- ✅ 正确:加上 scoped,只影响当前页面 -->
<style scoped>
.item { color: red; }
</style>
为什么踩:没 scoped,样式会污染其他页面。想象调料撒得到处都是,菜都串味了。
坑 4:computed 里别改原始数据
// ❌ 错误:在 computed 里直接修改数据
computed: {
totalPrice() {
this.shoppingData.items.forEach(item => {
item.done = true // ❌ 绝对不要在 computed 里赋值!
})
}
}
// ✅ 正确:用 methods 或在操作数据的地方改
methods: {
markAllDone() {
this.shoppingData.items.forEach(item => {
item.done = true
})
}
}
为什么踩:computed 是「只读」的,Vue 会缓存结果。在里面改数据会导致不可预期的行为。
坑 5:JSON 文件里别写注释
<!-- ❌ 错误:JSON 不支持注释 -->
{
"owner": "小明" // 这是注释,会报错
}
<!-- ✅ 正确:纯 JSON,不要注释 -->
{
"owner": "小明"
}
为什么踩:JSON 是「纯数据格式」,不支持注释。写注释会直接报错。
性能小贴士:懒加载图片
<!-- ❌ 一次性加载,慢 -->
<image src="https://example.com/big-image.jpg" />
<!-- ✅ 按需加载,快 -->
<image src="https://example.com/big-image.jpg" lazy-load />
调试技巧:console.log 是你最好的朋友
// 在 methods 里打印数据
methods: {
toggleDone(index) {
console.log('点击了第', index, '项')
console.log('当前数据', this.shoppingData.items[index])
this.shoppingData.items[index].done = !this.shoppingData.items[index].done
console.log('修改后', this.shoppingData.items[index])
}
}
注意! 发布时要删掉 console.log,或者用条件编译。线上打 console.log 会影响性能。
✏️ 练习题
练习 1(1 分钟):改个名字
- 输入:把
shopping.json里的 owner 从「小明」改成「小红」 - 预期输出:页面顶部显示「小红的购物清单」
- 提示:找到 JSON 文件,直接改中文就行
练习 2(2 分钟):加个判断
- 输入:在项目 2 基础上,如果总价超过 50 元,显示「老板大气!」
- 预期输出:
- 总价 ≤ 50:不显示额外文字
- 总价 > 50:底部多一行「老板大气!」
- 提示:在
computed里加一个判断,或者在模板里用v-if
练习 3(2 分钟):处理新数据
- 输入:新建
src/static/foods.json,包含 3 种食物(name、price、count),展示成列表 - 预期输出:正常显示 3 种食物的清单和总价
- 提示:参考项目 2 的代码,改改文件路径和数据字段名
练习 4(3 分钟):串起两个项目
- 输入:把项目 2 的购物清单和项目 3 的勾选功能结合,但数据来源改成
foods.json - 预期输出:食物清单可以勾选,勾选后总价变化
- 提示:把
shopping.json换成foods.json,其他逻辑不变
练习 5(2 分钟):找错题
- 输入:下面代码哪里错了?(无法运行)
data {
return {
name: '小明'
}
}
- 预期输出:指出错误并修正
- 提示:
data后面要加(),不是{
作业:做一个「小组预算分摊计算器」
需求描述:
你和小伙伴一起出去玩,回来要分摊费用。做一个小工具,输入几笔支出,自动算出每个人该出多少钱。
功能点:
1. 预设 3 个成员(可扩展)
2. 输入支出金额和付款人
3. 自动计算每人实际应付金额(总费用 / 人数)
4. 显示「谁欠谁」的关系(比如 A 欠 B 10 元)
加分项:
1. 数据保存在本地(下次打开还在)
2. 能删除某笔支出
验收标准:
- 能跑起来,不报错
- 输入数据后,显示正确的分摊结果
- 代码有适当注释
提交方式:评论区贴代码 或 GitHub 链接
📚 总结 + 资源
本章 3 个核心点
- 两种创建方式:HBuilderX 可视化(新手友好)vs CLI 命令行(更灵活)
- 项目目录结构:
pages/是页面,static/是资源,App.vue是根组件 - 数据驱动视图:数据变 → 页面自动变(
v-for循环、computed计算)
延伸资源
- 📖 uniapp 官方文档 - 快速上手
- 📖 Vue3 官方教程(uniapp 基于 Vue,懂 Vue 更顺手)
- 🎬 DCloud 官方视频教程(B 站搜「uniapp 入门」)
互动钩子
你创建 uniapp 项目时,遇到过什么奇葩报错?「缺 Node.js」「npm 不认识」「运行一片空白」?评论区聊聊,老粉优先回复!顺便告诉我你用的是 HBuilderX 还是 CLI,下一章我们讲「页面与路由」,会用到
pages.json这个配置文件,提前预习效果翻倍!
下一章预告:学会了创建项目,下一章我们来讲「页面之间怎么跳转」——pages.json 不只是配置,它决定了用户在你的 App 里能去哪、怎么去。吊胃口预警:学完下一章,你就能做出「点一下跳到另一个页面」的效果了!

评论(0)