nodejs从实战到入门

前言

很多人学习node.js,学习完还是一知半解,感觉学会了但是没有完全学会,当在项目中要用到时,还是会有一种无从下手的感觉。本文主要是通过几个实际应用的例子来给初学者讲解node.js在实际项目中的应用,用尽量简单的代码先做出些东西来提高初学者学习的信心。如果要深入学习,还是要以官网为主,也可以找一些书辅助,比如《node.js开发指南》、《了不起的Node.js》、《Node.js实战》、《深入浅出Node.js》等等,然后广泛涉猎各种项目。

本文不讲基础知识,以实战为主,对于没有学过node.js的同学可以先花半个小时或一个小时浏览一下官网入门教程(本文第三节有一点小建议),对于本文有什么错误的地方,欢迎大家指正。

本文中所有代码git地址:https://gitee.com/liaofeiyu/node-study ,复制或者戳这里

一、nodejs能干什么?

nodejs是什么?官网是这么写的: Node.js 是一个开源和跨平台的 JavaScript 运行时环境。也就是说nodejs就只是一个运行JavaScript 的环境,提供了一些内部的方法,它能够做什么,完全看使用者发挥。

我们常常会用nodejs做以下几种事请:

  1. 作为后端服务器
    这个没说的,不管是官网还是书本,讲的都是这个。但是后端能人又多又便宜,干嘛要花大价钱找一个node.js高手呢?何况没几个运维懂node,你还得教会运维部署,再过分一点的还要你自己写docker(博主的惨痛经历),还是早点下班陪女朋友玩吧(狗头)。
  2. 作为中间件使用。
    因为js处理json数据要比其它后端语言(如java)方便得多,所以有些公司会在后端与前端之间加多一个node服务来做数据处理,返回前端使用起来最舒服的数据格式,让前后端干起活来更快乐。但是多了一个中间商赚差价,成本一下就上去了,所以一般公司没有这一层。
  3. 作为前端构建工具----比如webpack
  4. 自制的效率工具
    比如你拿到一份excel,需要把它转换成json放到本地的静态文件中,你可能在网上找不到合适的工具,那么随手写一个node.js工具处理一下也是极好的(不要说python,js才是最好的语言)。

二、使用nodejs制作个人效率工具

看了那么多的文字了,先写点代码放松一下吧。

处理excel

node处理excel使用node-xlsx来解析会比较方便,使用nodemon可以不用每次更改js都重新 node xx.js
安装

npm install node-xlsx
npm install nodemon -g

例子比较简单,直接贴代码

const xlsx = require('node-xlsx')
const fs=require('fs');
let file = 'demo.xlsx'
// __dirname是当前目录,node全局变量之一
let path = `${
      
      __dirname}/input/${
      
      file}`;
// excel处理
const xlsxToXlsx = (path) => {
    
    
    //表格解析
    let sheetList = xlsx.parse(path);
    //对数据进行处理
    sheetList.forEach((sheet) => {
    
    
        sheet.data.forEach((row, index) => {
    
    
            // 第一行是标题 不处理
            if(index == 0){
    
    
                row[3] = '新的列'
                return;
            }
            // 加一列
            row[3] = row[2] + row[1]
        })
    })

    // xlsx将对象转成二进制流
    let buffer = xlsx.build(sheetList);
    // 写入
    fs.writeFile(path.replace(/input/, 'output').replace(/\./, '修改版.'), buffer, (err) => {
    
    
        if (err) {
    
     console.log(err); }
    });
}
xlsxToXlsx();

处理excel数据并转换成json

const xlsx = require('node-xlsx')
const fs=require('fs');
let file = 'demo.xlsx'
let path = `${
      
      __dirname}/input/${
      
      file}`;
// excel转json
const xlsxToJson = (path) =>{
    
    
    //表格解析
    let sheetList = xlsx.parse(path);
    // 拿到第一个sheet数据
    let sheet = sheetList[0].data
    let ret = [];
    sheet.forEach((row, index) => {
    
    
        // 第一行是标题 不处理
        if(index == 0){
    
    
            return;
        }
        // 已经知道每一列是什么了,直接按索引拿
        ret.push({
    
    
            city: row[0],
            code: row[1],
            name: row[2]
        })
    })
    // 转成字符串
    let str = JSON.stringify(ret);
    console.log(str)
    // 写入
    fs.writeFile(path.replace(/input/, 'output').replace(/\.xlsx/, '.json'), str, (err) => {
    
    
        if (err) {
    
     console.log(err); }
    })
}
xlsxToJson(path);

作为模拟服务器返回模拟数据

厚颜无耻的推荐一下本人第一篇博客:几种前端模拟数据使用方案

在这里插入图片描述

三、node官网入门教程导读

注意:仅限于第一次浏览需要关注的点,有学过基础的就跳过吧
1、运行与退出
node xx,js 运行
ctrl+c退出
代码里面用processs.exit(0)退出
2、参数相关—记住下面的代码就行

// 读取环境变量
process.env.NODE_ENV 
// 命令行输入传参
node my.js --name=joe --param2=abcdefg
// 代码中取参数,可以自己打印出来看看为什么是slice(2)
const args = require('minimist')(process.argv.slice(2))
args['name'] // joe
args['param2'] // abcdefg

3、consle

// 调试
consle.log();
// 记录错误日志----服务器开发用
console.error()
// 查看代码执行调用了哪些函数----定位错误很有用
console.trace()
// 代码执行时间----看代码性能
time()
timeEnd()

4、从命令行接收输入----vue-cli中会讲到。
5、REPL ----在命令行输入node,然后可以在命令工具中写js,一般人不这么干
6、npm安装相关,安装到哪、npm依赖的使用、npm版本----npx很有用
7、pacakge.json----需要细看的配置项
9、事件循环----看不懂也不影响你使用node,水平不够看懂了也没啥用。先学着应付面试,慢慢学,以后总会懂的。
process.nextTick()、setImmediate()同样可以先不懂
引出的小知识:在定时器和promis中,非语法类的报错并不会影响你其它代码执行,node中尽量用try catch。
10、定时器、promise、async、await----这是你js就该会的
11、事件触发----类比vue的$emit,但是它这个是全局的,算是eventBus
12、http相关----搭建服务器用,直接看实战吧
13、文件系统----api文档超长,直接实战中翻文档
14、操作系统模块----作者用得不多,基本上使用的插件都做了封装
举个最简单的小栗子:node判断是mac还是window,区分路径的斜杠跟反斜杠。
15、buffer跟流----让文件传输更快,提升文件处理性能用的
16、typescript----官网的示例都是js,不看它,学会了typescipt再来看
17、WebAssembly----几乎用不上,不看它

四、使用nodejs实现vue的devServer

本节涉及到http、文件系统、热更新(热更新就是改了js代码,浏览器会自动刷新)实现、express简单实现(真的很简单)、vue代理的原理(vue用的是http-proxy)
代码的目录结构
在这里插入图片描述

话不多说,上代码,代码写了别人看不懂,废话再多也没用
ps:为了同一个模块集中放一块,代码用了var,按顺序一段段复制成一个文件就能跑起来
1、启动服务器(这里用http是为了对应官网学习,用express会比较舒服,用koa也可以,但是得自己加依赖)

const fs=require('fs');
const http = require('http')
const Url = require('url')
const port = 3000;
const basePath = __dirname;
// 这里返回指的是访问localhost:3000
const server = http.createServer();
// 启动监听
server.listen(port, () => {
    
    
  console.log("启动成功")
})

2、监听请求

server.on('request', (req, res) => {
    
    
    // http接数据很麻烦,还是用express封装好的爽
    let data = '';
    req.on('data', chunk => {
    
    
        data += chunk;
    })
    // 为了代码简单,直接接收完数据在处理
    req.on('end', async () => {
    
    
        // 把数据挂在body上
        req.body = data;
        // vue的代理实现
        if(await getProxy(req, res)){
    
    
            return;
        };
        // 作为前端服务器,返回静态文件
        if(responseFile(req, res)){
    
    
            return;
        }
        // 作为后端服务器,只接收get post
        express.run(req, res);
    })
});

3、返回静态文件

// 返回文件
var resFile = (res, path, type, contentType) => {
    
    
    let realPath = `${
      
      basePath}/html/${
      
      path}${
      
      type}`
    fs.readFile(realPath, (err, data) => {
    
    
        if (err) {
    
    
            response404(res);
        }
        res.writeHead(200, {
    
    'Content-Type': contentType})
        res.end(data);
    });
}
// demo只支持返回js 跟 index.html
var responseFile = (req, res)=>{
    
    
    const {
    
     path }  = Url.parse( req.url );
    if( !path || path === '/' ){
    
    
        resFile(res, 'index', '.html', 'text/html;charset=UTF-8')
        return true;
    }
    let jsIndex = path.indexOf('.js');
    if(jsIndex > -1){
    
    
        resFile(res, path.substring(0, jsIndex), '.js', 'application/javascript;charset=UTF-8');
        return true;
    }
    return false;
}
// 简单返回404
var response404 = (res) =>{
    
    
    res.writeHead(404, {
    
    'Content-Type': 'text/plain'});
    res.end('404');
}

4、返回get、post请求

// 自制个express,实际上express也就比我多了亿点点细节
var express = {
    
    
    postUrl: {
    
    },
    getUrl: {
    
    },
    run(req,res){
    
    
        // 作为后端服务器返回get,post
        const method = req.method.toLowerCase();
        const {
    
     path }  = Url.parse( req.url );
        // 加多个json返回方法,简化版express.json()
        res.json = (data)=>{
    
    
            res.setHeader('Content-Type', 'text/html;charset=UTF-8');
            res.end(JSON.stringify(data))
        }
        // 判断是get还是post,路径在不在列表里,在列表里就调回调函数
        if(method === 'get' && this.getUrl[path]){
    
    
            this.getUrl[path](req, res);
            return;
        }
        if(method === 'post' && this.postUrl[path]){
    
    
            this.postUrl[path](req, res);
            return;
        }
        // 没有就返回404
        response404(res);
    },
    // get post都只是把callback存起来,
    post(url, callBack){
    
    
        this.postUrl[url] = callBack;
    },
    get(url, callBack){
    
    
        this.getUrl[url] = callBack;
    }
}
// 定义两个接口
express.post('/setData', (req, res)=>{
    
    
    res.json({
    
    code:200, msg:'success'})
})
express.get('/getData', (req, res)=>{
    
    
    res.json({
    
    code:200,data:{
    
    count:1},msg:'success'})
})

5、简单的代理实现

// 假如是vue devServer传进来的
var proxyRouter = {
    
    
    '/api': {
    
    
        target: 'http://localhost:3000',
        pathRewrite: path => {
    
    
            return path.replace('/api', '');
        },
    }
}
// 实现代理
var getProxy = (serverReq, serverRes)=>{
    
    
    const url  = Url.parse( serverReq.url );
    const path = url.path;
    // 斜杆是index.html
    if(path === '/'){
    
    
        return;
    }
    // 判断是否在代理列表中
    let currentRoute = '';
    for(let route in proxyRouter){
    
    
        if( path.indexOf(route) === 0 ){
    
    
            currentRoute = route;
            break;
        }
    }
    if(!currentRoute){
    
    
        return false;
    }
    // 解析proxyRouter
    let target = Url.parse( proxyRouter[currentRoute].target );
    // pathRewrite的作用
    let targetPath = proxyRouter[currentRoute].pathRewrite(url.path)
    // 真正的请求地址及配置
    const options = {
    
    
        hostname: target.hostname,
        port: target.port,
        path: targetPath,
        method: serverReq.method,
    };
    // 创建请求到真正的地址
    var request = http.request(options, (res) => {
    
    
        res.setEncoding('utf8');
        let data = '';
        res.on('data', (chunk) => {
    
    
            data += chunk;
        });
     	// 请求完把接收到的数据返回到前端
        res.on('end', () => {
    
    
            serverRes.setHeader('Content-Type', 'text/html;charset=UTF-8');
            serverRes.end(data);
        });
    });
    // 请求数据发送到真正的地址
    request.write(serverReq.body);
    request.end();
    return true;
}

这里有一篇代理的介绍,感兴趣可以戳一戳前端代理浅析

6、热更新,用到了socket.io(官网推荐,必是精品)

const {
    
     Server } = require("socket.io");
const io = new Server(server);
io.on('connection', (socket) => {
    
    
    console.log('a user connected');
});

// 简单监听文件夹,文件夹
fs.watch('./html', {
    
     encoding: 'utf-8' }, (eventType, filename) => {
    
    
    console.log(filename);
    io.emit('message',123)
});

7、前端index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">
    <style>
        #app {
    
     text-align: center; color: #fff; line-height: 400px; width: 400px; height: 400px; border-radius: 200px; background: greenyellow; }
    </style>
</head>
<body>
	<div id="app">生活必须带点绿</div>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src=./demo.js></script>
    <script src=./404.js></script>
    <script type="importmap">
        {
    
     "imports": {
    
     "socket.io-client": "https://cdn.socket.io/4.4.1/socket.io.esm.min.js" } }
    </script>
    <script type="module">
        import {
    
     io } from "socket.io-client";
        const socket = io('ws://localhost:3000');
        socket.on('message',function(evt){
    
    
            location.reload();
        })
    </script>
    <script>
        console.log('onload')
    </script>
</body>
</html>

8、前端demo.js

// get示例
axios.get('http://localhost:3000/getData').then(function(data){
    
    
    console.log(data)
})
// post示例
axios.post('http://localhost:3000/setData').then(function(data){
    
    
    console.log(data)
})
// 代理示例,代理完访问的还是/setData
axios({
    
    url:'http://localhost:3000/api/setData',method:'post',data: {
    
    value: 1}}).then(function(data){
    
    
    console.log(data)
})

五、使用nodejs简单的实现一个vue-cli

vue-cli的原理就是给你下载了一个工程到本地,然后添加了很多指令跟参数让你可以定制一个你需要的脚手架,直接上代码做一个简易版。运行需要先下载代码,戳这里
在这里插入图片描述

这里就不讲cli怎么发布了,篇幅已经很长了。讲一下本地怎么跑起来。

  1. r-cli文件夹执行npm instll 安装依赖
  2. 把 r-cli文件夹放到 C:\Users\window登录用户\AppData\Roaming\npm\node_modules
  3. 把shell里面的脚本文件放到 C:\Users\window登录用户\AppData\Roaming\npm
  4. 最后随便找一个空文件夹执行 r-cli init
  5. mac比较麻烦,暂时不支持

ps:基本上都是用的node插件,代码是作者原本项目中实现的cli,简化后重新改了注释,简单看看代码原理吧,毕竟你去看vuecli源码连注释都没几个。

#!/usr/bin/env node
// r是作者名字首字母,没别的意思
process.title = 'r-cli';
// 指令模块,可以生成一个可以在cmd执行的指令
const program = require('commander');
// node总是离不开文件读写
const fs = require('fs');
// 文件读写总是离不开用path
const path = require('path');
// node跟命令窗口交互的插件
const inquirer = require('inquirer');
// 插件作用:在代码中实现在cmd中执行命令的效果
const execa = require('execa');
// chalk是一个node在命令窗口按颜色输出文字的插件
const chalk = require('chalk');
// node在命令窗口显示loading的工具
const ora = require('ora');
const spinner = ora();

// 在cmd中执行 r-cli -v 查看版本
program.version(require('../package').version, '-v, --version')
    .usage('<command> [options]')
// r-cli init  直接开始创建
program
    .command('init')
    .description('init a project in current folder')
    .action(function() {
    
    
        initProjectInCurrentFolder();
    });
// 读取命令参数到program中
program.parse(process.argv);
// 根据项目名name,项目路径path生成项目
async function makeProjectWithProjectConfig({
     
      name, path }) {
    
    
    // 根据类型,名称和路径生成项目
    await makeProject({
    
    
        name,
        path
    });
    // 询问是不是要执行 npm install
    const installAnswers = await inquirer.prompt([{
    
    
        type: 'list',
        name: 'install',
        message: 'install project dependency packages ?',
        choices: [{
    
    
            name: 'yes',
            value: 'yes'
         }, {
    
    
            name: 'no',
            value: 'no'
         }]
    }]);
    // cmd中输入了yes
    if (installAnswers.install === 'yes') {
    
    
        // 执行npm i 
        await execa('npm', ['i', '--prefix', path],{
    
    
            stdio: 'inherit'
        });
    }
    else {
    
    
    	// 返回执行命令的描述
        showMakeProjectResult({
    
    
            name,
            path,
            isInstall: installAnswers.install === 'yes'
        });
        return ;
    }
    // 询问是不是要执行npm run dev
    const startAnswers = await inquirer.prompt([{
    
    
        type: 'list',
        name: 'start',
        message: 'start project now?',
        choices: [{
    
    
            name: 'yes',
            value: 'yes'
         }, {
    
    
            name: 'no',
            value: 'no'
         }]
    }]);

    if (startAnswers.start === 'yes') {
    
    
        await execa('npm', ['run', 'dev', '--prefix', path],{
    
    
            stdio: 'inherit'
        });
        return ;
    }
    else {
    
    
    	// 返回执行命令的描述
        showMakeProjectResult({
    
    
            name,
            path,
            isInstall: installAnswers.install === 'yes'
        })
    }
    
}
// 返回执行命令的描述
function showMakeProjectResult({
     
     
    name,
    path,
    isInstall
}) {
    
    
    console.log(chalk.green(`
        Success! Created ${
      
      name} at ${
      
      path}.
        We suggest that you begin by typing:
        cd ${
      
      path}
        ${
      
      isInstall ? '': 'npm install'}
        npm run dev
        Happy developing!`
    ));
}
// 获取文件夹名后开始创建项目
async function initProjectInCurrentFolder() {
    
    
    // 获取当前文件夹的名称作为项目名称
    let pathArr = process.cwd().split(/\\|\//);
    let projectName = pathArr.pop();
    makeProjectWithProjectConfig({
    
    
        name: projectName,
        path: process.cwd()
    });
}
// 根据输入的参数,生成对应的项目
async function makeProject(projectArgs) {
    
    
    const {
    
     path } = projectArgs;
    // path 为clone 代码后,项目所在的目录
    await downloadTemplate(path);
    await resetProjectWithArgs(projectArgs);
}
// 更改packagejson的内容
async function resetProjectWithArgs(projectArgs) {
    
    
    // 更改package.json的name
    const packageJsonFilePath = path.resolve(projectArgs.path, 'package.json');
    const packageJson = JSON.parse(fs.readFileSync(packageJsonFilePath));
    packageJson.name = projectArgs.name;
    fs.writeFileSync(packageJsonFilePath, JSON.stringify(packageJson, null, '    '));
}
// 执行git clone下载项目
function downloadTemplate(path) {
    
    
    const successSymbol = chalk.green('✔');
    const failSymbol = chalk.red('x');
    return new Promise( resolve => {
    
    
        spinner.text = `start to clone vue project template from https://gitee.com/liaofeiyu`;
        spinner.start();
        // 这里是使用git下载,所以得先安装有git
        execa('git', ['clone', 'https://gitee.com/liaofeiyu/node-study.git', path]).then(async () => {
    
    
            spinner.stopAndPersist({
    
    
                symbol: successSymbol,
                text: `clone vue project template successfully`
            });
            resolve();
        }).catch( error => {
    
    
            if (error.failed) {
    
    
                spinner.stopAndPersist({
    
    
                    symbol: failSymbol,
                    text: chalk.red(error.stderr)
                });
            }
        });
    });
}

总结

通过几个实战例子,你可能已经发现了。除了文件系统(fs、path)外,我们很少会直接用node.js提供的基础功能,就像我们很少会直接用原生js来开发项目。比如http用express,websocket用socket.io,与命令行交互用inquirer,想要console好看点用了chalk,与命令窗口指令交互用commander等等。
生态是无穷无尽的,总有一些工具被创造出来,懂得越多就会越发觉得自己知识匮乏。
然而,总有一些不变的东西,那就是js,不管用了什么插件,代码逻辑的编写都是用的js基础。学好js,发挥自己的主动性与创造力,你就是下一个vue作者、react作者。

ps:作者是尽量把代码写简单了,实际项目中需要加上很多细节,比如使用ts或者做一些类型判断,用try catch来打印错误日志等。写的过程中经常越写越嗨越写越多,最后删删改改还是写了这么多,就酱吧。

pps:如果觉得本文对你有帮助,帮忙点个赞,让作者知道自己写的东西还有一点点价值。如果写的有什么不对的地方,也欢迎留言指正。

猜你喜欢

转载自blog.csdn.net/qq_38217940/article/details/123736492