一份更好的vite+pnpm+monorepo项目管理方案

4,873 阅读7分钟

在一家公司内部,我们同时会有很多项目,每创建一个项目就要重新搭建一份脚手架并重新复制一份公司内部的配置文件和公共组件。这对于不了解项目的新员工来说,非常不友好。每次都需要根据不同的项目重新了解一些公司的配置文件和路径,非常麻烦。但是如果有一种管理方式,可以让把所有项目放在一个管理项目中,但又可以单独启动,单独打包升级,同时,所有项目可以共用我们之前就配置好的公共组件和配置文件,就会带来很多工作上的便捷和精力。

最近出现了 monorepo 的项目管理方式,就很好的解决了这个问题,所有项目可以共用公共的代码,同时项目与项目之间互相不影响,非常方便,比较热闹的是monorepo+lerna的管理方式,同时随着yarn/pnpm新发布了workspace的功能(工作空间(Workspace) | pnpm),管理方式变得更简单。

刚好之前入职了一家新公司,项目非常多,技术栈也比较混乱,每次新的项目需要重新创建和配置脚手架,还有一些axios的配置和基础文件,非常不方便,之前接触过monorepo的管理方式,所以干脆查阅了一些网上的帖子和之前的经验搞了一套pnpm+monorepo的项目管理方式的架子,还是挺香的,下面就详细介绍一下。

为什么要用pnpm

为什么要用pnpm,什么是pnpm

pnpmyarnnpm一样,也是替代包管理器,语法上基本没有差别,可以无缝从npm转移过来,它效率更高,速度更快,而且可以节省硬盘空间,所以,可以试着全面拥抱pnpm吧,具体可以看(官网 | pnpm)。

项目开始

首先安装pnpm、创建一个vite,这里我们直接选择vue3+ts的版本,具体创建过程掠过。

npm install -g pnpm

pnpm create vite

cd test
pnpm install
pnpm run dev

我们在根目录下创建一个packages文件,在这里不同的是,网上其他的教程都直接把所有项目创建在这个文件下,公共的代码放在其他位置,但其实,我们可以把所有代码和配置代码放在这里,让项目只是单纯的路由引用。

├── packages 
| ├── basic 
| | ├── api
| | └── core
| ├── components 
| | ├── base
| | └── business
| ├── modules
| | ├── demo1
| | └── demo2
| ├── projects
| | ├── vite-demo1
| | └── vite-demo2
├── package.json

如图所示,我们把基础配置文件放在basic里面,将业务组件或者公共放在components里面,modules里面则存放具体的项目,project则用来存放各个环境项目或者各个客户的定制项目,内部只是一些基础的启动打包配置,及一些路由引用,这样即使是modules中的同一份项目要给不同的客户,我们也可以通过路由引用来引入即可,不需要重新编写迁移代码。

注:此处只代表个人的开发习惯,可以自己选择建立

vite已经预装了ts,我们不再重复下载,先下载几个其他的准备包:

pnpm i --save-dev cross-env execa inquirer -w
// -w(--workspace-root) 是为了将依赖下载在根文件中。

在根目录下创建pnpm-workspace.yaml文件,pnpm-workspace.yaml 定义了 工作空间 的根目录,并能够使您从工作空间中包含 / 排除目录 。 默认情况下,包含所有子目录。输入以下代码,把我们自身的代码部分放入根目录中

packages:
    - 'packages/**'

打开package.json文件,配置scripts的启动项,之前默认的是dev,我们可以新增一个start

    "start": "cross-env type=start node start.config.js"

同理,升级项目,打包项目或者其他操作,也可以这样使用。

紧跟上步骤,我们来配置start.config.js

根目录下创建start.config.js文件,这里我们需要用到inquirer包,用法可以自行查阅,代码如下:

import inquirer from "inquirer";
import { execaCommand } from "execa";
import projects from "./config/project/index.js";
import { magentaBright, greenBright, bold } from "colorette";

