2024年4月

2024年:如何根据项目具体情况选择合适的CSS技术栈

(请注意,这是一篇主观且充满个人技术偏好的文章)

方案一: antd/element ui/类似竞品

适合情形: 项目没有设计师 or 大部分人不熟悉CSS且项目赶时间。
antd自带样式,开发人员无需学习CSS,仅需查看参考文档就可以制作出基本不丑的UI界面。

方案二: TailwindCSS + shadcn/ui

适合情形: 设计师对还原度和定制化有较高要求 and 大部分人熟悉CSS and 项目赶时间。
使用headless组件库可以充分将样式和逻辑解耦,满足设计师高度定制化的需求。
shadcn/ui的设计理念是目前headless组件库的最高效的使用方式。它没有对底层headless组件库进行任何封装,而是提供丰富和高质量的示例代码。开发者只需选择自己需要的组件,将代码复制并粘贴到自己的项目中,并根据自己的需求进行自定义。
TailwindCSS可以提高样式开发速度和统一design token.

方案三: 现代CSS

适合情形: 大家希望磨练技术 and 项目不赶时间
现代 CSS 涵盖了许多新特性,适用于那些想要深入了解 CSS 并提升技术水平的团队。

不推荐方案: sass/less/stylus

现代CSS已经拥有了以上这些预处理器的有价值的关键功能。且样式本质上是HTML的配置,无法脱离HTML单独存在。脱离文档结构进行样式的抽象和复用就好像在构造一个精巧的空中楼阁,需要高水平的开发者长期小心翼翼地维护,否则一不小心就会坍塌。

总结

选择合适的 CSS 技术栈是一个重要的决策,每个方案都有其优势和限制。我们需要根据项目的具体情况,权衡各种因素,选择最适合的方案。

vue3 快速入门系列 - 基础

前面我们已经用 vue2 和 react 做过开发了。

从 vue2 升级到 vue3 成本较大,特别是较大的项目。所以许多公司对旧项目继续使用vue2,新项目则使用 vue3。

有些UI框架,比如ant design vue1.x 使用的 vue2。但现在 ant design vue4.x 都是基于 vue3,示例默认是 TypeScript。比如
table
组件管理。

另外 vue3 官网介绍也使用了 TypeScript,例如:
响应式 API:核心

本篇主要介绍:vite 创建vue3项目、组合式api、响应式数据、计算属性、监听、ref、ts、生命周期、自定义hooks。

vue3 简介

Vue.js 3.0,代号海贼王,于2020年9月18日发布 ——
v3.0.0 海贼王

主要有如下改进:

  • 性能
    改进:与 Vue 2 相比,Vue 3 在包大小(通过 Tree-Shaking 减少最多 41%)、初始渲染(快 55%)、更新(快 133%)和内存使用方面表现出了显着的性能改进(最多减少 54%)。
  • 拥抱
    TypeScript
    :更好的支持 TS。有的公司在 vue2 中就用 TS 了
  • 用于应对规模问题的新 API:引入了
    Composition API
    ——一组新的 API,旨在解决大规模应用程序中 Vue 使用的痛点。Composition API 构建在反应性 API 之上,支持类似于 React hooks 的逻辑组合和重用、更灵活的代码组织模式以及比 2.x 基于对象的 API 更可靠的类型推断。
  • 分层内部模块:还公开了较低级别的 API,可解锁许多高级用例

创建 vue3 工程

vue-cli 创建

前面我们用 vue-cli 创建过
vue2 的项目
,用其构建 vue3 也类似,差别就是选择 vue3 版本。最后生成的项目结构如下:

Vue CLI 是官方提供的基于 Webpack 的 Vue 工具链,它现在处于维护模式。我们
建议使用 Vite 开始新的项目
,除非你依赖特定的 Webpack 的特性。在大多数情况下,Vite 将提供更优秀的开发体验 —— 官网 -
项目脚手架

vite 创建

另一种方式是使用
vite
。有如下优势:

  • 对 TypeScript、JSX、CSS 等支持开箱即用。
  • 无论应用程序大小如何,都始终极快的模块热替换(HMR)
  • 极速的服务启动。使用原生 ESM(参考
    mdn esm
    ) 文件,无需打包

Tip
:

  1. vue脚手架(vue-cli) 和创建 react的脚手架(create-react-app)都是基于 webpack。而 vite 也是一种构建工具,和 webpack 类似,也有一些区别,其作者就是 Vue.js 的创始人尤雨溪
  2. HMR 它用于开发环境,不适用于生产环境。更多介绍请看
    这里
  3. jsx 在学习 react 中用到过(请看
    这里
    ),vue 中用 template 写视图部分,react 用 jsx。在 Vue 3 项目中使用 JSX 时,Vite 会将 JSX 语法编译为 Vue 3 的渲染函数。

笔者首先使用
npm create vite@latest
创建项目,自己根据需要选择对应预设(比如要 TypeScript or javascript),创建完成后根据提示进入项目,安装依赖,本地启动:

npm install
npm run dev

结果报错:

> vite-vue3@0.0.0 dev \test-projects\vite-vue3
> vite

(node:40312) UnhandledPromiseRejectionWarning: SyntaxError: Unexpected token '??='
    at Loader.moduleStrategy (internal/modules/esm/translators.js:145:18)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:40312) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:40312) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

说是 node 版本可能低了。

Tip
: Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本 —— vite 官网-
搭建第一个 Vite 项目

于是使用 nvm 安装 18.16.0。步骤如下:

// 目前版本 14.19
PS \test-projects\vite-vue3> node -v
v14.19.0

// nvm 已安装
PS \test-projects\vite-vue3> nvm -v
1.1.10

// nvm 安装 18.16.0
PS \test-projects\vite-vue3> nvm install 18.16.0
Downloading node.js version 18.16.0 (64-bit)...
Extracting node and npm...
Complete
npm v9.5.1 installed successfully.


Installation complete. If you want to use this version, type

nvm use 18.16.0

根据提示切换到 18.16.0

PS \test-projects> nvm use 18.16.0
Now using node v18.16.0 (64-bit)
PS \test-projects> node -v
v18.16.0
npm create vue

使用
npm create vue@latest
创建 vue3 项目 —— vue3 官网
创建一个 Vue 应用
(这里提到 node 需要18+):

PS \test-projects>  npm create vue@latest
Need to install the following packages:
  create-vue@3.9.2
Ok to proceed? (y) y

Vue.js - The Progressive JavaScript Framework

√ 请输入项目名称: ... hello_vue3
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是

正在构建项目 \test-projects\hello_vue3...

项目构建完成,可执行以下命令:

  cd hello_vue3
  npm install
  npm run dev

npm notice
npm notice New major version of npm available! 9.5.1 -> 10.4.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.4.0
npm notice Run npm install -g npm@10.4.0 to update!
npm notice

根据提示按照依赖,本地启动项目成功:

PS \test-projects> cd .\hello_vue3\
PS \test-projects\hello_vue3> npm install

added 63 packages, and audited 64 packages in 20s

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
PS \test-projects\hello_vue3> npm run dev

> hello_vue3@0.0.0 dev
> vite


  VITE v5.1.3  ready in 3045 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
npm create vite/vue

npm create vite@latest 和 npm create vue@latest 作用和用途不同,两者效果也不同,总的来说前者创建 Vite 项目,而
npm create vue@latest
是用来创建 Vue.js 项目。

PS \test-projects>  npm create vite@latest
Need to install the following packages:
  create-vite@5.2.0
Ok to proceed? (y) y
√ Project name: ... hello-vue3
√ Select a framework: » Vue
√ Select a variant: » TypeScript

Scaffolding project in \test-projects\hello-vue3...

Done. Now run:

  cd hello-vue3
  npm install
  npm run dev
vite 本地启动非常快

vite 本地启动非常快。真正按需编译,不在等待整个应用编译完成。

用 webpack 本地启动服务器,需要经历如下几步:
entry->route->module->bundle->服务器启动
(下图左);而用 vite 启动服务器,服务器启动却从末尾移到开头(下图右)

有点像懒加载,你需要访问哪个路由,就加载哪个,非常快速。

vue3项目目录结构浅析

前面我们用 vite 创建了 hello_vue3 项目。目录结构如下:

我们先说其他文件,最后在分析src文件夹

extensions.json

内容如下:

