第一个vue项目

1:搭建vue-cli脚手架

目录

1:搭建vue-cli脚手架

2:对vue文件的一些认识

3写好各组件的的样式,结构

4:路由组件的搭建vue-router

4.1:配置路由

4.2小结

4.3路由跳转的形式

4.4路由传参

4.4全局组件的注册与使用

5:对axios进行二次封装

6:nprogress进度条的使用

7:vuex的使用

7.1:vuex实现模块式开发,其实就是在store里面为每个模块建一个小仓库

8动态渲染三级联动的数据

9节流与防抖:

10:三级联动的路由跳转以及参数的传递。

11:三级联动在不同组件中实现显示与隐藏

11.1显示与隐藏的动画实现

12:利用swiper组件写轮播图

13:Search模块的开发。

14:getter()方法简化获取数据

15:面包屑相关问题

15.1:组件间的通信

 15.2:子向父传递数据

16 排序模块

17:Detail详情模块的开发(步骤:静态组件,路由,请求接口数据,传递参数)

17.1引入静态组件

17.2路由模块

17.3获取api中的数据,之前类似的写过很多次了,这里就简单介绍下步骤

17.4动态展示数据

 17.5点击谁,谁高亮的效果

17.6 放大镜

18:购物车模块

19购物车列表模块

 20登录注册业务

20.1注册模块

20.2登录模块

21交易模块

 22支付模块

 22.1生成二维码

 23个人中心

24图片懒加载vue-lazyload

 25:表单验证

26:路由懒加载

27打包文件(前台项目完结撒花!!!)

28:购买服务器,发布项目


首先要确定我们电脑有安装node,webpack,以及淘宝镜像cnpm

搭建vue-cli脚手架初始化项目

第一个vue项目_第1张图片

创建一个名为app的vue项目

第一个vue项目_第2张图片  

接着选择vue2,等待下载

2:对vue文件的一些认识

第一个vue项目_第3张图片

关闭eslint校验工具,比如我们申明一个变量但是未使用,这时vue工具会给我们报错,关闭此校验就不会有这个问题。

在根目录下创建vue.config.js文件

module.exports = {
    // 关闭ESLINT校验工具
    lintOnSave: false,
};

 可以给src配置别名为@,因为后期项目经常会用到src,查找的时候也方便

具体方法,在jsconfig.json里面配置,代码如下

{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "@/*": [
                "src/*"
            ]
        }
    },
    "exclude": [
        "node_modules",
        "dist"
    ]
}

3写好各组件的的样式,结构

(此处省略一万行代码)

4:路由组件的搭建vue-router

先安装vue-router ,在黑窗口中输入cnpm install --save vue-router

4.1:配置路由

路由组件一般放在pages文件夹里面

然后创建router文件夹,对路由组件进行管理

// 配置路由
import Vue from 'vue';
import VueRouter from 'vue-router';

// 使用插件
Vue.use(VueRouter);

//  引入路由组件
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Login from '@/pages/Login'
import Register from '@/pages/Register'

export default new VueRouter({
    //  配置路由
    routes: [
        {
            path: "/home",
            component: Home
        },
        {
            path: "/search",
            component: Search
        },
        {
            path: "/login",
            component: Login
        },
        {
            path: "/register",
            component: Register
            }
        }
    ]
})

再回到文件的入口文件中引入路由

// 引入路由
import router from '@/router'

new Vue({
  render: h => h(App),
//注册路由
  router
}).$mount('#app')

最后回到app.vue中展示路由(使用router-view)

4.2小结


路由组件与非路由组件的区别?
1:路由组件一般放置在pages|views文件夹,非路由组件一般放置components文件夹中
2:路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以标签的形式使用
3:注册完路由,不管路由路由组件、还是非路由组件身上都有$route、$router属性
$route:一般获取路由信息【路径、query、params等等】
$router:一般进行编程式导航进行路由跳转【push|replace】

4.3路由跳转的形式

声明式导航和编程式导航

声明式导航:

//to代表要去的地方
登录

编程式导航: 

 methods: {
    // 向Search路由进行跳转
    goSearch() {
      //按钮中的的gosearch方法
      this.$router.push("/search")
    }
}

路由原信息meta

例如

routes: [
        {
            path: "/home",
            component: Home,
            meta: {
                show: true
            }
        },

访问时可以通过$route.meta.show

4.4路由传参

goSearch() {
      // 路由传递参数:
      // 第一种:字符串形式
      // this.$router.push("/search/"+this.keyword+"?k="+this.keyword.toUpperCase());
      // 第二种:模板字符串
      // this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()})`);
      // 第三种:对象(最常用)
      // 对象形式传参要写name!!
      this.$router.push({
        name: "search",
        params: { keyword: this.keyword },
        query: { k: this.keyword.toUpperCase() },
      });
    },
  },

params传参要进行占位

// 传params参数要进行占位,问号代表params参数可传可不传
 path: "/search/:keyword?",

4.4全局组件的注册与使用

import TypeNav from '@/components/TypeNav'
// 第一个参数 全局组件的名字  第二个参数   哪一个组件
Vue.component(TypeNav.name, TypeNav)

使用时直接写标签,不需要引入了。

5:对axios进行二次封装

向服务器发请求的方法有:
XMLHttpRequest、fetch、JQ、axios(最常用)
为什么需要进行二次封装axios?
请求拦截器、响应拦截器:请求拦截器,可以在发请求之前可以处理一些业务、响应拦截器,当服务器数据返回以后,可以处理一些事情。

通常在src下新建api文件夹,然后在request.js文件中对axios进行二次封装

// 对axios进行二次封装
import axios from 'axios';

// 1 利用axios对象的方法create,去创建一个axios实例
// 2:request就是axios,只不过稍微配置一下
const requests = axios.create({
    // 配置对象
    // 基础路径,发请求的时候,路径当中会出现api
    baseURL: "/api",
    // 请求超时的时间
    timeout: 5000
});
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
// interceptors拦截
requests.interceptors.request.use((config) => {
    // config:配置对象,独享里面有一个属性很重要,包含header请求头
    return config;
});

// 响应拦截器
requests.interceptors.response.use((res) => {
    //成功的回调函数:服务器响应的数据回来后,响应拦截器可以检测到,可以做一些事情
    return res.data;
}, (error) => {
    // 响应失败的回调函数
    // 比如终结promise
    return Promise.reject(new Error('fail'));
});

export default requests;

当我们需要向后台发送数据时,可以在api文件夹下的index.js文件写发送请求的代码

// 当前这个模块: 对API进行统一的管理
import requests from './request';

// 三级联动接口 api/product/getBaseCategoryList   get方法  无参数
export const reqCategoryList = () => {
    // 发请求  axios发请求返回结果是promise对象
    return requests({ url: '/product/getBaseCategoryList', method: 'get' });
}

在main.js中引入,并测试能否从后台服务器获取到数据

import { reqCategoryList } from '@/api'

reqCategoryList()

测试后我们发现会遇到跨域的问题,跨域就是协议,域名,端口号不同的请求。

一般的解决方法有jsonp,cros,代理

这里我们用代理的方法来解决

我们在项目中的vue.config.js文件中引入如下代码段

// 解决跨域  webpack devserve里面赋值一段代码
    // 这里是代理跨域
    devServer: {
        proxy: {
            '/api': {
                target: 'http://39.98.123.211',
                // pathRewrite: { '^/api': '' },
            },
        },
    }

6:nprogress进度条的使用

安装nprogress插件   cnpm install --save nprogress

我们在二次封装axios的文件中去引入nprogress

当请求拦截器捕获到请求的时候,进度条开始动

当服务器返回数据成功后进度条结束


// 引入进度条
import nprogress from 'nprogress'
// 引入进度条样式
import "nprogress/nprogress.css"
// start 进度条开始  done: 进度条结束

const requests = axios.create({
    baseURL: "/api",
    timeout: 5000
});
// interceptors拦截
requests.interceptors.request.use((config) => {
    // 进度条开始动
    nprogress.start()
    return config;
});
requests.interceptors.response.use((res) => {
    // 进度条结束
    nprogress.done();
    return res.data;
}, (error) => {
    return Promise.reject(new Error('fail'));
});

效果图 

 

进度条颜色可以在nprogress.css文件中修改

 

7:vuex的使用

vuex是什么?vuex是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据,但并不是所有的项目都需要vuex,项目小就不需要。

首先安装vuex:   cnpm install --save vuex

这里我们需要注意安装vuex的版本,我之前下的是4.0.2的版本导致崩了,下回3的版本才解决错误。。。。。

vuex的使用:

在store文件夹中,index.js代码如下:

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);

// state:仓库存储数据的地方
const state = {};

// mutations:修改state的唯一手段
const mutations = {};

// action:处理action,可以书写自己的业务逻辑,也可以处理异步任务
const actions = {};

// getters 理解为计算属性,用于简化仓库数据的,让仓库获取数据更加简单
const getters = {};

// 对外暴露Store类的一个实例
export default new Vuex.Store({
        state,
        mutations,
        actions,
        getters
})

同样,需要在入口文件中引入并注册。

7.1:vuex实现模块式开发,其实就是在store里面为每个模块建一个小仓库

第一个vue项目_第4张图片

// home模块的小仓库
// state:仓库存储数据的地方
const state = {
    a: 1
};
// mutations:修改state的唯一手段
const mutations = {};
// action:处理action,可以书写自己的业务逻辑,也可以处理异步任务
const actions = {};
// getters 理解为计算属性,用于简化仓库数据的,让仓库获取数据更加简单
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters
}

在store里面暴露一下就好啦

// 对外暴露Store类的一个实例
export default new Vuex.Store({
    modules: {
        home,
        search
    }

})

8动态渲染三级联动的数据

三级联动的组件是TypeNav,首先在TypeNav组件中向vuex发送请求

// 组件挂载完毕,可以向服务器发送请求
  mounted() {
    // 通知vuex发送请求,将数据存储于仓库中
    this.$store.dispatch("categoryList");
  },

由于是在home模块,所以找到home模块的小仓库,对actions,mutations,state函数进行完善

// home模块的小仓库
import { reqCategoryList } from '@/api'
const state = {
    categoryList: []
};
const mutations = {
    CATEGORYLIST(state, categoryList) {
        state.categoryList = categoryList
    }
};
const actions = {
    //通知API里面的接口函数,向服务器发送请求,获取服务器的数据
    async categoryList({ commit }) {               //解构commit
        let result = await reqCategoryList();
        if (result.code == 200) {
            commit("CATEGORYLIST", result.data)    //向mutations发送数据
        }

    }
};

最后回到TypeNav中即可获取到数据

computed: {
    ...mapState({
      // 注入了一个参数,即为大仓库中的数据
      categoryList: (state) => {
        return state.home.categoryList
      },
    }),
  },

然后在上面进行渲染就ok啦

三级联动的动态背景:

首先给元素绑定一个鼠标经过触发事件 @mouseenter="changeindex(index)"

函数为changeindex(index) { this.currentIndex = index;}

然后通过判断给盒子动态添加样式(cur为自定义样式)

最后记得定义鼠标离开事件mouseleave。

9节流与防抖:

节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发 只会执行一次节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发。

简单来说就是

防抖:用户操作很频繁,但是只是执行一次
节流:用户操作很频繁,但把频繁的操作变为少量操作(可以给浏览器有充裕的时间解析代码)

三级联动的节流:使用node包里面的lodash

1 引入lodash:  import _ from 'lodash'

但是最好按需引入

import { throttle } from "lodash";

//下面是es5的写法   为了写节流函数
    changeindex: throttle(function (a) {
      this.currentIndex = a;
    }, 50),

throttle函数不要用箭头函数,防止出现this指向问题

10:三级联动的路由跳转以及参数的传递。

①不能用声明式导航,声明式导航会出现卡顿现象,因为router-link是一个组件,当服务器返回数据时会循环出很多组件实例,导致卡顿。所以我们使用编程式导航,结合事件委派来写。

第一个vue项目_第5张图片

存在一些问题:

①事件委派,是把全部的子节点【h3、dt、dl、em】的事件委派给父亲节点

②点击a标签的时候,才会进行路由跳转【怎么能确定点击的一定是a标签】

