蛋疼のNode学习之路(持续更新…)

Day1

Previously on ‘Preface’,对于一个几乎零基础的我来说,搭建博客的过程是十分痛苦的,这现在只是一个简单的修改博客主题和编辑的过程啊(前端知识盲区o(╥﹏╥)o)。所以我决定花上一定时间来学习Node.js。希望学完之后我能更好的运维我的BLog(顺便复【yu】习一下Web相关知识,HTML5,CSS3,JavaScript什么的都快忘光光啦)话不多说,今天开始第一天的学习!

①那么,Node.js是乜嘢?(what?)

解释:

  • 不是一门语言,不是库,不是框架。而是一个js的运行时 ,通俗地说就是能够一个能够运行js的平台环境。
  • 可提供服务器级别的api(需要引入相关的模块,比如文件读写需要’fs’,也就是File-System):文件读写,网络服务构建,网络通信,http服务器(http模块)……//后面会讲模块系统

特性:

  1. 事件驱动
  2. 非阻塞IO模型(异步)————————————-啊啥是同步和异步?,又要去恶补了o((⊙﹏⊙))o
  3. 轻量和高效

与Node.js相关的东东

npm:全称是Node Package Manager。世界上最大的开源生态系统(大多数的js相关的包都在npm里)可在命令行通过下列语法获取相关包

npm install [jquery]

②Node.js能干啥哇?(do what?)

  • web服务器后台(能干类似活的有Java,,PHP,Python,balabala…….)

  • 命令行工具

    • npm
    • git
    • hexo (这个我熟哈哈)

③参考资源(resource)

  • 《深入浅出Node.js》(偏理论)
  • 《Node.js权威指南》
  • JavaScript标准参考教程(alpha)作者:阮一峰(惊了,英文名也叫Frank)
  • Node入门(fen)
  • cNode社区

④安装Node(install)

会的都会,不会的戳一戳下面的教程:

参考教程:https://www.runoob.com/nodejs/nodejs-install-setup.html

⑤模块系统(Module system)

  1. 在Node中,每一个js文件都是一个模块,另外Node中也没有全局作用域的概念。
  2. 在Node中,只能通过require()方法加载js文件。
  3. require()方法只加载其中的代码。既然没有全局作用域,那么各个js文件之间是模块作用域。

    • 模块全封闭。
    • 外部和内部之间是无法互相访问的。
  4. 其实每个模块中有都一个exports专属的对象,该对象中可以封装各自模块中的数据和方法,方便其他模块应用

    • 举个例子

      var demo =require('./a')//调用模板a,并且获得a的exports对象,并且用一个变量demo接收
      

——————-分割线,下面进入简单的实操环节——————–

  1. 启动一个简单服务器

    var http = require('http')//创建http对象
    var server = http.createServer()//利用http对象创建一个服务器对象server
    server.listen(4000, function () {//选择端口
       console.log("贪玩蓝月服务器,琦动!")
    })
    server.on('request', function (request, response) {//server监听是否收到客户端发送过来的request,收到后根据回调函数再返回响应
       console.log("收到请求")
       response.writeHeader(200, {'Content-Type': 'text/html;charset=utf-8'})//<-----------解决乱码的方法
       var url = request.url
       if (url == '/') {
           response.end("欢迎来到贪玩蓝月,是兄弟就来砍我")
       }
       else if(url=='/login'){
           response.end("貪玩藍月登錄註冊界面...")
       }
       else if(url=='/register'){
           response.end("快進到貪玩藍月の註冊界面")
       }
       else{
           response.end("404 not found...")
       }
    
    })
    

    但是第一次运行的时候出现这种情况…….

    第一反应:响应头没有设置utf-8编码

不用慌,在回调函数里的第一行加上

response.writeHeader(200, {'Content-Type': 'text/html;charset=utf-8'})

运行后的截图

当然一个网站肯定不能只是傻傻地响应同一段内容,服务器必须根据浏览器(客户端)发来的请求信息来对应地响应合适的内容。

实现方法也特别简单,加入一组if-else的判断就可。(详情见上代码line10-21)

实现效果:

And:

2.模块系统

解释:在demo中引入模板a—>执行a的代码,在控制台打印starting语句—>引入模板b—>打印模板b的opening和closed—>a中的引用结束,打印a的end—>打印从a模板获取的值foo=200

从中我们发现输出的foo值是模板a中的,而不是b模板中的。这也体现了模块作用域的好处:可以加载执行多个文件,可以完全避免变量命名而产生的冲突(认真脸)


