一个项目文件不同于测试功能用的demo,一个项目因为代码量很多,所以需要把一些代码拆分开来,下图就是这次项目的文件目录(不过这并是不最终版,但是也差不多了)
一、环境配置
先看packgae.json
{
"name": "blog-1",
"version": "1.0.0",
"description": "",
"main": "bin/www.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js",
"prd": "cross-env NODE_ENV=production nodemon ./bin/www.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cross-env": "^5.2.0",
"nodemon": "^1.19.1"
},
"dependencies": {
"mysql": "^2.17.1"
}
}
- 需要关注的几点:main的值是bin/www.js,这代表bin文件夹中www.js文件就是入口文件
- 然后再看devDependencies和dependencies
- 需要特别注意devDependencies和dependencies的区别,前者是用于开发运行的依赖库,主要用于辅助我们开发,而后者是项目中用到的依赖库,比如react中的antd这些。
- 在安装的时候如果安装的是devDependencies中的依赖输入命令
npm install 要安装的库 --save -dev - dependencies的命令则是
npm install 要安装的库 --save - devDependencies中安装了两个依赖库:cross-env和nodemon
- cross-env的作用是让你知道运行的环境是什么,是开发环境还是线上环境,因为有时候开发环境和线上环境运行的代码会有些不同,比如说开发环境用的数据库和线上的数据库肯定不能是同一个
- nodemon的作用是让你每次代码进行更改的时候可以立刻更新一次,不然每次更新代码都要重启会非常的麻烦
- 继续看scripts
- test是原本就有的不管他
- dev代表这是开发环境,那么怎么给他这个‘开发环境’的标识呢,就是之前下载的cross-env,dev后面的值是
"cross-env NODE_ENV=dev nodemon ./bin/www.js"需要识别环境的地方取出NODE_ENV的值,在这里定义了是dev,那么就知道是开发环境了,之后输入npm run dev(是不是很眼熟)就可以用刚刚下载的依赖包nodemon来运行bin文件夹下面的www.js文件了 - prd的值和dev差不多,只是修改了NODE_ENV的值,是prd,这样如果是线上跑线上环境的话就
npm run prd,那么利用cross-env就会知道是线上环境了
二、代码结构
1、www.js
现在输入npm run dev或者npm run prd都是先从www.js文件开始运行,先给上www.js的代码
const http = require('http');
const serverHandle = require('../app');
const PORT = 8000;
const server = http.createServer(serverHandle);
server.listen(PORT);
console.log("ok");
这里代码很少,只有六行代码,在这个文件配置的主要是端口号,创建HTTP服务,而服务的内容在二行const serverHandle = require('../app');他是卸载app.js文件中的
这里这样写好直接基本就不用去改了,除非需要更改一下端口号。
2、app.js
先附上app.js的代码,里面内容比较多,先不用仔细看
const querystring = require("querystring")
const handBlogRouter = require("./src/router/blog");
const handUserRouter = require("./src/router/user");
const getPostData = (req) => {
const promise = new Promise((resolve, reject) => {
if (req.method !== "POST") {
resolve({});
return
}
if (req.header["content-type"] !== "application/json") {
resolve({});
return
}
let postData = '';
req.on('data',chunk => {
postData += chunk;
})
if (!postData) {
resolve({});
return
}
resolve(
JSON.parse(postData)
)
})
return promise;
}
const serverHandle = (req, res) => {
res.setHeader('Content-type', 'application/json');
//获取path
const url = req.url;
req.path = url.split("?")[0];
//解析query
req.query = querystring.parse(url.split('?')[1]);
//解析cookie
req.cookie = {};
const cookieStr = req.headers.cookie || '';
cookieStr.split(";").forEach(item => {
if (!item) {
return
}
const arr = item.split("=");
const key = arr[0].trim();
const val = arr[1].trim();
req.cookie[key] = val;
});
getPostData(req).then(postData => {
req.body = postData
//博客路由
const blogResult = handBlogRouter(req, res);
if(blogResult) {
blogResult.then(blogData => {
res.end(
JSON.stringify(blogData)
)
})
return
}
//登录路由
const userResult = handUserRouter(req, res);
if(userResult) {
userResult.then(userData => {
res.end(
JSON.stringify(userData)
)
})
return
}
//没有路由,返回404
res.writeHead(404,{"Content-type":"text/plain"});
res.write("404 Not Found\n");
res.end();
})
}
module.exports = serverHandle
仔细看的话其实也不难里面,定义了一个serverHandle函数,把他导出出去,在www.js中结束,他接收两个参数,req和res,req用于接收前端传来的数据,res用于返回数据给前端。至于这些数据怎么来的,先往下看
3、src/conf/db.js
- 数据从哪来?当然是从数据库里来,这个文件是用来配置数据库的参数的,代码如下:
const env = process.env.NODE_ENV;
let MYSQL_CONF;
//环境为dev时候的配置(线下服务)mysql配置
if (env === 'dev') {
MYSQL_CONF = {
host: "localhost",
user: "root",
password: "7777",
port: "3306",
database: "node_blog"
}
}
//环境为production(线上服务器)mysql的配置
if (env === 'production') {
MYSQL_CONF = {
host: "localhost",
user: "root",
password: "7777",
port: "3306",
database: "node_blog"
}
}
module.exports = {
MYSQL_CONF
};
- 第一行就是用来读取运行环境的,通过process.env然后是自己定义的NODE_ENV的值来判断是dev环境还是prd环境下运行的,这里因为都是在自己电脑上运行,并不是,不是在服务器上,多云线上环境和dev环境都是一样的参数,但是还是先预留了这个功能,因为产品上线的话是肯定不可能用localhost这个本地地址的。
- 定义好MYSQL_CONF这个参数后将他导出,在src/db/mysql.js文件接收这个参数
4、src/db/mysql.js
代码如下
const mysql = require("mysql");
const { MYSQL_CONF } = require("../conf/db");
//连接数据库
const con = mysql.createConnection(MYSQL_CONF);
con.connect();
function exec(sql) {
const promise = new Promise((resolve, reject) => {
con.query(sql, (err, result) => {
if (err) {
reject(err);
return
}
resolve(result)
})
})
return promise;
}
module.exports = { exec }
连接上配置好的mysql数据,然后通过sql语句来对数据库进行操作,这文件最重要的就是exec这个函数,他接收一个sql参数,之前有需要操作数据库的地方,写好sql语句之后调用这个这个函数,然后他会以promise的方式返回执行完sql语句后返回的数据
5、src/model/resModel.js
- 先了解下这个函数存在的意义:有时候sql语句输入后得到的预期效果不是用户想要的,比如说登录失败这类错误,前端接收到这个错误之后可以以更友好的方式来提醒一些错误信息,代码如下
class BeseModel {
constructor (data, message) {
if (typeof data === "string") {
this.message = data;
data = null;
message = null;
}
if (data) {
this.data = data
}
if (message) {
this.message = message
}
}
}
class SuccessModel extends BeseModel {
constructor (data, message) {
super(data, message);
this.errno = 0;
}
}
class ErrorModel extends BeseModel {
constructor (data, message){
super (data, message);
this.errno = -1;
}
}
module.exports = {
SuccessModel,
ErrorModel
}
- 如果和预想的一样,则调用SuccessModel函数,并且返回对应的数据,errno返回0,否则调用ErrorModel函数,给出错误信息,并且errno返回1,前端利用errno来判断该给出错误提示还是成功之后的操作。
6、src/controller/xxx(user)
为什么这里打xxx了呢,因为之前的都是固定的文件,就是一个封装好的js调用这些功能,每个项目都是这样写的,而从这里开始,就要根据项目所需要的功能来了,比如说我这里取名叫user.js,那么所有与用户有关的操作都在这里:
const { exec } = require("../db/mysql");
const login = (username, passoword) => {
let sql = `select realname,username from users where username='${username}' and password='${passoword}'`;
return exec(sql).then(rows => {
return rows[0] || {}
})
}
module.exports = login
- 比如说这个文件我只写了一个login函数,是关于用户登录的,把sql语句传到exec函数中去,然后返回结果到login函数中,最后把他导出。
- 这个文件夹下放的主要用来写sql语句并且返回结果,但是不用来处理返回结果之后的操作
7、src/router/xxx(user)
同理,router文件夹下的js文件要根据项目需要的功能来,controller文件夹下每新建一个js文件都要在router下新建一个对应的js文件,代码如下:
const login = require("../controller/user")
const { SuccessModel, ErrorModel } = require("../model/resModel")
const handleUserRouter = (req, res) => {
const method = req.method;
const getCookieTime = () => {
let d = new Date();
d.setTime(d.getTime() + (24*60*60*1000));
return d.toGMTString();
}
//登录
if(method === 'GET' && req.path === '/api/user/login'){
// const { username, password } = req.body;
const { username, password } = req.query;
const result = login(username, password);
return result.then(data => {
if (data.username) {
//存入cookie
res.setHeader("Set-Cookie", `username=${data.username};path=/; httpOnly; expires=${getCookieTime()}`);
return new SuccessModel()
}
return new ErrorModel("用户名或密码错误")
})
}
//登录测试
if (method === "GET" && req.path === '/api/user/login-test') {
if (req.cookie.username) {
return Promise.resolve(new SuccessModel({
username:req.cookie.username
}))
}
return Promise.resolve(new ErrorModel('尚未登录'))
}
}
module.exports = handleUserRouter;
router文件夹看名字也知道他主要的用处是什么,他是用来定义路由的,比如这段写了两个路由,一个是登录路由'/api/user/login'和一个检测是否登录的路由/api/user/login-test请求方式都是get请求。当然登录请求应该用post请求,这里为了方便测试,通过URL来传递数据,所以先这样写。
7、app.js
现在再回过头去看看app.js中的内容
const querystring = require("querystring")
const handBlogRouter = require("./src/router/blog");
const handUserRouter = require("./src/router/user");
const getPostData = (req) => {
const promise = new Promise((resolve, reject) => {
if (req.method !== "POST") {
resolve({});
return
}
if (req.header["content-type"] !== "application/json") {
resolve({});
return
}
let postData = '';
req.on('data',chunk => {
postData += chunk;
})
if (!postData) {
resolve({});
return
}
resolve(
JSON.parse(postData)
)
})
return promise;
}
const serverHandle = (req, res) => {
res.setHeader('Content-type', 'application/json');
//获取path
const url = req.url;
req.path = url.split("?")[0];
//解析query
req.query = querystring.parse(url.split('?')[1]);
//解析cookie
req.cookie = {};
const cookieStr = req.headers.cookie || '';
cookieStr.split(";").forEach(item => {
if (!item) {
return
}
const arr = item.split("=");
const key = arr[0].trim();
const val = arr[1].trim();
req.cookie[key] = val;
});
getPostData(req).then(postData => {
req.body = postData
//博客路由
const blogResult = handBlogRouter(req, res);
if(blogResult) {
blogResult.then(blogData => {
res.end(
JSON.stringify(blogData)
)
})
return
}
//登录路由
const userResult = handUserRouter(req, res);
if(userResult) {
userResult.then(userData => {
res.end(
JSON.stringify(userData)
)
})
return
}
//没有路由,返回404
res.writeHead(404,{"Content-type":"text/plain"});
res.write("404 Not Found\n");
res.end();
})
}
module.exports = serverHandle
- router中路由参数哪里来,是从app这里接收的,这里的req是可以通用的,比如这里定义了
req.path = url.split("?")[0];那么其他地方只要接收到req参数,都可以通过req.path来拿到解析之后的路由; - getPostData用来解析post请求发送过来的比较多的json数据
res.setHeader('Content-type', 'application/json');用来定义返回的参数都都是json格式的数据
res.end(
JSON.stringify(userData)
)
用来返回数据登录后返回的数据,当然也有可能是登录失败的数据
三、总结
- 环境配置,先下载好依赖包,然后在package.json里面配置运行环境,用cross-env读出运行环境,用nodemon方便更新node代码,不然要重启才能看到效果
- 在bin/www.js文件中创建服务并配置端口号
- app.js中解析路由,解析post数据流,还有cookie的一些参数统一放到req中,方便其他函数中调用参数,最后通过res把解析后的json数据发送到给前端
- 在conf/db.js文件中配置MySQL参数,根据不同的运行环境连接不同的数据库
- 在db/mysql.js文件中连接数据,并且把sql语句传过去操作数据库最后返回操作完数据库后返回的数据库
- 在model/resModel.js文件中设置操作成功和失败的返回结果,用errno来表示是否操作成功
- 在controller文件中编写sql语句,传入到mysql.js中,最后返回数据库中的数据;每一类功能新建一个文件,比如user文件中可以写关于用户登录,管理员对用户表的增删改查sql语句。
- 在router文件下定义路由,在这里决定是返回成功还是失败的操作(调用SuccessModel还是调用ErrorModel)。controller和r文件夹下的js文件一一对应