万字长文详解从零搭建企业级 vue3 + vite2+ ts4 框架全过程

12,155 阅读34分钟

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

本文不仅仅是搭建个脚手架这么简单,还会带你了解每一步、甚至每一个配置项的作用,和每个配置的知识点

涉及到的知识点:Vue3、Vite、TypeScript、ESlint、Stylelint、Vue-router、Axios、Pinia

本文项目初始化时间为2022/2/19,运行初始化命令获得的 vite 最新版本为 2.8.0

整个搭建过程体系因为我想把每个点都写得详细一点,所以内容会很多,全文主要分为两个部分:

第一部分为开发环境及脚手架基本能力的搭建:

  • 基本环境准备
  • 初始化 vite项目
  • 扩展 vite 配置
  • 配置 TypeScript
  • 接入代码规范

第二部分为应用层能力的建设:

  • 接入 vue-router
  • 接入 Axios
  • 接入 UI 库
  • 接入 Pinia
  • 接入 Echarts5

源码地址:github.com/JasonLuox/f…

内容较多,笔者能力也有限,难免会有所遗漏和错误,欢迎在评论区指出,所有合理的批评和建议我都会采纳和表示感谢~

写在前面

早在去年年初的时候,在某乎上刷到了尤大的一条对前端变化的评论(时刻关注着尤大的一言一行对我们这些基于vue 的开发者还是很有好处的):

image-20220219135117705.png

那时好像也是正值 vite2.0 发布的时候,我满怀欣喜的就去体验了下,不得不说,在饱受了 webpack 速度的摧残之下,vite 简直是奇迹!

当时正好是负责项目的整个 UI 重构,对项目使用的整个前端框架体系都有了一定的认知和熟练,所以也尝试搭建了 vue3 + vite 的框架体系。当时踩了不少坑,众所周知在一门新技术出来的时候,很难通过搜索引擎搜索到相关的教程/报错解决方法,唯一有价值的参考就是官方文档和 issues。当时踩坑结束后其实就萌发了写一篇这样的文章的打算,不过拖延症让我成功拖到了现在。

这一年多来我也在各类公众号、博客里看到很多类似的文章。虽然现在写这篇文章有炒冷饭的嫌疑,但正好借着掘金活动的助力,还是打算将这篇鸽了已久的文章通过自己的能力去完成,而且要尽量呈现更多的细节。我已经准备好一台全新的环境,一边搭建一边撰写这篇文章,动手实操才是掌握能力/发现问题最快的方式!

旧项目升级建议

vue3 已经发布很长一段时间了(刚出的时候其实比较多人都是在选择观望的,现在都在积极拥抱,不得不说:用过都说好!),有着更快的速度,更高效的语法,生态圈也逐渐完善了起来,对中小型应用的开发完全不在话下。看着代码仓库里的 vue2 项目难免有了些嫌弃的想法,那么到底要不要对 vue2 的项目进行升级呢?

  • 如果所在公司有比较成熟的 UI 规范的话,一般 UI 规范都是基于某个特定 UI 库制定的,所以从 vue2 升级到 vue3 就需要考虑旧项目使用的 UI 库是否有提供 vue3 的兼容版本。

    这点比较坑,我一直想把公司项目升级到 vue3,奈何 UI 库的 vue3 适配版本还在开发中没有正式发布,所以现在只能等啦,不过框架落后,技术必须先行,开发人员的技术储备必须时刻保持在行业前沿才不会被淘汰!

  • 虽然 vue3 几乎是对 vue2 的全面降维打击,如果是大体量的产品,我更建议维持旧版本的开发,在新项目上再使用 vue3 + vite ,vue2 与 vue3 在语法、上手难度上都有着一定的差距,盲目的升级不但会加大人力、资源的投入(虽然对技术人员来说是正相关的,学到新技术,开发出更高效的代码),但也会增加对产品正常迭代延期的风险,要时刻保持认知:一个团队中不仅仅有技术开发人员,更是产品、设计、交互、测试等多方面的协作。

  • 当然以上只是个人的浅薄看法,具体情况还需要看各个技术团队对新技术的认知和投入,需要各个技术负责人对整体进行考量。

既然是从零开始,那当然得从node 装起了:

第一部分

零、安装 Nodejs

为什么要 nodejs ?

大家可能会感到奇怪,我们开发 vue 的,直接<script src="https://unpkg.com/vue@next"></script>引入不就能跑了,装 node 做什么?

(哈哈,还记得当年是一个后台开发问得我这个问题,当时还真没答上来)

Node.js 是能够在服务器端运行 JavaScript 的开放源代码、跨平台执行环境。

就像浏览器能够提供 JS 运行的环境一样,Nodejs 的作用和浏览器一致。

如果只是跑一个简单的 html ,或者只是在生产环境运行打包好的静态文件,确实是用不上 node.js 的。node.js 的作用其实是提供前端工程化的基石,为了方便你的开发,提高开发效率的。

比如 vue-cli 脚手架,比如常用的包管理工具npm,比如常用的打包工具 webpack/vite/rollup 等等,node.js 都能够提供对应工具/轮子的运行环境,这样就省去了大量的配置过程,对效率提升杠杠的。

兼容性

vite 需要 Node.js 版本 >= 12.0.0

安装

打开 nodejs 官网,我们就选择最新的长期维护版本进行下载(稳定才是一个规范的开发团队需要保证的东西,尝鲜版更适合单独奋战的人/狂热的技术爱好者,我用的是windows所以只能展示windows环境的安装了)

