1. 程式人生 > 實用技巧 >81、nodejs學習——2020年07月22日13:30:25

81、nodejs學習——2020年07月22日13:30:25

81、nodejs

2020年07月22日12:56:30

一、Node的發展歷史和非同步IO機制

1. 故事的開端

很久很久以前,瀏覽器只能展示文字和圖片,並不能像現在這樣有動畫,彈窗等絢麗的特效。為了提升瀏覽器的互動性,Javascript就被設計出來;而且很快統一了所有瀏覽器,成為了前端指令碼開發的唯一標準。

2. 瀏覽器之戰

隨著網際網路的不斷普及和Web的迅速發展,幾家巨頭公司開始了瀏覽器之戰。微軟推出了IE系列瀏覽器,Mozilla推出了Firefox瀏覽器,蘋果推出了Safari瀏覽器,谷歌推出了Chrome瀏覽器。其中,微軟的IE6由於推出的早,並和Windows系統繫結,在早期成為了瀏覽器市場的霸主。沒有競爭就沒有發展。微軟認為IE6已經非常完善,幾乎沒有可改進之處,就解散了IE6的開發團隊。而Google卻認為支援現代Web應用的新一代瀏覽器才剛剛起步,尤其是瀏覽器負責執行JavaScript的引擎效能還可提升10倍,於是自己偷偷開發了一個高效能的Javascript解析引擎,取名V8,並且開源。在瀏覽器大戰中,微軟由於解散了最有經驗、戰鬥力最強的瀏覽器團隊,被Chrome遠遠的拋在身後。。。

3. Node的誕生

瀏覽器大戰和Node有何關係?

話說有個叫Ryan Dahl的歪果仁,他的工作是用C/C++寫高效能Web服務。對於高效能,非同步IO、事件驅動是基本原則,但是用C/C++寫就太痛苦了。於是這位仁兄開始設想用高階語言開發Web服務。他評估了很多種高階語言,發現很多語言雖然同時提供了同步IO和非同步IO,但是開發人員一旦用了同步IO,他們就再也懶得寫非同步IO了,所以,最終,Ryan瞄向了JS。因為JavaScript是單執行緒執行,根本不能進行同步IO操作,只能使用非同步IO。

另一方面,因為V8是開源的高效能JavaScript引擎。Google投資去優化V8,而他只需拿來改造一下。

於是在2009年,Ryan正式推出了基於JavaScript語言和V8引擎的開源Web伺服器專案,命名為Node.js。雖然名字很土,但是,Node第一次把JavaScript帶入到後端伺服器開發,加上世界上已經有無數的JavaScript開發人員,所以Node一下子就火了起來。(v8是c++寫的,node是改造過的的v8引擎)

4. 瀏覽器端JS和Node端JS的區別

相同點就是都使用了Javascript這門語言來開發。

瀏覽器端的JS,受制於瀏覽器提供的介面。比如瀏覽器提供一個彈對話方塊的Api,那麼JS就能彈出對話方塊。瀏覽器為了安全考慮,對檔案操作,網路操作,作業系統互動等功能有嚴格的限制,所以在瀏覽器端的JS功能無法強大,就像是壓在五行山下的孫猴子。

NodeJs完全沒有了瀏覽器端的限制,讓Js擁有了檔案操作,網路操作,程序操作等功能,和Java,Python,Php等語言已經沒有什麼區別了。而且由於底層使用效能超高的V8引擎來解析執行,和天然的非同步IO機制,讓我們編寫高效能的Web伺服器變得輕而易舉。Node端的JS就像是被唐僧解救出來的齊天大聖一樣,法力無邊。

5. NodeJs下載與安裝

  • 下載地址:http://nodejs.cn/download/
  • 安裝完畢,在命令列輸入:node -v檢視node的版本,如果能成功輸出,證明安裝沒有問題。
    • node -v: 提供nodejs程式碼的執行環境
    • npm -v:node包管理工具,類比於apt-get

