Created by huqi at 2019-5-5 13:01:14
Updated by huqi at 2019-5-20 15:57:37javascript
上個月月底@D2開源組 開源了使用 D2Admin 適配 人人企業版(專業版) 的前端工程--d2-admin-renren-security-enterprise,具體詳情見☞D2Admin 人人企業版適配發布。因爲最近有開發後臺管理系統的需求,加上其餘諸多因素,如:想學習優秀的開源項目、恰好參加@jsliang 組織的暴走前端、以前項目使用過renren-fast-vue等等,因而乎萌生了根據commits學習瞭解d2-admin如何改造renren-security前端的想法。固然,也督促本身產出一篇相關的文章,來記錄此次有趣的學習之旅。css
所謂「工欲善其事必先利其器」,連我這樣的Copy攻城獅都要搭建的前端基礎開發環境,都9102年啦,再沒有node環境都無法進行前端開發了,再不濟運行d2-admin的環境總該有吧!html
安裝環境
這裏請參考D2 Admin快速上手部分:☞安裝環境前端
Fork D2-Admin@1.6.18
這裏緊跟@FairyEver 大大的思路,基於D2-Admin@1.6.18 開發。固然也能夠經過D2 Admin CLI來初始化項目,具體操做參照:☞下載項目vue
瞭解D2Admin項目結構
有過開發經歷的同窗在項目開發以前通常都有過項目結構搭建的經歷,固然若是您不幸和我同樣只會用別人搭建好的工程腳手架,那您必定會先了解整個項目目錄結構,否則還真不知道源碼要寫在哪裏。java
├─ docs // 文檔
├─ packages // 額外的包
├─ public // 公共文件
├─ src // 源碼目錄
│ ├─ assets // 資源
│ │ ├─ icons
│ │ ├─ image
│ │ ├─ library
│ │ └─ style
│ ├─ components // 組件
│ │ ├─ charts
│ │ ├─ core
│ │ └─ demo
│ ├─ i18n // 多語言
│ ├─ menu // 菜單
│ ├─ mock // 模擬數據
│ ├─ pages // 頁面
│ ├─ plugin // 插件
│ ├─ router // 路由
│ ├─ store // vuex
│ ├─ utils
│ ├─ App.vue
│ └─ main.js
├─ tests // 測試文件
├─ .browserslistrc // 瀏覽器兼容設置
├─ .env // 環境變量
├─ .env.development // 開發環境變量
├─ .env.nomock // nomock環境變量
├─ .env.travis // 生成環境變量
├─ .eslintignore // ESLint忽略
├─ .eslintrc.js // ESLint配置
├─ .gitignore // git忽略
├─ .postcssrc.js // postcss配置
├─ .travis.yml // 持續集成服務
├─ babel.config.js // babel配置
├─ cdnrefresh-dirs.txt // cdn設置
├─ jest.config.js // jest設置
├─ LICENSE // 開源協議
├─ package-lock.json // 包文件鎖版本
├─ package.json // 包文件
├─ qiniu-config // 七牛雲配置
├─ qshell // 七牛API服務命令行工具
├─ README.md
|— README.zh.md
├─ vue.config.js // vue配置
複製代碼
刪除無關文件
刪除.browserslistrc、.env.nomock、.env.travis 、.gitignore、.postcssrc.js、.travis.yml、cdnrefresh-dirs.txt 、package-lock.json、 qiniu-config 、qshell、README.zh.md、README.md、doc/image、package/*。具體可查看:☞刪除暫時未用到模塊node
修改package.json
移除暫時未用到的包,如多語言,這個版本將簡化多語言目錄結構,如圖表庫、富文本編輯、右鍵菜單等:
countup.js
echarts
github-markdown-css
highlight.js
marked
mockjs
simplemde
v-charts
v-contextmenu
vue-grid-layout vue-i18n
vue-json-tree-view
vue-splitpane
vue-ueditor-wra
@kazupon/vue-i18n-loader
刪除build:nomock命令,增長環境變量文件.env、.env.production、.env.production.sit、.env.production.uat等。至此,能夠經過npm install
或 yarn
來安裝項目依賴,並經過npm run dev
之類的指令運行項目,具體指令可查看 package.json 文件中 scripts 部分。webpack
至於爲何要重寫,要問大佬了。我也只能妄加揣測:簡化結構!以前的結構是一個index.js+lang文件夾,lang文件夾裏又包含多個語言文件夾,如今的結構直接了當--index.js+多個語言js文件。關於國際化我也只是很膚淺的瞭解,雖然以前接觸過的項目也作過,裏邊坑的確挺多的,除了基本的翻譯還要結合當地的文化習俗,這裏就不展開討論,搜索關鍵字i18n便有衆多的解決方案。回到大佬@FairyEver 的源碼,跟着他了解一下vue-i18n的使用:ios
npm install vue-i18n
複製代碼
// ...
// i18n
import i18n from '@/i18n'
// ...
new Vue({
i18n,
// ...
)}
複製代碼
新建語言包,構建js
核心代碼:git
// 引入相關依賴及語言包
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import Cookies from 'js-cookie'
// 附帶引入element-ui的多語言切換
import zhCNLocale from 'element-ui/lib/locale/lang/zh-CN'
import zhTWLocale from 'element-ui/lib/locale/lang/zh-TW'
import enLocale from 'element-ui/lib/locale/lang/en'
// 引入語言包
import zhCN from './zh-CN'
import zhTW from './zh-TW'
import enUS from './en-US'
Vue.use(VueI18n)
// 定義使用的語言
export const messages = {
'zh-CN': {
'_lang': '簡體中文',
...zhCN,
...zhCNLocale
},
'zh-TW': {
'_lang': '繁體中文',
...zhTW,
...zhTWLocale
},
'en-US': {
'_lang': 'English',
...enUS,
...enLocale
}
}
// 默認從cookie中讀取或設置爲中文
export default new VueI18n({
locale: Cookies.get('language') || 'zh-CN',
messages
})
複製代碼
語言包以灣灣繁體爲例:
// 定義語言對象
const t = {}
t.loading = '加載中...'
// 構建對象
t.brand = {}
t.brand.lg = '人人權限企業版'
t.brand.mini = '人人'
// ...
export default t
複製代碼
使用
// 選擇語言
import Cookies from 'js-cookie'
import { messages } from '@/i18n'
export default {
name: 'app',
watch: {
'$i18n.locale': 'i18nHandle'
},
created () {
this.i18nHandle(this.$i18n.locale)
},
methods: {
i18nHandle (val, oldVal) {
Cookies('language', val)
document.querySelector('html').setAttribute('lang', val)
document.title = messages[val].brand.lg
// 非登陸頁面,切換語言刷新頁面
if (this.$route.name !== 'login' && oldVal) {
window.location.reload()
}
}
}
}
複製代碼
頁面中使用,如:
// template
{{ $t('login.motto.text') }}
:placeholder="$t('login.form.placeholderUsername')"
// script
this.$t('login.motto.text')
複製代碼
檢驗成果
實踐是檢驗真理的惟一標準。 修改i18n/index.js 將locale改成灣灣繁體,就能直觀的看到title的變化,(別問我爲啥頁面上的文字怎麼沒變化?由於寫死爲簡體中文啦!)
// i18n
import i18n from '@/i18n'
// Element
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// Element
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
})
複製代碼
文字部分所有修改成國際化後,就能夠看到明顯的效果啦:
既然有了國際化的基礎,那麼實現一個多語言切換的小功能應該是水到渠成。來看看@FairyEver是怎麼教的! 偶然間看到d2-admin中的標籤可使用flex這個屬性,感到很好奇。
這裏經過elemen-ui的el-dropdown實現,經過command事件修改語言設置
<el-dropdown size="small" @command="command => $i18n.locale = command">
<span class="page-login--content-header-side-text"><d2-icon name="language"/> {{ $t('login.language') }}</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(language, index) in $languages" :key="index" :command="language.value">{{ language.label }}</el-dropdown-item>
</el-dropdown-menu>
</el 複製代碼
通常來講,作登陸頁的時候,咱們或多或少會遇到驗證碼的需求,對了,這裏的驗證碼指的是圖形驗證碼。最簡單的實踐是直接拿後臺給過來的圖片直接渲染的在頁面上,使用 img標籤 或者 background-image 引入。以前作renren-fast-vue二次開發的時候用的img標籤,這裏用的背景圖片,思路都同樣:拿後臺給的圖片直接渲染。衆所周知,Just do it!
定義獲取uuid的工具函數:
/** * @description [ renren ] 獲取uuid */
util.getUUID = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
})
}
複製代碼
<template slot="append">
<div class="login-captcha" :style="{ backgroundImage: `url(${captchaPath})` }" @click="updateUUID" />
</template>
複製代碼
// 選擇語言
import Cookies from 'js-cookie'
import { messages } from '@/i18n'
export default {
name: 'app',
watch: {
'$i18n.locale': 'i18nHandle'
},
created () {
this.i18nHandle(this.$i18n.locale)
},
methods: {
i18nHandle (val, oldVal) {
Cookies('language', val)
document.querySelector('html').setAttribute('lang', val)
document.title = messages[val].brand.lg
// 非登陸頁面,切換語言刷新頁面
if (this.$route.name !== 'login' && oldVal) {
window.location.reload()
}
}
}
}
複製代碼
// 引用相關依賴及方法
import axios from 'axios'
import { Message } from 'element-ui'
import Cookies from 'js-cookie'
import { isPlainObject } from 'lodash'
import qs from 'qs'
// import util from '@/libs/util'
import router from '@/router'
import store from '@/store'
// 記錄和顯示錯誤
function errorLog (error) {
// 添加到日誌
store.dispatch('d2admin/log/push', {
message: '數據請求異常',
type: 'danger',
meta: {
error
}
})
// 打印到控制檯
if (process.env.NODE_ENV === 'development') {
// util.log.danger('>>>>>> Error >>>>>>')
console.log(error)
}
// 顯示提示
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
}
// 建立一個 axios 實例
const service = axios.create({
baseURL: process.env.VUE_APP_API,
timeout: 1000 * 180, // 請求超時時間
withCredentials: true // 當前請求爲跨域類型時是否在請求中協帶cookie
})
/**
* 請求攔截
*/
service.interceptors.request.use(
config => {
// 在請求發送以前作一些處理,如設置headers
config.headers['Accept-Language'] = Cookies.get('language') || 'zh-CN'
config.headers['token'] = Cookies.get('token') || ''
// 默認參數
var defaults = {}
// 防止緩存,GET請求默認帶_t參數
if (config.method === 'get') {
config.params = {
...config.params,
...{ '_t': new Date().getTime() }
}
}
if (isPlainObject(config.data)) {
// 純粹對象解構賦值
config.data = {
...defaults,
...config.data
}
if (/^application\/x-www-form-urlencoded/.test(config.headers['content-type'])) {
// 序列化請求數據
config.data = qs.stringify(config.data)
}
}
return config
},
error => {
// 發送失敗
console.log(error)
return Promise.reject(error)
}
)
/**
* 響應攔截
*/
service.interceptors.response.use(
response => {
// 處理響應
if (response.data.code === 401 || response.data.code === 10001) {
// clearLoginInfo()
// alert('TODO clearLoginInfo')
// TODO: 清除用戶信息
router.replace({ name: 'login' })
return Promise.reject(response.data.msg)
}
if (response.data.code !== 0) {
errorLog(new Error(response.data.msg))
return Promise.reject(response.data.msg)
}
return response.data.data
},
error => {
errorLog(error)
return Promise.reject(error)
}
)
export default service
複製代碼
import request from '@/plugin/axios'
export function login (data) {
return request({
url: '/login',
method: 'post',
data
})
}
複製代碼
調用api進行登陸:
// ...
import { login } from '@api/sys.login'
// ...
submit () {
this.$refs.loginForm.validate((valid) => {
if (!valid) return
login(this.form)
.then(async res => {
await this.login(res)
this.$router.replace(this.$route.query.redirect || '/')
})
.catch(this.updateUUID)
})
}
// ...
複製代碼
固然還須要對數據進行處理,好比登陸狀態持久化、設置vuex用戶信息等等,這裏暫時只作簡單的處理,另外安利一個vscode插件(乳溝您恰巧用的宇宙第一神器)--TODO Highlight,用來突出顯示代碼中的todo、fixme和其餘註釋,據說老司機都在用。有時,在將代碼發佈到生產環境以前,在編碼時忘記查看添加的TODO。因此就有了這個拓展,提醒咱們有一些筆記或者事情尚未完成。mark一下!
做爲後臺管理系統,免不了涉及到cookie的使用,按照大佬的思路,定義了工具集函數並基於js-cookie二次封裝了cookie。通常來講,cookie用得最多的就是get和set兩個方法。
import Cookie from 'js-cookie'
/** * @description 存儲 cookie 值 * @param {String} name cookie name * @param {String} value cookie value * @param {Object} setting cookie setting */
export const cookieSet = function (name = 'default', value = '', cookieSetting = {}) {
let currentCookieSetting = {
expires: 1
}
Object.assign(currentCookieSetting, cookieSetting)
Cookie.set(`d2admin-${process.env.VUE_APP_VERSION}-${name}`, value, currentCookieSetting)
}
/** * @description 拿到 cookie 值 * @param {String} name cookie name */
export const cookieGet = function (name = 'default') {
return Cookie.get(`d2admin-${process.env.VUE_APP_VERSION}-${name}`)
}
/** * @description 拿到 cookie 所有的值 */
export const cookieGetAll = function () {
return Cookie.get()
}
/** * @description 刪除 cookie * @param {String} name cookie name */
export const cookieRemove = function (name = 'default') {
return Cookie.remove(`d2admin-${process.env.VUE_APP_VERSION}-${name}`)
}
複製代碼
節流這個知識點我也是一直懵懵懂懂,常常和防抖混淆,理解不深入,還只是停留在字面意思理解上:函數節流是指定時間間隔內只執行一次,函數防抖是頻繁觸發只有間隔超過指定時間間隔才執行。請參考debouncing-throttling-explained-examples 這裏簡單粗暴的用了lodash--一個一致性、模塊化、高性能的 JavaScript 實用工具庫。。 lodash中包含一系列數組、數字、對象、字符串等操做的API,固然還有一些經常使用的工具函數如節流(throttle)、防抖(debounce)。
// ...
import { debounce } from 'lodash'
// ...
submit: debounce(function () {
// ...
}, 1000, { 'leading': true, 'trailing': false })
// _.debounce(func, [wait=0], [options={}])
// options.leading 與|或 options.trailing 決定延遲先後是先調用後等待,仍是先等待後調用
// ...
複製代碼
先後對比:
未處理的時候,觸發的請求使人髮指!
處理以後,控制檯讓人感受很清爽
項目作得太少了,尤爲還不會java,對網站的全局配置這一塊的理解還停留在初級認知階段。通常來講,在網頁開發中每每一些版本控制、CDN靜態資源、api接口地址、經常使用的公共變量等都會寫到window下面並提高至首頁方便管理,如網易一些爆款的H5中這種手法很是常見。在我以前使用開源的renren-fast-vue中這種手法更是大量運用,此次學習d2-admin也借鑑一下這種全局變量的使用(掛載變量一時爽,一直掛載一直爽,當心別翻車了)。先無論了,一頓Copy操做猛如虎,定睛一看,註釋佔了百分之九十五!固然,代碼了瞬間有了後端的痕跡,不過在本項目 public/index.html中使用的模板語法來源於 lodash 模板插入,和public文件夾相關的內容能夠去翻翻d2-admin文檔關於cli 和 webpack 配置部分,這裏就再也不贅述,總之,萬丈高樓平地起,基礎建設很重要!
window.SITE_CONFIG = {};
window.SITE_CONFIG['version'] = '<%= process.env.VUE_APP_VERSION %>'; // 版本
window.SITE_CONFIG['nodeEnv'] = '<%= process.env.VUE_APP_NODE_ENV %>'; // node env
window.SITE_CONFIG['apiURL'] = '<%= process.env.VUE_APP_API %>'; // api請求地址
window.SITE_CONFIG['storeState'] = {}; // vuex本地儲存初始化狀態(用於不刷新頁面的狀況下,也能重置初始化項目中全部狀態)
window.SITE_CONFIG['contentTabDefault'] = { // 內容標籤頁默認屬性對象
'name': '', // 名稱, 由 this.$route.name 自動賦值(默認,名稱 === 路由名稱 === 路由路徑)
'params': {}, // 參數, 由 this.$route.params 自動賦值
'query': {}, // 查詢參數, 由 this.$route.query 自動賦值
'menuId': '', // 菜單id(用於選中側邊欄菜單,與this.$store.state.sidebarMenuActiveName進行匹配)
'title': '', // 標題
'isTab': true, // 是否經過tab展現內容?
'iframeURL': '' // 是否經過iframe嵌套展現內容? (以http[s]://開頭, 自動匹配)
};
window.SITE_CONFIG['menuList'] = []; // 左側菜單列表(後臺返回,未作處理)
window.SITE_CONFIG['permissions'] = []; // 頁面按鈕操做權限(後臺返回,未作處理)
window.SITE_CONFIG['dynamicRoutes'] = []; // 動態路由列表
window.SITE_CONFIG['dynamicMenuRoutes'] = []; // 動態(菜單)路由列表
window.SITE_CONFIG['dynamicMenuRoutesHasAdded'] = false; // 動態(菜單)路由是否已經添加的狀態標示(用於判斷是否須要從新拉取數據並進行動態添加操做)
複製代碼
大廠某H5案例中全局配置掛載
感受像我這種資深Copy級別的零級工程師,對於一些炫酷的頁面效果,除了感嘆"牛掰",就是一頓複製粘貼。當我看到d2-admin使用的NProgress是0.2.0版本的時候,我覺得是個比較新的第三方庫,抱着刨根到底的學習心態,我點開了NProgress的github倉庫,看到做者@rstacruz的主頁,不由讚歎:"牛掰!"。說來也巧,@justjavac 大神翻譯的速查表就源自做者的cheatsheets。雖然NProgress誕生於2013年8月,(那時我還在學校把妹,對js的瞭解還只是不當心按到F12),@rstacruz對她的維護長達5年之久,目前有18.8K的star,而@rstacruz本尊更是值得我輩瞻仰的大神。
//...
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
//...
NProgress.start()
//...
NProgress.done()
複製代碼
NProgress的實現原理也很好理解,源碼比較簡潔,大概是加載開始調用start,加載完成調用done,至於加載進度、具體加載到哪了,都不關心,中間狀態是隨機的進度,從源碼中看到大概加載到99.4%的位置就停了。
NProgress.inc = function(amount) {
var n = NProgress.status;
if (!n) {
return NProgress.start();
} else if(n > 1) {
return;
} else {
if (typeof amount !== 'number') {
if (n >= 0 && n < 0.2) { amount = 0.1; }
else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
else if (n >= 0.5 && n < 0.8) { amount = 0.02; }
else if (n >= 0.8 && n < 0.99) { amount = 0.005; }
else { amount = 0; }
}
n = clamp(n + amount, 0, 0.994);
return NProgress.set(n);
}
};
//...
/** * Helpers */
function clamp(n, min, max) {
if (n < min) return min;
if (n > max) return max;
return n;
}
複製代碼
感興趣的同窗能夠看看源碼學習學習!☞nprogress.js
在d2-admin中,實際上是有實現iframe類型的內容頁組件的-- d2-container-frame,從源碼來看,是iframe是嵌套在d2-container組件中的,利用絕對定位實現iframe充滿d2-container盒子。
<template>
<d2-container v-bind="$attrs"> <iframe class="d2-container-frame" :src="src" frameborder="0"/> </d2-container> </template> <script> export default { name: 'd2-container-frame', props: { src: { type: String, required: false, default: 'https://doc.d2admin.fairyever.com/zh/' } } } </script> <style lang="scss" scoped> .d2-container-frame { position: absolute; top: 0px; left: 0px; height: 100%; width: 100%; } </style> 複製代碼
在改造renren的項目中,大佬巧妙的利用組裝route的方式,實現了iframe單獨渲染,具體能夠看下源碼:☞支持 iframe 加載方式:
// ...
// 組裝路由
var route = {
path: '',
component: null,
name: '',
meta: {
...window.SITE_CONFIG['contentTabDefault'],
menuId: menuList[i].id,
title: menuList[i].name
}
}
// ...
route['path'] = route['name'] = `i-${menuList[i].id}`
route['meta']['iframeURL'] = URL
route['component'] = {
render (h) {
return h('d2-container', {}, [
h('iframe', {
style: {
position: 'absolute',
top: '0px',
left: '0px',
height: '100%',
width: '100%'
},
attrs: {
src: URL,
frameborder: 0
}
})
])
}
}
// ...
複製代碼
源碼雖然沒有細看,不過仍是根據commits提交記錄,粗略的一步一步copy實現了一番。整個過程仍是頗有趣的,畢竟算是參與了開源,還給大佬提了issue,捉了bug。可是,總得來講,還有不少知識點沒細看,如vue的mixins、衆多頁面的具體實現、iconfont的使用、Vuex的使用、自定義皮膚的實現、頂部菜單欄的實現等等,期間也遇到一些編譯上的問題,如el-table的坑☞Error: if there's nested data, rowKey is required.。感受整個過程還學的不踏實,不少知識點只是一搜帶過,可能仍是項目作少了。路漫漫其修遠,慢慢摸索吧。 HR的領導來電話催回家了,匆匆落筆,結束此篇,江湖再見!