Loading...
墨滴

当归归

2022/01/13  阅读:96  主题:全栈蓝

万字逐步搭建基于Vue3的团队前端脚手架

万字逐步搭建基于Vue3的团队前端脚手架

这么香的Vue3怎么能不用呢。

前言

由于 vue3.2 版本的发布,<script setup> 的实验性标志已经去掉,这说明这个语法提案已经正式开始使用,Vite也发布了2.0版,完全可以用于生产环境

下面我将从 18 个方向,逐渐搭建一套属于自己的脚手架,制定一套合理的解决方案,为项目打下良好的基础。

目录

  1. 创建模板
  2. 环境配置
  3. 项目目录结构
  4. 集成UI库
  5. 样式
  6. 代码风格与规范
  7. Vite常用配置
  8. 页面布局
  9. 路由配置
  10. 状态管理
  11. 全局变量
  12. 加载外部配置
  13. 全局Api管理
  14. 网络请求
  15. 主题配置
  16. 工具类
  17. 插件
  18. 文档

脚手架搭建

1.使用Vite提供的模板

官方已提供完整的Vue+ts模板,省去了自己添加各种依赖

yarn create vite my-vue-app --template vue-ts

接下来在此基础上做加法即可

2.环境配置

我们需要通过不同的命令来启动和构建不同环境的包,比如

yarn start-dev
yarn start-test
yarn build-prod
2.1 环境变量配置

vite会从.env文件中加载配置好的环境变量,然后在import.meta.env对象上暴露环境变量。

我们可以在配置文件vite.config.ts中指定加载的env文件夹

import { defineConfig } from 'vite'
export default () => defineConfig({
  envDir'viteEnv'
})

然后在项目根目录创建viteEnv文件夹,再创建三个文件分别对应不同环境

.env               VITE_PROJECT_ENV=development
.env.test          VITE_PROJECT_ENV=testing
.env.production    VITE_PROJECT_ENV=production

然后在项目中可以通过import.meta.env.VITE_PROJECT_ENV来获取当前环境

最后修改package.json

"scripts": {
  "start-dev""vite",
  "start-test""vite --mode testing",
  "start-prod""vite --mode prod",
  "build-dev""vite build",
  "build-test""vite build --mode testing",
  "build-prod""vue-tsc --noEmit && vite build --mode prod",
},

注意: vue-tsc --noEmit是用来检查ts类型的,会影响打包速度,如不需要可以删除

2.2 公共基础路径

因为项目可能会部署在子路径下,所以要支持根据不用的环境来配置公共基础路径

踩坑:Vite在构建阶段是无法通过import.meta.env来获取环境变量的

解决方案 使用loadEnv

// config/publicBase.ts
export default (env: string) => {
  switch (env) {
    case 'develop':
      return './'
    case 'testing':
      return './'
    case 'product':
      return './'
    default:
      return './'
  }
}

// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import publicBase from './src/config/publicBase'
export default ({ mode }) => defineConfig({
  base: publicBase(loadEnv(mode, 'viteEnv').VITE_PROJECT_ENV)
})
2.3 公共接口请求前缀

正常来说可以使用import.meta.env获取环境变量然后配置对应的请求前缀即可,但团队想做到通过加载外部配置来获取请求前缀,这里留到后边讲

3.项目目录结构

项目目录结构需符合公司的规范,一股脑儿新建就行,用的时候再配置

├── src                # 项目源码
│   ├── resources      # 项目公用资源
│   │   ├── stylus     # 样式
│   │   ├── images     # 图片
│   ├── config         # 公共配置
│   ├── constant       # 项目全局静态变量
│   ├── components     # 公共组件
│   ├── hooks          # 公共Hooks
│   ├── stores         # 状态管理
│   ├── services       # api管理
│   ├── pages          # 项目模块
│   ├── router         # 路由配置
│   ├── types          # ts类型配置
│   ├── utils          # 工具类
│   ├── App.vue        # 入口模块
│   ├── main.ts        # 入口文件
│   ├── env.d.ts       # 环境变量适配定义
├── package.json       # 项目依赖,启动脚本等
├── README.md          # 项目说明
├── tsconfig.json      # ts配置
└── vite.config.ts     # vite配置

