前端优化常用技术心得

栏目: JavaScript · 发布时间: 6年前

内容简介:从建立http连接开始,到页面展示到浏览器里,经历了加载、执行、渲染,重构的几个阶段。将分享下我自己的心得和其他人的优秀经验。浏览器是友善的客户端,对同域名并发请求是有数量限制,过去浏览器一般是2个,支持H5的一般是6个;并且服务器端是可以关闭请求。 有朋友不理解,为什么不是并发越多越好?举个例子:百万级的PV,并发数量过大会造成什么样的后果? 由此,所有的优化都是基于这个点和单线程而延伸出来的。 所以,前端的资源加载优化有两个方向js文件加载后是否要立即执行?立即执行是否会影响页面渲染?过去浏览器在加

从建立http连接开始,到页面展示到浏览器里,经历了加载、执行、渲染,重构的几个阶段。将分享下我自己的心得和其他人的优秀经验。

加载和执行

浏览器是友善的客户端,对同域名并发请求是有数量限制,过去浏览器一般是2个,支持H5的一般是6个;并且服务器端是可以关闭请求。 有朋友不理解,为什么不是并发越多越好?举个例子:百万级的PV,并发数量过大会造成什么样的后果? 由此,所有的优化都是基于这个点和单线程而延伸出来的。 所以,前端的资源加载优化有两个方向

  1. 开源 增加域名 既然同域名不能太多,那么就多域名;简单来说就是cdn,可以是第三方,也可以自己多弄几个二级域名
  2. 节流 资源压缩、按需加载 同域名内的文件充分的进行压缩,比如:本来2M的资源,如果压缩到1M以下(去除空格,gzip等)速度的提升就是50%;再有现在spa是将文件合并后进行压缩和打包,如果文件总体并不大,性能不会有太大影响;一旦开发中引入的UI库或第三方插件多了,总文件体量也不在少数;就有了:按需加载、延时加载的用武之地。比如在webpack打包的时候从template的html中单独加入某个css或js;更有webpack-http-require的库。
    当然,图片也需要做很多相应的处理
  • css实现效果(按钮、阴影等)
  • 压缩尺寸和size
  • sprite合并
  • svg、toff字体图
  • canvas绘制大图(地图相关)

阻塞性优化

js文件加载后是否要立即执行?立即执行是否会影响页面渲染?过去浏览器在加载和执行js文件时是阻塞状态,就是按照栈原理一个个来;所以,原来要求把js文件放到html代码底部前,现代浏览器某种程度上解决了并行加载的问题,也可以进行预加载,但是执行之后会否对页面造成重排?所以要灵活应用dns-prefetch、preload和defer|async,当然defer和async不是所有浏览器都生效,webkit核心的就没生效。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo</title>
<link rel="dns-prefetch" href="//cdn.com/">
<link rel="preload" href="//js.cdn.com/currentPage-part1.js" as="script">
<link rel="preload" href="//js.cdn.com/currentPage-part2.js" as="script">
<link rel="preload" href="//js.cdn.com/currentPage-part3.js" as="script">
<link rel="prefetch" href="//js.cdn.com/prefetch.js">
</head> 
<body>
<!-- html code -->

<script type="text/javascript" src="//js.cdn.com/currentPage-part1.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part2.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part3.js" defer></script>
</body>
</html>
复制代码

js执行优化

  1. 作用域优化,变量层级不要太深或嵌套太多,最好是本级;大家在看各大框架或库的时候,经常可以看到这种写法:
(function(w,d){})(window,document)

// 目的就是如此,再比如说的缓存某个变量或对象

function check(){
  var d = document, t = document.getElementById('t'), l = t.children;
  for(let i=0;i<l;i++){
    //code
  }
}
复制代码
  1. 循环优化 循环是编程中最常见的结构,优化循环是性能优化过程中很重要的一部分。一个循环的基本优化步骤如下:

