---
sidebar_position: 1
---

# 开发组件

本章将要介绍如何使用模块工程解决方案开发组件项目。

## 初始化项目

<CH.Spotlight>

```bash 交互式问题
npx @modern-js/create@latest components-project

? 请选择你想创建的工程类型：Npm 模块
? 请填写项目名称：components-demo
? 请选择开发语言：TS
? 请选择包管理工具：pnpm
```

---

推荐使用 `@modern-js/create` 命令来初始化一个 npm 项目。

```bash 交互式问题
npx @modern-js/create@latest components-project

? 请选择你想创建的工程类型：Npm 模块
? 请填写项目名称：components-demo
? 请选择开发语言：TS
? 请选择包管理工具：pnpm
```

---

初始化的目录结构。

```bash 初始化的目录结构。
.
├── README.md
├── node_modules/
├── dist/
├── modern.config.ts
├── package.json
├── pnpm-lock.yaml
├── src
│   ├── index.ts
│   └── modern-app-env.d.ts
└── tsconfig.json
```

---

修改 `./src/index.ts` 文件后缀和内容。

此时就初始化了一个组件项目。

```tsx ./src/index.tsx
export default () => {
  return <div>hello world</div>;
};
```

</CH.Spotlight>

## 使用 Storybook 调试代码

<CH.Spotlight>

```bash 终端
pnpm run new

? 请选择你想要的操作 启用可选功能
? 请选择功能名称 启用「Storybook」
```

---

在项目根目录下执行 `new` 命令，可以开启 Storybook 功能。

```bash 终端
pnpm run new

? 请选择你想要的操作 启用可选功能
? 请选择功能名称 启用「Storybook」
```

---

成功开启后，会看到 `package.json` 中新增了依赖。同时创建了 `stories` 目录和相关的初始化文件。

<CH.Code>

```json ./package.json focus=4:7
{
  "name": "components-demo",
  "devDependencies": {
    "@modern-js/plugin-storybook": "x.y.z",
    "@modern-js/runtime": "x.y.z",
    "react": "^17",
    "react-dom": "^17"
  }
}
```

```bash 目录结构
.
├── src
│   ├── index.ts
│   └── modern-app-env.d.ts
├── stories
│   ├── .eslintrc.js
│   ├── index.stories.tsx
│   └── tsconfig.json
```

</CH.Code>

---

初始化后，在 `./stories` 目录下的 `tsconfig.json` 文件中默认设置了与项目同名的 `paths` 配置。

```json ./stories/tsconfig.json focus=5:7
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "../",
    "paths": {
      "components-demo": ["./"],
      "components-demo/*": ["./*"]
    }
  },
  "include": ["**/*"]
}
```

---

这样的配置可以让你在 Story 代码中直接使用项目的名称引入代码。

<CH.Code>

```tsx ./stories/index.stories.tsx
import Component from 'components-demo';

export const YourStory = () => <Component />;

export default {
  title: 'Your Stories',
};
```

---

```tsx ./src/index.tsx
export default () => {
  return <div>hello world</div>;
};
```

</CH.Code>

---

此时 Storybook 会根据项目的 `package.json` 文件中的 `main`、`exports` 这样的字段确认导入代码的入口；
根据 `types` 字段确定类型文件的位置。

```json package.json
{
  "name": "components-demo",
  "main": "./dist/esm/index.js",
  "types": "./dist/types/index.d.ts"
}
```

---

虽然也支持导入源代码进行调试，不过使用项目产物调试更加可靠。

使用源代码调试存在局限性：某些配置无法在 Storybook 和原本构建支持的配置等价使用。

**因此更推荐使用产物进行调试**。

```tsx ./stories/index.stories.tsx
import Component from '../src';

export const YourStory = () => <Component />;

export default {
  title: 'Your Stories',
};
```

</CH.Spotlight>

## 开发样式

接下来我们可以给组件添加样式。

目前支持开发样式的能力有：

- CSS/PostCSS
- Less
- Scss/Sass
- Tailwind CSS
- CSS Modules