⑥相关知识补充

  1. 同步(Synchronous)

    A程序调用B程序,必须等待B程序完成才能返回到A程序,A程序继续执行……从流程上看是一条”直线“

  2. 异步(Asynchronous)

    A程序调用B程序,此时A不会等待B程序是否执行完毕,而是继续往后执行自己的代码……从流程上看是出现了一条从主线产生的“支线”

  3. 回调函数

    A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.(来自谷歌的解释)

    通俗讲就是

    ①你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。回答完毕。

    什么还不懂?行,还有个例子:②约会结束后你送你女朋友回家,离别时,你肯定会说:“到家了给我发条信息,我很担心你。” 对不,然后你女朋友回家以后还真给你发了条信息。小伙子,你有戏了。其实这就是一个回调的过程。你留了个参数函数(要求女朋友给你发条信息)给你女朋友,然后你女朋友回家,回家的动作是主函数。她必须先回到家以后,主函数执行完了,再执行传进去的函数,然后你就收到一条信息了。~(单身的看例子①就好了)

Day2

今天主要围绕模板引擎的概念以及应用方式、渲染这两点展开,并且相对应地结合两个小案例来更进一步的理解感受它们。话不多少,先介绍主要概念。

神么是模板引擎?

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。这就好比于我们的写作模板,我们想写什么类型的文章比如说明文,记叙文,抒情文,应用文。它们都有固定的一套模式,我们只要按照这套模式来,往里面添加我们自己的内容即可。

常用模板:art-template(art-template 是一个简约、超快的模板引擎。
它采用作用域预声明的技术来优化模板渲染速度,从而获得接近 JavaScript 极限的运行性能,并且同时支持 NodeJS 和浏览器。)

安装方法:

npm install art-template --save

啥是渲染?

在电脑绘图中,渲染是指在指用软件从模型生成图像的过程。而在HTML中则是指如何加工并且显示出最终的HTML页面。我们又根据加工场景位置的不同把HTML渲染分为服务端渲染和客户端渲染。

互联网早期,用户使用浏览器浏览的都是一些没有复杂逻辑的、简单的页面,这些页面都是在后端将html拼接好的然后将之返回给前端完整的html文件,浏览器拿到这个html文件之后就可以直接解析展示了,而这也就是所谓的服务器端渲染了。而随着前端页面的复杂性提高,前端就不仅仅是普通的页面展示了,而可能添加了更多功能性的组件,复杂性更大,另外,彼时ajax的兴起,使得业界就开始推崇前后端分离的开发模式,即后端不提供完整的html页面,而是提供一些api使得前端可以获取到json数据,然后前端拿到json数据之后再在前端进行html页面的拼接,然后展示在浏览器上,这就是所谓的客户端渲染了,这样前端就可以专注UI的开发,后端专注于逻辑的开发。

服务端渲染aka后端渲染(service-side render)の优缺点:

  • 优点
  1. 减轻前端压力
  2. 不用占用前端的资源,比如在用手机浏览网页时,浏览器的负担减轻,因此能够节省手机的电量
  3. 有利于SEO(Search Engine Optimization)搜索引擎优化。爬虫能够爬取到在后端的HTML文档
  • 缺点
  1. 不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,则对于前端复杂度高的项目,不利于项目高效开发。另外,如果是服务器端渲染,则前端一般就是写一个静态html文件,然后后端再修改为模板,这样是非常低效的,并且还常常需要前后端共同完成修改的动作; 或者是前端直接完成html模板,然后交由后端。另外,如果后端改了模板,前端还需要根据改动的模板再调节css,这样使得前后端联调的时间增加。
  2. 占用服务器端资源。即服务器端完成html模板的解析,如果请求较多,会对服务器造成一定的访问压力。而如果使用前端渲染,就是把这些解析的压力分摊了前端,而这里确实完全交给了一个服务器。

客户端渲染aka前端渲染(client-side render)的优缺点:

  • 艾达王替矩(advantages):

    1. 前后端分离,前端能够自定义UI而不用去过度依赖后端
  • diss艾达王替矩:

    1. 前端的响应速度会变慢,这就是为什么我们在浏览一些网页时,经常会“爱的魔力转圈圈”。(后端的渲染速度要比前端的快)
    2. 不利于SEO。目前比如百度、谷歌的爬虫对于SPA都是不认的,只是记录了一个页面,所以SEO很差。因为服务器端可能没有保存完整的html,而是前端通过js进行dom的拼接,那么爬虫无法爬取信息。 除非搜索引擎的seo可以增加对于JavaScript的爬取能力,这才能保证seo。

小案例一:模拟Apacheの目录HTML(服务端渲染)

