一、技术栈

我们先简单了解一下要搭建一个团队的 UI 组件库,会涉及到哪些技术栈:

  • Create React App:官方支持的 CLI 脚手架,提供一个零配置的现代构建设置;
  • React: 用于构建用户界面的 JavaScript 库;
  • Ant Design:一套企业级 UI 设计语言和 React 组件库;
  • Storybook: 辅助 UI 控件开发的工具,通过story创建独立的控件,让每个控件开发都有一个独立的开发调试环境;
  • TypeScript:2020 最火的前端语言,是JavaScript类型的超集;
  • ESLint && husky:统一团队代码风格;
  • Jest:JavaScript 测试框架,用于组件库的单元测试;
  • Travis CI: 提供持续集成服务,用于进行项目的持续集成以及持续部署;

二、项目准备

2.1 创建组件库的工程环境

使用 Create React App 创建 UI 组件库的前端工程环境。

npx create-react-app ii-admin-base --typescript

2.2 安装 Storybook

采用自动方式安装 Storybook,命令如下:

npx -p @storybook/cli sb init --type react_scripts

  • 参数 react_scripts 用来告诉 Storybook 当前项目使用 Create React App 创建的,Storybook会根据该参数来自动安装合适的包。

2.3 安装 Storybook 插件

2.3.1 addon-info 插件

addon-info 插件会自动识别组件传递的 props 生成表格。

yarn add @storybook/addon-info -D yarn add @types/storybook__addon-info -D

三、配置 Storybook

在配置 Storybook 之前,先简单了解下 stories 的加载流程。

stories 的加载是在 .storybook/main.js 或 .storybook/preview.js 这两个文件中进行。加载 stories 的最简便方式是按文件名进行加载。假设你的 stories 文件位于 src/components 目录,则可以通过如下方式进行加载:

// .storybook/main.jsmodule.exports = {  stories: ['../src/components/**/*.stories.js'],};复制代码

或者可以在 .storybook/preview.js 中加载所有的 stories :

import { configure } from '@storybook/react';configure(require.context('../src/components', true, /.stories.js$/), module);复制代码

注意:在 .storybook/preview.js 文件中,只能调用一次 configure 函数。

configure 函数接收参数为:

  • 单个 require.context “req”
  • 从多个地方加载文件的 “req”s 数组;
  • 返回值是 void 或 an array of module exports 的加载函数;

如果想从多个地方进行加载,可采用数组方式,如下所示:

import { configure } from '@storybook/react';configure(  [    require.context('../src/components', true, /.stories.js$/),    require.context('../lib', true, /.stories.js$/),  ],  module);复制代码

注:

如果想引入一个文件夹下面的所有文件,或者引入能匹配一个正则表达式的所有文件,可以使用函数require.context()。require.context() 函数有 3 个参数:

  • 要搜索的文件夹目录;
  • 是否还应该搜索它的子目录;
  • 以及一个匹配文件的正则表达式;

3.1 配置 stories 显示顺序

若想改变 stories 的显示顺序,该如何操作?示例如下,将 welcome.stories.tsx 先添加至数组中,从而改变 stories 的显示顺序:

import { configure } from '@storybook/react';// 将 welcome 文档说明置于顶部const loaderFn = () => {  const allExports = [require('../src/welcome.stories.tsx')];  const req = require.context('../src/components', true, /.stories.tsx$/);  req.keys().forEach((fname) => allExports.push(req(fname)));  return allExports;};// automatically import all files ending in *.stories.tsxconfigure(loaderFn, module);复制代码

3.2 支持 Typescript

要搭建的基础组件库是基于 Typescript 进行编写的,因此还需添加 Typescript 支持。配置 ./storybook/main.js 文件,内容如下:

  webpackFinal: async (config) => {    config.module.rules.push({      test: /.(ts|tsx)$/,      use: [        {          loader: require.resolve('babel-loader'),          options: {            presets: [require.resolve('babel-preset-react-app')],          },        },      ],    });    return config;  }复制代码

