一:webpack核心功能
webpack安装和使用
推荐本地安装 / 基本安装与使用
mkdir webpack-demo && cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
编译结果分析
编译的代码为什么放在 eval 中
报错时,看不到其他代码的干扰,相当于一个新的环境。更加容易调试
配置文件
默认情况下,webpack会读取webpack.config.js 文件作为配置文件,但也可以通过cli参数 —config 来指定某个配置文件.
npx webpack --config test.js
配置文件中通过 CommonJS 模块导出一个对象,对象中的各种属性对应不同的webpack配置
//webpack.config.js
module.exports = {}
问题:webpack 的配置文件只能使用CommonJS 模块化?
答:在打包的过程中,是在node中进行的,参与运行的。
可以想像为 执行 var config = require(“./webpack.config.js”)这段代码
基本配置
1.mode : 编译模式 development(开发) production(生产)
默认值是 production
2.entry :入口文件
entry:"./src/main.js"
3.output: 出口文件
output:{
filename:'bundle.js'
}
devtool 配置
source map 源码地图
实际上是一个配置,配置中记录了原始代码,还记录了转换后的代码和原始代码的对应关系
注:不建议在生产环境使用,文件较大,且代码暴露
编译过程
入口和出口
1.模块化代码中,比如require(“./“),表示当前js文件所在的目录
2.在路径处理中,”./“表示node运行目录
__dirname : 所有情况下,都表示当前js文件所在的目录
var path = require("path")
path.resolve(__dirname,"dist")
入口 entry
默认值是./src/index.js
通过 entry 进行配置,真正配置的是 chunk
出口 output
默认值是./dist/main.js
output 告诉webpack在哪里输出它所创建的bundle,以及如何命名这些文件。
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};
多个入口的时候,就不可以使用静态规则了
规则
name : chunkname
hash : 总的资源hash,通常用于解决缓存问题
拓展:为什么要多页面打包
很多场景下,单页应用的开发模式并不适用。比如公司经常开发一些活动页:
[https://www.demo.com/activity/activity1.html](https://www.demo.com/activity/activity1.html)
[https://www.demo.com/activity/activity2.html](https://www.demo.com/activity/activity2.html)
[https://www.demo.com/activity/activity3.html](https://www.demo.com/activity/activity3.html)
上述三个页面是完全不相干的活动页,页面之间并没有共享的数据。然而每个页面都使用了React框架,并且三个页面都使用了通用的弹框组件。在这种场景下,就需要使用webpack多页面打包的方案了:
- 保留了传统单页应用的开发模式:使用Vue,React等前端框架(当然也可以使用jQuery),支持模块化打包,你可以把每个页面看成是一个单独的单页应用
- 独立部署:每个页面相互独立,可以单独部署,解耦项目的复杂性,你甚至可以在不同的页面选择不同的技术栈
因此,我们可以把多页应用看成是乞丐版的前端微服务。
Loader
补充: 其对应的配置是 module
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
loader 有两个属性
test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个 loader。
webpack.config.js
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
}
};
module.exports = {
mode: "development",
module: {
rules: [{
test: /index\.js/, //正则表达式匹配模块路径
use: [{
loader: "./loaders/test-loader.js"
} //每个加载器的使用是一个对象
] //匹配之后,使用哪些加载器
}], //模块匹配规则
}
}
拓:可不可以实现自己的loader?
loader 的使用规则是从后往前。
loader 中是否可以使用ES6 Module ?
不可以,是在打包过程中遇到的,使用的是node环境。
plugin
loader功能定位 是转换代码
plugin本质就是一个带有 apply 方法的对象
var plugin = {
apply: function(compiler){
}
}
通常将该对象写成构造函数的模式
class MyPlugin{
apply(compiler){//在这里注册事件,类似window.onload
}
}
var plugin = new MyPlugin()
使用
webpack.config.js
var MyPlugin = require("./plugins/MyPlugin");
module.exports = {
mode: "development",
plugins: [new MyPlugin()],
};
区分环境
使用不同的配置文件进行打包
使用webpack自带的函数功能
module.exports = function (env) {
console.log(env);
if (env && env.prod) {
return {
mode: "production",
devtool: "none",
};
} else {
return {
mode: "development",
devtool: "source-map",
};
}
};
"scripts": {
"dev": "webpack",
"prod": "webpack --env.prod"
},
其他细节配置
20分钟
二:常用扩展
清除输出目录
即 再次 打包的时候,删除之前的打包文件
安装使用
npm i --save-dev clean-webpack-plugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports
plugins: [
new CleanWebpackPlugin(),
],
};
自动生成页面
html-webpack-plugin
理解:在打包后自动引入js文件,因为js文件可能名称是变化的,所以需要这个插件。
npm i --save-dev html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins:[
new HtmlWebpackPlugin()
]
如何实现
emit
利用 fs 模块生成一个页面文件
给文件内容的合适的位置添加一个script元素
元素的src路径引用打包后的js
根据页面的模板去生成
new HtmlWebpackPlugin({
template: "./public/index.html", //模板配置文件
chunks:"all" // 默认配置
}),
复制静态资源
copy-webpack-plugin
把静态文件拷贝到指定目录,纯静态内容
npm i copy-webpack-plugin -D
开发服务器
开发阶段:往往我们希望把最终生成的代码部署到服务器上,来模拟真实环境
webpack-dev-server 库
npm i webpack-dev-server -D
这里使用需要注意版本问题
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.11.0"
当执行 webpack-dev-server 命令原理:
1.内部执行webpack命令,传递命令参数
2.开启watch
3.注册hooks :类似于 plugin. webpack-dev-server 会向webpack中注册一些钩子函数
1.将资源列表(assets)保存起来
2.禁止webpack输出文件
4.用 express 开启一个服务器,监听某个端口,当请求到达后,根据请求的路径,给与相应的资源内容
配置
- port 端口
open 自动打开浏览器
devServer:{
port: 8000,
open:true,
// index:"index.html"
}
proxy 代理
代理服务器,前面的 “/api” 是一个正则匹配,后面则是替换的网址
proxy:{
"/api":'http://open.duyiedu.com'
}
注意:这样代理请求的结果中 host 依旧是本地的域名 localhost:8000
proxy: {
"/api": {
target: "http://open.duyiedu.com",
changeOrigin: true, //更改请求头的host和origin
},
},
- stats 输出信息
stats:{
modules:false
}
普通文件处理
- file-loader 生成依赖文件到输出目录,然后将模块文件设置为:导出一个路径
- url-loader 将依赖的文件转换为: 导出一个base64格式的字符串
url-loader 在 小图片文件 的时候可以直接生成 base64 实现,减少网络请求npm install file-loader --save-dev
解决路径问题
三:css工程化
css工程化概述
css的问题与如何解决?
类名冲突的问题
- 命名约定
- bem
- css in js
- RN中使用较多
- css model
- 命名约定
重复样式
网站的主色调,背景 文字 边框
- css in js
- 预编译器
- less
- sass
- css文件细分问题
利用 webpack 拆分css
css-loader
注:要在页面中进行使用才能发现 css-loader 的用处 ,下面这张是没有试用css代码,直接打包,并无报错。
如果在css代码中增加复杂处理,如图片
.red{
background:url("./bg.png")
}
经过css-loader 转换后变成js代码
var import1 = require("./bg.png")
module.exports = `.red{
background:url("${import1}")
}`
注意
var res = require("./assets/banner.css");
var css = res.default.toString(); //注意这里的数据结构
var style = document.createElement("style");
style.innerHTML = css;
document.head.appendChild(style);
总结:css-loader 做了什么
- 将css文件的内容作为字符串导出
- 将css中的其他依赖作为require导入,以便webpack分析依赖
style-loader**
style-loader可以将css-loader转换后的代码进一步处理,将css-loader导出的字符串加入到页面的style元素中
BEM
Block Element Modifier
- Block 页面中的大区域,表示最顶级的划分
- element 区域中的组成部分
- modifier 可选,通常表示状态
css in js
css module
预编译器less
解决重复样式的问题
四:js兼容性
babel 的安装和使用
补充:@babel/polyfill 已过时,目前被core-js 与 generator-runtime 所取代
安装
- @babel/core babel 核心库,提供了编译所需的所有api
- @babel/cli 提供一个命令行工具,调用核心库的api完成编译
npm i -D @babel/core @babel/cli
使用
- 按文件编译
babel 要编译的文件 -o 编译结果文件
- 按目录编译
babel 要编译的整个目录 -d 编译结果文件
npx babel js/a.js -o js/b.js
配置
{
"presets": ["@babel/preset-env"]
}
兼容的浏览器
@babel/preset-env 需要根据兼容的浏览器范围来确定如何编译,如postcss一样,可以使用文件 .browserslistrc
来描述浏览器的兼容范围
last 3 version
> 1%
not ie <= 8
自身的配置
{
"presets": [
["@babel/preset-env",{
"配置项1":"配置值"
}]
]
}
比较常见的配置项是 usebuiltins ,默认值是false
由于该预设仅转换新的语法,并不对新的api进行任何处理
babel 插件
@babel/plugin-proposal-optional-chaining
安全性的读取
const obj = {
foo:{
bar:{
baz:42
}
}
}
const baz = obj?.foo?.bar?.baz //42
babel-plugin-transform-remove-console
该插件会移除源码中的控制台输出语句
@babel/plugin-transform-runtime
用于提供一些公共的API 这些api会帮助代码转换
在webpack中使用babel
npm i -D @babel/core babel-loader
module.exports = {
mode: "development",
devtool: 'source-map',
module: {
rules: [{
test: /\.js$/,
use: "babel-loader"
}]
}
}
拓展课程 未学习
五:性能优化
性能优化概述
不要过早的去关注性能
构建性能
指的是开发阶段的构建性能。优化的目标,是降低从打包开始,到代码效果呈现所经过的时间
传输性能
指的是js代码传输到浏览器经过的时间
1.总传输量
2.文件数量 即js文件数量
3.浏览器缓存
运行性能
取决于如何书写高性能的代码
减少模块解析
模块解析 :抽象语法树分析 依赖分析 模块语法替换
哪些模块不需要解析
模块中无其它依赖,一些已经打包好的第三方库,比如 jquery
配置 module.noParse
正则表达式
module.exports ={
mode:"development",
module:{
noParse:/jquery/
}
}
对比打包时间
优化loader性能
- 限制loader 的应用范围
**
对于某些库,不使用 loader
例如,babel-loader 可以转换ES6或更高版本的语法,可是有些库本身就是用ES5语法写的,不需要转换,使用babel-loader 反而浪费构建时间 loadsh
module.rule.exclude
module.rule.include
module.exports = {
module:{
rules:[
{
test:/.\js$/,
exclude:/lodash/,
use:"babel-loader"
}
]
}
}
- 缓存loader的结果
如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变。
cache-loader
放到最前面却能够决定后续的loader是否运行。
实际上,loader运行的过程中,还包含一个过程,即 pitch
module.exports = {
mode:'development',
devtool:'source-map',
module:{
rules: [{
test: /.\js$/,
use: [{
loader: "cache-loader",
options: {
cacheDirectory: './cache'
}
}, "babel-loader"]
}]
}
}
- 为 loader 的运行开启多线程
thread-loader
会开启一个线程池,线程池中包含适量的线程
它会把后续的loader 放到线程池的线程中运行,以提高构建效率
由于后续的loader会放到新的线程中,所以,后续的loader不能
- 使用webpack api 生成文件
- 无法使用定义的 plugin api
- 无法访问 webpack options
热替换 HMR
与 webpack-dev-server
的区别
dev-server: 代码变动 => 浏览器刷新 重新请求所有资源
热替换 : 代码变动 => 浏览器仅请求改动的资源
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:"development",
devtool: "source-map",
devServer:{
open:true,
hot:true
},
plugins:[new HtmlWebpackPlugin({
template:"./public/index.html"
})]
}
import a from "./a";
console.log(a)
if(module.hot){
//是否开启了热更新
module.hot.accept() //接受热更新
}
当开启了热更新后,webpack-dev-server 会向打包结果中注入 module.hot 属性
默认情况下,webpack-dev-server 不管是否开启了热更新,当重新打包后,都会调用 location.reload
刷新页面,但是如果运行了 module.hot.accept()
,将改变这一行为module.hot.accept()
的作用是让 webpack-dev-server
通过 socket
管道,把服务器更新的内容发送到浏览器,然后将结果交给插件 HotModuleReplacementPlugin
注入的代码执行。
热替换发生在代码运行期
替换过程
手动分包 (传输性能)
dll : dynamic link library 动态链接库
原理:
1.先单独的打包公共模块
单独配置 webpack.dll.config.js 文件进行打包
const webpack = require("webpack");
const path = require("path")
module.exports = {
mode:'production',
entry:{
jquery:['jquery'],
lodash:['lodash']
},
output:{
filename:'dll/[name].js',
library:"[name]" //每个bundle暴露的全局变量名
},
plugins:[
new webpack.DllPlugin({
path:path.resolve(__dirname,"dll","[name].manifest.json"),
name:"[name]"
})
]
}
webpack.DllPlugin
webpack 自带的工具
自动分包
代码压缩
使用什么压缩工具?
目前最流行的代码压缩工具主要有两个, UglifyJS
和 Terser
UglifyJs
是一个传统的代码压缩工具,但是不支持 ES6 语法
Terser
是一个新的代码压缩工具。支持 ES6+ 语法。 webpack安装后会内置 Terser
关于副作用 (side effect)
副作用:函数运行过程中,可能会对外部环境造成影响的功能
如果函数中包含以下代码,该函数叫做副作用函数
1.异步代码
2.localStorage
3.对外部数据的修改
如果一个函数没有副作用,同时,函数的返回结果仅依赖参数,则该函数叫做纯函数(pure function)
tree shaking
场景:某些模块导出的代码并不一定会被用到
使用:webpack2
开始就支持了 tree shaking
只要是生产环境 tree shaking 自动开启
我们在编写代码的时候,尽量
- 使用
export default
导出 而不使用export default {xxx}
导出 - 使用
import {xxx} from 'xxx'
而不使用import xxx from 'xxx'
使用第三方库
lodash使用的是 commonjs 的方式导出,对于这些库,tree shaking 无法发挥作用。lodash-es
两次打包对比
作用域分析 (不推荐)
tree-shaking 本身并没有完善的作用域分析,可能导致一些 dead code 函数中的依赖仍然被视为依赖webpack-deep-scope-plugin
提供了作用域分析,可解决这些问题
css tree shakingpurgecss-webpack-plugin
懒加载
**
Eslint(其它优化)
代码风格检查
安装使用npm i -D eslint
配置项
env
配置代码的运行环境
- browser : 代码是否在浏览器环境中运行
- es6 : 是否启用es6的全局api 例如 Promise
parseroptions
该配置指定 eslint 对哪些语法的支持
- ecmaVersion: 支持的ES语法版本
- sourceType
- script 传统脚本
- module 模块化脚本
rules
查阅文档
bundle analyzer
bundle分析webpack-bundle-analyzer
gzip
使用 compression-webpack-plugin
插件对打包结果进行预压缩,可以移除服务器的压缩时间