第6章 6.4 nvue 原生渲染

🎯 开场 3 分钟:为什么要学这个?

上一章我们聊了各种小程序平台的差异,你会发现:不同平台长得不一样就算了,连底层技术都各不相同。这就尴尬了——你写的东西在这个平台能跑,换个平台可能就卡成 PPT。

痛点来了:

  • 用 vue 写界面,渲染慢、帧率低,玩复杂动画直接卡死
  • 想做个视频播放界面原生一点,但 vue 里的 video 组件体验稀碎
  • 听说 nvue 能解决性能问题,但打开文档一看——啥是 weex?啥是 subNVue?直接劝退

这一章我们就来解决这个问题。学完之后,你就能判断什么时候该用 nvue、什么时候继续用 vue,还能写出一个能跑的原生态列表页面。下章我们还会用它来做一个真正的跨端电商 App,所以这一章是铺垫!


🧱 基础 25 分钟:核心概念(小白视角)

6.4.1 先搞清楚:nvue 是个啥?

类比时间到: 想象你去餐厅吃饭。

  • vue 就像是你点了一份"外卖」:厨师做好之后,装进盒子里,骑手送到你手上\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n。你吃到的味道,和厨房里出锅的味道,中间隔了好几个环节。
  • nvue 就像是你坐在「透明厨房」里:厨师就在你面前做,你看到的是第一手的成品,没有中间商赚差价。

翻译成技术话:nvue 是 uniapp 基于 Weex 引擎的原生渲染模式,它直接用原生组件来构建界面,而不是 WebView 里的 H5 页面。

6.4.2 nvue vs vue:一张表说清楚

对比项 vue 页面 nvue 页面
渲染引擎 WebView(浏览器内核) Weex(原生渲染)
性能 一般,复杂动画容易卡 流畅,接近原生体验
支持的平台 所有平台 App 端(iOS/Android)+ 部分小程序
组件库 H5 组件,样式丰富 原生组件,样式有限
写法 Vue 标准写法 Vue 写法 + Weex 限制
支持 5+ ❌ 不支持 ✅ 支持

什么时候用 nvue?

  • 要做高性能需求的页面(瀑布流、长列表、游戏界面)
  • 需要和原生组件深度交互(视频、地图、直播推流)
  • 做 App 端开发,追求原生体验

什么时候继续用 vue?

  • 快速开发,跨平台兼容要求高
  • 页面比较简单,不需要极致性能
  • 需要丰富的 H5 组件和生态

6.4.3 第一个 nvue 页面:手把手

好,废话不多说,先跑起来再说。

步骤 1:创建 nvue 文件

在 uniapp 项目里,找 pages 目录,新建一个文件叫 nvue-demo.nvue(注意后缀是 .nvue 不是 .vue)。

步骤 2:写代码

<template>
<view class="container">
<text class="title">我的第一个 nvue 页面</text>
<text class="subtitle">这是原生渲染的哦!</text>
<button @click="handleClick">点我试试</button>
</view>
</template>

<script>
export default {
data() {
return {
  clickCount: 0
}
},
methods: {
handleClick() {
  this.clickCount++
  uni.showToast({
    title: `点击了 ${this.clickCount} 次`,
    icon: 'none'
  })
}
}
}
</script>

<style>
.container {
flex: 1;
justify-content: center;
align-items: center;
padding: 20px;
}
.title {
font-size: 22px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 30px;
}
</style>

代码解释:

  • <template> 里写界面结构,用的是原生组件(viewtextbutton
  • data() 定义数据,这里存了个点击计数器
  • methods 定义方法,点击按钮就 +1
  • <style> 里的写法比 vue 简化很多,只支持 flex 布局(这也是个坑,后面会讲)

步骤 3:配置路由

pages.json 里添加:

{
"pages": [
{
  "path": "pages/nvue-demo/nvue-demo",
  "style": {
    "navigationBarTitleText": "nvue 初体验"
  }
}
]
}

运行一下,你会看到页面的渲染速度明显比 vue 快!

6.4.4 关键概念:原生组件和 Weex 限制

nvue 用的是原生组件,不是 H5 组件。这意味着:

❌ nvue 里没有这些 vue 常用组件:

  • <div><span><p> 这些 HTML 标签
  • <image> 用不了,得用 <image> 组件(注意大小写)
  • <input><textarea> 有限制

✅ nvue 里用这些原生组件:

  • <view> - 容器,相当于 div
  • <text> - 文本,相当于 span/p
  • <image> - 图片
  • <scroll-view> - 可滚动区域
  • <list> - 长列表专用组件,比 scroll-view 性能好太多!
  • <cell> - 列表项,专和 <list> 配合使用
  • <video><map><canvas> 等原生组件

重点:list-view 性能差异

如果你要做长列表(商品列表、聊天记录),一定要用 <list> 而不是 <scroll-view>。性能差距有多大?100 条数据以内看不出区别,1000 条以上,scroll-view 可能卡成幻灯片,list 依然流畅。

<template>
<list class="list">
<cell v-for="item in itemList" :key="item.id" class="list-item">
  <text>{{ item.name }}</text>
</cell>
</list>
</template>

<script>
export default {
data() {
return {
  itemList: Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `商品 ${i + 1}`
  }))
}
}
}
</script>

<style>
.list {
flex: 1;
}
.list-item {
padding: 15px;
border-bottom: 1px solid #eee;
}
</style>

6.4.5 subNVue:弹出层的解决方案

有时候你需要在页面上覆盖一个原生组件(比如弹出层、原生导航栏、侧边栏),用 vue 做的话很费劲,但 subNVue 就是专门干这个的。

类比: subNVue 就像是在视频上叠加一个「弹幕层」——弹幕是独立的,能覆盖视频内容,但又和视频同步。

使用场景:

  • 原生下拉刷新
  • 原生弹出菜单
  • 原生侧滑导航
  • 直播弹幕

配置方法:

pages.json 里配置 subNVue:

{
"pages": [
{
  "path": "pages/index/index",
  "style": {
    "subNVues": [
      {
        "id": "popup",
        "path": "pages/subNVue/popup",
        "type": "popup"
      }
    ]
  }
}
]
}

然后在 vue 页面里控制它:

// 打开 popup
const subNVue = uni.getSubNVueById('popup')
subNVue.show('slide-in-bottom', 300)

// 关闭 popup
subNVue.hide('slide-out-bottom', 300)

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

项目 1(5 分钟):仿原生联系人列表

需求: 用 nvue 做一个联系人列表,点击联系人不报错(只是弹 Toast 提示)。

完整代码:

<!-- pages/contacts/contacts.nvue -->
<template>
<view class="page">
<view class="header">
  <text class="header-title">通讯录</text>
  <text class="header-count">{{ contactList.length }} 位联系人</text>
</view>
<list class="contact-list" @scroll="handleScroll">
  <cell v-for="contact in contactList" :key="contact.id" class="contact-item" @click="onContactClick(contact)">
    <view class="avatar">
      <text class="avatar-text">{{ contact.name.charAt(0) }}</text>
    </view>
    <view class="info">
      <text class="name">{{ contact.name }}</text>
      <text class="phone">{{ contact.phone }}</text>
    </view>
  </cell>
</list>
</view>
</template>

<script>
export default {
data() {
return {
  contactList: [
    { id: 1, name: '张三', phone: '13800138001' },
    { id: 2, name: '李四', phone: '13800138002' },
    { id: 3, name: '王五', phone: '13800138003' },
    { id: 4, name: '赵六', phone: '13800138004' }
  ]
}
},
methods: {
onContactClick(contact) {
  uni.showToast({
    title: `联系 ${contact.name}`,
    icon: 'none'
  })
},
handleScroll(e) {
  console.log('滚动中', e.contentOffset)
}
}
}
</script>

<style>
.page {
flex: 1;
background-color: #f5f5f5;
}
.header {
padding: 15px;
background-color: #fff;
border-bottom: 1px solid #eee;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.header-count {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.contact-list {
flex: 1;
}
.contact-item {
flex-direction: row;
align-items: center;
padding: 12px 15px;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
}
.avatar {
width: 45px;
height: 45px;
border-radius: 22px;
background-color: #007AFF;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.avatar-text {
color: #fff;
font-size: 18px;
font-weight: bold;
}
.info {
flex: 1;
}
.name {
font-size: 16px;
color: #333;
}
.phone {
font-size: 13px;
color: #999;
margin-top: 3px;
}
</style>

预期输出: 运行后能看到带头像的联系人列表,点击任意联系人会弹 Toast。


项目 2(15 分钟):从 JSON 读数据的商品展示页

需求: 读取本地 JSON 文件,展示商品列表,每个商品显示图片、名称、价格,点击弹出商品详情(用 subNVue)。

先准备商品数据文件 static/goods.json

[
{
"id": 1,
"name": "iPhone 15 Pro",
"price": 7999,
"image": "/static/iphone.png",
"desc": "苹果旗舰手机,A17 Pro 芯片"
},
{
"id": 2,
"name": "小米 14 Ultra",
"price": 5999,
"image": "/static/xiaomi.png",
"desc": "徕卡影像,骁龙 8 Gen3"
},
{
"id": 3,
"name": "华为 Mate 60 Pro",
"price": 6999,
"image": "/static/huawei.png",
"desc": "麒麟 9000S,卫星通话"
}
]

nvue 页面代码:

<!-- pages/goods/goods.nvue -->
<template>
<view class="page">
<view class="header">
  <text class="header-title">商品列表</text>
</view>
<list class="goods-list">
  <cell v-for="goods in goodsList" :key="goods.id" class="goods-item" @click="showDetail(goods)">
    <image class="goods-image" :src="goods.image" mode="aspectFill"></image>
    <view class="goods-info">
      <text class="goods-name">{{ goods.name }}</text>
      <text class="goods-desc">{{ goods.desc }}</text>
      <text class="goods-price">¥{{ goods.price }}</text>
    </view>
  </cell>
</list>
</view>
</template>

<script>
export default {
data() {
return {
  goodsList: []
}
},
onLoad() {
this.loadGoods()
},
methods: {
loadGoods() {
  // 模拟从本地 JSON 读取数据
  uni.request({
    url: '/static/goods.json',
    success: (res) => {
      this.goodsList = res.data
    },
    fail: () => {
      // 如果读取失败,用内置数据(演示用)
      this.goodsList = [
        { id: 1, name: '商品示例', price: 999, desc: '这是演示数据', image: '' }
      ]
    }
  })
},
showDetail(goods) {
  uni.showModal({
    title: goods.name,
    content: `${goods.desc}\n\n价格:¥${goods.price}`,
    showCancel: false
  })
}
}
}
</script>

<style>
.page {
flex: 1;
background-color: #f5f5f5;
}
.header {
padding: 15px;
background-color: #fff;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.goods-list {
flex: 1;
}
.goods-item {
flex-direction: row;
padding: 10px 15px;
background-color: #fff;
border-bottom: 1px solid #eee;
}
.goods-image {
width: 80px;
height: 80px;
border-radius: 8px;
background-color: #f0f0f0;
margin-right: 12px;
}
.goods-info {
flex: 1;
justify-content: center;
}
.goods-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
.goods-desc {
font-size: 13px;
color: #888;
margin-top: 4px;
}
.goods-price {
font-size: 15px;
color: #ff5000;
font-weight: bold;
margin-top: 6px;
}
</style>

预期输出: 页面加载后显示商品卡片列表,点击任意商品弹出详情弹窗。


项目 3(15 分钟):组合做一个「待办清单小工具」

需求: 用 nvue 做一个待办清单,支持添加任务、标记完成、删除任务,数据存本地。

核心思路:

  • <list> 渲染任务列表
  • uni.setStorageSync 做本地持久化
  • 点击切换完成状态
<!-- pages/todo/todo.nvue -->
<template>
<view class="page">
<!-- 输入区域 -->
<view class="input-area">
  <input class="todo-input" v-model="newTask" placeholder="输入新任务..." @confirm="addTask" />
  <button class="add-btn" @click="addTask">添加</button>
</view>

<!-- 任务列表 -->
<list class="todo-list">
  <cell v-for="(task, index) in taskList" :key="task.id" class="todo-item">
    <view class="todo-content" @click="toggleTask(index)">
      <view :class="['checkbox', task.done ? 'checkbox-done' : '']">
        <text v-if="task.done" class="check-icon">✓</text>
      </view>
      <text :class="['todo-text', task.done ? 'todo-done' : '']">{{ task.text }}</text>
    </view>
    <text class="delete-btn" @click="deleteTask(index)">删除</text>
  </cell>
</list>

<!-- 统计 -->
<view class="stats">
  <text class="stats-text">共 {{ taskList.length }} 项,已完成 {{ doneCount }} 项</text>
  <text class="clear-btn" @click="clearDone">清除已完成</text>
</view>
</view>
</template>

<script>
export default {
data() {
return {
  newTask: '',
  taskList: [],
  nextId: 1
}
},
computed: {
doneCount() {
  return this.taskList.filter(t => t.done).length
}
},
onLoad() {
this.loadTasks()
},
methods: {
loadTasks() {
  const stored = uni.getStorageSync('my-todos')
  if (stored) {
    this.taskList = stored.list
    this.nextId = stored.nextId
  }
},
saveTasks() {
  uni.setStorageSync('my-todos', {
    list: this.taskList,
    nextId: this.nextId
  })
},
addTask() {
  const text = this.newTask.trim()
  if (!text) {
    uni.showToast({ title: '请输入任务内容', icon: 'none' })
    return
  }
  this.taskList.unshift({
    id: this.nextId++,
    text: text,
    done: false
  })
  this.newTask = ''
  this.saveTasks()
},
toggleTask(index) {
  this.taskList[index].done = !this.taskList[index].done
  this.saveTasks()
},
deleteTask(index) {
  this.taskList.splice(index, 1)
  this.saveTasks()
},
clearDone() {
  this.taskList = this.taskList.filter(t => !t.done)
  this.saveTasks()
}
}
}
</script>

<style>
.page {
flex: 1;
background-color: #f5f5f5;
}
.input-area {
flex-direction: row;
padding: 15px;
background-color: #fff;
border-bottom: 1px solid #eee;
}
.todo-input {
flex: 1;
height: 40px;
padding: 0 15px;
background-color: #f5f5f5;
border-radius: 20px;
font-size: 14px;
}
.add-btn {
width: 70px;
height: 40px;
margin-left: 10px;
background-color: #007AFF;
border-radius: 20px;
color: #fff;
font-size: 14px;
justify-content: center;
align-items: center;
}
.todo-list {
flex: 1;
}
.todo-item {
flex-direction: row;
align-items: center;
padding: 15px;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
}
.todo-content {
flex: 1;
flex-direction: row;
align-items: center;
}
.checkbox {
width: 22px;
height: 22px;
border-radius: 11px;
border: 2px solid #ddd;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.checkbox-done {
background-color: #07c160;
border-color: #07c160;
}
.check-icon {
color: #fff;
font-size: 14px;
font-weight: bold;
}
.todo-text {
font-size: 15px;
color: #333;
}
.todo-done {
color: #999;
text-decoration: line-through;
}
.delete-btn {
color: #ff3b30;
font-size: 14px;
padding: 5px 10px;
}
.stats {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
background-color: #fff;
border-top: 1px solid #eee;
}
.stats-text {
font-size: 13px;
color: #666;
}
.clear-btn {
font-size: 13px;
color: #007AFF;
}
</style>

预期输出:

  1. 输入框输入文字,点击「添加」或按回车,任务出现在列表顶部
  2. 点击任务左侧圆圈,任务变绿并带删除线
  3. 点击「删除」直接删掉该任务
  4. 刷新页面数据依然在(本地存储)

💪 进阶 20 分钟:常见坑 + 性能小贴士

🕳️ 坑 1:nvue 里只能用 flex 布局

❌ 错误写法:

<style>
.box {
display: block;
position: absolute;
left: 10px;
}
</style>

✅ 正确写法:

<style>
.box {
flex-direction: row; /* 或者 column */
}
</style>

nvue 的样式只支持 flex 布局,不支持普通流、position 定位(absolute/fixed 不生效)。如果你要做「相对于父容器居中」,就用 justify-content: center; align-items: center;


🕳️ 坑 2:文字必须包在 <text> 标签里

❌ 错误写法:

<view>这是纯文本</view>

✅ 正确写法:

<view><text>这是文本</text></view>

nvue 里,所有文字都必须是 <text> 组件的子节点,否则不会显示。


🕳️ 坑 3:图片路径要注意平台差异

❌ 错误写法:

<image src="../../static/logo.png"></image>

✅ 正确写法(使用绝对路径):

<image src="/static/logo.png"></image>

nvue 里图片路径建议使用绝对路径(以 / 开头),相对路径在 App 端可能会出现找不到图片的问题。


🕳️ 坑 4:onLoad 可能在某些场景不触发

nvue 页面的生命周期和 vue 略有不同。如果你的 onLoad 不触发,可以试试把初始化逻辑放到 onShow 里:

export default {
onShow() {
console.log('nvue 页面显示了')
this.loadData()
}
}

🕳️ 坑 5:v-model 在 nvue 里有限制

nvue 不完全支持 v-model,尤其是输入框。上面项目 3 里我们用的是 :value + @input 的组合。如果你要用 v-model,最好测试一下是否正常工作。


⚡ 性能小贴士:用 <list> 而不是 <scroll-view>

这条我们项目里已经用过了,再强调一次:长列表必须用 list 组件

<!-- ❌ 卡顿警告 -->
<scroll-view scroll-y>
<view v-for="item in list">...</view>
</scroll-view>

<!-- ✅ 流畅体验 -->
<list scroll-y>
<cell v-for="item in list">...</cell>
</list>

🔧 调试技巧:console.log 和 webview 调试

nvue 的调试比 vue 麻烦一点,但有几个方法:

  1. 直接用 console.log:在 HBuilderX 的控制台能看到输出
  2. App 端 webview 调试:在手机设置里打开「开发者选项 → USB 调试」,然后在 Chrome 访问 chrome://inspect

✏️ 练习题 + 作业题

练习题(5 道,10 分钟内完成)

练习 1(2 分钟):改个标题
- 输入:把项目 1 的页面标题改成「我的通讯录」
- 预期输出:页面顶部显示「我的通讯录」
- 提示:找到 header-title 对应的 <text> 标签改文字

练习 2(2 分钟):加个判断
- 输入:在项目 1 里,如果联系人数量为 0,显示「暂无联系人」
- 预期输出:没有联系人时,列表区域显示「暂无联系人」文字
- 提示:用 v-if 控制列表显示,配合一个 <text> 标签做空状态提示

练习 3(3 分钟):换个数据源
- 输入:用项目 2 的方法,但读取另一个 JSON 文件(自己创建一个 fruits.json,放几种水果的名字和价格)
- 预期输出:页面显示水果列表,不是商品列表
- 提示:JSON 格式参考项目 2 的 goods.json

练习 4(5 分钟):串起来
- 输入:把项目 2 的商品列表改成「待办事项」(用项目 3 的本地存储思路,但用 nvue + list 实现)
- 预期输出:一个可以用 nvue 渲染的待办清单,数据存本地
- 提示:核心是把项目 3 的 todo 逻辑迁移到 nvue 的 list 结构里

练习 5(3 分钟):报错分析
- 输入:下面这段 nvue 代码运行后报「Cannot read property 'charAt' of undefined」,请分析原因

<text class="avatar-text">{{ contact.name.charAt(0) }}</text>
  • 预期输出:说出原因并给出修复方案
  • 提示:检查 contact.name 是否可能为空,或者数据结构是否符合预期

作业题(30 分钟 - 2 小时)

作业:做一个「nve 原生渲染实战工具」

需求描述:
做一个「图书管理小工具」,用 nvue 原生渲染,实现以下功能:

功能点:

  1. 图书列表展示:用 <list> 展示图书,每本书显示书名、作者、评分
  2. 添加图书:底部有输入框,可以添加新书(书名、作者必填,评分默认 8.0)
  3. 删除图书:左滑或点击删除按钮删除图书
  4. 本地持久化:刷新页面后数据不丢失

加分项:

  1. 实现「按评分排序」(高的在前)
  2. 用 subNVue 做一个「添加成功」的轻提示动画

验收标准:

  • 能跑起来,不报错
  • 添加图书后立即显示在列表
  • 关闭再打开 App,数据依然在
  • 列表滚动流畅(用 <list> 组件)

提交方式: 评论区贴核心代码片段或 GitHub 链接,老粉优先回复!


📚 总结 + 资源

本文学到的 3 个核心点:

  1. nvue 是基于 Weex 的原生渲染,比 vue 性能好,但只支持 App 端和小程序
  2. nvue 只能用 flex 布局和原生组件,没有 div/span,需要适应新的写法
  3. 长列表必须用 <list> + <cell>,这是 nvue 性能优化的关键

延伸学习资源:

互动钩子:
「你做过需要高性能的页面吗?是用 nvue 解决的还是用 vue 优化的?评论区聊聊,老粉优先回复!」


📖 上一章我们学了各种小程序平台差异,下一章我们要用 nvue + vue 组合,做一个真正的跨端电商 App实战项目,把前面学的跨平台知识真正用起来!

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