二、ES6常用新語法

Nodejs完全支援ES6語法,本課程的內容,是已經假設你有過一些JavaScript的使用經驗的,並不是純粹的零基礎。

1. ES6新語法

什麼是ES6?

ECMA(European Computer Manufacturers Association)

由於JavaScript是上個世紀90年代,由Brendan Eich在用了10天左右的時間發明的;雖然語言的設計者很牛逼,但是也扛不住"時間緊,任務重"。因此,JavaScript在早期有很多的設計缺陷;而它的管理組織為了修復這些缺陷,會定期的給JS新增一些新的語法特性。JavaScript前後更新了很多個版本,我們要學的是ES6這個版本。

ES6是JS管理組織在2015年釋出的一個版本,這個版本和之前的版本大不一樣,包含了大量實用的,擁有現代化程式語言特色的內容,比如:Promise, async/await, class繼承等。因此,我們可以認為這是一個革命性的版本。

2. 定義變數

  • 使用const來定義一個常量,常量也就是不能被修改,不能被重新賦值的變數。
  • 使用let來定義一個變數,而不要再使用var了,因為var有很多坑;可以認為let就是修復了bug的var。比如,var允許重複宣告變數而且不報錯;var的作用域讓人感覺疑惑。
  • 最佳實踐:優先用const,如果變數需要被修改才用let;要理解目前很多早期寫的專案中仍然是用var
var i = 10;
console.log("var :" , i);

var i = 100;
console.log("var :" , i);


function test () {
    var m = 10;
    console.log("test m :", m);
}

test();
//console.log("test outside :", m);


let j = "hello"
console.log("j :" , j);

j = "HELLO"
console.log("j :" , j);

const k = [1,2,3,4];
console.log("k0 :" , k);

k[0] = 100;
console.log("k1 :" , k);

//k = [4,5,6,7];
//console.log("k2 :" , k)

3. 解構賦值

ES6 允許我們按照一定模式,從陣列和物件中提取值,對變數進行賦值,這被稱為解構(Destructuring)

  • 陣列的解構賦值

    const arr = [1, 2, 3] //我們得到了一個數組
    let [a, b, c] = arr //可以這樣同時定義變數和賦值
    console.log(a, b, c); // 1 2 3
    
  • 物件的解構賦值(常用)

    const obj = { name: '俊哥',address:'深圳', age: '100'} //我們得到了一個物件
    let {name, age} = obj //可以這樣定義變數並賦值
    console.log(name, age); //俊哥 100
    
  • 函式引數的解構賦值(常用)

    const person = { name: '小明', age: 11}
    function printPerson({name, age}) { // 函式引數可以解構一個物件
        console.log(`姓名:${name} 年齡:${age}`);
        //console.log("姓名:", name,  "年齡:", age);
    }
    printPerson(person) // 姓名:小明 年齡:11
    

4. 函式擴充套件

ES6 對函式增加了很多實用的擴充套件功能。

  • 引數預設值,從ES6開始,我們可以為一個函式的引數設定預設值, go語言有預設值嗎?

    function foo(name, address = '深圳') {
        console.log(name, address);
    }
    foo("小明") // address將使用預設值
    foo("小王", '上海') // address被賦值為'上海'
    
  • 箭頭函式,將function換成=>定義的函式,就是箭頭函式

  • 只適合用於普通函式,不要用在建構函式,不要用在成員函式,不要用著原型函式

    function add(x, y) {
        return x + y
    }
    //演示自執行函式
    //函式也是變數,可以賦值
    // 這個箭頭函式等同於上面的add函式
    (x, y) => x +y;
    // 如果函式體有多行,則需要用大括號包裹
    (x, y) => {
        if(x >0){
            return x + y
        }else {
            return x - y
        }
    }
    

5. Class繼承

由於js一開始被設計為函式式語言,萬物皆函式。所有物件都是從函式原型繼承而來,通過繼承某個函式的原型來實現物件的繼承。但是這種寫法會讓新學者產生疑惑,並且和傳統的OOP語言差別很大。ES6 封裝了class語法來大大簡化了物件的繼承。

