0%

nodejs后台渲染核心知识点

nodejs后台渲染核心知识点

express

express路由

模块化

  • app.use(path, ‘exported router’)
  • 分文件嵌套使用,按功能命名划分,放置在routes

路由传参

  • 见url参数一节

  • 重定向: res.redirect('/admin/open-course')

自定义中间件

  • 自定义一个模块并导出
  • app中require引入该模块,
  • 并且用app.use注册该中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//功能:初始化全局变量, 应用级中间件,在每个路由生效
module.exports.initLocals = function(req, res, next) {
//存储数据到全局属性res.locals.courses中,res和req是贯穿整个server的各个阶段,,不需要render传递参数, 模板引擎中就可用,
//app.locals是存储在app实例上的全局属性
res.locals.courses = [{ // 数据需要从数据库中查询得到
url: '/vip-course/web', //注意路径,不能少了前面的/,
icon: 'https://www.kaikeba.com/vipcourse/web',
name: 'web全栈架构师',
desc: '深度对标百度'
},
{
url: '/vip-course/python',
icon: 'https://www.kaikeba.com/vipcourse/python',
name: 'python全栈架构师',
desc: '深度对标百度'
}
];
//存储布局页layout.hbs中的导航栏视图名称, 根据用户是否登陆,返回不同的导航视图名,布局页所有的路由都要使用,所以不用具路由render变量了
const isLogin=true; //实际应根据session或数据库获取用户是否登陆
res.locals.navName=isLogin? 'nav': 'nav-noAuth';
res.locals.obj={key:'value'};

next(); // 进入下一个中间件
};

-------------------------------------------------------------------------------------
//全局中间件优化:使用数据库数据(先判断缓存)

const {query}=require ('../models/db');
//缓存courses 存在则不查询,服务器重启才会消失
let coursesCache=null; //会改变courses的指向 所以值会变 用let

module.exports.initLocals=async function(req,res,next){
//存储布局页layout.hbs中的导航栏视图名称, 根据用户是否登陆,返回不同的导航视图名,布局页所有的路由都要使用,在全局中间件中存储变量
const isLogin=true; //实际应根据session或数据库获取用户是否登陆
res.locals.navName=isLogin? 'nav': 'nav-noAuth';
res.locals.obj={key:'value'};
if(coursesCache) {
res.locals.courses = coursesCache;
next();// 进入后续中间件
} else {
const sql = 'SELECT * FROM kkb.vip_course';
try {
const courses = await query(sql);//query必须返回Promise才能使用await
// cooperation处理一下
courses.forEach(course =>
course.cooperation = course.cooperation.split(','));
//console.log(courses);
coursesCache = res.locals.courses = courses;
next();// 进入后续中间件
} catch (err) {
next(err)
}
}
};

错误处理:

  • createError+error handler(带err参数的callback)
  • 一般放在最后,按中间件的顺序,若 执行到这,说明有错误,createError进入 error handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// createError
app.use(function(req, res, next) {
console.log('createerror')
//next() //带err的中间件 是错误处理,,createError时才会触发进入error handler
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
console.log(' error handler')
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500); //没有指定错误码时 便是500
console.log('render error')
res.render('error');
});

全局变量

  • 见自定义中间件例子,除了res.locals还有app.locals
  • 全局变量的使用: 比如 在layout.hbs使用按登录状态的nav whichPartial navName

模板引擎

设置hbs模板引擎

1
2
3
4
5
6
// 设置视图模板目录
app.set('views', path.join(__dirname, 'views'));
// 设置模板引擎
app.set('view engine', 'hbs');
// 设置模板文件的后缀名
//

render

  • 以views目录为基准 res.render('path' , options)
  • options中layout默认为’layout’,渲染布局页,布局页用 三括号 body三括号 承载其他模板

