整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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

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

Github: https://github.com/huangwei9527/quark-h5

演示地址:http://47.104.247.183:4000/

演示帐号密码均admin

编辑器预览:



技术栈

前端: 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 src="/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

.防抖和节流

有些频繁的操作会导致页面性能和用户体验度低,例如: 输入框搜索会频繁调端口接口,方法缩小等

(1)防抖-debounce当持续触发事件时,一定时间内没有再触发事件,事件处理函数才会执行一次,若设定时间到来之前又一次触发事件,就重新开始延时。

const debounce = (fn, delay) => {
    let timer = null;
    return (...agrs)=> {
        clearTimeout(timer)
        timer = setTimeout(()=> {
            fn.apply(this,args)
        },delay)
    }
}

(2) 节流-thottle当持续触发事件时,保证一段时间内只调用一次时间处理函数

const thottle = (fn, delay=500) => {
    let flag =true;
    return (...arg) => {
        if(!flag) return;
        flag = false;
        setTimeout(()=> {
            fn.apply(this, args);
            flag = true;
        },delay)
    }
}

2.jsonp原理

**jsonp跨域的关键就在于**:服务端需要在返回的数据外包裹一个在客户端定义好的回调函数,这样就在script发送请求之后就能获取到数据。

jsonp的缺点
1.只能get请求,不支持post,put,delete
2.不安全 xss攻击

//通过JQuery Ajax 发起jsonp请求
(注:不是必须通过jq发起请求 , 
     例如:Vue Resource中提供 $.http.jsonp(url, [options]))
$.ajax({
    // 请求方式
    type: "get", 
    // 请求地址
    url: "http://169.254.200.238:8080/jsonp.do", 
    // 标志跨域请求
    dataType: "jsonp",				
    // 跨域函数名的键值,即服务端提取函数名的钥匙(默认为callback)
    jsonp: "callbackparam",   
    // 客户端与服务端约定的函数名称
    jsonpCallback: "jsonpCallback",
    // 请求成功的回调函数,json既为我们想要获得的数据
    success: function(json) {
        console.log(json);
    },
    // 请求失败的回调函数
    error: function(e) {
	alert("error");
    }
});

jsonp解决跨域的方法:
<script>
    var script = document.createElement('scritp');
    script.type = 'text/javascript';
    
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
    document.head.appendChild(script)
    
    // 接受参数的回调函数
    function onBack(res) {
        console.log(res)
    }
</script>

3.vue中的nextTick

涉及场景: vue生命周期create()钩子在进行DOM操作时一定要放在Vue.nextTick()回调函数中。
原因是,在create()钩子执行的时候DOM其实并没有进行任何渲染,而此时进行DOM操作是徒劳无工的,所以此时一定需要吧js代码放进Vue.nextTick()的回调函数中。因为该钩子执行的时候所有的DOM都渲染完毕,此时在这个钩子的回调中进行任何的渲染都是可以的。
Vue官方解释:Vue在更新DOM的时候是异步执行的,只要监听到数据的变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,如果同一个watcher被触发多次,只会被推入队列中一次,这种缓冲时去除重复数据对于避免不必要的计算和DOM操作是必不可少的,然后再下一个时间""tick"中,Vue刷新队列并执行实际(去重后)工作,Vue在内部尝试使用原生的Promise.then, MutationObserver和setTimediate,如果执行环境不支持,则会采用setTimeout(fn,0)代替。

4.css选择器的优先级

1. !importent
2. 内联样式(1000)
3. id选择器(100)
4. class选择器、属性选择器、伪类选择器(10)
5. 元素选择器、关系选择器、伪元素选择器 (1)
6. 通配符(0)

5.BFC

BFC全称块级格式化上下文(Block Formatting Context),BFC提供了一个独立的上下文环境。个环境中的元素不会影响到其他环境中的布局,比如浮动元素会形成BFC浮动元素内部子元素主要受浮动元素的影响,两个浮动元素相互不影响。可以说BFC是一个独立的容器,这个容器内的布局丝毫不受容器外布局的影响。

