【脚手架开发】技术实现以及用到的一些包

132 阅读7分钟

一、前言

相信大家都使用过脚手架来创建项目, 比如你可以用 @vue/cli 创建 vue 项目、 用 create-vite 创建 vite + react/vue 项目、 或者用 create-react-app 创建 webpack + react 项目

npm install -g @vue/cli
vue create vue-project

npx create-vite

npx create-react-app --template=typescript test-project2

因为自己手动来配置一个开发环境的过程是非常复杂并且繁琐的,如果每次构建项目都要从0开始,使用Webpack、Vite、Babel、Typescript等工具搭建项目环境,对新手来说增加了门槛,对老手来说也是个负担。所以社区中已经将大量重复、被验证有效的模式封装成开箱即用的脚手架工具,包括上面提到的那些

脚手架能够帮助我们迅速搭建一套符合业界实践的开发环境,里面集成了各种工具,能够快速搭建出开发环境

既然有开源的脚手架,为什么要自己开发脚手架呢

因为 create-vite、cra、@vue/cli 等脚手架创建的项目都只是基本代码,而我们实际项目开发会封装很多东西,需要在生成的代码基础上做很多修改。每个项目都要从 0 到 1 做很多事情,当项目多了就很繁琐。那么我们可以把封装好的项目结构做成脚手架的模版,然后直接用这个模版创建项目

大公司的基建基本都有自己的脚手架,封装一些项目模版,可以做到开箱即用

二、实现原理

1. 模版发包

首先当然是需要有已经搭建好了的项目模版, create-vite脚手架是直接将模版放在了本地的,这样不好单独管理模版的版本

image.png

于是当我们将模版环境搭建测试好以后,可以选择将每个template单独发包,这样每个版本都有版本历史。当用户选择了特定的模版之后,使用npminstall包将模版下载到目标目录当中即可

使用的包

npminstall

我们需要从npm仓库下载模版到目标目录,就会使用该包来下载。使用npminstall来下载包实现的实际上是pnpm的下载逻辑

npm install --save npminstall

// src/npminstall.js
const npminstall = require('npminstall');

(async () => {
  await npminstall({
    pkgs: [
        { name: '需要下载的包的名称', version: 'latest' },
    ],
    // 目标文件夹
    root: process.cwd() + '/test',    
    // npm源
    registry: 'https://registry.npmjs.org',
  });
})().catch(err => {
  console.error(err);
});

2. 项目组织管理——monorepo风格

这样的话我们的一个脚手架项目中会包含很多包(模版包、cli包、utils包等等),如果为每一个包都要单独创建一个git仓库的话,这些 git 仓库每个都要单独来一套编译、lint、发包等工程化的工具和配置,重复很多次,并且包越多仓库越多并且难以管理

工程化部分重复还不是最大的问题,最大的问题有下面三个

  1. 首先是包互相依赖的问题,如果A依赖于B,B的代码更新之后需要重新发包,A这边也需要重新下载才能够使用到B的最新代码;确实,我们还有npm link可以使用来简化上面这个问题,但是包越多,npm link重复的越多,并且你还要根据包之间的依赖关系去link。这样一想,是不是头都大了
  2. 其次是当你需要在每个包里执行命令的时候,需要分别进入到不同的目录中多次。最关键的是有一些包需要根据依赖关系来确定执行命令的先后顺序
  3. 最后是版本更新的问题,在需要更新版本时,要手动更新所有包的版本,如果这个包更新了,那么依赖它的包也要手动更新发个新版本才行,这也是一件麻烦的事情并且手动更新包的版本容易出错

所以整个脚手架项目我们选择使用monorepo的组织架构管理,monorepo 是多个包在同一个git仓库中管理的方式,每个包都有自己的package.json文件,可以进行单独发包,主流的开源包基本都是用 monorepo 的形式管理的

多包的管理我们选择pnpm workspace + changeset方式,这种方式可以很好的解决上面的问题~

使用的包

@changeset/cli

changeset可以用来自动控制版本并且批量发包,假如A包依赖于B包,基于changeset修改B包的版本之后,A包版本也会自动变化,不用手动修改。并且基于changeset来发包的话会为我们的包打上tag标签

// 下载changeset
pnpm add --save-dev -w @changesets/cli prettier-plugin-organize-imports prettier-plugin-packagejson 
// 初始化
npx changeset init

初始化命令执行完之后项目会多一个.changeset文件夹

image.png changeset会基于git上次的commit来判断代码有没有进行变更,如果代码变更后执行npx changeset add命令可以对指定包修改major、minor等版本号。命令执行完毕后.changeset 下会生成一个临时文件记录着这次变更的信息,然后执行 version 命令可以在版本变化的包下生成最终的 CHANGELOG.md 还有更新版本信息

// 添加变更,升级版本
npx changeset add

// 更新版本信息
npx changeset version

// 发包,并且会自动打上tag
npx changeset publish

3.声明自己的命令

假如我们运行 vue create myapp 命令来创建一个 Vue 工程。

如果我们没有运行 npm install -g vue-cli 安装 vue-cli 脚手架,在命令行窗口中直接运行 vue create myapp,会报错,报错如下图所示:

image.png

可见 vue 不是系统命令, vue 只是 vue-cli 脚手架声明的一个命令

那么如何给自己的脚手架声明一个命令呢?关键点在于package.json文件中的bin字段。一般我们都会为脚手架项目创建一个cli包,在该包的package.json中声明该脚手架的命令

{
  "name": "cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "bin":{
    "pika-cli": "./dist/index.js"
  }
}

这样我们就声明了一个叫做pika-cli的命令,./dist/index.js文件夹就是我们使用pika-cli命令之后会运行的文件夹

4.域包

域包,一般是以@开头的,如@babel/xxx@alilc/xxx,这种命名方式允许开发者在一个命名空间下组织和管理多个相关的包,而不用担心包名冲突的问题。域包也允许开发者将相关的包组织在一起,便于管理和维护。我们可以在npm官网上注册/登录账号来创建我们自己的域

image.png

image.png

这样我就创建了一个叫做@pika-cli的域包,我可以将这个脚手架所需要的包名都改为这个域下的,比如上面我们提到过的cli包。注意需要将publishConfig配置修改为access:public,因为域包默认是私有的,如果你希望社区能够贡献代码或者使用你的包,那么将域包设置为公共的可以降低使用门槛

image.png

5.命令行交互

我们使用inquirer 来做交互,实现类似于下面create-vite选择模版的效果,脚手架中会根据用户的选择为用户下载不同的模版到目标目录当中

image.png

使用的包

inquirer

下面是使用该包的一个小案例和效果,相信大家看过之后都能知道该包大概是干什么的了,详细使用细节请移步www.npmjs.com/package/inq…

import { select, input, confirm } from "@inquirer/prompts";

// 选择项目模版
    const projectTemplate = await select({
        message: '请选择项目模版',
        choices: [
          {
            name: 'react 项目',
            value: 'react'
          },
          {
            name: 'vue 项目',
            value: 'vue'
          }
        ],
    });

    // 输入项目名
    let projectName = '';
    while(!projectName) {
        projectName = await input({ message: '请输入项目名' });
    }
    
    console.log(projectTemplate,projectName)

image.png

三、结语

以上只是一些脚手架开发的技术实现/原理,并不是详细的步骤流程

对于前端工程化和开发脚手架,笔者也还在还在摸索和实践的阶段,有什么错误的地方请读者多多见谅。关于之后开发的的详细步骤也会记录下来陆续发出