Ant Design Pro使用笔记
一、安装 dva-cli
cnpm install dva-cli -g
二、创建应用
dva new dva-project
三、启动应用
cd dva-project
npm start
四、安装并使用antd
cnpm install antd babel-plugin-import --save
编辑 .webpackrc,使 babel-plugin-import 插件生效。
{ "extraBabelPlugins": [ ["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": "css" }] ] }
使用 webstorm 打开 Ant Design Pro 项目 发现如下图所示的报错
ESLint: Expected linebreaks to be 'LF' but found 'CRLF'. (linebreak-style)
解决办法:
在.eslintrc文件下添加"linebreak-style": ["error", "windows"],
问题解决
五、Ant Design Pro项目安装
git clone --depth=1 https://github.com/ant-design/ant-design-pro.git my-project
cnpm install
或者使用
cnpm install ant-design-pro-cli -g
pro new # 安装脚手架
项目目录结构
├── mock # 本地模拟数据 ├── public │ └── favicon.ico # Favicon ├── src │ ├── assets # 本地静态资源 │ ├── common # 应用公用配置,如导航信息 │ ├── components # 业务通用组件 │ ├── e2e # 集成测试用例 │ ├── layouts # 通用布局 │ ├── models # dva model │ ├── routes # 业务页面入口和常用模板 │ ├── services # 后台接口服务 │ ├── utils # 工具库 │ ├── g2.js # 可视化图形配置 │ ├── theme.js # 主题配置 │ ├── index.ejs # HTML 入口模板 │ ├── index.js # 应用入口 │ ├── index.less # 全局样式 │ └── router.js # 路由入口 ├── tests # 测试工具 ├── README.md └── package.json
运行
cnpm start
布局
Ant Design 官网https://ant.design/components/layout-cn/ 提供了两套布局方案:Layout 和 Grid
页面整体布局是一个产品最外层的框架结构,往往会包含导航、页脚、侧边栏、通知栏以及内容等。在页面之中,也有很多区块的布局结构。
栅格组件
栅格布局是网页中最常用的布局,其特点就是按照一定比例划分页面,能够随着屏幕的变化依旧保持比例,从而具有弹性布局的特点。
而 Ant Design 的栅格组件提供的功能更为强大,能够设置间距、具有支持响应式的比例设置,以及支持 flex 模式,基本上涵盖了大部分的布局场景。
https://ant.design/components/grid-cn/
在 12 栅格系统的基础上,将整个设计建议区域按照 24 等分的原则进行划分。
建议横向排列的盒子数量最多四个,最少一个。
栅格组件概述
基于行(row)和列(col)来定义信息区块的外部框架,以保证页面的每个区域能够稳健地排布起来。
-
通过row在水平方向建立一组column(简写col)
-
内容应当放置于col内,并且,只有col可以作为row的直接元素
-
栅格系统中的列是指1到24的值来表示其跨越的范围。例如,三个等宽的列可以使用.col-8来创建
-
col总和超过 24,那么多余的col会作为一个整体另起一行排列
栅格常常需要和间隔进行配合,你可以使用 Row 的 gutter 属性,我们推荐使用 (16+8n)px 作为栅格间隔。(n 是自然数) 如果要支持响应式,可以写成 { xs: 8, sm: 16, md: 24, lg: 32 }。
<Row gutter={16}> <Col className="gutter-row" span={6}> <div className="gutter-box">col-6</div> </Col> <Col className="gutter-row" span={6}> <div className="gutter-box">col-6</div> </Col> <Col className="gutter-row" span={6}> <div className="gutter-box">col-6</div> </Col> <Col className="gutter-row" span={6}> <div className="gutter-box">col-6</div> </Col> </Row>
使用 offset 可以将列向右侧偏。例如,offset={4} 将元素向右侧偏移了 4 个列(column)的宽度。
<Col span={4} offset={4}>col-4 col-offset-4</Col>
通过使用 push 和 pull 类就可以很容易的改变列(column)的顺序。
<Row> <Col span={18} push={6}>col-18 col-push-6</Col> <Col span={6} pull={18}>col-6 col-pull-18</Col> </Row>
Flex 布局
允许子元素在父节点内的水平对齐方式 - 居左、居中、居右、等宽排列、分散排列。子元素与子元素之间,支持顶部对齐、垂直居中对齐、底部对齐的方式。同时,支持使用 order 来定义元素的排列顺序。
使用 row-flex 定义 flex 布局,其子元素根据不同的值 start,center,end,space-between,space-around,分别定义其在父节点里面的排版方式。
<Row type="flex" justify="start|center|end|space-between|space-around">
......
</Row>
Flex 子元素垂直对齐
<Row type="flex" justify="start|center|end|space-between|space-around" align="top|middle|bottom">
......
<Row>
通过 Flex 布局的 Order 来改变元素的排序
<Row type="flex"> <Col span={6} order={4}>1 col-order-4</Col> <Col span={6} order={3}>2 col-order-3</Col> <Col span={6} order={2}>3 col-order-2</Col> <Col span={6} order={1}>4 col-order-1</Col> </Row>
预设五个响应尺寸:xs sm md lg xl xxl
span pull push offset order 属性可以通过内嵌到 xs sm md lg xl xxl 属性中来使用。 其中 xs={6} 相当于 xs={{ span: 6 }}。
<Row> <Col xs={{ span: 5, offset: 1 }} lg={{ span: 6, offset: 2 }}>Col</Col> <Col xs={{ span: 11, offset: 1 }} lg={{ span: 6, offset: 2 }}>Col</Col> <Col xs={{ span: 5, offset: 1 }} lg={{ span: 6, offset: 2 }}>Col</Col> </Row>
六、Ant Design Layout 组件
协助进行页面级整体布局。
尺寸
一级导航项偏左靠近 logo 放置 辅助菜单偏右放置
- 顶部导航(大部分系统):一级导航高度 64px,二级导航 48px。
-
顶部导航(展示类页面):一级导航高度 80px,二级导航 56px。
-
顶部导航高度的范围计算公式为:48+8n。
-
侧边导航宽度的范围计算公式:200+8n。
交互
-
一级导航和末级的导航需要在可视化的层面被强调出来;
-
当前项应该在呈现上优先级最高;
-
当导航收起的时候,当前项的样式自动赋予给它的上一个层级;
-
左侧导航栏的收放交互同时支持手风琴和全展开的样式,根据业务的要求进行适当的选择。
视觉
导航样式上需要根据信息层级合理的选择样式:
-
大色块强调
建议用于底色为深色系时,当前页面父级的导航项。
-
高亮火柴棍
当导航栏底色为浅色系时使用,可用于当前页面对应导航项,建议尽量在导航路径的最终项使用。
-
字体高亮变色
从可视化层面,字体高亮的视觉强化力度低于大色块,通常在当前项的上一级使用。
-
字体放大
12px、14px 是导航的标准字号,14 号字体用在一、二级导航中。字号可以考虑导航项的等级做相应选择。
Layout组件概述
-
Layout:布局容器,其下可嵌套 Header Sider Content Footer 或 Layout 本身,可以放在任何父容器中。
-
Header:顶部布局,自带默认样式,其下可嵌套任何元素,只能放在 Layout 中。
-
Sider:侧边栏,自带默认样式及基本功能,其下可嵌套任何元素,只能放在 Layout 中。
-
Content:内容部分,自带默认样式,其下可嵌套任何元素,只能放在 Layout 中。
-
Footer:底部布局,自带默认样式,其下可嵌套任何元素,只能放在 Layout 中。
<Layout> <Sider>Sider</Sider> <Layout> <Header>Header</Header> <Content>Content</Content> <Footer>Footer</Footer> </Layout> </Layout>
最基本的『上-中-下』布局
<Layout> <Header></Header> <Content></Content> <Footer></Footer> </Layout>
顶部-侧边布局-通栏
同样拥有顶部导航及侧边栏,区别是两边未留边距,多用于应用型的网站。
顶部-侧边布局
拥有顶部导航及侧边栏的页面,多用于展示类网站。
侧边布局
侧边两列式布局。页面横向空间有限时,侧边导航可收起。
左右的结构,内容根据浏览器终端进行自适应,提高横向空间的使用率,但是整个页面排版不稳定。侧边导航的模式层级扩展性强,一、二、三级导航项目可以更为顺畅且具关联性的被展示,同时侧边导航可以固定,使得用户在操作和浏览中可以快速的定位和切换当前位置,有很高的操作效率。但这类导航横向页面内容的空间会被牺牲一部份。
自定义触发器
要使用自定义触发器,可以设置 trigger={null} 来隐藏默认设定。自定义 trigger,设置为 null 时隐藏 trigger。
<Layout> <Sider trigger={null} collapsible collapsed={this.state.collapsed} > ...... </Sider> </Layout>
响应式布局
Layout.Sider 支持响应式布局。
说明:配置 breakpoint 属性即生效,视窗宽度小于 breakpoint 时 Sider 缩小为 collapsedWidth 宽度,若将 collapsedWidth 设置为零,会出现特殊 trigger。
<Layout> <Sider breakpoint="xs|sm|md|lg|xl|xxl" collapsedWidth="0" onCollapse={(collapsed, type) => { console.log(collapsed, type); }} > ...... </Sider> <Layout>
onCollapse:展开-收起时的回调函数,有点击 trigger 以及响应式反馈两种方式可以触发 (collapsed, type) => {}
固定头部
一般用于固定顶部导航,方便页面切换。
固定侧边栏
当内容较长时,使用固定侧边栏可以提供更好的体验。
{
xs: '480px',
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px',
xxl: '1600px',
}
根据不同场景区分抽离布局组件
在大部分场景下,我们需要基于上面两个组件封装一些适用于当下具体业务的组件,包含了通用的导航、侧边栏、顶部通知、页面标题等元素。例如 Ant Design Pro 的 BasicLayout
把抽象出来的布局组件,放到跟 routes、 components 平行的 layouts 文件夹中方便管理。
处理 this.props.children 属性
在抽离的过程中,往往需要处理布局包含的内容组件,而 this.props.children 就代表了标签中的内容,如果你需要对其子元素进行筛选处理,可以使用 React.children.forEach 方法。
React.Children.forEach(children, function[(thisArg)])
但是如果你需要传递额外的 props 给子元素,我们则推荐采用 Context 的方式。
...
const {
currentUser, collapsed, fetchingNotices, notices, routerData, match, location,
} = this.props;
...
<Layout> <SiderMenu logo={logo} // 不带Authorized参数的情况下如果没有权限,会强制跳到403界面 // If you do not have the Authorized parameter // you will be forced to jump to the 403 interface without permission Authorized={Authorized} menuData={getMenuData()} collapsed={collapsed} location={location} isMobile={this.state.isMobile} onCollapse={this.handleMenuCollapse} /> ...... </SiderMenu> <Layout>
handleMenuCollapse = (collapsed) => { this.props.dispatch({ type: 'global/changeLayoutCollapsed', payload: collapsed, }); }
global.js
state: {
collapsed: false,
notices: [],
},
... changeLayoutCollapsed(state, { payload }) { return { ...state, collapsed: payload, }; }, ...
Ant Design Pro 的布局
-
BasicLayout:基础页面布局,包含了头部导航,侧边栏和通知栏:
-
BlankLayout:空白的布局
-
PageHeaderLayout:带有标准 PageHeader 的布局
-
UserLayout:抽离出用于登陆注册页面的通用布局
如何使用 Ant Design Pro 布局
我们为了统一方便的管理路由和页面的关系,将配置信息统一抽离到 src/common/router.js 下,同时应用动态路由,通过如下配置:
const routerConfig = { '/': { component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')), }, '/dashboard/analysis': { component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')), }, '/user': { component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')), }, '/user/login': { component: dynamicWrapper(app, ['login'], () => import('../routes/User/Login')), }, };
映射路由和页面布局(组件)的关系。详细的映射转换实现,参看 router.js。
七、路由和菜单
路由和菜单是组织起一个应用的关键骨架。
如果你想了解更多关于 browserHistory 和 hashHistory,请参看 构建和发布。
dva@2
- dva@2.0:基于 react 和 react-router@4
- dva-react-router-3@1.0:基于 react 和 react-router@3
- dva-no-router@1.0:无路由版本,适用于多页面场景,可以和 next.js 组合使用
- dva-core@1.0:仅封装了 redux 和 redux-saga
还有一些社区基于 dva-core 的实现:
- dva-wxapp,dva 应用于微信小程序
脚手架依赖 dva@2,路由方面是基于 react-router@4 的实现。
基本结构
在这一部分,脚手架通过结合一些配置文件、基本算法及工具函数,搭建好了路由和菜单的基本框架,主要涉及以下几个模块/功能:
-
路由生成 结合路由信息的配置文件和预设的基本算法,提供了在不同层级文件中自动生成路由列表的能力。
-
菜单生成 由确定数据结构的菜单配置文件生成,其中的菜单项名称,嵌套路径与路由有一定关联关系。
-
面包屑 组件 PageHeader 中内置的面包屑也可由脚手架提供的配置信息自动生成。
路由
目前在脚手架中,除了顶层路由(/src/router.js),其余路由列表都是自动生成,其中最关键的就是中心化配置文件 src/common/router.js,它的主要作用有两个:
-
配置路由相关信息。如果只考虑生成路由,你只需要指定每条配置的路径及对应渲染组件。
-
输出路由数据,并将路由数据(routerData)挂载到每条路由对应的组件上。
这样我们得到一个基本的路由信息对象,它的结构大致是这样:
{ '/dashboard/analysis': { component: DynamicComponent(), name: '分析页', }, '/dashboard/monitor': { component: DynamicComponent(), name: '监控页', }, '/dashboard/workplace': { component: DynamicComponent(), name: '工作台', }, }
为了帮助自动生成路由,在 src/utils/utils.js 中提供了工具函数 getRoutes,它接收两个参数:当前路由的 match 路径及路由信息 routerData,主要完成两个工作:
/** * Get router routing configuration * { path:{name,...param}}=>Array<{name,path ...param}> * @param {string} path * @param {routerData} routerData */ export function getRoutes(path, routerData) { let routes = Object.keys(routerData).filter(routePath => routePath.indexOf(path) === 0 && routePath !== path); // Replace path to '' eg. path='user' /user/name => name routes = routes.map(item => item.replace(path, '')); // Get the route to be rendered to remove the deep rendering const renderArr = getRenderArr(routes); // Conversion and stitching parameters const renderRoutes = renderArr.map((item) => { const exact = !routes.some(route => route !== item && getRelation(route, item) === 1); return { ...routerData[`${path}${item}`], key: `${path}${item}`, path: `${path}${item}`, exact, }; }); return renderRoutes; }
筛选路由信息,筛选的算法为只保留当前 match.path 下最邻近的路由层级(更深入的层级留到嵌套路由中自行渲染),举个例子(每条为一个 route path):
// 当前 match.path 为 / /a // 没有更近的层级,保留 /a/b // 存在更近层级 /a,去掉 /c/d // 没有更近的层级,保留 /c/e // 没有更近的层级,保留 /c/e/f // 存在更近层级 /c/e,去掉
自动分析路由 exact 参数,除了下面还有嵌套路由的路径,其余路径默认设为 exact。
经过 getRoutes 处理之后的路由数据就可直接用于生成路由列表:
// src/layouts/BasicLayout.js getRoutes(match.path, routerData).map(item => ( <Route key={item.key} path={item.path} component={item.component} exact={item.exact} /> ))
(如果你不需要自动生成路由,也可以用 routerData 自行处理)
菜单
菜单信息配置在 src/common/menu.js 中,它的作用是:
-
配置菜单相关数据,菜单项的跳转链接为配置项及其所有父级配置 path 参数的拼接。
-
为 src/common/router.js 提供路由名称(name)等数据,根据拼接好的跳转链接来匹配相关路由。
(如果项目不需要菜单 直接在 src/common/router.js 中配置 name 信息)
面包屑
之前提到的路由信息 routerData 可以直接传递给 PageHeader 组件用以生成面包屑,你可以用 props 或者 context 的方式进行传递。脚手架里的示例。
实例
脚手架默认提供了两种布局模板:基础布局 - BasicLayout 以及 账户相关布局 - UserLayout:
只需要在路由及菜单配置中增加一条即可:
// src/common/router.js '/dashboard/test': { component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/Test')), },
// src/common/menu.js const menuData = [{ name: 'dashboard', icon: 'dashboard', // https://demo.com/icon.png or <Icon type="dashboard" /> path: 'dashboard', children: [{ name: '分析页', path: 'analysis', }, { name: '监控页', path: 'monitor', }, { name: '工作台', path: 'workplace', }, { name: '测试页', path: 'test', }], }, { // 更多配置 }];
新增布局模板
新建 Layout 模板
// src/common/router.js '/new': { component: dynamicWrapper(app, ['monitor'], () => import('../layouts/NewLayout')), }, '/new/page1': { component: dynamicWrapper(app, ['monitor'], () => import('../routes/New/Page1')), }, '/new/page2': { component: dynamicWrapper(app, ['monitor'], () => import('../routes/New/Page2')), },
// src/common/menu.js const menuData = [{ name: '新布局', icon: 'table', path: 'new', children: [{ name: '页面一', path: 'page1', }, { name: '页面二', path: 'page2', }], }, { // 更多配置 }];
在根路由中增加这组新模板:
// src/router.js <Router history={history}> <Switch> <Route path="/new" render={props => <NewLayout {...props} />} /> <Route path="/user" render={props => <UserLayout {...props} />} /> <Route path="/" render={props => <BasicLayout {...props} />} /> </Switch> </Router>
带参数的路由/菜单
脚手架默认支持带参数的路由、菜单及面包屑配置,直接在路由的 key 以及菜单中的 path 配置即可:
// src/common/router.js '/dashboard/:workplace': { component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Workplace')), }, '/:list/table-list': { component: dynamicWrapper(app, ['rule'], () => import('../routes/List/TableList')), },
// src/common/menu.js const menuData = [{ name: 'dashboard', icon: 'dashboard', path: 'dashboard', children: [{ name: '分析页', path: 'analysis', }, { name: '监控页', path: 'monitor', }, { name: '工作台', path: ':workplace', }], }, { name: '列表页', icon: 'table', path: ':list', children: [], }, { // 更多配置 }];
嵌套布局
具体可以参照 src/common/router.js /list/search 相关配置,及相关组件文件。
嵌套路由同级展示
脚手架默认使用工具函数 getRoutes 对 routerData 进行处理,然后生成路由列表,根据基本算法,在每一级组件中只会渲染当前 match.path 下最邻近的路由,所以,如果你要实现嵌套路由的同级展示(如:将 /list/search 和 /list/search/projects 在同一个地方渲染),就需要手动获取该路由的数据并添加在合适的地方。
{/* src/layouts/BasicLayout.js 类比你的上层 layout 组件 */} <Content style={{ margin: '24px 24px 0', height: '100%' }}> <div style={{ minHeight: 'calc(100vh - 260px)' }}> <Switch> {/* 默认生成的路由列表,不包含 /list/search/projects */} { getRoutes(match.path, routerData).map(item => ( <Route key={item.key} path={item.path} component={item.component} exact={item.exact} /> )) } {/* 补充 /list/search/projects 的路由 */} <Route exact path="/list/search/projects" component={routerData['/list/search/projects'].component} /> <Redirect exact from="/" to="/dashboard/analysis" /> <Route render={NotFound} /> </Switch> </div> </Content>
同时在嵌套 layout 的文件中去掉这一条路由(如果还有下层路由需要 render)。
{/* src/routes/List/List.js 类比你的嵌套 layout 组件 */} <Switch> { getRoutes(match.path, routerData).filter(item => item.path !== '/list/search/projects').map(item => ( <Route key={item.key} path={item.path} component={item.component} exact={item.exact} /> ) ) } </Switch>
隐藏菜单
如果需要隐藏某条菜单项,可以在该条数据中增加 hideInMenu 参数。
// src/common/menu.js const menuData = [{ name: 'dashboard', icon: 'dashboard', path: 'dashboard', children: [{ name: '分析页', path: 'analysis', }, { name: '监控页', path: 'monitor', }, { name: '工作台', path: 'workplace', hideInMenu: true, // 隐藏该条 }], }, { name: '表单页', icon: 'form', path: 'form', hideInMenu: true, // 隐藏该组 children: [{ name: '基础表单', path: 'basic-form', }, { name: '分步表单', path: 'step-form', }, { name: '高级表单', path: 'advanced-form', }], }, { // 更多配置 }];
隐藏面包屑
如需隐藏面包屑中的某个层级,可以增加 hideInBreadcrumb 属性。
// src/common/router.js '/dashboard/analysis': { component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')), hideInBreadcrumb: true, // 隐藏该条 }, '/dashboard/monitor': { component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/Monitor')), },
八、新增页面
新增 js、less 文件
在 src/routes 下新建页面的 js 及 less 文件,如果相关页面有多个,可以新建一个文件夹来放置相关文件。
样式文件默认使用 CSS Modules,如果需要,你可以在样式文件的头部引入 antd 样式变量文件:
@import "~antd/lib/style/themes/default.less";
这样可以很方便地获取 antd 样式变量并在你的文件里使用,有利于保持页面的一致性,也方便实现定制主题。
将文件加入菜单和路由
参照路由那一段
新增 model、service
如果需要用到 dva 中的数据流,还需要在 src/models src/services 中建立相应的 model 和 service,具体可以参考脚手架内置页面的写法。
九、新增业务组件
对于一些可能被多处引用的功能模块,建议提炼成业务组件统一管理。这些组件一般有以下特征:
-
只负责一块相对独立,稳定的功能;
-
没有单独的路由配置;
-
可能是纯静态的,也可能包含自己的 state,但不涉及 dva 的数据流,仅受父组件(通常是一个页面)传递的参数控制。
新建文件
新建一个文件夹 ImageWrapper 在文件夹下 新增 index.js 和 index.less 两个文件。
在使用组件时,默认会在 index.js 中寻找 export 的对象,如果组件比较复杂,可以分为多个文件,最后在 index.js 中统一 export,就像这样:
// MainComponent.js export default ({ ... }) => (...); // SubComponent1.js export default ({ ... }) => (...); // SubComponent2.js export default ({ ... }) => (...); // index.js import MainComponent from './MainComponent'; import SubComponent1 from './SubComponent1'; import SubComponent2 from './SubComponent2'; MainComponent.SubComponent1 = SubComponent1; MainComponent.SubComponent2 = SubComponent2; export default MainComponent;
如项目中:
Description.js:
DescriptionList.js:
index.js:
简单代码大概:
// index.js import React from 'react'; import styles from './index.less'; // 按照 CSS Modules 的方式引入样式文件。 export default ({ src, desc, style }) => ( <div style={style} className={styles.imageWrapper}> <img className={styles.img} src={src} alt={desc} /> {desc && <div className={styles.desc}>{desc}</div>} </div> );
// index.less .imageWrapper { padding: 0 20px 8px; background: #f2f4f5; width: 400px; margin: 0 auto; text-align: center; } .img { vertical-align: middle; max-width: calc(100% - 32px); margin: 2.4em 1em; box-shadow: 0 8px 20px rgba(143, 168, 191, 0.35); }
到这儿组件就建好了
使用
在需要使用的地方直接引入使用就可以了:
import React from 'react'; import ImageWrapper from '../../components/ImageWrapper'; // 注意保证引用路径的正确 export default () => ( <ImageWrapper src="https://os.alipayobjects.com/rmsportal/mgesTPFxodmIwpi.png" desc="示意图" />; )
十、样式
基础的 CSS 知识或查阅属性,可以参考 MDN文档。
less
Ant Design Pro 默认使用 less 作为样式语言。
CSS Modules
在样式开发过程中,有两个问题比较突出:
-
全局污染 —— CSS 文件中的选择器是全局生效的,不同文件中的同名选择器,根据 build 后生成文件中的先后顺序,后面的样式会将前面的覆盖;
-
选择器复杂 —— 为了避免上面的问题,在编写样式的时候不得不小心翼翼,类名里会带上限制范围的标识,变得越来越长,多人开发时还很容易导致命名风格混乱,一个元素上使用的选择器个数也可能越来越多。
为了解决上述问题,我们的脚手架默认使用 CSS Modules 模块化方案,先来看下在这种模式下怎么写样式。
// example.js import styles from './example.less'; export default ({ title }) => <div className={styles.title}>{title}</div>;
// example.less .title { color: @heading-color; font-weight: 600; margin-bottom: 16px; }
在上面的样式文件中,.title 只会在本文件生效,可以在其他任意文件中使用同名选择器,也不会对这里造成影响。不过有的时候,全局生效的样式呢?可以使用 :global。
// example.less .title { color: @heading-color; font-weight: 600; margin-bottom: 16px; } /* 定义全局样式 */ :global(.text) { font-size: 16px; } /* 定义多个全局样式 */ :global { .footer { color: #ccc; } .sider { background: #ebebeb; } }
CSS Modules 的基本原理很简单,就是对每个类名(非 :global 声明的)按照一定规则进行转换,保证它的唯一性。如果在浏览器里查看这个示例的 dom 结构,你会发现实际渲染出来是这样的:
<div class="title___3TqAx">title</div>
类名被自动添加了一个 hash 值,这保证了它的唯一性。
除此之外还要注意一些关键点:
-
CSS Modules 只会对 className 以及 id 进行转换,其他的比如属性选择器,标签选择器都不进行处理,推荐尽量使用 className。
-
由于不用担心类名重复,你的 className 可以在基本语意化的前提下尽量简单一点儿。
CSS Modules 扩展学习文档:
样式文件类别
src/index.less:
全局样式文件,在这里你可以进行一些通用设置,比如脚手架中自带的:
html, body, :global(#root) { height: 100%; } body { text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } // temporary font size patch :global(.ant-tag) { font-size: 12px; }
注:因为 antd 中会自带一些全局设置,如字号,颜色,行高等,所以在这里,只需要关注自己的个性化需求即可,不用进行大量的 reset。
src/utils/utils.less:
这里可以放置一些工具函数供调用,比如清除浮动 .clearfix。
模块样式
针对某个模块/页面生效的文件。
通用模块级
src/layouts/Xxxxxx.less
页面级
src/routes/Xxxxxx/xxx.less
组件级
src/components/Xxxxxx/Index.less
注意:以上样式类别都是针对独立的样式文件,有时样式配置特别简单,也没有重复使用,你也可以用内联样式 style={{ ... }}
来设置。
覆盖组件样式
例如:
antd Select 在多选状态下,默认会展示所有选中项,这里我们给它加一个限制高度,超过此高度就出滚动条。
// TestPage.js import { Select } from 'antd'; import styles from './TestPage.less' const Option = Select.Option; const children = []; for (let i = 10; i < 36; i++) { children.push(<Option key={i.toString(36) + i}>{i.toString(36) + i}</Option>); } ReactDOM.render( <Select mode="multiple" style={{ width: 300 }} placeholder="Please select" className={styles.customSelect} > {children} </Select> , mountNode);
// TestPage.less .customSelect { :global { .ant-select-selection { max-height: 51px; overflow: auto; } } }
方法很简单,有两点需要注意:
-
引入的 antd 组件类名没有被 CSS Modules 转化,所以被覆盖的类名 .ant-select-selection 必须放到 :global 中。
-
因为上一条的关系,覆盖是全局性的,为了防止对其他 Select 造成影响,可以给组件添加 className,只对这类组件进行覆盖,也可以利用外层类名实现这种限制。
十一、和服务端进行交互
Ant Design Pro 是一套基于 React 技术栈的单页面应用,前端代码和本地模拟数据的开发模式, 通过 Restful API 的形式和任何技术栈的服务端应用一起工作。
前端请求流程
在 Ant Design Pro 中,一个完整的前端 UI 交互到服务端处理流程是这样的:
-
UI 组件交互操作;
-
调用 model 的 effect;
-
调用统一管理的 service 请求函数;
-
使用封装的 request.js 发送请求;
-
获取服务端返回;
-
然后调用 reducer 改变 state;
-
更新 model。
为了方便管理维护,统一的请求处理都放在 services 文件夹中,并且一般按照 model 维度进行拆分文件,如:
services/
user.js
api.js
...
其中,utils/request.js 是基于 fetch 的封装,便于统一处理 POST,GET 等请求参数,请求头,以及错误提示信息等。具体可以参看 request.js。
例如在 services 中的一个请求用户信息的例子:
// services/user.js import request from '../utils/request'; export async function query() { return request('/api/users'); } export async function queryCurrent() { return request('/api/currentUser'); } // models/user.js import { queryCurrent } from '../services/user'; ... effects: { *fetch({ payload }, { call, put }) { ... const response = yield call(queryUsers); ... }, }
Effect 处理异步请求
在处理复杂的异步请求的时候,很容易让逻辑混乱,陷入嵌套陷阱,所以 Ant Design Pro 的底层基础框架 dva 使用 effect 的方式来管理同步化异步请求:
effects: { *fetch({ payload }, { call, put }) { yield put({ type: 'changeLoading', payload: true, }); // 异步请求 1 const response = yield call(queryFakeList, payload); yield put({ type: 'save', payload: response, }); // 异步请求 2 const response2 = yield call(queryFakeList2, payload); yield put({ type: 'save2', payload: response2, }); yield put({ type: 'changeLoading', payload: false, }); }, },
通过 generator 和 yield 使得异步调用的逻辑处理跟同步一样,更多可参看 dva async logic。
通常来讲只要 mock 的接口和真实的服务端接口保持一致,那么只需要重定向 mock 到对应的服务端接口即可。
// .roadhogrc.mock.js export default { 'GET /api/(.*)': 'https://your.server.com/api/', };
这样你浏览器里这样的接口 http://localhost:8000/api/applications 会被反向代理到 https://your.server.com/api/applications 下。
关闭 mock
关闭 mock 的方法我们推荐采用环境变量,你可以在 package.json 中设置:
"script" : { "start": "roadhog server", "start:no-proxy": "NO_PROXY=true roadhog server" }
然后在 .roadhogrc.mock.js 中做个判断即可:
const noProxy = process.env.NO_PROXY === 'true'; ... export default noProxy ? {} : delay(proxy, 1000);
十二、引入外部模块
除了 antd 组件以及脚手架内置的业务组件,还可以引入其他外部模块,例如富文本组件 react-quill 。
引入依赖
首先安装模块
npm install react-quill --save
(加上 --save 参数会自动添加依赖到 package.json 中去)
使用
import React from 'react'; import { Button, notification, Card } from 'antd'; import ReactQuill from 'react-quill'; import 'react-quill/dist/quill.snow.css'; export default class NewPage extends React.Component { state = { value: 'test', }; handleChange = (value) => { this.setState({ value, }) }; prompt = () => { notification.open({ message: 'We got value:', description: <span dangerouslySetInnerHTML={{ __html: this.state.value }}></span>, }); }; render() { return ( <Card title="富文本编辑器"> <ReactQuill value={this.state.value} onChange={this.handleChange} /> <Button style={{ marginTop: 16 }} onClick={this.prompt}>Prompt</Button> </Card> ); } }
这样就成功引入了一个富文本组件。有几点值得注意:
-
import 时需要注意组件暴露的数据结构;
-
有一些组件需要额外引入样式。
十三、构建和发布
构建
当项目开发完毕,只需要运行一行命令就可以打包你的应用:
npm run build
由于 Ant Design Pro 底层使用的 roadhog 工具,已经将复杂的流程封装完毕,对于大部分场景,构建打包文件只需要一个命令 roadhog build,构建打包成功之后,会在根目录生成 dist 文件夹,里面就是构建打包好的文件,通常是 ***.js、***.css、index.html 等静态文件。
自定义构建,比如指定目录等,则需要通过 .webpackrc 进行配置,详情参看:roadhog 配置。
环境变量
当你需要区别开发和部署以及测试环境的时候,可以通过在 .webpackrc 中设置 env 环境变量来实现。
"env": { // server 环境 "development": { "extraBabelPlugins": [ "dva-hmr", ] }, // build 环境 "production": { "extraBabelPlugins": [ "transform-runtime", "transform-decorators-legacy", ["import", { "libraryName": "antd", "style": true }] ] } },
分析构建文件体积
构建文件很大,可以通过 analyze 命令构建并分析依赖模块的体积分布,从而优化代码。
npm run analyze
然后打开 dist/stats.html 查看体积分布数据。
发布
将 dist 文件夹的静态文件发布到 cdn 或者静态服务器即可,其中的 index.html 通常是后台服务的入口页面,在确定了 js 和 css 的静态之后可能需要改变页面的引入路径。
代码分割和动态加载
只有进入对应路由后才会加载相应的代码,避免首屏加载过多不必要的 JS 文件,提高大规模前端应用研发的扩展性。
├── 0.async.js ├── 1.async.js ├── 10.async.js ├── 11.async.js ├── 12.async.js ├── 13.async.js ├── 14.async.js ...
注:如果开启了 hash,会变成 0.2df975b2.async.js 的形式,修改代码后 hash 会变化,可以避免前端缓存问题。
这种方式对于部署有一定的要求,可以将 dist 整体部署到后端应用的静态资源目录下(通常为 static 或者 public),这样默认的静态资源引用路径直接指向应用的根目录 //your.application.domain/***.js 和 //your.application.domain/***.css。
如果静态资源域名和应用域名不同(例如独立的 cdn 地址),需要使用在 .webpackrc 文件里用 publicPath 对生产环境的静态路径进行配置。可以参考 create-react-app 的部署文档
{ "publicPath": "https://cdn.com/your_app" }
如果不需要代码分割和动态加载功能, Ant Design Pro 1.0 版本后我们 .webpackrc 里使用了 "disableDynamicImport": true 默认关掉了动态加载(roadhog@2.x 支持),回退为单文件 index.js 和 index.css 的构建方式。如果需要动态加载删掉这个配置即可。
前端路由与服务端的结合
Ant Design Pro 中,前端路由使用的是 React Router,所以可以选择两种方式:browserHistory 和 hashHistory。
两者的区别简单来说是对路由方式的处理不一样,hashHistory 是以 # 后面的路径进行处理,通过 HTML 5 History 进行前端路由管理,而 browserHistory 则是类似我们通常的页面访问路径,并没有 #,通过服务端的配置,能够访问指定的 url 都定向到当前页面,从而能够进行前端的路由管理。
所以如果 url 里有 #,想去掉的话,需要切换为 browserHistory。
如果使用的是静态站点,那么使用 browserHistory 可能会无法访问你的应用,因为假设你访问 http://localhost:8000/dashboard/monitor,那么其实你的静态服务器并没有能够映射的文件,而使用 hashHistory 则不会有这个问题,因为它的页面路径是以 # 开始的,所有访问都在前端完成,如:http://localhost:8000/#/dashboard/monitor。
不过如果有对应的后台服务器,那么推荐采用 browserHistory,只需要在服务端做一个映射,比如:
express 的例子
app.use(express.static(path.join(__dirname, 'build'))); app.get('/*', function (req, res) { res.sendFile(path.join(__dirname, 'build', 'index.html')); });
egg 的例子
// controller exports.index = function* () { yield this.render('App.jsx', { context: { user: this.session.user, }, }); }; // router app.get('home', '/*', 'home.index');
在 Ant Design Pro 中使用前端路由
路由包含的信息在 router.js 中,不过关于 history 的配置是在 index.js 入口文件中,传入配置信息给 dva 构造器即可。
由于使用了 react-router@4,所以引入 browserHistory 与原本 dva 的方式有所改变。
import dva from 'dva'; // 引入 browserHistory import browserHistory from 'history/createBrowserHistory' import models from './models'; import './index.less'; // use browserHistory const app = dva({ history: browserHistory(), }); // default hashHistroy const app = dva();
关于路由更多可以参看 React Router 。 关于 react-router@4 更多可以参看 All About React Router 4 。
十四、图表
Ant Design Pro 提供了由设计师精心设计抽象的图表类型,是在 BizCharts 图表库基础上的二次封装,同时提供了业务中常用的图表套件,可以单独使用,也可以组合起来实现复杂的展示效果。
目前一共包涵 10 个图表类型,以及 2 个图表套件:
-
饼状图(Pie)
-
柱状图(Bar)
-
仪表盘(Gauge)
-
雷达图(Radar)
-
标签云(TagCloud)
-
水波图(WaterWave)
-
迷你柱状图(MiniBar)
-
迷你区域图(MiniArea)
-
迷你进度条(MiniProgress)
-
带有时间轴的折线图(TimelineChart)
-
图表卡片(ChartCard)
-
图表字段(Field)
Charts 图表套件是在 ant-design-pro/lib/Charts 包中,引用到项目就像使用其它组件一样:
import { ChartCard, MiniBar } from 'ant-design-pro/lib/Charts'; import { Tooltip, Icon } from 'antd'; const visitData = [ { x: "2017-09-01", y: 100 }, { x: "2017-09-02", y: 120 }, { x: "2017-09-03", y: 88 }, { x: "2017-09-04", y: 65 } ]; ReactDOM.render( <ChartCard title="支付笔数" action={ <Tooltip title="支付笔数反应交易质量"> <Icon type="exclamation-circle-o" /> </Tooltip> } total="6,500" contentHeight={46} > <MiniBar height={46} data={visitData} /> </ChartCard>, mountNode );
使用 BizCharts 绘制图表
如果 Ant Design Pro 不能满足你的业务需求,可以直接使用 BizCharts 封装自己的图表组件。
引入 BizCharts
npm install bizcharts --save
在项目中使用
import { Chart, Axis, Tooltip, Geom } from 'bizcharts'; const data = [...]; <Chart height={400} data={data} forceFit> <Axis name="month" /> <Axis name="temperature" label={{ formatter: val => `${val}°C` }} /> <Tooltip crosshairs={{ type : "y" }} /> <Geom type="line" position="month*temperature" size={2} color={'city'} /> <Geom type='point' position="month*temperature" size={4} color={'city'} /> </Chart>
参看 更多 Demo。
十五、业务图标
如果没有在 antd Icon 中找到需要的图标,可以到 iconfont.cn 上采集并生成自己的业务图标库,再进行使用。
生成图标库代码
首先,搜索并找到你需要的图标,将它采集到你的购物车里,在购物车里,你可以将选中的图标添加到项目中(没有的话,新建一个),后续生成的资源/代码都是以项目为维度的。
如果你已经有了设计稿,只是需要生成相关代码,可以上传你的图标后,再进行上面的操作。
来到刚才选中的项目页,点击『生成代码』的链接,会在下方生成不同引入方式的代码,下面会分别介绍。
引入
现在推荐两种引入方式供你选择:Unicode 及 Font class。
Unicode
这是最原始的方式,需要三步来完成引入:
拷贝项目生成的字体库代码,可以新建一个样式文件来放置图标相关的样式。
加入图标样式代码,如果没有特殊的要求,可以直接复用 Ant Design 图标的样式。
.iconfont { display: inline-block; font-style: normal; vertical-align: baseline; text-align: center; text-transform: none; line-height: 1; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; &:before { display: block; font-family: "iconfont" !important; /* 注意与 font-face 中的匹配 */ } }
在项目中鼠标移动到要用的图标上,点击『复制代码』,就得到了图标对应的字体编码,现在可以直接引入了:
<i class="iconfont"></i>
Font Class
切换到 Font class 页签,在页面头部引入下面生成的 css 代码:
(如果不喜欢标签引入的方式,也可以直接拷贝上面链接中的代码到样式文件中。如果不喜欢网站默认生成的类名,自己重写这部分代码即可)
- .icon-ali-pay:before { content: "\e66b"; } // 修改前 + .monitor-icon-alipay:before { content: "\e66b"; } // 修改后
这时可以选择拷贝图标对应代码(就是类名,如果类名被重写过,这里记得用修改后的),直接使用:
<i class="iconfont icon-ali-pay"></i>
不过更推荐 antd Icon,将它封装一下:
import React from 'react'; const BizIcon = (props) => { const { type } = props; return <i className={`iconfont icon-${type}`} />; }; export default BizIcon;
现在可以更加方便地使用:
<BizIcon type="ali-pay" />
Unicode 和 Font Class 本质上就是字体,通过一些字体的样式属性去控制这种图标的展现,同时浏览器兼容性很好,但不支持多色图标。
除了上面两种方式,你还可以通过 Symbol 方式引入,相关内容可以参考:
十六、Mock 和联调
Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发所阻塞。
在 Ant Design Pro 中,因为底层的工具是 roadhog,而它自带了代理请求功能,通过代理请求就能够轻松处理数据模拟的功能。
使用 roadhog 的请求代理功能
在通过配置 .roadhogrc.mock.js 来处理代理请求,支持基于 require 动态分析的实时刷新,支持 ES6 语法,以及友好的出错提示,详情参看 roadhog。
export default { // 支持值为 Object 和 Array 'GET /api/users': { users: [1, 2] }, // GET POST 可省略 '/api/users/1': { id: 1 }, // 支持自定义函数,API 参考 express@4 'POST /api/users/create': (req, res) => { res.end('OK'); }, // Forward 到另一个服务器 'GET /assets/*': 'https://assets.online/', // Forward 到另一个服务器,并指定子路径 // 请求 /someDir/0.0.50/index.css 会被代理到 https://g.alicdn.com/tb-page/taobao-home, 实际返回 https://g.alicdn.com/tb-page/taobao-home/0.0.50/index.css 'GET /someDir/(.*)': 'https://g.alicdn.com/tb-page/taobao-home', };
当客户端(浏览器)发送请求,如:GET /api/users,那么本地启动的 roadhog server 会跟此配置文件匹配请求路径以及方法,如果匹配到了,就会将请求通过配置处理,就可以像样例一样,可以直接返回数据,也可以通过函数处理以及重定向到另一个服务器。
比如定义如下映射规则:
'GET /api/currentUser': { name: 'momo.zxy', avatar: imgMap.user, userid: '00000001', notifyCount: 12, },
访问的本地 /api/users 接口:
引入 Mock.js
Mock.js 是常用的辅助生成模拟数据的第三方库,当然也可以用任意其他的库来结合 roadhog 构建数据模拟功能。
import mockjs from 'mockjs'; export default { // 使用 mockjs 等三方库 'GET /api/tags': mockjs.mock({ 'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }], }), };
添加跨域请求头
设置 response 的请求头即可:
'POST /api/users/create': (req, res) => { ... res.setHeader('Access-Control-Allow-Origin', '*'); ... },
合理的拆分你的 mock 文件
对于整个系统来说,请求接口是复杂并且繁多的,为了处理大量模拟请求的场景,通常把每一个数据模型抽象成一个文件,统一放在 mock 的文件夹中,然后再在 .roadhog.mock.js 中引入。
.roadhog.mock.js 的样例如下:
import mockjs from 'mockjs'; // 引入分离的 mock 文件 import { getRule, postRule } from './mock/rule'; import { getActivities, getNotice, getFakeList } from './mock/api'; import { getFakeChartData } from './mock/chart'; import { getProfileBasicData } from './mock/profile'; import { getProfileAdvancedData } from './mock/profile'; import { getNotices } from './mock/notices'; const proxy = { 'GET /api/fake_list': getFakeList, 'GET /api/fake_chart_data': getFakeChartData, 'GET /api/profile/basic': getProfileBasicData, 'GET /api/profile/advanced': getProfileAdvancedData, 'GET /api/notices': getNotices, }; export default proxy;
如何模拟延迟
为了更加真实的模拟网络数据请求,往往需要模拟网络延迟时间。
手动添加 setTimeout 模拟延迟
你可以在重写请求的代理方法,在其中添加模拟延迟的处理,如:
'POST /api/forms': (req, res) => { setTimeout(() => { res.send('Ok'); }, 1000); },
使用插件模拟延迟
上面的方法虽然简便,但是当需要添加所有的请求延迟的时候,可能就麻烦了,不过可以通过第三方插件来简化这个问题,如:roadhog-api-doc。
import { delay } from 'roadhog-api-doc'; const proxy = { 'GET /api/project/notice': getNotice, 'GET /api/activities': getActivities, 'GET /api/rule': getRule, 'GET /api/tags': mockjs.mock({ 'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }] }), 'GET /api/fake_list': getFakeList, 'GET /api/fake_chart_data': getFakeChartData, 'GET /api/profile/basic': getProfileBasicData, 'GET /api/profile/advanced': getProfileAdvancedData, 'POST /api/register': (req, res) => { res.send({ status: 'ok' }); }, 'GET /api/notices': getNotices, }; // 调用 delay 函数,统一处理 export default delay(proxy, 1000);
生成 API 文档
在跟服务端联调开发的时候,往往需要约定接口,包含路径,方法,参数,返回值等信息,roadhog-api-doc 提供通过 .roadhog.mock.js 自动生成文档的功能,安装使用详见:roadhog-api-doc。
而相关的文档信息,同样需要按照 roadhog-api-doc 提供的方式写到 .roadhog.mock.js 中。
import { postRule } from './mock/rule'; import { format } from 'roadhog-api-doc'; const proxy = { 'GET /api/currentUser': { // 接口描述 $desc: "获取当前用户接口", // 接口参数 $params: { pageSize: { desc: '分页', exp: 2, }, }, // 接口返回 $body: { name: 'momo.zxy', avatar: imgMap.user, userid: '00000001', notifyCount: 12, }, }, 'POST /api/rule': { $params: { pageSize: { desc: '分页', exp: 2, }, }, $body: postRule, }, }; // format 函数用于保证本地的模拟正常,如果写了文档,这个函数转换是必要的 export default format(proxy);
联调
当本地开发完毕之后,如果服务器的接口满足之前的约定,那么你只需要不开本地代理或者重定向代理到目标服务器就可以访问真实的服务端数据,非常方便。
十七、更换主题
我们基于 Ant Design React 进行开发,完全支持官方提供的 less 变量定制功能,具体方式如下:
在脚手架目录中找到 src/theme.js 如下。
// https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less module.exports = { 'font-size-base': '14px', 'badge-font-size': '12px', 'btn-font-size-lg': '@font-size-base', 'menu-dark-bg': '#00182E', 'menu-dark-submenu-bg': '#000B14', 'layout-sider-background': '#00182E', 'layout-body-background': '#f0f2f5', };
在 所有变量表 找到需要修改的变量,修改后启动 npm start,就可以在你的应用界面看到效果了。
更多方式可以参考官方文档:定制主题。
小技巧:可以把以上的主题配置文件 theme.js 发布成单独的 npm 包来引入,方便不同项目之间的复用。 在 .webpackrc 中修改 "theme": "./node_modules/your-package/theme.js" 为相应路径即可。
样式覆盖
全局覆盖组件
比如在 src/index.less 里修改所有标签的字体大小。
// src/index.less :global { .ant-tag { font-size: 12px; } }
单独覆盖指定组件
// sample.less .customTag { font-size: 18px; }
import styles from './sample.less'; ... return <Tag className={styles.customTag}>定制标签</Tag>;
(不推荐进行样式覆盖,一是默认主题和组件是经过了设计师精心调节,强行覆盖可能会影响整体效果;二是覆盖代码可能因为组件库版本升级而失效。)
十八、UI 测试
Ant Design Pro 封装了一套简洁易用的 React 单元测试和 E2E 测试方案,在项目根目录运行以下命令就能运行测试用例。
npm run test:all # 执行所有测试
单元测试
单元测试用于测试 React UI 组件的表现。我们参考了 create-react-app,使用 jest 作为测试框架。
jest 是一个 node 端运行的测试框架,使用了 jsdom 来模拟 DOM 环境,适合用于快速测试 React 组件的逻辑表现,需要真实浏览器可以参考 E2E 测试部分。、
写一个用例
比如,我们可以建一个文件 src/routes/Result/Success.test.js 来测试成功页面组件的 UI 表现。
import React from 'react'; import { shallow } from 'enzyme'; import Success from './Success'; // 引入对应的 React 组件 it('renders with Result', () => { const wrapper = shallow(<Success />); // 进行渲染 expect(wrapper.find('Result').length).toBe(1); // 有 Result 组件 expect(wrapper.find('Result').prop('type')).toBe('success'); // Result 组件的类型是成功 });
这里使用了 enzyme 作为测试库,它提供了大量实用的 API 来帮助我们测试 React 组件。断言部分沿用了 jest 默认的 jasmine2 expect 语法。
本地执行
使用以下的命令将统一搜索和执行 src 下 *.test.js 格式的用例文件。
npm test .test.js
执行单个或一组用例
npm test src/routes/Result/Success.test.js # 测试 Success.test.js npm test src/routes # 测试 routes 下的所有用例文件
测试 dva 包装组件
被 dva connect 的 React 组件可以使用下面方式进行测试。
import React from 'react'; import { shallow } from 'enzyme'; import Dashboard from './Dashboard'; it('renders Dashboard', () => { // 使用包装后的组件 const wrapper = shallow( <Dashboard.WrappedComponent user={{ list: [] }} /> ); expect(wrapper.find('Table').props().dataSource).toEqual([]); });
e2e 测试
端到端测试也叫冒烟测试,用于测试真实浏览器环境下前端应用的流程和表现,相当于代替人工去操作应用。
我们引入了 nightmare 作为 E2E 测试的工具,nightmare 默认使用 electron 作为浏览器环境运行你的应用,并且提供了非常语义化的 API 来描述业务逻辑。
写一个 e2e 用例
假设有一个需求,用户在登录页面输入错误的用户名和密码,点击登录后,出现错误提示框。
我们写一个用例来保障这个流程。在 src/e2e/ 目录下建一个 Login.e2e.js 文件,按上述业务需求描述测试用例。
import Nightmare from 'nightmare'; describe('Login', () => { it('should login with failure', async () => { await Nightmare().goto('http://localhost:8000/#/user/login') .type('#userName', 'mockuser') .type('#password', 'wrong_password') .click('button[type="submit"]') .wait('.ant-alert-error') // should display error .end(); }); });
更多 nightmare 的方法可以参考 segmentio/nightmare#api。
运行用例
运行下列命令将执行 src 下所有的 *.e2e.js 用例文件。
npm test .e2e.js
(注意,本地测试 e2e 用例需要启动 npm start,否则会报 Failed: navigation error 的错误。)
watch 模式
npm test -- --watch
添加 --watch 配置可以进入 watch 模式,当你修改和保存文件时,Jest 会自动执行相应用例。Jest 的命令行工具也提供了各种方便的快捷键来执行你需要的用例。
测试覆盖率
npm test -- --coverage
添加 --coverage 配置可以显示项目的测试覆盖率。
参考链接
十九、独立使用 Pro 组件
Ant Design Pro 脚手架内提供了一套默认业务组件,这些组件抽象了控制台业务中的一些常见区块。
如何使用
Ant Design Pro 脚手架内用到的组件分为两种:
对于脚手架的用户,可以在脚手架中直接引用/新增/改造 pro 的自带组件,具体方式可参考 新增组件。
对于没有使用这套脚手架的开发者,提供了一种方式来调用这套内建组件。
默认业务组件会发布到 npm 的 ant-design-pro 上。
npm install ant-design-pro --save
import 'ant-design-pro/dist/ant-design-pro.css'; // 统一引入样式
然后你就可以像使用 Ant Design 组件一样调用 pro 组件了。
import Result from 'ant-design-pro/lib/Result';
ReactDOM.render(<Result type="success" />, mountNode);
(注意,pro 组件默认依赖于 antd@3.0,需要保证 antd 版本的一致性。)
二十、错误处理
在用户使用过程中,可能遇到各种异常情况,比如页面404,申请结果失败,请求的返回异常等等,这篇文档会按照报错形式的不同,分别介绍下相应的处理建议。
应用场景
-
路由直接引导到报错页面,比如你输入的网址没有匹配到任何页面,可以由路由引导到预设的 404 页面。
-
代码控制跳转到报错页面,比如根据请求返回的数据,将没有权限的用户引导到 403 页面。
实现
针对页面级的报错,提供了两个业务组件供你选择,可以很方便地实现一个报错页面:
<Exception type="404" />
默认支持 404,403,500 三种错误,也可自定义文案等内容。
<Result type="error" title="提交失败" description="请核对并修改以下信息后,再重新提交。" actions={<Button size="large" type="primary">返回修改</Button>} />
这个组件一般用在提交结果展示,文案操作等均可自定义。
脚手架默认会将无法匹配到页面的网址引导到预设的 404 页面,如果需要自定义此页面,可以修改这个文件 ./src/routes/Exception/404.js,相关的路由配置在这里 BasicLayout.js#L362
提示性报错
应用场景
-
表单项校验报错。
-
操作反馈。
-
网络请求错误。
实现
对于操作反馈和网络请求错误提示,有一些组件可能会用到:
在单页应用中,最常见的需求就是处理网络错误信息,可以利用 message 和 notification 来吐出响应的接口/网络/业务数据错误。
import fetch from 'dva/fetch'; import { notification } from 'antd'; ... fetch(url) .then(response => response.json()) .catch((error) => { // 处理接口返回的数据格式错误的逻辑 if (error.code) { notification.error({ message: error.name, description: error.message, }); } if ('stack' in error && 'message' in error) { notification.error({ message: `请求错误: ${url}`, description: error.message, }); } return error; });
Ant Design Pro 封装了一个 request.js 统一处理请求,完整代码可参考:https://github.com/ant-design/ant-design-pro/blob/master/src/utils/request.js
二十一、使用 CLI 工具
npm install ant-design-pro-cli -g
运行
pro new
pro generate
目前提供的标准模板如下:
-
页面模板
-
Blank
-
BasicList
-
BasicForm
-
-
组件模板
-
stateless
-
normal
-
-
service
-
model
二十二、权限管理
权限控制是中后台系统中常见的需求之一,可以利用权限控制组件,实现一些基本的权限控制功能,脚手架中也包含了几个简单示例以提供参考。
权限组件 Authorized
使用方式详见 Authorized 文档。
控制菜单显示
如需对某些菜单进行权限控制,只须对菜单配置文件 menu.js 中的菜单项设置 authority 属性即可,代表该项菜单的准入权限,菜单生成文件中会默认调用 Authorized.check 进行判断处理。
{ name: '表单页', icon: 'form', path: 'form', children: [{ name: '基础表单', path: 'basic-form', }, { name: '分步表单', path: 'step-form', }, { name: '高级表单', authority: 'admin', // 配置准入权限 path: 'advanced-form', }], }
控制路由导向
与菜单控制类似,路由权限的配置也很简单:
// src/common/router.js '/dashboard/analysis': { component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')), }, '/dashboard/monitor': { component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/Monitor')), }, '/dashboard/workplace': { component: dynamicWrapper(app, ['project', 'activities', 'chart'], () => import('../routes/Dashboard/Workplace')), authority: 'admin', // 配置准入权限 },
注意:菜单中配置的权限会自动同步到对应路由中,如果 router.js 中有不同的配置,路由控制以 router.js 为准。
控制页面元素显示
使用 Authorized 或 Authorized.Secured 可以很方便地控制元素/组件的渲染,具体使用方式参见组件文档。
修改当前权限
脚手架中使用 localstorage 模拟权限角色,实际项目中可能需要从后台读取。
脚手架中实现了一个简单的刷新权限方法,在登录/注销等关键节点对当前权限进行了更新。
具体可以查看login.js中对 reloadAuthorized 的调用。