前言
把公司的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
的一般管理方案有lerna
、yarn workspace
、pnpm
(后起之秀)。
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/node
和 mini-vite/src/client
中ts配置不同,因此可通过extends
对tsconfig.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.cjs
eslint配置如下:
// @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 -w
,ts-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