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

安装基础包

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,所以我得降级一下:[email protected],在配置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
/// 

关于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

所以新建以下文件:

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

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

  • 在编写路由的时候我们引入组件,必须有.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中日期等组件默认是英文,所以我们把组件改为中文:

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

修改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>

这样就会出现中文了。

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

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

  • 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>

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

自动按需引入的原理是通过识别