Monorepo pnpm模式管理多个web项目(Vue3)

5,705 阅读7分钟

1、全局安装pnpm

npm install pnpm -g

2、创建项目文件夹monorepo-manage及其packages文件夹

md monorepo-manage
cd monorepo-manage

3、初始化项目

//根目录下
pnpm init

1.png

4、创建pnpm-workspace.yaml文件

在根目录下,创建pnpm-workspace.yaml文件,内容:

type nul >pnpm-workspace.yaml
packages: 
  - 'packages/*' # 代表所有项目都放在packages文件夹之下

注释:代表所有项目都放在packages文件夹之下

5、创建.npmrc文件

根目录下创建.npmrc文件,内容:

type nul >.npmrc
shamefully-hoist = true

注释:三方依赖一也有依赖,要是项目中使用了第三方的依赖,要是哪天第三方卸载不在该包了,那就找不到了,称之为“幽灵依赖” ,所以需要“羞耻提升”,暴露到外层中,即在根目录下的node_modules内,而非在.pnpm文件夹中。 0.png

6、创建packages文件夹

md packages
cd packages

2.png

7、创建shared跟web等子项目

在packages下创建shared文件夹,后续再创建web1、web2...项目。

md shared

注释:shared项目用来服务其他多个web项目,提供公共方法、组件、样式等等。

8、全局安装Vue

pnpm install vue -w

注释:-w的意思是,workspace-root把依赖包安装到工作目录的根路径下,则根目录下会生成node_modules文件夹。可以共用,后续每个项目需要用到vue的,都直接从根目录node_modules里取。

4.png

可以看出根目录下的node_modules里,vue安装到了与.pnpm同层级位置当中了,这就是第3步骤shamefully-hoist = true的效果,把vue从.pnpm内提到node_modules中,并且vue的相关依赖,也拍平到了该层级文件夹中。

注释:若是此时看到vue的依赖没有拍平到node_modules下,还是在.pnpm当中,不用慌,执行pnpm uninstall vue -w,接着删掉了.npmrc文件重新创建.npmrc文件,然后删除node_modules文件,这会终端就会提示:“ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF  This modules directory was created using a different public-hoist-pattern value. Run "pnpm install" to recreate the modules directory.”,按照指示,执行“pnpm install”即可,

9、全局安装typescript

pnpm install typescript -w -D

10、初始化shared项目

pnpm init初始化shared项目的package.json

shared/package.json:

{
  "name": "@manage/shared",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

注释: 注意name的名字,后续需要配置关联项目之间,要对应。

11、shared下创建分享共用的内容

md components
md utils
md fetch
md style
......

5.png utils/index.ts 随意添加点导出函数,等会给web1项目用。

12、初始化web1项目,pnpm安装vue项目

vue官网安装指南

8.png

pnpm create vite web1 -- --template vue

注意:安装时候可能会出现如下图告警信息,导致安装失败 9.png 安装指令需要改成:

pnpm create vite web1 --- --template vue

前2杠变成3杠即可,详见官方issue

注释:打开paclage.json,注意name的名字,改成“@manage/web1”,后续需要关联项目之间,要对应。

10.png 可以看出,安装成功之后vue、@vitejs/plugin-vue、vite,这三者也会被安装在当前的web1之下node_modules

  "dependencies": {
    "vue": "^3.2.25"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^2.3.3",
    "vite": "^2.9.9"
  }

web1下执行pnpm install,验证一下

11.png 果真,web1/node_modules下有依赖包,那么问题来了,就是说,将来我们这个项目会有web2、web3...出来,不能每个子项目都在自身安装vue、@vitejs/plugin-vue、vite,这样不好管理,而且依赖包臃肿,应当把这三者安装到全局当中,以备共用。 卸载vue、@vitejs/plugin-vue、vite

pnpm uninstall vue
pnpm uninstall @vitejs/plugin-vue -D
pnpm uninstall vite -D

全局安装vue、@vitejs/plugin-vue、vite vue在第8步骤已经安装过,此时不用安装了

pnpm install @vitejs/plugin-vue vite -D -w

12.png 可以看出web1/node_modules下已经没有vue跟vite的文件夹。 接着跑项目试试

pnpm run dev

没问题,说明web1所依赖的资源,自身没有的话,就往上层去取。

13、建立关联

13.1 指定版本号

pnpm install @manage/shared@workspace --filter @manage/web1

会添加版本号,代表只能使用@manage/shared的version:1.0.0版本 13.png 13.2 不指定版本号,取最新版本

pnpm install @manage/shared@* --filter @manage/web1

14.png

14、web1开始引用的shared的数据

此时我们发现,我们是可以用相对路径引用shared,好比:

//尝试在App.vue中引用
import { isObject } from '../../shared/utils'; 

但我们不能这么做,low,而且路径引用层级不好管理。 所以需要加入tsconfig.json来配置路径,定义按照规则去查找shared 安装typescript到全局

pnpm install typescript -D -w
根路径下执行:
pnpm tsc --init

tsconfig.json配置如下:

{
  "compilerOptions": {
   "outDir":"dist", // 输出的目录
   "sourceMap": true, //采用sourcemap 
   "target": "es2016", // 目标语法
   "module": "esnext", // 模块格式
   "moduleResolution": "node", // 模块解析
   "strict": false, // 严格模式
   "resolveJsonModule": true, // 解析json模块
   "esModuleInterop": true,// 允许通过es6语法引入commonjs模块
   "jsx":"preserve",// jsx不转义
   "lib":["esnext","dom"],// 支持的类库esnext及dom
   "baseUrl": ".",// 当前是以该路径进行查找
   "paths":{
    //  "@manage/*":[
    //   "packages/*/src",
    //  ], // 即以@manage开头的都去该路径下查找,是个数组
    "@manage/shared/components":["packages/shared/components"],
    "@manage/shared/utils":["packages/shared/utils"],
    "@manage/shared/fetch":["packages/shared/fetch"],
    "@manage/shared/styles":["packages/shared/styles"],
    // 或者用*号处理匹配
    "@manage/shared/*":["packages/shared/*"]
   }
  }
}

15. web项目正确引用shared的utils

import { isObject } from '@manage/shared/utils';

15.png

16. web项目正确引用shared的styles

// 全局安装sass,执行指令
pnpm install sass -D -w

/* 本子项目variables.scss */
@import 'variables.scss';
/* 本子项目mixin.scss */
@import 'mixin.scss';
/* 引用项目最顶层基础初始化样式 , web1/src/styles/index.scss */
@import "@manage/shared/styles/index.scss";
// 此处不能省略结尾的.scss后缀

1.png

17. 封装shared共用组件

//packages/components/src下,创建
type nul >Button.vue
//packages/components/src/index.ts
type nul >index.ts

2.png 3.png

18. 配置指令,可以在项目内的任何路径下跑起web项目

因为开发项目比较多,有时候总是cd与cd ../切换或者手动重新切换终端路径,比较麻烦,费时间。 在全局的package.json中配置scripts

{
  "scripts": {
    "dev:web1": "cd packages/web1 & pnpm dev"
  },
  // 或者
   "scripts": {
    "dev:web1": "pnpm -C packages/web1 & pnpm dev"
  },
}

跑项目时候,执行

pnpm run -w dev:web1
// 就能跑起web1项目,同理web2、web3也一样配置。

后续:(shared项目的components、fetch、utils中的index.ts文件,都移到了src文件内,也修改了ts.config.json的paths配置)

"paths":{
//  "@manage/*":[
//   "packages/*/src",
//  ], // 即以@manage开头的都去该路径下查找,是个数组
"@manage/shared/*":["packages/shared/*"]
}

19. 封装自己的插件库xyplayer

packages下执行

md plugins
cd plugins
md xyplayer
cd xyplayer
md src
type nul >index.ts

20. xyplayer项目关联shared项目

pnpm-workspace.yaml添加

 - 'packages/plugins/*'

xyplayer下执行:

pnpm install @manage/shared@* --filter @manage/xyplayer

1.png

21. 处理插件库的打包

根目录下创建scripts文件夹

md scripts
cd scripts
type nul >dev-plugins.js

安装minimist esbuild

pnpm install typescript minimist esbuild -w -D

build-plugins.js:

// minimist 可以解析命令行参数,非常好用,功能简单
import minimist from 'minimist'
// 打包模块
import { build } from 'esbuild'
// node 中的内置模块
import path from 'path'
import fs from 'fs'
const __dirname = path.resolve();
const args = minimist(process.argv.slice(2));
const target = args._[0];
const format = args.f || "global";
const entry = path.resolve(__dirname, `../packages/plugins/${target}/src/index.ts`);
/*  iife 立即执行函数(function(){})()
    cjs node中的模块 module.exports
    esm 浏览器中的esModule模块 import */
const outputFormat = format.startsWith("global") ?
  "iife" :
  format === "cjs" ?
  "cjs" :
  "esm";
const outfile = path.resolve(__dirname, `../packages/plugins/${target}/dist/${target}.${format}.js`);
const pkaPath = `../packages/plugins/${target}/package.json`;
const pkaOps = JSON.parse(fs.readFileSync(pkaPath, 'utf8'));
const packageName = pkaOps.buildOptions?.name;
build({
  entryPoints: [entry],
  outfile,
  bundle: true,
  sourcemap: true,
  format: outputFormat,
  globalName: packageName,
  platform: format === "cjs" ? "node" : "browser",
  watch: {
    onRebuild(error) {
      if (!error) {
        console.log(`rebuild~~~`);
      }
    },
  },
}).then(() => {
  console.log("watching~~~");
});

dev-plugins.png

22. 开始撸插件库xyplayer代码

plugins/xyplayer/src/index.ts:

import { isObject } from "@manage/shared/utils";
// 测试
export const testFunc = ()=>{
  return isObject({})
}

23. 运行打包插件库

全局配置打包指令,package.json中scripts

  "scripts": {
    "dev:web1": "cd packages/web1 & pnpm dev",
    "dev:xyplayer": "node scripts/dev-plugins.js xyplayer -f global"
  },

执行:

pnpm -w run dev:xyplayer

24. 试验插件库的调用

引入打包好的dist下的文件,使用试试,没问题 5.png

25. vite-cli方式初始化vue3项目

目标:pnpm + vue3.0 + vite + pinia + vueuse + ts

25.1 vite-cli脚手架指令

//在packages文件夹目录下 终端执行:
pnpm create vite

25.2 输入项目名

? Project name: » web2

25.3 选中vue3

√ Project name: ... web2
? Select a framework: » - Use arrow-keys. Return to submit.
    vanilla
>   vue
    react
    preact
    lit
    svelte

25.4 选中vue-ts

√ Project name: ... web2
√ Select a framework: » vue
? Select a variant: » - Use arrow-keys. Return to submit.
   vue
>    vue-ts

25.5 完成配置,进入web2,执行

Done. Now run:

  cd web2
  pnpm install
  pnpm run dev

1.png

25.6 移除web2中的vue、@vitejs/plugin-vue、vite、typescript,因为顶层已经安装共用的了。

pnpm uninstall vue
pnpm uninstall @vitejs/plugin-vue -D
pnpm uninstall vite -D
pnpm uninstall typescript -D
// 全局安装vue-tsc
pnpm install vue-tsc -w -D

绑定关联

25.7 绑定web2与shared的关联

pnpm install @manage/shared@* --filter @manage/web2

25.8 重新运行web2

pnpm run dev
// 跑服务出现了问题跑不下去,然后我重新安装了顶层跟web2的依赖包,就可以了

25.9 为保证 node 的使用

pnpm i @types/node --save-dev

25.10 安装element-plus、unplugin-auto-import、unplugin-vue-components,并且修改main.ts,vite.config.ts

  1. 安转element-plus: element-plus.gitee.io/zh-CN/guide…
  2. 按需导入介绍: element-plus.gitee.io/zh-CN/guide…

mian.ts:

import { createApp } from 'vue'
import App from './App.vue'
import router from '@/router'
// 引入全局样式
import '@/styles/index.scss'
// 引入Element
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
app.use(router)
// 注册element所有icon
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
}
app.use(ElementPlus)

app.mount('#app')

vite.config.ts:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// import myPlugin from './zip'
// https://vitejs.dev/config/
export default defineConfig({
  base: "./",
  plugins: [
    vue(),
    // myPlugin('dist', require('path').resolve(__dirname, './dist')),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),],
  resolve: {
    // 启用别名
    alias: {
      "@": "/src/",
      assets: "/src/assets/",
      api: "/src/api/",
      views: "/src/views/",
      components: "/src/components/",
    },
  },
  css: {
    preprocessorOptions: {
      // 配置scss,自动引入指定的scss文件
      scss: {
        additionalData: `
              @import "@/styles/mixin.scss";
              @import "@/styles/variables.scss";
              `,
      },
    },
  },
  server: {
    host: "0.0.0.0", // 将监听所有地址,包括局域网和公网地址。Network显示
  },
})

25.11 安装eslint格式化统一代码(VsCode编辑器安装Volar、ESlint插件)

pnpm install eslint eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin -D -w

25.12 创建.eslintignore,将不需要进行eslint的文件过滤

node_modules/
dist/

25.12 创建.eslintrc.js

module.exports = {
    parser: 'vue-eslint-parser',
    parserOptions: {
        parser: '@typescript-eslint/parser',
        ecmaVersion: 2020,
        sourceType: 'module',
        ecmaFeatures: {
            jsx: true
        }
    },
    extends: [
        'plugin:vue/vue3-recommended',
        'plugin:@typescript-eslint/recommended'
    ],
    rules: {
        '@typescript-eslint/ban-ts-ignore': 'off',
        '@typescript-eslint/explicit-function-return-type': 'off',
        '@typescript-eslint/no-explicit-any': 'off',
        '@typescript-eslint/no-var-requires': 'off',
        '@typescript-eslint/no-empty-function': 'off',
        'vue/custom-event-name-casing': 'off',
        'no-use-before-define': 'off',
        // 'no-use-before-define': [
        //   'error',
        //   {
        //     functions: false,
        //     classes: true,
        //   },
        // ],
        '@typescript-eslint/no-use-before-define': 'off',
        // '@typescript-eslint/no-use-before-define': [
        //   'error',
        //   {
        //     functions: false,
        //     classes: true,
        //   },
        // ],
        '@typescript-eslint/ban-ts-comment': 'off',
        '@typescript-eslint/ban-types': 'off',
        '@typescript-eslint/no-non-null-assertion': 'off',
        '@typescript-eslint/explicit-module-boundary-types': 'off',
        '@typescript-eslint/no-unused-vars': [
            'error',
            {
                argsIgnorePattern: '^h$',
                varsIgnorePattern: '^h$'
            }
        ],
        'no-unused-vars': [
            'error',
            {
                argsIgnorePattern: '^h$',
                varsIgnorePattern: '^h$'
            }
        ],
        'space-before-function-paren': 'off',
        quotes: ['error', 'single'],
        'comma-dangle': ['error', 'never']
    }
};

25.13 配置eslint的pnpm scripts指令

{
    "scripts":{
        "eslint:command": "终端上可以找出不符合规则的代码以及位置",
        "eslint": "eslint --ext .js,.ts,vue packages",
        "lint:fix:command": "找出不符合规则的代码并尝试安装项目配置的.eslintrc.js规则进行修正",
        "lint:fix": "eslint --fix --ext .js,.ts,vue packages"
    }
}

25.14 安装prettier

pnpm install prettier eslint-config-prettier eslint-plugin-prettier -D -w
(此处有个问题,安转之后,.vue文件也能按照.eslintrc.js的配置进行格式化,而原本我是打算根目录创建.prettierrc.js,结果貌似没用,我就去掉了.prettierrc.js)

25.15 .vscode配置

根目录下创建.vscode文件夹,里面再新建一个settings.json,用来设置vscode的该项目的全局配置。(需要重启VsCode)

// settings.json:
{
  "editor.fontSize": 20, // 编辑器字体大小
  "terminal.integrated.fontSize": 18,	// terminal 框的字体大小
  "editor.tabSize": 2, // Tab 的大小 2个空格
  "editor.formatOnSave": true, // 保存是格式化
  "prettier.singleQuote": true, // 单引号,没安转prettier,无效
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.format.enable": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "html",
    "vue",
    "typescript",
    "typescriptreact"
  ],
}

26. 安装或删除全局依赖包跟所有子项目依赖包,配置pnpm scripts指令

应开发有需要一键安装(或删除)全部子项目以及全局的node_modules,特此配置pnpm scripts指令

npm install rimraf -g
// 指令需要用rimraf来快速删除node_modules包,所以需要安装
{
    "scripts":{
        "installAllNm:command": "安装全局依赖以及的所有子项目的依赖",
        "installAllNm": "pnpm install",
        "clearAllNm:command": "删除全局依赖以及的所有子项目的依赖",
        "clearAllNm": "rimraf node_modules && rimraf */**/node_modules"
    }
}