【前端工程化】深入浅出vite(二)--vue3全家桶+ts构建后管系统

news2024/11/15 20:38:34

安装基础包

npm create vite@latest
# 这里选择的是Vue+Typescript的组合
cd vue-admin
npm install

# 先安装基础包
npm install vue-router@4
npm i pinia
npm i axios
npm install sass --save-dev
npm install element-plus --save
npm install @element-plus/icons-vue
npm install -D unplugin-vue-components unplugin-auto-import
npm i eslint -D

# 提交规范
npm i lint-staged husky  --save-dev
npm install @commitlint/cli @commitlint/config-conventional -D

代码规范

npm init @eslint/config

接下来会有一堆提示,选择如下:

Need to install the following packages:
  @eslint/create-config
Ok to proceed? (y)
√ How would you like to use ESLint? · style       
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · standard-with-typescript
√ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard-with-typescript@latest
The config that you've selected requires the following dependencies:

eslint-plugin-vue@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.50.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm
Installing eslint-plugin-vue@latest, eslint-config-standard-with-typescript@latest, @typescript-eslint/eslint-plugin@^5.50.0, eslint@^8.0.1, eslint-plugin-import@^2.25.2, eslint-plugin-n@^15.0.0, eslint-plugin-promise@^6.0.0, typescript@*

在项目中就会生成一个.eslintrc.cjs文件,接下来配置一下脚本验证一下:

"lint": "eslint src/**/*.{js,jsx,vue,ts,tsx} --fix"

然而运行的时候报错了,由于我当前的"typescript": "^5.1.3",@typescript-eslint/typescript-estree支持的ts版本范围为:=3.3.1 <5.1.0,所以我得降级一下:typescript@5.0.4,在配置eslint路上出现了很多问题,直接提供解决方案:

首先是修改.eslintrc.cjs:

module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'standard-with-typescript'
  ],
  parser: "vue-eslint-parser",
  overrides: [
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: ["./tsconfig.json"],
    parser: "@typescript-eslint/parser",
    extraFileExtensions: ['.vue']
  },
  plugins: [
    'vue'
  ],
  rules: {
    'space-before-function-paren': [2, {
      anonymous: 'always',
      named: 'never',
      asyncArrow: 'always'
    }],
    'vue/multi-word-component-names': 0,
    "space-before-function-paren": 0,
    "@typescript-eslint/consistent-type-assertions": 0,
    "@typescript-eslint/ban-types": [
      "error",
      {
        "extendDefaults": true,
        "types": {
          "{}": false
        }
      }
    ]
  }
}

vite-env.d.ts加上一行注释,忽略检查:

// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="vite/client" />

关于eslint配置中遇到的问题,可以参考这个大佬写的,更详细些:Eslint:vue3项目添加eslint(standard规则)

commit规范

git init

package.json中新增如下代码,利用它来调用 eslint 和 stylelint 去检查暂存区内的代码

"lint-staged": {
    "*.{vue,js}": [
      "npm run lint"
    ]
  }

执行:

npm pkg set scripts.postinstall="husky install"
# 等同于执行npm i,执行过程中会生成.husky文件夹
npm run postinstall

npx husky add .husky/pre-commit "npm lint"
git add .husky/pre-commit

这样我们执行git commit的时候就会自动执行npm lint

很尴尬,在跑的过程中,报错了node不是内部或外部命令。node -v是木有问题的,大抵是nvm这个工具的问题,所以后面就换了volta来做node的版本控制。

npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"

新建commitlint.config.cjs

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', [
      'feat', // 新增功能
      'update', // 更新功能
      'ui', // 样式改动
      'fix', // 修复功能bug
      'merge', // 合并分支
      'refactor', // 重构功能
      'perf', // 性能优化
      'revert', // 回退提交
      'style', // 不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等)
      'build', // 修改项目构建工具(例如 glup,webpack,rollup 的配置等)的提交
      'docs', // 文档新增、改动
      'test', // 增加测试、修改测试
      'chore' // 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
    ]],
    'scope-empty': [0],
    // 'scope-empty': [2, 'never'], 作用域不为空
    'scope-case': [0],
    'subject-full-stop': [0],
    'subject-case': [0]
  }
}

修改tsconfig.json

"include": [
  //...
  "commitlint.config.cjs"
 ],

修改.eslintrc.cjs

project: ["./tsconfig.json", "./commitlint.config.cjs"],
git add .
# 失败
git commit -m "commit校验"
# 成功
git commit -m "feat: commit校验"

设置路径别名

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

const resolve = (dist) => path.resolve(__dirname, dist)

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve('src')
    },
    // 顺便把可以省略的后缀配置一下,在vite中不支持省略.vue
    extensions: [".js", ".ts", ".tsx", ".jsx"]
  }
})

修改tsconfig.json,新增:

"compilerOptions": {
    // ...
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },

重置样式

assets文件夹下新建styles/reset.css