减值迭代——大多数循环使用一个从0开始,增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加有效。 简化终止条件——由于每次循环过程都会计算终止条件,故必须保证它尽可能快,即避免属性查找或其它O(n)的操作。 简化循环体——循环体是执行最多的,故要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。 使用后测试循环——最常用的for和while循环都是前测试循环,而如do-while循环可以避免最初终止条件的计算,因些计算更快。

for(var i = 0; i < values.length; i++) {
    process(values[i]);
}
复制代码

优化1:简化终止条件

for(var i = 0, len = values.length; i < len; i++) {
    process(values[i]);
}
复制代码

优化2:使用后测试循环(注意:使用后测试循环需要确保要处理的值至少有一个)

var i values.length - 1;
if(i > -1) {
    do {
        process(values[i]);
    }while(--i >= 0);
}
复制代码
  1. 展开循环

当循环的次数确定时,消除循环并使用多次函数调用往往更快 当循环的次数不确定时,可以使用Duff Service来优化,基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。如下:

// Jeff Greenberg for JS implementation of Duff's Device
// 假设:values.length > 0
function process(v) {
    alert(v);
}
 
var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17];
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;
 
do {
    switch(startAt) {
        case 0 : process(values[i++]);
        case 7 : process(values[i++]);
        case 6 : process(values[i++]);
        case 5 : process(values[i++]);
        case 4 : process(values[i++]);
        case 3 : process(values[i++]);
        case 2 : process(values[i++]);
        case 1 : process(values[i++]);
    }
    startAt = 0;
}while(--iterations > 0);
复制代码

如上展开循环可以提升大数据集的处理速度。接下来给出更快的Duff装置技术,将do-while循环分成2个单独的循环。(注:这种方法几乎比原始的Duff装置实现快上40%。)

// Speed Up Your Site(New Riders, 2003)
function process(v) {
    alert(v);
}
 
var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17];
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;
 
if(leftover > 0) {
    do {
        process(values[i++]);
    }while(--leftover > 0);
}
 
do {
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
}while(--iterations > 0);
复制代码

针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。

  1. 避免双重解释

当JS代码想解析JS代码时就会存在双重解释惩罚,当使用eval()函数或是Function构造函数以及使用setTimeout()传一个字符串时都会发生这种情况。如下

eval("alert('hello world');"); // 避免
var sayHi = new Function("alert('hello world');"); // 避免
setTimeout("alert('hello world');", 100);// 避免
复制代码

以上代码是包含在字符串中的,即在JS代码运行的同时必须新启运一个解析器来解析新的代码。实例化一个新的解析器有不容忽视的开销,故这种代码要比直接解析要慢。以下这几个例子,除了极少情况下eval是必须的,应尽量避免使用上述。对于Function构造函数,直接写成一般的函数即可。对于setTimeout可以传入函数作为第一个参数。如下:

alert('hello world');
var sayHi = function() {
    alert('hello world');
};
setTimeout(function() {
    alert('hello world');
}, 100);
复制代码

总之,若要提高代码性能,尽可能避免出现需要按照JS解释的代码。

  1. 性能的其它注意事项 原生方法更快——只要有可能,使用原生方法而不是自已用JS重写。原生方法是用诸如C/C++之类的编译型语言写出来的,要比JS的快多了。很多人认为自定义的 排序 会比sortby更快,事实比对效果还是原生方法更加优秀。 switch语句较快——若有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码,还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化。 位运算较快——当进行数学运算时,位运算操作要比任何布尔运算或算数运算快。选择性地用位运算替换算数运算可以极大提升复杂计算的性能,诸如取模,逻辑与和逻辑或也可以考虑用位运算来替换。

  2. 最小化语句数 JS代码中的语句数量也会影响所执行的操作的速度,完成多个操作的单个语句要比完成单个操作的多个语句块快。故要找出可以组合在一起的语句,以减来整体的执行时间。这里列举几种模式

1.多个变量声明

// 避免
var i = 1;
var j = "hello";
var arr = [1,2,3];
var now = new Date();
 
// 提倡
var i = 1,
    j = "hello",
    arr = [1,2,3],
    now = new Date();
复制代码

2.插入迭代值

// 避免
var name = values[i];
i++;
 
// 提倡
var name = values[i++];
复制代码

3.使用数组和对象字面量,避免使用构造函数Array(),Object()

// 避免 
var a = new Array();
a[0] = 1;
a[1] = "hello";
a[2] = 45;
 
var o = new Obejct();
o.name = "bill";
o.age = 13;
 
// 提倡
var a = [1, "hello", 45];
var o = {
    name : "bill",
    age : 13
};
复制代码

4.优化DOM交互 在JS中,DOM无疑是最慢的一部分,DOM操作和交互要消耗大量时间,因为它们往往需要重新渲染整个页面或者某一个部分,故理解如何优化与DOM的交互可以极大提高脚本完成的速度。后面会针对性说明

数组存储

计算机科学中有个经典问题:通过改变数据存储的位置来获得最佳的读写性能,数据存储的位置关系到代码执行过程中数据的检索速度。在JS中这个问题相对简单,因为只有4种方案。

  • 字面量 字面量就代表自身,不存储在特定位置。JS字面量有:字符串、数字、布尔、对象、数组、函数、正则表达式和特殊的null、undefined
  • 本地变量 使用var定义的数据存储单元
  • 数组元素 存储在JS对象内部,以数字作为索引
  • 对象成员 存储在JS对象内部,以字符串作为索引 每一种数据存储的位置都有不同的读写消耗。大多情况下差不多,数组和对象代价稍高一些,具体表现看浏览器的性能和js解释器。尽量使用字面量和局部变量,减少数组项和对象成员的使用。

作用域

理解作用域概念是JS和核心关键,不仅从性能还得从功能的角度。简单说:生效的范围(域),哪些变量可以被函数访问,this的赋值,上下文(context)的转换。说到作用域就不能绕开作用域链。理解了作用域链和标识符就理解了作用域。

作用域链和标识符解析

每个函数都是Function对象的实例,Function对象和其它对象一样,拥有可以编程访问的属性,和一系列不能通过代码访问而仅供JS引擎存取的内部属性。其中一个内部属性是[[Scope]],有ECMA-262标准第三版定义

内部属性[[Scope]]包含了一个函数被创建的作用域中对象的集合。这个集合被称为函数的作用域链,它决定那些数据能被函数访问,函数作用域中的每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,他的作用域链会被创建此函数的作用域中可访问的数据对象所填充。例如:

function fn(a,b){
    return res = a*b;
}
复制代码

当fn创建时,它的作用域链中插入了一个对象变量,这个全局对象代表着所有在全局范围内定义的变量。该全局对象包含window、navigator、document等。fn执行的时候就会用到作用域,并创建执行环境也叫执行上下文。它定义了一个函数执行时的环境,即便是同一个函数,每次执行都创建新的环境,函数执行完毕,环境就销毁。 每个环境都要根据作用域和作用域链解析参数、变量。可以理解为作用域链好比一个堆栈,栈顶就是当前的活动对象(环境创建时函数[[Scope]]属性中的对象集合)大多情况也可以理解为函数内部定义的局部变量。

而闭包的是根据JS允许函数访问局部作用域之外的数据,虽然会带来性能问题,因为执行环境虽然销毁,但激活的对象依然存在,所以可以缓存变量,从而不用全局对象。适用

对象

属性和方法,两者都是对象的成员,引用了函数就是方法,非函数就是属性。为什么对象访问慢呢?因为原型链问题。

原型和原型链

直接看代码

function fun(name,age){
    this.name = name+'';
    this.age = age
}
fun.prototype.getName = function(){
    return this.name;
}
var fn = new fun(); 

true = (fn instanceof fun) //true
true = (fn instanceof Object) 

fn.__proto__ = fun.prototype

