Vue2+Vue3基础入门到实战项目(前接六 副线一)—— 面经 项目

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Vue2+Vue3基础入门到实战项目(前接六 副线一)—— 面经 项目

day1

接口文档地址:https://www.apifox.cn/apidoc/project-934563/api-20384515

一、项目功能演示

1.目标

启动准备好的代码,演示移动端面经内容,明确功能模块

2.项目收获

二、项目创建目录初始化

vue-cli 建项目

1.安装脚手架 (已安装)

npm i @vue/cli -g

2.创建项目

vue create hm-vant-h5
  • 选项
Vue CLI v5.0.8
? Please pick a preset:
  Default ([Vue 3] babel, eslint)
  Default ([Vue 2] babel, eslint)
> Manually select features     选自定义
  • 手动选择功能

  • 选择vue的版本
3.x
> 2.x
  • 是否使用history模式

  • 选择css预处理

  • 选择eslint的风格 (eslint 代码规范的检验工具,检验代码是否符合规范)
  • 比如:const age = 18; => 报错!多加了分号!后面有工具,一保存,全部格式化成最规范的样子

  • 选择校验的时机 (直接回车)

  • 选择配置文件的生成方式 (直接回车)

  • 是否保存预设,下次直接使用? => 不保存,输入 N

  • 等待安装,项目初始化完成

  • 启动项目
npm run serve

三、ESlint代码规范及手动修复

代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格?一句结束是否是要加;?…

没有规矩不成方圆

ESLint:是一个代码检查工具,用来检查你的代码是否符合指定的规则(你和你的团队可以自行约定一套规则)。在创建项目时,我们使用的是 JavaScript Standard Style 代码风格的规则。

1.JavaScript Standard Style 规范说明

建议把:https://standardjs.com/rules-zhcn.html 看一遍,然后在写的时候, 遇到错误就查询解决。

下面是这份规则中的一小部分:

  • 字符串使用单引号 – 需要转义的地方除外
  • 无分号没什么不好。不骗你!
  • 关键字后加空格if (condition) { ... }
  • 函数名后加空格function name (arg) { ... }
  • 坚持使用全等 === 摒弃 == 一但在需要检查 null || undefined 时可以使用 obj == null
2.代码规范错误

如果你的代码不符合standard的要求,eslint会跳出来刀子嘴,豆腐心地提示你。

下面我们在main.js中随意做一些改动:添加一些空行,空格。

import Vue from 'vue'
import App from './App.vue'
import './styles/index.less'
import router from './router'
Vue.config.productionTip = false
new Vue ( {
  render: h => h(App),
  router
}).$mount('#app')

按下保存代码之后:

你将会看在控制台中输出如下错误:

eslint 是来帮助你的。心态要好,有错,就改。

3.手动修正

根据错误提示来一项一项手动修正。

如果你不认识命令行中的语法报错是什么意思,你可以根据错误代码(func-call-spacing, space-in-parens,…)去 ESLint 规则列表中查找其具体含义。

打开 ESLint 规则表,使用页面搜索(Ctrl + F)这个代码,查找对该规则的一个释义。

四、通过eslint插件来实现自动修正

  1. eslint会自动高亮错误显示
  2. 通过配置,eslint会自动帮助我们修复错误
  • 如何安装

  • 如何配置
// 当保存的时候,eslint自动帮我们修复错误
"editor.codeActionsOnSave": {
    "source.fixAll": true
},
// 保存代码,不自动格式化
"editor.formatOnSave": false
  • 注意:eslint的配置文件必须在根目录下,这个插件才能才能生效。打开项目必须以根目录打开,一次打开一个项目
  • 注意:使用了eslint校验之后,把vscode带的那些格式化工具全禁用了 Beatify

settings.json 参考

{
    "window.zoomLevel": 2,
    "workbench.iconTheme": "vscode-icons",
    "editor.tabSize": 2,
    "emmet.triggerExpansionOnTab": true,
    // 当保存的时候,eslint自动帮我们修复错误
    "editor.codeActionsOnSave": {
        "source.fixAll": true
    },
    // 保存代码,不自动格式化
    "editor.formatOnSave": false
}

五、调整初始化目录结构

强烈建议大家严格按照老师的步骤进行调整,为了符合企业规范

为了更好的实现后面的操作,我们把整体的目录结构做一些调整。

目标:

  1. 删除初始化的一些默认文件
  2. 修改没删除的文件
  3. 新增我们需要的目录结构

1.删除文件

  • src/assets/logo.png
  • src/components/HelloWorld.vue
  • src/views/AboutView.vue
  • src/views/HomeView.vue

2.修改文件

main.js 不需要修改

router/index.js

删除默认的路由配置

import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
]
const router = new VueRouter({
  routes
})
export default router

App.vue

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

3.新增目录

  • src/api 目录
  • 存储接口模块 (发送ajax请求接口的模块)
  • src/utils 目录
  • 存储一些工具模块 (自己封装的方法)

目录效果如下:

六、vant组件库及Vue周边的其他组件库

组件库:第三方封装好了很多很多的组件,整合到一起就是一个组件库。

https://vant-contrib.gitee.io/vant/v2/#/zh-CN/

比如日历组件、键盘组件、打分组件、登录组件等

组件库并不是唯一的,常用的组件库还有以下几种:

pc: element-uielement-plusiviewant-design

移动:vant-uiMint UI (饿了么) Cube UI (滴滴)

七、全部导入和按需导入的区别

目标:明确 全部导入按需导入 的区别

区别:

1.全部导入会引起项目打包后的体积变大,进而影响用户访问网站的性能

2.按需导入只会导入你使用的组件,进而节约了资源

八、全部导入

  • 安装vant-ui
yarn add vant@latest-v2
// 或者 npm i vant@latest-v2
  • 在main.js中
import Vant from 'vant';
import 'vant/lib/index.css';
// 把vant中所有的组件都导入了
Vue.use(Vant)
  • 即可使用
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>

vant-ui提供了很多的组件,全部导入,会导致项目打包变得很大。

九、按需导入

  • 安装vant-ui
npm i vant@latest-v2  或  yarn add vant@latest-v2
  • 安装一个插件
npm i babel-plugin-import -D
  • babel.config.js中配置
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
}
  • 按需加载,在main.js
import { Button, Icon } from 'vant'
Vue.use(Button)
Vue.use(Icon)
  • app.vue中进行测试
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
<van-button type="default">默认按钮</van-button>
<van-button type="warning">警告按钮</van-button>
<van-button type="danger">危险按钮</van-button>
  • 把引入组件的步骤抽离到单独的js文件中比如 utils/vant-ui.js
import { Button, Icon } from 'vant'
Vue.use(Button)
Vue.use(Icon)

main.js中进行导入

// 导入按需导入的配置文件
import '@/utils/vant-ui'

十、项目中的vw适配

官方说明:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/advanced-usage

yarn add postcss-px-to-viewport@1.1.1 -D
  • 项目根目录, 新建postcss的配置文件postcss.config.js
// postcss.config.js
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      viewportWidth: 375,
    },
  },
};

viewportWidth:设计稿的视口宽度

  1. vant-ui中的组件就是按照375的视口宽度设计的
  2. 恰好面经项目中的设计稿也是按照375的视口宽度设计的,所以此时 我们只需要配置375就可以了
  3. 如果设计稿不是按照375而是按照750的宽度设计,那此时这个值该怎么填呢?

十一、路由配置-一级路由

但凡是单个页面,独立展示的,都是一级路由

路由设计:

  • 登录页 (一级) Login
  • 注册页(一级) Register
  • 文章详情页(一级) Detail
  • 首页(一级) Layout
  • 面经(二级)Article
  • 收藏(二级)Collect
  • 喜欢(二级)Like
  • 我的(二级)My

一级路由

router/index.js配置一级路由, 一级views组件于准备好的中直接 CV 即可

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
Vue.use(VueRouter)
const router = new VueRouter({
  routes: [
    { path: '/login', component: Login },
    { path: '/register', component: Register },
    { path: '/article/:id', component: Detail },
    {
      path: '/',
      component: Layout
    }
  ]
})
export default router

清理 App.vue

<template>
  <div id="app">
    <router-view/>
  </div>
</template>
<script>
export default {
  created () {
  }
}
</script>

十二、路由配置-tabbar标签页

https://vant-contrib.gitee.io/vant/v2/#/zh-CN/tabbar

vant-ui.js 引入组件

import { Button, Icon, Tabbar, TabbarItem } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)

layout.vue

  1. 复制官方代码
  2. 修改显示文本及显示的图标
<template>
  <div class="layout-page">
    首页架子 - 内容区域 
    <van-tabbar>
      <van-tabbar-item icon="notes-o">面经</van-tabbar-item>
      <van-tabbar-item icon="star-o">收藏</van-tabbar-item>
      <van-tabbar-item icon="like-o">喜欢</van-tabbar-item>
      <van-tabbar-item icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>

十三、路由配置-配置主题色

整体网站风格,其实都是橙色的,可以通过变量覆盖的方式,制定主题色

https://vant-contrib.gitee.io/vant/v2/#/zh-CN/theme

babel.config.js 制定样式路径

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      // 指定样式路径
      style: (name) => `${name}/style/less`
    }, 'vant']
  ]
}

vue.config.js 覆盖变量

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  css: {
    loaderOptions: {
      less: {
        lessOptions: {
          modifyVars: {
            // 直接覆盖变量
            'blue': '#FA6D1D',
          },
        },
      },
    },
  },
})

重启服务器生效!

十四、路由配置-二级路由

1.router/index.js配置二级路由

在准备好的代码中去复制对应的组件即可

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
import Like from '@/views/Like'
import Article from '@/views/Article'
import Collect from '@/views/Collect'
import User from '@/views/User'
Vue.use(VueRouter)
const router = new VueRouter({
  routes: [
    { path: '/login', component: Login },
    { path: '/register', component: Register },
    { path: '/article/:id', component: Detail },
    { 
      path: '/',
      component: Layout,
      redirect: '/article',
      children: [
        { path: 'article', component: Article },
        { path: 'like', component: Like },
        { path: 'collect', component: Collect },
        { path: 'user', component: User }
      ]
    }
  ]
})
export default router

2.layout.vue 配置路由出口, 配置 tabbar

<template>
  <div class="layout-page">
    //路由出口
    <router-view></router-view> 
    <van-tabbar route>
      <van-tabbar-item to="/article" icon="notes-o">面经</van-tabbar-item>
      <van-tabbar-item to="/collect" icon="star-o">收藏</van-tabbar-item>
      <van-tabbar-item to="/like" icon="like-o">喜欢</van-tabbar-item>
      <van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>

十五、登录静态布局

使用组件

  • van-nav-bar
  • van-form
  • van-field
  • van-button

vant-ui.js 注册

import Vue from 'vue'
import {
  NavBar,
  Form,
  Field
} from 'vant'
Vue.use(NavBar)
Vue.use(Form)
Vue.use(Field)

Login.vue 使用

