接前面二,下面咱们实现右键菜单、http通讯、路由。html
本系列教程是用Vue.js + Nuxt.js + Element + Vuex + 开源js绘图库,打造一个属于本身的在线绘图软件,最终效果:http://topology.le5le.com 。若是你以为好,欢迎给文章和开源库点赞,让咱们更有动力去作好!vue
本系列教程源码地址:Githubnode
右键菜单原理很简单:自定义html的oncontextmenu事件:react
<div id="topology-canvas" className={styles.full} onContextMenu={this.hanleContextMenu} />
屏蔽默认右键菜单事件,计算右键鼠标位置,弹出一个咱们本身的div自定义菜单便可ios
hanleContextMenu = (event: any) => { event.preventDefault() event.stopPropagation() if (event.clientY + 360 < document.body.clientHeight) { this.setState({ contextmenu: { position: 'fixed', zIndex: '10', display: 'block', left: event.clientX + 'px', top: event.clientY + 'px', bottom: '' } }); } else { this.setState({ contextmenu: { position: 'fixed', zIndex: '10', display: 'block', left: event.clientX + 'px', top: '', bottom: document.body.clientHeight - event.clientY + 'px' } }); } }
<div style={this.state.contextmenu} > <CanvasContextMenu data={this.state.selected} canvas={this.canvas} /> </div>
在本项目中,封装了一个右键菜单组件“CanvasContextMenu”,经过父组件,传递canvas实例和选中的属性数据git
export interface CanvasContextMenuProps { data: { node?: Node, line?: Line, multi?: boolean, nodes?: Node[], locked?: boolean }; canvas: Topology; }
其中,属性data含义为:github
data: { node: null, // 选中节点 line: null, // 选中连线 nodes: null, // 选中多个节点 multi: false, // 选中多个节点/连线 locked: false // 选中对象是否被锁定 }
而后,咱们根据菜单事件和属性来调用canvas的相应接口函数,参考开发文档vuex
这里,咱们不去从零写个后端服务,直接采用topology.le5le.com线上接口服务。
固然Umi.js支持Mock数据redux
首先,咱们须要给.umirc.ts添加http代理配置,这样开发环境下的http请求,自动代理转发给topology.le5le.com,获取到真实数据。canvas
proxy: { '/api/': { target: 'http://topology.le5le.com', changeOrigin: true }, '/image/': { target: 'http://topology.le5le.com', changeOrigin: true } }
其中,proxy的含义是指:全部/api/、/image/开头的请求,自动转发给http://topology.le5le.com/ ,其余的不转发。
一般,咱们经过前缀/api/表示这是后端接口请求,而不是静态资源请求;/image/表示静态资源图片请求。
咱们直接使用umi-request,和axios差很少,没有谁好谁坏。
yarn add umi-request --save yarn add le5le-store --save // cookie
新建一个utils/request.tsx拦截器文件。
http拦截器的做用是,每次请求和数据返回时,自动帮咱们处理一些全局公用操做。好比身份认证token的添加。
import _request, { extend } from 'umi-request'; import { notification } from 'antd'; import router from 'umi/router'; import { Cookie } from 'le5le-store'; const codeMessage: any = { 200: '服务器成功返回请求的数据。', 201: '新建或修改数据成功。', 202: '一个请求已经进入后台排队(异步任务)。', 204: '删除数据成功。', 400: '发出的请求有错误,服务器没有进行新建或修改数据的操做。', 401: '用户没有权限(令牌、用户名、密码错误)。', 403: '用户获得受权,可是访问是被禁止的。', 404: '发出的请求针对的是不存在的记录,服务器没有进行操做。', 406: '请求的格式不可得。', 410: '请求的资源被永久删除,且不会再获得的。', 422: '当建立一个对象时,发生一个验证错误。', 500: '服务器发生错误,请检查服务器。', 502: '网关错误。', 503: '服务不可用,服务器暂时过载或维护。', 504: '网关超时。', }; // response拦截器, 处理response _request.interceptors.response.use((response: any, options) => { if (response.body.error) { notification.error({ message: `服务错误`, description: response.body.error, }); } return response; }); /** * 异常处理程序 */ const errorHandler = (error: any) => { const { response = {} } = error; const { status } = response; const errortext = codeMessage[response.status] || response.statusText; if (status === 401) { notification.error({ message: '请先登陆。', }); return; } // environment should not be used if (status === 403) { router.push('/'); return; } if (status <= 504 && status >= 500) { notification.error({ message: `服务错误`, description: errortext, }); return; } if (status >= 404 && status < 422) { router.push('/'); } }; /** * 配置request请求时的默认参数 */ const request = extend({ errorHandler, // 默认错误处理 headers: { 'Authorization': Cookie.get('token') // 自动添加header }, credentials: 'omit' }); export default request;
而后直接使用上面咱们扩展的request请求便可:
import request from '@/utils/request'; export async function get() { return request('/api/user/profile'); }
在models文件夹下新增一个user.tsx。这里,咱们用到了异步请求,所以新增了effects,专门用于异步数据提交;获得异步数据后,再经过reducers操做(这里为set),真正提交数据到store。
import { Reducer } from 'redux'; import { Effect } from 'dva'; import { get } from '@/services/user'; export interface IUser { current: any } export interface UserModelType { namespace: 'user'; state: IUser; effects: { fetch: Effect; }; reducers: { set: Reducer<IUser>; }; } const UserModel: UserModelType = { namespace: 'user', state: { current: null }, effects: { *fetch(_, { call, put }) { const response = yield call(get); yield put({ type: 'set', payload: response, }); }, }, reducers: { set(state, action) { return { ...state, current: action.payload, }; }, }, }; export default UserModel;
其中,http请求用户数据被封装在独立的service里:@/services/user
import request from '@/utils/request'; export async function get() { return request('/api/user/profile'); }
{current ? ( <SubMenu title={ <span> <Avatar style={{ backgroundColor: '#f56a00', verticalAlign: 'middle' }} size="small"> {current.username[0]} </Avatar> <span className="ml5">{current.username}</span> </span> } className={styles.right}> <Menu.Item className={styles.subTtem}> <a href={accountUrl} target="_blank"> 退出 </a> </Menu.Item> </SubMenu> ) : ( <Menu.Item className={styles.right}> <a href={accountUrl} target="_blank"> 登陆/注册 </a> </Menu.Item> ) }
这里,咱们直接省略登陆页面,直接跳转到线上登陆页面account.le5le.com,共享登陆状态。
凡是le5le.com的子域名,经过共享cookie中的token来共享le5le.com的登陆状态。首先,咱们修改本地电脑的host文件,新增一条local.le5le.com子域名,映射到本地电脑:
127.0.0.1 local.le5le.com
如何修改host文件,请google。
而后,咱们把 http://localhost:8000/ 换成 http://local.le5le.com:8000/ 去在浏览器中打开咱们的开发页面,这时,咱们就能够点击右上角“登陆/注册”,去登陆。
在le5le.com上,是使用jwt的方式去用户认证的。jwt的token值存储在cookie中,方便子域名共享登陆。而后每一个http请求headers里面加上Authorization: token值,后端服务就能够认证用户身份。
在第一次打开网页初始化时,只需在请求后端服务/api/user/profile获取用户便可。当接口/api/user/profile返回用户数据,表示用户已登陆;当返回401表示未登陆。这里,咱们先判断了是否存在cookie下的token在请求用户接口。参考headers.tsx:
componentDidMount() { const { dispatch } = this.props as any; if (Cookie.get('token')) { dispatch({ type: 'user/fetch', }); } }
这里,发送一个redux请求数据指令'user/fetch',models/user.tsx的effects/fetch就会请求用户数据。
而后,经过 connect,把 models/users 赋值到 header.tsx的props
export default connect((state: any) => ({ canvas: state.canvas, user: state.user }))(Headers);
注释掉.umirc.ts里面的路由配置,咱们采用“约定优于配置”的方式
// routes: [ // { // path: '/', // component: '../layouts/index', // routes: [{ path: '/', component: '../pages/index' }], // }, // ],
把原有的画布页面index.tsx及组件移动到 workspace下。新增一个index.tsx首页
import React from 'react'; import { connect } from 'dva'; import router from 'umi/router'; import { Avatar, Pagination } from 'antd'; import { list } from '@/services/topology'; import styles from './index.less'; class Index extends React.Component<{}> { state = { data: { list: [], count: 0 }, search: { pageIndex: 1, pageCount: 8 } }; componentDidMount() { this.getList(); } async getList(page?: number) { const data = await list(page || this.state.search.pageIndex, this.state.search.pageCount); this.setState({ data }); } handlePage = (page: number) => { this.setState({ search: { pageIndex: page, pageCount: 8 } }); this.getList(page); } open(data: any) { router.push({ pathname: '/workspace', query: { id: data.id, }, }); } render() { return ( <div className={styles.page}> <div className={styles.nav}> <label>热门图文</label> </div> <div className="flex wrap"> {this.state.data.list.map((item: any, index) => { return ( <div className={styles.topo} key={index} onClick={() => { this.open(item) }}> <div className={styles.image}> <img src={item.image} /> </div> <div className="ph15 pv10"> <div className={styles.title} title={item.name}>{item.name}</div> <div className={styles.desc} title={item.desc}>{item.desc}</div> <div className="flex mt5"> <div className="full flex middle"> <Avatar style={{ backgroundColor: '#f56a00', verticalAlign: 'middle' }} size="small"> {item.username[0]} </Avatar> <span className="ml5">{item.username}</span> </div> <div> <span className="hover pointer mr15" title="赞"> <i className={item.stared ? 'iconfont icon-appreciatefill' : 'iconfont icon-appreciate'} /> <span className="ml5">{item.star || 0}</span> </span> <span className="hover pointer" title="收藏"> <i className={item.favorited ? 'iconfont icon-likefill' : 'iconfont icon-like'} /> <span className="ml5">{item.hot || 0}</span> </span> </div> </div> </div> </div> ) })} </div> <div> <Pagination defaultPageSize={8} current={this.state.search.pageIndex} total={this.state.data.count} onChange={this.handlePage} /> </div> </div> ); } } export default connect((state: any) => ({ event: state.event }))(Index);
在componentDidMount里面去请求数据列表,而后经过open去跳转到workspace路由。
# 最后
自此,一个麻雀虽小五脏俱全的小项目就完成了,包含:框架搭建、插件、vuex、身份认证、http通讯、路由等功能。
整个项目功能细节还不完善,欢迎你们提pr:
完整细节可参考:http://topology.le5le.com/ ,开发文档 。可加入贡献者名单哦!也欢迎加群交流讨论:
经过GitHub的pr方式: