项目创建过程

1、数据库连接

后端项目安装mysql

npm i mysql

services/db.js

let mysql = require('mysql');
// 数据库操作
exports.base = (sql, data, callback) => {

    // 创建数据库连接
    let connection = mysql.createConnection({
        host: 'localhost',
        user: 'root',
        password: 'root',
        database: 'meituan'
    })

    // 真正的连接数据库
    connection.connect();

    // 操作数据库(异步)
    connection.query(sql, data, function(error, results, fields) {
        if (error) throw error;
        // 用回调函数处理所有的操作
        callback(results);
    });

    // 关闭数据库
    connection.end();
}

router/index.js

let db = require('../service/db');
router.get('/api/sellList', function (req, res, next) {
    let sql = 'select * from sellList';
    db.base(sql, null, (result) => {
        res.send({
            code: 200,
            msg: 'ok',
            info: result
        });
    })
});

2、axios封装

//api/config.js

// axios基础配置
import axios from 'axios'

// 配置基础的请求路径
axios.defaults.baseURL = 'http://localhost:5001';
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 放入token
    let token = localStorage.getItem('token');
    if (token) {
        config.headers.Authorization = "Bearer " + token;
    }

    // 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
}, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
});
export default axios;
//api/request.js
import axios from '@/api/config';

export function userLogin(data){//post方式
    return axios({
        url:'/login',
        method:'post',
        data
    })
}

3、文件上传

后端

npm i multer  // nodejs用于上传文件的模块 
npm i uuid //uuid用来生成唯一标识符

callback(null, "static/uploadImg"),参数2是指定的upload文件夹

配置静态文件夹:app.use('/uploadImg', express.static(path.join(__dirname, 'static/uploadImg')));

//utils/upload.js
const multer = require("multer")  // nodejs用于上传文件的模块
const uuid = require("uuid")  //uuid用来生成唯一标识符

/*
  multer是node的中间件, 处理表单数据 主要用于上传文件  multipart/form-data
*/

// 指定存储位置
const storage = multer.diskStorage({
  // 存储位置
  destination(req, file, callback) {
    // 参数一 错误信息   参数二  上传路径(此处指定upload文件夹)
    callback(null, "static/uploadImg")
  },
  // 确定文件名
  filename(req, file, cb) {
    //文件扩展名
    let extName = file.originalname.slice(file.originalname.lastIndexOf('.'))
    //新文件名
    let fileName = uuid.v1()
    cb(null, fileName + extName)
  }
})
// 得到multer对象  传入storage对象
const upload = multer({ storage })
module.exports = upload;
//route/uploadRouter.js
let express = require('express');
let uploadRouter = express.Router();
let upload = require("../utils/upload");
//图片上传
uploadRouter.post("/upload", upload.single("file"), (req, res) => {
    // 需要返回图片的访问地址    域名+文件名
    const url = "http://localhost:5000/uploadImg/" + req.file.filename
    // console.log(req.file.filename);
    res.json({ url })
})
module.exports = uploadRouter;

前端

<template>
  <div class="classify">
    <h1>首页</h1>
    <!-- <p><input type="file" @change="upload" ref="file" /></p> -->
    <p><input type="file" ref="file" /></p>
    <p><button @click="upload">上传图片</button></p>
    <p>图片回显:</p>
    <img v-if="img" :src="img" alt="" />
  </div>
</template>

<script>
  import axios from "axios";
  export default {
    data() {
      return {
        img: "",
      };
    },
    methods: {
      upload() {
        // 新建表单数据对象,用来存储上传的文件及上传的其它数据
        let param = new FormData();
        //获取图片信息
        let file = this.$refs.file.files[0];
        console.log(file);
        //"file"为前后端约定好的属性名
        param.append("file", file);
        axios.post("http://localhost:5000/upload", param, {
                headers: { 
                  // 默认提交的类型
                  // "content-type": "application/json"
                  // 复杂的表单数据(只要上传文件,就必须是下面的类型)
                  "Content-Type": "multipart/form-data", // Content-Type注意大小写
                },
            })
          .then((res) => {
            let { url } = res.data;
            this.img = url;
          });
      },
    },
  };
</script>

<style>
</style>

4、vue中token使用、鉴权

① 安装依赖包

npm install jsonwebtoken --save
//注意版本
npm install express-jwt@6.1.1
  • jsonwebtoken的两个api

  • 生成token的方法 sign

  • 验证token的方法 verify

  • 依赖包: express-jwt

② 后端代码

//utils/token.js
const jwt = require('jsonwebtoken');

// 密钥
const jwtSecret = 'wangwushan';  //签名

