关于这个项目的相应问题逐字稿

632 阅读16分钟

1、说说项目中svg-icon的实现思路

  • 了解什么是require.context
    • 概念:是一个webpack的api,通过执行require.context函数获取一个特定的上下文,主要用来实现自动化导入模块,在前端工程中,如果遇到从一个文件夹引入很多模块的情况,可以使用这个api,它会遍历文件夹中的指定文件,然后自动导入,使得不需要每次显式的调用import导入模块。
    • 参数:接收三个参数
      • directory {String} -读取文件的路径
      • useSubdirectories {Boolean} -是否遍历文件的子目录
      • regExp {RegExp} -匹配文件的正则
    //核心三行代码
    //require.context(目录,是否扫描子目录, 正则表达式)扫描某个目录下的文件,
    const req = require.context('./svg', false, /\.svg$/) // 这里为false,则不扫描svg下面的子目录

    const requireAll = rquireContext => requireContext.keys().map(requireContext)
    <=>等价于
    const requireAll = requireComtext => requireContext.keys().map(item => requireContext(item))
    requireAll(req)

image.png

在vue-config.js中引入svg-sprite-loader插件

image.png

调用方式

image.png

逐字稿:定义个全局组件svg-icon,然后通过require的context方法对svg文件夹下面的以svg结尾的文件进行全部扫描,得到一个包含所有svg的路径数组用常量req进行接收;即['./dashborad.svg'....]等。然后通过 const requireAll = requireContext => requireContext.keys().map(item => requireContext(item)) requierAll(res)这样的一段代码将所有路径转成对应的模块 module。然后配合 svg-sprite-loader这个插件 用进行调用。其中dashborad是svg文件名。

2、说说项目中vuex的设计思想

  • vuex是一个状态管理工具,集中式储存管理vue中所有组件的状态。他有5个属性:
    • state属性:基本数据
    • getters属性:从 state 中派生出的数据,相当于state的计算属性
    • mutation属性:更新 store 中数据的唯一途径,其接收一个以 state 为第一参数的回调函数
    • action 属性:提交 mutation 以更改 state,其中可以包含异步操作,数据请求
    • module 属性:用于将 store分割成不同的模块。
  • 模块化区分,不同功能的数据放入对应的js文件中,方便后期的维护和管理。在人资项目中,我们新建了user.js子模块,它负责项目中token和用户信息的存取。此外,我们也新建了getters.js模块,通过这个模块能使得更方便地获取仓库中state的状态。
  • 抽离相同的代码,在项目中我们将state里面的数据全部放入getters文件中,在项目中使用的时候能达到代码的简化效果。
    // index.js 仓库主模块
    import Vue from 'vue'
    import Vuex from 'vuex'
    import getters from './getters'
    import app from './modules/app'
    import settings from './modules/settings'
    import user from './modules/user'

    Vue.use(Vuex)

    const store = new Vuex.Store({
      modules: {
        app,
        settings,
        user
      },
      getters
    })
    export default store

    // getters.js 模块
    const getters = {
      sidebar: state => state.app.sidebar,
      device: state => state.app.device,
      token: state => state.user.token,
      avatar: state => state.user.userInfo.staffPhoto,
      company: state => state.user.userInfo.company,
      departmentName: state => state.user.userInfo.departmentName,
      routes: state => state.user.routes,
      name: state => state.user.userInfo.username,
      userId: state => state.user.userInfo.userId
    }
    export default getters
    
    // user.js 模块
    import { login, userInfoData } from '@/api/user'
    import { constantRoutes, resetRouter } from '@/router'
    import { getToken, setToken, removeToken } from '@/utils/auth'
    export default {
      namespaced: true,
      state: {
        token: getToken(),
        userInfo: {},
        routes: constantRoutes
      },
      mutations: {
        setRoutes(state, newRoutes) {
          state.routes = newRoutes
        },
        updataToken(state, newToken) {
          state.token = newToken
          setToken(newToken)
        },
        removeToken(state) {
          state.token = null
          removeToken()
        },
        getUserInfo(state, newInfo) {
          state.userInfo = newInfo
        }
      },
      actions: {
        // 在用户退出登录和token失效时封装清除token和用户信息代码
        delTokenMsg(context) {
          context.commit('removeToken')
          context.commit('getUserInfo', {})
          resetRouter()
        },
        // 用户token
        async updataTokenAsync(context, data) {
          const token = await login(data)
          context.commit('updataToken', token)
        },
        // 用户信息
        async updateUserInfo(context) {
          // 调用获取用户信息接口
          const data = await userInfoData()
          context.commit('getUserInfo', data)
          return data
        }
      }
    }

3、登录表单校验的规则如何实现的

这个需要配合el-form提供的modle/ref/rules三个属性配合进行校验

  • 首先需要通过prop配合rules给每个表单进行规则设置
  • 当用户输入完毕内容之后,我们在点击登录按钮的时候通过this.$refs.form.validate()进行校验
  • 涉及到用户协议勾选的时候,我们需要使用自定义校验(validator(规则, 绑定的表单元素的值, 回调函数))进行处理。
    // 复选框绑定form.isAgree
    <el-checkbox v-model="form.isAgree">
      用户平台使用协议
    </el-checkbox>

    //自定义校验
    isAgree: [{
      validator: (rule, value, callback) => {
        value ? callback() : callback(new Error('没有勾选用户平台协议'))
      }
    }]

4、分析下登录的流程(表单校验通过后做了哪些事情)

点击登录按钮的时候,我们首先要做的是对表单的数据进行校验,校验通过后会得到后端返回的token。我们需要将token存入vuex仓库,但是在界面刷新的时候,vuex里面的值会被清空,所以我们需要通过localStrage进行本地存储。这样就实现了token持久化存储。

    // 点击登陆按钮
    async submit() {
      await this.$refs.form.validate()
      console.log('校验通过')
      await this.$store.dispatch('user/updataTokenAsync', this.form)
      this.$router.push('/')
    }
    
   // 仓库中actions调用端口获取token
    async updataTokenAsync(context, data) {
      const token = await login(data)
      context.commit('updataToken', token)
    }
    
   // 仓库中mutations接收actions派发的token,然后将其存入state中
   updataToken(state, newToken) {
      state.token = newToken
      setToken(newToken)
   }

5、项目中是如何解决跨域的?

了解跨域,我们首先需要了解什么是同源。同源指请求URL域名、协议、端口均为相同。若其中一个不一样就会产生跨域问题。也就会导致我们无法请求到相应的数据。解决跨域的问题有:cors(浏览器判断跨域为简单请求时候,在响应头response Header中设置 Access-Control-Allow-Origin, 它表示我们的请求源,CORS服务端会将该字段作为跨源标志。)、nignx代理服务器、jsonP三种方式。在项目开发阶段,我们使用的是vue-cli代理解决跨域。而在项目上线阶段,我们使用的是nignx服务器代理解决跨域问题的。具体操作如下:

 // vue-cli解决跨域  
   // 1、 在vue.config.js里面的devServer添加proxy属性 
        proxy: { '/api': { target: '要代理的目标地址' } }
        
        这里的/api对应的是开发环境中.env.development文件里面的代码:
        # just a flag
        ENV = 'development'

        # base api
        VUE_APP_BASE_API = '/api'   // 这里是重点

    // 2、删除原有的before配置选项 
        before: require('./mock/mock-server.js') //模拟数据
        
    // 3、请求代码
        const instance = axios.create({
          baseURL: process.env.VUE_APP_BASE_API,
          timeout: 10000
        })
        
 // nignx
    这里不修改会导致进入界面后再次刷新出现404的错误出现
    location / {
        # root   html;
        # index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    // 解决跨域
    location /prod-api {
      proxy_pass https://heimahr-t.itheima.net;
    }

6、项目中权限验证如何实现?

项目中的权限验证我们是在前置守卫中去实现的。当我们访问登录页的时候,我们先从vuex中获取token,然后判断token是否存在,有token的话我们再判断去访问的地址是否是登录页,若是登录页的话我们直接放行。若不是的话我们再进行判断用户的信息数据是否存在,没有用户数据的话,我们从vuex的actions中调用获取用户信息的接口去获取数据。若有客户信息的话直接放行。若存在用户信息我们直接放行。 若token缺失,则判断需要去访问的界面路径是否存在白名单里面,存在=>放行。不存在 => 切换到登录页。

056a09b2fce580a562f63d7cc81ce83.png

拓展:什么是鉴权

  • 鉴权(authentication)是指验证用户是否拥有访问系统的权利。
  • 分类
    • HTTP Basic Authentication
    • session-cookie
    • Token验证(项目中使用的)
    • OAuth(开放授权)

7、项目中获取用户资料的位置及具体逻辑处理?

我们一般在前置守卫部分获取用户信息,在用户访问项目界面的时候,我们先判断是否存在token,若携带token且没有访问登录界面,此刻我们会判断用户ID是否存在,没有的话我们就会在vuex中的actions里面调用获取用户信息的接口接收用户信息并存入到仓库中。

const whiteList = ['/login', '404']
router.beforeEach(async(to, form, next) => {
  nProgress.start()// 开启进度条
  const token = store.getters.token
  if (token) {
    if (to.path === '/login') {
      next('/')
      nProgress.done()// 关闭进度条
    } else {
      if (!store.getters.userId) {
        const { roles } = await store.dispatch('user/updateUserInfo')  // 用户信息在此获取
        const filterRoutes = asyncRoutes.filter(item => {
          return roles.menus.includes(item.name)
        })
        router.addRoutes([...filterRoutes, { path: '*', redirect: '/404', hidden: true }])
        store.commit('user/setRoutes', [...constantRoutes, ...filterRoutes])
        next(to.path)
      } else {
        next()
      }
    }
  } else {
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next('/login')
      nProgress.done()// 关闭进度条
    }
  }
})

8、项目中如何处理token失效?

token是用户进入界面访问的令牌。在开发项目的时候我们会在拦截器中统一处理失效问题,当token失效的时候,后端会返回401业务码,我们在拦截器中获取401后,会将vuex和localStorage里面的token信息进行删除,并给用户token失效的提示信息同时将界面跳转至登录页。

if (error.response.status === 401) {
    store.dispatch('user/delTokenMsg')
    router.push('/login')
}

9、分析左侧菜单sideBar组件的渲染过程

1685622706775.png

  • 通过计算属性引入路由
  • 在slidebaritem组件中,通过v-if判断路由中定义的hidden属性是否存在,若不存在则渲染item
  • 遍历路由生成的sidebar-item
  • 渲染生成的图标和标题

10、组织架构模块中的树形结构是如何实现的?

属性结构使用的是elementui提供的el-tree组件,但是树形结构需要树形数据提供支持才能进行渲染,而我们从后端获取到的是列表数据,因为获取到的列表数据子部门的pid与上级部门的id是一致的,所以我们可以通过递归来将列表数据转换为树形数据。这样就完成了树形结构的渲染。

    export const translateListToTreeData = (list, rootVal) => {
      const arr = []
      list.forEach(item => {
        if (item.pid === rootVal) {
          const children = translateListToTreeData(list, item.id)
          if (children.length) item.children = children
          arr.push(item)
        }
      })
      return arr
    }

11、项目中是否封装过组件及实现的过程

  • 在这个项目开发中我们,为了便于代码的复用,我们会将使用频率高的代码封装为组件。我们封装的组件有:级联组件、弹窗组件、svg图片组件
  • 封装过程
    • 级联组件:新建一个存放级联组件的vue文件,在通过查阅elementui文档确定将Cascader级联选择器作为实现这个组件的主要结构;该组件有三个属性:options-绑定的数据;props-自定义绑定的数据属性;separator-自定义数据间隔的属性;使用步骤:从接口获取数据,将列表数据转为树形数据;此组件使用的是 value label,而后端返回的数据是id和name,所以需要通过props重置属性为:id和name;将数据赋值给treeData
// 级联组件封装
<el-cascader
  style="width: 100%"
  :options="treeData"
  :props="props"
  separator="-"
  size="mini"
  :value="value"
  @change="changeValue"
/>

methods: {
    async getDepartmentList() {
      const res = await departs()
      this.treeData = translateListToTreeData(res, 0)
    },
    // 改变部门的时候获取最后一个部门的ID
    changeValue(list) {
      if (list.length > 0) {
        this.$emit('input', list[list.length - 1])
        console.log(list[list.length - 1])
      } else {
        this.$emit('input', null)
      }
   }
}

12、角色管理模块,行内编辑是如何实现的?

角色编辑的话可以通过弹窗编辑以及行内编辑的方式实现,我们这个项目中使用的是行内编辑方式实现的。

  • 给每一项添加变量isEdit(是否进入编辑状态,默认不编辑)
  • 为了让点击编辑的时候能获取当前行的数据,需要加上插槽
  • 点击编辑将isEdit变为true,进入编辑状态
 <template>
    <el-input v-if="row.isEdit" >
    <span>{{row.name}}</span>
 </template>
  • 修改角色:此处为了对角色做出操作,也需要template插槽配合进行,但是当我们点击编辑按钮的时候,并没有出现想要的切换。原因在于在vue中动态添加的属性,不具备响应式的能力。(vue底层使用了Object.defineProperty属性)
// 给每个数据添加isEdit属性
  this.tableList.forEach(item => {
    this.$set(item, 'isEdit', false)
    // 给每行数据添加rowEdit缓存数据
    this.$set(item, 'rowEdit', {
      name: item.name,
      description: item.description,
      state: item.state
    })
  })
  • 行内数据缓存 #$set的作用 #如何缓存数据 #点击确定和取消按钮之后,做了什么?

      1. 初始化定义缓存数据,因为是动态添加,需要使用this.$set方法 image.png
    • 2、点击编辑对数据进行编辑的时候,但是经过取消操作后,再进行编辑的话,此时的数据应该与界面初始化的数据保持一致。然而,出现的还是编辑后的值,这个逻辑是不对的。所以我们需要在点击编辑的时候,将row里面的name、state和description赋值给rowEdit里面相对应的值。
    // 给每行数据添加rowEdit缓存数据
    this.$set(item, 'rowEdit', {
      name: item.name,
      description: item.description,
      state: item.state
    })

    再给对应的表单元素进行值的双向绑定 v-model="row.rowEdit.name/description/state"

    每次点击编辑的时候对rowEdit进行再赋值

    row.rowEdit = {
        name: row.name,
        state: row.state,
        description: row.description
     }
  • 确定编辑的角色 点击确定按钮的时候,判断row.rowEdit.name && row.rowEdit.description是否为空
点击事件() {
    if(row.rowEdit.name && row.rowEdit.description) {
        await updateRole({...row.rowEdit, id: row.id})
        
        row.name = row.rowEdit.name  //但是这样eslint工具会报错(误报)
        
        所以这样写:相同的,后者覆盖前者,不相同的话后者与前者合并
        
        Object.assign(row, {
            ...row.rowEdit,
            isEdit: false
        })
    }else{
        this.$message.warning('角色或描述不能为空')
    }
}

13、员工管理模块,说说左树右表的大致实现思路?

在实现左树结构的时候,我这边通过与后端沟通后确定子部门的pid与上级部门的id保持一致,这样的话前端就可以通过这一规律运用递归的方式进行左树结构效果的实现。

  • 左树

    • 列表数据=>树形数据

      • 我们使用到el-tree组件,但是这个组件接收的数据是树形数据,而后端返回的是列表数据,所以我们需要通过递归的方法将列表数据转换为树形数据。
    • 初始化顶级pid高亮显示

      • 首先初始化记录部门的id,然后配合elementui里面的setCurrentPage和node-key方法进行实现。因为里面涉及到异步操作,所以我们需要用到vue里面提供的$nextTick方法。
  • 右表

    • 通过el-table进行员工的信息渲染,通过el-pagination进行列表数据的相应切换。这里设计的用户图像的显示和聘用形式格式的转换。图像我用作用域插槽暴露每行数据,el-avatar组件配合实现,若获取不到员工的图像,这使用charAt()方法获取员工的姓加上相应的样式替代图片进行显示。
  • 左树右表的切换实现思路

    • 首先将当前的页码设置为1
    • 将当前行的员工id赋值给初始化记录部门的id,通过这个id获取相应的员工数据并进行渲染
996a343d2cd14c1cf8262400963a0b4.png

员工管理模块,左树和右表如何实现联动效果?

  • 给左侧的数据注册点击事件
  • 将当前页码设置为1
  • 通过点击事件可以获取到当前部门的ID,并将当前部门的id赋值给queryParams.departmentId
  • 重新渲染列表数据

14、RBAC的思想是什么?

RBAC是Role-Based Access Control四个单词的缩写:意思是基于角色的访问控制。具体理解为:员工拥有角色,给员工分配权限;角色拥有权限,给角色分配权限。 RBAC思想的好处:假设公司来了员工,他有对应的3个权限,若是运用RBAC思想,你只需要分配一个角色即可,否则你需要分配3个权限。所以RBAC思想使得访问管理变得更容易。

15、员工管理模块,针对模糊搜索是如何进行优化的?

当用户进行搜索的时候会实时向后端接口请求数据,这样对性能的提升是不友好的,我们可以使用防抖的方式进行相应的处理,(所谓防抖就是触发事件n秒后执行事件,若在此时间内你触发事件,那么就会重新计算函数的执行时间),这里使用延时器进行处理。

cf8ae823dc822d63bfd1af05d879f05.png
  • 补充:
    • change和input两个事件的区别:

      • change:仅在输入框失去焦点或用户按下回车时触发
      • input:在input值改变的时候触发
    • 为何不在data里面设置timer,而是用this.timer data里面的数据都是响应式的,这里的timer只需要接收延时器的id,然后每次调用接口之前只需要对这个做清除处理即可。这样做能提升性能。

    • 防抖(debounce)和节流(throttle)的区别

      • 防抖:就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
      • 节流:指连续触发事件但是在 n 秒中只执行一次函数
    • 防抖和节流的应用场景

      • 防抖:1、用户在不断输入值时,用防抖来节约请求资源。2、- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
      • 节流:1、鼠标不断点击触发,mousedown(单位时间内只触发一次);2、监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

16、员工管理模块,员工导出excel功能的实现思路?

  • 封装导出员工excel的接口,这里需要注意添加接收数据的类型 d615c9ef6c27731a5e9743458dc2461.png
  • 处理报错:因为这个接口返回的数据不包含message,所以需要对拦截器的对应代码做处理。通过instanceof判断返回的数据是否为Blob的实例,如果是的话则直接返回response.data fafe1b2d61324527a2c99e470f4cfe7.png
  • 通过点击事件导出员工数据,这里需要FileSaver插件将数据生成表格形式并下载 3d3d8104fce4957e65ac83539c22bfd.png

17、员工头像组价的封装实现思路?

这里用到了elementui提供的插槽再配合v-if对图像的路径是否存在进行判断,有的话直接展示图像,没有的话显示人名的姓氏,这个涉及到获取第一个字符串的方法charAt(),它默认获取第一个字符串,所以不写0也可以。

image.png

18、新增/编辑员工共用同一个组件, 如何区分并实现对应的功能?

我们项目在给新增/编辑按钮设置同一个事件名,但是里面通过传递不一样参数进行区分。比如编辑的时候参数为1、新增的时候参数为;2、当用户触发事件的时候,代码会对所传的参数做出判断。

19、vue项目中有使用echarts图表么, 怎么引入的?以及如何优化呢?

