项目工程化 vite

631 阅读10分钟

一、项目工程化

1.1初始环境

  • node v18.17.0
  • pnpm 8.15.5
  • vite 5.2.0

1.1.1安装pnpm

pnpm:performant npm ,意味“高性能的 npm”。pnpm由npm/yarn衍生而来,解决了npm/yarn内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为“最先进的包管理工具”

pnpm安装指令

npm i -g pnpm

1.1.2搭建脚手架

pnpm create vite

进入到项目根目录pnpm i安装全部依赖.

2.2项目配置

一、配置eslint

eslint中文官网:eslint.nodejs.cn/

ESLint最初是由Nicholas C. Zakas 于2013年6月创建的开源项目。它的目标是提供一个插件化的javascript代码检测工具

vite@4.4.5自带.eslintrc.cjs (以下是默认内容, 之后要被全部替换)

module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
  },
}

1.1 react 环境代码校验插件说明(目前共14个)
###### 让所有与prettier规则存在冲突的Eslint rules失效,并使用prettier进行代码检查
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",

###### 运行更漂亮的Eslint,使prettier规则优先级更高,Eslint优先级低
"eslint-plugin-prettier": "^4.2.1",

###### 该解析器允许使用Eslint校验所有babel code
"@babel/eslint-parser": "^7.19.1",

###############################################
"@typescript-eslint/eslint-plugin": "^6.1.0", //typescript对应的eslint 规则插件
"@typescript-eslint/parser": "^6.1.0",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.33.0",
"prettier": "^3.0.0"

###### import 排序功能
"eslint-plugin-import"
"@typescript-eslint/parser"
"eslint-import-resolver-typescript"
"eslint-import-resolver-alias"

安装指令( 先装11个,再装3个 )

