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做以下几种事请:
- 作为后端服务器
这个没说的,不管是官网还是书本,讲的都是这个。但是后端能人又多又便宜,干嘛要花大价钱找一个node.js高手呢?何况没几个运维懂node,你还得教会运维部署,再过分一点的还要你自己写docker(博主的惨痛经历),还是早点下班陪女朋友玩吧(狗头)。 - 作为中间件使用。
因为js处理json数据要比其它后端语言(如java)方便得多,所以有些公司会在后端与前端之间加多一个node服务来做数据处理,返回前端使用起来最舒服的数据格式,让前后端干起活来更快乐。但是多了一个中间商赚差价,成本一下就上去了,所以一般公司没有这一层。 - 作为前端构建工具----比如webpack
- 自制的效率工具
比如你拿到一份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怎么发布了,篇幅已经很长了。讲一下本地怎么跑起来。
- r-cli文件夹执行npm instll 安装依赖
- 把 r-cli文件夹放到 C:\Users\window登录用户\AppData\Roaming\npm\node_modules
- 把shell里面的脚本文件放到 C:\Users\window登录用户\AppData\Roaming\npm
- 最后随便找一个空文件夹执行 r-cli init
- 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:如果觉得本文对你有帮助,帮忙点个赞,让作者知道自己写的东西还有一点点价值。如果写的有什么不对的地方,也欢迎留言指正。