动机
当市面上支流的组件库不能满足咱们业务需要的时候,那么咱们就有必要开发一套属于本人团队的组件库。
环境
开发环境:
- vue 3.0
- vue/cli 4.5.13
- nodeJs 12.16.3
- npm 6.14.4
步骤
创立我的项目
应用 vue-cli 创立一个 vue3 我的项目,假如我的项目名为 custom-npm-ui
$ vue create custom-npm-ui
手动抉择设置。
布局目录
├─ build // 打包脚本,用于寄存打包配置文件│ ├─ rollup.config.js ├─ examples // 原 src 目录,改成 examples 用于示例展现│ ├─ App.vue│ ├─ main.ts├─ packages // 新增 packages 目录,用于编写寄存组件,如button│ ├─ SubmitForm│ │ ├─ src/│ │ ├─ index.ts│ ├─ index.ts├─ typings // 新增 typings 目录, 用于寄存 .d.ts 文件,把 shims-vue.d.ts 挪动到这里│ ├─ shims-vue.d.ts├─ .npmignore // 新增 .npmignore 配置文件├─ vue.config.js // 新增 vue.config.js 配置文件
将 src
目录改为 examples
,并将外面的 assets
和 components
目录删除,移除 App.vue
里的组件援用。
我的项目配置
vue.config.js
新增 vue.config.js
配置文件,适配从新布局后的我的项目目录:
// eslint-disable-next-line @typescript-eslint/no-var-requiresconst path = require('path')module.exports = { // 批改 pages 入口 pages: { index: { entry: "examples/main.ts", //入口 template: "public/index.html", //模板 filename: "index.html" //输入文件 } }, // 扩大 webpack 配置 chainWebpack: (config) => { // 新增一个 ~ 指向 packages 目录, 不便示例代码中应用 config.resolve.alias .set('~', path.resolve('packages')) }}
.npmignore
新增 .npmignore
配置文件,组件公布到 npm
中,只有编译后的公布目录(例如lib
)、package.json
、README.md
才是须要被公布的,所以咱们须要设置疏忽目录和文件
# 疏忽目录.idea.vscodebuild/docs/examples/packages/public/node_modules/typings/# 疏忽指定文件babel.config.jstsconfig.jsontslint.jsonvue.config.js.gitignore.browserslistrc*.map
或者配置pkg#files
:
"files": [ "lib/", "package.json", "README.md" ],
装置依赖后的目录构造:
└─custom-npm-ui │ package.json │ README.md └─lib index.css index.esm.js index.min.js
tsconfig.json
批改 tsconfig.json 中 paths 的门路
"paths": { "@/*": [ "src/*" ]}
改为
"paths": { "~/*": [ "packages/*" ] }
Notes:typescript
反对的别名。
批改 include 的门路
"include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ]
改为
"include": [ "examples/**/*.ts", "examples/**/*.tsx", "examples/**/*.vue", "packages/**/*.ts", "packages/**/*.tsx", "packages/**/*.vue", "typings/**/*.ts", "typings/shims-vue.d.ts", "tests/**/*.ts", "tests/**/*.tsx" ]
package.json
批改 package.json 中公布到 npm 的字段
name
:包名,该名字是惟一的。可在npm近程源搜寻名字,如果存在则需换个名字。version
:版本号,每次公布至 npm 须要批改版本号,不能和历史版本号雷同。description
:形容。main
:入口文件,该字段需指向咱们最终编译后的包文件。typings
:types文件,TS组件须要。keyword
:关键字,以空格拆散心愿用户最终搜寻的词。author
:作者信息private
:是否公有,须要批改为 false 能力公布到 npmlicense
: 开源协定
参考设置:
{ "name": "custom-npm-ui", "version": "0.1.0", "private": false, "description": "基于ElementPlus二次开发的前端组件库", "main": "lib/index.min.js", "module": "lib/index.esm.js", "typings": "lib/index.d.ts", "keyword": "vue3 element-plus", "license": "MIT", "author": { "name": "yourname", "email": "youremail@163.com" } }
在 package.json 的 scripts 新增编译和公布的命令
"scripts": { "build": "yarn build:clean && yarn build:lib && yarn build:esm-bundle && rimraf lib/demo.html", "build:clean": "rimraf lib", "build:lib": "vue-cli-service build --target lib --name index --dest lib packages/index.ts", "build:esm-bundle": "rollup --config ./build/rollup.config.js"}
其中 build:lib
是利用 vue-cli
进行 umd
形式打包,build:esm-bundle
是利用 rollup
进行 es
形式打包。
build:lib
具体参数解析如下:
--target
: 构建指标,默认为利用模式。改为lib
启用库模式。--name
: 输入文件名--dest
: 输入目录,默认dist
。改成lib
[entry]
: 入口文件门路,默认为src/App.vue
。这里咱们指定编译packages/
组件库目录。
build:esm-bundle
打包后的资源在webpack2+
、rollup
环境中可通过pkg#module
配置载入应用。
rollup.config.js
新增 build/rollup.config.js
,rollup
打包脚本:
import cjs from "@rollup/plugin-commonjs"; // commonjs转es module —— rollup只反对es moduleimport resolve from "@rollup/plugin-node-resolve"; // 搭配@rollup/plugin-commonjs应用// import ts from '@rollup/plugin-typescript' // 【报错】应用ts报错import typescript from "rollup-plugin-typescript2"; // 解析TS语法import vue from "rollup-plugin-vue"; // 解析vueimport babel from "@rollup/plugin-babel";import scss from "rollup-plugin-scss"; // 解析scss// import requireContext from "rollup-plugin-require-context"; // 【不可用】反对webpack的require.context API —— 须要装置npm install --save-dev generate-source-map@0.0.5import { writeFileSync, existsSync, mkdirSync } from "fs";const extensions = [".js", ".ts", ".vue"];export default { input: "packages/index.ts", output: [ { file: "lib/index.esm.js", // 多文件输入的话,须要应用dir代替file format: "es", globals: { vue: "Vue", // 通知rollup全局变量Vue即是vue }, }, ], extensions, plugins: [ // 程序很重要 scss({ output: function (styles, styleNodes) { if (!existsSync("lib/")) { mkdirSync("lib/"); } writeFileSync("lib/index.css", styles); }, }), vue({ compileTemplate: true, }), // requireContext(), resolve({ jsnext: true, main: true, browser: true, extensions, }), cjs(), typescript(), babel({}), ], external: ["vue", "element-plus"],};
开发组件
注意事项
- 组件内不能应用懒加载
- 组件不能应用
require.context()
对立治理 - 不反对
JSX
语法编写模版 —— 更好的抉择React
依赖装置
环境依赖
$ npm i -D rimraf rollup @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup-plugin-typescript2 rollup-plugin-vue @rollup/plugin-babel rollup-plugin-scss
开发依赖
$ npm i -D element-plus@1.0.2-beta.69 babel-plugin-import
配置.babel.config.js
module.exports = { presets: ["@vue/cli-plugin-babel/preset"], plugins: [ [ "import", { libraryName: "element-plus", }, ], ],};
更新examples/main.ts
:
import { createApp } from "vue";import App from "./App.vue";import "element-plus/lib/theme-chalk/index.css";createApp(App).mount("#app");
编写组件
在 packages 目录下新建 index.ts
文件和 SubmitForm/
文件夹,在 SubmitForm
下新建 index.ts
和 src/index.vue
,构造如下:
.├── SubmitForm│ ├── SubmitForm.stories.ts│ ├── index.ts│ └── src│ ├── FormRender.vue│ ├── fileds│ │ ├── Color.vue│ │ ├── Datetime.vue│ │ ├── Radio.vue│ │ ├── Select.vue│ │ ├── Switch.vue│ │ ├── Text.vue│ │ ├── Upload.vue│ │ ├── hooks│ │ │ └── useValueHandleHook.ts│ │ └── index.ts│ ├── index.vue│ ├── schemas│ │ ├── baseSchema.ts│ │ └── schemasProp.ts│ └── store│ └── index.ts├── common│ └── getType.ts└── index.ts
packages/SubmitForm/src/index.vue
<script lang="ts">import { computed, defineComponent } from "vue";import { ElForm } from "element-plus";import { FormPropsType } from "./schemas/schemasProp";import FormRender from "./FormRender.vue";import { values, updateValues } from "./store";export default defineComponent({ name: "SubmitForm", props: FormPropsType, emits: ["runtimeChange"], components: { "el-form": ElForm, FormRender, }, setup(props, { emit }) { const schemasCpt = computed(() => props.schema); const defaultValueCpt = computed(() => props.defaultValue); function handleRuntimeChange(name: string, value: any) { updateValues(name, value); emit("runtimeChange", name, value); } return { schemasCpt, defaultValueCpt, values, handleRuntimeChange, }; },});</script><template> <el-form label-width="100px" :model="values"> <template v-for="(schema, idx) of schemasCpt" :key="idx"> <FormRender :schema="schema" :defaultValue="defaultValueCpt" v-bind="$attrs" :onRuntimeChange="handleRuntimeChange" /> </template> </el-form></template>
packages/SubmitForm/index.ts
,独自组件的入口文件,在其余我的项目能够应用 import { SubmitForm
} from 'custom-npm-ui' 形式进行单个组件援用
import type { App } from "vue";import SubmitForm from "./src/index.vue";// 定义 install 办法,接管 Vue 作为参数。如果应用 use 注册插件,那么所有的组件都会被注册SubmitForm.install = function (Vue: App) { // 遍历注册全局组件 Vue.component(SubmitForm.name, SubmitForm);};export default SubmitForm;
packages/index.ts
作为组件库的入口文件,能够在其余我的项目的 main.ts
引入整个组件库,内容如下
import type { App } from "vue";import SubmitForm from "./SubmitForm";const components = [SubmitForm];// 定义 install 办法,接管 Vue 作为参数。如果应用 use 注册插件,那么所有的组件都会被注册const install = function (Vue: App): void { // 遍历注册全局组件 components.map((component) => Vue.component(component.name, component));};export { // 以下是具体的组件列表 SubmitForm,};export default { // 导出的对象必须具备 install,能力被 Vue.use() 办法装置 install,};
这样,咱们就实现一个简略的 SubmitForm
组件,后续须要扩大其余组件,依照 SubmitForm
的构造进行开发,并且在 index.ts
文件中 components
组件列表增加即可。
编写示例调试
examples/main.ts
import { createApp } from "vue";import App from "./App.vue";import CustomeUI from "~/index";import "element-plus/lib/theme-chalk/index.css";createApp(App).use(CustomeUI).mount("#app");
examples/App.vue
删除我的项目初始化的 HelloWorld
组件
<script lang="ts">import { defineComponent, reactive, ref, toRaw } from "vue";import formSchema from "./layouts/case.layout";export default defineComponent({ name: "App", components: {}, setup() { const submitFormRef = ref(); const schema = reactive(formSchema); const defaultValues = reactive({}); function formRuntimeChange(name: string, value: any) { console.log(name, " = ", value); } function submit() { console.log(submitFormRef.value.values); } return { submitFormRef, schema, defaultValues, formRuntimeChange, submit, }; },});</script><template> <submit-form ref="submitFormRef" :schema="schema" :defaultValue="defaultValues" @runtimeChange="formRuntimeChange" > </submit-form> <button @click="submit">保留</button></template><style lang="scss">#app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px;}</style>
启动我的项目,测试一下
$ npm run serve
组件开发实现后,执行编译库命令:
$ npm run build
引入打包后的文件回归测试一下,没有问题再公布到 npm 仓库。
在示例入口 main.ts
援用咱们的组件库:
import { createApp } from "vue";import App from "./App.vue";import CustomeUI from "../lib/index.esm.js";import "element-plus/lib/theme-chalk/index.css";createApp(App).use(CustomeUI).mount("#app");
编写申明文件
创立目录构造
.├── typings│ ├── index.d.ts│ ├── component.d.ts│ └── packages│ └── submit-form.vue.d.ts├── common│ └── getType.ts└── index.ts
更新package.json
配置
// package.json{ ... "typings": "./typings/index.d.ts", "files": [ "lib/", "package.json", "typings/" ], "publishConfig": { "registry": "https://abc.com/" }}
外围文件
// typings/index.d.tsimport type { App } from "vue";export * from "./component.d";export declare const install: (app: App, opt: any) => void;declare const _default: { install: (app: App<any>, opt: any) => void;};export default _default;
// typings/component.d.tsexport { default as SubmitForm } from "./packages/submit-form.d";
// typings/packages/submit-form.d.tsimport { DefineComponent, ComponentOptionsMixin, VNodeProps, AllowedComponentProps, ComponentCustomProps, EmitsOptions, ComputedGetter, WritableComputedOptions,} from "vue";import { FormRowType } from "../schemas/schemasProp";declare const _default: DefineComponent< Record<string, unknown>, { refName: string; schema: FormRowType[]; defaultValue: Record<string, any>; }, Record<string, unknown>, Record<string, ComputedGetter<any> | WritableComputedOptions<any>>, // computed Record<string, () => void>, // methods ComponentOptionsMixin, ComponentOptionsMixin, EmitsOptions, string, VNodeProps & AllowedComponentProps & ComponentCustomProps, Readonly<Record<string, unknown> & Record<string, unknown>>, Record<string, unknown>>;export default _default;
公布组件
配置NPM仓库地址
组件开发并测试通过后,就能够公布到 npm 仓库提供给其余我的项目应用了,首先编写.npmrc
文件配置要上传的源地址:
registry=https://abc.com
更举荐更新pkg#publishConfig
指定仓库地址:
"publishConfig": { "registry": "https://abc.com" },
Notes:应用.npmrc
配置NPM仓库地址,nrm
无奈切换源。
获取NPM账号、明码
在npm官网注册即可
登录npm账号
在我的项目中 terminal 命令窗口登录 npm 账号
$ npm loginUsername:Password:Email:(this IS public)
输出在 npm的账号、明码、邮箱
公布
$ npm publish
组件文档
创立Storybook
敌对型环境
在我的项目中 terminal 命令窗口执行命令:
$ npx -p @storybook/cli sb init
storybook
是一个能够辅助UI开发的工具,是一个UI组件的开发环境。
在sb init
初始化过程中,storybook
会查看现有我的项目的dependencies
,而后根据我的项目的现有框架,提供最佳的组装形式。
Storybook
初始化做了以下步骤:
- 装置
Storybook
须要的依赖 - 更新
pkg#run-script
减少预设的配置文件
.storybook/main.js
.storybook/preview.js
- 减少示例模版
stories/
更新package.json
... "scripts": { "storybook": "start-storybook -p 6006 -h 0.0.0.0", // 启动本地服务以预览 "build-storybook": "build-storybook" // 构建 },...
$ npm run storybook # 启动本地服务拜访storybook我的项目
更新目录构造
├─ .storybook // 预设的配置文件│ ├─ main.js // 入口文件│ ├─ preview.js // 管制Stories的出现、全局资源的加载├─ stories // 示例模版
main.js
module.exports = { "stories": [ // Storybook会抓取、载入配置门路下的指定文件渲染展现 "../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)" ], "addons": [ // Storybook所用插件 —— Storybook性能加强 "@storybook/addon-links", "@storybook/addon-essentials" ], "framework": "@storybook/vue3" // Storybook所用框架 —— Vue环境反对}
编写示例
入口配置
更新.storybook/main.js
module.exports = { "stories": [ // Storybook会抓取、载入配置门路下的指定文件渲染展现 "../packages/**/*.stories.@(js|jsx|ts|tsx)", "../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)" ], ...}
组件Story编写
import SubmitForm from "./index"; // 引入组件import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";const caseSchema = [ // 示例数据 { key: "moduleName", name: "title", type: SchemaType.Text, label: "栏目名称", placeholder: "请输出栏目名称", attrs: { // }, rules: [ { required: true, message: "栏目名称必填~", trigger: RuleTrigger.Blur, }, ], }, ...];export default { title: "ui组件/SubmitForm", // 展现题目:应用门路定义命名空间 —— 分组、分类 component: SubmitForm,};const Template = (args: any) => ({ // 渲染组件 components: { SubmitForm }, setup() { return { ...args, }; }, template: '<submit-form :schema="schema"></submit-form>',});export const 根本利用 = Template.bind({}); // 组件利用示例(根本利用 as any).args = { schema: caseSchema, ref: "submitFormRef",};
能够应用props
&computed
去承接args
这样更合乎Vue3
的书写格局:
// 后续的补充内容,和此处上下文无关。const Template = (args: any) => ({ props: Object.keys(args), components: { SubmitForm, ElButton }, setup(props) { const refName = computed(() => props.refName) const submitFormRef = ref(); function submit() { console.log(submitFormRef.value.values); } function onRuntimeChange(name: string, value: any) { console.log(name, " = ", value); } return { submit, onRuntimeChange, [refName.value]: submitFormRef, ...props, }; }, template: ` <submit-form :schema="schema" :ref="refName" @runtimeChange="onRuntimeChange"></submit-form> <el-button @click="submit">提交</el-button> `,});
全局依赖配置
因为示例代码中依赖element-plus
,通过上述展示的页面没有款式,所以,StoryBook
渲染须要额定引入element-plus
主题:
// preview.jsimport "element-plus/lib/theme-chalk/index.css";export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }}
启动本地服务
更新命令脚本
// package.json "scripts": { "storybook": "start-storybook -p 6006 -h 0.0.0.0", "build-storybook": "build-storybook" },
-h 0.0.0.0
以反对局域网拜访。
执行命令
$ npm run storybook
成果展现
在Stories
中应用第三方UI库
以ElementPlus
为例:
全局配置
如果babel.config
没有配置按需加载,可间接编辑.storybook/preview.js
:
// .storybook/preview.jsimport elementPlus from 'element-plus';import { app } from '@storybook/vue3'app.use(elementPlus);export const decorators = [ (story) => ({ components: { story, elementPlus }, template: '<elementPlus><story/></elementPlus>' })];import "element-plus/lib/theme-chalk/index.css";export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }}
Notes:配置按需加载后,import elementPlus from 'element-plus';
导入elementPlus
报错:elementPlus is not defined
—— 全局加载、按需加载不能在同一我的项目中应用。
按需加载
在须要应用ElementPlus
的Stories
中间接引入即可:
// packages/SubmitForm/SubmitForm.stories.tsimport { ElButton } from 'element-plus';import SubmitForm from "./index";import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";const caseSchema = [ { key: "moduleName", name: "title", type: SchemaType.Text, label: "栏目名称", placeholder: "请输出栏目名称", attrs: { // }, rules: [ { required: true, message: "栏目名称必填~", trigger: RuleTrigger.Blur, }, ], }, ...];export default { title: "ui组件/SubmitForm", component: SubmitForm,};const Template = (args: any) => ({ components: { SubmitForm, ElButton }, setup() { return { ...args, }; }, template: '<submit-form :schema="schema" ref="submitFormRef"></submit-form><el-button @click="submit">提交</el-button>',});export const 根本利用 = Template.bind({});(根本利用 as any).args = { schema: caseSchema,};
示例代码增加交互
// packages/SubmitForm/SubmitForm.stories.tsimport { ElButton } from "element-plus";import { ref } from "vue";import SubmitForm from "./index";import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";const caseSchema = [ { key: "moduleName", name: "title", type: SchemaType.Text, label: "栏目名称", placeholder: "请输出栏目名称", attrs: { // }, rules: [ { required: true, message: "栏目名称必填~", trigger: RuleTrigger.Blur, }, ], }, ...];export default { title: "ui组件/SubmitForm", component: SubmitForm,};const Template = (args: any) => ({ components: { SubmitForm, ElButton }, setup() { const { refName } = args; const submitFormRef = ref(); function submit() { console.log(submitFormRef.value.values); } function onRuntimeChange(name: string, value: any) { console.log(name, " = ", value); } return { submit, onRuntimeChange, [refName]: submitFormRef, ...args, }; }, template: ` <submit-form :schema="schema" :ref="refName" @runtimeChange="onRuntimeChange"></submit-form> <el-button @click="submit">提交</el-button> `,});export const 根本利用 = Template.bind({});(根本利用 as any).args = { refName: "submitFormRef", schema: caseSchema,};
这里做了两件事:
- 减少提交按钮
- 减少数据提交交互
配置参数详情
默认文档展现
默认查看到的文档参数是以下样子:
参数配置
通过配置argTypes
能够补充参数信息:
// packages/SubmitForm/SubmitForm.stories.ts...export default { title: "ui组件/SubmitForm", component: SubmitForm, argTypes: { refName: { description: '表单组件援用', type: { required: true, }, table: { defaultValue: { summary: 'defaultNameRef', } }, control: { type: 'text' } }, schema: { type: { required: true, }, table: { type: { summary: '渲染表单所需JSON构造', detail: 'JSON构造蕴含表单渲染、交互所须要的必要字段,也蕴含表单的校验规定', }, defaultValue: { summary: '[]', detail: `[ { key: "moduleName", name: "title", type: SchemaType.Text, label: "栏目名称", placeholder: "请输出栏目名称", attrs: { // }, rules: [ { required: true, message: "栏目名称必填~", trigger: RuleTrigger.Blur, }, ], } ] ` } } }, runtimeChange: { description: '实时监听表单的更新', table: { category: 'Events', }, } }};...
具体配置见链接。
现实成果
文档部署
执行命令:
$ npm run build-storybook
生成动态页面,间接部署动态页面即可。
目录构造:
│ 0.0a0da810.iframe.bundle.js│ 0.0a0da810.iframe.bundle.js.LICENSE.txt│ 0.0a0da810.iframe.bundle.js.map│ 0.799c368cbe88266827ba.manager.bundle.js│ 1.9ebd2fb519f6726108de.manager.bundle.js│ 1.9face5ef.iframe.bundle.js│ 1.9face5ef.iframe.bundle.js.LICENSE.txt│ 1.9face5ef.iframe.bundle.js.map│ 10.07ff4e93.iframe.bundle.js│ 10.a85ea1a67689be8e19ff.manager.bundle.js│ 11.f4e922583ae35da460f3.manager.bundle.js│ 11.f4e922583ae35da460f3.manager.bundle.js.LICENSE.txt│ 12.1415460941f0bdcb8fa8.manager.bundle.js│ 2.8a28fd4e.iframe.bundle.js│ 2.8a28fd4e.iframe.bundle.js.LICENSE.txt│ 2.8a28fd4e.iframe.bundle.js.map│ 3.50826d47.iframe.bundle.js│ 4.779a6efa.iframe.bundle.js│ 5.f459d151315e6780c20f.manager.bundle.js│ 5.f459d151315e6780c20f.manager.bundle.js.LICENSE.txt│ 6.3bd64d820f3745f262ff.manager.bundle.js│ 7.3d04765dbf3f1dcd706c.manager.bundle.js│ 8.b541eadfcb9164835dfc.manager.bundle.js│ 8.c6cb825f.iframe.bundle.js│ 9.411ac8e451bbb10926c7.manager.bundle.js│ 9.51f84f13.iframe.bundle.js│ 9.51f84f13.iframe.bundle.js.LICENSE.txt│ 9.51f84f13.iframe.bundle.js.map│ favicon.ico│ iframe.html│ index.html // 入口页面│ main.4c3140a78c06c6b39fba.manager.bundle.js│ main.e86e1837.iframe.bundle.js│ runtime~main.1e621db5.iframe.bundle.js│ runtime~main.91a0c7330ab317d35c4a.manager.bundle.js│ vendors~main.0d1916dd840230bedd21.manager.bundle.js│ vendors~main.0d1916dd840230bedd21.manager.bundle.js.LICENSE.txt│ vendors~main.8b18b60f.iframe.bundle.js│ vendors~main.8b18b60f.iframe.bundle.js.LICENSE.txt│ vendors~main.8b18b60f.iframe.bundle.js.map│└─static └─media element-icons.5bba4d97.ttf element-icons.dcdb1ef8.woff
参考文档
- Storybook官网
参考文章很多,如狐疑内容参考,请分割,会思考减少到参考文档中