前端体系之【全链路体系优化工程介绍与落地实践】(含优化前后比较)

前端体系之【全链路体系优化工程介绍与落地实践】(含优化前后比较)

(1). 工程化体系定义:

. 广义上,一切以"提高效率、降低成本、保障质量"为目的的手段,都属于工程化的范畴.. 通过一系列的规范、流程、工具达到"研发提效、自动化、保障质量、服务稳定、预警监控".. 可以借助于Node,将研发链路延伸到整个DevOps中去.. 前端工程化指使用软件工程的技术与方法对前端开发的技术、工具、流程、经验、方案等指标标准化:
    a. 模块化
    b. 组件化
    c. 规范化
    d. 自动化

⑤. 目的:
    a. 降低成本
    b. 增加效率

(2). 团队标准:

. 互联网前端标配:
    组件化、工程化、自动化

②. 规模的团队:
    a. 根据自身业务与梯度来设计符合业务的DevOps流程.

(3). 简单DevOps:

. 常规基建:
    a. 组件库 + 脚手架 + 工具库 + 模板 + CLI. Git Flow:
    a. 通过常规Git Flow工作流,不同branch不同功能 + Code Review

③. CICD:
    a. Webhook +脚本

④. 说明:
    a. 上述DevOps流程,作为小型团队搭建工程化的起点,性价比极高.
    b. 在团队没有制定规则,也没有基础建设时,通常先从最基础的CLI工具开始然后切入到整个工程化的搭建.

(4). 个人发展:

编写业务代码  =>  使用前端工程化解决生产问题  =>  前端架构设计  =>  技术管理岗晋升

(5). 业务痛点:

随着需求迭代的步伐加速,可能会产生以下问题

. 构建配置、打包配置、公共组件、工具函数等代码片段,每次新开项目都要复制粘贴

②. 团队成员的编码风格大相径庭,导致从仓库拉取下来的代码运行起来让控制台一片红

③. 团队协作的规范、环境、模块、仓库和文档,太多基建措施导致团队新成员无从入手

④. 随着需求迭代引起项目结构与工程文件不断变化,处理不当让项目直接走向重构道路

前端工程化的开发思维与解决方案应用到项目中,解决非业务需求,为业务降本增效.

. 前端工程化不是某个具体的工具:
    a. 对项目的整体架构与整体规划,使开发者能在未来可判时间内动态规划发展走向,以提升整个项目对用户的服务周期

②. 闭环:
    a. 理解项目的完整流程
    b. 在复杂的流程中快速定位并解决问题
    c. 根据知识储备制定一些可扩展流程
    d. 预见项目的未来发展方向

前端工程化体系:

. 明确前后端任务分离的能力:
    a. 任务属于前端还是后端,利于前端工程化的接入
    b. 基于前端工程化解决问题的基础

②. 核心特性:
    a. 模块化、组件化、规范化和自动化
    b. 如何实现?各自的标准是什么?. 前端工程化领域实践:
    a. 利用工程架构的知识重构项目
    b. 脚手架、组件库、工具库、多包仓库、私有仓库、接口系统、文档系统、监控系统、CI/CD、可移植容器
    c. 从手动处理流程转换为自动处理流程,让其它成员更专注于自身业务需求

前端工程化的意义:

. 前后分离:
    a. 前后端自成体系,且与后端分离
    b. 不限于规范、服务、环境、构建、组织和部署方面

②. 技术选型:
    a. 不能以一个框架满足所有业务场景
    b. 制定多套框架解决方案避免技术瓶颈的出现

③. 重构封装:
    a. 新生技术不断涌现就要避免改头换面式的重构
    b. 重复需求不断出现就要学会举一反三的封装

④. 工程设计:
    a. 解决方案要合理分层且互相独立,随时应对各种变化
    b. 任何一层可低成本被替换与淘汰

⑤. 所有的基建都是要依托业务才能发挥最大的作用

2. 如何开发一个前端脚手架?

. 功能:
    a. 脚手架是一套命令集,不只用来创建项目.
    b. 解耦 - 脚手架与模板分离:
       (1). 脚手架负责构建流程,通过命令行与用户交互,获取项目信息
       (2). 脚手架需要检测模板的版本是否有更新,支持模板的删除与新建
       (3). 模板负责统一项目结构、工作流程、依赖项管理

②. 作用:
    a. 减少重复工作,不需要复制其它项目再删除无关代码,或从零创建一个项目和文件.
    b. 根据交互动态生成项目结构和配置文件.

1. CLI工具集:

. 构建:
    a. 提供本地构建功能
    b. 接管发布构建

②. 质量:
    a. 自动化测试
    b. Eslint校验

③. 模板:
    a. 创建模板
    b. 创建区块

④. 工具合集:
    a. 其他可以内置的工具类

(1). 构建:

. 小团队构建流程:
    a. 在一套或多套模板中使用webpack或rollup构建工具,配置多个文件,.env.production、.env.development、.env.staging
    b. 通过Shell脚本来构建项目
    c. 进行部署,实现了简单、通用的CI/CD流程

②. 构建过程不可控:
    a. 团队的开发成员都可以修改发布配置项
    b. 误操作,如选择的是dev模式,没有对构建代码压缩混淆、没有注入一些全局统一方法等.. 优化:
    a. 构建配置和项目模板分离:
       (1). 将构建配置、过程从项目模板中抽离出来,统一使用CLI管理构建流程:
       (2). 不再读取项目中的配置
       (3). 通过CLI使用统一配置(每一类项目都可以自定义一套标准构建配置)进行构建.
    b. 避免业务开发同学修改了错误配置而导致的生产问题.

(2). 质量:

. 通用的自动化测试、常规的格式校验统一:
    a. 如每个开发的习惯不同,导致ESLINT校验规则不同.
    b. 同一个团队必须使用同一套代码校验规则最好.. 优化:
    a. 将自动化测试、校验从项目中剥离,使用CLI接管,从而保证整个团队同一类项目代码格式的统一性.

(3). 模板:

. 可以快速、便捷初始化一个项目或代码片段.. Cli工具产出最高、收益最明显的功能模块.

(4). 工具合集:

. 通用的工具类:
    a. 图片压缩(png压缩)、上传CDN等
    b. 项目升级(如通用配置更新了,提供一键升级模板的功能)
    c. 项目部署、发布npm包等操作

②. 其它一些重复性的操作

3. 生成最简化脚手架:

(1). 初始化package.json文件:

yarn init

(2). 在package.json中,新增bin属性:

{
  "name": "cli",
  "main": "index.js",
  "bin": {
    "gl-cli": "./index.js" // gl-cli表示脚手架的名称
  }
}

(3). 根目录下新增cli.js文件:

#!/usr/bin/env node
// Node CLI 应用入口文件必须要有这样的文件头,用于指定脚本的解释程序

console.log('gl-cli working!')

:. Linux或maxOS,需要修改此文件的读写权限为755:
    chmod 755 cli.js

(4). 把本项目/应用链接到yarn全局缓存(链接到全局),只是方便开发调试:

yarn link      // 在当前根目录执行,yarn unlink可卸载

:. 检查当前yarn的bin位置:
    a. yarn global bin  =>  /Users/xxx/.yarn/bin  => 有一个gl-cli执行命令

②. 检查当前 yarn 的 全局安装位置
    a. yarn global dir  =>  /Users/xxx/.config/yarn/global  =>  下面有一个link文件

(5). 测试执行本cli命令:

gl-cli

. 当打包引入的第三方库时,vender.js会很大:
    a. 一些常用固定的第三方库,不会改动源代码,不会每次都发生变化.
    b. 导致加载时空白页时间过长.
    c. 没必要每次都生成hash值,让用户重新加载.同时还会消耗带宽流量.. webpack提供的externals属性:
    a. externals可以将依赖的第三方库从打包文件剔除
    b. 大大减小了文件包大小,同时大幅提升编译效率.

(1). 工作原理:

. externals配置在所创建bundle时:
    a. 会依赖于用户环境(consumer's environment)中的依赖,防止将某些import的包(package)打包到bundle中
    b. 在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies). webpack会检测这些组件是否在externals中注册,如果注册则不会将其打包到app.js中

③. 修改了记得重启webpack

④. 在需要使用它的时候,可以通过CMDAMD、或window全局方式访问

2. 哪些第三方库适合?

. vue、vue-router、axios、element-ui、qs、crypto-js、vuex、moment、highlight.js

②. 要考虑大小不超过500kb,如果用到ueditor大型工具库需要单独打包.

(2). element-ui分析:

. 都会把element-ui打包进去,每次修改都会下载element-ui.. 独立出去用cdn加载,用户下一次就有缓存.. 后边随便怎么改,只要有缓存就不会在下element-ui.

(3). 例子:

// externals中的key是后面需要require的名字,value是第三方库暴露出来的方法名
// 'alias': 'ObjName'
// 简单的配置如上,alias 是项目内使用时的组件名称,ObjName 是某外部组件对外暴露的名称。
// 比如 vue 的 window 全局名称是 Vue
// 比如 vue-router的 window 全局名称是 VueRouter
// 比如 jquery 的 window 全局名称是 Jquery
module.exports = {
  externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'axios': 'axios',
    'element-ui': 'Element',
    'qs': 'Qs'
  }
}

(1). 优化vue.js:

. 修改vue.config.js:
    const isProd = process.env.NODE_ENV === 'production'
    const getProdExternals = () => {
      return {
        'vue': 'Vue',
        // 'vue-router': 'VueRouter',
        // 'vuex': 'Vuex'
      }
    }
    module.exports = {
      ...
      configureWebpack: {
        ...
        externals: isProd ? getProdExternals() : {}
      }
    }.public/index.html文件中引入vue cdn路径:
    <script src="//cdn.bootcss.***/vue/2.6.10/vue.min.js"></script>
    a. 不写协议前缀,会与网站的协议相同.所以,可以不写https.. 'vue': 'Vue'说明:
    a. key是node模块名称,value是项目中对模块的引用
    b. 前面的vue是代码中import A from B中的B
    c. 后面的Vue是引入的cdn暴露的变量:
       (1). 可以在console控制台打印window,会发现window.Vue
       (2). 这个Vue就是需要的变量名称

(2). 优化index.html写法:

vue.config.js:

const cdn = {
  css: [],
  js: [
    // 与package.json里面的版本对应
    '//cdn.bootcss.***/vue/2.6.10/vue.min.js',
    '//cdn.bootcss.***/vue-router/3.0.6/vue-router.min.js',
    '//cdn.bootcss.***/vuex/3.1.0/vuex.min.js'
  ]
}
module.exports = {
  chainWebpack(config) {
    ...
    config.plugins.delete('prefetch')

    // 加载配置
    config.plugin('html').tap(args => {
      if (process.env.NODE_ENV === 'production') {
        args[0].cdn = cdn
      }
      return args
    })

    ...
  }
}

index.html:

<!-- 使用CDN的CSS文件 -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="external nofollow" rel="external nofollow" rel="preload" as="style">
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="external nofollow" rel="external nofollow" rel="stylesheet">
<% } %>
<!-- 使用CDN的JS文件 -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
  <!-- <link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="external nofollow" rel="preload" as="script"> -->
  <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>

1. 全局引入:

. 安装:
    npm i element-ui -S. main.js引入:
    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';
    Vue.use(ElementUI);. 弊端:
    a. 打包的文件过大.

2. 按需引入:

. 安装组件:
    yarn add babel-plugin-***ponent -D. 修改babel.config.js:
    {
      "plugins": [
        [
          "***ponent",
          {
            "libraryName": "element-ui",
            "styleLibraryName": "theme-chalk"
          }
        ]
      ]
    }. main.js引入(下面有项目实战):
    import { Button, Select } from 'element-ui';
    import App from './App.vue';
    // 方式一
    Vue.***ponent(Button.name, Button);
    Vue.***ponent(Select.name, Select);
    // 方式二
    Vue.use(Button)
    Vue.use(Select)

