从零开始搭建前后端独立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,大多数依赖包使用官方配置的,避免出现某些意外。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.1.RELEASE</version>
</parent>

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

  • properties

设置一些变量

<properties>
    <java.version>1.8</java.version>
</properties>

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

  • dependencies

设置依赖包

<dependencies>
    <!-- spring boot dependence begin -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- spring boot dependence end -->

    <!-- other dependence begin -->
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.module</groupId>
        <artifactId>jackson-module-jaxb-annotations</artifactId>
    </dependency>
    <!-- other dependence end -->
</dependencies>

以上依赖包均不用设置版本号,都继承自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

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

<build>
    <finalName>${project.name}</finalName>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
                <encoding>utf-8</encoding>
                <failOnError>true</failOnError>
                <executable>${env.JAVA_HOME}/bin/javac</executable>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

插件简单说明:

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: `
        <h1>Hello {{name}}!</h1>
    `
})

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作为实际访问的页面,内容如下:

<!DOCTYPE html>
<html>
<head>
    <script>document.write('<base href="' + document.location + '"/>')</script>
    <title>Hello World!</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">

    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
        System.import('app')
                .catch(function (err) {
                    console.error(err);
                });
    </script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

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

$ npm start

此时会打开浏览器,自动访问 http://localhost:3000/ ,页面会出现<h1>标签样式的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<string> {
        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: `
        <h1>Hello {{name}}!</h1>
    `,
    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!字样。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,679评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,057评论 25 707
  • 你背对着我美的如画, 我问你哭了吗没人回答。 我帮你撑着伞, 因为雨一直下。
    紫色心愿阅读 138评论 0 0
  • 古人云:每逢佳节倍思亲。其实,即使不是佳节,思家的情绪也会一直暗藏于游子的心中,只不过是平日里缺少情绪爆发的触发点...
    咚2咯咯阅读 258评论 0 2