/*
 * fun的原型方法

__proto__ = null
hasOwnProperty = (function)
isPrototypeOf = (function)
propertyIsEnumerable = (function)
toLocaleString = (function)
toString = (function)
valueOf = (function)
*/
复制代码

平时普通变量也是这样一级级向上直到根(window)下,没有此变量或属性或方法,才返回undefined;

DOM编程

DOM操作代价高昂,这是web application最常见的性能瓶颈,Document Oject Module(DOM)是独立于语言的,用于操作xml和html文档的的程序接口,而且在浏览器中是通过js实现的。 各个公司的浏览器渲染和js解释引擎都不同,著名的V8相信大家都知道,是一个js引擎;但Chrome的渲染是WebCore。每个浏览器都有两套解释器,并相对独立。这就意味着每次操作都需要(V8<=>WebCore)==>Browser 两个解释器都是需要连接和通讯成本。减少两解释器通讯并减少页面改变的频率就是优化的方向。

重绘repaint和重排reflow

DOM树里的每个需要显示的节点在渲染树中至少存在一个对应的节点,隐藏的(display:none)的DOM元素则没有;渲染树的节点被称为帧(frames) 盒(boxes),DOM和渲染树构建完毕,浏览器就开始绘制页面元素(paint)

何时发生重重绘?当页面的几何属性发生变化,影响到现有的文档流需要重新调整页面排版的时候。举几个例子:

  • 添加或删除可见的DOM元素;
  • DOM元素位置改变;
  • DOM元素尺寸改变:容器padding、border、margin属性变化等;
  • 容器内的内容变化导致宽高变化:文本行数变多(少)、图片坍塌、图片被另一张大图替换
  • 浏览器窗口初始化和尺寸改变 重排结束后,就需要重绘。所以,尽可能的避免重排的产生,为了避免或少的进行重绘和重排,需要尽可能少的访问某些变量:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle() (currentStyle in IE)

function scroller(){
    var H = document.body.offsetHeight || scrollHeight
    return function(){
        var args = arguments,ct = this;
        // your code
    }
}
复制代码

为了最小和最少的影响到重绘和重排,应该尽可能少的修改DOM,访问影响重排的属性。如果非要修改,尽量尊从三个步骤: 1.元素脱离文档流 2.一次性应用多重改变 3.恢复到文档流中 第一和第三步都会发生重排,所以核心的还是第二步。现在虚拟dom大:fire:,我们稍微了解下基础做法即可。 一次性更新的几种方式:字符串或数组.join('') innerHTML方式,createElement最后appendChild,document.createDocumentFragment,cloneNode需要改变的节点到缓存节点中,改完替换。 再者,动画时也需要尽可能少重绘和重排,例如:沿对角线,从左上移动到右下角

function move2RB(){
    var dom = document.getElementById('id'),curent = dom.style.top;
    while(curent<500){
        curent++
        dom.style.cssText = 'left:'+curent+'px; top:'+curent+'px';
    }
}
// 不要写成每次都去获取,left=dom.style.left再加1,甚至是dom.style.left = (pareSint(dom.style.left,10)+1)+'px'这种写法,直接改变className也是可以的。
复制代码

总结起来就几句话:少访问DOM,在js里处理计算完了再一次性修改,善用缓存和原生API;用现在的三大框架(angular、react、vue)即可不用操心这些 :)

算法和流程控制

代码的整体结构是影响运行速度的主要因素之一,数量与运行速度不一定成正比。组织结构、思路和执行效率才是核心!! JS属于ECMA的范畴,是一种脚本类语言,很多流程上的控制,工程化的思路是从 java 、c等语言上借鉴过来的,所以,知道后端语言的编码和工程化有助于我们加深理解。

Loop循环

  1. for
for(var i=0;i<10;i++){
    // code
}
复制代码

倒序可以在大数据量时提高少许效率,i<obj.length;i-- 2. while 前置循环

var i=0;
while(i<10){
    // code
    i++;
}
复制代码

后置循环

var i=0;
do{
    // code
}while(i++<10)
复制代码
  1. for - in
