构建:将源代码转换为可执行的JavaScript、css、HTML代码 主要包括:
- 代码转换(ts->js、scss->css等、Sass编译)
- 文件优化(CSS压缩)
- 代码分割、模块合并
- 自动刷新(监听本地源代码的变化,自动重新构建、刷新浏览器)
- 代码校验(验证JavaScript语法)、自动发布
Babel支持浏览器环境,可以实现网页实时将 ES6 代码转为 ES5 代码,但是会对网页性能有影响,所以需要使用构建工具在生产环境将 ES6 代码进行提前转码。
代码打包
打包时候需要关注代码,以避免因体积过大而导致加载时间过长。
- “懒加载”当前用户所需要的内容,显著地提高你的应用性能。这是诸如 Webpack,Rollup 和 Browserify(factor-bundle)这类打包器支持的一项技术,能够创建多个包并在运行时动态加载。
- 尽管并没有减少应用整体的代码体积,但可以避免加载用户永远不需要的代码,并在初始加载的时候减少所需加载的代码量。
- 应用中引入代码分割的最佳方式是通过动态 import() 语法,当 Webpack 解析到该语法时,会自动进行代码分割。
- 代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。通过entry配置、import动态加载实现
浏览器逐渐支持模块化,但是开发者仍倾向于用代码打包工具webpack
原因:
- 首次访问网站时候,相比加载多个小型模块,加载一个代码包用户体验更佳
- 单页应用的流行,网页的功能、实现代码变得庞大,趋向模块化,相比不打包-模块的依赖关系完全由文件的加载顺序决定,webpack优化模块加载顺序,同时将许多碎小文件打包成一个整体,减少单页面内的衍生请求次数,提高网站效率
- webpack也衍生了很多功能来协助开发,例如热替换、线上部署依赖打包工具
- 使用webpack构建浏览器和服务器环境的应用(同构应用),即用一份代码分别编译为浏览器和服务器环境下的代码。使用API和模块时需要考量两个环境是否都支持。
- 将JavaScript模块化等高级语法进行转换编译,以兼容老版本的浏览器
- 将代码打包,同时进行混淆,提高代码的安全性。
webpack核心概念
- entry 入口
- module 一切文件皆模板
- loader 转换文件 把module原内容转换成新的
- plugin 注入钩子,webpack构建过程中广播对应事件,插件可以监听这些事情的发生
- chunk 最后输出模块化的组合,一个chunk由多个module组成
- 获取初始化参数:shell命令行+配置文件webpack.config.js
- 开始编译:上一步得到参数初始化Compiler对象,加载所有配置的插件,通过执行run开始执行编译
- 确定入口:根据配置中的entry
- 编译模块:从入口出发,调用所有配置的Loader对模块进行翻译
- 解决依赖:具体的方法看webpack采用的模块机制,common.js、AMD、ESM
- 输出资源:根据依赖将多个module打包成一个chunk,再将其转换为文件加入输出列表。
- 完成输出:根据配置文件,输出到对应目录
Compiler对象负责监听文件、启动编译
模块依赖:apply函数传入module.export对象
loader:apply方法传入Compiler对象
1、Entry:模块的入口
- context:查找相对路径的根目录
- entry:可以是字符串、数组、对象、函数。前两者只会生成一个 “main” chunk。对象则会生成以键为名的多个chunk。
动态Entry:异步、同步 项目有多个页面且每个页面都有一个入口,且入口数还在不断增多
entry:()=>{
return {
a:'./pages/a',
b:'./pages/b',
}
};
entry:()=>{
return new Promise((resolve)=>{
resolve({
a:'./pages/a',
b:'./pages/b',
});
});
};
2、output:输出配置对象
output是一个对象,属性是配置如何输出最终想要的代码的配置项
输出文件名
- 有入口的chunk的output output. filename, 用string写死
当多个Chunk,根据chunk的标识来区分输出文件
filename:'[ name ]. js'
filename:'[ hash:8 ]. js' 可以指定hash值的长度
- 无入口的chunk的output chunkFilename只用于在运行中生成的chunk
使用CommonChunkPlugin、使用import动态加载等的情况
内置变量
| 变量名 | 含义 |
|---|---|
| id | chunk的唯一标识,从0开始 |
| name | |
| hash | chunk的唯一标识的hash值 |
| chunkhash | chunk内容的hash值 |
输出的路径
output. path输出文件放在本地的位置,string类型的绝对路径
一般通过node.js的path模块去获取绝对路径。webpack运行在node.js环境
异步加载的资源
- webpack打包复杂项目时候,可能会有构建出的资源需要异步加载,就要把资源放在线上资源的对应的URL地址
- 发布到线上的html文件使用jsonp引入异步加载资源 url+ filename
- 涉及异步加载, output. crossOriginLoading配置异步插入的Script标签的crossorigin值
anonymous加载资源可能默认不会带上用户的cookie,使用use-credentials带cookie
构建导出库
构建被其他模块导入使用的库,搭配使用libraryTarget、library
library导出库的名称
libraryTarget配置以什么方式导出库,字符串的枚举类型。 导出的库可以支持多种配置导入:
- var
- commonJS方式导入使用
- commonJS2
- this
- window
- global
output.library='Name'
lb_code是指导出库的代码内容,那输出和使用
// var
// webpack输出代码
var Name=lb_code;
// 使用库的方法
Name.doSomething();
// commonJS方式导入使用
exports['Name']=lb_code;
require('library-name-in-npm')['Name'].doSomething();
// commonJS2
module.exports=lb_code;
require('library-name-in-npm').doSomething();
// this
this['Name']=lb_code;
this.Name.doSomething();
// window
window['Name']=lb_code;
window.Name.doSomething();
// global
global['Name']=lb_code;
global.Name.doSomething();
commonJS、commonJS2方式导入使用的时候,可以配置要导出的子模块
module.exports=lb_code['a'];
require('library-name-in-npm')===1;
3、Module:配置处理模块的规则
rules
loader:处理源文件
- Webpack 本身只能处理 JavaScript 模块,如果要处理其他类型的文件,就需要使用loader将不同语言转换为javascript,可以串联多个loader,将返回值交给下一个loader继续处理
- webpack是运行在node之上的,一个loader就是一个node模块,这个模块需要导出一个处理函数。webpack也为loader提供了一些可以调用的API,例如source map,处理文件的上下文
- 根据应用场景,loader有同步异步之分,具体体现在内部实现上
- 写好的loader一般发布到远程npm,然后下载源码到node_module下面
css文件
npm install style-loader css-loader --save-dev
原理:将CSS的内容用JavaScript里的字符串存储起来,在网页执行JavaScript时通过DOM操作,动态地向HTML的head标签中插入style标签。
然后配置webpack.config.js文件,增加modules中的规则,实现针对性的规则配置
'use strict';
const path=require('path');
module.exports={
entry:'./src/index.js', //打包文件路径
context:
output:{
path:path.join(__dirname,'dist'), //输出文件路径
filename:'bundle.js' //输出文件名
},
mode:'production'
modules:{
rules:[
//正则匹配到以css结尾的文件使用下面的loader
test:/\.css$/,
use:['style-loader','css-loader']
]
}
};
- 通过test、include、exclude三个配置项来选中Loader要应用规则的文件
- 配置规则除了正则表达式,也支持正则表达式的数组,数组各项代表|或
- 默认处理顺序是从后往前,先交给css-loader
- 可以通过enforce选项可以将一个loader的执行顺序提到最前pre,或者最后post
这样做会导致JavaScript文件变大且网页加载时间变长,可以通过webpack Plugin机制来实现单独输出CSS文件
图片文件也有file-loader
parse与noParse
-
noParse配置项可以让webpack忽略对没有采用模块化的文件的递归解析和处理,可以提高构建性能。
-
因为例如jQuery、chartjs等庞大又没有采用模块化标准(不含import、require、define,可以直接在浏览器中运行),解析他们耗时有没有意义
-
parse可以更细粒度地控制哪些模块语法被解析,精确到语法层面
parse:{
amd: false, // 禁用
commjs: false,
system: false,
harmony: false, // 禁用ES6 import/export
browserify: false,
}
因为webpack是以模块化JavaScript文件为入口的,所以内置了对模块化JavaScript的解析功能。
4、resolve寻找模块所对应的文件
配置webpack如何寻找模块所对应的文件
alias 文件路径简写
import Utility from '../../utilities/utility';
import Utility from 'Utilities/utility'; // 使用别名
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'), // 用Utilities代替后面的路径
}
可以在给定对象的键后的末尾添加 $,以表示精准匹配以xyz结尾的导入语句
alias: {
xyz$: path.resolve(__dirname, 'path/to/file.js')
}
mainFields 找取适合的代码
有些第三方库会针对不同的环境提供几份代码(ES5、ES6...),那么webpack在打包import导入的第三方包的时候会根据mainFields的顺序去选取导入的文件
mainFields: ["browser", "module", "main"]
查看第三方包的package.json 含有这些字段:
{
...
main: 'build/d3.Node.js',
browser: 'build/d3.js',
module: 'index',
...
}
extensions 导入文件的文件后缀
自动加入导入文件的文件后缀
extensions: [".js", ".json"]
先找file.js再找file.json
import File from '../path/to/file'
modules 配置查找第三方库的目录
webpack默认到node_module下查找
modules: ["node_modules"]
如果添加一个目录到模块搜索目录,此目录优先于 node_modules/ 搜索
modules: ['./src/components', "node_modules"]
import './src/components/button' import '.button'
5、plugins扩展功能:作用于整个构建过程
plugins是一个plugin数组
- plugin:用于资源加载以外的其他打包/压缩/文件处理等功能
- 是一个函数,或者包含apply方法的对象:
- apply 方法有一个参数 compiler,通过 compiler 可以给 webpack 编译打包过程中添加钩子
- 构造函数里面挂载两个结果回调函数,done、fail
- 通过钩子的回调函数 callback 拿到打包结果对象 compilation(通过compilation.assets 获取资源文件信息),然后对打包结果对象 compilation 进行修改
使用
模块加载器
- 扩展webpack功能,通过在构建流程中注入钩子函数来实现,几乎所有webpack无法实现的功能都可以找到开源的plugin解决。
- 涉及webpack.config.js文件的内容。
- webpack运行中设计很多事件,plugin可以监听这些事件,在特定的时刻调用webpack提供的API
- 来实现对webpack现有功能的扩展,比如打包优化、文件压缩功能。
上面提到的输出单独的CSS文件
'use strict';
const path=require('path');
module.exports={
entry:'./src/index.js', //打包文件路径
output:{
path:path.join(__dirname,'dist'), //输出文件路径
filename:'bundle.js' //输出文件名
},
mode:'production'
modules:{
rules:[
//正则匹配到以css结尾的文件使用下面的loader
test:/\.css$/,
use:['style-loader','css-loader']
],
plugins:[
new ExtractTextPlugin({
filename: `[name]_[contenthash:8].css`
}),
]
}
};
然后安装新引入的插件
npm i -D extract-text-webpack-plugin
npm下载、require语法导入、webpack.config.js的plugins数组中配置注册
部分需要快速执行命令的,在package.json中注册script
plugins属性是一个数组,里面的每一项都是插件的一个实例,在实例化一个组件时可以通过构造函数传入这个组件支持的配置属性。
DevServer
启动一个HTTP服务器用于启动webpack,服务网页请求,并接收webpack发出的文件更变信号,通过webSocket协议自动刷新网页做到实时预览。
安装:
npm i -D webpack-dev-server
开启后控制台显示:
project is running at http://localhost:8080/
访问这个网址就能获取项目根目录下的index.html文件
而生成的bundle.js的网址是 http://localhost:8080/bundle.js,所以需要在index.html文件中script引入bundle.js
监听的实现:DevServer会让webpack在构建出的JavaScript代码里注入一个代理客户端用于控制网页,网页和DevServer通过webSocket通信,DevServer在收到来自webpack的文件变化通知时,通过注入的客户端控制网页刷新。通过inline配置
来自webpack的文件:即entry本身和依赖的文件才会被webpack添加到监听列表里。上述index.html文件是脱离JavaScript模块系统的,不会被webpack探索到。
devServer.hot 模块热替换
默认会自动刷新整个页面来做实时预览
模块热替换:不重新加载整个网页的情况下,局部更新,默认关闭
启动DevServer的时候带上--hot参数
或者dev.Server.hot配置
devServer.inline代理客户端自动注入
- 实时预览功能依赖一个注入页面的代理客户端,取接收来自devServer的命令并刷新网页的工作。
- devServer.inline配置是否将这个代理客户端自动注入将运行在页面中的chunk
- 不开启devServer.将无法直接控制要开发的网页,这时他会通过刷新iframe来实时预览
常用命令
即package.json文件中的script
npm run dev
执行dev-server.js文件,关于该文件的了解看 使用express、http-proxy-middleware代理的中间件等实现本地服务器的搭建,注意这里是负责模拟后台的数据
和webpack-dev-server到底有什么区别和联系
webpack-dev-server
- webpack官方提供的一个小型Express服务器。
- webpack-dev-server 主要为静态文件提供web服务以及自动刷新和热替换(HMR)
- 通过websocket协议来告知浏览器更新文件,以实现实时的预览。
编辑src中的代码,保存后浏览器端会实时自动刷新。LiveReload技术
npm run build
执行build.js文件,控制编译过程
package.json
管理依赖项:
dependencies、devDependencies、peerDependencies dependencies : npm i
- utility libraries such as lodash, classnames etc
- the "main" libraries of your project
peerDependencies :
- react, react-dom
- styled-components
- etc
devDependencies:npm i -D
- formatting libraries: eslint, prettier, ...
- bundlers: webpack, gulp, parceljs, ...
- babel and all its plugins
- everything related to tests: enzyme, jest, ...
- a bunch of other libraries: storybook, react-styleguidist, husky, ...
当我们的包在开发时依赖另一个包的时候,我们应该把依赖包作为devDependencies来安装,这样他们就不会污染模块使用者的node_modules。npm install 会按照实际执行情况按需下载devDependencies里的依赖
例子:
"devDependencies": {
"@types/node": "^15.6.0",
"typescript": "^4.2.4",
"webpack-dev-server": "^3.11.2",
"gulp": "3.9.0",
"del": "2.2.0"
},
"dependencies": {
"webpack-cli": "^4.7.0"
}
保存这个文件后,IDE将自动开始安装gulp和del。 若没有,请右击package.json文件选择Restore Packages。
peerDependencies的目的是提示宿主环境去安装满足插件peerDependencies所指定依赖的包,然后在插件import或者require所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题。A的peerDependencies会和B一起被下载 适用于同一个页面中只出现一个版本或一个实例的npm包
注意:NPM3反对同行的依赖(peer dependencies)
版本号
package.json 中版本号 ^ 和 ~
- 波浪符号:~1.15.2,库会去匹配更新到1.15.x的最新版本,但是他不会自动更新到1.16.0
- 插入符号:^3.3.4,库会去匹配3.x.x中最新的版本,但是他不会自动更新到4.0.0。
语义版本号 分为X.Y.Z三位,分别代表主版本号、次版本号和补丁版本号。当代码变更时,版本号按以下原则更新。
- 如果只是修复bug,需要更新Z位。
- 如果是新增了功能,但是向下兼容,需要更新Y位。
- 如果有大变动,向下不兼容,需要更新X位