Vue项目实战(移动端)

相关资料

  1. vue-cli脚手架: vue2脚手架
  2. vue3脚手架: vite
  3. vue官网: [https://cn.vuejs.org/v2/guide/index.html
  4. 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组件提示红色错误,如下图 image.png 解决办法: 文件 -> 首选项 -> 设置 然后输入eslint -> 选择Vetur -> 把√取消即可 image.png

(三) devtool

vue开发调试工具

  1. 下载 http://soft.huruqing.cn
  2. 添加到chrome扩展程序里

(四) 添加less支持

  1. npm install less less-loader@6.0.0 --save-dev

  2. 在vue文件这样写即可, scoped表示样式只在当前文件有效, 不会影响其他组件

    ps: less-loader要安装6.0版本, 不然有兼容问题

    <style lang="less" scoped> 
    .box {
      .text {
        color: red;
      }
    } 
    </style>
    

(五) vue路由配置(背诵)

(1)一个简单路由配置

  1. npm i vue-router  安装路由插件
  2. 在src创建views文件夹, 创建各个模块的组件
  3. 在src内创建router文件夹, 新建index.js(代码如下)
  4. 在main.js里, 把router挂载到vue的实例
  5. 配置路由出口, 详见下方第(2)点router-view
  6. 使用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

  1. 路由出口
  2. 路由匹配到的组件将渲染在这里
  3. 在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组件中的属性,用来做选中样式的切换;

  1. 只要路由中包含to里面的路由, 就能匹配到, 就会高亮, 比如: /product, /product/list, /product/detail都会使下面的第二个router-link高亮
  2. 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配置方式

  1. vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。

  2. 如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面

  3. 使用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')
  }

(六) 父子组件通信(背诵)

知识点(背诵):

  1. 父传子: 父组件通过(绑定)属性的方式传数据给子组件, 子组件使用props接收数据
  2. 子传父: 父组件在子组件上绑定一个自定义事件, 子组件通过$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. 子传父
  1. 父组件在子组件上绑定一个自定义事件(事件名称我们自己定义的, vue本身是没有这个事件的)
  2. 父组件给自定义事件绑定一个函数, 这个函数可以接受来自子组件的数据
  3. 子组件使用$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拦截器(背诵)

  1. 对ajax请求进行拦截
    1. 在请求头添加token
  2. 对ajax响应数据进行拦截
    1. 统一处理请求失败的情况, 这样就不需要在每个组件里处理失败的情况
    2. 有些接口需要登录才能访问, 在没登录的情况下跳转到登录页面
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插槽大大的扩展子组件的功能。 image.png

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为例

  1. 属性, 该组件提供了哪些绑定属性
  2. 事件, 该组件提供了哪些事件
  3. 插槽, 该组件提供了哪些插槽

(十三) 三种路由传参方式(背诵)

知识点:

  1. 通过params传参, 使用$route.params接收参数
  2. 动态路由传参, 使用$route.params接收参数
  3. 通过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;

(十四) 模拟数据

  1. 文档地址:  https://www.npmjs.com/package/json-server
  2. npm i json-server -g    //全局安装
  3. 根目录创建db.json
  4. 启动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" }
}
  1. 访问接口
http://localhost:3000/posts/1
  1. 将命令添加到package.json, 可以使用 npm run json 启动项目
 "scripts": {
    "json": "json-server --watch db.json" 
  },

(十五) 计算属性computed和属性观察watch

https://www.jianshu.com/p/e6e9071703dd

  1. computed的作用
  2. watch的作用
  3. 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创建的对象

  1. 普通对象
    1. 创建对象
    2. 定义对象的属性
    3. 修改对象的属性
    4. 读取对象属性
  2. vuex
    1. 创建仓库
    2. 定义状态
    3. 修改状态
    4. 读取状态

(2) 相关概念

  1. 概念vuex是什么: 创建一个仓库, 然后在仓库里定义若干状态, 并且管理这些状态. Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. vuex有哪几个核心概念, 都是用来做什么的
    1. state 定义状态
    2. getters 派生状态
    3. mutation 修改状态(同步)
    4. action 修改状态(异步)
    5. module 模块化
  3. 如何使用vuex进行跨组件通信
  4. 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 修改状态(异步)

  1. 定义状态
  2. 定义mutation, 通过mutation来修改状态
  3. 定义action , 通过action来提交(commit)mutation
  4. 用户派发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应用

  • 创建仓库
    1. 需要先安装vuex npm i vuex --save
    2. 创建仓库
    3. 挂载仓库
