簡單的Web應用,從資料的獲取到頁面的展示

BillsonKam發表於2018-07-27


  • 大神請繞道,如有說的不對的地方望指正。
  • 應屆畢業生在求職中,很多都是因為經驗不足而不被錄用(很牛的程式猿另說),在面試的時候不僅要對專業的技能熟悉掌握,最好是有自己的一些小作品小專案等等,才能博取面試官的青睞,於是很多畢業生開始自己的面試專案製作,記得回看剛出來求職的面試專案,簡直慘不忍睹,不知道你們是不是也這樣,為了打救像我一樣選擇了前端又經常逃課的應屆畢業生,於是開始了長篇大論的逼逼。
  • 最近無聊抓了點資料,寫了下很久沒用的vue,發現了一些不錯的元件。
  • 先上圖

簡單的Web應用,從資料的獲取到頁面的展示

開始入坑(資料的抓取)

* 開始專案之前,必須確立主題,想完成一個什麼Web應用(商城、音樂、外賣等等),今天就以外賣來舉個栗子。 * 首先是定製資料的需求,需要商家->商品->商品分類...這些資料的來源當然是網上了,隨便找一下一大堆關於使用Node抓取的資料,以eleme為例子,開啟eleme的官網,看到Network中獲取到的資料,根據請求可以知道資料的具體來源等資訊,可以使用node直接呼叫介面來儲存返回的資料。 > Eleme的地區商家列表資料獲取 ?
//資料地址:https://www.ele.me/restapi/shopping/restaurants?extras%5B%5D=activities&geohash=ws0e5f7pu8xe&latitude=23.041037&limit=24&longitude=113.372243&offset=0&terminal=web

let imgCount = 0,imgIndex = 0,dataCount = 0,dataIndex = 0	
// 獲取的圖片總數    正在處理的圖片下標      獲取商家的總數     正在處理的商家資料下標
const start = () => {                       //開始呼叫資料介面使用UTF8編碼獲取資料
	https.get(url, (res, req) => {
		res.setEncoding('utf8')
		let data = ""
		res.on("data", (chunk) => {
			data += chunk
		}).on("end", () => {
			gatData(JSON.parse(data))   //JSON格式化一下,再去對資料做處理,過濾一些自己想要的資料
		})
	})
}
複製程式碼
Eleme的地區商家列表資料過濾與處理 ?
const gatData = (data) => {
	let arr = [],imgarr = []
	data.forEach((item, index) => {
		let obj = {
			sid:item.id,			//商家ID
			name:item.name,			//商家名字
			address: item.address,								
			latitude: item.latitude,			//商家經度
			longitude: item.longitude,			//商家緯度
			icon: getImg(item.image_path).src	//商家圖示    
			//此處省略一大堆過濾資料
		}
		if(imgarr.indexOf(item.image_path) === -1){
			imgarr.push(item.image_path)
		}
		arr.push(obj)
	})
	imgCount = imgarr.length - 1
	dataCount = arr.length - 1
	console.log("共獲取圖片:" + imgarr.length + "張")
	console.log("共獲取資料:" + arr.length + "條")
}
複製程式碼
Eleme商家列表的圖片獲取 ?
//圖片獲取
const saveImg = (_src,list) => {
	let src = getImgType(_src).src,         //獲取圖片的地址
		name = getImgType(_src).type        //根據自己需求確定儲存圖片的名字
	https.get(src, (res) => {
		let imgData = ''
		res.setEncoding("binary")           //注意請求返回的編碼
		res.on('data', (chunk) => {
			imgData += chunk
		})
		res.on('end', () => {
			fs.writeFile("./儲存圖片的路徑/" + _src + "." + name, imgData, 'binary', (error) => {
				if(error) {
					console.log('下載失敗')
				}
				imgIndex++
				console.log('下載成功!',src)
				if(imgIndex === imgCount){
					return
				}
				download(list)
			})
		})
	})
}
複製程式碼
Eleme商家列表的資料庫的寫入-> 使用MySQL的 ?
const saveDB = (list,i) => {
	if(dataIndex === dataCount + 1){
		return console.log("全部插入完成!!!!")
	}
	//此處對資料處理 -----   必須對資料的格式做些處理
	let SQL = "INSERT INTO `ele_seller` (`sel_id`, `sel_name`,....) VALUES (NULL, '" + list[i].name + "', "....");"
	db.query(SQL, function(error, rows) {
		if(error) {
			return console.log(error)
		}
		dataIndex++
		saveDB(list,dataIndex)
		console.log("插入資料庫成功!!!__")
	})
}
//這是在人家沒有對返回的資料進行加密、還有沒有對請求域攔截的情況下可行
複製程式碼
下面是關於某商城直接抓頁面的資料 ?
//獲取資料的來源:https://www.yohobuy.com/list/ci56-gd1.html?page=1
//先是獲取左側商品的分類,再通過分類獲取具體的所屬商品
// 上衣(全部)下裝(全部)鞋子(全部)包類(全部)居家生活(全部)服配(全部)
const start = () => {
	let _url = "https://www.yohobuy.com/list/ci" + typeArr[typeIndex] + "-gd1.html?page=" + page
	superagent.get(_url).end((err, res) => {
		if(err) {
			return console.log("err", err)
		}
		gatData(res.text,typeArr[typeIndex] + "--" + page)
	})
}
const gatData = (html) => {
	let $ = cheerio.load(html),sknArr = []
	$(".good-info").map((index, item) => {
		let obj = {
			id:$(item).attr("data-skn"),
			img:imgForm($(item).find("img").eq(0).attr("data-original"))
			//此處省略一大堆資料
		}
		sknArr.push(obj)
	})
	console.log(sknArr)
} //使用Http直接請求頁面會返回403,通過superagent模組去請求頁面,再通過cheerio抓取對應資料,這種方式獲取的資料不全
複製程式碼
  • 傷心病狂
    簡單的Web應用,從資料的獲取到頁面的展示

後臺搭建

* 資料有了後臺就簡單使用express搭建,可以考慮各種問題,資料的加密,路由的攔截,使用者的註冊登入許可權判斷等等 * 資料的返回格式根據個人而定,個人比較懶,返回的一般資料結構都是: ``` { code:狀態碼, msg:操作的結果, timestamp:時間戳, data:返回的資料 } ```
express的資料加密 -> AES加密 ?
//加密的模組很多,推薦使用crypto、crypto-js
//這裡的加密解密是把加密向量一同傳送的,每次加密的加密向量都是一段固定長度的隨機字串,所以每次請求同一個URI,返回的資料一樣,但是返回的加密資料是不一樣的
//加密      data為需要加密的資料
const encode = data => {
	let key = cryptojs.enc.Latin1.parse(加密的KEY),
		ivs = crypt(),                          //這只是固定長度的隨機字串
		iv = cryptojs.enc.Latin1.parse(ivs)     //加密向量
	let backdata = cryptojs.AES.encrypt(JSON.stringify(data), key, {
			iv: iv,
			mode: cryptojs.mode.CBC,
			padding: cryptojs.pad.ZeroPadding
		}).toString()
	let json = {
		data:backdata,
		iv:ivs
	}
	return json
}
//解密     
const decode = data => {
    let key = cryptojs.enc.Latin1.parse(加密的KEY),
		iv = cryptojs.enc.Latin1.parse(config.data.result.iv)   //加密向量
	let decrypted = cryptojs.AES.decrypt(config.data.result.data, key, {
		iv: iv,
		padding: cryptojs.pad.ZeroPadding
	})
	let data = config.data
	data.result.data = JSON.parse(decrypted.toString(cryptojs.enc.Utf8))
	return data
}
複製程式碼
路由的鑑權(可以使用session,寫檔案,[token](https://mp.weixin.qq.com/s/82kGtrI1QK7gkswtd-QsAQ)等等) ?
  • token一般格式為:字串A.字串B.字串C,字串A為Header(頭部),字串B為Payload(負載),字串C為Signature(簽名)

    • Header:存放使用者的非敏感元資訊
    • Payload:存放實際傳遞的資料
    • Signature:是對前兩部分的簽名,防止修改
  • SESSION,可以使用redis或者寫檔案的方式儲存使用者的登入方式、逾期時間等等

* 這裡以Token為例子,JWT(Json Web Token)為伺服器無狀態token,一旦確認使用者身份,就頒發token,知道token過期之前,都是有權請求資源的
* Token的加密方法很多,預設採用HMAC SHA256演算法加密
const jwt = require("jsonwebtoken")
//頒發Token
const getToken = data => {      //data必須為JSON格式的物件,儲存使用者非敏感的元資訊
    jwt.sign(data, "加密的KEY", {
		expiresIn: 60 * 60 * 24,        //token的過期時間
		algorithm: "HS256",             //token的加密方式
		subject: "webside"              //token主題     
	})      //token帶的引數
}
//驗證token
jwt.verify(token, "加密的KEY", (err, decoded) => {
	if(err) {
		return false
	}
	return decoded      //decoded為token前兩個json的資料
})
複製程式碼
express路由的搭建與路由攔截鑑權 ?
  • express的路由採用由上到下的匹配模式,一旦匹配到了路由將不會繼續往下個路由匹配
//設定所有路由公用的配置
app.all("*", function(req, res, next) {
    //CROS 設定跨域、請求的方式、允許攜帶的請求頭等等
	if(req.path !== "/" && !req.path.includes(".")) {
		res.header("Access-Control-Allow-Origin", req.headers["origin"] || "*");
		res.header("Access-Control-Allow-Credentials", true);
		res.header("Access-Control-Allow-Headers", 'Content-Type,Content-Length, Authorization,\'Origin\',Accept,X-Requested-With');
		res.header("Access-Control-Allow-Methods", "POST,GET,PUT,DELETE,OPTIONS");
		res.header("Content-Type", "application/json;charset=utf-8");
	}
	next()  //路由一級一級往下傳遞,必須定義next才會執行傳遞下個路由
})
app.use("/api/eleme/catalog", require("./eleme/getCatalog"), (req, res, next) => {
	console.log("eleme => 獲取商家經營分類")
})

app.use("/api/eleme/map", require("./eleme/getAddress"), (req, res, next) => {
	console.log("eleme => 獲取城市地區列表")
})
//--------------
//上面路由匹配的是從/api/eleme/子路由進去
//下面路由匹配為/api/eleme => 定義相同 /api/eleme/子路由的許可權鑑定
//--------------
app.use("/api/eleme", (req, res, next) => {
	let token = req.body.token || req.query.token || req.headers['x-access-token']
	if(token) {
	    //許可權的鑑定
		next()      //成功才匹配/api/eleme/下的子路由
	} else {
	   //獲取token失敗,token的獲取與存放在什麼位置自定義就好
	}
})
//上面一個/api/eleme/路由開啟鑑定,下面的路由必須通過上面路由的鑑權成功才能訪問
app.use("/api/eleme/search", require("./eleme/getSearch"), (req, res, next) => {
	console.log("eleme => 商家搜尋")
})
複製程式碼
  • 剩下就是各個路由路面的細節邏輯,具體業務邏輯具體操作。

最後工作交給前端了(Web的使用者介面)

* 下面以Vue作為例子去演示Web的構建
Vue生命週期 ?
生命鉤子函式 Data DOM 描述 適用環境
beforeCreate 未初始化 未初始化 loading載入效果
created 初始化 未初始化 屬性繫結DOM,不存在,$el不存在 結束loading
beforeMount 初始化 初始化 $el為元素的虛擬節點
mounted 不保證組建在document中,$el被DOM替換 ajax請求
beforeUpdate 元件更新之前
updated 元件更新之後
activated 使用keep-alive,元件被啟用時使用
deactivated 使用keep-alive,元件被移除時使用
beforeDestory 元件被銷燬前呼叫 判斷元件是否應被刪除
destroyed 元件被銷燬後呼叫 元件移除
Vue路由的搭建 ?
//使用路由懶載入
const 頁面A = resolve => require(['./../view/頁面A.vue'], resolve)
const 頁面B = resolve => require(['./../view/頁面B.vue'], resolve)
//路有鉤子          將公用的路由鉤子業務扔到公用的鉤子函式,也可以給元件單獨設定
beforeRouteEnter (to, from, next) {
// 在渲染該元件的對應路由被 confirm 前呼叫
// 不!能!直接獲取元件例項 `this`,在next中可以用引數獲取間接獲取例項
// 因為當鉤子執行前,元件例項還沒被建立
}
beforeRouteUpdate (to, from, next) {
// 在當前路由改變,但是該元件被複用時呼叫
// 舉例來說,對於一個帶有動態引數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候,
// 由於會渲染同樣的 Foo 元件,因此元件例項會被複用。而這個鉤子就會在這個情況下被呼叫。可以訪問元件例項 `this`
}
beforeRouteLeave (to, from, next) {
// 導航離開該元件的對應路由時呼叫   可以訪問元件例項 `this`
}
const router = new Router({
	mode: 'history',
	routes: [{
		name: 'index',
		path: '/',
		component: ElemeView,
		meta:{          //對應路由的元資訊 =>  可動態設定路由下的title、description
		    title:"首頁",
		    description:"這是對首頁的描述"
		}
		redirect: '/home',
		children: [{
			name: 'home',
			path: 'home',
			component: ElemeHome
		}, {
			name: 'order',
			path: 'order',
			component: ElemeOrder,
			redirect: '/order/home'
		}]
	},{
		name: 'orderInfo',
		path: '/order/:id',
		component: OrderInfo,
		meta:{          
		    title:"訂單詳情頁",
		    description:"這是對訂單詳情頁面的描述"
		}
	}, {
		path: '*',
		redirect: '/home'
	}]
})
export default router
//當動態對meta設定的時候
router.beforeEach((to, from, next) => {
    if (to.meta.title) {            //當有title或者其他的元資訊時,可以手動動態設定(並不知道會不會對SEO有沒有效)
        document.title = to.meta.title
    }
    next()      //傳遞下去
})
複製程式碼
vuex 單向資料流的模組化編寫 ?
  • 當應用逐漸變的臃腫時,將狀態樹利用模組分開,有效管理,在呼叫mutations、actions時,帶上模組的名字,為了防止各個模組中的狀態,方法重新命名,應該使用空間命名
const Store = new Vuex.Store({
	getters: {
		router: state => state.common.router,
		cartList: state => state.cart.cartList,
		cartCount: state => state.cart.cartCount,
		cartPay: state => state.cart.cartPay,
		sellerInfo: state => state.common.sellerInfo,
		sellerList: state => state.common.sellerList,
		orderList: state => state.order.orderList
	},
	mutations: {
	    getState(state){
	        console.log(state)          //state為全域性state
	    }
	},
	modules: {
		common: commonStore,
		order: orderStore,
		cart: shopcartStore
	}
})
//元件中呼叫某模組的mutations時
this.$store.commit("order/finishOrder", e.target.dataset.id)
this.$store.commit("模組名字/該模組下的方法名字", 傳過去的資料)
//元件中使用非同步actions
this.$store.commit("模組名字/actions名字")
//直接呼叫全域性的mutations或actions時,不需要加模組名便可以呼叫
複製程式碼
ajax庫 -> 順便在ajax攔截器中將後臺的資料解密 ?
  • 只使用過兩個,都夠你用了(axios:比較好用,Fly:比較輕量可在小程式mpvue中使用)
//以axios為例子,其實都差不多的         加密解密方法上面提到,跟伺服器的方法一樣
axios.defaults.timeout = 10000              //請求超時時間
axios.defaults.retry = 4                    //請求失敗後,繼續請求,請求次數為5次

axios.interceptors.request.use((config) => {            //傳送請求的攔截    
	config.headers["Content-Type"] = "application/x-www-form-urlencoded"
	//在這裡加密    
	return config
})

axios.interceptors.response.use((res) => {              //響應請求的攔截
	if(res.status === 654) {
		window.alert('請求超時!')
	}
	if(res.data.code < 200 || res.data.result > 300) {
		window.alert('資料返回有誤')
		return Promise.reject(res)
	}
	//在這裡解密 -> 直接將解密的資料返回,其餘的無關欄位可以不用管
}, (error) => {             //失敗的後續操作
	let config = error.config
	if(!config || !config.retry) return Promise.reject(error)
	config.__retryCount = config.__retryCount || 0

	if(config.__retryCount >= config.retry) {
		return Promise.reject(error)
	}
	config.__retryCount += 1
	let backoff = new Promise(function(resolve) {
		setTimeout(function() {
			resolve()
		}, config.retryDelay || 1)
	})
	return backoff.then(function() {
		return axios(config)
	})
})

export default axios
複製程式碼
一些比較好看的vue元件 ?
  • vuePutTo:可以實現上拉載入,下拉重新整理的炫酷操作,無意中發小的,元件還挺小的。
  • vue-awesome-swiper:簡直是vue版的swiper,有點大了,其介面與swiper一致
  • vue-msgbox:彈框
  • 如果是使用大型的元件庫的時候,可以使用babel中的plugins對元件庫進行按需載入

總結

  • 以上的操作後,放上Online Demo,侵刪。
  • 獻上我剛畢業時弄的個人網站:Billson
  • 上面的一些教程,對應屆畢業生來說應該挺簡單的了,應該總比拿著學校的專案去面試要好吧,如果你是比較牛的可以考慮寫成SSR,這樣子對SEO很友好,不管技術上還是使用上,有比這個SPA更高大上一些,肯定會在面試多加積分。

相關文章