<template>
  <div class="login-page">
    <!-- 导航栏部分 -->
    <van-nav-bar title="面经登录" />
    <!-- 一旦form表单提交了,就会触发submit,可以在submit事件中
         根据拿到的表单提交信息,发送axios请求
     -->
    <van-form @submit="onSubmit">
      <!-- 输入框组件 -->
      <!-- \w 字母数字_   \d 数字0-9 -->
      <van-field
        v-model="username"
        name="username"
        label="用户名"
        placeholder="用户名"
        :rules="[
          { required: true, message: '请填写用户名' },
          { pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' }
        ]"
      />
      <van-field
        v-model="password"
        type="password"
        name="password"
        label="密码"
        placeholder="密码"
        :rules="[
          { required: true, message: '请填写密码' },
          { pattern: /^\w{6,}$/, message: '密码至少包含6个字符' }
        ]"
      />
      <div style="margin: 16px">
        <van-button block type="info" native-type="submit">提交</van-button>
      </div>
    </van-form>
  </div>
</template>
<script>
export default {
  name: 'LoginPage',
  data () {
    return {
      username: 'zhousg',
      password: '123456'
    }
  },
  methods: {
    onSubmit (values) {
      console.log('submit', values)
    }
  }
}
</script>

login.vue添加 router-link 标签(跳转到注册)

<template>
  <div class="login-page">
    <van-nav-bar title="面经登录" />
    <van-form @submit="onSubmit">
      ...
    </van-form>
    <router-link class="link" to="/register">注册账号</router-link>
  </div>
</template>

login.vue调整样式

<style lang="less" scoped>
.link {
  color: #069;
  font-size: 12px;
  padding-right: 20px;
  float: right;
}
</style>

十六、登录表单中的细节分析

  1. @submit事件:当点击提交按钮时会自动触发submit事件
  2. v-model双向绑定:会自动把v-model后面的值和文本框中的值进行双向绑定
  3. name属性:收集的key的值,要和接口文档对应起来
  4. label:输入的文本框的title
  5. :rules: 表单的校验规则
  6. placeholder: 文本框的提示语

十七、注册静态布局

Register.vue

<template>
  <div class="login-page">
    <van-nav-bar title="面经注册" />
    <van-form @submit="onSubmit">
      <van-field
        v-model="username"
        name="username"
        label="用户名"
        placeholder="用户名"
         :rules="[
          { required: true, message: '请填写用户名' },
          { pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' }
        ]"
      />
      <van-field
        v-model="password"
        type="password"
        name="password"
        label="密码"
        placeholder="密码"
        :rules="[
          { required: true, message: '请填写密码' },
          { pattern: /^\w{6,}$/, message: '密码至少包含6个字符' }
        ]"
      />
      <div style="margin: 16px">
        <van-button block type="primary" native-type="submit"
          >注册</van-button
        >
      </div>
    </van-form>
    <router-link class="link" to="/login">有账号,去登录</router-link>
  </div>
</template>
<script>
export default {
  name: 'Register-Page',
  data () {
    return {
      username: '',
      password: ''
    }
  },
  methods: {
    onSubmit (values) {
      console.log('submit', values)
    }
  }
}
</script>
<style lang="less" scoped>
.link {
  color: #069;
  font-size: 12px;
  padding-right: 20px;
  float: right;
}
</style>

十八、request模块 - axios封装

接口文档地址:https://apifox.com/apidoc/project-934563/api-20384515

基地址:http://interview-api-t.itheima.net/h5/

目标:将 axios 请求方法,封装到 request 模块

我们会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址,请求响应拦截器等等)

一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用

  1. 安装 axios
npm i axios
  1. 新建 utils/request.js 封装 axios 模块
    利用 axios.create 创建一个自定义的 axios 来使用
    http://www.axios-js.com/zh-cn/docs/#axios-create-config
/* 封装axios用于发送请求 */
import axios from 'axios'
// 创建一个新的axios实例
const request = axios.create({
  baseURL: 'http://interview-api-t.itheima.net/h5/',
  timeout: 5000
})
// 添加请求拦截器
request.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})
// 添加响应拦截器
request.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error)
})
export default request
  1. 注册测试
// 监听表单的提交,形参中:可以获取到输入框的值
async onSubmit (values) {
  console.log('submit', values)
  const res = await request.post('/user/register', values)
  console.log(res)
}

十九、封装api接口 - 注册功能

1.目标:将请求封装成方法,统一存放到 api 模块,与页面分离

2.原因:

以前的模式:

  • 页面中充斥着请求代码,
  • 可阅读性不高
  • 相同的请求没有复用请求没有统一管理

3.期望:

  • 请求与页面逻辑分离
  • 相同的请求可以直接复用请求
  • 进行了统一管理

4.具体实现

新建 api/user.js 提供注册 Api 函数

import request from '@/utils/request'
// 注册接口
export const register = (data) => {
  return request.post('/user/register', data)
}

register.vue页面中调用测试

methods: {
  async onSubmit (values) {
    // 往后台发送注册请求了
    await register(values)
    alert('注册成功')
    this.$router.push('/login')
  }
}

二十、toast 轻提示

https://vant-contrib.gitee.io/vant/v2/#/zh-CN/toast

两种使用方式

  1. 组件内js文件内 导入,调用
import { Toast } from 'vant';
Toast('提示内容');
  1. **组件内 **通过this直接调用

main.js

import { Toast } from 'vant';
Vue.use(Toast)
this.$toast('提示内容')

代码演示

this.$toast.loading({
    message:'拼命加载中...',
    forbidClick:true
})
try{
    await register(values)
    this.$toast.success('注册成功')
    this.$router.push('/login')
}catch(e){
    this.$toast.fail('注册失败')
}

二十一、响应拦截器统一处理错误提示

响应拦截器是咱们拿到数据的第一个“数据流转站”

import { Toast } from 'vant'
...
// 添加响应拦截器
request.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data
}, function (error) {
  if (error.response) {
    // 有错误响应, 提示错误提示
    Toast(error.response.data.message)
  }
  // 对响应错误做点什么
  return Promise.reject(error)
})

二十二、封装api接口 - 登录功能

api/user.js 提供登录 Api 函数

// 登录接口
export const login = (data) => {
  return request.post('/user/login', data)
}

login.vue 登录功能

import { login } from '@/api/user'
methods: {
  async onSubmit (values) {
    const { data } = await login(values)
    this.$toast.success('登录成功')
    localStorage.setItem('vant-mobile-exp-token', data.token)
    this.$router.push('/')
  }
}

二十三、local模块 - 本地存储

新建 utils/storage.js

const KEY = 'vant-mobile-exp-token'
// 直接用按需导出,可以导出多个
// 获取
export const getToken = () => {
  return localStorage.getItem(KEY)
}
// 设置
export const setToken = (newToken) => {
  localStorage.setItem(KEY, newToken)
}
// 删除
export const delToken = () => {
  localStorage.removeItem(KEY)
}

登录完成存储token到本地

import { login } from '@/api/user'
import { setToken } from '@/utils/storage'
methods: {
  async onSubmit (values) {
    const { data } = await login(values)
    setToken(data.token)
    this.$toast.success('登录成功')
    this.$router.push('/')
  }
}

day2

一、全局前置守卫-语法认识

这个 面经移动端 项目,只对 登录用户 开放,如果未登录,一律拦截到登录

  1. 如果访问的是 首页, 无token, 拦走
  2. 如果访问的是 列表页,无token, 拦走
  3. 如果访问的是 详情页,无token, 拦走

分析:哪些页面,是不需要登录,就可以访问的! => 注册登录 (白名单 - 游客可以随意访问的)

路由导航守卫 - 全局前置守卫

  • 访问的路径一旦被路由规则匹配到,都会先经过全局前置守卫
  • 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容

router/index.js

router.beforeEach((to, from, next) => {
  // 1. to   往哪里去, 到哪去的路由信息对象  
  // 2. from 从哪里来, 从哪来的路由信息对象
  // 3. next() 是否放行
  //    如果next()调用,就是放行
  //    next(路径) 拦截到某个路径页面
})

二、全局前置守卫-访问拦截处理

拦截或放行的关键点? → 用户是否有登录权证 token

核心逻辑:

  1. 判断用户有没有token, 有token, 直接放行 (有身份的人,想去哪就去哪~)
  2. 没有token(游客),如果是白名单中的页面,直接放行
  3. 否则,无token(游客),且在访问需要权限访问的页面,直接拦截到登录

// 全局前置守卫:
// 1. 所有的路由一旦被匹配到,在真正渲染解析之前,都会先经过全局前置守卫
// 2. 只有全局前置守卫放行,才能看到真正的页面
// 任何路由,被解析访问前,都会先执行这个回调
// 1. from 你从哪里来, 从哪来的路由信息对象
// 2. to   你往哪里去, 到哪去的路由信息对象
// 3. next() 是否放行,如果next()调用,就是放行 => 放你去想去的页面
//    next(路径) 拦截到某个路径页面
import { getToken } from '@/utils/storage'
const whiteList = ['/login', '/register'] // 白名单列表,记录无需权限访问的所有页面
router.beforeEach((to, from, next) => {
  const token = getToken()
  // 如果有token,直接放行
  if (token) {
    next()
  } else {
    // 没有token的人, 看看你要去哪
    // (1) 访问的是无需授权的页面(白名单),也是放行
    //     就是判断,访问的地址,是否在白名单数组中存在 includes
    if (whiteList.includes(to.path)) {
      next()
    } else {
      // (2) 否则拦截到登录
      next('/login')
    }
  }
})

三、面经列表-认识Cell组件-准备基础布局

1.认识静态结构

2.注册组件:

  • van-cell
import Vue from 'vue'
import { Cell } from 'vant'
Vue.use(Cell)

3.静态结构 Article.vue

<template>
  <div class="article-page">
    <nav class="my-nav van-hairline--bottom">
      <a
        href="javascript:;"
        >推荐</a
      >
      <a
        href="javascript:;"
        >最新</a
      >
      <div class="logo"><img src="@/assets/logo.png" alt=""></div>
    </nav>
    <van-cell class="article-item" >
      <template #title>
        <div class="head">
          <img src="http://teachoss.itheima.net/heimaQuestionMiniapp/%E5%AE%98%E6%96%B9%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F%402x.png" alt="" />
          <div class="con">
            <p class="title van-ellipsis">宇宙头条校招前端面经</p>
            <p class="other">不风流怎样倜傥 | 2022-01-20 00-00-00</p>
          </div>
        </div>
      </template>
      <template #label>
        <div class="body van-multi-ellipsis--l2">
          笔者读大三, 前端小白一枚, 正在准备春招, 人生第一次面试, 投了头条前端, 总共经历了四轮技术面试和一轮hr面, 不多说, 直接上题&nbsp;一面
        </div>
        <div class="foot">点赞 46 | 浏览 332</div>
      </template>
    </van-cell>
  </div>
</template>
<script>
export default {
  name: 'article-page',
  data () {
    return {
    }
  },
  methods: {
  }
}
</script>
<style lang="less" scoped>
.article-page {
  margin-bottom: 50px;
  margin-top: 44px;
  .my-nav {
    height: 44px;
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    z-index: 999;
    background: #fff;
    display: flex;
    align-items: center;
    > a {
      color: #999;
      font-size: 14px;
      line-height: 44px;
      margin-left: 20px;
      position: relative;
      transition: all 0.3s;
      &::after {
        content: '';
        position: absolute;
        left: 50%;
        transform: translateX(-50%);
        bottom: 0;
        width: 0;
        height: 2px;
        background: #222;
        transition: all 0.3s;
      }
      &.active {
        color: #222;
        &::after {
          width: 14px;
        }
      }
    }
    .logo {
      flex: 1;
      display: flex;
      justify-content: flex-end;
      > img {
        width: 64px;
        height: 28px;
        display: block;
        margin-right: 10px;
      }
    }
  }
}
.article-item {
  .head {
    display: flex;
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
    .con {
      flex: 1;
      overflow: hidden;
      padding-left: 10px;
      p {
        margin: 0;
        line-height: 1.5;
        &.title {
          width: 280px;
        }
        &.other {
          font-size: 10px;
          color: #999;
        }
      }
    }
  }
  .body {
    font-size: 14px;
    color: #666;
    line-height: 1.6;
    margin-top: 10px;
  }
  .foot {
    font-size: 12px;
    color: #999;
    margin-top: 10px;
  }
}
</style>

四、封装 ArticleItem 组件

说明:每个文章列表项,其实就是一个整体,封装成一个组件 → 可阅读性 & 复用性

步骤:

  • 新建 components/ArticleItem.vue 组件,贴入内容
  • 注册成全局组件
  • Article.vue 页面中应用

新建 components/ArticleItem.vue 组件

<template>
  <van-cell class="article-item">
    <template #title>
      <div class="head">
        <img  src="http://teachoss.itheima.net/heimaQuestionMiniapp/%E5%AE%98%E6%96%B9%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F%402x.png"
          alt=""
        />
        <div class="con">
          <p class="title van-ellipsis">宇宙头条校招前端面经</p>
          <p class="other">不风流怎样倜傥 | 2022-01-20 00-00-00</p>
        </div>
      </div>
    </template>
    <template #label>
      <div class="body van-multi-ellipsis--l2">
        笔者读大三, 前端小白一枚, 正在准备春招, 人生第一次面试, 投了头条前端,
        总共经历了四轮技术面试和一轮hr面, 不多说, 直接上题&nbsp;一面
      </div>
      <div class="foot">点赞 46 | 浏览 332</div>
    </template>
  </van-cell>
</template>
<script>
export default {
  name: 'ArticleItem'
}
</script>
<style lang="less" scoped>
.article-item {
  .head {
    display: flex;
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
    .con {
      flex: 1;
      overflow: hidden;
      padding-left: 10px;
      p {
        margin: 0;
        line-height: 1.5;
        &.title {
          width: 280px;
        }
        &.other {
          font-size: 10px;
          color: #999;
        }
      }
    }
  }
  .body {
    font-size: 14px;
    color: #666;
    line-height: 1.6;
    margin-top: 10px;
  }
  .foot {
    font-size: 12px;
    color: #999;
    margin-top: 10px;
  }
}
</style>

注册成全局组件使用

import ArticleItem from '@/components/ArticleItem.vue'
Vue.component('ArticleItem', ArticleItem)

Article.vue页面中

<template>
  <div class="article-page">
    ... 
    <ArticleItem></ArticleItem>
  </div>
</template>

五、封装 api 接口-获取文章列表数据

接口:https://apifox.com/apidoc/project-934563/api-20384521

1.新建 api/article.js 提供接口函数

import request from '@/utils/request'
export const getArticles = (obj) => {
  return request.get('/interview/query', {
    params: {
      current: obj.current,
      sorter: obj.sorter,
      pageSize: 10
    }
  })
}

2.页面中调用测试

import { getArticles } from '@/api/article'
export default {
  name: 'article-page',
  data () {
    return {
    }
  },
  async created () {
    const res = await getArticles({
      current: 1,
      sorter: 'weight_desc'
    })
    console.log(res)
  },
  methods: {
  }
}

3.发现 401 错误, 通过 headers 携带 token

注意:这个token,需要拼上前缀 Bearer token标识前缀

// 封装接口,获取文章列表
export const getArticles = (obj) => {
  const token = getToken()
  return request.get('/interview/query', {
    params: {
      current: obj.current, // 当前页
      pageSize: 10, // 每页条数
      sorter: obj.sorter // 排序字段 =>  传"weight_desc" 获取 推荐, "不传" 获取 最新
    },
    headers: {
      // 注意 Bearer 和 后面的空格不能删除,为后台的token辨识
      Authorization: `Bearer ${token}`
    }
  })
}

六、请求拦截器-携带 token

utils/request.js

每次自己携带token太麻烦,通过请求拦截器统一携带token更方便

import { getToken } from './storage'
// 添加请求拦截器
request.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  const token = getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

七、响应拦截器-处理token过期

说明:token 是有过期时间的 (6h),一旦 过期 或 失效 就无法正确获取到数据!

utils/request.js

// 添加响应拦截器
request.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data
}, function (error) {
  if (error.response) {
    // 有错误响应, 提示错误提示
    if (error.response.status === 401) {
      delToken()
      router.push('/login')
    } else {
      Toast(error.response.data.message)
    }
  }
  // 对响应错误做点什么
  return Promise.reject(error)
})

八、面经列表-动态渲染列表

article.vue

存储数据

import {getArticles} from '@/api/article'
data () {
  return {
    list: [],
    current: 1,
    sorter: 'weight_desc'
  }
},
async created () {
  const { data } = await getArticles({
    current: this.current,
    sorter: this.sorter
  })
  this.list = data.data.rows
},

