乐趣区

关于前端:手把手带你入门前端工程化超详细教程

本文将分成以下 7 个大节:

  1. 技术选型
  2. 对立标准
  3. 测试
  4. 部署
  5. 监控
  6. 性能优化
  7. 重构

局部大节提供了十分具体的实战教程,让大家入手实际。

另外我还写了一个前端工程化 demo 放在 github 上。这个 demo 蕴含了 js、css、git 验证,其中 js、css 验证须要装置 VSCode,具体教程在下文中会有提及。

技术选型

对于前端来说,技术选型挺简略的。就是做选择题,三大框架当选一个。集体认为能够根据以下两个特点来选:

  1. 选你或团队最熟的,保障在遇到辣手的问题时有人能填坑。
  2. 选市场占有率高的。换句话说,就是选好招人的。

第二点对于小公司来说,特地重要。原本小公司就不好招人,要是还选一个市场占有率不高的框架(例如 Angular),简历你都看不到几个 …

UI 组件库更简略,github 上哪个 star 多就用哪个。star 多,阐明用的人就多,很多坑他人都替你踩过了,省事。

对立标准

代码标准

先来看看对立代码标准的益处:

  • 标准的代码能够促成团队单干
  • 标准的代码能够升高保护老本
  • 标准的代码有助于 code review(代码审查)
  • 养成代码标准的习惯,有助于程序员本身的成长

当团队的成员都严格依照代码标准来写代码时,能够保障每个人的代码看起来都像是一个人写的,看他人的代码就像是在看本人的代码。更重要的是咱们可能意识到标准的重要性,并保持标准的开发习惯。

如何制订代码标准

倡议找一份好的代码标准,在此基础上联合团队的需要作个性化批改。

上面列举一些 star 较多的 js 代码标准:

  • airbnb (101k star 英文版),airbnb- 中文版
  • standard (24.5k star) 中文版
  • 百度前端编码标准 3.9k

css 代码标准也有不少,例如:

  • styleguide 2.3k
  • spec 3.9k

如何查看代码标准

应用 eslint 能够查看代码符不合乎团队制订的标准,上面来看一下如何配置 eslint 来查看代码。

  1. 下载依赖
// eslint-config-airbnb-base 应用 airbnb 代码标准
npm i -D babel-eslint eslint eslint-config-airbnb-base eslint-plugin-import
  1. package.jsonscripts 加上这行代码 "lint": "eslint --ext .js test/ src/ bin/"。而后执行 npm run lint 即可开始验证代码。

不过这样查看代码效率太低,每次都得手动查看。并且报错了还得手动批改代码。

为了改善以上毛病,咱们能够应用 VSCode。应用它并加上适当的配置能够在每次保留代码的时候,主动验证代码并进行格式化,省去了入手的麻烦。

css 查看代码标准则应用 stylelint 插件。

因为篇幅无限,具体如何配置请看我的另一篇文章 ESlint + stylelint + VSCode 主动格式化代码(2020)。

git 标准

git 标准包含两点:分支治理标准、git commit 标准。

分支治理标准

个别我的项目分主分支(master)和其余分支。

当有团队成员要开发新性能或改 BUG 时,就从 master 分支开一个新的分支。例如我的项目要从客户端渲染改成服务端渲染,就开一个分支叫 ssr,开发完了再合并回 master 分支。

如果改一个 BUG,也能够从 master 分支开一个新分支,并用 BUG 号命名(不过咱们小团队嫌麻烦,没这样做,除非有特地大的 BUG)。

git commit 标准

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

大抵分为三个局部(应用空行宰割):

  1. 题目行: 必填, 形容次要批改类型和内容
  2. 主题内容: 形容为什么批改, 做了什么样的批改, 以及开发的思路等等
  3. 页脚正文: 能够写正文,BUG 号链接

type: commit 的类型

  • feat: 新性能、新个性
  • fix: 批改 bug
  • perf: 更改代码,以进步性能
  • refactor: 代码重构(重构,在不影响代码外部行为、性能下的代码批改)
  • docs: 文档批改
  • style: 代码格局批改, 留神不是 css 批改(例如分号批改)
  • test: 测试用例新增、批改
  • build: 影响我的项目构建或依赖项批改
  • revert: 复原上一次提交
  • ci: 继续集成相干文件批改
  • chore: 其余批改(不在上述类型中的批改)
  • release: 公布新版本
  • workflow: 工作流相干文件批改
  1. scope: commit 影响的范畴, 比方: route, component, utils, build…
  2. subject: commit 的概述
  3. body: commit 具体批改内容, 能够分为多行.
  4. footer: 一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接.

示例

fix(修复 BUG)

如果修复的这个 BUG 只影响以后批改的文件,可不加范畴。如果影响的范畴比拟大,要加上范畴形容。