//登录接口 生成token的方法
// setToken携带的参数及参数的数量自定义
const setToken = function (uname) {
  return new Promise((resolve, reject) => {
    //expiresln 设置token过期的时间
    //{ user_name: user_name, user_id: user_id } 传入需要解析的值( 一般为用户名,用户id 等)
    // const token = jwt.sign({ user_name: user_name }, jwtSecret, { expiresIn: '24h' });
    const token = jwt.sign({ uname: uname }, jwtSecret, { expiresIn: '10h' });
    // 注意:expiresIn 过期事件,可以调整为24h后过期,10s太短了
    resolve(token)
  })
}
//各个接口需要验证token的方法
const getToken = function (token) {
  return new Promise((resolve, reject) => {
    if (!token) {
      console.log('token是空的')
      reject({
        error: 'token 是空的'
      })
    }
    else {
      // 验证token
      var info = jwt.verify(token.split(' ')[1], jwtSecret);
      resolve(info);  //解析返回的值(sign 传入的值)
    }
  })
}

module.exports = {
  setToken,
  getToken
}
//app.js
let express = require('express');
let path = require("path");
let cors = require("cors");
//验证token是否过期,并规定哪些路由不用验证token
const expressJwt = require('express-jwt')
let app = express();

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'static')));

// 生成token和验证token是否正确的函数
const vertoken = require('./utils/token');
let reqIcon = require('./static/mock/reqIcon');
let router = require('./routers/index');
let userRouter = require('./routers/user');

//=============================================验证token

//解析token获取用户信息
app.use(function (req, res, next) {
    let token = req.headers['authorization'];
    if (token == undefined) {
      next();
    } else {
      vertoken.getToken(token).then((data) => {
        console.log('解析后的token', data);
        req.data = data;
        next();
      }).catch((error) => {
        next();
      })
    }
  });
  
  //验证token是否过期并规定那些路由不需要验证
  app.use(expressJwt({
    secret: 'wangwushan',
    // 加密算法
    algorithms: ['HS256']
  }).unless({
    path: ['/login','/icon']  //不需要验证的接口名称
  }))
  /*
    注意:直接安装使用expressJwt的时候,会在运行的时候报错
    因为express-jwt升级了,后以前的用法应该是不同了
    只需要将pakage.json中的express-jwt版本改为6.1.1,重新npm i 即可使用。
  */
  
  //token失效返回信息
  app.use(function (err, req, res, next) {
    if (err.status == 401) {
      res.status(401).send('token失效11111111')
    } else {
      next()
    }
  })


app.use(reqIcon);
app.use(router);
app.use(userRouter);

app.listen(5001,()=>{
    console.log("5001 server start");
})
//routers/user.js
var express = require('express');
let db = require('../service/db');
let vartoken = require('../utils/token');
var userRouter = express.Router();
userRouter.post('/login', (req, res) => {
    // console.log(req.body);
    let { name, pwd } = req.body;
    let sql = `select * from login where name='${name}' and pwd='${pwd}'`;
    db.base(sql, null, (result) => {
        console.log(result);
        if (result.length) {
            //==============================================调用生成token的方法
            vartoken.setToken(name).then(token => {
                data = {
                    code: 200,
                    message: '登录成功',
                    token: token
                    //前端获取token后存储在localStroage中,
                    //**调用接口时 设置axios(ajax)请求头Authorization的格式为`Bearer ` +token
                }
                res.send(data)
            })
        } else {
            data = {
                code: 400,
                msg: '登录失败 '
            }
            res.send(data)
        }
    })
})
module.exports = userRouter;

③ 前端代码

//src/api/config.js
// axios基础配置
import axios from 'axios'

// 配置基础的请求路径
axios.defaults.baseURL = 'http://localhost:5001';
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    //   console.log(config);
    // 放入token
    let token = localStorage.getItem('token');
    if (token) {
        config.headers.Authorization = "Bearer " + token;
    }

    // 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
}, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
});
export default axios;
//views/login.vue
<template>
    <div>
        <div class="top">
            <h4>登录</h4>
            <div>
                <input type="text" placeholder="登录名" v-model="form.name">
            </div>
            <div>
                <input type="text" placeholder="密码" v-model="form.pwd">
            </div>
            <div>
                <button @click="go()">登录</button>
            </div>
        </div>
    </div>
</template>
<script>
import {login} from '@/api/iconRequest'
export default {
    data() {
        return {
            form:{
                name:'',
                pwd:''
            }
        }
    },
    methods: {
        go(){
            let to = this.$route.query.redirect;
            //发送请求
            login(this.form).then(res=>{
                console.log(res);
                //获取后端生成的token
                let {code,token} = res.data;
                if(code==200){
                    localStorage.setItem('token',token);
                    //页面跳转
                    this.$router.replace(to);
                }else{
                    alert('登录失败');
                    this.form.name = '';
                    this.form.pwd = '';
                }
            })

        }
    },
}
</script>
<style scoped>
</style>
//routers/index.js
{
    path: '/index',
    name: 'index',
    component: Index,
    meta: {
        title: '首页',
        requiresAuth: false//进入此页面是否需要验证(鉴权)
    }
},

