构建组件并在不同的包之间重用它们,让我得出结论,有必要在一个单一的结构中组织这些项目的内容的正确方法。构建工具应该是相同的,包括测试环境、lint规则和组件库的高效资源分配。
我正在寻找能够为我带来高效和有效的构建强大组合的工具。结果,一个强大的三人组出现了。在本文中,我们将使用所有这些工具创建几个包。
工具
在我们开始之前,让我们来看看每个工具都做了什么。
- Lerna:管理具有多个包的JavaScript项目;它优化了围绕使用Git和NPM管理多包存储库的工作流程。
- Vite:提供快速热模块替换、开箱即用的ES模块支持、广泛的功能和React插件支持的构建工具。
- Storybook:一个用于在隔离环境中开发和组织UI组件的开源工具,也是视觉测试和创建交互式文档的平台。
Lerna 初始设置
第一步是设置Lerna项目。创建一个名为lerna_vite_monorepo的文件夹,在该文件夹内通过终端运行npx lerna init — 这将为Lerna项目创建一个基本结构。它会生成两个文件 — lerna.json、package.json — 和一个空的packages文件夹。
lerna.json — 该文件使Lerna能够简化您的monorepo配置,提供关于如何链接依赖项、定位包、实施版本策略和执行其他任务的指令。
Vite 初始设置
安装完成后,将会有一个packages文件夹。我们的下一步是在packages文件夹内创建几个额外的文件夹:
- vite-common
- footer-components
- body-components
- footer-components
要创建这些项目,我们需要使用项目名称运行npm init vite。选择React作为框架,Typescript作为变体。这些项目将使用相同的lint规则、构建流程和React版本。
在每个包中进行此过程将生成一堆文件和文件夹。
Storybook 初始设置
为我们的每个包设置一个Storybook。转到其中一个包的文件夹,运行npx storybook@latest init进行Storybook安装。对于关于eslint-plugin-storybook的问题,输入Y进行安装。之后,将启动安装依赖项的过程。
这将在src中生成一个.storybook文件夹和stories。让我们删除stories文件夹,因为我们将构建自己的组件。
现在,运行安装npx sb init --builder @storybook/builder-vite — 这将帮助您使用Vite快速启动和HMR构建您的故事。
假设每个文件夹都有相同的配置。如果这些安装已经完成,那么您可以在包文件夹内运行yarn storybook并运行Storybook。
初始配置
我们的想法是重用所有包的常见设置。让我们从每个存储库中删除一些我们不需要的文件。最终,您拥有的每个文件夹应包含以下一组文件夹和文件。
现在,让我们从一个包文件夹的package.json中提取所有devDependencies,并将它们全部放入根package.json的devDependenices中。
在根目录中运行npx storybook@latest init,并在main.js中修复属性。
从根目录的package.json中删除两个脚本。
在每个包文件夹中添加一个带有index.tsx文件的components文件夹。
我们可以建立适用于所有包的通用配置。这包括Vite、Storybook、Jest、Babel和Prettier的设置,可以进行通用配置。
根文件夹必须具有以下文件。
我们不会在此示例中考虑Babel、Jest和Prettier的设置。
Lerna 配置
首先,让我们检查Lerna配置文件,它有助于管理我们的多包monorepo项目。
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"packages": ["packages/*"],
"version": "independent"
}
首先,"$schema"为Lerna配置提供了结构和验证。
当"useWorkspaces"为true时,Lerna将使用yarn工作区来更好地链接和管理跨包的依赖关系。如果为false,Lerna将管理monorepo中的包之间的依赖关系。
"packages"定义了Lerna在项目中可以找到包的位置。
"version"设置为"independent"时,Lerna允许monorepo中的每个包具有自己的版本号,为单独的包发布更新提供了灵活性。
通用 Vite 配置
现在,让我们检查vite.config.ts文件中的必要元素。
import path from "path";
import { defineConfig } from "vite";
import pluginReact from "@vitejs/plugin-react";
const isExternal = (id: string) => !id.startsWith(".") && !path.isAbsolute(id);
export const getBaseConfig = ({ plugins = [], lib }) =>
defineConfig({
plugins: [pluginReact(), ...plugins],
build: {
lib,
rollupOptions: {
external: isExternal,
output: {
globals: {
react: "React",
},
},
},
},
});
该文件将导出Vite的通用配置,其中包括我们将在每个包中重用的额外插件和库。defineConfig在Vite的配置文件中充当实用程序函数。虽然它不直接执行任何逻辑或更改传递的配置对象,但它的主要作用是增强类型推断,并在特定代码编辑器中促进自动完成。
rollupOptions允许您指定自定义的Rollup选项。Rollup是Vite在其构建过程中使用的模块打包工具。通过直接提供选项给Rollup,开发人员可以更精细地控制构建过程。rollupOptions中的external选项用于指定应将哪些模块视为外部依赖项。
通常情况下,使用external选项可以通过排除已存在于代码将在其中运行的环境中的依赖项来帮助减少捆绑包的大小。
在Rollup的配置中,output选项中的globals: { react: "React" }意味着在生成的捆绑包中,对react的任何导入语句都将被替换为全局变量React。实质上,它假定React已经存在于用户的环境中,并且应该作为全局变量访问,而不是包含在捆绑包中。
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
tsconfig.node.json文件用于专门控制TypeScript与vite.config.ts文件的转译,确保其与Node.js兼容。Vite用于提供和构建前端资产,运行在Node.js环境中。这种分离是必要的,因为Vite配置文件可能需要与您的前端代码不同的TypeScript设置,后者旨在在浏览器中运行。
{
"compilerOptions": {
// ...
"types": ["vite/client", "jest", "@testing-library/jest-dom"],
// ...
},
"references": [{ "path": "./tsconfig.node.json" }]
}
在tsconfig.json中包含"types": ["vite/client"]是必要的,因为Vite在import.meta对象上提供了一些不属于标准JavaScript或TypeScript库的附加属性,例如import.meta.env和import.meta.glob。
通用 Storybook 配置
.storybook目录定义了Storybook的配置、插件和装饰器。这对于自定义和配置Storybook的行为至关重要。
├── main.ts
└── preview.ts
对于通用配置,这里有两个文件。让我们一起检查它们。
main.ts是Storybook的主配置文件,允许您控制Storybook的行为。正如您所看到的,我们只是导出通用配置,我们将在每个包中重用。
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
addons: [
{
name: "@storybook/preset-scss",
options: {
cssLoaderOptions: {
importLoaders: 1,
modules: {
mode: "local",
auto: true,
localIdentName: "[name]__[local]___[hash:base64:5]",
exportGlobals: true,
},
},
},
},
{
name: "@storybook/addon-styling",
options: {
postCss: {
implementation: require("postcss"),
},
},
},
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"storybook-addon-mock",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;
preview.ts文件允许我们使用装饰器包装故事,我们可以使用它们为我们的故事提供上下文或在全局范围内设置样式。我们还可以使用此文件配置全局参数。它还将导出用于包使用的通用配置。
import type { Preview } from "@storybook/react";
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
options: {
storySort: (a, b) => {
return a.title === b.title
? 0
: a.id.localeCompare(b.id, { numeric: true });
},
},
layout: "fullscreen",
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
根 package.json
在Lerna monorepo项目中,package.json的作用与任何其他JavaScript或TypeScript项目中的作用类似。但是,某些方面对monorepo是独特的。
{
"name": "root",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"start:vite-common": "lerna run --scope vite-common storybook --stream",
"build:vite-common": "lerna run --scope vite-common build --stream",
"test:vite-common": "lerna run --scope vite-common test --stream",
"start:vite-body": "lerna run --scope vite-body storybook --stream",
"build": "lerna run build --stream",
"test": "NODE_ENV=test jest --coverage"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.22.1",
"@babel/preset-env": "^7.22.2",
"@babel/preset-react": "^7.22.3",
"@babel/preset-typescript": "^7.21.5",
"@storybook/addon-actions": "^7.0.18",
"@storybook/addon-essentials": "^7.0.18",
"@storybook/addon-interactions": "^7.0.18",
"@storybook/addon-links": "^7.0.18",
"@storybook/addon-styling": "^1.0.8",
"@storybook/blocks": "^7.0.18",
"@storybook/builder-vite": "^7.0.18",
"@storybook/preset-scss": "^1.0.3",
"@storybook/react": "^7.0.18",
"@storybook/react-vite": "^7.0.18",
"@storybook/testing-library": "^0.1.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.1",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^4.0.0",
"babel-jest": "^29.5.0",
"babel-loader": "^8.3.0",
"eslint": "^8.41.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"eslint-plugin-storybook": "^0.6.12",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"lerna": "^6.5.1",
"path": "^0.12.7",
"prettier": "^2.8.8",
"prop-types": "^15.8.1",
"sass": "^1.62.1",
"storybook": "^7.0.18",
"storybook-addon-mock": "^4.0.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.2",
"vite": "^4.3.2"
}
}
脚本将管理monorepo。运行所有包的测试或构建所有包。此package.json还包括跨多个包共享的开发依赖项,例如测试库或构建工具。private字段通常在此package.json中设置为true,以防止意外发布。
当然,脚本可以扩展为其他包,用于测试、构建等,例如:
"start:vite-footer": "lerna run --scope vite-footer storybook --stream",
包级配置
就像我们从根目录导出了所有配置以便在包级别重用这些配置一样。
Vite配置将使用根Vite配置,我们只需导入getBaseConfig函数并在此处提供lib。此配置用于将我们的组件包构建为独立库。它指定了我们包的入口点、库名称和输出文件名。使用此配置,Vite将生成一个编译后的文件,以指定的库名称公开我们的组件包,从而允许它在其他项目中使用或单独分发。
import * as path from "path";
import { getBaseConfig } from "../../vite.config";
export default getBaseConfig({
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "ViteFooter",
fileName: "vite-footer",
},
});
对于.storybook,我们使用相同的方法。我们只需导入commonConfigs。
import commonConfigs from "../../../.storybook/main";
const config = {
...commonConfigs,
stories: ["../src/**/*..mdx", "../src/** /*.stories.@(js|jsx|ts|tsx)"],
};
export default config;
并且也要预览它。
import preview from "../../../.storybook/preview";
export default preview;
对于.storybook文件夹中的最后一个文件,我们需要添加preview-head.html。
<script>
window.global = window;
</script>
最棒的部分是,我们有一个非常干净的package.json,没有依赖项,我们都从根目录使用它们。
{
"name": "vite-footer",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"vite-common": "^2.0.0"
}
}
唯一的区别是vite-common,这是我们在Footer组件中使用的依赖项。
组件
通过以这种方式组织我们的组件包,我们可以轻松管理和独立发布每个包,同时共享monorepo提供的通用依赖项和基础设施。
让我们看看Footer组件的src文件夹。其他组件将是相同的,但配置只是有所不同。
├── assets
│ └── flow.svg
├── components
│ ├── Footer
│ │ ├── Footer.stories.tsx
│ │ └── index.tsx
│ └── index.ts
├── index.ts
└── vite-env.d.ts
src文件夹中的vite-env.d.ts文件有助于TypeScript理解和提供项目中与Vite相关的代码的准确类型检查。它确保TypeScript可以识别和验证Vite特定的属性、函数和特性。
/// <reference types="vite/client" />
在src文件夹中,index.ts包含:
export * from "./components";
以及消耗vite-common组件的组件如下:
import { Button, Links } from "vite-common";
export interface FooterProps {
links: {
label: string;
href: string;
}[];
}
export const Footer = ({ links }: FooterProps) => {
return (
<footer>
<Links links={links} />
<Button label="Click Button" backgroundColor="green" />
</footer>
);
};
export default Footer;
以下是该组件的stories:
import { StoryFn, Meta } from "@storybook/react";
import { Footer } from ".";
export default {
title: "Example/Footer",
component: Footer,
parameters: {
layout: "fullscreen",
},
} as Meta<typeof Footer>;
const mockedLinks = [
{ label: "Home", href: "/" },
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
];
const Template: StoryFn<typeof Footer> = (args) => <Footer {...args} />;
export const FooterWithLinks = Template.bind({});
FooterWithLinks.args = {
links: mockedLinks,
};
export const FooterWithOneLink = Template.bind({});
FooterWithOneLink.args = {
links: [mockedLinks[0]],
};
在这个例子中,我们使用了四个包,但方法是相同的。一旦创建了所有的包,您就可以独立地构建、运行和测试它们。在所有包都在根级别之前,运行yarn install,然后yarn build来构建所有包,或者构建yarn build:vite-common,然后您可以开始在其他包中使用该包。
发布
要发布monorepo中的所有包,我们可以使用npx lerna publish命令。此命令将指导我们根据所做的更改对每个包进行版本控制和发布。
lerna notice cli v6.6.2
lerna info versioning independent
lerna info Looking for changed packages since vite-body@1.0.0
? Select a new version for vite-body (currently 1.0.0) Major (2.0.0)
? Select a new version for vite-common (currently 2.0.0) Patch (2.0.1)
? Select a new version for vite-footer (currently 1.0.0) Minor (1.1.0)
? Select a new version for vite-header (currently 1.0.0)
Patch (1.0.1)
? Minor (1.1.0)
Major (2.0.0)
Prepatch (1.0.1-alpha.0)
Preminor (1.1.0-alpha.0)
Premajor (2.0.0-alpha.0)
Custom Prerelease
Custom Version
Lerna将询问我们每个包的版本,然后您可以发布它。
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...
lerna success All packages have already been published.
结论
我正在寻找我们公司前端组件组织的稳固架构解决方案。对于每个项目,我们都有一个强大、高效的开发环境,具有帮助我们独立的通用规则。这种组合为我提供了简化的依赖管理、隔离的组件测试和简化的发布。
参考
- 存储库
- Vite与Storybook
本文暂时没有评论,来添加一个吧(●'◡'●)