vue-admin学习

344 阅读8分钟

1.定制配置

vue.config.js

基础配置:指定应⽤上下⽂、端⼝号

const port = 7070;
module.exports = {
    publicPath: '/best-practice', // 部署应⽤包时的基本 URL
    devServer: {
        port,
    }
};

配置webpack:

configureWebpack

范例:设置⼀个组件存放路径的别名

const path = require('path')
module.exports = {
    configureWebpack: {
        resolve: {
            alias: {
                comps: path.join(__dirname, 'src/components'),
            }
        }
    }
}

范例:设置⼀个webpack配置项⽤于⻚⾯title, vue.config.js
module.exports = {
    configureWebpack: {
        name: "vue项⽬"
    }
};

//在宿主⻚⾯使⽤lodash插值语法使⽤它, ./public/index.html
<title><%= webpackConfig.name %></title>
//webpack-merge合并出最终选项
//范例:基于环境有条件地配置, vue.config.js

// 传递⼀个函数给configureWebpack
// 可以直接修改,或返回⼀个⽤于合并的配置对象
configureWebpack: config => {
    config.resolve.alias.comps = path.join(__dirname, 'src/components')
    if (process.env.NODE_ENV === 'development') {
        config.name = 'vue项⽬'
    } else {
        config.name = 'Vue Project'
    }
}

chainWebpack

webpack-chain称为链式操作,可以更细粒度控制webpack内部配置。

范例: svg icon引⼊

  • 下载图标,存⼊src/icons/svg中

  • 安装依赖: svg-sprite-loader

      npm i svg-sprite-loader -D  
    

修改规则和新增规则,

// resolve定义⼀个绝对路径获取函数
const path = require('path')
function resolve(dir) {
    return path.join(__dirname, dir)
}
//...
chainWebpack(config) {
    // 配置svg规则排除icons⽬录中svg⽂件处理
    // ⽬标给svg规则增加⼀个排除选项exclude:['path/to/icon']
    config.module.rule("svg")
        .exclude.add(resolve("src/icons"))
    // 新增icons规则,设置svg-sprite-loader处理icons⽬录中的svg
    config.module.rule('icons')
        .test(/\.svg$/)
        .include.add(resolve('./src/icons')).end()
        .use('svg-sprite-loader')
        .loader('svg-sprite-loader')
        .options({ symbolId: 'icon-[name]' })
}


//使⽤图标, App.vue
<template>
<svg>
    <use xlink:href="#icon-wx" />
</svg>
</template>
<script>
    import '@/icons/svg/wx.svg'
</script>

//⾃动导⼊
//创建icons/index.js
const req = require.context('./svg', false, /\.svg$/)
req.keys().map(req);

//创建SvgIcon组件, components/SvgIcon.vue
<template>
<svg :class="svgClass" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>

<script>
export default {
    name: 'SvgIcon',
    props: {
        iconClass: {
            type: String,
            required: true
        },
        className: {
            type: String,
            default: ''
        }
    },
    computed: {
        iconName() {
            return `#icon-${this.iconClass}`
        },
        svgClass() {
            if (this.className) {
                return 'svg-icon ' + this.className
            } else {
                return 'svg-icon'
            }
        }
    }
}
</script>
<style scoped>
.svg-icon {
    width: 1em;
    height: 1em;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
}
</style>

环境变量和模式

  • 如果想给多种环境做不同配置,可以利⽤vue-cli提供的模式。
  • 默认有 development 、 production 、 test 三种模式,对应的,它们的配置⽂件形式 是 .env.development 。

范例:定义⼀个开发时可⽤的配置项,

//创建.env.dev
# 只能⽤于服务端
foo=bar
# 可⽤于客户端
VUE_APP_DONG=dong

修改mode选项覆盖模式名称, package.json

   "serve": "vue-cli-service serve --mode dev"
   
   

权限控制

  • 路由分为两种: constantRoutes 和 asyncRoutes ,前者是默认路由可直接访问,后者中定义的路由
  • 需要先登录,获取⻆⾊并过滤后动态加⼊到Router中。

