likes
comments
collection
share

基于pnpm搭建monorepo前端工程

作者站长头像
站长
· 阅读数 7

前面几个章节我们分析了monorepo的优势与劣势,学习了webpack、pnpm、babel等前端工程化工具的基本配置。这一节我们来学习一下如何搭建一个monorepo架构的前端工程。

一、了解package.json

monorepo的简单理解就是单仓库多包管理,这就要求我们必须掌握包管理package.json的一些基础配置知识,在现代前端开发的过程中,我们对package.json文件也非常的熟悉,package.json文件描述了一个包的基本信息,依赖信息、发布信息等,所以我们在初始化monorepo之前一定要掌握清楚关于package.json的基础字段配置含义。

相信每一个前端开发都执行过npm install这个命令,那么我们执行这个命令的背后发生了什么呢,其实就是脚本获取当前执行上下文中的package.json中关于包的配置信息来进行down和reslove,我们姑且可以认为每一个有package.json的目录都是一个包,即使这个包不会发布到npm远程仓库。 package.json config官方文档 1.name

If you plan to publish your package, the most important things in your package.json are the name and version fields as they will be required. The name and version together form an identifier that is assumed to be completely unique. Changes to the package should come along with changes to the version. If you don't plan to publish your package, the name and version fields are optional.

我理解的是name属性是区分npm包的唯一属性,也是最重要的属性,这就和我们人的名字一样,只不过npm中name必须唯一不能重复,比如有的人昵称为 “好牛的滑子”、“奶白色的雪子”、“火鸡味的锅巴”等等,扯偏了拉回来哈~,说了这么多,就是因为name是npm包中最最最重要的了所以我们的package.json中什么属性都可以没有,但必须要有name属性。

{
    "name": "farmerui"
}

这样我们就标识了当前目录包名为“farmerui”,如果我发布到npm远程仓库,其他开发者就可以通过npm install farmerui来安装啦

对于包名称我们还要了解一个概念叫坐标,具有相同坐标的包会被安装到同一子目录下。例如 @vue/reactivity@vue/runtime-core 会被安装到 node_modules 目录的 @vue 目录下,vue 不属于任何坐标,就会被安装到 node_modules 根目录。

📦node_modules
 ┣ 📂@vue
 ┃ ┣ 📂reactivity
 ┃ ┗ 📂runtime-core
 ┣ 📂vue

通常情况下,属于同一个体系、项目下的包会被安排在一个坐标下,比如我们创建的示例项目就都会发布到 @farmerui 这个坐标下,那么包名就需要设定为 @farmerui/xxx

基于pnpm搭建monorepo前端工程

2.version

If you plan to publish your package, the most important things in your package.json are the name and version fields as they will be required. The name and version together form an identifier that is assumed to be completely unique. Changes to the package should come along with changes to the version. If you don't plan to publish your package, the name and version fields are optional.

翻译一下哈,就是name是一个包最重要的,但是只有name和version一起组合起来才能证明你用的是唯一的,因为一个包中间有可能有几个大版本的迭代,前后可能api完全不一样,譬如vue2和vue3,你都属于vue,但是版本不同可是天差地别的。

{
    "name": "farmerui",
    "version: "0.0.1"
}

上面两个属性说明当前包名为“farmerui”版本为0.0.1,说到这想起来我们的react-native目前最新版本是0.72一个大版本都没更新过呢,但不影响它的伟大。“0.0.1”这三个数字都代表着什么呢,请看下图。

基于pnpm搭建monorepo前端工程

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。 先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。 版本约束限制了包管理器为项目安装依赖时可选的版本范围:
  • ^ 的含义是安装最新的 minor 版本。例如 ^1.2.0 的约束下,会为项目安装最新的 minor 版本 1.X.Y,但不会安装下一个 major 版本 2.0.0
  • ~ 的含义是安装最新的 patch 版本。例如 ~1.2.0 的约束下,会为项目安装最新的 patch 版本 1.2.X,但不会安装下一个 minor 版本 1.3.0
  • 如果版本号前面没有任何标识符,表示固定版本号,无论如何都只安装这个固定版本。

版本号规则细则

