Giter VIP home page Giter VIP logo

weekly's People

Contributors

xdimh avatar yf0721 avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar

weekly's Issues

react native Image defaultSource

/**
 * 加载网络图片的Image组件(提供默认图片)
 * @author rainypin
 * @log
 */


import React, {Component, PropTypes} from 'react';
import {
  View,
  Image
} from 'react-native';

export default class NetworkImage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      loaded : false
    };
    this.prefetchTask = Image.prefetch(props.source.uri);
  }

  static defaultProps = {};
  static propTypes = {
    defaultSourceStyle : PropTypes.oneOfType([
      PropTypes.object,
      PropTypes.number
    ]),
    ...Image.propTypes
  };
  componentWillMount() {
    let self = this;
    this.prefetchTask.then(()=>{
      self.setState({
        loaded : true
      });
    },err => {
      self.setState({
        loaded : false
      });
    }).catch(err => {
      self.setState({
        loaded : false
      });
    })
  }
  renderImage() {
    let { loaded } = this.state;
    if(loaded) {
      return (
        <Image
          {...this.props}
        />
      )
    } else {
      return (
        <View style={[this.props.defaultSourceStyle]}>
        </View>
      )
    }
  }

  render() {
    return this.renderImage();
  }
}

使用ngrok让你的本地mock可以提供给外网访问

使用ngrok让你的本地mock可以提供给外网访问

问题: 前端开发往往需要在本地启动mock服务提供模拟数据进行代码调试和自测。简单的mock已经足够满足本地代码调试和自测。然而开发过程中往往需要和其他端进行联调或者将代码作为demo给别人展示,例如:在开发完react-native页面,需要将页面嵌入到原生代码中,和native的同学对接,这个时候本地的mock接口往往是这样的接口地址http://localhost:9000/getData不能成功被请求,如果处于同一个内网这个时候可以通过写死接口ip地址来解决,如果不在同一个内网则(远程协作等情况)的时候,往往需要对接代码的人在本地也同样mock一份数据,过程较为复杂。

ngrok 介绍

ngrok 可以让本地地址穿透nat,firewall映射到能够在外网被访问的某个域名地址。这样本地请求可以改成使用这个统一的域名地址去请求,从而对接的时候不用手动去修改代码中的请求地址,提高了对接联调的效率。然后天下没有免费的午餐,自定义子域名这个功能只有付费的用户才能使用,但国内已经有人无私贡献了ngrok服务,造福于开发者,网站地址ngrok 国内

ngrok 安装使用

  • 下载对对应平台的ngrok (eg: mac osx)
  • 解压ngrok
    官方版本只有一个ngrok可执行文件 ,国内版本有两个文件一个ngrok(后续需要手动添加可执行权限)和 ngrok.cfg 配置文件。国内重命名为ngrok2并添加可执行权限 mv ngrok ngrok2 && chmod u+x ngrok2
  • mv unzip-path/ngrok unzip-path/ngrok2 /usr/local/bin 或者 再path环境变量中添加解压后的目录路径。
  • 使用
    1. 配置ngrok 配置文件 (国内版本不需要)

       authtoken: youtoken 
       json_resolver_url: ""
       dns_resolver_ips: []
       tunnels:
           dev:
               addr: 9000
               auth: username:paswd
               proto: http
         #    subdomain: xxxxx 付费用户才能用
      

      token获取

    2. 启动本地mock

       [SUCCESS] 16:52:38 mock 服务启动成功: http://:::9000
      
    3. 启用ngrok

       国内 : ngrok2 --config=ngrok.cfg -subdomain xxx 9000 
       官方: ngrok start --config=ngrok.yml --all
      
    访问地址分别是: http://xxx.tunnel.qydev.com/getDatahttp://b79fb37b.ngrok.io/getData

alias 命令

设置别名:在~/.bashrc 或者 ~/.bash_profile 中添加alias ngrok2="ngrok2 --config=/usr/local/opt/config/ngrok2.cfg" 就可以直接使用ngrok2 -subdomain xxx 9000 启动服务
ngrok 运行结果

正则表达式正向环视逆向环视

  • // 对手机号码参数进行调整,去除手机号码中非数字字符,然后对手机号码应用格式化正则规则
    • // (?=(?:\d{4})+$) 这个部分匹配以整数倍4个数字结尾的位置
    • // (\d) 这里用来匹配一个数字,即只有以整数倍4个数字结尾的位置前面还有一个数字的情况下才需要添加空格。
    • // 因JavaScript 不支持逆向环视(逆向断言) 所以不能用replace(/(?<=\d)(?=(?:\d{4})+$)/g,' ');
    • return (num + '').replace(/[^\d]/g, '').replace(/(\d)(?=(?:\d{4})+$)/g,'$1 ');

Redux 之我见

Redux 之我见

  一个单页应用,最重要的在于应用的数据管理,状态维护,还有页面的路由管理。Redux作为一个JavaScript应用的可预测状态容器,不仅仅给你很爽的编程体验,而且让你写的应用在不同平台上有很好的一致性。

  到目前为止,在两个项目中用到了Redux,一次是结合react框架,一次是结合网易的regular框架。对于一个比较复杂的系统,在开发之前我们往往会进行组件的划分,从顶层组件到底层的子组件,从容器组件到展示组件。在没有引进Redux之前,我们往往需要自己去维护整个系统的状态的管理,状态的一致性。一般我们会将子组件状态的变更通过emit事件到上层组件,然后再由上层组件emit事件告知顶层根组件,然后顶层根组件接收到事件和数据,变更状态,然后将新的状态数据一层一层的下发下去。如下图:

没有引入redux之前

这样会造成顶层更组件逻辑随着应用系统的逐渐复杂变成越来越庞大,即使对顶层逻辑进行拆分,整个系统的状态State的管理也不会很轻松,且组件直接的通信都需要通过事件的方式告知共同的父组件才能进行,随着系统的复杂度的提高,将会导致代码变得杂乱难以维护和扩展。所以Redux恰到好处的出现,作为整个系统状态State的管家,使得你可以专心的去关注业务逻辑代码,省去了很多状态管理上耗费的精力,使得代码也变得更清晰更容易扩展和维护。引入Redux之后的组件关系图如下:

引入Redux之后

底层组件状态变更或者与其他组件交互的时候不再需要一层一层的往上抛出事件,然后由上层组件去管理了,而是通过Redux,通过dispatch一个action给Redux,然后Redux进行相应的处理,告知顶层组件有部分状态已经发生变化,然后顶层组件告知相应的子组件进行状态变更,渲染页面。这样,你的状态处理的逻辑不再维护在顶层组件里,而且Redux可以很方便的看到action处理前的状态和action处理后的状态,一旦逻辑出现问题,可以很快的进行定位。同时组件的交互不再是杂乱无章的,而是通过Redux,再由顶层组件下发数据变更,使得数据交互变得更单一更清晰。

