【前端新手也能做大項目】:跟我一起,從零打造一個屬於自己的在線Visio項目實戰【ReactJS + UmiJS + DvaJS】 (三)

前面二,下面我們實現右鍵菜單、http通信、路由。

本系列教程是用Vue.js + Nuxt.js + Element + Vuex + 開源js繪圖庫,打造一個屬於自己的在線繪圖軟件,最終效果:http://topology.le5le.com 。如果你覺得好,歡迎給文章和開源庫點贊,讓我們更有動力去做好!

本系列教程源碼地址:Github

目錄

右鍵菜單

右鍵菜單原理很簡單:自定義html的oncontextmenu事件:

<div id="topology-canvas" className={styles.full} onContextMenu={this.hanleContextMenu} />複製代碼

屏蔽默認右鍵菜單事件,計算右鍵鼠標位置,彈出一個我們自己的div自定義菜單即可

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實例和選中的屬性數據

export interface CanvasContextMenuProps {
  data: {
    node?: Node,
    line?: Line,
    multi?: boolean,
    nodes?: Node[],
    locked?: boolean
  };
  canvas: Topology;
}複製代碼

其中,屬性data含義爲:

data: {
  node: null,       // 選中節點
  line: null,         // 選中連線
  nodes: null,    // 選中多個節點
  multi: false,   // 選中多個節點/連線
  locked: false // 選中對象是否被鎖定
}複製代碼

然後,我們根據菜單事件和屬性來調用canvas的相應接口函數,參考開發文檔

 

http通信

這裏,我們不去從零寫個後端服務,直接採用topology.le5le.com線上接口服務。當然Umi.js支持Mock數據

代理配置

首先,我們需要給.umirc.ts添加http代理配置,這樣開發環境下的http請求,自動代理轉發給topology.le5le.com,獲取到真實數據。

 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/表示靜態資源圖片請求。

 

http請求和攔截器

我們直接使用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');
}複製代碼

 

用戶登錄

1. 給redux新增user

在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');
}複製代碼

 

2. 右上角添加用戶頭像和暱稱

{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>
            )
          }複製代碼

 

3. http請求用戶登錄狀態

這裏,我們直接省略登錄頁面,直接跳轉到線上登錄頁面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/ 去在瀏覽器中打開我們的開發頁面,這時,我們就可以點擊右上角“登錄/註冊”,去登錄。

4. 第一次打開網頁,讀取用戶是否登錄

在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/ ,開發文檔 。可加入貢獻者名單哦!也歡迎加羣交流討論:

topology技術討論羣2

如何貢獻

通過GitHub的pr方式:

  1. 閱讀開發文檔,瞭解相關屬性。
  2. fork倉庫到自己名下
  3. 本地修改並提交到自己的git倉庫
  4. 在自己的fork倉庫找到 “Pull request” 按鈕,提交
    file

開源項目不易,歡迎大家一起參與,給【文章GitHub開源庫】點星點贊,或資助服務器:
file

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章