vue3.0项目实战 - ElementUI框架版

654 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

 项目实战

1.Vue运行环境安装,为什么需要安装运行环境

预装环境 :node v8+  (Vue3需要node 版本8以上)

为什么需要运行环境?   VUE项目==文件类型是.vue他是需要被编译成.js文件,才可以被浏览器识别 安装复杂度 

安装node环境 下载 | Node.js 中文网  nodejs.cn/download/

查看node版本: node –v  

2.npm镜像安装

3.VueCli脚手架安装

4.Vue-Cli基础

Vue CLI 和 Vue的区别

脚手架是一个基于 Vue.js 进行快速开发的完整系统,通过@vue/cli 实现快速搭建标准化项目的脚手架  

Vue的版本和VueCLI的版本的关系

Vue版本不受脚手架版本的影响 使用VueCLI构建项目过程,可以根据需求选择相应版本的Vue

查看VueCLI版本号:  vue -v

5.使用Vue-Cli构建Vue3项目

详见博主文章

同一台电脑 实现 vue-cli2和vue-cli3同时并存 及 常见命令 未完待续。。。_BMG-Princess的博客-CSDN博客

​编辑

6.Vue3与Vue2 main.js对比

​编辑

​编辑

​编辑  

7.Vue3生命周期变化  

与 2.x 版本生命周期相对应的组合式 API

1.beforeCreate -> 使用 setup()

2.created -> 使用 setup()

3.beforeMount -> onBeforeMount

4.mounted -> onMounted

5.beforeUpdate -> onBeforeUpdate

6.updated -> onUpdated

7.beforeDestroy -> onBeforeUnmount

8.destroyed -> onUnmounted

9.errorCaptured -> onErrorCaptured

onRenderTracked  检查依赖被追踪。当render函数被调用时,会检查哪个响应式数据被收集依赖 onRenderTriggered 当执行update操作时,会检查哪个响应式数据导致组件重新渲染。

在这里插入图片描述​编辑  在这里插入图片描述​编辑

8.ref,reactive响应式api

reactive与ref

被响应式api标记过的数据才可以成为响应式数据

ref--用来标记简单类型数据

reactive—标记复杂类型数据(深度响应式)

如果用ref对象/数组, 会自动将对象/数组转换为reactive的代理对象

ref的数据操作: 在js中要.value, 在模板中不需要(内部解析会自动添加.value)

​编辑

9.引入http请求框架axios +封装

​编辑

http.js说明

export function 可导出多个 ;此例只导出post;;后续补充get...

import axios from 'axios'
// import router from '@/router'
// import qs from "qs"
import { ElMessage } from 'element-plus'

//响应拦截器
axios.interceptors.response.use(
  response => {
    return response
  },
  error => {
    if (error.response) {
      switch (error.response.status) {
        // 401: 未登录
        // 只有在当前路由不是登录页面才跳转
        case 401:
          router.replace({
            path: '/',
          });
          break;
          // 403 token过期
          // 清除本地token
          // 跳转登录页面
        case 403:
          ElMessage.error('登录过期,请重新登录')
          router.replace({
            path: '/',
          });
          break;
          // 404请求不存在
        case 404:
          ElMessage.error('网络请求不存在')
          // router.replace({
          //   path: '/',
          // });
          break;
          // 其他错误,直接抛出错误提示
        default:
          ElMessage.error(error.response.data.message)
      }
    }
  })
//post
export function post(url, params) {
      return new Promise((resolve, reject) => {
        axios({
            url: url,
            method: 'post',
            data: params,
            timeout: 1000 * 60,
            headers: {
              'Content-Type': 'application/json;charset=utf-8',
              // 'token': Dcookie.getCookie('token'),
            }
          })
          .then(res => {
            if (res.data.code == 1000) {
              ElMessage.error(res.data.msg)
            //   localStorage.clear();
            //   Dcookie.setCookie('token', '', -1, '/', 'derunht.cn')
            //   router.push({
            //     path: "/"
            //   });
            } else {
              resolve(res.data);
            }
          })
          .catch(err => {
            reject(err)         
          });
      });
  }

api.js说明 

//接口统一管理
import { post } from './http.js'

//角色列表
export const roleList = params => post('/power-dev/role/water/list', params);

10.Vue路由

import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/login.vue'
 
const routes = [
  {
    path: '/Home',
    name: 'Home',
    component: Home,
    meta:{
      isShow:false,
    },
    children:[
      {
        path: '/courseList',
        name: 'CourseListin',
        meta:{
          isShow:true,
          title:'课程列表'
        },
        component: () => import(/* webpackChunkName: "login" */ '../views/courseList.vue')
      },
      {
        path: '/teacherList',
        name: 'TeacherList',
        meta:{
          isShow:true,
          title:'讲师列表'
        },
        component: () => import(/* webpackChunkName: "login" */ '../views/teacherList.vue')
      },
      {
        path: '/personal',
        name: 'Personal',
        meta:{
          isShow:true,
          title:'个人中心'
        },
        component: () => import(/* webpackChunkName: "login" */ '../views/personal.vue')
      }
    ]
  },
  {
    path: '/',
    name: 'Login',
    component: Login,
    meta:{
      isShow:false,
    }
  },
 
]
 
const router = createRouter({
  history: createWebHashHistory(),
  routes
})
 
export default router

11.Vue3 vs Vue2

​编辑

12.emit 子调父

<script setup> 中必须使用 defineEmits API 来声明 emits ,它具备完整的类型推断并且无需引用、直接可用

<script setup>
 import { reactive, ref } from 'vue'
 
 //定义事件
 const emit = defineEmits(["resetForm"])
 //子调父方法
 const click= () => {
  emit("resetForm")
 }
</script>

13. setup函数

setup是compositon Api在Vue3中的入口

beforeCreate和created会在setup中执行

无法使用this拿到当前组件的相关数据 setup只能同步执行,不可以异步。

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script> 语法,它具有更多优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 Typescript 声明 props 和抛出事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。

14.ts

因为使用的是typescript,所以标注lang="ts"。

<script lang="ts">

</script>

因为使用了ts语法,所以写明 index 为 number 类型。

setup(){
  const list = ref(["1","2","3"]);
  const aa = ref('');
  const selectClick=(index:number)=>{
    aa.value = list.value[index]
  }
}

框架配置

1.vite.config.js

将文件夹vite.config.js修改为以下,直接复制粘贴即可,跨域地址自行修改

// 使用 vite 创建项目完成后会自动生成 一个 vite.config.js 代码如下
import {
    defineConfig
} from 'vite' // 帮手函数,这样不用 jsdoc 注解也可以获取类型提示
import vue from '@vitejs/plugin-vue'
const {
    resolve
} = require('path')

export default () => defineConfig({
    plugins: [ //配置需要使用的插件列表
        vue(),
    ],
    // 强制预构建插件包
    optimizeDeps: {
        //检测需要预构建的依赖项
        entries: [],
        //默认情况下,不在 node_modules 中的,链接的包不会预构建
        include: ['schart.js', 'axios'],
        exclude: ['your-package-name'] //排除在优化之外
    },
    //静态资源服务的文件夹
    publicDir: "public",
    base: './',
    //静态资源处理
    assetsInclude: "",
    //控制台输出的级别 info 、warn、error、silent
    logLevel: "info",
    // 设为false 可以避免 vite 清屏而错过在终端中打印某些关键信息
    clearScreen: false,
    resolve: {
        alias: [ //配置别名
            {
                find: '@',
                replacement: resolve(__dirname, 'src')
            }
        ],
        // 情景导出 package.json 配置中的exports字段
        conditions: [],
        // 导入时想要省略的扩展名列表
        // 不建议使用 .vue 影响IDE和类型支持
        extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json']
    },
    // css
    css: {
        // 配置 css modules 的行为
        modules: {},
        // postCss 配置
        postcss: {},
        //指定传递给 css 预处理器的选项
        preprocessorOptions: {
            scss: {
                additionalData: `$injectedColor:orange;`
            }
        }
    },
    json: {
        //是否支持从 .json 文件中进行按名导入
        namedExports: true,
        //若设置为 true 导入的json会被转为 export default JSON.parse("..") 会比转译成对象字面量性能更好
        stringify: false
    },
    //继承自 esbuild 转换选项,最常见的用例是自定义 JSX
    esbuild: {
        jsxFactory: "h",
        jsxFragment: "Fragment",
        jsxInject: `import Vue from 'vue'`
    },
    //本地运行配置,以及反向代理配置
    server: {
        overlay: {
            warning: false,
            err: false
        },
        hot: true,
        hotOnly: true, // 是否热更新
        host: 'localhost',
        port: 8080,
        autoOpenBrowser: false, //是否自动打开浏览器
        errorOverlay: true,
        notifyOnErrors: true,
        poll: false,
        devtool: 'cheap-module-eval-source-map',
        cacheBusting: true,
        cssSourceMap: true,
        // 反向代理配置
        proxy: {
            //告诉node, 我接口只要是'/power-dev'开头的才用代理
            '/power-dev': {
                target: 'http://backstage-*****.****-vue',
                changeOrigin: true, //是否允许跨域
                // secure: true,   //是否https接口
                logLevel: 'debug',
                pathRewrite: {
                    //'^/power-dev': ''的含义是将路径中/power-dev替换成空
                    '^/power-dev': '/power-dev'
                }
            },
        },
        disableHostCheck: true
    },
    //打包配置
    build: {
        //浏览器兼容性  "esnext"|"modules"
        target: "modules",
        //指定输出路径
        outDir: "dist",
        //生成静态资源的存放路径
        assetsDir: "assets",
        //小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求。设置为 0 可以完全禁用此项
        assetsInlineLimit: 4096,
        //启用/禁用 CSS 代码拆分
        cssCodeSplit: true,
        //构建后是否生成 source map 文件
        sourcemap: false,
        //自定义底层的 Rollup 打包配置
        rollupOptions: {},
        //@rollup/plugin-commonjs 插件的选项
        commonjsOptions: {},
        //构建的库
        lib: {},
        //当设置为 true,构建后将会生成 manifest.json 文件
        manifest: false,
        // 设置为 false 可以禁用最小化混淆,
        // 或是用来指定使用哪种混淆器
        // boolean | 'terser' | 'esbuild'
        minify: "terser", //terser 构建后文件体积更小
        //传递给 Terser 的更多 minify 选项。
        terserOptions: {},
        //设置为 false 来禁用将构建后的文件写入磁盘
        write: true,
        //默认情况下,若 outDir 在 root 目录下,则 Vite 会在构建时清空该目录。
        emptyOutDir: true,
        //启用/禁用 brotli 压缩大小报告
        brotliSize: true,
        //chunk 大小警告的限制
        chunkSizeWarningLimit: 500
    },
    ssr: {
        // 列出的是要为 SSR 强制外部化的依赖
        external: [],
        //列出的是防止被 SSR 外部化依赖项
        noExternal: []
    }
})

ElementUI

1、使用element-plus框架

官方组件库:

Button 按钮 | Element Plus (gitee.io)

2、安装:

npm install element-plus

3、引用:

import { createApp } from 'vue' 
// 全局引用
import ElementPlus from 'element-plus'; 
// 引用所有样式
import 'element-plus/lib/theme-chalk/index.css'; 
import App from './App.vue'; 

const app = createApp(App) 
// 使用
app.use(ElementPlus) 
app.mount('#app')

配置引入

  1. 只能引入插件的 css 文件,但是 base 样式和icon 还需要手动引入
  2. 下载插件: npm install babel-plugin-import -D
  3. 配置 babel.config.js

 babel.config.js文件内容如下:

module.exports = {
    presets: ['@vue/app'],
    plugins: [
        [
            "import",
            {
                libraryName: 'element-plus',
                customStyleName: (name) => {
                    name = name.slice(3)
                    return `element-plus/packages/theme-chalk/src/${name}.scss`;
                },
            },
        ],
    ],
}

子页面使用

<template>
    <div class="login-wrap">
        <div class="ms-login">
            <div class="ms-title">后台管理系统</div>
            <el-form :model="param" :rules="rules" ref="login" label-width="0px" class="ms-content">
                <el-form-item prop="username">
                    <el-input v-model="param.username" placeholder="username">
                        <template #prepend>
                            <el-button icon="el-icon-user"></el-button>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input type="password" placeholder="password" v-model="param.password"
                        @keyup.enter="submitForm()">
                        <template #prepend>
                            <el-button icon="el-icon-lock"></el-button>
                        </template>
                    </el-input>
                </el-form-item>
                <div class="login-btn">
                    <el-button type="primary" @click="submitForm()">登录</el-button>
                </div>
                <p class="login-tips">Tips : 用户名和密码随便填。</p>
            </el-form>
        </div>
    </div>
</template>

<script>
    import {
        ref,
        reactive
    } from "vue";
    import {
        useStore
    } from "vuex";
    import {
        useRouter
    } from "vue-router";
    import {
        ElMessage
    } from "element-plus";
    import {
        roleList
    } from '@/request/api.js'

    export default {
        setup() {
            const router = useRouter();
            const param = reactive({
                username: "admin",
                password: "123123",
            });
            const rules = {
                username: [{
                    required: true,
                    message: "请输入用户名",
                    trigger: "blur",
                }, ],
                password: [{
                    required: true,
                    message: "请输入密码",
                    trigger: "blur"
                }, ],
            };// 一个普通对象,修改后不会被proxy拦截,进而页面也不会动态更新
            const login = ref(null);
            const submitForm = () => {
                login.value.validate((valid) => {
                    if (valid) {
                        submitFormLink();
                    } else {
                        ElMessage.error("登录失败");
                        return false;
                    }
                });
            };
            // 使用响应式函数reactive构建proxy响应式对象
            const params = reactive({
                ***: 用户名,
                ***: 密码,
            })
            const submitFormLink = () => {
                roleList(params).then(res => {
                    if (res.code == 0) {
                        ElMessage.success("登录成功");
                        localStorage.setItem("ms_username", param.username);
                        router.push("/");
                    } else if (res.code !== 0) {
                        this.$message.error(res.msg);
                    }
                })
            };
            const store = useStore();
            store.commit("clearTags");
            // 使用时,要把对象return出去,才能在template中使用
            return {
                param,
                rules,
                login,
                submitForm,
                params,
                submitFormLink
            };
        },
    };
</script>

<style scoped>
    .login-wrap {
        position: relative;
        width: 100%;
        height: 100%;
        background-image: url(../assets/img/login-bg.jpg);
        background-size: 100%;
    }

    .ms-title {
        width: 100%;
        line-height: 50px;
        text-align: center;
        font-size: 20px;
        color: #fff;
        border-bottom: 1px solid #ddd;
    }

    .ms-login {
        position: absolute;
        left: 50%;
        top: 50%;
        width: 350px;
        margin: -190px 0 0 -175px;
        border-radius: 5px;
        background: rgba(255, 255, 255, 0.3);
        overflow: hidden;
    }

    .ms-content {
        padding: 30px 30px;
    }

    .login-btn {
        text-align: center;
    }

    .login-btn button {
        width: 100%;
        height: 36px;
        margin-bottom: 10px;
    }

    .login-tips {
        font-size: 12px;
        line-height: 30px;
        color: #fff;
    }
</style>

4、首页布局实现 - 参考Element官网 

<template>
  <div class="home">
    <el-container>
      <el-header class="top-box">
          <el-row :gutter="24">
          <el-col :span="18">
              <img class="logo" src="../assets/logo.png" alt="">
          </el-col>
          <el-col :span="6">
            <div class="info-box">
              <span>Tina</span>
              <el-button class="out-btn" type="text">退出</el-button>
            </div>
          </el-col>
        </el-row>
        </el-header>
      <el-container>
        <el-aside width="200px">
            <el-menu
             router='true'
              class="el-menu-vertical-demo">
              <el-menu-item :route="i.path" v-for="i in tabList" :key="i.name" :index="i.name">
                <!-- <i class="el-icon-document"></i> -->
                <template #title>{{i.meta.title}}</template>
              </el-menu-item>
              
            </el-menu>
        </el-aside>
        <el-main>
          <div class="content-box">
            <router-view/>
          </div>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>
 
<script>
// @ is an alias to /src
// import {reactive} from "vue";
import router from "../router/index";
export default {
  name: "Home",
  setup() {
    let tabList = router.options.routes[0].children;
    return {
      tabList,
    };
  },
};
</script>
<style lang="scss" scoped>
.top-box {
  background: #efefef;
  padding: 5px;
  .logo {
    width: 50px;
  }
  .info-box {
    text-align: right;
  }
  .out-btn {
    margin-left: 10px;
  }
}
.content-box{
  padding:20px;
}
</style>

5、开发疑问+解答

1、dark

<script lang="ts" setup>
import { isDark } from '~/composables/dark'
</script>

<template>
  <div class="flex">
    <el-button color="#626aef" :dark="isDark">Default</el-button>
  </div>
</template>

:dark="isDark"  目前调研此为错误

2、引用示例

  • 注意script写法,v-model内值,定义时加入ref
<template>
  <el-cascader :options="options" />
</template>

<script lang="ts" setup>
const options = [
  {
    value: 'guide',
    label: 'Guide',
    children: [
      {
        value: 'notice',
        label: 'Notice',
        children: [
          {
            value: 'message-box',
            label: 'MessageBox',
          },
        ],
      },
    ],
  },
]
</script>

<script lang="ts" setup>
import { ref } from 'vue'
const checked1 = ref(true)
const checked2 = ref(false)
</script>

<template>
  <el-checkbox-group v-model="checkList">
    <el-checkbox label="Option A" />
    <el-checkbox label="Option B" />
    <el-checkbox label="Option C" />
    <el-checkbox label="disabled" disabled />
    <el-checkbox label="selected and disabled" disabled />
  </el-checkbox-group>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const checkList = ref(['selected and disabled', 'Option A'])
</script>

<template>
  <el-checkbox
    v-model="checkAll"
    :indeterminate="isIndeterminate"
    @change="handleCheckAllChange"
    >Check all</el-checkbox
  >
  <el-checkbox-group
    v-model="checkedCities"
    @change="handleCheckedCitiesChange"
  >
    <el-checkbox v-for="city in cities" :key="city" :label="city">{{
      city
    }}</el-checkbox>
  </el-checkbox-group>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const checkAll = ref(false)
const isIndeterminate = ref(true)
const checkedCities = ref(['Shanghai', 'Beijing'])
const cities = ['Shanghai', 'Beijing', 'Guangzhou', 'Shenzhen']

const handleCheckAllChange = (val: boolean) => {
  checkedCities.value = val ? cities : []
  isIndeterminate.value = false
}
const handleCheckedCitiesChange = (value: string[]) => {
  const checkedCount = value.length
  checkAll.value = checkedCount === cities.length
  isIndeterminate.value = checkedCount > 0 && checkedCount < cities.length
}
</script>