第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 - 配图1


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 运行

  1. 在 HBuilderX 中打开你的项目
  2. Ctrl + R(或 Cmd + R)运行
  3. 选择「运行到浏览器」或「运行到小程序模拟器」

用 CLI 运行

# H5 网页版
npm run dev:h5
# 然后浏览器打开 http://localhost:5173

# 微信小程序(需要安装微信开发者工具)
npm run dev:mp-weixin
# 打开微信开发者工具,导入 dist/dev/mp-weixin 目录

配图2 - 配图2


🔥 实战 35 分钟:3 个递进的小项目

学习公式:看懂 10 遍,不如自己动手跑 1 遍。
下面的项目,我都帮你写好了完整代码。复制 → 粘贴 → 运行 → 看结果


项目 1:改改看!让你的首页显示「Hello Uniapp」(5 分钟)

目标:修改首页文字,验证项目能跑起来。

步骤

  1. 打开 src/pages/index/index.vue
  2. 找到 <template> 里的文字,修改成你想显示的内容
  3. 保存,按 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:真实项目中,数据往往来自文件或接口。这里先学把数据「喂」给页面

步骤

  1. 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 }
]
}
  1. 修改 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 系统文件名严格区分大小写,Staticstatic 是两个文件夹。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 个核心点

  1. 两种创建方式:HBuilderX 可视化(新手友好)vs CLI 命令行(更灵活)
  2. 项目目录结构pages/ 是页面,static/ 是资源,App.vue 是根组件
  3. 数据驱动视图:数据变 → 页面自动变(v-for 循环、computed 计算)

延伸资源

互动钩子

你创建 uniapp 项目时,遇到过什么奇葩报错?「缺 Node.js」「npm 不认识」「运行一片空白」?评论区聊聊,老粉优先回复!顺便告诉我你用的是 HBuilderX 还是 CLI,下一章我们讲「页面与路由」,会用到 pages.json 这个配置文件,提前预习效果翻倍!


下一章预告:学会了创建项目,下一章我们来讲「页面之间怎么跳转」——pages.json 不只是配置,它决定了用户在你的 App 里能去哪、怎么去。吊胃口预警:学完下一章,你就能做出「点一下跳到另一个页面」的效果了!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。