Vue项目实战(移动端)
- 相关资料
- (一) 创建项目
- (二) 禁用Eslint
- (三) devtool
- (四) 添加less支持
- (五) vue路由配置(背诵)
- (六) 父子组件通信(背诵)
- (七) axios拦截器(背诵)
- (八) Sticky 粘性布局
- (九) 图片懒加载
- (十) 全局注册组件
- (十一) slot插槽
- (十二) 使用ui库需要关注的三点
- (十三) 三种路由传参方式(背诵)
- (十四) 模拟数据
- (十五) 计算属性computed和属性观察watch
- (十六) vuex(背诵)
- (十七) 浏览器缓存cookie,sessionStorage,localStorage
- (十八) token(令牌)和session(会话)
- (十九) vue过滤器
- (二十) 微信支付-轮询和websocket
- (二十一) 进入组件, 滚动条不在顶部的问题
- (二十二) keep-alive(背诵)
- (二十三) 配置环境变量
- (二十四) rem移动端适配
- (二十五) mixin(混入)
- (二十六) watch监听对象
- (二十七) props检查类型
- (二十八) ref获取dom节点和子组件实例
- (二十九) nextStick
- (三十) 配置跨域和模拟数据
相关资料
- vue-cli脚手架: vue2脚手架
- vue3脚手架: vite
- vue官网: [https://cn.vuejs.org/v2/guide/index.html
- vscode插件 - vetur 必备工具
- vue-helper 一些辅助功能
- Vue VSCode Snippets 片段
 
(一) 创建项目
01 安装vue-cli脚手架
npm install -g @vue/cli
02 查看vue脚手架版本
出现版本号表示成功
vue --version
03 创建一个新项目
创建项目
vue create hello-world  // 1.创建项目
运行项目
cd hello-world  // 2.进入项目文件夹
npm run serve		// 3.运行项目
(二) 禁用Eslint
// 根目录新增vue.config.js
module.exports = {
    lintOnSave: false
}
如果vue组件提示红色错误,如下图  解决办法: 文件 -> 首选项 -> 设置 然后输入eslint -> 选择Vetur -> 把√取消即可
 解决办法: 文件 -> 首选项 -> 设置 然后输入eslint -> 选择Vetur -> 把√取消即可 
(三) devtool
vue开发调试工具
- 下载 http://soft.huruqing.cn
- 添加到chrome扩展程序里
(四) 添加less支持
- npm install less less-loader@6.0.0 --save-dev
- 在vue文件这样写即可, scoped表示样式只在当前文件有效, 不会影响其他组件 - ps: less-loader要安装6.0版本, 不然有兼容问题 - <style lang="less" scoped> .box { .text { color: red; } } </style>
(五) vue路由配置(背诵)
(1)一个简单路由配置
- npm i vue-router安装路由插件
- 在src创建views文件夹, 创建各个模块的组件
- 在src内创建router文件夹, 新建index.js(代码如下)
- 在main.js里, 把router挂载到vue的实例
- 配置路由出口, 详见下方第(2)点router-view
- 使用router-link进行跳转, 详见下方第(3)点路由跳转
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router); 
// 路由数组
const routes = [
    {
        path: '/product',
        component: ()=>import('@/views/product/index.vue')
    },
    {
        path: '/cart',
        component: ()=>import('@/views/cart/index.vue')
    },
]
const router = new  Router({
    routes
})
export default router;
// main.js 代码
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
Vue.config.productionTip = false
new Vue({
  // 把router挂载到vue实例
  router,
  render: h => h(App),
}).$mount('#app')
(2) router-view
- 路由出口
- 路由匹配到的组件将渲染在这里
- 在app.vue配置
<template>
  <div id="app"> 
    <!-- 路由出口 -->
    <router-view></router-view>
  </div>