/**
 * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
 * http://cssreset.com
 */
 
 html, body, div, span, applet, object, iframe,
 h1, h2, h3, h4, h5, h6, p, blockquote, pre,
 a, abbr, acronym, address, big, cite, code,
 del, dfn, em, img, ins, kbd, q, s, samp,
 small, strike, strong, sub, sup, tt, var,
 b, u, i, center,
 dl, dt, dd, ol, ul, li,
 fieldset, form, label, legend,
 table, caption, tbody, tfoot, thead, tr, th, td,
 article, aside, canvas, details, embed, 
 figure, figcaption, footer, header, hgroup, 
 menu, nav, output, ruby, section, summary,
 time, mark, audio, video{
   margin: 0;
   padding: 0;
   border: 0;
   font-size: 100%;
   font: inherit;
   font-weight: normal;
   vertical-align: baseline;
 }
 /* HTML5 display-role reset for older browsers */
 article, aside, details, figcaption, figure, 
 footer, header, hgroup, menu, nav, section{
   display: block;
 }
 ol, ul, li{
   list-style: none;
 }
 blockquote, q{
   quotes: none;
 }
 blockquote:before, blockquote:after,
 q:before, q:after{
   content: '';
   content: none;
 }
 table{
   border-collapse: collapse;
   border-spacing: 0;
 }
  
 /* custom */
 a{
   color: #7e8c8d;
   text-decoration: none;
   backface-visibility: hidden;
   -webkit-backface-visibility: hidden;
 }
 ::-webkit-scrollbar{
   width: 5px;
   height: 5px;
 }
 ::-webkit-scrollbar-track-piece{
   background-color: rgba(0, 0, 0, 0.2);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 ::-webkit-scrollbar-thumb:vertical{
   height: 5px;
   background-color: rgba(125, 125, 125, 0.7);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 ::-webkit-scrollbar-thumb:horizontal{
   width: 5px;
   background-color: rgba(125, 125, 125, 0.7);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 html, body{
   width: 100%;
   font-family: "Arial", "Microsoft YaHei", "黑体", "宋体", "微软雅黑", sans-serif;
 }
 body{
   line-height: 1;
   -webkit-text-size-adjust: none;
   -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 }
 html{
   overflow-y: scroll;
 }
  
 /*清除浮动*/
 .clearfix:before,
 .clearfix:after{
   content: " ";
   display: inline-block;
   height: 0;
   clear: both;
   visibility: hidden;
 }
 .clearfix{
   *zoom: 1;
 }
  
 /*隐藏*/
 .dn{
   display: none;
 }
 

使用Scss

Vite 提供了对 .scss, .sass, .less, .styl.stylus 文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖。

一般我们会在项目中定义一些主题色:

// variable.scss
$font-color-gray:rgb(147,147,147);

或者是一些封装好的集合样式:

// mixins.scss
@mixin line-clamp($lines) {
  word-break: break-all;
  display: -webkit-box;
  overflow: hidden;
  text-overflow: ellipsis;
  -webkit-line-clamp: $lines;
  -webkit-box-orient: vertical;
}

@mixin ellipsis() {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}


然后我们在vite.config.js中配置:

css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
      }
    }
  }

删除目录中的style.css,新建styles/common.scss

// common.scss
@import url('./reset.css');

然后在main.ts中引入:

import '@/assets/styles/common.scss'

这样全局样式就初始化了,接下来测试一下variable.scssmixins.scss是否起作用,修改HelloWorld.vue查看是否为灰色且两行省略:

<script setup lang="ts">
</script>

<template>
  <div class="box">没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。</div>
</template>

<style scoped lang="scss">
.box {
  color: $font-color-gray;
  height: 40px;
  line-height: 20px;
  width: 200px;
  @include line-clamp(2);
}
</style>

配置路由

现在要来配置路由,希望有这样的结果:

- 登录页
- 带菜单栏的框架
  - 主页
  - 人员管理
    - 客户管理
    - 员工管理
- 404

所以新建以下文件:

在这里插入图片描述

在这个过程中我们会遇到几个问题:

  • 在编写路由的时候我们引入组件,必须有.vue后缀;
  • import xxx from '@/xxx'时会报错,是因为你没有在上面提到的那样在tsconfg.json中设置baseUrlpaths值。

由于layout文件中要嵌套子路由,所以layout中要加入router-view:

<template>
  <div>布局</div>
  <router-view></router-view>
</template>

其他的文件只要写成这样就行:

<template>
  <p>主页</p>
</template>

新建router文件夹:

- router
  -hooks # 后期做登录校验和鉴权用的
  - routes
    - index.ts # 总输出文件
    - others.ts # 不需要layout这一层的路由均可以放在这里
    - person.ts # 人员管理模块
  - index.ts   # 总输出文件

接下里是各个文件的内容:

// person.ts
export default [
  {
    path: '/person',
    name: 'Person',
    meta: { title: '人员管理' },
    redirect: '/person/customer',
    children: [
      {
        path: '/person/customer',
        name: 'PersonCustomer',
        meta: { title: '客户管理' },
        component: () => import('@/views/person/customer/index.vue')
      },
      {
        path: '/person/staff',
        name: 'PersonStaff',
        meta: { title: '员工管理' },
        component: () => import('@/views/person/staff/index.vue')
      }
    ]
  }
];

// others.ts
export default [
  {
    path: '/login',
    name: 'Login',
    meta: { title: '登录' },
    component: () => import('@/views/login/index.vue')
  }
];

// router/routes/index.ts
import Layout from '@/views/layout/index.vue';
import personRoutes from './person';
import otherRoutes from './others';

export default [
  {
    path: '/',
    name: 'Layout',
    component: Layout,
    children: [
      {
        path: '/',
        name: 'Index',
        meta: { title: '主页' },
        component: () => import('@/views/index/index.vue')
      },

      ...personRoutes,
    ]
  },
  ...otherRoutes,
  {
    path: '/404',
    name: 'NotFound',
    meta: { title: '404' },
    component: () => import('@/views/404/index.vue')
  },
  {
    path: "/:pathMatch(.*)",
    redirect: "/404",
    name:'ErrorPage',
    meta: { title: '' },
  }
];

先抛开hooks文件夹,简单的写一下index.ts

// router/index.ts
import routes from "./routes";
export default routes;

新建一个src/plugins/index.ts,之前我们注册内容的时候都是直接放在main.ts中,不太容易维护,所以以后统一在这里挂载:

// src/plugins/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import routes from '@/router/index';

export default (app: any) => {
  // 注册路由
  const router = createRouter({
    history: createWebHashHistory(),
    routes
  })

  app.use(router);
}

不要忘了修改App.vue:

<template>
  <router-view></router-view>
</template>
import { createApp } from 'vue'
import '@/assets/styles/common.scss'
import App from './App.vue'
import installPlugins from '@/plugins';

const app = createApp(App);
installPlugins(app);
app.mount('#app')

这样就可以测试了:

http://127.0.0.1:5173/#/
http://127.0.0.1:5173/#/login
http://127.0.0.1:5173/#/person
http://127.0.0.1:5173/#/person/customer
http://127.0.0.1:5173/#/person/staff

如果在配置过程中发现报错:找不到模块“xxx.vue”或其相应的类型声明,则在vite-env.d.ts中新增:

declare module '*.vue' {
  import type { DefineComponent } from 'vue';
  const vueComponent: DefineComponent<{}, {}, any>;
  export default vueComponent;
} 

使用element plus

如果您使用 Volar,请在 tsconfig.json 中通过 compilerOptions.type 指定全局组件类型。

// tsconfig.json
{
  "compilerOptions": {
    // ...
    // 然而这个配置在后期打包的时候报错了...
    "types": ["element-plus/global"]
  }
}

这里采用了按需引入的方式,如果对体积不追求的,可以采用完整引入:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// 新增
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

const resolve = (dist) => path.resolve(__dirname, dist)

export default defineConfig({
  plugins: [
    vue(),
    // 新增
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    // 新增
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  resolve: {
    alias: {
      '@': resolve('./src')
    },
    extensions: [".js", ".ts", ".tsx", ".jsx"]
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
      }
    }
  }
})


element plus中日期等组件默认是英文,所以我们把组件改为中文:

在这里插入图片描述

修改App.vue

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
</script>

<template>
  <el-config-provider :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

这样就会出现中文了。

在这里插入图片描述

关于引入的两个插件,这里解释一下:

  • unplugin-vue-components用于自动识别Vue模板中使用的组件,自动按需导入和注册;
  • unplugin-auto-import可以在vite、webpack等环境下自动按需导入配置库常用的API,如Vueref,不需要手动import,所以我们可以配置一下,并删除一些API的引入:
export default defineConfig({
  plugins: [
    // ...
    AutoImport({
      imports: [
        'vue',
        'vue-router',
        'pinia'
      ],
      eslintrc: {
        enabled: true,
        filepath: './.eslintrc-auto-import.json',
        globalsPropValue: true
      },
      resolvers: [ElementPlusResolver()]
    }),
    // ...
  ],
})

保存生效后,auto-imports.d.ts 会自动填充内容,并且会在项目根目录生成 .eslintrc-auto-import.json eslint 全局变量配置。

然后修改tsconfg.json.eslintrc.cjs

// tsconfg.json
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "commitlint.config.cjs", "auto-imports.d.ts"],

// .eslintrc.cjs
project: ["./tsconfig.json", "./commitlint.config.cjs", './.eslintrc-auto-import.json'],

忽略 auto-imports.d.ts ESLint 校验

# .eslintignore
auto-imports.d.ts

这里需要注意一下:

  1. 不是全部 API,例如 Vue Router 的 createRouter 就不会导入。具体可以自动导入的 API 参考 unplugin-auto-import/src/presets
  2. 生成 .eslintrc-auto-import.json 文件后如不需要增加配置建议将 enabled: true 设置为 false,否则每次都会生成这个文件。

配置完可以删除页面中的一些引用,发现是没有问题的。

<script lang='ts' setup>
// import { storeToRefs } from 'pinia'
// import { useRouter } from 'vue-router'
// ...
</script>

测试一下组件:

<template>
  <p><el-button>测试</el-button></p>
</template>

这样页面上就会显示按钮了。

自动按需引入的原理是通过识别<template>中使用的组件自动导入,那类似ElMessage 这类直接在 JS 中调用方法的组件,插件并不会识别并完成自动导入,所以还是需要自己手动导入一下(建议按需引入的方式,仍然引入完整的样式文件,避免这类边界问题):

修改vite-env.d.ts,不然在ts中引入element plus会报错:

declare module "element-plus";

plugins/element-plus.ts中小试一下:

import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

const options = {
  size: 'small',
  zIndex: 3000
}

const components = [
  ElLoading,
  ElMessage,
  ElMessageBox, 
  ElNotification
]

export default function install (app: any): void {
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
  }

  components.forEach((component) => {
    app.use(component, options)
  })
}


// plugins/index.ts
export default (app: any) => {
  // ...

  // 注册element-plus
  installElementPlus(app);
}

测试一下:

<template>
  <p><el-button @click="onTest">测试</el-button></p>
</template>

<script setup lang="ts">
const onTest = () => {
  ElLoading.service({ fullscreen: true });
}
</script>

globalProperties

按照以前的习惯,loading的调用肯定不用上面的方式,而是挂载在全局Vue.prototype上,然而在这个项目中,我们用的是按需引入,且在Vue3中,写法变了,你可能想这么写:

app.config.globalProperties.$loading = ElLoading;
app.config.globalProperties.$message = ElMessage;
app.config.globalProperties.$msgBox = ElMessageBox;
app.config.globalProperties.$notification = ElNotification;

然后再使用的过程中:

<script setup lang="ts">
const instance = getCurrentInstance()

onMounted(() => {
  instance.proxy.$message.success('setup - getCurrentInstance() 成功使用')
  // 也可以使用 appContext
  console.log(instance.appContext.config.globalProperties.$message === instance.proxy.$message) // true
})
</script>

但是查阅了官方文档,并没有getCurrentInstance该方法,大概是不符合规范吧。所以全局方法的注入,我采用了provide/inject