例如这次 BUG 修复影响到全局,能够加个 global。如果影响的是某个目录或某个性能,能够加上该目录的门路,或者对应的性能名称。

// 示例 1
fix(global): 修复 checkbox 不能复选的问题
// 示例 2 上面圆括号里的 common 为通用治理的名称
fix(common): 修复字体过小的 BUG,将通用治理下所有页面的默认字体大小批改为 14px
// 示例 3
fix: value.length -> values.length
feat(增加新性能或新页面)
feat: 增加网站主页动态页面

这是一个示例,假如对点检工作动态页面进行了一些形容。这里是备注,能够是放 BUG 链接或者一些重要性的货色。
chore(其余批改)

chore 的中文翻译为日常事务、例行工作,顾名思义,即不在其余 commit 类型中的批改,都能够用 chore 示意。

chore: 将表格中的查看详情改为详情

其余类型的 commit 和下面三个示例差不多,就不说了。

验证 git commit 标准

验证 git commit 标准,次要通过 git 的 pre-commit 钩子函数来进行。当然,你还须要下载一个辅助工具来帮忙你进行验证。

下载辅助工具

npm i -D husky

package.json 加上上面的代码

"husky": {
  "hooks": {
    "pre-commit": "npm run lint",
    "commit-msg": "node script/verify-commit.js",
    "pre-push": "npm test"
  }
}

而后在你我的项目根目录下新建一个文件夹 script,并在上面新建一个文件 verify-commit.js,输出以下代码:

const msgPath = process.env.HUSKY_GIT_PARAMS
const msg = require('fs')
.readFileSync(msgPath, 'utf-8')
.trim()

const commitRE = /^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/

if (!commitRE.test(msg)) {console.log()
    console.error(`
        不非法的 commit 音讯格局。请查看 git commit 提交标准:https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md
    `)

    process.exit(1)
}

当初来解释下各个钩子的含意:

  1. "pre-commit": "npm run lint",在 git commit 前执行 npm run lint 查看代码格局。
  2. "commit-msg": "node script/verify-commit.js",在 git commit 时执行脚本 verify-commit.js 验证 commit 音讯。如果不合乎脚本中定义的格局,将会报错。
  3. "pre-push": "npm test",在你执行 git push 将代码推送到近程仓库前,执行 npm test 进行测试。如果测试失败,将不会执行这次推送。

我的项目标准

次要是我的项目文件的组织形式和命名形式。

用咱们的 Vue 我的项目举个例子。

├─public
├─src
├─test

一个我的项目蕴含 public(公共资源,不会被 webpack 解决)、src(源码)、test(测试代码),其中 src 目录,又能够细分。

├─api(接口)├─assets(动态资源)├─components(公共组件)├─styles(公共款式)├─router(路由)├─store(vuex 全局数据)├─utils(工具函数)└─views(页面)

文件名称如果过长则用 – 隔开。

UI 标准

UI 标准须要前端、UI、产品沟通,相互磋商,最初制订下来,倡议应用对立的 UI 组件库。

制订 UI 标准的益处:

  • 对立页面 UI 规范,节俭 UI 设计工夫
  • 进步前端开发效率

测试

测试是前端工程化建设必不可少的一部分,它的作用就是找出 bug,越早发现 bug,所须要付出的老本就越低。并且,它更重要的作用是在未来,而不是当下。

构想一下半年后,你的我的项目要加一个新性能。在加完新性能后,你不确定有没有影响到原有的性能,须要测试一下。因为工夫过来太久,你对我的项目的代码曾经不理解了。在这种状况下,如果没有写测试,你就得手动一遍一遍的去试。而如果写了测试,你只须要跑一遍测试代码就 OK 了,省时省力。

写测试还能够让你批改代码时没有心理累赘,不必始终想着改这里有没有问题?会不会引起 BUG?而写了测试就没有这种放心了。

在前端用得最多的就是单元测试(次要是端到端测试我用得很少,不熟),这里着重解说一下。

单元测试

单元测试就是对一个函数、一个组件、一个类做的测试,它针对的粒度比拟小。

它应该怎么写呢?

  1. 依据正确性写测试,即正确的输出应该有失常的后果。
  2. 依据异样写测试,即谬误的输出应该是谬误的后果。

对一个函数做测试

例如一个取绝对值的函数 abs(),输出 1,2,后果应该与输出雷同;输出 -1,-2,后果应该与输出相同。如果输出非数字,例如 "abc",应该抛出一个类型谬误。

对一个类做测试

假如有这样一个类:

class Math {abs() { }

    sqrt() {}

    pow() {}
    ...
}

单元测试,必须把这个类的所有办法都测一遍。

对一个组件做测试

组件测试比拟难,因为很多组件都波及了 DOM 操作。

例如一个上传图片组件,它有一个将图片转成 base64 码的办法,那要怎么测试呢?个别测试都是跑在 node 环境下的,而 node 环境没有 DOM 对象。

咱们先来回顾一下上传图片的过程:

  1. 点击 <input type="file" />,抉择图片上传。
  2. 触发 inputchange 事件,获取 file 对象。
  3. FileReader 将图片转换成 base64 码。

这个过程和上面的代码是一样的:

document.querySelector('input').onchange = function fileChangeHandler(e) {const file = e.target.files[0]
    const reader = new FileReader()
    reader.onload = (res) => {
        const fileResult = res.target.result
        console.log(fileResult) // 输入 base64 码
    }

    reader.readAsDataURL(file)
}

下面的代码只是模仿,真实情况下应该是这样应用

document.querySelector('input').onchange = function fileChangeHandler(e) {const file = e.target.files[0]
    tobase64(file)
}

function tobase64(file) {return new Promise((resolve, reject) => {const reader = new FileReader()
        reader.onload = (res) => {
            const fileResult = res.target.result
            resolve(fileResult) // 输入 base64 码
        }

        reader.readAsDataURL(file)
    })
}

能够看到,下面代码呈现了 window 的事件对象 eventFileReader。也就是说,只有咱们可能提供这两个对象,就能够在任何环境下运行它。所以咱们能够在测试环境下加上这两个对象:

// 重写 File
window.File = function () {}

// 重写 FileReader
window.FileReader = function () {this.readAsDataURL = function () {
        this.onload
            && this.onload({
                target: {result: fileData,},
            })
    }
}

而后测试能够这样写:

// 提前写好文件内容
const fileData = 'data:image/test'

// 提供一个假的 file 对象给 tobase64() 函数
function test() {const file = new File()
    const event = {target: { files: [file] } }
    file.type = 'image/png'
    file.name = 'test.png'
    file.size = 1024

    it('file content', (done) => {tobase64(file).then(base64 => {expect(base64).toEqual(fileData) // 'data:image/test'
            done()})
    })
}

// 执行测试
test()

通过这种 hack 的形式,咱们就实现了对波及 DOM 操作的组件的测试。我的 vue-upload-imgs 库就是通过这种形式写的单元测试,有趣味能够理解一下。

TDD 测试驱动开发

TDD 就是依据需要提前把测试代码写好,而后依据测试代码实现性能。

TDD 的初衷是好的,但如果你的需要常常变(你懂的),那就不是一件坏事了。很有可能你天天都在改测试代码,业务代码反而没怎么动。
所以到当初为止,三年多的程序员生涯,我还没尝试过 TDD 开发。

尽管环境如此艰巨,但有条件的状况下还是应该试一下 TDD 的。例如在你本人负责一个我的项目又不忙的时候,能够采纳此办法编写测试用例。

部署

在没有学会主动部署前,我是这样部署我的项目的:

  1. 执行测试 npm run test
  2. 推送代码 git push
  3. 构建我的项目 npm run build
  4. 将打包好的文件放到动态服务器。

一次两次还行,如果天天都这样,就会把很多工夫节约在反复的操作上。所以咱们要学会主动部署,彻底解放双手。

主动部署(又叫继续部署 Continuous Deployment,英文缩写 CD)个别有两种触发形式:

  1. 轮询。
  2. 监听 webhook 事件。

轮询

轮询,就是构建软件每隔一段时间主动执行打包、部署操作。

这种形式不太好,很有可能软件刚部署完我就改代码了。为了看到新的页面成果,不得不等到下一次构建开始。

另外还有一个副作用,如果我一天都没更改代码,构建软件还是会不停的执行打包、部署操作,白白的浪费资源。

所以当初的构建软件根本采纳监听 webhook 事件的形式来进行部署。

监听 webhook 事件

webhook 钩子函数,就是在你的构建软件上进行设置,监听某一个事件(个别是监听 push 事件),当事件触发时,主动执行定义好的脚本。

例如 Github Actions,就有这个性能。

对于新人来说,仅看我这一段解说是不可能学会主动部署的。为此我顺便写了一篇自动化部署教程,不须要你提前学习自动化部署的常识,只有照着指引做,就能实现前端我的项目自动化部署。

前端我的项目自动化部署——超具体教程(Jenkins、Github Actions),教程曾经奉上,各位大佬看完后要是感觉有用,不要忘了点赞,感激不尽。

监控

监控,又分性能监控和谬误监控,它的作用是预警和追踪定位问题。

性能监控

性能监控个别利用 window.performance 来进行数据采集。

Performance 接口能够获取到以后页面中与性能相干的信息,它是 High Resolution Time API 的一部分,同时也交融了 Performance Timeline API、Navigation Timing API、User Timing API 和 Resource Timing API。

这个 API 的属性 timing,蕴含了页面加载各个阶段的起始及完结工夫。


为了不便大家了解 timing 各个属性的意义,我在知乎找到一位网友对于 timing 写的简介(忘了姓名,起初找不到了,见谅),在此转载一下。

timing: {// 同一个浏览器上一个页面卸载 (unload) 完结时的工夫戳。如果没有上一个页面,这个值会和 fetchStart 雷同。navigationStart: 1543806782096,

    // 上一个页面 unload 事件抛出时的工夫戳。如果没有上一个页面,这个值会返回 0。unloadEventStart: 1543806782523,

    // 和 unloadEventStart 绝对应,unload 事件处理实现时的工夫戳。如果没有上一个页面, 这个值会返回 0。unloadEventEnd: 1543806782523,

    // 第一个 HTTP 重定向开始时的工夫戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回 0。redirectStart: 0,

    // 最初一个 HTTP 重定向实现时(也就是说是 HTTP 响应的最初一个比特间接被收到的工夫)的工夫戳。// 如果没有重定向,或者重定向中的一个不同源,这个值会返回 0. 
    redirectEnd: 0,

    // 浏览器筹备好应用 HTTP 申请来获取 (fetch) 文档的工夫戳。这个工夫点会在查看任何利用缓存之前。fetchStart: 1543806782096,

    // DNS 域名查问开始的 UNIX 工夫戳。// 如果应用了继续连贯(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart 统一。domainLookupStart: 1543806782096,

    // DNS 域名查问实现的工夫.
    // 如果应用了本地缓存(即无 DNS 查问)或长久连贯,则与 fetchStart 值相等
    domainLookupEnd: 1543806782096,

    // HTTP(TCP)域名查问完结的工夫戳。// 如果应用了继续连贯(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart 统一。connectStart: 1543806782099,

    // HTTP(TCP)返回浏览器与服务器之间的连贯建设时的工夫戳。// 如果建设的是长久连贯,则返回值等同于 fetchStart 属性的值。连贯建设指的是所有握手和认证过程全副完结。connectEnd: 1543806782227,

    // HTTPS 返回浏览器与服务器开始平安链接的握手时的工夫戳。如果以后网页不要求平安连贯,则返回 0。secureConnectionStart: 1543806782162,

    // 返回浏览器向服务器收回 HTTP 申请时(或开始读取本地缓存时)的工夫戳。requestStart: 1543806782241,

    // 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的工夫戳。// 如果传输层在开始申请之后失败并且连贯被重开,该属性将会被数制成新的申请的绝对应的发动工夫。responseStart: 1543806782516,

    // 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最初一个字节时
        //(如果在此之前 HTTP 连贯曾经敞开,则返回敞开时)的工夫戳。responseEnd: 1543806782537,

    // 以后网页 DOM 构造开始解析时(即 Document.readyState 属性变为“loading”、相应的 readystatechange 事件触发时)的工夫戳。domLoading: 1543806782573,

    // 以后网页 DOM 构造完结解析、开始加载内嵌资源时(即 Document.readyState 属性变为“interactive”、相应的 readystatechange 事件触发时)的工夫戳。domInteractive: 1543806783203,

    // 当解析器发送 DOMContentLoaded 事件,即所有须要被执行的脚本曾经被解析时的工夫戳。domContentLoadedEventStart: 1543806783203,

    // 当所有须要立刻执行的脚本曾经被执行(不管执行程序)时的工夫戳。domContentLoadedEventEnd: 1543806783216,

    // 以后文档解析实现,即 Document.readyState 变为 'complete' 且绝对应的 readystatechange 被触发时的工夫戳
    domComplete: 1543806783796,

    // load 事件被发送时的工夫戳。如果这个事件还未被发送,它的值将会是 0。loadEventStart: 1543806783796,

    // 当 load 事件完结,即加载事件实现时的工夫戳。如果这个事件还未被发送,或者尚未实现,它的值将会是 0.
    loadEventEnd: 1543806783802
}

通过以上数据,咱们能够失去几个有用的工夫

// 重定向耗时
redirect: timing.redirectEnd - timing.redirectStart,
// DOM 渲染耗时
dom: timing.domComplete - timing.domLoading,
// 页面加载耗时
load: timing.loadEventEnd - timing.navigationStart,
// 页面卸载耗时
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 申请耗时
request: timing.responseEnd - timing.requestStart,
// 获取性能信息时以后工夫
time: new Date().getTime(),

还有一个比拟重要的工夫就是 白屏工夫,它指从输出网址,到页面开始显示内容的工夫。

将以下脚本放在 </head> 后面就能获取白屏工夫。