</template>
<script>
export default {
  name: "App",
  components: {},
};
</script>
(3) 路由跳转
// 方式一
<router-link to="/cart">cart</router-link>
// 方式二
this.$router.push('/cart');
(4) 子路由配置
使用子路由进行模块路由配置,结构比较分明 比如我们的网站有商品模块,有列表页面和详情页面, 路由如下 /product 商品模块总路由 /prodcut/list 子路由 /product/detail 子路由
{
    path: '/product',
    component: () => import('@/views/product/index'),
    children: [
        {
            path: 'list',
            component: ()=>import('@/views/product/children/list')
        },
        {
            path: 'detail',
            component: ()=>import('@/views/product/children/detail')
        }
    ]
}
(5) active-class
active-class是vue-router模块的router-link组件中的属性,用来做选中样式的切换;
- 只要路由中包含to里面的路由, 就能匹配到, 就会高亮, 比如: /product, /product/list, /product/detail都会使下面的第二个router-link高亮
- exact 表示精确匹配, 只有路由完全一样才能被匹配
<router-link to="/" active-class="on" exact>首页</router-link>
<router-link to="/product" active-class="on">product</router-link>
<router-link to="/cart" active-class="on">cart</router-link>
<router-link to="/my" active-class="on">my</router-link>
<router-link to="/order" active-class="on">order</router-link>
(6) history模式
vue2配置方式
- vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 
- 如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面 
- 使用history需要后端支持, vue-cli创建的devServer可以支持 - const router = new VueRouter({ mode: 'history', // 默认hash routes: [...] })
vue3配置方式
const router = createRouter({ 
 	history: createWebHistory(),  // history模式
 	//history: createWebHashHistory(), // hash模式
  	routes
});     
(7) redirect重定向
当访问 '/', 我们使用redirect使它默认跳到 '/product'
{
    path: '/',
    redirect: '/product'
},
(8) 404配置
假如用户访问了一个没有的路由, 我们让它跳转到404页面
  {
    path: '*',
    component:()=>import('@/components/NotFound')
  }
(六) 父子组件通信(背诵)
知识点(背诵):
- 父传子: 父组件通过(绑定)属性的方式传数据给子组件, 子组件使用props接收数据
- 子传父: 父组件在子组件上绑定一个自定义事件, 子组件通过$emit触发该自定义事件, 同时可以传入数据
1.父传子
- 父组件给子组件绑定属性, 属性的值是需要传递的信息
- 子组件通过props接收父组件的信息
 // 例子1: 使用普通属性
// demo.vue
<template>
  <div>
    <h3>父组件</h3>
    <hr />
    <Son msg="hello world" username="张三"/>
  </div>
</template>
<script>
import Son from "./Son";
export default {
  components: {
    Son,
  },
};
</script> 
// Son.vue
<template>
  <div>
    <h4>子组件</h4>
    <p>msg: {{ msg }}</p>
    <p>username: {{ username }}</p>
  </div>
</template>
<script>
export default {
  props: ["msg", "username"],
};
</script> 
// 例子2: 使用绑定属性(可传变量)
// demo.vue
<template>
  <div>
    <h3>父组件</h3>
    <hr />
    <Son :msg="msg" :username="username" />
  </div>
</template>
<script>
import Son from "./Son";
export default {
  components: {
    Son,
  },
  data() {
    return {
        msg: '哈哈哈',
        username: '李四'
    };
  },
};
</script> 
// Son.vue
<template>
  <div>
    <h4>子组件</h4>
    <p>msg: {{ msg }}</p>
    <p>username: {{ username }}</p>
  </div>
</template>
<script>
export default {
  props: ["msg", "username"],
};
</script> 
父传子实践: 把首页拆分为多个组件 技巧: 如果某个部分只是做展示用, 尽量把它变成子组件
2. 子传父
- 父组件在子组件上绑定一个自定义事件(事件名称我们自己定义的, vue本身是没有这个事件的)
- 父组件给自定义事件绑定一个函数, 这个函数可以接受来自子组件的数据
- 子组件使用$emit触发(调用)该事件, 并把数据以参数形式传给父组件
// 例子1: 一个简单的例子
// demo.vue
<template>
  <div>
    <h3>父组件</h3>
    <hr />
    <Son @aaa="say"/>
  </div>
</template>
<script>
import Son from "./Son";
export default {
  components: {
    Son,
  },
  data() {
    return { 
    };
  },
  methods: {
    say(data) {
      alert(data)
    }
  }
};
</script> 
// 子组件
<template>
  <div>
    <h4>子组件</h4>
    <button @click="$emit('aaa','我是子组件')">点击</button>
  </div>
</template>
<script>
export default {
  props: ["msg", "username"],
};
</script>  
(七) axios拦截器(背诵)
- 对ajax请求进行拦截 - 在请求头添加token
 
- 对ajax响应数据进行拦截 - 统一处理请求失败的情况, 这样就不需要在每个组件里处理失败的情况
- 有些接口需要登录才能访问, 在没登录的情况下跳转到登录页面
 
import axios from "axios";
import Vue from "vue";
import { Toast } from "vant";
Vue.use(Toast);
const service = axios.create({
  baseURL: "http://huruqing.cn:3003",
  timeout: 50000, // 请求超时时间(因为需要调试后台,所以设置得比较大)
});
// request 对请求进行拦截
service.interceptors.request.use(
  (config) => {
    // 开启loading
    Toast.loading({
      message: "加载中...",
      forbidClick: true,
      loadingType: "spinner",
    });
    // 请求头添加token
    config.headers["token"] =
      "gg12j3h4ghj2g134kj1g234gh12jh34k12h34g12kjh34kh1g";
    return config;
  },
  (error) => {
    Promise.reject(error);
  }
);
// response 响应拦截器
service.interceptors.response.use(
  (response) => {
    Toast.clear();
    const res = response.data;
    if (res.code == 666) {
      return res;
    } else {
      // 成功连接到后台, 但是没有返回正确的数据
      Toast.fail(res.msg);
    }
  },
  (error) => {
    Toast.clear();
    // 跟后台连接失败
    Toast.fail("网络异常,请稍后再试");
  }
);
export default service;
(八) Sticky 粘性布局
(九) 图片懒加载
http://huruqing.cn/Vue/new/02.html#_23-%E5%9B%BE%E7%89%87%E6%87%92%E5%8A%A0%E8%BD%BD
(十) 全局注册组件
// 注册全局组件除了多了个template之外,其它跟平时写组件类似
// 在main.js,实例化vue组件之前执行以下代码
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">你打了我 {{ count }} 次</button>'
})
// 在其他组件就可以使用
<template>
	<div>
  	<button-counter></button-counter>
  </div>  
</template>
// 改造checkbox, 官网例子
Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
// 然后就可以像下面这样来使用
<template>
  <div> 
   <base-checkbox v-model="flag"></base-checkbox>
		<p>{{flag}}</p>
  </div>
</template>
<script>
export default {
  data: function () {
    return {
      flag: false
    };
  },
}
</script>
// 另外需要在根目录的vue.config.js中开启运行时编译
module.exports = {
    runtimeCompiler: true
}
(十一) slot插槽
元素作为承载分发内容的出口 一个内存插槽, 当内存插上之后,插槽就可以接收来自内存的信息, slot取名插槽含义也贴切, 在子组件配置插槽slot, 当父组件"插"信息进来的时候, 插槽slot就能接收到这个信息. slot插槽大大的扩展子组件的功能。 

1. vant有赞ui库中slot的例子
<van-nav-bar title="标题" left-text="返回" left-arrow> 
   <p slot="right">
     <van-icon name="search" size="18" />
   </p>
</van-nav-bar>
2. 普通插槽
// 父组件demo.vue代码
<template>
  <div>
    <h3>父组件</h3>
    <hr>
    <Son><button>按钮</button></Son>
  </div>
</template>
<script>
import Son from "./Son";
export default {
  components: {
    Son,
  }
};
</script> 
// 子组件Son.vue
<template>
  <div>
    <slot></slot>
  </div>
</template> 
3. 具名插槽
// father.vue代码
<template>
  <div>
    <h3>这是父组件</h3>
    <Child>
      <header slot="header" style="background: yellow">这是头部</header>
      <footer slot="footer" style="background: green;">这是底部</footer>
      <div style="border:1px solid;">
        <button>a</button>
        <button>b</button>
        <button>c</button>
        <button>d</button>
      </div>
    </Child>
  </div>
</template>
<script>
import Child from "@/components/Child";
export default {
  components: {
    Child
  }
};
</script>
接收父组件带 slot="footer" 的内容
接收不带slot="xxx" 的内容
// Child.vue代码
<template>
  <div style="margin-top: 30px;background: gray;height: 200px;">
    <h5>这是子组件</h5>
		<!--接收父组件带 slot="header" 的内容-->
    <slot name="header"></slot>
		<!--接收父组件带 slot="footer" 的内容-->
    <slot name="footer"></slot>
		<!--接收剩余内容-->
    <slot></slot>
  </div>
</template>
自定义组件
// demo.vue
<template>
  <div> 
    <NavBar title="首页" @click-left="clickLeft" @click-right="clickRight"></NavBar>
  </div>
</template>
<script> 
import NavBar from './Nav-Bar.vue'
export default {
    components: {
      NavBar
    },
    methods:{
      clickLeft() {
        alert('左边被点击了'); 
      },
      clickRight() {
        alert('右边被点击了')
      }
    }
}
</script>
// Nav-Bar.vue
<template>
    <div class="nav flex jc-sb pl-15 pr-15 bg-fff aic">
      <p class="blue flec aic" @click="$emit('click-left')">
        <van-icon name="arrow-left" />
        <span>返回</span>
      </p>
      <p>{{title?title:'标题'}}</p>
      <slot name="right"> <span  class="blue" @click="$emit('click-right')">按钮</span></slot>
    </div>
</template>
<script>
export default {
  props: ['title']
}
</script> 
<style lang="less">
.nav {
  height: 50px;
  .blue {
    color: #1989fa;
  }
}
</style>
(十二) 使用ui库需要关注的三点
以vant 的导航栏组件van-nav-bar为例
- 属性, 该组件提供了哪些绑定属性
- 事件, 该组件提供了哪些事件
- 插槽, 该组件提供了哪些插槽
(十三) 三种路由传参方式(背诵)
知识点:
- 通过params传参, 使用$route.params接收参数
- 动态路由传参, 使用$route.params接收参数
- 通过query传参, $route.query接收参数
注意: router和route不是一回事 
1.通过name+params传参
// 1.配置路由的时候添加name
  {
        path: "detail",
        name: 'product-detail',
        component: () => import("@/views/order/children/detail"),
  },
// 2.跳转
 this.$router.push({
        // 要跳转到的路由名称
        name: 'product-detail',
         params: { productId: '123' }
      })
// 3.接收参数
this.$route.params.productId
2.动态路由传参
// 1.配置路由
{
  path: "detail/:productId", 
  component: () => import("@/views/product/children/detail.vue"),
},
  
// 2. 跳转
this.$router.push('/product/detai/22222')
<router-link to="/product/detail/333333">传参</router-link>
  
// 3.接收参数
 created() {
    let params = this.$route.params;
    console.log('params',params); 
  },
  