3.包的基本信息 包的基本信息缺失虽然不影响包的整体功能,但是对于包的使用者非常重要,对于包的易用性有着举足轻重的地位。当我们和后端连调接口时,如果后端只把实体和接口地址甩给你,让你自己猜,你也会非常的不爽吧,心里肯定会骂“这个XX写的什么XX玩意儿”,看都看不懂,怎么调。所以我们在开发一个npm包时一定要重视包的基本信息。

{
  //包名
  "name": "farmerui",
  //版本
  "version: "0.0.1",
  // 简介,可以作为关键字搜索的依据
  "description": "",
  // 关键字、标签,正确设置可以提高在 npm 的搜索权重与曝光度
  "keywords": ["js"],
  // 作者,主要 Owner
  "author": "coderlwh",
  // 许可证
  "license": "MIT",
  // 项目主页
  "homepage": "https://github.com/coderliweihong/farmerui/blob/main/README.md",
  // 源码仓库
  "repository": {
    "type": "git",
    "url": "git+https://github.com/coderliweihong/farmerui"
  },
  // bug反馈方式,支持 `bugs.email` 等邮箱字段
  "bugs": { 
    "url" : "https://github.com/coderliweihong/farmerui/issues"
  }
}

3.依赖管理 我们再讲webpack的时候遗留了一个知识点,那就是关于包管理依赖中dependencies、devDependencies和peerDependencies的真正含义。我们原来在开发中可能都听说过,devDependencies是开发依赖,dependencies是生产依赖,这样理解稍显狭隘且不全面,这里我们详细介绍一下它们之间的区别。

依赖类型定义在项目中定义在依赖中总结示例
dependencies会被安装会被安装定义包运行所需要的依赖包某前端项目使用 react 进行开发,需要将 react 添加到 dependencies 中
devDependencies会被安装不会被安装定义包在开发时所需要的依赖包antd 使用了 @testing-library/react 进行测试,需要将 @testing-library/react 添加到 devDependencies 中
peerDependencies不会被安装不会被安装,但是如果指向的依赖没有被安装或不符合时会有警告(peerDependenciesMeta 会影响该行为)定义该包运行所需要的依赖环境,一般和 devDependencies 一起使用antd 是一个 react 组件库,为了不和使用它的项目中的 react 版本定义造成冲突,需要将支持的 react 版本添加到 peerDependencies 中

开发 Web 应用 时,即使将所有依赖声明在 devDependencies 中,也不会影响应用的成功构建、打包与运行。

因此 dependencies = 生产依赖,devDependencies = 开发依赖 的说法是片面的。 我们常说的 “生产环境”、“开发环境” 是构建时行为,构建并不是包管理器的职责,而是 webpackrollupvite 的工具的工作,此时包管理器起的作用仅仅是执行脚本而已。 各种包管理器处理 dependenciesdevDependencies 差异的行为都发生在依赖安装时期,即 npm install 的过程中。

图解devDependencies和dependencies: devDependencies 假设我们有项目 a,其 package.json 结构如下

{
  "name": "a",
  "dependencies": {
    "b": "^1.0.0"
  },
  "devDependencies": {
    "c": "^1.0.0"
  }
}

a 的依赖 b 和 c 的依赖信息如下:

// b/node_modules/b/package.json
{
  "name": "b",
  "dependencies": {
    "d": "^1.0.0"
  },
  "devDependencies": {
    "e": "^1.0.0"
  }
}
// c/node_modules/c/package.json
{
  "name": "c",
  "dependencies": {
    "f": "^1.0.0"
  },
  "devDependencies": {
    "g": "^1.0.0"
  }
}

我们用实线表示 dependencies 依赖,用虚线表示 devDependencies 依赖,项目 a 的依赖树如下图:

基于pnpm搭建monorepo前端工程 执行 npm install 后,a 的 node_modules 目录最终内容如下:

node_modules
├── b       // a 的 dependencies
├── c       // a 的 devDependencies   
├── d       // b 的 dependencies    
└── f       // c 的 dependencies

npmyarn所安装的包都被平铺到 node_modules 目录.

可见,包管理器将以项目的 package.json 为起点,安装所有 dependenciesdevDependencies 中声明的依赖。 但是对于这些一级依赖项具有的更深层级依赖,在深度遍历的过程中,只会安装 dependencies 中的依赖,忽略 devDependencies 中的依赖。 因此,bcdevDependencies —— eg 被忽略, 而它们的 dependencies —— df 被安装。

为什么会这样呢?因为包管理器认为:作为包的使用者,我们当然不用再去关心它们开发构建时的依赖,所以会为我们忽略 devDependencies。 而 dependencies 是包产物正常工作所依赖的内容,当然有必要安装。

回到 Web 应用 开发的场景,Web 应用 的产物往往部署到服务器,不会发布到 npm 仓库供其他用户使用, 而包管理器对于一级依赖,无论 dependencies 还是 devDependencies 都会悉数安装。 这种情况下, dependenciesdevDependencies 可能真的只有语义化约定的作用了。

4.包入口信息

main

The main field is a module ID that is the primary entry point to your program. That is, if your package is named foo, and a user installs it, and then does require("foo"), then your main module's exports object will be returned.

This should be a module relative to the root of your package folder.

For most modules, it makes the most sense to have a main script and often not much else.

If main is not set, it defaults to index.js in the package's root folder.

简单理解就是main作为最为古老且原始的入口字段,由node和npm定义,如果main字段不存在,将会默认index.js为入口文件。 使用方法:

{
  "main": "./index.js"
}

module

module 字段提供符合 ESM 规范的模块入口。

但 Node 却并未采纳,而是使用了 { "type": "module" } 代替。

不过,打包工具普遍支持了该字段。只是实现的与提案有很大差距,实际情况是,modulemain 一样对待,只是优先级更高。 使用方法:

{
  "module": "./index.esm.js"
}

browser

If your module is meant to be used client-side the browser field should be used instead of the main field. This is helpful to hint users that it might rely on primitives that aren't available in Node.js modules. (e.g. window)

browser 字段提供对浏览器环境更友好的模块入口。

{
  "browser": "./index.browser.js"
}

browser(字符串) 将代替 mainmodule

另一种对象的写法,键名(Key)匹配被访问的路径,键值(Value)则是实际路径:

{
  "main": "./index.js",
  "module": "./index.mjs",
  "browser": {
    "./index.js": "./index.browser.js",
    "./index.mjs": "./index.browser.esm.js"
  }
}

browser(对象) 不仅可以作为入口文件的别名,也可以用于包内部依赖的别名,比如:

{
  "main": "./index.js",
  "browser": {
    "axios": "./axios.js",
    "./dom.js": "./dom.browser.js",
    "log": false
  }
}

当 ./index.js 文件使用到这三个依赖时:

  • axios 模块解析到本地文件 ./axios.js
  • ./dom.js 本地文件解析到另一个本地文件 ./dom.browser.js
  • 禁用 log 模块。

exports

使用方法: (1)exports['.'].require 字段用于设置 require() 方式的加载入口(cjs 规范)

// 入口定义
{
  "name": "my-module",
  "main": "index.js",
  "exports": {
    ".": {
      "require": "index.js"
    },
    // ...
  }
}
// 代码中使用
const app = require('my-module') // 实际路径 node_modules/my-module/index.js

(2)exports.*.import 字段用于设置 import 的加载入口(esm 规范 import { useState } from 'react')

// 入口定义
{
  "name": "my-module",
  "main": "index.js",
  "module": "index.mjs",
  "exports": {
    ".": {
      "require": "index.js",
      "import": "index.mjs"
    },
    // ...
  }
}
// 使用
import app from 'my-module' // 实际路径 node_modules/my-module/index.mjs

(3)exports.*.types 字段用于设置 d.ts 类型声明的加载入口(TypeScript 专属)

// 入口定义
{
  "name": "my-module",
  "main": "index.js",
  "module": "index.mjs",
  "types": "index.d.ts",
  "exports": {
    ".": {
      "require": "index.js",
      "import": "index.mjs",
      "types": "index.d.ts"
    },
    // ...
  }
}

(4)exports 比起 mainmoduletypes,它可以暴露更多的出口,而后者只能定义主出口。

// 入口定义
{
  "name": "my-module",
  "main": "index.js",
  "exports": {
    ".": {
      "require": "index.js",
    },
    "./locale/*": {
      "require": "./locale/*",
    },
    "./plugins/*": {
      "require": "./dist/plugins/*",
    }
    // ...
  }
}
// 使用
const app = require('my-module') // 实际路径 node_modules/my-module/index.js
const zhCn = require('my-module/locale/zh-Cn') // 实际路径 node_modules/my-module/locale/zh-Cn.js
const testPlugin = require('my-module/plugins/test') // 实际路径 node_modules/my-module/dist/plugins/test.js
// import 同理

最后,当 exports 和另外三个入口字段出现重复定义时,会有更高的优先级。 更多关于 exports 的规则和细节详见

5.发布信息

The optional files field is an array of file patterns that describes the entries to be included when your package is installed as a dependency. File patterns follow a similar syntax to .gitignore, but reversed: including a file, directory, or glob pattern (***/*, and such) will make it so that file is included in the tarball when it's packed. Omitting the field will make it default to ["*"], which means it will include all files.

files 指定了发布为 npm 包时,哪些文件或目录需要被提交到 npm 服务器中。

{
  "files": [
    "LICENSE",
    "README.md",
    "dist"
  ]
}

You can also provide a .npmignore file in the root of your package or in subdirectories, which will keep files from being included. At the root of your package it will not override the "files" field, but in subdirectories it will. The .npmignore file works just like a .gitignore. If there is a .gitignore file, and .npmignore is missing, .gitignore's contents will be used instead.

我们可以自己设置.npmignore文件,如果没设置.gitignore会代替.npmignore

  • .git
  • CVS
  • .svn
  • .hg
  • .lock-wscript
  • .wafpickle-N
  • .*.swp
  • .DS_Store
  • ._*
  • npm-debug.log
  • .npmrc
  • node_modules
  • config.gypi
  • *.orig

6.private

If you set "private": true in your package.json, then npm will refuse to publish it.

This is a way to prevent accidental publication of private repositories. If you would like to ensure that a given package is only ever published to a specific registry (for example, an internal registry), then use the publishConfig dictionary described below to override the registry config param at publish-time. private 用于指定项目是否为私有包。

当我们的项目不想被意外发布到公共 npm 仓库时,就设置 private: true

publishConfig

This is a set of config values that will be used at publish-time. It's especially handy if you want to set the tag, registry or access, so that you can ensure that a given package is not tagged with "latest", published to the global public registry or that a scoped module is private by default.

当我们的项目需要发布到私有的 npm 仓库时(比如公司内网的仓库),需要设置 publishConfig 对象。

{
  "publishConfig": {
    "registry": "https://yournpm.com",
  },
}

7.脚本信息

The "scripts" property is a dictionary containing script commands that are run at various times in the lifecycle of your package. The key is the lifecycle event, and the value is the command to run at that point.

脚本信息我们都非常熟悉,我们常用的npm run dev 等都是通过scripts中配置的命令别名来实现的

{
  "script": {
    "show": "echo 'Hello World!'",
    "dev": "webpck"
  },
}

二、工作空间

一个 workspace 的根目录下必须有 pnpm-workspace.yaml 文件, 也可能会有 .npmrc 文件。

例如以下的 pnpm-workspace.yaml 文件定义:a 目录、b 目录、c 目录下的所有子目录,都会各自被视为独立的模块。

packages:
  - a
  - b
  - c/*

⚠️pnpm 并不是通过目录名称,而是通过目录下 package.json 文件的 name 字段来识别仓库内的包与模块的。

在 workspace 模式下,代码仓根目录通常不会作为一个子模块或者 npm 包,而是**主要作为一个管理中枢,执行一些全局操作,安装一些共有的依赖。下面介绍一些常用的管理操作。

pnpm install

根据当前目录 package.json 中的依赖声明安装全部依赖,在 workspace 模式下会一起处理所有子模块的依赖安装

  • 安装项目公共开发依赖,声明在根目录的 package.json - devDependencies 中。-w 选项代表在 monorepo 模式下的根目录进行操作。

  • 每个子包都能访问根目录的依赖,适合把 TypeScriptwebpackeslint 等公共开发依赖装在这里

pnpm install -wD xxx

workspace 模式下,pnpm 主要通过 --filter 选项过滤子模块,实现对各个工作空间进行管理。

1. 为指定模块安装外部依赖。

  • 下面的例子指为 a 包安装 lodash 外部依赖。
  • 同样的道理,-S-D 选项分别可以将依赖安装为正式依赖(dependencies)或者开发依赖(devDependencies)。
# 为 a 包安装 lodash
pnpm --filter a i -S lodash
pnpm --filter a i -D lodash

2. 指定内部模块之间的互相依赖。

  • 指定模块之间的互相依赖。下面的例子演示了为 a 包安装内部依赖 b
# 指定 a 模块依赖于 b 模块
pnpm --filter a i -S b
{
  "name": "a",
  // ...
  "dependencies": {
    "b": "workspace:^"
  }
}

在实际发布 npm 包时,workspace:^ 会被替换成内部模块 b 的对应版本号(对应 package.json 中的 version 字段)。替换规律如下所示:

{
  "dependencies": {
    "a": "workspace:*", // 固定版本依赖,被转换成 x.x.x
    "b": "workspace:~", // minor 版本依赖,将被转换成 ~x.x.x
    "c": "workspace:^"  // major 版本依赖,将被转换成 ^x.x.x
  }
}
ADBC依赖依赖依赖依赖依赖
  • --filter 的还有更多超乎我们想象的能力,它支持依赖关系筛选,甚至支持根据 git 提交记录进行筛选。
# 为 a 以及 a 的所有依赖项执行测试脚本
pnpm --filter a... run test
# 为 b 以及依赖 b 的所有包执行测试脚本
pnpm --filter ...b run test

# 找出自 origin/master 提交以来所有变更涉及的包
# 为这些包以及依赖它们的所有包执行构建脚本
# README.md 的变更不会触发此机制
pnpm --filter="...{packages/**}[origin/master]"
  --changed-files-ignore-pattern="**/README.md" run build

# 找出自上次 commit 以来所有变更涉及的包
pnpm --filter "...[HEAD~1]" run build

三、monorepo工程初始化(实战来啦~)

1.划分子模块

我们将按照 element-plus 的思路将组件库拆分为多个模块,但更近一步的是,我们要尝试对 UI 组件的 components 包进一步拆分到单个组件的粒度,将每一个 UI 组件都作为一个独立的模块发包。

这来源于一个实际的需求:很多项目不希望全量引入组件库。这个需求主要有以下两方面的考虑:

  • 项目仅仅使用组件库的个别组件,不希望全量引入,增大产物体积(其实按需引入、摇树机制可以规避)。
  • 组件库的维护者往往会做整体更新,但是项目维护者却只希望最小限度变更。例如项目方面需要 Button 组件修复一个问题,仅仅希望升级这个 Button 组件,而不要升级其他无关组件,以免带来更多的风险。

我们为这个示例组件库起名为 farmer-ui(因为我是段友哈哈哈)。

//工程概要结构:
farmer-ui
├── docs          # 组件库文档 demo 模块
├── packages      # 组件库的各个实现模块放在 packages 目录下
|   ├── button    # 按钮组件
|   ├── input     # 输入框组件
|   ├── form      # 表单组件
|   ├── theme     # 组件库的样式与主题
|   ├── ...       # 更多 UI 组件
|   ├── ui        # 归纳各个 UI 组件的入口,即组件库的主包
|   ├── shared    # 其他工具方法
├── package.json

我们前面储存了那么多知识,有5w字的前置,又有本章上面一万多字的介绍。是骡子是马得拉出来溜溜了,你的枪里有没有子弹得先打一枪看看了,接下来我们开干~

⚠️我默认小伙伴们的Node.js 与 npm 都可以正常工作,首先通过 npm i -g pnpm 安装好 pnpm,后续包管理相关的命令一律使用 pnpm 执行

1.创建工程 我们去github上创建一个崭新的farmer-ui工程

基于pnpm搭建monorepo前端工程

基于pnpm搭建monorepo前端工程 2.将工程文件克隆到本地

基于pnpm搭建monorepo前端工程

cd Desktop && git clone git@github.com:coderliweihong/farmer-ui.git

3.用自己喜欢的编辑器打开farmer-ui工程 基于pnpm搭建monorepo前端工程 4.初始化package.json

pnpm init

在项目根目录中生成了 packages.json 文件,但是根目录并不是任何一个模块,它将作为整个组件库 monorepo 项目的管理中枢。我们把对这个 package.json 中 name 以外的字段都删去,后续我们要根据自己的需要自定义。

{
  "name": "farmer-ui",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
-   "test": "echo "Error: no test specified" && exit 1"
- },
- "keywords": [],
- "author": "",
- "license": "ISC"
}

5.在项目根目录下创建 pnpm-workspace.yaml 文件

这个文件的存在本身,会让 pnpm 要使用 monorepo 的模式管理这个项目,他的内容告诉 pnpm 哪些目录将被划分为独立的模块,这些所谓的独立模块被包管理器叫做 workspace(工作空间)。

touch pnpm-workspace.yaml

在pnpm-workspace.yaml添加包管理内容

packages:
  # 根目录下的 docs 是一个独立的文档应用,应该被划分为一个模块
  - docs
  # packages 目录下的每一个目录都作为一个独立的模块
  - packages/*

6.创建子包 暂时只建立docs、 UI 组件 button(按钮)input(输入框) 以及公共方法模块 shared。将根目录下的 package.json 文件复制到每个工作空间中,完成操作后的目录树:

📦farmer-ui
 ┣ 📂docs
 ┃ ┗ 📜package.json
 ┣ 📂packages
 ┃ ┣ 📂button
 ┃ ┃ ┗ 📜package.json
 ┃ ┣ 📂input
 ┃ ┃ ┗ 📜package.json
 ┃ ┗ 📂shared
 ┃   ┗ 📜package.json
 ┣ 📜package.json
 ┣ 📜pnpm-workspace.yaml
 ┗ 📜README.md
 ┗ 📜.gitignore
 ┗ 📜LICENSE

7.修改package.json (1)根目录下的package.json(⚠️注释只做讲解,json里不允许有注释的哈,自己到时候删除)

//farmer-ui/package.json
{
  "name": "farmer-ui",
  "private": true,
  "scripts": {
    // 定义脚本
    "hello": "echo 'hello world'"
  },
  "devDependencies": {
    // 定义各个模块的公共开发依赖
  }
}
  • private: true:根目录在 monorepo 模式下只是一个管理中枢,它不会被发布为 npm 包。

  • devDependencies:所有模块都会有一些公共的开发依赖,例如构建工具、TypeScript、Vue、代码规范等,将公共开发依赖安装在根目录可以大幅减少子模块的依赖声明

(2)子包下的package.json(以button组件为例,其余子包参照此例自行添加)

// farmer-ui/packages/button/package.json
{
  // 标识信息
  "name": "@farmerui/button",
  "version": "0.0.0",

  // 基本信息
  "description": "",
  "keywords": ["react", "ui", "component library"],
  "author": "coderlwh",
  "license": "MIT",
  "homepage": "https://github.com/coderliweihong/farmer-ui/blob/main/README.md",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/coderliweihong/farmer-ui.git"
  },
  "bugs": { 
    "url" : "https://github.com/coderliweihong/farmer-ui/issues"
  },


  // 定义脚本,由于还没有集成实际的构建流程,这里先以打印命令代替
  "scripts": {
    "build": "echo build",
    "test": "echo test"
  },

  // 入口信息,由于没有实际产物,先设置为空字符串
  "main": "",
  "module": "",
  "types": "",
  "exports": {
    ".": {
      "require": "",
      "module": "",
      "types": ""
    }
  },

  // 发布信息
  "files": [
    "dist",
    "README.md"
  ],
  // "publishConfig": {},

  // 依赖信息
  "peerDependencies": {
    "react": ">=18.2.0",
    "react-dom": ">=18.2.0"
  },
  "dependencies": {},
  "devDependencies": {}
}

(3)项目文档的 package.json

// farmer-ui/docs/package.json
{
  "name": "@farmerui/docs",
  "private": true,
  "scripts": {
    // 定义脚本,由于还没有集成实际的构建流程,这里先以打印命令代替
    "dev": "echo dev",
    "build": "echo build"
  },
  "dependencies": {
    // 安装文档特有依赖
  },
  "devDependencies": {
    // 安装文档特有依赖
  }
}

好基本的monorepo项目目录我们已经搭建完毕,接下来我们会集成webpack、typescript、eslint等开发工具🔧。期待与你相遇,和你共同成长,jy们下节见~~~

farmer-ui源码地址

转载自:https://juejin.cn/post/7308923428149297187
评论
请登录