Vue3,Vuex,ES6,json-serve,pinia

ES6 模块化

ES6 模块化规范是浏览器端与服务端通用的模块化开发规范

  • 导入其它模块使用 require() 方法
  • 模块对外共享成员使用 module.exports 对象

ES6 模块化规范中定义:

  • 每个 js 文件都是一个独立的模块
  • 导入其它模块成员使用 import 关键字
  • 向外共享模块成员使用 export 关键字

开启ES6语法

添加 type:module

json 复制代码
{
  "name": "view_data_vue2",
  "version": "0.1.0",
  "type": "module",
  "scripts": {

基本使用

① 默认导出与默认导入

向外导出 n1 和 show

js 复制代码
let n1 = 10
let n2 = 20

function show() {
}

// 向外导出一个对象,包含n1和show
export default {
    n1, show
}

控制台就会打印导出的 n1 和show方法

javascript 复制代码
import m1 from './02导出.js'

console.log(m1)

默认导出一个模块只能有一个

② 默认导入 + 按需导入,m1 是默认导入,花括号内是按需导入

js 复制代码
import m1, {n1, n2} from './02Test.js'

③ 直接导入并执行模块中的代码

js 复制代码
import './02Test.js'

回调地域 & Promise .then

多层回调函数的相互嵌套,就形成了回调地狱。

回调地狱的缺点:

  • 代码耦合性太强,牵一发而动全身,难以维护
  • 大量冗余的代码相互嵌套,代码的可读性变差

Promise 解决此问题

Promise 是一个构造函数

  • 我们可以创建 Promise 的实例 const p = new Promise()
  • new 出来的 Promise 实例对象,代表一个异步操作

Promise.prototype 上包含一个 .then() 方法

  • 每一次 new Promise() 构造函数得到的实例对象,
  • 都可以通过原型链的方式访问到 .then() 方法,例如 p.then()

.then() 方法用来预先指定成功和失败的回调函数

  • p.then(成功的回调函数,失败的回调函数)
  • p.then(result => { }, error => { })
  • 调用 .then() 方法时,成功的回调函数是必选的、失败的回调函数是可选的

由于 node.js 官方提供的 fs 模块仅支持以回调函数的方式读取文件,不支持 Promise 的调用方式。

因此需要安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件。

依赖

npm 复制代码
npm install then-fs -S

使用 Promise 解决回调地狱问题,使读取文件顺序一致

如果上一个 .then() 方法中返回了一个新的 Promise 实例对象,则可以通过下一个 .then() 继续进行处理。通 过 .then() 方法的链式调用,就解决了回调地狱的问题。

js 复制代码
// 导入 then-fs 模块,它是一个 Promise 包装的文件系统模块
import thenFs from 'then-fs'

// 读取文件 1.txt 的内容,指定编码为 utf-8
thenFs.readFile('./1.txt', 'utf-8').then(res => {
    // 当读取完成后,执行回调函数,将文件内容打印到控制台
    console.log(res)

    // 返回一个新的 Promise,继续读取文件 2.txt 的内容
    return thenFs.readFile('./2.txt', 'utf-8').then(res => {
        // 当读取完成后,执行回调函数,将文件内容打印到控制台
        console.log(res)

        // 返回一个新的 Promise,继续读取文件 3.txt 的内容
        return thenFs.readFile('./3.txt', 'utf-8').then(res => {
            // 当读取完成后,执行回调函数,将文件内容打印到控制台
            console.log(res)
        })
    })
})

遇到错误不在执行 .then

js 复制代码
thenFs.readFile('./11.txt', 'utf-8').then(res => {

}).catch(err => {
    // 打印错误 [Error: ENOENT: no such file or directory, open
    console.log(err)
})

遇到错误继续执行

js 复制代码
thenFs.readFile('./11.txt', 'utf-8')
    // 先捕获错误并处理,继续执行 .then
    .catch(err => {
        console.log(err)
    })
    .then(res => {
        console.log(res)
        return thenFs.readFile('./2.txt', 'utf-8').then(res => {
            console.log(res)
            return thenFs.readFile('./3.txt', 'utf-8').then(res => {
                console.log(res)
            })
        })
    })

并行执行 Promise.all

js 复制代码
// 导入 then-fs 模块,它是一个 Promise 包装的文件系统模块
import thenFs from 'then-fs'

// 定义一个 promise 数组,每个元素是一个文件读取操作的 Promise
const arr = [
    thenFs.readFile('./1.txt', 'utf-8'), // 读取文件 1.txt 的内容,返回一个 Promise
    thenFs.readFile('./2.txt', 'utf-8'), // 读取文件 2.txt 的内容,返回一个 Promise
    thenFs.readFile('./3.txt', 'utf-8')  // 读取文件 3.txt 的内容,返回一个 Promise
]

// 使用 Promise.all 方法,传入 promise 数组,它会等待所有的 Promise 都完成
Promise.all(arr).then(res => {
    // 当所有的 Promise 都完成后,执行回调函数
    // 参数 res 包含了各个 Promise 完成时的结果,顺序与 promise 数组一致
    console.log(res) // [111,222,333]
}).catch(err => {
    // 如果有任何一个 Promise 失败(比如文件不存在),会进入这个 catch 分支
    console.error('读取文件出错:', err) // 打印错误信息
})

赛跑机制,只获取最快执行完毕的 Promise.race

js 复制代码
// 导入 then-fs 模块,它是一个 Promise 包装的文件系统模块
import thenFs from 'then-fs'

// 定义一个 promise 数组,每个元素是一个文件读取操作的 Promise
const arr = [
    thenFs.readFile('./1.txt', 'utf-8'), // 读取文件 1.txt 的内容,返回一个 Promise
    thenFs.readFile('./2.txt', 'utf-8'), // 读取文件 2.txt 的内容,返回一个 Promise
    thenFs.readFile('./3.txt', 'utf-8')  // 读取文件 3.txt 的内容,返回一个 Promise
]

// 使用 Promise.race 方法,传入 promise 数组,它会在任何一个 Promise 完成时解析
Promise.race(arr).then(res => {
    // 当任何一个 Promise 完成时,执行回调函数,参数 res 是第一个完成的 Promise 的结果
    console.log(res) // 111
}).catch(err => {
    // 如果有任何一个 Promise 失败(比如文件不存在),会进入这个 catch 分支
    console.error('读取文件出错:', err) // 打印错误信息
})

异步操作

new Promise 之后就可以使用 .then 去获取结果。这就是一个异步操作,那么只需要将函数传递给 Promise去异步执行就可以了

js 复制代码
/**
 * 创建一个方法用来异步执行读取文件
 * 传入一个路径
 */
function getFile(fpath) {
    /**
     * resolve 成功
     * reject 失败
     */
    return new Promise(function (resolve, reject) {
        fs.readFile(fpath, 'utf-8', (err, dataStr) => {
            // 如果发生错误,执行 err
            if (err) return reject(err)
            // 执行成功
            resolve(dataStr)
        })
    })
}

// 调用
getFile('./1.txt').then(res => {
    console.log(res)
}).catch(err => {
    console.log(err.message)
})

上面这样写其实有的麻烦的,因为 fs.readFile(fpath, 'utf-8', (err, dataStr)本身返回的就是 Promise,无需再次封装,简化

js 复制代码
// 导入 then-fs 模块,它是一个 Promise 包装的文件系统模块
import fs from 'then-fs'

/**
 * 创建一个方法用来异步执行读取文件
 * 传入一个路径
 * @param {string} fpath - 文件路径
 * @returns {Promise<string>} 返回一个 Promise,解析后的数据是文件内容的字符串
 */
function getFile(fpath) {
    // 使用 then-fs 的 readFile 方法,它返回一个 Promise 对象
    // 所以不需要手动创建 Promise
    return fs.readFile(fpath, 'utf-8')
}

// 调用 getFile 方法
getFile('./1.txt').then(res => {
    console.log(res) // 打印文件内容
}).catch(err => {
    console.error(err.message) // 打印错误信息
})

async / await

用来简化 Promise 异步操作

由于 .then 可读性差,代码冗余,不易理解

解决返回 Promise 对象问题

js 复制代码
// 导入 then-fs 模块,它是一个 Promise 包装的文件系统模块
import fs from 'then-fs'

/**
 * 异步函数,用于读取文件并打印内容
 */
async function getAllFile() {
    try {
        // 使用 await 等待异步操作完成
        // fs.readFile 返回一个 Promise,await 等待其解析并将结果赋给 readFile 变量
        let readFile = await fs.readFile('./1.txt', 'utf-8')
        console.log(readFile)
    } catch (err) {
        // 捕获可能的错误并进行处理
        console.error(err.message)
    }
}

// 调用异步函数
getAllFile()

注意事项:第一个 await 之前的代码会同步执行,之后的代码会异步执行

同步任务和异步任务

JS 是单线程语言,如果前面的任务耗时很久,则后续的任务会一直等待,导致程序假死问题

为了防止某个耗时任务导致程序假死的问题,JavaScript 把待执行的任务分为了两类:

① 同步任务(synchronous)

  • 又叫做非耗时任务,指的是在主线程上排队执行的那些任务
  • 只有前一个任务执行完毕,才能执行后一个任务

② 异步任务(asynchronous)

  • 又叫做耗时任务,异步任务由 JavaScript 委托给宿主环境进行执行
  • 当异步任务执行完成后,会通知 JavaScript 主线程执行异步任务的回调函数
  1. 同步任务由 javaScript 主线程次序执行
  2. 异步任务委托给宿主环境执行
  3. 已完成的异步任务对应的回调函数会被放到任务队列中等待主线程执行
  4. JavaScript 主线程的栈被清空后会读取任务队列的回调函数,次序执行

JavaScript 主线程从任务队列中读取异步任务的回调函数,放到执行栈中执行这个过程是一直循环不断的,所以这个机制称之为 EventLoop(事件循环)

宏任务和微任务

JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:

① 宏任务(macrotask)

  • 异步 Ajax 请求
  • setTimeout、setInterval
  • 文件操作
  • 其它宏任务

② 微任务(microtask)

  • Promise.then、.catch 和 .finally
  • process.nextTick
  • 其它微任务

微任务和宏任务的执行顺序

宏任务执行完毕会检查是否有待执行的微任务,如果有则执行所有待执行的微任务,否则执行下一个宏任务

注意:先执行所有同步任务,再执行微任务,然后执行下一个宏任务

API 接口案例,操作数据库

基于 MySQL 数据库 + Express 对外提供用户列表的 API 接口服务。

  • 第三方包 express 和 mysql2
npm 复制代码
npm install express@4.17.1 mysql2@2.2.5 

第一步监听端口

js 复制代码
// 导入Express.js框架
import express from 'express'

// 创建Express应用程序实例
const app = express()

// 启动Express服务器监听端口80,并在服务器启动后执行回调函数
app.listen(80, () => {
    console.log("服务器已启动,正在监听端口80...")
})

创建mysql,并连接

js 复制代码
// 导入mysql2模块
import mysql from 'mysql2'

// 创建MySQL连接池,允许多个连接,提高性能
const cool = mysql.createPool({
    host: '127.0.0.1',   // MySQL服务器的主机名
    port: 3306,          // MySQL服务器的端口号,默认是3306
    database: 'my_db_01', // 要连接的数据库名称
    user: 'root',         // 数据库用户的用户名
    password: '1234'      // 数据库用户的密码
})

// 通过promise()方法使连接池支持Promise
export default cool.promise()

创建 user_ctrl 模块

其中使用 res.status(200).json 发送表明是传递的JSON数据,并且响应200

也可以使用通用方式 res.send({ 直接发送对象,语法不清晰

js 复制代码
// 导入所需的模块
import db from 'your-db-module';

// 导出异步函数
export async function getAllUser(req, res) {
    try {
        // 使用await等待查询结果,返回的是数组,我们可以通过解构赋值获取数据
        const [rows] = await db.query('SELECT id, username, nickname FROM ev_users');
        // 返回数据给客户端
        res.status(200).json({
            status: 0,
            message: '获取用户列表成功!',
            data: rows
        });
    } catch (error) {
        // 捕获并处理异常
        console.error('获取用户列表失败:', error);
        res.status(500).json({
            status: 1,
            message: '获取用户列表失败!',
            desc: error.message
        });
    }
}

创建 user_router 模块

js 复制代码
import express from "express";
import { getAllUser } from "../controller/user_ctrl.js";

// 创建路由对象
const router = new express.Router();

// 处理 GET 请求到 /user 路径的路由
router.get('/user', getAllUser);

export default router;

在 app.js 中添加

js 复制代码
// 导入名为 user_router 的路由模块
import user_router from "./router/user_router.js";

// 创建一个 Express 应用程序实例
const app = express();

// 将 user_router 路由模块挂载到 统一/api 路径下
app.use('/api', user_router);

在终端启动

npm 复制代码
nodemon app.js

如果报错nodemon : 无法将"nodemon"项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。 所在位置 行:1 字符: 1

说明没有安装包

执行全局安装

npm 复制代码
npm install -g nodemon

再次执行报一下错误

nodemon app.js ~ 一元运算符"+"后面缺少表达式。 所在位置 行:1 字符: 3 nodemon app.js 表达式或语句中包含意外的标记"nodemon"。

解决方式: 管理员身份打开powerShell

cmd 复制代码
set-ExecutionPolicy RemoteSigned

按是 即可

然后我们打开 postman 查询,发现返回结果是正确的

js 复制代码
http://localhost:87/api/user

.sync 修饰符

子组件和父组件值的双向绑定

  • :visible.sync="isShow" 表示向子组件传递一个值,并绑定起来
js 复制代码
<BaseDialog :visible.sync="isShow"></BaseDialog>

当子组件想修改这个值时,注意 visible 这个变量要和 prop 接收过来的一致,后面传递想修改为...

js 复制代码
this.$emit('update:visible',false)

vuex 状态管理工具

vuex 是一个插件,可以帮我们管理 vue 通用的数据 (多组件共享的数据)

优势:

  • 共同维护一份数据,数据集中化管理
  • 响应式变化
  • 操作简洁 (vuex提供了一些辅助函数)

配置

vue2 对应的vuex是3,vue3对应的vuex是4

npm 复制代码
npm i vuex@3 -S

创建文件 src/store/index.js(这里面就存放 vuex的核心代码)

js 复制代码
// 在这里存放 vuex 的相关代码
import Vue from 'vue'
import Vuex from 'vuex'

// 插件安装
Vue.use(Vuex)

// 创建空仓库
const store = new Vuex.Store({})
// 导出
export default store

在 main.js 中导入

js 复制代码
import Vue from 'vue'
import App from './App.vue'
// 导入 vuex
import store from '@/store/index'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  // 使用
  store
}).$mount('#app')

访问仓库通过

js 复制代码
this.$store

获取仓库的方式 / 存入数据

通过 import 导入

模板中: {{ $store.state.xxx }}

组件逻辑中: this.$store.state.xxx

JS模块中: store.state.xxx

直接存入

严格模式:不允许直接修改,需要通过辅助函数

js 复制代码
// 创建仓库
const store = new Vuex.Store(
  {
  // 开启严格模式--上线可以关闭,因为消耗性能
    strict: true,
    // 定义数据
    state: {
      title: '大标题',
      count: 100
    }
  }
)

vuex 辅助函数映射 mapState,只读

mapState 是辅助函数,帮助我们把 store中的数据自动映射到组件的计算属性中

我们可以使用 this.title 直接访问

js 复制代码
// 引入辅助函数 mapState
import { mapState } from 'vuex'
export default {
    computed: {
        // 映射仓库 title 属性
        ...mapState(['title', 'count'])
    }
}

vuex 辅助函数映射 mutations 可修改

开启了严格模式,直接修改虽然可以成功,但是有报错

在仓库中定义修改的方法

js 复制代码
// 创建空仓库
const store = new Vuex.Store(
  {
    // 开启严格模式
    strict: true,
    state: {
      title: '大标题',
      count: 100
    },
    // 定义 mutations
    mutations: {
      // 第一个参数是当前 store 的 state 属性
      addCount (state, n) {
        state.count += n
      }
    }
  }
)

在组件中调用该方法,后面参数的名称要一致

  • 参数一:方法名称
  • 参数二:要传递的参数(只能传递一个,没有第三个参数)
js 复制代码
this.$store.commit('addCount', n)

注意:在严格模式下不可以使用 v-model 和 vuex 的数据进行绑定。

vuex 映射方法 mapMutations

mapMutations 把位于 mutations 中的方法提取了出来,映射到组件 methods 中

js 复制代码
// 导 mapMutations 包
import { mapMutations } from 'vuex'
export default {
  name: 'app',
  methods: {
    // 映射过来
    ...mapMutations(['changeCount']),
    // 可以直接使用
    handleInput (e) {
      this.changeCount(+e.target.value)
    }
  }
}

核心概念 actions / 映射

mutations 必须是同步的,便于检测数据变化,记录调试,无法执行异步操作

actions 处理异步操作

在仓库定义

js 复制代码
// 处理异步,不能直接操作state,需要调用 commit 执行
actions: {
  // context 上下文(此时未分模块,可以看作是 仓库)
  changeCountAction (context, num) {
    // 模拟异步
    setTimeout(() => {
      context.commit('changeCount', num)
    }, 2000)
  }
}

使用

js 复制代码
this.$store.dispatch('changeCountAction', 666)

vuex 映射方法 mapActions

js 复制代码
// 导入包
import { mapActions } from 'vuex'
export default {
  name: 'Son2Com',
  // 在方法中映射 changeCountAction
  methods: {
    ...mapActions(['changeCountAction']),
    // 直接使用
    change () {
      this.changeCountAction(888)
    }
  }
}

getters 计算属性,mapGetters 辅助函数

在仓库中添加

js 复制代码
// getters 类似于计算属性
getters: {
  // 注意到:参数1:state,必须有返回值
  filterList (state) {
    // 过滤
    return state.list.filter(item => item > 5)
  }
}

直接使用

html 复制代码
<div>{{ $store.getters.filterList}}</div>

辅助函数使用 mapGetters

js 复制代码
// 引入 mapGetters
import { mapActions, mapGetters } from 'vuex'
export default {
  name: 'Son2Com',
  computed: {
    // 引用
    ...mapGetters(['filterList'])
  }
}

使用

html 复制代码
<div>{{ filterList }}</div>

注意事项:mapGetters 和 mapStat 是在计算属性中映射变量,而 mapMutations 和 mapActions 是映射方法

分模块开发

由于 vuex 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时, store 对象就有可能变得相当臃肿。(当项目变得越来越大的时候,Vuex会变得越来越难以维护)

定义模块--创建 src/store/modules/user.js 模块

js 复制代码
const state = {
  userinfo: {
    name: 'zs',
    age: 18
  },
  score: 80
}
const mutations = {}
const actions = {}
const getters = {}
// 导出
export default {
  state,
  mutations,
  actions,
  getters
}

然后导入该模块,并挂载

js 复制代码
// 导入模块
import User from '@/store/modules/user'
Vue.use(Vuex)

// 创建空仓库
const store = new Vuex.Store(
  {
    strict: true,
    state: {
      title: '大标题',
      count: 100,
      list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }
    // 添加模块
    modules: {
      User
    }
  }
)

打开vue调试工具,可以看到安装成功

官方推荐 state 写法

js 复制代码
export default {
  namespaced: true,
  state () {
    return {}
  }
}

获取模块下的 state 数据

  1. 原生获取
html 复制代码
<!--  先获取状态,再获取模块,再获取值  -->
<div>{{$store.state.User.userinfo.name}}</div>
  1. 映射的方式获取
js 复制代码
computed: {
    ...mapState(['User'])
}

使用

html 复制代码
<!--  先获取状态,再获取模块,再获取值  -->
<div>{{ User }}</div>

上面方式还是复杂,有没有基于模块去找值呢

首先:在模块中开启命名空间

js 复制代码
export default {
  // 开启命名空间
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

参数一:字符串,命名空间

参数二:要映射的数组

js 复制代码
computed: {
    ...mapState('User', ['userinfo'])
},

获取模块下的 getters 数据

注意:分模块后 state 指的是本模块的 state

js 复制代码
const getters = {
  // 分模块后 state 指的是本模块的 state
  UpperCaseName (state) {
    return state.userinfo.name
  }
}

原生获取

html 复制代码
<!--  模块名 / getters名  -->
<div>{{ $store.getters['User/UpperCaseName'] }}</div>

辅助函数(需要开命名空间)

可以直接使用 UpperCaseName

js 复制代码
computed: {
    ...mapGetters('User', ['UpperCaseName'])
},

mutation 和 actions

注意 mutation 和 actions 默认会被挂在到全局,需要开启命名空间,才能挂在到子模块

mutation 修改子模块的数据

原生

js 复制代码
this.$store.commit('User/setUser', { name: '小王', age: 25 })

使用辅助函数

  • setScore 是分模块中的函数
js 复制代码
...mapMutations('User', ['setScore'])

actions

js 复制代码
const actions = {
  setUserInfo (context, newUserInfo) {
    setTimeout(() => {
      // context 根据当前模块,找当前方法,不需要加命名空间
      context.commit('setUser', newUserInfo)
    }, 1000)
  }
}

原生

js 复制代码
this.$store.dispatch('User/setUserInfo', 666)

辅助函数

js 复制代码
methods: {
    ...mapActions('User', ['setUserInfo']),
    changeFn () {
      this.setUserInfo(666)
    }
}

json-serve 快速生成后端

全局安装

npm 复制代码
npm i json-server -g
  • 创建一个JSON文件,基于此我文件启动
  • 在 powershell 中启动
js 复制代码
json-server --watch .\db\index.json

会自动基于 JSON文件模拟接口数据

Vue3

node 版本 v16 及以上

vite 依赖

cmd 复制代码
npm install esbuild

创建项目

cmd 复制代码
npm init vue@latest

目录变化

之前的 vue.config 改成了 vite.config.js

组件,无需注册,导入就可以用

template 分为header和main不在唯一根元素
这里我使用 yarn install 安装依赖,不清楚为什么卡住

生命函数

setup 函数,执行比 create 要早

  1. 在这个函数中是获取不到 this,打印是 undefined
  2. 可以提供函数和提供数据

定义函数,定义变量

html 复制代码
<script>
export default {
  setup() {
    // 使用 Composition API 的 setup 函数
    const message = 'hello'

    // 定义一个方法,用于在点击按钮时打印 message
    const logMessage = () => {
      console.log(message)
    }

    // 返回数据和方法
    return {
      message,
      logMessage
    }
  }
}
</script>

<template>
  <!-- 在模板中显示 message -->
  <div>{{ message }}</div>
  
  <!-- 添加一个按钮,当点击时调用 logMessage 方法 -->
  <button @click="logMessage">打印 message </button>
</template>

每次都要 return 太麻烦了

简化--底层 returen

js 复制代码
<script setup>
const message = 'hello'
const logMessage = () => {
  console.log(message)
}
</script>

响应式对象 reactive ref

reactive 只能接收对象
作用:接收对象并将转换成响应式对象返回

如果不使用,那么使用它将不会响应到页面

html 复制代码
<script setup>
// 引入 reactive 函数,用于创建响应式对象
import { reactive } from 'vue'

// 创建一个响应式对象 state,包含 count 属性
const state = reactive({
  count: 0
})

// 创建一个方法 setCount,用于增加 count 的值
const setCount = () => {
  state.count++
}
</script>

<template>
  <div>{{ state.count }}</div>
  <button @click="setCount">+1</button>
</template>

ref 可以接收对象也可以接收简单类型

本质:其实是对数据进行了加工,外层包了一层对象,将其转化成复杂类型再次借助 reactive 实现响应式

注意点:

  1. 在js脚本中使用需要加 .value 获取数据
  2. 在 template 中使用不需要加 .value
html 复制代码
<script setup>
// 第一步引包
import {ref} from 'vue'
// 创建响应式简单类型
const count = ref(0)
// 打印 count 值需要 .value
console.log(count.value)
// 实现递增
const setCount = () => {
  count.value++
}
</script>

<template>
  <!-- 在template 自动去壳,不需要 .value -->
  <div>{{ count }}</div>
  <button @click="setCount">+1</button>
</template>

计算属性 computed

html 复制代码
<script setup>
import {ref, computed} from 'vue'
// 声明数组
const list = ref([1, 2, 3, 4, 5, 6, 7, 8, 9])
// 使用 computed 创建计算属性
const computedList = computed(() => {
  return list.value.filter(item => item > 2)
})
</script>

<template>
  <!-- 在template 自动去壳,不需要 .value -->
  <div>原始数据:{{ list }}</div>
  <div>计算数据:{{ computedList }}</div>
</template>

watch 监听

执行 watch 函数传入要侦听的响应式数据(ref对象)和回调函数

html 复制代码
<script setup>
import {ref, watch} from 'vue'

const count = ref(0)
const name = ref("张三")
const setCount = () => {
  count.value++
}
const setName = () => {
  name.value = '李四'
}
// 监视单个变量
// watch(count, (newValue, oldValue) => {
//   console.log(newValue, oldValue)
// })
// 监视多个变量
watch([count, name], (newValue, oldValue) => {
  console.log(newValue, oldValue)
})
</script>

<template>
  <div>{{ count }}</div>
  <button @click="setCount">修改count</button>
  <div>{{ name }}</div>
  <button @click="setName">修改name</button>
</template>

深度监听

html 复制代码
<script setup>
import {ref, watch} from "vue";

const userInfo = ref({
  name: '张三',
  age: 18
})
const setUserInfo = () => {
  userInfo.value.age++
}
// 由于监听的是复杂类型,watch监听不到,需要开启深度监听
watch(userInfo, (newValue, oldValue) => {
  console.log(newValue, oldValue)
}, {
  // 初始化执行一次
  immediate: true,
  // 深度监听,可以监听到子属性的变化
  deep: true
})
</script>

<template>
  <div>{{ userInfo }}</div>
  <button @click="setUserInfo">修改 userInfo</button>
</template>

只监听对象中的某个属性写法

js 复制代码
// 只监听对象中的某个属性
watch(() => userInfo.value.age, (newValue, oldValue) => {
  console.log(newValue, oldValue)
})

生命周期函数

左侧的是 vue2的周期函数,右侧是vue3

html 复制代码
<script setup>
// beforeCreate 和 create 在vue3中写在setup中
import {onMounted} from "vue";

const getList = () => {
  console.log("获取数据")
}
getList()
// mounted 在 vue3 中 onMounted
// 并且可以声名多个,按照顺序执行
onMounted(() => {
  console.log("处理逻辑 1")
})
onMounted(() => {
  console.log("处理逻辑 2")
})
</script>

组件通信

注意:引入组件需要加后缀,否则会找不到

父传子

在子组件中变量需要定义编译宏获取 props

在脚本中需要通过 props.传递的变量获取

在 template 中直接使用,不需要 props

可以传递 ref()响应式变量,会随着改变动态更新

html 复制代码
<script setup>
// 局部导入
import HelloWorld from '@/components/HelloWorld.vue'
</script>

<template>
  <!-- 父传子 -->
  <HelloWorld car="宝马车"></HelloWorld>
</template>

html 复制代码
<script setup>
// 由于写了 setup 无法直接写 props
// 借用 编译器宏
const props = defineProps({
  car: String
})
// 在脚本中需要 props.传递的变量获取
console.log(props.car)
</script>

<template>
  <div>这是子组件---{{ car }}</div>
</template>

子传父

父组件给子组件绑定 @监听事件

子组件通过 emit 触发父组件定义的事件

子组件不能通过 props直接修改,会报错

html 复制代码
<script setup>
// 局部导入
import HelloWorld from '@/components/HelloWorld.vue'
import {ref} from "vue";

let money = ref(100)
const setMoney = (num) => {
  money.value -= num
}
</script>

<template>
  <!-- 父传子 -->
  <HelloWorld :money="money" @setMoney="setMoney"></HelloWorld>
</template>

html 复制代码
<script setup>
// 引入 props
const props = defineProps({
  money: Number
})
// 引入 emit
const emits = defineEmits(['setMoney']);

const setMoney = () => {
  // 触发父组件方法
  emits('setMoney', 10)
}
console.log(props.money)
</script>

<template>
  <div>这是子组件---{{ money }}</div>
  <button @click="setMoney">花钱</button>
</template>

子组件修改父组件数据

方式一,默认名称

父组件---必须 v-model 绑定数据

js 复制代码
<script setup>
import { ref } from 'vue'
const aa = ref(0)
</script>
<test v-model="aa" />
</template>

子组件

注意:props 必须是 modelValue,并且在使用处,绑定方法为 update:modelValue

js 复制代码
<script setup>
import { defineEmits } from 'vue'
defineProps({
  modelValue: {
    type: [String, Number],
    required: true
  }
})
const emit = defineEmits(['update:modelValue'])
</script>
<template>
  <el-select
    :model-value="modelValue"
    @update:modelValue="emit('update:modelValue', $event)"
    class="m-2"
    placeholder="Select"
    size="large"
  >
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value"
    />
  </el-select>

方式二自定义名称

父组件---传入的时候使用 v-model:传递的变量

html 复制代码
<script setup>
import Test from '@/views/article/test.vue'
import { ref } from 'vue'
const params = ref({
  tid: 0
})
</script>
<template>
    <test v-model:cid="params.tid" />
</template>

子组件---全部用cid接收,emit改成cid,等等

html 复制代码
<script setup>
import { defineEmits } from 'vue'
defineProps({
  cid: {
    type: [String, Number],
    required: true
  }
})
const emit = defineEmits(['update:cid'])
</script>
<template>
  <el-select
    :model-value="cid"
    @update:modelValue="emit('update:cid', $event)"
    class="m-2"
    placeholder="Select"
    size="large"
  >
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value"
    />
  </el-select>
</template>

模板引用 获取 dom

注意:在 setUp 函数中无法操作dom,dom未渲染完成

变量名称需要和 input标签的ref值要一致,且 ref函数值为null

html 复制代码
<script setup>
import {ref} from "vue"

// 获取 input 变量名称需要和 input标签的ref值要一致,且 ref函数值为null
const inp = ref(null)
// 点击聚焦
const inpClick = () => {
  inp.value.focus()
}
</script>

<template>
  <input ref="inp"/>
  <button @click="inpClick">点击聚焦</button>
</template>

使用 子组件的方法(defineExpose)

默认情况下,script setUp中的函数,数据是不对父组件开放的

可以通过defineExpose编译宏指定哪些属性和方法允许访问

html 复制代码
<script setup>
const count = 999
const sayHi = () => {
  console.log("打招呼")
}
// 指定哪些变量或者函数,父组件可以访问
defineExpose({
  count,
  sayHi
})
</script>

父组件调用

html 复制代码
<script setup>

import HelloWorld from "@/components/HelloWorld.vue"
import {ref} from "vue";
// 获取dom
const hello = ref(null)

const getClick = () => {
  // 调用父组件的方法以及函数
  console.log(hello.value.count)
  hello.value.sayHi()
}
</script>

<template>
  <HelloWorld ref="hello"/>
  <button @click="getClick">获取组件</button>
</template>

provide 和 inject 跨层传递数据

普通数据跨层传递

js 复制代码
import {provide} from "vue";
// 跨层传递的普通数据
provide('theme-color', 'pink')

接收

js 复制代码
import {inject} from "vue"

const theme = inject('theme-color')

跨层传递响应式数据

js 复制代码
// 跨层传递的响应式数据
const count = ref(100)
provide('count', count)

让子层组件修改数据的方式

父组件

js 复制代码
import {provide, ref} from "vue";
// 跨层传递的响应式数据
const count = ref(100)
provide('count', count)
// 定义子组件修改数据的函数
provide('changeCount', (newValue) => {
    count.value = newValue
})
</script>

子组件

html 复制代码
<script setup>
import {inject} from "vue";

const count = inject('count');
// 获取子组件定义修改数据的函数
const changeData = inject('changeCount');
</script>
<template>
    <div>这是最后的组件--{{ count }}</div>
    <button @click="changeData(10000)">修改数据~~~~</button>
</template>

defineOption 新特性

用于定义 OptionsApi 选项,比如 name

js 复制代码
// 在 setup 里
defineOptions({
    name: 'BottomCom'
})

defineModel(实验特性)

defineModel 可以简化 v-model 和 update修改操作,将值在子组件获取后,可以直接修改

父组件

html 复制代码
<script setup>
import CenterCom from "@/components/centerCom.vue"
import { ref } from "vue"

// 使用 ref 创建响应式变量 txt,初始值为 '123456'
const txt = ref('123456')
</script>

<template>
  <!-- 使用 centerCom 组件,并通过 v-model 绑定 txt 变量 -->
  <centerCom v-model="txt"/>
  <!-- 显示 txt 变量的值 -->
  {{ txt }}
</template>

子组件

html 复制代码
<script setup>
// 导入 defineModel 函数
import { defineModel } from "vue"

// 使用 defineModel 创建 modelValue 变量,用于 v-model 绑定
const modelValue = defineModel()
</script>

<template>
  <!-- 使用 input 元素,通过 :value 和 @input 实现双向绑定 -->
  <input type="text" :value="modelValue" @input="e => modelValue = e.target.value">
</template>

vue3 路由

其中里面的 import.meta.env.BASE_URL 配置项在 vite.config.js 中的 base: '/jd',

js 复制代码
/*
 * createRouter:创建路由实例
 * createWebHistory:路由模式 不带 #号
 * createWebHashHistory:路由模式:hash模式,带 #号
 * */
import {
  createRouter,
  createWebHistory,
  createWebHashHistory
} from 'vue-router'

// 创建路由实例
const router = createRouter({
  // import.meta.env.BASE_URL 是环境变量,参数的意思表示前缀带什么,例如:jd/list,jd 为前缀
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: []
})

export default router

路由拦截器

js 复制代码
// 登录拦截器
// 根据返回值决定,是放行还是拦截
// 返回值 undefined / true 直接放行
// false 返回 from 的地址页面
// 具体路径 或者 路径对象拦截到对应的地址
// ./login {name: 'login'}
router.beforeEach((to) => {
  // 如果没有 toke,且访问的是非登录页,拦截到登录,其他正常
  const userStore = useUserStore()
  if (!userStore.token && to.path !== '/login') {
    return '/login'
  }
  return true
})

export default router

封装 request 拦截器

js 复制代码
import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import router from '@/router'

const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
  baseURL,
  timeout: 10000
})
// request 请求拦截器
instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = userStore.token
    }
    return config
  },
  (err) => Promise.reject(err)
)
// 响应拦截器
instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    if (res.data.code === 0) {
      // TODO 4. 摘取核心响应数据
      return res
    }
    // 返回失败的信息
    ElMessage.error(res.data.message || '服务异常')
    return Promise.reject(res.data)
  },
  (err) => {
    // TODO 5. 处理401错误
    if (err.response?.status === 401) {
      router.push('/login')
    }

    // 错误的默认情况
    ElMessage.error(err.response.data.message || '服务异常')
    return Promise.reject(err)
  }
)

export default instance
export { baseURL }

pinia (vuex 升级版)

Pinia 是 Vue 的最新 状态管理工具 ,是 Vuex 的 替代品

  1. 提供更加简单的API (去掉了 mutation )
  2. 提供符合,组合式风格的API (和 Vue3 新语法统一)
  3. 去掉了 modules 的概念,每一个 store 都是一个独立的模块
  4. 配合 TypeScript 更加友好,提供可靠的类型推断

安装依赖

npm 复制代码
yarn add pinia -S

main 导入

js 复制代码
import {createPinia} from "pinia"
// 创建根实例
const app = createApp(App);
// 创建 Pinia 实例
app.use(createPinia())
app.mount('#app')

pinia 基本使用(数据,方法,计算属性)

在store创建单个模块

js 复制代码
import {defineStore} from "pinia";
import {computed, ref} from "vue";
// 定义 store
// defineStore (仓库的唯一标识,()=> {....})

// counter 标识当前仓库名称
// useCountStore 标识导出的名称
export const useCountStore = defineStore('counter', () => {
    // 声明数据 state - count
    const count = ref(20)
    // 声明操作数据的方法 action
    const addCount = () => count.value++
    const subCount = () => count.value--
    // 声明基于数据派生的计算属性方法 getters(computed)
    const double = computed(() => count.value * 2)
    // 声明数据 state - msg
    const msg = ref('hello msg');
    // 返回
    return {
        count,
        msg,
        double,
        addCount,
        subCount
    }
})

使用

html 复制代码
<script setup>
// 导入定义的仓库
import {useCountStore} from '@/store/counter'

// 打印实例
console.log(useCountStore())
const counterStore = useCountStore()
</script>

<template>
    <div>
        <!--    打印里面的状态数据    -->
        <h3>app 根组件---{{ counterStore.count}}</h3>
    </div>
</template>

异步方法action

直接定义即可

js 复制代码
export const useChannelStore = defineStore('channel', () => {
    // 声明数据
    const channelList = ref([])
    // 声明操作数据的方法
    const getList = async () => {
        // 支持异步
        const {data: {data: res}} = await axios.get('http://geek.itheima.net/v1_0/channels')
        console.log(res.channels);
    }
    // 声明计算属性
    return {
        channelList,
        getList
    }
})

数据解体丢失响应式问题

看下面代码,将仓库的每个数据解体出来,使用,此时点击按钮,会发现仓库的数据变化了,而当前页面的数据没有变化,就丢失了响应式数据

js 复制代码
const {subCount, count, double} = useCountStore()

</script>
<template>
    <div>
        我是Son1--- {{ count }} - {{ double }} -
        <button @click="subCount()">-</button>
    </div>
</template>

解决方案

js 复制代码
import {storeToRefs} from "pinia"
// 保留响应式数据并结构
const store = useCountStore()
const {count, double} = storeToRefs(store)
// 函数结构
const {getList} store

store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value,就像 setup 中的 props 一样,如果你写了,我们也不能解构它,如果直接解构,就等于将数据复制到了解构的变量上,将失去响应式

pinia 持久化

安装依赖,pinia 版本必须 2.0 以上

npm 复制代码
yarn add pinia-plugin-persistedstate

挂载时有点不同

  1. 先将持久化挂哉到 pinia上
  2. 在将pinia挂载到 vue上

pinia 插件注册

js 复制代码
import {createPinia} from "pinia"
// 持久化插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 创建根实例
const app = createApp(App);
// 创建 Pinia 实例
const pinia = createPinia();
// 挂载
app.use(pinia.use(piniaPluginPersistedstate))

开启持久化

在仓库中第三个参数添加

js 复制代码
export const useCountStore = defineStore('counter', () => {

    },
    {
        // 开启持久化
        // persist: true,
        persist: {
            // 存储的kay名
            key: 'hm-counter',
            // 存储哪些数据
            paths: ['count']
        }
    }
)

pinia 独立维护

在 store 创建 index.js 文件

js 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersistedState from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedState)
export default pinia

在 main中使用

js 复制代码
import pinia from '@/stores'
app.use(pinia)

pinia 统一导出

由于导入太多太复杂希望路径统一

在 store index.js 文件中导入并导出仓库

js 复制代码
// 下面导入仓库,并导出出去
// import { useUserStore } from '@/stores/user'
// export { useUserStore }
// 等价于上面的写法
export * from '@/stores/user'

pinia vue2 安装

cmd 复制代码
npm install pinia  --save
npm i pinia @vue/composition-api --save

main.js

js 复制代码
import { createPinia, PiniaVuePlugin } from 'pinia'
import piniaPlugin from 'pinia-plugin-persistedstate'

const pinia = createPinia().use(piniaPlugin)
Vue.use(PiniaVuePlugin)

new Vue({

  pinia
}).$mount('#app')

基本使用查看上面的

优化包管理器以及校验标准

安装 全局 pnpm

cmd 复制代码
npm install -g pnpm

最后一个是美化代码的

pnpm dev 运行

优点:比同类工具快2倍左右、节省磁盘空间

在根目录.eslintrc.cjs文件中添加以下代码

js 复制代码
module.exports = {
    rules: {
        'prettier/prettier': [
            'warn',
            {
                singleQuote: true, // 单引号
                semi: false, // 无分号
                printWidth: 80, // 每行宽度至多80字符
                trailingComma: 'none', // 不加对象|数组最后逗号
                endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
            }
        ],
        'vue/multi-word-component-names': [
            'warn',
            {
                ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
            }
        ],
        'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验
        // 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
        'no-undef': 'error'
    }
}

代码提交前检查

全量检查,耗时比较久的

首先创建 git 仓库

安装依赖

cmd 复制代码
pnpm dlx husky-init

会生成一个文件夹

pre-commit:文件会在git项目提交前进行配置相关操作

bash 复制代码
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

// 项目提交前进行检查
pnpm lint

继续执行

  • git add .
  • git commit -m '提交初次'

如果有错误会提示,在哪一行那个位置的

为了保证仓库中自己的代码符合规范,只校验自己的代码,需要做以下调整

安装依赖

cmd 复制代码
pnpm i lint-staged -D

在 package.json 中添加

json 复制代码
{
  "name": "vue-big-events-admin",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    // 新增 lint-staged
    "lint-staged": "lint-staged"
  },
  // lint-staged: 这是配置 lint-staged 插件的部分
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix"
    ]
  }
}

修改 pre-commit 执行代码为

cmd 复制代码
pnpm lint-staged

element-plus 组件使用

依赖

cmd 复制代码
pnpm install element-plus

按需引入,安装相关依赖

cmd 复制代码
cnpm install -D unplugin-vue-components unplugin-auto-import

配置按需引入,在 vite.config.js 中

js 复制代码
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // 配置 element-plus 按需引入
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ]
})

此时就可以直接使用 element-plus 了,会自动注册

element 表单校验

  1. 要先设置数据存放
  2. 要设置校验规则
  3. 给元素加上v-model
  4. prop 设置校验那个规则
js 复制代码
// 表单数据存放
const formModel = ref({
  username: '',
  password: '',
  repassword: ''
})
// 表单校验规则
const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 5, max: 10, message: '用户名必须是 5 - 10 位', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码必须是 6-15 位且不能是非空',
      trigger: 'blur'
    }
  ]
}
html 复制代码
<!--   表单   -->
<el-form
    ref="form"
    :model="formModel"
    :rules="rules"
    size="large"
    autocomplete="off"
    v-if="isRegister"
>

<el-form-item prop="username">
  <el-input
    v-model="formModel.username"
    :prefix-icon="User"
    placeholder="请输入用户名"
  ></el-input>
</el-form-item>

<el-form-item prop="password">
  <el-input
    v-model="formModel.password"
    :prefix-icon="Lock"
    type="password"
    placeholder="请输入密码"
  ></el-input>
</el-form-item>

自定义校验规则

js 复制代码
// 表单校验规则
const rules = {
  repassword: [
    {
      // 自定义校验
      /**
       * rule:当前校验规则相关信息
       * value:当前校验的表达,当前值
       * callback:成功回调,必须调用,无论失败还是成功
       */
      validator: (rule, value, callback) => {
        // 判断值是不是和密码一致
        if (value !== formModel.value.password) {
          callback(new Error('两次输入密码不一致'))
        } else {
          callback()
        }
      },
      // 默认是change
      trigger: 'blur'
    }
  ]
}