//全局前置守卫
router.beforeEach((to, from, next) => {
    console.log(to);
    document.title = to.meta.title;
    // 鉴权
    if (to.matched.some(record => record.meta.requiresAuth)) {
        if (localStorage.getItem('token')) {
            next();
        } else {
            next({
                path: '/login',
                query: {
                    redirect: to.fullPath
                }
            })
        }
    } else {
        next();
    }
})

5、vue-cli脚手架

(1)安装

首先查看是否已安装过vue-cli

vue -V
//如果有的话,且不是需要的版本,就用下面命令卸载
npm uninstall vue-cli -g 
npm i  @vue/cli -g
//也可使用cnpm 来安装 ,比较快些 ( 安装cnpm的命令: npm i cnpm –g )
cnpm install @vue/cli -g

安装完成之后输入 vue -V(注意这里是大写的“V”),如果出现相应的版本号,则说明安装成功。

安装成功后,可用vue -V查看版本

npm root -g 查看全局安装的位置 

(2)vue-cli搭建项目

vue create 项目名

6、vue单页面应用(路由)

路由的实现:

7、vuex

安装:

(在创建vue-cli时,没有选择vuex的情况下,也即手动使用vuex)

vue-cli项目中新建store/index.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
    state:{},
    getters:{},
    mutations:{},
    actions:{},
    modules:{}
})

修改main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

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

调用数据

在views/index.vue中,调用store/index.js中state中的数据

//store/index.js
state:{
        city:'郑州'
    }

//views/index.vue的template模板
{{$store.state.city}}

//views/index.vue的computed属性中
this.$store.state.city

修改state中的数据

注意:不可以直接拿到数据进行改变。

//store/index.js
mutations:{//真正修改数据
    changeCity(state,newCity){//state:当前state中的数据
        state.city = newCity;
    }
},
actions:{//(在此可以axios请求数据)
    changeCity(ctx,newCity){//ctx:当前的Store
        // 调用mutations中的方法
        ctx.commit('changeCity',newCity);
    }
},

//views/index.vue
methods:{
    change(){
        let newCity = '上海';
        //调用actions中的changeCity方法,传入newCity
        this.$store.dispatch('changeCity',newCity);
    }
}
//当传递多个值时
this.$store.dispatch('changeCity',['北京','朝阳区']);

getters(可以认为是store的计算属性)

可以在很多页面中显示过滤后的值,不必每个页面都进行过滤然后再显示

//store/index.js
getters:{
    newStudent(state){
       return state.stu.filter(item=>item.age>20);
    }
},
//views/index.vue
$store.getters.newStudent

辅助函数

actions和mutations在methods中映射

state和getters在computed中映射

this.$store.getters.newStudent这样子太过繁琐,使用辅助函数进行映射,可以简写

//views/index.vue
import { mapActions, mapGetters, mapState ,mapMutations} from 'vuex';

computed:{
    ...mapState(['city','area']),//映射的是state中的数据
    ...mapGetters(['newStudent'])//映射的是getters中的计算属性
},
methods:{
    ...mapActions(),
    ...mapMutations()
}

//使用
{{city}}--{{area}}

使用映射时,mutations和actions中的方法名字不能再一样了,否则调用的时候就不知道调用的是哪个了。

//store/index.js
mutations:{
    changeCity(state,city){
        state.city = city;
    }
},
//views/index.vue
methods: {
    ...mapMutations(['changeCity']),
    change(){
        this.changeCity('上海');
    }
},
//调用
this.changeCity('上海');

文件拆分

//store/state.js
export default {
    city: '郑州',
    area: '中原区'
}
//store/mutations.js
export default {
    changeCity(state, city) {
        state.city = city;
    },
    changearea(state, area) {
        state.area = area
    }
}
//store/index.js
import state  from './state';
import mutations  from './mutations';
export default new Vuex.Store({
    state,
    mutations
})

vuex----module的使用

//store/index.js
let moduleA = {
    state: {
        city: '郑州'
    },
    mutations: {
        changeCity(state, city) {
            state.city = city;
        }
    }
}
let moduleB = {
    state: {
        area: '中原区'
    },
    mutations: {
        changearea(state, area) {
            state.area = area
        }
    },
    actions: {
        changeArea(ctx, area) {
            ctx.commit('changearea', area);
        }
    },
}
export default new Vuex.Store({
    modules: {
        a:moduleA,
        b:moduleB
    }
})

//views/index.vue
{{$store.state.a.city}}
{{$store.state.b.area}}
//views/index.vue
<template>
    <div class="student">
        <h3>学生信息---{{$store.state.a.city}}-{{$store.state.b.area}}</h3>
        <div><button @click="change()">修改city信息</button></div>
        <div><button @click="changeA()">修改area信息</button></div>
    </div>
</template>

<script>
import { mapActions ,mapMutations} from 'vuex';

export default {
    methods: {
        ...mapMutations(['changeCity']),
        ...mapActions(['changeArea']),
        change(){
            this.changeCity('上海');
        },
        changeA(){
            this.changeArea('浦东新区');
        }
    }
}
</script>

<style></style>

8、路由

声明式路由、编程式路由

