macOs Sonoma command + tab 无法切换窗口

问题描述

最近更新电脑(MacBook Pro)系统升级到 macOS Sonoma,在使用过程中发现下面的问题,忍受了一段时间,最终还是决定解决一下。

全屏应用、非全屏应用这两种应用,在使用 command + tab 切换应用的时候,捷键的指令已经执行完毕,应用状态也已经切换了,但是无法“聚焦切换”到对应的应用上。

图1:全屏应用、非全屏应用这两种应用,在使用 command + tab 切换应用的时候,捷键的指令已经执行完毕,应用状态也已经切换了,但是无法“聚焦切换”到对应的应用上。

解决方案

最终在系统设置中找到了解决方案,如下图所示:

图2:切换到某个应用程序时,会切换到包含该应用程序的打开窗口的空间

设置入口:

  1. 屏幕左上角「🍏」 > “系统设置”,
  2. 点按边栏中的“桌面与程序坞” ,前往右侧的“调度中心”,
  3. 然后打开“切换到某个应用程序时,会切换到包含该应用程序的打开窗口的空间”。

官方解释:

图3:切换到某个应用程序时,会切换到包含该应用程序的打开窗口的空间

图4:When switching to an application, switch to a Space with open windows for the application

参考资料

怎么区分Arc浏览器和Chrome浏览器

背景

在给 Workflows 页面里面的 Chrome 书签,添加 快速使用示例代码 时,发现在 Arc 浏览器里面,无法正常使用,而在 Chrome 浏览器里面可以正常使用。

看看怎么区分一下 Arc 浏览器和 Chrome 浏览器,然后在 Arc 浏览器不给提示。

快速使用示例代码

查看两个浏览器的 User Agent,一毛一样:

Arc 浏览器:

Arc Browser User Agent

> navigator.userAgent
< 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'

Chrome 浏览器:

Chrome Browser User Agent

> navigator.userAgent
< 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'

解决方案

搜索找到了这篇文章:Can JavaScript detect the Arc browser? – Stack Overflow

根据这篇文章里面的提示,可以通过 获取 Arc 浏览器 的 User Agent Atylesheet(浏览器默认样式) 来区分 Arc 浏览器和 Chrome 浏览器。

User Agent Atylesheet(浏览器默认样式)

:root {
    --arc-palette-cutoutColor: #ECEDFEFF;
    --arc-palette-backgroundExtra: #FDFDFFFF;
    --arc-palette-minContrastColor: #ECEDFEFF;
    --arc-palette-hover: #D3D4FDFF;
    --arc-palette-background: #E9EAFEFF;
    --arc-palette-focus: #9094FBFF;
    --arc-palette-foregroundPrimary: #3139FBFF;
    --arc-palette-maxContrastColor: #212AFBFF;
    --arc-background-gradient-color0: #C8CAFEFF;
    --arc-palette-subtitle: #A6AAFBFF;
    --arc-palette-foregroundSecondary: #8489FBFF;
    --arc-palette-foregroundTertiary: #ECEDFEFF;
    --arc-background-gradient-color1: #FFE6E6FF;
    --arc-palette-title: #0A0D4BFF;
}

只判断是否有值其中一个变量存在就可以了。

<script setup lang="ts">
import { onMounted, ref } from 'vue'
const isArcBrowser = ref(false);

onMounted(() => {
  isArcBrowser.value = getComputedStyle(document.documentElement)
    .getPropertyValue('--arc-palette-title') ? true : false;
  console.log('isArcBrowser', isArcBrowser.value);
});
</script>

参考

  1. Can JavaScript detect the Arc browser? – Stack Overflow

Workflows with Alfred

caniuse

willfarrell/alfred-caniuse-workflow: Alfred App Workflow for caniuse.com

alfred-caniuse-workflow

cdnjs

Alfred 3 workflow to search libs on cdnjs

chitacan/alfred-cdnjs

alfred-cdnjs

Chrome 书签

bayleedev/alfred-chrome-bookmarks: :bookmark: Fast Chrome bookmark searcher for Alfred.

alfred-chrome-bookmarks

Font Awesome

ruedap/alfred-font-awesome-workflow: 🎩 Font Awesome workflow for Alfred

alfred-font-awesome-workflow

GitHub

willfarrell/alfred-github-workflow: Searching Github repos.

alfred-github-workflow

gitignore

jdno/alfred-gitignore: Create .gitignore files using Alfred

