Vue 全家桶、原理及优化简议

Easter79
• 阅读 886

不少互联网公司都在使用vue技术栈,或称为vue全家桶。

使用过vue的程序员一般这样评价它,“vue.js兼具angular.js和react.js的优点”。Vue.js 是一个JavaScript MVVM(Model-View-ViewModel)库,用于渐近式构建用户界面。它以数据驱动和组件化思想构建,采用自底向上增量开发的设计思想。相比Angular.js,Vue.js API更加简洁;相比 React + Redux 复杂的架构,Vue.js 上手更加容易。 

目录

一、vue全家桶包括什么

    vue-router路由

    vuex

    vue-resource

    构建工具vue-cli

    调度工具Devtools

    关于UI组件库

二、vue工程目录结构

    编辑器

三、vue使用简介

    数据代理

    vue实例生命周期图解

四、vue的运行原理

    双向绑定图解

    模板是如何解析的

五、发布前优化

    UI组件按需加载

    路由懒加载

    使用异步组件(动态组件)

    图片压缩与合并

    使用CDN加速vue类库

    压缩代码

    v-for和v-if不要同时使用

    使用Object.freeze冻结大数据

    使用Keep-alive标签优化组件创建

    使用Set

    在scope中少用元素选择器

    关于template的优化

-------------------------------------------------

一、vue全家桶包括什么

vue-router路由

网站:http://router.vuejs.org。使用npm工具来安装vue-router

npm install vue-router 

通过import导入Vue模块、vue-router模块及其它组件。

import Vue from’vue’ 

importRouter from’vue-router’

在使用路由前,必须要通过 Vue.use() 明确地安装路由功能。 

Vue.use(Router)

通过const router= new VueRouter()定义路由,并传入对应的配置,包括路径path和组件components等。

Vue 全家桶、原理及优化简议

在使用newVue来创建和挂载vue根实例的时候,记得要通过 router配置参数注入路由。使用router-link:

Vue 全家桶、原理及优化简议

有两种模式:

  • hash 模式

  • history 模式

vuex

网站:http://vuex.vuejs.org

在vue开发实战中,多个组件共享数据时,单向数据流的简洁性很容易被破坏。为解决多个视图使用同一数据及多个视图驱动同一数据更新的问题,vuex应运而生。

当网站足够大时,一个状态树下,根的部分字段繁多,解决这个问题就要模块化 vuex,官网提供了模块化方案,允许我们在初始化 vuex 的时候配置 modules。每一个 module 里面又分别包含 state 、action 等,看似是多个状态树,其实还是基于 rootState 的子树。细分后整个 state 结构就清晰了,管理起来也方便许多。

Vue 全家桶、原理及优化简议

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 的四个核心概念是: 

  • The state tree:Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个唯一数据源(SSOT)而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。 

  • Getters:用来从 store 获取 Vue 组件数据。 

  • Mutators:事件处理器用来驱动状态的变化。 

  • Actions:可以给组件使用的函数,以此用来驱动事件处理器 mutations。(注:此许或许称之为EventHandler更为恰当。)

Vuex和简单的全局对象是不同的。当Vuex从store中读取状态值的时候,若状态发生了变化,那么相应的组件也会更新。并且改变store中状态的唯一途径就是提交commit mutations。只要发生了状态的变化,一定伴随着mutation的提交。 例如:

Vue 全家桶、原理及优化简议

通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

Vue 全家桶、原理及优化简议

由于 vuex 的灵活性,带来了编码不统一的情况,完整的闭环是 store.dispatch('action') -> action -> commit -> mutation -> getter -> computed,实际上中间的环节有的可以省略,因为 API 文档提供了以下几个方法 mapState、mapGetters、mapActions、mapMutations,然后在组件里可以直接调取任何一步,还是项目小想怎么调用都可以,项目大的时候,就要考虑 vuex 使用的统一性,有人建议是不论多简单的流程都跑完整个闭环,形成代码的统一,方便后期管理,在组件里只允许出现 dispatch 和 mapGetters,其余的流程都在名为 store 的 vuex 文件夹里进行。

注:mapGetters 工具函数会将 store 中的 getter 映射到局部计算属性中。它的功能和 mapState 非常类似。

vue-resource

网站:https://github.com/pagekit/vue-resource