① 声明式路由

<router-link to="/student" tag="button">学生页面</router-link>

② 编程式路由(路由传参)

//views/index.vue
<button @click="go()">datainfo</button>

methods:{
    go(){
        //不传参
        this.$router.push('/datainfo');
        //or
        this.$router.push({path:'/datainfo'});
    }
}

//router/index.js
const routes = [
  {
    path:'/datainfo',
    component:DataInfo
  }
]
//views/index.vue
<button @click="go()">datainfo</button>

methods:{
    go(){
        //传参
        this.$router.push({name:'/datainfo',params:{id:1}});
        //or
        this.$router.push({path:'/datainfo',query:{id:2}});
        //or
        this.$router.push({path:'/datainfo?id=3'});
    }
}

//router/index.js
const routes = [
  {
    path:'/datainfo',
    name:'datainfo',
    component:DataInfo
  }
]

//views/datainfo.vue
created:{
    //接收参数
    //1
    console.log('params',this.$route.params);
    //2、3
    console.log('query',this.$route.query);
}
//views/index.vue
<button @click="go()">datainfo</button>

methods:{
    go(){
        let id = 10;
        this.$router.push({path:`/datainfo/${id}`});
    }
}

//router/index.js
const routes = [
  {
    path:'/datainfo/:id',
    name:'datainfo',
    component:DataInfo
  }
]

//views/datainfo.vue
created:{
    //接收参数
    console.log('params',this.$route.params);
}

③ router.go(n)

//跳转到上一个页面(上一个页面是保存在history中的)
this.$router.go(-1);

④ router.replace()

//跳转到目标to页面中,当前页面不会保存在history中
this.$router.replace(to);

//a->to->b
//此时页面to不保存在history中,跳转到b页面中,点击返回上一页,会返回到a页面中,而不是to
//也可以理解为a页面的地址替代了to的页面地址
this.$router.go(-1);

⑤ 路由传参--props

//router/index.js
children:[
  {
    path:'datacount/:type',
    component:dataCount,
    props:true
  }
]
//views/find.vue
<router-link to="/datainfo/datacount/beauty">丽人</router-link>

//components/findPage/findCont.vue
<template>
    <div>datainfo子页面---{{$route.params.type}}--{{type}}</div>
</template>

<script>
export default {
    props:['type']
}
</script>

⑥ 导航守卫

执行顺序

进入某页面时

全局前置守卫-->路由独享守卫-->组件内守卫beforeRouteEnter-->全局解析守卫-->全局后置守卫

离开某页面时

组件内守卫beforeRouteLeave-->全局前置守卫-->全局解析守卫-->全局后置守卫

某页面中二级路由跳转时

全局前置守卫-->组件内守卫beforeRouteUpdate-->全局解析守卫-->全局后置守卫

全局前置守卫beforeEach(⭐)

next(false)---和不写next是一样的,不往下进行

next({path:'/shop'})---跳转到shop

//router/index.js
router.beforeEach((to,from,next)=>{
  console.log('全局前置守卫',to);
  console.log('全局前置守卫',from);
    //进入下一个钩子函数,如果没有,正常进行页面跳转
  next();
    //or
 next({path:'/shop'})//这样子直接写会进入死循环,可以添加条件防止死循环
})

//可以重定向
if (to.fullPath == '/upload') {
    next({ path: '/my' })
} else {
    next();
}

全局解析守卫beforeResolve

router.beforeResolve((to,from,next)=>{
  console.log('全局解析',to);
  console.log('全局解析',from);
  next();
})

全局后置守卫afterEach

没有next

router.afterEach((to,from)=>{
  console.log('全局后置守卫',to);
  console.log('全局后置守卫',from);
})

路由独享beforeEnter

//router/index.js
{
    path: '/upload',
    component: Upload,
    beforeEnter: (to, from, next) => {
      console.log('路由独享',to);
      console.log('路由独享',from);
      next();
    }
},

组件内守卫

beforeRouteEnter-->进入该页面时触发

beforeRouteLeave-->离开该页面时触发

beforeRouteUpdate-->例如:该页面二级路由从一个跳转到另一个时触发

//views/index.vue
export default{
    beforeRouteEnter (to, from, next) {//不能获取组件实例this
        // ...
    },
    beforeRouteUpdate (to,from,next){//可以获取this
},
    beforeRouteLeave (to,from,next){//可以获取this
}
}

⑦ 路由元信息--路由鉴权

定义路由的时候可以配置meta字段

//router/index.js
{
    path: '/upload',
    name: 'upload',
    component: Upload,
    meta:{
      title:'首页'
    }
},

进入某个页面需要鉴权,修改meta

meta:{
    title:'首页',
    requiresAuth:true
}

进行鉴权,可以在全局前置守卫里,判断meta,检查元字段

//router/index.js
router.beforeEach((to, from, next) => {
  console.log(to);
  document.title = to.meta.title;
  // 鉴权
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (localStorage.getItem('token')) {
      next();
    } else {
      next({
        path: '/login',
        query: {
          redirect: to.fullPath
        }
      })
    }
  } else {
    next();
  }
})

