1 vite
1.1 [1]说明
01.什么是Vite
a.定义
Vite是一个现代化的前端构建工具,由Vue.js作者尤雨溪开发
在开发环境下提供极速的冷启动和热模块替换(HMR)
在生产环境下提供优化的打包输出
b.核心特点
基于原生ESM的开发服务器,利用浏览器原生ES模块支持
使用esbuild进行依赖预构建,大幅提升启动速度
丰富的插件生态系统,兼容Rollup插件
c.技术优势
开发服务器启动速度极快,比传统工具快10-100倍
热更新性能优异,几乎瞬时完成
按需编译,只编译当前修改的文件
生产环境优化,基于Rollup进行代码分割和tree-shaking
02.发展背景
a.传统工具的痛点
Webpack等传统工具在大型项目中启动缓慢
热更新性能随项目规模增大而下降
配置复杂,学习曲线陡峭
b.技术革新
利用浏览器原生ESM支持,实现按需加载
使用esbuild替代webpack进行依赖预构建
简化配置,提供合理的默认值
c.生态系统
与主流框架无缝集成(Vue、React、Svelte等)
丰富的插件生态,满足各种开发需求
活跃的社区和持续的版本迭代
03.开发服务器
a.极速启动
基于原生ESM,无需打包整个应用
依赖预构建,将CommonJS/UMD转换为ESM
按需加载,只处理当前页面需要的模块
b.热模块替换(HMR)
基于ESM的HMR,保持应用状态
几乎瞬时的更新速度
精确到模块级别的更新
c.源码映射
开箱即用的源码映射支持
精确的错误定位
生产环境可选择性生成
04.构建优化
a.代码分割
自动识别代码分割点
动态导入支持
优化加载性能
b.Tree-shaking
移除未使用的代码
减小打包体积
提高运行时性能
c.资源优化
自动压缩CSS和JavaScript
图片资源优化
生成现代浏览器兼容的代码
05.插件系统
a.插件架构
基于Rollup插件系统
丰富的钩子函数
灵活的插件组合
b.官方插件
@vitejs/plugin-vue:Vue 3支持
@vitejs/plugin-react:React支持
@vitejs/plugin-legacy:浏览器兼容性支持
c.社区生态
丰富的第三方插件
框架特定插件
工具类插件
06.配置系统
a.配置文件
支持vite.config.js/ts
ES模块语法支持
类型提示和自动补全
b.环境配置
基于模式的环境变量
开发/生产环境区分
自定义环境变量支持
c.构建选项
多目标构建支持
自定义输出格式
详细的构建配置
1.2 [1]配置Vite
01.基础配置
a.配置文件
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
open: true
},
build: {
outDir: 'dist',
assetsDir: 'assets'
}
})
b.配置函数
// 支持基于命令和模式的动态配置
export default defineConfig(({ command, mode }) => {
if (command === 'serve') {
return {
// 开发环境特定配置
server: {
host: true
}
}
} else {
return {
// 构建环境特定配置
build: {
sourcemap: false
}
}
}
})
c.共享配置
// 多个配置文件共享配置
import baseConfig from './vite.base.config'
import { defineConfig } from 'vite'
export default defineConfig({
...baseConfig,
// 特定环境配置
})
02.服务器配置
a.开发服务器选项
server: {
host: '0.0.0.0', // 监听地址
port: 3000, // 端口号
open: true, // 自动打开浏览器
cors: true, // 启用CORS
proxy: { // 代理配置
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
b.HMR配置
server: {
hmr: {
overlay: true, // 显示错误覆盖层
port: 24678 // HMR端口
}
}
c.中间件支持
// 自定义中间件
export default defineConfig({
server: {
middlewareMode: true,
configureServer(server) {
server.middlewares.use((req, res, next) => {
// 自定义中间件逻辑
next()
})
}
}
})
03.构建配置
a.输出配置
build: {
outDir: 'dist', // 输出目录
assetsDir: 'assets', // 静态资源目录
sourcemap: true, // 生成源码映射
minify: 'terser', // 压缩工具
target: 'es2015' // 目标浏览器
}
b.代码分割配置
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
utils: ['lodash', 'axios']
}
}
}
}
c.优化配置
build: {
chunkSizeWarningLimit: 1000, // 块大小警告限制
assetsInlineLimit: 4096, // 内联资源大小限制
cssCodeSplit: true // CSS代码分割
}
1.3 [1]插件系统
01.插件基础
a.插件结构
// my-vite-plugin.js
export default function myVitePlugin(options = {}) {
return {
name: 'my-vite-plugin',
// 插件钩子
resolveId(id) {
if (id === 'virtual-module') {
return id
}
},
load(id) {
if (id === 'virtual-module') {
return 'export default "virtual"'
}
}
}
}
b.插件钩子
// 通用钩子
export default {
name: 'my-plugin',
// 构建前
buildStart() {
console.log('构建开始')
},
// 解析ID
resolveId(source, importer) {
// 自定义解析逻辑
},
// 加载模块
load(id) {
// 自定义加载逻辑
},
// 转换代码
transform(code, id) {
// 自定义转换逻辑
}
}
c.插件配置
// vite.config.js
import myPlugin from './my-vite-plugin'
export default defineConfig({
plugins: [
myPlugin({
// 插件选项
option1: 'value1',
option2: 'value2'
})
]
})
02.常用插件
a.框架插件
// Vue插件
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// 编译器选项
}
}
})
]
})
// React插件
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react({
jsxRuntime: 'automatic',
// 其他React选项
})
]
})
b.工具插件
// 自动导入
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
AutoImport({
imports: ['vue', 'vue-router'],
dts: true
}),
Components({
dts: true
})
]
})
// 图标处理
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
export default defineConfig({
plugins: [
Icons({
compiler: 'vue3',
autoInstall: true
}),
Components({
resolvers: [IconsResolver()]
})
]
})
c.优化插件
// 压缩优化
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
viteCompression({
algorithm: 'gzip',
ext: '.gz'
})
]
})
// PWA支持
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
// Workbox选项
}
})
]
})
03.插件开发
a.虚拟模块
export default function virtualModulePlugin() {
const virtualModuleId = 'virtual:my-module'
const resolvedVirtualModuleId = '\0' + virtualModuleId
return {
name: 'virtual-module-plugin',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `export default { message: 'Hello from virtual module' }`
}
}
}
}
b.代码转换
export default function transformPlugin() {
return {
name: 'transform-plugin',
transform(code, id) {
if (id.endsWith('.custom')) {
// 自定义文件转换逻辑
return {
code: transformedCode,
map: sourceMap
}
}
}
}
}
c.构建钩子
export default function buildHookPlugin() {
return {
name: 'build-hook-plugin',
buildStart() {
console.log('构建开始')
},
buildEnd() {
console.log('构建结束')
},
generateBundle(options, bundle) {
// 生成bundle时的处理
}
}
}
1.4 [2]依赖预构建
01.预构建原理
a.为什么需要预构建
Vite在开发环境中将所有代码作为原生ESM提供服务
但许多依赖以CommonJS或UMD格式发布
某些ESM依赖内部包含大量模块,导致大量HTTP请求
b.预构建过程
使用esbuild将依赖转换为ESM格式
将多模块依赖打包为单个模块
缓存预构建结果,提高后续启动速度
c.性能优势
减少HTTP请求数量
提高模块加载速度
兼容性更好,支持更多浏览器
02.预构建配置
a.依赖优化
// vite.config.js
export default defineConfig({
optimizeDeps: {
// 强制预构建的依赖
include: ['vue', 'vue-router'],
// 排除预构建的依赖
exclude: ['some-large-dep'],
// 自定义预构建行为
esbuildOptions: {
target: 'es2020'
}
}
})
b.缓存控制
optimizeDeps: {
// 强制重新预构建
force: true,
// 禁用预构建缓存
noDiscovery: true
}
c.自定义处理
optimizeDeps: {
// 自定义依赖处理
entries: ['src/main.js'],
// 预构建所有链接的依赖
include: ['linked-dep']
}
03.预构建调试
a.调试信息
// 启动时显示预构建信息
DEBUG=vite:deps npm run dev
b.预构建结果查看
// node_modules/.vite/deps目录下查看预构建结果
// 可以看到转换后的代码和依赖关系
c.常见问题解决
// 预构建失败时的处理
optimizeDeps: {
// 排除有问题的依赖
exclude: ['problematic-dep'],
// 强制包含特定依赖
include: ['specific-dep/file']
}
1.5 [2]环境变量与模式
01.环境变量基础
a.环境变量文件
# .env # 所有情况下都会加载
# .env.local # 所有情况下都会加载,但会被git忽略
# .env.[mode] # 只在指定模式下加载
# .env.[mode].local # 只在指定模式下加载,但会被git忽略
b.变量命名规则
# 只有以VITE_开头的变量才会暴露给客户端代码
VITE_APP_TITLE=My App
VITE_API_URL=https://api.example.com
# 其他变量只在构建过程中可用
DATABASE_URL=postgresql://...
c.变量访问
// 在代码中访问环境变量
console.log(import.meta.env.VITE_APP_TITLE)
// 获取所有环境变量
console.log(import.meta.env)
02.模式配置
a.默认模式
# 开发模式:vite或vite dev
# 生产模式:vite build
# 预览模式:vite preview
b.自定义模式
# package.json
{
"scripts": {
"dev": "vite --mode development",
"staging": "vite --mode staging",
"build": "vite build --mode production"
}
}
c.模式特定配置
// vite.config.js
export default defineConfig(({ command, mode }) => {
// 根据模式返回不同配置
if (mode === 'development') {
return {
// 开发模式配置
}
} else if (mode === 'staging') {
return {
// 预发布模式配置
}
} else {
return {
// 生产模式配置
}
}
})
03.环境变量类型
a.内置环境变量
import.meta.env.MODE // 当前模式
import.meta.env.PROD // 是否为生产环境
import.meta.env.DEV // 是否为开发环境
import.meta.env.SSR // 是否为服务端渲染
import.meta.env.BASE_URL // 基础URL
b.自定义环境变量
# .env.production
VITE_APP_TITLE=My App (Production)
VITE_API_URL=https://api.production.com
# .env.staging
VITE_APP_TITLE=My App (Staging)
VITE_API_URL=https://api.staging.com
c.类型定义
// vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_URL: string
// 更多环境变量类型定义
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
1.6 [2]静态资源处理
01.资源引用方式
a.直接导入
import logo from './logo.png'
// 开发环境:logo为路径字符串
// 生产环境:logo为优化后的URL
b.URL引用
import logoUrl from './logo.png?url'
// 始终返回资源URL,不进行任何处理
c.内联引用
import logoInline from './logo.png?inline'
// 将资源作为base64字符串内联
d.原始内容
import logoRaw from './logo.png?raw'
// 将资源作为字符串导入
02.资源类型处理
a.图片资源
// 自动优化和压缩
import smallImage from './small.png' // 小图片转为base64
import largeImage from './large.png' // 大图片复制到输出目录
// 响应式图片
import icon from './icon.svg?raw' // SVG作为字符串导入
import iconComponent from './icon.svg' // SVG作为React/Vue组件导入
b.CSS资源
import './style.css' // CSS文件导入
import './module.css' // CSS模块导入
import './style.scss' // SCSS文件导入(需要预处理器插件)
// CSS中的资源引用
.background {
background: url('./bg.png'); // Vite会处理此引用
}
c.JSON资源
import data from './data.json' // JSON对象导入
import dataRaw from './data.json?raw' // JSON字符串导入
import dataUrl from './data.json?url' // JSON URL导入
03.资源优化配置
a.资源内联阈值
// vite.config.js
export default defineConfig({
build: {
assetsInlineLimit: 4096, // 小于4kb的资源内联为base64
// 静态资源目录
assetsDir: 'assets'
}
})
b.资源处理插件
// 图片优化
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import viteImagemin from 'vite-plugin-imagemin'
export default defineConfig({
plugins: [
viteImagemin({
// 图片压缩配置
gifsicle: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
pngquant: { quality: [0.65, 0.8] },
svgo: {
plugins: [
{ removeViewBox: false },
{ removeEmptyAttrs: false }
]
}
})
]
})
c.资源CDN配置
// 开发环境使用本地资源,生产环境使用CDN
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production'
return {
base: isProduction ? 'https://cdn.example.com/' : '/',
build: {
// 生产环境资源处理
assetsDir: isProduction ? 'assets' : 'static'
}
}
})
2 pinia
2.1 [0]对比vuex
01.pinia
a.说明
Pinia 起始于 2019 年 11 月左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。
从那时起,我们就倾向于同时支持 Vue 2 和 Vue 3,并且不强制要求开发者使用组合式 API,我们的初心至今没有改变。
除了安装和 SSR 两章之外,其余章节中提到的 API 均支持 Vue 2 和 Vue 3。
虽然本文档主要是面向 Vue 3 的用户,但在必要时会标注出 Vue 2 的内容,因此 Vue 2 和 Vue 3 的用户都可以阅读本文档。
b.为什么取名 Pinia?
Pinia (发音为 /piːnjʌ/,类似英文中的 "peenya") 是最接近有效包名 piña (西班牙语中的 pineapple,即"菠萝") 的词。
菠萝花实际上是一组各自独立的花朵,它们结合在一起,由此形成一个多重的水果。 与 Store 类似,每一个都是独立诞生的,
但最终它们都是相互联系的。 它(菠萝)也是一种原产于南美洲的美味热带水果。
02.为什么使用pinia
a.说明
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。如果你熟悉组合式 API 的话,
你可能会认为可以通过一行简单的 export const state = reactive({}) 来共享一个全局状态。
对于单页应用来说确实可以,但如果应用在服务器端渲染,这可能会使你的应用暴露出一些安全漏洞。
b.说明
而如果使用 Pinia,即使在小型单页应用中,你也可以获得如下功能:
测试工具集
插件:可通过插件扩展 Pinia 功能
为 JS 开发者提供适当的 TypeScript 支持以及自动补全功能。
支持服务端渲染
c.Devtools 支持
追踪 actions、mutations 的时间线
在组件中展示它们所用到的 Store
让调试更容易的 Time travel
d.热更新
不必重载页面即可修改 Store
开发时可保持当前的 State
03.vuex、pinia对比
a.结论
Vuex 和 Pinia 一样,天生就是用来做"跨组件"和 SPA 内的"跨页面"状态共享的。这是它们共同的核心价值。
b.Pinia
API 层面:Pinia 废除了 Mutations,并通过扁平化的 Store 设计取代了 Vuex 的 modules,代码更简洁、直观
类型安全:Pinia 提供开箱即用的完美 TypeScript 支持,解决了 Vuex 的最大痛点
开发范式:Pinia 深度拥抱 Vue 3 的组合式 API,在 <script setup> 中的开发体验远超 Vuex
轻量与高效:Pinia 的体积更小,性能也更好
c.跨组件、跨页面
在应用内部,通过路由 (Vue Router) 从一个视图切换到另一个视图(例如,从 /home 页面跳转到 /profile 页面),
应用本身并未重新加载,只是组件在不断地被挂载和卸载。
-----------------------------------------------------------------------------------------------------
核心机制:
无论是 Vuex 还是 Pinia,它们都会在你的 Vue 应用根实例上创建一个全局唯一的、响应式的 Store 实例。
全局单例:这个 Store 实例独立于任何组件之外,存活于整个应用的生命周期中。
组件通信:任何组件都可以读取这个 Store 中的状态。当一个组件修改了 Store 中的状态时,所有依赖这个状态的其他组件都会自动接收到更新。
路由切换:当你从 /home 切换到 /profile 时,Home 组件被卸载,Profile 组件被挂载。但那个全局的 Store 实例始终存在,没有被销毁。因此,Profile 组件可以无缝地访问到之前 Home 组件可能设置或修改过的状态。
d.对比 Vuex 3.x/4.x
Vuex 3.x 只适配 Vue 2,而 Vuex 4.x 是适配 Vue 3 的。
-----------------------------------------------------------------------------------------------------
Pinia API 与 Vuex(<=4) 也有很多不同,即:
mutation 已被弃用。它们经常被认为是极其冗余的。它们初衷是带来 devtools 的集成方案,但这已不再是一个问题了。
无需要创建自定义的复杂包装器来支持 TypeScript,一切都可标注类型,API 的设计方式是尽可能地利用 TS 类型推理。
无过多的魔法字符串注入,只需要导入函数并调用它们,然后享受自动补全的乐趣就好。
无需要动态添加 Store,它们默认都是动态的,甚至你可能都不会注意到这点。注意,你仍然可以在任何时候手动使用一个 Store 来注册它,但因为它是自动的,所以你不需要担心它。
不再有嵌套结构的模块。你仍然可以通过导入和使用另一个 Store 来隐含地嵌套 stores 空间。虽然 Pinia 从设计上提供的是一个扁平的结构,但仍然能够在 Store 之间进行交叉组合。你甚至可以让 Stores 有循环依赖关系。
不再有可命名的模块。考虑到 Store 的扁平架构,Store 的命名取决于它们的定义方式,你甚至可以说所有 Store 都应该命名。
2.2 [1]Store:定义
01.方式一:选项式
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
02.方式二:组合式
// src/stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, double, increment }
})
03.使用 Store
<script setup>
import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {
store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
04.从 Store 解构
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
2.3 [1]State:类似data
01.方式一:JS写法
// src/stores/exampleStore.js
import { defineStore } from 'pinia'
// 定义 store
export const useExampleStore = defineStore('exampleStore', {
state: () => ({
count: 0,
name: 'Eduardo',
isAdmin: true,
items: [],
hasChanged: false,
}),
})
02.方式二:TS写法
// src/stores/exampleStore.ts
// @ts-ignore
import { defineStore } from 'pinia'
interface UserInfo {
name: string
age: number
}
interface State {
userList: UserInfo[]
user: UserInfo | null
}
// 定义 store
export const useExampleStore = defineStore('exampleStore', {
state: (): State => ({
userList: [],
user: null,
}),
})
03.访问state
const store = useStore()
store.count++
默认情况下,你可以通过 store 实例访问 state,直接对其进行读写。
注意,新的属性如果没有在 state() 中被定义,则不能被添加。它必须包含初始状态。例如:如果 secondCount 没有在 state() 中定义,我们无法执行 store.secondCount = 2。
04.重置state
const store = useStore()
store.$reset()
05.变更state
方式一:
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})
方式二:
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
06.替换state
你不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是,你可以 patch 它。
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })
---------------------------------------------------------------------------------------------------------
你也可以通过变更 pinia 实例的 state 来设置整个应用的初始 state。这常用于 SSR 中的激活过程。
pinia.state.value = {}
07.订阅state
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
mutation.storeId // 'cart'
// 只有 mutation.type === 'patch object'的情况下才可用
mutation.payload // 传递给 cartStore.$patch() 的补丁对象。
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('cart', JSON.stringify(state))
})
---------------------------------------------------------------------------------------------------------
类似于 Vuex 的 subscribe 方法,你可以通过 store 的 $subscribe() 方法侦听 state 及其变化。
比起普通的 watch(),使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次 (例如,当使用上面的函数版本时)。
2.4 [1]Getter:类似computed
01.定义Getter
Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore() 中的 getters 属性来定义它们。
a.JS写法
推荐使用箭头函数,并且它将接收 state 作为第一个参数:
// src/stores/counterStore.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne: (state) => state.count * 2 + 1,
},
})
b.TS写法
// src/stores/counterStore.js
// @ts-ignore
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注
return this.doubleCount + 1
},
},
})
02.访问其他getter
与计算属性一样,你也可以组合多个 getter。通过 this,你可以访问到其他任何 getter。
在这种情况下,你需要为这个 getter 指定一个返回值的类型。
a.JS写法
// 你可以在 JavaScript 中使用 JSDoc (https://jsdoc.app/tags-returns.html)
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne() {
return this.doubleCount + 1
},
},
})
b.TS写法
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
},
})
03.向getter传递参数
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
---------------------------------------------------------------------------------------------------------
如果需要向 getter 传递参数,可以将 getter 设计为返回一个函数:
export const useUserListStore = defineStore('userList', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
---------------------------------------------------------------------------------------------------------
在组件中使用:
<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数
</script>
<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>
---------------------------------------------------------------------------------------------------------
请注意,当你这样做时,getter 将不再被缓存。它们只是一个被你调用的函数。
不过,你可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好
export const useUserListStore = defineStore('userList', {
getters: {
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active)
return (userId) => activeUsers.find((user) => user.id === userId)
},
},
})
04.访问其他 store 的 getter
如果需要在一个 store 的 getter 中访问另一个 store 的 getter,可以直接调用另一个 store
---------------------------------------------------------------------------------------------------------
// src/stores/otherStore.js
import { defineStore } from 'pinia'
export const useOtherStore = defineStore('other', {
state: () => ({
data: 42,
}),
})
---------------------------------------------------------------------------------------------------------
// src/stores/mainStore.js
import { defineStore } from 'pinia'
import { useOtherStore } from './otherStore'
export const useMainStore = defineStore('main', {
state: () => ({
localData: 10,
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
05.使用 setup() 时的用法
作为 store 的一个属性,你可以直接访问任何 getter(与 state 属性完全一样):
---------------------------------------------------------------------------------------------------------
<script setup>
const store = useCounterStore()
store.count = 3
store.doubleCount // 6
</script>
06.使用选项式 API 的用法
a.创建Stores
// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
},
})
b.使用 setup()
<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
quadrupleCounter() {
return this.counterStore.doubleCount * 2
},
},
})
</script>
c.不使用 setup()
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counter'
export default {
computed: {
// 允许在组件中访问 this.doubleCount
// 与从 store.doubleCount 中读取的相同
...mapState(useCounterStore, ['doubleCount']),
// 与上述相同,但将其注册为 this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'doubleCount',
// 你也可以写一个函数来获得对 store 的访问权
double: (store) => store.doubleCount,
}),
},
}
2.5 [1]Action:类似methods
01.定义Action
Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。
类似 getter,action 也可通过 this 访问整个 store 实例,并支持完整的类型标注(以及自动补全✨)。
不同的是,action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action!
a.定义
// src/stores/counterStore.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
async fetchData() {
try {
const response = await fetch('https://api.example.com/data')
const data = await response.json()
this.count = data.value
} catch (error) {
console.error('Failed to fetch data:', error)
}
},
},
})
b.使用:Action 可以像函数或者通常意义上的方法一样被调用
<!-- src/components/CounterComponent.vue -->
<template>
<div>
<p>Count: {{ store.count }}</p>
<button @click="increment">Increment</button>
<button @click="randomizeCounter">Randomize</button>
<button @click="fetchData">Fetch Data</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counterStore'
const store = useCounterStore()
// 调用 store 的 action
function increment() {
store.increment()
}
function randomizeCounter() {
store.randomizeCounter()
}
function fetchData() {
store.fetchData()
}
</script>
02.访问其他 store 的 action
如果一个 action 需要调用另一个 store 的 action,可以直接在 action 内部调用另一个 store
---------------------------------------------------------------------------------------------------------
// src/stores/authStore.js
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
isAuthenticated: false,
}),
actions: {
async login() {
// 登录逻辑
this.isAuthenticated = true
},
},
})
---------------------------------------------------------------------------------------------------------
// src/stores/settingsStore.js
import { defineStore } from 'pinia'
import { useAuthStore } from './authStore'
export const useSettingsStore = defineStore('settings', {
state: () => ({
preferences: null,
}),
actions: {
async fetchUserPreferences() {
const authStore = useAuthStore()
if (authStore.isAuthenticated) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
03.使用选项式 API 的用法
a.创建Stores
// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
},
})
b.使用 setup()
<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
methods: {
incrementAndPrint() {
this.counterStore.increment()
console.log('New Count:', this.counterStore.count)
},
},
})
</script>
c.不使用 setup()
import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'
export default {
methods: {
// 访问组件内的 this.increment()
// 与从 store.increment() 调用相同
...mapActions(useCounterStore, ['increment'])
// 与上述相同,但将其注册为this.myOwnName()
...mapActions(useCounterStore, { myOwnName: 'increment' }),
},
}
04.订阅 action
你可以通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。
after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。
同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。
这些函数对于追踪运行时错误非常有用,类似于Vue docs 中的这个提示。
// 在某个组件或文件中
import { useCounterStore } from './counterStore'
const counterStore = useCounterStore()
const unsubscribe = counterStore.$onAction(
({ name, store, args, after, onError }) => {
const startTime = Date.now()
console.log(`Start "${name}" with params [${args.join(', ')}].`)
after((result) => {
console.log(
`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`
)
})
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// 手动删除监听器
unsubscribe()
---------------------------------------------------------------------------------------------------------
在组件卸载后保留订阅
如果希望即使在组件卸载后仍保留订阅,可以将 true 作为第二个参数传递给 $onAction:
<script setup>
import { useCounterStore } from '@/stores/counterStore'
const counterStore = useCounterStore()
counterStore.$onAction((context) => {
// 处理 action 相关的逻辑
}, true) // 即使组件卸载后也保留订阅
</script>
2.6 [1]Plugin:插件
01.为 Store 添加新的属性
你可以通过插件为每个 store 添加新的属性。以下示例中,我们为每个 store 添加一个 secret 属性:
import { createPinia } from 'pinia'
// 定义插件
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// 使用插件
pinia.use(SecretPiniaPlugin)
// 在 store 中使用新增的属性
const store = useStore()
console.log(store.secret) // 'the cake is a lie'
02.扩展 Store 的 State 和 Methods
你可以通过插件直接在 store 上添加新的 state 或 methods:
import { ref } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(({ store }) => {
// 添加新的 state
if (!store.$state.hasOwnProperty('hasError')) {
store.$state.hasError = ref(false)
}
store.hasError = toRef(store.$state, 'hasError')
// 添加新的方法
store.resetError = () => {
store.hasError = false
}
})
const useErrorStore = defineStore('error', {
state: () => ({
hasError: false,
}),
actions: {
triggerError() {
this.hasError = true
}
}
})
const store = useErrorStore()
store.triggerError()
console.log(store.hasError) // true
store.resetError()
console.log(store.hasError) // false
03.包装现有的方法
你可以包装现有的 methods 以添加额外的功能,如防抖动:
import debounce from 'lodash/debounce'
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(({ options, store }) => {
if (options.debounce) {
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(store[action], options.debounce[action])
return debouncedActions
}, {})
}
})
const useSearchStore = defineStore('search', {
actions: {
searchContacts() {
// 执行搜索
}
},
debounce: {
searchContacts: 300,
}
})
const store = useSearchStore()
store.searchContacts() // 此方法会被防抖动处理
04.添加外部属性(如路由器)
将外部属性如路由器添加到 store 中:
import { markRaw } from 'vue'
import { createPinia } from 'pinia'
import { router } from './router' // 引入路由器实例
const pinia = createPinia()
pinia.use(({ store }) => {
store.router = markRaw(router)
})
const useStore = defineStore('store', {})
const store = useStore()
console.log(store.router) // 使用 router 实例
05.订阅 Store 的变更和 Actions
在插件中使用 store.$subscribe 和 store.$onAction 来监听 store 的变更:
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(({ store }) => {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
store.$onAction(({ name, after, onError }) => {
after((result) => {
console.log(`Action "${name}" resolved with ${result}`)
})
onError((error) => {
console.error(`Action "${name}" failed with ${error}`)
})
})
})
const useStore = defineStore('store', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
const store = useStore()
store.increment() // 触发监听器
06.在 Nuxt.js 中使用 Pinia 插件
在 Nuxt.js 中,你需要创建一个插件并将其注册到 Pinia:
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[ ${mutation.storeId}]: ${mutation.type}.`)
})
return { creationTime: new Date() }
}
const myPlugin: Plugin = ({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
}
export default myPlugin
2.7 [2]使用:选项式
01.state:类似data
a.说明
state 属性必须是一个**函数**,它返回 Store 的初始状态对象
b.代码
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state 必须是一个返回对象的函数,以防止在服务端渲染 (SSR) 场景下多实例共享状态。
state: () => ({
count: 0,
userName: 'Guest',
}),
})
c.为什么 `state` 必须是函数?
这与 Vue 组件中 `data` 必须是函数的原因相同。
如果 `state` 是一个普通对象,那么在应用的整个生命周期中,所有 Store 实例将共享同一个状态对象,
这在 SSR 场景下会导致交叉请求状态污染。函数形式确保了每个 Store 实例都拥有自己独立的状态副本。
02.getters:类似computed
a.说明
getters 用于派生计算状态,完全等同于 Vue 组件中的 `computed` 属性。
Getter 会接收 `state` 作为其第一个参数。
Getter 之间可以通过 `this` 互相调用。
所有 Getter 都会被缓存,只有当其依赖的状态发生变化时才会重新计算。
b.代码
getters: {
// 接收 state 作为第一个参数
doubleCount: (state) => state.count * 2,
// 也可以通过 this 访问 store 实例,从而调用其他 getter
doubleCountPlusOne() {
return this.doubleCount + 1
},
// 结合多个 state
welcomeMessage: (state) => `Welcome, ${state.userName}! Your score is ${state.count}.`,
},
03.action:类似methods
a.说明
actions 相当于组件中的 `methods`,用于定义业务逻辑和修改状态。
Actions 可以是异步的 (`async/await`)。
在 Action 内部,通过 `this` 关键字可以访问整个 Store 实例,从而读取 `state`、调用其他 `getters` 或 `actions`。
修改 `state` 时,可以直接通过 `this.propertyName = ...` 进行修改。对于批量修改,建议使用 `$patch` 方法以优化性能。
b.代码
actions: {
// 同步 action
increment() {
this.count++
},
// 异步 action
async fetchAndSetUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`)
const userData = await response.json()
// 使用 $patch 进行批量、高效的状态更新
this.$patch({
count: userData.score,
userName: userData.name,
})
} catch (error) {
console.error('Failed to fetch user:', error)
}
},
// 调用其他 action
async incrementAndFetch(userId) {
this.increment() // 调用同一个 store 的另一个 action
await this.fetchAndSetUser(userId)
}
}
04.完整示例
a.代码
// stores/counter.options.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counterOptions', {
state: () => ({
count: 0,
userName: 'Guest',
}),
getters: {
doubleCount: (state) => state.count * 2,
welcomeMessage: (state) => `Welcome, ${state.userName}!`,
},
actions: {
increment(amount = 1) {
this.count += amount
},
reset() {
this.count = 0
},
async fetchNewCount() {
const response = await new Promise(resolve => setTimeout(() => resolve({ newCount: 100 }), 1000));
this.count = response.newCount;
}
},
})
b.优点
结构清晰: state, getters, actions 的划分非常明确,强制代码组织
易于上手: 对于有 Vuex 或 Vue Options API 背景的开发者几乎没有学习成本
心智模型简单: 概念与 Vuex 高度一致
c.缺点
类型推断略显繁琐: 虽然 Pinia 对 TypeScript 支持很好,但在 Options Store 中,this 的类型有时需要手动注解才能获得完美的类型提示。
灵活性较低: 所有逻辑必须归类到三个属性中,不利于复杂逻辑的抽离和复用。
2.8 [2]使用:组合式
01.核心映射关系
a.说明
在 Setup Store 中,我们不再需要 `state`, `getters`, `actions` 这些固定的属性名,而是直接使用 Vue 的响应式 API 来构建。
b.state -> ref() & reactive()
a.说明
Store 的状态被定义为使用 `ref()`、`reactive()` 或 `shallowRef()` 创建的响应式变量。
b.示例
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
export const useUserStore = defineStore('userSetup', () => {
// ref() 用于定义原始类型或简单对象的响应式状态
const userId = ref(null)
const lastLogin = ref(new Date())
// reactive() 用于定义复杂对象的响应式状态
const profile = reactive({
name: 'Anonymous',
email: '',
isAdmin: false,
})
// ...
})
c.getters -> computed()
a.说明
派生状态(Getters)通过 Vue 的 `computed()` 函数创建。
b.示例
import { computed } from 'vue'
// computed() 创建一个只读的响应式引用,其行为与 getter 完全一致
const welcomeMessage = computed(() => `Hello, ${profile.name}!`)
const permissions = computed(() => {
return profile.isAdmin ? ['read', 'write', 'delete'] : ['read']
})
// ...
d.actions -> function()
a.说明
业务逻辑(Actions)就是简单的函数。这些函数可以直接访问和修改由 `ref` 或 `reactive` 定义的状态。
b.示例
// 普通函数作为 action
function logout() {
userId.value = null
profile.name = 'Anonymous'
profile.email = ''
profile.isAdmin = false
}
// 异步函数作为 action
async function login(email, password) {
try {
const userData = await api.login(email, password)
userId.value = userData.id
profile.name = userData.name
profile.email = userData.email
profile.isAdmin = userData.isAdmin
lastLogin.value = new Date()
return true
} catch (error) {
console.error('Login failed:', error)
logout() // 可以直接调用其他 action
return false
}
}
02.关键部分:return语句
a.说明
与组件的 setup 函数不同,Store 的 setup 函数必须返回一个对象,该对象包含了所有需要向外部暴露的状态、计算属性和方法。
b.代码
// 将所有需要暴露的 state, getters, actions 返回
return {
// State
userId,
lastLogin,
profile,
// Getters
welcomeMessage,
permissions,
// Actions
login,
logout,
}
03.高级用法:Store内的组合式能力
a.说明
Setup Store 最大的优势在于可以充分利用组合式 API 的所有能力。
b.使用watch()
a.说明
你可以在 Store 内部监听状态变化并执行副作用
b.代码
import { watch } from 'vue'
// 监听 profile.name 的变化,并将其持久化到 localStorage
watch(
() => profile.name,
(newName) => {
localStorage.setItem('userName', newName)
}
)
c.逻辑复用
a.说明
你可以将复杂的、可复用的逻辑抽离成独立的 composable 函数,并在多个 Store 或组件中使用
b.代码
// composables/useAuth.js
export function useAuth() {
// ... 复杂的认证逻辑
return { ... }
}
// stores/user.js
export const useUserStore = defineStore('user', () => {
// 在 store 内部使用 composable
const { user, login, logout } = useAuth()
// ...
return { user, login, logout }
})
04.优缺点分析
a.优点
与组合式 API 统一: 与 Vue 3 的 <script setup> 拥有完全一致的开发体验。
极致的类型推断: TypeScript 支持近乎完美,无需额外配置,自动推断所有返回值的类型。
高度灵活: 可以自由组织代码,使用 watch、provide/inject 以及外部 composable,轻松应对复杂场景。
更好的代码组织: 相关联的状态、计算属性和方法可以就近放置,而非强制分离。
b.缺点
结构约束较弱: 对于习惯了严格分层的开发者,可能会觉得过于自由。
需要理解响应式原理: 必须熟悉 ref, reactive, computed 等 Vue 核心响应式 API。
2.9 [3]各种环境使用
01.在 Vue 组件外使用 Pinia Store
即使在组件外,你仍然可以访问和操作 Pinia store。
首先,你需要在应用的入口文件中创建和配置 Pinia 实例,然后在其他 JavaScript 文件中使用它。
a.创建和配置 Pinia 实例
在你的应用入口文件(例如 main.js 或 main.ts)中,你应该创建 Pinia 实例并将其传递给 Vue 应用
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
b.在组件外使用 Store
在组件外使用 store,你需要先导入定义的 store,然后通过 useStore 函数来访问它。以下是一个示例:
a.stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
}
})
b.services/someService.js
import { useCounterStore } from '../stores/counter'
export function performAction() {
const counterStore = useCounterStore()
counterStore.increment()
console.log('Count after increment:', counterStore.count)
}
c.总结
在 someService.js 文件中,我们直接导入并使用了 useCounterStore。这样,即使在 Vue 组件之外,我们也能操作 store。
02.在 Vue 3 的 Composition API 中使用 Store
你可以在 Vue 3 的 Composition API 中使用 Pinia store,虽然这通常发生在组件内部,但也可以在其他 JS 文件中使用:
a.stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: 'John Doe'
}),
actions: {
setName(newName) {
this.name = newName
}
}
})
b.utils/userUtils.js
import { useUserStore } from '../stores/user'
export function updateUserName(newName) {
const userStore = useUserStore()
userStore.setName(newName)
console.log('User name updated:', userStore.name)
}
03.在非组件环境中使用 Store(例如在 Node.js 环境中)
在某些非浏览器环境(如 Node.js)中使用 Pinia,首先确保你的环境支持 ES 模块和 Pinia。以下是一个简单的示例:
// server.js
import { createPinia } from 'pinia'
import { useCounterStore } from './stores/counter'
// 创建 Pinia 实例
const pinia = createPinia()
// 使用 store
const counterStore = useCounterStore()
counterStore.increment()
console.log('Count:', counterStore.count)
04.在 Vue 3 中的异步操作中使用 Store
在处理异步操作时,你可能需要在 Promise 或异步函数中使用 store:
// api/userApi.js
import { useUserStore } from '../stores/user'
export async function fetchUserData() {
const userStore = useUserStore()
try {
const response = await fetch('/api/user')
const data = await response.json()
userStore.setName(data.name)
console.log('User name from API:', userStore.name)
} catch (error) {
console.error('Failed to fetch user data:', error)
}
}
05.使用 Store 进行测试
在测试环境中使用 Pinia store,可以模拟 store 状态或方法:
// tests/userStore.test.js
import { createPinia } from 'pinia'
import { useUserStore } from '../stores/user'
const pinia = createPinia()
const userStore = useUserStore()
userStore.setName('Test User')
test('User store name should be Test User', () => {
expect(userStore.name).toBe('Test User')
})
3 uniapp
3.1 nuve:原理
01.主要说明
a.说明1
uni-app App 端内置了一个基于 weex 改进的原生渲染引擎,提供了原生渲染能力。
在 App 端,如果使用 vue 页面,则使用 webview 渲染;如果使用 nvue 页面(native vue 的缩写),则使用原生渲染。
一个 App 中可以同时使用两种页面,比如首页使用 nvue,二级页使用 vue 页面,hello uni-app 示例就是如此。
b.说明2
虽然 nvue 也可以多端编译,输出 H5 和小程序,但 nvue 的 css 写法受限,所以如果你不开发 App,那么不需要使用 nvue。
以往的 weex ,有个很大的问题是它只是一个高性能的渲染器,没有足够的 API 能力(比如各种 push sdk 集成、蓝牙等能力调用),
使得开发时非常依赖原生工程师协作,开发者本来想节约成本,结果需要前端、iOS、Android 3 拨人开发,适得其反。
nvue 解决了这个问题,让前端工程师可以直接开发完整 App,并提供丰富的插件生态和云打包。这些组合方案,帮助开发者切实的提高效率、降低成本。
c.说明3
同时uni-app扩展了 weex 原生渲染引擎的很多排版能力,修复了很多 bug。比如
Android 端良好支持边框阴影,详情
iOS 端支持高斯模糊,详情
可实现区域滚动长列表+左右拖动列表+吸顶的复杂排版效果
优化圆角边框绘制性能
扩展了更多的 css
02.适用场景
a.说明1
nvue 的组件和 API 写法与 vue 页面一致,其内置组件还比 vue 页面内置组件增加了更多,详见。
如果你熟悉 weex 或 react native 开发,那么 nvue 是你的更优选择,能切实提升你的开发效率,降低成本。
如果你是 web 前端,不熟悉原生排版,那么建议你仍然以使用 vue 页面为主,
b.说明2
在 App 端某些 vue 页面表现不佳的场景下使用 nvue 作为强化补充。这些场景如下:
需要高性能的区域长列表或瀑布流滚动。webview 的页面级长列表滚动是没有性能问题的(就是滚动条覆盖 webview 整体高度),但页面中某个区域做长列表滚动,则需要使用 nvue 的list、recycle-list、waterfall等组件(详见)。这些组件的性能要高于 vue 页面里的区域滚动组件scroll-view。
复杂高性能的自定义下拉刷新。uni-app 的 pages.json 里可以配置原生下拉刷新,但引擎内置的下拉刷新样式只有雪花和 circle 圈 2 种样式。如果你需要自己做复杂的下拉刷新,推荐使用 nvue 的 refresh 组件。当然插件市场里也有很多 vue 下的自定义下拉刷新插件,只要是基于 renderjs 或 wxs 的,性能也可以商用,只是没有 nvue 的refresh组件更极致。
左右拖动的长列表。在 webview 里,通过swiper+scroll-view实现左右拖动的长列表,前端模拟下拉刷新,这套方案的性能不好。此时推荐使用 nvue,比如新建 uni-app 项目时的新闻示例模板,就采用了 nvue,切换很流畅。
实现区域滚动长列表+左右拖动列表+吸顶的复杂排版效果,效果可参考 hello uni-app 模板里的swiper-list
03.纯原生渲染模式
a.说明
uni-app 在 App 端,支持 vue 页面和 nvue 页面混搭、互相跳转。也支持纯 nvue 原生渲染。
启用纯原生渲染模式,可以减少 App 端的包体积、减少使用时的内存占用。因为 webview 渲染模式的相关模块将被移除。
在 manifest.json 源码视图的"app-plus"下配置"renderer":"native",即代表 App 端启用纯原生渲染模式。
此时 pages.json 注册的 vue 页面将被忽略,vue 组件也将被原生渲染引擎来渲染。
b.如果不指定该值,默认是不启动纯原生渲染的。
// manifest.json
{
// ...
// App平台特有配置
"app-plus": {
"renderer": "native", //App端纯原生渲染模式
}
}
04.编译模式
a.说明
weex 编译模式和 uni-app 编译模式
如你之前是 weex 开发者,可以继续查阅本章节,否则可以跳过看下一节快速上手。
weex 的组件和 JS API,与 uni-app 不同。uni-app 与微信小程序相同。
考虑到 weex 用户的迁移,uni-app 也支持 weex 的代码写法。在 manifest.json 中可以配置使用weex 编译模式或uni-app 编译模式。
选择 weex 编译模式时将不支持 uni-app 的组件和 jsapi,需要开发者参考 weex 官方文档的写法来写代码。
比如 weex 编译模式用<div>。uni-app 编译模式则使用<view>。
一般情况建议使用uni-app模式,除非历史 weex 代码较多,需要逐步过渡。
同时注意 weex 编译模式的切换是项目级的,不支持同项目下某个 nvue 页面使用 weex 模式,另一个 nvue 页面使用 uni-app 模式。
b.对比
weex 编译模式 uni-app 编译模式
平台 仅 App 所有端,包含小程序和 H5
组件 weex 组件如 div uni-app 组件如 view
生命周期 只支持 weex 生命周期 支持所有 uni-app 生命周期
JS API weex API、uni API、Plus API weex API、uni API、Plus API
单位 750px 是屏幕宽度,wx 是固定像素单位 750rpx 是屏幕宽度,px 是固定像素单位
全局样式 手动引入 app.vue 的样式即为全局样式
页面滚动 必须给页面套或组件 默认支持页面滚动
c.在 manifest.json 中修改 2 种编译模式,manifest.json -> app-plus -> nvueCompiler 切换编译模式。
nvueCompiler 有两个值:
weex
uni-app
-----------------------------------------------------------------------------------------------------
// manifest.json
{
// ...
// App平台特有配置
"app-plus": {
"nvueCompiler":"uni-app" //是否启用 uni-app 模式
}
}
-----------------------------------------------------------------------------------------------------
在 manifest.json 配置文件中,HBuilderX2.4 之前版本,默认值为 weex 模式,HBuilderX2.4+版本默认值改为 uni-app 模式。
3.2 nuve:页面
01.新建 nvue 页面
在 HBuilderX 的 uni-app 项目中,新建页面,弹出界面右上角可以选择是建立vue页面还是nvue页面,或者 2 个同时建。
不管是 vue 页面还是 nvue 页面,都需要在pages.json中注册。如果在 HBuilderX 中新建页面是会自动注册的,如果使用其他编辑器,则需要自行在 pages.json 里注册。
如果一个页面路由下同时有 vue 页面和 nvue 页面,即出现同名的 vue 和 nvue 文件。那么在 App 端,会仅使用 nvue 页面,同名的 vue 文件将不会被编译到 App 端。而在非 App 端,会优先使用 vue 页面。
如果不同名,只有 nvue 页面,则在非 app 端,只有 uni-app 编译模式的 nvue 文件才会编译。
02.开发 nvue 页面
a.说明
nvue 页面结构同 vue, 由 template、style、script 构成。
b.template:模板写法、数据绑定同 vue。组件支持 2 种模式,
weex 组件,同 weex 写法,参考:weex 内置组件;
uni-app 组件,同 uni-app 写法。
App 端 nvue 专用组件,详见https://uniapp.dcloud.io/component/barcode。
c.style
由于采用原生渲染,并非所有浏览器的 css 均支持,布局模型只支持 flex 布局,虽然不会造成某些界面布局无法实现,但写法要注意。详见:样式
d.script:写法同 vue,并支持 3 种 API:
nvue API :仅支持 App 端,uni-app 编译模式也可使用。使用前需先引入对应模块,参考:模块引入 API
uni API:https://uniapp.dcloud.io/api/README
plus API:仅支持 App 端。http://www.html5plus.org/doc/h5p.html
03.调试 nvue 页面
a.说明
HBuilderX 内置了 weex 调试工具的强化版,包括审查界面元素、看 log、debug 打断点
3.3 nuve:对比vue
00.汇总
a.分类1
01.nvue 页面控制显隐只可以使用v-if不可以使用v-show
02.nvue 页面只能使用flex布局,不支持其他布局方式。页面开发前,首先想清楚这个页面的纵向内容有什么,哪些是要滚动的,然后每个纵向内容的横轴排布有什么,按 flex 布局设计好界面。
03.nvue 页面的布局排列方向默认为竖排(column),如需改变布局方向,可以在 manifest.json -> app-plus -> nvue -> flex-direction 节点下修改,仅在 uni-app 模式下生效。详情。
04.nvue页面编译为H5、小程序时,会做一件css默认值对齐的工作。因为weex渲染引擎只支持flex,并且默认flex方向是垂直。而H5和小程序端,使用web渲染,默认不是flex,并且设置display:flex后,它的flex方向默认是水平而不是垂直的。所以nvue编译为H5、小程序时,会自动把页面默认布局设为flex、方向为垂直。当然开发者手动设置后会覆盖默认设置。
05.文字内容,必须、只能在<text>组件下。不能在<div>、<view>的text区域里直接写文字。否则即使渲染了,也无法绑定js里的变量。
06.只有text标签可以设置字体大小,字体颜色。
07.布局不能使用百分比、没有媒体查询。
08.nvue 切换横竖屏时可能导致样式出现问题,建议有 nvue 的页面锁定手机方向。
09.支持的css有限,不过并不影响布局出你需要的界面,flex还是非常强大的。详见
10.不支持背景图。但可以使用image组件和层级来实现类似web中的背景效果。因为原生开发本身也没有web这种背景图概念
b.分类2
11.css选择器支持的比较少,只能使用 class 选择器。详见
12.nvue 的各组件在安卓端默认是透明的,如果不设置background-color,可能会导致出现重影的问题。
13.class 进行绑定时只支持数组语法。
14.Android端在一个页面内使用大量圆角边框会造成性能问题,尤其是多个角的样式还不一样的话更耗费性能。应避免这类使用。
15.nvue页面没有bounce回弹效果,只有几个列表组件有bounce效果,包括 list、recycle-list、waterfall。
16.原生开发没有页面滚动的概念,页面内容高过屏幕高度并不会自动滚动,只有部分组件可滚动(list、waterfall、scroll-view/scroller),要滚的内容需要套在可滚动组件下。这不符合前端开发的习惯,所以在 nvue 编译为 uni-app模式时,给页面外层自动套了一个 scroller,页面内容过高会自动滚动。(组件不会套,页面有recycle-list时也不会套)。后续会提供配置,可以设置不自动套。
17.在 App.vue 中定义的全局js变量不会在 nvue 页面生效。globalData和vuex是生效的。
18.App.vue 中定义的全局css,对nvue和vue页面同时生效。如果全局css中有些css在nvue下不支持,编译时控制台会报警,建议把这些不支持的css包裹在条件编译里,APP-PLUS-NVUE
19.不能在 style 中引入字体文件,nvue 中字体图标的使用参考:加载自定义字体。如果是本地字体,可以用plus.io的API转换路径。
20.目前不支持在 nvue 页面使用 typescript/ts。
21.nvue 页面关闭原生导航栏时,想要模拟状态栏,可以参考文章。但是,仍然强烈建议在nvue页面使用原生导航栏。nvue的渲染速度再快,也没有原生导航栏快。原生排版引擎解析json绘制原生导航栏耗时很少,而解析nvue的js绘制整个页面的耗时要大的多,尤其在新页面进入动画期间,对于复杂页面,没有原生导航栏会在动画期间产生整个屏幕的白屏或闪屏。
01.显隐控制方式差异
a.nvue
- 仅支持 `v-if`。
<view v-if="showContent">显示内容</view>
<text v-if="isLogin">已登录</text>
<text v-if="!isLogin">未登录</text>
b.vue
- 支持 `v-if` 和 `v-show`。
<view v-show="showContent">显示内容</view>
<text v-if="isLogin">已登录</text>
<text v-show="!isLogin">未登录</text>
02.布局方式差异
a.nvue
- 仅支持 flex 布局。
<view class="container">
<view class="item">项目1</view>
<view class="item">项目2</view>
</view>
<style>
.container {
display: flex;
flex-direction: row;
}
.item {
flex: 1;
}
</style>
b.vue
- 支持多种布局方式(如浮动布局、网格布局)。
<view class="float-container">
<view class="float-left">左浮动</view>
<view class="float-right">右浮动</view>
</view>
<style>
.float-left { float: left; }
.float-right { float: right; }
</style>
03.文字内容容器差异
a.nvue
- 文字必须在 `<text>` 组件内。
<view>
<text>这是一段文字</text>
</view>
b.vue
- 文字可直接在容器内。
<view>这是一段文字</view>
04.字体样式设置差异
a.nvue
- 仅 `<text>` 可设置字体样式。
<text style="font-size: 16px; color: #ff0000;">红色16号文字</text>
b.vue
- 任意容器可设置字体样式。
<view style="font-size: 16px; color: #ff0000;">红色16号文字</view>
05.背景图实现差异
a.nvue
- 不支持背景图,需用 `<image>` 模拟。
<view class="bg-container">
<image src="/static/bg.png" class="bg-image"></image>
<text class="bg-text">文字内容</text>
</view>
<style>
.bg-container {
position: relative;
width: 100%;
height: 200px;
}
.bg-image {
width: 100%;
height: 100%;
position: absolute;
}
.bg-text {
position: absolute;
color: white;
}
</style>
b.vue
- 支持 `background-image`。
<view style="background-image: url('/static/bg.png'); height: 200px;">
<text style="color: white;">文字内容</text>
</view>
06.CSS选择器支持差异
a.nvue
- 仅支持 class 选择器。
<view class="container">
<text class="text-primary">主文字</text>
</view>
<style>
.container { padding: 10px; }
.text-primary { color: #007aff; }
</style>
b.vue
- 支持多种选择器(如 id、伪类选择器)。
<view id="container">
<text class="text-primary">主文字</text>
</view>
<style>
#container { padding: 10px; }
.text-primary { color: #007aff; }
</style>
07.class绑定语法差异
a.nvue
- 仅支持数组语法。
<view :class="[isActive ? 'active' : '']">内容</view>
<style>
.active { background-color: #eee; }
</style>
b.vue
- 支持多种绑定语法(如对象语法)。
<text :class="{ success: type === 'success', error: type === 'error' }">状态文字</text>
<style>
.success { color: green; }
.error { color: red; }
</style>
08.页面滚动实现差异
a.nvue
- 需用可滚动组件包裹(如 `scroll-view` 或 `list`)。
<scroll-view class="scroll-container" scroll-y>
<view v-for="i in 20" :key="i" class="item">{{ i }}</view>
</scroll-view>
<style>
.scroll-container {
flex: 1;
height: 100%;
}
.item {
height: 50px;
margin: 5px;
background-color: #eee;
}
</style>
b.vue
- 内容超出自动滚动。
<view class="container">
<view v-for="i in 20" :key="i" class="item">{{ i }}</view>
</view>
<style>
.item {
height: 50px;
margin: 5px;
background-color: #eee;
}
</style>
09.全局JS变量生效差异
a.nvue
- 不支持 App.vue 的全局变量,需用 globalData 或 vuex。
<text>{{ getApp().globalData.userName }}</text>
<script>
App({
globalData: {
userName: '张三'
}
})
</script>
---
b.vue
- 支持 App.vue 的全局变量。
<text>{{ globalVar.appName }}</text>
<script>
export default {
onLaunch() {
Vue.prototype.globalVar = {
appName: '我的应用'
}
}
}
</script>
10.字体文件引入差异
a.nvue
- 需通过 plus.io 转换路径。
<text class="custom-font">自定义字体</text>
<script>
export default {
onLoad() {
const fontPath = plus.io.convertLocalFileSystemURL('/static/fonts/custom.ttf')
const domModule = weex.requireModule('dom')
domModule.addRule('fontFace', {
'fontFamily': 'CustomFont',
'src': `url('${fontPath}')`
})
}
}
</script>
<style>
.custom-font {
fontFamily: 'CustomFont';
font-size: 18px;
}
</style>
b.vue
- 可直接在 style 中引入。
<text class="custom-font">自定义字体</text>
<style>
@font-face {
font-family: 'CustomFont';
src: url('/static/fonts/custom.ttf');
}
.custom-font {
font-family: 'CustomFont';
font-size: 18px;
}
</style>