项目配置
vue.config.js
在根目录下创建vue.config.js
,做一些相应的配置
//为什么是module.exports,因为此文件的执行环境是node环境
module.exports={
publicPath:'/best-practice',//部署应用包时的基本URL
devServer:{
port:port
},
configureWebpack:{
// 想index.html注入标题
name:'vue最佳实践'
}
}
// index.html——使用webpack中配置的name
<title><%= webpackConfig.name %></title>
优雅使用icon——svg
icon发展史
- 最初的时候,大部分图标都是用
img
来实现的,如果一个项目中涉及到很多图标的时候,那一个页面中的请求资源中img
占了大部分; - 后面慢慢出现了雪碧图,将很多图标整合到一个图片上,css利用background-position 定位显示不同的 icon 图标,比起最开始
img
的方式,雪碧图已经有了很大的优化,但是依然有一个非常大的痛点,那就是每新增一个图标,就得去更新原先的雪碧图,这样做风险是很大的,不小心影响了原先图标的位置时,那页面中某些图标就加载不出来了,这种方式是极难维护的。 - Font Awesome 出现,Font Awesome官网的体验不是太好,图标很小,找起来相当费力。
- iconfont出现,阿里的开源库,找起来相当轻松,而且图标数量相当可人。
iconfont使用方式
先总体讲一下,在iconfont官网上将需要的图标打包下载下来后,如下:
将上面的这些包放到项目中,如果仅仅用unicode和font-class的方式来展示图标的话,那么仅仅需要引入iconfont.css
即可,如果需要增加图标,新下载了图标包下来后,只需要将iconfont.css
文件更新一下即可。
如果使用svg的方式展示图标的话,这时候起作用的是iconfont.js
文件,如果图标有更新的话,将iconfont.js
文件替换掉就行。
-
unicode
Unicode 是字体在网页端最原始的应用方式,特点是:
- 兼容性最好,支持 IE6+,及所有现代浏览器。
- 支持按字体的方式去动态调整图标大小,颜色等等。
- 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。
注意:新版 iconfont 支持多色图标,这些多色图标在 Unicode 模式下将不能使用,如果有需求建议使用symbol 的引用方式
Unicode 使用步骤如下:
第一步:引入
iconfont.css
文件// iconfont.css @font-face {font-family: "iconfont"; src: url('iconfont.eot?t=1593657248234'); /* IE9 */ src: url('iconfont.eot?t=1593657248234#iefix') format('embedded-opentype'), /* IE6-IE8 */ //更新的时候最重要的是更新这一部分 url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), url('iconfont.woff?t=1593657248234') format('woff'), url('iconfont.ttf?t=1593657248234') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ url('iconfont.svg?t=1593657248234#iconfont') format('svg'); /* iOS 4.1- */ } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-yundong-:before { content: "\e611"; } .icon-yundong:before { content: "\e6ea"; } .icon-zhoubao:before { content: "\e68c"; } // 新增的图标后面直接加入即可 .icon-meilishuo:before { content: "\ea15"; } .icon-meilishuo1:before { content: "\e78e"; }
第二步:挑选相应图标并获取字体编码,应用于页面
<span class="iconfont">3</span>
-
font-class
font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。
与 Unicode 使用方式相比,具有如下特点:
- 兼容性良好,支持 IE8+,及所有现代浏览器。
- 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
- 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
- 不过因为本质上还是使用的字体,所以多色图标还是不支持的。
font-class 使用步骤如下:
第一步:引入
iconfont.css
文件<link rel="stylesheet" href="./iconfont.css">
注意:修改
iconfont.css
文件的方式与unicode
的方式是一样的第二步:挑选相应图标并获取类名,应用于页面:
<span class="iconfont icon-xxx"></span>
-
symbol
这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:
- 支持多色图标了,不再受单色限制。
- 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
- 兼容性较差,支持 IE9+,及现代浏览器。
- 浏览器渲染 SVG 的性能一般,还不如 png。
使用步骤如下:
第一步:引入项目下面生成的 symbol 代码:
<script src="./iconfont.js"></script>
第二步:加入通用 CSS 代码(引入一次就行):
.icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; }
第三步:挑选相应图标并获取类名,应用于页面:
<svg class="icon" aria-hidden="true"> <use xlink:href="#icon-xxx"></use> </svg>
vue项目中使用svg
使用svg好处
- 支持多色图标了,不再受单色限制。
- 支持像字体那样通过font-size,color来调整样式。
- 支持 ie9+
- 可利用CSS实现动画。
- 减少HTTP请求。
- 矢量,缩放不失真
- 可以很精细的控制SVG图标的每一部分
使用svg-icon的好处是我再也不用发送woff|eot|ttf| 这些很多个字体库请求了,所有的svg都可以内联在html内。
svg是真正的矢量图,放大缩小都不会失真。
安装loader
mpm i svg-sprite-loader -D
svg-sprite-loader
将加载的 svg
图片拼接成雪碧图,放到页面中,其它地方通过 复用。
本来只添加svg-sprite-loader
就行了,但是svg
也是图片的一种,所以file-loader
也会对其进行处理,所以就会冲突,解决的办法就是,在项目中新建一个文件icons
,使用file-loader
编译svg
的时候不编译icons
里面的图标
添加配置——vue.config.js
vue inspect --rules
:可查看webpack中有哪些相关的规则
[
'vue',
'images',
'svg',
'media',
'fonts',
'pug',
'css',
'postcss',
'scss',
'sass',
'less',
'stylus',
'js',
'eslint',
'icons'
]
vue inspect --rule svg
:单独查看某一项在webpack
中的配置
// 对svg处理的默认配置:使用file-loader
{
test: /\.(svg)(\?.*)?$/,
use: [
{
loader: 'file-loader',
options: {
name: 'static/img/[name].[hash:8].[ext]'
}
}
]
}
添加链式操作
vue-cli
默认情况下使用file-loader
对svg进行处理,会将svg放到/img
目录下,现在需要更改webpack配置,使用svg-sprite-loader处理
svg`。
-
直接将,默认配置中的
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
中的svg
删掉,不过这样处理是有风险的,因为项目中可能有的svg
确实需要当图片资源使用,而且第三方库中也有可能使用到svg
-
在webpack中重新做配置,只处理需要处理的那一部分
svg
module.exports={
publicPath:'/best-practice',//部署应用包时的基本URL
devServer:{
port:port
},
configureWebpack:{
// 想index.html注入标题
name:'vue最佳实践'
},
chainWebpack(config){
// set svg-sprite-loader
// 1.svg rule中要排除icons目录
config.module
.rule('svg')
.exclude.add(resolve('src/icons'))
.end()
// 2加一个规则icons
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
}
}
自动导入svg图标
// icons/index.js
const req = require.context('./svg', false, /\.svg$/)
req.keys().map(req);
// main.js
import './icons'
使用:
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xxx"></use>
</svg>
其实到这一步,项目中已经可以随意使用svg
图标了,为了能统一处理svg
图标的样式以及代码的可阅读性,咱们一起去创建一个svg
的公用组件去~
创建svg组件
- svg组件
<template>
<svg :class="svgClass" aria-hidden="true">
<use :href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal () {
return isExternal(this.iconClass)
},
iconName () {
return `#icon-${this.iconClass}`
},
svgClass () {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon () {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>
- 全局注册
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'
// 全局注册svg组件
Vue.component('svg-icon', SvgIcon)
- 使用
<svg-icon icon-class="keeping"></svg-icon>
权限控制及动态路由
路由定义
路由分为两种:constantRoutes
和 asyncRoutes
constantRoutes
是常规路由,不需要守卫,用户可直接访问
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error-page/401'),
hidden: true
}
]
asyncRoutes
:权限页面,用户需要登录并且拥有访问的权限角色才能访问
export const asyncRoutes = [
{
path: "/about",
component: Layout,
redirect: "/about/index",
children: [
{
path: "index",
component: () =>
import(/* webpackChunkName: "home" */ "@/views/About.vue"),
name: "about",
meta: {
title: "About",
icon: "qq",
roles: ['admin', 'editor']
},
}
]
}
]
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: constRoutes
})
路由守卫
// 白名单
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
if (whiteList.indexOf(to.path) !== -1) {
// 白名单, go directly
next()
} else {
api.shadow().then(async () => {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取权限
const roles = await store.dispatch('user/getInfo')
resetRouter()
// 挂载路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
router.addRoutes(accessRoutes)
next({
...to,
replace: true
}) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
} catch (error) {
next('login')
}
}
}).catch(() => {
next('login')
})
}
添加动态路由
根据用户角色过滤出可访问的路由并动态添加到router。
创建permission模块,store/modules/permission.js
import { asyncRoutes, constantRoutes } from '@/router'
/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
注意:我在公司项目里面生成动态路由的时候就犯难了,思考了很多,把有权限的路由与菜单栏展示一起考虑了,用户既有权限访问这些有权限的路由,然而某些有权限的路由还不能在菜单栏上呈现出来,比如有一些二级的详情页,菜单栏上不需要呈现这一项菜单的,当时候思考了很多,思绪也比较混乱,这次重新整理这部分的时候,思路突然清晰了很多,动态路由就是将所有有权限的路由都挂载上,这里不要跟菜单栏混淆,菜单栏渲染的那里有自己的逻辑处理,这样就各自处理各自的逻辑就好了。
按钮权限
- 封装按钮权限指令
封装一个指令v-permission
,从而实现按钮级别的权限控制,在src下创建directive目录,创建permission.js
// src/directive/permission.js
import store from '@/store'
export default {
inserted(el, binding, vnode) {
const { value } = binding
const roles = store.getters && store.getters.roles
if (value && value instanceof Array && value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`need roles! Like v-permission="['admin','editor']"`)
}
}
}
- 全局注册指令
在main.js
中,将自定义的权限指令全局注册
import permission from "./directive/permission";
Vue.directive("permission", permission);
v-permission
指令的使用
<button v-permission="['admin']">admin button</button>
- element中el-tabs使用指令无效的问题
上面封装的v-permission
指令,只能删除挂载指令的元素,对于额外生成的和指令无关的元素是不生效的
<el-tabs>
<el-tab-pane label="用户管理" name="first" v-permission="['admin', 'editor']">用户管理</el-tab-pane>
<el-tab-pane label="配置管理" name="second" v-permission="['admin', 'editor']">配置管理</el-tab-pane>
<el-tab-pane label="角色管理" name="third" v-permission="['admin']">角色管理 </el-tab-pane>
<el-tab-pane label="定时任务补偿" name="fourth" v-permission="['admin', 'editor']">定时任务补偿</el-tab-pane>
</el-tabs>
在el-tab-pane
标签上加上v-permission
指令后,根据上图中生成的代码,可发现,也仅仅是将此tab下的内容设置了权限,而几个可切换的tab并没有受到权限的控制,你会发现页面展示的时候,3个tab都存在,也可正常切换,但是每个tab下的内容会根据权限来展示与否,虽然也算是曲线实现了权限控制这个功能,但是总是不太优雅,此时只能用v-if来实现这个功能了:
在utils
文件下创建一个checkPermission
公用的方法:
import store from '@/store'
/**
* @param {Array} value
* @returns {Boolean}
* @example see @/views/permission/directive.vue
*/
export default function checkPermission(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
return false
}
return true
} else {
console.error(`need roles! Like v-permission="['admin','editor']"`)
return false
}
}
v-if
实现权限校验的使用
<template>
<el-tabs>
<el-tab-pane v-if="checkPermission(['admin'])">
</el-tabs>
</template>
导航菜单生成
导航菜单是根据路由信息并结合权限判断而动态生成的。它需要支持路由的多级嵌套,所以这里要用到递归组件。
创建:layout/Sidebar/index.vue
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'permission_routes',
'sidebar'
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
创建:layout/Sidebar/SidebarItem.vue
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
注意:使用了递归组件,递归组件必须设置name
面包屑
面包屑导航是通过$route.matched
数组动态生成的,面包屑是根据路由嵌套来生成的,所以设置路由的时候要根据业务去配置好。
<template>
<el-breadcrumb class="app-breadcrumb" separator-class="el-icon-arrow-right">
<transition-group name="breadcrumb">
<el-breadcrumb-item :key="item.path" v-for="(item,index) in levelList">
<span class="no-redirect" v-if="item.redirect==='noRedirect'||index==levelList.length-1">{{ $t('sidebar.'+item.meta.title) }}</span>
<a @click.prevent="handleLink(item)" v-else>{{ $t('sidebar.'+item.meta.title) }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import pathToRegexp from 'path-to-regexp'
export default {
data () {
return {
levelList: null
}
},
watch: {
$route: {
handler (route) {
this.getBreadcrumb()
},
immediate: true
}
},
methods: {
getBreadcrumb () {
let matched = this.$route.matched.filter(
item => item.meta && item.meta.title
)
const first = matched[0]
// 这一部分根据自己的项目设置:我们的项目home页是不需要面包屑的,所以设置了breadcrumb: false
if (this.$route.path === '/home') {
matched = [
{ path: '/', redirect: '/home', meta: { title: 'home', breadcrumb: false } }
].concat(matched)
} else if (!this.isHome(first)) {
// 根匹配只要不是home,就作为home下一级
matched = [
{ path: '/', redirect: '/home', meta: { title: 'home' } }
].concat(matched)
}
this.levelList = matched.filter(
item => item.meta && item.meta.title && item.meta.breadcrumb !== false
)
},
isHome (route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'home'.toLocaleLowerCase()
},
pathCompile (path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink (item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 36px;
background-color: #eeeeee;
margin-bottom: 20px;
width: 100%;
padding-left: 10px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
处理面包屑这里,最重要的一点就是:要将路由嵌套做好,路由嵌套做好了,后续的面包屑处理起来就非常容易了。另外,
path-to-regexp
处理path有点问题,就是如果path上跟了?tab=2
这样的参数时,点击面包屑的时候会报错,或许是我没找到path-to-regexp
库的正确使用方式,我自己重新进行了处理才解决这个问题的。
服务封装
数据交互流程:
api service => request=>local mock/esay-mock/server api
axios拦截器
- 有时需要对请求头、响应进行统一处理
- 请求不同数据源时url会变化,需要根据环境自动修改url
- 可能出现的跨域问题
import axios from 'axios'
import store from '../store/'
import {
Loading
} from 'element-ui'
let loading = null
const service = axios.create({
baseURL: process.env.BASE_URL, // 请求地址
timeout: 40000, // request timeout
withCredentials: true
})
// request interceptor
service.interceptors.request.use(config => {
// config.headers['Accept'] = 'application/vnd.sd.v2+json'
config.params = {
...config.params,
// 混入 多语言
lang: store.getters.language
}
if (config.isDefaultLoading) {
loading = Loading.service()
}
return config
}, error => {
Promise.reject(error)
})
service.interceptors.response.use(
response => {
const res = response.data
const { code } = res
if (response.config.isDefaultLoading) {
loading.close()
}
if (code !== 0) {
// 未登录
if (code === 401 || code === 403) {
if (window.location.pathname !== '/th/login') {
store.dispatch('user/logout').then(() => {
location.reload()
})
}
}
// @params config.isExistCode
// @doc 处理 服务端 特殊返回格式
// @default false
if (!response.config.isExistCode) {
return Promise.reject(res)
}
return Promise.reject(res)
} else {
return res
}
},
error => {
return Promise.reject(error)
}
)
export default service
BASE_URL
是设置的环境变量,创建.env.development/.env.production文件
// base api
BASE_API = '/dev-api'
数据mock
数据模拟两种常见方式,本地mock和线上esay-mock
本地mock
本地mock:修改vue.config.js,给devServer添加相关代码
- 安装
npm i body-parser -D
注意:post请求需要额外安装依赖:body-parser,用于解析post请求时的body中的参数
- vue.config.js
const bodyParser = require("body-parser");
module.exports = {
devServer: {
before: app => {
// 设置参数处理中间件,之后的每次请求都经过这两个中间件
app.use(bodyParser.json()); //处理post参数
app.use(
bodyParser.urlencoded({
// 处理url参数
extended: true
})
);
app.post("/th/api/login/api", (req, res) => {
const { login_name,password } = req.body;
if (login_name === "admin" && Number(password) === 123456) {
res.json({
code: 0,
message:'登录成功',
msg:'登录成功',
data: login_name
});
} else {
res.json({
code: -1,
message: "用户名或密码错误",
msg:'用户名或密码错误'
});
}
});
}
}
}
}
注意:本地mock了数据,服务端同时也提供了相关的接口,优先走本地的接口,服务端的接口是不请求的。 其实本地mock数据这种方式,是需要懂一些服务端代码的,而且前端自己mock数据工作量蛮大的,那咱们一起去看一下更优的方式
mockjs
与easy-mock
mockjs
介绍
- 数据类型丰富
支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。
- 拦截 Ajax 请求
不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据。安全又便捷
API
- Mock.mock(url, type, data)
使用
- 安装
yarn add mockjs -D
- 简单使用
import Mock from 'mockjs'
let dataSource = Mock.mock({
'dataSource|5': [{
'key|+1': 1,
'mockTitle|1': ['哑巴', 'Butter-fly', '肆无忌惮', '摩天大楼', '初学者'],
'mockContent|1': ['你翻译不了我的声响', '数码宝贝主题曲', '摩天大楼太稀有', '像海浪撞破了山丘'],
'mockAction|1': ['下载', '试听', '喜欢']
}]
})
export default dataSource
- 以请求的方式mock数据
// mock/api.js
// api.js
import Mock from 'mockjs'
const url = {
tableDataOne: 'http://20181024Mock.com/mode1/tableDataOne',
tableDataTwo: 'http://20181024Mock.com/mode1/tableDataTwo',
tableDataThi: 'http://20181024Mock.com/mode1/tableDataThi'
}
export default [
Mock.mock(url.tableDataOne, {
message: '获取成功',
msg: '操作成功',
code: 0,
'data|5': [{
'key|+1': 1,
'mockTitle|1': ['哑巴', 'Butter-fly', '肆无忌惮', '摩天大楼', '初学者'],
'mockContent|1': ['你翻译不了我的声响', '数码宝贝主题曲', '摩天大楼太稀有', '像海浪撞破了山丘'],
'mockAction|1': ['下载', '试听', '喜欢']
}]
})
]
// 请求数据
import '@/mock/api.js'
import $axios from 'axios'
$axios.get('http://20181024Mock.com/mode1/tableDataOne').then(res => {
if (res.status === 200) {
let data = res.data.data
console.log(data)
}
})
- 创建node服务
mock/mock-server.js
let express = require('express') // 引入express
let Mock = require('mockjs') // 引入mock
let app = express() // 实例化express
// 解决跨域问题
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
res.header('Access-Control-Allow-Headers', 'X-Requested-With')
res.header('Access-Control-Allow-Headers', 'Content-Type')
next()
})
app.use('/mode2/DataOne', function (req, res) {
res.json(Mock.mock({
'status': 200,
'code': 0,
'data|1-9': [{
'key|+1': 1,
'mockTitle|1': ['肆无忌惮'],
'mockContent|1': ['角色精湛主题略荒诞', '理由太短 是让人不安', '疑信参半 却无比期盼', '你的惯犯 圆满', '别让纠缠 显得 孤单'],
'mockAction|1': ['下载', '试听', '喜欢']
}]
}))
})
app.listen('3333', () => {
console.log('监听端口 3333')
})
// 获取数据
$axios.get('http://localhost:3333/mode2/DataOne').then(res => {
if (res.status === 200) {
let data = res.data.data
console.log(data)
}
})
easy-mock
easy-mock
实现了cors,所以咱们使用的时候,不用担心跨域的问题。
- 准备工作
- 步骤
因为easy-mock的官网不是太稳定,自己在本地搭建一个easy-mock的服务器
- 启动redis
redis-server
- 启动mongodb
mongod
- 切换node版本
nvm use 8.16
- 启动easy-mock
npm run dev
- 登录easy-mock
- 创建项目
- 创建需要的接口
- 修改base_url,.env.development
VUE_APP_BASE_API = 'https://easy-mock.com/mock/5cdcc3fdde625c6ccadfd70c/kkb-
cart'
- 项目中直接调用相关接口即可
解决跨域
现在开发基本都是前后端分离的,接口服务器会与前端分离开来,所以经常情况下会造成跨域的问题。那么跨域到底是前端的问题还是后端的问题呢?所有的跨域问题一定是后端的问题,跟前端关系不大,但是前端是有办法解决的。
浏览器的问题引起的,只要发出去的请求和当前网站所在的地址不一样(包括:协议、端口号、子域名),会阻止请求发出去,有以下几种方式解决跨域问题:
- jsonp script的src,get请求(其实是躲避,没有真正解决)现在基本已经淘汰了
- cors options预处理(浏览器会发送2次请求)公共api一般用这种方式
- proxy nginx代理,不直接与api server打交道就不会跨域,服务器请求服务器是不存在跨域问题的
- webpack中配置proxy代理
devServer:{
port:port,
proxy: {
// 代理/th/api/login 到http://scm.ksher.cn/th/api/login
[process.env.BASE_URL]: {
target: "http://scm.ksher.cn",
changeOrigin: true,
pathRewrite:{
// 重写之后:代理/th/api/login 到http://scm.ksher.cn/login
["^"+process.env.BASE_URL]:""
}
},
}
注意:如果接口本身没有/th/api,则需要通过pathRewrite来重写了地址