从零开始搭建前后端独立Web App环境Part0:讲在前面

Written by C.H.

长文不看

后端使用Java语言进行编码,利用Maven进行工程管理,使用Spring-Boot启动服务。
前端使用AngularJS2框架,使用Node.JS启动服务。
主要内容为搭建环境并运行Hello World。

一点前提

本文不包含架构,不包含优化,甚至没有经过设计,只是一个“能做”什么的大概文章,如果您拔冗阅览之后觉得有用,可以继续关注后面的相关文章。

为什么要前后端独立?

不需要做过多的介绍,相信需要做这一种部署方式的同学都是有这样的需求或者已经了解过这样部署的好处。

  • 优势:

    • 根据负载自行调整前后端节点数
    • 版本自行控制
    • 减少部署依赖
    • etc...
  • 劣势:

    • 跨域访问,需要考虑CORS以及CSRF
    • 前后端需要各自考虑外网区域
    • 更多的防火墙配置
    • etc...

为什么要前后端独立?

不需要做过多的介绍,相信需要做这一种部署方式的同学都是有这样的需求或者已经了解过这样部署的好处。

  • 优势:

    • 根据负载自行调整前后端节点数
    • 版本自行控制
    • 减少部署依赖
    • etc...
  • 劣势:

    • 跨域访问,需要考虑CORS以及CSRF
    • 前后端需要各自考虑外网区域
    • 更多的防火墙配置
    • etc...

如何前后端独立?

同样不需要做过多的介绍,常见的方法是前端使用Ajax访问后端的接口,后端提供RESTful接口访问。

环境、IDE介绍

笔者在Linux环境下进行开发,本文以及后续文章若无特殊说明,均为Ubuntu(64位,14.04-LTS),若有需要,请自行查阅其他资料。

  • Oracle JDK 1.8 -- 可以用Open JDK替代。
  • Node.JS 6.0+ -- 笔者用的是v6.7.0。
  • npm 3.0+ -- 笔者用的是3.10.3。
  • JetBrains IntelliJ IDEA 2016 Ultimate -- 非免费,条件允许的情况下请支持正版,Community版本无法对 typescript / javascript 完美支持。
  • Chrome -- 最新版是能支持到 typescript 的debug。

安装过程不再赘述,确保在终端内能够直接执行java和npm。

Hello World

1. 新建Java工程

在IDEA中新建Maven工程,自行填写GroupId、ArtifactId等信息。
建好的工程应该具有这样的文件结构:

project
|- .idea                    --- IDEA的工程配置文件夹,不熟勿动
|- src
|---|- main
|---|---|- java             --- IDEA下应显示蓝色,maven标准工程的java源文件都放在这个目录下
|---|---|- resource         --- IDEA下应带有资源标记,这下面的所有文件最终都会被打包到jar里面
|---|- test
|---|---|- java             --- IDEA下应显示为绿色,maven标准工程测试相关的java源文件都放在这个目录下
|- ****.iml                 --- IDEA的模块配置,不熟勿动
|- pom.xml                  --- maven的配置文件

2. 配置Maven

  • parent

继承自spring-boot-starter-parent,大多数依赖包使用官方配置的,避免出现某些意外。


    org.springframework.boot
    spring-boot-starter-parent
    1.4.1.RELEASE

本例中使用的spring-boot是1.4.1版本,可根据具体情况使用其他版本,1.3.x以上(1.4.0除外,官网遗漏了某个feature)。

  • properties

设置一些变量


    1.8

目前只需要设置JDK的版本号。

  • dependencies

设置依赖包


    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-devtools
        true
    
    

    
    
        com.google.code.gson
        gson
        test
    
    
        javax.servlet
        javax.servlet-api
    
    
        com.fasterxml.jackson.core
        jackson-annotations
    
    
        com.fasterxml.jackson.core
        jackson-databind
    
    
        com.fasterxml.jackson.module
        jackson-module-jaxb-annotations
    
    

