阅读 14

Node.js学习笔记

一、学习目的和任务

1. 学习目的

  • 了解服务端编程;
  • 使用 Express 构建 Web 项目;

2. 学习任务

  • Node.js 是什么,诞生的作用;
  • Node.js 的基本使用;
  • 如何使用 Express 构建项目;

3. 什么时候适合进阶

  • 学习后端开发;
  • Node.js 作为主要后端语言学习;

二、Node.js概述

参见上一篇文章《Node.js的异步I/O

三、Node.js的基本使用

1. 模块化

Javascript 存在的历史性问题:

  1. 文件依赖:文件的依赖按照导入的先后顺序确定,依赖不明确且需要手动控制导入顺序;

  2. 命名冲突:不同文件中,同一个变量、方法等重复命名,后命名的会覆盖前者;

规范:

  • 使用 exports 导出本模块中的成员、方法等;
  • 使用 require 导入其他模块;

导入系统模块

const http = require('http');
复制代码

导出自定义方法:

function test() {
    console.log('test');
}
exports.test = test;
复制代码

导入自定义方法:

const file = require('../Server/01-server.js')
file.test();
复制代码

另外,也可以使用 module.exports 导出,效果一样:

module.exports.test = test;
复制代码

2. module.exports VS exports

本质是:*obj exports = module.exports

也就是说,exports 是一个指针,指向 module 对象的 exports 属性。默认情况下两者的使用结果一致,但是如果手动修改了module 对象的 exports 属性,exports 对象的指向不会一起改变,仍然指向原来的那个内存地址;

比如:

console.log(module.exports);
console.log(exports);
module.exports = {param1: 'heheda'}
console.log(module.exports);
复制代码

总结:

  1. 如果使用了两者,那么以 module.exports 为准;
  2. 两个导出都一样,别闲的没事去改 module.exports ~~~

3. 系统模块的使用

fs.readFile:读取文件 path.join:系统不一样,分隔符也不一样; __dirname:当前文件所在文件夹绝对路径;

4. 第三方模块的使用

本地安装:在指定的文件夹下安装,只有当前目录可以使用; 全局安装:在根目录下安装(~),所有项目都可以使用;或者使用 -g

一般而言,命令行工具全局安装,库文件使用本地安装。项目中要用到哪些模块时,应该使用本地安装,不要把全局文件夹弄得很复杂很大。需要注意的是,需要先运行npm init -y 初始化项目之后才能安装第三方库。

npm init -y
npm install express
复制代码

四、创建Web服务器

导入模块:

// 创建服务器
const http = require('http')
// url解析模块
const url = require('url')
复制代码
  1. 开启服务器
const app = http.createServer()
复制代码
  1. 监听request事件
app.on('request', (req, res) => {
    // next codeing...
});
复制代码
  1. 判断请求方法
    var method = req.method.toLowerCase();
    if (method == 'post') {

    } else if (method == 'get'){

    }
复制代码
  1. 解析出请求路径
var path = '';
if (method == 'post') {
    path = req.url;
} else if (method == 'get'){
    path = url.parse(req.url).pathname;
}
复制代码
  1. 获取参数
if (method == 'post') {
    // 事件驱动
    // 开始传递参数时,触发data时间
    var params = '';
    req.on('data', (param) =>{
        console.log('params:' + param);
        params += param;
    });

    // 参数传递完成时触发end时间
    req.on('end', () =>{
        console.log(params);
    });
} else if (method == 'get'){
    var params = url.parse(req.url, true).query;
}
复制代码
  1. 路由
if (path == '/' || path == '/index') {
    res.writeHead('200', {'content-type': 'text/html;charset=UTF8'});
    res.end('<h1>北京欢迎您!</h1>')
} else if (path == '/list'){
    res.writeHead('200', {'content-type': 'text/html;charset=UTF8'});
    res.end('<h1>列表页</h1>')
} else {
    res.writeHead('404', {'content-type': 'text/html;charset=UTF8'});
    res.end('<h1>404 NOT FOUND!</h1>')
}
复制代码
  1. 监听端口
app.listen('3000');
console.log('服务器启动');
复制代码
  1. 最终代码
const http = require('http')
const app = http.createServer()
const url = require('url')

