Vue踩坑记(一):使用Vue-Cli3快速搭建项目

最近几天学习了Vue全家桶,对于我这个主后端的程序员来说,Vue确实相对于Angular更容易上手,各种插件和中文文档十分齐全,而且如果使用Vue-Cli脚手架的话真的可以说是达到“开箱即用”的效果~

有了Vue,就可以彻底告别以前jQuery那一套了,双向绑定简直不能太好用,整个前端开发变得流畅起来。所以各位想要走前端路线的程序员,或者是像我这样主要关心后端但自己开发项目想用一个方便的前端框架,那么我强烈推荐学习Vue。

官方的教程已经很清晰明了,在里面可以了解Vue的基本使用方法:

https://cn.vuejs.org/v2/guide/

所以我们这里就不抄文档了233,下面的内容偏向实战踩坑,不过注意这系列文章严格来说不是教程,只能说是我自己学习和使用时遇到问题的记录吧。

Vue-Cli3脚手架

虽然完全可以通过引入js文件到页面的方式来快速使用Vue的功能,但如果想要构建大型单页应用(SPA)的话,推荐使用官方提供的Vue-Cli工具来快速搭建项目。

SPA

什么是SPA?单页应用和传统多页面Web应用最大的区别就是页面跳转全部由前端控制,所以能够达到无刷新的流畅体验。

传统的Web项目,在页面跳转时,是由前端去请求后端地址,然后由后端返回要显示的页面;而在SPA项目里,公共资源会在第一次载入时加载一次,之后前端仅仅从后端获取必要的数据,然后局部刷新页面内容,而不需要重新载入整个页面。

使用Vue-Cli3搭建项目

要实现SPA需要前端框架的支持,对于Vue来说有Vue-Router这个项目来实现前端路由,这种开发方式下前端项目也需要相对工程化的结构,而Vue-Cli脚手架可以一键生成前端工程并完成配置,即便不懂webpack也可以开心使用,因为Vue-Cli都已经配置好了~

首先我们需要有nodeJs环境,然后就可以使用npm来安装Vue-Cli,这里我们选择全局安装,现在Vue-Cli的最新版本是3,这系列文章也都是以Vue-Cli3为基础来进行讲解的。

# 安装vue-cli
npm install -g @vue/cli
 
# 查看vue-cli版本看看是否安装成功
vue -V
# 3.2.1

安装完毕后,我们切到工作目录来新建项目:

# 新建项目
vue create my-app

执行之后,Vue-Cli会提示选择一个预设,刚刚安装完毕的话应该只有一个叫做default的预设(default上面两个是我自己加的),这个预设里是没有集成Vue-Router的,所以我们选择最后一个选项来自定义预设。

然后就列出了Vue-Cli可以自动集成的插件,按空格键选择需要的,比如前端路由Router、状态管理Vuex,最后按下回车。

如果有选择集成Router的话,这里会询问要不要使用history模式,关于history模式的优劣可以查看Vue-Router的官方文档,当然,如果以后有机会可能我也会讲讲233。

总只,一般来说我是推荐使用history模式的。

如果有选择集成Linter/Formatter的话,这里需要选择默认配置,根据自己的需求选择吧,我是选的ESLint + Prettier。

什么时候进行拼写检查。

最后,选择这些配置是分别放在独立的文件中还是一起挤在package.json里面。

是否保存预设,保存的话下次就可以在第一步的时候选择这个预设而不用上面一步步配置了。一切就绪后,Vue-Cli就开始自动工作了。略微等待它跑完,会是这个样子:

好了,现在我们就得到了一个Vue单页应用项目了!

目录结构

打开项目后的目录结构大概是这个样子的,十分清晰明了。main.js 便是程序的入口,.vue文件则是Vue组件的一种文件格式,需要webpack来进行编译,所幸这方面的配置Vue-Cli已经帮我们做好了~

运行和编译

运行和编译打包相当简单,运行以下命令即可:

# 运行
npm run serve
 
# 编译打包
npm run build

默认的端口是8080,运行后访问的效果就是这个样子啦~

配置文件

Vue-Cli提供了一个叫做vue.config.js的配置文件,不过这个文件在搭建项目时并不会被创建出来,所以需要自己手动在和package.json同级目录下新建这个文件。

可用配置项可以参考官方文档:https://cli.vuejs.org/zh/config/

比如,如果想要更改默认端口,就可以这么配置:

module.exports = {
    devServer: {
        port: 8181
    }
}

漂亮的MD风格UI框架:Vuetify

有非常多支持Vue的前端UI框架,这次我选用的是近年的一个后起之秀:Vuetify。

Vuetify实现了Google的Material Design设计规范,可以在PC和手机上获得不错的用户体验,并且还提供了Vue-Cli一键集成的插件:

# 加入vuetify组件
vue add vuetify

只要跑一下这个命令,vuetify便集成到项目中了,注意要在一个新项目里这么做,因为它会替换一些文件。

然后我们用npm run build方法运行一下项目看看效果:

关于Vuetify的各种特性,可以去看官方文档,有中文哦:

https://vuetifyjs.com/zh-Hans/

尾巴

这篇的内容就到这了,下次会讲讲正式开发里遇到的坑,虽然我也不知道自己什么时候会更新就是了233

Ubuntu安装OpenJDK 11

Java 11版本Oracle将计划对企业收费,Java 8版本也将于2019年1月停止提供免费支持。

不过Oracle提供了的官方的OpenJDK 11编译版本,这个版本和Oracle Java 11在功能上没有区别,并且由OpenJDK社区维护,所以可以考虑安装这个。

下载地址:https://jdk.java.net/11/

下面我们主要介绍如何在Ubuntu系统下安装和设置OpenJDK 11,测试环境是Ubuntu Server 16.04。

# 下载和解压
sudo wget https://download.java.net/java/GA/jdk11/13/GPL/openjdk-11.0.1_linux-x64_bin.tar.gz
sudo tar xvf openjdk-11.0.1_linux-x64_bin.tar.gz
 
# 设置环境变量,编辑~/.bashrc文件,加入下面内容
JAVA_HOME=/usr/java/jdk-11.0.1
JRE_HOME=$JAVA_HOME/jre
JAVA_BIN=$JAVA_HOME/bin
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
export JAVA_HOME JRE_HOME PATH CLASSPATH
 
# 让设置起效
source ~/.bashrc

# 证实设置已起效
java -version

如果一切顺利,会输出当前Java的版本:

openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)

用Certbot申请Let's Encrypt野卡证书

网上看了很多关于申请Let's Encrypt野卡(wildcard)证书的教程,但很多感觉各种复杂,在我服务器上也没能正常运作,最后还是去看了官方推荐的certbot工具的文档终于找到了相对简单可靠的申请方式,在这里分享一下。

测试环境是Ubuntu Server 16.04 x64。

安装Certbot

sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot python-certbot-nginx

安装certbot-dns-cloudflare插件

野卡(wildcard)证书需要进行dns-01挑战,certbot官方提供了主流DNS服务商的插件,但是我没有找到单独的下载地址,所以只能直接去certbot的源码里找了。

注意:可能需要python3版本。
git clone https://github.com/certbot/certbot.git
cd certbot/certbot-dns-cloudflare
python setup.py build
python setup.py install

获取API Key

登录CloudFlare

如图所示找到API Key

创建一个cloudflare.ini文件用于配置API Key信息。

# Cloudflare API credentials used by Certbot
dns_cloudflare_email = cloudflare@example.com
dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567

上面的邮箱和API Key要替换成自己的。

申请证书

certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini -d *.example.com

--dns-cloudflare-credentials后面是前文创建的cloudflare.ini的路径,-d后面就是要申请证书的域名,申请野卡证书记得在域名前加*号。

如果顺利申请成功的话,会看到下面输出信息:

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/luxnk.xyz-0001/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/luxnk.xyz-0001/privkey.pem
   Your cert will expire on 2018-10-14. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
- If you like Certbot, please consider supporting our work by:
 
   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

这里面的两个文件路径就是证书的路径了,之后将它配置到相应Web服务器中就可以了~

证书的有效期是90天,到期后需要续签证书,续签使用下面的命令即可:

certbot renew

也可以通过创建crontab任务实现自动续签证书。

crontab -e
 
# 打开crontab文件后加入此行
0 3 */7 * * /bin/certbot renew --renew-hook "/etc/init.d/nginx reload"
 
# 重启cron服务
service cron restart

另外,通过下面指令可以查看当前用certbot申请的证书以及剩余有效时间:

certbot certificates

可能遇见的问题

在执行申请证书的命令时,certbot可能会出现警告:

Unsafe permissions on credentials configuration file: ~/certbot/cloudflare.ini

这是因为保存了API Key的cloudflare.ini文件访问权限不安全,将该文件权限设置为600就不会再出现这个警告。

chmod 600 cloudflare.ini

参考

JavaWeb环境下Shiro动态权限管理的一次实践

唉,好久没更新博客了,原因一个是懒,一个是忙。

懒癌这种事的话就……不提了~最近忙的当然也是工作上项目相关的事情,主要在研究折腾Shiro权限管理框架这块,我是打算弄出完全动态权限管理的效果,然而Google上的结果全都是针对Spring框架集成的,在我们公司这个没有用什么框架的破烂项目上当然没法套用!

无奈之下只能寄希望于翻阅API文档和代码,最终还是天无绝人之路让我走出一条大道~所以这篇文章就是记录一下这个思路以及实现,虽然因为个人实力原因很多东西也不是太懂,可能说的不是很清晰明了,但这篇文章是99%的个人原创哦!

表的设计

太复杂的设计我这样的萌新也不懂,这次就用相对较简单的结构,仅仅用来管理URL地址访问的权限, 一个用户可以有多个角色,一个角色拥有多个权限,然后每个URL对应一个指定的权限,当系统要知道这个用户能不能访问指定URL时,只需要查出该用户所有的权限然后丢给Shiro框架即可。

总共六个表,用户表、角色表、权限表、用户角色关系表、角色权限关系表以及URL规则表,这里表弄的比较简单,真正开发的时候还是要根据需求来设计表结构。

准备工作

这里的步骤我不会详细说明,因为这篇文章主要集中在动态权限读取的实践上,不会的同学可以先去Google找找Shiro基础教程,推荐开涛大佬的《跟我学Shiro》

首先当然是将依赖的jar包丢进项目。

在src根目录下创建shiro.ini配置文件

[main]



authc.loginUrl=/my/index.jsp
perms.unauthorizedUrl=unauthorized.jsp



#声明一个自定义Realm

userRealm=com.fivestar.my.shiro.realm.userRealm



#指定securityManager的realms实现

#securityManager会根据指定的顺序进行认证,若不显示配置此行则会按照声明的顺序进行认证

securityManager.realms=$userRealm

要通过Shiro实现动态权限管理,需要操作Shiro的FilterChainResolver对象,最终Shiro框架是从这个对象里获取权限规则(URL和对应要经过的过滤器)信息的,这里说一下和思路(不使用Spring套件的纯J2E项目)。

以下代码皆来自母婴项目。

在web.xml下加入下面配置:

    <listener>
        <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
    </listener>
    <filter>
        <filter-name>iniShiroFilter</filter-name>
        <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
        <init-param>
            <param-name>configPath</param-name>
            <param-value>classpath:shiro.ini</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>iniShiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <context-param>
        <param-name>shiroEnvironmentClass</param-name>
        <param-value>com.fivestar.my.shiro.env.MyIniWebEnvironment</param-value>
    </context-param>

WebEnvironment相当于Shiro框架的默认设置,项目启动时,EnvironmentLoaderListener监听器会读取配置文件并初始化WebEnvironment对象。

WebEnvironment的默认实现类是IniWebEnviroment,这里我们将自定义的MyIniWebEnvironment类(继承了默认的实现类IniWebEnvironment)设置为Shiro框架要初始化的WebEnvironment对象。

自定义WebEnvironment

继承了IniWebEnviroment类后,我们只用重写createFilterChainResolver方法,这个方法便是WebEnviroment初始化时创建FilterChainResolver对象的地方。

IniWebEnviroment中该方法最终的结果是从ini配置文件中读取[urls]部分的过滤器规则并依此创建FilterChainResolver对象,重写后我们增加了从数据库总读取url规则到FilterChainResolver中的代码。(Shiro框架里创建FilterChainResolver的过程运用了精妙的工厂设计模式,值得研究学习)

/**
* 自定义Shiro环境
* 用于应用启动时从数据库载入规则列表到FilterChain规则链
*/
public class MyIniWebEnvironment extends IniWebEnvironment {


    @Override
    protected FilterChainResolver createFilterChainResolver() {
        
        // 调用父类方法获得默认的FilterChainResolver对象(此时已经加载好ini文件中的规则)
        PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) super.createFilterChainResolver();


        // 获取FilterChainManager对象
        DefaultFilterChainManager filterChainManager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();


        // 这部分是从数据库中读取权限规则信息
        Map<String, String> params = new HashMap<String, String>();
        params.put("enable", "1");
        Ls rules = new permissionDao().getPermissionRuleList(params, 0, 0);
        
        // 循环读取到的权限规则信息并用FilterChainManager提供的方法添加规则
        for (Rs rule : rules.getItems()) {
            String url = rule.get("url");
            String alias = rule.get("alias");
            if (!url.isEmpty() && !alias.isEmpty()) {
                filterChainManager.addToChain(url, "perms", alias);
            }
        }


        // 将操作完毕的FilterChainManager对象更新到FilterChainResolver中
        filterChainResolver.setFilterChainManager(filterChainManager);


        return filterChainResolver;
    }
}

实现动态读取权限规则

完成上面代码之后实际上我们的系统权限规则已经是从数据库中获取了,但上面的代码只会在系统运行时跑一次,那能不能在不重启项目的情况下更新规则信息呢?

当然是可以的,从WebEnvironment对象中可以获取到FilterChainResolver对象,而在项目中想要获取WebEnviroment对象的话,只需要使用WebUtils.getWebEnvironment(ServletContext)方法即可!

之后我们就能直接操作FilterChainResolver对象了。

下面是一些操作方法,但这里实现的并不是很完善,有待改进。

/**
* Shiro框架FilterChain规则链管理
* @author Luxnk
*/
public class ShiroFilterChainManager {


    /**
    * 初始化Shiro权限管理的FilterChain规则链
    */
    public void initFilterChains() {


        // 获取WebEnvironment对象
        MyIniWebEnvironment env = (MyIniWebEnvironment) WebUtils.getWebEnvironment(App.get().getRequest().getServletContext());


        // 线程同步,同一时间只能有一个线程访问WebEnvironment
        synchronized (env) {


            // 获取FilterChianResolver对象
            PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) env.getFilterChainResolver();


            // 获取FilterChainManager对象
            DefaultFilterChainManager filterChainManager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();


            // 获取过滤器Map列表
            Map<String, Filter> defaultFilters = new HashMap<String, Filter>(filterChainManager.getFilters());


            // 获取默认FilterChain
            Map<String, NamedFilterList> defaultFilterChains = new HashMap<String, NamedFilterList>(filterChainManager.getFilterChains());


            // 删除以前老的FilterChain并注册默认的
            filterChainManager.getFilterChains().clear();
            if (defaultFilterChains != null) {
                filterChainManager.getFilterChains().putAll(defaultFilterChains);
            }


            // 从数据库中获取权限规则并注册到FilterChain中
            Map<String, String> params = new HashMap<String, String>();
            params.put("enable", "1");
            Ls rules = new permissionDao().getPermissionRuleList(params, 0, 0);
            for (Rs rule : rules.getItems()) {
                String url = rule.get("url");
                String alias = rule.get("alias");
                if (!url.isEmpty() && !alias.isEmpty()) {
                    filterChainManager.addToChain(url, "perms", alias);
                }
            }


            filterChainResolver.setFilterChainManager(filterChainManager);
            env.setFilterChainResolver(filterChainResolver);
        }


    }


    /**
    * 添加规则到FilterChain规则链
    */
    public void addToFilterChain(String url, String alias) {


        MyIniWebEnvironment env = (MyIniWebEnvironment) WebUtils.getWebEnvironment(App.get().getRequest().getServletContext());


        synchronized (env) {


            PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) env.getFilterChainResolver();


            DefaultFilterChainManager filterChainManager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();


            // 获取默认FilterChain
            Map<String, NamedFilterList> defaultFilterChains = new HashMap<String, NamedFilterList>(filterChainManager.getFilterChains());


            // 删除以前老的FilterChain并注册默认的
            filterChainManager.getFilterChains().clear();
            if (defaultFilterChains != null) {
                filterChainManager.getFilterChains().putAll(defaultFilterChains);
            }


            // 添加规则
            filterChainManager.addToChain(url, "perms", alias);


            filterChainResolver.setFilterChainManager(filterChainManager);
            env.setFilterChainResolver(filterChainResolver);
        }
    }


    /**
    * 从FilterChain规则链中移除规则
    */
    public void removeToFilterChain(String url) {


        MyIniWebEnvironment env = (MyIniWebEnvironment) WebUtils.getWebEnvironment(App.get().getRequest().getServletContext());


        synchronized (env) {


            PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) env.getFilterChainResolver();


            DefaultFilterChainManager filterChainManager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();


            // 获取默认FilterChain
            Map<String, NamedFilterList> defaultFilterChains = new HashMap<String, NamedFilterList>(filterChainManager.getFilterChains());


            // 删除以前老的FilterChain并注册默认的
            filterChainManager.getFilterChains().clear();
            if (defaultFilterChains != null) {


                // 如果有则删除规则
                if (defaultFilterChains.containsKey(url)) {
                    defaultFilterChains.remove(url);
                }
                filterChainManager.getFilterChains().putAll(defaultFilterChains);
            }


            filterChainResolver.setFilterChainManager(filterChainManager);
            env.setFilterChainResolver(filterChainResolver);
        }
    }


}

(a ==1 && a== 2 && a==3)结果是否为true

最近在查资料的时候发现了一个有趣的帖子:

https://stackoverflow.com/questions/48270127/can-a-1-a-2-a-3-ever-evaluate-to-true

内容是帖主在一个大型IT公司面试时遇到的奇怪面试题,题目是这样的:

在JavaScript中,(a ==1 && a== 2 && a==3)可能被判断为 true 吗?

贴主当时回答了“不可能”,然后面试就没有然后了……贴主表示两个星期来自己都没有弄明白这个问题的解法,于是才来发帖求助。

…………

……

恩,对于这个问题,我这类渣渣看了之后也是一脸懵逼的。a作为一个变量,怎么可能同时等于1又等于2还等于3呢……所以我很快就放弃了思考直接往下看大佬们的解答。

最高票答案

const a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}

if(a == 1 && a == 2 && a == 3) {
  console.log('Hello World!');
}

是不是有点懵,我也一样,让我们慢慢来看这个代码……

我们的思路是,如何让a在用==运算符和其他数值进行比较时,那一瞬间a的值和右边的数值一样。

所以我们首先要明白JavaScript中==运算符的机制,使用==进行比较时,若两边的参数类型不一致,就会尝试将左边的参数类型转换为与右边相同。而在这段代码中,a是一个对象,右边与a进行比较的参数则是数字,于是会首先尝试调用valueOf方法,如果失败,则会继续尝试调用toString方法。

知道了这个原理就可以来搞事情了,就像这段代码里一样,直接重写了a对象的toString方法,让它返回a对象的i属性,之后i属性自增1。

于是输出结果当然就是:

Hello World!

看完后我只能说:“还有这种操作!”

另外也可以直接重写valueOf方法来实现,不过这种操作属于奇技淫巧,纯粹用于解答这个面试问题,拓宽思路,正常开发时还是最好不要这样重写valueOf……

还有这种操作?!

等等,这个帖子下面还有更多其他解答,让我们来看看……

var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if(aᅠ==1 && a== 2 &&ᅠa==3) {
    console.log("Why hello there!")
}

这是什么操作?!没错,如果你仔细观察原题目,会发现语句里面有两个奇怪的空格间距。

那真的是空格吗?如果是空格的话,编译时会忽略掉,这三个仍然是同一个变量,最后的表达式的结果当然也是false。

不过,如果你像给出这个答案的答主一样博闻广见,也许会想到这个答案,这个间距其实是半宽度韩文,一个Unicode编码中的空格字符,但是ECMAScript并不将其解释为空格,所以aᅠaᅠa其实是不同的对象……

那结果当然就是true啦~

还有这种操作???!

var a = 1;
var a = 2;
var а = 3;
if(a == 1 && a == 2 && а == 3) {
    console.log("Why hello there!")
}

恩,这段代码看上去似乎是不可能在控制台打印出东西的……因为这次没有什么奇怪的间距了……

等等,中间那个a好像有点奇怪?好吧,其实这三个a真不是同一个字符。

  • a——正常的拉丁字母小写a
  • ——全角下的拉丁字母小写a
  • а——西里尔字母的小写a

恩……所以这仨也不是同一个变量,if判断的最后结果也就是true。

还有这种操作???!!!

var a = 9;

if‌(a==1 && a== 2 && a==3)
{
    console.log("Yes, it is possible!:sunglasses:");
}

function if‌(){return true;}

等等!怎么可能可以重写if,关键词不是不能重写吗,我怕是学了假的JavaScript。

天真,看了上面的几个答案后难道还认为这个if真的就是if吗?没错……这其实是一个自定义的函数,名字是if‌,虽然看起来好像和关键词长一个样了,但实际上……这个if后面有一个隐藏的字符,这是Unicode编码中的零宽度字符(U+200C,维基参考零宽不连字),所以这样就不跟关键词的if冲突啦。

然后这个自定义函数不论三七二十一直接返回个true,问题就解决了~

结尾

其实后面这种障眼法多少有些抬杠的感觉,但是很有趣嘛就分享出来了2333,这个帖子还有一些有趣和高大上到我看不懂的答案,可以自己去看看,不失为写代码之间的调味剂。