本文檔主要包括以下是三個部分
-
vue 框架構建及開發規范
-
webpack 打包優化
-
前端錯誤日志收集
規范目的:
-
統一編碼風格,規范,提高團隊協作效率
-
在團隊協作中輸出可讀性強,易維護,風格一致的代碼
-
本文檔主要針對 vue 框架制定的開發規范,其余前端基本規范可參考 http://wiki.ym/pages/viewpage.action?pageId=7867375
1.1.1、腳手架構建
// 1.安裝 vue-cli 腳手架構建工具 npm install --global vue-cli // 2.構建於 webpack 模板的一個新項目,填寫相關項目信息 vue init webpack my-project // 3.安裝項目依賴 npm install |
按如下文件目錄搭建項目框架
src 主要源碼目錄 |-- assets 靜態資源,統一管理 |-- components 公用組件,全局組件 |-- javascript JS相關操作處理 |-- ajax axios封裝的請求攔截 |-- filters 全局過濾器 |-- utils 全局封裝的工具類 |-- datas 模擬數據,臨時存放 |-- router 路由,統一管理 |-- store vuex, 統一管理 |-- views 視圖目錄 |-- order 視圖模塊名 |-- |-- orderList.vue 模塊入口頁面 |-- |-- orderDetail.vue 模塊入口頁面 |-- |-- components 模塊通用組件文件夾 |
-
PC 端:依次推薦使用 antDesign, iView
-
移動端:依次推薦使用 vant,vux
1.1.4、css 預處理器
推薦使用 less,scss , 可在 common.css 設置全局樣式,如
-
常用樣式設置原子類名
-
主題顏色, UI 設計規范等樣式
-
全局組件公共樣式
.colorTheme { color: #40a9ff !important; } .fl { float: left; } .fr { float: right; } .clearfix:after { clear: both; content: ''; display: block; width: 0; height: 0; visibility: hidden; } |
-
以藍湖 750px 設計稿。
-
使用 rem 適配,代碼書寫 px,用 px2rem-loader 將 px 轉化為 rem。
const px2remLoader = { loader: 'px2rem-loader', options: { remUnit: 100 //1rem=多少像素 這里的設計稿是750px。 } } |
在 Vue 項目中使用路由,相信大家都已經很熟悉怎么使用了,要新增一個頁面的話,需要到路由配置中配置該頁面的信息。
但是如果頁面越來越多的話,那么如何讓我們的路由更簡潔呢?
1.2.1、根據不同的業務模塊進行路由拆分,在每個子模塊導出一個路由配置數組,如 userCard.js 導出會員卡模塊的路由,order.js 導出訂單模塊的路由
const routes = [ { path: '/userCardList', component: function(resolve) { require(['@/view/userCard/userCardList'], resolve) } }, { path: '/userCardEdit', component: function(resolve) { require(['@/view/userCard/userCardEdit'], resolve) } } ] |
1.2.2、在路由根目錄在 index.js 中導入所有子模塊
import Vue from 'vue' import Router from 'vue-router' import userCard from '.userCard' import order from './order' let routes = [...userCard, ...order] Vue.use(Router) export default new Router({ mode: 'hash', routes: routes }) |
1.3.1、設置請求攔截和響應攔截
const PRODUCT_URL = 'https://test-o2o-store-all.iauto360.cn' const MOCK_URL = 'http://39.104.49.240:19090' let http = axios.create({ baseURL: process.env.NODE_ENV === 'production' ? PRODUCT_URL : MOCK_URL }) // 請求攔截器 http.interceptors.request.use( config => { // 設置token,Content-Type var token = sessionStorage.getItem('UserLoginToken') config.headers['token'] = token config.headers['Content-Type'] = 'application/json;charset=UTF-8' // 請求顯示loading效果 if (config.loading === true) { vm.$loading.show() } return config }, error => { vm.$loading.hide() return Promise.reject(error) } ) // 響應攔截器 http.interceptors.response.use( res => { vm.$loading.hide() // token失效,重新登錄 if (res.data.code === 401) { // 重新登錄 } return res }, error => { vm.$loading.hide() return Promise.reject(error) } ) |
1.3.2、封裝 get 和 post 請求方法
function get(url, data, lodaing) { return new Promise((resolve, reject) => { http.get(url) .then( response => { resolve(response) }, err => { reject(err) } ) .catch(error => { reject(error) }) }) } function post(url, data, loading) { return new Promise((resolve, reject) => { http.post(url, data, { loading: loading }) .then( response => { resolve(response) }, err => { reject(err) } ) .catch(error => { reject(error) }) }) } export { get, post } |
1.3.3、把 get,post 方法掛載到 vue 實例上。
// main.js import { get, post } from './js/ajax' Vue.prototype.$http = { get, post } |
1.4.1 添加方法到 vue 實例的原型鏈上
export default { install (Vue, options) { Vue.prototype.util = { method1(val) { ... }, method2 (val) { ... }, } } |
1.4.2 在 main.js 通過 vue.use()注冊
import utils from './js/utils' Vue.use(utils) |
——讓團隊當中其他人看你的代碼能一目了然
-
文件夾和文件命名以業務或者模塊名字為主,駝峰式命名。
-
組件命名遵循以下原則,使用駝峰命名(
carLib
)進行組件聲明,使用短橫線分隔命名(<car-lib></car-lib>
)進行使用。 -
當項目中需要自定義比較多的基礎組件的時候,比如一些 button,input,icon,建議以一個統一的前綴如 Base 開頭,這樣做的目的是為了方便查找。
-
method 方法命名使用駝峰式,動詞+名詞,如 getData, submitForm
-
變量命遵循語義化原則,使用駝峰式。
1.6.1、 vue 風格推薦
Prop 定義應該盡量詳細。
// bad props: ['status'] // good props: { status: String } // better props: { status: { type: String, required: true, validator: function (value) { return ['syncing','synced','version-conflict','error'].indexOf(value) !== -1 } } } |
使用 v-for 必須加上 key 值
<!-- bad --> <ul> <li v-for="todo in todos">{{ todo.text }}</li> </ul> <!-- good --> <ul> <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li> </ul> |
不要把 v-if 和 v-for 同時用在同一個元素上。
<!-- bad --> <ul> <li v-for="user in users" v-if="shouldShowUsers" :key="user.id">{{ user.name }}</li> </ul> <!-- good --> <ul v-if="shouldShowUsers"> <li v-for="user in users" :key="user.id">{{ user.name }}</li> </ul> |
組件的 data 必須是一個函數
// bad Vue.component('some-comp', { data: { foo: 'bar' } }) // good Vue.component('some-comp', { data: function() { return { foo: 'bar' } } }) |
組件模板應該只包含簡單的表達式,復雜的表達式則應該重構為計算屬性或方法。
// bad {{ fullName.split(' ').map(function (word) { return word[0].toUpperCase() + word.slice(1) }).join(' ') }} // good // 在模板中 {{ normalizedFullName }} // 復雜表達式已經移入一個計算屬性 computed: { normalizedFullName: function () { return this.fullName.split(' ').map(function (word) { return word[0].toUpperCase() + word.slice(1) }).join(' ') } } |
指令縮寫
<!-- bad --> <input v-bind:value="newTodoText" :placeholder="newTodoInstructions" v-on:input="onInput" /> <!-- good --> <input :value="newTodoText" :placeholder="newTodoInstructions" @input="onInput" /> |
為組件樣式設置作用域
/* bad */ <style> .btn-close { background-color: red; } </style> /* good */ <style scoped> .button-close { background-color: red; } </style> |
若要改變第三方組件庫的樣式,需要加上頂級作用域。
/* bad */ .ivu-input { width: 254px !important; } /* good */ .customerForm .ivu-input { width: 254px !important; } /* .customerForm為當前組件的頂級dom */ |
組件結構遵循從上往下 template,script,style 的結構。
<template> <div></div> </template> <script> export default {} </script> <style lang="scss" scoped></style> |
script 部分各方法成員遵循以下順序放置。
- name - components - props - data - methods - computed - watch - created - mounted - update |
以下情況需要加注釋,以方便代碼維護和他人理解
-
公共組件使用說明
-
各組件中重要函數或者類說明
-
復雜的業務邏輯處理說明
-
特殊情況的代碼處理說明,對於代碼中特殊用途的變量、存在臨界值、函數中使用的 hack、使用了某種算法或思路等需要進行注釋描述。
-
多重 if 判斷語句
建議不再使用雙引號,靜態字符串使用單引號,動態字符串使用反引號銜接。
// bad const foo = 'jack' const bar = foo + ',前端工程師' // good const foo = 'jack' const bar = `${foo},前端工程師` |
使用數組展開操作符 ... 復制數組。
// bad const len = items.length const itemsCopy = [] let i for (i = 0; i < len; i += 1) { itemsCopy[i] = items[i] } // good const itemsCopy = [...items] |
使用數組對象解構賦值
const arr = [1, 2, 3, 4] // bad const first = arr[0] const second = arr[1] // good const [first, second] = arr |
使用對象屬性速記語法
const name = 'Luke' const age = 20 // bad const obj = { name: name, age: age } // good const obj = { name, age } |
7.1、在 index.html 添加百度統計腳本
var _hmt = _hmt || [](function() { var hm = document.createElement('script') hm.src = 'https://hm.baidu.com/hm.js?d1dbfd0ce8ca70cf72828b2844498250' var s = document.getElementsByTagName('script')[0] s.parentNode.insertBefore(hm, s) })() |
7.2、在全局路由守衛添加需要統計的頁面
router.afterEach(to => { if (window._hmt) { window._hmt.push(['_trackPageview', '/#' + to.fullPath]) } }) |
為什么要優化打包?
-
項目越做越大,依賴包越來越多,打包文件太大
-
單頁面應用首頁白屏時間長,用戶體驗差
我們的目的
-
減小打包后的文件大小
-
首頁按需引入文件
-
優化 webpack 打包時間
2.1、 按需加載
2.1.1 路由組件按需加載
const router = [ { path: '/index', component: resolve => require.ensure([], () => resolve(require('@/components/index'))) }, { path: '/about', component: resolve => require.ensure([], () => resolve(require('@/components/about'))) } ] |
2.1.2 第三方組件和插件。按需加載需引入第三方組件
// 引入全部組件 import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(ElementUI) // 按需引入組件 import { Button } from 'element-ui' Vue.component(Button.name, Button) |
2.1.3 對於一些插件,如果只是在個別組件中用的到,也可以不要在 main.js 里面引入,而是在組件中按需引入
// 在main.js引入 import Vue from vue import Vuelidate from 'vuelidate' Vue.use(Vuelidate) // 按組件按需引入 import { Vuelidate } from 'vuelidate' |
-
優化正則匹配
-
通過 cacheDirectory 選項開啟緩存
-
通過 include、exclude 來減少被處理的文件。
module: { rules: [ { test: /\.js$/, loader: 'babel-loader?cacheDirectory', include: [resolve('src')] } ] } |
-
extension 配置之后可以不用在 require 或是 import 的時候加文件擴展名,會依次嘗試添加擴展名進行匹配。
-
mainFiles 配置后不用加入文件名,會依次嘗試添加的文件名進行匹配
-
alias 通過配置別名可以加快 webpack 查找模塊的速度。
resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), } }, |
-
sourceMap 本質上是一種映射關系,打包出來的 js 文件中的代碼可以映射到代碼文件的具體位置,這種映射關系會幫助我們直接找到在源代碼中的錯誤。
-
打包速度減慢,生產文件變大,所以開發環境使用 sourceMap,生產環境則關閉。
2.5、代碼壓縮
-
UglifyJS: vue-cli 默認使用的壓縮代碼方式,它使用的是單線程壓縮代碼,打包時間較慢
-
ParallelUglifyPlugin: 開啟多個子進程,把對多個文件壓縮的工作分別給多個子進程去完成
兩種方法使用如下:
plugins: [ new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false } }, sourceMap: true, parallel: true }), new ParallelUglifyPlugin({ //緩存壓縮后的結果,下次遇到一樣的輸入時直接從緩存中獲取壓縮后的結果並返回, //cacheDir 用於配置緩存存放的目錄路徑。 cacheDir: '.cache/', sourceMap: true, uglifyJS: { output: { comments: false }, compress: { warnings: false } } }) ] |
打包速度和打包后的文件大小啊對比
方法 | 文件大小 | 打包速度 |
---|---|---|
不用插件 | 14.6M | 32s |
UglifyJsPlugin | 12.9M | 33s |
ParallelUglifyPlugi | 7.98M | 17s |
-
相同資源重復被加載,浪費用戶流量,增加服務器成本。
-
每個頁面需要加載的資源太大,導致網頁首屏加載緩慢,影響用戶體驗。
webpack3 使用 CommonsChunkPlugin 的實現:
plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function(module, count) { console.log(module.resource, `引用次數${count}`) //"有正在處理文件" + "這個文件是 .js 后綴" + "這個文件是在 node_modules 中" return module.resource && /\.js$/.test(module.resource) && module.resource.indexOf(path.join(__dirname, './node_modules')) === 0 } }), new webpack.optimize.CommonsChunkPlugin({ name: 'common', chunks: 'initial', minChunks: 2 }) ] |
webpack4 使用 splitChunks 的實現:
module.exports = { optimization: { splitChunks: { cacheGroups: { vendor: { priority: 1, //添加權重 test: /node_modules/, //把這個目錄下符合下面幾個條件的庫抽離出來 chunks: 'initial', //剛開始就要抽離 minChunks: 2 //重復2次使用的時候需要抽離出來 }, common: { //公共的模塊 chunks: 'initial', minChunks: 2 } } } } } |
-
隨着項目越做越大,依賴的第三方 npm 包越來越多,構建之后的文件也會越來越大。
-
再加上又是單頁應用,這就會導致在網速較慢或者服務器帶寬有限的情況出現長時間的白屏。
1、將 vue、vue-router、vuex、element-ui 和 axios 這五個庫,全部改為通過 CDN 鏈接獲取,在 index.html 里插入 相應鏈接。
<head> <link rel="stylesheet" href="https://cdn.bootcss.com/element-ui/2.0.7/theme-chalk/index.css" /> </head> <body> <div id="app"></div> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script> <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script> <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script> <script src="https://cdn.bootcss.com/element-ui/2.6.1/index.js"></script> <!-- built files will be auto injected --> </body> |
2、在 webpack.config.js 配置文件
module.exports = { ··· externals: { 'vue': 'Vue', 'vuex': 'Vuex', 'vue-router': 'VueRouter', 'element-ui': 'ELEMENT', 'Axios':'axios' } }, |
3、卸載依賴的 npm 包,npm uninstall axios element-ui vue vue-router vuex
4、修改 main.js 文件里之前的引包方式
// import Vue from 'vue' // import ElementUI from 'element-ui' // import 'element-ui/lib/theme-chalk/index.css' // import VueRouter from 'vue-router' import App from './App.vue' import routes from './router' import utils from './utils/Utils' Vue.use(ELEMENT) Vue.use(VueRouter) const router = new VueRouter({ mode: 'hash', //路由的模式 routes }) new Vue({ router, el: '#app', render: h => h(App) }) |
-
由於運行在 Node.js 之上的 Webpack 是單線程模型的,所以 Webpack 需要處理的事情需要一件一件的做,不能多件事一起做。
-
HappyPack 就能讓 Webpack 把任務分解給多個子進程去並發的執行,子進程處理完后再把結果發送給主進程。
-
HappyPack 對 file-loader、url-loader 支持的不友好,所以不建議對該 loader 使用。
使用方法如下:
1、HappyPack 插件安裝: npm i -D happypack
2、webpack.base.conf.js 文件對 module.rules 進行配置
module: { rules: [ { test: /\.js$/, use: ['happypack/loader?id=babel'], include: [resolve('src'), resolve('test')], exclude: path.resolve(__dirname, 'node_modules') }, { test: /\.vue$/, use: ['happypack/loader?id=vue'] } ] } |
3、在生產環境 webpack.prod.conf.js 文件進行配置
const HappyPack = require('happypack') // 構造出共享進程池,在進程池中包含5個子進程 const HappyPackThreadPool = HappyPack.ThreadPool({ size: 5 }) plugins: [ new HappyPack({ // 用唯一的標識符id,來代表當前的HappyPack是用來處理一類特定的文件 id: 'babel', // 如何處理.js文件,用法和Loader配置中一樣 loaders: ['babel-loader?cacheDirectory'], threadPool: HappyPackThreadPool }), new HappyPack({ id: 'vue', // 用唯一的標識符id,來代表當前的HappyPack是用來處理一類特定的文件 loaders: [ { loader: 'vue-loader', options: vueLoaderConfig } ], threadPool: HappyPackThreadPool }) ] |
Sentry 是一個開源的錯誤追蹤工具,可以幫助開發人員實時監控和修復系統中的錯誤。其專注於錯誤監控以及提取一切事后處理所需的信息;支持幾乎所有主流開發語言(JS/Java/Python/php)和平台, 並提供了 web 來展示輸出錯誤。sentry 官網:https://sentry.io
sentry 的特性
-
支持多種語言和框架
-
相同錯誤合並
-
定制規則進行郵件通知
-
支持導入 sourcemap 自動解析和還原代碼
-
友好的可視化 Web 界面
3.1、注冊賬號,新建項目。
新建項目后,您將獲得一個我們稱之為 DSN 或數據源名稱的值.它看起來很像一個標准的 URL,但它實際上只是 Sentry SDK 所需的配置的標識。它由以下幾個部分組成。
-
使用的協議: http 或 https;
-
驗證 sdk 的公鑰和密鑰;
-
目標 sentry 服務器;
-
驗證用戶綁定的項目 id.
import Raven from 'raven-js' import RavenVue from 'raven-js/plugins/vue' Raven.config('https://a60af910fe4449e98fcf7fbb0c714f1a@sentry.io/1516655') .addPlugin(RavenVue, Vue) .install() |
常用參數
-
DSN :項目的地址,用於收集錯誤信息的 sentry 分配的地址
-
debug :是否開啟 debug 模式,開啟 debug,就會把信息打印到控制台上面
-
release : 代碼的版本號,可以確定當前的錯誤/異常屬於哪一個發布的版本
-
environment : 環境名稱
-
attachStacktrace : 是否開啟堆棧跟蹤,開啟后跟着消息一起收集
-
beforeSend : 發送前操作
3.3、基本使用
最簡單的方式是主動觸發:
try { doSomething(a[0]) } catch (e) { Raven.captureException(e) } |
window.onerror 捕捉異常
window.onerror = function(e) { Raven.captureException(e) } |
在 vue 里可以使用 Vue.config.errorHandler 鈎子來捕捉
Vue.config.errorHandler = (err, vm, info) => { Raven.captureException(err) } |
對於接口報錯,可以在全局攔截里實現
request.interceptors.response.use(null, error => { console.error(error) Raven.captureException(error) return Promise.reject(error) }) |
-
創建新的 token
-
安裝@sentry/webpack-plugin 插件, 一般會同時安裝@sentry/cli
- npm i @sentry/webpack-plugin --dev
-
根目錄創建.sentryclirc 文件
[defaults] url = https://sentry.io/ org = your org project = your project [auth] token = your token |
- 在 config/prod.env.js 創建環境變量
const release = 'demo-test001' // 可以根據package.json的版本號或者Git的tag命名 process.env.RELEASE_VERSION = release module.exports = { NODE_ENV: '"production"', RELEASE_VERSION: `"${release}"` } |
- 在 webpack.prod.conf 配置
const SentryPlugin = require('@sentry/webpack-plugin') new SentryPlugin({ release: process.env.RELEASE_VERSION, //發布的版本 include: path.join(__dirname, '../dist/static/js/'), //需要上傳到sentry服務器的資源目錄 ignore: ['node_modules', 'webpack.config.js'], // 忽略文件目錄 configFile: `.sentryclirc`, // 面包含了 -o組織 -p項目 以及authtoken urlPrefix: '~/static/js' // 線上對應的url資源的相對路徑 }) |
Sentry 默認會將所有采集到的異常發送警報郵件,有時我們可能希望只收到某個版本下某些規則下的警報郵件,這時候就需要刪除默認的警報規則,然后新建自定義規則。 在項目設置中找到 Alerts,左上角 “New Alert Rule”即可添加設置報警規則。
3.6、信息收集
-
錯誤類型及具體錯誤信息
-
豐富的上下文周圍的錯誤
-
用戶信息(ip, 機型, 操作系統, 瀏覽器版本, 時間)
-
錯誤代碼的定位(具體文件,具體行數)
-
同一個錯誤發生的次數和用戶數
-
區分錯誤的環境及版本