3.通过path+query传参
// 带查询参数,query传参会把参数拼接到地址栏,变成 /register?plan=aaa, 使用了path,参数不能通过params传递
this.$router.push({ path: '/register', query: { plan: 'aaa' }})
// 获取参数
this.$route.query;
(十四) 模拟数据
- 文档地址: https://www.npmjs.com/package/json-server
- npm i json-server -g //全局安装
- 根目录创建db.json
- 启动json-server
json-server --watch db.json
// db.json
{
  "posts": [
    { "id": 1, "title": "json-server", "author": "typicode" }
  ],
  "comments": [
    { "id": 1, "body": "some comment", "postId": 1 }
  ],
  "profile": { "name": "typicode" }
}
- 访问接口
http://localhost:3000/posts/1
- 将命令添加到package.json, 可以使用 npm run json 启动项目
 "scripts": {
    "json": "json-server --watch db.json" 
  },
(十五) 计算属性computed和属性观察watch
https://www.jianshu.com/p/e6e9071703dd
- computed的作用
- watch的作用
- computed和watch的区别
// computed
<template>
  <div>
    <p>姓: {{ xing }}</p>
    <p>名: {{ ming }}</p>
    <p>姓名: {{ xingming }}</p>
    <button @click="change">修改xing</button>
  </div>
</template>  
<script>
export default {
  data() {
    return {
      xing: "张",
      ming: "无忌",
    };
  },
  // 计算属性
  computed: {
    // xingming这个属性是由xing属性和ming计算得来
    xingming() {
      return this.xing + this.ming;
    },
  },
  methods: {
    change() {
      this.xing = "李";
    },
  },
};
</script>
(十六) vuex(背诵)
(1) 普通对象 VS vuex创建的对象
- 普通对象 - 创建对象
- 定义对象的属性
- 修改对象的属性
- 读取对象属性
 
- vuex - 创建仓库
- 定义状态
- 修改状态
- 读取状态
 
(2) 相关概念
- 概念vuex是什么: 创建一个仓库, 然后在仓库里定义若干状态, 并且管理这些状态. Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
- vuex有哪几个核心概念, 都是用来做什么的 - state 定义状态
- getters 派生状态
- mutation 修改状态(同步)
- action 修改状态(异步)
- module 模块化
 
- 如何使用vuex进行跨组件通信
- vuex持久化
// getters派生状态
// 1.  在 src/store/index.js
state: {
    token: "",
    username: "张三",
    age: 100,
    phone: "123456789",
  },
  getters: {
    // 派生状态
    str(state) {
      return `我叫${state.username},我的年龄是${state.age}`
    }
  },
    
// 2. 在组件里使用
    
<template>
	<div> 
  {{str}}
  </div>  
  
</template>    
    
import {mapGetters} from 'vuex';
export default {
 computed:{
 		...mapGetters(['str'])
 } 
}
// action 修改状态(异步)
- 定义状态
- 定义mutation, 通过mutation来修改状态
- 定义action , 通过action来提交(commit)mutation
- 用户派发action
import Vue from "vue";
import Vuex from "vuex";
import $http from '@/utils/http';
// 导入持久化插件
import createPersistedState from "vuex-persistedstate";
Vue.use(Vuex);
// 创建仓库
const store = new Vuex.Store({
  plugins: [createPersistedState()],
  // 1.定义状态
  state: {
    token: "",
    phone: "123456789",
    username: "张三",
    age: 100,
  },
  getters: {
    // 派生状态
    str(state) {
      return `我叫${state.username},我的年龄是${state.age}`
    }
  },
  // 2.定义mutaion
  mutations: {
    // 修改token
    set_token(state,payload) {
      state.token = payload
    },
    // 修改phone的状态
    set_phone(state, payload) {
      state.phone = payload;
    },
    /**
     * 定义修改username的muation
     * @param {*} state 状态
     * @param {*} payload 传入的新数据
     */
    set_username(state, payload) {
      state.username = payload;
    },
    // 定义修改age的mutation
    set_age(state, payload) {
      state.age = payload;
    },
  },
  // 3.定义action
  actions: {
    LOGOUT(store,payload) {
      $http.post('/user/logout').then(res=> {
          // 清除token和phone
          store.commit('set_token','');
          store.commit('set_phone','');
      })
    }
  } 
});
export default store;
// 4.退出登录时派发action
 <p class="red" @click="logout2">退出登录</p>
