Giter Site home page Giter Site logo

blog's Introduction

Hi there 👋

Zhouzhili's github stats

blog's People

Contributors

zhouzhili avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

shouxie

blog's Issues

webGL 学习总结 - 持续更新

平移和缩放

  1. 需要掌握基本的矢量知识及相关运算方法

  2. 矢量运算中不满足乘法交换律,及 AxB 和 BxA 是不相等的,在设置变换矩阵时,应当设置为

// 点位
attribute vec4 aPosition;
// 变换矩阵
uniform mat4 uTranslateMatrix;
void main() {
  // 变换矩阵×点位矩阵
  gl_Position =uTranslateMatrix * aPosition;
}

要注意矩阵的位置,变换矩阵 × 点位矩阵

  1. gl.readPixels

readPixels

需要注意的是,前两个参数 x,y 代表的坐标系原点是 canvas 左下角,向上为 Y 轴正方向,向右为 X 轴正方形

面试常问题

1.Vue生命周期(6个)

beforeCreate,created,beforeMounte,mounted,beforeDestroy,destroyed

2.JWT是否了解,

答:原理不是很了解,只知道token的使用

3.VueX的API有哪些,一般怎么使用,用在什么地方?

答:token的存储,用户信息的存储,公共数据的存储。

Api:state、getter、mutation、action、(map…),

Store本质是在构造函数中new 一个Vue示例vm,vm的data为{state},同时设置state的get和set方法,get直接返回vm.state,set则发出警告,只能通过mutation进行设置数据。

getter可以对数据进行缓存,即为vm的computed属性,调用getter的时候实际是调用内部vm对应的computed属性。

mutation函数会存到一个数组中(因为会有重名),然后调用commit的时候会拿出对应的方法对state进行修改。

Action会判断函数是否有then方法,进行异步调用。

namespace本质是state.[namespace],map...方法做了转换

4.Vue-Router 是否了解,原理是什么?

Vue路由有2种,hash和history,hash监听hashChange, history监听popstate事件。

5.箭头函数的特点,有无arguments,是否可以使用new ,为什么

答:箭头函数为匿名函数,作用域为其上层作用域,this为其上层的this,无arguments参数,不可以作为构造函数。

6.Webpack的loader和plugin的区别

Loader是对各种文件的加载和处理,为流处理,上一个loader的结果为下一个的参数。

Plugin是插件,是对webpack的补充,在webpack的loader处理之后对结果进行修改,它是基于事件机制,在webpack编译过程的事件进行监听,处理相关结果。

7.Websocket是否了解,在哪些地方用会比较合适?

最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。(聊天室)

8.ES6 的import 、export是否有了解,谈下自己的认识

Import是静态引入,路径不能有参数,

9.路由权限有无做过?

有,在meta中配置角色标签,根据登录用户的角色去匹配,动态生成用户的路由表。AddRoutes方法;也可以直接根据用户在后端配置好路由表,再通过接口获取用户的路由。

10.Location.href 和vue-router 跳转有什么区别?

无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动

11.ES6的Set和Map有什么用处,let和var的区别

let的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效.

12.Promise如何在成功和失败后都去掉页面请求的loading?

答:在finally中处理

13. 协商缓存和强缓存

  1. 强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码;

  2. 协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;
    两者的共同点是,都是从客户端缓存中读取资源;区别是强缓存不会发请求,协商缓存会发请求

14.Vue 指令的钩子有哪些?指定的参数有什么,指令会执行几次?

钩子函数有:bind,inserted,update,componentUpdate,unbind。

参数:el,name,binding,vnode,oldVnode

15.Vue create 里写一个this.$nextTick(()=>console.log(‘aa’)) ,mounted里面也有console.log() ,谁先执行?

16.浏览器内核知道多少? (这简直了,谁知道)

Trident(IE)、Gecko(firefox)、Blink(Chrome新)、Webkit(safari)

17.浏览器多个标签页之间如何通信? (worker可以)

Worker 可以跨域通信,postmessage,

Localstorege也可以跨标签使用。

18.有没有做过一些组件封装 ?(table页面的封装)

19.页面为什么会白屏,以及对首页白屏的处理

在地址栏输入URL会经历以下步骤:

1、 查找域名DNS,找到对应服务器的IP,

2、 建立TCP连接,向服务器发出请求。

3、 服务器响应请求,浏览器拿到数据进行解析

4、 浏览器拿到html,从上向下进行解析渲染,解析DOM和CSS,计算页面布局,遇到JS会进行解析,运行JS,阻塞页面渲染。一边渲染一边显示页面内容,图片等内容会异步加载。

优化:选择合适的CDN加速,提高服务器带宽和响应速度,首屏减少内容,按需加载,优化JS位置,防止页面阻塞,使用骨架屏,防止用户等待过久。

20.web标签语义化怎么理解

21.iframe通信问题及处理

22.浏览器兼容性问题,css,布局相关

23.浏览器安全相关的问题

24.Taro和Uniapp的区别

25.VueRouter的钩子函数

26.瀑布流和懒加载

27.ES6用过哪些

28.CSS会阻塞JS文件执行吗

会,CSS不会阻塞DOM树的解析,但是会阻塞渲染,以及JS的执行。(不然的话,如果在JS里获取DOM元素的颜色,如果不阻塞的话会导致JS不可靠)

29.微任务和宏任务,哪些是微任务哪些是宏任务

JS事件循环,分为同步任务和异步任务,异步任务会放到事件循环任务队列,当同步任务执行完以后,会把异步事件队列中的任务放到主线程中。

微任务先执行,再执行宏任务

宏任务常见的有:setTimeout、setInterval、setImmediate

微任务有:Promise、process.nextTick、MutationObserver

30.堆和栈简单说一下

堆是存放引用类型,栈是存放基本类型

31.一段函数需要执行60多秒,怎么防止页面阻塞,任务如何拆分

32.多段文字如何查找到某个字符在哪一段

33.闭包是什么?会导致内存泄漏吗

JS里面只有函数作用域和函数作用域,闭包就是在一个函数里面返回另外一个函数,返回的函数绑定了它所在函数的当前上下文。闭包不会导致内存泄漏,闭包会导致变量无法回收,占用大量内存,在旧的IE浏览器中会导致内存泄漏。

34.如何查找内存泄漏,如何分析

35.vue vnode和diff算法简单说一下,什么是vnode?

虚拟DOM 就是使用js模拟真实DOM,所有操作都在虚拟DOM上进行,数据更新时对比虚拟DOM,计算出更新DOM的最小操作,由框架去完成DOM的更新,让用户更专注于业务逻辑。

36.Vue修改一个组件的数据,更新对比是从该组件开始还是从根节点开始

应该是在该组件进行更新,因为有依赖收集,通知依赖的地方进行更新。

37.Vue一个Home页面有A,B 2个组件,它们的生命周期是怎么样的?

一个页面模板结构如下:

<div>
    <A/>
    <B/>
</div>

首先是Home组件初始化,调用 beforeCreatecreatedbeforeMount ;然后根据子组件顺序,分别调用A,B组件的 beforeCreatecreatedbeforeMount 生命周期钩子函数,然后是A组件的 mounted , B 组件的mounted ,最后是 Home的 mounted

销毁的时候:先是Home 的 beforeDestroy ,然后是A 的 beforeDestroydestroyed , B的beforeDestroydestroyed ,最后Home的destroyed

38.Vue组件里是先解析template还是先挂载data

先初始化data,原因是在created生命周期完成数据初始化,数据设置为响应式,在getter收集依赖,在setter中通知视图进行更新,然后在beforeMount里编译模板,这时候模板里使用到数据的时候会出发getter依赖被收集,因此,先初始化data,再初始化模板,然后挂载渲染。

工作知识点记录

1.Axios 在 GET 请求中传递数组问题

今天在重构之前写的代码的时候,发现一个接口请求数据有问题,传递给后端的是一个数组,但是在控制台里面却变了样,参数名称后面诡异的加了一个'[]',回忆了一下自身并没有对这个参数进行了格式化操作,Google 一番发现这是 axios 的锅

经过实验,axios 的 get 方法中使用 params 时对于 js 数组类型的参数的默认操作比较诡异,会使得参数名后带上'[]'字符串,原因是 axios 对 params 格式化采用的是qs这个库,因此可以使用 qs 自带的 arrayFormat 参数配置解决这个问题,大致配置如下:

const qs = require('qs')

axios.get(url, {
  params: {
    arr: [1, 2, 3]
  },
  paramsSerializer: function(params) {
    return Qs.stringify(params, { arrayFormat: 'repeat' })
  }
})

后端接收为 url-query 数组,即'id=1&id=2&id=3'

2. vue 组件没有 prototype

如果是在组件中需要挂载全局变量的话,没法直接使用 this.prototype,组件实例没有这个属性,只有proto,但是这个属性是被废弃的,不建议使用。在组件中使用 this.$options._base 即为 Vue 构造函数,在构造函数原型上挂载属性即可挂载到 Vue 全局中,在所有组件中使用。
参考:Vue 技术揭秘:构造子类构造函数

3.在项目中组件的封装

项目中的组件常分为基础组件和业务组件,基础组件为对一些页面基本构成部分的封装,如 element-ui 等属于这一类,业务组件则为适应业务要求而对基础组件进行的再封装,一般基础组件的适应性更强。

  • 在组件封装中不要过度封装,组件应该着重于展示部分,接收数据,根据数据展现出应有的形态,应本着高内聚,低耦合原则。prop 尽量只传递一些基本数据类型,并做数据验证。不要将非展示的逻辑强行封装到组件中,导致组件适应性差,不应把父组件应处理的逻辑放到组件中处理,组件只接受数据,返回渲染结果

  • 业务组件可以适当的将业务逻辑封装进组件内部,有时候业务性质高度统一,但是又是不同的页面只是数据接口改变而已,这样可以将整个页面进行封装,实现复用。

示例图

如上图所示,在产品中已对 QueryForm 和 table 进行了一次封装,但是现在有 2 个类似的页面,都有对表格数据的增删改查需求,表格操作按钮的弹窗和逻辑都高度相似,UI 部分和业务逻辑部分都基本不变,只是接口名改变了。简单的方式就是新建一个文件,代码复制一遍。如果封装的话,需要把各自的增删改查接口通过 prop 传进组件里面才能实现最大的复用。因此我选择了后者,如果后面再出一个类似的页面就可以直接使用了。但是问题是需要增删改查接口传递的参数一致才行,不然需要针对每个接口做数据格式化,写一堆 if else 语句来进行判断,因此业务组件的耦合性相当高,扩展性不强。如果把页面所需要处理的事件都冒泡到父组件来处理的话,那也就失去了组件化的意义。如何对组件进行封装以及封装到哪种地步都需要根据情况来考虑。

我封装的组件使用方式如下所示:

<template>
  <div>
    <div v-if="showIndex" class="flex-1-box">
      <service-list
        service-type="mysql"
        :get-table-list-api="getMysqlList"
        :init-api="init"
        :update-api="update"
        :rest-api="resetPsw"
      />
    </div>
    <router-view v-else></router-view>
  </div>
</template>

简单解析虚拟DOM

目前前端开发框架 2 巨头ReactVue都使用到了虚拟 DOM(virtual dom)技术,以及现在面试基本都会问到的问题:你了解虚拟 DOM 吗?那么,现在我就来简单的聊一聊虚拟 DOM

为什么要用虚拟 DOM

在 web 开发中,操作 DOM 是非常损耗性能的,由于浏览器 JS 是单进程,如果在页面渲染方面占用了太长时间,那么在功能影响方法就会堵塞,十分影响用户体验。同时,现代 web 应用越来越庞大,功能也更加复杂,以及 AJAX 的使用,页面也能响应更多的用户操作,页面的改变也越来越大,如果使用以前的 JQuery,那么 web 开发工作将会越发复杂,需要不停的响应用户的操作,更新数据,更新页面,从开发维护到应用性能上都很受影响。

如何提高开发效率,减少维护成本以及提高 web 应用性能呢?React给我们带来了virtual dom

什么是虚拟 DOM

虚拟 DOM 就是使用 JS 模拟出来的 DOM 结果,比如一段简单的 HTML:

<div id="title">this is title</div>

使用 JS 我们可以这样表示:

{
  "tagName": "div",
  "props": {
    "id": "title"
  },
  "children": ["this is title"]
}

对于更复杂的 HTML 标签,我们在 JS 里都可以用以上的结构来模拟:tagNamepropschildren。这样,我们就可以用 JS 来描述页面 UI 了。

如何解析虚拟 DOM

有了虚拟 DOM 之后,我们页面要呈现出来还是需要转换为 HTML 标签才行,那么现在就是如何解析虚拟 DOM 了,现在,我们有这样一段 VNode:

const vNode = {
  tagName: 'div',
  props: null,
  children: [
    {
      tagName: 'p',
      props: {
        class: 'list p',
        onClick: e => {
          console.log(e)
        }
      },
      children: ['apple']
    },
    {
      tagName: 'p',
      props: { class: 'list p' },
      children: ['orange']
    }
  ]
}

通过分析可以发现,这是一个 DIV 标签,同时含有 2 个 P 标签,我们首先通过tagName创建DOMElement,如果没有tagName我们创建TextNode,然后为 Element 添加属性以及子元素,我们可以设计一个 render 函数来解析它,render 函数接收 vNode 以及解析后挂载的位置:

function render(vNode, dom) {
  // 文本标签
  if (isString(vNode)) {
    const text = document.createTextNode(vNode)
    dom.appendChild(text)
    // 数组
  } else if (isArray(vNode)) {
    vNode.forEach(child => render(child, dom))
    // 对象
  } else if (isObject(vNode)) {
    const { tagName, props, children } = vNode
    const root = document.createElement(tagName)
    dom.appendChild(root)
    if (props) {
      addAttribute(root, props)
    }
    if (isArray(children)) {
      children.forEach(child => render(child, root))
    }
  }
}

这里还有一些判断 JS 类型的工具函数:

const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]'
const isObject = obj => Object.prototype.toString.call(obj) === '[object Object]'
const isString = str => Object.prototype.toString.call(str) === '[object String]'

在属性解析里面,我们还可能会有事件需要设置,我们编写一个简单的属性设置函数:

function addAttribute(el, props) {
  Object.keys(props).forEach(key => {
    const val = props[key]
    // 添加事件
    if (key.startsWith('on')) {
      const eventName = key.toLowerCase().slice(2)
      if (eventName) {
        el.addEventListener(eventName, val, false)
      }
    } else {
      el.setAttribute(key, val)
    }
  })
}

使用虚拟 DOM

这样一个简单的虚拟 DOM 解析函数就写好了,它可以根据虚拟 DOM 渲染成真实的 UI 界面,如何测试并使用它呢?我们可以使用 Jason Miller 开发的 htm,它可以无需编译即可在浏览器中使用,十分简单,使用如下:

import htm from '../src/index.mjs'

function h(tagName, props, ...children) {
  return { tagName, props, children }
}

const html = htm.bind(h)

let fruits = ['apple', 'orange']

const handleClick = (e, index) => {
  console.log(e, index)
}

const vNode = html`
  <div>
    ${fruits.map(
      (f, i) =>
        html`
          <p class="list p" onClick=${e => handleClick(e, i)}>${f}</p>
        `
    )}
  </div>
`

render(vNode, document.body)

这样就可以轻松把我们的虚拟 DOM 渲染到页面上了。但是仅仅这样使用的话还不如直接插入innerHTML,在效率上并没有很大的提高,其实虚拟 DOM 的优势还需要结合diff算法才能体现,现在我们只是实现了虚拟 DOM 的解析工作。

以上只是一个简单的虚拟 DOM 示例,实际中,我们可能还会包括类组件,函数组件等,我们需要对他们进行一一的解析,后面将会逐步展开。

其实,虚拟 DOM 真正的意义在于使用函数式来开发 UI,使得UI=h(vNode),其中的 h 函数就是我们解析 VNOde 的函数,并且其他与 UI 相关的绘制工作我们都可以使用函数式来完成,唯一需要修改的就是我们的解析函数了,无论怎么样UI=h(vNode)

webpack 区分环境使用CDN以及HtmlWebpackPlugin插件的编写

webpack 区分环境使用CDN以及HtmlWebpackPlugin插件的编写

最近项目由1.0版开发到了1.1版,内容越来越多,引入的库也越来越多,每次打包的时候webpack都会提示,包的大小太大什么的,影响性能,作为一个有追求的前端当然不会放任不管,于是,Google一番,引入cdn,在index.html中写入

 <script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
 <script src="https://unpkg.com/[email protected]/dist/vuex.min.js"></script>
 <script src="https://unpkg.com/[email protected]/lib/index.js"></script>

在webpack.base.conf.js中添加要忽略的包

 externals: {
    vue: 'Vue',
    vuex: 'Vuex',
    'element-ui': 'ELEMENT'
  }

一切都看起来很不错,but,当我背着电脑回家,打开项目,npm run dev,输入localhost:8080,页面出不来,额,忘了没有联网。。。但是,没有网就不能开发吗,显然不能,于是注释掉上面这些代码,页面终于出来了,这么注释来注释去,不够优雅,我只想在生产环境下使用CDN,在开发环境下还是想用本地的,有了问题我们来解决

区分环境使用CDN

HtmlWebpackPlugin插件是我们开发时基本上必用的一个webpack插件,网上的介绍也很多,它主要是完成对index.html的js,css等资源的注入,同时它也能定义一些变量,template即我们的模板文件如果没有使用loader的时候,默认以ejs格式处理,这样,我们就可以在index.html方便的处理了

  • 首先,去掉index.html中我们添加的CDN引用

  • 去掉webpack.base.conf.js中的externals配置,将其放到webpack.prod.conf.js中

  • 在webpack.prod.conf.js中的HtmlWebpackPlugin中添加一个变量cdn,最终修改为如下所示:

    new HtmlWebpackPlugin({
          filename: config.build.index,
          template: 'index.html',
          inject: true,
          cdn:[
            'https://unpkg.com/[email protected]/dist/vue.min.js',
            'https://unpkg.com/[email protected]/dist/vuex.min.js',
            'https://unpkg.com/[email protected]/lib/index.js'
          ],
          minify: {
            removeComments: true,
            collapseWhitespace: true,
            removeAttributeQuotes: true
          },
          chunksSortMode: 'dependency'
        })
  • 将index.html中添加如下内容:

    <% if(htmlWebpackPlugin.options.cdn){ %> <% for(var i in htmlWebpackPlugin.options.cdn){ %>
        <script src="<%= htmlWebpackPlugin.options.cdn[i] %>"></script>
        <% } %> <% } %>

    使用ejs语法将定义在HtmlWebpackPlugin中的cdn写入,这样就解决了只在生产环境使用CDN,在开发环境下使用本地的。但是,作为一个完美主义者,修改HtmlWebpackPlugin插件的配置以及在index.html中写入一串类似js的代码总觉得不够优雅,难道不能直接在HtmlWebpackPlugin注入的阶段插入CDN吗,于是一顿操作,最终发现HtmlWebpackPlugin提供了事件接口,我们可以在它的基础上开发自己的插件,bingo,那么就来开发一个吧。

    HtmlWebpackPlugin插件的编写

    HtmlWebpackPlugin插件在3.x和4.x版本上的写法是不一样的,我们的开发环境使用的webpack3.x的,因此下面我基于3.x进行开发。

    HtmlWebpackPlugin提供了5个异步事件和1个同步事件,这些在官方GitHub和其他网友的博客中都有写到,我就不再叙述了,我主要使用了html-webpack-plugin-before-html-processing事件,在HTML处理之前将js注入进去,官网给了开发插件的例子:

    function MyPlugin(options) {
      // Configure your plugin with options...
    }
    
    MyPlugin.prototype.apply = function(compiler) {
      // ...
      compiler.plugin('compilation', function(compilation) {
        console.log('The compiler is starting a new compilation...');
    
        compilation.plugin('html-webpack-plugin-before-html-processing', function(htmlPluginData, callback) {
          htmlPluginData.html += 'The magic footer';
          callback(null, htmlPluginData);
        });
      });
    
    };
    
    module.exports = MyPlugin;

    htmlPluginData我们打印出来一看,属性一目了然:

    {
        html:''//HTML模板内容,
        assets:{
            chunks:[],//入口文件的内容
            js:[],//要插入的js文件的路径  ---就是我们要找的
            css:[],//要插入的css文件的路径
            manifest:''
        },
        ....
    }

    assets.js就是我们要找的,编写插件其实只要一句话

    htmlPluginData.assets.js = this.options.js.concat(htmlPluginData.assets.js)

    so easy,下面是插件的完整代码:

    class InjectCDN {
      constructor (options) {
        const defaultOpt = {
          js: []
        }
        this.options = Object.assign(defaultOpt, options)
      }
    
      // 处理方法
      apply (compiler) {
        // webpack3.x
        compiler.plugin('compilation', compilation => {
          compilation.plugin(
            'html-webpack-plugin-before-html-processing',
            (htmlPluginData, callback) => {
              // 将js添加进去
              htmlPluginData.assets.js = this.options.js.concat(htmlPluginData.assets.js)
              if (callback) {
                return callback(null, htmlPluginData)
              } else {
                return Promise.resolve(htmlPluginData)
              }
            }
          )
        })
      }
    }
    module.exports = InjectCDN

    在webpack.prod.conf.js中这样使用:

    new HtmlWebpackPlugin({}),
    new InjectCDN({
          js: [
            'https://unpkg.com/[email protected]/dist/vue.min.js',
            'https://unpkg.com/[email protected]/dist/vuex.min.js',
            'https://unpkg.com/[email protected]/lib/index.js'
          ]
    })

    new InjectCDN需要放到new HtmlWebpackPlugin后面。

    这样,就完成了在发布的时候使用cdn,在开发的时候使用本地文件。同时,这个插件也可以实现不同环境下不同CDN的切换,方便调试和发布。