v-for循环展示

<template>
  <div class="article-page">
    ...
    <ArticleItem v-for="(item,i) in list" :key="item.id" :item="item"></ArticleItem>
  </div>
</template>

子组件接收渲染

<template>
  <van-cell class="article-item" @click="$router.push(`/detail/${item.id}`)">
    <template #title>
      <div class="head">
        <img :src="item.avatar" alt="" />
        <div class="con">
          <p class="title van-ellipsis">{{ item.stem }}</p>
          <p class="other">{{ item.creator }} | {{ item.createdAt }}</p>
        </div>
      </div>
    </template>
    <template #label>
      <div class="body van-multi-ellipsis--l2" v-html="item.content"></div>
      <div class="foot">点赞 {{ item.likeCount }} | 浏览 {{ item.views }}</div>
    </template>
  </van-cell>
</template>
<script>
export default {
  name: 'ArticleItem',
  props: {
    item: {
      type: Object,
      default: () => ({})
    }
  }
}
</script>
<style lang="less" scoped>
.article-item {
  .head {
    display: flex;
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
    .con {
      flex: 1;
      overflow: hidden;
      padding-left: 10px;
      p {
        margin: 0;
        line-height: 1.5;
        &.title {
          width: 280px;
        }
        &.other {
          font-size: 10px;
          color: #999;
        }
      }
    }
  }
  .body {
    font-size: 14px;
    color: #666;
    line-height: 1.6;
    margin-top: 10px;
  }
  .foot {
    font-size: 12px;
    color: #999;
    margin-top: 10px;
  }
}
</style>

九、面经列表-响应拦截器-简化响应

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data
}, function (error) {
  // console.log(error)
  // 有错误响应,后台正常返回了错误信息
  if (error.response) {
    if (error.response.status === 401) {
      // 清除掉无效的token
      delToken()
      // 拦截到登录
      router.push('/login')
    } else {
      // 有错误响应,提示错误消息
      // this.$toast(error.response.data.message)
      Toast(error.response.data.message)
    }
  }
  // 对响应错误做点什么
  return Promise.reject(error)
})

Login.vue

setToken(data.token)

Article.vue

async created () {
  // 获取推荐的,第1页的10条数据
  const res = await getArticles({
    current: this.current,
    sorter: this.sorter
  })
  this.list = res.data.rows
},

十、面经列表-分页加载更多

https://vant-contrib.gitee.io/vant/v2/#/zh-CN/list

<van-list
  v-model="loading"
  :finished="finished"
  finished-text="没有更多了"
  @load="onLoad"
>
  <ArticleItem v-for="(item,i) in list" :key="i" :item="item"></ArticleItem>
</van-list>
data () {
  return {
    list: [],
    current: 1,
    sorter: 'weight_desc',
    loading: false,
    finished: false
  }
},
methods: {
  async onLoad () {
    const { data } = await getArticles({
      current: this.current,
      sorter: this.sorter
    })
    this.list = data.rows
  }
}

加载完成,重置 loading, 累加数据,处理 finished

async onLoad () {
  const { data } = await getArticles({
    current: this.current,
    sorter: this.sorter
  })
  this.list.push(...data.rows)
  this.loading = false
  this.current++
  if (this.current > data.pageTotal) {
    this.finished = true
  }
}

十一、面经列表-推荐和更新

1.切换推荐和最新 获取不同的数据

2.切换推荐和最新 点击的tab页签应该高亮

article.vue

<a
  @click="changeSorter('weight_desc')"
  :class="{ active: sorter === 'weight_desc' }"
  href="javascript:;"
  >推荐</a
>
<a
  @click="changeSorter(null)"
  :class="{ active: sorter === null }"
  href="javascript:;"
  >最新</a
>

提供methods

changeSorter (value) {
  this.sorter = value
  // 重置所有条件
  this.current = 1 // 排序条件变化,重新从第一页开始加载
  this.list = []
  this.finished = false // finished重置,重新有数据可以加载了
  // this.loading = false
  // 手动加载更多
  // 手动调用了加载更多,也需要手动将loading改成true,表示正在加载中(避免重复触发)
  this.loading = true
  this.onLoad()
}

十二、面经详情-动态路由传参-请求渲染

1.跳转路由传参

核心知识点:跳转路由传参

准备动态路由 (已准备)

const router = new VueRouter({
  routes: [
  ...,
    { path: '/article/:id', component: Detail },
    {
      path: '/',
      component: Layout,
      redirect: '/article',
      children: [
        ...
      ]
    }
  ]
})

点击跳转 article.vue

<template>
  <!-- 文章区域 -->
  <van-cell class="article-item" @click="$router.push(`/detail/${item.id}`)">
    <template #title>
      ...
    </template>
    <template #label>
      ...
    </template>
  </van-cell>
</template>

页面中获取参数

this.$route.params.id

2.动态渲染 (页面代码准备)

准备代码:

导入图标组件:

Vue.use(Icon)

静态结构:

<template>
  <div class="detail-page">
    <van-nav-bar
      left-text="返回"
      @click-left="$router.back()"
      fixed
      title="面经详情"
    />
    <header class="header">
      <h1>大标题</h1>
      <p>
        2050-04-06 | 300 浏览量 | 222 点赞数
      </p>
      <p>
        <img src="头像" alt="" />
        <span>作者</span>
      </p>
    </header>
    <main class="body">
      <p>我是内容</p>
      <p>我是内容</p>
      <p>我是内容</p>
      <p>我是内容</p>
    </main>
    <div class="opt">
      <van-icon class="active" name="like-o"/>
      <van-icon name="star-o"/>
    </div>
  </div>