### CSS/PostCSS

模块工程支持 PostCSS，并且内置了以下 postcss 插件：

- [flexbugs-fixes](https://github.com/luisrudge/postcss-flexbugs-fixes)
- [custom-properties](https://github.com/postcss/postcss-custom-properties)
- [initial](https://github.com/maximkoretskiy/postcss-initial)
- [page-break](https://github.com/shrpne/postcss-page-break)
- [font-variant](https://github.com/postcss/postcss-font-variant)
- [media-minmax](https://github.com/postcss/postcss-media-minmax)
- [nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting#readme)

因此我们可以在项目中创建 `.css` 文件，并且可以直接在 css 文件中使用这些插件提供的语法支持和能力。

<CH.Spotlight>

```css ./src/index.css
a,
b {
  color: red;

  /* "&" comes first */
  & c,
  & d {
    color: white;
  }

  /* "&" comes later, requiring "@nest" */
  @nest e & {
    color: yellow;
  }
}
```

---

源代码。

```css ./src/index.css

```

---

css 产物。

```css ./dist/es/index.css
a,
b {
  color: red;
}
a c,
b c,
a d,
b d {
  color: white;
}
e a,
e b {
  color: yellow;
}
```

</CH.Spotlight>

### Less

模块工程支持使用 Less 开发样式。

> 目前内置的 Less 版本为 v4.1.3

<CH.Spotlight>

```less ./src/common.less
@bg: black;
@bg-light: boolean(luma(@bg) > 50%);

div {
  background: @bg;
  color: if(@bg-light, black, white);
}
```

---

源代码。

```less ./src/common.less

```

---

Less 产物。

```css ./dist/es/common.css
div {
  background: black;
  color: white;
}
```

</CH.Spotlight>

### Sass/Scss

模块工程支持使用 Scss/Sass 开发样式。

> 目前内置的 Sass 版本为 v1.54.4

<CH.Spotlight>

```sass ./src/common.sass
$font-stack: Helvetica, sans-serif;
$primary-color: #333;

body {
  font: 100% $font-stack;
  color: $primary-color;
}
```

---

源代码。

```sass ./src/common.sass

```

---

Less 产物。

```css ./dist/es/common.css
body {
  font: 100% Helvetica, sans-serif;
  color: #333;
}
```

</CH.Spotlight>

### Tailwind CSS

模块工程支持使用 Tailwind CSS 开发组件样式。

默认情况下，模块工程没有开启该功能，需要按照下面的方式开启它。

<CH.Spotlight>

```bash 终端
pnpm run new

? 请选择你想要的操作 启用可选功能
? 请选择功能名称 启用「Storybook」
```

---

在项目根目录下执行 `new` 命令，可以开启 Tailwind CSS 功能。

```bash 终端
pnpm run new

? 请选择你想要的操作 启用可选功能
? 请选择功能名称 启用 「Tailwind CSS」 支持
```

---

成功开启后，会看到 `package.json` 中新增了依赖。

```json ./package.json
{
  "devDependencies": {
    "@modern-js/plugin-tailwindcss": "x.y.z"
  }
}
```

</CH.Spotlight>

Tailwind CSS 提供了两种使用方式：

#### HTML 类名

<CH.Spotlight>

```tsx ./src/index.tsx
import 'tailwindcss/utilities.css';

export default () => {
  return <div className="bg-black text-white">hello world</div>;
};
```

---

Tailwind CSS 支持在 HTML 标签上使用类名的方式增加样式。

```tsx ./src/index.tsx focus=5:5

```

---

**当使用 HTML 类名的时候，一定要注意导入 Tailwind CSS 相应的 css 文件。**

```tsx ./src/index.tsx focus=1:1

```

---

样式产物。

> 此时是 bundle 构建。

```css ./dist/es/index.css
/* ../../node_modules/.pnpm/tailwindcss@2.2.19/node_modules/tailwindcss/utilities.css */
.table {
  display: table;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
@keyframes ping {
  75%,
  100% {
    transform: scale(2);
    opacity: 0;
  }
}
@keyframes pulse {
  50% {
    opacity: 0.5;
  }
}
@keyframes bounce {
  0%,
  100% {
    transform: translateY(-25%);
    animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
  }
  50% {
    transform: none;
    animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
  }
}
.bg-black {
  --tw-bg-opacity: 1;
  background-color: rgba(0, 0, 0, var(--tw-bg-opacity));
}
.text-white {
  --tw-text-opacity: 1;
  color: rgba(255, 255, 255, var(--tw-text-opacity));
}
*,
::before,
::after {
  --tw-shadow: 0 0 #0000;
}
*,
::before,
::after {
  --tw-ring-inset: var(--tw-empty);
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: rgba(59, 130, 246, 0.5);
  --tw-ring-offset-shadow: 0 0 #0000;
  --tw-ring-shadow: 0 0 #0000;
}
@media (min-width: 640px) {
}
@media (min-width: 768px) {
}
@media (min-width: 1024px) {
}
@media (min-width: 1280px) {
}
@media (min-width: 1536px) {
}
```

</CH.Spotlight>

#### `@apply`

Tailwind CSS 提供了 [`@apply`](https://v2.tailwindcss.com/docs/functions-and-directives#apply) 指令，可以通过它将 Tailwind CSS 提供的样式内联到我们编写的样式中。

`@apply` 可以用于 CSS、Less、Sass 中。

```css
.btn {
  @apply font-bold py-2 px-4 rounded;
}
```

但是使用过程中，对于 Less 和 Sass 有些情况需要注意：

##### Sass

当将 Tailwind 与 Sass 一起使用时，`@apply` 后面存在 `!important` 的时候，需要使用插值来让 Sass 正确编译。

<CH.Spotlight>

```sass
.alert {
  @apply bg-red-500 !important;
}
```

---

不能正常工作。

```sass

```

---

能够正常工作。

```sass
.alert {
  @apply bg-red-500 #{!important};
}
```

</CH.Spotlight>

##### Less

在与 Less 一起使用 Tailwind 时，你不能嵌套 Tailwind 的 `@screen` 指令。

<CH.Spotlight>

```less
.card {
  @apply rounded-none;

  @screen sm {
    @apply rounded-lg;
  }
}
```

---

不能正常工作。

```less

```

---

相反，使用常规的媒体查询和 `theme()` 函数来引用你的屏幕尺寸，或者干脆不要嵌套你的 `@screen` 指令。

<CH.Code>

```less 方法一
// Use a regular media query and theme()
.card {
  @apply rounded-none;

  @media (min-width: theme('screens.sm')) {
    @apply rounded-lg;
  }
}
```

---

```less 方法二
// Use the @screen directive at the top-level
.card {
  @apply rounded-none;

  @media (min-width: theme('screens.sm')) {
    @apply rounded-lg;
  }
}
```

</CH.Code>

</CH.Spotlight>

#### 推荐方式

**推荐使用 `@apply` 指定的方式开发样式，这样在样式产物中仅包含通过指令内联的样式。**

当使用 HTML 类名的方式添加样式的时候，默认情况下 Tailwind 不仅会将本身类名对应的样式加入产物中，同时还会有额外的样式代码存在，虽然这些代码可能不会对本身的样式产生影响。

#### bundle 和 bundleless 构建产物区别

对于以下代码，在 bundle 和 bundleless 两种模式下，构建产物会有很大区别。

> 所谓 bundle 和 bundleless 可以查看 [【Bundle 和 Bundleless】](/guide/advance/in-depth-about-build#bundle-和-bundleless)

```tsx ./src/index.tsx
import 'tailwindcss/utilities.css';

export default () => {
  return <div className="bg-black text-white">hello world11</div>;
};
```

Bundle 模式下，会将第三方依赖打包进来。

对于样式则会生成一份单独的产物文件，并且在 Js 产物文件中并不会存在导入样式的相关代码。

如果需要将样式注入 Js 产物中，可以开启 [`style.inject` API](/api/config/build-config#inject)。

<CH.Code>
```css ./dist/es/index.css
/* ../../node_modules/.pnpm/tailwindcss@2.2.19/node_modules/tailwindcss/utilities.css */
.table {
  display: table;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
@keyframes ping {
  75%, 100% {
    transform: scale(2);
    opacity: 0;
  }
}
@keyframes pulse {
  50% {
    opacity: .5;
  }
}
@keyframes bounce {
  0%, 100% {
    transform: translateY(-25%);
    animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
  }
  50% {
    transform: none;
    animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
  }
}
.bg-black {
  --tw-bg-opacity: 1;
  background-color: rgba(0, 0, 0, var(--tw-bg-opacity));
}
.text-white {
  --tw-text-opacity: 1;
  color: rgba(255, 255, 255, var(--tw-text-opacity));
}
*,
::before,
::after {
  --tw-shadow: 0 0 #0000 ;
}
*,
::before,
::after {
  --tw-ring-inset: var(--tw-empty, );
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: rgba(59, 130, 246, 0.5);
  --tw-ring-offset-shadow: 0 0 #0000;
  --tw-ring-shadow: 0 0 #0000 ;
}
@media (min-width: 640px) {
}
@media (min-width: 768px) {
}
@media (min-width: 1024px) {
}
@media (min-width: 1280px) {
}
@media (min-width: 1536px) {
}
```
---
``` js ./dist/es/index.js
// src/index.tsx
import { jsx } from "react/jsx-runtime";
var src_default = () => {
  return /* @__PURE__ */ jsx("div", {
    className: "bg-black text-white",
    children: "hello world11"
  });
};
export {
  src_default as default
};
```
</CH.Code>

Bundleless 模式下，并不会将第三方依赖打包进来，此时不会有样式产物生成。

```js ./dist/es/index.js
import { jsx } from 'react/jsx-runtime';
import 'tailwindcss/utilities.css';
var src_default = () => {
  return /* @__PURE__ */ jsx('div', {
    className: 'bg-black text-white',
    children: 'hello world11',
  });
};
export { src_default as default };
```

### CSS Modules

模块工程支持使用 CSS Module 开发样式。默认情况下会将以下文件识别为 CSS Module 文件：

- `.module.css`
- `.module.less`
- `.module.scss`
- `.module.sass`

如果需要对 CSS Modules 进行配置，可以查看一下 API：

- [autoModules](/api/config/build-config#automodules)
- [modules](/api/config/build-config#modules)

下面是一个代码示例：

<CH.Code>

```tsx ./src/index.tsx
import style from './index.module.css';

export default () => {
  return <div className={style.btn}>hello world</div>;
};
```

```css ./src/index.module.css
.btn {
  color: blue;
}
```

</CH.Code>

## 配置构建产物

根据组件项目使用的多数场景，**推荐使用 `npm-component` 构建预设**。该预设得到的产物目录结构为：

```bash
.
├── dist
│   ├── es
│   ├── lib
│   └── types
```

- `./dist/es`: 包含了支持 es6 语法的 ES modules 格式的 bundleless 产物。
- `./dist/lib`: 包含了支持 es6 语法的 CommonJS 格式的 bundleless 产物。
- `./dist/types`: 包含了类型文件。

如果对使用语法支持有要求，可以手动配置 [`buildPreset`](/api/config/build-preset)，并且支持在 `npm-component` 基础上增加后缀的方式修改支持的语法：

```tsx
export default defineConfig({
  buildPreset: 'npm-component-es2019',
});
```

如果对构建产物目录结构有特殊需求，则可以使用 [`buildConfig` API](/api/config/build-config)，可以通过以下文档来了解使用方式：

- [修改输出产物](/guide/basic/modify-output-product#构建配置对象)
- [深入理解构建](/guide/advance/in-depth-about-build)

## 测试组件

关于如何对组件进行测试，可以参考 [【测试项目】](/guide/basic/test-your-project)。

## 发布组件

推荐使用模块工程提供版本发布功能，可以参考 [【版本管理与发布】](/guide/basic/publish-your-project)。