image-20220219145307229.png 安装挺简单的,直接无脑下一步就行了

这里会提示说有一些 npm modules 会依赖 c/c++ 的编辑环境,图省事的话也可以勾上,未来也许用的上

image-20220219152438428.png

安装结束后打开命令行(快捷键win + r后输入 cmd)输入node -v

image-20220219152803021.png

可以看到已安装成功的 node.js 版本,到此我们就安装完毕了!

看不到的需要去配置下环境变量(右键此电脑=>属性=>高级系统设置=>高级=>环境变量=>Path),添加一条对应安装目录\nodejs就可以了,比如我是默认安装的,就添加一条C:\Program Files\nodejs\

正常安装的话应该是会默认帮你添加到环境变量的,当然难免会有些意外发生需要手动添加。

安装cnpm

因为 npm 源毕竟在国外,用起来会比较慢,我们可以安装淘宝镜像 cnpm 提升安装依赖速度

npm install -g cnpm -registry=https://registry.npm.taobao.org

cnpm 的操作命令和使用方式和 npm 是完全一致的

安装完成后查看是否安装完成cnpm -v

image-20220219160152458.png 当然,还有其他包管理工具yarn、pnpm等等等等,选择一个自己喜欢用的就行

一、初始化 vite 项目

Home | Vite 官方中文文档

npm 7+直接运行npm create vite@latest my-vue-app -- --template vue-ts

image-20220219161837935.png

0. npm 低于 7+ 的建议升级下 npm,命令npm install npm -g 0. 也可以不输入后面的-- --template vue-ts参数进行手动选择 0. 使用 cnpm 运行这行命令指定项目名称和模板会不生效,很奇怪的Bug

可以看到,当前目录下就生成了一个名为 my-vue-app 的 vite 项目了,目录结构如下

​
├── public              静态资源
├── src
│   ├── assets           资源目录(图片、less、css等)
│   ├── components       项目组件
│   ├── App.vue          主应用
│   ├── env.d.ts         全局声明
│   └── main.ts          主入口
├── .gitignore           git忽略配置
├── index.html           模板文件
├── package.json        依赖包/运行脚本配置文件
├── README.md
├── tsconfig.json        ts配置文件
├── tsconfig.node.json   ts配置文件
└── vite.config.ts       vite配置

运行初始化成功后命令行给出的命令

cd my-vue-app
npm install
npm run dev

可以看到项目默认在3000端口运行,构建时间只花了 398ms:

image-20220219163528574.png

浏览器打开 http://localhost:3000 ,是标志性的 vue 首页和一个简单的累加器组件:

image-20220219163546736.png 初始化项目就完成了,当然,作为一个企业级应用肯定是不够格的,接下来的扩展才是重头戏。

二、拓展 vite配置

和我们熟悉的 webpack 不同,打开 vite 的配置文件vite.config.js可以发现,好像啥都没有:

image-20220219164547319.png 真正的开箱即用!当然,为了更好的扩展功能与更好的开发体验,可以在defineConfig函数里添加如下配置

公共基础路径 base

如果你需要在嵌套的公共路径下部署项目,只需指定 base 配置项,然后所有资源的路径都将据此配置重写。

由 JS 引入的资源 URL,CSS 中的 url() 引用以及 .html 文件中引用的资源在构建过程中都会自动调整,以适配此选项。公共路径默认为/,我们指定为当前项目根目录./即可

base: './'

开发服务器选项 server

当我们在没有任何配置的时候,在运行服务的时候,vite 是会自动跑在本地的3000端口,所以我们可以扩展下配置

server: {
    port: 4000, // 设置服务启动端口号,如果端口已经被使用,Vite 会自动尝试下一个可用的端口
    open: true, // boolean | string 设置服务启动时是否自动打开浏览器,当此值为字符串时,会被用作 URL 的路径名
    cors: true, // 为开发服务器配置 CORS,配置为允许跨域
​
    // 设置代理,根据我们项目实际情况配置
    proxy: {
        '/api': {
            target: 'http://127.0.0.1:8000',    // 后台服务地址
            changeOrigin: true, // 是否允许不同源
            secure: false,      // 支持https
            rewrite: path => path.replace(/^/api/, '')
        }
    }
}

proxy 代理使用的是 http-proxy,用法很简单,如上配置就是将127.0.0.1:4000/api的请求 url 替换成127.0.0.1:8000,是本地开发调试解决跨域问题很常用的一种方式。

别名 resolve.alias

在我们项目开发过程中,会有很多嵌套层级的目录,所以要找到某个目录经常用相对路径../../..,层级一多就显得眼花缭乱,通过 alIas 别名,我们可以快速地指定首层的目录,并且相比相对路径减少了路径索引的消耗,在性能上来说也是更优解

为了后续更好的文件管理和更加清晰的目录层级,我们现在在 src 下添加如下目录,每个目录的作用后文都会提及

├── src
│   ├── router           路由配置
│   ├── stores           状态管理
│   ├── typings          ts公共类型
│   ├── utils            工具类函数封装
│   └── views            页面视图

添加别名配置:

// 指定解析路径
import { resolve } from 'path'const pathResolve = (dir: string) => resolve(__dirname, dir)
​
resolve: {
    alias: {
        '@': pathResolve('./src'),  // 设置 `@` 指向 `src` 目录
         views: pathResolve('./src/views'), // 设置 `views` 指向 `./src/views` 目录,下同
         components: pathResolve('./src/components'),
         assets: pathResolve('./src/assets'),
    },
},

指定解析路径使用的 path module需要先安装@type/node,我们安装在开发环境即可:

npm install @types/node --save-dev

构建选项 build

当需要将应用部署到生产环境时,只需运行 npm run build 命令就会执行package.json对应的vue-tsc --noEmit && vite build命令,从而对项目中使用到的 typescript 类型进行校验,校验通过后执行打包功能

build: {
      outDir: 'dist',   // 指定打包路径,默认为项目根目录下的 dist 目录
      terserOptions: {
          compress: {
              keep_infinity: true,  // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的性能问题
              drop_console: true,   // 生产环境去除 console
              drop_debugger: true   // 生产环境去除 debugger
          },
      },
      chunkSizeWarningLimit: 1500   // chunk 大小警告的限制(以 kbs 为单位)
}

outDir 可以用来指定打包文件存放的路径,默认为项目根目录下的 dist 文件夹

terser 是一个用于 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。我们可以通过 terserOptions.compress 属性对 js 进行一定的压缩,减少打包文件体积。

chunkSizeWarningLimit默认为500,当打包后单个 chunk 体积超过500就会有警告,在实际项目中太小了,我们这里调整为1500

vite 的基本配置就到这了。

完整配置

vite.config.js目前的完整配置如下

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'const pathResolve = (dir: string) => resolve(__dirname, dir)
​
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    outDir: 'dist',     // 指定打包路径,默认为项目根目录下的 dist 目录
    terserOptions: {
      compress: {
        keep_infinity: true,  // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的性能问题
        drop_console: true, // 生产环境去除 console
        drop_debugger: true // 生产环境去除 debugger
      },
    },
    chunkSizeWarningLimit: 1500 // chunk 大小警告的限制(以 kbs 为单位)
  },
  resolve: {
    alias: {
      '@': pathResolve('./src'), // 设置 `@` 指向 `src` 目录
      views: pathResolve('./src/views'),
      components: pathResolve('./src/components'),
      assets: pathResolve('./src/assets'),
    },
  },
  base: './', // 设置公共基础路径
  server: {
    port: 4000, // 设置服务启动端口号
    open: true, // 设置服务启动时是否自动打开浏览器
    cors: true, // 允许跨域
​
    // 设置代理,根据我们项目实际情况配置
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000',
        changeOrigin: true,
        secure: false,
        rewrite: path => path.replace(/^/api/, '')
      }
    }
  }
})
​

三、配置 TypeScript

TypsScript 简介

TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale

TypeScript 是基于Javascript 的强类型编程语言,对任何规模的项目都能提供更好的工具化体验。

注:以上描述是官网对于 TypeScript 的定义。

其实偶尔也能看到一些人讨论:小型项目或者业务场景特别简单只需要一两个前端开发就能完全开发的项目还有必要用 typescript 吗?不会平白添加开发成本吗?

其实作为一名成熟的业务开发人员,会发现用到的前端技术也就来来回回那几样,所以当你在团队中发现很难在项目中学到什么的时候,除了跳槽,也可以考虑往项目中加一点新的技术,比如 typecsript,不但能减少bug的产出,还能提升自己的知识储备,为以后接触多人协作的大型项目做铺垫,何乐而不为呢?

而且在 vite 项目中:

  • Vite 天然支持引入 .ts 文件。
  • Vite 使用 esbuild 将 TypeScript 转译到 JavaScript,约是 tsc 速度的 20~30 倍,同时 HMR 更新反映到浏览器的时间小于 50ms。

ts 只要有一定 js 基础入门也很简单,以下网站是我觉得还不错的入门教程供大家学习

tsconfig 文件详解

第二步初始化 vite 项目的时候可以看到,项目根目录下多出了 tsconfig.jsontsconfig.node.json两个文件

顾名思义,这个文件就是指定了用来编译这个项目的根文件和编译选项。(为啥多了个tsconfig.node.json?下文会解释道)

当我们使用

  • 不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。
  • 不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。

当命令行上指定了输入文件时,tsconfig.json文件会被忽略。

还记得我们运行npm run build打包时会执行的脚本命令吗?(在 package.json 里)

"build": "vue-tsc --noEmit && vite build",

vue-tsc是一个基于 volar 的 vue3 命令行类型检查工具,我们也是可以通过tsconfig.json去配置vue-tsc --noEmit需要检查的内容和方式。

打开tsconfig.json看看,有一些基础的配置:

{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"]
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

tsconfig.node.json

{
  "compilerOptions": {
    "composite": true,
    "module": "esnext",
    "moduleResolution": "node"
  },
  "include": ["vite.config.ts"]
}

先来看看tsconfig.json最后一个references,可以看到是用来引用tsconfig.node.json中的内容的。

原来 references属性是 TypeScript 3.0 的新特性,允许将 TypeScript 程序拆分结构化。这和我们写 vue 组件的思想有着异曲同工之妙,避免了在一个文件中处理过多的内容,而是通过拆分成多个文件,分别配置不同的部分,达到更加清晰,可维护性更高的目的。

本文只介绍几个需要着重理解的配置选项,完整的配置可以参考编译选项 · TypeScript中文网 · TypeScript——JavaScript的超集

include

这个属性很好理解,就是指定需要编译的文件范围。

默认是对 src 下的 .ts、.d.ts、.tsx、.vue结尾的文件都需要进行编译。

可以看到tsconfig.node.json中的"include": ["vite.config.ts"]表明这个单独拆分出来的配置文件只是负责编译 vite 的配置文件vite.config.ts

与之相对的,我们可以添加 exclude 配置不需要编译的文件范围:

exclude

"exclude": ["node_modules","dist"]

将第三方依赖包、打包后的静态文件都排除在外。

compilerOptions.skipLibCheck

作用:忽略所有的声明文件( *.d.ts)的类型检查。

"compilerOptions": {
    "skipLibCheck": true
}

简而言之,这个属性不但可以忽略 npm 不规范带来的报错,还能最大限度的支持类型系统。设置为 true 就不用怕使用的第三方库不规范了。

可能有人好奇,为什么要跳过这些第三方库的检查?

我们做类型检查(以及下文的代码规范等)的目的是为了对团队内业务代码开发保持统一和规范,以保证开发成员之间的快速上手和后续维护。所以我们要做的是将各种规则集成到业务代码模块,而一些框架上的或者第三方库的内容就不用多此一举了。

compilerOptions.baseUrl

作用:设置baseUrl来告诉编译器到哪里去查找模块。 所有非相对模块导入都会被当做相对于 baseUrl

注意相对模块的导入不会被设置的baseUrl所影响,因为它们总是相对于导入它们的文件.

这个设置的作用和之前构建选项 build 中的 base 是类似的,我们配置为当前根目录即可

"baseUrl": "./"

compilerOptions.paths

作用:模块名到基于 baseUrl的路径映射的列表。

请注意"paths"是相对于"baseUrl"进行解析。

还记不记得我们在第三节配置了路径别名resolve.alias,为了让编译 ts 时也能够解析对应的路径,我们还需要配置 paths 选项:

"paths": {
    "@/*": ["src/*"],
    "views/*": ["src/views/*"],
    "components/*": ["src/components/*"],
    "assets/*": ["src/assets/*"]
}

compilerOptions.isolatedModules

作用:将每个文件作为单独的模块

typescript 将没有导入/导出的文件视为旧脚本文件。因为这样的文件不是模块和它们在全局命名空间中合并的任何定义。该配置项会禁止此类文件。将任何导入或导出添加的文件都视为模块

这个设置在 vite 官方文档中是被要求应该设置为 true 的,项目初始化的时候并没有默认加上这条,所以需要我们自己来:

"isolatedModules": true

compilerOptions.types

作用:添加要包含的类型声明文件名列表,只有在这里列出的模块的声明文件才会被加载进来

"types": ["vite/client"]

可以将 vite/client 添加到 types中,这会提供以下类型定义补充:

  • 资源导入 (例如:导入一个 .svg 文件)
  • import.meta.env 上 Vite 注入的环境变量的类型定义
  • import.meta.hot 上的 HMR API 类型定义

我们需要补充的配置差不多就这些了,现阶段完整配置如下:

完整配置

{
  "compilerOptions": {
    "baseUrl": "./",
    "skipLibCheck": true,
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "types": ["vite/client"],
    "isolatedModules": true,
    "paths": {
      "@/*": ["src/*"],
      "views/*": ["src/views/*"],
      "components/*": ["src/components/*"],
      "assets/*": ["src/assets/*"]
    }
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue"
  ],
  "exclude": ["node_modules", "dist"]
}

env.d.ts 文件详解

打开 src 文件夹下的 env.d.ts 文件,可以看到长这样

/// <reference types="vite/client" />declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}

让我们看看第一行的这个三斜杠指令,在官方文档客户端类型中解释到了:

Vite 默认的类型定义是写给它的 Node.js API 的。要将其补充到一个 Vite 应用的客户端代码环境中,请添加一个 d.ts 声明文件:/// <reference types="vite/client" />

请注意:三斜杠指令是包含单个XML标签的单行注释,注释的内容会做为编译器指令使用,只有在文件的最顶部才会生效,可以查看TypeScript: Documentation - Triple-Slash Directives学习相关内容。

所以这个文件的作用就呼之欲出了:帮助编译器识别类型

TypeScript 相比 JavaScript 增加了类型声明。原则上,TypeScript 需要做到先声明后使用。这就导致开发者在调用很多原生接口(浏览器、Node.js)或者第三方模块的时候,因为某些全局变量或者对象的方法并没有声明过,导致编译器的类型检查失败。

当我们遇上Property xxx does not exist on type ...报错的时候,就可以定位到是没有声明过这个方法/属性。我们就可以在这个文件作全局声明。

可以看到,该文件已经默认为所有的 vue 文件声明了 DefineComponent的组件类型,这就意味着只要我们的单文件组件使用

<script lang="ts">
    import { defineComponent } from 'vue'
    export default defineComponent({
        ...
    })
</script>

的写法,就能避免 vue 文件中大多数类型声明的报错,比如使用路由的this.$routerthis.$route命令

四、接入代码规范

代码规范

作为一个企业级框架,代码规范是必不可少的。虽然我们使用了具有类型规范的 typescript,但是仍然无法规避团队成员使用不同的开发风格,比如语句结尾用不用;,缩进使用两个空格还是四个空格,变量名用小驼峰还是连字符等等等等。

统一的代码规范,不但能增强代码的可维护性,也能提高团队成员的相互协作,更可贵的是能够培养前端开发者完善的代码风格和开发意识。

ESlint

简介

ESlint 被称作下一代的 JS Linter 工具,能够将 JS 代码解析成 AST 抽象语法树,然后检测 AST 是否符合既定的规则。而这些“规则”,具有非常灵活的配置。

2019 年 1 月,TypeScirpt 官方决定全面采用 ESLint 作为代码检查的工具,并创建了一个新项目 typescript-eslint,提供了 TypeScript 文件的解析器 @typescript-eslint/parser 和相关的配置选项 @typescript-eslint/eslint-plugin 等。话不多说,我们来看看如何接入框架中去吧:

安装

直接运行命令即可安装:

npm install --save-dev eslint

ESLint 默认使用的是 Espree 进行语法解析,所以无法对部分 typescript 语法进行解析,因此我么还需要安装 @typescript-eslint/parser 替换掉默认的解析器

npm install @typescript-eslint/parser --save-dev

接下来需要安装对应的插件 @typescript-eslint/eslint-plugin 它作为 eslint 默认规则的补充,提供了一些额外的适用于 ts 语法的规则:

npm install --save-dev @typescript-eslint/eslint-plugin

当然不要忘了安装eslint-plugin-vue,借用这个 plugin 的能力才能让 eslint 识别 vue 文件

npm install --save-dev eslint-plugin-vue

安装好依赖插件后,我们就可以在项目根目录下

  1. 新建一个.eslintrc.js用来配置 ESlint 的相关规则

  2. 新建一个 .eslintignore文件用来指定不进行 ESlint 校验的目录/文件,我们将以下文件/文件夹忽略 ESlint 校验

    /build/
    /dist/
    /node_modules/
    *.js
    

配置

老规矩,我们来了解一下 ESlint 中几个比较重要的配置项:

env

作用:提供预定义的环境变量。

因为node 或者浏览器中的全局变量很多,如果我们一个个声明会显得繁琐,因此就需要用到env,这是对环境定义的一组全局变量的预设。

env: {
    browser: true,
    es2021: true,  // 添加所有 ECMAScript 2021 全局变量并自动将 ecmaVersion 解析器选项设置为 12
    node: true,
},

parser

作用:指定要使用的解析器。我们指定为vue-eslint-parser即可

parser: 'vue-eslint-parser',

parserOptions

作用:给解析器传入一些其他的配置参数

比如我们之前安装的@typescript-eslint/parser就可以在这里进行配置

parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: "latest",  // 支持的es版本
    sourceType: 'module',  // 代模块类型,默认为script,我们设置为module
},