<script>
    whiteScreen = new Date() - performance.timing.navigationStart
</script>

通过这几个工夫,就能够得悉页面首屏加载性能如何了。

另外,通过 window.performance.getEntriesByType('resource') 这个办法,咱们还能够获取相干资源(js、css、img…)的加载工夫,它会返回页面以后所加载的所有资源。

它个别包含以下几个类型

  • sciprt
  • link
  • img
  • css
  • fetch
  • other
  • xmlhttprequest

咱们只需用到以下几个信息

// 资源的名称
name: item.name,
// 资源加载耗时
duration: item.duration.toFixed(2),
// 资源大小
size: item.transferSize,
// 资源所用协定
protocol: item.nextHopProtocol,

当初,写几行代码来收集这些数据。

// 收集性能信息
const getPerformance = () => {if (!window.performance) return
    const timing = window.performance.timing
    const performance = {
        // 重定向耗时
        redirect: timing.redirectEnd - timing.redirectStart,
        // 白屏工夫
        whiteScreen: whiteScreen,
        // DOM 渲染耗时
        dom: timing.domComplete - timing.domLoading,
        // 页面加载耗时
        load: timing.loadEventEnd - timing.navigationStart,
        // 页面卸载耗时
        unload: timing.unloadEventEnd - timing.unloadEventStart,
        // 申请耗时
        request: timing.responseEnd - timing.requestStart,
        // 获取性能信息时以后工夫
        time: new Date().getTime(),
    }

    return performance
}

// 获取资源信息
const getResources = () => {if (!window.performance) return
    const data = window.performance.getEntriesByType('resource')
    const resource = {xmlhttprequest: [],
        css: [],
        other: [],
        script: [],
        img: [],
        link: [],
        fetch: [],
        // 获取资源信息时以后工夫
        time: new Date().getTime(),
    }

    data.forEach(item => {const arry = resource[item.initiatorType]
        arry && arry.push({
            // 资源的名称
            name: item.name,
            // 资源加载耗时
            duration: item.duration.toFixed(2),
            // 资源大小
            size: item.transferSize,
            // 资源所用协定
            protocol: item.nextHopProtocol,
        })
    })

    return resource
}

小结

通过对性能及资源信息的解读,咱们能够判断出页面加载慢有以下几个起因:

  1. 资源过多
  2. 网速过慢
  3. DOM 元素过多

除了用户网速过慢,咱们没方法之外,其余两个起因都是有方法解决的,性能优化将在下一节《性能优化》中会讲到。

谬误监控

当初能捕获的谬误有三种。

  1. 资源加载谬误,通过 addEventListener('error', callback, true) 在捕捉阶段捕获资源加载失败谬误。
  2. js 执行谬误,通过 window.onerror 捕获 js 谬误。
  3. promise 谬误,通过 addEventListener('unhandledrejection', callback)捕获 promise 谬误,然而没有产生谬误的行数,列数等信息,只能手动抛出相干错误信息。

咱们能够建一个谬误数组变量 errors 在谬误产生时,将谬误的相干信息增加到数组,而后在某个阶段对立上报,具体如何操作请看代码

// 捕捉资源加载失败谬误 js css img...
addEventListener('error', e => {
    const target = e.target
    if (target != window) {
        monitor.errors.push({
            type: target.localName,
            url: target.src || target.href,
            msg: (target.src || target.href) + 'is load error',
            // 谬误产生的工夫
            time: new Date().getTime(),
        })
    }
}, true)

// 监听 js 谬误
window.onerror = function(msg, url, row, col, error) {
    monitor.errors.push({
        type: 'javascript',
        row: row,
        col: col,
        msg: error && error.stack? error.stack : msg,
        url: url,
        // 谬误产生的工夫
        time: new Date().getTime(),
    })
}

// 监听 promise 谬误 毛病是获取不到行数数据
addEventListener('unhandledrejection', e => {
    monitor.errors.push({
        type: 'promise',
        msg: (e.reason && e.reason.msg) || e.reason || '',
        // 谬误产生的工夫
        time: new Date().getTime(),
    })
})

小结

通过谬误收集,能够理解到网站谬误产生的类型及数量,从而能够做相应的调整,以缩小谬误产生。
残缺代码和 DEMO 请看我另一篇文章前端性能和谬误监控的开端,大家能够复制代码(HTML 文件)在本地测试一下。

数据上报

性能数据上报

性能数据能够在页面加载完之后上报,尽量不要对页面性能造成影响。

window.onload = () => {
    // 在浏览器闲暇工夫获取性能及资源信息
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
    if (window.requestIdleCallback) {window.requestIdleCallback(() => {monitor.performance = getPerformance()
            monitor.resources = getResources()})
    } else {setTimeout(() => {monitor.performance = getPerformance()
            monitor.resources = getResources()}, 0)
    }
}

当然,你也能够设一个定时器,循环上报。不过每次上报最好做一下比照去重再上报,防止同样的数据反复上报。

谬误数据上报

我在 DEMO 里提供的代码,是用一个 errors 数组收集所有的谬误,再在某一阶段对立上报(延时上报)。
其实,也能够改成在谬误产生时上报(即时上报)。这样能够防止在收集完谬误延时上报还没触发,用户却曾经关掉网页导致谬误数据失落的问题。

// 监听 js 谬误
window.onerror = function(msg, url, row, col, error) {
    const data = {
        type: 'javascript',
        row: row,
        col: col,
        msg: error && error.stack? error.stack : msg,
        url: url,
        // 谬误产生的工夫
        time: new Date().getTime(),
    }
    
    // 即时上报
    axios.post({url: 'xxx', data,})
}

SPA

window.performance API 是有毛病的,在 SPA 切换路由时,window.performance.timing 的数据不会更新。
所以咱们须要另想办法来统计切换路由到加载实现的工夫。
拿 Vue 举例,一个可行的方法就是切换路由时,在路由的全局前置守卫 beforeEach 里获取开始工夫,在组件的 mounted 钩子里执行 vm.$nextTick 函数来获取组件的渲染结束工夫。

router.beforeEach((to, from, next) => {store.commit('setPageLoadedStartTime', new Date())
})
mounted() {this.$nextTick(() => {this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
    })
}

除了性能和谬误监控,其实咱们还能够做得更多。

用户信息收集

navigator

应用 window.navigator 能够收集到用户的设施信息,操作系统,浏览器信息 …

UV(Unique visitor)

是指通过互联网拜访、浏览这个网页的自然人。拜访您网站的一台电脑客户端为一个访客。00:00-24:00 内雷同的客户端只被计算一次。一天内同个访客屡次拜访仅计算一个 UV。
在用户拜访网站时,能够生成一个随机字符串 + 工夫日期,保留在本地。在网页产生申请时(如果超过当天 24 小时,则从新生成),把这些参数传到后端,后端利用这些信息生成 UV 统计报告。

PV(Page View)

即页面浏览量或点击量,用户每 1 次对网站中的每个网页拜访均被记录 1 个 PV。用户对同一页面的屡次拜访,访问量累计,用以掂量网站用户拜访的网页数量。

页面停留时间

传统网站
用户在进入 A 页面时,通过后盾申请把用户进入页面的工夫捎上。过了 10 分钟,用户进入 B 页面,这时后盾能够通过接口捎带的参数能够判断出用户在 A 页面停留了 10 分钟。
SPA
能够利用 router 来获取用户停留时间,拿 Vue 举例,通过 router.beforeEach destroyed 这两个钩子函数来获取用户停留该路由组件的工夫。

浏览深度

通过 document.documentElement.scrollTop 属性以及屏幕高度,能够判断用户是否浏览完网站内容。

页面跳转起源

通过 document.referrer 属性,能够晓得用户是从哪个网站跳转而来。

小结

通过剖析用户数据,咱们能够理解到用户的浏览习惯、喜好等等信息,想想真是恐怖,毫无隐衷可言。

前端监控部署教程

后面说的都是监控原理,但要实现还是得本人入手写代码。为了防止麻烦,咱们能够用现有的工具 sentry 去做这件事。

sentry 是一个用 python 写的性能和谬误监控工具,你能够应用 sentry 提供的服务(收费性能少),也能够本人部署服务。当初来看一下如何应用 sentry 提供的服务实现监控。

注册账号

关上 https://sentry.io/signup/ 网站,进行注册。

抉择我的项目,我选的 Vue。

装置 sentry 依赖

选完我的项目,上面会有具体的 sentry 依赖装置指南。

依据提醒,在你的 Vue 我的项目执行这段代码 npm install --save @sentry/browser @sentry/integrations @sentry/tracing,装置 sentry 所需的依赖。

再将上面的代码拷到你的 main.js,放在 new Vue() 之前。

import * as Sentry from "@sentry/browser";
import {Vue as VueIntegration} from "@sentry/integrations";
import {Integrations} from "@sentry/tracing";

Sentry.init({
  dsn: "xxxxx", // 这里是你的 dsn 地址,注册完就有
  integrations: [
    new VueIntegration({
      Vue,
      tracing: true,
    }),
    new Integrations.BrowserTracing(),],

  // We recommend adjusting this value in production, or using tracesSampler
  // for finer control
  tracesSampleRate: 1.0,
});

而后点击第一步中的 skip this onboarding,进入控制台页面。

如果忘了本人的 DSN,请点击右边的菜单栏抉择 Settings -> Projects -> 点击本人的我的项目 -> Client Keys(DSN)

创立第一个谬误

在你的 Vue 我的项目执行一个打印语句 console.log(b)

这时点开 sentry 主页的 issues 一项,能够发现有一个报错信息 b is not defined

这个报错信息蕴含了谬误的具体信息,还有你的 IP、浏览器信息等等。

但奇怪的是,咱们的浏览器控制台并没有输入报错信息。

这是因为被 sentry 屏蔽了,所以咱们须要加上一个选项 logErrors: true

而后再查看页面,发现控制台也有报错信息了:

上传 sourcemap

个别打包后的代码都是通过压缩的,如果没有 sourcemap,即便有报错信息,你也很难依据提醒找到对应的源码在哪。

上面来看一下如何上传 sourcemap。

首先创立 auth token。

这个生成的 token 一会要用到。

装置 sentry-cli@sentry/webpack-plugin

npm install sentry-cli-binary -g
npm install --save-dev @sentry/webpack-plugin

装置完下面两个插件后,在我的项目根目录创立一个 .sentryclirc 文件(不要忘了在 .gitignore 把这个文件增加上,免得裸露 token),内容如下:

[auth]
token=xxx

[defaults]
url=https://sentry.io/
org=woai3c
project=woai3c

把 xxx 替换成方才生成的 token。

org 是你的组织名称。

project 是你的项目名称,依据上面的提醒能够找到。

在我的项目下新建 vue.config.js 文件,把上面的内容填进去:

const SentryWebpackPlugin = require('@sentry/webpack-plugin')

const config = {
    configureWebpack: {
        plugins: [
            new SentryWebpackPlugin({
                include: './dist', // 打包后的目录
                ignore: ['node_modules', 'vue.config.js', 'babel.config.js'],
            }),
        ],
    },
}

// 只在生产环境下上传 sourcemap
module.exports = process.env.NODE_ENV == 'production'? config : {}

填完当前,执行 npm run build,就能够看到 sourcemap 的上传后果了。

咱们再来看一下没上传 sourcemap 和上传之后的报错信息比照。

未上传 sourcemap

已上传 sourcemap

能够看到,上传 sourcemap 后的报错信息更加精确。

切换中文环境和时区

选完刷新即可。

性能监控

关上 performance 选项,就能看到你每个我的项目的运行状况。具体的参数解释请看文档 Performance Monitoring。

性能优化

性能优化次要分为两类:

  1. 加载时优化
  2. 运行时优化

例如压缩文件、应用 CDN 就属于加载时优化;缩小 DOM 操作,应用事件委托属于运行时优化。

在解决问题之前,必须先找出问题,否则无从下手。所以在做性能优化之前,最好先考察一下网站的加载性能和运行性能。

手动查看

查看加载性能

一个网站加载性能如何次要看白屏工夫和首屏工夫。

  • 白屏工夫:指从输出网址,到页面开始显示内容的工夫。
  • 首屏工夫:指从输出网址,到页面齐全渲染的工夫。

将以下脚本放在 </head> 后面就能获取白屏工夫。

<script>
    new Date() - performance.timing.navigationStart
</script>

首屏工夫比较复杂,得思考有图片和没有图片的状况。

如果没有图片,则在 window.onload 事件里执行 new Date() - performance.timing.navigationStart 即可获取首屏工夫。

如果有图片,则要在最初一个在首屏渲染的图片的 onload 事件里执行 new Date() - performance.timing.navigationStart 获取首屏工夫,施行起来比较复杂,在这里限于篇幅就不说了。

查看运行性能

配合 chrome 的开发者工具,咱们能够查看网站在运行时的性能。

关上网站,按 F12 抉择 performance,点击左上角的灰色圆点,变成红色就代表开始记录了。这时能够模拟用户应用网站,在应用结束后,点击 stop,而后你就能看到网站运行期间的性能报告。如果有红色的块,代表有掉帧的状况;如果是绿色,则代表 FPS 很好。

另外,在 performance 标签下,按 ESC 会弹出来一个小框。点击小框右边的三个点,把 rendering 勾进去。

这两个选项,第一个是高亮重绘区域,另一个是显示帧渲染信息。把这两个选项勾上,而后浏览网页,能够实时的看到你网页渲染变动。

利用工具查看

监控工具

能够部署一个前端监控零碎来监控网站性能,上一节中讲到的 sentry 就属于这一类。

chrome 工具 Lighthouse

如果你装置了 Chrome 52+ 版本,请按 F12 关上开发者工具。

它不仅会对你网站的性能打分,还会对 SEO 打分。

应用 Lighthouse 审查网络应用

如何做性能优化

网上对于性能优化的文章和书籍多不胜数,但有很多优化规定曾经过期了。所以我写了一篇性能优化文章前端性能优化 24 条倡议(2020),剖析总结出了 24 条性能优化倡议,强烈推荐。

重构

《重构 2》一书中对重构进行了定义:

所谓重构(refactoring)是这样一个过程:在不扭转代码外在行为的前提下,对代码做出批改,以改良程序的内部结构。重构是一种经千锤百炼造成的井井有条的程序整顿办法,能够最大限度地减小整顿过程中引入谬误的概率。实质上说,重构就是在代码写好之后改良它的设计。

重构和性能优化有相同点,也有不同点。

雷同的中央是它们都在不改变程序性能的状况下批改代码;不同的中央是重构为了让代码变得更加易读、了解,性能优化则是为了让程序运行得更快。

重构能够一边写代码一边重构,也能够在程序写完后,拿出一段时间专门去做重构。没有说哪个形式更好,视集体状况而定。

如果你专门拿一段时间来做重构,倡议你在重构一段代码后,立刻进行测试。这样能够防止批改代码太多,在出错时找不到谬误点。

重构的准则

  1. 事不过三,三则重构。即不能反复写同样的代码,在这种状况下要去重构。
  2. 如果一段代码让人很难看懂,那就该思考重构了。
  3. 如果曾经了解了代码,然而十分繁琐或者不够好,也能够重构。
  4. 过长的函数,须要重构。
  5. 一个函数最好对应一个性能,如果一个函数被塞入多个性能,那就要对它进行重构了。

重构手法

在《重构 2》这本书中,介绍了多达上百个重构手法。但我感觉有两个是比拟罕用的:

  1. 提取反复代码,封装成函数
  2. 拆分太长或性能太多的函数

提取反复代码,封装成函数

假如有一个查问数据的接口 /getUserData?age=17&city=beijing。当初须要做的是把用户数据:{age: 17, city: 'beijing'} 转成 URL 参数的模式:

let result = ''const keys = Object.keys(data)  // {age: 17, city:'beijing'}
keys.forEach(key => {result += '&' + key + '=' + data[key]
})

result.substr(1) // age=17&city=beijing

如果只有这一个接口须要转换,不封装成函数是没问题的。但如果有多个接口都有这种需要,那就得把它封装成函数了:

function JSON2Params(data) {
    let result = ''
    const keys = Object.keys(data)
    keys.forEach(key => {result += '&' + key + '=' + data[key]
    })

    return result.substr(1)
}

拆分太长或性能太多的函数

假如当初有一个注册性能,用伪代码示意:

function register(data) {
    // 1. 验证用户数据是否非法
    /**
     * 验证账号
     * 验证明码
     * 验证短信验证码
     * 验证身份证
     * 验证邮箱
     */

    // 2. 如果用户上传了头像,则将用户头像转成 base64 码保留
    /**
     * 新建 FileReader 对象
     * 将图片转换成 base64 码
     */

    // 3. 调用注册接口
    // ...
}

这个函数蕴含了三个性能,验证、转换、注册。其中验证和转换性能是能够提取进去独自封装成函数的:

function register(data) {
    // 1. 验证用户数据是否非法
    // verify()

    // 2. 如果用户上传了头像,则将用户头像转成 base64 码保留
    // tobase64()

    // 3. 调用注册接口
    // ...
}

如果你对重构有趣味,强烈推荐你浏览《重构 2》这本书。

参考资料:

  • 《重构 2》

总结

写这篇文章次要是为了对我这一年多工作教训作总结,因为我基本上都在钻研前端工程化以及如何晋升团队的开发效率。心愿这篇文章能帮忙一些对前端工程化没有教训的老手,通过这篇文章入门前端工程化。

如果这篇文章对你有帮忙,请点一下赞,感激不尽。

求职启事

自己具备三年 + 前端工作教训,32 岁,高中学历,现寻求天津、北京地区的前端工作机会。

上面是我把握的一些技能:

  1. 熟练掌握 HTML、CSS、JavaScript。
  2. 熟练掌握 Vue 全家桶并钻研过 Vue1.0 源码及 Vue3.0 局部源码。
  3. 应用 nodejs 写过脚本和集体博客,没有开发过企业应用。
  4. 学习计算机原理并实现一个简略的 cpu 和内存模块运行在模拟器上(github 我的项目地址)。
  5. 学习操作系统并做试验实现了一个简略的内核(github 我的项目地址)。
  6. 学习编译原理写过一个简略编译器(github 我的项目地址)。
  7. 对计算机网络应用层和传输层的常识比拟理解。
  8. 数据结构与算法有学习过,还刷了 300+ 道 leetcode,但成果不是很好。

社交网站

  • Github
  • 知乎

如果您感觉我的条件还能够,能够私信我或在评论区留言,谢谢。

退出移动版