LOADING

加载过慢请开启缓存 浏览器默认开启

IceOfSummerの博客

还是自己搭的博客靠谱

后面一辈子的博客都在这了!

K8s

k8s 2024/2/8
阅读全文

从0创建一个nodejs + Typescript项目

2024/1/19

因为踩了很多坑,所以记录一下这个项目是怎么搭的。

最后用了这些东西:

  • Typescript
  • Webpack(打生产的包)
  • Eslint

因为是nodejs项目,所以这边我还研究了很久怎么在 ts 文件上打断点进行debug,在开发环境下是不会用到 webpack 的。

nodejs版本:18。

1. 初始化项目

npm init创建一个package.json文件,然后安装所需的依赖:

yarn add eslint @typescript-eslint/parser --dev
yarn add typescript ts-loader ts-node tsconfig-paths --dev
yarn add webpack webpack-cli source-map-support --dev

之后在package.json里添加"type": "module"的属性,这样就可以直接在项目里直接使用ESModules了,这个东西在webpack里天生支持按需导入,不需要额外配置(要了解更多的话可以去搜Tree Sharking)。

2. 配置eslint

这步比较简单,就直接过了,基本没有什么坑。

// .eslintrc.cjs
module.exports = {
  // 下面这行必须加
  parser: '@typescript-eslint/parser',
  rules: {
    // 这些是我常用的一些规则
    quotes: ["error", 'single'],
    'key-spacing': ["error", { "beforeColon": false }],
    semi: [2, 'never'],
    'block-spacing': 'error',
    'object-curly-spacing': ["error", "always"],
    indent: ['error', 2]
  },
  // 这里也要加,不然用import会报错
  parserOptions: {
    "ecmaVersion": 7,
    "sourceType": "module"
  }
};

3. 配置Typescript

这里的配置都是可以直接用的,碰到的坑在后面说。

// tsconfig.json
{
  "include": ["src/**/*.ts"],
  "compilerOptions": {
    "module": "esnext",
    "lib": ["ES2022"],
    "isolatedModules": true,
    "esModuleInterop": true,
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "target": "ESNext",
    "strict": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "sourceMap": true,
    "outDir": "dist/dev",
    "paths": {
      // 记住这个别名,待会要考
      "~/*": ["./src/*"]
    }
  },
  "ts-node": {
    "experimentalSpecifierResolution": "node",
    "esm": true,
    "transpileOnly": true
  }
}

后缀问题

这里碰到的第一个坑,就是后缀问题。

这个问题只有在 nodejs + esm 才会有,什么意思呢,来看下面的代码:

// ------------------------------------------
// util.ts
export default "hello world"

// ------------------------------------------
// index.ts
import util from './util'

console.log(util)

看上去没有什么问题,然后我们用ts-node执行一下:

 throw new ERR_MODULE_NOT_FOUND(
          ^
CustomError: Cannot find module 'xx\src\util' imported from xx\src\index.ts

如果这个时候你去网上搜,你基本碰到的回答都是让你加上js后缀:

// index.ts
import util from './util.js'

console.log(util)

首先不说这个丑的一批,而且我后面还发现这玩意还会导致另外一个bug:在用webpack打包的时候,如果你加了js后缀,webpack会直接提醒你找不到xx/src/util.js,坑爹呢这不是!

所以肯定是不能加后缀的,然后我也是在网上翻了好久,才找到这个参数:experimentalSpecifierResolution,虽然前面带了个experimental,但其实已经很稳定了,直接在tsconfig.json中添加配置:

{
    // ...
    "ts-node": {
        // 把值改为node
        "experimentalSpecifierResolution": "node",
        // 这个忘了当时为啥要加了,不加好像也不会报错
        "esm": true
    }
}

或者使用命令行参数:--experimental-specifier-resolution=node

加完之后,不带文件后缀也可以成功运行,webpack打包也不会有任何影响。

别名问题

可以看到我开头提供的tsconfig.json里面有个这样的配置:

{
    "paths": {
      // 记住这个别名,待会要考
      "~/*": ["./src/*"]
    }
}

例如我们有这样的目录结构:

src
├── util
│   └── StringUtils.ts
└── index.ts

我们在index.ts里面导入StringUtils就可以这样写:

import StringUtils from '~/util/StringUtils'

这个功能其实可有可无,但是我就是有强迫症,就是不想用相对路径!

首先啥都不加,直接ts-node运行,居然还报错了:

throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base));
        ^
CustomError: Cannot find package '~' imported from 'xxx/src/index.ts'

好好好,这都能报错,去查了一下,才知道这玩意是给webpack那些玩意提供声明的:

// webpack.config.cjs
module.exports = {
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
        alias: {
            '~': path.resolve(__dirname, 'src')
        }
    },
}

在这里,你只写webpack的别名配置,在 ts 里是会报错的,因为ts才不会管你webpack的配置,所以才需要我们的tsconfig.json来提供一个声明。

行,报错了我就去搜,几下就搜到了,不就是加个tsconfig-paths吗,加上命令行参数:-r tsconfig-paths/register,开跑!

结果万万没想到,又爆了相同的错。。。。

然后又翻了很久的Github(真的很久),终于被我找到了:ESM loader that handles TypeScript path mapping

复制里面的loader.js,然后修改启动命令为:node --loader ./scripts/loader.js src/index.ts,就可以正常使用别名了。

使用WebStorm调试代码

因为我们是nodejs项目,肯定是不能少了打断点调试的。

我们可以直接用tsc编译项目为js代码后,直接用Webstorm进行Debug。

因为Webstorm运行js文件基本不需要配置,直接右键点几下就跑起来了。

但是这样很傻批,我们还要分两步进行,而且 ts 文件的变动可能会导致我们在 js 文件上打的断点消失。


谢天谢地,Webstorm是真的很聪明(牛逼),我们只需要简单配置几下就可以直接在 ts 上打断点运行了。

run configuration

配完后,直接在ts文件上断点就可以停住。

4. 配置Webpack

至于为什么要用 Webpack,是因为我最后不想带着node_modules这个累赘来上生产,最后直接打包成一个文件多爽,直接node xxx.js就跑起来了。

这里是我最后用到的配置:

// webpack.config.cjs
const path = require('path');
const webpack = require('webpack')

module.exports = function (env, args) {
    return {
        entry: './src/index.js',
        target: 'node',
        module: {
            rules: [
                {
                    test: /\.tsx?$/,
                    use: 'ts-loader',
                    exclude: /node_modules/,
                },
            ],
        },
        resolve: {
            extensions: ['.tsx', '.ts', '.js'],
            alias: {
                '~': path.resolve(__dirname, 'src')
            }
        },
        output: {
            filename: (pathData) => {
                return pathData.chunk.name === 'main' ? 'main.cjs' : 'libs.cjs';
            },
        },
        plugins: [
            new webpack.SourceMapDevToolPlugin({
                exclude: ['libs.cjs']
            })
        ],
        optimization: {
            splitChunks: {
                chunks: 'all'
            },
        },
    };
}

注意文件后缀是cjs,不然用module.exports会报错。

打包时直接用webpack --mode=production就可以了。

配置sourcemap

在Webpack打包后,我们的所有代码都被压缩到一行了,而且变量名都变得六亲不认了,想象一下,假如运行过程中报一个错,你能定位到问题发生在哪吗。。。

所以这个时候我们需要用到 sourcemap 来对我们的代码进行索引。

直接使用 sourcemap 文件是不行的,因为这玩意是给浏览器用到,我们需要导入依赖 source-map-support 来加载sourcemap。

这里你可以把 sourcemap 分离成一个单独的文件,也可以让它内嵌到代码里面。

这里我推荐内嵌到代码里面,便于后面代码分发,没必要分出来。

在 Webpack 添加配置devtool: 'inline-source-map'

还没完,也要在tsconfig.json里面添加"sourceMap": true的配置,如果少了这一步,最终生成的 sourcemap 行数会对不上,因为这个时候 Webpack 只会对编译后的 js 文件来构建索引,而 ts 编译后的文件中,空行(一行什么内容都没有的)会被删除,因此导致行数对不上。

最后在代码入口添加加载的代码:

import sourceMapSupport from 'source-map-support'

sourceMapSupport.install()

进一步压缩

如果你观察生成的文件,会发现生成 sourcemap 会导致文件变得非常大,基本会变大 5 ~ 6 倍左右。

如果你把 sourcemap 分成单独的文件,然后打开开一下,会发现 Webpack 也给 node_modules 里面的代码生成了 sourcemap!

作为一个强迫症患者,我是绝对不能忍受这种情况的!

我们肯定是想给自己的代码生成精准的 sourcemap,而第三方库,可以考虑不生成,或者只使用简单的 sourcemap。

翻了一下 Webpack 文档,发现有个 SourceMapDevToolPlugin 插件可以指定/排除为哪些模块生成 sourcemap。

试了一下,在exclude属性里面不管怎么填,都无法忽略掉node_modules

在查了一下午的文档以及翻看了源码之后,终于知到怎么配了:

// webpack.config.cjs
module.exports = {
  output: {
    filename: (pathData) => {
      return pathData.chunk.name === 'main' ? 'main.cjs' : 'libs.cjs';
    },
  },
  plugins: [
    new webpack.SourceMapDevToolPlugin({
        exclude: ['libs.cjs']
    })
  ],
  optimization: {
    splitChunks: {
        chunks: 'all'
    },
  },
}

加上上面的配置,就可以做到把node_modules里面的代码全部打到libs.cjs中,而我们的业务代码全部打到main.cjs中,同时配置我们的SourceMapDevToolPlugin不为libs.cjs生成 sourcemap。

DLC:node20版本

之前导入模块我们为了省略后缀,在配置中添加了experimentalSpecifierResolution: node参数,在node20上,这个参数仍然可用,但是已经有了更好的替代。

文档:Loaders

并且官方也给了一个样例来代替上面的启动参数:commonjs-extension-resolution-loader

这里直接摆上我用的代码:

// extension-loader.js
/**
 * 处理ts-node导入时必须加后缀
 */
// https://github.com/nodejs/loaders-test/blob/main/commonjs-extension-resolution-loader/loader.js
import { isBuiltin } from 'node:module'
import { dirname } from 'node:path'
import { cwd } from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { promisify } from 'node:util'

import resolveCallback from 'resolve/async.js'

const resolveAsync = promisify(resolveCallback)

const baseURL = pathToFileURL(cwd() + '/').href


export async function resolve(specifier, context, next) {
  const { parentURL = baseURL } = context

  if (isBuiltin(specifier)) {
    return next(specifier, context)
  }

  // `resolveAsync` works with paths, not URLs
  if (specifier.startsWith('file://')) {
    specifier = fileURLToPath(specifier)
  }
  const parentPath = fileURLToPath(parentURL)

  let url
  try {
    const resolution = await resolveAsync(specifier, {
      basedir: dirname(parentPath),
      // For whatever reason, --experimental-specifier-resolution=node doesn't search for .mjs extensions
      // but it does search for index.mjs files within directories
      extensions: ['.js', '.json', '.node', '.mjs', '.ts'],
    })
    url = pathToFileURL(resolution).href
  } catch (error) {
    if (error.code === 'MODULE_NOT_FOUND') {
      // Match Node's error code
      error.code = 'ERR_MODULE_NOT_FOUND'
    }
    throw error
  }

  return next(url, context)
}
// path-loader.js
/**
 * 处理ts路径别名报错
 */
import { resolve as resolveTs } from 'ts-node/esm'
import * as tsConfigPaths from 'tsconfig-paths'
import { pathToFileURL } from 'url'

const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig()
const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths)

export async function resolve (specifier, ctx, defaultResolve) {
  const match = matchPath(specifier)
  let realPath
  if (match) {
    realPath = pathToFileURL(`${match}`).href
  } else {
    realPath = specifier
  }
  const r = await defaultResolve(realPath, ctx)
  return resolveTs(r.url, ctx, defaultResolve)
}

export { load, transformSource } from 'ts-node/esm'
// register-hooks.js
import { register } from 'node:module'

register('./extension-loader.js', import.meta.url)
register('./path-loader.js', import.meta.url)

然后把我们的启动命令换成:node --import register-hooks.js src/index.ts

移除掉tsconfig.json里的experimentalSpecifierResolution,然后就可以正常启动了。

阅读全文

Rust

Rust 2023/12/4
阅读全文

TypeScript骚操作

前端 2023/11/27

可变参数

写过ReactNative的都知道,ReactNavigation库里面有一个非常牛逼的类型声明,它可以根据你传入的参数,来判断是否需要第二个参数。

例如下面的定义:

interface RouteDef {
    home: undefined
    shop: {
        time: number
    }
}

那么它的路由方法就变得很牛逼了:

// 实际的泛型肯定不是这么传的,因为我写的时候没用RN了,所有忘了咋用的了
const nav = useNavigation<RouteDef>()

// 正确
nav('home')

// 错误: ts提示这里只需要一个参数 
nav('home', undefined)

// 错误,ts提示这里需要两个参数
nav('shop')

// 正确
nav('shop', { time: Date.now() })

// 错误: 第二个参数需要{ time: number }类型,而提供的是number
nav('shop', Date.now())

很牛逼啊有没有,连参数的数量都给你变了!而且还能根据属性的定义来决定参数。

那么这玩意具体是咋写的呢?我仔细研究了一下,研究完后,仿佛开启了新大门!

我这里直接写一个Demo来看:

type EmitFuncArgs<Events, Key extends keyof Events> = void extends Events[Key]
  ? [evt: Key]
  : [evt: Key, data: Events[Key]]

type EmitFunc<Events> = <T extends keyof Events> (...args: EmitFuncArgs<Events, T>) => string

export interface AppEvents {
  ON_LOGOUT: string
  ON_LOGIN: void
}

先来看下面的EmitFunc定义,首先这玩意的泛型接收一个Events类型(直接把它看成下面的AppEvents接口就行),然后它的参数是EmitFuncArgs
决定的。

来看EmitFuncArgs,可以发现这玩意居然返回了一个数组,并且还用上了三目运算符。

到了这里,大伙都应该可以理解是怎么玩的了,就是首先判断值是不是void,如果是,则返回一个长度为1的数组,反之则返回长度为2的数组,其中的第二个
参数为接口定义的类型。

阅读全文

Linux用户权限管理

2023/11/13

最近突然碰到有关用户授权的问题了,发现自己真的是一点都不会,所有写篇博客记一下常见指令以及碰到过的坑。

1. 常用指令

1.1 用户

  • 新增用户:useradd [username]
  • 设置用户密码:passwd [username]
  • 删除用户:userdel [username]
  • 将用户添加到用户组(a为追加):usermod -aG [groupname] [username]

1.2 用户组

  • 新建用户组:groupadd [groupname]

1.3 授权

1.3.1 chown

chown用于修改文件/文件夹的所属权以及其所属的用户组。

修改文件/文件夹所属用户

chown [username] [directory/file]
# 递归授权
chown -R [username] [directory]

修改文件/文件夹所属用户组

chown :[usergroup] [directory/file]
# 递归授权
chown -R :[usergroup] [directory]

修改文件/文件夹所属用户和用户组

chown [username]:[usergroup] [directory/file]
# 递归授权
chown -R [username]:[usergroup] [directory]

1.3.2 chmod

chmod用于细化对所有者,用户组,以及其它用户的相关权限。

一共用三种类型的用户,它们的名字以及符号为:

  • 所有者:u
  • 用户组:g
  • 其它:o

例如使用ll输出的信息:

drwx--x--x 2 username groupname83 11月 13 15:28 folder

第一部分开头的字符表示的是文件的类型:

  • 文件夹:d
  • 文件: - (一个横杠,不管是可执行文件还是普通文件)

后面连续9个字符,每3个为一组,分别表示用户所有者、用户组、其它用户的权限。
例如上面的例子:

  • 所有者:可读(r)、可写(w)、可执行(x)
  • 用户组:可执行(x)
  • 其它:可执行(x)

修改用户权限

例如要给用户组(u)添加读和写的权限:

chmod g+rw [directory/file]
chmod -R g+rw [directory]

移除权限:

chmod g-rw [directory/file]

很简单,也就是中间有个加减符号,其实还有个等号,这个就类似于替换了,会覆盖掉之前的权限。

另外,给所有用户授权用a就行了:

chmod a+rw [directory/file]

其实也可以用数字替代rwx,但是这样有点不便于记忆。。

2. 应用

2.1 文件夹授权了却打不开?

提问:是不是只要某个用户有一个文件夹的读权限,就能打开文件夹了?

大部分可能都会是这样认为的,这也确实比较符合我们的认知,都能读了,还不能打开文件夹?

那么实际呢?我们来试一下。

我们用测试用户(testuser)来进行测试:

[testuser@localhost opt]# ll
drwx---r--  2 root   root         36 11月 13 16:44 backup

这里我们可以看到,testuser拥有backup文件夹的读权限,尝试进入一下:

[testuser@localhost opt]$ cd backup
bash: cd: backup: 权限不够

发现权限是不够的…

这里也不卖关子了,这里其实是需要执行权限才能进入文件夹,这里也是比较容易忽略的一点。

2.2 可执行文件授权了却打不开?

提问:是不是某个用户只要有一个文件的可执行权限,就可以直接执行文件了?

这里就不卖关子了,答案是不一定。

同样这也是一个非常容易被忽略的问题,以为只要给了可执行权限就能执行了。

来演示一下反例:

drwx------  2 bim   root         36 11月 13 16:44 backup

这里我们的backup文件夹里面放有可执行文件,但是这个文件夹没有对外授权。

[root@localhost backup]# ll
总用量 23796
-rwx-----x 1 bim root 12691576 11月 13 16:44 mysql
[root@localhost backup]# pwd
/opt/backup

在里面的可执行文件mysql,对其它人拥有可执行权限,我们来切换用户试一下:

[root@localhost backup]# su testuser
[testuser@localhost backup]$ /opt/backup/mysql
bash: /opt/backup/mysql: 权限不够

可以发现权限不够,即使你拥有可执行权限。

解决方法在前面也说了,只需要给可执行文件的父目录,也就是这里的backup目录授权可执行就行了,这里就不多演示了。

阅读全文

记一次Runtime.exec遇到的坑

首先来看一段代码(Java17, Java8同样也有这个问题):

Runtime runtime = Runtime.getRuntime();
runtime.exec("docker exec mysql-test2 mysql -ubim -pxx -h11.11.11.11 -P3306 bim -e \"use bim;source bim.sql;\"");

这段代码是用来执行mysqldump备份出来的mysql文件,咋一看好像没问题。

运行后,byd好像也确实没什么问题👿👿👿

如果真的没问题就好了,那么也不会有这篇博客了。

写完后打包丢到Linux上去跑的时候,你就会发现。。。

运行后发现执行失败了,exitCode为1,看了一下它的输出,它居然直接把mysql的帮助菜单给打印出来了???

见过离谱的,没见过这么离谱的,我这个指令可是跟帮助菜单一点关系都没有啊?


如果大家去搜java怎么去执行命令行指令的时候,可能会得到两个结果,一种就是用Runtime,另外一个就是用ProcessBuilder,大部分人可能都会用Runtime,因为这玩意给ProcessBuilder封装了一层,用起来方便,直接一个exec把指令丢进去就可以了。

也就是因为这个,突然想起来之前在python里面,想要执行shell命令必须要把指令以数组的形式传进去(其实也可以使用shell=True参数),而ProcessBuilder也是这样,你直接丢一个字符串进去是执行不了的,必须要传数组进去。

到这里就怀疑Runtime是不是直接暴力调用了split(" "),然后把参数丢给ProcessBuilder,结果看了下源码,还真是这样:

public Process exec(String command, String[] envp, File dir)
    throws IOException {
    if (command.isEmpty())
        throw new IllegalArgumentException("Empty command");

    StringTokenizer st = new StringTokenizer(command);
    String[] cmdarray = new String[st.countTokens()];
    for (int i = 0; st.hasMoreTokens(); i++)
        cmdarray[i] = st.nextToken();
    return exec(cmdarray, envp, dir);
}

StringTokenizer可能大家没见过,但是如果你用java写算法,并且了解过输入优化,你就会知到这玩意是干嘛的。简单点来说它的效果和Scanner一样,但是效率更高(如果你写过算法就知道这玩意速度吊打Scanner),Scanner就不多讲了,感觉是个人就用过。。。

如果你还看不懂,没事,我直接给你上图:
debug

可以发现我们后面用双引号包裹起来的参数被分开了,实际传到mysql那里就会导致执行失败。

但是这玩意在windows上能执行成功也是很离谱的。

知到原因后,直接改用ProcessBuilder手动控制参数:

Process process = new ProcessBuilder(backupConfig.getMysqlPath(),
                    "-u" + backupConfig.getUsername(),
                    "-p" + backupConfig.getPassword(),
                    "-h" + backupConfig.getHost(),
                    "-P" + backupConfig.getPort(),
                    "-e",
                    String.format("\"use %s;source %s;\"", ignore, ignore))
                    // 标准错误流重定向到标准输出,方便拿错误信息
                    .redirectErrorStream(true)
                    .start();

看到我的String.format没,我这里用引号包起来了好让他们是一个整体。

完?。。


丢到服务器上跑,结果又报错了👿,不过至少这次没打帮助菜单,提示""use %s;source %s;""不是一个mysql指令。

byd原来引号是自作多情多加上了,最后把引号给删掉就能跑起来了。。

这bug也是花了我挺多时间的吧,一开始以为是mysql的问题,结果居然是jdk自己的问题。

阅读全文
1 2 3 ... 4
avatar
IceOfSummer

这个人很懒,没有个人简介