有使用的,我们用echarts图表对项目中的数据进行可视化的一个展示,为了提升性能,我们在想用对echarts进行按需引入;具体引入的模块包括:核心包、图表的类型(比如柱状图、折线图)等;引入提示框、标题和坐标系;最后引入渲染器。然后我们通过ecahrts的use方法对引入的模块进行使用。

    // 优化前
    import * as echarts from 'echarts'
    
    //优化后
    import * as echarts from 'echarts/core'   // 核心 包
    import { LineChart } from 'echarts/charts'  // 图表 - 柱状/折线
    import { GridComponent } from 'echarts/components' // 引入提示框,标题,坐标系
    import { CanvasRenderer } from 'echarts/renderers'  //引入渲染器
    
    // 使用
    echarts.use([
        LineChart,
        GridComponent,
        CanvasRenderer
    ])

20、项目中有封装过自定义指令么, 如何实现?

首先我们需要在main.js中使用vue的directive方法封装自定义指令v-permission来判断Vuex中的points是否有对应的标识,有的话对应的按钮正常显示,没有的话删除或者禁用按钮。

// 编写指令代码
Vue.directive('permission', {
    inserted(el, binding){
        const points = store.state.user.userInfo?.roles?.points || []
        if(!points.includes(binding.value)) {
            el.remove()
            //el.disabled = true
        }
    }
})

// 应用指令
<el-button v-permission="'add-employee'">添加角色</el-button>

21/22、如何根据权限筛选出动态路由?动态添加路由是如何实现的?

动态路由的添加,我们是放在前置守卫里面处理的。我们首先从后端获取包含用户访问权限的menus数据。然后导入全部的动态路由信息,通过数组的filter和include方法进行配合筛选出用户能访问的路由信息。接着运用路由的addRoutes方法将其添加到路由里面去。但这有2个问题:1、404需要放入路由的最末端。2、在添加完路由信息之后,需要通过next(to.path)来解决动态路由不显示的问题。

if (token) {
    if (to.path === '/login') {
      next('/')
      nProgress.done()// 关闭进度条
    } else {
      if (!store.getters.userId) {
        const { roles } = await store.dispatch('user/updateUserInfo')
        const filterRoutes = asyncRoutes.filter(item => {
          return roles.menus.includes(item.name)
        })
        router.addRoutes([...filterRoutes, { path: '*', redirect: '/404', hidden: true }])
        next(to.path)   //添加动态路由之后,需要转发一下
      } else {
        next()
      }
    }
  } else {
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next('/login')
      nProgress.done()// 关闭进度条
    }
  }

23、项目中按钮权限标识如何分配?

我们项目中按钮权限分为访问权限和操作权限。

  • 访问权限:我们项目中的访问呢权限分配是基于RBAC思想,即给角色分配权限,给员工分配角色。我们这个项目采用的是菜单权限控制。具体做法是:首先我们将路由拆分为静态路由和动态路由,静态路由是在没有权限的情况下都能访问。动态路由只能在员工拥有相应的权限才能访问。我们在获取每个员工访问权限的时候是在前置守卫处理的。后端在返回用户信息的时候包含访问权限数据menu。然后我们会将返回的权限运用数组的方法filter和includes从项目中所有的动态路由中进行过滤筛选,筛选后的路由信息就是用户拥有的路由访问权限。在获取访问权限后通过路由的addRoutes方法将其添加到项目的路由中去。因为404路由信息位于静态路由里面,而且他的路径匹配的是所有路由,所以我们需要在添加动态路由信息的时候将404路由信息添加至末尾。避免访问界面出现404的情况。再一个就是动态添加路由后会产生一个问题界面出现白屏。我们可以通过next(to.path)进行解决。

if (token) {
    if (to.path === '/login') {
      next('/')
      nProgress.done()// 关闭进度条
    } else {
      if (!store.getters.userId) {
        const { roles } = await store.dispatch('user/updateUserInfo')
        const filterRoutes = asyncRoutes.filter(item => {
          return roles.menus.includes(item.name)
        })
        router.addRoutes([...filterRoutes, { path: '*', redirect: '/404', hidden: true }])
        next(to.path)   //添加动态路由之后,需要转发一下
      } else {
        next()
      }
    }
  } else {
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next('/login')
      nProgress.done()// 关闭进度条
    }
  }

  • 操作权限:项目中的操作权限指的拥有操作权限的角色才有对应的操作权限。考虑到操作权限的需求很多组件都使用到,这一功能的实现我们是通过基于编写一个全局的自定义组件进行实现的。我们在项目的入口文件main.js中运用vue提供的directive方法编写一个名为permission的自定义组件,在自定义组件里我们从后端获取含有当前操作权限的数据point,通过判断绑定的按钮里面的标识在返回中的数据里面是否存在来确定当前角色是否拥有相应的权限。
