**vue-element-admin 框架结构粗解**
vue-element-admin は、vue と element-ui に基づくバックエンド フロントエンド ソリューションです。最新のフロントエンド テクノロジ スタック、組み込みの i18 国際化ソリューション、動的ルーティング、権限検証を使用し、典型的なビジネス モデルを洗練し、豊富な機能コンポーネントを提供し、エンタープライズ レベルのミッドバックグラウンド製品のプロトタイプを迅速に構築するのに役立ちます。
1. プロジェクト環境構築
1.1 インストール開始
依存関係のインストールの失敗の問題を解決するには、cnpm をインストールします。インストール後、cnpm install を使用して、既存の依存関係または新しく追加された依存関係をインストールできます。
- 1 つは、外部ネットワークへの依存関係を正常にインストールできることです (倉庫は中国にあります)。
- 2 番目の高速な cnpm インストールは次のとおりです: npm install -g cnpm --registry=https://registry.npm.taabao.org
- npm run dev #開発モードでサービスを開始する
1.2 プロジェクトのディレクトリ構造
§── ビルド # ビルド関連
§── モック # プロジェクトのモック シミュレーション データ
§── public # 静的リソース
│ │── favicon.ico # ファビコン アイコン
│ └── Index.html # HTML テンプレート
§── src # ソース コード
│ §── api # すべてのリクエスト
│ ├── アセット # テーマフォントなどの静的リソース
│ §── コンポーネント # グローバルパブリックコンポーネント
│ §── icons # プロジェクトのすべての svg アイコン
│ §── レイアウト # グローバルレイアウト
│ §── ルータ # ルーティング
│ §── ストア # グローバル ストア管理
│ §── スタイル # グローバル スタイル
│ §── utils # グローバル パブリック メソッド
│ §── ベンダー # パブリック ベンダー
│ §── ビュー # すべてのページの表示
│ §── App.vue # エントリーページ
│ §── main.js # コンポーネントの初期化などのエントリーファイル読み込み
│ └─ Permission.js # 権限管理
│ └─ settings.js # 設定ファイル
§── testing #テスト
§── . env.xxx # 環境変数の設定
§── .eslintrc.js # eslint の設定項目
§── .babelrc # babel-loader の設定
§── .travis.yml # 自動化された CI 設定
§── vue.config.js # vue-cli の設定
§── postcss。 config.js # postcss 設定
└── package.json # package.json
プロジェクトに取り組むときに最も注目するのは、すべてのソース コードとリソースが含まれる src ディレクトリであり、その他のディレクトリについては、すべてプロジェクト環境とツールの構成が含まれます。
1.3 バックエンド環境の構成
- IP とポートの構成:
開発段階では、config/index.js で proxyTable を構成し、現在 /api プロキシ バックエンドの IP + ポートを使用します。
運用環境では、/api プロキシ バックエンド (Nginx で構成) の IP + ポートも構成します。
コードは以下のように表示されます:
location /api/ {
proxy_pass http://139.196.59.97:8090/;
}
1.4 アイコン管理
Alibaba アイコン ライブラリ (http://iconfont.cn/home/index?spm=a313x.7781069.1998910419.2) にログインしてプロジェクトを作成します。
見つかったアイコンをショッピングカートに入れます
見つけたアイコンを個人プロジェクトに入れます
アイコンを紹介します
単一のアイコンは、上記の手順をスキップし、svg ファイルを直接ダウンロードし、src/icons/svg に配置して使用できます (js や css を導入する必要はありません)。
プロジェクトでは、メソッドを使用してマテリアルを直接インポートし、URL をデータにバインドすることもできます。
注: プロジェクトが ie と互換性がある必要がある場合は、svg イメージを使用せず、package.json 内の svg-sprite-loader を削除してください (svg-sprite-loader プラグインは ie ブラウザーと互換性がないため)。
2.1. SRC におけるプロジェクトの動作メカニズムとメインファイルコードの導入
目标
: 現在のテンプレートの基本的な操作メカニズムとインフラストラクチャを理解する
├── src # 源代码
│ ├── api # 所有请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── icons # 项目所有 svg icons
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
│ └── settings.js # 配置文件
main.js
1. VUE のインスタンス化
2. ルーティングのマウント
3. vuex-store のマウント
4. ElementUI の登録
5. ルートコンポーネント
import Vue from 'vue'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// import locale from 'element-ui/lib/locale/lang/en' // lang i18n
import '@/styles/index.scss' // global css
import App from './App'
import store from './store'
import router from './router'
import Components from '@/components'
import * as directives from '@/directives'
import * as filters from '@/filters'
import i18n from '@/lang'
import CheckPermission from '@/mixin/checkPermission'
import '@/icons' // icon
import '@/permission' // permission control
// set ElementUI lang to EN
// Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui,按如下方式声明
Vue.use(ElementUI, {
// element本身支持i18n的处理
// 此时 i18n就会根据当前的locale属性去寻找对应的显示内容
i18n: (key, value) => i18n.t(key) // t方法 会去对应的语言包里寻找对应的内容
// 改变locale的值 就可以改变对应的当前语言
})
// for in
Object.keys(directives).forEach(key => {
Vue.directive(key, directives[key]) // 注册自定义指令
})
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key]) // 注册自定义过滤器
})
// 注册自定义组件
Vue.use(Components)
// 全局混入检查对象
Vue.mixin(CheckPermission) // 表示所有的组件都拥有了检查的方法
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
i18n,
render: h => h(App)
})
** の部分をコメントアウトし、 mock数据
src 配下のmock
** フォルダーを削除してください。図に示すように、開発時にシミュレーション データは使用しません。
同時にvue.config.js
** ** : require('./mock/mock-server.js')の前をコメントアウトしてください。
app.vue
<template>
<div id="app">
<router-view /> //一级路由器
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
** # グローバル レイアウト**
\src\layout\index.vue
許可.js
src 配下には main.js の他に 2 つのファイル
permission.js
と **settings.js
**
permission.js
ページのログイン権限を制御するファイルですが、ここのコードは構築作業を経ないとわかりにくいので、ここのコードをコメントしてから権限関数をビルドする際に0から1まで構築していきます。
コメントコード
settings.js
一部のプロジェクト情報を設定するためのもので、title
(プロジェクト名)、fixedHeader
(固定ヘッダー)、sidebarLogo
(左メニューロゴの表示)
** settings.js
** 内のファイルは他の場所で参照されるため、当面はここでのファイルの変更は行いません。
module.exports = {
// 程序标题
title: 'Vue Admin 管理后台',
// 是否在右侧显示设置入口
showSettings: false,
// 是否显示标签多文档视图
tagsView: true,
// 是否固定标题头
fixedHeader: false,
// 是否显示侧边栏的LOGO
sidebarLogo: false,
/**
* @type {string | array} 'production' | ['production', 'development']
* @description Need show err logs component.
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production', 'development']
*/
errorLog: 'production'
}
Vuex の構造
現在の Vuex の構造は共有状態を管理するためにモジュール形式を採用しており、その構造は次のとおりです
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
Vue.use(Vuex)
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', true, /\.js$/)
// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
// set './app.js' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {
})
const store = new Vuex.Store({
modules,
getters
})
export default
店
このうち、app.js モジュールと settings.js モジュールは完全な機能を備えており、変更する必要はありません。user.js モジュールは、後の開発に集中する必要があるコンテンツなので、ここでは user.js のコンテンツを削除し、デフォルト設定をエクスポートします。
export default {
namespaced: true,
state: {
},
mutations: {
},
actions: {
}
}
同時に、ユーザー内の状態がゲッター内で参照されるため、ゲッター内の状態を次のように変更します。
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device
}
export default getters
import Vue from 'vue'
import Router from 'vue-router'
// 引入多个模块的规则
import approvalsRouter from './modules/approvals'
import departmentsRouter from './modules/departments'
import employeesRouter from './modules/employees'
import permissionRouter from './modules/permission'
import attendancesRouter from './modules/attendances'
import salarysRouter from './modules/salarys'
import settingRouter from './modules/setting'
import socialRouter from './modules/social'
import userRouter from './modules/user'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true if set true, item will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu
* if not set alwaysShow, when item has more than one children route,
* it will becomes nested mode, otherwise not show the root menu
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin','editor'] control the page roles (you can set multiple roles)
title: 'title' the name show in sidebar and breadcrumb (recommend set)
icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
}
*/
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/dashboard/index'),
meta: {
title: '首页', icon: 'dashboard' }
}]
},
{
path: '/import',
component: Layout,
hidden: true, // 不显示在左侧菜单中
children: [{
path: '', // 什么都不写表示默认的二级路由
component: () => import('@/views/import')
}]
},
userRouter // 放置一个都可以访问的路由
// 404 page must be placed at the end !!!
]
// 定义一个动态路由变量
// 这里导出这个变量 后面做权限的时候会用到
export const asyncRoutes = [
approvalsRouter,
departmentsRouter,
employeesRouter,
permissionRouter,
attendancesRouter,
salarysRouter,
settingRouter,
socialRouter
]
const createRouter = () => new Router({
mode: 'history', // require service support
base: 'hr/',
scrollBehavior: () => ({
y: 0 }),
routes: [...constantRoutes] // 静态路由和动态路由的临时合并
})
const router = createRouter() // 实例化一个路由
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
スクス
このプロジェクトでは、css の拡張言語としてscss
styles
も使用されています。** ** ディレクトリには、scss の関連ファイルがあります。関連する使用法については、次のセクションで説明します[
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
@import './btn.scss';
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
アイコン
アイコンの構成は以下の通りです
以上が vue-element-admin の基礎と導入ですが、この過程で基本的なテンプレート操作の仕組みを体験していただければ幸いです。
本节任务
: ディレクトリ構造と設計図に従って、上記の内容を誰もが理解します。
SCSS 処理の理解と使用
目标
: Scss プロセッサの仕様と使い方を理解して学習します。
まず第一に、sass とここでの scss の関係に注目してください。
Sass と scss は実際には ** 一样的
**css 前処理言語です。SCSS は Sass 3 で導入された新しい構文であり、その接尾辞はそれぞれ .sass と .scss です。
3.0 より前の SASS バージョンには接尾辞 .sass が付き、3.0 以降のバージョンには接尾辞 .scss が付きます。
両者は異なり、sass以降、scssの記述仕様は基本的にcssと同じですが、sassの時代には厳密なインデント仕様があり、「{}」や「;」はありません。
scss は css 仕様と一致しています。
パブリックリソースの画像とユニフォームのスタイル
目标
公開画像とスタイルのリソースを指定したディレクトリに配置します。
全体的な基本モジュールを簡単に紹介しましたが、次にプロジェクトで使用する画像やスタイルを統一する必要があります。
画像リソース
画像リソースは教材の画像ファイル内にあります。
common
** ** フォルダーを **assets
** ディレクトリにコピーするだけです。
スタイル
スタイルリソースは resource/style ディレクトリにあります
編集variables.scss
****
** common.scss
**を追加
variables.scss
** ** にいくつかの基本的な変数値を追加しました
common.scss
コンテンツ スタイルの一部が組み込まれたパブリック ** ** スタイルを提供します。これは、開発中にページ スタイルとレイアウトを迅速に実装するのに役立ちます。
2 つのファイルをstyleディレクトリに配置し、index.scss
** ** にスタイルをインポートします。
@import './common.scss'; //引入common.scss样式表
コードを送信する
本节注意
: scss ファイルでは、**@import** を通じて他のスタイル ファイルをインポートします。最後にセミコロンを追加することに注意する必要があります。そうしないと、エラーが報告されます。
本节任务
公共リソースの画像とスタイルを指定した位置に配置する
2.2 APIモジュールとリクエストカプセル化モジュールの導入
axios のインターセプタ原理 axios は
、ネットワーク リクエストのサードパーティ ツールとして、リクエストと応答をインターセプトできます。
2.1 アクシオスインターセプター
新しい axios インスタンスは create によって作成されます。
// 创建了一个新的axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // 超时时间
})
3.2 リクエストインターセプター
リクエストインターセプターは主にトークンを処理します ** 统一注入问题
**
// axios的请求拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['X-Token'] = getToken()
}
return config
},
error => {
return Promise.reject(error)
}
)
3.3 レスポンスインターセプター
応答インターセプターは主に、返された ** 数据异常
** および ** 数据结构
** の問題を処理します。
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 20000) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
ここでは、後でコードをよりわかりやすく書くために、元のコードをコメントアウトし、次のコードに置き換えます。
// 导出一个axios的实例 而且这个实例要有请求拦截器 响应拦截器
import axios from 'axios'
const service = axios.create() // 创建一个axios的实例
service.interceptors.request.use() // 请求拦截器
service.interceptors.response.use() // 响应拦截器
export default service // 导出axios实例
3.4 API モジュールの個別のカプセル化
私たちは習慣的にすべてのネットワークリクエストをAPIディレクトリに入れて一元管理し、モジュールごとに分割しています。
コードを個別にカプセル化する
import request from '@/utils/request'
export function login(data) {
return request({
url: '/vue-admin-template/user/login',
method: 'post',
data
})
}
export function getInfo(token) {
return request({
url: '/vue-admin-template/user/info',
method: 'get',
params: {
token }
})
}
export function logout() {
return request({
url: '/vue-admin-template/user/logout',
method: 'post'
})
}
上記のコードでは、カプセル化されたリクエスト ツールが使用されており、各インターフェイス リクエストには导出
個別のメソッドがあります。この利点は、任意の場所でリクエストが必要な場合に、エクスポートしたリクエスト メソッドを直接参照できることです。
将来の開発を改善するために、最初にuser.jsコードのメソッドを空に設定し、後で修正することができます。
// import request from '@/utils/request'
export function login(data) {
}
export function getInfo(token) {
}
export function logout() {
}
コードを送信する
本节任务
: リクエストとユーザー モジュールのコードをクリーンアップし、リクエストとモジュールのカプセル化を理解する
import request from '@/utils/request'
export function login(data) {
return request({
url: '/vue-admin-template/user/login',
method: 'post',
data
})
}
export function logout() {
return request({
url: '/vue-admin-template/user/logout',
method: 'post'
})
}
3.5 ルーティング設定仕様
管理プラットフォームとして使用する場合、サイドバー Sidebar はメニューバー表示として使用され、メニューで発生する問題は次のとおりです。
ブレッドクラムの第 1 レベルのメニューが第 2 レベルのメニューに表示されない場合は、解決する子の最初の path="" を設定します。
router.js で静的ルートを定義する場合、同じ名前を使用することはできません。同じ名前を使用すると、TagView コンポーネントのクローズ関数で異常が発生します (サブルートの名前が同じだと、リフレッシュ ページのルートが変更されます)。
router.js で動的ルートを定義する場合、親ルートの名前と子ルートの子ルートの名前が同じであってはならず、また親ルートの名前が欠落していてはなりません。そうでない場合、 this.$router.push( ) 最初の子ルートを開くと、別の画面が開きます
また、現在の動的メニューの実装ロジックは、すべてのフロントエンド ページのルーティング テーブルを router/index に定義することであり、サイドバーに表示されますが、それ以外の場合は表示されません (関連するコードはストアにあります) /modules/permission.js); 純粋な動的メニュー実装に変更することもできます フロントエンドはルーティング テーブルを維持しなくなり、バックグラウンドのマルチレベル メニュー関係データを通じて完全にルーティング テーブルを動的に生成しますフロントエンド。
静的ページをプロジェクトに追加するだけの場合は、router/index.js での通常のルーターの記述方法に従ってください。
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRouterMap = [
{
path: '/',
redirect: '/nextShow',
name: '首页',
hidden: true
},
{
path: '/nextShow',
hidden:false,
component: _import('product/nextShow')
},
{
path: '/next',
hidden:false,
component: _import('product/next')
}
]
export default new Router({
scrollBehavior: () => ({
y: 0 }),
routes: constantRouterMap
})
3. ログインモジュール
3.1 固定のローカルアクセスポートとWebサイト名の設定
ローカルサービスポート:vue.config.js
に設定
vue.config.js は
、vue プロジェクトに関連するコンパイル、構成、パッケージ化、およびサービスの開始に関する設定ファイルであり、その中核は webpack にありますが、webpack の改良版に相当する webpack とは異なります。
// If your port is set to 80,
// use administrator privileges to execute the command line.
// For example, Mac: sudo npm run
// You can change the port by the following methods:
// port = 9528 npm run dev OR npm run dev --port = 9528
const port = process.env.port || process.env.npm_config_port || 9528 // dev port
プロジェクトの下に、.env.development と .env.production という 2 つのファイルが見つかりました。development
=> 開発環境
production => 実稼働環境
開発とデバッグのために npm run dev を実行すると、.env.development がロードされて実行されます。ファイルの内容は
次のとおりです。 npm run build:prod を実行して運用環境をパッケージ化すると、.env.production ファイルの内容がロードされて実行されます。
3.2 ウェブサイト名
Web サイト名は実際には、configureWebpack オプションの name オプションに含まれており、コードを読むと、名前が実際にはsrc ディレクトリ内のsettings.js
ファイルに由来していることがわかります。
module.exports = {
// 程序标题
title: 'Vue ERP 管理后台',
// 是否在右侧显示设置入口
showSettings: false,
// 是否显示标签多文档视图
tagsView: true,
// 是否固定标题头
fixedHeader: false,
// 是否显示侧边栏的LOGO
sidebarLogo: false,
/**
* @type {string | array} 'production' | ['production', 'development']
* @description Need show err logs component.
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production', 'development']
*/
errorLog: 'production'
}
3.3. ログインページの基本レイアウト
1 ヘッダーの背景を設定する
<div class="title-container">
<h3 class="title">
<img src="@/assets/common/login-logo.png" alt="">
</h3>
</div>
2 背景画像を設定する
.login-container {
background-image: url("https://img0.baidu.com/it/u=3612597965,1770541226&fm=26&fmt=auto&gp=0.jpg");
background-position: center;
background-size: 100% 100%;
}
3.4. ログインフォームの検証
1 ユーザー名とパスワードの確認
loginRules: {
username: [{
required: true, trigger: 'blur', validator: validateUsername }],
password: [
{
required: true, trigger: 'blur' },
{
min: 6, max: 12, trigger: 'blur', message: '密码长度应该在6-12位之间' }
]
},
2 修飾子について
@keyup.enter はキー修飾子に属します。Enter キーが押されたときのトリガーを監視したい場合は、次のように記述できます。
<input v-on:keyup.enter="submit">
3.5.Vue-Cl.i によるクロスドメイン プロキシの構成
1 クロスドメインはなぜ発生するのですか?
現在、最も一般的な方法は、フロント プロジェクトとバック エンド プロジェクトを分離することです。つまり、フロントエンド プロジェクトとバックエンド インターフェイスが同じドメイン名の下にないため、フロントエンドのクロスドメイン動作が必要になります。プロジェクトを使用してバックエンド インターフェイスにアクセスします。
2 開発環境のクロスドメイン問題を解決する 開発
環境のクロスドメイン、つまり vue-cli スキャフォールディング環境でサービスを開発して開始するとき、インターフェースにアクセスするときに遭遇するクロスドメイン問題、 vue-cli はローカルでサービスを開きます。このサービスを使用すると、リクエストをプロキシし、クロスドメインの問題を解決できます。
proxy: {
'/api/private/v1/': {
target: 'http://127.0.0.1:8888', // 跨域请求的地址
changeOrigin: true // 只有这个值为true的情况下 才表示开启跨域
}
}
3.6. 別のログインインターフェースをカプセル化する
export function login(data) {
return request({
url: 'login',
method: 'post',
data
})
}
3.7 Vuex のログイン アクションとプロセス トークンをカプセル化する
3.7.1 store/modules/user.jsの基本設定を実装する
// 状态
const state = {
}
// 修改状态
const mutations = {
}
// 执行异步
const actions = {
}
export default {
namespaced: true,
state,
mutations,
actions
}
3.7.2 トークンの共有状態の設定
const state = {
token: null
}
3.7.3 オペレーショントークン
utils/auth.js の基本テンプレートには、トークンの取得、トークンの設定、トークンの削除のメソッドが用意されており、これらを直接使用できます。
const TokenKey = 'xiaoyou'
export function getToken() {
return localStorage.getItem(TokenKey)
}
export function setToken(token) {
return localStorage.setItem(TokenKey, token)
}
export function removeToken() {
return localStorage.removeItem(TokenKey)
}
import Cookies from 'js-cookie'
const TokenKey = 'vue_admin_template_token'
const UserIdKey = 'vue_admin_template_userid'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
// 由于store/modules/user.js里面getDefaultState需要获得用户ID
// 否则刷新获取不到userid信息导致无法获得用户信息,因此和token一样处理userid
export function getUserId() {
return Cookies.get(UserIdKey)
}
export function setUserId(userid) {
return Cookies.set(UserIdKey, userid)
}
3.7.4 トークンステータスの初期化 - store/modules/user.js
import {
getToken, setToken, removeToken } from '@/utils/auth'
const state = {
token: getToken() // 设置token初始状态 token持久化 => 放到缓存中
}
3.7.5 トークンを変更するミューテーションを提供する
const mutations = {
setToken(state, token) {
state.token = token // 设置token 只是修改state的数据 123 =》 1234
setToken(token) // vuex和 缓存数据的同步
},
removeToken(state) {
state.token = null // 删除vuex的token
removeToken() // 先清除 vuex 再清除缓存 vuex和 缓存数据的同步
}
}
3.7.6 ログインアクションのカプセル化
ログイン アクションで行うこと、ログイン インターフェイスを呼び出し、成功後にトークンを vuex に設定し、失敗した場合は失敗を返す
const actions = {
async login(context, data) {
const result = await login(data) // 实际上就是一个promise result就是执行的结果
if (result.data.success) {
context.commit('setToken', result.data.data)
}
}
}
3.7.7 異なる環境での axios のリクエストベースアドレスを区別する
.env.development と .env.production で変数を定義でき、その変数は自動的に現在の環境の値になります。基本
テンプレートは、上記のファイルで変数 VUE_APP_BASE_API を定義し、axios によって要求されるベース URL として使用できます。テンプレートでは、2 つの値は /dev-api と /prod-api です。
3.7.8 axiosのレスポンスインターセプタの処理
service.interceptors.response.use(response => {
const {
success, message, data } = response.data
if (success) {
return data
} else {
Message.error(message) // 提示错误消息
return Promise.reject(new Error(message))
}
}, error => {
Message.error(error.message) // 提示错误信息
return Promise.reject(error) // 返回执行错误 让当前的执行链跳出成功 直接进入 catch
})
3.8 (重要なポイント) ログイン要求を行う
ログイン ロジック: ユーザーがアカウント パスワードを入力した後、それが正しいかどうかをサーバーに検証します。検証に合格すると、サーバーはトークンを返します。トークンを取得した後 (トークンは Cookie に保存して、確実にトークンを取得できます)ページが更新された後もユーザー ログインを記憶できる) 状態)、フロントエンドはトークンに従って user_info インターフェイスをプルし、ユーザーの詳細 (ユーザー権限、ユーザー名など) を取得します。
認可の検証: トークンを通じてユーザーの対応する役割を取得し、ユーザーの役割に従って対応する許可されたルートを動的に計算し、
router.addRoutes を通じてこれらのルートを動的にマウントします。
まず、ログインの具体的なプロセスを見てみましょう。
this.$store が表示されます。これが表示されたら、store ディレクトリに移動して、
src\views\login\index2.vueを探します。
handleLogin() {
// 处理密码登录事件
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store
.dispatch('user/login', this.loginForm)
.then(() => {
this.$router.push({
path: this.redirect || '/' })
this.loading = false
})
.catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
},
src\store\modules\user.js
は、指定されたアドレス user の前のセクションに従って見つかり、次にユーザーを見つけ、
後者に従って対応する関数名が見つかります。
これについて言えば、プロジェクトの構造は比較的明確になっているはずです。それでもわかりにくいと感じる場合は、プロジェクトの構造に対する理解が十分ではないことを示しているだけです。
const actions = {
// user login //处理登录业务
login({
commit }, userInfo) {
// 常规登陆方式
//userinfo是表单传过来的对象
const {
username, password } = userInfo
return new Promise((resolve, reject) => {
//发送网络请求,进行登录操作
user.login({
username: username.trim(), password: password }).then(response => {
const {
result } = response // 获取返回对象的 result
// console.log(result)// 记录数据
var token = result.accessToken // 用户令牌
var userId = result.userId // 用户id
// 修改State对象,记录令牌和用户Id
commit('SET_TOKEN', token)
commit('SET_USERID', userId)
// 存储cookie
setToken(token)
setUserId(userId)
resolve()
}).catch(error => {
reject(error)
})
})
},
さて、ログイン機能が再び表示されましたが、これはどこから来たのでしょうか?
'@/api/system/user' からユーザーをインポートします。 '@/api/system/tokenauth' からトークン認証を
インポートします。 import { getToken, setToken,removeToken, getUserId, setUserId } '@/utils/auth' から
インポート ルーター、{ replaceRouter } '@/router' から
、すべてのインターフェイスが API から来ていることがわかります。パンツをはいた大男のモードに従って、API ディレクトリの直下でユーザーを見つけるだけです。
import request from '@/utils/request'
import BaseApi from '@/api/base-api'
// 业务类自定义接口实现, 通用的接口已经在BaseApi中定义
class Api extends BaseApi {
login(data) {
return request({
url: '/abp/TokenAuth/Authenticate',
method: 'post',
data: {
UsernameOrEmailAddress: data.username,
password: data.password
}
})
}
getInfo(id) {
return request({
url: '/abp/services/app/User/Get',
method: 'get',
params: {
id }
})
}
logout() {
// return request({
// url: '/api/user/logout',
// method: 'post'
// })
var p = new Promise(function(resolve, reject) {
// 做一些异步操作
setTimeout(function() {
resolve({
code: 20000,
data: 'success'
});
}, 200);
});
return p;
}
これだけ?いいえ、いいえ...
直接リクエスト({xxxxx})を返します。リクエストは何ですか? それはどこから来たのか?
引き続き utils ディレクトリに直接移動し、パンツをはいた大男のパターンに従ってリクエストを見つけます。
- リクエストは何ですか? (もちろんリクエストを送ったのはサラです)
- request はパッケージ化された axios で、主にネットワーク リクエストの送信に使用されます。
- 中身がとても複雑なので、理解できなかったらどうすればいいでしょうか?
- それは問題ではありません。データの取得方法を知っておく必要があるだけです
API のログイン メソッドは次のとおりです。上記のインポートでは、utils の下にリクエストが導入されています。本質的には、request.js を呼び出します。request.js は axios (統合されたリクエスト インターセプトとレスポンス インターセプト) をカプセル化し、バックグラウンド インターフェイスをリクエストするために使用されます。インターフェース要求が成功した場合は、login.vue ページの .then() メソッド ルートに戻り、ログイン ページにジャンプします。
import axios from 'axios'
import {
Message } from 'element-ui'
import store from '@/store'
import {
getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
// baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request 请求拦截
service.interceptors.request.use(
config => {
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['X-Token'] = getToken()
config.headers['Authorization'] = 'Bearer ' + getToken()
}
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
response => {
const res = response.data
return res;
},
error => {
if (error.response) {
var custErr = error.response.data.error
if (custErr) {
console.log('error:' + custErr.details)// for debug
Message({
message: custErr.message,
type: 'error',
duration: 10 * 1000,
showClose: true
})
} else {
console.log('err:' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000,
showClose: true
})
}
}
return Promise.reject(error)
}
)
export default service
バックグラウンドから返される値にはコードコードが必要と規定されています。ここで華パンツさんは20000を指定するのが普通です。これを自分で定義した通常の値に変更するだけです。たとえば、私の場合は 0 (実際のバックエンドが優先されます)
3.9 全体的なログインプロセス
4. 権限管理
認可の検証: トークンを介してユーザーの対応するロールを取得し、ユーザーのロールに従って対応する認可されたルートを動的に計算し、
router.addRoutes
これらのルートを を介して動的にマウントします。エントリ コードはpermission.js
以下にあり、ルーターのインターセプターは許可インターセプトの実装に使用されます。
パーミッションは主にグローバル ルーティング ガードとログイン判定を担当します。インターセプタとして理解できます。具体的なロジックを見てみましょう。
4.1 Permission.jsの説明
コードの観点からは、主にトークンありとトークンなしの 2 つの状況があります。
- トークンがあります: ログイン ページに移動しているかどうかを確認してください。ログイン ページはブロックされていてはなりません。ログイン ページの場合は、直接移動してもかまいません。ログイン ページではない場合は、ローカルにユーザー情報があるかどうかを確認し、Cookie (トークンである必要はなく、ローカル ストレージの場合もあります) にユーザー情報があるかどうかを確認します。ユーザー情報がある場合はそのままにしておきます。ユーザー情報がない場合は、インターフェイスを呼び出してログイン情報を取得します。router.addRoutes では、ルートを動的に追加します。ユーザー情報を取得後リリースします。ユーザー情報の取得処理中にエラーが報告された場合は、ログインページに戻ってください。
- トークンなし: まず、ユーザーが入力しようとしているページがホワイト リストにあるかどうかを確認します。一般に、ログイン、登録、パスワードを忘れた場合はすべてホワイト リストに含まれます。これらのページは、トークンなしで直接許可することもできます。ホワイトリストにない場合は、ログイン ページに戻ります。
import router from './router'
import store from './store'
import {
Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import {
getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({
showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // 白名单,在白名单当中的路由可以免登录,直接进入。
//比较常见的使用场景是进入登陆界面或者是进入扫码下载界面
router.beforeEach(async(to, from, next) => {
//全局前置守卫,当有路由进行跳转时就会进入这个守卫,
//这个守卫方法接收三个参数:
//to: Route: 即将要进入的目标 路由对象
//from: Route: 当前导航正要离开的路由
//next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
NProgress.start() // 开始加载进度条
document.title = getPageTitle(to.meta.title)//设置页面标题
const hasToken = getToken() //判断用户是否登录,也就是是否能获得token值,
//在实际开发当中有token值就意味着已经登录了
if (hasToken) {
//如果有token值(用户登录了!)
if (to.path === '/login') {
//如果路由要跳转到登录页面
next({
path: '/' }) //界面会重定向到首页,
//这种场景不是应用于退出登录的,一般是用于因为有人在路径当中直接输入/login来进行路由跳转,
//然后就会重定向回首页
NProgress.done() //进度条结束
} else {
// 否则路由要跳转到其他界面,比如首页
const hasGetUserInfo = store.getters.name // 去vuex仓库拿取用户名字
if (hasGetUserInfo) {
// 如果拿取到了用户的名字信息就直接让它跳转到下一个路由
next() // 跳转到下一个路由
} else {
//否则
try {
await store.dispatch('user/getInfo') // 触发vux仓库的获取用户信息的事件,获取用户信息
next() // 成功获取到用户信息,跳转到下一个路由
} catch (error) {
// 获取用户信息失败,进入以下一级
await store.dispatch('user/resetToken') // 获取用户信息失败后,就删除token
Message.error(error || 'Has Error') //提示相应的错误
next(`/login?redirect=${
to.path}`) //并跳转回登录界面,重新登录
NProgress.done() // 进度条结束
}
}
}
} else {
// 进入这一级就意味着没有获得token,也就是没有登录
if (whiteList.indexOf(to.path) !== -1) {
//进行遍历如果要去往的路由在白名单内
next() // 就允许直接跳转
} else {
//否则,说明要去往的路由不在白名单内而且用户也没登录
next(`/login?redirect=${
to.path}`) // 那么,哦里给,滚回登录页去吧,或者只能留在登录页
NProgress.done() //结束了,bor
}
}
})
router.afterEach(() => {
//全局后置钩子
NProgress.done()
})
ページの更新によって発生する vuex でのセッション状態の損失の問題には、2 つの解決策があります。
App.vue のリスナーを通じてこの問題を解決します。これは、更新する前にブラウザのメモリにデータを保存するというものです。ページを更新した後、以前のデータをメモリに再度読み込みます。
getToken()判定後、hasLoginがあれば再度トークンデータがあるか確認し、無い場合はインターフェースを呼び出してデータを取得し、vuexに保存します
4.2 動的権限ルーティング、サイドバーメニューの表示
まず、要素管理フレームワークの実装について説明します。現在のユーザーの権限を取得してルーティング テーブルを比較し、現在のユーザーの権限でアクセスできるルーティング テーブルを生成し、それを に動的にマウントし
router.addRoutes
ますrouter
。フロントエンドで権限を制御します。
しかし、実際には、多くの企業のビジネス ロジックはこのようなものではない可能性があります。例を挙げると、多くの企業の要件は、このプロジェクトのハードコーディングされたプリセットとは異なり、各ページの権限が動的に構成されることです。しかし実際には原理は同じです。たとえば、バックグラウンドでツリー コントロールまたはその他のプレゼンテーション フォームを使用して各ページのアクセス許可を動的に構成し、このルーティング テーブルをバックエンドに保存できます。ユーザーがログインすると
roles
、フロントエンドはロールに従ってバックエンドにアクセス可能なルーティングテーブルを要求し、それによってアクセス可能なページを動的に生成し、router.addRoutes
それらをルーターに動的にマウントします。
バックエンドのリターン ルーティング テーブルをローカル コンポーネントにマップするのは、もう 1 つのステップにすぎません。
const map={
login:require('login/index').default // 同步的方式
login:()=>import('login/index') // 异步的方式
}
//你存在服务端的map类似于
const serviceMap=[
{
path: '/login', component: 'login', hidden: true }
]
//之后遍历这个map,动态生成asyncRoutes
并将 component 替换为 map[component]
このルートには 2 つのルーティング構成があることに注意してください。
1 つは許可のない固定ルーティング構成で、
2 つ目はユーザーの権限に応じてサイドバーを表示する許可付きのルーティング構成です。
上記のコンポーネントマップは実際には、権限に従って動的にロードする必要があるルーティング テーブルです。
まず、サイドバーがどのようにレンダリングされるかを理解する必要があります。layout/components/slibar/index.vue のコードを見てください。
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
src\store\getters.js
const getters = {
sidebar: state => state.app.sidebar,
language: state => state.app.language,
size: state => state.app.size,
device: state => state.app.device,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
userid: state => state.user.userid,
introduction: state => state.user.introduction,
roles: state => state.user.roles,
roleNames: state => state.user.roleNames,
permits: state => state.user.permits,
permission_routes: state => state.permission.routes,
errorLogs: state => state.errorLog.logs
}
export default getters
知らせ:
mapGetters ヘルパー関数は、ストア内のゲッターをローカルで計算されたプロパティにマップするだけです。
このpermission_routesは実際にはルートのメタ情報であり、配列です。
ユーザー情報を取得すると、ユーザーが所有するロールを取得し、そのロールに従って動的に追加された一致するルートをフィルターで除外します。つまり、上記の Permission.js 内のルートは、実際には上記 2 つのアクセス許可で構成される配列です
。 GenerateRoutes メソッドで、store/permission/generateRoutes のコードを見てみましょう。
@/router/index からの constantRoutes、componentMap が最初に導入されました
GenerateRoutes メソッドでは、バックグラウンドから取得したメニュー リストがツリー形式に変換されていることがわかります (実際、この部分は、parentId に従ってバックグラウンドで再帰的にクエリできます)。ツリー データ構造を利用可能なルーティング形式に変換し、vuex に保存する必要があります。ここではまずマップにフロントエンドコンポーネントを書き込みます。キーはルート名です。ルートを生成するためにトラバースするときにキーを押してコンポーネントを取り出し、背景がルート名を返した後、このマップに移動します。 RouteMap['user'] などと一致します。
概要: ここでは、元の取得ルーティング コンポーネントのロジックを、元のルート/インデックスからバックグラウンドからの非同期フェッチに変更し、バックグラウンドからフェッチした後、コンポーネントに結合します。
次に、これら 2 つの文を呼び出します
router.addRoutes(store.getters.addRouters); // アクセス可能なルーティング テーブルを動的に追加します
router.options.routes=store.getters.routers;
router.addRoutes() メソッドは、ルーティング設定を動的に追加するメソッドであり、パラメータはルーティング設定に準拠した配列であり、ルーティング メタ情報は、ルーティング メタ情報に使用する必要があるため、マージされたルーティング メタ情報に変換されます。サイドバーをレンダリング中!
5. ホームページモジュール
5.1. ホーム ページの左側のナビゲーション スタイル
左側のナビゲーションコンポーネントのスタイルファイルstyles/siderbar.scss
5.2.1 左側のナビゲーション背景画像を設定する
.scrollbar-wrapper {
background: url('~@/assets/common/leftnavBg.png') no-repeat 0 100%;
}
5.2.2 左のロゴ画像の表示 src/settings.js
module.exports = {
title: '小优电商后台管理系统',
fixedHeader: false,
sidebarLogo: true // 显示logo
}
5.2.3 ヘッダー画像構造の設定
src/layout/components/Sidebar/Logo.vue
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link key="collapse" class="sidebar-logo-link" to="/">
<img src="@/assets/common/logo.png" class="sidebar-logo ">
</router-link>
</transition>
</div>
5.2. ヘッダーコンテンツのレイアウトとスタイルを設定する
ページは図のように設定する必要があります
5.2.1 ヘッドコンポーネントの場所layout/components/Navbar.vue
<div class="app-breadcrumb">
北京小优智慧城市科技有限公司
<span class="breadBtn">V1.0</span>
</div>
5.2.2 右のドロップダウン メニューの設定
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img src="@/assets/common/bigUserHeader.png" class="user-avatar">
<span class="name">管理员</span>
<i class="el-icon-caret-bottom" style="color:#fff" />
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<router-link to="/">
<el-dropdown-item>
首页
</el-dropdown-item>
</router-link>
<a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/">
<el-dropdown-item>项目地址</el-dropdown-item>
</a>
<el-dropdown-item divided @click.native="logout">
<span style="display:block;">退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
5.3. ユーザー情報の保存
5.3.1 変数 userInfo アクションの追加 src/store/modules/user.js
state: {
token: getToken(),
userInfo: {
} // 用于存储用户对象信息
},
5.3.2 ユーザープロファイルミューテーションの設定と削除
// 设置用户信息
setUserInfo(state, userInfo) {
state.userInfo = userInfo
},
// 删除用户信息
removeUserInfo(state) {
state.userInfo = {
}
}
5.3.3 ユーザー名マッピング src/store/getters.js の作成
// 设置用户信息
setUserInfo(state, userInfo) {
state.userInfo = userInfo
},
// 删除用户信息
removeUserInfo(state) {
state.userInfo = {
}
}
5.3.4 ヘッダーメニューの名前を実際のユーザー名に置き換えます
<div class="avatar-wrapper">
<img src="@/assets/common/bigUserHeader.png" class="user-avatar" />
<span class="name">{
{
username }}</span>
<i class="el-icon-caret-bottom" style="color: #fff" />
</div>
5.4. カスタムディレクティブ
グローバル登録カスタム ディレクティブの構文 - Get Focus ディレクティブ
Vue.directive('focus', {
inserted: function (el) {
console.log(el.children[0])
el.children[0].focus()
}
})
このディレクティブをログインコンポーネントで使用します
<el-input
ref="mobile"
v-model="loginForm.username"
v-focus
placeholder="手机号"
name="mobile"
type="text"
tabindex="1"
/>
5.5. ログアウト機能の実装
5.5.1 ログアウトアクション src/store/modules/user.js
logout(context) {
// 删除token
context.commit('removeToken') // 不仅仅删除了vuex中的 还删除了缓存中的
// 删除用户资料
context.commit('removeUserInfo') // 删除用户信息
}
5.5.2 突然変異
removeToken(state) {
state.token = null // 将vuex的数据置空
removeToken() // 同步到缓存
},
removeUseInfo(state) {
state.userInfo = {
}
}
5.5.3 ヘッドメニュー呼び出しアクション src/layout/components/Navbar.vue
async logout() {
await this.$store.dispatch('user/logout') // 这里不论写不写 await 登出方法都是同步的
this.$router.push(`/login`) // 跳到登录
}
5.6. トークン障害への積極的な介入
5.6.1 フローチャート変換コード
リクエストのカプセル化
コードは src/utils/request/Util.js にあります。ここで、バックグラウンド インターフェイス プロトコルに従って、content-type 属性と Accept (response-type) 属性が設定されることに注意してください。現在使用されている content-type は次のとおりです。 2 つのフォームまたは application/json さらに、
ファイルのダウンロードなどのインターフェイスの場合、フォームの送信によって新しいウィンドウのみを開くことができ、インターフェイスはダウンロードを実現するためにバイト ストリームを返すため、この共通インターフェイスは使用できないことに注意してください。インターフェイスの調整に使用されるため、コード内で process.env.BASE_URL を使用して、ダウンロード インターフェイスを呼び出すようにハードコードされたインターフェイスの IP+ポートにアクセスします。リクエスト インターセプターは
、トークンを HTTP リクエスト ヘッダーに追加します。
さらに、バックエンド インターフェイスで 404 や 403 などのステータス コードを直接返さないでください。これにより、データがエラー コールバック関数内にあるため、レスポンス インターセプタが応答を返すのに不便になります。例外を均一に処理します。
すべてのバックエンド インターフェイス (ダウンロード ファイルを除く) は src/api でインターフェイス URL を宣言し、それをビジネスにインポートして使用する必要があります (メンテナンスやビジネスからの切り離しに便利です)
src/utils/request.js
const timeKey = 'hrsaas-timestamp-key' // 设置一个独一无二的key
// 获取时间戳
export function getTimeStamp() {
return Cookies.get(timeKey)
}
// 设置时间戳
export function setTimeStamp() {
Cookies.set(timeKey, Date.now())
}
5.6.2 src/utils/request.js
import axios from 'axios'
import store from '@/store'
import router from '@/router'
import {
Message } from 'element-ui'
import {
getTimeStamp } from '@/utils/auth'
const TimeOut = 3600 // 定义超时时间
const service = axios.create({
// 当执行 npm run dev => .evn.development => /api => 跨域代理
baseURL: process.env.VUE_APP_BASE_API, // npm run dev => /api npm run build => /prod-api
timeout: 5000 // 设置超时时间
})
// 请求拦截器
service.interceptors.request.use(config => {
// config 是请求的配置信息
// 注入token
if (store.getters.token) {
// 只有在有token的情况下 才有必要去检查时间戳是否超时
if (IsCheckTimeOut()) {
// 如果它为true表示 过期了
// token没用了 因为超时了
store.dispatch('user/logout') // 登出操作
// 跳转到登录页
router.push('/login')
return Promise.reject(new Error('token超时了'))
}
config.headers['Authorization'] = store.getters.token
}
return config // 必须要返回的
}, error => {
return Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(response => {
// axios默认加了一层data
const {
success, message, data } = response.data
// 要根据success的成功与否决定下面的操作
if (success) {
return data
} else {
// 业务已经错误了 还能进then ? 不能 ! 应该进catch
Message.error(message) // 提示错误消息
return Promise.reject(new Error(message))
}
}, error => {
Message.error(error.message) // 提示错误信息
return Promise.reject(error) // 返回执行错误 让当前的执行链跳出成功 直接进入 catch
})
// 超时逻辑 (当前时间 - 缓存中的时间) 是否大于 时间差
function IsCheckTimeOut() {
var currentTime = Date.now() // 当前时间戳
var timeStamp = getTimeStamp() // 缓存时间戳
return (currentTime - timeStamp) / 1000 > TimeOut
}
export default service
5.6.2 同様に、ログイン時に、ログインが成功した場合はタイムスタンプを設定します
async login(context, data) {
const result = await login(data) // 实际上就是一个promise result就是执行的结果
context.commit('setToken', result)
setTimeStamp() // 将当前的最新时间写入缓存
}
6. ルーティングとページ
複雑なミドルエンド プロジェクトには多くのページがあるため、管理とメンテナンスのためにすべてのビジネスを 1 つのファイルに集中させることは不可能です。そして最も重要なのは、フロントエンド ページは主に 2 つの部分に分かれており、1 つの部分は誰でもアクセスできることです。複数のモジュールを分割すると、制御が容易になります。
6.1. 新しいルートの作成
router ディレクトリの下に新しいディレクトリ モジュールを作成し、このディレクトリ内に各ルーティング モジュールを作成します
ルーティング モジュールのディレクトリ構造
6.2. 各モジュールのルーティングルールを設定する
// 导出属于用户的路由规则
import Layout from '@/layout'
export default {
path: '/user', // 路径
name: '', // 给路由规则加一个name
component: Layout, // 组件
// 配置二级路的路由表
children: [{
path: '', // 这里当二级路由的path什么都不写的时候 表示该路由为当前二级路由的默认路由
name: 'user', // 给路由规则加一个name
component: () => import('@/views/Users'),
// 路由元信息 其实就是存储数据的对象 我们可以在这里放置一些信息
meta: {
title: '用户管理' // meta属性的里面的属性 随意定义
}
}]
}
6.3. 静的ルーティングと動的ルーティングが一時的に結合されて左側のメニューが形成されます
一時合併とは何ですか?
動的ルーティングにはアクセス権限が必要ですが、動的ルーティングの権限へのアクセスは非常に複雑です。まず、権限に関係なく、静的ルーティングと動的ルーティングを組み合わせて、後でこの問題を解決します。ルーティング メイン ファイル src/router/index
.js
// 引入多个模块的规则
import Layout from '@/layout'
import userRouter from './modules/user'
import roleRouter from './modules/role'
import rightsRouter from './modules/right'
import goodsRouter from './modules/goods'
import categoryRouter from './modules/category'
import reportsRouter from './modules/report'
**// 动态路由**
export const asyncRoutes = [
userRouter, roleRouter, rightsRouter, goodsRouter, categoryRouter, reportsRouter
]
const createRouter = () => new Router({
scrollBehavior: () => ({
y: 0 }), // 管理滚动行为 如果出现滚动 切换就让 让页面回到顶部
routes: [...constantRoutes, ...asyncRoutes]
})
このシステム
// 合并组件模块的路由到一起
export const asyncRoutes = Object.assign(routes, componentsRouter)
7. コンテンツモジュール
7.1. 時刻フォーマットの処理を解決するフィルター
Vue.js を使用すると、一般的なテキストの書式設定に使用できるフィルターをカスタマイズできます。フィルターは、二重中括弧補間と v-bind 式の 2 つの場所で使用できます (後者は 2.1.0 以降でサポートされています)。フィルターは、「パイプ」記号で示される JavaScript 式の最後に追加する必要があります。
<el-table-column label="入职时间" sortable prop="timeOfEntry">
<template slot-scope="obj">{
{
obj.row.timeOfEntry | 过滤器}}</template>
</el-table-column>
インストールの瞬間
npm i moment
ライトフィルター機能
import moment from 'moment'
export function formatTime(value) {
return moment(value * 1000).format('YYYY-MM-DD HH:mm:ss')
}
main.js にフィルターをグローバルに登録する
import * as filters from '@/filters'
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key])
})
7.2. 新しいユーザーの追加
views/user ディレクトリにポップアップ レイヤー コンポーネント src/views/user/components/add-user.vue を作成します。
<template>
<el-dialog title="新增用户" :visible.sync="dialogVisible" width="50%">
<el-form ref="form" :rules="rules" :model="userForm" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="userForm.password" />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="userForm.mobile" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email" />
</el-form-item>
<el-form-item label="部门" prop="department_title">
<el-input v-model="userForm.department_title" @focus="getAllDepartment"/>
<el-tree v-if="showTree" v-loading="loading" :data="treeData" :props="{
label: 'department_title' }" @node-click="handleNodeClick"/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="btnCancel">取 消</el-button>
<el-button type="primary" @click="saveUser">确 定</el-button>
</span>
</el-dialog>
</template>
<script>
import {
getDepartMent } from '@/api/department'
import {
tranListToTreeData } from '@/utils'
import {
addUser } from '@/api/user'
export default {
data() {
return {
treeData: [], // 存储部门的树形数据
showTree: false, // 部门文本框获取焦点时,设置为true,展示部门信息
loading: false, // 显示或隐藏进度
dialogVisible: false,
userForm: {
username: '',
password: '',
email: '',
mobile: '',
department_id: '',
department_title: ''
},
rules: {
username: [
{
required: true, trigger: 'blur', message: '用户名不能为空' },
{
min: 6, max: 10, trigger: 'blur', message: '长度在6-10位之间' }
],
password: [
{
required: true, trigger: 'blur', message: '密码不能为空' },
{
min: 6, max: 12, trigger: 'blur', message: '长度在6-12位之间' }
],
mobile: [
{
required: true, trigger: 'blur', message: '手机号不能为空' },
{
pattern: /^1[3-9]\d{
9}$/, trigger: 'blur', message: '手机号格式不正确' }
],
email: [
{
required: true, trigger: 'blur', message: '邮箱不能为空' },
{
pattern: /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{
2,3}$/,trigger: 'blur', message: '邮箱格式不正确' }
],
department_title: [{
required: true, message: '部门不能为空', trigger: 'change' }]
}
}
},
methods: {
onSubmit() {
}
}
}
</script>
親コンポーネント、ポップアップレイヤーで参照される
import AddUser from './components/add-user'
<add-user ref="adduser" />
「ユーザーの追加」ボタンをクリックして、ポップアップレイヤーをポップアップ表示します。
<el-button size="small" type="primary" @click="adduser">新增用户</el-button>
ボタンをクリックしてポップアップレイヤーを表示するキーは、コンポーネントの el-dialog コンポーネントにある次のプロパティの値を設定することです。
:visible.sync="dialogVisible"
ボタンは親コンポーネントにあり、変数dialogVisibleは子コンポーネントにあります。これを変更するにはどうすればよいですか?
サブコンポーネントの props に新しいプロパティを作成できます
dialogVisible
次に、親コンポーネントで値を割り当てます。
最後に、親コンポーネントのデータに変数を定義します。
addDialogVisible:false
しかし、上記の解決策には問題があります。ダイアログ ボックスの右上隅にある [X] をクリックするか、[キャンセル] ボタンをクリックするか、他の領域をクリックしてダイアログ ボックスを閉じると、次のエラーがスローされます。
**エラーの理由: **上記の操作を実行すると、props の DialogVisible 変数の値が自動的に変更されますが、これは許可されません** 解決策: **上記の実装を
親コンポーネントで直接参照してください。子コンポーネントのデータ変数の値
7.3. ユーザーインポートコンポーネントパッケージ
7.3.1 Excelインポート機能はnpmパッケージxlsxを使用する必要があるため、xlsxプラグインをインストールする必要があります
$ npm i xlsx
7.3.2 vue-element-admin が提供するインポート機能を使用して新しいコンポーネントを作成します。
位置:src/components/UploadExcel
7.3.3 グローバルインポートExcelコンポーネントの登録
import CommonTools from './CommonTools'
import UploadExcel from './UploadExcel'
export default {
install(Vue) {
Vue.component('CommonTools', CommonTools) // 注册工具栏组件
Vue.component('UploadExcel', UploadExcel) // 注册导入excel组件
}
}
7.3.4 新しいパブリック インポート ページを作成し、ルート src/router/index.js をマウントします。
{
path: '/import',
component: Layout,
hidden: true, // 隐藏在左侧菜单中
children: [{
path: '', // 二级路由path什么都不写 表示二级默认路由
component: () => import('@/views/import')
}]
},
7.3.5 インポートルーティングコンポーネントの作成 src/views/import/index.vue
<template>
<!-- 公共导入组件 -->
<upload-excel :on-success="success" />
</template>
7.3.6 インポートされた Excel データの取得、インポート Excel インターフェイス
async success({
header, results }) {
// 如果是导入用户
const userRelations = {
'入职日期': 'create_time',
'手机号': 'mobile',
'用户名': 'username',
'密码': 'password',
'邮箱': 'email',
'部门':'部门'
}
const arr = []
results.forEach(item => {
const userInfo = {
}
Object.keys(item).forEach(key => {
userInfo[userRelations[key]] = item[key]
})
arr.push(userInfo)
})
await importUser(arr) // 调用导入接口
this.$router.back()
}
8、言語、テーマ、全画面切り替え
8.1. 全画面プラグインへの参照
最初のステップは、グローバル プラグインをスクリーンフルにインストールすることです
$ npm i screenfull
2 番目のステップは、全画面表示用のプラグイン src/components/ScreenFull/index.vue をカプセル化することです。
<template>
<div>
<svg-icon icon-class="fullscreen" style="color:#fff; width: 20px; height: 20px" @click="changeScreen" />
</div>
</template>
<script>
import ScreenFull from 'screenfull'
export default {
methods: {
changeScreen() {
if (!ScreenFull.isEnabled) {
this.$message.warning('此时全屏组件不可用')
return
}
ScreenFull.toggle()
}
}
}
</script>
3 番目のステップは、コンポーネントをグローバルに src/components/index.js に登録することです。
import ScreenFull from './ScreenFull'
Vue.component('ScreenFull', ScreenFull) // 注册全屏组件
4 番目のステップは、layout/navbar.vue に配置することです。
<screen-full class="right-menu-item" />
.right-menu-item {
vertical-align: middle;
}
8.2. 動的なテーマ設定
最初のステップは、色選択コンポーネント ThemePicker コード アドレスをカプセル化することです: @/components/ThemePicker
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
console.log(themeCluster, originalCluster)
const $message = this.$message({
message: ' Compiling the theme',
customClass: 'theme-message',
type: 'success',
duration: 0,
iconClass: 'el-icon-loading'
})
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${
version}/lib/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const {
innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
$message.close()
}
},
methods: {
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{
[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) {
// when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${
red}${
green}${
blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${
red}${
green}${
blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
.el-color-picker {
height: auto !important;
}
</style>
import ThemePicker from './ThemePicker'
Vue.component('ThemePicker', ThemePicker)
2 番目のステップは、layout/navbar.vue に配置することです。
8.3. 多言語の実装
最初のステップでは、まずパッケージを国際化する必要があります
$ npm i vue-i18n
2 番目のステップでは、単一の多言語インスタンス化ファイル src/lang/index.js が必要です。
import customZH from './zh' // 引入自定义中文包
import customEN from './en' // 引入自定义英文包
Vue.use(VueI18n) // 全局注册国际化包
export default new VueI18n({
locale: Cookie.get('language') || 'zh', // 从cookie中获取语言类型 获取不到就是中文
messages: {
en: {
...elementEN, // 将饿了么的英文语言包引入
...customEN
},
zh: {
...elementZH, // 将饿了么的中文语言包引入
...customZH
}
}
})
3 番目のステップは、i18n プラグインを main.js にマウントし、要素を現在の言語に設定することです。
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
})
new Vue({
el: '#app',
router,
store,
i18n,
render: h => h(App)
})
4番目のステップは、左側のメニューで適用することです
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="$t('route.'+onlyOneChild.name)" />
5 番目のステップは、多言語コンポーネント src/components/lang/index.vue をカプセル化することです。
<template>
<el-dropdown trigger="click" @command="changeLanguage">
<div>
<svg-icon style="color:#fff;font-size:20px" icon-class="language" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="zh" :disabled="'zh'=== $i18n.locale ">中文</el-dropdown-item>
<el-dropdown-item command="en" :disabled="'en'=== $i18n.locale ">en</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
import Cookie from 'js-cookie'
export default {
methods: {
changeLanguage(lang) {
Cookie.set('language', lang) // 切换多语言
this.$i18n.locale = lang // 设置给本地的i18n插件
this.$message.success('切换多语言成功')
}
}
}
</script>
6 番目のステップは、Navbar コンポーネントに導入することです。
<lang class="right-menu-item" />
9、オンラインでパッケージ化
9.1. パッケージ化前の配線パターン
ハッシュ モード: #以下はフロントエンド アクセスを特徴とするルーティング パスです。#後者の変更はサーバーを通過しません。
履歴モード: 通常/アクセス モード。バックエンド アクセスを特徴とし、アドレスが変更されるとサーバーにアクセスします。
履歴モードへの変更は非常に簡単で、ルートのモード タイプを履歴に変更するだけです。
履歴モードへの変更は非常に簡単で、ルートのモード タイプを履歴に変更するだけです。
const createRouter = () => new Router({
mode: 'history', // require service support
scrollBehavior: () => ({
y: 0 }), // 管理滚动行为 如果出现滚动 切换就让 让页面回到顶部
routes: [...constantRoutes] // 改成只有静态路由
})
9.2. webpack はパッケージングを除外します
まずvue.config.jsを見つけて、webpackがxlsxと要素をパックしないようにexternalを追加します。
externals:
{
'vue': 'Vue',
'element-ui': 'ELEMENT',
'xlsx': 'XLSX'
}
9.3. CDN ファイル構成
const cdn = {
css: [
// element-ui css
'https://unpkg.com/element-ui/lib/theme-chalk/index.css' // 样式表
],
js: [
'https://unpkg.com/vue/dist/vue.js',
'https://unpkg.com/element-ui/lib/index.js',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js'
]
}
ただし、このときの設定は実際には開発環境と本番環境の両方で有効であることに注意してください。開発環境では CDN を使用する必要はありません。このとき、環境変数を使用して区別できます。
let cdn = {
css: [], js: [] }
// 通过环境变量 来区分是否使用cdn
const isProd = process.env.NODE_ENV === 'production' // 判断是否是生产环境
let externals = {
}
if (isProd) {
// 如果是生产环境 就排除打包 否则不排除
externals = {
// key(包名) / value(这个值 是 需要在CDN中获取js, 相当于 获取的js中 的该包的全局的对象的名字)
'vue': 'Vue', // 后面的名字不能随便起 应该是 js中的全局对象名
'element-ui': 'ELEMENT', // 都是js中全局定义的
'xlsx': 'XLSX' // 都是js中全局定义的
}
cdn = {
css: [
'https://unpkg.com/element-ui/lib/theme-chalk/index.css' // 提前引入elementUI样式
], // 放置css文件目录
js: [
'https://unpkg.com/vue/dist/vue.js', // vuejs
'https://unpkg.com/element-ui/lib/index.js', // element
'https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js', // xlsx 相关
'https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js' // xlsx 相关
] // 放置js文件目录
}
}
9.4. CDN ファイルのテンプレートへの挿入
次に、html-webpack-plugin を介してそれをindex.html に挿入します。
config.plugin('html').tap(args => {
args[0].cdn = cdn
return args
})
找到 public/index.html。通过你配置的CDN Config 依次注入 css 和 js。
<head>
<!-- 引入样式 -->
<% for(var css of htmlWebpackPlugin.options.cdn.css) {
%>
<link rel="stylesheet" href="<%=css%>">
<% } %>
</head>
<!-- 引入JS -->
<% for(var js of htmlWebpackPlugin.options.cdn.js) {
%>
<script src="<%=js%>"></script>
<% } %>
9.5 最後にパッケージ化
$ npm run build:prod