9、webpack打包项目

浏览器只能够识别html、css、js

webpack就是将.vue后缀名的文件,转为html、css、js

npm run build

打好包之后多个dist文件夹

dist/js

以“chunk-”开头的文件是使用到的第三方的包

以“app-”开头的是自己的代码

打包之后的文件打开:

新开窗口,打开dist文件夹,

然后dist/index.html-->右键open with live server

mode设置为history需要和后端配合,不然就会报错

const router = new VueRouter({
  routes,
  //mode:history,
  linkExactActiveClass: 'active'
})

将文件单独打包---路由懒加载

{
    path: '/my',
    name: 'my',
    component:()=>import('../views/My.vue'),
    meta: {
        title: '我的',
        requiresAuth: true
    }
},

component使用import引入,就可以将“我的”这个页面单独打包生成一个文件

10、使用webpack打包

webpack 是前端项目工程化的具体解决方案

webpack有打包的功能,但是默认只能对js文件直接进行打包

1-6 只打包js文件

① 创建对应的文件【对应的依赖关系】

//src/foo.js
export default function a(){
    console.log('foo');
}
//src/bar.js
import a from './foo'
export default function bar() {
  console.log('bar');
  a();
}
//src/index.js
import bar from './bar';
bar();
console.log('index');

② 安装

webpack-dev-server 可以让 webpack 监听项目源代码的变化,从而进行自动打包构建

配置了 webpack-dev-server 之后,打包生成的文件存放到了内存

  • 不再根据 output 节点指定的路径,存放到实际的物理磁盘上

  • 提高了实时打包输出的性能,因为内存比物理磁盘速度快很多

webpack-dev-server 生成到内存中的文件,默认放到了项目的根目录中,而且是虚拟的、不可见的

  • 可以直接用 / 表示项目根目录后面跟上要访问的文件名称,即可访问内存中的文件

  • 例如 /bundle.js 就表示要访问 webpack-dev-server 生成到内存中的 bundle.js 文件

npm install webpack@4.32.2 webpack-cli@3.3.2 webpack-dev-server@3.5.1 -D-S

③ 配置入口文件/出口文件

webpack.config.js 是 webpack 的配置文件。webpack 在真正开始打包构建之前,会先读取这个配置文件,从而基于给定的配置,对项目进行打包。

  • 默认的打包入口文件为 src -> index.js

  • 默认的输出文件路径为 dist -> main.js

可以在 webpack.config.js 中修改打包的默认约定

  • 通过 entry 节点指定打包的入口

  • 通过 output 节点指定打包的出口

//  /webpack.config.js
const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
  }
};

④ 配置命令行

//package.json
"scripts": {
    "start": "webpack --config webpack.config.js --mode development"
}

⑤ 执行命令行

npm run start

⑥ 版本低导致的错误

解决:

//vscode的终端中输入
$env:NODE_OPTIONS="--openssl-legacy-provider"

//重新打包
npm run start
// /index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="dist/bundle.js"></script>
</head>
<body>
    
</body>
</html>

⑦ 打包含css的文件(loader)

创建css文件

/src/css/index.css

安装插件loader

npm  install  css-loader@3 --save-dev            
npm install  style-loader@2 --save-dev

配置插件

//webpack.config.js
module: {
    rules: [
        {
            test: /\.css$/,
            use: [
                'style-loader' ,
                'css-loader'
            ]
        }
    ]
}

打包

//css文件会被打包在bundle.js中
npm run start

⑧ 打包自动生成html文件(插件plugins)

8、9结合起来一块用

安装

html-webpack-plugin 是 webpack 中的 HTML 插件,可以通过此插件自定制 index.html 页面的内容。将 src 目录下的 index.html 首页,复制到项目根目录中一份

//将js自动注入index.html
npm i html-webpack-plugin@4.5.0 -D-S

⑨ 打包,删除目录

安装

npm i clean-webpack-plugin@^3.0.0 -D-S

配置插件

const webpack = require("webpack");
//自动生成html
const HtmlWebpackPlugin = require('html-webpack-plugin');
//自动删除
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
......
plugins: [// 对应的插件
    new HtmlWebpackPlugin({    //配置
        filename: 'index.html',    //输出文件名
        //以当前目录下的index.html文件为模板生成dist/index.html文件
        template: './public/index.html',   
        inject: 'body' // 这个地方必须是body,否则vue打包会有bug
    }),
    new CleanWebpackPlugin(), //删除目录
 ]

注意:

原index.html中的script引入可以删掉,打包生成的html文件中会自动引入。

打包npm run start

⑩ 热启动、热更新

热启动:自动打开浏览器进行预览

热更新:更改原来的代码,页面随之改变

在模块内添加属性:devServer

//webpack.config.js
devServer: {//配置此静态文件服务器,可以用来预览打包后项目
    hot: true,//热加载
    contentBase: path.resolve(__dirname, 'dist'),//开发服务运行时的文件根目录
    host: 'localhost',//主机地址
    port: 9090,//端口号
    compress: true//开发服务器是否启动gzip等压缩
}

在package.json的scripts中添加

"dev": "webpack-dev-server --mode development --open"

启动

npm run dev

⑪ 解析.vue文件

安装

npm i vue-loader@15.9.5 -D-S
npm i vue-template-compiler@2.6.12 -D-S
npm i vue@2.6.12 -S

11、element-UI

elementUI

① 安装

npm i element-ui -S

② 引入并使用

//main.js
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);

③侧边栏

element-ui侧边栏

侧边栏路由跳转

父级标签中设置:router="true"

子级标签中设置index="/home/dashboard"要跳转到的路由(路由要先在router/index.js中设置好)

//homeAside.vue
<el-menu :router="true">
    <el-menu-item index="/home/dashboard">
        <span slot="title">首页</span>
    </el-menu-item>
</el-menu>

侧边栏高亮

设置default-active的激活路径

el-menu有默认的边框,可以取消掉

<el-menu :default-active="$route.fullPath">

<el-menu-item index="/home/dashboard">

侧边栏折叠

折叠的时候点击折叠按钮,侧边栏会折叠起来。折叠按钮位于头部,和侧边栏不在同一个组件,属于非父子组件进行传值。两个组件之间通过父级传递参数改变状态。

1、父级中定义参数,然后传递给侧边栏

//父级
<home-aside :is-collapse="isCollapse"></home-aside>//侧边栏组件
data() {
    return {
        isCollapse:false
    }
},

//侧边栏
<el-menu :collapse="isCollapse">

props:{
    isCollapse:Boolean
},

2、头部给父组件传值,父组件进行修改

//头部
change(){
    this.$emit('collapse')
}

//父组件
<home-header @collapse="collapse()"></home-header>
collapse(){
    this.isCollapse = !this.isCollapse
}

折叠时候侧边栏的宽度问题

<style>
.el-menu-vertical-demo:not(.el-menu--collapse) {
    width: 200px;
    min-height: 400px;
 }
</style>

侧边栏遍历

设置meta元信息,通过这个判断路由是几级路由,据此遍历(一级路由只有一个页面,不可以展开)

//router/index.js
{
    path: '/home',
    component: () => import('@/views/Home'),
    redirect:'/home/dashboard',
    meta:{
        title:'首页',
        level:1,
        icon:'el-icon-s-home'
    },
    children:[
      {
        path:'dashboard',
        component: () => import('@/views/Dashboard'),
        meta:{
            title:'看板'
        }
      }
    ]
  },

将使用到的侧边导航封装成组件(./AsideNav.vue),传入路由信息

使用v-if、v-else进行遍历,通过item.meta.level判断是几级路由,是一级就显示<el-menu-item>,不是就显示<el-submenu>

<template>
    <div>
        <el-menu-item :index="item.path+'/'+item.children[0].path" v-if="item.meta.level">
            <i :class="item.meta.icon+' el-icon'"></i>
            <span slot="title">{{item.meta.title}}</span>
        </el-menu-item>
        <el-submenu :index="item.path" v-else>
            <template slot="title">
                <i :class="item.meta.icon+' el-icon'"></i>
                <span>{{item.meta.title}}</span>
            </template>
            <el-menu-item-group>
                <el-menu-item :index="item.path+'/'+child.path" v-for="child in item.children" :key="child.path">{{child.meta.title}}</el-menu-item>
            </el-menu-item-group>
        </el-submenu>
    </div>
</template>

props:{
    item:Object
}

④首页

看板图表

⑤表格

刷新表格数据

增删改查之后重新请求一次表格数据即可。

过滤器

| :表示过滤

{{scope.row.status|switchStatus}}:switchStatus是过滤return之后要显示的文本,scope.row.status传入过滤器的值

<template>
    <el-table-column prop="status" label="活动状态" width="120">
        <template slot-scope="scope">
            <el-tag :type="type(scope.row.status)">{{scope.row.status|switchStatus}}</el-tag>
        </template>
    </el-table-column>
</template>

<script>
export default{
    filters:{
        switchStatus(val){
            let result;
            result = val===1?'启用':'禁用';
            return result;
        }
    }
}
</script>

插槽获取数据

表格中插槽

<template slot-scope="scope">配合scope.row可以获取数据

scope.row是表格中这一行的所有内容

<template>
    <el-table-column prop="status" label="活动状态" width="120">
        //插槽
        <template slot-scope="scope">
            <el-tag :type="type(scope.row.status)">{{scope.row.status|switchStatus}}</el-tag>
        </template>
    </el-table-column>
</template>

日期时间格式化

插件及使用:http://momentjs.cn/

安装

npm install moment --save

使用

