最近碰到個需要自動生成表格的任務,作為前端的我,就想在 node 和瀏覽器中生成強大的表格,所以特此研究了很多關于表格的 npm 庫
支持讀寫 Excel 的 node.js 模塊
node-xlsx: 基于 Node.js 解析 excel 文件數據及生成 excel 文件,僅支持 xlsx 格式文件
js-xlsx: 目前 Github 上 star 數量最多的處理 Excel 的庫,支持解析多種格式表格 XLSX / XLSM / XLSB / XLS / CSV,解析采用純 js 實現,寫入需要依賴 nodejs 或者 FileSaver.js 實現生成寫入 Excel,可以生成子表 Excel,功能強大,但上手難度稍大。不提供基礎設置 Excel 表格 api 例單元格寬度,文檔有些亂,不適合快速上手;普通版本不支持定義字體、顏色、背景色等,有這個功能需要的可以使用 pro 版,是要聯系客服收費的,害我照著 API 設置調試了好多次都失敗。好在樣式設置問題有一些教程,通過研究本人已解決,可設置寬度顏色等等,見根目錄本人修改的 xlsx.js
xlsx-style 基于 xlsx 封裝的樣式庫,可以在 xlsx 的基礎上設置樣式。樣式不全,寬度都設置不了,好多年前作者就不維護了.寬度設置問題本人已解決了,見修改的 xlsx-style.js 文件
exceljs 在使用此庫之前,本人已花費了很大的精力,用以上庫做好了表格,但是發現不能設置頁眉頁腳,添加圖片,打印選項設置等等,直到發現了這個庫,文檔齊全,功能強大,并且還免費.但是star較少,差一點就錯過了。本教程主要針對這個庫
代碼庫地址
https://github.com/lingxiaoyi/excel
安裝
npm install
npm install -g nodemon
調試使用,替代 node 命令,實現保存文件,node 自動重新啟動執行,必須全局安裝才能運行
使用
nodemon app.js
js-xlsx 具體 api 使用方法請參考 main.js demo 使用,app.js 中修改為 require('./src/main.js');
exceljs 具體 api 使用方法請參考 main-exceljs.js demo 使用,app.js 中修改為 require('./src/main-exceljs.js');
因為每次生成完表格,每次都需要打開表格查看樣式,在 windows 電腦中,打開表格之后就鎖定不能生成新文件了,本來想著能導出一個 html 文件對應表格的樣式
node 調試
vscode 中打開調試右側設置編輯,將下方代碼復制進去,點 nodemon 啟動就可以進行 debug 調試了
{
"type": "node",
"request": "launch",
"name": "nodemon",
"runtimeExecutable": "nodemon",
"program": "${workspaceFolder}/app.js",
"restart": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": ["<node_internals>/**"]
},
webpack 目錄的作用
每次生成完新表格,都需要重新打開表格查看樣式,在 windows 電腦中,打開表格之后就鎖定了,再次生成新表格就會報錯,文件已鎖定,不能寫入,對于想偷懶的我,能不能實現像 webpack 熱更新功能那種,修改樣式 js 頁面自動更新呢?
wps 自帶另存 html 文件功能,但是沒有提供生成的 api ,網上也搜索不到對應的轉換功能,
本來以為自己要實現一套表格轉 html 的功能。通過不斷嘗試,偶然間發現手機瀏覽器可以直接打開預覽 xlsx 文件,內心狂喜啊
使用方法
進入 webpack 目錄安裝依賴包,安裝好之后執行
npm run dev
啟動成功之后,會自動打開帶有 ip 地址的預覽地址,此時在電腦瀏覽器會自動下載 xlsx 文件,忽略不管,用手機直接打開此地址,就能看到 xlsx 表格的內容了,并且每次新修改內容和樣式,都會自動刷新頁面顯示新表格.
小技巧
谷歌瀏覽器插件:
生成二維碼的插件生成二維碼方便手機掃描
劃詞翻譯 用來翻譯一些看不懂的英文文檔
browser 目錄
瀏覽器中實現生成 xlsx 表格方法
進入 browser 目錄安裝依賴包,安裝好之后執行
npm run dev
啟動成功之后,拖動根目錄 src 下的李四表格到頁面上的輸入框里,成功生成表格之后會生成一個下載鏈接地址,右鍵在新標簽頁打開鏈接,即會生成一個新的表格文件出來,完整 api 使用和 demo 文件請參考 index.js
vue 和 react 用法可以參考此例子,如果有必要也可以此版本庫的例子
一些概念
在使用這個庫之前,先介紹庫中的一些概念。
workbook 對象,指的是整份 Excel 文檔。我們在使用 js-xlsx 讀取 Excel 文檔之后就會獲得 workbook 對象。
worksheet 對象,指的是 Excel 文檔中的表。我們知道一份 Excel 文檔中可以包含很多張表,而每張表對應的就是 worksheet 對象。
cell 對象,指的就是 worksheet 中的單元格,一個單元格就是一個 cell 對象。
xlsx 使用注意事項
constXLSX = require('xlsx');
let html = XLSX.utils.sheet_to_html(workbook.Sheets.Sheet1)
生成 html 的用法,并且不會有任何樣式
exceljs 使用注意
讀取文件問題
因為 exceljs 讀取文件不支持 sync 同步讀取,給的實例也是 await 例子.導致我讀取完遇到一個問題,就是老是生成不成功,最后發現必須要把所有邏輯全部放入函數中,像下方這樣
(async function (params) {
let res = await workbook.xlsx.readFile(`${__dirname}/趙六.xlsx`);
//執行所有數據處理邏輯
//執行寫的邏輯
workbook.xlsx.writeFile(path.resolve(__dirname, '../webpack/test222.xlsx'));
});
所有邏輯全部要寫入這個函數中,這樣本來是可以的,但是出錯調試幾率較大,并且讀取到的數據龐大還需要額外處理,所以我讀取數據邏輯就用的 node-xlsx,十分簡單方便,如果你用的 exceljs 讀取文件數據出現問題,大概率是異步同步邏輯搞錯了,多加注意即可
寬度設置
列寬不知道是以什么為單位,反正不是像素(已測量),例子中是以厘米為單位再乘以 4.7 的結果設置的,4.7 是不斷測試的結果.
快捷查看列寬的方法,打開 wps 表格,長按列與列字母間的豎線,就能看到列寬,取厘米的單位即可.見下圖
前景色
前景色設置必須右鍵單元格選擇設置單元格格式,然后選擇圖案樣式選擇顏色,就可以前景色填充
worksheet.getCell('A2').fill = { type: 'pattern', pattern:'darkTrellis', fgColor:{argb:'FFFFFF00'}, bgColor:{argb:'FF0000FF'} };
背景色
worksheet.getCell('A2').fill = { type: "pattern", pattern: "solid", fgColor: { argb: next.bgColor }, }
排版不一致的問題
解決 Mac 下編輯 Microsoft Office Word 文檔與 Windows 排版不一致的問題,,不同的系統用 wps 打開相同的表格,打印預覽的時候,表格寬度顯示不一樣
問題詳細說明地址
我的解決辦法就是 mac 下顯示正常,按 mac 下的寬度來設置就可以了
參考資料
exceljs
node-xlsx
js-xlsx
函數節流與函數防抖是我們解決頻繁觸發DOM事件的兩種常用解決方案,但是經常傻傻分不清楚。。。這不,在項目中又用遇到了,在此處記錄一下
函數防抖 debounce
原理:將若干函數調用合成為一次,并在給定時間過去之后,或者連續事件完全觸發完成之后,調用一次(僅僅只會調用一次?。。。。。。。。?!)。
舉個栗子:滾動scroll事件,不停滑動滾輪會連續觸發多次滾動事件,從而調用綁定的回調函數,我們希望當我們停止滾動的時,才觸發一次回調,這時可以使用函數防抖。
原理性代碼及測試:
// 給盒子較大的height,容易看到效果
<style>
* {
padding: 0;
margin: 0;
}
.box {
width: 800px;
height: 1200px;
}
</style>
<body>
<div class="container">
<div class="box" style="background: tomato"></div>
<div class="box" style="background: skyblue"></div>
<div class="box" style="background: red"></div>
<div class="box" style="background: yellow"></div>
</div>
<script>
window.onload = function() {
const decounce = function(fn, delay) {
let timer = null
return function() {
const context = this
let args = arguments
clearTimeout(timer) // 每次調用debounce函數都會將前一次的timer清空,確保只執行一次
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
}
}
let num = 0
function scrollTap() {
num++
console.log(看看num吧 ${num}
)
}
// 此處的觸發時間間隔設置的很小
document.addEventListener('scroll', decounce(scrollTap, 500))
// document.addEventListener('scroll', scrollTap)
}
</script>
</body>
此處的觸發時間間隔設置的很小,如果勻速不間斷的滾動,不斷觸發scroll事件,如果不用debounce處理,可以發現num改變了很多次,用了debounce函數防抖,num在一次上時間的滾動中只改變了一次。
調用debouce使scrollTap防抖之后的結果:
直接調用scrollTap的結果:
補充:瀏覽器在處理setTimeout和setInterval時,有最小時間間隔。
setTimeout的最短時間間隔是4毫秒;
setInterval的最短間隔時間是10毫秒,也就是說,小于10毫秒的時間間隔會被調整到10毫秒。
事實上,未優化時,scroll事件頻繁觸發的時間間隔也是這個最小時間間隔。
也就是說,當我們在debounce函數中的間隔事件設置不恰當(小于這個最小時間間隔),會使debounce無效。
函數節流 throttle
原理:當達到了一定的時間間隔就會執行一次;可以理解為是縮減執行頻率
舉個栗子:還是以scroll滾動事件來說吧,滾動事件是及其消耗瀏覽器性能的,不停觸發。以我在項目中碰到的問題,移動端通過scroll實現分頁,不斷滾動,我們不希望不斷發送請求,只有當達到某個條件,比如,距離手機窗口底部150px才發送一個請求,接下來就是展示新頁面的請求,不停滾動,如此反復;這個時候就得用到函數節流。
原理性代碼及實現
// 函數節流 throttle
// 方法一:定時器實現
const throttle = function(fn,delay) {
let timer = null
return function() {
const context = this
let args = arguments
if(!timer) {
timer = setTimeout(() => {
fn.apply(context,args)
clearTimeout(timer)
},delay)
}
}
}
// 方法二:時間戳
const throttle2 = function(fn, delay) {
let preTime = Date.now()
return function() {
const context = this
let args = arguments
let doTime = Date.now()
if (doTime - preTime >= delay) {
fn.apply(context, args)
preTime = Date.now()
}
}
}
需要注意的是定時器方法實現throttle方法和debounce方法的不同:
在debounce中:在執行setTimeout函數之前總會將timer用setTimeout清除,取消延遲代碼塊,確保只執行一次
在throttle中:只要timer存在就會執行setTimeout,在setTimeout內部每次清空這個timer,但是延遲代碼塊已經執行啦,確保一定頻率執行一次
我們依舊可以在html頁面中進行測試scroll事件,html和css代碼同debounce,此處不贅述,運行結果是(可以說是一場漫長的滾輪滾動了):
最后再來瞅瞅項目中封裝好的debounce和throttle函數,可以說是很優秀了,考慮的特別全面,希望自己以后封裝的函數也能考慮的這么全面吧,加油!
/*
空閑控制 返回函數連續調用時,空閑時間必須大于或等于 wait,func 才會執行
@param {function} func 傳入函數,最后一個參數是額外增加的this對象,.apply(this, args) 這種方式,this無法傳遞進函數
@param {number} wait 表示時間窗口的間隔
@param {boolean} immediate 設置為ture時,調用觸發于開始邊界而不是結束邊界
@return {function} 返回客戶調用函數
/
const debounce = function(func, wait, immediate) {
let timeout, args, context, timestamp, result;
const later = function() {
// 據上一次觸發時間間隔
let last = Number(new Date()) - timestamp;
// 上次被包裝函數被調用時間間隔last小于設定時間間隔wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
// 如果設定為immediate===true,因為開始邊界已經調用過了此處無需調用
if (!immediate) {
result = func.call(context, ...args, context);
if (!timeout) {
context = args = null;
}
}
}
};
return function(..._args) {
context = this;
args = _args;
timestamp = Number(new Date());
const callNow = immediate && !timeout;
// 如果延時不存在,重新設定延時
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) {
result = func.call(context, ...args, context);
context = args = null;
}
return result;
};
};
/*
頻率控制 返回函數連續調用時,func 執行頻率限定為 次 / wait
@param {function} func 傳入函數
@param {number} wait 表示時間窗口的間隔
@param {object} options 如果想忽略開始邊界上的調用,傳入{leading: false}。
如果想忽略結尾邊界上的調用,傳入{trailing: false}
@return {function} 返回客戶調用函數
*/
const throttle = function(func, wait, options) {
let context, args, result;
let timeout = null;
// 上次執行時間點
let previous = 0;
if (!options) options = {};
// 延遲執行函數
let later = function() {
// 若設定了開始邊界不執行選項,上次執行時間始終為0
previous = options.leading === false ? 0 : Number(new Date());
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function(..._args) {
let now = Number(new Date());
// 首次執行時,如果設定了開始邊界不執行選項,將上次執行時間設定為當前時間。
if (!previous && options.leading === false) previous = now;
// 延遲執行時間間隔
let remaining = wait - (now - previous);
context = this;
args = _args;
// 延遲時間間隔remaining小于等于0,表示上次執行至此所間隔時間已經超過一個時間窗口
// remaining大于時間窗口wait,表示客戶端系統時間被調整過
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
//如果延遲執行不存在,且沒有設定結尾邊界不執行選項
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
本文旨在通過一個簡單的例子,練習vuex的幾個常用方法,使初學者以最快的速度跑起來一個vue + vuex的示例。
學習vuex需要你知道vue的一些基礎知識和用法。相信點開本文的同學都具備這個基礎。
另外對vuex已經比較熟悉的大佬可以忽略本文。
基于vue-cli腳手架生成一個vue項目
常用npm命令:
npm i vue-vli -g vue --version vue init webpack 項目名
進入項目目錄,使用npm run dev先試著跑一下。
一般不會出現問題,試跑成功后,就可以寫我們的vuex程序了。
使用vuex首先得安裝vuex,命令:
npm i vuex --save
介紹一下我們的超簡單Demo,一個父組件,一個子組件,父組件有一個數據,子組件有一個數據,想要將這兩個數據都放置到vuex的state中,然后父組件可以修改自己的和子組件的數據。子組件可以修改父組件和自己的數據。
先放效果圖,初始化效果如下:
如果想通過父組件觸發子組件的數據,就點“改變子組件文本”按鈕,點擊后效果如下:
如果想通過子組件修改父組件的數據,就在子組件點擊“修改父組件文本”按鈕,點擊后效果如下:
首先是Parent.vue組件
<template> <div class="parent"> <h3>這里是父組件</h3> <button type="button" @click="clickHandler">修改自己文本</button> <button type="button" @click="clickHandler2">修改子組件文本</button> <div>Test: {{msg}}</div> <child></child> </div> </template> <script> import store from '../vuex' import Child from './Child.vue' export default { computed: {
msg(){ return store.state.testMsg;
}
}, methods:{
clickHandler(){
store.commit('changeTestMsg', '父組件修改自己后的文本')
},
clickHandler2(){
store.commit('changeChildText', '父組件修改子組件后的文本')
}
}, components:{ 'child': Child
},
store,
} </script> <style scoped> .parent{ background-color: #00BBFF; height: 400px;
} </style>
下面是Child.vue子組件
<template> <div class="child"> <h3>這里是子組件</h3> <div>childText: {{msg}}</div> <button type="button" @click="clickHandler">修改父組件文本</button> <button type="button" @click="clickHandler2">修改自己文本</button> </div> </template> <script> import store from '../vuex' export default { name: "Child", computed:{
msg(){ return store.state.childText;
}
}, methods: {
clickHandler(){
store.commit("changeTestMsg", "子組件修改父組件后的文本");
},
clickHandler2(){
store.commit("changeChildText", "子組件修改自己后的文本");
}
},
store
} </script> <style scoped> .child{ background-color: palegreen; border:1px solid black; height:200px; margin:10px;
} </style>
最后是vuex的配置文件
import Vue from 'vue' import Vuex from 'vuex';
Vue.use(Vuex) const state = { testMsg: '原始文本', childText:"子組件原始文本" } const mutations = {
changeTestMsg(state, str){
state.testMsg = str;
},
changeChildText(state, str){
state.childText = str;
}
} const store = new Vuex.Store({ state: state, mutations: mutations
}) export default store;
通過該vuex示例,了解vuex的常用配置及方法調用。希望對不怎么熟悉vuex的同學快速上手vuex項目有點幫助。
因為沒太多東西,我自己也是剛接觸,本例就不往GitHub扔了,如果嘗試了本例,但是沒有跑起來的同學,可以一起交流下。
無論是 pc 端還是移動端,無可避免都會涉及到列表查詢有關的操作,但對于這兩種不同的設備,其列表查詢的最佳處理方式也是完全不同。
對于 pc 端列表查詢來說,前端通常是給與服務端當前需要獲取的數據量(如 pageCount,limit 等參數)以及所需要獲取數據的位置(如 pageSize,offset 等參數)作為查詢條件。然后服務端然后返回數據總數,以及當前數據,前端再結合這些數據顯示頁面總數等信息。這里我稱為相對位置取數。
對于移動端而言,沒有pc 端那么大的空間展示以及操作,所以基本上都會采用下拉取數這種方案。
那么我們在處理移動端列表查詢時候使用這種相對位置取數會有什么問題呢?
通過相對位置取數會具有性能問題,因為一旦使用 offset 信息來獲取數據,隨著頁數的增加,響應速度也會變的越來越慢。因為在數據庫層面,我們每次所獲取的數據都是“從頭開始第幾條”,每次我們都需要從第一條開始計算,計算后舍棄前面的數據,只取最后多條數據返回前端。
當然了,對于相對位置取數來說,數據庫優化是必然的,這里我就不多做贅述了。對于前端開發來說,優秀的的查詢條件設計可以在一定方面解決此問題。
事實上,對于一個實際運行的項目而言,數據更新才是常態,如果數據更新的頻率很高或者你在當前頁停留的時間過久的話,會導致當前獲取的數據出現一定的偏差。
例如:當你在獲取最開始的 20 條數據后,正準備獲取緊接著的后 20 條數據時,在這段時間內 ,發生了數據增加,此時移動端列表就可能會出現重復數據。雖然這個問題在 pc 端也存在,但是 pc 端只會展示當前頁的信息,這樣就避免了該問題所帶來的負面影響。
我們在上面的問題中說明了,移動端下拉加載中使用相對位置查詢取數是有問題的。
那么,如果當前不能迅速結合前后端進行修改 api 的情況下,當服務端傳遞過來的數據與用戶想要得的數據不一致,我們必須在前端進行處理,至少處理數據重復問題所帶來的負面影響。
因為當前分頁請求時無狀態的。在分頁取到數據之后前端可以對取得的數據進行過濾,過濾掉當前頁面已經存在的 key(例如 id 等能夠確定的唯一鍵)。
通過這種處理方式,我們至少可以保證當前用戶看到的數據不會出現重復。同時當列表數據可以編輯修改的時候,也不會出現因為 key 值相同而導致數據錯亂。
如果不使用相對位置獲取數據,前端可以利用當前列表中的最后一條數據作為請求源參數。前端事先記錄最后一條數據的信息。例如當前的排序條件為創建時間,那么記錄最后一條數據的創建時間為主查詢條件(如果列表對應的數據不屬于個人,可能創建時間不能唯一決定當前數據位置,同時還需要添加 ID 等信息作為次要查詢條件)。
當我們使用絕對位置獲取數據時候,雖然我們無法提供類似于從第 1 頁直接跳轉 100 頁的查詢請求,但對于下拉加載這種類型的請求,我們不必擔心性能以及數據重復顯示的問題。
對于相對位置取數來說,前端可以根據返回數據的總數來判斷。但當使用絕對位置取數時,即使獲取數據總數,也無法判斷當前查詢是否存在后續數據。
從服務器端實現的角度來說,當用戶想要得到 20 條數據時候,服務端如果僅僅只向數據庫請求 20 條數據,是無法得知是否有后續數據的。服務端可以嘗試獲取當前請求的數據條數 + 1, 如向數據庫請求 21 條數據,如果成功獲得 21 條數據,則說明至少存在著 1 條后續數據,這時候,我們就可以返回 20 條數據以及具有后續數據的信息。但如果我們請求 21 條數據卻僅僅只能獲取 20 條數據(及以下),則說明沒有后續數據。
如可以通過 “hasMore” 字段來表示是否能夠繼續下拉加載的信息。
{ data: [], hasMore: true }
事實上,前面我們已經解決了移動端處理列表查詢的問題。但是我們做的還不夠好,前端還需要結合排序條件來處理并提供請求參數,這個操作對于前端來說也是一種負擔。那么我們就聊一下 HATEOAS 。
HATEOAS (Hypermedia As The Engine Of Application State, 超媒體即應用狀態引起) 這個概念最早出現在 Roy Fielding 的論文中。REST 設計級別如下所示:
HATEOAS 會在 API 返回的數據中添加下一步要執行的行為,要獲取的數據等 URI 的鏈接信息??蛻舳酥灰@取這些信息以及行為鏈接,就可以根據這些信息進行接下來的操作。
對于當前的請求來說,服務端可以直接返回下一頁的信息,如
{ data: [], hasMore: true, nextPageParams: {}
}
服務端如此傳遞數據,前端就不需要對其進行多余的請求處理,如果當前沒有修改之前的查詢以及排序條件,則只需要直接返回 “nextPageParams” 作為下一頁的查詢條件即可。
這樣做的好處不但符合 REST LEVEL 3,同時也減輕了前端的心智模型。前端無需配置下一頁請求參數。只需要在最開始查詢的時候提供查詢條件即可。
當然,如果前端已經實現了所有排序添加以及查詢條件由服務端提供,前端僅僅提供組件,那么該方案更能體現優勢。 前端是不需要知道當前業務究竟需要什么查詢條件,自然也不需要根據查詢條件來組織下一頁的條件。同時,該方案的輸入和輸出都由后端提供,當涉及到業務替換( 查詢條件,排序條件修改)時候,前端無需任何修改便可以直接替換和使用。
一旦涉及到移動端請求,不可避免的會有網絡問題,當用戶在火車或者偏遠地區時候,一旦下拉就會涉及取數,但是當前數據沒有返回之前,用戶多次下拉可能會有多次取數請求,雖然前端可以結合 key 使得渲染不出錯,但是還是會在緩慢的網絡下請求多次,無疑雪上加霜。這時候我們需要增加條件變量 loading。
偽代碼如下所示:
// 查詢 function search(cond) {
loading = true api.then(res => {
loading = false }).catch(err => {
loading = false })
} // 獲取下一頁數據 function queryNextPage() { if (!nextPageParams) return if (!loading) return search(nextPageParams)
}
為了降低開發的復雜度,以后端為出發點,比如:Struts、SpringMVC 等框架的使用,就是后端的 MVC 時代;
以 SpringMVC 流程為例:
優點:
前后端職責很清晰: 前端工作在瀏覽器端,后端工作在服務端。清晰的分工,可以讓開發并行,測 試數據的模擬不難,前端可以本地開發。后端則可以專注于業務邏輯的處理,輸出 RESTful等接 口。
前端開發的復雜度可控: 前端代碼很重,但合理的分層,讓前端代碼能各司其職。這一塊蠻有意思 的,簡單如模板特性的選擇,就有很多很多講究。并非越強大越好,限制什么,留下哪些自由,代 碼應該如何組織,所有這一切設計,得花一本書的厚度去說明。
-部署相對獨立: 可以快速改進產品體驗
缺點:
代碼不能復用。比如后端依舊需要對數據做各種校驗,校驗邏輯無法復用瀏覽器端的代碼。如果可 以復用,那么后端的數據校驗可以相對簡單化。
全異步,對 SEO 不利。往往還需要服務端做同步渲染的降級方案。 性能并非最佳,特別是移動互聯網環境下。
SPA 不能滿足所有需求,依舊存在大量多頁面應用。URL Design 需要后端配合,前端無法完全掌控。
NodeJS 帶來的全棧時代
前端為主的 MV* 模式解決了很多很多問題,但如上所述,依舊存在不少不足之處。隨著 NodeJS 的興 起,JavaScript 開始有能力運行在服務端。這意味著可以有一種新的研發模式:
————————————————
版權聲明:本文為CSDN博主「叁有三分之一」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/iME_cho/article/details/105654633
塊級元素(inline):
塊級元素可以包含行內元素和其它塊級元素,且占據父元素的整個空間,可以設置 width 和 height 屬性,瀏覽器通常會在塊級元素前后另起一個新行。
常見塊級元素:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> div{ width: 150px; height: 150px; background-color: cadetblue; } </style> </head> <body> <div>塊級元素1</div> <div>塊級元素2</div> </body> </html>
分析:
塊級元素的高和寬可以被修改,而且塊級元素會在一個塊級元素之后另起一行。
行級元素
行級元素(block):
一般情況下,行內元素只能包含內容或者其它行內元素,寬度和長度依據內容而定,不可以設置,可以和其它元素和平共處于一行。
常見行級元素:
a,b,strong,span,img,label,button,input,select,textarea
特點:
和相鄰的行內元素在一行上
高度和寬度無效,但是水平方向上的padding和margin可以設置,垂直方向上的無效
默認的寬度就是它本身的寬度
行內元素只能容納純文本或者是其他的行內元素(a標簽除外)
例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> span{ width: 150px; height: 150px; font-size: 40px; background-color: cadetblue; } </style> </head> <body> <span>行級元素1</span> <span>行級元素2</span> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> span{ width: 150px; height: 150px; font-size: 20px; background-color: cadetblue; display: inline-block; } </style> </head> <body> <span>以前我是行級元素,</span> <span>現在我只想做行內塊級元素。</span> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> div{ width: 150px; height: 150px; font-size: 30px; background-color: cadetblue; display: inline; } </style> </head> <body> <div>我以前是塊級元素,</div> <div>現在我是行級元素!</div> </body> </html>
分析:
在VSC中,修改寬高的代碼已經出現了波浪線,證明他是錯誤的,所以現在的div
已經變成了行級元素。
————————————————
版權聲明:本文為CSDN博主「董小宇」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/lolly1023/article/details/105715892
其實VSCode編輯器本身自帶了一個功能(Interactive Editor Playground :可以讓你快速了解VSCode的特性,并且是可以交互的),
但很可惜它的內容是全英文的(將VSCode設置為中文也沒用哦~),
我將每一部分截圖下來,并為你說明關鍵內容,教你學會使用 Interactive Editor Playground
還有一些顯而易見的特性,我不會再用文字敘述一遍(它們都是潛移默化的)
在下文中會涉及到大量快捷鍵的介紹,如果看不懂快捷鍵請自行百度
鼠標 = 文本光標 = 光標
本文成于2020年4月22日,隨著VSCode的版本更迭,此部分內容可能會略有差異(小更改不影響觀看,有較大影響的更新請在評論區告之,我會及時更新的)
打開VSCode > Help > Interactive Playground
你將會打開 Interactive Editor Playground 頁面
VS代碼中的核心編輯器包含許多特性。此頁高亮顯示了10個特性,每個特性介紹中都提供了代碼行供你編輯
接下來的10行內容(你可以理解為目錄,對應10個特性)
多光標編輯(Multi-Cursor Editing)- 選擇一塊區域,選擇所有匹配項,添加其余光標等
智能感應(intelliSense)- 獲取代碼和外部模塊的代碼幫助和參數建議
行操作(Line Actions )- 快速移動行以重新排序代碼
重命名重構(Rename Refactoring)- 快速重命名代碼庫中的符號(比如變量名、函數名)
格式化(Formatting)- 使用內置文檔和選擇格式使代碼看起來很棒
代碼折疊(Code Folding) - 通過折疊其他代碼區域,關注代碼中最相關的部分
錯誤和警告(Errors and Warnings)- 寫代碼時請參閱錯誤和警告
片段(Snippets)- 花更少的時間輸入片段
Emmet - 只需要敲一行代碼就能生成你想要的完整HTML結構等(極大方便前端開發)
JavaScript Type Checking- 使用零配置的TypeScript對JavaScript文件執行類型檢查。
Multi-Cursor Editing
使用多光標編輯可以同時編輯文檔的多個部分,極大地提高了工作效率
框式選擇
鍵盤同時按下 Shift + DownArrow(下鍵)、Shift + RightArrow(右鍵)、Shift + UpArrow(上鍵)、Shift + LeftArrow(左鍵) 的任意組合可選擇文本塊
也可以用鼠標選擇文本時按 Shift + Alt 鍵
或使用鼠標中鍵拖動選擇(可用性很高)
添加光標
按 Ctrl + Alt + UpArrow 在行上方添加新光標
或按 Ctrl + Alt + DownArrow 在行下方添加新光標
您也可以使用鼠標和 Alt + Click 在任何地方添加光標(可用性很高)
在所有出現的字符串上創建光標
選擇字符串的一個實例,例如我用鼠標選中所有background,然后按 Ctrl + Shift + L,文本中所有的background都將被選中(可用性很高)
IntelliSense
Visual Studio Code 預裝了強大的JavaScript和TypeScript智能感知。
在代碼示例中,將文本光標放在錯誤下劃線的上面,會自動調用IntelliSense
這只是智能提示的冰山一角,還有懸停在函數名上可以看到參數及其注釋(如果有)等等,它會潛移默化的帶給你極大幫助
其他語言在安裝對應插件后,會附帶對應語言的IntelliSense
Line Actions
分別使用 Shift + Alt + DownArrow 或 Shift + Alt + UpArrow 復制光標所在行并將其插入當前光標位置的上方或下方
分別使用 Alt + UpArrow 和 Alt + DownArrow 向上或向下移動選定行(可用性很高)
用 Ctrl + Shift + K 刪除整行(可用性很高)
通過按 Ctrl + / 來注釋掉光標所在行、切換注釋(可用性很高)
Rename Refactoring
重命名符號(如函數名或變量名)
重命名操作將在項目中的所有文件中發生(可用性很高)
代碼如果沒有良好的編寫格式,閱讀起來是一個折磨
Formatting可以解決編寫格式問題:無論你的代碼的格式寫的有多么糟糕,它可以將代碼格式化為閱讀性良好的格式
格式化整個文檔 Shift + Alt + F (可用性很高)
格式化當前行 Ctrl + K Ctrl + F(即先按Ctrl,再按K,最后按F)
鼠標右鍵 > Format Document (格式化整個文檔)
將格式化操作設置為自動化(保存時自動格式化整個文檔):Ctrl + , 輸入 editor.formatOnSave
鼠標操作,自己嘗試一下,秒懂
快捷鍵:
折疊代碼段是基于基于縮進
錯誤和警告將在你出現錯誤時,高亮該代碼行
在代碼示例中可以看到許多語法錯誤(如果沒有,請你隨便修改它,讓它出現錯誤)
按F8鍵可以按順序在錯誤之間導航,并查看詳細的錯誤消息(可用性很高)
Snippets
通過使用代碼片段,可以大大加快編輯速度
在代碼編輯區,你可以嘗試輸入try并從建議列表中選擇try catch,
然后按Tab鍵或者Enter,創建try->catch塊
你的光標將放在文本error上,以便編輯。如果存在多個參數,請按Tab鍵跳轉到該參數。
Emmet
Emmet將代碼片段的概念提升到了一個全新的層次(前端開發的大寶貝)
你可以鍵入類似Css的可動態解析表達式,并根據在abrevision中鍵入的內容生成輸出
比如說:
然后Enter
————————————————
版權聲明:本文為CSDN博主「索兒呀」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/Zhangguohao666/article/details/105676173
隨著項目體積的增加,參與到項目中的同學越來越多,每個人都有自己的打 git log 的習慣:
add: 添加...
[add]: 添加...
Add 添加...
為了形成統一的規范,達成共識,從而降低協作開發成本,需要對 git commit 記錄進行規范。
規范 git commit 記錄,需要做兩件事情:
問:既然已經交互式生成了規范記錄,為什么需要在 hooks 進行檢查?
交互式生成 commit 記錄,需要用戶調用自定義的 npm scripts,例如npm run commit
。但還是可以直接調用原生 git 命令 git commit
來提交記錄。而檢查是在正式提交前進行的,因此不符合要求的記錄不會生效,需要重新 commit。
前期調研結果,關于 commit 提示有兩種做法:
方法 1 的優缺點:
優點 1: 直接安裝對應的 adapter 即可
優點 2: 無開發成本
缺點 1: 無法定制,不一定滿足團隊需要
方法 2 的優缺點:
優點 1: 可定制,滿足開發需求
優點 2: 單獨成庫,發布 tnpm,作為技術建設
缺點 1: 需要單獨一個倉庫(但開發成本不高)
在實際工作中,發現方法 1 中的常用規范,足夠覆蓋團隊日常開發場景。所以,選擇了方法 1.
step1: 安裝 npm 包
npm i --save-dev commitizen cz-conventional-changelog @commitlint/cli @commitlint/config-conventional husky
添加 package.json 的配置:
"scripts": { "commit": "git-cz" }, "husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" }
}, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" }
}
在項目根目錄下創建commitlint.config.js
:
module.exports = { extends: ["@commitlint/config-conventional"]
};
使用方法:不再使用git commit -m ...
,而是調用npm run commit
。
<img src="https://tva1.sinaimg.cn/large/006tNbRwly1gbjcfr3xb5j30cw00tjrd.jpg" style="width: 100% !important;"/>
微信小程序的wxss
、阿里旗下淘寶、支付寶小程序的acss
等等語法很類似原生css,但是在web開發里用慣了動態css語言,再寫回原生css很不習慣,尤其是父子樣式的嵌套寫法非常繁瑣。
因此,我希望能有一個自動化構建方案,能夠簡單地將scss轉換成小程序的樣式語言。
以前寫微信小程序的依賴庫時用過,使用gulp編譯,將源碼和編譯后的代碼分別放到src和dist兩個目錄。gulp會處理src下面的所有文件,將其中的scss轉換成css,并將其他所有文件原封不動挪到dist下相應位置。
這里就不詳細說了,代碼參考Wux。
非常簡單直接,使用Webstorm
/IDEA
的File Watchers
功能實時轉換。
確保命令行輸入sass -v
能出現版本號,安裝過程略。
File Watchers
到插件市場上搜索并安裝(已安裝則跳過)
現在安裝完插件打開項目會自動彈出scss轉css的向導,方便了很多。但還需要做一些修改,配置如下:
首先要將生成文件的后綴名改掉,比如這里我的淘寶小程序就得是acss
。
其次,將Arguments改為:
$FileName$:$FileNameWithoutExtension$.acss --no-cache --sourcemap=none --default-encoding utf-8 --style expanded
如果不加--no-cache
,scss文件同目錄下會出現一個.sass-cache
目錄。
如果不加--sourcemap=none
, scss文件同目錄下會出現一個.map
文件。
如果不加--default-encoding utf-8
, scss文件如果有中文注釋轉換就會報錯。
style
可不加,這里用的是無縮進和壓縮的風格,反正小程序打包發布時還會壓,這里保持可讀性。
現在這個scss轉換是單獨作用于項目的,如果新建一個小程序項目,就需要重新添加(不建議設置成global,容易誤傷)。
注意到File Watchers
列表的右側操作欄下方有導入導出按鈕,可以將現在配好的設置導出保存,將來新建項目時只要導入一下就行了。
之后還有一個問題,如果我手動將編譯后的css(即wxss
或者acss
,下略)文件刪除,scss文件不改動的話,就不會重新編譯出css文件。
或者萬一監聽失效或者不夠及時,css還有可能是舊的。
所以還需要一個命令,用來將整個目錄下的scss文件統一轉換,確保沒有遺漏和保持代碼。
不過我看了半天sass
和sass-convert
的文檔,沒有找到一個可用的寫法,能讓命令行遍歷指定目錄下的所有scss文件,將其轉換成css放到源文件所在目錄,并且將后綴名改為wxss
或者acss
。
所以遍歷這個行為只能交給nodejs來實現,代碼如下:
創建編譯腳本build/scss-convert.js
:
var path = require("path") var fs = require("fs") const { exec } = require('child_process') const basePath = path.resolve(__dirname, '../') function mapDir(dir, callback, finish) {
fs.readdir(dir, function(err, files) { if (err) { console.error(err) return }
files.forEach((filename, index) => { let pathname = path.join(dir, filename)
fs.stat(pathname, (err, stats) => { // 讀取文件信息 if (err) { console.log('獲取文件stats失敗') return } if (stats.isDirectory()) {
mapDir(pathname, callback, finish)
} else if (stats.isFile()) { if (!['.scss'].includes(path.extname(pathname))) { return }
callback(pathname)
}
}) if (index === files.length - 1) {
finish && finish()
}
})
})
}
mapDir(
basePath, function (file) { const newFileWithoutExt = path.basename(file, '.scss') if (newFileWithoutExt.startsWith('_')) { return // 按照scss規則,下劃線開頭的文件不會生成css } // exec可以讓nodejs執行外部命令 exec(`sass --no-cache --sourcemap=none --default-encoding utf-8 --style expanded ${file}:${newFileWithoutExt}.acss`, { cwd: path.dirname(file) // 不寫這個會導致生成的文件出現在根目錄 }, (err, stdout, stderr) => { if (err) { console.log(err) return } console.log(`stdout: ${stdout}`)
})
}, function() { // console.log('xxx文件目錄遍歷完了') }
)
在package.json
里添加一條script:
"scripts": { "scss": "node build/scss-convert",
},
ES6 允許按照一定模式,從數組和對象中提取值,對變量進行賦值,這被稱為解構(Destructuring)
本質上,這種寫法屬于“模式匹配”,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值
如果解構不成功,變量的值就等于undefined
解構賦值的規則是,只要等號右邊的值不是對象或數組,就先將其轉為對象。由于undefined和null無法轉為對象,所以對它們進行解構賦值,都會報錯、
交換變量的值
例如:let x=1,y=2;[x,y] = [y,x]
從函數返回多個值
函數只能返回一個值,如果要返回多個值,只能將它們放在數組或對象里返回。有了解構賦值,取出這些值就非常方便
函數參數的定義
解構賦值可以方便地將一組參數與變量名對應起來
提取 JSON 數據,很多接口數據只需要其中某部分
例如aa.axios.get(res=>{let {data:result}=res;}),則res.data.result = result了
函數參數的默認值
指定參數的默認值,就避免了在函數體內部再寫var foo = config.foo || ‘default foo’;這樣的語句
遍歷 Map 結構
Map 結構原生支持 Iterator 接口,配合變量的解構賦值,獲取鍵名和鍵值就非常方便
輸入模塊的指定方法
加載模塊時,往往需要指定輸入哪些方法。解構賦值使得輸入語句非常清晰。* const { SourceMapConsumer, SourceNode } = require(“source-map”);
左右兩側數據解構須得吻合,或者等號左邊的模式,只匹配一部分的等號右邊的數組(屬于不完全解構)
特殊情況使用…擴展運算符,無值是空數組
左右兩邊等式的性質要相同,等號的右邊不是數組(或者嚴格地說,不是可遍歷的結構),那么將會報錯,只要某種數據結構具有 Iterator
接口,都可以采用數組形式的解構賦值,例如Set結構
解構賦值允許指定默認值,當一個數組成員嚴格等于undefined,默認值才會生效,否則取賦值的值;如果默認值是一個表達式,那么這個表達式是惰性求值的,即只有在用到的時候,才會求值;默認值可以引用解構賦值的其他變量,但該變量必須已經聲明
// 數組的解構賦值 let [a,b] = [1,2]; console.log([a,b],a);//[1, 2] 1 let [aa] = [11,22]; console.log(aa)//11 let [aaa,bbb] = [111]; console.log(aaa,bbb)//111 undefined let [head, ...tail] = [1, 2, 3, 4]; console.log(head,tail)//1,[2,3,4] let [x, y, ...z] = ['a']; console.log(x,y,z)//a undefined [] // 等號右邊不是數組會報錯 // let [ab] = 121; // conosle.log(ab)//TypeError: 121 is not iterable // let [abc] = {} // conosle.log(abc)//TypeError: {} is not iterable // 默認值賦值 let [zz = 1] = [undefined]; console.log(zz)//1 let [zzz = 1] = [null]; console.log(zzz)//null let [foo = true] = []; console.log(foo)// true let [xxx, yyy = 'b'] = ['a']; console.log(xxx,yyy)//a,b let [xxxx, yyyy = 'b'] = ['a', undefined]; console.log(xxxx,yyyy)//a,b function f() { console.log('aaa'); } let [xx = f()] = [1]; console.log(xx)//1 let [qq=ww,ww=11] = [23,44]; console.log(qq,ww)//23,44,因為ww申明比qq晚所以是undefined;
2、對象的解構賦值
對象的解構賦值的內部機制,是先找到同名屬性,然后再賦給對應的變量。真正被賦值的是后者,而不是前者
數組是按照位置區分,對象則是按照鍵名區分的,同樣的解構失敗則為undefine
可將已有方法對象解構賦值
嵌套賦值,注意是變量是否被賦值是模式還是鍵值
對象的解構賦值可以取到繼承的屬性
如果要將一個已經聲明的變量用于解構賦值,必須非常小心
let xx; // {xx} = {xx: 1}這樣會報錯,
解構賦值允許等號左邊的模式之中,不放置任何變量名。因此,可以寫出非常古怪的賦值表達式
({} = [true, false]);//可執行
由于數組本質是特殊的對象,因此可以對數組進行對象屬性的解構
objFuc(){ // 對象解構賦值 let {b,a} = {a:1} console.log(a,b)//1 undefined // 已有對象解構賦值 let { sin, cos } = Math;//將Math對象的對數、正弦、余弦三個方法,賦值到對應的變量上 console.log(sin);//log() { [native code] } const { log } = console; log('hello') // hello // let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; console.log(baz);//aaa // 嵌套賦值 let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p,p:[x, { y }] } = obj; console.log(x,y,p)//Hello World p: ['Hello',{ y: 'World' }] //繼承賦值 const obj1 = {}; const obj2 = { foo: 'bar' }; Object.setPrototypeOf(obj1, obj2);//obj1繼承obj2 const { foo } = obj1; console.log(foo) // "bar" // 默認值 // 錯誤的寫法 let xx; // {xx} = {xx: 1};// SyntaxError: syntax error,Uncaught SyntaxError: Unexpected token '=' ({xx} = {xx: 1});//正確寫法 console.log(xx) // 古怪的,等式左邊可為空 // ({} = [true, false]); // 對象可解構數組 let arr = [1, 2, 3]; let {0 : first, [arr.length - 1] : last} = arr; console.log(first,last)//1 3 },
strFuc(){ // str:'yan_yan' let [a,b,c,d,e,f,g] = this.str; console.log(a,b,c,d,e,f,g)//y a n _ y a n // 對數組屬性解構賦值 let {length} = this.str; console.log(length)//7 },
4、數值和布爾值的解構賦值
let {toString: s} = 123; console.log(s === Number.prototype.toString,s)//true ? toString() { [native code] } let {toString: ss} = true; console.log(ss === Boolean.prototype.toString,ss)// true ? toString() { [native code] } // 右側必須是數組或對象,undefined和null無法轉為對象,所以對它們進行解構賦值,都會報錯 // let { prop: x } = undefined; // TypeError // let { prop: y } = null; // TypeError
5、函數參數的解構賦值
// 函數的解構賦值可使用默認值,注意默認值是指實參的默認值而不是形參的默認值 function move({x=1, y=1}={}) { return [x, y]; } function move1({x, y} = { x: 0, y: 0 }) { return [x, y]; } function move2({x, y=1} = { x: 0, y: 0 }) { return [x, y]; } console.log(move({x: 3, y: 8})); // [3, 8] console.log(move({x: 3})); // [3, 1] console.log(move({})); // [1, 1] console.log(move()); // [1,1] console.log(move1({x: 3, y: 8})); // [3, 8] console.log(move1({x: 3})); // [3, 1] console.log(move1({})); // [undefined, 1] console.log(move1()); // [0,0] console.log(move2({x: 3, y: 8})); // [3, 8] console.log(move2({x: 3})); // [3, 1] console.log(move2({})); // [undefined, 1] console.log(move2()); // [0,0]
6、圓括號問題 解構賦值雖然很方便,但是解析起來并不容易。對于編譯器來說,一個式子到底是模式,還是表達式,沒有辦法從一開始就知道,必須解析到(或解析不到)等號才能知道。 由此帶來的問題是,如果模式中出現圓括號怎么處理。ES6 的規則是,只要有可能導致解構的歧義,就不得使用圓括號。 可以使用圓括號的情況只有一種:賦值語句的非模式部分,可以使用圓括號 總結: 不管是哪一類的解構賦值,等式右邊的數據必須是對象形式(數組也是一種對象形式) ———————————————— 版權聲明:本文為CSDN博主「Yan_an_n」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。 原文鏈接:https://blog.csdn.net/weixin_44258964/article/details/105643553
藍藍設計的小編 http://www.syprn.cn