vue商城项目源码_CMS全栈项目之Vue和React篇(下)(含源码)
今天給大家介紹的主要是我們全棧CMS系統的未講解完的后臺部分和前臺部分,如果對項目背景和技術棧不太了解,可以查看我之前的文章
基于nodeJS從0到1實現一個CMS全棧項目(上)
基于nodeJS從0到1實現一個CMS全棧項目(中)
基于nodeJS從0到1實現一個CMS全棧項目的服務端啟動細節
摘要
本文將主要介紹如下內容:
- 實現自定義的koa中間件和restful API
- koa路由和service層實現
- 模版引擎pug的基本使用及技巧
- vue管理后臺頁面的實現及源碼分享
- react客戶端前臺的具體實現及源碼分享
- pm2部署以及nginx服務器配置
由于每一個技術點實現的細節很多,建議先學習相關內容,不懂的可以和我交流。如果只想了解vue或react相關的內容,可以直接跳到文章的第4部分。
正文
1.實現自定義的koa中間件和restful API
Koa 應用程序是一個包含一組中間件函數的對象,它是按照類似堆棧的方式組織和執行的。我們可以使用koa提供的use接口和async函數去自定義一些中間件。一個用來實現打印log的中間件如下:
// logger app.use(async (ctx, next) => {await next();const rt = ctx.response.get('X-Response-Time');console.log(`${ctx.method} ${ctx.url} - ${rt}`); });有關koa的更多介紹可以去官網學習,我們開始正式進入實現中間件的環節。
在我第一章介紹CMS時剖出了目錄結構和層級,我們在源碼中找到middlewares目錄,首先我們來看看common.js,這個文件是存放我們通用中間件的地方,一共定義了如下中間件:
源碼如下:
import logger from 'koa-logger'; import koaBody from 'koa-body'; import session from 'koa-session'; import cors from 'koa2-cors'; import sessionStore from '../lib/sessionStore'; import redis from '../db/redis'; import statisticsSchema from '../db/schema/statistics';// 設置日志 export const Logger = app => app.use(logger()) // 處理請求體 export const KoaBody = app => app.use(koaBody())// 配置跨域資源共享 export const Cors = app => app.use(cors({origin: function(ctx) {if (ctx.url.indexOf('/api') > -1) {return false;}return '*';},exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],maxAge: 5,credentials: true,allowMethods: ['GET'],allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Requested-With'],}) )// 設置session export const Session = app => {app.keys = ['xujiang']const SESSION_CONFIG = {key: 'zxzkCMS',maxAge: 12 * 60 * 60 * 1000, // session的失效時間,設置為半天store: new sessionStore(redis),signed: true}app.use(session(SESSION_CONFIG, app)); }// 統計網站數據 export const siteStatistics = app => app.use(async (ctx, next) => {if(ctx.url.indexOf('articleList?iSaJAx=isAjax') > -1) {const views = await statisticsSchema.hget('views')statisticsSchema.hmset('views', +views + 1)}await next() })其實實現一個中間件很簡單,我們只需要在app.use的參數中創建自己的async業務函數就好了,比如siteStatistics,可以參考此方法去做自定義的中間件。
關于restful API的實現,我們在基礎架構層來實現。可以看源碼的lib下的descorator.js文件。大致分為幾塊內容:
這塊實現會涉及到更多的es6+知識,包括修飾器,symbol等,如有不懂的可以和我交流溝通。
2.koa路由和service層實現
這一塊主要采用MVC模式,我們在之前定義了基礎的路由類,這樣我們就可以正式處理服務端業務,我們可以按模塊定義不同的業務接口,通過路由控制器統一管理。
我們實現router和service分離的模式如上圖,在api router下我們只會定義請求相應相關的內容,具體的業務邏輯和數據操作統一在service層處理,這樣做的好處是方便后期擴展和管理業務邏輯,讓代碼更可讀。當然也可以把數據操作和http統一放在router里,但是這樣會造成代碼耦合度過高,不利于項目管理。我們來看看具體的實現方式:
這里我們舉了個簡單的例子方便大家理解,至于admin和config等模塊的開發也類似,可以結合自己的業務需要去處理。其他模塊的代碼已寫好,可以在我的github中找到。如有不懂,可以和我交流。
3.模版引擎pug的基本使用及技巧
模版引擎這塊不是項目中的重點,在項目中也沒有涉及到諸如jade,ejs這些模版引擎,但是作為前端,這些多了解還是很好的。我在這里簡單介紹一下pug(也就是jade的升級版)。
為了在koa項目中使用模版引擎,我們可以使用koa-views來做渲染,具體使用方式如下:
/***** koa-view基本使用 *****/import views from 'koa-views';app.use(views(resolve(__dirname, './views'), { extension: 'pug' }));app.use(async (ctx, next) => {await ctx.render('index', {name: 'xujiang',years: '248歲'})});具體頁面的pug文件:
pug采用縮進的方式來規定代碼層級,可以使用繼承等語法,感興趣可以參考pug官網學習。這里不做詳細介紹。
4.vue管理后臺頁面的實現及源碼分享
首先我們看看vue管理后臺的組織架構:
由于后臺大部分是動態配置的數據,而且還會有預覽功能,所以涉及到大量數據共享的情況,這里我們統一采用vuex來管理狀態,vuex的模型如下:
state用來定義初始化store,mutation主要用來處理同步action,action用來處理異步action,type是用來定義state類型的接口文件,如下:
// type.ts export interface State {name: string;isLogin: boolean;config: Config;[propName: string]: any; // 用來定義可選的額外屬性 }export interface Config {header: HeaderType,banner: Banner,bannerSider: BannerSider,supportPay: SupportPay }export interface HeaderType {columns: string[],height: string,backgroundColor: string,logo: string }export interface Banner {type: string,label: string[],bgUrl: string,bannerList: any[] }export interface BannerSider {tit: string,imgUrl: string,desc: string }export interface SupportPay {tit: string,imgUrl: string }// 處理相應的類型 export interface Response {[propName: string]: any; }mutation內容如下:
action如下:
//action.ts import { HeaderType,Banner,BannerSider,SupportPay,Response} from './type' import http from '../utils/http' import { uuid, formatTime } from '../utils/common' import { message } from 'ant-design-vue'export default {/**配置 */setConfig(context: any, paylod: HeaderType) {http.get('/config/all').then((res:Response) => {context.commit('setConfig', res.data)}).catch((err:any) => {message.error(err.data)})},/**header */saveHeader(context: any, paylod: HeaderType) {http.post('/config/setHeader', paylod).then((res:Response) => {message.success(res.data)context.commit('saveHeader', paylod)}).catch((err:any) => {message.error(err.data)}) },/**banner */saveBanner(context: any, paylod: Banner) {http.post('/config/setBanner', paylod).then((res:Response) => {message.success(res.data)}).catch((err:any) => {message.error(err.data)}) },/**文章列表 */getArticles(context: any) {http.get('article/all').then((res:Response) => {context.commit('getArticles', res.data);}).catch((err:any)=>{message.error(err.data)})},addArticle(context: any, paylod: any) {paylod.id = uuid(8, 10);paylod.time = formatTime(Date.now(), '/');paylod.views = 0;paylod.flover = 0;return new Promise((resolve:any, reject:any) => {http.post('/article/saveArticle', paylod).then((res:Response) => {context.commit('addArticle', paylod)message.success(res.data)resolve()}).catch((err:any) => {message.error(err.data)reject()})}) }// ... };這里大致列舉了幾個典型的action,方便大家學習和理解,再進一步的化,我們可以基于它去封裝baseAction,這要可以減少大部分復用信息,這里大家可以試試做封裝一波。 最后我們統一在index里統一引入:
import Vue from 'vue'; import Vuex from 'vuex'; import { state } from './state'; import mutations from './mutation'; import actions from './action';Vue.use(Vuex);export default new Vuex.Store({state,mutations,actions, });通過這種方式管理vuex,對于后期可擴展性和可維護性,也有一定的幫助。
vue頁面部分大家根據之前node篇的用例和數據模型可以知道大致的頁面模塊和功能點,這里就不在細談。我們來看看幾個關鍵點:
- 如何保證頁面刷新導航可以正確定位
- 如何切換頁面時做自定義緩存
- 如何實現模擬pc端,移動端預覽
- 如何使用vuex高級api實現數據監聽機制
- 如何做登錄鑒權
接下來我直接剖出我的方案,大家可以參考。
1.如何保證頁面刷新導航可以正確定位
// layout.vue // 頁面路由表 const routeMap: any = {'/': '1','/banner': '2','/bannerSider': '3','/article': '4','/addArticle': '4','/support': '5','/imgManage': '6','/videoManage': '7','/websiteAnalysis': '8','/admin': '9', };// 監聽路由變化,匹配當前選中導航 @Watch('$route') private routeChange(val: Route, oldVal: Route) {// do somethingif(val.path.indexOf('/preview') < 0) {this.curSelected = routeMap[val.path] || routeMap[oldVal.path];} }2.如何切換頁面時做自定義緩存
我們使用keep-alive做緩存,被他包裹的路由視圖下傳遞key值來確定下次是否被走緩存:
<template><div id="app"><keep-alive><router-view :key="key" /></keep-alive></div> </template><script lang="ts"> import { Vue } from 'vue-property-decorator'; import Component from 'vue-class-component';@Component export default class App extends Vue {get key() {// 緩存除預覽頁面之外的其他頁面console.log(this.$route.path)if(this.$route.path.indexOf('/preview') > -1) {return '0'}else if(this.$route.path === '/login') {return '1'}else {return '2'}} } </script>由于我們的業務是預覽和管理頁面切換的時候要更新到最新數據,所以我們在這兩個模塊切換時不走緩存,調用最新數據。登錄同理,通過設置不同的key來做分布式緩存。
3.如何實現模擬pc端,移動端預覽
實現預覽主要我采用基于寬度來做的模擬,通過定義預覽路由,來定義pc和移動的屏幕。如果有不懂的,可以和我交流,當然你們也可以采用iframe用模擬。
4.如何使用vuex高級api實現數據監聽機制
這里直接剖代碼:
public created() {let { id, label } = this.$route.query;this.type = id ? 1 : 0;if(id) {// 監聽vuex中文章數據的變化,變化則觸發action顯示文章數據// 注:這里這么做是為了防止頁面刷新數據丟失let watcher = this.$store.watch((state,getter) => {return state.articles},() => {this.getDetail(id, label, watcher)})if(Object.keys(this.$store.state.articles).length) {this.getDetail(id, label, watcher)}}}我們使用vuex的watch去監聽store的變化,然后去做相應的處理,watch API接受兩個回調參數,第一個回調返回一個值,如果值變化了,就會觸發第二個參數的回調,這有點類似與react hooks的memo和callback。
5.如何做登錄鑒權
登錄鑒權主要是和后端服務協商一套規則,后臺通過校驗是否登錄或者是否有權限操作某個模塊,一般通過response的相應數據通知給前端,這里我們主要講一下登錄鑒權的,如果當前用戶沒登錄或者session過期,node服務端會返回401,這樣前端就可以去做重定向操作了:
//http模塊封裝 import axios from 'axios' import qs from 'qs'axios.interceptors.request.use(config => {// loadingreturn config }, error => {return Promise.reject(error) })axios.interceptors.response.use(response => {return response }, error => {return Promise.resolve(error.response) })function checkStatus (response) {// loading// 如果http狀態碼正常,則直接返回數據if(response) {if (response.status === 200 || response.status === 304) {return response.data// 如果不需要除了data之外的數據,可以直接 return response.data} else if (response.status === 401) {location.href = '/login';} else {throw response.data}} else {throw {data:'網絡錯誤'}}}// axios默認參數配置 axios.defaults.baseURL = '/api/v0'; axios.defaults.timeout = 10000;export default {post (url, data) {return axios({method: 'post',url,data: qs.stringify(data),headers: {'X-Requested-With': 'XMLHttpRequest','Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}}).then((res) => {return checkStatus(res)})},get (url, params) {return axios({method: 'get',url,params, // get 請求時帶的參數headers: {'X-Requested-With': 'XMLHttpRequest'}}).then((res) => {return checkStatus(res)})},del (url, params) {return axios({method: 'delete',url,params, // get 請求時帶的參數headers: {'X-Requested-With': 'XMLHttpRequest'}}).then((res) => {return checkStatus(res)})} }至于具體的axios請求攔截器和響應攔截器的設置,我們可以根據具體業務來操作和添加自定義邏輯。
5.react客戶端前臺的具體實現及源碼分享
react部分我主要采用自己搭建的webpack做模塊打包,想學習webpack的可以參考我的webpack配置,目前打包文件可以兼容到ie9+。
這幾部分都是通過vue后臺配置出來的,大家也可以配置符合自己風格的網站。 react前臺我們主要使用react hooks來搭建,沒有采用redux等狀態管理庫,如果想學習redux相關知識,可以進入我們的學習群一起學習。 首頁代碼如下:
import React, { useState, useEffect } from "react" import { Carousel } from 'antd' import ArticleItem from '../../components/ArticleItem' import { isPC, ajax, unparam } from 'utils/common'import './index.less'function Home(props) {let [articles, setArticles] = useState([])let { search } = props.locationfunction getArticles(cate = '', num = 10, page = 0) {ajax({url: '/article/articleList',method: 'get',data: { cate, num, page }}).then(res => {setArticles(res.data || [])}).catch(err => console.log(err))}if(search && sessionStorage.getItem('prevCate') !== search) {getArticles(unparam(search).cate)sessionStorage.setItem('prevCate', search)}useEffect(() => {getArticles()return () => {sessionStorage.removeItem('prevCate')}}, [])return <div className="home-wrap"><div className="banner-wrap">{isPC ?<React.Fragment><div className="banner-sider"><div className="tit">{ props.bannerSider.tit }</div><img src={props.bannerSider.imgUrl} alt="" /><div className="desc">{ props.bannerSider.desc }</div></div>{+props.banner.type ?<Carousel autoplay className="banner">{props.banner.bannerList.map((item, i) => (<div key={i}><a className="banner-img" href="" style={{ backgroundImage: 'url('+ item.imgUrl +')'}}><p className="tit">{ item.tit }</p></a></div>))}</Carousel>:<div className="banner"><div className="banner-img" style={{backgroundImage: 'url('+ props.banner.bgUrl +')'}}>{props.banner.label.map((item, i) => (<span className="banner-label" style={{left: 80*(i+1) + 'px'}} key={i}>{ item }</span>))}</div></div>}</React.Fragment>:<Carousel autoplay className="banner">{props.banner.bannerList.map((item, i) => (<a className="banner-img" href="" key={i} style={{ backgroundImage: 'url('+ item.imgUrl +')'}}><p className="tit">{ item.tit }</p></a>))}</Carousel>}</div><div className="article-list"><div className="tit">最新文章</div>{articles.map((item, i) => (<ArticleItem {...item} key={i} />))}</div></div> }export default Home文章詳情:
import React, { useState, useEffect } from "react" import { Button, Modal, Skeleton, Icon } from 'antd' import { ajax, unparam } from 'utils/common' import QTQD from 'images/logo.png' import './index.less'function ArticleDetail(props) {let [isPayShow, setIsPayShow] = useState(false)let [detail, setDetail] = useState(null)let [likeNum, setLikeNum] = useState(0)let [articleContent, setArticleContent] = useState(null)let [isShowLike, setShowLike] = useState(false)function toggleModal(flag) {setIsPayShow(flag)}function getcontent(url) {ajax({url}).then(res => {setArticleContent(res.content)})}function showLike() {if(!isShowLike) {ajax({url: `/article/likeArticle/${unparam(props.location.search).id}`,method: 'post'}).then(res => {setShowLike(true)setLikeNum(prev => prev + 1)})}}useEffect(() => {ajax({url: `/article/${unparam(props.location.search).id}`}).then(res => {setDetail(res.data)setLikeNum(res.data.flover)getcontent(res.data.articleUrl)})return () => {};}, [])return !detail ? <Skeleton active /> :<div className="article-wrap"><div className="article"><div className="tit">{ detail.tit }</div><div className="article-info"><span className="article-type">{ detail.label }</span><span className="article-time">{ detail.time }</span><span className="article-views"><Icon type="eye" /> { detail.views }</span><span className="article-flover"><Icon type="fire" /> { likeNum }</span></div><div className="article-content" dangerouslySetInnerHTML={{__html: articleContent}}></div><div className="article-ft"><div className="article-label"></div><div className="support-author"><p>給作者打賞,鼓勵TA抓緊創作!</p><div className="support-wrap"><Button className="btn-pay" type="danger" ghost onClick={() => toggleModal(true)}>贊賞</Button><Button className="btn-flover" type="primary" onClick={showLike} disabled={isShowLike}>{ !isShowLike ? '點贊' : '已贊'}({likeNum})</Button>{isShowLike && <Icon type="like" className="like-animation" />}</div></div></div></div><div className="sider-bar"><h2>友情贊助</h2><div className="sider-item"><img src={QTQD} alt=""/><p>公眾號《趣談前端》</p></div></div><Modal visible={isPayShow} onCancel={() => toggleModal(false)} width="300px"footer={null}><div className="img-wrap"><img src={props.supportPay.imgUrl} alt={props.supportPay.tit} /><p>{ props.supportPay.tit }</p></div></Modal></div> }export default ArticleDetail由于前臺實現起來比較簡單,至于如何定義router,如何使用骨架屏,我都在代碼里寫了完整注釋,感興趣的可以和我交流。
6.pm2部署以及nginx服務器配置
pm2做服務器持久化以及nginx做多站點的配置以及如何優化代碼的內容我會用整篇文件做一個詳細的介紹,希望大家有所收獲,如果想學習項目源碼,可以在《趣談前端》學習討論。
總結
以上是生活随笔為你收集整理的vue商城项目源码_CMS全栈项目之Vue和React篇(下)(含源码)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lodop打印不显示页码_Lodop插件
- 下一篇: gets函数会读取回车吗_会做二次函数吗