(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
④. 在需要使用它的时候,可以通过CMD、AMD、或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