如何在浏览器端实现模块化 {ignore}
(webpack实际上是前端工程化过程中,用到的一个工具,叫做构建工具。webpack是很重要,可以说是彻底改变了你的开发方式,过去建立一个页面,然后在页面中引用一些js,js里面写代码,有了了webpack之后,开发方式发生翻天覆地的变化……)
## 课程简介
前置知识:ES6、模块化、包管理器、git
讲解特点:
1. 合适的深度:webpack使用层面很简单,但原理层面非常复杂
2. 合适的广度:webpack生态圈极其繁荣,有海量的第三方库可以融入到webpack
## 浏览器端的模块化
问题:效率问题:精细的模块划分带来了更多的JS文件,更多的JS文件带来了更多的请求,降低了页面访问效率
兼容性问题:浏览器目前支持ES6的模块化标准,但还存在兼容性问题
工具问题:浏览器不支持npm下载的第三方包
这些仅仅是前端工程化的一个缩影。当开发一个具有规模的程序,将遇到非常多的非业务问题,这些问题包括:执行效率、兼容性、代码的可维护性可扩展性、团队协作、测试 …,我们将这些问题称之为工程问题。工程问题与业务无关,但它深刻的影响到开发进度,如果没有一个好的工具解决这些问题,将使得开发进度变得极其缓慢,同时也让开发者陷入技术的泥潭。
## 根本原因 思考:上面提到的问题,为什么在node端没有那么明显,反而到了浏览器端变得如此严重呢?
答:在node端,运行的JS文件在本地,因此可以本地读取文件,它的效率比浏览器远程传输文件高的多
根本原因:在浏览器端,开发时态(devtime)和运行时态(runtime)的侧重点不一样
hope - 开发时态,devtime:
1. 模块划分越细越好
2. 支持多种模块化标准
3. 支持npm或其他包管理器下载的模块
4. 能够解决其他工程化的问题
hope - 运行时态,runtime:
1. 文件越少越好
2. 文件体积越小越好
3. 代码内容越乱越好
4. 所有浏览器都要兼容
5. 能够解决其他运行时的问题,主要是执行效率问题
这种差异在小项目中表现的并不明显,可是一旦项目形成规模,就越来越明显,如果不解决这些问题,前端项目形成规模只能是空谈
## 解决办法
既然开发时态和运行时态面临的局面有巨大的差异,因此,我们需要有一个工具,这个工具能够让开发者专心的在开发时态写代码,然后利用这个工具将开发时态编写的代码转换为运行时态需要的东西。这样的工具,叫做构建工具
这样一来,开发者就可以专注于开发时态的代码结构,而不用担心运行时态遇到的问题了。
## 常见的构建工具
- webpack**
- grunt
- gulp
- browserify
- fis
- 其他
webpack的安装和使用 {ignore}(webpack是构建工具是把开发时候写的很爽的代码,转变成运行时候很爽的代码)
> webpack官网:https://www.webpackjs.com/
> 目前的最新版本:webpack4
## webpack简介
webpack是基于模块化的打包(构建)工具,它把一切视为模块(不管是js代码、图片、css……开发时候的东西全部视为模块,这里面的模块的概念范畴比之前认识的要大)它通过一个开发时态的入口模块为起点(通常是一个js文件 .js ),分析出所有的依赖关系,然后经过一系列的过程(压缩、合并),最终生成运行时态的文件。
webpack的特点:
- 为前端工程化而生:webpack致力于解决前端工程化,特别是浏览器端工程化中遇到的问题,让开发者集中注意力编写业务代码,而把工程化过程中的问题全部交给webpack来处理
- 简单易用:支持零配置,可以不用写任何一行额外的代码就使用webpack
- 强大的生态:webpack是非常灵活、可以扩展的,webpack本身的功能并不多,但它提供了一些可以扩展其功能的机制,使得一些第三方库可以融于到webpack中
- 基于nodejs:由于webpack在构建的过程中需要读取文件,因此它是运行在node环境中的(浏览器不能读取文件)
- 基于模块化:webpack在构建过程中要分析依赖关系,方式是通过模块化导入语句进行分析的,它支持各种模块化标准,包括但不限于CommonJS、ES6 Module
## webpack的安装
webpack通过npm安装,它提供了两个包:npm i -D webpack webpack-cli
- webpack:核心包,包含了webpack构建过程中要用到的所有api
- webpack-cli:提供一个简单的cli命令,它调用了webpack核心包的api,来完成构建过程
( 这里安装的是开发依赖不是生产依赖,生产环境里面最终运行的是webpack构建之后的代码,构建完成之后还需要webpack参与运行吗?webpack只是在构建过程中起作用,一旦构建完成就和webpack没有关系了,所以构建过程使开发阶段的东西,我们开发中写代码,通过webpack构建,构建完成之后就直接运行了,所以就是在开发依赖中 )
安装方式:
- 全局安装:可以全局使用webpack命令,但是无法为不同项目对应不同的webpack版本
- 本地安装**:推荐,每个项目都使用自己的webpack版本进行构建
## 使用shell<br />**webpack**<br />
默认情况下,webpack会以**./src/index.js**
作为入口文件分析依赖关系,打包到**./dist/main.js**
文件中
通过—mode选项可以控制webpack的打包结果的运行环境(npx webpack —mode=development开发环境;npx webpack —mode=production生产环境)
模块化兼容性
由于webpack同时支持CommonJS和ES6 module,因此需要理解它们互操作时webpack是如何处理的
## 同模块化标准
如果导出和导入使用的是同一种模块化标准,打包后的效果和之前学习的模块化没有任何差异
## 不同模块化标准
不同的模块化标准,webpack按照如下的方式处理
## 最佳实践
代码编写最忌讳的是精神分裂,选择一个合适的模块化标准,然后贯彻整个开发阶段。( 绝大部分的第三方库,使用的是CommonJS的方式导出 )
练习 — 酷炫数字查找特效(”dev”: “webpack —mode=development —watch“ 加上watch,表示监控,用来监控文件的变化。如果文件一变化就会自动的打包)
首先初始化一下,npm init ( 做配置 ),安装webpack webpack-cli;dist目录里面都是我们要运行的东西,页面就放在那就可以;
新建文件夹util,模块isPrime.js 判断是否是素数;模块radColor.js 产生一个随机的颜色和产生一个随机数字;模块number.js 创建一个类,里面有开启定时器、关闭定时器的两个方法,其中在开启定时器中要引入一个回调函数,在渲染页面的时候定时器要启动;
新建文件夹page,模块appendNumber.js,对产生的自然数和质数进行的渲染操作,有创建span标签里面放入自然数 || 质数,是质数的时候要添加一些样式,然后中间的样式操作,创建div加入到body中,然后对它进行了添加样式的操作,在原来的css样式中已经添加了transition过渡,这样就发生了样式变化,用getComputedStyle()让其重新渲染,就可以做到样式的变化,就可以进行过渡;模块event.js 总和模块的功能,就是注册事件,加入基本操作。
src文件中index.js,导入模块import ‘./page/event.js’
编译结果分析 — 其实是对main.js进行分析
my-main.js—我们需要做的事情,webpack根据一个入口文件src.index.js文件,来分析出这个文件的依赖关系,分析出来之后把这些依赖关系,依赖的模块全部合并成一个文件。合并的结果就是一个普通的js代码,不存在任何的模块化;实现模块化的时候,每一个路径作为一个对象的属性名,全部集合到一个对象里面传参到立即执行函数里面作为参数;
配置文件
webpack提供的cli支持很多的参数,例如--mode
,但更多的时候,我们会使用更加灵活的配置文件来控制webpack的行为
默认情况下,webpack会读取**webpack.config.****j****s`**``文件作为配置文件,但也可以通过CLI参数
—config```来指定某个配置文件 npx webpack —config xx.js
配置文件中通过CommonJS模块导出一个对象,对象中的各种属性对应不同的webpack配置( webpack支持多模块化,但是配置文件里面,只能用CommonJS,不能用es6,之前说webpack支持多种模块,指的是通过入口文件构建依赖关系的时候,无论使用CommonJS还是es6模块化……它都能够识别,在配置文件的时候在打包的过程中,这个是在node环境里面,它要读取配置文件的内容require(’ ./webpack.config.js ‘), 也就是说它要把这个配置文件运行一次, 是在node环境里面执行的,就要使用CommonJS导出,然后读取配置文件之后,构建依赖关系打包过程中并不会运行代码,打包完成之后到了./dist/main.js中才会运行。配置文件的代码是要在编码过程中参与运行的,又是node环境所以只能使用CommonJs模块化)
注意:配置文件中的代码,必须是有效的node代码()
当命令行参数与配置文件中的配置出现冲突时,以命令行参数为准。
基本配置:
1. mode:编译模式,字符串,取值为development或production,指定编译结果代码运行的环境,会影响webpack对编译结果代码格式的处理
2. entry:入口,字符串,指定入口文件
3. output:出口,对象,指定编译结果文件
devtool 配置 {ignore} 解决调试问题
## source map 源码地图
## webpack中的source map
前端发展到现阶段,很多时候都不会直接运行源代码,可能需要对源代码进行合并、压缩、转换等操作,真正运行的是转换后的代码
这就给调试带来了困难,因为当运行发生错误的时候,我们更加希望能看到源代码中的错误,而不是转换后代码的错误;
例如:jquery压缩后的代码:https://code.jquery.com/jquery-3.4.1.min.js 为了解决这一问题,chrome浏览器率先支持了source map,其他浏览器纷纷效仿,目前,几乎所有新版浏览器都支持了source map;source map实际上是一个配置,配置中不仅记录了所有源码内容,还记录了和转换后的代码的对应关系
下面是浏览器处理source map的原理
**最佳实践:
1. source map 应在开发环境中使用,作为一种调试手段
2. source map 不应该在生产环境中使用,source map的文件一般较大,不仅会导致额外的网络传输,还容易暴露原始代码。即便要在生产环境中使用source map,用于调试真实的代码运行问题,也要做出一些处理规避网络传输和代码暴露的问题。
## webpack中的source map
使用 webpack 编译后的代码难以调试,可以通过 devtool 配置来优化调试体验
具体的配置见文档:https://www.webpackjs.com/configuration/devtool/
webpack 编译过程 {ignore}**
webpack 的作用是将源代码编译(构建、打包)成最终代码
整个过程大致分为三个步骤
1. 初始化
2. 编译
3. 输出
## 初始化
此阶段,webpack会将CLI参数、配置文件、默认配置进行融合,形成一个最终的配置对象。对配置的处理过程是依托一个第三方库yargs
完成的(了解就好);此阶段相对比较简单,主要是为接下来的编译阶段做必要的准备;
目前,可以简单的理解为,初始化阶段主要用于产生一个最终的配置
## 编译
1. 创建chunk**
chunk是webpack在内部构建过程中的一个概念,译为块
,它表示通过某个入口找到的所有依赖的统称。(可以把它简单的认为我们构建源代码的时候,要通过一个入口来构建找到依赖关系,这些依赖关系的模块就是统称 chunk)
根据入口模块(默认为./src/index.js
)创建一个chunk;每一个chunk是有名字的,意思是chunk有可能是多个的,实际上入口文件是可以多个的。
每个chunk都有至少两个属性:
- name:默认为main
- id:唯一编号,开发环境和name相同,生产环境是一个数字,从0开始
2. 构建所有依赖模块**
> AST(抽象语法树)在线测试工具:https://astexplorer.net/
简图
3. 产生chunk assets资源**
在第二步完成后,chunk中会产生一个模块列表,列表中包含了模块id和模块转换后的代码**
接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets
,资源列表可以理解为是生成到最终文件的文件名和文件内容(文件名:./dist/main.js 文件内容:( function( modules ) { } )( { …… } );为什么叫做资源(文件)列表呢?它不是单个文件吗?有可能是多个文件,文件名:./dist/main.js.map,文件内容:……;这个文件也是同一个chunk生成出来的,后面还可能有别的文件,通常情况下没有配置的时候,确实生成一个文件。例子:之前我们得到了表格,把模块最终形成一个文件名,文件名默认的名称和chunk的name是一样的,然后还有对应的文件内容,这个时候还没有生成文件。我们的资源列表中所有的文件内容联合起来,相当于作为一个字符串拼接,通过hash算法生成固定长度的字符串)
> chunk hash 是根据所有chunk assets的内容生成的一个hash字符串
hash:一种算法,具体有很多分类,特点是将一个任意长度的字符串转换为一个固定长度的字符串,而且可以保证原始内容不变,产生的hash字符串就不变
简图
4. 合并chunk assets**
将多个chunk的assets合并到一起,并产生一个总的hash
## 输出
此步骤非常简单,webpack将利用node中的fs模块(文件处理模块),根据编译产生的总的assets,生成相应的文件。
## 总过程
涉及术语
1. module:模块,分割的代码单元,webpack中的模块可以是任何内容的文件,不仅限于JS
2. chunk:webpack内部构建模块的块,一个chunk中包含多个模块,这些模块是从入口模块通过依赖分析得来的
3. bundle:chunk构建好模块后会生成chunk的资源清单,清单中的每一项就是一个bundle,可以认为bundle就是最终生成的文件
4. hash:最终的资源清单所有内容联合生成的hash值
5. chunkhash:chunk生成的资源清单内容联合生成的hash值
6. chunkname:chunk的名称,如果没有配置则使用main
7. id:通常指chunk的唯一编号,如果在开发环境下构建,和chunkname相同;如果是生产环境下构建,则使用一个从0开始的数字进行编号
入口和出口
> node内置模块 - path: https://nodejs.org/dist/latest-v12.x/docs/api/path.html
出口
这里的出口是针对资源列表的文件名或路径的配置
出口通过output进行配置
入口
入口真正配置的是chunk 入口通过entry进行配置
规则:**- name:chunkname
- hash: 总的资源hash,通常用于解决缓存问题
- chunkhash: 使用chunkhash
- id**: 使用chunkid,不推荐
入口和出口的最佳实践 {ignore}
具体情况具体分析;下面是一些经典场景
##一个页面一个JS
源码结构
<br />|—— src<br /> |—— pageA 页面A的代码目录<br /> |—— index.js 页面A的启动模块<br /> |—— ...<br /> |—— pageB 页面B的代码目录<br /> |—— index.js 页面B的启动模块<br /> |—— ...<br /> |—— pageC 页面C的代码目录<br /> |—— main1.js 页面C的启动模块1 例如:主功能<br /> |—— main2.js 页面C的启动模块2 例如:实现访问统计的额外功能<br /> |—— ...<br /> |—— common 公共代码目录<br /> |—— ...<br />
webpack配置js<br />module.exports = {<br /> entry:{<br /> ** pageA: "./src/pageA/index.js"**,<br />** pageB: "./src/pageB/index.js",**<br /> ** pageC: ["./src/pageC/main1.js", "./src/pageC/main2.js"]**<br /> },<br /> output:{<br /> **filename:"[name].[chunkhash:5].js"**<br /> }<br />}<br />
这种方式适用于页面之间的功能差异巨大、公共代码较少的情况,这种情况下打包出来的最终代码不会有太多重复
## 一个页面多个JS
源码结构
<br />|—— src<br /> |—— pageA 页面A的代码目录<br /> |—— index.js 页面A的启动模块<br /> |—— ...<br /> |—— pageB 页面B的代码目录<br /> |—— index.js 页面B的启动模块<br /> |—— ...<br /> |—— statistics 用于统计访问人数功能目录<br /> |—— index.js 启动模块<br /> |—— ...<br /> |—— common 公共代码目录<br /> |—— ...<br />
webpack配置js<br />module.exports = {<br /> entry:{<br /> ** pageA: "./src/pageA/index.js",**<br /> ** pageB: "./src/pageB/index.js",**<br /> ** statistics: "./src/statistics/index.js"**<br /> },<br /> output:{<br /> ** filename:"[name].[chunkhash:5].js"**<br /> }<br />}<br />
这种方式适用于页面之间有一些独立、相同的功能,专门使用一个chunk抽离这部分JS有利于浏览器更好的缓存这部分内容。
## 单页应用
所谓单页应用,是指整个网站(或网站的某一个功能块)只有一个页面,页面中的内容全部靠JS创建和控制。 vue和react都是实现单页应用的利器。
源码结构
<br />|—— src<br /> |—— subFunc 子功能目录<br /> |—— ...<br /> |—— subFunc 子功能目录<br /> |—— ...<br /> |—— common 公共代码目录<br /> |—— ...<br /> |—— index.js<br />
webpack配置js<br />module.exports = {<br /> **entry: "./src/index.js",**<br /> output:{<br /> ** filename:"index.[hash:5].js"**<br /> }<br />}<br />
loader
webpack做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中。更多的功能需要借助webpack loaders( 加载器 )和 webpack plugins( 插件 )完成。
webpack loader: loader本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回。(通常会把loader写成一个模块,创建loders文件)
loader函数的将在模块解析的过程中被调用,以得到最终的源码。
全流程:
chunk中解析模块的流程:
chunk中解析模块的更详细流程:
处理loaders流程:
loader配置:
完整配置
```js
module.exports = {
module: { //针对模块的配置,目前版本只有两个配置,rules、noParse
rules: [ //模块匹配规则,可以存在多个规则
{ //每个规则是一个对象
test: /.js$/, //匹配的模块正则
use: [ //匹配到后应用的规则模块
{ //其中一个规则
loader: “模块路径”, //loader模块的路径,该字符串会被放置到require中
options: { //向对应loader传递的额外参数
}
}
]
}
]
}
}
```
简化配置
js<br />**module.exports = {**<br />** module: { //针对模块的配置,目前版本只有两个配置,rules、noParse**<br />** rules: [ //模块匹配规则,可以存在多个规则**<br />** { //每个规则是一个对象**<br />** test: /\.js$/, //匹配的模块正则**<br />** use: ["模块路径1", "模块路径2"]//loader模块的路径,该字符串会被放置到require中**<br />** }**<br />** ]**<br />** }**<br />**}**<br />
有一个第三方库来简析这个module中options npm i -D loader-utils;let loaderUtils = require( “loder-utils” );let options = loderUtils.getOptions(this);除了往options里面添加之外,也可以在loader:” ./loaders/test_loaders?changeVar=未知数 “这样options也是可以读出来的。
plugin
loader的功能定位是转换代码,而一些其他的操作难以使用loader完成,比如:
- 当webpack生成文件时,顺便多生成一个说明描述文件
- 当webpack编译启动时,控制台输出一句话表示webpack启动了……
这种类似的功能需要把功能嵌入到webpack的编译流程中,而这种事情的实现是依托于plugin的( plugin 就是在注册事件的 )
plugin的本质是一个带有apply方法的对象
js<br />**var plugin = {**<br />** apply: function(compiler){**<br />** **<br />** }**<br />**}**<br />
通常,习惯上,我们会将该对象写成构造函数的模式
```js
class MyPlugin{
apply(compiler){
}
}
v**ar plugin = new MyPlugin();**
```
要将插件应用到webpack,需要把插件对象配置到webpack的plugins数组中,如下:
js<br />**module.exports = {**<br />** plugins:[**<br />** new MyPlugin()**<br />** ]**<br />**}**<br />
apply函数会在初始化阶段,创建好Compiler对象后运行。
compiler对象是在初始化阶段构建的,整个webpack打包期间只有一个compiler对象,后续完成打包工作的是compiler对象内部创建的compilation;
apply方法会在创建好compiler对象后调用,并向方法传入一个compiler对象
(compiler 和 compilation区别:compiler是在内部创建的compilation具体的打包过程是compilation来完成的。当webpack启动监听的时候,watch: true;这样打包完成之后不会停止。也就是我们的compilation重新创建了,compiler没有重新创建)
compiler对象提供了大量的钩子函数(hooks,可以理解为事件),plugin的开发者可以注册这些钩子函数,参与webpack编译和生成。
你可以在apply方法中使用下面的代码注册钩子函数:js<br />**class MyPlugin{**<br />** apply(compiler){**<br />** compiler.hooks.事件名称.事件类型(name, function(compilation){**<br />** //事件处理函数**<br />** })**<br />** }**<br />**}**<br />
事件名称
即要监听的事件名,即钩子名,所有的钩子:https://www.webpackjs.com/api/compiler-hooks
事件类型
这一部分使用的是 Tapable API,这个小型的库是一个专门用于钩子函数监听的库。
它提供了一些事件类型:
- tap:注册一个同步的钩子函数,函数运行完毕则表示事件处理结束
- tapAsync:注册一个基于回调的异步的钩子函数,函数通过调用一个回调表示事件处理结束
- tapPromise:注册一个基于Promise的异步的钩子函数,函数通过返回的Promise进入已决状态表示事件处理结束
处理函数
处理函数有一个事件参数```compilation**```
区分环境 {ignore}**
有些时候,需要针对生产环境和开发环境分别书写webpack配置( webpack里面是有一个 webpack.config.js 配置文件在发挥作用,配置文件里面可能出现这种情况,将来根据源代码打包,可能是生产环境的打包也有可能是开发环境的打包,它不仅仅区别在于里面设置的mode:”development” ,例如:devtool:”source-map”,开发环境面我们就直接使用source-map文件,那么在生产环境里面,我们可能不需要这样生成,devtool:”none”.因此我们可能需要根据不同的环境做不同的配置 )
为了更好的适应这种要求,webpack允许配置不仅可以是一个对象,还可以是一个函数**
js<br />**module.exports = env => {**<br />** return {**<br />** //配置内容**<br />** }**<br />**}**<br />
在开始构建时,webpack如果发现配置是一个函数,会调用该函数,将函数返回的对象作为配置内容,因此,开发者可以根据不同的环境返回不同的对象。在调用webpack函数时,webpack会向函数传入一个参数env,该参数的值来自于webpack命令中给env指定的值,例如
```shell
npx webpack —env abc # env: “abc”
npx webpack —env.abc # env: {abc:true}
npx webpack —env.abc=1 # env: {abc:1}
npx webpack —env.abc=1 —env.bcd=2 # env: {abc:1, bcd:2}
```
这样一来,我们就可以在命令中指定环境,在代码中进行判断,根据环境返回不同的配置结果。
其他细节配置 {ignore}
## context
js<br />**context: path.resolve(__dirname, "app")**<br />
该配置会影响入口和loaders的解析,入口和loaders的相对路径会以context的配置作为基准路径,这样,你的配置会独立于CWD(current working directory 当前执行路径)
## output
### library
js<br />**library: "abc"**<br />
这样一来,打包后的结果中,会将自执行函数的执行结果暴露给abc
### libraryTarget
js<br />**libraryTarget: "var"**<br />
该配置可以更加精细的控制如何暴露入口包的导出结果
其他可用的值有:
- var:默认值,暴露给一个普通变量
- window:暴露给window对象的一个属性
- this:暴露给this的一个属性
- global:暴露给global的一个属性
- commonjs:暴露给exports的一个属性
- 其他:https://www.webpackjs.com/configuration/output/#output-librarytarget
## target
js<br />**target:"web" //默认值**<br />
设置打包结果最终要运行的环境,常用值有
- web: 打包后的代码运行在web环境中
- node:打包后的代码运行在node环境中
- 其他:https://www.webpackjs.com/configuration/target/
## module.noParsejs<br />**noParse: /jquery/**<br />
不解析正则表达式匹配的模块,通常用它来忽略那些大型的单模块库,以提高构建性能
## resolve
resolve的相关配置主要用于控制模块解析过程
./src/index.js
例子1:let $ = require( “jquery” ); A: node在查找jQuery B: webpack在查找jQuery)
例子2:if( Math.random( ) < .5 ){ require( “./a” ) 打包结果中包含:A:index.js 和 a.js B: index.js C: index.js 有可能 a.js }
例子3:let a = require( “./a” ); 为什么我没有书写后缀名,我仍然可以找到a.js?答:因为webpack会根据extensions配置自动补全后缀名。(场景在webpack中)
### modulesjs<br />**modules: ["node_modules"] //默认值**<br />
当解析模块时,如果遇到导入语句,**require("test")**
,webpack会从下面的位置寻找依赖的模块
1. 当前目录下的node_modules
目录
2. 上级目录下的node_modules
目录
3. …
### extensionsjs<br />**extensions: [".js", ".json"] //默认值**<br />
当解析模块时,遇到无具体后缀的导入语句,例如require("test")
,会依次测试它的后缀名
- test.js
- test.json
### aliasjs<br />**alias: {**<br />** "@": path.resolve(__dirname, 'src'),**<br />** "_": __dirname**<br />**}**<br />
有了alias(别名)后,导入语句中可以加入配置的键名,例如require("@/abc.js")
,webpack会将其看作是require(src的绝对路径+"/abc.js")
。在大型系统中,源码结构往往比较深和复杂,别名配置可以让我们更加方便的导入依赖
## externalsjs<br />**externals: {**<br />** jquery: "$",**<br />** lodash: "_"**<br />**} **从最终的bundle中排除掉配置的配置的源码<br />
例如,入口模块是js<br />**//index.js**<br />**require("jquery")**<br />**require("lodash")**<br />
生成的bundle是:
js<br />**(function(){**<br />** ...**<br />**})({**<br />** "./src/index.js": function(module, exports, __webpack_require__){**<br />** __webpack_require__("jquery")**<br />** __webpack_require__("lodash")**<br />** },**<br />** "jquery": function(module, exports){**<br />** //jquery的大量源码**<br />** },**<br />** "lodash": function(module, exports){**<br />** //lodash的大量源码**<br />** },**<br />**})**<br />
但有了上面的配置后,则变成了
js<br />**(function(){**<br />** ...**<br />**})({**<br />** "./src/index.js": function(module, exports, __webpack_require__){**<br />** __webpack_require__("jquery")**<br />** __webpack_require__("lodash")**<br />** },**<br />** "jquery": function(module, exports){**<br />** module.exports = $;**<br />** },**<br />** "lodash": function(module, exports){**<br />** module.exports = _;**<br />** },**<br />**})**<br />
这比较适用于一些第三方库来自于外部CDN的情况,这样一来,即可以在页面中使用CDN,又让bundle的体积变得更小,还不影响源码的编写
## stats stats控制的是构建过程中控制台的输出内容
webpack安装和使用
./src/index.js---
require('./a');
console.log(`webpack I'm coming`);
./src/a.js---
import bbb from './a_copy.js'
console.log(`我在a.js中` + bbb);
./src/a_copy.js---
export default 'a-copy';
console.log('a_copy.js')
npx webpack --mode development
dist/main.js---
/* a_copy.js
我在a.js中a-copy
webpack I'm coming */
炫酷的数字查找特效
./src/index.js
import "./page/event"
./src/util/isPrime.js---
/*判断n是否是素数(质数) 素数:仅能被1和自身整除 6 2,5 */
export default function (n) {
if (n < 2) {
return false;
}
for (let i = 2; i <= n - 1; i++) {
if (n % i === 0) {
//发现,2到n-1之间,有一个数能整除n
return false;
}
}
return true;
}
./src/util/number.js---
/*生成数字*/
import isPrime from "./isPrime"
export default class NumberTimer {
constructor(duration = 500) {
this.duration = duration;
this.number = 1; //当前的数字
this.onNumberCreated = null; //当一个数字产生的时候,要调用的回调函数
this.timerId = null;
}
start() {
if (this.timerId) {
return;
}
this.timerId = setInterval(() => {
this.onNumberCreated && this.onNumberCreated(this.number, isPrime(this.number))
this.number++;
}, this.duration)
}
stop() {
clearInterval(this.timerId);
this.timerId = null;
}
}
./src/util/radColor.js---
/*返回一个随机的颜色*/
var colors = ["#f26395", "#62efab", "#ef7658", "#ffe868", "#80e3f7", "#d781f9"];
export function getRandom(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
export default function () {
var index = getRandom(0, colors.length);
return colors[index];
}
./src/page/appendNumber.js---
/* 在页面中创建并且生成数字 */
import radColor from "../util/radColor"
import { getRandom } from "../util/radColor"
import $ from "jquery";
var divContainer = $("#divContainer");
var divCenter = $("#divCenter");
export default function (n, isPrime) {
var span = $("<span>").text(n);
if (isPrime) {
var color = radColor();
span.css("color", color);
createCenterPrimeNumber(n, color)
}
divContainer.append(span);
//产生中间的数字
createCenterNumber(n);
}
function createCenterNumber(n) {
divCenter.text(n)
}
/* 在中间产生一个素数 */
function createCenterPrimeNumber(n, color) {
var div = $("<div>").addClass("center").css("color", color).text(n);
$("body").append(div);
//加入了div后,强行让页面重新渲染
getComputedStyle(div[0]).left; //只要读取某个元素的位置或尺寸信息,则会导致浏览器重新渲染 reflow
div.css("transform", `translate(${getRandom(-200, 200)}px, ${getRandom(-200, 200)}px)`)
.css("opacity", 0)
}
./src/page/event.js---
import NumberTimer from "../util/number"
import appendNumber from "./appendNumber"
var n = new NumberTimer(100);
n.onNumberCreated = function (n, isPrime) {
appendNumber(n, isPrime);
}
//该模块用于注册事件
var isStart = false; //默认没有开始
window.onclick = function () {
if (isStart) {
n.stop();
isStart = false;
}
else {
n.start();
isStart = true;
}
}
编译结果分析 其实就是对./dist/main.js分析
//合并两个模块 ./src/a.js ./src/index.js
(function (modules) {
var moduleExports = {}; //用于缓存模块的导出结果
//require函数相当于是运行一个模块,得到模块导出结果
function __webpack_require(moduleId) { //moduleId就是模块的路径
if (moduleExports[moduleId]) {
//检查是否有缓存
return moduleExports[moduleId];
}
var func = modules[moduleId]; //得到该模块对应的函数
var module = {
exports: {}
}
func(module, module.exports, __webpack_require); //运行模块
var result = module.exports; //得到模块导出的结果
moduleExports[moduleId] = result; //缓存起来
return result;
}
//执行入口模块
return __webpack_require("./src/index.js"); //require函数相当于是运行一个模块,得到模块导出结果
})({ //该对象保存了所有的模块,以及模块对应的代码
"./src/a.js": function (module, exports) {
/* console.log('module_a');
module.exports = '2333' */
eval("console.log(\"module a\")\nmodule.exports = \"a\";\n //# sourceURL=webpack:///./src/a.js")
},
"./src/index.js": function (module, exports, __webpack_require) {
eval("console.log(\"index module\")\nvar a = __webpack_require(\"./src/a.js\")\na.abc();\nconsole.log(a)\n //# sourceURL=webpack:///./src/index.js")
/* let data = require('./src/a.js')
console.log(data)
console.log('index.js') */
}
});
配置文件
webpack.config.js---
module.exports = {
entry: "./src/main.js",
output: {
filename: "bundle.js"
}
}
入口和出口
webpack.config.js---
var path = require("path")
module.exports = {
mode: "production",
entry: {
main: "./src/index.js", //属性名:chunk的名称, 属性值:入口模块(启动模块)
a: ["./src/a.js", "./src/index.js"] //启动模块有两个
},
output: {
path: path.resolve(__dirname, "target"), //必须配置一个绝对路径,表示资源放置的文件夹,默认是dist
filename: "[id].[chunkhash:5].js" //配置的合并的js文件的规则
},
devtool: "source-map"
}
pathtest.js---
//该对象提供了大量路径处理的函数
var path = require("path") //导出了一个对象
var result = path.resolve(__dirname, "src");
console.log(result);
/* c:\Users\lenovo\Desktop\DuYi-Webpack-master\1. webpack核心功能\1-9. 入口和出口\node\src */
dirname.js---
/*node环境中
./ : 1. 模块化代码中,比如require("./"),表示当前js文件所在的目录
2. 在路径处理中,"./"表示node运行目录
__dirname: 所有情况下,都表示当前运行的js文件所在的目录,它是一个绝对路径 */
console.log(__dirname)
/* c:\Users\lenovo\Desktop\DuYi-Webpack-master\1. webpack核心功能\1-9. 入口和出口\node */
loders
webpack.config.js---
module.exports = {
mode: "development",
module: {
rules: [
{//数组里面的东西是从下往上面看的
test: /index\.js$/, //正则表达式,匹配模块的路径
use: ["./loaders/loader1", "./loaders/loader2"] //匹配到了之后,使用哪些加载器
}, //规则1
{
test: /\.js$/, //正则表达式,匹配模块的路径
use: ["./loaders/loader3", "./loaders/loader4"] //匹配到了之后,使用哪些加载器
} //规则2 require(url)
],
}
}
2 1 4 3
./loaders/loader1.js && ./loders/loaders2.js && ./loders/loader3.js && ./loders/loader4.js
module.exports = function(sourceCode){
console.log("loader1");
return sourceCode;
}
module.exports = function(sourceCode){
console.log("loader2");
return sourceCode;
}
module.exports = function(sourceCode){
console.log("loader3");
return sourceCode;
}
module.exports = function(sourceCode){
console.log("loader4");
return sourceCode;
}
练习1--处理样式
webpack.config.js---
module.exports = {
mode: "development",
devtool: "source-map",
module: {
rules: [{
test: /\.css$/,
use: ["./loaders/style-loader"]
}]
}
}
./loaders/style-loader.js---
module.exports = function (sourceCode) {
var code = `var style = document.createElement("style");
style.innerHTML = \`${sourceCode}\`;
document.head.appendChild(style);
module.exports = \`${sourceCode}\``;
return code;
}
./src/index.js---
var content = require("./assets/index.css")
/* 这个请求是不是交给CommonJS来请求的呢?不是的,也不会交给es6模块化运行,这么写是为了给webpack 看的,形成依赖关
系,webpack做的事情就是把这个文件内容读出来,处理形成抽象语法树,找到其他依赖……但是这个交给webpack是否能够运行呢?
当然可以,但是默认情况下,这个抽象语法树一分析,就会报错。那么现在要做的就是无非就是写一个loaders,凡是看到后缀名
为css的文件,交给相应的loader,只要这个loader返回一个可以是别的js代码,能够让他进行后续的抽象语法树分析就可以了 */
console.log(content); //css的源码字符串
./src/assets/index.css---
body{
background: #333;
color: #fff;
}
练习2--处理图片
webpack.config.js---
module.exports = {
mode: "development",
devtool: "source-map",
module: {
rules: [
{
test: /\.(png)|(jpg)|(gif)$/, use: [{
loader: "./loaders/img-loader.js",
options: {
limit: 3000, //3000字节以上使用图片,3000字节以内使用base64
filename: "img-[contenthash:5].[ext]"
}
}]
}
]
}
}
./src/index.js---
var src = require("./assets/webpack.png")
console.log(src);
var img = document.createElement("img")
img.src = src;
document.body.appendChild(img);
./src/assets---- png
./loaders/img-loader.js---
var loaderUtil = require("loader-utils")
function loader(buffer) { //给的是buffer
console.log("文件数据大小:(字节)", buffer.byteLength);
var { limit = 1000, filename = "[contenthash].[ext]" } = loaderUtil.getOptions(this);
if (buffer.byteLength >= limit) {
var content = getFilePath.call(this, buffer, filename);
}
else{
var content = getBase64(buffer)
}
return `module.exports = \`${content}\``;
}
loader.raw = true; //该loader要处理的是原始数据
module.exports = loader;
function getBase64(buffer) {
return "data:image/png;base64," + buffer.toString("base64");
}
function getFilePath(buffer, name) {
var filename = loaderUtil.interpolateName(this, name, {
content: buffer
});
this.emitFile(filename, buffer);
return filename;
}
plugin
webpack.config.js---
var MyPlugin = require("./plugins/MyPlugin")
module.exports = {
mode: "development",
watch: true,
plugins: [
new MyPlugin()
]
}
./plugins/Myplugin.js--
module.exports = class MyPlugin {
apply(compiler) {
//在这里注册事件,类似于window.onload $(function(){})
/* compiler.hooks.事件名称.事件类型(name,function(){} ) */
compiler.hooks.done.tap("MyPlugin-done", function(compilation){
//事件处理函数
console.log("编译完成");
})
}
}
练习-添加文件列表
webpack.config.js--
var FileListPlugin = require("./plugins/FileListPlugin")
module.exports = {
mode: "development",
devtool: "source-map",
plugins: [
new FileListPlugin("文件列表.md")
]
}
plugins/FileListPlugin.js--
module.exports = class FileListPlugin {
constructor(filename = "filelist.txt"){
this.filename = filename;
}
apply(compiler) {
compiler.hooks.emit.tap("FileListPlugin", complation => {
var fileList = [];
for (const key in complation.assets) {
var content = `【${key}】
大小:${complation.assets[key].size()/1000}KB`;
fileList.push(content);
}
var str = fileList.join("\n\n");
complation.assets[this.filename] = {
source() {
return str
},
size() {
return str.length;
}
}
})
}
}
区分环境:
webpack.config.js--
var baseConfig = require("./webpack.base")
var devConfig = require("./webpack.dev")
var proConfig = require("./webpack.pro")
module.exports = function (env) {
if (env && env.prod) {
return {
...baseConfig,
...proConfig
}
}
else {
return {
...baseConfig,
...devConfig
}
}
}
webpack.base.js--
module.exports = {
entry: "./src/index.js",
output: {
filename: "scripts/[name]-[hash:5].js"
}
}
webpack.dev.js--
module.exports = {
mode: "development",
devtool: "source-map"
}
webpack.pro.js--
module.exports = {
mode: "production",
devtool: "none"
}
package.json--
"scripts": {
"dev": "webpack",
"prod": "webpack --env.prod"
},