extends

作用:使用预设的 lint 包

如果要我们自己去设置各个规则未免会显得繁琐,所以可以直接使用业界的最佳实践

extends: [
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended'
],

我们选择 plugin:vue/vue3-recommendedplugin:@typescript-eslint/recommended 作为基础规则即可

plugins

作用:增强 ESlint 功能

还记得我们最开始安装的@typescript-eslint/eslint-plugin吗?它就是用来给 eslint 提供一些额外的适用于 ts 语法的规则插件名中的 eslint-plugin- 可以省略:

plugins: ['@typescript-eslint'],

rules

作用:创建自定义规则。

规则列表:

规则定义值:

  • off 或 0 - 关闭规则
  • warn 或 1 - 开启规则, 使用警告 程序不会退出
  • error 或 2 - 开启规则, 使用错误 程序退出

示例:

rules: {
    // 禁止出现未使用过的变量
    'no-unused-vars': 'error',
    // 缩进使用 4 个空格,并且 switch 语句中的 Case 需要缩进
    // https://eslint.org/docs/rules/indent
    'indent': ['error', 4, {
        'SwitchCase': 1,
        'flatTernaryExpressions': true
    }],
    // 只有一个参数时,箭头函数体可以省略圆括号
    // https://eslint.org/docs/rules/arrow-parens
    'arrow-parens': 'off',
}

虽然我们通过 extends 引入了行业内最佳实践的一些规则,但是每个团队还是有每个团队自己的开发习惯,所以我们可以通过 rules 添加或者修改 extends 中的规则。

这里我就分享一下我们团队喜欢用的规则,当时是团队leader拉着我们几个老员工一条一条过出来的。

定义的规则比较多,为了不占位置,就不直接放代码了,感兴趣的可以直接去代码仓库中查看。

其他

做完以上步骤之后,可能有人会发现:咋不管用呢?

这是因为没有在各自的编辑器开启 ESlint 的功能,例如 webstorm:

image-20220223004059633.png 使用其他编辑器的小伙伴也可以自行搜索开启方法。

除了编辑器自带的一键修复 ESlint 问题,我们也可以在package.json通过添加如下命令

"lint": "eslint --ext .js,.vue,.ts src",
"lint:fix"": "npx eslint ./src/**/*.vue --fix",

这样就可以通过npm run lint在控制台查看不符合规则的代码列具,和通过npm run lint:fix快速自动修复 ESlint 问题了,当然,有些问题还是需要手动修复的。

Stylelint

上面我们通过 ESlint 负责检查了 html 和 JavaScrript,css 当然不能缺席,所以我们可以通过接入 Stylelint 规范 css 的书写风格。

安装

我们选择使用 scss 来增强 css 的语法能力,安装以下包

npm install sass stylelint stylelint-scss --save-dev

日常开发中,我们主要用到的是 scss 提供的层级能力,这样对父子级的展示就非常明了,使用示例:

<template>
    <div class="test">
        <div class="test2"></div>
    </div>
</template><!-- 在 style 标签中添加 lang属性指定为 scss 就可以了 -->
<style lang="scss">
  .test {
      .test2 {
          border: 1px solid black;
          width: 100px;
          height: 200px;
      }
  }
}
</style>

当然 scss 还有其他变量、混合等好用的功能,可以查看文档进行学习

StyleLint 的初始化和 ESlint 差不多,在项目根目录下新建两个文件:

  • .stylelintrc.js用来配置 stylelint 规则

  • .stylelintignore 用来配置不需要通过 stylelint 约束的文件,我们添加以下文件:

    # 其他类型文件
    *.js
    *.ts
    *.jpg
    *.woff
    ​
    # 测试和打包目录
    /dist/
    /node_modules/
    

配置

我们安装都是最新版本的依赖包,所以会有一些额外的配置,比如 stylelint 14 所需要的配置

Stylelint 14+ 不再包含 Scss,Sass,Less或者SugarSS这种类css的预编译器的解析了,所以我们可以通过 extending 共享配置来包含何时的语法解析,我们就使用stylelint-config-standard-scss来作为公共规则

npm install --save-dev stylelint-config-standard-scss

使用:

"extends": ["stylelint-config-standard-scss"],

当然,为了然 stylelint 能够读 vue (.html, .xml, .svelte, .vue etc.)文件,我们还需要安装postcss-html

npm install --save-dev postcss-html

然后配置 customSyntax 属性

"customSyntax": "postcss-html"

规则定义

我们可以使用一些自定义规则,为了节省篇幅,可以到代码仓库中去查看

使用

我们开启编辑器/IDE的 stylelint 就可以享受约束带来的成就感了:

image-20220223232751405.png

接着在 package.json 中添加如下命令:

"stylelint": "npx stylelint --aei .vue src",
"stylint:fix": "npx stylelint ./src/**/*.vue --fix"

就可以通过npm run stylelint在控制台快速查看不匹配 stylelint 的文件具体代码,和npm run stylint:fix一键修复了。

.editorconfig

定义了这么多规则,但是编辑器不够智能怎么办?

敲个tab、回车键缩进总是不对,每次手动对齐那不难受的很?

我们可以在项目根目录下添加.editorconfig文件,这个文件可以帮助开发者在不同的编辑器和 IDE 之间定义和维护一致的代码风格,使用示例:

# Editor configuration, see http://editorconfig.org# 表示是最顶层的 EditorConfig 配置文件
root = true[*]     # 表示所有文件适用
charset = utf-8     # 设置文件字符集为 utf-8
indent_style = space    # 缩进风格(tab | space)
indent_size = 4    # 缩进大小
end_of_line = lf    # 控制换行类型(lf | cr | crlf)(lf是\n, cr是\r, crlf是\r\n)
trim_trailing_whitespace = true     # 去除行首的任意空白字符
insert_final_newline = true     # 始终在文件末尾插入一个新行[*.md]  # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

git hooks

既然已经定义好了这么多的规则,但如何防止不符合 ESlint 代码规范的代码被提交到 git 仓库?我们可以使用 husky 和 lint-staged

  • husky 是控制代码提交的钩子,在代码被提交到 Git 仓库之前,我们可以在这里做一些预检查或者格式化工作。
  • lint-staged 是一个前端文件过滤的工具(仅仅是文件过滤器),可以对文件系统进行过滤,使得每次提交不必对所有文件进行校验。

安装

npm install --save-dev husky@4.3.8 lint-staged

注意:最新版本 husky 存在bug,所以我们使用低版本的就可以了

修改 package.json 配置

"husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
 },
"lint-staged": {
    "src/**/*.{ts,vue}": [
        "eslint --fix"
    ],
    "*.vue": [
        // stylelint14+ 需要 --custom-syntax postcss-html的配置
        "stylelint --fix  --custom-syntax postcss-html"
    ]
},

这样,每次在 commit 代码之前,都会校验你提交的文件师傅符合项目中配置的 ESlint 规则了,不符合则不能提交,且会自动帮你执行eslint --fixstylelint --fix自动修复部分问题。示例:

image-20220224000755769.png

基本配置已经接入完毕了,接下来,我们就要对框架的应用部分进行扩展:

第二部分

五、接入 UI 库

Naive UI 是尤大推荐的 vue3 UI 库,全量使用 TypeScript 编写,但是毕竟是一个比较年轻的 UI 库,相对于 element 和 ant design 等老牌强者还是略有不足的,在真正的项目级环境应该暂时不会考虑使用 naive ui。本着学习新知识的目的,我们还是可以来了解一下的。

UI 库的使用其实都大差不差,在框架中的引入我们需要考虑的是以下两点:

  • 按需引入
  • 打包优化

按需引入能够灵活应用 tree-shaking的能力将没有使用到的排除出去,避免打包体积的冗余;而打包优化之前在webpack 中比较常用的是对大型的三方库做单独打包、避免重复打包等,如何在 vite 中做打包优化呢?哈哈,其实我也不熟,后续会花时间重点研究一下再补充进来。

首先看看 Naive UI 支持的依赖版本

  • Vue > 3.0.5
  • TypeScript > 4.1

好的,没有什么问题,接下来步入正轨

安装&引入

我们使用 npm 安装 naive-ui ,配套的字体 vfonts 就不安装了,使用默认的即可

npm i -D naive-ui

使用方面,当然最佳实践是按需引入:

在我们的src/utils新建一个demand-import.ts,将我们日常开发会使用的比较频繁的组件引入进来即可

