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 项目名
![](https://img-blog.csdnimg.cn/img_convert/520b5d4c9f4f458703e4df2a192db05e.png)
![](https://img-blog.csdnimg.cn/img_convert/95f6ce47fc3b63aa5b93484d3adf5ee5.png)
![](https://img-blog.csdnimg.cn/img_convert/c06c2e6326235354f1e681c1b118d4f4.png)
![](https://img-blog.csdnimg.cn/img_convert/e930a613ecdf87d4ba648e38b44d95a4.png)
![](https://img-blog.csdnimg.cn/img_convert/e39fc705f37e6dd90b13636287105339.png)
![](https://img-blog.csdnimg.cn/img_convert/b4fb2fab82b549ee1a316818f335cbfb.png)
![](https://img-blog.csdnimg.cn/img_convert/b9e124df9c42a5fb6ed5c35fee0a03dc.png)
![](https://img-blog.csdnimg.cn/img_convert/a49fe03c0a33e159d957bcd077ce188e.png)
![](https://img-blog.csdnimg.cn/img_convert/580a06d949f3c246228bee19c6aff30e.png)
6、vue单页面应用(路由)
路由的实现:
7、vuex
![](https://img-blog.csdnimg.cn/img_convert/6560094ee346b55beb4b8712275660f7.png)
安装:
![](https://img-blog.csdnimg.cn/img_convert/09bc10111138dd5f888b6fd0bbcdd9ef.png)
(在创建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
⑥ 版本低导致的错误
![](https://img-blog.csdnimg.cn/img_convert/802b430ce5dbc2a063d435cff8e3fdb7.png)
解决:
//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
① 安装
npm i element-ui -S
② 引入并使用
//main.js
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
③侧边栏
侧边栏路由跳转
![](https://img-blog.csdnimg.cn/img_convert/86586272be719eacb7f80e9a0d023beb.png)
父级标签中设置: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>
侧边栏高亮
![](https://img-blog.csdnimg.cn/img_convert/f3bcb4629b468d4a549e071944487e64.png)
设置default-active的激活路径
el-menu有默认的边框,可以取消掉
<el-menu :default-active="$route.fullPath">
<el-menu-item index="/home/dashboard">
侧边栏折叠
![](https://img-blog.csdnimg.cn/img_convert/54a78d170dbe436a606858ea1601ea8e.png)
折叠的时候点击折叠按钮,侧边栏会折叠起来。折叠按钮位于头部,和侧边栏不在同一个组件,属于非父子组件进行传值。两个组件之间通过父级传递参数改变状态。
![](https://img-blog.csdnimg.cn/img_convert/56db455361591046cab204d8d0618f78.png)
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弹窗关闭
![](https://img-blog.csdnimg.cn/img_convert/ce957440f56c97cd518bdfa855c5d286.png)
父子组件传值,改变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;
前端
前端请求接口是需要通过token验证的,但是文件上传不是通过axios上传的(token配置在了axios拦截器中),所以需要添加一个属性headers,通过它携带token提交到后端。
![](https://img-blog.csdnimg.cn/img_convert/2b628348b2fdaec9df9999e6c6b771a4.png)
//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>
表格分页
![](https://img-blog.csdnimg.cn/img_convert/f0b93840497c638149daf25d62a1d55f.png)
![](https://img-blog.csdnimg.cn/img_convert/c4fc111e0cecf78b0992f50564148f00.png)
![](https://img-blog.csdnimg.cn/img_convert/a136caee69d7450900b016bdb0166001.png)
<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属性,就可以显示边框了。
![](https://img-blog.csdnimg.cn/img_convert/736772a5c664c0c6438c4310ad103bad.png)
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
}
}