记一次拆分前后端模块部署Jar过程

背景

单体应用,前端使用React框架,静态资源(JS,CSS等)都放在src\main\resources\static目录下面:

.babelrc
.gitignore
.mvn
node_modules
package-lock.json
package.json
pom.xml
src
--main
	--java
	--resources
		--static
		--templates
			--dist
webpack.config.js

此时的前端构建配置文件webpack.config.js

var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

let devServer = {}
let plugins = [
    new webpack.DefinePlugin({
        ISDEV: JSON.stringify(process.env.NODE_ENV === 'development'),
    }),
    new HtmlWebpackPlugin({
        template: "./src/main/resources/templates/index.html",
        // ?
        inject: true
    }),
]
if (process.env.NODE_ENV === 'development') {
    devServer = {
        contentBase: path.join(__dirname, './src/main/resources/templates/dist'),
        host: 'localhost',
        port: '8000',
        open: true, // 自动拉起浏览器
        hot: true, // 热加载
    }
    plugins.push(new webpack.HotModuleReplacementPlugin())
}

module.exports = {
    entry: './src/main/resources/static/index.js',
    output: {
        path: path.resolve(__dirname, './src/main/resources/templates/dist'),
        publicPath: process.env.NODE_ENV === 'development' ? '' : './templates/dist/',
        filename: process.env.NODE_ENV === 'development' ? 'build.js' : 'build.[chunkhash].js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: [{
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', "@babel/preset-react"],
                        plugins: ['@babel/plugin-proposal-object-rest-spread', "@babel/plugin-proposal-function-bind", '@babel/plugin-proposal-class-properties']
                    }
                },
                    // {
                    //   loader: 'eslint-loader', // 指定启用eslint-loader
                    //   options: {
                    //     formatter: require('eslint-friendly-formatter'),
                    //     emitWarning: false
                    //   }
                    // }
                ]
            },
            {
                test: /\.(scss|css|less)$/,
                exclude: /node_modules/,  // 用于自定义样式处理,需去除node_modules文件夹
                use: [{
                    loader: "style-loader"
                }, {
                    loader: "css-loader",
                    options: {
                        modules: true,   // 采用className={styles.xxx}的方式进行添加样式
                        localIdentName: '[local]--[hash:base64:5]'
                    }
                }, {
                    loader: "less-loader",
                }]
            },
            {
                test: /\.(scss|css|less)$/,
                include: /node_modules/,  // 用于antd, 需包括node_modules
                use: [{
                    loader: "style-loader"
                }, {
                    loader: "css-loader",
                }, {
                    loader: "less-loader",
                    options: {
                        modifyVars: {
                        	// 自定义主题色
                            "primary-color": "#6E54B0",
                            "link-color": "#6E54B0",
                            'border-radius-base': '2px',
                        },
                        javascriptEnabled: true,
                    }
                }]
            },
            {
                test: /\.(eot|ttf|woff|woff2)(\?\S*)?$/,
                loader: 'file-loader'
            },
            {
                test: /\.(png|jpg|gif|svg)$/,
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]?[hash]'
                }
            }
        ]
    },
    resolve: {
        extensions: ['.js', '.jsx', '.json'],
    },
    performance: {hints: false},
    plugins: plugins,
    devServer
}

前端package.json文件没什么特殊之处:

 {
 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "set NODE_ENV=development&& webpack --mode=development",
    "pre": "set NODE_ENV=development&& webpack --mode=development",
    "prod": "set NODE_ENV=production&& webpack --mode=production",
    "start": "set NODE_ENV=development&&webpack-dev-server --mode development --port=8001"
  }
}

后端有一个配置文件StaticResourcesConfig.java

@Configuration
public class StaticResourcesConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");
        super.addResourceHandlers(registry);
    }

    // 无关此文主题,解决跨域的配置
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                .maxAge(3600)
                .allowCredentials(true);
    }
}

现在,想要做前后端分离,此为背景。

实践

项目架构调整:
在这里插入图片描述
根目录pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
    <parent>
        <groupId>com.aaa.framework.microservice</groupId>
        <artifactId>microservice-starter-parent</artifactId>
        <version>1.1.9</version>
    </parent>
    <groupId>com.aaa.cbd.platform</groupId>
    <artifactId>octopus-backend-parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0.0-SNAPSHOT</version>
    <modules>
        <module>octopus-backend-ui</module>
        <module>octopus-backend</module>
    </modules>

    <properties>
        <module.version>1.0.0-SNAPSHOT</module.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.aaa.cbd.platform</groupId>
                <artifactId>octopus-backend</artifactId>
                <version>${module.version}</version>
            </dependency>
            <dependency>
                <groupId>com.aaa.cbd.platform</groupId>
                <artifactId>octopus-backend-ui</artifactId>
                <version>${module.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.7.0</version>
                    <configuration>
                        <verbose>true</verbose>
                        <fork>true</fork>
                        <compilerVersion>${java.version}</compilerVersion>
                        <source>${java.version}</source>
                        <target>${java.version}</target>
                        <encoding>UTF8</encoding>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

后端pom.xml文件依赖于前端构建产物Jar:

<project>
    <parent>
        <groupId>com.aaa.cbd.platform</groupId>
        <artifactId>octopus-backend-parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>octopus-backend</artifactId>

    <properties>
        <octopus-backend-ui.version>1.0.0-SNAPSHOT</octopus-backend-ui.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.aaa.cbd.platform</groupId>
            <artifactId>octopus-backend-ui</artifactId>
            <version>${octopus-backend-ui.version}</version>
        </dependency>
    </dependencies>
</project>

而前端工程的pom.xml文件配置如下:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.aaa.cbd.platform</groupId>
    <version>1.0.0-SNAPSHOT</version>
    <artifactId>octopus-backend-ui</artifactId>
    <properties>
        <build-plugin.exec.version>1.4.0</build-plugin.exec.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>exec-npm-install</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>npm</executable>
                            <arguments>
                                <argument>install</argument>
                            </arguments>
                        </configuration>
                    </execution>
                    <execution>
                        <id>npm-build</id>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>npm</executable>
                            <arguments>
                                <argument>run</argument>
                                <argument>build</argument>
                            </arguments>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <nonFilteredFileExtensions>
                        <nonFilteredFileExtension>woff</nonFilteredFileExtension>
                        <nonFilteredFileExtension>ttf</nonFilteredFileExtension>
                        <nonFilteredFileExtension>woff2</nonFilteredFileExtension>
                        <nonFilteredFileExtension>eot</nonFilteredFileExtension>
                        <nonFilteredFileExtension>swf</nonFilteredFileExtension>
                        <nonFilteredFileExtension>ico</nonFilteredFileExtension>
                    </nonFilteredFileExtensions>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>dist</directory>
                <targetPath>META-INF/ui</targetPath>
                <filtering>true</filtering>
            </resource>
        </resources>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>${build-plugin.exec.version}</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>2.6</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

前端webpack.config.js仅列出有变化的部分:

let plugins = [
    new HtmlWebpackPlugin({
    	// change 1
        template: "./index.html",
    }),
]
if (process.env.NODE_ENV === 'development') {
    devServer = {
		// change 2
        contentBase: path.join(__dirname, './dist'),
        host: 'localhost',
        port: '8000',
        open: true,
        hot: true,
    }
    plugins.push(new webpack.HotModuleReplacementPlugin())
}

module.exports = {
	// change 3
    entry: './src/index.js',
    output: {
    	// change 4
        path: path.resolve(__dirname, './dist'),
        // change 5
        publicPath: process.env.NODE_ENV === 'development' ? '' : './',
        filename: process.env.NODE_ENV === 'development' ? 'build.js' : 'build.[chunkhash].js'
    }
}

根据父工程的pom文件配置,打开maven面板,先执行clean然后执行install:
在这里插入图片描述
可以得到如下构建产物:
在这里插入图片描述
可以发现:前端工程会打包为一个Jar,并作为一个依赖放在后端Spring Boot应用Jar包里面。

此时,我们可以启动应用:java -jar -Dserver.port=8082 octopus-backend-1.0.0-SNAPSHOT.jar

浏览器输入:http://localhost:8082/hs,返回响应OK。因为我们写有一个应用健康状态Controller接口:

@RestController
public class HealthController {
    @RequestMapping(value = "/hs")
    public String hs() {
        return "OK";
    }

问题来了,怎么访问前端页面呢?

答案是:StaticResourcesConfig配置文件。

之前的配置为:
registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");

我们替换为:
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/META-INF/");

同时pom.xml文件更新为:

<resources>
    <resource>
        <directory>dist</directory>
        <targetPath>META-INF/static</targetPath>
        <filtering>true</filtering>
    </resource>
<resources>

然后浏览器输入:http://localhost:8082/static/static/index.html#/,可以访问:
在这里插入图片描述
如何去掉两层static嵌套呢?

血与泪的尝试,ResourceHandler一定要是两层目录:
registry.addResourceHandler("/**/**").addResourceLocations("classpath:/META-INF/");

同时pom.xml文件更新为:

<resources>
    <resource>
        <directory>dist</directory>
        <targetPath>META-INF/</targetPath>
        <filtering>true</filtering>
    </resource>
</resources>

解压缩前端工程Jar包:
在这里插入图片描述
发现dist文件夹复制到META-INF目录下。

新问题

浏览器打开登录页
http://localhost:8082/index.html#/

登录成功后进入首页: 404
http://localhost:8082/index

实际上能够正常显示的 首页:
http://localhost:8082/index.html#/index

其他页面:
http://localhost:8082/index.html#/appList

言外之意,页面需要再嵌套加一层index.html#/

怎么解决这个问题呢?看起来是前端路径的问题,此时如果把精力放在前端构建文件webpack.config.js,则是走了弯路。

最后的解决方法,还是配置StaticResourcesConfig文件:

package config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class StaticResourcesConfig extends WebMvcConfigurerAdapter {
    @Value("${server.contextPath:}")
    private String contextPath;

    @Value("${applications.indexForwardPath:index.html}")
    private String forwardPath;

    @Value("${applications.resourceLocations:classpath:/static/,classpath:/public/}")
    private String resourceLocations;

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // contextPath为空,if条件不成立
        if (StringUtils.hasText(contextPath)) {
            registry.addRedirectViewController(contextPath, contextPath + "/");
        }
        // 下面两个addViewController有什么区别
        registry.addViewController(contextPath + "/").setViewName(forwardPath);
        registry.addViewController("/*/").setViewName("forward:index.html");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        resourceLocations += ",classpath:/META-INF/";
        registry.addResourceHandler("/**")
                .addResourceLocations(resourceLocations.split(","))
                .resourceChain(true);
    }
}

另外还要去掉一个Controller:

@RequestMapping("")
@Controller
public class UserController {
    @GetMapping
    public String goToLogin() {
        return "dist/index";
    }
}
posted @   johnny233  阅读(34)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示