校验整个表单

首先获取这个表单的 ref 对象

js 复制代码
const register = async () => {
  // 如果校验失败会自动提示
  await form.value.validate()
}

element 按需引入报错问题

由于引入了 element-ui 再次引入会触发样式问题,按需导入会自动移入,无需手动引入

解决 eslint 报错 .eslintrc.cjs文件中添加

js 复制代码
module.exports = {
    globals: {
        ElMessage: 'readonly',
        ElMessageBox: 'readonly',
        ElLoading: 'readonly'
    }
}

element ui plus 配置中文

可以包局部的组件,也可以包全局,实际是对内部变量做操作,zh-cn就是中文

在 app.vue 中,包住路由

js 复制代码
<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn'
</script>

<template>
  <div>
    <el-config-provider :locale="zhCn">
      <router-view />
    </el-config-provider>
  </div>
</template>

Element ui Upload 文件上传

js 复制代码
<script setup>
import { ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
const imageUrl = ref('')

/*
 * 文件上传的方法
 * 参数1, 单图片,参数2 多文件
 */
const onSelectFile = (upload, uploads) => {
  // console.log(upload)
  // 预览图片
  imageUrl.value = URL.createObjectURL(upload.raw)
  // 存入对象用于提交--将 upload.raw
}
</script>
<template>
  <el-form>
    <el-form-item label="文章封面">
      <!--   1. 导入文件上传
           :auto-upload="false" 关闭文件上传
           :on-change 文件发生改变的方法
      -->
      <el-upload
        class="avatar-uploader"
        :show-file-list="false"
        :auto-upload="false"
        :on-change="onSelectFile"
      >
        <img v-if="imageUrl" :src="imageUrl" class="avatar" />
        <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
      </el-upload>
    </el-form-item>
  </el-form>
</template>

富文本编辑器 vue quil(vue3 适配)

依赖

cmd 复制代码
npm i @vueup/vue-quill@latest -S

快速使用(局部引入)

js 复制代码
<script setup>
// 引入组件
import { QuillEditor } from '@vueup/vue-quill'
// 引入样式
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import { ref } from 'vue'

const n = ref('')
</script>
<template>
  <el-form>
    <el-form-item>
      <!--使用一个div会自动换行,这个组件应该有问题吧,不理解
      v-model:content 绑定数据源
      contentType 绑定数据
      -->
      <div>
        <QuillEditor theme="snow" v-model:content="n" contentType="html">
        </QuillEditor>
      </div>
    </el-form-item>
  </el-form>
</template>

小知识

添加和编辑快速写法

我们创建一个组件,在组件中写个弹框,父组件中调用组件的方法,使用扩展运算符将数据赋值给value,并打开弹窗

调用接口,判断表单数据中有没有ID,如果有id表示修改,如果没有是添加

ELement plus 封装了 date.js 可以直接使用

图片上传需要的是 file 格式,需要通过axios,将地址转换为file

相关推荐
世俗ˊ18 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92118 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_23 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人32 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript