普通视图

发现新文章,点击刷新页面。
昨天以前aiktb's blog

Stop Using kuromoji.js: @sglkc/kuromoji is a Better, More Modern Fork

2023年11月3日 08:00

Stop Using kuromoji.js: @sglkc/kuromoji is a Better, More Modern Fork ​

TL;DR ​

  1. kuromoji.js has been the top choice for Japanese morphological analysis in JavaScript.
  2. However, kuromoji.js lacks direct browser compatibility and Service Worker support.
  3. Meet @sglkc/kuromoji, a fork that resolves these limitations.

Why Choose kuromoji.js ​

kuromoji.js is a Node.js version of kuromoji, the main purpose of which is to perform morphological analysis of Japanese, providing information on the segmentation and pronunciation of Japanese text.

I developed Furigana Maker, a browser extension that adds ruby character annotations to Japanese text on any page, like the example below. And the core logic of this extension is morphological analysis of Japanese text, so I was in great need of such a library to do this task for me.

watashi言語gengo境界kyokaiは、watashi世界sekai境界kyokai意味imiする。

Considering the npm landscape, kuromoji.js remains the primary solution for Japanese morphological analysis in JavaScript. Most packages related to "Japanese morphological" indirectly rely on kuromoji.js, leaving minimal alternatives.

Challenges with kuromoji.js ​

Tolerable Issues ​

Firstly kuromoji.js disrespects the kuromoji API in its porting, changing a large number of field names, and worst of all it goes so far as to change the word_position field, which starts at 0, to start at 1. This certainly greatly diminishes the happiness of programmers.

Secondly kuromoji.js doesn't support promise, only callback function, which can make the code structure messy, this can be solved by manually writing code to encapsulate it as a promise, a solution will be provided at the end of the article.

Unacceptable Limitations ​

By default, integrating kuromoji.js into the browser involves referencing a CDN or directly including build/kuromoji.js in the project. However, this method negates many advantages of build tools, disrupts project structures, and crucially restricts ESM usage, because build/kuromoji.js is not an ES module.

Using a build tool to package a project dependent on kuromoji.js and running it in the browser leads to a cascade of errors:

  1. kuromoji.js uses zlib.js, which does not run in the browser.
  2. kuromoji.js uses path , which is part of the Node.js core module.

Also kuromoji.js consumes a steady 130MB (not extensively tested) of memory once it is active, which is a huge overhead, whereas browser extensions can very easily take advantage of Service Worker by starting it only when it is needed, and killing the process when it is not needed, rather than just letting it reside in memory.

But Service Worker only supports the Fetch API, and kuromoji.js uses XMLHttpRequest, which will bring another error in Service Worker.

The Solution: @sglkc/kuromoji ​

Don't try to solve these problems with polyfill, I've wasted a lot of time with that, modifying the kuromoji.js source code is necessary to solve the issues.

And with the last commit of the kuromoji.js project in 2018 and the author Takuya Asano's last activity on github in 2022, it's to be expected that we won't be able to get any help from him, including merge Pull Request.

The only solution was to fork this repo and then commit, publish, and luckily when I was about to start solving it myself, I was pleasantly surprised at NPM to find someone who had done everything I needed not too long ago, namely @sglkc/kuromoji, a fork of kuromoji.js.

The changes can be seen in the sglkc's commit log:

  1. Substituting zlib.js with fflate.
  2. Eliminating reliance on the path module.
  3. Transitioning from XMLHttpRequest to the Fetch API.

This solves all the key issues, and now we can easily package it up with the build tool and run it in the browser and Service Worker without any errors, and @sglkc/kuromoji doesn't have any changes to the kuromoji.js API.

Thanks to @sglkc for his excellent work!

Further Reading ​

Using Promise ​

This just needs a simple wrapper, this code references kuromojin.

typescript
import { getTokenizer } from './getTokenizer'

const tokenizer = await getTokenizer()
const tokens = tokenizer.tokenize('私の言語の境界は')
/* [
  {"word_position": 1, "surface_form": "私", "pos": "名詞", "pronunciation": "ワタシ"},
  {"word_position": 2, "surface_form": "の", "pos": "助詞", "pronunciation": "ノ"},
  {"word_position": 3, "surface_form": "言語", "pos": "名詞", "pronunciation": "ゲンゴ"},
  {"word_position": 5, "surface_form": "の", "pos": "助詞", "pronunciation": "ノ"},
  {"word_position": 6, "surface_form": "境界", "pos": "名詞", "pronunciation": "キョーカイ"},
  {"word_position": 8, "surface_form": "は", "pos": "助詞", "pronunciation": "ワ"}
] */
import { getTokenizer } from './getTokenizer'

const tokenizer = await getTokenizer()
const tokens = tokenizer.tokenize('私の言語の境界は')
/* [
  {"word_position": 1, "surface_form": "私", "pos": "名詞", "pronunciation": "ワタシ"},
  {"word_position": 2, "surface_form": "の", "pos": "助詞", "pronunciation": "ノ"},
  {"word_position": 3, "surface_form": "言語", "pos": "名詞", "pronunciation": "ゲンゴ"},
  {"word_position": 5, "surface_form": "の", "pos": "助詞", "pronunciation": "ノ"},
  {"word_position": 6, "surface_form": "境界", "pos": "名詞", "pronunciation": "キョーカイ"},
  {"word_position": 8, "surface_form": "は", "pos": "助詞", "pronunciation": "ワ"}
] */
typescript
// No need for `@ts-ignore`, contains index.d.ts by default.
import kuromoji from '@sglkc/kuromoji'

type Tokenizer = {
  tokenize: (text: string) => kuromoji.IpadicFeatures[]
}

class Deferred {
  promise: Promise<Tokenizer>
  resolve!: (value: Tokenizer) => void
  reject!: (reason: Error) => void
  constructor() {
    this.promise = new Promise<Tokenizer>((resolve, reject) => {
      this.resolve = resolve
      this.reject = reject
    })
  }
}

const deferred = new Deferred()
let isLoading = false

export const getTokenizer = () => {
  if (isLoading) {
    return deferred.promise
  }
  isLoading = true
  const builder = kuromoji.builder({
    dicPath: './assets/dicts'
  })
  builder.build((err: undefined | Error, tokenizer: Tokenizer) => {
    if (err) {
      deferred.reject(err)
    } else {
      deferred.resolve(tokenizer)
    }
  })
  return deferred.promise
}
// No need for `@ts-ignore`, contains index.d.ts by default.
import kuromoji from '@sglkc/kuromoji'

type Tokenizer = {
  tokenize: (text: string) => kuromoji.IpadicFeatures[]
}

class Deferred {
  promise: Promise<Tokenizer>
  resolve!: (value: Tokenizer) => void
  reject!: (reason: Error) => void
  constructor() {
    this.promise = new Promise<Tokenizer>((resolve, reject) => {
      this.resolve = resolve
      this.reject = reject
    })
  }
}

const deferred = new Deferred()
let isLoading = false

export const getTokenizer = () => {
  if (isLoading) {
    return deferred.promise
  }
  isLoading = true
  const builder = kuromoji.builder({
    dicPath: './assets/dicts'
  })
  builder.build((err: undefined | Error, tokenizer: Tokenizer) => {
    if (err) {
      deferred.reject(err)
    } else {
      deferred.resolve(tokenizer)
    }
  })
  return deferred.promise
}

WanaKana &ZeroWidthSpace;

For processing Japanese text, there is a very nice library WanaKana that handles [romoji, hiragana, katakana] interconversions, as well as determining which of [kanji, romoji, hiragana, katakana] a Unicode character is.

Note that this is not as simple as you might think, and I recommend using WanaKana directly to bypass the complexity behind this, and only use the regex if the results don't meet your needs.

Getting kanji pronunciation &ZeroWidthSpace;

I accomplished something similar by extracting the kanji pronunciations from the Japanese text in the following form.

typescript
// It's not just kanji, such as "市ヶ谷" (イチガヤ), "我々" (ワレワレ).
export type KanjiToken = {
  original: string
  reading: string
  start: number // Indexes start from 0
  end: number
}
// It's not just kanji, such as "市ヶ谷" (イチガヤ), "我々" (ワレワレ).
export type KanjiToken = {
  original: string
  reading: string
  start: number // Indexes start from 0
  end: number
}

Since it takes less than 150 lines of code, there is no need to publish it to NPM and the full code can be read at Github.

Why Plasmo is an Excellent Choice for Developing Browser Extensions

2023年11月3日 08:00

Why Plasmo is an Excellent Choice for Developing Browser Extensions &ZeroWidthSpace;

Introduction &ZeroWidthSpace;

Getting started with browser extension development can be quite challenging, often leading inexperienced developers to spend hours or even days troubleshooting issues that are unrelated to the application logic.

This article aims to address the common pain points encountered in conventional browser development and explore how Plasmo alleviates these issues, ultimately enhancing the developer experience.

Why the Need for Something Else? &ZeroWidthSpace;

The structure of browser extensions in the Chrome Extension Documentation appears deceptively simple, resembling the most basic web development. However, this structure, as described in the documentation, is primarily a release structure, often representing the final stage of the development process.

Similar to how most developers prefer using tools like React, TypeScript, or TailwindCSS, a good library or framework significantly accelerates development. Few opt for hand-writing CSS, JavaScript, or HTML due to the low development efficiency and maintainability it implies.

According to a simple data analysis of the top 50 browser extensions on Github sorted by star count, the majority employ webpack or manually scripted builds, with nearly one-third utilizing TypeScript. This data serves as a testament to the issue at hand.

Browser Extension Stats

Why Plasmo Stands Out &ZeroWidthSpace;

After reviewing the various building methods used by the top 50 browser extensions on GitHub, only one employed Plasmo. However, after experimenting with numerous methods, I discovered that Plasmo was the only tool that truly proved to be effective, significantly reducing the burdens on developers.

Tools like Webpack or Vite aren’t inherently tailored for browser extension development. Existing ecosystem plugins for browser extension development rely on intricate configurations, causing frustration, particularly when these plugins frequently don’t align well with certain common tech stacks.

For instance, when I aimed to develop a browser plugin to inject the translate="no" attribute into pre elements on a page to prevent erroneous translations by translators, it should have been a task requiring less than 400 lines of code, excluding various configuration files. However, before I used Plasmo, it took me a week to figure out how to integrate Vue, TypeScript, TailwindCSS, and more into browser extension development.

So, when I opened the Plasmo README.md, I was immediately captivated. It's a tool built specifically for browser extension development, offering key features like:

  • First-class support for React and Typescript
  • Declarative Development
  • Live-reloading and React HMR
  • Optional support for Svelte and Vue

Plasmo provides everything I need in browser development. It’s nearly zero-config and doesn’t even require a manifest.json.

Simply install dependencies, include tailwind.config.ts, tsconfig.json, and more. Write TS files directly in the contents directory or Vue files in the popup directory (of course, React is also an option). Then, start the development server with pnpm dev, open Developer mode in the browser, click on Load unpacked, load the build/chrome-mv3-dev directory, and enjoy it.

All of these elements should be fundamental in a modern JavaScript project. However, browser development before using Plasmo was an exasperating experience. The work done by @PlasmoHQ is commendable, as they've made browser development feel like a breeze instead of a struggle!

Conclusion &ZeroWidthSpace;

If you're eager to dive straight into developing the application logic for your browser extension, don't hesitate—immediately check out the Plasmo documentation!

Plasmo boasts a relatively active community where you can seek assistance through Github Issues and Github Discussions.

Oh-My-Zsh + Powerlevel10k: Zsh One-Click Configuration Script

2023年11月3日 08:00

Oh-My-Zsh + Powerlevel10k: Zsh One-Click Configuration Script &ZeroWidthSpace;

This article utilizes Ubuntu & Termius, run the configuration script here.

Why Use Zsh? &ZeroWidthSpace;

  1. Aesthetic Shell themes and code highlighting.
  2. Enhanced code prompts and auto-completion compared to Bash.
  3. Support for a variety of plugins and themes.

Oh-My-Zsh &ZeroWidthSpace;

Given the wealth of plugins and themes in the Zsh ecosystem, Oh-My-Zsh serves as an out-of-the-box tool for managing plugins and themes, simplifying Zsh configuration.

Here's the list of readily available themes and plugins on GitHub:

However, these lists lack succinct descriptions. Many plugins are mainly used by developers and may not be of significant use. One must navigate through the links, wasting time. Hence, this list is more suitable for users interested in extensive exploration, particularly those seeking alias plugins. For regular users, the recommended plugins and themes suffice.

Moreover, numerous Zsh plugins and themes are not integrated into Oh-My-Zsh, such as Powerlevel10k, zsh-autosuggestions, requiring downloads from the respective GitHub repositories to be used in Zsh.

Plugins &ZeroWidthSpace;

Since I'm not a fan of using aliases, my list of recommended plugins excludes command aliasing:

The thefuck plugin is incompatible with sudo since they both utilize the Double ESC shortcut.

Name Oh-My-Zsh Priority Description
zsh-syntax-highlighting High Supports code highlighting in Zsh terminal.
zsh-autosuggestions High Supports suggestions for Zsh terminal code completion.
zsh-history-substring-search Medium Supports searching for history commands using keyword with up/down arrow keys.
sudo Medium Adds sudo to the previous or current command with double ESC.
colored-man-pages Medium Syntax coloring for man help manual.
extract Low Command x to extract various types of compressed files.
autojump Low Command j to automatically jump directories based on history.
jsontools Low Command pp_json to format JSON inputs.

colored man pages

Theme &ZeroWidthSpace;

The only recommended theme is Powerlevel10k. No other Zsh theme is suggested due to Powerlevel10k's succinct and elegant design.

P10K is presently the most commonly used theme for Zsh and is not included in Oh-My-Zsh default configuration. This underlines Powerlevel10k's excellence and popularity.

Powerlevel10k is a Zsh theme that emphasizes speed, flexibility, and out-of-the-box experience.

The Powerlevel10k theme offers multiple customizable options. Upon its initial installation or using the p10k configure command, prompts appear for configuring the display, such as whether to show Unicode characters or gaps between multiple commands.

powerlevel10k theme

Zsh & Bash &ZeroWidthSpace;

Linux users must be aware of the differences between Zsh and Bash to avoid pitfalls:

  1. Zsh is compatible with most Bash syntax but lacks compatibility with some Bash file wildcards, specifically the use of *.
  2. Zsh offers additional syntax extensions absent in Bash. Given the current prevalence of Bash in default Linux installations, it is advisable not to use Zsh extended syntax. Shell scripts should also utilize #!/bin/bash to ensure compatibility.

Configure Script &ZeroWidthSpace;

Specific Things To Do &ZeroWidthSpace;

Zsh configuration entails three actions:

  1. Installing common plugins and the Powerlevel10k theme.
  2. Moving .zcompdump-* files to the $ZSH/cache directory.
  3. Adding configurations to all new users through the /etc/skel/ directory.

Regarding the second action, Zsh saves files used to expedite command completion in the format below, which defaults to the $HOME directory:

bash
-rw-r--r--  1 aiktb aiktb  49K May 15 11:13 .zcompdump
-rw-r--r--  1 aiktb aiktb  50K May 15 11:13 .zcompdump-shiro-5.8.1
-r--r--r--  1 aiktb aiktb 115K May 15 11:13 .zcompdump-shiro-5.8.1.zwc
-rw-r--r--  1 aiktb aiktb  49K May 15 11:13 .zcompdump
-rw-r--r--  1 aiktb aiktb  50K May 15 11:13 .zcompdump-shiro-5.8.1
-r--r--r--  1 aiktb aiktb 115K May 15 11:13 .zcompdump-shiro-5.8.1.zwc

This format is undoubtedly unsightly and requires a configuration directory change. The solution for this can be found on StackOverflow.

The code below efficiently accomplishes the aforementioned three tasks. Linux users employing the apt package manager can use this script directly, while users of other package managers may need to modify the code accordingly.

Regarding the third action, files in the /etc/skel/ directory are automatically copied to the corresponding home directory during the creation of a new Linux user, sparing the hassle of reconfiguring Zsh for each user.

One-Click Configuration Script &ZeroWidthSpace;

It is recommended to use the following command to download my one-click configuration script:

bash
curl -sL https://raw.githubusercontent.com/aiktb/dotzsh/master/zsh.sh | bash && zsh
curl -sL https://raw.githubusercontent.com/aiktb/dotzsh/master/zsh.sh | bash && zsh

The code below efficiently accomplishes the aforementioned three tasks. Linux users employing the apt package manager can use this script directly, while users of other package managers may need to modify the code accordingly.

bash
#!/bin/bash

sudo apt install zsh -y
# Install oh-my-zsh.
0>/dev/null sh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
ZSH_CUSTOM="$HOME/.oh-my-zsh/custom"
export ZSH_CUSTOM
# Configure plugins.
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${ZSH_CUSTOM}"/plugins/zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-autosuggestions.git "${ZSH_CUSTOM}"/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-history-substring-search "${ZSH_CUSTOM}"/plugins/zsh-history-substring-search
sed -i 's/^plugins=.*/plugins=(git\n extract\n sudo\n autojump\n jsontools\n colored-man-pages\n zsh-autosuggestions\n zsh-syntax-highlighting\n zsh-history-substring-search\n)/g' ~/.zshrc
# Install powerlevel10k and configure it.
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${ZSH_CUSTOM}"/themes/powerlevel10k
sed -i 's/^ZSH_THEME=.*/ZSH_THEME="powerlevel10k\/powerlevel10k"/g' ~/.zshrc
# Move ".zcompdump-*" file to "$ZSH/cache" directory.
sed -i -e '/source \$ZSH\/oh-my-zsh.sh/i export ZSH_COMPDUMP=\$ZSH\/cache\/.zcompdump-\$HOST' ~/.zshrc
# Configure the default ZSH configuration for new users.
sudo cp ~/.zshrc /etc/skel/
sudo cp ~/.p10k.zsh /etc/skel/
sudo cp -r ~/.oh-my-zsh /etc/skel/
sudo chmod -R 755 /etc/skel/
sudo chown -R root:root /etc/skel/
#!/bin/bash

sudo apt install zsh -y
# Install oh-my-zsh.
0>/dev/null sh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
ZSH_CUSTOM="$HOME/.oh-my-zsh/custom"
export ZSH_CUSTOM
# Configure plugins.
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${ZSH_CUSTOM}"/plugins/zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-autosuggestions.git "${ZSH_CUSTOM}"/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-history-substring-search "${ZSH_CUSTOM}"/plugins/zsh-history-substring-search
sed -i 's/^plugins=.*/plugins=(git\n extract\n sudo\n autojump\n jsontools\n colored-man-pages\n zsh-autosuggestions\n zsh-syntax-highlighting\n zsh-history-substring-search\n)/g' ~/.zshrc
# Install powerlevel10k and configure it.
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${ZSH_CUSTOM}"/themes/powerlevel10k
sed -i 's/^ZSH_THEME=.*/ZSH_THEME="powerlevel10k\/powerlevel10k"/g' ~/.zshrc
# Move ".zcompdump-*" file to "$ZSH/cache" directory.
sed -i -e '/source \$ZSH\/oh-my-zsh.sh/i export ZSH_COMPDUMP=\$ZSH\/cache\/.zcompdump-\$HOST' ~/.zshrc
# Configure the default ZSH configuration for new users.
sudo cp ~/.zshrc /etc/skel/
sudo cp ~/.p10k.zsh /etc/skel/
sudo cp -r ~/.oh-my-zsh /etc/skel/
sudo chmod -R 755 /etc/skel/
sudo chown -R root:root /etc/skel/

Many Zsh plugin installation documents utilize the following Zsh syntax extension. Please refrain from using this in Bash:

sh
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}

Further Reading &ZeroWidthSpace;

Termius &ZeroWidthSpace;

All demo images in this article were executed using Termius, which, as you can see, is a very nice terminal!

Termius is currently included in the GitHub Student Developer Pack, which is free for one year for any certified student!

Httpie &ZeroWidthSpace;

Another Shell Tool: httpie, with a detailed documentation.

In brief, it serves as an alternative to curl, providing highlighted outputs and automatic JSON formatting for commands using http and https.

bash
sudo apt install httpie
sudo apt install httpie

httpie preciew

Why Plasmo is an Excellent Choice for Developing Browser Extensions

2023年11月3日 08:00

Why Plasmo is an Excellent Choice for Developing Browser Extensions &ZeroWidthSpace;

Introduction &ZeroWidthSpace;

Getting started with browser extension development can be quite challenging, often leading inexperienced developers to spend hours or even days troubleshooting issues that are unrelated to the application logic.

This article aims to address the common pain points encountered in conventional browser development and explore how Plasmo alleviates these issues, ultimately enhancing the developer experience.

Why the Need for Something Else? &ZeroWidthSpace;

The structure of browser extensions in the Chrome Extension Documentation appears deceptively simple, resembling the most basic web development. However, this structure, as described in the documentation, is primarily a release structure, often representing the final stage of the development process.

Similar to how most developers prefer using tools like React, TypeScript, or TailwindCSS, a good library or framework significantly accelerates development. Few opt for hand-writing CSS, JavaScript, or HTML due to the low development efficiency and maintainability it implies.

According to a simple data analysis of the top 50 browser extensions on Github sorted by star count, the majority employ webpack or manually scripted builds, with nearly one-third utilizing TypeScript. This data serves as a testament to the issue at hand.

Browser Extension Stats

Why Plasmo Stands Out &ZeroWidthSpace;

After reviewing the various building methods used by the top 50 browser extensions on GitHub, only one employed Plasmo. However, after experimenting with numerous methods, I discovered that Plasmo was the only tool that truly proved to be effective, significantly reducing the burdens on developers.

Tools like Webpack or Vite aren’t inherently tailored for browser extension development. Existing ecosystem plugins for browser extension development rely on intricate configurations, causing frustration, particularly when these plugins frequently don’t align well with certain common tech stacks.

For instance, when I aimed to develop a browser plugin to inject the translate="no" attribute into pre elements on a page to prevent erroneous translations by translators, it should have been a task requiring less than 400 lines of code, excluding various configuration files. However, before I used Plasmo, it took me a week to figure out how to integrate Vue, TypeScript, TailwindCSS, and more into browser extension development.

So, when I opened the Plasmo README.md, I was immediately captivated. It's a tool built specifically for browser extension development, offering key features like:

  • First-class support for React and Typescript
  • Declarative Development
  • Live-reloading and React HMR
  • Optional support for Svelte and Vue

Plasmo provides everything I need in browser development. It’s nearly zero-config and doesn’t even require a manifest.json.

Simply install dependencies, include tailwind.config.ts, tsconfig.json, and more. Write TS files directly in the contents directory or Vue files in the popup directory (of course, React is also an option). Then, start the development server with pnpm dev, open Developer mode in the browser, click on Load unpacked, load the build/chrome-mv3-dev directory, and enjoy it.

All of these elements should be fundamental in a modern JavaScript project. However, browser development before using Plasmo was an exasperating experience. The work done by @PlasmoHQ is commendable, as they've made browser development feel like a breeze instead of a struggle!

Conclusion &ZeroWidthSpace;

If you're eager to dive straight into developing the application logic for your browser extension, don't hesitate—immediately check out the Plasmo documentation!

Plasmo boasts a relatively active community where you can seek assistance through Github Issues and Github Discussions.

Oh-My-Zsh + Powerlevel10k: Zsh One-Click Configuration Script

2023年11月3日 08:00

Oh-My-Zsh + Powerlevel10k: Zsh One-Click Configuration Script &ZeroWidthSpace;

This article utilizes Ubuntu & Termius, run the configuration script here.

Why Use Zsh? &ZeroWidthSpace;

  1. Aesthetic Shell themes and code highlighting.
  2. Enhanced code prompts and auto-completion compared to Bash.
  3. Support for a variety of plugins and themes.

Oh-My-Zsh &ZeroWidthSpace;

Given the wealth of plugins and themes in the Zsh ecosystem, Oh-My-Zsh serves as an out-of-the-box tool for managing plugins and themes, simplifying Zsh configuration.

Here's the list of readily available themes and plugins on GitHub:

However, these lists lack succinct descriptions. Many plugins are mainly used by developers and may not be of significant use. One must navigate through the links, wasting time. Hence, this list is more suitable for users interested in extensive exploration, particularly those seeking alias plugins. For regular users, the recommended plugins and themes suffice.

Moreover, numerous Zsh plugins and themes are not integrated into Oh-My-Zsh, such as Powerlevel10k, zsh-autosuggestions, requiring downloads from the respective GitHub repositories to be used in Zsh.

Plugins &ZeroWidthSpace;

Since I'm not a fan of using aliases, my list of recommended plugins excludes command aliasing:

The thefuck plugin is incompatible with sudo since they both utilize the Double ESC shortcut.

Name Oh-My-Zsh Priority Description
zsh-syntax-highlighting High Supports code highlighting in Zsh terminal.
zsh-autosuggestions High Supports suggestions for Zsh terminal code completion.
zsh-history-substring-search Medium Supports searching for history commands using keyword with up/down arrow keys.
sudo Medium Adds sudo to the previous or current command with double ESC.
colored-man-pages Medium Syntax coloring for man help manual.
extract Low Command x to extract various types of compressed files.
autojump Low Command j to automatically jump directories based on history.
jsontools Low Command pp_json to format JSON inputs.

colored man pages

Theme &ZeroWidthSpace;

The only recommended theme is Powerlevel10k. No other Zsh theme is suggested due to Powerlevel10k's succinct and elegant design.

P10K is presently the most commonly used theme for Zsh and is not included in Oh-My-Zsh default configuration. This underlines Powerlevel10k's excellence and popularity.

Powerlevel10k is a Zsh theme that emphasizes speed, flexibility, and out-of-the-box experience.

The Powerlevel10k theme offers multiple customizable options. Upon its initial installation or using the p10k configure command, prompts appear for configuring the display, such as whether to show Unicode characters or gaps between multiple commands.

powerlevel10k theme

Zsh & Bash &ZeroWidthSpace;

Linux users must be aware of the differences between Zsh and Bash to avoid pitfalls:

  1. Zsh is compatible with most Bash syntax but lacks compatibility with some Bash file wildcards, specifically the use of *.
  2. Zsh offers additional syntax extensions absent in Bash. Given the current prevalence of Bash in default Linux installations, it is advisable not to use Zsh extended syntax. Shell scripts should also utilize #!/bin/bash to ensure compatibility.

Configure Script &ZeroWidthSpace;

Specific Things To Do &ZeroWidthSpace;

Zsh configuration entails three actions:

  1. Installing common plugins and the Powerlevel10k theme.
  2. Moving .zcompdump-* files to the $ZSH/cache directory.
  3. Adding configurations to all new users through the /etc/skel/ directory.

Regarding the second action, Zsh saves files used to expedite command completion in the format below, which defaults to the $HOME directory:

bash
-rw-r--r--  1 aiktb aiktb  49K May 15 11:13 .zcompdump
-rw-r--r--  1 aiktb aiktb  50K May 15 11:13 .zcompdump-shiro-5.8.1
-r--r--r--  1 aiktb aiktb 115K May 15 11:13 .zcompdump-shiro-5.8.1.zwc
-rw-r--r--  1 aiktb aiktb  49K May 15 11:13 .zcompdump
-rw-r--r--  1 aiktb aiktb  50K May 15 11:13 .zcompdump-shiro-5.8.1
-r--r--r--  1 aiktb aiktb 115K May 15 11:13 .zcompdump-shiro-5.8.1.zwc

This format is undoubtedly unsightly and requires a configuration directory change. The solution for this can be found on StackOverflow.

The code below efficiently accomplishes the aforementioned three tasks. Linux users employing the apt package manager can use this script directly, while users of other package managers may need to modify the code accordingly.

Regarding the third action, files in the /etc/skel/ directory are automatically copied to the corresponding home directory during the creation of a new Linux user, sparing the hassle of reconfiguring Zsh for each user.

One-Click Configuration Script &ZeroWidthSpace;

It is recommended to use the following command to download my one-click configuration script:

bash
curl -sL https://raw.githubusercontent.com/aiktb/dotzsh/master/zsh.sh | bash && zsh
curl -sL https://raw.githubusercontent.com/aiktb/dotzsh/master/zsh.sh | bash && zsh

The code below efficiently accomplishes the aforementioned three tasks. Linux users employing the apt package manager can use this script directly, while users of other package managers may need to modify the code accordingly.

bash
#!/bin/bash

sudo apt install zsh -y
# Install oh-my-zsh.
0>/dev/null sh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
ZSH_CUSTOM="$HOME/.oh-my-zsh/custom"
export ZSH_CUSTOM
# Configure plugins.
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${ZSH_CUSTOM}"/plugins/zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-autosuggestions.git "${ZSH_CUSTOM}"/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-history-substring-search "${ZSH_CUSTOM}"/plugins/zsh-history-substring-search
sed -i 's/^plugins=.*/plugins=(git\n extract\n sudo\n autojump\n jsontools\n colored-man-pages\n zsh-autosuggestions\n zsh-syntax-highlighting\n zsh-history-substring-search\n)/g' ~/.zshrc
# Install powerlevel10k and configure it.
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${ZSH_CUSTOM}"/themes/powerlevel10k
sed -i 's/^ZSH_THEME=.*/ZSH_THEME="powerlevel10k\/powerlevel10k"/g' ~/.zshrc
# Move ".zcompdump-*" file to "$ZSH/cache" directory.
sed -i -e '/source \$ZSH\/oh-my-zsh.sh/i export ZSH_COMPDUMP=\$ZSH\/cache\/.zcompdump-\$HOST' ~/.zshrc
# Configure the default ZSH configuration for new users.
sudo cp ~/.zshrc /etc/skel/
sudo cp ~/.p10k.zsh /etc/skel/
sudo cp -r ~/.oh-my-zsh /etc/skel/
sudo chmod -R 755 /etc/skel/
sudo chown -R root:root /etc/skel/
#!/bin/bash

sudo apt install zsh -y
# Install oh-my-zsh.
0>/dev/null sh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
ZSH_CUSTOM="$HOME/.oh-my-zsh/custom"
export ZSH_CUSTOM
# Configure plugins.
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${ZSH_CUSTOM}"/plugins/zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-autosuggestions.git "${ZSH_CUSTOM}"/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-history-substring-search "${ZSH_CUSTOM}"/plugins/zsh-history-substring-search
sed -i 's/^plugins=.*/plugins=(git\n extract\n sudo\n autojump\n jsontools\n colored-man-pages\n zsh-autosuggestions\n zsh-syntax-highlighting\n zsh-history-substring-search\n)/g' ~/.zshrc
# Install powerlevel10k and configure it.
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${ZSH_CUSTOM}"/themes/powerlevel10k
sed -i 's/^ZSH_THEME=.*/ZSH_THEME="powerlevel10k\/powerlevel10k"/g' ~/.zshrc
# Move ".zcompdump-*" file to "$ZSH/cache" directory.
sed -i -e '/source \$ZSH\/oh-my-zsh.sh/i export ZSH_COMPDUMP=\$ZSH\/cache\/.zcompdump-\$HOST' ~/.zshrc
# Configure the default ZSH configuration for new users.
sudo cp ~/.zshrc /etc/skel/
sudo cp ~/.p10k.zsh /etc/skel/
sudo cp -r ~/.oh-my-zsh /etc/skel/
sudo chmod -R 755 /etc/skel/
sudo chown -R root:root /etc/skel/

Many Zsh plugin installation documents utilize the following Zsh syntax extension. Please refrain from using this in Bash:

sh
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}

Further Reading &ZeroWidthSpace;

Termius &ZeroWidthSpace;

All demo images in this article were executed using Termius, which, as you can see, is a very nice terminal!

Termius is currently included in the GitHub Student Developer Pack, which is free for one year for any certified student!

Httpie &ZeroWidthSpace;

Another Shell Tool: httpie, with a detailed documentation.

In brief, it serves as an alternative to curl, providing highlighted outputs and automatic JSON formatting for commands using http and https.

bash
sudo apt install httpie
sudo apt install httpie

httpie preciew

Stop Using kuromoji.js: @sglkc/kuromoji is a Better, More Modern Fork

2023年11月3日 08:00

Stop Using kuromoji.js: @sglkc/kuromoji is a Better, More Modern Fork &ZeroWidthSpace;

TL;DR &ZeroWidthSpace;

  1. kuromoji.js has been the top choice for Japanese morphological analysis in JavaScript.
  2. However, kuromoji.js lacks direct browser compatibility and Service Worker support.
  3. Meet @sglkc/kuromoji, a fork that resolves these limitations.

Why Choose kuromoji.js &ZeroWidthSpace;

kuromoji.js is a Node.js version of kuromoji, the main purpose of which is to perform morphological analysis of Japanese, providing information on the segmentation and pronunciation of Japanese text.

I developed Furigana Maker, a browser extension that adds ruby character annotations to Japanese text on any page, like the example below. And the core logic of this extension is morphological analysis of Japanese text, so I was in great need of such a library to do this job for me.

watashi言語gengo境界kyokaiは、watashi世界sekai境界kyokai意味imiする。

Considering the npm landscape, kuromoji.js remains the primary solution for Japanese morphological analysis in JavaScript. Most packages related to "Japanese morphological" indirectly rely on kuromoji.js, leaving minimal alternatives.

Challenges with kuromoji.js &ZeroWidthSpace;

Tolerable Issues &ZeroWidthSpace;

Firstly kuromoji.js disrespects the kuromoji API in its porting, changing a large number of field names, and worst of all it goes so far as to change the word_position field, which starts at 0, to start at 1. This certainly greatly diminishes the happiness of programmers.

Secondly kuromoji.js doesn't support promise, only callback function, which can make the code structure messy, this can be solved by manually writing code to encapsulate it as a promise, a solution will be provided at the end of the article.

Unacceptable Limitations &ZeroWidthSpace;

By default, integrating kuromoji.js into the browser involves referencing a CDN or directly including build/kuromoji.js in the project. However, this method negates many advantages of build tools, disrupts project structures, and crucially restricts ESM usage, because build/kuromoji.js is not an ES module.

Using a build tool to package a project dependent on kuromoji.js and running it in the browser leads to a cascade of errors:

  1. kuromoji.js uses zlib.js, which does not run in the browser.
  2. kuromoji.js uses path , which is part of the Node.js core module.

Also kuromoji.js consumes a steady 130MB (not extensively tested) of memory once it is active, which is a huge overhead, whereas browser extensions can very easily take advantage of Service Worker by starting it only when it is needed, and killing the process when it is not needed, rather than just letting it reside in memory.

But Service Worker only supports the Fetch API, and kuromoji.js uses XMLHttpRequest, which will bring another error in Service Worker.

The Solution: @sglkc/kuromoji &ZeroWidthSpace;

Don't try to solve these problems with polyfill, I've wasted a lot of time with that, modifying the kuromoji.js source code is necessary to solve the issues.

And with the last commit of the kuromoji.js project in 2018 and the author Takuya Asano's last activity on github in 2022, it's to be expected that we won't be able to get any help from him, including merge Pull Request.

The only solution was to fork this repo and then commit, publish, and luckily when I was about to start solving it myself, I was pleasantly surprised at NPM to find someone who had done everything I needed not too long ago, namely @sglkc/kuromoji, a fork of kuromoji.js.

The changes can be seen in the sglkc's commit log:

  1. Substituting zlib.js with fflate.
  2. Eliminating reliance on the path module.
  3. Transitioning from XMLHttpRequest to the Fetch API.

This solves all the key issues, and now we can easily package it up with the build tool and run it in the browser and Service Worker without any errors, and @sglkc/kuromoji doesn't have any changes to the kuromoji.js API.

Thanks to @sglkc for his excellent work!

Further Reading &ZeroWidthSpace;

Using Promise &ZeroWidthSpace;

This just needs a simple wrapper, this code references kuromojin.

typescript
import { getTokenizer } from './getTokenizer'

const tokenizer = await getTokenizer()
const tokens = tokenizer.tokenize('私の言語の境界は')
/* [
  {"word_position": 1, "surface_form": "私", "pos": "名詞", "pronunciation": "ワタシ"},
  {"word_position": 2, "surface_form": "の", "pos": "助詞", "pronunciation": "ノ"},
  {"word_position": 3, "surface_form": "言語", "pos": "名詞", "pronunciation": "ゲンゴ"},
  {"word_position": 5, "surface_form": "の", "pos": "助詞", "pronunciation": "ノ"},
  {"word_position": 6, "surface_form": "境界", "pos": "名詞", "pronunciation": "キョーカイ"},
  {"word_position": 8, "surface_form": "は", "pos": "助詞", "pronunciation": "ワ"}
] */
import { getTokenizer } from './getTokenizer'

const tokenizer = await getTokenizer()
const tokens = tokenizer.tokenize('私の言語の境界は')
/* [
  {"word_position": 1, "surface_form": "私", "pos": "名詞", "pronunciation": "ワタシ"},
  {"word_position": 2, "surface_form": "の", "pos": "助詞", "pronunciation": "ノ"},
  {"word_position": 3, "surface_form": "言語", "pos": "名詞", "pronunciation": "ゲンゴ"},
  {"word_position": 5, "surface_form": "の", "pos": "助詞", "pronunciation": "ノ"},
  {"word_position": 6, "surface_form": "境界", "pos": "名詞", "pronunciation": "キョーカイ"},
  {"word_position": 8, "surface_form": "は", "pos": "助詞", "pronunciation": "ワ"}
] */
typescript
// No need for `@ts-ignore`, contains index.d.ts by default.
import kuromoji from '@sglkc/kuromoji'

type Tokenizer = {
  tokenize: (text: string) => kuromoji.IpadicFeatures[]
}

class Deferred {
  promise: Promise<Tokenizer>
  resolve!: (value: Tokenizer) => void
  reject!: (reason: Error) => void
  constructor() {
    this.promise = new Promise<Tokenizer>((resolve, reject) => {
      this.resolve = resolve
      this.reject = reject
    })
  }
}

const deferred = new Deferred()
let isLoading = false

export const getTokenizer = () => {
  if (isLoading) {
    return deferred.promise
  }
  isLoading = true
  const builder = kuromoji.builder({
    dicPath: './assets/dicts'
  })
  builder.build((err: undefined | Error, tokenizer: Tokenizer) => {
    if (err) {
      deferred.reject(err)
    } else {
      deferred.resolve(tokenizer)
    }
  })
  return deferred.promise
}
// No need for `@ts-ignore`, contains index.d.ts by default.
import kuromoji from '@sglkc/kuromoji'

type Tokenizer = {
  tokenize: (text: string) => kuromoji.IpadicFeatures[]
}

class Deferred {
  promise: Promise<Tokenizer>
  resolve!: (value: Tokenizer) => void
  reject!: (reason: Error) => void
  constructor() {
    this.promise = new Promise<Tokenizer>((resolve, reject) => {
      this.resolve = resolve
      this.reject = reject
    })
  }
}

const deferred = new Deferred()
let isLoading = false

export const getTokenizer = () => {
  if (isLoading) {
    return deferred.promise
  }
  isLoading = true
  const builder = kuromoji.builder({
    dicPath: './assets/dicts'
  })
  builder.build((err: undefined | Error, tokenizer: Tokenizer) => {
    if (err) {
      deferred.reject(err)
    } else {
      deferred.resolve(tokenizer)
    }
  })
  return deferred.promise
}

WanaKana &ZeroWidthSpace;

For processing Japanese text, there is a very nice library WanaKana that handles [romoji, hiragana, katakana] interconversions, as well as determining which of [kanji, romoji, hiragana, katakana] a Unicode character is.

Note that this is not as simple as you might think, and I recommend using WanaKana directly to bypass the complexity behind this, and only use the regex if the results don't meet your needs.

Getting kanji pronunciation &ZeroWidthSpace;

I accomplished something similar by extracting the kanji pronunciations from the Japanese text in the following form.

typescript
// It's not just kanji, such as "市ヶ谷" (イチガヤ), "我々" (ワレワレ).
export type KanjiToken = {
  original: string
  reading: string
  start: number // Indexes start from 0
  end: number
}
// It's not just kanji, such as "市ヶ谷" (イチガヤ), "我々" (ワレワレ).
export type KanjiToken = {
  original: string
  reading: string
  start: number // Indexes start from 0
  end: number
}

Since it takes less than 150 lines of code, there is no need to publish it to NPM and the full code can be read at Github.

Browser Extension开发中的陷阱:构建工具与第三方库

2023年7月16日 08:00

Browser Extension开发中的陷阱:构建工具与第三方库 &ZeroWidthSpace;

Web Extension

Introduction &ZeroWidthSpace;

Chrome Extension Doc中的浏览器拓展源码结构看起来非常简单,只需用manifest.json组织好代码就能让拓展正常工作,但事实上文档中描述的结构只是一个Release,绝大多数情况下是开发流程的最后一环构建的产物。

实际的浏览器开发拓展在起步阶段可谓困难重重,足以让没有经验的开发者在与应用逻辑完全无关的地方浪费数小时、数天的时间仍不能解决问题,这无疑是一种痛苦的开发体验,这篇文章正是分析我个人在开发中遇到的那些让人头疼的问题。

这篇文章大体分为两大部分,第一部分关于构建工具的选择,第二部分关于第三方库可用性,这两部分有许多交叉的内容,可能会有所重复。

我很喜欢TailWind CSS作者Adam Wathan的一句话:

🚫 Plan, plan, plan, build
✅ Build, rebuild, rebuild

— Adam Wathan (@adamwathan) July 12, 2023

前期计划也许很重要,但是不开始Coding永远不知道问题在哪里以及该如何解决问题。

Build &ZeroWidthSpace;

你可以在我的Github Gist查看Github中按照star数量排序的前50名浏览器拓展使用的构建工具的简易数据,这有助你选择技术栈。

下面是一个统计,排除了一些奇怪的东西(Ruby、C++):

BrowserExtensionStats

在编程语言的选择上,可以看出TypeScript的使用率不低,在较大型的浏览器拓展项目中的采用率较高,这可能与TypeScript的可维护性较高有关,由于TypeScript需要编译为JavaScript,所有采用TypeScript的项目都使用了构建工具或脚本构建。

在构建方法的选择上,有接近40%的项目使用Webpack,是绝对的主流构建方法。30%的项目没有使用任何构建工具,Github仓库里存放的仅仅是Release,这可能是不现实的。有一定比例的开发者选择自己编写脚本构建输出目录来符合浏览器拓展规范,虽然使用的语言不同,但是思路是一致的。剩余的项目使用的构建工具都是特定于个别项目的,没有普适性,如gulp、nx、plasmo等。

下面按照构建复杂度依次介绍这些方法。

No Tool &ZeroWidthSpace;

在介绍常见的浏览器拓展构建工具前,首先要介绍最原始的不使用任何构建工具来开发拓展的方法,这意味着不可以使用node.js环境,因为Release里不会有package.jsonnode_modules,这就会涉及第三方库问题,因为没有构建工具来自动打包依赖,所以开发者必须将所需的最小化依赖*.min.js文件放到vendors文件夹才能在项目中正常引用这些文件,同时这也引入了一个新问题,开发者可能无法使用ES6 module,因为这些最小化依赖文件基本上都是没有使用模块的,这对于熟悉ES6 module的开发者来说可能会非常痛苦。

之所以浏览器拓展开发会让人如此痛苦,正是因为其过于原始,和目前普遍使用vite、webpack等构建工具的模块化开发相差甚远,如果不使用任何构建工具,浏览器依赖要自己去第三方库的官网下载,IDE的静态分析会变成残废,也无法使用测试工具,可谓全是缺点,建议千万不要考虑这种方法,只有在项目的复杂度非常低时才可以考虑。

另外Github中使用这种方法开发的拓展,多数是古老遗留项目,在浏览器兼容下能正常运行,同时因为没有模块只能使用全局变量,类型定义也没有,项目的可读性极差,连一个变量是哪来的都分不清,这样的项目不推荐看源码,心智负担极重。

Script build &ZeroWidthSpace;

如果使用脚本构建,那么正常的node.js坏境就可以使用,该有的tsc、test支持都与一般的前端开发没有区别。如果要使用tsc,就在tsconfig.json中配置好以下选项建立ts文件映射关系:

json
{
    "compilerOptions": {
        "rootDir": "src",
   		"outDir": "dist/js"
    }
    "include": ["src/**/*.ts", "src/**/*.d.ts"]
}
{
    "compilerOptions": {
        "rootDir": "src",
   		"outDir": "dist/js"
    }
    "include": ["src/**/*.ts", "src/**/*.d.ts"]
}
txt
project    -->   dist
└─src 	         └─js
   └─*.ts          └─*.js
project    -->   dist
└─src 	         └─js
   └─*.ts          └─*.js

然后编写脚本:运行测试、运行tsc编译TypsScript到JavaScript、复制必要的文件如manifest.json、在浏览器开发者模式查看产品等...

看起来很简单?大错特错,这种方式有以下问题:

  1. 仍然需要vendors目录,找支持浏览器的库源码很麻烦;
  2. 没有文件热重载,除非你想自己手写一个watch;
  3. 构建耗时长,复制粘贴文件这部分逻辑考虑到构建速度可能会很麻烦;
  4. bash/bat是操作系统特定的,在Linux构建不能在Windows上快速查看产物;
  5. 跨操作系统的JS脚本要引入一堆从未了解过的开发时依赖库。

node.js中常用的"删除文件"是rimraf,"复制文件"我建议使用cpy-cli,不要使用cpx,该库存在多个高危漏洞且无人维护。

总结:如果你只想简单用一用TypeScript、Jest等,那么这是可行的,在项目复杂度很低的时候这比不使用任何构建工具要容易得多,最重要的是IDE的所有静态分析以及node.js环境都可以正常工作,不至于两眼一抹黑。但我仍然不推荐使用脚本构建,心智负担很重,构建脚本写起来很麻烦。

Build tool &ZeroWidthSpace;

无论是使用webpack、vite等主流构建工具,或者nx、gulp等小众构建工具,在浏览器拓展开发上都只有一个目的:最大限度简化开发中的繁琐事务。基于这些工具已有的庞大生态,有许多工作都可以被简化,如库文件打包、文件映射、复制资产文件以及文件热重载等...上文提到的痛点大部分可以转移到构建工具的配置上。

chrome-extension-typescript-starter是一个Github有2k Star的模板仓库,旨在为只想在开发中使用TypeScript的开发者提供便利,注意该模板已过时,不要使用这个模板。该库没有使用"type": "module",当你想一起正常使用ES6 module和TypeScript时将给你带来无尽的痛苦。

vite-plugin-web-extension也许是个不错的选择,它在默认配置下工作良好,只是文件结构略显混乱。

上文提到的这些构建工具没有一个是为浏览器拓展开发而生的,简化浏览器拓展只是它们生态中的一个插件而已,虽然已方便许多但仍显麻烦。那么到底有没有一个最终解决方案能改变这一切?

在收集资料时我看到了plasmo,一个为简化浏览器拓展开发而生的开源框架,官网的介绍非常诱人,我认为这值得一试。

plasmo

Library availability &ZeroWidthSpace;

关于第三方库可用性只有一条准则:千万不要使用含有node.js core modules的第三方库。这种库只能在node环境上运行,在浏览器上是没有这些模块的,所以这些库无法运行在浏览器上。

目前的主流构建工具如webpack、vite等都不再默认包含polyfill,你可以尝试手动添加polyfill,webpack、rollup、vite生态中有许多这样的polyfill,如果只是需要polyfill一些简单的node专有全局变量,如path__dirname等,这也许是可行的,但是他们不一定有用,这很可能只是在浪费时间。上文统计的50个拓展仓库基本没有使用任何polyfill。

CommonJS module属于上面提到的问题,现在已经2023年了,离ES6 module发布的2015年已经过了8年,还在用CJS的库毫无疑问已过时,尽可能不要用,因为有问题基本也没人帮你解决。

从这个问题上也感受到node.js看起来第三方库的数量远超其他语言,基本什么功能都能找到库,但质量较差,维护者也不太上心,加上ES6 module割裂了以往的CJS生态,用起来真不算是舒服。相比10多年保持一致体验的Java依赖管理仓库Maven,可谓天差地别。

如果有一个库你必须要用它,又是node专有,而且polyfill没用,考虑将这部分逻辑分割到服务器端,用request/reponse的形式来做,这是一个可行的解决方案。

VitePress利用默认主题拓展开发博客实录

2023年4月30日 08:00

VitePress利用默认主题拓展开发博客实录 &ZeroWidthSpace;

cover

本文使用VitePress v1.0.0-beta.1,VitePress仍在开发中,请留意更新。

Before the start &ZeroWidthSpace;

这篇博客的目的是给出自定义VitePress博客主题的注意事项,但是如果想构建一个完整的项目你仍然需要熟悉VitePress文档。

这个博客项目参考了许多博客和GitHub仓库,我会使用外链表明参考对象。

如果你觉得我的这篇博客和项目对你有帮助的话,不妨为我的GitHub点个Star⭐!

Q&A &ZeroWidthSpace;

Q: 用VitePress构建自定义主题的博客有哪些优点?

A: 开箱即用、所需技术门槛低、SPA加载速度快以及构建速度快。

Q: 需要掌握哪些技术才能自定义主题?

A: 最基本的CSSHTMLJavaScript,这里使用的所有TypeScript都可以很简单的更换为JavaScript,无需任何顾虑。

Q: 有哪些有价值的文档可以参考?

A: GitHub IssueVitePress DocsMDN Web Docs

Q: 如何部署博客到公网?

A: 建议使用GitHub Actiongithub.io的子域名即可,不需要任何花费。

Tool Box &ZeroWidthSpace;

Name Features
Typora 所见即所得的markdown编辑器,简洁美观,功能齐全。
Canva 用于设计博客所用的封面图片,提供云保存、在线编辑和大量免费模板。
IconScout 搜寻博客需要使用的各种SVG图标,免费图标就够用。
SM.MS 免费的在线图床服务,如果不想将图片保存在GitHub Repo的话很有用。
PicGo-APP 和Typora配合实现粘贴图片自动转换为Webp并上传到多种图床。
Figma SVG编辑器有很多,真的好用还得是Figma。
ChatGPT 提供各种关于编码的建议,如果你没有太多前端开发经验,那么这很重要。

Develop &ZeroWidthSpace;

Getting Started &ZeroWidthSpace;

在WebStorm或VSCode新建一个空项目,执行以下命令:

bash
npm install -D vitepress
npx vitepress init
npm install -D vitepress
npx vitepress init

项目目录结构:

txt
├─.github
│  └─workflows
├─docs
│  ├─.vitepress
│  │  ├─cache
│  │  ├─dist
│  │  └─theme
│  │      └─components
│  ├─posts
│  └─public
└─node_modules
├─.github
│  └─workflows
├─docs
│  ├─.vitepress
│  │  ├─cache
│  │  ├─dist
│  │  └─theme
│  │      └─components
│  ├─posts
│  └─public
└─node_modules

填写目录名称时,docs是默认名,该目录在GitHub的代码占比分析中会被忽略,参考linguist,如果你的Repo Languages显示不正常,应该创建.gitattributes在你的项目根目录(最外层),添加类似行:

txt
docs/** -linguist-documentation
docs/** -linguist-documentation

这样你的Repo Languages在项目开发完后应该类似:

repo-languages

tsconfig.json 如果你使用JavaScript那么可以忽略它。
json
{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true,
    /* Linting */
    "strict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  },
  "include": ["docs/.vitepress/**/*.ts", "docs/.vitepress/**/*.vue"]
}
{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true,
    /* Linting */
    "strict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  },
  "include": ["docs/.vitepress/**/*.ts", "docs/.vitepress/**/*.vue"]
}

config.ts &ZeroWidthSpace;

sidebar &ZeroWidthSpace;

对于文档而言这是必要的,但对博客而言需要去掉它来节省空间,从config.ts中删除以下行关闭它:

typescript
sidebar: [ 
    {
		text: 'Guide',
        items: [
            { text: 'Introduction', link: '/introduction' },
            { text: 'Getting Started', link: '/getting-started' }
        ]
    }
]
sidebar: [ 
    {
		text: 'Guide',
        items: [
            { text: 'Introduction', link: '/introduction' },
            { text: 'Getting Started', link: '/getting-started' }
        ]
    }
]

lineNumbers &ZeroWidthSpace;

这是我唯一使用的markdown配置,且很有用,我查看了许多人的VitePress项目,发现他们都没有开启代码块行号显示选项,我建议你在config.ts中开启它:

typescript
markdown: {
    lineNumbers: true
}
markdown: {
    lineNumbers: true
}

head &ZeroWidthSpace;

除去官网介绍的简单添加favicon功能,head还可以做很多事,MDN Web Docs中提到的都是可添加项,用类似下面的方法可以将其添加到你的博客或文档中。

最常用的就是加载外部JavaScript,比如支持Google Analytics,就像这样:

typescript
head: [
    [
        'script',
        {async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-**********'}
    ],
    [
        'script',
        {},
        `window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', 'G-**********');`
    ],
]
head: [
    [
        'script',
        {async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-**********'}
    ],
    [
        'script',
        {},
        `window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', 'G-**********');`
    ],
]
html
<!-- HTML generated by VitePress -->
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-**********"/></script>
<script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'G-**********');
</script>
<!-- HTML generated by VitePress -->
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-**********"/></script>
<script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'G-**********');
</script>

像这样控制Chrome for Android的主题颜色:

typescript
head: [
    [
        'meta',
        {name: 'theme-color', content: '#4df5ff'}
    ],
]
head: [
    [
        'meta',
        {name: 'theme-color', content: '#4df5ff'}
    ],
]

你还可以像这样加载Google Fonts中的JetBrains Mono字体,以便在CSS中直接使用它,以下两种方式都是可行的:

typescript
head: [
	[
        'link',
        {rel: 'preconnect', href: 'https://fonts.googleapis.com'}
    ],
    [
        'link',
        {rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: ''},
    ],
    [
        'link',
        {rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap'},
    ],
]
head: [
	[
        'link',
        {rel: 'preconnect', href: 'https://fonts.googleapis.com'}
    ],
    [
        'link',
        {rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: ''},
    ],
    [
        'link',
        {rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap'},
    ],
]
typescript
head: [
    [
        'style',
        {},
        `@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap');`
    ],
]
head: [
    [
        'style',
        {},
        `@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap');`
    ],
]

buildEnd &ZeroWidthSpace;

这个功能可以在VitePress构建完成后调用特定的JavaScript,很适合用来添加类似RSS Feed和Site Map的功能。

我将在这里用几步教会你为博客生成RSS Feed:

  1. 运行npm i -D feed安装依赖;
  2. 编辑/theme/rss.tsconfig.ts文件:
typescript
import {rss} from './theme/rss'

export default defineConfig({
    buildEnd: rss,
})
import {rss} from './theme/rss'

export default defineConfig({
    buildEnd: rss,
})
typescript
import path from 'path'
import { writeFileSync } from 'fs'
import { Feed } from 'feed'
import { type ContentData, createContentLoader, type SiteConfig } from 'vitepress'

const id: string = 'aiktb'
const baseUrl: string = `https://${id}.com`
type RssGenerator = (config: SiteConfig) => Promise<void>
export const rss: RssGenerator = async (config) => {
  const feed: Feed = new Feed({
    title: `${id}'s blog`,
    description: 'My Personal Blog',
    id: baseUrl,
    link: baseUrl,
    language: 'zh-CN',
    image: `${baseUrl}/avatar.jpg`,
    favicon: `${baseUrl}/favicon.svg`,
    copyright: `Copyright (c) 2023 ${id}`
  })

  const posts: ContentData[] = await createContentLoader('posts/*.md', {
    excerpt: true,
    render: true,
    transform: (rawData) => {
      return rawData.sort((a, b) => {
        return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
      })
    }
  }).load()

  for (const { url, excerpt, frontmatter, html } of posts) {
    feed.addItem({
      title: frontmatter.title as string,
      id: `${baseUrl}${url}`,
      link: `${baseUrl}${url}`,
      description: excerpt as string,
      content: html as string,
      author: [{ name: `${id}` }],
      date: frontmatter.date
    })
  }

  writeFileSync(path.join(config.outDir, 'rss.xml'), feed.rss2())
}
import path from 'path'
import { writeFileSync } from 'fs'
import { Feed } from 'feed'
import { type ContentData, createContentLoader, type SiteConfig } from 'vitepress'

const id: string = 'aiktb'
const baseUrl: string = `https://${id}.com`
type RssGenerator = (config: SiteConfig) => Promise<void>
export const rss: RssGenerator = async (config) => {
  const feed: Feed = new Feed({
    title: `${id}'s blog`,
    description: 'My Personal Blog',
    id: baseUrl,
    link: baseUrl,
    language: 'zh-CN',
    image: `${baseUrl}/avatar.jpg`,
    favicon: `${baseUrl}/favicon.svg`,
    copyright: `Copyright (c) 2023 ${id}`
  })

  const posts: ContentData[] = await createContentLoader('posts/*.md', {
    excerpt: true,
    render: true,
    transform: (rawData) => {
      return rawData.sort((a, b) => {
        return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
      })
    }
  }).load()

  for (const { url, excerpt, frontmatter, html } of posts) {
    feed.addItem({
      title: frontmatter.title as string,
      id: `${baseUrl}${url}`,
      link: `${baseUrl}${url}`,
      description: excerpt as string,
      content: html as string,
      author: [{ name: `${id}` }],
      date: frontmatter.date
    })
  }

  writeFileSync(path.join(config.outDir, 'rss.xml'), feed.rss2())
}

这个方法基本参考了尤雨溪Vue Blog源码,但他使用了错误的'feed.rss'文件名,应该使用.xml格式,否则RSS订阅文件将无法被浏览器正确显示。

并且我的方法依赖每篇文章中开头有如下格式的frontmatter,并且博客文章目录名为posts,关于frontmatter的应用接下来还会详细提到。

markdown
---
title: 'VitePress Blog Title'
date: 2023-04-30
---
---
title: 'VitePress Blog Title'
date: 2023-04-30
---
  1. 注意时间格式很重要,不要修改它,那会导致报错。
  2. Google Search支持使用RSS Feed作为站点地图,没有必要再单独生成sitemap。

socialLinks &ZeroWidthSpace;

重要的只有一点:如何引用SVG文件图标为网站添加一个VitePress默认支持以外的图标(比如Telegram、Email)?

图标可以从iconscout找,但VitePress Docs只给出了一种SVG硬编码引用方式,其实有更好的方法。

在Vue和JavaScript文件你都可以类似使用以下的格式引用,这需要你的viewBox设置和原始SVG一致并xlink:href引用正确的SVG文件名和id:

typescript
themeConfig: {
	socialLinks: [
		{
			icon: {
				svg: `<svg role="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
                        <title>RSS</title>
                        <use xlink:href="/rss.svg#rss"/>
                      </svg>`
            },
            link: '/rss.xml'
        },
    ]
}
themeConfig: {
	socialLinks: [
		{
			icon: {
				svg: `<svg role="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
                        <title>RSS</title>
                        <use xlink:href="/rss.svg#rss"/>
                      </svg>`
            },
            link: '/rss.xml'
        },
    ]
}
xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" id="rss">
    <path d=" ... "/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" id="rss">
    <path d=" ... "/>
</svg>

search &ZeroWidthSpace;

新建立的文档和博客还没有内容来充实它,申请Algolia DocSearch有点为难人,不过VitePress就在alpha.66版本加入了Local Search功能,可以按照文档的说明简单快捷的启用它,只需要几行代码:

typescript
 themeConfig: {
    search: {
      provider: 'local'
    }
  }
 themeConfig: {
    search: {
      provider: 'local'
    }
  }

需要注意的是Local Search并不完美,仅仅是"能用"而已,还存在许多问题有待解决,特别是中文的处理上表现很糟糕。

以前有vitepress-plugin-search插件用于支持本地搜索,我使用过该插件,效果上似乎差距不明显,但在样式上Local Search完胜,这能减轻CSS开发工作量。

search

在我的博客大体开发完成后发布了3篇文章时,仅仅过了3个小时Algolia就通过了我的申请,他们的工作效率真的很高:)

但是默认的Algolia Search样式并不美观,需要花一些时间调整。

algolia search

Layout &ZeroWidthSpace;

Layout组件的slot是自定义VitePress博客的要点,正是因为有了slot才使VitePress在默认主题下页面仍有一定可拓展的空间,Layout一共有3种布局,我只使用了homedoc,没有使用page布局。

我的博客一共使用了5个slot用于插入自定义的VUE组件:

Slot Component Function
doc-top <Progress/> 在页面顶部显示阅读进度条
doc-after <Comments/> 在博客文章的末尾提供评论区
aside-outline-before <Avatar/> 在右侧加入<Member/>显示头像和联系方式
home-hero-before <Hero/> 在主页显示头像和一些简短的描述
home-hero-after <Page/> 按时间排序分页显示所有发布的博客文章

Progress.vue &ZeroWidthSpace;

这个组件比较特殊,虽然我使用了doc-top插槽,但实际上它可以根据需要放在任何一个插槽中,因为我使用了<Teleport to="body"/>来将这个组件传送到了外层的body中。

Comments.vue &ZeroWidthSpace;

对于博客来说,评论区很重要,这也能充实博客页面的空间,我尝试了3种解决方案:

Disqus和Gitalk存在我无法解决的Bug,Giscus官方的Vue组件也有Bug,最终被放弃,只有下面这一种方法是靠谱的。

Giscus有以下优点:

  • 没有明显的样式或逻辑Bug;
  • 使用GitHub Discussion而不是Issue;
  • Transparent Dark主题能自适应网站配色。

以下代码可以完成构建一个美观<Comments/>组件的任务,具体的参数参照giscus.app

:key用于阻止Vue组件重用,如果没有该属性,评论区在页面路由后不能正常更新。useRouteuseRouter中的数据不是响应式的,不可以用于:key

vue
<script setup lang="ts">
import {useData} from 'vitepress'

const {title} = useData()
</script>

<template>
  <div class="comments">
    <component
        :is="'script'"
        :key="title"
        src="https://giscus.app/client.js"
        data-repo="......"
        data-repo-id="......"
        data-category="......"
        data-category-id="......"
        data-mapping="pathname"
        data-strict="0"
        data-reactions-enabled="1"
        data-emit-metadata="0"
        data-input-position="top"
        data-lang="en"
        data-theme="transparent_dark"
        data-loading="lazy"
        async
    />
  </div>
</template>
<script setup lang="ts">
import {useData} from 'vitepress'

const {title} = useData()
</script>

<template>
  <div class="comments">
    <component
        :is="'script'"
        :key="title"
        src="https://giscus.app/client.js"
        data-repo="......"
        data-repo-id="......"
        data-category="......"
        data-category-id="......"
        data-mapping="pathname"
        data-strict="0"
        data-reactions-enabled="1"
        data-emit-metadata="0"
        data-input-position="top"
        data-lang="en"
        data-theme="transparent_dark"
        data-loading="lazy"
        async
    />
  </div>
</template>
vue
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import Comments from "./Comments.vue"

const {Layout} = DefaultTheme
</script>

<template>
  <Layout>
    <template #doc-after>
      <Comments/>
    </template>
  </Layout>
</template>
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import Comments from "./Comments.vue"

const {Layout} = DefaultTheme
</script>

<template>
  <Layout>
    <template #doc-after>
      <Comments/>
    </template>
  </Layout>
</template>
typescript
import DefaultTheme from 'vitepress/theme'
import Layout from './components/Layout.vue'

export default {
    ...DefaultTheme,
    Layout: Layout,
}
import DefaultTheme from 'vitepress/theme'
import Layout from './components/Layout.vue'

export default {
    ...DefaultTheme,
    Layout: Layout,
}

可以在加入类似comments: falsefrontmatter,并在VUE中根据这一特征来决定是否加载Giscus来关闭评论。

这个功能很简单,但是锁定GitHub Discussion也可以做到,我认为没有必要再引入更多复杂性,所以没有加入。

giscus

Avatar.vue &ZeroWidthSpace;

这个组件很简单,为VitePress提供的<VPTeamMembers/>组件添加头像、描述和4个链接就完成了。

需要注意的是.vue文件可以用以下语法导入SVG文件,比config.ts方便的多。

这种方法导入的SVG图标触摸时不会显示文字,可以在SVG文件中添加title标签修复。

Vue+TypeScript需要一个svg.d.ts文件提供类型声明,否则将导致一个TS无法导入模块的报错。

vue
<script setup lang="ts">
import {VPTeamMembers} from 'vitepress/theme'
import email from '/email.svg?raw' 

const members = [
  {
    ...
    links: [
      {
        icon: {svg: email}, 
        link: 'mailto:hey@aiktb.com'
      }
    ]
  }
]
</script>

<template>
  <VPTeamMembers size="small" :members="members"/>
</template>
<script setup lang="ts">
import {VPTeamMembers} from 'vitepress/theme'
import email from '/email.svg?raw' 

const members = [
  {
    ...
    links: [
      {
        icon: {svg: email}, 
        link: 'mailto:hey@aiktb.com'
      }
    ]
  }
]
</script>

<template>
  <VPTeamMembers size="small" :members="members"/>
</template>
xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="email">
    <title>Email</title> 
    <path d=" ... "/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="email">
    <title>Email</title> 
    <path d=" ... "/>
</svg>
typescript
declare module '*.svg?raw' {
    import Vue, {VueConstructor} from 'vue'
    const content: VueConstructor<Vue>
    export default content
}
declare module '*.svg?raw' {
    import Vue, {VueConstructor} from 'vue'
    const content: VueConstructor<Vue>
    export default content
}

members

Hero.vue & Page.vue &ZeroWidthSpace;

这两个组件构成了我的博客主页,主要参考了clark-cui的博客,这并不复杂,仅仅依赖了在frontmatter中自定义的titledate

Page-Testing

createContentLoader &ZeroWidthSpace;

Page.vue使用了这个函数用来获取posts目录下的所有.md文件数据,并且用TypeScript处理数据并渲染页面。

createContentLoader需要按照文档的说明新建立一个posts.data.ts文件来使用,因为这个函数无法在.vue文件中导入。

typescript
import {createContentLoader} from 'vitepress'

export interface Post {
    title: string
    url: string
    date: string
}

declare const data: Post[]
export {data}

export default createContentLoader('posts/*.md', {
    transform: (rawData) => {
        return rawData.sort((a, b) => {
            return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
        }).map(post => {
            return {
                title: post.frontmatter.title,
                url: post.url,
                date: formatDate(post.frontmatter.date)
            }
        })
    }
})

function formatDate(raw: string): Post['date'] {
    const date = new Date(raw)
    date.setUTCHours(12)
    return date.toLocaleDateString('en-US', {
            year: 'numeric',
            month: 'long',
            day: 'numeric'
        }
    )
}
import {createContentLoader} from 'vitepress'

export interface Post {
    title: string
    url: string
    date: string
}

declare const data: Post[]
export {data}

export default createContentLoader('posts/*.md', {
    transform: (rawData) => {
        return rawData.sort((a, b) => {
            return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
        }).map(post => {
            return {
                title: post.frontmatter.title,
                url: post.url,
                date: formatDate(post.frontmatter.date)
            }
        })
    }
})

function formatDate(raw: string): Post['date'] {
    const date = new Date(raw)
    date.setUTCHours(12)
    return date.toLocaleDateString('en-US', {
            year: 'numeric',
            month: 'long',
            day: 'numeric'
        }
    )
}

Deploy &ZeroWidthSpace;

GitHub Action &ZeroWidthSpace;

在这一点上我真的要称赞VitePress团队,因为文档中的deploy.yml文件不需要做任何修改就能在GitHub Action上直接使用,只需要将它放在你的.github/workflow目录下面。

这种方式有一个优点是可以滚动更新,你的服务不会下线,如果你还使用github.io子域,那么你将没有任何花费。

自定义域名非常简单,只需要两步:

  1. 设置你的DNS解析记录一条A记录和AAAA记录指向GitHub的IPV4和IPV6地址,参考文档
  2. 在以下GitHub仓库路径的设置将Custom domain设置为你想要的顶级域或子域。
txt
https://github.com/${USER}/${REPO}/settings/pages
https://github.com/${USER}/${REPO}/settings/pages

自定义域设置需要一些时间,如果你的域运行在Cloudflare的CDN上,而且以前申请过SSL证书,在这段时间你的网站会显示"526 Invalid SSL certificate"。

顺带一提,如果你在用Cloudflare的CDN,并且发现你的VitePress项目404页面无法正常显示,那么参考issue#2270

github-setting

Others &ZeroWidthSpace;

VPS直接部署:

zsh
git clone https://github.com/aiktb/rea.git
cd rea
npm install
tmux new-session -d 'npm run docs:build && npm run docs:preview'
git clone https://github.com/aiktb/rea.git
cd rea
npm install
tmux new-session -d 'npm run docs:build && npm run docs:preview'

终止服务:

bash
tmux kill-session -t 0
tmux kill-session -t 0

Oh-My-Zsh + Powerlevel10k: Zsh一键配置脚本

2023年4月28日 08:00

Oh-My-Zsh+Powerlevel10k: Zsh一键配置脚本 &ZeroWidthSpace;

cover

本文使用Ubuntu22.04 & Windows Terminal,点击这里直接跳转到自动配置脚本。

Why use zsh? &ZeroWidthSpace;

  1. 美观的Shell主题以及代码高亮;
  2. 比Bash更好用的代码提示和自动补全;
  3. 丰富的插件以及主题支持。

zsh

Oh-My-Zsh &ZeroWidthSpace;

由于Zsh生态有大量插件和主题,需要Oh-My-Zsh这个开箱即用的工具用于管理插件和主题,并简化Zsh的配置。

以下是Oh-My-Zsh在GitHub开箱即用的主题和插件列表:

由于没有直接列出简短的描述,而且很多插件是开发者使用且没有什么大用,还需要点开链接才能查看具体的描述,不得不说这简直是浪费时间,所以以上的列表只适合想要淘宝的用户,尤其是在找寻alias插件的用户,普通用户建议使用我推荐的插件和主题即可。

也有很多Zsh的插件和主题没有集成到Oh-My-Zsh中,比如Powerlevel10k、zsh-autosuggestions,这些需要去对应的GitHub仓库按要求下载才能在Zsh中使用。

oh-my-zsh

Plugins &ZeroWidthSpace;

因为我不太喜欢使用alias,所以不会包含命令别名的插件,以下是我个人使用的插件推荐:

thefuck插件与sudo不兼容,他们都使用Double ESC快捷键。

Name Oh-My-Zsh Priority Description
zsh-syntax-highlighting High 支持Zsh终端输入代码高亮
zsh-autosuggestions High 支持Zsh终端输入代码补全建议
zsh-history-substring-search Medium 支持方向键上下移动按关键字搜索历史命令
sudo Medium 按两次ESC为上一条或当前命令添加sudo
colored-man-pages Medium 支持man帮助手册语法着色
extract Low 命令x解压所有类型压缩包
autojump Low 命令j根据以往记录自动跳转目录
jsontools Low 命令pp_json接受JSON输入将其格式化输出

colored-man-pages

Theme &ZeroWidthSpace;

唯一指定主题推荐Powerlevel10k,不推荐其他任何Zsh主题,使用Powerlevel10k的原因只有一个:简洁美观。

P10K是目前Zsh使用人数最多的主题,并且没有包含在Oh-My-Zsh的默认配置中,足以看出Powerlevel10k的优秀和受欢迎。

Powerlevel10k is a theme for Zsh. It emphasizes speed, flexibility and out-of-the-box experience.

Powerlevel10k的主题外观有多个可调整的选项,第一次安装完Powerlevel10k或者使用p10k configure命令时有界面提示可以配置Powerlevel的显示外观,比如是否显示Unicode字符、多条命令之间是否有间隙等。

powerlevel10k

Zsh & Bash &ZeroWidthSpace;

作为Linux用户必须知道Zsh与Bash的几个不同之处,以防踩坑:

  1. Zsh兼容大部分Bash语法,但有少部分不兼容,特别是Zsh不兼容Bash文件通配符*的使用;

  2. Zsh有一部分Bash不含有的扩展语法,在目前Linux主流默认安装Bash的情况下建议不要使用Zsh扩展语法,Shell脚本也请使用#!/bin/bash以保证兼容性。

neofetch

Configure Script &ZeroWidthSpace;

Zsh配置有3件事要做:

  1. 安装常用插件和Powerlevel10k主题;
  2. .zcompdump-*文件移动到$ZSH/cache目录;
  3. 通过/etc/skel/目录将配置添加给所有新用户。

关于第2点,zsh使用以下格式保存用于加速命令补全的文件,默认放在$HOME目录:

bash
-rw-r--r--  1 aiktb aiktb  49K May 15 11:13 .zcompdump
-rw-r--r--  1 aiktb aiktb  50K May 15 11:13 .zcompdump-shiro-5.8.1
-r--r--r--  1 aiktb aiktb 115K May 15 11:13 .zcompdump-shiro-5.8.1.zwc
-rw-r--r--  1 aiktb aiktb  49K May 15 11:13 .zcompdump
-rw-r--r--  1 aiktb aiktb  50K May 15 11:13 .zcompdump-shiro-5.8.1
-r--r--r--  1 aiktb aiktb 115K May 15 11:13 .zcompdump-shiro-5.8.1.zwc

这无疑是丑陋的,需要修改配置目录,对应的解决方法参考了StackOverflow

关于第3点,/etc/skel/目录中的文件会在Linux新用户创建时自动复制到对应的home目录中,这样就免去了为每一个用户重新配置zsh之苦。

建议使用以下命令下载我的脚本一键配置:

bash
curl -sL https://raw.githubusercontent.com/aiktb/dotzsh/master/zsh.sh | bash && zsh
curl -sL https://raw.githubusercontent.com/aiktb/dotzsh/master/zsh.sh | bash && zsh

以下就是具体的代码,很好地的完成了以上3点任务,使用apt包管理器的Linux用户可以直接使用,其余包管理器需要自行更改代码。

bash
#!/bin/bash

# Or yum.
sudo apt install zsh -y
# Install oh-my-zsh.
0>/dev/null sh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
ZSH_CUSTOM="$HOME/.oh-my-zsh/custom"
export ZSH_CUSTOM
# Configure plugins.
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${ZSH_CUSTOM}"/plugins/zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-autosuggestions.git "${ZSH_CUSTOM}"/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-history-substring-search "${ZSH_CUSTOM}"/plugins/zsh-history-substring-search
sed -i 's/^plugins=.*/plugins=(git\n extract\n sudo\n autojump\n jsontools\n colored-man-pages\n zsh-autosuggestions\n zsh-syntax-highlighting\n zsh-history-substring-search\n)/g' ~/.zshrc
# Install powerlevel10k and configure it.
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${ZSH_CUSTOM}"/themes/powerlevel10k
sed -i 's/^ZSH_THEME=.*/ZSH_THEME="powerlevel10k\/powerlevel10k"/g' ~/.zshrc
# Move ".zcompdump-*" file to "$ZSH/cache" directory.
sed -i -e '/source \$ZSH\/oh-my-zsh.sh/i export ZSH_COMPDUMP=\$ZSH\/cache\/.zcompdump-\$HOST' ~/.zshrc
# Configure the default ZSH configuration for new users.
sudo cp ~/.zshrc /etc/skel/
sudo cp ~/.p10k.zsh /etc/skel/
sudo cp -r ~/.oh-my-zsh /etc/skel/
sudo chmod -R 755 /etc/skel/
sudo chown -R root:root /etc/skel/
#!/bin/bash

# Or yum.
sudo apt install zsh -y
# Install oh-my-zsh.
0>/dev/null sh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
ZSH_CUSTOM="$HOME/.oh-my-zsh/custom"
export ZSH_CUSTOM
# Configure plugins.
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${ZSH_CUSTOM}"/plugins/zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-autosuggestions.git "${ZSH_CUSTOM}"/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-history-substring-search "${ZSH_CUSTOM}"/plugins/zsh-history-substring-search
sed -i 's/^plugins=.*/plugins=(git\n extract\n sudo\n autojump\n jsontools\n colored-man-pages\n zsh-autosuggestions\n zsh-syntax-highlighting\n zsh-history-substring-search\n)/g' ~/.zshrc
# Install powerlevel10k and configure it.
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${ZSH_CUSTOM}"/themes/powerlevel10k
sed -i 's/^ZSH_THEME=.*/ZSH_THEME="powerlevel10k\/powerlevel10k"/g' ~/.zshrc
# Move ".zcompdump-*" file to "$ZSH/cache" directory.
sed -i -e '/source \$ZSH\/oh-my-zsh.sh/i export ZSH_COMPDUMP=\$ZSH\/cache\/.zcompdump-\$HOST' ~/.zshrc
# Configure the default ZSH configuration for new users.
sudo cp ~/.zshrc /etc/skel/
sudo cp ~/.p10k.zsh /etc/skel/
sudo cp -r ~/.oh-my-zsh /etc/skel/
sudo chmod -R 755 /etc/skel/
sudo chown -R root:root /etc/skel/

很多Zsh插件的安装文档使用了以下Zsh语法拓展,请勿在Bash中使用:

zsh
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}

Other than zsh &ZeroWidthSpace;

Navi &ZeroWidthSpace;

推荐一个不在Zsh生态中的Shell Tool: navi,它可以在一定程度上替代man,提供更方便易懂的命令行手册查询。

由于navi依赖fzf,下载要先安装fzf,且不支持apt包管理器,使用以下命令手动安装:

bash
sudo apt install fzf
bash <(curl -sL https://raw.githubusercontent.com/denisidoro/navi/master/scripts/install)
sudo apt install fzf
bash <(curl -sL https://raw.githubusercontent.com/denisidoro/navi/master/scripts/install)

安装完成以后重启shell即可正常使用navi命令,然后下载所有的手册提示,就可以享受到更优秀的手册了。

navi

Httpie &ZeroWidthSpace;

以及另外一个Shell Tool: httpie文档中有详细的介绍和说明,简单来说这是一个curl的替代品,使用命令httphttps具有将类似curl输出高亮和JSON自动格式化的能力,个人认为在一定程度上比curl好用并且更美观。

apt包管理器可以直接安装,注意httpie虽然包含在oh-my-zsh的插件列表中,却和fzf一样需要其他配置才能正常使用,并不如apt方便:

bash
sudo apt install httpie
sudo apt install httpie

httpie

Powershell 7 &ZeroWidthSpace;

也许你正在使用WSL2,那么我想你还偶尔会使用Powershell,Windows 11目前自带的是Powershell 5,但Powershell 7自带了类似zsh-autosuggestions的CLI命令补全提示功能,这对你会有帮助的,参考Microsoft的文档用下面这条命令在Powershell 5中下载Powershell 7。

powershell
 winget install --id Microsoft.Powershell --source winget
 winget install --id Microsoft.Powershell --source winget

powershell7

Nginx Proxy Manager实现HTTPS反向代理排疑

2023年4月7日 08:00

Nginx Proxy Manager实现HTTPS反向代理排疑 &ZeroWidthSpace;

cover

本文使用Ubuntu22.04 Server。

介绍以及安装 &ZeroWidthSpace;

介绍 &ZeroWidthSpace;

Nginx Proxy Manager是由jc21开发的一款使用Web管理Nginx反向代理的工具,其开发理念是"It had to be so easy that a monkey could do it",相比较传统的Nginx反向代理配置,真的简单太多,普通的反向代理我们只要动动鼠标就ok了,SSL证书的申请也很简单,还会自动为证书请续期,比用Certbot脚本申请还简单!

安装 &ZeroWidthSpace;

如果你还没有安装docker,我推荐参照Docker Documentation的文档使用apt包管理器下载。

bash
# -p 创建多级目录
mkdir -p ~/Docker/npm 
cd ~/Docker/npm
# 打开nano编辑器,如果没有安装使用sudo apt install nano
nano docker-compose.yml
# -p 创建多级目录
mkdir -p ~/Docker/npm 
cd ~/Docker/npm
# 打开nano编辑器,如果没有安装使用sudo apt install nano
nano docker-compose.yml

按照文档将以下代码使用Ctrl+Shift+V粘贴到打开的nano编辑器中,键盘敲击Ctrl+X,nano编辑器底部显示Save modified buffer?时敲击Y键+Enter回车键保存文件。

yaml
version: '3.8'
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '81:81' # Nginx Proxy Manager在本机的映射端口
      - '443:443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt # SSL证书挂载目录
version: '3.8'
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '81:81' # Nginx Proxy Manager在本机的映射端口
      - '443:443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt # SSL证书挂载目录

然后使用命令docker compose -d up让Nginx Proxy Manager服务在后台运行。

bash
# 当前运行目录应该在~/Docker/npm
docker compose -d up
# 查看Docker Container运行情况,有输出即为正常
docker container ls | grep nginx-proxy-manager 
# 检查Nginx Proxy Manager是否正常运行监听81端口,输出含有0.0.0.0:81即为正常
ss -ltn | grep 81
# 当前运行目录应该在~/Docker/npm
docker compose -d up
# 查看Docker Container运行情况,有输出即为正常
docker container ls | grep nginx-proxy-manager 
# 检查Nginx Proxy Manager是否正常运行监听81端口,输出含有0.0.0.0:81即为正常
ss -ltn | grep 81

检查防火墙UFW &ZeroWidthSpace;

默认的Ubuntu22.04系统是没有开启UFW防火墙的,如果你开启了UFW防火墙,那么应该开放Docker使用的所有端口,如上述Nginx Proxy Manager使用的80、81、443端口,否则你的服务将不能从外网正常访问。

bash
sudo ufw status numbered
# 输出Status: active即为开启了UFW,以下命令开放本机端口到外网
sudo ufw allow 80 comment 'HTTP Server'
sudo ufw allow 443 comment 'HTTPS Server'
sudo ufw allow 81 comment 'Nginx Proxy Manager'
sudo ufw status numbered
# 输出Status: active即为开启了UFW,以下命令开放本机端口到外网
sudo ufw allow 80 comment 'HTTP Server'
sudo ufw allow 443 comment 'HTTPS Server'
sudo ufw allow 81 comment 'Nginx Proxy Manager'

通过Web管理反向代理和SSL证书 &ZeroWidthSpace;

登录 &ZeroWidthSpace;

如果你和主机位于同一个局域网可以使用http://127.0.0.1:81访问Web面板,否则只能使用http://host_ip:81(host_ip由你的VPS供应商提供)去访问Web面板。

根据Nginx Proxy Manager的文档,使用默认邮箱admin@example.com和默认密码changeme登录:

login

添加SSL证书 &ZeroWidthSpace;

登录之后修改密码的提示会弹窗,你可以立刻修改密码,但要注意此时的连接是HTTP,密码明文传输在公网并不安全,建议开启HTTPS后再修改密码。

  1. 主页选中SSL Certificates页面,选择Add SSL Certificates
  2. 添加申请的SSL证书对应的域名,我这里使用一个泛域名证书*.aiktb.com和单域名证书aiktb.com,这样以后只要做好DNS解析所有的域名都可以只用这一张证书;
  3. 配置DNS Challenge用于证书申请,CA机构通过DNS Challenge来验证你是否是这个域名的所有者,我使用Cloudflare按照官网的要求获取API Token,然后填入对应的位置;
  4. Propagation Seconds不要填写即为使用默认值,选中同意Let's Encrypt的政策,点击Save即可完成证书申请。

ssl

ssl

反向代理Nginx Proxy Manager &ZeroWidthSpace;

开放防火墙端口 &ZeroWidthSpace;

开启了UFW的在反向代理时必须要开放需要代理的端口对应的防火墙端口(同时也是Docker映射的端口),Nginx Proxy Manager使用81端口,其他的服务设置为对对应的端口即可。

bash
sudo ufw allow 81 comment 'Nginx Proxy Manager'
sudo ufw allow 81 comment 'Nginx Proxy Manager'

添加DNS解析 &ZeroWidthSpace;

按照域名提供商的对应方法添加一条DNS解析用于反向代理服务,这里我新增一条npm.aiktb.com的DNS解析即可。

实际配置 &ZeroWidthSpace;

点击主页右上角的Add Proxy Host,填写对应的主机静态IP和端口:

add-proxy-host

注意Schema是根据你代理的服务是否实际开启了HTTPS来设置的:

  1. 使用HTTP的服务选择HTTPS Schema会导致502 Bad Gateway,这是Nginx Proxy Manager的一个最常见错误设置;
  2. 一般将客户端到服务器的路径进行TLS加密就足够了,没有必要再为单独的服务开启TLS加密;
  3. 使用Docker的服务开启TLS加密需要在容器中单独申请证书或者用文件挂载将证书映射到容器内,操作繁琐,不建议使用。

另外,不可以填写127.0.0.1作为除Nginx Proxy Manager以外其他服务的Forward Hostname/IP,Docker处于一个独立的网络环境,默认配置的桥接网络是无法通过127.0.0.1访问到运行在其他Docker网络环境的服务的。

解释一下3个选项的作用,不关心的话全部启用就好。

  1. Cache Assets:开启服务器缓存,提高访问速度;
  2. Block Common Exploits:阻止常见的漏洞利用,提高安全性;
  3. Websockets Support: 开启Websocket支持,可以参考Nginx官方的解释

选中右上角的SSL,一目了然,添加刚刚自动申请的证书:

add-ssl

SSL相关的配置都是安全相关的,不关心的话全部启用就好。

  1. Force SSL:强制开启SSL加密,HTTP重定向到HTTPS;
  2. HTTP/2 Support:使用更安全的HTTP2协议;
  3. HSTS Enabled:参考Cloudflare的这篇文章;
  4. HSTS Subdomains:为子域名开启HSTS。

点击Save,这样就完成Nginx Proxy Manager反向代理的全部配置,在浏览器使用https://domain就能访问到对应的服务了。

当前已开启HTTPS连接,建议使用1password更换一个16位以上大小写字母+符号的强力随机密码。

域名重定向 &ZeroWidthSpace;

假如你想要将www.aiktb.com重定向到aiktb.com,那么你应该将HTTP Code设置为308 Permanent Redirect,并设置SSL,否则网页将无法打开:

edit-redirection-host

常见错误排除 &ZeroWidthSpace;

可以关闭防火墙81端口阻断Nginx Proxy Manager的Web UI,系统管理的应用不适合长时间暴露在公网。

  1. 检查防火墙配置,端口一定要开放;
  2. 远程进不了Web面板别忘了开启代理,有可能是GFW的干扰;
  3. HTTP的服务选中HTTPS Schema,会导致502 Bad Gateway
  4. 页面异常有可能是浏览器缓存导致,清理浏览器的缓存再尝试;
  5. 忘记密码参考issue#1634
❌
❌