for(var prop in object){
    // code
}
复制代码

除了for-in循环其它效率所差不多,那么能够提高效率的点也就两个

  • 每次迭代处理的事务
  • 迭代的次数 一般数组array遍历写法的循环中,每次都会有如下操作: 1.在控制条件中查找一次属性array.length 2.在控制条件中执行一次数值比较 (i<array.length) 3.比较循环条件是否满足,(i<array.length === true) 4.一次自增或自减操作 (i++||i--) 5.数组、对象查找 array[i] 6.具体事务处理 将length提前获取并存到变量中可以减少两步(1和2),当循环复杂度为O(n)时,减少每次迭代的工作量是最有效的,当复杂度大于O(n),需要着重减少迭代次数。

条件判断

if-else vs switch 条件数量越多,switch的迭代效率会更高;当只有二选一或简单判断if-else的易读性更好。在实际coding中,如果只有二选一,有些情况甚至可以不用if-else,采用三目运算:result = (true||false)?condition0:condition1;还有将最可能发生的条件写到“if()”里面,减少判断次数,延伸开来就是if-elseif的判断可能性要从大到小。甚至可以采用二分法:

//---假设某参数的值非正即负,或查询二叉树,或查询不同SP的手机号
if(parse>0){
    if(parse>10){
        //code
    }else if(parse<5&&parse>1){
        //code
    }else{

    }
}else{
    //code 负数处理
}
复制代码

当然,这是个简单的栗子,还有很多其它的方式可以在代码中引入算法,提高效率,比如星期几的输出

function getweek(){
    var w = ['日','一','二','三','四','五','六'],
    now = new Date(),
    d = now.getDay();
    return '星期'+w[d];
}
复制代码

可以将字符串、变量、方法,存到数组或对象中。因为是引用,效率也非常快

递归

1.递归

//---阶乘
function facttail(n){
    if(n==0){
        return 1;
    }else{
        return n*facttail(n-1);
    }
}
//---幂次方
function fn(s,n){
    if(n==0){
        return 1;
    }else{
        return s*fn(s,n-1);
    }
}
复制代码

但是递归如果结束条件不明确就会导致一直运行,页面长时间不响应。处于假死状态!!而且,每个浏览器的“调用栈”都是有上限的。有兴趣的可以自己实验。为避免此问题,除了明确结束条件,还可以采用“尾递归” 2.尾递归

//---阶乘
function facttail(n,res){
    if(n<0){
        return 0;
    }else if(n==0){
        return 1;
    }else if(n==1){
        return res;
    }else{
        return facttail(n-1, n*res);
    }
}
//---幂次方
function fn(s,n){
    if(n==0){
        return 1;
    }else{
        return s*fn(s,n-1);
    }
}
复制代码

缓存记忆

利用闭包特性,某个方法内部可以存储计算过的数据或变量,比如阶乘函数重写

function memfacttail(n){
    if(!memfacttail.cache){
        memfacttail.cache = {
            "0":1,
            "1":1
        };
    }
    if(!memfacttail.cache.hasOwnProperty(n)){
        memfacttail.cache.n = n * memfacttail(n-1);
    }
    return memfacttail.cache.n;
}
复制代码

字符串和正则

*?+ 这个部份也需要蛮长的篇幅,占坑先。。。

快速响应的页面

老生常谈的内容,如果让页面秒开;可优化的点有哪些?服务器直渲、首页优化、组件懒加载、bigpipe、性能监控和针对性优化等等。

浏览器线程

先挖坑,我会再专门的文章里共享一点自己的心得

event loop

阮一峰老师说的更好,请移步链接This link

Web Workers

从Google的Gears插件提出了Worker Pool API,它就是Web Workers的“原型”,最初希望能够增强浏览器的功能,比如支持离线浏览(离线访问缓存页面,重新上线后提交离线操作),但(2015/11)已经被弃用了。HTML5开始Web Workers API被分离出去,成立单独的规范。自此,我们可以将计算、编解码、真正的异步请求等放到Web Workers里.

  • 运行环境 worker的global context并不是window,而是self,self也提供一系列接口,包括self.JSON、self.Math、self.console等等,最直观的区别是document对象没了,但location(readonly)、navigator还在;所以DOM访问也不存在。要启用它只能在创建一个独立的js文件并通过下面的方式调用