那么Redux内部用了什么黑科技?

  Redux本身很简单,我们的应用系统的整个状态一般会表示成一个对象,对象中存放了应用系统的各个状态,而Redux就替我们接管了这个对象,管理着这个对象的变更。在Redux中,我们只能通过dispatch action来变更这个State对象,然后Redux会有相应的叫Reducer的函数去处理这个action,这个函数接受两个参数,当前State对象,和action对象,然后进行相应的处理返回新的State。Redux再把State的变更告诉整个应用,此时应用再去进行相应变更的渲染。那么这里提到的action,Reducer是什么? action 和 Reducer又是怎样对应起来的? Redux又是怎么样将整个应用状态数据变更告知应用的?

  1. 黑科技之action

    Redux的三大原则其中之一就是:唯一改变State的方式是dispatch action。什么是action,action其实就是对发生的事情的一个描述对象。比如一次用户按钮点击,一次ajax请求,请求数据的返回等等这些在Redux中都可以用一个唯一对应的action去描述他们。在这个对象中必须包含一个key为type的字段,值为字符串常量,代表这个action的类型。这是用于后面Reducer处理的时候区分action的关键字段。比如这里的按钮点击,我们的action的type值可以是XXX_BUTTON_CLICK。因为是字符串常量,所以这里用大写进行区分。除了type字段之外,你还想在action中添加什么字段完全取决你自己,一般情况下我们有可能会带上需要传给Reducer的数据信息。所以完整的action可能会像下面一样:

       //action 示例
       {
           type : "XXX_BUTTON_CLICK",
           payload : {
              data1 : "data1"
              ...
           }
       }
  2. 黑科技之action creator

    顾名思义,action creator即那些仅仅用来创建并返回action对象的函数。这些函数很简单,只做一件事情,所以很方便测试。如上action,我们就可以有一个action creator来生成并返回上面的XXX_BUTTON_CLICK action,如下:

     function xxxButtonClick() {
        return {
            type : 'XXX_BUTTON_CLICK',
            payload : {
                "data1" : "data1",
                 ...
            }
        }
     }
  3. 黑科技之Reducer

    前面的action定义并描述了发生的事件,但是并没有指定对应的处理方法,那么这里的Reducer就是用来处理action所描述的事件的函数。该函数接受两个参数,分别是previous state 和 action。然后返回新的state。之所以称为Reducer,主要是因为这个函数的行为和数组原型中的reduce方法很像,Array.prototype.reduce()。在应用最开始的时候,Redux在没有初始state的时候,就会给state赋值undefined,为了避免这种情况,所以在Reducer的时候应该设置一个初始化状态。如下:

    function xxxButtonClickReducer(state = initialState,action) {
        // todo something
        return newState;   
     }

    在实践过程中,我们发现整个系统的State会变得比较大,而每一个Reducer需要关注的状态并不是全部的State,可能是其中的某一个状态数据,所以如果每次都将全部State传给只关注部分状态数据的Reducer,就会导致这个Reducer的处理逻辑变得复杂,你不得不在这个Reducer中通过action的type来处理特定的数据,这样就导致switch case变得冗长。所以Redux提供了一种拆分Reducer的方式,让某个Reducer只关注他需要关注的状态数据。最后在通过Redux的 combineReducers,将多个拆分的Reducers合成一个rootReducer,这个rootReducer会返回所有全部的state状态数据。代码如下:

       function visibilityFilter(state = 'SHOW_ALL', action) {
         switch (action.type) {
           case 'SET_VISIBILITY_FILTER':
             return action.filter
           default:
             return state
         }
       }
       
       function todos(state = [], action) {
         switch (action.type) {
           case 'ADD_TODO':
             return [
               ...state,
               {
                 text: action.text,
                 completed: false
               }
             ]
           case 'COMPLETE_TODO':
             return state.map((todo, index) => {
               if (index === action.index) {
                 return Object.assign({}, todo, {
                   completed: true
                 })
               }
               return todo
             })
           default:
             return state
         }
       }
       
       import { combineReducers, createStore } from 'redux'
       let reducer = combineReducers({ visibilityFilter, todos })
       let store = createStore(reducer)

    在了解action 和 处理action的Reducer之后,我们来看下Redux是怎么将这两者结合起来的。

  4. 黑科技之Store

    Redux的三大原则之一的第一个原则说的就是:单一数据源原则,整个应用的数据状态将被Redux的一个Store维护着。Store主要有以下功能:

    • 管理着整个应用的状态
    • 允许我们通过getState()来访问整个应用的状态
    • 允许我们通过dispatch(action)来更新应用的状态。
    • 注册应用状态变更事件监听函数subscribe(listener)
    • 解绑状态变更事件监听函数,通过使用subscribe(listener)的返回值。

    我们通过调用Redux提供的createStore方法来创建一个store 对象,第一个参数为rootReducer,第二个参数可以是initialStatestore 对象提供了几个方法,分别是dispatch,subscribe,用来让我们能够将 actionReducer 关联在一起,将整个 store 维护的 state 的变更告知整个系统应用。通过调用store.dispatch(action)可以让对应的 Reducer 进行处理,从而响应action描述的事件,完成应用状态的变更。再通过store.subscribe(fn)在fn回调函数中通过store.getState()获取到变更后的应用状态state,然后从应用的顶层组件下发到底层的子组件更新相应的页面展示。