3.3 配置 less

要搭建的基础组件库是基于 Ant Design 做的二次封装,因此就不得不支持 less。针对 less,配置 ./storybook/main.js 文件,内容如下:

 // .storybook/main.js webpackFinal: async (config) => {    config.module.rules.push({      test: /.(ts|tsx)$/,      use: [        {          loader: require.resolve('babel-loader'),          options: {            presets: [require.resolve('babel-preset-react-app')],          },        },      ],    });    config.module.rules.push({      test: /.less$/,      loaders: [        'style-loader',        'css-loader',        {          loader: 'less-loader',          options: {            lessOptions: {              javascriptEnabled: true,            },          },        },      ],      include: [path.resolve(__dirname, '../src'), /[/]node_modules[/].*antd/],    });    return config;  },复制代码

完成上述内容配置,发现导入的 less 文件不生效。针对这一问题,进行了以下几点排查。

问题1: 如果 less-loader 版本是 6.0 以上,则如下配置会报错

{  loader: "less-loader",  options: {    javascriptEnabled: true  }}复制代码

需修改成:

 {   loader: 'less-loader',     options: {       lessOptions: {         javascriptEnabled: true,       },     }, }复制代码

问题2: storybook 5.3.0 与 storybook 5.2.x 存在一些差异,见参考链接。cra(create-react-app)的 file-loader 会拦截所有其他文件,导致less 文件不能进入less-loader中。针对这一问题,需配置 @storybook/preset-create-react-app,配置内容如下:

{  name: '@storybook/preset-create-react-app',    options: {      craOverrides: {        fileLoaderExcludes: ['less'],      },  },}复制代码

问题3: 此次搭建的基础组件库是基于 Ant Design 做的二次封装,再对 Ant Design 组件进行引用时,发现样式不生效。针对这一问题,可以在 preview.tsx 进行如下配置:

import { configure } from '@storybook/react';import 'antd/dist/antd.less'    // 引入 antd 样式复制代码

3.4 添加全局装饰器

启动 Storybook,会发现右侧 stories 内容紧靠着左侧菜单栏,整体感觉非常紧凑、不美观。针对这种情况通常可以通过添加 padding 来解决。那么该如何让 padding 对 Storybook 中的所有 stories 进行生效呢?这个时候就需使用到全局装饰器。

在 .storybook 目录下,创建全局装饰器,如下所示:

// .storybook/decorators/WrapperDecorator/index.tsximport React from 'react';const wrapperStyle: React.CSSProperties = {  padding: '20px 40px',};// 创建一个样式包裹的装饰器const WrapperDecorator = (storyFn) => 
{storyFn()}

;export default WrapperDecorator;复制代码

然后在 preview.tsx 添加该装饰器即可。

// .storybook/preview.tsx import { addDecorator, configure } from '@storybook/react';import WrapperDecorator from './decorators/WrapperDecorator';import 'antd/dist/antd.less';// 通过addDecorator添加插件addDecorator(WrapperDecorator);复制代码

最后效果如下所示。

四、组件开发

4.1 验证码输入组件

此次要示例的验证码输入组件是一个带验证码发送功能的 Input 组件,如下图所示。

整个组件是在 Ant Design 的 Input 组件上进行的二次开发,详细代码如下图示所示:

import React, { useState, FC } from 'react';import { Input } from 'antd';import { InputProps } from 'antd/lib/input';import classNames from 'classnames';export interface InputVerifyProps extends InputProps {  /** 发送验证码接口函数 */  sendCode: () => void;  /** 倒计时时间 */  countDown?: number;  /** 初始验证码文本内容 */  initCodeText?: string;  /** 重新发送验证码文本内容 */  reCodeText?: string;  /** 验证码类名 */  codeClassname?: string;}export const InputVerify: FC = (props) => {  const { sendCode, countDown, initCodeText, reCodeText, codeClassname, ...restProps } = props;  const [codeText, setCodeText] = useState(initCodeText);  const [codeStatus, setCodeStatus] = useState(false);    const handleCountDown = (timer: ReturnType | null, count: number) => {    if (timer) {      clearTimeout(timer);    }    if (count <= 0) {      setCodeText(reCodeText);      setCodeStatus(false);    } else {      setCodeText(`${count} s`);      const newTimer: ReturnType = setTimeout(() => {        handleCountDown(newTimer, count - 1);      }, 1000);    }  };  const handleCodeClick = () => {    if (codeStatus) return;    sendCode && sendCode();    setCodeStatus(true);    handleCountDown(null, countDown as number);  };  const codeCls = classNames('ii-verify-button', codeClassname, {    'ii-verify-button-disabled': codeStatus,  });  return (              {codeText}              }    />  );};InputVerify.defaultProps = {  countDown: 60,  initCodeText: '发送验证码',  reCodeText: '重新发送',};export default InputVerify;复制代码

4.2 添加单元测试

完成组件开发任务后,接下来就需添加单元测试。针对验证码输入组件,单元测试主要分两个方面,一方面测试 antd 原生 Input 组件是否正常工作,另一方面则是测试验证码输入组件是否正常工作。

import React from 'react';import { render, fireEvent, wait, RenderResult } from '@testing-library/react';import '@testing-library/jest-dom';import '@testing-library/jest-dom/extend-expect';import InputVerify, { InputVerifyProps } from './InputVerify';const antdProps: InputVerifyProps = {  placeholder: 'antd input placeholder',  size: 'large',  sendCode: jest.fn(),  onPressEnter: jest.fn(),  onChange: jest.fn(),};const selfProps: InputVerifyProps = {  countDown: 3,  initCodeText: '发送验证码',  reCodeText: '再次发送',  sendCode: jest.fn(),};let wrapper: RenderResult, inputElement: HTMLInputElement;describe("Test InputVerify component on the props of antd's input component", () => {  beforeEach(() => {    wrapper = render();    inputElement = wrapper.getByTestId('test-input-verify') as HTMLInputElement;  });  it("should have the input's class of antd", () => {    expect(inputElement).toBeInTheDocument();    expect(inputElement).toHaveClass('ant-input');  });  it('should support size', () => {    expect(inputElement).toHaveClass('ant-input-lg');  });  it('should trigger onChange event correctly', () => {    fireEvent.change(inputElement, { target: { value: 'input test' } });    expect(antdProps.onChange).toHaveBeenCalled();    expect(inputElement.value).toEqual('input test');  });});describe("Test InputVerify component on the self's props", () => {  beforeEach(() => {    wrapper = render();  });  it('should render the correct InputVerify component', () => {    const suffixElement = wrapper.getByText('发送验证码');    expect(suffixElement).toBeInTheDocument();    expect(suffixElement).toHaveClass('ii-verify-button');  });  it('click verify button should call the right callback ', async () => {    const suffixElement = wrapper.getByText('发送验证码');    fireEvent.click(suffixElement);    expect(selfProps.sendCode).toHaveBeenCalled();    await wait(      () => {        expect(wrapper.getByText('再次发送')).toBeInTheDocument();      },      { timeout: 4000 }    );  });});复制代码

4.3 组件说明文档

当开发完单个组件,还需添加相应的文档说明,告诉其他人该如何使用这个组件。

4.3.1 自动生成说明文档

如果想通过注释方式来自动生成组件的说明文档,这个时候就需借助 react-docgen 插件。由于 @storybook/addon-info依赖包对 react-docgen 插件已进行了集成,所以编写注释的时候只需按照 JSDoc 标准来编写就会生成相应的说明文档。