pnpm i eslint eslint-plugin-import eslint-plugin-react eslint-plugin-node @babel/eslint-parser @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-import @typescript-eslint/parser eslint-import-resolver-typescript eslint-import-resolver-alias -D
1.2修改.eslintrc.cjs配置文件(全选覆盖)
module.exports = {
  // 运行环境
  env: {
    browser: true,
    es2021: true,
    node: true,
    jest: true,
  },
  /* 指定如何解析语法 */
  /** 优先级低于 parse 的语法解析配置 */
  parserOptions: {
    parser: '@typescript-eslint/parser', // 指定解析器
    ecmaVersion: 'latest', // 允许解析那个版本的特性
    sourceType: 'module', // 允许使用 import
    jsxPragma: 'React',
    ecmaFeatures: {
      jsx: true, // 允许对JSX进行解析
    },
  },
  /* 继承已有的规则 */
  extends: [
    'eslint:recommended', // vite自带
    'plugin:@typescript-eslint/recommended', // vite自带 @typescript-eslint/eslint-plugin的推荐规则
    'plugin:react-hooks/recommended', // vite自带
    'plugin:import/typescript', // eslint-plugin-import 抛出导入等支持的规则
    'plugin:prettier/recommended', // eslint-plugin-prettier 的推荐规则
    'plugin:import/recommended', // import排序
  ],
  // https://stackoverflow.com/questions/64126764/how-do-i-fix-typescript-compiler-errors-on-css-files
  ignorePatterns: ['**/*.css', '**/*.scss'],
  parser: '@typescript-eslint/parser', // 指定解析器
  plugins: ['react-refresh', '@typescript-eslint', 'react', 'prettier'],
  /*
   * "off" 或 0 - 关闭规则
   * "warn" 或 1 - 打开规则作为警告(不影响退出代码)
   * "error" 或 2 - 打开规则作为错误(退出代码将为 1)
   */
  rules: {
    // react
    'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
    'react/no-children-prop': 'off',
    'react-refresh/only-export-components': 'off',

    // eslint(https://eslint.bootcss.com/docs/rules/)
    'no-var': 'error', // 要求使用 let 或 const 而不是 var
    'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-unexpected-multiline': 'error', // 禁止空余的多行
    'no-useless-escape': 'off', // 禁止不必要的转义字符
    'prettier/prettier': ['error', { endOfLine: 'auto' }],

    // typeScript (https://typescript-eslint.io/rules)
    '@typescript-eslint/no-unused-vars': 'off', // 禁止定义未使用的变量
    '@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
    '@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
    '@typescript-eslint/semi': 'off',

    // import order (https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md)
    'import/default': 'off',
    'import/no-named-as-default': 'off',
    'import/no-named-as-default-member': 'off',
    'import/order': [
      'error',
      {
        // 对导入模块进行分组,分组排序规则如下
        groups: [
          'type', // 类型文件
          'builtin', // 内置模块
          'external', // 外部模块
          'internal', // 内部引用
          'parent', // 父节点依赖
          'sibling', // 兄弟依赖
          'index', // index文件
          'object',
          'unknown',
        ],
        //通过路径自定义分组
        pathGroups: [
          {
            group: 'type',
            pattern: 'react',
            position: 'before',
          },
          {
            group: 'type',
            pattern: 'react-router-dom',
          },
          {
            group: 'type',
            pattern: '@/types/**',
            position: 'after',
          },
          {
            group: 'external',
            pattern: 'react',
            position: 'before',
          },
          {
            group: 'external',
            pattern: 'react-router-dom',
            position: 'before',
          },
          {
            group: 'internal',
            pattern: '@/**', // 把@开头的应用放在当前分组后面
          },
        ],
        pathGroupsExcludedImportTypes: ['react', 'react-router-dom'],
        distinctGroup: true, // 是否开启独特组,用于区分自定义规则分组和其他规则分组
        'newlines-between': 'never', // 每个分组之间换行
        alphabetize: { order: 'asc' }, // 相同分组排列规则 按字母升序排序
      },
    ],
  },
  settings: {
    'import/resolver': {
      typescript: true,
      node: true,
      alias: {
        map: [['', './public']],
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    },
  },
}

1.3创建.eslintignore忽略文件
build
dist
node_modules
1.4配置脚本

package.json新增两个运行脚本

  "scripts": {
    ...
    "fix": "eslint src --fix",
  },

通过pnpm run lint去检测语法,如果出现不规范格式,通过pnpm run fix 修改

二、配置prettier

有了eslint,为什么还要有prettier?eslint针对的是javascript,他是一个检测工具,包含js语法以及少部分格式问题,在eslint看来,语法对了就能保证代码正常运行,格式问题属于其次;

而prettier属于格式化工具,它看不惯格式不统一,所以它就把eslint没干好的事接着干,另外,prettier支持

包含js在内的多种语言。

总结起来,eslint和prettier这俩兄弟一个保证js代码质量,一个保证代码美观。

2.1安装依赖包

先装7个,再装3个

pnpm i prettier eslint-plugin-prettier eslint-config-prettier -D
2.2创建.prettierrc.cjs, 添加规则
// https://prettier.io/docs/en/options.html
module.exports = {
  printWidth: 100, //单行长度
  tabWidth: 2, //缩进长度
  useTabs: false, //使用空格代替tab缩进
  semi: false, //句末分号
  singleQuote: true, //js中使用单引号
  jsxSingleQuote: false, // jsx中使用双引号
  quoteProps: 'as-needed', //仅在必需时为对象的key添加引号
  trailingComma: 'all', //多行时尽可能打印尾随逗号
  bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar }
  bracketSameLine: false, //多属性html标签的‘>’折行放置
  arrowParens: 'always', //单参数箭头函数参数周围使用圆括号-eg: (x) => x
  requirePragma: false, //无需顶部注释即可格式化
  insertPragma: false, //在已被preitter格式化的文件顶部加上标注
  proseWrap: 'preserve', //什么都不做,让散文保持原样
  htmlWhitespaceSensitivity: 'ignore', //对HTML全局空白不敏感
  vueIndentScriptAndStyle: true, //不对vue中的script及style标签缩进
  endOfLine: 'lf', //结束行形式
  embeddedLanguageFormatting: 'auto', //对引用代码进行格式化
  singleAttributePerLine: false, //在 HTML、Vue 和 JSX 中强制每行使用单一属性
}

2.3创建.prettierignore忽略文件
/.git
/.vscode
/build
/dist/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*
2.4配置脚本
 "scripts": {
    ...
    "format": "prettier --write \"./src/**/*.{tsx,ts,jsx,js,json,html,md}\"",
 }

代码格式化 pnpm run format

CTRL + SHIFT + P 输入 reload window 重启VsCode生效

image.png

三、配置husky

在上面我们已经集成好了我们代码校验工具,但是需要每次手动的去执行命令才会格式化我们的代码。如果有人没有格式化就提交了远程仓库中,那这个规范就没什么用。所以我们需要强制让开发人员按照代码规范来提交。

要做到这件事情,就需要利用husky在代码提交之前触发git hook(git在客户端的钩子),然后执行 pnpm run format来自动的格式化我们的代码。

安装 husky

pnpm install -D husky@8.0.3

查看包版本 npm view husky versions

执行

pnpm exec husky init

会在根目录下生成个一个.husky目录,在这个目录下面会有一个pre-commit文件,这个文件里面的命令在我们执行commit的时候就会执行

尝试在.husky/pre-commit文件添加如下命令:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm fix
pnpm format
git add .

.husky/pre-commit文件在之后配置 lint-staged 时还要再次被修改! 在这里写是为了理解文件可以写一些什么代码, 方便理解.

当我们对代码进行commit操作的时候,就会执行命令,对代码进行格式化,然后再提交。

四、配置cz-customizable

修改 package,json 中的 "type": "commonjs",

pnpm i -D cz-customizable

根目录创建.cz-config.js

module.exports = {
  types: [
    {
      value: '✨ feat',
      name: '✨ feat:     新功能',
    },
    {
      value: '🐛 fix',
      name: '🐛 fix:      修复bug',
    },
    {
      value: '🚀 chore',
      name: '🚀 chore:    构建/工程依赖/工具',
    },
    {
      value: '💄 style',
      name: '💄 style:    代码的样式美化',
    },
    {
      value: '♻️ refactor',
      name: '♻️  refactor: 重构',
    },
    {
      value: '⚡️ perf',
      name: '⚡️ perf:     性能优化',
    },
    {
      value: '✅ test',
      name: '✅ test:     测试',
    },
    {
      value: '🎉 init',
      name: '🎉 init:     初始化',
    },
    {
      value: '📝 docs',
      name: '📝 docs:    文档变更',
    },
    {
      value: '⏪️ revert',
      name: '⏪️ revert:   回退',
    },
    {
      value: '🔧 config',
      name: '🔧 config:   修改配置',
    },
    {
      value: '📦️ build',
      name: '📦️ build:    打包',
    },
    {
      value: '👷 ci',
      name: '👷 ci:       CI related changes',
    },
  ],
  messages: {
    type: '请选择提交类型(必填)',
    customScope: '请输入文件修改范围(可选)',
    subject: '请简要描述提交(必填)',
    body: '请输入详细描述(可选)',
    breaking: '列出任何BREAKING CHANGES(可选)',
    footer: '请输入要关闭的issue(可选)',
    confirmCommit: '确定提交此说明吗?',
  },
  allowCustomScopes: true,
  allowBreakingChanges: [':sparkles: feat', ':bug: fix'],
  subjectLimit: 72,
}

配置脚本, 注意修改"type": "commonjs"

  "type": "commonjs",
  "scripts": {
    ...
    "prepare": "husky install",
    "commit": "cz-customizable"
  },

以后使用 pnpm commit 代替 git commit -m

但这样做还是会有人使用 git commit,所以要配置 commitlint

五、配置commitlint

git log 日志规范

5.1安装规范包

带gitmoji图标( ✨🎨🔥🐛👽️👥🤡🙈🧐🌱 ... )

pnpm add @commitlint/cli  commitlint-config-cz  commitlint-config-git-commit-emoji  -D
5.2创建commitlint.config.cjs(注意是cjs),然后添加下面的代码:
module.exports = {
  extends: ['git-commit-emoji', 'cz']
}
5.3在 package.json中配置scripts命令
# 在scrips中添加下面的代码
  "scripts": {
    ...
    "commitlint": "commitlint --config commitlint.config.cjs -e -V"
  },
5.4配置husky
npx husky add .husky/commit-msg 

在生成的commit-msg文件中添加下面的命令

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm run commitlint

这样git commit -m就提交不了了,必须用pnpm commit提交!

