如何将 Angular 应用部署在虚拟目录/子目录

假设我们开发了一个 Angular 应用,我们希望将它部署在 https://www.iconben.com/app1,我们该怎么做呢?

这涉及两方面的配置,一方面是 Nginx 配置文件中,配置 server 下面的子目录;另一方面,因为 Angular 应用中的所有请求都会经过 url 拼装,需要让 Angular 知道自己处于子目录的环境下。

前者通过 nginx 配置文件内 server 中配置 location 可以实现(有 root 和 alias 两种用法,若子目录与angular 应用的文件夹不同名,则用 alias 配置):

server {
        server_name   www.iconben.com;
        listen 80;

        location /app1 {
            alias  /srv/www/angular-app1;
            index  index.html  index.htm login.html;
            client_max_body_size  1024m;
        }

        location / {
            root  /srv/www/www.iconben.com;
            try_files $uri $uri/ /index.html;
            index  index.html  index.htm login.html;
            client_max_body_size  1024m;
        }
}

后者通过修改 Angular 应用的 base href 可以实现。即修改index.html 中的 base 标签为 <base href=”app1″>(默认为<base href=””>,表示配置在站点根目录)。

考虑到开发环境多数并非子目录环境,以上这一步要么在打包的时候才实现(如果是 CI/CD 的环境,配置好相关脚本就好);要么则是不修改打包结果,而在 nginx 中用 sub_filter 配置 index.html 被请求的时候动态替换base 标签的 href 值。

以下为完整配置:

server {
        server_name   www.iconben.com;
        listen 80;

        location /app1 {
            alias  /srv/www/angular-app1;
            sub_filter '<base href="/">' '<base href="/app1/">';
            sub_filter_once on;
            try_files $uri $uri/ /app1/index.html;
            index  index.html  index.htm login.html;
            client_max_body_size  1024m;
        }

        location / {
            root  /srv/www/www.iconben.com;
            try_files $uri $uri/ /index.html;
            index  index.html  index.htm login.html;
            client_max_body_size  1024m;
        }
}

网上有很些相关文章,但很多没讲到点子上。

一般官方文档是很好的参考来源。以上配置涉及 angular 部分参考这里

使用容器化的 php-fpm 的文件读写权限问题

手头有两个站点,在服务器上通过 nginx 对外服务,nginx 转发 php 请求给基于 php-fpm 容器。在如下架构下部署了 wordpress 站点。站点能运行,但涉及写文件的时候(譬如上传图片)则会报错。

一开始以为是 nginx 权限问题,查了 nginx 运行在用户 nginx 之下,因此尝试对网站文件夹给予 nginx 用户读写权限,但没解决问题。

后来知道了是 php-fpm 容器使用了 www-data 用户,因此对网站文件给予 www-data 用户读写权限就行了,譬如运行: chown -R www-data:www-data ./wordpress-website/

此操作有一个注意事项,即是多半在 host 系统中没有 www-data 用户,只在 php-fpm 容器中有,因此需要先进入 php-fpm 容器命令行,然后做以上操作。

使用 certbot 生成Let’s Encrypt通配符SSL证书

当我们有多个二级域名都需要启用 SSL 证书的时候,如果一个一个的进行证书的设置与管理是比较费时间精力的事情,还好有通配符证书。通配符证书能支持譬如 *.iconben.com 这样的多个二级域名,十分方便。

Let’s Encrypt 从2018-03-13起开始支持通配符证书。因此这给了我们免费实现系列站二级域名共用同一个证书的机会。因我们的域名解析托管在 DNSPod,以下简要说明如何使用客户端 certbot 工具,配合 certbot-dns-dnspod 插件,对于托管在 DNSPod 的域名生成通配符证书。

生成证书

安装 Certbot,选择相应的 Web Server 和操作系统,按照说明安装 Certbot。

然后通过 DNS 验证的方式生成证书:可以安装 Certbot’s DNS plugins。手动和自动都可以生成 SSL 证书,因通配符的证书有效期也是3个月,使用插件自动生成的话续期十分方便。

因本站域名解析使用的是DNSPod,可以使用 certbot-dns-dnspod,使用 pip 安装,如 pip3 install certbot-dns-dnspod。

针对那些没有在列表中DNS服务商,可以去自行搜索相关插件。

安装完成后,还需要登录到 DNSPod控制台 生成API Token,并保存在配置文件/path/credential.ini中。注意 DNSPod 已经启用了 V3 版 API,但我们这里使用的是 V2 版。配置文件中需要有 email 参数,必须是 DNSPod 账号的登录邮箱,api_token 参数,必须是双引号括起来的 “ID,Token”的形式,例子如下:

dns_dnspod_email = someone@example.com
dns_dnspod_api_token = "12345,c54cd52eae137f765af143d1dfader3434"

接下来运行命令行:

sudo certbot certonly -a dns-dnspod --dns-dnspod-credentials /path/credentials.ini -d example.com -d "*.example.com" 

