阅读视图

发现新文章,点击刷新页面。

不用 eas-cli 编译 React Native (Expo) 应用的 Android 版本

为什么不用 eas-cli

  • 它需要你注册 Expo 帐号,并且建立一个新的 Project 在上面
  • 它每次运行都要连接 Expo 的云服务
  • 它不让你在 Windows 上跑。气抖冷!!

(当然如果以上方面对你不是问题的话,eas 其实挺适合懒人在自己的电脑/基础设施上跑的,特别是你不在内地的电脑上进行开发时)

需求

  • 配好 JDK 和 Android SDK,并且指定好它们的 PATHJAVA_HOMEANDROID_HOME详见
  • 项目启用 Prebuild

配置 Keystore

为了让编译出来的应用使用我们自己的 Keystore,我们需要修改 android/app/build.gradle 文件,在 signingConfigs 加入自己的 Keystore 信息,并且在 buildTypesrelease 中设置 signingConfig signingConfigs.release

由于我们的项目使用 Expo Managed Workflow,为了确保 Native 部分不会出现不同步的问题,这里通过编写项目级插件的方式实现修改:

// plugins/withAndroidSignature.js

const { withAppBuildGradle } = require("@expo/config-plugins");
const fs = require("fs");
const path = require("path");

module.exports = function withAndroidSignature(config) {
    return withAppBuildGradle(config, config => {
        if (config.modResults.language === "groovy") {
            config.modResults.contents = setAndroidSignature(config.modResults.contents);
        } else {
            throw new Error("如果不是 groovy,则无法在 app/build.gradle 中设置 signingConfigs");
        }
        return config;
    });
};