class Person {
    constructor(name, age){
        this.name = name
        this.age = age
    }
    // 注意:沒有function關鍵字
    sayHello(){
        console.log(`大家好,我叫${this.name}`);
    }
}


class Man extends Person{
    constructor(name, age){
        super(name, age)
    }
    //重寫父類的方法
    sayHello(){
        console.log('我重寫了父類的方法!');
    }
}
let p = new Person("小明", 33) //建立物件
p.sayHello() // 呼叫物件p的方法,列印 大家好,我叫小明
let m = new Man("小五", 33)
m.sayHello() // 我重寫了父類的方法!

6. 總結

ES6 的新語法有很多,有人將它總結為了一本書。當然,ES6提出的只是標準,各大瀏覽器和node基本實現了90%以上的新特性,極其個別還沒有實現。我們目前講的是最基本的一些語法,由於你們還未了解同步和非同步的概念;Promise和async/await的內容將會在後面的課程中講解。

7. 學習資源

ES6 入門教程http://es6.ruanyifeng.com/

各大瀏覽器的支援程度http://kangax.github.io/compat-table/es6/


三、NodeJS的事件驅動和非同步IO

NodeJS在使用者程式碼層,只啟動一個執行緒來執行使用者的程式碼(go語言?)。每當遇到耗時的IO操作,比如檔案讀寫,網路請求等,nodejs會將這些耗時操作丟給底層的事件迴圈去執行,而自己則不會等待,繼續執行下面的程式碼。當底層的事件迴圈執行完耗時IO時,會執行我們的回撥函式來作為通知。這個過程就是非同步處理過程。

1. 同步vs非同步

  • 同步就是你去銀行排隊辦業務,排隊的時候啥也不能幹(阻塞)。
  • 非同步就是你去銀行用取號機取了一個號,此時你可以自由的做其他事情,到你的時候會用大喇叭對你進行事件通知。而銀行系統相當於底層的事件迴圈,不斷的處理耗時的業務(IO)。

注意,無論你感覺系統返回的有多快,那它都是非同步的。

2. 回撥函式callback

  • Node.js 非同步程式設計的直接體現就是回撥。
  • 回撥函式在完成任務後就會被呼叫,Node 使用了大量的回撥函式,Node 所有 API 都支援回撥函式。
  • 回撥函式一般作為引數的最後一個引數出現

回撥函式使用場景:我們可以一邊讀取檔案,一邊執行其他命令,在檔案讀取完成後,我們將檔案內容作為回撥函式的引數返回。這樣在執行程式碼時就沒有阻塞或等待檔案 I/O 操作。這就大大提高了 Node.js 的效能,可以處理大量的併發請求。

function foo1(name, age, callback) { }
function foo2(value, callback1, callback2) { }

3. 同步呼叫(阻塞)

請事先在當前目錄下準備檔案"input.txt",寫入任意資料。

var fs = require("fs");

var data = fs.readFileSync('input.txt');

console.log(data.toString());
console.log("程式執行結束!");

4. 非同步呼叫(非阻塞)

var fs = require("fs");

//error first callback
fs.readFile('input.txt', function (err, data) {
    if (err) return console.error(err);
    console.log(data.toString());
});

console.log("程式執行結束!");

demo

//node內建的讀取檔案的模組
let fs = require('fs')

let filename = '1.txt'

//先測試同步讀取檔案
let data = fs.readFileSync(filename, 'utf-8')
console.log('同步讀取檔案內容 data :', data)

//同步執行:
//1. 同步讀取檔案時,無需回撥函式,返回值就是讀取的資料內容
//2. 主執行緒阻塞在讀取函式這裡,一直到讀取完成後,才繼續向下執行
//3. 慢,效率低


//非同步執行:
//1. 需要註冊一個回撥函式
//2. 主線發現是非同步呼叫時,直接把任務丟給nodejs的後臺執行緒執行, 主執行緒繼續向下執行其他程式碼
//3. 當後臺執行完成時,會通知通過回撥函式通知主執行緒,主執行緒執行。



//測試非同步讀取檔案
fs.readFile(filename, 'utf-8', /*回撥函式*/ function (err, data) {
    if (err) {
        console.log('讀取檔案出錯:', err)
        return
    }

    console.log('非同步讀取檔案資料:', data)
})

console.log('非同步讀取資料2222!')

5. 非同步呼叫原理(瞭解即可)

- 事件處理機制

Node.js在主執行緒裡維護一個事件佇列,當接收到一個請求之後,會將這個請求當成一個事件放在事件佇列中,然後繼續接收其他請求。當主執行緒空閒時,就會開始遍歷這個佇列。檢查佇列中是否有要處理的事件,如果需要處理的事件不是I/O任務,那麼主執行緒親自處理,通過回撥函式,返回到上層呼叫。

如果是I/O任務,就從執行緒池裡拿一個執行緒處理這個事件,並且指定回撥函式,然後繼續迴圈處理佇列中的其他事件。

- 事件迴圈原理

當執行緒中的IO任務完成之後,會呼叫回撥函式,並且將這個完成的事件放到事件佇列的尾部,等待事件迴圈,當主執行緒再次迴圈到這個事件時,就會處理它,並且返回給上層呼叫,這個過程就是事件迴圈(Event Loop)。

- node.js執行原理圖

從左到右,從上到下,node.js被分為四層:應用層、v8引擎層、node api層、LIBUV層。

  • 應用層:JavaScript互動層,常見的就是Node.js的模組,如"http", "fs"。
  • V8引擎層:利用v8引擎來解析JavaScript語法,進而與下層api互動。
  • Node API層:為上層模組提供系統呼叫,一般由C語言來實現,和作業系統進行互動。
  • LIBUV層:是跨平臺的底層封裝,實現了事件迴圈,檔案操作等,是Node.js實現非同步的核心。

- 思維誤區

node.js內部是通過執行緒池來完成I/O操作的,但是LIBUV層對不同平臺的差異實現了統一的封裝。

Node.js的單執行緒是指JavaScript程式碼執行在單執行緒中,並不是說Node.js是單執行緒的,Node.js是多執行緒的平臺,但是對於JavaScript的處理是單執行緒的。

- Node.js的特點和適用性

  • Node.js在處理I/O任務的時候,會把任務交給執行緒池來處理,高效簡單,因此Node.js適合用於處理I/O密集型的任務,但是不適合處理CPU密集型的任務,這是因為對於非I/O任務Node.js都是通過主執行緒親自計算的,前面的任務沒有處理完的情況下就會導致後來的任務堆積,就會出現響應緩慢的情況。即使是多核CPU在使用Node.js處理非I/O任務的時候,由於Node.js只有一個事件迴圈佇列,所以只佔用一個CPU核心,但是其他的核心都會處於空閒狀態,因此會造成響應緩慢,CPU資源浪費的情況,所以Node.js不適合用於處理CPU密集型的任務。那這個功能就交由C++,C,Go,Java這些語言實現。像淘寶,京東這種大型網站絕對不是一種語言就可以實現的。語言只是工具,讓每一種語言做它最擅長的事,才能構建出穩定,強大的系統。
  • Node.js還有一個優點是執行緒安全,單執行緒的JavaScript執行方式保證了執行緒的安全,不用擔心同一個變數被多個執行緒進行讀寫造成程式崩潰。同時也免去了在多執行緒程式設計中忘記對變數加鎖或者解鎖造成隱患。

6. NodeJs能做什麼?

四、常用資料型別

1. Buffer

JavaScript 語言自身只有字串資料型別,沒有二進位制資料型別,因此在 Node.js中,定義了一個 Buffer 類,該類用來建立一個專門存放二進位制資料的快取區。

console.log("=============== buffer 與 編碼");
const buf = Buffer.from('runoob', 'ascii');
console.log(buf);
console.log(buf.toString());
console.log(buf.toString('utf8'));
console.log(buf.toString('hex'));


console.log("=============== 建立buffer")
//指定長度,預設值為buffer,string,number
const buf1 = Buffer.alloc(10, 'test')
console.log("buf1 " + buf1)

//陣列,buffer,string
let buffer = Buffer.from([1, 2,3,4])

console.log(buffer)
console.log(buffer.toString())
console.log(buffer.toJSON())

console.log("=============寫入buffer")

let buf2 = Buffer.alloc(152);
let len = buf2.write('hello world!')
console.log("len : " + len);

2. 事件

Node.js 有多個內建的事件,我們可以通過引入 events 模組,並通過例項化 EventEmitter 類來繫結和監聽事件。

let event = require('events');


//建立event
var eventEmitter = new event.EventEmitter();

//繫結事件
//名字,響應函式
eventEmitter.on('eat', function(){
    console.log("begin to eat!");
    eventEmitter.emit('drink', 'beer');
})

//繫結多個事件
eventEmitter.on('drink', function(what){
    console.log("begin to drink :", what);
    eventEmitter.emit('think');
})

console.log("before eat!");

//觸發事件
let test = ()=> {
    eventEmitter.emit('eat');
}

function thinkHandler(){
    console.log('who am I!');
}

//必須寫處理函式,否則拋異常
//eventEmitter.addListener('think');
eventEmitter.addListener('think',thinkHandler);
eventEmitter.removeListener('think', thinkHandler);
eatCount = eventEmitter.listenerCount('eat');
console.log("eat count :" +eatCount);


test();

EventEmitter一般不會單獨使用,它是基類,各個模組繼承自EventEmitter,例如fs,http等。

開啟檔案,傳送請求都是一個事件,都有觸發的名字和處理函式。

五、常用模組

1.模組系統(exports,require)

為了讓Node.js的檔案可以相互呼叫,Node.js提供了一個簡單的模組系統

一個檔案就是一個模組,使用export關鍵字實現

//hello.js
let Hello = () => {
    console.log("hello world!")
}
module.exports = Hello;

有匯出才有匯入,兩者一定是配合使用的。

//main.js 
var Hello = require('./hello'); 
Hello();

2. 全域性變數

全域性變數是指我們在任何js檔案的任何地方都可以使用的變數。

  • __dirname:當前檔案的目錄
  • __filename:當前檔案的絕對路徑
  • console:控制檯物件,可以輸出資訊
  • process:程序物件,可以獲取程序的相關資訊,環境變數等
  • setTimeout/clearTimeout:延時執行
  • setInterval/clearInterval:定時器
let array = [1,2,3,4];
console.table(array);
console.log(array);


let obj = {name : 'lily', age : 20, address : 'Shenzhen'};
console.table(obj);

console.table("hello world!");


console.log(__dirname)
console.log(__filename)



let argv = process.argv;

console.table(argv);

argv.forEach((value, index) => {
    console.log(index, value);
})


let t1 = setTimeout(function () {
   console.log('帥不過三秒');
   clearInterval(t2);
}, 3000);


let t2 = setInterval(function () {
   console.log("======= 1s 又 1s ")
    //clearTimeout(t1);
}, 1000);

3. path模組

/Users/duke/go/src/01_授課程式碼/01_shanghai_1/04_nodeCode/06-require.js

/Users/duke/go/src/01_授課程式碼 + xxxx + "/xxxx/" + "/" +

/Users/duke/go/src/01_授課程式碼/01_shanghai_1//04_nodeCode///06-require.js

path模組供了一些工具函式,用於處理檔案與目錄的路徑

  • path.basename:返回一個路徑的最後一部分
  • path.dirname:返回一個路徑的目錄名
  • path.extname:返回一個路徑的副檔名
  • path.join:用於拼接給定的路徑片段
  • path.normalize:將一個路徑正常化
  • path.resolve([from ...], to) 基於當前的執行目錄,返回一個絕對路徑,退一層演示
