Yongzhi

theme

monorepo工程化实践 - 脚手架开发

背景

因当下场景需要快速创建项目,目前参考了viteastro等脚手架的工程化实践,记录下本次monorepo工程化实践。废话不多说下面开始👇

monorepo项目搭建

有关于什么是monorepo这里不过多赘述,有兴趣可以点击博客查看,我个人推荐使用pnpm作为包管理工具,因为pnpm的特性可以很好的解决monorepo的依赖管理问题,这里不过多赘述,有兴趣可以点击博客查看。

我个人推荐使用pnpm创建项目,相关细节请移步pnpm monorepo查看。

初始化项目

mkdir terky
cd terky
pnpm init

新建pnpm-workspace.yaml文件,配置packages目录下的所有包

packages:
  - 'packages/*'

因为这个项目是一个脚手架,所以不仅要有脚手架包,还要有模板包,这是我的项目目录结构

├─packages
│  ├─create-terky
│     ├─template-vue3-ts
│     ├─template-vue3
│     ├─src

eslint

此配置作为基础配置,会被packages下的包继承

  • eslint eslint核心包
  • @typescript-eslint/parser eslint解析器,用于解析typescript代码
  • @typescript-eslint/eslint-plugin eslint插件,用于检查typescript代码
pnpm add eslint -D -w
pnpm add @typescript-eslint/parser -D -w
pnpm add @typescript-eslint/eslint-plugin -D -w

新建.eslintrc.json文件,配置eslint

{
  "parser": "@typescript-eslint/parser",
  "env": {
    "es6": true
  },
  "extends": [
      "eslint:recommended",
      "plugin:@typescript-eslint/recommended"
  ],
  "parserOptions": {
    "sourceType": "module",
    "ecmaVersion": 2021
  },
  "rules": {

  }
}

commit规范

首先全局安装 commitizen,这样可以在本地使用czgit cz替代git commit

pnpm add -g commitizen

目前有很多commit工具帮助我们进行规范化commit格式,关于commit规范你可以点击这里查看 推荐使用cz-git,官方提供了commit规范模版,以及支持emoji、中文英文双语,而且官网也是中文描述

pnpm add cz-git -D -w

具体如何配置,请移步官网文档查看,写的很清楚,可以直接选择喜欢的配置模版直接使用,建议搭配commitlint使用。安装完成后向.commitlintrc.js添加检测规则

{
  ...
  extends: ['@commitlint/config-conventional']
  ...
}

添加scripts脚本命令

{
  "scripts": {
    "commitlint": "commitlint --config .commitlintrc.js -e -V"
  }
}

commitlint需要搭配husky,需要添加commit-msg hooks

pnpm add husky @commitlint/{config-conventional,cli} -D -w
husky install
npx husky add .husky/commit-msg 'npm run commitlint'

create-terky

terky是一个现代化的脚手架,通过使用npm create terky可以快速从github的模版列表中拉取我们所需要的项目基础模版,省去重复的webpack配置 和一起其他的项目配置

  • npm create <package> 是什么?

一般在初始化项目的时候,都会去执行npm init,会在当前目录生成一个package.json文件,在init后面还可以跟一个参数<initializer>, 当增加上这个参数时,npm会去查询名为create-<initializer>这个包,如果本地存在那么就执行本地缓存,反之则同步远程包到本地执行。使用 npm exec执行create-<initializer>包的bin下定义的命令create-<initializer>bin是在package.json中定义的。 npm v6版本给init命令增加了一个别名create,所以 init等于create,详细信息可以点击这里查看

实战

首先需要在本地初始化一个package.json,并安装所需依赖

pnpm init

pnpm add -D typescript
pnpm add -D unbuild
pnpm add prompts
pnpm add ora
pnpm add kolorist
pnpm add fs-extra
pnpm add execa
pnpm add minimist
  • typescript 我使用typescript编写项目
  • unbuild 打包typescript代码
  • prompts 给用户提供交互式选择
  • ora 可以在控制台使用loading动画
  • kolorist 修改控制台中输出的文字的颜色
  • fs-extra 基于fs模块更好的文件操作API
  • execa 在nodejs中执行命令
  • minimist 解析传入的参数

使用tsc --init初始化tsconfig.json

{
  "include": ["build.config.ts","src"],
  "compilerOptions": {
    "outDir": "dist",
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "declaration": false,
    "sourceMap": false,
    "noUnusedLocals": true,
    "esModuleInterop": true,
    "noEmit": true,
    "allowImportingTsExtensions": true
  }
}

创建build.config.ts提供unbuild打包配置,并在package.json中增加命令

import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
    entries: ['src/index.ts'],
    clean: true,
    rollup: {
        inlineDependencies: true,
        esbuild: {
            minify: true,
        },
    },
})
{
  ...
  "script": {
    "build": "unbuild"
  },
  ...
}

