
暂无个人介绍
能力说明:
掌握企业中如何利用常见工具,进行前端开发软件的版本控制与项目构建和协同。开发方面,熟练掌握Vue.js、React、AngularJS和响应式框架Bootstrap,具备开发高级交互网页的能力,具备基于移动设备的Web前端开发,以及Node.js服务器端开发技能。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明后续也会更新TypeScript相关系列文章,欢迎持续关注> TypeScript 的类型断言看起来概念比较简单,但是对于刚接触 TypeScript 的使用者,可能对使用场景缺少认识,希望本文可以帮助你更了解类型断言。当你使用一个值,但是 TypeScript 不知道具体类型 或者 TypeScript 记录的类型没有办法满足使用要求时,可以使用类型断言来明确指定为自己想要使用的类型。#### 语法:类型断言有两种方式:1. 使用 `<>` 语法1. 使用 `as` 关键字`<>` 会和 JSX 语法冲突,一般使用 `as` 。我们来看几个类型断言的示例1.对于通过标签获取的DOM,TypeScript可以推断出类型,但是对于其他方式,TypeScript无法推断,我们可以使用类型断言来明确指定元素类型。```const aEle = document.querySelector('a') // HTMLAnchorElement | nullconst canvasEle = document.querySelector('#my_canvas') as HTMLCanvasElement```canvasEle变量也就有了HTMLCanvasElement类型,编辑器也会更好的进行代码提示和类型检查。```React.useEffect(() => { if (props.autoFocus) { const $this = ref.current as HTMLInputElement; ... }}, []);```AntD中的示例:[ActionButton.tsx](https://github.com/ant-design/ant-design/blob/4.3.4/components/modal/ActionButton.tsx)2.对于空对象占位,可以断言为特定类型,以获取正确的代码提示和类型推断```const [user, setUser] = useState<User | null>(null);setUser(newUser);const [user, setUser] = useState<User>({} as User);setUser(newUser);```#### const 断言`const` 断言告诉编译器为表达式推断出它能推断出的最窄或最特定的类型,而不是通用类型。```// point变成一个readonly 数组类型,修改数组内容会提示错误。let point = [3, 4] as const; // readonly [3, 4]point[0] = 1 // Error```我们来看一个代码示例:```function useDarkMode() { const [mode, setMode] = React.useState<'dark' | 'light'>(() => { // ... return 'light' }) ... return [mode, setMode] as const}const [mode, setMode] = useDarkMode() // 伪代码,hook需要再函数组件中使用```我们来对比一下 mode 和 setMode 使用 as const 之后的差别:在使用 const 断言之前,mode 和 setMode 类型为:```const mode: "dark" | "light" | React.Dispatch<React.SetStateAction<"dark" | "light">>const setMode: "dark" | "light" | React.Dispatch<React.SetStateAction<"dark" | "light">>```调用setMode时,会提示错误,因为 'dark' | 'light' 并不是可调用类型。使用 as const 断言之后,mode 和 setMode 类型为:```const mode: "dark" | "light"const setMode: React.Dispatch<React.SetStateAction<"dark" | "light">>```调用传参错误时,也会有类型错误提示。可以看到,对于数组来说,每个元素的类型是整个数组元素类型的联合类型```const arr = [1,'2'] // const arr: (string | number)[]```使用 as const 断言之后,数组会变成 readonly 数组且**每个元素有了自己的特定类型**,也有了更好的错误提示。再来看一个 rxjs 中的示例:[fromEvent.ts](https://github.com/ReactiveX/rxjs/tree/master/src/internal/observable/fromEvent.ts)```// These constants are used to create handler registry functions using array mapping below.// 这些常量用于使用下面的数组映射创建处理程序注册表函数const nodeEventEmitterMethods = ['addListener', 'removeListener'] as const;const eventTargetMethods = ['addEventListener', 'removeEventListener'] as const;const jqueryMethods = ['on', 'off'] as const;```使用 as const之后,类型检测更为严格:- readonly 数组,每个元素都有自己的字面量类型,无法调整为其他值,杜绝被意外修改的可能- 在访问数组元素或进行数组解构时,因数组长度固定,避免越界,更不容易出错const 断言和 typeof 搭配使用:[useSelection.tsx](https://github.com/ant-design/ant-design/blob/4.3.4/components/table/hooks/useSelection.tsx)字符串使用 as const 之后,变量就有了字面量类型,typeof 操作符可以提取其字面量类型使用。```export const SELECTION_ALL = 'SELECT_ALL' as const;export const SELECTION_INVERT = 'SELECT_INVERT' as const;export type INTERNAL_SELECTION_ITEM = | SelectionItem | typeof SELECTION_ALL | typeof SELECTION_INVERT;```#### 规避类型检查TypeScript **只允许类型断言为一个更具体或者更不具体的类型**,这个规则可以阻止一些错误的强制类型转换:```const x = "hello" as number;// Error:将 'string' 类型转换为 'number' 类型可能是一个错误,因为这两种类型都没有充分重叠。如果这是故意的,请先将表达式转换为“unknown”。```我们再来看一个 Antd 中的使用示例: [back-top](https://github.com/ant-design/ant-design/blob/4.3.4/components/back-top/index.tsx)```React.useEffect(() => { bindScrollEvent(); return () => { if (scrollEvent.current) { scrollEvent.current.remove(); } (handleScroll as any).cancel(); };}, [props.target]);```handleScroll 是一个函数,但是其他文件中被增加了 cancel 属性,此处直接调用 cancel 方法, TypeScript会提示错误,可以断言为 any 来规避 TypeScript 的类型检查。当然也可以使用命名空间为函数增加静态属性,类似:```typescriptfunction buildLabel(name: string): string { return buildLabel.prefix + name + buildLabel.suffix;}namespace buildLabel { export let suffix = ""; export let prefix = "Hello, ";}console.log(buildLabel("Sam Smith"));```#### 双重断言对于我们已经明确的变量类型,如果不存在重叠,可以先断言为一个宽泛的类型(any、unknown),再断言为一个具体的类型。```// es default export should use const instead of letconst ExportTypography = (RefTypography as unknown) as React.FC<TypographyProps>;```[Typography](https://github.com/ant-design/ant-design/blob/4.3.4/components/typography/Typography.tsx)**注意:类型断言只能够「欺骗」`TypeScript` 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。当使用断言时,应该确保你了解当前值的类型,避免出错。对于可以收窄的类型,尽量使用类型守卫收窄而非断言。**> TypeScript系列会更新多篇文章,欢迎关注。
文章首发公众号“混沌前端”Docz 是零配置的,基于Gatsby + MDX,如果你们部门没有使用文档工具,那么你可以轻松接入,如果已经使用了文档工具不妨了解一下Docz,做个对比。为什么要为项目组件/组件库添加文档组件库文档可以帮我们解决以下问题:不知道项目中有哪些公共组件(比如组员写的组件,但是你并不知道),导致重复开发。复用组件时需要再去翻代码,看看怎么使用,传什么参数。以前写的代码,需要修改或者重构时,无从下手有了组件库文档,基本就可以解决上面的问题,同时也提升了代码复用率和开发效率。前置概念MarkDownMDX MarkDown + JSX,可以在md文件中渲染组件YMAL Front Matter 常见的文档工具vuePressgitbookMDX MarkDown + JSX支持导入React组件 JSON数据 MD或MDX文档支持remark 生态系统中的任何插件Playground 实时修改,实时预览Gatsby生态丰富,有各种各样的插件,支持MDX。JSDoc根据javascript文件中注释信息,生成JavaScript应用程序或库、模块的API文档 的工具参考文档:jsdoc-to-markdownTSDoc参考文档:https://tsdoc.org/playReact Styleguidist 基于JSDOC 可以帮助react项目快速构建项目文档StoryBook一个强大的集组件开发,查看,测试的文档工具,支持多种框架。使用”组件驱动开发“理念。支持多种框架 React Vue Angular Ember Preact Svelte等参考文档:CDDdocsify特点:简单轻便 没有静态构建的 html 文件 多个主题Dumi开箱即用为组件开发而生,支持Markdown扩展,可以渲染组件主题系统,支持自定义渲染样式API自动生成,基于TypeScript类型定义自动生成组件APIDoczbisheng Ant Design组件库文档就是使用bisheng构建的为什么推荐使用Docz基于MDX进行了封装完全使用Gatsby构建,可以使用Gatsby的插件和工具生态零配置支持TypeScript、CSS预处理器内置Playground组件,编辑组件可以进行实时渲染内置Props组件,注释直接生成文档我们来看一下Docz常用的两个内置组件吧。Playground你可能看过AntDesign组件库文档,代码演示 模块中提供了跳转到CodeSandBox、CodePen等网站查看和编辑示例代码的功能。详细内容可以翻阅这篇文章:Antd中示例代码是怎么直接在CodeSandBox中打开的。但是在Docz中你可以直接进行编辑并可以实时查看预览效果。Props只需使用Props组件,即可将参数类型和文档注释生成文档,通过表格进行展示import { Playground, Props } from 'docz'import { Button } from './'# Button<Props of={Button} />## Basic usage<Playground> <Button>Click me</Button> <Button kind="secondary">Click me</Button></Playground>import React from 'react'import t from 'prop-types'const Button = ({ children, kind }) => { // We use the kind prop to determine the button's class return <button className={kind}>{children}</button>}Button.propTypes = { /** * This is a pretty good description for this prop. */ kind: t.oneOf(['primary', 'secondary', 'cancel', 'dark', 'gray']),}Button.defaultProps = { kind: 'primary',}export Button安装、配置和构建安装npm install docz # react react-dom运行"scripts": { "docz:dev": "docz dev", "docz:build": "docz build", "docz:serve": "docz build && docz serve"}开发创建.mdx文件即可(指定name和route)。构建npm run build # 生成静态资源在.docz/dist目录中npm run build -- --dest docs-site-directory # 通过--dest 指定文档生成目录也可以在配置中指定打包输出目录// doczrc.jsexport default { dest: '/some-folder'}部署构建之后可以使用任何静态站点托管服务进行部署。MDX支持可以直接引入.jsx/.tsx组件,样式;内置组件PlaygroundPlayground支持编辑实时渲染,支持函数组件和StateProps组件内的prop-types定义和typeScript的Interface会通过<Props>转换成表格展示文档设置使用YMAL自定义文档设置(也可以自定义属性,用于自定义theme)---name: My Documentroute: /custom-routemenu: Documentshidden: false---CSS预处理器需要Gatsby提供的能力,安装插件TypeScript支持// doczrc.jsexport default { typescript: true}如果需要精确控制组件后缀,可以使用filterComponents and docgenConfig进行过滤支持自定义主题项目配置基本配置base 页面访问的basePathsrc 指定组件存放目录files 指定docz解析文件查找路径规则 默认会查找所有扩展名为.mdx的文件ignore 需要忽略解析的文件dest 指定docz build的目录title Header展示title,默认会去package.json中name字段description HTML中meta字段typescript typescript支持 默认false .mdx文件中需要引入TypeScript组件则需要设置propsParser props格式化 供<Props />渲染使用,禁用可以提升性能。config 指定docz配置文件 默认顺序 docz.json | .doczrc | doczrc.json |doczrc.js | docz.config.js | docz.config.jsonpublic 指定公共目录,绝对资源路径会从这个目录下取数据editBranch 点击 Github 按钮时用于编辑文档的分支host devServer地址 默认 '127.0.0.1'port devServer 端口构建流程menu 可指定菜单中文档的顺序plugins 指定要使用的插件数组组件和HooksAPIComponentsProvider 将组件传递给 MDX,它们将在您将 Markdown 转换为 html 时使用Playground 渲染组件并在其中显示代码的可编辑版本Props 获取组件并根据组件中属性定义生成属性表的组件useComponents 配合ComponentsProvider使用useDocs 获取所有已解析文档的列表, 当要创建菜单或列表之类的内容时会很有用。useMenus 返回 Docz 构建的菜单useConfig 获取项目配置中项目配置对象支持自定义插件和MDX插件使用注意:每次涉及到路由的变化都需要重启生效,遇到缓存问题可以删除.docz文件夹后重启// 一个简单的docz配置 doczrc.jsexport default { files: './docs/mdx/*.{md,markdown,mdx}', dest: './docs/site', title: 'Flex-Ctrip-Offline', typescript: true}使用Gatsby生态你可以直接在Docz中使用丰富的Gatsby插件,只需在根目录创建gatsby-config.js文件即可。比如,你想在Docz页面中引入script脚本,可以使用gatsby-plugin-load-script插件:module.exports = { plugins: [ { resolve: 'gatsby-plugin-load-script', options: { disable: false, src: '//www.example.com/demo.js', crossorigin: 'anonymous', onLoad: '() => console.log("add ubt script!")', } }, { resolve: 'gatsby-plugin-load-script', options: { disable: false, src: '//www.example.com/demo.js', crossorigin: 'anonymous', onLoad: '() => console.log("add shark script!")', } } ], }如果你想自定义webpack打包配置,可以新建gatsby-node.js:// 此文件是用于docz自定义webpack配置。是Gatsby提供的功能:https://www.gatsbyjs.com/docs/how-to/custom-configuration/add-custom-webpack-config/const path = require('path')exports.onCreateWebpackConfig = ({ stage, rules, loaders, plugins, actions,}) => { actions.setWebpackConfig({ resolve: { alias: { '@components': path.resolve(__dirname, '../src/components'), '@styles': path.resolve(__dirname, '../src/styles'), '@models': path.resolve(__dirname, '../src/models'), '@utils': path.resolve(__dirname, '../src/utils') } } })}
本篇主要分享什么内容:常用的文档/静态站点生成工具有哪些每个工具有什么特点工具适应场景前置概念MarkDownMDX MarkDown + JSXYMAL Front Matter组件库文档工具选型AndD组件库文档是怎么制做的,使用了什么工具。以AntD Button组件为例,我们看一下antd组件库的文档页面结构构成和文档生成:Button文档Button文档仓库源文件网站文档仓库源文件按钮类型bishengAntD使用了bisheng来生成组件库文档,把MarkDown进行拼接和渲染成最终的文档展示页面。静态站生成工具方案vuePressgitbookMDXMarkDown + JSX支持导入React组件支持remark 生态系统中的任何插件Playground 实时修改,实时预览基础支持MarkDown语法完全支持JSX 以<字符开头的行都视为JSX代码块支持import 和 exportsimport 组件 json数据 md或mdx文档MDXProvider提供MarkDown渲染HTML使用组件的映射 组件列表GatsbyDemo初始化npm init gatsbynpm install -g gatsby-cligatsby new运行npm run develop特点:生态好,功能丰富,有各种各样的插件,支持MDX。Gatsby 有一个强大的功能,称为数据层,使用 Gatsby 的数据层,您可以组合来自多个来源的数据,这让您可以为每种类型的数据选择最佳平台。http://localhost:8000/___graphql 中可以看到GraphQL数据数据来源Gatsby-source-* 数据拉入:页面数据拉入 使用页面查询,页面中导出 query,通过graphql查询即可 组件中拉入数据 可以使用useStaticQuery钩子拉入,动态创建页面Gatsby的 文件系统路由 API定义用于命名src/pages目录中文件的特殊语法,它允许您根据数据层中的节点集合为站点动态创建新页面。JSDoc根据javascript文件中注释信息,生成JavaScript应用程序或库、模块的API文档 的工具安装npm install -D jsdoc使用jsdoc xxx.js默认会输出文档到out文件夹,可以通过--destination指定输出路径jsdoc-to-markdownTSDochttps://tsdoc.org/playReact StyleguidistStoryBook一个强大的集组件开发,查看,测试的文档工具,支持多种框架。使用”组件驱动开发“理念。- 支持多种框架 React Vue Angular Ember Preact Svelte等 Tutorials CDDdocsify特点:简单轻便 没有静态构建的 html 文件 多个主题安装npm i docsify-cli -g初始化docsify init ./docs预览docsify serve docs目录结构index.html 文件入口README.md 主页.nojekyll 防止GitHub Pages忽略以下划线开头的文件侧边栏创建 _sidebar.md(支持目录层级嵌套)。_sidebar.md中页面会自动生成标题和子标题自定义导航栏html标签_navbar.md(同样支持目录层级嵌套,展示形式为弹窗)封面_coverpage.md #/ 首页全屏展示可以指定背景图和背景色可以指定只展示封面配置window.$docsify = {el:'#app', // 根元素repo:'docsifyjs/docsify/', //Git仓库地址maxLevel: 6, // 目录最大层级loadNavbar: false, // 加载_navbar.md作为导航栏(或者直接指定md路径)loadSidebar: false, // 加载_sidebar.md作为侧边栏hideSidebar: true, // 隐藏侧边栏subMaxLevel: 0, // 在自定义侧边栏中添加目录(最大层级)auto2top: true, // 页面路径改变时滚动到屏幕顶部homepage: 'README.md', // `#/` 主页basePath: '/path/', // 基本路径, 可以将其设置为其他目录或其他域名relativePath: false, // 如果为 true,则链接是相对于当前上下文的。coverpage: false, // 封面 默认加载_coverpage.md,也可以指定md路径logo,name,nameLink,markdown, // 自定义渲染MarkDown为HTML [文档](https://docsify.js.org/#/markdown)themeColor,executeScript: true,mergeNavbar: true, // 小屏幕上的导航栏将与侧边栏合并externalLinkTarget: '_self', // default: '_blank' 打开默认连接方式routerMode: 'history', // default: 'hash' 路由模式onlyCover: false, // `#/`只展示封面requestHeaders: { 'x-token': 'xxx', }, // 设置请求资源头notFoundPage: true, // 加载_404.md 或指定相应的mdvueComponents, // 注册vue组件, 可在md中直接使用vueGlobalOptions,vueMounts}主题 官方和社区制作的主题插件 全文检索,谷歌分析,表情符号,第三方脚本支持,图片缩放,在github上编辑,jsfiddler Demo预览,复制到剪切板,Gitalk, 分页和标签等PWASSR嵌入文件:支持视频, 音频,iframe或代码块,甚至MarkDownDocz基于MDX进行了封装完全使用Gatsby构建,可以使用Gatsby的插件和工具生态零配置TypeScript支持安装npm install docz # react react-dom运行"scripts": { "docz:dev": "docz dev", "docz:build": "docz build", "docz:serve": "docz build && docz serve"}开发创建.mdx文件即可(指定name和route)。构建npm run build # 生成静态资源在.docz/dist目录中npm run build -- --dest docs-site-directory # 通过--dest 指定文档生成目录也可以在配置中指定打包输出目录// doczrc.jsexport default { dest: '/some-folder'}部署构建之后可以使用任何静态站点托管服务进行部署。MDX支持可以直接引入.jsx/.tsx组件,样式;内置组件PlaygroundPlayground支持编辑实时渲染,支持函数组件和StateProps组件内的prop-types定义和typeScript的Interface会通过<Props>转换成表格展示文档设置使用YMAL自定义文档设置(也可以自定义属性,用于自定义theme)---name: My Documentroute: /custom-routemenu: Documentshidden: false---CSS预处理器需要Gatsby提供的能力,安装插件TypeScript支持// doczrc.jsexport default { typescript: true}如果需要精确控制组件后缀,可以使用filterComponents and docgenConfig进行过滤支持自定义主题项目配置基本配置base 页面访问的basePathsrc 指定组件存放目录files 指定docz解析文件查找路径规则 默认会查找所有扩展名为.mdx的文件ignore 需要忽略解析的文件dest 指定docz build的目录title Header展示title,默认会去package.json中name字段description HTML中meta字段typescript typescript支持 默认false .mdx文件中需要引入TypeScript组件则需要设置propsParser props格式化 供<Props />渲染使用,禁用可以提升性能。config 指定docz配置文件 默认顺序 docz.json | .doczrc | doczrc.json |doczrc.js | docz.config.js | docz.config.jsonpublic 指定公共目录,绝对资源路径会从这个目录下取数据editBranch 点击 Github 按钮时用于编辑文档的分支host devServer地址 默认 '127.0.0.1'port devServer 端口构建流程menu 可指定菜单中文档的顺序plugins 指定要使用的插件数组组件和HooksAPIComponentsProvider 将组件传递给 MDX,它们将在您将 Markdown 转换为 html 时使用Playground 渲染组件并在其中显示代码的可编辑版本Props 获取组件并根据组件中属性定义生成属性表的组件useComponents 配合ComponentsProvider使用useDocs 获取所有已解析文档的列表, 当要创建菜单或列表之类的内容时会很有用。useMenus 返回 Docz 构建的菜单useConfig 获取项目配置中项目配置对象支持自定义插件和MDX插件使用注意:每次涉及到路由的变化都需要重启生效,遇到缓存问题可以删除.docz文件夹后重启// 一个简单的docz配置 doczrc.jsexport default { files: './docs/mdx/*.{md,markdown,mdx}', dest: './docs/site', title: 'Flex-Ctrip-Offline', typescript: true}Dumi开箱即用为组件开发而生,支持Markdown扩展,可以渲染组件主题系统,支持自定义渲染样式API自动生成,基于TypeScript类型定义自动生成组件API组件开发脚手架npx @umijs/create-dumi-lib # 初始化一个文档模式的组件库开发脚手架npx @umijs/create-dumi-lib --site # 初始化一个站点模式的组件库开发脚手架 (比文档模式多一个主页,主页使用docs/index.md)# 也可手动切换文档模式 => 站点模式: 修改.umirc.ts,添加mode:'site'静态站点脚手架npx @umijs/create-dumi-app运行npm install npm start构建及部署npm run build目录结构├── README.md├── docs # 组件库文档目录│ ├── index.md # 组件库文档首页(不存在会使用README.md)│ └── otherDir # 组件文档其他路由│ ├── index.md│ ├── sample.md│ └── help.md├── src # 组件库源码目录(单纯文档站点可忽略)│ ├── Foo│ └── index.ts├── .umirc.ts # dumi配置文件└── .fatherrc.ts # father-build的配置文件用于组件库打包代码块jsx和tsx的代码块会被dumi解析为React组件,并进行渲染。dumi引入组件原则:像用户一样使用组件:直接引入组件库进行文档demo演示。不仅可以用来调试组件、编写文档,还能用来被用户直接拷贝到项目中使用。dumi会为我们自动创建组件库NPM包->组件库源代码的映射。外部demo可以引入外部文件作为demo渲染,并可支持查看demo源代码<code src="/path/to/complex-demo.tsx"></code>直接嵌入渲染```jsx/** * inline: true */import React from 'react';export default () => '我会被直接嵌入';```embed Markdow嵌套<!-- 引入全量的 Markdown 文件内容 --><embed src="/path/to/some.md"></embed><!-- 根据行号引入指定行的 Markdown 文件内容 --><embed src="/path/to/some.md#L1"></embed><!-- 根据行号引入部分 Markdown 文件内容 --><embed src="/path/to/some.md#L1-L10"></embed><!-- 根据正则引入部分 Markdown 文件内容 --><embed src="/path/to/some.md#RE-/^[^\r\n]+/"></embed>组件API自动生成JS Doc注释 + TypeScript类型定义的方式实现组件API自动生成如何在非-umi-项目中使用-dumiDEMO理念目前我们选择的是使用Docz来做为业务组件库的文档生成工具,下一篇会讲一下我们为什么选择Docz,它有什么优点。欢迎持续关注,微信公众号”混沌前端“
Typora中可以通过配置图片上传服务的自定义命令,在自定义服务中上传图片并打印上传结果,当插入图片时就会将本地图片上传,并替换成网络图片地址。以file-uploader-cli为例, 配置fuc(windows)或/usr/local/bin/node /usr/local/bin/fuc(MacOS)之后,插入图片就会调用file-uploader-cli并传入本地图片地址,图片上传完成Typora会捕获到console打印的网络地址并替换本地图片。Typora是怎么获取到file-uploader-cli中console打印结果呢?Node.js中可以通过chid process创建子进程,在子进程中执行任务,并捕获子进程stdout输出。Child Process我们先了解一下Node.js创建方式子进程有哪几种方式,child process提供了spawn、exec、execFile、fork四种方式来创建异步子进程,execFile、exec、spawn有对应的同步子进程(会阻塞 Node.js 事件循环,直到子进程退出或终止),exec、execFile、fork底层都是通过spawn/spawnSync来实现的。首先了解一个概念:stdin,stdout,stderr是啥?用于输入、输出和错误输出的标准流,一般是从键盘读取,而将标准输出和标准错误打印到屏幕上。每个进程都会有相应的文件描述符绑定到stdin,stdout,stderr用来获取输入输出。stdout和stderr输出管道的容量是有限的(且特定于平台),如果子进程在没有捕获输出的情况下写入标准输出超过该限制,则子进程会阻塞等待管道缓冲区接受更多数据。exec和execFile可以通过maxBuffer指定stdout和stderr上允许的最大数据量(以字节为单位,默认值: 1024 * 1024),超出容量则子进程将终止并截断任何输出。不同方式的实现创建一个打印输出的文件,我们来获取执行print.js的输出。#!/usr/bin/env node // print.js console.log('http://baidu.com')spawn先看一个官方的小示例// spawn、fork、exec和execFile方法都返回 ChildProcess 实例, 实现了EventEmitter API,允许父进程·调用的监听器函数 const { spawn } = require('child_process'); const ls = spawn('ls', ['-lh', '.']); ls.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); ls.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); ls.on('close', (code) => { console.log(`child process exited with code ${code}`); });spawn实现读取stdout输出数据创建一个test.js文件来获取输出,stdout输出流的缓冲机制,数据量大时捕获会多次接收到数据。#!/usr/bin/env node const { spawn } = require('child_process'); const [command, ...rest] = process.argv.slice(2) const sp = spawn(command, rest); let output = [],errorOutput = []; sp.stdout.on('data', (data) => { output.push(data) }); sp.stderr.on('data', (data) => { errorOutput.push(data) }); sp.on('close', (code) => { // 默认获取到的是Buffer,转成字符 const outputStr = code === 0 ? Buffer.concat(output).toString() : Buffer.concat(errorOutput).toString() // 处理结尾增加的空行 const printText = outputStr.toString().split(/\r?\n/)[0] // print.js输出的内容 console.log('获取到的输出:',printText) });为文件添加执行权限, 并执行:chmod +x test.js ./test.js node ./print.js # or node test.js node ./print.js我们就可以看到test.js捕获到子进程的输出了。如果为print.js创建了软连接,也可直接使用./test.js printexecexec 会创建一个新的shell,在shell中执行,执行完成后会将stdout和stderr传入回调,所以stdout会一次获取到子进程所有输出数据。exec实现读取stdout输出数据#!/usr/bin/env node const { exec } = require('child_process'); exec(`${process.argv.slice(2).join(' ')}`, (error, stdout, stderr) => { if (error) { console.error(error); return; } // 默认获取到的是Buffer,转成字符 处理结尾增加的空行, const printText = stdout.toString().split(/\r?\n/)[0] console.log(printText); });execSync:execSync和exec不同之处在于:execSync子进程完全关闭之前不会返回。#!/usr/bin/env node const { execSync } = require('child_process'); const result = execSync(`${process.argv.slice(2).join(' ')}`); const printText = result.toString().split(/\r?\n/)[0] console.log(printText)execFile和exec类似,不过不会创建新的shell,指定的可执行文件作为新进程,exec也是通过调用execFile来实现的。fork创建新的 Node.js 进程并使用建立的 IPC 通信通道(其允许在父子进程之间发送消息)其他pipe管道打印输出在创建的子进程中执行任务,比如创建子进程通过npm为项目安装依赖,这个时候子进程执行信息需要输出到父进程中打印,用户才能看到执行进度和结果。// 父进程监听子进程输出并打印 sp.stdout.on('data', (data) => { console.log(data) }); sp.stderr.on('data', (data) => { console.log(data) });stdout和stderr属于标准输出流,可以使用Steam API,通过pipe管道传递到父进程中。sp.stdout.pipe(process.stdout); sp.stderr.pipe(process.stderr);也可以通过指定 option.stdio 配置父进程和子进程之间建立的管道。const { spawn } = require('child_process'); // 子进程将使用父进程的标准输入输出。 spawn('prg', [], { stdio: 'inherit' }); // 衍生仅共享标准错误的子进程。 spawn('prg', [], { stdio: ['pipe', 'pipe', process.stderr] }); // 打开额外的文件描述符=4,以与呈现 startd 风格界面的程序进行交互。 spawn('prg', [], { stdio: ['pipe', null, null, null, 'pipe'] });Windows和类Unix(Mac,Linux,Unix)区别child_process.execFile() 在类Unix系统上执行效率可能快,因为不用创建新的shell。但是在Windows上.bat和.cmd文件在没有终端的情况下不能单独执行,所以不能使用execFile。可以通过spawn传入shell配置调用,或调用cmd.exe并传入.bat或.cmd文件。使用cross-spawn或execa来帮我们执行命令,解决跨平台问题,更优雅调用child_process方法
Typora是我经常使用的一款软件,用来写MarkDown很舒适,有着非常优秀的使用体验:实时预览自定义图片上传服务文档转换主题自定义起因不过我遇到一个非常好玩的事情,当我复制Typora内容粘贴到文本编辑器时,会得到MarkDown格式的内容;复制到富文本编辑器时,可以渲染出富文本效果:复制到VS Code:复制到其他富文本编辑器:我很好奇为什么会出现两种不同的结果,Typora应该是使用Electron(或类似技术)开发的,我尝试用Clipboard API来进行测试:// 为什么使用setTimeout:我是在Chrome控制台进行的测试,clipboard依托于页面,所以我需要设置1s延时,以便可以点击页面聚焦 setTimeout(async()=>{ const clipboardItems = await navigator.clipboard.read(); console.log(clipboardItems) },1000)然后看到了剪切板中有两种不同类型的内容:纯文本text/plain和富文本text/html。所以不同的内容接收者选择了不同的内容作为数据,文本编辑器拿到的是纯文本,富文本编辑器获取的是富文本格式数据。再来看看获取到的具体内容吧:setTimeout(async()=>{ const clipboardItems = await navigator.clipboard.read(); console.log(clipboardItems) for (const clipboardItem of clipboardItems) { for (const type of clipboardItem.types) { const contentBlob = await clipboardItem.getType(type) const text = await contentBlob.text() console.log(text) } } },1000)Clipboard塞入数据试一下:setTimeout(async ()=>{ await navigator.clipboard.write([ new ClipboardItem({ ["text/plain"]: new Blob(['# 纯文本和富文本'],{type:'text/plain'}), ["text/html"]: new Blob(['<h1 cid="n21" mdtype="heading" class="md-end-block md-heading md-focus" style="box-sizing: border-box; break-after: avoid-page; break-inside: avoid; orphans: 4; font-size: 2.25em; margin-top: 1rem; margin-bottom: 1rem; position: relative; font-weight: bold; line-height: 1.2; cursor: text; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: rgb(238, 238, 238); white-space: pre-wrap; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-family: "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, "Segoe UI Emoji", sans-serif; font-style: normal; font-variant-caps: normal; letter-spacing: normal; text-align: start; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration: none;"><span md-inline="plain" class="md-plain md-expand" style="box-sizing: border-box;">纯文本和富文本</span></h1>'],{type:'text/html'}), }) ]); },[1000])尝试了几个富文本编辑器得到的结果(不同富文本编辑器的具体实现可能存在差异):如果只存在纯文本(仅保留上段代码中的纯文本部分), 会读取剪切板中纯文本内容如果存在纯文本和富文本,会读取剪切板中富文本内容那这个效果是Typora帮我们实现的吗?我们先来看一下复制富文本的默认行为,打开一个网页,复制网页文本,然后使用刚才的代码尝试一下,看看读取到的剪切板内容。我们可以看到,在复制富文本的时候,Chrome实现的clipboard API都会生成两份结果,一份是纯文本格式text/plain,一份是富文本格式text/html。不同的是:当我们在Typora复制时,得到的是Markdown格式的纯文本和富文本,是Typora帮我们进行了处理。监听复制,写入剪切板监听复制我们可以使用HTMLElement.oncopy实现:打开任意一个网页,切换到控制台:document.body.oncopy = function(e){ console.log(e) var text = e.clipboardData.getData("text"); console.log(text) }复制页面中内容,我们就可以的看到打印的结果了: 本来为数据会在clipboardData中,但是尝试了一下并没有获取到内容,看了一下API, 需要在copy事件中通过setData设置数据,在paste时间中getData获取数据。我们可以通过Selection API来获取选中的内容。document.addEventListener('copy', function(e){ e.preventDefault(); // 防止我们筛入的数据被覆盖 const selectionObj = window.getSelection() const rangeObj = selectionObj.getRangeAt(0) const fragment = rangeObj.cloneContents() // 获取Range包含的文档片段 const wrapper = document.createElement('div') wrapper.append(fragment) e.clipboardData.setData('text/plain', wrapper.innerText + '额外的文本'); e.clipboardData.setData('text/html', wrapper.innerHTML+ '<h1>额外的文本</h1>'); });或者使用clipboard.write实现写入:document.body.oncopy = function(e){ e.preventDefault(); const selectionObj = window.getSelection() const rangeObj = selectionObj.getRangeAt(0) const fragment = rangeObj.cloneContents() // 获取Range包含的文档片段 const wrapper = document.createElement('div') wrapper.append(fragment) navigator.clipboard.write([ new ClipboardItem({ ["text/plain"]: new Blob([wrapper.innerText,'额外的文本'],{type:'text/plain'}), ["text/html"]: new Blob([wrapper.innerHTML,'<h1>额外的富文本</h1>'],{type:'text/html'}), }) ]) }监听复制还可以用来添加版权信息,比如上面代码中的额外信息就会出现在复制的文本中。对于复制和粘贴内容也可以通过document.execCommand,不过目前属于已经被弃用的API,不建议使用更多文章欢迎关注“混沌前端”参考文档:ClipboardItemClipboard-writeelement.oncopySelectionRange
使用过Antd的小伙伴应该很熟悉,Antd组件文档有在CodeSandBox和CodePen中打开直接预览和编辑的功能,这么炫酷且实用的功能具体是怎么实现的?codesandbox.io[1] 是一个前端代码的在线编辑器,支持各种不同的框架,可以随时预览代码的运行结果。创建沙盒“在CodeSandBox中打开”是CodeSandbox提供的功能,让我们可以通过直接调用API来创建CodeSandbox沙盒。CodeSandbox提供了几种导入到沙盒中预览的方式:直接使用提供的公共模板从GitHub导入:https://codesandbox.io/s/github使用GitHubBox:将仓库地址中github.com替换为githubbox.comhttps://github.com/Iamxiaozhu/file-uploader-cli => https://githubbox.com/Iamxiaozhu/file-uploader-cli安装浏览器扩展,打开GitHub,页面中会添加一个“在CodeSandBox中打开”的按钮通过命令行从本地导入:npm install -g codesandboxcodesandbox ./通过调用API方式创建沙箱[2]:CodeSandbox提供了通过API让我们可以通过编程的方式来创建sandbox。我们可以在文档里通过示例代码来创建sandbox,方便用户编辑和查看。通过Get和Post请求调用https://codesandbox.io/api/v1/sandboxes/define,即可实现创建CodeSandbox沙箱。Get调用Demo[3] Post调用Demo[4]Important:CodeSandBox官方Demo[5]Antd中示例代码跳转CodeSandbox、CodePen等:模板示例[6]嵌入SandBox[7]CodeSandBox还支持直接嵌入:在文档,博客和其他网站中嵌入沙箱,可以展示代码和预览效果:以官方Demo[8]为例:点击Share,这里选择Embed自定义展示内容和主题,复制嵌入代码就可以了,是通过iframe标签来嵌套页面。类似CodeSandBox的在线编辑器有很多,比如:CodePen[9]、StackBlitz[10]、JSFiddle[11]、JSBin[12]、JSRun[13]等。微软和GitHub也都推出了自己的在线代码编辑器(和上面几个不同,只提供了代码编辑功能,无法实时预览):Online VS:https://online.visualstudio.com/GitHub CodeSpaces: https://github.com/features/codespaces其他相关:Code-Server[14]这里推荐一个可以自定义部署的在线代码编辑器:Code-Server。实际上就是VSCode的在线版本,支持安装VSCode插件,内嵌Terminal中会直接在服务器端运行,非常强大。Sandpack[15]Sandpack 是 CodeSandbox 的浏览器打包器。参考资料[1]codesandbox.io: https://codesandbox.io/[2]通过调用API方式创建沙箱: https://codesandbox.io/docs/importing#define-api[3]Get调用Demo: https://codesandbox.io/s/6yznjvl7nw[4]Post调用Demo: https://codesandbox.io/s/qzlp7nw34q[5]CodeSandBox官方Demo: https://codesandbox.io/examples/package/codesandbox[6]模板示例: https://hub.fastgit.org/ant-design/ant-design/blob/master/site/theme/template/Content/Demo/index.jsx[7]嵌入SandBox: https://codesandbox.io/docs/embedding#embed-options[8]官方Demo: https://codesandbox.io/s/react-new?from-embed=&file=/src/App.js[9]CodePen: https://codepen.io/[10]StackBlitz: https://stackblitz.com/[11]JSFiddle: https://jsfiddle.net/[12]JSBin: https://jsbin.com/[13]JSRun: http://jsrun.net/[14]Code-Server: https://github.com/cdr/code-server[15]Sandpack: https://github.com/codesandbox/sandpack
截图来自vue的PR:1.commit-message规范必要性统一格式的提交记录,更清晰和易读可以通过提交记录来了解本次提交的目的,更好的CR和重构更容易了解变更,定位和发现问题每个提交描述都是经过思考的,改善提交质量直接生成ChangeLog2.规范选型常见的commit message规范有:atom,eslint和Angular等,其中Angular规范更为通用。3.Angular的Commit Message规范简介每条提交记录包含三个部分:header,body和footer<header> <BLANK LINE> <body> <BLANK LINE> <footer> Commit Message Header<type>(<scope>): <short summary> │ │ │ │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end. │ │ │ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core| │ elements|forms|http|language-service|localize|platform-browser| │ platform-browser-dynamic|platform-server|router|service-worker| │ upgrade|zone.js|packaging|changelog|dev-infra|docs-infra|migrations| │ ngcc|ve │ └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test 其中type和summary是必要的,scope是可选的Type 必须是以下的类型feat: 新增页面或功能fix: bug修复build: 影响构建系统或外部依赖项的更改ci: 对 CI 配置文件和脚本的更改docs: 文档改动perf: 性能提升改动refactor: 不影响功能的代码重构(既不修复错误也不添加功能)test: 添加或修改测试用例Summary用来提供更改的简洁描述4.规范实施通过commitizen进行交互式提交,husky + commit-msg hook进行提交校验,cz-customizable来自定义交互提交流程和文案,standard-changelog来自动生成changelogimage-202106151731130381.使用commitizen工具,在commit时可以交互的方式选择type安装commitizennpm i -D commitizen package.json中添加对应的npm script"commit":"cz" 改动添加到暂存区后执行commit提交npm run commit 2. 通过husky在git hooks中对不符合规范的提交进行拦截,拦截commitlint进行校验安装husky , commitlint 和 符合angular提交规范的配置npm i -D husky commitlint @commitlint/config-conventional 添加git hooksnpx husky install package.json中添加prepare的npm hook, 在每次install自动执行(yarn v2+不支持prepare)"prepare": "husky install" 执行添加commit-msg hook如果使用husk v4.x版本(推荐升级到新版本),直接在package.json中或.huskyrc.json中新增commit-msg钩子即可package.json"husky": { "hooks": { "commit-msg": "commitlint --edit $1" } } .huskyrc,.huskyrc.json,.huskyrc.js或husky.config.js"hooks": { "commit-msg": "commitlint --edit $1" } 如果使用husky v6+版本,需要添加对应的shell调用方式(husky v6对添加方式做了改动,所以无法通过添加配置到package.json中运行)npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1' 添加commintlint配置(也可以放到package.json中指定)echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js package.json中添加commitlint配置"commitlint": { "extends": [ "@commitlint/config-conventional" ]} 3. 扩展和自定义规范commitizen提供的交互式默认是英文的,如果改成中文或者对交互流程进行改动,可以使用cz-customizable进行扩展和自定义安装cz-customizablenpm i -D cz-customizable package.json中添加配置"config": { "commitizen": { "path": "node_modules/cz-customizable" }, "cz-customizable": { "config": ".cz-config.js" } } .cz-config.js就是cz-customizable配置的具体文件了,可以参考CZ-Config-Example并进行改动, 可以把文案翻译成中文,自定义修改提示等。也可以通过fork cz-customizable创建内封配置文件的npm包npm i -D your-own-package "config": { "commitizen": { "path": "node_modules/your-own-package" } } 配置文件可以自定义交互内容,比如可以只保留type scope breakchange confirm * 选择提交类型 * 简单描述本次改动 * 是否有重大变更 * 确定提交 配置文件中设置skipQuestions: \['body','customScope','scope','footer'\],即可忽略其他选项 allowBreakingChanges: \['feat', 'fix'\], 仅在feat和fix时提示 breakchange ### 4.自动生成changelog 可以使用standard-changelog来自动生成changelog npm i -D standard-changelog 配置npm script "gen-changelog": "standard-changelog" ## 5.其他 通过npm script进行commit,如果eslint没有通过(在pre-commit 钩子中做了eslint检测),但是又想提交可以通过加'––'来向npm script传参 npm run commit -- --no-verify # or npm run commit -- -n
第一章 专业主义清楚你想要什么承担责任了解你的领域失误率永远不能等于0,但你有责任让它无限接近于零。每个专业软件开发人员必须了解的技能:设计模式。必须能够描述GOF书中的全部24种模式。设计原则 必须了解SOLID原则,深刻理解组件设计原则结构图 流程图 决策表 状态迁移图表第二章 说“不”大多数时间我们都希望能够说“是”。健康的团队都会努力寻找给他人肯定的答复。运作良好的团队经理和开发人员,会相互协商,直到达成共同认可的行动方案。但是有时候,获取正确决策的唯一途径,便是勇敢无畏的说“不”。这个是比说“是”更负责任,更专业,也更困难的能力。第三章 说“是”遵守承诺如果这个事情依赖他人,无法掌控,需要采取一些具体行动来达成目标。比如坐下来讨论一下具体行动,来一步一步接近目标,直至完成目标。如果确实无法完成,赶紧去调整别人对你的预期,越快越好第四章 编码给他人提供帮助并非说明你更聪明,而是你带来了一个新的视角,对解决问题起到了显著的催化作用。PS:这个描述有点绝对,事实上能够提供完善的解决方案,一眼看出问题出在什么地方也是一种可贵的能力和丰富经验的累积。辅导年轻的程序员是经验丰富程序员的职责所在,向资深的导师寻求帮助也是年轻程序员的专业职责。第五章 TDD的三项法则在编写好失败单元测试之前,不编写任何产品代码只要有一个单元测试失败了就不需要再写测试代码;无法通过编译也是一种失败情况产品代码恰好能够让当前失败的单元测试通过即可,不要多写。TDD可以提升代码确定性、降低代码缺陷率,优化文档和设计的原则。测试先行,会迫使你去思考什么是好的设计。事后测试只是一种防守,先行编写测试则是进攻。事后编写的测试已经受制于已有代码,已经知道问题是如何解决的。测试先行的防守编写测试代码比起来,后写的测试在深度和捕获错误的灵敏度方面要逊色很多。第六章 练习-自身经验的扩展老板的职责不包括避免你的技术落伍,也不包括为你打造一份好看的简历。保持自己的技能不落伍是自己的责任。第七章 验收测试做业务和写程序的人都容易陷入一个陷阱:过早进行精细化。不确定性原则东西画在纸上与真正做出来,是不一样的。业务方看到真正运行的情况就会意识到,自己想要的东西根本不是这样。一看到已经满足的需求,关于到底想要什么,他们就会有更好的想法——通常并不是他们当时想看到的样子。预估焦虑即便拥有全面准确的信息,评估也通常会存在巨大的变数。其次,因为不确定原则的存在,不可能通过反复推敲实现早起的精准性。需求一定会变化的,所以过早追求精确性是徒劳的。身为专业开发人员,你的职责是协助开发团队开发出最棒的软件。也就是说每个人都需要关心错误和疏忽,并协助改正。验收测试和单元测试验收测试是业务方的,是正式的需求文档,描述了业务方认为系统应该如何运行。关心验收测试结果的是业务方和程序员。单元测试是程序员写给程序员的,它是正式的设计文档,描述了底层结构和代码行为,关心单元测试结果的只是程序员。它们的主要目的是如实描述系统的设计、结构和行为。当然他们也可以验证设计、结构和行为是否达到了具体指标,但是它们的真正价值不是在测试上,而是在具体指标上。第八章 测试策略尽管公司可能设有独立的QA小组专门负责测试软件,但是开发小组仍然要把“QA应该找不到任何错误”作为努力的目标。对于QA找到的每一个问题,开发团队都应高度重视,认真对待。应该反思为什么出现这种错误,并采取措施避免今后重犯。第九章 时间管理关于会议,有两条真理会议是必需的会议浪费了大量的时间在走入死胡同后可以快速意识到,并且有勇气走回头路。这就是坑法则:如果你掉进了坑里,别挖。第十章 预估当发现预估的时间不足时,最重要的是努力解决这个问题,并向外部同步进展。预估真正的问题在于:业务方认为是承诺,开发方认为是猜测。不要给出承诺,除非你确切知道可以完成。第十二章 协作你需要理解手上正在编写的代码业务价值是什么,了解雇佣你的企业将如何从你的工作中获得回报。对做的事情充满激情是好的,但是,最好把注意力集中到付我们薪水的老板所追求的目标上。(关注业务和业务目标)
给大家推荐几个很方便的绘制流程图,架构图的神器。画图画的好,加薪少不了!绘图是一项软技能,图形可以表达比文字更丰富的内容,更清楚的展示逻辑,好的图形可以帮我们更好的表达自己,来看一下几个绘制图形的好工具吧。1. ProcessOn 在线绘制首先放一个注册链接:点击注册 ,推荐使用这个地址注册,可以获得7天会员。可以在7天之内创建多个文件备用,7天之后也会保留。特点类型丰富:流程图,思维导图,思维笔记,原型图,UML类图等,。模板市场:每个账号有9个免费文件额度。有模板市场,非常丰富精美的模板,免费的模板也很多2. 金山文档 在线绘制特点类型丰富:Word, Excel, PPT,思维导图,流程图等,支持协同操作。文件数量不限,总容量1G。(但是单个文件绘制的节点有限制,一般来说还是很够用的)。3. Draw.io 在线绘制 本地客户端绘制特点多种使用方式:在线网站,VSCode插件,本地客户端,也支持NextCloud类型非常丰富:支持各种格式图表支持存储到GitHub仓库。最重要的是:开源!免费!在线使用:VSCode插件方式(安装插件之后,创建.drawio后缀的文件,用VSCode打开即可):本地安装客户端:
业务组件库必要性项目经过长期维护之后往往会沉淀出很多公共组件,当一个组件编写完成之后,其他维护者想要使用这个组件,了解这个组件是做什么的,应该怎么用,必须再去翻看源码,或者没有压根儿注意到这个组件导致重发开发。这个时候一个完善的组件库就很有必要了,可以保障开发者之间进行良好的协作。组件库可以帮我们解决以下问题:业务组件跨项目复用,提升开发效率统一代码实现,统一代码质量保障组件库文档提供清晰的使用方式和直观的展示效果组件库的组成业务组件库是基于基础组件库进行编写的,基础组件库是使用公司的antd。我们会对基础组件针对不同需求进行封装,达到可以直接引用无需二次开发的目的(比如:产线下拉选择组件,我们会将接口请求数据、选项模糊查询、多语言等封装到组件内部,无需二次开发,接口请求也不需要再散落到各个页面中)。目标引⼊即可使⽤,⽆需⼆次开发完善的文档和组件效果演示,支持代码一键拷贝良好的代码质量:使用Jest进行单元测试,保障代码质量良好的编码规范和代码提交规范:ESLint、Husky、commit-lint等工具进行校验和拦截根据代码提交自动生成ChangeLog组件效果演示支持编辑实时渲染(在文档中编辑组件,实时变更展示效果)开发计划准备组件库文档选型评审首批组件梳理开发基础架构设计目录结构规范本地开发环境构建代码规范校验代码提交规范校验CR和发布规范TypeScript支持单元测试Jest + @testing-library组件开发组件库文档生成本地预览调试本地构建初次发布发布和使用初次版本迭代升级业务组件新增业务组件迭代组件库文档托管在线访问可视化构建页面区块和页面业务组件库完成之后,就可以尝试低代码了,可以使用拖拽或者图像识别自动生成业务代码
2022年11月
2022年05月
2021年12月
2021年09月
2021年08月