接前面二,下面咱們實現右鍵菜單、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方式: