Exia

Exia 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

Exia 赞了回答 · 5月25日

解决git怎么删除远程分支

删除远程分支和tag

在Git v1.7.0 之后,可以使用这种语法删除远程分支:

$ git push origin --delete <branchName>
删除tag这么用:

git push origin --delete tag <tagname>

否则,可以使用这种语法,推送一个空分支到远程分支,其实就相当于删除远程分支:

git push origin :<branchName>
这是删除tag的方法,推送一个空tag到远程tag:

git tag -d <tagname>
git push origin :refs/tags/<tagname>
两种语法作用完全相同。

删除不存在对应远程分支的本地分支

假设这样一种情况:

我创建了本地分支b1并pull到远程分支 origin/b1;
其他人在本地使用fetch或pull创建了本地的b1分支;
我删除了 origin/b1 远程分支;
其他人再次执行fetch或者pull并不会删除这个他们本地的 b1 分支,运行 git branch -a 也不能看出这个branch被删除了,如何处理?
使用下面的代码查看b1的状态:

$ git remote show origin
* remote origin
  Fetch URL: git@github.com:xxx/xxx.git
  Push  URL: git@github.com:xxx/xxx.git
  HEAD branch: master
  Remote branches:
    master                 tracked
    refs/remotes/origin/b1 stale (use 'git remote prune' to remove)
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)
这时候能够看到b1是stale的,使用 git remote prune origin 可以将其从本地版本库中去除。

更简单的方法是使用这个命令,它在fetch之后删除掉没有与远程分支对应的本地分支:

git fetch -p

http://zengrong.net/post/1746...

关注 9 回答 5

Exia 赞了回答 · 5月21日

求助fullpage.js可以悬停某个元素的时候禁止滚动页面吗?

图片描述

已解决~

关注 1 回答 1

Exia 赞了文章 · 4月6日

vue 组件中添加样式不生效

如何产生

在开发项目中遇到在组件中添加样式不生效的情况。具体场景如下

//// vue 组件
<template>
    <div class="box" data-v-33f8ed40></div>
<template>

//我用js在上面div标签中插入一个<p class='text'>text goes here</p>

<script>
    export default {
    ...
    mounted(){
    $('.box').html('<p class="text">text goes here</p>')
      },
    ...
    }
</script>
//style , vue组件scoped样式都会在选择器的最后加上data-v-***属性
<style scoped> 
    //样式添加了scoped
    
    .box{
    color:red;
    }
    .text{
    color:blue;
    }
</style>




浏览器渲染的html 和 style 如下:

//html
<div class="box" data-v-33f8ed40>
    <p class='text'>text goes here</p>
</div>

//style
.box[data-v-33f8ed40]{
color:red;
}

.text[data-v-33f8ed40]{ //样式不生效,因为p标签里没有属性data-v-33f8ed40
color:blue;
}

如何解决

很简单将去掉 style 的 scoped 属性。

查看原文

赞 1 收藏 0 评论 1

Exia 赞了文章 · 3月21日

前端面试之websocket篇

还是同以往一样,面试会考到的地方,我都会做出标记,websocket如何在前端如何用的,这个得用,别这个都不知道,那这个教程就没用了。如果你想对其原理进行深入了解,那么本教程将非常适合你,除此之外,我也把讲解HTTP和HTTPS进行一起讲解,让你对照看着更加清除。
websocket是HTML5的一个新协议,它允许服务端向客户端传递信息,实现浏览器和客户端双工通信。websocket弥补了HTTP不支持长连接的特点,那么在学习websocket之前我们先来了解以下HTTP。

HTTP

HTTP是用于传输如HTML文件,图片文件,查询结果的应用层协议。它被设计于用于服务端和客户端之间的通信。在工作的时候,客户端打开一个连接以发出请求,然后等待服务端响应,服务端不能主动向客户端发送请求。HTTP是无状态协议,意味着服务器不会在两个请求之间保留任何数据。那么这就带来了一个问题,比如说在一个电商网站中,把某个物品加入了购物车,换了一个页面后,在添加一个物品,两次添加物品的请求没有联系,浏览器无法知道用户选择了那些商品。解决方法是在HTTP头部中加入cookie信息这样每次请求都能够共享相同的状态。

那么HTTP请求响应的工作流程是什么呢?

1、客户端连接到web服务器,与web服务器的HTTP端口(默认是80)建立一个TCP套接字连接
2、发送HTTP请求 通过TCP套接字,客户端向服务器发送一个文本的请求报文,一个请求报文由请求行,请求头部,空行和请求数据四个部分构成
3、服务端接受请求并返回HTTP响应 web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。
4、释放连接TCP连接 Web服务器主动关闭TCP套接字,释放TCP连接;客户端被动关闭TCP套接字,释放TCP连接。
5、客户端浏览器解析HTML内容 客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
关于HTTP内容不多讲,太多了我也讲不了。推荐大家去看《HTTP权威指南》很小的一本书,讲的很清楚。

HTTPS

HTTPS是什么呢,他与HTTP的关系又是什么。在上一段讲过HTTP是我们平时浏览网页时使用的一种协议。HTTP协议传输的数据都是没有加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常的不安全。为了让这些隐私数据能够加密传输,便设计了SSL协议对HTTP传输的数据进行加密,从而诞生了HTTPS。而后对SSL进行不断的升级,出现了TLS。但是呢,名字用久了也就产生了感情,所以也一直延续着SSL是HTTPS的代名词的习惯。
下面这张图能够让你很清晰的认识到HTTP和HTTPS之间的关系这儿有一张图