// html中直接写
var worker = new Worker('worker.js')
// 或通过主页面的js文件调用,例如:main.js
//---主页面
if (window.Worker) {
    var worker = new Worker('worker.js');
    var data = {a: 1, b: [1, 2, 3], c: 'string'};
    worker.postMessage(data);
    worker.onmessage = function(e) {
        console.log('main thread received data');
        console.log(e.data);

        // 接到消息立即停止worker,onerror将不会触发
        // worker.terminate();
        // terminate之后收不到后续消息,但post不报错
        // worker.postMessage(1);
    }
    worker.onerror = function(err) {
        console.log('main thread received err');
        console.log(err.message);
        // 阻止报错
        err.preventDefault();
    }
}
复制代码

worker.js

//---处理js,可以引入其它依赖
// importScripts('lib.js');
// importScripts('a.js', 'b.js');
onmessage = function(e) {
    console.log(self); // 看看global变量身上有些什么
    var data = e.data;
    console.log('worker received data');
    console.log(data);

    var res = data;
    res.resolved = true;

    postMessage(res);

    setTimeout(function() {
        throw new Error('error occurs');

        // close,立即停止,相当于主线程中的worker.terminate()
        // close();
    }, 100);
};
复制代码
  • 通信 主线程和worker线程收发消息方式一致(postMessage发,onmessage/onerror收,数据从MessageEvent的data属性取),PS:线程之间传递的是值copy,而不是共享引用
  • 加载外部文件 importScripts可以引入其它js文件,外部文件中的全局变量将被粘在self上,worker里可以直接引用。importScripts是同步的,下载并执行完毕后执行下一行,所以,需要注意阻塞性问题。 应用范围:
  • 音频/视频解码 如果尝试过audioContext.decodeAudioData之类的操作就会发现,我们迫切需要一个能“干重活”的后台线程
  • 图片预处理 比如头像上传前的裁剪,甚至添加水印、拼合、添马赛克,如果在客户端能够完成,就能避免大量的临时文件传输
  • 排序等数据处理算法 减轻服务器压力,遇到超大数据或超过200ms不能处理完毕的方法 数据过大的JSON对象,超出允许时间
//---main
var worker = new Worker('worker.js')
worker.onmessage = (e)=>{
    var jsonData = e.data // 回传回来的数据
    evaluateData(jsonData)
}
worker.postmessage(jsonText)
///---worker
self.onmessage = (e){
    var jsonText = e.data // main传过来的数据
    var jsonData = JSON.parse(jsonText) // 解析转换
    self.postMessage(jsonData)
}
复制代码
  • 客户端模版 比如markdown,或者服务端返回JSON,客户端拿到后交给后台线程解析并应用模版HTML生成页面,这些操作都由客户端完成的话,需要传输的东西就更少了
  • 共享worker 必须是同源!必须是同源!必须是同源!
//---main
var sWorker = new SharedWorker('worker.js')
sWorker.port.start()
//---first
first.onchange = function() {
  sWorker.port.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}
//---first
second.onchange = function() {
  sWorker.port.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

sWorker.port.onmessage = function(e) {
  result1.textContent = e.data;
  console.log('Message received from worker');
}
复制代码

通过这种方式建立前端的“类似”多线程


以上所述就是小编给大家介绍的《前端优化常用技术心得》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Mobilizing Web Sites

Mobilizing Web Sites

Layon, Kristofer / 2011-12 / 266.00元

Everyone has been talking about the mobile web in recent years, and more of us are browsing the web on smartphones and similar devices than ever before. But most of what we are viewing has not yet bee......一起来看看 《Mobilizing Web Sites》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具