更进一步

  在页面交互过程中存在着两种操作,同步操作和异步操作,对于同步操作页面需要等待完这个操作结束才能继续响应用户,而异步的操作页面可以继续响应用户的操作,待异步结果返回再相应的更新页面状态。那么作为描述这些事件的对象action,也存在同步和'异步',这里的异步指的是通过一个中间件,提供的类似语法糖的功能,可以让你的actionCreator返回的不是action对象而是一个function,在这个function中存在着异步操作,以及针对异步操作的每个阶段所dispatch的action。

  我们举个栗子,对于ajax请求的操作,我们的页面往往有几种状态的变更,第一,请求发起(这个时候页面会出现loading状态,俗称转菊花),这个时候会有一个action表示开始请求事件。第二,请求成功响应,这个时候页面会获取到服务器返回的数据,更新UI,这个阶段会有一个action,描述请求成功事件,并会将成功的数据带上给Reducer处理更新相应的UI。第三,在没有第二的情况下,请求失败,这个时候的action代表的是请求失败事件,UI往往会弹出失败提示。所以在一次异步过程中,你将会进行三次dispatch。

   面对一次异步操作就需要三次action,让事情变得繁琐,但Redux提供了对应的方法来解决这个问题,让你在需要异步请求的时候,只有触发封装好了三个异步请求的函数,就可以完成以上过程。如下:

const ImportRecordsActionCreators = {
    startLoadRecords : () => {
        return {
            type : 'START_LOAD_IMPORT_RECORDS'
        }
    },
    onRecordsLoad : (data) => {
        return {
            type : 'ON_IMPORT_RECORDS_LOAD',
            payload : data
        }
    },
    onRecordsFail : (err) => {
        return {
            type : 'ON_IMPORT_RECORDS_FAIL',
            payload : err
        }
    },
    getImportRecords : (params) => {
        params.id = '601';
        return function (dispatch) {
            dispatch(ImportRecordsActionCreators.startLoadRecords());
            $.ajax({
                type: 'post',
                url: '/slice/explore',
                data: params,
                dataType: 'json',
                success: (data) => {
                    dispatch(ImportRecordsActionCreators.onRecordsLoad(data));
                },
                error: (err) => {
                    dispatch(ImportRecordsActionCreators.onRecordsFail(err))
                }
            });
        }
    }
};

export default ImportRecordsActionCreators;

为了使得store能够处理function形式的action,在创建store的过程中需要传入redux-thunk提供的中间件thunkMiddleware,对于中间件,我们后面会讲到。先来看下代码:

import thunkMiddleware from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers'

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware, // lets us dispatch() functions
  )
)

这样你的store就具备了dispatch异步action(函数)的能力。这样我们只要调用dispatch(ImportRecordsActionCreators.getImportRecords)就可以一次触发过程中所涉及到的action了。

中间件

  中间件这个词对大家来说并不陌生,一般都是在输入和输出中间进行处理的那层插件,对于Express 和 Koa 用户来说,中间件就是请求之后和响应之前中间处理层,比如添加CORS headers, 日志记录, 压缩等。而对于Redux中间件来说,主要是一些第三方扩展,用在dispatch action之后和action 到达具体的Reducer进行处理之前的那层,用于日志记录,崩溃报告,处理异步action,路由等。Redux中间件的代码结构我们一redux-thunk为例子,代码如下:

//redux-thunk代码
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
  }

  return next(action);
};
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

Redux中间件的写法可以参考这个插件,通过一个函数创建返回如下形式的函数:

({dispatch, getState}) => next => action => {
    //里面为这个中间件的处理逻辑
    //处理完后不要忘记将action交由下一个中间件处理
    //此时需要调用next(action)
    next(action);
}

Reducer 处理逻辑拆分新思路

  根据之前Reducer的介绍,如果系统应用的状态变得复杂,数据层次嵌套多的时候,必然需要我们去拆分Reducer的处理逻辑,让多个Reducer处理不同的,自己关心的状态数据,最后通过combineReducer进行整合。但是这也会突出新的问题,嵌套层数多的State就需要嵌套的combineReducer,可以参看深入到源码:解读 redux 的设计思路与用法这篇文章提出的针对复杂State的三种解决方案。这里我们提出一种新的方式,使得Reducer的逻辑能得以拆分。

  在这种方案下我们会按业务拆分Reducer,相同业务的Reducers被拆分到一个单独的文件里,通过Map的形式代替switch case语句来组织相同业务的Reducers,其中Map的key为对应Reducer所要处理的action中的type,Map的值为对应的Reducer函数。然后我们会在一个名为reducer.js的文件中导入所有的Reducers,导出一个rootReducer用于创建store。如下:

  • import-records-reducers.js

    import {message} from 'antd';
    const immutable = require("object-path-immutable");
    
    export default {
        'START_IMPORT_RECORDS' : (state) => {
            return immutable(state).set('xxx.loading',true).value();
        },
        'ON_IMPORT_RECORDS_LOAD' : (state,payload) => {
            let immOp = immutable(state).set('xxx.loading',false);
            if(payload) {
               // todo something
            }
            return immOp.value();
        },
        'ON_IMPORT_RECORDS_FAIL' : (state,payload) => {
            return immutable(state).set('xxx.loading',false).value();
        }
    }
  • reducer.js

     import r1 from 'reducers/import-records-reducer';
     
     //初始State状态
     const initialState = {
         
     }
     
     //合并所有reducer Map
     const reducerMap = $.extend({}, r1/*, r2, r3, r4, r5, r6,r7,r8*/);
     
     //导出rootReducer
     export default function (state = initialState, action) {
         if (reducerMap[action.type]) {
             return reducerMap[action.type](state, typeof action.payload === 'undefined' ? null : action.payload);
         } else {
             return state
         }
     } 

