Skip to main content

· 2 min read

对于大多数现代编码系统(如 UTF-8),每个汉字通常占用 3 个字节,而每个英文字母、数字和常见符号通常占用 1 个字节。

如下示例,让我们逐个字符计算:

Q3NKA综合-头客-优选服务立减测试_优惠详情:7418945413963368458
  • Q3NKA:5 个英文字母和数字,每个占 1 个字节,共 5 个字节。
  • 综合:2 个汉字,每个占 3 个字节,共 6 个字节。
  • -:2 个减号,每个占 1 个字节,共 2 个字节。
  • 头客:2 个汉字,每个占 3 个字节,共 6 个字节。
  • 优选服务立减测试:8 个汉字,每个占 3 个字节,共 24 个字节。
  • _:1 个下划线,占 1 个字节。
  • 优惠详情:4 个汉字,每个占 3 个字节,共 12 个字节。
  • ::1 个冒号,占 1 个字节。
  • 7418945413963368458:19 个数字,每个占 1 个字节,共 19 个字节。

总字节数为:

5 + 6 + 2 + 6 + 24 + 1 + 12 + 1 + 19 = 76 个字节

· 30 min read

移动端组件库

借鉴ant-design-mobile使用gulp开发移动端组件库,使用dumi生成组件库文档,使用uView的色彩,并参考其组件样式

info

本地调试ant-design-mobile:

  • node版本切到16.16.0
  • nnrm 切到 taobao
  • 执行 pnpm i
  • 更改gulpfile.js并执行pnpm build,查看每一步task的产出 gulp

ant-design-mobile中的patch CSS 是针对浏览器不支持 CSS 变量的替代方案,比如button.patch.less是将button.less中的CSS 变量转换为不使用CSS 变量。

复用ant-design-mobilegulpfile.js

执行pnpm build报错:

  • Cannot find module 'node:path' 查看node版本发现是14.14.0,使用nvm use 16.16.0后解决

  • Cannot find module @rollup/rollup-darwin-arm64. 解决:删掉pnpm-lock.yaml重新执行pnpm install

  • Cannot find module './Foo'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option? 解决:tsconfig.json中设置"moduleResolution": "node"

  • SyntaxError in plugin "gulp-babel" index.d.ts: Unexpected token, expected "," 解决:tsconfig.json中设置"declaration": false

  • Error in plugin "webpack-stream" Module not found: Error: Can't resolve 'babel-loader' 解决:pnpm add -D babel-loader

  • Error in plugin "webpack-stream" Module not found: Error: Cannot find package '@babel/preset-env' 解决:pnpm add -D @babel/preset-env pnpm add -D @babel/core

  • Error in plugin "webpack-stream" Module not found: Error: Cannot find package '@babel/preset-typescript' 解决:pnpm add -D @babel/preset-typescript

  • Error in plugin "webpack-stream" Module not found: Error: Cannot find package '@babel/preset-react' 解决:pnpm add -D @babel/preset-react

使用的依赖包

删除文件

through2

每天一个npm包:through2

对Node.js Streams.Transform (Streams2/3) 的封装,提供了更为易用的objectMode模式。

@babel/plugin-transform-modules-commonjs

该插件将 ECMAScript 模块转换为 CommonJS。请注意,只有导入/导出语句 (如import "./mod.js") 和导入表达式 (如import('./mod.js')) 的语法被转换

ES Module

export default 42;

转为CommonJS

Object.defineProperty(exports, "__esModule", {
value: true,
});

exports.default = 42;

postcss-px-multiple

一个 postcss 的插件,可以把 css 文件中含 px 的样式乘以倍数,注意大写的 PX 不会转换。

这个插件对设计稿定义 pt 单位,实际 1pt = 2px 情况下很有用。另外当 viewport 设置成固定值且不为 device-width 时,比如 width=750,当引入第三方组件中的 css 时候,第三方组件一般都是按 device-width 写的尺寸,此时用此插件很好解决问题。

webpack-stream

将 webpack 作为流运行以方便地与 gulp 集成。

classnames

用于有条件地将类名连接在一起。

const classNames = require('classnames');
classNames('foo', 'bar'); // => 'foo bar'

classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

classNames(null, false, 'bar', undefined, 0, { baz: null }, ''); // => 'bar'

const arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'

lodash

assignWith(object, sources, [customizer])
const customizer = function(objValue, srcValue, key, object, source) {}

@react-spring/web

React Spring 是一个用于构建交互式、数据驱动和动画 UI 组件的库。它可以为 HTML、SVG、Native Elements、Three.js 等制作动画。

# Install the entire library
npm install react-spring
# or just install your specific target (recommended)
npm install @react-spring/web
yarn add @react-spring/web
pnpm add @react-spring/web

use-sync-external-store

React.useSyncExternalStore 的向后兼容垫片。

pnpm add use-sync-external-store
pnpm add -D @types/use-sync-external-store

使用的gulp插件

gulp-typescript

  • 用于处理 TypeScript 编译工作流程的 gulp 插件。该插件暴露TypeScript的编译器options供gulp使用

  • { declaration: true }设置编译时是否为每个脚本生成类型声明文件.d.ts

  • { emitDeclarationOnly: true }设置编译后只生成.d.ts文件,不生成.js文件

gulp-less

gulp-postcss

gulp-replace

gulp-rename

规范提交及代码格式化

husky

自动检查您的提交消息、代码,并在提交或推送时运行测试。

husky 整个安装主要有以下几步:

  1. 安装 husky 依赖: npm install -D husky

  2. 安装 husky 目录:npx husky install

    npx husky install 命令,是为了在项目中创建一个 git hook 目录,同时将本地 git 的 hook 目录指向项目内的 husky 目录。

  3. 添加 npm prepare 钩子:npm pkg set scripts.prepare="husky install"

    info

    npm 中也有一些生命周期钩子,prepare 就是其中一个,以下是对他的运行时机介绍:

    • npm publishnpm pack 期间运行
    • 在本地 npm install 时运行
    • 在prepublish和prepublishOnly期间运行
  4. 添加 git pre-commit 钩子:npx husky add .husky/pre-commit "npm run test"

    npx husky add 命令用于添加 git hook 脚本, 这个命令中自动添加了文件头及文件可执行权限。

依次执行完这四步,我们就完成了 husky 的安装以及 一个 pre-commit 钩子的创建。总的来说,当执行 npx husky install 时,会通过一个 git 命令,将 git hook 的目录指向 husky 的目录,由于 git 仓库的设置不会同步到远程仓库,所以 husky 巧妙地通过添加 npm 钩子以保证新拉取的仓库在执行 npm install 后会自动将 git hook 目录指向 husky 的目录。

使用husky 如图在运行 ESLint 时遇到了错误消息 "ESLint couldn't find the config 'prettier' to extend from",这意味着 ESLint 无法找到名为 'prettier' 的配置文件来扩展你的 ESLint 配置。这通常是因为你在 ESLint 配置文件中指定了继承自 'prettier' 的配置,但是没有安装相应的 eslint-config-prettier 包。npm install --save-dev eslint-config-prettier 安装完成后,确保你的 ESLint 配置文件(通常是 .eslintrc.js.eslintrc.json)正确地引用了 'prettier' 配置。例如:

{
"extends": [
"prettier"
]
}

lint-staged

针对暂存的 git 文件运行 linter,不要让它溜进您的代码库!设置pre-commit git hook 来运行 lint-staged

  • 安装:pnpm add -D lint-staged

  • 配置: 可以在package.json中使用lint-staged字段定义配置;或者在项目根目录下新建lint-staged.config.js.lintstagedrc.js进行配置

    package.json
    "lint-staged": {
    "*.{md,json}": [
    "prettier --write --no-error-on-unmatched-pattern" // prettier格式化md json类型的文件,当没有匹配到md json类型的文件prettier时不报错
    ],
    "*.{css,less}": [ // 以下多条命令会同时被执行
    "stylelint --fix",
    "prettier --write" // prettier格式化css less类型的文件
    ],
    "*.{js,jsx}": [
    "eslint --fix",
    "prettier --write"
    ],
    "*.{ts,tsx}": [
    "eslint --fix",
    "prettier --parser=typescript --write"
    ]
    },
  • 结合husky使用,生成的.husky/pre-commit文件中:pnpm lint-staged

commitlint

commitlint 帮助您的团队遵守提交约定。

  • 安装:pnpm add -D @commitlint/cli @commitlint/config-conventional

  • 配置

    • 在项目根目录新建commitlint.config.js.commitlintrc.js.commitlintrc.commitlintrc.json.commitlintrc.yml

      commitlint.config.js
      module.exports = {
      extends: ['@commitlint/config-conventional'],
      rules: {
      'header-max-length': [1, 'always', 100], // 提交信息超过100字符则警告
      'type-enum': [ // 提交信息非以下类型则报错
      2,
      'always',
      [
      'feat',
      'fix',
      'enhance',
      'chore',
      'test',
      'docs',
      'refactor',
      'style',
      'revert',
      ],
      ],
      },
      };
    • 或者在package.json文件中使用commitlint字段来定义配置

      package.json
      "commitlint": {
      "extends": [
      "@commitlint/config-conventional"
      ]
      },
  • 通过 git hooks 在commit message时立即检查

    • 安装husky: npm install husky --save-dev
    • 激活hooks:npx husky install
    • 添加hook: npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'

使用commitlint

prettier

  • 安装:pnpm add -D prettier

  • 配置:在项目根目录下新建.prettierrc.js .prettierignore

配置选项:
.prettierrc.js
module.exports = {
// 1.一行代码的最大字符数,默认是80(printWidth: <int>)
printWidth: 80,
// 2.tab宽度为2空格(tabWidth: <int>)
tabWidth: 2,
// 3.是否使用tab来缩进,我们使用空格(useTabs: <bool>)
useTabs: false,
// 4.结尾是否添加分号,false的情况下只会在一些导致ASI错误的其工况下在开头加分号,我选择无分号结尾的风格(semi: <bool>)
semi: false,
// 5.使用单引号(singleQuote: <bool>)
singleQuote: true,
// 6.object对象中key值是否加引号(quoteProps: "<as-needed|consistent|preserve>")as-needed只有在需求要的情况下加引号,consistent是有一个需要引号就统一加,preserve是保留用户输入的引号
quoteProps: 'as-needed',
// 7.在jsx文件中的引号需要单独设置(jsxSingleQuote: <bool>)
jsxSingleQuote: false,
// 8.尾部逗号设置,es5是尾部逗号兼容es5,none就是没有尾部逗号,all是指所有可能的情况,需要node8和es2017以上的环境。(trailingComma: "<es5|none|all>")
trailingComma: 'es5',
// 9.object对象里面的key和value值和括号间的空格(bracketSpacing: <bool>)
bracketSpacing: true,
// 10.jsx标签多行属性写法时,尖括号是否另起一行(jsxBracketSameLine: <bool>)
jsxBracketSameLine: false,
// 11.箭头函数单个参数的情况是否省略括号,默认always是总是带括号(arrowParens: "<always|avoid>")
arrowParens: 'always',
// 12.range是format执行的范围,可以选执行一个文件的一部分,默认的设置是整个文件(rangeStart: <int> rangeEnd: <int>)
rangeStart: 0,
rangeEnd: Infinity,
// 18. vue script和style标签中是否缩进,开启可能会破坏编辑器的代码折叠
vueIndentScriptAndStyle: false,
// 19. endOfLine: "<lf|crlf|cr|auto>" 行尾换行符,默认是lf,
endOfLine: 'lf',
// 20.embeddedLanguageFormatting: "off",默认是auto,控制被引号包裹的代码是否进行格式化
embeddedLanguageFormatting: 'off',
}

// 14. requirePragma: <bool>,格式化有特定开头编译指示的文件 比如下面两种
/**
* @prettier
*/
// or
/**
* @format
*/

// 15.insertPragma: <bool> 自当插入pragma到已经完成的format的文件开头

// 16. proseWrap: "<always|never|preserve>" 文章换行,默认情况下会对你的markdown文件换行进行format会控制在printwidth以内

// 13. 指定parser,因为pretter会自动选择,所以一般不用指定(parser: "<string>" parser: require("./my-parser"))
// "babel" (via @babel/parser) Named "babylon" until v1.16.0
// "babel-flow" (same as "babel" but enables Flow parsing explicitly to avoid ambiguity) First available in v1.16.0
// "babel-ts" (similar to "typescript" but uses Babel and its TypeScript plugin) First available in v2.0.0
// "flow" (via flow-parser)
// "typescript" (via @typescript-eslint/typescript-estree) First available in v1.4.0
// "espree" (via espree) First available in v2.2.0
// "meriyah" (via meriyah) First available in v2.2.0
// "css" (via postcss-scss and postcss-less, autodetects which to use) First available in v1.7.1
// "scss" (same parsers as "css", prefers postcss-scss) First available in v1.7.1
// "less" (same parsers as "css", prefers postcss-less) First available in v1.7.1
// "json" (via @babel/parser parseExpression) First available in v1.5.0
// "json5" (same parser as "json", but outputs as json5) First available in v1.13.0
// "json-stringify" (same parser as "json", but outputs like JSON.stringify) First available in v1.13.0
// "graphql" (via graphql/language) First available in v1.5.0
// "markdown" (via remark-parse) First available in v1.8.0
// "mdx" (via remark-parse and @mdx-js/mdx) First available in v1.15.0
// "html" (via angular-html-parser) First available in 1.15.0
// "vue" (same parser as "html", but also formats vue-specific syntax) First available in 1.10.0
// "angular" (same parser as "html", but also formats angular-specific syntax via angular-estree-parser) First available in 1.15.0
// "lwc" (same parser as "html", but also formats LWC-specific syntax for unquoted template attributes) First available in 1.17.0
// "yaml" (via yaml and yaml-unist-parser) First available in 1.14.0

// 17. htmlWhitespaceSensitivity: "<css|strict|ignore>" html中的空格敏感性

// 针对不同文件或目录设置不同配置的方法,json格式例子
// {
// "semi": false,
// "overrides": [
// {
// "files": "*.test.js",
// "options": {
// "semi": true
// }
// },
// {
// "files": ["*.html", "legacy/**/*.js"],
// "options": {
// "tabWidth": 4
// }
// }
// ]
// }
插件

pnpm add -D prettier-plugin-organize-imports prettier-plugin-packagejson

  • prettier-plugin-organize-imports 按引用深度排序合并 import 声明;移除未使用的 import 声明。使用Prettier2版本 的话,Prettier将自动加载该插件。无需配置。

  • prettier-plugin-packagejson 合理排序 package.json 的 key 顺序

Prettier 3 使用插件:

{
"plugins": ["prettier-plugin-organize-imports"]
}
pretty-quick

pretty-quick 是一个 NPM 包,它是 Prettier 的一个封装,用于在 Git 提交时自动格式化你的代码。它可以作为一个 pre-commit 钩子与 Husky 一起使用,以确保在代码提交到仓库之前,它们已经被格式化。这有助于保持代码库的一致性和可读性。

pretty-quick --staged 是一个命令,用于运行 pretty-quick 工具,它会检查并格式化所有已经暂存(staged)的文件。这通常在一个 Git pre-commit 钩子中使用,以确保只有那些即将被提交的文件被格式化。这样可以避免对未暂存或未跟踪的文件进行不必要的格式化操作。

stylelint

Stylelint是一个强大、先进的 CSS 代码检查器(linter),可以帮助你规避 CSS 代码中的错误并保持一致的编码风格。

  • 安装 Stylelint 及其 标准配置: npm install --save-dev stylelint stylelint-config-standard

  • 在项目的根目录中创建 .stylelintrc.json 配置文件,并写入以下内容:

    {
    "extends": "stylelint-config-standard"
    }
  • 让 Stylelint 处理项目中的所有 CSS 文件:npx stylelint "**/*.css"

可以在根目录下创建.stylelintignore文件来配置允许Stylelint忽略的文件,比如:

test/**/*.less

当被检查的文件全都在.stylelintignore中配置的规则下时,执行stylelint会报错:Error: All input files were ignored because of the ignore pattern. Either change your input, ignore pattern or use "--allow-empty-input" to allow no inputs,可以使用stylelint --allow-empty-input

eslint

ESLint 是一个静态代码分析工具,用于识别和报告 ECMAScript/JavaScript 代码中的模式和错误。其目标是使代码更加一致并避免错误。

在项目中手动设置ESLint
  • 安装:npm install --save-dev eslint

  • 在项目根目录新建.eslintrc.js,增加配置,例如:

    .eslintrc.js
    module.exports = {
    "env": {
    "browser": true,
    "es2021": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
    },
    }
  • 使用 ESLint CLI 的 Lint 代码:npx eslint project-dir/ file1.js

使用命令设置ESLint

可以使用以下命令安装和配置 ESLint:npm init @eslint/config

配置项

eslint 配置指南

.eslintrc.js
module.exports = {
/*
对于项目中的某些文件,我们想跳过 eslint 对它们的检查,可以配置 ignorePatterns 字段。告诉 eslint 忽略某些文件。
*/
ignorePatterns: ['/*', '!/src', '/src/**/*.js'],
/*
env 字段用于配置项目的工作环境可用的全局 API,例如,某个项目是基于 Nodejs 开发的,那自然没有 BOM 和 DOM 两个对象的全局 API。
*/
'env': {
'browser': true,
'es6': true,
},
/*
extends 字段用来继承来自其他的规则、插件和语言选项的共享配置。例如 eslint 内置通用的核心规则插件 eslint:recommended,这样我们就不手动一个个去定义规则了。
*/
'extends': [
'eslint:recommended',
/*
对应的依赖包为eslint-plugin-react
*/
'plugin:react/recommended',
/*
对应的依赖包为eslint-plugin-react-hooks
*/
"plugin:react-hooks/recommended",
/*
对应的依赖包为@typescript-eslint/eslint-plugin
*/
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
/*
对应的依赖包为eslint-config-prettier,告诉 ESLint 使用 eslint-config-prettier 包提供的规则来覆盖可能与 Prettier 冲突的 ESLint 规则。
*/
'prettier',
],
/*
eslint 最核心的功能就是负责将文件内的 javascript 代码转换成 AST(抽象语法树)去解析代码的格式、代码语法等。那么做这个功能的就是 eslint 提供的 parser 接口,eslint 内置的 parser 是 Espree。我们还可以使用其他的 parser,它只需要能够符合 eslint 的parser 接口,比如@typescript-eslint/parser
*/
'parser': '@typescript-eslint/parser',
/*
parser的配置项
*/
'parserOptions': {
'project': 'tsconfig.json',
'sourceType': 'module',
},
/*
一个 plugin 定义了一组规则、环境和配置。
*/
'plugins': ['@typescript-eslint'],
/*
eslint 预设了很多的规则来保证代码的可靠性。规则的值可以是
- off 或者 0, 表示关闭规则
- warn 或者 1, 表示开启规则,当代码未能通过该规则时,代码的下划线展示黄色的波浪线
- error 或者 2,表示开启规则,当代码未能通过该规则时,代码的下划线部分展示红色的波浪线
*/
'rules': {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/camelcase': 'off',
'react/prop-types': 'off',
'react/jsx-no-target-blank': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'no-case-declarations': 'off',
'no-prototype-builtins': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-inner-declarations': 'off',
'react/no-unescaped-entities': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
ignoreRestSiblings: true,
},
],
'no-empty': [
'error',
{
allowEmptyCatch: true,
},
],
'no-constant-condition': [
'error',
{
checkLoops: false,
},
],
'@typescript-eslint/ban-types': [
'error',
{
extendDefaults: true,
types: {
'{}': false,
},
},
],
// 下面的是临时规则
'react/display-name': 'off',
},
/*
使用 settings 来指定应该在所有规则中共享的信息。
*/
settings: {
react: {
version: 'detect',
},
},
/*
overides 字段用于对目录下的某些特定的文件配置相关的 rule 或者 parser。
*/
overrides: [
{
'files': ['**/demos/**/*'],
'rules': {
'react/react-in-jsx-scope': 'off',
'react/display-name': 'off',
},
},
{
'files': ['**/tests/**/*'],
'rules': {
'react/react-in-jsx-scope': 'off',
'react/display-name': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
],
}

导出约定

  1. 每个组件都有1个单独的文件夹,该文件夹下的index.ts用于默认导出,其他文件都使用命名导出
  2. 组件库src/index.ts使用重新导出语法

相比ant-design-mobile的改动点

  • cloneElement不建议再使用了。替代方案:使用 render prop 传递数据。把ant-design-mobile的src/utils/native-props.ts改写为src/utils/render-native-props.ts
  • use-sync-external-storeReact.useSyncExternalStore 的向后兼容垫片。useSyncExternalStore 是React 18新增的一个Hook。想将import {useSyncExternalStore} from 'use-sync-external-store/shim' 改为 import {useSyncExternalStore} from 'react',考虑到使用组件库的项目依赖的React版本可能低于18,所以不改动。

ant-design-mobile的疑问点

dot-loading的实现

<svg height='1em' viewBox='0 0 100 40'>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g transform='translate(-100.000000, -71.000000)'>
<g transform='translate(95.000000, 71.000000)'>
<g transform='translate(5.000000, 0.000000)'>
<rect width='8' height='8' x='20' y='16' />
</g>
</g>
</g>
</g>
</svg>
  • 为啥使用 <g> 元素进行多次平移?不使用g transform也能实现同样的效果

  • <svg height='1em' viewBox='0 0 100 40'> 要想形状大小自动适配字号,则需要svg的内部元素可以缩放,就需要设置viewBox属性;形状大小自动适配字号,则指定svg的高度与父元素文本字体大小保持一致(即设置svg height='1em'),根据设置viewBox的宽高,就可以自动计算出svg的宽度(svg的宽=指定的高*(viewBox宽/viewBox高) 16px*(100/40)=40px),以及其内部元素的缩放比例(<rect>的宽高为指定值*(指定svg的高/viewBox高) 8*(16px/40)=3.2px 缩小了2.5倍)。为啥指定viewBox='0 0 100 40'

  • 为啥使用 <g> 元素 统一设置 fill='none' fillRule='evenodd'

spin-loading的实现

const motionReduced = useMotionReduced()
const { percent } = useSpring({
cancel: motionReduced,
loop: {
reverse: true,
},
from: {
percent: 80,
},
to: {
percent: 30,
},
config: {
duration: 1200,
},
})
  • cancel: motionReduced的意义是啥?看commit message是'add useMotionReduced to prevent infinite animation calculation in react-spring' 防止react-spring中的无限动画计算

src/utils/reduce-and-restore-motion.ts

import { Globals } from '@react-spring/web'
import { useSyncExternalStore } from 'use-sync-external-store/shim'

let reduced = false

const subscribers = new Set<() => void>()

function notify() {
subscribers.forEach(subscriber => {
subscriber()
})
}

export function reduceMotion() {
reduced = true
notify()
Globals.assign({
skipAnimation: true,
})
}

export function restoreMotion() {
reduced = false
notify()
Globals.assign({
skipAnimation: false,
})
}

export function isMotionReduced() {
return reduced
}

function subscribe(onStoreChange: () => void) {
subscribers.add(onStoreChange)
return () => {
subscribers.delete(onStoreChange)
}
}

export function useMotionReduced() {
return useSyncExternalStore(subscribe, isMotionReduced, isMotionReduced)
}
  • Globals.assign 不影响 reduceMotionrestoreMotion的使用(查看qs-ui-mobile的减弱动效中的例子)

  • 不调用notify()则不生效

  • Globals.assign 可以单独使用,即不与useSyncExternalStore结合使用,但是会造成页面卡死

  • Globals.assignuseSyncExternalStore结合使用,reduceMotionrestoreMotion中如果只想使用Globals.assign,而不调用notify(),则会造成页面卡死

  • useMotionReduced 没被使用的话(指的是useMotionReduced的返回值没用于useSpring),直接通过reduceMotionrestoreMotion来使用Globals.assign,也会造成页面卡死,可能这就是spin-loadingcancel: motionReduced的意义

  • 通过reduceMotionrestoreMotion来使用Globals.assign,如果自定义组件也使用了react-spring,则调用reduceMotion或者restoreMotion后能控制所有动画(包括自定义组件的动画),但页面会卡死;如果自定义组件使用了useMotionReduced来控制动画的动与静(控制其他行为如loop则页面仍会卡死),则调用reduceMotion或者restoreMotion后能控制所有动画(包括自定义组件的动画),并且页面不会卡死

组件库文档工具

对比三个强大的组件文档展示工具

主题目录结构: dum1-theme

dumi2.x

使用 React Library 模板

# 先找个地方建个空目录。
mkdir myapp && cd myapp

# 通过官方工具创建项目,选择你需要的模板
npx create-dumi

# 选择一个模板
? Pick template type › - Use arrow-keys. Return to submit.
❯ Static Site # 用于构建网站
React Library # 用于构建组件库,有组件例子
Theme Package # 主题包开发脚手架,用于开发主题包

# 安装依赖后启动项目
npm start

生成的工程中的.dumi目录不用push到远程,每次执行pnpm start都会生成这个目录

主题目录结构

当 dumi 提供的默认主题无法满足项目需要时,即可选择对 dumi 的默认主题进行局部定制或全部定制。无论是单独发布的主题包(dumi-theme-[name]/src/)还是项目本地主题包(.dumi/theme/),都应符合如下目录结构:

.
├── builtins # 全局组件,注册的组件可直接在 Markdown 中使用
│ ├── Hello # {Component}/index.tsx 会被识别,可在 md 里使用 <Hello></Hello>
│ │ └── index.tsx
│ └── World.tsx # {Component}.tsx 会被识别,可在 md 里使用 <World></World>
├── locales # 国际化文案,通过 `import { useIntl, FormattedMessage } from 'dumi'` 来调用文案,自动根据当前的 locale 切换
│ └── zh-CN.json
├── layouts # 布局组件,会被 dumi 直接引用
│ ├── GlobalLayout # 全局 Layout,通常用来放置 ConfigProvider
│ ├── DocLayout # 文档 Layout,包含导航、侧边菜单、TOC 等,包裹 Markdown 正文做渲染
│ └── DemoLayout # 组件示例 Layout,需要控制 demo 独立访问页(`/~demos/:id`)的布局时使用
├── slots # 局部组件(具体有哪些组件取决于主题包实现,应由布局组件引用,以下仅为举例示意)
│ ├── Navbar # 导航栏
│ ├── NavbarLogo # 导航栏 LOGO 区域
│ ├── SideMenu # 侧边栏
│ ├── Homepage # 首页内容
│ └── HomepageHero # 首页 Hero 区域
└── plugin # dumi 插件文件夹,plugin/index.ts(也可以是 plugin.ts)会被自动注册为插件
└── index.ts

移动端组件研发

只需要安装移动端组件研发主题即可: pnpm add -D dumi-theme-mobile@^2.0.0

配置项

配置文件为根目录下的.dumirc.ts

  • resolve.atomDirs 用于配置资产(例如组件、函数、工具等)路由。默认值为[{ type: 'component', dir: 'src' }]。在默认配置下,src/Foo/index.md 将被解析为 components/foo 的路由。如下,将src/components/button/index.md解析为components/button

    .dumirc.ts
    resolve: {
    atomDirs: [
    {
    type: 'component',
    dir: 'src/components',
    }
    ],
    },
    warning

    配置resolve.atomDirs后发现,在src/components/*/demos中的文件名不能重复,否则会覆盖,比如src/components/loading/index.md中引用dot-loading和spin-loading的demo:

    ## DotLoading 点状加载中

    <code src="../dot-loading/demos/demo1.tsx"></code>

    ## SpinLoading 转圈加载中

    <code src="../spin-loading/demos/demo1.tsx"></code>

    虽然是不同的目录,但是都是demo1,则下面的会覆盖上面的

  • themeConfig.logo 配置导航栏上的站点 LOGO,如果需要配置为本地图片文件,可将图片资源放入 public 文件夹,例如放置 public/logo.png,则配置 '/logo.png' 即可。配置为 false 时不展示 LOGO。

  • themeConfig.nav link为路由,比如,基于以上resolve.atomDirs配置,要link到src/components/button/index.md则设置link'/components/button'

    .dumirc.ts
    themeConfig: {
    nav: [
    { title: '指南', link: '/guide/theming' },
    { title: '组件', link: '/components/button' },
    ],
    },
  • themeConfig.name 配置导航栏上的站点名称,不配置时不展示。

  • themeConfig.footer 配置页脚内容,可以是 HTML,配置 false 时不展示。

  • alias 配置别名,对 import 语句的 source 做映射。

    .dumirc.ts
    alias: {
    'demos': '/src/components/demos/index.ts'
    },
  • favicons 配置站点favicon

    .dumirc.ts
    favicons: ['/logo.png'], // 确保你的项目含有 `public/logo.png`

首页渲染

站点首页为docs/index.md,该markdown文档中的一级目录用于设置首页的页面title

---
hero:
title: QS UI Mobile
description: A react UI library for building mobile web apps
actions:
- text: 开始使用
link: /guide/theming
- text: GitHub
link: https://github.com/fqishuai/qs-ui-mobile
features:
- title: 体验
emoji: 💎
description: 参考ant-design-mobile和uView中组件的外观及交互,助力打造极致体验的产品
- title: 外观
emoji: 🌈
description: 基于 CSS 变量,便于高效地调整组件的外观或创建自己的主题
- title: 性能
emoji: 🚀
description: 无需配置,即可拥有极致性能
---

# QS UI Mobile <!-- 用于设置首页的页面title -->

常见问题

  • dumi 和 Umi 的关系是什么?

    Umi 是前端开发框架,适用于前端应用研发;dumi 是在 Umi 的基础上打造的静态站点框架,适用于组件研发。

  • 如何完全自定义首页?

    创建 .dumi/pages/index.tsx 即可用 React 来编写首页,注意不要同时在文档解析的根目录中创建 index.md,会导致路由冲突。

  • 为什么不支持 CSS Modules?

    主要两个原因:

    • 使用者很难覆写样式,因为最终 className 不稳定
    • 自动 CSS Modules 依赖 babel 编译产物,给使用项目带来额外的编译成本,而大部分框架默认都不编译 node_modules(比如 Umi 框架就需要配置 extraBabelIncludes 才会编译 node_modules 下的产物)

    也许大部分人选择在组件库项目中使用CSS Modules,是因为做前端应用研发时的习惯性选型,但它其实不适合组件库项目;另外,原因 2 也会产生额外的调试成本,比如『为什么 dev 生效、发布后在项目里不生效?』造成的调试成本

dumi1.x

目录结构:

  • /config dumi配置文件,用于配置菜单、导航栏、主题、插件、别名等
  • .dumi 用于存放自定义的dumi插件及主题

发布npm包

antd-tools

使用@ant-design/tools比对npm包

npm install -D @ant-design/tools
# 全局
npm install -g @ant-design/tools

minimist

· 2 min read

给Nginx配置一个自签名的SSL证书

gencert.sh
#!/bin/sh

# create self-signed server certificate:

read -p "Enter your domain [www.example.com]: " DOMAIN

echo "Create server key..."

openssl genrsa -des3 -out $DOMAIN.key 1024

echo "Create server certificate signing request..."

SUBJECT="/C=US/ST=Mars/L=iTranswarp/O=iTranswarp/OU=iTranswarp/CN=$DOMAIN"

openssl req -new -subj $SUBJECT -key $DOMAIN.key -out $DOMAIN.csr

echo "Remove password..."

mv $DOMAIN.key $DOMAIN.origin.key
openssl rsa -in $DOMAIN.origin.key -out $DOMAIN.key

echo "Sign SSL certificate..."

openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt

echo "TODO:"
echo "Copy $DOMAIN.crt to /etc/nginx/ssl/$DOMAIN.crt"
echo "Copy $DOMAIN.key to /etc/nginx/ssl/$DOMAIN.key"
echo "Add configuration in nginx:"
echo "server {"
echo " ..."
echo " listen 443 ssl;"
echo " ssl_certificate /etc/nginx/ssl/$DOMAIN.crt;"
echo " ssl_certificate_key /etc/nginx/ssl/$DOMAIN.key;"
echo "}"

执行:

sudo sh ./gencert.sh

会生成4个文件,Web服务器需要把xxx.crt发给浏览器验证,然后用xxx.key解密浏览器发送的数据,剩下两个文件不需要上传到Web服务器上。

express启动https服务:

const fs = require('fs')
const https = require('https')
const express = require('express')

const app = express()

const options = {
key: fs.readFileSync('./xxx.key'),
cert: fs.readFileSync('./xxx.crt')
};
https.createServer(options, app).listen(443)

· 5 min read

看电视剧《老酒馆》能学到不少做人做事的道理:

陈大哥一行人刚到好汉街就被摆了一道,大哥第一时间看出了其中的猫腻,一方面,暗中让雷子亮子跟好这个别人手中的“棋子”,将其变为自己手中的“底料”,另一方面,按着别人说的“剧本”往下演,这样才能探得其中的深浅,引出幕后的手,后来幕后的手出来了,是官爷,他仍然陪着官爷过了两招,也没有一上来就亮底牌,这就是做人做事的智慧了,首先他就一小老百姓,当官的他惹不起,总得让官爷得些好处,但是当官爷化身无底洞想把大哥掏空这时候就不能忍了,所谓人得“站着”,请记者,开水浇人等各种硬手段,有谋略有手段,事儿想得透彻,办得明明白白。包括后面和金小手过招,也都是有谋略有手段,分得清啥时候该忍啥时候不该忍。大哥做事很稳,被下套子了,波澜不惊,酒馆生意起来了,也没有飘飘然。

· 6 min read

自动化测试意味着测试是独立于代码的。它们以各种方式运行我们的函数,并将结果与预期结果进行比较。

行为驱动开发(BDD)包含了三部分内容:测试、文档和示例。在 BDD 中,规范先行,实现在后。

测试开发流程

  1. 编写初始规范,测试最基本的功能。
  2. 创建一个最初始的实现。
  3. 检查它是否工作,我们运行测试框架 Mocha 来运行测试。当功能未完成时,将显示错误。我们持续修正直到一切都能工作。
  4. 现在我们有一个带有测试的能工作的初步实现。
  5. 我们增加更多的用例到规范中,或许目前的程序实现还不支持。无法通过测试。
  6. 回到第 3 步,更新程序直到测试不会抛出错误。
  7. 重复第 3 步到第 6 步,直到功能完善。

测试库

以下这些库都既适用于浏览器端,也适用于服务器端。

  • Mocha: 提供了包括通用型测试函数 describeit,以及用于运行测试的主函数。
  • Chai: 提供很多断言(assertion)支持的库。它提供了很多不同的断言。
  • Sinon: 用于监视函数、模拟内建函数和其他函数的库。

Mocha

describe("pow", function() {

it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
});

});
  • 一个规范(specification, spec)包含三个主要的模块:

    • describe("title", function() { ... }) 表示我们正在描述的功能是什么。
    • it("use case description", function() { ... }) 以一种 易于理解 的方式描述特定的用例,第二个参数是用于对其进行测试的函数。保持测试之间独立,一个 it 只测试一个功能。
    • assert.equal(value1, value2) 如果实现是正确的,它应该在执行的时候不产生任何错误。当 assert 触发一个错误时,it 代码块会立即终止。
  • 规范有三种使用方式:

    • 作为 测试 —— 保证代码正确工作。
    • 作为 文档 —— describeit 的标题告诉我们函数做了什么。
    • 作为 案例 —— 测试实际工作的例子展示了一个函数可以被怎样使用。
  • 除了手动地编写 it 代码块,我们可以使用 for 循环来生成它们:

    describe("pow", function() {

    function makeTest(x) {
    let expected = x * x * x;
    it(`${x} in the power 3 is ${expected}`, function() {
    assert.equal(pow(x, 3), expected);
    });
    }

    for (let x = 1; x <= 5; x++) {
    makeTest(x);
    }

    });
  • 可以使用嵌套的 describe 来进行分组:

    describe("pow", function() {

    describe("raises x to power 3", function() {

    function makeTest(x) {
    let expected = x * x * x;
    it(`${x} in the power 3 is ${expected}`, function() {
    assert.equal(pow(x, 3), expected);
    });
    }

    for (let x = 1; x <= 5; x++) {
    makeTest(x);
    }

    });

    // ……可以在这里写更多的测试代码,describe 和 it 都可以添加在这。
    });
  • 可以设置 before/after 函数来在运行测试之前/之后执行。也可以使用 beforeEach/afterEach 函数来设置在执行 每一个 it 之前/之后执行。

    describe("test", function() {

    before(() => alert("Testing started – before all tests"));
    after(() => alert("Testing finished – after all tests"));

    beforeEach(() => alert("Before a test – enter a test"));
    afterEach(() => alert("After a test – exit a test"));

    it('test 1', () => alert(1));
    it('test 2', () => alert(2));

    });
    /*
    运行顺序为:
    Testing started – before all tests (before)
    Before a test – enter a test (beforeEach)
    1
    After a test – exit a test (afterEach)
    Before a test – enter a test (beforeEach)
    2
    After a test – exit a test (afterEach)
    Testing finished – after all tests (after)
    */

Codecov

Codecov是一个代码覆盖率服务,它提供了一种测量软件测试覆盖率的方法,并帮助开发者了解哪些代码行被测试覆盖到,哪些没有。它可以与GitHub、Bitbucket和GitLab等代码托管平台集成,并支持多种编程语言和测试框架。

开发者可以在本地运行测试套件并生成覆盖率报告,然后将这些报告上传到Codecov。Codecov会分析这些报告,提供一个可视化的界面来展示代码覆盖率,并能够跟踪覆盖率随时间的变化。它还可以在代码审查过程中提供覆盖率信息,帮助开发者确保新提交的代码得到了适当的测试。

· 4 min read

实现撤回: 直接调用map.pm.Draw.Polygon._removeLastVertex();不生效

查看源码:Draw.Polygon继承自Draw.Line,查看Draw.Line_removeLastVertex()的实现

重新实现了撤回操作,但是辅助线的状态一直不更新:设置原始辅助线为opacity: 0,然后手动画辅助线

手画辅助线这种方式可以,但是手动触发drawend后,甚至disableDraw后,mouseover事件仍然被监听

map.removeLayer()不生效

 let customHintLine = L.polyline([], { color: 'red', dashArray: [5, 5], className: 'customHintLine' });
const customHintLineLayer = layers.find(layer => layer.options.className === 'customHintLine');

if (customHintLineLayer) {
console.log('draw start customHintLineLayer::', customHintLineLayer)
map.removeLayer(customHintLineLayer);
}
console.log('draw start remove layers', layers)
hintLineRef.current = customHintLine.addTo(map);

MapContainer中嵌入的自定义组件,点击穿透,使用e.stopPropagation();不好使

L.DomEvent.on(domRef.current, 'click', handler) 执行了2遍

如果 L.DomEvent.on(domRef.current, 'click', handler) 执行了两遍,可能是因为事件监听器被多次添加,或者组件在 React 中重新渲染时没有正确清理旧的事件监听器。以下是一些可能的原因和解决方法:

1. 确保事件监听器只添加一次

在 React 中,使用 useEffect 钩子来确保事件监听器只在组件挂载时添加,并在组件卸载时移除:

import React, { useEffect, useRef } from 'react';
import L from 'leaflet';

const MyComponent = () => {
const domRef = useRef(null);

useEffect(() => {
const handleClick = () => {
console.log('Element clicked!');
};

if (domRef.current) {
L.DomEvent.on(domRef.current, 'click', handleClick);
}

// 清理函数,确保在组件卸载时移除事件监听器
return () => {
if (domRef.current) {
L.DomEvent.off(domRef.current, 'click', handleClick);
}
};
}, []);

return <div ref={domRef}>Click me</div>;
};

export default MyComponent;

2. 检查组件是否多次渲染

确保组件没有因为某些原因多次渲染,导致事件监听器被多次添加。你可以在组件中添加一些日志来调试:

import React, { useEffect, useRef } from 'react';
import L from 'leaflet';

const MyComponent = () => {
const domRef = useRef(null);

useEffect(() => {
console.log('Component mounted or updated');

const handleClick = () => {
console.log('Element clicked!');
};

if (domRef.current) {
L.DomEvent.on(domRef.current, 'click', handleClick);
}

return () => {
if (domRef.current) {
L.DomEvent.off(domRef.current, 'click', handleClick);
}
};
}, []);

return <div ref={domRef}>Click me</div>;
};

export default MyComponent;

3. 确保事件处理函数的引用不变

在 React 中,如果事件处理函数的引用在每次渲染时都发生变化,可能会导致事件监听器被多次添加。确保事件处理函数的引用不变:

import React, { useEffect, useRef, useCallback } from 'react';
import L from 'leaflet';

const MyComponent = () => {
const domRef = useRef(null);

const handleClick = useCallback(() => {
console.log('Element clicked!');
}, []);

useEffect(() => {
if (domRef.current) {
L.DomEvent.on(domRef.current, 'click', handleClick);
}

return () => {
if (domRef.current) {
L.DomEvent.off(domRef.current, 'click', handleClick);
}
};
}, [handleClick]);

return <div ref={domRef}>Click me</div>;
};

export default MyComponent;

通过这些方法,你可以确保事件监听器只被添加一次,并在组件卸载时正确移除,避免重复执行的问题。

· 3 min read

vercel

info
  • 部署后默认域名后缀是.vercel.app
  • vercel.app域名被Greate Firewall拦截了

netlify

tip
  • 部署后默认域名后缀是.netlify.app
  • 使用netlify是真的爽,域名netlify.app前面的name可以自由设置
  • netlify和vercel操作非node前端项目都很简单:1.使用github登录 2.给netlify/vercel授权 3.deploy

Render

环境变量

  • 默认的环境变量

  • 使用环境变量:

    • 本地开发:在根目录下创建一个.env文件并将其加入到.gitignore避免提交。许多语言都有用于读取 .env 文件的库,例如 Node.js 的 dotenv 和 Python 的 python-dotenv
    • render部署:可以在Environment--->Environment Variables配置诸如Node.js版本、运行环境等环境变量
    • 在代码中使用:以配置的NODE_VERSION为例,process.env.NODE_VERSION
  • 使用Secret Files:

    • 本地开发:同上

    • render部署:可以在Environment--->Secret Files配置诸如数据库链接、私钥等,为了和本地开发保持一致,可以设置Filename为.env,Contents为key=value值,在构建期间和运行时从应用程序的指定根目录读取这些文件。 secret files

    • 在代码中使用:以配置的DATABASE_URL为例,process.env.DATABASE_URL

    info

    安装:npm install dotenv --save

    使用(在根目录下新建.env文件):

    .env
    HOST=localhost
    PORT=4000
    const Dotenv = require('dotenv');

    Dotenv.config();

    const server = Hapi.server({ host: process.env.HOST, port: process.env.PORT });

使用客户端路由

如果您使用 Reach Router 或 React Router 进行客户端路由,则需要将所有路由请求定向到 index.html,以便路由库可以处理它们。您可以通过为静态站点定义重写规则来轻松完成此操作。在您的服务的 Redirects/Rewrites 选项卡添加具有以下值的规则: Redirects/Rewrites

Surge

GitHub Pages

· 6 min read

面试准备

准备好简历、了解清楚应聘公司的岗位描述

简历

阿里P8级前端高级工程师来告诉你,前端简历到底该如何写,前端面试应该如何去准备!

免费简历生成网址:https://www.wondercv.com/

5年简历模板: 1 2 3 4

个人信息

  • 姓名
  • 电话
  • 邮箱
  • 求职意愿
  • 学历/blog/github/年龄 看情况
info

推荐写法:

  • 标题:姓名-职位-工龄
  • 手机号
  • 邮箱
  • 所在城市
  • 个人状态(在职与否)

专业技能

主要能够体现研发技术栈即可,如果不确定可以不加了解/熟悉/精通等词

info

参考模板:

  • HTML、CSS、JS、AJAX等基础知识;
  • 前端框架:React、Vue;
  • 多端开发:weex、React Native;
  • Node:Koa、express等常见框架;
  • 前端构建工具:webpack、Vite,基于业务定制脚手架;
  • 版本管理:Git;
  • 常见的设计模式、算法及网络协议;

工作经历

  • 每段经历的时间
  • 所担任的职责
  • 工作内容
info

推荐写法:

  • 一句话描述对应工作公司、工作时间、工作职级;
  • 一句话描述使用的完整技术栈;
  • 简要地描述这段经历带来的技术产出和业务价值,突出在这段经历中的成长和亮点;

举例: 工作经历模板

项目经历

tip
  • 避免流水账,可以用以下关键词串联:

    • 项目概述
    • (角色/负责的模块)个人职责
    • (碰到的问题)项目难点
    • 解决思路/解决方案
    • 工作成果
    • 总结沉淀
  • 避免堆积项目,2-3个质量高的项目即可

  • 项目描述

    • 1句话写清楚业务背景
    • 1-2句话写清楚技术选型和最终的技术方案
    • 重点写如何使用对应的技术手段完成业务目标,遇到的难点有哪些
    • 项目带来的业务价值
info

推荐写法:

  • 建议写3个左右的核心项目,每个项目最多不超过3个亮点;
  • 每个项目可以按照这个格式描述:
    • 一句话描述业务背景;
    • 1-2句话描述技术选型;
    • 核心内容讲解如何通过技术手段实现;
    • 一句话描述带来的研发和业务的增量价值;

团队横向产出的项目举例: 团队横向产出

难点/亮点的项目举例: 难点/亮点

负责业务的项目举例: 业务

  • 微前端
  • Node.js BFF
  • 脚手架(webpack插件)
  • vscode插件
  • 页面性能优化
  • 多端

梳理项目,项目中用到的技术点/知识点得掌握

教育经历

自我评价

可以不写

举例: 自我评价

面试题

5年以上,资深前端,要求:广度 深度 具有产品视野;研发效能 质量 资源使用率等方面有技术手段;技术调研 最优技术方案;方法论;业务理解深度;如何帮助团队成员成长;能够针对具体的业务场景,提供完善的技术方案;在负责的业务领域,掌握前沿技术

先准备好项目,项目涉及的知识点都熟练,然后再看跟项目相关的八股文(不用把所有的八股文都看完都准备好)

画图梳理项目涉及的功能点(怎么实现的),以及涉及的知识点

React面试题

面试过程

可能是3+1技术面+HR面

  • 1轮技术面:通常包含有coding
  • 2轮技术面:项目深度 业务知识 技术攻坚
  • 3轮技术面:前端认知
  • 4轮技术面:通常为交叉面,用于定职级

学会引导面试官

自我介绍

  • 个人信息
  • 技术能力
  • 技术擅长(性能优化、架构设计、带项目等)

项目介绍

  • 项目概述
  • (角色/负责的模块)个人职责
  • (碰到的问题)项目难点
  • 解决思路/解决方案
  • 工作成果
  • 总结沉淀

提问

  • 团队情况
  • 涉及业务

HR

  • 加班问题:可以回答接受加班但不加无意义的班
  • 离职原因
  • 谈薪:可以要预期偏上,HR会压价

面试记录