app.on('request', (req, res) => {
    var path = '';
    var method = req.method.toLowerCase();

    if (method == 'post') {
        // 事件驱动 开始传递参数时,触发data事件 
        var params = '';
        req.on('data', (param) => {
            console.log('params:' + param);
            params += param;
        });

        // 参数传递完成时触发end时间
        req.on('end', () => {
            console.log(params);
        });
        path = req.url;
    } else if (method == 'get') {
        params = url.parse(req.url, true).query;
        path = url.parse(req.url).pathname;
    }

    if (path == '/' || path == '/index') {
        res.writeHead('200', {'content-type': 'text/html;charset=UTF8'});
        res.end('<h1>北京欢迎您!</h1>')
    } else if (path == '/list') {
        res.writeHead('200', {'content-type': 'text/html;charset=UTF8'});
        res.end('<h1>列表页</h1>')
    } else {
        res.writeHead('404', {'content-type': 'text/html;charset=UTF8'});
        res.end('<h1>404 NOT FOUND!</h1>')
    }
});

app.listen('3000');
console.log('服务器启动');
复制代码

总结: Node 开发中,都是以事件驱动,其意义就是什么事件在何时触发做出怎样的响应。

五、 静态资源的处理

静态资源:同一个请求地址,没有参数,直接返回对应的资源文件; 动态资源:同一个请求地址,参数不同,返回不同的资源文件

实现: 创建一个静态资源文件夹,当客户端请求时,通过路由的方式,直接将文件夹中对应的文件响应给客户端。

最简单的处理就是将 http://domain/staticFile 路由到服务器中的 http://domain/public/staticFile,复杂点的会包含文件的递归查询等

缺点: url 中包含其他路径,不直接请求文件时会出现错误;

其中涉及到的几个点:

  1. url.parse url.parse 方法可以对传入的 url 进行解析,返回一个对象,其中几个常用的属性包括: hostname:域名 query:参数部分(?后面的内容不包含?) pathname:访问的文件名 path:完整的访问路径(包括query和?)

  2. path.join 因为系统的不同会导致文件路径中的分割符的不同,所以不能直接使用 /分割符,而是使用系统提供的 join 方法来拼接字符串,方法内部会判断系统从而决定分隔符。

  3. 第三方插件mime 使用mime来根据路径分析出请求文件的格式并在响应头中设置。虽然大多数高级浏览器都可以自动分析文件的类型从而进行展示,但是安全起见,最好是指定类型。这样设置之后,响应头中也就有了 Content-Type字段。

最终代码:

// 用于创建http服务器
const http = require('http');
// 用于将url字符串处理成对象
const url = require('url');
// 用于拼接路径
const path = require('path');
// 用于读取文件
const fs = require('fs');
// 根据请求路径分析文件类型
const mime = require('mime');

// 开启服务器
const app = http.createServer();
// 监听请求事件
app.on('request',(req,res)=>{
    var method = req.method.toLowerCase();
    
    // get方法
    if (method == 'get') {
        var pathName = url.parse(req.url).pathname;
        
        // 拼接文件路径
        var filePath = path.join(__dirname, 'public', pathName);
        if (pathName == '/'){
            // 首页处理
            filePath = filePath + 'index.html';
        }

        // 获取请求文件的类型
        var fileType = mime.getType(filePath);
        
        if (pathName.length) {
            // 读取文件,异步操作
            fs.readFile(filePath, (error, result)=>{
                if (!error) {
                    // 设置文件格式
                    res.writeHead('200',{
                        'Content-Type': fileType
                    });
                    res.end(result);
                } else {
                    // 设置响应头的响应码、文件类型、编码方式
                    res.writeHead('404',{
                        'Content-Type': 'text/html;charset=utf8'
                    })
                    // 返回响应
                    res.end('error');
                }
            });
        }
    }
})
// 开启监听
app.listen(3000);
复制代码

六、异步编程

Node 中的异步 I/O 相关的概念请查阅:Node.js的异步I/O

1. 什么是Promise

Promise 是一个容器,存放着一个操作的执行状态。这个操作通常是一个异步操作,状态分为 pending、resolved、reject。promise 实例对象在创建之后会立即执行,此时状态为 pending,而 resolved() 和 reject() 函数是改变 pending 状态并传递参数的执行函数,而 then 方法则是设置对应的回调函数。当状态改变,对应的回调函数就会异步执行。