自己动手开发一个 markdown 转微信文章

最近闲来无事,又开始着手打理自己的公众号,想着还是需要有个地方记录一下自己的学习情况以及跟大家分享,长久以来,我都是习惯于使用 markdown 来写文章,但是当我打开微信公众号新建一篇图文素材,what?这简陋的富文本编辑器,窗口还这么小,一边写文章还要一边进行排版,都 9012 年了,微信的交互还是这样?

no

实在难以吐槽。打开浏览器一搜,什么 135 编辑器啥的,这花花绿绿的是啥,我总算知道了有些公众号里那些非主流的排版是从哪里来的了。

what

作为程序员,我经常使用 markdown 来编写博客,markdown 语法简洁容易掌握,同时又可以专注于写作本身,不用写两句然后又转去排版,思路比较连贯,那么,不可以直接把 markdown 预览后的格式直接粘贴到微信里面吗?

what

答案是可以的,网上有很多这样的尝试,做的比较好的有两个:Lyric 的wechat-format和胡子哥的WxMarkdown两个都写的比较好了,能实现大部分的需求,但是他们都没有提供自定义样式,我只能选择和他们提供的样式一样。这就很束缚了,我喜欢放飞自我的感觉,那么,只能自己动手来实现了。

我基于 Lyric 的代码进行修改,思路如下:

  • 解析 markdown,我使用marked进行解析,marked 会将 markdown 解析为 HTML 元素,并且在期 render 中可以对各个元素进行处理,比如应用自己设定的样式之类,十分方便。

  • 代码高亮,作为程序员,代码高亮是必不可少的。我使用highlight.js处理 marked 解析之后的 code 标签进行代码高亮,highlight 有上百种代码样式,但是并不是所有的都很美观,因此我删除了其中的一些样式。在切换高亮风格的时候只需要切换 css 文件即可。我们只需要在初始化的时候创建一个 link 标签,在切换样式的时候动态修改 link 标签的 href 属性就可以了。

  • 样式自定义。自定义样式只需要在默认的样式文件上修改保存即可。本来自定义样式以 css 文件方式存储最为简单,但是在修改 css 的时候获取此刻编辑的标签十分困难,不够直观。因此,我选择以 JSON 存储样式,使用jsoneditor作为样式编辑器,我们就可以在编辑某个样式的时候高亮当前编辑的元素,使得编辑更加直观。

由于没有后台支撑,自定义样式目前只支持创建一个(太懒,不想写了),而且保存在浏览器的 localStorage,因此在您清除浏览器缓存之后可能会消失,建议先保存到本地以防不测。

之后,电脑上的排版总是和手机会有些不一样,主要是屏幕尺寸的问题,于是我参考墨刀做了个手机预览的功能,可以直观的看到你的文章在手机上是否合适,这个还是十分实用的,类似于这样的效果:

what

大道至简,这才是我想要的编辑器,专注于文章本身而不是在撰写和排版间来回切换,有同样烦恼的朋友可以尝试下,在线地址是:

http://zhouzhili.github.io/wechatformat/index.html#/

想要研究 markdown 如何解析成微信支持格式的也可以前往查看,网站上有源码地址,大家也可以动手做一个属于自己的格式转换器。

Vue组件扩展及权限管理的实现技巧

Vue 组件扩展

最近使用 ant-design-vue 作为 UI 库进行开发,不得不说 ant-design 的外观的确比 ElementUI 要好看,ElementUI 呈现的一种灰蒙蒙的感觉,而 ant-design 就让人感觉很亮,细节动画也很多。

另外,ant-design 的开发方式偏 React,很多示例也是使用 JSX 来编写的,在开发思路上得做一个切换。

在新的项目中,有很多 table 列表以及增删改查工作,列表样式基本都统一,而且下方有分页,列表第一页为序号,如下图所示:

no

本着少写一行就绝不多写一行,因此,我们可以把 table 封装起来,将一些属性设置为默认样式,但是有时候可能又会有所不同,因此,我们既要保持 table 的灵活性,同时又配置一些默认属性,因此,我们可以继承 table 原有的属性,同时进行一些扩展,并设置一些默认属性。这样使用 JSX 十分方便,我们可以如下进行开发一个 STable 进行扩展:

import T from 'ant-design-vue/es/table/Table'

export default {
  data() {
    return {
      selectedRowKeys: [],
      selectedRows: [],
      localPagination: Object.assign(
        {
          showTotal: total => `总共有${total}条数据`,
          showSizeChanger: true,
          pageSize: 10,
          current: 1
        },
        this.pagination
      )
    }
  },
  props: Object.assign({}, T.props, {
    // 是否分页
    showPagination: {
      type: Boolean,
      default: true
    },
    // 是否展示可选框
    showSelect: {
      type: Boolean,
      default: true
    },
    // 显示序号
    showSerialNo: {
      type: Boolean,
      default: true
    }
  }),
  methods: {
    handleTableChange(pagination, filters, sorter) {
      this.localPagination = Object.assign({}, this.pagination, {
        current: pagination.current,
        pageSize: pagination.pageSize
      })
      this.$emit('change', pagination, filters, sorter)
    },
    handleRowSelect(selectedRowKeys, selectedRows) {
      this.selectedRowKeys = selectedRowKeys
      this.$emit('rowSelection', selectedRowKeys, selectedRows)
    }
  },

  render() {
    const props = {}
    Object.keys(T.props).forEach(key => {
      // 分页
      if (key === 'showPagination') {
        if (!this.showPagination) {
          this.localPagination = false
        }
      } else if (key === 'pagination') {
        props[key] = Object.assign({}, this.localPagination, this[key])
      } else if (key === 'rowSelection') {
        // 是否显示第一行的选择框
        if (this.showSelect) {
          props.rowSelection = {
            selectedRows: this.selectedRows,
            selectedRowKeys: this.selectedRowKeys,
            onChange: (selectedRowKeys, selectedRows) => {
              this.handleRowSelect(selectedRowKeys, selectedRows)
            }
          }
        }
      } else if (key === 'rowKey') {
        props[key] = record => record.id
      } else if (key === 'columns') {
        const columns = [].concat(this[key])
        // 添加序号
        if (this.showSerialNo) {
          const { pageSize, current } = this.localPagination
          columns.splice(0, 0, {
            title: '编号',
            dataIndex: 'serialNo',
            key: 'serialNo',
            align: 'center',
            customRender: (text, record, index) => (current - 1) * pageSize + index + 1
          })
        }
        columns.forEach(col => {
          col.align = col.align || 'center'
        })
        props[key] = columns
      } else {
        props[key] = this[key]
      }
    })

    const table = (
      <a-table {...{ props, scopedSlots: { ...this.$scopedSlots } }} onChange={this.handleTableChange}>
        {Object.keys(this.$slots).map(name => (
          <template slot={name}>{this.$slots[name]}</template>
        ))}
      </a-table>
    )

    return <div>{table}</div>
  }
}

