前后端分离开发中前端需要克服的挑战
背景:
网页前后端分离开发,好处是关注点分离,以及并行开发提高效率;原生应用的前端和后端天生分离,原生应用开发在对接后端接口上需要克服比网页前端更多的挑战。
实际工作中碰到的问题:
- 并行不起来,前端依赖接口,在接口没好时,就只能等着。
- 要么,前端先写死一些数据开发页面,接口好了后,对接起来要修改大量代码,效率低下。
- 以及在后端发布之前,前端要修改大量代码去调用一个临时的终端节点,网页前端需要克服跨域问题,而原生应用往往需要解决自颁发证书的信任问题。
解决方案:
对于网页前端引入两种开发模式,而且通过良好的配置,使得在这两种开发模式可以自由切换,不用更改任何代码。
- 第一步:mock 模式下开发( npm run mock ):在接口没好时,使用 mock 模式开发。注意,mock 与直接页面写死数据不同,mock 是模拟接口返回的数据,至于请求接口这些代码仍然要写,而且当接口好了之后,切换到请求真实接口,不需要改动任何代码
- 第二步:代理模式下开发( npm run proxy ):在接口完成后,使用 代理 模式开发。注意,代理模式是从本地直接请求真实接口,这与运行时的请求真实接口不同,因为运行时的域名不会是 localhost。
项目发布后,是运行模式,这与以上两种模式都是有区别的。
umi 网页项目示例:
以 umi 项目为例,在 package.json 里加上两个 scripts :
{
...
"scripts": {
"mock": "umi dev",
"proxy": "UMI_ENV=dev MOCK=none umi dev"
}
...
}
可以看出, umi 项目自动开启 mock 开发模式,具体可以参考 umi 项目的 mock 配置。对于其他非 umi 项目,也可以自行配置。
而对于 proxy 模式,则需要本地通过 localhost 服务器代码去访问真实接口给前端用,这样才不会有跨域等问题。这个一般 webpack 项目都可以很简单地配置出来,如下:
proxy: {
'/api': {
target: 'https://your.domain.com/api',
changeOrigin: true,
pathRewrite: {'^/api': ''}
}
}
前端请求接口的代码如下:
fetch(`/api/xxx`, ...)
注意,通过以上配置后,无论是 mock 开发模式,还是 proxy 模式,还是运行时模式,前端请求接口的代码都不用变。
如果涉及到接口需要用户登录的场景,那么以上 proxy 配置改成这样即可:
proxy: {
'/api': {
target: 'https://your.domain.com/api',
changeOrigin: true,
pathRewrite: {'^/api': ''},
withCredentials: true,
headers: { Cookie: 'YourToken=9fk_i8oJE5xoqSvi4DS9RhDZARMskBLc-pD0oHDPK0r' },
}
}
非 umi webpack 网页项目示例:
只要引入 devServer 插件,在 setup 配置里配置好 mock 返回的数据,再通过一个 proxy 来实现在 mock 模式下向 mock 接口发起请求即可:
devServer: {
setup: app => {
app.get('/api/*', (req, res) => {
setTimeout(() => {
res.json({ custom: 'mock response' });
}, 3000);
});
},
proxy: process.env.MOCK ? {
'/api': {
target: 'http://localhost:3333/api',
changeOrigin: true
}
}/* mock 模式 */ : {
'/api': {
target: 'https://dev.example.com',
changeOrigin: true,
withCredentials: true,
headers: {
Cookie: ''
}
}
} /* dev 模式 */
}
安卓项目示例:
以上的 https://your.domain.com/api 终端节点,在开发过程中可能是临时的 url,并且其 https 证书往往是自颁发的证书,对于网页前端,通过恰当的 proxy 配置加上相应的浏览器设置都不会碰到问题,但是对于原生应用,就需要克服这个证书信任问题,可以通过一段比较丑的代码去强行信任所有的连接(只在开发时启用),比如对于安卓应用,可以使用下面这段代码:
// CustomTrust.java
package com.yourname.builder;
import org.jetbrains.annotations.NotNull;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
public class CustomTrust {
final static TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[] {};
}
}};
public static OkHttpClient getUnsafeOkHttpClient() {
try {
// Create a trust manager that does not validate certificate chains
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
// Create an ssl socket factory with our all-trusting manager
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
builder.hostnameVerifier(getHostnameVerifier());
OkHttpClient okHttpClient = builder.build();
return okHttpClient;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@NotNull
public static HostnameVerifier getHostnameVerifier() {
return new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
}
}
在正常获取到 Http 客户端后,利用上述的 CustomTrust 改写其信任设置:
import okhttp3.OkHttpClient
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
...
val okHttpClientBuilder = OkHttpClient.Builder()
trustAll(okHttpClientBuilder)
...
private fun trustAll(okHttpClientBuilder: OkHttpClient.Builder) {
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, CustomTrust.trustAllCerts, java.security.SecureRandom())
okHttpClientBuilder.sslSocketFactory(sslContext.socketFactory, CustomTrust.trustAllCerts[0] as X509TrustManager)
okHttpClientBuilder.hostnameVerifier(CustomTrust.getHostnameVerifier())
}
总结:
本文针对前后端并行开发时碰到的典型问题进行了总结,并给出了可行的改进方案。这个可行是指作者亲测有效,还对于网页前端和安卓前端给出了关键代码示例。