多单页面应用的开发与部署
关于
目前大多数的前端项目都是以单页面(SPA)的方式进行开发,单页面的开发一大特点是前后端解耦,很多情况下前端会单独部署,但通常一个公司会有多个前端项目共同开发,且面临着多个应用的相互关联,涉及到开发和部署的问题。
开发状态
SPA 项目开发的时候需要依赖后端提供的 API , 那前端项目运行的时候就需要将这些 api proxy 到后端服务器,其基本工作流程为:
- 前端模块的编译,jsx,es6
- 静态资源的拷贝
- 启动静态资源访问服务器
- 服务器设置后端 api proxy 代理
- 启动一个监听服务器 livereload 或者 hot-reload
如果基于 express 的,可以使用 http-proxy-middleware
一个基本的配置:
const proxy = require('http-proxy-middleware');
const config = {
"default": {
"api": "http://127.0.0.1:8080",
"endpoints": [
"/api/*",
]
}
}
// Proxy middleware
const addProxyMiddlewares = (app, options) => {
const services = require(options.config);
Object.keys(services).forEach((key) => {
const service = services[key];
const api = service.api;
const loglevel = service.logLevel || 'info';
const Proxy = proxy({
target: api,
logLevel: loglevel,
changeOrigin: true,
});
service.endpoints.forEach((endpoint) => {
app.all(endpoint, Proxy);
});
});
};
在某些情况下,一个 SPA 的开发状态需要依赖其他项目,如每个 SPA 都有一套统一的单点登录系统。但如上的工作流中并没有启动单点登录相关的配置,这时候应该有几种情况:
- 每个应用基于子域名的方式
- 确定域名,这是硬编码在代码中的,得在早期确定
- 确定登录系统的域名访问地址 eg: login.domain.com
- 应用 url 为,app.domain.com
- 修改配置 host 文件
- 将 login.domain.com 代理到对应的开发服务器 ip
- 将 app.domain.com 代理到本机 127.0.0.1
- 多个 SPA 的开发协同通过开发服务器和域名中转
- 确定域名,这是硬编码在代码中的,得在早期确定
- 多个应用在相同的域名下面
- 在某些情况,配置多个子域名并不是一件容易的事儿,这种情况下域名的结构可能是
- 登录 domain.com/login
- 应用 domain.com/app
- 修改域名的方式协同
- 将 domain.com 代理到后端开发服务器
- 仍然将 app.domain.com 代理到本机 127.0.0.1
- 直接配置 proxy 的方式
- 修改上面 proxy 例子中的 endpoint,添加对应要代理的 endpoint 到其中,这样不需要 host 代理也能直接访问其他 SPA(本质是代理的点变成了有 http-proxy-middleware 来提供)
- 这种配置方式的缺点就是不能一劳永逸,如果依赖新的应用,新的应用所有的 url 都得 proxy,并且还得避免应用的 assets 这些静态资源访问路径不会冲突
- 在某些情况,配置多个子域名并不是一件容易的事儿,这种情况下域名的结构可能是
- 域名不固定的情况
- 有时候域名并不能确认,比如开发的项目是一套解决方案,会部署到不同客户的环境中,域名都是不一致的
- 域名的设置也通常是按照 2 的方式,多个应用在相同域名下,只用分配一个域名就可以工作
- 域名硬编码的方式
- 域名硬编码会导致在客户机房部署的时候遇到更多的配置问题,如果想做到一键部署,那么是不能硬编码到前端的
- 如果一定要硬编码,那也得做一个域名配置文件,由后端来告诉前端其他服务所在的 url 是什么
- 可以通过提供一个 api 获取其他服务的地址
- 可以直接输出到 html hidden input 中
- 同样也可以按照 2.3 中的方式,通过 proxy 来配置
- 每个应用开发完成过后,需要以部署态部署到开发服务器中
- 如果本地同时在开发多个 SPA 呢?
- 单个项目能支持多个 html 入口,如 webpack 支持多个 entry 的打包
- 多个项目通过 nginx 来替换本地 server 配置来实现,见下面 nginx 部署应用
- 一个在线的 SPA 依赖本地的开发项目
- 不同项目在不同子域名
- 如在线项目地址是 app1.domain.com
- 本地开发的项目地址是 app2.domain.com
- 这个时候可以将 app2.domain.com 代理到本地开发服务器来调试和开发
- 相同域名
- 项目在 domain.com/app1 和 domain.com/app2
- 解决方案 1. 本地启用一个 nginx 服务domain.com 代理到本地,将 app2 代理到本地服务器,其他代理到线上服务器 2. 如果 js 是基于 CDN 的(有单独的域名),那么可以设置一个 js 静态文件的代理服务器,将线上的 js 访问代理到本地的 build 目录
- 不同项目在不同子域名
部署状态
前端应用的部署总的来说有两种方式
- 把应用放在后端开发框架中
- 通过 Nginx 直接代理
前端静态资源的部署也可分为两种
- 通过 cdn 上传静态资源
- 开发服务器放置静态资源
把应用放在后端开发框架中
这是前后端未解耦的状态,也是大家最能理解的状态,如 PHP 直接输出 HTML ,配置一个路由,server 返回对应路由的 HTML 渲染结果,多个应用对应多个不同的路由,或者在不同的 server 中实现
- 优点:
- 简单,容易理解
- 后端可以直接加载一些数据到 html 文件中,或者实现 server render,加速页面渲染
- 缺点:
- 前端每次发布都得修改对应的后端代码仓库的文件,主要是 js 访问地址
把应用放置在 Nginx 中
前端完全独立部署,将前端应用的打包结果直接放置在 WWW 目录中,配置 Nginx 服务器,将对应的路由连接到对应的应用,一个基本的结构是
- www
+ app1
* assets
* index.html
+ app2
* assets
* index.html
+ app3
* assets
* index.html
nginx 的配置,app1 为默认应用,访问根路径会 rewrite 到 app1/index.html
server {
set $api_server http://server-api-domain.com;
set $root /Users/admin/project-path/build;
listen 80;
server_name localhost;
access_log logs/host.access.log;
error_log logs/error.log debug;
rewrite_log on;
# rewrite root to index app
location = / {
root html;
index index.html index.htm;
rewrite ^(.*)$ /app1/index.html;
}
# apps with .html extention
location ~ /([a-z|\d]+)\.html$ {
root $root;
rewrite ^/([a-z|\d]+)\.html$ /$1/;
index index.html index.htm;
}
location / {
root $root;
if ( $content_type = "application/json" ) {
proxy_pass $api_server ;
}
index index.html index.htm;
client_max_body_size 1000m;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
对应的前端编译问题
这样的部署方式同时需要前端编译的修改,因为在服务其中,静态资源的位置为
/app/assets
而在本地开发模式中,静态资源的访问位置为
/assets
如果以 host/app.html 的方式访问或者 host/ 访问,那么静态资源位置就会访问错误
这时候可以修改 webpack 的 public 路径来解决(开发调试状态不用修改,production 状态需要修改)
本地同时开发多个 SPA 的情况
实际上在本地开发的时候也可用上面的 nginx 服务器配置,用 nginx 代替 express 和 proxy 中间件,对应如果本地同时开发多个项目,如开发 app1, app2 ,那么可以:
- 将 app1 转发对应到 app1 项目的 /app1/build 目录
- 将 app2 转发到对应的 app2 项目的 /app2/build 目录
- 将其他的应用和后端 API 访问都转发给后端服务器
# development multiple project
#location ^~ /app1/ {
# root $app1_development_root
#}
#location ^~ /app2/ {
# root $app2_development_root
#}
# or proxy_pass other project
#location ~ /([a-z|\d]+) {
# set $appname $1;
# proxy_pass $server;
#}
通过 cdn 上传静态资源
前端除了 html 外需要部署的就是静态资源,如果使用如七牛这样的云服务,或者公司有自己的 CDN 服务,可以直接将打包好的前端资源发布到 CDN 中,并且通过版本区分
每次 release push 的时候。通过在 gitlab 或者 github 上面实现一个 hook,hook 调用上传脚本
然后对应前端在 production build 的时候修改 public 的 url 对应到 cdn 中的位置
总结
单页面应用以及前后端解耦都快成了老生常谈的事儿了,但是对于不同成熟度的前端的团队在尝试单页面应用时遇到的挑战都是不同的,有的团队是有现成的产品基础,只是修修补补,有的则是从 0 到 1。
个人觉得单页面应用的部署和开发问题可能也是大多数团队遇到的问题,以上的这些 idea 仅供参考,如果您有更多的想法,欢迎讨论
mark
mark