/** * 带验证码功能的输入组件,适用于要发送验证码的场景。 * * ## 引用方法 * * ~~~javascript * import { InputVerfiy } from 'ii-admin-base' * ~~~ */export const InputVerify: FC = (props) => {复制代码

注意: react-docgen 插件要求组件还需通过 export 方式进行导出。

4.3.2 过滤 Prop Types

@storybook/addon-info 插件在自动生成 Prop Types 的说明文档时,会连组件继承的 Props 也自动生成,这里面不仅包括了第三方依赖包携带的 props,还可能包括 HTML 元素的原生 Props。若要过滤这些 Props,就需借助依赖包 react-docgen-typescript-loader。

先安装该依赖:

yarn add react-docgen-typescript-loader -D

然后配置 main.js文件,配置内容如下:

// .storybook/main.jsconfig.module.rules.push({  test: /.(ts|tsx)$/,  use: [    {      loader: require.resolve('babel-loader'),      options: {        presets: [require.resolve('babel-preset-react-app')],      },    },    // 过滤 node_modules 中的 props    {      loader: require.resolve('react-docgen-typescript-loader'),      options: {        // 将枚举或者联合类型转换成字符串形式,避免字符串字面量显示别名。        shouldExtractLiteralValuesFromEnum: true,        // 避免显示原生内置属性        propFilter: (prop) => {          if (prop.parent) {            return !prop.parent.fileName.includes('node_modules');          }          return true;        },      },    },  ],});复制代码

注意:

在使用最新的 Storybook v5.3.19 版本时,发现上述配置并不生效。针对这一问题,可以将 Storybook 版本降至 5.3.18 来进行规避。

五、构建及测试

5.1 打包构建

5.1.1 创建组件库的模块入口文件

在 src/index.tsx 文件中将所有组件都导入,再导出。这样就可以从入口文件直接导入所有组件。

export { default as InputVerfiy } from './components/InputVerify';复制代码

5.1.2 编译 TS 文件

使用 CRA(Create-React-App) 默认会创建一个 tsconfig.json 文件,该配置文件是与开发环境相关的。要针对组件库进行打包编译,并生成标准的 ES modules,还需单独创建一个 tsconfig.build.json 文件。

/** * 用于最后打包编译 */{  "compilerOptions": {    // 文件输出目录    "outDir": "dist",    // ESNext: 是标准的ES Modules形式    "module": "esnext",    // 指定编译以后符合什么样的ES标准    "target": "es5",    // 为每一个js文件生成一个对应的.d.ts类型文件,方便使用组件库的用户可以获得类型检查和ts提示    "declaration": true,    // jsx 是一种语法糖,是React.createElement的缩写。此处置为react,编译出来的文件就可以用React.createElement来代替JSX语法的过程    "jsx": "react",    // tsc 处理模块的方式和node不一样,默认处理方式是"classic",针对绝对路径有的时候会找不到文件(一直向上找文件),所以需设置成'node'。    "moduleResolution": "node",    // 默认不支持 import React from 'react',只支持 import * as React from 'react'    "allowSyntheticDefaultImports": true  },  // 要编译哪些文件  "include": ["src"],  "exclude": ["src/**/*.test.tsx", "src/**/*.stories.tsx"]}复制代码

然后在 package.json 文件中添加 build:ts 脚本,用于将 TS 文件编译成 ES modules 文件。

"build:ts": "tsc -p tsconfig.build.json",

5.1.3 编译 less 文件

在 package.json 文件中添加 build:css 脚本,用于将 less 文件编译成 css 。

"build:css": "lessc ./src/styles/index.less ./dist/index.css"

5.1.4 配置最终构建脚本

在 package.json 中配置最终的构建脚本 build,如下所示:

 "clean": "rimraf ./dist", "build:ts": "tsc -p tsconfig.build.json", "build:css": "lessc ./src/styles/index.less ./dist/index.css", "build": "npm run clean && npm run build:ts && npm run build:css",复制代码
  • 使用 rimraf 来完成跨平台文件的删除;

5.2 本地测试组件库

5.2.1 添加入口文件

在进行本地组件库测试之前,还需添加组件库的入口文件。配置 package.json 文件,添加如下字段:

"main": "dist/index.js","module": "dist/index.js","types": "dist/index.d.ts",复制代码

其中:

  • main 字段:定义了 npm 包的入口文件;
  • module 字段:定义了 npm 包的 ES6 模块规范的入口文件;

注: 此处使用 main 字段和 module 字段,相当于在一个包内同时发布了两种模块规范的版本。当打包工具遇到我们的模块时:

  1. 如果它已经支持 pkg.module 字段则会优先使用 ES6 模块规范的版本,这样可以启用 Tree Shaking 机制;
  2. 如果它还不识别 pkg.module 字段则会使用我们已经编译成 CommonJS 规范的版本,也不会阻碍打包流程。

5.2.2 使用 npm link 测试本地组件库

在组件库目录下,运行 npm link 命令,即创建软链接到全局的 node_modules 下。

/Users/xxx/.nvm/versions/node/v12.14.0/lib/node_modules/ii-admin-base -> /Users/xxx/Job/ii-admin-base

在项目外层,创建一个测试目录 test-ii-admin-base,然后在该目录下运行 npm link ii-admin-base 命令,将测试目录的组件库 ii-admin-base 链接到全局:

➜ test-ii-admin-base npm link ii-admin-base /Users/xxx/Job/test-ii-admin-base/node_modules/ii-admin-base -> /Users/xxx/.nvm/versions/node/v12.14.0/lib/node_modules/ii-admin-base -> /Users/xxx/Job/ii-admin-base

然后修改测试目录 test-ii-admin-base 的 package.json文件,手动添加依赖:

 "dependencies": {    ...,    "ii-admin-base": "0.1.0"复制代码

这样就可以在测试目录中引用组件库 ii-admin-base。

import { InputVerfiy } from 'ii-admin-base'import 'ii-admin-base/dist/index.css'import 'antd/dist/antd.css'复制代码

注: 如果在测试的过程中,报如下错误:

这是因为我们开发组件库时使用了一个React版本,测试目录又使用了一个React版本,当一个项目中如果出现多个 React 版本就会报上述错误。针对这种情况,只需要在组件库目录下运行如下命令: npm link ../test-ii-admin-base/node_modules/react , 即将组件库的 react 版本链接到测试组件目录下,然后重新运行项目即可。

六、发布至 NPM

6.1 登录 NPM 账号

先切换官方镜像源。

npm config set registry registry.npmjs.org/

检测当前账号是否登录:

npm whoami

如果未登录,则使用 npm adduser 进行账号登录。

6.2 发布至 NPM

6.2.1 添加描述信息

在发布到 NPM 之前,还需配置 package.json 文件,添加一些必要的描述信息:

{  "name": "ii-admin-base",  "version": "0.1.0",  "private": false,  "description": "A library of react components, which mainly stores components that can be reused by all business lines of AI-Indeeded Company.",  "author": "ShiMu",  "license": "MIT",  "keywords": [    "React",    "Component"  ],  "homepage": "https://lagrangelabs.github.io/ii-admin-base",  "repository": {    "type": "git",    "url": "https://github.com/LagrangeLabs/ii-admin-base.git"  },  "files": [    "build"  ],  "scripts": {    "start": "react-scripts start",    "build": "react-scripts build",    "test": "react-scripts test",    "eject": "react-scripts eject",    "storybook": "start-storybook -p 9009 -s public",    "build-storybook": "build-storybook -s public",    "prepublishOnly": "npm run build"  },  ...}复制代码

其中:

  • 将 private 字段置为 false , 表示非私有包;
  • 添加 description 、 author 、 license 、 keywords 等相关字段;
  • 添加 homepage 字段,即项目主页URL;
  • 添加 repository 字段,即项目仓库地址URL;
  • 添加 files 字段,表示要将哪些文件上传到 npm 上去。如果什么都不写,则默认会使用 .gitignore 里面的信息。但要注意,不管 .gitignore 怎么配置,一些文件会始终发布到 package 上,这些文件包括 package.json 、 README.md 、 LICENSE 等等;
  • 添加 prepublishOnly 钩子函数,在该钩子函数中运行 npm run build ,用来确保 NPM 包发布之前采用的是最新编译的代码;

6.2.2 配置 peerDependencies 字段

此次搭建的组件库是在 React 基础上对 Ant Design 进行的二次封装。为了减少组件库体积,通常不会将React、Ant Design 等第三方依赖打包进去,其次若打包进去,可能还会造成各个版本之间的冲突。

针对这种情况,可以提示用户如果要想使用当前组件库,还需安装以下核心依赖,如 react 、 Ant Design 等,这个时候就需利用 package.json 中的 peerDependencies 字段。当使用 npm install 安装依赖时, peerDependencies 声明的依赖不会被自动安装,而是通过输出 warining 日志告诉用户需安装以下依赖。

"peerDependencies": {    "react": ">= 16.8.0",    "react-dom": ">= 16.8.0",    "antd": ">= 4.3.5" },复制代码
  • react 版本需大于等于 v16.8.0,因为在 v16.8.0 以上版本才引入了 React Hooks;

6.2.3 代码规范检查和单元测试检查

对于一个组件库来说,代码质量是非常重要的。为了防止不符合团队规范的代码或未通过单元测试的代码被commit 亦或者被 publish,需要使用一些钩子函数来验证开发者是否通过代码规范检查和单元测试检查。

6.2.3.1 添加代码规范检查

在 package.json文件中,添加 lint 脚本,针对 src 目录下的文件进行 eslint 检查。

 "lint": "eslint --ext js,ts,tsx, src --max-warnings 5",复制代码
  • --max-warnings 5 : 表示最大允许的 warnings 警告是 5;

6.2.3.2 添加单元测试检查

在使用 CRA 创建项目时,默认会创建 test 脚本,但该脚本是用于开发环境,执行完后不会返回执行结果(即不会返回执行通过还是未通过),而是一直处于 watch 模式下。针对这一情况,可以设置环境变量 CI=true ,即可返回测试运行结果。

 "test:nowatch": "cross-env CI=true npm run test"复制代码
  • 在不同的操作系统环境下,设置环境变量方式不一样。故需借助 cross-env 依赖包完成跨平台的环境变量设置。

6.2.3.3 commit 或 publish 前的流程检查

针对 commit 代码,安装 husky 依赖,在代码提交前先进行单元测试检查和代码规范检测,如下所示。在发布NPM 包之前也进行同样配置。

"scripts": {    ...,    "prepublishOnly": "npm run test:nowatch && npm run lint && npm run build"  },  "husky": {    "hooks": {      "pre-commit": "npm run test:nowatch && npm run lint"    }  },复制代码

完成上述配置后,运行命令 npm publish 即可完成 NPM 包的发布。

七、配置持续集成环境

通常从各个业务线上抽离的基础组件库,会是各个业务线的团队成员一起来维护,这个时候就可以利用 Github 提供的 Github Organization 来共同维护这个组件库。

在这种情况下,使用 Travis CI 进行持续集成就没有那么简单了。之前写过一篇文章如何使用Travis CI对Github Organization下的代码进行持续集成,可以参照该文章完成基础组件库的持续集成环境配置。

八、持续发布

Travis CI 还可以自动将组件库发布到 NPM 上,具体设置如下:

  1. 进入 npm 个人账号中心,生成一个新的Token(权限选择 Read and Publish),用于Travis CI 进行 npm 包的发布。
  1. 在组件库目录下,运行 travis setup npm --force 命令,注意该命令会改写之前的 travis.yml 文件。此时会提示输入 NPM api key,如下所示,将刚才生成的Token值复制粘贴此处即可。

NPM api key: ************************************

改写后的 travis.yml 文件:

language: node_jsnode_js:  - stablecache:  directories:    - node_modulesenv:  matrix:    - CI=truescript:  - npm run build-storybookdeploy:  # 发布到 gh-pages 上  - provider: script    skip_cleanup: true    script: bash scripts/deploy.sh    on:      branch: master  # 发布到 npm 上  - provider: npm    skip_cleanup: true    email: xxxx@qq.com    api_key:      secure: Lsb1/coESXgnDgcotaObyV7QKDVeZJWpAcduyZt/bxAqspN/EdOR2duraPpBHKzme7tOHT4ybIAQusJqSl36K/WX2WFXqhKHw+FoFOobK1aa/azQDkpwllgdxrlx0fCbLpxBPDdKxbJspXwphSgCi2rjY8F/PBdy4+g8IEh/FJQckuFHAEhpTuk+SZPJT5eAqhctxXSaNKB712x4vX9AJLHRT791nB388dsjKOz2NWGNJ14arxukvnb/Yt02hHWKpGQPgQQY9QjfENYnspFYBXYssKV2nhC+0EFoXNn6UK3C4gXo96hV2yqFbP0AhZdHiYxOJ/v1KN7xt+I3popw+puETFyno4TgepGqU/EvkB5r3DnB9CrYsOpeN4+wZtfVtwxMxxxJ8q/EbC7RH45b39056B0i7PnJViIHLWps3XxFQ/bi1CgWdiFyzNofwCYVV6uT0UNR0XZDqUzre10GBrvDogMNWPKMaTmJCWVA8c6AkB4XjfU/jY1xaWxbNuD+Z+p3uLSTKm+c2xrUJFl5KW4/ocyS8No/J+e/9uNkXYcTEdkwnBioWfT7OaBrIpzrkKL9RftkDzjkeUo8h9/XpXNHEUGMK6ZDO0n3zlQ8/qcMHJvS5dXbKmvwZ9GNnOS1EvR1X32MlTfcW0EzDgCXufyAK6UdUGm7jm+dfJJkD60g=    on:      branch: master      repo: LagrangeLabs/ii-admin-base复制代码

注: Travis CI 在对组件库进行持续发布的时候,如果报如下问题:

sh: 1: cross-env: not foundnpm ERR! code ELIFECYCLEnpm ERR! syscall spawnnpm ERR! file sh复制代码

针对这一问题,将 package.json 文件中的 cross-env 换成 ./node_mdoules/.bin/cross-env 即可。

 "test:nowatch": "./node_modules/.bin/cross-env CI=true npm run test"

作者:辻目
原文链接:https://juejin.im/post/5ef7328cf265da22a8513da2

引入antd组件样式_扩大团队技术影响力,搭建团队自己的 UI 组件库相关推荐

  1. 组件是全局怎么修改样式_用一个落地项目,帮你掌握Sketch组件的制作思路

    相信大家对于sketch组件并不陌生,作为sketch官方推出的功能,相对比较久了,不知大家是否还停留在只会建立单个组件,不会组合使用的情况.我最近在项目中也在做这个组件的事情,接下来会以落地项目.实 ...

  2. react 组件样式_如何使用样式化组件为React组件创建视觉变体

    react 组件样式 by Gilad Dayagi 通过吉拉德·达亚吉 如何使用样式化组件为React组件创建视觉变体 (How to create visual variants for Reac ...

  3. spring aop组件_安全性中的Spring AOP –通过方面控制UI组件的创建

    spring aop组件 以下文章将显示在我参与的一个项目中,我们如何使用Spring的AOP来介绍一些与安全性相关的功能. 这样的概念是,为了使用户能够看到某些UI组件,他需要具有一定级别的安全特权 ...

  4. Vue2基础、组件化编程、脚手架、Vuex、Vue路由、UI组件库

    尚硅谷张天禹老师讲课 学习视频 1.Vue简介 Vue2中文官网 1.1 Vue 介绍 一套用于构建用户界面的渐进式JavaScript框架 构建用户界面:把数据通过某种办法变成用户界面 渐进式:可以 ...

  5. android 界面组件,安卓开发学习周第三篇——Android中的UI组件

    原标题:安卓开发学习周第三篇--Android中的UI组件 在Android APP中,所有的用户界面元素都是由View和ViewGroup的对象构成的.View是绘制在屏幕上的用户能与之交互的一个对 ...

  6. 引入antd组件样式_个人作品:一个技术栈koa2+ mysql+react + antd的个人博客

    前言 此项目是个人博客,有前端界面+后台管理系统:目的是当做react和node的练手项目,同时还可以了解到服务器nginx部署web站点以及备案和域名的基本操作流程. 项目预览地址 https:// ...

  7. 引入antd组件样式_如何使用 dumi 和 fatherbuild 创建组件库

    在这个文章中,将会简单的介绍如何使用 dumi 和 father-build 来创建组件库和维护组件库.最终我们会演示如何组织组件库的代码.还会输出一个简单的组件库. 脚手架初始化 新建一个空文件夹, ...

  8. 项目-团队-技术-个人 (团队建设篇)

    可能是工作的时间长了,加上自己也是个有点心的人,最近一年开始思考一些技术周边的事情. 团队建设. 团队如何高效. 如何提高团队成员的水平. 如何让团队保持进取心,保持积极的工作态度,保持他们对于技术的 ...

  9. 前端md转html添加样式_前端文档站点搭建方案

    ? 这是第 46篇不掺水的原创,想要了解更多,请戳上方蓝色字体:政采云前端团队关注我们吧- 本文首发于政采云前端团队博客:前端文档站点搭建方案 https://www.zoo.team/article ...

最新文章

  1. 翻译机之后,搜狗再推智能硬件产品录音笔
  2. Squid access.log 转发到其他syslog服务器(OSSIM)
  3. Python 技术篇-python生成html源码功能实现演示,html代码自动生成技巧,列表生成式的灵活应用
  4. matlab光学远轴光的折射,光学课后习题
  5. java ssh 下载excel,SSH整合WEB导出EXCEL案例
  6. vue slot scope使用_20、slot插槽的用法
  7. 第5章 IDA Pro
  8. 阿里云宣布3年再投2000亿
  9. 克服Dropout缺陷,简单又有效的正则方法:R-Drop
  10. vim格式化代码实际上就是 缩进代码, 命令是等号=
  11. 【转】vue中如何实现数据的双向绑定
  12. 使用tensorflow神经网络预测房价模型
  13. 25匹赛马得前三名问题求解
  14. win10部署milvus以图搜图2.0
  15. 魔方教程--很多教程都不对,整理并实践了一下
  16. 《九日集训》(第一讲)函数
  17. 查互联网ip(公网ip)
  18. html基础-几种布局
  19. MySQL 8.0.12 微妙和毫秒的支持
  20. IDC具体是干什么的?

热门文章

  1. Discuz!NT发帖回复后没有积分动画的去除和修复方法
  2. mpls企业组网怎么样?
  3. 软体定义网路(SDN)的多重意义
  4. BGP MPLS中MCE技术介绍
  5. Git清除本地账号密码/保存账号密码
  6. 在脚本中调用psql如何自动输入密码
  7. RedHat5 开启telnet,ftp等服务
  8. java.lang.NoClassDefFoundError: com/mchange/v2/ser/Indirector解决方法
  9. 团队项目第二阶段冲刺站立会议01
  10. 65条最常用正则表达式