搭建一个webpack多目录部署项目
一、首先在开始写webpack之前要完成各种环境变量的准备
ROOT_PATH = (exports.ROOT_PATH = path.resolve(__dirname,'./'));// 找到项目的根目录
STATIC_PATH = (exports.STATIC_PATH = path.join(ROOT_PATH, 'static'));// 找到静态资源的路径
PAGE_PATH = (exports.PAGE_PATH = path.join(STATIC_PATH, 'page'));// 找到部署页面的目录
二、获取部署目录下所有页面路径的方法
function envPaths() {
// PROJECT_PATH project_path DIR dir 是输入命令行的变量
// 例如PROJECT_PATH = 项目目录1,项目目录2 npm start 获取到项目目录名称
// reduce的一个参数是callback函数 第二个参数是[] 作为函数第一个参数的默认值
// callback函数的第一个参数是累计器累计回调的返回值; 它是上一次调用回调时返回的累积值
// callback函数的第二个参数是数组中正在处理的元素
// 第三个参数是数组中正在处理的当前元素的索引
// 第四个参数是调用reduce()的数组
var paths = ['PROJECT_PATH', 'project_path', 'DIR', 'dir'].reduce(function(
dirs,
key
) {
// process.env[key] key 是 输入的变量 比如 project_path
if (!process.env[key]) {
// 如果process.env[key]是undefined 就返回[] 表示没有部署的目录
return dirs;
}
return dirs.concat(
// process.env[key] 输入多目录时获取的目录名称是 react,vue,node
process.env[key].split(',').filter(function(name) {
// fs.existsSync判断文件是否真的存在
// 如果输入的目录真实存在就返回true
// 目录或者是入口js存在的情况
return (
fs.existsSync(path.join(PAGE_PATH, name)) ||
// 入口ts存在的情况 用ts代替/js$/
fs.existsSync(path.join(PAGE_PATH, name.replace(/js$/, 'ts'))) ||
// 入口tsx存在的情况
fs.existsSync(path.join(PAGE_PATH, name.replace(/js$/, 'tsx')))
);
})
);
},
[]);
if (!paths.length) {
paths.push('fake');
console.info('You may should set PROJECT_PATH or DIR environment variable');
}
// paths就是最后多目录部署的目录名称
return paths;
}
三、获取部署目录下的所有符合条件的部署文件
function pathEntries(dirs) {
// dirs 多目录部署的目录名称
return dirs.reduce(function(entries, dir) {
// 部署目录的绝对路径
var dirPath = path.join(PAGE_PATH, dir);
// 如果传入的是一个入口文件, 则直接放入数组中返回
// 匹配-entry并且jsx或者tsx结尾的文件
// fs.statSync(dirPath)返回给定文件的路径信息 size birthtime等
// fs.statSync(dirPath).isFile() 返回一个文件是否是文件
if (dirPath.match(/-entry\.[tj]sx?$/)) {
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isFile()) {
return entries.concat(dirPath);
}
var tsEntry = dirPath.replace(/js$/, 'ts');
if (fs.existsSync(tsEntry) && fs.statSync(tsEntry).isFile()) {
return entries.concat(tsEntry);
}
var tsxEntry = dirPath.replace(/js$/, 'tsx');
if (fs.existsSync(tsxEntry) && fs.statSync(tsxEntry).isFile()) {
return entries.concat(tsxEntry);
}
return entries;
}
// 如果不是入口文件, 则读取目录下的所有入口文件
// 不允许嵌套目录, 只取单层目录下的入口文件, 子目录下的不管
// fs.readdirSync 读取目录下的所有文件
return entries.concat(
fs
.readdirSync(dirPath)
.filter(function(item) {
return (
item.match(/-entry\.[tj]sx?$/) &&
fs.statSync(path.join(dirPath, item)).isFile()
);
})
.map(function(item) {
// 返回全路径
return path.join(dirPath, item);
})
);
}, []);
}
最终代码的实现
项目目录
var path = require('path');
var fs = require('fs');
var ROOT_PATH = (exports.ROOT_PATH = path.resolve(__dirname, '.'));
module.exports =pathEntries(envPaths()).map(function(entryFile){
var entryPath = path.dirname(entryFile);
var entryDir = path.relative(ROOT_PATH, entryPath);
return {
entry:function() {
var result = {};
result[entryDir] = entryFile;
return result;
},
output: {
path: path.resolve('./') + '/dist',
filename:'[name]_bundle.js',
libraryTarget: 'umd',
umdNamedDefine: true
},
mode: "development",
resolve: {
extensions: ['.js', '.jsx']
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 8787,
inline: false
},
module: {
rules: [{
test: /\.(css|scss)$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(js|jsx|es6)$/,
loader: 'babel-loader?cacheDirectory',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-proposal-class-properties']
}
}]
}
}
})
function pathEntries(dirs) {
return dirs.reduce(function(entries, dir) {
var dirPath = path.join(ROOT_PATH, dir);
return entries.concat(
fs
.readdirSync(dirPath)
.filter(function(item) {
return (
item.match(/index\.jsx?$/) &&
fs.statSync(path.join(dirPath, item)).isFile()
);
})
.map(function(item) {
// 返回全路径
return path.join(dirPath, item);
})
);
}, []);
}
function envPaths() {
var paths = ['PROJECT_PATH', 'project_path', 'DIR', 'dir'].reduce(function(
dirs,
key
) {
if (!process.env[key]) {
return dirs;
}
return dirs.concat(
process.env[key].split(',').filter(function(name) {
// 目录或者是入口js存在的情况
return (
fs.existsSync(path.join(ROOT_PATH, name))
);
})
);
},
[]);
if (!paths.length) {
paths.push('fake');
console.info('You may should set PROJECT_PATH or DIR environment variable');
}
return paths;
}
科普阶段
一、学习一下以上用到的path方法
1.path.resolve()
path.resolve:方法会把一个路径或路径片段的序列解析为一个绝对路径
例如:
const path1 = path.resolve('/a/b', '/c/d'); // 输出: /c/d
const path2 = path.resolve('/a/b', 'c/d'); // 输出:/a/b/c/d
const path3 = path.resolve('/a/b', '../c/d'); // 输出:/a/c/d
const path4 = path.resolve('a', 'b');// 输出:/Users/xiao/work/test/a/b
resolve把‘/’当成根目录 path.resolve()方法可以将多个路径解析为一个规范化的绝对路径。
其处理方式类似于对这些路径逐一进行cd操作,与cd操作不同的是,这引起路径可以是文件。
并且可不必实际存在(resolve()方法不会利用底层的文件系统判断路径是否存在,而只是进行路径字符串操作);
如下所示
path.resolve('www', 'static', '../public', 'src', '..');
// cd www /Users/xiao/work/test/www
// cd static /Users/xiao/work/test/www/static
// cd ../public /Users/xiao/work/test/www/public
// cd src /Users/xiao/work/test/www/public/src
// cd .. /Users/xiao/work/test/www/public
2.与path.resolve()操作分不开的是path.join()操作
二者区别如下
1.join是把各个path片段连接在一起, resolve把‘/’当成根目录
path.join('/a', '/b');
// /a/b
path.resolve('/a', '/b');
// /b
2.resolve在传入非/路径时,会自动加上当前目录形成一个绝对路径,而join仅仅用于路径拼接
// 当前路径为
/Users/xiao/work/test
path.join('a', 'b', '..', 'd');
// a/d
path.resolve('a', 'b', '..', 'd');
// /Users/xiao/work/test/a/d
3.__dirname __filename process.cwd() ./ 或者 ../ 的区别
__dirname 获得当前执行文件所在目录的完整目录名
__filename: 总是返回被执行的 js 的绝对路径
process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径
./: 跟 process.cwd() 一样,返回 node 命令时所在的文件夹的绝对路径
注意点
项目目录如下所示
react-page
-index/
-nodejs/
-1.findLargest.js
-2.path.js
-3.fs.js
-regs
-regx.js
-test.txt
在项目根目录react-page下运行node index/nodejs/3.fs.js
3.fs 内容如下
fs.readFile('./1.findLargest.js',(err)=>{
console.log(err) // no such file or directory
})
// 原因是当前运行脚本的目录是react-page目录 但是文件的目录是
/Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs
所以找不到文件
在刚才报错的 3.fs 文件中
require('./1.findLargest.js');
却可以输出{A:1} 因为require是编译时执行,read是运行时执行
4.path.relative
path.relative() 方法根据当前工作目录返回 from 到 to 的相对路径
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// 返回: '../../impl/bbb'
5.path.basename
path.basename('/目录1/目录2/文件.html');
// 返回: '文件.html'
path.basename('/目录1/目录2/文件.html', '.html');
// 返回: '文件'
6.path.extname
path.extname('index.html');
// 返回: '.html'
path.extname('index.coffee.md');
// 返回: '.md'
二、process.env
process.env 项目运行所在环境的一些信息
如何配置环境变量
process.env.NODE_ENV == 'dev' ? 'http://dev.com' :
process.env.NODE_ENV == 'product' ? 'http://product.com':
'localhost:3000'
比如运行项目时输入的一些参数也在process.env下
例如 PROJECT_PATH = 项目目录1,项目目录2 npm start
process.env.PROJECT_PATH 就可以获取到运行的 项目目录1 和 项目目录2
三、webpack devTool的属性值
1.eval 每个 module 会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL
webpackJsonp([1],[
function(module,exports,__webpack_require__){
eval(
...
//# sourceURL=webpack:///./src/js/index.js?'
)
},...])
2.source-map生成一个 SourceMap 文件
webpackJsonp([1],[
function(e,t,i){...},
function(e,t,i){...},
function(e,t,i){...},
function(e,t,i){...},
...
])
// # sourceMappingURL=index.js.map
// 与此同时,你会发现你的 output 目录下多了一个 index.js.map 文件。
// 看看这个index.js.map
{
"version":3,
"sources":[
"webpack:///js/index.js",
"webpack:///./src/js/index.js",
"webpack:///./~/.npminstall/css-loader/0.23.1/css-loader/lib/css-base.js",
...
],
"names":["webpackJsonp","module","exports"...],
"mappings":"AAAAA,cAAc,IAER,SAASC...",
"file":"js/index.js",
"sourcesContent":[...],
"sourceRoot":""
}
3.hidden-source-map和 source-map 一样,但不会在 bundle 末尾追加注释
与 source-map 相比少了末尾的注释,但 output 目录下的 index.js.map 没有少
4.inline-source-map生成一个 DataUrl 形式的 SourceMap 文件
webpackJsonp([1],[
function(e,t,i){...},
function(e,t,i){...},
function(e,t,i){...},
function(e,t,i){...},
...
])
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9...
5.eval-source-map每个 module 会通过 eval() 来执行,并且生成一个 DataUrl 形式的 SourceMap
6.cheap-source-map生成一个没有列信息(column-mappings)的 SourceMaps 文件,不包含 loader 的 sourcemap(譬如 babel 的 sourcemap)
和 source-map 生成结果差不多。output 目录下的 index.js 内容一样。
但是cheap-source-map生成的 index.js.map 的内容却比 source-map 生成的 index.js.map 要少很多代码,
我们对比一下上文 source-map 生成的index.js.map 的结果,发现 source 属性里面少了列信息,只剩一个"webpack:///js/index.js"。
// index.js.map
{
"version":3,
"file":"js/index.js",
"sources":["webpack:///js/index.js"],
"sourcesContent":[...],
"mappings":"AAAA",
"sourceRoot":""
}
7.cheap-module-source-map生成一个没有列信息(column-mappings)的 SourceMaps 文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。
开发环境推荐:
cheap-module-eval-source-map
生产环境推荐:
cheap-module-source-map (这也是下版本 webpack 使用-d命令启动 debug 模式时的默认选项)
原因如下:
**使用 cheap 模式可以大幅提高 souremap 生成的效率。**大部分情况我们调试并不关心列信息,而且就算 sourcemap 没有列,有些浏览器引擎(例如 v8) 也会给出列信息。
**使用 eval 方式可大幅提高持续构建效率。**参考官方文档提供的速度对比表格可以看到 eval 模式的编译速度很快。
使用 module 可支持 babel 这种预编译工具(在 webpack 里做为 loader 使用)。
**使用 eval-source-map 模式可以减少网络请求。**这种模式开启 DataUrl 本身包含完整 sourcemap 信息,并不需要像 sourceURL 那样,浏览器需要发送一个完整请求去获取 sourcemap 文件,这会略微提高点效率。而生产环境中则不宜用 eval,这样会让文件变得极大。
四、学习MiniCssExtractPlugin如何做到的分开打包
在 Webpack 4 之前,我们使用 extract-text-webpack-plugin 插件来提取项目中引入的样式文件,打包到一个单独的文件中
从 Webpack 4 开始,这个插件就过时了,需要使用 MiniCssExtractPlugin
此插件为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件,并支持 CSS 和 SourceMap 的按需加载。
注意:这里说的每个包含 CSS 的 JS 文件,并不是说组件对应的 JS 文件,而是打包之后的 JS 文件!接下来会详细说明。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, 'css-loader','postcss-loader' // postcss-loader 可选
],
},{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, 'css-loader','postcss-loader','less-loader' // postcss-loader 可选
],
}
],
},
};
情景一
// 入口文件 app.js
import Root from './components/Root'
// Root.js
import '../styles/main.less'
import Topics from './Topics'
// Topics.js
import "../styles/topics.less"
这种情况下,Topics 会和 Root 同属一个 chunk,所以会一起都打包到 app.js 中,
结果就是 main.less 和 topics.less 会被提取到一个文件中:app.css。而不是生成两个 css 文件。
情景二
但是,如果 Root.js 中并没有直接引入 Topics 组件,而是配置了代码分割 ,比如模块的动态引入,那么结果就不一样
因为这个时候有两个 chunk,对应了两个 JS 文件,所以会提取这两个 JS 文件中的 CSS 生成对应的文件。
这才是“为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件”的真正含义。
情景三
但是,如果分割了 chunk,还是只希望只生成一个 CSS 文件怎么办呢?也是可以做到的。
但需要借助 Webpack 的配置 optimization.splitChunks.cacheGroups。
optimization.splitChunks 是干什么的呢?在 Webpack 4 以前,我们使用 CommonsChunkPlugin 来提取重复引入的第三方依赖,比如把 React 和 Jquery 单独提取到一个文件中。而从 Webpack 4 开始,CommonsChunkPlugin 被 optimization.splitChunks 替代了。从命名也能看出来,它是用来拆分 chunk 的。怎么在这里需要用到这个配置呢?先来看看配置怎么写的:
optimization: {
splitChunks: {
cacheGroups: {
// Extracting all CSS/less in a single file
styles: {
name: 'styles',
test: /\.(c|le)ss$/,
chunks: 'all',
enforce: true,
},
}
}
}
打包结果:
Asset Size Chunks Chunk Names
app.js 281 KiB 2 [emitted] [big] app
styles.bundle.js 402 bytes 0 [emitted] styles
styles.css 332 bytes 0 [emitted] styles
topics.bundle.js 2.38 KiB 5 [emitted] topics
可以看出,样式确实都被提取到一个 styles.css 文件中了。但与此同时多了一个 style.bundle.js 文件,
这就是 optimization.splitChunks.cacheGroups 的效果。具体原理就不在此深究,感兴趣的话可以研究一下。
MiniCssExtractPlugin vs style-loader 区别
MiniCssExtractPlugin 提取 JS 中引入的 CSS 打包到单独文件中,然后通过标签 <link>添加到头部;
style-loader 则是通过 <style> 标签直接将 CSS 插入到 DOM 中。
通常,基本的 CSS 配置都是类似这样的。先 style-loader,然后 css-loader。
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader', 'css-loader'
],
},
],
}
但后来由于想要提取 CSS 到单独的文件里,就需要用上 MiniCssExtractPlugin。那么问题来了,如下的配置可行吗?
{
test: /\.css$/,
use: [
'style-loader', MiniCssExtractPlugin.loader, 'css-loader','postcss-loader'
],
}
生产模式
根据 MiniCssExtractPlugin 文档 中说到的,此插件适用于没有style-loader 的生产模式中,以及需要 HMR 的开发模式。
This plugin should be used only on production builds without style-loader in the loaders chain, especially if you want to have HMR in development.
也就是说,在生产模式中,以上的配置同时使用了style-loader 和 MiniCssExtractPlugin 是不合适的(试了一下,style-loader不会起作用)。
我们只能取其一。也可以如下两者结合,开发模式中使用 style-loader,生产模式中使用 MiniCssExtractPlugin。各取所需,毕竟这两者的作用还是很不同。
{
test: /\.css$/,
use: [
devMode?'style-loader':MiniCssExtractPlugin.loader,'css-loader','postcss-loader'
]
}
样式文件热更新(HMR)
在开发模式中, 我们可以用 MiniCssExtractPlugin 实现样式的 HMR(Hot Module Replacement,模块热更新)
样式文件的 HMR 是指什么呢?如果没有配置 HMR,开发模式下,修改 CSS 源文件的时候,页面并不会自动刷新加载修改后的样式。
需要手动刷新页面,才会加载变化。而 HMR 实现了被修改模块的热更新,使得变化即时显示在页面上,不再需要刷新整个页面。
style-loader 实现了 HMR 接口
因此开发环境下,这两个插件都是可以热更新 CSS 的,只是 MiniCssExtractPlugin 的配置可能更丰富一些。
比如说:style-loader 只热更新 JS 中引入的样式,如果 index.html 中通过 <link> 引入了服务器中的一个CSS 文件
如果开发模式下,修改 test.css 的源码,style-loader 不会热更新变化 CSS,而是需要刷新整个页面,
但 MiniCssExtractPlugin 则会自动重新加载所有的样式。
const devMode = process.env.NODE_ENV === 'development'; // 是否是开发模式
//......
module.exports = {
//......
module: {
rules:[
{
test: /\.less$/i,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
// 只在开发模式中启用热更新
hmr: devMode,
// 如果模块热更新不起作用,重新加载全部样式
reloadAll: true,
},
},
'css-loader','postcss-loader','less-loader'
]
},
// ......
]
}
}
五、webpack的output
output: {
filename: 打包后的文件名称
path: 打包后的目录
chunkFilename:指未被列在 entry 中,却又需要被打包出来的 chunk 文件的名称。一般来说,这个 chunk 文件指的就是要懒加载的代码。
publicPath: 在复杂的项目里可能会有一些构建出的资源需要异步加载,加载这些异步资源需要对应的 URL 地址。
output.publicPath 配置发布到线上资源的 URL 前缀,为string 类型。默认值是空字符串 '',即使用相对路径。
示例 : filename:'[name]_[chunkhash:8].js'
publicPath: 'https://cdn.example.com/assets/'
<script src='https://cdn.example.com/assets/a_12345678.js'></script>
crossOriginLoading:Webpack 输出的部分代码块可能需要异步加载,而异步加载是通过 JSONP 方式实现的。JSONP 的原理是动态地向 HTML 中插入一个 <script src="url"></script> 标签去加载异步资源。output.crossOriginLoading 则是用于配置这个异步插入的标签的 crossorigin 值。
script 标签的 crossorigin 属性可以取以下值:
anonymous(默认) 在加载此脚本资源时不会带上用户的 Cookies;
use-credentials 在加载此脚本资源时会带上用户的 Cookies。
通常用设置 crossorigin 来获取异步加载的脚本执行时的详细错误信息。
libraryTarget 和 library :当用 Webpack 去构建一个可以被其他模块导入使用的库时需要用到它们。
output.libraryTarget 配置以何种方式导出库。
output.library 配置导出库的名称。
output.libraryTarget 是字符串的枚举类型,支持以下配置
var(默认):// Webpack 输出的代码
var LibraryName = lib_code;
// 使用库的方法
LibraryName.doSomething();
commonJS:// Webpack 输出的代码
exports['LibraryName'] = lib_code;
// 使用库的方法
require('library-name-in-npm')['LibraryName'].doSomething();
commonjs2:// Webpack 输出的代码
module.exports = lib_code;
// 使用库的方法
require('library-name-in-npm').doSomething();
this: // Webpack 输出的代码
this['LibraryName'] = lib_code;
// 使用库的方法
this.LibraryName.doSomething();
window:// Webpack 输出的代码
window['LibraryName'] = lib_code;
// 使用库的方法
window.LibraryName.doSomething();
global:// Webpack 输出的代码
global['LibraryName'] = lib_code;
// 使用库的方法
global.LibraryName.doSomething();
}
libraryExport
output.libraryExport 配置要导出的模块中哪些子模块需要被导出。 它只有在 output.libraryTarget 被设置成 commonjs 或者 commonjs2 时使用才有意义。
假如要导出的模块源代码是:
export const a=1;
export default b=2;
现在你想让构建输出的代码只导出其中的 a,可以把 output.libraryExport 设置成 a,那么构建输出的代码和使用方法将变成如下:
// Webpack 输出的代码
module.exports = lib_code['a'];
// 使用库的方法
require('library-name-in-npm')===1;
一、chunkFilename
比如说我们业务代码中写了一份懒加载 lodash 的代码
async function getAsyncComponent() {
const { default: _ } = await import('lodash');
element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' ');
return element;
}
output.chunkFilename 默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id] 或 [id].)
如果我们显式配置 chunkFilename,就会按配置的名字生成文件
六、webpack的hash、chunkhash、contenthash
对于webpack的hash,常用于cdn缓存
[hash] is a "unique hash generated for every build" //每次build都重新生成
[chunkhash] is "based on each chunks' content" //根据每个块的内容生成
[contenthash] is "generated for extracted content" // 提取的内容生成
[hash:8] 代表取8位 Hash 值,默认是20位
hash
module.exports = {
// mode: 'development',
// mode: 'production',
entry: {
index: './src/index.js',
detail: './src/detail.js',
},
output: {
filename: '[name].[hash].js',
path: path.resolve(__dirname, 'dist')
},
}
上面代码运行生成的index文件和detail文件的hash是一样的
只改index.js文件它的hash串变了,detail.js的hash串也会变
chunkhash 每一个文件最后的hash根据它引入的chunk决定
// file1.js
console.log('file1')
// file2.js
console.log('file2')
// file3.js
console.log('file3')
// index.js
require('./file2')
console.log('index')
// detail.js
require('./file1')
console.log('detail')
变更
// index.js
require('./file2')
require('./file3')
console.log('index')
index的hash变了但是detail的hash也变了
原因是 module identifier,因为 index 新引入的模块改变了以后所有模块的 id 值,
所以 detail 文件中引入的模块 id 值发生了改变,
于是 detail 的 chunkhash 也随着发生改变。
webpack已经提供方案了,解决方案是将默认的数字 id 命名规则换成路径的方式。
webpack 4 中当 mode 为 development 会默认启动,
但是production环境还是默认的id方式,webpack也提供了相应的plugin来解决这个问题
plugins: [
new webpack.HashedModuleIdsPlugin(),
]
加上这个plugin后,变更后,detail的hash串仍然没有变化,符合预期。
在webpack中,有css的情况下,每个entry file会打包出来一个js文件和css文件,
在使用chunkhash的情况下,js和css的文件的hash会是一样的,这个时候暴露出来的一个问题:
你修一个react的bug,但是并没有改样式,最后更新后,js和css的文件的hash都变了。
这个还是不太好,css文件的hash串不变最好,再继续升级!
contenthash
contenthash是根据抽取到的内容来生成hash。
生产环境使用一个MiniCssExtractPlugin来进行css的压缩,
在plugin里面指定hash为contenthash,修改js文件后,
js文件的hash串变了,css的hash串没变!完美。
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].chunk.css'
})
optimization.moduleIds: "hashed"也能达到相同的效果
所以目前最佳实践是contenthash+HashedModuleIdsPlugin/optimization.moduleIds: "hashed"
七、externals用法详解
用externals来防止第三方依赖包被打包
UMD模块规范输出的模块所以
兼容commonjs模块规范(在node环境中使用)。
兼容AMD模块规范(用require.js引入使用)。
兼容CMD模块规范(用sea.js引入使用)。
八、mode
mode可设置development production两个参数
如果没有设置,webpack4 会将 mode 的默认值设置为 production
production模式下会进行tree shaking(去除无用代码)和uglifyjs(代码压缩混淆)
九、优化打包速度
影响前端发布速度的有两个方面,一个是构建,一个就是压缩,把这两个东西优化起来,可以减少很多发布的时间。
1.mode 的默认值设置为 production 进行tree shaking和uglifyjs(代码压缩混淆)
2.缩小文件的搜索范围当我们代码中出现 import 'vue'时, webpack会采用向上递归搜索的方式去node_modules 目录下找。
为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。
3.noParse 当我们代码中使用到import jq from 'jquery'时,webpack会去解析jq这个库是否有依赖其他的包。
但是我们对类似jquery这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加noParse属性,告诉webpack不必解析,以此增加打包速度。
4.(第一张图)extensions webpack会根据extensions定义的后缀查找文件(频率较高的文件类型优先写在前面)
5.(第二张图)使用HappyPack开启多进程Loader转换
6.(第三张图)使用webpack-parallel-uglify-plugin 增强代码压缩
更多webpack优化请看这篇文章,本文优化打包速度参考这篇文章
https://zhuanlan.zhihu.com/p/99959392
thread-loader 优化构建
示例
var threadLoader = require('thread-loader');
var cssWorkerPool = {
// 一个 worker 进程中并行执行工作的数量
// 默认为 20
workerParallelJobs: 2,
poolTimeout: 2000
};
threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);
var jsWorkerPool = {
// options
// 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
// 当 require('os').cpus() 是 undefined 时,则为 1
workers: 2,
// 闲置时定时删除 worker 进程
// 默认为 500ms
// 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
poolTimeout: 2000
};
threadLoader.warmup(jsWorkerPool, ['babel-loader']);
module:{
rules:[
{
test: /\.s?css$/,
oneOf: [
{
loader: [
isIncludes ? MiniCssExtractPlugin.loader : { loader: 'style-loader' },
{
loader: 'thread-loader',
options: cssWorkerPool
},
{ loader: 'css-loader', options: { minimize: true }},
{ loader: 'sass-loader' }
]
}
]
},
]
}
每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。
同时跨进程的数据交换也会被限制。请在高开销的loader中使用,否则效果不佳
十、代码分割的三种方式
详细参考文章 快狗打车前端
https://juejin.cn/post/6844904103848443912
入口起点:使用 entry 配置手动地分离代码。
动态导入:通过模块的内联函数调用来分离代码。
防止重复:使用 splitChunks 去重和分离 chunk。
第一种方式,很简单,只需要在 entry 里配置多个入口即可
entry: { app: "./index.js", app1: "./index1.js" }
第二种方式,就是在代码中自动将使用 import() 加载的模块分离成独立的包:import("./a");
第三种方式,splitChunks 默认配置
splitChunks: {
// 表示选择哪些 chunks 进行分割,可选值有:async,initial和all
chunks: "async",
// 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
minSize: 30000,
// 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
minChunks: 1,
// 表示按需加载文件时,并行请求的最大数目。默认为5。
maxAsyncRequests: 5,
// 表示加载入口文件时,并行请求的最大数目。默认为3。
maxInitialRequests: 3,
// 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js
automaticNameDelimiter: '~',
// 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。
name: true,
// cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
//
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
以上配置,概括如下4个条件:
模块在代码中被复用或者来自 node_modules 文件夹
模块的体积大于等于30kb(压缩之前)
当按需加载 chunks 时,并行请求的最大数量不能超过5
初始页面加载时,并行请求的最大数量不能超过将3
index.js 作为入口文件,属于入口起点手动配置分割代码的情况,因此会独立打包。(app.js)
a.js 通过 import() 进行加载,属于动态导入的情况,因此会独立打出一个包。(1.js)
vue 来自 node_modules 目录,并且大于30kb;将其从 a.js 拆出后,
与 a.js 并行加载,并行加载的请求数为2,未超过默认的5;
vue 拆分后,并行加载的入口文件并无增加,未超过默认的3。
vue 也符合 splitChunks 的拆分条件,单独打了一个包(2.js)
chunks 用以告诉 splitChunks 的作用对象,其可选值有 async、 initial 和 all。
默认值是 async,也就是默认只选取异步加载的chunk进行代码拆分。
当 chunks 值为 initial 时,splitChunks 的作用范围变成了非异步加载的初始 chunk,
例如我们的 index.js 就是初始化的时候就存在的chunk。而 vue 模块是在异步加载的chunk a.js 中引入的,所以并不会被分离出来。
十、babel 我学废了
babel 原理
https://www.zoo.team/article/babel-2
一、@babel-core 的作用是把js代码分析成ast,方便各个插件分析语法进行相应的处理。
有些新语法在低版本js 中是不存在的,如箭头函数,rest 参数,函数默认值等,
这种语言层面的不兼容只能通过将代码转为ast,分析其语法后再转为低版本js。
二、@babel/preset-env 是一个灵活的预设,你可以无需管理目标环境需要的语法转换或浏览器 polyfill ,就可以使用最新的JavaScript。
三、@babel/plugin-transform-runtime 该插件会开启对Babel 注入的 helper code ( helper 可译为辅助函数)的复用,以节省代码体积。
这些用到的辅助函数都从 @babel/runtime 中去加载,这样就可以做到代码复用了
四、@babel/preset-react react语法包,这个包,是专门作为react的优化,让你在代码中可以使用React ES6 classes的写法,同时直接支持JSX语法格式。
@babel/preset-react是@babel/plugin-syntax-jsx、@babel/plugin-transform-react-jsx和@babel/plugin-transform-react-display-name
五、@babel/plugin-proposal-decorators 可以使用装饰器
十、想要运行webpack-dev-server之后自动打开页面的两种方式
第一种
"scripts": {
"start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --open",
}
第二种
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 9999,
inline: false,
open:true
}