function setAndroidSignature(appBuildGradle) {
    if (!fs.existsSync(path.resolve(__dirname, "../credentials.json"))) {
        console.warn("警告:没有设置正式版本的 Android Keystore 文件,因为 credentials.json 不存在。");
        return appBuildGradle;
    }
    const info = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../credentials.json"), { encoding: "utf8" }));

    // 使用正则表达式插入签名信息
    let output = appBuildGradle.replace(
        /(signingConfigs\s*\{)/,
        `$1
        release {
            storeFile file(${JSON.stringify(path.resolve(__dirname, "../credentials/android-release.keystore"))})
            storePassword ${JSON.stringify(info.android.keystore.keystorePassword)}
            keyAlias ${JSON.stringify(info.android.keystore.keyAlias)}
            keyPassword ${JSON.stringify(info.android.keystore.keyPassword)}
        }`,
    );

    // 使用正则表达式替换 signingConfig
    output = output.replace(
        /(release\s*\{)[^}]*?signingConfig\s+signingConfigs\.debug/s,
        `$1
            signingConfig signingConfigs.release
`,
    );

    return output;
}

为了方便维护,我们将 Keystore 的本体放在项目根目录下 credentials/android-release.keystore ,而 Keystore 的信息放在 credentials.json 里。

(这里的 credentials.json 文件格式与 Expo 文档中的 Use local credentials 一致)

{  
  "android": {  
    "keystore": {  
      "keystorePath": "credentials/android-release.keystore",  
      "keystorePassword": "your keystore password",  
      "keyAlias": "your key alias",  
      "keyPassword": "your key password"  
    }  
  },  
  "ios": {  
    "provisioningProfilePath": "ios/certs/profile.mobileprovision",  
    "distributionCertificate": {  
      "path": "ios/certs/dist-cert.p12",  
      "password": "password"  
    }  
  }  
}

然后,在 app.json 中指定这个项目级插件:

{  
  "expo": {  
    "plugins": [  
      "./plugins/withAndroidSignature"
    ]
  }  
}

编译

首先,我们需要执行一次 npx expo prebuild,让它生成 android 文件夹。这个文件夹包含了我们项目的 Native 部分。

打包给 Google Play 商店用的 .aab 文件:

cd android
./gradlew bundleRelease

你的编译产物会出现在项目根目录下 android/app/build/outputs/bundle/release/app-release.aab

而如果是给别人直接安装的 .apk 文件:

cd android
./gradlew assembleRelease

你的编译产物会出现在项目根目录下 android/app/build/outputs/apk/release/app-release.apk

如果在 Windows 下编译,需要把 ./gradlew 改成 ./gradlew.bat

还有一些东西……

为了简化上述的操作,我推荐在 package.json 中设置好相应的脚本。

{
  "scripts": {
    "prepare": "pnpm run clean && expo prebuild",
    "build:android": "cd android && ./gradlew assembleRelease",
    "clean": "node -e \"const opt = { recursive: true, force: true }; fs.rmSync('./ios', opt); fs.rmSync('./android', opt)\""
  }
}

另外,不要忘了把这些东西加到你的 .gitignore 里:

/credentials/*.keystore
/credentials.json
  
/ios
/android

这篇博文受到了上面那个「eas-cli 不能在 Windows 跑」的 Issue 中 这篇回复 的启发,我对于其中的操作流程进行了一些改进,让它更适合 Expo Managed Workflow。

【更正版】简单实现类型安全的、能触发 CustomEvent 的 EventTarget

我想写一个 TypeScript 类,这个类提供一系列的事件可供监听。为了实现类型安全,改进开发体验,我自己研究了一下,实现了一个可以以泛型输入所有可能的事件类型的 TypedEventTarget 类。

经过 JackWorks 的指正,代码改成了这样。

typed-event-target.d.ts 文件内容:

export class TypedEventTarget<T> extends EventTarget {
    // 这个类型体操是我从 `lib.dom.d.ts` 抄的我会乱说(
    addEventListener<K extends keyof T>(
        type: K,
        listener: (this: TypedEventTarget<T>, ev: TypedCustomEvent<K, T[K]>) => any,
        options?: boolean | AddEventListenerOptions,
    ): void;
    addEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | AddEventListenerOptions,
    ): void;
    removeEventListener<K extends keyof T>(
        type: K,
        listener: (this: TypedEventTarget<T>, ev: TypedCustomEvent<K, T[K]>) => any,
        options?: boolean | EventListenerOptions,
    ): void;
    removeEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | EventListenerOptions,
    ): void;
    dispatchEvent<K extends keyof T>(event: TypedCustomEvent<K, T[K]>): void;
}

export class TypedCustomEvent<S, T> extends CustomEvent<T> {
    constructor(type: S, eventInitDict?: CustomEventInit<T> | undefined);
}

typed-event-target.js 文件内容:

// 这里我们小小的欺骗了一下 tsc。
// 在运行时下,这个 EventTarget 其实就是原来的 EventTarget,
// 而非从 EventTarget 继承出来的类。这样可以避免非必要的性能开销。
export const TypedEventTarget = EventTarget;
export const TypedCustomEvent = CustomEvent;

将这两个文件同时放在一个合适的目录下,就搞定了!

使用方法

我们想要写一个 Person 类,这个类有 nameChangeageChange 这两个自定义事件。那么,我们可以这么写:

import { TypedEventTarget, TypedCustomEvent } from "@/utils/typed-event-target"; // 请根据项目实际情况修改路径

// 创建 Person 类的事件表
export interface PersonEventMap {
    nameChange: string;
    ageChange: number;
}

export default class Person extends TypedEventTarget<PersonEventMap> {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        super();
        this.name = name;
        this.age = age;
    }

    setName(name: string) {
        this.name = name;
        // 在使用 dispatchEvent 时,如果类型不正确,会出现错误
        this.dispatchEvent<"nameChange">(
            new CustomEvent("nameChange", {
                detail: name,
            }),
        );
    }

    setAge(age: number) {
        this.age = age;
        this.dispatchEvent<"ageChange">(
            new CustomEvent("ageChange", {
                detail: age,
            }),
        );
    }
}

调用这个类时,我们绑定事件也会有正确的补全提示和类型检查。

本博文的上一个版本

我想写一个 TypeScript 类,这个类提供一系列的事件可供监听。为了实现类型安全,改进开发体验,我自己研究了一下,实现了一个可以以泛型输入所有可能的事件类型的 TypedEventTarget 类。

废话不多说,直接上代码。

typed-event-target.d.ts 文件内容:

export default class TypedEventTarget<T> extends EventTarget {
    // 这个类型体操是我从 `lib.dom.d.ts` 抄的我会乱说(
    addEventListener<K extends keyof T>(
        type: K,
        listener: (this: TypedEventTarget, ev: T[K]) => any,
        options?: boolean | AddEventListenerOptions,
    ): void;
    addEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | AddEventListenerOptions,
    ): void;
    removeEventListener<K extends keyof T>(
        type: K,
        listener: (this: TypedEventTarget, ev: T[K]) => any,
        options?: boolean | EventListenerOptions,
    ): void;
    removeEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | EventListenerOptions,
    ): void;
    dispatchEvent<K extends keyof T>(event: T[K]): void;
}

typed-event-target.js 文件内容:

// 这里我们小小的欺骗了一下 tsc。
// 在运行时下,这个 EventTarget 其实就是原来的 EventTarget,
// 而非从 EventTarget 继承出来的类。这样可以避免非必要的性能开销。
export default EventTarget;

将这两个文件同时放在一个合适的目录下,就搞定了!

使用方法

我们想要写一个 Person 类,这个类有 nameChangeageChange 这两个自定义事件。那么,我们可以这么写:

import TypedEventTarget from "@/utils/typed-event-target"; // 请根据项目实际情况修改路径

// 创建 Person 类的事件表
export interface PersonEventMap {
    nameChange: CustomEvent<string>;
    ageChange: CustomEvent<number>;
}

export default class Person extends TypedEventTarget<PersonEventMap> {
    name: string;
    age: number;
    
    constructor(name: string, age: number) {
        super();
        this.name = name;
        this.age = age;
    }
    
    setName(name: string) {
        this.name = name;
        // 在使用 dispatchEvent 时,如果类型不正确,会出现错误
        this.dispatchEvent<"nameChange">(
            new CustomEvent("nameChange", {
                detail: name,
            }),
        );
    }
        
    setAge(age: number) {
        this.age = age;
        this.dispatchEvent<"ageChange">(
            new CustomEvent("ageChange", {
                detail: age,
            }),
        );
    }
}

调用这个类时,我们绑定事件也会有正确的补全提示和类型检查:

类型提示
在调用 addEventListener 事件时,会提供准确的名称提示。
类型提示
调用事件的详情内容时,推导出来的类型也是准确的。

在 IE11 使用 CSS Grid 实现多列卡片列表布局

我们维护的某网站需要多列布局的、分页的、内容高度不固定的卡片列表,效果如下:

网格
网格布局的效果。它们的内容高度均不一致,但是在网格中必须做到视觉高度一致。卡片本身的代码是从 Adam Wathan 的传教博文 抄的,感激不尽(

实现这种网格列表,其实使用 CSS Grid 是最为科学的方案,因为它灵活、好用、易于理解,同时对于这种内容不定高的多列卡片列表非常友好:

.grid-list {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 20px;
}

但是有一个棘手的问题是:这个网站的浏览器兼容性要求是 Chrome 60+ 和 IE11。没错,都 2022 年了,还有苦逼开发者(比如我)还需要想办法让网站能在 IE11 正常工作。

不过幸运的是,通过对 CSS Grid 兼容性 的查询,我们可以得知:

  • IE10+ 支持旧版的规范,且需要添加 -ms- 的前缀
  • Chrome 57+ 开箱支持

根据前辈的指引,我意识到在 IE11 中用 CSS Grid 实现这种网格列表是完全没问题的,只是需要一些技巧。

我们的项目使用到了 SCSS、PostCSS 和 Autoprefixer,所以,首先在 postcss.config.js 中声明使用针对 IE11 的 CSS Grid 转译:

module.exports = {  
    plugins: {  
        autoprefixer: {  
            grid: "autoplace"
        }  
    }  
};

然后,把这个 mixin 丢进项目中:

@mixin gridList($maxRow, $column, $rowSize: auto, $columnSize: 1fr, $gapX: 0, $gapY: 0) {
    @for $i from 1 through $maxRow {
        &--grid-#{$i} {
            display: grid;
            grid-template-columns: repeat(#{$column}, #{$columnSize});
            grid-template-rows: repeat(#{$i}, #{$rowSize});
            gap: $gapY $gapX;
        }
    }
}

然后在需要的地方使用这个 mixin(假设我们的网格列表一页最多有 9 项,每列有 3 项,即最多会有 3 行):

.grid-list {
    padding: 30px;
    @include gridList(3, 3, $gapX: 30px, $gapY: 30px)
}

嗯,那么这个 mixin 是干什么的呢?

我们前面提到了 IE11 支持旧版的 CSS Grid 规范,而在该版本的规范中,必须显式声明你会用到的行数和列数;而且它并不支持 gap 属性。

那么,对于需要显式声明行数和列数的问题,我们可以换一个思路:借助 SCSS 函数的力量,把所有可能的卡片列数的 CSS 都生成出来;然后在 HTML 部分,由模板渲染器告诉页面一共有多少行。

以下是 mixin 生成的代码:

.demo-list {
  padding: 30px;
}

.demo-list--grid-1 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(1, auto);
  gap: 30px 30px;
}

.demo-list--grid-2 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(2, auto);
  gap: 30px 30px;
}

.demo-list--grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, auto);
  gap: 30px 30px;
}

这样,我们就得到了行数有一行、两行和三行时所对应的 CSS 类。我们只要在生成模板时,使用对应的 CSS 类即可:

<!-- 以下伪代码仅供参考,实际使用时请替换为你使用的模板引擎/界面库的语法 -->
<ul class="demo-list demo-list--grid-{{ Math.ceil(Page.PageList.length / 3) }}">
	{%- for PageItem in Page.PageList -%}  
		<li class="demo-card">
			<!-- 略 -->
		</li>
    {%- endfor -%}
</ul>

IE 使用的旧版规范属性是以 -ms- 开头的,同时我们还需要通过空白的网格来模拟 gap 属性。不过,Autoprefixer 可以帮我们进行转译:

.demo-list {
  padding: 30px;
}

.demo-list--grid-1 {
  display: -ms-grid;
  display: grid;
  -ms-grid-columns: 1fr 30px 1fr 30px 1fr;
  grid-template-columns: repeat(3, 1fr);
  -ms-grid-rows: auto;
  grid-template-rows: repeat(1, auto);
  gap: 30px 30px;
}

.demo-list--grid-1 > *:nth-child(1) {
  -ms-grid-row: 1;
  -ms-grid-column: 1;
}

.demo-list--grid-1 > *:nth-child(2) {
  -ms-grid-row: 1;
  -ms-grid-column: 3;
}

.demo-list--grid-1 > *:nth-child(3) {
  -ms-grid-row: 1;
  -ms-grid-column: 5;
}

.demo-list--grid-2 {
  display: -ms-grid;
  display: grid;
  -ms-grid-columns: 1fr 30px 1fr 30px 1fr;
  grid-template-columns: repeat(3, 1fr);
  -ms-grid-rows: auto 30px auto;
  grid-template-rows: repeat(2, auto);
  gap: 30px 30px;
}

.demo-list--grid-2 > *:nth-child(1) {
  -ms-grid-row: 1;
  -ms-grid-column: 1;
}

.demo-list--grid-2 > *:nth-child(2) {
  -ms-grid-row: 1;
  -ms-grid-column: 3;
}

.demo-list--grid-2 > *:nth-child(3) {
  -ms-grid-row: 1;
  -ms-grid-column: 5;
}

.demo-list--grid-2 > *:nth-child(4) {
  -ms-grid-row: 3;
  -ms-grid-column: 1;
}

.demo-list--grid-2 > *:nth-child(5) {
  -ms-grid-row: 3;
  -ms-grid-column: 3;
}

.demo-list--grid-2 > *:nth-child(6) {
  -ms-grid-row: 3;
  -ms-grid-column: 5;
}

/* 后略... you get the idea */ 

结论

经过一系列的努力,我们终于拥有了在 IE11 正确显示的多列卡片列表。好耶!

尽管业界正积极淘汰 IE11,但现实情况是,还有一些商业应用仍然有着对 IE11 的兼容需求。确实,兼容老旧浏览器是一个让人头疼的问题,然而通过变换思路,我们可以找到一些取巧的解决方案。

当然更好的方法是,提桶跑路,去给历史包袱不重的项目填坑(逃

后记

这篇博文本来是一篇在 2022 年私下写的笔记,因为一开始只是写给我自己看的,加上我想应该需要支持 IE11 的项目越来越少,一直没有发布。

但是现实总是骨感的,就连不久前发布了 4.0.0 BETA 的 jQuery 团队 都说要继续支持 IE11,直到 jQuery 5 再放弃支持。所以这篇博文大概还是能发挥余热吧。

自建云游戏服务的尝试(2024 更新)

为什么?

我最近经常玩《原神》,但是我:

  • 只有一台能带动这游戏的远程工作站,我还很难物理访问它
  • 平时在外面只能使用弱鸡的旧款 MacBook Pro
  • 用 iPhone 12 Pro 玩,感觉体验堪忧

显然,再为这么一款游戏而专门购买游戏本是不划算的,而且游戏本有着笨重、噪音大、外观普遍中二感爆棚、电池续航几乎不存在等缺点。

考虑到近年来 StadiaGeForce Now 等云游戏服务在国外的兴起,我有了新的主意:拿自己的远程工作站搭建云游戏服务。

本博文不是真正意义上的教程,但是记录了我对若干方案的尝试和使用体验。

环境

  • 服务端(工作站)和客户端都在重重 NAT 之下。现在不是问题了,家里和移动网络都有公网 IPv6,可以点对点直连。同样我也不再推荐直接 ECS 当跳板、承载游戏流量的方案了,因为流量费是真的奢侈啊 TAT
  • 有一台阿里云 ECS 做跳板机,安装有:
    • Wireguard(用于和不同的远程设备连接)
    • nginx(通过 stream 模块向公网转发端口)

RDP

在此之前,我经常会使用 RDP 来连接我的远程工作站。启动 RDP 服务不需要复杂的服务端设置,在网关机上也只要简单转发一个端口到公网即可。

其实 RDP 对于一般的应用程序是没有问题的,但是完全不适合打游戏:

Moonlight + Sunshine

我的工作站配备有 GTX 1650 Super,因此可以借助 GeForce Experience 的游戏串流(Streaming)功能和 Moonlight 客户端,轻松搭建自己的云游戏服务。

总体来说,在有合适的网络条件下,Moonlight 可以提供足够好的游戏体验。但是使用官方的 GeForce Experience 时,会有一些蛋疼的问题:

  • 配对时必须在远程主机上输入四位数字确认
  • 一旦连接过 RDP(因为我平时还要工作!),串流服务就会停止工作
    • 所以,上述的配对过程也不能在 RDP 进行,必须使用其它的远程桌面服务(如 Teamviewer);
      同时,每次连接过 RDP,都需要用 Teamviewer 再连接一次。

不过幸运的是,现在有 Sunshine 了。Sunshine 是一个兼容 Moonlight 的游戏串流服务端,支持带有视频硬件编码功能的 AMD、Intel 和 Nvidia 显卡,而且完美解决了上述的配对麻烦(用 Web GUI 就可以配对)、RDP 导致服务端罢工的问题。

对了,如果你在 Xiaomi HyperOS 设备上使用 Moonlight for Android,需要呼出虚拟键盘,得同时用三指按动屏幕,而且每根手指距离要有大约 2cm 以上。别问我是怎么知道的(躺

Parsec

Parsec 是一款与 Moonlight 类似的应用,但是对非技术导向的用户更加友好,而且解决了 Moonlight + Sunshine 的一些痛点:

  • 验证是基于他们自家帐号系统的(比在远程主机确认配对方便多了!)
  • 连完 RDP 以后可以直接连接
  • 灵活的连接方式,可以自动视情况通过局域网、NAT 打洞、Wireguard 等方式连接到服务端
  • 支持 AMD 和 Intel 显卡(虽然我现在用的是 Nvidia 显卡)

不过,Parsec 也有一些缺点:

  • 你只能使用 Parsec 他们家的帐号系统,而且他们的客户端比较黑箱。我不是很在乎就是了……
  • 视频质量并不算很好,就算你在 GUI 把码率开到最大也无济于事。不过,蚊子写过一篇博文,提到手动在配置文件中加上 encoder_min_bitrate = 50 就可以改善
  • Android 客户端不算那么好用,而且不支持手柄振动,也不支持将触摸屏当作笔记本触摸板使用。

让 GPU 一直工作

我在淘宝上随便买了个 HDMI 锁屏宝,解决了这个问题。但是不要买的太随便,因为如果锁屏宝模拟的显示器分辨率和刷新率不够高,那么想要更好的串流分辨率和帧率是没门的。

不过,也可以试试蚊子提到的 虚拟显示器

对比

总之,不同的连接方案有着它们的优缺点,所以还得视场合选择使用哪种方式连接到我的工作站。

服务/特性 RDP Moonlight + Sunshine Parsec
支持显卡 N/A 各种 各种
配置难度 中等
首次连接 输入本机登录信息 在服务端完成配对操作 输入 Parsec 登录信息
流量消耗 较高 中等
图形渲染位置 客户端 服务端 服务端
针对游戏优化
使用 RDP 后再连接 N/A 可直接连接 可直接连接
自动打洞
剪贴板共享 有(仅限文本)
开源软件 客户端/服务端均开源

自建云游戏服务的优越性

就是可以玩各种自己喜欢的游戏啦,而不只局限于云游戏厂商提供的可玩游戏。

不过,如果你没有长期使用 Windows 远程桌面的需求,而且只想玩一些大众游戏,那么类似于 网易云游戏平台腾讯云游戏 等服务都是可以考虑的。

Update: 如果你是原神国服玩家,还可以考虑一下官方的云・原神

流量费

别人花 648 抽优菈,我花 648 让游戏跑起来(笑

不过现在四大运营商都推出了便宜到爆炸、而且每月流量至少有 100G 的 5G 流量卡(不是物联网卡),同时通过与家里进行点对点连接,现在出门在外都可以省掉不少流量费了。

然后我靠着免费的原石抽到了她(

优菈

在 Termux 编译和使用 bwm-ng(需要 root)

bwm-ng 是一个很方便的命令行工具,可以实时监控操作系统的网速和磁盘读写速度。

我想在 Termux 使用它,但是目前 Termux 的软件源还没有,所以就只能自己编译安装了。

步骤

首先,准备好安装了 Termux,且已经 root 的 Android 手机。

然后,我们需要给 Termux 安装 root-repo,再安装 sudo

pkg install root-repo
pkg install sudo

安装 Git 和构建工具集:

pkg install git build-essential

接下来,就可以进行编译安装了,但是跟一般的编译安装有点区别:

git clone https://github.com/vgropp/bwm-ng
cd bwm-ng
sudo ./autogen.sh
sudo make

在这里,./autogen.shmake 命令的前面都增加了 sudo,这是为了让编译脚本能够正确识别到 /proc/ 下的特殊文件(在 Android 下,该目录只能由 root 用户访问)。如果不使用 sudo,会看到这样的报错:

checking for /proc/net/dev... no
checking for /proc/diskstats... no
checking for /proc/partitions... no
checking for sys/dkstat.h... no
checking whether cc and linker accepts -framework IOKit -framework CoreFoundation... no
checking for sg_get_network_io_stats,sg_get_disk_io_stats in -lstatgrab... no
configure: error: "NO INPUT FOUND"

编译完成后,将 src 目录下的 bwm-ng 二进制文件移动到合适的位置(一般在 $PATH 中指定过的路径),使用 sudo bwm-ng 就可以使用了。

  bwm-ng v0.6.3 (probing every 0.500s), press 'h' for help
  input: /proc/net/dev type: rate
  /         iface                   Rx                   Tx                Total
  ==============================================================================
      wifi-aware0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
             ifb0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
             sit0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
             p2p0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
    r_rmnet_data1:           0.00 KB/s            0.00 KB/s            0.00 KB/s
               lo:           0.00 KB/s            0.00 KB/s            0.00 KB/s
      rmnet_data2:           0.00 KB/s            0.00 KB/s            0.00 KB/s
      rmnet_data5:           0.00 KB/s            0.00 KB/s            0.00 KB/s
       rmnet_ipa0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
           dummy0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
      rmnet_data1:           0.00 KB/s            0.00 KB/s            0.00 KB/s
    r_rmnet_data2:           0.00 KB/s            0.00 KB/s            0.00 KB/s
      rmnet_data0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
            wlan1:         229.75 KB/s           10.00 KB/s          239.76 KB/s
      rmnet_data3:           0.00 KB/s            0.00 KB/s            0.00 KB/s
            wlan0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
    r_rmnet_data3:           0.00 KB/s            0.00 KB/s            0.00 KB/s
    r_rmnet_data0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
          ip_vti0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
          ip6tnl0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
            bond0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
             ifb2:           0.00 KB/s            0.00 KB/s            0.00 KB/s
             ifb1:           0.00 KB/s            0.00 KB/s            0.00 KB/s
       rmnet_mhi0:          24.65 KB/s          227.47 KB/s          252.12 KB/s
      rmnet_data4:          18.40 KB/s          231.23 KB/s          249.63 KB/s
         ip6_vti0:           0.00 KB/s            0.00 KB/s            0.00 KB/s
  ------------------------------------------------------------------------------
            total:         272.80 KB/s          468.71 KB/s          741.50 KB/s

如何在已有的 Vue CLI 项目使用 esbuild

EDIT: 其实可以试着用相同的方法把 Babel 换成 SWC,因为它最低可以编译到 ES5。不过我现在不需要负责这个兼容 IE11 的项目了哈哈哈哈

背景

我们的门户网站项目(Vue 2 / Vue CLI 5 / TypeScript / Element UI)前端部分编译所需时间太长,因此开始考虑在不对已有项目进行过于伤筋动骨的调整的前提下,提升编译速度的方式。

由于 Vite 使用了 esbuild 进行编译速度的提升,我们想到了一个主意:借助 esbuild-loader,把 Vue CLI 中的 Babel 替换为 esbuild。

分析

在项目根目录执行 npx vue-cli-service inspect,可以看到最终生成的 Webpack 配置中,以下部分涉及到了 babel-loader:

      /* config.module.rule('js') */
      {
        test: /\.m?jsx?$/,
        exclude: [
          function () { /* omitted long function */ }
        ],
        use: [
          /* config.module.rule('js').use('babel-loader') */
          // (略)
        ]
      },
      /* config.module.rule('ts') */
      {
        test: /\.ts$/,
        use: [
          /* config.module.rule('ts').use('babel-loader') */
          // (略)
          /* config.module.rule('ts').use('ts-loader') */
          // (略)
        ]
      },
      /* config.module.rule('tsx') */
      {
        test: /\.tsx$/,
        use: [
          /* config.module.rule('tsx').use('babel-loader') */
          // (略)
          /* config.module.rule('tsx').use('ts-loader') */
          // (略)
        ]
      }

由此可知,我们需要将 jststsx 的默认规则进行清空处理,然后让 jststsx 使用 esbuild-loader

操作

首先,安装 esbuild-loader

npm i -D esbuild-loader

然后,在 vue.config.js 下的 chainWebpack 中加入以下内容:

// 清空已有的使用 `babel-loader` 的规则
config.module.rule("js").uses.clear();
config.module.rule("ts").uses.clear();
config.module.rule("tsx").uses.clear();

// 注入使用 `esbuild-loader` 的新规则
config.module.rule("js")
    .test(/\.m?jsx?$/)
    .use("esbuild-loader")
    .loader("esbuild-loader")
    .options({
        loader: "jsx",
        target: "es2015"
    })
    .end();
config.module.rule("ts")
    .test(/\.ts$/)
    .use("esbuild-loader")
    .loader("esbuild-loader")
    .options({
        loader: "ts",
        target: "es2015"
    })
    .end();
config.module.rule("tsx")
    .test(/\.tsx$/)
    .use("esbuild-loader")
    .loader("esbuild-loader")
    .options({
        loader: "tsx",
        target: "es2015"
    })
    .end();

就可以了。

已知问题

  • 如果项目中有使用到 Web Worker,肯定会炸掉,出现 TypeError: Failed to construct 'URL': Invalid URL 错误。采用 Babel 则正常工作。

遗憾

尽管 esbuild 是好文明,我们依然无法在那个门户网站项目中使用;因为我们项目要求兼容 IE11,而 esbuild 目前最低只能编译到 ES2015,这就导致编译产物无法在 IE11 等不(完全)支持 ES2015 的浏览器中运行;不过,在本地调试时还是很爽的,因为开发环境冷启动和热更新速度确实快了不少。

不过,如果你的项目不需要兼容 IE11 等老浏览器,但恰好还没有升级到 Vue 3 + Vite,完全可以试一试。能快一点是一点啊)

我的 2021

于是,2021 年猝不及防的结束了。虽然我很多时候懒得写这种总结,但是考虑到我还有一个博客在运行,还是写一篇总结好了。

设备添置

今年,我利用自己的收入,给自己添置了很多新的装备。其中两件最重要的是:

同时,今年我把我的 Homelab 建设了起来。我通过购买一些全新/二手设备和配件,完成了我业余机房的基础建设。

功能上,可以分为图形虚拟化服务器、Linux 虚拟化服务器、云游戏服务器、NAS 和软路由;同时,还借助一台阿里云 ECS 实现端口转发、VPN 等功能。当然,这样的业余机房也不是没有代价的;一个是噪音比较大(就这我还没考虑机架式服务器),还有一个是电费要比以前高出不少(好在内蒙古的电价相对还是很低的)。但是,这套 Homelab 极大的改善了我(自认为是 Power User)的 IT 生活体验,包括云游戏也可以让我在任何地方享受家里安装的 PC 游戏。

说到 PC 游戏,其实这一块一直是我最缺乏了解的领域。我主要的游戏经历都是在家里的山寨 FC、后来(打着 MP4 旗号)的模拟器机、智能手机,以及 Nintendo Switch 上;由于以前家里的 PC 配置并不理想,加上父母的态度对 PC 游戏并不友好,我在 PC 上玩过的游戏寥寥无几(不过包括 Minecraft)。其实我开始涉足云游戏,只是希望拥有更好的远程桌面体验;虽然今年显卡价格蹭蹭蹭的上涨,但是因为我去年恰好买了张索泰的 GTX 1650 Super 当亮机卡(迷惑行为),我误打误撞的就开始玩起了《原神》,结果我一发不可收拾,还注册了 Steam 账号,开始研究我还能在 PC 上玩些什么有趣的游戏。因为就算是 1650 Super 这种入门级游戏显卡,它带来的游戏体验也是碾压对我来说的传统游戏设备(至少现今),而且键鼠操作也让我感觉更加灵活、舒适。

不过我也发现很多 PC 游戏都有着烦人的 DRM 和反作弊措施 (aka rootkit)。虽然我不认为我是什么隐私怪,但这让我感觉并不太爽;同时因为我工作站的十代 i7 算不上玩游戏的最佳选择,我从群友那边收了一套 B450 + Ryzen 5 3600 的套装,用手上的一些东西组装了专用的云游戏服务器。结果我的 1650 Super 反而成瓶颈了(

兼职

随着我不断接近毕业,学校的事情也变得越来越少。于是今年年初,在 @qwe7002 的介绍下,我开始利用空闲时间做一些前端开发的远程兼职,并陆续参与了几个项目:

  • 某微信小程序(2 月至 4 月)。这个小程序是经过了几个外包开发者之手,代码逻辑混乱不堪。我经过不懈努力,成功的重构了一小部分浑浊不清的代码,还写了几个新的页面。不过这个项目的经历也让我明白 TypeScript 对于这种多人合作的大型 JS 项目真的很关键,同时对小程序的好感进一步受损,因为这玩意即使是对开发者也很不友好啊。
  • 某 ERP 系统前端(3 月至年底)。这一次,我和另外几位小伙伴,从零开始了一个比较庞大的 Vue 2 项目。我参与了整个项目的选型和脚手架搭建工作,同时意志坚定的选择了 TypeScript(虽然代码里还是有一大坨 any。这个项目还是让我感觉非常愉悦的,因为毫无大型商业项目开发经验的我们居然成功的把项目做了出来,积累了不少经验;而且,还成功让我的小伙伴们掌握了 TypeScript 的基本使用(他们以前都只有 JavaScript 的使用经验);而最重要的是,我拿到了一笔非常可观的报酬。

12 月,我顺利的完成了毕业设计和毕设答辩,进入实习阶段。2022 年起,我将在福州开始我人生中的全新篇章——成为社畜。虽然不知道以后还会发生什么,但是我对我的未来还是充满期待的。

博客

今年我没有对自己博客花很多的时间。不过,我的博客一年来运行基本正常,没出幺蛾子,还是可喜可贺的。

以及,我开始用 Golang 重构我博客的评论系统了。原来写的 Node.js 后端虽然能用,但是算上依赖以后的体积非常臃肿,而且非常吃内存(要知道我的 VPS 也就是 1c1g 的配置);同时,我想找个东西来练练 Golang,所以就拿自己博客的评论系统开刀了。

目前还没有写完,但是到时候大概可以给我可怜的 VPS 节约不少内存吧。

2022……?

2022 年对于我来说会是变化最大的一年,因为我的身份要从学生变成社畜了,同时以后要开始在南方城市常年驻扎。所以未来的事情,只能说走一步看一步吧。

听某些人说,2022 年意味着 2020 年的重复,因为 2022 年拿英文读听起来像 Two thousand twenty too(也是 2020 年)。2021 年的世界依然不太平,所以只能希望 2022 年不要真的是这样,以及第三次世界大战永远不要发生了(至少在我的整个人生中)。

最后,祝大家新年快乐。

npm、镜像源与 package-lock.json

在国内开发涉及 Node.js 的应用都知道,裸连官方的 https://registry.npmjs.org 非常慢,等待时间令人捉急。

解决这种问题,我们自然想到的就是找镜像源(就像 Linux 发行版的包管理器那样)。

国内目前已经有一些 npm 镜像源,大多数情况下,它们其实还是可以用的。但是,package-lock.json 中会记录下各个包的原始 URL:

{
    "node_modules/@babel/compat-data": {
        "version": "7.16.0",
        "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/compat-data/-/compat-data-7.16.0.tgz",
        "integrity": "sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==",
        "dev": true,
        "license": "MIT",
        "engines": {
            "node": ">=6.9.0"
        } 
    },
    "node_modules/@babel/core": {
        "version": "7.16.0",
        "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/core/-/core-7.16.0.tgz",
        "integrity": "sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ==",
        "dev": true,
        "license": "MIT"
    }
}

其实如果项目仅仅在国内开发和编译,问题不会很大。但是如果你开发的是开源项目(特别是面向海外的)或者在国外服务器跑 CI,你的项目就有可能会遇到安装依赖时连接不稳定的问题(因为国内这些镜像源通常不会针对国外优化)。

对此,我的建议是:

  • 尽量避免使用镜像源。
    • 进行肉翻。
    • 对于连接缓慢的问题,可以给 npm 设置代理,或者利用国外 VPS 为 npm 自建 SNI Proxy,然后在本地修改 hosts 文件/进行 DNS 劫持。
  • 如果一定要使用镜像源,则需要:
    • 考虑清楚你的项目是否将完全在国内开发,并让团队中所有人使用相同的镜像源。
    • 尽量选择能够通过 sed 简单替换 package-lock.json 中所记载地址的镜像源(例如华为源、中科大源)。

新的桌面工作站:HP Z2 G5 Tower

我的上一代桌面工作站是台戴尔的 Inspiron 660,购于 2013 年初。经过数年来的多次升级,它拥有了三代 i7 处理器、16GB 内存和固态硬盘。

其实这台机器的性能直到今天依然不算逊色,基本上可以满足日常使用。但是,因为一些个人原因,我开始日常需要开启多个虚拟机,于是这台机器的 CPU 就开始有些吃不消了。

考虑到这台机器已经有 8 年历史,于是我便决定让它退居二线,同时将主力工作站进行升级换代。

经过反复的挑选,我最终选中了惠普的 Z2 G5 Tower。它在这些方面非常吸引我:

  • 免工具维护
  • 极高的可扩展性,有着合理的升级路线
  • 没有光污染,但是不失颜值的机箱箱体

在我购买这台机器时,我其实已经听说了 11 代酷睿系列处理器即将发布的消息,但是我这台机器预期服役 5 年以上,感觉差上一代应该问题不大。

以及我买的配置其实听起来就感觉有点尴尬:i7-10700 / 8GB DDR4 2666 x1 / 2TB 机械硬盘,不过我自己已经有一些配件了,可以直接换上去用。

最终,成功在国内商家以 6.8k 拿下。

安装

因为卖家发的是顺丰,所以我下单以后第三天就到了。毕竟在内蒙古这种地方,这个速度已经算是很快了。

包装

机箱的颜值确实不错,不过感觉比想象中的要略小一点。

  • 正面 IO:
    • 2 个 USB 3.1 Gen1
    • 2 个 USB 3.1 Gen2
    • 耳麦接口
  • 背面 IO:
    • 2 个 USB 2.0
    • 2 个 USB 3.1 Gen1
    • 2 个 USB 3.1 Gen2
    • 1 个 RJ-45(I219-LM,千兆以太网)
    • 线路输入和线路输出
    • 2 个全尺寸 DisplayPort
    • 1 个 VGA (Flex IO modules)
机箱前面 机箱后面

打开机箱也非常简单,只要扳动背面右侧的黑色开关即可。

初见机箱内部

后盖上粘贴的贴纸介绍了主板各个部位的用途。

后盖贴纸

中间那个横着的玩意是……显卡风扇。应该是辅助散热的,不过也给我后面的显卡安装带来了一些麻烦(见后文)。

显卡散热器

工作站的主板。这块主板的可扩充性的确非常丰富,主要亮点有:

  • 4 条 DDR4 内存插槽
  • 4 个 SATA 3 接口
  • 两个 M.2 M Key 插槽(只支持 2280 尺寸)和一个 M.2 E Key 插槽(我这台已经预装了 AX201)
  • 4 个 PCI-E 插槽

顺便主板是前后一体贯通的,而且是非标准螺丝孔位,所以是没法更换市售主板的。不过对我来说无所谓啦。

主板

传说中的 Flex IO modules;我这台配的是 VGA 输出。

Flex IO modules

预装的 AX201 网卡,支持 Wi-Fi 6 和蓝牙 5.0。

AX201 网卡

主板的 PCI-E 插槽,配置如下:

  • 1 个 PCI-E x16
  • 2 个 PCI-E x4(x1 信号)
  • 1 个 PCI-E x16(x4 信号)

PCI-E 插槽

其它配件

我的附带了一个笔记本尺寸的 DVD-RW 驱动器,不过还预留了一个空的 5.25 寸扩展槽。

DVD-RW 驱动器

700W 的电源适配器,有着 80 Plus 铂金认证。预留了两条 6+2 的显卡供电。

虽然是非标准的电源,不过使用一些高端显卡应该是绰绰有余。

Edit: 有 Telegram 群友指出这个电源适配器应该是符合 ATX12VO 标准的。电源参数看起来确实如此,但是实际上孔位和尺寸跟正常的 ATX 电源完全对不上,而且电源接口也是非标准的。

电源适配器

三星 8G DDR4 2666 内存。

内存

东芝 DT01ACA200 机械硬盘(2TB / CMR);拆下来以后被我塞进 NAS 里了。

机械硬盘

硬盘托架;这台机器可以安装两块 3.5 寸硬盘。

硬盘托架

其它附带的东西

包装中除了主机本体,还有小册子、品字电源线、键盘和鼠标。

其它物品

附带的键盘和鼠标;朴实无华,但是手感还算说得过去。

鼠标 键盘

安装我自己的配件

随后,我安装了我自己已有的一些配件:

  • 显卡:ZOTAC GTX 1650 Super (4GB GDDR6)
  • NVMe SSD:铠侠 RC10 500GB
  • 内存:英睿达 16GB DDR4 2666

但是安装显卡时,我发现我扣不上机箱的显卡散热器。仔细一看,发现了问题所在:我的显卡的供电口位置很尴尬,恰好和散热器冲突。

显卡散热器问题

所以,我只好先把固定散热器(?)的那个黑色的东西拆下来了,然后才有了足够的空间。拆下来以后看起来问题不大,机箱的显卡散热器还能固定住的样子。

那么把整个散热器都拆掉呢?我试了,结果机器在 POST 就会报错,抱怨风扇出现了问题。

理论上来说,我可以装个假负载,来让主板认为风扇在正常工作;但是这台机器的风扇接口也是非标准的,就有点尴尬。

使用体验

安装完毕以后,顺利开机,没有任何问题。

得益于 24G 的内存、8 核 16 线程的处理器和 Hyper-V,整体的体验是滑溜溜的,开 3 个 Windows 虚拟机都压力不大。

hwinfo 信息

CPU

2021 年 4 月更新:因为发现 500G 的存储空间有点捉襟见肘,再加上 Chia 挖矿潮即将兴起,所以提前把固态硬盘换成了建兴 T10 240GB + 铠侠 RC10 1TB 的组合。很爽。

总结

感觉这台机器买的很值,相比我 8 年前的旧电脑,是一次非常巨大的飞跃。开心!

顺便一提,我在购买这台机器时,有考虑上一块更好的显卡,但是现在显卡实在是贵的太离谱了,就先把之前买的 GTX 1650 Super 装上了。(摊手

我的 2020

好耶,我终于把 2020 年勉强过完了。

成就

我在 2019 年终总结里写道:

待定!计划赶不上变化的。

结果,受到 COVID-19 疫情的影响,今年的大计划其实几乎都泡汤了(包括计划参加的一个比赛、2020 年夏天的面基出游计划等)。

所以,总体上感觉今年是碌碌无为的一年。不过没办法……

买买买

不过今年买到了不少很棒的新硬件:

  • WI-1000XM2
    听感很好,佩戴起来相当舒适,而且 10 个小时的续航还是很香的。
  • 自组装台式机
    我第一次亲自从头开始组装的台式机,虽然有些配件选择上的遗憾,不过点亮后还是很有成就感。
  • 一加 8T
    强大的骁龙 865、65W 快充(虽然是私有协议)、滑溜溜的 120Hz 屏幕、很强的可折腾度……用起来太爽了!
  • AirPods Pro
    出门使用还是很方便的,包括超级简单的多设备切换功能!

今年并没有购买很多专辑,不过还是感觉这些是很不错的:

博客

今年的确没有写很多博客,不过还是做了不少工作,让它「符合潮流」:

  • 将评论系统后端用 TypeScript 进行了重构,修正了很多错误
  • 给自己博客增加了暗色模式,并支持跟随系统切换
  • 给自己写了一套全新的卡片式博客主题,使用了新的设计语言

2021 年计划?

在 Intel 版 MacBook Pro 以 EFI 的形式安装 Windows 10

为什么?

最近我想玩 Minecraft Windows 10 Edition,但是我在学校,我自己的电脑只有一部 MacBook Pro 2017。

那为什么不用 Bootcamp 呢?

因为我也不知道为什么,只有无尽的黑屏与 Windows 10 安装向导初始化的画面。我无法开始安装。

而且 Bootcamp 还有其它的缺点,具体可以看 这里

顺便少数派那个教程有点过时了,我在我的新款 MacBook Pro 安装时撞了些坑,所以决定写个新教程,造福人类。

准备材料

  • Windows 10 ISO
  • UNetbootin
  • 8G 以上的 U 盘(建议使用 USB 3.0 的)
  • USB Hub(建议使用 USB 3.0 的)
  • 支持 Windows 的键鼠套装(在安装完整的 Bootcamp 驱动以前,内置键盘和触摸板用不了的)
  • 机智的你

如果你使用的是带 T2 芯片的新款机型,请先根据 官方说明 允许通过外部 USB 设备启动。

我的安装环境

  • 机型:MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports)
  • 操作系统:macOS 10.13.5 (17F77)
  • 磁盘:256GB,只有 macOS 分区

分出 Windows 分区

假如你打算留下 macOS,请使用这个命令调整 macOS 分区大小:

diskutil apfs resizeContainer disk1 200GB    # 200GB 指你给 macOS 留的分区大小

这些数值可以酌情调整。但是,请确保有足够的剩余空间。

制作 Windows 10 安装盘

下载好 Windows 10 ISO,插上你的 U 盘,然后先格式化一下:

格式化 U 盘

我们把 格式 设置为 MS-DOS (FAT)方案 设置为 主引导记录

然后我们打开 UNetbootin,选择 Diskimage,打开我们准备好的 Windows 10 ISO。下面的 Type 当然选择 USB Drive,然后将 Drive 设置成你要制作安装盘的 U 盘(如果不确定的话,可以把主机上所有其它 U 盘和读卡器什么的都拔掉,这样就只有一个选项了)。

UNetbootin

然后点击 OK,耐心等待写入完毕就是了。

写入过程

在写入完毕后,我们打开 启动转换助理,选择 操作 => 下载 Windows 支持软件,并将保存位置设为我们的安装盘的根目录下。接下来我们会需要的。

下载即将结束时会向你请求权限,这是正常的,直接输入密码确认就行了。

下载 Windows 支持软件

如果你尝试过 Boot Camp 安装

如果你没有尝试过 Boot Camp 安装,请跳过这一节!

直接照着 这篇帖子 里面的方法做就行。

以及你需要先把 SIP 暂时关掉。

开始安装

把你的 U 盘、键盘和鼠标插上你的 USB Hub,并连接 MacBook。

重启你的 MacBook 并按住 Option 键,你会看到有好几个磁盘的选项。按方向键选择 黄色图标的 EFI Boot,然后回车。

然后先按提示一路走下去,然后到了选择磁盘这一步,你会发现没有磁盘可选。这是正常的,我们还需要加载驱动程序。

我们点击 加载驱动程序,再点击 浏览,找到 C:\WindowsSupport\$WinPEDriver$\AppleSSD64,确定。然后点击下一步。

稍后,我们就可以看到我们的磁盘分区了。我们按照正常的方法创建好分区,然后继续一路向前就是了。

安装驱动

当我们进入安装好的 Windows 10 以后,找到你安装 U 盘下的 WindowsSupport\BootCamp 文件夹,运行里面的 setup.exe,安装驱动程序,然后重新启动就是了。

然后就大功告成了,你的内置键盘、触摸板等一系列硬件都可以使用了。

(在我设备上的)已知问题

  • Windows 睡眠时间过长以后,你需要经过完整的开机过程才能还原。
  • 每次返回 macOS,你恐怕都需要按住 Option 键手工选择 macOS 分区。在设置里改 启动磁盘 不管用。
  • 蓝牙工作异常

为什么大家都在安利这一样东西?

大概无数次我在各大群组里(看见别人)求某种东西的推荐,然后群友们就异口同声的说:「XXX(指同一件东西)不香吗?」

但是这样的安利反而让我感觉不安,因为:

  • 一种东西肯定有它自己的优缺点,但是在那些群友的口中,给人一种 XXX 非常完美的错觉。
    真的,跑分不能说明一切。
  • 安利这种东西的群友中,大概率有相当一部分是它的 fanboy,然后如果你提出不太一样的安利,大概会跟你争个面红耳赤,甚至引起严重的不愉快。
    好吧,我大概是已经屈服于这种「政治正确」了。
  • 有的群友可能只是云用户,在瞎起哄而已。

所以为什么通常情况下,我越来越不想在群聊中为这种事情浪费时间了。我宁可自己问问 Google 娘。

新的家庭服务器:MicroServer Gen10

我的上一代家庭服务器是一台技嘉的 GB-BXBT-2807。其实作为家庭服务器来说,它是个还行的选择,但是内部只能安装一块 2.5 寸 9.5mm 以下的 SATA 硬盘,而且只有一个 USB 3.0 和两个 USB 2.0 接口。

我家的上传速率其实还是可以的,而随着我开始尝到私有云的甜头,我陆续增加了三块硬盘。然而因为它只有一个 USB 3.0 接口,所以最后我的家庭服务器变成了这个样子。

胶水 NAS

看来,我需要一台真正的多盘位 NAS 服务器了。我一度考虑过以下方案:

  • 搞个蜗牛星际。但是在 KK 家摸到真机以后,我对它的做工倍感失望。
  • 买个迎广 MS04,自己从头攒一台 NAS 出来。但是这样的话,总支出大概不会跟买一台 Gen10 差太远,而且保修这个问题会变得比较复杂。

最终,我还是决定购买 HPE ProLiant MicroServer Gen10。

可以说,这款主机对我来说,几乎是完美的:

  • 4 盘位(虽然并不是热插拔)
  • 低功耗(x3216 的 TDP 只有 15w)
  • 主机本身是正常的 x86 PC,可以很方便的安装各种主流 Linux 发行版
  • 做工良好,可靠性强
  • 两个 PCI-E 插槽(分别为 x8 和 x1),未来升级万兆会很方便
  • 接口丰富:四个 USB 3.0、两个 USB 2.0、两个千兆以太网口、两个全尺寸 DP 和一个 VGA
  • 预装 8GB DDR4 ECC 内存
  • ……

就这样,我以 2.8k CNY 的价格在美亚拿下了这玩意。其实我一开始考虑从德亚买的,只要 2.5k(当时),但是并不能直邮到中国。为了省事,我就直接在美亚买了。

安装

近两周以后,它抵达了我家。到手的第一件事,当然是拆开检查一下了。箱子中并没有太多的东西(主机、美标品字电源线和一堆小册子),不过对于它的目标用户来说大概够了吧。

顺便那条品字电源线用的是带接地的美标插头,这使得它并不能在新国标的插线板上使用。所以如果你家没有多余的品字电源线,别忘了单独买一条!

Gen10 正面

Gen10 背面

Gen10 左侧

PSU 是台达的 flex 电源,宽电压,最大输出 200W;输出有 24pin x1、大 4P x1 和小 4P(软驱电源插头)x1。

接下来就要安装硬盘了。这玩意并没有独立的硬盘仓,所以你需要这样安装硬盘:

  1. 从硬盘仓顶部拧下来 4 颗螺丝
  2. 把这 4 颗螺丝拧到硬盘两侧最左面和最右面的孔位上
    硬盘上的螺丝
  3. 把硬盘正面朝右,稍微用力的推进硬盘仓。移除硬盘也是超级容易的(见硬盘仓左下角贴纸):
    硬盘仓(安装后)

然后就开始安装系统 SSD 了。这玩意并没有 M.2 NVMe 插槽,但是在主板上提供了一个额外的 SATA 接口,所以我买了一块普通的 SATA SSD。

然而,Gen10 的 PSU 并没有多余的电源线,你能利用的只有那个小 4P 插头。所以你需要买这样的转接线:

我那个 Gamemax 机箱正好附送了一根这样的转接线,所以我就直接拿来用了。

至于 SSD 的固定……Gen10 上侧的那几个空位是给你固定笔记本光驱用的,所以你的 SSD 大概就只能这样放着。当然你可以再买个笔记本光驱位转硬盘位之类的东西,不过 NAS 这种东西本身也不需要经常挪动,再说 SSD 里面并没有活动的部件,所以我就无所谓了。

系统 SSD(安装后)

系统

我选用的是 Ubuntu 18.04 LTS。因为:

  • Ubuntu 是我熟悉的发行版系列
  • 我拥有充分的控制能力

安装过程没什么坑,一切都在预料之中。随后,我通过 apt 安装了各种我需要的软件(nginx、Aria2、Transmission 等),写好 /etc/fstab 表,就大功告成了。

目前,我的硬盘使用情况如下:

硬盘 安装位置 用途
东芝 240G SSD(TR200) 顶部 系统盘
东芝 2T 监控盘(DT01ABA200V) 硬盘仓 1 一般文件存储
西数 4T 蓝盘(WD40EZRZ) 硬盘仓 2 BT/PT 下载

同时,我还有一块 2T 的东芝移动硬盘用于冷备份;我会定期将它连接到 Gen10 上,运行我的脚本来进行 rsync。

嗯,我觉得目前就足够了。等将来有需求的话,再考虑加硬盘吧。

完工

2024 更新

这台机子还在服役,但是我后来又买了三块 4T 硬盘来组建 RAID-Z1 阵列,同时为了降低运维难度(其实是我懒),所以把系统换成了 TrueNAS Scale。

内存也加到了 16G,因为 ZFS 对内存的需求更多一些。

系统运行

我之前的确考虑过 CPU 的问题,毕竟这玩意是焊在主板上的。但我的需求不高(就是存取文件和下载啥的,顺便开个 web 服务器),即使是看动画的话,解码都在客户端完成,所以也就 Gen10 了。

实测我的 CPU 负载一般可以控制在 1.0 以下;如果我真的需要算力,大概我就直接拿主力机器搞事了。

neofetch


我的 2019

好耶,我终于把 2019 年瞎鸡巴过完了!

感觉今年反而是相当平淡的一年,所以其实并没有太多的要讲。都是无病呻吟

成就

  • 考到了 C1 驾照
  • 获得了 5000 元的奖学金(第一次!)

面基

今年去了广州、福州和深圳,在线下见到了 qwe7002(又一次)、whitebox、寿司同学和 kookxiang。

我们一起玩了马车 8,一起进行技术交流,还在 kookxiang 家参观了多种灵车。然后 tcdw 对蜗牛星际的做工倍感失望,一怒之下买了 HPE Microserver Gen10。

开心!

买买买

今年买到了不少很棒的新硬件:

  • Nintendo Switch
    相当便携;上面的第一方游戏真的太好玩了,还有很多不错的第三方游戏。吹爆!
  • Dell UltraSharp U2518D
    颜色非常舒适的 2K 显示器,还内置 USB Hub!
  • AOC P2491VWHE/BW
    还行的 1080p 显示器,功耗很低(我的实测只有 18w 左右)。最初是买来给宿舍用的,奈何我的桌子太小了,所以也搬回家里用了……Switch 大屏游戏?暂且算了吧 🤣
  • 小米 CC9e
    便宜,日常应用体验很流畅!虽然屏幕分辨率很低,不过对我的使用场景来说够用了(大概)。
  • 树莓派 4
    好耶,是千兆以太网和 USB 3.1 gen1。
  • Intel Core i7-3770(散片)
    我家那台戴尔应该可以再战几年了!
  • HPE Microserver Gen10(在撰写本博文时还在路上)

同时在文娱方面增加了不少支出;今年我购买了大量的音乐(139 张专辑和 EP,包括数字专辑、捡垃圾和海淘)。其中我非常喜欢的有:

以及我看着 Telegram 群里的大佬们买各种 灵车 买的不亦乐乎,结果我看了半天什么都没买……不过也没什么实际意义啊,而且没那么可靠吧(?)

博客

今年的确没有写很多博文。大概我对这种东西的兴趣也开始下降了吧,主要感觉很多事情不是不值得写就是不适合写……

不过放心,我目前还没有关掉这博客的打算呢。

稍后打算把我的域名都转移到 Cloudflare 了;name.com 续费死贵。

以及想写个新的博客主题了,但是没什么头绪……

2020 年计划?

待定!计划赶不上变化的。

在树莓派(Raspbian)上安装最新稳定版 nginx

如果需要在 Raspbian 上安装最新稳定版 nginx,其实官方是 提供了 Debian 的 apt 源 的,但是并没有提供 armhf 的二进制文件。

于是,我们只好自己通过 Debian 的方式编译安装了。

此方法优点

  • 安装好的 nginx,配置文件路径、维护方法等与官方 Debian 版 nginx 一致
  • 如果需要卸载 nginx,只需执行 sudo apt purge nginx

步骤

1. 安装添加官方 apt 源前所需的包

sudo apt install curl gnupg2 ca-certificates lsb-release

2. 添加官方 apt 源

# 添加官方 apt 源。与官方说明不同的是,由于我们需要源代码,这里添加的是 deb-src 而不是 deb
echo "deb-src http://nginx.org/packages/debian `lsb_release -cs` nginx" | sudo tee /etc/apt/sources.list.d/nginx.list

# 添加 PGP 公钥
curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo apt-key add -
sudo apt-key fingerprint ABF5BD827BD9BF62

# 下载最新的软件包列表
sudo apt update

3. 安装 nginx 所需的依赖

sudo apt build-dep nginx

4. 下载 nginx 的源代码

# 建议单独建立文件夹来存放此次 nginx 编译所需文件,因为你的工作目录会多出若干文件
mkdir nginx_build
cd nginx_build

# 下载源代码
apt source nginx

5. 编译安装

此时我们检查目录下有什么文件:

drwxr-xr-x 10 tcdw tcdw    4096 Nov  5 03:20  nginx-1.16.1
-rw-r--r--  1 tcdw tcdw  114248 Aug 13 17:17  nginx_1.16.1-1~buster.debian.tar.xz
-rw-r--r--  1 tcdw tcdw    1510 Aug 13 17:17  nginx_1.16.1-1~buster.dsc
-rw-r--r--  1 tcdw tcdw 1032630 Aug 13 17:17  nginx_1.16.1.orig.tar.gz

由此可见,此次我们需要先进入 nginx-1.16.1 文件夹。实际文件夹名称可能会由于版本更新而与本文不一致,但应该只有那一个文件夹,且所有相关文件 / 文件夹均以 nginx- 开头。

cd nginx-1.16.1

开始编译安装:

dpkg-buildpackage -uc -b

完成后,返回上一层目录,发现我们的目录中出现了 nginx_1.16.1-1~buster_armhf.deb 文件。我们现在可以开始安装了:

sudo dpkg -i nginx_1.16.1-1~buster_armhf.deb

大功告成!

nginx -V 的输出

升级新版本

重复第 4 - 5 步即可。

驾照

今天我考了科目四,一次过关。

我从 2017 年 6 月就在驾校报名了,但是因为我的拖延症和一些客观原因,我的战线延续了整整两年:

  • 科目一:2017 年 8 月
  • 科目二:2019 年 1 月
  • 科目三:2019 年 7 月
  • 科目四:今天(7 月 26 日)

太丢人了,同样是 2017 年参加完高考的学生,我居然在两年后的夏天才拿本(捂脸

不过结果实现了,我也就这样变成了一位有证司机了,还是可喜可贺的。

宿舍用小型 UPS 电源与新台灯

去年 11 月,我购买了一部二手的联想手机,把自己的电信米粉卡塞了进去,用来运行 Telegram SMS 和开热点。这部手机其实一切都好,但是有个问题:电池不耐用。因为我们会定时熄灯,导致手机并不能 24 小时充电,因此:

  • 如果一直开热点,第二天就没电了。
  • 如果按需开热点可以解决,但这部手机开热点的过程很麻烦,需要进入多级菜单。

我其实还考虑过使用充电宝,但是:

  • 我的品胜充电宝,如果重新插入电源,那么充电过程就会中止,必须手工再启动一次。
  • 我的紫米充电宝,尽管可以即插即用,但是如果在断电的情况下手机充满电,那么就会自动停止充电。
  • 我的 Anker 充电宝,容量太小了(只有 5000 毫安)。

虽然我理解这些充电宝的设计是有道理的,但显然不能满足我的需求。几个月以后,忍无可忍的我决定试试传说中的那种适合宿舍用路由器的 UPS。

几天以后,UPS 到了。

UPS 的外包装 正面 背面

我选购的这款 UPS 可同时提供 USB、5V 和 9V / 12V 输出(可以通过开关切换 9V / 12V);最大输出电流为 2A(所有端口总和)。

同时,它使用 DC 输入,非常节约空间。

我把它接上了我的手机,让这部 UPS 为我的手机持续供电。效果还是不错的:我同时开着 Telegram SMS、代理软件和热点,也不用担心手机中途断电啦。


不久以后,我们团队在一个省级比赛获得了一等奖,每个人都得到了这样的奖品:飞利浦台灯。

因为这部台灯使用 12V 输入,功耗很低,我便把它接入了我的 UPS。这样,即使熄灯也可以使用啦。

⇧ 旧的『宿舍神灯』。

⇧ 新的飞利浦台灯。从我的实际感觉看,光线还是比那部『宿舍神灯』要舒服的,而且还有四档调光,非常灵活。

总之,很开心获得这么一套装备升级。我可以变得更懒惰了!

20190612 更新:一加 3T

我的联想电池鼓包了,连盖子都盖不上了。考虑到这部联想的坑爹之处,我买了一部二手一加 3T 作为 Telegram SMS 服务器使用。

目前来看,它的电池并不是很坑爹,所以只需要用普通的 2A 电源定时充电即可。

于是这 UPS 的供电目标就只剩下那个台灯了。不过不知道未来还会发生什么…… 😅

phone spec

phone status

拆解我爸 2010 年装的廉价 PC

2010 年,我爸觉得我家的 P4 台式机太过时了,所以就花 2k 托人组装了一台新 PC。

然而这台 PC 不仅卡顿时常发生,而且非常不稳定,经常死机、蓝屏,期间我还被母上大人认为我是让那台 PC 不稳定的罪魁祸首,因为我安装的那么几款软件,结果我没少被母上大人数落。😒😒😒半年以后,那台机器突然无法开机,甚至不会 POST,然后我爸只好把同一位 JS 叫过来进行维修。然后,JS 把那块烧掉的双敏主板换掉了。

2011 年,我爸给我买了一块 1TB 的希捷移动硬盘,但是在那台机器上带!不!动!,必须连接外置电源才能正常启动。

2013 年,因为母上大人实在不能忍受那台 PC,所以购买了一台戴尔整机(经过我的几次硬件升级,目前还在我家服役中)。而那台廉价 PC 就拿到我爸那边做监控机,结果半年以后硬盘就完蛋了,SMART 状态为不良,0x05 爆表。随后,我舅舅又拿来一块闲置 80 GB 硬盘,在我爸那边做工控设备,直到 2018 年 7 月彻底完蛋,甚至不再 POST 了。然后那台机器就在仓库里又放了半年。

2019 年,我把那台 PC 从我爸仓库搬了回来,决定看看还有没有什么利用价值。

做工廉价的机箱

机箱

这个机箱从外面看还像是那么回事,但是轻飘飘的,而且金属部分甚至会划手。

不过我丢掉了这个机箱才想起来忘了拍照了。

前面板

做工粗糙,而且接线的地方是用热胶固定的……什么鬼……

机箱内部

机箱内部

散发着寒意而且相当脏乱差的内部。

CPU

安装在主板上的 CPU 和 768 MB DDR2 内存。CPU 与风扇之间的硅脂所剩无几的样子,不知道是当年给我爸装机的那 JS 敷衍了事,还是发生了什么黑魔法。

CPU 本体

AMD Athlon 7750。这台机器是 2010 年装的,但是据 @qwe7002 说,这片 CPU 在 2008 年就属于比较慢的了。

擦干净的主板

擦干净的主板,是 ASUS M2N68-AM PLUS。也是低端板子呢。

光驱

PHILIPS SPD2213,据说是市场上最便宜的光驱。

大炸弹

电源参数

嗯,来自惠州高级工厂的产物。这台机器的不稳定,一定程度上就是这个大炸弹搞得鬼了(包括 USB 供电不足的问题)。

顺便这个序列号其实没什么卵用,因为看起来他家所有产品都用这个序列号。

电源内部

电源内部

电源内部

于是我把大炸弹的风扇拆了出来,发现了绿色的奇怪地方。那是做什么用的呢?

电源内部

原来特么是用来遮盖固定两组线缆的部分!看起来这玩意还是从奇怪的电子垃圾上迫真剪下来的风扇,然后这样凑合固定一下,就装到电源里面了。

结论

这台机器,最后除了两根 SATA 线被我拆了出来,被我原封不动的扔到垃圾桶了。

对于我们这种装机小白,看起来还是买品牌整机比较好。虽然性价比不是那么高,但最起码很靠谱啊。

以及便宜没好货。

我的 2018

好耶,我终于把 2018 年瞎鸡巴过完了!让我们看看究竟发生了什么吧。

新增项目

大学以后,自由的时间越来越多啦,于是自己糊了一些奇怪的项目:

以及其它一些奇怪的多人合作和为别人糊的东西(小声

非主流氪金(?

没想到曾经作为网易云音乐、Spotify 等串流服务的忠实用户的我,居然在 qwe 前辈的影响下,开始买专辑听了。其中实体专辑占了相当一部分,而且基本上都是捡垃圾

同时终于意识到,听专辑其实是另外一种完全不同的听音乐的方式。一张专辑里面的曲目安排等等,按顺序听下来其实相当有趣的!而串流服务上的那些歌单,却把这些有趣的地方都掩盖了,就相当尴尬了(

以及

毕竟我还是相信那句话,要是真的喜欢的话,就花钱支持他们吧。(ref)

(我知道我的数字收藏还是比大佬们差远了 QAQ

设备的变迁

2018 年初,我给我家安装了网件的 GS308 千兆交换机,还买了块 2TB 的东芝监控盘和硬盘盒,用一部 NUC 搞起了自家的云存储服务。

然后我意识到我其实并不需要那么多存储……直到 2018 年底,我只使用了不到 200GB。毕竟以前很多垃圾文件盒所谓资源塞满了我的移动硬盘什么的,删除它们其实就没啥了。于是大概很长时间都不用担心存储空间不足的问题了。

蛤,你说那种网盘?我现在都几乎不用了(

以及手机的话,我的主力开始转向一部 iPhone 6S Plus 了(虽然是我爸淘汰的,他换了部高级黑色 oppo)。一个原因嘛,就是我跟 macOS 进行互联互通,不至于那么胶水化。以及 iOS 真的是不折腾党的福音啊!

杭州、上海、厦门

今年暑假,我做了件相当作死的事情:一个人去杭州、上海和厦门瞎几把玩。

(TODO:添加相关图片)

结果当然是不尽如人意的:我在杭州的最后一天中暑了,然后在上海的酒店休息了半天,最后潦草的去了趟外滩,隔着河观赏了东方明珠塔;同时中暑的其它后遗症一直持续到我的厦门行程,那是相当尴尬了……

不过其实游玩景点并不是重点。在这期间还是和许多在网络上认识的、从未谋面的人见了面,感觉相当开心!qwe7002、Librazy、TonyPrince、SJoshua、rsqppp、Jack Works、M0xkLurk3r,以及其它最终因故没有与我会面的朋友们,感谢你们的支持!

结果我关于美丽的景色毫无印象,却对与沙雕网友的交流印象深刻

杂项成就

  • 通过了英语四级考试

已知问题

  • 瞎几把花钱导致自己山穷水尽(比如 Bandcamp 上的音乐和各种蜜汁转接头
  • 我用的 MacBook Pro 没有 Touch Bar(因此也没有另外两个雷电 3 接口,导致我的外设设定是相当的 clunky 啊。。后悔死了,但我当时真的糊涂了
  • 对一些底层的事物了解不深
  • 很多东西只会瞎几把做,然后搞得一塌糊涂

2019 年要做点啥?

  • 把 Pomment 项目写到能够发布的程度
  • 去更多的城市旅行,包括但不限于南京 / 福州 / 广州 / 成都等等
  • 停止瞎氪金,买一些重要的东西:
    • Nintendo Switch
    • Hades Canyon
    • 2019 款 15 寸 MacBook Pro
    • 一个还行的 1080p 或 2k 显示器
  • 通过科目二考试
  • 把三次元的其它事情处理好

啊,差不多就这些了。

2019 年,大家要一起努力哦。

❌