这两天利用晚上的时间,操作了一把vite+vue3,现在把操作过程记录下来。
一、准备工作
首先,你需要把nodejs版本升级到12以上,官网:nodejs.org,中文站:http://nodejs.cn/,我直接下载16.14.2LTS版
然后,安装Visual Studio Code,官网:https://code.visualstudio.com/,为了更好的使用VSCode,根据自己的需要安装一系列插件
二、开始工作
1.vite
1.1.简要说明:vite是用来代替webpack的打包工具,原理是利用现代浏览器已经支持esmodule的import,vite通过拦截由import转变的http请求,做一些预编译,提升开发体验。
1.2.步骤过程:
a).使用npm创建工程,命令:npm init @vitejs/app。
b).系统提示输入工程名称,假设名称为ViteVue。
c).系统提示选择一个框架模板,通过上下箭头选择,这里当然选择vue。此处说明,vite不是专门为vue设计,它还支持react等其他框架模板。
d).上述三步完成之后,基于vite的vue项目就创建完毕。
e).进入工程目录,命令:cd vitevue
f).安装依赖包,命令:npm install
1.3.查看效果:启动项目,命令:npm run dev
2.vue-router
2.1.简要说明:与vue3搭配的vue-router版本是4.x,vue-router4.x和vue-router3.x在使用的过程中有些差异,需要查看一下官方文档。
2.2.步骤过程:
a).安装vue-router,命令:npm install vue-router@next --save
b).在src目录下创建router文件夹,在文件夹下创建index.js文件
c).在index.js文件编写以下代码
1 import * as vueRouter from 'vue-router'; 2 3 const routes = [ 4 { 5 path: "/", 6 redirect: "/home" 7 }, 8 { 9 path: '/home', 10 name: 'Home', 11 component: () => import("@/views/home/index.vue") 12 } 13 ] 14 15 const router = vueRouter.createRouter({ 16 history: vueRouter.createWebHistory(), 17 routes: routes 18 }) 19 20 export default router
d).修改src/views/home/index.js文件内容为以下内容
1 <template> 2 <HelloWorld :msg="msg"></HelloWorld> 3 </template> 4 5 <script> 6 import HelloWorld from "@/components/HelloWorld.vue" 7 8 export default ({ 9 name: "Home", 10 components: { HelloWorld }, 11 setup() { 12 return { 13 msg: "hello World", 14 } 15 } 16 }) 17 </script>
e).修改main.js文件,引入vue-router,代码如下
1 import * as Vue from 'vue' 2 3 import App from './App.vue' 4 import router from './router/index' 5 6 const app=Vue.createApp(App) 7 app.use(router) 8 app.mount('#app')
f).将App.vue文件中的内容替换为以下内容
1 <template> 2 <img alt="Vue logo" src="./assets/logo.png" /> 3 <router-view /> 4 </template> 5 6 <script> 7 export default { 8 name: 'App', 9 setup() { 10 11 } 12 } 13 </script>
g).修改vite.config.js文件,安装path模块,添加@别名
1.安装命令:npm install --save-dev @types/node
2.配置文件:
1 import { defineConfig } from 'vite' 2 import vue from '@vitejs/plugin-vue' 3 import path from 'path' // 需安装此模块 4 5 // https://vitejs.dev/config/ 6 export default defineConfig({ 7 plugins: [vue()], 8 resolve: { 9 alias: { 10 '@': path.resolve(__dirname, 'src') 11 } 12 } 13 })
2.3.查看效果:上述步骤过程执行完毕,运行命令:npm run dev,就看到效果了。
3.vuex
3.1.简要说明:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。与vue3搭配的vuex版本是4.x。
3.2.步骤过程:
a).安装vuex,命令:npm install vuex@next --save
b).在src下创建目录store文件夹,然后在其下创建index.js文件,内容如下
1 import Vuex from 'vuex' 2 import getters from './getters' 3 4 const store = Vuex.createStore({ 5 modules:{ }, 6 getters 7 }) 8 9 export default store
c).修改main.js文件,引入vuex,内容如下
1 import * as Vue from 'vue' 2 3 import App from './App.vue' 4 import store from './store/index' 5 6 const app=Vue.createApp(App) 7 app.use(store) 8 app.mount('#app')
d).vuex4.x的使用和vuex3.x版本差别不大,这里就不写示例了。
3.3.查看效果:启动项目,命令:npm run dev
4.elementplus
4.1.简要说明:element团队给搭配vue3的elementUI起了个新名字elementPlus,这里只是记录使用它的过程,你也可以使用阿里蚂蚁团队的ant-design-vue等。
4.2.步骤过程:
a).安装elementPlus,命令:npm install element-plus --save
b).修改main.js文件,引入elementplus,代码如下
1 import * as Vue from 'vue' 2 3 import App from './App.vue' 4 import ElementPlus from 'element-plus' 5 import 'element-plus/theme-chalk/index.css' 6 import zhCn from 'element-plus/es/locale/lang/zh-cn' 7 8 const app=Vue.createApp(App) 9 app.use(ElementPlus,{locale:zhCn}) 10 app.mount('#app')
c).修改App.vue文件,编写示例,代码如下
1 <template> 2 <img alt="Vue logo" src="./assets/logo.png" /> 3 4 <el-form :inline="true" :model="formInline" class="demo-form-inline"> 5 <el-form-item label="审批人"> 6 <el-input v-model="formInline.user" placeholder="审批人"></el-input> 7 </el-form-item> 8 <el-form-item label="活动区域"> 9 <el-select v-model="formInline.region" placeholder="活动区域"> 10 <el-option label="区域一" value="shanghai"></el-option> 11 <el-option label="区域二" value="beijing"></el-option> 12 </el-select> 13 </el-form-item> 14 <el-form-item> 15 <el-button type="primary" @click="onSubmit">查询</el-button> 16 </el-form-item> 17 </el-form> 18 </template> 19 20 <script> 21 export default { 22 data() { 23 return { 24 formInline: { 25 user: "", 26 region: "", 27 }, 28 }; 29 }, 30 31 methods: { 32 onSubmit() { 33 console.log("submit!"); 34 }, 35 }, 36 }; 37 </script>
4.3.查看效果:启动项目,命令:npm run dev
5.axios
5.1.简要说明:使用axios主要要把跨域问题解决好。
5.2.步骤过程:
a).安装axios,命令:npm install axios
b).修改App.vue文件,内容如下
1 <template> 2 <img alt="Vue logo" src="./assets/logo.png" /> 3 <div> 4 {{weatherinfo.city}} 5 {{weatherinfo.WD}} 6 {{weatherinfo.WS}} 7 </div> 8 </template> 9 10 <script> 11 import { onMounted,reactive } from "vue"; 12 import axios from "axios" 13 14 export default defineComponent({ 15 name: 'App', 16 setup() { 17 let weatherinfo = reactive({}) 18 19 onMounted(()=>{ 20 axios.get(`devApi/data/sk/101010100.html`).then(res=>{ 21 Object.assign( weatherinfo, res.data.weatherinfo) 22 console.log('weatherinfo', weatherinfo) 23 }).catch(err=>{ 24 console.log(err) 25 }) 26 }) 27 28 return { weatherinfo} 29 } 30 }) 31 </script>
c).由于涉及到跨域,修改vite.config.js文件配置代理
1 import {defineConfig} from 'vite' 2 import vue from '@vitejs/plugin-vue' 3 import path from 'path' // 需安装此模块 4 import cfgSetting from './src/configs/settings' 5 6 7 // https://vitejs.dev/config/ 8 export default defineConfig({ 9 plugins: [ 10 vue() 11 ], 12 resolve: { 13 alias: { 14 '@': path.resolve(__dirname, 'src') 15 } 16 }, 17 server: { 18 host: cfgSetting.spaAddress, 19 port: cfgSetting.spaPort, 20 strictPort: false, //设为true时端口被占用则直接退出,不会尝试下一个可用端口 21 cors: true, //为开发服务器配置CORS, 默认启用并允许任何源 22 open: true, //服务启动时自动在浏览器中打开应用 23 hmr: false, //禁用或配置 HMR 连接 24 //传递给 chockidar 的文件系统监视器选项 25 watch: { 26 ignored:["!**/node_modules/your-package-name/**"] 27 }, 28 proxy: { 29 [cfgSetting.apiType]: { 30 target: cfgSetting.svcAddress, //实际请求地址 31 changeOrigin: true, 32 ws: true, 33 rewrite: (path) => path.replace(cfgSetting.apiType, '') 34 } 35 }, 36 https: cfgSetting.svcIsHttps 37 } 38 })
d).settings.js文件内容见第三部分的第六小节。
5.3.查看效果:启动项目,命令:npm run dev
6.i18n
6.1.简要说明:一直以来我是不建议使用i18n配置国际化,原因很简单,语言和文化差异会使人的审美异同,众口难调,如果使用i18n配置国际化,就很难设计出一套具备个性化的方案,最后只能做出取舍,得出一套通用没有个性化的方案。
6.2.步骤过程:
a).安装vue-i18n,命令:npm install vue-i18n@next
b).在src目录下创建locales目录,并添加index.js,zh_cn.js,en_us.js三个文件
c).index.js文件内容如下
1 import { createI18n } from 'vue-i18n' //引入vue-i18n组件 2 import zh_cn from './zh_cn' //中文语言包 3 import en_us from './en_us' //英文语言包 4 5 // 实例化I18n 6 const i18n = createI18n({ 7 legacy: false, 8 globalInjection: true, 9 locale: "zh_cn", // 初始化配置语言 10 messages: { 11 zh_cn, 12 en_us 13 } 14 }) 15 16 export default i18n
d).zh_cn.js文件内容如下
1 export default { 2 message: { 3 Home: '首页', 4 About: '关于' 5 } 6 }
e).en_us.js文件内容如下
1 export default { 2 message: { 3 Home: 'Home', 4 About: 'About' 5 } 6 }
f).修改main.js文件,引入vue-i18n,代码如下
1 import * as Vue from 'vue' 2 3 import App from './App.vue' 4 import i18n from './locales/index' 5 6 const app=Vue.createApp(App) 7 app.use(i18n) 8 app.mount('#app')
g).修改App.vue文件,查看i18n效果
1 <template> 2 <img alt="Vue logo" src="@/assets/logo.png" /> 3 <a href="javascript:void(0)" @click="change('zh_cn')">中文</a> -- 4 <a href="javascript:void(0)" @click="change('en_us')">English</a> 5 <div>{{$t("message.Home")}}---{{$t("message.About")}}</div> 6 7 <router-view /> 8 </template> 9 10 <script> 11 import { useI18n } from 'vue-i18n' 12 13 export default ({ 14 name: "App", 15 setup() { 16 const { locale } = useI18n() 17 function change(type) { 18 locale.value = type; 19 } 20 return { 21 change 22 } 23 } 24 }) 25 </script>
6.3.查看效果:启动项目,命令:npm run dev
7.mock
7.1.简要说明:前后端分离之后,使用mock模拟产生后端提供的数据是非常有必要的,即即便后端一时无法提供服务,也不影响前端的正常编程工作。
7.2.步骤过程:
a).安装mockjs,命令:npm install mockjs --save-dev
b).安装vite-plugin-mock,命令:npm i vite-plugin-mock cross-env -D
c).在 package.json 中设置环境变量
1 { 2 "scripts": { 3 "dev": "cross-env NODE_ENV=development vite", 4 "build": "vite build", 5 "serve": "vite preview" 6 } 7 }
d).在 vite.config.js 中添加 mockjs 插件
1 import { defineConfig } from "vite" 2 import vue from "@vitejs/plugin-vue" 3 import { viteMockServe } from "vite-plugin-mock" 4 5 export default defineConfig({ 6 plugins: [ 7 vue(), 8 viteMockServe({ 9 supportTs: false //如果使用typescript开发,则需要配置supportTs为true 10 }) 11 ] 12 })
e).在工程中根目录创建 mock 文件夹,建立getUsers.js在其中创建需要的数据接口
1 export default [ 2 { 3 url: "/api/getUsers", 4 method: "get", 5 response: () => { 6 return { 7 code: 0, 8 message: "ok", 9 data: ["张三", "李四"], 10 } 11 } 12 } 13 ]
f).修改App.vue文件,请求接口,显示数据
1 <template> 2 <img alt="Vue logo" src="./assets/logo.png" /> 3 <div v-for="(item,index) in users"> 4 {{index+1}}-{{item}} 5 </div> 6 </template> 7 8 <script> 9 import { onMounted, ref } from "vue"; 10 import axios from "axios" 11 12 export default defineComponent({ 13 name: 'App', 14 setup() { 15 let users = ref([]) 16 17 onMounted(()=>{ 18 axios.get(`/api/getUsers`).then(res=>{ 19 users.value = res.data.data 20 console.log('users', users) 21 }).catch(err=>{ 22 console.log(err) 23 }) 24 }) 25 26 return { users } 27 } 28 }) 29 </script>
7.3.查看效果:启动项目,命令:npm run dev
8.sass
8.1.简要说明:Sass是一个将脚本解析成CSS的脚本语言,即SassScript。 它扩展了 CSS3,增加了规则、变量、混入、选择器、继承等等特性。
8.2.步骤过程:
a).安装sass,命令:npm i sass -D
b).使用sass,语法:<style lang="scss"></style>
c).特别说明:由于vite已经集成好了sass的相关loader,所以无需额外配置。
8.3.查看效果:启动项目,命令:npm run dev
9.gzip
9.1.简要说明:对打包后的文件进行压缩,在部署到Web服务器后,可以节省带宽资源,前提是Web服务器支持压缩。这里主要使用vite-plugin-compression插件实现,地址:https://github.com/anncwb/vite-plugin-compression
9.2.步骤过程:
a).安装vite-plugin-compression插件,命令:npm i vite-plugin-compression -D
b).修改vite.config.js文件,引入插件,代码如下
1 import { defineConfig } from 'vite' 2 import vue from '@vitejs/plugin-vue' 3 import viteCompression from 'vite-plugin-compression' 4 import path from 'path' 5 6 // https://vitejs.dev/config/ 7 export default defineConfig({ 8 plugins: [ 9 vue(), 10 viteCompression({ 11 threshold: 10240, //体积大于10kb压缩 12 filter: /\.(js|mjs|json|css|html)$/i, 13 algorithm: 'gzip', //压缩算法,gzip|brotliCompress|deflate|deflateRaw 14 disable: false, 15 deleteOriginFile: false //是否删除源文件 16 }) 17 ], 18 resolve: { 19 alias: { 20 '@': path.resolve(__dirname, 'src') 21 } 22 } 23 })
9.3.查看效果:启动项目,命令:npm run dev
10.copy
10.1.简要说明:在编程的过程中,有一些静态资源是不需要vite进行处理的,只需要简单拷贝到dist文件夹下即可,此时我们使用rollup-plugin-copy插件就可以实现。
10.2.步骤过程:
a).安装rollup-plugin-copy,命令:npm install rollup-plugin-copy -D
b).修改vite.config.js文件,引入插件,代码如下
1 import { defineConfig } from 'vite' 2 import vue from '@vitejs/plugin-vue' 3 import copy from 'rollup-plugin-copy' 4 import path from 'path' 5 6 // https://vitejs.dev/config/ 7 export default defineConfig({ 8 plugins: [ 9 vue(), 10 copy({ 11 targets: [ 12 { src: 'src/static', dest: 'dist' }, //执行拷贝 13 ], 14 hook: 'writeBundle' // notice here 15 }) 16 ], 17 resolve: { 18 alias: { 19 '@': path.resolve(__dirname, 'src') 20 } 21 } 22 })
10.3.查看效果:启动项目,命令:npm run dev
三、配置信息
1.vite.config.js
1 import { defineConfig } from 'vite' 2 import vue from '@vitejs/plugin-vue' 3 import { viteMockServe } from "vite-plugin-mock" 4 import viteCompression from 'vite-plugin-compression' 5 import copy from 'rollup-plugin-copy' 6 import path from 'path' 7 import cfgSetting from './src/configs/settings' 8 9 // https://vitejs.dev/config/ 10 export default defineConfig({ 11 base: './', 12 publicDir: "public", //静态资源服务的文件夹 13 logLevel: "info", //控制台输出的级别 info 、warn、error、silent 14 clearScreen: true, //设为false 可以避免 vite 清屏而错过在终端中打印某些关键信息 15 build: { 16 //浏览器兼容性 "esnext"|"modules" 17 target: "modules", 18 //指定输出路径 19 outDir: "dist", 20 //生成静态资源的存放路径 21 assetsDir: "assets", 22 //小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求。设置为 0 可以完全禁用此项 23 assetsInlineLimit: 4096, 24 //启用/禁用 CSS 代码拆分 25 cssCodeSplit: true, 26 //构建后是否生成 source map 文件 27 sourcemap: false, 28 //自定义底层的 Rollup 打包配置 29 rollupOptions: { 30 output: { 31 } 32 }, 33 //当设置为 true,构建后将会生成 manifest.json 文件 34 manifest: false, 35 /* 36 设置为 false 可以禁用最小化混淆,或是用来指定使用哪种混淆器 37 boolean | 'terser' | 'esbuild' 38 terser 构建后文件体积更小 39 */ 40 minify: 'terser', 41 //传递给 Terser 的更多 minify 选项。 42 terserOptions: { 43 compress:{ 44 drop_console: true, 45 drop_debugger: true 46 } 47 }, 48 //设置为 false 来禁用将构建后的文件写入磁盘 49 write: true, 50 //默认情况下,若 outDir 在 root 目录下,则 Vite 会在构建时清空该目录。 51 emptyOutDir: true, 52 //启用/禁用 brotli 压缩大小报告 53 brotliSize: true, 54 //chunk 大小警告的限制 55 chunkSizeWarningLimit: 500 56 }, 57 plugins: [ 58 vue(), 59 viteMockServe({ 60 supportTs: false //如果使用typescript开发,则需要配置supportTs为true 61 }), 62 viteCompression({ 63 threshold: 10240, //体积大于10kb压缩 64 filter: /\.(js|mjs|json|css|html)$/i, 65 algorithm: 'gzip', //压缩算法,gzip|brotliCompress|deflate|deflateRaw 66 disable: false, 67 deleteOriginFile: false //是否删除源文件 68 }), 69 copy({ 70 targets: [ 71 { src: 'src/static', dest: 'dist' }, //执行拷贝 72 ], 73 hook: 'writeBundle' // notice here 74 }) 75 ], 76 resolve: { 77 alias: { 78 '@': path.resolve(__dirname, 'src') 79 } 80 }, 81 server: { 82 host: cfgSetting.spaAddress, 83 port: cfgSetting.spaPort, 84 strictPort: false, //设为true时端口被占用则直接退出,不会尝试下一个可用端口 85 cors: true, //为开发服务器配置CORS, 默认启用并允许任何源 86 open: true, //服务启动时自动在浏览器中打开应用 87 hmr: false, //禁用或配置 HMR 连接 88 //传递给 chockidar 的文件系统监视器选项 89 watch: { 90 ignored:["!**/node_modules/your-package-name/**"] 91 }, 92 proxy: { 93 [cfgSetting.apiType]: { 94 target: cfgSetting.svcAddress, //实际请求地址 95 changeOrigin: true, 96 ws: true, 97 rewrite: (path) => path.replace(cfgSetting.apiType, '') 98 } 99 }, 100 https: cfgSetting.svcIsHttps 101 } 102 })
2.vue-router/index.js
1 import * as vueRouter from 'vue-router'; 2 3 const routes = [ 4 { 5 path: "/", 6 redirect: "/home" 7 }, 8 { 9 path: '/home', 10 name: 'Home', 11 component: () => import("@/views/home/index.vue") 12 } 13 ] 14 15 const router = vueRouter.createRouter({ 16 history: vueRouter.createWebHistory(), 17 routes: routes 18 }) 19 20 export default router
3.vuex/index.js
1 import Vuex from 'vuex' 2 import getters from './getters' 3 4 const store = Vuex.createStore({ 5 modules:{ }, 6 getters 7 }) 8 9 export default store
4.i18n/index.js
1 import { createI18n } from 'vue-i18n' //引入vue-i18n组件 2 import zh_cn from './zh_cn' //中文语言包 3 import en_us from './en_us' //英文语言包 4 5 // 实例化I18n 6 const i18n = createI18n({ 7 legacy: false, 8 globalInjection: true, 9 locale: "zh_cn", // 初始化配置语言 10 messages: { 11 zh_cn, 12 en_us 13 } 14 }) 15 16 export default i18n
5.main.js
1 import * as Vue from 'vue' 2 3 import App from './App.vue' 4 import router from './router/index' 5 import store from './store/index' 6 import i18n from './locales/index' 7 import ElementPlus from 'element-plus' 8 import 'element-plus/theme-chalk/index.css' 9 import zhCn from 'element-plus/es/locale/lang/zh-cn' 10 11 const app=Vue.createApp(App) 12 app.use(store) 13 app.use(router) 14 app.use(i18n) 15 app.use(ElementPlus,{locale:zhCn}) 16 app.mount('#app')
6.configs/settings.js
1 module.exports = { 2 apiType:'/devApi', 3 spaAddress:'127.0.0.1', 4 spaPort: 9000, 5 svcAddress:'http://www.weather.com.cn', 6 svcIsHttps: false 7 }
四、其他信息
1.目录结构
2.兼容性
2.1.由于vite利用了浏览器导模块的能力,所以主流浏览器的最低版本如下:
a).Chrome >=61
b).Firefox >=60
c).Safari >=11
d).Edge >=16
那些古老的浏览器(如IE)就不支持了。
2.2.针对兔民的怀旧情怀,官方也提供了低浏览器版本的解决方案,即使用插件@vitejs/plugin-legacy(https://github.com/vitejs/vite/tree/main/packages/plugin-legacy),使用它之后vite打包出来的程序就可以在低版本浏览器中运行。原理就是利用@babel/preset-env进行转换,自然打包效率就会大幅度降低,因此需要做取舍。
3.代码规范
为了使代码具备更好的可读性和可维护性,以及维护团队成员良好的协作效率,你可以在工程中配置代码规范格式管理约束,具体使用哪种工具和风格自我选择即可。
4.上述内容中完整的App.vue文件内容
1 <template> 2 <img alt="Vue logo" src="@/assets/logo.png" /> 3 <a href="javascript:void(0)" @click="change('zh_cn')">中文</a> -- 4 <a href="javascript:void(0)" @click="change('en_us')">English</a> 5 <div>{{$t("message.Home")}}---{{$t("message.About")}}</div> 6 7 <div> 8 {{weatherinfo.city}} 9 {{weatherinfo.WD}} 10 {{weatherinfo.WS}} 11 </div> 12 13 <div v-for="(item,index) in users"> 14 {{index+1}}-{{item}} 15 </div> 16 17 <el-form :inline="true" :model="formInline" class="demo-form-inline"> 18 <el-form-item label="审批人"> 19 <el-input v-model="formInline.user" placeholder="审批人"></el-input> 20 </el-form-item> 21 <el-form-item label="活动区域"> 22 <el-select v-model="formInline.region" placeholder="活动区域"> 23 <el-option label="区域一" value="shanghai"></el-option> 24 <el-option label="区域二" value="beijing"></el-option> 25 </el-select> 26 </el-form-item> 27 <el-form-item> 28 <el-button type="primary" @click="onSubmit">查询</el-button> 29 </el-form-item> 30 </el-form> 31 32 <router-view /> 33 </template> 34 35 <script> 36 import { useI18n } from 'vue-i18n' 37 import { onMounted, reactive, ref } from "vue"; 38 import axios from "axios" 39 40 export default ({ 41 name: "App", 42 setup() { 43 const { locale } = useI18n() 44 function change(type) { 45 locale.value = type; 46 } 47 48 let weatherinfo = reactive({}) 49 onMounted(()=>{ 50 axios.get(`/devApi/data/sk/101010100.html`).then(res=>{ 51 Object.assign(weatherinfo, res.data.weatherinfo) 52 }).catch(err=>{ 53 console.log(err) 54 }) 55 }) 56 57 let users = ref([]) 58 onMounted(()=>{ 59 axios.get(`/api/getUsers`).then(res=>{ 60 users.value = res.data.data 61 console.log('users', users) 62 }).catch(err=>{ 63 console.log(err) 64 }) 65 }) 66 67 return { 68 change, 69 weatherinfo, 70 users 71 } 72 }, 73 data() { 74 return { 75 formInline: { 76 user: "", 77 region: "", 78 } 79 } 80 }, 81 methods: { 82 onSubmit() { 83 console.log("submit!") 84 } 85 } 86 }) 87 </script>
五、参考信息
1.https://www.cnblogs.com/zsg88/p/15652011.html
2.https://juejin.cn/post/6989475484551610381
3.https://juejin.cn/post/6973288527802925092
4.https://juejin.cn/post/6946007023951544357