http

那么HTTPS的加密到底是怎么一回事呢。

加密过程如下:

1、浏览器将自己支持的一套加密规则发送给网站。
2、网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
3、获得网站证书之后浏览器要做以下工作:
(1) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。
(2) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。
(3) 使用约定好的HASH计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。
4、网站接收浏览器发来的数据之后要做以下的操作:
(1) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。
(2) 使用密码加密一段握手消息,发送给浏览器。
5、浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

websocket

讲了这么多终于讲到了websocket。websocket相对与HTTP协议来说是一个持久化的协议。下面是一个典型的websocket握手

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

可以看到相对于HTTP的头部多了一个头部,其实我们就可以说,websocket借用了HTTP的握手,是HTTP的一个解决特定问题的补丁。 我们在看看上面的头部相对HTTP头部都有哪些变化。

Upgrade: websocket
Connection: Upgrade

这就是websocket的核心,告诉服务器这是websocket请求,而不是http请求

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Sec-WebSocket-Key是一个Base64 encod的值,是一个随机生成的,用于验证是否是真正的websocket
然后Sec-WebSocket-Protocol是一个用户定义的字符串,用来区分同URL下,不同的服务需要不同的协议
Sec-WebSocket-Version这个不用说就是websocket的版本号。 那么服务端就会返回下列东西

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

Sec-WebSocket-Accept表示经过服务器确认,并且对客户端的Sec-WebSocket-Key进行了加密。
Sec-WebSocket-Protocol表示最终使用的协议。
在工作的时候websocket和HTTP有哪些差异呢,下面这张图很好的表示了区别。

websocket2

可以看到HTTP获取数据的时候,需要不断的问服务端是否有我要的数据啊,如果有数据就返回数据,没有就过一段时间再次询问服务端是否有我需要的数据。那websocket呢,它只建立一次连接,那么这个连接就不会断,服务端如果有数据的话,会自动返回数据给客户端,还有一个问题,在HTTP中我们提到,HTTP是无状态的,意思它健忘,上一次的请求和这次的请求都没什么联系,我们需要引用cookie才能解决。那么在websockt中,因为是一次长连接,那么这就不用一次次加入cookie,是不是方便很多了。 下面的代码就是websocket在前端代码中的应用

if ('WebSocket' in window) {
  websocket = new WebSocket("地址");
} else {
    // 不支持websocket
}
websocket.send = ('msg')
alert(websocket.readyState) // websocket的准备状况
websocket.onerror = function(){}
websocket.onopen = function(){}
websocket.onmessage = function(){}
websocket.onclose = function(){}
查看原文

赞 6 收藏 14 评论 0

Exia 回答了问题 · 3月16日

怎样自定义设置border-bottom的长度

我也是看到 @冯恒智 的回答,补充一下实现吧

<div class='block'></block>

style 可以这样写
.block::after {
content: '';
  width: 96%;
  height: 1px;
  display: block;
  margin: 0 auto;
  border-bottom: 1px solid rgba(151, 151, 151, 0.1);
}

关注 2 回答 2

Exia 收藏了文章 · 2019-12-19

Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5

前言

想必你一定使用过易企秀或百度H5等微场景生成工具制作过炫酷的h5页面,除了感叹其神奇之处有没有想过其实现方式呢?本文从零开始实现一个H5编辑器项目完整设计思路和主要实现步骤,并开源前后端代码。有需要的小伙伴可以按照该教程从零实现自己的H5编辑器。(实现起来并不复杂,该教程只是提供思路,并非最佳实践)

Github: 传送门<br/>
演示地址:传送门

编辑器预览:

技术栈

前端:
vue: 模块化开发少不了angular,react,vue三选一,这里选择了vue。
vuex: 状态管理
sass: css预编译器
element-ui:不造轮子,有现成的优秀的vue组件库当然要用起来。没有的自己再封装一些就可以了。
loadsh:工具类

服务端:
koa:后端语言采用nodejs,koa文档和学习资料也比较多,express原班人马打造,这个正合适。
mongodb:一个基于分布式文件存储的数据库,比较灵活。

阅读前准备

1、了解vue技术栈开发
2、了解koa
3、了解mongodb

工程搭建

基于vue-cli3环境搭建

  • 如何规划好我们项目的目录结构?首先我们需要有一个目录作为前端项目,一个目录作为后端项目。所以我们要对vue-cli 生成的项目结构做一下改造:
···
·
|-- client                // 原 src 目录,改成 client 用作前端项目目录
|-- server                // 新增 server 用于服务端项目目录
|-- engine-template        // 新增 engine-template 用于页面模板库目录
|-- docs                // 新增 docs 预留编写项目文档目录
·
···
  • 这样的话 我们需要再把我们webpack配置文件稍作一下调整,首先是把原先的编译指向src的目录改成client,其次为了 npm run build 能正常编译 client 我们也需要为 babel-loader 再增加一个编译目录:

    • 根目录新增vue.config.js,目的是为了改造项目入口,改为:client/main.js

          module.exports = {    
            pages: {        
              index: {            
                entry: "client/main.js"        
              }    
            }
          }
    • babel-loader能正常编译 client, engine-template目录, 在vue.config.js新增如下配置

      // 扩展 webpack 配置
      chainWebpack: config => {
          config.module
          .rule('js')
          .include.add(/engine-template/).end()
          .include.add(/client/).end()
          .use('babel')
          .loader('babel-loader')
          .tap(options => {
          // 修改它的选项...
          return options
          })
      }