4.集成UI库

目前支持vue3的库还不多,绝大多数vue2项目都用的Element,实际上Element-plus是支持vue3的版本,但是尤大推荐Naive UI,试用了一把觉得很不错,作者说该库只支持vue3,风格也接近Antd,组件非常完善。

4.1 安装

Naive UI 官方中文文档

注意了 安装到生产就够了

yarn add naive-ui -D

字体

yarn add vfonts -D
4.2 按需引入 自动引入

常规的按需引入实际上配置起来也麻烦,我在用哪些组件的时候还得去入口文件手动引入

使用unplugin-vue-components实现自动引入

yarn add unplugin-vue-components -D
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'

export default () => defineConfig({
 plugins: [
   vue(),
   Components({
     resolvers: [NaiveUiResolver()]
   })
  ],
})

使用的时候直接使用即可,无需引入

<template>
  <n-button>按钮</n-button>
</template>

注意: 此方式使用组件会失去props的类型检查,且不支持渲染函数渲染

5.样式

vite内置支持sasslessstylus 所以只需安装即可,此脚手架使用less

5.1 安装
yarn add less less-loader -D

直接使用即可

<style lang="less" scoped>
</style>
5.2 全局样式文件

顾名思义 在全局都生效的样式,可以用来添加全局样式或者覆盖组件样式

新建src/resources/styles/global.less文件

body {
 margin0;
 width100vw;
 height100vh;
}
....

main.ts中引入

import './resources/styles/global.less'

完成 全局生效

5.3 公共样式文件

比如公共的主题样式、变量、less函数之类的,由于预处理语法会在构建时转变成原生css,直接全局引入然后在局部使用的时候会找不到此函数从而报错,所以需要用到less-loader插件,前面已经安装过了。 新建src/resources/styles/base.less文件

// Example flex盒子居中函数
.y-center() {  
   display: flex;
   align-items: center;
   justify-content: center;
}

vite.config.ts

import { defineConfig } from 'vite'
export default () => defineConfig({
css: {
  preprocessorOptions: {
    less: {
      javascriptEnabledtrue,
      // 结尾的分号不能少
      additionalData`@import "${path.resolve(__dirname, 'src/resources/styles/base.less')}";`
    }
  }
 }
})

最后在局部直接使用即可

5.4 Tailwind.css

如果你选择Tailwind,CSS 预处理器的作用就会显得微乎其微,因为你无需再自定定义各种变量和mixins。有一定的上手成本,没有熟练使用还会降低效率,一旦熟悉之后会大大提高你的效率。

再三思索后并没有集成Tailwind,理由是UI库提供的组件已经完善了样式,某些小修改使用传统CSS也比较简单。

6.代码风格与规范

统一的代码风格和规范在团队开发中是很有必要的,比如基本的格式化工具,换同事的电脑格式化一下,由于规则不一样,代码就会乱套。就算是独立开发,在不同项目中切换也是有必要统一的。

6.1 Prettier

Prettier 是一款代码格式化工具 Vite脚手架中似乎已自带

yarn add prettier -D

在你的开发工具中也要安装该扩展并启用,并将默认的格式化改成prettier

在根目录新建文件 .prettierrc 配置好规则

{
  "singleQuote"true,
  "trailingComma""none",
  "arrowParens""avoid",
  "bracketSpacing"true,
  "printWidth"800,
  "proseWrap""never",
  "tabWidth"3,
  "jsxSingleQuote"true,
  "jsxPrintWidth"800,
  "htmlWhitespaceSensitivity""ignore",
  "semi"false,
  "quoteProps""as-needed",
  "overrides": [
    {
      "files"".prettierrc",
      "options": {
        "parser""json"
      }
    }
  ]
}

最后在根目录新建文件.prettierignore排除不需要格式化的文件

