1 | //创建以为人类 |
建造者模式和工厂模式的区别
工厂模式创建出来的是一个对象,它追求的是创建的结果,创建的过程不是重点,而建造者模式不仅可以得到创建的结果,也参与了创建的具体过程,对于创建的具体实现的细节也参与了干涉,创建的对象更复杂,这种模式创建的对象时一个复合对象
1 | //创建以为人类 |
工厂模式创建出来的是一个对象,它追求的是创建的结果,创建的过程不是重点,而建造者模式不仅可以得到创建的结果,也参与了创建的具体过程,对于创建的具体实现的细节也参与了干涉,创建的对象更复杂,这种模式创建的对象时一个复合对象
只提供一个类
1 | //蓝球基类 |
抽象工厂其实就是一个实现子类继承父类的方法,在这个方法中我们通过传递子类以及要继承父类(抽象类)的名称,在抽象工厂方法中增加一次对抽象类存在性的一次判断,如果存在,则将子类继承父类的方法,然后子类通过寄生式继承,继承父类过程中,在对过渡类的原型继承时,我们不是继承父类的原型,而是通过new复制父类的一个实例,这么做是因为过渡类不应该仅仅继承父类的原型方法,还要继承父类的对象属性,通过new关键字将父类构造函数执行一遍来复制构造函数中的属性和方法。
1 | //抽象工厂方法 |
vue.config.js:
1 | const UglifyJsPlugin=require('uglifyjs-webpack-plugin') |
把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加,以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件
1 | routes:[{ |
在日常使用的UI框架,例如element-plus,我们经常直接使用整个UI库
1 | import ElemtPlus from 'element-plus' |
但实际上我用到的组件只有按钮,分页,表格,输入与警告,所以我们需要按需引用
1 | import {Button,Input,Table,TableColumn,MessageBox} from 'element-plus' |
后端返回资源问题:
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头Service Worker
离线缓存前端合理利用localStorage
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
对于所有的图片资源,我们可以进行适当的压缩
对页面上使用到的icon
,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http
请求压力。
拆完包之后,我们再用gzip
做一下压缩 安装compression-webpack-plugin
1 | npm i compression-webpack-plugin -D |
vue.config.js
1 | const CompressionPlugin = require('compression-webpack-plugin') |
SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
从头搭建一个服务端渲染是很复杂的,vue
应用建议使用Nuxt.js
实现服务端渲染
一个包含vue-loader的简单webpack配置:
1 | const MiniCssExtractPlugin=require('mini-css-extract-plugin') |
通过vue-loader,webpack可以将.vue文件转化为浏览器可识别的javascript
将一个.vue文件分割成template,script,styles三部分
template部分通过compile生成render,staticRenderFns
获取script部分返回的配置对象scriptExports
styles部分,会通过css-loader,vue-style-loader,添加到head中,或者通过css-loader,MiniCssExtractPlugin提取到一个公共的css文件
使用vue-loader提供的normalizeComponent方法,合并scriptExports,render,staticRenderFns,返回构建vue组件需要的配置对象-options,即{data,props,methods,render,staticRenderFns…}
1 | // 从 template区域块 获取 render、 staticRenderFns 方法 |
当.vue文件中的style标签有scoped属性,它的css样式只作用于当前组件的元素
css scoped 的 工作流程 如下:
经历上述过程,style(scoped) 中的样式就变成了 组件的私有样式。
我们可以通过 >>> 操作符, 在 组件 中修改 子组件 的 私有样式。
1 | // child component |
有些像 Sass 之类的 预处理器 无法 **正确解析 >>>**。这种情况下我们可以使用 /deep/ 或 ::v-deep 操作符取而代之,两者都是 >>> 的 别名,同样可以正常工作。
深度作用选择器, 必须在含有 scoped 属性 的 style 标签中使用,否则无效。 这是因为 >>>、/deep/、::v-deep 需要被 postCSS 解析才能起作用。 只有 style 标签 中有 scoped 属性, 样式内容 才会被 postCSS 解析。
postCSS 解析样式内容的时候, 会给 >>> 操作符 前面 的 css选择器 添加 **[data-v-hash]**。
注意: 父组件 中修改 子组件 的 私有样式 时, 父组件 中的 样式的优先级 要大于 子组件 中的 样式的优先级, 否则会导致 父组件中定义的样式不生效。
我们也可以在 .vue 文件 的 style 标签 上添加 module 属性, 使得 style 标签 中的 样式 变为 组件私有,具体使用方法详见 - 官网。
css modules 和 css scoped 都可以使 样式 变为 组件私有,但是 原理 不一样。
css scoped 的实质是利用 css属性选择器 使得 样式 称为 局部样式,而 css modules 的实质是让 样式的类名、id名唯一 使得 样式 称为 局部样式。
css modules 的 工作流程 如下:
1 | import { render, staticRenderFns } from "./App.vue?vue&type=template&id=3512ffa2&scoped=true&" |
1 | exports = module.exports = require("../node_modules/_css-loader@3.2.0@css-loader/dist/runtime/api.js")(false); |
在处理过程中, css-loader 会将样式中的 类名、id名 等用一个 唯一的命名代替。
在执行 步骤1 的代码时,会执行上面的代码, 返回一个 对象, 即 步骤一 中的 style0 、style1, 格式如下:
1 | // css样式内容会通过 style-loader 提供的方法添加到 head 中 |
开发模式 下,当使用 vue-loader、 vue-style-loader 处理 .vue 文件 的时候, 会向 生成的js代码 中注入与 热更新 相关的代码逻辑。 当我们修改 .vue 文件 时, dev-server 会通知 浏览器 进行 热更新。
.vue 文件 的 各个区域块(template、script、styles) 对应的 热更新逻辑 都不一样。
vue-loader 会在 打包代码 中注入 热更新 template、script 区域块 的代码,如下:
1 | // 从 template区域块 获取 render、 staticRenderFns 方法 |
如果我们只修改了 .vue 文件 的 script 部分, 客户端(即浏览器) 会进行 热更新, 过程如下:
vue-style-loader 会在 打包代码 中注入 热更新 style区域块 的代码, 如下:
1 | ar add = require("!../node_modules/_vue-style-loader@4.1.2@vue-style-loader/lib/addStylesClient.js").default |
如果我们修改了 .vue 文件 的 styles 区域块,客户端(即浏览器) 会进行 热更新, 过程如下:
1 | // 热更新代码 |
执行上述 热更新代码, 会 更新 vue实例 的 $style 属性, 然后触发 vue 实例 的 $forceUpdate 方法, 重新渲染。
一个 style 区域块 对应一个 style 标签。修改某一个 style 区域块 之后,会更新对应的 style 标签。
style 区域块 的 热更新 和 template、script 区域块的 热更新 互不影响。
生产模式 下, webpack 默认启用 tree shaking。如果此时项目 根目录 中的 package.json 中的 sideEffects 的值为 false,且 .vue 文件 的 style 标签 没有 module 属性,使用 vue-loader 处理 .vue 文件 的时候, 会产生 样式丢失 的情况,即 styles 区域块 不会添加到 head 中或者 被提取到公共的css文件中。
首先,先看一下 .vue 文件 经过处理以后生成的 js代码, 如下:
1 | / 从 template区域块 获取 render、 staticRenderFns 方法 |
在上面的代码中,template 区域块 返回的 render、staticRenderFns, script 区域块 返回的 scriptExports, 都有被 normalizer 方法使用, 而 styles 区域块 返回的 style0、style1 则没有被使用。 在 打包代码 的时候, tree shaking 就会自动移除 styles 区域块 对应的代码,导致 样式丢失。
解决方法:
1 | "sideEffects": [ |
有了上述配置, webpack 在处理 .vue 文件的时候, 不会使用 tree shaking, 不会出现样式丢失的问题。
但是这种解决方法有一个问题, 如果 script 区域块 中通过 import 的方式引入了 未使用的模块,未使用的模块在最后打包代码的时候不会被删除。
1 | // webpackConfig: |
上述配置, 明确说明了 .vue 文件 的 style 区域块 在使用 tree shaking 的时候, 会有 副作用,在打包的时候不会删除。
这样的话,样式不会丢失, 并且如果 script 区域块 中通过 import 的方式引入了 未使用的模块,未使用的模块在最后打包代码的时候会被删除
本地工作流是本地工具链阶段的前端工程体系所对应的工作模式,此阶段的各个功能模块均由开发人员在本机环境下执行,所有功能模块的两个要素:
基本原则是单独编写一个适应各环境的“配置文件”,假设我们将此文件命名为manifest.js:
1 | const Domain=window.location.host; |
将主站域名区分测试环境和生产环境,并将对应的异步API域名以全局变量的形式暴露出来。随后在业务逻辑代码main.app.js中使用全局变量:
1 | $.ajax({ |
manifest.app.js可以作为一个通用模块不参与构建,并且必须在业务js文件之前引入:
1 | <script type="text/javascript" src="//static.app.com/common/manifest.js"> |
manifest.js作为一个通用模块,无法保证更新后完全兼容历史项目
代码分离的本质是架构层面的细节设计,架构是跟着业务需求的变动而不断改变的,所以manifest.js只能作为一种临时性的解决方法,而不是从工程角度出发的高度可适应方案
测试沙箱的原则是搭建一个仿真的生产环境,在工作流中加入测试沙箱的支持后,前端只需执行一次针对生产环境的构建行为即可,测试通过后可直接部署上线,无需二次构建
测试沙箱只需要模拟一个前端仿真环境,比如实现域名的映射,最原始的方案就是通过修改测试人员本地host文件实现,如果要统一规范可以搭建一个专属的VPN服务,所有参与测试的任意统一使用此VPN代理访问测试沙箱。测试沙箱的复杂度取决于生成环境的复杂度
云平台工作流在本地工作流的基础上,将容易因个体差异产生问题的功能模块(比如构建,部署等)提升到云平台运行,通过严谨的流程控制增强开发规范性,云平台的目标不仅是实现功能的集中管理,而且要在此基础上进一步优化工作流程。功能集中的同时以为着权限集中,这也是实现自动构建和自动部署的必要前提
开发人员负责一线开发工作
开发负责人汇总开发人员的各个分支并将其合并到dev分支
云平台管理人员负责项目的发起以及部署队列的控制
将webpack构建输出的文件存储在内存中,正常情况下,webpack构建产出的文件会存储在output配置项指定的硬盘目录中,webpack-dev-middleware在此基础上建立一个文件映射机制,每当匹配到一个webpack构建产出文件的请求后便会将内存中与其对应的数据返回给发起请求的客户端,由于是内存的文件系统,没有好使的硬盘读写,数据更新很快,这也是webpack相较其他同类工具的优势之一
实际上,webpacl-dev-server是在Express和webpack-dev-middleware基础上进行的封装,但由于不具备Mock服务,我们需要自己封装本地开发服务器,注意:
实际开发中并非所有的静态文件都参与构建,一些常用第三方库通常用单独的script和link标签引入,此类文件不参与构建,不在webpack-dev-middleware的监听范围内,也就不能通过文件映射策略将其对应的请求映射到内存文件系统中,只能借助express内置的static中间件将这些文件作为静态内容开放给Http服务,
1 | app.use('/libs',express.static(path.join(process.cwd(),'static'))) |
第一个参数/libs是客户端请求静态资源的路径,express.static的参数时本地存放静态资源的绝对路径,上述代码将/libs路径的http请求映射到本地项目的static目录
1 | const express=require('express') |
Mock进化的第二种形态就是以Mock.js为代表的客户端Mock,工作原理是在客户端拦截JavaScript代码发出的ajax请求并返回由Mock.js创建的假数据
1 | <script src="/libs/js/mock.js"></script> |
Mock.js可以随机创建假数据,还可以处理各种异常状态,然而几遍客户端Mock提供了许多便利,Mock相关代码或文件仍然必须存在于业务代码中,上线之前仍然需要删除,这对于产品质量保障始终存在一定隐患。
在开发阶段使用Mock Server提供的与真实接口规范和逻辑一致的本地接口进行开发
开发完成后,在构建阶段将Mock的地址修改为已完成的真实服务器端接口地址
Mock Server最普遍的使用场景就是模拟异步数据接口,比如使用Ajax或者JSONP获取和提交数据,模拟的方式通常有两种:
Mock Server本质上是一个简化版的Web Server,最基础的组件就是负责分发的路由
在开发阶段使用本地API代替真实API地址,使用本地JSON作为接口的返回数据,具体搭建流程:
通过路由创建一个可访问的本地域名API代替真实API,比如使用/login代替http://auth.app.com/login
在路由响应函数内对请求进行校验,比如是否为JSONP请求,然后返回本地JSON数据
前端工程师在开发阶段的业务代码中将http://auth.app.com/login修改为API/login
1 | const Path=require('path') |
开发环境使用Mock Server将所有真实接口地址修改为本地域名地址,在部署测试和生产环境之前必须将接口的地址复原。
接口地址的修改需求涉及两个方面:
执行环境:开发,测试,生产环境下的接口地址均不同,所以必须能够依据部署目标环境将接口修改为对应地址
字符串修改:对于JavaScript代码来说,接口地址就是一个字符串,我们要做的就是将该字符串修改为指定的值
1 | entry:{ |
源代码
1 | //请求位于auth.app.com域名下的login接口 |
经过构建后
1 | //请求位于auth.app.com域名下的login接口 |
结合环境变量,配置DefinePlugin指定不同环境下的替换值
1 | new webpack.DefinePlugin({ |
以上代码在开发环境下将接口映射到本地的Mock Server,非开发环境下修改为真实域名的地址
CDN(Connect Delivery Network)是一种部署策略,包括分布式存储,负载均衡,内容管理等模块,CDN的一个重要功能是将静态资源缓存到用户近距离的CDN节点上,不但能提高用户对静态资源的访问速度,还能节省服务器的带宽消耗,降低负载。实现此功能的一个重要前提是将静态资源部署到已接入CDN的专属服务器,而这类服务器通常与Web主页面处于不同域名下,这样做的主要目的是为了充分利用浏览器的并发请求能力,提高页面的加载速度。
1 | { |
多页面项目资源定位
编写一个html-wepack-plugin-before-html-processing插件,保证在html-wepack-plugin-before-html-processing阶段能够执行以下行为:
compiler对象代表的是webpack执行环境的完整配置,只会在启动webpack时被创建,并且在webpack运行期间不会被修改
compilation对象代表某个版本的资源对应的编译进程,当使用webpack的development中间件时,每次检测到项目文件有改动会创建一个compilation,进而能够针对改动生产全新的编译文件,compilation对象包含当前模块资源,待编译文件,有改动的文件和监听依赖的所有信息
编写一个构造函数用于接收配置:
1 | const HtmlWebpackPluginForLocate=function(options){ |
html-webpack-plugin-before-html-processing阶段捕获两个对象,callback的作用类似于Express中间件中的next函数,执行完当前插件的逻辑后必须调用callback以便进入后序流程,它属于webpack流程控制的一部分,没有构建相关的信息
实现方案是在引用资源的URL后添加请求参数,比如添加时间戳参数,
浏览器会将参数不同的URL视为全新的URL,所以浏览器向服务器请求并下载最新资源,我们只更新修改了的资源,为了避免手动改参数,将hash指纹(通过mds算法得到)作为URL的v参数
1 | <head> |
缺点:
将原本作为参数值的hash指纹作为资源文件名的一部分并且删除用于更新的url参数
1 | <head> |
在静态资源使用增量更新前提下可以将静态资源先于动态HTML部署,此时静态资源没有引用入口,不会对线上环境产生影响;动态HTML部署后即可在第一时间访问已存的最新静态资源。解决覆盖更新部署同步问题
增量更新修改了资源文件名,不会覆盖已存的旧版本文件,运维人员进行回滚操作只需回滚HTML即可,优化了版本控制,支持多版本共存
假设一个页面有主模块main.app.js,同步模块module.sync.js,构建后与主模块合并为主文件的main.app.[hash].js,同步加载,异步模块module.async.js,构建后为异步文件app.async.[hash].js
缺陷:当更新异步文件的hash指纹,主文件的hash没有同步修改,不能获取到最新资源
解决:
webpack中的chunkhash:
hash与chunkhash:
hash:the hash of compilation,webpack的compilation对象代表某个版本资源对应的编译进程,当使用webpack的development中间件,每次检测到项目文件有改动就会创建一个compilation,进而能够针对改动生产全新的编译文件。compilation对象不是针对单个文件,是针对项目中所有参与构建的文件,只要任何一个文件改动,compilation对象就改变,作为compilation的hash值就相应改变,因此不论同步文件还是异步文件构建输出后均有相同hash,显然不合理
chunk在webpack中含义可以理解为散列模块经合并后的“块”,比如同步模块module.sync.js和主模块main.app.js合并为一个“块”,异步模块module.async.js是另一个块,chunkhash就是一个个块依据自身代码内容计算所得的hash值。
1 | output:{ |
主文件 | 异步文件 | |
---|---|---|
原始状态 | main.app.8d136fcd.js | app.async.67fa68a0.js |
修改主模块main.app.js | hash改变 | hash不变 |
修改同步模块modue.sync.js | hash改变 | hash不变 |
修改异步模块module.async.js | hash改变 | hash改变 |
contenthash
webpack默认将CSS代码合并到js文件,要把CSS文件抽离处理独立维护,既有利于浏览器的渲染优化,又能够更好利用客户端缓存
1 | { |
contenthash就是解耦js与css文件hash指纹的关键,contenthash是由ExtractTextPlugin插件提供,代表被导出内容计算后的hash值,如果不用contenthash,main.app.css不会参与chunk的chunkhash计算,但也不会作为一个独立的Chunk进行单独计算,编译后的css文件取值与主文件相同
使用contenthash:
主文件hash | 异步文件hash | CSS文件hash | |
---|---|---|---|
修改主模块 | 变 | 不变 | 不变 |
修改同步模块 | 变 | 不变 | 不变 |
修改异步模块 | 不变 | 变 | 不变 |
修改CSS | 不变 | 不变 | 变 |
CSS的弱编程能力,CSS通过”delector-properties”的模式为HTML文档增加样式,但CSS不支持嵌套,运算,变量,复用等。
提供便捷的语法和特性供开发者编写源代码,随后经过专门的编译工具将源代码转化为CSS语法,最早的CSS预编译器是2007年起源于Ruby on Rails社区的SASS,目前不叫流行的如LESS,Stylus在一定程度上收到SASS影响
1 增强编程能力
2 增强源码可复用性,让CSS开发符合DRY(Don’t repeat yourself)的原则
3 增强源码可维护性
4 更便于解决浏览器兼容性
实现
嵌套是所有预编译器都支持的语法特性,mixin/继承是为了解决hack和代码复用,变量和运算增强了源码的可编程能力;模块化的支持不仅更利于代码复用,同时提高了源码的可维护性
PostCSS鼓励开发者使用规范的CSS原生语法编写源代码,然后配置浏览器需要兼容的浏览器版本,最后经过编译将源码转化为目标浏览器可用的CSS代码。PostCSS提供了丰富的插件用于实现不同场景的编译需求,最常用的比如autoprefix,Sprited等
PostCSS不是另一种CSS预编译器,与SASS LESS等预编译器不冲突,目前普遍方案将CSS预编译与PostCSS综合:
使用CSS预编译器弥补CSS源码的弱编程能力。比如变量,运算,继承等
使用PostCSS处理针对浏览器的需求,比如autoprefix,自动CSS Sprites
webpack结合预编译与PostCSS实现CSS构建
1 | { |
css-loader中的importLoaders选项的作用是:用于配置css-loader作用于@import的资源之前需要经过的其他loader的个数,@import用于CSS源码中引用其他模块的关键字,如果你的项目中确定不会涉及到模块化,可以忽略此配置项
如果需要将编译后的css文件独立导出,则需将style-loader替换为extract-text-webpack-plugin
1 | { |
css-loader:用于解析css源文件并获得其引用资源,比如@import引用的模块,url()引用的图片等,然后根据webpack配置编译这些资源
style-loader:负责将CSS代码通过style标签插入HTML文档中,所以如果独立导出CSS文件就不再需要style-loader,css-loader必须在style-loader之前执行
路由就是SPA(单页应用)的路径管理器,vue-router的单页应用中,则是路径之间的切换,也就是组件的切换,路由模块的本质就是建立起url和页面之间的映射关系
为什么不能用a标签,这是因为Vue左的是单页应用,当你的项目准备打包时,会生成dist文件夹,这里面只有静态资源和一个index.html页面,所有a标签跳转页面不起作用
单页应用的核心之一就是:更新视图而不重新请求页面,vue-router在实现单页面前端路由时,提供两种方式:Hash模式和History模式,根据mode参数来决定使用哪一种
vue-router默认hash模式,使用URL的hash模拟一个完整的URL,于是URL改变时,页面不会重新加载,hash(#)是URL的锚点,代表网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载页面,也就是hash出现在URL中(#后面的值),但不会被包含在http请求中,对后端没有影响,因此改变hash不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用“后退”按钮,就可以回到上一个位置,所以Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据,hash模式原理是onhashchange事件(监测hash值变化),可以在window对象上监听这个事件
1 | class Router{ |
hash模式在url中自带#,比较丑,可以用路由的history模式,只需要在配置路由规则时,加上’mode:history’
这种模式利用了html5 history Interface中新增的pushState()和replaceState()方法,这两个方法应用于浏览器记录栈,在当前已有的back,forwarc,go基础上,它们提供了对历史记录修改的功能,只是当修改时,虽然改变了当前的URL,但浏览器不会去请求服务器该路径下的资源,一旦刷新就会暴露,显示404,因此这种模式下需要后端的支持,在服务端增加一个覆盖所有情况的候选资源:如果URL匹配不到任何静态资源,就返回一个Index.html页面,这个页面就是app依赖的页面
export const routes=[
{path:”*”,redirect:’/‘}
]
history.pushState用于在浏览器中添加历史记录,但不触发跳转,此方法接收三个参数:
state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数,不需要这个对象时此处就填null
title:新页面标题,但是所有浏览器目前都忽略这个值,因此这里填null
url:新的网址,必须与当前页面处在同一个域,浏览器的地址栏将显示这个网址
新标准下路由的实现
1 | class Router{ |