这样我们的action也就可以根据业务进行划分到不同的文件里,这样使得代码更加便于维护和扩展。

最后

Redux固然好,但是我们需要根据我们自身项目的实际情况,从需要解决什么问题出发,去考虑是不是一定需要引入Redux,以及引入Redux利弊之间的权衡,以到达发挥工具所应有的作用。

参考资料

Redux 官方文档

React 生命周期

React 的生命周期

对于React的生命周期,只要知道在整个生命周期里,有的生命周期函数只调用一次,有的在每次数据更新的时候会被反复调用。如下图:

react-life-cycle

React 生命周期的三个阶段

1. 组件初始化

初始化阶段执行的生命周期函数(除了render)在整个生命周期中只会被执行一次。

  • getDefaultProps

    设置组件的初始属性,ES6写法和这里的ES5写法有所区别:

      static defaultProps = {
        defaultTxt : 'AA'
      };
    
  • getInitialState

    设置组件的初始状态,ES6写法和这里的ES5写法有所区别,是在ES6类的构造器中进行初始化:

     constructor(props) {
        super(props);
        this.state = {
          state1 : 'xxx'
        };
      }
    
  • componentWillMount

    组件即将被挂载前执行的生命周期函数,在这个函数里是组件挂载之前修改组件状态的最后机会。这个函数只会在第一次初始化的时候才会被执行到,后续组件存在期,组件状态的变更将不再被执行。

  • render

    将React Elements 渲染进DOM,整个生命周期被执行最多的函数,渲染的时候采用的是最小化增量更新,所以理论上React 会有不错的性能。

  • componentDidMount

    React 组件以及成功挂载到DOM中的hooks函数。在这个函数内可以对组件进行DOM操作,在初始化阶段,只有在这个方法内才能够通过this.refs.elementId读取到组件的DOM节点。

2. 组件存在期

组件的存在期,应该是整个生命周期中最长的一个阶段,也是组件响应用户的行为的一个阶段。

  • componentWillReceiveProps(nextProps)

    组件在存在期接受的父组件提供的属性发生变化,或者调用了setProps修改组件属性的时候将会被调用,开始一个组件更新的cycle。

  • shouldComponentUpdate(nextProps,nextState)

    这个生命周期方法只会在存在期阶段被调用,当组件的属性发生变化(父组件下发的属性改变,setProps被调用),组件的状态发生改变(setState被调用)该方法就会在一个cycle中被执行到且只有当该方法返回true,cycle中后续的函数才有机会被执行,不然后续函数将不会被执行,所以这个函数常常作为React性能优化的地方,省去不必要的渲染更新。

  • componentWillUpdate(nextProps,nextState)

    组件更新之前会被执行的函数。

  • render

    同初始阶段,但在componentWillUpdate之后componentDidUpdate之前执行。

  • componentDidUpdate(prevProps,prevState)

    组件成功更新后的hooks函数。当组件更新完成会被调用。

3. 组件销毁&清理

当切换页面,或者切换组件的时候,组件就会被销毁,这个时候,往往需要对组件进行一些清理操作,释放一些资源。

  • componentWillUnmount

    这个方法用来在组件销毁前做一些清理,释放占用资源,解除事件绑定等操作。整个生命周期会被执行一次。

react native 知识点&遇到的坑记录

// let self = this;
//   setTimeout(()=>{
//     self.refs.parent.measure((...args) => {
//       console.log('measure:',args);
//     });
//     self._child.measureInWindow((...args) => {
//       console.log('measureInWindow:',args);
//     });
//     self._child.measureLayout(ReactNative.findNodeHandle(this.refs.parent),(...args) => {
//       console.log('measureLayout:',args);
//     });
//   },0);
  }
}

base64的编码和解码

base64的编码和解码

base64编码在各种编码中应该算是比较简单的一种了,在前端中很多地方有被应用到,小图片base64后内联,与客户端交互的jsBridge中数据的base64编码传输,小程序中字体图标base64后内联等等。这次在项目中用到了base64的编码和解码,网上搜了一把有很多base64操作的js实现,之前一直对base64编码半知半解,看着代码中的各种位操作也是云里雾里,所以借这次项目机会稍微深入的了解了下base64这个东西。

什么是base64编码?

对于base64 我们首先需要先看下ASCII编码,想必大家都知道在计算机内部所有的信息数据都表现为二进制的形式,就是那些0101数字串,每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000到1111111。ASCII码就是用后7位二进制表示了128个字符,这对英语来说是够用了,所需要的字母都能在这后7位中表现出来。那么base64编码的规则又是什么,base64就是选出64个字符作为一个基本的字符集,然后在将其他文字符号都转换成这个字符集中的字符以予表示。这64个字符分别是a-z,A-Z ,0-9,符号+-,除了前面几位还有=占位符,不属于所表示的内容。

字符base64编码的几个步骤

  1. 将待转换的字符串用二进制的形式表示出来。
  2. 然后每三个字节一组,也就是24个二进制位分成一组。
  3. 再将这24个二进制位分成6组,每四个一组,每组6位二进制位。
  4. 在每一组最前面添加两个00补全成八位,使得24位变成32位刚好凑成4个字节。
  5. 然后计算每个字节所表示的数值(10进制),根据下表查表拼装转换后的字符形成最后base64字符。
数值 符号 数值 符号 数值 符号 数值 符号
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 18 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w
15 P 32 g 49 x
16 Q 33 h 50 y

在转换的过程中可以发现,并不是所有的带转换字符串最后表示的二进制串所含的字节数都是3的倍数。所以针对这些不到3个字节的情况,会有相应的处理方式。

  1. 最后剩两个字节的情况

    分成三组,前两组最前面加00组成两个字节,后面剩下的4位最前面加两个0,最后面加两个0,组成一个字节,最后补上一个=构成四个字节。

  2. 最后只剩一个字节的情况

    分成两组,第一组6位最前面添加两位0,后面还剩2位,在最前面添加两个0,然后在最后面添加四个0构成两个字节,补上两个=,构成四个字节。(为什么前面要补两个00,这样计算二进制一个字节所表示的数值才能一一映射到64个字符中)

Unicode

Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。但并没有规定具体在计算机中的存储方式。Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。

UTF-8就是其中的一种实现方式。后面会讲Unicode的编码方式如何转换成UTF-8实现方式的。Unicode有17个code plane,其中0x0000 ~ 0xffff 称为基本多语言平面,0x10000 ~ 0x10ffff 16个为辅助平面。其中基本多语言平面已经涵盖了大部分常用字,如大部分的汉字,所以只需要对这个范围进行处理已经够用。参考Unicode字符平面映射

UTF-8 和 Unicode之间的转换关系

首先,UTF-8是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码,是在互联网上使用最广的一种Unicode的实现方式。特点就是一种变长的编码方式,可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

  1. 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
  2. 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。
  3. 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。
  4. 其他极少使用的Unicode 辅助平面的字符使用四字节编码

具体的转换对应关系如下表:

code point UTF-8字节流
U+00000000 – U+0000007F 0xxxxxxx
U+00000080 – U+000007FF 110xxxxx 10xxxxxx
U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

由上表可见,转换后的字节数由第一个字节二进制串从左到右1的位数决定,0表示一个字节,110表示两个字节,1110对应三个字节,11110四个字节,后续字节都以10开始。根据这个规律我们就可以在代码实现上进行对Unicode和UTF-8之间进行转换。

JavaScript内部使用的编码方式

