综合案例

    核心知识点

    • 服务端开发
      • 服务端
      • Express
      • 数据库
      • HTTP
    • Ajax
    • 前后端交互开发

    学习目标

    • 能掌握使用 express 开放静态资源
    • 能掌握模板引擎中提取母版页和模板继承的使用

    • 能理解路由模块的提取

    • 能理解数据库操作模块的封装
    • 能完成分类列表异步加载的前后端实现
    • 能完成删除分类功能
    • 能完成添加分类功能
    • 能完成编辑分类功能
    • 能完成用户列表功能
    • 能完成添加用户功能
    • 能完成删除用户功能
    • 能完成编辑用户功能
    • 能完成用户登录功能
    • 能完成用户退出功能
    • 能完成添加文章功能
    • 能完成文章列表功能
    • 能完成删除文章功能
    • 能完成编辑文章功能
    • 能完成添加广告图功能
    • 能完成广告图列表功能
    • 能完成删除广告图功能
    • 能完成编辑广告图功能
    • 能完成网站设置功能
    • 能完成个人中心功能
    • 能完成修改密码功能
    • 能根据文档使用 jquery-validation 验证插件
    • 能根据文档使用富文本编辑器插件
    • 能根据文档使用 ajv 验证插件
    • 能理解分页接口的实现
    • 能根据文档使用客户端分页插件
    • 能够理解 MVC 模式在项目中的意义
    • 分类管理
    • 用户管理
      • 能够使用Ajax方式添加管理员
      • 能够使用Ajax方式展示管理员列表
      • 能够使用Ajax方式完成编辑管理员
      • 能够使用Ajax方式完成删除管理员
    • 用户登录
      • 能够使用传统方式完成用户登录
      • 能够使用Ajax方式完成用户登录
    • 文章管理
      • 能够使用Ajax方式完成发布新文章
      • 能够通过查看文档掌握富文本编辑器的使用
      • 能够理解分页技术的交互过程
      • 能够通过查看文档使用客户端分页插件
      • 能够使用Ajax方式完成展示文章列表
      • 能够使用Ajax方式完成编辑文章
      • 能够使用Ajax方式完成删除文章
    • 评论管理
      • 能够使用Ajax+分页方式展示评论列表
      • 能够使用Ajax方式删除评论
      • 能够使用Ajax方式操作评论的通过状态
    • 网站设置
      • 能够掌握传统方式的表单文件提交前后端处理流程
      • 能够掌握Ajax异步表单文件提交前后端处理流程
      • 能够使用Ajax方式完成网站基本信息设置
    • 图片轮播管理
      • 能够使用Ajax方式完成添加轮播项
      • 能够使用Ajax方式展示轮播列表
      • 能够使用Ajax方式编辑轮播项
      • 能够使用Ajax方式删除轮播项
    • 菜单管理
      • 能够使用Ajax方式完成添加导航菜单项
      • 能够使用Ajax方式展示导航菜单列表
      • 能够使用Ajax方式编辑导航菜单项
      • 能够使用Ajax方式删除导航菜单项
    • 客户端前台
      • 能够使用Ajax方式加载轮播图列表
      • 能够使用Ajax+分页方式加载内容列表
      • 能够使用动态路由导航方式加载内容详情
      • 能够使用Ajax方式完成发布评论
      • 能够使用Ajax+异步分页(加载更多)方式完成评论列表展示

    起步

    初始化目录结构

    使用 Express 创建 Web 服务

    1. 安装 Express
    1. app.js 中写入以下内容
    1. const express = require('express')
    2. const app = express()
    3. app.get('/', (req, res) => res.send('Hello World!'))
    4. app.listen(3000, () => console.log('Serve listening http://127.0.0.1:3000/'))
    1. 使用 启动开发模式
    1. nodemon app.js
    1. 在浏览器中访问 http://127.0.0.1:3000/

    导入并开放静态资源

    1. 将模板中的 html 静态文件放到项目的 views 目录中
    2. 将模板中的静态资源放到 public 目录中

    3. 在 Web 服务中把 public 目录开放出来

    1. ...
    2. const path = require('path')
    3. app.use('/public', express.static(path.join(__dirname, './public')))
    4. ...
    1. 测试访问 public 中的资源

    使用模板引擎渲染页面

    在 Node 中,不仅仅有 art-template 这个,还有很多别的。

    • ejs
    • pug
    • handlebars
    • nunjucks

    参考文档:

    1. 安装
    1. npm i art-template express-art-template
    1. 配置
    1. ...
    2. // res.render() 的时候默认去 views 中查找模板文件
    3. // 如果想要修改,可以使用下面的方式
    4. app.set('views', '模板文件存储路径')
    5. // express-art-template 内部依赖了 art-template
    6. app.engine('html', require('express-art-template'));
    7. app.set('view options', {
    8. debug: process.env.NODE_ENV !== 'production'
    9. })
    10. ...
    1. 使用
    1. app.get('/', (req, res, next) => {
    2. res.render('index.html')
    3. })
    1. 修改页面中的静态资源引用路径让页面中的资源正常加载

    提取路由模块

    • 简单应用提取一个路由文件模块

    • 将来路由越来越多,所以按照不同的业务分门别类的创建了多个路由文件模块放到了 routes 目录中,好管理和维护。

    提取路由模块操作步骤:

    1. 创建路由文件

    2. 写入以下基本内容

    1. const express = require('express')
    2. const router = express.Router()
    3. // 自定义路由内容
    4. // router.get
    5. // router.get
    6. // router.post
    7. // ...
    8. module.exports = router
    1. app.js 中挂载路由模块
    1. ...
    2. // 加载路由模块
    3. const 路由模块 = require('路由模块路径')
    4. ...
    5. // 挂载路由模块到 app 上
    6. app.use(路由模块)
    7. ...
    1. 打开浏览器访问路由路径进行测试。

    提取模板页

    走通页面路由导航

    路由表:

    导入数据库

    • 新建一个数据库命名为 alishow
    • alishow 数据库中执行下发的数据库文件 ali.sql

    封装数据库操作模块

    参考文档:

    1. 安装
    1. npm i mysql
    1. 基本使用
    1. var mysql = require('mysql');
    2. var connection = mysql.createConnection({
    3. host : 'localhost',
    4. user : 'me',
    5. password : 'secret',
    6. database : 'my_db'
    7. });
    8. connection.connect();
    9. connection.query('SELECT 1 + 1 AS solution', function (error, results, fields) {
    10. if (error) throw error;
    11. console.log('The solution is: ', results[0].solution);
    12. });
    13. connection.end();
    1. 上面的方式是创建了单个连接,不靠谱,一旦这个连接挂掉,就无法操作数据库。我们推荐使用连接池的方式来操作数据库,所以将单个连接的方式改为如下连接池的方式。
    1. var mysql = require('mysql');
    2. var pool = mysql.createPool({
    3. connectionLimit : 10,
    4. host : 'example.org',
    5. user : 'bob',
    6. password : 'secret',
    7. database : 'my_db'
    8. });
    9. pool.query('SELECT 1 + 1 AS solution', function (error, results, fields) {
    10. if (error) throw error;
    11. console.log('The solution is: ', results[0].solution);
    12. });
    1. 我们在项目的很多地方都要操作数据库,所以为了方便,我们将数据库操作封装为了一个单独的工具模块放到了 utils/db.js 中,哪里使用就在哪里加载。
    1. // 创建一个连接池
    2. // 连接池中创建了多个连接
    3. const pool = mysql.createPool({
    4. connectionLimit: 10, // 连接池的限制大小
    5. host: 'localhost',
    6. user: 'root',
    7. password: '123456',
    8. database: 'alishow63'
    9. })
    10. // 把连接池导出
    11. // 谁要操作数据库,谁就加载 db.js 模块,拿到 poll,点儿出 query 方法操作
    12. module.exports = pool
    1. 例如在 xxx 模块中需要操作数据库,则可以直接
    1. const db = require('db模块路径')
    2. // 执行数据库操作
    3. db.query()...

    分类管理

    分类列表

    一、页面加载,发起 Ajax 请求,获取分类列表数据,等待响应

    二、服务端收到请求,提供请求方法为 GET, 请求路径为 /api/categories 的路由,响应分类列表数据

    1. // 1. 添加接口路由
    2. router.get('/api/categories/list', (req, res, next) => {
    3. // 2. 操作数据库获取数据
    4. db.query('SELECT * FROM `ali_cate`', function (err, data) {
    5. if (err) {
    6. throw err
    7. }
    8. // 3. 把数据响应给客户端
    9. res.send({
    10. success: true,
    11. data
    12. })
    13. })
    14. })

    三、客户端正确的收到服务端响应的数据了,使用数据结合模板引擎渲染页面内容

    1. 配置客户端模板引擎

      1. 下载
      2. 引用
    2. 准备模板字符串

    1. <script type="text/html" id="list_template">
    2. {%each listData%}
    3. <tr>
    4. <td class="text-center"><input type="checkbox"></td>
    5. <td>{% $value.cate_name %}</td>
    6. <td>{% $value.cate_slug %}</td>
    7. <td class="text-center">
    8. <a href="javascript:;" class="btn btn-info btn-xs">编辑</a>
    9. <a data-id="{% $value.cate_id %}" name="delete" href="javascript:;" class="btn btn-danger btn-xs">删除</a>
    10. </td>
    11. </tr>
    12. {%/each%}
    13. </script>
    14. <script>
    15. // template('script 节点 id')
    16. // 当前页面是由服务端渲染出来的
    17. // 服务端先先对当前页面进行模板引擎处理
    18. // 服务端处理的时候根本不关心你的内容,只关心模板语法,我要解析替换
    19. // 当你的服务端模板引擎语法和客户端模板引擎语法一样的时候,就会产生冲突
    20. // 服务端会把客户端的模板字符串页给解析掉
    21. // 这就是所谓的前后端模板语法冲突
    22. // 修改模板引擎的语法界定符
    23. template.defaults.rules[1].test = /{%([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*%}/;
    24. </script>

    后续处理:

    1. <script>
    2. loadList()
    3. /*
    4. * 加载分类列表数据
    5. */
    6. function loadList() {
    7. $.ajax({
    8. url: '/api/categories',
    9. method: 'GET',
    10. data: {},
    11. dataType: 'json',
    12. success: function (data) {
    13. // 1. 判断数据是否正确
    14. // 2. 使用模板引擎渲染列表数据
    15. // 3. 把渲染结果替换到列表容器中
    16. if (data.success) {
    17. listData: data.data
    18. })
    19. $('#list_container').html(htmlStr)
    20. }
    21. },
    22. error: function (err) {
    23. console.log('请求失败了', err)
    24. }
    25. })
    26. }
    27. </script>
    • 客户端发起请求,等待响应
    • 服务端收到请求
    • 服务端处理请求
    • 服务端发送响应
    • 客户端收到响应
    • 客户端根据响应结果进行后续处理

    删除分类

    一、通过事件委托方式为动态渲染的删除按钮添加点击事件

    • 第一种把添加事件的代码放到数据列表渲染之后
    • 第二种使用事件代理(委托)的方式
    1. ...
    2. $('#list_container').on('click', 'a[name=delete]', handleDelete)
    3. ...

    二、在删除处理中发起 Ajax 请求删除操作

    1. function handleDelete() {
    2. if (!window.confirm('确认删除吗?')) {
    3. return
    4. }
    5. var id = $(this).data('id')
    6. // 点击确定,发起 Ajax 请求,执行删除操作
    7. $.ajax({
    8. url: '/api/categories/delete',
    9. method: 'GET',
    10. data: {
    11. id: id
    12. },
    13. dataType: 'json',
    14. success: function (data) {
    15. console.log(data)
    16. },
    17. error: function (err) {
    18. console.log(err)
    19. }
    20. })
    21. return false
    22. }

    三、在服务端添加路由接口处理删除操作

    1. router.get('/api/categories/delete', (req, res, next) => {
    2. // 获取要删除的数据id
    3. const { id } = req.query
    4. // 操作数据库,执行删除
    5. db.query('DELETE FROM `ali_cate` WHERE `cate_id`=?', [id], (err, ret) => {
    6. if (err) {
    7. throw err
    8. }
    9. res.send({
    10. success: true,
    11. ret
    12. })
    13. })
    14. })

    四、客户端收到响应结果,判断如果删除成功,重新请求加载数据列表

    1. ...
    2. success: function (data) {
    3. if (data.success) {
    4. // 删除成功,重新加载列表数据
    5. loadList()
    6. }
    7. }
    8. ...

    添加分类

    基本步骤:

    1. 客户端发起请求,提交表单数据,等待服务端响应
    2. 服务端收到请求,处理请求,发送响应
    3. 客户端收到响应,根据响应结果进行后续处理

    一、客户端发起添加请求

    • 表单的 submit 提交事件
    • 表单内容的获取 $(表单).serialize()
    1. // 表单提交
    2. // submit 提交事件
    3. // 1. button 类型为 submit 的会触发
    4. // 2. 文本框敲回车也会触发
    5. $('#add_form').on('submit', handleAdd)
    6. function handleAdd() {
    7. // serialize 会找到表单中所有的带有 name 的表单元素,提取对应的值,拼接成 key=value&key=value... 的格式数据
    8. var formData = $('#add_form').serialize()
    9. $.ajax({
    10. url: '/api/categories/create',
    11. method: 'POST',
    12. data: formData,
    13. // Content-Type 为 application/x-www-form-urlencoded
    14. // data: { // data 为对象只是为了让你写起来方便,最终在发送给服务器的时候,$.ajax 还会把对象转换为 key=value&key=value... 的数据格式
    15. // 普通的表单 POST 提交(没有文件),必须提交格式为 key=value&key=value... 数据,放到请求体中
    16. // key: value,
    17. // key2: value2
    18. // },
    19. dataType: 'json',
    20. success: function (resData) {
    21. console.log(resData)
    22. },
    23. error: function (error) {
    24. console.log(error)
    25. }
    26. })
    27. return false
    28. }

    二、服务端处理请求

    1. 在 app.js 中配置解析表单 POST 请求体
    1. ...
    2. /**
    3. * 该方法目前可以在 4.16.0 之前使用,以后可能会废弃掉,建议还是使用之前 body-parser 的方式
    4. * 配置解析表单 POST 请求体(只能解析 Content-Type 为 application/x-www-form-urlencoded 数据)
    5. * 配置好以后,我们就可以在请求处理函数中通过 req.body 获取请求体数据
    6. * express 已经内置 body-parser
    7. * express 通过 express.urlencoded 方法包装了 body-parser
    8. */
    9. app.use(express.urlencoded())
    10. ...
    1. 执行数据库操作和发送响应数据
    1. router.post('/api/categories/create', (req, res, next) => {
    2. // 1. 获取表单数据
    3. const body = req.body
    4. // 2. 验证数据的有效性(永远不要相信客户端的输入)
    5. // 3. 操作数据库,执行插入操作
    6. // { cate_name: 'xxx', cate_slug: 'xxx' }
    7. // query 方法会把对象转为 filed1==value1&filed2=value2... 格式,替换到 sql 语句中的 ?
    8. db.query('INSERT INTO `ali_cate` SET ?', body, (err, ret) => {
    9. if (err) {
    10. throw err
    11. }
    12. // 4. 发送响应给客户端
    13. res.send({
    14. success: true,
    15. data: ret
    16. })
    17. })
    18. })

    三、客户端收到响应,后续处理

    • 判断响应是否正确
    • 如果正确,则重新加载最新的列表数据,清空表单内容
    1. ...
    2. success: function (resData) {
    3. if (data.success) {
    4. // 添加成功,重新加载列表数据
    5. loadList()
    6. // 清空表单内容
    7. $('#add_form').find('input[name]').val('')
    8. }
    9. }
    10. ...

    编辑分类

    动态显示编辑模态框

    一、点击编辑,弹出模态框

    • Bootstrap 自带的 JavaScript 组件:模态框

    二、发起 Ajax 请求,获取 id=xxx 的分类数据

    三、服务端收到请求,获取 id,操作数据库,发送响应

    四、客户端收到服务端响应,进行后续处理

    提交编辑表单完成编辑操作

    一、注册编辑表单的提交事件

    二、在提交事件中,获取表单数据,发送 Ajax POST请求 /api/categories/update,提交的数据放到请求体中

    • 表单隐藏域的使用

    三、服务端收到请求,获取查询字符串中的 id,获取请求体,执行数据库修改数据操作,发送响应

    四、客户端收到响应,根据响应结果做后续处理

    自动挂载路由

    • 自动加载路由模块
      • 使用 glob 获取指定的文件路径
      • 循环路由模块文件路径挂载路由模块
    1. // 获取 routes 目录中所有的路由文件模块路径
    2. const routerFiles = glob.sync('./routes/**/*.js')
    3. // 循环路由模块路径,动态加载路由模块挂载到 app 中
    4. const router = require(routerPath)
    5. if (typeof router === 'function') {
    6. // router.prefix 是我们添加的自定义属性,作用是用来设定路由的模块的访问前缀
    7. // 当路由模块没有 prefix 的时候,我们给一个 / 作为默认值,相当于没有前缀限制。
    8. // 因为所有的请求路径都以 / 开头
    9. app.use(router.prefix || '/', router)
    10. }
    11. })

    客户端表单数据验证

    服务端数据验证

    • 基本数据校验
    • 业务数据校验

    服务端全局错误处理

    利用错误处理中间件:

    1. app.use((err, req, res, next) => {
    2. // 1. 记录错误日志
    3. // 2. 一些比较严重的错误,还应该通知网站负责人或是开发人员等
    4. // 可以通过程序调用第三方服务,发短信,发邮件
    5. // 3. 把错误消息发送到客户端 500 Server Internal Error
    6. res.status(500).send({
    7. error: err.message
    8. })
    9. })

    然后在我们的路由处理中,如果有错误,就调用 next 函数传递错误对象,例如

    1. rouget.get('xxx', (req, res, next) => {
    2. xxx操作
    3. if (err) {
    4. // 调用 next,传递 err 错误对象
    5. return next(err)
    6. }
    7. })

    客户端统一错误处理

    用户管理

    用户列表

    1. 添加路由,渲染 admin/users.html 页面
    2. 在 users.html 页面中套用模板页

    几个小点:

    • 把 art-template 文件资源的引用放到模板页中
    • 把修改模板引擎默认语法规则的代码放到模板页中
    • 把注册的全局 Ajax 错误处理方法放到模板页中

    添加用户

    • 插件表单验证
    • 异步请求验证

    编辑用户

    用户登录

    • 基本登录流程处理
    • 记录用户登录状态

    Cookie 和 Session

    • HTTP 协议本身是无状态的

    • Cookie 发橘子,往背后贴纸条

    • Session 超市存物柜,东西放到柜子里,你拿着小票

    使用 Session 存储登录状态

    参考文档:

    1. 安装
    1. 配置
    1. ...
    2. app.use(session({
    3. // 生成密文是有一套算法的来计算生成密文,如果网站都使用默认的密文生成方式, 就会有一定的重复和被破解的概率,所以为了增加这个安全性,算法对外暴露了一个混入私钥的接口,算法在生成密文的时候会混入我们添加的自定义成分
    4. secret: 'itcast',
    5. resave: false,
    6. // 如果为 true 无论是否往 Session 中存储数据,都直接给客户端发送一个 Cookie 小票
    7. // 如果为 false,则只有在往 Session 中写入数据的时候才会下发小票
    8. // 推荐设置为 true
    9. saveUninitialized: true
    10. }))
    11. ...
    1. 使用
    1. // 存储 Session 数据
    2. // 就想操作对象一样,往 Session 中写数据
    3. req.session.名字 =
    4. // 读取 Session 中的数据
    5. // 就是读取对象成员一样,读取 Session 中的数据
    6. req.session.名字

    页面访问权限控制

    1. /**
    2. * 统一控制后台管理系统的页面访问权限
    3. * 相当于为所有以 /admin/xxxxx 开头的请求设置了一道关卡
    4. *
    5. */
    6. app.use('/admin', (req, res, next) => {
    7. // 1. 如果是登录页面 /admin/login,允许通过
    8. if (req.originalUrl === '/admin/login') {
    9. // 这里 next() 就会往后匹配调用到我们的那个能处理 /admin/login 的路由
    10. return next()
    11. }
    12. // 2. 其他页面都一律验证登录状态
    13. const sessionUser = req.session.user
    14. // 如果没有登录页, 让其重定向到登录页
    15. if (!sessionUser) {
    16. return res.redirect('/admin/login')
    17. }
    18. // 如果登录了,则允许通过
    19. // 这里调用 next 就是调用与当前请求匹配的下一个中间件路由函数
    20. // 例如,当前请求是 /admin/users ,则 next 会找到我们那个匹配 /admin/users 的路由去处理
    21. // /admin/categories ,则 next 会找到我们添加的那个 /admin/categories 的路由去处理
    22. next()
    23. })

    用户退出

    • 实现用户退出接口
    1. /**
    2. * 用户退出
    3. */
    4. router.get('/admin/logout', (req, res) => {
    5. // 1. 清除登录状态
    6. delete req.session.user
    7. // 2. 跳转到登录页
    8. res.redirect('/admin/login')
    9. })
    • 使用 Ajax 请求完成用户退出

    展示当前登录用户信息

    • 把 Session 中的当前登录用户数据传递到页面模板中
    • app.locals 的应用

    简单点就是在每一次 render 页面的时候,把 req.session.user 传到模板中去使用。

    当你需要在多个模板中使用相同的模板数据的时候,每一次 render 传递就麻烦了。所以 express 提供了一种简单的方式,我们可以把模板中公用的数据放到 app.locals 中。app.locals 中的数据可以在模板中直接使用。

    Session 数据持久化

    参考文档:https://github.com/chill117/express-mysql-session

    Session 数据持久化的目的是为了解决服务器重启或者崩溃挂掉导致的 Session 数据丢失的问题。

    因为默认情况下 Session 数据是存储在内存中的,服务器一旦重启就会导致 Session 数据丢失。

    所了我们为了解决这个问题,把 Session 数据存储到了数据库中。

    1. 安装
    1. npm i express-mysql-session
    1. 配置
    1. ...
    2. const session = require('express-session')
    3. /**
    4. * 配置 Session 数据持久化
    5. * 参考文档:https://github.com/chill117/express-mysql-session#readme
    6. * 该插件会自动往数据库中创建一个 sessions 表,用来存储 Session 数据
    7. */
    8. const MySQLStore = require('express-mysql-session')(session)
    9. const sessionStore = new MySQLStore({
    10. host: 'localhost',
    11. port: 3306,
    12. user: 'root',
    13. password: '123456',
    14. database: 'alishow62'
    15. })
    16. const app = express()
    17. app.use(session({
    18. secret: 'keyboard cat',
    19. resave: false,
    20. saveUninitialized: true,
    21. store: sessionStore, // 告诉 express-session 中间件,使用 sessionStore 持久化 Session 数据
    22. }))
    23. ...

    记住我(*)

    记住我处理流程

    对称加解密:加解密使用的私钥必须一致。

    加密:

    1. const crypto = require('crypto');
    2. const cipher = crypto.createCipher('aes192', '私钥');
    3. let encrypted = cipher.update('要加密的数据', 'utf8', 'hex');
    4. encrypted += cipher.final('hex');
    5. console.log(encrypted);
    6. // Prints: ca981be48e90867604588e75d04feabb63cc007a8f8ad89b10616ed84d815504

    解密:

    1. const crypto = require('crypto');
    2. const decipher = crypto.createDecipher('aes192', '私钥');
    3. const encrypted =
    4. '要解密的数据';
    5. let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    6. decrypted += decipher.final('utf8');
    7. console.log(decrypted);
    8. // Prints: some clear text data

    文章管理

    添加文章

    一、客户端表单提交(带有文件的POST请求)处理

    1. function handleSubmit() {
    2. // 1. 获取表单数据
    3. // multipart/form-data
    4. var formEl = $('#new_form')
    5. var formData = new FormData(formEl.get(0))
    6. // 2. 表单提交
    7. $.ajax({
    8. url: '/api/posts/create',
    9. type: 'POST',
    10. data: formData,
    11. processData: false, // 不处理数据
    12. contentType: false, // 不设置内容类型
    13. success: function (resData) {
    14. // 3. 根据响应结果做后续处理
    15. console.log(resData)
    16. },
    17. error: function (err) {
    18. console.log(err)
    19. }
    20. })
    21. return false
    22. }

    二、服务端接口处理

    1. express 本身不处理文件上传
    2. 使用 处理带有文件的表单 POST 请求

    基本用法:(try-try-see)

    1. 安装
    1. npm i multer
    1. 基本示例
    1. var express = require('express')
    2. var multer = require('multer')
    3. var upload = multer({ dest: 'uploads/' }) // 指定上传文件的存储路径
    4. var app = express()
    5. // /profile 是带有文件的 POST 请求,使用 multer 解析文件上传
    6. // upload.single() 需要给定一个参数:告诉multer,请求体中哪个字段是文件
    7. app.post('/profile', upload.single('avatar'), function (req, res, next) {
    8. // req.file 是 `avatar` 文件的相关信息(原本的文件名,新的唯一名称,文件保存路径,文件大小...)
    9. // req.body 是请求体中的那些普通的文本字段
    10. // 数据库中不存储文件,文件还是存储在磁盘上,数据库中存储文件在我们 Web 服务中的 url 资源路径
    11. })
    1. multer 保存的文件默认没有后缀名,如果需要的话,就需要下面这样来使用
    1. var storage = multer.diskStorage({
    2. // 可以动态处理文件的保存路径
    3. destination: function (req, file, cb) {
    4. cb(null, '/tmp/my-uploads')
    5. },
    6. // 动态的处理保存的文件名
    7. filename: function (req, file, cb) {
    8. cb(null, file.fieldname + '-' + Date.now()) // 这里的关键是这个时间戳,能保证文件名的唯一性(不严谨)
    9. }
    10. })
    11. var upload = multer({ storage: storage })
    12. app.post('/profile', upload.single('avatar'), function (req, res, next) {
    13. // req.file 是 `avatar` 文件的相关信息(原本的文件名,新的唯一名称,文件保存路径,文件大小...)
    14. // req.body 是请求体中的那些普通的文本字段
    15. // 数据库中不存储文件,文件还是存储在磁盘上,数据库中存储文件在我们 Web 服务中的 url 资源路径
    16. })
    1. 处理多文件

    有多个名字都一样的 file 类型的 input

    1. app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
    2. // req.files is array of `photos` files
    3. // req.body will contain the text fields, if there were any
    4. })

    处理多个不同名字的 file 类型的 input:

    1. var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
    2. app.post('/cool-profile', cpUpload, function (req, res, next) {
    3. // req.files is an object (String -> Array) where fieldname is the key, and the value is array of files
    4. //
    5. // e.g.
    6. // req.files['avatar'][0] -> File
    7. // req.files['gallery'] -> Array
    8. //
    9. // req.body will contain the text fields, if there were any

    富文本编辑器 wangEditor

    这里我们以使用 wangEditor 为例:

    文章列表

    编辑文章

    网站设置

    轮播广告管理