// 自定义指令
Vue.directive('permission', {
  inserted(el, binding) {
    const points = store.state.user.userInfo?.roles?.points || []
    if (!points.includes(binding.value)) {
      el.remove()
    }
  }
})

// 在项目中的使用
<el-button v-permission="'add-perssion'">添加角色</el-button>

24、项目中有封装过指令吗?如何实现的。

有封装过的,我们项目的权限实现是根据RBAC思想进行的,即给角色分配权限,给员工分配角色。我们项目中有个按钮权限的需求,通过给按钮添加相应的标识,然后我们在main.js里面编写一个全局自定义指令,通过在自定义指令里面获取当前员工包含操作权限的数据point与操作按钮的标识进行对比若返回true即说明此员工有操作权限,否则没有。

// 自定义指令
Vue.directive('permission', {
  inserted(el, binding) {
    const points = store.state.user.userInfo?.roles?.points || []
    if (!points.includes(binding.value)) {
      el.remove()
    }
  }
})

// 在项目中的使用
<el-button v-permission="'add-perssion'">添加角色</el-button>

25、vue项目中有使用echarts图表么, 怎么引入的?以及如何优化呢?

我们项目中涉及到可视化数据的展示,所以我们用到了echarts图表。开始我们通过全局引入的方式进行图表渲染,但是在项目开发完毕后我们通过打包预览发现全局引用导致了效能的很大消耗。所以我们使用了按需导入的方式进行图表的渲染。即:使用哪个模块就引入哪个模块。这使得项目的性能得到了很大的提升。

    // 优化前
    import * as echarts from 'echarts'
    
    //优化后
    import * as echarts from 'echarts/core'   // 核心 包
    import { LineChart } from 'echarts/charts'  // 图表 - 柱状/折线
    import { GridComponent } from 'echarts/components' // 引入提示框,标题,坐标系
    import { CanvasRenderer } from 'echarts/renderers'  //引入渲染器
    
    // 使用
    echarts.use([
        LineChart,
        GridComponent,
        CanvasRenderer
    ])

26、请说说vue的两种路由模式及区别?

const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  mode: 'history',
  routes: constantRoutes
})

