Spring Boot + Vue 开发部署全过程记录

文章目录

  • Spring Boot + Vue开发部署全过程记录
      • 前后端分离
        • 验证码
        • 登录与会话超时
        • All in one jar 与 Nodejs 静态服务器
      • 测试
    • 构建、部署
      • 前端构建
      • 后端构建
      • 整体构建
    • 问题及解决
      • 跨域以及 `axios` 请求带 `Cookies`
      • `@RestController`
      • 后期加依赖,pom 却不下载的问题解决
      • 阻止浏览器刷新
        • 阻止跳转
        • 阻止刷新
      • 前后端数据交互
      • 枚举值
        • 前后端一致
        • 枚举值的展示与前后端传递
      • 回到顶端
      • 刷新回到主页——H5 history API
        • hash 模式与 history 模式
  • 复盘与总结
    • 理想与现实
      • 跨域处理
        • 开发时的跨域处理
      • 会话保持
      • 会话超时模拟
        • V 1.0
        • V 2.0
        • V 3.0
      • all in one jar 部署
        • 前端 pom
        • 后端 pom
    • 再认识前后端分离
    • 感想
      • 庸人自扰
      • 负重前行
      • 代码不是玄学——你常说的很奇怪的问题,往往都不奇怪
      • 软件开发——深刻的参与这个世界。
  • Vue
    • 开发模式(development)和生产模式(production)
    • 浏览器兼容
    • Vue 基本操作
      • Vue 重点
        • 1. 不直接操作 DOM——MVVM
        • 2. 组件化
        • 3. 响应式
          • 响应式系统原理简述
        • 总结

Spring Boot + Vue开发部署全过程记录

前后端分离

前后端分离的开发模式不同于以往严重依赖后端的模式,前者使得前端更加“智能”,能做的事情更多,后端只需要作为一个能回答问题的服务即可。前后端分离模式一般是基于单页应用(SPA-Single Page Application)。单页应用在第一次访问服务器时,就将整个应用涉及到的“所有页面”都发往前端,后续和服务器的交互只是涉及数据的往来,不需要再传递页面。

前后端分离,分离了代码,却加强了前后端程序员之间的耦合,需要前后端程序员不断的交流和协商。

验证码

验证码的本质就是验证操作的用户是人还是机器,防止服务被机器大量的访问攻击,这一业务本身就不需要后端的数据支持,当前端有能力时,就应该放在前端做这件事。在前端还是蛮荒时代,验证码的生成还需要后端的支持,所以一般的做法是后端生成一个验证码字符串,存入 session ,再生成一张图片传到前端展示,用户输入验证码,后端进行验证对错。而现在前端的做法是使用 JavaScriptCanvas 在前端绘制验证码图片,使用的是客户机的算力,没有请求发到后端。

登录与会话超时

以登录为例,当浏览器拿着用户名和密码发往后端时,后端只需要返回登录成功与否的状态即可,而不用操心失败了需要重定向的问题。同样会话超时也不需要返回登录页,返回超时状态即可。前端也可以采用 Web Workers 设置任务,模拟检查会话是否超时。

All in one jar 与 Nodejs 静态服务器

All in one jar 在下文有叙述,或参考A Lovely Spring View: Spring Boot & Vue.js、spring-boot应用前后端分离工程实践。

Nodejs 静态服务器,也是前后端分离部署的一种方式,不涉及后端数据的相关页面、静态文件部署在 Nodejs 服务器上,后端服务器只需要提供数据。前后端开发分离、部署分离。

测试

不涉及后端数据的测试:前端起自己的服务。npm start 运行测试配置。

涉及后端数据的测试:一般是后端起一个服务,前端起一个服务,前端的开发环境配置需要指向当前后端的运行地址。

构建、部署

如何将前端工程集成在后端 Spring Boot 上呢,构建思路是建一个父工程,前端工程和后端工程分别作为这个父工程的两个子模块。工程结构大致如下:

parent: 
		fe:
				fe-pom.xml
		be:
				be-pom.xml
		parent-pom.xml

前端构建

前端虽然都是 js 代码,但也可以加上 POM 文件让 Maven 去执行,前端的 POM 文件需要做的事情是:在构建前下载工程的第三方依赖(npm install),以生产模式构建工程(npm build)。这一系列事情都可以交给 Maven 来做。需要下载 frontend-maven-plugin 这个插件,这个插件支持下载 nodenpm 以及运行这些命令,这对于构建前端工程就够用了。POM 文件大致如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0modelVersion>
  
  <parent>
    <groupId>com.jxgroupId>
    <artifactId>parentartifactId>
    <version>0.0.1-SNAPSHOTversion>
  parent>
  
  <artifactId>com-jx-feartifactId>
  <name>com-jx-fename>
  <description>前端工程description>
  
  <build>
        <plugins>
            <plugin>
                <groupId>com.github.eirslettgroupId>
                <artifactId>frontend-maven-pluginartifactId>
               <version>${frontend-maven-plugin.version}version>
                <executions>
                    
                    <execution>
                        <id>install node and npmid>
                        <goals>
                            <goal>install-node-and-npmgoal>
                        goals>
                        <configuration>
                            
                            <nodeVersion>v8.11.3nodeVersion>
                        configuration>
                    execution>
                    
                    <execution>
                        <id>npm registry set to taobaoid>
                        <goals>
                            <goal>npmgoal>
                        goals>
                        <configuration>
                            <arguments>config set registry http://registry.npm.taobao.org/arguments>
                        configuration>
                    execution>
                    
                    <execution>
                        <id>npm installid>
                        <goals>
                            <goal>npmgoal>
                        goals>
                        <phase>generate-resourcesphase>
                        <configuration>
                            <arguments>installarguments>
                        configuration>
                    execution>
                    
                    <execution>
                        <id>npm run buildid>
                        <goals>
                            <goal>npmgoal>
                        goals>
                        <configuration>
                            <arguments>run buildarguments>
                        configuration>
                    execution>
                executions>
            plugin>
        plugins>
    build>
project>

还支持设置 npm 下依赖的库地址,这里设置成了淘宝的库。

现在只要在前端工程目录下运行 maven clean install 就会自动将依赖下载到工程目录下(若是第一次运行还会下载 node 和 npm ,注:这里运行和编译项目使用的 node 和 npm 和项目运行时的机器上安装的不是相同的,这里安装的 node 会在前端工程 node 目录下),再将工程编译产生 target\dist 目录(可使用 webpack自定义)。自此,前端工程构建完毕。

后端构建

但要想将前段工程依附到后端工程上,还需要后端工程能找到前端工程的文件,那简单,将前端工程构建后的文件复制到后端工程目录下即可,新建 be-pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>com.jxgroupId>
        <artifactId>parentartifactId>
        <version>0.0.1-SNAPSHOTversion>
    parent>
	<artifactId>beartifactId>
	<name>bename>
	<description>bedescription>

	<dependencies>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>
	dependencies>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-resources-pluginartifactId>
				<executions>
					<execution>
						<id>copy Vue.js frontend contentid>
						<phase>generate-resourcesphase>
						<goals>
							<goal>copy-resourcesgoal>
						goals>
						<configuration>
							<outputDirectory>src/main/resources/publicoutputDirectory>
							<overwrite>trueoverwrite>
							<resources>
								<resource>
									<directory>${project.parent.basedir}/fe/target/distdirectory>
									<includes>
										<include>static/include>
										<include>index.htmlinclude>
									includes>
								resource>
							resources>
						configuration>
					execution>
				executions>
			plugin>
		plugins>
	build>
project>

这个 POM 文件的作用就是将前段工程下 ${project.parent.basedir}/fe/target/dist 的文件复制到后端工程的 src/main/resources/public 下,这样后端工程就有 index.html了,工程已启动,就能访问前端工程。

整体构建

先构建前端工程,后构建后端工程,顺序不能乱。将父工程整体构建也可保证此顺序。

问题及解决

跨域以及 axios 请求带 Cookies

axios 跨域处理以及带 cookies 的请求

@RestController

用于前后端分离的 Controller 都应该是数据相关的,而不是页面相关的。

后期加依赖,pom 却不下载的问题解决

背景:后期增加对 IE9 的支持,需要增加依赖 babel-polyfill ,但是 package.json 已经加上,本地构建没问题,放在服务器环境构建却总是不会主动下载。

node 是安装在前端工程目录下的,并没有全局安装,因此不能直接运行 nodenpm命令,响应的依赖安装地址也可能对应不上。

解决办法:

手动安装依赖,移动到 node_modules 下。具体操作如下:

说明:在项目目录下有 nodenode_modules 两个目录,前者内含 nodenpm 命令,后者是整个前端项目的依赖,作用类似 lib

# 执行 npm 命令全局安装 babel-polyfill,因为不能直接运行 node 和 npm 命令,所以只能这样,npm 命令实际上也是运行 npm-cli.js 这个文件
./node/node node/node_modules/npm/bin/npm-cli.js install babel-polyfill -g

# 查看 node 全局的安装的依赖,顺带可以看见路径。注意:当依赖很多时,谨慎直接运行这条命令,会有很多输出
./node/node node/node_modules/npm/bin/npm-cli.js list -g

# 基本是在工程目录下生成一个 lib 目录,新下的依赖就在里面。最后把这个目录删除。

# 复制进 node_modules 即可

阻止浏览器刷新

当用户在编辑态时突然刷新,或者进入其他页面,那正在编辑的页面有义务提醒用户,当前刷新会丢失所做修改。

阻止跳转

阻止跳转到其他页面,可以使用 Vue 的路由守卫BeforeRouteLeave,它可以在路由发生变化之前作出一些决策,并依次决定是否进入下一个路由。

// 发生修改之后离开页面时的提醒
beforeRouteLeave(to, from, next) {
    // 修复编辑中会话过期自动跳转到主页被拦截的问题
    if (this.isAnyDirty() && to.path != "/") {
        this.$confirm({
            title: "请注意",
            content: "离开将丢弃当前页面修改的数据,确定要离开?",
            onOk() {
                next(true);
            },
            onCancel() {
                next(false);
            }
        });
    } else {
        next(true);
    }
}

阻止刷新

阻止刷新依然没有什么好的解决办法,只能用浏览器自带的提示,不好看也不友好。

mounted() {
    let _this = this;
    // 基本信息页面如果有修改,阻止刷新
    window.onbeforeunload = function(e) {
        if (_this.$route.path == "/baseinfo" && _this.isAnyDirty()) {
            e = e || window.event;
            // 兼容IE8和Firefox 4之前的版本
            if (e) {
                e.returnValue = "关闭提示";
            }
            // Chrome, Safari, Firefox 4+, Opera 12+ , IE 9+
            return "关闭提示";
        } else {
            window.onbeforeunload = null;
        }
    };
}

前后端数据交互

这里的前后端数据交互是只后端返回的字段名称和前端对应映射的字段名称,这两者最好是一一对应的,这会省去很多麻烦。在项目伊始,这件事情,就是前后端应该一起商定的。一般会在确定接口详情的时候一起确定。

枚举值

枚举值有两个问题:

前后端一致

对于变化频繁的枚举值,不应该在前端定义,这样不方便更新。可以后端提供接口,动态获取,这样也能前后端保持一致,特别是中文名称的一致。不会出现类似 微信公众号公众号 这类名称的不一致。

枚举值的展示与前后端传递

一般枚举值,如 :

{
    "010":"成功",
    "020":"不成功",
    "030":"待处理"
}

界面展示的是中文,前后端传输的是对应编码。从后端得到编码,转换成中文名进行展示;数据回传时,需要根据中文名得到编码。这往往就需要这些枚举值的对象能进行双向转化。

以下是两种转换方式,根据编码找名称,时间复杂度为 O(1)

// 没匹配到就返回原编码
function filter(code,enums){
    return enums[code] || code
}

根据名称找编码,时间复杂度为 O(n)

function filterValue(name,enums){
    for(let i in enums){
        if(enums[i] == name){
            return i;
        }
    }
    // 没匹配到就返回原名称
    return name;
}

回到顶端

网上找的一个例子进行整合。

/* 右侧悬浮栏 */





刷新回到主页——H5 history API

刷新这个操作几乎等同于世界末日,Vue 实例和 Route 对象都被杀死重建,Vue 失去刷新前的路由,所以只能通过客户端缓存进行记忆。

const vm = new Vue({
  el: '#app',
  router,
  store: Store,
  components: { App },
  template: '',
  watch: {
    // 缓存路由的变化,如果 Vue 被刷新,可以回到刷新前的页面
    $route(to, from) {
        window.sessionStorage.setItem('DaNgjIanLuO', to.path)
    }
  }
})

// 每次刷新相当于重新运行 main.js 所以从 sessionStorage 里拿出路径进行重定向即可
let path = window.sessionStorage.getItem('DaNgjIanLuO')
path && router.replace(path)

在开发模式, Webpack 可以配置 historyApiFallback 用于 history API 模式下,将 URL 重写到 index.html,如果不进行重写,则会以当前 URL 向后端发送请求,一般情况下会得到 404 的响应。

生产模式下,将当前的路由作为 URL 直接请求后端,肯定也是得不到正常响应的,所以需要后端统一设置,如果没有匹配到正确的路由,就将 index.html 也就是 Vue 的宿主页面,返回。后端配置如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;

@SpringBootApplication
public class BeApplication {
	public static void main(String[] args) throws Exception {
		SpringApplication.run(BeApplication.class, args);
	}
	@Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() {
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html");
                container.addErrorPages(error404Page);
            }
        };
    }
}

hash 模式与 history 模式

HTML5 History 模式

这是 Vue Router 里的两种模式。默认是 hash 模式,特征很明显,在 URL 中会出现 # , # 后面发生变化,不会发起请求,只会触发 Vue 进行组件切换,而 history 模式下,URL 变化就会发起请求。无疑,生产模式下是不可能使用 hash 模式的。

复盘与总结

理想与现实

此节涉及上文讲的开发前设想和开发后的现实操作对比。

实际操作和当初预想差异较大的几个方面:

跨域处理

最初的解决办法是在每个接口上添加 @CrossOrigin 注解来支持跨域,这是网上检索来的应急办法,能用,但不知道为什么起作用。开发后期,随着对跨域问题的更多了解,改而采用在拦截器里统一添加跨域支持。当浏览器检测到请求发往的地址和当前环境不是同一个域时,会发起请求方法为 OPTIONS 的预检请求,目的是询问服务器支不支持来自这个域的请求,服务器用 Access-Control-Allow-Origin 响应头回应自己支持的请求域。

其实始终都有一个问题没有搞明白,就是为什么将前端文件作为后端的静态资源一起打包发布也会有跨域的问题,理论上来讲,此时前端资源和后端服务是在同一个域中的,为什么仍然有跨域问题呢?后经同事提醒,才发现,我每次发请求,都带了类似这样的前缀 http:127.0.0.1:8080,这是后端接口运行的地址和端口。后来明白了,如果前后端资源确实在同一个域中,但是前端资源请求时声明了要请求的域(就像这样 http://127.0.0.1:8080/api/getuser),浏览器不会详细检查当前域和请求的域是否确实为同一个,就会认为是一个跨域请求。当去掉这样的前缀,开发时仍然有跨域问题,在生产模式,也就是将前端资源作为后端资源的静态资源一起打包,就不存在跨域问题了。

开发时的跨域处理

虽然集成部署后没有跨域问题,但是在开发时,前端起的是 node 服务访问后端另一个服务,还是存在跨域问题的。现在使用的是 Webpack 的 proxyTable 解决的。

config.index.js

module.exports = {
    dev:{
        proxyTable:{
            '/api':{                            // 代理转发的地址前缀以 /api 开头,/ 表示所有请求
                target:'http://127.0.0.1:8080', // 将所有请求转发到
                changeOrigin:true, 
                pathRewrite:{                   // URL 重写
                    '^/api':''                  // 将请求的 /api 前缀去掉 
                }
            }
        }
    }
}

然后在 dev.env.js 中配置:

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  MER_PATH: JSON.stringify("/api") // 基础URL前缀
})

将所有请求前缀加上 /api ,然而生产模式应该将 MER_PATH 置空(为了保持一致就没有去除)。

如果没有使用 webpack 的 proxyTable 解决开发时的跨域问题,也可以采用上文提到的在后端统一支持跨域来解决。

会话保持

最初是使用 axios 的 withCredentials 配置携带 Cookies ,但记不太清是某个接口,不知为什么总是带不了 Cookies,以此导致总是提示未登录。当时为了能继续下去,采用在每个请求头携带 sessionId 的方式暂时处理,但在请求头中携带 sessionId 总是不安全的。

最终的解决办法是使用 Cookies 携带,也就是在 axios 请求配置中加上 axios.defaults.withCredentials = true,这样后端前置拦截器几乎不用做什么改动。那会话保持也没什么可说的了,和传统前端开发一样。

会话超时模拟

会话超时模拟经历了三个版本:三个版本的共同点是:1. 在登录成功之后,缓存过期的时,以秒计,以毫秒计。2. 模拟的会话超时时间比实际的超时时间早一到两分钟。

V 1.0

设置 axios 的前置请求拦截器,在每个请求发出去之前,查看当前会话是否过期。预期是如果过期了,就阻断请求,可是问题是,各种阻断的方式都试过了,就是不能取消不符合要求的请求,导致将过期了会话信息发往了后端,结果就是被后端重定向,极端情况是,页面一刷新,则页面初始化的好几个请求同时发出,遭到服务器的多次重定向。

这种方式是用户体验最好的一种方案,但是却取消不了请求,在用了一段时间后,只能放弃。

V 2.0

当第一版需要修改时,就不得不想其他办法去模拟,一种最早期在内心闪过了想法又被回忆起来。做法是在登录成功之后调用 setTimeout 函数,延迟到会话超时时执行跳转到登录也的操作,但是之前的担心是,登录成功之后就跳转到主页,那在登录也调用的函数,不也就跟着登录页的销毁被杀死了嘛。但是事实证明,并不会。还是会被执行的,我理解成调用时被注册了。这种方案简单好用,直到一些诡异的事情出现。

V 3.0

诡异的事情就是,测试人员的电脑上,超时不会主动重定向,而我自己的电脑上从没有重现过。这让我百思不得其解,我还特地在 IE 上测试了 setTimeout 函数,也能执行。

组件销毁是不会杀死setTimeout函数的执行,但是页面刷新会啊,经过重现,才知道,测试是会经常刷新页面的,但我自己并没有,所以重现不出来。直到原因就好办了,只需要在刷新之后,继续之前的计时就可以了。还是老办法,将超时时间缓存在 sessionStorage 里,以求躲过惨无人道屠杀式的刷新。

let countdownTime = Number(sessionStorage.getItem('cdt'))
// 能被整除
const minIntervalSecond = 20
let interval = null

// 当前存在倒计时,且倒计时大于 minIntervalSecond 秒
// 如果每次恰好在计时器一次计时结束时刷新页面,那么刷新的次数不能大于 6 次,再刷新,前端的模拟计时就会晚于后端的计时
// 每刷新一次,前端的时间就会慢 [0,minIntervalSecond] 秒
// 后端是实时的,真实的时间,而前端只是模拟,非实时。
// 为什么是六次?因为前端的模拟时间比后端实际时间短了 120 秒
export function setSessionTimeOut(vm) {
    // 2.
    interval = setInterval(e => {
        loginTime = Number(sessionStorage.getItem('cdt'))
        let formNow = new Date().valueOf() - loginTime
        console.log('formNow:::::::::::second', formNow / 1000)
        if (formNow >= intervalTime) {
            clearInterval(interval)
            vm.$store.commit('setSessionId', '')
            sessionStorage.removeItem('cdt')
            vm.$router.replace('/')
            vm.$message.info('会话过期,请重新登录!', 3)
        }
    }, minIntervalSecond * 1000)
    // 1. 
    interval = setInterval(e => {
        countdownTime = Number(sessionStorage.getItem('cdt'))
        let cd = countdownTime - minIntervalSecond
        if (cd > 0) {
            sessionStorage.setItem('cdt', cd)
        } else {
            clearInterval(interval)
            vm.$store.commit('setSessionId', '')
            sessionStorage.removeItem('cdt')
            vm.$router.replace('/')
            vm.$message.info('会话过期,请重新登录!', 3)
        }
    }, minIntervalSecond * 1000)
}

全局这么做:

// 会话超时模拟
const minIntervalSecond = 20
let countdownTime = Number(sessionStorage.getItem('cdt'))
// 当前存在倒计时,且倒计时大于 minIntervalSecond 秒
if (countdownTime && countdownTime > minIntervalSecond) {
  console.log('main cd .........')
  setSessionTimeOut(vm)
}

如果有刷新,则登陆后的计时被中断,但是刷新之后, main.js 里的计时器就会开始工作。如果没刷新,则 main.js里的计时器不会起作用。

all in one jar 部署

实际上就是将前端资源作为后端的静态资源一起打包部署。这种部署方式,不会有跨域的问题。最初的做法是通过一个父工程作为前后端工程的桥梁,便于后端工程复制文件时能找到前端工程的位置。如果使用 jenkins 这种构建工具来构建,则可以将前端 pom 做的事情交给 jenkins,或是自动化部署等一切可以在构建过程执行命令的工具。

现在,前后端工程里 pom.xml 里做的事情几乎没什么变化。变化点:

前端 pom

去除。

后端 pom

由于没有父工程做桥梁,现在后端工程发现前端工程是使用的相对路径,服务器上和本地的前后端工程两者的相对路径可能不同,本地构建需要更改,否则前端做的修改不会被复制到后端。

<execution>
    <id>copy-frontend-staticid>
    <phase>validatephase>
    <goals>
        <goal>copy-resourcesgoal>
    goals>
    <configuration>
        <includeEmptyDirs>trueincludeEmptyDirs>
        <outputDirectory>src/main/resources/resourcesoutputDirectory>
        <resources>
            <resource>
                <directory>../../${profiles.active}-web-mer-fe/chanpay-web-mer-fe/distdirectory>
                <filtering>truefiltering>
            resource>
        resources>
    configuration>
execution>

再认识前后端分离

参考:

Web 前后端分离的意义大吗?

谈谈前后端分离之后

浅谈前后端分离技术

如何评价淘宝 UED 的 Midway Framework 前后端分离?

springboot+vue的前后端分离与合并方案

同志仍需努力。

感想

庸人自扰

当你不够专业,做出来的事就很可能是“庸人行径”,容易给自己挖坑,跳下去,然后爬上来。也容易化简为繁,最后把自己搞晕。

只是感叹,现实是还是要摸着石头过河,跌跌撞撞成长。

负重前行

常常看见的一句话:

哪有什么岁月静好,只不过是有人替你负重前行。

用在软件开发领域也同样适用。成熟的框架、三方包……无不是在做着这样的事。以前端来举例:

  • 一个 npm start 命令为什么就可以编译代码、启动 node 服务,还可以根据当前是什么环境执行不同的命令?
  • 为什么服务一启动,就会打开浏览器,默认打开 index.html 这个文件?
  • 为什么 componentA 组件下的 index.vue 可以通过 import componentA 的方式直接导入?
  • Spring Boot 真好用。

岁月静好固然向往,但生活是残酷和痛苦的这个本质不会变。总有一天,我们也会变成负重前行的人,栽树的人,那便要明白脚下走的路和肩上承载的使命,从不是轻而易举和理所当然的。

代码不是玄学——你常说的很奇怪的问题,往往都不奇怪

代码不是玄学,不会出现灵异事件,常常出现的很诡异的事,往往都是错过漏过某个细节造成的。要相信电脑,它会忠诚的执行每一条你下达的命令,要是与期望不相符,那你需要考虑的是自己下达的命令是否有问题,而不是往玄乎的方向想,不断得纠结自己。认真的比对更改前后的差异,以及更改可能会波及的代码。

例如:SpringMVC 里的前置拦截器,是访问每个请求前都会执行的,这一点毋庸置疑,这是框架层面决定的,经过万千程序员验证的。但如果发现前置拦截器没有被执行,千万不要怀疑是不是电脑或者 IDE 出问题了,来重启一下。顺藤摸瓜,看看拦截器是不是被注册到 Spring 容器里了。思维不要被局限在当前的问题上。

例如:修改了代码,再重新构建应用,却发现修改没起作用,代码还是修改之前的。不奇怪,不奇怪,不奇怪。想想你修改的代码在构建时被取到了吗?很大原因就是更新没被应用到新一次的构建中,至于没被取到的原因,可能是内存里的修改没被回写到磁盘(不同编辑器之间),可能是多个工程之间有构建顺序,或者少构建了什么。

凡此种种,都是些人类粗心的小毛病,就是没有奇怪的事。

软件开发——深刻的参与这个世界。

参与规则的制定,了解事物的运行原理。

Vue

开发模式(development)和生产模式(production)

这是两种模式,还有一种 测试模式 testing ,是用于前端自动化测试的。此项目没有用到前端自动化测试。

如果线上有几个环境,如测试环境、线上环境、测试环境、开发环境……这些应该都是属于生产模式,而不是测试环境就是 testing , 开发环境就是 development。当有几个线上环境时, Vue 和 Webpack 的一些配置文件就需要进行改了,比如 webpack.base.conf.js 里的一段配置:

原先是这样的,根据当前是否是生产模式来决定输出的目录是哪一个。

module.exports = {
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath // 生产模式配置
      : config.dev.assetsPublicPath   // 开发模式配置
  }
}

当有几个生产环境时,就会给不同生产环境命不同的环境名,如: prodution_online,production_test。此时应该这样改:

publicPath: process.env.NODE_ENV === 'development'
      ? config.dev.assetsPublicPath // 生产模式配置
      : config.build.assetsPublicPath   // 开发模式配置

或者这样:

const my_env = process.env.NODE_ENV
const isProduction = my_env.indexOf('production') != -1
publicPath: isProduction
      ? config.dev.assetsPublicPath // 生产模式配置
      : config.build.assetsPublicPath   // 开发模式配置

浏览器兼容

Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有兼容 ECMAScript 5 的浏览器。

Vuex 不兼容 IE9 。可以使用 babel-polyfill 进行兼容。

Vue 基本操作

最初是想写一个 Vue 简明教程的,但是Vue 文档看来看去,没什么可以精简的,本身就已经很精简了。Vue 算是现在流行的三大 MVVM 框架中上手最简单、概念最清晰、文档看起来最舒服的一个了,所以这一节也没什么可说的,花个一两天看 Vue 的官方文档就可以了。(以下:上半部分按顺序看,下半部分挑着看)

Vue 官方教程

Vuex 官方指南

Vue Router 官方指南


Vue Cookbook

Vue 官方风格指南

Vue 官方 API

Vue Cli

Vue 重点

以下是个人认为 Vue 中比较重要的概念或者说认识,不会很深的涉及具体的知识点,但是有了这些概念和认识,学习任何前端框架的具体知识都会很快。虽说是使用 Vue 搭建前端界面,但是任务最重的还是落在了使用 js 写逻辑上,这和 Vue 无关。

1. 不直接操作 DOM——MVVM

操作 DOM 是页面开发不可避免的一项繁重工作, Jquery 从问题出发,简化了 DOM 操作,从而盛极一时。但是当页面变得复杂,纵使使用 Jquery,操作 DOM 依然是一项复杂的工作。

MVVM (Model,View,ViewModel)Model 负责保存数据,View 负责展示,ViewModel 负责将 Model 的数据变化反应到 View 显示出来,将 View 的变化同步到 Model。

对应到 Vue 来说,就是通过数据绑定来实现 ViewModel 的功能。如:



其中