methods: {
	 logout2() {
      this.$store.dispatch('LOGOUT');
       this.$router.push('/my');
    },
}
// 模块化
// 1.定义模块的state getters mutaions actions
// src/store/modules/cart.js
export default {
    state: {
       cartNum: 100 
    },
    getters: { },
    mutaions: { },
    actions: {}
}
// src/store/modules/type.js
export default {
    state: {
       aaa: 333 
    },
    getters: { },
    mutaions: { },
    actions: {}
}
// 2.合并模块
import cart from './modules/cart';
const store = new Vuex.Store({
  modules:{
    cart,
    type, 
  },
}
                             
// 3.使用(在任何一个组件内)
<template>
	<div>
  	  {{ $store.state.cart.cartNum }}
      {{$store.state.type.aaa}}                            
  </div>                             
</template>                             
                             
(3) vuex应用
- 创建仓库 - 需要先安装vuex npm i vuex --save
- 创建仓库
- 挂载仓库
 
- 需要先安装vuex 
// 1. src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
// 创建仓库
const store = new Vuex.Store({
});
export default store;
// 2. 挂载到根实例 /src/main.js
import router from "./router/index";
import store from './store/index';
Vue.use(Vant);  
Vue.config.productionTip = false;
new Vue({
  store,
  router,
  render: (h) => h(App),
}).$mount("#app");
- 定义状态
const store = new Vuex.Store({
  // 定义状态
  state: {
    username: "张三",
    age: 100,
  },
});
- 获取状态- 直接获取 this.$store.state.username
 
- 直接获取 
<template>
  <div>
    <p>username: {{$store.state.username}}</p>
  </div>
</template>   
<script> 
export default {
    created() {
      console.log(this.$store.state);
    } 
};
</script>
- 通过mapState获取, mapState是vuex提供的方法, 可以让我们更方便的获取属性
<template>
  <div>
    <p>username: {{username}}</p>
    <p>age: {{age}}</p>
  </div>
</template>  
<script>
import {mapState} from 'vuex';
export default { 
    computed: {
      ...mapState(['username','age'])
    }
};
</script>
- 修改状态: 通过mutation进行修改- 修改状态只能通过mutation来修改, 不可以直接修改
- mutation只支持同步操作
 
- 步骤: - 定义mutation
- 提交mutation
 
// 1.定义mutation
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
// 创建仓库
const store = new Vuex.Store({
  // 定义状态
  state: {
    username: "张三",
    age: 100,
  },
  // 定义mutaion
  mutations: {
      /**
       * 定义修改username的muation 
       * @param {*} state 状态
       * @param {*} payload 传入的新数据
       */
      set_username(state,payload) {
        state.username = payload;
      },
      // 定义修改age的mutation
      set_age(state,payload) {
        state.age = payload;
      }
  } 
});
export default store;
// 2. 提交mutaion
<template>
  <div>
    <p>username: {{$store.state.username}}</p>
    <button @click="change">修改状态</button>
  </div>
</template>   
<script> 
export default {  
    methods: {
      change() {
        // 提交mutation,参数1 mutation的名称, 参数2 新的数据
        this.$store.commit('set_username','李四'); 
      }
    }
};
</script>
项目应用
- 定义一个状态 phone, 值为空
- 登录成功之后, 修改phone的状态
- 在个人中心页面, 获取phone状态 - 若有phone, 显示phone
- 若没有, 就显示立即登录
 
vuex持久化
- 安装插件 npm i vuex-persistedstate -S
- 应用插件
import Vue from "vue";
import Vuex from "vuex";
// 导入持久化插件
import createPersistedState from "vuex-persistedstate"; 
Vue.use(Vuex); 
an 
const store = new Vuex.Store({
  plugins: [createPersistedState()],
})
(十七) 浏览器缓存cookie,sessionStorage,localStorage
(1) 对比
- 三者都是浏览器缓存,可以将数据存储在浏览器上, 其中后两者是html5的新特性
- cookie存储容量较小,一般浏览器4KB, 后两者5M
- sessionStorage:临时存储, 浏览器关闭就销毁, localStorage: 永久存储, 销毁需要手动销毁
(2) 操作
- cookie使用相关js库 _js_-_cookie_
- sessionStorage,localStorage使用其自带方法
// 存储数据
localStorage.setItem(key,value);  // 比如:localStorage.setItem('username','张三')
// 获取数据
localStorage.getItem(key);        // 比如: localStorage.getItem('username');
// 清除数据
localStorage.clear();
(十八) token(令牌)和session(会话)
相同点: 两者都是用来识别用户的
- session会话, sessionId - 对于特定接口, 前端需要登录才能访问, 所以第一次访问时需要登录, 登录成功, 服务器会返回一个sessionId
- 下次前端再访问同一个接口的时候, 把sessionId带上(cookie), 这样服务器就能识别是谁在访问, 如果这个人已经登录过, 就不再需要再登录, session一般设有效期
 
- token令牌, 或叫同行证 - 前端在登录成功时, 服务器会把用户的相关信息加密, 得到一个密文, 这就是token, 返回给前端
- 前端再次访问接口时, 把token带上, 服务器端收到token就对它进行解密, 得到用户信息
 
项目应用
- 在vuex里定义token状态和相关的mutation
- 在登录成功的时候, 把token存入vuex
- 在axios的拦截器里, 把token放入请求头, 这样, 每次发请求的时候, 都会自动带上token
// 1.  在vuex里定义token状态和相关的mutation
state: {
    token: "",
    username: "张三",
    age: 100,
    phone: "123456789",
  },
  // 定义mutaion
  mutations: {
    // 修改token
    set_token(state,payload) {
      state.token = payload
    },
  }
  
  
  // 2. 在登录成功的时候, 把token存入vuex
   $http.post('/user/login',data).then(res=> {
        // 把手机号码存入store, 修改phone状态
        this.$store.commit('set_phone',this.phone);
        // 把token存入store
        this.$store.commit('set_token',res.result.token);
        // 从哪里来回哪里去
        this.$router.go(-1); 
      })
  // 3. 在axios的拦截器里, 把token放入请求头, 这样, 每次发请求的时候, 都会自动带上token
  
  
import axios from "axios";
import Vue from "vue";
import { Toast } from "vant";
// 导入store
import store from '@/store/index';
Vue.use(Toast);
  
  
// request 对请求进行拦截
service.interceptors.request.use(
  (config) => {
    // 获取token
    let token = store.state.token;  
    // 开启loading
    Toast.loading({
      message: "加载中...",
      forbidClick: true,
      loadingType: "spinner",
    });
    // 请求头添加token
    config.headers["user-token"] = token;
    return config;
  },
  (error) => {
    Promise.reject(error);
  }
);
(十九) vue过滤器
作用: 格式化数据
// 组件内的过滤器
<template>
  <div>
      {{num | f}}
  </div>
</template>   
<script>
export default {
  data() {
    return {
      num: 10
    }
  }, 
  filters: {
    f(num) {
      return Number(num).toFixed(2);
    }
  }
}
</script>
// 全局过滤器
Vue.filter('fMoney', (money)=> {
  let num = money/100;
  return num.toFixed(2);
})
new Vue({})
// 定义好全局过滤器后, 组件内可以直接使用
<template>
  <div>
      {{num | fMoney}}
  </div>
</template>   
<script>
export default {
  data() {
    return {
      num: 1000
    }
  }, 
}
</script>
(二十) 微信支付-轮询和websocket
(1) 微信支付流程
- 用户点击提交订单, 商户(服务器端)创建订单, 并返回订单信息和支付二维码给用户
- 用户扫码支付(货值调起微信支付)
- 支付平台收到钱后, 返回支付信息给用户, 同时通知商户(服务器端)已收到用户的钱
- 商户(服务器端)修改订单的状态
- 用户(web端)获取支付结果, 得到结果后做相应操作 - 轮询方式
- websocket
 
(2) 获取支付结果的两种方式
获取支付结果, 可以使用轮询或者websocket
- 轮询, 定时给服务器请求, 询问结果, 直到有结果为止, 轮询不需要服务器特别的支持
- websocket, 前端只需跟后台建立连接即可(长连接), 有了结果服务器可以给前端主动推送信息, websocket是长连接, 而http请求是一次性连接, websocket需要服务器端创建socket接口, 很多网站的客服服务就是使用websocket做的
// 轮询
<template>
  <div class="payment pay"></div>
</template>
<script>
export default {
  data() {
    return {
      timer: null,
      orderId: 'sdfasdfasdfasdfasdfasdfas'
    };
  },
  created() {
    this.waitResult();
  },
  beforeDestroy() {
    // 销毁定时器
    clearInterval(this.timer);
  },
  methods: {
    async waitResult() {
      // 创建定时器
      this.timer = setInterval(async () => {
        let res = await this.$axios.post("/order/detail", {
          orderId: this.orderId,
        });
        if (res.result.orderStatus === "01") {
          clearInterval(this.timer);
          // 支付成功, 返回首页
          this.$router.push("/");
        }
      }, 2000);
    },
  },
};
</script> 
// webSocket
<template>
  <div>{{result}}</div>
</template>
// webSocket
<template>
  <div>{{result}}</div>
</template>
<script>
export default { 
  data() {
    return {
      result: ''
    }
  },
  created() {
    this.connect();
  },
  methods: {
    connect() {
      this.result = '等待支付结果...';
      // 跟后端建立连接
      var ws = new WebSocket("ws://huruqing.cn:3003/socket");
      // onopen连接结果
      ws.onopen = () => {
        console.log("连接成功");
      };
      // 等待后端推送信息
      ws.onmessage = (res) => {  
        this.result = res.data;
      };
    },
  },
};
</script> 
(二十一) 进入组件, 滚动条不在顶部的问题
解决办法
// router/index.js
const routes = [...];
const router = new Router({
  mode: "history",
  scrollBehavior: () => ({
    y: 0
  }),
  routes
});
(二十二) keep-alive(背诵)
问题: 用户从列表的第3页, 点击某个商品进入了商品详情, 当用户点击返回的时候, 默认会返回到列表页的第一页而不是第3页, 这样的体验很不好, 所以我们希望可以回到列表页的原来位置, 这样的用户体验会比较好. 分析: 之所以会回到第一页, 是因为返回到列表页的时候, 组件会重新创建, 从新执行created方法, 所以页面页重新渲染 解决: 使用keep-alive可以缓存组件的状态, 具体做法: (1) 对列表页使用keep-alive, 使其即使离开了组件, 也不会销毁 组件挂载完毕的时候绑定滚动事件, 记录滚动的位置 (2) 从详情页返回的时候, 滚动的原来的位置(在activated生命周期) **注: **
- 被keep-live包裹的组件会被缓存
- 使用keep-alive的组件crated和mounted只会执行一次
- 离开组件会触发deactivated生命周期(只有被缓存的组件才有的生命周期)
- 进入组件会触发activated生命周期
// 方法1 APP.vue
<template>
  <div id="app"> 
    <keep-alive>
      <router-view></router-view>
    </keep-alive>
  </div>
</template> 
// 方法2, 给路由配置keepAlive属性
// (1) /router/index.js
 {
    path: "/product",
    component: () => import("@/views/product/index.vue"),
    redirect: "/product/list",
    children: [
      {
        path: "list",
        // 缓存次组件
        meta: {
          keepAlive: true,
          tittle: '列表'
        },
        component: () => import("@/views/product/children/list2.vue"),
      },
      {
        path: "detail/:productId",
        component: () => import("@/views/product/children/detail.vue"),
      },
    ],
  },
// APP.vue
<template>
  <div id="app"> 
      <!-- 渲染需要缓存的组件 -->
     <keep-alive>  
        <router-view v-if="$route.meta.keepAlive"></router-view>
     </keep-alive>
      <!-- 渲染不需要缓存的组件 -->
      <router-view v-if="!$route.meta.keepAlive"></router-view>
  </div>
</template> 
// 上面需求的实现
(1) 在mounted绑定window.scroll事件, 滚动的时候保存滚动条的位置
(2) 返回时候, 重新滚动到原来保存的位置 
  mounted() {
    window.addEventListener('scroll',()=>{  
      // 保存滚动条位置
      if (window.scrollY>0) {
          this.scrollHeight = window.scrollY;
      }
    },false);
  },  
  // 进入组件
  activated() { 
    // 滚动到最初的位置
    setTimeout(()=> {
      window.scrollTo(0,this.scrollHeight); 
    },0)
  },
(二十三) 配置环境变量
项目开发的时候, 一般会有多个环境, 比如开发环境, 测试环境, 生产环境, 我们调用接口的时候, 不同环境调用不同的接口, 所以要配置环境, ,方便访问。
// utils/http.js 核心代码
let env = process.env.NODE_ENV;
let baseURL;
// 开发环境
if (env === "development") {
  baseURL = "http://localhost:3003";
} else {
  baseURL = "http://huruqing.cn:3003";
}
const service = axios.create({
  // 如果换了新的项目, 需要更换为新的接口地址
  baseURL: baseURL,
  timeout: 50000, // 请求超时时间(因为需要调试后台,所以设置得比较大)
});
(二十四) rem移动端适配
(1) 元素单位有哪些:
(2) rem和根标签字体大小的关系
// rem例子 demo1.html
<!DOCTYPE html>
<html lang="en" style="font-size: 100px;">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style> 
        div{
            width: 1rem;
            height: 1rem;
            background-color: gray;
        }
    </style>
</head>
<body>
    <div>
    </div>
</body>
</html>
// rem例子 demo1.html
<!DOCTYPE html>
<html lang="en" style="font-size: 112px;">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style> 
        div{
            width: 1rem;
            height: 1rem;
            background-color: green;
        }
    </style>
</head>
<body>
    <div>
    </div>
</body>
</html>
(3) 移动端rem适配原理
- 设置一个设备参考值(比如iPhone6)
- 跟据设备宽度等比缩放根标签字体大小
(4) vue项目配置rem
- 安装插 npm i amfe-flexible --save
- 在main.js导入插件 import 'amfe-flexible'
- px自动转rem - 安装插件 npm i postcss-pxtorem@5.1.1
- 在/vue.config.js添加px2rem插件,把项目中的px转为rem
 
- 安装插件 
const pxtorem = require("postcss-pxtorem");
module.exports = {
  css: {
    loaderOptions: {
        // 后处理器配置
      postcss: {
        plugins: [
          // 把px转为rem
          pxtorem({
            rootValue: 37.5,
            propList: ["*"]
          })
        ]
      }
    }
  }
};
- 插件会修改html和body的字体大小, 而字体会继承, 所以要重新设置body的font-size为合适的字体大小
(二十五) mixin(混入)
mixin 其实是一个对象,里面的结构大致跟普通组件的 script 里面的一样,有 data 属性,钩子函数和方法等 混入 (mixins) 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
1.组件内混入
(1) 混入对象的生命周期先执行 (2) data里的状态若有重名, 取的是组件里的状态
// mixin.js
export default {
    data: function() {
      return {
        username: "huruqing",
        age: 100
      };
    },
    created() {
        console.log('这是混入对象')
    },
    methods: {
        say() {
            console.log('hahahhahahha');
        }
    }
  };
// demo.vue
<template>
  <div>
    <p>{{username}}</p>
    <p>{{msg}}</p>
    <p>{{age}}</p>
  </div>
</template>
<script>
import mixin from './mixin'
export default {
  mixins:[mixin],
  data() {
    return {
      username: '张三',
      msg: 'hahahahahahha'
    }
  },
  created() {
    console.log('组件的created');
    this.say();
  }
}
</script> 
2.全局混入
// mixin.js
export default {
    methods: {
        loadingStart() {
            this.$toast.loading({
                message: '加载中...',
                forbidClick: true,
                duration:0
              });
        },
        loadingFinish() {
            this.$toast.clear();
        }
    }
}
// main.js,这个代码放在Vue.use(Vant)之后
import mixin from '@/utils/mixin';
Vue.mixin(mixin);
// 其他组件就可以直接使用下面代码来显示loading
this.loadingStart(); 
(二十六) watch监听对象
- 普通的监听方法, 对对象无效
- watch的参数 - handler 监听器(有改变就执行
- immediate 马上执行
- deep 监听引用数据类型
 
<template>
  <div>
    <input type="text" v-model="obj.username" />
    <p>{{ obj.username }}</p>
  </div>
</template>
<script>
export default {
  data: function () {
    return {
      obj: {
        username: "张三",
      },
    };
  },
  watch: { 
    obj: {
      // 发生改变时执行的函数
      handler(newObj) {
        console.log(newObj.username);
      },
       // 首次绑定watch就执行
      immediate: true,
      // 深层监控,不设置,引用数据类型监控不到
      deep: true
    }
  },
};
</script>
(二十七) props检查类型
// demo.vue
<template>
  <div>
      <!-- 不传参数 -->
    <Son/> 
    <!-- 传了一个字符串 -->
    <!-- <Son :msg="'他是张三'" />  -->
    <!-- 传了一串数字 -->
    <!-- <Son :msg="22222"/> -->
  </div>
</template>
<script>
import Son from "./Son.vue";
export default {
  components: {
    Son,
  } 
};
</script> 
// Son.vue
<template>
  <div>{{msg}}</div>
</template>
<script>
export default {
    // props: ['msg']
    props:{
        msg: {
            type:String,
            default: 'hello'
        }
    }
}
</script> 
(二十八) ref获取dom节点和子组件实例
- ref可以获取原生dom节点和子组件实例
<template>
  <div>
    <span ref="demo">ref例子</span> 
    <button @click="handleClick">点击</button>
    <hr />
    <Son ref="son" />
  </div>
</template>
<script>
import Son from "./Son.vue";
export default {
  components: {
    Son,
  },
  methods: {
    handleClick() {
      console.log(this.$refs.demo.innerText);
      console.log(this.$refs.son.msg);
    },
  },
};
</script> 
// Son.vue
<template>
    <div>
        {{msg}}
    </div> 
</template>
<script>
export default { 
    data() {
        return {
            msg: 'hello',
            title: '2222222'
        }
    }
}
</script>
- 应用
// 父组件控制子组件的显示和隐藏(子组件无状态)
// demo.vue
<template>
  <div>
    <button @click="show = !show">点击</button>
    <hr>
    <Son :show="show" />
  </div>
</template>
<script>
import Son from "./Son.vue";
export default {
  components: {
    Son,
  },
  data() {
    return {
      show: true,
    };
  },
};
</script> 
// Son.vue
<template>
  <div v-if="show">
      <p>子组件内容</p>
      <p>子组件内容</p>
      <p>子组件内容</p>
      <p>子组件内容</p>
  </div>
</template>
<script>
export default {
    props: ['show'] 
}
</script> 
// 父组件控制子组件的显示和隐藏(子组件有状态)
<template>
  <div> 
    <button @click="$refs.aaa.show=!$refs.aaa.show">显示弹窗</button>  
    <Alert ref="aaa"/>
  </div>
</template>
<script> 
import Alert from './Alert.vue'
export default {  
  components: {
    Alert
  }, 
};
</script>
// Son.vue(假设子组件是别人设计的组件, 多处地方在使用, 所以并不适宜去改动, 不然容易改出bug, 子组件有个show的状态来控制其显示和隐藏
<template>
  <div v-if="show">
      <p>{{msg}}</p>
      <p>{{msg}}</p>
      <p>{{msg}}</p>
  </div>
</template>
<script>
export default { 
  data() {
    return {
      show: true,
      msg: 'hello vue'
    }
  }
}
</script> 
(二十九) nextStick
修改了数据之后, dom节点并不会立马更新, dom节点的更新是异步的, 想要拿到更新后的dom需要使用nextStick
<template>
  <div>
    <li ref="aa">{{ count1 }}</li>
    <li ref="bb">{{ count2 }}</li>
    <li ref="cc">{{ count3 }}</li>
    <button @click="handleClick">修改数据</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      count1: 0,
      count2: 0,
      count3: 0,
    };
  },
  methods: {
    handleClick() {
      this.count1 = 1;
      // this.count1=1;执行后dom并不会立即更新,dom节点的更新是异步的
      console.log(this.$refs.aa.innerHTML);  // 0
      // 当dom节点更新完毕, 会立即调用nextStick里的回调函数
      this.$nextTick(() => {
        console.log(this.$refs.aa.innerHTML);
      });
      this.count2 = 2;
      this.count3 = 3;
    },
  },
};
</script> 
(三十) 配置跨域和模拟数据
// 1. 在根目录新建mock文件夹
// 2. 添加/category/all.json和 /product/getBanners.json, json的数据就根据接口文档进行模拟
// 3. vue.config.js里配置
devServer: {
    // 代理
    proxy: {
      // 只要请求地址有'api'都会匹配上
      "/api": {
        target: "http://81.71.65.4:3003",
        ws: true,
        // 允许跨域
        changeOrigin: true,
        pathRewrite: {
          "^/api": "", //通过pathRewrite重写地址,将前缀/api转为/
        },
      },
    },
    before(app) { 
      // 模拟接口数据, 前面都加上了api是为了跨域设置的需要  
      // 分类列表
      app.get("/api/category/all", (req, res) => { 
        res.json(require('./mock/category/all.json'));
      });
      // banner列表
      app.get("/api/product/getBanners", (req, res) => { 
        res.json(require('./mock/product/getBanners.json'));
      });
    },
  },
