摘要:七牛云接入本系統(tǒng)的圖片,音視頻是放在七牛云,所以需要接入七牛云。在服務(wù)端通過接口請求來獲取七牛云上傳,客戶端獲取到七牛云,通過不同方案將帶上。
效果展示 github
moment-server github地址
moment github地址
moment-manage github地址
articles聊聊畢業(yè)設(shè)計系列 --- 項目介紹
聊聊畢業(yè)設(shè)計系列 --- 系統(tǒng)實現(xiàn)
前言在上一篇文章中,主要是對項目做了介紹,并且對系統(tǒng)分析和系統(tǒng)設(shè)計做了大概的介紹。那么接下來這篇文章會對系統(tǒng)的實現(xiàn)做介紹,主要是選擇一些比較主要的模塊或者說可拿出來與大家分享的模塊。好了,接入正題吧~~
MongoDB服務(wù)端這邊使用的是Express框架,數(shù)據(jù)庫使用的是MongoDB,通過Mongoose模塊來操作數(shù)據(jù)庫。這邊主要是想下對MongoDB做個介紹,當(dāng)然看官了解的話直接往下劃~~
在項目開始前要確保電腦是否安裝mongoDB,下載點我,圖像化工具Robo 3T 點我,下載好具體怎么配置還請問度娘或Google吧,本文不做介紹了哈。注意:安裝完mongoDB的時候進行項目時要把lib目錄下的mongod服務(wù)器打開哈~~
MongoDB 是一個基于分布式文件存儲的數(shù)據(jù)庫,是一個介于關(guān)系型數(shù)據(jù)庫和非關(guān)系型數(shù)據(jù)庫之間的開源產(chǎn)品,它是功能最為豐富的非關(guān)系型數(shù)據(jù)庫,也是最像關(guān)系型數(shù)據(jù)庫的。但是和關(guān)系型數(shù)據(jù)庫不同,MongoDB沒有表和行的概念,而是一個面向集合、文檔的數(shù)據(jù)庫。其中的文檔是一個鍵值對,采用BSON(Binary Serialized Document Format),BSON是一種類似于JSON的二進制形式的存儲格式,并且BSON具有表示數(shù)據(jù)類型的擴展,因此支持的數(shù)據(jù)非常豐富。MongoDB有兩個很重要的數(shù)據(jù)類型就是內(nèi)嵌文檔和數(shù)組,而且在數(shù)組內(nèi)可以嵌入其他文檔,這樣一條記錄就能表示非常復(fù)雜的關(guān)系。
Mongoose是在node.js異步環(huán)境下對MongoDB進行簡便操作的對象模型工具,能從數(shù)據(jù)庫提取任何信息,可以用面向?qū)ο蟮姆椒▉碜x寫數(shù)據(jù),從而使操作MongoDB數(shù)據(jù)庫非常便捷。Mongoose中有三個非常重要的概念,便是Schema(模式),Model(模型),Entity(實體)。
Schema: 一種以文件形式存儲的數(shù)據(jù)庫模型骨架,不具備數(shù)據(jù)庫的操作能力,創(chuàng)建它的過程如同關(guān)系型數(shù)據(jù)庫建表的過程,如下:
//Schema const mongoose = require("mongoose"); const Schema = mongoose.Schema; const UserSchema = new Schema({ token: String, is_banned: {type: Boolean, default: false}, //是否禁言 enable: { type: Boolean, default: true }, //用戶是否有效 is_actived: {type: Boolean, default: false}, //郵件激活 username: String, password: String, email: String, //email唯一性 code: String, email_time: {type: Date}, phone: {type: String}, description: { type: String, default: "這個人很懶,什么都沒有留下..." }, avatar: { type: String, default: "http://p89inamdb.bkt.clouddn.com/default_avatar.png" }, bg_url: { type: String, default: "http://p89inamdb.bkt.clouddn.com/FkagpurBWZjB98lDrpSrCL8zeaTU"}, ip: String, ip_location: { type: Object }, agent: { type: String }, // 用戶ua last_login_time: { type: Date }, ..... });
Model: 由Schema發(fā)布生成的模型,具有抽象屬性和行為的數(shù)據(jù)庫操作對象
//生成一個具體User的model并導(dǎo)出 const User = mongoose.model("User", UserSchema); //第一個參數(shù)是集合名,在數(shù)據(jù)庫中會把Model名字字母全部變小寫和在后面加復(fù)數(shù)s //執(zhí)行到這個時候你的數(shù)據(jù)庫中就有了 users 這個集合 module.exports = User;
Entity: 由Model創(chuàng)建的實體,他的操作也會影響數(shù)據(jù)庫,但是它操作數(shù)據(jù)庫的能力比Model弱
const newUser = new UserModel({ //UserModel 為導(dǎo)出來的 User email: req.body.email, code: getCode(), email_time: Date.now() });
Mongoose中有一個東西個人感覺非常主要,那便是populate,通過populate他可以很方便的與另一個集合建立關(guān)系。如下,user集合可以與article集合、user集合本身進行關(guān)聯(lián),根據(jù)其內(nèi)嵌文檔的特性,這樣子他便可以內(nèi)嵌子文檔,子文檔中有可以內(nèi)嵌子文檔,這樣子它返回的數(shù)據(jù)就會異常的豐富。
const user = await UserModel.findOne({_id: req.query._id, is_actived: true}, {password: 0}).populate({ path: "image_article", model: "ImageArticle", populate: { path: "author", model: "User" } }).populate({ path: "collection_film_article", model: "FilmArticle", }).populate({ path: "following_user", model: "User", }).populate({ path: "follower_user", model: "User", }).exec();
服務(wù)端主要是操作數(shù)據(jù)庫,對數(shù)據(jù)庫進行增刪改查(CRUD)等操作。項目中的接口,Mongoose的各種方法這邊就不對其做詳細(xì)介紹,大家可以查看Mongoose文檔。
用戶身份認(rèn)證實現(xiàn) 介紹本系統(tǒng)的用戶身份認(rèn)證機制采用的是JSON Web Token(JWT),它是一種輕量的認(rèn)證規(guī)范,也用于接口的認(rèn)證。我們知道,HTTP協(xié)議是一種無狀態(tài)的協(xié)議,這便意味著每個請求都是獨立的,當(dāng)用戶提供了用戶名和密碼來對我們的應(yīng)用進行用戶認(rèn)證,那么在下一次請求的時候,用戶需要再進行一次用戶的認(rèn)證才可以,因為根據(jù)HTTP協(xié)議,我們并不能知道是哪個用戶發(fā)出的請求,本系統(tǒng)采用了token的鑒權(quán)機制。這個token必須要在每次請求時傳遞給服務(wù)端,它應(yīng)該保存在請求頭里,另外,服務(wù)端要支持CORS(跨來源資源共享)策略,一般我們在服務(wù)端這么做就可以了Access-Control-Allow-Origin: *。
在用戶身份認(rèn)證這一塊有很多方法,最常見的像cookie ,session。那么他們?nèi)g又有什么區(qū)別,這里有兩篇文章介紹的挺全面。
正確理解HTTP短連接中的Cookie、Session和Token
小白必讀:閑話HTTP短連接中的Session和Token
token 與 session的區(qū)別在于,它不同于傳統(tǒng)的session認(rèn)證機制,它不需要在服務(wù)端去保留用戶的認(rèn)證信息或其會話的信息。系統(tǒng)一旦比較大,都會采用機器集群來做負(fù)載均衡,這需要多臺機器,由于session是保存在服務(wù)端,那么就要 去考慮用戶到底是在哪一臺服務(wù)器上進行登錄的,這便是一個很大的負(fù)擔(dān)。
那么就有人想問了,你這個系統(tǒng)這么小,為什么不使用傳統(tǒng)的session機制呢?哈~因為之前自己的項目一般都是使用session做登錄,沒使用過token,想嘗試嘗試入入坑~~哈哈哈~
實現(xiàn)思路JWT主要的實現(xiàn)思路如下:
在用戶登錄成功的時候創(chuàng)建token保存于數(shù)據(jù)庫中,并返回給客戶端。
客戶端之后的每一次請求都要帶上token,在請求頭里加入Authorization,并加上token.
在服務(wù)端進行驗證token的有效性,在有效期內(nèi)返回200狀態(tài)碼,token過期則返回401狀態(tài)碼
如下圖所示:
在node中主要用了jsonwebtoken這個模塊來創(chuàng)建JWT,jsonwebtoken的使用請查看jsonwebtoken文檔。項目中創(chuàng)建token的中間件createToken如下
/** * createToken.js */ const jwt = require("jsonwebtoken"); // 引入jsonwebtoken模塊 const secret = "我是密鑰" //登錄時:核對用戶名和密碼成功后,應(yīng)用將用戶的id(user_id)作為JWT Payload的一個屬性 module.exports = function(user_id){ const token = jwt.sign({ user_id: user_id }, secret, { //密鑰 expiresIn: "24h" //過期時間設(shè)置為24h。那么decode這個token的時候得到的過期時間為:創(chuàng)建token的時間+設(shè)置的值 }); return token; };
return 出來的 token 類似eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0.Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM。我們仔細(xì)看這字符串,分為三段,分別被 "." 隔開?,F(xiàn)在我們分別對前兩段進行base64解碼如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ===> {"alg":"HS256","typ":"JWT"} 其中 alg是加密算法名字,typ是類型 eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0 ===> {"user_id":"admin","iat":1534684070,"exp":1534770470} 其中 name是我們儲存的內(nèi)容,iat創(chuàng)建的時間戳,exp到期時間戳。 Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM ===> 最后一段是由前面兩段字符串,HS256加密后得到。所以前面的任何一個字段修改,都會導(dǎo)致加密后的字符串不匹配。
當(dāng)我們根據(jù)用戶的id創(chuàng)建獲取到token之后,我們需要把token返回到客戶端,客戶端對其在本地(localStorage)保存, 客戶端之后的每一次請求都要帶上token,在請求頭里加入Authorization,并加上token,服務(wù)端進行驗證token的有效性。那么我們?nèi)绾悟炞Ctoken的有效性呢? 所以我們需要checkToken這個中間件來檢測token的有效性。
/** * checkToken */ const jwt = require("jsonwebtoken"); const secret = "我是密鑰" module.exports = async ( req, res, next ) => { const authorization = req.get("Authorization"); if (!authorization) { res.status(401).end(); //接口需要認(rèn)證但是有沒帶上token,返回401未授權(quán)狀態(tài)碼 return } const token = authorization.split(" ")[1]; try { let tokenContent = await jwt.verify(token, secret); //如果token過期或驗證失敗,將拋出錯誤 next(); //執(zhí)行下一個中間件 } catch (err) { console.log(err) res.status(401).end(); //token過期或者驗證失敗返回401狀態(tài)碼 } }
那么現(xiàn)在咱們只要在需要用戶認(rèn)證的接口上,在操作數(shù)據(jù)之前,加上checkToken中間件即可,如下調(diào)用:
//更新用戶信息 router.post("/updateUserInfo", checkToken, User.updateUserInfo) //如果checkToken檢測不成功,它便返回401狀態(tài)碼,不會對User.updateUserInfo做任何操作, 只有檢測token成功,才能處理User.updateUserInfo
我們?nèi)绾伪WC每次請求都能在請求頭里加入Authorization,并加上token,這就要用到Axios的請求攔截,并且也用到了它的響應(yīng)攔截,因為在服務(wù)端返回401狀態(tài)碼之后應(yīng)要執(zhí)行登出操作,清楚本地token的存儲,具體代碼如下:
//request攔截器 instance.interceptors.request.use( config => { //每次發(fā)送請求之前檢測本地是否存有token,都要放在請求頭發(fā)送給服務(wù)器 if(localStorage.getItem("token")){ if (config.url.indexOf("upload-z0.qiniup.com/putb64") > -1){ config.headers.Authorization = config.headers["UpToken"]; //加上七牛云上傳token } else { config.headers.Authorization = `token ${localStorage.getItem("token")}`.replace(/(^")|("$)/g, ""); //加上系統(tǒng)接口token } } console.log("config",config) return config; }, err => { console.log("err",err) return Promise.reject(err); } ); //response攔截器 instance.interceptors.response.use( response => { return response; }, error => { //默認(rèn)除了2XX之外的都是錯誤的,就會走這里 if(error.response){ switch(error.response.status){ case 401: console.log(error.response) store.dispatch("ADMIN_LOGINOUT"); //可能是token過期,清除它 router.replace({ //跳轉(zhuǎn)到登錄頁面 path: "/login", query: { redirect: "/dashboard" } // 將跳轉(zhuǎn)的路由path作為參數(shù),登錄成功后跳轉(zhuǎn)到該路由 }); } } return Promise.reject(error.response); } );
其中的if else 是因為本系統(tǒng)的圖片,音視頻是放在七牛云,上傳需要七牛云上傳base64圖片的時候token是放在請求頭的,正常的圖片上傳不是放在請求頭,所以這邊對token做了區(qū)分,如何接入七牛云也會在下面模塊介紹到。
七牛云接入本系統(tǒng)的圖片,音視頻是放在七牛云,所以需要接入七牛云。七牛云分了兩種情況,正常圖片和音視頻的上傳和base64圖片的上傳,因為七牛云在對他們兩者上傳的Content-Type和domain(域)有所不同,正常圖片和音視頻的Content-Type是headers: {"Content-Type":"multipart/form-data"}domain是domain="https://upload-z0.qiniup.com",而base64圖片的上傳則是headers:{"Content-Type":"application/octet-stream"}domain是domain="https://upload-z0.qiniup.com/putb64/-1",所以他們請求的時候token放的地方不同,base64就像上面所說的放在請求頭Authorization中,而正常的放在form-data中。在服務(wù)端通過接口請求來獲取七牛云上傳token,客戶端獲取到七牛云token,通過不同方案將token帶上。
base64的上傳: headers:{"Content-Type":"application/octet-stream"} 和 domain="https://upload-z0.qiniup.com/putb64/-1",token放在請求頭Authorization中。
正常圖片和音視頻的上傳: headers: {"Content-Type":"multipart/form-data"}和domain="https://upload-z0.qiniup.com",token 放在 form-data中。
服務(wù)端通過qiniu這個模塊進行創(chuàng)建token,服務(wù)端代碼如下:
/** * 構(gòu)建一個七牛云上傳憑證類 * @class QN */ const qiniu = require("qiniu") //導(dǎo)入qiniu模塊 const config = require("../config") class QN { /** * Creates an instance of qn. * @param {string} accessKey -七牛云AK * @param {string} secretKey -七牛云SK * @param {string} bucket -七牛云空間名稱 * @param {string} origin -七牛云默認(rèn)外鏈域名,(可選參數(shù)) */ constructor (accessKey, secretKey, bucket, origin) { this.ak = accessKey this.sk = secretKey this.bucket = bucket this.origin = origin } /** * 獲取七牛云文件上傳憑證 * @param {number} time - 七牛云憑證過期時間,以秒為單位,如果為空,默認(rèn)為7200,有效時間為2小時 */ upToken (time) { const mac = new qiniu.auth.digest.Mac(this.ak, this.sk) const options = { scope: this.bucket, expires: time || 7200 } const putPolicy = new qiniu.rs.PutPolicy(options) const uploadToken = putPolicy.uploadToken(mac) return uploadToken } } exports.QN = QN; exports.upToken = () => { return new QN(config.qiniu.accessKey, config.qiniu.secretKey, config.qiniu.bucket, config.qiniu.origin).upToken() //每次調(diào)用都創(chuàng)建一個token }
//獲取七牛云token接口 const {upToken} = require("../utils/qiniu") app.get("/api/uploadToken", (req, res, next) => { const token = upToken() res.send({ status: 1, message: "上傳憑證獲取成功", upToken: token, }) })
由于正常圖片和音視頻的上傳和base64圖片的上傳,因為七牛云在對他們兩者上傳的Content-Type和domain(域)有所不同,所以的token請求存放的位置有所不同,因此要區(qū)分,客戶端調(diào)用上傳代碼如下:
//根據(jù)獲取到的上傳憑證uploadToken上傳文件到指定域 //正常圖片和音視頻的上傳 uploadFile(formdata, domain="https://upload-z0.qiniup.com",config={headers:{"Content-Type":"multipart/form-data"}}){ console.log(domain) console.log(formdata) return instance.post(domain, formdata, config) }, //base64圖片的上傳 //根據(jù)獲取到的上傳憑證uploadToken上傳base64到指定域 uploadBase64File(base64, token, domain = "https://upload-z0.qiniup.com/putb64/-1", config = { headers: { "Content-Type": "application/octet-stream", }, }){ const pic = base64.split(",")[1]; config.headers["UpToken"] = `UpToken ${token}` return instance.post(domain, pic, config) },
function upload(Vue, data, callbackSuccess, callbackFail) { //獲取上傳token之后處理 Vue.prototype.axios.getUploadToken().then(res => { if (typeof data === "string"){ //如果是base64 const token = res.data.upToken Vue.prototype.axios.uploadBase64File(data, token).then(res => { if (res.status === 200){ callbackSuccess && callbackSuccess({ data: res.data, result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}` }) } }).catch((error) => { callbackFail && callbackFail({ error }) }) } else if (data instanceof FormData){ //如果是FormData data.append("token", res.data.upToken) data.append("key", `moment${Date.now()}${Math.floor(Math.random() * 100)}`) Vue.prototype.axios.uploadFile(data).then(res => { if (res.status === 200){ callbackSuccess && callbackSuccess({ data: res.data, result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}` }) } }).catch((error) => { callbackFail && callbackFail({ error }) }) } else { const formdata = new FormData() //如果不是formData 就創(chuàng)建formData formdata.append("token", res.data.upToken) formdata.append("file", data.file || data) formdata.append("key", `moment${Date.now()}${Math.floor(Math.random() * 100)}.${data.file.type.split("/")[1]}`) // 獲取到憑證之后再將文件上傳到七牛云空間 console.log("formdata",formdata) Vue.prototype.axios.uploadFile(formdata).then(res => { console.log("res",res) if (res.status === 200){ callbackSuccess && callbackSuccess({ data: res.data, result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}` //返回的圖片鏈接 }) } }).catch((error) => { console.log(error) callbackFail && callbackFail({ error }) }) } }) } export default upload路由權(quán)限模塊
系統(tǒng)的后臺管理面向的是合作作者和管理員,涉及到兩種角色,故此要做權(quán)限管理。不同的權(quán)限對應(yīng)著不同的路由,同時側(cè)邊欄的菜單也需根據(jù)不同的權(quán)限,異步生成,不同于以往的服務(wù)端直接返回路由表,由前端動態(tài)生成,接下來介紹下登錄和權(quán)限驗證的思路:
登錄:當(dāng)用戶填寫完賬號和密碼后向服務(wù)端驗證是否正確,驗證通過之后,服務(wù)端會返回一個token,拿到token之后前端會根據(jù)token再去拉取一個getAdminInfo的接口來獲取用戶的詳細(xì)信息(如用戶權(quán)限,用戶名等等信息)。
權(quán)限驗證:通過token獲取用戶對應(yīng)的role,動態(tài)根據(jù)用戶的role算出其對應(yīng)有權(quán)限的路由,通過vue-router的beforeEach進行全局前置守衛(wèi)再通過router.addRoutes動態(tài)掛載這些路由。
代碼有點多,這邊就直接放流程圖哈~~
最近正好也在公司做中后臺項目,公司的中后臺項目的這邊是由服務(wù)端生成路由表,前端進行直接渲染,畢竟公司的一整套業(yè)務(wù)比較成熟。但是我們會在想能不能由前端維護路由表,這樣不用到時候項目迭代,前端每增加頁面都要讓服務(wù)端兄弟配一下路由和權(quán)限,當(dāng)然前提可能是項目比較小的時候。
賬號模塊賬號模塊是業(yè)務(wù)中最為基礎(chǔ)的模塊,承擔(dān)著整個系統(tǒng)所有的賬號相關(guān)的功能。系統(tǒng)實現(xiàn)了用戶注冊、用戶登錄、密碼修改、找回密碼功能。
系統(tǒng)的賬號模塊使用了郵件服務(wù),針對普通用戶的注冊采用了郵件服務(wù)來發(fā)送驗證碼,以及密碼的修改等操作都采用了郵件服務(wù)。在node.js中主要采用了Nodemailer,Nodemailer是一個簡單易用的Node.js郵件發(fā)送組件,它的使用可以摸我摸我摸我,通過此模塊進行郵件的發(fā)送。你們可能會問,為什么不用短信服務(wù)呢?哈~因為短信服務(wù)要錢,哈哈哈
/* * email 郵件模塊 */ const nodemailer = require("nodemailer"); const smtpTransport = require("nodemailer-smtp-transport"); const config = require("../config") const transporter = nodemailer.createTransport(smtpTransport({ host: "smtp.qq.com", secure: true, port: 465, // SMTP 端口 auth: { user: config.email.account, pass: config.email.password //這里密碼不是qq密碼,是你設(shè)置的smtp授權(quán)碼 } })); let clientIsValid = false; const verifyClient = () => { transporter.verify((error, success) => { if (error) { clientIsValid = false; console.warn("郵件客戶端初始化連接失敗,將在一小時后重試"); setTimeout(verifyClient, 1000 * 60 * 60); } else { clientIsValid = true; console.log("郵件客戶端初始化連接成功,隨時可發(fā)送郵件"); } }); }; verifyClient(); const sendMail = mailOptions => { if (!clientIsValid) { console.warn("由于未初始化成功,郵件客戶端發(fā)送被拒絕"); return false; } mailOptions.from = ""ShineTomorrow"" transporter.sendMail(mailOptions, (error, info) => { if (error) return console.warn("郵件發(fā)送失敗", error); console.log("郵件發(fā)送成功", info.messageId, info.response); }); }; exports.sendMail = sendMail;
賬號的注冊先是填寫email,填寫好郵箱之后會通過Nodemailer發(fā)送一封含有有效期的驗證碼郵件,之后填寫驗證碼、昵稱和密碼即可完成注冊,并且為了安全考慮,對密碼采用了安全哈希算法(Secure Hash Algorithm)進行加密。賬號的登錄以賬號或者郵箱號加上密碼進行登錄,并且采用上文所說的JSON Web Token(JWT)身份認(rèn)證機制,從而實現(xiàn)用戶和用戶登錄狀態(tài)數(shù)據(jù)的對應(yīng)。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/19356.html
摘要:又將整個文藝類閱讀系統(tǒng)的業(yè)務(wù)劃分為兩大部分,分別是面向管理員和合作作者的后臺管理系統(tǒng)和面向用戶的移動端,系統(tǒng)的需求分析將圍繞這兩部分進行展開。 效果展示 showImg(https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195); showImg(https://user...
摘要:又將整個文藝類閱讀系統(tǒng)的業(yè)務(wù)劃分為兩大部分,分別是面向管理員和合作作者的后臺管理系統(tǒng)和面向用戶的移動端,系統(tǒng)的需求分析將圍繞這兩部分進行展開。 效果展示 showImg(https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195); showImg(https://user...
摘要:七牛云接入本系統(tǒng)的圖片,音視頻是放在七牛云,所以需要接入七牛云。在服務(wù)端通過接口請求來獲取七牛云上傳,客戶端獲取到七牛云,通過不同方案將帶上。 效果展示 showImg(https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195); showImg(https://user...
摘要:簡單字符串緩存實戰(zhàn)完整實戰(zhàn)種設(shè)計模式設(shè)計模式是面向?qū)ο蟮淖罴褜嵺`成為專業(yè)程序員路上用到的各種優(yōu)秀資料神器及框架成為一名專業(yè)程序員的道路上,需要堅持練習(xí)學(xué)習(xí)與積累,技術(shù)方面既要有一定的廣度,更要有自己的深度。 微型新聞系統(tǒng)的開發(fā)(PHP 5.4 + MySQL 5.5) 微型新聞系統(tǒng)的開發(fā)(PHP 5.4 + MySQL 5.5) 九個很有用的 PHP 代碼 php 代碼 國內(nèi)值得關(guān)注的...
閱讀 3005·2023-04-26 02:22
閱讀 2349·2021-11-17 09:33
閱讀 3247·2021-09-22 16:06
閱讀 1159·2021-09-22 15:54
閱讀 3596·2019-08-29 13:44
閱讀 2020·2019-08-29 12:37
閱讀 1372·2019-08-26 14:04
閱讀 1975·2019-08-26 11:57