开源项目学习-V部落

开源项目-V部落

github  --   https://github.com/lenve/VBlog

一、前期工作(此处只是展示如何开启前后端工程):

1. IDEA创建Springboot工程

工程名:v-springboot

2. VSCode创建Vue-cli3工程

1) node.js安装

2) 安装vue-cli3脚手架

  • npm install -g @vue/cli

3) 安装vue-cli服务

  • npm install -g @vue/cli-service-global

4) 创建v-vue工程 

  • vue create my-project
  • 若出现错误:管理员运行powershell并设置set-ExecutionPolicy RemoteSigned为true

二、登录模块

1. 前端工作流程

启动前端和后端,前端初始化地址:

http://localhost:8080/

首先看前端的路由地址,位于router(对用的插件--vue-router)下的index.js:

{
  path: '/',
  name: '登录',
  hidden: true,
  component: Login
}

前端是如何先到登录的这个路由的呢?如下图:

1) 前端命令

npm run dev

对应的脚本命令

"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",

因此对应的基本配置为:

build/webpack.dev.conf.js

看此具体配置

'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')  // 合并配置的
// dev和pro通用的配置
// 1. entry: 入口,此处指定了这个程序的启动入口:./src/main.js
// 2. output: 出口,根路径下的index.js
// 3. resolve: 配置 Webpack 如何寻找模块所对应的文件,文件拓展后缀
// 4. module: 模块系统,rules:一些资源匹配的必要规则
const baseWebpackConfig = require('./webpack.base.conf')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

// 根据 process.env.HOST 的值判断当前是什么环境
// 命令:npm run build -- test ,process.env.HOST就设置为:'test'
const HOST = process.env.HOST
// 端口号
const PORT = process.env.PORT && Number(process.env.PORT)

// 开发环境的基本配置
const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    // 新增的css匹配规则
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  // 跨域的一些配置
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: true,
    hot: true,
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },
  plugins: [
    // 设置环境变量插件,主要是dev和pro的切换
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    // 热更新插件
    new webpack.HotModuleReplacementPlugin(),
    // HMR shows correct file names in console on update.
    new webpack.NamedModulesPlugin(), 
    // 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段。这样可以确保输出资源不会包含错误
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    // 可以生成创建html入口文件
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
  ]
})
// Promise解决回调地狱问题
module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      // 识别某些类别的webpack错误,并清理,聚合和优先级
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})

开源项目学习-V部落_第1张图片

2) 前端登录模块发出请求,后端才会根据请求地址传回数据

Login.vue:使用element-ui模板进行登录页面的开发

axios:Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中

在./utils/api.js定义Restful请求处理规则:get、post、delete、put

methods: {
  submitClick: function () {
    var _this = this;
    this.loading = true;
    postRequest('/login', {
      username: this.loginForm.username,
      password: this.loginForm.password
    }).then(resp=> {
      _this.loading = false;
      if (resp.status == 200) {
        //成功
        var json = resp.data;
        if (json.status == 'success') {
          _this.$router.replace({path: '/home'});
        } else {
          _this.$alert('登录失败!', '失败!');
        }
      } else {
        //失败
        _this.$alert('登录失败!', '失败!');
      }
    }, resp=> {
      _this.loading = false;
      _this.$alert('找不到服务器⊙﹏⊙∥!', '失败!');
    });
  }
}

2. 后端接受请求进行处理并返回数据

前端输出用户名与密码后,请求地址:localhost:8081/login

先认识一下springboot中的websecurityconfig中的formLogin配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
            .formLogin()
                .loginPage("/login.html")//用户未登录时,访问任何资源都转跳到该路径,即登录页面
                .loginProcessingUrl("/login")//登录表单form中action的地址,也就是处理认证请求的路径
                .usernameParameter("uname")///登录表单form中用户名输入框input的name名,不修改的话默认是username
                .passwordParameter("pword")//form中密码输入框input的name名,不修改的话默认是password
                .defaultSuccessUrl("/index")//登录认证成功后默认转跳的路径
                .and()
            .authorizeRequests()
                .antMatchers("/login.html","/login").permitAll()//不需要通过登录验证就可以被访问的资源路径
                .antMatchers("/biz1").hasAnyAuthority("biz1")  //前面是资源的访问路径、后面是资源的名称或者叫资源ID
                .antMatchers("/biz2").hasAnyAuthority("biz2")
                .antMatchers("/syslog").hasAnyAuthority("syslog")
                .antMatchers("/sysuser").hasAnyAuthority("sysuser")
                .anyRequest().authenticated();
    }
    
}

