本篇目录
- 前言
- 一、图片上传
- 1.前端部分
- 2.后端部分
- 二、部署项目到服务器
- 1.选择一
- 2.选择二
- 三、关于完善项目的思考
- 1.整体思路
- 2.下一步
前言
博客一直更新地很慢,多谢各位老铁的支持与耐心等候。一开始的时候真的没有想到做教程是这么蛋疼的一件事,这个项目我也就利用业余时间搞了一两周,没想到十篇文章竟然写了四个月。当然,更新慢的主要原因,还是我这该死的懒惰。
这四个月里有 80 天左右的时间处在强制加班状态。忙完休了十几天假,回了躺老家,各种探亲访友,体验了一下咸鱼的生活,算是回了一波血。
这篇文章主要讲讲图片的上传。近来留言提建议的老铁越来越多了,十分感谢大家的支持,但是毕竟做教程面面俱到很难,我的主要目的是在大家毫无头绪的时候抛个砖,相信大家对框架有了一定认识之后,只要是想做的功能都可以实现。解决问题的过程可能会有坎坷,但事后想来一定会有满足感。
一、图片上传
之前我们的封面图片保存在网上的图床中,显然有些瓜皮,现在我们来完善一下。
上传文件的逻辑很简单:前端向后端发送 post 请求,后端对接收到的数据进行处理(压缩、格式转换、重命名等),并保存到服务器中指定的位置,再把该位置对应的 URL 返回给前端即可。
1.前端部分
利用 element 提供的组件 <el-upload>
可以轻松搞定前端。该组件的详细文档地址如下:
/#/zh-CN/component/upload
为了不让原有组件的代码量太大,我新建了一个组件,命名为 ,对上传组件做了一些简单的配置,代码如下:
<template>
<el-upload
class="img-upload"
ref="upload"
action="http://localhost:8443/api/covers"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:on-success="handleSuccess"
multiple
:limit="1"
:on-exceed="handleExceed"
:file-list="fileList">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
</template>
<script>
export default {
name: 'ImgUpload',
data () {
return {
fileList: [],
url: ''
}
},
methods: {
handleRemove (file, fileList) {
},
handlePreview (file) {
},
handleExceed (files, fileList) {
this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
},
beforeRemove (file, fileList) {
return this.$confirm(`确定移除 ${file.name}?`)
},
handleSuccess (response) {
this.url = response
this.$emit('onUpload')
this.$message.warning('上传成功')
},
clear () {
this.$refs.upload.clearFiles()
}
}
}
</script>
可以看出,action 属性指定了上传操作对应的 api。各种事件钩子可以顾名思义。我们主要用了 :before-remove
、:on-success
、:on-exceed
几种,其它的可以根据需要自行编写。
还有一个重要的属性是 multiple,和 :limit
属性配合使用可以检测上传文件的数量。
根据 :on-success
事件对应的方法,当该组件接收到后端返回的成功信息时,会触发父组件 的
onUpload
事件,把接收到的 URL 赋给图书信息表单的 cover 字段,这个 URL 需要我们在后端根据资源存放位置生成。提交后,数据库里就会保存服务器上的资源对应的 URL。
对应的修改有两处,一是在表单中封面字段的位置添加该组件,即下图的效果:
需要把
<el-form-item label="封面" :label-width="formLabelWidth" prop="cover">
<el-input v-model="" autocomplete="off" placeholder="图片 URL"></el-input>
</el-form-item>
改为
<el-form-item label="封面" :label-width="formLabelWidth" prop="cover">
<el-input v-model="" autocomplete="off" placeholder="图片 URL"></el-input>
<img-upload @onUpload="uploadImg" ref="imgUpload"></img-upload>
</el-form-item>
别忘了导入该组件的语句。(import ImgUpload from './ImgUpload'
)要是写的时候顺序搞错了,先写标签再写导入语句,eslint 会检测出错误,出现这种情况就把标签再敲一遍就好了。
第二处修改是在 method 中添加对应的方法如下:
uploadImg () {
this.form.cover = this.$refs.imgUpload.url
}
然后可以测试一下,当然是用不了的,毕竟后端啥也没有嘛。
2.后端部分
后端主要解决如下两个问题:
- 如何接收前端传来的图片数据并保存
- 如何避免重名(图片资源的名字很可能重复,如不修改可能出现问题)
首先,在后端新建 utils
包,创建一个工具类 StringUtils
并编写生成指定长度随机字符串的方法:
package com.gm.wj.util;
import java.util.Random;
public class StringUtils {
public static String getRandomString(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
然后在 LibraryController
中添加 PostMapping:
@CrossOrigin
@PostMapping("api/covers")
public String coversUpload(MultipartFile file) throws Exception {
String folder = "D:/workspace/img";
File imageFolder = new File(folder);
File f = new File(imageFolder, StringUtils.getRandomString(6) + file.getOriginalFilename()
.substring(file.getOriginalFilename().length() - 4));
if (!f.getParentFile().exists())
f.getParentFile().mkdirs();
try {
file.transferTo(f);
String imgURL = "http://localhost:8443/api/file/" + f.getName();
return imgURL;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
这里涉及到对文件的操作,对接收到的文件重命名,但保留原始的格式。可以进一步做一下压缩,或者校验前端传来的数据是否为指定格式,这里不再赘述。
测试一下,成了!URL 变成了我们自己的了。
当然,现在这样还不行,这个 URL 的前缀是我们自己构建的,还需要把它跟我们设置的图片资源文件夹,即 D:/workspace/img
对应起来。
在 config\MyWebConfigurer
中添加如下代码:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/api/file/**").addResourceLocations("file:" + "d:/workspace/img/");
}
测试一下,改一改第一本书的封面
STEP 1
STEP 2
STEP 3
一本好好的书,瞬间变得又 low 又丑!
查看资源文件夹,图片也成功保存了下来。
有的老铁可能想到了, 6 位的随机数也可能随出完全一样的名字,这个问题嘛,也交给你们想办法解决喽。
二、部署项目到服务器
这部分其实是附赠内容,一开始觉得不用说,但考虑到整个教程的完整性(主要是本篇字数不太够),我决定简单唠一唠。
我们在开发时采用了前后端分离的模式,在部署的时候有两种选择:
选择一: 把前端项目部署在 web 服务器中,把后端项目部署在应用服务器中
选择二: 把前端项目打包,作为后端项目的静态文件,再把后端项目部署在应用服务器中
一般来讲,既然我们前后端分离了,那选择一是自然而然的,前面也说过,使用 web 服务器的好处有如下几点:
- 可以实现反向代理,提高网站的安全性
- 方便维护,一些小的修改不必同时协调前后端开发人员
- 对静态资源的加载速度更快
但考虑到成本、开发团队技术能力等问题,选择二也有其存在的意义。
下面具体讲一下两种选择的操作方法。
1.选择一
首先下载 nginx,官方网址如下:
/en/
我选择了 Windows 下的最新版本 1.17.3,下载下来是一个压缩包
把它解压到某个位置,比如我是 D 盘根目录。
打开前端项目,执行 npm run build,等候进程完成。这时,项目的 dist 文件夹下将出现我们打包好的内容。
把它拷贝进 nginx\html
下(如果该文件夹里有内容,需要把原来的内容删掉)
接着,配置一下服务器的默认端口,打开 nginx\conf\
,找到 server 的配置处,把 listen 80 改为 listen 8081,方便测试,注意后面还有配置虚拟主机的地方,是加了注释符号的,不要找错了
最后,由于 nginx 无法直接处理 vue 的 history 模式路由(感谢热心网友【张敬远】、【甜蜜云豆】、【二哥很难】反馈),通过地址栏输入地址或刷新页面会导致页面无法访问。
这是由于输入地址或刷新操作会向服务器发出请求,但我们这个单页面应用表面上像更改了地址,实际上还是通过 js 来控制页面的变化,nginx 上并不存在与请求所对应的页面,也就无法做出响应。因此,与后端的做法相同,我们需要把这个请求转发到 。
让我们再次打开 文件,添加一条 location 配置,同时将原来默认的 location 注释掉:
#location / {
# root html;
# index ;
#}
location / {
try_files $uri $uri/ /;
}
最后,为了能够默认打开首页,我们在前端 router\
里添加一条路由:
{
path: '/',
name: 'index',
redirect: '/index',
component: AppIndex,
meta: {
requireAuth: true
}
},
这样在已登录状态下访问 http://localhost:8081/ 会跳转到 /index
,否则会跳转到登录页面。
配置完成后,运行 nginx 根目录下的 即可,访问 http://localhost:8081/ ,发现自动跳转到了登录界面。当然,这时候没有后端的验证,是登录不了的。
接着,部署后端项目,流程基本类似。正常来讲开发 Java Web 应用都是要配置 tomcat 的,只是由于我们使用的 Spring Boot 内置了一个 tomcat,所以省了不少功夫。更牛逼的是,把 Spring Boot 项目打成 jar 包,这个 tomcat 就被内置到了 jar 包里,也就是说你只需要把这个 jar 包放在有 Java 环境的服务器上直接执行,就万事大吉了。
一般来说 Web 项目我们会打包成 war,然鹅前后端分离嘛,Java 项目只是提供接口,跟传统的 Java 服务端程序类似,打成 jar 包更加轻便。
下面说说打包的步骤。
首先打开后端项目的 ,修改
<packaging>
标签里的 war 为 jar ,除此之外,还可以配置版本号、jar 包名称等。
在该文件夹下执行 mvn install
命令(可以利用 IDEA 的终端)
等待程序执行完成,在项目的 target 文件夹下就会出现我们的 jar 包(. 是上一次打包的备份文件)
然后在控制台中到 jar 包对应的目录下执行 java -jar wj-1.0.
(注意名称)即可。
最后测试一下,访问 http://localhost:8081 ,发现可以正常登录并使用后端接口了。
第一本书的封面真丑啊。。。
2.选择二
前端打包的方式是相同的,不同的是需要把前端项目 dist
文件夹中的两个文件 static 和 拷贝到后端项目的 \src\main\resources\static
目录下。
之后,把后端项目打成 war 包。打包之前,我们需要把内置的 tomcat 排除出去,避免冗余。
排除的方法有两种,第一种是在 spring-boot-starter-web
的依赖里添加一个 <exclusion>
,代码如下:
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId></groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
第二种是在 dependencies
中添加 tomcat 相关依赖如下:
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
这个 provided
的意思是 tomcat 由外部提供,不用打包。(如果过去复制了我提供的 ,这条依赖是本来就存在的)
接下来记得把 <packaging>
处的 jar 改为 war 执行打包命令 mvn install
,然后 war 包就出现啦。
至于如何部署 war 包我就不多说了,记得把 tomcat 的默认端口修改为 8443(根据前端的转发端口设置)即可。
三、关于完善项目的思考
1.整体思路
作为详细教程的部分我想基本上是结束了,但这个项目本身是不够完善的,主要是目前的功能难以发挥实际作用。
我的设想是,把这个项目作为我个人的 知识库。作用有下面几处:
- 把我搜集到的有用的资料、自己的心得体会之类都有效地管理起来,并能够方便的加以利用。这个库可以不断地更新,不断地扩展,这样再过几十年,一定是一份了不得的财富。
- 对知识库的内容以及操作记录等进行自动分析,监测自身学习行为规律,甚至可以利用一些模型分析某种知识带来的经济效益。利用这些分析结果,可以及时优化学习规划与学习策略。
- 用这个知识库系统的历代版本作为未来开发技术发展的一个见证,也作为保持我个人技术能力的一条伏线。由于种种原因,我的工作逐渐偏离了开发岗位,但我对技术的热情不会变,希望成为真正理解技术的人的愿望不会变。
接下来我会继续完善项目本身,并使用更规范的方式开发,争取尽快把它做成一个可以上线的成熟产品。前一段趁活动在腾讯云买了 3 年的服务器,刚好可以玩一玩。
这个系列的后续,我会尝试转变 “贴代码 + 讲解基础概念” 这种模式。这前十篇可以看作面向 “程序员” 的文章,是对框架的一个基本认识,接下来的文章我想面向 “开发者”,从产品、项目管理等角度分享开发的经验。
2.下一步
- 优化搜索功能,这个我想了一下,一是目前对模糊查询的支持程度不够,需要优化 sql 语句,二是全站查询时不仅仅针对图书这一种资源,需要更强大的功能,可以尝试使用开源的站内搜索引擎。这部分内容我会尽快补充到本篇文章里。
- 重建项目基础框架,分析并剥离前后台功能。
- 着手开发项目第二部分——用户角色权限管理模块。
希望 Vue3.0 早日问世。
项目的进程,还请关注 github 仓库:
/Antabot/White-Jotter
查看系列文章目录:
/article/details/88925013