本文章只是用于记录学习webpack的一些基础知识
webpack版本是4以上版本
一、初识webpack
1、webpack的配置文件名称
默认配置文件名称:webpack.config.js
修改默认配置文件名称:webpack --config 指定名称
2、在项目中安装webpack,并且初始化
1)在项目的根目录下,运行npm init -y,生成package.json文件
2)在项目根目录下,安装webpack webpack-cli:npm i webpack webpack-cli -D (webpack4以上将他们分开了)
通过运行下面脚本,如果输出了webpack webpack-cli的版本信息,就说明本地安装成功了
./node_modules/.bin/webpack -v
3、最基本的配置信息
项目的根目录信息如下:
1)在根目录下新建文件webpack.config.js,内容如下:
'use strict';
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
mode: 'production'
}
2)在根目录下新建文件夹src,在src下面新建文件helloworld.js index.js
helloworld.js内容如下
export function helloworld() {
return 'hello world!'
}
index.js内容如下:
import { helloworld } from './helloworld';
document.write(helloworld())
3、执行打包命令
默认情况下的打包命令:在项目根目录下执行下面的脚本
./node_modules/.bin/webpack
会在根目录下生成dist/bundle.js
在dist下新建文件index.html,内容如下:
<!doctype html lang="en">
<meta charset="utf-8" />
<title>test webpack</title>
<body>
<script src="./bundle.js" type="text/javascript"></script>
</body>
</html>
修改默认的打包命令:
默认的打包命令:
./node_modules/.bin/webpack
在package.json文件添加属性script,添加下面脚本
script: {
"build": "webpack"
}
执行npm run build就和执行.node_modules/.bin/webpack是一样的效果的
二、webpack的基础用法
1、entry---指定webpack的打包入口
1)单入口---entry是一个字符串,对应源代码
module.exports={
entry: './src/index.js'
}
2)多入口---entry是一个对象
module.exports={
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
}
}
注意:不管单入口还是多入口,都只有一个output,单入口可以指定filename的具体名称,但是多入口需要用占位符[name].js(确保文件名称唯一)代替
2、output---告诉webpack如何将编译好后的文件输出到磁盘,对应转换后的代码
单入口的写法:
'use strict';
const path = require('path');
module.exports = {
entry: './src/index.js', // 入口字段的值为字符串
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js' // 可以指定输出的具体名称
},
mode: 'production'
}
多入口的写法:
'use strict';
const path = require('path');
module.exports = {
entry: { // 对象
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js' // 占位符,用entry里面定义的key值代替占位符
},
mode: 'production'
}
3、loader--webpack本身只支持js json两种文件类型,通过loader去支持其他文件类型,并转化成有效模块,添加到依赖图中
loader本身是一个函数,入参为源文件,返回转换的结果,loader处理webpack不能处理的文件类型
常见loader:
名称 | 描述 |
babel-loader | 转换ES6、ES7等JS新特性语法 |
css-loader | 支持css文件的加载和解析 |
less-loader | 将less文件转换成css |
ts-loader | 将TS转换成JS |
file-loader | 进行图片、字体等的打包 |
raw-loader | 将文件以字符串的形式导入 |
thread-loader | 多进程打包js和css |
loader的用法:在module属性里面的rules属性定义规则
'use strict';
const path = require('path');
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.txt$/, // test--指定匹配规则
use: 'raw-loader' // use--指定使用的loader名称
}
]
},
mode: 'production'
}
4、plugins---用于bundle文件的优化,资源管理,环境变量注入----作用于整个构建过程
loader不能完成的交由plugin处理
常见plugins
名称 | 描述 |
CommonsChunkPlugin | 将chunks相同的模块代码提取成公共js |
CleanWebpackPlugin | 清理构建目录 |
ExtractTextWebpackPlugin | 将css从bundle文件里提取成一个独立的css文件 |
CopyWebpackPlugin | 将文件或者文件夹拷贝到构建的输出目录 |
HtmlWebpackPlugin | 创建html文件去承载输出的bundle |
UglifyjsWebpackPlugin | 压缩js |
ZipWebpackPlugin | 将打包出的资源生成一个zip包 |
Plugins的用法:
'use strict';
const path = require('path');
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.txt$/,
use: 'raw-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({template: '.src/index.html'})
],
mode: 'production'
}
5、mode---用来指定当前的构建环境,production development none
设置mode,可以使用webpack的内置函数,默认值为production
mode的内置函数的功能
选项 | 描述 |
production | 设置process.env.NODE_ENV = production,开启FlagDependencyUsagePlugin,FlagIncludedChunksPlugin, ModuleConcatenationPlugin,NoEmitOnErrorsPlugin, OccurrenceOrderPlugin,SideEffectsFlagPlugin, TerserPlugin |
development | 设置process.env.NODE_ENV = development,开启NamedChunksPlugin NamedModulesPlugin |
none | 不开启任何优化选项 |
三、webpack的使用
1、解析ES6语法------babel-loader,babel的配置文件.babelrc,如前面所写
资源解析:增加ES6的babel preset配置----解析ES6
{
"presets":[
"@babel/preset-env"
],
"plugins": [
"@babel/proposal-class-properties"
]
}
栗子:资源解析,解析ES6语法
1)安装依赖:@babel/core @babel/preset-env (解析ES6语法) babel-loader(将ES6语法转换成ES5)
npm i @babel/core @babel/preset-env babel-loader -D
2)对babel的设置,在根目录下新建文件.babelrc,因为只需要解析ES6,所以只需要设置@babel/preset-env
{
"presets": [
"@babel/preset-env"
]
}
3)设置loader的配置,在module.exports.module.ruls增加一条规则
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
通过以上的配置,就可以用ES6语法进行开发了
栗子:资源解析:解析React JSX
1)在.babelrc添加React的babel preset配置
{
"presets": [
"@babel/preset-env", // ES6的babel preset 配置
"@babel/preset-react" // React的babel preset配置
]
}
2)在项目中安装React相关插件
npm i react react-dom @babel/preset-react -D
3)在src目录下添加search.js
'use stric';
import React from 'react'
import ReactDOM from 'react-dom'
class Search extends React.Component {
render() {
return <div>Search text</div>
}
}
ReactDOM.render(
<Search />,
document.getElementBtId('root')
)
4)在dist目录下新建main.html文件,引进打包出来的文件search.js
<!DOCTYPE html>
<body>
<div id="root"></div>
<script src="./search.js" type="text/script"></script>
</body>
</html
执行,npm run build就可以在浏览器打开文件main.html,看到效果
栗子:解析css,less,sass
资源解析:解析css
css-loader用于加载.css文件,并且转换成commonjs对象
style-loader将样式通过<style>标签插入到head中
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
首先,安装css-loader style-loader
npm i style-loader css-loader -D
然后在配置中添加style-loader css-loader,注意use里面的loader的执行顺序是后往前执行的,即先执行css-loader,在执行style-loader
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
通过以上的步骤,webpack就可以识别.css文件了
资源解析:解析less
在配置中添加less-loader,就可以识别.less文件内容了,less-loader将less转化成css
首先,安装less-loader,该loader依赖于less,所以两个都要安装
npm i less less-loader -D
然后,在配置中添加.less文件的处理规则
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
}
]
}
通过以上步骤,webpack就可以识别.less文件了
栗子:资源解析:解析图片和字体
解析图片:file-loader用于处理文件
首先,在项目中安装file-loader
npm i file-loader -D
然后在module.rules添加如下配置
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
use: 'file-loader'
}
这样就可以在项目中添加图片了,打包出来的dist根目录下面会有类似下面的文件名,这就是打包出来的图片
84869cf192b7950f4f6dd3f4c1173b51.jpg
解析字体:file-loader也可以用于处理字体
首先,在项目中安装file-loader
npm i file-loader -D
然后,在配置中module.rules添加如下配置
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: 'file-loader'
}
接着,在入口的css文件或者其他css文件中引进字体库,如aa.otf
@font-face{
font-family: 'aa';
src: url(./aa.otf)
}
.search-text{
font-size: 20px;
color: red;
font-family: 'aa';
}
通过以上步骤,webpack就可以识别引进的字体了
注意:file-loader和url-loader都可以对图片和字体等文件进行转换处理,但是url-loader可以设置小资源,并且可以自动base64的转换,其内部也是调用了file-loader
下面看下url-loader是如何设置小资源的时候自动转化base64从而减少http请求 limit属性的作用
首先,安装url-loader
npm i url-loader -D
然后,设置配置
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240
}
}
]
}
上面配置的作用,就是将size<10240字节的图片进行base64的转码,但是js或者html的体积会增大
栗子:webpack中的文件监听
文件监听是在发现源码发生变化的时候,自动重新构建出新的输出文件
webpack开启监听模式,有两种方式:
1)启动webpack命令时,带上--watch参数
2)在配置webpack.config.js中设置watch:true
添加watch命令,这种方式的缺点就是每次需要手动刷新浏览器
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"watch": "webpack --watch"
}
栗子:webpack中的热更新与及原理分析
1)热更新方式一:webpack-dev-server---不刷新浏览器;不输出文件而是放在内存中;结合HotModuleReplacementPlugin插件使用
热更新只是针对开发环境,所以mode:development
首先,在package.json文件的script添加执行语句
"scripts": {
"dev": "webpack-dev-server --open"
}
其次,webpack-dev-server是在开发中使用,所以在webpack.config.js文件中将mode修改成development
mode: "development"
接着,配置plugins属性和devServer,由于HotModuleRepalcementPlugin属于webpack中的属性,需要引进webpack
const webpack = require('webpack')
mode: 'production',
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
contentBase: './dist', // 服务的基础目录
hot: true // 开启热更新
}
2)热更新方式二:使用webpack-dev-middleware
WDM将webpack输出的文件传输给服务器,适用于灵活的定制场景
热更新的原理分析:
Webpack Compile:将js编译成Bundle
HMR Server:将热更新的文件输出给HMR Runtime
Bundle Server:提供文件在历览器的访问
HMR Runtime:会被注入到浏览器,更新文件的变化
bundle.js:构建输出的文件
栗子:文件指纹策略:chunkhash contenthash hash
文件指纹一般用于生产配置:mode:"production"
文件指纹:打包后输出的文件名的后缀,如下面所示_d45jf...
index_d45jfjde556nf.js
文件指纹的作用:
1)版本管理,只需要将修改过的文件发布,没修改过的文件的指纹不需要改变
2)没修改过的文件,还可以使用本地缓存,加速页面的访问速度
文件指纹的生成:
Hash:和整个项目的构建有关,只要项目文件有修改,整个项目构建的hash值就会修改
Chunkhash:和webpack打包的chunk有关,不同的entry会生成不同的Chunkhash值
Contenthash:根据文件内容来定义hash,文件内容不变的时候,则Contenthash不变
下面看下上述3个指纹的使用场景
1、打包出来的js文件使用chunkhash:设置output的filename,使用[chunkhash]
每一个入口对应的打包出来的chunk,就可以使用chunkhash
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name][chunkhash:8].js'
}
2、打包出来的css使用contenthash:设置MiniCssExtractPlugin的filename,使用contenthash]
一般在项目中使用css-loader,style-loader会将css插入html中的heade中,因此需要plugin:MiniCssExtractPlugin将css提取出来
首先,安装插件:
npm i mini-css-extract-plugin -D
然后,添加配置:
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
plugins: [
new MiniCssExtractPlugin({
filename:`[name][contenthash:8].css`
})
]
注意:由于style-loader是通过style标签将css插入heade中,这和mini-css-extract-loader的功能相反--将css提取到单独的文件里面 ,所以style-loader和mini-css-extract-loader的loader不能共存
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
}
]
},
3、设置file-loader、url-loader的name,使用[hash]
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name][hash:8].[ext]'
}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'font/[name][hash:8].[ext]'
}
}
]
}
占位符名称 | 含义 |
[path] | 文件的相对路径 |
[folder] | 文件所在的文件夹 |
[name] | 文件名 |
[contenthash] | 文件内容的hash,默认是md5生成 |
[hash] | 文件内容的hash,默认是md5生成 |
[ext] | 资源后缀名 |
[emoji] | 一个随机的指代文件内容的emoji |
在package.json文件里面可以写两个执行命令,以区分生产和开发环境使用那个webpack配置
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js --open"
}
栗子:代码压缩,包括html css js压缩
js文件的压缩:在webpack4以上是内置了uglifyjs-webpack-plugin,默认打包出来的文件是压缩过了的
css文件的压缩:使用optimize-css-assets-webpack-plugin,同时使用cssnano
html文件压缩:修改html-webpack-plugin,设置压缩参数
css文件压缩
首先,安装插件optimize-css-assets-webpack-plugin,cssnano
npm i cssnano -D
npm i optimize-css-assets-webpack-plugin -D
然后,在webpack.prod.js文件中配置
const OptimizeCssAssetsWebpackPlugin= require('optimize-css-assets-webpack-plugin')
plugins: [
new MiniCssExtractPlugin({
filename: '[name_[contenthash:8].css'
}),
new OptimizeCssAssetsWebpackPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
]
通过以上步骤就可以将css中的空白符号和注释等去掉
html文件压缩
首先,安装html-webpack-plugin插件
npm i html-webpack-plugin -D
然后,在配置中添加如下配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new MiniCssExtractPlugin({
filename: '[name_[contenthash:8].css'
}),
new OptimizeCssAssetsWebpackPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/search.html'), // html模板所在的一个位置
filename: 'search.html', // 指定html打包出来的以恶搞文件名称
chunks: ['search'], // 生成的html要使用那些chunks
inject: true, // 为true,则打包出来的chunk会自动注入到html文件中
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: true,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html'), // html模板所在的一个位置
filename: 'index.html', // 指定html打包出来的以恶搞文件名称
chunks: ['index'], // 生成的html要使用那些chunks
inject: true, // 为true,则打包出来的chunk会自动注入到html文件中
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: true,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
})
]
这里由于是多页面应用,所以html-webpack-plugin使用了两次来配置相应的信息
三、webpack的进阶用法
栗子:自动清理构建目录产物
问题:每次构建的时候不会清理目录,造成构建的输出目录output文件越来越多
解决方法1:通过npm scripts 清理构建目录
rm -rf ./dist && webpack
解决方法2:使用clean-webpack-plugin,默认在构建前会自动删除output指定的输出目录
避免每次构建前都需要手动删除dist目录
首先:安装插件clean-webpack-plugin
npm i clean-webpack-plugin -D
然后,在webpack.dev.js webpack.prod.js文件里面引进插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [
new CleanWebpackPlugin()
]
通过以上步骤,在每次打包的时候,构建出来的目录里面的文件数量是不会增加的
栗子:对css的一些增强功能使用
postCss插件,autoprefixer自动补齐css3前缀
首先,安装插件
npm i postcss-loader autoprefixer -D
然后在配置中添加如下规则
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: () => {
require('autoprefixer')({
browsers: ['last 2 version', '>1%', 'ios 7']
})
}
}
}
}
]
}
]
},
栗子:移动端css px自动转换成rem---需要适应不同的分辨率
传统方式:css媒体查询实现响应式布局
缺陷:需要写多套适配样式代码
@media screen and (max-width: 980px) {
.header{
width: 900px;
}
}
@media screen and (max-width: 480px) {
.header{
width: 400px;
}
}
使用px2rem-loader,,,待续
栗子:静态资源内联
资源内联的意义:
1)代码层面:
页面框架的初始化脚本
上报相关打点
css内联避免页面闪动
2)请求层面:减少HTTP请求次数
小图片或者字体内联(url-loader)
html和js内联
raw-loader内联html
raw-loader内联js
raw-loader的基本原理:读取文件内容,返回string,然后插入到对应的位置
使用的是html-webpack-plugin,默认的模板引擎是ejs引擎,所以可以识别语法:${代码}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 引进meta信息 -->
<!-- 使用raw-loader,引进的文件的路径 -->
${ require('raw-loader!./meta.html') }
<title>kuai</title>
<!-- 引进脚本 -->
<!-- 使用raw-loader,在用babel-loader解析,引进脚本的路径 -->
${require('raw-loader!babel-loader!../node_modules/leven/index.js')}
</head>
<body></body>
</html>
c s s内联
1)借助style-loader
module: {
rules: [
{
test: /\.scss$/,
use: [
{
loader: 'style-loader',
options: {
insertAt: 'top', // 样式插入到<head>中
singleton: true // 将所有到style标签合并成一个
}
},
'css-loader',
'sass-loader'
]
}
}
}
2)html-inline-css-webpack-plugin
栗子:多页面应用打包通用方案
多页面应用概念:每一次页面跳转的时候,后台服务器都会返回一个新的html文档,这种类型的网站也就是多页网站,也叫多页应用
优点:1)页面解耦 2)seo更加友好
A、多页面打包基本思路:每个页面对应一个entry,一个html-webpack-plugin
缺点:每次新增或者删除页面的时候,都需要修改webpack配置
B、动态获取entry,和设置html-webpack-plugin的数量,利用glob.sync
规则:这种方法需要约定,每个页面的入口文件命名都为index.js,html模版文件名为index.html
下面的语句的路径,是获取根目录下的src目录下的一级目录下的index.js,即:src/search/index.js
entry: glob.sync(path.join()__dirname, './src/*/index.js')
glob.sync同步读取文件,返沪数组,数组内容为匹配上的路径名
1)安装glob插件
npm i glob -D
2)修改文件路径如下图所示,每个文件夹下都有index.js,index.html
3)修改配置
先是利用glob.sync获取到入口文件的路径
然后对存放路径的数组遍历,获取每一个路径的的文件名,为每个入口添加插件
最后,将入口和插件配置到odule.exports里面去
const glob = require('glob')
const setMPA = () => {
const entry = {}
const htmlWebpackPlugins = []
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js')) // 读取文件路径
console.log(entryFiles)
entryFiles.map(item => {
const match = item.match(/src\/(.*)\/index\.js$/) // 匹配为数组,第一个是匹配上的路径,第二个是()里面的内容
const pageName = match && match[1]
entry[pageName] = item // 文件名
htmlWebpackPlugins.push( // 每个入口添加插件
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`), // html模板所在的一个位置
filename: `${pageName}.html`, // 指定html打包出来的文件名称
chunks: [pageName], // 生成的html要使用那些chunks
inject: true, // 为true,则打包出来的chunk会自动注入到html文件中
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: true,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
})
)
})
return {
entry,
htmlWebpackPlugins
}
}
const {entry, htmlWebpackPlugins} = setMPA()
module.exports = {
entry,
plugins: [
new CleanWebpackPlugin(),
// new HtmlWebpackPlugin({
// template: path.join(__dirname, 'src/search.html'), // html模板所在的一个位置
// filename: 'search.html', // 指定html打包出来的以恶搞文件名称
// chunks: ['search'], // 生成的html要使用那些chunks
// inject: true, // 为true,则打包出来的chunk会自动注入到html文件中
// minify: {
// html5: true,
// collapseWhitespace: true,
// preserveLineBreaks: true,
// minifyCSS: true,
// minifyJS: true,
// removeComments: true
// }
// }),
// new HtmlWebpackPlugin({
// template: path.join(__dirname, 'src/index.html'), // html模板所在的一个位置
// filename: 'index.html', // 指定html打包出来的以恶搞文件名称
// chunks: ['index'], // 生成的html要使用那些chunks
// inject: true, // 为true,则打包出来的chunk会自动注入到html文件中
// minify: {
// html5: true,
// collapseWhitespace: true,
// preserveLineBreaks: true,
// minifyCSS: true,
// minifyJS: true,
// removeComments: true
// }
// })
].concat(htmlWebpackPlugins)
}
栗子:使用SourceMap
定义:SourceMap就是一个存储源代码与编译代码对应位置映射的信息文件
作用:进行debugger和错误排查等调试用的,一般开发环境开启,线上环境关闭
对应的webpack配置项为devtool,值设置可以参考文档https://v4.webpack.js.org/configuration/devtool/#devtool
SourceMap的关键字:匹配规则:"^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$"
关键字 | 作用 |
eval | 编译后的代码,使用eval包裹 |
source map | 编译后的代码,产生.map文件 |
cheap | 编译后的代码,不包含列信息 |
inline | 编译后的代码,将.map作为DataURI嵌入,不单独生成.map文件 |
module | 编译后的代码,包含loader的source map |
module.exports = {
devtool: 'eval'
}
栗子:提取公共资源
基础库分离
思路:将react react-dom基础包通过cdn引入,不打入bundle中
方法1: 使用html-webpack-externals-plugin插件
首先,安装插件
html-webpack-externals-plugin
然后,修改配置文件
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
module.exports = {
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://11.url.cn/now/lib/16.2.0/react.min.js', // 本地文件,或者cdn文件
global: 'React'
},
{
module: 'react-dom',
entry: 'https://11.url.cn/now/lib/16.2.0/react-dom.min.js',
global: 'ReactDOM'
}
]
})
]
}
最后,在模版文件,引进cdn脚本,这样就可以了
<!DOCTYPE html>
<head>
<title>document</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react.min.js"></script>
<script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react-dom.min.js"></script>
</body>
</html>
方法2:使用webpack4内置的SplitChunksPlugin进行公共脚本分离,替代CommonsChunkPlugin插件,test:匹配出需要分离的包
满足设定的条件,才会打出公共包
chunks参数说明:
async:异步引入的库进行分离(默认)
inital:同步引入的库进行分离
all:所有引入的库进行分离(推荐)
minChunks:设置最小引用次数
minSize:分离包体积大小
将插件react react-dom单独打包出来
// 在htmlWebpackPlugin里面要添加打包出来的包名,才能使用,chunks: ['vendors', pageName]
const setMPA = () => {
const entry = {}
const htmlWebpackPlugins = []
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))
console.log(entryFiles)
entryFiles.map(item => {
const match = item.match(/src\/(.*)\/index\.js$/)
const pageName = match && match[1]
entry[pageName] = item
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`), // html模板所在的一个位置
filename: `${pageName}.html`, // 指定html打包出来的文件名称
chunks: ['vendors', pageName], // 生成的html要使用那些chunks,这里添加公共的包名
inject: true, // 为true,则打包出来的chunk会自动注入到html文件中
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: true,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
})
)
})
return {
entry,
htmlWebpackPlugins
}
}
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(react|react-dom)/
name: 'vendor',
chunks: 'all'
}
}
}
}
}
生成一个通用的公共代码抽离的配置
module.exports = {
optimization: {
splitChunks: {
minSize: 0, // 有引用就打出包
cacheGroups: {
commons: {
name: 'common', // 打包出来的文件名
chunks: 'all',
minChunks: 2 // 公共文件引用次数至少2次
}
}
}
}
}
栗子:tree shaking
概念:一个模块可能有多个方法,只要其中一个方法被使用,则整个文件就会被打包到bundle文件里面。tree shaking就是把用到的方法才打包到bundle里面,没用到的方法会在uglify阶段擦除掉
使用:webpack默认支持,在.babelrc里设置modules: false;production mode默认开启
要求:必须是ES6语法,CJS方式不支持
DEC:
1) 代码永远不会被执行,不可到达,如下面语句永远不会执行
if (false) {
return 1
}
2)代码执行的结果不会被用到,比如,写了一个方法,并且返回结果,但该结果没有使用
3)代码只会影响死变量(只写不读),比如定义了一个变量,条件变了会修改变量的值,但是并没有用到该变量
栗子:scope hoisting的使用
现象:构建后的代码存在存在大量的必包代码
问题:大量函数闭包包裹代码,导致体积增大(模块越多越明显);运行带代码时,创建的函数作用域变多,内存开销变大
模块转换分析:模块转化成模块初始化函数
1)被webpack转换后的模块会带上一层包裹
2)import会被转换成_webpack_require
webpack的模块机制
1)打包出来是一个IIFE(匿名闭包)
2)modules是一个数组,每一项是一个模块初始化函数
3)_webpack_require用来加载模块,返回module.exports
4)通过WEBPACK_REQUIRE_METHOD(0)启动程序
scope hoisting原理
原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突;当模块被引用了一次,就会被内联进来,不会创建新的的函数声明,否则还是会的
对比:通过scope hoisting可以减少函数声明代码和内存开销
scope hoisting使用
webpack mode为production默认开启,不需要任何配置
必须是ES6语法,CJS不支持
如果mode不是production,则需要配置:module.exports.plugins = [new webpack.optimize.ModuleConcatenationPlugin()]
栗子:代码分割和动态import
代码分割的意义:
对应大的web应用而言,将所有的代码都放在一个文件中显然不够有效,特别是当某部分代码是在某些时候才才会被用到。webpack又一个功能,就是将代码分割成chunks(语块),当代码运行到需要他们的时候在加载
适用场景:
1)抽离相同代码到一个共享块
2)脚本懒加载,使用初始下载的代码更小
懒加载JS脚本的方式:
1)CommonJS:require.ensure()
2)ES6:动态import(目前还没有原生支持,需要babel转换)
如何使用动态import
1)安装babel插件:npm i @babel/plugin-syntax-dynamic-import -D
2)ES6:动态import,在.babelrc文件里面添加:"plugins":["@babel/plugin-syntax-dynamic-import"]
3)在test.js文件添加代码
import React from 'react'
export default () => <div>danymic import</div>
4)在index.js,动态添加该组件:点击图片的时候,在加载代码,可以在控制台看到效果,一个是点击图片的时候,在network会有一个请求,或者在html会插入<script>
'use stric';
import React from 'react'
import ReactDOM from 'react-dom'
import './search.less'
import sea from './images/sea.jpg'
import '../../common'
class Search extends React.Component {
constructor () {
super(...arguments)
this.state = {
Text: null // 定义组件变量
}
}
loadComponent () {
import('./test.js').then(text => { // 动态引进组件
this.setState({
Text: text.default
})
})
}
render() {
return <div className="search-text">
{
Text ? <Text /> : null
}
Search text<img src={ sea } onClick={ this.loadComponent.bind(this) }/>
</div>
}
}
ReactDOM.render(
<Search />,
document.getElementById('root')
)
栗子:在webpack中使用ESLint
使用ESLint的必要性:js报错,可能导致页面白屏,避免在发布代码前发现问题
行业内优秀的eslint规范实践:
1)Airbnb:eslint-config-airbnb适合react项目的语法检查
2)eslint-config-airbnb-base适合vue项目
制定团队的ESlint规范
1)不重复造轮子,基于eslint:recommend配置并改进
2)能够帮助发现代码错误的规则,全部开启
3)帮助保持团队代码风格统一,而不是限制开发体验
ESlint如何执行落地
1)和CI/CD系统集成
2)和webpack集成
方案一:webpack与CI/CD集成
本地开发阶段增加precommit钩子
首先,安装husky
npm i husky -D
然后,在package.json添加npm script,通过lint-staged增量检查修改的文件
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rm -rf ./dist/ && webpack --config webpack.prod.js",
"watch": "webpack --watch",
"dev": "rm -rf ./dist/ && webpack --config webpack.dev.js",
"clear": "rm -rf ./dist/",
"precommit": "lint-staged"
},
"lint-staged": {
"linters": {
"*.{js,csss}": ["eslint --fix", "git add"]
}
}
方案二:webpack与eslint集成
使用eslint-loader,构建时检查js规范,eslint官方文档:http://eslint.cn/docs/rules/indent
以eslint-config-airbnb为栗子
首先,安装插件,参考文档:https://www.npmjs.com/package/eslint-config-airbnb
npm i eslint eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y -D
然后,在配置文件中添加如下eslint-loader规则
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader',
'eslint-loader'
]
}
}
接着,在项目根目录下创建文件.eslintrc.js,这里用的是js所以以.js文件结尾,同时需要用module.exports
module.exports = {
"parser": "babel-eslint", // 指定解析器
"extends": "airbnb", // 继承airbnb
"env": {
"browser": true, // 指定环境
"node": true
},
"rules": {
"indent": ["error", 4] // 修改规则
}
}
由于解析器使用babel-eslint,需要继承airbnb,所以需要安装他们
npm i eslint-loader eslint-config-airbnb -D
接着,构建的时候,不符合规定的语法都会报错
栗子:webpack打包组件和库
作用:webpack可以打包应用,也可以打包js库
目的:实现一个大整数加法库的打包
1)需要打包压缩版和非压缩版
2)支持AMD/CJS/ESM模块引入,script引进
// 支持ESM
import * as bigNumber from 'big-number'
bigNumber.add('888', '8')
// 支持CJS
const bigNumbers = require('big-number)
bigNumber.add('888', '8')
// 支持AMD
require(['big-number'], function (big-number) {
bigNumber.add('888', '8')
})
// 支持脚本的引进方式
<script type="text/javascript" src="https://unpkg.com/big-number"></script>
<script>
// 全局变量
bigNumber.add('8888', '8')
// window对象的一个属性
window.bigNumber.add('8888', '8')
</script>
如何将库暴露出去
module.exports = {
mode: 'production',
entry: {
"big-number": "./src/index.js",
"big-number.min": "./src/index.js"
},
output: {
filename: "[name].js",
library: "bigNumber.js", // 指定库的全局变量
libraryExport: "default",
libraryTarget: "umd" // 支持库引入的方式
}
}
首先,新建项目big-number,对项目进行webpack初始化
mkdir big-number
npm init -y
npm i webpack webpack-cli -D
然后,在项目根目录下创建src/index.js,内容如下
export default function add (a, b) {
// a,b为string类型
let i = a.length - 1
let j = b.length - 1
let carry = 0 // 进位符
let ret = '' // 返回的结果
// 从个位开始加起,一直向十位,百位
while (i >= 0 || j >= 0) {
let x = 0
let y = 0
let sum
if (i >= 0) {
x = a[i--] - '0' // 将字符串转为数字
}
if (j >= 0) {
y = b[j--] - '0' // 将字符串转为数字
}
sum = x + y + carry
if (sum >= 10) {
carry = 1
sum -= 10
} else {
carry = 0
}
ret = sum + ret // 相当于字符串拼接
}
if (carry) {
ret += carry
}
return ret
}
其次,配置文件,只对指定文件进行代码压缩。使用插件terser-webpack-plugin,只对指定文件进行代码压缩。mode="production"默认压缩文件,这里不用uglifyplugin是因为uglifyplugin插件遇到es6语法会报错,而TerserPlugin不会
npm i terser-webpack-plugin -D
接着,编写配置文件webpack.config.js
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none', // 将压缩都去掉
entry: {
'big-number': './src/index.js',
'big-number.min': './src/index.js'
},
output: {
filename: '[name].js',
library: 'bigNumber',
libraryTarget: 'umd',
libraryExport: 'default'
},
optimization: { // 设置该选项,设置.min.js文件才压缩
minimize: true,
minimizer: [
new TerserPlugin({
include: /\.min\.js$/
})
]
}
}
在接着,在package.json文件添加脚本
"build": "rm -rf ./dist/ && webpack"
在接着,设置入口文件,package.json的main字段为index.js--这个就是入口文件,根据环境引不同的包,在根目录下创建index.js文件
if (process.env.NODE_ENV === 'production') { // mode = 'production'则process.env.NODE_ENV = 'production'
module.exports = require('./dist/big-number.min.js')
} else {
module.exports = require('./dist/big-number.js')
}
通过npm login 登陆你的账号,就可以通过npm publish发布你的包了
栗子:webpack实现SSR打包
SSR代码实现思路
服务端:1)使用react-dom/server的renderToString方法将react组件渲染成字符串;---因为服务端没有window document等对象
2)服务端路由返回对应的模板
客户端:1)打包出针对服务端的组件
首先,在项目根目录下新建针对ssr的配置文件,webpack.ssr.js
然后,在package.json文件添加脚本
"build:ssr": "rm -rf ./dist/ && webpack --config webpack.ssr.js"
接着,创建服务端的目录,在根目录下创建server/index.js这个就是服务端的脚本,服务端采用express,所以先安装express
npm i express -D
再接着,编写server文件,再server/index.js添加如下文件
const express = require('express')
const { renderToString } = require('react-dom/server') // 将客户端的组件渲染成字符串
const SSR = require('../dist/search-ssr') // 将组件引进来
// 设置监听的端口
const server = (port) => {
const app = express() // 实例化express
app.use(express.static('dist')) // 用静态目录
// 写一个路由
app.get('/search', (req, res) => {
const html = renderMarku(renderToString(SSR))
// res.status(200).send(renderToString(SSR)) // 这样返回的是一个字符串,但时机需要返回html页面,所以需要模板包装
res.status(200).send(html)
})
// 监听端口
app.listen(port, () => {
console.log(`server is running on port: ${port}`)
})
}
server(process.env.PORT || 3000)
const renderMarku = (str) => {
return `
<!doctype html>
<head>
<title>document</title>
</head>
<body>
<div id=""root>${str}</div>
</body>
</html>
`
}
在接着编写客户端代码,在src/search/index-server.js添加如下代码
'use strict';
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const glob = require('glob')
// const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
const setMPA = () => {
const entry = {}
const htmlWebpackPlugins = []
const entryFiles = glob.sync(path.join(__dirname, './src/*/index-server.js')) // 修改入口文件
console.log(entryFiles)
entryFiles.map(item => {
const match = item.match(/src\/(.*)\/index-server\.js$/)
const pageName = match && match[1]
if (pageName) {
// 有的时候才添加
entry[pageName] = item
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`), // html模板所在的一个位置
filename: `${pageName}.html`, // 指定html打包出来的文件名称
chunks: ['vendors', pageName], // 生成的html要使用那些chunks
inject: true, // 为true,则打包出来的chunk会自动注入到html文件中
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: true,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
})
)
}
})
return {
entry,
htmlWebpackPlugins
}
}
const {entry, htmlWebpackPlugins} = setMPA()
module.exports = {
entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]-server.js', // 删除的hash值去掉
libraryTarget: 'umd',
publicPath: './'
},
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader'
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: () => {
require('autoprefixer')({
browsers: ['last 2 version', '>1%', 'ios 7']
})
}
}
}
}
]
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'img_[name][hash:8].[ext]'
}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'font/[name][hash:8].[ext]'
}
}
]
}
]
},
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name_[contenthash:8].css'
}),
new OptimizeCssAssetsWebpackPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
].concat(htmlWebpackPlugins),
optimization: {
splitChunks: {
minSize: 0, // 有引用就打出包
cacheGroups: {
commons: {
// test: /(react|react-dom)/, // /(react|react-dom)/
name: 'common',
chunks: 'all',
minChunks: 2 // 公共文件引用次数至少2次
}
}
}
}
}
在接着,修改webpack.ssr.js文件,主要是output去掉hash,加上-ssr;设置libraryTarget:‘umd’;修改入口文件名
'use strict';
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const glob = require('glob')
// const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
const setMPA = () => {
const entry = {}
const htmlWebpackPlugins = []
const entryFiles = glob.sync(path.join(__dirname, './src/*/index-server.js')) // 修改入口文件
console.log(entryFiles)
entryFiles.map(item => {
const match = item.match(/src\/(.*)\/index-server\.js$/)
const pageName = match && match[1]
if (pageName) {
// 有的时候才添加
entry[pageName] = item
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`), // html模板所在的一个位置
filename: `${pageName}.html`, // 指定html打包出来的文件名称
chunks: ['vendors', pageName], // 生成的html要使用那些chunks
inject: true, // 为true,则打包出来的chunk会自动注入到html文件中
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: true,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
})
)
}
})
return {
entry,
htmlWebpackPlugins
}
}
const {entry, htmlWebpackPlugins} = setMPA()
module.exports = {
entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]-server.js', // 删除的hash值去掉
libraryTarget: 'umd'
},
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader'
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: () => {
require('autoprefixer')({
browsers: ['last 2 version', '>1%', 'ios 7']
})
}
}
}
}
]
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'img_[name][hash:8].[ext]'
}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'font/[name][hash:8].[ext]'
}
}
]
}
]
},
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name_[contenthash:8].css'
}),
new OptimizeCssAssetsWebpackPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
].concat(htmlWebpackPlugins),
optimization: {
splitChunks: {
minSize: 0, // 有引用就打出包
cacheGroups: {
commons: {
// test: /(react|react-dom)/, // /(react|react-dom)/
name: 'common',
chunks: 'all',
minChunks: 2 // 公共文件引用次数至少2次
}
}
}
}
}
执行npm run build:ssr就可以构建成功了
再接着,运行node端服务
node server/index.js
但是会抛出错误,node服务端没有这些对象
1)ReferenceError: self is not defined
2)ReferenceError: window is not defined
处理上述问题,需要在server/index.js增加hack
if (typeof windwo === 'undefined') {
global.windows = {}
}
if (typeof self === 'undefined') {
global.self = {}
}
在运行node端服务的时候,报下面错误
webpack打包时Error: Automatic publicPath is not supported in this browser
原因:因为打包资源后不能解析“./”之类的路径,需要通过publicPath配置
在webpack.ssr.js文件的output.publicPath = './' 不一定设置成该值,根据公式自行配置
在运行服务端脚本,就可以了
webpack ssr打包问题
1)浏览器的全局变量(Node.js没有window docum)
A、组件适配,将不兼容的组件根据打包环境进行适配
B、请求适配,将fetch ajax发送请求的写法改成isomorphic-fetch axios
2)样式问题(Node.js无法解析css)
方案一:服务端打包通过ignore-loader忽略掉css的解析
方案二:将style-loader替换成isomorphic-style-loader
解决样式不显示问题
使用打包出来的浏览器端·html为模版
设置占位符,动态插入组件
首先,修改server/index.js中的模版
if (typeof windwo === 'undefined') {
global.windows = {}
}
if (typeof self === 'undefined') {
global.self = {}
}
// 将打包出来的模版引进来
const express = require('express')
const { renderToString } = require('react-dom/server') // 将客户端的组件渲染成字符串
const SSR = require('../dist/search-server') // 将组件引进来
const path = require('path')
const fs = require('fs')
const template = fs.readFileSync(path.join(__dirname, '../dist/search.html'), 'utf-8')
// 设置监听的端口
const server = (port) => {
const app = express() // 实例化express
app.use(express.static('dist')) // 用静态目录
// 写一个路由
app.get('/search', (req, res) => {
const html = renderMarku(renderToString(SSR))
// res.status(200).send(renderToString(SSR)) // 这样返回的是一个字符串,但时机需要返回html页面,所以需要模板包装
res.status(200).send(html)
})
// 监听端口
app.listen(port, () => {
console.log(`server is running on port: ${port}`)
})
}
server(process.env.PORT || 3000)
// 占位符
const renderMarku = (str) => {
// 这里的字符串的值,是在html模版中的占位符一致
return template.replace('<!--HTML_PLACEHOLDER-->', str)
}
在search.html文件添加占位符
<!DOCTYPE html>
<head>
<title>document</title>
</head>
<body>
<div id="root"><!--HTML_PLACEHOLDER--></div>
<script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react.min.js"></script>
<script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react-dom.min.js"></script>
</body>
</html>
首屏数据如何处理
服务端获取数据
替换占位符
首先,在search目录下新增mock数据的文件data.json
{
"sodar_query_id": "PmvfX_X5Eomk8AXttY24Dg",
"injector_basename": "sodar2",
"bg_hash_basename": "PbZvCEkorD5rxjWOexle1_regFmuc5-vrUA2zacPm4s",
}
然后,在search.html添加占位符
<!DOCTYPE html>
<head>
<title>document</title>
</head>
<body>
<div id="root"><!--HTML_PLACEHOLDER--></div>
<script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react.min.js"></script>
<script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react-dom.min.js"></script>
<!--INITAL_DATA_PLACEHOLDER-->
</body>
</html>
在server.index.js引进数据,这里只是把主要的写出来,其他的同上
// 引进数据
const data = require('../src/search/data.json')
// 占位符
const renderMarku = (str) => {
const dataStr = JSON.stringify(data)
// 这里的字符串的值,是在html模版中的占位符一致
return template.replace('<!--HTML_PLACEHOLDER-->', str)
.replace('<!--INITAL_DATA_PLACEHOLDER-->', `<script>windwo.__inital_data=${dataStr}</script>`)
}
栗子:优化构建时命令行的显示日志
统计信息stats
preset | alternative | description |
“errors-only” | none | 只在错误时输出 |
“minimal” | none | 只在错误时或有新的编译时输出 |
”none“ | false | 没有输出 |
“normal” | true | 标准输出 |
”verbose“ | none | 全部输出 |
在配置中添加:
module.exports.stats = 'errors-only'
使用插件friendly-errors-webpack-plugin,构建success warning error日志提示
npm i friendly-errors-webpack-plugin -D
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
module.exports.plugins = [new FriendlyErrorsWebpackPlugin()]