(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'constgetProdExternals=()=>{return{'vue':'Vue',// 'vue-router': 'VueRouter',// 'vuex': 'Vuex'}}
module.exports ={...configureWebpack:{...externals: isProd ?getProdExternals():{}}}
②. 在public/index.html文件中引入vue cdn路径:<script src="//cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
a. 不写协议前缀,会与网站的协议相同.所以,可以不写https.
②.'vue':'Vue'说明:
a. key是node模块名称,value是项目中对模块的引用
b. 前面的vue是代码中importAfromB中的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.com/vue/2.6.10/vue.min.js','//cdn.bootcss.com/vue-router/3.0.6/vue-router.min.js','//cdn.bootcss.com/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) { %>
<linkhref="<%= htmlWebpackPlugin.options.cdn.css[i] %>"rel="external nofollow"rel="external nofollow"rel="preload"as="style"><linkhref="<%= 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"> --><scriptsrc="<%= 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-component -D
②. 修改babel.config.js:{"plugins":[["component",{"libraryName":"element-ui","styleLibraryName":"theme-chalk"}]]}
③. main.js引入(下面有项目实战):import{ Button, Select }from'element-ui';import App from'./App.vue';// 方式一
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);// 方式二
Vue.use(Button)
Vue.use(Select)
(2). 项目中完整组件列表和引入方式 - src/core/lazy_use.js:
import Vue from'vue'import{
Pagination,
Dialog,// Autocomplete,// 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,// Autocomplete,// 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.serviceVue.prototype.$msgbox = MessageBox
Vue.prototype.$alert = MessageBox.alert
// Vue.prototype.$confirm = MessageBox.confirm// Vue.prototype.$prompt = MessageBox.prompt// Vue.prototype.$notify = NotificationVue.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-CN' // 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.com/[email protected]/lib/theme-chalk/index.css"><!-- 引入组件库 --><script src="https://unpkg.com/[email protected]/lib/index.js"></script>
(1). 安装插件:
$ yarn add babel-plugin-transform-remove-console -D
(2). 修改babel.config.js文件:
宸汐项目
constIS_PROD=['production','prod'].includes(process.env.NODE_ENV)const plugins =[['component',{'libraryName':'element-ui','styleLibraryName':'theme-chalk'}]]// 只有生产环境去掉console.logif(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 中设置属性 accept-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). 安装插件:
①. 安装 compression-webpack-plugin:
yarn add [email protected]
②. 新版本 7.x 会报错:
a. Cannot read property 'tapPromise'ofundefined
(2). 在 vue.config.js 中配置:
const CompressionPlugin =require('compression-webpack-plugin');
module.exports ={chainWebpack(config){...// 方式一:
config
.when(process.env.NODE_ENV==='production',config=>{
config
.plugin('compression').use(CompressionPlugin).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('compression-webpack-plugin').use(newCompressionPlugin({test:/\.js$|\.html$|\.css/,threshold:10240,deleteOriginalAssets:false}))}}}
①. test 另种写法:const productionGzipExtensions =['html','js','css']test:newRegExp('\\.('+ productionGzipExtensions.join('|')+')$')
②. 打包后目录会多出 .gz 文件:-rw-r--r--1 xx staff 4275632823:01 app.9c5d6e51.js
-rw-r--r--1 xx staff 1449532823:01 app.9c5d6e51.js.gz
-rw-r--r--1 xx staff 1407232823:01 chunk-19edcdf1.2e318185.js
-rw-r--r--1 xx staff 479132823:01 chunk-19edcdf1.2e318185.js.gz
// 有些没有gz是因为大小没有超过设定的10k-rw-r--r--1 xx staff 1132823: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 32910:39.gz // 没有名字的gz文件-rw-r--r--1 xx staff 39K 32910:39 app.3c690d0c.js
a. 要修改 filename 的设置为 filename ,老版本为'[path].gz[query]'.
(3). 服务器开启 gzip:
server {// 表示静态加载本地的gz文件// 浏览器请求xx.js/css等文件时,服务器返回对应的xxx.js.gz文件// 服务器会根据Request Headers的Accept-Encoding标签进行鉴别,如果支持gzip就返回.gz文件.// gzip_static开启后,nginx就会读取预先压缩的gz文件,可以减少每次请求进行gzip压缩的CPU资源消耗
gzip_static on;
gzip_http_version 1.1;}
(4). 检查是否开启Gzip成功:
curl -I-H"accept-encoding: gzip, deflate""https://admin.chaidoudou.cn/static/css/chunk-elementUI.a8b08852.css"HTTP/2200server: nginx/1.14.0(Ubuntu)date: Tue,30 Mar 202115:59:15GMT
content-type: text/css
content-length:33216
last-modified: Tue,30 Mar 202108:15:15GMTetag:"6062de13-81c0"
content-encoding: gzip
(4). 看Network:
如果发现两个大小不一样,表示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. Accept-Encoding: gzip,deflate:(1). 表示用户浏览器支持二种压缩,包括 gzip 的压缩方式.(2). deflate 与 gzip 使用的压缩算法几乎相同.
压缩前的 request:
压缩后的 request:
(3). 其它:
②. nginx 配置了静态 gz 加载后,请求文件变小不会导致请求卡线程.
③. 保留了源文件,当删除 gz 后,浏览器会自动去请求原始文件,不会导致界面出现任何问题.
④. 静态加载 gz 文件的响应头:
Content-Encoding: gzip
版权归原作者 完美句号 所有, 如有侵权,请联系我们删除。