var path = require("path");

// 格式化路徑
console.log('normalization : ' + path.normalize('/test/test1//2slashes/1slash/tab/..'));

// 連線路徑
console.log('joint path : ' + path.join('/test', 'test1', '2slashes/1slash', 'tab', '..'));

// 轉換為絕對路徑
console.log('resolve : ' + path.resolve('main.js'));

// 路徑中檔案的字尾名
console.log('ext name : ' + path.extname('main.js'));

4. fs模組

檔案操作相關的模組

  • fs.stat/fs.statSync:訪問檔案的元資料,比如檔案大小,檔案的修改時間

  • fs.readFile/fs.readFileSync:非同步/同步讀取檔案

  • fs.writeFile/fs.writeFileSync:非同步/同步寫入檔案

  • fs.readdir/fs.readdirSync:讀取資料夾內容

  • fs.unlink/fs.unlinkSync:刪除檔案

  • fs.rmdir/fs.rmdirSync:只能刪除空資料夾,思考:如何刪除非空資料夾?

    使用fs-extra 第三方模組來刪除。

  • fs.watchFile:監視檔案的變化

let fs = require('fs')

let filename = '1.txt'
let data = fs.readFileSync(filename)

console.log('data :', data.toString())
console.log('讀取結束!')

fs.writeFileSync('./2.txt', data.toString())
console.log('同步寫結束!')


fs.writeFile('./3.txt', data.toString(), function (err) {
    if (err) {
        return
    }

    console.log('非同步寫檔案成功!')
})

console.log('非同步寫還沒成功...')

fs.stat('./1.txt', function (err, stat) {
    console.log('isFile :', stat.isFile())
    console.log('isDir :', stat.isDirectory())
})

5. stream

流(stream)是一種在 Node.js 中處理流式資料的抽象介面(基類),分為四種類型:可讀、可寫、或是可讀寫。 所有的流都是 EventEmitter 的例項。

- 有四種流型別

  • Readable - 可讀操作(例如:fs.createReadStream())。
  • Writable - 可寫操作。(例如:fs.createWriteStream())。
  • Duplex - 可讀可寫操作.(例如:net.Socket)。
  • Transform - 操作被寫入資料,然後讀出結果。(例如:zlib.createDefate())。

- 常用事件

所有的 Stream 物件都是 EventEmitter 的例項。常用的事件有:

  • data - 當有資料可讀時觸發。
  • end - 沒有更多的資料可讀時觸發。
  • error - 在接收和寫入過程中發生錯誤時觸發。
  • finish - 所有資料已被寫入到底層系統時觸發。

- 操作大檔案

傳統的fs.readFile在讀取小檔案時很方便,因為它是一次把檔案全部讀取到記憶體中;假如我們要讀取一個3G大小的電影檔案,那麼記憶體不就爆了麼?node提供了流物件來讀取大檔案。

流的方式其實就是把所有的資料分成一個個的小資料塊(chunk),一次讀取一個chunk,分很多次就能讀取特別大的檔案,寫入也是同理。這種讀取方式就像水龍頭裡的水流一樣,一點一點的流出來,而不是一下子湧出來,所以稱為流。

const fs = require('fs')
const path = require('path')

// fs.readFile('bigfile', (err, data)=>{
//     if(err){
//         throw err;
//     }
//     console.log(data.length);
// })

// 需求複製一份MobyLinuxVM.vhdx檔案,簡寫為pipe,on效果一致
const reader = fs.createReadStream('MobyLinuxVM.vhdx')
const writer = fs.createWriteStream('MobyLinuxVM-2.vhdx')
// let total = 0
// reader.on('data', (chunk)=>{
//     total += chunk.length
//     writer.write(chunk)
// })
// reader.on('end',()=>{
//     console.log('總大小:'+total/(1024*1024*1024));
// })
reader.pipe(writer);