以上依赖包均不用设置版本号,都继承自spring-boot-starter-parent。
简单说明:

spring-boot-starter-web 提供web访问框架,类似spring mvc,默认8080端口
spring-boot-devtools spring-boot的开发工具套件,先配置在这里
gson 谷歌提供的json解析,spring-boot继承jackson时的依赖
servlet-api 提供HttpServletRequest、HttpServletResponse等接口
jackson json解析,用于自动解析web接口的参数和提供json字符串的返回值
  • build

主要设置一些插件和编译属性


    ${project.name}
    
        
            org.apache.maven.plugins
            maven-compiler-plugin
            
                ${java.version}
                ${java.version}
                utf-8
                true
                ${env.JAVA_HOME}/bin/javac
            
        
        
            org.springframework.boot
            spring-boot-maven-plugin
        
    

插件简单说明:

maven-compiler-plugin 编译插件,设置编译过程中的一些参数
spring-boot-maven-plugin spring-boot在maven中的插件,用于命令行执行mvn的时候提供goal

3. Web Application 入口

新建Application类,所在包作为root,如:org.calvados.ansp.demo.Application

  • Application类增加注解:@SpringBootApplication
@SpringBootApplication
public class Application {

}

说明:

  1. @SpringBootApplication注解用于标注Spring-Boot入口,等价于旧版本中同时使用@Configuration@EnableAutoConfiguration@ComponentScan三个注解。
  2. @Configuration注解指该类为配置类,其中可提供相关的配置,更进一步用法请自行查阅。
  3. @EnableAutoConfiguration注解指明自动进行默认配置,更进一步用法请自行查阅。
  4. @ComponentScan注解指明让Spring去扫描指定目录下的相关注解,更进一步用法请自行查阅。
  • 增加main方法
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
}

4. HelloWorld RESTful接口

新建HelloWorldController类,如:org.calvados.ansp.demo.controller.HelloWorldController

  • HelloWorldController类增加注解:@RestController
@RestController
public class HelloWorldController {
    
}

说明:

  1. @RestController注解指明该类为RESTful风格的Controller类,等价于同时使用@Controller注解标注该类以及@ResponseBody注解标注该类下的所有方法
  2. @Controller注解标注该类为Spring MVC中的Controller,更进一步用法请自行查阅。
  3. @ResponseBody注解标注该方法的返回值作为http response的body部分直接返回给调用方,而不是ModelAndView结构,更进一步用法请自行查阅。
  • 增加hello方法
@GetMapping("/hello")
public String hello(@RequestParam(required = false)String name) {
    if (name != null) {
        return "Hello " + name + "!";
    }
    return "Hello World!";
}

说明:

  1. @GetMapping注解等价于@RequestMapping(method = RequestMethod.GET)@RequestMapping注解的用法请自行查阅。
  2. @RequestParam注解指明该参数需要由http request传递,更进一步用法请自行查阅。

5. 启动后端测试

在IDEA中Maven Projects窗口中(若没有该窗口,请在View -> Tool Window -> Maven Projects打开窗口),执行spring-boot:run这个goal(Plugins -> spring-boot -> spring-boot:run),也可以在命令行中执行mvn spring-boot:run。

待输出以下文字内容时,即表明启动完毕且没有报错。

Started Application in xxxx seconds (JVM running for xxxx)

在chrome中访问 http://localhost:8080/hello ,应该出现hello world字样,如下:

Hello World!

访问 http://localhost:8080/hello?name=123 ,应该出现hello 123字样,如下:

Hello 123!

至此,server端已能正常启动,暂时先将后端放在一边。

6. 配置npm

本例中使用Node.JS作为前端的web容器,同时也作为typescript的编译器,最终所有的ts文件会编译成js文件和对应的map文件。最终部署可以按照实际情况自行部署前端。