触发BFC的条件:
    1.根元素或其他包含他的元素
    2.浮动元素(float不为none)
    3.绝对定位元素(position为absolute或fixed)
    4.内联块(display: inline-block)
    5.表格单元格(display:table-cell)
    6.表格标题(display:table-caption)
    7.具有overflow且值不为visible
    8.弹性盒:flex或inline-flex
    9.display:flow-root
    10.cloumn-span: all
    
BFC的约束规则
    1.内部盒会在垂直方向一个一个的排列(可以看做bfc内部有一个常规流)
    2.处于同一个bfc中的元素相互影响,可能会出现边距重叠
    3.每个元素margin box左边,与容器块border box左边相接触(对于从左往右的格式化,否则相反),即使浮动也是如此。
    4.bfc就是容器上的一个独立容器,容器内的子元素不会影响到容器外的元素,反之亦然。
    5.计算bfc高度是,考虑容器内包含的所有元素,包括浮动元素。
    6.浮动盒不会叠加到bfc容器上
BFC可以解决的问题
    1.垂直外边距重叠问题
    2.去除浮动
    3.自适应两列布局

6.盒模型

盒模型包括了,内容区域,内填充区域,边框区域,外边距区域

实现左边宽度固定,右边自适应布局:
<div class="box">
    <div class="box-left"></div>
    <div class="box-right"></div>
</div>

1.利用float、margin实现
.box {
    height: 200px
}
.box>div {
    height: 100%
}
.box-left {
    float: left,
    width: 200px,
}
.box-right {
    margin-left: 200px
}

2.利用calc计算宽度
.box {
    height: 200px
}
.box>div {
    height: 100%
}
.box-left {
    float: left,
    width: 200px,
}
.box-right {
    float: right,
    margin-left: calc(100% - 200px)
}

3.利用float、overflow实现
.box {
    height: 200px
}
.box>div {
    height: 100%
}
.box-left {
    float: left,
    width: 200px,
}
.box-right {
    overflow: hidden
}

4.利用flex
.box {
    height: 200px,
    display: flex
}
.box > div {
    height: 100%
}
.box-left {
    width: 200px
}
.box-right: {
    flex: 1,
    overflow: hidden
}

7.缓存

缓存分为协商缓存,强缓存。强缓存不过服务器,协商缓存需要经过服务器,协商缓存返回的状态码是304。两种缓存机制可以同时存在,强缓存的优先级要高于协商缓存的。

1.强缓存: 浏览器不会向服务器发送任何请求,直接从本地缓存数据中读取数据并返回状态码200。
    header参数:
    1.Expires:过期时间,若设置了过期时间,则浏览器会直接在设置的时间内读取缓存,不在请求。
    2.Cache-Control: 当值设置为max-age=300,则代表这个请求正确返回的时间5分钟再次加载资源,则命中强缓存。
    cache-control设置的值:
        max-age: 用来设置资源可以被缓存的时间。
        s-maxage:和max-age是一样额,只是他只针对代理服务器缓存而言。
        public:指示响应可以被任何缓存区缓存
        private: 只针对个人用户,不会被代理服务器缓存
        no-cache: 强制客户端直接向服务器发送请求,也就是每次请求都必须向服务器发送,服务器接收到请求,会判断资源是否变更,是则返回变更的资源,否则返回304,未变更。
        no-store: 禁止一切缓存
        
        
    
2.协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数加判断是否命中协商缓存,若命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源。
    (1)Etag/if-none-match
    etag: 它是由服务器返回给前端的,用来帮助服务器控制web端的缓存验证
    if-none-match: 当资源过期时,浏览器发现响应头里面有etag,则再次向服务器请求时带上if-none-match,服务器收到请求进行对比,决定返回200还是304
    
    (2)Last-Modifed/If-Modifed-Since
    last-modifed: 浏览器向服务器发送资源的最后修改时间
    if-modifed-since: 当资源过期时,发现响应头具有last-modifed声明,则再次向服务器请求时带上if-modifed-since,表示请求时间。服务器收到请求后发现有if-modified-since则与被请求资源的最后修改时间进行对比(Last-Modified),若最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;若最后修改时间较旧(小),说明资源无新修改,响应HTTP 304 走缓存

8.首屏加载优化方案

1.vue-router懒加载
2.使用cdn加速,将通用的库从vendor中抽离
    cdn原理:cdn全称(content delivery network)内容分发网络,其目的是通过现有的internet中增加一层新的cache(缓存)层,将网站内容发布发布到最接近用户的网络"边缘"的节点,使用户可以就近获取所需的资源,提高用户访问网站的响应速度。从技术层面上讲,决定于,网络宽带的小,用户访问量的大,网络分布不均等原因。
    简单的说就是将你的源站资源缓存到全球的各个cdn节点上,用户获取资源时从就近的就近的节点上获取而不需要每个用户都从源站上获取,避免网络堵塞,缓解源站压力。
3.nginx的gzip
4.vue异步组件
5.服务端渲染ssr
6.按需加载ui库
7.webpack开启gzip压缩
8.若首屏是登录页,可以做成多入口
9.service worker缓存文件处理(静态资源离线缓存)
   "https://blog.csdn.net/screaming_color/article/details/78912396"

9.Event loop

Event loop指的是计算机系统的一个运行机制。javascript就采用这种运行机制用来解决单线程的一些问题。
浏览器: 
    1.javascript执行线程:负责执行js代码
    2.ui线程:负责ui展示
    3.javascript事件循环线程: 
    (ps:ui线程不能和javascript线程同时执行,可能在操作DOM的时候会冲突),
    
    javascript中的线程都是排队执行,不会并列执行,若并列执行的话也可能会在例如操作一些dom的时候导致冲突。
    javascript中的任务分为同步任务和异步任务
        同步任务: 赋值操作,循环操作,求和运算,表单处理分支语句等
        异步任务: dom事件,ajax,dom的一些api
    事件循环机制:javascript的执行引擎的主线程从任务列表中获取任务,若任务是异步任务,则运行到异步任务是会退出主线程,主线程进行下一个任务的获取。若异步任务处理完成则又会插入到任务列表的末尾,等待主线程处理。当遇到同步任务的时候主线程直接就执行了。
node: 
    node会先开启一个event loop,当接收到req的时候会把这个任务,会把它关闭然后进行处理,然后去服务下一个请求。当这个请求完成,他就被放回到队列中,当达到队列开头,就将这个任务结果返回给用户。
    
浏览器和nodejs的 event loop的区别:
    1.浏览器的每个微任务必须在主任务执行完毕之后执行
    2.node的微任务在各阶段之间执行。

10.算法相关

排序
(1)冒泡排序: 比较相邻两项的值,如果第一项大于第二项则交换位置,元素项向上运动就好像气泡往上冒一样。

function bubbleSort(arr) {
    let len = arr.length;
    for(let i = 0; i< len; i++) {
        for(let j = 0; j< len-1-i; j++) {
            if (arr[j] > arr[j+1]) {
                [arr[j+1], arr[j]] = [arr[j], arr[j+1]]
            }
        }
    }
    return arr
}

(2)选择排序: 首先在排序序列中找到最小(大)的元素,存放到序列的起始位置,然后再从剩余的序列中查找最小(大)的元素,存放到已排序序列的末尾。以此类推直至排序完毕。

function selectSort(arr) {
    let len = arr.length;
    let minIndex, temp;
    
    for(let i=0; i< len-1; i++){
        minIndex = i;
        for(let j = i+1; j< len; j++){
            if(arr[j]< arr[minIndex]) {
                minIndex = j;
            }
        }
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr
}

11.网络安全相关(XXS, CSRF)

xxs: 跨站脚本攻击,恶意注入html代码,其他用户访问时会被执行,特点:能注入恶意的脚本(html/javascript)代码到用户浏览的网站上,从未获取Cookie资料窃取,会话劫持,钓鱼等攻击手段。
防止手段: 
    1.网站进行严格的输入格式检测
    2.通过编码转义输出
    3.浏览器禁止页面的js访问带有htmlOnly属性的cookie
csrf: 攻击跨站请求伪造,特点:重要操作的所有参数都是可以被攻击者猜测到的,攻击者预测到url的所有参数和参数值,才能成功的构成一个伪造请求。
防止手段:
    1.token验证机制,请求字段中带一个token,响应请求时校验时效性。
    2.用户操作限制,比如验证码
    3.请求来源限制,比如http referer才能完成操作(防御效果相比较差)

12.webpack优化

打包体积优化:
1.提取第三方库使用cdn引入第三方库
2.代码压缩插件uglifyJsPlugin
3.服务器采用gzip
4.按需加载资源文件 require.ensure
5.优化devtool中的source-map
6.剥离css文件,单独打包
7.去除不必要的插件,通常是开发环境和生产环境用了同一套配置导致的。

打包效率优化:
1.开发环境采用增量构建,启用热更新
2.开发环境不做无意义的工作,比如提取css文件的hash等
3.配置devtool
4.选择合适的loader
5.个别loader开启cache,比如babel-loader
6.第三方库采用引入方式
7.提取公共代码
8.优化构建时的搜索路径,明确需要构建的目录和不需要构建的目录
9.模块化引入需要的部分

13.url相关

url到界面显示发生了什么
1.dns解析(域名解析):本地域名服务器-->根域名服务器-->com顶级域名服务器,一次类推下去。就是先本地缓存查找,再一层一层的查找。将常见的域名地址解析成唯一的ip地址。
2.tcp连接,三次握手,没收到信息就重新发。
    1.主机向服务器发送一个建立连接的请求。
    2.服务器接收到请求后发送同意连接的信号
    3.主机接收到同意连接的信号后,再次向服务器发送确认信号,至此主机与服务器建立了连接。
3.发送http请求 浏览器会解析url,并设置好请求报文发出。请求报文中包括,请求头,请求行,请求体空行。https默认端口403,http默认80。
4.服务器处理请求并返回http报文
5.浏览器解析渲染页面
    1.通过html解析器解析html文档,构建一个dom tree,通过css解析器解析html中存在的css,构建一个style rules,两者结合构成一个呈现树(render tree)
    2.render tree 构建完毕进入布局阶段,将会为每个阶段分配一个出现在屏幕位置上的准确坐标
    3.最后将全部的节点遍历绘制出来,一个页面就展示出来了,当遇到script会暂停下来执行script,所以通常吧script放到底部。
6.结束连接

14.组件封装

封装组件的目的: 为了复用,提高开发效率和代码质量。
组件封装应该注意:低耦合,单一职责,可复用性,可维护性
    1.分析布局
    2.初步开发
    3.化繁为简
    4.抽象组件

15.内存泄露

内存泄露的定义: 程序中已动态分配的堆内存由于某种原因程序未释放或无法释放所引发的各种问题,js中可能会出现内存泄露的情况。
结果会导致程序延迟大,程序崩溃
导致内存泄露的情况:
    1.使用全局变量
    2.dom清空时还在引用
    3.ie中使用闭包
    4.定时器未清理
    5.子元素存在引起的内存泄露
如何避免:
    1.减少不必要得全局变量,及时对无用的数据进行垃圾回收。
    2.注意程序的逻辑,避免死循环
    3.避免创建过多的对象
    4.减少层级过多的引用

16.vue相关

(1) spa的理解

spa(single-page application)仅在页面初始化的时候完成对html,css,js的加载,一旦加载完成,不会再因为用户的操作再去加载或跳转,取而代之利用页面路由来进行页面之间的切换。
    优点:
        1.用户体验好,内容的改变不会重新加载页面,避免了不必要的渲染和跳转。
        2.相对于服务器的压力较小
        3.前后端分离架构清晰,前端负责交互逻辑,后盾负责数据处理
    缺点:
        1.初次加载耗时
        2.前进后退路由管理:由于所有的页面都是在一个页面内显示的,所以浏览器的前进后退按钮不能使用
        3.seo难度大:由于所有的内容都是在一个页面内切换显示的。

(2) 什么是mvvm

mvvm(model-view-viewModel):mvvm是一种软件架构模式源自mvc,它促进了前端与后端的业务逻辑分离,极大的提高的前端的开发效率,它的核心就是viewModel层,负责转换model中的数据对象让数据变得更容易管理和使用。向上与视图层进行双向数据绑定,向下与model逻辑层惊喜接口请求进行数据交互,起到了承上启下的作用。

(3) 双向数据绑定的实现原理

https://github.com/Laofu-zhang/ZVue/blob/master/ZVue.js

(4) v-if、v-show的区别

v-if:真正的条件渲染,因为他确保在切换过程中条件块内的事件监听器和子组件适当的销毁和重建
v-show:不管条件是什么,元素总是会被渲染。并且只是简单的基于css的display属性进行切换。

(5) computed和watch的区别

computed: 计算属性,依赖其他的属性值,并且computed的值有缓存,只有当他依赖的属性值发生了改变,下一次获取到的computed的值时才会被重新计算computed的值。
watch: 更多的是从当一个观察的作用,类似于某些数据的监听回调,每当监听到数据变化是都会执行回调进行后续的操作。

应用场景:
computed:当我们需要进行数值计算,并且依赖于其他的数据时,应该使用computed,因为可以利用computed的缓存特性,避免每次获取值时都需要重新计算。
watch:当我们需要在数据变化时执行异步或者开销较大的操作时,应该使用watch。使用watch允许我们执行异步操作,限制我们的操作频率,并在我们得到最终结果前,设置中间状态,这些都是computed无法做到的。

(6) vue生命周期

1.beforeCreate: 组件实例被创造之初,组件属性生效之前
2.created:组件实例创建完成,属性也绑定了,但是真实的dom还没有生成,$el还不能用。
3.beforeMount: 在挂载开始之前被调用,相关的render函数被首次调用
4.mounted:el被新创建的vm.$el替换,并挂载到实例上去之后调用该钩子
5.beforeUpdate: 组件更新之前被调用,发生在虚拟dom补丁之前
6.updated: 组件更新之后
7.activited: keep-alive专属钩子,组件被激活时调用。
8.deactivited:keep-alive专属钩子,组件被销毁时调用。
9.beforeDistory: 组件被销毁之前。
10.distoryed: 组件被销毁之后。

(7) 那个生命周期异步数据请求最合适

由于created,beforeMount,mounted三个钩子中的data都已经创建,可以将服务端返回的数据进行赋值,所以理论上讲这三个钩子都可以请求数据。
但是created相对来说更好点。
    1.能更快的获得服务端数据,减少页面的loading时间
    2.ssr不支持beforeMount,mounted这两个钩子,所以放在created中有助于一致性。

(8) 在什么时候能够DOM操作

由于在mounted被调用之前,vue已经将编译好的模板挂载到了页面。所以此时可以进行dom操作
若想要在created中进行dom操作则需要Vue.nextTick()的回调中进行操作。

(9) vue组件之间的通信方式

1. 父子组件通信:
    1. props/$emit
    2. ref和$parent/$children(访问父子实例)
2. 兄弟组件的通信:
    EventBus($on,$emit)适用于父子,兄弟,隔代组件通信。 这种方法就是通过一个空的Vue实例作为事件总线,用它来触发($emit)和监听($on)事件从而时间组件之间的通信。
3.隔代组件通信:
    1.$attr/$listeners,通过v-bind="$attr",v-on="$listeners"传入内部组件。
    例子地址:https://juejin.im/post/5cbd29d4f265da03914d608d
    2.provide/inject:祖先组件通过provide来提供变量,子孙组件通过inject来注入变量。
    例子地址: https://juejin.im/post/5c983d575188252d9a2f5bff
4.vuex 适用于兄弟,父子,隔代组件之间的通信。

(10) vue组件中的data为什么是一个函数

因为Object是引用数据类型,如果不用function返回一个object,则每个组件的data引用的都是同一个地址,一个数据改变了,其他的也就都改变了。所以组件内的data必须是一个function返回的object,这样就避免了污染全局。

(11) vue中key的作用

key是为vue中的vnode的唯一标记,通过这个key,我们的diff操作可以更快速,更准确。

(12) nextTick

作用: nextTick接受一个回调函数作为参数,它的作用将回调函数延迟到下一次DOM更新周期之后执行。
用途:在视图更新之后需要对新的视图进行操作。

http

http(超文本传输协议),是用于传输超文本文档应用层协议,他是为了web应用层和web服务器的通信而设计的。遵循经典的客户端-服务端模型,客户端打开一个连接发送请求,然后等待他收到服务端的响应,http是一个无状态协议,这就意味着服务器不会在两个请求之间保存任何的数据(状态)。

作用域和作用域连

1.作用域是在运行时代码中某些特定部分中的变量,函数和对象的可访问性。也就是说,作用域决定了代码块中变量和其他资源的可见性。
作用域是一个独立的地盘,让变量不会外泄,暴露出去。也就是起到了变量隔离的作用,不同作用域中的相同变量不会相互影响。
    1.作用域的分层: 
        内层的作用域可以访问外层作用域的变量,外层作用域不能访问内层作用域的变量。
    2。块级作用域:
        1.可通过let const声明。
        2.在一个函数内部。
        3.在一个代码块内部。
2.作用域链
    1.自由变量:
    当作用域没有定义的变量,这称为自由变量。自由变量的值会从父级作用域中寻找。
    2.作用域链:
    若自由变量在父级作用域中没找到,就还会向父级寻找,一直往上。若找到全局作用域还没找到,则宣布放弃,这层关系链就称为作用域链。

闭包

定义:闭包是指有权访问另一个函数作用域中的变量的函数。从技术的角度讲,所有的javascript函数都是闭包:他们都是对象,它们都关联到作用域链。

也就是某个函数在定义的词法作用域之外被调用,闭包可以使该函数极限访问定义时的词法作用域。

垃圾回收和内存泄露

程序在运行的时候需要分配内存,所谓的内存泄露就是,不再使用的内存,没有的到及时释放,就会造成内存泄露。为了更好的避免内存泄露,我们则就会用到垃圾回收机制。

垃圾回收重要性:由于字符串,对象,数组没有固定的大小,所以只有当他们的大小已知时才能对他们进行内存的分配,只要想这样动态的分配了内存,最终都需要释放掉这些分配的内存才能够再次被利用。否则javascript解释器会消耗掉系统中所有的内存,导致系统崩溃。所以垃圾回收机制尤为重要。

垃圾回收机制
    找出不在使用了的变量,然后释放其占用的内存,但是此过程不是实时的,应为其开销较大,所以垃圾回收机制会按照固定的时间间隔周期执行。
    垃圾回收的方法:
    1.标记清除
    垃圾收集器会在运行时会给存储在内存中的变量都叫上标记。当变量进入执行环境的时候,就标记这个变量为执行环境,理论上执行环境的变量的内存永远不可能被释放,因为随时可能被使用。当变量离开的时候就标记为离开环境,这个时候就能被释放。
    2.引用计数
    所谓引用计数就是保存在内存中的资源被引用的次数,若一个值得引用次数为0,就表示这个值不被用到,即可释放内存。
内存泄露
    造成内存泄露的原因
    1.以外的全局变量
    2.被遗忘的计时器和回调函数
    3.闭包(由于闭包可以维持函数内部的变量,使其得不到释放)
        解决办法:将事件函数定义到外部,解除闭包
    4.未清理DOM元素的引用
    
避免内存泄露的方法:
    1.减少不必要的全局变量或者生命周期较长的对象,及时对其进行垃圾回收
    2.注意程序逻辑,避免死循环
    3.避免创建多个对象
垃圾回收优化:
    1.数组长度及时置位0
    2.对象尽量复用,不用的对象置位null
    3.在循环中的函数表达式尽量放置到循环外边

原型

原型:是ECMAscript实现继承的过程中产生的一个概念。
继承:继承是在一个对象的基础上创建新对象的过程,原型指在这过程中作为基础的对象。
new操作符: 可以用构造函数生成一个实例对象,但是有个缺陷,无法做到属性和方法的共享。
prototype:考虑到构造函数不能共享属性的特点,为了解决此问题出现了prototype这个属性。所有实例对象需要共享的属性和方法都放到这个prototype下面,不需要的则放到构造函数里面。
原型链:原型链是通过Object.create()和.prototype时生成的一个__proto__的一个指针来实现的。
    访问:优先在对象本身查找,没有则顺着原型链向上查找。
    修改:只能修改和删除自身属性,不会影响到原型链上的其他对象。
总结: 由于所有的实例对象共享了一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像继承了prototype对象一样。

new操作符

new操作符的作用:
实现一个new:
1. new操作符会返回一个对象,所以需要在函数内部创建的一个新的对象。
2. 这个对象,也就是构造函数中的this,可以访问到挂载在this上的任意值。
3. 这个对象可以访问到构造函数原型上的属性。所以需要对象与够着函数连接起来
4.返回原始值需要忽略,返回对象需要正常处理。

function create(Con,...args) {
    let obj = {}
    Object.setPrototypeOf(obj, Con.prototype)
    let result = Con.apply(obj, args)
    return result instanceof Object ? result : obj
}

js事件循环

js是一门单线程非阻塞的脚本语言,也就是说只有一个线程来执行任务,非阻塞的意思就是说当代码需要执行异步任务的时候,主线程就会挂起,当异步任务执行完成以后,主线程会根据一定的规则去执行回调。
事实上,当任务完毕时,js将这个事件加入一个队列(事件队列)。被放入这个队列的时间不会立刻执行,而是等待当前执行栈中所有的任务执行完毕后,主线程回去查找事件队列中是否有任务。
任务又分为宏任务和微任务,不同类型的任务会被分配到不同的任务队列中。
执行栈中的所有任务执行完毕之后,主线程会去查找事件回调中的任务,如果存在,则依次执行直至任务为空。然后再去宏任务队列中取出一个事件,把对应的回调加入到当前执行栈。当前执行栈中所有任务执行完毕,检查微任务队列事件是否有任务,无限循环此过程,就称为事件循环。

webpack中loader和plugins的区别

loader: 对模块的源代码进行转换。
plugins: 是用来扩展webpack的功能的,主要通过构建流程里注入钩子实现,plugins是为了完成webpack所不能实现的复杂功能

AMD CMD commonjs三者区别

AMD:amd是requireJs在推广过程中产生的产物。它的规范规则是非同步加载模块,允许指定回调函数。
    标准的api: require([module], callback) define(id, [depends], callback)
    test.js
    define(['package/lib'], function(){
        function foo() {
            lib.log('hello world')
        }
    })
    require([test.js], function(test){
        test.foo()
    })
CMD: CMD是seaJs在推广过程中对模块化定义的规范产出,他是同步模块定义
所有的模块都通过define来定义
    define(function(require, exports, module){
        var $ =require('jquery')
        var test = require('test.js')
        exports.doSomthing = {}
        modules.exports = {}
    })
commonJs规范: 目前nodejs使用此规范,它的核心思想是通过require来同步加载所依赖的其他模块然后通过export和module.exports导出暴露。
ES6: export/import来进行导出导入

数组方法总结

includes:判断数组中是否存在某个元素
forEach:遍历数组,没有返回值且不会改变原数组
map:会返回一个新数组但不会改变原数组,默认返回undefined
find:根据检索条件,查找出第一个满足该条件的值,若找不到返回undefined
findIndex:根据检索条件,查找出满足该条件的值得下标,若找不到返回undefined
filter:过滤数组,返回新数组但不会改变原数组
push,pop:数组末尾的追加和删除
unshift,shift:数组头部添加和删除元素
concat:数组后面拼接一个新元素。该方法不会改变原数组,会返回新拼接好的数组
reverse:数组反转,返回一个新数组,且改变原数组
sort:数组排序
join,split:数组转字符串,字符串转数组
every:判断数组中每一项都满足设定条件,则返回true
some:数组中只要有一项满足设定条件,则返回true
indexOf,lastIndexOf:两个方法都是用来查找索引,接受两个产生,第一个是查找的对象,第二个值是起始位置。找到了返回索引值,没找到返回-1
slice:数组截取接受两个参数,开始位置和结束位置。不改变原数组
splice:从数组中添加和删除数组只,会改变原数组
reduce:arr.reduce((prev, current, index, array) => {}, initialValue),为数组中每一个元素一次执行回调函数,不包括被删除的和为赋值的元素

对象常用方法总结

Object.assign: 用于对象合并,此方法实行浅拷贝。第一个参数时合并的目标对象,后面的参数是合并的对象
Object.create: 使用指定原型去创建一个新的对象
Object.defineProperties:直接在对象上定义新属性或修改属性第一个值是目标对象,第二个值时属性,第三个值是属性描述
Object.keys: 返回一个由自身可枚举属性组成的数组
Object.values: 返回对象自身可枚举属性值
Object.entries: 返回对象自身可枚举属性键值对数组
hasOwnProperty: 判断自身属性中是否有指定属性
Object.getOwnPropertyDescriptor(obj, prop): 返回指定对象上的属性描述
Object.getOwnPropertyDescriptors(obj): 返回一个对象所有自身属性的描述
Object.getOwmPropertyNames: 返回一个对象所有自身属性的属性名,包括可枚举和不可枚举的
Object.getPrototypeOf: 返回指定对象的原型
isPrototypeOf: 判断一个对象是否在另一个对象的原型连上
Object.setPrototypeOf(obj, prototype):设置对象的原型
Object.is: 判断两个对象是否相同
Object.freeze: 冻结一个对象
Object.isFrozen: 判断一个对象是否被冻结

iterator遍历过程:

1.创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历起对象的本质就是一个指针对象
2.第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
不断调用指针对象的next方法直至它指向数据结构的结束位置。

浏览器兼容性的处理

样式方面兼容:
由于各个浏览器厂商的内核不同所以得样式添加前缀
    1. ie trident -ms
    2. fireFox gecko -moz
    3. opera presto -o
    4. chrome和safari webkit -webkit

ajax

let xhr = new XMLHttpRequest();
xhr.onreadystatechange = (callback)=> {
    if (xhr.readyState === 4){
        if ((xhr.state >= '200' && xhr.state <= '300') || xhr.state === 304) {
            callbakc(xhr.reaponseText)
        } else {
            console.error('error')
        }
    }
}
    
xhr.open('get', 'exmaple.json', true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send()

web storage cookie session的区别


上一篇:JavaScript 返回顶部实例
下一篇:HTML 链接