烈虎

烈虎 查看完整档案

北京编辑聊城大学东昌学院  |  软件工程 编辑AB.inc  |  web工程师 编辑 peichenhu.cn 编辑
编辑

失去人性,失去很多;
失去兽性,失去一切。

个人动态

烈虎 发布了文章 · 2019-10-09

JS-new操作构造函数深度解析

现看一段很熟悉的代码:

function Parent(name) {
    this.name = name;
    this.say = function () {
        return this.name;
    };
}
Parent.prototype.age = 18;

var child = new Parent("pch");

console.log(child); // 输出: Parent { name: 'pch', say: [Function] }
console.log(child.name); // 输出:pch
console.log(child.say()); // 输出:pch
console.log(child.age); // 输出:18

说到继承就必先说构造函数 new Function()
那么 new 操作符究竟内部是如何实现的?
它是如何创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例?

查阅资料我们得知内部操作是:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。

现在我们自己实现一个 new Function() 同样功能的模拟构造函数 _new():

1、先创建我们的目标函数

function Parent(name) {
    this.name = name;
    this.say = function () {
        return this.name;
    };
    return {};
}

// 原型也加点属性测试原型链继承情况
Parent.prototype.age = 18;

2、创建我们的 _new 模拟构造函数:

function _new() { }

3、如何调用模拟构造函数呢?

我们假定这样调用:

var child = _new(Parent, "pch");

4、实现函数内部逻辑


function _new() { 
    // 4.1、我们处理参数
    // 检验第一个参数是不是函数。不是函数返回提示信息
    if (Object.prototype.toString.call(arguments[ 0 ]) !== "[object Function]") {
        throw "The first parameter must be a function.";
    }
    
    // 4.2、创建一个空的简单JavaScript对象(即{});
    
    var obj = {};
    
    // 4.3、链接该对象(即设置该对象的构造函数)到另一个对象(我认为说的是目标函数对象,绑定原型) ;
    obj.__proto__ = arguments[ 0 ].prototype;
    
    // 4.4、将步骤1新创建的对象作为this的上下文(大概是切换 this的意思,我们可以使用 apply 进行参数传递);
    var res = arguments[ 0 ].apply(obj, Array.prototype.slice.call(arguments).slice(1));
    
    // 4.5、如果该函数没有返回对象,则返回this(大概意思是说目标函数不存在返回值时,返回新对象,否则返回目标函数的返回值)。
    // 经过测试,使用 new 操作符时,
    // 目标函数返回值是 Object 就返回目标函数的返回值,
    // 否则就返回新对象
    return ( res instanceof Object ) ? res : obj;
}

5、测试我们的函数

// 测试代码

var child = _new(Parent, "pch");

console.log(child); // 输出: Parent { name: 'pch', say: [Function] }
console.log(child.name); // 输出:pch
console.log(child.say()); // 输出:pch
console.log(child.age); // 输出:18

// 测试错误传参
try {
    _new(1, 2, 3);
} catch (e) {
    console.log(e); // 输出:The first parameter must be a function.
}

// 测试带返回值的目标函数
function ParentHasReturn() {
    return "pch";
}

console.log(_new(ParentHasReturn)); // 输出:pch

6、总结:测试成功

补充内容:

  • 对象有__proto__属性,函数有prototype属性;
  • 对象由函数生成;
  • 生成对象时,对象的__proto__属性指向函数的prototype属性。
更多内容请移驾:我的博客
查看原文

赞 1 收藏 0 评论 0

烈虎 发布了文章 · 2019-09-20

React 封装 Echarts 公共组件

前序

LZ 之前工作一直在用 Vue,但最近听说 Vue 新版也要 All IN JS,所以想着干脆换到 React 算了,所以目前在学习 React + TS + Hook,顺手拿了一个老项目重构,今天主要讲 React 封装 Echarts 公共组件, 因为第一次正式搞,所以本文中如果有 React 代码哪里不规范还请大佬们批评指正哈!

目录:

  1. 需求分析
  2. 技术评估
  3. 实现思路
  4. 测试优化
  5. 总结分享

1. 需求分析

  • ECharts 图表要用到很多,所以要将公共部分抽取成组件减少重复代码
  • ECharts 是需要操作到真实dom的第三方库,和MVVM框架一起使用需要做一些特殊处理
  • Vue项目中使用的Vue-ECharts,组件自动去处理实例挂、 数据更新等
  • React也有echarts-for-react等优秀的开源组件,但为了学习和更舒适的使用,我们需要自己去造新的轮子
  • 参考资料:

2. 技术评估

我们需要用到四个东西:

  • React
  • React Hook
  • Echarts
  • TypeScript

3. 实现思路

1) 一个组件需要的参数配置 ChartProps

参数描述必填
keystring用于保持多图表时每一个图表的独立性
optionobject 或者 nullEcharts 配置参数
style { width: string, height: string }用于保持多图表时每一个图表的独立性
classNamestring组件样式类 className
onRenderonRender?(instance): void;渲染时回调函数,返回图表示例

2) 参数类型检查接口 interface

interface ChartProps {
    key: string;
    option: object | null;
    style: {
        width: string;
        height: string;
    };
    className?: string;
    onRender?(instance): void;
}

3) 基础组件 Chart.tsx

import * as React from "react";
import echarts from "echarts";

const Chart = (props: ChartProps): React.ReactElement => {
    // 挂载节点
    let chartDom = null;

    // 元素挂载到浏览器事件
    const refOnRender = (el): void => chartDom = el;
    
    // 返回组件
    return React.createElement("div", {
        ref: refOnRender,
        style: props.style,
        className: props.className
    });

};