③存在另外一个问题:即使你能确定点击的是a标签,如何区分是一级、二级、三级分类的标签。

我们可以给每一个子节点当中a标签加上自定义属性data-categoryName,其余的子节点是没有的

 例如  :data-categoryName="c1.categoryName"  :data-category1Id="c1.categoryId"

goSearch(event) {
      //获取节点
      let element = event.target;

      //节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
      //注意自定义属性会自动变成小写!
      let { categoryname,category1id,category2id,category3id }=element.dataset;
      
      if (categoryname) {
        // 整理路由跳转的参数
        let location = { name: "search" };
        let query = { categoryName: categoryname };

        //一级分类、二级分类、三级分类的a标签
        if (category1id) {
          query.catrgory1Id = category1id;
        } else if (category2id) {
          query.catrgory2Id = category2id;
        } else {
          query.catrgory3Id = category3id;
        }
        // 整理完参数
        // console.log("12", location, query);
        location.query = query;

        // 路由跳转
        this.$router.push(location);
      }
    },
  },

11:三级联动在不同组件中实现显示与隐藏

在三级联动组件中利用mounted挂载来实现

if (this.$route.path != "/home") {
      this.show = false;
    }

再结合mouseenter与mouseleave来实现。

11.1显示与隐藏的动画实现

①在需要动画的地方包裹上transition标签(最好加上名字)

②写动画效果

// 开始状态
    .sort-enter {
      height: 0px;
      // transform: rotate(0deg);
    }
      // 结束状态
    .sort-enter-to {
      height: 461px;
      // transform: rotate(360deg);
    }
    // 定义动画的时间,速率
    .sort-enter-active {
      transition: all 0.5s linear;
    }

性能优化:由于会多次用到TypeNav组件,导致会多次发送同样的请求,所以我们将请求放在根组件里面。即App.vue

mounted() {
    // 通知vuex发送请求,将数据存储于仓库中
    this.$store.dispatch("categoryList");
  },
};

这样就只会调用一次。

12:利用swiper组件写轮播图

①安装swiper :  cnpm install --save swiper@5

②引入 css和js文件和js代码段

import Swiper from'swiper'

import "swiper/css/swiper.css"

js代码段放在mounted里面。

但是这样做以后会发现没有效果,为什么呢?因为我们遇到了异步的问题

第一个vue项目_第6张图片

第一个vue项目_第7张图片

 第一个vue项目_第8张图片

 我们期待的顺序时①②③④,因为这样就能正常的渲染数据

第一个vue项目_第9张图片

 而调试完才发现顺序是①②④③,

第一个vue项目_第10张图片

可以看到bannerlist数据还没回来,mounted就挂载完毕了,所以获取不到数据,关键就是dispatch方法,这是一个异步语句,导致v-for遍历的时候结构还不完全,所以我们需要解决这个问题。

第一个vue项目_第11张图片

 我们需要用watch监听属性和$nextTick来解决这个问题

官方介绍nextTick:在下次DOM更新 循环结束之后 执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
      当执行了handler方法,代表组件实例身上这个属性的属性更新了,因为一修改数据之后,watch就会检测到,然后使用nextTick方法,获取更新后的 DOM.当你执行这个回调的时候,服务器的数据回来了,v-for执行完毕了,所以轮播图的结构就一定有了。

        