// 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,
  },
});
  • 获取状态
    1. 直接获取 this.$store.state.username
<template>
  <div>
    <p>username: {{$store.state.username}}</p>
  </div>
</template>   
<script> 
export default {
    created() {
      console.log(this.$store.state);
    } 
};
</script>
  1. 通过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>

项目应用

  1. 定义一个状态 phone, 值为空
  2. 登录成功之后, 修改phone的状态
  3. 在个人中心页面, 获取phone状态
    1. 若有phone, 显示phone
    2. 若没有, 就显示立即登录

vuex持久化

  1. 安装插件 npm i vuex-persistedstate -S
  2. 应用插件
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) 对比

  1. 三者都是浏览器缓存,可以将数据存储在浏览器上, 其中后两者是html5的新特性
  2. cookie存储容量较小,一般浏览器4KB, 后两者5M
  3. sessionStorage:临时存储, 浏览器关闭就销毁, localStorage: 永久存储, 销毁需要手动销毁

(2) 操作

  1. cookie使用相关js库 _js_-_cookie_
  2. sessionStorage,localStorage使用其自带方法
// 存储数据
localStorage.setItem(key,value);  // 比如:localStorage.setItem('username','张三')
// 获取数据
localStorage.getItem(key);        // 比如: localStorage.getItem('username');
// 清除数据
localStorage.clear();

(十八) token(令牌)和session(会话)

相同点: 两者都是用来识别用户的

  1. session会话, sessionId
    1. 对于特定接口, 前端需要登录才能访问, 所以第一次访问时需要登录, 登录成功, 服务器会返回一个sessionId
    2. 下次前端再访问同一个接口的时候, 把sessionId带上(cookie), 这样服务器就能识别是谁在访问, 如果这个人已经登录过, 就不再需要再登录, session一般设有效期
  2. token令牌, 或叫同行证
    1. 前端在登录成功时, 服务器会把用户的相关信息加密, 得到一个密文, 这就是token, 返回给前端
    2. 前端再次访问接口时, 把token带上, 服务器端收到token就对它进行解密, 得到用户信息

项目应用

  1. 在vuex里定义token状态和相关的mutation
  2. 在登录成功的时候, 把token存入vuex
  3. 在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) 微信支付流程

  1. 用户点击提交订单, 商户(服务器端)创建订单, 并返回订单信息和支付二维码给用户
  2. 用户扫码支付(货值调起微信支付)
  3. 支付平台收到钱后, 返回支付信息给用户, 同时通知商户(服务器端)已收到用户的钱
  4. 商户(服务器端)修改订单的状态
  5. 用户(web端)获取支付结果, 得到结果后做相应操作
    1. 轮询方式
    2. websocket

(2) 获取支付结果的两种方式

获取支付结果, 可以使用轮询或者websocket

  1. 轮询, 定时给服务器请求, 询问结果, 直到有结果为止, 轮询不需要服务器特别的支持
  2. 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生命周期) **注: **

  1. 被keep-live包裹的组件会被缓存
  2. 使用keep-alive的组件crated和mounted只会执行一次
  3. 离开组件会触发deactivated生命周期(只有被缓存的组件才有的生命周期)
  4. 进入组件会触发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适配原理
  1. 设置一个设备参考值(比如iPhone6)
  2. 跟据设备宽度等比缩放根标签字体大小
(4) vue项目配置rem
  1. 安装插 npm i amfe-flexible --save
  2. 在main.js导入插件 import 'amfe-flexible'
  3. px自动转rem
    1. 安装插件 npm i postcss-pxtorem@5.1.1
    2. 在/vue.config.js添加px2rem插件,把项目中的px转为rem
const pxtorem = require("postcss-pxtorem");

module.exports = {
  css: {
    loaderOptions: {
        // 后处理器配置
      postcss: {
        plugins: [
          // 把px转为rem
          pxtorem({
            rootValue: 37.5,
            propList: ["*"]
          })
        ]
      }
    }
  }
};
  1. 插件会修改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监听对象

  1. 普通的监听方法, 对对象无效
  2. watch的参数
    1. handler 监听器(有改变就执行
    2. immediate 马上执行
    3. 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节点和子组件实例

  1. 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>
  1. 应用
// 父组件控制子组件的显示和隐藏(子组件无状态)
// 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'));
      });

    },
  },