# -a:指定使用的插件
# --dns-dnspod-credentials :指定 dnspod 使用的 含 email 和 api_token 的配置文件
# -d:指定要生成证书的域名

其他DNS plugins的使用也是类似的。

运行命令后,根据向导输入邮箱,同意条款,选择是否与 EFF 共享个人邮箱信息,然后 DNS plugin 创建 DNS TXT 记录,进行域名验证,然后生成 SSL 证书,然后删除 DNS TXT记录。最后提示证书生成成功!

CORS 的 preflight 机制

昨天帮某客户配置 wordpress 的 SSL,本以为是个很简单的活儿,结果遭遇问题,弄了很久才解决。在此过程中对跨域访问的预检机制有了较多的了解,在此记录。

需求:
之所以要将该 wordpress 网站升级为整站 https,除了网站本身的安全性和对外形象之外,最主要是该网站的文章是另外一个 web 应用的数据来源。具体来说,是客户另外那个 web 应用通过 rest api 获取该网站的文章列表并显示。而 web 应用已经率先整站 https,若其通过基于未加密的 http 协议来访问该 wordpress 网站的 rest api,浏览器中会有安全提醒。

第一步:
客户 wordpress 网站基于 centos7,apache2.4.6,php7.0 等。帮客户升级至 php7.4,安装 certbot 配置 let‘s encrypt 证书并配置自动续期,配置安全规则允许 443 端口访问。https 成功可以访问,初步成功。

问题1:
虽然网站在浏览器中可以使用 https 进行访问,但客户的 web 应用上对该网站的 rest api 请求却失败。仔细查看浏览器控制台输出,发现报的错误是:Cross origin resource sharing error: MissingAllowOriginHeader。含义是在 rest api 的 HTTP 响应头中,没有包含 Access-Control-Allow-Origin 的 header。

解决1:
WordPress 虽然是全球用量很大的 CMS 系统,但其文档及社区却不敢恭维,主要是因为其版本众多,各种网络文章针对不同版本不容易分辨,且 PHP 这门世界上最好的语言实在太容易各种 hack,所以对于一个问题的解决方案也是八仙过海,各显神通,良莠不齐。了解到 wordpress 默认不允许跨域访问。经过试验各种配置(包括调整 apache 的默认配置和 VirtualHost 配置,调整 wordpress 的.htaccess 文件内的配置),最后使用的方案是在 wordpress 的 wp-include 文件夹下修改 function.php 实现跨域访问的域名白名单。

问题2:
前一步成功以后,查看浏览器中该 api 访问的预检仍然报错:Cross origin resource sharing error: HeaderDisallowedbyPreflightresponse。这是提示请求头中有不允许的 header,通过排查,发现是请求方应用中有某几个自定义的请求头,通过在服务器端设置 Access-Control-Allow-Headers 而成功。

在 ARM 架构的虚拟机上安装 ImageMagick 的 JPEG 和 PNG 支持

手上在用一台 ARM 架构的虚拟机,在一个项目中需要用到 ImageMagick 库提供图片文件的裁剪服务。因官方软件源不包含 ImageMagick,于是手动下载源代码并编译安装。但发现默认不支持 JPEG 和 PNG 格式的文件,因此需要手动安装这两个格式的支持。

关于安装方法,网上找到一些文章,包括官方文档似乎也语焉不详(官方文档不算友好,起码从搜索结果来看是如此)。经过一番探索才搞定。

其实不需要像网上有些地方所说的要从源代码安装之类的。只需要:
1. 安装 openjpeg2 和 libpng15 库:
sudo yum install openjpeg2
sudo yum install libpng15

2. 重新安装 ImageMagick:
./configure –disable-shared –with-openjp2=yes –with-png=yes
make
sudo make install

Angular 9 报 Invalid package name “__ngcc_entry_points__.json”

最近在某个项目从 Angular 8 升级的时候,允许 npm install 的时候遇到如下报错:

 
npm ERR! code EINVALIDPACKAGENAME
npm ERR! Invalid package name "__ngcc_entry_points__.json": name cannot start with an underscore

网上搜索到的方法主要是讲通过如下操作解决:

rm -rf node_modules
npm install

估计有的人行,但我这里仍然报错。实际上还应该删除 package-lock.json 文件才行。

rm -f package-lock.json

我主要是各种包的版本升级折腾遇到的,估计也是小概率吧。

阿里云 Linux 服务器创建 Swap 分区

服务器有时候会突发需要大量内存(譬如我们有运行一个自动构建工具,当代码某个分支发布新版本,会触发编译打包),若内存不够会导致有些进程被杀掉,严重的时候甚至整个服务器瘫掉。因此,适当设置 swap 分区还是很有必要的。

下面的命令主要是在 CentOS 上创建 4G 大小的 Swap 分区:

sudo dd if=/dev/zero of=/swapfile bs=512 count=8388616
sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
sudo echo "/swapfile swap swap defaults 0 0" >>/etc/fstab
sudo swapon -s sudo sysctl vm.swappiness=10 && sysctl vm.vfs_cache_pressure=80 sudo vi /etc/sysctl.conf

最后那一行,是指如果希望 vm.swappiness 和 vm.vfs_cache_pressure 的设置永久生效,可以修改 /etc/sysctl.conf 文件设置。

以上可参考阿里云的相关文章,如:https://developer.aliyun.com/article/52098

将 Angular 6 项目升级到 Angular 10

Angular 项目的升级说起来也简单,也复杂,也有踩坑的地方。

简单的方面就是,官方提供了升级的指导,访问 https://update.angular.io/ 选择项目的当前版本号和想要升级的版本号,就会出来相关的步骤作为指导。

复杂的地方就是,ng update 工具其实只能完成一部分工作,通常我们升级的过程中,会需要手工处理不少冲突与不兼容的地方,踩坑的地方也主要就在这里。

具体来说,有如下经验供参考:

1,不建议一次性从6 升级到 10,而建议逐个版本依次升级。因为每个版本迭代都可能有很多地方调整修改,涉及对应的依赖包版本的调整或者应用代码的修改。若一次跨多个版本升级,对于遇到问题时候的定位会造成困难。

有一点小细节需要注意:执行 ng update 升级成功并不代表代码一定能正常编译运行。因此,建议对于每个版本升级,都把代码启动起来测试,以验证升级是否成功。

2,注意各个依赖包的版本兼容问题

基本上每次升级,都伴随着对应的很多依赖库需要相应升级。ng update 能自动升级一部分,但不是全部。尤其是当你用了很多三方包的时候。一般 ng update 失败的话,常见的原因就是提示某某库依赖于另外某个库的某个版本,而你将要安装又是某个版本。这时候就需要对相关的包进行升级。

主要注意点是,对于版本的选择并非是最新版就最好,譬如当我们从 Angular 6 升级到 7 的时候,其实应选择的是最兼容当时 Angular 版本的包版本。譬如 Angular 7 发布于 2018年10月,而 Angular 8 发布于2019年6月,那么就可以在 npm 官方站查找所要升级的依赖包的版本历史,选择发布时间介于 2018年10月和2019年6月的则基本就可以。若仍然提示版本问题,则可以尝试其它版本。

3,Angular 8 升级到 Angular 9 遇到 typings.d.ts 中的类型定义未生效的问题,这个主要是在 Angular 9 中有了 tsconfig.app.json 文件,在 ng update 的过程中,该文件的对应配置错误导致,一般应修改如下内容:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
  "outDir": "../out-tsc/app",
  "baseUrl": "./",
  "types": []
  },
  "files": [
    "main.ts",
    "polyfills.ts"
  ],
  "include": [
    "**/*.d.ts",
    "typings.d.ts"
  ]
}

上面代码中最后那一项就是我们项目中丢失的,添加上去后,类型定义找不到的问题就解决了。

4,注意看官方提示,有些已经修改过的用法或者已经启用的用法就果断修改。这里主要涉及到两方面,一方面是 Angular 的变化,另一方面是 TypeScript 的变化。前者譬如 Angular 7  之后,废除了 @angular/http 而一律改用 @angular/common/http。又譬如 Angular 9 中,Module 定义中就去除了 entryComponent 的配置,所以删掉对应的语句即可。

5,有些三方库的写法发生了变化,有可能需要修改对应的代码。譬如我们项目中用到了 SweetAlert2 这个库。在升级到 Angular 10的时候,我们也将该库升级到了最新版本。这时候 ng update 升级整个项目虽然没遇到关于这个库的错误,但当运行项目的时候就遇到SweetAlert.fire()这个方法报错,具体来说,我们在这个方法的第一个参数里,有用 title 关键字来指定对话框的标题,但在新版本中,这个关键字改为了 icon。这是通过查找  SweetAlertOptions 的类型定义而知道的。所以依次修改掉即可。

总结下来,整个过程一要参考官方指导,二要认真研究出错信息,找到对应出处,其实问题都不难解决。

整个升级前后跨越了五天左右的时间,头三天花的时间较多,主要是各种踩坑及各种试错。由于有洁癖,后两天再花了一点点时间,用摸索出来的最合理的办法,将升级过程重新做了一遍。

最后,看一下漂亮的提交历史:(其实每个版本的升级都开了分支,每个升级其实并不止一次 commit,为了整洁,在 merge 回来的时候加了 –squash 参数)

熟悉的相随

于人群中多看你一眼
初见 就如已相识千年
宿命于此刻 撞进胸口
把几辈子的记忆泛起在心头

我曾一次次的随您同行吧?
上路,相遇,前路漫长
在多少世的轮回里
如影 相随
共赴远方

而再见是陌生的脸庞
心却说那是熟悉的模样
眼里升起闪烁的星星啊
那又是哪一世思念的感伤
再湿了眼眶