export default Chart;

4) 当组件挂载到真实DOM,初始化Echarts实例,使用Hook

// 生命钩子函数
type Callback = () => void;
React.useEffect((): Callback => {

    // 加载状态
    function showLoading(instance): void {
        instance.showLoading("default", {
            text: "",
            color: "#c23531",
            textColor: "#000000",
            maskColor: "rgba(255, 255, 255, 0.8)",
            zlevel: 0
        });
    }

    // 获取实例对象
    let instance = echarts.getInstanceByDom(chartDom) || echarts.init(chartDom);

    // 默认加载状态
    showLoading(instance);
    
    // 如果存在参数,渲染图表
    if (props.option) {
        // 关闭加载状态
        if (instance) instance.hideLoading();
        // 渲染图表
        instance.setOption(props.option);
    }
    
}, [props.option]);

5)浏览器窗口大小变化时图表大小自适应重绘

// 大小自适应
const resize = (): void => instance.resize();
window.removeEventListener("resize", resize);
window.addEventListener("resize", resize);

6) 给图表加动画需要图表实例,设置回调函数将图表实例返回

// 回调函数返回实例
if (props.onRender) props.onRender(instance);

7) 组件销毁时清除 监听器组件状态值

// 销毁并清除状态
return (): void => {
    echarts.dispose(instance);
    window.removeEventListener("resize", resize);
};

8) 最终完整组件

import * as React from "react";
import echarts from "echarts";

/**
 * 参数列表
 * key: string; 唯一值
 * option: object | null; 图表数据
 * style: {
 *      width: string; 图表宽度
 *      height: string; 图表高度
 * };
 * className?: string; 图表CSS样式类名称
 * onRender?(instance): void; 图表回调函数返回图表实例
 */

interface ChartProps {
    key: string;
    option: object | null;
    style: {
        width: string;
        height: string;
    };
    className?: string;

    onRender?(instance): void;
}

const Chart = (props: ChartProps): React.ReactElement => {

    // 挂载节点
    let chartDom = null;

    // 生命钩子函数
    type Callback = () => void;
    React.useEffect((): Callback => {
        console.log("useEffect");

        // 加载状态
        function showLoading(instance): void {
            instance.showLoading("default", {
                text: "",
                color: "#c23531",
                textColor: "#000000",
                maskColor: "rgba(255, 255, 255, 0.8)",
                zlevel: 0
            });
        }

        // 获取实例对象
        let instance = echarts.getInstanceByDom(chartDom) || echarts.init(chartDom);

        // 大小自适应
        const resize = (): void => instance.resize();
        window.removeEventListener("resize", resize);
        window.addEventListener("resize", resize);

        // 默认加载状态
        showLoading(instance);

        // 渲染图表
        if (props.option) {
            if (instance) instance.hideLoading();
            instance.setOption(props.option);
        }

        // 回调函数返回实例
        if (props.onRender) props.onRender(instance);

        // 销毁并清除状态
        return (): void => {
            echarts.dispose(instance);
            window.removeEventListener("resize", resize);
        };

    }, [chartDom, props]);


    // 元素挂载到浏览器事件
    const refOnRender = (el): void => chartDom = el;
    
    // 返回组件
    return React.createElement("div", {
        ref: refOnRender,
        style: props.style,
        className: props.className
    });

};

// 导出组件模块
export default Chart;

测试优化

你可以:

  1. 自行搭建React+TS+Echarts开发环境
  2. 使用 Clone 本项目测试(暂不支持 sry)

主要代码 Test.tsx

import * as React from "react";
// 导入 Chart 组件
import Chart from "../chart";

const chartEmpty = {
    title: {
        text: "暂无数据",
        show: true,
        textStyle: {
            color: "grey",
            fontSize: 20
        },
        left: "center",
        top: "center"
    }
};

const Test= (): React.ReactElement => {
    // 图表1数据
    let [chart1Data, setChart1Data] = React.useState(null);
    // 图表2数据
    let [chart2Data, setChart1Data] = React.useState(null);
    
    // 防护监控数据 实例
    let [chart1, chart2] = [null, null];
    
    // 模拟异步更新图表数据
    function updateChart(): void{
        let opts: null;
        
        chartEmpty.title.text = "图表 1 暂无数据" + (+new Date());
        opts = JSON.parse(JSON.stringify(chartEmpty));
        setChart1Data(opts);
        
        chartEmpty.title.text = "图表 2 暂无数据" + (+new Date());
        opts = JSON.parse(JSON.stringify(chartEmpty));
        setChart2Data(opts);
    }
    
    // 获取图表实例,添加自定义图表事件
    React.useEffect((): void => {
        console.log("chart1", chart1);
    }, [chart1]);
    
    // 返回组件
    return (
        <div>
           <Chart
                key="chart1"
                className="chart1"
                option={chart1Data}
                onRender={(e): void => chart1 = e}
                style={{width: "100%", height: "400px"}}/>
           <hr>       
           <Chart
                key="chart2"
                className="chart2"
                option={chart2Data}
                style={{width: "100%", height: "400px"}}/>
           <button onClick={updateChart}> 异步更新图表数据 </button>
        </div>
    )
}

export default Test;

总结分享

学习了React组件的封装,由此及彼,以后其他第三方库应该也可以轻松集成了
学习了React Hook使用方法。
学习了React + TS的开发模式。
不到100行代码的组件相比于echarts-for-react等其他优秀组件的少了很多高级和细节自定义。(开源的话会加进去吧)
欢迎大佬检阅。

查看原文

赞 1 收藏 0 评论 0

烈虎 发布了文章 · 2019-06-02

使用 Vue Router 的 addRoutes 方法实现动态添加用户的权限路由

最近做vue 单页项目涉及到多角色用户权限问题,不同的角色用户拥有不同的功能权限, 不同的功能权限对应的不同的页面

git: https://github.com/pch1024/dy...

online: http://dynamicrouter.peichenh...

举个例子:
    角色A =>功能1
         =>功能2
         =>功能3
         
    角色B =>功能1
         =>功能4
         =>功能5

第1步 定义默认路由和动态路由

//动态路由(所有角色的都在这里,我们都做好组件页面了所以我们一定有这个,防君子不防小人)
export const dynamicRouter = [
    { path: '/b', name: 'b', component: pageB },
    { path: '/c', name: 'c', component: pageC },
];

//默认路由(无需登录就可以使用)
const routes = [
    { path: '/', redirect: '/login' },
    { path: '/login', component: pageLogin},
    { path: '/404', component: page404},
    { path: '*', redirect: '/404' },
];

const router = new VueRouter({
    mode: 'history',
    routes, // (缩写) 相当于 routes: routes
});

第2步 登录获取权限规则

当然,登录还要获取token、用户信息等,我们暂时不关注,我们的权限规则需要在多处使用所以我们将它存到vuex里

// vue 组件
<li @click="login(['b'])">
  模拟用户1登录,权限['b'],跳转到页面B
</li>
<li @click="login(['c'])">
  模拟用户2登录,权限['c'],跳转到页面B,(用户2没有页面B权限,强行进入会gun去页面404)
</li>

// 登录模块---------------------------------------------------------
import { mapActions } from "vuex";
export default {
  methods: {
    ...mapActions([
      "set_roleRouterRules"
    ]),
    login(roleRouterRules) {
      // 登录成功,vuex 存储角色路由
      this.set_roleRouterRules(roleRouterRules);
      // 跳转到动态注册B
      this.$router.replace({ path: "/b" });
    }
  }
};

// vuex 对应功能实现-----------------------------------------------
// 引入第1步 定义的dynamicRouter 
import { dynamicRouter } from './router';
// 私有变量
state: {
    isAddRoutes: false,
    // 后端返回的原始数据默认存到 localStorage,每次初始化取出来
    roleRouterRules: JSON.parse(localStorage.getItem('roleRouterRules')),
},
// 公共变量 => 派生私有变量
getters: {
    isAddRoutes: state => state.isAddRoutes,
    // 根据 roleRouterRules 生成当前角色的动态路由配置数据(addRoutes方法可以直接使用的路由数组)
    roleRouter: state => {
        if (state.roleRouterRules) {
            return dynamicRouter.filter(
                router => state.roleRouterRules.indexOf(router.name) >= 0,
            );
        } else return null;
    },
},
// 私有方法(同步) => 改变静态变量
mutations: {
    set_isAddRoutes: (state, data) => (state.isAddRoutes = data), // payload: true/false
    set_roleRouterRules: (state, data) => (state.roleRouterRules = data), // payload: true/false
},
// 公共方法(可异步)=> 调用私有方法
actions: {
    set_isAddRoutes({ commit }, data) {
        commit('set_isAddRoutes', data);
    },
    set_roleRouterRules({ commit }, data) {
        // 保存到vuex
        commit('set_roleRouterRules', data);
        // 保存到 localStorage,当用户强制刷新浏览器时我们要使用这一份数据初始化 state.roleRouterRules
        localStorage.setItem('roleRouterRules', JSON.stringify(data));
    },
}

第3步 登录成功跳转权限页面(核心)

基本思路:

  • 所有的路由跳转都要做鉴权,
  • 不是动态路由(也就是默认路由)直接放过,
  • 是动态路由(也就是还未创建的,强行进入会被重定向到404,但依然可以在to.redirectedFrom获取到用户希望进去的路由),检查前端是否有路由权限规则数据

    • 没有,让他去登录页
    • 有,就根据 roleRouterRules 生成当前角色的动态路由配置数据并addRoutes添加到真实router,此时通过let path = to.redirectedFrom || to.path;next(path); 再走一遍鉴权(这一次真实router上有它就进页面,还没有就代表这个用户没有这个页面访问权限gun去404)

注意事项:

  • addRoutes() 方法一个用户只能使用一次,所以要加一个状态值isAddRoutes到vuex里,每次用户进动态路由时检查 addRoutes 使用过没有
  • next() 方法没有参数会直接放行,有参数(例如 next({path:'/404'})) 放行后会再次进入router.beforeEach,一不小心就是死循环
import vuex from './vuex';
router.beforeEach((to, from, next) => {
    let path = to.redirectedFrom || to.path;
    // 白名单 放行
    if (whiteList.indexOf(path) >= 0) return next();
    // 黑名单
    if (!vuex.getters.roleRouter) return next({ path: '/login' });
    if (!vuex.getters.isAddRoutes) {
        console.log('path未注册,存在角色路由,立即注册尝试匹配');
        router.addRoutes(vuex.getters.roleRouter);
        vuex.dispatch('set_isAddRoutes', true);
        next(path);
    } else {
        console.log('已注册过动态路由,尝试匹配');
        next();
    }
});

第4步 切换不同角色用户

此处有坑, Vue Router 只提供了 addRoutes ,却没有删除和替换方法,所以只能通过强刷新浏览器来重置 Vue Router,先清空localStorage,在刷新时,初始化的Vue Router只有默认路由,用户只能去登录页了
还有一种方法我没看懂,感兴趣可以查看:https://github.com/vuejs/vue-...

exit() {
  localStorage.clear();
  window.location.reload();
}
查看原文

赞 0 收藏 0 评论 0

烈虎 发布了文章 · 2019-06-02

使用 Vue Router 的 addRoutes 方法实现动态添加用户的权限路由

最近做vue 单页项目涉及到多角色用户权限问题,不同的角色用户拥有不同的功能权限, 不同的功能权限对应的不同的页面

git: https://github.com/pch1024/dy...

online: http://dynamicrouter.peichenh...

举个例子:
    角色A =>功能1
         =>功能2
         =>功能3
         
    角色B =>功能1
         =>功能4
         =>功能5

第1步 定义默认路由和动态路由

//动态路由(所有角色的都在这里,我们都做好组件页面了所以我们一定有这个,防君子不防小人)
export const dynamicRouter = [
    { path: '/b', name: 'b', component: pageB },
    { path: '/c', name: 'c', component: pageC },
];

//默认路由(无需登录就可以使用)
const routes = [
    { path: '/', redirect: '/login' },
    { path: '/login', component: pageLogin},
    { path: '/404', component: page404},
    { path: '*', redirect: '/404' },
];

const router = new VueRouter({
    mode: 'history',
    routes, // (缩写) 相当于 routes: routes
});

第2步 登录获取权限规则

当然,登录还要获取token、用户信息等,我们暂时不关注,我们的权限规则需要在多处使用所以我们将它存到vuex里

// vue 组件
<li @click="login(['b'])">
  模拟用户1登录,权限['b'],跳转到页面B
</li>
<li @click="login(['c'])">
  模拟用户2登录,权限['c'],跳转到页面B,(用户2没有页面B权限,强行进入会gun去页面404)
</li>

// 登录模块---------------------------------------------------------
import { mapActions } from "vuex";
export default {
  methods: {
    ...mapActions([
      "set_roleRouterRules"
    ]),
    login(roleRouterRules) {
      // 登录成功,vuex 存储角色路由
      this.set_roleRouterRules(roleRouterRules);
      // 跳转到动态注册B
      this.$router.replace({ path: "/b" });
    }
  }
};

// vuex 对应功能实现-----------------------------------------------
// 引入第1步 定义的dynamicRouter 
import { dynamicRouter } from './router';
// 私有变量
state: {
    isAddRoutes: false,
    // 后端返回的原始数据默认存到 localStorage,每次初始化取出来
    roleRouterRules: JSON.parse(localStorage.getItem('roleRouterRules')),
},
// 公共变量 => 派生私有变量
getters: {
    isAddRoutes: state => state.isAddRoutes,
    // 根据 roleRouterRules 生成当前角色的动态路由配置数据(addRoutes方法可以直接使用的路由数组)
    roleRouter: state => {
        if (state.roleRouterRules) {
            return dynamicRouter.filter(
                router => state.roleRouterRules.indexOf(router.name) >= 0,
            );
        } else return null;
    },
},
// 私有方法(同步) => 改变静态变量
mutations: {
    set_isAddRoutes: (state, data) => (state.isAddRoutes = data), // payload: true/false
    set_roleRouterRules: (state, data) => (state.roleRouterRules = data), // payload: true/false
},
// 公共方法(可异步)=> 调用私有方法
actions: {
    set_isAddRoutes({ commit }, data) {
        commit('set_isAddRoutes', data);
    },
    set_roleRouterRules({ commit }, data) {
        // 保存到vuex
        commit('set_roleRouterRules', data);
        // 保存到 localStorage,当用户强制刷新浏览器时我们要使用这一份数据初始化 state.roleRouterRules
        localStorage.setItem('roleRouterRules', JSON.stringify(data));
    },
}

第3步 登录成功跳转权限页面(核心)

基本思路:

  • 所有的路由跳转都要做鉴权,
  • 不是动态路由(也就是默认路由)直接放过,
  • 是动态路由(也就是还未创建的,强行进入会被重定向到404,但依然可以在to.redirectedFrom获取到用户希望进去的路由),检查前端是否有路由权限规则数据

    • 没有,让他去登录页
    • 有,就根据 roleRouterRules 生成当前角色的动态路由配置数据并addRoutes添加到真实router,此时通过let path = to.redirectedFrom || to.path;next(path); 再走一遍鉴权(这一次真实router上有它就进页面,还没有就代表这个用户没有这个页面访问权限gun去404)

注意事项:

  • addRoutes() 方法一个用户只能使用一次,所以要加一个状态值isAddRoutes到vuex里,每次用户进动态路由时检查 addRoutes 使用过没有
  • next() 方法没有参数会直接放行,有参数(例如 next({path:'/404'})) 放行后会再次进入router.beforeEach,一不小心就是死循环
import vuex from './vuex';
router.beforeEach((to, from, next) => {
    let path = to.redirectedFrom || to.path;
    // 白名单 放行
    if (whiteList.indexOf(path) >= 0) return next();
    // 黑名单
    if (!vuex.getters.roleRouter) return next({ path: '/login' });
    if (!vuex.getters.isAddRoutes) {
        console.log('path未注册,存在角色路由,立即注册尝试匹配');
        router.addRoutes(vuex.getters.roleRouter);
        vuex.dispatch('set_isAddRoutes', true);
        next(path);
    } else {
        console.log('已注册过动态路由,尝试匹配');
        next();
    }
});

第4步 切换不同角色用户

此处有坑, Vue Router 只提供了 addRoutes ,却没有删除和替换方法,所以只能通过强刷新浏览器来重置 Vue Router,先清空localStorage,在刷新时,初始化的Vue Router只有默认路由,用户只能去登录页了
还有一种方法我没看懂,感兴趣可以查看:https://github.com/vuejs/vue-...

exit() {
  localStorage.clear();
  window.location.reload();
}
查看原文

赞 0 收藏 0 评论 0

烈虎 发布了文章 · 2018-07-03

nodejs 全自动使用 Tinypng (免费版,无需任何配置)压缩图片

## 无需任何插件, 随意CV,一行命令搞定:

node ./tinypng.js -f ./test -deep

大体思路:

  • 递归获取本地文件夹里的文件
  • 过滤文件,格式必须是.jpg.png,大小小于5MB.(文件夹递归)
  • 每次只处理一个文件(可以绕过20个的数量限制)
  • 处理返回数据拿到远程优化图片地址
  • 取回图片更新本地图片
  • 纯node实现不依赖任何其他代码片段

`

/** 
 * 帮助文档
 * -------
 * 
 * 获取帮助
 * 指令 -h
 * 
 * 获取命令执行文件夹 
 * 指令 -f 
 * 参数 ./
 * 必填,待处理的图片文件夹
 * 
 * 获取是否深度递归处理图片文件夹
 * 指令 -deep
 * 可选,默认不深度递归
 * 
 * 命令行脚本参考示例
 * > node ./tinypng.js -f ./test -deep
 *  */

const fs = require('fs');
const path = require('path');
const https = require('https');
const URL = require('url').URL;
const EventEmitter = require('events');
const err = msg => new EventEmitter().emit('error', msg);

if (getHelp()) return false;

const conf = {
    files: [],
    EntryFolder: getEntryFolder(),
    DeepLoop: getDeepLoop(),
    Exts: ['.jpg', '.png'],
    Max: 5200000, // 5MB == 5242848.754299136
}

fileFilter(conf.EntryFolder)

console.log("本次执行脚本的配置:", conf);
console.log("等待处理文件的数量:", conf.files.length)

conf.files.forEach(img => fileUpload(img));

//////////////////////////////// 工具函数

/**
 * 获取帮助命令
 * 指令 -h
 */
function getHelp() {
    let i = process.argv.findIndex(i => i === "-h");
    if (i !== -1) {
        console.log(
        * 帮助文档
        * -------
        * 
        * 获取帮助
        * 指令 -h
        * 
        * 获取命令执行文件夹 
        * 指令 -f 
        * 参数 ./
        * 必填,待处理的图片文件夹
        * 
        * 获取是否深度递归处理图片文件夹
        * 指令 -deep
        * 可选,默认不深度递归
        * 
        * > node ./tinypng.js -f ./test -deep
        )
        return true;
    }
}

/**
 * 获取命令执行文件夹 
 * 指令 -f 
 * 参数 ./
 * 必填,待处理的图片文件夹
 */
function getEntryFolder() {
    let i = process.argv.findIndex(i => i === "-f");
    if (i === -1 || !process.argv[i + 1]) return err('获取命令执行文件夹:失败');
    return process.argv[i + 1];
}

/**
 * 获取是否深度递归处理图片文件夹
 * 指令 -deep
 * 可选,默认不深度递归
 */
function getDeepLoop() {
    return process.argv.findIndex(i => i === "-deep") !== -1;
}

/**
 * 过滤待处理文件夹,得到待处理文件列表
 * @param {*} folder 待处理文件夹
 * @param {*} files 待处理文件列表
 */
function fileFilter(folder) {
    // 读取文件夹
    fs.readdirSync(folder).forEach(file => {
        let fullFilePath = path.join(folder, file)
        // 读取文件信息
        let fileStat = fs.statSync(fullFilePath);
        // 过滤文件安全性/大小限制/后缀名
        if (fileStat.size <= conf.Max && fileStat.isFile() && conf.Exts.includes(path.extname(file))) conf.files.push(fullFilePath);
        // 是都要深度递归处理文件夹
        else if (conf.DeepLoop && fileStat.isDirectory()) fileFilter(fullFilePath);
    });
}

/**
 * TinyPng 远程压缩 HTTPS 请求的配置生成方法
 */

function getAjaxOptions() {
    return {
        method: 'POST',
        hostname: 'tinypng.com',
        path: '/web/shrink',
        headers: {
            rejectUnauthorized: false,
            "X-Forwarded-For": Array(4).fill(1).map(() => parseInt(Math.random() * 254 + 1)).join('.'),
            'Postman-Token': Date.now(),
            'Cache-Control': 'no-cache',
            'Content-Type': 'application/x-www-form-urlencoded',
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
        }
    }
}

/**
 * TinyPng 远程压缩 HTTPS 请求
 * @param {string} img 待处理的文件
 * @success {
 *              "input": { "size": 887, "type": "image/png" },
 *              "output": { "size": 785, "type": "image/png", "width": 81, "height": 81, "ratio": 0.885, "url": "https://tinypng.com/web/output/7aztz90nq5p9545zch8gjzqg5ubdatd6" }
 *           }
 * @error  {"error": "Bad request", "message" : "Request is invalid"}
 */
function fileUpload(imgPath) {
    let req = https.request(getAjaxOptions(), (res) => {
        res.on('data', buf => {
            let obj = JSON.parse(buf.toString());
            if (obj.error) console.log(压缩失败!\n 当前文件:${imgPath} \n ${obj.message});
            else fileUpdate(imgPath, obj);
        });
    });

    req.write(fs.readFileSync(imgPath), 'binary');
    req.on('error', e => console.error(请求错误! \n 当前文件:${imgPath} \n, e));
    req.end();
}

// 该方法被循环调用,请求图片数据
function fileUpdate(entryImgPath, obj) {
    let options = new URL(obj.output.url);
    let req = https.request(options, res => {
        let body = '';
        res.setEncoding('binary');
        res.on('data', (data) => body += data);
        res.on('end', () => {
            fs.writeFile(entryImgPath, body, 'binary', err => {
                if (err) return console.error(err);
                let log = 压缩成功 ,
                log += 优化比例: ${ (( 1 - obj.output.ratio) * 100).toFixed(2) }% ,
                log += 原始大小: ${ (obj.input.size / 1024).toFixed(2) }KB ,
                log += 压缩大小: ${ (obj.output.size / 1024).toFixed(2) }KB ,
                log += 文件:${entryImgPath}
                console.log(log);
            });
        });
    });
    req.on('error', e => console.error(e));
    req.end();
}

// node ./tinypng.js -f ./static/smp/m/course_task

`

查看原文

赞 6 收藏 5 评论 7

烈虎 发布了文章 · 2018-06-05

数码照片Exif - Orientation 自动修正解决方案

使用Canvas + exif-js自动修正数码照片

使用场景,在做朋友圈 H5 时,时常遇到需要用户拍照上传图片需求,但是在一些手机(iso)上拍出来的照片会出现奇怪的旋转角度来呈现。经过各种百度才发现相机拍出来的图片拥有很多属性,其中一项是Orientation ,用于记录拍摄时相机物理旋转角度,例如把相机倒过来Orientation3,顺时针竖起来Orientation6,逆时针竖起来Orientation8,正常模式Orientation1。根据这个属性我们可以使用Canvas来对图片重绘。

Orientation 示意图

sadas

Show Code


import EXIF from 'exif-js'; // 引入依赖插件

// 参数列表:img 对象,callback返回Base64图片编码,生成图片质量默认值0.9
export const FixImg = (img, callback, quality = 0.9) => {
    
  let Orientation, ctxWidth, ctxHeight, base64; // 定义所需变量

  EXIF.getData(img, function() {
    Orientation = EXIF.getTag(this, 'Orientation');
    ctxWidth = this.naturalWidth;
    ctxHeight = this.naturalHeight;
    
    console.log(Orientation, ctxWidth, ctxHeight);

    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');

    canvas.width = ctxWidth;
    canvas.height = ctxHeight;
    if ([5, 6, 7, 8].includes(Orientation)) {
      canvas.width = ctxHeight;
      canvas.height = ctxWidth;
    }

    switch (Orientation) {
      case 2:
        ctx.transform(-1, 0, 0, 1, ctxWidth, 0);
        break;
      case 3:
        ctx.transform(-1, 0, 0, -1, ctxWidth, ctxHeight);
        break;
      case 4:
        ctx.transform(1, 0, 0, -1, 0, ctxHeight);
        break;
      case 5:
        ctx.transform(0, 1, 1, 0, 0, 0);
        break;
      case 6:
        ctx.transform(0, 1, -1, 0, ctxHeight, 0);
        break;
      case 7:
        ctx.transform(0, -1, -1, 0, ctxHeight, ctxWidth);
        break;
      case 8:
        ctx.transform(0, -1, 1, 0, 0, ctxWidth);
        break;
      default:
        ctx.transform(1, 0, 0, 1, 0, 0);
    }

    ctx.drawImage(img, 0, 0, ctxWidth, ctxHeight);
    
    // 默认输出jpeg,也可以读取原图片格式,最后输出原图格式,搜索关键词 :File.type
    base64 = canvas.toDataURL('image/jpeg', quality); 
    callback(base64);
  });
};

性感照骗,在线修复: http://peichenhu.cn/demo/awesome/#/Exif


相关补充:从图片 Exif 信息中取到 Orientation 后,就可以根据它来自动旋转图片了,canvas、filter 滤镜、vml、css3 都可以实现图片的旋转。
参考文章:https://imququ.com/post/how-t...

查看原文

赞 8 收藏 7 评论 0

烈虎 发布了文章 · 2018-04-21

JS代码复用模式

复用是一项非常重要的生活技能,因为生命是有限的,无意义的重复等于浪费生命。作为一个程序开发者,代码的复用既是一种能力,也是对积极生活的一种态度。那么JS 在代码复用方面都有哪些方法?
...................................................................................................

构造模式

构造函数与普通函数的唯一区别在于调用方式不同(构造函数首字母大写只是惯例),任何函数都可以用new关键字来作为构造函数调用(构造函数 = new + 普通函数)。

function Parent() {
  this.name = "jim";
  this.say = function() {
    console.log(this.name);
  };
  console.log(this.name);
}
Parent(); // 输出 jim
console.log(Parent); // 输出 Parent (){/* 函数体-略 */}

var child1 = new Parent(); // 输出 jim 构造函数创建 child1 对象(解析执行)
var child2 = new Parent(); // 输出 jim 构造函数创建 child2 对象(解析执行)

console.log(child1); // 输出 Parent {name: "jim", say: ƒ ()}
console.log(child1.say); // 输出 ƒ () {/* 函数体-略 */}

child1.say(); // 输出 jim
child2.say(); // 输出 jim

console.log(child1.name); // 输出 jim (child1 继承了 Parent name)
console.log(child2.name); // 输出 jim (child2 继承了 Parent name)

child1.name = "tom1"; // 修改 child 的 name 属性
child2.name = "tom2"; // 修改 child 的 name 属性

