基于slate构建文档编辑器
基于slate构建文档编辑器
slate.js
是一个完全可定制的框架,用于构建富文本编辑器,在这里我们使用slate.js
构建专注于文档编辑的富文本编辑器。
描述
富文本编辑器是一种可内嵌于浏览器,所见即所得的文本编辑器。现在有很多开箱即用的富文本编辑器,例如UEditor
、WangEditor
等,他们的可定制性差一些,但是胜在开箱即用,可以短时间就见到效果。而类似于Draft.js
、Slate.js
,他们是富文本编辑器的core
或者叫做controller
,并不是一个完整的功能,这样就能够让我们有非常高的可定制性,当然也就会造成开发所需要的时间比较多。在实际应用或技术选型的时候,还是要多做一些调研,因为在业务上框架没有绝对的优势与劣势,只有合适不合适。
在slate
的文档中有对于框架的设计原则上的描述,搬运一下:
- 插件是一等公民,
slate
最重要的部分就是插件是一等公民实体,这意味着你可以完全定制编辑体验,去建立像Medium
或是Dropbox
这样复杂的编辑器,而不必对库的预设作斗争。 - 精简的
schema
核心,slate
的核心逻辑对你编辑的数据结构进行的预设非常少,这意味着当你构建复杂用例时,不会被任何的预制内容所阻碍。 - 嵌套文档模型,
slate
文档所使用的模型是一个嵌套的,递归的树,就像DOM
一样,这意味着对于高级用例来说,构建像表格或是嵌套引用这样复杂的组件是可能的,当然你也可以使用单一层次的结构来保持简单性。 - 与
DOM
相同,slate
的数据模型基于DOM
,文档是一个嵌套的树,其使用文本选区selections
和范围ranges
,并且公开所有的标准事件处理函数,这意味着像是表格或者是嵌套引用这样的高级特性是可能的,几乎所有你在DOM
中可以做到的事情,都可以在slate
中做到。 - 直观的指令,
slate
文档执行命令commands
来进行编辑,它被设计为高级并且非常直观地进行编辑和阅读,以便定制功能尽可能地具有表现力,这大大的提高了你理解代码的能力。 - 可协作的数据模型,
slate
使用的数据模型特别是操作如何应用到文档上,被设计为允许协同编辑在最顶层,所以如果你决定要实现协同编辑,不必去考虑彻底重构。 - 明确的核心划分,使用插件优先的结构和精简核心,使得核心和定制的边界非常清晰,这意味着核心的编辑体验不会被各种边缘情况所困扰。
前边提到了slate
只是一个core
,简单来说他本身并不提供各种富文本编辑功能,所有的富文本功能都需要自己来通过其提供的API
来实现,甚至他的插件机制也需要通过自己来拓展,所以在插件的实现方面就需要自己制定一些策略。slate
的文档虽然不是特别详细,但是他的示例是非常丰富的,在文档中也提供了一个演练作为上手的基础,对于新手还是比较友好的。在这里我们构建了专注于文档编辑的富文本编辑器,交互与ui
方面对于飞书文档的参考比较多,整体来说坑也是比较多的,尤其是在做交互策略方面,不过做好兜底以后实现基本的文档编辑器功能是没有问题的。在这里我使用的slate
版本为0.80.0
,不排除之后的框架策略调整,所以对于版本信息也需要注意。
插件策略
上边我们提到了,slate
本身并没有提供插件注册机制,这方面可以直接在文档的演练部分看出,同时也可以看出slate
暴露了一些props
使我们可以拓展slate
的功能,例如renderElement
、renderLeaf
、onKeyDown
等等,也可以看出slate
维护的数据与渲染是分离的,我们需要做的是维护数据结构以及决定如何渲染某种类型的数据,所以在这里我们需要基于这些注册机制来实现自己的插件拓展方案。
这是文档中演练最后实现的代码,可以简单了解一下slate
的控制处理方案,可以看到块级元素即<CodeElement />
的渲染是通过renderElement
来完成的,行内元素即bold
样式的渲染是通过renderLeaf
来完成的,在onKeyDown
中我们可以看到通过监听键盘的输入,我们对slate
维护的数据通过Transforms
进行了一些处理,通过匹配Node
将attributes
写入了数据结构,然后通过两种render
的props
将其渲染了出来,所以这就是slate
的拓展机制与数据渲染分离结构。
const initialValue = [
{
type: 'paragraph',
children: [{ text: 'A line of text in a paragraph.' }],
},
]
const App = () => {
const [editor] = useState(() => withReact(createEditor()))
const renderElement = useCallback(props => {
switch (props.element.type) {
case 'code':
return <CodeElement {...props} />
default:
return <DefaultElement {...props} />
}
}, [])
// Define a leaf rendering function that is memoized with `useCallback`.
const renderLeaf = useCallback(props => {
return <Leaf {...props} />
}, [])
return (
<Slate editor={editor} value={initialValue}>
<Editable
renderElement={renderElement}
// Pass in the `renderLeaf` function.
renderLeaf={renderLeaf}
onKeyDown={event => {
if (!event.ctrlKey) {
return
}
switch (event.key) {
case '`': {
event.preventDefault()
const [match] = Editor.nodes(editor, {
match: n => n.type === 'code',
})
Transforms.setNodes(
editor,
{ type: match ? null : 'code' },
{ match: n => Editor.isBlock(editor, n) }
)
break
}
case 'b': {
event.preventDefault()
Transforms.setNodes(
editor,
{ bold: true },
{ match: n => Text.isText(n), split: true }
)
break
}
}
}}
/>
</Slate>
)
}
const Leaf = props => {
return (
<span
{...props.attributes}
style={{ fontWeight: props.leaf.bold ? 'bold' : 'normal' }}
>
{props.children}
</span>
)
}
插件注册
在上一节我们了解了slate
的插件拓展与数据处理方案,那么我们也可以看到这种最基本的插件注册方式还是比较麻烦的,那么我们就可以自己实现一个插件的注册方案,统一封装一下插件的注册形式,用来拓展slate
。在这里插件注册时通过slate-plugins.tsx
来实现,具体来说,每个插件都是一个必须返回一个Plugin
类型的函数,当然直接定义一个对象也是没问题的,函数的好处是可以在注册的时候传递参数,所以一般都是直接用函数定义的。
key
: 表示该插件的名字,一般不能够重复。priority
: 表示插件执行的优先级,通常用户需要包裹renderLine
的组件。command
: 注册该插件的命令,工具栏点击或者按下快捷键需要执行的函数。onKeyDown
: 键盘事件的处理函数,可以用他来制定回车或者删除等操作的具体行为等。type
: 标记其是block
或者是inline
。match
: 只有返回true
即匹配到的插件才会执行。renderLine
: 用于block
的组件,通常用作在其子元素上包裹一层组件。render
: 对于block
组件具体渲染的组件由该函数决定,对于inline
组件则与block
的renderLine
表现相同。
type BasePlugin = {
key: string;
priority?: number; // 优先级越高 在越外层
command?: CommandFn;
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => boolean | void;
};
type ElementPlugin = BasePlugin & {
type: typeof EDITOR_ELEMENT_TYPE.BLOCK;
match: (props: RenderElementProps) => boolean;
renderLine?: (context: ElementContext) => JSX.Element;
render?: (context: ElementContext) => JSX.Element;
};
type LeafPlugin = BasePlugin & {
type: typeof EDITOR_ELEMENT_TYPE.INLINE;
match: (props: RenderLeafProps) => boolean;
render?: (context: LeafContext) => JSX.Element;
};
在具体的实现上,我们采用了实例化类的方式,当实例化之后我们可以不断add
插件,因为toolbar
等插件是负责执行命令的,所以需要首先获取前边注册完成的插件的命令,将其传入后再注册到插件当中,通过这种注册的机制实现了统一的插件管理,在apply
之后,我们可以将返回的值传入到<Editable />
中,就可以将插件正常的拓展到slate
当中了。
const { renderElement, renderLeaf, onKeyDown, withVoidElements, commands } = useMemo(() => {
const register = new SlatePlugins(
ParagraphPlugin(),
HeadingPlugin(editor),
BoldPlugin(),
QuoteBlockPlugin(editor),
// ...
);
const commands = register.getCommands();
register.add(
DocToolBarPlugin(editor, props.isRender, commands),
// ...
);
return register.apply();
}, [editor, props.isRender]);
类型拓展
在slate
中预留了比较好的类型拓展机制,可以通过TypeScript
中的declare module
配合interface
来拓展BlockElement
与TextElement
的类型,使实现插件的attributes
有较为严格的类型校验。
// base
export type BaseNode = BlockElement | TextElement;
declare module "slate" {
interface BlockElement {
children: BaseNode[];
[key: string]: unknown;
}
interface TextElement {
text: string;
[key: string]: unknown;
}
interface CustomTypes {
Editor: BaseEditor & ReactEditor;
Element: BlockElement;
Text: TextElement;
}
}
// plugin
declare module "slate" {
interface BlockElement {
type?: { a: string; b: boolean };
}
interface TextElement {
type?: boolean;
}
}
实现方案
在这里是具体的插件实现方案与示例,每个部分都是一种类型的插件的实现,具体的代码都可以在 Github 中找到。在插件实现方面,整体还是借助了HTML5
的标签来完成各种样式,这样能够保持文档的标签语义完整性但是会造成DOM
结构嵌套比较深。使用纯CSS
来完成各种插件也是没问题的,而且实现上是更简单一些的,context
提供classList
来操作className
,只不过纯CSS
实现样式的话标签语义完整性就欠缺一些。这方面主要是个取舍问题,在此处实现的插件都是借助HTML5
的标签以及一些自定义的交互策略来完成的,交互的执行上都是通过插件注册命令后触发实现的。
Leaf
leaf
类型的插件是行内的元素,例如加粗、斜体、下划线、删除线等等,在实现上只需要注意插件的命令注册与在该命令下如何渲染元素即可,下面是bold
插件的实现,主要是注册了操作attributes
的命令,以及使用<strong />
作为渲染格式的标签。
declare module "slate" {
interface TextElement {
bold?: boolean;
}
}
export const boldPluginKey = "bold";
export const BoldPlugin = (): Plugin => {
return {
key: boldPluginKey,
type: EDITOR_ELEMENT_TYPE.INLINE,
match: props => !!props.leaf[boldPluginKey],
command: (editor, key) => {
Transforms.setNodes(
editor,
{ [key]: true },
{ match: node => Text.isText(node), split: true }
);
},
render: context => <strong>{context.children}</strong>,
};
};
Element
element
类型的插件是属于块级元素,例如标题、段落、对齐等等,简单来说是作用在行上的元素,在实现上不光要注意命令的注册和渲染元素,还有注意各种case
,尤其是在wrapper
嵌套下的情况。在下面的heading
示例中,在命令阶段处理了是否已经处于heading
状态,如果处于改状态那就取消heading
,生成的id
是为了之后作为锚点使用,在处理键盘事件的时候,就需要处理一些case
,在这里实现了我们回车的时候不希望在下一行继承heading
格式,以及当光标置于行最前点击删除则会删除该行标题格式。
declare module "slate" {
interface BlockElement {
heading?: { id: string; type: string };
}
}
export const headingPluginKey = "heading";
const headingCommand: CommandFn = (editor, key, data) => {
if (isObject(data) && data.path) {
if (!isMatchedAttributeNode(editor, `${headingPluginKey}.type`, data.extraKey)) {
setBlockNode(editor, { [key]: { type: data.extraKey, id: uuid().slice(0, 8) } }, data.path);
} else {
setBlockNode(editor, getOmitAttributes([headingPluginKey]), data.path);
}
}
};
export const HeadingPlugin = (editor: Editor): Plugin => {
return {
key: headingPluginKey,
type: EDITOR_ELEMENT_TYPE.BLOCK,
command: headingCommand,
match: props => !!props.element[headingPluginKey],
renderLine: context => {
const heading = context.props.element[headingPluginKey];
if (!heading) return context.children;
const id = heading.id;
switch (heading.type) {
case "h1":
return (
<h1 className="doc-heading" id={id}>
{context.children}
</h1>
);
case "h2":
return (
<h2 className="doc-heading" id={id}>
{context.children}
</h2>
);
case "h3":
return (
<h3 className="doc-heading" id={id}>
{context.children}
</h3>
);
default:
return context.children;
}
},
onKeyDown: event => {
if (
isMatchedEvent(event, KEYBOARD.BACKSPACE, KEYBOARD.ENTER) &&
isCollapsed(editor, editor.selection)
) {
const match = getBlockNode(editor, editor.selection);
if (match) {
const { block, path } = match;
if (!block[headingPluginKey]) return void 0;
if (isSlateElement(block)) {
if (event.key === KEYBOARD.BACKSPACE && isFocusLineStart(editor, path)) {
const properties = getOmitAttributes([headingPluginKey]);
Transforms.setNodes(editor, properties, { at: path });
event.preventDefault();
}
if (event.key === KEYBOARD.ENTER && isFocusLineEnd(editor, path)) {
const attributes = getBlockAttributes(block, [headingPluginKey]);
if (isWrappedNode(editor)) {
// 在`wrap`的情况下插入节点会出现问题 先多插入一个空格再删除
Transforms.insertNodes(
editor,
{ ...attributes, children: [{ text: " " }] },
{ at: editor.selection.focus, select: false }
);
Transforms.move(editor, { distance: 1 });
Promise.resolve().then(() => editor.deleteForward("character"));
} else {
Transforms.insertNodes(editor, { ...attributes, children: [{ text: "" }] });
}
event.preventDefault();
}
}
}
}
},
};
};
Wrapper
wrapper
类型的插件同样也是属于块级元素,例如引用块、有序列表、无序列表等,简单来说是在行上额外嵌套了一行,所以在实现上不光要注意命令的注册和渲染元素,还有注意各种case
,在wrapper
下需要注意的case
就特别多,所以我们也需要自己实现一些策略来避免这些问题。在下面的quote-block
示例中,实现了支持一级块引用,回车会继承格式,作为wrapped
插件不能与其他wrapped
插件并行使用,行空且该行为wrapped
首行或尾行时回车和删除会取消该行块引用格式,光标置于行最前点击删除且该行为wrapped
首行或尾行时则会取消该行块引用格式。
declare module "slate" {
interface BlockElement {
"quote-block"?: boolean;
"quote-block-item"?: boolean;
}
}
export const quoteBlockKey = "quote-block";
export const quoteBlockItemKey = "quote-block-item";
const quoteCommand: CommandFn = (editor, key, data) => {
if (isObject(data) && data.path) {
if (!isMatchedAttributeNode(editor, quoteBlockKey, true, data.path)) {
if (!isWrappedNode(editor)) {
setWrapNodes(editor, { [key]: true }, data.path);
setBlockNode(editor, { [quoteBlockItemKey]: true });
}
} else {
setUnWrapNodes(editor, quoteBlockKey);
setBlockNode(editor, getOmitAttributes([quoteBlockItemKey, quoteBlockKey]));
}
}
};
export const QuoteBlockPlugin = (editor: Editor): Plugin => {
return {
key: quoteBlockKey,
type: EDITOR_ELEMENT_TYPE.BLOCK,
match: props => !!props.element[quoteBlockKey],
renderLine: context => (
<blockquote className="slate-quote-block">{context.children}</blockquote>
),
command: quoteCommand,
onKeyDown: event => {
if (
isMatchedEvent(event, KEYBOARD.BACKSPACE, KEYBOARD.ENTER) &&
isCollapsed(editor, editor.selection)
) {
const quoteMatch = getBlockNode(editor, editor.selection, quoteBlockKey);
const quoteItemMatch = getBlockNode(editor, editor.selection, quoteBlockItemKey);
if (quoteMatch && !quoteItemMatch) setUnWrapNodes(editor, quoteBlockKey);
if (!quoteMatch && quoteItemMatch) {
setBlockNode(editor, getOmitAttributes([quoteBlockItemKey]));
}
if (!quoteMatch || !quoteItemMatch) return void 0;
if (isFocusLineStart(editor, quoteItemMatch.path)) {
if (
!isWrappedEdgeNode(editor, editor.selection, quoteBlockKey, quoteBlockItemKey, "or")
) {
if (isMatchedEvent(event, KEYBOARD.BACKSPACE)) {
editor.deleteBackward("block");
event.preventDefault();
}
} else {
setUnWrapNodes(editor, quoteBlockKey);
setBlockNode(editor, getOmitAttributes([quoteBlockItemKey, quoteBlockKey]));
event.preventDefault();
}
}
}
},
};
};
Void
void
类型的插件同样也是属于块级元素,例如分割线、图片、视频等,void
元素应该是一个空元素,他会有一个空的用于渲染的文本子节点,并且是不可编辑的,所以是一类单独的节点类型。在下面的dividing-line
示例中,主要需要注意分割线的选中以及void
节点的定义。
declare module "slate" {
interface BlockElement {
"dividing-line"?: boolean;
}
}
export const dividingLineKey = "dividing-line";
const DividingLine: React.FC = () => {
const selected = useSelected();
const focused = useFocused();
return <div className={cs("dividing-line", focused && selected && "selected")}></div>;
};
export const DividingLinePlugin = (): Plugin => {
return {
key: dividingLineKey,
isVoid: true,
type: EDITOR_ELEMENT_TYPE.BLOCK,
command: (editor, key) => {
Transforms.insertNodes(editor, { [key]: true, children: [{ text: "" }] });
Transforms.insertNodes(editor, { children: [{ text: "" }] });
},
match: props => existKey(props.element, dividingLineKey),
render: () => <DividingLine></DividingLine>,
};
};
Toolbar
toolbar
类型的插件是属于自定义的一类单独的插件,主要是用于执行命令,因为我们在插件定义的时候注册了命令,那么也就意味着我们完全可以通过命令来驱动节点的变化,toolbar
就是用于执行命令的插件。在下面的doc-toolbar
示例中,我们可以看到如何实现左侧的悬浮菜单以及命令的执行等。
const DocMenu: React.FC<{
editor: Editor;
element: RenderElementProps["element"];
commands: SlateCommands;
}> = props => {
const [visible, setVisible] = useState(false);
const affixStyles = (param: string) => {
setVisible(false);
const [key, data] = param.split(".");
const path = ReactEditor.findPath(props.editor, props.element);
focusSelection(props.editor, path);
execCommand(props.editor, props.commands, key, { extraKey: data, path });
};
const MenuPopup = (
<Menu onClickMenuItem={affixStyles} className="doc-menu-popup">
<Menu.Item key="heading.h1">
<IconH1 />
一级标题
</Menu.Item>
<Menu.Item key="heading.h2">
<IconH2 />
二级标题
</Menu.Item>
<Menu.Item key="heading.h3">
<IconH3 />
三级标题
</Menu.Item>
<Menu.Item key="quote-block">
<IconQuote />
块级引用
</Menu.Item>
<Menu.Item key="ordered-list">
<IconOrderedList />
有序列表
</Menu.Item>
<Menu.Item key="unordered-list">
<IconUnorderedList />
无序列表
</Menu.Item>
<Menu.Item key="dividing-line">
<IconEdit />
分割线
</Menu.Item>
</Menu>
);
return (
<Trigger
popup={() => MenuPopup}
position="bottom"
popupVisible={visible}
onVisibleChange={setVisible}
>
<span
className="doc-icon-plus"
onMouseDown={e => e.preventDefault()} // prevent toolbar from taking focus away from editor
>
<IconPlusCircle />
</span>
</Trigger>
);
};
const NO_DOC_TOOL_BAR = ["quote-block", "ordered-list", "unordered-list", "dividing-line"];
const OFFSET_MAP: Record<string, number> = {
"quote-block-item": 12,
};
export const DocToolBarPlugin = (
editor: Editor,
isRender: boolean,
commands: SlateCommands
): Plugin => {
return {
key: "doc-toolbar",
priority: 13,
type: EDITOR_ELEMENT_TYPE.BLOCK,
match: () => true,
renderLine: context => {
if (isRender) return context.children;
for (const item of NO_DOC_TOOL_BAR) {
if (context.element[item]) return context.children;
}
let offset = 0;
for (const item of Object.keys(OFFSET_MAP)) {
if (context.element[item]) {
offset = OFFSET_MAP[item] || 0;
break;
}
}
return (
<Trigger
popup={() => <DocMenu editor={editor} commands={commands} element={context.element} />}
position="left"
popupAlign={{ left: offset }}
mouseLeaveDelay={200}
mouseEnterDelay={200}
>
<div>{context.children}</div>
</Trigger>
);
},
};
};
Shortcut
shortcut
类型的插件是属于自定义的一类单独的插件,同样也是用于快捷键执行命令,这也是使用命令驱动的一种实现。在下面的shortcut
示例中,我们可以看到如何处理快捷键的输入以及命令的执行等。
const SHORTCUTS: Record<string, string> = {
"1.": "ordered-list",
"-": "unordered-list",
"*": "unordered-list",
">": "quote-block",
"#": "heading.h1",
"##": "heading.h2",
"###": "heading.h3",
"---": "dividing-line",
};
export const ShortCutPlugin = (editor: Editor, commands: SlateCommands): Plugin => {
return {
key: "shortcut",
type: EDITOR_ELEMENT_TYPE.BLOCK,
match: () => false,
onKeyDown: event => {
if (isMatchedEvent(event, KEYBOARD.SPACE) && isCollapsed(editor, editor.selection)) {
const match = getBlockNode(editor);
if (match) {
const { anchor } = editor.selection;
const { path } = match;
const start = Editor.start(editor, path);
const range = { anchor, focus: start };
const beforeText = Editor.string(editor, range);
const param = SHORTCUTS[beforeText.trim()];
if (param) {
Transforms.select(editor, range);
Transforms.delete(editor);
const [key, data] = param.split(".");
execCommand(editor, commands, key, { extraKey: data, path });
event.preventDefault();
}
}
}
},
};
};
每日一题
https://github.com/WindrunnerMax/EveryDay
参考
https://docs.slatejs.org/
https://github.com/ianstormtaylor/slate
https://www.slatejs.org/examples/richtext
http://t.zoukankan.com/kagol-p-14820617.html
https://rain120.github.io/athena/zh/slate/Introduction.html
https://www.wangeditor.com/v5/#%E6%8A%80%E6%9C%AF%E8%80%81%E6%97%A7
基于slate构建文档编辑器的更多相关文章
- PowerDesigner(九)-模型文档编辑器(生成项目文档)(转)
模型文档编辑器 PowerDesigner的模型文档(Model Report)是基于模型的,面向项目的概览文档,提供了灵活,丰富的模型文档编辑界面,实现了设计,修改和输出模型文档的全过程. 模型文 ...
- [Qt及Qt Quick开发实战精解] 第1章 多文档编辑器
这一章的例子是对<Qt Creator快速人门>基础应用篇各章节知识的综合应用, 也是一个规范的实例程序.之所以说其规范,是因为在这个程序中,我们对菜单什么时候可用/什么时候不可用.关 ...
- Linux_文档编辑器_简介
1. vi 2. vim 3. ubuntu 有一个 自己的图形化的 文档编辑器,用起来比较方便: gedit 4. 5.
- Web页面引入文档编辑器报风险
Web页面引入文档编辑器会报风险,则需要以下操作: <system.web> <httpRuntime requestValidationMode="2.0" / ...
- 使用Swing实现简易而不简单的文档编辑器
本文通过Swing来实现文档简易而不简单的文档编辑器,该文档编辑器的功能包括: 设置字体样式:粗体,斜体,下划线,可扩展 设置字体:宋体,黑体,可扩展 设置字号:12,14,18,20,30,40, ...
- 如何利用启明星Portal门户系统的Page模块构建文档库
利用启明星门户系统的Page模块构架可以搭建企业内部的文档管理系统. (一)应用背景 企业内部通常都会使用共享网盘的方式来存放不同部门之间的文档,例如管理员在服务器上对人事部门增加人事部文档文件夹. ...
- 在线HTML文档编辑器使用入门之图片上传与图片管理的实现
在线HTML文档编辑器使用入门之图片上传与图片管理的实现: 官方网址: http://kindeditor.net/demo.php 开发步骤: 1.开发中只需要导入选中的文件(通常在 webapp ...
- 基于word2vec的文档向量模型的应用
基于word2vec的文档向量模型的应用 word2vec的原理以及训练过程具体细节就不介绍了,推荐两篇文档:<word2vec parameter learning explained> ...
- 多线程串口编程工具CserialPort类(附VC基于MFC单文档协议通讯源程序及详细编程步骤)
老有人觉得MSComm通讯控件很土,更有人大声疾呼:忘了它吧.确实当我们对串口编程有了一定的了解后,应该用API函数写一个属于自己的串口程序,由于编程者对程序了解,对程序修改自如.但我一直没有停止过用 ...
随机推荐
- 如丝般顺滑:DDD再实践之类目树管理
在上次反思DDD实践之后,在类目树管理项目中再次实践DDD.从需求分析到建模和具体的落地,结合个人体会,都是干货.
- Java学习day6
今天跟着教学视频做了个简易的学生管理系统 在编写完全部代码之后出现了在空白处右键没有run as选项的问题,通过csdn与博客园上的多个帖子介绍,得知是jdk配置不对,正确配置后问题得到解决 明天学习 ...
- JavaWeb学习day1-web入门&服务器安装
初步了解了web的发展历程,对比分析学习了动态页面与静态页面的优缺点. 选择安装使用最广泛的Tomcat 安装流程: 1.打开官网:https://tomcat.apache.org/ 2.选择下载版 ...
- .NET MAUI发布了期待已久的候选版本(RC1)
作者:David Ortinau 我们激动地宣布在4/13/2022.NET多平台应用UI (.NET MAUI)发布了候选版本.SDK现在已经集成好了API,可以更新库,并为GA(通用可用性)兼容性 ...
- 面试突击42:synchronized和ReentrantLock有什么区别?
在 Java 中,常用的锁有两种:synchronized(内置锁)和 ReentrantLock(可重入锁),二者的功效都是相同得,但又有很多不同点,所以我们今天就来聊聊. 区别1:用法不同 syn ...
- 程序员延寿指南「GitHub 热点速览 v.22.17」
很多人对程序员的固有印象之一便是常加班.易"猝死"!近几年的许多报道似乎也进一步加深了这种印象.应该如何更好地健康地活着.敲喜欢的代码呢?HowToLiveLonger 教你如何从 ...
- [AcWing 776] 字符串移位包含问题
点击查看代码 #include<iostream> #include<algorithm> using namespace std; string a, b; int main ...
- mysql内连接查询之自连接
连接查询: 当查询数据时,通过连接操作查询出存放在多个表中的不同数据,当两个或者多个表中存在相同意义的字段时, 便可以通过这些字段对不同的表进行连接查询. 自连接: 如果在一个连接查询中,涉及的两个表 ...
- Linux启动故障排查和修复技巧
一个执着于技术的公众号 我发现Linux系统在启动过程中会出现一些故障,导致系统无法正常启动,我在这里写了几个应对单用户模式.GRUB命令操作.Linux救援模式的故障修复案例帮助大家了解此类问题的解 ...
- 8 种常见 SQL 错误用法
点击上方"开源Linux",选择"设为星标"回复"学习"获取独家整理的学习资料! 1.LIMIT 语句 分页查询是最常用的场景之一,但也通常 ...