JavaScript 引擎内部是自由的使用 UCS-2 或者 UTF-16。大多数引擎使用的是 UTF-16,无论它们使用什么方式实现,它只是一个具体的实现,这不会影响到语言的特性。然后对于 ECMAScript/JavaScript 语言本身,实现的效果是通过 UCS-2,而非 UTF-16。参考:JavaScript 的内部字符编码是 UCS-2 还是 UTF-16

所以对于JavaScript,无论是UCS-2还是UTF-16都是一样,采用的是两个字节来存储字符。

ECMAScript source text is represented as a sequence of characters in the Unicode character encoding,version 3.0 or later. ... ... ECMAScript source text is assumed to be a sequence of 16-bit code units for the purposes of this specification. Such a source text may include sequences of 16-bit code units that are not valid UTF-16 character encodings. If an actual source text is encoded in a form other than 16-bit code units it must be processed as if it was first converted to UTF-16. 参考:ECMA-262 5.1 Edition

为了在加密解密中文字符不出现乱码,所以需要在将中文字符编码成base64之前,先将UCS-2/UTF-16 转换成 UTF-8 (这里只考虑中文字符是UTF-8的情况),然后再应用base64编码规则进行编码得到最终结果。同样在解码的时候需要按照base64编码规则反向操作转成UTF-8格式,然后再将UTF-8转回成UCS-2/UTF-16

UTF-8 和 JavaScript 内部编码互相转换实现。

首先,了解JavaScript中几个方法String.charCodeAt,String.fromCharCode(),Number.prototype.toString

  • String.charCodeAt

    charCodeAt() 方法返回0到65535之间的整数,表示给定索引处的UTF-16代码单元 (在 Unicode 编码单元表示一个单一的 UTF-16 编码单元的情况下,UTF-16 编码单元匹配 Unicode 编码单元。但在——例如 Unicode 编码单元 > 0x10000 的这种——不能被一个 UTF-16 编码单元单独表示的情况下,只能匹配 Unicode 代理对的第一个编码单元) 。如果你想要整个代码点的值,使用 codePointAt()。

    '中'.charCodeAt(0);
    20013
  • String.fromCharCode

    charCodeAt的反向操作

    String.fromCharCode(20013);
    "中"
  • Number.prototype.toString

    将十进制码点转换成2进制。

    var code = 20013;
    code.toString(2);
    "100111000101101"

互相转换源代码如下:

  • UTF-16 -> UTF-8
    const Base64 = {
        ...,
         _utf8_encode: function(str) {
            // 将换行符统一成\n
            str = str.replace(/\r\n/g, "\n");
            let out = "";
            for (var n = 0; n < str.length; n++) {
              let unicode = str.charCodeAt(n);
              if ((unicode >= 0x0001) && (unicode <= 0x007f)) {
                //在这个范围内的是ASCII字符,只需一个字节。
                out += str.charAt(n);
              } else if (unicode > 0x07ff) {
                //将16位unicode前四位和1110xxxx 进行拼接
                out += String.fromCharCode(0xe0 | ((unicode >> 12) & 0x0f));
                //将接下来的6位和10xxxxxx进行拼接
                out += String.fromCharCode(0x80 | ((unicode >>  6) & 0x3f));
                //将接下来的6位和10xxxxxx进行拼接
                out += String.fromCharCode(0x80 | ((unicode >>  0) & 0x3f));
              } else {
                //将16位unicode前5位和110xxxxx 进行拼接
                out += String.fromCharCode(0xc0 | ((unicode >>  6) & 0x1f));
                //将接下来的6位和10xxxxxx进行拼接
                out += String.fromCharCode(0x80 | ((unicode >>  0) & 0x3f));
              }
            }
            return out;
          },
        ...
    }
  • UTF8 -> UTF-16
    const Base64 = {
      ...,
       _utf8_decode: function(str) {
          let out = "",n = 0, c1,c2,c3;
          c1 = c2 = c3 = 0;
          while (n < str.length) {
            c1 = str.charCodeAt(n);
            if (c1 < 0x80) {
              //编码为0xxxxxxx 表示utf8 一个字节
              out += String.fromCharCode(c1);
              n++
            } else if (c1 > 0xc0 && c1 < 0xe0) {
              //编码为110xxxxx 10xxxxxx 表示2个字节
              c2 = str.charCodeAt(n + 1);
              out += String.fromCharCode((c1 & 0x1f) << 6 | c2 & 0x3f);
              n += 2
            } else {
              //编码为1110xxxx 10xxxxxx 0xxxxxxx 表示utf8 三个字节
              c2 = str.charCodeAt(n + 1);
              c3 = str.charCodeAt(n + 2);
              out += String.fromCharCode((c1 & 0x0f) << 12 | (c2 & 0x3f) << 6 | c3 & 0x3f);
              n += 3
            }
          }
          return out
        },
      ...
    }