项目中的weSecurity配置如下: 

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/admin/category/all").authenticated()
            .antMatchers("/admin/**","/reg").hasRole("超级管理员")///admin/**的URL都需要有超级管理员角色,如果使用.hasAuthority()方法来配置,需要在参数中加上ROLE_,如下.hasAuthority("ROLE_超级管理员")
            .anyRequest().authenticated()//其他的路径都是登录后即可访问
            .and().formLogin().loginPage("/login_page")
//                .successHandler(new AuthenticationSuccessHandler() {
//                    @Override
//                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//                        httpServletResponse.setContentType("application/json;charset=utf-8");
//                        PrintWriter out = httpServletResponse.getWriter();
//                        out.write("{\"status\":\"success\",\"msg\":\"登录成功\"}");
//                        out.flush();
//                        out.close();
//                    }
//                })
//         此处这样写更容易理清逻辑
            .successForwardUrl("/login_success")
//                .failureHandler(new AuthenticationFailureHandler() {
//                    @Override
//                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//                        httpServletResponse.setContentType("application/json;charset=utf-8");
//                        PrintWriter out = httpServletResponse.getWriter();
//                        out.write("{\"status\":\"error\",\"msg\":\"登录失败\"}");
//                        out.flush();
//                        out.close();
//                    }
//                })
            .failureForwardUrl("/login_error")
            .loginProcessingUrl("/login")
            .usernameParameter("username").passwordParameter("password").permitAll()
            .and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(getAccessDeniedHandler());
}

当来到后端后,首先进行springSecurity校验,匹配的路由地址为/login--为表单的method

而在http.authorizeRequests()授权之前,需要认证

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
        @Override
        public String encode(CharSequence charSequence) {
            return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
        }

        /**
         * @param charSequence 明文
         * @param s 密文
         * @return
         */
        @Override
        public boolean matches(CharSequence charSequence, String s) {
            return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
        }
    });
}

userService implements UserDetailsService 并overwrite loadUserByUsername方法

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    User user = userMapper.loadUserByUsername(s);
    if (user == null) {
        //避免返回null,这里返回一个不含有任何值的User对象,在后期的密码比对过程中一样会验证失败
        return new User();
    }
    //查询用户的角色信息,并返回存入user中
    List roles = rolesMapper.getRolesByUid(user.getId());
    user.setRoles(roles);
    return user;
}

依次执行以下sql语句

// 查询用户是否存在
SELECT * FROM user WHERE username=? 

// 查询用户的权限
SELECT r.* FROM roles r,roles_user ru WHERE r.`id`=ru.`rid` AND ru.`uid`=? 

因此登录模块后端controller理清

@RequestMapping("/login_error")
public RespBean loginError() {
    return new RespBean("error", "登录失败!");
}

@RequestMapping("/login_success")
public RespBean loginSuccess() {
    return new RespBean("success", "登录成功!");
}

/**
 * 如果自动跳转到这个页面,说明用户未登录,返回相应的提示即可
 * 

* 如果要支持表单登录,可以在这个方法中判断请求的类型,进而决定返回JSON还是HTML页面 * * @return */ @RequestMapping("/login_page") public RespBean loginPage() { return new RespBean("error", "尚未登录,请登录!"); }

三、Home页面分析

主要使用的是Element-ui组件,该页面主要分为二大部分:

  • el-header
  • el-container(注意不要与大容器搞混):左侧导航栏(el-aside)和主题显示部分(el-container下的el-main)

1) el-header

开源项目学习-V部落_第2张图片

mounted: function () {
    this.$alert('为了确保所有的小伙伴都能看到完整的数据演示,数据库只开放了查询权限和部分字段的更新权限,其他权限都不具备,完整权限的演示需要大家在自己本地部署后,换一个正常的数据库用户后即可查看,这点请大家悉知!', '友情提示', {
      confirmButtonText: '确定',
      callback: action => {
      }
    });
    var _this = this;
    getRequest("/currentUserName").then(function (msg) {
      _this.currentUserName = msg.data;
    }, function (msg) {
      _this.currentUserName = '游客';
    });
  },
  data(){
    return {
      currentUserName: ''
    }
  }

mouted--vue的生命周期的钩子函数,在加载vue的同时就执行其下面的函数,路由/currentUserName请求后端地址

后端中在登录时使用spring security,其中就保存了登录的函数

// SecurityContextHolder中持有的是当前用户的SecurityContext,而SecurityContext持有的是代表当前用户相关信息的Authentication的引用。
SecurityContextHolder.getContext().getAuthentication().getPrincipal()

因此很轻松地获得了当前用户的所有信息,包括用户的权限

@RequestMapping("/currentUserName")
public String currentUserName() {
    return Util.getCurrentUser().getNickname();
}

该用户名的下拉菜单无实际意义,除了注销:

methods: {
  handleCommand(command){
    var _this = this;
    if (command == 'logout') {
      this.$confirm('注销登录吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(function () {
        getRequest("/logout")
        _this.currentUserName = '游客';
        _this.$router.replace({path: '/'});
      }, function () {
        //取消
      })
    }
  }
},

注销路由/logout,后端对应的spring security中logout(),清楚用户信息

2) 第二部分的左侧权限菜单


3) 第二部分右侧的主体显示部分


  
    首页
    
  
  
  
    
  
  

四、文章管理模块分析

开源项目学习-V部落_第3张图片

1) 文章列表

当点击文章列表时,触发路由--/articleList,并且主体部分显示ArticleList.vue模板,

生命周期钩子函数mouted:路由/isAdmin,判断是否为超级管理员来决定是否显示文章列表

mounted: function () {
  var _this = this;
  getRequest("/isAdmin").then(resp=> {
    if (resp.status == 200) {
      _this.isAdmin = resp.data;
    }
  })
},
@RequestMapping("/isAdmin")
public Boolean isAdmin() {
    List authorities = Util.getCurrentUser().getAuthorities();
    for (GrantedAuthority authority : authorities) {
        if (authority.getAuthority().contains("超级管理员")) {
            return true;
        }
    }
    return false;
}

文章列表显示部分向子组件传递参数[state、showEdit、showDelete、showRestore、activeName]五个属性值


  
    
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
    
  

这其中又引入两个模板BlogTable和BlogCfg

BlogTable模板:

① 通用搜索组件:

搜索

② 大部分通用表头

全部文章模块没有操作部分          

博客配置模块:组件BlogCfg





  

③ 通用表尾

  • v-show="this.articles.length>0 && showDelete"  控制“批量删除”按钮是否存在

  • :disabled="this.selItems.length==0"    控制“批量删除”按钮是否可用

  • el-pagination     分页插件

④ 点击文章列表后,最初定位“已发表”通过以下来确定  :activeName绑定name属性


  


data() {
  return {
    activeName: 'all',
    isAdmin: false
  };
},

“已发表”数据获取

// 生命周期函数
mounted: function () {
  var _this = this;
  this.loading = true;
  // 调用加载数据
  this.loadBlogs(1, this.pageSize);
  // var _this = this;
  // window.bus.$on('blogTableReload', function () {
  //   _this.loading = true;
  //   _this.loadBlogs(_this.currentPage, _this.pageSize);
  // })
},


loadBlogs(page, count){
  var _this = this;
  var url = '';
  // 获取全部文章的数据
  if (this.state == -2) {
    url = "/admin/article/all" + "?page=" + page + "&count=" + count + "&keywords=" + this.keywords;
  } else {
  // 获取当前表头的数据
    url = "/article/all?state=" + this.state + "&page=" + page + "&count=" + count + "&keywords=" + this.keywords;
  }
  getRequest(url).then(resp=> {
    _this.loading = false;
    if (resp.status == 200) {
      _this.articles = resp.data.articles;
      _this.totalCount = resp.data.totalCount;
    } else {
      _this.$message({type: 'error', message: '数据加载失败!'});
    }
  }, resp=> {
    _this.loading = false;
    if (resp.response.status == 403) {
      _this.$message({type: 'error', message: resp.response.data});
    } else {
      _this.$message({type: 'error', message: '数据加载失败!'});
    }
  }).catch(resp=> {
    //压根没见到服务器
    _this.loading = false;
    _this.$message({type: 'error', message: '数据加载失败!'});
  })
},

当前端初始化(即全部文章)路由:/article/all?page=1&count=6&keywords=''

当前端点击"已发表":前端传递路由:/article/all?state=1&page=1&count=6&keywords=''

当前端点击"草稿箱":前端传递路由:/article/all?state=0&page=1&count=6&keywords=''

当前端点击"回收站":前端传递路由:/article/all?state=2&page=1&count=6&keywords=''

当前端点击"博客管理":前端传递路由:/article/all?state=-2&page=1&count=6&keywords=''

后端都执行以下controller:

@RequestMapping(value = "/all", method = RequestMethod.GET)
public Map getArticleByState(@RequestParam(value = "state", defaultValue = "-1") Integer state, @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "count", defaultValue = "6") Integer count,String keywords) {
    int totalCount = articleService.getArticleCountByState(state, Util.getCurrentUser().getId(),keywords);
    List
articles = articleService.getArticleByState(state, page, count,keywords); Map map = new HashMap<>(); map.put("totalCount", totalCount); map.put("articles", articles); return map; }

得到的数据articles和totalCount,以下实现数据的填充:具体的了解可以看el-table(这是element-ui的组件)

  • el-table中 :data="articles"接受数据(数据来自后端传给前端的articles的属性值)
  • 使用v-loading在接口为请求到数据之前,显示加载中,直到请求到数据后消失
  • @selection-change="handleSelectionChange":获取勾选后的index索引值--所在列的索引值


  
  • slot-scope="scope":通过scope slot可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据
  • @click="itemClick(scope.row)":传递的参数当前行,当点击改行,就会push这行的路由,所用的知识是vue-router动态路由,保存路由的顺序
  • {{ scope.row.titel }}:当前行的标题

  
  • {{ scope.row.editTime | formatDateTime}}:时间过滤器 Vue.filter()   将时间戳变为标准时间
  • 后端也有时间处理器,只不过是将时间变为时间戳

  • 直接通过prop属性获取当前行nickname属性值

  • 与nickname同理

操作栏中各个方法:

@click="handleEdit(scope.$index, scope.row)"   push动态路由:/editBlog  -----> 组件PostArticle

在加载PostArticle组件时,同时执行了生命周期函数   根据文章id查询出来并显示内容   

mounted: function () {
  this.getCategories();
  var from = this.$route.query.from;
  this.from = from;
  var _this = this;
  if (from != null && from != '' && from != undefined) {
    var id = this.$route.query.id;
    this.id = id;
    this.loading = true;
    getRequest("/article/" + id).then(resp=> {
      _this.loading = false;
      if (resp.status == 200) {
        _this.article = resp.data;
        var tags = resp.data.tags;
        _this.article.dynamicTags = []
        for (var i = 0; i < tags.length; i++) {
          _this.article.dynamicTags.push(tags[i].tagName)
        }
      } else {
        _this.$message({type: 'error', message: '页面加载失败!'})
      }
    }, resp=> {
      _this.loading = false;
      _this.$message({type: 'error', message: '页面加载失败!'})
    })
  }
},
@RequestMapping(value = "/{aid}", method = RequestMethod.GET)
public Article getArticleById(@PathVariable Long aid) {
    return articleService.getArticleById(aid);
}

编辑器插件:mavon-editor

同理:还原与删除操作也这样分析

  • handleRestore方法:即将改变文章的state

  • handleDelete方法:也是改变文章的state

@click="deleteMany"   批量删除也是改变多个文章state的操作

注意:需要修改

deleteMany(){
  var selItems = this.selItems;
  console.log(selItems)
  for (var i = 0; i < selItems.length; i++) {
    this.dustbinData.push(selItems[i].id)
    this.deleteToDustBin(selItems[i].state)  // 送入回收站操作应该在此处,这样才能实现批量删除操作
  }
},

上一页|当前页|下一页   具体看el-pagination组件

博客配置:邮箱的更新操作---不在详述

2) 发表文章

{
  path: '/postArticle',
  name: '发表文章',
  component: PostArticle,
  meta: {
    keepAlive: false
  }
}

发表文章组件显示,编辑器插件


  
    
    
    
  
  
  
  
    {{tag}}
  
  
  
  
  
  +Tag

最重要的是文章增加功能

saveBlog(state){
  if (!(isNotNullORBlank(this.article.title, this.article.mdContent, this.article.cid))) {
    this.$message({type: 'error', message: '数据不能为空!'});
    return;
  }
  var _this = this;
  _this.loading = true;
  postRequest("/article/", {
    id: _this.article.id,
    title: _this.article.title,
    mdContent: _this.article.mdContent,
    htmlContent: _this.$refs.md.d_render,
    cid: _this.article.cid,
    state: state,
    dynamicTags: _this.article.dynamicTags
  }).then(resp=> {
    _this.loading = false;
    if (resp.status == 200 && resp.data.status == 'success') {
      _this.article.id = resp.data.msg;
      _this.$message({type: 'success', message: state == 0 ? '保存成功!' : '发布成功!'});
//            if (_this.from != undefined) {
      window.bus.$emit('blogTableReload')
//            }
      if (state == 1) {
        _this.$router.replace({path: '/articleList'});
      }
    }
  }, resp=> {
    _this.loading = false;
    _this.$message({type: 'error', message: state == 0 ? '保存草稿失败!' : '博客发布失败!'});
  })
},

前端路由:/article/       传递的参数:

  • id--文章的id
  • title--标题
  • mdContent--文章截取内容   可理解为摘要
  • htmlContent--完整内容
  • cid--文章栏目
  • state--文章状态     表发和保存草稿决定
  • dynamicTags--动态标签

后端controller:

public int addNewArticle(Article article) {
    //处理文章摘要
    if (article.getSummary() == null || "".equals(article.getSummary())) {
        //直接截取
        String stripHtml = stripHtml(article.getHtmlContent());
        article.setSummary(stripHtml.substring(0, stripHtml.length() > 50 ? 50 : stripHtml.length()));
    }
    if (article.getId() == -1) {
        //添加操作
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        if (article.getState() == 1) {
            //设置发表日期
            article.setPublishDate(timestamp);
        }
        article.setEditTime(timestamp);
        //设置当前用户
        article.setUid(Util.getCurrentUser().getId());
        int i = articleMapper.addNewArticle(article);
        //打标签
        String[] dynamicTags = article.getDynamicTags();
        if (dynamicTags != null && dynamicTags.length > 0) {
            int tags = addTagsToArticle(dynamicTags, article.getId());
            if (tags == -1) {
                return tags;
            }
        }
        return i;
    } else {
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        if (article.getState() == 1) {
            //设置发表日期
            article.setPublishDate(timestamp);
        }
        //更新
        article.setEditTime(new Timestamp(System.currentTimeMillis()));
        int i = articleMapper.updateArticle(article);
        //修改标签
        String[] dynamicTags = article.getDynamicTags();
        if (dynamicTags != null && dynamicTags.length > 0) {
            int tags = addTagsToArticle(dynamicTags, article.getId());
            if (tags == -1) {
                return tags;
            }
        }
        return i;
    }
}

图片的添加删除操作不在详述

五、用户管理模块分析--只有一个子路由

用户卡片形式,很漂亮   只有展示用户,没有添加用户功能   可以自己实现

开源项目学习-V部落_第4张图片

六、栏目管理--不在赘述

七、数据chart    插件:vue-echarts

@RequestMapping("/dataStatistics")
public Map dataStatistics() {
    Map map = new HashMap<>();
    List categories = articleService.getCategories();
    List dataStatistics = articleService.getDataStatistics();
    map.put("categories", categories);
    map.put("ds", dataStatistics);
    return map;
}


/**
 * 获取最近七天的日期
 * @return
 */
public List getCategories() {
    return articleMapper.getCategories(Util.getCurrentUser().getId());
}

/**
 * 获取最近七天的数据
 * @return
 */
public List getDataStatistics() {
    return articleMapper.getDataStatistics(Util.getCurrentUser().getId());
}

e-charts很值得研究,以后的项目再去仔细研究    百度可视化神器

 

你可能感兴趣的:(前后端分离开源项目系列)