App.vue

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'

provide('$loading', ElLoading)
provide('$message', ElMessage)
provide('$messagebox', ElMessageBox)
provide('$notification', ElNotification)
</script>

<template>
  <el-config-provider :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

修改element-plus.ts

import { App } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

// const options = {
//   size: 'small',
//   zIndex: 3000
// }

// const components = [
//   ElLoading,
//   ElMessage,
//   ElMessageBox, 
//   ElNotification
// ]

export default function install (app: App): void {
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
  }

  // components.forEach((component) => {
  //   app.use(component, options)
  // })
}

新增src/types/global.d.ts

import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'

export interface ComponentCustomProperties {
  $message: typeof ElMessage
  $msgBox: typeof ElMessageBox
  $loading: typeof ElLoading
  $notification: typeof ElNotification
}


测试:

<template>
  <p><el-button @click="onTest">测试</el-button></p>
</template>

<script setup lang="ts">
const $loading = inject('$loading') as any 
const onTest = () => {
  $loading.service({
    lock: true,
    text: 'Loading',
  })
}
</script>

复习一些常见的ts写法

开发服务器和打包器将只会对ts文件执行语法转义,而不会执行任何类型检查,保证了vite开发服务器在使用ts时也能保持飞快的速度。

下面是一些常见的例子:

<!-- 对props的类型声明和默认值 -->
<script setup lang="ts">
interface Props {
  msg?: string
  labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
   msg: 'hello',
   labels: () => ['one', 'two']
})
</script>


<!-- 另一种方式 -->
<script setup lang="ts">
interface Book {
  title?: string
  author: string
}
const props = defineProps({
  book: Object as PropType<Book>
})
</script>


<!-- 对emits进行声明 -->
<script setup lang="ts">
// 1.运行时
const emit = defineEmits(['change', 'update'])

// 2.基于类型
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>


<!-- 为computed指定返回类型 --->
<script setup lang="ts">
const double = computed<number>(() => { /** return number */ })
</script>

<!-- 为函数参数标注类型 -->
<script setup lang="ts">
const onClick = (e: Event) => {
  console.log((event.target as HTMLInputElement).value)
}
</script>

<!-- project和inject 然而我很少用到 -->
<script setup lang="ts">
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>
provide(key, 'foo')

const foo = inject<string>('foo', 'bar')
</script>

<!-- 为模板引用标注类型 -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)

onMounted(() => {
  el.value?.focus()
})
</script>

<template>
  <input ref="el" />
</template>


<!-- 为组件模板引用标注类型 -->
<script setup lang="ts">
import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {
  modal.value?.open()
}
</script>

登录静态页面

在登录页面中会使用到el-input组件,一般对于这种表单组件,后管使用的频率还是很高的,所以倾向于进行二次封装再使用,一般情况下会优化它的前后空格问题,并将它与textarea拆解出来:

<script lang='ts' setup>
import { computed } from 'vue';
const emits = defineEmits<{
  (e: 'update:value', value: string): void;
  (e: 'blur'): void;
  (e: 'focus'): void;
  (e: 'change', value: string): void;
  (e: 'clear'): void;
}>()

const props = defineProps({
  type: {
    type: String,
    default: 'string'
  },
  value: {
    type: [String, Number],
    default: '',
    required: true
  },
  maxlength: [String, Number],
  minlength: [String, Number],
  placeholder: {
    type: String,
    default: '请输入',
  },
  clearable: {
    type: Boolean,
    default: true
  },
  showPassword: {
    type: Boolean,
    default: false
  },
  disabled: {
    type: Boolean,
    default: false
  },
  prefixIcon: {
    type: String,
    default: ''
  },
  suffixIcon: {
    type: String,
    default: ''
  },
  inputStyle: [String, Object],
  showWordLimit: {
    type: Boolean,
    default: true
  },
  rows: Number,
  autosize: [Boolean, Object]
})

const input = computed({
  get(){
    console.log(props.value)
    return props.value;
  },

  set(val: any){
    if(typeof val === 'string') {
      val = val ? val.trim() : val;
    }
    emits('update:value', val);
  }
})

const onFocus = () => {
  emits('focus');
}

const onBlur = () => {
  emits('blur');
}

const onClear = () => {
  emits('clear');
}

const onChange = (val: string) => {
  emits('change', val);
}

</script>

<template>
  <el-input
    v-if="type === 'textarea'"
    v-model="input"
    :rows="rows"
    type="textarea"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :show-word-limit="showWordLimit"
    :disabled="disabled"
    :prefixIcon="prefixIcon"
    :suffixIcon="suffixIcon"
    :autosize="autosize"
    :inputStyle="inputStyle"
    @focus="onFocus"
    @blur="onBlur"
    @change="onChange"
  />
  <el-input 
    v-else
    v-model="input" 
    :type="type"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :clearable="clearable"
    :showPassword="showPassword"
    :disabled="disabled"
    :prefixIcon="prefixIcon"
    :suffixIcon="suffixIcon"
    :inputStyle="inputStyle"
    @focus="onFocus"
    @blur="onBlur"
    @clear="onClear"
    @change="onChange" />
    
</template>

<style scoped lang='scss'>

</style>

plugins/components.ts下全局注册:

import type { Component } from 'vue'
import ArInput from '@/components/form/input/index.vue';

const componentObj: {[propName: string]: Component} = {
  ArInput
};

export default function install(app: any) {
  Object.keys(componentObj).forEach((key) => {
    app.component(key, componentObj[key])
  })
}

记得在plugins/index.ts中加入:

import installComponents from './components'

export default (app: any) => { 
  // ...

  // 注册自定义组件
  installComponents(app);
}

login.vue的源码:

<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus'
import { ref, reactive } from 'vue'


const formRef = ref<FormInstance>();
const form = reactive({
  name: '',
  password: ''
})
const rules = ref<FormRules>({
  name: [
    { required: true, message: '请输入账号', trigger: 'blur' },
    { min: 8, max: 12, message: '账号长度为8-12', trigger: 'blur' },
  ],
  password: [
  { required: true, message: '请输入密码', trigger: 'blur' },
  ]
})

const onSubmit = async() => {
  await formRef.value.validate((valid, fields) => {
    if (valid) {
      console.log('submit!')
    } else {
      console.log('error submit!', fields)
    }
  })
}

</script>

<template>
  <div class="login">
    <div class="login-inner">
      <el-form ref="formRef" :model="form" :rules="rules">
        <el-form-item label="账号" prop="name" required>
          <ArInput v-model:value="form.name" prefix-icon="User" />
        </el-form-item>
        <el-form-item label="密码" prop="password" required>
          <ArInput v-model:value="form.password" type="password" prefix-icon="Lock" />
        </el-form-item>
        <el-button type="primary" class="login-btn" @click="onSubmit">登录</el-button>
      </el-form>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.login {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  background-color: #ccc;

  &-inner{
    margin-top: 20%;
    width: 300px;
    padding: 32px;
    background-color: #fff;
    border-radius: 4px;
  }

  &-btn {
    width: 100%;
  }
}
</style>

最后出来的页面效果即:

在这里插入图片描述

环境变量

在完成页面提交动作之前,先解决环境变量的问题,在测服、预发布或者生产中我们总有些变量是不一样的,所以需要做环境区分

新建env文件夹,内部新增三个四个文件:

.env                # 所有情况下都会加载
.env.development     # 开发环境
.env.release         # 预发布环境
.env.production      # 正服环境

举个例子:

# .env.development 
VITE_ENV = devalopment
# 请求接口
VITE_API_URL = https://api.vvhan.com/testapi/saorao

这样就可以在不同的环境中设置变量,然后我们修改一下脚本命令,区分一下环境:

"scripts": {
    "watch": "vite",
    "watch:release": "vite --mode release",
    "watch:production": "vite --mode production",
    "build:development": "vue-tsc && vite build --mode development",
    "build:release": "vue-tsc && vite build --mode release",
    "build:production": "vue-tsc && vite build --mode production",
    // ...
  },

在根目录新建build/utils.ts文件:

// Read all environment variable configuration files to process.env
export function wrapperEnv(envConf: Recordable) {
  const result: any = {}

  for (const envName of Object.keys(envConf)) {
    let realName = envConf[envName].replace(/\\n/g, '\n')
    realName =
      realName === 'true' ? true : realName === 'false' ? false : realName

    result[envName] = realName
    if (typeof realName === 'string') {
      process.env[envName] = realName
    } else if (typeof realName === 'object') {
      process.env[envName] = JSON.stringify(realName)
    }
  }
  return result
}

然后修改vite.config.js,将我们定义的变量注入进去:

import { defineConfig, loadEnv, UserConfig, ConfigEnv  } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { wrapperEnv } from './build/utils'

const resolve = (dist) => path.resolve(__dirname, dist)

// https://vitejs.dev/config/
export default ({ command, mode }: ConfigEnv): UserConfig => {
  const env = loadEnv(mode, './env')
  wrapperEnv(env)

  return {
    plugins: [
      vue(),
      AutoImport({
        imports: [
          'vue',
          'vue-router',
          'pinia'
        ],
        eslintrc: {
          enabled: false,
          filepath: './.eslintrc-auto-import.json',
          globalsPropValue: true
        },
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ],
    resolve: {
      alias: {
        '@': resolve('./src')
      },
      extensions: [".js", ".ts", ".tsx", ".jsx"]
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
        }
      }
    }
  }
}

这样我们就可以在下面的axios封装中使用了。哦对可能会有点ts的报错,修改tsconfig.node.json

"include": ["vite.config.ts", "build**/*.ts"]

axios封装

新建src/utils/request.ts

import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'

const request: AxiosInstance | any = axios.create({
  timeout: 100000,
  headers: {
    post: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  },
  withCredentials: true
});

request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  let token = localStorage.getItem("token");
  if (token && token !== '') {
    config.headers['Authorization'] = token;
  }

  // 获取环境变量!!!
  const projectUrlPrefix = import.meta.env.VITE_API_URL;
  // 这样更支持多域名接口的情况
  if (config && config.url && !/^(http(|s):\/\/)|^\/\//.test(config.url)) {
    config.url = projectUrlPrefix + config.url;
  }
  return config;
});

request.interceptors.response.use((res: any) => {
  if (res.data.status && res.data.status !== 200) {
    ElMessage.error(res.data.msg || '请求失败,请稍后重试')
    return Promise.reject(res.data)
  }
  // 如果这里是登录信息过期,那么应该给个弹窗提示什么的,最后都应该重定向到登录页面
  return res.data
}, (error: any) => {
  console.log(`%c 接口异常 `, 'background-color:orange;color: #FFF;border-radius: 4px;', error);
})

export default request;
export const $get = (url: string, params = {}) => {
  return request.get(url, {
    params
  })
}
export const $post = (url: string, params = {}) => {
  return request.post(url, params)
}

去登录页面做一下测试:

<script lang="ts" setup>
const $post = inject('$post') as any
// ...

const onSubmit = async () => {
  // ...
  const res = await $post('/login', form)
  // ...
}
</script >

测试不同环境下的域名前缀,如果不一样,则说明配置成功了~

https://api.vvhan.com/testapi/saorao/login
https://api.vvhan.com/releaseapi/saorao/login
https://api.vvhan.com/api/saorao/login