在此之前我们还需要用到fs模块的readFile()和readdir()方法,简单介绍一下这两个方法:

  • readFile():
    能够读取指定路径下的文件并保存到data中,data是一个二进制的数据流(里面全是010101),因此要将其转变为我们认识的字符串需要用到toString()方法。

  • readdir():

    读取指定目录下的所有文件夹名,并返回一个由这些文件夹名构成的字符串数组files。

    var fs = require('fs');
    fs.readdir('readdirtest', function(err,files){
     if(err){
      console.log(err);
     }
     console.log(files);
    })
    

操作描述:

  1. 加载必要的模块fs,http…

  2. 设置端口号xxxx

  3. 为服务器设置响应信息

    • 读取模板文件template.html(目录显示页),并且将要替换的区域使用特殊符号进行标记,这里我使用了-.-这个标记
    • 读取目录列表的所有文件夹
    • 并将要替换的部分用一个变量content表示
    • 用forEach方法依次取出files里的文件夹名并构建content(见代码20—24)
    • 将第一步读取的data转换为字符串
    • 简单字符串解析替换操作,将template中替换的区域-.-(特殊标记)用content代替

    • 发送响应

下面是一小段代码:

var fs = require('fs')
var http = require('http')
var server = http.createServer()
var wwwDir = 'F:/www'

server.on('request', function (req, res) {

    fs.readFile('./template.html', function (err, data) {//获得模板template.html
        if (err) {
            console.log('找不到文件......请重试')
        }


        fs.readdir(wwwDir, function (err, files) {//得到wwwDir的目录列表中的文件名和目录名
            if (err) {
                return console.log('读取目录失败')
            }
            console.log(files)//files是一个字符数组,元素为目录中的文件名
            var content = ''//将模板中要替换的部分构建为content
            files.forEach(function (item) {//利用forEach,取出files数组的每个元素(item),进行渲染${item}替换
                content += `<tr><td data-value="新建 Microsoft Word 文档.docx"><a class="icon file" draggable="true" href="/F:/www/%E6%96%B0%E5%BB%BA%20Microsoft%20Word%20%E6%96%87%E6%A1%A3.docx">${item}</a></td>
                                <td class="detailsColumn" data-value="0">0 B</td>
                                <td class="detailsColumn" data-value="1612679895">2021/2/7 下午2:38:15</td>
                            </tr>`
            })


            data = data.toString()//将二进制流的data转换为字符串
            data=data.replace('-.-', content)//简单字符串解析替换操作,将template中替换的区域-.-(特殊标记)用content代替
            res.end(data)//发送响应数据
        })
    })
})

server.listen(4001, function () {
    console.log('服务器,琦动!')
})

结果截图:

小案例二:art-template

需要一个用于储存模板的html和一个js文件

html模板:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>This is Frank</title>
</head>

<body>
<p>My name is {{ name }}.</p>
<p>I am {{ age }} years old.</p>
<p>I am from {{ province }}.</p>
<p>I like {{ each hobbies }} {{ $value }} {{/each}}</p>
</body>

</html>

如果没有art-template,运行结果如下:

加了art-template后:

var fs=require('fs')
var http=require('http')
var template=require('art-template')
http
    .createServer(function(req,res){
        var url=req.url
        if(url==='/'){
            fs.readFile('./Frank.html',function(err,data){
                if(err){
                    return res.end('你访问的网址不存在噢')
                }
                data=data.toString()
                var Frank=template.render(data,{
                    name:'Frank',
                    age:20,
                    province:'ChongQing',
                    hobbies:['打篮球','写博客','吹水']
                })
                res.end(data)
            })
        }

    })