路由的模式有hash、history、abstrat模式。

  • hash:带#,#后面的地址变化不会引起页面的刷新,因为它兼容所有的浏览器和服务器,是最安全的模式。hash值变化浏览器不会重新发起请求,hash 通过监听浏览器 onhashchange 事件变化,查找对应路由应用。通过改变 location.hash 改变页面路由。

    • 补充:在传统的hash模式中(http://localhost:8080#home),即使不需要配置,静态服务器始终会去寻找index.html并返回给我们,然后vue-router会获取#后面的字符作为参数,对前端页面进行变换。
  • history:没有#,地址变化会引起页面刷新,更符合页面地址的规范(开发环境不刷新—webpack配置),它包括back/forward/go三个方法,对应浏览器的前进、后退、跳转操作。原理是:利用 html5 的history Interface 中新增的 pushState() 和 replaceState() 方法,改变页面路径。

  • 之所以使用history而不使用hash是因为带#号的url不适合宣传和推广

    • 在这个项目中,当我们将打包好的文件放入到服务器后,打开链接出现了404报错,这个时候我们需要在nginx中的配置文件修改相关的配置,因为在开发环境中我们使用了webpack进行了跨域问题的解决,但是项目打包后没有webpack代码,所以我们需要在nginx中添加配置文件解决的跨域问题。
    • 补充:我们用nginx部署项目,然后在地址栏输入http://localhost:8080(这里配置的端口是8080),你会发现地址栏之后会变为http://localhost:8080/home,并且看起来一切正常,似乎路由也可以正常切换而不会发生其他问题。看起来好像不需要按官网告诉我们的那样配置后端也能实现history模式,但如果你直接在地址栏输入http://localhost:8080/home,你会发现你获得了一个404页面。
php
复制代码
    // 在nginx中的配置文件nginx.conf处理
    
    // 解决404报错问题
    location / {
        # root   html;
        # index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    
    // 解决跨域问题
    location /prod-api {
      proxy_pass https://heimahr-t.itheima.net;
    }

复制代码
    history.go(-2);//后退两次 
    history.go(2);//前进两次 
    history.back(); //后退
    hsitory.forward(); //前进
  • abstrat:在已存在的路由页面中内嵌其他的路由页面,而保持在浏览器当中依旧显示当前页面的路由path,这就利用到了abstract这种与浏览器分离的路由模式。

27、说说你的项目上线前的准备工作有哪些?

项目部署:手动部和自动部署 我们打包上线的话通过运维工程师手动部署的。打包上线我们首先需要通过npm run preview进行打包预览;echarts的全局导入变成按需导入;CDN加速,将项目中的elementui文件通过外部cnd引入的方式导入到项目中。通过nignx代理服务器解决跨域问题。完成后,我们会交给运维进行自动部署。

28、聊聊打包上线的过程?

在对项目进行打包前,我们需要通过命令npm run preiew对项目进行打包分析。通过打开的界面可以看到代码中需要优化的模块,,比如可以去除mock.js的引用,将项目中的ui库用cdn引入等等。尽可能地优化项目中影响加载的因素。优化后,前端工程师先通过npm build打包html css js到文件夹dist里面,后端工程师将dist文件夹里面的代码放到nignx代理服务器里面进行部署。然后公众通过地址进行访问。

29、项目中做了哪些优化(围绕代码角度)

首屏加载、业务代码、上线角度 路由懒加载通过import导入路由的方式进行实现的;keep-alive组件的使用;字体图标的使用;CDN加速;封装组件;防抖;骨架屏;

30、说说你对vue响应式原理的理解?

  • vue2的具体实现
    • vue使用的是MVVM的软件设计模式,是一种简化用户界面的事件驱动编程方式。M表示数据层,V表示视图层,VM表示视图模型。其中,视图模型是MVVM架构的核心,它是连接view和model的桥梁。MVVM实现了view和model的自动同步,当model属性发生改变的时候,视图会自动更新。反之亦然。我们称之为数据的双向绑定。
    • 双向数据绑是如何实现的?
      • vue2的双向数据绑定我们会用到v-model;v-model本质上是语法糖,在使用v-model后既绑定了数据,又添加了一个@input事件监听;等价于<input :bind='search' @input='search = $event.target.value'>即:v-bind绑定响应式数据;触发input事件并传递数据(核心和重点)
      • 其底层原理就是(双向数据绑定原理): 1、 一方面model层通过Object.defineProperty()来劫持每个属性,一旦监听到变化通知相关的页面元素进行更新。 2、 另一方面通过编译模板文件,为控件的v-model绑定input事件,从而页面输入能实时更新相关data属性值。
    • 响应式:Vue2的响应式是数据劫持结合观察者模式实现的。
  • vue2的问题
  • vue3的具体实现/优势

30.产品的开发流程

  • 产品经理会编写产品设计稿
  • 召集团队人员,进行产品研讨会
  • 分析产品的需求 比如xx功能,能否实现
  • 工期排期说明
  • 产品开始阶段
  • UI会根据产品设计稿 - 出psd图
  • 前端会根据psd图 - 写/画页面
  • 后端会开始编写接口,生成文档
  • 前端和后端的联调 - 遇到了问题,该怎么解决,状态码....
  • 加班 - 写代码....
  • 找测试 - 找bug
  • 修复bug
  • 产品的上线 - 开发环境 测试环境 - 生产环境
  • 项目的迭代优化