新建index.js提供程序主入口

#!/usr/bin/env node
import "./dist/index.mjs"

src目录结构如下

├─src
|  ├─index.ts 
|  ├─utils.ts 
|  ├─actions 
|     ├─utils.ts 
  • utils.ts

在这个文件中需要提供一些工具函数及问答交互,有关prompts的用法请移步文档查看

import { blue, yellow } from 'kolorist'
import fs from  'fs'
// 默认项目名称
export const defaultDir = 'terky-app'
// 问答交互
export const FRAMEWORKS = [
  {
    name: 'vue3',
    display: 'JavaScript',
    color: yellow,
    value: 'template-vue3'
  },
  {
    name: 'vue3-ts',
    display: 'TypeScript',
    color: blue,
    value: 'template-vue3-ts'
  },
]
// 判断目录是否存在
export const isEmpty = (dir: string) => fs.existsSync(dir)
// 创建项目目录
export const mkdirSync = (dir: string) => fs.mkdirSync(dir)
  • git.ts

在这个函数中其实只做了一件事,那就是将用户所选择的模版下载到本地

import path, { dirname } from "path";
import { fileURLToPath } from 'url';
import { execa } from 'execa'
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export const createRepo = async (targetDir: string, template: string) => {
  const filePath = path.join(__dirname, `../${template}`)
  await execa('cp', ['-r',filePath, targetDir])
}
  • index.ts

这个文件是一个主入口,也是我们打包的入口,执行命令其实就是相当于执行这个文件中的main函数

1.首先根据utils中定义的问答列表,向用户提问🙋。 2.根据用户输入和选择的内容判断目录是否存在 a.如果存在那么退出程序,提示用户目录已经存在 b.不存在目录那么新建目录 3.执行git中所提供createRepo函数,初始化目录并拉取模版代码

import { defaultDir, FRAMEWORKS, isEmpty, mkdirSync} from './utils.ts'
import { createRepo } from './actions/git.ts'
import { red, reset,  } from 'kolorist'
import prompts from 'prompts'
import path from "path"
import ora from 'ora'
async function main () {
  const result = await prompts([
    {
      type: 'text',
      name: 'projectName',
      message: reset('👉 项目名称 | Project name:'),
      initial: defaultDir
    },
    {
      type: 'select',
      name: 'template',
      message: reset('👉 选择模板 | Select template:'),
      choices: FRAMEWORKS.map((framework) => ({
          title: framework.color(framework.name),
          value: framework.value,
      }))
    }
  ], {
    onCancel: () => {
      console.log(red('✖') + '操作被取消 | Operation canceled')
    }
  })
  const spinner = ora('正在创建项目 | Creating project...').start()
  const { projectName, template } = result
  const root = path.join(process.cwd(), projectName)
  if (isEmpty(root)) {
    console.log(red('✖') + `目录已存在 | Directory already exists: /${projectName}`)
    ora().fail('创建失败 | Create failed')
    spinner.stop()
    return
  }
  spinner.text = '正在创建目录 | Creating directory...'
  await mkdirSync(projectName)
  await createRepo(root, template, spinner)
  spinner.succeed('创建成功 | Create success')
}

main().catch((err) => {
  console.error(err)
  process.exit(1)
})

本地测试

还记得前面所提到npm create的原理吗,现在需要向package.json中添加bin字段

{
  ...
  "bin": {
    "create-terky": "index.js"
  },
  "scripts": {
    "build": "unbuild"
  }
}

现在我们可以执行pnpm build,生成dist/index.mjs,因为我们主入口文件需要它

#!/usr/bin/env node
import "./dist/index.mjs"

完成以上步骤后,我们需要在当前目录执行npm link,将此包链接到全局就可以直接使用bin下定义的命令create-terky

发布

本地测试完成之后就可以发布到npm,在发布之前您需要先拥有一个npm账号,您可以在这里注册。如果您已经拥有账号, 您可以忽略上一步。现在还需要在本地新建一个名为.npmrc的文件,并写入内容

registry=https://registry.npmjs.org/

这个文件可以让我们在当前项目的目录下执行npm login时,总是登录到公共的npm。这样可以避免您登录到可能正在使用您自己的组织内部的私有npm服务。

现在准备工作已经完毕,开始吧

npm login

在发布之前请您确保您的package.json中的name字段在npm中是独一无二的,避免与其他模块冲突

npm public

如果没有出现error提示,那么您就可以登录npm,搜索自己的包名了。当然您也可以直接尝试npm create terky来使用我所发布的工具。

完成代码可以点击查看,您也会有好的想法或者模版, 您可以提交PR或发送邮件至<[email protected]>,我会尽快回复您🦀️🦀️。

在下面您可以看到一些对您有用的链接👇