.listen(3000,function(){
    console.log('琦动!')

由此可见,art-template只关注中的值,不会去关心html中的格式。因此可以理解为js数据传到html中展示出来

Day3

Node中的模块系统

模块の定义:

node应用由模块组成,采用的commonjs模块规范。每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。require方法用于加载模块。


使用Node编写应用程序主要就是在使用:

  • Ecmascript
  • 核心模块
    • 文件操作fs
    • http服务的http
    • url路径操作的模块
    • path路径处理模块
    • os操作系统模块
  • 第三方模块

    • art-template
    • 必须通过npm下载
  • 开发者自个儿写的模块(自定义的)

模块化

  • 文件作用域

  • 通信规则

    • 加载require
    • 导出

      CommonJs模块规范

这就是要谈论的——模 块 系 统

  • 模块作用域
  • 使用require方法用来加载模块
  • 使用exports接口对象来导出模块中的成员

加载require

操作:

var 自定义变量名称=require('模块')

两个作用:

  • 执行被加载模块中的代码

  • 得到被加载模块中的exports导出接口对象

导出exports

  • Node中是模块作用域,默认文件中所有成员只在当前文件模块有效
  • 如果我们想要访问其他模块的成员,可以把这些成员加载到exports上去

导出多个成员:

exports.demo=foo
exports.test='233'
exports.fun=function(){
    xxxxxxxxxxxxx
}

也可以这样导出多个成员:

module.exports={
    add: function(){
    return xxxxxxx
    },
    foo:'123'
}

导出单个成员(指定的):

module.exports='666'
module.exports=function(){

}

注意:在使用module.exports时,后一个的会覆盖前一个的module.exports

原理

在底层代码中:exports是module.exports的一个引用—>exports=module.exports

console.log(exports===module.exports)//会返回true
exports.foo='666'
module.exports.foo='666'//等价

require方法加载规则

  • 核心模块

    • 模块名
  • 第三方模块

    • 模块名
  • 开发者自己写的

    • 路径
  • 优先从缓存加载

    • 加载过的模块会暂存入缓存中,若下次还有调用,直接从缓存中读取, 能有效提高模块加载效率

package.json

每一个项目最好有一个包描述文件(package.json,就像产品的说明书)。可通过npm init生成 。有了该文件,再用npm install就能把文件里的依赖项全部下载回来,就相当于原先有了备份,现在进行备份还原

  1. 在每个项目的根目录下应该有一个package.json文件
  2. 每次安装第三方包时,应该在后面加上–save`,可以用来保存依赖项信息

npm

  • [npm网站]: npmjs.com

  • 命令行工具

npm install --global npm//更新命令
  • 常用命令

    • npm init -y//快速生成package.json文件
      
    • npm install
      
    • npm install 包名//下载指定包
      
    • npm install --save 包名//下载指定包并保存到package.json的dependencies中
      
    • npm uninstall 包名
      
    • npm help//查看使用帮助
      
    • npm 命令 --help//查看指定命令的使用帮助
      
  • 如何解决npm被墙的问题

    • 安装淘宝的cnpm:

      npm install --global cnpm//--global表示安装到全局,不能省略。以后就能用cnpm代替npm了
      
    • 如果不想安装cnpm但是又想用淘宝的服务器下载,可以通过:

      npm config set registry https://registry.npm.taobao.org//默认使用淘宝服务器下载
      

Express

一种Web开发框架

npm install express --save//安装express

拿到框架的第一步当然是Hello World!

var express=require('express')
var app=express()//相当于以前的server对象

app.listen(3000,function(){
    console.log('服务器,琦动')
})

app.get('/',function(req,res){//以get方法请求'/',并执行对应的处理函数
    res.send('Hello World!')
})

静态服务(静态 资源获取)

// 开放静态资源
// 1.当以/public/开头的时候,去./public/目录中找对应资源
// 访问:http://127.0.0.1:3000/public/login.html
app.use('/public/',express.static('./public/')); 

// 2.当省略第一个参数的时候,可以通过省略/public的方式来访问
// 访问:http://127.0.0.1:3000/login.html
// app.use(express.static('./public/'));   

// 3.访问:http://127.0.0.1:3000/a/login.html
// a相当于public的别名
// app.use('/a/',express.static('./public/')); 

在Express中获取表单请求数据

获取get请求数据:

Express内置了一个api,可以直接通过req.query来获取数据

// 通过requery方法获取用户输入的数据
// req.query只能拿到get请求的数据
 var comment = req.query;
获取post请求数据:

在Express中没有内置获取表单post请求体的api,这里我们需要使用一个第三方包body-parser来获取数据。

安装:

npm install --save body-parser;

配置:

// 配置解析表单 POST 请求体插件(注意:一定要在 app.use(router) 之前 )

var express = require('express')
// 引包
var bodyParser = require('body-parser')

var app = express()

// 配置body-parser
// 只要加入这个配置,则在req请求对象上会多出来一个属性:body
// 也就是说可以直接通过req.body来获取表单post请求数据
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))

// parse application/json
app.use(bodyParser.json())

使用:

app.use(function (req, res) {
  res.setHeader('Content-Type', 'text/plain')
  res.write('you posted:\n')
  // 可以通过req.body来获取表单请求数据
  res.end(JSON.stringify(req.body, null, 2))
})

在express中配合和使用art-template

安装:

npm install --svae art-template
npm install --save express-art-template

配置:

app.engine('html',require('express-art-template'))//第一个参数表示渲染以art结尾的文件

使用:

app.get('/',function(req,res){
    //express默认会从项目的views目录去寻找index.html
    res.render('index.html',{
        title:'hello world'
    })
})

如果希望修改视图渲染的目录(原本是views),可:

app.set('views',目录路径)

CRUD(待更…)

其他项

①修改完代码自动重启

以前每次修改完代码都要傻乎乎地去node xxx.js现在有了nodemon这一个第三方命令行工具,就能解决这一问题。

安装方法,懂得都懂

npm install nodemon --global

②封装异步API

回调函数:获取异步操作的结果

function fn(callback){
    // var callback = funtion(data){ console.log(data); }
    setTimeout(function(){
        var data = 'hello';
        callback(data);
    },1000);
}
// 如果需要获取一个函数中异步操作的结果,则必须通过回调函数的方式来获取
fn(function(data){
    console.log(data);
})

③关于json文件格式の坑

json文件格式要求极其严格,比如

致命错误在:id为5的一行末尾的逗号,一定要去掉!!!结尾的一个花括号不能有逗号(就这个错误折磨了我半个小时……)

小项目

一个简单的表单提交网站

路由设计

请求方法 请求路径 get参数 post参数 备注
GET /students 渲染首页
GET /students/new 渲染添加学生页面
POST /students/new name,age,gender,hobbies 处理添加学生请求
GET /students/edit id 渲染编辑页面
POST /students/edit id,name,age,gender,hobbies 处理编辑请求
GET /students/delete id 处理删除请求

提取路由模块

router.js:

/**
* router.js路由模块
* 职责:
*      处理路由
*      根据不同的请求方法+请求路径设置具体的请求函数
* 模块职责要单一,我们划分模块的目的就是增强代码的可维护性,提升开发效率
*/
var fs = require('fs');

// Express专门提供了一种更好的方式
// 专门用来提供路由的
var express = require('express');
// 1 创建一个路由容器
var router = express.Router();
// 2 把路由都挂载到路由容器中

var Students=require('./student')

router.get('/students', function(req, res) {
    // res.send('hello world');
    // readFile的第二个参数是可选的,传入utf8就是告诉他把读取到的文件直接按照utf8编码,直接转成我们认识的字符
    // 除了这样来转换,也可以通过data.toString()来转换
        Students.find(function(err,students){
            if (err) {
                return res.status(500).send('Server error.')
            }

            res.render('index.html',{students:students})
        })

});
router.get('/students/new',function(req,res){
    res.render('new.html')
});

router.post('/students/new',function(req,res){
    //获取表单数据
    console.log(JSON.stringify(req.body))
    //处理数据
    //将数据写入json文件
    //发送响应
    //读取json文件并转换为对象-->在对象中添加数据-->将对象转换为字符串-->将字符串写入json文件
    Students.save(req.body,function (err){
        if(err){
            return res.status(500).send('Server error.')
        }
        res.redirect('/')
    })
});


router.get('/students/edit',function(req,res){

});

router.post('/students/edit',function(req,res){

});

router.get('/students/delete',function(req,res){

});

// 3 把router导出
module.exports = router;

app.js:

var router = require('./router');

// router(app);
// 把路由容器挂载到app服务中
// 挂载路由
app.use(router);

数据操作文件模块

/**
 * student.js
 * 数据操作文件模块
 * 职责:操作文件中的数据,只处理数据,不关心业务
 */
var fs = require('fs');
var dbPath = './db.json'
/**
 * 获取所有学生列表
 * return []
 */
exports.find = function (callback) {
    fs.readFile(dbPath, 'utf8', function (err, data) {
        if (err) {
            return callback(err)
        }
        callback(null, JSON.parse(data).students)
    })
}


/**
 * 获取添加保存学生
 */
exports.save = function (student, callback) {
    fs.readFile(dbPath, 'utf8', function (err, data) {
        if (err) {
            return callback(err)
        }
        //将json字符串转换为数组对象
        var students = JSON.parse(data).students
        //将对象保存到数组对象中
       if (students.length==0) {
            student.id =1
        }
        else{
            student.id=students[students.length-1].id+1
        }
        students.push(student)
        //将数组对象转换为json文件(字符串)
        var newData = JSON.stringify({students: students})
        //字符串写入到文件中
        fs.writeFile(dbPath, newData, function (err) {
            if (err) {
                return callback(err)
            }
            callback(null)
        })
    })
}

/**
 * 更新学生
 */
exports.update = function () {

}

/**
 * 删除学生
 */
exports.delete = function () {

}

成功插入数据の截图:

点击submit,成功重定向回主页并渲染完成の截图

小结:

通过这个小项目的练习,可以体会到当进行异步编程时,要想获取异步函数的数据就要使用回调函数