node.js全棧開發之旅(啓航篇),一分鐘實現todo list後臺

作爲一名前端開發工程師,時常糾結因而否要學習後端開發,成爲一名真正的全棧,後端開發固然首選是node.js,由於不須要從新學習一門新的開發語言,但是node.js好像很難啊,深刻淺出node.js這本書成功勸退了許多想學全棧的同窗,由於一開始就把後端開發最艱深的一面展現了出來,如模塊和內存管理等,雖然對於有更高追求的開發者來講, 這些原理確定是掌握的越深刻越好,可是對於普通的後臺業務開發來講,其實只須要會用能實現業務就能夠了。javascript

咱們今天就從最簡單的todo list項目開始,揭開後臺開發的神祕面紗,幫你走上全棧開發工程師之路。爲何選擇todo list?由於它是最具表明性的項目,也最爲你們所熟悉,接受難度最小,麻雀雖小,五臟俱全,它包含了先後端開發必備的技能,能夠說只要徹底掌握了todo list,就能夠依葫蘆化瓢完成大部分項目的功能開發。 本文是一個能夠完整包含先後端代碼可以直接運行的項目, 效果以下:php

本教程適合有必定前端基礎、輕微瞭解node.js的開發人員(其實只須要知道npm安裝和引用模塊就已足夠)html

咱們打算從零開始,步步爲營逐漸完善整個項目代碼前端

  1. 第一步,先實現一個最簡化的純靜態版本的todolist
  2. 第二步,創建後臺接口,用mock測試數據填充,讓頁面先可以正常顯示出來
  3. 第三步,設計數據庫,將測試數據改成真正的接口

其實這也是真實項目的開發流程,如今的先後臺開發都是分離的,先後臺開發先按照接口約定各自開發代碼,前端用靜態數據模擬(mock)可以完成99%以上的功能開發,後端都是純數據,本身單元測試就行了,所有開發完畢以後,先後臺對接部署在測試環境。vue

邁步從頭躍,前端純靜態版todolist

爲了保持教程的簡單,前端代碼用jquery實現,先創建一個index.html的空白文件,在頁面引入jquery,並添加一個ul元素 作爲容器。加一個文本框和添加按鈕,html代碼以下:java

<ul id="list"></ul>
<input type="text" id="title" placeholder="輸入待辦事項">
<button id="btn_add">添加</button>
複製代碼

如今尚未後臺數據庫,數據只能先用靜態的數組來定義,每條記錄有id,title,status 三個屬性,分別表示事項的編號、標題、是否已完成。 定義一個數組存放用測試數據,而後用forEach循環該數組,對html字符串進行拼接,最後合併html生成dom,該示例用了ES6的模板字符串,方便書寫和演示。node

代碼以下:mysql

function render(){
        var html="";
        var result=[
        {id:1,title:"hello",status:0},
        {id:2,title:"hello",status:0} 
        ];
        result.forEach(function(item){
            var checked=item.status>0?"checked":"";
            var li=`<li itemId="${item.id}"> <input type="checkbox" ${checked}> <input type="textbox" value="${item.title}"> <a href="javascript:" class="delete">刪除</a> </li>`
            html+=li;
        })
        $("#list").html(html);
       
    
}
$(function (){
    render();
})
複製代碼

運行 index.html,成功顯示出頁面。react

第二次迭代,用mock數據,鏈接真實的後臺接口

好了,靜態頁面渲染已經寫好,要邁出後臺開發的第一步了,把靜態數據改爲調用ajax 接口,對render函數稍作改造便可jquery

function render(){
    $.post("http://localhost/todo/list",{},function (result){
        var html="";
        result.forEach(function(item){
            var checked=item.status>0?"checked":"";
            var li=`<li itemId="${item.id}"> <input type="checkbox" ${checked}> <input type="textbox" value="${item.title}"> <a href="javascript:" class="delete">刪除</a> </li>`
            html+=li;
        })
        $("#list").html(html);
    });
}
複製代碼

可是問題是如今尚未後臺服務器,並且本地的html文件發送ajax請求會遭遇跨域錯誤,有在本地作過開發的同窗必定知道ajax會產生cors異常,由於本地的文件是用file://協議打開的,若是訪問http://協議的頁面,會由於同源策略致使跨域錯誤,同源策略要求:

  1. 協議相同
  2. 域名相同
  3. 端口相同

控制檯的報錯信息以下圖所示:

這個問題實際上是有辦法解決的,只要後臺接口實現了cors跨域,就能夠暢通無阻的調用了,這樣你的代碼不用部署到遠程服務器,就能夠調用遠程服務器的接口,很是的方便。那麼先來搭建個http服務實現cors吧,少年!咱們今天不用express,也不用koa,由於他們都沒有mock功能,跨域也要額外編寫不少代碼。所以我本身封裝了一個,名爲webcontext,github地址: github.com/windyfancy/…