child1.say(); // 输出 tom1(说明 child 本地实例化了name属性 )
child2.say(); // 输出 tom2(说明 child 本地实例化了name属性 )
console.log(child1.name); // 输出 tom1(说明 child 本地实例化了name属性 )
console.log(child2.name); // 输出 tom2(说明 child 本地实例化了name属性 )

delete child1.name; // 删除 child1 的 name 属性
delete child2.name; // 删除 child2 的 name 属性

console.log(child1.name); // 输出 undefined(说明 child1 本地实例化name属性已删除 )
console.log(child2.name); // 输出 undefined(说明 child2 本地实例化name属性已删除 )

Parent(); // 输出 jim (说明构造函数属性 和 构造对象属性 没有关系)

缺点:无法复用父对象属性方法,当子对象数量变多,反复使用 new 重新创建父对象.

原型模式

我们知道所有引用类型都是 Object,也就是说引用类型的原型是 Object,他们是一个继承的关系。另外,原型的属性可以自定义。

function fn() {
  this.keyThis = ["fnThisValue"];
}
// name: "fn" prototype: {constructor: fn()} __proto__: Object
// 函数名是 fn
// 函数 prototype 指向一个对象,该对象的属性constructor 指向函数自身
// 函数 __proto__ 指向 Object(重点 __proto__ 是一个原型引用指针,指向父级原型)
// 此时fn 未执行, this 虽然指向window , 但是 keyThis 并未声明和赋值

// 以上是 JS 内部已经实现好的,下面我们来自定义一个原型属性
fn.prototype.keyProto = ["fnProtoValue"];
console.log(fn.prototype);
// 输出 {keyProto: ["fnProtoValue"], constructor: fn(),__proto__: Object}

var foo = new fn(); // fn() 执行, this指向window,key1声明和赋值
console.log(foo);
// 输出
// fn{
//    keyThis:["fooThisValue"],
//    __proto__:{ keyProto: ["fnProtoValue"], constructor: fn(), __proto__: Object}
// }
// foo 仅仅是一个构造对象(重点对象没有原型属性),原型引用指针__proto__指向 fn 的原型
// 原型链 就是 __proto__:{__proto__:{···}}

console.log(foo.keyThis); // 输出 ["fooThisValue"]
console.log(foo.keyProto); // 输出 ["fnProtoValue"]

foo.keyThis.push("fooThis");
foo.keyProto.push("fooProto");

console.log(foo);
// 输出
// fn{
//    keyThis:["fooThisValue", "fooThis"],
//    __proto__:{ keyProto: ["fnProtoValue", "fooThis"], constructor: fn(), __proto__: Object}
// }
// foo 的原型属性竟然被修改了,这应该不是我们想要的(小本本记下来),所以父级常量最好用 this 来定义

console.log(fn.prototype);
// 输出{ keyProto: ["fnProtoValue", "fooThis"], constructor: fn(), __proto__: Object}

缺点:虽然复用父对象属性方法,当子对象数量变多,反复使用 new 重新创建父对象.

借用模式

JS 基础数据类型操作系列(四)函数 中,我们介绍了 call,apply 和 bind 的函数作用域借用操作,这也是一种代码复用的好方法。

function Parent() {
  this.keyThis = ["fnThisValue"];
}
Parent.prototype.keyProto = ["fnProtoValue"];
function Child() {
  Parent.call(this);
  console.log(this.keyThis); // 输出 ["fnThisValue"]
  console.log(this.keyProto); // 输出 undefined
}
Child();
// 这种借用只能够针对 this 绑定的属性方法起作用。

var jim = new Child();
console.log(jim.keyThis); // 输出 ["fnThisValue"]
console.log(jim.keyProto); // 输出 undefined
// 这种借用只能够针对 this 绑定的属性方法起作用。

代理模式

function inherit(parent, child) {
  var F = function() {};
  F.prototype = parent.prototype;
  child.prototype = new F();
  child.prototype.constructor = child;
}

function Parent() {
  this.keyThis = ["fnThisValue"];
}
Parent.prototype.keyProto = ["fnProtoValue"];

function Child() {}
inherit(Parent, Child);

var jim = new Child();
console.log(jim.keyThis); // 输出 undefined
console.log(jim.keyProto); // 输出 ["fnProtoValue"]

缺点:只是代理了原型

标准模式

在 ES 5 中,提供了Object.create()方法来实现原型构造继承(语法糖)。
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

语法 :Object.create(proto, [propertiesObject]) 。

第二个可选参数是 null 或一个对象,添加到新创建对象的自定义可枚举属性,对应 Object.defineProperties()的第二个参数。

function Parent() {}
Parent.prototype.keyProto = ["fnProtoValue"];

var jim = Object.create(Parent, {
  key: { value: "val" }
});

console.log(jim); // 输出 Function {key: "val",__proto__: Parent()}
jim.hasOwnProperty("key");


var Fn = {
    key:"value"
}
Object.create(Fn)
// {__proto__:{ key:"value"}}

克隆模式

通过复制属性来实现继承

浅克隆

简单对象,单层克隆

function extend(parent, child) {
  var i;
  child = child || {};
  for (i in parent) {
    if (parent.hasOwnProperty(i)) {
      child[i] = parent[i]; // 这里只是引用, 并非实例化
    }
  }
  return child;
}

var Parent = {
    key:"value",
    arr:[1,2,3,4],
    obj:{
        key:"value",
        arr:[1,2,3,4],
    }
}
var kid = extend(Parent)
kid.arr.push(4);
console.log(Parent.arr)  // 输出 [1,2,3,4,4]

深克隆

复杂对象,递归克隆

function extendDeep(parent, child) {
  var i,
    toStr = Object.prototype.toString,
    astr = "[object Array]";
  child = child || {};
  for (i in parent) {
    if (parent.hasOwnProperty(i)) {
      if (typeof parent[i] === "object") {
        child[i] = toStr.call(parent[i]) === astr ? [] : {};
        arguments.callee(parent[i], child[i]);
      } else {
        child[i] = parent[i];
      }
    }
  }
  return child;
}
var Parent = {
    key:"value",
    arr:[1,2,3,4],
    obj:{
        key:"value",
        arr:[1,2,3,4],
    }
}
var kid = extendDeep(Parent)
kid.arr.push(4);
console.log(Parent.arr)  // 输出 [1,2,3,4]

缺点:针对的是对象,不是函数,当然对象用这个是最好的

总结

综上了解,我们想要一个既可以继承this属性,又可以继承prototype属性的方法。继承this属性最好用的是借用模式,继承prototype属性最好用的是Object.create()标准模式。

function parent() {
  this.money = 1000;
}
parent.prototype.say = function(money) {
  console.log("I have " + (this.money + money));
}

function inherit(parent,childParams){
    function Child() {
        parent.call(this);      // 借用 父级 this 属性
    }
    childParams = childParams || {}; // 定义额外参数
    Child.prototype = Object.create(parent.prototype,childParams);
    // parent.prototype 指向原型对象parent Prototype
    // Object.create(parent.prototype)
    // 输出 {__proto__:{ say:ƒ (money),constructor:ƒ parent(), __proto__:Object}}
    Child.prototype.constructor = Child; // 原型的构造函数应该永远指向自身
    return new Child()
}

var jim = inherit(parent);
var tom = inherit(parent,{key:{value:500}});
jim.say(100);   //输出 I have 1100
tom.say(500);   //输出 I have 1100
tom.key         //输出 500
查看原文

赞 1 收藏 5 评论 0

烈虎 发布了文章 · 2018-04-16

Local Storage 操作小插件 iStorage 介绍

iStorage

Local Storage 是 HTML 5 新增的一个本地存储 API,所谓 Local Storage 就是一个小仓库的意思,它有 5 M 的大小空间,存储在浏览器中,我们可以通过 JavaScript 来操纵 Local Storage。

iStorage 介绍

iStorage是针对浏览器 Local Storage 的一个便捷操作插件。它支持用户直接存储获取数字(非 NaN)、字符串、数组、JSON 类型数据。

iStorage 是基于原生 JavaScript 实现的,它编译压缩后仅 2kb 左右,请放心试用。也是作者的第一个 NPM 插件,不足之处还请见谅和指正。

iStorage 安装

iStorage 支持 CommonJSRequireJS<script> 方式引入。

// NPM 安装
npm i istorage

// ES6
import iStorage from "istorage";

// require
var iStorage = require('istorage');

// html
<script data-original="http://peichenhu.cn/doc/lib/iStorage.min.js"></script>

iStorage 使用

iStorage 支持 getLengthgetIndexgetItemsetItemremoveItemclearAll 操作。debug 尾参数可选,用于在控制台打印操作详情日志。

// 获取 Local Storage Length
iStorage.getLength(debug: Boolean);

// 根据 Local Storage 的长度(length)作为索引值,来获取键名
iStorage.getIndex(index:Number, debug: Boolean);

// 依据键名获取值
iStorage.getItem(key: String, debug: Boolean);

// 添加键值对,值类型允许长度为0
iStorage.setItem(key:String, value:!NaN || Number || String || Array || JSON, debug:Boolean)

// 删除键值对
iStorage.removeItem(key: String, debug: Boolean);

// 清楚该域的所有Local Storage 记录
iStorage.clearAll(key: String, debug: Boolean);

iStorage 本地测试

// 请先fork,下载到本地后命令行初始化开发调试环境:
> npm i // 安装 uglify-js 用于压缩生产
> npm test // webpack-dev-server open: http://localhost:9000/

// 若要修改插件,比如 0.0.3 版,请参考以下指令
// 修改未压缩版本 iStorage.js, 修改完后,使用 uglify-js 压缩
> uglifyjs iStorage.js  -m -c -o iStorage.min.js

iStorage 未来

使用 TypeScript 重构,添加 sessionStorage 和 cookie,并希望支持异步操作.

iStorage 更新日志

0.0.1

第一版代码比较粗糙,仅用于验证代码结构和方法的设计、插件的实际用途。基本实现了数字、字符串、数组、JSON 对象四种基本数据的类型检测,直接存储和获取;方法上实现了:

  • check: 检查某个键名是否存在
  • get:获取某个键名的值
  • set:设置键值对
  • remove:根据键名删除某个键值对

详见 redeme@0.0.1

0.0.2

该版本在代码上进行了完善,并提供了更好 debug log 信息。

详见 redeme@0.0.2

0.0.3

此版本改动较大,涉及方法名称的语义化,新方法的添加,check 方法废弃,新增本地 Webpack 调试代码,debug log 信息进一步优化。

查看原文

赞 0 收藏 0 评论 1

烈虎 关注了标签 · 2018-04-16

关注 2

烈虎 关注了标签 · 2018-04-15

tween.js

用js封装的很多的动画效果函数

关注 1

认证与成就

  • 获得 17 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • iStorage

    localStorage 小插件,支持直接存储和获取数字(非NaN)、字符串、数组、JSON类型数据.支持npm和常规安装

注册于 2016-03-06
个人主页被 881 人浏览