想必你一定使用過易企秀或百度H5等微場景生成工具制作過炫酷的h5頁面,除了感嘆其神奇之處有沒有想過其實現方式呢?本文從零開始實現一個H5編輯器項目完整設計思路和主要實現步驟,并開源前后端代碼。有需要的小伙伴可以按照該教程從零實現自己的H5編輯器。(實現起來并不復雜,該教程只是提供思路,并非最佳實踐)
Github: https://github.com/huangwei9527/quark-h5
演示地址:http://47.104.247.183:4000/
演示帳號密碼均admin
編輯器預覽:
前端: vue: 模塊化開發(fā)少不了angular,react,vue三選一,這里選擇了vue。 vuex: 狀態(tài)管理 sass: css預編譯器。 element-ui:不造輪子,有現成的優(yōu)秀的vue組件庫當然要用起來。沒有的自己再封裝一些就可以了。 loadsh:工具類
服務端: koa:后端語言采用nodejs,koa文檔和學習資料也比較多,express原班人馬打造,這個正合適。 mongodb:一個基于分布式文件存儲的數據庫,比較靈活。
1、了解vue技術棧開發(fā) 2、了解koa 3、了解mongodb
基于vue-cli3環(huán)境搭建
···
·
|-- client // 原 src 目錄,改成 client 用作前端項目目錄
|-- server // 新增 server 用于服務端項目目錄
|-- engine-template // 新增 engine-template 用于頁面模板庫目錄
|-- docs // 新增 docs 預留編寫項目文檔目錄
·
···
復制代碼
這樣我們搭建起來一個簡易的項目目錄結構。
|-- client --------前端項目界面代碼
|--common --------前端界面對應靜態(tài)資源
|--components --------組件
|--config --------配置文件
|--eventBus --------eventBus
|--filter --------過濾器
|--mixins --------混入
|--pages --------頁面
|--router --------路由配置
|--store --------vuex狀態(tài)管理
|--service --------axios封裝
|--App.vue --------App
|--main.js --------入口文件
|--permission.js --------權限控制
|-- server --------服務器端項目代碼
|--confog --------數據庫鏈接相關
|--middleware --------中間件
|--models --------Schema和Model
|--routes --------路由
|--views --------ejs頁面模板
|--public --------靜態(tài)資源
|--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 // 是否是表單控件,用于表單提交時獲取表單數據
}
復制代碼
用戶在左側組件區(qū)域選擇組件添加到頁面上,編輯區(qū)域通過動態(tài)組件特性渲染出每個元素組件。
最后,點擊保存將頁面數據提交到數據庫。至于數據怎么轉成靜態(tài) HTML方法有很多。還有頁面數據我們全部都有,我們可以做頁面的預渲染,骨架屏,ssr,編譯時優(yōu)化等等。而且我們也可以對產出的活動頁做數據分析~有很多想象的空間。
編輯器核心代碼,基于 Vue 動態(tài)組件特性實現:
為大家附上 Vue 官方文檔:cn.vuejs.org/v2/api/#is
編輯畫板只需要循環(huán)遍歷pages[i].elements數組,將里面的元素組件JSON數據取出,通過動態(tài)組件渲染出各個組件,支持拖拽改變位置尺寸.
在client目錄新建plugins來管理組件庫。也可以將該組件庫發(fā)到npm上工程中通過npm管理
編寫組件,考慮的是組件庫,所以我們竟可能讓我們的組件支持全局引入和按需引入,如果全局引入,那么所有的組件需要要注冊到Vue component 上,并導出:
client/plugins下新建index.js入口文件
```
/**
* 組件庫入口
* */
import Text from './text'
// 所有組件列表
const components = [
Text
]
// 定義 install 方法,接收 Vue 作為參數
const install = function (Vue) {
// 判斷是否安裝,安裝過就不繼續(xù)往下執(zhí)行
if (install.installed) return
install.installed = true
// 遍歷注冊所有組件
components.map(component => Vue.component(component.name, component))
}
// 檢測到 Vue 才執(zhí)行,畢竟我們是基于 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>
復制代碼
按照這個組件開發(fā)方式我們可以擴展任意多的組件,來豐富組件庫
需要注意的是這里的組件最外層寬高都要求是100%
Quark-h5編輯器左側選擇組件區(qū)域可以通過一個配置文件定義可選組件 新建一個ele-config.js配置文件:
export default [
{
title: '基礎組件',
components: [
{
elName: 'qk-text', // 組件名,與組件庫名稱一致
title: '文字',
icon: 'iconfont iconwenben',
// 給每個組件配置默認顯示樣式
defaultStyle: {
height: 40
}
}
]
},
{
title: '表單組件',
components: []
},
{
title: '功能組件',
components: []
},
{
title: '業(yè)務組件',
components: []
}
]
復制代碼
公共方法中提供一個function 通過組件名和默認樣式獲取元素組件JSON,getElementConfigJson(elName, defaultStyle)方法
公共樣式屬性編輯比較簡單就是對元素JSON對象commonStyles字段進行編輯操作
1.為組件的每一個prop屬性開發(fā)一個屬性編輯組件. 例如:QkText組件需要text屬性,新增一個attr-qk-text組件來操作該屬性 2.獲取組件prop對象 3.遍歷prop對象key, 通過key判斷顯示哪些屬性編輯組件
動畫效果引入Animate.css動畫庫。元素組件動畫,可以支持多個動畫。數據存在元素JSON對象animations數組里。
監(jiān)聽mouseover和mouseleave,當鼠標移入時將動畫className添加入到元素上,鼠標移出時去掉動畫lassName。這樣就實現了hover預覽動畫
組件編輯時支持動畫預覽和單個動畫預覽。
封裝一個動畫執(zhí)行方法
/**
* 動畫方法, 將動畫css加入到元素上,返回promise提供執(zhí)行后續(xù)操作(將動畫重置)
* @param $el 當前被執(zhí)行動畫的元素
* @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`
// 如果是循環(huán)播放就將循環(huán)次數置為1,這樣有效避免編輯時因為預覽循環(huán)播放組件播放動畫無法觸發(fā)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 如果是編輯模式的化動畫只執(zhí)行一次,不然無法監(jiān)聽到動畫結束animationend事件
執(zhí)行動畫前先將元素樣式style緩存起來,當動畫執(zhí)行完再將原樣式賦值給元素
let cssText = this.$el.style.cssText;
runAnimations(this.$el, animations, true, () => {
this.$el.style.cssText = cssText
})
復制代碼
提供事件mixins混入到組件,每個事件方法返回promise,元素被點擊時按順序執(zhí)行事件方法
參考百度H5,將腳本以script標簽形式嵌入。頁面加載后執(zhí)行。 這里也可以考慮mixins方式混入到頁面或者組件,可根據業(yè)務需求自行擴展,都是可以實現的。
將psd每個設計圖中的每個圖層導出成圖片保存到靜態(tài)資源服務器中,
服務端安裝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
};
})
復制代碼
最后把獲取的數據轉義并返回給前端,前端獲取到數據后使用系統統一方法,遍歷添加統一圖片組件
這里只需要注意下圖片跨域問題,官方提供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"
執(zhí)行npm run lib:h5-swiper 生成引擎模板js如圖
ejs中引入模板
<script src="/third-libs/swiper.min.js"></script>
使用組件
<engine-h5-swiper :pageData="pageData" />
工程目錄上文已給出,也可以使用 koa-generator 腳手架工具生成
app.js
//配置ejs-template 模板引擎
render(app, {
root: path.join(__dirname, 'views'),
layout: false,
viewExt: 'html',
cache: false,
debug: false
});
復制代碼
因為html2canvas需要圖片允許跨域,所以在靜態(tài)資源服務中所有資源請求設置'Access-Control-Allow-Origin':'*'
app.js
//配置靜態(tài)web
app.use(koaStatic(__dirname + '/public'), { gzip: true, setHeaders: function(res){
res.header( 'Access-Control-Allow-Origin', '*')
}});
復制代碼
app.js
const fs = require('fs')
fs.readdirSync('./routes').forEach(route=> {
let api = require(`./routes/${route}`)
app.use(api.routes(), api.allowedMethods())
})
復制代碼
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
}
})
}
復制代碼
當接口發(fā)布到線上,前端通過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中統一管理
const mongoConf = require('./config/mongo');
mongoConf.connect();
復制代碼
... 服務端具體接口實現就不詳細介紹了,就是對頁面的增刪改查,和用戶的登錄注冊難度不大
npm run dev-client
復制代碼
npm run dev-server
復制代碼
注意: 如果沒有生成過引擎模板js文件的,需要先編輯引擎模板,否則預覽頁面加載頁面引擎.js 404報錯
npm run lib:h5-swiper
有些頻繁的操作會導致頁面性能和用戶體驗度低,例如: 輸入框搜索會頻繁調端口接口,方法縮小等
(1)防抖-debounce當持續(xù)觸發(fā)事件時,一定時間內沒有再觸發(fā)事件,事件處理函數才會執(zhí)行一次,若設定時間到來之前又一次觸發(fā)事件,就重新開始延時。
const debounce = (fn, delay) => {
let timer = null;
return (...agrs)=> {
clearTimeout(timer)
timer = setTimeout(()=> {
fn.apply(this,args)
},delay)
}
}
(2) 節(jié)流-thottle當持續(xù)觸發(fā)事件時,保證一段時間內只調用一次時間處理函數
const thottle = (fn, delay=500) => {
let flag =true;
return (...arg) => {
if(!flag) return;
flag = false;
setTimeout(()=> {
fn.apply(this, args);
flag = true;
},delay)
}
}
**jsonp跨域的關鍵就在于**:服務端需要在返回的數據外包裹一個在客戶端定義好的回調函數,這樣就在script發(fā)送請求之后就能獲取到數據。
jsonp的缺點
1.只能get請求,不支持post,put,delete
2.不安全 xss攻擊
//通過JQuery Ajax 發(fā)起jsonp請求
(注:不是必須通過jq發(fā)起請求 ,
例如: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>
涉及場景: vue生命周期create()鉤子在進行DOM操作時一定要放在Vue.nextTick()回調函數中。
原因是,在create()鉤子執(zhí)行的時候DOM其實并沒有進行任何渲染,而此時進行DOM操作是徒勞無工的,所以此時一定需要吧js代碼放進Vue.nextTick()的回調函數中。因為該鉤子執(zhí)行的時候所有的DOM都渲染完畢,此時在這個鉤子的回調中進行任何的渲染都是可以的。
Vue官方解釋:Vue在更新DOM的時候是異步執(zhí)行的,只要監(jiān)聽到數據的變化,Vue將開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數據變更,如果同一個watcher被觸發(fā)多次,只會被推入隊列中一次,這種緩沖時去除重復數據對于避免不必要的計算和DOM操作是必不可少的,然后再下一個時間""tick"中,Vue刷新隊列并執(zhí)行實際(去重后)工作,Vue在內部嘗試使用原生的Promise.then, MutationObserver和setTimediate,如果執(zhí)行環(huán)境不支持,則會采用setTimeout(fn,0)代替。
1. !importent
2. 內聯樣式(1000)
3. id選擇器(100)
4. class選擇器、屬性選擇器、偽類選擇器(10)
5. 元素選擇器、關系選擇器、偽元素選擇器 (1)
6. 通配符(0)
BFC全稱塊級格式化上下文(Block Formatting Context),BFC提供了一個獨立的上下文環(huán)境。個環(huán)境中的元素不會影響到其他環(huán)境中的布局,比如浮動元素會形成BFC浮動元素內部子元素主要受浮動元素的影響,兩個浮動元素相互不影響。可以說BFC是一個獨立的容器,這個容器內的布局絲毫不受容器外布局的影響。
觸發(fā)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的約束規(guī)則
1.內部盒會在垂直方向一個一個的排列(可以看做bfc內部有一個常規(guī)流)
2.處于同一個bfc中的元素相互影響,可能會出現邊距重疊
3.每個元素margin box左邊,與容器塊border box左邊相接觸(對于從左往右的格式化,否則相反),即使浮動也是如此。
4.bfc就是容器上的一個獨立容器,容器內的子元素不會影響到容器外的元素,反之亦然。
5.計算bfc高度是,考慮容器內包含的所有元素,包括浮動元素。
6.浮動盒不會疊加到bfc容器上
BFC可以解決的問題
1.垂直外邊距重疊問題
2.去除浮動
3.自適應兩列布局
盒模型包括了,內容區(qū)域,內填充區(qū)域,邊框區(qū)域,外邊距區(qū)域
實現左邊寬度固定,右邊自適應布局:
<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
}
緩存分為協商緩存,強緩存。強緩存不過服務器,協商緩存需要經過服務器,協商緩存返回的狀態(tài)碼是304。兩種緩存機制可以同時存在,強緩存的優(yōu)先級要高于協商緩存的。
1.強緩存: 瀏覽器不會向服務器發(fā)送任何請求,直接從本地緩存數據中讀取數據并返回狀態(tài)碼200。
header參數:
1.Expires:過期時間,若設置了過期時間,則瀏覽器會直接在設置的時間內讀取緩存,不在請求。
2.Cache-Control: 當值設置為max-age=300,則代表這個請求正確返回的時間5分鐘再次加載資源,則命中強緩存。
cache-control設置的值:
max-age: 用來設置資源可以被緩存的時間。
s-maxage:和max-age是一樣額,只是他只針對代理服務器緩存而言。
public:指示響應可以被任何緩存區(qū)緩存
private: 只針對個人用戶,不會被代理服務器緩存
no-cache: 強制客戶端直接向服務器發(fā)送請求,也就是每次請求都必須向服務器發(fā)送,服務器接收到請求,會判斷資源是否變更,是則返回變更的資源,否則返回304,未變更。
no-store: 禁止一切緩存
2.協商緩存:向服務器發(fā)送請求,服務器會根據這個請求的request header的一些參數加判斷是否命中協商緩存,若命中,則返回304狀態(tài)碼并帶上新的response header通知瀏覽器從緩存中讀取資源。
(1)Etag/if-none-match
etag: 它是由服務器返回給前端的,用來幫助服務器控制web端的緩存驗證
if-none-match: 當資源過期時,瀏覽器發(fā)現響應頭里面有etag,則再次向服務器請求時帶上if-none-match,服務器收到請求進行對比,決定返回200還是304
(2)Last-Modifed/If-Modifed-Since
last-modifed: 瀏覽器向服務器發(fā)送資源的最后修改時間
if-modifed-since: 當資源過期時,發(fā)現響應頭具有l(wèi)ast-modifed聲明,則再次向服務器請求時帶上if-modifed-since,表示請求時間。服務器收到請求后發(fā)現有if-modified-since則與被請求資源的最后修改時間進行對比(Last-Modified),若最后修改時間較新(大),說明資源又被改過,則返回最新資源,HTTP 200 OK;若最后修改時間較舊(小),說明資源無新修改,響應HTTP 304 走緩存
1.vue-router懶加載
2.使用cdn加速,將通用的庫從vendor中抽離
cdn原理:cdn全稱(content delivery network)內容分發(fā)網絡,其目的是通過現有的internet中增加一層新的cache(緩存)層,將網站內容發(fā)布發(fā)布到最接近用戶的網絡"邊緣"的節(jié)點,使用戶可以就近獲取所需的資源,提高用戶訪問網站的響應速度。從技術層面上講,決定于,網絡寬帶的小,用戶訪問量的大,網絡分布不均等原因。
簡單的說就是將你的源站資源緩存到全球的各個cdn節(jié)點上,用戶獲取資源時從就近的就近的節(jié)點上獲取而不需要每個用戶都從源站上獲取,避免網絡堵塞,緩解源站壓力。
3.nginx的gzip
4.vue異步組件
5.服務端渲染ssr
6.按需加載ui庫
7.webpack開啟gzip壓縮
8.若首屏是登錄頁,可以做成多入口
9.service worker緩存文件處理(靜態(tài)資源離線緩存)
"https://blog.csdn.net/screaming_color/article/details/78912396"
Event loop指的是計算機系統的一個運行機制。javascript就采用這種運行機制用來解決單線程的一些問題。
瀏覽器:
1.javascript執(zhí)行線程:負責執(zhí)行js代碼
2.ui線程:負責ui展示
3.javascript事件循環(huán)線程:
(ps:ui線程不能和javascript線程同時執(zhí)行,可能在操作DOM的時候會沖突),
javascript中的線程都是排隊執(zhí)行,不會并列執(zhí)行,若并列執(zhí)行的話也可能會在例如操作一些dom的時候導致沖突。
javascript中的任務分為同步任務和異步任務
同步任務: 賦值操作,循環(huán)操作,求和運算,表單處理分支語句等
異步任務: dom事件,ajax,dom的一些api
事件循環(huán)機制:javascript的執(zhí)行引擎的主線程從任務列表中獲取任務,若任務是異步任務,則運行到異步任務是會退出主線程,主線程進行下一個任務的獲取。若異步任務處理完成則又會插入到任務列表的末尾,等待主線程處理。當遇到同步任務的時候主線程直接就執(zhí)行了。
node:
node會先開啟一個event loop,當接收到req的時候會把這個任務,會把它關閉然后進行處理,然后去服務下一個請求。當這個請求完成,他就被放回到隊列中,當達到隊列開頭,就將這個任務結果返回給用戶。
瀏覽器和nodejs的 event loop的區(qū)別:
1.瀏覽器的每個微任務必須在主任務執(zhí)行完畢之后執(zhí)行
2.node的微任務在各階段之間執(zhí)行。
排序
(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
}
xxs: 跨站腳本攻擊,惡意注入html代碼,其他用戶訪問時會被執(zhí)行,特點:能注入惡意的腳本(html/javascript)代碼到用戶瀏覽的網站上,從未獲取Cookie資料竊取,會話劫持,釣魚等攻擊手段。
防止手段:
1.網站進行嚴格的輸入格式檢測
2.通過編碼轉義輸出
3.瀏覽器禁止頁面的js訪問帶有htmlOnly屬性的cookie
csrf: 攻擊跨站請求偽造,特點:重要操作的所有參數都是可以被攻擊者猜測到的,攻擊者預測到url的所有參數和參數值,才能成功的構成一個偽造請求。
防止手段:
1.token驗證機制,請求字段中帶一個token,響應請求時校驗時效性。
2.用戶操作限制,比如驗證碼
3.請求來源限制,比如http referer才能完成操作(防御效果相比較差)
打包體積優(yōu)化:
1.提取第三方庫使用cdn引入第三方庫
2.代碼壓縮插件uglifyJsPlugin
3.服務器采用gzip
4.按需加載資源文件 require.ensure
5.優(yōu)化devtool中的source-map
6.剝離css文件,單獨打包
7.去除不必要的插件,通常是開發(fā)環(huán)境和生產環(huán)境用了同一套配置導致的。
打包效率優(yōu)化:
1.開發(fā)環(huán)境采用增量構建,啟用熱更新
2.開發(fā)環(huán)境不做無意義的工作,比如提取css文件的hash等
3.配置devtool
4.選擇合適的loader
5.個別loader開啟cache,比如babel-loader
6.第三方庫采用引入方式
7.提取公共代碼
8.優(yōu)化構建時的搜索路徑,明確需要構建的目錄和不需要構建的目錄
9.模塊化引入需要的部分
url到界面顯示發(fā)生了什么
1.dns解析(域名解析):本地域名服務器-->根域名服務器-->com頂級域名服務器,一次類推下去。就是先本地緩存查找,再一層一層的查找。將常見的域名地址解析成唯一的ip地址。
2.tcp連接,三次握手,沒收到信息就重新發(fā)。
1.主機向服務器發(fā)送一個建立連接的請求。
2.服務器接收到請求后發(fā)送同意連接的信號
3.主機接收到同意連接的信號后,再次向服務器發(fā)送確認信號,至此主機與服務器建立了連接。
3.發(fā)送http請求 瀏覽器會解析url,并設置好請求報文發(fā)出。請求報文中包括,請求頭,請求行,請求體空行。https默認端口403,http默認80。
4.服務器處理請求并返回http報文
5.瀏覽器解析渲染頁面
1.通過html解析器解析html文檔,構建一個dom tree,通過css解析器解析html中存在的css,構建一個style rules,兩者結合構成一個呈現樹(render tree)
2.render tree 構建完畢進入布局階段,將會為每個階段分配一個出現在屏幕位置上的準確坐標
3.最后將全部的節(jié)點遍歷繪制出來,一個頁面就展示出來了,當遇到script會暫停下來執(zhí)行script,所以通常吧script放到底部。
6.結束連接
封裝組件的目的: 為了復用,提高開發(fā)效率和代碼質量。
組件封裝應該注意:低耦合,單一職責,可復用性,可維護性
1.分析布局
2.初步開發(fā)
3.化繁為簡
4.抽象組件
內存泄露的定義: 程序中已動態(tài)分配的堆內存由于某種原因程序未釋放或無法釋放所引發(fā)的各種問題,js中可能會出現內存泄露的情況。
結果會導致程序延遲大,程序崩潰
導致內存泄露的情況:
1.使用全局變量
2.dom清空時還在引用
3.ie中使用閉包
4.定時器未清理
5.子元素存在引起的內存泄露
如何避免:
1.減少不必要得全局變量,及時對無用的數據進行垃圾回收。
2.注意程序的邏輯,避免死循環(huán)
3.避免創(chuàng)建過多的對象
4.減少層級過多的引用
spa(single-page application)僅在頁面初始化的時候完成對html,css,js的加載,一旦加載完成,不會再因為用戶的操作再去加載或跳轉,取而代之利用頁面路由來進行頁面之間的切換。
優(yōu)點:
1.用戶體驗好,內容的改變不會重新加載頁面,避免了不必要的渲染和跳轉。
2.相對于服務器的壓力較小
3.前后端分離架構清晰,前端負責交互邏輯,后盾負責數據處理
缺點:
1.初次加載耗時
2.前進后退路由管理:由于所有的頁面都是在一個頁面內顯示的,所以瀏覽器的前進后退按鈕不能使用
3.seo難度大:由于所有的內容都是在一個頁面內切換顯示的。
mvvm(model-view-viewModel):mvvm是一種軟件架構模式源自mvc,它促進了前端與后端的業(yè)務邏輯分離,極大的提高的前端的開發(fā)效率,它的核心就是viewModel層,負責轉換model中的數據對象讓數據變得更容易管理和使用。向上與視圖層進行雙向數據綁定,向下與model邏輯層驚喜接口請求進行數據交互,起到了承上啟下的作用。
https://github.com/Laofu-zhang/ZVue/blob/master/ZVue.js
v-if:真正的條件渲染,因為他確保在切換過程中條件塊內的事件監(jiān)聽器和子組件適當的銷毀和重建
v-show:不管條件是什么,元素總是會被渲染。并且只是簡單的基于css的display屬性進行切換。
computed: 計算屬性,依賴其他的屬性值,并且computed的值有緩存,只有當他依賴的屬性值發(fā)生了改變,下一次獲取到的computed的值時才會被重新計算computed的值。
watch: 更多的是從當一個觀察的作用,類似于某些數據的監(jiān)聽回調,每當監(jiān)聽到數據變化是都會執(zhí)行回調進行后續(xù)的操作。
應用場景:
computed:當我們需要進行數值計算,并且依賴于其他的數據時,應該使用computed,因為可以利用computed的緩存特性,避免每次獲取值時都需要重新計算。
watch:當我們需要在數據變化時執(zhí)行異步或者開銷較大的操作時,應該使用watch。使用watch允許我們執(zhí)行異步操作,限制我們的操作頻率,并在我們得到最終結果前,設置中間狀態(tài),這些都是computed無法做到的。
1.beforeCreate: 組件實例被創(chuàng)造之初,組件屬性生效之前
2.created:組件實例創(chuàng)建完成,屬性也綁定了,但是真實的dom還沒有生成,$el還不能用。
3.beforeMount: 在掛載開始之前被調用,相關的render函數被首次調用
4.mounted:el被新創(chuàng)建的vm.$el替換,并掛載到實例上去之后調用該鉤子
5.beforeUpdate: 組件更新之前被調用,發(fā)生在虛擬dom補丁之前
6.updated: 組件更新之后
7.activited: keep-alive專屬鉤子,組件被激活時調用。
8.deactivited:keep-alive專屬鉤子,組件被銷毀時調用。
9.beforeDistory: 組件被銷毀之前。
10.distoryed: 組件被銷毀之后。
由于created,beforeMount,mounted三個鉤子中的data都已經創(chuàng)建,可以將服務端返回的數據進行賦值,所以理論上講這三個鉤子都可以請求數據。
但是created相對來說更好點。
1.能更快的獲得服務端數據,減少頁面的loading時間
2.ssr不支持beforeMount,mounted這兩個鉤子,所以放在created中有助于一致性。
由于在mounted被調用之前,vue已經將編譯好的模板掛載到了頁面。所以此時可以進行dom操作
若想要在created中進行dom操作則需要Vue.nextTick()的回調中進行操作。
1. 父子組件通信:
1. props/$emit
2. ref和$parent/$children(訪問父子實例)
2. 兄弟組件的通信:
EventBus($on,$emit)適用于父子,兄弟,隔代組件通信。 這種方法就是通過一個空的Vue實例作為事件總線,用它來觸發(fā)($emit)和監(jiān)聽($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 適用于兄弟,父子,隔代組件之間的通信。
因為Object是引用數據類型,如果不用function返回一個object,則每個組件的data引用的都是同一個地址,一個數據改變了,其他的也就都改變了。所以組件內的data必須是一個function返回的object,這樣就避免了污染全局。
key是為vue中的vnode的唯一標記,通過這個key,我們的diff操作可以更快速,更準確。
作用: nextTick接受一個回調函數作為參數,它的作用將回調函數延遲到下一次DOM更新周期之后執(zhí)行。
用途:在視圖更新之后需要對新的視圖進行操作。
http(超文本傳輸協議),是用于傳輸超文本文檔應用層協議,他是為了web應用層和web服務器的通信而設計的。遵循經典的客戶端-服務端模型,客戶端打開一個連接發(fā)送請求,然后等待他收到服務端的響應,http是一個無狀態(tài)協議,這就意味著服務器不會在兩個請求之間保存任何的數據(狀態(tài))。
1.作用域是在運行時代碼中某些特定部分中的變量,函數和對象的可訪問性。也就是說,作用域決定了代碼塊中變量和其他資源的可見性。
作用域是一個獨立的地盤,讓變量不會外泄,暴露出去。也就是起到了變量隔離的作用,不同作用域中的相同變量不會相互影響。
1.作用域的分層:
內層的作用域可以訪問外層作用域的變量,外層作用域不能訪問內層作用域的變量。
2。塊級作用域:
1.可通過let const聲明。
2.在一個函數內部。
3.在一個代碼塊內部。
2.作用域鏈
1.自由變量:
當作用域沒有定義的變量,這稱為自由變量。自由變量的值會從父級作用域中尋找。
2.作用域鏈:
若自由變量在父級作用域中沒找到,就還會向父級尋找,一直往上。若找到全局作用域還沒找到,則宣布放棄,這層關系鏈就稱為作用域鏈。
定義:閉包是指有權訪問另一個函數作用域中的變量的函數。從技術的角度講,所有的javascript函數都是閉包:他們都是對象,它們都關聯到作用域鏈。
也就是某個函數在定義的詞法作用域之外被調用,閉包可以使該函數極限訪問定義時的詞法作用域。
程序在運行的時候需要分配內存,所謂的內存泄露就是,不再使用的內存,沒有的到及時釋放,就會造成內存泄露。為了更好的避免內存泄露,我們則就會用到垃圾回收機制。
垃圾回收重要性:由于字符串,對象,數組沒有固定的大小,所以只有當他們的大小已知時才能對他們進行內存的分配,只要想這樣動態(tài)的分配了內存,最終都需要釋放掉這些分配的內存才能夠再次被利用。否則javascript解釋器會消耗掉系統中所有的內存,導致系統崩潰。所以垃圾回收機制尤為重要。
垃圾回收機制
找出不在使用了的變量,然后釋放其占用的內存,但是此過程不是實時的,應為其開銷較大,所以垃圾回收機制會按照固定的時間間隔周期執(zhí)行。
垃圾回收的方法:
1.標記清除
垃圾收集器會在運行時會給存儲在內存中的變量都叫上標記。當變量進入執(zhí)行環(huán)境的時候,就標記這個變量為執(zhí)行環(huán)境,理論上執(zhí)行環(huán)境的變量的內存永遠不可能被釋放,因為隨時可能被使用。當變量離開的時候就標記為離開環(huán)境,這個時候就能被釋放。
2.引用計數
所謂引用計數就是保存在內存中的資源被引用的次數,若一個值得引用次數為0,就表示這個值不被用到,即可釋放內存。
內存泄露
造成內存泄露的原因
1.以外的全局變量
2.被遺忘的計時器和回調函數
3.閉包(由于閉包可以維持函數內部的變量,使其得不到釋放)
解決辦法:將事件函數定義到外部,解除閉包
4.未清理DOM元素的引用
避免內存泄露的方法:
1.減少不必要的全局變量或者生命周期較長的對象,及時對其進行垃圾回收
2.注意程序邏輯,避免死循環(huán)
3.避免創(chuàng)建多個對象
垃圾回收優(yōu)化:
1.數組長度及時置位0
2.對象盡量復用,不用的對象置位null
3.在循環(huán)中的函數表達式盡量放置到循環(huán)外邊
原型:是ECMAscript實現繼承的過程中產生的一個概念。
繼承:繼承是在一個對象的基礎上創(chuàng)建新對象的過程,原型指在這過程中作為基礎的對象。
new操作符: 可以用構造函數生成一個實例對象,但是有個缺陷,無法做到屬性和方法的共享。
prototype:考慮到構造函數不能共享屬性的特點,為了解決此問題出現了prototype這個屬性。所有實例對象需要共享的屬性和方法都放到這個prototype下面,不需要的則放到構造函數里面。
原型鏈:原型鏈是通過Object.create()和.prototype時生成的一個__proto__的一個指針來實現的。
訪問:優(yōu)先在對象本身查找,沒有則順著原型鏈向上查找。
修改:只能修改和刪除自身屬性,不會影響到原型鏈上的其他對象。
總結: 由于所有的實例對象共享了一個prototype對象,那么從外界看起來,prototype對象就好像是實例對象的原型,而實例對象則好像繼承了prototype對象一樣。
new操作符的作用:
實現一個new:
1. new操作符會返回一個對象,所以需要在函數內部創(chuàng)建的一個新的對象。
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是一門單線程非阻塞的腳本語言,也就是說只有一個線程來執(zhí)行任務,非阻塞的意思就是說當代碼需要執(zhí)行異步任務的時候,主線程就會掛起,當異步任務執(zhí)行完成以后,主線程會根據一定的規(guī)則去執(zhí)行回調。
事實上,當任務完畢時,js將這個事件加入一個隊列(事件隊列)。被放入這個隊列的時間不會立刻執(zhí)行,而是等待當前執(zhí)行棧中所有的任務執(zhí)行完畢后,主線程回去查找事件隊列中是否有任務。
任務又分為宏任務和微任務,不同類型的任務會被分配到不同的任務隊列中。
執(zhí)行棧中的所有任務執(zhí)行完畢之后,主線程會去查找事件回調中的任務,如果存在,則依次執(zhí)行直至任務為空。然后再去宏任務隊列中取出一個事件,把對應的回調加入到當前執(zhí)行棧。當前執(zhí)行棧中所有任務執(zhí)行完畢,檢查微任務隊列事件是否有任務,無限循環(huán)此過程,就稱為事件循環(huán)。
loader: 對模塊的源代碼進行轉換。
plugins: 是用來擴展webpack的功能的,主要通過構建流程里注入鉤子實現,plugins是為了完成webpack所不能實現的復雜功能
AMD:amd是requireJs在推廣過程中產生的產物。它的規(guī)范規(guī)則是非同步加載模塊,允許指定回調函數。
標準的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在推廣過程中對模塊化定義的規(guī)范產出,他是同步模塊定義
所有的模塊都通過define來定義
define(function(require, exports, module){
var $ =require('jquery')
var test = require('test.js')
exports.doSomthing = {}
modules.exports = {}
})
commonJs規(guī)范: 目前nodejs使用此規(guī)范,它的核心思想是通過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),為數組中每一個元素一次執(zhí)行回調函數,不包括被刪除的和為賦值的元素
Object.assign: 用于對象合并,此方法實行淺拷貝。第一個參數時合并的目標對象,后面的參數是合并的對象
Object.create: 使用指定原型去創(chuàng)建一個新的對象
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: 判斷一個對象是否被凍結
1.創(chuàng)建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷起對象的本質就是一個指針對象
2.第一次調用指針對象的next方法,可以將指針指向數據結構的第一個成員
不斷調用指針對象的next方法直至它指向數據結構的結束位置。
樣式方面兼容:
由于各個瀏覽器廠商的內核不同所以得樣式添加前綴
1. ie trident -ms
2. fireFox gecko -moz
3. opera presto -o
4. chrome和safari webkit -webkit
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()
*請認真填寫需求信息,我們會在24小時內與您取得聯系。