任務:用以下知識點完成大檔案的拷貝。

  • fs.createReadStream/fs.createWriteStream
  • reader.pipe(writer)

6. Promise和asnyc/await

我們知道,如果我們以同步的方式編寫耗時的程式碼,那麼就會阻塞JS的單執行緒,造成CPU一直等待IO完成才去執行後面的程式碼;而CPU的執行速度是遠遠大於硬碟IO速度的,這樣等待只會造成資源的浪費。非同步IO就是為了解決這個問題的,非同步能儘可能不讓CPU閒著,它不會在那等著IO完成;而是傳遞給底層的事件迴圈一個函式,自己去執行下面的程式碼。等磁碟IO完成後,函式就會被執行來作為通知。

雖然非同步和回撥的程式設計方式能充分利用CPU,但是當代碼邏輯變的越來越複雜後,新的問題出現了。請嘗試用非同步的方式編寫以下邏輯程式碼:

先判斷一個檔案是檔案還是目錄,如果是目錄就讀取這個目錄下的檔案,找出結尾是txt的檔案,然後獲取它的檔案大小。

恭喜你,當你完成上面的任務時,你已經進入了終極關卡:Callback hell回撥地獄!

為了解決Callback hell的問題,Promiseasync/await誕生。

  • promise的作用是對非同步回撥程式碼包裝一下,把原來的一個回撥函式拆成2個回撥函式,這樣的好處是可讀性更好。語法如下:

    語法注意:Promise內部的resolve和reject方法只能呼叫一次,呼叫了這個就不能再呼叫了那個;如果呼叫,則無效。

    ```javascript let fs = require('fs')

//想把非同步讀取檔案的過程封裝成一個promise let readFilePromise = new Promise(function (resolve/成功時呼叫/, reject/失敗時呼叫/) {

  fs.readFile('./1.txt', 'utf-8', /*回撥函式*/ function (err, data) {
      if (err) {
          // console.log('讀取檔案出錯:', err)
          // return
          reject(err)
      }

      // console.log('非同步讀取檔案資料:', data)
      resolve(data)
  })

})

//第一次改寫,使用then方式呼叫 readFilePromise.then(res => { console.log('data :', res) }).catch(err => { console.log(err) })

  

- `async/await`的作用是直接**將Promise非同步程式碼變為同步的寫法,注意,程式碼仍然是非同步的**。這項革新,具有革命性的意義。

  語法要求:

  - `await`只能用在`async`修飾的方法中,但是有`async`不要求一定有`await`。
  - `await`後面只能跟`async`方法和`promise`。



  功能需求:寫一個函式,讀取,寫入,返回檔案狀態,傳統寫法如下:

  ```javascript
  let fs = require('fs')

  //解決辦法:把每一個非同步函式都封裝成一個pomise
  let checkStat1 = () => {
      fs.readFile('./1.txt', 'utf-8', function (err, data) {
          console.log('讀取檔案: ', data)

          fs.writeFile('./2.txt', 'utf-8', function (err) {
              if (err) {
                  return
              }

              console.log('寫檔案成功!')

              fs.stat('./2.txt', function (err, stat) {
                  if (err) {
                      return
                  }
                  console.log('檔案狀態:', stat)
                  return stat
              })
          })
      })
  }

  // checkStat1()

promise寫法如下:

let fs = require('fs')

//解決辦法:把每一個非同步函式都封裝成一個pomise
let readFilePromise = () => {
    return new Promise((resolve, reject) => {
        try {
            fs.readFile('./1.txt', 'utf-8', function (err, data) {
                console.log('讀取檔案: ', data)
                resolve(data)
            })
        } catch (e) {
            reject(e)
        }
    })
}

let writeFilePromise = (data) => {
    return new Promise((resolve, reject) => {
        fs.writeFile('./2.txt', data, 'utf-8', function (err) {
            if (err) {
                reject(err)
            }
            resolve('寫入成功!')
        })
    })
}