inquirer
  .prompt([
    {
      type: "list",
      message: `选择要启动的项目:`,
      name: "mono",    // 存储答案的字段
      default: projects[0].enName,   // 默认启动项
      choices: projects.map((p) => {   // 选择
        return { name: p.cnName, value: p.enName };
      }),
    }
  ])
  .then(({mono: prd}) => {
    const project = projects.find((v) => v.enName == prd);
    console.log(`>>> 当前项目标识:当前产品标识:${bold(magentaBright(project.enName))}`);

    const cmd = `pnpm --F @projects/${project.enName} run dev `;
    // 这里的启动项就去找到我们projects里面的项目的文件启动

    let envVars = {
      selectedProject: project.enName,
      product: prd,
      // project: project.projectKey,
      // isLocal,
    };

    execaCommand(cmd, { stdio: "inherit", env: envVars });

  }).catch((err) => {
    console.log("error", err)
  });

这样,我们就可以在启动的时候选择要启动的项目,例如,输入pnpm start启动,出现:

image.png

现在你应该能理解我们要做什么了,是不是很方便?当然,我们还需要其他配置才能实现。

删除根目录下的vite.config.ts。新建一个config文件夹,在内部创建projectvite两个文件夹,在project内部我们主要存放项目的基本信息,例如端口、域名,打包名称之类的,vite内部我们则是统一配置,先做一个基础的共用的vite.config.ts,或者也可以写一个共有的方法,再创建其他项目自定义的部分或者标识名。这个全凭个人需要,配置部分掠过。

再在当前文件夹下创建runVite.js文件,配置他们的启动文件。

import { execaCommand } from "execa";

/**
 * 按照项目名称启动项目
 * @param projectName 项目名称
*/
export default function runVite(projectName) {
  let cmd = `vite --config ./vite.config.ts`

  execaCommand(cmd, { stdio: "inherit" })
}

接着,我们在projects文件下创建项目,使用pnpm create vite,安装好后,同样删除vite.config.ts文件。我们这里先创建vite-demo1,vite-demo2两个项目,分别新建start.js两个文件,引用了上个文件中的启动功能。

import runVite from "../../../config/vite/runVite.js";

runVite("vite-demo1");

接下来再打开根目录下config/project文件,创建如下几个文件

base.js:公司的版权及基础配置信息

/** 基础配置 */
export const BASE = {
  protocol: "https:",
  host: "xxx.cn",
  beian: "粤ICPxxxxx号-1",
  copyright: "© 2022 All Rights Reserved 广州市vue3科技有限公司",
  rancherUrl: "https://www.google.com",
};

index.js

import allList from "./project-default.js";

export default [...allList]

project-list.js:项目列表,被start.config.js引入,用来展现启动列表项。

export default [
   {
    enName: "vite-demo1",
    cnName: "vite-demo1",
    root: "vite-demo1",
    projectKey: "product",
  },
   {
    enName: "vite-demo2",
    cnName: "vite-demo2",
    root: "vite-demo2",
    projectKey: "test",
  },
]

project-default.js:默认环境项目。根据项目自行创建

import { BASE } from "./base.js";
import PRODUCTS, { createProductConfig } from "../product/index.js";

const RANCHER_KEY = "c-757ff:p-m9mbf";
const PROJECT_KEY = "default";

export default {
  ...BASE,
  enName: PROJECT_KEY,
  cnName: "开发环境",
  rancherKey: RANCHER_KEY,
  port: 3100,
  ...
};

这时候,你就可以使用pnpm start启动项目了。

至此,还有些工作要做。当我们引用其他文件内的项目时,需要安装项目依赖。

首先,你要对具体的项目文件初始化,例如components/business,需要在文件路径下执行pnpm init初始化,并将package.jsonname属性也做更改(@components/business)

然后回到vite-demo1路径中,安装@components/business的依赖:

pnpm i @components/business

我们就可以直接在vite-demo1内部引用这个路径下的文件了:

import Button from "@components/business/buttonTest/Button.vue"

在其他的文件中也是如此。

按照之前的思路,我们可以再做一些优化。

对于vite-demo1的src路径下的main.ts文件,每次我们都需要注册一些组件,不如我们统一写在外部,让所有项目都可以使用注册的一些第三方插件或者自己定义的全局组件,只在需要特殊独立依赖的项目里单独写自己的注册文件即可。这样又少了很多工作,非常方便。

现在,在vite-demo1中,我们只在src路径下编写router文件即可,所需要的组件都从其他模块引入,例如@modulescomponentsbasic之中。第二个第三个项目也可以如此重复使用代码。

这是一份大体的方案和思路,先讲这么多,读者有什么疑问可以留言讨论。