前言
本文主要复盘笔者的nodeJS,通过一个线上的实战案例来总结node生态常用的技术点和最佳实践。后面会花费大概一个月的时间输出3篇以实战为主的nodeJs项目,本文是第一篇,主要介绍如何使用nodeJs开发一个图床应用。该项目对于测试和个人服务型网站非常实用,大家可以基于此扩展出更强大的应用。本文的图床项目主要使用Koa进行开发,不熟悉的可以先研究一下koa官网,或者看笔者之前写的nodeJS的文章。
你将收获
- Node应用基本架构方式以及开发NodeJS应用的流程
- Koa + Koa-Router + glob + Node基本API使用
- 跨域解决方案Koa Cors的使用介绍,以及如何和前协作跨域
- 基于@koa/multer封装文件上传中间件
- 使用React开发前端应用以及xui基本使用
正文
首先图床应用要保证不同域下都可以访问我们的图片资源,不存在跨域问题,并且可以支持在不同域下的应用都可以上传图片到图床上,如下图所示:
结合上图我们可以先做应用的需求分析:
以上是一个非常简单的图床应用的需求分析,我们接下来将根据这个分析来搭建项目架构并开发我们的应用程序。在开始之前我们先看看简单的实现效果:
- 访问并上传图片
- 获取图片链接地址
- 删除图片
- 这个展示界面只是一个例子,我们可以通过前端的方式设计专属于自己的图床管理界面。这里提供的公共API在任何域名下都是可以调用的,没有跨域问题。
前台地址:基于xui搭建的图床界面前台
api开放地址:图床开放地址(免费勿黑)
1.Node应用基本架构方式以及开发NodeJS应用的流程
有关nodejs的项目架构以及如何组织nodejs目录,我在30分钟教你优雅的搭建nodejs开发环境及目录设计这篇文章中有详细的说明,大家在读完本文之后可以学习研究一下.
开发任何一个应用之前首先要做的就是了解需求,需求理清楚之后就可以做技术选型了,开发基于nodeJS的后端应用的技术方案很多,如果对nodejs很熟悉,完全可以使用原生nodejs来开发应用; 对于中小型应用我们可以直接采用Koa来开发,其中间件机制和插拔式的设计理念可以很方便的让我们开发自己的中间件;如果是涉及到比较复杂的业务线我们可以采用egg.js或者nest.js来作为nodeJS的框架选型,由于本文的图床应用比较简单,所以笔者这里直接采用koa生态来做开发. 接下来先看看我们图床应用的目录结构:
2.Koa + Koa-Router + glob + Node基本API使用
学习koa最快的方式就是直接看官方文档, koa的官方文档非常简单也非常详细,所以不懂的可以先看看官网.
1.服务端路由(接口)设计
服务端路由我们主要使用koa-router, 使用方式也很简单, 代码如下:
const Koa = require('koa'); const Router = require('@koa/router'); const app = new Koa(); const router = new Router(); // 获取列表的路由接口 router.get('/api/list', (ctx, next) => { // 获取列表的逻辑 }); // 上传图片的路由接口 router.post('/api/upload', (ctx, next) => { // 上传图片的逻辑 }); app .use(router.routes()) .use(router.allowedMethods());
因为图床的应用非常简单,我们这里就直接使用传统的方式实现, 有关nodeJS的MVC架构可以参考我之前写的node的文章.
2.使用glob来批量获取图片路径
这里批量获取图片路径我们主要使用glob来通过遍历目录来获取, 这种方式在图片数据量小的时候可以使用,但是一旦图片量指数级增长,更建议用数据库来存取,毕竟IO操作还是比较费性能的.笔者这里为了方便采用glob来实现. glob是一个基于node的第三发库,支持我们使用模式匹配的方式遍历文件目录, 具体用法如下:
import glob from 'glob' // 读取文件 router.get('/api/v0/files', ctx => { const files = glob.sync(`${staticPath}/uploads/*`) const result = files.map(item => { return `${config.staticPath}${item.split('public')[1]}` }) ctx.body = { state: 200, result } } );
这样就实现了批量获取图片的api,是不是很简单呢? 我们只需要访问这个接口,就可以拿到图床的所有图片列表了.当我们访问这个接口时,会返回如下数据:
3.跨域解决方案Koa Cors的使用介绍,以及如何和前协作跨域
由于浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一个与当前页面地址不同就被算作跨域。实现跨域的方式也很多,比如JSONP跨域,nginx反向代理,服务器端修改header,设置document.domain,使用postMessage技术等,但是目前主流的方式还是基于cors来实现.
为了让图床提供的服务给不同的域使用, 我们需要配置跨域,这里我们采用koa2-cors提供的应答式跨域解决方案,其实原理也很简单,就是配置http的请求响应头信息, 让我们的服务器支持不同的ip访问.其基本用法如下:
import cors from 'koa2-cors' // 设置跨域 app.use(cors({ origin: function (ctx) { console.log(111, ctx.url) if (ctx.url.indexOf('/api/v0') > -1) { return "*"; // 允许来自所有域名请求 } return 'http://qutanqianduan.com'; // 这样就能只允许 http://qutanqianduan.com 这个域名的请求了 }, exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'], // 获取额外的header信息 maxAge: 5, // 该字段可选,用来指定本次预检请求的有效期,单位为秒 credentials: true, allowMethods: ['GET', 'POST', 'DELETE'], // 请求允许的方法 allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'x-requested-with'] // 允许的header字段名 }))
通过以上的配置,我们就可以实现基本的跨域了.如果我们想只让某些特定的接口实现跨域,我们可以设置接口白名单, 也可以通过设置域名白名单来达到只让特定的域名访问我们的api接口.这种情况更适用于公司内部多个子系统间互相协作通信的情景.
4.基于@koa/multer封装文件上传中间件
服务器要想接受客户端上传的文件,我们还需要提供文件上传接口, 这里笔者采用koa生态比较主流的实现方式@koa/multer. 具体使用介绍官网写的也很详细,大家可以看官网学习@koa/multer.
1.实现文件上传接口
接下来我们基于它实现文件上传中间件.具体实现如下:
import multer from '@koa/multer' import { resolve } from 'path' import fs from 'fs' const rootImages = resolve(__dirname, '../../public/uploads') //上传文件存放路径、及文件命名 const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, rootImages) }, filename: function (req, file, cb) { let [name, type] = file.originalname.split('.'); cb(null, `${name}_${Date.now().toString(16)}.${type}`) } }) //文件上传限制 const limits = { fields: 10,//非文件字段的数量 fileSize: 1024 * 1024 * 2,//文件大小 单位 b files: 1//文件数量 } export const upload = multer({storage,limits})
由以上代码可知我们在destination目录下设置了文件上传的目标目录, 通过filename接口来设置上传之后的文件名. limits是对文件操作的限制,具体的可以根据自己的需求来配置.
其次结合koa-router来实现文件上传接口:
// lib/upload.js // 为了捕获multer的错误 export const uploadSingleCatchError = async (ctx, next) => { let err = await upload.single('file')(ctx, next).then(res => res) .catch(err => err); if(err) { ctx.status = 500 ctx.body = { state: 500, msg: err.message } } } // index.js // 上传文件 router.post('/api/v0/upload', uploadSingleCatchError, ctx => { let { filename, path, size } = ctx.file; let { source } = ctx.request.body || 'unknow'; let url = `${config.staticPath}${path.split('/public')[1]}` ctx.body = { state: 200, filename, url, source, size } } );
这样我们就能通过任意一个客户端上传图片到我们的图床上了.
2. 删除文件接口实现
我们用原生nodejs实现删除文件的功能, 这里会用到fs模块,具体实现如下:
// lib/upload.js // 删除文件 export const delFile = (path) => { return new Promise((resolve, reject) => { fs.unlink(path, (err) => { if(err) { reject(err) }else { resolve(null) } }) }) } // 删除文件接口 router.get('/api/v0/del', async ctx => { const { id } = ctx.query if(id) { const err = await delFile(`${staticPath}/uploads/${id}`) if(!err) { ctx.body = { state: 200, result: '删除成功' } }else { ctx.code = 500 ctx.body = { state: 500, result: '文件不存在,删除失败' } } }else { ctx.code = 500 ctx.body = { state: 500, result: 'id不能为空' } } } )
这样,我们在自己的客户端应用中点击删除按钮就可以删除图床上的文件了.当然本图传应用还有很多接口实现细节, 这里就不一一介绍了,感兴趣的朋友可以研究一下.
5.使用React开发前端应用以及xui基本使用
接下来借来实现我们的图床客户端,客户端的实现以及设计风格完全可以由自己来定,所以这里只是介绍一下笔者实现的客户端,笔者将采用react全家桶以及自己开发的第三方ui库xui——基于react的轻量级UI组件库来实现,关于如何开发一个专属于自己的组件库,可以参考笔者之前的文章. 首先我们简单开发一个图床应用的界面:
我们先引入组件库:
import React, { Component } from 'react' import { Notification, message, Layout, Icon } from '@alex_xu/xui' const { Header, Content, Footer } = Layout
接着搭建我们的页面:
class UploadPage extends Component { state = { fileList: [] } componentDidMount() { fetch(apiUrl + '/files').then(res => res.json()).then(res => { this.setState({ fileList: res.result }) }) } showAddress = (item) => { Notification.config({ placement: 'topRight', }) Notification.pop({ type: 'success', message: '图片地址', duration: 10, description: item }) } render() { return ( <div className="upload-wrap"> <Layout> <Header fixed> <div className="logo"><Icon type="FaBattleNet" style={{fontSize: '30px', marginRight: '12px'}} />XOSS</div> </Header> <Content style={{marginTop: '48px', backgroundColor: '#f0f2f5'}}> { this.state.fileList.map((item, i) => { return <div key={i} className="imgBox" onClick={this.showAddress.bind(this, item)}> <img src={item} alt=""/> <span className="del-btn" onClick={this.delFile.bind(this, item)}><Icon type="FaMinusCircle" style={{fontSize: '24px'}} /></span> </div> }) } </Content> <Footer style={{color: 'rgba(0,0,0, .5)'}}>趣谈前端 -- 徐小夕</Footer> </Layout> </div> ) } } export default UploadPage
关于http库我们可以使用任何一种主流的库比如axios, umi-request等. 本客户端代码已发布到github,大家可以clone本地运行一下: