前言
先解释一下来由,大概率也是大家工作中经常会面临的问题:
- 所有的项目都会有一套自己的webpack配置,版本差异差的离谱,不同项目完全不兼容;
- 所有的项目都是从0到1的,搭建者能力不同、或者技术设计的针对点不同、考虑的全面程度不一样的等,很难给出一套比较实用又接近完美的配置,缺少沉淀;
我理想的状态是在cover住一定的兼容版本的前提下,尽可能去沉淀去优化一套打包编译工具,集思广益。 即使出现下一个版本,跨版本的问题,也可以基于当前比较完全的配置直接去针对性升级。这也是我理解的工程化的样子。
工程化组件,一定要考虑的是可复用性以及可扩展性。理解是,我是小霸王的一张游戏卡,插到指定版本的小霸王游戏机上,都可以运行~当然,前端技术迭代飞快,我们面对的是工程,要的是稳定而不是实验,所以,所有的工程化方案都是基于相对稳定版本依赖实现。
EasyBuilders作为一项前端打包编译通用解决方案提出,支持Webpacker、Rolluper、Viter,全部适配实际项目多环境打包的案例。
接下来结合实际项目配置,讲解一下常用配置。
- Webpacker
- Viter
Webpacker
主要功能:
- 热更新:完善的TypeScript + React + scss技术栈热更新支持。
- 环境区分:
process.env.D_ENV
判断,值为dev
,test
,pre
,production
。 - 构建性能优化。
- 预设拆包最佳实践。
- 完全可扩展,暴露方法可以传入对应自定义配置进行融合。
规划:
1.less -F
2.常用到的,能提升性能的打包配置/插件全部加上
3.完善为 TypeScript/javascript/ es6 + React/Vue + scss/less 技术栈热更新支持。
4.Vue
5.js/es6
接下来,详细讲下webpacker以及结合webpacker,看一下webpack的常用配置。
传参解析
export interface ConfigOptions {
static: string; // 镜像节点 举例 https://aaaa.com/ccc/ddd
devPort?: number; // 开发环境端口号
report?: boolean; //是否调用webpack打印日志报告
}
主要分为以下几部分:
getBaseConfig 获取通用基础配置
一些基础配置,无论是开发环境、测试环境、还是生产环境,都通用的配置。
import { Root } from './utils';
import merge from 'webpack-merge';
import * as webpack from 'webpack';
import { createHappyPlugin } from './happypack';
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
- ► mode
由指定环境config的mode进行配置,默认production,不使用任何优化。 选项 | 描述 ---|--- development | 会将 DefinePlugin 中
process.env.NODE_ENV
的值设置为 development。打包的文件默认不被压缩,开发时可以设置为development。 production | 会将 DefinePlugin 中process.env.NODE_ENV
的值设置为 production。 打包的文件默认被压缩 none | 不使用任何默认优化选项
mode: 'none', //由指定环境config的mode进行配置,默认production,不使用任何优化
- ► entry
打包入口
entry: {
app: Root('src/index'), // name ==> app
},
===========================知识补充
===========================
1.MPA多页面配置:由于此时是多页面应用,
- 故entry不再是数组形式按照原来数组的格式配置每个入口项,对应到entry对象中的某个key值。
- webpack.config.js中找到plugins:
复制三个new HtmlWepackPlugin实例,并修改其中的第二个参数的 template 项,并在其中增加filename项和chunks:['query'] 项
。配置完毕尝试 build 编译。若不指定chunks,则所有的chunks会载入html中
# MPA
# entry 配置样例
entry: {
app: './src/app.js',
search: './src/search.js'
},
对象中变量可直接应用于输出配置文件名等,eg.output/filename: '[name].js'
- ► output
打包输出配置
output: {
path: Root("dist"),
/**
* path是webpack所有文件的输出的路径,必须是绝对路径,
* 比如:output输出的js,url-loader解析的图片,HtmlWebpackPlugin生成的html文件,
* 都会存放在以path为基础的目录下,
* “path”仅仅告诉Webpack结果存储在哪里,
*/
publicPath: '/',
//“publicPath”项则被许多Webpack的插件用于在生产模式下更新内嵌到css、html文件里的url值,
/**
* 例如,在localhost(即本地开发模式)里的css文件中边你可能用“./test.png”这样的url来加载图片,
* 但是在生产模式下“test.png”文件可能会定位到CDN上并且你的Node.js服务器可能是运行在HeroKu上边的。
* 这就意味着在生产环境你必须手动更新所有文件里的url为CDN的路径。
*
* 在生产模式下,对你的页面里面引入的资源的路径做对应的补全
* 比如在 prod配置 publicPath: `https://static.ccc.com/a/`,
* 生产环境url loader会把css中的url直接更新为 https://static.ccc.com/a/xxxxx
*
* 在dev/test 配置 publicPath: '/',
* 在开发阶段,我们要用devServer启动一个开发服务器,这里也有一个publicPath需要配置。
* webpack-dev-server打包的文件是放在内存中的而不是本地上,这些打包后的资源对外的根目录就是publicPath。
* http://localhost:9000/dist/ + 资源名, 就可以访问到该资源
*
* 生产环境: 当打包的时候,webpack会在静态文件路径前面添加publicPath的值,当我们把资源放到CDN上的时候,把publicPath的值设为CDN的值就可以了
* 开发环境: 但是当我们使用webpack-dev-server 进行开发时,它却不是在静态文件的路径上加publicPath的值,
* 相反,它指的是webpack-dev-server 在进行打包时生成的静态文件所在的位置, 相当于/+url
* 样例,开发环境,将注入到html中的静态资源文件路径前面加上制定地址
* webpack配置,publicPath: '/cd/';
* 实际打包,index.html中引入静态资源:
* <script src="/cd/assets/js/runtime_ae78c761.js"></script>
* <script src="/cd/assets/js/chunks/ui-libs_0f0fdd38.js"></script>
* <script src="/cd/assets/js/chunks/vendors_218bc1f2.js"></script>
* <script src="/cd/assets/js/chunks/app_bb776138.js"></script></body>
*/
filename: "assets/js/[name]_[hash:8].js",
// app_HHHJKKLS.js
// 考虑缓存的问题,一定要有hash
// filename: '[name]-[hash].bundle.js'
// 是项目级别的,就是有一个文件发生改动,打包后的所有文件 hash值都会发升变化
// filename: '[name]-[chunkhash].bundle.js'
// 在打包过程当中,只要是同一路的打包,那么chunkhash 都是相同的
// filename: '[name]-[contenthash:8].bundle.js'
// 文件级别的 hash,根据输出文件的内容生成的 hash 值,:8 设置的是 hash值的长度
chunkFilename: 'assets/js/chunks/[name]_[contenthash:8].js',
/**
* chunkname我的理解是未被列在entry中,却又需要被打包出来的文件命名配置。
* 没指定也会打包,此处为显式指定该部分文件的名称
*
* 什么场景需要呢?
* 在按需加载(异步)模块的时候,这样的文件是没有被列在entry中的,
* 如使用CommonJS的方式异步加载模块
* require.ensure(["modules/tips.jsx"],
function(require) { var a = require("modules/tips.jsx"); // ... }, 'tips');
* output.chunkFilename 默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id] 或 [id].),
* 没有配置该项的时候也会有打包输出,只是命名为默认值.
eg.
*/
/*
* 执行默认配置
* {
* entry: {
* index: "../src/index.js"
* },
* output: {
* filename: "[name].min.js", // index.min.js
* }
* }
* 打包结果:
* output.filename 的输出文件名是 [name].min.js,
* [name] 根据 entry 的配置推断为 index,所以输出为 index.min.js;
* 由于 output.chunkFilename 没有显示指定,
* 就会把 [name] 替换为 chunk 文件的 id 号,这里文件的 id 号是 1,所以文件名就是 1.min.js
* 执行显式设置
* {
* entry: {
* index: "../src/index.js"
* },
* output: {
* filename: "[name].min.js", // index.min.js
* chunkFilename: 'bundle.js', // bundle.js
* }
* }
* 打包结果:
* output.filename 的输出文件名是 [name].min.js,
* [name] 根据 entry 的配置推断为 index,所以输出为 index.min.js;
* output.chunkFilename 的输出文件名是 bundle.js
*/
},
- **► resolve **
解析配置
resolve: {
modules: [Root('node_modules'), 'node_modules', Root('src')],
/**
* 配置 Webpack 去哪些目录下寻找第三方模块,默认是只会去 node_modules 目录下寻找
* 越靠前优先级越高。
* 相对路径将类似于 Node 查找 'node_modules' 的方式进行查找。
* 使用绝对路径,将只在给定目录中搜索。
*
* 为什么有src ???
*
* 有时你的项目里会有一些模块会大量被其它模块依赖和导入,
* 由于其它模块的位置分布不定,针对不同的文件都要去计算被导入模块文件的相对路径, 这个路径有时候会很长,
* 就像这样 import '../../../components/button'
* 这时你可以利用 modules 配置项优化,
* 假如那些被大量导入的模块都在 ./src/components 目录下,
* 把 modules 配置成modules:['./src/components','node_modules']后,
* 你可以简单通过 import 'button' 导入。
*/
alias: {
// components: './src/components/'
}
/**
* 当你通过 import Button from 'components/button 导入时,
* 实际上被 alias 等价替换成了 import Button from './src/components/button' 。
* 以上 alias 配置的含义是把导入语句里的 components 关键字替换成 ./src/components/ 。
*/
extensions: ['.tsx', '.ts', '.jsx', '.js'],
// 在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在
},
- ► module
Module 中可以配置对指定的文件类型进行指定的 Loader 解析规则
module: {
rules: [
// 引入各种处理loader
// 以及happyPack等
// loader处理规则为由后至前,是栈的形式执行
{
test: /\.tsx?$/,
//把对.tsx? 的文件处理交给id为happy-ts 的HappyPack 的实例执行,happyPack实例在plugins中配置完成
use: 'happypack/loader?id=happy-ts',
/**
* 需要结合 happyPlugin 来处理
*
* 能同一时间处理多个任务,发挥多核 CPU 电脑的威力,HappyPack 就能让 Webpack 做到这点,
* 它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程
*
* js 的情况
* 把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
* loader: 'happypack/loader?id=happyBabel',
*/
exclude: /node_modules/,
},
/**
* 使用 @babel/preset-typescript 取代 awesome-typescript-loader和ts-loader
*
* 1. awesome-typescript-loader方案是如何对TypeScript进行处理的
*
* 2.@babel/preset-typescript
* 要使用@babel/preset-typescript,务必确保你是Babel7+
*
* @babel/preset-typescript和@babel/preset-react类似,是将特殊的语法转换为JS
* 但是有点区别的是,@babel/preset-typescript是直接移除TypeScript,转为JS,这使得它的编译速度飞快。
* 并且只需要管理Babel一个编译器就行了,因为我将脚手架中的typescript库卸载后,依然可以完美运行。
* 而且重要的是你写的TypeScript不会再进行类型检测,使得你改动代码后中断运行的页面。
*/
/**
* ts ===> es ===> js
* 首先我们需要知道TypeScript是一个将TypeScript转换为指定版本JS代码的编译器,
* 而Babel同样是一个将新版本JS新语法转换为低版本JS代码的编译器。
* 所以我们之前的方案每次修改了一点代码,都会将TS代码传递给TypeScript转换为JS,
* 然后再将这份JS代码传递给Babel转换为低版本JS代码。
* 因此我们需要配置两个编译器,并且每次做了一点更改,都会经过两次编译。
*/
{
test: /\.jpe?g$|\.ico$|\.png$|\.svg$/,
use: {
loader: 'file-loader',
options: {
name: '[name].[hash:8].[ext]',
outputPath: 'assets/images/',
},
},
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac|gif)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1024,
name: 'assets/media/[name].[hash:7].[ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1024,
name: 'assets/fonts/[name].[hash:7].[ext]',
},
},
// 将.js文件中的es6语法转成es5语法
{ test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ },
// 配置 vue-loader 来处理 .vue 文件
{ test: /\.vue$/, use: 'vue-loader' },
// {
// test: /\.md$/,
// use: {
// loader: 'raw-loader',
// options: {
// name: '[name].[hash:8].[ext]',
// outputPath: 'assets/docs/',
// },
// },
// },
],
},
===========================知识补充
===========================
sass-loader与node-sass版本协调的问题,
切换node版本需要 npm rebuild node-sass
- ► optimization
打包优化项
optimization: {
// 增加模块标识: development默认都为true, production默认为false,
// 选择是否给module和chunk更有意义的名称
namedModules: true,
// 形式有所不同: 为true打包编译之后会是{'./src/b.js': (funciton()) },
// 为false会是[(function())]
namedChunks: true,
// false采用索引[1,2,1], true [1,"runtime~app","b"]
splitChunks: {
// 主要就是根据不同的策略来分割打包出来的`bundle`
chunks: 'async', // 默认
// 同时分割同步和异步代码,
// 拆分模块的范围,它有三个值async、initial和all
// async表示只从异步加载得模块(动态加载import())里面进行拆分,分割异步打包的代码
// initial表示只从入口模块进行拆分,也会同时打包同步和异步,
// 但是异步内部的引入不再考虑,直接打包在一起
// all表示以上两者都包括,同时分割同步和异步代码,推荐。
minSize: 30000, // 最小拆分组件大小
minChunks: 1, // 最小公用模块次数,默认为1
maxAsyncRequests: 5,
// 限制异步模块内部的并行最大请求数的,对应chunks => async
// 当整个项目打包完之后,一个按需加载的包最终被拆分成n个包,maxAsyncRequests就是用来限制n的最大值
maxInitialRequests: 3,
// 允许入口并行加载的最大请求数 ,同上,对应chunks => initial
name: true,
//split 的 chunks name,
// 默认为true,返回${cacheGroup的key} ${automaticNameDelimiter} ${moduleName},可以自定义
cacheGroups: {
// 设置缓存的 chunks 策略
'ui-libs': {
test: chunk => chunk.resource
&& /\.js$/.test(chunk.resource)
&& /node_modules/.test(chunk.resource)
&& /react|mobx|redux|antd|@ant-*|ora-ui/.test(chunk.resource),
chunks: 'initial',
name: 'ui-libs',
priority: 4,
},
'chart-libs': {
test: chunk => chunk.resource
&& /\.js$/.test(chunk.resource)
&& /node_modules/.test(chunk.resource)
&& /echarts/.test(chunk.resource),
chunks: 'initial',
name: 'chart-libs',
priority: 3,
},
vendors: {
test: chunk => chunk.resource
&& /\.js$/.test(chunk.resource)
&& /node_modules/.test(chunk.resource),
chunks: 'initial',
name: 'vendors',
priority: 1,
},
'async-vendors': {
test: /[\\/]node_modules[\\/]/,
minChunks: 2,
chunks: 'async',
name: 'async-vendors',
},
},
},
runtimeChunk: {
name: 'runtime',
/**
* 设置runtimeChunk是将包含chunks 映射关系的 list单独从 app.js里提取出来,
* 因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,
* 如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。
* 设置runtimeChunk之后,webpack就会生成一个个runtime~xxx.js的文件。
* 然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。
* 如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,
* 如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。
* 现在设置了runtimeChunk,就解决了这样的问题。
* 所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。
*/
/**
* 虽然每次构建后app的hash没有改变,但是runtime~xxx.js会变啊。
* 每次重新构建上线后,浏览器每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,
* 所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中。
* 这边我们使用script-ext-html-webpack-plugin来实现。
* (也可使用html-webpack-inline-source-plugin,其不会删除runtime文件。)
*/
// 它的作用是将包含chunks 映射关系的 list单独从 app.js里提取出来,
// 因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,
// 所以你每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。
// 缓存就失效了。
},
},
===========================知识补充
===========================
一些打包实例:
- ► plugins
webpack构建插件
plugins: [
// new CopyPlugin({
// patterns: [
// { from: Root('src/favicon.ico'), to: '.' }
// ]
// }),
// 并非旨在复制从构建过程中生成的文件,而是在构建过程中复制源树中已经存在的文件
/**
* from 定义要拷贝的源文件 from:__dirname+'/src/components'
* to 定义要拷贝到的目标文件夹 to: __dirname+'/dist'
* toType file 或者 dir 可选,默认是文件
* force 强制覆盖前面的插件 可选,默认是文件
* context 可选,默认base context可用specific context
* flatten 只拷贝指定的文件 可以用模糊匹配
* ignore 忽略拷贝指定的文件 可以模糊匹配
*/
// happypack
createHappyPlugin('happy-ts', [
{
loader: 'ts-loader',
options: {
happyPackMode: true,
// 在添加happyPackMode: true后要和fork-ts-checker-webpack-plugin进行配合使用,
// 完善检查机制
transpileOnly: true,
},
},
]),
// 创建id为happy-ts的happyPack实例。
CleanWebpackPlugin(['dist'], {
root: Root(), //一个根的绝对路径
verbose: true,// 将log写到 console.
dry: false,// 不要删除任何东西,主要用于测试.
exclude: []//排除不删除的目录,主要用于避免删除公用的文件
}),
new ForkTsCheckerWebpackPlugin(),
// The minimal webpack config (with ts-loader)
],
===========================知识补充
===========================
cleanWebpackPlugin:
htmlWebpackPlugin:
添加minify
配置,进行html代码压缩
htmlWebpackPlugin.
chunks
: 如果不指定chunks,默认会把所有entry相关的chunks都载入到html中;如果指定了一个entry的chunks,也就是入口,只会把这个入口相关的文件插入html。
getProdBaseConfig
在getBaseConfig的基础之上,再做处理,使用webpack.merge()。
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
import * as ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';
const optimizeCss = require('optimize-css-assets-webpack-plugin');
- ► mode
mode: "production",
- ► module 增加样式压缩
module: {
rules: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
/**
* mini-css-extract-plugin:
* 从css文件中提取css代码到单独的文件中,对css代码进行代码压缩等
*
* 版本兼容坑。
* 在使用mini-css-extract-plugin的0.9.0版本的时候估计是和其他某个插件冲突了,会有这么一个错误
* No module factory available for dependency type: CssDependency
* 可以尝试降级到0.8.2或者0.8.0版本即可解决
*
* 第二个,使用了mini-css-extract-plugin的loader必须配合plugin部分一起使用。
*/
},
{
test: /.less$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
}
],
},
- ► plugins
plugins: [
new MiniCssExtractPlugin({
filename: '/assets/styles/style_[hash:8].css',
}),
new optimizeCss({
cssProcessor: require('cssnano'), //引入cssnano配置压缩选项
cssProcessorOptions: {
discardComments: { removeAll: true }
},
canPrint: true //是否将插件信息打印到控制台
})
/**
* optimize-css-assets-webpack-plugin & cssnano
* 普通压缩:
* plugins: [
* new optimizeCss()
* ]
*
*
* 使用cssnano规则压缩:
* plugins: [
* new optimizeCss({
* cssProcessor: require('cssnano'), //引入cssnano配置压缩选项
* cssProcessorOptions: {
* discardComments: { removeAll: true }
* },
* canPrint: true //是否将插件信息打印到控制台
* })
* ]
*/
],
- ► optimazation
optimization: {
minimizer: [
new ParallelUglifyPlugin({
cacheDir: '.cache/',
// 缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回,
// cacheDir 用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。
uglifyES: {
// 用于压缩 ES6 代码时的配置,Object 类型,直接透传给 UglifyES 的参数。
// uglifyJS:用于压缩 ES5 代码时的配置,Object 类型,直接透传给 UglifyJS 的参数。
output: {
comments: false,
beautify: false,
},
compress: {
drop_console: false,
collapse_vars: true,
reduce_vars: true,
},
},
/**
* test: 使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/.
* include: 使用正则去包含被 ParallelUglifyPlugin 压缩的文件,默认为 [].
* exclude: 使用正则去不包含被 ParallelUglifyPlugin 压缩的文件,默认为 [].
* workerCount:开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1。
* sourceMap:是否为压缩后的代码生成对应的Source Map, 默认不生成,开启后耗时会大大增加,
* 一般不会将压缩后的代码的
*/
/**
* webpack默认提供了UglifyJS插件来压缩JS代码,
* 但是它使用的是单线程压缩代码,
* 也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。
* 所以说在正式环境打包压缩代码速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,
* 再去应用各种规则分析和处理AST,导致这个过程耗时非常大)。
*
* 当webpack有多个JS文件需要输出和压缩时候,原来会使用UglifyJS去一个个压缩并且输出,
* 但是ParallelUglifyPlugin插件则会开启多个子进程,
* 把对多个文件压缩的工作分别给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。
* 无非就是变成了并行处理该压缩了,并行处理多个子任务,效率会更加的提高。
*/
})
]
},
- ► devtool
配置sourceMap
devtool: "cheap-module-source-map",
getDevConfig
开发环境webpack配置
基于getBaseConfig。
import * as HtmlPlugin from 'html-webpack-plugin';
import * as betterProgress from 'better-webpack-progress';
import * as FriendlyErrorsPlugin from 'friendly-errors-webpack-plugin';
基础准备
const interfaces = require('os').networkInterfaces();
for (const devName in interfaces) {
for (const alias of interfaces[devName]) {
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
return alias.address;
}
}
}
return '';
};
const PORT = options?.devPort || 9000;
const IP = getIPAddress();
- ► mode
mode: "development",
- ► resolve
resolve: {
alias: {
"react-dom": "@hot-loader/react-dom"
// 在构建react项目时,默认使用的webpack-dev-serve有热刷新功能,
// 但是局限是修改一处会使整个页面刷新,当引入了react-hot-loader时,可以实现局部刷新,
// 即同个页面上,某一处的数据修改不会让整个页面一起刷新
// 在react16.6+以后,推荐使用兼容性更好的 @hot-loader/react-dom 来代替react-dom
// 因此需要本alias, 但我理解这个更应该在业务项目中做处理
}
},
- ► module
module: {
rules: [
{
test: /.s?css$/,
use: ['css-hot-loader', 'style-loader', 'css-loader', 'sass-loader'],
/**
* style-loader——将处理结束的CSS代码存储在js中,运行时嵌入<style>后挂载至html页面上
* css-loader——加载器,使webpack可以识别css模块
* sass-loader——加载器,使webpack可以识别scss/sass文件,默认使用node-sass进行编译
*
* css-hot-loader 在大多数情况下,我们可以通过style-loader实现CSS热重载。
* 但是样式加载器需要将样式标签注入文档中,在js就绪之前,网页将没有任何样式
* 使用webpack4 时存在问题。请使用mini-css-extract-plugin替换extract-text-webpack-plugin。
*/
},
{
test: /.less$/,
use: ['css-hot-loader', 'style-loader', 'css-loader', 'less-loader'],
},
]
},
- ► devServer
devServer: {
port: PORT,
open: true,
quiet: true,
historyApiFallback: true,
/**
* devServer.historyApiFallback的意思是当路径匹配的文件不存在时不出现404,
* 而是取配置的选项historyApiFallback.index对应的文件
*
* 单页应用(SPA)一般只有一个index.html, 导航的跳转都是基于HTML5 History API,
* 当用户在越过index.html 页面直接访问这个地址或是通过浏览器的刷新按钮重新获取时,
* 就会出现404问题;
* 比如 直接访问/login, /login/online,这时候越过了index.html,去查找这个地址下的文件。
* 由于这是个一个单页应用,最终结果肯定是查找失败,返回一个404错误。
* 这个中间件就是用来解决这个问题的;
* 只要满足下面四个条件之一,这个中间件就会改变请求的地址,指向到默认的index.html:
* 1 GET请求
* 2 接受内容格式为text/html
* 3 不是一个直接的文件请求,比如路径中不带有 .
* 4 没有 options.rewrites 里的正则匹配
*/
inline: true,
hot: true,
/**
* inline选项会为入口页面添加“热加载”功能,即代码改变后重新加载页面。
*
* 当使用--hot参数时,只能使用hash,如果使用chunkhash会报错
* 在使用--inline时,hash和chunkhash都可以使用
*
* webpack的hash字段是根据每次编译compilation的内容计算所得,也可以理解为项目总体文件的hash值,而不是针对每个具体文件的。
* chunkhash是根据模块内容计算出的hash值。
*/
contentBase: Root('dist'),
host: '0.0.0.0',
},
- ► plugins
plugins: [
new webpack.HotModuleReplacementPlugin(),
/**
* webpack官方文档(devserverhot)中介绍,使用hmr的时候,需要满足两个条件:
* 配置devServer.hot为true
* 配置webpack.HotModuleReplacementPlugin插件
*/
new webpack.DefinePlugin({
'process.env.D_ENV': isLocal ? '"local"' : '"dev"',
}),
new HtmlPlugin({
template: Root('src/index.html'),
filename: 'index.html',
env: isLocal ? 'local' : 'dev',
}),
new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [
`This APP is runing at:
- Network: http://${IP}:${PORT}/
- Local: http://localhost:${PORT}/
`,
],
},
}),
new webpack.ProgressPlugin(betterProgress({
mode: 'compact', // or 'detailed' or 'bar'
})),
],
- ► devtool
devtool: 'cheap-module-eval-source-map'
===========================知识补充
===========================
DefinePlugin:
Webpack的DefinePlugin要求我们将所有东西都包装在JSON.stringify中
# 样例
plugins: [
new webpack.DefinePlugin({
DEV: JSON.stringify('development'), // production
flag: 'true',
calc: '1 + 1'
})
]
# 取值
// DefinePlugin定义值
console.log(DEV); // development
console.log(flag); // true
console.log(calc); // 2
// console.log(process);
getTestConfig
基于getProdBaseConfig
export function getTestConfig(options: ConfigOptions, extra: Configuration = {}) {
return merge(
getProdBaseConfig(options),
{
output: {
publicPath: `${options.static}`,
},
plugins: [
new webpack.DefinePlugin({
'process.env.D_ENV': '"test"',
}),
new HtmlPlugin({
template: Root('src/index.html'),
filename: 'index.html',
env: 'test',
})
],
devtool: "cheap-module-source-map"
} as Configuration,
extra
);
}
getProdConfig
const config: Configuration = merge(
getProdBaseConfig(options),
{
output: {
publicPath: `${options.static}`,
},
plugins: [
new webpack.DefinePlugin({
'process.env.D_ENV': '"production"',
}),
new HtmlPlugin({
template: Root('src/index.html'),
filename: 'index.html',
env: 'production',
}),
] as any,
},
extra
);
需要额外处理的部分,是bundleAnalysis
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
if (options.report) {
config.plugins = config.plugins || [];
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'server',
/**
* 可以是`server`,`static`或`disabled`。
* 在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。
* 在“静态”模式下,会生成带有报告的单个HTML文件。
* 在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。
*
*/
analyzerHost: '127.0.0.1', // 将在“服务器”模式下使用的主机启动HTTP服务器。
analyzerPort: 9999,// 将在“服务器”模式下使用的端口启动HTTP服务器。
reportFilename: 'report.html',// 路径捆绑,将在`static`模式下生成的报告文件。 相对于捆绑输出目录。
defaultSizes: 'parsed',// 模块大小默认显示在报告中。应该是`stat`,`parsed`或者`gzip`中的一个。
openAnalyzer: true,// 在默认浏览器中自动打开报告
generateStatsFile: false,// 如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
statsFilename: 'stats.json',
// 如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
// 相对于捆绑输出目录。
statsOptions: null,
/**
* stats.toJson()方法的选项。
* 例如,您可以使用`source:false`选项排除统计文件中模块的来源。
* 在这里查看更多选项:https: //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
*/
logLevel: 'info',// 日志级别。可以是'信息','警告','错误'或'沉默'。
})
);
}
utils
- ► Root()
import * as path from 'path';
export function Root(...paths: string[]) {
return path.join(process.cwd(), ...paths);
// process.cwd() 是当前执行node命令时候的文件夹地址
// 工作目录, 保证了文件在不同的目录下执行时,路径始终不变
// __dirname 是被执行的js 文件的地址
// 文件所在目录, 实际上不是一个全局变量,而是每个模块内部的
};
- ► createHappy()
import * as HappyPack from 'happypack';
import { Rule } from 'webpack';
import * as os from 'os';
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
export function createHappyPlugin(id: string, loaders: Rule[]) {
return new HappyPack({
id,
loaders,
threadPool: happyThreadPool,
});
}
- 在 Loader 配置中,所有文件的处理都交给了 happypack/loader 去处理,使用紧跟其后的 querystring ?id=babel 去告诉 happypack/loader 去选择哪个 HappyPack 实例去处理文件。
- 在 Plugin 配置中,新增了两个 HappyPack 实例分别用于告诉 happypack/loader 去如何处理 .js 和 .css 文件。选项中的 id 属性的值和上面 querystring 中的 ?id=babel 相对应,选项中的 loaders 属性和 Loader 配置中一样。
- id: String 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件.
- loaders: Array 用法和 webpack Loader 配置中一样.
- threads: Number 代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数。
- verbose: Boolean 是否允许 HappyPack 输出日志,默认是 true。
- threadPool: HappyThreadPool 代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。
- verboseWhenProfiling: Boolean 开启webpack --profile ,仍然希望HappyPack产生输出。
- debug: Boolean 启用debug 用于故障排查。默认 false。
所引用的插件及版本:
"dependencies": {
"@babel/preset-typescript": "^7.13.0",
"@hot-loader/react-dom": "^16.13.0",
"@types/html-webpack-plugin": "^3.2.3",
"@types/node": "^14.6.0",
"@types/react-hot-loader": "^4.1.1",
"@types/webpack-bundle-analyzer": "^3.9.1",
"@types/webpack-env": "^1.15.2",
"@types/webpack-merge": "^4.1.5",
"@types/webpack-node-externals": "^2.5.0",
"awesome-typescript-loader": "^5.2.1",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1",
"better-webpack-progress": "^1.1.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.0.3",
"css-hot-loader": "^1.4.4",
"css-loader": "^4.2.1",
"cssnano": "^4.1.10",
"file-loader": "^6.0.0",
"fork-ts-checker-webpack-plugin": "^5.1.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"happypack": "^5.0.1",
"html-webpack-plugin": "^4.3.0",
"less": "^3.13.1",
"less-loader": "^8.0.0",
"mini-css-extract-plugin": "^0.10.0",
"node-sass": "^4.14.1",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"progress-bar-webpack-plugin": "^2.1.0",
"react-hot-loader": "^4.12.21",
"sass-loader": "^9.0.3",
"style-loader": "^1.2.1",
"ts-import-plugin": "^1.6.6",
"ts-loader": "^8.0.2",
"ts-node": "^9.0.0",
"typescript": "^4.0.2",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.8",
"webpack": "^4.44.1",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^5.1.2",
"webpack-node-externals": "^2.5.1",
"webpack-parallel-uglify-plugin": "^1.1.2"
},
"devDependencies": {
"@types/fork-ts-checker-webpack-plugin": "^0.4.5",
"npm-check-updates": "^7.1.1"
}