likes
comments
collection
share

Vue3技术栈,开发CMS系统-复盘不一样的解决方案

作者站长头像
站长
· 阅读数 50

我正在参加「掘金·启航计划」

写在前边

随着Vue3版本不断升级和优化,选择使用Vue3开发新项目越来越多,Vue官网文档越来越完善,但是在实际开发中,还是会有不同的需求和问题,结合在工作中遇到的问题以及需求,分享一些技术方案~~

技术选型版本:Vue3:3.2.25 / vite: 2.5.10 / webpackbar: 5.0.2

一、Weppack打包工具替换Vite

创建Vue 应用,Vue3 官方的项目脚手架工具create-vue,生成的基础模板是基于Vite打包。

vite.config.js配置项:

// vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import styleImport from 'vite-plugin-style-import'
import alias from "@rollup/plugin-alias";
// 中文包
import path from "path"
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    alias(),
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
      // 自动导入composition api
      imports: ["vue", "vue-router"]
    }),
    // 初次启动批量导入会出现 ✨ dependencies updated, reloading page
    Components({
      resolvers: [ElementPlusResolver()]
    }),
    styleImport({
      libs: [{
        libraryName: 'element-plus',
        esModule: true,
        resolveStyle: (name) => {
          return `element-plus/theme-chalk/${name}.css`
        }
      }]
    })
  ],
  build:{
    minify: "terser",
    chunkSizeWarningLimit: 1500
  },
  
  server: {
    host: '0.0.0.0'
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src")
    },
    extensions: ['.js']
  },
  css: {
    preprocessorOptions: {
      scss: {
        charset: false,
        additionalData: `@import "@/style/variables.scss";`
      }
    },
    postcss: {
      plugins: [
        {
          postcssPlugin: 'internal:charset-removal',
          AtRule: {
            charset: (atRule) => {
              if (atRule.name === 'charset') {
                atRule.remove()
              }
            }
          }
        }
      ]
    }
  }
})

项目在线上环境或者打包过程中,有时会出现打包错误:

问题1: Missing ref owner context. ref cannot be used on hoisted vnodes. A vnode with ref must be created inside the render function.

Vue3技术栈,开发CMS系统-复盘不一样的解决方案

About Missing ref owner contextMissing ref owner context #3930 github issue

解决:在vite.cofig加个配置参数

resolve: {     dedupe: ['vue'] }

问题2: 关于Vite rollup部署 import “/@/XXX” form “XXX“问题

因为rollup本身不具备路径解析能力

解决:

安装依赖  
`yarn add @rollup/plugin-alias`

vite.config.ts
import alias from "@rollup/plugin-alias";

export default defineConfig({
  plugins: [alias(),vue()],
  resolve: {
    alias: {
      "/@": path.resolve(__dirname, "./src"),
    },
  },
})

方案:退而求其次的方案,在生产中,使用Weppack打包工具替换Vite:

使用Vue CLI开发、改造(提示:Vue CLI 现已处于维护模式!)现在官方推荐使用 create-vue 来创建基于 Vite 的新项目。 另外请参考 Vue 3 工具链指南 以了解最新的工具推荐。

vue.config.js 的完整配置

const path = require('path');
const HotHashWebpackPlugin = require('hot-hash-webpack-plugin');
const WebpackBar = require('webpackbar');
const resolve = (dir) => path.join(__dirname, '.', dir);

module.exports = {
    productionSourceMap: false,
    publicPath: './',
    outputDir: 'dist',
    assetsDir: 'assets',
    devServer: {
        port: 9999,
        host: '0.0.0.0',
        https: false,
        open: true
    },

    chainWebpack: (config) => {
        const types = ['vue-modules', 'vue', 'normal-modules', 'normal'];
        types.forEach(type => {
            let rule = config.module.rule('less').oneOf(type)
            rule.use('style-resource')
                .loader('style-resources-loader')
                .options({
                    patterns: [path.resolve(__dirname, './lessVariates.less')]
                });
        });

        config.resolve.alias
            .set('@', resolve('src')) 
            .set('api', resolve('src/apis'))
            .set('common', resolve('src/common'))

        config.module.rule('images').use('url-loader')
            .tap(options => ({
                name: './assets/images/[name].[ext]',
                quality: 85,
                limit: 0,
                esModule: false,
            }));

        config.module.rule('svg')
            .test(/\.svg$/)
            .include.add(resolve('src/svg'))
            .end()
            .use('svg-sprite-loader')
            .loader('svg-sprite-loader');

        config.plugin('define').tap(args => [{
            ...args, 
            "window.isDefine": JSON.stringify(true)
        }]);

        // 生产环境配置
        if (process.env.NODE_ENV === 'production') {
            config.output.filename('./js/[name].[chunkhash:8].js');
            config.output.chunkFilename('./js/[name].[chunkhash:8].js');
            config.plugin('extract-css').tap(args => [{
                filename: 'css/[name].[contenthash:8].css',
                chunkFilename: 'css/[name].[contenthash:8].css'
            }]);
            config.plugin('hotHash').use(HotHashWebpackPlugin, [{ version : '1.0.0'}]);
            config.plugin('webpackBar').use(WebpackBar);

            config.optimization.minimize(true)
                .minimizer('terser')
                .tap(args => {
                    let { terserOptions } = args[0];
                    terserOptions.compress.drop_console = true;
                    terserOptions.compress.drop_debugger = true;
                    return args
                });
            config.optimization.splitChunks({
                cacheGroups: {
                    common: {
                        name: 'common',
                        chunks: 'all',
                        minSize: 1,
                        minChunks: 2,
                        priority: 1
                    },
                    vendor: {
                        name: 'chunk-libs',
                        chunks: 'all',
                        test: /[\\/]node_modules[\\/]/,
                        priority: 10
                    }
                }
            });
        }
    }
};

参考文档:

Vue CLI

二、动态路由权限控制

开发后端管理系统,不同的用户,不同的角色,需要不同的权限,动态路由就派上用场了

方案1:根据后台返回菜单数据,动态生成符合路由规则的路由

方案2:根据现有路由过滤后台返回的菜单数据,生成符合路由规则的路由(推荐)

核心逻辑在动态菜单: store/modules/menu.js

import router from "@/router";
import Layout from '@/layout/index.vue'
function filterRouter(menuList) {
  let filterRouterlist = [];
  menuList.filter((route) => {
    // 通过menuList生成每个路由器项目
    const itemFromReqRouter = getRouteItemFromReqRouter(route)
    if (route.children?.length) {
      itemFromReqRouter.children = filterRouter(route.children)
    }
    filterRouterlist.push(itemFromReqRouter)
  });
  return filterRouterlist;
}

const getRouteItemFromReqRouter = (route) => {
  const tmp = { meta: {} }
  const routeKeyArr = ['path', 'component', 'redirect', 'alwaysShow', 'name', 'hidden']
  const metaKeyArr = ['title', 'activeMenu', 'elSvgIcon', 'icon']
  // @ts-ignore
  // const modules = import.meta.glob('../views/**/**.vue')
  /*
  generator routeKey
  */
  routeKeyArr.forEach((fItem) => {
    if (fItem === 'component') {
      if (route[fItem] === 'Layout') {
        tmp[fItem] = Layout
      }
    } else if (fItem === 'path') {
      tmp[fItem] = `${route.url}`
    } else if (['hidden', 'alwaysShow'].includes(fItem)) {
      tmp[fItem] = !!route[fItem]
    } else if (route[fItem]) {
      tmp[fItem] = route[fItem]
    }
  })
  /*
  generator metaKey
  */
  metaKeyArr.forEach((fItem) => {
    if (route[fItem]) tmp.meta[fItem] = route[fItem]
  })
  /*
  route extra insert
  */
  if (route.extra) {
    Object.entries(route.extra.parse(route.extra)).forEach(([key, value]) => {
      if (key === 'meta') {
        tmp.meta[key] = value
      } else {
        tmp[key] = value
      }
    })
  }
  return tmp
}

export const menu = {
  namespaced: true,
  state: {
    menuList: []
  },
  getters: {
    getMenus: (state) => {
      return state.menuList;
    },
  },
  mutations: {
    setMenus(state, systemMenu) {
      state.menuList = systemMenu
    }
  },
  actions: {
    getSysMenus({ commit }, menuList) {
      let accessedRoutes = []
      accessedRoutes = filterRouter(menuList)
        accessedRoutes.forEach((route) => {
          router.addRoute(route)
        })
        commit("setMenus", accessedRoutes);
        return Promise.resolve(accessedRoutes)
    }
  }
}

需要获取路由数据的页面:

menulists.value = store.getters["menu/getMenus"]

参考项目:vue3-admin-plus

三、拖拽组件的需求以及封装

在项目开发中,有些table需要有拖拽功能,需要封装一些组件来满足需求

比如:拖拽表格组件封装--el-table实现行拖拽效果

方案:可以使用第三方组件:vuedraggable,基于Sortable.js的vue组件,用以实现拖拽功能。

//引入插件 vuedraggable

npm install vuedraggable

//Sortable 页面中使用

import Sortable from 'sortablejs';


<template>
  <div class="page_eight">
    <el-table :data="tableData" :row-key="(row) => row.id" :row-class-name="({row}) => row.id">
      <el-table-column prop="id" label="id"></el-table-column>
      <el-table-column prop="name" label="姓名"></el-table-column>
      <el-table-column prop="address" label="地址"></el-table-column>
      <el-table-column prop="date" label="日期"></el-table-column>
    </el-table>
  </div>
</template>
<script>
import { defineComponent, onMounted, reactive, toRefs } from "vue";
import Sortable from "sortablejs";
export default defineComponent({
  name: "pageSeven",
  components: {},
  setup() {
    const state = reactive({
      tableData: [
        {
          id: "1",
          date: "2016-05-02",
          name: "王小虎1",
          address: "上海市普陀区金沙江路 100 弄",
        },
        {
          id: "2",
          date: "2016-05-04",
          name: "王小虎2",
          address: "上海市普陀区金沙江路 200 弄",
        }
      ],
      videoListarr:[]
    });
    // 行拖拽
    function rowDrop() {
      const tbody = document.querySelector( ".el-table__body-wrapper tbody" );
      Sortable.create(tbody, {
        animation: 180,
        delay: 0,
        // 结束拖拽后的回调函数
        onEnd: (evt) => {
          console.log("evt-to.newIndex",evt.newIndex,"old-index",evt.oldIndex)
          const currentRow = state.tableData.splice(evt.oldIndex, 1)[0];
          state.tableData.splice(evt.newIndex, 0, currentRow);
          if (evt.newIndex !== evt.oldIndex) {
            state.videoListarr = []
               
                Array.from(evt.to.rows).forEach(item => {
                  console.log(item.className.split(' ')[1],evt.newIndex)
                  state.videoListarr.push(item.className.split(' ')[1])
                })
                 
              }
             console.log("videoListarr----id",state.videoListarr[evt.newIndex])
        },
        onSort: (evt) => {
          // console.log("evt-onSort",evt.to.rows)
        }
      });
    }
    onMounted(() => {
      rowDrop();
    });
    return {
      ...toRefs(state),
      rowDrop,
    };
  },
});
</script>
<style lang="scss">
.page_eight {
  width: 100%;
  height: 100%;
}
</style>

根据业务以及后端接口需要修改传参以及接受参数数据:

//实际开发中,页面中核心代码:
function rowDrop() {
  const tbody = document.querySelector(".el-table__body-wrapper tbody");
  Sortable.create(tbody, {
    animation: 180,
    delay: 0,
    // 结束拖拽后的回调函数
    onEnd: (evt) => {
      EndIndex.value = evt.newIndex;
      const currentRow = tableDatavalue.value.splice(evt.oldIndex, 1)[0];
      tableDatavalue.value.splice(evt.newIndex, 0, currentRow);
      const tableDatavalueNew = tableDatavalue.value;
      setDate(StartIndex.value, EndIndex.value, tableDatavalueNew);
    },
    onStart: (evt) => {
      StartIndex.value = evt.oldIndex;
    },
  });
}
const setDate = (StartIndex, EndIndex, tableDatavalueNew) => {
  if (EndIndex > StartIndex) {
    const newArr = tableDatavalueNew.slice(StartIndex, EndIndex + 1);
    const newSortArr = sortList.value.slice(StartIndex, EndIndex + 1);
    for (let i = 0; i < newArr.length; i++) {
      var paramsArr = [];
      for (let i = 0; i < newSortArr.length; i++) {
        paramsArr.push({
          id: newArr[i].id,
          sort: newSortArr[i],
        });
      }
    }
  } else {
    const newArr = tableDatavalueNew.slice(EndIndex, StartIndex + 1);
    const newSortArr = sortList.value.slice(EndIndex, StartIndex + 1);
    for (let i = 0; i < newArr.length; i++) {
      var paramsArr = [];
      for (let i = 0; i < newSortArr.length; i++) {
        paramsArr.push({
          id: newArr[i].id,
          sort: newSortArr[i],
        });
      }
    }
  }
  api.updateSortAdvertise(paramsArr).then((res) => {
    if (res.code === "200") {
      ElMessage({
        type: "success",
        message: "排序成功",
      });
      getSubjectList();
    } else {
      ElMessage({
        type: "error",
        message: "排序失敗",
      });
    }
  });
};

参考文章:

vue+element-pus实现表格拖拽功能

el-table实现行拖拽效果(Vue3.0)

Github vue.draggable.next

四、权限按钮解决方案

针对后台管理系统,需求涉及到用户权限细分到按钮权限,

方案:前端可以使用自定义指令,来判断:

app.directive('pButton',{
  mounted: function (el, binding) {
  let menuList = JSON.parse(localStorage.getItem("HomeMenulist")) || [];
  let buttonList = []
  for (let i = 0; i < menuList.length; i++) {
    let outerChildren = menuList[i].children;
    let len = outerChildren.length;
    for (let j = 0; j < len; j++) {
      let innerChildren = outerChildren[j].children;
      // menuList[i].children.push(...innerChildren);
      buttonList.push(...innerChildren);
    }
  }
  if (buttonList.findIndex(item => item.perms === binding.value) === -1) {
      el.parentNode.removeChild(el)
    }
  }
 })

页面中使用:

 <el-button class="allBut" type="primary" v-pButton="'videoCourse:update'" @click="handleEdit(scope.row)">编辑</el-button>

五、 element UI中对table组件的二次封装

在我们的日常开发中,尤其的后台管理系统,大量使用到element ui的table组件,出于能少写一行代码绝不多写一个字母的程序员来说,对常用的组件进行二次封装是非常必要的

<template>
  <div>
    <div class="table-content">
      <el-table
        :data="tableDatalist"
        :row-key="(row) => row.id"
        :row-class-name="({ row }) => row.id"
        border
        fix
        style="width: 100%"
        size="large"
        :header-cell-style="{ background: '#F3F3F3', color: '#999' }">
        <el-table-column v-for="item in tableOptions" :key="item.id" :prop="item.prop" :label="item.label" :min-width="item.minWidth" align="center">
          <!-- 特殊列扩展 -->
          <template #default="scope">
            <slot v-if="item.slot" :name="item.slot" :scope="scope"></slot>
            <span v-else>{{ scope.row[item.prop] }}</span>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination">
        <el-pagination
          :page-size="pageSize"
          :page-sizes="[10, 20, 30, 40, 50, 60]"
          :small="small"
          v-if="tableDatalist.length"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          align="center" />
      </div>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  tableDatalist: {
    type: Array,
    default: () => [],
  },
  tableOptions: {
    type: Array,
    default: () => [],
  },
  pageSize: {
    typeof: Number,
    default: 10,
  },
  total: {
    typeof: Number,
    default: 0,
  },
  small: {
    type: Boolean,
    default: true,
  },
});

// 翻页
let handleSizeChange = (val) => {
  emits("handleSizeChange", val);
};

let handleCurrentChange = (val) => {
  emits("handleCurrentChange", val);
};

// 分发事件
let emits = defineEmits(["handleSizeChange", "handleCurrentChange"]);
</script>

页面中使用:

<TableComon
        :tableDatalist="tableData"
        :tableOptions="columns"
        :total="total"
        :pageSize="dataSearch.pageSize"
        @handleSizeChange="handleSizeChange"
        @handleCurrentChange="handleCurrentChange">
        <!-- 章节数量 -->
        <template #count="{ scope }">
          共{{ scope.row.chapterNum }}大章{{ scope.row.sectionNum }}小节
        </template>
        <!-- 操作热门 -->
        <template #hot="{ scope }">
          <el-switch :active-value="1" :inactive-value="0" v-model="scope.row.hot" v-pButton="'videoCourse:publish'" @click="changeHotHandle(scope.row, 'hot')" />
        </template>
        <!-- 操作状态 -->
        <template #status="{ scope }">
          <el-switch :active-value="1" :inactive-value="0" v-model="scope.row.publish" v-pButton="'videoCourse:publish'" @click="changestatusHandle(scope.row)" />
        </template>
        <!-- 操作项 -->
        <template #action="{ scope }">
          <div style="text-align: left">
            <el-button class="allBut" type="primary" v-pButton="'videoCourse:update'" @click="handleEdit(scope.row)">编辑</el-button>
            <el-button class="allBut" type="primary" @click="handleGoPage(scope.row)">目录</el-button>
            <el-button class="allBut" type="danger" v-if="scope.row.publish === 0" @click="handleDelete(scope)">删除</el-button>
          </div>
        </template>
      </TableComon>
      
js部分:
      
const tableData = ref([]);
const columns = ref([
  { prop: "name", label: "课程名称", minWidth: 120 },
  { prop: "chapterNum", label: "章节数量", minWidth: 80, slot: "count", align: "center" },
  { prop: "creatorName", label: "创建人", minWidth: 120 },
  { prop: "createTime", label: "创建时间", minWidth: 120 },
  { prop: "hot", label: "热门", minWidth: 80, slot: "hot", align: "center" },
  { prop: "status", label: "发布状态", minWidth: 80, slot: "status", align: "center" },
  { label: "操作", align: "center", slot: "action", minWidth: "120px" },
]);

参考课程:Vue3.0+TS打造企业级组件库

六、其他问题

1、在使用wangeditor富文本编辑器表单校验,不为空

解决:

//editor为空会获取 <p><br/></p>使表单为空校验不通过,需要清空
function withEmptyHtml(editor) {
  const { getHtml } = editor;
  const newEditor = editor;
  newEditor.getHtml = () => {
    if (newEditor.isEmpty()) {
      return "";
    }
    return getHtml();
  };

  return newEditor;
}
Boot.registerPlugin(withEmptyHtml);

写在后边

技术在升级,技术方案也会升级,文章部分代码有更新和不妥之处,请提出宝贵意见。彼此分享,彼此成长~~

欢迎交流

转载自:https://juejin.cn/post/7248888621364985915
评论
请登录