实战 web 应用 Docker 镜像解耦交付

把大象放进冰箱须要几步?把一个 web 应用塞进集装箱呢?html

随着几回浏览器大战的硝烟散尽和 Flash 的背影远去,当下的 web 应用开发通过十余年的发展,在工程化、测试、持续集成等方面都已经汇入了软件开发的快车道。前端

然而虽然新概念、新特性层出不穷,细分领域越发专业化,但其究极奥义始终未变 -- 无论你怎么折腾,生成出来的交付物还是 HTML/CSS/JS 老三样等静态资源,加上若干动态请求 的形式。从直接把文件拖放到 FTP 软件中手动上传的刀耕火种时代,到现在 Docker 镜像成为一种常见的部署格式 ,研发团队和运维团队的交互也在发生变化。vue

本文将在我的经验的基础上,尝试以一个前端项目为案例,浅谈其面向部署时的一些固有问题,以及与 Docker 相关的部分实践。node

拥抱 Docker 时的麻烦

在此以前,要部署一个前端项目,运维人员须要作什么呢?react

  • 安装完整的 node 环境并保持其更新
  • 阅读前端项目中 README 中的相关说明并更改相关文件中的设置项
  • 用 npm 安装一些全局依赖项
  • 保证 npm run build 流程的正确运行
  • 和前端开发同事协做解决因为打包机器不一样可能带来的问题

等搞定这么一全套的“份外”工做,才能获得打包后的目标文件并开始部署;这不可是多么痛的一种领悟,也是工做流层面一系列莫大的耦合。nginx

"All problems in computer science can be solved by another level of indirection." -- David John Wheelergit

面对代码封装中出现的耦合类问题,即使不了解 SOLID 原则、DRY 原则等等,以上这句 “啥都不叫事,抽象就对了!” 也算得上应该谨记的万金油了,是解耦的根本所在。github

Docker 镜像就做为这样一种优良的抽象层,为研发团队和运维团队更好地解耦提供了可能。web

然而在实际开发和部署中,囿于旧有经验和认知水平,可能会存在一些新问题:ajax

利用不一样的环境变量分别编译

严格来讲这不算遇到 Docker 后才有的问题,能够说绝大部分前端项目一直都是默认这么作的。

根据 BUILD_ENV 环境变量,分别对开发、测试、预发、生产环境等区分编译不一样的 API 的访问前缀 -- 好比对 GET /api/shops 数据接口的访问地址被分别编译成 http://test.com/api/shopshttps://api.stage.com/shops 等,虽然在传统的物理主机/虚拟主机工做流中这是无可指摘的标准作法;但在 Docker 语境中,这会致使分屡次生成几个不一样的镜像,从理论上难以保证“所测试的就是所部署的”这一理念。

此外,没法控制团队中的开发人员会利用这一特性添加什么其它的变量,甚至由于线上 bug 在本地难以重现而加以滥用做出特殊处理的也并不鲜见,这些都会对项目部署形成未知的干扰。

因此对于环境变量,或许咱们应该稍稍反思并保证最小化使用,从而探索更适于 Docker 的新经验。

在镜像外独立构建等

不管对于分发仍是部署,镜像越小越好,这是面对 Docker 时的一条广泛共识。对于构建过程当中常见的优化方式有:

  • 选用 alpine 版本的基础镜像
  • && 操做符来实现链式的 RUN 等指令以减小分层
  • 在容器中使用 nginx 而非 node 来伺服静态文件(服务器软件自己至少能减小 70M+)

另外,编译过程当中的依赖文件 也是没有必要包含在最终镜像中的,通常的处理如:

  • 在 Dockerfile 中编译而后用指令语句删除一些文件
  • 分为可复用的依赖镜像和最终打包镜像
  • 利用 Docker 的多阶段构建,在一个 Dockerfile 中解决问题;后面会有介绍

比较糟糕的一种作法多是,每次让运维人员利用相似 npm run build && docker build ... 的命令,在服务器上构建项目再打包到 Docker 镜像中。这样作既增长了运维团队的负担,使其和传统模式同样深陷在环境依赖和繁复流程中;又没法保证其手动调整项目配置项等代码后总体的正确性;且 npm 打包环境异于开发者,有较高的不肯定性。

构建参数

--build-arg 自己是个很方便的属性,能在 docker build 时传入必要的参数。但和项目中的环境变量相似,若是应用不当也会形成不一样环境下镜像不一致的问题。所以交由运维人员或者自动化执行的 docker build 命令最好没有构建参数。

SASS 依赖

不一样于其它依赖项,npm 安装 node-sass 包时,会从 github.com 上下载 .node 文件等。因为网络环境的问题,这个下载时间一般会很长,甚至致使超时失败。这每每成为了运维人员一个意料以外的痛点。

通常的解决办法是在 Dockerfile 中用 ENV 指令指定淘宝源:

ENV SASS_BINARY_SITE https://npm.taobao.org/mirrors/node-sass/
复制代码

而有些项目的构建环境更加极端,出于安全等考虑没法访问外网,其它依赖从公司内部的私有 npm 源上获取。这时针对 node-sass 问题,处理起来就要更特殊一些:

  • 访问 github.com/sass/node-s… .node 文件
  • npm i node-sass --sass_binary_path=<下载的.node文件> 语句整合进 Dockerfile

让镜像更易于交付

汇总以前分析的种种细节,来相对完整地看看如何配置镜像:

Dockerfile 多阶段构建

Docker 多阶段构建 是 17.05 版本开始后才有的一个特性。多阶段构建容许咱们将多个 FROM 语句放在同一个 Dockerfile 中。

每条 FROM 指令均可以使用各自不一样的基础镜像。每一个 FROM 语句也都标记了 Docker 构建过程当中一个新阶段的开始。咱们能够拷贝一个阶段的产出物到另外一个阶段,也能够抛弃不须要的部分。

这是个很是有用的特性,能避免最终镜像中存在编译过程当中的依赖文件,也就是镜像会变得更小了 。

# stage 0
FROM node:10-alpine as build-stage
WORKDIR /app
COPY package.json ./
ENV SASS_BINARY_SITE https://npm.taobao.org/mirrors/node-sass/
RUN npm install --registry=https://registry.npm.taobao.org
COPY . .
RUN npm run build-prod --silent 

# stage 1 (nginx)
FROM nginx:1.17-alpine
COPY config/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"]
复制代码

注意咱们经过 –from= 引用了 构建阶段 stage 0,并从构建阶段的工做目录拷贝了项目代码。

用数据卷覆盖镜像内配置

既然说了 npm 项目构建阶段用环境变量写入 API 请求地址等行为破坏 Docker 镜像的一致性,那到底如何请求到正确的端点呢?总要有个相似变量的东西传进去呀 ?!

但因为一来浏览器中没法用 process 感知环境,二来 Nginx 又不似 Node.js 应用同样能够直接传入参数;咱们只好稍费周章,想办法 写入一些 Nginx 能够伺服的文件做为变量来源

采用的技术正是 Docker 中的数据卷(volume),也就是在 docker run 时加载指定的目录或文件,用以在容器内建立或覆盖某些路径。单以写入 API 请求地址的需求为例,具体作法以下:

  • 在服务器上建立一个 endpoint.json 文件,内容为:
{
	"ENDPOINT": "http://api.app.com:5678"
}
复制代码
  • 在 ``docker run时加入参数-v`:
docker run -p 48081:8081 -v <JSON文件绝对地址>:/usr/share/nginx/html/endpoint.json:ro -d <镜像名>
复制代码

这样就在容器中的项目根目录下楔入了一个咱们能够随意配置的文件。

项目局部的异步改造

配置文件很轻松的就解决了,那么有了 endpoint.json 配置文件,如何在 runtime 将其应用于每一次异步请求呢?思路彷佛也颇为简单:

  1. 项目启动时先异步读取配置文件中的 ENDPOINT 属性
  2. 将读取到的属性放入项目中 fetch/ajax 框架的构造函数中,完成统一注入

注:某些构建糟糕的项目可能要多费些事了,须要将本来分散写在各处的请求前缀收敛为由统一的 fetch/ajax 框架处理

但或许麻烦就来自于异步请求这里 -- 因为一些状态管理工具的 store 里也存在异步请求,甚至 router 等处也会引用到 store,就颇有可能形成 其异步调用早于 fetch/ajax 框架的构造函数 执行,,从而形成一些请求的失败;咱们要作的就是对这些部分改成延迟加载。

以 vue 项目为例,在 main.js 中:

  • 删除原有的 import 语句:
// import router from './router';
// import store from './store';
复制代码
  • 改成延迟加载:
const init = async () => {
  const store = await import('./store');
  const router = await import('./router');
  return new Vue({
    i18n,
    router: router.default,
    store: store.default,
    render: h => h(App),
  }).$mount('#app');
};
复制代码
  • 保证顺序的初始化:
fetch('/endpoint.json').then(res => res.json()).then(cfg => {
  window.API_ENDPOINT = cfg.ENDPOINT;
  init();
});
复制代码

以及,在 fetch 框架中的引用:

const FetchWrapper = function(option) {
  const r = new QuickFetch(mergeWith({
    endpoint: window.API_ENDPOINT,
    baseURL: '/api'
  }, option));
  
  ...
}
复制代码

总结

面向以 Docker 镜像为交付物的前端开发,代码层面所需的调整其实不是不少,主要是观念上是否勇于从传统温馨的工做模式稍微跳脱出来。

另外在团队中多换位思考,让开发链条中处于下游的运维小伙伴更乐于对接你的工做,共同提高开发部署效率和质量,也是很重要的。

参考资料



--End--

查看更多前端好文
请搜索 fewelife 关注公众号

转载请注明出处

相关文章
相关标签/搜索