經過npm能夠安裝,咱們在硬盤上建個目錄,例如todo_sample,用於存放後臺項目,切換進入該文件夾,運行npm install安裝

npm install --save webcontext
複製代碼

安裝好以後,在項目根目錄建一個app.js,用於啓動http服務,寫兩行代碼:

app.js

const WebContext = require('webcontext');
const app = new WebContext();
複製代碼

運行node app.js,http服務就啓動了,而後訪問http://localhost/,可以輸出hello信息,表示http服務已經搭建成功!

接下來,在項目根目錄/service目錄中創建一個todo子目錄,將在todo目錄中創建一個list.ejs空白文件,目錄結構以下

|-- service                          
|     |--todo
|         |--list.ejs
複製代碼

把以前的靜態數據存入list.ejs中,它實際上是個ejs模板文件,固然也能夠存放json

[
    {id:1,title:"hello",status:0},
    {id:2,title:"hello",status:0}
]
複製代碼

如今再來用瀏覽器直接訪問:http://localhost/todo/list ,已經能夠直接輸了該文件的內容了,可是用本地的index.html調用這個接口仍然是返回跨域錯誤的,爲了安全考慮, webcontext 跨域的配置默認是關閉的,咱們打開項目根目錄的web.config.json(首次運行時會自動生成),修改cors屬性中的allowOrigin字段值爲*便可開啓跨域

"cors":{
        "allowOrigin":"*"
    }
複製代碼

如今再來訪問本地的index.html,發現已經能夠請求成功了,http 響應頭正確的輸出了cors 跨域的信息。

戰前準備工做,設計mysql數據庫

上一步完成了後臺http接口的搭建,用mock靜態數據驗證了todolist的加載功能正常。終於到了激動人心的數據庫開發環節了,想一想立刻就能夠作個高大上的CURD boy了,走上人生巔峯,出任CEO,心情真是有點小激動呢。

簡單瞭解一下吧,顧名思義,數據庫是用來存放數據的地方,目前主流的數據庫是關係數據庫,如mysql、oracle、sql server等,以行列結構存儲一張張表的數據,就如同一個excel表格,每一張表是一個獨立的sheet,咱們把剛纔靜態的json數據轉換成表的形式以下:

id title status
1 hello 0
2 world 0

以mysql爲例,來定義一下這張todo_list表的結構,共有3個字段:

  • id:表示待辦事項的編號,數值類型,在數據庫中以int類型表示,它是每條記錄的惟一標識,即主鍵
  • title:表示待辦事項的名稱,字符串類型,在數據庫中以varchar變長字符串類型表示
  • status:表示待辦事項的狀態,布爾類型,爲了便於擴展咱們定義成數據類型,也用int類型表示

咱們來建表吧,首先確定要安裝mysql了

上官方網站https://www.mysql.com/downloads/ 下載安裝一下,完整安裝一下。裝 好以後,就 能夠用自帶的workbench鏈接數據庫了。用你剛纔安裝時初始設定的密碼鏈接一下:

鏈接上以後,數據庫仍是空的呢,要先新建一個數據庫(schema),名稱爲todo_db,而後在這個數據庫中新建一個表,能夠用工具欄或菜單中的create new table快速建立一張表:

固然也能夠用sql代碼去建立表:

CREATE TABLE `todo_list` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(45) DEFAULT NULL,
  `status` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
)
複製代碼

建立好數據庫表以後,咱們嘗試用代碼鏈接數據庫,webcontext框架已經集成了數據庫的鏈接,只須要配置一下,在app.js啓動時就能夠自動鏈接數據庫了,不用編寫任何額外的代碼

修改web.config.json,設置數據庫鏈接參數

"database":{ 
        "host":"127.0.0.1",
        "port":"3306",
        "user":"root",
        "password":"你的密碼",
        "database":"todo_db"
    } 
複製代碼

注:MySQL8.0以上版本密碼認證協議發生了改變,須要用mysql workbench執行以下代碼才能使用node.js鏈接數據庫,sql代碼以下: ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密碼';

好了,如今從新運行一下node app.js,就能夠自動鏈接數據庫了

第三次迭代,編寫數據庫查詢保存接口,實現CURD

數據庫鏈接成功後,咱們要測試一下,可否正常查詢和寫入數據,由於如今數據庫仍是空的,咱們先從插入數據開始。

什麼是CURD?

