首页

uni-app提交表单到后端,接收表单数据

seo达人

要想接收表单数据,首先要在表单进行数据的绑定,我们可以使用v-model="keyword"进行绑定。


然后在js获取这个绑定的值。


index.vue


<template>

<view>

   <view class="search-con">

           <view class="form-con">

           <form class="search-form">

               <input type="text" v-model="keyword" @tap="showsearchbtn" focus="true"/>

               <button form-type="submit" hover-class='none' @tap="keyword">提交</button>  

           </form>

           </view>

   </view>

</view>

<template>

js


<script>

export default {

   data() {

       return {


           }

       },

       methods: {

           keyword(e){

               // 获取表单值

               let kw = this.keyword;

               console.log(kw);

           }

       }

   }

</script>

Author:TANKING

Web:http://www.likeyun.cn

Date:2020-8-13

WeChat:face6009

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

滴滴出行小程序I18n最佳实践

seo达人

背景

I18n = Internationalization,国际化,因为单词由首末字符i/n和中间18个字母组成,简称i18n。对程序来说,就是要在不修改内部代码的情况下,能根据不同语言及地区显示相应的界面,以支持不同语言的人顺利使用程序。

业务背景

互联网行业进入下半场,精细化运营是关键。多语言支持能让产品更好地服务境内的其他语言用户,也为产品出海打下基础,随着 WeChat/Alipay 的全球化,你的小程序是否做好准备了呢?

4月初,滴滴出行小程序团队接到支持英文版的需求,预计上线时间为6月上旬。当前滴滴出行小程序集成的众多业务线和各种公共库,展示给用户的有前端硬编码的静态文本和服务端下发的文案,都要同步接入多语言。考虑到小程序当前的体量,光文本收集、语料翻译、npm package 支持,联调,测试,沟通成本等等,并且前端开发只投入1.5人力的情况下,时间是蛮紧迫的,但是我们抗住了压力,最终英文版滴滴出行小程序如期上线,截止目前运行稳定,用户反馈良好,得到了超出预期的收益。

当然这一切得益于各团队同学的工作,和各团队的通力配合,更得益于部门技术团队 Mpx框架优雅的多语言能力支持。划重点来咯,所谓工欲善其事必先利其器,如果你的公司业务需要开发小程序,也需要接入多语言,那么请搬好小板凳,我们来看一下小程序框架 Mpx 是如何优雅支持多语言能力。相信看完这篇,可以帮助你认识 Mpx(https://github.com/didi/mpx) ,加深对框架的理解,最终利用 Mpx 框架迭代小程序,年终奖多出那部分可以打赏一下作者,买杯咖啡哈(偷笑.jpg)

以下是滴滴出行小程序的中英文版本对比:

滴滴出行微信小程序i18n

也欢迎大家在微信/支付宝里搜索滴滴出行小程序,实际使用感受下。PS:切换语言的方法是,打开小程序,点击左上角用户头像,进入侧边栏设置页面,点击切换中英文即可体验。

技术背景

在上述业务背景下,Mpx 框架——滴滴自研的专注提升小程序开发体验的增强型小程序框架,内建 i18n 能力便提上日程。

与 WEB 不同,小程序(本文以微信小程序为例)运行环境采用双线程架构设计,渲染层的界面使用 WebView 进行渲染,逻辑层采用 JSCore 线程运行 JS脚本。逻辑层数据改变,通过 setData 将数据转发到 Native(微信客户端),Native 再将数据转发到渲染层,以此更新页面。由于线程间通信成本较高,实际项目开发时需要控制频次和数量。另外小程序的渲染层不支持运行 JS ,一些如事件处理等操作无法在渲染层实现,因此微信官方提供了一套脚本语言 WXS ,结合 WXML ,可以构建出页面的结构(不了解 WXS ?戳这里)。

基于小程序的双线程架构设计,实现 i18n 存在一些技术上的难点与挑战,由于 Mpx 框架早期构建起来的强大基础,最终得以优雅支持多语言能力,实现了和vue-i18n 基本一致的使用体验。

使用

在使用上,Mpx 支持 i18n 能力提供的 API 与 vue-i18n 大体对齐,用法上也基本一致。

模板中使用 i18n

编译阶段通过用户配置的 i18n 字典,结合框架内建的翻译函数通过 wxs-i18n-loader 合成为可执行的 WXS 翻译函数,并自动注入到有翻译函数调用的模板中,具体调用方式如下图。

// mpx文件 <template> <view> <view>{{ $t('message.hello', { msg: 'hello' })}}</view> 

<!-- formattedDatetime计算属性,可基于locale变更响应刷新 --> <view>{{formattedDatetime}}</view> </view> </template>

JS 中使用 i18n

通过框架提供的 wxs2js 能力,将 WXS 翻译函数转换为 JS 模块注入到 JS 运行时,使运行时环境中也能够调用翻译函数。

// mpx文件 <script> import mpx, { createComponent } from '@mpxjs/core' createComponent({ 

ready () { // js中使用 console.log(this.$t('message.hello', { msg: 'hello' }))

// 局部locale变更,生效范围为当前组件内 this.$i18n.locale = 'en-US' setTimeout(() =>

{ // 全局locale变更,生效范围为项目全局 mpx.i18n.locale = 'zh-CN' }, 10000)

}, computed: { formattedDatetime () { return this.$d(new Date(), 'long') } } }) </script>

定义 i18n 字典

项目构建时传入 i18n 配置对象,主要包括语言字典和默认语言类型。

