手摸手教你用 create-react-app 创建支持 Typescript 的 React 组件库

最近打算自己用 React 来实现 Bootstrap 的组件库,由于之前的 React 项目一直都在基于 Create-react-app(CRA) 作为脚手架,因此在想是否也可用 CRA 作为编写 React 组件库的启动脚手架,在网上搜了一圈并亲自实践后发现还是比较容易实现我们的需求的。话不多说,下面直接进入正题

创建项目

首先我们先来创建项目,由于是基于 CRA 来创建,因此我们可以直接通过 npx (npm >= 5.2.0)和 create-react-app 直接初始化项目,这里暂定项目名称为 react-bootstrap-components-lib

$ npx create-react-app react-bootstrap-components-lib --scripts-version=react-scripts-ts

--scripts-version=react-scripts-ts 是什么鬼?

TypeSccript-React-Starter 是微软创建的用于快速初始化支持 TypeScript 的脚手架,react-scripts-ts 是帮助实现引入 TypeScript的工具

注意:创建好项目后,先执行 eject 命令将 CRA 封装起来的内容弹出来,因为下面我们添加 Enzyme 的时候需要修改 package.json 文件中的 Jest 配置

$ yarn eject

先完善一下 package.json 中的信息,如下所示

{
  "name": "react-bootstrap-components-lib",
  "version": "0.1.0",
  "description": "The popular UI framework based on React.",
  "author": "Kevin Lee",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/RunningCoderLee/react-bootstrap-components-lib.git"
  },
  "bugs": {
    "url": "https://github.com/RunningCoderLee/react-bootstrap-components-lib/issues"
  },
  "homepage": "https://github.com/RunningCoderLee/react-bootstrap-components-lib#readme",
  "private": true,
  "dependencies": {
    "react": "^16.4.1",
    "react-dom": "^16.4.1",
    "react-scripts-ts": "2.16.0"
  },
  "scripts": {
    "start": "node start",
    "build": "node build",
    "test": "node test --env=jsdom"
  },
  "devDependencies": {
    "@types/jest": "^23.3.0",
    "@types/node": "^10.5.2",
    "@types/react": "^16.4.6",
    "@types/react-dom": "^16.0.6",
    "typescript": "^2.9.2"
  }
  ...
}

添加测试

CRA 里默认已整合了对 Jest 测试框架的支持,为了更好的对 React 组件 进行测试,我们只需添加对 Enzyme 的支持即可。

安装 Enzyme

首先安装 Enzyme 和针对 react(v16.x.x) 的适配器

$ yarn add -D enzyme enzyme-adapter-react-16 @types/enzyme @types/enzyme-adapter-react-16

添加配置文件

src 文件夹下创建 setupTests.ts 文件,在文件中添加如下内容:

// Temporary hack to suppress error
// https://github.com/facebookincubator/create-react-app/issues/3199

window.requestAnimationFrame = (callback) => {
  setTimeout(callback, 0);
  return 0;
};

import * as Enzyme from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });

这个全局配置文件负责初始化全局测试环境并特别指明使用针对 React v16 版本的适配器

更改 Jest 配置

打开 package.json 文件,找到 Jest 对应的配置项,添加下列配置

{
  "jest": {
    "setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts",
  }
}

验证是否成功

在完成配置后,我们可以来验证下是否成功

打开 App.test.tsx 文件,替换如下代码

import { shallow } from 'enzyme';
import * as React from 'react';
import App from './App';

describe('Test <App />', () => {
  it('should render without throwing an error', () => {
      expect(shallow(<App />).find('.App')).toHaveLength(1);
  });
});

执行如下命令

$ yarn test

样式

由于 bootstrap v4 版本采用 Sass,所以这里也以引入 Sass 为例

根据 CRA 官方文档我们先安装所要用到的工具包

# 安装 Sass 命令行工具 node-sass-chokidar
# https://github.com/michaelwayman/node-sass-chokidar
$ yarn add -D node-sass-chokidar

# 安装跨平台运行多个 npm-scripts 的工具 npm-run-all
# https://github.com/mysticatea/npm-run-all
$ yarn add -D npm-run-all

# 安装帮助复制文件的工具 cpx
$ yarn add -D cpx

安装成功后,我们去 package.json 中添加一些脚本命令:

"scripts": {
  "start-js": "node start",
  "start": "npm-run-all -p watch-sass-to-css start-js",
  "build-js": "node build",
  "build": "npm-run-all -p build-sass-to-css build-js",
  "test": "node test --env=jsdom",
  "copy-css-to-lib": "npm-run-all -s copy-base-css copy-component-css",
  "copy-base-css": "cpx \"./src/**/*.css\" ./build/lib/",
  "copy-component-css": "cpx \"./src/components/**/*.css\" ./build/lib/components",
  "build-sass-to-css": "node-sass-chokidar src/ -o src/",
  "watch-sass-to-css": "node-sass-chokidar src/ -o src/ --watch --recursive"
}

添加完毕后,我们把样式文件的扩展名从 .css 变为 .scss

一切准备就绪后,我们来测试下运转是否正常

$ yarn start

执行命令后如果一切运转正常,则会在浏览器中看到CRA脚手架的展示页面

组件

现在我们开始开发我们的组件,在开发之前先统一一下文件夹结构,以 Button 组件为例

src/
  ┣━ components/
  ┃       ┣━ Button
  ┃       ┃    ┣━ Button.tsx
  ┃       ┃    ┣━ Button.scss
  ┃       ┃    ┣━ Button.scss
  ┃       ┃    ┗━ README.md
  ┃       ┣━ ...
  ┃       ┣━ index.scss
  ┃       ┗━ index.tsx
  ┗━ ...

接着开始完善各个文件中的内容

index.tsx

这个文件是引用所有组件并统一导出的索引文件,另外这里还需引入由 index.scss 编译后的 index.css 文件

import './index.css';

export {default as Button} from './Button';

index.scss

这个文件用于添加我们所需的所有样式文件,因为我是要用 React 来实现 Bootstrap,因此这里首先需要引入 Bootstrap 的样式文件

先通过 yarnnpm 安装 Bootstrap

$ yarn add -D bootstrap

安装成功后后在此文件中引入 Bootstrap 的样式文件

// import the default bootstrap scss from node modules
@import 'node_modules/bootstrap/scss/bootstrap';

Button.tsx

按钮组件的实现

import * as classNames from 'classnames';
import * as React from 'react';

enum Type {
  Primary = "primary",
  Secondary = "secondary",
  Succcess = "success",
  Danger = "danger",
  Warning = "warning",
  Info = "info",
  Light = "light",
  Dark = "dark",
  Link = "link",
}

export interface IProps {
  type?: Type
  className?: string
  children?: JSX.Element[] | JSX.Element
}

export interface IState {
  disabled: boolean
}

class Button extends React.Component<IProps, IState> {

  public static defaultProps: IProps = {
    type: Type.Primary
  }

  constructor(props: IProps) {
    super(props);
    this.state = {
      disabled: false,
    }
  }

  public render() {
    const {type, children, className, ...props} = this.props;
    const {disabled} = this.state;
    const clsName = classNames('btn', {
      [`btn-${type}`]: true,
    }, className)

    return (
      <button {...props} disabled={disabled} className={clsName}>{children}</button>
    );
  }
}

export default Button;  

index.ts

这个文件用于引用并导出 Button 组件,当然我们可以直接在 Button.tsx 文件中直接导出,这样做的目的是方便日后扩展,比如增添 ButtonGroup 组件

export {default} from './Button';

README.md

这里的说明文档一会将和 Styleguidist 一起搭配使用

Button example:

~~~js
  <Button type="primary">Primary</Button>
  <Button type="secondary">Secondary</Button>
  <Button type="success">Success</Button>
  <Button type="danger">Danger</Button>
  <Button type="warning">Warning</Button>
  <Button type="info">Info</Button>
  <Button type="light">Light</Button>
  <Button type="dark">Dark</Button>
  <Button type="link">Link</Button>
~~~ 

Styleguide

Styleguidist 是一款可以帮助我们生成组件展示页面的工具

安装

$ yarn add -D react-styleguidist

安装成功后,我们在 package.json 里添加下面的几个脚本命令

{
  ...
  "scripts": {
    "styleguide": "npm-run-all -p watch-sass-to-css styleguidist",
    "styleguidist": "styleguidist server",
    "styleguide-build": "styleguidist build"
  }
  ...
}

最后再安装一个用来解析 typescript 中定义属性的工具 react-docgen-typescript

$ yarn add -D react-docgen-typescript

配置

在项目根目录中创建 styleguide.config.js 文件,内容如下:

module.exports = {
  components: 'src/components/**/*.{ts,tsx}',
  ignore: [
    'src/setupTests.ts',
    '**/*.spec.ts',
    '**/*.spec.tsx',
    '**/*.test.ts',
    '**/*.test.tsx',
    '**/*.d.ts'
  ],
  propsParser: require('react-docgen-typescript').parse,
  webpackConfig: require('./config/webpack.config.dev.js'),
}

一切准备就绪后,我们可以通过执行下面的命令来打开 styleguide 页面

$ yarn styleguide

部署

首先继续完善 package.json 文件,如下:

{
  "name": "react-bootstrap-components-lib",
  "version": "0.1.0",
  "description": "The popular UI framework based on React.",
  "author": "Kevin Lee",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/RunningCoderLee/react-bootstrap-components-lib.git"
  },
  "files": [
    "build/lib"
  ],
  "bugs": {
    "url": "https://github.com/RunningCoderLee/react-bootstrap-components-lib/issues"
  },
  "homepage": "https://github.com/RunningCoderLee/react-bootstrap-components-lib#readme",
  "private": false,
  "dependencies": {
    "classnames": "^2.2.6"
  },
  "peerDependencies": {
    "react": ">=16.0.0",
    "react-dom": ">=16.0.0"
  },
  "scripts": {
    "start": "npm-run-all -p watch-sass-to-css start-js",
    "start-js": "node scripts/start.js",
    "build": "npm-run-all -p build-sass-to-css build-js",
    "build-lib": "tsc && npm run build-sass-to-css && npm run copy-css-to-lib",
    "build-js": "node scripts/build.js",
    "test": "node scripts/test.js --env=jsdom",
    "copy-css-to-lib": "npm-run-all -s copy-base-css copy-component-css",
    "copy-base-css": "cpx \"./src/**/*.css\" ./build/lib/",
    "copy-component-css": "cpx \"./src/components/**/*.css\" ./build/lib/components",
    "build-sass-to-css": "node-sass-chokidar src/ -o src/",
    "watch-sass-to-css": "node-sass-chokidar src/ -o src/ --watch --recursive",
    "styleguide": "npm-run-all -p  watch-sass-to-css styleguidist",
    "styleguidist": "styleguidist server",
    "styleguide-build": "styleguidist build"
  },
  "devDependencies": {
    "@types/classnames": "^2.2.5",
    "@types/enzyme": "^3.1.12",
    "@types/enzyme-adapter-react-16": "^1.0.2",
    "@types/jest": "^23.3.0",
    "@types/node": "^10.5.2",
    "@types/react": "^16.4.6",
    "@types/react-dom": "^16.0.6",
    "autoprefixer": "7.1.6",
    "babel-jest": "^22.1.0",
    "babel-loader": "^7.1.2",
    "babel-preset-react-app": "^3.1.1",
    "bootstrap": "^4.1.2",
    "case-sensitive-paths-webpack-plugin": "2.1.1",
    "chalk": "1.1.3",
    "css-loader": "0.28.7",
    "cpx": "^1.5.0",
    "dotenv": "4.0.0",
    "dotenv-expand": "4.2.0",
    "enzyme": "^3.3.0",
    "enzyme-adapter-react-16": "^1.1.1",
    "extract-text-webpack-plugin": "3.0.2",
    "file-loader": "0.11.2",
    "fork-ts-checker-webpack-plugin": "^0.2.8",
    "fs-extra": "3.0.1",
    "html-webpack-plugin": "2.29.0",
    "jest": "22.4.2",
    "node-sass-chokidar": "^1.3.0",
    "npm-run-all": "^4.1.3",
    "object-assign": "4.1.1",
    "postcss-flexbugs-fixes": "3.2.0",
    "postcss-loader": "2.0.8",
    "promise": "8.0.1",
    "raf": "3.4.0",
    "react": "^16.4.1",
    "react-dom": "^16.4.1",
    "react-dev-utils": "^5.0.1",
    "react-docgen-typescript": "^1.6.2",
    "react-styleguidist": "^7.1.0",
    "resolve": "1.6.0",
    "source-map-loader": "^0.2.1",
    "style-loader": "0.19.0",
    "sw-precache-webpack-plugin": "0.11.4",
    "ts-jest": "22.0.1",
    "ts-loader": "^2.3.7",
    "tsconfig-paths-webpack-plugin": "^2.0.0",
    "tslint": "^5.7.0",
    "tslint-config-prettier": "^1.10.0",
    "tslint-react": "^3.2.0",
    "typescript": "^2.9.2",
    "uglifyjs-webpack-plugin": "^1.1.8",
    "url-loader": "0.6.2",
    "webpack": "3.8.1",
    "webpack-dev-server": "2.9.4",
    "webpack-manifest-plugin": "1.3.2",
    "whatwg-fetch": "2.0.3"
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}"
    ],
    "setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts",
    "setupFiles": [
      "<rootDir>/config/polyfills.js"
    ],
    "testMatch": [
      "<rootDir>/src/**/__tests__/**/*.(j|t)s?(x)",
      "<rootDir>/src/**/?(*.)(spec|test).(j|t)s?(x)"
    ],
    "testEnvironment": "node",
    "testURL": "http://localhost",
    "transform": {
      "^.+\\.(js|jsx|mjs)$": "<rootDir>/node_modules/babel-jest",
      "^.+\\.tsx?$": "<rootDir>/config/jest/typescriptTransform.js",
      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|mjs|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$"
    ],
    "moduleNameMapper": {
      "^react-native$": "react-native-web"
    },
    "moduleFileExtensions": [
      "web.ts",
      "ts",
      "web.tsx",
      "tsx",
      "web.js",
      "js",
      "web.jsx",
      "jsx",
      "json",
      "node",
      "mjs"
    ],
    "globals": {
      "ts-jest": {
        "tsConfigFile": "/Users/kevin/development/github/mine/react-bootstrap-components-lib/tsconfig.test.json"
      }
    }
  },
  "babel": {
    "presets": [
      "react-app"
    ]
  },
  "eslintConfig": {
    "extends": "react-app"
  }
}

接着我们需要更新 tsconfig.json 中的内容

{
  "compilerOptions": {
    "baseUrl": ".",
    "outDir": "build/lib",
    "module": "commonjs",
    "target": "es5",
    "lib": ["es5", "es6", "dom"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDir": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "declaration": true
  },
  "exclude": [
    "node_modules",
    "build",
    "scripts",
    "acceptance-tests",
    "webpack",
    "jest",
    "src/setupTests.ts",
    "**/*.spec.ts",
    "**/*.test.ts",
    "config",
    "dist"
  ]
}

最后,我们开始编译、打包和发布我们的组件库

$ yarn build-lib
$ npm publish
如果此文有帮助到你,你可以选择请我喝杯☕️ ,感谢你对我分享内容的认可😃