2. 基本使用:

**resolve():**将异步操作的结果置为 resolve 并将数据作为入参传递; **reject():**将异步操作的结果置为 reject 并将数据作为入参传递; **then():**绑定回调,可以接收两个函数作为参数,第一个为 resolve 的回调函数,第二个为 reject 的回调函数; **catch():**相当于 then() 的第二个参数,绑定 reject 的回调

3. 几个比较重要的点

  1. Promise() 是对象的构造方法,入参函数会被立即执行;
function f2() {
    return new Promise((resolve, reject) => {
        // Promise 新建之后内部的代码会立即执行,因为Promise()是构造函数
        console.log('Promise-f2');
        // resolve 和 reject 是改变异步操作的结果,但是触发回调的时机并不只是调用这两个方法
        resolve();
    });
}

var promise = f2();
// then是异步操作,其内部应该是通过消息来传递和调用
promise.then(()=>{
    console.log('resolve');
},(error)=>{
    console.log('reject');
});

console.log('sync');
复制代码

输出结果:

'Promise-f2
sync
resolve
复制代码
  1. 异步操作一旦发生改变就永远固定,且状态固定之后再绑定回调仍然能够收到回调;
  2. 绑定回调的方法内部应该是包含一个查询操作和一个绑定操作。查询操作确保结果已经固定时至少收到一次回调,绑定操作确保结果从未固定(pending)变成已固定时,能够收到通知;
  3. 绑定回调的方法(then、catch)无论结果是进行中(pending)还是已经固定(resolve、reject),入参函数都是通过类似于消息的机制来传递结果,所以是异步执行;
  4. then() 只负责绑定回调,resolve()reject()只负责改变结果并传递参数;内部的消息传递机制不仅仅是靠这两个方法的调用来触发的,比如状态已固定时使用 then 绑定仍然能够进行回调。

上代码:

function f2() {
    return new Promise((resolve, reject) => {
        // Promise 新建之后内部的代码会立即执行,因为Promise()是构造函数
        console.log('Promise-f2');

        // resolve 和 reject 是改变异步操作的结果,但是触发回调的时机并不只是调用这两个方法
        resolve();

        var filePath = path.join(__dirname, 'file3.txt');

        // fs.readFile(filePath, (error, result) => {     console.log('文件读取完毕:' +
        // result); });
    });
}
/*
    then可以接收两个参数:resolve 的回调、reject 的回调;
*/
var promise = f2();
/**
 * then、resolve、reject虽然可以触发回调,但是严格来讲,回调的触发有一个类似于消息系统的存在来进行分发;
 * 基本上就是先有一个查询操作,然后:
 * 状态为pending,即为未固定。监听状态并者进行绑定,改变之后从绑定表中循环发送消息执行回调;
 * 状态已经固定时(resolve、reject),直接发送一个消息,执行回调
 * 查询操作保证状态已经固定的情况下至少收到一次结果变更的通知;
 * 绑定操作保证状态处于pending时的操作在结果改变时能够收到通知;
 * 所以,所有的回调都是异步操作,即使状态已固定时设置回调
 */
promise.then(()=>{
    console.log('resolve');
    // 结果一旦改变,之后就永远不会变;
    promise.then(()=>{
        console.log('resolve-inner');
    })
    console.log('sync-inner');

},(error)=>{
    console.log('reject');
});

console.log('sync');
复制代码

输出结果:

Promise-f2
sync
resolve
sync-inner
resolve-inner
复制代码

4. 回调地狱和Promise的代码对比

不使用 Promise:

const fs = require('fs')
fs.readFile('./1.txt', 'utf-8', (err, result) => {
    console.log(result);
    fs.readFile('./2.txt', 'utf-8', (err, result) => {
        console.log(result);
        fs.readFile('./3.txt', 'utf-8', (err, result) => {
            console.log(result);
        });
    });
});
复制代码

使用 Promise:

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

function f1() {
    return new Promise((resolve,reject) => {
        var filePath = path.join(__dirname, 'file1.txt');
        fs.readFile(filePath, (error, result) => {
            resolve(result);
            console.log('第一个文件读取完毕:' + result);
        });
    });
}