**/*.md
**/*.svg
**/*.ejs
**/*.html
package.json

6.2 Eslint

使用eslint来规范你的代码并提供自动检验代码的程序

配置过程异常麻烦,不同的技术栈有不同的配置方式,不同的版本也有不同的方式,官方甚至有很多破坏性更新导致之前的不能用,网上的教程也是一个比一个不靠谱,研究许久终于搞定。

先安装

yarn add eslint -D

然后在开发工具中安装eslint插件,以在开发中自动进行eslint校验 并且在开发工具设置中进行如下设置,我用的是vscode,此配置的目的是开启eslint,配置支持的语言,以及保存文件自动修复错误。

"eslint.enable"true,
"eslint.validate": [
  "javascript",
  "typescript",
  "javascriptreact",
  "typescriptreact",
  "vue-html"
],
"eslint.run""onType",
"editor.codeActionsOnSave": {
  "source.fixAll.eslint"true
 }

安装官方提供的Vue插件,可以检查 .vue文件中的语法错误

yarn add eslint-plugin-vue -D

修改eslintrc文件

// .eslintrc.js
...
extends: [
  'plugin:vue/vue3-recommended' // ++
]
...

为了规范团队成员代码格式,以及保持统一的代码风格,使用当前业界最火的Airbnb规范

yarn add eslint-config-airbnb-base -D

修改eslintrc文件

// .eslintrc.js
...
extends: [
  'plugin:vue/vue3-recommended',
  'airbnb-base' // ++
]
...

使用eslint插件将prettier作为eslint规则执行

yarn add eslint-plugin-prettier -D

修改eslintrc文件

// .eslintrc.js
...
plugins: ['prettier'], // ++
rules: {
  'prettier/prettier''off' // ++
}
...

配置到此时,大概率会遇到eslint规则和prettier规则冲突的情况,比如eslint告诉我们要使用单引号,但是改为单引号以后,prettier又告诉我们要使用双引号。 这时候就需要另一个eslint的插件eslint-config-prettier,这个插件的作用是禁用所有与格式相关的eslint规则,也就是说把所有格式相关的校验都交给prettier处理。

yarn add eslint-config-prettier -D

修改eslintrc文件

// .eslintrc.js
...
plugins: ['prettier'],
extends: [
  'plugin:vue/vue3-recommended',
  'airbnb-base',
  'plugin:prettier/recommended'// ++
],
rules: {
  'prettier/prettier''off'
},
...

plugin:prettier/recommended 的配置需要注意的是,一定要放在最后。因为extends中后引入的规则会覆盖前面的规则。

到此配置结束 我们可以根据业务加入自定义的规则

rules: {
  'prettier/prettier''off',
  // 允许使用console.log
  'no-console''off',
  // 省略引入的后缀名
  'import/extensions': [
    'error',
    'always',
    {
      js'never',
      mjs'never',
      jsx'never',
      ts'never',
      tsx'never'
    }
  ],
  // 当只有一个导出时允许不使用default导出
  'import/prefer-default-export': [0],
  // 引入各种模块
  'import/no-extraneous-dependencies': ['error', { devDependenciestrue }],
  // 允许修改函数中对象参数中的value
  'no-param-reassign': ['error', { propsfalse }],
  // 函数返回值除非用圆括号括起来,否则不允许赋值
  'no-return-assign': ['error''except-parens'],
  // 允许未被使用的声明
  'no-unused-vars''off',
  // 允许使用各种表达式
  'no-unused-expressions''off',
  // 允许++ --
  'no-plusplus''off',
  // vue prop相关
  'vue/require-default-prop''off'
}

最后在package.json中加入命令,用来在命令行中手动检查项目,报错信息会在命令行提示

"scripts": {
  "lint""eslint src --ext .js,.vue,.ts",
}
6.3 Git提交规范

奈何公司不用Git所以这块暂时没加上去了

7. Vite常用配置

Vite官方中文文档 可以根据官方文档来配

7.1 别名配置

vite.config.ts

resolve: {
  alias: [{ find'@'replacement: path.resolve(__dirname, 'src') }]
}

然后就可以使用@/src来访问了,但是esling会报错,我们需要修改eslintrc文件

settings: {
  'import/resolver': {
    alias: {
      map: [['@''./src']],
      extensions: ['.ts''.js''.jsx''.json''.vue']
    }
  }
}

这样就完美了

7.2 开启IP访问、指定端口

vite.config.ts

server: {
  host'0.0.0.0',
  port3000
}

其他的太多就不讲了

8.页面布局

页面整体布局是一个产品最外层的框架结构,往往会包含导航、页脚、侧边栏等。在页面之中,也有很多区块的布局结构。在真实项目中,页面布局通常统领整个应用的界面,有非常重要的作用,所以单独拆分出来也是非常有必要的,代码就不贴了

主要功能

  • Layout布局 支持自定义 可控
  • 自定义标题、Logo、操作栏
  • 支持自动切换暗黑模式 可控
  • 支持自动渲染路由列表为侧边栏

9.路由配置

安装官方提供的vue-router就行,由于我们使用的是vue3对应路由版本是4

yarn add vue-router@4

main.ts中引入

import router from './router'

const app = createApp(App)
app.use(router)
9.1 创建路由

新建router/route.ts文件 配置路径 项目中写了一个404后跳转的页面Error

import { RouteRecordRaw } from 'vue-router'
import Home from '../pages/Home/index.vue'
import Error from '../pages/Error/index.vue'

const routes: RouteRecordRaw[] = [
   { path'/'component: Home },
   // 未找到路由就会跳转到此页面
   { path'/:pathMatch(.*)'componentError }
]
export default routes

新建router/index.ts文件

import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './route'

const router = createRouter({ history: createWebHashHistory(), routes })
export default router
9.2 路由扩展

路由会在布局组件中被渲染成侧边栏,所以我们要扩展一下Meta,支持自定义icon,title,以及是否显示在侧边栏。相关的功能代码已在布局组件中封装好。

修改router/index.ts文件

declare module 'vue-router' {
   interface RouteMeta {
      icon?: any
      title?: string
      hideLayout?: boolean
   }
}

对应的路由配置

import { Home as HomeIcon } from '@vicons/ionicons5'

const routes: RouteRecordRaw[] = [
   { path'/'component: Home, meta: { title'Home'icon: HomeIcon } },
   { path'/:pathMatch(.*)'componentErrormeta: { hideLayouttrue } }
]
9.3 加载进度条

在路由切换的时候加载浏览器的进度条 提升视觉上的体验

$loadingBar怎么来的在后面会讲到

修改router/index.ts文件

路由跳转时加载进度条,路由完成后结束

import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './route'

const router = createRouter({ history: createWebHashHistory(), routes })
router.beforeEach((to, from, next) => {
   if (window.$app_global.$loadingBar) window.$app_global.$loadingBar.start()
   next()
})
router.afterEach(to => {
   if (window.$app_global.$loadingBar) window.$app_global.$loadingBar.finish()
})
export default router
9.4 修改浏览器标题

在路由切换的时候修改浏览器标题为当前路由的标题 修改router/index.ts文件

import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './route'

const router = createRouter({ history: createWebHashHistory(), routes })

router.beforeEach((to, from, next) => {
   if (to.meta.title) {
      document.title = to.meta.title as any
   }
   next()
})
export default router
9.5 路由动画

切换路由的时候加入过渡动画提升视觉体验 在全局样式中写控制动画的css

/* global.less */
.fade-slide-leave-active,
.fade-slide-enter-active {
 transition: all 0.3s;
}
.fade-slide-enter-from {
 opacity0;
 transformtranslateX(-30px);
}
.fade-slide-leave-to {
 opacity0;
 transformtranslateX(30px);
}

修改App.vue

<router-view v-slot="{ Component }">
  <transition name="fade-slide" mode="out-in" appear>
    <component :is="Component" />
  </transition>
</router-view>
10. 状态管理Pinia

使用Pinia替代Vuex。 和Vuex差不多 比它更简单 尤大说以后就是官方库

安装

yarn add pinia 

main.ts中引入

import { createPinia } from 'pinia'

const app = createApp(App)
const pinia = createPinia()
app.use(pinia)

创建store/index.ts文件

import { defineStore } from 'pinia'
// 必须声明defineStore的第一个参数或在第二个参数中声明id 不然找不到store
export const useDemoStore = defineStore('store_demo', {
  // id: 'store_demo', 
  state() => ({
   name'当归',
  }),
  getters: {
    getNamestate => state.name + 'HAHA',
  },
  actions: {
    setName(name: string) {
      this.name = name
    }
  }
})

使用方式

<script setup lang='ts'>
import { useDemoStore, storeToRefs } from '@/stores'

const demoStore = useDemoStore()
// 使用state
const name = demoStore.state
// 使用getters
const getName = demoStore.getName
// 使用actions
demoStore.setName('biu biu biu')
// 使用storeToRefs解构会将解构后的值变为响应式
const nameRef = storeToRefs(demoStore).getName
</script>
11.全局变量

vue3 取消了挂载原型链的定义全局变量方式,由于替代方法过于繁琐所以在 window 对象中维护了一个全局变量 $app_global

修改src/env.d.ts文件

declare interface Window {
 $app_global: {
  ...
 }
}
12.加载外部配置

加载外部配置的方式可以将全局请求前缀以及各种可控的服务放在外部配置中,以便上了生产后可以修改后实时生效,不需要打包。 我们可以用过自己编写Vite插件来实现,原理是在Vite最终构建完输出Html文件的时候往里面插入我们用来加载配置文件的函数,然后将配置注入到window对象中以便全局访问. 具体如何实现,请看源码。 配置文件如下

window.$app_global.config = {
   // 项目名称
   name'Vite-App',
   // sso相关配置
   sso: {
      enablefalse,
      ssoConf: {
         mapping'/security/SecurityService',
         url'https://chief-te.yifengx.com/fire/fire-sso'
      }
   }
}

base.develop.jsonbase.testing.jsonbase.product.json

{
 "baseUrl""https://yfapi-te.pharmacyyf.com/fire/fire-example/boss",
 "ossConf": {
  "url""https://fire.pharmacyyf.com/oss/FileService/apply",
  "merchantId""YFX1326BqMdXsTEST",
  "appId""fire-example"
 }
}

通过插件解析后,会根据不同的环境去加载对应的配置文件

13.全局Api管理

由于我们已经获取到了全局api前缀

新建services/index.ts文件

export const EXAMPLE_URL = `${window.$app_global.base.baseUrl}/example`

在实际开发中应该对应不同的模块创建文件然后统一在index中导出

export * from './company'
export * from './user'
...
14.网络请求
14.1 基于 axios 的封装

安装

yarn add axios

新建config/config.ts文件,使用Class来封装功能代码,配合外部配置文件来开启对应功能

由于业务需要 这里加入了长整型类型转字符串类型的功能parseJsonData

import axios, { AxiosRequestConfig } from 'axios'

class Config {
   constructor(prop?: Props) {
      const handleError = prop?.handleError === undefined ? true : prop.handleError
      Config.initAxios()
   }
   // 配置axios
   static initAxios() {
      axios.defaults.timeout = 20000 // 请求超时时间
      axios.defaults.withCredentials = true // 请求携带缓存
      axios.defaults.transformResponse = data => {
         if (data.indexOf('Proxy') === 0) {
            return data
         }
         return parseJsonData(data)
      }
   }
}

export default Config

14.2 统一拦截错误
static interceptorAxiosError() {
  axios.interceptors.response.use(
      (config: any) => {
         return config
      },
      error => {
        if (!window.$app_global.$message) return Promise.reject(error)
        if (error.message.includes('timeout')) {
            window.$app_global.$message.error('网络请求超时')
            return false
        }
        if (error.message.includes('404')) {
            window.$app_global.$message.error('网络请求失败')
            return false
        }
        if (error.message.includes('500')) {
           const message = error.response.data.message || '系统异常,请联系管理员。'
           window.$app_global.$message.error(message)
           return false
        }
        window.$app_global.$message.error(error.response.data.message)
        return Promise.reject(error)
        }
    )
   }
14.3 封装更方便的网络请求Hooks

设想一下,编写请求代码时,我们通常会定义这么几个变量:

  • data: 储存请求数据
  • loading: 请求加载状态 尤其是 loading,我们需要在请求前设置为 true,请求结束后设置为 false。

上面的封装方式,是对基础的功能封装,因为我们在使用 vue3,所以可以进行再一次的封装成为 hook,我们使用起来会更加方便。

例如下面这个样子:

<script setup lang='ts'>
import { COMPANY_LIST } from '@/services'
import useRequest from '@/hooks/useRequest'

const { data, run, loading } = useRequest(COMPANY_LIST, { 
   manualtrue,
   onSuccessres =>{
      console.log(res)
   }
})

目前已经支持以下功能

  • 自动请求
  • 手动请求
  • 并行请求
  • 返回loading状态
  • loading延迟
  • 成功 / 错误回调函数
  • 传参,返回数据的类型推断
  • 防抖
  • 节流
  • 表格分页请求
15.组件封装

将业务抽象化能大大提高开发效率

15.1 布局组件

已经介绍过了

15.2 表格组件

如果是管理后台的话,十个页面就有十个表格,将查询、表格、请求、分页、甚至新增和编辑都封装成一个组件那岂不是很爽。 具体代码就不贴了,看文档吧。

15.3 通用功能组件

比如messagedialognotification组件,其实UI库已经有现成的了,但是调用起来还是不方便,封装一下会方便很多,最终效果

window.$app_global.$message直接使用

15.4 主题配置

提供默认和暗黑主题配置

config/theme.ts文件

import { GlobalThemeOverrides } from 'naive-ui'

export const light: GlobalThemeOverrides = {
   common: {
      errorColor'#FF4D4F',
      errorColorHover'#FF7874'
   },
   Layout: {
      color'#F5F7F9'
   },
   Card: {
      paddingMedium'20px'
   }
}

export const dark: GlobalThemeOverrides = {
   common: {
      errorColor'#FF4D4F',
      errorColorHover'#FF7874'
   },
   Layout: {
      color'#101014'
   },
   Card: {
      paddingMedium'20px'
   }
}
16. 工具类

规定utils目录为工具类

因为Vue3中经常用到需要使用js渲染组件,所以需要封装常用的渲染函数,比如一个带有确认提示的删除按钮,常用在表格中

新建utils/render.ts

import { NPopconfirm, NButton } from 'naive-ui'

export const renderPopconfBt = (text: string, content: any, callBack: () => void) => {
   return h(NPopconfirm, { onPositiveClick: callBack }, { trigger() => content, default() => text })
}

export const renderRemoveBt = (callBack: () => void, loading?: boolean | undefined) => {
   const bt = h(NButton, { type'error'size'small', loading }, { default() => '删除' })
   return renderPopconfBt('你确定要删除吗', bt, callBack)
}

然后utils/index.ts中导出

export * from './render'
17. 插件
vscode插件

推荐使用Volar来替代Vetur

chrome插件

Vue.js devtools,要安装支持 vue3 的版本,而且此版本对 pinia 支持的也非常友好

18. 文档

做到这一步,整个脚手架开发已经接近于尾声,但是做了这么多,别人并不知道如何使用,甚至自己过一段时间也会忘记,所以必须养成良好的编写文档习惯。

文档地址

使用该脚手架开发,文档部分使用Markdown语法,自己写了一个Markdown解析工具

19.源码地址

脚手架源码

结尾

大多都是根据当前业务选择,属于班门弄斧,很多细节没有弄好,尝试一下前沿技术也不是啥坏事。

当归归

2022/01/13  阅读:96  主题:全栈蓝

作者介绍

当归归