image.png

  • 路由定义, router/index.js
  • 创建⽤户登录⻚⾯, views/Login.vue
  • 路由守卫:创建./src/permission.js,并在main.js中引⼊

⽤户登录状态维护

维护⽤户登录状态:路由守卫 => ⽤户登录 => 获取token并缓存

image.png

  • 路由守卫: src/permission.js
  • 请求登录: components/Login.vue
  • user模块:维护⽤户数据、处理⽤户登录等, store/modules/user.js

⽤户⻆⾊获取和权限路由过滤

登录成功后,请求⽤户信息获取⽤户⻆⾊信息,然后根据⻆⾊过滤asyncRoutes,并将结果动态添加⾄ router

维护路由信息,实现动态路由⽣成逻辑, store/modules/permission.js 获取⽤户⻆⾊,判断⽤户是否拥有访问权限, permission.js

// 引⼊store
import store from './store'
router.beforeEach(async (to, from, next) => {
    // ...
    if (hasToken) {
        if (to.path === '/login') { }
        else {
            // 若⽤户⻆⾊已附加则说明权限以判定,动态路由已添加
            const hasRoles = store.getters.roles && store.getters.roles.length > 0;
            if (hasRoles) {
                // 说明⽤户已获取过⻆⾊信息,放⾏
                next()
            } else {
                try {
                    // 先请求获取⽤户信息
                    const { roles } = await store.dispatch('user/getInfo')
                    // 根据当前⽤户⻆⾊过滤出可访问路由
                    const accessRoutes = await
                        store.dispatch('permission/generateRoutes', roles)
                    // 添加⾄路由器
                    router.addRoutes(accessRoutes)
                    // 继续路由切换,确保addRoutes完成
                    next({ ...to, replace: true })
                } catch (error) {
                    // 出错需重置令牌并重新登录(令牌过期、⽹络错误等原因)
                    await store.dispatch('user/resetToken')
                    next(`/login?redirect=${to.path}`)
                    alert(error || '未知错误')
                }
            }
        }
    } else {
        // 未登录...
    }
})

异步获取路由表

可以当⽤户登录后向后端请求可访问的路由表,从⽽动态⽣成可访问⻚⾯,操作和原来是相同 的,这⾥多了⼀步将后端返回路由表中组件名称和本地的组件映射步骤:

// 前端组件名和组件映射表
const map = {
//xx: require('@/views/xx.vue').default // 同步的⽅式
xx: () => import('@/views/xx.vue') // 异步的⽅式
}
// 服务端返回的asyncRoutes
const asyncRoutes = [
{ path: '/xx', component: 'xx',... }
]
// 遍历asyncRoutes,将component替换为map[component]
function mapComponent(asyncRoutes) {
asyncRoutes.forEach(route => {
route.component = map[route.component];
if(route.children) {
route.children.map(child => mapComponent(child))
}
})
}
mapComponent(asyncRoutes)

按钮权限: ⻚⾯中某些按钮、链接有时候需要更细粒度权限控制,这时候可以封装⼀个指令v-permission,放在需 要控制的按钮上,从⽽实现按钮级别权限控制 创建指令, src/directives/permission.js 测试, About.vue 该指令只能删除挂载指令的元素,对于那些额外⽣成的和指令⽆关的元素⽆能为⼒,⽐如

<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>
<template>
    <el-tab-pane v-if="checkPermission(['admin'])">
</template>
<script>
export default {
    methods: {
        checkPermission(permissionRoles) {
            return roles.some(role => {
                return permissionRoles.includes(role);
            });
        }
    }
}
</script>

⾃动⽣成导航菜单

导航菜单是根据路由信息并结合权限判断⽽动态⽣成的。它需要对应路由的多级嵌套,所以要⽤到递归 组件。

  • 创建侧边栏组件, components/Sidebar/index.vue
  • 创建侧边栏菜项⽬组件, layout/components/Sidebar/SidebarItem.vue
  • 创建侧边栏菜单项组件, layout/components/Sidebar/Item.vue