hbs语法

  • 官方文档:https://handlebarsjs.com/guide/partials.html#basic-partials

  • 插值表达式

  • 条件判断if switch

  • 循环each (数组/对象)

  • partials/helpers

    • 注册partial目录:hbs.registerPartials(path.join(__dirname, '../views/partials'));

    • 自定义helper: var helper = require('./helper');,app中引入即可,hbs是单例

    • 如代码搬家context.fn(this)/context.inverse(this)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const blocks = {}; //代码块缓存对象  全局变量,服务器开着就不会消失,, 
    hbs.registerHelper('extend', function(name, context) {
    // context 是上下文,保存有用方法和数据,最后一个参数永远是context,,hbs内部调用时传入
    let block = blocks[name]; // block用来存放代码块
    if (!block) {
    block = blocks[name] = [];
    }
    // 编译指令中代码块并放入block
    block.push(context.fn(this)); //context.fn方法将数据编译到非else的模板中 传递this,块级helper中的变量才会编译进模板内容
    // 与context.fn()配对还有一个方法
    //block.push(context.inverse(this));// //context.inverse()方法将数据编译到else的分支模板中,,块级partial中可以用else
    //总之:context.fn和inverse是取得块级模板中的模板内容,传递this则会把块级模板内的变量也编译进模板内容中,最后输出到页面(helper有返回值才行,没有返回值就不会有东西输出给页面)
    });

    hbs.registerHelper('block', function(name) {
    const val = (blocks[name] || []).join('');
    blocks[name] = []; //清空缓存
    return val;
    });
  • 行级partial

    1
    2
    {{> nav}}   // 行级partial,像这种不需要传参可用
    {{> (whichPartial navName)}} // 动态行级partial
  • 块级partial

    1
    2
    3
    4
    <!--用法1 块级partial 错误处理-->
    {{#> ooxx}}
    因为没有ooxx视图,所以出现这句话
    {{/ooxx}}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!--用法2 块级partial 传递内容: 封装窗口为例-->
    {{#> win obj }}<!--传参方式1:哈希,键=值(变量)如title='wintitle' a='a' b='b';传参方式2:对象,则win视图中可直接使用相关属性-->
    <div style="color:yellow">这是窗口的内容</div> // 块级partail里 没有命名的默认为@partial-block, 组件中通过> @partial-block引用内容
    {{/win}}

    //对应hbs
    <div class="win">
    <div class="win-title">{{key}} {{title}} {{a}} {{b}} </div>
    {{!-- 插槽@partial-block --}}
    {{> @partial-block}}
    {{> @partial-block}}
    </div>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <!--用法2 块级partial 布局组件-->
    {{#> layout}}
    {{!-- partial调用时命名多个插槽 #*inline--}}
    {{#*inline 'top'}}
    这是top
    {{/inline}}
    {{#*inline 'foo'}}
    foo
    {{/inline}}
    {{#*inline 'bar'}}
    bar
    {{/inline}}
    {{/layout}}

    对应hbs
    <div class="top">
    {{> top}} // partial内部按插槽名引用内容
    </div>
    <div class="content">
    <div class="left">
    {{> foo}}
    </div>
    <div class="right">
    {{> bar}}
    </div>
    </div>

组件化

  • 参考模板引擎partial使用
  • 主要模式: 组件提取+代码搬家,将某一功能html/css/js写成一个组件,用代码搬家,最终整合到layout模板的指定位置
  • 案例参考: views/partials/pager 分页组件的使用

url参数

完整url

  • http://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash'

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // url 中间件
    url.parse结果: 一切url参数的处理 可以参考
    {
    protocol: 'http:', // 协议
    slashes: true, //
    auth: 'user:pass', // 用户登录才能访问
    host: 'sub.example.com:8080', // 域名(包含端口)
    port: '8080', //端口
    hostname: 'sub.example.com', // 域名(不包含端口)
    hash: '#hash', // 哈希值
    search: '?query=string', // ?及?后面的部分
    query: 'query=string', // ?后面的部分
    pathname: '/p/a/t/h', //端口后面, ?之前的部分
    path: '/p/a/t/h?query=string', // 端口后面的部分,不包含哈希
    href:'http://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash' // 完整请求url
    }

参数类型

get(url占位参,查询参), post(urlencoded, json)

get传参

  • 原生: url模块 : let urlObj = url.parse(str, true)

  • express: 封装在req.query(查询参), req.params(url参数)

post传参

  • 原生: querystring模块 let obj = qs.parse(param); querystring是用来处理urlencoded形式的数据的方法,,不论get,post,, post的数据默认也是这种形式

  • express

    • express自带: app.use(express.json()) app.use(express.urlencoded({ extended: false }));
    • body-parser: app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json());
    • 其他: 如multiparty,,则支持表单普通类型和文件类型数据的提交
    • 除了文件类型,都是通过req.body获取表单字段数据,,,文件通过req.file;; 响应json数据可以通过res.send或res.json

表单(todo)

数据提交

表单默认方式

  • get
  • post
  • enctype

ajax restful提交

  • urlencoded
  • json
  • datatype/content-type

文件上传/数据校验

文件上传和数据校验是通过针对某一个路由,添加问价上传和数据校验的中间件进行实现

文件上传

express中multer模块为例

文件上传: 表单设置multi-formdata, 一般文件用file获取,表单项用name获取

  • multer: const multer = require('multer');

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    // 设置存储目录和filename
    const storage = multer.diskStorage({ //比单个dest内容更丰富
    destination: function(req, file, cb) { // 存储目录
    cb(null, 'public/images');
    },
    filename: function(req, file, cb) {
    let extname = '';
    switch (file.mimetype) {
    case 'image/jpeg':
    extname = '.jpg';
    break;
    case 'image/png':
    extname = '.png';
    break;
    case 'image/gif':
    extname = '.gif';
    break;
    }
    cb(null, Date.now() + extname);
    }
    });


    // 实例化multer
    const upload = multer({
    // dest: 'public/images',
    storage,
    limits: { fileSize: 2 * 1024 * 1024 }, //最大2M
    fileFilter: function(req, file, cb) {
    console.log(file);
    // 判断文件是否合法,合法则处理,不合法则拒绝
    if (file.mimetype === 'image/gif' ||
    file.mimetype === 'image/jpeg' ||
    file.mimetype === 'image/png') {
    // 接收文件
    cb(null, true);
    } else {
    cb(new Error('请上传图片格式'), false);
    }
    }
    });

数据校验

1
2
3
4
5
6
7
const { body, validationResult } = require('express-validator/check'); //解构得到body(req.body) 和 validationResult  ;body针对post data,还有query params对应不同传参方式的参数
const validations = [ //validations每一项都是一个中间件
body('name').not().isEmpty().withMessage('名称必填'), //链式方法
body('description').not().isEmpty().withMessage('描述信息必填'),
body('time').not().isEmpty().withMessage('时间必填')
.isAfter(new Date().toString()).withMessage('截止日期必须晚于当前时间')
];

中间件综合上传和校验

1
2
3
4
5
6
7
8
9
10
11
12
13
router.post('/open-course', [upload.single('file'), ...validations],  // 中间件callback多个的应用
async(req, res, next) => {
})


// 文件获取
upload.single('id')将单文件存放到静态资源目录,数据库存储文件名req.file.filename

// 校验结果
let errors = validationResult(req);
errors = errors.formatWith(({ msg }) => msg); // 简化校验信息
errors.array() //查看校验信息
errors.isEmpty() //

mysql

crud sql

  • 常规query

    1
    2
    3
    4
    5
    await query('select * from open_course');

    await query(sql, [offset, pageSize])

    await query('insert into open_course set ?', req.body)
  • mysql 格式化sql+query,防止注入

    1
    2
    sql = mysql.format('update  open_course set ? where id =?', [req.body, id]);
    const result=await query(sql);

普通查询封装promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//db.js
const mysql=require('mysql');
const cfg={
host:'localhost',
user:'kkb_admin',
password:'admin',
database:'kkb'
};

module.exports={
query:function(sql, value){
return new Promise(function(resolve, reject){
const conn =mysql.createConnection(cfg);
conn.connect();//可省略
conn.query(sql, value, (err, results)=>{
if(err)
reject(err); //err是promise的值
else
resolve(results);//results是promise的值
});
conn.end();//end方法会在所有查询结束后执行 比较智能 不用写在回调里
});
}
};

// 外部使用db async await
const {query}= require('../models/db');
router.get('/', async (req, res, next)=>{
const sql = 'SELECT * FROM open_course ORDER BY time DESC LIMIT ?,?';// LIMIT offset,pageSize
const results = await query(sql, [offset, pageSize]);//sql多个参数,用数组
}

连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//连接池  普通创建连接池的写法
// const pool = mysql.createPool(cfg);//创建连接池
// module.exports={
// query:function(sql, value){//不传回调,用promise的resolve和reject替代回调 返回promise, 在外部把之前要传的回调传给promise的resolve和reject
// return new Promise(function(resolve, reject){
// pool.getConnection((err, conn)=>{//获取连接
// conn.query(sql, value, (err, results)=>{
// if(err)
// reject(err); //err是promise的值
// else
// resolve(results);//results是promise的值
// });
// conn.release(); // 释放连接 不是end
// }) ;
//
// });
// }
// };


//连接池 精简版 pool去查询 等价于创建连接 连接去查询 和释放连接
const pool = mysql.createPool(cfg);//创建连接池
module.exports={
query:function(sql, value){//不传回调,用promise的resolve和reject替代回调 返回promise, 在外部把之前要传的回调传给promise的resolve和reject
return new Promise(function(resolve, reject){
//直接使用pool去查询
pool.query(sql, value,(err, results)=>{
if(err) reject(err);
else resolve(results);
})

});
}
};

sequelize

  • 安装: npm i sequelize mysql2 -S // mysql2 为mysql对应的驱动

  • orm,不用再写具体sql,语义化查询

  • 基本使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    //1 引入
    const Sequelize = require('sequelize');
    //2 实例化
    const sequelize = new Sequelize('kkb', 'kkb_admin', 'admin', {
    host: 'localhost',
    dialect: 'mysql', //方言
    pool: { max: 5, acquire: 30000, idle: 10000 }, //连接池
    timestamps: false
    });

    //3 定义模型
    const User = sequelize.define('user', { //模型名!, 字段名
    firstName: Sequelize.STRING, //更多设置使用对象
    lastName: Sequelize.STRING,
    age: Sequelize.INTEGER

    }, { //其他参数
    //freezeTableName:true //表名默认是模型名+s 冻结则不会
    });

    //4 同步数据库 force:true 强制更新 先删表
    // model.sync建表 model.create插入数据
    User.sync({ force: false }).then(() => { //返回的promise,直接then
    return User.create({ // 插入数据
    firstName: 'Tom',
    lastName: 'Cruise',
    age: 30
    })
    }).then(() => { // 查询
    //查询插入的数据 findAll 看执行的sql
    User.findAll().then(user => {
    console.log(user);
    })
    });
  • 模块化

    将每个模型单独存放一个文件,利用import整合,统一导出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    //封装
    const fs = require('fs');
    const path = require('path');
    const db = [Sequelize, sequelize]; //导出类和实例对象, Sequelize为了定义数据类型
    //模型导入 除了db.js index.js 其他文件都是单个模型
    fs.readdirSync(__dirname)
    .filter(file => (file !== 'index.js' && file !== 'db.js'))
    .forEach(file => {
    const model = sequelize.import(path.join(__dirname, file));
    db[model.name] = model; // 增加到db
    });
    module.exports = db;
    -------------------------------------------------------------------------------
    // 使用
    //sequelize 模型使用案例,,动态分页
    const {OpenCourse} = require('../models');
    router.get('/bySeq', async (req, res, next)=>{
    try{
    const page = +req.query.page || 1;// 获取当前页码,如没有则默认1
    const size = +req.query.size || 3;// 每页条数
    //返回带总条数的对象 {rows: [], count}
    const results= await OpenCourse.findAndCountAll({ // 不再需要提供sql
    offset:(page-1)*size,//偏移量
    limit:size,//每页大小
    order:[['time','DESC']]//支持多字段排序
    });
    res.render('open-course', {
    title:'公开课',
    openCourses:results.rows,//查询结果时RowDataPacket对象数组
    pagination: getPagination(results.count, page, size)
    })
    }
    catch(err){
    console.log(err);
    next(err);
    }
    })

    事务

  • 连接池+事务 Promise封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const pool = mysql.createPool(cfg);//创建连接池
module.exports={
query:function(sql, value){
return new Promise(function(resolve, reject){
pool.query(sql, value,(err, results)=>{
if(err) reject(err);
else resolve(results);
})

});
},
pool,
query2: function (conn, sql, value) { // 不使用连接池的查询,事务得明确得知道哪个连接
return new Promise((resolve, reject) => {
conn.query(sql, value, (err, results) => {
if (err) reject(err);
else resolve(results);
})
});
},
getConnection: function () {
return new Promise((resolve, reject) => {
pool.getConnection((err, conn) => { // 但获取连接 还是可以用pool获取连接, 事务的时候知道是哪个conn使用即可
if (err) reject(err);
else resolve(conn);
})
})
},
beginTransaction: function (conn) {
return new Promise((resolve, reject) => {
conn.beginTransaction(err => {
if (err) reject(err);
else resolve();
});
})
},
rollback: function (conn) {
return new Promise((resolve, reject) => {
conn.rollback(resolve);
conn.end();
});
},
commit: function (conn) {
return new Promise((resolve, reject) => {
conn.commit(err => {
if (err) reject(err);
else resolve();
});
conn.end();
});
}
};

跨域/restful

restful

  • get/post/put/delete/header/option
  • 简单请求/preflight
  • restful api : api是提供接口数据,不对页面进行渲染

跨域

  • 同源策略: 针对脚本
  • 解决方式:
    • jsonp(同源对script无效,传递callback search参数,返回callback(data)执行); ajax回调和jsonp不同,ajax是xhr根据状态监听执行回调
    • cors: Access-Control-Allow-Origin/Headers/Methods/Credentials