它表明數據庫的 建立(Create)、更新(Update)、讀取(Retrieve)和刪除(Delete) 4個基本操做。對於關係型數據庫,能夠經過sql (結構化查詢語言 Structured Query Language 簡稱SQL) 來編寫代碼支持這4個基本操做,分別對應insert,update,select,delete 4條語句。

webcontext已經對insert,update,select,delete進行了封裝,所以對於簡單的讀寫操做,不須要編寫原始的sql語句了。

insert

在項目目錄建立一個js文件,/service/todo/add.js,webcontext實現了頁面地址自動路由,不用手工編寫路由代碼,當訪問http://localhost/service/todo/add時,會自動調用這個路徑(/service/todo/add.js)文件中的onLoad方法,爲add.js並編寫以下代碼:

module.exports= {
    async onLoad() {
        await this.database.insert("todo_list",{  title:"hello",status:0});
        this.render({code:"OK"})
    }
}
複製代碼

只有4行代碼, 咱們來逐行解釋一下

  1. module.exports:表示導出這個對象,這樣webcontext框架才能自動引用到它,這個對象會自動從Context類繼承,你不用編寫任何extend或修改prototype的代碼 , webcontext框架內部實現了對/service目錄下的文件自動添加路由和自動繼承的功能。不用任何的require和extend,具體原理也很是簡單,能夠看下個人這篇文章->:Node.js 實現相似於.php,.jsp的服務器頁面技術,自動路由
  2. async onLoad 函數:首先,這是一個異步函數,因爲第3行代碼訪問數據庫用了await,因此這裏必需要加async關鍵字,而後onLoad是一個事件,也能夠說是一個回調函數,它表示這個函數的代碼是在後臺接收到http請求後執行。
  3. this.database.insert, 因爲當前文件自動繼承自Context類,能夠經過this獲取到請求對象request、響應對象response,以及database對象,database對象封裝了基本的curd操做。insert方法第一個參數表示要插入的表名,第二個參數是一個對象,表示要插入的字段名和字段值,爲了便於測試,咱們先用死數據測試一下 { title:"hello",status:0}
  4. this.render 將傳入的字符串或對象輸出到http響應。傳入object的話會自動stringify。

寫好以後,咱們來訪問http://localhost/service/todo/add,而後使用mysql workbench查看一下數據庫,發現數據已經成功入庫了。 測試成功後,咱們把這行寫死的數據改過來吧,this.request.data["title"]表示獲取post表單中的title字段。

await this.database.insert("todo_list",{  title:this.request.data["title"],status:0});
複製代碼

select

如今數據庫裏已經有數據了,咱們要把請求mock的接口改爲讀取真實數據。刪掉list.ejs(固然不刪留着它作活口也是能夠的) 在/service/todo目錄新建一個list.js文件,書寫代碼以下:

/service/todo/list.js

module.exports= {
    async onLoad() {
        var result=await this.database.select("todo_list")
        this.render(result); 
    }
}
複製代碼

不用過多解釋了,套路和上一個頁面同樣,你惟一要改的就是把insert方法改爲select方法,這個例子比較簡單,只須要傳一個表名就能夠了。實際上select方法已經封裝的很是強大,支持各類where條件、排序、數據庫層分頁、多表鏈接等,暫不展開講述。

update

/service/todo/update.js

module.exports= {
    async onLoad() {
        await this.database.update("todo_list",this.request.data)
        this.render({code:"OK"})
    }
}
複製代碼

update和insert寫法幾乎同樣,爲了增長點新鮮感,第二個參數直接用this.request.data表單數據了,這樣能夠節省不少代碼,可是若是別人惡意post不合法的數據的話你的代碼會報錯。

delete

/service/todo/delete.js

module.exports= {
    async onLoad() {
        await this.database.delete("todo_list",{ id:this.request.data["id"]})
        this.render({code:"OK"})
    }
}
複製代碼

刪除,只須要表名和id參數

優化:從新編寫路由

好了,如今CURD操做都已經完成了,業務代碼只有8行,其實能夠更少,由於mysql 支持replace into語句,insert 和 update能夠合二爲一。即便加上一些參數的合法性校驗,代碼量也是很是少的。如今先後臺分離以後,數據庫業務的後臺開發 真的要比前端要簡單不少,除了一些多表鏈接和統計的sql語句比較難寫以外,其它都是重複度很高的數據操做代碼,不過話說回來,自從前端普及了vue,react以後,開發門檻也下降了不少。

這個項目比較簡單,後臺只有這麼幾行代碼,卻要拆成4個文件實現,能不能更精簡一點呢,答案是確定的,webcontext支持經過擴名直接映到js文件中的函數!例如請求http://localhost/todo.list,會直接調用/service/todo.js中的list()方法。咱們刪掉剛纔編寫的todo目錄的代碼,從新實現 ,把他們整理到一個文件中便可。

