Vite源码系列——开发环境搭建

809 阅读7分钟

前言

把公司的vue cli项目迁移到vite后,编译速度从之前的5分钟到现在秒开!!简直是惊艳。因此“闲来无事”就浅读了一下vite部分代码,才发现也好惊艳!! 不管是从项目架构、代码组织、功能的实现可圈可点之处太多(可能我太菜鸡,看到都觉得很🐂),不得不说vue团队强的。

我认为学习新知识首先从模仿开始,因此想一点点的去实现一个vite,然后在这个过程中不断反问自己:这里怎么写?为什么这么写?怎样能更好?

那先从初始化项目开始吧~

初始化项目

目录结构

.
├── packages
│   ├── mini-vite
│   │   ├── bin
│   │   │   └── vite.js
│   │   ├── dist
│   │   ├── package.json
│   │   ├── rollup.config.js
│   │   ├── src
│   │   │   ├── client
│   │   │   └── node
│   │   │       ├── __tests__
│   │   │       │   └── sum.test.ts
│   │   │       ├── cli.ts
│   │   │       ├── index.ts
│   │   │       ├── sum.ts
│   │   │       └── tsconfig.json
│   │   ├── tsconfig.base.json
│   │   └── types
│   └── other
├── .editorconfig
├── .eslintrc.cjs
├── .gitattributes
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── jest.config.js
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
│   └── verifyCommit.ts

从目录结构得知,项目采用了Monorepo——在一个大的项目仓库(repo)中 管理多个模块/包(package),即在packages下分多个项目的管理模式 ,而Monorepo的好处主要有两点:

  • 统一管理:在一个仓库中统一管理所有的包,比如从业务代码中抽离出来的组件库,工具包等
  • 依赖提升:若多个包都依赖同一个包——lodash,则可通过workspace工作空间提升到最外层,以达到复用效果,减少项目体积。

Monorepo的一般管理方案有lernayarn workspacepnpm(后起之秀)。

vite项目选择了pnpm来做依赖包的管理,pnpm相关可阅读:Pnpm: 最先进的包管理工具;pnpm还解决了包管理中的Phantom dependencies(幽灵依赖)以及NPM doppelgangers(依赖重复安装)的痛点。

pnpm配置

可先阅读pnpm官方文档

pnpm init:新建package.json

// 添加engines以限定node以及pnpm版本
// 配置overrides以重写vite的依赖,就是在当前项目中vite的依赖都用workspace中的包覆盖
{
  "engines": {
    "node": ">=10",
    "pnpm": ">=3"
   },
  "pnpm": {
    "overrides": {
      "mini-vite": "workspace:*"
    }
  }
}

新建pnpm-workspace.yaml

pnpm-workspace.yaml定义了 pnpm工作空间 (workspace是一种协议)的根目录,并能够使您从工作空间中包含 / 排除目录 。 默认情况下,包含所有子目录。

pnpm 用 add安装依赖,而要为子包单独安装依赖的话,pnpm提供了filter指令,如我想把某个包单独安装到mini-vite中,就可以执行如下命令。pnpm add tslib -D --filter mini-vite

typescript+rollup配置

typescript配置

typescript、rimraf这样通用的依赖(即每个子包都会用到),通常可以把他们安装到根目录。

-W 表示你确认此依赖是通用依赖,要安装到根目录,不加的话就会抛出错误,当然也可选择忽略。

安装 tslib 工具库,为了可使用typescript的工具函数,例如继承的 __extends,用于异步函数的 __awaiter。

// 安装typescript后,新建并配置tsconfig.base.json文件
pnpm add typescript -D -W

//  安装 tslib
pnpm add tslib -D --filter mini-vite

在mini-vite目录先新建tsconfig.base.json作为基础的ts配置,因为mini-vite/src/nodemini-vite/src/client中ts配置不同,因此可通过extendstsconfig.base.json做继承。

rollup配置

通过阅读rollup官方文档,首先安装依赖

// 安装rollup配置相关依赖
pnpm add rollup --filter mini-vite
pnpm add @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve -D --filter mini-vite

// 安装rimraf, rollup构建会用到,用来删除原有的dist目录以重新生成新的dist目录
pnpm add rimraf -D -W

新建rollup.config.js,同时往package.json添加命令行脚本"dev": "rimraf dist && rollup -c -w",

rollup.js编译源码中的模块引用默认只支持 ES6+的模块方式import/export。然而大量的npm模块是基于CommonJS模块方式,这就导致了大量 npm 模块不能直接编译使用。所以辅助rollup.js编译支持 npm模块和CommonJS模块方式的插件就应运而生。

  • rollup-plugin-node-resolve 插件允许加载第三方模块
  • @rollup/plugin-commons 插件将npm模块转换为ES6版本
// node API——path,用来处理路径
import path from 'path'
// 允许加载第三方模块
import nodeResolve from '@rollup/plugin-node-resolve'
// rollup支持typescript文件的编译
import typescript from '@rollup/plugin-typescript'
// 将npm模块转换为ES6版本
import commonjs from '@rollup/plugin-commonjs'
// 将.json文件转换为ES6模块(前面说了rollup模块引用只支持ES6+)
import json from '@rollup/plugin-json'