数据交互

数据交互流程: api服务 => axios请求 => 本地mock/线上mock/服务器api 封装request

对axios做⼀次封装,统⼀处理配置、请求和响应拦截。

安装axios: npm i axios -S
  • 创建@/utils/request.js
  • 设置VUE_APP_BASE_API环境变量,创建.env.development⽂件
  • 编写服务接⼝,创建@/api/user.js

数据mock

数据模拟两种常⻅⽅式

  1. 本地mock: 利⽤webpack-dev-server提供的before钩⼦可以访问express实例,从⽽定义接⼝ 修改vue.config.js,给devServer添加相关代码 调⽤接⼝, @/store/modules/user.js

  2. 线上esay-mock 诸如easy-mock这类线上mock⼯具优点是使⽤简单, mock⼯具库也⽐较强⼤,还能根据swagger规范 ⽣成接⼝。 使⽤步骤:

  3. 登录easy-mock

  • 若远程不可⽤,可以搭建本地easy-mock服务(nvm + node + redis + mongodb)
  • 先安装node 8.x、 redis和mongodb
  • 启动命令:
  • 切node v8: nvm list , nvm use 8.16.0
  • 起redis: redis-server
  • 起mongodb: mongod
  • 起easy-mock项⽬: npm run dev
  1. 创建⼀个项⽬
  2. 创建需要的接⼝
// user/login
{
    "code": function({ _req }) {
        const { username } = _req.body;
        if (username === "admin" || username === "jerry") {
            return 1
        } else {
            return 10008
        }
    },
    "data": function({ _req }) {
        const { username } = _req.body;
        if (username === "admin" || username === "jerry") {
            return username
        } else {
            return ''
        }
    }
}
// user/info
{
    code: 1,
        "data": function({ _req }) {
            return _req.headers['authorization'].split(' ')[1] === 'admin' ?
                ['admin'] : ['editor']
        }
}

调⽤:修改base_url, .env.development

VUE_APP_BASE_API = 'http://localhost:7300/mock/5e9032aab92b8c71eb235ad5'

项⽬测试

测试分类

常⻅的开发流程⾥,都有测试⼈员,他们不管内部实现机制,只看最外层的输⼊输出,这种我们称为⿊ 盒测试。⽐如你写⼀个加法的⻚⾯,会设计N个⽤例,测试加法的正确性,这种测试我们称之为E2E测 试。

还有⼀种测试叫做⽩盒测试,我们针对⼀些内部核⼼实现逻辑编写测试代码,称之为单元测试。 更负责⼀些的我们称之为集成测试,就是集合多个测试过的单元⼀起测试。 组件的单元测试有很多好处:

  • 提供描述组件⾏为的⽂档
  • 节省⼿动测试的时间
  • 减少研发新特性时产⽣的 bug
  • 改进设计
  • 促进重构

准备⼯作

在vue-cli中,预置了Mocha+Chai和Jest两套单测⽅案,我们的演示代码使⽤Jest,它们语法基本⼀致

新建vue项⽬时

选择特性 Unit Testing 和 E2E Testing

  • 单元测试解决⽅案选择: Jest
  • 端到端测试解决⽅案选择: Cypress 在已存在项⽬中集成
  • 集成Jest: vue add @vue/unit-jest
  • 集成cypress: vue add @vue/e2e-cypress

编写单元测试 单元测试(unit testing),是指对软件中的最⼩可测试单元进⾏检查和验证。

新建test/unit/mytest.spec.js, *.spec.js 是命名规范

function add(num1, num2) {
    return num1 + num2
}
// 测试套件 test suite
describe('mytest', () => {
    // 测试⽤例 test case
    it('测试add函数', () => {
        // 断⾔ assert
        expect(add(1, 3)).toBe(3)
        expect(add(1, 3)).toBe(4)
        expect(add(-2, 3)).toBe(1)
    })
})