使用npm来安装Vue-resource:

$ npm install vue-resource 

在安装并引入vue-resource后,可以基于全局的Vue对象使用http,也可以基于某个Vue实例使用http。 

Vue 全家桶、原理及优化简议

在发送请求后,使用then方法来处理响应结果,then方法有两个参数,第一个参数是响应成功时的回调函数,第二个参数是响应失败时的回调函数。 

vue-resource的请求API是按照REST风格设计的,它提供了7种请求API: 

· get(url,[options]) 

· head(url,[options]) 

· delete(url,[options]) 

· jsonp(url,[options]) 

· post(url,[body], [options]) 

· put(url, [body],[options]) 

· patch(url,[body], [options])

构建工具vue-cli

vue-cli是vue标准的开发工具。网站:https://cli.vuejs.org/

安装

npm install -g @vue/cli

最新版本为3.4.0。

创建项目

vue create my-project

以上是命令行创建。也可以通过 vue ui 命令以图形化界面创建和管理项目:

vue ui

运行

npm run serve

调度工具Devtools

vue在调试方面,可以选择安装chrome插件vue Devtools。打开vue项目,在调试vue应用的时候,chrome开发者工具中会看一个vue的一栏,点击之后就可以看见当前页面vue对象的一些信息。

Vue 全家桶、原理及优化简议

在Devtools工具中,可以选择组件,查看对应组件内的数据信息。也可以选择Vuex选项,查看该项目内Vuex的状态变量信息。 

Vue 全家桶、原理及优化简议

关于UI组件库

可以自己写,为提高开发效率也可以复用第三方组件库。element(https://github.com/ElemeFE/element)是一个最好支持vue2.0的UI组件库。

二、vue工程目录结构

这是一个简单的vue项目的大概结构:

Vue 全家桶、原理及优化简议

  • components/文件夹:用来存放Vue 组件。个人建议,把每一个组件中使用到的image图片放置到对应的组件子文件目录下,便于统一的管理 

  • Node_modules/:npm安装的该项目的依赖库 

  • vuex/文件夹:存放的是和 Vuex store 相关的东西(state对象,actions,mutations) 

  • router/文件夹:存放的是跟vue-router相关的路由配置项 

  • build/文件:是 webpack 的打包编译配置文件 

  • static/文件夹:存放一些静态的、较少变动的image或者css文件 

  • config/文件夹:存放的是一些配置项,比如服务器访问的端口配置等 

  • dist/该文件夹:一开始是不存在,在我们的项目经过 build 之后才会产出 

  • App.vue根组件,所有的子组件都将在这里被引用 

  • index.html整个项目的入口文件,将会引用我们的根组件 App.vue 

  • main.js入口文件的 js 逻辑,在webpack 打包之后将被注入到 index.html 中

编辑器

VSCode with Vetur

Vue 全家桶、原理及优化简议

三、vue使用简介

数据代理

每个 Vue.js 应用都是通过构造函数 Vue 创建一个 Vue 的根实例 启动的。每个 Vue 实例都会代理其 data 对象里所有的属性:

var data = { a: 1 }

var vm = new Vue({

  data: data

})

vm.a === data.a // -> true

设置新值也会同步影响:

vm.a = 2

data.a // -> 2

// ... 反之亦然

data.a = 3

vm.a // -> 3

实现数据代理的伪代码如下:

var self = this;   // this为vue实例, 即vm

Object.keys(this.data).forEach(function(key) {

    Object.defineProperty(this, key, {    // this.title, 即vm.title

        enumerable: false,

        configurable: true,

        get: function getter () {

            return self.data[key];   //触发对应data[key]的getter

        },

        set: function setter (newVal) {

            self.data[key] = newVal;  //触发对应data[key]的setter

        }

    });

}

Vue 实例暴露了一些有用的实例属性与方法。这些属性与方法都有前缀 $,以便与代理的 data 属性区分。例如:

vm.$data === data // -> true

vm.$el === document.getElementById('example') // -> true

vue实例生命周期图解

Vue 全家桶、原理及优化简议

四、vue的运行原理

Vue采用简洁的模板语法,以声明的方式将数据渲染进 DOM。vue代码是没有办法直接被浏览器解析的,必须经过“编译”,变为浏览器可以识别为html、js与css代码。这种声明式开发方式把方便留给了程序员,转换工作交给了自动化工具。

Vue 全家桶、原理及优化简议

注:el是element的缩写,指Vue实例挂载的元素节点。

双向绑定图解

一般说的双向绑定,指:

  • 数据变动 --> 视图更新

  • 视图更新 --> 数据变动

视图更新 --> 数据变动,这个方向的绑定比较简单。主要通过事件监听来改变数据,比如input控件可以监听input事件,一旦事件触发,调用JS改变data。

Vue 全家桶、原理及优化简议

模型层(model)只是普通 JavaScript 对象,修改它,DOM本是不能更新的。当程序员把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。在每个setter中,可以做许多事件,使表面看起来数据变了,视图就更新了。并且这种数据更新,和原来一样,只是 vm.a=123 这样的简单更新。

Vue 全家桶、原理及优化简议

如上所求,每个vue组件实例都有相应的 watcher 实例对象,它会在vue组件渲染的过程中把需要用到的属性(getter)记录为依赖。之后,当依赖项的 setter 被(其它JS代码)调用时,setter 会通知 watcher 重新计算,从而致使它关联的组件得以更新。

此处实现的是一个观察者模式。

通过object.defineProperty遍历设置this.data里面所有属性,在每个属性的setter里面去通知对应的回调函数,这里的回调函数包括dom视图重新渲染的函数、使用$watch添加的回调函数等,这样我们就通过object.defineProperty劫持了数据,当我们对数据重新赋值时,如this.title = 'hello vue',就会触发setter函数,从而触发dom视图重新渲染的函数,实现数据变动,对应视图更新。

那么,如何在setter里面触发所有绑定该数据的回调函数呢?

既然绑定该数据的回调函数不止一个,我们就把所有的回调函数放在一个数组里面,一旦触发该数据的setter,就遍历数组触发里面所有的回调函数,我们把这些回调函数称为订阅者。数组最好就定义在setter函数的最近的上级作用域中,如下面实例代码所示。

Object.keys(this.data).forEach(function(key) {

    var subs = [];  // 在这里放置添加所有订阅者的数组

    Object.defineProperty(this.data, key, {    // this.data.title

        enumerable: false,

        configurable: true,

        get: function getter () {

            console.log('访问数据啦啦啦')

            return this.data[key];   //返回对应数据的值

        },

        set: function setter (newVal) {

            if (newVal === this.data[key]) {   

                return;    // 如果数据没有变动,函数结束,不执行下面的代码

            }

            this.data[key] = newVal;  //数据重新赋值

            subs.forEach(function () {

                // 通知subs里面的所有的订阅者

            })

        }

    });

}

那么,怎么把绑定数据的所有回调函数放到一个数组里面呢?这是通过gettter内部的代码完成的。

我们知道只要访问数据就会触发对应数据的getter,那我们可以先设置一个全局变量target,如果我们要在data里面title属性添加一个订阅者(changeTitle函数),我们可以先设置target = changeTitle,把changeTitle函数缓存在target中,然后访问this.title去触发title的getter,在getter里面把target这个全局变量的值添加到subs数组里面,添加完成后再把全局变量target设置为null,以便添加其他订阅者。

伪代码如下:

target = changeTitle

...

Object.keys(this.data).forEach(function(key) {

    var subs = [];  // 在这里放置添加所有订阅者的数组

    Object.defineProperty(this.data, key, {    // this.data.title

        enumerable: false,

        configurable: true,

        get: function getter () {

            console.log('访问数据啦啦啦')

            if (target) {

                subs.push(target);                

            }

            return this.data[key];   //返回对应数据的值

        },

        set: function setter (newVal) {

            if (newVal === this.data[key]) {   

                return;    // 如果数据没有变动,函数结束,不执行下面的代码

            }

            this.data[key] = newVal;  //数据重新赋值

            subs.forEach(function () {

                // 通知subs里面的所有的订阅者

            })

        }

    });

}

上面代码中提到的changeTitle,即是上面最近一张图解中的watcher。vue通过getter收集watcher集合。因为vue充许在运行时添加代码,所以该收集行为不能仅限制于模板“编译”之前。(注:vue中是不存在严格的编译的,js是解析执行型语言,像C、Go等语言将源码编译为目标平台的二进制文件,才是真的编译。)

模板是如何解析的

假如说有下面这一段代码,我们怎么把它解析成对应的html呢?

{{title}}

注:该示例实现的效果是,在input输入框内输入任何内容,下方h1文本同步更新。

先简单介绍视图更新函数的用途,比如解析指令v-model="title",v-on:click="changeTitle",还有把{{title}}替换为对应的数据等。

回到上面那个问题,如何解析模板?我们只要去遍历所有dom节点包括其子节点:

  • 如果节点属性含有v-model,视图更新函数就为把input的value设置为title的值

  • 如果节点为文本节点,视图更新函数就为先用正则表达式取出大括号里面的值'title',再设置文本节点的值为data['title']

  • 如果节点属性含有v-on:xxxx,视图更新函数就为先用正则获取事件类型为click,然后获取该属性的值为changeTitle,则事件的回调函数为this.methods['changeTitle'],接着用addEventListener监听节点click事件。

五、发布前优化

使用vue-cli部署生产包时,发现资源包很大,打包后的vendor.js达到了1M+。

UI组件按需加载

如果使用了第三方组件/UI库,如element-ui, mint-ui,echarts等,如果全部引入,项目体积非常大,这时可以按需引入组件。

安装 babel-plugin-component

npm install babel-plugin-component -D

然后,将.babelrc 修改为:

{

  "presets": [["es2015", { "modules": false }]],

  "plugins": [

    [

      "component",

      {

        "libraryName": "element-ui",

        "styleLibraryName": "theme-chalk"

      }

    ]

  ]

}

然后引入部分组件,这样一来,就不需要引入样式了,插件会帮我们处理。

// main.js

import Vue from 'vue'

import { Dialog, Loading } from 'element-ui'

Vue.use(Dialog)

Vue.use(Loading.directive)

Vue.prototype.$loading = Loading.service

// 然后正常使用组件

注:Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码。让不支持ES6的宿主环境,支持使用一套源码开发。

mint-ui是element-ui的移动端组件,所以它的使用和引入几乎和element-ui一样。

路由懒加载

vue-router官方推荐syntax-dynamic-import插件,不过它要求同时安装@bable/core^7.0.0,如果你安装了babel-core6,可能有版本冲突的,解决方法如下:

npm install babel-plugin-syntax-dynamic-import --save-dev(^6.18.0)

当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。

// router.js

const login = () => import('@/components/login')

const router = new VueRouter({

  routes: [

    { path: '/login', component: login }

  ]

})

还有一种魔法注释用法,不推荐使用。

使用异步组件(动态组件)

app bundle 文件过大,可以尝试通过组件懒加载优化。

动态组件主页面加载是不会加载,等到触发条件时才加载该组件,并且加载一次后就有缓存。如果组件在页面加载时不需要,只在调用时用到,这时可以使用异步组件的写法。仅仅是引入和组件注册写法不同:

// template

// script

  components: {

    test: () => import('./test') // 将组件异步引入,告诉webpack,将该部分代码分割打包

  },

  methods:{

      clickTest () {

          this.showTest = !this.showTest

    }

  }

图片压缩与合并

无损压缩图片:https://tinypng.com/。可以将图片制成雪碧精灵图。

使用CDN加速vue类库

一般项目里用到的第三方js库主要有:vue、vue-router、vuex、vue-resource、axio、qiniu等。这些依赖库的js文件被一起打包到vender那个js文件里面,导致vender这个文件很大,那首屏加载速度肯定会被拖慢。

类库文件使用cdn加速

修改 build/webpack.base.conf.js

module.exports = {

context: path.resolve(__dirname, '../'),

entry: {

app: './src/main.js'

},

externals:{

'vue': 'Vue',

'vue-router': 'VueRouter',

'vuex':'Vuex',

'vue-resource': 'VueResource'

}

排除已经手动收入的js文件

利用webpack的externals。具体做法就是在 build/webpack.base.conf.js文件的module里面与rules同层加入externals。具体做法,修改src/main.js src/router/index.js 注释掉import引入的vue,vue-resource等:

// import Vue from 'vue'

// import VueResource from 'vue-resource'

// Vue.use(VueResource)

上面已经引用过。

压缩代码

vue-cli已经使用UglifyJsPlugin 插件来压缩代码,可以设置成如下配置:

new webpack.optimize.UglifyJsPlugin({

  compress: {

    warnings: false,

    drop_console: true,

    pure_funcs: ['console.log']

  },

  sourceMap: false

})

其中sourceMap: false是禁用除错功能。如果设为true,在部署包中会生成.map结尾的js文件。它用于在代码混淆压缩的情况下仍可进行调试。这个功能虽好,但会大大增加整体资源包的体积,所以将其禁用。

v-for和v-if不要同时使用

在vue中v-for和v-if不要放在同一个元素上使用。由于 v-for 和 v-if 放在同一个元素上使用会带来一些性能上的影响,在计算属性上过滤之后再进行遍历。反例:

Vue 全家桶、原理及优化简议

使用Object.freeze冻结大数据

对于前端纯大数据展示(纯大数据指:拿到数据就是直接用于展示的,不需要做修改其中字段等处理的,而且数据量比较大)的情况下,使用Object.freeze方法来包裹变量,那边vue内部不会使用defineproperty去监听数据内部的变化,只有本身变化时才会触发,在大量数据的情况下,vue内部不在去监听数据的变化会提高性能。使用demo如下:

Vue 全家桶、原理及优化简议

使用Keep-alive标签优化组件创建

vue提供了keep-alive标签来存储缓存,对于一些视频控件object或图表类的使用,我们经常会使用v-if指令,而v-if是会创建和销毁的,如果频繁操作在ie下的内存会持续上升,而keep-alive可以有效的缓存,抑制内存的持续上升。

见:https://cn.vuejs.org/v2/api/#keep-alive

使用Set

Es6集合Set()可优化遍历速度,set集合是可用于查找该集合内是否存在某个元素。但如果使用了Bable自动转化,该优化无效。

在scope中少用元素选择器

scope中元素选择器尽量少用。在 scoped 样式中,类选择器比元素选择器更好,因为大量使用元素选择器是很慢的。

为了给样式设置作用域,Vue 会为元素添加一个独一无二的特性,例如 data-v-f3f3eg9。然后修改选择器,使得在匹配选择器的元素中,只有带这个特性才会真正生效 (比如 button[data-v-f3f3eg9])。问题在于大量的元素和特性组合的选择器 (比如 button[data-v-f3f3eg9]) 会比类和特性组合的选择器 慢,所以应该尽可能选用类选择器。

关于template的优化

v-show,v-if 用哪个?在我来看要分两个维度去思考问题,第一个维度是权限问题,只要涉及到权限相关的展示无疑要用 v-if,第二个维度在没有权限限制下根据用户点击的频次选择,频繁切换的使用 v-show,不频繁切换的使用 v-if,这里要说的优化点在于减少页面中 dom 总数,我比较倾向于使用 v-if,因为减少了 dom 数量,加快首屏渲染,至于性能方面我感觉肉眼看不出来切换的渲染过程,也不会影响用户的体验。

不要在模板里面写过多的表达式与判断 v-if="isShow && isAdmin && (a || b)",这种表达式虽说可以识别,但是不是长久之计,当看着不舒服时,适当的写到 methods 和 computed 里面封装成一个方法,这样的好处是方便我们在多处判断相同的表达式,其他权限相同的元素再判断展示的时候调用同一个方法即可。

循环调用子组件时添加 key,key 可以唯一标识一个循环个体,可以使用例如 item.id 作为 key,假如数组数据是这样的 ['a' , 'b', 'c', 'a'],使用 :key="item" 显然没有意义,更好的办法就是在循环的时候 (item, index) in arr,然后 :key="index"来确保 key 的唯一性。

2019年2月14日

--------------------------------------------------------

参考资料:

本文分享自微信公众号 - 程序员LIYI(CoderLIYI)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
2年前
Vue 全家桶
vue全家桶。使用过vue的程序员一般这样评价它,“vue.js兼具angular.js和react.js的优点”。Vue.js是一个JavaScriptMVVM(ModelViewViewModel)库,用于渐近式构建用户界面。它以数据驱动和组件化思想构建,采用自底向上增量开发的设计思想。相比Angular.js,Vue.jsAPI更加简洁;
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
2年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k