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 可以通过事件绑定 ModelModel 可以通过数据绑定 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元素、也可以传递动态变量、甚至是可交互的应用组件。

框架特点:

  1. 声明式设计:React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React能高效更新并渲染合适的组件。

  2. 组件化: 构建管理自身状态的封装组件,然后对其组合以构成复杂的 UI。

  3. 高效:React通过对DOM的模拟,最大限度地减少与DOM的交互。

  4. 灵活:无论你现在使用什么技术栈,在无需重写现有代码的前提下,通过引入React来开发新功能。

1.5.2、 AngularJS(字面意思是:有角的; 用角测量的)

AngularJS 是一个 JavaScript 框架。它可通过 <script> 标签添加到 HTML 页面。AngularJS通过指令扩展了HTML,并且通过表达式绑定数据到 HTML。

官网: https://www.angularjs.org

中文社区:http://www.angularjs.cn

中文网:http://www.apjs.net

优点:

  1. AngularJS模板功能强大丰富,自带了极其丰富的angular指令。

  2. AngularJS是完全可扩展的,与其他库的兼容效果很好,每一个功能可以修改或更换,以满足开发者独特的开发流程和功能的需求。

  3. AngularJS是一个比较完善的前端MVC框架,包含服务,模板,数据双向绑定,模块化,路由,过滤器,依赖注入等所有功能;

  4. AngularJS是互联网巨人谷歌开发,这也意味着他有一个坚实的基础和社区支持。

缺点:

  1. AngularJS强约束导致学习成本较高,对前端不友好。但遵守 AngularJS 的约定时,生产力会很高,对 Java 程序员友好。

  2. AngularJS不利于SEO,因为所有内容都是动态获取并渲染生成的,搜索引擎没法爬取。

1.5.3、Vue.js

 Vue.js 是一个轻巧、高性能、可组件化的MVVM库,是一套构建用户界面的渐进式框架,同时拥有非常容易上手的API,作者是尤雨溪(中国人)、

官网: http://cn.vuejs.org/

仓库: https://github.com/vuejs

文档与资源大全: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、生命周期

对于生命周期来说,整体上变化不大,只是大部分生命周期钩子名称上 + “on”,功能上是类似的。不过有一点需要注意,Vue3 在组合式API(Composition API,下面展开)中使用生命周期钩子时需要先引入,而 Vue2 在选项API(Options API)中可以直接调用生命周期钩子,如下所示。
// 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)

Vue3 提供 Suspense 组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如 loading ,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态。
<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、移除了某些元素

  1. 实例方法 $on 移除   (eventBus现有实现模式不再支持 可以使用三方插件替代)

  2. 过滤器 filter 移除 (插值表达式里不能再使用过滤器 可以使用methods替代)

  3. 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,我们可以用包含多个选项的对象来描述组件的逻辑,例如 datamethods 和 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;
}

14、运行命令:npm start

posted @ 2022-10-20 13:10  __fairy  阅读(723)  评论(0编辑  收藏  举报