美文网首页Web前端之路Nodejs学习笔记Nodejs
手把手教你用nodejs搭建后台最小系统(大量图文)系列一

手把手教你用nodejs搭建后台最小系统(大量图文)系列一

作者: MASON_S | 来源:发表于2019-08-10 12:40 被阅读0次

前言:很久以前就想写这么一篇文章了,因为之前都是用其他语言写后台,但是环境配置不好弄,而且占用运行内存也大,所以趁着这段时间有空,特地开发一套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语句的最小系统(通用后台)。

下面先预览一下权限判断以及数据库增删改查操作:

image

if (!Routebase.IsLogin(req, res)) { return; }

if (!Routebase.IsPermit(req, res, "00017")) { return; }

image

ds.TransRunQuery(Public.OperationSQLParams(record, OperationEnum.Create))

//ds.TransRunQuery为底层sql操作、Public.OperationSQLParams为底层公共方法、record为实体类实例、OperationEnum.Create为操作枚举

image

client.DeleteByIds(ZK_PARAMINFO, ids.split(","))

//client.DeleteByIds为底层sql操作方法、ZK_PARAMINFO为实体类、ids为记录的主键

image

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

/****** 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

image

角色表:ZK_ROLEINFO

/****** 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

image

权限表:ZK_PERMITINFO

/****** 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

image

配置表:ZK_PERMITCONFIG

/****** 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

image

公共维度表:ZK_PARAMINFO

/****** 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

image

投融事件表:ZK_INVESTMENT

/****** 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

image

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可以正常安装也是可用的,这两个脚手架用法一样。

全局安装express-generator脚手架

npm install express-generator -g

image image

PS:我试过安装express脚手架,发现是真的安装不上

第二步:创建express应用

和vue-cli脚手架一样,通过简单命令就可以创建一个项目,express也是如此。首先我们去到合适的目录,然后cmd执行以下代码(node-base-demo为项目名字,可换成其他的):

express node-base-demo

image image

此时可以看到目录中多了一个新文件夹node-base-demo

第三步:安装依赖

既然项目已经创建好了,那当然是去安装依赖呀(可以理解为下载插件和库),首先我们cd进项目文件夹,如下:

image

闲话不多说,安装依赖使用下面的代码:

npm install

image

执行该命令时可以使用淘宝镜像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脚手架下,都会自动给我们打开浏览器的呀,怎么这里不会呢?

其实这里是正常的,没毛病,项目已经正常启动了,不信你手动在浏览器输入以下网址试一下

http://localhost:3000

image

哎,没毛病吧,nodejs不会帮你自动打开浏览器的。

如果很不幸,你的项目启动失败,那往往是模块没有全部安装或者端口冲突。模块的问题见我前面说的,删除node_modules文件夹,重新安装一次;如果是端口问题,接着往下看你会得到答案。

PS:停止运行是通用的命令 CTRL键 + C

5、项目结构分析

对于每一个项目,都要认真分析它的项目结构,这样才有助于我们更好的开发,我们先看一下node-base-demo项目的结构

image

bin:项目核心文件夹,内部有一个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三个文件

image

bin/www文件主要是创建并启动http服务,一般来说我们是不用修改代码的。但是如果端口冲突无法正常启动,那就需要来稍微修改一下了。该项目的默认端口是3000,如果冲突,直接在第15行修改成其他端口就可以了(前面说端口冲突的小伙伴看到这个解决方案了吗?)。

image

app.js可以理解成入口文件,里面主要是加载创建express实例,然后配置路由(其实还有更重要的拦截器、也可以叫过滤器,后面再说)。注意第22-23行

app.use('/', indexRouter);

app.use('/users', usersRouter);

这两句话的意思是说,如果我的路径是http:localhost:3000/,那么我使用indexRouter路由;如果我的路径是http:localhost:3000/users,那么我使用usesRouter路由。

image

routes/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

image

cnpm 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命令

npm run dev

image

项目正常启动,这时候如果代码发生改动,该项目会自动重启,如下

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为例,其内容为

//定义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;

image

其中propertys为对应数据表的字段名、ZK_USERINFO为实体类名、constructor为构造函数,get/set分别为获取和设置属性的方法。

其他五个文件都是如此,这里不再展开。

PS:如果不熟悉什么是类和ES6,那就把这里的类比喻成js下的一个构造函数(实际也是如此)

class ZK_USERINFO{ } 就是 function ZK_USERINFO{ }

为了方便导出数据,我们再在model下创建一个文件model.module.js,该文件用于把全部的实体类导出

image

8、开发底层模块

在这里,我认为你具备面向对象的开发思路,并且对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模块)

image

mssql模块安装成功了,不过还是那句话,记得加--save,这是个好习惯。

那怎么才能连接上数据库呢?首先你得创建了数据库,然后配置好连接字符串,再然后connect上去,最后执行查询。

从官方文档我们连接字符串有两种写法,第一种是对象写法,第二种是字符串写法:

//第一种,对象形式

const config = {

user:"用户,一般是sa,超级管理员",

password :"密码",

server:"地址,一般是localhost、127.0.0.1或者远程服务器的ip",

database:"数据库名,在我这里就是AuthPublish",

options:{

encrypt: true //如果你是window系统,就加上这个

}

}

//第二种:字符串形式

mssql://username:password@localhost/database

我就特别喜欢第二种字符串形式了,因为简单多好。

为了方便,我们新建一个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 image

DataAccess.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

接着把测试代码修改一下,继续测试

var 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);

});

image

实验证明,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再去修改,那有什么方便的方法吗?有,下面就是简便的方法(这段代码你们现在是运行不起来的)。

let 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;

});

image

没有看到任何的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代表倒序(从大到小)


篇幅有限,后续硬货详见下一篇文章

传送门:《手摸手,教你用nodejs搭建后台最小系统(大量图文)系列二》

相关文章

网友评论

    本文标题:手把手教你用nodejs搭建后台最小系统(大量图文)系列一

    本文链接:https://www.haomeiwen.com/subject/azxrjctx.html