一、 搭建Vite环境
1.创建目录&初始化包配置&安装Vite依赖
mkdir gresgying-ui
cd gresgying-ui
npm init
npm i vite -D
2.根目录创建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Gresgying UI</title>
</head>
<body>
Hello Gresgying UI!
</body>
</html>
3.运行Vite
npx vite
结果如图:
浏览器查看网址,结果如图,可以正常显示,说明Vite已正确配置:
4.测试 TS:
创建文件 src/index.ts
const str: String = 'Hello Gresgying UI';
console.log(str)
在 index.html 的body标签中引入
<script src="./src/index.ts"></script>
保存后刷新浏览器,可以在控制台中看到如图显示,说明 TS 可以正常使用:
5.在根目录的 package.json 中添加启动命令
"scripts": {
"dev": "vite"
},
运行命令 npm run dev
结果如图所示:
Vite环境搭建完成
二、开发一个组件
1.基础组件
- 安装Vue
npm i vue
- 实现一个简单的 Button,创建 src/button/index.ts 因为 vue 默认不支持模板编译功能,所以使用 Render 函数的写法
import { defineComponent, h } from 'vue';
export default defineComponent({
name: 'GButton',
render() {
return h("button", null, "MyButton");
}
})
- 在index.html中增加根容器,展示组件
<div id="app"></div>
- 在 src/index.ts 中创建 Vue 实例并使用组件
import { createApp } from 'vue'
import GButton from './button'
createApp(GButton).mount('#app')
- 启动项目后,浏览器没有显示按钮,而且控制台报错
Uncaught SyntaxError: Cannot use import statement outside a module (at index.ts:1:1)
因为 src/index.ts中使用了 es6 的语法,所以在 index.html 中引入时需要指定为模块导入:
<script src="./src/index.ts" type="module"></script>
- 修改后按钮显示了,但是在浏览器控制台会有以下告警
Feature flags VUE_OPTIONS_API, VUE_PROD_DEVTOOLS, VUE_PROD_HYDRATION_MISMATCH_DETAILS are not explicitly defined. You are running the esm-bundler build of Vue, which expects these compile-time feature flags to be globally injected via the bundler config in order to get better tree-shaking in the production bundle.
官方说明:当以带有构建步骤的方式使用 Vue 时,可以配置一些编译时标志以启用/禁用特定的功能。使用编译时标志的好处是,以这种方式禁用的功能可以通过 tree-shaking 从最终的打包结果中移除。即使没有显式地配置这些标志,Vue 也会正常工作。然而,建议始终对它们进行配置,以便在可能的情况下正确地删除相关功能。
解决办法:安装插件 npm i @vitejs/plugin-vue -D
根目录新建 vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(), // VUE插件
],
})
启动项目时会出现如下提示
The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
在 package.json 中增加 "type": "module"
运行项目,可以看到按钮,启动时和浏览器控制台就都没有警告了。
2.单文件组件
-
Vue3.0 默认不支持模板编译,Vite默认只支持 TS 文件编译,Vue的模板需要再编译阶段转为TS代码(渲染函数)才能运行。 所以需要安装上面解决问题是安装的 Vite 的 Vue 组件,该组件不仅提供了模板编译,还支持单文件组件编译。如果没有安装
@vitejs/plugin-vue
组件,先按照上面的步骤安装并使用该组件。 -
实现单文件组件 src/SFCButton.vue
import { createApp } from 'vue'
// import GButton from './button'
import SFCButton from "./SFCButton.vue";
createApp(SFCButton).mount('#app')
-
此时代码静态检查会出现告警:TS2307: Cannot find module ./SFCButton.vue or its corresponding type declarations. 这是因为 ts 默认不支持 vue类型的模块
-
需要增加声明文件 src/shims-vue.d.ts
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
- 根目录下增加 tsconfig.json 配置文件
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": false,
"jsx": "preserve",
"moduleResolution": "node"
}
}
- 运行
npm run dev
就可以看到如图结果:
3.JSX 组件
- 安装支持jsx的插件
npm i @vitejs/plugin-vue-jsx -D
- 修改 vite.config.ts 配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vue(), // VUE 插件
vueJsx() // JSX 插件
],
})
- 创建 JSX 组件 src/JSXButton.tsx
import { defineComponent, h } from 'vue'
export default defineComponent({
name: "JSXButton",
render(){
return <button>JSXButton</button>
}
})
- 在 src/index.ts 中使用
import { createApp } from 'vue'
// import GButton from './button'
// import SFCButton from "./SFCButton.vue";
import JSXButton from "./JSXButton";
createApp(JSXButton).mount('#app')
- 运行后如图显示:
4.封装库文件
-
组件库需要支持两种导入方式
-
完整引入:一次性引入全部组件,通过 Vue.use 以 Vue 插件的方式引入
-
按需引入:导入单个组件,使用Vue.component 注册
-
-
创建入口文件 src/entry.ts
-
导出全部组件
-
实现一个 Vue 插件,插件中实现 install 方法,将所有组件安装到 Vue 实例
-
import { App } from "vue";
import GButton from "./button";
import SFCButton from "./SFCButton.vue";
import JSXButton from "./JSXButton";
// 导出单独组件
export { GButton, SFCButton, JSXButton }
// 实现 install 方法
export default {
install(app: App) : void {
app.component(GButton.name, GButton)
app.component(SFCButton.name, SFCButton)
app.component(JSXButton.name, JSXButton)
}
}
- 修改 vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
const rollupOptions = {
external: ["vue", "vue-router"],
output:{
globals: {
vue: "Vue"
}
}
}
export default defineConfig({
plugins: [
vue(), // VUE 插件
vueJsx() // JSX 插件
],
build: {
rollupOptions,
// 可以指定压缩工具terser, 需要安装后使用 npm i terser -D
minify: false,
// 是否生成 sourcemap 文件,方便debug
sourcemap: true,
// css 代码分割
cssCodeSplit: true,
lib: {
entry: "./src/entry.ts",
name: "GresgyingUI",
fileName: "gresgying-ui",
// 输出常用的三种模块类型
formats: ["esm", "umd", "iife"]
}
}
})
- 在 package.json 增加 build 命令打包
"scripts": {
"dev": "vite",
"build": "vite build"
},
- 运行 npm run build 结果如图所示
根目录 dist 目录下也生成了对应的文件
- 验证全量导入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>全量加载组件</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp } from "vue/dist/vue.esm-bundler.js"
import GresgyingUI from "../../dist/gresgying-ui.js"
import "uno.css"
createApp({
template: `
<GButton/>
<SFCButton/>
<JSXButton/>
`
}).use(GresgyingUI).mount("#app")
</script>
</body>
</html>
运行后访问,如图所示:
- 验证按需导入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>按需加载组件</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp } from "vue/dist/vue.esm-bundler.js"
import { GButton, SFCButton, JSXButton } from "../../dist/gresgying-ui.js"
import "uno.css"
createApp({
template: `
<GButton/>
<SFCButton/>
<JSXButton/>
`
}).component(GButton.name, GButton)
.component(SFCButton.name, SFCButton)
.component(JSXButton.name, JSXButton)
.mount("#app")
</script>
</body>
</html>
运行后访问,如图所示:
三、使用 UnoCSS 原子化 CSS
1.引入Unocss
- 安装依赖
npm i unocss -D
# 字体图标库
npm i @iconify-json/ic -D
- vite中增加 Unocss 插件配置
import Unocss from "unocss/vite"
import { presetUno, presetAttributify, presetIcons } from "unocss"
export default defineConfig({
plugins: [
...
Unocss({
presets: [presetUno(), presetAttributify(), presetIcons()]
})
]})
- 运行时,提示[unocss] Entry module not found. Did you add
import 'uno.css'
in your main entry?
修改 src/index.ts 文件,增加
import 'uno.css'
- 在 Button 组件中使用 Unocss, 将 src/button/index.ts 重命名为 src/button/index.tsx, 并修改内容为:
import { defineComponent, PropType, toRefs } from 'vue';
import 'uno.css'
export default defineComponent({
name: 'GButton',
setup(props, { slots }) {
return () => <button
class = {`
py-2
px-4
font-semibold
rounded-lg
shadow-md
text-white
bg-green-500
hover:bg-green-700
border-none
cursor-pointer
`}>
{slots.default ? slots.default(): ''}
</button>
}
})
- 在 src/index.ts 中引用测试
import { createApp } from 'vue'
import GresgyingUI from './entry'
createApp({
template: '<GButton>普通按钮</GButton>'
}).use(GresgyingUI).mount('#app')
运行后打开,没有显示按钮,且浏览器控制台显示告警:Component provided template option but runtime compilation is not supported in this build of Vue. Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".
因为组件中包含了template 模版,Vue默认使用的是运行时版本,不能处理 template 是字符串的情况,需要使用包含运行时编译器的 vue 版本,修改 vite.config.ts
export default defineConfig({
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js'
}
},
...
})
结果如图所示:
2.组件使用属性定制样式
- 定义属性类型&注册组件属性 src/button/index.tsx
import { defineComponent, PropType, toRefs } from 'vue';
import 'uno.css'
export type GColor = 'black' | 'gray' | 'red' | 'yellow' | 'green' | 'blue' | 'indigo' | 'purple' | 'pink'
export const props = {
color: {
type: String as PropType<GColor>,
default: 'blue'
}
}
export default defineComponent({
name: 'GButton',
props,
setup(props, { slots }) {
return () => <button
class = {`
py-2
px-4
font-semibold
rounded-lg
shadow-md
text-white
bg-${props.color}-500
hover:bg-${props.color}-700
border-none
cursor-pointer
`}>
{slots.default ? slots.default(): ''}
</button>
}
})
- 修改 src/index.ts 测试
import { createApp } from 'vue'
import GresgyingUI from './entry'
createApp({
template: `
<GButton color="blue">蓝色按钮</GButton>
<GButton color="green">绿色按钮</GButton>
<GButton color="gray">灰色按钮</GButton>
<GButton color="yellow">黄色按钮</GButton>
<GButton color="red">红色按钮</GButton>
`
}).use(GresgyingUI).mount('#app')
可以看到并没有按照我们传入的颜色显示,是因为 UnoCSS 是按需生成的,只能生成代码中使用过的样式。
- UnoCSS提供了安全列表选项,我们需要将样式中变量的取值添加到 Safelist中,新建 config/unocss.ts
import { presetUno, presetAttributify, presetIcons, defineConfig } from 'unocss'
import Unocss from "unocss/vite";
const colors = [
"white",
"black",
"gray",
"red",
"yellow",
"green",
"blue",
"indigo",
"purple",
"pink"
]
const safelist = [
...colors.map(v => `bg-${v}-500`),
...colors.map(v => `hover:bg-${v}-700`)
]
export default defineConfig({
safelist,
presets: [presetUno(), presetAttributify(), presetIcons()]
})
- 在 vite.config.ts 中引入重构的 unocss 配置
export default defineConfig({
...
plugins: [
vue(), // VUE 插件
vueJsx(), // JSX 插件
UnoCSS({
configFile: './config/unocss.ts'
})
],
})
如图所示,可以正常显示我们传入的颜色
- 如果运行时,提示[unocss] Entry module not found. Did you add
import 'uno.css'
in your main entry? 在 src/entry.ts中加入import 'uno.css'
3.图标按钮组件
- UnoCSS 中引入图标,只需要加载
@unocss/preset-icon
即可,它提供了大量的 iconify 图标,可以查看网址
- 这个在之前的实现中已经加载,现在只需要定制图标安全列表,修改 config/unocss.ts
const icons = [
"search",
"edit",
"check",
"message",
"star-off",
"delete",
"add",
"share"
]
const safelist = [
//新增
...icons.map(v => `i-ic-baseline-${v}`),
]
- src/button/index.tsx 中注册icon属性, 并添加字体图标
export const props = {
...
icon: {
type: String,
default: ''
}
}
export default defineComponent({
name: 'GButton',
props,
setup(props, { slots }) {
return () => <button
class = {`
...
`}>
{props.icon != "" ? <i class={`i-ic-baseline-${props.icon} p-3`}></i> : ""}
{slots.default ? slots.default(): ''}
</button>
}
})
- 修改测试文件 src/index.ts
import { createApp } from 'vue'
import GresgyingUI from './entry'
createApp({
template: `
<GButton color="blue" icon="search">蓝色按钮</GButton>
<GButton color="green" icon="edit">绿色按钮</GButton>
<GButton color="gray" icon="check">灰色按钮</GButton>
<GButton color="yellow" icon="message">黄色按钮</GButton>
<GButton color="red" icon="delete">红色按钮</GButton>
`
}).use(GresgyingUI).mount('#app')
可以看到带图标的按钮:
四、Vitepress搭建文档网站
1.搭建文档网站
- 安装依赖
npm i vitepress -D
- 新建 vitepress 的 vite 配置 docs/vite.config.ts
import {defineConfig} from "vite"
import vueJsx from "@vitejs/plugin-vue-jsx";
import UnoCSS from "unocss/vite";
export default defineConfig({
plugins: [
vueJsx(),
UnoCSS({
configFile: './config/unocss.ts'
})
],
server:{
port: 3000
}
})
- 创建文档首页 docs/index.md
## hello Vitepress
const str:String = "hello vitepress"
- 增加文档启动脚本 package.json
"scripts": {
...
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:serve": "vitepress serve docs"
},
- 运行
npm run docs:dev
打开网址,效果如图:
2.引入组件并展示
- 配置菜单 docs/.vitepress/config.ts
const sidebar = {
'/': [
{
text: 'Guide',
items: [
{ text: '快速开始', link: '/' },
{ text: '通用', link: '/components/button/' }
]
}
]
}
const config = {
themeConfig: {
sidebar
}
}
export default config
- 引入组件 docs/.vitepress/theme/index.ts
import Theme from 'vitepress/dist/client/theme-default/index.js'
import GresgyingUI from '../../../src/entry'
export default {
...Theme,
enhanceApp({ app }){
app.use(GresgyingUI)
}
}
- 组件文档 docs/components/button/index.md
# Button 按钮
<div style="margin-bottom: 20px">
<GButton color="blue">主要按钮</GButton>
<GButton color="green">绿色按钮</GButton>
<GButton color="gray">灰色按钮</GButton>
<GButton color="yellow">黄色按钮</GButton>
<GButton color="red">红色按钮</GButton>
</div>
- 运行
npm run docs:dev
,结果如图所示:
3.引入组件代码
- 修改菜单配置 docs/.vitepress/config.ts
const sidebar = {
'/': [
{
text: 'Guide',
items: [
{ text: '快速开始', link: '/' },
{ text: '通用', link: '/components/button/' }
]
},
{
text: 'Components',
items: [
{ text: '组件', link: '/components/' },
{ text: '按钮', link: '/components/button/' }
]
}
]
}
- 修改组件文档
# Button 按钮
常用操作按钮
## 基础用法
基础函数用法
<div style="margin-bottom: 20px">
<GButton color="blue">主要按钮</GButton>
<GButton color="green">绿色按钮</GButton>
<GButton color="gray">灰色按钮</GButton>
<GButton color="yellow">黄色按钮</GButton>
<GButton color="red">红色按钮</GButton>
</div>
<div style="margin-bottom: 20px">
<GButton color="blue" icon="search">搜索按钮</GButton>
<GButton color="green" icon="edit">编辑按钮</GButton>
<GButton color="gray" icon="check">成功按钮</GButton>
<GButton color="yellow" icon="message">提示按钮</GButton>
<GButton color="red" icon="delete">删除按钮</GButton>
</div>
<div style="margin-bottom: 20px">
<GButton color="blue" icon="search"></GButton>
<GButton color="green" icon="edit"></GButton>
<GButton color="gray" icon="check"></GButton>
<GButton color="yellow" icon="message"></GButton>
<GButton color="red" icon="delete"></GButton>
</div>
::: details code
使用`size`、`color`、`pain`、`round`属性来定义 Button 样式。
```vue
<template>
<div style="margin-bottom: 20px">
<GButton color="blue">主要按钮</GButton>
<GButton color="green">绿色按钮</GButton>
<GButton color="gray">灰色按钮</GButton>
<GButton color="yellow">黄色按钮</GButton>
<GButton color="red">红色按钮</GButton>
</div>
<div style="margin-bottom: 20px">
<GButton color="blue" icon="search">搜索按钮</GButton>
<GButton color="green" icon="edit">编辑按钮</GButton>
<GButton color="gray" icon="check">成功按钮</GButton>
<GButton color="yellow" icon="message">提示按钮</GButton>
<GButton color="red" icon="delete">删除按钮</GButton>
</div>
<div style="margin-bottom: 20px">
<GButton color="blue" icon="search"></GButton>
<GButton color="green" icon="edit"></GButton>
<GButton color="gray" icon="check"></GButton>
<GButton color="yellow" icon="message"></GButton>
<GButton color="red" icon="delete"></GButton>
</div>
</template>
:::
图标按钮
带图标的按钮可以增加辨识度或者不显示文字
::: details code
设置icon
属性即可
<template>
<div class="flex flex-row">
<GButton icon="edit" plain></GButton>
<GButton icon="delete" plain></GButton>
<GButton icon="share" plain></GButton>
<GButton icon="search" plain round>搜索</GButton>
</div>
</template>
:::
API
Props
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
color | 按钮颜色, 可选值black gray red yellow green blue indigo purple pink |
String | blue |
icon | 按钮图标 | String | - |
- 运行效果如图
五、Jest 单元测试
1.搭建 Jest 环境
-
安装依赖
npm i jest -g
-
根目录创建测试文件 add.js
const add = (a, b) => a + b;
module.exports = add;
- Jest 测试函数 /src/tests/add.test.js
const add = require('../../add.js')
describe("测试 Add 函数", () => {
test("add(1, 2) === 3", () => {
expect(add(1, 2)).toBe(3)
});
test("add(1, 1) === 2", () => {
expect(add(1, 1)).toBe(2)
})
})
运行 jest
,结果如图所示:
2.Mock数据测试无法执行的函数
-
安装 axios
npm i axios -D
-
根目录创建测试文件 fetch.js
const axios = require('axios')
exports.getData = () => axios.get('/abc/bcd')
- 测试文件 /src/tests/fetch.test.js
const { getData } = require('../../fetch.js')
const axios = require('axios')
jest.mock('axios')
it("fetch", async () => {
axios.get.mockResolvedValueOnce('123')
axios.get.mockResolvedValue('456')
const data1 = await getData()
const data2 = await getData()
expect(data1).toBe('123')
expect(data2).toBe('456')
})
- 执行
jest
, 结果如图
- 使用 jest.fn() 创建 mock 函数, 在 /src/tests/fetch.test.js 中实现:
test('测试jest.fn()调用', () => {
let mockFn = jest.fn()
let result = mockFn(1, 2, 3)
// 断言 mockFn 执行返回undefined
expect(result).toBeUndefined()
// 断言 mockFn 会被调用
expect(mockFn).toBeCalled()
// 断言 mockFn 会被调用一次
expect(mockFn).toBeCalledTimes(1)
// 断言 mockFn 传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3)
})
- 结果如图:
3.测试前端页面
-
安装 dom 仿真依赖 jsdom,
npm i jsdom -D
-
配置jsdom, 根目录下创建 jsdom-config.js
const jsdom = require('jsdom')
const { JSDOM } = jsdom
const dom = new JSDOM('<!DOCTYPE html><head/><body></body>',{
url: 'http://localhost/',
referrer: 'https://example.com/',
contentType: 'text/html',
userAgent: 'Mellblomenator/9000',
includeNodeLocations: true,
storageQuota: 10000000,
})
global.window = dom.window
global.document = window.document
global.navigator = window.navigator
- 根目录下创建被测文件 dom.js
exports.generateDiv = () => {
const div = document.createElement("div")
div.className = 'cc'
document.body.appendChild(div)
}
- 编写测试文件 src/tests/dom.test.js
const { generateDiv } = require('../../dom.js')
require('../../jsdom-config.js')
describe('DOM 测试', () => {
test('测试 dom 操作', () => {
generateDiv()
expect(document.getElementsByClassName('cc').length).toBe(1)
})
});
- 运行
jest
效果如图
六、Vitest 单元测试
1.搭建环境
- 安装依赖
// 测试框架, 用于执行整个测试过程并提供断言库、mock、覆盖率
npm i vitest -D
// 用于提供在 node 环境中的 Dom 仿真模型
npm i happy-dom -D
// 测试工具库
npm i @vue/test-utils
- vite 配置修改 vite.config.ts ,最上面需要增加声明
/// <reference types="vitest" />
export default defineConfig({
...
test: {
globals: true,
environment: 'happy-dom',
transformMode: {
web: [/.[tj]sx$/],
},
}
})
- 重构组件,将 src/button/index.tsx 重命名为 Button.tsx , 然后再 src/button/ 下新增入口文件 src/button/index.ts
import Button from './Button'
export default Button;
2.编写测试用例
- 测试用例文件 src/button/__tests__Button.test.ts
import Button from "../Button";
import {shallowMount} from "@vue/test-utils";
import {describe, expect, test} from "vitest";
describe('Button', () => {
test('mount @vue/test-utils', () => {
const wrapper = shallowMount(Button, {
slots: {
default: 'Button'
}
})
expect(wrapper.text()).toBe('Button')
})
})
- 修改运行脚本 package.json
"scripts": {
...
"test": "vitest"
},
- 启动测试
npm run test
,结果之前的测试用例跑不过了,需要修改一下
src/tests/add.test.js
import add from '../../add.js'
describe("测试 Add 函数", () => {
test("add(1, 2) === 3", () => {
expect(add(1, 2)).toBe(3)
});
test("add(1, 1) === 2", () => {
expect(add(1, 1)).toBe(2)
})
})
src/tests/dom.test.js
import { generateDiv } from '../../dom.js'
import '../../jsdom-config.js'
describe('DOM 测试', () => {
test('测试 dom 操作', () => {
generateDiv()
expect(document.getElementsByClassName('cc').length).toBe(1)
})
});
src/tests/fetch.test.js
vi.mock('axios');
test('测试jest.fn()调用', () => {
let mockFn = vi.fn()
let result = mockFn(1, 2, 3)
// 断言 mockFn 执行返回undefined
expect(result).toBeUndefined()
// 断言 mockFn 会被调用
expect(mockFn).toBeCalled()
// 断言 mockFn 会被调用一次
expect(mockFn).toBeCalledTimes(1)
// 断言 mockFn 传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3)
})
- 再次运行,结果如图所示,可以看到 Vitest 兼容了大部分 Jest 的用法。
- 上面只是测试了按钮的默认状态,组件重要的是对于不同的 props 实现不同的样式,这里以 color 为例,在 src/button/tests/Button.test.ts 中继续增加测试用例
// 增加以下代码
describe('color', () => {
test('default', () => {
const wrapper = shallowMount(Button, {
slots: {
default: 'Button'
}
});
expect(wrapper.classes().map(v => v.replace('\n', '')).includes('bg-blue-500')).toBe(true);
})
test('red', () => {
const wrapper = shallowMount(Button, {
slots: {
default: 'Button'
},
props: {
color: 'red'
}
})
expect(wrapper.classes().map(v => v.replace('\n', '')).includes('bg-red-500')).toBe(true);
})
})
- 运行后结果如图