export default (commandLineArgs)=> {
  // 执行dev脚本命令时,rollup -c -w 会带有(watch: Boolean)参数
  // 通过命令行是否存在watch监听的参数,判断是否是dev环境
  const isDev = commandLineArgs.watch
  const isProduction = !isDev
  return [
    {
      input: {
        index: path.resolve(__dirname, 'src/node/index.ts'),
        cli: path.resolve(__dirname, 'src/node/cli.ts')
      },
      output: {
        dir: path.resolve(__dirname, 'dist'),
        entryFileNames: `node/[name].js`,
        chunkFileNames: 'node/chunks/dep-[hash].js', // 用于代码分割时,对公共模块的命名格式
        exports: 'named', // 使用命名导出,区别于export default https://rollupjs.org/guide/en/#outputexports
        format: 'cjs',
        externalLiveBindings: false, // rollup不会对导入的模块进行监听绑定?——代码优化
        freeze: false,
        // 是否生成源代码
        sourcemap: !isProduction
      },
      // dependencies依赖排除,不打进包内;devDependencies在开发环境排除,rollup执行时不编译
      external: [
        ...Object.keys(require('./package.json').dependencies),
          ...(isProduction
            ? []
            : Object.keys(require('./package.json').devDependencies))
      ],
      plugins: [
        nodeResolve({ preferBuiltins: true }),
        typescript({
          tsconfig: 'src/node/tsconfig.json',
          module: 'esnext',
          target: 'es2019',
          include: ['src/**/*.ts', 'types/**'],
          exclude: ['src/**/__tests__/**'],
          esModuleInterop: true,
        }),
        commonjs({
          extensions: ['.js'],
        }),
        json(),
      ]
    }
  ]
}

至此便可开始在开发环境coding了。执行pnpm run dev --filter mini-vite ,即可跑起来,并生成了dist目录。

只是还有很多不完善的地方,比如代码规范, 代码规范包括git提交规范、代码语法规范、代码格式规范,它们分别对应Git hooks控制(commit message的检查)、eslint检查、prettier格式化;还有代码质量——jest单元测试;下面完善一下吧~

代码规范

Git Hooks🪝

Git 能在特定的重要动作发生时触发自定义脚本,其中比较常用的有:pre-commit、commit-msg、pre-push 等钩子(hooks)。我们可以在 pre-commit 触发时进行代码格式验证,在 commit-msg 触发时对 commit 消息和提交用户进行验证,在 pre-push 触发时进行单元测试、e2e 测试等操作。

Git 在执行 git init 进行初始化时,会在 .git/hooks 目录生成一系列的 hooks 脚本

每个脚本的后缀都是以 .sample 结尾的,在这个时候,脚本是不会自动执行的。我们需要把后缀去掉之后才会生效,即将 pre-commit.sample 变成 pre-commit 才会起作用。

这篇文章详细介绍了如何编写 git hooks 脚本,这里使用simple-git-hooks通过简单配置即可执行git hooks脚本,同时安装lint-staged在Git操作时辅以执行其他操作。

安装:pnpm add simple-git-hooks lint-stage -D -W

package.json添加配置:

// package.json
// lint-staged会执行代码检查
{
  "simple-git-hooks": {
    "pre-commit": "pnpm exec lint-staged --concurrent false",
    "commit-msg": "pnpm exec ts-node scripts/verifyCommit.ts $1"
  },
  "lint-staged": {
    "*": [
      "prettier --write --ignore-unknown"
    ],
    "packages/*/{src,types}/**/*.ts": [
      "eslint --ext .ts"
    ],
    "packages/**/*.d.ts": [
      "eslint --ext .ts"
    ]
  }
}

可以看到 "commit-msg": "pnpm exec ts-node scripts/verifyCommit.ts $1" 即提交commit message的时候去执行pnpm exec ts-node scripts/verifyCommit.ts $1命令,而scripts/verifyCommit.ts中则是校验message的相关代码,如下:

import colors from 'picocolors'
import { readFileSync } from 'fs'
// 获取commit message在.git中存储的路径,然后用readFileSync对文本进行读取
const msgPath = process.argv[2]
const msg = readFileSync(msgPath, 'utf8').trim()
// 以下对msg进行校验
// 正则校验!!
const releaseRE = /^v\d/
const commitRE =
  /^(revert: )?(feat|fix|docs|dx|refactor|perf|test|workflow|build|ci|chore|types|wip|release|deps)((.+))?: .{1,50}/