六、配置 lint-staged

pnpm i lint-staged -D

package.json文件

"scripts": {
  ...
  "lint-staged": "lint-staged"
},
"lint-staged": {
  "./src/**/*.{html,tsx,ts,jsx,js,json,vue}": [
    "eslint src --fix",
    "prettier --write \"./src/**/*.{tsx,ts,jsx,js,json,html,md}\""
  ]
},

.husky\pre-commit 文件内容如下:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm run lint-staged

lint-staged 的作用是只校验 git add 中的文件

没有被 git add 的文件不校验

七、强制使用pnpm包管理器工具

团队开发项目的时候,需要统一包管理器工具,因为不同包管理器工具下载同一个依赖,可能版本不一样,

导致项目出现bug问题,因此包管理器工具需要统一管理!!!

在根目录创建 scripts/preinstall.js文件,添加下面的内容

if (!/pnpm/.test(process.env.npm_execpath || '')) {
  console.warn(
    `\u001b[33mThis repository must using pnpm as the package manager ` +
    ` for scripts to work properly.\u001b[39m\n`,
  )
  process.exit(1)
}

package.json配置命令

"scripts": {
  ...
  "preinstall": "node ./scripts/preinstall.js"
},

当我们使用npm或者yarn来安装包的时候,就会报错了。原理就是在install的时候会触发preinstall(npm提供的生命周期钩子)这个文件里面的代码。

八、配置 @ 别名

安装 node ts 包

pnpm i @types/node -D

配置 vite.config.ts 进行语法上的解析

import { join } from 'node:path'
import react from '@vitejs/plugin-react-swc'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      // 配置 @ 的路径别名
      '@': join(__dirname, './src/'),
    }
  }
})

配置 tsconfig.json 进行编辑器的代码提示功能

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

vscode中执行Ctrl+Shift+P输入reload window重启vscode

image.png

如果不配置此项, 会发现vscode引入文件中有波浪线警告

image.png

二、项目最终配置

package.json

{
  "name": "demo",
  "private": true,
  "version": "0.0.0",
  "type": "commonjs",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "fix": "eslint src --fix",
    "format": "prettier --write \"./src/**/*.{tsx,ts,jsx,js,json,html,md}\"",
    "prepare": "husky",
    "commit": "cz-customizable",
    "commitlint": "commitlint --config commitlint.config.cjs -e -V",
    "lint-staged": "lint-staged",
    "preinstall": "node ./scripts/preinstall.js"
  },
  "lint-staged": {
    "./src/**/*.{html,tsx,ts,jsx,js,json,vue}": [
      "eslint src --fix",
      "prettier --write \"./src/**/*.{tsx,ts,jsx,js,json,html,md}\""
    ]
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/eslint-parser": "^7.24.1",
    "@commitlint/cli": "^19.2.1",
    "@types/node": "^20.12.2",
    "@types/react": "^18.2.66",
    "@types/react-dom": "^18.2.22",
    "@typescript-eslint/eslint-plugin": "^7.4.0",
    "@typescript-eslint/parser": "^7.4.0",
    "@vitejs/plugin-react-swc": "^3.5.0",
    "commitlint-config-cz": "^0.13.3",
    "commitlint-config-git-commit-emoji": "^1.0.0",
    "cz-customizable": "^7.0.0",
    "eslint": "^8.57.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-import-resolver-alias": "^1.1.2",
    "eslint-import-resolver-typescript": "^3.6.1",
    "eslint-plugin-import": "^2.29.1",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-prettier": "^5.1.3",
    "eslint-plugin-react": "^7.34.1",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.6",
    "husky": "^8.0.3",
    "lint-staged": "^15.2.2",
    "prettier": "^3.2.5",
    "typescript": "^5.2.2",
    "vite": "^5.2.0"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* Mine Custom Config */
    "baseUrl": "./",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

三、react组件通信方式

组件通信方式很重要,不管是项目还是面试都是经常用到的知识点。

  • props: 可以实现父子组件、子父组件、甚至兄弟组件通信(状态提升)
  • context: 可以实现任意组件通信
  • 集中式状态管理等: Redux、Mobox、Zustand、Resso 等...
  • 消息订阅-发布: pubsub 等...