import {
    // create naive ui
    create,
    // component
    NButton
} from 'naive-ui'export const naive = create({
    components: [NButton]
})

然后在main.ts中引入

import { createApp } from 'vue'
import App from './App.vue'
import { naive } from './utils/demand-import'const app = createApp(App as any)
app.use(naive)

就可以在页面上使用了:

<template>
    <n-button type="tertiary">
        Tertiary
    </n-button>
</template>

在使用上,直接照着官方文档就可以愉快的做 cv 侠了,我就不在此赘述了

如果对性能真的洁癖到极致,可以采用按需引入 + 直接引入的方式,就是对比较常用的组件采用按需引入的方式注册到全局,对不常用的组件直接在相关处使用import xxx from 'naive-ui'引入使用。

六、接入 vue-router

vue 强大的单页面应用能力其实主要依靠基于 vue-router 带来的无缝切换。

安装

npm install vue-router --save

配置

首先我们在src/views/home新建一个空白的 index.vue 文件

<script lang="ts" setup>
// .
</script><template>
    <div>hello  vue3</div>
</template>

然后在 src/router 目录下新建一个index.ts,引入刚才新建的文件路由地址设为首页/

import {
    createRouter, createWebHashHistory, RouteRecordRaw,
} from 'vue-router'const routes: Array<RouteRecordRaw> = [
    { path: '/', name: 'Home', component: () => import('views/home/index.vue')}
]
​
const router = createRouter({
    history: createWebHashHistory(),    // history 模式则使用 createWebHistory()
    routes,
})
​
export default router

我们使用的是 hash 模式,当然,这是我们团队的习惯,history 模式和 hash 也差不多,选择适合自己的就行

接着在src/main.ts中引入该文件

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'const app = createApp(App as any)
app.use(router)

src/App.vue中添加router-view组件

<template>
    <div class="app-view">
        <router-view></router-view>
    </div>
</template>

运行服务,可以看到首页的内容,这样我们的基本路由就配置成功了

image-20220225000235042.png

七、接入状态管理工具 pinia

pinia 是一个轻量级的状态管理库,属于 vue3 生态圈新的成员之一,也可以把它看做 vuex5,同时支持 vue2 和 vue3,模块化的设计让它的结构十分地清晰明了,别的不说,看尤大推文中的预期就能知道 pinia 的牛逼之处了:

image-20220226230621582.png 安装

npm install pinia --save

运行完发现有警告:

peerDependencies WARNING pinia@latest requires a peer of @vue/composition-api@^1.4.0 but none was installed
peerDependencies WARNING pinia@2.0.11 › vue-demi@* requires a peer of @vue/composition-api@^1.0.0-rc.1 but none was installed
​

说 pinia 依赖于@vue/composition-api@^1.4.0包,但是composition-api是 vue2 用到的,可以在 vue2 中使用 vue3的composition-api,我们忽略它就行了。

引入

main.ts中引入

import { createPinia } from 'pinia'
​
app.use(createPinia())

使用

我们以一个简单的累加器作为例子:在src/stores下新建一个counters.ts文件

首先,我们需要使用defineStore()来定义一个 store

import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', {
    state: () => {
        return {
            count: 0
        }
    },
    getters: {
        count() {
            return this.count
        }
    },
    actions: {
        increment() {
            this.count++
        }
    }
})

注意:defineStore中的第一个参数需要在你整个应用当中保持唯一。

这种写法和我们之前使用的 vuex 简直一模一样,唯一的区别就是 pinia 将mutations给干掉了,其实之前在使用过程中也确实觉得mutationsactions功能有很大程度的重合,这波升级简直完美。

当然,pinia 的魅力自然不止于此,Vue 的 setup 语法带来的一系列提升才是最大的亮点:

我们可以在defineStore中使用类setup的写法:直接将上面的配置项替换成一个函数:

import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', () => {
    const count = ref(0)
    function increment() {
      count.value++
    }
​
    return { count, increment }
})

然后在页面中就可以这样使用了:

<script lang="ts" setup>
    import { useCounterStore } from '@/stores/counter'
​
    const counter = useCounterStore()
</script>
<template>
    <div @click="counter.increment()">
        {{ counter.count }}
    </div>
</template>

counter 可以被看做是一个reactive对象,所以,counter就像setup中的 props 一样,解构是被禁止使用的:

const counter = useCounterStore()
const { count } = counter
​
<div @click="counter.increment()">{{ count }}</div>
​

我们会发现,无论怎样点击count的值都不会变化,只会维持它的初始值不变。

当然pinia很贴心的提供了storeToRefs方法,让我们可以享受解构的乐趣:

const { count } = storeToRefs(counter)

八、接入图表库 echarts5

B端产品图表的运用还是很频繁的,对市面上比较火的几款图表库进行横向对比后,还是觉得 echarts 最香。尤其是 echarts5版本接入了很多新特性以及对老版的优化,话不多说,我们直接开始接入吧:

安装&引入

npm install echarts --save

之所以强调第5版,是因为在这个版本,echarts 增加了性能党最爱的按需引入:

src/utils/下新建echarts.ts用来引入我们需要使用的组件