// .vscode/extensions.json
{
  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

推荐你安装这两个插件,当你用 vscode 启动项目,点击切换到其他文件上,vscode 右下角就会提示你是否安装这两个插件。就像这样:

这两个是vue官方给 vscode 提供的插件:

  • TypeScript Vue Plugin (Volar)
  • Vue Language Features

env.d.ts

内容如下:

/// <reference types="vite/client" />

是一个在 Vue.js 项目中使用 Vite 构建工具时引入的指令,它的作用是让 TypeScript 编译器能够识别并利用 Vite 客户端类型声明文件提供的类型信息,以提供更好的智能编码功能和类型检查支持。

Tip
:如果你删除 node_modules 文件夹,你在vscode 中会发现
vite/client
下有红色波浪线。

TypeScript 主要用于处理 JavaScript 代码,并且在处理模块时,它会关注 .ts、.tsx、.js 和 .jsx 这些与 JavaScript 相关的文件类型。

TypeScript 默认情况下并不会识别或处理像 .txt、.gif 这样的非 TypeScript 文件类型。这个文件的作用就是让 ts 认识 txt、jpg、gif等。

比如你在src 下新建 a.txt、b.ts,然后在 b.ts 中编写:

import a from 'a.txt'
console.log(a)

当你清空
env.d.ts
,你会发现
import a from 'a.txt'
中 a.txt 下有红色波浪线。再次还原
env.d.ts
则好了。

通过 ctrl + 鼠标点击进入
vite/client
,你会发现 vue 给我们声明好了我们需要使用的其他类型文件。比如 txt:

declare module '*.txt' {
  const src: string
  export default src
}

index.html

index.html 这就是我们的
入口文件
。内容如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

你可以尝试改成

<body>
  a
</body>

无需重启服务,页面就显示
a

其他

  • tsconfig 文件,ts 配置相关,不要删,ts 可能会有问题:
tsconfig.app.json
tsconfig.json
tsconfig.node.json
  • vite.config.ts 项目配置文件。比如代理、安装插件

  • public/favicon.ico 页签图标

  • package.json、package-lock.json

src

src 就是我们编码的地方。

我们先将 src 中的文件都删除,我们自己重新创建。

创建 main.ts 和 App.vue 两个文件。内容如下:

  • main.ts 是index.html加载的入口文件
// src/main.ts
import {createApp} from 'vue'
// 项目的根
import App from './App.vue'

// Vue.js 3.x 中用于创建和挂载应用
// 创建一个新的 Vue 应用,并将根组件指定为 App。.mount('#app') 将应用挂载到指定的 DOM 元素上
createApp(App).mount('#app')
// src/App.vue
<template>
    <div>
        你好 vue3
    </div>
</template>

<!-- 可以指定语言是 ts,ts中也可以写js -->
<script lang="ts">

</script>

<style scoped>

</style>

浏览器访问,页面显示
你好 vue3

前面我们说到 vite 启动后,服务器就已就绪。然后会根据用户请求哪里,就会给你加载哪里。

vue3 向下兼容 vue2 语法

有些项目使用了 vue3,但写法还是 vue2 ——
不建议这么做

为了证明 vue3 中能写 vue2,笔者在 vue3 项目中写一个 vue2 示例。请看代码:

// src/App.vue
<template>
  <section>
    <p>name: {{ name }}</p>
    <p>date: {{ date }}</p>
    <p><button @click="changeDate">change date</button></p>
  </section>
</template>

<script lang="ts">
export default {
    name: 'App',
    data() {
        return {
          name: 'pengjiali',
          date: -1,
        }
    },
    methods: {
      changeDate() {
        this.date = new Date().getTime();
      }
    }
}
</script>

浏览器显示:

name: pengjiali

date: -1

// 按钮,点击后,date 后的数字就会变化
change date

options Api 和 compositionApi

Vue 2 使用的是选项式 API,而 Vue 3 引入了组合式 API

虽然 Vue 3 推荐使用组合式 API,但它仍然完全支持 Vue 2 的选项式 API,以保持向下兼容性。所以在 Vue 3 中,你可以自由选择使用选项式 API 或组合式 API 来编写你的组件逻辑。

选项式API有一个缺点:新增一个功能,需要分别在 data、methods、computed、watch等选项中修改代码,如果代码上千,修改或抽取封装这部分功能,有困难。

Tip
:我们用
大帅老猿
的图说明以下这个问题

而组合式 api 可以简化这个问题,我们可以感受下(代码如何实现暂时不用管):

Tip
: 具体如何拆分,请看本篇最后
自定义 hooks
章节。

setup

setup
函数是
组合式 API 的入口
,用于组合组件的逻辑和功能。

setup 概述

首先我们用 vue2 语法写一个示例:展示名字和日期,点击按钮能改变日期。代码如下:

<template>
  <section>
    <p>name: {{ name }}</p>
    <p>date: {{ date }}</p>
    <p><button @click="changeDate">change date</button></p>
  </section>
</template>

<script lang="ts">
export default {
    name: 'App',
    data() {
        return {
          name: 'pengjiali',
          date: -1,
        }
    },
    methods: {
      changeDate() {
        this.date = new Date().getTime();
      }
    }
}
</script>

现在我们把 data 和 methods 两个配置去除,改成 setup 就完成了 vue3 示例的重构

<template>
不变...
</template>

<script lang="ts">
export default {
    name: 'App',
    setup() {
      let name = 'pengjiali2'
      let date = -1

      function changeDate(){
        date = new Date().getTime();
        console.log('date: ', date);
      }
      // 将数据和方法都交出去
      return {name, date, changeDate}
    }
}
</script>

setup 是一个方法,平时如何定义变量和方法,这里就怎么写,最后将方法和变量都交出去。

这里其实还有一个问题,点击 button 日期在界面没变,但方法却执行了。这是因为 date 变量不是
响应式
的。

Tip
:现在我们先说 setup,后面在将响应式的东西。这里要修复可以使用 ref(这个 ref 和 vue2 中指向元素或组件的ref,不是同一个东西):

 <script lang="ts">
+import {ref} from 'vue'
 export default {

     name: 'App',
     setup() {
       let name = 'pengjiali2'
-      let date = -1
+      let date = ref(-1)

       function changeDate(){
-        date = new Date().getTime();
+        date.value = new Date().getTime();
         console.log('date: ', date);
       }
       // 将数据和方法都交出去

另外 setup 中的 this 是undefined,vue3 开始弱化 this。

最后说一下 setup 执行时机,比 beforeCreat 还早:

  name: "App",
  beforeCreate() {
    console.log(1);
  },
  setup() {
    console.log(2);

先输出 2 再输出 1。

setup 返回函数

setup 返回值也可以是一个函数,比如这个:

return () => 'hello vue3'

页面就会显示
hello vue3
,模板是什么都不重要了,直接根据这个函数返回值渲染

这种用法不多,常用的还是返回对象。

setup 和配置项的关系

  • setup 能否和 data、method 能否同时写,如果冲突,以谁为准?
  • 配置项能否读取setup 中的东西,setup 能否读取setup 中的东西?
setup 能和 data、method 同时存在

请看示例:

     <p>name: {{ name }}</p>
     <p>date: {{ date }}</p>
+    <p>age: {{ age }}</p>                                                                                                                                                    
+    <p><button @click="sayAge">获取年龄</button></p>       
   </section>                                                                                                                                                                 
   
 </template>

export default {
   beforeCreate() {
     console.log("1: ", 1);
   },
+  data() {
+    return {
+      age: 18
+    }
+  },
+  methods: {
+    sayAge() {
+      console.log('我的年龄', this.age)
+    }
+  },
   setup() {
     console.log("2: ", 2);
     let name = "pengjiali2";

属性 age和方法 sayAge 都能正常使用。

setup 和 beforeCreate 执行顺序

beforeCreate() {
  console.log("beforeCreate");
},
setup() {
  console.log("setup");
  return () => 'hello vue3'
},
setup
beforeCreate

data 读取 setup 中的属性

data 能够读取 setup 中的属性。请看示例:

     <p><button @click="sayAge">获取年龄</button></p>
+    <p>dataName: {{ dataName }}</p>
   </section>
 </template>

export default {
   },
   data() {
     return {
       age: 18,
+      dataName: this.name
     }
   },
   methods: {

setup 是最早的生命周期(将vue2 中beforeCreat、created合并),这里证明 data 中可以取得 setup 中的数据。就像 vue2 中 data 可以读取 props 中的数据,因为 props 比 data 先初始化 ——
initstate 初始化状态

在 setup 中无法使用 data 中的数据。请看示例,直接报错:

// vscode 报错
let newAge = age,
// vscode 报错 - setup 中没有this
let newAge2 = this.age,

setup 语法糖

每次都得写 setup(),还需要将方法或属性交出去,能否只写属性和方法,自动交出去?

方式1
setup() {
  let name = "pengjiali";
  let date = ref(-1);

  function changeDate() {
    date.value = new Date().getTime();
    console.log("date: ", date);
  }
  // 将数据和方法都交出去
  return { name, date, changeDate };
},

有的。将 setup() 专门提取出去。就像这样:

<script lang="ts">
import { ref } from "vue";
export default {
  name: "App",
};
</script>

<script lang="ts" setup>
// 属性和方法自动交出去

let name = "pengjiali";
let date = ref(-1);

function changeDate() {
  date.value = new Date().getTime();
  console.log("date: ", date);
}
</script>
方式2

方式一还是需要写l了两个
<script>
,其中一个专门用于定义组件名。

<script lang="ts">
import { ref } from "vue";
export default {
  name: "App",
};
</script>

不想写两个
<script>
,可以利用插件
vite-plugin-vue-setup-extend

先安装:

PS \test-projects\hello_vue3> npm i vite-plugin-vue-setup-extend -D
npm WARN deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead

added 3 packages, and audited 67 packages in 6s

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

修改 vite.config.ts 配置文件:

 import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
+import setupExtend from 'vite-plugin-vue-setup-extend'
 
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [
     vue(),
+    setupExtend(),
   ],
   resolve: {
     alias: {

最后应用:

-<script lang="ts" setup>
+<script lang="ts" setup name="App3">

响应式数据

vue2 中放在 data 中的数据都是响应式的,在vue3 中可以通过 ref和reactive 两种方式来处理响应式。

通过 vue devtools,我们知道数据为尊,因为方法放在后面(除了方法,其他的也会放在这里),而数据放在前面。

ref创建基本类型响应式数据

想让哪个数据是响应式的,就将数据用 ref 包裹一下。


:这里的 ref 和 vue2 中 ref 不是一个东西

用法请看示例(和注释):

<template>
  <section>
    <p>name: {{ name }}</p>
    <!-- 不能写 date.value,这里自动会给 value -->
    <p>date: {{ date }}</p>
    <p><button @click="changeDate">change date</button></p>

  </section>
</template>

<script lang="ts" setup name="App">

import { ref } from "vue";

let name = "pengjiali";
// 通过 ref 创建一个基本类型的响应式数据
let date = ref(-1);

// 使用 ref 函数创建的响应式变量是一个包装过的对象,你需要通过 .value 来访问和修改其值
// 使用 ref 创建变量时,实际上你得到的是一个包含了值的对象,而不是直接的值。因此,在修改这个变量时,你需要通过 .value 来访问和修改实际的值,这样 Vue 才能够正确地追踪变化并进行响应。
// 使用 ref 创建的变量必须通过 .value 来访问和修改其值,这是为了确保 Vue 能够正确捕捉变化并更新视图。
function changeDate() {
  // date: RefImpl {__v_isShallow: false, dep: Map(1), __v_isRef: true, _rawValue: -1, _value: -1}
  console.log('date: ', date);
  // 通过 value 修改响应式数据。
  date.value = new Date().getTime();
  console.log("date: ", date);
}
</script>

变量用ref包裹后,类型变成
RefImpl
。需要通过
.value
来访问和修改实际的值。

Tip
:越过 .value 直接整体替换是不可以的,就像这样:

let count = ref(0)

function changeCount(){
  // 生效
  count = 9

  // 失效
  // count = ref(9)
}


:模板中不需要 .value

有点像 proxy 的感觉:

// 创建一个普通的对象作为目标对象
let target = {
  name: 'Alice',
  age: 30
};

// 创建一个 Proxy 对象,用来代理目标对象
let proxy = new Proxy(target, {
  // 拦截属性读取的操作
  get: function(target, property) {
    console.log(`Reading ${property} property`);
    return target[property]; // 返回目标对象相应的属性值
  },
  // 拦截属性设置的操作
  set: function(target, property, value) {
    console.log(`Setting ${property} property to ${value}`);
    target[property] = value; // 设置目标对象相应的属性值
  }
});

// 通过 Proxy 访问和修改属性
// Reading name property
// Alice
console.log(proxy.name); // 读取属性

// Setting age property to 35
// 35
proxy.age = 35; // 设置属性

Tip
:Proxy 是 ES6 引入的一个特性,它允许你创建一个代理对象,可以用来拦截并自定义目标对象的基本操作,比如属性查找、赋值、删除等

reactive 定义响应式数据

利用 reactive 将
对象
转成响应式,重写上述示例:

<template>
  <section>
    <p>name: {{ person.name }}</p>
    <p>date: {{ person.date }}</p>
    <p><button @click="changeDate">change date</button></p>

  </section>
</template>

<script lang="ts" setup name="App">

import { ref, reactive } from "vue";

const person = reactive({
  name: "pengjiali",
  date: -1,
})
function changeDate() {
  // Proxy(Object) {name: 'pengjiali', date: -1}
  console.log('person: ', person);
  person.date = new Date().getTime();
}
</script>

经过 reactive 封装后的对象类型变成 Proxy。专业术语叫
响应式对象

reactive 同样可以处理数组(数组也是对象),请看示例:

<ul>
  <li v-for="(item, index) in ages" :key="index">{{ item }}</li>
</ul>

const ages = reactive([18, 19, 20])

对深层次对象也同样起作用。请看示例:

<p>d: {{ obj.a.b.d }} <button @click="changeD">change d</button></p>

let obj = reactive({
  a: {
    b: {
      d: 10
    }
  }
})

function changeD(){
  obj.a.b.d = new Date().getTime()
}

不能定义基本类型,比如将字符串转成响应式,vscode 和浏览器控制台报错如下:

// vscode:类型“string”的参数不能赋给类型“object”的参数。
// 控制台:value cannot be made reactive: #abc
const color = reactive('#abc');

ref 定义对象类型数据

直接看示例,我们将 reactive 示例中的 reactive 换成 ref,修改值时加 .value 即可,模板不用动。

 import { ref, reactive } from "vue";

-const person = reactive({
+const person = ref({
   name: "pengjiali",
   date: -1,
 })

 function changeDate() {
-  person.date = new Date().getTime();
+  person.value.date = new Date().getTime();
 }

 </script>

能显示,能修改,一切正常。

虽然 ref 能处理基本类型和对象,但是遇到对象,实际上是摇人了。请看示例:

const person = ref({
  name: "pengjiali",
  date: -1,
})

const count = ref(1)
// count: RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 1, _value: 1}
console.log('count: ', count);
// person:  RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: {…}, _value: Proxy(Object)}
console.log('person: ', person);

查看 person 对象的 value 属性,发现了
Proxy(Object)
,所以本质上是 reactive 处理了对象

ref vs reactive

宏观:

  • ref 能定义基本类型和对象的响应式数据
  • reactive 只能用于对象
ref 自动生成 .value

写代码时还得记着是 ref 类型,需要增加 .value,好麻烦。可以使用 vscode 插件:

vscode 直接安装
Vue - Official
(vscode 提示
TypeScript Vue Plugin (Volar)
已弃用,使用 Vue - Official 替代)

通过 vscode 设置,勾选
Auto-complete Ref value with .value
,并设置
Applies to all profiles

重启后,只要输入 ref 变量,则会自动添加 .value,非常方便。

const person = ref({
  name: "pengjiali",
  date: -1,
})
const person2 = reactive({
  name: "pengjiali",
  date: -1,
})
// 输入 person 则会自动添加 .value
person.value

// 对于非 ref 则不会添加 .value
person2
reactive 的局限性

reactive 重新分配一个对象,会失去响应式(可使用 Object.assign 整体替换)。请看示例:

<template>
  <section>
    <p>name: {{ person.name }}</p>
    <p>age: {{ person.age }}</p>
    <p><button @click="changePerson">change person</button></p>
  </section>
</template>

<script lang="ts" setup name="App">

import { ref, reactive } from "vue";

let person = reactive({
  name: "pengjiali",
  age: 18,
})

function changePerson() {
  // 失效 - 响应性连接已丢失!
  // person = reactive({name: 'peng', age: 25})

  // 失效
  // person = {name: 'peng', age: 25}

  // 正常
  Object.assign(person, {name: 'peng', age: 25})
}
</script>

Tip
: Object.assign() 静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并
返回
修改后的目标对象。

let target = {a: 1, b: 2};
let source1 = {b: 4, c: 5};
let source2 = {c: 6, d: 7};

Object.assign(target, source1, source2);

console.log(target); // 输出: {a: 1, b: 4, c: 6, d: 7}

如果是 ref,直接替换即可。就像这样

let person = ref({
  name: "pengjiali",
  age: 18,
})

function changePerson() {
  // 直接替换
  person.value = {name: 'peng', age: 25}
}

ref 和 reactive 使用场景

由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API —— 官网 -
reactive 局限性

笔者习惯:

  • 需要一个基本类型的响应式数据,只可使用 ref
  • 对象使用 reactive
  • 如果是表单,使用 ref 会出现很多 .value,不好看

toRefs

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

不明白请看下面代码。

比如这段代码:

<template>
  <section>
    <p>name: {{ person.name }}</p>
    <p>age: {{ person.age }}</p>
    <p><button @click="changePerson">change person</button></p>
  </section>
</template>

<script lang="ts" setup name="App">

import { ref, reactive } from "vue";

let person = reactive({
  name: "pengjiali",
  age: 18,
})

function changePerson() {
  Object.assign(person, {name: 'peng', age: 25})
}
</script>

我从响应式对象中解构出 age,然后通过方法修改 age 的值,发现页面没更新:

+    <p><button @click="changeAge">change age</button></p>      
     <p><button @click="changePerson">change person</button></p>
   </section>
 </template>
let person = reactive({
   age: 18,
 })

+let {age} = person
+
+function changeAge(){
+  age += 1;
+}
+

这是因为解构出的 age 不在是响应式。可以使用 toRefs,就像这样:

-import { ref, reactive } from "vue";
+import { ref, reactive, toRefs } from "vue";

 let person = reactive({
   name: "pengjiali",
   age: 18,
 })

-let {age} = person
+let {age} = toRefs(person)
+// age: ObjectRefImpl {_object: Proxy(Object), _key: 'age', _defaultValue: undefined, __v_isRef: true}
+console.log('age: ', age);

 function changeAge(){
-  age += 1;
+  age.value += 1;
 }

toRef

说 toRef 用的较少。

比如层级比较深的场景,请看示例:

<template>
  <h4>姓名:{{ name }}</h4>
  <h4>薪资:{{ salary }}</h4>
  <button @click="name += '!'">修改姓名</button>
  <button @click="salary++">涨薪</button>
</template>

<script lang="ts" setup name="App">
import { ref, reactive, toRefs, toRef } from "vue";

let person = reactive({
  name: "张三",
  age: 18,
  job: {
    ja: {
      salary: 20,
    },
  },
});
let name = toRef(person, "name");
let salary = toRef(person.job.ja, "salary");
</script>

计算属性

作用和vue2相同,先回忆下
vue2 中的计算属性
。写法如下:

computed: {
  now: function () {
    
  }
}

改成 vue3 需要使用 computed 方法。就像这样:

let now = computed(() => {
  return Date.now()
})

请看示例:

   <section>
     <p>name: {{ person.name }}</p>
     <p>age: {{ person.age }}</p>
-
+    <p>name_age: {{ name_age }}</p>
     <p><button @click="changePerson">change person</button></p>
   </section>
 </template>

 <script lang="ts" setup name="App">
-import { ref, reactive } from "vue";
+import { ref, reactive, computed } from "vue";

 let person = reactive({
   name: "pengjiali",
   age: 18,
 });

+const name_age = computed(() => `${person.name}-${person.age}`)
 function changePerson() {
   Object.assign(person, { name: "peng", age: 25 });
 }

Tip
:和 vue2 中类似,set很少用。不多介绍,用法大致如下:


let fullname = computed({
  get(){

  },
  set(){

  }
})

// 触发 set 方法
fullName.value = 'li-si'

watch

vue3 中 watch 作用应该和 vue2 中相同,先回忆下vue2 中 watch 写法。就像这样:

new Vue({
  data: {
    message: 'Hello, Vue!'
  },
  watch: {
    message: function(newValue, oldValue) {
      console.log('消息从', oldValue, '变为', newValue);
    }
  }
});

vue3 中说 watch 只能监视4种数据:

  • ref定义的数据
  • reactive 定义的数据
  • 函数返回一个值(getter函数)
  • 一个包含上述内容的数组

Tip
:
vue2 watch
中有deep、immediate、unwatch,下文 vue3 中 watch 也都有。

ref 基本类型

请看示例:

<template>
  <section>
    <p>age: {{ age}}</p>
    <p><button @click="age += 1">change age</button></p>
    <p><button @click="stopWatch">停止监听 age 变化</button></p>
  </section>
</template>

<script lang="ts" setup name="App">
import { ref, watch } from "vue";

let age = ref(18)
// watch(age.value, ... ) 错误写法 
let stopWatch = watch(age, (newValue, oldValue) => {
  console.log('年龄从', oldValue, '变为', newValue);
});

</script>
  • watch 监视的ref变量,无需增加
    .value
    。安装好vscode 插件,在这种情况下也不会自动给你加 .value。
  • watch 返回一个函数,执行后将解除监视。就像 vue2 中的 vm.$watch 方法,返回 unwatch。

ref 对象类型

核心语法:

watch(person, (newValue, oldValue) => {

}, { deep: true});

比如用 ref 定义一个对象,里面有两个按钮,一个只改变“年龄”,一个改变整个 ref 对象。就像这样:

<template>
  <section>
    <p>name: {{ person.name }}</p>
    <p>age: {{ person.age }}</p>
    <p><button @click="person.age += 1">change age</button></p>
    <p><button @click="changePerson">change person(替换整个对象)</button></p>
  </section>
</template>

<script lang="ts" setup name="App">
import { ref, watch } from "vue";

let person = ref({
  name: "pengjiali",
  age: 18,
});

// 完全替换person,newValue 和 oldValue 不同
// 只替换person中属性,newValue 和 oldValue 相同。通常工作只关心新值
watch(person, (newValue, oldValue) => {
      console.log('Person changed');
      console.log('New person:', newValue);
      console.log('Old person:', oldValue);
    }, );

function changePerson() {
  person.value = {name: 'peng', age: 100}
}
</script>

只有改变整个对象时 watch 中的方法才会执行,而改变ref对象中的属性,watch 方法却不会执行。

加上一个配置项,这样改变整个对象,以及改变ref对象中的属性,watch 中的方法都会执行。

       console.log('New person:', newValue);
       console.log('Old person:', oldValue);
-    }, );
+    }, {deep: true});

其实还有一个属性 immediate,初始时就会执行 watch 中的方法。就像这样:

// 完全替换person,newValue 和 oldValue 不同
// 只替换person中属性,newValue 和 oldValue 相同。通常工作只关心新值
watch(person, (newValue, oldValue) => {
      console.log('Person changed');
      console.log('New person:', newValue);
      console.log('Old person:', oldValue);
    }, { deep: true, immediate: true });

reactive

核心语法:

watch(person, (newValue, oldValue) => {
     
});

完整示例:

<template>
  <section>
    <p>name: {{ person.name }}</p>
    <p>age: {{ person.age }}</p>
    <p><button @click="person.age += 1">change age</button></p>
    <p><button @click="changePerson">change person(替换整个对象)</button></p>
  </section>
</template>

<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";

let person = reactive({
  name: "pengjiali",
  age: 18,
});

// 默认开启深度监听,而且通过 {deep: false} 也关闭不了
watch(person, (newValue, oldValue) => {
      console.log('Person changed');
      console.log('New person:', newValue);
      console.log('Old person:', oldValue);
    }, {deep: false});

function changePerson() {
  // 不能整个替换,只能用 Object.assign。不能像 ref.value = {...} 
  Object.assign(person, {name: 'peng', age: 100})
}
</script>

监视 ref 或 reactive 的对象中的某属性

前面我们监视的都是整个对象,比如现在要监视对象中的某个属性。这里分为
基本类型

对象类型

// reactive 和 ref 都可以用如下形式
// 利用 getter。如果需要则增加 deep
watch(() => person.car, () => {

}, {deep: true})
基本类型

就以 reactive 对象为例,直接将监视源改为
person.name
vscode 就会出现红色波浪线:

<template>
  <section>
    <p>name: {{ person.name }}</p>
    <p>age: {{ person.age }}</p>
    <p><button @click="person.age += 1">change age</button></p>
    <p><button @click="person.name += '~'">change name</button></p>
  </section>
</template>

<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";

let person = reactive({
  name: "pengjiali",
  age: 18,
});

-watch(person, (newValue, oldValue) => {
+watch(person.name, (newValue, oldValue) => {
       console.log('Person changed');
 });

</script>

运行后在浏览器控制台中报错更明显:

// 无效的监视源:只能是 getter 函数、ref、reactive object、或这些类型的数组
App.vue:17 [Vue warn]: Invalid watch source:  pengjiali A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types. 
  at <App>

现在 person.name 不属于上述4种类型。

将 person.name 改成 getter。代码如下:

Tip
:getter 一个函数,返回一个值 ——
vue3 watch

watch(() => person.name, (newValue, oldValue) => {
      console.log('Person changed');
});

这样修改 age 时不会触发 watch,只有 name 改变时才会触发 watch。

对象类型

这里给 person 定义了一个 jineng 的对象属性,并定义两个按钮,一个会改变 jineng 的属性,一个改变整个技能。代码如下:

<template>
  <section>
    <p>jineng.a: {{ person.jineng.a }}</p>
    <p><button @click="person.jineng.a += '~'">change jineng.a</button></p>
    <p><button @click="changeJineng">替换 jineng</button></p>

  </section>
</template>

<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";
let person = reactive({
  name: "pengjiali",
  age: 18,
  jineng: {
    a: '吃饭',
    b: '睡觉',
  }
});
console.log('person: ', person);
// person.jineng:  Proxy(Object) {a: '吃饭', b: '睡觉'}
console.log('person.jineng: ', person.jineng);
function changeJineng(){
  person.jineng = {a: 'a吃饭', b:'a睡觉'}
}
</script>

首先我们这么写,发现只能监听 jineng 里面的属性改变:

// 点击`change jineng.a` 执行
// 点击`替换 jineng` 不执行
watch(person.jineng, () => {
  console.log('watch jineng');
})

Tip
:通过打印我们知道 person.jineng 类型是Proxy,也就是 reactive 类型,根据前文我们知道 reactive 默认开启深度监视,而且不能整个替换,之前用的都是 Object.assign,这里用的是
person.jineng = {a: 'a吃饭', b:'a睡觉'}

改成 getter 发现只能监听替换整个 jineng:

// 点击`change jineng.a` 不执行
// 点击`替换 jineng` 执行
watch(() => person.jineng, () => {
  console.log('watch jineng');
})

在 getter 基础上增加 {deep: tree} 则都能监视到:

// 点击`change jineng.a` 执行
// 点击`替换 jineng` 执行
// 说官网一直都是用函数
watch(() => person.jineng, () => {
  console.log('watch jineng');
}, {deep: true})

Tip
:将上述示例从 reactive 改成 ref,watch 监视方式还是不变。请看代码:

<template>
  <section>
    <p>jineng.a: {{ person.jineng.a }}</p>
    <p><button @click="person.jineng.a += '~'">change jineng.a</button></p>
    <p><button @click="changeJineng">替换 jineng</button></p>

  </section>
</template>

<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";
let person = ref({
  name: "pengjiali",
  age: 18,
  jineng: {
    a: '吃饭',
    b: '睡觉',
  }
});
// person.jineng:  Proxy(Object) {a: '吃饭', b: '睡觉'}
console.log('person.jineng: ', person.value.jineng);
function changeJineng(){
  person.value.jineng = {a: 'a吃饭', b:'a睡觉'}
}

watch(() => person.value.jineng, () => {
  console.log('watch jineng');
}, {deep: true})

</script>

监视多个

核心语法:

watch([() => xx.name, () => xx.xx.age], (newValue, oldValue) {
  // newValue oldValue 是整个数组
})

// 通常这么写
watch([() => xx.name, () => xx.xx.age], (value) {
  const [name, age] = value;
  // ...
})

前面几种学完了,监视多个就是赠送。请看示例:

<template>
  <section>
    <p>age: {{ person.age }}</p>
    <p>jineng.a: {{ person.jineng.a }}</p>
    <p><button @click="person.age += 1">change age</button></p>
    <p><button @click="person.jineng.a += '~'">change jineng.a</button></p>
    <p><button @click="changeJineng">替换 jineng</button></p>

  </section>
</template>

<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue"; 
let person = reactive({
  name: "pengjiali",
  age: 18,
  jineng: {
    a: '吃饭',
    b: '睡觉',
  }
});
function changeJineng(){
  person.jineng = {a: 'a吃饭', b:'a睡觉'}
}

watch([() => person.name, () => person.jineng.a], (newVal, oldVal) => {
  console.log('newVal: ', newVal, 'oldVal: ', oldVal);
})
</script>

总结

用的较多的有:

  • ref 基本类型
  • 监视对象中某个属性,反手就是一个函数,无论是基本类型、ref还是reactive都可以。

watchEffect

核心语法:

// watchEffect 是一个立即执行的副作用操作,因此回调函数会在组件渲染时立即执行一次,并在每个相关响应式数据变化时再次执行。
watchEffect(() => {
   // 立即执行
  console.log('立即执行');
  if(temp.value > 60 || height.value >80){
    ...
  }
})

比如我需要在”温度“和”高度“大于20的时候发出请求,用 watch 可以这么实现:

<template>
  <section>
    <p>Temperature: {{ temp }}</p>
    <p>Height: {{ height }}</p>
    <button @click="increaseTemp">Increase Temperature by 10</button> <br>
    <button @click="increaseHeight">Increase Height by 10</button>
  </section>
</template>

<script lang="ts" setup name="App">
import { reactive, ref, watch, watchEffect } from "vue";
const temp = ref(0);
const height = ref(0);

const increaseTemp = () => {
  temp.value += 10;
};

const increaseHeight = () => {
  height.value += 10;
};

watch([temp, height], (val) => {
  const [temp, height] = val
  // console.log('val: ', val);
  if (temp > 20 || height > 20) {
    // 在条件满足时执行副作用代码
    console.log("watch: Temperature is greater than 20 or height is greater than 20", temp, height);
    // 可以在这里进行一些逻辑处理
  }
})
</script>

可以直接替换成 watchEffect(变量直接用就好,框架会自动帮你监视),效果和上述例子相同,但代码量少一些。

watchEffect(() => {
  if (temp.value > 20 || height.value > 20) {
    // 在条件满足时执行副作用代码
    console.log("Temperature is greater than 20 or height is greater than 20", temp.value, height.value);
  }
});

Tip
:笔者最初测试 watchEffect 时遇到了问题。笔者认为每次修改 watchEffect 监视的变量应该都会执行,如果条件有满足的就应该触发,但没有。后来用 watch 重写后发现 watchEffect 又符合期望。可能是本地环境出了点问题。

ref

vue2 中 ref 可以作用于
普通dom元素
,也可以作用于
vue 组件
。vue3 中也是这样。

普通dom元素

请看示例:

<template>
  <section>
    <p ref="pElem">hello</p>
    <button @click="say">click</button>
  </section>
</template>

<script lang="ts" setup name="App">
import { reactive, ref } from "vue";
// 创建一个容器
const pElem = ref();

function say(){
  // <p>hello</p>
  console.log(pElem.value)
}
</script>

点击按钮,将打印 p 元素。

vue 组件

下面我们定义个组件 Dog.vue,然后在 App.vue 引入。请看示例:

在 Dog 中定义了两个变量,通过 defineExpose 将 a 交出去:

// Dog.vue
<template>
  <section>
    dog
  </section>
</template>

<script lang="ts" setup name="Dog">
import { ref } from 'vue';

const a = ref(1)
const b = ref(2)

// 无需引入
defineExpose({a})
</script>

Tip
: defineExpose 是一个用于在组合式 API 中将组件的属性或方法暴露给父组件的函数。它可以让父组件直接访问子组件内部的属性和方法。

App.vue 中直接引入Dog,无需注册即可使用,然后用 ref 指向 Dog,点击按钮则能通过 ref 访问 Dog 暴露出的变量:

// App.vue:
<template>
  <section>
    <Dog ref="box"/>
    <p><button @click="handle1">click</button></p>
  </section>
</template>

<script lang="ts" setup name="App">
// 自动帮你注册组件
import { ref } from "vue";
// 笔者 vscode 在Dog这里有红色波浪线,提示:vue3 Module has no default export.Vetur(1192),笔者将 Vetur 插件卸载重启就好了。
// vetur 可以高亮 .vue 文件,禁用后,笔者安装 Vue - Official 插件也能高亮 .vue 
import Dog from "./Dog.vue";

const box = ref()

function handle1(){
  // Proxy(Object) {a: RefImpl, __v_skip: true}
  console.log('box.value: ', box.value);
  // 1
  console.log(box.value.a);
}
</script>

Tip
: 这里的 ref 用法和 react 中的
create Ref
用法很像。

简单引入 TypeScript

Tip
:若不熟悉 ts,可以参考:
前端 Typescript 入门

新建 ts 文件,定义两个类型:

// src/types/index.ts
export interface Person{
    name: string,
    age: number,
}

export type Persons = Person[]

App.vue 引用 ts 类型:


<script lang="ts" setup name="App">

//  注:需要增加 type,因为这是一个规范,不是值
import { type Person, type Persons } from '@/types';

let p2: Person = { name: 'peng', age: 18 }

//   let persons: Person[] = [
//   let persons: Array<Person> = [
let persons: Persons = [
  { name: 'peng', age: 18, id: 3 },
  { name: 'peng2', age: 18 },
  { name: 'peng3', age: 18 }
]

</script>

Tip
:由于 Person 是一个规范,所以引入时需要增加 type,告诉自己这是一个规范。笔者中间报错:
文件 "...../src/types/index.ts" 不在项目 "...../tsconfig.app.json" 的文件列表中。项目必须列出所有文件,或使用 "include" 模式
。其实不用到 tsconfig.app.js 中修改 include 属性值。


:将类型用于 reactive,可使用如下泛型形式:

<script lang="ts" setup name="App">
import { ref, reactive, computed } from "vue";

import {type Person, type Persons} from '@/types';

// 笔者这样写不起作用。多了 id 也没给错误提示。
let person:Persons = reactive([
  { name: 'peng', age: 18, id: 3},
  { name: 'peng2', age: 18},
  { name: 'peng3', age: 18}
]);

// 调用函数 reactive 时增加泛型,会提示: “id”不在类型“Person”中
let person2 = reactive<Persons>([
  { name: 'peng', age: 18, id: 3},
  { name: 'peng2', age: 18},
  { name: 'peng3', age: 18}
]);

</script>

props

父组件通过 props 给子组件传递数据,而通过事件给子组件传递方法。

首先回顾下
vue2 props

核心语法

// 孩子没脾气
defineProps(['persons'])

// 接收+限制类型
defineProps<{persons:Persons}>()

// 接收+限制类型+限制必要性 —— 可以不传
defineProps<{persons?:Persons}>()

// 接收+限制类型+限制必要性+默认值
import {withDefaults} from 'vue'
withDefaults(defineProps<{persons?:Persons}>(), {
  persons: () => []
})

defineProps 基本使用

父组件传递两个属性:

<template>
  <section>
  <Dog name="peng" :age="18"/>
  </section>
</template>

<script lang="ts" setup name="App">
import Dog from './Dog.vue'

</script>

子组件通过 defineProps 接收(较vue2 中更灵活),可以直接在模板中使用:

<template>
  <section>
    
    <p>props.name:{{ props.name}}</p>
    <!-- props 在模板中直接用即可 -->
    <p>name:{{ name}}</p>
    <p>{{ props.age}}</p>
  </section>
</template>

<script lang="ts" setup name="Dog">
import { ref } from 'vue';
import { defineProps } from 'vue';
const props = defineProps(['name', 'age'])
// props: Proxy(Object) {name: 'peng', age: 18}
console.log('props: ', props);

if(props.age < 20){
  // 在 Vue 2.x 中,子组件不应该直接修改 props。单向数据流,如果需要修改可以让父组件修改
  // props.age += 10;
}


</script>

有两点:

  • defineProps 接收一个也需要数组
// 即使一个也需要使用数组。
// 没有与此调用匹配的重载。
// const props2 = defineProps('name')
  • defineProps 返回值能接收 props
  • 子类中不能修改 props 属性,否则报错

传递一个对象给子组件,核心代码如下:

// 父组件
 <Dog name="peng" :persons="persons"/>

  let persons = reactive<Persons>([
    { name: 'peng', age: 18},
    { name: 'peng2', age: 19},
    { name: 'peng3', age: 20}
  ]);
// 子组件
<ul>
  <li v-for="(p, index) in persons" :key="index">{{ p.name }} - {{ p.age }}</li>
</ul>


import { defineProps } from 'vue';
defineProps(['persons'])

接收+限制类型+限制必要性+默认值

vue3官网针对 ts 写法介绍如下:

const props = defineProps<{
  foo: string
  bar?: number
}>()

请看示例:

// 子组件
<template>
  <section>
    <ul>
      <li v-for="(p, index) in persons" :key="index">{{ p.name }} - {{ p.age }}</li>
    </ul>
   
  </section>
</template>

<script lang="ts" setup name="Dog">
import { withDefaults } from 'vue';
import {type Persons} from '@/types'

// 只接收。不传也不报错
// defineProps(['persons'])

// 接收 persons + 类型。不传,父组件报错:Missing required prop: "persons" 
// defineProps<{persons:Persons}>()

// 接收 persons + 类型 + 必要性
// 通过 ? 表明可以不传,父组件不会报错
// defineProps<{persons?:Persons}>()
withDefaults(defineProps<{persons?:Persons}>(), {
  // 注:默认值需要通过函数返回,类似 vue2 中数据返回也是通过函数
  // persons: [{ name: '空2', age: 0},]
  persons: () => [{ name: '空', age: 0},]
})
</script>


:defineExpose、defineProps 不需要引入。
define
XXx 是宏函数,在vue3 中无需引入(笔者自测通过)。

生命周期

创建、挂载、更新、销毁

vue2

4个阶段,8个钩子:

  1. 创建阶段:
    beforeCreate: 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
    created: 实例已经创建完成之后被调用。在这一步,实例已经完成了数据观测、属性和方法的运算,但是尚未开始挂载到页面上。

  2. 挂载阶段:
    beforeMount: 在挂载开始之前被调用:相关的 render 函数首次被调用。
    mounted: el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。

  3. 更新阶段:
    beforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
    updated: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。

  4. 销毁阶段:
    beforeDestroy: 在实例销毁之前调用。实例仍然完全可用。
    destroyed: 在实例销毁之后调用。该钩子被调用时,Vue 实例的所有指令都被解绑定,所有事件监听器被移除,所有子实例也被销毁。

vue3

和 vue2 类似,4个阶段,8个钩子,但稍微有差异:

  • 没有 beforeCreat、created,由 setup 替代(setup 比 beforeCreat 早)
  • beforeMount 改成 onBeforeMount,mounted 改成 onMounted (加
    on和驼峰
    ,其他几个也这样)
  • 销毁改成
    卸载
    ,和挂载对应
  • 生命周期钩子函数语法改为
    function onBeforeMount(callback: () => void): void

Tip
:这只是最基本的生命周期钩子,比如路由也会有,现学现查即可。

请看示例:

// 子组件
<template>
  <section>
    {{ count }}
    <button @click="count += 1">change count</button>
  </section>
</template>

<script lang="ts" setup name="Dog">
import {onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue'

import {ref} from 'vue'

let count = ref(0);

// 没有 beforeCreat、created,由 setup 替代
console.log('创建')


onBeforeUpdate(() => {
  console.log('更新前');
})
onUpdated(() => {
  console.log('更新后');
})
onBeforeUnmount(() => {
  console.log('卸载前');
})
onUnmounted(() => {
  console.log('卸载后');
})

// 故意将其放在末尾
onBeforeMount(() => {
  console.log('挂载前');
})
onMounted(() => {
  console.log('挂载后');
})
</script>
// 父组件
<template>
  <section>
    <Dog name="peng" v-if="isShow" />
    <button @click="isShow = !isShow">toggle show</button>
  </section>
</template>

<script lang="ts" setup name="App">
import Dog from './Dog.vue'

import { ref } from "vue";

let isShow = ref(true)
</script>

点击“change count”更新子组件,点击“toggle show”会销毁或创建子组件。控制台输出:

创建
挂载前
挂载后
更新前
更新后
卸载前
卸载后

vue3 父组件子组件生命周期

在上述示例中,我们给父组件也增加对应的生命周期,然后看一下生命周期钩子函数的顺序。

// 父组件
<template>
  <section>
    <Dog name="peng" v-if="isShow" />
    <button @click="isShow = !isShow">toggle show</button>
  </section>
</template>

<script lang="ts" setup name="App">
import Dog from './Dog.vue'

import {onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue'
import { ref } from "vue";

let isShow = ref(true)

let count = ref(0);

console.log('父创建')

onBeforeUpdate(() => {
  console.log('父更新前');
})
onUpdated(() => {
  console.log('父更新后');
})
onBeforeUnmount(() => {
  console.log('父卸载前');
})
onUnmounted(() => {
  console.log('父卸载后');
})

// 故意将其放在末尾
onBeforeMount(() => {
  console.log('父挂载前');
})
onMounted(() => {
  console.log('父挂载后');
})

</script>

页面初始时输出如下(和 vue2 中父元素、子元素的顺序一致,甚至和 react 中几乎相同):

父创建
父挂载前

子创建
子挂载前
子挂载后

父挂载后

点击“change count”,输出:
子更新前 子更新后

点击“toggle show”,输出(和笔者猜测相同):

父更新前

子卸载前
子卸载后

父更新后

常用生命钩子

在实际开发中,
最常用
的 Vue2 生命周期钩子函数通常是:

  • created: 在实例创建完成后立即调用,通常用于进行初始化操作、
    数据请求
    等。
  • mounted: 在实例挂载到 DOM 后调用,通常用于执行
    DOM 操作
    、访问第三方库等与 DOM 相关的操作。
  • updated: 当数据更新导致虚拟 DOM 重新渲染和打补丁后调用,通常用于在更新后执行一些需要基于 DOM 的操作。
  • beforeDestroy: 在实例销毁之前调用,通常用于进行一些清理工作、
    清除定时器

    解绑事件监听器
    等。

vue3 中常用的和vue2 类似:
onMounted

onUpdated

onBeforeUnmount

自定义hooks

在 Vue 3 中,自定义 Hooks 是指开发者自己定义的可复用的 Hooks 函数,用于组件逻辑的封装和复用。

前面我们说到 vue3 的组合式Api,比如下面这个 vue 包含两部分的功能,我们利用 hooks 将其拆分:

<template>
  <main>
    <section>
    功能A:
    <p>name: {{ name }}</p>
    <p><button @click="changeName">change name</button></p>
  </section>

  <section>
    功能B:
    <p>age: {{ age }}   放大2倍: {{ bigAge }}</p>
    <p><button @click="changeAge">change age</button></p>
  </section>
  </main>

</template>

<script lang="ts" setup name="App">

import {onMounted, computed} from 'vue'
import { ref } from "vue";

let name = ref('peng');

let age = ref(18);

function changeName(){
  name.value += '~'
}

function changeAge(){
  age.value += 1
}

let bigAge = computed(() => {
  return age.value * 2
})
onMounted(() => {
  age.value += 2
  console.log('B mounted: 将age增加2')
})
</script>

使用 hooks 将变成如下样子:

  • App.vue
<template>
  <main>
   // 不变
  </main>

</template>

<script lang="ts" setup name="App">
import FunctionA from '@/hooks/FunctionA'
import FunctionB from '@/hooks/FunctionB'

// 引入功能A
const {name, changeName} = FunctionA()
// // 引入功能B
const {age, changeAge, bigAge,} = FunctionB()
</script>
  • FunctionA.ts - 将app.vue 中的
    <script
    代码复制,放入
    function
    中,需要
    导出
    (export)和
    暴露
    (return)
// 所有功能A的数据、方法、钩子、计算属性等等
import { ref } from "vue";

// 必须导出
// 用函数包裹
export default function () {
    let name = ref('peng');

    function changeName() {
        name.value += '~'
    }

    // 暴露出去
    return {name, changeName}
}
  • FunctionB.ts
import { onMounted, computed } from 'vue'
import { ref } from "vue";

export default function () {
    let age = ref(18);

    function changeAge() {
        age.value += 1
    }

    let bigAge = computed(() => {
        return age.value * 2
    })
    onMounted(() => {
        age.value += 2
        console.log('B mounted: 将age增加2')
    })

    return {age, changeAge, bigAge,}
}

这样运行后效果和改动之前完全相同。

Tip
:自定义 Hooks 函数本身不能直接包含模板代码,因为 Hooks 函数主要用于组件逻辑的封装和复用,并不包含视图逻辑。

vs vue2中的mixin

相似:组件逻辑的封装和复用

不同:

  • 作用域不同:混入的逻辑会影响到所有使用该 Mixin 的组件,自定义 Hooks 由开发者有选择地在组件中引入和使用
  • 组织逻辑方式不同:Mixin 通常将逻辑分散在多个生命周期钩子函数中,自定义 Hooks 将逻辑封装在一个函数中,更加集中

其他

Vue Devtools

vue-devtools是一款基于chrome游览器的插件,可以用于调试vue应用

有两种方法安装,一种是有条件,直接通过 chrome 应用商店搜索 vue 安装

没有条件的,可以访问
极简插件
,搜索,下载,解压后,打开 chrome 开发者模式,拖拽到扩展插件

三个花括号

在使用 vscode 编写 vue 模板时,有时会遇到输入一个花括号
{
,结果出现三个花括号
{ }}
,还得手动删除一个,非常烦人。

解决办法:
打开设置 -> 搜索Closing Brackets -> 找到 Auto Closing Brackets -> 将 aways 改成beforeWhitespace
。请看下图

可以多个根标签

<Person/>
<Person/>
<Person/>

在学习的过程中,我们总是容易遇到很多晦涩难懂和复杂了,尤其是我熟悉的编程领域。就像我很喜欢的一本书《从一到无穷大》说的那样,但其实很多概念本身并没有多复杂,只是解读的人喜欢添油加醋而已,

其实很多看似隐晦,高大上的专业名词和概念,都可以用简单的比喻的方式就很容易让人理解。

例如信息安全领域的中间人攻击 MITM 概念,很多书籍和博客都讲不清楚,更有甚者用一堆更专业晦涩的名词来解释名词,越看越头疼。让人想要放弃。

对于这个概念其实可以用一个简单的比喻来解释,例如用一个大家都知道的童话故事的比喻来表达如下:

  1. 外婆告诉小红帽今天要来她家(建立连接)
  2. 但是外婆在路上被大灰狼吃掉了(中间人信息拦截)
  3. 然后大灰狼伪装成外婆的模样(中间人信息篡改)
  4. 大灰狼伪装成外婆欺骗小红帽开门(伪装凭证)
  5. 最后小红帽就这样被骗了(中间人到获取信息)

这就是整个过程了。其实很简单,其实并不难,总是有太多的人喜欢把简单的东西搞的复杂。好像不复杂就无法体现自己的水平,历史已经无数次证明,过度的复杂最终只是沦落为自娱自乐,例如 EJB 框架,SOA 架构等等。过于复杂只有你自己能理解,你只能自己玩,大家参与不进来,这样的复杂度完全没有任何意义。

本文想强调一个核心理念:保持简单。在很多时候,我们经常遇到复杂的概念和系统,但这并不意味着我们的解释和学习方法也需要同样复杂。通过简单的方式去学习和理解事物。简单化并不等同于缩水或降低标准,而是尝试去找到事务的最核心的本质的点来进行传达。(要相信再复杂的事物,它的本质也是简单的,这是大道至简的道理)这种方法有助于可以激发人们的兴趣,降低门槛。用简单的语言表达复杂的思想,用生活中的比喻解释专业的概念。还能促进普及和传播,让更多的人受益。让我们一起努力,做一个简单的人。

搞过自动化测试的小伙伴,相信都知道,在Web自动化测试中,有一款自动化测试神器工具:
selenium
。结合标准的WebDriver API来编写Python自动化脚本,可以实现解放双手,让脚本代替人工在Web浏览器上完成指定的操作。

虽然
selenium
有完备的文档,但也需要一定的学习成本,对于一个纯小白来讲还是有些门槛的。

最近,
微软开源
了一个非常强大的自动化项目叫
playwright-python
,项目地址:

https://github.com/microsoft/playwright-python

它支持主流的浏览器,包含:
Chrome

Firefox

Safari

Microsoft Edge
等,同时支持以
无头模式

有头模式运行
,并提供了
同步

异步
的 API,可以结合
Pytest
测试框架使用,并且支持浏览器端的自动化脚本录制。

而对于Python爱好者来说,还有一个更大的福利,这个项目是针对Python语言的纯自动化工具,
可以做到,连一行代码都不用写,就能实现自动化功能
。听起来,简直太碉堡了!

可能你会觉得有点不可思议,真的不用写一行代码吗?但它真的就是这么厉害。下面我们一起看下这个神器。

1.
Playwright介绍

Playwright是一个强大的Python库,仅用一个API即可自动执行
Chromium

Firefox

WebKit
等主流浏览器自动化操作,并同时支持以
无头模式

有头模式
运行。

Playwright提供的自动化技术是绿色的、功能强大、可靠且快速,支持
Linux

Mac
以及
Windows
操作系统。

官网:

https://playwright.dev/

从官网的解释,官方给
Playwright
定位是一款真正意义上的Web端到端测试工具。

2.
Playwright使用

2.1 安装

Playwright功能强大,但它的安装步骤,非常简单,只需要 2 步:

第 1 步,安装 playwright-python 依赖库
(需要注意的是,playwright库需要依赖Python3.7+以上)

# 安装依赖库

➜  ~ pip3 install playwright
Looking in indexes: https://pypi.douban.com/simple
Collecting playwright
  Downloading https://pypi.doubanio.com/packages/08/f0/9f937ccff3221685d4a8bd406649c85855b9b6a2fafe75920b02151b48e0/playwright-0.162.2-py3-none-macosx_10_13_x86_64.whl (58.2 MB)
     |████████████████████████████████| 58.2 MB 1.6 MB/s
Collecting greenlet==1.0a1
  Downloading https://pypi.doubanio.com/packages/aa/74/6e93515873829a8d894863bbae1d709405bdd50d66fdf239480cc9db0598/greenlet-1.0a1-cp38-cp38-macosx_10_9_x86_64.whl (86 kB)
     |████████████████████████████████| 86 kB 6.9 MB/s
Collecting typing-extensions
  Downloading https://pypi.doubanio.com/packages/60/7a/e881b5abb54db0e6e671ab088d079c57ce54e8a01a3ca443f561ccadb37e/typing_extensions-3.7.4.3-py3-none-any.whl (22 kB)
Collecting pyee>=8.0.1
  Downloading https://pypi.doubanio.com/packages/0d/0a/933b3931107e1da186963fd9bb9bceb9a613cff034cb0fb3b0c61003f357/pyee-8.1.0-py2.py3-none-any.whl (12 kB)
Installing collected packages: greenlet, typing-extensions, pyee, playwright
Successfully installed greenlet-1.0a1 playwright-0.162.2 pyee-8.1.0 typing-extensions-3.7.4.3

可以在
https://pypi.org/project/playwright/
查看它的依赖版本信息。

第 2 步,安装主流的浏览器驱动

这样,会将 Chromeium、Firefox、Webkit 浏览器驱动下载到本地

# 安装浏览器驱动(安装过程稍微有点慢,请耐心等待)
➜  ~ python3 -m playwright install
Downloading chromium v827102 - 121.3 Mb [====================] 100% 0.0s
chromium v827102 downloaded to /Users/xxx/Library/Caches/ms-playwright/chromium-827102
Downloading firefox v1205 - 74.1 Mb [                    ] 1% 37767.9s

3.
如果想查看
Playwright
支持的功能

, 可以直接在命令行输入:

➜  ~ python3 -m playwright help
Usage: index [options] [command]

Options:
  -V, --version                          output the version number
  -b, --browser <browserType>            browser to use, one of cr, chromium, ff, firefox, wk,
                                         webkit (default: "chromium")
  --color-scheme <scheme>                emulate preferred color scheme, "light" or "dark"
  --device <deviceName>                  emulate device, for example  "iPhone 11"
  --geolocation <coordinates>            specify geolocation coordinates, for example
                                         "37.819722,-122.478611"
  --lang <language>                      specify language / locale, for example "en-GB"
  --proxy-server <proxy>                 specify proxy server, for example "http://myproxy:3128" or
                                         "socks5://myproxy:8080"
  --timezone <time zone>                 time zone to emulate, for example "Europe/Rome"
  --timeout <timeout>                    timeout for Playwright actions in milliseconds (default:
                                         "10000")
  --user-agent <ua string>               specify user agent string
  --viewport-size <size>                 specify browser viewport size in pixels, for example "1280,
                                         720"
  -h, --help                             display help for command

Commands:
  open [url]                             open page in browser specified via -b, --browser
  cr [url]                               open page in Chromium
  ff [url]                               open page in Firefox
  wk [url]                               open page in WebKit
  codegen [options] [url]                open page and generate code for user actions
  screenshot [options] <url> <filename>  capture a page screenshot
  pdf [options] <url> <filename>         save page as pdf
  install                                Ensure browsers necessary for this version of Playwright
                                         are installed
  help [command]                         display help for command

从命令行帮助信息中可以看出,
Playwright
支持的功能相当丰富!

3. 实操演示

开篇就提到,使用Playwright无需写一行代码,我们只需手动操作浏览器,它会录制我们的操作,然后自动生成代码脚本。

3.1 录制脚本

我们先查看录制脚本的命令说明

➜  ~ python3 -m playwright codegen --help
Usage: index codegen [options] [url]

open page and generate code for user actions

Options:
  -o, --output <file name>  saves the generated script to a file
  --target <language>       language to use, one of javascript, python, python-async, csharp (default: "python")
  -h, --help                display help for command

Examples:

  $ codegen
  $ codegen --target=python
  $ -b webkit codegen https://example.com

其中

  • python -m playwright codegen 录制脚本

  • --help 帮助文档

  • -o 生成自动化脚本的目录

  • --target 脚本语言,包含 JS 和 Python,分别对应值为:python 和 javascript

  • -b 指定浏览器驱动

比如,我要在baidu.com搜索,用chromium驱动,将结果保存为mikezhou.py的python文件。

# 我们通过下面命令打开 Chrome 浏览器开始录制脚本
# 指定生成语言为:Python(默认Python,可选)
# 保存的文件名:mikezhou.py(可选)
# 浏览器驱动:webkit(默认webkit,可选)
# 最后跟着要打开的目标网站(默认仅仅是打开浏览器,可选)
python3 -m playwright codegen --target python -o 'mikezhou.py' -b chromium https://www.baidu.com

命令行输入后会自动打开浏览器,然后可以看见在浏览器上的一举一动都会被自动翻译成代码,如下所示:

最后,自动化脚本会自动生成,保存到文件中
mikezhou.py
, 且上述所有的人工操作,都会被自动转化成代码:

from playwright import sync_playwright

def run(playwright):
    browser = playwright.chromium.launch(headless=False)
    context = browser.newContext()

    # Open new page
    page = context.newPage()

    # Go to https://www.baidu.com/
    page.goto("https://www.baidu.com/")

    # Click input[name="wd"]
    page.click("input[name=\"wd\"]")

    # Fill input[name="wd"]
    page.fill("input[name=\"wd\"]", "禾目大")

    # Press CapsLock
    page.press("input[name=\"wd\"]", "CapsLock")

    # Fill input[name="wd"]
    page.fill("input[name=\"wd\"]", "自动化测试实战宝典 ")

    # Press Enter
    page.press("input[name=\"wd\"]", "Enter")
    # assert page.url() == "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95%E5%AE%9E%E6%88%98%E5%AE%9D%E5%85%B8%20&fenlei=256&rsv_pq=af40e9aa00012d5a&rsv_t=c659gpz2%2Fjri1SAoIXdT9gP%2BmrqufXzRtMSSAL0n0fv7GSoLF5vaiNVPA3U&rqlang=cn&rsv_enter=1&rsv_dl=tb&rsv_sug3=38&rsv_sug1=22&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&inputT=8034&rsv_sug4=9153"

    # Close page
    page.close()

    # ---------------------
    context.close()
    browser.close()

with sync_playwright() as playwright:
    run(playwright)

3.2 支持同步

同步的关键字为:
sync_playwright

比如,我们依次使用三个浏览器内核打开浏览器,然后百度一下,接着对在搜索界面截图,最后关闭浏览器

from time import sleep
from playwright import sync_playwright

# 注意:默认是无头模式
with sync_playwright() as p:
    # 分别对应三个浏览器驱动
    for browser_type in [p.chromium, p.firefox, p.webkit]:

        # 指定为有头模式,方便查看
        browser = browser_type.launch(headless=False)
        page = browser.newPage()
        page.goto('http://baidu.com')

        # 执行一次搜索操作
        page.fill("input[name=\"wd\"]", "自动化测试实战宝典")
        with page.expect_navigation():
            page.press("input[name=\"wd\"]", "Enter")

        # 等待页面加载完全
        page.waitForSelector("text=搜索工具")
        
        # 截图
        page.screenshot(path=f'test-{browser_type.name}.png')

        # 休眠3s
        sleep(3)

        # 关闭浏览器
        browser.close()

需要指出的是,
playwright-python
内置的 API 基本上囊括常见的自动化操作

3.3 支持异步

异步步的关键字为:
async_playwright
,异步操作可结合asyncio同时进行三个浏览器操作。

import asyncio
from playwright import async_playwright

# 异步执行
async def main():
    async with async_playwright() as p:
        for browser_type in [p.chromium, p.firefox, p.webkit]:
            # 指定为有头模式,方便查看
            browser = await browser_type.launch(headless=False)
            page = await browser.newPage()

            await page.goto('http://baidu.com')

            # 执行一次搜索操作
            await page.fill("input[name=\"wd\"]", "自动化测试实战宝典")
            await page.press("input[name=\"wd\"]", "Enter")

            # 等待页面加载完全
            await page.waitForSelector("text=搜索工具")

            # 截图
            await page.screenshot(path=f'test-{browser_type.name}.png')
            await browser.close()

asyncio.get_event_loop().run_until_complete(main())

3.4 支持移动端

更厉害的是,playwright还可支持移动端的浏览器模拟。下面是官方文档提供的一段代码,模拟在给定地理位置上手机iphone 11 pro上的Safari浏览器,首先导航到maps.google.com,然后执行定位并截图。

from playwright import sync_playwright

with sync_playwright() as p:
    iphone_11 = p.devices['iPhone 11 Pro']
    browser = p.webkit.launch(headless=False)
    context = browser.newContext(
        **iphone_11,
        locale='en-US',
        geolocation={ 'longitude': 12.492507, 'latitude': 41.889938 },
        permissions=['geolocation']
    )
    page = context.newPage()
    page.goto('https://maps.google.com')
    page.click('text="Your location"')
    page.screenshot(path='colosseum-iphone.png')
    browser.close()

3. 5 支持Pytest框架

另外,还可以配合pytest插件一起使用,给出一段官网示例:

def test_playwright_is_visible_on_google(page):
    page.goto("https://www.google.com")
    page.type("input[name=q]", "Playwright GitHub")
    page.click("input[type=submit]")
    page.waitForSelector("text=microsoft/Playwright")

当然,除了上面列举出来的特性,还有更多有意思的用法,感兴趣的读者可以自行探索一下。

4. 最后

playwright相比已有的自动化测试框架来说,具有有很多优势,比如:

  • 跨浏览器,支持Chromium、Firefox、WebKit
  • 跨操作系统,支持Linux、Mac、Windows
  • 可提供录制生成代码功能,解放双手
  • 可用于移动端

目前存在的缺点就是生态和文档还不是非常完备,比如没有API中文文档、没有较好的教程和示例供学习。不过相信,随着知道的人越来越多,未来会越来越好。

最后,再说一个小秘密,Playwright 是一个跨语言的自动化框架,除了支持 Python,也支持Java、JS 等,更加详细的功能可以通过官方项目去解锁!

如果你觉得文章还不错,请大家
点赞、分享、留言
下,因为这将是我持续输出更多优质文章的最强动力!

Deep Learning Demo of Primary

下面介绍一个入门案例,如何使用TensorFlow和Keras构建一个CNN模型进行手写数字识别,以及如何使用该模型对自己的图像进行预测。尽管这是一个相对简单的任务,但它涵盖了深度学习基本流程,包括:

  • 数据准备
  • 模型构建
  • 模型训练
  • 模型预测

输入:

import tensorflow as tf
from tensorflow import keras
import numpy as np
from PIL import Image

# 加载MNIST数据集(用于训练模型)
# 这部分代码加载了MNIST数据集,这是一个广泛使用的手写数字图像数据集,包含60,000个训练样本和10,000个测试样本。
# 我们将像素值除以255.0,将它们归一化到0-1的范围内,这是神经网络输入的标准做法。
mnist = keras.datasets.mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

# 规范化像素值
train_images, test_images = train_images / 255.0, test_images / 255.0


# 构建CNN模型
# 这部分代码构建了一个卷积神经网络(CNN)模型。我们使用Keras的Sequential API,它允许我们按顺序堆叠不同的层。
# 我们添加了两个卷积层和两个最大池化层,用于从图像中提取特征。
# 然后,我们添加了一个展平层,将特征映射到一个一维向量。
# 最后,我们添加了两个全连接层,第一个具有128个神经元,第二个具有10个神经元,用于对手写数字进行分类。
# 最后一层使用softmax激活函数输出每个数字的概率。
model = keras.Sequential([
    keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
    keras.layers.MaxPooling2D((2, 2)),
    keras.layers.Conv2D(64, (3, 3), activation='relu'),
    keras.layers.MaxPooling2D((2, 2)),
    keras.layers.Flatten(),
    keras.layers.Dense(128, activation='relu'),
    keras.layers.Dense(10, activation='softmax')
])

# 编译模型
# 这部分代码构建了一个卷积神经网络(CNN)模型。我们使用Keras的Sequential API,它允许我们按顺序堆叠不同的层。
# 我们添加了两个卷积层和两个最大池化层,用于从图像中提取特征。然后,我们添加了一个展平层,将特征映射到一个一维向量。
# 最后,我们添加了两个全连接层,第一个具有128个神经元,第二个具有10个神经元,用于对手写数字进行分类。
# 最后一层使用softmax激活函数输出每个数字的概率。
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

# 训练模型
model.fit(train_images, train_labels, epochs=5)

# 函数:预测手写数字图像
# 在构建模型之后,我们需要编译它。
# 我们指定了使用Adam优化器,稀疏分类交台熵损失函数(适用于整数标签),并监控准确率指标。
# 然后,我们使用model.fit函数在训练数据上训练模型,迭代5个epoch。
def predict_digit(img_path):
    # 加载图像
    img = Image.open(img_path).convert('L')
    img = img.resize((28, 28))
    img_array = np.array(img) / 255.0
    img_array = np.expand_dims(img_array, axis=-1)
    img_array = np.expand_dims(img_array, axis=0)

    # 进行预测
    predictions = model.predict(img_array)
    predicted_digit = np.argmax(predictions)

    return predicted_digit

# 测试
# 这个 predict_digit 函数用于预测手写数字图像。它接受一个图像文件路径作为输入。
# 首先,它使用PIL库加载图像,将其转换为灰度模式,并调整大小为28x28像素。
# 然后,它将图像转换为NumPy数组,并进行与训练数据相同的归一化处理。
# 由于CNN模型需要一个4D张量作为输入(batch_size, height, width, channels),
# 我们需要使用 np.expand_dims 在最后两个维度上扩展数组形状。
#
# 接下来,我们使用训练好的模型的 predict 方法对预处理后的图像数据进行预测,得到一个包含10个概率值的列表,每个值对应一个数字(0-9)的概率。
# 我们使用 np.argmax 找到概率值最大的索引,即模型预测的数字。
# 最后,函数返回预测的数字。
digit = predict_digit('image-8.png')
print(f'预测的数字是: {digit}')

输出:
预测的数字是: 8

但是完全不知道程序都做了什么...,那就学习它的流程吧。

Process:

  1. 首先,我们加载内置的MNIST数据集,并将像素值归一化到0-1之间。
  2. 然后,我们使用Keras的Sequential API构建一个CNN模型。该模型包含两个卷积层、两个最大池化层、一个展平层和两个全连接层。
    最后一层使用softmax激活函数输出10个数字的概率。
  3. 我们使用稀疏分类交叉熵损失函数和Adam优化器编译模型。
  4. 接下来,我们使用训练数据train_images和train_labels训练模型5个epoch。
  5. 我们定义了一个predict_digit函数,用于预测手写数字图像。这个函数接受一个图像文件路径作为输入。
  6. 在predict_digit函数中,我们首先使用Pillow库加载图像,并将其转换为灰度模式和28x28大小。
    然后,我们将图像数据转换为Numpy数组,并进行相同的归一化处理。
    由于模型的输入维度为(批次大小, 高度, 宽度, 通道数),我们需要使用np.expand_dims在最后两个维度上扩展数组形状。
  7. 接下来,我们使用训练好的模型的predict方法对预处理后的图像数据进行预测,得到一个包含10个概率值的列表,每个值对应一个数字(0-9)的概率。
    我们使用np.argmax找到概率值最大的索引,即模型预测的数字。
  8. 最后,我们调用predict_digit函数,传入你自己的图像文件路径,并打印预测结果。