跟着官网学习webpack
声明:以下内容基本上都是对webpack官网案例进行学习后一些总结,方便自己后续进行复习,更详细的内容请查阅官网
webpack的安装
创建目录webpack-demo,并且进行npm的初始化
npm init -y // 生成package.json文件
npm install --save-dev webpack
如果你使用 webpack 4+ 版本,你还需要安装 CLI。
npm install --save-dev webpack-cli
请确保安装了 Node.js 的最新版本。使用 Node.js 最新的长期支持版本(LTS - Long Term Support),是理想的起步。使用旧版本,你可能遇到各种问题,因为它们可能缺少 webpack 功能以及/或者缺少相关 package 包*
我使用的node的版本
v14.17.1
起步案例
目录结构:
index.js文件
// 在index.js文件在index.html里面进行引入之前,还存在依赖lodash
// 提前将依赖打包进来
import _ from 'lodash';
function component() {
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
index.html:(在index.html中将打包后的文件引入)
<!doctype html>
<html>
<head>
<title>起步</title>
<!-- <script src="https://unpkg.com/lodash@4.16.6"></script> -->
</head>
<body>
<!-- script标签存在隐式的依赖关系,index.js文件执行之前,还依赖于script标签所引入的lodash -->
<!-- <script src="./src/index.js"></script> -->
<!-- 现在已经直接将lodash打包进去index.html -->
<!-- npx webpack命令,可以将index.js作为入口起点,输出main.js命令 -->
<!-- 直接打开index.html,查看到hello,webpack -->
<script src="test.js"></script>
</body>
</html>
webpack文件配置:
module.exports = {
entry: './src/index.js',
output: {
filename: 'test.js',
path: path.resolve(__dirname, 'dist')
}
}
运行webpack.config.js文件的方式:
1: 使用cli:
npx webpack --config webpack.config.js
2: 配置package.json
"scripts": {
"webpackTest": "webpack"
},
启动:
npm run webpackTest
webpack管理资源(css文件,图片,字体文件等)
1:下载对应的loader
css文件对应的loader
npm install --save-dev style-loader css-loader
图片,字体对应的loader
npm install --save-dev file-loader
数据(xml等格式文件对应的loader):
npm install --save-dev xml-loader
管理输出(多打包入口,使用插件)
多增加一个print.js文件
export default function printMe() {
console.log('I get called from print.js!');
}
修改原来的index.js文件:此时index.js文件中引用print.js文件
import _ from 'lodash';
+ import printMe from './print.js';
function component() {
var element = document.createElement('div');
+ var btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ btn.innerHTML = 'Click me and check the console!';
+ btn.onclick = printMe;
+
+ element.appendChild(btn);
return element;
}
document.body.appendChild(component());
修改webpack.config.js(注意clear-webpack-plugin的用法和webpack官网的用法有差别)
// webpack的配置文件
const path = require('path');
// 当入口文件增多时,在index.html中的script标签手动引入生成的打包文件还是很麻烦
// 使用html-webpack-plugin插件可以解决
const HtmlWebpackPlugin = require('html-webpack-plugin');
const htmlPlugins = new HtmlWebpackPlugin();
// 每次打包都会在dist文件夹生成内容,当生成的内容过多,可以使用插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// entry: './src/index.js',
entry: {
app: './src/index.js',
printer: './src/print.js'
},
output: {
// filename: 'test.js',
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
htmlPlugins,
new CleanWebpackPlugin()
]
}
在开发中使用
1:source map 的使用:明确错误来源,不在生产环境中使用
在webpack.config.js中配置以下一句话即可:
devtool: 'inline-source-map',
2:代码发生变化后自动编译代码的三种方式:
方式1:在package.json文件中配置script脚本:
(webpack --watch)可以实现代码发生改变之后自动编译,但是浏览器要看到变化还是得手动刷新
"script":{
"watch": "webpack --watch“
}
方式2: webpack-dev-server(内容更新之后浏览器会自动刷新)访问在localhost:8080
npm install --save-dev webpack-dev-server
在webpack.config.js中加入以下配置(webpack官网未进行更新,contentBase写法已经被放弃,mode选项也是必须加上的):
devServer: {
static: { //服务器所加载文件的目录
directory: path.join(__dirname, 'dist'),
},
},
mode: 'development' // 设置mode
更改package.json,配置npm script
"webpackDevServer": "webpack-dev-server --open --mode development"
方式3: webpack-dev-middleware 访问在localhost:3000(省略)
模块热替换(Hot module raplacement HMR)
定义:允许在运行时更新各种模块,而无需完全进行刷新,只适用于开发阶段(官网的new webpack.NamedModulesPlugin()已经被默认配置,直接删去即可)
更改webpack.config.js,引入webpack包:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const webpack = require('webpack');
module.exports = {
entry: {
+ app: './src/index.js'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
+ hot: true
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Hot Module Replacement'
}),
+ new webpack.HotModuleReplacementPlugin()
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
print.js是嵌入到index.js中进行使用的,所以当print.js模块发生更改时应该让webpack接受更新新的模块
index.js加入以下代码:
// 在index.js文件在index.html里面进行引入之前,还存在依赖lodash
// 提前将依赖打包进来
import _ from 'lodash';
import './index.css';
import printMe from './print.js';
import printME from './print.js';
function component() {
var element = document.createElement('div');
var btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
// element.innerHTML必须得放在btn.innerHTMl前面,否则不显示
btn.innerHTML = '点击我获取';
btn.onclick = printME;
element.appendChild(btn);
return element;
}
// document.body.appendChild(component());
let element = component();
document.body.appendChild(element);
// 修改 index.js 文件,以便当 print.js 内部发生变更时可以告诉 webpack 接受更新的模块。
if(module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updates printMe module!');
// printMe();
document.body.removeChild(element);
element = component();
document.body.appendChild(element);
})
}
完成上面的操作,点击的时候
此时当print.js发生改变时,页面自动更新:下面是浏览器控制台的输出:
HMR还可以用来修改样式表:
借助于 style-loader 的帮助,CSS 的模块热替换实际上是相当简单的。当更新 CSS 依赖模块时,此 loader 在后台使用 module.hot.accept 来修补(patch) style 标签。
所以,不需要任何配置,在更改css文件时,页面就能自动进行更新
tree shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)
新的 webpack 4 正式版本,扩展了这个检测能力,通过 package.json 的 “sideEffects” 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯的 ES2015 模块)”,由此可以安全地删除文件中未使用的部分。
pageage.json的配置:
{
"name": "your-project",
// ,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false
// "sideEffects": false
// 如果有代码存在副作用,将存在副作用的代码文件陈列
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
在webpack.config.js中配置生产模式即可删除多余的代码:
+ mode: "production"
生产环境构建
开发环境和生产环境的构建目标差异很大:
开发环境:要求具有实时重载或者热模块替换能力的source map 或者localhost server
生产环境:要求体积更小的bundle
所以:提取公共的webpack配置,并且为生产环境和开发环境提供不同的webpack配置
将webpack.config.js文件拆分成三个文件:
webpack.common.js (公共的配置)
// webpack的配置文件
const path = require('path');
// 当入口文件增多时,在index.html中的script标签手动引入生成的打包文件还是很麻烦
// 使用html-webpack-plugin插件可以解决
const HtmlWebpackPlugin = require('html-webpack-plugin');
const htmlPlugins = new HtmlWebpackPlugin();
// 每次打包都会在dist文件夹生成内容,当生成的内容过多,可以使用插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// entry: './src/index.js',
entry: {
app: './src/index.js',
// printer: './src/print.js'
},
output: {
// filename: 'test.js',
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
htmlPlugins,
new CleanWebpackPlugin(),
],
}
webpack.dev.js
const { merge } = require('webpack-merge');
const webpack = require('webpack');
const common = require('./webpack.common.js');
const path = require('path');
module.exports = merge(common, {
devServer: {
static: { //服务器所加载文件的目录
directory: path.join(__dirname, 'dist'),
},
// 配置模块热替换
hot: true
},
devtool: 'inline-source-map',
plugins: [
new webpack.HotModuleReplacementPlugin()
],
})
webpack.prod.js
+ const webpack = require('webpack');
const {merge} = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'source-map',
plugins: [
new UglifyJSPlugin({
sourceMap: true
- })
+ }),
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': JSON.stringify('production')
+ })
]
});
这三个文件需要使用webpack-merge包才能进行合并
注意: react库中任何位于 /src 的本地代码都可以关联到 process.env.NODE_ENV 环境变量,所以以下检查也是有效的:`
+ if (process.env.NODE_ENV !== 'production') {
+ console.log('Looks like we are in development mode!');
+ }`
代码分离
三种代码分离的方式:
1: 入口起点:(多打包入口)
如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
2:防止重复
(官网的CommonsChunkPlugin插件已经被废弃)
在webpack.config.js中使用以下的配置
// 提取公共的模块
optimization: {
splitChunks: {
chunks: 'all',
name: 'aa'
}
},
3:动态导入(ECMAScript提案中的import()语法)
去除掉上面两种方法对代码的改动,只需将index.js文件更改成下方:
// 动态导入分离chunk
function getComponent() {
// import()返回一个promise
// 注意,import()是动态导入,返回promise,与import语句导入是不相同的
return import (/*webpackChunkName: "lodash" */ 'lodash').then( _ => {
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element
}).catch( error => {
return error
})
}
getComponment().then( element => {
document.body.appendChild(element)
})
运行结果:dist目录下面生成了lodash.bundle.js
懒加载
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
将index.js改造成点击button之后再加载print模块
import _ from 'lodash';
import './index.css';
// import printME from './print.js';
// math.js分别导出两个模块,但是index.js只是导入其中的一个模块
import { cube } from './math.js';
function component() {
var element = document.createElement('div');
var btn = document.createElement('button');
var element = document.createElement('pre');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
element.innerHTML = [
'Hello webpack!',
'5 cubed is equal to ' + cube(5)
].join('\n\n');
// element.innerHTML必须得放在btn.innerHTMl前面,否则不显示
btn.innerHTML = '点击我获取';
// btn.onclick = printME;
// 让button在被点击的时候再进行动态加载
btn.onclick = e => {
// /* webpackChunkName: "print" */是给动态加载的模块进行命名
// ./print是print路径
import(/* webpackChunkName: "print" */ './print').then(module => {
// 注意当调用 ES6 模块的 import() 方法(引入模块)时,必须指向模块的 .default 值,因为它才是 promise 被处理后返回的实际的 module 对象
var print = module.default;
print()
})
}
element.appendChild(btn);
return element;
}
缓存
webpack 会生成一个可部署的 /dist 目录,然后把打包后的内容放置在此目录中。只要 /dist 目录中的内容部署到服务器上,客户端(通常是浏览器)就能够访问网站此服务器的网站及其资源。
获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,使访问速度更快。
然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。
> 所以:更新了的文件在webpack重新打包时输出的模块名称不能和之前相同
确保模块依赖发生变化之后的输出模块名称和之前的不能相同:
使用【chunkhash】
配置webpack.config.js的output里面的filename一项:
output: {
// filename: '[name].bundle.js',
// 为了保证更新文件之后webpack重新打包之后的模块名称和之前并不相同,可以在模块名称中使用chunkhash
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'dist'),
chunkFilename: '[name].bundle.js'
},
运行npm run webpack(webpack是我在package.json文件里面给脚本自定义的一个名称)
先后运行两次,发现模块名称中的hash值并没有发生变化(应该是webpack作出了改进,官网中是有变化的,需要进行CommonsChunkPlugin的配置)
更改index.js文件随便console.log一些数字,再次进行打包,打包的名称发生了变化
性能优化的操作:将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,这是因为,它们很少像本地的源代码那样频繁修改。所以保证他们打包出来的模块的名称不变即可。
配置webpack.config.js
entry: {
app: './src/index.js',
vender: [
// 将一些基本不修改的第三方库单独提取出来,这样子就不收到其他的文件变化的影响
'lodash'
]
},
更改了index.js文件的内容之后,只是app模块的hash值发生了变化,vender这个第三方包的打包模块并没有收到影响
修改前:
修改后:
创建library(省略)
Shimming(细粒度)
一些传统的模块依赖的 this 指向的是 window 对象。当模块运行在 CommonJS 环境下这将会变成一个问题,也就是说此时的 this 指向的是 module.exports
module: {
+ rules: [
+ {
+ test: require.resolve('index.js'),
+ use: 'imports-loader?this=>window'
+ }
+ ]
+ },
渐进式网络应用程序
渐进式网络应用程序(Progressive Web Application - PWA),是一种可以提供类似于原生应用程序(native app)体验的网络应用程序(web app)。PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的网络技术来实现的。
TypeScript
> npm install --save-dev typescript ts-loader
配置tsconfig.json来支持jsx,并且将typescript编译到ES5
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"sourceMap": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}
webpack.config.js的配置:
const path = require('path');
module.exports = {
entry: './src/index.ts',
// 溯源错误的具体位置
devtool: 'inline-source-map',
module: {
rules: [
// 配置解析tsx文件
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};