/service/todo.js

module.exports= {  
  onRequest() {},
  async list() {
    var result=await this.database.select("todo_list")
    this.render(result); 
  }, 
  async add() {
      await this.database.insert("todo_list",{  title:this.request.data["title"],status:0});
      this.render({code:"OK"})
  },
  async update() {
      await this.database.update("todo_list",this.request.data)
      this.render({code:"OK"})
  },
  async delete() {
    await this.database.delete("todo_list",{ id:this.request.data["id"]})
    this.render({code:"OK"})
  }
}


複製代碼

咱們在地址欄,訪問http://localhost/todo.list 測試一下,成功!可以正常返回數據。

第四次迭代,增長添加,保存和刪除前端代碼

如今後臺接口都完成了,可是添加、修改、刪除的前端代碼尚未實現,咱們用事件委託的方式實現,前端代碼都很是簡單再也不詳細描述,完整代碼以下。

function saveItem(target){
    var li=$(target).parents("li");
        var id=li.attr("itemId")
        var title=li.find("input[type=textbox]").val();
        var status=li.find("input[type=checkbox]").prop("checked")?1:0;
        $.post("http://localhost/todo.update",{id:id,title:title,status:status},function (res){
            if(res.code="OK"){
                render();
            }
        });
}
function deleteItem(target){
    var id=$(target).parents("li").attr("itemId")
    $.post("http://localhost/todo.delete",{id:id},function (res){
        if(res.code="OK"){
            render();
        }
    });
}
$("#list").on("change","input[type=textbox]",function (e){
    saveItem(e.target);
})
$("#list").on("click","input[type=checkbox]",function (e){
    saveItem(e.target)
})
$("#list").on("click",".delete",function (e){
    deleteItem(e.target)
})
$("#btn_add").click("click",function (e){
    $.post("http://localhost/todo.add",{title:$("#title").val()},function (res){
        if(res.code="OK"){
            render();
        }
    });
})
複製代碼

清理戰場 ,靜態文件遷移部署到http服務器

如今全部代碼都已經完工,但是html和js文件總不能一直放在本地吧,webcontext已經內置了靜態文件服務,只須要把本地的index.html和jquery.js存放在站點根目錄下/client目錄下,再來訪問http://localhost/index.html,就能夠訪問到了。 具體原理請參考:Node.js 實現相似於.php,.jsp的服務器頁面技術,自動路由

最後,既然已經都在同一個http路徑下了,能夠把代碼裏$.post的絕對路徑都改爲相對路徑。

總結

本文用了8行代碼實現了todolist 後臺接口,爲何只須要這麼少的代碼,源於webcontext的設計理念:約定優於配置,默認配置優於手工配置,配置優於編碼,將開發者的用戶體驗放在第一位。

舉例來講:

  1. 它實現了服務器頁面技術,再也不須要添加路由的代碼,由於頁面地址自己已經包含了路由信息,一個文件處理一個請求,也便於解耦,也能夠一個文件處理多個請求,經過url直接調用js文件中的方法,如todo.list就調用todo.js文件中的list方法,不用每增長一個請求路徑就去修改路由文件。
  2. 配置了數據庫鏈接字符串,自動鏈接數據庫,一行代碼實現CURD,也實現了ORM數據庫實體映射功能,實現了不須要寫sql就能夠操做數據庫。
  3. 默認配置文件是自動生成的,大部分狀況下都不須要修改這些配置。
  4. 對於表單、上傳、json,各個環節都是自動解析爲對象的,甚至能夠將表單數據直接寫入數據庫,省去中間轉換參數的冗餘代碼

webcontext的結構設計參考了asp.net的優雅設計,用一個context對象作爲容器,很是方便 的調用request,response,session這些http請求處理操做類,即便在ejs模板中,也能夠經過this獲取到context,直接進行http處理或數據庫操做。並擴展了許多功能,如使用database對象操做數據庫,logger對象寫入日誌等。 主要結構以下:

雖然提供瞭如此多的功能,它的代碼卻很是精簡,只有千行左右,很容易讀懂,它雖然強大,卻不是陽春白春,並不高深,若是你想了解一個web框架是如何鑄成的,不妨來讀一下它的源碼,若是你認爲有用,請不要吝惜你的star,它如今仍是一棵小樹,須要你的支持。

github地址:github.com/windyfancy/…

本文的所有代碼也已經上傳github,在webcontext_examples項目中,有須要的同窗能夠自行查閱。

相關文章
相關標籤/搜索