整体布局

在这里插入图片描述

  • 将路由模块中的路径转为菜单显示在左边;
  • 上面部分是用户信息,以及可以退出;
  • 切换路由,会出现一个类似浏览器的tab,可以点击tab切换,也可以关闭当前页面;
  • 最后是页面的主体内容显示。

先看看菜单组件:

<script lang='ts' setup>
import { ref, computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import routes from '@/router/index';
import { IRouterItem } from '@/types/menu'
import { useTagViewsStore } from '@/store/tagViews';

const route = useRoute();
const router = useRouter();
const isCollapse = ref(false);
const defaultActive = ref('0')
const tagViewsStore = useTagViewsStore();
const { visitedViews } = storeToRefs(tagViewsStore);

// 1. 从声明的路由中获取当前显示的菜单栏(还可以过滤一些不是菜单栏的页面)
const currentMenu = computed(() => {
  if(routes && routes.length) {
    const routesArr: any = routes.filter((route) => route.name === 'Layout');
    if(routesArr && routesArr[0] && routesArr[0].children){
      const res = routesArr[0].children as IRouterItem[];
      return res;
    }else {
      return [];
    }
  }else {
    return []
  }
});

// 获取路由对应的菜单下标(如果一打开是客户列表页,则高亮客户列表)
const currentMenuToObj = computed(() => {
  const routes = currentMenu.value;
  if(routes && routes.length) {
    let obj: {[key: string]: any} = {};
    for(let i = 0; i < routes.length; i++) {
      const item = routes[i];
      if(item.children) {
        for(let j = 0; j< item.children.length; j++) {
          const subItem = item.children[j];
          obj[subItem.path] = {
            index: `${i}-${j}`,
            item: subItem
          };
        }
      }else {
        obj[item.path] = {
          index: '' + i,
          item
        }; 
      }
    }

    return obj;
  }else {
    return {};
  }
})

// 监听路由获取当前高亮的值,store的使用在后面详细说一下
watch(
  () => route.path,
  (val: string) => {
    if(!visitedViews.value.length) {
      const item = {
        path: '/',
        name: 'Index',
        meta: { title: '主页' },
      }
      tagViewsStore.addVisitedViews(item)
      tagViewsStore.setActivitedView(item)
    }
    if(val) {
      const obj = currentMenuToObj.value[val];
      defaultActive.value = obj.index;
      tagViewsStore.addVisitedViews(obj.item)
      tagViewsStore.setActivitedView(obj.item)
    }
  }, {
    immediate: true
  }
)

// 点击菜单栏进行跳转
const onToPage = (item: IRouterItem) => {
  if(route.path === item.path) return;
  tagViewsStore.addVisitedViews(item)
  tagViewsStore.setActivitedView(item)
  router.push(item.path)
}

</script>

<template>
  <div class="menu">
    <div class="menu-logo">Logo</div>
    <div class="menu-main">
      <el-menu
        :default-active="defaultActive"
        :collapse="isCollapse"
        background-color="#191919"
        text-color="#7e7e7e"
        active-text-color="#ffffff"
      >
        <template v-for="(item, index) in currentMenu" :key="index">
          <el-menu-item :index="'' + index" v-if="!item.children || !item.children.length" @click="onToPage(item)">
            <template #title>{{ item.meta.title  }}</template>
          </el-menu-item>
          <el-sub-menu :index="'' + index" v-else>
            <template #title>
              <span>{{ item.meta.title  }}</span>
            </template>
            <el-menu-item v-for="(subItem, subIndex) in item.children" :key="`${index}-${subIndex}`" :index="`${index}-${subIndex}`" @click="onToPage(subItem)">{{ subItem.meta.title  }}</el-menu-item>
          </el-sub-menu>
        </template>
      </el-menu>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.menu {
  height: 100vh;
  max-width: 280px;
  background-color: #191919;
  
  &-logo {
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
  }

  &-main {
    height: calc(100vh - 100px);
  }
}
</style>

<style lang="scss">
.menu .el-menu {
  border: none !important;
}
</style>

接下来是顶部栏信息:

<script lang='ts' setup>

</script>

<template>
  <div class="nav">
    <div class="nav-left"></div>
    <div class="nav-right">
      <div class="nav-right-item">
        <el-dropdown>
          <span class="el-dropdown-link">
            您好,XXX
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item>退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px;
  height: 60px;
  border-bottom: solid 1px var(--el-menu-border-color);
  background-color: #fff;

  &-right{
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: flex-end;

    &-item {
      cursor: pointer;
    }
  }
}
</style>

接下来是关于导航栏tab的开发了,这里我们运用到了pinia来做状态管理,安装之后我们先在plugins/index.ts中注册:

import { createPinia } from 'pinia';

// ...
// 注册store (建议这个放在所有注册的首位,方便其他插件可能会用到它)
app.use(createPinia());

声明关于导航啦tabstore,在src/store/tagViews.ts中:

import { defineStore } from 'pinia';
import { IRouterItem } from '@/types/menu'

export const useTagViewsStore = defineStore('tagViews', {
  state: () => {
    return {
      // 访问过的页面
      visitedViews: [] as IRouterItem[],
      // 当前访问的页面
      activitedView: {} as IRouterItem
    }
  },

  actions: {
    // 新增页面
    addVisitedViews(view: IRouterItem){
      const item = this.visitedViews.find((item) => item.path === view.path)
      if(item) return;
      this.visitedViews.push(view);
    },
    
    // 删除页面
    deleteVisitedViews(index: number) {
      this.visitedViews.splice(index, 1);
    },
    
    // 高亮某个页面
    setActivitedView(view: IRouterItem) {
      this.activitedView = view
    }
  }
})