if (!releaseRE.test(msg) && !commitRE.test(msg)) {
  console.error(
    `  ${colors.bgRed(colors.white(' ERROR '))} ${colors.red(
      `invalid commit message format.`
    )}\n\n` +
      colors.red(
        `  Proper commit message format is required for automated changelog generation. Examples:\n\n`
      ) +
      `    ${colors.green(`feat: add 'comments' option`)}\n` +
      `    ${colors.green(`fix: handle events on blur (close #28)`)}\n\n` +
      colors.red(`  See .github/commit-convention.md for more details.\n`)
  )
  // 在执行脚本时,如果以非零的值退出程序,将会中断 git 的提交/推送流程。
  // 所以在 hooks 脚本中验证消息/代码不通过时,就可以用非零值进行退出,中断 git 流程。
  process.exit(1)
}

测试一下~

代码语法规范

语法规范规范了代码中,哪些语法可用,哪些不可用,以保证代码的可读性与可维护性(按一样的规范,团队成员阅读代码时更没障碍),如不能使用var命名变量,不能使用require导入指定模块等。

//  安装eslint;eslint-define-config用来自定义eslint的配置
pnpm add eslint eslint-define-config -D -W

// 安装eslint相关拓展(可选),用来拓展eslint的规则
pnpm add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-node -D -W

.eslintrc.cjseslint配置如下:

// @ts-check
const { defineConfig } = require('eslint-define-config')

module.exports = defineConfig({
  root: true,
  // 拓展eslint规则
  extends: [
    'eslint:recommended',
    'plugin:node/recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module',
    ecmaVersion: 2021
  },
  // 自定义规则
  rules: {
    'no-debugger': ['error'],
    'node/no-restricted-require': [
      'error',
      Object.keys(require('./packages/mini-vite/package.json').devDependencies).map(
        (d) => ({
          name: d,
          // 自定义报错信息
          message:
            `devDependencies can only be imported using ESM syntax so ` +
            `that they are included in the rollup bundle. If you are trying to ` +
            `lazy load a dependency, use (await import('dependency')).default instead.`
        })
      )
    ]
    // ...省略
  },
  // 更细粒度地控制eslint规则,如某文件下,新增某规则
  overrides: [
    {
      files: ['packages/mini-vite/src/node/**'],
      rules: {
        // 该文件下用console将会报错
        'no-console': ['error']
      }
    }
    // ...省略
  ]
})

同时往package.json添加命令行脚本"lint": "eslint packages/*/{src,types}/**",然后执行该命令,就可以在控制台看到elsint跑出来的结果了。

代码格式规范

格式规范就是好不好看的问题,整齐划一的代码风格可以让阅读者如沐春风~,因此这里借助prettier来格式化代码。

安装:pnpm add prettier -D -W

.prettierrc.json文件配置如下:

// semi:是否带分好;,tabWidth:tab的缩进为2;singleQuote:用单引号
{
  "semi": false,
  "tabWidth": 2,
  "singleQuote": true,
  "printWidth": 80,
  "trailingComma": "none",
  "overrides": [
    {
      "files": ["*.yml"],
      "options": {
        "singleQuote": false
      }
    }
  ]
  // ...省略
}

同时往package.json添加命令行脚本"format": "prettier --write .",,然后执行该命令,就可以在控制台看到代码被prettier格式化了。

代码质量

jest单元测试

安装:pnpm add jest @types/jest ts-jest -D -wts-jest使得typescript可在jest中使用。当然你也可用babel

jest.config.js配置如下。

module.exports = {
  coverageDirectory: 'coverage',
  preset: 'ts-jest',
  testEnvironment: 'node',
  testRegex: '(/__tests__/.*|(\.|/)(test|spec))\.tsx?$'
}

同时往package.json添加命令行脚本"test": "jest --coverage",然后执行该命令,就可以在控制台看到jest单元测试的信息以及覆盖率了。

优化

以上,代码规范检查以及代码质量检查时都要手动执行命令就显得很繁琐,因此借助vscode工具中的插件拓展以及自定义配置,会让效率大大提升,比如实时的eslint检查,保存文件时执行prettier格式化,自动跑单元测试。

在项目下新建一个 .vscode 文件夹,里面提供一个 settings.json,可以为项目独立配置,更加灵活;提供extensions.json设置推荐安装的拓展(具体看源代码)。

{
  // ===
  // Event Triggers
  // ===
	// 保存自动格式化,elisnt检查,设置默认格式化defaultFormatter
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.fixAll.stylelint": true,
    "source.fixAll.markdownlint": true
  },
  "eslint.validate": ["typescript", "javascript", "javascriptreact"],
  "typescript.tsdk": "node_modules/typescript/lib",

  // ===
  // JS(ON)
  // ===
	// 在测试文件内自动跑jest单测
  "jest.autoEnable": true,
  "jest.enableCodeLens": true,
  "javascript.format.enable": false,
  "json.format.enable": false,
}

至此整个项目的开发环境基本完备,可以着手功能上的开发了。

后记

以上是阅读源码后+自己动手琢磨,不一定与源码一致,如jest单测配置,主要学习它的思路。 把开发环境搭建起来后,可以进入功能的开发了 ,其他有意思的东西也会以文章记录下来,如:

  • 发布与版本管理,vite并没有借助lerna进行版本的管理而是自己写了一套版本管理,并且配合了Github Action。
  • 命令行工具CLI的使用。
  • ws、httpServer启动以及如何通信的。
  • 文件监听以及依赖收集 。
  • HRM热更新的实现。
  • plugin插件以及middleware 中间件的实现。
  • esbuild

源码

github.com/AutumnWhj/m…,本文对应tag: master-v1.0.1-20220310

相关阅读