import * as echarts from 'echarts/core'
import {
    BarChart,
    // 系列类型的定义后缀都为 SeriesOption
    BarSeriesOption,
    // LineChart,
    LineSeriesOption
} from 'echarts/charts'
import {
    TitleComponent,
    // 组件类型的定义后缀都为 ComponentOption
    TitleComponentOption,
    TooltipComponent,
    TooltipComponentOption,
    GridComponent,
    GridComponentOption,
    // 数据集组件
    DatasetComponent,
    DatasetComponentOption,
    // 内置数据转换器组件 (filter, sort)
    TransformComponent,
    LegendComponent
} from 'echarts/components'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = echarts.ComposeOption<
    | BarSeriesOption
    | LineSeriesOption
    | TitleComponentOption
    | TooltipComponentOption
    | GridComponentOption
    | DatasetComponentOption
>
​
// 注册必须的组件
echarts.use([
    TitleComponent,
    TooltipComponent,
    GridComponent,
    DatasetComponent,
    TransformComponent,
    BarChart,
    LabelLayout,
    UniversalTransition,
    CanvasRenderer,
    LegendComponent
])
​
// eslint-disable-next-line no-unused-vars
const option: ECOption = {
    // ...
}
​
export const $echarts = echarts

就可以在页面中使用了:

<script lang="ts" setup>
    import { onMounted } from 'vue'
    import { $echarts, ECOption } from '@/utils/echarts'
​
    onMounted(() => {
        // 测试echarts的引入
        const ele = document.getElementById('echarts') as HTMLCanvasElement
        const myChart = $echarts.init(ele)
        const option: ECOption = {
            title: {
                text: 'ECharts 入门示例'
            },
            tooltip: {},
            legend: {
                data: ['销量']
            },
            xAxis: {
                data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
            },
            yAxis: {},
            series: [
                {
                    name: '销量',
                    type: 'bar',
                    data: [5, 20, 36, 10, 10, 20]
                }
            ]
        }
        myChart.setOption(option)
</script>

注意:只有在onMounted及之后才能通过document.getElementById拿到页面上的 dom 元素然后通过$echarts.init(ele)初始化。 使用效果:

image-20220227153241398.png

九、配置统一 axios 处理

前后端的通讯离不开 ajax 方法,而 axios 库作为前端最流行的 ajax 库别树一帜。

除了简单使用get/post等简单调用,我们还可以添加状态码拦截、请求取消、错误处理等等能力。但是限于篇幅有限,本文就不做那么复杂的封装了,后面会抽空我专门写一篇从 axios 原理分析到完整的封装处理。

安装&引入

npm install axios --save

我们在src/utils下新建一个axios.ts,在这里封装axios的基本功能

首先引入我们的主角Axios、错误处理类型接口AxiosError和响应结果接口AxiosResponse

import Axios, {AxiosError, AxiosResponse} from 'axios'

我们可以通过Axios.create()方法创建一个自定义配置的axios实例

const BASE_URL = 'https://api.example.com'
const TIME_OUT = 10 * 1000
​
const instance = Axios.create({
    baseURL: BASE_URL,
    timeout: TIME_OUT
})

比如指定每个请求的baseURL,指定请求的超时时间为10秒

接着我们使用后置拦截器,对获取的响应进行拦截:

instance.interceptors.response.use(
    (res: AxiosResponse) => {
        if (String(res.status).indexOf('2') !== 0) {
            return {
                code: res.status,
                message: res.data.message || '请求异常,请刷新重试',
                result: false
            }
        }
        return Promise.reject(res.data)
    },
    (error: AxiosError) => {
        if (error && error.response) {
            errorHandle(error.response.status, error.response)
            return Promise.reject(error.response)
        }
        console.log('网络请求失败, 请刷新重试')
        return Promise.reject(error)
    }
)

当我们获取的响应状态码不是以 2 开头的,视作响应失败

当捕获到错误请求时,可以自定义错误处理:

const errorHandle = (status: number, error): void => {
    // HTTP状态码判断
    switch (status) {
        case 401:
            return alert(`Error Code: ${status}, Message: ${error.msg || '登录失效,请重新登录'}`)
        case 403:
            return alert(`Error Code: ${status}, Message: ${error.msg || '你没有访问权限'}`)
        case 500:
            return alert(`Error Code: ${status}, Message: ${error.msg || '后台错误,请联系管理员'}`)
        case 502:
            return alert(`Error Code: ${status}, Message: ${error.msg || '平台环境异常'}`)
        default:
            alert(`Error Code: ${status}, Message: ${error.msg || '未知错误,请刷新重试'}`)
    }
}

最后再导出我们常用的请求方法:

const getPromise = (method, url, params, config = {}) => {
    return new Promise((resolve, reject) => {
        instance[method](method, url)(params, config).catch(e => e.response.data)
            .then(res => resolve(res))
            .catch(err => reject(err))
    })
}
​
const get = (url: string, params?: any) => getPromise('get', url, { params })
const post = (url: string, params: any, config?: AxiosRequestConfig) => getPromise('post', url, params, config)
​
export {
    get,
    post,
}

十一、参考/指引文档

Vue.js

Home | Vite 官方中文文档

Node.js

TypeScript: JavaScript With Syntax For Types.

ESLint - Pluggable JavaScript linter

Home | Stylelint

Naive UI: 一个 Vue 3 组件库

pinia

TypeScript 入门教程

Introduction · TypeScript Handbook(中文版)

Getting Started | Axios Docs

总结

奈何最近工作确实忙碌,其实很多细节都省略掉了,当然最早列的写作大纲其实远不止这点内容,我们还可以添加单元测试,mock 数据等等。后续会不断的补充与完善~