1、 項目準備javascript
npm init -y
在項目中初始化npm環境,以後就能夠用npm裏面的方法- node模塊化標準:commonjs,使用npm install某個模塊後,就會多一個node_module文件夾
- 使用vscode進行debug,相似於谷歌瀏覽器
2、 node接口處理:html
- get請求:http模塊--createServer方法建立服務,其傳入的函數中req有url,method(必須是大寫如GET),headers(content-type)等屬性,on方法,querystring模塊--parse方法處理url的參數
- post請求:接受數據採用數據流的方式,使用req.on的data方法接收數據,chunk是個二進制的參數,使用res的setHeader方法設置返回頭爲html或json,將chunk轉成字符串後的數據累加接收
const http = require("http") const server = http.createServer((req, res) => { console.log('content-type',req.headers['content-type']) //接受數據 let postData = ''; req.on('data', chunk =>{ postData +=chunk.toString() }) req.on('end', () =>{ console.log(postData) res.end("hello world") }) }) server.listen(8000);
ps:當終端裏運行了該文件以後,請求中的斷點就不會再進來了,因此想要調試的時候得把終端關掉。且想要看的斷點要是當前打開的窗口才行,否則斷點不會從那裏第一步走前端
3、搭建開發環境java
- 使用nodemon檢測文件變化,自動重啓node
- 使用cross-env設置環境變量,在package.json中的scripts屬性中根據起服務時傳的dev等值不同,選擇不一樣NODE_ENV的值
- 搭建時將server的配置和請求處理相關的配置分別寫在兩個文件中,拆分開後相互引用。而且路由和請求的處理也是分開處理的,這就是模塊化的使用。
www.js文件中:node
const http = require('http'); const PORT = 3000 const serverHandle = require('../app') const server = http.createServer(serverHandle) server.listen(PORT)
app.js文件中:mysql
const handleBlogRouter = require('./src/router/blog') const handleUserRouter = require('./src/router/user') const serverHandle = (req, res) => { // 設置返回格式 json res.setHeader('Content-type', 'application/json') //處理blog路由 const blogData = handleBlogRouter(req,res) const url = req.url req.path = url.split("?")[0] if(blogData){ res.end( JSON.stringify(blogData) ) return } }
-
初始化路由:先作出路由,再將路由和數據處理分離,這樣才符合設計原則。注意:路由是從緊挨端口號的斜槓開始對比的nginx
路由文件中:es6
const handleBlogRouter = (req, res) => { const method = req.method //獲取博客列表 if(req.path ==='/api/blog/list' && method==='GET'){ return { msg: '這是博客列表的接口' } } } module.exports = handleBlogRouter
-
所犯錯誤:web
- 把路由最前面的路徑裏的斜槓忽略了,因此返回404
- 請求的method寫成了小寫
- 調試的模式下打開的不是監聽請求的文件
- req的path賦值放在了調用路由文件方法以後,因此沒賦上值
-
開發路由,暫時拆分爲四層:redis
a>. 網絡端口及服務建立(www.js)。
b>.請求和返回參數的統一設置(app.js)。
c>.將app.js中的req和res傳進來,主要負責路由路徑的匹配(router),將該路由匹配到其所須要的處理數據的方法中,可是具體數據的處理方法如何他無論。
d>.只關心數據的處理(controller),傳入參數,返回數據便可。
6.開發post方式的路由(router負責對應路由,給各個路由分配數據就行,controller負責經過來的參數,處理具體的數據)。 其中接收數據和處理數據是異步的,因此須要用到promise,防止回調地獄(callback-hell),而且這樣能夠將獲取postData的方法和處理postData的方法用.then方法拆分開來,不用嵌套的寫在一塊兒那麼冗雜。
-
*promise講解
-
永遠返回一個promise實例,這個實例的then方法是成功的時候執行的,即then方法裏是callback方法,函數裏面的參數是形參,resolve裏傳的是實參。
-
.then方法其實是以resolve異步後成功拿到數據的實參來執行的,因此完成了異步,只有兩層嵌套,後面須要的時候再使用.then方法就行,永遠只有兩層,不會再多嵌套
//用於處理post data const getPostData = (req) => { const promise = new Promise((resolve,reject) =>{ if(req.method !=='POST'){ resolve({}) return; } if(req.headers['content-type'] !== 'application/json'){ resolve({}) return } let postData = '' req.on('data',chunk =>{ postData += chunk.toString() }) req.on('end', ()=>{ if(!postData) { resolve({}) return } resolve( JSON.parse(postData) ) }) }) return promise } 這樣,在serverHandle中使用: getPostData(req).then(postData => {})就能引用到上面異步接收到的postData數據。
//示例: 更新博客的返回(controller.js) const updateBlog = (id, blogData = {})=> {//兼容寫法,空的話就返回空對象 // id就是要更新博客的id //blogData 是一個博客對象,包含title,content屬性 return true }
es6的解構定義: const {username, password} = req.body
4、數據庫的鏈接
-
mysql的安裝和使用.
下載路徑:https://dev.mysql.com/downloads/mysql/ -
操做數據庫 (使用workbench圖形化界面)
a> 建庫
(1)建立myblog數據庫
(2)執行show databases;查詢
b> 建表
NN -- 不能爲空
PK -- 是否爲主鍵
AI -- 自動增長
右鍵左側列表中的「table」,使用drop table 刪除表,使用alter table修改表
c> 操做數據庫的表
注意:操做數據庫須要先寫use + 庫名 ,如use myblog,這樣才能訪問到庫裏的數據 在mysql中,兩個橫線加一個空格是註釋 (1)(增、刪、改、查)
注:users是表名稱 增: insert into users(username,`password`, realname) values ('lisi','123','李四'); 刪: delete from users where username='lisi'; 軟刪除: update users set state='0' where username='lisi'//設置用戶名是lisi的狀態爲0 改: update users set realname='李四2' where username<>'lisi';//<>這個符號是不等號 查: select * form users;//查找users表內的所有數據 select * form users where username='lisi';//查找users表內的username是lisi的數據 select id,username from users;//查找id,username這兩列 select id from users order by id desc; //按照id逆序排序插敘
- node操做數據庫,鏈接數據庫的文件寫在config文件夾下的db.js中,定義變量env= NODE_ENV區分開發環境和線上環境的配置。此處是非node項目中操做數據庫的代碼,node中操做的在下面的下面顯示。
const mysql = require('mysql') //建立連接對象 const con = mysql.createConnection({ host: 'localhost', user: 'root', password: 'zving10301dbk', port: '3306', database: 'myblog'//數據庫名稱 }) //開始鏈接 con.connect() //執行sql語句 //const sql = 'select id,username from users' //const sql = `update users set realname='李四2' where username='lisi'` //const sql = `insert into blogs (title,content,createtime,author) value('標題3','內容3',2222,'zhangsan')` const sql = 'select * from blogs' con.query(sql, (err,result) => { if(err) { console.log(err) return } console.log(result) }) //關閉鏈接 con.end()
上述代碼在項目中封裝後才能使用,即便用config文件夾中的db.js將環境變量保存好,,以下所示:
const env = process.env.NODE_ENV // 環境變量,直接用就行,不用引用,定義於package.json中 //配置 let MYSQL_CONF let REDIS_CONF if(env === 'dev'){ MYSQL_CONF = { host: 'localhost', user: 'root', password: 'zving10301dbk', port: '3306', database: 'myblog'//數據庫名稱 } //redis REDIS_CONF = { port: 6379, host: '127.0.0.1' } } if(env === 'production'){ MYSQL_CONF = { host: 'localhost', user: 'root', password: 'zving10301dbk', port: '3306', database: 'myblog'//數據庫名稱 } //redis REDIS_CONF = { port: 6379, host: '127.0.0.1' } } module.exports = { MYSQL_CONF,REDIS_CONF }
而後使用db文件夾的mysql.js文件將執行查詢的代碼使用promise異步包裝起來,mysql.js文件以下:
//建立鏈接對象 const con = mysql.createConnection(MYSQL_CONF) //開始鏈接 con.connect() //統一執行sql的函數,使用promise異步傳遞數據庫中查詢到的數據 function exec(sql){ const promise = new Promise((resolve,reject) => { con.query(sql,(err, result) =>{ if(err){ reject(err) return } resolve(result) }) }) return promise } module.exports = { exec }
- node的api對接mysql
從controller.js中將返回的數據寫成上面exec返回的promise實例,router的對應文件中就可使用then方法拿到實例中的數據,幷包裹errno後再返回給app.js一個promise實例,app.js中再用promise實例的then方法獲取數據便可,注意都是異步的,須要在異步外return.
//統一執行sql的函數 function exec(sql){ const promise = new Promise((resolve,reject) => { con.query(sql,(err, result) =>{ if(err){ reject(err) return } resolve(result) // console.log(result) }) }) return promise }
controller.js中: const sql =`insert into blogs (title,content,createtime,author) values ('${title}','${content}','${createtime}','${author}')` return exec(sql).then(insertData=>{ //console.log(insertData) return {id:insertData.insertId} })
router.js中: const result = getList(author, keyword) return result.then(listData => { return new SuccessModel(listData) //then方法裏返回的值會做爲新的promise對象,後面的then方法裏能夠拿到這個值 })
連環使用promise的then時,要記得使用return把promise傳出來 ,才能繼續傳的是promise。且在return中使用then方法進一步處理數據以後再return,後面的then拿到的數據都是上一步處理數據後的promise。(即return了一個promise的then方法,返回的是then方法處理過的promise數據)
5、登錄
- cookie
cookie是存在瀏覽器的一段字符串,最大5kb,跨域不共享。每次發送http請求,都會帶過去,server端能夠修改cookie並返回給瀏覽器。瀏覽器也能夠經過javascript修改cookie(有限制)
客戶端操做cookie:(不經常使用)
document.cookie='k1=v1' //能夠對cookie字符串進行累加
server端操做cookie用於登錄驗證:
在查詢後,數據庫中存在發過來用戶密碼後,後端將這樣cookie放到請求裏: res.setHeader('Set-Cookie',`username=${data.username}; path=/; httpOnly;expires=${getCookieExpires()}`) //path=/代表全部根目錄下都加該cookie //httpOnly表示只容許後端改cookie,不容許前端改 //getCookieExpires是過時時間,通常設置當前請求的一天後爲過時
即:經過get方法獲取到username和password,而後後端設置cookie給客戶端,客戶端下次再請求的時候就帶着cookie過來了。這是cookie的經常使用模式。可是很危險,會暴露username。
- session
爲解決上面暴露username的問題,能夠在cookie中存儲userid,server端對應username。這個解決方案就叫session,是一個統稱。即server端存儲用戶信息。
使用session開發登陸:
在app.js中設置userId的值,當沒有時將userId設置成時間戳的惟一值,並把對應的session(SESSION_DATA[userId])設置成空對象,當請求登錄接口時把這個對象加上username和realname的key值,之後經過userId就能匹配到對應的username了 關鍵代碼:
//解析session let userId = req.cookie.userid let needSetCookie = false if(userId) { if(!SESSION_DATA[userId]){ SESSION_DATA[userId] = {} } }else { needSetCookie = true userId = `${Date.now()}_${Math.random()}` SESSION_DATA[userId] = {} } req.session = SESSION_DATA[userId]
session的原理是切換別的用戶時,userId不會變只是把req.session裏的username和realname的value值改變了,同時也變了SESSION_DATA[userId]。
可是session的問題:
a. session直接是js變量,放在nodejs進程的內存中(web server中)
b. 進程內存有限,訪問量過大,session過大容易致使進程崩潰
c. 正式線上的運行是多線程的,進程之間內存沒法共享
解決方案:redis
- redis:
redis是web server最經常使用的緩存方案,他的數據存放在內存中(容易斷電丟失),相比於mysql這樣的硬盤數據庫,內存訪問速度快。可是成本更高,可存儲的數據量更小(內存的硬傷)。只把session放到redis中,就會解決進程擠爆的問題,而且多個進程訪問同一個redis,也不會出現沒法共享的問題。
ps:爲何session適合用redis?
a. session訪問拼房,對性能要求極高
b. session可不考慮斷電丟失數據的問題(內存的硬傷)(丟了從新登錄就好) c. session數據量不會很大(相比於mysql中存儲的數據)
node鏈接redis:
(1)在config文件夾中寫好get和set方法
//建立客戶端 const redisClient = redis.createClient(REDIS_CONF.port,REDIS_CONF.host) redisClient.on('error',err =>{ console.error(err) }) function set(key, val){ if(typeof val === 'object'){ val = JSON.stringify(val) } redisClient.set(key,val, redis.print) } function get(key){ console.log('key',key) const promise = new Promise((resolve, reject) => { redisClient.get(key,(err,val)=>{ console.log('val',val) if(err) { reject(err) return } if(key == null){ resolve(null) return } try{ resolve( JSON.parse(val) ) }catch(ex){ resolve(val) } resolve(val) }) }) return promise }
(2)在app中修改代碼,當cookie中沒有userId時,將userId賦爲隨機數,並用上一步的set方法將這個隨機數寫入redis中,並將對應的value值設置爲一個空對象。
(3)在user.js中,當調用登錄接口時,將從mysql中匹配到的用戶名和密碼使用set方法賦值給redis中上一步的空對象,此後的調用接口時,使用userId去查找,若是redis中對應的userId有非空值,就將這個值賦到req.session上,用來其餘接口的登錄驗證。
(4)從新登錄時,會更新redis中該userId對應的對象值,並賦給req.session,這樣就能夠對別的接口進行驗證是否已登錄。(6-12的視頻就是在app.js中多了查找redis的代碼,和user.js中多了set的代碼。) ps:其實很簡單,就是把session的一個變量(key是隨機的userId,value是一個對象,裏面放的username和realname的一個對象)放redis裏查,每次調接口以前查一下,登錄的時候更新一下值
6、服務端和前端聯調
- 登陸功能依賴cookie,必須用瀏覽器聯調。並且cookie跨域不共享,前端和server端必須同域。須要用到nginx作代理,讓先後端同域。
- nginx介紹:
(1)高性能的web服務器,開源免費
(2)通常用做靜態服務、負載均衡
(3)反向代理(客戶端控制不了的代理)
(4)nginx配置:/usr/local/etc/nginx/nginx.conf ,修改配置文件:sudo vi /usr/local/etc/nginx/nginx.conf,i → Insert 模式,按 ESC 回到 Normal 模式. x → 刪當前光標所在的一個字符。 :wq → 存盤 + 退出 (:w 存盤, :q 退出) (陳皓注::w 後能夠跟文件名) dd → 刪除當前行,並把刪除的行存到剪貼板裏 p → 粘貼剪貼板 就在這個文件裏,改了三個地方:(1)是把原來的端口號80改爲8080,(2)是把原來的location根目錄原來代理的文件路徑改掉,改爲本章寫的前端項目所起端口8001,(3)是加上了api路徑的代理,代理到後端所起服務8000端口下。這樣就能夠實現先後端同域聯調了。location / { proxy_pass http://localhost:8001; } location /api/ { proxy_pass http://localhost:8000; proxy_set_header Host $host; }
(5)nginx命令:nginx 啓動。nginx -s reload 重啓,nginx -s stop 中止,nginx -t測試配置文件是否正確