在日常开发中,偶尔会遇到需要复制对象的情况,需要进行对象的复制。
由于现在流行标题党,所以,一文带你了解js数据储存及深复制(深拷贝)与浅复制(浅拷贝)
首先就需要理解 js 中的数据类型了
js 数据类型包含
基础类型
:String
、Number
、 null
、undefined
、Boolean
以及ES6
引入的Symbol
、es10
中的BigInt
引用类型
:Object
由于 js 对变量的储存是栈内存
、堆内存
完成的。
基础类型
将数据保存在栈内存
中引用类型
将数据保存在堆内存
中由于 js 在数据读取和写入的时候,对基础类型
是直接读写栈内存
中的数据,引用类型
是将一个内存地址保存在栈内存中,读写都是修改栈内存中指向堆内存的地址
以如下代码为例
1 | let obj = { |
在内存中的表现为
我们声明个obj1
1 | let obj1 = obj; |
因为这个赋值,把内存变成了这样
然后,内存中只是给js栈内存新增了一个指向堆内存
的地址而已,这种就叫做浅复制
。因为如图可以看到,如果我们修改obj.a
的话,实际修改的是堆内存0x88888888
中的变量a
,由于obj1
也指向这个地址,所以obj1.a
也被修改了
深复制
是指,不单单复制引用地址,连堆内存都复制一遍,使obj
和obj1
不指向同一个地址。
分开来看深复制
与浅复制
由上述图可知,浅复制只是复制第一层,也就是,基本类型
复制新值,引用类型
复制引用地址
浅复制
可以使用的方案有循环赋值
、扩展运算符
、object.assign()
,
1 | let obj = { |
由于是浅复制,所以引用类型只是复制了内存地址,修改其中一个对象的子属性后,引用这个地址的值都会被修改。
浅克隆图解如下
由于浅复制只是复制第一层,为了解决引用类型的复制,需要使用深复制来完成对象的复制,基本类型
复制新值,引用类型
开辟新的堆内存
。
深复制
可以使用的方案有JSON.parse(JSON.stringify(obj))
、循环赋值
。
1 | let obj = { |
看起来是复制成功了!!~地址也变了,修改obj
,obj1
的引用地址不会跟着变化。
但是我们来console
一下obj
以及obj1
1 | console.log(obj) |
似乎发现了离奇的事情,只有obj.a
以及obj.c
正确的复制了,日期类型
、方法
、正则表达式
均没有复制成功,发生了一些奇怪的事情
那么为了解决这种事情,就需要写一个deepClone
方法来完成深复制了,参考了许多开源库的写法,将所有的复制项单独拆出,方便未来对特殊类型进行扩展,也防止不同功能间的变量互相干扰
1 | //既然是深复制,一定要传入一个object,再return 一个新的 Object |
这样一个基本上满足功能的深复制就完成了。先测试一下
1 | let obj = { |
再console
一下
1 | console.log(obj) |
这样,就完成了deepClone
深复制方法
经过深复制后,图解如下
上述代码还有优化空间,参考了lodash
库,在进行 new 对象时,可以使用 constructor
构造函数 来进行创建新的实例,这样
1 | function deepClone(obj){ |
然后可以有一个合并版本的,比较节省代码,将下方区分开的复制方法,合并到deepClone
中,可以极大地减少代码体积
1 | function deepClone(obj){ // |
随着前端业务的发展,
我们一般在写一个较为大型的vue
项目时候,会使用到vue-router
,来根据指定的url
或者hash
来进行内容的分发,可以达到不像服务端发送请求,就完成页面内容的切换,能够减少像服务器发送的请求,让用户进行页面跳转时候能够更快,体验更好
在初学vue-router
的时候,一般人都会有一个印象,router-link
以及router-view
都是vue
原生自带的标签。但是这个印象是错误的,vue-router
本质上是一个vue
的插件,通过Vue.use(VueRouter)
来使用这个插件。router-link
以及router-view
也是这个插件实现的自定义标签。
本文以开发插件的模式,撸一个vue-router
插件以加深对其原理的了解
也就是说,要实现一个简单的vue-router
,需要完成以下需求
1 | vue create my-vue-router |
由于只着重于vue-router
的内容,所以先使用原本的vue-router
这样只替换vue-router
源码文件即可
增加vue-router
1
vue add router
然后项目目录就变成了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19my-vue-router
|- node_modules
|- public
|- src
|- assets
|- components
|- HellowWorld.vue
|- router
|- index.js
|- views
|- About.vue
|- Home.vue
|- App.vue
|- main.js
|- .gitinore
|- babel.config.js
|- package.json
|- README.md
|- yarn.lock
在目录中,新建一个myRouter.js
的文件,来放置我们的源码
1 | my-vue-router |
此时,@/src/router/index.js
中的内容里,我们将导入的vue-router
替换为我们的myRouter.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 import Vue from 'vue'
- import VueRouter from 'vue-router'
+ import VueRouter from './myRouter'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
这里我们可以看到,代码执行的流程为
引入myRouter.js
->配置routes对象
->new VueRouter
->export default
导出
此处用到了 Vue.use()
这个API
vue
中的插件,一个核心的api
就是vue.use()
安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为install 方法。install 方法调用时,会将 Vue 作为参数传入。
该方法需要在调用 new Vue() 之前被调用。
当 install 方法被同一个插件多次调用,插件将只会被安装一次。
也就是说,我们在自己造的myRouter
里得实现这个install
方法
new VueRouter
来生成实例url
变化,并双向绑定current方法router-link
与router-view
1 | let Vue;//由于使用者肯定是使用vue.use引入的这个插件,所以代码里就不引入vue.js了,防止重复打包 |
注释都写在代码里啦,可以执行简单的路由双向绑定功能,有哪里有疑问可以提出~互相学习~
觉得好的话,可以给我的 github点个star
哦
Vue组件化中,当我们在设计一个组件的时候,可能会保留一部分,让使用者自定义的内容,比如:
在这种场景下,把保留给使用者的部分,叫做插槽(slot)
理解:
default
的插槽写法:1
2
3
4
5
6
7
8
9
10
11
12
13//自定义组件中
<template>
<div>
<slot><slot>//匿名插槽
</div>
</template>
//页面(使用者)使用
<template>
<div>
<myComponent><p>我被放进了插槽中</p></myComponent>
</div>
</template>
代码中我被放进了插槽中
这句话,就进入了自定义组件的匿名插槽中,从而变成了1
2
3
4
5
6
7
8//自定义组件中
<template>
<div>
<div>
<p>我被放进了插槽中</p><!-- 匿名插槽中放入了内容 -->
</div>
</div>
</template>
理解:所谓具名插槽,就是这个插槽里,这个插槽被命了名,使用者放进来的东西,声明了插槽的名称,会被分发进这个具名插槽中。
写法:使用template
标签声明具名插槽
名称<template v-slot:插槽名></template>
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>
<div>
<div class='slot1'>
<slot name='slot1'><slot><!-- 名为“slot1”的具名插槽 -->
</div>
<div class='slot2'>
<slot name='slot2'><slot><!-- 名为“slot2”的具名插槽 -->
</div>
<slot><slot>//这里是个匿名插槽
</div>
</template>
//页面(使用者)使用
<template>
<div>
<myComponent>
<template v-slot:slot1>
<p>名为slot1的具名插槽中</p>
</template>
<a>啦啦啦啦德玛西亚</a>
<template v-slot:slot2>
<p>名为slot2的具名插槽中</p>
</template>
<p>啦啦啦啦德玛西亚</p>
</myComponent>
</div>
</template>
代码被分发到对应插槽后的内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//自定义组件中
<template>
<div>
<div class='slot1'>
<div>
<p>名为slot1的具名插槽中</p>
</div>
</div>
<div class='slot2'>
<div>
<p>名为slot2的具名插槽中</p>
</div>
</div>
<div>
<a>啦啦啦啦德玛西亚</a>
<p>啦啦啦啦德玛西亚</p>
</div>
</div>
</template>
理解:一种能够将子组件可用的内容暴露给父组件的插槽。
比如:我们有的时候,需要一些子组件里的东西,做内容拼接,就像一个用户名输入框,我们希望所有的用户名,都跟随一个user_
的前缀,此处就可以使用到作用域插槽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//自定义组件中
<template>
<div>
<slot :user='username'><slot>//匿名插槽
</div>
</template>
<script>
export default {
data(){
return {
username:{
prefix:"user_"
}
}
}
}
</script>
//页面(使用者)使用
<template>
<div>
<myComponent v-slot='obj'>
{{obj.user.prefix}}小寒大人
</myComponent>
</div>
</template>
编译后的结果就变成了1
2
3
4
5
6
7<template>
<div>
<div>
user_小寒大人
</div>
</div>
</template>
插槽是拥有默认值功能的,如果对应的slot没有传入内容,则会使用slot的默认值
以匿名参数为例1
2
3
4
5
6
7
8
9
10
11
12
13
14//自定义组件中
<template>
<div>
<slot><p>这里是默认的内容</p></slot>
</div>
</template>
//页面(使用者)使用
<template>
<div>
<myComponent></myComponent>
<myComponent>替换了</myComponent>
</div>
</template>
最终表现结果为1
2
3
4
5
6
7
8
9
10<template>
<div>
<div>
<p>这里是默认的内容</p>
</div>
<div>
替换了
</div>
</div>
</template>
可以使用动态值来定某些内容进入某些具名插槽中
正常的具名插槽为v-slot:插槽名
,动态的写法为v-slot:[dynamicSlotName]
,此写法仅在vue2.6.0后的vue中版本使用
正常的具名插槽为v-slot:插槽名
,简写的写法为#插槽名
,此写法仅在vue2.6.0后vue中版本使用
Vue中有个非常重要的核心思想,就是组件化,组件化是为了代码复用
组件化,就像一个电脑主机里的主板,有内存条的插口,有硬盘,光驱等等的插口,我们的项目,就像一个电脑主机,通过各种组件化的模块(硬盘、内存等),来拼合成一个完整的电脑。
(图片来源 vue-组件化应用构建)
如图,每一个块都是一个组件,由许许多多的组件拼合而成,可以无限的嵌套下去
组件部分1
2
3
4
5
6
7
8<template>
<div></div>
</template>
<script>
export default {
name:"myAlert"
};
<style></style>
使用者部分1
2
3
4
5
6
7
8
9
10
11
12
13<template>
<div>
<myAlert></myAlert> <!-- 实例中使用组件 -->
</div>
</template>
<script>
import myAlert from '@/components/alert.vue';//导入自己写的组件
export default {
components:{myAlert}//在这个vue实例中注册组件
};
</script>
<style></style>
1 | // 定义名为 todo-item 的新组件 |
html部分使用1
2
3<div>
<myAlert></myAlert>
</div>
Vue
中存在的组件之间传值的方案如下
子组件中声明props
,父组件往对应的props值中传递
父组件使用this.$refs.组件名.变量
来选中子组件并修改子组件的内容
父组件使用this.$children[0].变量
来选中并修改子组件的内容
需要注意的是:由官网vm.$children得知
$children 并不保证顺序,也不是响应式的
所以一般不建议使用此方法来进行传值,因为不能很稳定的找到指定组件的实例,除非这个页面只有一个子组件
另外,此例中,this.$children[0]
不是响应式的this.$children[0].变量
是响应式的。
此处为观察者模式
this.$emit('confirm','点击了确定')
来派发confirm
事件<myAlert @confirm='successCallback'></myAlert>
来监听事件;confirm
事件的派发者和监听者,都是myAlert
组件,myAlert
组件监听完毕后将调用父组件的successCallback
回调事件,当然这个监听的事件名
和触发的事件名
都是可以自定义
的。使用公共父级组件$parents
或者$root
1 | //组件一 |
使用任意两个组件之间传值的方案 点击查看
概念,所谓跨辈分传值,就是
祖辈->父辈->子辈->孙辈->…
其中,垮了一个辈分或多个辈分的就是跨辈分传值,例如,祖辈及孙辈
由于多级嵌套,使用props
传递显然是不现实的
针对这种情况,vue
提供了 provide/inject
两个API
来完成这件事
provide
声明一个变量inject
来注入祖辈声明的变量写法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//祖辈
export default {
provide(){//此处可以传入动态变量,与data类似
return {
componentYeye:this
}
},
data(){
return {
yeyedeBianliang:'爷爷的变量'
}
}
}
//孙辈
export default {
inject:['componentYeye'],//此处为数组,注入祖辈声明的变量
mounted(){
console.log(this.componentYeye.yeyedeBianliang);//爷爷的变量
}
}
注意
vuex
中建议变量修改都是用commit
类似所谓原型挂载,就是在main.js
中将公共变量,事件,都挂在到Vue原型上1
2
3
4
5
6
7
8
9
10
11
12
13
14//main.js
Vue.prototype.$globalData = {}
Vue.prototype.$sayName = function(e){
console.log('我的名字是',e)
}
new Vue({...})
//组件一
Vue.prototype.$globalData.name='小明';
this.$sayName('小王');//我的名字是小王
//组件二
console.log(this.$sayName.name);//小明
this.$sayName('小王');//我的名字是小王
所谓事件总线,就是在当前的Vue
实例之外,再创建一个Vue实例来专门进行变量传递,事件处理,管理回调事件等1
2
3
4
5
6
7
8
9
10//main.js中
Vue.prototype.$bus = new Vue();
new Vue({...})
//组件一
this.$bus.$on('sayName',(e)=>{
console.log('我的名字是',e)
})
//组件二
this.$bus.$emit('sayName','小明');//我的名字是 小明
Vuex
是Vue
提供的一种,专门用来管理vue
中的公共状态,事件等等。详见 从0开始探究vue-公共变量的管理
在Vue
项目中,我们总会遇到一些公共数据的处理,如方法拦截,全局变量等,本文旨在解决这些问题
所谓事件总线,就是在当前的Vue
实例之外,再创建一个Vue实例来专门进行变量传递,事件处理,管理回调事件等1
2
3
4
5
6
7
8
9
10
11//main.js中
Vue.prototype.$bus = new Vue();
new Vue({...})
//页面一
this.$bus.$on('sayName',(e)=>{
alert('我的名字是',e)
})
//页面二
this.$bus.$emit('sayName','小明');//我的名字是 小明
所谓原型挂载,就是在main.js
中将公共变量,事件,都挂在到Vue原型上1
2
3
4
5
6
7
8
9
10
11
12
13
14//main.js
Vue.prototype.$globalData = {}
Vue.prototype.$sayName = function(e){
console.log('我的名字是',e)
}
new Vue({...})
//组件一
Vue.prototype.$globalData.name='小明';
this.$sayName('小王');//我的名字是小王
//组件二
console.log(this.$sayName.name);//小明
this.$sayName('小王');//我的名字是小王
Vuex
是Vue
提供的一种,专门用来管理vue
中的公共状态,事件等等,以应用登录为例
1 | //新建store.js |
vue是一个非常优秀的框架,其优秀的双向绑定原理,mvvm模型,组件,路由解析器等,非常的灵活方便,也使开发者能够着重于数据处理,让开发者更清晰的设计自己的业务。
双向绑定,就是数据变化的时候,自动触发视图的变化。
我们都理解,vue2.0中,双向绑定的核心为Object.defineProperty(obj, prop, descriptor)
,方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象
参数obj
为要在其上定义属性的对象。
参数prop
为要定义或修改的属性的名称。
参数descriptor
为将被定义或修改的属性描述符。
返回被传递给函数的对象。
我们可以新建一个项目,用来模拟及学习vue
双向绑定的相关内容
1 | + vue相关 |
修改index.html中的内容,引入自己创建的myVue.js。1
2
3
4
5
6
7
8
9
10
11
12+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+ </head>
+ <body>
+
+ <script src="./js/myVue.js"></script>
+ </body>
+ </html>
编辑myVue.js
的内容,来尝试一下Object.defineProperty(obj, prop, descriptor)
这个API
1 | + var data = { |
在浏览器中,打开页面,并查看控制台,对data.a
进行操作1
2
3
4
5
6
7
8
9
10
11data.a myVue.js:8
我被读取了,返回了_a的值 undefined
undefined
——————————————————————————————————————————————————————————————————————————————
data.a = 10 myVue.js:14
我被设置了,被设置的值为 10 并放进了a的对象中
10
——————————————————————————————————————————————————————————————————————————————
data.a myVue.js:8
我被读取了,返回了_a的值 10
10
可以看到,我们对data.a进行的操作,实际改变的变量是我们已经拦截的_a
变量。
目前出现的问题是,第一次读取的时候,这个值没有被设置上,在下面来模拟解决方案
声明概念_
开头的变量一版为私有变量,外部无法访问,但是我们现在在控制台中输入并修改私有变量data._a
1 | data._a |
却可以拿到私有变量中的_a
的值,也可以进行无拦截的修改,这显然是我们所不希望的
所以我们可以为Object.defineProperty(obj, prop, descriptor)
来进行封装一次,把我们理解的_a
变成一个私有变量
修改myVue.js
的内容为如下内容,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20var data = {
a: 1
}
myDefineProperty(data, 'a')
function myDefineProperty(obj,key){//对Object.defineProperty进行一次拦截,使外界无法访问私有变量value
var value = obj[key];
Object.defineProperty(obj,key,{// 为data增加
configurable: true, // 是否允许删除属性,默认true
enumerable: true, // 是否允许遍历,默认true
get: function () {
console.log('我被读取了,返回了value的值', value)
return value
},
set(newValue) {
value = newValue;
console.log('我被设置了,被设置的值为', newValue, '并放进了value的对象中')
}
})
}
然后再查看控制台
1 | data._a |
开发者就无法自行操作我们设计的私有变量value的内容了
自此,我们解决了,对一个对象属性的拦截,并且阻止了用户对我们设计的私有变量进行操作
我们知道了拦截属性之后,那么就进一步来实现,一个简单的双向绑定
我们修改一下index.html
中的内容1
2
3
4
5
6
7
8
9
10
11
12
13
14 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
+ <input type="text" @change='changeIpt'>
+ <p id='changeValue'></p>
</head>
<body>
<script src="./js/myVue.js"></script>
</body>
</html>
并在myVue.js
中,在set中添加双向绑定的操作
1 | var data = { |
然后我们在input
框里输入内容,便表现出了双向绑定的能力。
记得打开控制台哦!~下方为录制的屏幕,可能无法正常显示,可以点击上方demo来查看
觉得好的话,可以给我的 github点个star
哦
很多人在写前端代码的时候,都不做请求封装,这样容易出现的情况是,假设在一个项目中,有100个页面,每个页面都需要向服务器发送请求,来完成数据的交互,突然有一天,产品的哥们说,咱们得加个请求的验证,给请求交互的data里,加一个加密的数据,来保证数据的安全性,如果未进行封装的话,这100个页面中,每个请求都需要改一次,这就是个很庞大的工程,所以,我们在开始写代码的时候,对请求的封装,是一个很重要的设计
所谓
我们以Vue项目为例,使用vue-cli搭建项目,使axios来发送请求
文件结构如下1
2
3
4
5
6
7
8
9
10
11
12my-projext
|-node_modules
|-public
|-src
|-assets
|-components
|-router
|-store
|-views
|-App.vue
|-main.js
|-package.json
首先,在src下新建一个文件夹api
并新建一个api.js
文件
1 | my-projext |
api.js
添加如下内容1
2
3
4
5
6
7
8
9
10const apiUrl = 'https://www.yoursite.com/api/';
const axios = require('axios');
export function request(options = {}) {
axios[options.methods](
apiUrl+options.url,
options.data
).then(options.success)
.catch(options.fail)
}
经过这样的封装后,在页面中使用,就变成了
1 | page中 |
也可以在main.js
中进行统一引入,然后注入到Vue.prototype
中,这样就不用单独页面进行单独引入了
1 | //main.js |
这样的话,如果想在请求前增加请求拦截
,只需要在公共方法中,增加一个拦截即可
1 | const apiUrl = 'https://www.yoursite.com/api/'; |
我们将请求及请求拦截器,单独放置在vuex中的一个module
里
1 | my-projext |
api.js
中的代码为
1 | const axios = require('axios'); |
Page
中的代码为
1 | import {request} from '@/api/api.js'; |
懒加载的概念,意思是,使用到再加载,如果没有使用到,则不去加载
例如,我们在写一个瀑布流的图片展示站点时,一个页面,可能会有超出屏幕数倍长度的内容,如果一次性加载所有的图片资源,会造成页面的卡顿,影响用户体验,于是有了懒加载的概念,只有滚动到这一屏幕,才会加载这一屏幕的内容(一般滚动类的会加载当前屏幕及上下各一屏幕的内容),webpack
中的懒加载
,也可以这样理解。
在上一节的基础上,对项目目录进行改造
1 | webpack_learn |
上一章节中src
中index.js
的内容
1 | import _ from "lodash"; |
我们可以看到,getLodash()
是每次都会调用的,可以设想下,如果,我们不是每次都调用,改成点击按钮才调用的话,如果用户不点击这个按钮,就不会加载了
1 | import _ from "lodash"; |
这样就变成了,用户点击按钮时候,才会加载lodash
了,这个思路可以在日常做需求的时候使用,可能更好的减少初始加载速度
清理一下文件并新增一个生产与开发环境及通用配置的webpack.config.js
文件
1 | webpack_learn |
修改config
文件
1 | const path = require('path'); |
然后修改src
中index.js
的内容
1 | - import _ from "lodash"; |
然后执行打包指令
1 | npm run build # 或者 yarn build |
然后可以看到,文件目录结构变成了
1 | webpack_learn |
这样动态引入的lodash
就被打包进入了vendors~lodash.bundle.js
文件中,从而完成了模块的动态导入
针对一个多页面向目来讲,我们希望每个包的体积都更小一些。
可以使用webpack的提供的三种分离方案
清理一下文件并新增一个生产与开发环境及通用配置的config
文件
1 | webpack_learn |
修改新增的配置文件内容
修改 webpack.config.js
文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
math: './src/math.js'
},
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
title: 'Code Splitting'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
然后修改package.json
中的scripts
快捷指令
1 | { |
修改一下index.js
的内容1
2
3let name = require('./let.js');
let sayName = require('./get.js');
sayName('大家好,我的名字是'+name)
然后执行打包指令1
npm run build #也可以yarn build 打包生产环境
index.bundle.js
打包后可以看到编译的结果
1 | webpack_learn |
并且index.html
中的内容如下,引入了两个js
文件,而且,index.js
及math.js
中的代码被分离开了,达成了通过entry
的配置方式进行分离
1 | <!DOCTYPE html> |
CommonsChunkPlugin
插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
继续修改文件目录1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 webpack_learn
|- node_modules
|- package.json
|- webpack.config.js
|- yarn.lock
|- src
|- let.js
|- get.js
|- index.js
+ |- index2.js
|- math.js
|- dist
|- index.html
|- index.bundle.js
|- math.bundle.js
1 | //index2.js |
修改 webpack.config.js
文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
index2: './src/index2.js',
math: './src/math.js'
},
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
title: 'Code Splitting'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
然后执行打包指令1
npm run build #也可以yarn build 打包生产环境
然后可以看到,index2.bundle.js
与index.bundle.js
中,均出现了重复的代码
1 | ([function(e,t){e.exports="小明"},function(e,t){e.exports=function(e){console.log(e)}} |
为了解决这种情况,可以使用CommonsChunkPlugin
去重和分离 chunk
修改 webpack.config.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 const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
+ const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
index2: './src/index2.js',
math: './src/math.js'
},
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
title: 'Code Splitting'
- })
+ }),
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'common' // 指定公共 bundle 的名称。
+ })
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
然后执行打包指令1
npm run build #也可以yarn build 打包生产环境
此时打包报错了,提示信息1
Error: webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead.
修改 webpack.config.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 const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
- const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
index2: './src/index2.js',
math: './src/math.js'
},
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
title: 'Code Splitting'
- }),
- new webpack.optimize.CommonsChunkPlugin({
- name: 'common' // 指定公共 bundle 的名称。
- })
],
+ optimization: {
+ splitChunks: {
+ cacheGroups: {
+ commons: {
+ name: "commons",
+ chunks: "all",
+ minChunks: 2
+ }
+ }
+ }
+ },
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
webpack.optimize.CommonsChunkPlugin
这个模块被移除了,请使用config.optimization.splitChunks
代替,查询文档地址
但是我们发现,再次打包,也没有单独生成一个我们命名的commons.bundle.js
初步怀疑是,公共代码包里的代码体积过小,导致的没有打包,我们将index.js
和index2.js
中引入一个巨大的lodash
模块再来尝试一下
1 | //index.js |
然后执行打包指令1
npm run build #也可以yarn build 打包生产环境
打包结果如下
Asset | Size | Chunks | Chunk Names |
---|---|---|---|
commons.bundle.js | 71.1 KiB | 0 [emitted] | commons |
index.bundle.js | 1.64 KiB | 1 [emitted] | index |
index.html | 379 bytes | [emitted] | |
index2.bundle.js | 1.58 KiB | 2 [emitted] | index2 |
math.bundle.js | 1.18 KiB | 3 [emitted] | math |
可以看到,lodash
模块被打包进了commons.bundle.js
从而实现了公共代码的分离
通常,我们在开发一个应用的时候,会有生产环境与开发环境的概念,生产环境中,我们需要体积最小的代码,以及图片等静态文件使用cdn
上的服务,开发环境上,需要热更新,source-map等来快速排错,目标不同。
我们可以分别为生产环境与测试环境编写一个配置文件,并且使用webpack-merge
来组合通用配置及专属配置。
首先安装webpack-merge
1 | npm install webpack-merge # 或者yarn add webpack-merge |
清理一下文件并新增一个生产与开发环境及通用配置的config
文件,webpack.common.js
、webpack.dev.js
、webpack.prod.js
。
1 | webpack_learn |
修改新增的配置文件内容
webpack.common.js
通用文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ const path = require('path');
+ const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+ module.exports = {
+ entry: {
+ app: './src/index.js'
+ },
+ plugins: [
+ new CleanWebpackPlugin(),
+ new HtmlWebpackPlugin({
+ title: 'Production'
+ })
+ ],
+ output: {
+ filename: '[name].bundle.js',
+ path: path.resolve(__dirname, 'dist')
+ }
+ };
webpack.dev.js
开发环境配置文件1
2
3
4
5
6
7
8
9+ const merge = require('webpack-merge');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+ devtool: 'inline-source-map',
+ devServer: {
+ contentBase: './dist'
+ }
+ });
webpack.prod.js
生产环境配置文件1
2
3
4
5
6
7
8
9+ const merge = require('webpack-merge');
+ const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+ plugins: [
+ new UglifyJSPlugin()
+ ]
+ });
dist
目录,新建index.html
dist
目录建立本地服务器,并添加source-map
来快速排错,且使用webpack-merge
来合并webpack.common.js
通用配置文件中的配置内容tree-shakeing
,且使用webpack-merge
来合并webpack.common.js
通用配置文件中的配置内容然后修改package.json
中的scripts
快捷指令
1 | { |
然后执行打包指令1
2npm run serve #也可以yarn serve 运行开发环境
npm run build #也可以yarn build 打包生产环境
可以看到报了个错
1 | ERROR in app.bundle.js from UglifyJs |
这个问题在于,UglifyJs
对es6
语法的解析有问题,需要转成es5
后再次打包
webpack.prod.js
中增加babel配置
1 |
|
然后执行打包
1 | npm run build #也可以yarn build 打包生产环境 |
就打爆了生产版本,经过了tree-shaking的版本
]]>由于,我们在开发过程中,会引入很多大大小小的模块,比如一个计算方法通用的大包,封装了很多很多的方法,但是我们只用到了其中一两个方法,往往希望,打包只打包使用到的方法,未使用到的方法,不被打包进最后的代码中,以保持体积的最小化。
webpack为这种情况,提供了tree shaking
math.js
。1 | webpack_learn |
math.js
的内容1 | //math.js |
修改index.js
1 | let name = require('./let.js'); |
然后执行打包指令1
npm run build #也可以yarn build
打包后的代码如下1
2
3
4
5!function(n){var t={};function e(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return n[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}e.m=n,e.c=t,e.d=function(n,t,r){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:r})},e.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},e.t=function(n,t){if(1&t&&(n=e(n)),8&t)return n;if(4&t&&"object"==typeof n&&n&&n.__esModule)return n;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:n}),2&t&&"string"!=typeof n)for(var o in n)e.d(r,o,function(t){return n[t]}.bind(null,o));return r},e.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(t,"a",t),t},e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},e.p="",e(e.s=0)}([function(n,t,e){var r=e(1),o=e(2),u=e(3);o("大家好,我的名字是"+r),console.log(u.add(2,3))},function(n,t){n.exports="小明"},function(n,t){n.exports=function(n){console.log(n)}},function(n,t,e){"use strict";function r(n){return n*n}function o(n){return n*n*n}function u(n,t){return n+t}function c(n,t){return n-t}e.r(t),
e.d(t,"square",(function(){return r})),
e.d(t,"cube",(function(){return o})),
e.d(t,"add",(function(){return u})),
e.d(t,"cut",(function(){return c}))}]);
可以看出,我们只使用了math.js
中的add
方法,但是打包后的内容里math.js
中导出的square
、cube
、add
、cut
方法都被打包了,这显然不是我们想要的结果。
我们修改package.json
的内容
1 | { |
然后执行打包指令1
npm run build #也可以yarn build
然后我们发现,打包指令中,依然包含math.js
导出的四个方法
修改index.js
文件
1 | let name = require('./let.js'); |
修改webpack.config.js
文件增加mode:"production"
再次打包
1 | !function(e) { |
最后的结果,可以看出,add方法被打包成了
1 | function r(e, t) { |
而其他的方法并没有被打包进来,实现了代码的裁剪,根据表现来看,如果你希望使用webpack
的tree-shaking
的话,需要如下使用
exports
导出,使用方法的地方,使用impoet {xxx} from 'xx/xx'
来按需导入,使用require
是无法完成按需导入的package.json
中,增加"sideEffects": false
声明文档安全性是否拥有副作用,config.webpack.js
中,增加mode:production
或者使用webpack
的UglifyJSPlugin
插件在上一节中,已经能够生成一个能够自动重新编译本地服务器了,但是,有一个问题,在项目写的比较大的时候,每次打包的时间就会变得很长,为了解决这个问题,webpack也支持使用模块热替换的方式来进行差异修改,也就是热更新,可以在每次修改的时候,只重新刷新修改的那一部分,这样刷新的速度就会变得很快了。
webpack为这种情况,提供了插件
HotModuleReplacementPlugin
启用热模块更换,也称为HMRNamedModulesPlugin
该插件会显示模块的相对路径1 | //config.webpack.js |
然后修改package.json
中的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 //package.json
{
"dependencies": {
"babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.1.1",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.1"
},
"name": "webpack_learn",
"version": "1.0.0",
"main": "index.js",
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/preset-env": "^7.7.7",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0"
},
"scripts": {
"build": "webpack",
"watch": "webpack --watch",
- "serve": "webpack-dev-server --open",
+ "serve": "webpack-dev-server --hotOnly",
"dev": "webpack --mode development",
"product": "webpack --mode production",
"testParams": "webpack --mode production --env.production product --param1 1 --param2 2 --explane 这是一个说明",
"showColorAndProgress": "webpack --progress --colors",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": ""
}
然后在命令行中执行1
npm run serve #也可以yarn watch
此时,就是模块热更新的开发状态啦
]]>在我们的开发中,很多的代码会频繁的变动,如果每次改动都使用npm run build
来打包的话,是一件效率很低的事情,而且这个服务,一般来说,打包的时候,才需要使用,因此需要一个本地服务的,可以快捷的看到打包后的内容
webpack为这种情况,提供了观察者模式,在每次文件发生变动的时候,自动执行编译,就不用每次都npm run build
来打包了
我们可以在scripts
中,新加一条命令webpack --watch
来启动观察者模式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//package.json
{
"dependencies": {
"babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.1.1",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10"
},
"name": "webpack_learn",
"version": "1.0.0",
"main": "index.js",
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/preset-env": "^7.7.7",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0"
},
"scripts": {
"build": "webpack",
+ "watch": "webpack --watch",
"dev": "webpack --mode development",
"product": "webpack --mode production",
"testParams": "webpack --mode production --env.production product --param1 1 --param2 2 --explane 这是一个说明",
"showColorAndProgress": "webpack --progress --colors",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": ""
}
然后在命令行中执行
1 | npm run watch #也可以yarn watch |
然后我们在浏览器中,打开刚才的文件,并且查看控制台,控制台中打印了大家好,我的名字是小明
,然后我们修改一下/src/let.js
,及index.html
文件
1 | //let.js |
然后保存文件,然后在刚打开的浏览器里,刷新页面,然后控制台中,变成了大家好,我的名字是小刚
。这样,每次修改就不用执行打包指令了
开发中,最重要的一个环节,就是代码的调试,如果我们在写的代码中,出现了报错,希望很快的知道是哪里的报错,但是在webpack中,代码经过层层的模块打包,合并成了最后的一个js文件,里面的内容是编译后的内容,
我们尝试一下报错,将刚刚的get.js
中,加一个报错信息。
1 | //get.js |
1 | bundle.js:1 Uncaught Error: 这里有个报错 |
报错信息指向了bundle.js
打包后的文件,这显然不是我们想要的
这个时候,我们可以使用sourceMap
来解决这个问题。
1 | //webpack.config.js |
然后重新执行1
npm run watch
这样,控制台的报错就变成了1
2
3
4
5
6get.js:3 Uncaught Error: 这里有个报错
at e.exports (get.js:3)
at Object.<anonymous> (index.js:4)
at n (bootstrap:19)
at bootstrap:83
at bundle.js:1
指向了正确的位置,我们复原代码,进行下一步尝试1
2
3
4
5 //get.js
module.exports = function(name){
console.log(name)
- throw new Error('这里有个报错')
};
前面讲述了观察者模式及报错位置查找,但是美中不足的是,我们每次需要手动刷新来观察变化,这种情况下,可以使用webpack
中的的webpack-dev-server
模块,来完成自动更新的行为
首先安装webpack-dev-server
1 | npm install webpack-dev-server #或者yarn add webpack-dev-server |
安装完毕后,修改一下webpack.config.js
文件
1 | const path = require('path') |
声明开发服务,并将 dist
目录下的文件,作为可访问文件,并且修改package.json
中声明一条
1 | { |
然后执行
1 | npm run serve # 或者yarn serve |
执行完毕后,浏览器自动打开了一个地址为localhost:8080
的网页,打开控制台,可以看到我们刚刚写入的大家好,我的名字是小刚
,然后我们继续把小刚,改回刚才的小明
1 | //let.js |
然后保存,浏览器的控制台,自动的就变化成了大家好,我的名字是小明
;
这样就建立了一个开发用的服务器,且每次修改,都会重新编译。
]]>webpack
中配置文件
、loader
、插件
、如何安装
了这里统一加一个名词解释
参数 | 说明 | 备注 | |
---|---|---|---|
mode | 项目环境 | webpack4.0中出的模式概念,可以传入development(开发环境)/production(生产环境),理解为可以通过不同的config文件来分别配置生产与测试环境 | |
entry | 项目入口 | webpack开始打包的入口文件,打包这个文件里,引用的其他包,及引入的包所再次引入的包,每个html文档只使用一个依赖图,也就是entry | |
output | 项目出口 | webpack打包完成后,输出的文件,可以使用webpack占位符占位符 | |
module | 开发中每一个文件都可以看做 module,模块不局限于 js,也包含 css、图片等 | 使校验、调试、测试、扩展、复用都轻而易举 例如:什么是模块 | |
loader | 模块转化器,模块的处理器,对模块进行转换处理 | 提供了处理前端构建步骤的强大方法,如将额外的语言typescript 转化为javascript ,将图片 转换为base64格式 来减少一次请求等 loader使用方式 | |
plugin | 扩展插件,插件可以处理 chunk,也可以对最后的打包结果进行处理,可以完成 loader 完不成的任务 | 插件是 webpack 的支柱功能,由于插件可以携带参数/选项,你必须在 webpack 配置中,向 plugins 属性传入 new 实例。 | |
context | 上下文,也是基础目录,绝对路径,用于从配置中解析入口起点(entry point)和 loader | 默认值为path.resolve(__dirname, “app”),也就是当前路径 如何修改context |
模板 | 描述 | |
---|---|---|
[hash] | 模块标识符(module identifier)的 hash | |
[chunkhash] | chunk 内容的 hash | |
[name] | 模块名称 | |
[id] | 模块标识符(module identifier) | |
[query] | 模块的 query,例如,文件名 ? 后面的字符串 |
ES2015
中的 import
语句CommonJS
中的 require()
语句AMD
define
和 require
语句css/sass/less
文件中的 @import
语句。url(...)
)或 HTML 文件(<img src=...>
)中的图片链接(image url)1 | // webpack.config.js |
备注:context是一切打包的相对地址,所以这个值,一定要填写一个绝对地址,默认为当前目录
]]>webpack
是一个打包工具在webpack
中,还有个概念,叫插件plugins
webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。
例如,我们想在打包的时候,把A文件夹中的数据,放到B文件夹中,可以使用copy-webpack-plugin
来进行文件的复制行为
copy-webpack-plugin文档
修改原来的文件目录
1 | webpack_learn |
由于使用了copy-webpack-plugin
模块,根据commonJs
规范,我们需要require
这个文件,并且使用npm
安装它
1 | npm install copy-webpack-plugin #或者yarn add copy-webpack-plugin |
我们将config.webpack.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//config.webpack.js
const path = require('path')
+ const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
//配置函数接受两个参数env和argv
//env:环境对象
//Webpack-CLI 的命令行选项
entry: './src/index.js', //设置默认
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [{
//里面为匹配的规则
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
},
+ plugins: [
+ new CopyWebpackPlugin([
+ {
+ from: path.join(__dirname, '/staticFrom/'),//将staticFrom文件夹内所有文件
+ to: path.join(__dirname+'/dist/')//复制到根目录下/dist/文件夹内
+ }
+ ])
+ ]
}
然后再执行
1 | npm run build #或者yarn build |
然后,打包后的文件目录,变成了
1 | webpack_learn |
test.txt
文件就被复制到了dist
目录下
这样就成功的使用了webpack
插件中的复制插件copy-webpack-plugin
我们在每次打包的时候,如果是同名文件,则会覆盖之前的文件,但是,并没有清除,已经更改后的文件,例如dist
目录下的main.js
此时,我们可以使用clean-webpack-plugin
插件来清理dist
目录,但时刚才,我们的index.html
是自己手动新建的,如果清除掉,还需要新建,为了自动化完成这个工作,有两种方案
copy-webpack-plugin
插件,复制一个index.html
文件到dist
目录,方法参考上方html-webpack-plugin
插件,每次都创建一个index.html
文件首先我们使用npm
安装它们
1 | npm install clean-webpack-plugin #或者yarn add clean-webpack-plugin |
然后修改config.webpack.js
1 | //config.webpack.js |
然后再执行
1 | npm run build #或者yarn build |
这样,dist
目录,每次都会清理并新建index.html
文件了
webpack
是一个打包工具在webpack
中,还有个概念,叫插件plugins
webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。
例如,我们想在打包的时候,把A文件夹中的数据,放到B文件夹中,可以使用copy-webpack-plugin
来进行文件的复制行为
copy-webpack-plugin文档
修改原来的文件目录
1 | webpack_learn |
由于使用了copy-webpack-plugin
模块,根据commonJs
规范,我们需要require
这个文件,并且使用npm
安装它1
npm install copy-webpack-plugin #或者yarn add copy-webpack-plugin
我们将config.webpack.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//config.webpack.js
const path = require('path')
+ const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
//配置函数接受两个参数env和argv
//env:环境对象
//Webpack-CLI 的命令行选项
entry: './src/index.js', //设置默认
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [{
//里面为匹配的规则
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
},
+ plugins: [
+ new CopyWebpackPlugin([
+ {
+ from: path.join(__dirname, '/staticFrom/'),//将staticFrom文件夹内所有文件
+ to: path.join(__dirname+'/dist/')//复制到根目录下/dist/文件夹内
+ }
+ ])
+ ]
}
然后再执行
1 | npm run build #或者yarn build |
然后,打包后的文件目录,变成了
1 | webpack_learn |
test.txt
文件就被复制到了dist
目录下
这样就成功的使用了webpack
插件中的复制插件copy-webpack-plugin
webpack
可以使用配置文件,也就是名为webpack.config.js
的文件,webpack.config.js
也是遵循CommonJS规范
的1 | webpack_learn |
webpack.config.js为webpack的配置文件
1 | //webpack.config.js |
之后,我们再继续运行来打包看一下
1 | npm run build |
由于设置了输出文件的文件名,此时,文档结构改变为
1 | webpack_learn |
将index.html中的引入文件,改变为bundle.js,一样可以console出“大家好,我的名字是小明”
一条一条刷配置的事情,我们先放到后面,一步一步来
讲到配置文件了,而且配置文件遵循CommonJS规范
,那么,我们只需要遵守最终使用module.exports
来输出配置对象即可,一开始也提到过了,每个使用模块的地方,只关心引入模块的结果,并不关心内部如何实现的,我们尝试将webpack.config.js
文件修改为如下内容:
1 | //webpack.config.js |
然后为加一个script
1 | { |
然后执行指令
1 | npm run testParams #也可以使用 yarn testParams |
查看终端的console记录
1 | { production: 'product' } { |
env
参数为{ production: 'product' }
argv
参数包含了我们在package.json里写入的param1
,param2
,explane
参数,我们可以根据script携带参数,来在webpack的配置文件里做一些操作
另外,这个文件也是支持promise来操作的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//webpack.config.js
const path = require('path');//引入nodejs的path模块
module.exports = function(env,argv){
//配置函数接受两个参数env和argv
//env:环境对象
//Webpack-CLI 的命令行选项
console.log('promise前',(new Date().getTime()));//promise前 1578884886284
return new Promse((resolve,reject)=>{
setTimeout(()=>{
console.log('promise后',(new Date().getTime()));//promise后 1578884889286
resolve({
entry: './src/index.js',//设置入口js文件
output: {
filename: 'bundle.js',//设置输出的js文件名
path: path.resolve(__dirname, 'dist'//设置输出的地址
}
})
},3000)
});
}
// path.resolve() 方法将路径或路径片段的序列解析为绝对路径
// __dirname为项目根目录
可以看出,使用Promise,可以使webpack三秒后打包
webpack也支持多次打包
1 | module.exports = [ |
关于使用配置文件,webpack是会默认寻找根目录下的webpack.config.js
文件,所以执行指令的时候,执行的是
1 | npx webpack #这是简写模式 |
webpack 可以通过–config指令,来指定使用哪个config文件来进行打包
例如
1 | //package.json |
目前的内容,有一些什么问题呢?
我们在代码中,使用了let
、module.exports
等es6出的内容,众所周知,es6新加的内容,在一些老版本的浏览器里并不支持,就会报错,为了解决这种问题,我们就需要引入babel
来将es6
转化为es5
,这样大部分老版本浏览器就支持了
这个时候,就要引入webpack
的loader
的概念,loader将在下一节记录
webpack
的配置文件,也就是名为webpack.config.js
的文件中,有一个module
的属性配置,可以在此属性内,配置相关的规则
可以使用module.rules
来进行规则匹配,键值为一个数组,分别传入不同的匹配规则rule
创建模块时,匹配请求的规则数组。这些规则能够修改模块的创建方式。这些规则能够对模块(module)应用 loader,或者修改解析器(parser)。
每个rule
可以分为三部分 - 条件(condition)
,结果(result)
和嵌套规则(nested rule)
。
例如:我们想给js文件,使用babel-loader来把es6代码转为es5代码
既然使babel-loader
,那么我们首先需要安装babel-loader
,根据babel-loader
的npm社区所写
1 | npm install -D babel-loader @babel/core @babel/preset-env webpack |
建议使用yarn
来安装
1 | yarn add -D babel-loader @babel/core @babel/preset-env webpack |
然后将webpack.config.js
中加入规则
1 | //webpack.config.js |
然后再执行打包指令
1 | npm run build #或者 yarn build |
然后打包后的代码,就没有了let
与module.exports
了,就可以在不支持es6
的浏览器中使用了,如果要是用其他的loader
,也可以使用哦
上一章创建的项目中,我们新建几个文件
1 | webpack_learn |
其中代码如下
1 | //let.js中↓↓↓↓↓↓ |
然后,我们执行打包命令
1 | npx webpack |
然后我们看到了,文档目录变成了
1 | webpack_learn |
多出来了一个dist目录,和main.js文件,这个文件,就是被webpack所打包出来的文件,整合了let.js、get.js、index.js的所有js代码。
我们再修改一下目录
1 | webpack_learn |
新建一个index.html文件,并引入刚打包好的js文件1
2
3
4
5
6
7
8
9
10
11//index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="./main.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>
将这个文件运行到浏览器中,查看控制台,就可以看到打印了“大家好,我的名字是小明”。
这个地方使用了webpack的npx webpack打包指令
可以这样说,执行 npx webpack,会将我们的脚本作为入口起点,然后 输出 为 main.js。Node 8.2+ 版本提供的 npx 命令,可以运行在初始安装的 webpack 包(package)的 webpack 二进制文件
由表现,我们可以看出,配置项中
1 | 默认入口是/src/index.js |
我们也可以通过package.json来增加命令,来达到更好理解的编译指令,相当于给npm run 建了一个快捷方式
1 | //package.json |
这样,我们就可以使用快捷方式来打包啦
1 | npm run build |
另外,对webpack传入指令 –mode可以指定开发模式 development为开发模式 production为生产模式,后面会提到
1 | //package.json |
这样,可以使用
1 | npm run product#运行生产模式 |
备注:
小技巧:
可以通过webpack-cli的一些参数,来让webpack的编译更有趣一些