alfred-gitignore

Hash

willfarrell/alfred-hash-workflow: Hashing Strings

alfred-hash-workflow

MDN Search

alfred-workflows/mdn-search at main · gilbarbara/alfred-workflows

mdn-search

Open url in Browser

linxz/alfredWorkflow-Open-url-in-Browser-without-http: 自从用了alfred后,比较依赖这个东西,然后很多时候都是直接通过这个打开浏览器的……无意间发现其实可以同时打开多个,于是稍微调整一下……

Open url in Browser

Package Repo Search

willfarrell/alfred-pkgman-workflow: Package Repo Search

所有命令

Package Repo Search

Youdao Translator

wensonsmith/YoudaoTranslator: Alfred Youdao Translate Workflow

YoudaoTranslator

WiFi Workflow

ravelll/wifi-workflow: A Alfred workflow to simply control Wi-Fi setting on macOS.

wifi-workflow

Workflows with Chrome

Chrome 书签

以下是一些常用的 Chrome 书签,使用步骤:

如何使用

  1. 手动创建书签

    1. 新建书签 新建书签
    2. 使用以下代码替换 URL 使用以下代码替换 URL
    3. 执行 执行
  2. 快速使用示例代码

    1. Chrome、Firefix 直接拖拽到书签栏即可
    2. Safari(我的浏览器版本是:17.0 (19616.1.27.111.16)
      1. 设置-高级-【勾选】显示网页开发者功能
      2. 设置-开发者-【勾选】允许在智能搜索栏中使用 JavaScript
      3. 拖拽 快速使用示例代码 到书签栏即可

TAPD 项目管理 计算花费工时

  1. 工作台 – 仪表盘 – 添加卡片 – 统计工时时长 – 选择项目 – 选择时间 – 生成卡片
  2. 查看全部
  3. 点击书签栏中的 TAPD 项目管理 计算花费工时 即可
javascript: (function () {
    var arr = [];
    var totalTime = 0;
    var trs = document.querySelectorAll('.g_table_autowidth tbody tr');
    var name = document.querySelector('h3.name span').innerText;

    for (let index = 0; index < trs.length; index++) {
        let tr = trs[index];
        let tds = tr.querySelectorAll('td');
        if (tds.length === 8) {
            tds[0].remove();
            tds[1].remove();
            tds[2].remove();
        } else if (tds.length === 6) {
            tds[0].remove();
        }
    }


    var trs = document.querySelectorAll('.g_table_autowidth tbody tr');

    for (let index2 = 0; index2 < trs.length; index2++) {
        let tr = trs[index2];
        let tds = tr.querySelectorAll('td');
        if (!arr.length && tds[0].innerText) {
            totalTime += Number(tds[1].innerText);
            arr.push({
                project: tds[0].innerText,
                countTime: Number(tds[1].innerText)
            });
        } else if(arr.length && tds[0].innerText) {
            var arrExistIndex = arr.findIndex((item) => item.project === tds[0].innerText);
            if (arrExistIndex !== -1) {
                totalTime += Number(tds[1].innerText);
                arr[arrExistIndex].countTime += Number(tds[1].innerText);
            } else {
                totalTime += Number(tds[1].innerText);
                arr.push({
                    project: tds[0].innerText,
                    countTime: Number(tds[1].innerText)
                });
            }
        }
    }
    var str = '';
    for (let index3 = 0; index3 < arr.length; index3++) {
        var {project, countTime} = arr[index3];
        
        var zanbi = (countTime/totalTime * 100).toFixed() / 100;
        str += `${project}\t${countTime}\t${ zanbi ? `${name}${(Math.round(zanbi * 10) / 10)}` : '' }\n`;
    }
    str += ` \t${totalTime}`;
    prompt("复制粘贴到 Excel 中", str);
})();

Yapi body 参数 转换为 JSDoc 注释

  1. 打开 Yapi 项目
  2. 选择接口
  3. 点击书签栏中的 Yapi body 参数 转换为 JSDoc 注释 即可
javascript: (function () {
    function contains(selector, text) {
        var elements = document.querySelectorAll(selector);
        return Array.prototype.filter.call(elements, function(element){
            return RegExp(text).test(element.textContent);
        });
        }
    function capitalizeFirstLetter(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    }

    var trs1 = document.querySelector('.panel-view');

    var apiName = trs1.querySelector('.row .colName').innerText;

    var trs = contains('div', /^Body/)[0].querySelectorAll('.ant-table-tbody tr');

    console.log('trs', trs)

    var str = `/**\n * ${ apiName }\n`;

    for (let index = 0; index < trs.length; index++) {
        let tr = trs[index];
        let tds = tr.querySelectorAll("td");

        str += ` * @param {${ capitalizeFirstLetter(tds[1].innerText) }} data.${ tds[0].innerText } ${ tds[4].innerText }\n`;
    }

    str += ` * @url ${ window.location.href }\n * @returns Promise\n */`;

    var x = document.createElement("DIALOG");
    var t = document.createTextNode(str);

    x.appendChild(t);
    x.style = "white-space: pre;"
    
    document.body.appendChild(x);
    
    x.addEventListener('copy',function(e){
        setTimeout(() => {
            x.hidden = true;
            x.remove();
        }, 320);
    });
    
    x.showModal();
})();

批量删除 Snapmail.cc 单个邮箱内的邮件

www.snapmail.cc

  1. 打开 Snapmail.cc
  2. 选择邮箱
  3. 点击书签栏中的 批量删除 Snapmail.cc 单个邮箱内的邮件 即可
javascript: (function () {
    var lis = document.querySelectorAll('.email-container .sidebar-scrollable-content .email-list li[ng-repeat="item in filtered = (items | orderBy:\'time\':!reverse | filter: search) "]');

    for (let index = 0; index < lis.length; index++) {
        const emailId = lis[index].querySelector('a').getAttribute('href').split('#/email/')[1];

        fetch(`https://www.snapmail.cc/email/${emailId}`, {
        "headers": {
            "accept": "application/json, text/plain, */*",
            "accept-language": "zh-CN,zh-TW;q=0.9,zh;q=0.8,ja-JP;q=0.7,ja;q=0.6,en-US;q=0.5,en;q=0.4,ko-KR;q=0.3,ko;q=0.2",
            "cache-control": "no-cache",
            "pragma": "no-cache",
            "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"macOS\"",
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "same-origin"
        },
        "referrer": "https://www.snapmail.cc/",
        "referrerPolicy": "strict-origin-when-cross-origin",
        "body": null,
        "method": "DELETE",
        "mode": "cors",
        "credentials": "include"
        }).then(res => {
            return res.json()
        });
    }
})();

公众号文章信息

  1. 打开公众号文章
  2. 点击书签栏中的 公众号文章信息 即可
  3. 自动复制内容到剪切板
javascript: (function () {
    function getarticleInfo() {
        let obj = {};
    
        obj['title'] = msg_title || document.title;
        obj['content'] = msg_desc || "";
        obj['author'] = title || "";
        obj['createTime'] = document.querySelector('#publish_time').innerText || new Date().getTime();
        obj['image'] = msg_cdn_url || "";
        obj['blankUrl'] = location.href;
    
        return JSON.stringify(obj, null, 2);
    }
    
    setTimeout(() => {
        navigator.clipboard.writeText(`${getarticleInfo()},`);
    }, 320);
})();

结果内容:

{
  "title": "主播说联播丨大胆闯、大胆试、自主改!",
  "content": "",
  "author": "央视新闻",
  "createTime": "2023-09-26 22:47",
  "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/oq1PymRl9D6Ok0VQvMrI5flK4zRNtnQCKkXwibaq19DHGTCKjaneibGV9Dw7IRzIFDp8ru3ekovHuCE3xSsDPDuQ/0?wx_fmt=jpeg",
  "blankUrl": "https://mp.weixin.qq.com/s/ppTuriLssKstpWL-yqe9HA"
},

公众号文章封面图

  1. 打开公众号文章
  2. 点击书签栏中的 公众号文章封面图 即可
  3. 自动打开两个窗口:1、正方形(1:1) 2、长方形(2.35:1
javascript: (function () {
    cdn_url_1_1 && window.open(cdn_url_1_1);
    msg_cdn_url && window.open(msg_cdn_url);
})();

快速使用示例代码: 拖拽 ta 到 书签栏 👉 <a class="js-drag" href="javascript: (function () {
cdn_url_1_1 && window.open(cdn_url_1_1);
msg_cdn_url && window.open(msg_cdn_url);
})();">公众号文章封面图</a>

统计美团外卖订单金额大于30元的订单

  1. 打开美团外卖订单详情
  2. https://h5.waimai.meituan.com/waimai/mindex/olist
  3. 点击书签栏中的 统计美团外卖订单金额大于30元的订单 即可
javascript: (function () {
    const lis = document.querySelectorAll('div[class^="orderList"] li')
    const 订单列表 = [];
    const 金额 = 30;
    for (let index = 0; index < lis.length; index++) {
        // console.log(lis[index].innerText)
        var li = lis[index];
        var 单个订单金额 = li.querySelector('.price_Ul_Wom').innerText.split('实付¥')[1] * 1;
        if (单个订单金额*1 >= 金额) {
            订单列表.push({
                日期: li.querySelector('*[class^="time"]').innerText.split('\n')[0],
                订单号: li.querySelector('div[class^="orderWrap"]').dataset.id.split('id')[1],
                商家: li.querySelector('*[class^="name"]').innerText,
                金额: li.querySelector('.price_Ul_Wom').innerText.split('实付¥')[1]
            })
        }
    }
    setTimeout(() => {
        navigator.clipboard.writeText(`${JSON.stringify(订单列表.reverse(), null, 2)}`);
    }, 320);
})();

结果内容:

[
  {
    "日期": "2023-01-01 00:00",
    "订单号": "8888888888888888888",
    "商家": "麦当劳&麦咖啡(北京西大望路店)",
    "金额": "30.0"
  }
]

统计美团外卖在某个商家下的订单

小商家,订单满100,1000,统一开发票,需要统计订单

  1. 打开美团外卖订单详情
  2. https://h5.waimai.meituan.com/waimai/mindex/olist
  3. 点击书签栏中的 统计美团外卖在某个商家下的订单 即可
javascript: (function () {
    var lis = document.querySelectorAll('div[class^="orderList"] li')
    const 订单列表 = []
    const 商家 = '门框卤煮(双井店)';
    for (let index = 0; index < lis.length; index++) {
        // console.log(lis[index].innerText)
        var li = lis[index]
        if (商家 === li.querySelector('*[class^="name"]').innerText) {
            订单列表.push({
                日期: li.querySelector('*[class^="time"]').innerText.split('\n')[0],
                订单号: li.querySelector('div[class^="orderWrap"]').dataset.id.split('id')[1],
                金额: li.querySelector('.price_Ul_Wom').innerText.split('实付¥')[1]
            })
        }
    }
    setTimeout(() => {
        navigator.clipboard.writeText(`${JSON.stringify(订单列表.reverse(), null, 2)}`);
    }, 320);
})();

当前页面以窗口形式打开

  1. 打开页面(浏览器非全屏模式下)
  2. 点击书签栏中的 当前页面以窗口形式打开
  3. 输入窗口宽度:默认 540
  4. 输入窗口高度:默认 800
javascript: (function () {
    var defaultWidth = 540, defaultHeight = 800;
    var openWindowWidth = window.prompt('窗口宽度(width)', defaultWidth);
    var openWindowHeight = window.prompt('窗口高度(height)', defaultHeight);
    var screenHeight = window.screen.height;
    var screenWidth = window.screen.width;
    var screenX = (screenHeight - openWindowHeight) / 2;
    var screenY = (screenWidth - openWindowWidth) / 2;
    var windowFeatures = 'height='+ openWindowHeight +', width=' + openWindowWidth + ', top=' + screenX + 'px, left=' + screenY + 'px, toolbar=no, menubar=no, scrollbars=no, resizable=no, location=no, status=no';
    
    window.open(window.location.href, 'newwindow', windowFeatures);
})();
  • toolbar:工具栏
  • menubar:菜单栏
  • scrollbars:滚动条
  • resizable:是否可以调整窗口大小
  • location:是否显示地址栏
  • status:是否显示状态栏

生成二维码

依赖 2.webclown.net

  1. 打开页面
  2. 点击书签栏中的 生成二维码
  3. 自动打开新窗口,显示二维码
javascript: (function () {
    var baseUrl = 'http://2.webclown.net/?';
    var url = baseUrl + 'url=' + encodeURIComponent(location.href);
    window.open(url);
})();

快速使用示例代码: 拖拽 ta 到 书签栏 👉 <a class="js-drag" href="javascript: (function () { var baseUrl = ‘http://2.webclown.net/?’; var url = baseUrl + ‘url=’ + encodeURIComponent(location.href); window.open(url); })();">生成二维码</a>

复制转载信息

  1. 点击书签栏中的 复制转载信息
  2. 自动复制内容到剪切板
javascript: (function () {
    navigator.clipboard.writeText('[' + document.title + '](' + window.location.href + ')');
})();

快速使用示例代码: 拖拽 ta 到 书签栏 👉 <a class="js-drag" href="javascript: (function () { navigator.clipboard.writeText(‘[‘ + document.title + ‘](‘ + window.location.href + ‘)’);})();">复制转载信息</a>

生成短网址

  1. 点击书签栏中的 生成短网址
  2. 自动复制内容到剪切板
javascript: !(function () {
    var myHeaders = new Headers();
    myHeaders.append("Content-Type", "application/json");
    
    var raw = JSON.stringify({
      longUrl: window.location.href,
    });
    
    var requestOptions = {
      method: "POST",
      headers: myHeaders,
      body: raw,
      redirect: "follow",
    };
    
    fetch("https://00s.top/api/shorten", requestOptions)
      .then((response) => response.json())
      .then((result) => {
        if (result.shortUrl) {
          const link = "https://00s.top/" + result.shortUrl;
          const dialog = document.createElement("dialog");
          dialog.innerHTML =
            "<div class='position: fixed;top: 1em;right: 1em;background: #fff;'><p style='text-align: center; padding: 1em; background: #1d91eb; color: #fff;'>短网址已生成:(自动复制)</p><p style='padding: 1em;'>" +
            link +
            "</p></div>";
          document.body.appendChild(dialog);
          dialog.showModal();
          navigator.clipboard.writeText('[' + document.title + '](' + link + ')');
          setTimeout(() => dialog.close(), 2000);
        }
      })
      .catch((error) => {
          const dialog = document.createElement("dialog");
          dialog.innerHTML =
            "<div class='position: fixed;top: 1em;right: 1em;background: #fff;'><p style='text-align: center; padding: 1em; background: #1d91eb; color: #fff;'>生成失败</p><p style='padding: 1em;'>当前站点禁止使用书签脚本,请访问:https://00s.top 生成。</p><form method='dialog'><button>关闭</button></form></div>";
          document.body.appendChild(dialog);
          dialog.showModal();
      });
})();

生成短网址:成功

生成短网址:失败

K12-教育资料合集

这个网站不错,可以生成不同格式的纸张类别:笔记本、康奈尔笔记、米字格、方格纸、网格纸、备忘录、漫画稿纸、五线谱、麦肯锡笔记等。

英文中的连字符和下划线

英文中的连字符(-)和下划线(_)

起因

下面两种命名方式,在编辑器双击 button_height 全部选中,编辑器双击 button-height 不能全部选中,编辑器为什么这样设计?

  1. button-height
  2. button_height

在 页面仔 群问了一下,得到了答案:

linxz:因为 – 是连字符

linxz:从英文单词的角度去考虑,这里算是两个单词

linxz:而双击选中一般是选择单词,中文是选择词

连字符(hyphen)

牛津词典

  • 将多个或更多的词组成复合词
    • hard-hearted(铁石心肠)
    • fork-lift truck(叉车)
    • mother-to-be(孕妇)
  • 将前缀和转悠名词组成复合词
    • pre-Raphaelite(前拉斐尔派)
    • pre-European(欧洲前史)
  • 书写21-99之间的复合数字时
    • seventy-three(73)
    • thirty-one(31)
  • 有时用在英式英语中用以将以元音结尾的前缀与以相同元音开始的词隔开
    • co-operate(合作)
    • pre-eminent(卓越的)
  • 转行时用于单词的前半部分之后

下划线(underscore)

牛津词典 中 这个不算标点符号,在这个单词 underscore 的释义下说明:下划线、底线,用于字母下划线或计算机命令和互联网网址中。

在计算机中,一般用于不允许出现空格的地方,比如:

  • 文件名
    • The_Dark_Knight_2008_1080p.mkv
  • 变量名
    • const MAX_LENGTH = 100;
  • 函数名
    • function get_user_info() {}
  • 等等

拓展

如何配置编辑器(Visual Studio Code),使得双击 button-height 全部选中?

  1. 打开 settings.json 文件
  2. 添加/编辑如下配置
    {
        ...
        // "editor.wordSeparators": "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?", // 默认配置,去掉 -
        "editor.wordSeparators": "`~!@#$%^&*()=+[{]}\\|;:'\",.<>/?",
        ...
    }
    
  3. 保存配置文件,即可

