vue3的学习笔记:MVC、Vue3概要、模板、数据绑定、用Vue3 + element ui、react框架实现购物车案例
一、前端MVC概要
1.1、库与框架的区别
框架是一个软件的半成品,在全局范围内给了大的约束。库是工具,在单点上给我们提供功能。框架是依赖库的。Vue是框架而jQuery则是库。
1.2、MVC(Model View Controller)
1.2.1、MVC 是什么?
MVC是模型(model)-视图(view)-控制器(controller)的缩写,是一种软件设计典范。它是用一种业务逻辑、数据与界面显示分离的方法来组织代码,将众多的业务逻辑聚集到一个部件里面,在需要改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑,达到减少编码的时间。
1.2.2、MVC的核心理念是?
把管理数据的代码(Model)、业务逻辑的代码(Controller)、以及向用户展示数据的代码(View)清晰的分离开
-
模型:代表应用当前的状态
-
视图:用于展示数据,用于接口
-
控制器:用来管理模型和视图之间的关系
典型思路是 View 层通过事件通知到 Controller 层,Controller 层经过对事件的处理完成相关业务逻辑,要求 Model 层改变数据状态,Model 层再将新数据更新到 View层。
1.2.3、MVC 的缺点
View 层和 Model 层相互持有、相互操作,导致紧密耦合,在可维护性上有待提升。由此,MVP 模式应运而生
通过MVC框架又衍生出了许多其它的架构,统称MV*,最常见的是MVP与MVVM
1.3、MVP (Model View Presenter)
MVP 模式将程序分为三个部分:模型(Model)、视图(View)、管理层(Presenter)。
Model 模型层: 只负责存储数据,与 View 呈现无关,也与 UI 处理逻辑无关,发生更新也不用主动通知 View;
View 视图层: 人机交互接口,一般为展示给用户的界面;
Presenter 管理层 : 负责连接 Model 层和 View 层,处理 View 层的事件,负责获取数据并将获取的数据经过处理后更新 View;
MVP 模式的目的就是:将 View 层和 Model 层完全解耦,使得对 View 层的修改不会影响到 Model 层,而对 Model 层的数据改动也不会影响到View 层。
典型流程是 :View 层触发的事件传递到 Presenter 层中处理,Presenter 层去操作 Model 层,并且将数据返回给 View层,这个过程中,View 层和 Model 层没有直接联系。而 View 层不部署业务逻辑,除了展示数据和触发事件之外,其它时间都在等着 Presenter 层来更新自己,被称为「被动视图」。
MVP 缺点:由于 Presenter 层负责了数据获取、数据处理、交互逻辑、UI 效果等等功能,所以 Presenter 层就变得强大起来,相应的,Model 层只负责数据存储,而 View 层只负责视图,Model 和 View 层的责任纯粹而单一,如果我们需要添加或修改功能模块,只需要修改 Presenter 层就够了。由于 Presenter 层需要调用 View 层的方法更新视图,Presenter 层直接持有 View 层导致了 Presenter 对 View 的依赖。
正如上所说,更新视图需要 Presenter 层直接持有 View 层,并通过调用 View 层中的方法来实现,还是需要一系列复杂操作,有没有什么机制自动去更新视图而不用我们手动去更新呢,所以,MVVM 模式应运而生。
MVP的主要特点:
-
把Activity里的许多逻辑都抽离到View和Presenter接口中去,并由具体的实现类来完成。
-
View与Model不直接交互,而是通过与Presenter来完成交互
-
Presenter中除了应用逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,会导致Presenter臃肿,维护困难
1.4、MVVM (Model View ViewModel)
1.4.1、介绍
MVVM 模式将程序分为三个部分:模型(Model)、视图(View)、视图模型(View-Model)。
1.4.2、为什么要用 MVVP ?
在 MVP 中,View 和 Presenter 是直接交互的,View 的状态变化,都是通过 Presenter 直接改变和传递。 这会有几个问题:
-
View 和 Presenter 的强耦合: View 和 Presenter 直接调用,不相互独立
-
无法跟踪视图的状态变化。对于视图中发生的不同状态变化,没有可追溯性
-
可测试性不足,特别是 MVVM,只能通过观察属性的变化来进行测试
-
多线程环境中可能会有状态冲突的问题
- Vue 与 Angular 就是一个 MVVM 框架,MVVM 与 MVC 最大的区别是模型与视图实现了双向绑定。
1.4.3、MVVM的特点:
-
可以将 ViewModel 看作是 Model 和 View 的连接桥梁,View 可以通过事件绑定 Model,Model 可以通过数据绑定 View,通过 ViewMode 可以实现数据和视图的完全分离。
-
在 MVVM 设计模式中, Model 层负责存储数据,View 层用于显示数据。但 MVVM 设计模式中,没有 Presenter 层,取而代之的是ViewModel层级。
-
而 ViewModel 并不需要我们来 进行编写,使用 MVVM 设计模式进行编码的时候,无需关注 ViewModel 这一层是如何实现的,它完全是 Vue 内置的。而我们只需要更多的关注 M 层与 V层,即模型层和视图层。
-
在Vue框架下,我们可以直接操作数据而不是 dom 元素来实现 View 的改变
1.5、前端使用 MVVM 的框架有什么?
1.5.1、React
React官网地址: http://facebook.github.io/react
Github地址: https://github.com/facebook/react
React是用于构建用户界面的JavaScript库, 起源于Facebook的内部项目,该公司对市场上所有 JavaScript MVC框架都不满意,决定自行开发一套,用于架设 Instagram 的网站
框架用途:React主要用于构建UI。你可以在React里传递多种类型的参数,如声明代码,帮助你渲染出UI、也可以是静态的HTML、DOM元素、也可以传递动态变量、甚至是可交互的应用组件。
框架特点:
-
声明式设计:React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React能高效更新并渲染合适的组件。
-
组件化: 构建管理自身状态的封装组件,然后对其组合以构成复杂的 UI。
-
高效:React通过对DOM的模拟,最大限度地减少与DOM的交互。
-
灵活:无论你现在使用什么技术栈,在无需重写现有代码的前提下,通过引入React来开发新功能。
1.5.2、 AngularJS(字面意思是:有角的; 用角测量的)
AngularJS 是一个 JavaScript 框架。它可通过 <script> 标签添加到 HTML 页面。AngularJS通过指令扩展了HTML,并且通过表达式绑定数据到 HTML。
优点:
-
AngularJS模板功能强大丰富,自带了极其丰富的angular指令。
-
AngularJS是完全可扩展的,与其他库的兼容效果很好,每一个功能可以修改或更换,以满足开发者独特的开发流程和功能的需求。
-
AngularJS是一个比较完善的前端MVC框架,包含服务,模板,数据双向绑定,模块化,路由,过滤器,依赖注入等所有功能;
-
AngularJS是互联网巨人谷歌开发,这也意味着他有一个坚实的基础和社区支持。
缺点:
-
AngularJS强约束导致学习成本较高,对前端不友好。但遵守 AngularJS 的约定时,生产力会很高,对 Java 程序员友好。
-
AngularJS不利于SEO,因为所有内容都是动态获取并渲染生成的,搜索引擎没法爬取。
1.5.3、Vue.js
Vue.js 是一个轻巧、高性能、可组件化的MVVM库,是一套构建用户界面的渐进式框架,同时拥有非常容易上手的API,作者是尤雨溪(中国人)、
文档与资源大全:https://vue3js.cn/
易学易用
基于标准 HTML、CSS 和 JavaScript 构建,提供容易上手的 API 和一流的文档。
性能出色
经过编译器优化、完全响应式的渲染系统,几乎不需要手动优化。
灵活多变
丰富的、可渐进式集成的生态系统,可以根据应用规模在库和框架间切换自如。
1.5.4、当前三大前端MVC框架的对比
二、Vue3 简介
2.1、vue3 的介绍
Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。
Vue3 中文文档 :https://vue3js.cn/docs/zh/
Vue3 设计理念 :https://vue3js.cn/vue-composition/ 破坏性语法更新
官网 : https://vuejs.org/ https://cn.vuejs.org/
2.2、Vue2 与 Vue3 的区别
2.2.1、生命周期
// vue3 <script setup> import { onMounted } from 'vue'; // 使用前需引入生命周期钩子 onMounted(() => { // ... }); // 可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不会被覆盖 onMounted(() => { // ... }); </script> // vue2 <script> export default { mounted() { // 直接调用生命周期钩子 // ... }, } </script>
注意: setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地去定义。
2.2.2、多根节点
// vue2中在template里存在多个根节点会报错 <template> <header></header> <main></main> <footer></footer> </template> // 只能存在一个根节点,需要用一个<div>来包裹着 <template> <div> <header></header> <main></main> <footer></footer> </div> </template> // Vue3 支持多个根节点,也就是 fragment <template> <header></header> <main></main> <footer></footer> </template>
2.2.3、Composition API
Vue2 是选项API(Options API)一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。
Vue3 是组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案。
2.2.3、异步组件(Suspense)
<tempalte> <suspense> <template #default> <List /> </template> <template #fallback> <div> Loading... </div> </template> </suspense> </template>
在 List 组件(有可能是异步组件,也有可能是组件内部处理逻辑或查找操作过多导致加载过慢等)未加载完成前,显示 Loading...(即 fallback 插槽内容),加载完成时显示自身(即 default 插槽内容)
2.2.4、Teleport(传送门)
Vue3 提供 Teleport 组件可将部分 DOM 移动到 Vue app 之外的位置。比如项目中常见的 Dialog 弹窗。
<button @click="dialogVisible = true">显示弹窗</button> <teleport to="body"> <div class="dialog" v-if="dialogVisible"> 我是弹窗,我直接移动到了body标签下 </div> </teleport>
2.2.5、响应式原理
Vue2 响应式原理基础是 Object.defineProperty;Vue3 响应式原理基础是 Proxy
- Object.defineProperty 基本用法:
-
直接在一个对象上定义新的属性或修改现有的属性,并返回对象。
-
进行数据劫持,即重写getter和setter,当数据改变的时候通知订阅者去改变。
-
- Proxy Proxy 是 ES6 新特性,通过第2个参数 handler 拦截目标对象的行为。相较于 Object.defineProperty 提供语言全范围的响应能力,消除了局限性。
-
局限性:
(1)、对象/数组的新增、删除
(2)、监测 .length 修改
(3)、Map、Set、WeakMap、WeakSet 的支持
-
☺Object.defineProperty 栗子如下:
let obj = {}; let name = 'leo'; Object.defineProperty(obj, 'name', { enumerable: true, // 可枚举(是否可通过 for...in 或 Object.keys() 进行访问) configurable: true, // 可配置(是否可使用 delete 删除,是否可再次设置属性) // value: '', // 任意类型的值,默认undefined // writable: true, // 可重写 get() { return name; }, set(value) { name = value; } });
注意: writable
和 value
与 getter
和 setter
不共存。
function defineReactive(obj, key, val) { // 一 key 一个 dep const dep = new Dep() // 获取 key 的属性描述符,发现它是不可配置对象的话直接 return const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 获取 getter 和 setter,并获取 val 值 const getter = property && property.get const setter = property && property.set if((!getter || setter) && arguments.length === 2) { val = obj[key] } // 递归处理,保证对象中所有 key 被观察 let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, // get 劫持 obj[key] 的 进行依赖收集 get: function reactiveGetter() { const value = getter ? getter.call(obj) : val if(Dep.target) { // 依赖收集 dep.depend() if(childOb) { // 针对嵌套对象,依赖收集 childOb.dep.depend() // 触发数组响应式 if(Array.isArray(value)) { dependArray(value) } } } } return value }) // set 派发更新 obj[key] set: function reactiveSetter(newVal) { ... if(setter) { setter.call(obj, newVal) } else { val = newVal } // 新值设置响应式 childOb = observe(val) // 依赖通知更新 dep.notify() } }
这个方法的缺点是:无法监听对象或数组新增、删除的元素。
解决方法:针对常用数组原型方法 push、pop、shift、unshift、splice、sort、reverse 进行了 hack 处理;提供 Vue.set 监听对象/数组新增属性。对象的新增/删除响应,还可以 new 个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象。
☺ Proxy Proxy 基本用法:创建对象的代理,从而实现基本操作的拦截和自定义操作。栗子如下:
let handler = { get(obj, prop) { return prop in obj ? obj[prop] : ''; }, set() { // ... }, ... }; function createReactiveObject(target, isReadOnly, baseHandlers, collectionHandlers, proxyMap) { ... // collectionHandlers: 处理Map、Set、WeakMap、WeakSet // baseHandlers: 处理数组、对象 const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy }
2.2.6、v-if 和 v-for的优先级
vue2:
我们最好不要把 v-if 和 v-for 同时用在一个元素上,这样会带来性能的浪费(每次都要先渲染才会进行条件判断)
//v-for 优先于 v-if 生效 <div v-if="index == 1" v-for="(item,index) in arr" :key="index">{{item}}</div>
vue3:
//v-if 优先于 v-for 生效 <div v-if="index == 1" v-for="(item,index) in arr" :key="index">{{item}}</div>
以上的写法,vue 中会给我们报警告:
意思就是:属性 “index” 在渲染期间被访问,但未在实例上定义(v-if 先进行判断,但是这时候v-for还没有渲染,所以index是找不到的)
2.2.7、事件缓存
Vue3 的 cacheHandler
可在第一次渲染后缓存我们的事件。相比于 Vue2 无需每次渲染都传递一个新函数。加一个 click 事件。
<div id="app"> <h1>vue3事件缓存讲解</h1> <p>今天天气真不错</p> <div>{{name}}</div> <span onCLick=() => {}><span> </div>
渲染函数如下所示。
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n) const _hoisted_1 = { id: app } const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, vue3事件缓存讲解, -1 /* HOISTED */)) const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天气真不错, -1 /* HOISTED */)) const _hoisted_4 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(span, { onCLick: () => {} }, [ /*#__PURE__*/_createElementVNode(span) ], -1 /* HOISTED */)) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(div, _hoisted_1, [ _hoisted_2, _hoisted_3, _createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */), _hoisted_4 ])) }
注意:观察以上渲染函数,你会发现 click 事件节点为静态节点(HOISTED 为 -1),即不需要每次重新渲染。
2.2.8、Diff 算法优化
搬运 Vue3 patchChildren 源码。patchFlag 帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对。function patchChildren(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) { // 获取新老孩子节点 const c1 = n1 && n1.children const c2 = n2.children const prevShapeFlag = n1 ? n1.shapeFlag : 0 const { patchFlag, shapeFlag } = n2 // 处理 patchFlag 大于 0 if(patchFlag > 0) { if(patchFlag && PatchFlags.KEYED_FRAGMENT) { // 存在 key patchKeyedChildren() return } els if(patchFlag && PatchFlags.UNKEYED_FRAGMENT) { // 不存在 key patchUnkeyedChildren() return } } // 匹配是文本节点(静态):移除老节点,设置文本节点 if(shapeFlag && ShapeFlags.TEXT_CHILDREN) { if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { unmountChildren(c1 as VNode[], parentComponent, parentSuspense) } if (c2 !== c1) { hostSetElementText(container, c2 as string) } } else { // 匹配新老 Vnode 是数组,则全量比较;否则移除当前所有的节点 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense,...) } else { unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true) } } else { if(prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(container, '') } if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(c2 as VNodeArrayChildren, container,anchor,parentComponent,...) } } } }
patchUnkeyedChildren 源码如下所示。
function patchUnkeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) { c1 = c1 || EMPTY_ARR c2 = c2 || EMPTY_ARR const oldLength = c1.length const newLength = c2.length const commonLength = Math.min(oldLength, newLength) let i for(i = 0; i < commonLength; i++) { // 如果新 Vnode 已经挂载,则直接 clone 一份,否则新建一个节点 const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as Vnode)) : normalizeVnode(c2[i]) patch() } if(oldLength > newLength) { // 移除多余的节点 unmountedChildren() } else { // 创建新的节点 mountChildren() } }
2.2.9、打包优化
Tree-shaking:模块打包 webpack、rollup 等中的概念。移除 JavaScript 上下文中未引用的代码。主要依赖于 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。
以 nextTick 为例子,在 Vue2 中,全局API暴露在Vue实例上,即使未使用,也无法通过 tree-shaking 进行消除。
import Vue from 'vue'; Vue.nextTick(() => { // 一些和DOM有关的东西 });
Vue3 中针对全局和内部的API进行了重构,并考虑到 tree-shaking 的支持。因此,全局API现在只能作为ES模块构建的命名导出进行访问。
import { nextTick } from 'vue'; // 显式导入 nextTick(() => { // 一些和DOM有关的东西 });
通过这一更改,只要模块绑定器支持 tree-shaking,则Vue应用程序中未使用的 api 将从最终的捆绑包中消除,获得最佳文件大小。
受此更改影响的全局API如下所示。
- Vue.nextTick
- Vue.observable (用 Vue.reactive 替换)
- Vue.version
- Vue.compile (仅全构建)
- Vue.set (仅兼容构建)
- Vue.delete (仅兼容构建)
内部API也有诸如 transition、v-model 等标签或者指令被命名导出。只有在程序真正使用才会被捆绑打包。Vue3 将所有运行功能打包也只有约22.5kb,比 Vue2 轻量很多。
2.2.10、TypeScript支持
Vue3 由 TypeScript 重写,相对于 Vue2 有更好的 TypeScript 支持。
-
Vue2 Options API 中 option 是个简单对象,而 TypeScript 是一种类型系统,面向对象的语法,不是特别匹配。
-
Vue2 需要 vue-class-component 强化 vue 原生组件,也需要 vue-property-decorator 增加更多结合Vue特性的装饰器,写法比较繁琐。
2.2.11、移除了某些元素
-
实例方法 $on 移除 (eventBus现有实现模式不再支持 可以使用三方插件替代)
-
过滤器 filter 移除 (插值表达式里不能再使用过滤器 可以使用methods替代)
-
sync 语法移除 (和v-model语法合并)
2.3、Vue 组件可以用两种不同的 API 风格编写:Options API 和 Composition API
2.3.1、Options API
使用 Options API,我们使用选项对象定义组件的逻辑,例如 data、methods 和 mounted。由选项定义的属性在 this 内部函数中公开,指向组件实例,如下所示。
<template> <button @click="increment">count is: {{ count }}</button> </template> <script> export default { data() { return { count: 0 } }, methods: { increment() { this.count++; } }, mounted() { console.log(`The initial count is ${this.count}.`); } } </script>
2.3.2、Composition API
使用 Composition API,我们使用导入的 API 函数定义组件的逻辑。在 SFC 中,Composition API 通常使用
<template> <button @click="increment">Count is: {{ count }}</button> </template> <script setup> import { ref, onMounted } from 'vue'; const count = ref(0); function increment() { count.value++; } onMounted(() => { console.log(`The initial count is ${count.value}.`); }) </script>
2.4、创建项目
2.4.1、使用vue-cli 命令构建,前提是安装了node.js
① 如果之前安装过Vue2,需要把Vue2卸载
npm uninstall vue-cli -g
② 安装 Vue CLI 命令如下:
npm install -g @vue/cli # 或者 yarn global add @vue/cli # 升级全局的 Vue CLI 包 npm update -g @vue/cli # 或者 yarn global upgrade --latest @vue/cli
③ 使用 vue create
命令创建项目,命令如下:
# 创建项目
vue create 项目名称
注意:项目名中不能包含大写字母
此时,开始选择模板:
我们现在用最后一个(手动选择并创建项目)为例,一般想要快捷的话,直接选择第一个就好啦!
(1)按需选择依赖,学习阶段一般选择前两个和路由
使用空格键可以选择,使用上下键可以移动
解析:
Babel:将源代码转换成指定版本的JS,如ES6=>ES5
TypeScript:使用强类型的JavaScript预处理语言
PWA:使用渐进式网页应用
Router:使用Vue路由
Vuex:使用Vuex状态管理
CSS Pre-processors:CSS预处理器,如Less、Sass
Linter/Formatter:使用代码风格检查和格式化器
Unit Testing:使用单元测试
E2E Testing:使用一种End to End (端到端)的黑盒测试
(2)选择版本
(3)是否使用Class风格的组件定义语法?
即原本是:home = new Vue()创建vue实例
使用装饰器后:class home extends Vue{}
(4)使用Babel与TypeScript一起用于自动检测的填充? 选择 Y(忘记截图了 ☹)
(5)Babel,ESLint 等配置文件的存放方式
(6)是否需要保存当前配置,为以后生成新项目时进行快速构建,这里选择n,不保存
(7)创建成功
(8)开始运行
(9)运行结果
2.4.2、使用图形化界面创建项目
(1)通过 vue ui
命令以图形化界面创建和管理项目:
浏览器自动打开以下页面
(2)选择创建新项目
(3)写文件名
(4)选择预设
(5)选择功能
(6)选择配置
(7)不设置预设名
(8)创建成功
(9)详细信息
2.4.2、使用 Vite(下一代前端开发与构建工具)构建项目
官方文档:v3.cn.vuejs.org/guide/insta…
vite官网:vitejs.cn
-
什么是vite?—— 是Vue团队打造的新一代前端构建工具。
-
优势如下:
- 开发环境中,无需打包操作,可快速的冷启动。
- 轻量快速的热重载(HMR)。
- 真正的按需编译,不再等待整个应用编译完成。
-
传统构建 与 vite构建对比图
- 传统构建模式,是将所有资源都打包好,再上线
而 Vite 有点按需加载的意思在那里了~
接下来我们就用 Vite 来创建一个Vue3的项目
(1)创建工程:npm init vite-app 项目名称
(2)进入工程目录
(3)安装依赖:npm install
(4)运行项目:npm run dev
(5)运行结果
2.5、项目结构
(1)打包项目
生成dist目录
注意:如果需要运行打包后的 html 文件就需要重新打开根目录为 dist 的文件夹,再重新运行 html 文件才可以!!!
(2)、目录结构
Vue/cli文档:https://cli.vuejs.org/zh/guide/
|-dist -- 生成打包后文件 |-node_modules -- 项目依赖包的目录 |-public -- 项目公用文件 |--favicon.ico -- 网站地址栏前面的小图标 |--index.html -- 项目的默认首页,Vue的组件需要挂载到这个文件上 |-src -- 源文件目录,程序员主要工作的地方 |-api -- 与后端交互使用相关方法和配置 |-assets -- 静态文件目录,图片图标、样式,比如网站logo |-components -- Vue3.x的自定义组件目录 |-router -- vue-router相关配置 |--utils -- 通用工具包 |--views -- 页面 |--App.vue -- 项目的根组件,单页应用都需要的 |--main.css -- 一般项目的通用CSS样式写在这里,main.js引入 |--main.js -- 项目入口文件,SPA单页应用都需要入口文件 |--.gitignore -- git的管理配置文件,设置那些目录或文件不管理 |--package-lock.json -- 项目包的锁定文件,用于防止包版本不一样导致的错误 |--package.json -- 项目配置文件,包管理、项目名称、版本和命令
vue.config.js:当我们想要定义一些全局变量,比如常用的包管理器或者部署应用包的基本URL时,可以通过vue.config.js配置文件定义。这个文件是可选的,如果在项目的根目录下,那么它会被 @vue/cli-service 自动加载,比如我们常用到的vue-cli-service serve或者vue-cli-service build。当然,如果不想用这个文件,也可以直接写在package.json的vue字段。如果根目录下存在vue.config.js文件,那么package.json文件中的vue字段的配置会被忽略。
.browserslistrc:打包时我们需要兼容哪些浏览器
解析:
> 1% 代表着全球超过1%人使用的浏览器
last 2 versions 表示所有浏览器兼容到最后两个版本
not dead 不是24个月没有官方支持或更新的浏览器。目前它是IE 11,IE_Mob 11,黑莓10,黑莓7,三星4,OperaMobile 12.1和百度的所有版本。
not ie 11 排除ie11
如果你的项目要兼容IE,但你的browserslist规则是这样的,那么打包的就会有个提示:All browser targets in the browserslist configuration have supported ES module.Therefore we don't build two separate bundles for differential loading.那么这里就不会打包兼容IE的代码。
babel.config.js:ES6向上兼容
README.md:项目的概要信息
tsconfig.json:TypeScript的配置信息
(3)分析文件
main.ts 文件:
App.vue 文件(单文件组件):
HelloWorld.vue 文件:
2.6、栗子:自定义组件,实现Hello Vue3的练习
(1)创建一个 test.vue 文件
(2)test.vue文件 代码如下:
<template> <h1>{{msg}}</h1> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ name: 'test', // 组件名称 props: { // 组件的属性 msg: String, }, }); </script> <style scoped> h1 { text-align: center; line-height: 1500%; } </style>
(3)App.vue文件 代码如下:
<template> <test msg="你好啊,Vue3!!!" /> </template> <!-- 用的语言为TypeScript --> <script lang="ts"> // 导入模块,并解构出定义组件的函数 import { defineComponent } from 'vue'; // 导入组件 import test from './components/test.vue'; // 将定义好的组件以默认对象的形式导出 export default defineComponent({ name: 'App', // 组件名称 components: { // 注册本组件要使用到的组件 test } }); </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
(4)使用 npm run serve 命令,热部署运行项目
(5)运行结果
三、单文件组件
在大多数启用了构建工具的 Vue 项目中,我们可以使用一种类似 HTML 格式的文件来书写 Vue 组件,它被称为单文件组件 (也被称为 *.vue
文件,英文 Single-File Components,缩写为 SFC)。顾名思义,Vue 的单文件组件会将一个组件的逻辑 (JavaScript),模板 (HTML) 和样式 (CSS) 封装在同一个文件里
下面我们将用单文件组件的格式实现计数器栗子:
<script> export default { data() { return { count: 0 } } } </script> <template> <button @click="count++">Count is: {{ count }}</button> </template> <style scoped> button { font-weight: bold; } </style>
四、API 风格
Vue 的组件可以按两种不同的风格书写:选项式 API (Vue2 常用)和 组合式 API(Vue3 常用)
4.1、选项式 API (Options API)
使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data
、methods
和 mounted
。选项所定义的属性都会暴露在函数内部的 this
上,它会指向当前的组件实例。
<script> export default { // data() 返回的属性将会成为响应式的状态 // 并且暴露在 `this` 上 data() { return { count: 0 } }, // methods 是一些用来更改状态与触发更新的函数 // 它们可以在模板中作为事件监听器绑定 methods: { increment() { this.count++ } }, // 生命周期钩子会在组件生命周期的各个不同阶段被调用 // 例如这个函数就会在组件挂载完成后被调用 mounted() { console.log(`The initial count is ${this.count}.`) } } </script> <template> <button @click="increment">Count is: {{ count }}</button> </template>
4.2、组合式 API (Composition API)
通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup>
搭配使用。这个 setup
attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup>
中的导入和顶层变量/函数都能够在模板中直接使用。
下面是使用了组合式 API 与 <script setup>
改造后和上面的模板完全一样的组件:
<template> <div> <button type="button" @click="increment()">count is : {{count}}</button> </div> </template> <script> // 导入需要用到的东西 import { ref, onMounted } from 'vue' export default { name: 'Hello', setup () { const count = ref(0); // 如果有多个,可以用数组或对象的方式 function increment () { // 自定义方法,发生事件时调用 count.value++ } onMounted(() => { // 钩子函数,生命周期 console.log(count) }) // 一定要觉得导出哦! return { count, increment } } } </script> <style scoped> button{ font-weight: bold; } </style>
注意:使用vue3项目的时候,一般使用组合式!!!
五、Vue3 在HTML中创建多个App,并实现动态绑定
5.1、在浏览器中直接使用Vue3.0
借助 script 标签直接通过 CDN 来使用 Vue:
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
或者直接在浏览器打开 https://unpkg.com/vue@3/dist/vue.global.js 链接,另存为,保存在本地
这里我们使用了 unpkg,但你也可以使用任何提供 npm 包服务的 CDN,例如 jsdelivr 或 cdnjs。当然,你也可以下载此文件并自行提供服务。
通过 CDN 使用 Vue 时,不涉及“构建步骤”。这使得设置更加简单,并且可以用于增强静态的 HTML 或与后端框架集成。但是,你将无法使用单文件组件 (SFC) 语法。
5.2、使用全局构建版本的栗子:
<body> <div id="app"> 姓名:<input type="text" v-model="yourname"> <h2>你好,{{yourname}}</h2> </div> <script src="./js/vue.global.js"></script> <script> const { createApp } = Vue; createApp({ data() { return { yourname: "大美女", }; }, }).mount("#app"); </script> </body>
5.3、同时创建多个App 的栗子:
<body> <div id="app1"> <h2>{{msg}}</h2> </div> <div id="app2"> <h2>{{msg}}</h2> </div> <script src="./js/vue.global.js"></script> <script> const { createApp } = Vue; createApp({ data() { return { msg: "我是app1", }; }, }).mount("#app1"); createApp({ data() { return { msg: "我是app2", }; }, }).mount("#app2"); </script> </body>
5.4、动态绑定:设置h2标签的标题为当前日期时间
代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app1"> <h2>{{msg}}</h2> </div> <div id="app2"> <h2 :title="currentDate">{{msg}}</h2> </div> <script src="./js/vue.global.js"></script> <script> const { createApp } = Vue; createApp({ data() { return { msg: "我是app1", }; }, }).mount("#app1"); createApp({ data() { return { msg: "我是app2", currentDate:"当前日期时间为:"+new Date().toLocaleString() }; }, }).mount("#app2"); </script> </body> </html>
运行结果:
六、Vue3的v-if与v-for简单模板语法
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>vue3介绍</title> </head> <body> <div id="app3"> <span v-if="isShow"> isShow为true时你可以看到我 </span> </div> <div id="app4"> <span v-if="isShow"> <table border="1" cellspacing="1" cellpadding="1" width="50%"> <tr> <th>序号</th> <th>名称</th> <th>价格</th> </tr> <tr v-for="(obj,index) in fruits"> <td>{{index+1}}</td> <td>{{obj.name}}</td> <td>{{obj.price}}</td> </tr> </table> </span> </div> <script src="./js/vue3.js" type="text/javascript" charset="utf-8"></script> <script type="text/javascript"> //if指令 const { createApp } = Vue; //vue应用对象 var app3 = createApp({ data() { return { isShow: true, }; }, }).mount("#app3"); //循环指令 var app4 = createApp({ data() { return { isShow: true, fruits: [ { name: "苹果", price: "6.8", }, { name: "橙子", price: "3.5", }, { name: "香蕉", price: "2.3", }, ], }; }, }).mount("#app4"); </script> </body> </html>
这个例子演示了我们不仅可以把数据绑定到 DOM 文本或特性,还可以绑定到 DOM 结构。
七、Vue3事件与计算属性
实现文本倒序:
<body> <div id="app"> <button @click="reverseString">{{msg}}</button> <input type="text" v-model="msg"> </div> <script src="./js/vue.global.js" type="text/javascript" charset="utf-8"></script> <script type="text/javascript"> let {createApp,ref} = Vue; createApp({ setup() { let msg = ref("Hello,大美女"); function reverseString(){ msg.value = msg.value.split("").reverse().join(""); } return { msg,reverseString }; } }).mount("#app") </script> </body>
运行结果:
实现加法运算栗子:
<body> <div id="app"> <button @click="n1++">+</button> <input type="text" v-model="n1"> + <input type="text" v-model="n2"> = <input type="text" v-model="sum"> </div> <script src="./js/vue.global.js" type="text/javascript" charset="utf-8"></script> <script type="text/javascript"> let {createApp,ref,computed} = Vue; createApp({ setup() { let n1 = ref(0); let n2 = ref(0); let sum = computed(()=>{ return parseInt(n1.value) + parseInt(n2.value); }) return { n1,n2,sum }; } }).mount("#app") </script> </body>
运行结果:
八、javascript数组
8.1、创建数组的语法
var arrayObj = new Array();
var arrayObj = new Array([size]);
var arrayObj = new Array([element0[, element1[, ...[, elementN]]]])
栗子:
// 8.1、创建数组
var array11 = new Array(); //空数组 var array12 = new Array(5); //指定长度,可越界 var array13 = new Array("a","b","c",1,2,3,true,false); //定义并赋值 var array14=[]; //空数组,语法糖 var array15=[1,2,3,"x","y"]; //定义并赋值
8.2、访问与修改
var testGetArrValue=arrayObj[1];
arrayObj[1]= "值";
栗子:
//8.2、访问与修改 array12[8]="hello array12"; //赋值或修改 console.log(array12[8]); //取值 //遍历 for (var i = 0; i < array13.length; i++) { console.log("arrayl3["+i+"]="+array13[i]); } //枚举 for(var i in array15){ console.log(i+"="+array15[i]); //此处的i是下标 }
运行结果:
8.3、添加元素
将一个或多个新元素添加到数组未尾,并返回数组新长度
arrayObj. push([item1 [item2 [. . . [itemN ]]]]);
将一个或多个新元素添加到数组开始,数组中的元素自动后移,返回数组新长度
arrayObj.unshift([item1 [item2 [. . . [itemN ]]]]);
将一个或多个新元素插入到数组的指定位置,插入位置的元素自动后移,返回被删除元素数组,deleteCount要删除的元素个数
arrayObj.splice(insertPos,deleteCount,[item1[, item2[, . . . [,itemN]]]])
栗子:
//8.3、添加元素 var array31=[5,8]; //添加到末尾 array31.push(9); var len=array31.push(10,11); console.log("长度为:"+len+"——"+array31); //添加到开始 array31.unshift(4); var len=array31.unshift(1,2,3); console.log("长度为:"+len+"——"+array31); //添加到中间 var len=array31.splice(5,1,6,7); //从第5位开始插入,删除第5位后的1个元素,返回被删除元素 console.log("被删除:"+len+"——"+array31);
8.4、删除
移除最后一个元素并返回该元素值
arrayObj.pop();
移除最前一个元素并返回该元素值,数组中元素自动前移
arrayObj.shift();
删除从指定位置deletePos开始的指定数量deleteCount的元素,数组形式返回所移除的元素
arrayObj.splice(deletePos,deleteCount);
栗子:
//8.4、删除 var array41=[1,2,3,4,5,6,7,8]; console.log("array41:"+array41); //删除最后一个元素,并返回 var e=array41.pop(); console.log("被删除:"+e+"——"+array41); //删除首部元素,并返回 var e=array41.shift(); console.log("被删除:"+e+"——"+array41); //删除指定位置与个数 var e=array41.splice(1,4); //从索引1开始删除4个 console.log("被删除:"+e+"——"+array41);
运行结果:
8.5、截取和合并
以数组的形式返回数组的一部分,注意不包括 end 对应的元素,如果省略 end 将复制 start 之后的所有元素
arrayObj.slice(start, [end]);
将多个数组(也可以是字符串,或者是数组和字符串的混合)连接为一个数组,返回连接好的新的数组
arrayObj.concat([item1[, item2[, . . . [,itemN]]]]);
栗子:
//8.5、截取和合并 var array51=[1,2,3,4,5,6]; var array52=[7,8,9,0,"a","b","c"]; //截取,切片 var array53=array51.slice(2); //从第3个元素开始截取到最后 console.log("被截取:"+array53+"——"+array51); var array54=array51.slice(1,4); //从第3个元素开始截取到索引号为3的元素 console.log("被截取:"+array54+"——"+array51); //合并 var array55=array51.concat(array52,["d","e"],"f","g"); console.log("合并后:"+array55);
运行结果:
8.6、拷贝
返回数组的拷贝数组,注意是一个新的数组,不是指向
arrayObj.slice(0);
返回数组的拷贝数组,注意是一个新的数组,不是指向
arrayObj.concat();
因为数组是引用数据类型,直接赋值并没有达到真正实现拷贝,地址引用,我们需要的是深拷贝。
8.7、合并成字符
返回字符串,这个字符串将数组的每一个元素值连接在一起,中间用 separator 隔开。
arrayObj.join(separator);
栗子:
//8.7、合并成字符与将字符拆分成数组 var array81=[1,3,5,7,9]; var ids=array81.join(","); console.log(ids); //拆分成数组 var text="hello nodejs and angular"; var array82=text.split(" "); console.log(array82);
8.8、排序
反转元素(最前的排到最后、最后的排到最前),返回数组地址
arrayObj.reverse();
对数组元素排序,返回数组地址
arrayObj.sort();
arrayObj.sort(function(obj1,obj2){});
栗子:
var array71=[4,5,6,1,2,3]; array71.sort(); console.log("排序后:"+array71); var array72=[{name:"tom",age:19},{name:"jack",age:20},{name:"lucy",age:18}]; array72.sort(function(user1,user2){ return user1.age<user2.age; }); console.log("排序后:"); for(var i in array72) console.log(array72[i].name+","+array72[i].age);
运行结果:
九、JavaScript排序
9.1、概要
javascript内置的sort函数是多种排序算法的集合,数组在原数组上进行排序,不生成副本。
JavaScript实现多维数组、对象数组排序,其实用的就是原生的sort()方法,用于对数组的元素进行排序。
sort() 方法用于对数组的元素进行排序。语法如下:
ArrayObject.sort(order);
9.2、简单排序
如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。
栗子:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>排序</title> </head> <body> <script> var numbers=[2,4,6,8,0,1,2,3,7,9]; numbers.sort(); //默认按升序排列 console.log(numbers.join(',')); numbers.reverse(); //反转 console.log(numbers.join(',')); //将元素用逗号连接成一个字符串 </script> </body> </html>
运行结果:
9.3、简单数组自定义排序
如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:
// 升序: a>b 返回 1 // 不变 a=b 返回 0 // 降序 a<b 返回 -1
栗子:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>排序</title> </head> <body> <script> var numbers=[2,4,6,8,0,1,2,3,7,9]; //当a>b的结果为正数时则为升序 numbers.sort(function(a,b){ if(a>b){return 1;} if(a<b){return -1;} return 0; }); console.log(numbers.join(',')); //简化,注意类型 numbers.sort(function(a,b){ return a-b; }); console.log(numbers.join(',')); //降序 numbers.sort(function(a,b){ return b-a; }); console.log(numbers.join(',')); </script> </body> </html>
运行结果:
9.4、简单对象List自定义属性排序
栗子:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>排序</title> </head> <body> <script> //对象数组 var pdts = [{ title: "z-paint pot", quantity: 9, price: 3.95 },{ title: "iPhone XS", quantity: 10, price: 8906.72 },{ title: "polka dots", quantity: 17, price: 12.3 }, { title: "pebbles", quantity: 5, price: 6.71 }, { title: "Mi Note5", quantity: 8, price: 2985.6 }]; //按价格升序 pdts.sort(function(x,y){ return x.price-y.price; }); document.write(JSON.stringify(pdts)); document.write("<br/>"); //按名称排序 pdts.sort(function(x,y){ if(x.title>y.title) return 1; if(x.title<y.title) return -1; return 0; }); document.write(JSON.stringify(pdts)); </script> </body> </html>
9.5、封装通用的排序函数
如果排序的条件要不断变化,将反复写简单的排序函数,封装可以带来方便:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>排序</title> </head> <body> <script> //对象数组 var pdts = [{ title: "z-paint pot", quantity: 9, price: 3.95 }, { title: "iPhone XS", quantity: 10, price: 8906.72 }, { title: "polka dots", quantity: 17, price: 12.3 }, { title: "pebbles", quantity: 5, price: 6.71 }, { title: "Mi Note5", quantity: 8, price: 2985.6 }]; //根据排序关键字与是否为升序产生排序方法 var sortExp = function(key, isAsc) { return function(x, y) { if(isNaN(x[key])) { //如果当前排序的不是数字 if(x[key] > y[key]) return 1*(isAsc?1:-1); if(x[key] < y[key]) return -1*(isAsc?1:-1); return 0; }else{ return (x[key]-y[key])*(isAsc?1:-1); } } }; //按价格升序 pdts.sort(sortExp("price",true)); document.write(JSON.stringify(pdts)); document.write("<br/>------------------------------<br/>"); pdts.sort(sortExp("price",false)); document.write(JSON.stringify(pdts)); document.write("<br/>------------------------------<br/>"); pdts.sort(sortExp("title",true)); document.write(JSON.stringify(pdts)); document.write("<br/>------------------------------<br/>"); pdts.sort(sortExp("title",false)); document.write(JSON.stringify(pdts)); </script> </body> </html>
十、路由在Vue3的创建与使用(相当于选项卡)
10.1、创建项目时,把路由也勾选上
10.2、在view目录下创建一个hello.vue文件,代码如下:
<template lang=""> <div> <h1>{{msg}}</h1> </div> </template> <script lang="ts"> import {defineComponent,ref} from 'vue'; export default defineComponent({ name:"hello", setup(){ let msg = ref("作业1"); return {msg} } }) </script> <style lang=""> </style>
10.3、在router目录下的ts文件,添加两个代码
10.4、在App.vue文件里面使用,代码如下:
<template> <nav> <router-link to="/">作业一</router-link> | <router-link to="/about">作业二</router-link> </nav> <router-view/> </template> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } nav { padding: 30px; } nav a { font-weight: bold; color: #2c3e50; } nav a.router-link-exact-active { color: #42b983; } </style>
10.5、运行结果:注意路径
结果一:
结果二:
十一、Vue3 + element ui 实现购物车功能
11.1、MySQL 数据库代码:
create table shopping ( id int primary key auto_increment comment '编号', `name` varchar(100) not null comment '名称', price float not null comment '价格', quantity int not null comment '数量' )auto_increment=10001 insert into shopping(`name`,price,quantity) values('Xiaomi Civi 2','2399','1'), ('Xiaomi MIX Fold 2','8999','1'), ('Redmi K50 至尊版','2999','1'), ('Xiaomi 12S Ultra','5999','1'), ('Xiaomi 12S Pro','4699','1'), ('Xiaomi 12S','3999','1') select * from shopping
11.2、spring boot 后台代码:
实体类:
package com.fairy.shopping.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 实体类 */ @Data @NoArgsConstructor @AllArgsConstructor public class Shopping { private int id; // 编号 private String name; // 名称 private float price; // 价格 private int quantity; // 数量 }
dao 包:
package com.fairy.shopping.dao; import com.fairy.shopping.entity.Shopping; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * 映射 */ @Mapper public interface ShoppingDao { /** 查询所有购物车商品 */ List<Shopping> findAll(); /** 删除某条商品信息 */ int delShopping(int id); }
mapper.xml文件
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.fairy.shopping.dao.ShoppingDao"> <!-- 查询所有的购物车信息 --> <select id="findAll" resultType="Shopping"> SELECT shopping.id, shopping.`name`, shopping.price, shopping.quantity FROM shopping </select> <!-- 删除一条购物车信息 --> <delete id="delShopping"> delete from shopping where id = #{id} </delete> </mapper>
yaml 文件:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 url: jdbc:mysql://localhost:3306/shoppingtrolley?serverTimezone=GMT&useUnicode=true&characterEncoding=utf-8 jackson: date-format: yyyy-MM-dd mybatis: type-aliases-package: com.fairy.shopping.entity mapper-locations: classpath:/mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # pagehelper pagehelper: helperDialect: mysql reasonable: true supportMethodsArguments: true params: count=countSql
服务类接口:
package com.fairy.shopping.service; import com.fairy.shopping.entity.Shopping; import java.util.List; /** * 服务类 */ public interface ShoppingService { /** 查询所有购物车商品 */ List<Shopping> findAll(); /** 删除某条商品信息 */ int delShopping(int id); }
服务实现类:
package com.fairy.shopping.service.impl; import com.fairy.shopping.dao.ShoppingDao; import com.fairy.shopping.entity.Shopping; import com.fairy.shopping.service.ShoppingService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class ShoppingServiceImpl implements ShoppingService { @Autowired ShoppingDao shoppingDao; @Override public List<Shopping> findAll() { return shoppingDao.findAll(); } @Override public int delShopping(int id) { return shoppingDao.delShopping(id); } }
11.3、Vue3 前端代码:
添加 element ui 的依赖:
npm install element-plus --save
全局引入:
如果你对打包后的文件大小不是很在乎,那么使用完整导入会更方便。
// main.ts import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.mount('#app')
子组件:
<template> <div class="hello"> <!-- :data="datas.arr:所有的数据 --> <!-- @sort-change="sorts:自定义排序的方法 --> <!-- 定义完排序的方法之后,在需要排序的列中定义sortable="custom" --> <el-table :data="datas.arr" border @sort-change="sorts" > <el-table-column prop="id" label="编号" sortable="custom" /> <el-table-column prop="name" label="名称" sortable="custom" /> <el-table-column prop="price" label="价格" sortable="custom" /> <el-table-column prop="quantity" id="shuliang" sortable="custom" label="数量" #default="scope"> <el-input-number v-model="scope.row.quantity" :min="1" :max="10" /> </el-table-column> <el-table-column label="小计"> <!-- #default="scope":数据 --> <template #default="scope">{{scope.row.price*scope.row.quantity}}</template> </el-table-column> <el-table-column label="操作"> <template #default="scope"> <el-button size="small" type="danger" @click="delshopping(scope.row.id)">移除</el-button> </template> </el-table-column> </el-table> </div> </template> <script lang="ts"> // 导入需要的依赖 import { defineComponent, reactive} from 'vue'; import axios from 'axios' // 注册组件 export default defineComponent({ name: 'HelloWorld', props: { msg: String, }, setup() { // 定义数组,存储所有的数据 var datas = reactive({ arr: [] }); // 查询所有的数据 async function init() { let { data: res } = await axios.get("http://localhost:8080/findAll"); datas.arr = res; } // 加载时就调用 init(); // 删除购物车里某条商品信息 async function delshopping(id: object) { let { data: res } = await axios.delete("http://localhost:8080/delShopping", { params: { id: id } }) if (res > 0) { alert("删除成功!"); init(); } } //根据排序关键字与是否为升序产生排序方法 // key:代表属性,isAsc:代表是否为真 var sortExp = function (key: any, isAsc: any) { // x:代表是的数组的任意一个数,y:也是如此 // 它们的意义:两个数对比,哪个数大就在前面,相当于冒泡排序 return function (x: any, y: any) { if (isNaN(x[key])) { //如果当前排序的不是数字 // x[key]:这个数的属性值是什么 // 返回1:升序;返回2:降序 if (x[key] > y[key]) return 1 * (isAsc ? 1 : -1); if (x[key] < y[key]) return -1 * (isAsc ? 1 : -1); return 0; } else { return (x[key] - y[key]) * (isAsc ? 1 : -1); } } }; // 自定义排序的方法 async function sorts(sb: any) { // 如果是升序 if (sb.order === "ascending") { datas.arr = reactive(datas.arr.sort(sortExp(sb.prop, true))); } else { datas.arr = reactive(datas.arr.sort(sortExp(sb.prop, false))); } console.log(sb); } // 暴露方法出去 return { datas, delshopping, sorts } } }); </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped></style>
App.vue 组件:
<template> <h1>cc商城购物车</h1> <div id="big"> <!-- 把子组件的东西放这里 --> <hello /> </div> </template> <script lang="ts"> import hello from './components/HelloWorld.vue' export default { name: 'app', components:{ hello } }; </script> <style> #big{ width: 80%; margin: auto; } h1{ text-align: center; } </style>
运行命令:npm run serve;
运行结果如下:
十二、react 框架实现购物车功能
12.1、创建 react 项目
npx create-react-app 项目名称 // 创建react项目
cd 项目名称 // 切换到项目的目录里
npm start // 运行项目
12.2、导入 axios 插件
npm i axios // 如果报错了,在这个命令后面加 --legacy-peer-deps
12.3、项目结构
12.4、数据库上面有!!!
12.5、后台 curd 方法
12.5.1、dao 包
package com.fairy.shopping.dao; import com.fairy.shopping.entity.Shopping; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * 映射 */ @Mapper public interface ShoppingDao { /** 查询所有购物车商品 */ List<Shopping> findAll(); /** 删除某条商品信息 */ int delShopping(int id); /** 添加商品信息 */ int addShopping (Shopping shopping); /** 修改商品信息 */ int updateShopping (Shopping shopping); }
12.5.2、mapper.xml 文件
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.fairy.shopping.dao.ShoppingDao"> <!-- 查询所有的购物车信息 --> <select id="findAll" resultType="Shopping"> SELECT shopping.id, shopping.`name`, shopping.price, shopping.quantity FROM shopping </select> <!-- 删除一条购物车信息 --> <delete id="delShopping"> delete from shopping where id = #{id} </delete> <insert id="addShopping"> insert into shopping values (0,#{name},#{price},#{quantity}) </insert> <update id="updateShopping"> update shopping set `name` = #{name}, `price` = #{price}, `quantity` = #{quantity} where `id` = #{id} </update> </mapper>
12.5.3、服务类接口
package com.fairy.shopping.service; import com.fairy.shopping.entity.Shopping; import java.util.List; /** * 服务类 */ public interface ShoppingService { /** 查询所有购物车商品 */ List<Shopping> findAll(); /** 删除某条商品信息 */ int delShopping(int id); /** 添加商品信息 */ int addShopping (Shopping shopping); /** 修改商品信息 */ int updateShopping (Shopping shopping); }
12.5.4、服务实现类
package com.fairy.shopping.service.impl; import com.fairy.shopping.dao.ShoppingDao; import com.fairy.shopping.entity.Shopping; import com.fairy.shopping.service.ShoppingService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class ShoppingServiceImpl implements ShoppingService { @Autowired ShoppingDao shoppingDao; @Override public List<Shopping> findAll() { return shoppingDao.findAll(); } @Override public int delShopping(int id) { return shoppingDao.delShopping(id); } @Override public int addShopping(Shopping shopping) { return shoppingDao.addShopping(shopping); } @Override public int updateShopping(Shopping shopping) { return shoppingDao.updateShopping(shopping); } }
12.5.5、控制器
package com.fairy.shopping.controller; import com.fairy.shopping.entity.Shopping; import com.fairy.shopping.service.ShoppingService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; /** * 控制器 */ @RestController @CrossOrigin(origins="*") @RequiredArgsConstructor // 注入service public class ShoppingController { // 注意:前面记得加final,否则会报错 final ShoppingService shoppingService; @GetMapping("/findAll") public List<Shopping> findAll(){ return shoppingService.findAll(); } @DeleteMapping("/delShopping") public int delShopping(int id){ return shoppingService.delShopping(id); } @PostMapping("/addShopping") public int addShopping(@RequestBody Shopping shopping){ return shoppingService.addShopping(shopping); } @PutMapping("/updateShopping") public int updateShopping(@RequestBody Shopping shopping){ return shoppingService.updateShopping(shopping); } }
13、前端代码
13.1、App.js
import './App.css'; import React from 'react'; import axios from 'axios'; import '../src/css/index.css' // 类组件 // this返回的都是APP组件 export default class App extends React.Component{ // 存储变量的 state = ({list:[],productInfo:{id:0,name:null,price:null,quantity:null},total:0}) // 返回给页面的东西 render() { return ( <div> <h1>CC商城的购物车</h1> <table border={1} width={"80%"}> <tbody> <tr> <th>编号</th> <th>姓名</th> <th>价格</th> <th>数量</th> <th>小计</th> <th>操作</th> </tr> </tbody> {/* 循环遍历商品信息 */} {this.state.list.map((item,i)=> <tbody key={item.id}> <tr> <td>{item.id}</td> <td>{item.name}</td> <td>{item.price}</td> <td> <button className='btncount' onClick={()=>this.count(i,1)}>+</button> <input type='text' id='count' value={item.quantity}></input> <button className='btncount' onClick={()=>this.count(i,0)}>-</button> </td> <td>{item.price * item.quantity}</td> <td className='ywd2'> <button onClick={()=>this.del(item.id)}>删除</button> <button id='update' onClick={()=>this.show(item)}>编辑</button> </td> </tr> </tbody> )} <tbody> <tr > <td colSpan={6} > <span id="total">合计:{this.state.total}</span> </td> </tr> </tbody> </table> {/* 下面文本框的编写 */} <fieldset id='fieldset'> <legend>商品管理</legend> <p> <label>商品名称:</label> <input type="text" id='sname' defaultValue={this.state.productInfo.name} onInput={()=>this.inputss('name','sname')}/> </p> <p> <label>商品价格:</label> <input type="text" id='prices' defaultValue={this.state.productInfo.price} onInput={()=>this.inputss('price','prices')} /> </p> <p> <label>商品数量:</label> <input type="number" id='q1' defaultValue={this.state.productInfo.quantity} onInput={()=>this.inputss('quantity','q1')}/> </p> <div className='ywd'> <button onClick={this.addShopping}>添加</button> <button onClick={this.updateds}>修改</button> </div> </fieldset> </div> ) } ///////////////////////////////////////////////////////////////////////////////////////////// // 1、查询所有方法 async init(){ let {data:res} = await axios.get("http://localhost:8080/findAll"); this.state.list = res this.setState({list:this.state.list}) this.total(); } ///////////////////////////////////////////////////////////////////////////////////////////// // 2、计算合计的价格 total = ()=>{ // 循环所有的数据,然后存一个变量total在state里面,+=遍历出来的所有数据里面的价格 this.state.list.forEach(item=>{ this.state.total += (item.quantity*item.price); }) // 进行修改操作 this.setState({total:this.state.total}) } ///////////////////////////////////////////////////////////////////////////////////////////// // 3、改变数量的方法,传下标和1或0的数(目的是:判断是+还是-) count(i,number){ if (number) { // 如果数不是0就为true this.state.list[i].quantity+=1; this.setState({list:this.state.list}) }else{ this.state.list[i].quantity-=1; // 如果数量小于0的话,它就一直等于0 if (this.state.list[i].quantity<0) { this.state.list[i].quantity = 0; } // 修改list对象的状态 this.setState({list:this.state.list}) } // 调用计算总价的方法,对总价进行刷新 this.total(); } ///////////////////////////////////////////////////////////////////////////////////////////// // 4、添加的方法 addShopping = async()=>{ let {data:res} = await axios.post('http://localhost:8080/addShopping',this.state.productInfo) if(res){ alert('添加成功') this.init() }else{ alert('添加失败') } } ///////////////////////////////////////////////////////////////////////////////////////////// // 5、删除的方法 async del(id){ let {data:res} = await axios.delete('http://localhost:8080/delShopping',{ params:{ id } }) if(res){ alert('删除成功') this.init() }else{ alert('删除失败') } } ///////////////////////////////////////////////////////////////////////////////////////////// // 6、编辑的方法,通过这个方法拿到该行的数据,进行重新赋值给productInfo show(row){ this.state.productInfo.id = row.id; this.state.productInfo.name = row.name; this.state.productInfo.price = row.price; this.state.productInfo.quantity = row.quantity; this.setState({productInfo:this.state.productInfo}) } // 改变文本框的值,便于进行修改操作,传属性值和id值 inputss (key,value){ // DOM操作获取文本框的值,重新赋值给productInfo的某个属性值 this.state.productInfo[key] = document.getElementById(value).value this.setState({list:this.state.list,productInfo:this.state.productInfo}) } ///////////////////////////////////////////////////////////////////////////////////////////// // 7、修改的方法 updateds = async()=>{ let {data:res} = await axios.put('http://localhost:8080/updateShopping',this.state.productInfo) if(res){ alert('修改成功') this.init() }else{ alert('修改失败') } } ///////////////////////////////////////////////////////////////////////////////////////////// // 8、清除下面文本框的方法 clearInfo = ()=>{ this.setState ({productInfo:{id:0,name:this.state.productInfo.name,price:null,quantity:null}}) this.setState({productInfo:{id:0,name:'',price:null,quantity:null}}) } ///////////////////////////////////////////////////////////////////////////////////////////// //生命周期 componentDidMount(){ this.init() } }
13.2、src/css/index.css
.ywd{ display: flex; } h1{ text-align: center; } table{ height: 250px; margin: auto; text-align: center; border-collapse: collapse; } th{ background-color: darkblue; color: #fff; } button{ width: 70px; height: 30px; background-color: cornflowerblue; color: #fff; border-radius: 20px; border: 0; margin-left: 10px; } #update{ background-color: coral; } #count{ text-align: center; width: 40px; } #fieldset{ width: 30%; margin-left: 10%; margin-top: 30px; } #total{ font-size: 25px; color: rebeccapurple; float: right; margin-right: 20px; } .btncount{ width: 30px; height: 20px; border-radius: 5px; margin: 5px; }