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处理constxlsxToXlsx=(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转jsonconstxlsxToJson=(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:3000const 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(awaitgetProxy(req, res)){return;};// 作为前端服务器,返回静态文件if(responseFile(req, res)){return;}// 作为后端服务器,只接收get post
express.run(req, res);})});
3、返回静态文件
// 返回文件varresFile=(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.htmlvarresponseFile=(req, res)=>{const{ path }= Url.parse( req.url );if(!path || path ==='/'){resFile(res,'index','.html','text/html;charset=UTF-8')returntrue;}let jsIndex = path.indexOf('.js');if(jsIndex >-1){resFile(res, path.substring(0, jsIndex),'.js','application/javascript;charset=UTF-8');returntrue;}returnfalse;}// 简单返回404varresponse404=(res)=>{
res.writeHead(404,{'Content-Type':'text/plain'});
res.end('404');}
4、返回get、post请求
// 自制个express,实际上express也就比我多了亿点点细节var express ={postUrl:{},getUrl:{},run(req,res){// 作为后端服务器返回get,postconst 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;}// 没有就返回404response404(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','');},}}// 实现代理vargetProxy=(serverReq, serverRes)=>{const url = Url.parse( serverReq.url );const path = url.path;// 斜杆是index.htmlif(path ==='/'){return;}// 判断是否在代理列表中let currentRoute ='';for(let route in proxyRouter){if( path.indexOf(route)===0){
currentRoute = route;break;}}if(!currentRoute){returnfalse;}// 解析proxyRouterlet 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();returntrue;}
这里有一篇代理的介绍,感兴趣可以戳一戳前端代理浅析
6、热更新,用到了socket.io(官网推荐,必是精品)
const{ Server }=require("socket.io");const io =newServer(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)})// 代理示例,代理完访问的还是/setDataaxios({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');// 文件读写总是离不开用pathconst 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生成项目asyncfunctionmakeProjectWithProjectConfig({ name, path }){// 根据类型,名称和路径生成项目awaitmakeProject({
name,
path
});// 询问是不是要执行 npm installconst installAnswers =await inquirer.prompt([{type:'list',name:'install',message:'install project dependency packages ?',choices:[{name:'yes',value:'yes'},{name:'no',value:'no'}]}]);// cmd中输入了yesif(installAnswers.install ==='yes'){// 执行npm i awaitexeca('npm',['i','--prefix', path],{stdio:'inherit'});}else{// 返回执行命令的描述showMakeProjectResult({
name,
path,isInstall: installAnswers.install ==='yes'});return;}// 询问是不是要执行npm run devconst 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'){awaitexeca('npm',['run','dev','--prefix', path],{stdio:'inherit'});return;}else{// 返回执行命令的描述showMakeProjectResult({
name,
path,isInstall: installAnswers.install ==='yes'})}}// 返回执行命令的描述functionshowMakeProjectResult({
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!`));}// 获取文件夹名后开始创建项目asyncfunctioninitProjectInCurrentFolder(){// 获取当前文件夹的名称作为项目名称let pathArr = process.cwd().split(/\\|\//);let projectName = pathArr.pop();makeProjectWithProjectConfig({name: projectName,path: process.cwd()});}// 根据输入的参数,生成对应的项目asyncfunctionmakeProject(projectArgs){const{ path }= projectArgs;// path 为clone 代码后,项目所在的目录awaitdownloadTemplate(path);awaitresetProjectWithArgs(projectArgs);}// 更改packagejson的内容asyncfunctionresetProjectWithArgs(projectArgs){// 更改package.json的nameconst 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下载项目functiondownloadTemplate(path){const successSymbol = chalk.green('✔');const failSymbol = chalk.red('x');returnnewPromise(resolve=>{
spinner.text =`start to clone vue project template from https://gitee.com/liaofeiyu`;
spinner.start();// 这里是使用git下载,所以得先安装有gitexeca('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:如果觉得本文对你有帮助,帮忙点个赞,让作者知道自己写的东西还有一点点价值。如果写的有什么不对的地方,也欢迎留言指正。
版权归原作者 飞飞飞鱼 所有, 如有侵权,请联系我们删除。