# 配置小记
# 多页面配置
实现多页面配置需要修改的地方:
入口配置
模板插件
HtmlWebpackPlugin
自定义服务启动
实现页面自动更新
入口配置
假设当前的页面目录
- src
- views
- page1
- index.js
- page1
- index.js
规定 /src/views
下的目录代表一个页面,需要在 Webpack entry
属配置多入口:
entry: {
page1: './src/views/page1/index.js',
page2: './src/views/page2/index.js'
}
下面是自动根据目录添加 entry
配置的辅助方法:
const glob = require("glob");
function multyEntry() {
const entry = {};
//读取src目录所有page入口
glob.sync('./src/views/*/index.js')
.forEach(function (filePath) {
console.log('filePath', filePath)
var name = filePath.match(/\/src\/views\/(.+)\/index.js/);
name = name[1];
entry[name] = [filePath, 'webpack-hot-middleware/client?reload=true'];
});
console.log('entry', entry)
return entry;
};
{
entry: multyEntry()
}
模板插件 HtmlWebpackPlugin
跟入口文件一样,一个页面需要添加一个 HtmlWebpackPlugin
, 那么怎么跟入口文件对应呢? 通过 chunt
。chunt
就是上面例子的 entry
的 key
: page1
和 page2
function multyHtmlWebpackPlugin(){
const htmlPlugin = [];
glob.sync('./src/views/*/index.js')
.forEach(function (filePath) {
var name = filePath.match(/\/src\/views\/(.+)\/index.js/);
name = name[1];
htmlPlugin.push(
new HtmlWebpackPlugin({
title: 'Output Management',
filename: './' + name + '/index.html',
template: './index.html',
inject: true,
chunks: [name]
})
)
});
return htmlPlugin;
}
自定义服务启动
Webpack 本质是通过 Webpack-dev-server
来启动服务的,配置多页面多后如果直接使用 Webpack-dev-server
,在访问页面的时候需要输入完整的页面地址才能访问,比如要访问上例中的 page1
, 我们需要输入 localhost:8080/page1.html
。通过自己搭个服务可以通过创建路由的方式方便我们访问页面
先了解下 Webpack-dev-server
中主要用到了两个核心模块:
webpack-dev-middleware
: 是一个处理静态资源的middleware
webpack-hot-middleware
: 结合webpack-dev-middleware
使用的middleware
,它可以实现浏览器的无刷新更新(hot reload)。这也是 Webpack 文档里常说的 HMR(Hot Module Replacement)
所以自定义搭建的服务如下:
// dev-server.js
var express = require('express')
var webpack = require('webpack')
var path = require('path')
var webpackHotMiddleware = require('webpack-hot-middleware')
var WebpackDevMiddleware = require('webpack-dev-middleware')
var multyWebpack = require('./webpack.multy')
var webpackConfig = multyWebpack(require('./webpack_config')({}))
var app = express();
// webpack编译器
var compiler = webpack(webpackConfig);
// webpack-dev-server中间件
var devMiddleware = WebpackDevMiddleware(compiler, {
publicPath: '/',
// publicPath: webpackConfig.output.publicPath,
stats: {
colors: true,
chunks: false
},
progress: true,
inline: true,
hot: true
});
app.use(devMiddleware)
app.use(webpackHotMiddleware(compiler))
// 路由
app.get('*', function(req, res, next) {
const pageKeys = Object.keys(webpackConfig.entry)
const pageList = pageKeys.map(item => {
return `<h2><a href="/${item}">${item}.html</a></h2>`
})
res.set('content-type', 'text/html');
res.send(pageList.join(''));
res.end();
});
module.exports = app.listen(8080, function(err) {
if (err) {
// do something
return;
}
console.log('Listening at http://localhost:' + '8000' + '\n')
})
当前输入的地址没有匹配到我们多页面的文件时,将会进入上面的路由配置,上面的将根据入口文件返回页面列页列表
实现页面自动更新
用自己的服务服务启动项目后,发现当修改代码时,控制台重新编译了,但是页面并没有自动更新。所以我需要额外实现模块热更新功能
给每个页面注入热更新插件 webpack-hot-middleware/client?reload=true
,所以优化上文的获取入口的函数
const glob = require("glob");
function multyEntry() {
const entry = {};
//读取src目录所有page入口
glob.sync('./src/views/*/index.js')
.forEach(function (filePath) {
console.log('filePath', filePath)
var name = filePath.match(/\/src\/views\/(.+)\/index.js/);
name = name[1];
entry[name] = [filePath, 'webpack-hot-middleware/client?reload=true'];
});
return entry;
};
{
entry: multyEntry()
}
完整的热更新还需要实现两点:
在 Webpack 配置中添加
plugin
插件new webpack.HotModuleReplacementPlugin()
在 Express 服务中添加中间件
webpack-hot-middleware
# Less
yarn add less less-loader
rules: [
{
test: /\.(css|less)$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
},
{
loader: "less-loader",
options: {
lessOptions: {
strictMath: true,
},
},
},
],
},
]
# scss
yarn add node-sass sass-loader
rules: [
{
test: /\.scss$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
},
{
loader: "sass-loader",
},
],
},
]
如果在 vue 项目,还需要添加以下配置:
{
test: /\.vue$/,
exclude: /node_modules/,
loader: 'vue-loader',
options: {
loaders: {
'scss': 'style-loader!css-loader!sass-loader'
}
}
},
# 支持React
需要添加的依赖:react
, react-dom
, @babel/preset-react
,
.babelrc配置
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs":2
}
],
"@babel/preset-react"
]
}
# 支持Vue
需要添加的依赖:vue
, vue-loader
, vue-template-compiler
webpack 添加Loader规则
rules: [
{
test: /\.vue$/,
exclude: /node_modules/,
use:[
"vue-loader"
]
},
]
webpack 添加Vue插件
Vue-loader
在 15.*
之后的版本 vue-loader
的使用都是需要添加插件 VueLoaderPlugin
const VueLoaderPlugin = require('vue-loader/lib/plugin')
plugins: [ new VueLoaderPlugin() ]
如果运行时提示这个报错:You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build(您正在使用仅在运行时构建的Vue,其中模板编译器不可用。要么将模板预编译为呈现函数,要么使用包含编译器的构建)
添加 webpack 配置显式改变运行时引用包
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
}
}
# CDN 配置
要注意的是CDN服务一般都会为资源开启很长时间的缓存,例如用户在获取 index.html
这个文件后,即使之后的发布操作将 index.html
文件重新覆盖了,但是用户在很长一段时间内还是会运行之前的版本,这会导致新的发布不能立即生效。
针对 HTML 文件:不开启缓存,将 HTML 放在自己的服务器上,而不是 CDN 服务上,同时关闭自己服务器的缓存。自己的服务器只提供HTML文件和数据接口。
针对静态的
JS
,CSS
,图片等文件:开启 CDN 和缓存,上传到 CDN 服务上,同时为每个文件名带上由文件内容算出的 Hash 值。
如果对形如 //cdn.com/id/app_a....css
这样的 URL 感到陌生,则我们需要知道这种 URL 省掉了前端的 http:
或者 https:
前缀。这样做的好处是,在访问这些资源时会自动根据当前的 HTML 的 URL 采用什么模式去决定是采用 HTTP 还是 HTTPS 模式。
# Webpack实现CDN的接入
静态资源的导入
URL
需要变成指向 CDN 服务的绝对路径的URL,而不是相对于 HTML 文件的 URL静态资源的文件名需要带上由文件内容算出来的
Hash
值,以防止被缓存将不类型的资源放在不同域名的 CDN 服务上,以防止资源的并行加载被阻塞。
Webapck的配置如下:
const path = require("path")
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const { WebPlugin } = require('web-webpack-plugin')
module.exports = {
output: {
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
publishpath: '//js.cdn.com/id/',
},
modules: {
rules: [
{
test: /\.css/,
use: ExtractTextPlugin.extract({
// 压缩css代码
use:['css-loader?minimize'],
// 指定存放css中导入的资源(例如图片)的CDN目录URL
publishPath: '//img.cdn.com/id/'
})
},
{
test: /\.png/,
use: ExtractTextPlugin.extract({
// 压缩css代码
use: ['file-loader?name=[name]_[hssh:8].[ext]'],
})
}
]
},
plugins: [
new WebPlugin({
template: './template.html',
filename: 'index.html',
stylePublishPath: '//css.cdn.com/id/',
}),
new ExtractTextPlugin({
// 为输出的css文件名加上Hash值
filename: '[name]_[contenthash:8].css'
})
]
}
在以上代码中最核心的部分是通过 publicPath
,参数设置存放静态资源的 CDN 目录 URL 。为了让不同类型的资源输出不同的 CDN,需要分别进行如下设置。
在
output.publicPath
中设置 JavaScript 的地址css-loader.publicPath
中设置被 CSS 导入的资源的地址WebPlugin-stylePublishPath
中设置 CSS 文件的地址
设置好 publishPath
后,WebPlugin
在生成 HTML 文件并将 css-loader
转换 CSS 代码时,会使用配置中的 publishPath
,用对应的线上替换原来的相对地址
# Webpack中提取公共代码的配置
Webapck 内置了专门用提取多个 Chunk
中的公共部分的插件 CommonsChunkPlugin
const CommonChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin')
new CommonChunkPlugin({
// 从哪些Chunk中提取仅供代码
chunks: ['a', 'b'],
// 提取公共部分成型一个新的Chunk
name: 'common'
})
通过以上配置就能从网页A 到网页B 中抽离出公共部分,放到 common
中
每个 CommonChunkPlugin
实例都会生成一个新的 Chunk
,在这个 Chunk
中包含了被提取的代码,配置中的参数说明:
name
:告诉插件新生成的 Chunk 的名称chunks
: 指明从哪些已有的 Chunk 中提取,如果不填该属性,则默认会从所有已知的 Chunk 中提取
TIP
webpack4后删除了 CommonsChunkPlugin,改用 SplitChunksPlugin
# SplitChunksPlugin
若不加以配置,webpack4 会自动使用 splitChunks 对资源块进行拆分,默认拆分规则如下:
块被多处引用或者来自 node_modules 目录
块大小大于 30kb(压缩前)
按需加载的块并行请求数不能大于 5 个
初始化加载的块并行请求数不能大于 3 个
也就是说 splitChunks 只会拆分被多次引用的块,或者该块来自于 node_modules 目录;并且该块在压缩前的大小应大于 30kb,否则不拆离。同时,最后两个条件对并行请求数做出了限制,这就意味着为了满足后两个条件,可能会产生体积较大的块
splitChunksPlugin 插件提供了配置参数,使得开发者能够对包进行自定义配置和拆分块。
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // all async initial 选择对哪些块进行优化
minSize: 30000, // 被拆分的最小大小(压缩前)
minChunks: 1, // 被共享的最小次数
maxAsyncRequests: 5, // 最大按需求并行请次数
maxInitialRequests: 3, // 最大初始化并行请求数
automaticNameDelimiter: '~', // 自动命名分隔符
name: true, // 自动为块命名
cacheGroups: {
vendor: { // key 为entry中定义的 入口名称
chunks: "initial", // 必须三选一: "initial" | "all" | "async"(默认就是异步)
test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk
name: "vendor", // 要缓存的 分隔出来的 chunk 名称
minSize: 0,
minChunks: 1,
enforce: true,
maxAsyncRequests: 1, // 最大异步请求数, 默认1
maxInitialRequests : 1, // 最大初始化请求书,默认1
reuseExistingChunk: true // 可设置是否重用该chunk(查看源码没有发现默认值)
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
# Prepack使用
Prepack
优化原理:
编译代码时提前将计算结果放在编译后的代码中,而不是在代码运行时才去求值
以如下代码为例:
import React, { Component } from 'react'
import { renderToString } from 'react-dom/server'
function hello(name) {
return 'hello' + name
}
class Button extends Component{
render() {
return hello(this.props.name)
}
}
console.log(renderToString(<Button name='webpack'>))
被Prepack转换后竟然直接输出:
console.log('hello webpack')
可以看出Prepack通过在编译阶段预先执行源码来得到执行结果 ,再直接运行结果输出以提升性能
# Prepack 运行原理
通过Babel将JavaScript源码解析成语法树(AST),以更细粒度分析源码
Prepack 实现了一个 JavaScript 解释器,用于执行源码。借助这个解释器,
Prepack
才能理解源码具体如何执行的,并将执行过程中的结果返回到输出中。
Prepack
还处于初期也仅仅是 ,所以还有一些局限性:
不能识别 DOM API 和部分 Node.js,如果在源码中有调用依赖运行环境的 API,就会导致
Prepack
报错代码在优化的性能可能更差
代码在优化后,文件的尺寸可会大大增加
# Webpack接入Prepack
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default
module.exports = {
plugins: [
new PrepackWebpackPlugin()
]
}
# 关于静态资源的输出路径
关于路径先分清楚在路径名前有没有带 /
的情况下他们的区别:
假设我们的HTML页面有引入一张 bg.png
的图片
/assets/bg.png
: 相对于启动服务(server-relative)assets/bg.png
: 相对于 HTML 页面./assets/
: 相对于 HTML 页面
官方 (opens new window)对于 output.publicPath
的解释:
- 参数:
string
|function
,在多数情况下此选项的值都会以/结束
对于按需加载(on-demand-load)或加载外部资源(external resources)(如JS文件、图片、文件等)来说,output.publicPath
是很重要的选项。如果指定了一个错误的值,则在加载这些资源时会收到 404
错误。
从这里可以知道一个信息,这个配置影响的是按需加载和静态资源的引入路径
来一个没有配置 output.publicPath
的例子和打包结果
// vue.deploy.config.js
entry: {
index: './src/index.js',
},
output: {
filename: '[name].js',
chunkFilename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
打包后引入的JS文件地址是:
<script type="text/javascript" src="index.js"></script>
HTML打开也是能正常加这个JS文件,接下我们加入 output.publicPath
output: {
filename: '[name].js',
chunkFilename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/out/',
},
打包后会发现这个JS加载失败了,因为他们引入路径是 src="/out/index.js"
,可以看到路径前多了 output.publicPath
配置的路径
所以 output.publicPath
一般用于生产环境中,假如生产环境我们需要将静态资源放在其它目录或者放入 CDN 时,就需要配置output.publicPath
,这样才能正确访问到真正的资源
打包静态资源的路径除了受 output.publicPath
影响,也会被 Loader 的配置影响,比如 file-loader
, file-loader
的配置中有两个配置属性 outputPath
和 publicPath
,
outputPath
: 指定输出文件放在哪个路径下,这个路径是会生成在输出目录中的publicPath
: 跟output.publicPath
类似,会在资源路径前加上publicPath
,一般也是用于生产
// vue.deploy.config.js
{
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/',
publicPath: 'assets/'
}
}
]
},
// 给body添加样式
body{
background: url("../imgs/bg.png");
}
打包后,图片的路径为 url(/assets/bg.png)
并且 file-loader
中的 publicPath
和 output.publicPath
同时存在时, file-loader
中的 publicPath
会覆盖 output.publicPath
,如果 file-loader
中的 publicPath
没设置,则使用 output.publicPath
# 文件指纹
Webpack中的静态资源文件指纹 (opens new window)
Webpack 提供了两种方式给输出的文件设置文件指纹
hash
: 就是每个构建过程生成的唯一hash
chunkhash
: 基于每个chunk
的内容而生成的hash
entry: {
index: './src/index.js',
print: './src/print.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[hash].js',
publicPath: '/'
},
第一次build的结果:
第二次build的结果:
即使没有修改文件,也会输出不同的 hash
文件
所以我们加入 chunkFilename
output.filename
不会影响那些「按需加载 chunk」的输出文件。对于这些文件,需要使用 output.chunkFilename
选项来控制输出
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name][hash].js',
chunkFilename: '[name][chunkhash].js',
publicPath: '/'
}
每个文件的 hash
指纹都不相同,上线后无改动的文件不会失去缓存
# chunkhash的问题
webpack 的理念是一切都是模块:把所有类型的文件都以 js 为汇聚点,不支持 js 文件以外的文件为编译入口。所以如果我们要编译 style
文件,唯一的办法是在 js 文件中引入 style
文件。如下
import './style.css';
webpack 默认将 js、image、css文件统统编译到一个 js 文件中,
这样的模式下有个很严重的问题,当我们希望将 css 单独编译输出并且打上 hash
指纹,按照前文所述的使用 chunkhash
配置输出文件名时,编译的结果是 js 和 css 文件的 hash
指纹完全相同
即使借助 extract-text-webpack-plugin
将 style
文件单独编译输出,webpack 也仍将 css 文件视为 js 的一部分,
所以不论是单独修改了 js 代码还是 css 代码,编译输出的 js/css
文件都会打上全新的相同的 hash
指纹
好在 extract-text-webpack-plugin
提供了另外一种 hash
值:contenthash
。顾名思义,contenthash
代表的是文本文件内容的hash
值,也就是只有 style
文件的 hash
值。这个 hash
值就是解决上述问题的银弹。修改配置如下:
new ExtractTextPlugin('[name].[contenthash].css');
此时不修改样式只修改 js 部分的话,样式文件输出的 contenthash
是一样的,但是只修改了 style
文件,未修改 index.js
文件,编译输出的 js 文件的 hash
指纹还是会改变的
这是因为webpack计算chunkhash
时,以index.js文件为编译入口,整个chunk的内容会将style.css的内容也计算在内
如果要解决这个问题可以使用插件 webpack-md5-hash
来解决
// vue.deploy.config.js
var WebpackMd5Hash = require('webpack-md5-hash');
module.exports = {
// ...
output: {
//...
chunkFilename: "[chunkhash].[id].chunk.js"
},
plugins: [
new WebpackMd5Hash()
]
};
# 输出不换行
为了方便看打包输出的代码,有时候希望输出文件能换行,此时就需要通过覆盖默认 TerserPlugin
的配置来实现
optimization: {
usedExports: true,
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
output: {
beautify: true,
},
}
})
],
},
# 删除console.log
optimization: {
minimize: true,
minimizer: [new TerserPlugin({
terserOptions: {
compress: {
pure_funcs: ["console.log"]
}
}
})]
}
# 引入全局less变量
# lessOptions
// 另一种办法,不引入全局文件,单独配置全局变量
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
globalVars: {
'@primary-color': 'red', // ten可以是ten,也可以是@ten,效果一样,下同
//或者
'hack': `true; @import "your-less-file-path.less";`, // 引用定义变量的文件,这个文件变量将被应用到全局当中
},
}
},
]
},
# style-resources-loader
// 在这里引入要增加的全局less文件
{
loader: 'style-resources-loader',
options: {
patterns: path.resolve(__dirname,'../src/pages/count/global.less')
}
}`
# 常规的 label 配置
{
"presets": [
["@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime", {
"corejs": 2
}
]
]
}
使用
@babel/plugin-transform-runtime
引入新的全局方法使用
@babel/preset-env
转换新语法为何还要使用
useBuiltIns
呢?,主是为了某些对象原型上的方法,以[].includes
为例,transform-runtime
导出的 Array 是没有includes
方法的,所以还是需要使用useBuiltIns
进行加持
# React 实现样式 scoped
通过配置 cssLoaderOptions modules (opens new window) 实现样式 scoped
我的使用粟子
{
test: /\.css$/i,
loader: "css-loader",
options: {
modules: {
localIdentContext: path.resolve(__dirname, "src"), // 只作用在特定路径下面
mode: (resourcePath) => {
if (/.global./.test(resourcePath)) { // 只有匹配到文件名为 .global. 时才是全局的
return "global";
}
if(resourcePath.indexOf('node_modules')>-1){ // 这一步应该是多余的操作
return 'global'
}
return "local"; // 默认启用 scoped
},
localIdentName: "[local]_[hash:base64:5]"
},
},
}
如下例子
/* index.global.less*/
.addr-panel{
position: relative;
background: #fff;
border-radius: 12px;
}
直接 import 'index.global.less'
,这个文件中的样式是全局影响的
对于实现 scoped
的组件的使用方式:
import style from './layout.less'
function Header(){
return (
<div className={`${style.head} flex`}>
头部
</div>
);
}
export default Header;
注意需要用 style[className]
的方式去使用类名
# QA
在运行 DEMO 时,发现打包出来的文件,虽然项目中的 js 语法转换了,但是 webpack 模块化相关的代码还是出现的箭头函数等ES6 代码
因为 webpack5 默认使用ecma 6。解决方式,在 webpack 配置需要添加
target: ['web', 'es5']
← 构建优化方案