(2). 项目中完整组件列表和引入方式 - src/core/lazy_use.js:

import Vue from 'vue'
import {
  Pagination,
  Dialog,
  // Auto***plete,
  // Dropdown,
  // DropdownMenu,
  // DropdownItem,
  // Menu,
  // Submenu,
  // MenuItem,
  // MenuItemGroup,
  Input,
  // InputNumber,
  // Radio,
  // RadioGroup,
  // RadioButton,
  // Checkbox,
  // CheckboxButton,
  // CheckboxGroup,
  Switch,
  Select,
  Option,
  // OptionGroup,
  Button,
  // ButtonGroup,
  Table,
  TableColumn,
  DatePicker,
  // TimeSelect,
  // TimePicker,
  // Popover,
  // Tooltip,
  // Breadcrumb,
  // BreadcrumbItem,
  Form,
  FormItem,
  // Tabs,
  // TabPane,
  // Tag,
  // Tree,
  Alert,
  // Slider,
  // Icon,
  Row,
  Col,
  // Upload,
  // Progress,
  // Spinner,
  // Badge,
  Card,
  // Rate,
  Steps,
  Step,
  // Carousel,
  // CarouselItem,
  // Collapse,
  // CollapseItem,
  // Cascader,
  // ColorPicker,
  // Transfer,
  // Container,
  // Header,
  // Aside,
  // Main,
  // Footer,
  // Timeline,
  // TimelineItem,
  // Link,
  // Divider,
  // Image,
  // Calendar,
  // Backtop,
  // PageHeader,
  // CascaderPanel,
  // Loading,
  MessageBox,
  Message,
  // Notification,
  Drawer
} from 'element-ui'

const maps = {
  Pagination,
  Dialog,
  // Auto***plete,
  // Dropdown,
  // DropdownMenu,
  // DropdownItem,
  // Menu,
  // Submenu,
  // MenuItem,
  // MenuItemGroup,
  Input,
  // InputNumber,
  // Radio,
  // RadioGroup,
  // RadioButton,
  // Checkbox,
  // CheckboxButton,
  // CheckboxGroup,
  Switch,
  Select,
  Option,
  // OptionGroup,
  Button,
  // ButtonGroup,
  Table,
  TableColumn,
  DatePicker,
  // TimeSelect,
  // TimePicker,
  // Popover,
  // Tooltip,
  // Breadcrumb,
  // BreadcrumbItem,
  Form,
  FormItem,
  // Tabs,
  // TabPane,
  // Tag,
  // Tree,
  Alert,
  // Slider,
  // Icon,
  Row,
  Col,
  // Upload,
  // Progress,
  // Spinner,
  // Badge,
  Card,
  // Rate,
  Steps,
  Step,
  // Carousel,
  // CarouselItem,
  // Collapse,
  // CollapseItem,
  // Cascader,
  // ColorPicker,
  // Transfer,
  // Container,
  // Header,
  // Aside,
  // Main,
  // Footer,
  // Timeline,
  // TimelineItem,
  // Link,
  // Divider,
  // Image,
  // Calendar,
  // Backtop,
  // PageHeader,
  // CascaderPanel,
  Drawer
}

// 只有一部分组件是use引入
Object.keys(maps).forEach(item => {
  Vue.use(maps[item])
})

// Vue.use(Loading.directive)

// Vue.prototype.$loading = Loading.service
Vue.prototype.$msgbox = MessageBox
Vue.prototype.$alert = MessageBox.alert
// Vue.prototype.$confirm = MessageBox.confirm
// Vue.prototype.$prompt = MessageBox.prompt
// Vue.prototype.$notify = Notification
Vue.prototype.$message = Message

(3). 项目中main.js引入:

import './core/lazy_use'

// 之前的全部注释掉
// import ElementUI from 'element-ui'
// import 'element-ui/lib/theme-chalk/index.css'
// import locale from 'element-ui/lib/locale/lang/zh-***' // lang i18n

// set ElementUI lang to EN
// Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui,按如下方式声明
// Vue.use(ElementUI)

(4). 打包出来chunck可以放到cdn上.


3. 在index.html中指定版本cdn加载:

<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.***/element-ui@3.x.x/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.***/element-ui@3.x.x/lib/index.js"></script>

(1). 安装插件:

$ yarn add babel-plugin-transform-remove-console -D

(2). 修改babel.config.js文件:

宸汐项目

const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
const plugins = [
  [
    '***ponent',
    {
      'libraryName': 'element-ui',
      'styleLibraryName': 'theme-chalk'
    }
  ]
]
// 只有生产环境去掉console.log
if (IS_PROD) {
  plugins.push('transform-remove-console')
}

module.exports = {
  ...
  plugins
}

(3). 修改babel.config.js文件(vue-cli4):

疾控项目

module.exports = {
  env: {
    development: {
      plugins: ["dynamic-import-node"]
    },
    production: {
      plugins: ["transform-remove-console"]
    }
  }
}

少了1kb左右,在源码中也找不到console.log

(4). 缺点:

. 自己写的console去除了.. index.html内联的runtime代码没去除console,自己单独分离的chunk也没去除.

1. why?

. 如果存在很多过大文件时,会导致可能阻塞后面的进程.. 减少包的大小:
    a. 更快的加载速度以及更好的用户体验.

(1). gzip:

. 是一种 http 请求优化方式:
    a. 通过减少文件体积来提高加载速度
    b. 对于用户量多的网站,开启 gizp 压缩会大大降低服务器压力,提高加载速度、降低服务器流量成本.
    c. 节省了服务器的网络带宽,节约的流量非常可观.. 必须浏览器与服务器都支持gzip.. gzip算法特性:
    a. 代码相似率越大压缩效率越高.

(2). 工作原理图:

. 浏览器发送请求:
    a. 在 request header 中设置属性 a***ept-encoding:gzip
    b. 表示浏览器支持 gzip.. 服务器收到请求后:
    a. 判断浏览器是否支持 gzip:
       (1). 如果支持 gzip,则向浏览器传送压缩过的内容.
       (2). 不支持则向浏览器发送未经压缩的内容.
    b. Response headers返回包含 content-encoding:gzip.. 浏览器接收响应后判断内容是否被压缩,如果被压缩则解压缩显示页面内容:
    a. 浏览器先解压再使用,对于用户是无感知的.

2. 两种 gzip 压缩方式:

. webpack打包生成 .gz 文件:
    a. 通过 webpack 配置生成对应的 .gz 文件.
    b. 浏览器请求 xx.js/css 等文件时,服务器返回对应的 xxx.js.gz 文件.. 服务器实时在线将请求 xx.js 文件进行gzip压缩后传输给浏览器:
    a. 压缩文件过程本身有额外开销.
    b. 服务器压缩的时间开销和 CPU 开销(及浏览器解析压缩文件的开销)为代价,来节省传输过程中的时间开销.

1. 配置:

(1). 安装插件:

. 安装 ***pression-webpack-plugin:
    yarn add ***pression-webpack-plugin@6.1.1 -D. 新版本 7.x 会报错:
    a. Cannot read property 'tapPromise' of undefined

(2). 在 vue.config.js 中配置:

const ***pressionPlugin = require('***pression-webpack-plugin');

module.exports = {
    chainWebpack(config) {
        ...
        // 方式一:
        config
            .when(process.env.NODE_ENV === 'production',
                config => {
                    config
                        .plugin('***pression')
                        .use(***pressionPlugin)
                        .tap(() => [{
                            test: /\.js$|\.html$|\.css$/, // 匹配文件名,开启js、css压缩
                            filename: '[path].gz[query]', // 压缩后的文件名(保持原文件名,后缀加.gz)
                            minRatio: 1, // 压缩率小于1才会压缩
                            threshold: 10240, // 对超过10k的数据压缩
                            deleteOriginalAssets: false // 是否删除未压缩的源文件(不设置或设置为false)
                            // 保留非gzip的资源,删除打包后的gz后还可以加载到原始资源文件,建议不要设置为true
                        }])
                }
            )
        // 方式二:
        if (process.env.NODE_ENV === 'production') {
            config.plugin('***pression-webpack-plugin')
                .use(new ***pressionPlugin({
                    test: /\.js$|\.html$|\.css/,
                    threshold: 10240,
                    deleteOriginalAssets: false
                }))
        }
    }
}. test 另种写法:
    const productionGzipExtensions = ['html', 'js', 'css']
    test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'). 打包后目录会多出 .gz 文件:
    -rw-r--r--  1 xx  staff   42756  3 28 23:01 app.9c5d6e51.js
    -rw-r--r--  1 xx  staff   14495  3 28 23:01 app.9c5d6e51.js.gz
    -rw-r--r--  1 xx  staff   14072  3 28 23:01 chunk-19edcdf1.2e318185.js
    -rw-r--r--  1 xx  staff    4791  3 28 23:01 chunk-19edcdf1.2e318185.js.gz
    // 有些没有gz是因为大小没有超过设定的10k
    -rw-r--r--  1 xx  staff      11  3 28 23:01 chunk-47179b48.01af0134.js
    .... 打包只有一个没有名称的 .gz 文件,并提示:
    warning
    Conflict: Multiple assets emit different content to the same filename static/js/.gz
    ...
    -rw-r--r--   1 xx  staff    48K  3 29 10:39 .gz     // 没有名字的gz文件
    -rw-r--r--   1 xx  staff    39K  3 29 10:39 app.3c690d0c.js
    a. 要修改 filename 的设置为 filename ,老版本为'[path].gz[query]'.

(3). 服务器开启 gzip:

server {
    // 表示静态加载本地的gz文件
    // 浏览器请求xx.js/css等文件时,服务器返回对应的xxx.js.gz文件
    // 服务器会根据Request Headers的A***ept-Encoding标签进行鉴别,如果支持gzip就返回.gz文件.
    // gzip_static开启后,nginx就会读取预先压缩的gz文件,可以减少每次请求进行gzip压缩的CPU资源消耗
    gzip_static on;
    gzip_http_version 1.1;
}

(4). 检查是否开启Gzip成功:

curl -I -H "a***ept-encoding: gzip, deflate" "https://admin.chaidoudou.***/static/css/chunk-elementUI.a8b08852.css"
HTTP/2 200
server: nginx/1.14.0 (Ubuntu)
date: Tue, 30 Mar 2021 15:59:15 GMT
content-type: text/css
content-length: 33216
last-modified: Tue, 30 Mar 2021 08:15:15 GMT
etag: "6062de13-81c0"
content-encoding: gzip

(4). 看***work:

如果发现两个大小不一样,表示Gzip压缩过


2. 分析:

(1). gzip 压缩比率:

. 压缩前:
    a. 整个页面加载完是 8.89s.
    b. 最大的 js 文件加载是 8.31s ,大小为 593k.. 压缩后:
    a. 整个页面加载完是 2.21s.
    b. 最大的 js 文件加载是 1.75s,大小为 146k.. gzip 压缩比率在 4 倍左右.

压缩前:

压缩后:

(2). Request、Response 比对:

. Request Headers:
    a. A***ept-Encoding: gzip, deflate:
       (1). 表示用户浏览器支持二种压缩,包括 gzip 的压缩方式.
       (2). deflate 与 gzip 使用的压缩算法几乎相同.

压缩前的 request:

压缩后的 request:

(3). 其它:

. nginx 配置了静态 gz 加载后,请求文件变小不会导致请求卡线程.. 保留了源文件,当删除 gz 后,浏览器会自动去请求原始文件,不会导致界面出现任何问题.. 静态加载 gz 文件的响应头:
    Content-Encoding: gzip
转载请说明出处内容投诉
CSS教程_站长资源网 » 前端体系之【全链路体系优化工程介绍与落地实践】(含优化前后比较)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买