前言:很久以前就想写这么一篇文章了,因为之前都是用其他语言写后台,但是环境配置不好弄,而且占用运行内存也大,所以趁着这段时间有空,特地开发一套nodejs版的后台。不过需要注意的是nodejs终究是js的拓展,存在着一些隐患,大型项目还是建议使用C#、Java或者php等实现。
为了方便理解,先把在线项目放出来,需要的小伙伴可以先clone下来:https://gitee.com/zhkumsg/node-base-demo
1、简介
nodejs兴起已经有一段时间了,功能也是越来越强大,而且听闻百度音乐已经淘宝内部系统都以及在用nodejs了,所以为了完善自己的技术栈,还有有必要了解一下的。
那么nodejs难不难呢?真的不难,只要你有js基础和面向对象思想,那么一切问题都是纸老虎。nodejs的语法与js的基本一样,只不过普通js使用es5来写的,而nodejs用到的更多是es6甚至es7。
那nodejs能干嘛呢?怎么说呢,这样说吧,NodeJs is Everything。只有你想不到的,没有nodejs做不到的(有点夸张了哦,不过事实的确如此)。nodejs可以作为服务端语言也可以实现爬虫(但是更多的是用python实现),甚至还可以实现大数据分析和人脸识别(尽管精度有待提高)。这里我们只介绍如何用nodejs来实现后台系统的搭建。
在本系统中,我们用的语言主要是nodejs,采用的框架是十分流行express框架,所用到的数据库是微软的SQL Server 2008 r2 和mysql数据库。我们的目标是开发一个带有权限限制以及不用写sql语句的最小系统(通用后台)。
下面先预览一下权限判断以及数据库增删改查操作:
imageimageif (!Routebase.IsLogin(req, res)) { return; }
if (!Routebase.IsPermit(req, res, "00017")) { return; }
imageds.TransRunQuery(Public.OperationSQLParams(record, OperationEnum.Create))
//ds.TransRunQuery为底层sql操作、Public.OperationSQLParams为底层公共方法、record为实体类实例、OperationEnum.Create为操作枚举
imageclient.DeleteByIds(ZK_PARAMINFO, ids.split(","))
//client.DeleteByIds为底层sql操作方法、ZK_PARAMINFO为实体类、ids为记录的主键
ds.TransRunQuery(Public.OperationSQLParams(record, OperationEnum.Update))
// ds.TransRunQuery为底层sql操作、 Public.OperationSQLParams为底层公共方法、record为实体类实例、 OperationEnum.Update为操作枚举
image
client.Query(QueryModel.ZK_PARAMINFO, condition, null, Number.parseInt(pagesize, 10), Number.parseInt(pageno, 10), true, sort)
//client.Query为通用查询方法、QueryModel.ZK_PARAMINFO是查询枚举、condition是需要过滤的参数、pagesize和pageno是页数和页码、sort是排序顺序
是不是很简单,都不用拼接sql就可以完成业务,下面继续介绍这个项目的需求。
2、确定系统需求
对于每一个系统,只有确定好了需求才能够正确开发,本系统作为一个nodejs的后台最小系统,需求不会特别难,只需要下面几点:
第一:有用户模块
第二:有权限控制
第三:有列表和详情示范
第三:能快速开发(小白都会用)
第一点很好理解,也就是该系统必须有用户才行,其中包括用户注册、用户登陆、查看用户信息、修改用户信息、删除用户等功能。
对于第二点,也就是第一点的拓展,每一个用户都有一个自己的角色,然后不同角色有不用的操作权限,从而实现不用用户所能查看和维护的数据不一样。
第三点,每一个系统都不可避免有信息展示,比如文章系统,或多或少都会有列表与详情的展示。对于列表,能查询、能排序,对于详情,可以修改和查看。
第四点,也就是本系统的核心所在(底层),希望开发出来的东西小白都能用。小白只需要复制粘贴代码即可完成一个新模块功能的开发(看到我这篇文章不要关,裹上鸡蛋液,粘上面包糠,下锅炸至金黄酥脆控油捞出,老人小孩都爱看,隔壁小白都馋哭了)。当然这部分设计的东西特别多,后面会特别介绍,不急。
image那么总的来说有三个核心模块,用户模块、角色模块、权限模块
用户模块主要包括:用户主键、用户名、用户密码、用户头像和所属角色等字段;
角色模块主要包括:角色主键、角色名、角色等级、是否具备管理权和描述等字段;
权限模块包含权限列表与权限分配两个子模块:
权限列表子模块(对应的页面):页面主键、页面路径、页面组件(vue组件)、页面名称、页面图标、是否叶子节点、父级主键等字段;
权限分配子模块:分配主键、角色主键、权限主键等字段;
除了这几个核心模块,还可以添加文章系统模块、日志模块和调度模块,这里不再展开,可自由补充。
需要统一说明的是,上面的每一个模块都有几个公共的字段,那就是是否删除、创建人主键、创建时间、最后修改人、最后修改时间。其中是否删除字段是用来逻辑删除的,毕竟数据是宝贵的,避免误删除后数据无法找回,其他几个创建修改字段是用来记录操作人的。
3、初始化数据库
有了需求,我们就要创建数据库了,从上面分析我们知道有四个表,分别是:
用户表:ZK_USERINFO
角色表:ZK_ROLEINFO
权限表:ZK_PERMITINFO
配置表:ZK_PERMITCONFIG
为了后面介绍方面,这里插入两个表,分别是公共维度表(也就是本系统用到的公共参数)和投融事件表(用来做列表详情演示的),如下:
公共维度表:ZK_PARAMINFO
投融事件表:ZK_INVESTMENT
PS:ZK为前缀,没有什么特别意义哈,也可换成其他的,只是一个命名习惯。
那么是时候选择数据库了,本系统采用的主数据库是微软的sql server 2008,主要是因为一直在用,懒得换其他的,有需要也可以换成mysql,主要代码还是一样,稍微改一下连接字符串和查询底层就可以了。这里以sql server为例。
第一步:先创建数据库,命名为AuthPublish(如果不是这个命名,后面需要修改连接字符串)
第二步:依次创建上面六个表,为了方便,我直接把创建语句放出来,直接运行即可
用户表:ZK_USERINFO
image/****** Object: Table [dbo].[ZK_USERINFO] Script Date: 09/20/2018 17:47:19 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE TABLE [dbo].[ZK_USERINFO]( [ZK_ID] nvarchar NOT NULL, [ZK_PASSWORD] nvarchar NULL, [ZK_NAME] nvarchar NOT NULL, [ZK_SEX] nvarchar NULL, [ZK_PHONE] nvarchar NULL, [ZK_EMAIL] nvarchar NULL, [ZK_ROLE] nvarchar NULL, [ZK_DEPARTMENT] nvarchar NULL, [ZK_ISHANDLER] nvarchar NULL, [ZK_REMARK] nvarchar NULL, [ZK_HEAD_PORTRAIT] nvarchar NULL, [EB_ISDELETE] nvarchar NULL, [EB_CREATE_DATETIME] [datetime] NULL, [EB_CREATEBY] nvarchar NULL, [EB_LASTMODIFY_DATETIME] [datetime] NULL, [EB_LASTMODIFYBY] nvarchar NULL, [ZK_USERSOURCE] nvarchar NULL, CONSTRAINT [PK_ZK_USERINFO] PRIMARY KEY CLUSTERED ( [ZK_ID] ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]GOEXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'用户表' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'ZK_USERINFO'GO
角色表:ZK_ROLEINFO
image/****** Object: Table [dbo].[ZK_ROLEINFO] Script Date: 09/20/2018 17:47:19 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE TABLE [dbo].[ZK_ROLEINFO]( [ZK_ID] nvarchar NOT NULL, [ZK_ROLE] nvarchar NOT NULL, [ZK_DESC] nvarchar NULL, [ZK_LEAVE] [int] NOT NULL, [ZK_ISADMIN] nvarchar NOT NULL, [EB_ISDELETE] nvarchar NULL, [EB_CREATE_DATETIME] [datetime] NULL, [EB_LASTMODIFYBY] nvarchar NULL, [EB_LASTMODIFY_DATETIME] [datetime] NULL, [EB_CREATEBY] nvarchar NULL, CONSTRAINT [PK_ZK_ROLEINFO] PRIMARY KEY CLUSTERED ( [ZK_ID] ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]GO
权限表:ZK_PERMITINFO
image/****** Object: Table [dbo].[ZK_PERMITINFO] Script Date: 09/20/2018 17:47:19 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE TABLE [dbo].[ZK_PERMITINFO]( [ZK_ID] nvarchar NOT NULL, [ZK_PATH] nvarchar NULL, [ZK_COMPONENT] nvarchar NULL, [ZK_NAME] nvarchar NULL, [ZK_ISHIDDEN] nvarchar NULL, [ZK_ICON] nvarchar NULL, [ZK_ISLEAF] nvarchar NULL, [ZK_PARENT] nvarchar NULL, [ZK_SORT] [int] NULL, [EB_ISDELETE] nvarchar NULL, [EB_CREATE_DATETIME] [datetime] NULL, [EB_LASTMODIFYBY] nvarchar NULL, [EB_LASTMODIFY_DATETIME] [datetime] NULL, [EB_CREATEBY] nvarchar NULL, CONSTRAINT [PK_ZK_PERMITINFO] PRIMARY KEY CLUSTERED ( [ZK_ID] ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]GOEXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'排序,同级中越小越靠前' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'ZK_PERMITINFO', @level2type=N'COLUMN',@level2name=N'ZK_SORT'GO
配置表:ZK_PERMITCONFIG
image/****** Object: Table [dbo].[ZK_PERMITCONFIG] Script Date: 09/20/2018 17:47:19 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE TABLE [dbo].[ZK_PERMITCONFIG]( [ZK_ID] nvarchar NOT NULL, [ZK_ROLE] nvarchar NULL, [ZK_PERMITID] nvarchar NULL, CONSTRAINT [PK_ZK_PERMITCONFIG] PRIMARY KEY CLUSTERED ( [ZK_ID] ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]GO
公共维度表:ZK_PARAMINFO
image/****** Object: Table [dbo].[ZK_PARAMINFO] Script Date: 09/20/2018 17:47:19 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE TABLE [dbo].[ZK_PARAMINFO]( [ZK_ID] nvarchar NOT NULL, [ZK_KEY] nvarchar NULL, [ZK_VALUE] nvarchar NULL, [ZK_DESC] nvarchar NULL, [EB_ISDELETE] nvarchar NOT NULL, [EB_CREATE_DATETIME] [datetime] NULL, [EB_CREATEBY] nvarchar NULL, [EB_LASTMODIFY_DATETIME] [datetime] NULL, [EB_LASTMODIFYBY] nvarchar NULL, CONSTRAINT [PK_ZK_PARAMINFO] PRIMARY KEY CLUSTERED ( [ZK_ID] ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]GO
投融事件表:ZK_INVESTMENT
image/****** Object: Table [dbo].[ZK_INVESTMENT] Script Date: 09/20/2018 17:47:19 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE TABLE [dbo].[ZK_INVESTMENT]( [ZK_ID] nvarchar NOT NULL, [ZK_TITLE] nvarchar NULL, [ZK_LOGO] nvarchar NULL, [ZK_INDUSTRY] nvarchar NULL, [ZK_ROUNDS] nvarchar NULL, [ZK_MONEY] nvarchar NULL, [ZK_INVESTORS] nvarchar NULL, [ZK_TIME] nvarchar NULL, [ZK_DESC] nvarchar NULL, [ZK_ISSYNC] nvarchar NULL, [ZK_FOREIGNKEY] nvarchar NULL, [EB_ISDELETE] nvarchar NULL, [EB_CREATE_DATETIME] [datetime] NULL, [EB_CREATEBY] nvarchar NULL, [EB_LASTMODIFYBY] nvarchar NULL, [EB_LASTMODIFY_DATETIME] [datetime] NULL, CONSTRAINT [PK_ZK_INVESTMENT] PRIMARY KEY CLUSTERED ( [ZK_ID] ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]GO
PS:使用其他数据库的小伙伴对照着图片修改下数据类型即可,这里介绍的是sql server数据库。
再次声明一下,基本每一个表都会有这几个字段:是否删除、创建人主键、创建时间、最后修改人、最后修改时间。这也用在底层判断上,希望先都加上。
到这里,数据库骨架已经搭建完成,下面正式开始代码部分。
4、创建express项目
在几年的时间里,node.js逐渐发展成一个成熟的开发平台,吸引了众多开发者,我们常用它来开发一些快速移动Web框架。nodejs下有着许多的web框架,比如Primus、Geddy、Express等,这里以入门简单的express为例。
创建express web框架,可以一点一点的创建,也可以用脚手架创建,如果你用过vue-cli,你会被脚手架的方便性深深折服。所以这次我们也是站在巨人的肩膀上,使用脚手架创建express项目。
第一步:全局安装express-generator脚手架
哎,这时候就有机智的小伙伴发问了,不是应该安装express脚手架吗,怎么是安装express-generator?
其实是这样的,express在最新版本上出现了问题,难以成功安装,为了避免踩坑,统一安装express-generator,当然如果express可以正常安装也是可用的,这两个脚手架用法一样。
image image全局安装express-generator脚手架
npm install express-generator -g
PS:我试过安装express脚手架,发现是真的安装不上
第二步:创建express应用
和vue-cli脚手架一样,通过简单命令就可以创建一个项目,express也是如此。首先我们去到合适的目录,然后cmd执行以下代码(node-base-demo为项目名字,可换成其他的):
image imageexpress node-base-demo
此时可以看到目录中多了一个新文件夹node-base-demo
第三步:安装依赖
既然项目已经创建好了,那当然是去安装依赖呀(可以理解为下载插件和库),首先我们cd进项目文件夹,如下:
image闲话不多说,安装依赖使用下面的代码:
imagenpm install
执行该命令时可以使用淘宝镜像cnpm。npm install用来安装依赖(也叫做下载模块),如果不写模块名,那么npm将在当前路径寻找package.json项目文件,然后安装所配置的模块。
安装成功后会在项目目录中新建一个node_modules文件夹,如果安装出错(一般来说警告是可以理解的,错误就一定要排除),需要把node_modules文件夹全部删掉,重新npm install。
第四步:启动项目
具体启动是什么命令呢?除了可以去官网查看启动命令和查看控制台输出外,我在这里推荐一个方法,百试百灵。那就是在cmd中输入:
npm run
npm执行这句话时,会从package.json项目文件中查找全部的命令
image这时候我们就很明确知道npm run start就是启动命令了,期待了这么久,那赶紧来启动看看。
image明明已经启动了,怎么没有效果呢?
好吧,这也是我一度疑惑的问题,在vue和react脚手架下,都会自动给我们打开浏览器的呀,怎么这里不会呢?
其实这里是正常的,没毛病,项目已经正常启动了,不信你手动在浏览器输入以下网址试一下
image
哎,没毛病吧,nodejs不会帮你自动打开浏览器的。
如果很不幸,你的项目启动失败,那往往是模块没有全部安装或者端口冲突。模块的问题见我前面说的,删除node_modules文件夹,重新安装一次;如果是端口问题,接着往下看你会得到答案。
PS:停止运行是通用的命令 CTRL键 + C
5、项目结构分析
对于每一个项目,都要认真分析它的项目结构,这样才有助于我们更好的开发,我们先看一下node-base-demo项目的结构
imagebin:项目核心文件夹,内部有一个www文件,用于创建express实例(修改端口在这里);
node_modules:模块文件夹,本项目用到的全部依赖都在里面,相当于一个个的插件(自动生成,不用修改);
public:静态资源文件夹,主要存放css、html、js和图片等(基本不用);
routes:路由文件夹,放路由逻辑,这是我们核心部分;
views:视图文件夹,主要是一些视图文件(基本不用);
app.js:入口文件,相当于vue和react下的main.js,这也是我们的核心部分(和www文件类似,一个是逻辑入口,一个是物理入口,只不过www文件我们一般不用改,而app.js则是经常变动);
package.json:项目文件,存放项目信息;
为了篇幅考虑,这里先介绍bin/www、app.js和routes/index.js三个文件
imagebin/www文件主要是创建并启动http服务,一般来说我们是不用修改代码的。但是如果端口冲突无法正常启动,那就需要来稍微修改一下了。该项目的默认端口是3000,如果冲突,直接在第15行修改成其他端口就可以了(前面说端口冲突的小伙伴看到这个解决方案了吗?)。
imageapp.js可以理解成入口文件,里面主要是加载创建express实例,然后配置路由(其实还有更重要的拦截器、也可以叫过滤器,后面再说)。注意第22-23行
app.use('/', indexRouter);
app.use('/users', usersRouter);
这两句话的意思是说,如果我的路径是http:localhost:3000/,那么我使用indexRouter路由;如果我的路径是http:localhost:3000/users,那么我使用usesRouter路由。
imageroutes/index.js是路由的具体实现,这个文件最终导出一个express.router实例,这里面的代码意思是说,在当前路由下,如果路径是“/”,那么返回index视图。其中req为请求,res为响应,通过req可以拿到请求参数和请求头等信息,通过res可以执行响应。
在本系统下,基本不会用到视图,上面的res.render()一般改成res.send()。如routes/user.js
image如果想测试,直接把index.js下的响应改成res.send(************)即可,如果想新建一个api,可以添加下面的代码
router.get('/test', function(req, res, next) {
res.send('test');
});
这样浏览器打开http://localhost:3000/test就可以看到返回的信息。
哎~,机智的你又发现问题了,我明明已经改了响应内容了,为什么页面没有效果呢?
很遗憾告诉你,express-generator不支持热加载,你得重新启动才可以。
npm run start
的确有点麻烦,每次修改都得重新启动一下,不过办法还是有滴,接着往下看吧。
6、安装热加载nodemon模块
用习惯了webpack的热加载,在express-generator下每次修改都要重新启动的确有点悲催,这时候是时候祭出nodemon模块了。
nodemon模块是一个支持在express下热启动的模块,安装和使用都非常简单。
第一步:安装nodemon
imagecnpm install nodemon --save-dev
加--save-dev或者--save可是一个好习惯,这样可以把项目依赖情况写入项目文件中,共享项目的时候提供package.json就可以了。
第二步:配置nodemon
怎么在node项目中添加自己的命令脚本呢?其实很简单,需要修改package.json下的scripts就可以了(是通用的,其他脚手架下也一样),我们添加以下命令:
"dev": "nodemon ./bin/www"
添加命令为dev主要是为了规范,因为一般开发环境都是用dev命令的,也可以改成其他的。
image第三步:使用nodemon
这时候npm命令就有两个了,一个是start,一个是dev。我们在cmd下执行dev命令
imagenpm run dev
项目正常启动,这时候如果代码发生改动,该项目会自动重启,如下
image image会发现项目被自动重启,这时候刷新http://localhost:3000会看到最新信息,再也不需要手动启动项目了(手动刷新还是要的)
项目结构基本介绍完了,下面正式开始说具体业务代码。
7、开发数据模型(model)
其实开发这个系统的时候,主要是吸取了MVC思路和ORM的一些想法,我们把数据模型与数据库表一一对应起来,操作数据模型(model)就相当于操作数据库表。
按照我们前面时候说的需求分析,我们创建了几个表,分别是ZK_USERINFO、ZK_ROLEINFO、ZK_PERMITINFO、ZK_PERMITCONFIG、ZK_PARAMINFO和ZK_INVESTMENT共六张基础表。
接着我们对应这六张表,创建六个实体类(数据模型),各个实体类的属性与表字段一一对应。
首先我们在根目录创建一个model文件夹,然后依次创建ZK_USERINFO.js ~~~~ZK_INVESTMENT.js等六个文件,这里以ZK_USERINFO.js为例,其内容为
image//定义ZK_USERINFO实体类
const propertys = [
"ZK_ID", "ZK_PASSWORD", "ZK_NAME", "ZK_SEX", "ZK_PHONE", "ZK_EMAIL", "ZK_ROLE", "ZK_DEPARTMENT", "ZK_ISHANDLER", "ZK_REMARK", "ZK_HEAD_PORTRAIT", "ZK_USERSOURCE", "EB_CREATE_DATETIME", "EB_CREATEBY", "EB_LASTMODIFYBY", "EB_LASTMODIFY_DATETIME"
];
class ZK_USERINFO {
constructor() { propertys.forEach(name => { this[name] = undefined; }); this.set(arguments[0]); } get(key) { return this[key]; } set() { if (typeof arguments[0] === "object") { for (let name in arguments[0]) { if (propertys.indexOf(name) > -1) { this[name] = arguments[0][name]; } } } }
}
module.exports = ZK_USERINFO;
其中propertys为对应数据表的字段名、ZK_USERINFO为实体类名、constructor为构造函数,get/set分别为获取和设置属性的方法。
其他五个文件都是如此,这里不再展开。
PS:如果不熟悉什么是类和ES6,那就把这里的类比喻成js下的一个构造函数(实际也是如此)
class ZK_USERINFO{ } 就是 function ZK_USERINFO{ }
为了方便导出数据,我们再在model下创建一个文件model.module.js,该文件用于把全部的实体类导出
image8、开发底层模块
在这里,我认为你具备面向对象的开发思路,并且对es6、sql有一定的了解。
第一步:连接数据库
数据就是基础,没有数据什么系统都跑不起来。我们这次采用的数据库是sql server(为了方便其他小伙伴,也会顺便说一下mysql的使用)
nodejs连接sql server(也叫mssql)数据库,是用用上相应的模块的,这里用到的模块是mssql,官网地址是:https://www.npmjs.com/package/mssql,github地址是:https://github.com/tediousjs/node-mssql,里面是全英介绍(其实也不是很难看懂)。
node-base-demo项目中按照mssql模块(使用mysql的小伙伴请安装mysql模块)
imagemssql模块安装成功了,不过还是那句话,记得加--save,这是个好习惯。
那怎么才能连接上数据库呢?首先你得创建了数据库,然后配置好连接字符串,再然后connect上去,最后执行查询。
从官方文档我们连接字符串有两种写法,第一种是对象写法,第二种是字符串写法:
//第一种,对象形式
const config = {
user:"用户,一般是sa,超级管理员",
password :"密码",
server:"地址,一般是localhost、127.0.0.1或者远程服务器的ip",
database:"数据库名,在我这里就是AuthPublish",
options:{
encrypt: true //如果你是window系统,就加上这个
}
}
//第二种:字符串形式
我就特别喜欢第二种字符串形式了,因为简单多好。
为了方便,我们新建一个web.config.js文件,该文件存放本项目的一些配置信息,如数据库的连接配置和其他参数。
image其中DbConnectionString为mssql的连接字符串,各位小伙伴使用的时候记得修改账号和密码。
具体怎么连接呢?我们先看官方的文档
image image官方链接:https://github.com/tediousjs/node-mssql#promises
都说手摸手,那当然不能丢个官方文档出来了事,所以接着往下看。
第二步:封装DataAccess底层
面向对象编程的特点是什么?封装、继承和多态。我们总不能每写一个业务就敲一次数据库连接吧,这样工作量得多大啊,所以我们要把数据库访问封装起来。
首先我们在根目录新建一个文件夹common,然后在common文件夹下新建一个DataAccess.js文件,该文件将封装我们的数据库访问。
在DataAccess.js中,我们先把mssql模块和web.config.js配置文件引入,然后创建一个类叫DataAccess,并导出该类。DataAccess类中创建三个方法,第一个是constructor构造函数,第二个是GetTable获取数据表函数,第三个是RunQuery执行增删改函数,函数体先不写,此时代码如下
image紧接着我们在constructor构造函数中连接上sql server数据库(需要注意的是,这里的mssql模块只允许一个全局的连接,所以我们的连接上下文是放到全局环境上的),如下
image然后按照官方文档,使用Promise方式,查询和增删改封装如下:
image imageDataAccess.js完整代码
const sql = require('mssql');
const { DbConnectionString } = require('../web.config');
class DataAccess {
constructor(config) { try { if (!global["globalConnection"]) { if (config !== undefined) { sql.close(); } global["globalConnection"] = sql.connect(config || DbConnectionString); } this.connection = global["globalConnection"]; } catch (err) { throw err; } } /** * 执行查询 * @param {*} sqlstring */ GetTable(sqlstring) { return new Promise((resolve, reject) => { this.connection.then(pool => { return pool.request().query(sqlstring); }).then(result => { resolve(result.recordset); }).catch(err => { reject(err); }); sql.on("error", err => { reject(err); }); }); } /** * 执行增删改 * @param {*} sqlstring */ RunQuery(sqlstring) { return new Promise((resolve, reject) => { this.connection.then(pool => { let transaction = pool.transaction(); transaction.begin(err => { let request = transaction.request(); request.query(sqlstring, (err, result) => { if (err) { transaction.rollback(); resolve(false); } else { transaction.commit(); resolve(result.rowsAffected[0] > 0); } }); }); }); sql.on("error", err => { reject(err); }); }); }
}
module.exports = DataAccess;
这里使用mysql的小伙伴也别放弃,我的目标是DataAccess.js支持访问多种数据库,但是时间有限,只封装了一种,下面给出mysql的连接底层,感兴趣的可以整合到DataAccess中。
image image
MysqlHelper.js的完整代码如下
const mysql = require("mysql");
const { MySqlConnectionCfg } = require('../web.config');
class MySqlHelper {
/** * 构造器 */ constructor() { this.connection = mysql.createConnection(MySqlConnectionCfg); } /** * 执行查询 * @param {*} sqlstring */ GetTable(sqlstring) { return new Promise((resolve, reject) => { this.connection.connect(); this.connection.query(sqlstring, (err, result) => { if (err) { reject(err.message); } else { let _result = []; result.forEach(item => { let obj = {}; for (let name in item) { obj[name] = item[name]; } _result.push(obj); }); resolve(_result); } }); this.connection.end(); }); } /** * 执行增删改 * @param {*} sqlstring */ ExecuteNonQuery(sqlstring) { return new Promise((resolve, reject) => { this.connection.connect(); this.connection.query(sqlstring, (err, result) => { if (err) { reject(err.message); } else { console.log(result) resolve(result); } }); this.connection.end(); }); }
}
module.exports = MySqlHelper;
可以看到mssql和mysql的封装还是很类似的。
DataAccess底层封装了基础部分了,那么看一下如何使用
首先需要把DataAccess导入,然后实例化DataAccess,最后调用具体方法并传入参数,我们在app.js下测试一下
image image接着把测试代码修改一下,继续测试
imagevar DataAcess = require('./common/DataAccess');
var ds = new DataAcess();
ds.GetTable("SELECT NAME FROM SYSOBJECTS WHERE TYPE='U'").then(dt => {
console.log(dt);
}).catch(err => {
console.log(err);
});
实验证明,DataAccess.js封装基本没问题,那么先继续往下走(记得把测试代码删掉哦)。
第三步:封装通用查询(核心)
又有小伙伴要发言了,不是说不用写sql语句吗,你怎么还写sql呢?(这位同学,请坐下,你影响后面陈独秀同学发言了)
是这样的,一个复杂的业务不写sql还是有点困难的,但是。。。对于单表操作甚至是普通的多表操作,用我这个系统还真的可以不要写sql,那么我们接着往下。
开发这么多个项目,查询功能比增删改多很多,而且复杂度也复杂好多,所以我计划封装一个通用查询,只需要传入查询枚举、查询条件、分页设置以及排序顺序,就可以帮你实现查询。
好吧,这一块难到我了,倒不是如何实现难到我,而是我没办法一时解析清楚,因为这一部分设涉及的内容比较多,不过我相信我说的你能明白。
由浅入深吧,我先把演示如何使用,使大家有一个宏观概念,再把用到的文件先事先说明,最后顺着前面的演示一步步去实现代码。
问题1:我们平常怎么去获取用户列表的呢?
答:这不是很简单吗?一句sql语句不就搞定了?select * from ZK_USERINFO。
问题2:那要获取没有被删除的用户呢(逻辑删除)?
答:这不也很简单吗?也是一句sql的是。select * from ZK_USERINFO where EB_ISDELETE = '0'。
问题3:那我要分页获取没有被删除的用户呢?
答:这不也很简单吗?就是sql长了一点而已,(噼里啪啦噼里啪啦)。
有没有发现这些sql代码拼的有点不舒服,改一个需求我还得去读懂sql再去修改,那有什么方便的方法吗?有,下面就是简便的方法(这段代码你们现在是运行不起来的)。
imagelet condition = []; //查询条件实体类数组
let pagesize = 30; //页数
let pageno = 1; //页码
let sort = new SortParam(); //排序实体类
client.Query(QueryModel.ZK_USERINFO, condition, null, pagesize, pageno, true, sort).then(m => {
console.log(m.result); //分页查询结果
}).catch(err => {
throw err;
});
没有看到任何的sql代码吧,它的确是可以实现查询。
接着我们先把需要新建的文件及其功能列出来
1、common/TableCfg.js (数据库表与主键配置)
2、common/QueryModel.js(通用查询枚举)
3、common/MType.js (sql数据类型枚举)
4、common/MLogic.js (sql判断逻辑枚举)
5、common/MOperator.js (sql匹配枚举)
6、common/Direction.js (sql排序枚举)
7、common/SortParam.js (自定义排序参数实体类)
8、commom/MemoryCondition.js (自定义查询条件实体类)
9、common/MemoryResult.js (自定义查询结果实体类)
10、common/ServiceClient.js (暴露出去的通用查询接口)
11、common/Single.js (底层核心)
在commo文件夹下依次创建上述文件
image上述的是11个文件,前9个都是简单的配置或者枚举,下面直接把代码放出来
》common/TableCfg.js
module.exports = {
ZK_PERMITINFO: { name: "ZK_PERMITINFO", pk: "ZK_ID" }, ZK_PERMITCONFIG: { name: "ZK_PERMITCONFIG", pk: "ZK_ID" }, ZK_USERINFO: { name: "ZK_USERINFO", pk: "ZK_ID" }, ZK_ROLEINFO: { name: "ZK_ROLEINFO", pk: "ZK_ID" }, ZK_PARAMINFO: { name: "ZK_PARAMINFO", pk: "ZK_ID" }, ZK_INVESTMENT: { name: "ZK_INVESTMENT", pk: "ZK_ID" }
};
TableCfg.js下每一个元素分别对应数据库的表(映射)
image上面的图片展示的是nodejs下的ZK_USERINFO实体类对应数据库表ZK_USERINFO,该数据库表的主键是ZK_ID(如果是多主键,以英文逗号分隔)
当新增模块的时候,需要在这里更新数据库映射。
》common/QueryModel.js
module.exports = {
ZK_USERINFO: "ZK_USERINFO", ZK_PERMITINFO: "ZK_PERMITINFO", ZK_PERMITCONFIG: "ZK_PERMITCONFIG", ZK_ROLEINFO: "ZK_ROLEINFO", ZK_PARAMINFO: "ZK_PARAMINFO", ZK_INVESTMEN: "ZK_INVESTMEN"
};
js是弱类型语言,其实是不支持枚举的,然而枚举非常常用,所以这里用对象的形式来模拟枚举,通过QueryModel.ZK_USERINFO可以实现获取信息(下同)
这里的枚举是自由配置的,并不一定需要与实体类对应,只是为了方便,因为该枚举只是在ServiceClient通用查询中做switch判断。
》common/MType.js
module.exports = {
Mstring: "Mstring", Mint: "Mint", Mdatetime: "Mdatetime", Mdouble: "Mdouble", Mnull: "Mnull"
};
nodejs下基础变量类型有好几个(string、number、boolean、null、undefined),数据库的数据类型也有好多个(varchar、nvarchar、text、int、double、datetime等),总的来说就都有字符串、整型、浮点型、时间类型和空,因此这里把数据类型做成枚举,方便底层sql字符串拼接(是的,你不用写sql代码,由底层进行自动拼接)。
》common/MLogic.js
module.exports = {
And: "And", Or: "Or"
};
sql中两个条件间的关系无非就是“与”和“非”,所以这里把条件逻辑封装成枚举。
》common/MOperator.js
module.exports = {
Like: "Like", NotLike: "NotLike", Equal: "Equal", NotEqual: "NotEqual", LargeEqualThan: "LargeEqualThan", LargeThan: "LargeThan", LessEqualThan: "LessEqualThan", LessThan: "LessThan", Between: "Between", In: "In", NotIn: "NotIn", IsNull: "IsNull", LikeIn: "LikeIn", NotLikeIn: "NotLikeIn"
};
前面MLogic.js说了条件间的逻辑,那么sql条件中有哪些匹配逻辑呢?
了解sql的小伙伴就可以拍着胸脯说出来了:
模糊匹配:like
全等匹配: =
大于:>
小于:<
介于:between
范围:in
等等....
其实还有挺多这样的匹配逻辑的,为了方便,我们也是封装一个匹配逻辑枚举。具体作用是在sql底层中拼接字符串。
》common/Direction.js
module.exports = {
ASC: "ASC", DESC: "DESC"
};
当然这里也不能少排序枚举,在数据查询中经常需要根据某个字段进行正序或者倒序排序,所以我们也封装了一个sql排序枚举。ASC代表正序(从小大大);DESC代表倒序(从大到小)
篇幅有限,后续硬货详见下一篇文章
网友评论