有时候并不希望使用第三方组件库,以及大部分时候需要自己定制一套组件实现,以供公司或项目内部使用。开发自己的组件有如重复造轮子,会花费相当多的精力制造。如果是为了一种长远公司发展,这份努力是值得的,如果不想自己耗费精力,也有许多开源的组件实现,比如material-ui 。一般都是拿来改造一下,可以避免很多开发或兼容性上的坑。
Architecture
编写这篇文章时,笔者用的都是最新的包以及依赖,坑比较多,比如webpack4 ,改掉以及删除很多功能;又如babel7 弃用了旧的写法,全部带上了scoped packages
,即在presets前面带上模块标志:
1 2 3 4 module .exports = { presets : ["@babel/env" ], plugins : ["@babel/transform-arrow-functions" ], };
主要关键技术:
webpack
typescript
react
state management(redux or mobx)
react-router-4
component hot reload
svg icon
jest
UI lib
less
eslint
主要库以及版本:
webpack 4.x.x
typescript 2.9.x
react 16.7.x
react-router-dom 4.2.x
react-hot-loader 4.6.x
…
node 10.13.x
注意:react-router v4为第四代react-router,react-dom为v2或v3的旧版本。新版本的react-router v4可以说是重写了路由,react-router v4 被一分为三:react-router-dom
(for web)、react-router-native
(for native)、react-router
(core)。仅在浏览器中使用的话,一般引入react-router-dom
即可。如果从旧版本迁移,可以参考这里 。
目录结构:
1 2 3 4 5 6 7 8 9 10 11 --.storybook --public 模板目录 --src --components 自定义组件 --docs 代码入口<App/> --stories 文档说明 eslintrc.yml tsconfig.json .babelrc package.json 依赖包 webpack.config.js
Babel
Babel用于将浏览器不能识别的ES以及TS转换为JS,首先添加依赖:
1 yarn add --dev @babel/core @babel/cli @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-object-rest-spread @babel/plugin-transform-runtime
然后在根目录添加.babelrc,
1 2 3 4 5 6 7 8 9 10 11 { "presets": [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript" ], "plugins": [ "@babel/plugin-proposal-object-rest-spread", "@babel/plugin-transform-runtime" ] }
Unit Test
添加单元测试,单测默认识别目录__tests__
,例如
1 2 3 4 5 6 7 --src --components --Link --__tests__ Link.react.test.js Link.react.js index.js
首先,使用TypeScript编写一个组件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import React from 'react' ;import PropTypes from 'prop-types' ;const STATUS = { HOVERED : 'hovered' , NORMAL : 'normal' , }; export class MLink extends React.Component { constructor (props ) { super (props); this ._onMouseEnter = this ._onMouseEnter .bind (this ); this ._onMouseLeave = this ._onMouseLeave .bind (this ); this .state = { class : STATUS .NORMAL , }; } _onMouseEnter ( ) { this .setState ({ class : STATUS .HOVERED }); } _onMouseLeave ( ) { this .setState ({ class : STATUS .NORMAL }); } render ( ) { return ( <a className ={this.state.class} href ={this.props.page || '#'} onMouseEnter ={this._onMouseEnter} onMouseLeave ={this._onMouseLeave} > {this.props.children} </a > ); } } MLink .propTypes = { children : PropTypes .node .isRequired , page : PropTypes .node , };
对应其__tests__
目录添加单元测试,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import React from 'react' ;import { MLink } from '../Link.react' ;import renderer from 'react-test-renderer' ;test ('MLink changes the class when hovered' , () => { const component = renderer.create ( <MLink page ="http://www.facebook.com" > Facebook</MLink > , ); let tree = component.toJSON (); expect (tree).toMatchSnapshot (); tree.props .onMouseEnter (); tree = component.toJSON (); expect (tree).toMatchSnapshot (); tree.props .onMouseLeave (); tree = component.toJSON (); expect (tree).toMatchSnapshot (); });
把单元测试所需的配置补充上,添加单元测试所需要的依赖
1 yarn add --dev jest @types/jest babel-jest ts-jest
另外还需要一个Enzyme,它是React的测试工具,还需要enzyme-to-json 转换,
1 yarn add --dev jest react-test-renderer enzyme enzyme-adapter-react-16 enzyme-to-json
更新一下package.json,加上对应的脚本
1 2 3 4 5 6 7 8 9 "scripts" : { "test" : "jest" , "test:watch" : "jest --watch" , "test:coverage" : "jest --coverage" } , "jest" : { "setupFiles" : [ "./test/jestsetup.js" ] , "snapshotSerializers" : [ "enzyme-to-json/serializer" ] }
创建一个test/jestsetup.js文件,自定义测试环境
1 2 3 4 5 6 7 8 import Enzyme , { shallow, render, mount } from 'enzyme' ;import Adapter from 'enzyme-adapter-react-16' ;Enzyme .configure ({ adapter : new Adapter () });global .shallow = shallow;global .render = render;global .mount = mount;
对于CSS模块,添加以下配置到package.json
1 2 3 4 5 "jest" : { "moduleNameMapper" : { "^.+\\.(css|scss)$" : "identity-obj-proxy" } }
运行命令进行测试
1 jest --env=jsdom --coverage --no-cache --detectOpenHandles
jest可以测试的特性有:
component render
props
events
event handlers
…
更多例子可以参考这里 。
ESLint
首先加入依赖:
1 yarn add --dev eslint eslint-config-airbnb-base eslint-loader eslint-plugin-import eslint-plugin-react typescript-eslint-parser
ESLint用于代码检查用,配置也比较无脑,当你用像WebStorm这些开发工具时,工具会自动检测到ESLint,自动为你格式化和检查代码。下面是我自己的一份配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 env: browser: true commonjs: true es6: true jest: true ecmaFeatures: modules: true spread: true restParams: true extends: - 'eslint:recommended' - 'plugin:react/recommended' parser: 'babel-eslint' parserOptions: ecmaFeatures: jsx: true ecmaVersion: 2018 sourceType: module plugins: - react rules: indent: - error - 2 linebreak-style: - error - windows quotes: - error - single semi: - error comma-dangle: off no-unused-vars: warn no-console: error no-unexpected-multiline: warn import/prefer-default-export: off settings: react: pragma: React version: detect
像VSCode,可以在首选项 -> 设置 中找到eslint.validate,加入typescript与typescriptreact,分别用于监听ts与tsx文件,如下
1 2 3 4 5 6 "eslint.validate" : [ "javascript" , "javascriptreact" , "typescript" , "typescriptreact" ]
更多配置可以自己定制。
Storybook
storybook的主要作用是为自己实现的组件编写文档,可以在这里 查看更多细节。
安装storybook:
1 yarn add --dev @storybook/react
加入package.json的script
1 2 3 4 5 { "scripts" : { "storybook" : "start-storybook -p 9001 -c .storybook" } }
默认storybook会在.storybook文件夹作为配置,配置文件为config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 import { configure } from '@storybook/react' ;import React from 'react' ;const req = require .context ('../src/stories' , true , /\.stories\.js$/ );function loadStories ( ) { req.keys ().forEach ((filename ) => req (filename)); } configure (loadStories, module );
上面配置指定了创建stories的位置,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // .storybook/config.js中修改路径 import { configure } from '@storybook/react'; function loadStories() { require('../src/stories/button.stories.js'); } configure(loadStories, module); // src/stories/button.stories.js import React from 'react'; import { storiesOf } from '@storybook/react'; import {Button} from '../components/Button'; storiesOf('Button', module) .add('基本用法',() => ( <Button>按钮</Button> )) // src/components/Button.js import React from 'react' export class Button extends React.Component{ constructor (props) { super(props) } render () { return ( <button style={{backgroundColor: '#fff', border: '1px solid #ccc'}}>{this.props.children}</button> ) } }
启动stroybook服务
默认会自动打开浏览器
出现以上页面说明配置已经成功了。
Live Editing
默认地,webpack的development mode在每次更新代码时,会自动刷新页面。但刷新不同于Live Edit,我不希望编写一个CSS还要重新refresh一下,页面直接变化就最好了!有很多工具可以实现这种方式,这里仅介绍react-hot-loader:
1 yarn add --dev react-hot-loader
在script里面加入一个--hot
选项即可。
1 2 3 "scripts": { "start": "./node_modules/.bin/webpack-dev-server --hot --mode development --progress --colors --config ./webpack.config.js", },
出现以下效果,说明Live Editing功能实现了。
Summary
由于使用的东西比较多,而且开发的方式也比较灵活,具体技术细节可以具体深入理解。
完整代码,可以参考我的github