执⾏单元测试

执⾏: npm run test:unit

断⾔API简介

  • describe : 定义⼀个测试套件
  • it :定义⼀个测试⽤例
  • expect :断⾔的判断条件

测试Vue组件

vue官⽅提供了⽤于单元测试的实⽤⼯具库 @vue/test-utils

  • 创建⼀个vue组件components/mytest.vue
  • 测试该组件, test/unit/mytest.spec.js
import mytest from '@/components/mytest.vue'
describe('mytest.vue', () => {
    // 检查组件选项
    it('要求设置created⽣命周期', () => {
        expect(typeof mytest.created).toBe('function')
    })
    it('message初始值是vue-test', () => {
        // 检查data函数存在性
        expect(typeof mytest.data).toBe('function')
        // 检查data返回的默认值
        const defaultData = mytest.data()
        expect(defaultData.message).toBe('vue-test')
    })
})
//检查mounted之后预期结果
//使⽤@vue/test-utils挂载组件
import { mount } from '@vue/test-utils'
it("mount之后测data是xxx", () => {
    const wrapper = mount(mytest);
    expect(wrapper.vm.message).toBe("xxx");
});
it("按钮点击后", () => {
    const wrapper = mount(mytestComp);
    wrapper.find("button").trigger("click");
    // 测试数据变化
    expect(wrapper.vm.message).toBe("按钮点击");
    // 测试html渲染结果
    expect(wrapper.find("span").html()).toBe("<span>按钮点击</span>");
    // 等效的⽅式
    expect(wrapper.find("span").text()).toBe("按钮点击");
});

测试覆盖率

Jest⾃带覆盖率,很容易统计我们测试代码是否全⾯。如果⽤的mocha,需要使⽤istanbul来统计覆盖 率。 package.json⾥修改jest配置

"jest": {
    "collectCoverage": true,
        "collectCoverageFrom": ["src/**/*.{js,vue}"],
}

若采⽤独⽴配置,则修改jest.config.js:

module.exports = {
    "collectCoverage": true,
    "collectCoverageFrom": ["src/**/*.{js,vue}"]
}
在此执⾏ npm run test: unit
  • % stmts是语句覆盖率(statement coverage):是不是每个语句都执⾏了?
  • % Branch分⽀覆盖率(branch coverage):是不是每个if代码块都执⾏了?
  • % Funcs函数覆盖率(function coverage):是不是每个函数都调⽤了?
  • % Lines⾏覆盖率(line coverage):是不是每⼀⾏都执⾏了?

E2E测试

借⽤浏览器的能⼒,站在⽤户测试⼈员的⻆度,输⼊框,点击按钮等,完全模拟⽤户,这个和具体的框 架关系不⼤,完全模拟浏览器⾏为。 运⾏E2E测试

npm run test: e2e
//修改e2e / spec / test.js
// https://docs.cypress.io/api/introduction/api.html
describe('测试代码', () => {
    it('先访问⼀下', () => {
        cy.visit('/')
        // cy.contains('h1', 'Welcome to Your Vue.js App')
        cy.contains('span', 'xxxx')
    })
})

//测试未通过,因为没有使⽤MyComponent.vue,修改App.vue
< div id = "app" >
    <img alt="Vue logo" src="./assets/logo.png">
        <!-- <HelloWorld msg="Welcome to Your Vue.js App" /> -->
        <MyComponent></MyComponent>
</div>

import MyComponent from './components/MyComponent.vue'
export default {
    name: 'app',
    components: {
        HelloWorld, MyComponent
    }
}
//测试通过~
//测试⽤户点击
// https://docs.cypress.io/api/introduction/api.html
describe('测试代码', () => {
    it('先访问⼀下', () => {
        cy.visit('/')
        // cy.contains('h1', 'Welcome to Your Vue.js App')
        cy.contains('#message', 'xxxxx')
        cy.get('button').click()
        cy.contains('span', '按钮点击')
    })
})