• vue开发规范


    本文档主要包括以下是三个部分

    • vue 框架构建及开发规范

    • webpack 打包优化

    • 前端错误日志收集

    1、vue 框架构建及开发规范

    规范目的:

    • 统一编码风格,规范,提高团队协作效率

    • 在团队协作中输出可读性强,易维护,风格一致的代码

    • 本文档主要针对 vue 框架制定的开发规范,其余前端基本规范可参考  http://wiki.ym/pages/viewpage.action?pageId=7867375

    1.1、vue 项目框架搭建

    1.1.1、脚手架构建
    // 1.安装 vue-cli 脚手架构建工具
    npm install --global vue-cli
    // 2.构建于 webpack 模板的一个新项目,填写相关项目信息
    vue init webpack my-project
    // 3.安装项目依赖
    npm install
    1.1.2、项目目录

    按如下文件目录搭建项目框架

     

    src                               主要源码目录
    |-- assets                        静态资源,统一管理
    |-- components                    公用组件,全局组件
    |-- javascript                    JS相关操作处理
        |-- ajax                      axios封装的请求拦截
        |-- filters                   全局过滤器
        |-- utils                     全局封装的工具类
        |-- datas                     模拟数据,临时存放
    |-- router                        路由,统一管理
    |-- store                         vuex, 统一管理
    |-- views                         视图目录
       |-- order                      视图模块名
       |-- |-- orderList.vue          模块入口页面
       |-- |-- orderDetail.vue        模块入口页面
       |-- |-- components             模块通用组件文件夹
    1.1.3、UI 框架选择
    • PC 端:依次推荐使用 antDesign, iView

    • 移动端:依次推荐使用 vant,vux

    1.1.4、css 预处理器

    推荐使用 less,scss , 可在 common.css 设置全局样式,如

    • 常用样式设置原子类名

    • 主题颜色, UI 设计规范等样式

    • 全局组件公共样式

     

    .colorTheme {
      color: #40a9ff !important;
    }
    .fl {
      float: left;
    }
    .fr {
      float: right;
    }
    .clearfix:after {
      clear: both;
      content: '';
      display: block;
       0;
      height: 0;
      visibility: hidden;
    }
    1.1.5、移动端适配
    • 以蓝湖 750px 设计稿。

    • 使用 rem 适配,代码书写 px,用 px2rem-loader 将 px 转化为 rem。

    const px2remLoader = {
      loader: 'px2rem-loader',
      options: {
        remUnit: 100 //1rem=多少像素 这里的设计稿是750px。
      }
    }

    1.2、拆分路由

    在 Vue 项目中使用路由,相信大家都已经很熟悉怎么使用了,要新增一个页面的话,需要到路由配置中配置该页面的信息。

    但是如果页面越来越多的话,那么如何让我们的路由更简洁呢?

    1.2.1、根据不同的业务模块进行路由拆分,在每个子模块导出一个路由配置数组,如 userCard.js 导出会员卡模块的路由,order.js 导出订单模块的路由

     

    const routes = [
      {
        path: '/userCardList',
        component: function(resolve) {
          require(['@/view/userCard/userCardList'], resolve)
        }
      },
      {
        path: '/userCardEdit',
        component: function(resolve) {
          require(['@/view/userCard/userCardEdit'], resolve)
        }
      }
    ]

    1.2.2、在路由根目录在 index.js 中导入所有子模块

    import Vue from 'vue'
    import Router from 'vue-router'
    import userCard from '.userCard'
    import order from './order'
    
    let routes = [...userCard, ...order]
    Vue.use(Router)
    export default new Router({
      mode: 'hash',
      routes: routes
    })

    1.3、axios 请求封装

    1.3.1、设置请求拦截和响应拦截

     

    const PRODUCT_URL = 'https://test-o2o-store-all.iauto360.cn'
    const MOCK_URL = 'http://39.104.49.240:19090'
    let http = axios.create({
      baseURL: process.env.NODE_ENV === 'production' ? PRODUCT_URL : MOCK_URL
    })
    // 请求拦截器
    http.interceptors.request.use(
      config => {
        // 设置token,Content-Type
        var token = sessionStorage.getItem('UserLoginToken')
        config.headers['token'] = token
        config.headers['Content-Type'] = 'application/json;charset=UTF-8'
        // 请求显示loading效果
        if (config.loading === true) {
          vm.$loading.show()
        }
        return config
      },
      error => {
        vm.$loading.hide()
        return Promise.reject(error)
      }
    )
    // 响应拦截器
    http.interceptors.response.use(
      res => {
        vm.$loading.hide()
        // token失效,重新登录
        if (res.data.code === 401) {
          //  重新登录
        }
        return res
      },
      error => {
        vm.$loading.hide()
        return Promise.reject(error)
      }
    )

    1.3.2、封装 get 和 post 请求方法

     

    function get(url, data, lodaing) {
      return new Promise((resolve, reject) => {
        http.get(url)
          .then(
            response => {
              resolve(response)
            },
            err => {
              reject(err)
            }
          )
          .catch(error => {
            reject(error)
          })
      })
    }
    
    function post(url, data, loading) {
      return new Promise((resolve, reject) => {
        http.post(url, data, { loading: loading })
          .then(
            response => {
              resolve(response)
            },
            err => {
              reject(err)
            }
          )
          .catch(error => {
            reject(error)
          })
      })
    }
    
    export { get, post }

    1.3.3、把 get,post 方法挂载到 vue 实例上。

     

    // main.js
    import { get, post } from './js/ajax'
    Vue.prototype.$http = { get, post }

    1.4、工具类函数封装

    1.4.1 添加方法到 vue 实例的原型链上

    export default {
      install (Vue, options) {
        Vue.prototype.util = {
          method1(val) {
            ...
          },
          method2 (val) {
           ...
          },
      }
    }

    1.4.2 在 main.js 通过 vue.use()注册

     

    import utils from './js/utils'
    Vue.use(utils)

    1.5、命名规范

    ——让团队当中其他人看你的代码能一目了然

    1. 文件夹和文件命名以业务或者模块名字为主,驼峰式命名。

    2. 组件命名遵循以下原则,使用驼峰命名(carLib)进行组件声明,使用短横线分隔命名(<car-lib></car-lib>)进行使用。

    3. 当项目中需要自定义比较多的基础组件的时候,比如一些 button,input,icon,建议以一个统一的前缀如 Base 开头,这样做的目的是为了方便查找。

    4. method 方法命名使用驼峰式,动词+名词,如 getData, submitForm

    5. 变量命遵循语义化原则,使用驼峰式。

    1.6、编码规范

    1.6.1、 vue 风格推荐

    Prop 定义应该尽量详细。

    // bad
    props: ['status']
    
    // good
    props: {
      status: String
    }
    
    // better
    props: {
      status: {
        type: String,
        required: true,
        validator: function (value) {
          return ['syncing','synced','version-conflict','error'].indexOf(value) !== -1
        }
      }
    }

    使用 v-for 必须加上 key 值

    <!-- bad -->
    <ul>
      <li v-for="todo in todos">{{ todo.text }}</li>
    </ul>
    
    <!-- good -->
    <ul>
      <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
    </ul>

    不要把 v-if 和 v-for 同时用在同一个元素上。

    <!-- bad -->
    <ul>
      <li v-for="user in users" v-if="shouldShowUsers" :key="user.id">{{ user.name }}</li>
    </ul>
    
    <!-- good -->
    <ul v-if="shouldShowUsers">
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
    
    

    组件的 data 必须是一个函数

     

    // bad
    Vue.component('some-comp', {
      data: {
        foo: 'bar'
      }
    })
    
    // good
    Vue.component('some-comp', {
      data: function() {
        return {
          foo: 'bar'
        }
      }
    })

    组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。

    // bad
    {{
      fullName.split(' ').map(function (word) {
        return word[0].toUpperCase() + word.slice(1)
      }).join(' ')
    }}
    
    //  good
    //  在模板中
    {{ normalizedFullName }}
    // 复杂表达式已经移入一个计算属性
    computed: {
      normalizedFullName: function () {
        return this.fullName.split(' ').map(function (word) {
          return word[0].toUpperCase() + word.slice(1)
        }).join(' ')
      }
    }
    
    

    指令缩写

    <!-- bad -->
    <input v-bind:value="newTodoText" :placeholder="newTodoInstructions" v-on:input="onInput" />
    <!-- good -->
    <input :value="newTodoText" :placeholder="newTodoInstructions" @input="onInput" />

    1.6.2、关于组件内样式

    为组件样式设置作用域

     

    /* bad  */
    <style>
    .btn-close {
      background-color: red;
    }
    </style>
    
    /* good  */
    <style scoped>
    .button-close {
      background-color: red;
    }
    </style>

    若要改变第三方组件库的样式,需要加上顶级作用域。

     

    /* bad */
    .ivu-input {
       254px !important;
    }
    
    /* good */
    .customerForm .ivu-input {
       254px !important;
    }
    /* .customerForm为当前组件的顶级dom  */
    1.6.3、关于组件结构

    组件结构遵循从上往下 template,script,style 的结构。

     

    <template>
      <div></div>
    </template>
    
    <script>
      export default {}
    </script>
    
    <style lang="scss" scoped></style>

    script 部分各方法成员遵循以下顺序放置。

     

    - name
    - components
    - props
    - data
    - methods
    - computed
    - watch
    - created
    - mounted
    - update
    1.6.4、关于注释规范

    以下情况需要加注释,以方便代码维护和他人理解

    • 公共组件使用说明

    • 各组件中重要函数或者类说明

    • 复杂的业务逻辑处理说明

    • 特殊情况的代码处理说明,对于代码中特殊用途的变量、存在临界值、函数中使用的 hack、使用了某种算法或思路等需要进行注释描述。

    • 多重 if 判断语句

    1.6.5、其他规范

    建议不再使用双引号,静态字符串使用单引号,动态字符串使用反引号衔接。

    // bad
    const foo = 'jack'
    const bar = foo + ',前端工程师'
    
    // good
    const foo = 'jack'
    const bar = `${foo},前端工程师`

    使用数组展开操作符 ... 复制数组。

     

    // bad
    const len = items.length
    const itemsCopy = []
    let i
    
    for (i = 0; i < len; i += 1) {
      itemsCopy[i] = items[i]
    }
    
    // good
    const itemsCopy = [...items]

    使用数组对象解构赋值

     

    const arr = [1, 2, 3, 4]
    
    // bad
    const first = arr[0]
    const second = arr[1]
    
    // good
    const [first, second] = arr

    使用对象属性速记语法

     

    const name = 'Luke'
    const age = 20
    
    // bad
    const obj = {
      name: name,
      age: age
    }
    
    // good
    const obj = {
      name,
      age
    }

    1.7、百度统计

    7.1、在 index.html 添加百度统计脚本

     

    var _hmt =
      _hmt ||
      [](function() {
        var hm = document.createElement('script')
        hm.src = 'https://hm.baidu.com/hm.js?d1dbfd0ce8ca70cf72828b2844498250'
        var s = document.getElementsByTagName('script')[0]
        s.parentNode.insertBefore(hm, s)
      })()

    7.2、在全局路由守卫添加需要统计的页面

     

    router.afterEach(to => {
      if (window._hmt) {
        window._hmt.push(['_trackPageview', '/#' + to.fullPath])
      }
    })

    2、webpack 打包优化

    为什么要优化打包?

    • 项目越做越大,依赖包越来越多,打包文件太大

    • 单页面应用首页白屏时间长,用户体验差

    我们的目的

    • 减小打包后的文件大小

    • 首页按需引入文件

    • 优化 webpack 打包时间

    2.1、 按需加载

    2.1.1 路由组件按需加载

     

    const router = [
      {
        path: '/index',
        component: resolve => require.ensure([], () => resolve(require('@/components/index')))
      },
      {
        path: '/about',
        component: resolve => require.ensure([], () => resolve(require('@/components/about')))
      }
    ]

    2.1.2 第三方组件和插件。按需加载需引入第三方组件

     

    // 引入全部组件
    import ElementUI from 'element-ui'
    import 'element-ui/lib/theme-chalk/index.css'
    Vue.use(ElementUI)
    
    // 按需引入组件
    import { Button } from 'element-ui'
    Vue.component(Button.name, Button)

    2.1.3 对于一些插件,如果只是在个别组件中用的到,也可以不要在 main.js 里面引入,而是在组件中按需引入

     

    // 在main.js引入
    import Vue from vue
    import Vuelidate from 'vuelidate'
    Vue.use(Vuelidate)
    
    // 按组件按需引入
    import { Vuelidate } from 'vuelidate'

    2.2、优化 loader 配置

    • 优化正则匹配

    • 通过 cacheDirectory 选项开启缓存

    • 通过 include、exclude 来减少被处理的文件。

     

    module: {
      rules: [
        {
          test: /.js$/,
          loader: 'babel-loader?cacheDirectory',
          include: [resolve('src')]
        }
      ]
    }

    2.3、优化文件路径

    • extension 配置之后可以不用在 require 或是 import 的时候加文件扩展名,会依次尝试添加扩展名进行匹配。

    • mainFiles 配置后不用加入文件名,会依次尝试添加的文件名进行匹配

    • alias 通过配置别名可以加快 webpack 查找模块的速度。

     

      resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
          'vue$': 'vue/dist/vue.esm.js',
          '@': resolve('src'),
        }
      },

    2.4、生产环境关闭 sourceMap

    • sourceMap 本质上是一种映射关系,打包出来的 js 文件中的代码可以映射到代码文件的具体位置,这种映射关系会帮助我们直接找到在源代码中的错误。

    • 打包速度减慢,生产文件变大,所以开发环境使用 sourceMap,生产环境则关闭。

    2.5、代码压缩

    • UglifyJS: vue-cli 默认使用的压缩代码方式,它使用的是单线程压缩代码,打包时间较慢

    • ParallelUglifyPlugin: 开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成

          两种方法使用如下:

     

    plugins: [
      new UglifyJsPlugin({
        uglifyOptions: {
          compress: {
            warnings: false
          }
        },
        sourceMap: true,
        parallel: true
      }),
    
      new ParallelUglifyPlugin({
        //缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回,
        //cacheDir 用于配置缓存存放的目录路径。
        cacheDir: '.cache/',
        sourceMap: true,
        uglifyJS: {
          output: {
            comments: false
          },
          compress: {
            warnings: false
          }
        }
      })
    ]

    打包速度和打包后的文件大小啊对比

    方法文件大小打包速度
    不用插件 14.6M 32s
    UglifyJsPlugin 12.9M 33s
    ParallelUglifyPlugi 7.98M 17s

    2.6、提取公共代码

    • 相同资源重复被加载,浪费用户流量,增加服务器成本。

    • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

    webpack3 使用 CommonsChunkPlugin 的实现:

     

    plugins: [
      new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',
        minChunks: function(module, count) {
          console.log(module.resource, `引用次数${count}`)
          //"有正在处理文件" + "这个文件是 .js 后缀" + "这个文件是在 node_modules 中"
          return module.resource && /.js$/.test(module.resource) && module.resource.indexOf(path.join(__dirname, './node_modules')) === 0
        }
      }),
      new webpack.optimize.CommonsChunkPlugin({
        name: 'common',
        chunks: 'initial',
        minChunks: 2
      })
    ]

    webpack4 使用 splitChunks 的实现:

     

    module.exports = {
      optimization: {
        splitChunks: {
          cacheGroups: {
            vendor: {
              priority: 1, //添加权重
              test: /node_modules/, //把这个目录下符合下面几个条件的库抽离出来
              chunks: 'initial', //刚开始就要抽离
              minChunks: 2 //重复2次使用的时候需要抽离出来
            },
            common: {
              //公共的模块
              chunks: 'initial',
              minChunks: 2
            }
          }
        }
      }
    }

    2.7、CDN 优化

    • 随着项目越做越大,依赖的第三方 npm 包越来越多,构建之后的文件也会越来越大。

    • 再加上又是单页应用,这就会导致在网速较慢或者服务器带宽有限的情况出现长时间的白屏。

    1、将 vue、vue-router、vuex、element-ui 和 axios 这五个库,全部改为通过 CDN 链接获取,在 index.html 里插入 相应链接。

     

    <head>
      <link rel="stylesheet" href="https://cdn.bootcss.com/element-ui/2.0.7/theme-chalk/index.css" />
    </head>
    <body>
      <div id="app"></div>
      <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
      <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
      <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
      <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script>
      <script src="https://cdn.bootcss.com/element-ui/2.6.1/index.js"></script>
      <!-- built files will be auto injected -->
    </body>

    2、在 webpack.config.js 配置文件

     

    module.exports = {
     ···
        externals: {
          'vue': 'Vue',
          'vuex': 'Vuex',
          'vue-router': 'VueRouter',
          'element-ui': 'ELEMENT',
          'Axios':'axios'
        }
      },

    3、卸载依赖的 npm 包,npm uninstall axios element-ui vue vue-router vuex

    4、修改 main.js 文件里之前的引包方式

     

    // import Vue from 'vue'
    // import ElementUI from 'element-ui'
    // import 'element-ui/lib/theme-chalk/index.css'
    // import VueRouter from 'vue-router'
    
    import App from './App.vue'
    import routes from './router'
    import utils from './utils/Utils'
    
    Vue.use(ELEMENT)
    Vue.use(VueRouter)
    
    const router = new VueRouter({
      mode: 'hash', //路由的模式
      routes
    })
    
    new Vue({
      router,
      el: '#app',
      render: h => h(App)
    })

    2.8、使用 HappyPack 多进程解析和处理文件

    • 由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的事情需要一件一件的做,不能多件事一起做。

    • HappyPack 就能让 Webpack 把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

    • HappyPack 对 file-loader、url-loader 支持的不友好,所以不建议对该 loader 使用。

    使用方法如下:

    1、HappyPack 插件安装: npm i -D happypack

    2、webpack.base.conf.js 文件对 module.rules 进行配置

    module: {
      rules: [
        {
          test: /.js$/,
          use: ['happypack/loader?id=babel'],
          include: [resolve('src'), resolve('test')],
          exclude: path.resolve(__dirname, 'node_modules')
        },
        {
          test: /.vue$/,
          use: ['happypack/loader?id=vue']
        }
      ]
    }

    3、在生产环境 webpack.prod.conf.js 文件进行配置

     

    const HappyPack = require('happypack')
    // 构造出共享进程池,在进程池中包含5个子进程
    const HappyPackThreadPool = HappyPack.ThreadPool({ size: 5 })
    plugins: [
      new HappyPack({
        // 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
        id: 'babel',
        // 如何处理.js文件,用法和Loader配置中一样
        loaders: ['babel-loader?cacheDirectory'],
        threadPool: HappyPackThreadPool
      }),
      new HappyPack({
        id: 'vue', // 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
        loaders: [
          {
            loader: 'vue-loader',
            options: vueLoaderConfig
          }
        ],
        threadPool: HappyPackThreadPool
      })
    ]

    3、前端错误日志收集(sentry)

    Sentry 是一个开源的错误追踪工具,可以帮助开发人员实时监控和修复系统中的错误。其专注于错误监控以及提取一切事后处理所需的信息;支持几乎所有主流开发语言(JS/Java/Python/php)和平台, 并提供了 web 来展示输出错误。sentry 官网:https://sentry.io

    sentry 的特性

    • 支持多种语言和框架

    • 相同错误合并

    • 定制规则进行邮件通知

    • 支持导入 sourcemap 自动解析和还原代码

    • 友好的可视化 Web 界面

    3.1、注册账号,新建项目。

    新建项目后,您将获得一个我们称之为 DSN 或数据源名称的值.它看起来很像一个标准的 URL,但它实际上只是 Sentry SDK 所需的配置的标识。它由以下几个部分组成。

    1. 使用的协议: http 或 https;

    2. 验证 sdk 的公钥和密钥;

    3. 目标 sentry 服务器;

    4. 验证用户绑定的项目 id.

    3.2、安装依赖,引入脚本

    import Raven from 'raven-js'
    import RavenVue from 'raven-js/plugins/vue'
    Raven.config('https://a60af910fe4449e98fcf7fbb0c714f1a@sentry.io/1516655')
      .addPlugin(RavenVue, Vue)
      .install()

    常用参数

    • DSN :项目的地址,用于收集错误信息的 sentry 分配的地址

    • debug :是否开启 debug 模式,开启 debug,就会把信息打印到控制台上面

    • release : 代码的版本号,可以确定当前的错误/异常属于哪一个发布的版本

    • environment : 环境名称

    • attachStacktrace : 是否开启堆栈跟踪,开启后跟着消息一起收集

    • beforeSend : 发送前操作

    3.3、基本使用

    最简单的方式是主动触发:

    try {
      doSomething(a[0])
    } catch (e) {
      Raven.captureException(e)
    }

    window.onerror 捕捉异常

    window.onerror = function(e) {
      Raven.captureException(e)
    }

    在 vue 里可以使用 Vue.config.errorHandler 钩子来捕捉

    Vue.config.errorHandler = (err, vm, info) => {
      Raven.captureException(err)
    }

    对于接口报错,可以在全局拦截里实现

    request.interceptors.response.use(null, error => {
      console.error(error)
      Raven.captureException(error)
      return Promise.reject(error)
    })

    3.4、source-map 配置

    • 创建新的 token

    • 安装@sentry/webpack-plugin 插件, 一般会同时安装@sentry/cli

    • npm i @sentry/webpack-plugin --dev
    • 根目录创建.sentryclirc 文件

    [defaults]
    url = https://sentry.io/
    org = your org
    project = your project
    
    [auth]
    token = your token
    • 在 config/prod.env.js 创建环境变量

     

    const release = 'demo-test001' // 可以根据package.json的版本号或者Git的tag命名
    process.env.RELEASE_VERSION = release
    module.exports = {
      NODE_ENV: '"production"',
      RELEASE_VERSION: `"${release}"`
    }
    • 在 webpack.prod.conf 配置

     

    const SentryPlugin = require('@sentry/webpack-plugin')
    new SentryPlugin({
      release: process.env.RELEASE_VERSION, //发布的版本
      include: path.join(__dirname, '../dist/static/js/'), //需要上传到sentry服务器的资源目录
      ignore: ['node_modules', 'webpack.config.js'], //  忽略文件目录
      configFile: `.sentryclirc`, //  面包含了 -o组织 -p项目 以及authtoken
      urlPrefix: '~/static/js' //  线上对应的url资源的相对路径
    })

    3.5、 报警邮件发送规则

    Sentry 默认会将所有采集到的异常发送警报邮件,有时我们可能希望只收到某个版本下某些规则下的警报邮件,这时候就需要删除默认的警报规则,然后新建自定义规则。 在项目设置中找到 Alerts,左上角 “New Alert Rule”即可添加设置报警规则。

    3.6、信息收集

    • 错误类型及具体错误信息

    • 丰富的上下文周围的错误

    • 用户信息(ip, 机型, 操作系统, 浏览器版本, 时间)

    • 错误代码的定位(具体文件,具体行数)

    • 同一个错误发生的次数和用户数

    • 区分错误的环境及版本



  • 相关阅读:
    windows7 下 apache2.4 和 php5.5 及 mysql5.6 的安装与配置
    JavaScript高级编程 (1)
    关于解决乱码问题的一点探索之二(涉及Unicode(utf-16)和GBK)
    关于解决乱码问题的一点探索之一(涉及utf-8和GBK)
    Windows上安装、配置MySQL的常见问题
    解析DXF图形文件格式
    vue三十一:vue配置反向代理
    vue三十:eslint修复错误和打包用于生产
    vue二十九:多个文件组件集成
    vue二十八:Vue-Cli之环境搭建之node安装脚手架和使用脚手架创建vue项目
  • 原文地址:https://www.cnblogs.com/ivan5277/p/12980531.html
Copyright © 2020-2023  润新知