function f2() {
    return new Promise((resolve,reject) => {
        var filePath = path.join(__dirname, 'file2.txt');
        fs.readFile(filePath, (error, result) => {
            resolve(result);
            console.log('第二个文件读取完毕:' + result);
        });
    });
}

function f3() {
    return new Promise((resolve,reject) => {
        var filePath = path.join(__dirname, 'file3.txt');
        fs.readFile(filePath, (error, result) => {
            resolve(result);
            console.log('第三个文件读取完毕:' + result);
        });
    });
}

f1().then(f2).then(f3);
复制代码

f1().then(f2).then(f3);必须这么写,f2 和 f3 都不能加括号,否则函数作为参数就变成了函数的执行结果成了参数,最后 then 接收的是一个 Promise 对象作为参数,会报错;

如果使用 Promise 版本的 readFile:

const path = require('path');
const readFile = require('fs-readfile-promise');

var filePath = path.join(__dirname, 'file1.txt');
readFile(filePath)
// 返回的是包含readFile操作的promise对象
.then((buffer)=>{
    // 这里包含文件读取结果的数据,此第三方框架都使用buffer来返回数据
    console.log(buffer.toString());
})
// 上一个then方法返回的是一个空的promise对象
.then((data)=>{
    // promise内部应该只是简单的调用了resolve,以此来完成链式编程或者实现同步的代码形式
    // data为空
    console.log(data);
    
    var filePath2 = path.join(__dirname, 'file2.txt');
    // readFile经过包装会返回一个promise对象
    return readFile(filePath2);
})
// 上一个then方法返回的是一个包含了readfile操作的promise对象
.then((buffer)=>{
    console.log(buffer.toString());
});
复制代码

fs-readfile-promise 这个第三方框架会把 readFile 和 then 方法进行包装返回一个 Promise 对象,这样就不需要手动去包装异步操作了,具体的内容和进阶用法等可以去官网查阅。

另外,写一个返回 Promise 对象的函数进行封装,操作可以简化成:

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

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      console.log(data.toString());
      resolve(data);
    });
  });
};

readFile(path.join(__dirname,'file1.txt'))
.then((result)=>{
    return readFile(path.join(__dirname,'file2.txt'));
})
.then((result)=>{
    return readFile(path.join(__dirname,'file3.txt'));
})
复制代码

注意,千万不能写成如下的方式:

readFile(path.join(__dirname,'file1.txt'))
.then(readFile(path.join(__dirname,'file2.txt')))
.then(readFile(path.join(__dirname,'file3.txt')));
复制代码

这样 then 方法接收的参数是一个 Promise 对象,执行结果就可能不如预期了。

七、异步编程解决方案的演变

1. 遍历器iterator

遍历器是一个对象,一般是由集合对象返回。这个对象可以通过执行 next() 方法来遍历集合对象中的每一个元素。执行 next() 方法返回一个对象,包含集合当前元素的值、是否遍历完成,即{ value: 'a', done: false }

遍历器一般部署在 Symbol.iterator 中,系统为 Array 集合提供了遍历器:

var array = ['a', 'b', 'c', 'd'];
var notDone = true;
var iter = array[Symbol.iterator]();
while (notDone) {
    var obj = iter.next();
    console.log(obj);
    notDone = !obj.done;
}
复制代码

输出:

{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: 'c', done: false }
{ value: 'd', done: false }
{ value: undefined, done: true }
复制代码

2. Generator

首先,Generator 是一个函数,但是这个函数比较特殊:

  1. 这个函数的形式需要加 * :
function* helloWorldGenerator() {

}
复制代码
  1. 返回值是一个遍历器 iterator 对象,可以执行 next() 方法;

  2. 惰性执行,只有运行 next() 方法之后才会依次执行代码并获取返回值;

  3. 根据关键字 yeild 分段执行代码并返回对应的值,具备记忆功能;

  4. 可以获取多个返回值,return为最后一个返回值,表示函数执行完毕;

上代码:

function* helloWorldGenerator() {
    console.log('第一次调用next后本行代码执行');
    yield 'hello';// 第一次next()后在此处暂停
    console.log('第二次调用next后本行代码执行');
    yield 'world';// 第二次next()后在此处暂停
    console.log('第三次调用next后本行代码执行');
    return function(){
        console.log('ending function excuted');
    };// 函数全部执行完毕
}