watch: { //监听bannerList数据的变化:因为这条数据发生过变化----由空数组变为数组里面有四个元素 bannerList: { handler(newValue, oldValue) { this.$nextTick(() => { //swiper的模板 var mySwiper = new Swiper( this.$refs.mySwiper, { loop: true, // 循环模式选项 // 如果需要分页器 pagination: { el: ".swiper-pagination", clickable: true, }, // 如果需要前进后退按钮 navigation: { nextEl: ".swiper-button-next", prevEl: ".swiper-button-prev", }, } ); }); }, }, },

props传递数据:

父组件Home传给子组件Floor数据

 子组件接收:  

 floor模块继续使用swiper组件,但是这里却可以直接把js代码放在mounted中, 之前不能放在mounted中是因为在组件内部发送请求,动态渲染,这里的轮播图数据是父组件中传来的,所以可直接放在mounted中,不用使用watch+$nextTick。

mounted() {
    // 之前不能放在mounted中,因为是在组件内部发送请求,动态渲染的,这里是父组件中传来的数据,所以可以直接放在mounted中,不用使用nextTick
    var mySwiper = new Swiper(this.$refs.cur, {
      // direction: "vertical", // 垂直切换选项
      loop: true, // 循环模式选项

      // 如果需要分页器
      pagination: {
        el: ".swiper-pagination",
        clickable: true,
      },

      // 如果需要前进后退按钮
      navigation: {
        nextEl: ".swiper-button-next",
        prevEl: ".swiper-button-prev",
      },
    });
  },

13:Search模块的开发。

在前面的学习中,我们基本可以了解开发一个模块的大概流程。

①静态页面和静态组件

②发送请求(API)

③vuex的三连环

④组件获取仓库数据,动态展示数据。

这里的第一步就先省略了,来到第二步的api

现在API中发送请求,获取数据。

通过查阅API文档我们知道当前这个函数需要接收外部传递参数,要给服务器传送一个默认参数(至少是一个空对象),所以在api/index.js文件下的代码如下:

export const reqGetSearchInfo = (params) => requests({ url: "/list", method: "post", data: params })

还要再main.js中引入

import { reqGetSearchInfo } from '@/api'

③vuex三连环

第一步在search仓库中写以下代码

第一个vue项目_第12张图片

 第二步在Search组件中派发getSearchList方法

 vuex三连环比较简单,就不过多讲解了。

14:getter()方法简化获取数据

之前我们用mapState获取数据时比较繁琐,需要state.仓库名.数据.属性来获取数据。

import {mapState} from 'vuex'

.....
computed{
    ...mapState({
      goodsList:state=>state.search.searchList.goodsList  
    })
}

所以使用getter可来简化仓库中的数据

首先在仓库中使用getter方法:

const getters = {
    goodsList(state) {
         //这里需要定义一个空数组是因为加入当前网络不给力的话,goodlist返回的是undefined,就会遍历出错
        return state.searchList.goodsList || [];
    }
};

然后再回到组件中调用以下就好了

import { mapGetters } from "vuex";
.......

computed: {
    ...mapGetters(["goodsList"]),
  },

因为getters是全局属性,所以数据部分模块,我们直接...mapGetters["数据名"]就能获取数据。

下一步,根据不同的参数获取数据并渲染

这是我们需要的数据

第一个vue项目_第13张图片

 我们在组件挂载之前获取一次数据,把接口需要的参数进行整理,这里我们用到了ES6新增的语法,用Object.assign来合并对象

//当组件挂载完毕之前执行一次【先与mounted之前】
  beforeMount() {
    //复杂的写法
    // this.searchParams.category1ld =this.$route.query.categorylld;
    // this.searchParams.category2Id =this.$route.query.category2Id;
    // this.searchParams.category3Id =this.$route.query.category3Id;
    // this.searchParams.categoryName = this.$route.query.categoryName;
    // this.searchParams.keyword = this.$route.params.keyword;//0bject.assign:ES6新增的语法,合并对象
    // Object.assign:ES6新增语法,合并对象
    Object.assign(this.searchParams, this.$route.query, this.$route.params);
  },

之前放在mounted里面的发送数据方法,要放到methods里面,方便调用

 mounted() {
    // 发请求之前把参数带给服务器
    this.getData();
  },
  methods: {
    getData() {
      this.$store.dispatch("getSearchList", this.searchParams);
    },
  },

18.2:search模块优化

上面的方法只能搜索一次,这里我们使用watch监测数据,一旦路由的参数改变我们就再次发送请求。

watch: {
    $route(newValue, oldValue) {
      // 再次发送请求之前整理好带给服务器的参数
      Object.assign(this.searchParams, this.$route.query, this.$route.params);
      // 再次发起ajax请求
      this.getData();
    },
  },

15:面包屑相关问题

 这些带有‘X’的我们称为面包屑,进入分类界面或者输入关键字进行搜索时,都会出现面包屑,且分类会带有params参数,关键字搜索带有query参数,所以只要参数发生变化,watch就能监测到,从而发起ajax请求。

当我们点击‘X’时,要调用函数来删除面包屑,同时重新渲染页面,让网页的参数变回之前的样子,这时候我们就需要调用一个自定义函数,具体代码如下:

  • {{ searchParams.categoryName}}x
  • // 删除分类的名字
        removeCategoryName() {
       // 把带给服务器的参数置空后,还需向服务器发送请求
       // 带给服务器的参数可有可无:应该把字段变为undefined,变为空的话还会带给服务器,而undefined不会
           this.searchParams.categoryName = undefined;
           this.searchParams.category1Id = undefined;
           this.searchParams.category2Id = undefined;
           this.searchParams.category3Id = undefined;
           this.getData();
       // 地址栏也需要修改:可以用路由跳转的方法(跳到自己原来的路由)
       // 但是这个方法不够严谨,这样会把params和query参数都置空,而这里params不需要置空,应该这么写:
          if (this.$route.params) {
            this.$router.push({ name: "search", params: this.$route.params });
          }
       },

    15.1:组件间的通信

    在搜索时,我们的关键字会出现在面包屑上,当我们点击删除面包屑,也要使搜索栏的关键字置空,我们知道搜索栏是属于Header组件的,而面包屑是属于Search组件的,这时候就需要用到组件间的通信,$bus

    首先我们在main.js中配置全局事件总线

    第一个vue项目_第14张图片

    然后在Search模块中通知Header组件去触发函数

    第一个vue项目_第15张图片

     最后在Hearer模块中的mounted挂载里面实现关键字的清除,组件挂载时就监听clear事件

    第一个vue项目_第16张图片

     15.2:子向父传递数据

    父组件绑定一个自定义函数

    子组件中定义一个函数并传递数据

    第一个vue项目_第17张图片

    子组件中触发父组件的函数传递数据

    第一个vue项目_第18张图片

     父组件中自定义事件的测试

     这样就完成了子向父传递数据,当我们点击子组件时,会向父组件传递数据,并得到数据

    点击苹果(一个子组件)

    可以在父组件中得到数据

     

     最终我们是要在父组件中获取子组件的一些参数

    第一个vue项目_第19张图片

    品牌的面包屑删除也是同理

    
    
  • {{ searchParams.trademark.split(":")[1]}} x
  • removeTradeMark() {
          // 将品牌信息置空
          this.searchParams.trademark = undefined;
          // 再次发送请求
          this.getData();
    },

    search模块基本结束

    16 排序模块

    先在阿里图标找到我们需要的图标,生成代码,复制到public文件夹中,在index文件里面引入,记得加https:

    第一个vue项目_第20张图片

    按钮的升序降序

    data(){
      return{
         .......
         // 排序
         order: "1:asc",
      }
    } 
    
    //排序的操作
        changeOrder(flag) {
          //这里获取到的是最开始的状态
          let originFlag = this.searchParams.order.split(":")[0];
          let orginSort = this.searchParams.order.split(":")[1];
          //准备一个新的order属性值
          let newOrder = "";
    
          // 若点击的是同一个按钮,改变升序和降序
          if (flag == originFlag) {
            newOrder = `${flag}:${orginSort == "desc" ? "asc" : "desc"}`;
            // newOrder = `${originFlag}:${orginSort == "desc" ? "asc" : "desc"}`;
          }
     
          else {
            //不是同一个按钮,取传入的按钮,然后默认为降序
            newOrder = `${flag}:${"desc"}`;}
    
          //将新的order赋了searchParams
          this.searchParams.order = newOrder;
          //再次发请求
          this.getData();
        }

    17:Detail详情模块的开发(步骤:静态组件,路由,请求接口数据,传递参数)

    17.1引入静态组件

    17.2路由模块

    ①引入路由

    import Detail from '@/pages/Detail'

    ②配置路由

     routes: [
            {
                 //params参数占位
                path: "/detail/:skuid",
                component: Detail,
                meta: {
                    show: true
                }
            },

    我们从search页面跳转到detail页面时,要传递参数,我们的效果是点击图片进入详情页面,所以这里我们可以借用声明式路由导航跳转,并传递参数。

    第一个vue项目_第21张图片

     当路由配置信息太多的时候,我们可以新建一个路由配置文件routes.js,再引入router文件夹下的index.js文件。

    当我们从一个路由跳到另一个路由时,如果我们想要滚动条置顶,我们可以在路由管理文件中加入scrollBehavior函数

    export default new VueRouter({
        //  配置路由
        routes,
        scrollBehavior(to, from, savedPosition) {
            // 始终滚动到顶部
            return { y: 0 }
        }
    })

    17.3获取api中的数据,之前类似的写过很多次了,这里就简单介绍下步骤

    ①在api文件中获取详情的数据

    export const reqGoodsInfo = (skuId) => requests({ url: "/item/${`skuId`}", method: "get" })

    ②在仓库中获取数据

    import { reqGoodsInfo } from '@/api';
    const state = {
        goodInfo: {},
    };
    const mutations = {
        GETGOODINFO(state, goodInfo) {
            state.goodInfo = goodInfo
        }
    };
    const actions = {
        async getGoodInfo({ commit }, skuid) {
            let result = await reqGoodsInfo(skuid)
            if (result.code == 200) {
                commit('GETGOODINFO', result.data)
            }
        }
    };
    const getters = {};
    export default {
        state,
        mutations,
        actions,
        getters
    }
    

     在挂载时派发请求 

    mounted() {
        this.$store.dispatch("getGoodInfo", this.$route.params.skuid);
      },

    17.4动态展示数据

    用getter来简化数据

    const getters = {
        categoryView(state) {
            return state.goodInfo.categoryView
        }
    };

    获取数据 

    import { mapGetters } from "vuex";
    
    .....
    
    computed: {
        ...mapGetters(["categoryView"]),
      },

    渲染数据

    {{categoryView.category1Name}} {{categoryView.category2Name}} {{categoryView.category3Name}}

    这样写数据能够显示出来,但是我们发现会有报错,

     原因其实很简单,因为第一次获取goodInfo时,数据是空的,但是我们还继续获取了goodInfo.categoryView,得到的数据是undefined,所以会报错,我们应该加一个空对象{}

     17.5点击谁,谁高亮的效果

    第一个vue项目_第22张图片

     这里我们要用到排他思想,要获取到当前点击的选项的数组,还有当前选项,然后把当前数组的ischeck全部换成非选中状态,然后令当前点击的选项设置为选中状态即可。

    第一个vue项目_第23张图片

     第一个vue项目_第24张图片

    17.6 放大镜

    先完成轮播图,之前做过,这里简单回顾一下,先引入swiper文件,再引入swiper样式,然后将js代码写入watch中(为什么不写入mounted之前有说过)

    watch: {
       // 监听数据:可以保证数据一定ok,但不能保证v-for循环是否完成,所以需要$nextTick来等待v-for完成
        skuImageList(newValue, oldValue) {
        // $nextTick() 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
    
          this.$nextTick(() => {
            new Swiper(this.$refs.cur, {
              loop: true, // 循环模式选项
              // 如果需要前进后退按钮
              navigation: {
                nextEl: ".swiper-button-next",
                prevEl: ".swiper-button-prev",
              },
            });
          });
        },

     接下来用js实现点击图片添加背景,之前也写过类似的

    首先默认选中第一张

    第一个vue项目_第25张图片

     然后写判断和点击事件

    第一个vue项目_第26张图片

     最后实现点击事件

    methods: {
        changeCurrentIndex(index) {
          this.currentIndex = index;
        },
    }

    接下来实现点击下面轮播图图片,上面大图也跟着变

    第一个vue项目_第27张图片

    因为这涉及到两个兄弟组件传递数据,所以要使用$bus,之前也实现过类似的功能 

    通知兄弟组件

    第一个vue项目_第28张图片

     兄弟组件接收数组,在mounted中接收即可

     mounted() {
        // 全局事件总线,获取兄弟组件传递过来的索引值
        this.$bus.$on("getIndex", (index) => {
          //修改当前响应式数据
          this.currentIndex=index
        });
      },

    放大镜移动效果,这里就不解释了

    handler(event) {
          let mask = this.$refs.mask;
          let big = this.$refs.big;
          let left = event.offsetX - mask.offsetWidth / 2;
          let top = event.offsetY - mask.offsetHeight / 2;
          //约束范围
          if (left <= 0) left = 0;
          if (left >= mask.offsetWidth) left = mask.offsetWidth;
          if (top <= 0) top = 0;
          if (top >= mask.offsetHeight) top = mask.offsetHeight;
          //修改元素的left|top属性值
          mask.style.left = left + "px";
          mask.style.top = top + "px";
          big.style.left = -2 * left + "px";
          big.style.top = -2 * top + "px";
        },

    18:购物车模块

    我们需要向购物车的借接口发送请求,但是这里我们并不需要从接口中获取数据

    第一步仍然是在api中写好接口

    export const reqAddOrUpdateShopCart = (skuId,skuNum) => 
    requests({ url: `/cart/addToCart/${ skuId }/${ skuNum }`, method: "post"})

    因为是在Detail文件中实现购物车功能,所以在detail的vuex文件中发送请求获取数据

    // 将产品添加到购物车中
    async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
      // 加入购物车返回的结果
      let result = await reqAddOrUpdateShopCart(skuId, skuNum)
      console.log(result);
    }

    当点击“加入购物车”按钮时,我们派发请求,

      // 加入购物车的回调函数
        addShopcar() {
          // 1发请求--将产品加入数据库(通知服务器)
          this.$store.dispatch("addOrUpdateShopCart", 
            {skuId: this.$route.params.skuid,skuNum: this.skuNum,});
    
          // 2存储成功,进行路由跳转
          // 3存储失败,给用户提示
        },

     这里我们并不需要vuex三连环,因为服务器并没有返回数据,我们只要成功发送请求就好。

    接下来我们要进行加入购物车成功与失败的判断及相关操作

    先回到仓库中进行成功与失败的判断

    第一个vue项目_第29张图片

     再回到点击加入购物车的函数中,我们要知道,我们派发的函数最后得到的结果一定是一个promise,因为我们要调用的函数带有async,所以在addShopcar函数中,我们要等待vuex中addOrUpdateShopCart函数返回结果,所以加上await等待promise返回结果 

    第一个vue项目_第30张图片

     将result进行打印,如果成功就会打印ok,失败会打印faile

    所以这里我们用try catch 来对不同结果进行处理

    第一个vue项目_第31张图片

     利用会话存储进行参数传递

    我们在进行路由跳转并把产品信息带给下一级路由组件时,简单的参数可以通过query参数带过去,但是复杂的数据要通过会话存储传递(不持久话,会话结束数据就消失)。

    本地存储/会话存储 一般存储的是字符串

    第一个vue项目_第32张图片

    获取数据:

     第一个vue项目_第33张图片

     之后再根据数据渲染就可以了

    19购物车列表模块

    还是一样的操作,写好路由,在api文件中获取接口,组件挂载派发action请求,建小仓库vuex三连环。

    首先封装请求函数

    export const reqCartList = () => requests({ url: '/cart/cartList', method: 'get' })

    因为这里我们要访问API获取详细信息,所以需要验证身份,我们用一个uuidToken来验证,

    我们建一个utils文件夹专门放uuid_token,

    import { v4 as uuidv4 } from 'uuid'
    export const getUUID = () => {
        // 先从本地存储获取一个随机字符串,且每次执行不能发生变化,游客身份持久存储
        let uuid_token = localStorage.getItem('UUIDTOKEN');
        // 如果没有
        if (!uuid_token) {
            // 生成游客身份
            uuid_token = uuidv4();
            // 本地存储一次
            localStorage.setItem('UUIDTOKEN', uuid_token);
        }
        return uuid_token;
    }

    上面这样写是利用本地存储生成一个唯一的id,然后在detail文件下获取uuid_token

    import { getUUID } from '@/utils/uuid_token'
    const state = {
        goodInfo: {},
        // 游客临时身份
        uuid_token: getUUID()
    };

    接着我们在请求拦截器中获取uuid_token

    在api/requests.js文件中首先引入store仓库

    import store from '@/store'

    然后再请求拦截器中写以下代码(if语句)

    // 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
    // interceptors拦截  config是配置对象
    requests.interceptors.request.use((config) => {
    
        if (store.state.detail.uuid_token) {
            // 请求头添加一个字段userTempId(该变量是项目中定义好的)
            config.headers.userTempId = store.state.detail.uuid_token;
        }
        nprogress.start()
        // config:配置对象,独享里面有一个属性很重要,包含header请求头
        return config;
    });

    可以在请求头中成功找到数据

    第一个vue项目_第34张图片

     修改购物车中商品数量:

    首先给两个按钮和输入框绑定同一个事件handler,分别传指令,数字和参数

    -
    
    +

     handler函数:

    async handler(type, disNum, cart) {
          switch (type) {
            case "add": disNum = 1; break;
    
            // 如果是减,要判断产品的个数是否小于1,大于1才传给服务器,小于1就传0
            case "minus":
              disNum = cart.skuNum > 1 ? -1 : 0; break;
          }
    
          try {
            //等待返回结果,用await,响应的handler要加async
            await this.$store.dispatch("addOrUpdateShopCart", {
              skuId: cart.skuId,
              skuNum: disNum,
            });
            //再次发送请求
            this.getData();
          } catch (error) {}
        },

    这样的话看似没有问题,但是如果用户疯狂点击减按钮,数量会变成负数,所以我们要使用节流,就是限制单位时间内点击的次数。  引入一下节流的文件然后稍微修改一下函数即可

    import { throttle } from "lodash";
      handler:throttle (async function (type, disNum, cart) {
          ......
        },500),

    删除购物车产品

    ①封装接口

    export const reqDeleteCartById = (skuId) => requests({ 
        url: `/cart/deleteCart/${skuId}`, method: 'delete' })

    ②vuex:

    const actions = {
        async deleteCartListBySkuId({ commit }, skuId) {
            let result = await reqDeleteCartById(skuId);
            if (result.code == 200) {
                return 'ok';
            } else {
                return Promise.reject(new Error('faile'));
            }
        }
    };

    vue文件中定义一个点击事件来触发请求xiuga

    methods:{
        async deleteCartById(cart) {
          try {
            //删除成功发送请求
            await this.$store.dispatch("deleteCartListBySkuId", cart.skuId);
            this.getData();
          } catch (error) {
            console.log(error.message);
          }
        },
    }

     修改产品的选中状态与删除购物车基本一致,就不再介绍了。

    一次删除多个商品

    由于该项目没有删除多个商品的API请求,所以我们只能多次调用删除单个商品的接口

    我们在删除按钮派发一个dispatch,这里用到一个新的参数context,我们不妨看看context内容是什么(默认传递的参数就是context)

     deleteAllCheckedCart(context) {
            console.log(context);
        }
    

    第一个vue项目_第35张图片

     可以看到有dispatch,getters,commit等,我们可以理解context为一个小仓库

    然后我们在vuex文件中调用

    deleteAllCheckedCart({ dispatch, getters }) {
       // context可以理解为小仓库
    
       let PromiseAll = [];
      // 获取购物车中的全部产品(一个数组)
       getters.cartList.cartInfoList.forEach(item => {
          let promise = item.isChecked == 1 ?dispatch('deleteCartListBySkuId', item.skuId):'';
          PromiseAll.push(promise);// 将每一次返回的Promise添加到数组中
       });
    
       // Promise.all([p1,p2,p3]) 函数:全部成功才返回成功,只要一个失败就返回失败
       return Promise.all(PromiseAll)
    }
    // 删除全部选中商品
        async deleteAllCheckedCart() {
          try {
            await this.$store.dispatch("deleteAllCheckedCart");
            this.getData();
          } catch (error) {
            error.message;
          }
        },

    这里用到一个专门用于promise的api,Promise.all ( [ p1 , p2 , p3] ),只有全部成功才返回成功。

    修改全部商品的选中状态

    与一次删除所有商品类似,修改全部商品选中状态也要调用修改单个商品的选中状态函数,

    // 修改全部产品的状态
    updateAllCartIsChecked({ dispatch, getters }, isChecked) {
        let promiseAll = []
        getters.cartList.cartInfoList.forEach((item) => {
             let promise = dispatch('updateCheckedById', { skuId: item.skuId, isChecked })
             promiseAll.push(promise)
        })
        return Promise.all(promiseAll)
    }

     在全选框定义点击事件updateAllCartChecked

    // 修改全部商品的选中状态
        async updateAllCartChecked(event) {
          try {
            let isChecked = event.target.checked ? "1" : "0";
            await this.$store.dispatch("updateAllCartIsChecked", isChecked);
            this.getData();
          } catch (error) {
            alert("error.message");
          }
        },

     最后有一个小bug,就是点击全选按钮后再逐个删除商品,商品删除完后全选按钮依旧是选中状态,所以当商品个数为0时我们要使全选框为不勾选状态

    第一个vue项目_第36张图片

     20登录注册业务

    注意:assets放置全部组件共用的静态资源

    20.1注册模块

    封装接口,建立仓库,调用接口,派发action

    仓库中获取验证码和点击注册的action

    const actions = {
        // h获取验证码
        async getCode({ commit }, phone) {
            let result = await reqGetCode(phone)
            if (result.code == 200) {
                commit("GETCODE", result.data)
                return 'ok'
            } else {
                return Promise.reject(new Error('faile'))
            }
        },
        // 用户注册
        async userRegister({ commit }, user) {
            let result = await reqUserRegister(user);
            if (result.code == 200) {
                return 'ok'
            } else {
                return Promise.reject(new Error('faile'))
            }
        }
    };

    这里用v-model来双向绑定数据

    第一个vue项目_第37张图片

     派发action,

    这里解释一下 const { phone, code, password, password1 } = this;(参考某位大佬的)

     const { phone, code, password, password1 } = this;
    
    等于
    
    const phone=this.phone
    const phone=this.code
    const passwored1=this.password1
    const passwored1=this.password1
    //用户注册 
    async userRegister() {
          try {
            const { phone, code, password, password1 } = this;
            phone &&code &&password == password1 &&(await this.$store.dispatch("userRegister", 
            {phone,code,password,}));
            this.$router.push("/login");
          } catch (error) {
            console.log(error.message);
          }
        },

    20.2登录模块

    action模块 

    //用户登录
        async userLogin({ commit }, data) {
            let result = await reqUserLogin(data)
            // 服务器会下发一个token,是用户的位移标识符(类似之前的uuid)
            // 将来经常通过token来找服务器要用户的信息进行展示
            if (result.code == 200) {
                commit("USERLOGIN", result.data.token)
                return 'ok'
            } else {
                return Promise.reject(new Error("faile"))
            }
        }

    登录函数

    // 登录按钮
    async userLogin() {
       try {
          const { phone, password } = this;
          phone &&password &&(await this.$store.dispatch("userLogin", { phone, password }));
            this.$router.push("/home");
       } catch (error) {
            alert(error.message);
        }
    },

    在登录时会收到一个token,由于vuex不是持久化存储, 刷新一下就没有了,所以为了持久化保存token,要用到本地存储

    action部分代码:

     //用户登录
      async userLogin({ commit }, data) {
          let result = await reqUserLogin(data)
          // 服务器会下发一个token,是用户的位移标识符(类似之前的uuid)
          // 将来经常通过token来找服务器要用户的信息进行展示
          if (result.code == 200) {
             commit("USERLOGIN", result.data.token);
             // 持久换存储token
             localStorage.setItem('TOKEN', result.data.token)
             return 'ok'
         } else {
             return Promise.reject(new Error("faile"))
         }
      },

    state部分的代码

    const state = {
        code: '',
        //获取本地存储的token
        token: localStorage.getItem('TOKEN'),
        // token: '',
        userInfo: {}
    };

    成功登陆后获取用户信息

    // 获取用户信息
        async getUserInfo({ commit }) {
            let result = await reqUserInfo()
            console.log(result);
            // 提交用户信息
            if (result.code == 200) {
                commit("GETUSERINFO", result.data)
                return 'ok'
            }
        }

    登录部分 还有很多未完善的地方,比如跳转到别的模块刷新一下,又不会显示登录人信息了

    退出登录

    action部分

    // 退出登录
    async userLogout({ commit }) {
        let result = await reqLogout()
        if (result.code == 200) {
            commit('CLEAR');
            return 'ok';
        } else {
            return Promise.reject(new Error('faile'))
        }
    }

    函数部分(记得跳转回首页)

    async logout() {
        try {
          await this.$store.dispatch("userLogout");
          //回到首页,路由跳转
          this.$router.push("/home");
        } catch (error) {
          console.log(error.message);
        }
    },

    路由守卫

    先简单复习一下各种路由守卫

    第一个vue项目_第38张图片

    21交易模块

    这里仍然是前面的四个步骤:

    ①封装API

    ②建立小仓库写好vuex三件套

    ③dispatch派发请求

    ④数据的渲染

    所以就不详细说明了,只讲一些讲得少的地方。

    第一个vue项目_第39张图片

     这里我们需要一个点击谁谁高亮的效果,所以用到排他思想

    首先绑定点击事件

    第一个vue项目_第40张图片

     再写函数(address是当前数组的,addressInfo是整个数组)

     changeDefault(address, addressInfo) {
          // 排他思想,全部的default变为0,当前点击的变为1
          addressInfo.forEach((item) => (item.isDefault = 0));
          address.isDefault = 1;
     },

    下面的收货人信息也要根据我们所选择的信息改变

    我们要用到find方法来选择数据

       // 将来提交订单最终选中的地址
        userDefaultAddress() {
          // find查找数组中符合条件的元素返回,就为最终的结果
          return this.addressInfo.find((item) => item.isDefault == 1)||{};
        },

    提交订单模块

    我们依旧是封装好提交订单的API,但是假设我们不用vuex来做,那么应该怎么做呢?

    首先在main.js文件中统一引入API文件

    // 统一接收api文件夹里面的全部函数请求,统一引入
    import * as API from '@/api'

    然后挂载到原型对象身上

    第一个vue项目_第41张图片

     定义一个点击事件

     这里就会用到$API

    第一个vue项目_第42张图片

    完整代码 (这里需要传的参数有点多,一定要细心)

    // 提交订单
        async submitOrder() {
          // 交易编码
          let { tradeNo } = this.orderInfo;
          // 其余6个参数
          let data = {
            consignee: this.userDefaultAddress.consignee, //最终收件人名字
            consigneeTel: this.userDefaultAddress.phoneNum, //手机号
            deliveryAddress: this.userDefaultAddress.fullAddress, //收件人地址
            paymentWay: "ONLINE", //支付方式
            orderCommit: this.msg, //买家留言
            orderDetailList: this.orderInfo.detailArrayList, //商品清单
          };
          // 需要带参数的tradeNo
          let result = await this.$API.reqSubmitOrder(tradeNo, data);
          
          if (result.code == 200) {
            this.orderId = result.data;
            // 路由跳转+路由传递参数
            this.$router.push("/pay?orderId=" + this.orderId);
          } else {
            alert(result.data);
          }
        },

     22支付模块

    封装支付的接口

    // 获取支付信息
    export const reqPayInfo = (orderId) => requests({ url: `/payment/weixin/createNative/${orderId}`, method: 'get' })

    尽量不要在生命周期函数写async await,所以在method中用

    // 尽量不要在生命周期函数写async await,所以在method中用
      // async mounted() {
      //   await this.$API.reqPayInfo(this.orderId)
      // },
      mounted() {
        this.getPayInfo();
      },
      methods: {
        async getPayInfo() {
          let result = await this.$API.reqPayInfo(this.orderId);
          console.log(result, "12121");
        },
      },

    后面就是获取数据渲染数据,大同小异

    async getPayInfo() {
        let result = await this.$API.reqPayInfo(this.orderId);
        if (result.code == 200) {
           this.payInfo = result.data;
        }
    }

    使用element-ui 先下载

    cnpm install --save element-ui

    再去element-ui官网看怎么使用

    推荐一个插件vue-helper,输入el就会有提示

    在main.js中引入

    import { Button, MessageBox } from 'element-ui'
    // 注册element-ui 全局组件
    Vue.component(Button.name, Button)
    // 注册element-ui 另一种方法 挂在原型
    Vue.prototype.$msgbox = MessageBox;
    Vue.prototype.$alert = MessageBox.alert;

    找到对应的组件并使用

    open() {
          this.$alert("这是 HTML 片段", "HTML 片段", {
            dangerouslyUseHTMLString: true,
            center: true,
            showCancelButton: true,
            cancelButtoText: "支付遇见问题",
            confirmButtonText: "已支付成功",
            showClose: false,
          });
        },

    第一个vue项目_第43张图片

     22.1生成二维码

     npm 中找qrcode

    下载 cnpm i qrcode

    引入qrcode

    import QRCode from "qrcode";

    传字符串即可生成二维码

    async open() {
          let url = await QRCode.toDataURL(this.payInfo.codeUrl);
          this.$alert(``, "请支付", {
            dangerouslyUseHTMLString: true,
            center: true,
            showCancelButton: true,
            cancelButtoText: "支付遇见问题",
            confirmButtonText: "已支付成功",
            showClose: false,
          });
    },

    在open函数中,我们先定义一个定时器timer,先在data中定义为null,然后根据返回的结果判断是否跳转到支付成功页面。

     if (!this.timer) {
            this.timer = setInterval(async () => {
              // 发请求获取用户支付状态
              let result = await this.$API.reqPayStatus(this.orderId);
              if (result.code == 200) {
                // 第一步,清除定时器
                clearInterval(this.timer);
                this.timer = null;
                // 保存支付成功返回的code
                this.code = result.code;
                // 关闭弹出框
                this.$msgbox.close();
                //跳转到下一路由
                this.$router.push("/paysuccess");
              }
            }, 1000);
         }

    弹出窗口按钮的配置

    第一个vue项目_第44张图片

     23个人中心

    这里我们来建立二级路由

    首先引入二级路由

    import Center from '@/pages/Center'
    
    // 引入二级路由
    import MyOrder from '@/pages/Center/myOrder'
    import GroupOrder from '@/pages/Center/groupOrder'

    引入二级路由的组件

    export default [
        {
            // 路由路径都是小写
            path: "/center",
            component: Center,
            meta: {
                show: true
            },
            // 引入二级组件路由
            children: [
                {
                    // 不用写/
                    path: 'myorder',
                    component: MyOrder
                },
                {
                    path: 'groupOrder',
                    component: GroupOrder
                }
            ],
    
        },

      在vue文件中跳转

    我的订单
    团购订单

    并且写路由文件的出口

     

     当进入center路由时,我们可以规定一开始就展示myorder组件,可以使用重定向

    第一个vue项目_第45张图片

    后面一些功能就很类似了。

    接下来讲一讲性能的优化

    24图片懒加载vue-lazyload

    直接搜索vue-lazyload,并安装cnpm i vue-lazyload

    首先引入插件并注册,还有定义好懒加载的图片是什么(图片和json都是暴露的,不需要手动去暴露)

    import james from '@/assets/images/james.gif'
    import VueLazyload from 'vue-lazyload'
    
    Vue.use(VueLazyload, {
      // 懒加载的图片
      loading: james
    })

    使用v-lazy标签

     

     25:表单验证

    大佬们都推荐用element-ui的from表单验证

    elemen-ui表单验证

    26:路由懒加载

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

    vue官网路由懒加载

    //普通形式
    // import Home from '@/pages/Home'
    
    // 懒加载形式
    const foo = () =>  import('@/pages/Home')
    
    
     {
            path: "/home",
            //普通形式
            // component: Home,
    
            //懒加载
            component: foo,
            meta: {
                show: true
            }
        },

    再升级,最懒的

     {
            path: "/home",
            //最懒的
            component: () => import('@/pages/Home'),
            meta: {
                show: true
            }
        },

    27打包文件(前台项目完结撒花!!!)

    第一个vue项目_第46张图片

     

    第一个vue项目_第47张图片

     项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。

    但是我们项目发布后不需要知道哪里出错了,所以不需要map文件。

    我们可以在vue.config.js中添加如下代码

    productionSourceMap:false;

    28:购买服务器,发布项目

    由于囊中羞涩,省略该步骤。。。

    你可能感兴趣的:(javascript,html,前端,vue.js,前端框架)