按道理,web前端应当新起一个工程,本例中暂时与后端工程放在一起。
在main文件夹同级,新建文件夹:front-end(文件夹名不重要,放置在何处也不重要,可自行决定,但是需要注意,不要打包打入jar包中)。
在frong-end文件夹中新建package.json文件,内容如下:

{
    "name": "helloworld",
    "version": "1.0.0",
    "scripts": {
        "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
        "lite": "lite-server -c lite-config/bs-config.json",
        "postinstall": "typings install",
        "tsc": "tsc",
        "tsc:w": "tsc -w",
        "typings": "typings"
    },
    "license": "ISC",
    "dependencies": {
        "@angular/common": "2.0.0",
        "@angular/compiler": "2.0.0",
        "@angular/core": "2.0.0",
        "@angular/forms": "2.0.0",
        "@angular/http": "2.0.0",
        "@angular/platform-browser": "2.0.0",
        "@angular/platform-browser-dynamic": "2.0.0",
        "@angular/router": "3.0.0",
        "@angular/upgrade": "2.0.0",
        
        "core-js": "^2.4.1",
        "reflect-metadata": "^0.1.3",
        "rxjs": "5.0.0-beta.12",
        "systemjs": "0.19.27",
        "zone.js": "^0.6.23",
        
        "angular2-in-memory-web-api": "0.0.20",
        "bootstrap": "^3.3.6"
    },
    "devDependencies": {
        "concurrently": "^2.2.0",
        "lite-server": "^2.2.2",
        "typescript": "^2.0.2",
        "typings":"^1.3.2"
    }
}

简单说明:

scripts中定义的start包括编译ts文件,启动lite-server作为web容器;tsc:w为持续检测ts文件变化,实时编译
其中的dependencies主要为AngularJS2的依赖,请根据实际情况自行选择版本

新建tsconfig.json文件,内容如下:

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": false
    }
}

简单说明:

该文件为ts编译的配置,可根据实际情况自行配置

新建typings.json文件,内容如下:

{
    "globalDependencies": {
        "core-js": "registry:dt/core-js#0.0.0+20160725163759",
        "jasmine": "registry:dt/jasmine#2.2.0+20160621224255",
        "node": "registry:dt/node#6.0.0+20160909174046"
    }
}

简单说明:

该文件为typings的必要依赖,可根据实际情况自行配置

在终端中进入到front-end文件夹,执行如下命令:

$ npm install

控制台会打印出详细信息,其中应该会出现大量WARN,全部忽略。但是请注意,如果出现ERROR,请根据具体情况逐个修复。

至此,文件结构如下:

front-end
|- node_modules                 --- Node.JS下载的源码包,含编译好的js文件
|- typings                      --- typings相关文件,编译ts文件的时候必要
|- package.json
|- tsconfig.json
|- typings.json

若缺少typings文件夹,可执行npm run postinstall继续下载安装。
$ npm run postinstall

7. Web前端

在front-end文件夹下,新建systemjs.config.js文件,内容如下:

(function (global) {
    System.config({
        paths: {
            // paths serve as alias
            'npm:': 'node_modules/'
        },
        // map tells the System loader where to look for things
        map: {
            // our app is within the app folder
            app: 'app',
            // angular bundles
            '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
            '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
            '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
            '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
            '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
            '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
            '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
            '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
            // other libraries
            'rxjs':                       'npm:rxjs',
            'angular2-in-memory-web-api': 'npm:angular2-in-memory-web-api'
        },
        // packages tells the System loader how to load when no filename and/or no extension
        packages: {
            app: {
                main: './main.js',
                defaultExtension: 'js'
            },
            rxjs: {
                defaultExtension: 'js'
            },
            'angular2-in-memory-web-api': {
                main: './index.js',
                defaultExtension: 'js'
            }
        }
    });
})(this);

简单说明:

该文件是AngularJS2的入口文件,定义了web文件夹,文件夹别名等。

system.config.js文件中,定义了web文件是放置在app文件夹下,因此在front-end文件夹下新建app文件夹。
然后新建main.ts文件,内容如下:

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

import {AppModule} from './app.module';

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

此时,IDEA应该会提示app.module.ts文件不存在,新建app.module.ts文件,内容如下:

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {HttpModule} from '@angular/http';

@NgModule({
    imports: [
        BrowserModule,
        HttpModule
    ],
    declarations: [],
    providers: [],
    bootstrap: []
})

export class AppModule {
}

此时,main.ts文件中提示的app.module.ts文件不存在的错误已经消除。但目前利用AngularJS2的bootstrap启动模块还未配置,现在我们来做主模块,app.component
app文件夹下,新建component文件夹,在component文件夹下新建app.component.ts文件,内容如下:

import {Component, OnInit} from '@angular/core';

@Component({
    moduleId: module.id,
    selector: 'my-app',
    template: `
        

Hello {{name}}!

` }) export class AppComponent implements OnInit { name: string; constructor() { } ngOnInit() { this.name = 'Test'; } }

简单说明:

这是一个很简单的模块,有一个成员name,name需要从后台传递,暂时我们先用很随意的方式来处理。

然后将AppComponent添加到AppModule中,修改app.module.ts文件:

import part
import {AppComponent} from './component/app.component';
NgModule part
@NgModule({
...
    declarations: [
        AppComponent
    ],
...
    bootstrap: [
        AppComponent
    ]
})

此时最基本的模块已经定义完毕,下面做页面展示。
在front-end文件夹下新增index.html作为实际访问的页面,内容如下:




    
    Hello World!
    
    

    
    
    
    

    
    


Loading...


页面只进行了一个操作,即载入相关的js。
启动Node lite-server,可以通过IDEA中的npm view启动,也可以通过命令行启动。

$ npm start

此时会打开浏览器,自动访问 http://localhost:3000/ ,页面会出现

标签样式的Hello Test!字样。

8. 前后端交互

本例中,后端通过向前端传递一个name,前端接收到之后显示在web页面上。

修改HelloWorldControllerhello方法,如下:

@GetMapping("/hello")
public String hello(@RequestParam(required = false, defaultValue = "World")String name) {
    return name;
}

给http request参数name增加默认值"World",并直接返回。

app文件夹下增加service文件夹,再新增hello.world.service.ts文件,内容如下:

import {Injectable} from '@angular/core';
import {Http} from '@angular/http';

import 'rxjs/add/operator/toPromise';

@Injectable()
export class HelloWorldService {

    constructor(private http: Http) {
    }

    hello(name: string): Promise {
        return this.http.get(`//localhost:8080/hello?name=${name}`)
            .toPromise()
            .then(r => r.text());
    }
}

修改app.component.ts文件:

import part
import {HelloWorldService} from '../service/hello.world.service';
Component part
@Component({
...
    template: `
        

Hello {{name}}!

`, providers: [HelloWorldService] })
class constructor part
constructor(private helloWorldService: HelloWorldService) {
}
class OnInit part
ngOnInit() {
    this.helloWorldService.hello('Sunshine').then(r => this.name = r);
}

这时web前端获取数据的时候讲遇到CORS问题,所以需要后端配置Access-Control-Allow-Origin,本例中简单进行处理。

修改Application类:

  • 增加嵌套类CORSFilter
@WebFilter(urlPatterns = "/")
class CORSFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}
  • 增加嵌套类FilterConfiguration
@Configuration
class FilterConfiguration {

    @Bean
    public CORSFilter corsFilter() {
        return new CORSFilter();
    }
}

重启Spring-Boot后端应用。

此时访问web页面,会显示Hello Sunshine!字样。

至此,本章的内容全部结束。

你可能感兴趣的:(从零开始搭建前后端独立Web App环境Part0:讲在前面)