tagViews组件的源码如下:

<script lang='ts' setup>
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router'
import { useTagViewsStore } from '@/store/tagViews';
import { IRouterItem } from '@/types/menu'

const router = useRouter();
const tagViewsStore = useTagViewsStore();
const { visitedViews, activitedView } = storeToRefs(tagViewsStore);

// 关闭页面
const onDel = (item: IRouterItem) => {
  const index = visitedViews.value.findIndex((view) => view.path === item.path);
  if(index === -1) return;

  tagViewsStore.deleteVisitedViews(index);
  if(item.path === activitedView.value.path) {
    const obj = visitedViews.value[index - 1]
    tagViewsStore.setActivitedView(obj);
    router.push(obj.path)
  }
}

// 切换页面
const onChange = (item: IRouterItem) => {
  tagViewsStore.setActivitedView(item)
  router.push(item.path)
}

</script>

<template>
  <el-scrollbar class="tags-scrollbar">
    <div class="tags">
      <div v-for="item in visitedViews" :key="item.path" :class="['tags-item', { active: activitedView.path === item.path }]" @click="onChange(item)" >
        <span class="tags-item-title">{{ item.meta ? item.meta.title : '' }}</span>
        <el-icon v-if="item.path !== '/'" @click.stop="onDel(item)"><Close /></el-icon>
      </div>
    </div>
  </el-scrollbar>
</template>

<style scoped lang='scss'>
.tags-scrollbar {
  height: 30px;
  overflow: hidden;
}
.tags {
  display: flex;
  background: #f3f3f3;
  border: 1px solid #f2f2f2;
  border-right: none;
  margin: -1px 0 0 -1px;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  &-item {
    display: flex;
    align-items: center;
    position: relative;
    cursor: pointer;
    height: 26px;
    line-height: 26px;
    border: 1px solid #d8dce5;
    color: #495060;
    background: #fff;
    padding: 0 8px;
    font-size: 12px;
    margin-left: 5px;
    margin-top: 4px;

    &-title {
      margin-right: 4px;
    }

    &:first-of-type {
      margin-left: 5px;
    }
    &:last-of-type {
      margin-right: 5px;
    }
    &.active {
      background-color: #e25050;
      color: #fff;
      border-color: #e25050;
      &::before {
        content: '';
        background: #fff;
        display: inline-block;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        position: relative;
        margin-right: 2px;
      }
    }
  }
}
</style>

pinia的使用比vuex简单了很多,最大的区别就是*mutations*不再存在了。

路由拦截

一般我们登录之后会将pinia中关于用户的信息进行更新,还会将一些信息进行加密之后存放在localstorage中。未登录的用户我们得拦截他们进入系统,并重定向到登录页面。(实际项目中还得考虑页面权限的拦截问题)

// router/hooks/index.ts
import type { Router } from 'vue-router'
import { USERINFO } from '@/constants/localstorage'

const routerHook = (router: Router) => {
  router.beforeEach(to => {
    if(to.path === '/login') {
      // 可以做一些清空登录信息的操作, 比如跟pinia相关的等操作
      localStorage.removeItem(USERINFO);
      return true;
    }else{
      // 在这里可以判断用户是否登录,跳转的某个页面是否有权限,这里只是粗略写一下
      const info = localStorage.getItem(USERINFO);
      if(!info) {
        return { name: 'Login' }
      }
    } 
  })
}

export default routerHook;


import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import routerHook from './hooks/index'

// 注册路由
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

routerHook(router)

export default router

修改一下登录页面,模拟一下:

const onSubmit = async () => {
  await formRef.value.validate(async (valid: boolean, fields: {[key: string]: any}) => {
    if (valid) {
      // 一般这种情况下,localStorage中存储的信息不能太重要,且需要加密,还应该更新pinia中的用户信息
      const info = {
        name: 'Armouy'
      }
      localStorage.setItem(USERINFO, JSON.stringify(info))
      router.push('/')
    } else {
      console.log('error submit!', fields)
    }
  })
}

未登录情况下都会重定向到登录页面。

打包

npm run build:production
npm run preview

参考链接

  • Eslint:vue3项目添加eslint(standard规则)
  • vite官网

如有错误,欢迎指出,感谢阅读~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/696869.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全志V3S嵌入式驱动开发(spi-nor image制作)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 其实&#xff0c;我们之前就讨论过怎么把image烧入到v3s的spi-nor当中去。当时使用的方法是借助于sunxi-fel工具&#xff0c;烧入的image也比计较小…

2. 计算点到平面的投影

参考 https://www.cnblogs.com/nobodyzhou/p/6145030.html 所以用各种平面提取算法后&#xff0c;得到的平面方程, 注意此处的平面方程abcd已经是归一化了。 a x b y c z d 0 ax by cz d 0 axbyczd0 此时的原点到该平面点的投影点为 C P ( − a d , − b d , − c …

渗透测试内网基础知识

点赞后看&#xff0c;养成习惯 喜欢的话 可以点个关注哟 你们的点赞支持对博主们来说很重要哦 &#xff01;&#xff01;&#xff01; 为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 内网概述02 工作组03 域04 活动目录05 域控制器和活动目录的…

【C/C++】使用类和对象 设计点和圆的关系案例

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

误删除boot目录 修复办法

误删除boot目录 修复办法 本次演示系统为kylin-v10-sp3-20230324&#xff0c;kernel&#xff1a;4.19.90-52.22.v2207.ky10.x86_64 文章目录 误删除boot目录 修复办法1.报错现象2.正常机器boot目录结构3.误删除boot目录4.故障修复挂载光盘&#xff0c;进入救援模式 5.非正常情况…

mysql8.0.26忘记密码后重置(centos8)

1、查看配置文件地址 mysql --help|grep my.cnf 2、编辑/etc/my.cnf配置文件 vim /etc/my.cnf增加跳过密码配置 [mysqld] skip-grant-tables 3、重启mysql service mysqld restart4、修改密码 use mysql; update user set authentication_string where userroot; FLUSH pr…

NoSQL之Redis配置与优化(初级)理论较多

目录 一、关系数据库与非关系型数据库 1、 关系型数据库 2、 非关系型数据库 二、关系型数据库和非关系型数据库区别 1、 数据存储方式不同 2、 扩展方式不同 3、 对事务性的支持不同 三、非关系型数据库产生背景 1、可用于应对Web2.0纯动态网站类型的三高问题&#xf…

大数据Doris(五十一):Export语法和结果

文章目录 Export语法和结果 一、语法 二、结果 Export语法和结果 一、语法 Export 需要借助 Broker 进程访问远端存储&#xff0c;不同的 Broker 需要提供不同的参数&#xff0c;这里以导出到HDFS为例介绍Export 写法&#xff0c;也可以通过"help export "命令查…

Azure CLI - Premium SSD V1磁盘迁移至Premium SSD V2

目录 1.从服务器卸载Premium SSD V1 磁盘 2.创建Premium SSD V1磁盘snapshot 3.创建Premium SSD V2磁盘 4.挂载Premium SSD V2磁盘到服务器 已经在生产使用的Premium SSD V1磁盘&#xff0c;通过制作快照后&#xff0c;购买Premium SSD V2磁盘。 1.从服务器卸载Premium SSD…

Intellij IDEA detected unusually high memory use

一、IDEA如图 二、解决 2.1 修改IDEA的内存堆 运行IntelliJ IDEA的Java虚拟机&#xff08;JVM&#xff09;分配一些预定义的内存量。默认值取决于平台。如果您遇到速度减慢&#xff0c;可能需要增加内存堆。 为运行IDE分配的堆大小与编译应用程序的堆大小不同。如果要为编译代…

服务行业的企业所得税高,这是为何,该如何解决?

服务行业的企业所得税高&#xff0c;这是为何&#xff0c;该如何解决&#xff1f; 《税筹顾问》专注于园区招商、企业税务筹划&#xff0c;合理合规助力企业节税&#xff01; 服务行业的发展历程&#xff0c;源远流长。它是为了满足我们生活中的需要应运而生&#xff0c;大家眼…

广州华锐互动:建筑施工VR虚拟仿真实训系统

VR技术在施工现场安全教育中有着广泛的应用。随着VR技术的不断发展&#xff0c;越来越多的企业开始将其应用于施工现场安全教育中&#xff0c;以提高工人的安全意识和应对能力。 传统的施工现场安全教育往往需要大量的时间和人力物力投入&#xff0c;而且效果并不理想.而VR技术…

生物数据下载

目录 1. 获取数据下载的地址 2. 生物数据常用的下载站点 1、核酸数据库 2、非编码RNA数据库 &#xff08;1&#xff09;.非编码小RNA数据库 &#xff08;2&#xff09;.长非编码RNA数据库&#xff1a; &#xff08;3&#xff09;.非编码RNA家族数据库 &#xff08;4&a…

C++ day41

1、总结类和结构体的区别 结构体中是变量和函数&#xff1b;类中是成员属性(成员变量)、成员方法(成员函数)/行为类的成员变量和成员函数的默认访问权限是私有的&#xff1b;结构体的成员变量和成员函数的默认访问权限是公开的类创建一个对象后&#xff0c;对该对象的任何修改…

2022年全国硕士研究生入学统一考试管理类专业学位联考数学试题——纯题目版

2022 年全国硕士研究生入学统一考试管理类专业学位联考数学试题 一、问题求解&#xff1a;第 1∼15 小题&#xff0c;每小题 3 分&#xff0c;共 45 分。下列每题给出的 A、B、C、D、E 五个选项中&#xff0c;只有一项是符合试题要求的&#xff0c;请在答&#xff0e;题&#…

PC市场的寒冬,该如何看待?

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

prometheus告警发送到钉钉群机器人的全部署流程

前置条件&#xff1a;prometheus安装完成&#xff0c;创建钉钉群机器人&#xff0c;我这里使用的是指定ip的方式 定义告警规则 修改Prometheus配置文件prometheus.yml,添加以下配置&#xff1a; rule_files:- /usr/local/prometheus/rules/*.rulesalerting:alertmanagers:- s…

Spring、Bean 创建 和 使⽤

目录 前言 1、创建 Spring 项目 1.1、创建一个 Maven 项目 1.2、在 Maven 项目中&#xff0c;添加 Spring 框架支持&#xff08;spring-context&#xff0c;spring-beans&#xff09; 1.2.1添加依赖 1.2.2 配置国内源 1.2.3 添加maven 1.3、创建一个启动类 和 main 方法…

Mybatis 操作数据库的基本 CRUD 以及查询操作详析

目录 1. 什么是 MyBaits &#xff1f; 2. 搭建 MyBaits 环境 3. 了解 MyBaits 设计模式 MyBaits 操作数据库&#xff1a; 前置工作&#xff1a; &#xff08;1&#xff09;建库建表 &#xff08;2&#xff09; 添加实体类 &#xff08;3&#xff09;添加 Mapper 接口 &#xff…

CPU上下文切换原理剖析

CPU上下文 CPU上下文其实是一些环境正是有这些环境的支撑&#xff0c;任务得以运行&#xff0c;而这些环境的硬件条件便是CPU寄存器和程序计数器。CPU寄存器是CPU内置的容量非常小但是速度极快的存储设备&#xff0c;程序计数器则是CPU在运行任何任务时必要的&#xff0c;里面…