</template>
<script>
export default {
  name: 'detail-page',
  data () {
    return {
      article: {}
    }
  },
  async created () {
  },
  methods: {
  }
}
</script>
<style lang="less" scoped>
.detail-page {
  margin-top: 44px;
  overflow: hidden;
  padding: 0 15px;
  .header {
    h1 {
      font-size: 24px;
    }
    p {
      color: #999;
      font-size: 12px;
      display: flex;
      align-items: center;
    }
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
  }
  .opt {
    position: fixed;
    bottom: 100px;
    right: 0;
    > .van-icon {
      margin-right: 20px;
      background: #fff;
      width: 40px;
      height: 40px;
      line-height: 40px;
      text-align: center;
      border-radius: 50%;
      box-shadow: 2px 2px 10px #ccc;
      font-size: 18px;
      &.active {
        background: #FEC635;
        color: #fff;
      }
    }
  }
}
</style>

3.代码实现

3.1封装api接口函数

api/article.js

export const getArticleDetail = (id) => {
  return request.get('interview/show', {
    params: {
      id
    }
  })
}

3.2动态渲染

Detail.vue

<template>
  <div class="detail-page">
    <van-nav-bar
      left-text="返回"
      @click-left="$router.back()"
      fixed
      title="面经详细"
    />
    <header class="header">
      <h1>{{ article.stem }}</h1>
      <p>
        {{ article.createdAt }} | {{ article.views }} 浏览量 |
        {{ article.likeCount }} 点赞数
      </p>
      <p>
        <img :src="article.avatar" alt="" />
        <span>{{ article.creator }}</span>
      </p>
    </header>
    <main class="body" v-html="article.content"></main>
    <div class="opt">
      <van-icon :class="{active:article.likeFlag}" name="like-o"/>
      <van-icon :class="{active:article.collectFlag}" name="star-o"/>
    </div>
  </div>
</template>
<script>
import { getArticleDetail } from '@/api/article'
export default {
  name: 'detail-page',
  data () {
    return {
      article: {}
    }
  },
  async created () {
    this.article = {}
    const { data } = await getArticleDetail(this.$route.params.id)
    this.article = data
  },
  methods: {
  }
}
</script>

十三、面经详情-点赞收藏

封装准备接口

api/article.js

export const updateLike = (id) => {
  return request.post('interview/opt', {
    id,
    optType: 1 // 喜欢
  })
}
export const updateCollect = (id) => {
  return request.post('interview/opt', {
    id,
    optType: 2 // 收藏
  })
}

Detail.vue

调用接口实现点赞收藏

<template>
  <div class="detail-page">
    <van-nav-bar
      left-text="返回"
      @click-left="$router.back()"
      fixed
      title="面经详细"
    />
    <header class="header">
      <h1>{{ article.stem }}</h1>
      <p>
        {{ article.createdAt }} | {{ article.views }} 浏览量 |
        {{ article.likeCount }} 点赞数
      </p>
      <p>
        <img :src="article.avatar" alt="" />
        <span>{{ article.creator }}</span>
      </p>
    </header>
    <main class="body" v-html="article.content"></main>
    <div class="opt">
      <van-icon @click="toggleLike" :class="{active:article.likeFlag}" name="like-o"/>
      <van-icon @click="toggleCollect" :class="{active:article.collectFlag}" name="star-o"/>
    </div>
  </div>
</template>
<script>
import { getArticleDetail, updateCollect, updateLike } from '@/api/article';
export default {
  name: 'detail-page',
  data() {
    return {
      article: {}
    };
  },
  async created() {
    this.article = {}
    const { data } = await getArticleDetail(this.$route.params.id)
    this.article = data;
  },
  methods: {
    async toggleLike () {
      await updateLike(this.article.id)
      this.article.likeFlag = !this.article.likeFlag
      if ( this.article.likeFlag ) {
        this.article.likeCount ++
        this.$toast.success('点赞成功')
      } else {
        this.article.likeCount --
        this.$toast.success('取消点赞')
      }
    },
    async toggleCollect () {
      await updateCollect(this.article.id)
      this.article.collectFlag = !this.article.collectFlag
      if ( this.article.collectFlag ) {
        this.$toast.success('收藏成功')
      } else {
        this.$toast.success('取消收藏')
      }
    }
  }
};
</script>
<style lang="less" scoped>
.detail-page {
  margin-top: 44px;
  overflow: hidden;
  padding: 0 15px;
  .header {
    h1 {
      font-size: 24px;
    }
    p {
      color: #999;
      font-size: 12px;
      display: flex;
      align-items: center;
    }
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
  }
  .opt {
    position: fixed;
    bottom: 100px;
    right: 0;
    > .van-icon {
      margin-right: 20px;
      background: #fff;
      width: 40px;
      height: 40px;
      line-height: 40px;
      text-align: center;
      border-radius: 50%;
      box-shadow: 2px 2px 10px #ccc;
      font-size: 18px;
      &.active {
        background: #FEC635;
        color: #fff;
      }
    }
  }
}
</style>

十四、我的收藏 (实战)

提供api方法

  • page: 表示当前页
  • optType:2 表示获取我的收藏数据

api/article.js

// 获取我的收藏
export const getArticlesCollect = (obj) => {
  return request.get('/interview/opt/list', {
    params: {
      page: obj.page, // 当前页
      pageSize: 5, // 可选
      optType: 2 // 表示收藏
    }
  })
}

collect.vue准备结构

<template>
  <div class="collect-page">
    <van-nav-bar fixed title="我的收藏" />
    <van-list
      v-model="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <ArticleItem v-for="(item, i) in list" :key="i" :item="item" />
    </van-list>
  </div>
</template>
<script>
import { getArticlesCollect } from '@/api/article'
export default {
  name: 'collect-page',
  data () {
    return {
      list: [],
      loading: false,
      finished: false,
      page: 1
    }
  },
  methods: {
    async onLoad () {
      // 异步更新数据
      const { data } = await getArticlesCollect({ page: this.page })
      this.list.push(...data.rows)
      this.loading = false
      this.page++
      if (this.page > data.pageTotal) {
        this.finished = true
      }
    }
  }
}
</script>
<style lang="less" scoped>
.collect-page {
  margin-bottom: 50px;
  margin-top: 44px;
}
</style>

十五、我的喜欢 (快速实现)

准备api函数

  • page: 表示当前页
  • optType:1 表示获取我的喜欢数据

api/article.js

// 获取我的喜欢
export const getArticlesLike = (obj) => {
  return request.get('/interview/opt/list', {
    params: {
      page: obj.page, // 当前页
      pageSize: 5, // 可选
      optType: 1 // 表示喜欢
    }
  })
}

Like.vue请求渲染

<template>
  <div class="like-page">
    <van-nav-bar fixed title="我的点赞" />
    <van-list
      v-model="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <ArticleItem v-for="(item,i) in list" :key="i" :item="item" />
    </van-list>
  </div>
</template>
<script>
import { getArticlesLike } from '@/api/article'
export default {
  name: 'like-page',
  data () {
    return {
      list: [],
      loading: false,
      finished: false,
      page: 1
    }
  },
  methods: {
    async onLoad () {
      // 异步更新数据
      const { data } = await getArticlesLike({ page: this.page })
      this.list.push(...data.rows)
      this.loading = false
      this.page++
      if (this.page > data.pageTotal) {
        this.finished = true
      }
    }
  }
}
</script>
<style lang="less" scoped>
.like-page {
  margin-bottom: 50px;
  margin-top: 44px;
}
</style>

十六、个人中心 (快速实现)

准备代码:

1 注册组件

import {
  Grid,
  GridItem,
  CellGroup
} from 'vant'
Vue.use(Grid)
Vue.use(GridItem)
Vue.use(CellGroup)

2 准备api

api/user.js

// 获取用户信息
export const getUserInfo = () => {
  return request('/user/currentUser')
}

3 页面调用渲染

<template>
  <div class="user-page">
    <div class="user">
      <img :src="avatar" alt="" />
      <h3>{{ username }}</h3>
    </div>
    <van-grid clickable :column-num="3" :border="false">
      <van-grid-item icon="clock-o" text="历史记录" to="/" />
      <van-grid-item icon="bookmark-o" text="我的收藏" to="/collect" />
      <van-grid-item icon="thumb-circle-o" text="我的点赞" to="/like" />
    </van-grid>
    <van-cell-group class="mt20">
      <van-cell title="推荐分享" is-link />
      <van-cell title="意见反馈" is-link />
      <van-cell title="关于我们" is-link />
      <van-cell @click="logout" title="退出登录" is-link />
    </van-cell-group>
  </div>
</template>
<script>
import { getUserInfo } from '@/api/user'
import { delToken } from '@/utils/storage'
export default {
  name: 'user-page',
  data () {
    return {
      username: '',
      avatar: ''
    }
  },
  async created () {
    const { data } = await getUserInfo()
    this.username = data.username
    this.avatar = data.avatar
  },
  methods: {
    logout () {
      delToken()
      this.$router.push('/login')
    }
  }
}
</script>
<style lang="less" scoped>
.user-page {
  padding: 0 10px;
  background: #f5f5f5;
  height: 100vh;
  .mt20 {
    margin-top: 20px;
  }
  .user {
    display: flex;
    padding: 20px 0;
    align-items: center;
    img {
      width: 80px;
      height: 80px;
      border-radius: 50%;
      overflow: hidden;
    }
    h3 {
      margin: 0;
      padding-left: 20px;
      font-size: 18px;
    }
  }
}
</style>

十七、打包发布

vue脚手架只是开发过程中,协助开发的工具,当真正开发完了 => 脚手架不参与上线

参与上线的是 => 打包后的源代码

打包:

  • 将多个文件压缩合并成一个文件
  • 语法降级
  • less sass ts 语法解析, 解析成css

打包后,可以生成,浏览器能够直接运行的网页 => 就是需要上线的源码!

打包命令

vue脚手架工具已经提供了打包命令,直接使用即可。

yarn build

在项目的根目录会自动创建一个文件夹dist,dist中的文件就是打包后的文件,只需要放到服务器中即可。

配置publicPath

module.exports = {
  // 设置获取.js,.css文件时,是以相对地址为基准的。
  // https://cli.vuejs.org/zh/config/#publicpath
  publicPath: './'
}

十八、路由懒加载

路由懒加载 & 异步组件, 不会一上来就将所有的组件都加载,而是访问到对应的路由了,才加载解析这个路由对应的所有组件

官网链接:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E4%BD%BF%E7%94%A8-webpack

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

const Detail = () => import('@/views/detail')
const Register = () => import('@/views/register')
const Login = () => import('@/views/login')
const Article = () => import('@/views/article')
const Collect = () => import('@/views/collect')
const Like = () => import('@/views/like')
const User = () => import('@/views/user')

PS: 如果想要手机上看到效果,可以将打包后的代码,上传到 gitee,利用 git pages 进行展示

相关文章
|
20天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
17天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
38 7
|
18天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
39 3
|
17天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
36 1
|
17天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
39 1
|
19天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
20天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
7天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
7天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
7天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。