更多配置阅读查看:Visual Studio Code User and Workspace Settings

参考

  1. 如何使用英文连字符 – Wordvice Blog
  2. Underscore | Punctuation | Grammar | Glossary | Ultius
  3. Underscore – Wikipedia
  4. underscore, n. meanings, etymology and more | Oxford English Dictionary
  5. 牛津词典

element-plus el-button 增加 v-blur 指令

element-plus Button focus

element-plus el-button 增加 v-blur 指令

背景 & 问题

解决点击按钮后,触发 el-dialog 等弹窗组件的显示交互,弹窗隐藏之后,按钮的 focus 依然存在的问题。

🤔 思考:这个属于正常的交互,可以理解为:用户点击的哪里触发的这个弹窗,弹窗隐藏之后,用户的焦点应该回到点击的地方。

如果产品经理要求,弹窗隐藏之后,隐藏的按钮不要 focus,那么就需要增加一个指令来解决这个问题。

解决方案

1、在项目中新增一个指令文件 src/directives/blur/index.ts,内容如下:

src/directives/blur/index.ts

import { Directive } from "vue";

export const blur: Directive = {
  mounted(el) {
    if (!el) return;
    /**
     * 是否是点击触发的 blur
     *  - 点击触发 blur 时,不触发 focus
     *  - 非点击触发 blur 时,触发 focus
     */
    let isClick = false;
    el.addEventListener("click", () => {
      isClick = true;
    });
    el.addEventListener("focus", () => {
      if (isClick) {
        el.blur();
      }
    });
    el.addEventListener("blur", () => {
      isClick = false;
    });
  }
};

2、在 main.ts 中引入该指令:

main.ts

import App from "./App.vue";
import { createApp } from "vue";

import { blur } from "./directives/blur";

const app = createApp(App);

// ...

app.directive("blur", blur);

app.mount("#app");

3、在需要使用的地方,使用 v-blur 指令即可:

<script setup lang="ts">
defineOptions({
  name: "Demo"
});

const showDialog = () => {
  // ...
};
</script>

<template>
  <div class="demo">
    <el-button v-blur @click="showDialog">添加</el-button>
  </div>
</template>

<style lang="scss" scoped>
// ...
</style>

最终方案(2023-08-03 更新)

使用 css 覆盖即可解决:

.el-button:focus-visible {
  outline: 0;
}

element-plus Button focus after

stylelint ELIFECYCLE  Command failed with exit code 129.

在项目中执行 stylelint 时,报错如下:

ELIFECYCLE  Command failed with exit code 129.

大概率是因为 stylelint 规则配置有问题,排查了好久,最后因为一个element-plus的一个变量命名导致的。

src/components/TableHeader/base/filterButton.vue
 288:16  ✖  Expected custom property name "--el-dropdown-menuItem-hover-color" to be kebab-case  custom-property-pattern

排查过程中,stylelint 始终输出这个行报错,经过以下步骤排查,最终定位到问题:

  1. 删除 node_modules,重新安装依赖
  2. 删除 pnpm-lock.json,重新安装依赖
  3. 删除 .cache,重新安装依赖
  4. 查看 stytlelint 最新是否有新版本更新,确实有,更新到最新版本
  5. 重新执行 stylelint,报错信息出现

制作一个简单的Link Preview服务

1. 什么是Link Preview

Link Preview是指鼠标移动到一个超链接上,会自动显示该链接的标题、描述、图片等信息,如下图所示:

维基百科

当我们在浏览维基百科时,会发现在发送维基百科的链接时,会自动显示维基百科的标题、描述、图片等信息,这就是Link Preview的效果。效果有点类似,标签的 title 属性,但是Link Preview的效果更加丰富,而且不需要等待太长时间。

也是最近逛推的时候,发现了这个网站:Cali Castle,增加了这个功能,大概实现是一个get请求返回了一张图片,然后在页面上显示了这张图片,这个图片就是Link Preview的效果。

Cali Castle

2. 如何实现(待更新)

待更新……

  1. 接口能力:使用 nodejs 实现一个接口,接收一个 url 参数,返回一个图片。
  2. 业务开发:使用 API 返回的图片。展示出来(显示隐藏等逻辑)。

3. 本地开发(待更新)

  • chrome-aws-lambda
  • puppeteer

4. 部署

这里提供了两种部署方式:Vercel、阿里云;因为我在Vercel上部署的时候,发现内存不足<sup> [问题1] </sup>,部分功能受到限制,所以就在阿里云上部署了。

4.1 部署到Vercel(待更新)

需要注意的是, Vercel 的免费版,内存有限制,我们只能实现基本的功能,访问日志和缓存截图文件<sup> [1] </sup>:

4.2 部署到阿里云ECS

4.2.1 安装依赖

我的服务器是阿里云的ECS,系统是CentOS Linux release 7.9.2009,安装以下依赖:

  • alsa-lib.x86_64
  • atk.x86_64
  • cups-libs.x86_64
  • gtk3.x86_64
  • ipa-gothic-fonts
  • libXcomposite.x86_64
  • libXcursor.x86_64
  • libXdamage.x86_64
  • libXext.x86_64
  • libXi.x86_64
  • libXrandr.x86_64
  • libXScrnSaver.x86_64
  • libXtst.x86_64
  • pango.x86_64
  • xorg-x11-fonts-100dpi
  • xorg-x11-fonts-75dpi
  • xorg-x11-fonts-cyrillic
  • xorg-x11-fonts-misc
  • xorg-x11-fonts-Type1
  • xorg-x11-utils
yum install -y alsa-lib.x86_64 atk.x86_64 cups-libs.x86_64 gtk3.x86_64 ipa-gothic-fonts libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXrandr.x86_64 libXScrnSaver.x86_64 libXtst.x86_64 pango.x86_64 xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-fonts-cyrillic xorg-x11-fonts-misc xorg-x11-fonts-Type1 xorg-x11-utils

安装依赖项后,您需要使用此命令更新 nss 库

yum update nss -y

参见官方文档:Chrome doesn’t launch on Linux

需要安装依赖,否则会报一些奇怪的错误 <sup> [问题2] </sup>。

4.2.2 执行部署

把代码上传到服务器。安装依赖:

阿里云上安装依赖,需要使用淘宝的镜像源,否则会报错,安装依赖:

cnpm install

测试代码是否可以正常运行:

node index.js
> Server is start 9000.

打开一个新的终端,测试接口是否正常:

curl http://localhost:9000/api?url=https://webclown.net

注意这里返回的是一个图片实体文件,终端输出内容比较多。

配置nginx,配置文件如下:

server {
    listen       80;
    server_name  webclown.com;

    location /api {
        proxy_pass http://localhost:9000;
        proxy_redirect http://localhost:9000 https://webclown.net;
    }
}

如果真正用于生成环境,设置防盗链功能,防止被恶意盗用,简单的防盗链可以以 referer、user Agent 为条件,设置防盗链,如下所示:

server {
    ...
    location /api {
        # 如果 reffer 不是 webclown.net 则返回 403
        if ($http_referer !~* (webclown.net)) {
            return 403;
        }
        # 如果没有 ua 则返回 403
        if ($http_user_agent = "") {
            return 403;
        }
        proxy_pass http://localhost:9000;
        proxy_redirect http://localhost:9000 https://webclown.net;
    }
    ...
}

问题

  • 问题1:Vercel 内存不足,无法正常运行。
    Error: Runtime exited with error: exit status 1
    Runtime.ExitError
  • 问题2:阿里云上,无法正常运行,没有安装依赖。
    error while loading shared libraries: libatk-bridge-2.0.so.0: cannot open shared object file: No such file or directory

注:

  1. 缓存截图文件:以链接的上的query参数的 md5 作为文件名,生成缓存截图文件,加快访问速度,这里也只是简单的实现,不考虑文件的持久化储存、定时清理等边界功能,这是一个复杂的点,这里不展开说明。

参考资料:

  1. Express
  2. Vercel
  3. Puppeteer
  4. alixaxel / chrome-aws-lambda
  5. Chrome doesn’t launch on Linux
  6. Cali Castle
  7. michaelkitas / Puppeteer-Vercel

获取统计相关免费数据源

Trading Economics

https://tradingeconomics.com/

世界银行

https://www.worldbank.org/en/home

Our World in Data

https://ourworldindata.org/

OECD

https://data.oecd.org/

IMF

https://www.imf.org/en/Data

中国统计局

http://www.stats.gov.cn/

TIPS

尽可能查找一手信息,一手信息的价值远远大于二手信息。

  • 一个国家数据
    • GDP
    • CPI
    • 就业率
    • ……
  • 一个公司
    • 公司年报
    • 创始人访谈
    • ……