new MpxWebpackPlugin({ i18n: { locale: 'en-US',

// messages既可以通过对象字面量传入,也可以通过messagesPath指定一个js模块路径,

在该模块中定义配置并导出,dateTimeFormats/dateTimeFormatsPath和numberFormats/numberFormatsPath同理

messages: { 'en-US': { message: { hello: '{msg} world' }

}, 'zh-CN': { message: { hello: '{msg} 世界' } } }, // messagesPath: path.resolve(__dirname, '../src/i18n.js') } })

如果是通过 Mpx 提供的 cli 工具生成的项目,这部分配置会在 mpx.conf.js 文件中,不光可以直接内联写在该文件中,也可以指定语言包的路径。

以上,Mpx 的 i18n 方案接入成本低,使用优雅,体验优秀。直观感受可参考下面 mpx i18n demo :https://github.com/didi/mpx/t...

方案

Mpx框架的 i18n 支持几乎完全实现了 vue-i18n 的全部能力,下面我们来详细说明 Mpx 框架 i18n 能力的具体实现。

方案探索

基于小程序运行环境的双线程架构,我们尝试了不同方案,具体探索过程如下:

方案一:基于 Mpx 框架已提供的数据增强能力 computed 计算属性,来支持 i18n 。该方案与 uniapp 的实现思路相似(后文会进行对比分析),存在一定不足,包括线程通信带来的性能开销和for循环场景下的处理较复杂等,最终放弃。
方案二:基于 WXS + JS 支持 i18n 适配。通过视图层注入 WXS,将 WXS 语法转换为 JS 后注入到逻辑层,这样视图层和逻辑层均可实现 i18n 适配,并且在一定程度上有效减少两个线程间的通信耗时,提高性能。

从性能和合理性上考虑,我们最终采用了方案二进行 Mpx 的 i18n 方案实现。

mpx-i18n内部流程示意图

Mpx i18n 架构设计图

由于各大小程序平台上,WXS 语法和使用均存在较大差异,因此该方案实现过程中也存在一些技术上的难点,这些难点基于 Mpx 框架的早期构建起来的跨平台能力也一一得以攻克,具体如下。

实现难点

WXS 在模板中运行的跨平台处理

WXS 是运行在视图层中的 JS,可以减少与逻辑层通信耗时,提高性能。因此 Mpx 框架在迭代初期便已支持在模板和 JS 运行环境使用 WXS 语言,并且针对小程序跨平台 WXS 语法进行抹平。
在模板中,Mpx 自定义一个 webpack chunk template,以微信 WXS 作为 DSL,利用 babylon 将注入的 WXS 转化成 ast,然后遍历 ast 节点,抹平各大平台对 WXS 语法的处理差异,输出各平台可以识别的类 WXS 文件。目前主要支持微信(WXS)、支付宝(sjs)、百度(filter)、QQ(qs)、头条(sjs)等小程序平台。

WXS 在逻辑层运行的跨平台处理

WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。并且 WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。
因此在逻辑层,Mpx 将注入的 WXS 语法转化为 JS,通过 webpack 注入到当前模块。例如 WXS 全局方法 getRegExp/getDate 在 JS 中是无法调用的,Mpx将它们分别转化成 JS 模块,再通过 webpack addVariable 将模块注入到 bundle.js 中。
同理,Mpx 会将编译时注入的 i18n wxs 翻译函数和 i18n 配置对象挂载到全局 global 对象上,利用 mixin 混入到页面组件,并监听 i18n 配置对象,这样JS和模板中即可直接调用 i18n 翻译函数,实现数据响应。

以上便是 Mpx 框架在小程序中支持 i18n 能力的技术细节,由于 WXS 是可以在视图层执行的类 JS 语法的一门语言,这样就减少了小程序逻辑层和视图层的通信耗时,提升性能。但是由于实现依赖类 WXS 能力,以及 WXS 执行环境的限制,目前模板上可直接使用的翻译函数包括 $t/$tc/$te ,如果需要格式化数字或日期可以使用对应的翻译函数在 JS 中 Mpx 提供的计算属性中实现。

输出 web 时使用 i18n

Mpx同时还支持转换产出H5,而 Mpx 提供的 i18n 能力在使用上与 vue-i18n 基本一致,输出 web 时框架会自动引入 vue-i18n,并使用当前的 Mpx i18n 配置信息对其进行初始化,用户无需进行任何更改,即可输出和小程序表现完全一致的 i18n web 项目。

对比

上面分析了 Mpx 框架的 i18n 方案的技术细节,我们来看下和其他方案的对比,主要是和 uniapp - 基于 Vue 编写小程序的方案,和微信官方的方案,两者提供的 i18n 支持与Mpx的对比有何优劣。

uniapp的方案

uniapp 提供了对 i18n 能力的支持,是直接引入vue-i18n。但小程序中无法在模板上调用 JS 方法,本质上是利用计算属性 Computed 转换好语言,然后利用模板插值在小程序模板中使用。

模板中:
<view>{{ message.hello }}</view>

JS里需要写:

 computed: {  
    message () { return { hello: this.$t('message.hello') }
    }
  }

因此该方案存在一个性能问题,最终的渲染层所看到的文本还是通过 setData 跨线程通信完成,这样就会导致线程间通信增多,性能开销较大。

并且,早期这种形式使用成本较高,后来 uniapp 也针对其做过优化,实现了可以在模板上写 $t() 的能力,使用上方便了不少。

这个 t() 的实现是在编译时候识别到t 就自动替换,帮你替换成一个 uniapp 的 computed 数据,因此数据部分还是和之前一样要维护两份。尤其是模板上的for循环,即使 for 里只有一个数据要被转换,整个列表都要被替换成一个计算属性,在线程间通信时进一步加大了性能开销。

微信官方的方案

微信小程序本身也提供了一个 i18n 的方案,仓库地址是:wechat-miniprogram/miniprogram-i18n 。

这个方案从 i18n 本身的实现来讲和Mpx框架的设计是类似的,也是基于 WXS 实现(英雄所见略同啊)。但因为周边配套上没有完整的体系,整体使用体验上就也略逊于基于Mpx框架来开发支持 i18n 的国际化小程序了。

主要的点就是,官方提供的方案,要基于 gulp 工具进行一次额外构建,同时在JS中使用时候还要额外引入一个 behavior 去让JS中也可以使用翻译能力。

而Mpx框架通过一次统一的Webpack构建产出完整的内容,用户无需担心语言包更新后忘记重新构建,在JS中使用的时候不光更方便,而且语言信息还是个响应式的,任何组件都可以很方便地监听语言值的变化去做一些其他的事情。

最后,Mpx的 i18n 方案对比微信官方的方案还有个巨大的优点,结合Mpx的跨平台能力,能实现均以这个方案,一套代码产出支持微信/支付宝/百度/QQ/头条多个平台的支持 i18n 的小程序。

总结

Mpx 框架专注小程序开发,期望为开发者提供最舒适的开发体验,有众多优秀的功能特性,帮助开发者提效。本文介绍的是其内置的 i18n 能力,通过对比分析得出相比其他框架方案在使用成本和性能等方面有明显的优势,欢迎各位有相关需求的同学进行体验尝试。

未来 Mpx 还会持续迭代优化,提供更多更好的能力帮助小程序开发者提效。在使用过程中遇到任何问题,欢迎大家在 Git 上提 issue,团队成员会及时响应。同时也鼓励大家一起为开源社区做贡献,参与到 Mpx 共建中来,为小程序技术发展添砖加瓦。

Git地址 [https://github.com/didi/mpx]
Mpx文档 [https://mpxjs.cn/]

欢迎技术交流与反馈,顺便star一下鼓励开源项目贡献者,我们将持续发力贡献社区。

附:以往Mpx文章链接
滴滴开源小程序框架Mpx - https://mpxjs.cn/articles/1.0.html
滴滴小程序框架Mpx发布2.0,支持小程序跨平台开发,可直接转换已有微信小程序 - https://mpxjs.cn/articles/2.0.html
小程序开发者,为什么你应该尝试下MPX - https://mpxjs.cn/articles/mpx1.html
Mpx 小程序框架技术揭秘 - https://mpxjs.cn/articles/mpx2.html

滴滴出行小程序体积优化实践 - https://mpxjs.cn/articles/size-control.html

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

JavaScript专题之深浅拷贝(系列五)[转载]

前端达人

JavaScript专题之深浅拷贝

了解拷贝背后的过程,避免不必要的错误,Js专题系列之深浅拷贝,我们一起加油~

目录

一、拷贝示例

当我们在操作数据之前,可能会遇到这样的情况:

  1. 会经常改动一组数据,但可能会用到原始数据
  2. 我需要两组一样的数据,但我不希望改动一个另一个随之改动
  3. 我需要对数据操作前后进行对比

当我们遇到类似需要场景时,首先想到的就是拷贝它,殊不知拷贝也大有学问哦~

下面简单的例子,你是否觉得熟悉?

1.1 基本类型拷贝示例
var str = 'How are you'; var newStr = str; newStr = 10 console.log(str); // How are you console.log(newStr); // 10 

大家都能想到,字符串是基本类型,它的值保存在栈中,在对它进行拷贝时,其实是为新变量开辟了新的空间。 strnewStr就好比两个一模一样的房间,布局一致却毫无关联。

1.2 引用类型拷贝示例
var data = [1, 2, 3, 4, 5]; var newData = data; newData[0] = 100; console.log(data[0]); // 100 console.log(newData[0]); // 100 

类似的代码段,但这次我们使用数组这个引用类型举例,你会发现修改赋值后的数据,原始数据也跟着改变了,这显然不满足我们的需要。本篇文章就来聊一聊引用数据拷贝的学问

如果大家对Js的数据类型存在着疑问,不妨看看《JavaScript中的基本数据类型》

在这里插入图片描述

二、浅拷贝

拷贝的划分都是针对引用类型来讨论的,浅拷贝——顾名思义,浅拷贝就是“浅层拷贝”,实际上只做了表面功夫:

var arr = [1, 2, 3, 4]; var newArr = arr; console.log(arr, newArr); // [1,2,3,4] [1,2,3,4] newArr[0] = 100; console.log(arr, newArr) // [100,2,3,4] [100,2,3,4] 

不发生事情(操作)还好,一旦对新数组进行了操作,两个变量中所保存的数据都会发生改变。

发生这类情况的原因也是因为引用类型的基本特性:

  • 存储在变量处的值是一个指针(point),指向存储对象的内存地址。赋值给新变量相当于配了一把新钥匙,房间并没有换。

数组中的slice和concat都会返回一个新数组,我们一起来试一下:

var arr = [1,2,3,4]; var res = arr.slice(); // 或者 res = arr.concat() res[0] = 100; console.log(arr); // [1,2,3,4] 

这个问题这么快就解决了?虽然对这一层数据进行了这样的的处理后,确实解决了问题,但!

var arr = [ { age: 23 }, [1,2,3,4] ]; var newArr = arr.concat(); arr[0].age = 18; arr[1][0] = 100; console.log(arr) // [ {age: 18}, [100,2,3,4] ] console.log(newArr) // [ {age: 18}, [100,2,3,4] ] 

果然事情没有那么简单,这也是因为数据类型的不同。

S 不允许我们直接操作内存中的地址,也就是说不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。

既然浅拷贝达不到我们的要求,本着效率的原则,我们找找有没有帮助我们实现深拷贝的方法。

在这里插入图片描述

三、深拷贝的方法?

数据的方法失败了,还有没有其他办法?我们需要实现真正意义上的拷贝出独立的数据。

3.1 JSON

这里我们利用JSON的两个方法,JSON.stringify()JSON.parse()来实现最简洁的深拷贝

var arr = ['str', 1, true, [1, 2], {age: 23}] var newArr = JSON.parse( JSON.stringify(arr) ); newArr[3][0] = 100; console.log(arr); // ['str', 1, true, [1, 2], {age: 23}] console.log(newArr); // ['str', 1, true, [100, 2], {age: 23}] 

这个方法应该是实现深拷贝最简洁的方法,但是,它仍然存在问题,我们先来看看刚才都做了些什么:

  1. 定义一个包含都过类型的数组arr
  2. JSON.stringify(arr), 将一个 JavaScript 对象或值转换为 JSON 字符串
  3. JSON.parse(xxx), 方法用来解析JSON字符串,构造由字符串描述的值或对象

理解:

我们可以理解为,将原始数据转换为新字符串,再通过新字符串还原为一个新对象,这中改变数据类型的方式,间接的绕过了拷贝对象引用的过程,也就谈不上影响原始数据。

限制:

这种方式成立的根本就是保证数据在“中转”时的完整性,而JSON.stringify()将值转换为相应的JSON格式时也有缺陷:

  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
  • 函数、undefined 被单独转换时,会返回 undefined,
    • 如JSON.stringify(function(){})
    • JSON.stringify(undefined)
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
  • NaN 和 Infinity 格式的数值及 null 都会被当做 null。
  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。

所以当我们拷贝函数、undefined等stringify转换有问题的数据时,就会出错,我们在实际开发中也要结合实际情况使用。

举一反三:

既然是通过改变数据类型来绕过拷贝引用这一过程,那么单纯的数组深拷贝是不是可以通过现有的几个API来实现呢?

var arr = [1,2,3]; var newArr = arr.toString().split(',').map(item => Number(item)) newArr[0] = 100; console.log(arr); // [1,2,3] console.log(newArr); // [100,2,3] 

注意,此时仅能对包含纯数字的数组进行深拷贝,因为:

  1. toString无法正确的处理对象和函数
  2. Number无法处理 false、undefined等数据类型

但我愿称它为纯数字数组深拷贝

在这里插入图片描述

3.2 Object.assign()

有的人会认为Object.assign(),可以做到深拷贝,我们来看一下

var obj = {a: 1, b: { c: 2 } } var newObj = Object.assign({}, obj) newObj.a = 100; newObj.b.c = 200; console.log(obj); // {a: 1, b: { c: 200 } } console.log(newObj) // {a: 100, b: { c: 200 } } 

神奇,第一层属性没有改变,但第二层却同步改变了,这是为什么呢?

因为 Object.assign()拷贝的是(可枚举)属性值。

假如源值是一个对象的引用,它仅仅会复制其引用值。MDN传送门

四、自己实现深浅拷贝

既然现有的方法无法实现深拷贝,不妨我们自己来实现一个吧~

4.1 浅拷贝

我们只需要将所有属性即其嵌套属性原封不动的复制给新变量一份即可,抛开现有的方法,我们应该怎么做呢?

var shallowCopy = function(obj) { if (typeof obj !== 'object') return; // 根据obj的类型判断是新建一个数组还是对象 var newObj = obj instanceof Array ? [] : {}; // 遍历obj,并且判断是obj的属性才拷贝 for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; } 

我们只需要将所有属性的引用拷贝一份即可~

4.2 深拷贝

相信大家在实现深拷贝的时候都会想到递归,同样是判断属性值,但如果当前类型为object则证明需要继续递归,直到最后

var deepCopy = function(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; } } return newObj; } 

我们用白话来解释一下deepCopy都做了什么

const obj = [1, { a: 1, b: { name: '余光'} } ]; const resObj = deepCopy(obj); 
  • 读取 obj,创建 第一个newObj
    • 判断类型为 []
    • key为 0 (for in 以任意顺序遍历,我们假定按正常循序遍历)
    • 判断不是引用类型,直接复制
    • key为 1
    • 判断是引用类型
    • 进入递归,重新走了一遍刚才的流程,只不过读取的是obj[1]

另外请注意递归的方式虽然可以深拷贝,但是在性能上肯定不如浅拷贝,大家还是需要结合实际情况来选择。




作者: 

转自:https://blog.csdn.net/jbj6568839z/article/details/107964274?utm_medium=distribute.pc_category.none-task-blog-hot-4.nonecase&depth_1-utm_source=distribute.pc_category.none-task-blog-hot-4.nonecase&request_id=

最详细完整的flex弹性布局

前端达人

初了解

在学习弹性布局之前首先就要明白其概念
flex 就是flexible box的缩写,意为弹性布局,用来为盒装模型提供最大的灵活性
任何一个容器都可以指定为flex布局

.box{ display: flex; } 

行内元素当然也可以使用flex布局

.box{ display: inline-flex; } 

Webkit 内核的浏览器,必须加上-webkit前缀。

.box{ display: -webkit-flex; /* Safari */ display: flex; } 

注意:设为 Flex 布局以后,子元素的float、clear和vertical-align属性将失效。

基本概念

采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。
在这里插入图片描述
容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。
项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size。
以上这些基础概念,请务必牢记,下面说属性时,不再重复说明!

属性

容器属性(container)

  • flex-direction
  • justify-content
  • align-items
  • flex-wrap
  • align-content
  • flex-flow

1.flex-direction

 flex items默认都是沿着main axis(主轴)从main start 开始往main end方向排布
    flex-direction决定了main axis的方向,有四个取值row(默认值)、row-reverse、column、column-reverse 
.box { flex-direction: row | row-reverse | column | column-reverse; } 

row(默认值):主轴为水平方向,起点在左端。
row-reverse:主轴为水平方向,起点在右端。
column:主轴为垂直方向,起点在上沿。
column-reverse:主轴为垂直方向,起点在下沿。

2.justify-content

 justify-content决定了flex item在main axis上的对齐方式

    flex-start(默认值):与main start对齐
    flex-end:与main end对齐
    center:居中
    space-between:flex items 之间的距离相等,与main start、main end两端对齐
    space-evently: flex items 之间的距离相等,flex items与main start 、main end 之间的距离等于flex items之间的距离
    space-around :flex items 之间的距离相等,flex items与main start 、main end 之间的距离等于flex items之间的距离的一半 

这个属性的目的主要就是为了排列main axis的item位置
在这里插入图片描述

在这里插入图片描述
当然,这些属性你可以自己尝试一下,这里就不再一一尝试了,但是注意,这些都是容器的属性,要写在容器的css中!

3.align-items

 决定flex items在cross axis上的对齐方式

    normal:在弹性布局中,效果和stretch一样
    stretch:前提是items不设置高度,当flex items 在cross axis 方向的size为auto时,会自动拉伸至填充flex container(或者换句话说:如果项目未设置高度或设为auto,将占满整个容器的高度。)
    flex-satrt:与cross start 对齐
    flex-end:与cross end 对齐
    center:居中对齐
    baseline:与基准线对齐 

4.flex-wrap

 决定了flex container 是单行还是多行
    nowrap(默认):单行
    warp:多行
    //这个比较少用
    wrap-reverse:多行(对比wrap,cross start 与cross end相反) 

默认情况下,项目都排在一条线(又称"轴线")上。flex-wrap属性定义,如果一条轴线排不下,如何换行。

5 align-content

 决定了多行flex items 在cross axis的对齐方式 用法与justify-content相似 一个是横轴。一个控制竖轴
    stretch(默认值):与align-items的stretch类似,当items有高度的时候,无效果
    flex-start:与cross start 对齐
    flex-end :与cross end 对齐
    center:居中对齐
    space-between:flex items 之间的距离相等,与cross start、cross end两端对齐
    space-evently: flex items 之间的距离相等,flex items与cross start 、cross end 之间的距离等于flex items之间的距离
    space-around :flex items 之间的距离相等,flex items与cross start 、cross end 之间的距离等于flex items之间的距离的一半 

6 flex-flow 是flex-direction与flex-wrap的简写

也就是说,当你使用这个属性的时候,你可以使用上述两个的属性值,例如:flex-flow: row wrap;(水平排列,多行显示)

flex 项目属性(item属性)

  • order
  • flex-grow
  • flex-shrink
  • flex-basis
  • align-self
  • flex

1 order

 order 决定flex items的排布顺序  (用的不多)
    可以设置为任意整数(正整数、负整数、0),值越小越排在前面
    默认值为0 

这个属性了解即可,说实话没怎么用过

2 align-self

 可以通过align-self 覆盖flex container 设置的align-items
    auto(默认值):遵从flex container的align-items设置
    stretch、flex-start、flex-end、center、baseline效果与align-items一致 

相当于继承父元素的align-items属性,如果没有父元素,则等同于stretch。

3 flex-grow

 决定了flex items如何扩展
    可以设置为任意非父数字(小数,整数 0),默认为0
    当flex container 在main axis方向上有剩余得size时,flex-grow属性才会有效

    如果所有flex items 的flex-grow 综合sum不超过1,这直接乘以剩余size就是扩展大小、
    如果超过1 扩展size=剩余size*flex-grow/sum 

flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。

4 flex-shrink

flex-shrink (shrink 缩小,收缩)与flex-grow相似,一个扩展,一个伸缩 
    可以设置为任意非父数字(小数,整数 0),默认为1
    当flex items在main axis 方向上超过了flex container 的size flex-shrink属性才会生效、
    如果所有flex items 的flex-shrink 总和sum超过1,每个flex item 收缩的size为:
        flex item 超出flex container 的size*收缩比例/每个flex items 的收缩比例之和
    如果sum不超过1,每个flex item 收缩的size为:
        size = 超出的size * flex-shrink值
    flex items收缩后的最终size不能小于min-width\min-height 

有扩大自然就会有缩小,flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。具体的可以自己动手尝试一下哦,最后将会给出一个骰子布局的案例!

5 flex-basis

用来设置flex items 在 main axis方向上的base size
    默认为auto,可以设置具体的宽度数值

    决定flex items最终base size 的因素,优先级从高到低
        max-width\max-height\min-width\min-height
        flex-basis
        width\height
        内容本身的size 

flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目(item)的本来大小。也可以设置跟width,height一样的宽高,表示item将占据固定的空间!

6 flex

flex 是flex-grow || flex-shink||flex-basis的简写
可以指定1 2 3个值 依次按照上述顺序!默认值为 0 1 auto 
.item { flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ] } 

注意:

  1. 该属性的默认值为 0 1 auto(注意顺序),后两个属性可选
  2. 该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。
  3. 如果需要这三个属性的时候,建议使用flex,而不是单独的三个分离的属性,因为浏览器会推算相关值

骰子布局实践

光说不练假把式,手撕代码真功夫!
下面利用flex写了几个骰子布局,可以参考一下!

 
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <style type="text/css">
            #container{
                background-color: #CCCCCC;
                height: 600px;
                width: 500px;
                /* flex */
                display: flex;
                justify-content: space-evenly;
                align-items: center;
            }
            .item{
                background-color: yellow;
                width: 100px;
                height: 100px;

            }
            /* 单点 */
            .one{
                /* 对点使用flex布局 */
                display: flex;
                justify-content: center;
                align-items: center;
            }
            /* 点 */
            .item-one{
                display: block;
                height: 20px;
                width: 20px;
                background-color: #1890FF;
                border-radius: 50%;
            }
            /* 三点 */
            .two{

                display: flex;
                justify-content: space-between;
            }
            .two span{
                margin: 2px;
                display: block;
                height: 20px;
                width: 20px;
                border-radius: 50%;
                background-color: #1890FF;
            }
            .two2{
                align-self: center;
            }
            .two3{
                align-self: flex-end;
            }
            /* 五点 */
            .three{
                display: flex;
                justify-content: space-around;
            }
            .three span{
                display: block;
                height: 20px;
                width: 20px;
                border-radius: 50%;
                background-color: #1890FF;
            }
            #three_one, #three_three{
                padding: 2px;
                display: flex;
                flex-direction: column;
                justify-content: space-between;
            }
            #three_two{
                display: flex;
                flex-direction: column;
                justify-content: center;
            }
            /* 六点 */
            .four{
                display: flex;
                justify-content: space-around;
            }
            .four span{
                display: block;
                height: 20px;
                width: 20px;
                border-radius: 50%;
                background-color: #1890FF;
            }
            #four1,#four2{
                padding: 2px;
                display: flex;
                flex-direction: column;
                justify-content: space-between;
            }
        </style>
    </head>
    <body>
        <div id="container">
            <!-- 一个点居中 -->
            <div class="item one">
                <span class="item-one"></span>
            </div>
            <!-- 三点 -->
            <div class="item two">
                <span class="two1"></span>
                <span class="two2"></span>
                <span class="two3"></span>
            </div>
            <!-- 五点 -->
            <div class="item three">
                <div id="three_one">
                    <span></span>
                    <span></span>
                </div>
                <div id="three_two">
                    <span></span>
                </div>
                <div id="three_three">
                    <span></span>
                    <span></span>
                </div>
            </div>
            <!-- 六点 -->
            <div class="item four">
                <div id="four1">
                    <span></span>
                    <span></span>
                    <span></span>
                </div>
                <div id="four2">
                    <span></span>
                    <span></span>
                    <span></span>
                </div>
            </div>

        </div>
    </body>
</html>

测试结果

在这里插入图片描述



远程桌面访问阿里云服务器的方法

seo达人

我用的是Windows版本的阿里云服务器。

首先,打开服务器,找到已经创建好的服务器实例并点击。

之后会跳转到实例页面,点击右侧的“管理”

然后配置安全组。安全组中就是设置哪些IP可以访问我们的服务器。

然后在安全组配置规则。

添加新规则。

想要让Windows电脑远程链接服务器需要开放3389端口。不然就无法用自己的电脑远程链接服务器了。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

配置snmpd for windows

seo达人

1.安装


Windows 安装请参考


默认安装为c:/usr/ 。可以执行如下文件,以便启动snmp agent:


snmpd

会提示需要配置snmpd.conf。


No log handling enabled - turning on stderr logging

Warning: no access control information configured.

 It's unlikely this agent can serve any useful purpose in this state.

 Run "snmpconf -g basic_setup" to help you configure the snmpd.conf file for this agent.

NET-SNMP version 5.5

不必使用提示中的命令,因为此命令引用了perl,但是perl的对应模块无法跑起来。看来perl要完蛋的传说并不是空穴来风。


snmpd.conf可以自己创建到/usr/etc/snmp/snmpd.conf内。


2.修改配置文件


配置之前的说明:我在网上看到的所有配置都是com2sec,group,access这三个配置,但是从默认的snmp.conf文件中有一段话:




没必要使用 com2sec/group/access配置,使用ro(w)user,ro(w)community结合合适的views,就可以覆盖大多数需求了。


2.1  配置监听地址

snmpd默认监听本地IP的UDP161端口,等待snmp请求


agentAddress udp:161

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

2.2  配置视图(view)

格式:view viewName type oid[mask]


参数说明:  


   

   viewName : view名称

   type : 有两个值:include和exclude(包括或者排除)

   oid:可以访问的oid(mib子树)

   [mask]:对oid的掩码 (可选参数)

案例:


view systemonly included .1.3.6.1.2.1

2.3  配置共同体(community)

格式:ro(w)community communityName source


参数说明:


rocommunity定义一个只读权限的共同体,rwcommunity定义一个读写权限的共同体

rocommunity6或   rwcommunity6表示监听IPv6。

communityName:共同体名称

source:可以访问的IP地址范围,默认为”default”,即任何IP都能访问。


               可选参数:-V viewName  限制共同体只能访问viewName下的节点


实际配置文件

agentAddress udp:161

view systemonly included .1.3.6.1.2.1

rocommunity public default

3. 测试

修改完配置之后,重启snmpd:


snmpd

连接测试:(该节点表示获取主机内存大小)


snmpwalk -v 1 -c public localhost .1.3.6.1.2.1.1.1

SNMPv2-MIB::sysDescr.0 = STRING: Windows DESKTOP-70OA76Q 6.2.9200   Professional

得到类似以上的结果,就说明snmpd配置成功。


配置MIB库

Windows版本的snmp自带有很多MIB,位于C:\usr\share\snmp\mibs


因此可以对OID和MIB name做互相转换:


snmptranslate -On SNMPv2-MIB::sysDescr.0

.1.3.6.1.2.1.1.1.0


snmptranslate .1.3.6.1.2.1.1.1.0

SNMPv2-MIB::sysDescr.0

添加自己的mib

之前的笔记有提到自己创建的一个定制mib文件


https://github.com/1000copy/tbit-guard-snmp/blob/master/tbit.mib

可以把它加入到C:\usr\share\snmp\mibs目录内,文件名无所谓。然后为snmptranslate加入选项 -mALL ,表示加载所有在此目录内的mib文件:


snmptranslate -m ALL .1.3.6.1.4.1.66666

你应该看到如下消息,表明mib加载成功:


tbitinc::tbit

详细的说明如下:


-m MIBLIST

     指定一个用冒号分隔的MIB模块列表(不是文件),以便为这个应用程序加载。 这将覆盖(或增加)环境变量MIBS、snmp.conf的内容。指令mibs,以及硬编码到Net-SNMP库中的MIBs列表。如果MIBLIST有一个前导的'-'或'+'字符,那么所列出的MIB模块将被加载到默认列表之外,分别位于该列表之前或之后。

     否则,将加载指定的MIB,而不是这个默认列表。特殊关键字ALL用于加载MIB目录搜索列表中的所有MIB模块。 每一个名字不以". "开头的文件都会被解析为是一个

     MIB文件。

-M DIRLIST

     指定一个以冒号分隔的目录列表来搜索MIB。 这将覆盖(或增强)环境变量MIBDIRS和snmp.conf指令mibdirs。

     以及硬编码到 Net-SNMP 库中的默认目录 (/usr/share/snmp/mibs)。


     如果DIRLIST有一个前导的'-'或'+'字符,那么给定的目录会被添加到默认列表中,在这个列表的目录之前或之后进行搜索。

     分别进行搜索。 否则,将搜索指定的目录,而不是这个默认列表。


     请注意,列表中出现在后面的目录要比前面的目录优先。 要避免搜索任何MIB目录,请将MIBDIRS环境中的

     变量为空字符串("")。


     请注意,使用-m选项或mibs配置指令指定的MIB将从-M选项(或等价物)列出的目录中加载。

     mibfile指令采用的是指定MIB文件的完整路径,所以不需要在MIB目录搜索列表中出现。

10分钟彻底搞懂单页面应用路由

seo达人

单页面应用特征

假设: 在一个 web 页面中,有1个按钮,点击可跳转到站内其他页面。


多页面应用: 点击按钮,会从新加载一个html资源,刷新整个页面;


单页面应用: 点击按钮,没有新的html请求,只发生局部刷新,能营造出一种接近原生的体验,如丝般顺滑。


SPA 单页面应用为什么可以几乎无刷新呢?因为它的SP——single-page。在第一次进入应用时,即返回了唯一的html页面和它的公共静态资源,后续的所谓“跳转”,都不再从服务端拿html文件,只是DOM的替换操作,是模(jia)拟(zhuang)的。


那么js又是怎么捕捉到组件切换的时机,并且无刷新变更浏览器url呢?靠hash和HTML5History。


hash 路由

特征

类似www.xiaoming.html#bar 就是哈希路由,当 # 后面的哈希值发生变化时,不会向服务器请求数据,可以通过 hashchange 事件来监听到 URL 的变化,从而进行DOM操作来模拟页面跳转

不需要服务端配合

对 SEO 不友好

原理

hash


HTML5History 路由

特征

History 模式是 HTML5 新推出的功能,比之 hash 路由的方式直观,长成类似这个样子www.xiaoming.html/bar ,模拟页面跳转是通过 history.pushState(state, title, url) 来更新浏览器路由,路由变化时监听 popstate 事件来操作DOM

需要后端配合,进行重定向

对 SEO 相对友好

原理

Html5 History


vue-router 源码解读

以 Vue 的路由vue-router为例,我们一起来撸一把它的源码。


Tips:因为,本篇的重点在于讲解单页面路由的两种模式,所以,下面只列举了一些关键代码,主要讲解:


注册插件

VueRouter的构造函数,区分路由模式

全局注册组件

hash / HTML5History模式的 push 和监听方法

transitionTo 方法

注册插件

首先,作为一个插件,要有暴露一个install方法的自觉,给Vue爸爸去 use。


源码的install.js文件中,定义了注册安装插件的方法install,给每个组件的钩子函数混入方法,并在beforeCreate钩子执行时初始化路由:


Vue.mixin({

 beforeCreate () {

   if (isDef(this.$options.router)) {

     this._routerRoot = this

     this._router = this.$options.router

     this._router.init(this)

     Vue.util.defineReactive(this, '_route', this._router.history.current)

   } else {

     this._routerRoot = (this.$parent && this.$parent._routerRoot) || this

   }

   registerInstance(this, this)

 },

 // 全文中以...来表示省略的方法

 ...

});

区分mode

然后,我们从index.js找到整个插件的基类 VueRouter,不难看出,它是在constructor中,根据不同mode 采用不同路由实例的。


...

import {install} from './install';

import {HashHistory} from './history/hash';

import {HTML5History} from './history/html5';

...

export default class VueRouter {

 static install: () => void;

 constructor (options: RouterOptions = {}) {

   if (this.fallback) {

     mode = 'hash'

   }

   if (!inBrowser) {

     mode = 'abstract'

   }

   this.mode = mode

         

   switch (mode) {

     case 'history':

       this.history = new HTML5History(this, options.base)

       break

     case 'hash':

       this.history = new HashHistory(this, options.base, this.fallback)

       break

    case 'abstract':

       this.history = new AbstractHistory(this, options.base)

       break

    default:

     if (process.env.NODE_ENV !== 'production') {

       assert(false, `invalid mode: ${mode}`)

     }

   }

 }

}

全局注册router-link组件

这个时候,我们也许会问:使用 vue-router 时, 常见的<router-link/>、 <router-view/>又是在哪里引入的呢?


回到install.js文件,它引入并全局注册了 router-view、router-link组件:


import View from './components/view';

import Link from './components/link';

...

Vue.component('RouterView', View);

Vue.component('RouterLink', Link);

在 ./components/link.js 中,<router-link/>组件上默认绑定了click事件,点击触发handler方法进行相应的路由操作。


const handler = e => {

 if (guardEvent(e)) {

   if (this.replace) {

     router.replace(location, noop)

   } else {

     router.push(location, noop)

   }

}

};

就像最开始提到的,VueRouter构造函数中对不同mode初始化了不同模式的 History 实例,因而router.replace、router.push的方式也不尽相同。接下来,我们分别扒拉下这两个模式的源码。


hash模式

history/hash.js 文件中,定义了HashHistory 类,这货继承自 history/base.js 的 History 基类。


它的prototype上定义了push方法:在支持 HTML5History 模式的浏览器环境中(supportsPushState为 true),调用history.pushState来改变浏览器地址;其他浏览器环境中,则会直接用location.hash = path 来替换成新的 hash 地址。


其实最开始读到这里是有些疑问的,既然已经是 hash 模式为何还要判断supportsPushState?是为了支持scrollBehavior,history.pushState可以传参key过去,这样每个url历史都有一个key,用 key 保存了每个路由的位置信息。


同时,原型上绑定的setupListeners 方法,负责监听 hash 变更的时机:在支持 HTML5History 模式的浏览器环境中,监听popstate事件;而其他浏览器中,则监听hashchange。监听到变化后,触发handleRoutingEvent 方法,调用父类的transitionTo跳转逻辑,进行 DOM 的替换操作。


import { pushState, replaceState, supportsPushState } from '../util/push-state'

...

export class HashHistory extends History {

 setupListeners () {

   ...

   const handleRoutingEvent = () => {

       const current = this.current

       if (!ensureSlash()) {

         return

       }

       // transitionTo调用的父类History下的跳转方法,跳转后路径会进行hash化

       this.transitionTo(getHash(), route => {

         if (supportsScroll) {

           handleScroll(this.router, route, current, true)

         }

         if (!supportsPushState) {

           replaceHash(route.fullPath)

         }

       })

     }

     const eventType = supportsPushState ? 'popstate' : 'hashchange'

     window.addEventListener(

       eventType,

       handleRoutingEvent

     )

     this.listeners.push(() => {

       window.removeEventListener(eventType, handleRoutingEvent)

     })

 }

 

 push (location: RawLocation, onComplete?: Function, onAbort?: Function) {

   const { current: fromRoute } = this

   this.transitionTo(

     location,

     route => {

       pushHash(route.fullPath)

       handleScroll(this.router, route, fromRoute, false)

       onComplete && onComplete(route)

     },

     onAbort

   )

 }

}

...


// 处理传入path成hash形式的URL

function getUrl (path) {

 const href = window.location.href

 const i = href.indexOf('#')

 const base = i >= 0 ? href.slice(0, i) : href

 return `${base}#${path}`

}

...


// 替换hash

function pushHash (path) {

 if (supportsPushState) {

   pushState(getUrl(path))

 } else {

   window.location.hash = path

 }

}


// util/push-state.js文件中的方法

export const supportsPushState =

 inBrowser &&

 (function () {

   const ua = window.navigator.userAgent


   if (

     (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&

     ua.indexOf('Mobile Safari') !== -1 &&

     ua.indexOf('Chrome') === -1 &&

     ua.indexOf('Windows Phone') === -1

   ) {

     return false

   }

   return window.history && typeof window.history.pushState === 'function'

 })()

HTML5History模式

类似的,HTML5History 类定义在 history/html5.js 中。


定义push原型方法,调用history.pusheState修改浏览器的路径。


与此同时,原型setupListeners 方法对popstate进行了事件监听,适时做 DOM 替换。


import {pushState, replaceState, supportsPushState} from '../util/push-state';

...

export class HTML5History extends History {


 setupListeners () {


   const handleRoutingEvent = () => {

   const current = this.current;

   const location = getLocation(this.base);

   if (this.current === START && location === this._startLocation) {

     return

   }


   this.transitionTo(location, route => {

     if (supportsScroll) {

       handleScroll(router, route, current, true)

     }

   })

   }

   window.addEventListener('popstate', handleRoutingEvent)

   this.listeners.push(() => {

     window.removeEventListener('popstate', handleRoutingEvent)

   })

 }

 push (location: RawLocation, onComplete?: Function, onAbort?: Function) {

   const { current: fromRoute } = this

   this.transitionTo(location, route => {

     pushState(cleanPath(this.base + route.fullPath))

     handleScroll(this.router, route, fromRoute, false)

     onComplete && onComplete(route)

   }, onAbort)

 }

}


...


// util/push-state.js文件中的方法

export function pushState (url?: string, replace?: boolean) {

 saveScrollPosition()

 const history = window.history

 try {

   if (replace) {

     const stateCopy = extend({}, history.state)

     stateCopy.key = getStateKey()

     history.replaceState(stateCopy, '', url)

   } else {

     history.pushState({ key: setStateKey(genStateKey()) }, '', url)

   }

 } catch (e) {

   window.location[replace ? 'replace' : 'assign'](url)

 }

}

transitionTo 处理路由变更逻辑

上面提到的两种路由模式,都在监听时触发了this.transitionTo,这到底是个啥呢?它其实是定义在 history/base.js 基类上的原型方法,用来处理路由的变更逻辑。

先通过const route = this.router.match(location, this.current)对传入的值与当前值进行对比,返回相应的路由对象;接着判断新路由是否与当前路由相同,相同的话直接返回;不相同,则在this.confirmTransition中执行回调更新路由对象,并对视图相关DOM进行替换操作。


export class History {

...

transitionTo (

   location: RawLocation,

   onComplete?: Function,

   onAbort?: Function

 ) {

   const route = this.router.match(location, this.current)

   this.confirmTransition(

     route,

     () => {

       const prev = this.current

       this.updateRoute(route)

       onComplete && onComplete(route)

       this.ensureURL()

       this.router.afterHooks.forEach(hook => {

         hook && hook(route, prev)

       })


       if (!this.ready) {

         this.ready = true

         this.readyCbs.forEach(cb => {

           cb(route)

         })

       }

     },

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

Vue.js教程-目录

前端达人

简介

  • 本目录作为Vue教程的首页,所以会持续更新。
  • 如果某篇章节中有错误的地方,希望大家能够指出来,我会更正,私信和评论里说都可以,不懂的地方也可以说,如果我也不会那就请教一下大佬们吧,毕竟我对前端的东西也不是特别了解,多多包涵!嘿嘿。

章节列表

章节名称 地址
Vue.js教程-安装和HelloWorld https://coderhqf.blog.csdn.net/article/details/107574556
Vue.js教程-Vue项目的目录结构和.vue文件的构成 https://coderhqf.blog.csdn.net/article/details/107621070
Vue.js教程-Vue基本指令 https://coderhqf.blog.csdn.net/article/details/107677588
Vue.js教程-组件化开发 https://coderhqf.blog.csdn.net/article/details/107783664

Vue简介

  • Vue官网
  • Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。
  • Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
  • Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

Vue特点

  • 易用:在有HTML CSS JavaScript的基础上,快速上手。
  • 灵活:简单小巧的核心,渐进式技术栈,足以应付任何规模的应用。
  • 性能:20kb min+gzip 运行大小、超快虚拟 DOM 、最省心的优化。

Vue中数据观测的实现

  • Vue.js利用了ES5的Object.defineProperty方法,直接将原生数据对象的属性改造为getter和setter,在这两个函数内部实现依赖的收集和触发,而且完美支持嵌套的对象结构。对于数组,则通过包裹数组的可变方法(比如push)来监听数组的变化。这使得操作Vue.js的数据和操作原生对象几乎没有差别[注:在添加/删除属性,或是修改数组特定位置元素时,需要调用特定的函数,如obj.$add(key, value)才能触发更新。这是受ES5的语言特性所限。],数据操作的逻辑更为清晰流畅,和第三方数据同步方案的整合也更为方便。

Vue项目打包

  • 在构建大型应用时,推荐使用Webpack+vue-loader这个组合以使针对组件的开发更。
  • 在后面的章节中会细说怎么打包并部署到服务器上,也会讲怎么白嫖阿里云(学生版),好像有大佬写过这个文章,大家搜一下也行,最重要的还是开发。

Vue的组件化开发

  • Vue最主要的是组件化开发,因为是单页面,也就是在一个页面中渲染出多个页面的效果,这个特点能够让非常多的组件在不同的项目中重复使用。
  • Vue中的组件基于Web components进行了上层功能的实现,例如数据绑定、动画系统等。

Vue与后端的数据交互:axios

  • 传统的一般都用Ajax,但如果请求有先后关系的话就容易产生回调地狱,套娃套的吧。
  • Vue2之后就推荐使用axios了,等写到前后端交互的时候再讲这个就行。

相关说明

  • Vue参考了AngularJS、KnockoutJS、Ractive.js、Rivets.js,可以是把他们的缺点都优化成了自己的优点,参考过程中不但去其糟粕,还加入了自己的特性,但目前也只有国内用Vue的比较多,毕竟社区小,资源少,但以后应该会是潮流,因为开发快好上手。
  • 其实Vue相对来说是非常好上手的,因为它只专注于视图层。如果只是要用的话,其实对原理也不用太纠结,但既然要全栈,何不贯彻到底,我也是在学习中,如果想正规学习的话,我比较推荐coderwhy-王红君老师的课,他讲的挺好的,有些原理讲的也是很清楚的,渠道嘛,B站大学、腾讯课堂啥的都有,还有他的微博,大家可以去网上找。
  • 再强调一遍,如果发现不对的地方请联系我,因为不想误人子弟,毕竟这是自己的总结,也不想以后自己还用着错误的东西,嘿嘿嘿。。。
  • Vue作为现在国内最潮流的前端框架,也逐渐成为后端开发人员需要学的新知识了,我看现在很多后端岗位的面试里都会提到这个前端框架,所以大家学一下是不亏的。
  • 在CSDN杂志中有一篇文章:Vue.js:轻量的前端组件化方案,如果大家有兴趣就看看吧。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45062103/article/details/107763788

JavaScript中的for循环

seo达人

JavaScript 语言中的 for 循环用于多次执行代码块,它是 JavaScript 中最常用的一个循环工具,还可用于数组的遍历循环等。


我们为什么要使用 for 循环呢?打个比方,例如我们想要控制台输出1到1000之间的所有数字,如果单写输出语句,要写1000句代码,但是如果使用 for 循环,几句代码就能实现。总之,使用 for 循环能够让我们写代码更方便快捷(当然啦,否则要它干嘛)。


for 循环语法

语法如下所示:


for(变量初始化; 条件表达式; 变量更新) {

   // 条件表达式为true时执行的语句块

}

变量初始化,表示代码块开始前执行。

条件表达式,定义运行循环代码块的条件。

变量更新,在循环代码块每次被执行之后再执行。

示例:

例如我们在一个HTML文件中,编写如下代码,实现计算1到100的总和:


<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<title>JS_侠课岛(9xkd.com)</title>

</head>

<body>

<script>

 var result = 0;

 for(var i = 1; i <= 100; i++) {

   result = result + i;

 }

 alert(result);

</script>

</body>  

</html>

在浏览器中打开这个文件,会弹出一个弹出层,弹出层中显示的是1到100的总和:



上述代码中,我们声明了一个变量 result 并给它赋值为 0,表示初始的总和为 0 。


然后在 for 循环中三个语句:


变量初始化 i = 1,表示从 1 开始计算。

条件表达式 i <= 100,表示只要 i 小于等于 100 循环就会一直执行,当 i 大于 100 循环会停止。

变量更新 i++,之前我们学运算符的时候学过,这是递增运算符 ++,表示为其操作数增加 1。

此时我们可以一点点来看这个 for 循环:


第一次循环: result = 0 + 1   // 此时result值为0,  i的值为1

第二次循环: result = 1 + 2   // 此时result值为0+1,i的值为2

第三次循环: result = 3 + 3   // 此时result值为1+2,i的值为3

第四次循环: result = 6 + 4   // 此时result值为3+3,i的值为4

第五次循环: result = 10 + 5  // 此时result值为6+4,i的值为5

...

我们只需要搞清楚 for 循环中的执行原理,不需要手动来计算求和,只要写好代码,执行代码后计算机会很快会告诉我们1到 100 的总和。


再补充一下,上述代码中result = result + i,我们也可以写成 result += i,这是我们之前学过的加赋值运算符,还记得吗?


示例:

再来看一个例子,例如我们可以使用 for 循环来实现数组遍历,首先定义一个数组 lst:


var lst = ["a", "b", "c", "d", "e"];

在写 for 循环时,首先就是要搞清楚小括号里面的三个语句,因为我们可以通过数组中元素的下标索引来获取元素的值,而数组的索引又是从 0 开始,所以变量初始化可以设置为i = 0。第二个条件表达式,因为数组中最后一个索引为 lst.length - 1,所以只要小于等于 lst.length - 1,循环就会一直执行。而i <= lst.length - 1 就相当于 i<lst.length。第三个变量更新,当循环每循环一次,索引值就加一,所以为 i++。


所以循环可以像下面这样写:


for(i = 0; i<lst.length; i++){

   console.log(lst[i]);  // 输出数组中的元素值,从索引为0的值开始输出,每次加1,一直到lst.length-1

}

输出:


a

b

c

d

e

其实遍历数组还有一种更好的方法,就是使用 for...in 循环语句来遍历数组。


for...in 循环

for...in 循环主要用于遍历数组或对象属性,对数组或对象的属性进行循环操作。for...in 循环中的代码每执行一次,就会对数组的元素或者对象的属性进行一次操作。


语法如下:


for (变量 in 对象) {

   // 代码块

}

for 循环括号内的变量是用来指定变量,指定的可以是数组对象或者是对象属性。


示例:

使用 for...in 循环遍历我们定义好的 lst 数组:


var lst = ["a", "b", "c", "d", "e"];

for(var l in lst){

   console.log(lst[l]);

}

输出:


a

b

c

d

e

除了数组,for...in 循环还可以遍历对象,例如我们遍历 侠侠 的个人基本信息:


var object = {

   姓名:'侠侠',

   年龄:'22',

   性别:'男',

   出生日期:'1997-08-05',

   职业:'程序员',

   特长:'跳舞'

}


for(var i in object) {

   console.log(i + ":" + object[i]);

}

输出:


姓名: 侠侠

年龄: 22

性别: 男

出生日期: 1997-08-05

职业:程序员

特长:跳舞

动手小练习

请自定义一个长度为7的数组,然后通过 for 循环将数组中的元素遍历出来。

求和:1~100的奇数和。

求和:1~100的偶数和。

使用对象定义一个人的个人信息(包括姓名、性别、年龄、出生日期、兴趣爱好、职业、特长等),然后使用 for...in 循环将这些信息遍历输出。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

封装element-ui表格

seo达人

表格需求

一般管理系统对表格会有以下需求


可以分页(需要有分页条)

可以多选(表格带复选框)

顶部需要加一些操作按钮(新增,删除等等)

表格每行行尾有操作按钮

表格行可以编辑

如下图为一个示例表格




如果我们直接使用element-ui提供的组件的话,那么开发一个这样的表格就需要使用到以下内容


需要使用表格的插槽功能,开发每一行的按钮

需要通过样式调整顶部按钮,表格,分页条的布局样式

需要监听分页的事件然后去刷新表格数据

顶部按钮或操作按钮如果需要获取表格数据,需要调用表格提供的api

对于有行编辑的需求,还需要通过插槽去渲染行编辑的内容,同时要控制行编辑的开关

不仅仅开发表格比较麻烦,而且还要考虑团队协作,如果每个人实现表格的方式存在差别,那么可能后期的维护成本也会变得很高。那怎么办呢?


项目安装

安装插件

在使用element-ui的项目中,可以通过以下命令进行安装


npm install vue-elementui-table -S

在项目中使用

在main.js中添加以下代码


import ZjTable from 'vue-element-table'


Vue.use(ZjTable)

然后即可像下文中的使用方式进行使用


表格配置

为了满足团队快速开发的需要,小编对上面提出来的需求进行了封装,然后使用的时候,开发人员只需要配置一些JSON便可以完成以上功能的开发。


基础配置

一个基础的表格包含了数据和列信息,那么如何用封装的表格去配置呢?


<template>

 <zj-table

   :columns="columns"

   :data="data"

   :pagination="false"

 />

</template>

<script>

export default {

 data() {

   return {

     // 表格的列信息, 数组每一项代表一个字段,可以使用element 列属性的所有属性,以下仅为示例

     columns: Object.freeze([

       {

         // 表头显示的文字

         label: '姓名',

         // 对应数据里面的字段

         prop: 'name'

       },

       {

         label: '性别',

         prop: 'sex',

         // 格式化表格,与element-ui 的表格属性相同

         formatter(row, column, cellValue) {

           return cellValue === 1 ? '男' : '女'

         }

       },

       {

         label: '年龄',

         prop: 'age'

       }

     ]),

     data: [

       {

         name: '子君',

         sex: 1,

         age: 18

       }

     ]

   }

 }

}

</script>

通过上面的配置,就可以完成一个基础表格的开发,完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/base.vue,效果如下图所示




表格默认会显示复选框,也可以通过配置selectable属性来关闭掉


添加分页

简单的表格用封装之后的或未封装的开发工作量区别并不大,我们继续为表格添加上分页


<template>

   <!--

   current-page.sync 表示页码, 添加上 .sync 在页码发生变化时自动同步页码

   page-size.sync 每页条数

   total  总条数

   height="auto" 配置height:auto, 表格高度会根据内容自动调整,如果不指定,表格将保持充满父容器,同时表头会固定,不跟随滚动条滚动

   @page-change 无论pageSize currentPage 哪一个变化,都会触发这个事件

 -->

 <zj-table

   v-loading="loading"

   :columns="columns"

   :data="data"

   :current-page.sync="currentPage"

   :page-size.sync="pageSize"

   :total="total"

   height="auto"

   @page-change="$_handlePageChange"

 />

</template>

<script>

export default {

 data() {

   return {

     columns: Object.freeze([

       // 列字段与上例一样,此处省略

     ]),

     data: [],

     // 当前页码

     currentPage: 1,

     // 每页条数

     pageSize: 10,

     // 总条数

     total: 0,

     // 是否显示loading

     loading: false

   }

 },

 created() {

   this.loadData()

 },

 methods: {

   // 加载表格数据

   loadData() {

     this.loading = true

     setTimeout(() => {

       // 假设总条数是40条

       this.total = 40

       const { currentPage, pageSize } = this

       // 模拟数据请求获取数据

       this.data = new Array(pageSize).fill({}).map((item, index) => {

         return {

           name: `子君${currentPage + (index + 1) * 10}`,

           sex: Math.random() > 0.5 ? 1 : 0,

           age: Math.floor(Math.random() * 100)

         }

       })

       this.loading = false

     }, 1000)

   },

   $_handlePageChange() {

     // 因为上面设置属性指定了.sync,所以这两个属性会自动变化

     console.log(this.pageSize, this.currentPage)

     // 分页发生变化,重新请求数据

     this.loadData()

   }

 }

}

</script>

完整代码请参考 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/pagination.vue


通过封装,表格将自带分页功能,通过上面代码,实现效果如下所示,是不是变得简单了一些。接下来我们继续给表格添加按钮




添加顶部按钮

表格上面可能会有新增,删除等等按钮,怎么办呢,接下来我们继续通过配置去添加按钮


<template>

 <zj-table

   :buttons="buttons"

 />

</template>

<script>

export default {

 data() {

   return {

     buttons: Object.freeze([

       {

         // id 必须有而且是在当前按钮数组里面是唯一的

         id: 'add',

         text: '新增',

         type: 'primary',

         icon: 'el-icon-circle-plus',

         click: this.$_handleAdd

       },

       {

         id: 'delete',

         text: '删除',

         // rows 是表格选中的行,如果没有选中行,则禁用删除按钮, disabled可以是一个boolean值或者函数

         disabled: rows => !rows.length,

         click: this.$_handleRemove

       },

       {

         id: 'auth',

         text: '这个按钮根据权限显示',

         // 可以通过返回 true/false来控制按钮是否显示

         before: (/** rows */) => {

           return true

         }

       },

       // 可以配置下拉按钮哦

       {

         id: 'dropdown',

         text: '下拉按钮',

         children: [

           {

             id: 'moveUp',

             text: '上移',

             icon: 'el-icon-arrow-up',

             click: () => {

               console.log('上移')

             }

           },

           {

             id: 'moveDown',

             text: '下移',

             icon: 'el-icon-arrow-down',

             disabled: rows => !rows.length,

             click: () => {

               console.log('下移')

             }

           }

         ]

       }

     ])

   }

 },

 created() {},

 methods: {

   // 新增

   $_handleAdd() {

     this.$alert('点击了新增按钮')

   },

   // 顶部按钮会自动将表格所选的行传出来

   $_handleRemove(rows) {

     const ids = rows.map(({ id }) => id)

     this.$alert(`要删除的行id为${ids.join(',')}`)

   },

   // 关注作者公众号

   $_handleFollowAuthor() {}

 }

}

</script>

表格顶部可以添加普通的按钮,也可以添加下拉按钮,同时还可以通过before来配置按钮是否显示,disabled来配置按钮是否禁用,上面完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/button.vue


通过上面的代码就可以配置出下面的表格,是不是很简单呢?




表格顶部可以有按钮,行尾也是可以添加按钮的,一起来看看


行操作按钮

一般我们会将一些单行操作的按钮放在行尾,比如编辑,下载等按钮,那如何给行尾配置按钮呢?


<template>

 <zj-table

   :columns="columns"

 />

</template>

<script>

export default {

 data() {

   return {

     columns: Object.freeze([

       {

         // 可以指定列的宽度,与element-ui原生用法一致

         width: 220,

         label: '姓名',

         prop: 'name'

       },

       // 行编辑按钮,在表格末尾出现,自动锁定右侧

       {

         width: 180,

         label: '操作',

         // 通过 actions 指定行尾按钮

         actions: [

           {

             id: 'follow',

             text: '关注作者',

             click: this.$_handleFollowAuthor

           },

           {

             id: 'edit',

             text: '编辑',

             // 可以通过before控制按钮是否显示,比如下面年龄四十岁的才会显示编辑按钮

             before(row) {

               return row.age < 40

             },

             click: this.$_handleEdit

           },

           {

             id: 'delete',

             text: '删除',

             icon: 'el-icon-delete',

             disabled(row) {

               return row.sex === 0

             },

             // 为了拿到this,这里需要用箭头函数

             click: () => {

               this.$alert('女生被禁止删除了')

             }

           }

         ]

       }

     ])

   }

 },

 methods: {

   // 关注作者公众号

   $_handleFollowAuthor() {

           console.log('微信搜索【前端有的玩】,这是对小编最大的支持')

   },

   /**

    * row 这一行的数据

    */

   $_handleEdit(row, column) {

     this.$alert(`点击了姓名为【${row.name}】的行上的按钮`)

   }

 }

}

</script>

行操作按钮会被冻结到表格最右侧,不会跟随滚动条滚动而滚动,上面完整代码见, https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/button.vue


通过上面的代码就可以完成以下效果




最后再来一起看看行编辑


行编辑

比如上例,我希望点击行尾的编辑按钮的时候,可以直接在行上面编辑用户的姓名与性别,如何配置呢?


<template>

 <zj-table

   ref="table"

   :columns="columns"

   :data="data"

 />

</template>

<script>

export default {

 data() {

   return {

     columns: Object.freeze([

       {

         label: '姓名',

         prop: 'name',

         editable: true,

         field: {

           componentType: 'input',

           rules: [

             {

               required: true,

               message: '请输入姓名'

             }

           ]

         }

       },

       {

         label: '性别',

         prop: 'sex',

         // 格式化表格,与element-ui 的表格属性相同

         formatter(row, column, cellValue) {

           return cellValue === '1' ? '男' : '女'

         },

         editable: true,

         field: {

           componentType: 'select',

           options: [

             {

               label: '男',

               value: '1'

             },

             {

               label: '女',

               value: '0'

             }

           ]

         }

       },

       {

         label: '年龄',

         prop: 'age',

         editable: true,

         field: {

           componentType: 'number'

         }

       },

       {

         label: '操作',

         actions: [

           {

             id: 'edit',

             text: '编辑',

             // 如果当前行启用了编辑,则不显示编辑按钮

             before: row => {

               return !this.editIds.includes(row.id)

             },

             click: this.$_handleEdit

           },

           {

             id: 'save',

             text: '保存',

             // 如果当前行启用了编辑,则显示保存按钮

             before: row => {

               return this.editIds.includes(row.id)

             },

             click: this.$_handleSave

           }

         ]

       }

     ]),

     data: [

       {

         // 行编辑必须指定rowKey字段,默认是id,如果修改为其他字段,需要给表格指定row-key="字段名"

         id: '0',

         name: '子君',

         sex: '1',

         age: 18

       },

       {

         // 行编辑必须指定rowKey字段,默认是id,如果修改为其他字段,需要给表格指定row-key="字段名"

         id: '1',

         name: '子君1',

         sex: '0',

         age: 18

       }

     ],

     editIds: []

   }

 },

 methods: {

   $_handleEdit(row) {

     // 通过调用 startEditRow 可以开启行编辑

     this.$refs.table.startEditRow(row.id)

     // 记录开启了行编辑的id

     this.editIds.push(row.id)

   },

   $_handleSave(row) {

     // 点击保存的时候,通过endEditRow 结束行编辑

     this.$refs.table.endEditRow(row.id, (valid, result, oldRow) => {

       // 如果有表单验证,则valid会返回是否验证成功

       if (valid) {

         console.log('修改之后的数据', result)

         console.log('原始数据', oldRow)

         const index = this.editIds.findIndex(item => item === row.id)

         this.editIds.splice(index, 1)

       } else {

         // 如果校验失败,则返回校验的第一个输入框的异常信息

         console.log(result)

         this.$message.error(result.message)

       }

     })

   }

 }

}

</script>

不需要使用插槽就可以完成行编辑,是不是很开心。上述完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/row-edit.vue


效果如下图所示:




其他功能

除了上面的功能之外,表格还可以配置其他许多功能,比如


可以指定字段为链接列,需要给列配置link属性

可以通过插槽自定义顶部按钮,行操作按钮,行字段等

可以在按钮区域右侧通过插槽配置其他内容

其他等等

表格开发说明

通过上面的代码示例,我们已经知道了封装之后的表格可以完成哪些事情,接下来一起来看看表格是如何实现的。完整代码见 https://github.com/snowzijun/vue-element-table/tree/master/src/components/zj-table


表格布局

整个表格是通过JSX来封装的,因为JSX使用起来更加灵活。对于我们封装的表格,我们从竖向可以分为三部分,分别是顶部按钮区,中间表格区,底部分页区,如何去实现三个区域的布局呢,核心代码如下


render(h) {

   // 按钮区域

   const toolbar = this.$_renderToolbar(h)

   // 表格区域

   const table = this.$_renderTable(h)

   // 分页区域

   const page = this.$_renderPage(h)


   return (

     <div class="zj-table" style={{ height: this.tableContainerHeight }}>

       {toolbar}

       {table}

       {page}

     </div>

   )

 }

通过三个render函数分别渲染对应区域,然后将三个区域组合在一起。


渲染表格列

通过前文的讲解,我们可以将表格的列分为以下几种


常规列

行编辑列

操作按钮列

插槽列

链接列(文档后续完善)

嵌套列(文档后续完善)

   $_renderColumns(h, columns) {

     // 整体是否排序

     let sortable = this.sortable ? 'custom' : false

     return columns

       .filter(column => {

         const { hidden } = column

         if (hidden !== undefined) {

           if (typeof hidden === 'function') {

             return hidden({

               columns,

               column

             })

           }

           return hidden

         }

         return true

       })

       .map(column => {

         const {

           useSlot = false,

           // 如果存在操作按钮,则actions为非空数组

           actions = [],

           // 是否可编辑列, 对于可编辑列需要动态启用编辑

           editable = false,

           // 是否有嵌套列

           nests,

           // 是否可点击

           link = false

         } = column

         let newSortable = sortable

         if (column.sortable !== undefined) {

           newSortable = column.sortable ? 'custom' : false

         }

         column = {

           ...column,

           sortable: newSortable

         }

         if (nests && nests.length) {

           // 使用嵌套列

           return this.$_renderNestColumn(h, column)

         } else if (editable) {

           // 使用编辑列

           return this.$_renderEditColumn(h, column)

         } else if (useSlot) {

           // 使用插槽列

           return this.$_renderSlotColumn(h, column)

         } else if (actions && actions.length > 0) {

           // 使用操作列

           column.sortable = false

           return this.$_renderActionColumn(h, column)

         } else if (link) {

           // 使用链接列

           return this.$_renderLinkColumn(h, column)

         } else {

           // 使用默认列

           return this.$_renderDefaultColumn(h, column)

         }

       })

   },

行编辑列

当前表格行编辑支持input,select,datepicker,TimeSelect,InputNumber等组件,具体渲染代码如下所示


// 编辑单元格

   $_renderEditCell(h, field) {

     const components = {

       input: Input,

       select: ZjSelect,

       date: DatePicker,

       time: TimeSelect,

       number: InputNumber

     }

     const componentType = field.componentType

     const component = components[componentType]

     if (component) {

       return this.$_renderField(h, field, component)

     } else if (componentType === 'custom') {

       // 如果自定义,可以通过component指定组件

       return this.$_renderField(h, field, field.component)

     }

     return this.$_renderField(h, field, Input)

   },

   $_renderField(h, field, Component) {

     // 编辑行的id字段

     const { rowId, events = {}, nativeEvents = {} } = field


     const getEvents = events => {

       const newEvents = {}

       Object.keys(events).forEach(key => {

         const event = events[key]

         newEvents[key] = (...rest) => {

           const args = [

             ...rest,

             {

               rowId,

               row: this.editRowsData[rowId],

               value: this.editRowsData[rowId][field.prop]

             }

           ]

           return event(...args)

         }

       })

       return newEvents

     }

     // 事件改写

     const newEvents = getEvents(events)

     const newNativeEvents = getEvents(nativeEvents)

     return (

       <Component

         size="small"

         on={newEvents}

         nativeOn={newNativeEvents}

         v-model={this.editRowsData[rowId][field.prop]}

         {...{

           attrs: field,

           props: field

         }}

       />

     )

   }

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务


日历

链接

个人资料

蓝蓝设计的小编 http://www.lanlanwork.com

存档