base64编码和解码的实现

  • base64编码
    const Base64 = {
      ...,
      //base64 所用的64个字符和其中的一个补位符'='
      _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
      encode: function(str) {
        //base64转换算法就是根据具体的规则将3个字符变成四个字符。
        let out = "",c1,c2,c3,
          outC1,outC2,outC3,outC4,i = 0;
        str = Base64._utf8_encode(str); //将utf16 转换成utf8,因为JavaScript内部采用的是utf16存储所以要进行一步转换。
        while (i < str.length) {
          // 三个三个字符一组进行转换
          c1 = str.charCodeAt(i++);
          c2 = str.charCodeAt(i++);
          c3 = str.charCodeAt(i++);
          outC1 = c1 >> 2; //第一个字符最前面添加两个0,剩余2位用作后面拼接
          outC2 = (c1 & 0x03) << 4 | c2 >> 4; // 第一个字符剩下两位和第二个字符前四位拼接,并在前面添加2个0拼成一个字符
          outC3 = (c2 & 0x0f) << 2 | c3 >> 6; //第二个字符剩余4位和第三个字符的前两位,并在前面添加2个0拼接成一个字符
          outC4 = c3 & 0x3f; //第三个字符剩下的6位前面添加两个0 拼接成一个字符
          //如果c2为不存在则最后两个字符为补位符'=' 如果c3不存在 则转换后最后一位为补位'='
          if (isNaN(c2)) { outC3 = outC4 = 64 } else if (isNaN(c3)) { outC4 = 64 }
          out = out + this._keyStr.charAt(outC1) + this._keyStr.charAt(outC2) + this._keyStr.charAt(outC3) + this._keyStr.charAt(outC4)
        }
        return out
      },
      ...
    }
  • base64 解码
    const Base64 = {
        ...,
        decode: function(str) {
            let out = '',c1,c2,c3,c4,outC1,outC2,outC3,i = 0;
            //去掉非base64字符
            str = str.replace(/[^A-Za-z0-9+/=]/g, "");
            //循环处理进行解码
            while (i < str.length) {
              //4个base64字符一组,解码后将转换成3个字符
              c1 = this._keyStr.indexOf(str.charAt(i++));
              c2 = this._keyStr.indexOf(str.charAt(i++));
              c3 = this._keyStr.indexOf(str.charAt(i++));
              c4 = this._keyStr.indexOf(str.charAt(i++));
              //每个字符前面都会有两个前导0
              outC1 = c1 << 2 | c2 >> 4; //第一个base64字符去掉两个0后和第二个字符的开头两个字符拼成一个字节
              outC2 = (c2 & 0x0f) << 4 | c3 >> 2; //第二个剩下的4位和第三个开始的四位拼成一个字节
              outC3 = (c3 & 0x03) << 6 | c4; // 第三个剩下的2位和第四个6位拼成一个字节
              out = out + String.fromCharCode(outC1);
              //如果倒数第二个不是补位符'='
              if (c3 != 64) { out = out + String.fromCharCode(outC2) }
              //如果倒数第一个不是补位符'='
              if (c4 != 64) { out = out + String.fromCharCode(outC3) }
            }
            out = Base64._utf8_decode(out); // 将utf8转成utf16
            return out
          },
        ...
    }

完整代码

参考资料

  1. 维基百科UTF-8
  2. 维基百科Unicode
  3. 字符编码笔记:ASCII,Unicode和UTF-8 —— 阮一峰
  4. Base64笔记 —— 阮一峰
  5. Unicode与JavaScript详解 —— 阮一峰
  6. Unicode编码及其实现:UTF-16、UTF-8,and more
  7. 通过javascript进行UTF-8编码

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.