前端工程化探索以及代码规范和插件分享

568 阅读7分钟
以下是我个人对前端工程化的探索以及vue3的一些技巧和插件分享,我会分为项目规范开发规范推荐插件使用技巧四个方面来介绍。

项目规范

项目架构

截屏2022-07-02 17.52.39.png

以这个vue3+ts+vite项目为例,我主要介绍一下src中的文件夹以及文件.

  • assets 主要用过来存放项目资源的文件夹,例如css样式文件 ,图片等等 截屏2022-07-02 18.01.00.png

  • base-ui这个文件夹是存放一些可复用公共的基础组件(我说的公共的意思是可以给其他项目使用,而基础组件的意思是我会以这些组件为基础进一步封装项目用的业务组件),例如table组件,from组件。

    截屏2022-07-02 18.05.58.png

  • component这个文件夹才是页面中真正使用到的业务组件,是由base-ui中的组件进一步封装的,只适合在当前项目中使用。

    截屏2022-07-02 18.08.56.png

  • global这个文件夹存放一些全局性的注册文件,例如ui组件的按需求引入注册文件,vue全局方法或者指令的注册文件等。

    截屏2022-07-02 18.14.07.png

  • hooks这个文件夹存放的是一些全局钩子文件,主要是一些业务组件(主要是compnents中的组件)的公共方法将其写成钩子函数的形式,以use-xxx的形式命名文件。

    截屏2022-07-02 18.22.36.png

  • router这个文件夹就是用来存放vue路由的文件。

    截屏2022-07-02 18.23.57.png

  • service这个文件夹用来存放网络请求相关的文件,例如我们一般都使用axios进行网络请求,可以将二次封装的axios文件放到这个里面。此外我们可以将所有的api请求专门弄一个文件夹放到serivce中,方便统一进行api管理。

    截屏2022-07-02 18.29.10.png

  • store这个文件夹是用来存放vue全局状态管理的,可以使用vuex或者pinia进行项目的全局状态管理。

    截屏2022-07-02 18.32.11.png

  • utils这个文件夹是用来存放工具类函数的文件。例如我们封装的localstorage文件,格式化日期文件等等。

    截屏2022-07-02 18.38.05.png

  • views这个文件夹是用来存放我们项目的页面文件,我们可以自行划分组织文件夹,根据路由或者功能等来创建。

    截屏2022-07-02 18.40.29.png

代码格式化规范

  1. 集成 editorconfig 配置

项目开发人员使用的IDE编辑器不同,代码格式化也不同,这样会不利于整个项目的统一规范编写。.editorconfig这个文件可以有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。首先我们先去vscode中安装一个插件:EditorConfig for VS Code

截屏2022-07-02 22.17.55.png

然后在项目的根目录下创建.editorconfig文件,代码如下:

root = true[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
  1. 使用prettier工具

prettire是一款代码格式化的工具,支持很多语言比如JS,TS,CSS,LESS等等。我们先安装prettier: npm i prettier -D,然后在项目的根目录下创建.prettierrc文件,代码如下:

{
  "useTabs": false, // 使用 tab 缩进还是空格缩进,选择 false;
  "tabWidth": 2, // tab 是空格的情况下,是几个空格,选择 2 个;
  "printWidth": 80, // 当行字符的长度,推荐 80;
  "singleQuote": true, // 使用单引号还是双引号,选择 true,使用单引号;
  "trailingComma": "none", // 在多行输入的尾逗号是否添加,设置为 none;
  "semi": false // 语句末尾是否要加分号,默认值 true,选择 false 表示不加;
}

类似于.gitignore文件我们并不是所有的文件都要格式化,所以在项目根目录下创建.prettierignore文件来对某些文件进行忽略格式化,代码如下:

/dist/*
.local
.output.js
/node_modules/**
​
**/*.svg
**/*.sh
​
/public/*

同时为了方便我们格式化文件,可以在package.json中配置一个scripts:

"prettier": "prettier --write ."
  1. 使用 ESLint 检测

我们现在vscode中安装ESLint插件

截屏2022-07-02 22.36.35.png

为了解决eslintprettier冲突的问题,我们可以安装这两个插件:

npm i eslint-plugin-prettier eslint-config-prettier -D

.eslintrc.js添加prettier插件

module.exports = {
  ...
  extends: [
    ...
    'plugin:prettier/recommended' // 添加 prettier 插件
  ],
  ...
}

这样,在执行eslint --fix命令时,ESLint就会按照prettier的配置规则来格式化代码,轻松解决二者冲突问题。

git提交规范

  1. git Husky

虽然我们已经要求项目使用 eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了:

  • 也就是我们希望保证代码仓库中的代码都是符合 eslint 规范的;
  • 那么我们需要在组员执行 git commit 命令的时候对其进行校验,如果不符合 eslint 规范,那么自动通过规范进行修复;

我们可以通过husky工具:

  • husky是一个 git hook 工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push

我们可以使用自动配置的命令:

npx husky-init && npm install

以上代码做了三件事:

  1. 安装依赖:

截屏2022-07-02 23.29.41.png

  1. 在项目根目录下创建了.husky文件夹(npx husky install):

截屏2022-07-02 23.30.52.png

  1. package.json 中添加一个脚本:

截屏2022-07-02 23.32.22.png

我们要在scripts中再配置一个命令:"lint": "eslint src --fix --ext .js,.ts,.jsx,.tsx,.vue && prettier . --write"

截屏2022-07-03 10.51.07.png 这样执行npm run lint时就可以对代码进行检查了。

接下来,我们需要去完成一个操作:在进行commit时,执行lint脚本:

截屏2022-07-03 10.54.01.png 但是这样配置有一个缺点,就是及时我们git add一个文件至暂存区,lint命令也会将所有的文件进行检查,这样显然是比较浪费性能的,特别是当项目比较庞大,检查的文件数量过多,那么检查速度会非常慢。所以我们再在装一个工具叫做lint-staged:

npm i lint-staged -D

这个工具可以让lint只检测暂存区的文件,所以检测的速度很快。然后我们再修改一下pre-commit

截屏2022-07-03 12.01.31.png

接着再配置一下package.json中的配置:

"lint-staged": {
    "*.{vue,js,ts,tsx,jsx}": [
      "eslint --fix",
      "prettier --write --ignore-unknown",
      "git add"
    ]
 },

这样git commit时触发pre-commit钩子,运行lint-staged命令,我们就可以检测暂存区的代码文件是否符合eslint的规则。

  1. git commit提交规范

一个比较规范的git commit应该是这样的:

截屏2022-07-03 12.31.49.png

这样可以快速定位每次提交的内容,方便之后对版本进行控制。

但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:Commitizen,它是一个帮助我们编写规范 commit message 的工具。首先我们来安装它:

npm i commitizen -D

接着安装cz-conventional-changelog,并且初始化 cz-conventional-changelog

截屏2022-07-03 12.35.58.png

npx commitizen init cz-conventional-changelog --save-dev --save-exact

这个命令会帮助我们安装cz-conventional-changelog

并且在 package.json中进行配置:

截屏2022-07-03 12.37.11.png

我们可以试着npx cz

第一步会提示你选择本次更新的类型:

截屏2022-07-03 12.39.06.png 这是每个选项对应的含义:

截屏2022-07-03 12.39.30.png 第二步选择本次修改的范围(作用域):

截屏2022-07-03 12.42.02.png

第三步选择提交的信息:

截屏2022-07-03 12.42.37.png

第四步提交详细的描述信息:

截屏2022-07-03 12.42.59.png

第五步是否是一次重大的更改:

截屏2022-07-03 12.44.25.png

第六步是否影响某个 open issue

截屏2022-07-03 12.45.13.png

建议我们在scripts中新建一个命令来执行cz:

"commit": "cz"

如果我们按照cz来规范了提交风格,但是依然有同事通过 git commit 按照不规范的格式提交应该怎么办呢?我们可以安装commitlint来限制提交:

npm i @commitlint/config-conventional @commitlint/cli -D

并且在根目录创建 commitlint.config.js 文件,配置 commitlint:

module.exports = {
  extends: ['@commitlint/config-conventional']
};

使用 husky 生成 commit-msg 文件,验证提交信息:

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

开发规范

  • 变量命名

    变量全部采用let进行定义,采用小驼峰命名。如果是常量,则使用const进行定义且常量名全部大写,必要时使用_下划线进行连接:

    let userName = 'zengge';
    const BASE_URL = 'www.goole.com';
    
  • 函数命名

    一般函数命名方式为小驼峰命名(构造函数使用大驼峰命名),前缀使用动词:

    动词含义返回值
    can判断是否可执行某个动作(权限)boolean
    has判断是否含有某个值boolean
    is判断是否为某个值boolean
    get获取某个值boolean
    set设置某个值void |boolean |object
    handle监听某个函数void

    例如:

    // 是否可读
    function canRead(info: IInfo): boolean {
      ...
      return true
    }
    ​
    // 监听浏览器窗口的变化
    function handleWindowReset(): void {
      console.log(document.body.clientWidth)
    }
    
  • 文件以及文件夹命名 统一采用小写命名,不要采用驼峰命名,必要的时候可以使用连接符号-连接单词。

    例如:

    截屏2022-07-03 15.13.56.png

    在创建hooks文件时的命名应该以use-xxx的格式命名,例如:

    截屏2022-07-02 18.22.36.png

  • 代码注释

    • 单行注释:在双斜线(//)与注释文字之间保留一个空格。

    • 多行注释:若至少三行注释时,第一行为/ ,最后行为/,其他行以开始,并且注释文字与保留一个空格。

    • 函数注释:在多行注释的基础上增加了一些注释关键字。

    • TODO注释:将因样式或者其他情况不能完成编程的时候,使用TODO注释。

      例如 :

      // 这是一个单行注释
      const userName = '单行注释';
      /*
      * 这是多行注释
      * 这里至少要有三行
      * 才使用多行注释
      */
      const str = '多行注释'
      /**
       * @description: 提交奖金申请
       * @param: {IInfo} applyInfo 奖金申请信息
       * @return: {Promise<void>}
       * @author: yuqi.zeng
       */
      async function setBonusApply(applyInfo: IInfo): Promise<void> {
        ...
      }
      // TODO: 这里是将要完成的功能

      使用函数注释的时候会比较麻烦,我待会会在插件推荐中分享注释插件。

      要查看有哪些TODO注释,我们可以在vscode中使用command + shift + p打开搜索框,然后搜索todo,选择代办事项:导出树状图:

      截屏2022-07-03 15.48.36.png

      截屏2022-07-03 15.49.15.png 这样就可以看到有哪些TODO了。

  • TS代码书写规范

    • 在定义enum时,使用大驼峰命名,enum成员中的键使用大写(vscode中的快捷键为conteol + command + u,可以自定义),成员中的值使用小写或者其他数字符号,必要时可以用_下划线连接:

      enum Status {
        WILL_DO = 'will_do',
        DOING = 'doing',
        COMPLETE = 'complete'
      }
      
    • 在定义interface时,使用I(大写i)开头代表接口,使用大驼峰命名:

      enum Status {
        WILL_DO = 'will_do',
        DOING = 'doing',
        COMPLETE = 'complete'
      }
      ​
      interface ITodoItem {
        status: Status,
        todo: string
      }
      ​
      interface ITodo {
        todoItems: ITodoItem[]
      }
      
    • 统一使用as关键字断言:

      // ts断言有两种方法 泛型断言和as关键字断言
      let message: any = "scc"
      // 推荐
      let mesLength: number = (message as string).length
      // 不推荐
      let mesLength: number = (<string>message).length
      

使用技巧

编码技巧以及规范

  • 使用解构获取对象的值

       const userInfo = {
         name: 'jack',
         age: 18,
         hobby: ['basketball', 'football']
       };
       
       // 可以别名 可以深层解构hobby
       const { name: userName, age, hobby: [hobby1, hobby2] } = userInfo;
       console.log(userName, age, hobby1, hobby2);
    
  • 使用async/await语法糖代替Promise().then回调函数:

    function handClick() {
      // 获取用户列表
      getUserList().then((res)=>{
        console.log(res);
      })
    }
    
    // 推荐使用
    async function handleClick() {
      // 获取用户列表
      const res = await getUserList();
      console.log(res);
    }
    
  • 使用class进行封装,例如axios的封装,api接口的封装,localStorage的封装等等。下面以封装api接口为例:

    class LoginRequest {
      // 登录请求
      static loginRequest(): Promise<IDataType<ILoginRes>> {
        return httpRequest.get<IDataType<ILoginRes>>({
          url: LoginAPI.LOGIN
        })
      }
      // 获取用户信息
      static userInfoRequestById(id: number): Promise<IDataType<IUserInfo>> {
        return httpRequest.post<IDataType<IUserInfo>>({
          url: LoginAPI.LOGIN_USERINFO + id,
          showLoading: false
        })
      }
    }
    
  • 将较为多且复杂的if判断语句用策略模式的Map代替:

    // 点击Map
    const clickMap = new Map([
      [ITabType.RELOAD, () => reload!()],
      [
        ITabType.CLOSE_ALL,
        () => {
          getTabs()
            .filter((tab) => tab.name !== 'console')
            .forEach((item) => {
              handleColseClick(item)
            })
        }
      ],
      [
        ITabType.CLOSE_CURR,
        () => {
          handleColseClick(item)
        }
      ],
      [
        ITabType.CLOSE_LEFT,
        () => {
          const findIndex = getTabs().findIndex((tab) => tab.name === item.name)
          const leftTabs = getTabs()
            .slice(0, findIndex)
            .filter((tab) => tab.name !== 'console')
          leftTabs.forEach((item) => {
            handleColseClick(item)
          })
        }
      ],
      [
        ITabType.CLOSE_RIGHT,
        () => {
          const findIndex = getTabs().findIndex((tab) => tab.name === item.name)
          const rightTabs = getTabs().slice(findIndex + 1)
          rightTabs.forEach((item) => {
            handleColseClick(item)
          })
        }
      ],
      [
        ITabType.CLOSE_OTHER,
        () => {
          const otherTabs = getTabs().filter((tab) => tab.name !== item.name && tab.name !== 'console')
          otherTabs.forEach((item) => {
            handleColseClick(item)
          })
        }
      ]
    ])
    

vue3使用技巧以及规范

  • 使用setup语法糖可以减少代码量更专注于业务编写,推荐统一使用:

    <script setup lang="ts"></script>
    
  • 顶层await

    <script setup lang="ts">
      // 模拟了一个网络请求
      const getHelloWorld = () =>
          new Promise((resolve) => {
            setTimeout(() => {
              resolve('hello world')
            }, 1000)
          })
      const str = await getHelloWorld() // setup语法糖下直接写await,不需要在外面定义一个async函数。
    </script>
    <template>
      <div>
        这是子组件
      </div>
    </template>
    

    不过使用顶层await的话会让组件变成异步组件,父组件使用该异步组件时必须加上<suspense></suspense>标签,不过为了方便我们可以直接在App.vuerouter-view组件上包裹<suspense></suspense>标签,这样我们所有的组件就都可以使用顶层await了:

    <template>
      <suspense>
        <router-view />
      </suspense>
    </template>
    
  • 使用自定义组件时若无插槽内容则使用单标签,有插槽则使用双标签:

    <template>
      // 单标签
      <page-search
          class="pageSearch"
          ref="pageSearchRef"
          :searchFormConfig="searchFormConfig"
        />
      // 双标签
      <page-content
        class="pageContent"
        ref="pageContentRef"
      >
          <template #header-options>
            <n-button size="small" secondary type="default">导出</n-button>
          </template>
          <template #other-header-options>
            <n-button size="small" secondary type="warning"> 我是页面新增的</n-button>
          </template>
       </page-content >
    </template>
    
  • 定义响应式变量时使用泛型写法:

    <script setup lang="ts">
      interface IUserInfo {
        age: number
        address: string
      }  
      
      import { ref, reactive } from 'vue';
      // 用户名
      const userName = ref<string>('');
      // 密码
      const password = ref<string>('');
      // 用户信息
      const userInfo = reactive<IUserInfo>({});
    </script>
    
  • 使用definePropsdefineEmits时使用泛型定义:

    <script setup lang="ts">
      const props = defineProps<{fold: boolean}>();
      const emits = defineEmits<{ (e: 'handleFoldChange', data: boolean): void }>();
    </script>
    
  • 使用响应式语法糖

    ref 和响应式对象使用 .value 无疑是很繁琐的,并且在没有类型系统的帮助时很容易漏掉。所以vue3官方文档响应性语法糖中给了这样一个解决方法:

    截屏2022-07-03 19.55.47.png

    那么如何开启响应式语法糖呢?

    我们可以在vite.config.js文件中配置:

    export default defineConfig({
      plugins:[
        vue({
          reactivityTransform: true
        })
      ]
    })
    

    然后在env.d.ts文件中加入:

    /// <reference types="vue/macros-global" />
    

    现在我们就可以使用响应式语法糖了:

    <script setup lang="ts">
      import { ref } from 'vue';
      // 用户名
      const userName = $ref<string>('zengge');
      // 注意这里直接可以写userName而不用写userName.value
      const copyUserName = userName;
      
      // 如果要对变量进行赋值操作,需要将const改为let,否则将会报错
      let password = $ref<string>('');
      password = '123456'
    </script>
    

    此外可以在 defineProps 时使用响应式变量相同的解构写法:

    <script setup lang="ts">
      const {
        msg,
        // 默认值正常可用
        count = 1,
        // 解构时命别名也可用
        // 这里我们就将 `props.foo` 命别名为 `bar`
        foo: bar
      } = defineProps<{
        msg: string
        count?: number
        foo?: string
      }>()
    ​
      watchEffect(() => {
        // 会在 props 变化时打印
        console.log(msg, count, bar)
      })
    </script>
    

    这样我们就可以不使用withDefaults()这个 API,withDefaults()看起来会很笨拙。

  • 使用pinia而非vuex

    我们打开vuexgithub,文档中有这样一段话:

    截屏2022-07-03 22.16.09.png 截屏2022-07-03 22.34.11.png

    明确指出了vue的官方状态管理库已经更改为了pinia,强烈建议我们使用piniavue3官方文档中的默认状态管理也变成了pinia

插件推荐

  • Vue插件

    1. unplugin-auto-import

    这个插件可以让我们不需要手动引入vueapi,直接使用如ref,reactive,wathch,useRouterapi。首先我们先安装插件:

    npm i unplugin-auto-import -D

    随后在vite.config.js文件中配置:

    import AutoImport from 'unplugin-auto-import/vite'
    export default defineConfig({
      plugins:[
        AutoImport({
          imports: ['vue', 'vue-router'],
          //为true时在项目根目录自动创建components.d.ts
          dts: true
        }),
      ]
    })
    

    这样配置之后我们就可以不引入直接使用api

    <script setup lang="ts">
      // 用户名
      const userName = ref<string>('');
      const router = useRouter();
    </script>
    
    1. unplugin-vue-components

      这个插件可以自动引入我们自定义的组件以及第三方的组件。首先我们安装插件:

      npm i unplugin-vue-components -D

      随后在vite.config.js文件中配置:

      import Components from 'unplugin-vue-components/vite'
      import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
      ​
      export default defineConfig({
         plugins:[
           AutoImport({
             resolvers: [ElementPlusResolver()],
             imports: ['vue', 'vue-router'],
             //为true时在项目根目录自动创建components.d.ts
             dts: true
         }),
         Components({
            resolvers: [ElementPlusResolver()],
            //自动加载的组件目录,默认值为 ['src/components']
            dirs: ['src/components'],
            //组件名称包含目录,防止同名组件冲突
            directoryAsNamespace: true,
            //指定类型声明文件,为true时在项目根目录创建
            dts: true
          })
        ]
      })
      

      这样配置之后我们就可以不引入组件直接使用(以nav-menu组件为例):

      截屏2022-07-03 21.03.08.png 在其他页面中使用nav-menu组件不需要手动引入而直接使用:

      <script setup lang="ts">
        // import navMenu from '@/components/nav-menu'; 不需要引入
      </script>
      <template>
        <nav-menu></nav-menu>
      </template>
      

      我们还可以使用element-plus组件也不需要手动引入而直接使用:

      <script setup lang="ts">
        // import { ELMessage } from 'element-plus'; 不需要引入
        ELMessage.success('hello world')
      </script>
      
    2. mitt

      这是一个vue3官网推荐的跨组件通信方式,mitt好在哪里呢?首先它足够小,仅有200bytes,其次支持全部事件的监听和批量移除,它还不依赖Vue实例,所以可以跨框架使用,React或者Vue,甚至jQuery项目都能使用同一套库。首先我们安装它:

      npm install --save mitt

      然后在全局挂载:

      // main.ts
      import { createApp } from 'vue';
      import App from './App.vue';
      import mitt from "mitt"const app = createApp(App)
      app.config.globalProperties.$mitt = mitt()
      

      使用方式其实跟vue2中的bus差不多:

      // 发送事件
      $mitt.emit('foo', { userName: 'zengge' })
      ​
      // 接收事件
      $mitt.on('foo', (info) => { console.log(info.userName) })
      ​
      // 清除所有的事件
      $mitt.all.clear()
      
  • vscode插件

    1. koroFileHeader

      这是vscode中的注释插件,用于生成文件头部注释和函数注释。上文中生成函数注释的时候使用这个就特别方便。

      截屏2022-07-03 20.40.46.png

      使用control + command + t来生成函数注释:

      截屏2022-07-03 20.42.51.png

      可以在setting.json中来手动配置:

      截屏2022-07-03 20.45.17.png