var hw = helloWorldGenerator();

var isDone = false
var obj;
var index = 0;
while(!isDone) {
    index++;
    obj = hw.next();
    console.log('第' + index + '次执行next后的返回值:', obj);
    isDone = obj.done;
}
// 执行Generator的最终返回值
obj.value();
复制代码

输出结果:

第一次调用next后本行代码执行
第1次执行next后的返回值: { value: 'hello', done: false }
第二次调用next后本行代码执行
第2次执行next后的返回值: { value: 'world', done: false }
第三次调用next后本行代码执行
第3次执行next后的返回值: { value: [Function], done: true }
ending function excuted
复制代码

3. yeild的由来

异步操作顺序执行编程解决方案有好几种:

  1. 循环嵌套 例子就不举了,上文顺序读取文件的最初代码就是最好的例子;

其最大的缺点是代码横向发展、上下关系嵌套严重,动一发儿牵全身。 2. Promise

例子也不举了,上文使用 Promise 写出来的顺序读取文件的代码就是例子;

给异步操作包装 Promise 对象会引起代码量的增加,同时各种 then 方法看起来很混乱、语义不清、不容易理解,头大~~~

  1. 协程 多个线程互相协作,使用 yield 关键字来交出执行权,暂停代码的运行,当相关操作完成之后再来执行 next 获取执行权继续运行代码,而Generator 就是对协程的实现,所以 Generator 是相对于 Promise 更优的方案;

举个栗子🌰:

function* asyncJob() {
  // ...其他代码
  var f = yield readFile(fileA);
  // ...其他代码
}
复制代码

上文中的 readFile 这一样将会暂停,等到下一次主动调用 next() 后继续执行后面的代码,通常上后续代大概是这样的:

var generator = asyncJob();
var promiseObj = generator.next();
promiseObj.then((error, result)=>{
  // ...其他代码
  generator.next();
});
复制代码

那到底什么是协程,Generator 又是怎样应用在异步编程上的呢??

4. Generator在异步函数中的应用

  1. Generator 函数是协程在 ES6 的实现;
  2. Generator 是通过关键字 yield 来暂停任务并交出控制权,通过 next() 重新获取控制权,继续执行代码;
  3. Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因;总体流程大概是:执行完异步操作之后,代码暂停,等到异步操作完成之后,继续执行。
  4. 数据交换,不仅 Generator 函数可以直接传参,next() 方法也可以直接传参:
略,见https://es6.ruanyifeng.com/#docs/generator-async
复制代码
  1. 错误处理机制 Generator 函数可以不用 yield 表达式,这时就变成了一个单纯的暂缓执行函数。

正因为暂缓执行的特性,才有了 Generator 异步函数中的应用。

同样是读取文件的例子,肯定不能使用嵌套,那么必须使用 Promise,然而利用一个函数封装 Promise 的包装操作和异步操作显然更好,所以

基础代码如下:

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

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      console.log(data.toString());
      resolve(data);
    });
  });
};
复制代码

如果是使用 Promise,代码的执行如下:

readFile(path.join(__dirname,'file1.txt'))
.then((result)=>{
    return readFile(path.join(__dirname,'file2.txt'));
})
.then((result)=>{
    return readFile(path.join(__dirname,'file3.txt'));
})
复制代码

而使用 Generator 则是这样的:

function* gen(){
    var result1 = yield readFile(path.join(__dirname,'file1.txt'));
    var result2 = yield readFile(path.join(__dirname,'file2.txt'));
    var result3 = yield readFile(path.join(__dirname,'file3.txt'));
}

var g = gen();
    var result1 = g.next();
    result1.value.then((data) => {
        return g.next().value;
    })
    .then(() => {
        return g.next().value;
    })
复制代码

注意,return g.next().value; 不能写成 return g.next();,否则返回值是一个对象而不是一个 Promise 对象,运行时顺序会和预期不一样;

Generator 有一个优点,如果不看下面的代码,只看上面 gen() 函数的代码,除了 yield 关键字,那么整体代码执行起来简直和同步操作的代码一模一样,如果能封装起来,那岂不是完美?

而就有这样的第三方框架:

var co = require('co');
co(gen);
复制代码

上面两行代码就可以直接替代后面的 then、next 的代码;

那么,第三方框架是怎么实现的呢?现在就要解决个问题:co模块时如何实现自动执行 next() 的呢?

原理就暂时就不讨论了,太深入了。

此时,只需要知道怎么使用就好了。

async函数

async 函数的本质是 Generator 语法的封装,但是有几个特点:

  1. 内置执行器 不需要再手动 next() 或者使用 co 模块来自动执行了。async 函数和普通函数一样,直接执行即可;

  2. 语法更加清晰 async 代替 * ,await 代替 yield,语义上更清楚;另外,co模块限制很多,比如 yield 后面的返回值必须是 Promise 对象等。异步函数 await 后面可以是基础类型,会被包装成 resolve 的 Promise 对象。

总之,使用异步函数就像使用普通函数一样即可;

  1. 异步函数返回值是 Promise。Generator 函数返回值时 Iterator 对象,需要手动执行,且继续执行需要包装成 Promise 对象,而异步函数直接返回 Promise 对象,在所有异步操作执行完毕之后可以直接使用 then 来继续执行后续代码;

上述的例子用 async 函数写,就是这个样子:

const asyncReadFile = async function () {
    var result1 = await readFile(path.join(__dirname, 'file1.txt'));
    var result2 = await readFile(path.join(__dirname, 'file2.txt'));
    var result3 = await readFile(path.join(__dirname, 'file3.txt'));
};
asyncReadFile().then(()=>{
    console.log('所有文件读取完毕');
});
复制代码

至此,代码算是相对完美了。。

另外,暂时不需要关注异步函数的实现原理,先会用,以后真正搞服务端了再去好好学习 Thunk 函数等知识;

总结

说了这么多,异步操作的顺序执行的演变经历了如下阶段:

回调地狱 -> Promise -> Generator -> 异步函数

这里将四种方法的代码都贴出来,方便以后对比学习:

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

const readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function (error, data) {
            if (error) return reject(error);
            console.log(data.toString());
            resolve(data);
        });
    });
};

function promiseMethod() {
    readFile(path.join(__dirname, 'file1.txt'))
        .then((result) => {
            return readFile(path.join(__dirname, 'file2.txt'));
        })
        .then((result) => {
            return readFile(path.join(__dirname, 'file3.txt'));
        })
}

function genMethod() {
    function* gen() {
        var result1 = yield readFile(path.join(__dirname, 'file1.txt'));
        var result2 = yield readFile(path.join(__dirname, 'file2.txt'));
        var result3 = yield readFile(path.join(__dirname, 'file3.txt'));
    }

    var g = gen();
    var result1 = g.next();
    result1.value.then((data) => {
        return g.next().value;
    })
    .then(() => {
        return g.next().value;
    })
}

function coMethod() {
    function* gen() {
        var result1 = yield readFile(path.join(__dirname, 'file1.txt'));
        var result2 = yield readFile(path.join(__dirname, 'file2.txt'));
        var result3 = yield readFile(path.join(__dirname, 'file3.txt'));
    }

    co(gen);
}

function asyncMethod() {
    const asyncReadFile = async function () {
        var result1 = await readFile(path.join(__dirname, 'file1.txt'));
        var result2 = await readFile(path.join(__dirname, 'file2.txt'));
        var result3 = await readFile(path.join(__dirname, 'file3.txt'));
    };
    asyncReadFile().then(()=>{
        console.log('所有文件读取完毕');
    });
}

// Promise方式调用
promiseMethod();
// Generator方式调用  这个函数还有问题。执行顺序不对
genMethod(); 
// co模块方式调用 
coMethod(); 
//async方式调用 
asyncMethod()
复制代码

执行结果:

[Running] node "/Users/caoxk/Demo/WebDev/Promise/generator02.js"
hellow Promise!

hellow Promise!

hellow Promise!

hellow Promise!

hellow Promise!--2

hellow Promise!--2

hellow Promise!--2

hellow Promise!--2

hellow Promise!--3

hellow Promise!--3

hellow Promise!--3

hellow Promise!--3

所有文件读取完毕

[Done] exited with code=0 in 0.24 seconds
复制代码

八、相关文章 Gulp Express MongoDB Ajax

更多内容

关注下面的标签,发现更多相似文章
评论