0%

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//创建以为人类
var Human=function(param){
this.skill=param&&param.skill||'保密';
this.hobby=param&&param.hobby||'保密'
}
//类人原型方法
Human.prototype={
getSkill:function(){
return this.skill
},
getHobby:function(){
return this.hobby
}
}
//实例化姓名类
var Named=function(name){
var that=this
//构造器
//构造函数解析姓名的姓与名
(function(name,that){
that.wholeName=name
if(name.indexOf(' ')>-1){
that.FirstName=name.splice(0,name.indexOf(' '))
that.SecondName=name.splice(name.indexOf(' '))
}
})(name,that)
}
//实例化职位类
var Work=function(work){
var that=this
(function(work,that){
switch(work){
case 'code':
that.work='工程师';
that.workDescript='每天沉迷于编程'
break;
case 'UI':
case 'UE':
that.work='设计师';
that.workDescript='设计是一种艺术'
break;
case 'teacher':
that.work='教师';
that.workDescript='分享是一种快乐'
break;
default:
that.work=work
that.workDescript='对不起,无您选择的职位'
}
})(work,that)
}
//更好期望的职位
Work.prototype.changeWork=function(work){
this.work=work
}
Work.prototype.changeDescript=function(sentecnce){
this.workDescript=sentecnce
}
//应聘者建造者
//参数 name work
var Person=function(name,work){
//创建应聘者缓存对象
var _person=new Human()
//创建应聘者姓名解析对象
_person.name=new Named(name)
//创建应聘者工作解析对象
_person.work=new Work(work)
return _person
}
var person=new Person('xiao ming','code')
console.log(person.skill)//保密
console.log(person.name.FirstName)//xiao
console.log(person.work.work)//工程师

建造者模式和工厂模式的区别

工厂模式创建出来的是一个对象,它追求的是创建的结果,创建的过程不是重点,而建造者模式不仅可以得到创建的结果,也参与了创建的具体过程,对于创建的具体实现的细节也参与了干涉,创建的对象更复杂,这种模式创建的对象时一个复合对象

简单工厂模式

只提供一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//蓝球基类
var Basketball=function(){
this.intro="篮球"
}
Basketball.prototype={
getMember:function(){
console.log('每个队伍5名队员')
},
getBallSize:function(){
console.log('蓝球很大')
}
}
//足球基类
var Football=function(){
this.intro='足球'
}
Football.prototype={
getMember:function(){
console.log('每个队伍需要11名队员')
},
getBallSize:function(){
console.log('租足球很大')
}
}
//运动工厂
var SportFactory=function(name){
switch(name){
case 'NBA':
return new Basketball();
case 'wordCup':
return new Football()
}
}
//为世界杯创建一个足球,只需要记住运动工厂,调用并创建
var football=SportFactory('wordCup')
console.log(football)
console.log(football.intro)
football.getMember()

抽象工厂模式

抽象工厂其实就是一个实现子类继承父类的方法,在这个方法中我们通过传递子类以及要继承父类(抽象类)的名称,在抽象工厂方法中增加一次对抽象类存在性的一次判断,如果存在,则将子类继承父类的方法,然后子类通过寄生式继承,继承父类过程中,在对过渡类的原型继承时,我们不是继承父类的原型,而是通过new复制父类的一个实例,这么做是因为过渡类不应该仅仅继承父类的原型方法,还要继承父类的对象属性,通过new关键字将父类构造函数执行一遍来复制构造函数中的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//抽象工厂方法
var VehicleFactory=function(subType,superType){
//判断抽象工厂中是否有该抽象类
if(typeof VehicleFactory[superType]==='function'){
//缓存类
function F(){ }
//继承父类属性和方法
F.prototype=new VehicleFactory[superType]()
//将子类constructor指向子类
subType.constructor=subType
//子类原型继承父类
subType.prototype=new F()
}else{
//不存在抽象类抛出错误
throw new Error('未创建该抽象类')
}
}
//汽车抽象类
VehicleFactory.Car=function(){
this.type='car'
}
VehicleFactory.Car.prototype={
getPrice:function(){
return new Error('抽象方法不能调用')
},
getSpeed:function(){
return new Error('抽象方法不能调用')
}
}
//公共车抽象类
VehicleFactory.Bus=function(){
this.type='bus'
}
VehicleFactory.Bus.prototype={
getPrice:function(){
return new Error('抽象方法不能调用')
},
getSpeed:function(){
return new Error('抽象方法不能调用')
}
}
//货车抽象类
VehicleFactory.Truck=function(){
this.type='truck'
}
VehicleFactory.Truck.prototype={
getPrice:function(){
return new Error('抽象方法不能调用')
},
getSpeed:function(){
return new Error('抽象方法不能调用')
}
}
//宝马汽车子类
var BMW=function(price,speed){
this.price=price;
this.speed=speed

}
//抽象工厂实现对Car抽象类继承
VehicleFactory(BMW,'Car')
BMW.prototype.getPrice=function(){
return this.price
}
BMW.prototype.getSpeed=function(){
return this.speed
}
//兰博基尼汽车子类
var Lamborghini=function (price,speed) {
this.price=price;
this.speed=speed
}
//抽象工厂实现对Car抽象类继承
VehicleFactory(Lamborghini,'Car')
Lamborghini.prototype.getPrice=function(){
return this.price
}
Lamborghini.prototype.getSpeed=function(){
return this.speed
}

vue.config.js:

1 UglifyPlugin Webpack Plugin 插件用来缩小js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const UglifyJsPlugin=require('uglifyjs-webpack-plugin')
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/Vue-chat/'
: '/' ,
// 输出文件目录
outputDir: 'dist',
// eslint-loader 是否在保存的时候检查
lintOnSave: true,
// use the full build with in-browser compiler?
// https://vuejs.org/v2/guide/installation.html#Runtime-Compiler-vs-Runtime-only
// compiler: false,
// webpack配置
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
chainWebpack: () => {},
//代码压缩
configureWebpack:{
plugins:[
new UglifyJsPlugin({
uglifyOptions:{
compress:{
drop_debugger:true,
drop_console:true,
pure_funcs:['console.log']//删除console.log语句
}
},
sourceMap:false,
parallel:true
})
],
},
// vue-loader 配置项
// https://vue-loader.vuejs.org/en/options.html
// vueLoader: {},
// 生产环境是否生成 sourceMap 文件
productionSourceMap: true,
// css相关配置
// css: {
// // // 是否使用css分离插件 ExtractTextPlugin
// extract: true,
// // 开启 CSS source maps?
// sourceMap: false,
// // // css预设器配置项
// loaderOptions: {
// css:{},//这里的选项会传递给css-loader
// postcss:{}//这里的选项会传递给postcss-loader
// },
// // // 启用 CSS modules for all css / pre-processor files.
// requireModuleExtension: false
// },
// use thread-loader for babel & TS in production build
// enabled by default if the machine has more than 1 cores
parallel: require('os').cpus().length > 1,// 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。
// 是否启用dll
// See https://github.com/vuejs/vue-cli/blob/dev/docs/cli-service.md#dll-mode
// dll: false,
// PWA 插件相关配置
// see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
pwa: {},
// webpack-dev-server 相关配置
devServer: {
open: process.platform === 'darwin',
disableHostCheck: true,
port:8081,
https: false,
hotOnly: false,

before: app => {}
},

// 第三方插件配置
pluginOptions: {
// ...
}
}

2 路由懒加载:

把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加,以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件

1
2
3
4
5
6
routes:[{
path:'/',
name:"通讯录",
component:()=>Promise.resolve(import("../components/contact/contact.vue"))

},

3 UI框架按需加载

在日常使用的UI框架,例如element-plus,我们经常直接使用整个UI库

1
import ElemtPlus from 'element-plus'

但实际上我用到的组件只有按钮,分页,表格,输入与警告,所以我们需要按需引用

1
2
import {Button,Input,Table,TableColumn,MessageBox} from 'element-plus'

4 静态资源本地缓存

后端返回资源问题:

  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头
  • 采用Service Worker离线缓存

前端合理利用localStorage

5 图片资源的压缩

图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素

对于所有的图片资源,我们可以进行适当的压缩

对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。

开启GZip压缩

拆完包之后,我们再用gzip做一下压缩 安装compression-webpack-plugin

1
npm i compression-webpack-plugin -D

vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}

6 使用SSR

SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器

从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染

一个包含vue-loader的简单webpack配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const MiniCssExtractPlugin=require('mini-css-extract-plugin')
const {VueLoadPlugin}=require('vue-loader')
const isProduction=process.evv.NODE_ENV==='production'
const extractLoader={
loader:MiniCssExtractPlugin.loader,
options:{
publicPath:'../',
hmr:process.env.NODE_ENV==='development'//开发环境下打开热更新
}
}
const cssExtractPlugin=new MiniCssExtractPlugin({
filename:'[name].css',
chunkFilename:'[id].css',
ignoreOrder:false
})
const webpackConfig={
entryd:{...},
output:{...},
optimization:{...},
resolve:{...},
modules:{
rules:[{
test:/\.vue$/,
loader:'vue-loader'
},{
test:/\.css$/,
oneof:[{
resourceQuery:/\?vue/,
use:[isProduction?extractLoader:'vue-style-loader','css-loader']
},{
use:[isproduction?extractLoader:'style-loader','css-loader']

}
]
}]
},
plugins:[
new VueLoaderPlugin(),
isProduction?cssExtractPlugin:''
]


}

vue-loader工作原理

通过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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 从 template区域块 获取 render、 staticRenderFns 方法
    import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
    // 从 script区域块 获取 组件的配置项对象
    import script from "./App.vue?vue&type=script&lang=js&"
    export * from "./App.vue?vue&type=script&lang=js&"
    // 获取 styles区域块的内容
    import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
    // 获取 styles(scoped)区域块的内容
    import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"


    /* normalize component */
    import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
    // 返回构建组件需要的配置项对象, 包含 data、props、render、staticRenderFns 等
    var component = normalizer(
    script,
    render,
    staticRenderFns,
    false,
    null,
    "7ba5bd90",
    null

    )

    component.options.__file = "src/App.vue"
    // 输出组件完整的配置项
    export default component.exports

    css scoped

    当.vue文件中的style标签有scoped属性,它的css样式只作用于当前组件的元素

    css scoped工作流程 如下:

    1. 使用 vue-loader 处理 .vue 文件, 根据 .vue 文件请求路径文件内容, 生成 .vue 文件hash 值, 如:7ba5bd90
    2. 如果 .vue 文件某一个 style 标签scoped 属性, 为 .vue 文件 生成一个 scopedIdscopedId 的格式为 data-v-hash, 如:data-v-7ba5bd90
    3. 使用 vue-loader.vue 文件 中获取 style区域块(scoped) 的 **样式内容(字符串)**;如果使用了 less 或者 sass, 要使用 less-loader 或者 sass-loader 处理 样式内容,使 样式内容 变为 浏览器可识别的css样式; 然后使用 PostCSS 提供的 parser 处理 样式内容, 为 样式内容 中的每一个 css选择器 添加 **[data-v-hash]**; 再使用 css-loader;最后使用 style-loadercss 样式 添加到 head 中或者通过 miniCssExtractPlugincss 样式 提取一个公共的 css 文件中。
    4. 通过 normalizer 方法返回 完整的组件配置项 optionsoptions 中有属性 _scopeId, 如 _scopedId: data-v-7ba5bd90;
    5. 使用 组件配置项 options 构建组件实例, 给 组件 中每一个 dom元素 添加属性: data-v-hash

    经历上述过程,style(scoped) 中的样式就变成了 组件的私有样式

    深度作用选择器

    我们可以通过 >>> 操作符, 在 组件 中修改 子组件私有样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // child component
    .hello {...}

    // parent component
    <style scoped>
    .parant .hello {...}
    .parent >>> .hello {...}
    </style>

    // 进过 postCSS 处理以后的 css
    .parent .hello[data-v-xxx] {...} // 无法影响子组件

    .parant[data-v-xxx] .hello {....} // 可影响子组件

    有些像 Sass 之类的 预处理器 无法 **正确解析 >>>**。这种情况下我们可以使用 /deep/::v-deep 操作符取而代之,两者都是 >>>别名,同样可以正常工作。

    深度作用选择器, 必须在含有 scoped 属性 的 style 标签中使用,否则无效。 这是因为 >>>、/deep/、::v-deep 需要被 postCSS 解析才能起作用。 只有 style 标签 中有 scoped 属性样式内容 才会被 postCSS 解析。

    postCSS 解析样式内容的时候, 会给 >>> 操作符 前面css选择器 添加 **[data-v-hash]**。

    注意: 父组件 中修改 子组件私有样式 时, 父组件 中的 样式的优先级 要大于 子组件 中的 样式的优先级, 否则会导致 父组件中定义的样式不生效

CSS Modules

我们也可以在 .vue 文件style 标签 上添加 module 属性, 使得 style 标签 中的 样式 变为 组件私有,具体使用方法详见 - 官网

css modulescss scoped 都可以使 样式 变为 组件私有,但是 原理 不一样。

css scoped 的实质是利用 css属性选择器 使得 样式 称为 局部样式,而 css modules 的实质是让 样式的类名、id名唯一 使得 样式 称为 局部样式

css modules工作流程 如下:

  1. 使用 vue-loader 处理 .vue 文件, 将 .vue 文件内容 转化为 js 代码。 如果 .vue 文件 中的 style 标签 中有 module 属性, 向 js 代码 中注入一个 injectStyle 方法, 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=3512ffa2&scoped=true&"  
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
import style0 from "./App.vue?vue&type=style&index=0&module=a&lang=css&"
import style1 from "./App.vue?vue&type=style&index=1&id=3512ffa2&module=true&scoped=true&lang=css&"
// 通过injectStyle方法, 会向vue实例中添加属性
function injectStyles (context) {
// 对应 <style module="a">...</style>
// 给vue实例添加属性a, 对应的值为使用css-loader处理样式内容以后返回的对象
this["a"] = (style0.locals || style0)
// 对应 <style module>...</style>
// 给vue实例添加属性$style, 对应的值为使用css-loader处理样式内容以后返回的对象
this["$style"] = (style1.locals || style1)
}
/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// normalize 会返回一个组件完整配置项对象
// 在执行过程中, 会将render方法重新包装成 renderWithStyleInjection 方法
// 执行 renderWithStyleInjection 方法时的时候, 先执行 injectStyles 方法, 再执行 原来的render 方法
var component = normalizer(
script,
render,
staticRenderFns,
false,
injectStyles,
"3512ffa2",
null

)
export default component.exports"
  1. 使用 css-loader 处理 .vue 文件style 区域块,会将 style 区域块 中的样式内容, 转化为 js 代码, 如下:
1
2
3
4
5
6
7
8
9
10
exports = module.exports = require("../node_modules/_css-loader@3.2.0@css-loader/dist/runtime/api.js")(false);  
// Module
exports.push([module.id, "\n#_3cl756BP8kssTYTEsON-Ao {\n font-family: 'Avenir', Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n._3IbrnaW__7RJMXk4rh9tW- {\n background-color: blue;\n}\n", ""]);
// Exports
exports.locals = {
// app是id名
"app": "_3cl756BP8kssTYTEsON-Ao",
// class 是 类名
"class1": "_3IbrnaW__7RJMXk4rh9tW-"
}

在处理过程中, css-loader 会将样式中的 类名id名 等用一个 唯一的命名代替
在执行 步骤1 的代码时,会执行上面的代码, 返回一个 对象, 即 步骤一 中的 style0style1, 格式如下:

1
2
3
4
5
6
7
// css样式内容会通过 style-loader 提供的方法添加到 head 中  
// 或者被 miniCssExtractPlugin 提取到一个 公共的css文件 中
style0 = [[css模块 id, css样式内容字符串, ''], ...]
style0.locals = {
"app": "_3cl756BP8kssTYTEsON-Ao",
"class1": "_3IbrnaW__7RJMXk4rh9tW-"
}
  1. 运行项目执行打包以后的js代码, 即 步骤1中的代码, 获取 renderstaticRenderFnsscriptExprotsstyle0style1, 然后通过 normalizer 方法返回 组件完整配置项 - options。 在执行过程中,将 render 方法重新包装成 renderWithStyleInjection 方法。
    构建 vue 实例 时,执行 renderWithStyleInjection 方法, 此时会 执行 injectStyles 方法,给 vue 实例 添加 $stylea 属性,属性值为 stlye0.localsstyle1.locals, 再执行原来的 render 方法。
    这样, 我们就可以通过 vue 实例$style、a 属性访问 样式类名id名。

热更新

开发模式 下,当使用 vue-loadervue-style-loader 处理 .vue 文件 的时候, 会向 生成的js代码 中注入与 热更新 相关的代码逻辑。 当我们修改 .vue 文件 时, dev-server 会通知 浏览器 进行 热更新

.vue 文件各个区域块(template、script、styles) 对应的 热更新逻辑 都不一样。

  • template & script

vue-loader 会在 打包代码 中注入 热更新 template、script 区域块 的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 从 template区域块 获取 render、 staticRenderFns 方法  
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
// 从 script区域块 获取 组件的配置项对象
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
// 获取 styles区域块的内容
import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
// 获取 styles(scoped)区域块的内容
import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"
/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// 返回构建组件需要的配置项对象, 包含 data、props、render、staticRenderFns 等
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
"7ba5bd90",
null

)
/* hot reload */
// .vue 文件的 script 区域块更改时, 客户端执行这一段代码
if (module.hot) {
var api = require("D:\\study\\demo\\webpack\\webpack-4-demo\\node_modules\\_vue-hot-reload-api@2.3.3@vue-hot-reload-api\\dist\\index.js")
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if (!api.isRecorded('7ba5bd90')) {
api.createRecord('7ba5bd90', component.options)
} else {
// 执行 reload 方法, 触发更新
// 使用 新的 options 替换原来的 options
api.reload('7ba5bd90', component.options)
}
module.hot.accept("./App.vue?vue&type=template&id=7ba5bd90&scoped=true&", function () {
// 当 .vue 文件的 template 区域块更改时, 客户端执行这一段代码
// 使用新的 render、staticRenderFns 更新原来的render、staticRenderFns
api.rerender('7ba5bd90', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
component.options.__file = "src/App.vue"
// 输出组件完整的配置项
export default component.exports

如果我们只修改了 .vue 文件script 部分, 客户端(即浏览器) 会进行 热更新, 过程如下:

  1. 服务端 通过 websocket 连接 通知 客户端 更新;
  2. 客户端 通过 动态添加script元素 的方式获取 更新以后的打包文件
  3. 安装打包文件,即执行 新的打包文件 中的 js 代码, 使用 打包文件中的 module 更新浏览器缓存的同名 module
  4. 重新安装组件对应的 module, 即 重新执行组件对应的js代码, 获取 renderstaticRenderFns 和 新的 scriptExports, 重新生成 组件 对应的 完整配置项
  5. 执行 api 提供的 reload 方法, 更新组件
    reload 方法中,会通过执行 父组件实例$forceUpdate 方法来 更新组件
    更新组件的时候, 由于组件配置项(data、props、methods等属性) 发生变化, 需要为 组件 生成 新的构造函数 VueComponent, 然后使用 新的构造函数,构建 新的组件实例
    即, 每次修改 .vue 文件script 部分, 都会为 组件 生成一个 新的实例对象销毁旧的实例对象
    如果我们只修改了 .vue 文件template 部分, 客户端(即浏览器) 会进行 热更新, 过程如下:
  6. 同上服务端 通过 websocket 连接 通知 客户端 更新;
  7. 同上客户端 通过 动态添加script元素 的方式获取 更新以后的打包文件
  8. 同上安装打包文件,即执行 新的打包文件 中的 js 代码, 使用 打包文件中的 module 更新浏览器缓存的同名 module
  9. 触发通过 module.hot.accept 注册的 callback
  10. 执行 api 提供的 rerender 方法, 更新组件
    执行 rerender 方法时, 会先获取 修改以后的template区域块 对应的 renderstaticRenderFns, 然后 更新原组件的 render、staticRenderFns, 然后执行 组件实例$forceUpdate 方法来更新 **组件(更新组件的时候, 会使用新的render方法, 生成新的vnode节点树)**。
    如果我们 同时 修改了 .vue 文件templatescript部分, 会按照上面 第一种情况 进行 热更新,并且不会触发上面代码中通过 module.hot.accept 注册的 callback
  • style

vue-style-loader 会在 打包代码 中注入 热更新 style区域块 的代码, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ar add = require("!../node_modules/_vue-style-loader@4.1.2@vue-style-loader/lib/addStylesClient.js").default  
var update = add("05835b6f", content, false, {});
// Hot Module Replacement
if(module.hot) {
// When the styles change, update the <style> tags
if(!content.locals) {
module.hot.accept("!!../node_modules/_css-loader@3.1.0@css-loader/dist/cjs.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/loaders/stylePostLoader.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css&", function() {
// 当 .vue 文件的 styles 区域块更改时, 客户端执行这一段代码
var newContent = require("!!../node_modules/_css-loader@3.1.0@css-loader/dist/cjs.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/loaders/stylePostLoader.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css&");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
// 执行update方法, 更新styles
update(newContent);
});
}
}
...

如果我们修改了 .vue 文件styles 区域块客户端(即浏览器) 会进行 热更新, 过程如下:

  1. 同上,服务端 通过 websocket 连接 通知 客户端 更新;
  2. 同上,客户端 通过 动态添加script元素 的方式获取 更新以后的打包文件
  3. 同上,安装打包文件,即执行 新的打包文件 中的 js 代码, 使用 打包文件中的 module 更新浏览器缓存的同名 module;
  4. 触发通过 module.hot.accept 注册的 callback
  5. 执行 update 方法, 更新样式
    更新样式 的时候, 会先 移除原来的 style 标签, 然后 添加新的 style 标签
    如果 style 标签 上有 module 属性,除了 vue-style-loader 会注入 热更新代码 外,vue-loader 也会在 打包代码 中注入 热更新代码,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 热更新代码  
module.hot && module.hot.accept(["./App.vue?vue&type=style&index=1&id=7ba5bd90&module=true&scoped=true&lang=css&"], function () {
// 当.vue的style区域块发生变化, 且style标签有module属性, 执行这一段逻辑
var oldLocals = cssModules["$style"]
if (oldLocals) {
// 获取新的唯一类名、id名
var newLocals = require("./App.vue?vue&type=style&index=1&id=7ba5bd90&module=true&scoped=true&lang=css&")
if (JSON.stringify(newLocals) !== JSON.stringify(oldLocals)) {
// 更新vue实例的$style属性
cssModules["$style"] = newLocals
// 执行vue实例的 $forceUpdate 方法,重新执行 render 方法
require("D:\\study\\demo\\webpack\\webpack-4-demo\\node_modules\\_vue-hot-reload-api@2.3.3@vue-hot-reload-api\\dist\\index.js").rerender("7ba5bd90")
}
}
})

执行上述 热更新代码, 会 更新 vue实例 的 $style 属性, 然后触发 vue 实例$forceUpdate 方法, 重新渲染
一个 style 区域块 对应一个 style 标签。修改某一个 style 区域块 之后,会更新对应的 style 标签
style 区域块热更新templatescript 区域块热更新 互不影响。

tree shaking 副作用

生产模式 下, webpack 默认启用 tree shaking。如果此时项目 根目录 中的 package.json 中的 sideEffects 的值为 false,且 .vue 文件style 标签 没有 module 属性,使用 vue-loader 处理 .vue 文件 的时候, 会产生 样式丢失 的情况,即 styles 区域块 不会添加到 head 中或者 被提取到公共的css文件中

首先,先看一下 .vue 文件 经过处理以后生成的 js代码, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/ 从 template区域块 获取 render、 staticRenderFns 方法
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
// 从 script区域块 获取 组件的配置项对象
import scriptExports from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
// 获取 styles区域块的内容
import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
// 获取 styles(scoped)区域块的内容
import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"


/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// 返回构建组件需要的配置项对象, 包含 data、props、render、staticRenderFns 等
var component = normalizer(
scriptExports,
render,
staticRenderFns,
false,
null,
"7ba5bd90",
null

)

component.options.__file = "src/App.vue"
// 输出组件完整的配置项
export default component.exports

在上面的代码中,template 区域块 返回的 renderstaticRenderFnsscript 区域块 返回的 scriptExports, 都有被 normalizer 方法使用, 而 styles 区域块 返回的 style0style1 则没有被使用。 在 打包代码 的时候, tree shaking 就会自动移除 styles 区域块 对应的代码,导致 样式丢失

解决方法:

  1. 修改 package.json 文件中的 sideEffects 属性, 告诉 webpack .vue 文件在使用 tree shaking 的时候会有 副作用, 如下:
1
2
3
"sideEffects": [  
"*.vue"
]

有了上述配置, webpack 在处理 .vue 文件的时候, 不会使用 tree shaking不会出现样式丢失的问题
但是这种解决方法有一个问题, 如果 script 区域块 中通过 import 的方式引入了 未使用的模块未使用的模块在最后打包代码的时候不会被删除

  1. 通过 rule.sideEffects 指定 具体的模块 在使用 tree shaking 的时候会有 副作用, 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpackConfig:  
{
test: /\.css$/,
oneOf: [{
resourceQuery: /\?vue/,
// 指定.vue文件的 style区域块 使用 tree shaking 时会有副作用
sideEffects: true,
use: [isProduction ? MiniCssExtractPlugin.loader : 'vue-style-loader', 'css-loader']
}, {
use: [isProduction ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader']
}]
},
{
test: /\.scss$/,
oneOf: [{
resourceQuery: /\?vue/,
// 指定.vue文件的 style(lang=scss)区域块 使用 tree shaking 时会有副作用
sideEffects: true,
use: [isProduction ? MiniCssExtractPlugin.loader : 'vue-style-loader', 'css-loader', 'sass-loader']
}, {
use: [isProduction ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader', 'sass-loader']
}]
}

// package.json
{
sideEffects: false
}

上述配置, 明确说明了 .vue 文件style 区域块 在使用 tree shaking 的时候, 会有 副作用在打包的时候不会删除
这样的话,样式不会丢失, 并且如果 script 区域块 中通过 import 的方式引入了 未使用的模块未使用的模块在最后打包代码的时候会被删除

本地工作流

本地工作流是本地工具链阶段的前端工程体系所对应的工作模式,此阶段的各个功能模块均由开发人员在本机环境下执行,所有功能模块的两个要素:

  • 执行人:前端开发人员
  • 执行环境:分散的本地开发环境

代码分离与测试沙箱

代码分离:

基本原则是单独编写一个适应各环境的“配置文件”,假设我们将此文件命名为manifest.js:

1
2
3
4
5
6
7
8
const Domain=window.location.host;
if(Domain==='test.app.com'){
//测试环境
window.ASYNC_API_DOMAIN='apitest.app.com';
}else if(Domain==='www.app.com'){
//生产环境
window.ASYNC_DOMAIN='api.app.com'
}

将主站域名区分测试环境和生产环境,并将对应的异步API域名以全局变量的形式暴露出来。随后在业务逻辑代码main.app.js中使用全局变量:

1
2
3
4
5
6
7
$.ajax({
url:`https://${window.ASYNC_API_DOMAIN}/login`,
dataType:'jsonp',
success(){

}
})

manifest.app.js可以作为一个通用模块不参与构建,并且必须在业务js文件之前引入:

1
2
3
<script type="text/javascript" src="//static.app.com/common/manifest.js">

<script type="text/javascript" src="//static.app.com/common/main.app.js">

manifest.js作为一个通用模块,无法保证更新后完全兼容历史项目

代码分离的本质是架构层面的细节设计,架构是跟着业务需求的变动而不断改变的,所以manifest.js只能作为一种临时性的解决方法,而不是从工程角度出发的高度可适应方案

测试沙箱

测试沙箱的原则是搭建一个仿真的生产环境,在工作流中加入测试沙箱的支持后,前端只需执行一次针对生产环境的构建行为即可,测试通过后可直接部署上线,无需二次构建

image-20220509224958318

测试沙箱只需要模拟一个前端仿真环境,比如实现域名的映射,最原始的方案就是通过修改测试人员本地host文件实现,如果要统一规范可以搭建一个专属的VPN服务,所有参与测试的任意统一使用此VPN代理访问测试沙箱。测试沙箱的复杂度取决于生成环境的复杂度

云平台工作流

云平台工作流在本地工作流的基础上,将容易因个体差异产生问题的功能模块(比如构建,部署等)提升到云平台运行,通过严谨的流程控制增强开发规范性,云平台的目标不仅是实现功能的集中管理,而且要在此基础上进一步优化工作流程。功能集中的同时以为着权限集中,这也是实现自动构建和自动部署的必要前提

开发人员负责一线开发工作

开发负责人汇总开发人员的各个分支并将其合并到dev分支

云平台管理人员负责项目的发起以及部署队列的控制

动态构建:

webpack-dev-middleware:

将webpack构建输出的文件存储在内存中,正常情况下,webpack构建产出的文件会存储在output配置项指定的硬盘目录中,webpack-dev-middleware在此基础上建立一个文件映射机制,每当匹配到一个webpack构建产出文件的请求后便会将内存中与其对应的数据返回给发起请求的客户端,由于是内存的文件系统,没有好使的硬盘读写,数据更新很快,这也是webpack相较其他同类工具的优势之一

实际上,webpacl-dev-server是在Express和webpack-dev-middleware基础上进行的封装,但由于不具备Mock服务,我们需要自己封装本地开发服务器,注意:

  • 如何启用源文件的监听并触发动态编译
  • 如何令客户端可访问由HTML引用但是并未参与构建的本地静态文件,比如jQuery等第三方库,这类文件由独立的script标签引入,不参与webpack构建

webpack-dev-middleware配置项:

  • lazy:是否开启惰性模式
  • watchOptions-监听细节配置(默认)
  • aggregateTimeouy:指定webpack的执行频率,webpack将在此段时间内针对源代码所有修改都聚合到一次重新编译行为中
  • ignored:指定不参与监听的文件
  • poll:指定webpack监听无效时轮询检验文件的频率

实际开发中并非所有的静态文件都参与构建,一些常用第三方库通常用单独的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express=require('express')
const webpack=require('webpack')
const path=require('path')
const webpackdevmiddleware=require('webpack-dev-middleware')
const app=express()
const compiler=webpack({
//webpack配置
})
//配置中间件
app.use('/libs',express.static(path.join(process.cwd(),'static')))
.use(webpackdevmiddleware(compiler,{
lazy:false,
watchOprtions:{
aggregateTimeout:300,
ignored:/node_modules/,
poll:false
}
}))
app.listen(8888,err=>{
if(err){
throw new Error(err)
}
})

Mock

1 假数据

2 客户端Mock

Mock进化的第二种形态就是以Mock.js为代表的客户端Mock,工作原理是在客户端拦截JavaScript代码发出的ajax请求并返回由Mock.js创建的假数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script src="/libs/js/mock.js"></script>
<script>
Mock.mock('/api',{
'code|1':{
200,
404,
500
},
'errMsg|1':[
'资源未找到',
'服务器错误'
],
'data':{
'a':1
}
})
loading=true;
$.ajax({
url:'/api',
dataType:'jsonp',
success(res){
if(res.code===200&&res.response){
data=res.response.data
}else{
alert(res.errMsg)
}
},
fail(){
alert('操作失败')
},
complete(){
loading=false;
}
})
</script>

Mock.js可以随机创建假数据,还可以处理各种异常状态,然而几遍客户端Mock提供了许多便利,Mock相关代码或文件仍然必须存在于业务代码中,上线之前仍然需要删除,这对于产品质量保障始终存在一定隐患。

3 Mock Server

在开发阶段使用Mock Server提供的与真实接口规范和逻辑一致的本地接口进行开发

开发完成后,在构建阶段将Mock的地址修改为已完成的真实服务器端接口地址

Mock Server最普遍的使用场景就是模拟异步数据接口,比如使用Ajax或者JSONP获取和提交数据,模拟的方式通常有两种:

  • Local:本地模式,使用本地JSON数据作为异步接口的请求响应
  • Proxy:代理模式,将异步接口代理到线上的其他接口地址,类似于转接者角色

Mock Server本质上是一个简化版的Web Server,最基础的组件就是负责分发的路由

Local模式:

在开发阶段使用本地API代替真实API地址,使用本地JSON作为接口的返回数据,具体搭建流程:

通过路由创建一个可访问的本地域名API代替真实API,比如使用/login代替http://auth.app.com/login

在路由响应函数内对请求进行校验,比如是否为JSONP请求,然后返回本地JSON数据

前端工程师在开发阶段的业务代码中将http://auth.app.com/login修改为API/login

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Path=require('path')
const express=require('express')
const app=express()
app.get('/login',(req,res)=>{
//path.join(process.cwd())//返回的是当前Node.js进程执行时的工作目录
const MockData=require(path.join(process.cwd(),'./mockdata.login.js'));
req.query.callback?res.jsonp(MockData):res.json(MockData);

}
);
app.listen(8888,err=>{
if(err){
throw new Error(err)
}
})

DefinePlugin和环境变量

开发环境使用Mock Server将所有真实接口地址修改为本地域名地址,在部署测试和生产环境之前必须将接口的地址复原。

接口地址的修改需求涉及两个方面:

执行环境:开发,测试,生产环境下的接口地址均不同,所以必须能够依据部署目标环境将接口修改为对应地址

字符串修改:对于JavaScript代码来说,接口地址就是一个字符串,我们要做的就是将该字符串修改为指定的值

1
2
3
4
5
6
7
8
9
entry:{
'main.app':'./js/main.app.js'
},
plugins:[
new webpack.DefinePlugin({
AUTH_API_DOMAIN:'//auth.app.com',
HMOE_API_DOMAIN:'//www.app.com'
})
]

源代码

1
2
3
4
5
6
7
8
9
10
//请求位于auth.app.com域名下的login接口
$.ajax({
url:`${AUTH_API_DOMAIN}/login`,
success(res){}
})
//请求位于www.app.com域名下的login接口
$.ajax({
url:`${HOME_API_DOMAIN}/login`,
success(res){}
})

经过构建后

1
2
3
4
5
6
7
8
9
10
//请求位于auth.app.com域名下的login接口
$.ajax({
url:`//auth.app.com/login`,
success(res){}
})
//请求位于www.app.com域名下的login接口
$.ajax({
url:`//www.app.com/login`,
success(res){}
})

结合环境变量,配置DefinePlugin指定不同环境下的替换值

1
2
3
4
new webpack.DefinePlugin({
AUTH_API_DOMAIN:process.env.NODE_ENV==='dev'?'':'//auth.app.com',
HOME_API_DOMAIN:process.env.NODE_ENV==='dev'?'':'//www.app.com'
})

以上代码在开发环境下将接口映射到本地的Mock Server,非开发环境下修改为真实域名的地址

CDN

CDN(Connect Delivery Network)是一种部署策略,包括分布式存储,负载均衡,内容管理等模块,CDN的一个重要功能是将静态资源缓存到用户近距离的CDN节点上,不但能提高用户对静态资源的访问速度,还能节省服务器的带宽消耗,降低负载。实现此功能的一个重要前提是将静态资源部署到已接入CDN的专属服务器,而这类服务器通常与Web主页面处于不同域名下,这样做的主要目的是为了充分利用浏览器的并发请求能力,提高页面的加载速度。

webpack的逆向注入模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
entry:{
'main.app':'./js/main.app.js'
},
output:{
path:'./dist',
filename:'[namr].[chunkhash].js',
publicPath:'//static.app.com/app/'
},
module:{
rules:[{
test:/\.js$/,
loader:'babel-loader'
},{
test:/\.css$/,
use:ExtractTextPlugin.extract({
use:'css-loader'
})
},
]
},
plugins:[
newExtractTextPlugin({
filename:'style/[name].[chunkhash].css',
}),
new HtmlWebpackPlugin({
filename:'index.html',
template:'index.html',
inject:true
})
]
}
  1. index.html源码没有引用main.app.css的标签和main.app.js的script标签,然而构建后的文档内容被注入了对应的引用标签并且引用地址为构建之后的资源地址
  2. 构建之后的资源地址被修改为带有CDN服务器域名和路径信息的完整URL,并且CDN的信息与配置中的publicPath一致,这是webpack提供的可用于指定静态资源CDN服务器信息的配置项

多页面项目资源定位

编写一个html-wepack-plugin-before-html-processing插件,保证在html-wepack-plugin-before-html-processing阶段能够执行以下行为:

  • 获得静态资源构建后的URL
  • 获得并且修改HTML文档的内容

编写webpack组件,清楚两个对象:compiler和compilation

compiler对象代表的是webpack执行环境的完整配置,只会在启动webpack时被创建,并且在webpack运行期间不会被修改

compilation对象代表某个版本的资源对应的编译进程,当使用webpack的development中间件时,每次检测到项目文件有改动会创建一个compilation,进而能够针对改动生产全新的编译文件,compilation对象包含当前模块资源,待编译文件,有改动的文件和监听依赖的所有信息

编写一个构造函数用于接收配置:

1
2
3
4
5
6
7
8
9
10
11
12
const HtmlWebpackPluginForLocate=function(options){
this.options={...options}//options用来提供给用户配置插件功能的细节

}
//针对指定事件阶段编写apply方法
HtmlWebpackPluginForLocate.prototype.apply=compiler=>{
compiler.plugin('compilation',compilation=>{
compilation.plugin('html-webpack-plugin-before-html-processing',(htmlPluginData,callback)=>{
//插件行为逻辑
})
})
}

html-webpack-plugin-before-html-processing阶段捕获两个对象,callback的作用类似于Express中间件中的next函数,执行完当前插件的逻辑后必须调用callback以便进入后序流程,它属于webpack流程控制的一部分,没有构建相关的信息

覆盖更新

实现方案是在引用资源的URL后添加请求参数,比如添加时间戳参数,

浏览器会将参数不同的URL视为全新的URL,所以浏览器向服务器请求并下载最新资源,我们只更新修改了的资源,为了避免手动改参数,将hash指纹(通过mds算法得到)作为URL的v参数

1
2
3
4
5
<head>
<link rel='stylesheet' href="main.home.css?v=858d5483">
<script type="text/javascript" src="main.home.js?v=bbcadaf73"></script>

</head>

缺点:

  • 必须保证html文件与改动的静态文件同步更新,否则会出现资源不同步的情况。如果是无服务端渲染的项目,html文件被视为金泰之源,并且与其他今天资源(js/css/图片等)部署到同一台服务器,在这种情况下,我们可以保证资源的同步更新,不会受到覆盖更新的影响。但是对于依赖服务端渲染的项目,大多数的部署方式是将网站的入口HTML和静态资源分开部署,比如,HTML与服务器代码一同部署到域名为ww.app.com对应的服务器,把JS/CSS等静态资源部署到static.app.com对应的服务器,两种资源分开部署必然有先后顺序,这就意味着两种资源的上线存在时间差,不论先部署哪种资源无法保证所有用户访问页面的正确性,即使时间差小,对于淘宝这种访问量庞大的网站来说也会影响用户群,这就是为什么很多团队总是选择在半夜或者凌晨这种访问量较小的时间段发布新版本的原因之一
  • 不利于版本回滚。由于覆盖更新每次迭代之后的资源都会覆盖服务器上原有的旧版本文件,这对于版本回滚操作很不友好,运维人员吗要么借助于服务器本身的缓存机制要么拿到旧版本文件再次覆盖

增量更新

将原本作为参数值的hash指纹作为资源文件名的一部分并且删除用于更新的url参数

1
2
3
4
5
<head>
<link rel='stylesheet' href="main.home.858d5483.css">
<script type="text/javascript" src="main.home.bbcadaf73.js"></script>

</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
2
3
4
output:{
filename:'[name].[chunkhahs:8].js'

}
主文件 异步文件
原始状态 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
test:/\.css$/,
use:ExtractTextPlugin.extract({
filename:'./dest/[name].[contenthash:8].css'
use:[{
loader:'css-loader',
options::{
importLoaders:2
}

}],
publishPath:'/'
})

}

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语法,最早的CSS预编译器是2007年起源于Ruby on Rails社区的SASS,目前不叫流行的如LESS,Stylus在一定程度上收到SASS影响

CSS预编译器提升了CSS开发效率:

1 增强编程能力

2 增强源码可复用性,让CSS开发符合DRY(Don’t repeat yourself)的原则

3 增强源码可维护性

4 更便于解决浏览器兼容性

实现

  • 嵌套
  • 变量
  • mixin/继承
  • 运算
  • 模块化

嵌套是所有预编译器都支持的语法特性,mixin/继承是为了解决hack和代码复用,变量和运算增强了源码的可编程能力;模块化的支持不仅更利于代码复用,同时提高了源码的可维护性

PostCSS

PostCSS鼓励开发者使用规范的CSS原生语法编写源代码,然后配置浏览器需要兼容的浏览器版本,最后经过编译将源码转化为目标浏览器可用的CSS代码。PostCSS提供了丰富的插件用于实现不同场景的编译需求,最常用的比如autoprefix,Sprited等

PostCSS不是另一种CSS预编译器,与SASS LESS等预编译器不冲突,目前普遍方案将CSS预编译与PostCSS综合:

使用CSS预编译器弥补CSS源码的弱编程能力。比如变量,运算,继承等

使用PostCSS处理针对浏览器的需求,比如autoprefix,自动CSS Sprites

webpack结合预编译与PostCSS实现CSS构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
test:/\.less$/
use:[{
loader:'style-loader',
options:{}//style-loader options
},
{
loader:'css-loader',
options:{
importLoaders:2//css-loader options
}

},
{
loader:'postcss-loader',
options:{}//postcss-loader
}
{
loader:'less-loader',
options:{}//less-loader options

}
]
}

css-loader中的importLoaders选项的作用是:用于配置css-loader作用于@import的资源之前需要经过的其他loader的个数,@import用于CSS源码中引用其他模块的关键字,如果你的项目中确定不会涉及到模块化,可以忽略此配置项

如果需要将编译后的css文件独立导出,则需将style-loader替换为extract-text-webpack-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
test:/\.less$/
use:ExtractTextPlugin.extract({
use:[{
loader:'style-loader',
options:{}//style-loader options
},
{
loader:'css-loader',
options:{
importLoaders:2//css-loader options
}

},
{
loader:'postcss-loader',
options:{}//postcss-loader
}
{
loader:'less-loader',
options:{}//less-loader options

}],
publicPath:'/'
}

})

区分css-loader和style-loader

css-loader:用于解析css源文件并获得其引用资源,比如@import引用的模块,url()引用的图片等,然后根据webpack配置编译这些资源

style-loader:负责将CSS代码通过style标签插入HTML文档中,所以如果独立导出CSS文件就不再需要style-loader,css-loader必须在style-loader之前执行

vue-router的本质:

路由就是SPA(单页应用)的路径管理器,vue-router的单页应用中,则是路径之间的切换,也就是组件的切换,路由模块的本质就是建立起url和页面之间的映射关系

为什么不能用a标签,这是因为Vue左的是单页应用,当你的项目准备打包时,会生成dist文件夹,这里面只有静态资源和一个index.html页面,所有a标签跳转页面不起作用

单页应用的核心之一就是:更新视图而不重新请求页面,vue-router在实现单页面前端路由时,提供两种方式:Hash模式和History模式,根据mode参数来决定使用哪一种

Hash模式

vue-router默认hash模式,使用URL的hash模拟一个完整的URL,于是URL改变时,页面不会重新加载,hash(#)是URL的锚点,代表网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载页面,也就是hash出现在URL中(#后面的值),但不会被包含在http请求中,对后端没有影响,因此改变hash不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用“后退”按钮,就可以回到上一个位置,所以Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据,hash模式原理是onhashchange事件(监测hash值变化),可以在window对象上监听这个事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Router{
constructor(){
//存储hash与callback键值对
this.routes={};
//当前hash
this.currentUrl="";
//记录出现过的hash
this.history=[];
//作为指针,默认指向this.history的末尾,根据后退前进指向history中不同的hash
this.currentIndex=this.history.length-1;
this.refresh=this.refresh.bind(this);
this.backoff=this.backoff.bind(this)
//默认不是后退操作
this.isBack=false
window.addEventListener('load',this.refresh,false)
window.addEventListener('hashchange',this.refresh,false)


}
route(path,callback){
this.routes[path]=callback||function(){}
}
refresh(){
//不能包括'#'
this.currentUrl=location.hash.slice(1)||'/';
//不是后退操作
if(!this.isBack){
//如果当前指针小于数组总长度,直接截取之前的部分储存下来,避免点击后退按钮后指针后移一位,当再次正常跳转时,指针前进一位,
//而在数组中添加新hash路由
//导致指针和路由不匹配
if(this.currentIndex<this.history.length-1){
this.history=this.history.slice(0,this.currentIndex+1)

}
this.history.push(this.currentUrl)
this.currentIndex++;
}
this.routes[this.currentUrl]()
this.isBack=false;
}
backoff(){
this.isBack=true
this.currentIndex<=0
?(this.currentIndex=0)
:(this.currentIndex=this.currentIndex-1)
//找到后退后的哈希地址
location.hash=`#${this.history[this.currentIndex]}`
//调用后退后的地址对应的函数
this.routes[this.history[this.currentIndex]]
}

}
window.Router=new Routers()
const content=document.querySelector('body')
const button=document.querySelector('button')
function changeBgColor(color){
content.style.background=color
}
Router.route('/blue',function(){
changeBgColor('blue')
})
Router.route('/green',function(){
changeBgColor('green')
})
Router.route('/red',function(){
changeBgColor('red')
})
button.addEventListener('click',Router.backoff,false)

history模式

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Router{
constructor(){
this.routes={};
this.bindPopState();
}
init(path){
history.replaceState({path:path},null,path)
this.routes[path]&&this.routes[path]()
}
route(path,callback){
this.routes[path]=callback||function(){}
}
go(path){
history.pushState({path:path},null,path)
this.routes[path]&&this.routes[path]()
}
_bindPopState(){
window.addEventListener('popstate',e=>{
const path=e.state&&e.state.path;
this.routes[path]&&this.routes[path]()
})
}
}
window.Router = new Routers();
Router.init(location.pathname);
const content = document.querySelector('body');
const ul = document.querySelector('ul');
function changeBgColor(color) {
content.style.backgroundColor = color;
}

Router.route('/', function() {
changeBgColor('yellow');
});
Router.route('/blue', function() {
changeBgColor('blue');
});
Router.route('/green', function() {
changeBgColor('green');
});