网站建设的几个阶段,个人网页源码,网上购物系统软件开发,湖南网站建设大全前言#xff1a;上一篇文章我主要分享了从 Monaco Editor 入口文件以及官方提供的示例项目入手#xff0c;对一部分源码进行剖析#xff0c;以及分享了初始化阶段代码的大致执行步骤#xff0c;这一篇了来讲一下我们要用 Monaco Editor 的时候该怎么用。其中会涉及到一些 A…前言上一篇文章我主要分享了从 Monaco Editor 入口文件以及官方提供的示例项目入手对一部分源码进行剖析以及分享了初始化阶段代码的大致执行步骤这一篇了来讲一下我们要用 Monaco Editor 的时候该怎么用。其中会涉及到一些 API我会对 API 的源码进行深入的解析但不保证能完全看懂 。这种复杂的源码不要着急实在深入不下去就换一个入口继续探究最后你学到的东西会呈网状交织在一起覆盖到所有的代码。
一、创建新的路由页面
咱们还是基于之前的项目来做在下面这个位置放置咱们自己的页面 主要就是仿照前几个路由的配置。我也没有学过react但是没关系学习一样东西最快的方式就是模仿
一创建路由
website/src/website/pages/routes.ts 在路由文件的最后一行仿照上面创建路由的形式新增一个路由
export const study new Route(./study.html);什么找不到 ./monarch.html等页面文件? 其实这个路径是通过 webpack 的插件机制生成滴上一篇文章也有详细的解释哦
二webpack 配置
在 website/webpack.config.ts 文件的 plugins 配置中增加一项配置意思就是应用 index chunk 生成一个 study.html 页面模版就使用 getHtml() 作为页面模版。仔细观察可以发现其他的路由页面也都是这么配置的。
new HtmlWebpackPlugin({chunks: [index],filename: study.html,templateContent: getHtml(),
}),三路由注册和配置
website/src/website/pages/App.tsx 在应用文件中需要引入路由并且注册路由以及定义路由和页面文件之间的对应关系。就仿照其他的路由页面写就行了
//...
// 引入 路由
import { docs, home, monarch, playground, study } from ./routes;
//...
// 引入 Study
import { StudyPage } from ./StudyPage;export class App extends React.Component {// 根据路由返回指定的组件render() {if (home.isActive) {return Home /;} else if (playground.isActive) {return PlaygroundPage /;} else if (docs.isActive) {return DocsPage /;} else if (monarch.isActive) {return MonarchPage /;} else if (study.isActive) {return StudyPage /}return Page does not exist/;}
}四创建路由页面
上面我们定义了study路由对应的是 ./StudyPage 页面我们需要创建一个新的文件里面写的简单一点直接渲染一个 div 先创建文件的目录也仿照其他页面就行 website/src/website/pages/StudyPage.tsx
import React require(react);
export class StudyPage extends React.Component{}, {} {render() {return (div我是Study/div);}
}五路由链接
路由链接定义的位置可以全局搜索这个类名找哦一下子就找到了 website/src/website/components/Nav.tsx
Navbar.Collapse idbasic-navbar-nav role!--省略一万字--Nav.Link active{study.isActive} href{study.href}Study/Nav.Link
/Nav接下我们去页面看看效果吧 点击路由就跳往 Study 页面啦 咦这里为什么和别人不一样呢是因为人家有用 Page自定义组件啦
六使用 Page 自定义组件
import React require(react);
import { Page } from ../components/Page;
export class StudyPage extends React.Component{}, {} {render() {return (Pagediv我是Study/div/Page);}
}这样就哦了保留了公共的页头
react 生命周期
先拐个弯儿加深一下基础
执行阶段函数名称执行时机创建阶段constructor初始化state中的数据 可以为事件绑定this创建阶段render每次组件渲染(初次渲染组件和更新组件)都会被触发作用是渲染UI; 注意不能够调用 setState因为setState会更新数据这样会导致递归渲染数据创建阶段componentDidMountDOM已经渲染完成了可以进行DOM操作和网络请求更新阶段render有三种情况会导致组件的更新-触发render函数① 组件接收到一个新的属性② 调用setState()③ 调用forceUpdate()方法更新阶段componentDidUpdate当组件中的数据更新完成后会触发卸载阶段componentWillUnmount组件将要卸载的时候会被触发可以做清除定时器。
二、创建 Monaco Editor
咱们先看一下 Monaco 这个路由这个路由下面其实就是有两个 Monaco 编辑器实例那么我们就先看一下这个页面是怎么创建 Monaco Editor 实例的 我们一起去往项目代码中这个路由对应的组件website/src/website/pages/MonarchPage.tsx 可以看到 Monaco Editor 其实是在 iframe 里面
PageiframeframeBorder{0}classNamefull-iframesrc./monarch-static.html/
/Page我们使用搜索路径的方式搜索 monaco-staticidea中的快捷键是 【shiftshift】就可以找到这个文件的定义位置 website/static/monarch-static.html 这里我们可以发现这个文件的路径也是经过处理的并不是真的在 ./ 目录下其实这也是 webpack 处理的
new CopyPlugin({patterns: [{from: ./static, to: ./}],
}),CopyPlugin 插件用于将文件或目录从源位置复制到构建目录中这样就可以通过 ./ 获取文件了 那么我们仿照这种创建文件的方式也新建一个 StudyPage 使用的 iframe 其实 monarch-static.html 中就包含了我们需要的内容咱们只需要其中的创建新建 Monaco Editor 的部分写一个最简单的示例为了更好的模块化把 js 仿照 monarch 放到另外的目录里面 website/static/study-static.html
!DOCTYPE html
htmlheadtitleHello World Monaco Editor/titlemeta http-equivContent-Type contenttext/html;charsetutf-8 /
/headbody
h2Hello World Monaco Editor/h2
div idcontainer stylewidth: 800px; height: 600px; border: 1px solid grey
/div
scriptsrchttps://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.jsintegritysha256-wS9gmOZBqsqWxgIVgA8Y9WcQOa7PgSIXrPA0VL2rbQcrossoriginanonymous
/script
scriptsrchttps://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.0/bootstrap.min.jsintegritysha256-ul2mGjpmGK/mFgUncmMcFKdMijvVJ3odlDJZSNUu8crossoriginanonymous
/scriptscriptvar require {paths: { vs: ./node_modules/monaco-editor/dev/vs },};
/script
script src./node_modules/monaco-editor/dev/vs/loader.js/script
script src./node_modules/monaco-editor/dev/vs/editor/editor.main.nls.js/script
script src./node_modules/monaco-editor/dev/vs/editor/editor.main.js/scriptscript data-inlineyes-please src./study/study.js/script
/body
/html
website/static/study/study.js
require([vs/editor/editor.main], function () {var editor monaco.editor.create(document.getElementById(container), {value: [function x() {, \tconsole.log(Hello world!);, }].join(\n),language: javascript});
});最后别忘了把 StudyPage.tsx 中iframe的src改为 ./study-static.html 历经千辛万苦我们终于创建出来了一个自己的 Monaco Editor 实例亲爱的们先去跳个舞奖励一下自己吧
三、monaco.editor.create
创建 Monaco Editor 的方法就是 monaco.editor.create()咱们可以打印一下 monaco.editor总之里面各种方法属性截图都放不下其中这几个就是跟创建编辑器相关的 通过打断点的方式我们可以看到 create() 方法执行的地方 根据我红圈圈 圈出的地方进行全局搜索。如果你的源码项目使用了 git 管理你可能会和我一样找不到费劲九牛二虎之力终于找到了这个类的定义原来是在 git 忽视的文件夹中所以搜不到 就在这个文件里面monaco-editor/out/monaco-editor/esm/vs/platform/instantiation/common/instantiationService.js 记得在 .gitignore 文件中把 out 文件夹注释掉哦 然后搜索 const instantiationService StandaloneServices.initialize(override || {}); 这句代码 在 out/monaco-editor/esm/vs/editor/standalone/browser/standaloneEditor.js 这个文件里面 这个 out 文件夹是咋生成的这就说来话长了其实是我们执行最开始最开始的 npm run build-monaco-editor 来生成本地的项目的时候生成的这个过程我会在第四章讲到哦。那么咱们现在主要来分析代码看看 create() 方法究竟是如何创建 Monaco Editor 的 out/monaco-editor/esm/vs/editor/standalone/browser/standaloneEditor.js
export function create(domElement, options, override) {// 初始化StandaloneServicesconst instantiationService StandaloneServices.initialize(override || {});// StandaloneEditor类// 内部就是根据 StandaloneEditor 创建一个实例return instantiationService.createInstance(StandaloneEditor, domElement, options);
}instantiationService.createInstance() 方法里面其实挺复杂的但是在这里就不发散太多了发散太多就忘记最初的目的了。作用其实就是创建 StandaloneEditor 类的实例 然后我们来看一下 StandaloneEditor 类的定义吧这个类里面的代码其实并不多因为它是继承别的类的并且有好几层继承我浅浅的分析了一下 constructor 方法的执行但是说实话实在是太复杂了实例化过程其实还是通过 super() 交给父级实现的。但是到这儿我觉得没有继续深入看的必要了因为成本有点高很难理解并且对编辑器的功能不了解看源码也理解不了它到底在干啥。所以咱们先知道方法在哪里定义的然后呢继续探索其他功能吧 out/monaco-editor/esm/vs/editor/standalone/browser/standaloneCodeEditor.js
constructor(domElement, _options, instantiationService, codeEditorService, commandService, contextKeyService, keybindingService, themeService, notificationService, configurationService, accessibilityService, modelService, languageService, languageConfigurationService, languageFeaturesService) {// 拷贝 optionsconst options { ..._options };// 更新配置服务的配置updateConfigurationService(configurationService, options, false);// 将 domElement 注册为编辑器容器const themeDomRegistration themeService.registerEditorContainer(domElement);if (typeof options.theme string) {// 设置主题themeService.setTheme(options.theme);}if (typeof options.autoDetectHighContrast ! undefined) {// 是否自动检测高对比度主题themeService.setAutoDetectHighContrast(Boolean(options.autoDetectHighContrast));}const _model options.model;delete options.model;// 使用 super 来调用父类的构造函数将实例化过程委托给父类构造函数完成super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, keybindingService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService);this._configurationService configurationService;this._standaloneThemeService themeService;this._register(themeDomRegistration);let model;if (typeof _model undefined) {// 获取语言标识符如果没有语言标识符就标记为纯文本const languageId languageService.getLanguageIdByMimeType(options.language) || options.language || PLAINTEXT_LANGUAGE_ID;// 创建文本模型model createTextModel(modelService, languageService, options.value || , languageId, undefined);// 表明编辑器实例已经拥有这个模型this._ownsModel true;}else {// 使用给定的模型model _model;this._ownsModel false;}// 将model附加到编辑器上this._attachModel(model);if (model) {const e {oldModelUrl: null,newModelUrl: model.uri};this._onDidChangeModel.fire(e);}}四、项目构建过程
我们下载完 Monaco Editor 项目之后第一步是运行 npm i 安装依赖第二步是运行 npm run build-monaco-editor 生成本地的项目这个命令的定义在Monaco的根目录的 package.json 中
build: ts-node ./build/build-languages,
build-monaco-editor: npm run build ts-node ./build/build-monaco-editor可以看到这一句 npm run build-monaco-editor 命令其实背后执行了两个步骤一个是 npm run build另一个是 ts-node ./build/build-monaco-editor而 npm run build 实际执行的就是上一句指定的 ts-node ./build/build-languages ts-node 命令就是用来运行后面紧跟着的 ts 文件的。那么我们分别来看一下这两个 ts 文件里面都干了什么吧
1、./build/build-languages
这个文件里面的代码总的来说就是重新构建 out/languages 目录
① 删除 out/languages 目录
import { copyFile, removeDir } from ./fs;
removeDir(out/languages);这里的 fs 并不是Node.js提供的 fs 模块而是二次封装的 fs 模块。这个方法的定义我们可以点进去瞅瞅。其中我已经注释好了就是递归删除所有的子文件然后删除文件夹 build/fs.ts
export function removeDir(_dirPath: string, keep?: (filename: string) boolean) {if (typeof keep undefined) {keep () false;}const dirPath path.join(REPO_ROOT, _dirPath);// fs.existsSync检查路径是否存在if (!fs.existsSync(dirPath)) {return;}rmDir(dirPath, _dirPath);console.log(Deleted ${_dirPath});function rmDir(dirPath: string, relativeDirPath: string): boolean {let keepsFiles false;// readdirSync 是 Node.js 中 fs 模块的一个方法用于同步地读取指定目录下的文件和子目录。const entries fs.readdirSync(dirPath);for (const entry of entries) {const filePath path.join(dirPath, entry);const relativeFilePath path.join(relativeDirPath, entry);// !是非空断言此处keep不可能为空if (keep!(relativeFilePath)) {// 如果调用方法的时候传进来了keep函数那么就可以// 通过keep函数设置哪些文件保留// 如果不传递keep函数则删除所有文件keepsFiles true;continue;}// fs.statSync 获取指定路径的文件状态信息// isFile() 就是判断目标是不是文件if (fs.statSync(filePath).isFile()) {// 删除指定文件fs.unlinkSync(filePath);} else {// 递归删除子文件keepsFiles rmDir(filePath, relativeFilePath) || keepsFiles;}}if (!keepsFiles) {// 如果子文件都被删除了就删除文件夹fs.rmdirSync(dirPath);}return keepsFiles;}
}② 生成 out/languages/amd-tsc 文件夹
这个文件夹中的代码存放的是我们在编写不同编程语言的时候的一些关键词和格式化规则等 就是这条代码的工作啦 runTsc(src/tsconfig.json); 我们来看一下具体的执行 build/utils.ts
export function runTsc(_projectPath: string) {const projectPath path.join(REPO_ROOT, _projectPath);console.log(Launching compiler at ${_projectPath}...);// 1、process.execPath 是 Node.js 中的一个属性它返回启动当前 Node.js 进程的可执行文件的绝对路径。// 类似于/usr/local/bin/node// 第二个参数是传递给 /usr/local/bin/node 命令的参数// 也就是说使用 node XX 命令运行 XX 文件// 2、../node_modules/typescript/lib/tsc.js 是 TypeScript 的官方命令行工具 tsc 的入口点// 用于将 TypeScript 代码编译为 JavaScript 代码。// -p 路径 是给tsc的参数指定 TypeScript 项目的配置文件路径// 3、stdio 配置选项设置为 inherit子进程继承了父进程的标准输入输出。// 总的来说就是执行命令 node tsc -p ../src/tsconfig.jsonconst res cp.spawnSync(process.execPath,[path.join(__dirname, ../node_modules/typescript/lib/tsc.js), -p, projectPath],{ stdio: inherit });console.log(Compiled ${_projectPath});if (res.status ! 0) {process.exit(res.status);}
}
主要是将一个配置文件传递给了 tsc 工具执行 tsc 命令。我们具体来看一下配置文件的内容吧
{compilerOptions: {// 表示生成对应的 .d.ts 类型声明文件declaration: true,// lib 选项指定要包含的类型声明文件列表以便正确地进行类型检查和类型推断。// dom包含了 DOM 相关的类型信息用于在 TypeScript 代码中进行浏览器 DOM 操作的类型检查。// es5包含了 ES5 标准库的类型信息用于在 TypeScript 代码中使用 ES5 标准库的类型检查。// es2015.collection包含了 ES2015 集合类型的类型信息如 Map、Set 等。// es2015.promise包含了 ES2015 Promise 类型的类型信息用于在 TypeScript 代码中进行 Promise 相关操作的类型检查。// es2015.iterable包含了 ES2015 迭代器类型的类型信息用于在 TypeScript 代码中进行迭代操作的类型检查。lib: [dom,es5,es2015.collection,es2015.promise,es2015.iterable],// 指定要使用的模块系统module: amd,// 指定模块解析策略moduleResolution: node,// 指定编译输出的目录outDir: ../out/languages/amd-tsc,// 启用严格的类型检查和更严格的编译选项strict: true,// 指定编译后的 JavaScript 代码的目标 ECMAScript 版本target: es5,// 表示生成源映射文件.mapsourceMap: true,// 表示允许编译器编译 JavaScript 文件.jsallowJs: true,// 表示禁用对 JavaScript 文件的类型检查checkJs: false}tsc编译会自动对配置文件同级的文件夹进行编译。由上述配置项可以得知使用 tsc 编译之后就会生成 out/languages/amd-tsc 目录。其实就是 src 目录下的这几个文件夹编译后放到了 out/amd-tsc 文件夹中 只不过编译后的文件是有好几个版本的
③ 生成 out/languages/bundled 文件夹’
这个文件夹里面的代码是用来定义 html、css、js、json、typescript 这几个语言的格式化以及提示规则啊等等 主要靠 buildESM() 这个方法。这个方法执行了好几次咱们就看一下第一个执行的过程
buildESM({base: language/typescript,entryPoints: [src/language/typescript/monaco.contribution.ts,src/language/typescript/tsMode.ts,src/language/typescript/ts.worker.ts],external: [monaco-editor-core, */tsMode, */monaco.contribution]
});方法的定义在 build/utils.ts 文件中
export function build(options: import(esbuild).BuildOptions) {// esbuild.build 是 esbuild 构建工具提供的一个方法用于构建 JavaScript 或 TypeScript// 项目。// esbuild 是一个快速的、低配置的 JavaScript/TypeScript 构建工具// 旨在提供高性能的构建和打包功能。它能够将源代码转换为浏览器可执行的 JavaScript// 同时支持代码压缩和优化等功能。// esbuild.build 方法用于配置和执行构建过程。它接受一个配置对象作为参数// 该对象描述了构建的输入和输出等信息。esbuild.build(options).then((result) {if (result.errors.length 0) {console.error(result.errors);}if (result.warnings.length 0) {console.error(result.warnings);}});
}export function buildESM(options: { base: string; entryPoints: string[]; external: string[] }) {build({entryPoints: options.entryPoints, // 构建的入口文件路径bundle: true, // 是否将所有模块打包到一个输出文件中target: esnext, // 构建的目标 JavaScript 版本表示目标是 ESNext 版本format: esm, // 输出文件的模块格式,表示输出文件采用 ES 模块的格式drop: [debugger], // 指定需要从输出文件中删除的代码或语句表示删除所有 debugger 语句define: { // 定义全局常量AMD: false},banner: { // 用于在输出文件的开头插入注释、版权声明或其他自定义信息以标识生成的文件或提供额外的说明js: bundledFileHeader // 用于 JavaScript 文件的自定义内容},external: options.external, // 需要排除的外部依赖模块不会被打包进输出文件中outbase: src/${options.base}, // 指定输出文件相对于源文件的基础路径outdir: out/languages/bundled/esm/vs/${options.base}/, // 指定输出文件的目录路径plugins: [ // 配置插件用于在构建过程中进行额外的处理alias({vscode-nls: path.join(__dirname, fillers/vscode-nls.ts)})]});
}这里执行了几次构建结果就是下面这个文件夹的内容 下面的几个 .d.ts 文件是类型定义上面的几个文件夹分别是开发环境下amd模式的未压缩的 js 代码、压缩版本的amd模式代码、esm模式代码
2、./build/build-monaco-editor
这个文件就稍微有一丢丢长了。 令人感动的是每一步的注释都非常的清楚所以说真正优秀的项目不仅功能优秀注释也要到位啊 咱们先不看具体方法先看一下大致都做了什么操作
// 删除文件夹 out/monaco-editor
removeDir(out/monaco-editor);// dev folder
// AMD开发环境打包
AMD_releaseOne(dev);// min folder
// AMD压缩版本打包
AMD_releaseOne(min);// esm folder
// esm 模式打包
ESM_release();// monaco.d.ts, editor.api.d.ts
// 生成文件 monaco.d.ts, editor.api.d.ts
releaseDTS();// 生成 ThirdPartyNotices.txt
releaseThirdPartyNotices();// 生成 esm/metadata.d.ts, esm/metadata.js
generateMetadata();// package.json
// 生成 out/monaco-editor/package.json
(() {const packageJSON readFiles(package.json, { base: })[0];const json JSON.parse(packageJSON.contents.toString());json.private false;delete json.scripts[postinstall];packageJSON.contents Buffer.from(JSON.stringify(json, null, ));writeFiles([packageJSON], out/monaco-editor);
})();// 生成README.md、CHANGELOG.md、LICENSE文件
(() {/** type {IFile[]} */let otherFiles [];otherFiles otherFiles.concat(readFiles(README.md, { base: }));otherFiles otherFiles.concat(readFiles(CHANGELOG.md, { base: }));otherFiles otherFiles.concat(readFiles(node_modules/monaco-editor-core/min-maps/**/*, {base: node_modules/monaco-editor-core/}));otherFiles otherFiles.concat(readFiles(node_modules/monaco-editor-core/LICENSE, {base: node_modules/monaco-editor-core/}));writeFiles(otherFiles, out/monaco-editor);
})();第一步是删除 out/monaco 目录咱们在上一小节已经看过了那么一起看一下往下的代码是怎么实现的吧
① AMD模式代码构建
AMD_releaseOne(dev);、AMD_releaseOne(dev); 两句代码分别执行的是AMD模式下开发环境代码构建和压缩版本代码构建
function AMD_releaseOne(type: dev | min) {// 读取库文件const coreFiles readFiles(node_modules/monaco-editor-core/${type}/**/*, {base: node_modules/monaco-editor-core/${type}});// 1、读取库文件的内容整合组件模块拼接组装成 editor.main.js// 2、追加 monaco.contribution 模块AMD_addPluginContribs(type, coreFiles);// 写到 min或者dev 文件夹中writeFiles(coreFiles, out/monaco-editor/${type});// 读取配置文件const pluginFiles readFiles(out/languages/bundled/amd-${type}/**/*, {base: out/languages/bundled/amd-${type},ignore: [**/monaco.contribution.js]});// 将配置文件写到 min或者dev 文件夹中writeFiles(pluginFiles, out/monaco-editor/${type});
}
② ESM模式代码构建
function ESM_release() {// 读取库文件中的 esm 模式源码const coreFiles readFiles(node_modules/monaco-editor-core/esm/**/*, {base: node_modules/monaco-editor-core/esm,// we will create our own editor.api.d.ts which also contains the plugins APIignore: [node_modules/monaco-editor-core/esm/vs/editor/editor.api.d.ts]});// 给所有的使用 import 导入模块的地方添加 .js 后缀ESM_addImportSuffix(coreFiles);// 整合组件模块构建esm/vs/editor/editor.main.jsESM_addPluginContribs(coreFiles);// 将库文件写到目录中writeFiles(coreFiles, out/monaco-editor/esm);// 添加依赖文件 vs/editor/editor.apiESM_releasePlugins();
}下面的几个方法的实现其实都是差不多的内容需要读取上一小节中生成的 out/languages 文件夹中的内容还有库文件源码内容然后可能需要对文件进行重命名或者拼接上注释或者读取文件内容进行整合生成新的文件。
其中反复使用的有一个方法就是 build/utils.ts 中的 writeFiles() 方法
export function writeFiles(files: IFile[], dest: string) {for (const file of files) {// 获取完整路径const fullPath path.join(REPO_ROOT, dest, file.path);// path.dirname 用于获取指定文件路径的目录部分即去除文件名后的路径。// 用来创建目录确保逐层的目录都村子ensureDir(path.dirname(fullPath));fs.writeFileSync(fullPath, file.contents);}
}ensureDir() 方法用来是用来创建目录的使用一个 Set 保存已经存在的目录避免重复创建。 如果当前文件夹比根目录更长就将目录放到 dirs 数组中然后去除当前层级路径获取父级并循环判断。这样获取的 dirs 数组是从子级到父级的目录将数组反转过来就可以保证目录层级是从上到下的。 然后再使用 fs 库提供的 fs.mkdirSync() 方法创建目录。 build/fs.ts
const REPO_ROOT path.join(__dirname, ../);
// 存在的文件夹缓存
const existingDirCache new Set();export function ensureDir(dirname: string) {/** type {string[]} */const dirs [];// 根目录不需要创建while (dirname.length REPO_ROOT.length) {dirs.push(dirname);// 去除当前层级路径即获取父级dirname path.dirname(dirname);}// 反转数组保证文件夹顺序从上到下即先创建父级文件夹dirs.reverse();dirs.forEach((dir) {if (!existingDirCache.has(dir)) {try {// 创建目录fs.mkdirSync(dir);} catch (err) {}existingDirCache.add(dir);}});
}也就是说npm run build-monaco-editor 做的事情就是创建 out 目录 所以说第三章提到的 Monaco 实例的创建 create() 方法其实是从库文件源码node_modules/monaco-editor-core中获取的
参考文章 1、React中生命周期的讲解