//filter/index.js
import moment from 'moment';
export function formatDate(val){
    return moment(val).format('YYYY-MM-DD hh:mm:ss');
}
//多次使用可以注册成全局组件
//main.js
import {formatDate} from './filters'
Vue.filter('formatDate',formatDate);//全局过滤器
<template slot-scope="scope">
    <el-tag :type="type(scope.row.status)">
        {{scope.row.status | switchStatus}}
    </el-tag>
</template>

dialog弹窗关闭

父子组件传值,改变dialogVisible的值,让弹窗显示关闭。

//hide不可以加括号,报错
<el-dialog title="添加活动" :visible.sync="dialogVisible" :before-close="hide">

⭐文件上传

后端

npm i multer  // nodejs用于上传文件的模块 
npm i uuid //uuid用来生成唯一标识符

callback(null, "static/uploadImg"),参数2是指定的upload文件夹

配置静态文件夹:app.use('/uploadImg', express.static(path.join(__dirname, 'static/uploadImg')));

//utils/upload.js
const multer = require("multer")  // nodejs用于上传文件的模块
const uuid = require("uuid")  //uuid用来生成唯一标识符

/*
  multer是node的中间件, 处理表单数据 主要用于上传文件  multipart/form-data
*/

// 指定存储位置
const storage = multer.diskStorage({
  // 存储位置
  destination(req, file, callback) {
    // 参数一 错误信息   参数二  上传路径(此处指定upload文件夹)
    callback(null, "static/uploadImg")
  },
  // 确定文件名
  filename(req, file, cb) {
    //文件扩展名
    let extName = file.originalname.slice(file.originalname.lastIndexOf('.'))
    //新文件名
    let fileName = uuid.v1()
    cb(null, fileName + extName)
  }
})
// 得到multer对象  传入storage对象
const upload = multer({ storage })
module.exports = upload;

upload.single("file")表示单文件上传,里边的file需要和前端传过来的参数名字一致,比如前端的input中,<input type="file" name="file">这个name值,前后端要一致,前端的name是file1,后端就是file1。

//route/uploadRouter.js
let express = require('express');
let uploadRouter = express.Router();
let upload = require("../utils/upload");
//图片上传
uploadRouter.post("/upload", upload.single("file"), (req, res) => {
    // 需要返回图片的访问地址    域名+文件名
    const url = "http://localhost:5001/uploadImg/" + req.file.filename
    // console.log(req.file.filename);
    res.json({ url })
})
module.exports = uploadRouter;

前端

Element upload

前端请求接口是需要通过token验证的,但是文件上传不是通过axios上传的(token配置在了axios拦截器中),所以需要添加一个属性headers,通过它携带token提交到后端。

//upload.vue
<template>
    <el-upload class="avatar-uploader" action="http://localhost:5001/upload" 
    :show-file-list="true"
    :headers="headers"
        :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
        <img v-if="imageUrl" :src="imageUrl" class="avatar">
        <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
</template>

<script>
export default {
    data() {
        return {
            imageUrl: '',
            headers:{
                Authorization : "Bearer " + localStorage.getItem('token')
            }
        };
    },
    methods: {
        handleAvatarSuccess(res, file) {
            this.imageUrl = URL.createObjectURL(file.raw);
        },
        beforeAvatarUpload(file) {
            const isJPG = file.type === 'image/jpeg';
            const isLt2M = file.size / 1024 / 1024 < 2;

            if (!isJPG) {
                this.$message.error('上传头像图片只能是 JPG 格式!');
            }
            if (!isLt2M) {
                this.$message.error('上传头像图片大小不能超过 2MB!');
            }
            return isJPG && isLt2M;
        }
    }
}
</script>
<style scoped>
.avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
}
.avatar-uploader .el-upload:hover {
    border-color: #409EFF;
}
.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;
    line-height: 178px;
    text-align: center;
}
.avatar {
    width: 178px;
    height: 178px;
    display: block;
}
</style>

表格分页

<div class="block">
    <el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange"
        :current-page="currentPage" :page-sizes="[5, 10, 15]" :page-size="5"
        layout="total, sizes, prev, pager, next, jumper" :total="total">
    </el-pagination>
</div>

data(){
    return{
        currentPage: 1,
        pagenum:5
    }
}

// 每页条数
handleSizeChange(val) {
    console.log(`每页 ${val} 条`);
    this.pagenum = val;
},
// 当前是第几页
handleCurrentChange(val) {
    console.log(`当前页: ${val}`);
    this.currentPage = val;
},

向后端请求数据,传入当前页数(currentPage)、每页条数(page-size)。

export function getRoute(data){
    return axios({
        url:'/getroute',
        method:'get',
        params:data
    })
}

sql查询

//参数1:开始查询的位置  参数2:每页条数
let sql = `select * from route limit ${(page-1)*pagenum},${pagenum}`;
//可以返回总共几条数据
let sql2 = `select count(*) as total from route`;

⑥ 头部

面包屑导航

在路由文件中设置meta信息,如果是一级路由,设置level,二级路由不设置,以此区分路由是几级的。

