概述

我们在开发项目时,经常需要部署测试环境。本文以webpack为例,记录一些常见的问题和解决方案。

目录

项目内的js参数配置

我们在搭建测试环境时,某些参数是不同的,比如api请求地址web页面的title某些第三方的appid等。我们有以下两种方案来解决这个问题:

  1. 在运行时判断当前环境,并使用相应的参数。
  2. 在编译时判断构建环境,直接将变量编译成常量。

我们知道,在运行时处理这些参数会导致编译得到的js文件更大(判断环境的代码加进去了嘛),并且更加消耗客户端的执行时间(每次运行都要判断当前环境)。因此方案二彻底打败了方案一,下面我们说说方案二如何实现。
为了达到在编译时区分项目环境的目的,我们必须告诉webpack当前构建的是什么环境。我们可以通过传递环境变量的方式(BUILD_ENV=development webpack --some-arguments)告诉webpack当前构建的环境。然后在webpack的配置文件(webpack.config.js)中注入配置到项目中去。这里我们要用到一个插件webpack.DefinePlugin

// webpack.config.js
const path = require('path')
const webpack = require('webpack')

const API_HOST = process.env.BUILD_ENV === "development" ? "https://dev.example.com/user/v1" : "https://api.example.com/user/v1";
// 编译时输出API_HOST
console.log(API_HOST);
module.exports = {
    entry: [
        './src/index.js'
    ],
    plugins: [
        new webpack.DefinePlugin({
            'API_HOST': JSON.stringify(API_HOST),
        }),
    ],
    output: {
        publicPath: '/',
        path: path.resolve(__dirname, 'dist'),
        filename: 'static/[name].js'
    },
    resolve: {
        extensions: ['*', '.js'],

    },
    optimization: {
        minimize: false,
    }
};

// index.js 用到API_HOST变量的文件
var loginURL = API_HOST + "/user/login";
// 运行时输出 loginURL
console.log(loginURL)

然后运行BUILD_ENV=development npm run build编译项目。观察编译得到的js文件:

// 编译得到的js
function(module, exports) {

var loginURL = "https://dev.example.com/user/v1" + "/user/login";
// 运行时输出 loginURL
console.log(loginURL)

/***/ }

我们发现源代码中的API_HOST变量被他的值https://dev.example.com/user/v1代替了。这样我们就完成了通过传递环境变量控制项目全局参数的功能。

Tips: 试着执行BUILD_ENV=production npm run build来确信API_HOST会随之变化。

使用docker部署测试环境

在部署web端项目时,我们需要选择一个高性能的web-server,如:nginx, apache httpd, caddy等。在这里我们选择caddy作为web服务器,主要是看中了他配置简单,支持Let’s Encrypt等特性。

Tips: 去caddy的官方网站下载一个适合你的操作系统的构建版本,然后放置到你的web项目目录下并运行他,就可以在浏览器中通过http来访问你的网站了。

使用docker部署web项目一般有多种做法,下面列举了2种我使用过的方案:

  • 使用官方的web-server镜像,通过docker volume挂载你的web项目。
  • 使用官方的web-server作为基础镜像,构建自己的docker镜像,将你的web项目打包到镜像中。

以上两种方案各有优劣,方案一更接近传统的部署方案,每次更新项目只需要更新host上被挂载目录中的文件即可。方案二则更像是刻录在实体光碟上的软件,每个版本都有对应的镜像,对版本更新回滚等操作更加友好。我更倾向于第二种方案,可以更好地配合git等实现CICD,在构建大型应用时也更加好用。我们以第二种方案为例,讲述一下从构建镜像到最终部署的流程。
首先,我们需要为我们的web项目编写Dockerfile

# Dockerfile
# 用node作为builder的基础镜像,因为构建我的项目需要用到node(react项目)
FROM node:latest as builder
WORKDIR /home/www
# 默认BUILD_ENV为production
ARG BUILD_ENV=production
COPY . .
# 安装npm依赖,根据网络环境会消耗一些的时间
RUN npm i
# 构建, 设置环境变量
RUN BUILD_ENV=${BUILD_ENV} npm run build

# 这里是最终运行镜像的环境,用到了caddy作为web-server
FROM narrowizard/caddy:latest
WORKDIR /srv
# 将刚才构建的文件复制过来
COPY --from=builder /home/www/dist/ .
# 复制Caddy的配置文件(需要放在项目目录中)
COPY --from=builder /home/www/Caddyfile /etc/Caddyfile

Tips: 这里没有用caddy官方的镜像,因为caddy官方镜像将/srv目录声明为volume,会导致镜像重建时更新失败的问题,具体原因参照: https://github.com/narrowizard/dockerfiles/tree/master/caddy

# Caddyfile, 在这个文件里配置web-server的相关内容
:80 {
    root /srv
    gzip # 开启gzip
    rewrite { # 重写路由, history模式的react项目或vue项目需要开启这个, 否则无法重定向路由到首页
        to {path} /
    }
    header / {
        Cache-Control "no-store" # 关闭根目录的缓存
    }
    header /static {
        Cache-Control "max-age=2592000" # 设置/static目录的缓存时间
    }
}

Caddyfile主要用来配置web-server的相关内容。
接下来运行docker --build-arg BUILD_ENV=dev build -t ${image name}:${tag} .就可以构建指定版本(dev或者prod)的docker镜像了。

Tips: 赶快运行 docker run -t some-container -p 80:80 ${image name}:${tag} 来运行你构建的镜像吧!

结合gitlab实现CICD

到这里为止,我们已经构建了一个web项目的镜像,接下来就需要将这个镜像运行到服务器上来完成部署。我们先来看一下还需要做多少事情才能把项目部署到服务器吧。

  • 将镜像托管到远程的registry服务
  • 通过ssh登陆目标(用来运行镜像的服务器)服务器
  • 从registry更新镜像到目标的版本
  • 终止旧版本的镜像
  • 运行新版本的镜像,完成!

乍一看好像不是很麻烦,但是想到以后每次更新都要做这么多事(还包括前面的的构建步骤),整个人都不太好了。不过不要着急,配合一些CICD的工具,我们就可以实现全自动化的项目更新。我们在这里以gitlab为例。
我们只需要在我们的项目根目录下创建一个.gitlab-ci.yml文件,并且在服务器上注册一个gitlab-runner,然后每次push master分支到gitlab,就会触发以下的任务,完成自动部署拉。

pre-release-build:
  image: docker:latest
  stage: build
  tags:
   - shared
  only:
   - master
  script:
    - docker --build-arg BUILD_ENV=dev build -t $CI_PROJECT_NAME:$CI_COMMIT_REF_NAME .
    - docker push $CI_PROJECT_NAME:$CI_COMMIT_REF_NAME

pre-release:
  stage: deploy
  variables:
    GIT_STRATEGY: none
  tags:
   - shared
  only:
   - master
  script:
   - docker stop $CI_PROJECT_NAME-PRE || true
   - docker rm $CI_PROJECT_NAME-PRE || true
   - docker pull $CI_PROJECT_NAME:$CI_COMMIT_REF_NAME
   - docker run -d -p 80:80 --name $CI_PROJECT_NAME-PRE $CI_PROJECT_NAME:$CI_COMMIT_REF_NAME

关于如何注册gitlab-runner的内容已经超出了本文的讲述范围,请自行查阅相关资料。