在我们的组件中,我们引入ant-design-vue/es/table/Table,并在组件的 Props 中继承 Table 的所有属性,Object.assign({}, T.props,{//扩展属性}),在渲染的时候,我们可以循环 Table 的 Props 属性,如果没有设置属性,我们自己给它设置一些默认属性,最终再把所有的属性绑定到 table 上,需要注意的是,在循环 Table 的 Props 属性时候,对于引用类型不可以直接进行修改,否则会陷入循环渲染。

这样,我们既保留了ant-design-vue/es/table/Table所有可配置的属性,同时也添加了针对项目的默认属性,使用起来十分方便也易于扩展。

增删改查权限的处理

vue 权限管理很多人都说过,不过常见的都是路由权限,登录之后根据用户的权限动态生成路由。这是页面权限,现在系统的权限更变态,不止页面权限,还有页面里面的权限,也就是某些用户只能看不能编辑,某些用户只能新增不能删除等等,其实总结起来就是页面增删改查权限也得配置。

好嘛,需求来了就得开干,首先,我们得知道用户有哪些权限,其次,我们要知道页面上的操作需要哪些权限,如果用户有这个权限,我们就显示这个模块,否则,我们不显示这个。例如如果用户没有修改权限,我们就可以不显示修改按钮。这种功能我们使用 Vue 指令就很好完成,代码如下:

// config: ['GET./api/v1/cachet', 'PATCH./api/v1/users']
import config from './config'
// 判断按钮以及页面各个部分是否有权限,没有的话就会移除当前el
export default {
  bind(el, binding) {
    setTimeout(() => {
      if (!config.includes(binding.value)) {
        el.parentNode.removeChild(el)
      }
    }, 0)
  }
}

然后我们在 main.js 中注册该指令:

import permissionDirective from './directives/permission'

Vue.directive('permission', permissionDirective)

然后在页面中可以这样使用:

<a-button v-permission="'DELETE./api/v1/users'">新建</a-button>

使用 CSS 绘制三角形

使用 CSS 绘制三角形

在项目开发上经常有一些小细节,特别上样式上,虽然使用 UI 切一个图片我们就能实现,然而如果界面颜色风格变了,我们又得去找 UI 重新切图,而且图片的自适应其实也不是很好,所有,能用 CSS 原生实现的,我们尽量使用原生来实现,这样灵活性更好。

例如,界面上经常出现的一些对话框、切角、三角形之类的,看似复杂,其实也很简单,这些都能使用 CSS 简单的来实现。

CSS 绘制三角形其实用到的就是很常见的border 属性,在 CSS 中,我们可以给一个元素的四个方位的边框设置不同的颜色和宽度,实现三角形的诀窍就在这里。

本文示例在线地址:CSS三角形在线地址

等腰三角形

首先我们定义一个三角形基本样式

.triangle {
  width: 0;
  height: 0;
  border: 50px solid transparent;
}

普通等腰三角形绘制只需将元素宽高设为 0、border 颜色设置为透明,将直角指向方向的反方向的 border 颜色设置为三角形的颜色即可。

上三角

上三角

.top {
  border-bottom-color: red;
}
<div class="triangle top"></div>
下三角

下三角

.bottom {
  border-top-color: red;
}
<div class="triangle bottom"></div>
左三角

左三角

.left {
  border-right-color: red;
}
<div class="triangle left"></div>
右三角

右三角

.right {
  border-left-color: red;
}
<div class="triangle right"></div>
消息框

学会了等腰三角形的绘制方法,我们就可以使用 CSS 做出常见的消息框样式,例如:如下的消息框

右三角

这种常见的消息框我们分解下其实是上边的矩形框和下边的小三角构成,矩形框大家都会做,但是下面的只有 2 条直角边的三角形可能会把大家难住。大家可能会想怎么绘制出只有 2 条边的三角形呢?其实换一种想法就可以了,既然我们我可以绘制出三角形,那么我绘制一个灰色和一个白色的三角形,然后用白色的三角形盖住灰色三角的一条边,这样不就是只有 2 条边的三角形么。那么这样想其实就很简单了,我们只需把未知的问题用已知的方法来解决,一切就都迎刃而解了。

在这里,我们使用伪类来实现,避免 HTML 要素过多

<div class="msg-box">
  <div class="msg-box-content"></div>
</div>
.msg-box {
  width: 200px;
  position: relative;
}
.msg-box-content {
  background: #fff;
  height: 40px;
  border: 1px solid #ddd;
  box-shadow: 0 0 4px #ddd;
  border-radius: 8px;
  margin-bottom: 8px;
}
.msg-box-content::before {
  content: ' ';
  position: absolute;
  z-index: 1;
  left: calc(50% - 8px);
  top: 100%;
  width: 0;
  height: 0;
  border: 8px solid transparent;
  border-top-color: #ddd;
}
.msg-box-content::after {
  content: ' ';
  position: absolute;
  z-index: 1;
  left: calc(50% - 8px);
  top: 98%;
  width: 0;
  height: 0;
  border: 8px solid transparent;
  border-top-color: #fff;
}

如果使用sass 或者less的话,我们的 CSS 量会更少。上面样式三角形没有阴影,如果消息框和三角形都需要阴影的话,上面的样式就不合适了,解决办法是使用 2 个矩形,底部矩形带阴影,上面的矩形不带,然后 2 个旋转 45 度来处理。

直角三角形

直角三角形与等腰三角形有所不同,绘制直角三角形需要将宽、高、边框都设置为 0,将直角所在的底部或顶部边框设置宽度与颜色,并将直角所在的左侧或右侧的反方向设置宽度即可

同理,首先设置一个直角三角形的基本样式

.orth-triangle {
  width: 0;
  height: 0;
  border: 0px solid transparent;
}
左下直角

左下三角

.orth-lb {
  border-bottom: 50px solid red;
  border-right-width: 50px;
}
<div class="orth-triangle orth-lb"></div>
右下直角

右下三角

.orth-rb {
  border-bottom: 50px solid red;
  border-left-width: 50px;
}
<div class="orth-triangle orth-rb"></div>
左上直角

右下三角

.orth-tl {
  border-top: 50px solid red;
  border-right-width: 50px;
}
<div class="orth-triangle orth-tl"></div>
右上直角

右下三角

.orth-tr {
  border-top: 50px solid red;
  border-left-width: 50px;
}
<div class="orth-triangle orth-tr"></div>
切角框

学会了直接三角形,我们可以使用它来绘制如下的切角框:

切角边框

处理逻辑一样,左上和右下的切角使用 2 个三角形覆盖来处理,样式如下:

.clip-content {
  position: relative;
  height: 80px;
  margin-top: 16px;
  border: 1px solid #ddd;
}
.clip-content .tl-triangle::before {
  content: ' ';
  position: absolute;
  z-index: 1;
  left: -1px;
  top: -1px;
  border-right: 16px solid transparent;
  border-top: 16px solid #ddd;
}
.clip-content .tl-triangle::after {
  content: ' ';
  position: absolute;
  z-index: 2;
  left: -1px;
  top: -2px;
  border-right: 16px solid transparent;
  border-top: 16px solid #fff;
}
.clip-content .br-triangle::before {
  content: ' ';
  position: absolute;
  z-index: 1;
  right: -1px;
  bottom: -1px;
  border-left: 16px solid transparent;
  border-bottom: 16px solid #ddd;
}
.clip-content .br-triangle::after {
  content: ' ';
  position: absolute;
  z-index: 2;
  right: -1px;
  bottom: -2px;
  border-left: 16px solid transparent;
  border-bottom: 16px solid #fff;
}
<div class="clip-content">
  <div class="tl-triangle"></div>
  <div class="br-triangle"></div>
</div>

当然这种切角还有其他的处理方法,例如使用css 的 background:linear-gradient也能实现,这里不做描述

使用CSS还可以绘制其他的图像,例如圆、椭圆、平行四边形、梯形、五角星等等,可以访问 css tricks网站 查看更多。

webGL二维有向距离场(SDF)及布尔运算

有向距离场

在着色器中绘图,类似于在一个方格纸上涂色,每个方格就是一个像素点。想象一下如果我们要在方格纸上绘制一个圆该怎么做呢,我们只需要把圆形内部区域涂成一个颜色,圆形外部区域的涂成另外一种颜色(或者不涂色),这样我们便有了一个圆。在数学上我们怎么定义圆呢?及圆周上的点到圆心的距离等于半径,圆内部的点到圆心的距离小于半径,圆外部的点到圆心的距离大于半径。这样一圈一圈,我们可以在图上得到每一个点到圆心的距离,圆内部的点到圆心的距离减去半径为负值,圆外部点到圆心的距离减去半径为正值,因此我们称之为:有向距离场。

图片

移动

在计算距离场的时候一般将坐标中点设置为原点,方便我们计算。但是通常,图形不是绘制在原点的位置,这样我们便需要原点位置设置在图形的中点,移动到原点是一个几乎所有图形都会用到的操作。如果图形的中点在(0.5,0.5),那么将要将(0.5,0.5)设置为原点,我们只需要减去(0.5,0.5),那么在绘制过程中便是以(0.5,0.5)为原点的。

vec2 translate(in vec2 p,in vec2 c) {
	return p-c;
}

圆形距离场

圆形的距离计算最为简单,我们只需要计算坐标到圆心的距离即可,假设圆心在(0.5,0.5),由于着色器坐标归一化之后范围为[0,1],那么绘图区域中点到圆心的距离范围为[0,0.5],如下图所示:

图

计算代码可以写为这样:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 uResolution;

void main(){
  vec2 st=gl_FragCoord.xy/uResolution;
  vec2 center=vec2(.5);
  float dis=distance(center,st.xy);
  gl_FragColor.rgb=dis*vec3(1.);
  gl_FragColor.a=1.;
}

矩形距离场

矩形距离场稍微要复杂一些,假设矩形中点在正中心,长为w,宽为h,那么对于矩形内部的点来说,它们的x轴到中心的距离为[0,w/2],它们的y轴到中心的距离为[0,h/2];而矩形外面的的坐标,它们的x轴到中心的范围为[w/2,0],它们Y轴到中心点范围为[h/2,1],如下图所示:那么减去长宽的一半之后,内部点x的范围为[-w/2,0],外部点x轴范围为[-w/2+1,1],如下图所示:

矩形

那么绘制矩形就有一个简单的方式了,矩形内部,x值和y值都必须小于0;矩形外部,x值和y值必然有一个是大于0 的,那么只要x,y的最大值小于0 ,就是矩形内部,否则,就是矩形外部,代码可以这样写

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 uResolution;

float plot_Rect(vec2 st,vec2 c,vec2 halfwh){
	// 移动到中心
	st-=c;
	// 以绝对值减去长宽
	st=abs(st)-halfwh;
	// 在矩形内部的点 x 范围为:[-w/2,0],同时 y的范围为[-h/2,0],所以,只要最大值小于0就可以表示点在矩形的范围
	return max(st.x,st.y);
}

void main(){
	vec2 st=gl_FragCoord.xy/uResolution;
	float dis = plot_Rect(st,vec2(0.5),vec2(0.3,0.1));
	gl_FragColor.rgb=step(dis,0.0)*vec3(1.0);
	gl_FragColor.a = 1.0;
}

对于更多更复杂的图形,绘制方法也是一样的,判断一个点处于图形内部还是图形外部就可以绘制他们,越复杂的图形,其距离场函数可能就会越复杂,需要有很深的数学功底,这些有一些距离场函数可以供参考:

shadertroy创始人的博客 里面有许多2D乃至3D的距离场函数,可以通过这个图粗略的了解一下:

2dsdf

以及 the book of shaders大佬的vscode-glsl-canvas 这个插件里面包含了一些可用的2d sdf函数片段,可以参考.

还有国外一位大佬写的着色器教程,很全面,很值得一看。

距离场布尔运算

通过距离场函数得到图形之后,可以通过布尔计算得到不同的组合,如下图所示,分别为圆于矩形进行交集(∩)、并集(∪)、差集(-)得到

布尔计算

代码如下图所示:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 uResolution;

#include <lib/util.glsl>
#include <lib/shape.glsl>
#include <lib/color.glsl>

// 合并,取并集
float merge(float dis1,float dis2) {
	return min(dis1,dis2);
}

// 相交,取交集
float intersect(float dis1,float dis2){
	return max(dis1,dis2);
}

// 相减
float subtract(float base, float subtraction){
	return max(base,-subtraction);
}


void main(){
	vec2 st=gl_FragCoord.xy/uResolution;
	// 交集
	float c1 = sCircle(st,vec2(0.1,0.5),0.2);
	float r1 = sPoly(st,vec2(0.2,0.6),0.2,4);
	float dis1 = intersect(c1,r1);
	
	//
	float c2 = sCircle(st,vec2(0.5,0.5),0.1);
	float r2 = sPoly(st,vec2(0.5,0.6),0.1,4);
	float dis2 = merge(c2,r2);
	
	//
	float c3 = sCircle(st,vec2(0.8,0.5),0.1);
	float r3 = sPoly(st,vec2(0.8,0.6),0.1,4);
	float dis3 = subtract(c3,r3);
	
	float dis = merge(dis1,dis2);
	dis = merge(dis,dis3);

	gl_FragColor.rgb=fill(dis,AZUR);
	gl_FragColor.a=1.;
}

我们可以通过这些做出更有意思的图形,图形学的世界其实就是数学的世界,正如大师所说的,使用着色器绘图就如同将一艘船放进瓶子里,过程是如此的复杂,但是结果却很美丽。共勉!

共勉

JS 知识汇总-持续更新

JS 知识汇总-持续更新

1. JS 中的 this 是由函数调用者调用的时候决定的。

this 是在函数被调用的时候决定的,如果没有显示的调用者,this 指向的是全局对象,在浏览器中指向的是 window,在 node 指向的是 global。但是在严格模式下指向的是 undefined。

2.如果你使用了 ES6 的 class 语法,所有在 class 中声明的方法都会自动地使用严格模式

3.箭头函数中的 this 永远绑定了定义箭头函数所在的那个对象

4.React 为什么需要手动绑定 this?

以 1-3 条知识为基础,react 的 render 函数中事件绑定原写法为

<button onClick={this.onDismiss}>删除</button>

相当于:

class App {
  onDismiss() {
    console.log(this)
  }

  render() {
    const onDismiss = this.onDismiss
    onDismiss()
  }
}

const app = new App()
app.render() // 输出结果为:undefined

实例 app 的 render 方法里面,将 App 对象的方法 onDismiss 方法赋值给 onDismiss,然后调用 onDismiss(),onDismiss 在调用时并没有显示的调用者,因此调用者会为全局对象,根据上面第 2 条,class 中使用的为严格模式,所有 this 指向的为 undefined.

以上,react 中事件调用需要手动绑定 this,在构造函数中手动 bind 或者使用箭头函数都可以

5.requestAnimationFrame回调函数中的时间?

requestAnimationFrame回调函数的时间是DOMHighResTimeStamp,它是自当前文档生命周期开始以来经过的毫秒数,而不是调用requestAnimationFrame开始的时间戳,如果中途调用cancelAnimationFrame取消了循环,在下次再调用requestAnimationFrame的时候时间并不会重置,如果需要重置,需要自己维护时钟。

6 数组循环中使用await async

在循环中使用await有2中方法,一种是使用 for in语法,如下:

async function start() {
  const array = [1, 2, 3, 4, 5]

  for (const a in array) {
    const result = await Promise.resolve(array[a])
    console.log(result)
  }
  console.log('end')
}
start()

另外一种是使用Promise.all结合map方法,如下:

async function start() {
  const array = [1, 2, 3, 4, 5]

  await Promise.all(array.map(async a => {
    const result = await Promise.resolve(a)
    console.log(result)
  }))
  console.log('end')
}
start()

2种方式都有着相同的输出:

1
2
3
4
5
end

构建自己的 GLSL 绘图器 - 2d 版

构建自己的 GLSL 绘图器 - 2d 版

学习 GLSL 绘图有一段时间了,感觉现在才刚刚摸到门路,到现在能够绘制一些简单的图形了,比如圆和矩形。其他复杂的形状比较难以绘制,GLSL 绘图感觉难点在于数学上,如何将图形用数学公式表达出来,用向量的加减乘除表达出来,这是最难的了,入门还得好久。

先撇开这些困难的 GLSL 绘图函数,先把基础搭建起来。要使用 GLSL 绘图,我们先创建一个简版的绘图类,它需要能初始化我们的绘图区域、加载 shader 文件、在 fragment shader 中提供一些全局变量这些基本的功能,那么,我们来一步一步的实现。

1. 初始化绘图区域

使用 GLSL 绘图,我们只要是在片元着色器中编写绘图方法,因此,我们只需要绘制一个矩形区域作为绘图面板即可。一般来说顶点着色器是不需要修改的,我们提供一个默认的顶点着色器:

#ifdef GL_ES
precision mediump float;
#endif

attribute vec4 aPosition;

void main(){
  gl_Position=aPosition;
  gl_PointSize=1.;
}`,
fragmentShader:`#ifdef GL_ES
precision mediump float;
#endif
void main(){
  gl_FragColor=vec4(0.,0.,0.,1.);
}

以及一个默认的片元着色器:

#ifdef GL_ES
precision mediump float;
#endif
void main(){
  gl_FragColor=vec4(0.,0.,0.,1.);
}

在初始化绘图区域的时候,我们绘制 2 个三角形组成一个矩形绘图区即可,至于如何绘制矩形等等基础知识这里就不一一复述了。想了解的可以前往webgl-fundamentals 学习。

2. 加载 shader 文件

在 webGL 中,shader 文件是以字符串的形式存在的,并且在传递给 webgl 对象进行编译处理的。如果我们以字符串形式存储我们的片元着色器的话,不仅在编写上繁琐,而且编辑器无法提供高亮与格式化支持,在组织上也是十分不友好的。因此,我们以.glsl 文件的形式存储片元着色器。

同时,在 webGL 中片元着色器是没有模块这一说法的,想要复用我们编写的着色器只能 Ctr+c,Ctr+v 了,但是,作为程序员,一段代码最好不要 ctr+v 超过 3 次。既然着色器代码是以字符串形式存在的,我们只要在传递给 webgl 之前处理成正确的格式就能实现代码复用了,而不用手动复制粘贴。

2.1 实现加载器

glsl 文件加载器十分简单,只需要使用 xhr 请求 glsl 文件获取文本即可:

async loadGLSL(name) {
    if (name) {
      const errorMsg = 'load glsl file failed: file name is ' + name
      try {
        const res = await fetch(this.baseFragPath + name)
        if (res.ok) {
          return await res.text()
        } else {
          throw errorMsg
        }
      } catch (error) {
        console.error(errorMsg)
        throw error
      }
    } else {
      console.error('glsl file name is required')
    }
  }

2.2 实现 glsl 模块化

由于 glsl 并没有制定模块化规则,我们可以定制自己的规则,比如,我们可以约定使用下面的语法作为我们加载 glsl 模块的方法:

#include <name.glsl>

在 glsl 中以#开头为注释语法,不会影响到其他的语句,我们只需要匹配出 name.glsl ,并将这段语句替换成 name.glsl 文件的内容就可以实现模块化了。

我们在将 shader 传递给 webgl 之前对其进行格式化处理,匹配出所有的 #include <>语句并进行替换,这里需要使用正则进行处理:

格式化处理函数如下,使用递归进行处理:

async _formatterCode(glslCode) {
    try {
      let code = glslCode
      // 判断是否包含 #include <*.glsl>
      const reg = /#include <(.*?.glsl)>/g
      if (reg.test(code)) {
        // 替换 include代码
        const includes = this._getIncludeGLSL(code)
        await Promise.all(includes.map(async item => {
          const subCode = await this.loadGLSL(item.target)
          const formatSubCode = await this._formatterCode(subCode)
          code = code.replace(item.reg, formatSubCode)
        }))
      }
      return code
    } catch (err) {
      const errorMsg = `load ${fileName} glsl file failed,check Is the include format correct`
      console.error(errorMsg)
      throw new Error(errorMsg)
    }
  }

正则匹配方法如下所示:

_getIncludeGLSL(glsl) {
    try {
      const reg = /#include <(.*?.glsl)>/g
      const arr = [];
      let r = null
      while (r = reg.exec(glsl)) {
        arr.push({
          reg: r[0],
          target: r[1]
        })
      }
      return arr
    } catch (error) {
      const errorMSg = 'the include format is not correct'
      console.log(errorMSg, error)
      throw error
    }
  }

这样我们就可以放心的使用我们自定义的模块化语法,实现了着色器代码的复用

3. 提供一些有用的全局变量

在使用片元着色器绘图,我们常用到的变量是:绘图区域分辨率、时间、鼠标位置这三个变量。我们预定三个变量值为 uResolution、uTime、uMouse,判断在着色器中是否启用了这些值,如果有则将变量值传递,否则不需要进行赋值。
例如 uResolution:

const uResolution = this.gl.getUniformLocation(this.program, 'uResolution')
if (uResolution) {
  this.gl.uniform2f(uResolution, this.gl.canvas.width, this.gl.canvas.height)
}

对于 uTime,如果启用,我们需要使用 requestAnimationFrame 进行循环绘图,以将时间传递给着色器,为了节约性能,我们设置需要手动启用时间:

const uTimeLocation = this.gl.getUniformLocation(this.program, 'uTime')

const animateDraw = () => {
  const time = new Date().getTime() - this.clock
  this.gl.uniform1f(uTimeLocation, time / 1000)
  commonDraw()
  this._animateInterval = requestAnimationFrame(animateDraw)
}

const commonDraw = () => {
  this.gl.clearColor(0.0, 0.0, 0.0, 1.0)
  this.gl.clear(this.gl.COLOR_BUFFER_BIT)
  this.gl.drawElements(this.gl.TRIANGLES, indexes.length * 3, this.gl.UNSIGNED_BYTE, 0)
}

if (this._animateInterval) {
  cancelAnimationFrame(this._animateInterval)
  console.log('clear animation frame')
}

if (this.enableTime && uTimeLocation) {
  console.log('start animate draw')
  this.clock = new Date().getTime()
  animateDraw()
}

到目前为止,我们的 GLSL 绘图器基本可用了,如下使用:

const gRender = new GRender({
  canvas: document.getElementById('gl-canvas'),
  basePath: './fragments/'
})

async function runCode() {
  try {
    const fragVal = monacoIns.getValue()
    const enableTime = fragVal.indexOf('uniform float uTime;')
    gRender.enableTime = enableTime !== -1
    await gRender.renderByShader(fragVal)
  } catch (e) {
    console.log(e)
  }
}

gRender
  .loadGLSL('wall.glsl')
  .then(code => {
    runCode()
  })
  .catch(err => {
    console.log('加载wall.glsl失败', err)
  })

我们可以专心于编写着色器代码,执行不同的着色器只需要修改gRender.loadGLSL()方法中的着色器名称。

[注]:GRender详细代码可前往我的webGL-webGIS-Learning 代码仓库中查看

webpack按需加载配置和babel编译

平时我们经常使用的一些开源库可以通过按需加载来减小引入资源的大小,这种一般是使用到了 webpack 的tree-shaking,时间久了大家也就自然而然的以为这是 webpack 本身就有的功能,那么,当我们在开发一个开源库的时候,怎么样也能让使用者能够按需加载呢?直接提供给对方打包后的文件或者 ES6 原始文件就可以了吗?答案是不行的,我们需要搞清楚 webpack 怎么样才能实现tree-shaking。这里我不在解释什么是tree-shaking,我只来说明怎么样才能让我们开发出来的库文件可以使用tree-shaking

1.使用 tree-shaking 的前提条件

webpack 文档写的很清楚,在 tree-shaking 章节下面的结论那里列了 4 个条件。

  1. 使用 ES2015 的模块语法。
  2. 确保没有编译器将 ES2015 模块语法转为 CommonJS 的模块语法。
  3. 在项目的 package.json 文件中,添加 "sideEffects" 属性。
  4. 使用 mode 为 "production" 的配置项以启用更多优化项,包括压缩代码与 tree shaking.

2.项目中如何配置

文档写的很清楚,前 3 个条件是针对我们库文件的条件,最后一个是在使用的时候需要注意的点,接下来我将分条目进行说明。

使用 ES2015 的模块语法

webpack 的tree-shaking依赖于 ES2015 的模块语法,即 ES6 的importexport语法,所以我们的库文件在提供出去的时候需要保持 ES6 的模块语法,不能转译为 CommonJS 的语法,这点需要注意,我们在发布我们的库文件到 NPM 上的时候,一般会提供一个 ES5 文件供浏览器中直接引入,另外再提供一份 ES6 模块文件供开发中使用,这里在babel中进行配置即可。

目前 babel 版本为 7,使用的配置文件为babel.config.js,我们可以通过环境变量NODE_ENV来区分不同的环境,通过设置presetmodules为 false 来保留 ES6 模块语法,可参考babel-preset-env,具体配置如下:

presets: [
  [
    '@babel/preset-env',
    {
      modules: false, // babel编译的时候保留 es module方式
      targets: {
        browsers: ['> 1%', 'last 2 versions', 'not ie <= 8']
      }
    }
  ]
]

第二条和第一条基本相同,我们开发的时候保留 ES6 模块即可。

在项目的 package.json 文件中,添加 "sideEffects" 属性。

sideEffects 即副作用影响,即在 webpack 打包的时候告诉 webpack 哪些是没有副作用的可以直接去掉,具体的说明可以看这篇文章,通常情况下我们可以直接设置sideEffects:false 来表明文件都是没有副作用的可以直接tree-shaking, 但是这里需要注意的是,在开发的过程中,会把一些 import 的但是没有使用的文件删除掉,例如一些单独导入的 less,css,scss 文件没有打包进去,因此在配置的时候需要额外的注意,我们可以配置哪些文件有副作用不用tree-shaking,配置如下:

"sideEffects": [
    "*.vue",
    "*.css",
    "*.scss",
    "*.sass",
    "*.less"
  ]

其他的一些说明。

在我们提供的npm库文件的时候,一般会在package.json中提供一个main字段来表明包的入口,其实还有一个module字段也可以设置包的入口,这些的设置也有助于tree-shaking的使用,具体可以看这篇博文

4. 使用tree-shaking

使用tree-shaking就相对比较简单了,如果我们的NPM包支持tree-shaking了,那么我们在编译的时候添加mode: 'production',并启用压缩即可,webpack会自动对我们引入的NPM包进行tree-shaking处理。

qcharts 介绍与推荐

QCharts

简介

QCharts 是一个基于 spritejs 封装的图表库,它以数据驱动,将图表以最小组件进行拆分,具有高度全面灵活的属性配置方法,可对图表绘制过程中所有的点、线、面的大小、位置、填充颜色、描边颜色、描边线型、透明度等属性进行配置,配置方法简单易懂,语义清晰,无论如何复杂的图表,qcharts都能轻松胜任,再也不用为无法实现产品经理的想法而加班加点了。

开始使用

说了这么多,现在就来体验一下,首先,我们实现一个最简单的折线图:

  1. 引入qcharts,由于qcharts依赖于spritejs,因此需要先引入spritejs,使用npm包的话则不需要单独引入spritejs。我们先通过CDN方式引入qcharts。
<script src="https://unpkg.com/spritejs/dist/spritejs.min.js"></script>
<script src="https://unpkg.com/@qcharts/core/lib/index.js"></script>
  1. 创建div图表容器,qcharts初始化container属性支持id选择器与class选择器
<div id='app'></div>
  1. 编写绘图代码
// 引入组件
const { Chart, Line, Legend,Axis } = qcharts
// 数据源
const data = [
  { date: '05-01', catgory: '图例一', sales: 15.2 },
  { date: '05-02', catgory: '图例一', sales: 39.2 },
  { date: '05-03', catgory: '图例一', sales: 31.2 },
  { date: '05-04', catgory: '图例一', sales: 65.2 },
  { date: '05-05', catgory: '图例一', sales: 55.2 },
  { date: '05-06', catgory: '图例一', sales: 75.2 },
  { date: '05-07', catgory: '图例一', sales: 95.2 },
  { date: '05-08', catgory: '图例一', sales: 65.2 }
]
// 创建chart对象
const chart = new Chart({
  container: '#app', // 指定容器id
  size:[600,400],   //  指定容器高宽
  forceFit:false   //  图表自适应
})

// 指定图表数据源
chart.source(data, {
  row: 'catgory',  // 以catgory字段分组
  value: 'sales', // 以sales字段取值
  text: 'date'   //  date为文本字段
})

// 创建折线对象
const line = new Line()
// 创建坐标轴对象
const axisBottom = new Axis()
// 创建坐标轴对象,设置为方向为左侧
const axisLeft = new Axis({ orient: 'left' })
// 创建图例对象并设置位置
const legend = new Legend({ align: ['center', 'bottom'] })
// 装载组件
chart.add([line, axisBottom, axisLeft, legend])
// 渲染图表
chart.render()

折线图就绘制成功:

看完上面的,你可能会说,这没什么啊,所有的图表库都能实现这些,qcharts究竟有什么不同的呢?

的确,对于普通图表,所有的图表库都能够胜任,qcharts的优势在于配置复杂图表。

示例1

试想一下如果你正好约了妹子下班去看电影,下班前10分钟产品经理在丢给你一个这样的图表:

看完这个设计稿想必你已经不淡定了,内心仿佛有无数草泥马奔跑。别慌,使用qchart让你永不加班,

首先,我们对图表进行分解,这是一个基础面积图,横轴显示label,纵轴只显示轴线(so easy),面积区域描边为蓝色实线(so easy),填充为渐变色(坑爹的设计师要什么渐变),最后一条数据上要显示一个圆点以及文字(这不是坑爹吗),最右侧还要空一个间隔。

开始编码,

  1. 基础数据以及图表初始化:
const data = [
  { date: '05-01', catgory: '图例一', sales: 15.2 },
  { date: '05-02', catgory: '图例一', sales: 39.2 },
  { date: '05-03', catgory: '图例一', sales: 31.2 },
  { date: '05-04', catgory: '图例一', sales: 65.2 },
  { date: '05-05', catgory: '图例一', sales: 55.2 },
  { date: '05-06', catgory: '图例一', sales: 75.2 },
  { date: '05-07', catgory: '图例一', sales: 95.2 },
  { date: '05-08', catgory: '图例一' }
]

const { Chart, Area, Legend, Tooltip, Axis } = qcharts

const chart = new Chart({
  container: '#app'
})

chart.source(data, {
  row: 'catgory',
  value: 'sales',
  text: 'date'
})
  1. 设置面积图填充色:
const area = new Area({ smooth: true })
area.style('area', function(attr, data, i) {
  return {
    fillColor: {
      vector: [0, 0, 0, 500],
      colors: [
        {
          offset: 0,
          color: 'rgba(71,161,255,0.5)'
        },
        {
          offset: 1,
          color: 'rgba(71,161,255,0.1)'
        }
      ]
    }
  }
})
  1. 最右侧的label
area.style('label', function(attr, data, i) {
  let text = data[i].sales
  if (i === 6) {
    return { text: text, anchor: [0.5, 1], padding: [5, 10], color: '#fff', fontSize: 18 }
  }
  return false
})
area.style('point', function(attr, data, i, j) {
  if (j === 6) {
    return { size: 6, strokeColor: 'transparent' }
  }
  return false
})
  1. 坐标轴设置
const axisBottom = new Axis()
  .style('axis', { fillColor: '#1e2944' })
  .style('grid', { lineDash: [0, 0], strokeColor: '#1e2944' })
  .style('scale', false)

const axisLeft = new Axis({ orient: 'left' })
  .style('axis', { fillColor: '#1e2944' })
  .style('scale', false)
  .style('grid', false)
  .style('label', false)
  1. 渲染完成
chart.add([area, axisBottom, axisLeft])
chart.render()

几十行代码如行云流水般在6分钟内就能写完,剩下4分钟整理下不多的头发正好下班。

示例2

这时,产品经理又丢给你一张图(就是想让你加班:doge):

内心虽然不爽,但是就这个图就能让我加班吗?使用qcharts,十行代码解决(主要代码)

  1. 图表数据
const data = [
  {
    type: '污染源',
    count: 4454
  },

  {
    type: '消防场所',
    count: 1239
  },

  {
    type: '安全生产',
    count: 3758
  },

  {
    type: '治安场所',
    count: 4353
  }
]

const { Chart, ArcPie, Legend, Tooltip } = qcharts

const chart = new Chart({
  container: '#app'
})

chart.source(data, {
  row: 'type',
  value: 'count'
})
  1. 设置样式
const arcPie = new ArcPie({
  pos: ['-10%', '10%'],
  radius: 0.6,
  innerRadius: 0.1,
  lineWidth: 15,
  padAngle: 0.2,
  title: d => `${d[0].dataOrigin.type}`,
  subTitle: d => `${d[0].dataOrigin.count}`
})

arcPie.style('arc', { lineCap: 'round' })
arcPie.style('title', { fontSize: 24 })
  1. 渲染
chart.add([arcPie])
chart.render()
chart.render()

4分钟解决,使用qcharts你就是办公室里编码最快的老哥。

通过以上的示例足以体现出qcharts强大的配置功能,无论多么复杂的图表它都可以轻松完成。基于qcharts良好的组合性和扩展性,它天然支持对React和 Vue 这两个常用框架的深度整合,qcharts官方维护着qcharts的React和Vue版本,在React环境下,我们推荐使用cat-charts-react ,在Vue环境下,我们推荐使用 cat-charts-vue。这两个产品都是基于qcharts的封装,与qcharts有着一致的开发体验。当然,你可以自己动手封装成其他框架下的组件,在qcharts下,这些封装成本非常低。

在微信小程序中使用qcharts

并且,qchart从v0.1.11开始支持微信小程序,并在qcharts npm包中内置了q-chart小程序组件,在小程序中构建NPM包,并在app.json中配置q-chart组件:

  "usingComponents": {
    "q-chart": "@qcharts/core/chart"
  }

先在页面index.wxss文件中编写设置图表大小的css样式:

.line-chart{
  height: 400px;
  width: 80%;
  margin: 0 auto;
}

然后在要使用的页面index.wxml文件中引入组件:

<view>
  <q-chart bindChartCreated="onChartCreated" chartId="my-chart" chart-class="line-chart">   </q-chart>
</view>

chartId为图表canvas的canvas-id值,默认值为“q-chart”,在当前page中必须唯一。chart-class可设置chart组件中canvas父容器的class属性,chart的与其父容器的大小保持一致。

bindChartCreated事件处理中通过参数可以获得创建好的chart对象:

const qcharts = require('@qcharts/core');

Page({
  onChartCreated({ detail: { chart } }) {
    const data = [
      { date: '05-01', catgory: '图例一', sales: 15.2 },
      { date: '05-02', catgory: '图例一', sales: 39.2 },
      { date: '05-03', catgory: '图例一', sales: 31.2 },
      { date: '05-04', catgory: '图例一', sales: 65.2 },
      { date: '05-05', catgory: '图例一', sales: 55.2 },
      { date: '05-06', catgory: '图例一', sales: 75.2 },
      { date: '05-07', catgory: '图例一', sales: 95.2 },
      { date: '05-08', catgory: '图例一', sales: 65.2 },
    ]

    const { Line, Legend, Tooltip, Axis } = qcharts

    chart.source(data, {
      row: 'catgory',
      value: 'sales',
      text: 'date'
    })

    const line = new Line()
    line.style('point', { strokeColor: '#fff' })

    const tooltip = new Tooltip({
      formatter: function(data) {
        return `${data.date} ${data.sales}`
      }
    })

    const axisBottom = new Axis()

    const axisLeft = new Axis({ orient: 'left' })
      .style('axis', false).style('scale', false)

    const legend = new Legend({ align: ['center', 'bottom'] })
      .style('icon', { borderRadius: 10 }).style('text', { fontSize: 12 })

    chart.add([line, tooltip, axisBottom, axisLeft, legend])
    chart.render()
  },
});
总结

QCharts 是一个拥有强大配置功能的图表库,在处理复杂图表时,它强大的配置项能让我们对绘制的图表进行全方位的掌控,并且它有着官方提供的React和Vue组件库,同时支持在微信小程序中使用,各个版本api基本一致,熟悉一个便可以贯通所有。使用它可以更好的完成数据展示任务,可以更好的提高我们的工作效率。

使用 Mpvue 开发小程序总结

最近使用 mpvue 做了几个小程序的开发,在开发过程中遇到了一些问题也发现了一些新的东西,在这里总结一下:

1.小程序 setData 很消耗性能,与展示无关的数据可以不用放到 data 中

例如我在开发一个长列表展示内容,如果用户一直往下滑动,数据量会一直增加,而且小程序 data 的容量有限制,如果数据量过多,界面会有卡顿设置崩溃。根据官方 API,setData 单次设置数据大小不能超过 1024KB,过大会产生错误,因此,设计给小程序的 API 数据尽可能精简,避免传递无关数据,比如列表数据获取的是后台的分页数据,当前页码的标识字段其实与界面渲染是无关的,可以放到 data 外面。

2.小程序 scroll-view 在纵向滚动需要设置的高度是当前窗口的高度

scroll-view 相当于一个内嵌的框架 列表在框架内滚动,它的高度其实就是屏幕的高度 不是 里边列表项目的高度

可以通过小程序提供的获取系统信息的 API 获取系统信息,**wx.getSystemInfoSync()**获取系统信息然后再获取屏幕高度,将这个高度绑定到 scroll-view 上面,也可以使用 css 的 vh 尺寸来设置高度。

3.小程序分享页面回到主页

小程序分享功能在生命周期函数中定义 onShareAppMessage 函数即可,该函数返回一个 Object,具体参数可以查看小程序 API,分享的卡面默认图片为当前页面的截图,如果觉得不好看可以自定义展示图片,在 imageUrl 中设置图片地址即可。

点击小程序分享页面后没有回退功能,如果想让用户回到主页一般有 2 种处理办法,一种是再 path 地址中加上标识,通过地址判断如果是通过分享页面进来的就在当前页面显示一个回到主页的按钮,通过点击按钮回到主页,这种方法比较简单,但是没有返回功能,用户按了返回键就会直接退出。另外一种是将分享函数返回的 Object 中的 path 设置为首页,并带上标志参数,在首页的 onShow 方法中检查参数,在 mpvue 中,通过 this.$root.$mp.query获取当前页面 url 的参数,如果带有参数,就跳转到分享页面,这样用户点击返回就能直接回到主页面了,不好的地方是点击分享的卡片会在主页停留一下再跳转到页面,用户体验不是很好。

4.分享按钮去除默认样式及传递参数

小程序分享功能只需要在页面中定义好 onShareAppMessage函数即可,点击右上角就能看到分享功能,如果需要通过按钮分享,只需要设置 button 的 open-type='share'即刻完成分享功能,但默认的 button 样式不美观,我需要使用自定义的图片来替代分享按钮。经过尝试,在其他标签上设置 open-type='share'并不起什么作用,看来只能使用 button 了,在去掉 button 的 border 上,怎么设置都没用,后面 Google 一番发现 button 的 border 使用的是伪类,坑爹的,使用下面一行 css 即可去掉 border:

button::after {
  border: none;
}

我的小程序有一个信息流列表,每个子项都有一个分享按钮,我希望点击分享的时候在分享的地址中带上当前列表的标识,这样用户点击分享的卡片就能直接进入当前子项的详情页,但是,分享功能并不是绑定在 click 事件上呀,经试验,onShareAppMessage 触发在 button 的 click 事件的前面,这样就无法通过 button 的 click 方法来获取点击的子项了。但是 onShareAppMessage 处理函数的参数中有一个 target,如果 from 值是 button,则 target 是触发这次转发事件的 button,否则为 undefined。soga,把子项的 id 放到 button 中就好啦,我是如下处理的:

<button open-type='share'
        :data-title='data.content'
        :data-id='data.id'
        class="share-btn">
        <img src="../../../static/share.png" />
        <span>分享</span>
</button>

然后在 onShareAppMessage 中可以直接通过 target.dataset 获取 button 的 data 属性了

5.mpvue 性能优化:for 循环中不要使用组件

在使用 for 循环渲染中尽量不要使用组件,以前写 vue 喜欢封装,列表渲染的时候就把渲染内容封装为一个组件,把数据通过 prop 传递给组件,这样简单又省事。但是,在 mpvue 中,如果在 for 循环中使用自定义组件的话,数据会在每个组件中都存储一份,这样会严重影响性能!!!

6.小程序的 HTTP 限制

小程序所有请求接口都要求是 HTTPS,图片如果要使用保存到本地的话也需要是 HTTPS 地址,限制太严格了,域名的话需要自己申请。一套走下来也需要费不少功夫的

7.其他

小程序的限制也比较多,个人开发的话工具类还是比较好实现的,不太需要后台加持。小程序用完即走,用户留存比较困难,虽然累积人数达 1K 以上可以投放广告了,但是用户留存和点击量还是比较难上去,变现困难。

webGL入门-绘制第一个三角形

webGL 是浏览器提供的 3D API,可以在浏览器端实现一些比较炫酷的 3D 效果,其实 webGL 只是一个光栅化引擎,能够根据给定的数据绘制出点、线、三角形,并将其填充,输出栅格图像,万丈高楼平地起,3D 图像其实也是由简单的三角形组合而成,webGL 能绘制出什么完全看你自己的使用。webGL 在电脑的 GPU 中运行,需要编写能在 GPU 端运行的代码:顶点着色器和片元着色器。顶点着色器定义图像的顶点并确定位置,片元着色器确定图像的颜色,webGL 所做的就是给顶点着色器传递数据确定位置,然后调用片元着色器填充颜色,这就是 webGL 绘图的简单流程。那么,在代码中如何编写呢,下面一步步的来看:

1.获取 webGL 上下文:WebGLRenderingContext

编写 webGL 程序需要一个 canvas 元素作为绘图区域,并创建 3D 绘图上下,3D 上下文有 4 种,我们只需要根据浏览器的支持来确定参数,可以编写如下的代码获取 webGL 绘图上下文:

const names = ['webgl', 'experimental-webgl', 'webkit-3d', 'moz-webgl']
let context = null
for (let ii = 0; ii < names.length; ++ii) {
  try {
    context = canvas.getContext(names[ii])
  } catch (e) {
    // no-empty
  }
  if (context) {
    break
  }
}

2. 编译着色器

着色器分为顶点着色器和片元着色器,顶点着色器主要是顶点装配,确定位置,一个基本的顶点着色器如下所示:

attribute vec4 aPosition;

void main() {
  gl_Position = aPosition;
}

片元着色器主要是确定渲染颜色,一个基本的片元着色器如下所示:

void main() {
  gl_FragColor = vec4(1.0);
}

由于片元着色器是以文本的形式存在于浏览器端的,在绘制之前需要先进行编译:
对于顶点着色去我们创建gl.VERTEX_SHADER类型的着色器,对于片元着色器我们创建gl.FRAGMENT_SHADER类型的着色器

const vertShdr = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertShdr, vertex)
gl.compileShader(vertShdr)

const fragShdr = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragShdr, fragment)
gl.compileShader(fragShdr)

3.连接到上下文 program

着色器编译之后我们需要链接到绘图上下文的 program 上面,这样程序才知道我们绘制的时候使用哪个着色器。
我们先创建一个 program,然后添加已编译的着色器,最后链接到上下文 program 上。

const program = gl.createProgram()
gl.attachShader(program, vertShdr)
gl.attachShader(program, fragShdr)
gl.linkProgram(program)

4.创建 buffer

编译好之后,我们就可以给着色器传递一些数据来绘制图形了,这里我们绘制一个基本的三角形。
在 webGL 中,坐标轴位于画布中心,xy 的范围为[-1,1],并且由于着色器是强类型的,因此我们在传递数据的是不能使用传统的 JS array,需要使用强类型的Float32Array。buffer 可以认为是 webGL 中的一个全局变量,设置之后绑定数据然后就着色器就可以在 buffer 中获取所需的数据,具体代码如下:

// 顶点数据
const points = new Float32Array([-1.0, 0.0, 1.0, 0.0, 0.0, 1.0])

// 创建buffer
const verticesBuffer = gl.createBuffer()
// 绑定我们创建的buffer
gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer)
// 给绑定的buffer填充数据
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)

5.启用位置点,并设置数据读取方式

在着色器中我们设置了一个变量:attribute vec4 aPosition;,现在我们要启用它,并从 buffer 中读取数据,由于着色器编译之后我们存储在内存中,

我们需要先获取它的位置:

const aPosition = gl.getAttribLocation(program, 'aPosition')

然后启用它:

gl.enableVertexAttribArray(aPosition)

然后告诉它怎么从缓冲中读取数据,一个隐藏的信息是gl.vertexAttribPointer将当前属性绑定到了ARRAY_BUFFER上,也就是我们之前创建并绑定的verticesBuffer上面。

// 告诉属性怎么从positionBuffer中读取数据 (ARRAY_BUFFER)
const size = 2 // 每次迭代运行提取两个单位数据
const type = this.gl.FLOAT // 每个单位的数据类型是32位浮点型
const normalize = false // 不需要归一化数据
const stride = 0 // 0 = 移动单位数量 * 每个单位占用内存(points.BYTES_PER_ELEMENT)
// 每次迭代运行运动多少内存到下一个数据开始点
const offset = 0 // 从缓冲起始位置开始读取
this.gl.vertexAttribPointer(aPosition, size, type, normalize, stride, offset)

aPosition 是一个 vec4 变量,相当于一个 length 为 4 的数据,在 webGL 中,gl_Position我们只设置了前 2 个分量,而后两个分量将使用默认值 0 和 1.

6.绘制

绘制之前我们先清除画布

// 清除
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.clear(gl.COLOR_BUFFER_BIT)

// 绘制图元
var primitiveType = gl.TRIANGLES
// 偏移
var offset = 0
// 绘制次数
var count = 3
gl.drawArrays(primitiveType, offset, count)

因为我们有 3 个点,因此绘制次数为 3 次,我们选择绘制的图元为三角形。最终:绘制效果如下:

结果

以上,webGL 本身做的事情很简单,就是根据我们给定的数据和着色器来绘制一个三角形,但是由于其 API 比较底层,每一步都需要我们来指定,从获取上下文到编译着色器以至于设置缓冲等都需要我们一步步的在程序中编写,webGL 难在于我们编写程序本身,如何从简单的三角形搭建出绚丽的三维效果,这就是困难所在。

Nginx 简单配置和使用

Nginx 简单配置和使用

nginx 自然不用多说,强大的静态文件服务器,前端工作做多了或多或少会有写服务部署的工作,正好自己也有一个便宜的阿里云服务器,那么就来折腾一下。

1. 基本配置

首先,先起一个静态文件服务器,简单配置如下:

# 启用服务使用的用户,Linux上跟权限有关
user nginx;

worker_processes auto;
# 错误日志
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
  worker_connections 1024;
}

http {
  # hide nginx version
  server_tokens off;

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  server {
    #端口
    listen 80;
    # 服务名
    server_name www.test.cn;
    # 文件夹
    root /usr/share/nginx/html;

    location / {
      index index.html ;
    }
  }
}