//router/index.js
{
    path:'/upload',
    component: () => import('@/views/Home'),
    meta:{
        title:'文件上传',
        level:1,  //2级路由不设置此字段
        icon:'el-icon-upload'
    },
    children:[
      {
        path:'upload',
        component:()=>import('@/views/UpLoad'),
        meta:{
            title:'文件上传'
        }
      }
    ]
  },
<el-breadcrumb separator-class="el-icon-arrow-right">
    <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
    <el-breadcrumb-item
      v-for="item in info"
      :key="item.path"
      :to="{ path: item.path }">{{ item.title }}</el-breadcrumb-item>
</el-breadcrumb>

//监听路由
watch: {
    $route: {
      handler: function (to, from) {
        // console.log(to);
        let arr = [];
        if (to.matched[0].meta.level) {//判断是几级路由
          arr.push({
            title: to.matched[1].meta.title,
            path: to.matched[1].path,
          });
        } else {
          to.matched.forEach((item) => {
            arr.push({
              title: item.meta.title,
              path: item.path,
            });
          });
        }
        this.info = arr;
      },
      immediate: true,
    },
  }

退出登录

思路:路由前置守卫可以判断token,没有token就会重新返回登录页面,所以可以清空localStorage:localStorage.clear();,之后刷新页面location.reload();

<el-dropdown @command="handleCommand">
    <span class="el-dropdown-link">
        user<i class="el-icon-arrow-down el-icon--right"></i>
    </span>
    <el-dropdown-menu slot="dropdown">
        <el-dropdown-item command="a" disabled>个人信息</el-dropdown-item>
        <el-dropdown-item command="b" disabled>个人中心</el-dropdown-item>
        <el-dropdown-item command="e" divided>退出登录</el-dropdown-item>
    </el-dropdown-menu>
</el-dropdown>

handleCommand(command) {
    //this.$message('click on item ' + command);
    localStorage.clear();
    location.reload();
}

⑦ 导入导出

(excel表格和json数据之间的转换)

导入

//前端安装
npm i xlsx@0.16.0 -S

/utils/readFiles.js

// 读取文件
export const readFile = (file)=>{
    return new Promise(resolve=>{
        let reader = new FileReader()
        reader.readAsBinaryString(file)
        reader.onload = ev =>{
            resolve(ev.target.result)
        }
    })
}
//import.vue
<el-upload action="" :auto-upload="false" :on-change="onChange" :limit="1">
  <el-button size="mini" type="success">上传文件</el-button>
</el-upload>

<script>
import xlsx from "xlsx";
import { readFile } from "@/utils/readFile";
export default {
  methods: {
    async onChange(file) {
      let data = await readFile(file.raw);
      let workBook = xlsx.read(data, { type: "binary", cellDates: true });
      let workSheet = workBook.Sheets[workBook.SheetNames[0]];
      let jsondata = xlsx.utils.sheet_to_json(workSheet);
      console.log(jsondata);
    },
  }
};
</script>

导出

//前端安装
npm i vue-json-excel@0.3.0
//export.vue
//tableData->表格数据
//jsonFields->数据,需要与tableData 对应
<download-excel class="export-btn" :data="tableData" :fields="jsonFields" type="xls" header="clue" name="clue.xls">导出</download-excel>

<script>
import JsonExcel from 'vue-json-excel';
export default {
    components:{
        DownLoadExcel:JsonExcel
    }
    data(){
        return{
            jsonFields:{
                '客户名称':'clientname',
                '客户标签':'clienttag',
                '手机号码':'telephone',
                '线索来源':'cluefrom',
                '意向车型':'want',
                '跟进人':'follow',
                '分配状态':'status'
          }
        }
    }
}
</script>

⑧注意事项

父子传值

父子传值是单向的,子组件中要修改父组件传递过来的值,可以通过$emit将数据传回父组件,在父组件中进行修改。

axios put传数据

要传输的数据应该用data接收

//错误写法
export function delClue(id){
    return axios({
        url:'/delclue',
        method:'put',
        id
    })
}
//正确写法
export function delClue(id){
    return axios({
        url:'/delclue',
        method:'put',
        data:id
    })
}

导航重复点击报错

//router/index.js
// 解决当前页面跳转到当前页面的报错信息
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}

解决scope波浪线问题

在代码之前加入<!-- eslint-disable-next-line -->

<!-- eslint-disable-next-line -->
<template slot="header" slot-scope="scope">

文件上传的边框不显示问题

style标签不要添加scoped属性,就可以显示边框了。

watch监听路由

注意:handler使用箭头函数,this指向发生了变化(箭头函数没有this),无法再通过this.info=arr这样子修改数值,打印this,this变成了undefined。

//this发生了变化
watch:{
    $route:{
        handler:(to,from)=>{
            console.log(to);
            console.log(this)//undefined
        },
        immediate:true
    }
}
//this指向Vue
watch:{
    $route:{
        handler:function(to,from){
            console.log(to)
        },
        immediate:true
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值