let statPromise = () => {
    return new Promise((resolve, reject) => {
        fs.stat('./2.txt', function (err, stat) {
            if (err) {
                reject(err)
            }
            // console.log('檔案狀態:', stat)
            resolve(stat)
        })
    })
}

//如果想使用async,await,promise,
//呼叫函式的外面修飾為async
//promise函式前面加上await

let checkStat2 = async () => {
    try {
        let data = await readFilePromise()

        let res = await writeFilePromise(data)
        console.log('res :', res)

        let stat = await statPromise()
        console.log('stat:', stat)

    } catch (e) {
        console.log(e)
    }
}

checkStat2()
console.log('333333333')

非同步程式碼的終極寫法:

  1. 先使用promise包裝非同步回撥程式碼,可使用node提供的util.promisify方法;
  2. 使用async/await編寫非同步程式碼。

六、NPM介紹

  • npm是Nodejs自帶的包管理器,當你安裝Node的時候就自動安裝了npm。
  • 當我們想使用一個功能的時候,而Node本身沒有提供,那麼我們就可以從npm上去搜索並下載這個模組。
  • 每個開發語言都有自己的包管理器,比如,java有maven,python有pip。
  • npm的海量模組,使得我們開發複雜的NodeJs的程式變得更為簡單。

檢測安裝情況

[duke ~]$ npm -v
6.2.0
[duke ~]$

- 使用淘寶NPM映象

  • 國內直接使用 npm 的官方映象是非常慢的,這裡推薦使用淘寶 NPM 映象。
  • 淘寶 NPM 映象是一個完整 npmjs.org 映象,你可以用此代替官方版本(只讀),
  • 同步頻率目前為 10分鐘 一次以保證儘量與官方服務同步。
  • 可以使用淘寶定製的 cnpm (gzip 壓縮支援) 命令列工具代替預設的 npm。

- 常用命令

# 在專案中初始化一個 package.json 檔案
# 凡是使用 npm 來管理的專案都會有這麼一個檔案
npm init

# 跳過嚮導,快速生成 package.json 檔案
# 簡寫是 -y
npm init --yes

# 一次性安裝 dependencies 中所有的依賴項
# 簡寫是 npm i
npm install

# 安裝指定的包,可以簡寫為 npm i 包名
# npm 5 以前只下載,不會儲存依賴資訊,如果需要儲存,則需要加上 `--save` 選項
# npm 5 以後就可以省略 --save 選項了
npm install 包名

# 一次性安裝多個指定包
npm install 包名 包名 包名 ...

# 安裝指定版本的包
npm install 包名@版本號

npm install [email protected]

# 解除安裝指定的包
npm uninstall 包名

# 安裝全域性包
npm install --global 包名

# 檢視包資訊
npm view 包名

# 檢視使用幫助
npm help

# 檢視某個命令的使用幫助
# 例如我忘記了 uninstall 命令的簡寫了,這個時候,可以輸入 `npm uninstall --help` 來檢視使用幫助
npm 命令 --help

# 檢視 npm 配置資訊
npm config list

- 設定淘寶映象

npm config set registry https://registry.npm.taobao.org

常見錯誤:

json parse faild }....@solc

解決辦法:

npm cache clean --force

4. 互動式直譯器

- 簡單計算

$ node
> 1 +4
5
> 5 / 2
2.5
> 3 * 6
18
> 4 - 1
3
> 1 + ( 2 * 3 ) - 4
3
>

- 使用變數

不用變數時直接輸出,使用時被變數接收

$ node
> x = 10
10
> var y = 10
undefined
> x + y
20
> console.log("Hello World")
Hello World
undefined

- 下劃線(_)變數

你可以使用下劃線(_)獲取上一個表示式的運算結果:

$ node
> var x = 10
undefined
> var y = 20
undefined
> x + y
30
> var sum = _
undefined
> console.log(sum)
30
undefined

END

2020年07月22日13:02:48