其中主要部分是 server 中的配置,配置好端口,服务名,以及文件夹三分部就可以运行期我们的服务了。

2.开启 Gzip 压缩

Gzip 能够压缩我们的网络资源,加快网路传输速度,简单的 gzip 配置如下:

# 开启gzip
gzip on;

# 启用gzip压缩的最小文件;小于设置值的文件将不会被压缩
gzip_min_length 1k;

# gzip 压缩级别 1-10
gzip_comp_level 2;

# 进行压缩的文件类型。
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;

由于图片本身已有压缩,开启 gzip 压缩不明显,而且对于大文件等开启压缩会占用较大的 CPU 资源,且效果不明显,所以不建议开启。

3.开启缓存

使用缓存可以使得再次打开网站的速度变快,如果网站本身静态资源变化不多的时候,开启缓存效果尤为明显,另外对于单页应用,js,css 等资源都有 hash 值,可以不用缓存,而 html 作为单一入口,可以不缓存,基本的配置如下:

# HTML不缓存
location ~ .*\.(html)(.*) {
  add_header Cache-Control max-age=no-cache;
  expires 0;
}
#JS 和 CSS 缓存时间设置
location ~ .*\.(css|js)(.*) {
  expires 3d;
}

# 通配所有以....结尾的请求
location ~* \.(png|jpg|jpeg|gif|gz|svg|mp4|ogg|ogv|webm|htc|xml|woff)(.*) {
  access_log off;
  expires 7d;
}

4.配置二级域名

域名注册和使用比较麻烦,而且注册完一个域名后如果我们只有一个服务器,但是想弄多个网站的话,我们可以配置一个二级域名,主域名注册之后二级域名不需要注册即可使用,nginx 同样支持二级域名的配置。

例如,我们的主域名为 test.cn,我们在服务器厂商那么配置了一个二级域名 a.test.cn,首先,我们的主配置文件 nginx.conf 配置和 1 里面的配置基本相同,主要区别在于 http 中的配置,nginx.confhttp 配置如下:

http {
  # hide nginx version
  server_tokens off;

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  # 新增
  include /etc/nginx/conf.d/*.conf;
}

首先,我们新建一个 conf.d 文件夹来存放我们不同网站的 server 配置,并在nginx.conf中 include 它们。

我们在conf.d文件夹中新建一个 test.cn.conf 配置文件,内容如下:

server {
  listen 80 default_server;
  listen [::]:80 default_server;
  # server name
  server_name www.test.cn test.cn;

  return 301 https://$server_name$request_uri;
}
server {
  listen 443 ssl http2 default_server;
  listen [::]:443 ssl http2 default_server;
  # server name
  server_name www.test.cn test.cn;
  root /usr/share/nginx/html/www;

  #设置长连接
  keepalive_timeout 70;

  #HSTS策略
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

  ssl_certificate "/etc/nginx/cert/4332672_www.test.cn.pem";
  ssl_certificate_key "/etc/nginx/cert/4332672_www.test.cn.key";
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;
  ssl_ciphers PROFILE=SYSTEM;
  ssl_prefer_server_ciphers on;

  # Load configuration files for the default server block.
  include /etc/nginx/default.d/*.conf;

  location / {
    index index.html ;
  }

  error_page 404 /404.html;
  location = /40x.html {
  }

  error_page 500 502 503 504 /50x.html;
  location = /50x.html {
  }
}

主域名test.cn 我们使用https,并将http请求重定向到https。

另外,我们在在conf.d文件夹中新建一个 a.test.cn.conf 二级域名配置文件,内容如下:

server {
  listen 80;
  # 服务名
  server_name a.test.cn;
  root /usr/share/nginx/html/a;

  location / {
    index index.html ;
  }

  include /etc/nginx/default.d/*.conf;
}

注意:子域名 a.test.cn的server_name需要设置为a.test.cn 不可添加www前缀,这样我们就完成了主域名和子域名的配置

nginx的基本使用如上所示,还有常用的 proxy_pass 待后面用到了再补充

React**要点

react 是一个优秀的前端框架,虽然平常都是用的 vue,但是 react 的大名也是如雷贯耳,受网上一些教程的启发,决定自己简单实现一遍 react,对它的工作方式以及原理也有了更深的了解

React 要点

react 主要的要点就是 JSX 和虚拟 DOM,

  1. JSX 是一种扩展语法,用于在 js 代码中编写 HTML 片段,如下:
const div = <div className="main">Hello World</div>

JSX 语法通过 babel 转义之后本质是调用函数来处理的,例如上面的代码经过 Babel 处理之后在 React 中会转成以下代码:

const div = React.createElement(
    'div', 
    { className: 'main' },
    'Hello World'
)

除了使用React.createElement()方法,我们也可以使用自己定义的函数来编译JSX,在**.babelrc**中配置下就可以

{
    "presets": ["env"],
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

transform-react-jsx 下面的pragma就是转换时使用的函数,我们可以换成其它的函数,只要在全局中存在这个函数名就可以。函数的参数为以下:tag(标签名),attrs(属性), children(子节点)

function createElement( tag, attrs, ...children ) {
    return {
        tag,
        attrs,
        children
    }
}

所以我们在开发过程中写的JSX代码,最后会被Babel编译成{tag,attrs,children}对象,我们只需要根据这个对象来创建相应的节点,设置属性,对其子元素递归调用render函数就可以了,最后挂载到节点上,完成渲染。

节点一般有以下几种情况:

  • 数字或者文本内容:这种直接创建文本节点就可以

  • 函数或者类:这种说明节点是一个组件,我们直接调用该组件的render方法获取其返回的JSX内容然后再继续进行解析即可(这也是为什么React组件必须实现Render方法或者必须返回JSX内容的原因)

  1. 虚拟DOM主要就是使用JS语法模拟出来的DOM对象,与原生DOM要素相比更加轻量,也方便使用JS来渲染节点。

    要保持数据更新之后视图随之变化,我们需要约定一种数据更新方法,以便我们能够在数据更新之后调用render方法来更新视图。所以React规定更新数据只能用setState ,这样我们就能在state变更之后调用render函数对数据进行跟新,setState方法简易实现如下:

    setState(newState) {
        this.state = Object.assign(this.state, newState)
        render(this)
     }

    在元素更新的时候我们有很多可以优化的地方,不用每次更改一点数据就全部重新渲染,我们可以只更新更改的部门,这就涉及到diff算法了,我们可以对已有的虚拟DOM和数据更改后的虚拟DOM进行对比,找出差异的部门进行更新,具体则不做描述。

  2. 虚拟DOM的**不仅能运用在HTML上,只要与视图相关的都可以使用,因为React的**就是view=render(data). 我们只要实现我们自己的render函数,根据{tag,attrs,children}来解析我们的JSX就可以,不必局限于DOM元素,canvas元素或者我们自定义的绘图要素都可以,唯一的限制是你的想象力。

Mapbox 入门及基础知识

mapbox 是一个优秀的 2D、3D 地图库,它提供精美的地图服务以及高效的地图渲染方式。map 使用 webGL 渲染地图,可以动态修改地图样式,满足不同的需求

1. Mapbox 中 layer 和 source 的关系

mapbox 使用数据来驱动图层的,和其他的如 leaflet,openlayer 这类地图库不一样,除了底图之外,其他的图层都需要提供 source 图层,layer 根据 source 作为数据进行渲染。每一个 layer 和 source 都需要有一个 id,通过 id 名称来进行区分。一个 layer 对应一个 source,但是通一个 source 可以对应多个 layer。这样就方便对数据进行分层管理,可以将 source 中不同类别的数据展示在不同的 layer 中。
mapbox 支持的 source 有这些:vector, raster, raster-dem, geojson, image, video.
例如:加载 geojson 数据:

map.addSource('geoData', {
  type: 'geojson',
  data: data
})

mapbox 没有提供加载文件的功能,所以如果是从文件中加载的话,需要自己写 XHR 请求,然后再设置 source,例如,从文件中加载 geojson 可以这么写:

fetch('./data/data.geojson')
  .then(resp => resp.json())
  .then(data => {
    //调用addSource方法
  })

2.mapbox 中的 paint

paint 属性用来设置 mapbox 图层的绘制规则,这是 mapbox 的精华所在,mapbox 文档有专门一栏讲解样式设置的,这里面可以设置绘制面的颜色,填充色,透明度等等,而且还可以通过一些逻辑判断来设置不同的属性,十分灵活和复杂。建议通过官网来学习

3.mapbox 拉起建筑物以及点击建筑物高亮示例

map.on('load', function() {
  // 请求geojson数据
  fetch('./data/data.geojson')
    .then(resp => resp.json())
    .then(data => {
      // 加载图片作为建筑物纹理,异步方法
      map.loadImage('./data/texture.jpg', (err, image) => {
        // 纹理加载
        map.addImage('texture', image)

        // 设置数据源,由于原数据中没有id属性,由 mapbox生成唯一ID值
        map.addSource('geoData', {
          type: 'geojson',
          data,
          generateId: true
        })

        // 建筑物贴图
        map.addLayer({
          id: '3d-build',
          // 设置数据源
          source: 'geoData',
          type: 'fill-extrusion',
          // 只显示属性值building==yes的数据
          filter: ['==', 'building', 'yes'],
          // 缩放级别大于15级显示
          minzoom: 15,
          paint: {
            // 拉升高度通过属性值height获取
            'fill-extrusion-height': ['to-number', ['get', 'height']],
            'fill-extrusion-base': 0,
            'fill-extrusion-opacity': 1,
            // 设置纹理
            'fill-extrusion-pattern': 'texture'
          }
        })

        // 建筑物顶部
        map.addLayer({
          id: 'buildTop',
          // 数据源
          source: 'geoData',
          type: 'fill-extrusion',
          // 针对建筑物
          filter: ['==', 'building', 'yes'],
          minzoom: 15,
          paint: {
            // 填充颜色根据状态切换
            'fill-extrusion-color': [
              'case',
              ['boolean', ['feature-state', 'hover'], true],
              '#aaa',
              'red'
            ],
            // 起始高度和拉升高度相同,则只是顶部的面被处理
            'fill-extrusion-height': ['to-number', ['get', 'height']],
            'fill-extrusion-base': ['to-number', ['get', 'height']],
            'fill-extrusion-opacity': 1
          }
        })

        // 点击事件
        let hoverId = null
        map.on('click', '3d-build', e => {
          if (e.features.length > 0) {
            // hoverId为空,表示是初次点击
            if (!hoverId) {
              // 获取要素ID,根据数据值和id设置状态
              hoverId = e.features[0].id
              map.setFeatureState({ source: 'geoData', id: hoverId }, { hover: false })
            } else {
              map.setFeatureState({ source: 'geoData', id: hoverId }, { hover: true })
              hoverId = null
            }
          }
        })
      })
    })
})

在线示例

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.