这样我们搭建起来一个简易的项目目录结构。

工程目录结构

|-- client                    --------前端项目界面代码
    |--common                    --------前端界面对应静态资源
    |--components                --------组件
    |--config                    --------配置文件
    |--eventBus                    --------eventBus
    |--filter                    --------过滤器
    |--mixins                    --------混入
    |--pages                    --------页面
    |--router                    --------路由配置
    |--store                    --------vuex状态管理
    |--service                    --------axios封装
    |--App.vue                    --------App
    |--main.js                    --------入口文件
    |--permission.js            --------权限控制
|-- server                    --------服务器端项目代码
    |--confog                    --------数据库链接相关
    |--middleware                --------中间件
    |--models                    --------Schema和Model
    |--routes                    --------路由
    |--views                    --------ejs页面模板
    |--public                    --------静态资源
    |--utils                    --------工具方法
    |--app.js                    --------服务端入口
|-- common                    --------前后端公用代码模块(如加解密)
|-- engine-template            --------页面模板引擎,使用webpack打包成js提供页面引用
|-- docs                    --------预留编写项目文档目录
|-- config.json                --------配置文件

前端编辑器实现

编辑器的实现思路是:编辑器生成页面JSON数据,服务端负责存取JSON数据,渲染时从服务端取数据JSON交给前端模板处理。

数据结构

确认了实现逻辑,数据结构也是非常重要的,把一个页面定义成一个JSON数据,数据结构大致是这样的:

页面工程数据接口

{
    title: '', // 标题
    description: '', //描述
    coverImage: '', // 封面
    auther: '', // 作者
    script: '', // 页面插入脚本
    width: 375, // 高
    height: 644, // 宽
    pages: [], // 多页页面
    shareConfig: {}, // 微信分享配置
    pageMode: 0, // 渲染模式,用于扩展多种模式渲染,翻页h5/长页/PC页面等等
}

多页页面pages其中一页数据结构:

{
    name: '',
    elements: [], // 页面元素
    commonStyle: {
        backgroundColor: '',
        backgroundImage: '',
        backgroundSize: 'cover'
    },
    config: {}
}

元素数据结构:

{
    elName: '', // 组件名
    animations: [], // 图层的动画,可以支持多个动画
    commonStyle: {}, // 公共样式,默认样式
    events: [], // 事件配置数据,每个图层可以添加多个事件
    propsValue: {}, // 属性参数
    value: '', // 绑定值
    valueType: 'String', // 值类型
    isForm: false // 是否是表单控件,用于表单提交时获取表单数据
}

编辑器整体设计

  • 一个组件选择区,提供使用者选择需要的组件
  • 一个编辑预览画板,提供使用者拖拽排序页面预览的功能
  • 一个组件属性编辑,提供给使用者编辑组件内部props、公共样式和动画的功能

如图:


用户在左侧组件区域选择组件添加到页面上,编辑区域通过动态组件特性渲染出每个元素组件。

最后,点击保存将页面数据提交到数据库。至于数据怎么转成静态 HTML方法有很多。还有页面数据我们全部都有,我们可以做页面的预渲染,骨架屏,ssr,编译时优化等等。而且我们也可以对产出的活动页做数据分析~有很多想象的空间。

核心代码

编辑器核心代码,基于 Vue 动态组件特性实现:

为大家附上 Vue 官方文档:cn.vuejs.org/v2/api/#is

画板元素渲染

编辑画板只需要循环遍历pages[i].elements数组,将里面的元素组件JSON数据取出,通过动态组件渲染出各个组件,支持拖拽改变位置尺寸.

元素组件管理

在client目录新建plugins来管理组件库。也可以将该组件库发到npm上工程中通过npm管理

组件库

编写组件,考虑的是组件库,所以我们竟可能让我们的组件支持全局引入和按需引入,如果全局引入,那么所有的组件需要要注册到Vue component 上,并导出:

client/plugins下新建index.js入口文件

/**
 * 组件库入口
 * */
import Text from './text'
// 所有组件列表
const components = [
    Text
]
// 定义 install 方法,接收 Vue 作为参数
const install = function (Vue) {
    // 判断是否安装,安装过就不继续往下执行
    if (install.installed) return
    install.installed = true
    // 遍历注册所有组件
    components.map(component => Vue.component(component.name, component))
}

// 检测到 Vue 才执行,毕竟我们是基于 Vue 的
if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
}

export default {
    install,
    // 所有组件,必须具有 install,才能使用 Vue.use()
    Text
}

组件开发

示例: text文本组件

client/plugins下新建text组件目录

|-- text                --------text组件
    |--src              --------资源
        |--index.vue    --------组件
    |--index.js         --------入口

text/index.js

// 为组件提供 install 方法,供组件对外按需引入
import Component from './src/index'
Component.install = Vue => {
    Vue.component(Component.name, Component)
}
export default Component

text/src/index.vue

<!--text.vue-->
<template>
  <div class="qk-text">
    {{text}}
  </div>
</template>

<script>
    export default {
        name: 'QkText', // 这个名字很重要,它就是未来的标签名<qk-text></qk-text>
        props: {
            text: {
                type: String,
                default: '这是一段文字'
              }
        }
    }
</script>

<style lang="scss" scoped>
</style>

编辑器里使用组件库:

// 引入组件库
import QKUI from 'client/plugins/index'
// 注册组件库
Vue.use(QKUI)

// 使用:
<qk-text text="这是一段文字"></qk-text>

按照这个组件开发方式我们可以扩展任意多的组件,来丰富组件库

需要注意的是这里的组件最外层宽高都要求是100%

配置文件

Quark-h5编辑器左侧选择组件区域可以通过一个配置文件定义可选组件
新建一个ele-config.js配置文件:

export default [
    {
        title: '基础组件',
        components: [
            {
                elName: 'qk-text', // 组件名,与组件库名称一致
                title: '文字',
                icon: 'iconfont iconwenben',
                // 给每个组件配置默认显示样式
                defaultStyle: {
                    height: 40
                }
            }
        ]
    },
    {
        title: '表单组件',
        components: []
    },
    {
        title: '功能组件',
        components: []
    },
    {
        title: '业务组件',
        components: []
    }
]

公共方法中提供一个function 通过组件名和默认样式获取元素组件JSON,getElementConfigJson(elName, defaultStyle)方法

元素属性编辑

公共属性样式编辑

公共样式属性编辑比较简单就是对元素JSON对象commonStyles字段进行编辑操作

props属性编辑

1.为组件的每一个prop属性开发一个属性编辑组件. 例如:QkText组件需要text属性,新增一个attr-qk-text组件来操作该属性
2.获取组件prop对象
3.遍历prop对象key, 通过key判断显示哪些属性编辑组件

元素添加动画实现

动画效果引入Animate.css动画库。元素组件动画,可以支持多个动画。数据存在元素JSON对象animations数组里。

选择面板hover预览动画


监听mouseover和mouseleave,当鼠标移入时将动画className添加入到元素上,鼠标移出时去掉动画lassName。这样就实现了hover预览动画

编辑预览动画

组件编辑时支持动画预览和单个动画预览。

封装一个动画执行方法

/**
 * 动画方法, 将动画css加入到元素上,返回promise提供执行后续操作(将动画重置)
 * @param $el 当前被执行动画的元素
 * @param animationList 动画列表
 * @param isDebugger 动画列表
 * @returns {Promise<void>}
 */
export default async function runAnimation($el, animationList = [], isDebug , callback){
    let playFn = function (animation) {
        return new Promise(resolve => {
            $el.style.animationName =  animation.type
            $el.style.animationDuration =  `${animation.duration}s`
            // 如果是循环播放就将循环次数置为1,这样有效避免编辑时因为预览循环播放组件播放动画无法触发animationend来暂停组件动画
            $el.style.animationIterationCount =  animation.infinite ? (isDebug ? 1 : 'infinite') : animation.interationCount
            $el.style.animationDelay =  `${animation.delay}s`
            $el.style.animationFillMode =  'both'
            let resolveFn = function(){
                $el.removeEventListener('animationend', resolveFn, false);
                $el.addEventListener('animationcancel', resolveFn, false);
                resolve()
            }
            $el.addEventListener('animationend', resolveFn, false)
            $el.addEventListener('animationcancel', resolveFn, false);
        })
    }
    for(let i = 0, len = animationList.length; i < len; i++){
        await playFn(animationList[i])
    }
    if(callback){
        callback()
    }
}

animationIterationCount 如果是编辑模式的化动画只执行一次,不然无法监听到动画结束animationend事件

执行动画前先将元素样式style缓存起来,当动画执行完再将原样式赋值给元素

let cssText = this.$el.style.cssText;
runAnimations(this.$el, animations, true, () => {
    this.$el.style.cssText = cssText
})

元素添加事件

提供事件mixins混入到组件,每个事件方法返回promise,元素被点击时按顺序执行事件方法

页面插入js脚本

参考百度H5,将脚本以script标签形式嵌入。页面加载后执行。
这里也可以考虑mixins方式混入到页面或者组件,可根据业务需求自行扩展,都是可以实现的。

redo/undo历史操作纪录

  1. 历史操作纪录存在状态机store.state.editor.historyCache数组中。
  2. 每次修改编辑操作都把整个pageDataJson字段push到historyCache
  3. 点击redo/undo时根据index获取到pageDataJson重新渲染页面

psd设计图导入生成h5页面

将psd每个设计图中的每个图层导出成图片保存到静态资源服务器中,

服务端安装psd依赖

cnpm install psd --save

加入psd.js依赖,并且提供接口来处理数据

var PSD = require('psd');
router.post('/psdPpload',async ctx=>{
    const file = ctx.request.files.file; // 获取上传文件
    let psd = await PSD.open(file.path)
    var timeStr = + new Date();
    let descendantsList = psd.tree().descendants();
    descendantsList.reverse();
    let psdSourceList = []
    let currentPathDir = `public/upload_static/psd_image/${timeStr}`
    for (var i = 0; i < descendantsList.length; i++){
        if (descendantsList[i].isGroup()) continue;
        if (!descendantsList[i].visible) continue;
        try{
            await descendantsList[i].saveAsPng(path.join(ctx.state.SERVER_PATH, currentPathDir + `/${i}.png`))
            psdSourceList.push({
                ...descendantsList[i].export(),
                type: 'picture',
                imageSrc: ctx.state.BASE_URL + `/upload_static/psd_image/${timeStr}/${i}.png`,
            })
        }catch (e) {
            // 转换不出来的图层先忽略
            continue;
        }
    }
    ctx.body = {
        elements: psdSourceList,
        document: psd.tree().export().document
    };
})

最后把获取的数据转义并返回给前端,前端获取到数据后使用系统统一方法,遍历添加统一图片组件

  • psd源文件大小最好不要超过30M,过大会导致浏览器卡顿甚至卡死
  • 尽可能合并图层,并栅格化所有图层
  • 较复杂的图层样式,如滤镜、图层样式等无法读取

html2canvas生成缩略图

这里只需要注意下图片跨域问题,官方提供html2canvas: proxy解决方案。它将图片转化为base64格式,结合使用设置(proxy: theProxyURL), 绘制到跨域图片时,会去访问theProxyURL下转化好格式的图片,由此解决了画布污染问题。
提供一个跨域接口

/**
 * html2canvas 跨域接口设置
 */
router.get('/html2canvas/corsproxy', async ctx => {
    ctx.body =  await request(ctx.query.url)
})

渲染模板

实现逻辑

在engine-template目录下新建swiper-h5-engine页面组件,这个组件接收到页面JSON数据就可以把页面渲染出来。跟编辑预览画板实现逻辑差不多。

然后使用vue-cli库打包命令将组件打包成engine.js库文件。ejs模板引入该页面组件配合json数据渲染出页面

适配方案

提供两种方案解决屏幕适配
1、等比例缩放
在将json元素转换为dom元素的时候,对所有的px单位做比例转换,转换公式为 new = old * windows.x / pageJson.width,这里的pageJson.width是页面的一个初始值,也是编辑时候的默认宽度,同时viewport使用device-width。
2.全屏背景, 页面垂直居中
因为会存在上下或者左右有间隙的情况,这时候我们把背景颜色做全屏处理

页面垂直居中只适用于全屏h5, 以后扩展长页和PC页就不需要垂直居中处理。

模板打包

package.json中新增打包命令

"lib:h5-swiper": "vue-cli-service build --target lib --name h5-swiper --dest server/public/engine_libs/h5-swiper engine-template/engine-h5-swiper/index.js"

执行npm run lib:h5-swiper 生成引擎模板js如图

页面渲染

ejs中引入模板

<script data-original="/third-libs/swiper.min.js"></script>

使用组件

<engine-h5-swiper :pageData="pageData" />

后端服务

初始化项目

工程目录上文已给出,也可以使用 koa-generator 脚手架工具生成

ejs-template 模板引擎配置

app.js

//配置ejs-template 模板引擎
render(app, {
    root: path.join(__dirname, 'views'),
    layout: false,
    viewExt: 'html',
    cache: false,
    debug: false
});

koa-static静态资源服务

因为html2canvas需要图片允许跨域,所以在静态资源服务中所有资源请求设置'Access-Control-Allow-Origin':'*'

app.js

//配置静态web
app.use(koaStatic(__dirname + '/public'), { gzip: true, setHeaders: function(res){
    res.header( 'Access-Control-Allow-Origin', '*')
}});

修改路由的注册方式,通过遍历routes文件夹读取文件

app.js

const fs =  require('fs')
fs.readdirSync('./routes').forEach(route=> {
    let api = require(`./routes/${route}`)
    app.use(api.routes(), api.allowedMethods())
})

添加jwt认证,同时过滤不需要认证的路由,如获取token

app.js

const jwt = require('koa-jwt')
app.use(jwt({ secret: 'yourstr' }).unless({
    path: [
        /^\/$/, /\/token/, /\/wechat/,
        { url: /\/papers/, methods: ['GET'] }
    ]
}));

中间件实现统一接口返回数据格式,全局错误捕获并响应

middleware/formatresponse.js

module.exports = async (ctx, next) => {
    await next().then(() => {
        if (ctx.status === 200) {
            ctx.body = {
                message: '成功',
                code: 200,
                body: ctx.body,
                status: true
            }
        } else if (ctx.status === 201) { // 201处理模板引擎渲染

        } else {
            ctx.body = {
                message: ctx.body || '接口异常,请重试',
                code: ctx.status,
                body: '接口请求失败',
                status: false
            }
        }
    }).catch((err) => {
        if (err.status === 401) {
            ctx.status = 401;
            ctx.body = {
                code: 401,
                status: false,
                message: '登录过期,请重新登录'
            }
        } else {
            throw err
        }
    })
}

koa2-cors跨域处理

当接口发布到线上,前端通过ajax请求时,会报跨域的错误。koa2使用koa2-cors这个库非常方便的实现了跨域配置,使用起来也很简单

const cors = require('koa2-cors');
app.use(cors());

连接数据库

我们使用mongodb数据库,在koa2中使用mongoose这个库来管理整个数据库的操作。

  • 创建配置文件

根目录下新建config文件夹,新建mongo.js

// config/mongo.js
const mongoose = require('mongoose').set('debug', true);
const options = {
    autoReconnect: true
}

// username 数据库用户名
// password 数据库密码
// localhost 数据库ip
// dbname 数据库名称
const url = 'mongodb://username:password@localhost:27017/dbname'

module.exports = {
    connect: ()=> {            
        mongoose.connect(url,options)
        let db = mongoose.connection
        db.on('error', console.error.bind(console, '连接错误:'));
        db.once('open', ()=> {
            console.log('mongodb connect suucess');
        })
    }
}

把mongodb配置信息放到config.json中统一管理

  • 然后在app.js中引入
const mongoConf = require('./config/mongo');
mongoConf.connect();

...
服务端具体接口实现就不详细介绍了,就是对页面的增删改查,和用户的登录注册难度不大

启动运行

启动前端

npm run dev-client

启动服务端

npm run dev-server

注意:
如果没有生成过引擎模板js文件的,需要先编辑引擎模板,否则预览页面加载页面引擎.js 404报错

编译engine.js模板引擎
npm run lib:h5-swiper
查看原文

Exia 赞了文章 · 2019-12-09

你可能不知道的 JS 特性:可选链

今天我们来介绍一个你可能没见过的 JS 新特性,目前处于 Stage 3 阶段,它叫作 可选链(optional chaining),它可能解决很多人都面对过的痛点,让我们来了解这下~

为什么我们需要它

想象一下你从某个 api 获取数据,返回的对象嵌套了好多层,这就意味着你需要写很长的属性访问:

// API response object
const person = {
    details: {
        name: {
            firstName: "Michael",
            lastName: "Lampe",
        }
    },
    jobs: [
        "Senior Full Stack Web Developer",
        "Freelancer"
    ]
} 
// Getting the firstName
const personFirstName = person.details.name.firstName;

上面的代码很容易产生错误,我们一般会这么改进:

// Checking if firstName exists
if( person &&
    person.details &&
    person.details.name ) {
        const personFirstName = person.details.name.firstName || 'stranger';
}

可以看到为了访问某个人的 firstName,代码变得非常不优雅。我们可以用 lodash 来优化一下:

_.get(person, 'details.name.firstName', 'stranger');

lodash 的写法可读性更高,但是需要引入额外的依赖,而且在团队内部大家可能不会统一都这么写,那么有没有更好的办法呢?

解决方案

可选链 就是为了解决这个问题而诞生的。

用法

可选链在语法上可能看起来比较陌生,但是用了几次之后你就会很容易适应这种写法。

const personFirstName = person?.details?.name?.firstName;

其实就是在属性访问符 . 的前面加了个问号。我们看上面语句中第一个 ?. ,从 JS 层面,它表示如果 person 的值为 null 或者 undefined,就不会报错而返回 undefined,否则才继续访问后面的 details 属性。而如果后面的属性访问链中有任何一个属性为 null 或者 undefined,那么最终的值就为 undefined

默认值

为了优雅地设置默认值,我们引入另外一个特性:空值合并运算符(nullish-coalescing-operator),听起来好像很复杂,其实也很简单:

const personFirstName = person?.details?.name?.firstName ?? 'stranger';

这个运算符就是 ??,如果它左侧表达式的结果是 undefinedpersonFirstName,就会取右侧的 stranger
是不是跟短路运算符 || 很像,那假如我们把 ?? 换成 || 呢?
上面的例子中,如果 firstName 的值为 0 或者空字符串等非 undefinedfalsy 值,那么最终的结果就不一样了。
?? 就是为了取代 || ,来做设置默认值这件事的。

动态属性

如果你需要使用动态属性,同样很简单:

const jobNumber = 1;
const secondJob = person?.jobs?.[jobNumber] ?? 'none';

上面的代码中, jobs?.[jobNumber]jobs[jobNumber] 的含义是一样的,区别就是前者不会报错。

函数或方法调用

同样的,如果想安全调用一个方法,只需要使用 ?.()

const currentJob = person?.jobs.getCurrentJob?.() ?? 'none';

如果 getCurrentJob 不是一个函数,currentJob 的值就是 none

现在就使用这个特性

很显然,这个特性的兼容性感人,不过没关系,我们有 babel!
立刻,马上就能让你使用它:

babel-plugin-proposal-optional-chaining

最后的话

这个特性在很多其他的语言如 C#Swift 中都有实现,并且 TypeScript 中也已经加入该特性。感兴趣的小伙伴还不快尝试一下,如果嫌安装 babel plugin 太麻烦,直接使用 lodash 的 get 也不失为一种保守的选择~

参考链接

原文链接
JS new feature: Optional Chaining
proposal-optional-chaining
babel-plugin-proposal-optional-chaining
babel nullish-coalescing-operator


欢迎关注我的公众号:码力全开(codingonfire)

关注并回复 副业, 获取技术人的副业秘籍

codingonfire.jpg

查看原文

赞 25 收藏 16 评论 5

Exia 收藏了文章 · 2019-10-11

基于Vue SEO的四种方案

前言:众所周知,Vue SPA单页面应用对SEO不友好,当然也有相应的解决方案,下面列出几种最近研究和使用过的SEO方案,SSR和静态化基于Nuxt.js来说。

  • 1.SSR服务器渲染;
  • 2.静态化;
  • 3.预渲染prerender-spa-plugin;
  • 4.使用Phantomjs针对爬虫做处理。

1.SSR服务器渲染

关于服务器渲染:Vue官网介绍,对Vue版本有要求,对服务器也有一定要求,需要支持nodejs环境。

使用SSR权衡之处:

  • 开发条件所限,浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行;
  • 环境和部署要求更高,需要Node.js server 运行环境;
  • 高流量的情况下,请准备相应的服务器负载,并明智地采用缓存策略。

优势:

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面;
  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。

不足:(开发中遇到的坑)
1.一套代码两套执行环境,会引起各种问题,比如服务端没有window、document对象,处理方式是增加判断,如果是客户端才执行:

if(process.browser){
 console.log(window);
}

引用npm包,带有dom操作的,例如:wowjs,不能用import的方式,改用:

if (process.browser) {
     var { WOW } = require('wowjs');
     require('wowjs/css/libs/animate.css');
 }

2.Nuxt asyncData方法,初始化页面前先得到数据,但仅限于页面组件调用:

// 并发加载多个接口:
  async asyncData ({ app, query }) {
    let [resA, resB, resC] = await Promise.all([
      app.$axios.get('/api/a'),
      app.$axios.get('/api/b'),
      app.$axios.get('/api/c'),
     ])
     
     return {
       dataA: resA.data,
       dataB: resB.data,
       dataC: resC.data,
     }
  }

在asyncData中获取参数:

1.获取动态路由参数,如:

/list/:id' ==>  '/list/123

接收:

async asyncData ({ app, query }) {
  console.log(app.context.params.id) //123
}
2.获取url?获取参数,如:

/list?id=123

接收:

async asyncData ({ app, query }) {
  console.log(query.id) //123
}

3.如果你使用v-if语法,部署到线上大概也会遇到这个错误:

Error while initializing app DOMException: Failed to execute 'appendChild' on 'Node': This node type does not support this method.
    at Object.We [as appendChild]

根据github nuxt上的issue第1552条提示,要将v-if改为v-show语法。

4.坑太多,留坑,晚点更。

2.静态化

静态化是Nuxt.js打包的另一种方式,算是 Nuxt.js 的一个创新点,页面加载速度很快。
在 Nuxt.js 执行 generate 静态化打包时,动态路由会被忽略。

-| pages/
---| index.vue
---| users/
-----| _id.vue

需要动态路由先生成静态页面,你需要指定动态路由参数的值,并配置到 routes 数组中去。

// nuxt.config.js
module.exports = {
  generate: {
    routes: [
      '/users/1',
      '/users/2',
      '/users/3'
    ]
  }
}

运行打包,即可看见打包出来的页面。
但是如果路由动态参数的值是动态的而不是固定的,应该怎么做呢?

  • 使用一个返回 Promise 对象类型 的 函数;
  • 使用一个回调是 callback(err, params) 的 函数。
// nuxt.config.js
import axios from 'axios'

export default {
  generate: {
    routes: function () {
      return axios.get('https://my-api/users')
      .then((res) => {
        return res.data.map((user) => {
          return {
            route: '/users/' + user.id,
            payload: user
          }
        })
      })
    }
  }
}

现在我们可以从/users/_id.vue访问的payload,如下所示:

async asyncData ({ params, error, payload }) {
  if (payload) return { user: payload }
  else return { user: await backend.fetchUser(params.id) }
}

如果你的动态路由的参数很多,例如商品详情,可能高达几千几万个。需要一个接口返回所有id,然后打包时遍历id,打包到本地,如果某个商品修改了或者下架了,又要重新打包,数量多的情况下打包也是非常慢的,非常不现实。
优势:

  • 纯静态文件,访问速度超快;
  • 对比SSR,不涉及到服务器负载方面问题;
  • 静态网页不宜遭到黑客攻击,安全性更高。

不足:

  • 如果动态路由参数多的话不适用。

3.预渲染prerender-spa-plugin

如果你只是用来改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。

$ cnpm install prerender-spa-plugin --save

vue cli 3 vue.config.js配置:

const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
const path = require('path');
module.exports = {
    configureWebpack: config => {
        if (process.env.NODE_ENV !== 'production') return;
        return {
            plugins: [
                new PrerenderSPAPlugin({
                    // 生成文件的路径,也可以与webpakc打包的一致。
                    // 下面这句话非常重要!!!
                    // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
                    staticDir: path.join(__dirname,'dist'),
                    // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
                    routes: ['/', '/product','/about'],
                    // 这个很重要,如果没有配置这段,也不会进行预编译
                    renderer: new Renderer({
                        inject: {
                            foo: 'bar'
                        },
                        headless: false,
                        // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
                        renderAfterDocumentEvent: 'render-event'
                    })
                }),
            ],
        };
    }
}

在main.js中添加:

new Vue({
  router,
  render: h => h(App),
  mounted () {
    document.dispatchEvent(new Event('render-event'))
  }
}).$mount('#app')

注意:router中必须设置 mode: “history”

打包出来可以看见文件,打包出文件夹/index.html,例如:about => about/index.html,里面有html内容。

优势:

  • 改动小,引入个插件就完事;

不足:

  • 无法使用动态路由;
  • 只适用少量页面的项目,页面多达几百个的情况下,打包会很很很慢;

4.使用Phantomjs针对爬虫做处理

Phantomjs是一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。
虽然“PhantomJS宣布终止开发”,但是已经满足对Vue的SEO处理。
这种解决方案其实是一种旁路机制,原理就是通过Nginx配置,判断访问的来源UA是否是爬虫访问,如果是则将搜索引擎的爬虫请求转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。

图片描述

具体代码戳这里:vue-seo-phantomjs
要安装全局phantomjs,局部express,测试:

$ phantomjs spider.js 'https://www.baidu.com'

如果见到在命令行里出现了一推html,那恭喜你,你已经征服PhantomJS啦。
启动之后或者用postman在请求头增加User-Agent值为Baiduspider,效果一样的。

部署上线
线上要安装nodepm2phantomjs,nginx相关配置:

upstream spider_server {
  server localhost:3000;
}

server {
    listen       80;
    server_name  example.com;
    
    location / {
      proxy_set_header  Host            $host:$proxy_port;
      proxy_set_header  X-Real-IP       $remote_addr;
      proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;

      if ($http_user_agent ~* "Baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|bingbot|Sosospider|Sogou Pic Spider|Googlebot|360Spider") {
        proxy_pass  http://spider_server;
      }
    }
}

优势:

  • 完全不用改动项目代码,按原本的SPA开发即可,对比开发SSR成本小不要太多;
  • 对已用SPA开发完成的项目,这是不二之选。

不足:

  • 部署需要node服务器支持;
  • 爬虫访问比网页访问要慢一些,因为定时要定时资源加载完成才返回给爬虫;
  • 如果被恶意模拟百度爬虫大量循环爬取,会造成服务器负载方面问题,解决方法是判断访问的IP,是否是百度官方爬虫的IP。

总结

如果构建大型网站,如商城类,别犹豫,直接上SSR服务器渲染,当然也有相应的坑等你,社区较成熟,英文好点,一切问题都迎刃而解。
如果只是个人博客、公司官网这类,其余三种都可以。
如果对已用SPA开发完成的项目进行SEO优化,而且支持node服务器,请使用Phantomjs

很少写文章,这是我这个月对Vue SEO方案的探索,写的不对的地方请指出,谢谢理解~

2020.4.8更
去年7月份上线改版的一呼百应商城,可以右键查看源代码看效果,就是用了服务器渲染SSR处理,本来由于项目进度以及对服务器渲染了解程度不够深入,前期是单页面,后来对SEO有要求,再把单页改造成SSR。当中遇到种种坑也一一解决了。

忙于工作,微信:iilengiyu,QQ:1058566903,欢迎沟通交流~

查看原文

Exia 回答了问题 · 2019-09-27

有没有办法给 background-image 设置透明度?

可以做到的,只要把用到的 background 属性都写成一条就可以了,如下

background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('http://placehold.it/350x150') no-repeat 0% 20%/ cover;

关注 7 回答 6

Exia 赞了文章 · 2019-09-21

2019年最快的Javascript框架

来体验一下2019年最快的Javascript框架:

图片描述

速度是纯nodejs2倍,更不用说其他依赖nodejs的框架例如express/koa/hapi了,根本不值一提,不但如此,性能还能吊打spring以及一众php7框架。

百闻不如一见,下面我们就来安装尝鲜:

npm install -g es4x-pm

这就装好了。下面我们来用它创建一个项目,新建一个空文件夹,然后:

es4x init

项目就建好了。

我们需要写一个最简单的index.js文件:

console.log('hello')

直接启动吧:

yarn start

嗯,报了个错误:

error package.json: Name can't start with a dot

我们package.json里的名字不能只是一个点,得改一下:

"name": "hello",

再次启动,还是启不起,哦,我们还没有安装依赖:

yarn add @vertx/core

这样就能运行了,但是会报一个警告:

ES4X is using graaljs in interpreted mode! Add the JVMCI compiler module in order to run in optimal mode!

对于我们这样有洁癖的人不能忍受啊。

这是因为我们现在系统环境里的Java还是个旧的Java,而Oracle最新推出的Graal才是最新最好的Java,所以我们先安装一下Graal:

brew cask install graalvm/tap/graalvm-ce

再次执行yarn start

现在警告也没有了,一切顺利!

试着写点ES6的语法:

const a = () => {
    console.log('hello')
}

a()

一样可以顺利执行!

再往后就是生成http服务器,连接mysql等等,这些就跟其他框架大同小异了,不再细讲。

原理

我始终认为,用什么语言根本就是无所谓的,只有外行才讲什么语言是最快的。JS慢只是因为底层的Node慢,而NodeV8引擎又不是拿JS写的,而是拿C++写的。

现在的这个es4x (中文文档)它一样能解释JS语言,只不过它的底层换成了Java,用的是EclipseVert.x技术,而原生的Vert.x技术用的是Java旧版引擎,对ES6支持不好,换成Oracle最新的GraalVM就一切都解决了。当然也不能就此说C++Java慢,那样你就又浅薄了。

有人抬杠说JS不能写操作系统底层,有什么不能写的?无非就是个语言而已,你把它编译成二进制就什么都能干了,没听说过nexe吗?

学了编译原理,你就知道语言是什么根本不重要。会说中文的没有必要看不起会说英文的,会说英文的没有必要看不起会说中文的,什么语言好,放在工程项目当中,重要的只是生态,其他都没有可比性。

查看原文

赞 20 收藏 9 评论 9

认证与成就

  • 获得 22 次点赞
  • 获得 43 枚徽章 获得 0 枚金徽章, 获得 9 枚银徽章, 获得 34 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-25
个人主页被 501 人浏览